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
@@ -20,64 +20,87 @@
20
20
  * COVERAGE.
21
21
  */
22
22
 
23
- /** The key in the consumer's `package.json` this package's settings are read from. */
23
+ import { existsSync, readFileSync } from "node:fs";
24
+ import { dirname, join, resolve } from "node:path";
25
+
26
+ /**
27
+ * The tool's name. Its settings file is named after it (`CONFIG_FILE`), its messages start with it,
28
+ * and the hooks resolve their scripts through it. It equals the package name, and
29
+ * `lib/paper-config.harness.mjs` checks that.
30
+ */
24
31
  export const CONFIG_KEY = "paperlint";
25
32
 
26
33
  /**
27
- * The key's name before 2.0.0, when the package was `research-paper-pipeline`. Still READ, with a
28
- * deprecation line from the CLI and `doctor`; `init` renames it. Both keys at once with different
29
- * contents is refused: there is no way to know which one the author means.
34
+ * THE SETTINGS FILE, ONE NAME AT TWO LEVELS, ONE SCHEMA (`SETTINGS_KEYS`):
35
+ *
36
+ * paperlint.json optional, at the project root: where the papers are, the
37
+ * project's rules, and defaults for every paper
38
+ * <papersDir>/<paper>/paperlint.json one paper: its venue preset, its kind, its own rules
39
+ *
40
+ * A paper's file merges over the root's: its `extends`, `kind` and `pdf` win, and its `rules` are
41
+ * applied after the root's. A venue preset (`paperlint:<name>`, or the project's own `./x.jsonc`)
42
+ * is the third level, below both.
43
+ */
44
+ export const CONFIG_FILE = "paperlint.json";
45
+
46
+ /**
47
+ * The files that make a directory a PAPER. A `paperlint.json` beside one of them is that paper's,
48
+ * never the project's — so walking up from inside a paper does not stop at the paper's own file.
30
49
  */
31
- export const LEGACY_CONFIG_KEY = "research-paper-pipeline";
50
+ export const PAPER_MARKERS = Object.freeze([
51
+ "paper.tex",
52
+ "paper.md",
53
+ "draft.md",
54
+ "PIPELINE-STATUS.md",
55
+ ]);
32
56
 
33
57
  /**
34
- * Where a parsed `package.json` keeps this package's settings — the new key, else the old one.
58
+ * The project root: the nearest directory, walking up from `startDir`, that holds a root
59
+ * `paperlint.json` (one not beside a paper's files); else the nearest directory holding a
60
+ * `package.json`; else `startDir` itself. The same walk eslint, prettier and tsc use for theirs.
35
61
  *
36
- * @returns `settings` (undefined when neither key is there, or on a conflict), `legacy` (read
37
- * from the old key), `conflict` (the message when both keys are there and differ, else null).
62
+ * @param isFile whether a path exists — `existsSync` by default; `src/` passes its Files port.
38
63
  */
