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
@@ -36,7 +36,6 @@ const ROOT = dirname(HERE);
36
36
  const {
37
37
  newPaper,
38
38
  nameProblem,
39
- wantedFiles,
40
39
  PACKAGE_TEMPLATES,
41
40
  OVERRIDE_DIR,
42
41
  STATUS_FILE,
@@ -63,7 +62,7 @@ async function lint(dir) {
63
62
 
64
63
  try {
65
64
  // ── the templates are FILES in the package, one per required file ──────────────────────
66
- for (const f of [STATUS_FILE, SOURCE_FILE.tex, SOURCE_FILE.md])
65
+ for (const f of [STATUS_FILE, SOURCE_FILE])
67
66
  check(
68
67
  `the package ships templates/paper/${f}`,
69
68
  existsSync(join(PACKAGE_TEMPLATES, f)),
@@ -85,13 +84,13 @@ try {
85
84
  );
86
85
  check(
87
86
  "a refused name creates nothing",
88
- newPaper(work, "Bad", "tex", { venues: [] }).ok === false &&
87
+ newPaper(work, "Bad", { venues: [] }).ok === false &&
89
88
  !existsSync(join(work, "Bad")),
90
89
  );
91
90
 
92
91
  // ── a fresh LaTeX paper ─────────────────────────────────────────────────────────────
93
92
  const papers = join(work, "papers");
94
- const r = newPaper(papers, "demo", "tex", { venues: [] });
93
+ const r = newPaper(papers, "demo", { venues: [] });
95
94
  check(
96
95
  "a fresh folder gets the scorecard, a .tex source and paperlint.json, all from the package",
97
96
  r.ok &&
@@ -129,36 +128,18 @@ try {
129
128
 
130
129
  // ── never overwrite; on an existing folder only the missing files ─────────────────────
131
130
  writeFileSync(join(papers, "demo", STATUS_FILE), "mine\n");
132
- const again = newPaper(papers, "demo", "md", { venues: [] });
131
+ const again = newPaper(papers, "demo", { venues: [] });
133
132
  check(
134
- "🔴 a second run overwrites nothing and adds no second source",
133
+ "🔴 a second run overwrites nothing",
135
134
  again.ok &&
136
135
  !again.fresh &&
137
136
  read(papers, "demo", STATUS_FILE) === "mine\n" &&
138
- !existsSync(join(papers, "demo", "paper.md")) &&
139
137
  again.files.every((f) => f.status === "kept"),
140
138
  );
141
- mkdirSync(join(papers, "old"), { recursive: true });
142
- writeFileSync(join(papers, "old", "paper.md"), "# Old\n");
143
- const migrated = newPaper(papers, "old", "tex", { venues: [] });
144
- check(
145
- "an old folder with only paper.md gets ONLY the scorecard — that is the migration",
146
- migrated.ok &&
147
- existsSync(join(papers, "old", STATUS_FILE)) &&
148
- !existsSync(join(papers, "old", "paper.tex")) &&
149
- read(papers, "old", "paper.md") === "# Old\n",
150
- );
151
- check(
152
- "wantedFiles asks for a source only when neither format is there",
153
- wantedFiles(join(papers, "old"), "tex").join() ===
154
- `${STATUS_FILE},paperlint.json` &&
155
- wantedFiles(join(papers, "nowhere"), "md").join() ===
156
- `${STATUS_FILE},paper.md,paperlint.json`,
157
- );
158
139
  writeFileSync(join(papers, "afile"), "x");
159
140
  check(
160
141
  "a name taken by a FILE is refused, not written through",
161
- newPaper(papers, "afile", "tex", { venues: [] }).ok === false,
142
+ newPaper(papers, "afile", { venues: [] }).ok === false,
162
143
  );
163
144
 
164
145
  // ── the project's template wins, file by file ─────────────────────────────────────────
@@ -167,7 +148,7 @@ try {
167
148
  join(papers, OVERRIDE_DIR, STATUS_FILE),
168
149
  "---\n---\n# House scorecard for {{name}}\n",
169
150
  );
170
- const own = newPaper(papers, "house", "md", { venues: [] });
151
+ const own = newPaper(papers, "house", { venues: [] });
171
152
  // Guards: the override slot — `fromTemplate` in new-paper.ts reads `<papers>/.template/<file>`
172
153
  // before the package's copy. Replace that lookup with the package's alone and this goes red.
173
154
  check(
@@ -176,11 +157,11 @@ try {
176
157
  read(papers, "house", STATUS_FILE) ===
177
158
  "---\n---\n# House scorecard for house\n" &&
178
159
  own.files.find((f) => f.file === STATUS_FILE)?.from === "project" &&
179
- own.files.find((f) => f.file === "paper.md")?.from === "package",
160
+ own.files.find((f) => f.file === SOURCE_FILE)?.from === "package",
180
161
  );
181
162
 
182
163
  // ── a missing template is a broken install, not an empty file ─────────────────────────
183
- const broken = newPaper(join(work, "b"), "x", "tex", {
164
+ const broken = newPaper(join(work, "b"), "x", {
184
165
  venues: [],
185
166
  packageTemplates: join(work, "no-such-dir"),
186
167
  });
@@ -41,7 +41,7 @@ const settingsOf = (dir: string) =>
41
41
  describe("paperlint new — paperlint.json", () => {
42
42
  it("is always written, from the package template: no venue chosen yet, and valid", () => {
43
43
  const papers = join(tmp(), "papers");
44
- const r = newPaper(papers, "demo", "tex", { venues: [] });
44
+ const r = newPaper(papers, "demo", { venues: [] });
45
45
  expect(r.ok && r.files.find((f) => f.file === "paperlint.json")).toEqual({
46
46
  file: "paperlint.json",
47
47
  status: "created",
@@ -60,6 +60,7 @@ describe("paperlint new — paperlint.json", () => {
60
60
  identity: null,
61
61
  talk: null,
62
62
  submission: null,
63
+ cycles: null,
63
64
  },
64
65
  });
65
66
  });
@@ -71,7 +72,7 @@ describe("paperlint new — paperlint.json", () => {
71
72
  join(papers, OVERRIDE_DIR, "paperlint.json"),
72
73
  '{ "extends": "paperlint:aisec", "kind": "research" }\n',
73
74
  );
74
- const r = newPaper(papers, "house", "tex", { venues: [] });
75
+ const r = newPaper(papers, "house", { venues: [] });
75
76
  expect(r.ok && r.files.find((f) => f.file === "paperlint.json")?.from).toBe(
76
77
  "project",
77
78
  );
@@ -88,7 +89,7 @@ describe("paperlint new — paperlint.json", () => {
88
89
  join(papers, "p", "paperlint.json"),
89
90
  '{"extends":"paperlint:aisec"}',
90
91
  );
91
- const r = newPaper(papers, "p", "tex", { venues: [] });
92
+ const r = newPaper(papers, "p", { venues: [] });
92
93
  expect(
93
94
  r.ok && r.files.find((f) => f.file === "paperlint.json")?.status,
94
95
  ).toBe("kept");
@@ -105,7 +106,7 @@ describe("paperlint lint — a paper with no venue preset chosen", () => {
105
106
  join(root, "package.json"),
106
107
  JSON.stringify({ name: "c", private: true }),
107
108
  );
108
- newPaper(join(root, "papers"), "demo", "tex", { venues: [] });
109
+ newPaper(join(root, "papers"), "demo", { venues: [] });
109
110
  if (extendsValue !== null)
110
111
  writeFileSync(
111
112
  join(root, "papers", "demo", "paperlint.json"),
@@ -487,7 +488,7 @@ describe("a project's own template", () => {
487
488
  mkdirSync(join(papers, OVERRIDE_DIR), { recursive: true });
488
489
  const src = join(papers, OVERRIDE_DIR, "paperlint.json");
489
490
  writeFileSync(src, '{\n // our house venue\n "extends": null\n}\n');
490
- const r = newPaper(papers, "p", "tex", {
491
+ const r = newPaper(papers, "p", {
491
492
  venues: [],
492
493
  venue: { extends: "paperlint:aisec", kind: null },
493
494
  });
@@ -503,7 +504,7 @@ describe("a project's own template", () => {
503
504
  mkdirSync(join(papers, OVERRIDE_DIR), { recursive: true });
504
505
  writeFileSync(join(papers, OVERRIDE_DIR, "paperlint.json"), "{}\n");
505
506
  const lines = reportNewPaper(
506
- newPaper(papers, "p", "tex", { venues: [] }),
507
+ newPaper(papers, "p", { venues: [] }),
507
508
  (p) => p,
508
509
  );
509
510
  expect(lines).toContain(
package/src/new-paper.ts CHANGED
@@ -8,8 +8,7 @@
8
8
  * Every rule below is taken from a tool that already solved it (docs/prior-art/paper-folder-scaffolding.md):
9
9
  * - the template is a FILE with a lookup order — the project's `<papers>/.template/<file>` first,
10
10
  * the package's `templates/paper/<file>` second (Hugo's archetypes);
11
- * - it NEVER overwrites: on an existing folder it adds only the missing required files, which is
12
- * also the migration for an old folder that has only `paper.md` (`cargo init`);
11
+ * - it NEVER overwrites: on an existing folder it adds only the missing required files (`cargo init`);
13
12
  * - the name is validated, because it becomes a path and, through the hooks, a shell argument.
14
13
  *
15
14
  * The only substitution is `{{name}}` → the folder name. Nothing else in a template is touched.
@@ -33,10 +32,6 @@ import {
33
32
  type NamedVenue,
34
33
  } from "./domain/venue-name.ts";
35
34
 
36
- export type PaperFormat = "tex" | "md";
37
- export const FORMATS: readonly PaperFormat[] = ["tex", "md"];
38
- export const DEFAULT_FORMAT: PaperFormat = "tex";
39
-
40
35
  /** The package's default templates. Read from disk, so they ship in the tarball (`files`). */
41
36
  export const PACKAGE_TEMPLATES = fileURLToPath(
42
37
  new URL("../templates/paper/", import.meta.url),
@@ -50,17 +45,12 @@ export const PACKAGE_TEMPLATES = fileURLToPath(
50
45
  export const OVERRIDE_DIR = ".template";
51
46
 
52
47
  export const STATUS_FILE = "PIPELINE-STATUS.md";
53
- export const SOURCE_FILE: Readonly<Record<PaperFormat, string>> = {
54
- tex: "paper.tex",
55
- md: "paper.md",
56
- };
48
+ /** A paper's source: LaTeX, one file at the paper's root. */
49
+ export const SOURCE_FILE = "paper.tex";
57
50
 
58
51
  /** The flag that lets `paperlint new` create a folder whose name names a venue. */
59
52
  export const ALLOW_VENUE_NAME = "--allow-venue-name";
60
53
 
61
- export const isFormat = (v: unknown): v is PaperFormat =>
62
- FORMATS.some((f) => f === v);
63
-
64
54
  /**
65
55
  * Why a name is refused, or null. `[a-z0-9._-]+` is the charset; a LEADING dot is refused on top
66
56
  * of it, because discovery skips dot-directories — a paper named `.x` would be created and then
@@ -109,20 +99,12 @@ export type NewPaperResult =
109
99
  }
110
100
  | { readonly ok: false; readonly reason: string };
111
101
 
112
- /**
113
- * The files a paper folder must end up with: the scorecard, a source in `format` — unless it
114
- * already has a source in EITHER format, in which case that one stands — and `paperlint.json`.
115
- */
116
- export function wantedFiles(dir: string, format: PaperFormat): string[] {
117
- return [...scorecardAndSource(dir, format), CONFIG_FILE];
118
- }
119
-
120
- function scorecardAndSource(dir: string, format: PaperFormat): string[] {
121
- const hasSource = Object.values(SOURCE_FILE).some((f) =>
122
- existsSync(join(dir, f)),
123
- );
124
- return hasSource ? [STATUS_FILE] : [STATUS_FILE, SOURCE_FILE[format]];
125
- }
102
+ /** The files a paper folder must end up with: the scorecard, the source and `paperlint.json`. */
103
+ export const WANTED_FILES: readonly string[] = [
104
+ STATUS_FILE,
105
+ SOURCE_FILE,
106
+ CONFIG_FILE,
107
+ ];
126
108
 
127
109
  /** The venue `paperlint new --venue` chose: what goes into the new paper's `paperlint.json`. */
128
110
  export interface VenueSetting {
@@ -195,7 +177,7 @@ function fromTemplate(
195
177
  return { ok: false, reason: `no template for ${file}: ${src} is missing` };
196
178
  const text = readFileSync(src, "utf8").split("{{name}}").join(name);
197
179
  const setClass = venue?.setClass ?? null;
198
- if (file === SOURCE_FILE.tex && setClass !== null)
180
+ if (file === SOURCE_FILE && setClass !== null)
199
181
  return { ok: true, value: { file, text: setClass(text), from } };
200
182
  if (file !== CONFIG_FILE || venue === null)
201
183
  return { ok: true, value: { file, text, from } };
@@ -217,7 +199,6 @@ export interface NewPaperOptions {
217
199
  export function newPaper(
218
200
  papersRoot: string,
219
201
  name: string,
220
- format: PaperFormat,
221
202
  opts: NewPaperOptions,
222
203
  ): NewPaperResult {
223
204
  const { packageTemplates = PACKAGE_TEMPLATES, venue = null } = opts;
@@ -230,7 +211,7 @@ export function newPaper(
230
211
 
231
212
  const files: FileOutcome[] = [];
232
213
  const toWrite: Planned[] = [];
233
- for (const file of wantedFiles(dir, format)) {
214
+ for (const file of WANTED_FILES) {
234
215
  if (existsSync(join(dir, file))) {
235
216
  files.push({ file, status: "kept" });
236
217
  continue;
@@ -20,6 +20,7 @@ import { tmpdir } from "node:os";
20
20
  import { join } from "node:path";
21
21
  import { afterEach, describe, expect, it } from "vitest";
22
22
  import { buildConfig, eslintConfig, run } from "./cli.ts";
23
+ import { texLanguage } from "#eslint-rules/latex-language";
23
24
  import {
24
25
  narrowToOwners,
25
26
  ownedPatterns,
@@ -247,7 +248,7 @@ describe("`rules` blocks — where the rule lands", () => {
247
248
  },
248
249
  ],
249
250
  },
250
- null,
251
+ texLanguage,
251
252
  ),
252
253
  ),
253
254
  });
@@ -257,9 +258,9 @@ describe("`rules` blocks — where the rule lands", () => {
257
258
  const status = await on("papers/a/PIPELINE-STATUS.md");
258
259
  expect(status["paper/source"]).toEqual([1]);
259
260
  expect(status["paper/section-word"]).toBeUndefined();
260
- const draft = await on("papers/a/paper.md");
261
- expect(draft["paper/section-word"]).toEqual([0]);
262
- expect(draft["paper/source"]).toBeUndefined();
261
+ const source = await on("papers/a/paper.tex");
262
+ expect(source["paper/section-word"]).toEqual([0]);
263
+ expect(source["paper/source"]).toBeUndefined();
263
264
  });
264
265
  });
265
266
 
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * THE FILES PAPERLINT OWNS — read off its own config, and made the only thing ESLint may reach.
3
3
  *
4
- * paperlint's blocks name the files they lint (`PIPELINE-STATUS.md`, `paper.md`, `paper.tex`,
5
- * `reviews/*.md`, …). That list is the whole scope of `paperlint lint`, and this module turns it
4
+ * paperlint's blocks name the files they lint (`PIPELINE-STATUS.md`, `paper.tex`, `reviews/*.md`,
5
+ * …). That list is the whole scope of `paperlint lint`, and this module turns it
6
6
  * into two things ESLint understands:
7
7
  *
8
8
  * 1. **A global ignore of everything else** (`scopeToOwned`). Handing ESLint a directory is not
@@ -17,7 +17,7 @@
17
17
  * 2. **Each rule scoped to the files it was written for** (`ruleOwners` + `narrowToOwners`).
18
18
  * One plugin NAME is bound to different plugin objects in different blocks: `paper` holds
19
19
  * `stages`/`source`/`author-list` beside `PIPELINE-STATUS.md` and `typography` beside
20
- * `paper.md`/`paper.tex`. So a consumer block that named `paper/source`
20
+ * `paper.tex`. So a consumer block that named `paper/source`
21
21
  * for every file under the papers directory reached `paper.tex`, where `paper` has no `source`
22
22
  * — and ESLint threw `Could not find "source" in plugin "paper"` from inside `lintFiles`,
23
23
  * after paperlint's own validation had (correctly) accepted the id. A block is therefore split
@@ -36,7 +36,7 @@
36
36
  * - a global-ignore pattern that matches a DIRECTORY hides everything inside it, so directories
37
37
  * must be un-ignored (`!` + a pattern ending in a slash) before owned files can be;
38
38
  * - that un-ignores `node_modules/` and `.git/` too — ESLint's own defaults — so they are
39
- * re-ignored after, or a `paper.md` inside a dependency would be linted;
39
+ * re-ignored after, or a `paper.tex` inside a dependency would be linted;
40
40
  * - later global-ignore patterns win, across blocks, in config order;
41
41
  * - `files: [["a", "b"]]` matches a file only when BOTH patterns match.
42
42
  */
@@ -0,0 +1,49 @@
1
+ import { expect, it } from "vitest";
2
+ import { latexReader } from "./adapters/latex/index.ts";
3
+ import { memoryFiles } from "./adapters/memory/index.ts";
4
+ import { paperProse } from "./paper-prose.ts";
5
+
6
+ const SRC = [
7
+ "\\documentclass{article}",
8
+ "\\usepackage{amsmath} % a preamble word count of its own",
9
+ "\\title{A Title}",
10
+ "\\begin{document}",
11
+ "\\maketitle",
12
+ "\\section{Introduction}",
13
+ "We measure it~\\cite{a}. % a comment",
14
+ "",
15
+ "A second paragraph \\input{body}",
16
+ "\\begin{filecontents*}{refs.bib}",
17
+ "@misc{a, author = {Ada Lovelace}, title = {Never Prose}}",
18
+ "\\end{filecontents*}",
19
+ "\\end{document}",
20
+ ].join("\n");
21
+
22
+ it("the body's prose, a blank line between passages, includes spliced; the preamble, comments, the inline .bib and the title stay out", () => {
23
+ const files = memoryFiles({
24
+ "/p/paper.tex": SRC,
25
+ "/p/body.tex": "\\subsection{Method}\nends here.",
26
+ });
27
+ expect(
28
+ paperProse("/p/paper.tex", SRC, { files, latex: latexReader }),
29
+ ).toEqual({
30
+ // A citation stands as `[1]`, where it is set; a heading — here an included one — ends a passage.
31
+ body: "We measure it [1].\n\nA second paragraph\n\nends here.",
32
+ headings: ["Introduction", "Method"],
33
+ files: [
34
+ { file: "paper.tex", text: SRC },
35
+ { file: "body.tex", text: "\\subsection{Method}\nends here." },
36
+ ],
37
+ });
38
+ });
39
+
40
+ it("a citation stands as `[1]` and a cross-reference as `1`, so the sentence still ends on them", () => {
41
+ const src =
42
+ "\\documentclass{article}\n\\begin{document}\nIt holds~\\cite{a}. It is shown (Section~\\ref{s}).\n\\end{document}\n";
43
+ expect(
44
+ paperProse("/p/paper.tex", src, {
45
+ files: memoryFiles({ "/p/paper.tex": src }),
46
+ latex: latexReader,
47
+ }).body,
48
+ ).toBe("It holds [1]. It is shown (Section 1).");
49
+ });
@@ -0,0 +1,67 @@
1
+ /**
2
+ * THE PROSE OF A paper.tex AS PLAIN TEXT — what `grade-paper-writing/prose-lint.mjs` measures.
3
+ *
4
+ * prose-lint counts sentences and words, and every metric it prints is a fraction of the word
5
+ * count. Read as raw text, a `.tex` puts the preamble, the `%` comments and an inline `filecontents`
6
+ * bibliography into that count (8127 "words" against about 4636 real ones on one paper), so every
7
+ * rate comes out roughly halved. This reads the paper the way the `tex/register` rule does: the
8
+ * includes spliced in (`readPaper`), then the body's prose as passages (`bodyProse` — the abstract
9
+ * to the back matter, without headings, captions, floats or code), each passage's sentences
10
+ * (`bodySentences`). A blank line separates two passages, so no sentence runs across them.
11
+ *
12
+ * A citation stands as `[1]` and a cross-reference as `1`, where they are set — not left out:
13
+ * prose-lint judges a sentence that ENDS on one (a wasted stress position) and counts references
14
+ * to other parts of the paper (`(Section 1)`), and both are invisible in a sentence the marks
15
+ * were cut out of. Math is left out; prose-lint counts no formula.
16
+ */
17
+ import { bodySentences } from "./domain/register.ts";
18
+ import { CITATION, MATH, REFERENCE } from "./domain/sentences.ts";
19
+ import { runText } from "./domain/tex-document.ts";
20
+ import { readPaper, type PaperDeps } from "./tex-paper.ts";
21
+
22
+ /** A sentence's text with each mark as a reader would see it set, and math left out. */
23
+ const rendered = (raw: string): string =>
24
+ raw
25
+ .replaceAll(CITATION, "[1]")
26
+ .replaceAll(REFERENCE, "1")
27
+ .replaceAll(MATH, "")
28
+ .replace(/\s+/gu, " ")
29
+ .trim();
30
+
31
+ /**
32
+ * A paper's body prose, one passage per paragraph, its section titles in document order, and the
33
+ * text of each file it is made of — `paper.tex` and every file it includes, once, named as the
34
+ * include names it — for what the body leaves out, such as captions.
35
+ */
36
+ export interface PaperProse {
37
+ readonly body: string;
38
+ readonly headings: readonly string[];
39
+ readonly files: readonly { readonly file: string; readonly text: string }[];
40
+ }
41
+
42
+ /** The prose and the headings of the paper whose main file is `filename` with the text `src`. */
43
+ export function paperProse(
44
+ filename: string,
45
+ src: string,
46
+ deps: PaperDeps,
47
+ ): PaperProse {
48
+ const { text, segments } = readPaper(filename, src, deps);
49
+ const passages = deps.latex
50
+ .bodyProse(text)
51
+ .map((p) =>
52
+ bodySentences([p])
53
+ .map((s) => rendered(s.raw))
54
+ .join(" "),
55
+ )
56
+ .filter((p) => p !== "");
57
+ return {
58
+ body: passages.join("\n\n"),
59
+ headings: deps.latex
60
+ .headings(text)
61
+ .filter((h) => h.command.kind === "heading" && h.argument === "title")
62
+ .map((h) => runText(h.title).replace(/\s+/g, " ").trim()),
63
+ files: [...new Map(segments.map((s) => [s.file, s.source])).entries()].map(
64
+ ([file, text]) => ({ file, text }),
65
+ ),
66
+ };
67
+ }
@@ -33,6 +33,7 @@ describe("parsePaperSettings", () => {
33
33
  identity: null,
34
34
  talk: null,
35
35
  submission: null,
36
+ cycles: null,
36
37
  },
37
38
  });
38
39
  expect(parsePaperSettings({})).toEqual({
@@ -45,6 +46,7 @@ describe("parsePaperSettings", () => {
45
46
  identity: null,
46
47
  talk: null,
47
48
  submission: null,
49
+ cycles: null,
48
50
  },
49
51
  });
50
52
  });
@@ -60,6 +62,7 @@ describe("parsePaperSettings", () => {
60
62
  identity: null,
61
63
  talk: null,
62
64
  submission: null,
65
+ cycles: null,
63
66
  },
64
67
  });
65
68
  });
@@ -164,6 +167,7 @@ const UNSET = {
164
167
  identity: null,
165
168
  talk: null,
166
169
  submission: null,
170
+ cycles: null,
167
171
  };
168
172
 
169
173
  describe("readPaperSettings — the root paperlint.json's defaults, the paper's file over them", () => {
@@ -220,8 +224,14 @@ describe("readPaperSettings — the root paperlint.json's defaults, the paper's
220
224
  identity: null,
221
225
  talk: null,
222
226
  submission: null,
227
+ cycles: null,
223
228
  });
224
229
  });
230
+ });
231
+
232
+ describe("readPaperSettings — a project key in a paper's file", () => {
233
+ const withFiles = (f: Record<string, string>) =>
234
+ memoryFiles({ "/work/package.json": "{}", ...f });
225
235
 
226
236
  it("papersDir in a paper's file: broken, naming the root file as its place", () => {
227
237
  const r = readPaperSettings(
@@ -264,6 +274,7 @@ describe("paperRules — `rules` in paperlint.json", () => {
264
274
  identity: null,
265
275
  talk: null,
266
276
  submission: null,
277
+ cycles: null,
267
278
  });
268
279
 
269
280
  it("no rules: none", () => {
@@ -352,3 +363,159 @@ describe("talk in a paper's paperlint.json", () => {
352
363
  expect(r.ok ? "" : r.error).toMatch(/"talk.mode"/);
353
364
  });
354
365
  });
366
+
367
+ describe("parsePaperSettings — `cycles`: the current attempt supplies extends, kind and submission", () => {
368
+ const closed = {
369
+ id: "alpha-2026",
370
+ venue: { kind: "preset", extends: "paperlint:acm-sigconf" },
371
+ kind: "short",
372
+ opened: "2026-07-01",
373
+ outcome: {
374
+ kind: "rejected",
375
+ date: "2026-09-08",
376
+ desk: true,
377
+ evidence: "reviews/alpha.md",
378
+ },
379
+ };
380
+ const open = {
381
+ id: "beta-2027",
382
+ venue: { kind: "preset", extends: "paperlint:ieee-conference" },
383
+ kind: "technical",
384
+ opened: "2026-09-09",
385
+ submission: { id: 52 },
386
+ outcome: { kind: "open" },
387
+ };
388
+ const settingsOf = (json: unknown) => {
389
+ const r = parsePaperSettings(json);
390
+ if (!r.ok) throw new Error(r.error);
391
+ return r.value;
392
+ };
393
+
394
+ it("an open cycle: its venue, kind and submission are the paper's", () => {
395
+ const s = settingsOf({ cycles: [closed, open] });
396
+ expect(s.extends).toBe("paperlint:ieee-conference");
397
+ expect(s.kind).toBe("technical");
398
+ expect(s.submission).toEqual({ id: 52 });
399
+ expect(s.cycles?.current.kind).toBe("cycle");
400
+ expect(s.cycles?.list.map((c) => c.id)).toEqual([
401
+ "alpha-2026",
402
+ "beta-2027",
403
+ ]);
404
+ });
405
+
406
+ it("parked (the last cycle closed) and none (`cycles: []`): no venue, as `extends: null` reads", () => {
407
+ const parked = settingsOf({ cycles: [closed] });
408
+ expect([parked.extends, parked.kind, parked.submission]).toEqual([
409
+ null,
410
+ null,
411
+ null,
412
+ ]);
413
+ expect(parked.cycles?.current.kind).toBe("parked");
414
+ expect(settingsOf({ cycles: [] }).cycles?.current).toEqual({
415
+ kind: "none",
416
+ });
417
+ });
418
+ });
419
+
420
+ describe("parsePaperSettings — `cycles`, closed and named", () => {
421
+ const closed = {
422
+ id: "alpha-2026",
423
+ venue: { kind: "preset", extends: "paperlint:acm-sigconf" },
424
+ kind: "short",
425
+ opened: "2026-07-01",
426
+ outcome: { kind: "withdrawn", date: "2026-09-08", evidence: "n.md" },
427
+ };
428
+ const open = {
429
+ id: "beta-2027",
430
+ venue: { kind: "preset", extends: "paperlint:ieee-conference" },
431
+ kind: "technical",
432
+ opened: "2026-09-09",
433
+ };
434
+ const settingsOf = (json: unknown) => {
435
+ const r = parsePaperSettings(json);
436
+ if (!r.ok) throw new Error(r.error);
437
+ return r.value;
438
+ };
439
+
440
+ it("an accepted last cycle keeps its venue and kind: camera-ready and talk are judged against it", () => {
441
+ const accepted = { kind: "accepted", date: "2026-08-21", evidence: "r.md" };
442
+ const s = settingsOf({ cycles: [{ ...closed, outcome: accepted }] });
443
+ expect([s.extends, s.kind, s.cycles?.current.kind]).toEqual([
444
+ "paperlint:acm-sigconf",
445
+ "short",
446
+ "accepted",
447
+ ]);
448
+ });
449
+
450
+ it("a named venue (no preset yet) resolves no preset: extends is null, the record stays", () => {
451
+ const venue = {
452
+ kind: "named",
453
+ name: "X '27",
454
+ url: "https://example.org/c",
455
+ };
456
+ const s = settingsOf({ cycles: [{ ...open, venue }] });
457
+ expect([s.extends, s.kind]).toEqual([null, "technical"]);
458
+ });
459
+ });
460
+
461
+ describe("parsePaperSettings — `cycles` refused", () => {
462
+ const open = {
463
+ id: "beta-2027",
464
+ venue: { kind: "preset", extends: "paperlint:ieee-conference" },
465
+ opened: "2026-09-09",
466
+ };
467
+ const alsoOpen = {
468
+ id: "alpha-2026",
469
+ venue: { kind: "preset", extends: "paperlint:acm-sigconf" },
470
+ opened: "2026-07-01",
471
+ };
472
+
473
+ it("one derived key beside `cycles` is refused in the singular", () => {
474
+ const error: unknown = expect.stringMatching(
475
+ /^"kind" beside "cycles": with cycles, it is the current attempt's/,
476
+ );
477
+ expect(parsePaperSettings({ kind: "short", cycles: [open] })).toEqual({
478
+ ok: false,
479
+ error,
480
+ });
481
+ });
482
+
483
+ it("a derived key written beside `cycles` is refused, naming it — two sources for one fact", () => {
484
+ const error: unknown = expect.stringMatching(
485
+ /^"extends", "kind" beside "cycles": with cycles, they are the current attempt's/,
486
+ );
487
+ expect(
488
+ parsePaperSettings({
489
+ extends: "paperlint:acm-sigconf",
490
+ kind: "short",
491
+ cycles: [open],
492
+ }),
493
+ ).toEqual({ ok: false, error });
494
+ });
495
+
496
+ it("two open cycles is refused as the file's problem: dual submission has no current venue", () => {
497
+ const error: unknown = expect.stringMatching(
498
+ /are both open — a paper is on one attempt/,
499
+ );
500
+ expect(parsePaperSettings({ cycles: [alsoOpen, open] })).toEqual({
501
+ ok: false,
502
+ error,
503
+ });
504
+ });
505
+
506
+ it("a malformed cycle is refused with the entry and key, through readPaperSettings as `broken`", () => {
507
+ const files = memoryFiles({
508
+ "/work/package.json": "{}",
509
+ [`${PAPER}/paperlint.json`]: JSON.stringify({
510
+ cycles: [{ ...open, opened: "soon" }],
511
+ }),
512
+ });
513
+ const why: unknown = expect.stringMatching(
514
+ /^cycles\[0\]: "opened" must be a date/,
515
+ );
516
+ expect(readPaperSettings(files, PAPER)).toEqual({
517
+ ok: false,
518
+ error: { kind: "broken", why },
519
+ });
520
+ });
521
+ });