paperlint 12.0.0 → 13.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 (147) hide show
  1. package/CONTRIBUTING.md +2 -1
  2. package/dist/adapters/memory/index.d.ts +5 -0
  3. package/dist/adapters/memory/index.d.ts.map +1 -1
  4. package/dist/adapters/memory/index.js +16 -0
  5. package/dist/adapters/memory/index.js.map +1 -1
  6. package/dist/adapters/node/index.d.ts +1 -0
  7. package/dist/adapters/node/index.d.ts.map +1 -1
  8. package/dist/adapters/node/index.js +1 -0
  9. package/dist/adapters/node/index.js.map +1 -1
  10. package/dist/adapters/node/move-file.io.d.ts +4 -0
  11. package/dist/adapters/node/move-file.io.d.ts.map +1 -0
  12. package/dist/adapters/node/move-file.io.js +12 -0
  13. package/dist/adapters/node/move-file.io.js.map +1 -0
  14. package/dist/adapters/yaml/index.d.ts +3 -0
  15. package/dist/adapters/yaml/index.d.ts.map +1 -0
  16. package/dist/adapters/yaml/index.js +17 -0
  17. package/dist/adapters/yaml/index.js.map +1 -0
  18. package/dist/cli.d.ts.map +1 -1
  19. package/dist/cli.js +48 -7
  20. package/dist/cli.js.map +1 -1
  21. package/dist/cycle-rules.d.ts +3 -3
  22. package/dist/cycle-rules.d.ts.map +1 -1
  23. package/dist/cycle-rules.js +1 -2
  24. package/dist/cycle-rules.js.map +1 -1
  25. package/dist/domain/cycle.d.ts +16 -2
  26. package/dist/domain/cycle.d.ts.map +1 -1
  27. package/dist/domain/cycle.js +21 -42
  28. package/dist/domain/cycle.js.map +1 -1
  29. package/dist/domain/paper-layout.d.ts.map +1 -1
  30. package/dist/domain/paper-layout.js +5 -2
  31. package/dist/domain/paper-layout.js.map +1 -1
  32. package/dist/domain/paths.d.ts +12 -0
  33. package/dist/domain/paths.d.ts.map +1 -1
  34. package/dist/domain/paths.js +22 -1
  35. package/dist/domain/paths.js.map +1 -1
  36. package/dist/domain/scaffold-layout.js +1 -1
  37. package/dist/domain/versions-migration.d.ts +51 -0
  38. package/dist/domain/versions-migration.d.ts.map +1 -0
  39. package/dist/domain/versions-migration.js +252 -0
  40. package/dist/domain/versions-migration.js.map +1 -0
  41. package/dist/domain/versions.d.ts +167 -0
  42. package/dist/domain/versions.d.ts.map +1 -0
  43. package/dist/domain/versions.js +457 -0
  44. package/dist/domain/versions.js.map +1 -0
  45. package/dist/eslint-rules/paper-stages.d.ts +7 -0
  46. package/dist/eslint-rules/paper-stages.d.ts.map +1 -1
  47. package/dist/eslint-rules/paper-stages.js +62 -7
  48. package/dist/eslint-rules/paper-stages.js.map +1 -1
  49. package/dist/eslint-rules/rule-context.d.ts +2 -0
  50. package/dist/eslint-rules/rule-context.d.ts.map +1 -1
  51. package/dist/ports/move-file.d.ts +7 -0
  52. package/dist/ports/move-file.d.ts.map +1 -0
  53. package/dist/ports/move-file.js +2 -0
  54. package/dist/ports/move-file.js.map +1 -0
  55. package/dist/ports/yaml.d.ts +5 -0
  56. package/dist/ports/yaml.d.ts.map +1 -0
  57. package/dist/ports/yaml.js +2 -0
  58. package/dist/ports/yaml.js.map +1 -0
  59. package/dist/submission.d.ts +2 -0
  60. package/dist/submission.d.ts.map +1 -1
  61. package/dist/submission.js +49 -9
  62. package/dist/submission.js.map +1 -1
  63. package/dist/version-rules.d.ts +16 -0
  64. package/dist/version-rules.d.ts.map +1 -0
  65. package/dist/version-rules.js +201 -0
  66. package/dist/version-rules.js.map +1 -0
  67. package/dist/versions-command.d.ts +37 -0
  68. package/dist/versions-command.d.ts.map +1 -0
  69. package/dist/versions-command.js +209 -0
  70. package/dist/versions-command.js.map +1 -0
  71. package/dist/versions.d.ts +76 -0
  72. package/dist/versions.d.ts.map +1 -0
  73. package/dist/versions.js +164 -0
  74. package/dist/versions.js.map +1 -0
  75. package/docs/configuration.md +76 -2
  76. package/docs/layout.md +5 -5
  77. package/docs/rules/cycle/camera-ready-source.md +62 -0
  78. package/docs/rules/cycle/draft-drift.md +47 -0
  79. package/docs/rules/cycle/port-done.md +2 -2
  80. package/docs/rules/cycle/record.md +2 -1
  81. package/docs/rules/cycle/submitted-frozen.md +65 -0
  82. package/docs/rules/paper/allowed-files-only.md +1 -1
  83. package/docs/rules/versions/layout.md +88 -0
  84. package/docs/rules.md +25 -11
  85. package/docs/submission.md +21 -3
  86. package/eslint-rules/paper-stages.test.mjs +62 -0
  87. package/eslint-rules/paper-stages.ts +67 -8
  88. package/eslint-rules/prereg-coverage.test.ts +1 -0
  89. package/eslint-rules/rule-context.ts +2 -0
  90. package/package.json +2 -1
  91. package/presets/agent.jsonc +75 -0
  92. package/presets/agent.md +70 -0
  93. package/presets/agentverify.jsonc +72 -0
  94. package/presets/agentverify.md +85 -0
  95. package/presets/ieee-conference.md +1 -1
  96. package/presets/nlbse.jsonc +76 -0
  97. package/presets/nlbse.md +85 -0
  98. package/scripts/coverage-config.test.ts +3 -0
  99. package/skills/CLAUDE.md +2 -2
  100. package/skills/CLAUDE.md.spec.ts +1 -1
  101. package/skills/paper-pipeline/references/pipeline-status-template.md +4 -1
  102. package/skills/submit-paper/SKILL.md +18 -10
  103. package/skills/submit-paper/SKILL.md.spec.ts +17 -9
  104. package/skills/submit-paper/references/publishers/acm.md +5 -3
  105. package/src/adapters/memory/index.test.ts +22 -0
  106. package/src/adapters/memory/index.ts +21 -0
  107. package/src/adapters/node/index.ts +1 -0
  108. package/src/adapters/node/move-file.io.ts +21 -0
  109. package/src/adapters/node/move-file.test.ts +51 -0
  110. package/src/adapters/yaml/index.test.ts +16 -0
  111. package/src/adapters/yaml/index.ts +17 -0
  112. package/src/cli.test.ts +43 -0
  113. package/src/cli.ts +58 -6
  114. package/src/cycle-rules.ts +4 -5
  115. package/src/domain/cycle.test.ts +27 -0
  116. package/src/domain/cycle.ts +38 -6
  117. package/src/domain/paper-layout.ts +5 -2
  118. package/src/domain/paths.test.ts +28 -1
  119. package/src/domain/paths.ts +34 -1
  120. package/src/domain/scaffold-layout.ts +1 -1
  121. package/src/domain/versions-migration.test.ts +410 -0
  122. package/src/domain/versions-migration.ts +429 -0
  123. package/src/domain/versions.test.ts +564 -0
  124. package/src/domain/versions.ts +706 -0
  125. package/src/paper-settings-commands.test.ts +1 -1
  126. package/src/ports/move-file.ts +8 -0
  127. package/src/ports/yaml.ts +5 -0
  128. package/src/preset-card-rule.test.ts +3 -0
  129. package/src/presets.test.ts +55 -31
  130. package/src/rule-cases.test.ts +157 -8
  131. package/src/submission-freeze.test.ts +320 -0
  132. package/src/submission-new.test.ts +11 -1
  133. package/src/submission.ts +82 -11
  134. package/src/tex-venue-rules.test.ts +3 -0
  135. package/src/venue-rules.test.ts +2 -2
  136. package/src/version-rules.test.ts +222 -0
  137. package/src/version-rules.ts +282 -0
  138. package/src/versions-command.test.ts +594 -0
  139. package/src/versions-command.ts +360 -0
  140. package/src/versions.ts +286 -0
  141. package/dist/eslint-rules/cycle-stage.d.ts +0 -39
  142. package/dist/eslint-rules/cycle-stage.d.ts.map +0 -1
  143. package/dist/eslint-rules/cycle-stage.js +0 -119
  144. package/dist/eslint-rules/cycle-stage.js.map +0 -1
  145. package/docs/rules/cycle/stage.md +0 -85
  146. package/eslint-rules/cycle-stage.test.mjs +0 -131
  147. package/eslint-rules/cycle-stage.ts +0 -141