39
- export function declaredSettings(pkg) {
40
- const has = (k) =>
41
- pkg !== null && typeof pkg === "object" && Object.hasOwn(pkg, k);
42
- const current = has(CONFIG_KEY);
43
- const legacy = has(LEGACY_CONFIG_KEY);
44
- if (
45
- current &&
46
- legacy &&
47
- JSON.stringify(pkg[CONFIG_KEY]) !== JSON.stringify(pkg[LEGACY_CONFIG_KEY])
48
- )
49
- return {
50
- settings: undefined,
51
- legacy: false,
52
- conflict:
53
- `package.json has both "${CONFIG_KEY}" and "${LEGACY_CONFIG_KEY}", and they differ. ` +
54
- `Keep "${CONFIG_KEY}" and delete "${LEGACY_CONFIG_KEY}" (its old name).`,
55
- };
56
- if (current)
57
- return { settings: pkg[CONFIG_KEY], legacy: false, conflict: null };
58
- if (legacy)
59
- return { settings: pkg[LEGACY_CONFIG_KEY], legacy: true, conflict: null };
60
- return { settings: undefined, legacy: false, conflict: null };
64
+ export function findProjectRoot(startDir, isFile = existsSync) {
65
+ const start = resolve(startDir);
66
+ const isPaper = (d) => PAPER_MARKERS.some((m) => isFile(join(d, m)));
67
+ const walk = (found) => {
68
+ for (let d = start; ; d = dirname(d)) {
69
+ if (found(d)) return d;
70
+ if (dirname(d) === d) return null;
71
+ }
72
+ };
73
+ return (
74
+ walk((d) => isFile(join(d, CONFIG_FILE)) && !isPaper(d)) ??
75
+ walk((d) => isFile(join(d, "package.json"))) ??
76
+ start
77
+ );
61
78
  }
62
79
 
80
+ const isObject = (v) =>
81
+ v !== null && typeof v === "object" && !Array.isArray(v);
82
+
63
83
  /**
64
- * The settings object alone, for readers that only need a value: the new key, else the old one,
65
- * and nothing on a conflict (the CLI, `doctor` and the edit guard report the conflict).
84
+ * The project's settings: the object in `<root>/paperlint.json`, or undefined when there is no
85
+ * such file, or it does not parse, or it is not an object. For readers that only need one key and
86
+ * fall back to its default; `paperlint lint` parses the file strictly and names what is wrong.
66
87
  */
67
- export function settingsOf(pkg) {
68
- const d = declaredSettings(pkg);
69
- return d.conflict === null ? d.settings : undefined;
88
+ export function settingsOf(root) {
89
+ let v;
90
+ try {
91
+ v = JSON.parse(readFileSync(join(root, CONFIG_FILE), "utf8"));
92
+ } catch {
93
+ return undefined;
94
+ }
95
+ return isObject(v) ? v : undefined;
70
96
  }
71
97
 
72
- /** The deprecation line for settings read from the old key. */
73
- export const LEGACY_KEY_MESSAGE = `"${LEGACY_CONFIG_KEY}" in package.json is the old name of this package's key — rename it to "${CONFIG_KEY}". \`npx paperlint init\` does it for you.`;
74
-
75
- /** Default: a consumer that declares nothing is assumed to keep papers in `papers/`. */
98
+ /** The default papers directory, used when the root `paperlint.json` declares none (or there is none). */
76
99
  export const DEFAULT_PAPERS_ROOT = "papers";
77
100
 
78
101
  /**
79
- * The field under CONFIG_KEY that names the papers directory:
80
- * `"paperlint": { "papersDir": "papers" }`.
102
+ * The field that names the papers directory: `{ "papersDir": "papers" }` in the root
103
+ * `paperlint.json`. Optional; absent means `DEFAULT_PAPERS_ROOT`.
81
104
  *
82
105
  * This is the one place the name is spelled. Code reads the field as
83
106
  * `settings[PAPERS_DIR_FIELD]`, never by a literal, so renaming it again is a one-line change
@@ -87,51 +110,24 @@ export const DEFAULT_PAPERS_ROOT = "papers";
87
110
  export const PAPERS_DIR_FIELD = "papersDir";
88
111
 
89
112
  /**
90
- * The field's old name. A config that still has it is REFUSED with `renamedFieldMessage()`,
91
- * even if it also has the new one: the package is unpublished, so there is nobody to migrate
92
- * quietly, and reading the old name as a fallback would leave it working forever.
93
- */
94
- export const OLD_PAPERS_DIR_FIELD = "papers";
95
-
96
- /**
97
- * The error text for a config that still uses the old field name, or `null` when it does not.
113
+ * Every key a `paperlint.json` may hold — the root's and a paper's, ONE schema — with who reads it.
114
+ * A key not listed here is REFUSED with its name, because a typo in a key otherwise reads as "not
115
+ * set". The keys in `ROOT_ONLY_KEYS` describe the whole project and are refused in a paper's file.
98
116
  *
99
- * @param settings the object under CONFIG_KEY
100
- * @param where where that object lives, as the reader should see it in the message
101
- */
102
- export function renamedFieldMessage(
103
- settings,
104
- where = `package.json → "${CONFIG_KEY}"`,
105
- ) {
106
- if (settings === null || typeof settings !== "object") return null;
107
- if (!Object.hasOwn(settings, OLD_PAPERS_DIR_FIELD)) return null;
108
- return `"${OLD_PAPERS_DIR_FIELD}" was renamed to "${PAPERS_DIR_FIELD}" in ${where}`;
109
- }
110
-
111
- /**
112
- * Every key the settings object may hold, with who reads it. A key not listed here is REFUSED
113
- * with its name (`paperlint lint`, `paperlint build`, `paperlint new`, `paperlint doctor` all read through the same
114
- * parser), because a typo in a key otherwise reads as "not set" — the defect `src/types.ts` names
115
- * in its first line.
116
- *
117
- * 🔴 ONE LIST, AND IT IS CHECKED AGAINST THE READERS, NOT TRUSTED. The settings object has readers
118
- * all over the package (the CLI, the skill scripts, the trigger-case loader), each reading its own
119
- * keys. `lib/paper-config.harness.mjs` walks the package's source, finds every
120
- * `[CONFIG_KEY]?.<key>` read and every field of `PaperlintConfig`, and requires each to be here — so a
121
- * new reader that forgets this list turns the harness red before a consumer's config does.
117
+ * 🔴 ONE LIST, AND IT IS CHECKED AGAINST THE READERS, NOT TRUSTED. `lib/paper-config.harness.mjs`
118
+ * walks the package's source, finds every `settingsOf(…)?.<key>` read and every field of
119
+ * `PaperlintConfig`, and requires each to be here — so a new reader that forgets this list turns
120
+ * the harness red before a consumer's config does.
122
121
  */
123
122
  export const SETTINGS_KEYS = Object.freeze({
124
- papersDir: "the papers directory (required) — every command, the hooks",
123
+ papersDir: `the papers directory, default "papers" — every command, the hooks`,
125
124
  structure: "which files a paper directory must hold — paperlint lint",
126
- authorListCommand: "paper/author-list — paperlint lint",
127
- typographyDebt: "paper/typography — paperlint lint",
128
- docFields: "doc/fields — paperlint lint",
129
- reviewSince: "review/findings-cause — paperlint lint",
130
- minFindings: "review/findings-cause — paperlint lint",
131
- causeMarker: "review/findings-cause — paperlint lint",
132
125
  rules:
133
- "extra ESLint config blocks, appended after paperlint's own — paperlint lint",
134
- buildScripts: "REMOVED; still named so paperlint build can say it is ignored",
126
+ "rule overrides: { id: severity } for every paper in scope, or ESLint blocks (files, ignores, rules) — paperlint lint",
127
+ extends:
128
+ "the venue preset: paperlint:<name> or ./path.jsonc — the pdf/ venue rules, paperlint build",
129
+ kind: "the kind of paper, whose page limit applies — pdf/limits",
130
+ pdf: "where the built PDF is, relative to the paper, when it is not paper.pdf — the facts",
135
131
  ledger: "the run ledger — skills/paper-pipeline/scripts/consumer.mjs",
136
132
  scripts: "how skill prose names the scripts — consumer.mjs",
137
133
  timezone: "dates in the ledger — consumer.mjs",
@@ -139,4 +135,17 @@ export const SETTINGS_KEYS = Object.freeze({
139
135
  citeChecks: "the consumer's citation checkers — run-mechanical.mjs",
140
136
  triggerCases:
141
137
  "the consumer's own skill trigger cases — lib/skill-trigger-cases.mjs",
138
+ $comment: "a note for humans (JSON Schema's own comment keyword); ignored",
142
139
  });
140
+
141
+ /** The keys that only mean something for the whole project: refused in a paper's `paperlint.json`. */
142
+ export const ROOT_ONLY_KEYS = Object.freeze([
143
+ "papersDir",
144
+ "structure",
145
+ "ledger",
146
+ "scripts",
147
+ "timezone",
148
+ "contactEmail",
149
+ "citeChecks",
150
+ "triggerCases",
151
+ ]);
@@ -117,8 +117,8 @@ process.exit(
117
117
  edits: [
118
118
  [
119
119
  DOCTOR,
120
- 'import {\n papersRoot,\n CONFIG_KEY,\n DEFAULT_PAPERS_ROOT,\n PAPERS_DIR_FIELD,\n OLD_PAPERS_DIR_FIELD,\n} from "../hooks/paper-edit-guard.hook.mjs";',
121
- 'import {\n papersRoot,\n DEFAULT_PAPERS_ROOT,\n PAPERS_DIR_FIELD,\n OLD_PAPERS_DIR_FIELD,\n} from "../hooks/paper-edit-guard.hook.mjs";\nconst CONFIG_KEY = "research-paper-pipeline";',
120
+ 'import {\n papersRoot,\n DEFAULT_PAPERS_ROOT,\n PAPERS_DIR_FIELD,\n CONFIG_FILE,\n} from "../hooks/paper-edit-guard.hook.mjs";',
121
+ 'import {\n papersRoot,\n DEFAULT_PAPERS_ROOT,\n PAPERS_DIR_FIELD,\n} from "../hooks/paper-edit-guard.hook.mjs";\nconst CONFIG_FILE = "paperlint.json";',
122
122
  ],
123
123
  ],
124
124
  },
@@ -70,7 +70,7 @@ import {
70
70
  //
71
71
  // The value now comes from the package, as the fourth carrier beside `papersRoot()` (where the
72
72
  // papers are), `ledgerPath()` (where the journal is) and `citeChecks` (where the consumer's own
73
- // citation checkers are). One declaration in `package.json`, default
73
+ // citation checkers are). One declaration in `paperlint.json`, default
74
74
  // `.claude/skills/paper-pipeline/scripts`, and the resolver REFUSES a root that is not on disk
75
75
  // or that resolves inside `node_modules`.
76
76
  //
@@ -53,9 +53,12 @@ function loadAt(root) {
53
53
  }).trim();
54
54
  }
55
55
 
56
- function fixture(pkgJson, casesFile) {
56
+ /** A consumer: a package.json, the root paperlint.json `settings` (none when null), a cases file. */
57
+ function fixture(settings, casesFile) {
57
58
  const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-cases-")));
58
- writeFileSync(join(root, "package.json"), JSON.stringify(pkgJson));
59
+ writeFileSync(join(root, "package.json"), JSON.stringify({ name: "c" }));
60
+ if (settings)
61
+ writeFileSync(join(root, "paperlint.json"), JSON.stringify(settings));
59
62
  if (casesFile) {
60
63
  mkdirSync(join(root, ".claude", "lib"), { recursive: true });
61
64
  writeFileSync(
@@ -114,7 +117,7 @@ soft("I. irrelevantFor still yields a full set for a one-collider case", () => {
114
117
  soft(
115
118
  "II. undeclared and absent is SILENT — a consumer with no skills of its own",
116
119
  () => {
117
- const out = loadAt(fixture({ name: "plain-consumer" }, null));
120
+ const out = loadAt(fixture(null, null));
118
121
  assert.match(out, /^OK \d+$/, `expected a clean load, got: ${out}`);
119
122
  assert.equal(
120
123
  Number(out.slice(3)),
@@ -125,7 +128,7 @@ soft(
125
128
  );
126
129
 
127
130
  soft("II. the DEFAULT path is picked up without any declaration", () => {
128
- const out = loadAt(fixture({ name: "defaulting-consumer" }, LOCAL_ONE));
131
+ const out = loadAt(fixture(null, LOCAL_ONE));
129
132
  assert.equal(
130
133
  out,
131
134
  `OK ${CASES.length + 1}`,
@@ -137,9 +140,7 @@ soft("II. the DEFAULT path is picked up without any declaration", () => {
137
140
  // is the same one `scriptsRoot()` draws: a path someone typed is a keystroke away from being wrong,
138
141
  // and substituting silence for it hides the typo behind a working run.
139
142
  soft("II. declared and missing THROWS, naming the declaration", () => {
140
- const out = loadAt(
141
- fixture({ paperlint: { triggerCases: "nope.mjs" } }, null),
142
- );
143
+ const out = loadAt(fixture({ triggerCases: "nope.mjs" }, null));
143
144
  assert.match(out, /^THROW /, `a typoed declaration loaded quietly: ${out}`);
144
145
  assert.match(
145
146
  out,
@@ -150,7 +151,7 @@ soft("II. declared and missing THROWS, naming the declaration", () => {
150
151
 
151
152
  soft("II. a consumer may not redefine a case this package owns", () => {
152
153
  const clash = `export const CASES = [{ skill: "${CASES[0].skill}", why: "x", prompts: ["a","b","c","d"], collides: [] }];\n`;
153
- const out = loadAt(fixture({ name: "clashing-consumer" }, clash));
154
+ const out = loadAt(fixture(null, clash));
154
155
  assert.match(out, /^THROW /, `a shadowing case was accepted: ${out}`);
155
156
  assert.match(
156
157
  out,
@@ -334,8 +334,8 @@ const PACKAGE_CASES = [
334
334
  // out of the code and into a declaration. This is the fifth of those carriers and reads the same
335
335
  // key from the same file.
336
336
  //
337
- // // <consumer>/package.json
338
- // "paperlint": { "triggerCases": ".claude/lib/skill-trigger-cases.local.mjs" }
337
+ // // <consumer>/paperlint.json
338
+ // { "triggerCases": ".claude/lib/skill-trigger-cases.local.mjs" }
339
339
  //
340
340
  // The module it names must export `CASES` in the shape documented at the top of this file.
341
341
  //
@@ -347,8 +347,7 @@ const PACKAGE_CASES = [
347
347
  // ⚠️ A CONSUMER MAY NOT REDEFINE A PACKAGE CASE. Two tables answering "what should fire on this
348
348
  // prompt" is two truths; the override would live in the consumer and the original would keep
349
349
  // looking authoritative here. Throws, naming the skill.
350
- import { readFileSync } from "node:fs";
351
- import { join, resolve } from "node:path";
350
+ import { resolve } from "node:path";
352
351
  import { pathToFileURL } from "node:url";
353
352
  import {
354
353
  CONFIG_KEY,
@@ -360,14 +359,7 @@ const DEFAULT_LOCAL_CASES = ".claude/lib/skill-trigger-cases.local.mjs";
360
359
 
361
360
  async function consumerCases() {
362
361
  const root = consumerRoot();
363
- let declared;
364
- try {
365
- declared = settingsOf(
366
- JSON.parse(readFileSync(join(root, "package.json"), "utf8")),
367
- )?.triggerCases;
368
- } catch {
369
- declared = undefined;
370
- }
362
+ const declared = settingsOf(root)?.triggerCases;
371
363
  const rel = declared === undefined ? DEFAULT_LOCAL_CASES : declared;
372
364
  if (typeof rel !== "string" || rel.length === 0)
373
365
  throw new TypeError(
@@ -382,9 +374,9 @@ async function consumerCases() {
382
374
  throw new Error(
383
375
  `${CONFIG_KEY}: could not load the consumer's trigger cases from ${abs}` +
384
376
  (declared === undefined
385
- ? ` (the default location; declare another in package.json under ` +
386
- `"${CONFIG_KEY}": { "triggerCases": … } if the file lives elsewhere).`
387
- : ` — it is declared in package.json as "triggerCases": ${JSON.stringify(declared)}.`) +
377
+ ? ` (the default location; declare another in paperlint.json as ` +
378
+ `{ "triggerCases": … } if the file lives elsewhere).`
379
+ : ` — it is declared in paperlint.json as "triggerCases": ${JSON.stringify(declared)}.`) +
388
380
  `
389
381
  A missing case set does not shrink the measurement, it CHANGES it: ` +
390
382
  `\`irrelevantFor()\` builds the false-positive set out of a case's colliders, so an ` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperlint",
3
- "version": "2.0.1",
3
+ "version": "3.0.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",
@@ -26,7 +26,7 @@ import { readFileSync } from "node:fs";
26
26
  export function guard(
27
27
  reportPath,
28
28
  eslintRc,
29
- { paths = ".", config = "eslint.config.mjs" } = {},
29
+ { paths = ".", config = "paperlint.json" } = {},
30
30
  ) {
31
31
  const lines = [];
32
32
  let res;
@@ -3,15 +3,12 @@
3
3
  "why": "A *.harness.* file tests the agent surface and imports runHook, runHarnessTest or runEval from vigiles. Plain unit tests are *.test.ts, run by vitest. These are the harness files that existed on main on 2026-09-25 and import none of the three. The list only shrinks: rename a file to *.test.ts (issue #77), or make it a real harness, then delete its entry. Checked by scripts/harness-api.test.ts, part of npm test.",
4
4
  "harnesses": [
5
5
  "action.harness.mjs",
6
- "eslint-rules/doc-fields.harness.mjs",
7
6
  "eslint-rules/install-path-literals.harness.mjs",
8
7
  "eslint-rules/latex-language.harness.mjs",
9
8
  "eslint-rules/paper-research-question.harness.mjs",
10
9
  "eslint-rules/paper-stages.harness.mjs",
11
- "eslint-rules/paper-typography.harness.mjs",
12
10
  "eslint-rules/papers.harness.mjs",
13
11
  "eslint-rules/pdf-last-page-balance.harness.mjs",
14
- "eslint-rules/review-findings-cause.harness.mjs",
15
12
  "eslint-rules/temp-root-realpath.harness.mjs",
16
13
  "eslint-rules/tex-build.harness.mjs",
17
14
  "fixtures/real-markdown-paper/real-markdown-paper.harness.mjs",
@@ -5,9 +5,6 @@
5
5
  "action.mutations.mjs": {
6
6
  "cases": 6
7
7
  },
8
- "eslint-rules/doc-fields.mutations.mjs": {
9
- "cases": 6
10
- },
11
8
  "eslint-rules/install-path-literals.mutations.mjs": {
12
9
  "cases": 3
13
10
  },
@@ -18,17 +15,11 @@
18
15
  "cases": 6
19
16
  },
20
17
  "eslint-rules/paper-stages.mutations.mjs": {
21
- "cases": 9
22
- },
23
- "eslint-rules/paper-typography.mutations.mjs": {
24
- "cases": 6
18
+ "cases": 4
25
19
  },
26
20
  "eslint-rules/papers.mutations.mjs": {
27
21
  "cases": 10
28
22
  },
29
- "eslint-rules/review-findings-cause.mutations.mjs": {
30
- "cases": 4
31
- },
32
23
  "eslint-rules/temp-root-realpath.mutations.mjs": {
33
24
  "cases": 5
34
25
  },
@@ -36,7 +27,7 @@
36
27
  "cases": 21
37
28
  },
38
29
  "fixtures/real-markdown-paper/real-markdown-paper.mutations.mjs": {
39
- "cases": 4
30
+ "cases": 3
40
31
  },
41
32
  "hooks/hooks.mutations.mjs": {
42
33
  "cases": 23
@@ -96,13 +87,13 @@
96
87
  "cases": 11
97
88
  },
98
89
  "src/cli.mutations.mjs": {
99
- "cases": 26
90
+ "cases": 25
100
91
  },
101
92
  "src/doctor.mutations.mjs": {
102
93
  "cases": 10
103
94
  },
104
95
  "src/hooks-settings.mutations.mjs": {
105
- "cases": 7
96
+ "cases": 6
106
97
  },
107
98
  "src/link-skills.mutations.mjs": {
108
99
  "cases": 6
@@ -5,7 +5,7 @@ context: fork
5
5
  allowed-tools: [WebSearch, WebFetch, Read, Write, Grep, Glob, Bash, Agent]
6
6
  ---
7
7
 
8
- <!-- vigiles:sha256:19d00fe70002d3fa compiled from skills/analyze-sibling-paper/SKILL.md.spec.ts -->
8
+ <!-- vigiles:sha256:f528cbe9e892d6d0 compiled from skills/analyze-sibling-paper/SKILL.md.spec.ts -->
9
9
 
10
10
  # analyze-sibling-paper — settle "isn't this just X?" before a reviewer does
11
11
 
@@ -44,9 +44,9 @@ far below the cost of a "novelty concern" reject.
44
44
  abstract → intro → method → claims (as much as is accessible). An abstract hides exactly the overlap
45
45
  that matters. Try the fetch routes in order — `arxiv.org/abs/<id>` for metadata, `arxiv.org/html/<id>v1`,
46
46
  `ar5iv.labs.arxiv.org/html/<id>`, then the PDF — and **a 403/404 on one route is not "inaccessible"**;
47
- switch channel before concluding. **Record what you actually read** in the card as a
48
- `**Read:**` line naming the sections you covered and the ones you could not — an abstract-only
49
- verdict is PROVISIONAL and must say so in that same line.
47
+ switch channel before concluding. **Record what you actually read** in the card's frontmatter as
48
+ `read: full | abstract | none`, and name in a `**Read:**` line the sections you covered and the
49
+ ones you could not — an abstract-only verdict is PROVISIONAL and must say so.
50
50
  🔴 **Verify the abstract's numbers against the body.** They disagree more often than you would expect,
51
51
  and a disagreement is itself a finding worth recording.
52
52
 
@@ -87,17 +87,21 @@ far below the cost of a "novelty concern" reject.
87
87
 
88
88
  ## Required shape of the saved card — two sections are machine-checked
89
89
 
90
- The card is not free-form. Two of its parts are checked by `.claude/hooks/paper-lint.mjs`
91
- (`checkSiblingCards`) and a card missing either is reported as an unfinished analysis:
90
+ The card is not free-form:
92
91
 
93
- - **`**Read:**`** — one line naming what of the full text you actually read and what you could
94
- not. This exists because "I read the paper" is the single easiest thing to skip while producing a
95
- card that looks complete.
92
+ - **`read:` in the frontmatter** — `full`, `abstract` or `none`: how much of the full text you
93
+ actually read. `paperlint lint` checks it (`sibling/frontmatter`, a warning on a card without it).
94
+ It exists because "I read the paper" is the single easiest thing to skip while producing a card that
95
+ looks complete. The `**Read:**` line under it names which sections.
96
96
  - **`## Sibling's References`** — their bibliography mined and diffed against yours (step 2), with the
97
97
  MUST CITE / SHOULD CONSIDER / NOT RELEVANT verdicts.
98
98
 
99
- Enforced only for cards dated on or after 2026-07-29; older cards get one summary nudge instead of
100
- burying every other finding. Frontmatter needs `title`, `created`, `tags` like any note.
99
+ ```yaml
100
+ ---
101
+ title: "Smith et al. 2025 — <title>"
102
+ read: abstract # full | abstract | none
103
+ ---
104
+ ```
101
105
 
102
106
  ## Save + surface — COLOCATED with the paper (mandatory — do not leave it in context)
103
107
  Each paper's competitive landscape lives **with that paper**, not in a shared research folder: write the
@@ -136,7 +140,7 @@ would have overwritten one another.
136
140
 
137
141
  ## Rules
138
142
  - **Read the paper, not the abstract.** An abstract-only "delta" is not a verdict — and the card must
139
- say, in its `**Read:**` line, which parts you actually read.
143
+ say so: `read: abstract` in its frontmatter, and which parts in its `**Read:**` line.
140
144
  - **Mine their references.** A sibling analysis that never opened the sibling's bibliography is half done;
141
145
  the scoop you missed is more likely to be in their reference list than in their results.
142
146
  - **Classify by contribution type** before judging overlap — it's the difference between a shared concept and a collision.
@@ -59,9 +59,9 @@ far below the cost of a "novelty concern" reject.
59
59
  abstract → intro → method → claims (as much as is accessible). An abstract hides exactly the overlap
60
60
  that matters. Try the fetch routes in order — \`arxiv.org/abs/<id>\` for metadata, \`arxiv.org/html/<id>v1\`,
61
61
  \`ar5iv.labs.arxiv.org/html/<id>\`, then the PDF — and **a 403/404 on one route is not "inaccessible"**;
62
- switch channel before concluding. **Record what you actually read** in the card as a
63
- \`**Read:**\` line naming the sections you covered and the ones you could not — an abstract-only
64
- verdict is PROVISIONAL and must say so in that same line.
62
+ switch channel before concluding. **Record what you actually read** in the card's frontmatter as
63
+ \`read: full | abstract | none\`, and name in a \`**Read:**\` line the sections you covered and the
64
+ ones you could not — an abstract-only verdict is PROVISIONAL and must say so.
65
65
  🔴 **Verify the abstract's numbers against the body.** They disagree more often than you would expect,
66
66
  and a disagreement is itself a finding worth recording.
67
67
 
@@ -102,17 +102,21 @@ far below the cost of a "novelty concern" reject.
102
102
 
103
103
  ## Required shape of the saved card — two sections are machine-checked
104
104
 
105
- The card is not free-form. Two of its parts are checked by \`.claude/hooks/paper-lint.mjs\`
106
- (\`checkSiblingCards\`) and a card missing either is reported as an unfinished analysis:
105
+ The card is not free-form:
107
106
 
108
- - **\`**Read:**\`** — one line naming what of the full text you actually read and what you could
109
- not. This exists because "I read the paper" is the single easiest thing to skip while producing a
110
- card that looks complete.
107
+ - **\`read:\` in the frontmatter** — \`full\`, \`abstract\` or \`none\`: how much of the full text you
108
+ actually read. \`paperlint lint\` checks it (\`sibling/frontmatter\`, a warning on a card without it).
109
+ It exists because "I read the paper" is the single easiest thing to skip while producing a card that
110
+ looks complete. The \`**Read:**\` line under it names which sections.
111
111
  - **\`## Sibling's References\`** — their bibliography mined and diffed against yours (step 2), with the
112
112
  MUST CITE / SHOULD CONSIDER / NOT RELEVANT verdicts.
113
113
 
114
- Enforced only for cards dated on or after 2026-07-29; older cards get one summary nudge instead of
115
- burying every other finding. Frontmatter needs \`title\`, \`created\`, \`tags\` like any note.
114
+ \`\`\`yaml
115
+ ---
116
+ title: "Smith et al. 2025 — <title>"
117
+ read: abstract # full | abstract | none
118
+ ---
119
+ \`\`\`
116
120
 
117
121
  ## Save + surface — COLOCATED with the paper (mandatory — do not leave it in context)
118
122
  Each paper's competitive landscape lives **with that paper**, not in a shared research folder: write the
@@ -151,7 +155,7 @@ would have overwritten one another.
151
155
 
152
156
  ## Rules
153
157
  - **Read the paper, not the abstract.** An abstract-only "delta" is not a verdict — and the card must
154
- say, in its \`**Read:**\` line, which parts you actually read.
158
+ say so: \`read: abstract\` in its frontmatter, and which parts in its \`**Read:**\` line.
155
159
  - **Mine their references.** A sibling analysis that never opened the sibling's bibliography is half done;
156
160
  the scoop you missed is more likely to be in their reference list than in their results.
157
161
  - **Classify by contribution type** before judging overlap — it's the difference between a shared concept and a collision.
@@ -5,7 +5,7 @@ context: fork
5
5
  allowed-tools: [Bash, Read, Agent]
6
6
  ---
7
7
 
8
- <!-- vigiles:sha256:f3c36e3e56bbd3cd compiled from skills/cold-read-diff/SKILL.md.spec.ts -->
8
+ <!-- vigiles:sha256:596fbd7e2eb85252 compiled from skills/cold-read-diff/SKILL.md.spec.ts -->
9
9
 
10
10
  # cold-read-diff — the reader who cannot fake understanding
11
11
 
@@ -101,6 +101,25 @@ Two more things the same run proved:
101
101
  comments, and hand the reader continuous prose. The findings that survive that are worth the run; the
102
102
  ones that do not are noise, and noise is how this check gets muted.
103
103
 
104
+ ## The report's frontmatter — the findings, as records
105
+
106
+ The report opens with YAML frontmatter that lists what it found, one record per finding.
107
+ `paperlint lint` validates it (`review/frontmatter`): an **open** finding must name the
108
+ pipeline `cause` that let it through — fix that, and the text edit falls out of running the
109
+ fixed tool.
110
+
111
+ ```yaml
112
+ ---
113
+ findings:
114
+ - id: 1
115
+ status: open # open | fixed | wontfix
116
+ cause: missing-skill # skill-defect | missing-skill | hook | rule — required when open
117
+ title: "§3 opens on a term the reader has not met"
118
+ - id: 2
119
+ status: fixed
120
+ ---
121
+ ```
122
+
104
123
  ## Record the verdict
105
124
 
106
125
  🔴 LAST step, once the deliverable exists:
@@ -106,6 +106,25 @@ Two more things the same run proved:
106
106
  comments, and hand the reader continuous prose. The findings that survive that are worth the run; the
107
107
  ones that do not are noise, and noise is how this check gets muted.
108
108
 
109
+ ## The report's frontmatter — the findings, as records
110
+
111
+ The report opens with YAML frontmatter that lists what it found, one record per finding.
112
+ \`paperlint lint\` validates it (\`review/frontmatter\`): an **open** finding must name the
113
+ pipeline \`cause\` that let it through — fix that, and the text edit falls out of running the
114
+ fixed tool.
115
+
116
+ \`\`\`yaml
117
+ ---
118
+ findings:
119
+ - id: 1
120
+ status: open # open | fixed | wontfix
121
+ cause: missing-skill # skill-defect | missing-skill | hook | rule — required when open
122
+ title: "§3 opens on a term the reader has not met"
123
+ - id: 2
124
+ status: fixed
125
+ ---
126
+ \`\`\`
127
+
109
128
  ## Record the verdict
110
129
 
111
130
  🔴 LAST step, once the deliverable exists:
@@ -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.