paperlint 7.1.0 โ†’ 8.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 (261) hide show
  1. package/.github/workflows/ci.yml +3 -0
  2. package/.github/workflows/deadlines.yml +100 -0
  3. package/README.md +6 -0
  4. package/dist/adapters/hotcrp/deadlines.d.ts +8 -0
  5. package/dist/adapters/hotcrp/deadlines.d.ts.map +1 -0
  6. package/dist/adapters/hotcrp/deadlines.io.d.ts +6 -0
  7. package/dist/adapters/hotcrp/deadlines.io.d.ts.map +1 -0
  8. package/dist/adapters/hotcrp/deadlines.io.js +26 -0
  9. package/dist/adapters/hotcrp/deadlines.io.js.map +1 -0
  10. package/dist/adapters/hotcrp/deadlines.js +134 -0
  11. package/dist/adapters/hotcrp/deadlines.js.map +1 -0
  12. package/dist/adapters/hotcrp/index.d.ts +2 -0
  13. package/dist/adapters/hotcrp/index.d.ts.map +1 -1
  14. package/dist/adapters/hotcrp/index.js +2 -0
  15. package/dist/adapters/hotcrp/index.js.map +1 -1
  16. package/dist/build.d.ts.map +1 -1
  17. package/dist/build.js +1 -6
  18. package/dist/build.js.map +1 -1
  19. package/dist/cli.d.ts +3 -4
  20. package/dist/cli.d.ts.map +1 -1
  21. package/dist/cli.js +53 -47
  22. package/dist/cli.js.map +1 -1
  23. package/dist/cycle-rules.d.ts +24 -0
  24. package/dist/cycle-rules.d.ts.map +1 -0
  25. package/dist/cycle-rules.js +212 -0
  26. package/dist/cycle-rules.js.map +1 -0
  27. package/dist/domain/cycle.d.ts +148 -0
  28. package/dist/domain/cycle.d.ts.map +1 -0
  29. package/dist/domain/cycle.js +281 -0
  30. package/dist/domain/cycle.js.map +1 -0
  31. package/dist/domain/deadline.d.ts +128 -0
  32. package/dist/domain/deadline.d.ts.map +1 -0
  33. package/dist/domain/deadline.js +281 -0
  34. package/dist/domain/deadline.js.map +1 -0
  35. package/dist/eslint-rules/cycle-stage.d.ts +39 -0
  36. package/dist/eslint-rules/cycle-stage.d.ts.map +1 -0
  37. package/dist/eslint-rules/cycle-stage.js +119 -0
  38. package/dist/eslint-rules/cycle-stage.js.map +1 -0
  39. package/dist/eslint-rules/latex-language.d.ts.map +1 -1
  40. package/dist/eslint-rules/latex-language.js +10 -13
  41. package/dist/eslint-rules/latex-language.js.map +1 -1
  42. package/dist/eslint-rules/paper-context.js +2 -2
  43. package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
  44. package/dist/eslint-rules/paper-typography.js +22 -91
  45. package/dist/eslint-rules/paper-typography.js.map +1 -1
  46. package/dist/eslint-rules/papers.d.ts +1 -3
  47. package/dist/eslint-rules/papers.d.ts.map +1 -1
  48. package/dist/eslint-rules/papers.js +2 -4
  49. package/dist/eslint-rules/papers.js.map +1 -1
  50. package/dist/eslint-rules/tex-build.d.ts.map +1 -1
  51. package/dist/eslint-rules/tex-build.js +8 -25
  52. package/dist/eslint-rules/tex-build.js.map +1 -1
  53. package/dist/init.d.ts +1 -4
  54. package/dist/init.d.ts.map +1 -1
  55. package/dist/init.js +3 -9
  56. package/dist/init.js.map +1 -1
  57. package/dist/lib/markdown.d.ts +5 -81
  58. package/dist/lib/markdown.d.ts.map +1 -1
  59. package/dist/lib/markdown.js +5 -116
  60. package/dist/lib/markdown.js.map +1 -1
  61. package/dist/lib/paper-config.d.ts.map +1 -1
  62. package/dist/lib/paper-config.js +2 -2
  63. package/dist/lib/paper-config.js.map +1 -1
  64. package/dist/lib/skill-eval-fixture.d.ts +6 -4
  65. package/dist/lib/skill-eval-fixture.d.ts.map +1 -1
  66. package/dist/lib/skill-eval-fixture.js +25 -18
  67. package/dist/lib/skill-eval-fixture.js.map +1 -1
  68. package/dist/new-paper.d.ts +5 -11
  69. package/dist/new-paper.d.ts.map +1 -1
  70. package/dist/new-paper.js +12 -23
  71. package/dist/new-paper.js.map +1 -1
  72. package/dist/paper-files.d.ts +4 -4
  73. package/dist/paper-prose.d.ts +17 -0
  74. package/dist/paper-prose.d.ts.map +1 -0
  75. package/dist/paper-prose.js +46 -0
  76. package/dist/paper-prose.js.map +1 -0
  77. package/dist/paper-settings.d.ts +7 -0
  78. package/dist/paper-settings.d.ts.map +1 -1
  79. package/dist/paper-settings.js +61 -8
  80. package/dist/paper-settings.js.map +1 -1
  81. package/dist/ports/venue-deadlines.d.ts +14 -0
  82. package/dist/ports/venue-deadlines.d.ts.map +1 -0
  83. package/dist/ports/venue-deadlines.js +2 -0
  84. package/dist/ports/venue-deadlines.js.map +1 -0
  85. package/dist/presets.d.ts +12 -0
  86. package/dist/presets.d.ts.map +1 -1
  87. package/dist/presets.js +19 -11
  88. package/dist/presets.js.map +1 -1
  89. package/dist/structure.d.ts.map +1 -1
  90. package/dist/structure.js +4 -3
  91. package/dist/structure.js.map +1 -1
  92. package/dist/submission.d.ts +8 -0
  93. package/dist/submission.d.ts.map +1 -1
  94. package/dist/submission.js +41 -10
  95. package/dist/submission.js.map +1 -1
  96. package/dist/tex-requirements.d.ts +3 -0
  97. package/dist/tex-requirements.d.ts.map +1 -1
  98. package/dist/tex-requirements.js +19 -0
  99. package/dist/tex-requirements.js.map +1 -1
  100. package/dist/tex-venue-rules.d.ts +19 -0
  101. package/dist/tex-venue-rules.d.ts.map +1 -1
  102. package/dist/tex-venue-rules.js +69 -0
  103. package/dist/tex-venue-rules.js.map +1 -1
  104. package/dist/types.d.ts +0 -2
  105. package/dist/types.d.ts.map +1 -1
  106. package/dist/venue-rules.d.ts +10 -0
  107. package/dist/venue-rules.d.ts.map +1 -1
  108. package/dist/venue-rules.js +26 -10
  109. package/dist/venue-rules.js.map +1 -1
  110. package/docs/configuration.md +146 -20
  111. package/docs/e2e.md +4 -6
  112. package/docs/prior-art/paper-folder-scaffolding.md +24 -29
  113. package/docs/rules/cycle/evidence.md +87 -0
  114. package/docs/rules/cycle/port-done.md +75 -0
  115. package/docs/rules/cycle/record.md +152 -0
  116. package/docs/rules/cycle/stage.md +85 -0
  117. package/docs/rules/tex/claim-provenance.md +0 -1
  118. package/docs/rules.md +48 -47
  119. package/docs/submission.md +5 -0
  120. package/eslint-rules/cycle-stage.test.mjs +131 -0
  121. package/eslint-rules/cycle-stage.ts +141 -0
  122. package/eslint-rules/latex-language.harness.mjs +3 -3
  123. package/eslint-rules/latex-language.ts +10 -13
  124. package/eslint-rules/paper-context.ts +2 -2
  125. package/eslint-rules/paper-typography.test.mjs +17 -85
  126. package/eslint-rules/paper-typography.ts +22 -98
  127. package/eslint-rules/papers.harness.mjs +1 -1
  128. package/eslint-rules/papers.ts +2 -4
  129. package/eslint-rules/tex-build.harness.mjs +2 -2
  130. package/eslint-rules/tex-build.ts +8 -25
  131. package/eslint.config.mjs +41 -2
  132. package/hooks/hooks.harness.ts +15 -15
  133. package/hooks/paper-edit-guard.hook.mjs +5 -8
  134. package/hooks/paper-edit-guard.test.ts +12 -3
  135. package/hooks/paper-skills-nudge.hook.mjs +2 -3
  136. package/hooks/paper-skills-nudge.test.ts +13 -3
  137. package/hooks/paper-status-gates.hook.mjs +2 -2
  138. package/hooks/paper-status-gates.test.ts +14 -2
  139. package/lib/markdown.test.mjs +5 -60
  140. package/lib/markdown.ts +9 -136
  141. package/lib/paper-config.ts +3 -2
  142. package/lib/skill-checks.test.mjs +4 -1
  143. package/lib/skill-eval-fixture.ts +25 -18
  144. package/package.json +1 -3
  145. package/presets/aidc.jsonc +24 -3
  146. package/presets/aidc.md +6 -3
  147. package/presets/msr.jsonc +25 -1
  148. package/presets/msr.md +5 -1
  149. package/presets/realm.jsonc +1 -1
  150. package/presets/venue-profile.schema.json +39 -0
  151. package/scripts/check.ts +4 -0
  152. package/scripts/harness-api.frozen.json +0 -5
  153. package/scripts/refresh-deadlines.test.ts +431 -0
  154. package/scripts/refresh-deadlines.ts +387 -0
  155. package/skills/cold-read-diff/SKILL.md +3 -3
  156. package/skills/cold-read-diff/SKILL.md.spec.ts +2 -2
  157. package/skills/grade-paper-writing/SKILL.md +14 -22
  158. package/skills/grade-paper-writing/SKILL.md.spec.ts +13 -21
  159. package/skills/grade-paper-writing/__snapshots__/prose-lint.test.ts.snap +40 -26
  160. package/skills/grade-paper-writing/prose-lint.mjs +108 -278
  161. package/skills/grade-paper-writing/prose-lint.test.ts +184 -81
  162. package/skills/paper-adversarial-review/SKILL.md +2 -2
  163. package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -1
  164. package/skills/paper-pipeline/SKILL.md +9 -12
  165. package/skills/paper-pipeline/SKILL.md.spec.ts +8 -11
  166. package/skills/paper-pipeline/description-language.eval.mjs +25 -15
  167. package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +25 -15
  168. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +25 -15
  169. package/skills/paper-pipeline/pipeline-firing.eval.mjs +27 -16
  170. package/skills/paper-pipeline/pipeline-language.eval.mjs +25 -15
  171. package/skills/paper-pipeline/references/acceptance-gate.md +16 -20
  172. package/skills/paper-pipeline/references/artifact-checklist.md +4 -5
  173. package/skills/paper-pipeline/references/pipeline-status-template.md +1 -1
  174. package/skills/paper-pipeline/scripts/README.md +0 -4
  175. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +8 -72
  176. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +48 -156
  177. package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +81 -75
  178. package/skills/paper-pipeline/scripts/generated-code.harness.mjs +3 -3
  179. package/skills/paper-status/SKILL.md +2 -2
  180. package/skills/paper-status/SKILL.md.spec.ts +1 -1
  181. package/skills/render-paper/SKILL.md +2 -2
  182. package/skills/render-paper/SKILL.md.spec.ts +1 -1
  183. package/skills/render-paper/extract-pdf-facts.mjs +2 -2
  184. package/skills/tighten-paper/SKILL.md +29 -42
  185. package/skills/tighten-paper/SKILL.md.spec.ts +28 -41
  186. package/skills/verify-citations/scripts/bib-authors.check.test.mjs +0 -13
  187. package/skills/verify-citations/scripts/bib-authors.cli.test.ts +23 -11
  188. package/skills/verify-citations/scripts/bib-authors.mjs +8 -93
  189. package/skills/verify-citations/scripts/bib-authors.test.mjs +1 -44
  190. package/src/adapters/hotcrp/cassette/README.md +22 -0
  191. package/src/adapters/hotcrp/cassette/deadlines-aidc26.html +17 -0
  192. package/src/adapters/hotcrp/cassette/deadlines-fse2027.html +24 -0
  193. package/src/adapters/hotcrp/cassette/deadlines-icse2027.html +25 -0
  194. package/src/adapters/hotcrp/cassette/deadlines-msr2027.html +25 -0
  195. package/src/adapters/hotcrp/cassette/deadlines-nosuch.html +18 -0
  196. package/src/adapters/hotcrp/cassette/deadlines-sosp26.html +26 -0
  197. package/src/adapters/hotcrp/deadlines.io.test.ts +86 -0
  198. package/src/adapters/hotcrp/deadlines.io.ts +27 -0
  199. package/src/adapters/hotcrp/deadlines.test.ts +170 -0
  200. package/src/adapters/hotcrp/deadlines.ts +161 -0
  201. package/src/adapters/hotcrp/index.ts +2 -0
  202. package/src/build.harness.mjs +2 -2
  203. package/src/build.test.ts +1 -1
  204. package/src/build.ts +1 -6
  205. package/src/cli.harness.mjs +26 -46
  206. package/src/cli.test.ts +41 -40
  207. package/src/cli.ts +57 -58
  208. package/src/cycle-rules.test.ts +278 -0
  209. package/src/cycle-rules.ts +273 -0
  210. package/src/domain/cycle.test.ts +456 -0
  211. package/src/domain/cycle.ts +432 -0
  212. package/src/domain/deadline.test.ts +386 -0
  213. package/src/domain/deadline.ts +472 -0
  214. package/src/folder-venue-rule.test.ts +1 -1
  215. package/src/init.test.ts +6 -7
  216. package/src/init.ts +3 -22
  217. package/src/lint-fix.test.ts +10 -4
  218. package/src/new-paper.harness.mjs +9 -28
  219. package/src/new-paper.test.ts +7 -6
  220. package/src/new-paper.ts +11 -30
  221. package/src/paper-files.test.ts +5 -4
  222. package/src/paper-files.ts +4 -4
  223. package/src/paper-prose.test.ts +49 -0
  224. package/src/paper-prose.ts +67 -0
  225. package/src/paper-settings.test.ts +167 -0
  226. package/src/paper-settings.ts +85 -11
  227. package/src/parse-args.test.ts +0 -1
  228. package/src/ports/venue-deadlines.ts +14 -0
  229. package/src/presets.test.ts +105 -0
  230. package/src/presets.ts +43 -10
  231. package/src/rule-cases.test.ts +144 -0
  232. package/src/structure.harness.ts +5 -11
  233. package/src/structure.ts +4 -3
  234. package/src/submission.test.ts +105 -1
  235. package/src/submission.ts +73 -12
  236. package/src/tex-requirements.ts +31 -0
  237. package/src/tex-venue-rules.test.ts +76 -0
  238. package/src/tex-venue-rules.ts +85 -0
  239. package/src/types.ts +0 -2
  240. package/src/venue-rules.test.ts +82 -0
  241. package/src/venue-rules.ts +44 -12
  242. package/fixtures/real-markdown-paper/baseline.json +0 -22
  243. package/fixtures/real-markdown-paper/baseline.ts +0 -70
  244. package/skills/paper-pipeline/PIPELINE-MAP.md +0 -381
  245. package/skills/paper-pipeline/pipeline-firing.baseline.json +0 -270
  246. package/skills/paper-pipeline/references/sweep-2026-08-09-ideal-pipeline.md +0 -585
  247. package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +0 -492
  248. package/skills/paper-pipeline/scripts/artifact-coverage.mjs +0 -397
  249. package/skills/paper-pipeline/scripts/artifact-coverage.test.ts +0 -86
  250. package/skills/paper-pipeline/scripts/check-provenance.mjs +0 -184
  251. package/skills/paper-pipeline/scripts/population-map.harness.mjs +0 -135
  252. package/skills/paper-pipeline/scripts/population-map.mjs +0 -188
  253. package/skills/paper-pipeline/scripts/population-map.test.mjs +0 -154
  254. package/skills/paper-pipeline/scripts/provenance.harness.mjs +0 -266
  255. package/skills/paper-pipeline/scripts/round-diff.harness.ts +0 -907
  256. package/skills/paper-pipeline/scripts/round-diff.mjs +0 -583
  257. package/skills/paper-pipeline/scripts/round-diff.test.mjs +0 -159
  258. package/skills/tighten-paper/__snapshots__/structure.test.ts.snap +0 -133
  259. package/skills/tighten-paper/structure.mjs +0 -364
  260. package/skills/tighten-paper/structure.test.ts +0 -117
  261. package/templates/paper/paper.md +0 -4