@@ -0,0 +1,282 @@
1
+ /**
2
+ * THE RULES OVER A PAPER'S FROZEN VERSIONS — the convention `versions/<cycle>/<stage>.{pdf,tex}` and
3
+ * its lock (src/domain/versions.ts):
4
+ *
5
+ * versions/layout error on PIPELINE-STATUS.md: every entry of versions/ is a cycle's
6
+ * folder or the lock, every file in a folder a stage's .pdf or
7
+ * .tex, every PDF with its source, nothing sent from a cycle still
8
+ * porting, every file as the lock recorded it
9
+ * cycle/submitted-frozen error on PIPELINE-STATUS.md: a cycle with a submission on the portal
10
+ * has `versions/<id>/submitted.pdf` (#308)
11
+ * cycle/draft-drift warn on paper.tex: the draft has moved on from the open cycle's
12
+ * frozen source, by so many lines
13
+ * cycle/camera-ready-source error on paper.tex: the last cycle is accepted and its camera-ready is
14
+ * frozen, and that camera-ready keeps less than half of the
15
+ * accepted source's lines — silent until the camera-ready is frozen
16
+ *
17
+ * ── WHY THE FOLDER IS LISTED AND NOT LINTED ─────────────────────────────────────────────────
18
+ * A frozen PDF is never handed to ESLint, and a frozen `.tex` is kept out of the linted set on purpose
19
+ * (src/paper-includes.ts): a rule fired per file could not see either. The folder is listed once per
20
+ * paper, from the scorecard every paper has, as `paper/allowed-files-only` lists the paper's root.
21
+ *
22
+ * ── WHO SPEAKS WHEN THERE IS NOTHING TO JUDGE ───────────────────────────────────────────────
23
+ * A paper that keeps no `cycles` (the flat form) has no attempt to name a folder after: all four are
24
+ * silent, and `paper/stages` with `paper/source` judge its `stages:` list as before. A `cycles` record
25
+ * that does not read is refused by `paperlint lint` before any rule runs.
26
+ *
27
+ * ── WHY THE DRIFT IS A WARNING, AND THE CAMERA-READY AN ERROR ───────────────────────────────
28
+ * Between venues the draft is SUPPOSED to move on from what is under review: the warning says how far,
29
+ * so an author answering a reviewer knows the line numbers point into the frozen file. Once the venue
30
+ * accepts, the camera-ready has to be what was accepted plus the changes asked for — building it from
31
+ * a draft that moved on sends the venue a paper its reviewers never read.
32
+ *
33
+ * ── WHY THE CAMERA-READY IS JUDGED ONLY ONCE IT IS FROZEN ───────────────────────────────────
34
+ * While the camera-ready is being edited, paper.tex differs from what was accepted BY DESIGN: the
35
+ * reviewers' changes go in. A check on the draft in those weeks fires on correct work, so the rule waits
36
+ * for `submission update --save` to freeze `camera-ready.{pdf,tex}`, then judges the frozen source:
37
+ * it must keep at least half of the accepted source's lines (`derivesFrom`, src/domain/versions.ts).
38
+ * After the freeze the draft is not judged at all — what the venue holds is fixed, and the draft is
39
+ * free to head for the next paper.
40
+ */
41
+ import { basename, dirname } from "node:path";
42
+ import type { Files } from "./ports/files.ts";
43
+ import {
44
+ derivesFrom,
45
+ driftText,
46
+ frozenKey,
47
+ judgeVersions,
48
+ lineage,
49
+ unfrozenSubmissions,
50
+ venueName,
51
+ type VersionFinding,
52
+ } from "./domain/versions.ts";
53
+ import type {
54
+ FolderRuleContext,
55
+ FolderRuleModule,
56
+ } from "./folder-venue-rule.ts";
57
+ import type { TexRuleModule } from "./tex-venue-rules.ts";
58
+ import { readPaper, type ProseDeps } from "./tex-paper.ts";
59
+ import { rulePageUrl } from "./venue-rules.ts";
60
+ import {
61
+ cyclesIn,
62
+ draftDrift,
63
+ entriesIn,
64
+ factsOfFrozen,
65
+ frozenText,
66
+ hasFrozen,
67
+ readLock,
68
+ versionsDir,
69
+ type VersionsDeps,
70
+ } from "./versions.ts";
71
+
72
+ const STATUS_FILE = "PIPELINE-STATUS.md";
73
+
74
+ const LOCK_COMMAND = "`npx paperlint versions lock <paper>`";
75
+
76
+ const LAYOUT_META: FolderRuleModule["meta"] = {
77
+ type: "suggestion",
78
+ docs: {
79
+ description:
80
+ "versions/ holds a folder per cycle and a file per stage, every PDF with its source, each file as versions/lock.json recorded it",
81
+ url: rulePageUrl("versions/layout"),
82
+ },
83
+ schema: [],
84
+ messages: {
85
+ stray: "`versions/{{path}}` is not part of the layout — {{hint}}",
86
+ unknownCycle:
87
+ "`versions/{{folder}}/` names no cycle in paperlint.json — a folder in versions/ is one attempt, named by its cycle's id ({{known}}): rename it to the attempt it holds, or move it to extra/",
88
+ noSource: `\`versions/{{pdf}}\` is frozen without its source: freeze the .tex it was built from as \`versions/{{tex}}\` and run ${LOCK_COMMAND} — a reviewer's line numbers point into that source, and squash merges and \`git gc\` destroy the commit that held it`,
89
+ porting:
90
+ "`versions/{{file}}` was sent in the cycle «{{id}}», whose phase is still `porting`: a PDF went to the venue while the record says the source is in the previous venue's template — finish the port and drop the phase, or the file is in the wrong folder",
91
+ noLock: `versions/ holds frozen files and no lock.json — ${LOCK_COMMAND} records their bytes and digests; paperlint writes the lock, nobody edits it`,
92
+ badLock: `versions/lock.json: {{why}} — paperlint writes this file: restore it from git, or write it anew with ${LOCK_COMMAND}`,
93
+ notLocked: `\`versions/{{file}}\` is not in versions/lock.json — ${LOCK_COMMAND} records it (a file \`submission … --save\` froze is locked already)`,
94
+ changed: `\`versions/{{file}}\` is not the file the lock recorded on {{frozen}} ({{want}} bytes then, {{got}} now, and another digest) — a frozen file does not change: restore it from git; if it was replaced on purpose, ${LOCK_COMMAND} records it anew`,
95
+ lockedGone: `versions/lock.json records \`{{file}}\`, and the file is not there — a frozen version was deleted: restore it from git; if it went on purpose, ${LOCK_COMMAND} drops the entry`,
96
+ },
97
+ };
98
+
99
+ const FROZEN_META: FolderRuleModule["meta"] = {
100
+ type: "suggestion",
101
+ docs: {
102
+ description:
103
+ "a cycle with a submission on the venue's portal has the PDF it sent frozen as versions/<cycle>/submitted.pdf",
104
+ url: rulePageUrl("cycle/submitted-frozen"),
105
+ },
106
+ schema: [],
107
+ messages: {
108
+ unfrozen: `cycle «{{id}}» has submission {{submission}} on the portal, and \`versions/{{pdf}}\` is not there: what the reviewers read is not frozen. \`npx paperlint submission update <paper> --save\` freezes what it sends; for an upload made before, put that PDF there (\`submission show\` gives its size and sha256) with the source it was built from beside it as submitted.tex, then run ${LOCK_COMMAND}`,
109
+ },
110
+ };
111
+
112
+ /** What the rules on the scorecard read with: the disk and its listings. */
113
+ export type VersionStatusDeps = VersionsDeps;
114
+
115
+ /** The findings of one scorecard rule for the paper folder `dir`; none for a paper without cycles. */
116
+ function statusFindings(
117
+ name: "layout" | "submitted-frozen",
118
+ dir: string,
119
+ deps: VersionStatusDeps,
120
+ ): readonly VersionFinding[] {
121
+ const cycles = cyclesIn(dir, deps.files);
122
+ if (cycles === null) return [];
123
+ const root = versionsDir(dir);
124
+ return name === "layout"
125
+ ? judgeVersions({
126
+ cycles: cycles.list,
127
+ root: entriesIn(root, deps),
128
+ inFolder: (folder) => entriesIn(`${root}/${folder}`, deps),
129
+ lock: readLock(dir, deps.files),
130
+ factsOf: factsOfFrozen(dir, deps.files),
131
+ })
132
+ : unfrozenSubmissions(cycles.list, hasFrozen(dir, deps.files));
133
+ }
134
+
135
+ const statusRule = (
136
+ meta: FolderRuleModule["meta"],
137
+ name: "layout" | "submitted-frozen",
138
+ deps: VersionStatusDeps,
139
+ ): FolderRuleModule => ({
140
+ meta,
141
+ create(context: FolderRuleContext) {
142
+ if (basename(context.filename) !== STATUS_FILE) return {};
143
+ return {
144
+ root() {
145
+ const at = context.sourceCode.getLocFromIndex(0);
146
+ statusFindings(name, dirname(context.filename), deps).forEach((f) => {
147
+ context.report({ loc: { start: at, end: at }, ...f });
148
+ });
149
+ },
150
+ };
151
+ },
152
+ });
153
+
154
+ /** The `versions` plugin's rule, on every `PIPELINE-STATUS.md`. */
155
+ export const versionLayoutRules = (
156
+ deps: VersionStatusDeps,
157
+ ): Readonly<Record<"layout", FolderRuleModule>> => ({
158
+ layout: statusRule(LAYOUT_META, "layout", deps),
159
+ });
160
+
161
+ /** The `cycle` plugin's rule over the frozen folder, on every `PIPELINE-STATUS.md`. */
162
+ export const submittedFrozenRules = (
163
+ deps: VersionStatusDeps,
164
+ ): Readonly<Record<"submitted-frozen", FolderRuleModule>> => ({
165
+ "submitted-frozen": statusRule(FROZEN_META, "submitted-frozen", deps),
166
+ });
167
+
168
+ // ── on paper.tex: the draft against the frozen source ────────────────────────────────────
169
+
170
+ const DRIFT_META: TexRuleModule["meta"] = {
171
+ type: "suggestion",
172
+ docs: {
173
+ description:
174
+ "paper.tex has moved on from the source frozen when the open cycle's PDF was sent — said with how many lines",
175
+ url: rulePageUrl("cycle/draft-drift"),
176
+ },
177
+ schema: [],
178
+ messages: {
179
+ drift:
180
+ "paper.tex has moved on from `versions/{{key}}`, the version under review at {{venue}}: {{drift}} — expected while the draft heads for its next venue; a reviewer's line numbers point into the frozen file, not this one",
181
+ },
182
+ };
183
+
184
+ const CAMERA_META: TexRuleModule["meta"] = {
185
+ type: "problem",
186
+ docs: {
187
+ description:
188
+ "once the last cycle is accepted and its camera-ready frozen, the camera-ready started from the source that was accepted",
189
+ url: rulePageUrl("cycle/camera-ready-source"),
190
+ },
191
+ schema: [],
192
+ messages: {
193
+ base: "cycle «{{id}}» is accepted, and its frozen camera-ready, `versions/{{id}}/camera-ready.tex`, keeps {{kept}} of the {{total}} lines of what was accepted, `versions/{{id}}/submitted.tex` — under half, so it was not built from the accepted source. The camera-ready is what was accepted plus the changes the reviewers asked for: copy submitted.tex over paper.tex, carry over those changes, build, and send it again with `npx paperlint submission update <paper> --save`, which replaces the frozen one",
194
+ },
195
+ };
196
+
197
+ /** The draft against the open cycle's frozen source, or none. */
198
+ function driftFinding(
199
+ dir: string,
200
+ draft: string,
201
+ deps: ProseDeps,
202
+ ): VersionFinding | null {
203
+ const cycles = cyclesIn(dir, deps.files);
204
+ if (cycles === null || cycles.current.kind !== "cycle") return null;
205
+ const c = cycles.current.cycle;
206
+ const d = draftDrift(dir, cycles, draft, deps.files);
207
+ return d.kind === "drift"
208
+ ? {
209
+ messageId: "drift",
210
+ data: { key: d.key, venue: venueName(c), drift: driftText(d.drift) },
211
+ }
212
+ : null;
213
+ }
214
+
215
+ /**
216
+ * The accepted cycle's frozen camera-ready against its frozen `submitted.tex`, or none: nothing to judge
217
+ * before the camera-ready is frozen (or without either source), and nothing wrong when it derives.
218
+ */
219
+ function cameraFinding(dir: string, files: Files): VersionFinding | null {
220
+ const cycles = cyclesIn(dir, files);
221
+ if (cycles === null || cycles.current.kind !== "accepted") return null;
222
+ const id = cycles.current.cycle.id;
223
+ const camera = frozenText(dir, frozenKey(id, "camera-ready", "tex"), files);
224
+ const accepted = frozenText(dir, frozenKey(id, "submitted", "tex"), files);
225
+ if (camera === null || accepted === null) return null;
226
+ const l = lineage(accepted, camera);
227
+ return derivesFrom(l)
228
+ ? null
229
+ : {
230
+ messageId: "base",
231
+ data: { id, kept: String(l.kept), total: String(l.total) },
232
+ };
233
+ }
234
+
235
+ /** A rule on paper.tex: its one finding, reported on the first line. */
236
+ const paperRule = (
237
+ meta: TexRuleModule["meta"],
238
+ finding: (dir: string, draft: () => string) => VersionFinding | null,
239
+ deps: ProseDeps,
240
+ ): TexRuleModule => ({
241
+ meta,
242
+ create(context) {
243
+ if (basename(context.filename) !== "paper.tex") return {};
244
+ return {
245
+ root() {
246
+ const sc = context.sourceCode;
247
+ const f = finding(
248
+ dirname(context.filename),
249
+ () => readPaper(context.filename, sc.raw ?? sc.text, deps).text,
250
+ );
251
+ if (f === null) return;
252
+ const at = sc.getLocFromIndex(0);
253
+ context.report({ loc: { start: at, end: at }, ...f });
254
+ },
255
+ };
256
+ },
257
+ });
258
+
259
+ /** The `cycle` plugin's rules over the draft, for every `paper.tex`. */
260
+ export const draftRules = (
261
+ deps: ProseDeps,
262
+ ): Readonly<Record<"draft-drift" | "camera-ready-source", TexRuleModule>> => ({
263
+ "draft-drift": paperRule(
264
+ DRIFT_META,
265
+ (dir, draft) => driftFinding(dir, draft(), deps),
266
+ deps,
267
+ ),
268
+ "camera-ready-source": paperRule(
269
+ CAMERA_META,
270
+ (dir) => cameraFinding(dir, deps.files),
271
+ deps,
272
+ ),
273
+ });
274
+
275
+ /** The levels they are on at in paperlint's own config. */
276
+ export const VERSION_STATUS_RULE_LEVELS: Readonly<
277
+ Record<"versions/layout" | "cycle/submitted-frozen", "error">
278
+ > = { "versions/layout": "error", "cycle/submitted-frozen": "error" };
279
+
280
+ export const DRAFT_RULE_LEVELS: Readonly<
281
+ Record<"cycle/draft-drift" | "cycle/camera-ready-source", "warn" | "error">
282
+ > = { "cycle/draft-drift": "warn", "cycle/camera-ready-source": "error" };