@@ -37,6 +37,8 @@
37
37
  * no geometry (no banal) pdf/measured (warn); geometry and body-size silent, page-limit judges
38
38
  * only a body counted before the references; fonts still judge
39
39
  * kind does not resolve pdf/profile (error); page-limit silent; everything else still judges
40
+ * cycle phase `porting` every rule silent โ€” the source is in the previous venue's template by
41
+ * declaration (src/domain/cycle.ts); cycle/port-done watches the declaration
40
42
  */
41
43
  import { dirname, isAbsolute, join, basename, relative } from "node:path";
42
44
  import {
@@ -65,6 +67,7 @@ import {
65
67
  type PageSplit,
66
68
  } from "./domain/body-pages.ts";
67
69
  import type { TextFacts } from "./facts-file.ts";
70
+ import { effectiveBlind, isPorting } from "./domain/cycle.ts";
68
71
 
69
72
  // โ”€โ”€ the verdict's vocabulary โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
70
73
 
@@ -96,8 +99,19 @@ export interface Resolved {
96
99
  export type Assessment =
97
100
  | { readonly kind: "no-venue" }
98
101
  /** A `paperlint.json` that extends no preset yet โ€” `pdf/measured` names the file to set. */
99
- | { readonly kind: "no-preset"; readonly file: string }
102
+ | {
103
+ readonly kind: "no-preset";
104
+ readonly file: string;
105
+ /** The paper keeps `cycles`: its venue is the current cycle's, and `extends` beside them is refused. */
106
+ readonly cycles: boolean;
107
+ }
100
108
  | { readonly kind: "unresolved"; readonly finding: Finding }
109
+ /**
110
+ * The current cycle declares its port to the venue's template as open work (`"phase": "porting"`):
111
+ * a PDF built with the previous venue's template says nothing about this venue's limits, so every
112
+ * venue rule is silent, and `cycle/port-done` speaks when the port is done and the phase still says so.
113
+ */
114
+ | { readonly kind: "porting" }
101
115
  | { readonly kind: "unbuilt"; readonly venue: Resolved }
102
116
  | { readonly kind: "stale"; readonly finding: Finding }
103
117
  | {
@@ -147,9 +161,13 @@ function kindOf(
147
161
  /** How the venue reviews, and who the paper says wrote it: what `anonymity/*` judges by. */
148
162
  const reviewOf = (
149
163
  preset: Pick<Preset, "blind">,
150
- settings: Pick<PaperSettings, "identity">,
164
+ settings: Pick<PaperSettings, "identity" | "cycles">,
151
165
  ): Pick<Resolved, "blind" | "identity"> => ({
152
- blind: preset.blind,
166
+ // An accepted attempt's camera-ready carries the authors: no longer blind.
167
+ blind: effectiveBlind(
168
+ preset.blind,
169
+ settings.cycles?.current ?? { kind: "none" },
170
+ ),
153
171
  identity: settings.identity,
154
172
  });
155
173
 
@@ -174,13 +192,25 @@ function freshFacts(
174
192
 
175
193
  const isFinding = (v: object): v is Finding => "messageId" in v;
176
194
 
195
+ /** The paper's declared venue and kind, resolved: what the judges compare the facts against. */
196
+ const resolvedOf = (preset: Preset, settings: PaperSettings): Resolved => ({
197
+ venue: preset.label,
198
+ format: preset.format,
199
+ ...kindOf(preset.label, preset.format, settings.kind),
200
+ ...reviewOf(preset, settings),
201
+ });
202
+
177
203
  /** One paper, assessed. Reads through `deps.files` only; never throws on a paper's files. */
178
204
  export function assessPaper(paperDir: string, deps: VenueRuleDeps): Assessment {
179
205
  const p = paperPreset(paperDir, deps);
180
206
  if (p.kind === "none")
181
207
  return p.settings === null
182
208
  ? { kind: "no-venue" }
183
- : { kind: "no-preset", file: join(paperDir, CONFIG_FILE) };
209
+ : {
210
+ kind: "no-preset",
211
+ file: join(paperDir, CONFIG_FILE),
212
+ cycles: p.settings.cycles !== null,
213
+ };
184
214
  if (p.kind === "settings-problem")
185
215
  return {
186
216
  kind: "unresolved",
@@ -191,13 +221,9 @@ export function assessPaper(paperDir: string, deps: VenueRuleDeps): Assessment {
191
221
  kind: "unresolved",
192
222
  finding: finding("preset", { why: presetProblemText(p.problem) }),
193
223
  };
194
- const label = p.preset.label;
195
- const venue: Resolved = {
196
- venue: label,
197
- format: p.preset.format,
198
- ...kindOf(label, p.preset.format, p.settings.kind),
199
- ...reviewOf(p.preset, p.settings),
200
- };
224
+ if (p.settings.cycles !== null && isPorting(p.settings.cycles.current))
225
+ return { kind: "porting" };
226
+ const venue = resolvedOf(p.preset, p.settings);
201
227
  const factsText = text(deps.files, factsPath(paperDir));
202
228
  if (factsText === null) return { kind: "unbuilt", venue };
203
229
  const facts = freshFacts(deps.files, paperDir, factsText);
@@ -558,7 +584,11 @@ const JUDGES: Readonly<Record<VenueRuleName, Judge>> = {
558
584
  fresh: (a) => (a.kind === "stale" ? [a.finding] : []),
559
585
  measured: (a, { shown }) =>
560
586
  a.kind === "no-preset"
561
- ? [finding("noPreset", { file: shown(a.file) })]
587
+ ? [
588
+ finding(a.cycles ? "noPresetCycle" : "noPreset", {
589
+ file: shown(a.file),
590
+ }),
591
+ ]
562
592
  : a.kind === "unbuilt"
563
593
  ? [
564
594
  finding("unbuilt", {
@@ -655,6 +685,8 @@ const META: Readonly<Record<VenueRuleName, Meta>> = {
655
685
  "not built yet, so `{{venue}}`'s page limit, fonts and format are not checked โ€” run `paperlint build`",
656
686
  noPreset:
657
687
  'this paper names no venue preset yet, so its page limit, fonts and format are not checked โ€” set "extends" in {{file}} (e.g. "paperlint:agenticdev"; see docs/rules.md)',
688
+ noPresetCycle:
689
+ 'this paper\'s current cycle names no venue preset, so its page limit, fonts and format are not checked โ€” set the venue in the current cycle in {{file}} ("venue": { "kind": "preset", "extends": "paperlint:โ€ฆ" }), or open a new cycle when the last one has ended',
658
690
  noGeometry:
659
691
  "the build measured no page geometry (banal was not found or failed), so page size, columns, page limits and font sizes were NOT checked โ€” run `paperlint toolchain`, then `paperlint build`",
660
692
  },
@@ -1,22 +0,0 @@
1
- {
2
- "$comment": [
3
- "What the rules say about this article TODAY, as data rather than as a sentence in a docblock.",
4
- "It is not a target and not a clean bill of health โ€” the fixture's README says findings here",
5
- "are measurements. Its job is the SILENCE half: a rule that starts reporting something new on",
6
- "225 lines of real prose has almost certainly gained a false positive, and the harness beside",
7
- "this file names the rule. That is not expressible on an eleven-line stub, where every line was",
8
- "written by someone who already knew which rule would read it.",
9
- "",
10
- "Recorded 2026-09-19, re-recorded 2026-09-26 for 3.0.0. Two of the three rules are about the",
11
- "scorecard being deliberately thin (no pdf, no frozen source) rather than about the prose.",
12
- "paper/leading-zero reports each of the 18 p-values of paperlint#44 where it is (one finding per",
13
- "occurrence since 3.0.0; before, paper/typography reported them as one count). They are real",
14
- "findings against IEEE / ISO 80000-1 style, in a blog post written for a general audience.",
15
- "The count is expected to stay. A drop is never a failure here, only growth is."
16
- ],
17
- "findings": {
18
- "paper/stages": 2,
19
- "paper/source": 1,
20
- "paper/leading-zero": 18
21
- }
22
- }
@@ -1,70 +0,0 @@
1
- /**
2
- * ONE reader of `baseline.json`, shared by every run that compares against one: the harness beside
3
- * this file (the repository's own `bin/paperlint.mjs`), `test/e2e/install/install.e2e.ts` (the
4
- * binary a consumer actually got), and `fixtures/accepted-papers/accepted-papers.test.ts` (one
5
- * baseline per accepted paper). Two copies of "growth fails, a drop never does" would drift the
6
- * first time one of them is tightened, and the runs would then disagree about the same rules.
7
- */
8
- import { readFileSync } from "node:fs";
9
- import { dirname, join } from "node:path";
10
- import { fileURLToPath } from "node:url";
11
- import { z } from "zod";
12
-
13
- /** Findings per rule id. */
14
- export type Counts = Record<string, number>;
15
-
16
- const Baseline = z.object({ findings: z.record(z.string(), z.number()) });
17
- const LintOutput = z.array(
18
- z.object({
19
- messages: z.array(z.object({ ruleId: z.string().nullable() })).nullish(),
20
- }),
21
- );
22
-
23
- const HERE = dirname(fileURLToPath(import.meta.url));
24
-
25
- /** The counts recorded in `dir`'s `baseline.json` (this article's by default), `{ruleId: n}`. */
26
- export function recordedFindings(dir: string = HERE): Counts {
27
- return Baseline.parse(
28
- JSON.parse(readFileSync(join(dir, "baseline.json"), "utf8")),
29
- ).findings;
30
- }
31
-
32
- /**
33
- * `paperlint lint --json` stdout โ†’ `{ruleId: n}`.
34
- * ๐Ÿ”ด Output that does not parse THROWS. An empty set read as "clean" is how this repository's
35
- * checks have gone hollow before.
36
- */
37
- export function countByRule(stdout: string): Counts {
38
- const out: Counts = {};
39
- for (const file of LintOutput.parse(JSON.parse(stdout)))
40
- for (const m of file.messages ?? []) {
41
- const rule = String(m.ruleId);
42
- out[rule] = (out[rule] ?? 0) + 1;
43
- }
44
- return out;
45
- }
46
-
47
- /**
48
- * Compare measured counts with the recording.
49
- * grew โ€” a rule says MORE about real prose than recorded: a false positive until shown
50
- * otherwise. Always a failure.
51
- * vanished โ€” a recorded rule went fully quiet without the recording being updated: how a check
52
- * dies unnoticed. Also a failure.
53
- * A partial drop is neither: it is what a fix looks like (paperlint#44 is expected to take typography
54
- * to zero, and then the recording is updated in the same change).
55
- */
56
- export function compareToBaseline(
57
- found: Readonly<Counts>,
58
- recorded: Readonly<Counts> = recordedFindings(),
59
- ): {
60
- grew: { rule: string; now: number; recorded: number }[];
61
- vanished: string[];
62
- } {
63
- const grew = Object.entries(found)
64
- .filter(([rule, n]) => n > (recorded[rule] ?? 0))
65
- .map(([rule, n]) => ({ rule, now: n, recorded: recorded[rule] ?? 0 }));
66
- const vanished = Object.keys(recorded).filter(
67
- (r) => (recorded[r] ?? 0) > 0 && !(r in found),
68
- );
69
- return { grew, vanished };
70
- }
@@ -1,381 +0,0 @@
1
- ---
2
- title: "Paper pipeline map โ€” what runs where"
3
- created: 2026-08-26
4
- updated: 2026-08-26
5
- tags: [paper-pipeline, ci, hooks, map]
6
- ---
7
-
8
- # What runs where: the pipeline map
9
-
10
- Assembled 2026-08-26 **from facts** โ€” `.claude/settings.json` and
11
- `.github/workflows/paper-gates.yml` โ€” not from memory. Occasion (translated from Russian): "I have
12
- no fucking idea how the pipeline works right now."
13
-
14
- > โญ **How to build checks** (the ladder, occupancy across 14 tools, measurements) โ€”
15
- > in the author's private notes: `<papers-root>/research/2026-08-26-sessiya-arhitektura-payplayna.md`
16
-
17
- ## Four tiers, and they fire at different times
18
-
19
- ```
20
- โ”Œโ”€ TIER 1: WHILE WRITING โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ instant, in-session โ”€โ”€โ”
21
- โ”‚ โ”‚
22
- โ”‚ you edit paper.md / paper.tex โ”‚
23
- โ”‚ โ”‚ โ”‚
24
- โ”‚ โ”œโ”€ PreToolUse โ†’ paper-lint pre ๐Ÿ›‘ THE ONLY BLOCKER โ”‚
25
- โ”‚ โ”‚ paragraph >200 words ยท โ‰ฅ9 numbers ยท โ”‚
26
- โ”‚ โ”‚ sentence >55 words ยท refuses the edit โ”‚
27
- โ”‚ โ”‚ outright โ”‚
28
- โ”‚ โ”‚ โ”‚
29
- โ”‚ โ”œโ”€ PreToolUse โ†’ paper-edit-guard ๐Ÿ›‘ a write from Bash โ”‚
30
- โ”‚ โ”‚ (bypassing PostToolUse gates) โ†’ deny โ”‚
31
- โ”‚ โ”‚ โ”‚
32
- โ”‚ โ””โ”€ PostToolUse โ†’ paper-lint post โš ๏ธ nudges only โ”‚
33
- โ”‚ plain-language ยท carries: ยท SHAVE โ”‚
34
- โ”‚ prose-lint ยท the numbers gate โ”‚
35
- โ”‚ โ”‚
36
- โ”‚ end of response โ†’ Stop โ†’ paper-lint stop (throttled to 1 hour) โ”‚
37
- โ”‚ 15 checks across all papers โ”‚
38
- โ”‚ + kb-lint stop: 12 checks across the base โ”‚
39
- โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
40
- โ”‚
41
- โ–ผ
42
- โ”Œโ”€ TIER 2: BUILD โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ when you run pdflatex โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
43
- โ”‚ โ”‚
44
- โ”‚ paper.tex โ”‚
45
- โ”‚ โ”‚ โ”‚
46
- โ”‚ โ–ผ โ”‚
47
- โ”‚ pdflatex โ”€โ”€โ”ฌโ”€โ†’ INSIDE, acmart runs (the document class) โ”‚
48
- โ”‚ โ”‚ it's the same thing that complains in the log: โ”‚
49
- โ”‚ โ”‚ no CCS ยท no libertine ยท โ”‚
50
- โ”‚ โ”‚ ยซACM reference format is mandatoryยป โ”‚
51
- โ”‚ โ”‚ โ”‚
52
- โ”‚ โ”œโ”€โ†’ paper.pdf โ† the final artifact, fonts EMBEDDED โ”‚
53
- โ”‚ โ”œโ”€โ†’ paper.log โ† acmart writes its warnings here โ”‚
54
- โ”‚ โ””โ”€โ†’ paper.blg โ† bibtex writes bibliography errors hereโ”‚
55
- โ”‚ โ”‚
56
- โ”‚ check-render.sh reads ALL THREE: โ”‚
57
- โ”‚ log โ†’ overfull ยท undefined refs ยท class warnings โ”‚
58
- โ”‚ blg โ†’ dropped entries ยท cited-but-absent โ”‚
59
- โ”‚ pdf โ†’ fonts (LinLibertine/LinBiolinum) โ† added 25.08 โ”‚
60
- โ”‚ + chktex on the .tex, if installed (advisory) โ”‚
61
- โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
62
- โ”‚
63
- โ–ผ
64
- โ”Œโ”€ TIER 3: CI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ on push/PR, 7 jobs โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
65
- โ”‚ โ”‚
66
- โ”‚ changes โ”€โ”€โ” a path classifier, ~10s, WITHOUT a checkout โ”‚
67
- โ”‚ โ”‚ decides which jobs even run โ”‚
68
- โ”‚ โ–ผ โ”‚
69
- โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
70
- โ”‚ โ”‚ prose eslint(md) ยท paper-lint --gate ๐Ÿ›‘ ยท bib-authors โ”‚ โ”‚
71
- โ”‚ โ”‚ prose-lint ยท artifact coverage ยท population map โ”‚ โ”‚
72
- โ”‚ โ”‚ structure ยท provenance ยท verify-cites ยท scorecardโ”‚ โ”‚
73
- โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚
74
- โ”‚ โ”‚ numbers paper_numbers selftest โ†’ gate โ”‚ โ”‚
75
- โ”‚ โ”‚ artifact/reproduce.py โ† wired in 26.08 โ”‚ โ”‚
76
- โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚
77
- โ”‚ โ”‚ anonymity check-anon over the bundle + every *.selftest.{sh,mjs} โ”‚
78
- โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚
79
- โ”‚ โ”‚ build texlive/texlive:latest container โ† FONTS LIVE HEREโ”‚ โ”‚
80
- โ”‚ โ”‚ aclpubcheck (optional) ยท builds ยท check-render โ”‚ โ”‚
81
- โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚
82
- โ”‚ โ”‚ hooks vigiles lint ๐Ÿ›‘ ยท every harness on disk โ”‚ โ”‚
83
- โ”‚ โ”‚ TeXtidote (pinned jar) ยท ledger selftest โ”‚ โ”‚
84
- โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚
85
- โ”‚ โ”‚ skill-firing once a week, a real model โ”‚ โ”‚
86
- โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
87
- โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
88
- โ”‚
89
- โ–ผ
90
- โ”Œโ”€ TIER 4: THE VENUE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ the one verdict that actually decides โ”€โ”€โ”
91
- โ”‚ โ”‚
92
- โ”‚ eRights (ACM) โ”€โ”€โ†’ issues a block of copyright commands โ”‚
93
- โ”‚ โ”‚ โ†“ paste into paper.tex, REBUILD โ”‚
94
- โ”‚ โ–ผ โ”‚
95
- โ”‚ upload goes to TWO PLACES: โ”‚
96
- โ”‚ โ€ข conference-publishing.com โ† goes to typesetting and ACM DL โ”‚
97
- โ”‚ โ€ข HotCRP paper/20/edit โ† a copy for the workshop chairs โ”‚
98
- โ”‚ โ”‚ โ”‚
99
- โ”‚ โ–ผ โ”‚
100
- โ”‚ their checker (TAPS / the portal's format checker) โ€” closed-source โ”‚
101
- โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
102
- ```
103
-
104
- ## What only runs by hand
105
-
106
- Skills (`grade-paper-writing`, `tighten-paper`, `pc-panel-review`, `cold-read-diff`,
107
- `verify-citations`, `argument-arc`, `harden-paper`โ€ฆ) are **not invoked by any event**. A human calls
108
- them. This is the judgment layer; its mechanical parts should keep getting pushed down into tiers
109
- 1โ€“3.
110
-
111
- ## Where each tier catches the same thing
112
-
113
- | defect | caught by |
114
- | ------------------------------------- | ---------------------------------------------------------------------------- |
115
- | a wall-of-text paragraph on insertion | tier 1, blocks |
116
- | a number that doesn't match the data | tier 1 (post) and tier 3 (numbers) |
117
- | a reference dropped by bibtex | tier 2 (`.blg`) โ†’ tier 3 (build) |
118
- | **wrong font** | tier 2 (the facts' font list) โ€” but it's PREVENTED by tier 3 (the container) |
119
- | a stage declared with no PDF | tier 3, `paper-lint --gate` ๐Ÿ›‘ |
120
- | ACM format compliance | **tier 4, and only tier 4** |
121
-
122
- ---
123
-
124
- # ๐Ÿ—บ๏ธ Roadmap: what is NOT built
125
-
126
- ## 1. ๐Ÿ”ด A requirement as a BUILD FAILURE, not a check afterward (the author's idea, 26.08)
127
-
128
- Right now almost everything is caught **after** the build. But `acmart` already knows how to fail
129
- the build โ€” it does exactly that on missing CCS concepts (_"CCS concepts are mandatory for papers
130
- over two pages"_). So the mechanism exists, and we're not using it.
131
-
132
- **Rung 2 of the ladder instead of rung 3:** there's nothing to check in something that can't even be
133
- built.
134
-
135
- | requirement | how to turn it into a build failure |
136
- | --------------------- | ---------------------------------------------------------------------------------------------------------------- |
137
- | **fonts** | `fontspec` + a font by path โ†’ file missing โ†’ the build fails instead of silently falling back to Computer Modern |
138
- | page limit | `\AtEndDocument` + a check on `\thepage` โ†’ `\PackageError` |
139
- | text-block dimensions | the class sets these; catch the fact that they were overridden |
140
- | no page numbers | check `\thepage` in the output |
141
-
142
- ๐Ÿ”ด **MEASURED on 2026-08-26, not assumed: `fontspec` with `pdflatex` DOES NOT WORK.** A minimal
143
- document, a `pdflatex` run, exit code 1, verbatim from the log:
144
-
145
- ```
146
- Package: fontspec Font selection for XeLaTeX and LuaLaTeX
147
- ! Fatal Package fontspec Error: The fontspec package requires either XeTeX or LuaTeX.
148
- ```
149
-
150
- โ‡’ The whole chain: only `fontspec` can load a font by path โ†’ `fontspec` requires XeLaTeX or LuaLaTeX
151
- โ†’ we run `pdflatex` โ†’ **the construction requires SWITCHING THE BUILD ENGINE**, not adding a
152
- package. That moves the item from "add a line" to "change the program that builds the paper."
153
-
154
- โš ๏ธ **Three unverified things that decide whether the first row is even feasible** (these are
155
- unknowns, not objections): `fontspec` requires XeLaTeX/LuaLaTeX instead of pdflatex ยท `acmart`
156
- assigns its own fonts and might conflict ยท whether the TAPS pipeline will even accept a LuaLaTeX
157
- source. Plus a risk: switching engines changes how microtype is handled, and there's no page-count
158
- slack.
159
-
160
- **Checked by a cheap spike** โ€” build a copy in scratch, see whether the class fights back and
161
- whether the layout shifts. Do this AFTER the 29.08 submission.
162
-
163
- ## 2. Mechanical camera-ready compliance โ€” 1 requirement out of ~25 is checked
164
-
165
- ACM's instructions list ~25 mechanical requirements. Checked: fonts. The rest โ€” text-block geometry,
166
- 9pt/8pt type size, column balance, DOIs on references, captions above tables and below figures,
167
- capitalization, section ordering โ€” **nothing checks any of it**.
168
-
169
- **A construction found and verified against source (26.08):**
170
-
171
- ```
172
- banal -json + pdf.js โ†’ paper.facts.json โ†’ @eslint/json
173
- (geometry) (fonts) (plain JSON) (the official plugin)
174
- ```
175
-
176
- - **`banal`** โ€” [kohler/hotcrp/src/banal](https://github.com/kohler/hotcrp/blob/master/src/banal),
177
- 1901 lines of Perl, dependencies are core Perl + `pdftohtml`. Standalone, alive (commits on
178
- 03.08.2026). paperlint feeds it pdftohtml-style XML written from pdf.js instead, so poppler is not
179
- needed (`src/adapters/banal/`: `xml.ts` writes it, `run.ts` runs banal; issue #61). Gives you `papersize`, `margin`, `bodyfontsize`, `leading`, `columns`, `npages`,
180
- `reffontsize`. It **doesn't know about fonts by construction** โ€” the words `type3`/`embed` don't
181
- appear in the file at all.
182
- - **banal does not accept a spec** โ€” the comparison lives in `checkformat.php`, the profile grammar
183
- is `papersize;pagelimit;columns;textblock;bodyfontsize;bodylineheight`. For ACM โ‰ˆ
184
- `letter;;2;18x23.5cm;9`.
185
- - **Why through JSON and not an ESLint language plugin:** `fileType: "binary"` in ESLint is **not
186
- implemented**, the docs say verbatim โ€” _"should be 'text' (in the future, we will also support
187
- 'binary')"_, and the runtime reads the file unconditionally as UTF-8 (`eslint-helpers.js:1281`),
188
- irreversibly mangling PDF bytes into U+FFFD. A hack is possible (`parse()` reads the disk itself),
189
- but it breaks `--fix`, the cache, and `eslint-disable` โ€” there's nowhere to put a directive inside
190
- a PDF.
191
- - **veraPDF is built the same way** โ€” the one PDF validator with user-defined rules: _"doesn't parse
192
- PDF documents directly. Instead it processes the machine readable report output"_. Matching the
193
- architecture of an industrial tool is a sign the construction is right.
194
-
195
- **Occupancy checked:** `eslint-plugin-pdf` โ†’ E404 ยท `textlint` accepts its own format but requires
196
- conversion to text first (precedent: `textlint-plugin-pptx`) ยท `Vale` won't take a custom parser ยท
197
- `MegaLinter`/`super-linter`/`reviewdog` are orchestrators, they won't give us a rule registry for our
198
- format.
199
-
200
- ## 3. Venue constants belong in a card, not in code
201
-
202
- > **Done (#79, 2.1.0), in a different shape than planned below.** The constants live in **venue
203
- > presets**, JSONC files in the package's `presets/`: `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
-
210
- Right now `LinLibertine`, `LinBiolinum`, `acmart` are hardcoded into `check-render.sh`. This is
211
- **venue data**, and it belongs in
212
- `.claude/skills/submit-paper/references/venues/<venue>.md`:
213
-
214
- ```yaml
215
- venue:
216
- template: acmart
217
- page_size: letter
218
- fonts: { text: LinLibertine, title: LinBiolinum }
219
- textblock: 18x23.5cm
220
- body_pt: 9
221
- body_limit: 5
222
- extra_ref_pages: 2
223
- ```
224
-
225
- The second consumer of this block is the comparison against `banal`'s output (six numbers). The
226
- first is the page limit, which currently exists in **four copies**, and they have already drifted
227
- apart (`venues/realm.md` says 8, `build-submission.sh` hardcodes 8, `PIPELINE-STATUS` writes "body
228
- 8/8," and the build prints 9).
229
-
230
- ## 4. `check-render.sh` โ€” 244 lines of bash full of greps
231
-
232
- The place is right (nothing else reads the build artifacts, and no offline ACM checker exists),
233
- **the form isn't**: no check registry, no config, no severity, thresholds are hardcoded. Bolting 25
234
- more greps onto it is the path we're getting off of. Deal with it together with items 2 and 3.
235
-
236
- ## 5. Check occupancy BEFORE writing โ€” the rule exists, it's rung 4
237
-
238
- Written down twice (05.08, 24.08), and the `sweep-design-space` skill exists. **It was violated
239
- three times overnight on 25/26.08** โ€” meaning prose and a skill don't work, both are rung 4.
240
-
241
- **Proposed construction:** a new check-file must carry an `occupancy:` line โ€” what was searched and
242
- why it didn't fit; the linter requires its presence. Same form as `carries:` for sections and
243
- `Cause:` for re-check findings. To be honest: this is rung 3, not 2 โ€” it doesn't stop you from
244
- skipping the search, but it makes **the absence of evidence visible**.
245
-
246
- ---
247
-
248
- # ๐ŸงŠ FREEZE on the current ad hoc state (the author's decision, 2026-08-26)
249
-
250
- Verbatim (translated from Russian): _"mark the current pipeline as shit by rule โ€” don't expand it as
251
- it is, only allow new, proper stuff."_
252
-
253
- > **Fixes โ€” yes. New checks โ€” no.**
254
- > A new check goes only in the new form โ€” but **only where the new form already exists.**
255
-
256
- Without the second half the rule is incoherent, and I slipped on it that same evening: announced the
257
- freeze and, two messages later, proposed bolting a check onto `paper-lint`.
258
-
259
- | check category | new form | where a new one goes |
260
- | ------------------------------- | --------------------------------------- | -------------------------------------------------------------- |
261
- | a single document: markdown, JS | **exists** โ€” ESLint | ๐Ÿ›‘ not allowed into the old form |
262
- | an artifact: PDF | **exists** โ€” venue profile + checker | ๐Ÿ›‘ not allowed into the old form |
263
- | cross-file, filesystem-shaped | **doesn't exist and isn't expected to** | stays a script โ€” that is the rule's boundary, not a workaround |
264
-
265
- **Enforcement:** `paper-lint.mjs` currently has exactly **18** check functions. The "there are 18"
266
- test fails on the nineteenth and forces an answer to "why isn't this in the new form." Small and
267
- readable, unlike a 27-entry debt ledger that would get muted within a day.
268
-
269
- ๐Ÿ”ด **The order is mandatory: freeze AFTER at least half is in the new form.** Right now it's one
270
- rule out of fifteen. Freezing before the new form becomes more convenient than the old one means
271
- getting a workaround and a dead rule. That has already happened in this repo three times.
272
-
273
- ---
274
-
275
- # ๐Ÿ“‹ Work queue, cheapest to most expensive
276
-
277
- ## 1. A marker instead of a blocklist โ€” one line
278
-
279
- `paperDirs()` today: `.filter((e) => e.name !== "research" && e.name !== "drafts")`. A hand-written
280
- list of two names; everything else under `papers/` is automatically counted as a paper.
281
-
282
- **Measured 2026-08-26:**
283
-
284
- | folder | `PIPELINE-STATUS.md` | counted as a paper today |
285
- | --------------------------------------------------- | -------------------- | ------------------------ |
286
- | `agenticdev-2026` `aisec-2026` `compile-rules-2026` | โœ… | โœ… correctly |
287
- | `scored-2026` | โ€” | ๐Ÿ”ด **yes, wrongly** |
288
- | `extension-shell-ifc` | โ€” | ๐Ÿ”ด **yes, wrongly** |
289
- | `research` `drafts` | โ€” | no (on the blocklist) |
290
-
291
- **The marker: a paper folder is one that has a `PIPELINE-STATUS.md`.** This has already been our
292
- convention since 22.07 ("every paper carries a PIPELINE-STATUS.md"), the code just doesn't use it.
293
- Self-declaration instead of a list: a new paper enters the checks the day it gets a scorecard.
294
-
295
- โš ๏ธ **Do NOT move the three papers into a separate subfolder.** The marker solves the same problem in
296
- one line, while a move breaks every form of reference โ€” when `.claude/pipeline/` was resettled there
297
- were 134 literal ones, and that wasn't even the whole count (paths built from segments, relative
298
- imports, paths inside regexes).
299
-
300
- ## 2. One build entry point for all papers, not one per paper
301
-
302
- > **Done (#59, 2026-09-24).** `paperlint build <paper>` is the entry point, and `paperlint toolchain` installs
303
- > TeX Live with the packages the venue presets declare; `ensure-toolchain.sh` and
304
- > `ci-install-texlive.sh` are deleted. What follows is the plan as it was written.
305
-
306
- **The hole:** `compile-rules` has `repro/build-submission.sh`, which calls `ensure-toolchain.sh`.
307
- **`agenticdev` has no entry point at all** โ€” it's built by hand with `pdflatex`, and nothing
308
- guarantees the fonts except the CI container. That's how the submitted `aisec` went out wrong.
309
-
310
- **The fix โ€” generalize the existing script to any paper folder**, instead of giving each one its own
311
- Makefile. It decides how to build from the files present: `paper.tex` exists โ†’ `pdflatex`;
312
- `paper.md` exists โ†’ the jinja2 pipeline. Three Makefiles would drift apart the same way the four
313
- copies of the page limit and the five copies of the package list already have.
314
-
315
- **Don't conflate the layers:**
316
-
317
- ```
318
- packages and fonts exist โ† the texlive container or ensure-toolchain.sh (apt)
319
- the compile cycle โ† latexmk (not in the toolchain yet, add it)
320
- fail if fonts are missing โ† \IfFileExists in paper.tex (done 26.08)
321
- ```
322
-
323
- `latexmk` replaces **only the middle layer** โ€” it does not install fonts.
324
-
325
- **There's then no need to check "does this folder have a Makefile"** โ€” there's one shared entry
326
- point. Nothing to check.
327
-
328
- ## 3. Remove duplicated constants
329
-
330
- | fact | how many copies | where they diverge |
331
- | -------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
332
- | page limit | **4** | `venues/realm.md` 8 ยท `build-submission.sh` 8 ยท `PIPELINE-STATUS` "8/8" ยท the build prints **9** |
333
- | the TeX package list | **5** | `ensure-toolchain.sh` (executable) ยท `SKILL.md` ยท `render-paper.harness.mjs` (pins it โœ…) ยท `build-submission.sh` ยท `PIPELINE-STATUS` |
334
-
335
- The canonical source is the venue preset (`presets/<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.
340
-
341
- ## 3-ter. Drop the font gate from `check-render.sh` once the rules run in CI
342
-
343
- The `pdffonts` block was added to `check-render.sh` on 25.08, and once the judgment moves into rules
344
- it becomes **a second source of truth about the same fonts**. Removing it shrinks the frozen script
345
- rather than extending it, so it does not break the freeze.
346
-
347
- โš ๏ธ **But not before the rules are actually running in CI.** Today `check-render.sh` is the only
348
- thing in CI that even looks at `agenticdev-2026` (the `build` job only builds papers that have
349
- `repro/build-submission.sh`, and that's one out of five). Removing it before the replacement is in
350
- place means trading a duplicate for a hole.
351
-
352
- ## 3-bis. ๐Ÿ”ด Remove the venue-profile duplicate by GENERATING it, not guarding it with an assert
353
-
354
- The venue numbers live in two places: `<venue>.yaml` (read by the rules) and `<venue>.tex` (read by
355
- LaTeX itself, which can't do YAML). On 26.08 `texMismatch()` inside `check-geometry.mjs` guarded
356
- against the drift โ€” **the script was torn out on 26.08, along with it**, because the whole thing was
357
- rung 5.
358
-
359
- **The assert was the wrong answer from the start.** It guards a duplicate instead of there being no
360
- duplicate. The right form is the same one we arrived at for the PDF facts: **`<venue>.tex` IS
361
- GENERATED from `<venue>.yaml` in the same step that builds the paper**, lives in `_build/`, and
362
- never enters git. Then the drift is unrepresentable, and there's nothing to guard โ€” rung 1, not rung 5.
363
-
364
- โš ๏ธ The objection "a generated file goes stale silently" **does not apply** here, and the distinction
365
- matters: what goes stale is a file that's committed and regenerated only occasionally. This one
366
- regenerates on every build, like the PDF facts do. Silent staleness requires that someone be able to
367
- read the OLD copy โ€” and there is nowhere to get one.
368
-
369
- **Until this is done, NOBODY guards the duplicate.** Recorded deliberately: the cost is a mismatch
370
- between the preamble guards and the rules; the odds are low โ€” profile edits happen about once per
371
- venue.
372
-
373
- ## 4. Tools: what's been checked and decided
374
-
375
- | | verdict |
376
- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
377
- | **`eslint-plugin-project-structure`** | **can do** conditional "if A then B" (`enforceExistence`, via a real `fs.existsSync`), and it ships a dummy parser + `files: ["**"]`, so it sees folders with not a single JS file. **But there's no OR semantics** โ€” "Makefile OR justfile" is not expressible. We don't need it after item 2; a candidate for other structural rules |
378
- | **`steiger`** | โŒ dropped โ€” its own README says _"not extendable with more rules"_, plus it's about FSD in JS |
379
- | **an ESLint config inside a paper folder** | โŒ **doesn't work in this repository**: there are **233** configs here, **2** of them ours โ€” the other 231 sit inside `compile-rules-2026/repro/` (other people's repos, pulled in). A nested lookup would pick up every one of them, which is exactly why we run with `--no-config-lookup`. The same result comes from a `files: ["<papers-root>/<paper>/**"]` section in the root config |
380
- | **`banal`** | โœ… adopted โ€” fetched by `paperlint toolchain` at a pinned HotCRP commit (GPL; not vendored into paperlint), run on pdf.js-written XML by `paperlint build` and `extract-pdf-facts.mjs` (measurement only; the ESLint rules do the judging) |
381
- | **`latexmk`** | superseded: `paperlint build` runs its own loop (`src/latex-loop.ts`) |