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,164 @@
1
+ /**
2
+ * A PAPER'S FROZEN VERSIONS, read from the disk: the folder `versions/`, its lock, and the draft
3
+ * measured against the frozen source it left (`src/domain/versions.ts` holds the convention and every
4
+ * decision; this file reads and writes through the ports).
5
+ *
6
+ * readVersions the cycles, the lock and the files of `versions/`, as the rules and the table read them
7
+ * draftDrift paper.tex, assembled as the build read it, against the frozen source of the attempt
8
+ * versionLines the version → state table (`paperlint versions`, `paperlint submission show`)
9
+ * freezePlan before a `submission --save` sends: what it will freeze, or why it cannot
10
+ * freezeSent after the portal took it: the PDF sent and its source, written and locked
11
+ */
12
+ import { join } from "node:path";
13
+ import { callerPath } from "./caller-path.js";
14
+ import { venueCycleOf } from "./domain/cycle.js";
15
+ import { sha256Hex } from "./domain/sha256.js";
16
+ import { messageOf } from "./domain/text.js";
17
+ import { err, ok } from "./domain/result.js";
18
+ import { driftBase, frozenKey, lineDrift, lockText, LOCK_FILE, parseLock, versionRows, versionTable, VERSIONS_DIR, } from "./domain/versions.js";
19
+ import { readCycles } from "./cycle-rules.js";
20
+ import { readPaper, decoded } from "./tex-paper.js";
21
+ import { MAIN } from "./build.js";
22
+ import { paperRecord } from "./paper-record.js";
23
+ /** The paper's `versions/` folder. */
24
+ export const versionsDir = (paperDir) => join(paperDir, VERSIONS_DIR);
25
+ /** A frozen file's path, by its key. */
26
+ export const frozenPath = (paperDir, key) => join(paperDir, VERSIONS_DIR, key);
27
+ /** Whether a frozen file is on disk, by its key. */
28
+ export const hasFrozen = (paperDir, files) => (key) => files.isFile(callerPath(frozenPath(paperDir, key)));
29
+ /** A frozen file's text, by its key; null when it is not there. */
30
+ export const frozenText = (paperDir, key, files) => decoded(files.readBytes(callerPath(frozenPath(paperDir, key))));
31
+ /** The size and digest of a frozen file on disk, by its key: zero bytes of nothing when it is gone. */
32
+ export const factsOfFrozen = (paperDir, files) => (key) => {
33
+ const b = files.readBytes(callerPath(frozenPath(paperDir, key))) ??
34
+ new Uint8Array();
35
+ return { bytes: b.length, sha256: sha256Hex(b) };
36
+ };
37
+ /** `versions/lock.json`: null when there is none; else parsed, or why it does not parse. */
38
+ export function readLock(paperDir, files) {
39
+ const b = files.readBytes(callerPath(join(versionsDir(paperDir), LOCK_FILE)));
40
+ if (b === null)
41
+ return null;
42
+ try {
43
+ return parseLock(JSON.parse(new TextDecoder().decode(b)));
44
+ }
45
+ catch (e) {
46
+ return err(`not JSON: ${messageOf(e)}`);
47
+ }
48
+ }
49
+ /** The entries of a folder, each a file or a folder, by name. */
50
+ export const entriesIn = (dir, deps) => [...deps.listDir(callerPath(dir))].sort().map((name) => ({
51
+ name,
52
+ kind: deps.files.isFile(callerPath(join(dir, name))) ? "file" : "dir",
53
+ }));
54
+ /** The paper's cycles, when it keeps them and they read; null for the flat form or a broken record. */
55
+ export function cyclesIn(paperDir, files) {
56
+ const r = readCycles(paperDir, { files });
57
+ return r.ok ? r.value : null;
58
+ }
59
+ /** paper.tex, assembled as the last build read it (its includes spliced in); null without one. */
60
+ export function draftText(paperDir, deps) {
61
+ const main = join(paperDir, MAIN);
62
+ const src = decoded(deps.files.readBytes(callerPath(main)));
63
+ return src === null ? null : readPaper(main, src, deps).text;
64
+ }
65
+ /** The draft against the frozen source of the paper's attempt: none, nothing frozen, the same, or drifted. */
66
+ export function draftDrift(paperDir, cycles, draft, files) {
67
+ const base = driftBase(cycles.current, hasFrozen(paperDir, files));
68
+ if (base === null)
69
+ return { kind: "none" };
70
+ const frozen = frozenText(paperDir, base.key, files);
71
+ if (frozen === null)
72
+ return { kind: "no-base", key: base.key };
73
+ const drift = lineDrift(frozen, draft);
74
+ return drift.added === 0 && drift.removed === 0
75
+ ? { kind: "same", key: base.key }
76
+ : { kind: "drift", key: base.key, drift };
77
+ }
78
+ /**
79
+ * The version → state table of the paper in `paperDir`: each attempt's frozen files and what they
80
+ * are, then the draft. Or why there is none: the paper keeps no `cycles`.
81
+ */
82
+ export function versionLines(paperDir, deps) {
83
+ const cycles = cyclesIn(paperDir, deps.files);
84
+ if (cycles === null)
85
+ return err('the paper keeps no "cycles" in paperlint.json (or they do not read — `paperlint lint` says why): versions are kept per attempt, versions/<cycle>/<stage>.pdf');
86
+ const lock = readLock(paperDir, deps.files);
87
+ const draft = draftText(paperDir, deps);
88
+ return ok(versionTable(versionRows({
89
+ cycles: cycles.list,
90
+ has: hasFrozen(paperDir, deps.files),
91
+ lock: lock?.ok === true ? lock.value : {},
92
+ draft: draft === null
93
+ ? { kind: "none" }
94
+ : draftDrift(paperDir, cycles, draft, deps.files),
95
+ })));
96
+ }
97
+ const refusal = (why, cycle) => `refused: ${why}, and the two could not be frozen together in versions/${cycle}/ — rebuild (\`paperlint build\`), run the dry run again, then --save; nothing was sent`;
98
+ /** Why the source on disk cannot be frozen as the PDF's; null when the build's record says it can. */
99
+ function sourceProblem(paperDir, deps) {
100
+ const record = paperRecord(paperDir, deps);
101
+ if (record.kind === "none")
102
+ return err("no record of the build (_build/sources.json), so which source the PDF was built from is not known");
103
+ if (record.kind === "stale")
104
+ return err(`${record.changed.map((x) => x.path).join(", ")} changed since the build, so the PDF is not what the source builds now`);
105
+ const source = draftText(paperDir, deps);
106
+ return source === null
107
+ ? err(`no ${MAIN} to freeze beside the PDF`)
108
+ : ok(source);
109
+ }
110
+ /**
111
+ * Before a `--save` sends: what it will freeze — the open cycle's `submitted`, or the accepted one's
112
+ * `camera-ready` — and the source to freeze beside the PDF. Refused when the build's record does not
113
+ * say the source on disk is the one the PDF was built from: a PDF frozen with another source is a
114
+ * pair that lies.
115
+ */
116
+ export function freezePlan(paperDir, cycles, deps) {
117
+ const c = cycles === null ? null : venueCycleOf(cycles.current);
118
+ if (cycles === null || c === null)
119
+ return ok({ kind: "flat" });
120
+ const source = sourceProblem(paperDir, deps);
121
+ return source.ok
122
+ ? ok({
123
+ kind: "freeze",
124
+ cycle: c.id,
125
+ stage: cycles.current.kind === "accepted" ? "camera-ready" : "submitted",
126
+ source: source.value,
127
+ })
128
+ : err(refusal(source.error, c.id));
129
+ }
130
+ const lockEntry = (bytes, frozen) => ({
131
+ kind: "file",
132
+ bytes: bytes.length,
133
+ sha256: sha256Hex(bytes),
134
+ frozen,
135
+ });
136
+ /** The lock with the two frozen files recorded, written; or why the lock on disk does not read. */
137
+ function lockSent(s, entries, deps) {
138
+ const lock = readLock(s.paperDir, deps.files) ?? ok({});
139
+ if (!lock.ok)
140
+ return lock;
141
+ deps.files.writeAtomic(callerPath(join(versionsDir(s.paperDir), LOCK_FILE)), new TextEncoder().encode(lockText({ ...lock.value, ...entries })));
142
+ return ok(null);
143
+ }
144
+ /**
145
+ * After the portal took the PDF: the PDF and its source written to `versions/<cycle>/<stage>.*`,
146
+ * replacing an earlier upload's, and recorded in the lock. The line to print; an error line when the
147
+ * lock does not read (the files are frozen, the lock left as it is).
148
+ */
149
+ export function freezeSent(s, deps) {
150
+ const { cycle, stage } = s.plan;
151
+ const source = new TextEncoder().encode(s.plan.source);
152
+ const pdfKey = frozenKey(cycle, stage, "pdf");
153
+ const texKey = frozenKey(cycle, stage, "tex");
154
+ deps.files.writeAtomic(callerPath(frozenPath(s.paperDir, pdfKey)), s.pdf);
155
+ deps.files.writeAtomic(callerPath(frozenPath(s.paperDir, texKey)), source);
156
+ const frozen = deps.now();
157
+ const locked = lockSent(s, { [pdfKey]: lockEntry(s.pdf, frozen), [texKey]: lockEntry(source, frozen) }, deps);
158
+ return locked.ok
159
+ ? ok(`frozen versions/${pdfKey} (${String(s.pdf.length)} bytes) and ${stage}.tex (${String(source.length)} bytes), recorded in versions/lock.json`)
160
+ : err(`versions/lock.json does not read (${locked.error}): versions/${pdfKey} and ${stage}.tex are frozen, not recorded — \`npx paperlint versions lock ${s.shownPaper}\` records them`);
161
+ }
162
+ /** What a send in the flat form says instead of a freeze. */
163
+ export const NOT_FROZEN = 'not frozen the paper keeps no "cycles" in paperlint.json: versions are frozen per attempt, in versions/<cycle>/ (docs/configuration.md, "The paper\'s cycles")';
164
+ //# sourceMappingURL=versions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"versions.js","sourceRoot":"","sources":["../src/versions.ts"],"names":[],"mappings":"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"}
@@ -164,7 +164,7 @@ is refused as a second source for one fact.
164
164
  "pdf": "build/paper.pdf",
165
165
  "cycles": [
166
166
  {
167
- "id": "alpha-2026", // what a stage's `cycle:` names
167
+ "id": "alpha-2026", // the name of its folder in versions/
168
168
  "venue": { "kind": "preset", "extends": "paperlint:acm-sigconf" },
169
169
  "kind": "short",
170
170
  "opened": "2026-07-01", // the day this attempt was decided
@@ -225,7 +225,7 @@ is refused as a second source for one fact.
225
225
 
226
226
  | field | what it is |
227
227
  | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
228
- | `id` | unique in the list; a scorecard stage names it as `cycle: <id>` ([`cycle/stage`](rules/cycle/stage.md)) |
228
+ | `id` | unique in the list; the name of the attempt's folder in `versions/` ([`versions/layout`](rules/versions/layout.md)), so one folder name: lowercase letters, digits and hyphens, starting with a letter or a digit (`msr-2027`). Anything else (`conf/2026`, `..`, `Conf-2026`) is refused when the record is read |
229
229
  | `venue` | `{ "kind": "preset", "extends": "paperlint:<name>" \| "./path" }` — resolved as `extends` is; or `{ "kind": "named", "name", "url" }` for a venue with no preset yet. A named venue has no format rules |
230
230
  | `kind` | a key of the preset's `format.kinds`; omit when the venue has one kind |
231
231
  | `opened` | `YYYY-MM-DD`, the day this attempt was decided — the boundary a consumer judges the scorecard's rows against |
@@ -242,6 +242,80 @@ camera-ready and talk are judged against it, and the venue's blindness no longer
242
242
 
243
243
  Papers without `cycles` read exactly as before.
244
244
 
245
+ ### The frozen versions: one folder per attempt
246
+
247
+ A paper that keeps `cycles` freezes each version it sends in `versions/<cycle id>/`, one file per
248
+ stage. No date is typed into a name: when a file was frozen is in `versions/lock.json`, which
249
+ paperlint writes.
250
+
251
+ ```text
252
+ versions/
253
+ ├── alpha-2026/ ← rejected: historical
254
+ │ ├── submitted.pdf
255
+ │ └── submitted.tex
256
+ ├── beta-2027/ ← open: CURRENT, under review
257
+ │ ├── submitted.pdf
258
+ │ └── submitted.tex
259
+ └── lock.json ← generated: bytes, sha256 and when each file was frozen
260
+ ```
261
+
262
+ Each version's state follows from its cycle's `outcome`, never typed: a `submitted` version of the
263
+ open cycle is under review; anything in a rejected or withdrawn cycle's folder is historical; an
264
+ accepted cycle's `submitted` is the camera-ready's base, and its `camera-ready` is final. When a
265
+ decision arrives you change one value, the cycle's `outcome`, and every state follows.
266
+ `npx paperlint versions` prints the table, and `paperlint submission show` prints it after the
267
+ portal's copy:
268
+
269
+ ```text
270
+ beta-2027 submitted CURRENT (under review at paperlint:ieee-conference) 186504 B frozen 2026-10-23 16:58 UTC
271
+ alpha-2026 submitted historical (rejected) 305412 B frozen 2026-07-22
272
+ paper.tex draft target not chosen differs from beta-2027/submitted.tex: 18 lines added, 18 removed
273
+ ```
274
+
275
+ `paperlint submission new --save` and `submission update --save` freeze what they send: the exact
276
+ PDF, and the source the build read — a paper split into `sections/` is frozen whole, its includes
277
+ spliced in — as `versions/<cycle>/submitted.{pdf,tex}`, or `camera-ready.{pdf,tex}` once the cycle is
278
+ accepted, and record both in the lock. A later save in the same cycle replaces them: the last upload
279
+ is the one under review. A save whose sources changed since the build is refused before anything is
280
+ sent, because the PDF and the source on disk would no longer be a pair. A file you put there yourself
281
+ (an arXiv copy, an upload made on the portal by hand) is recorded with `npx paperlint versions lock`.
282
+
283
+ **Between venues.** Freeze at the send — `--save` does it. Keep editing `paper.tex`: it is always the
284
+ draft, the only file you edit, and [`cycle/draft-drift`](rules/cycle/draft-drift.md) says how far it
285
+ has moved from what the reviewers read, so a reply to them quotes the frozen file. Open the next
286
+ cycle only once the current one has an outcome (one open cycle at most). If the venue accepts, the
287
+ camera-ready starts from the frozen `submitted.tex`, not from the drifted draft; once the camera-ready
288
+ is sent and frozen, [`cycle/camera-ready-source`](rules/cycle/camera-ready-source.md) checks that it
289
+ kept at least half of the accepted source's lines. A cycle that has a submission and nothing frozen is
290
+ [`cycle/submitted-frozen`](rules/cycle/submitted-frozen.md).
291
+
292
+ **From the `stages:` list.** A paper that recorded its frozen versions as a `stages:` list in
293
+ `PIPELINE-STATUS.md`, with dated files in `versions/`, moves to the folders with one command:
294
+
295
+ ```console
296
+ $ npx paperlint versions migrate papers/my-paper
297
+ moved versions/2026-07-22-submitted.pdf → versions/alpha-2026/submitted.pdf
298
+ moved versions/2026-07-22-submitted.tex → versions/alpha-2026/submitted.tex
299
+ recorded versions/lock.json, 2 entries
300
+ removed the stages: list from PIPELINE-STATUS.md — the folders and the lock are the record now
301
+ ```
302
+
303
+ Each entry's `cycle:` names its folder, its `bytes:` and `sourceBytes:` are checked against the
304
+ files once more, its `date:` becomes the day the lock records, and `sourceLost: true` becomes a lock
305
+ entry that records the loss. An entry without a `cycle:`, a file that is no longer the one declared,
306
+ or a dated file no entry names stops the whole migration before anything moves, with what to do;
307
+ so does a destination that is already there, such as a version `submission update --save` froze
308
+ while the list was still in place. A source two stages were built from (a `submitted` and an `arxiv`
309
+ sharing one `.tex`) is copied to each. Paths in the list are read in one spelling (`./versions/a.tex` is `versions/a.tex`), and one
310
+ outside the paper folder (`../a.tex`) is refused. Before anything moves, the whole plan is replayed on
311
+ the paths it touches: a plan that could stop half-way is refused with nothing moved.
312
+ Any other loose file at the top of `versions/` (a withdrawn `STALE` copy, a note on the old
313
+ convention) moves to `extra/versions/`, since [`versions/layout`](rules/versions/layout.md) allows
314
+ nothing there beside `lock.json`. The YAML comments inside the list leave with it: read them first,
315
+ since a provenance note kept there (which commit a source came from, whether a PDF is the portal's
316
+ bytes or a rebuild) belongs in the cycle's evidence or the scorecard's prose. A paper without
317
+ `cycles` keeps its `stages:` list: `paper/stages` and `paper/source` still read it.
318
+
245
319
  ### Deadlines
246
320
 
247
321
  A deadline records **what each page said**, and optionally **a human decision that overrides it**:
package/docs/layout.md CHANGED
@@ -27,8 +27,8 @@ flowchart TB
27
27
  end
28
28
  S4["<b>4 · draft</b><br/>draft-paper · argument-arc · tighten-paper<br/>writes: paper.tex, sections/, figures/<br/>checked by: the tex/* rules<br/>hook paper-edit-guard: no shell writes"]
29
29
  S5["<b>5 · review</b><br/>grade-paper-writing · verify-citations<br/>pc-panel-review · harden-paper<br/>study-accepted-papers, mode C or D<br/>writes: reviews/, the scorecard<br/>in PIPELINE-STATUS.md<br/>checked by: review/frontmatter"]
30
- S6["<b>6 · submit</b><br/>paperlint build · submit-paper<br/>writes: paper.pdf, _build/,<br/>versions/, submission/<br/>checked by: format/page-limit, pdf/*,<br/>anonymity/identity, paper/stages"]
31
- S7["<b>7 · camera-ready</b><br/>camera-ready<br/>writes: versions/, talk/<br/>checked by: paper/stages, talk/*"]
30
+ S6["<b>6 · submit</b><br/>paperlint build · submit-paper<br/>writes: paper.pdf, _build/,<br/>versions/, submission/<br/>checked by: format/page-limit, pdf/*,<br/>anonymity/identity, versions/layout"]
31
+ S7["<b>7 · camera-ready</b><br/>camera-ready<br/>writes: versions/, talk/<br/>checked by: versions/layout, talk/*"]
32
32
  S8["<b>8 · extend</b><br/>extend-paper<br/>writes: a new paper folder"]
33
33
  N --> S1 --> S2 --> D
34
34
  Y --> S4 --> S5 --> S6 --> S7 --> S8
@@ -64,7 +64,7 @@ creates one.
64
64
  ├── paper.pdf ← the built PDF
65
65
  ├── *.tex *.bib *.cls *.sty *.bst ← what TeX and bibtex look up beside paper.tex
66
66
  ├── _build/ ← the facts `paperlint build` writes
67
- ├── versions/ ← each stage's frozen PDF and source
67
+ ├── versions/ ← each stage's frozen PDF and source — per attempt, versions/<cycle>/, with lock.json
68
68
  ├── reviews/ ← dated reviews and pass reports
69
69
  ├── siblings/ ← the cards of competing papers
70
70
  ├── research/ ← dated research and decision records
@@ -102,7 +102,7 @@ Both are explained in [`venues.md`](venues.md).
102
102
  | ------------------------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
103
103
  | `<papers-root>/venues/<venue>/` | 2 venue | `study-accepted-papers`, its first run for that venue (mode A, usually) | nothing ([why](venues.md#what-checks-it)) |
104
104
  | the paper folder's root | before 1 | `paperlint new`: the three files, and an empty `.gitkeep` folder for `reviews/`, `research/`, `figures/`, `versions/`, `submission/`, `repro/tools/`, `repro/data/` (`repro/` gets a stub index) | [`paper/allowed-files-only`](rules/paper/allowed-files-only.md); the hook `paper-folder-layout` refuses a new stray entry |
105
- | `PIPELINE-STATUS.md` | every stage | `paperlint new`, then `paper-pipeline`, `paper-status`; `study-accepted-papers` (the scorecard) | `paper/stages`, `paper/source`, [`cycle/stage`](rules/cycle/stage.md) ([rules](rules.md)) |
105
+ | `PIPELINE-STATUS.md` | every stage | `paperlint new`, then `paper-pipeline`, `paper-status`; `study-accepted-papers` (the scorecard) | `paper/stages`, `paper/source`, [`cycle/submitted-frozen`](rules/cycle/submitted-frozen.md) ([rules](rules.md)) |
106
106
  | `paperlint.json` | before 1, then 2 | `paperlint new`; `find-venue` names the venue | `paperlint lint` refuses an unknown key; [`cycle/record`](rules/cycle/record.md) |
107
107
  | `siblings/` | 1 idea | `map-prior-work`, `analyze-sibling-paper` | `sibling/frontmatter` ([records](configuration.md#records-in-frontmatter-checked-by-shipped-json-schemas)) |
108
108
  | `reviews/` | 2, then 4–5 | `study-accepted-papers` (its report), `grade-paper-writing`, `cold-read-diff`, the review skills | `review/frontmatter` ([records](configuration.md#records-in-frontmatter-checked-by-shipped-json-schemas)) |
@@ -119,7 +119,7 @@ Both are explained in [`venues.md`](venues.md).
119
119
  | `paper.tex`, `sections/`, `*.bib` | 4 draft | `draft-paper`, `argument-arc`, `tighten-paper` | the `tex/*`, `paper/*` and `bib/*` rules ([rules](rules.md)); the hook `paper-edit-guard` refuses a shell write |
120
120
  | `CLAIMS.md`, `figures/` | 4 draft | you | nothing |
121
121
  | `paper.pdf`, `_build/` | 6 submit | `paperlint build` | `pdf/*`, [`format/page-limit`](rules/format/page-limit.md), [`anonymity/identity`](rules/anonymity/identity.md), the reference rules |
122
- | `versions/` | 6, then 7 | you, at submission and at camera-ready (`submit-paper` says how): the frozen PDF and source of each stage | `paper/stages`, `paper/source` |
122
+ | `versions/` | 6, then 7 | you, at submission and at camera-ready (`submit-paper` says how): the frozen PDF and source of each stage | [`versions/layout`](rules/versions/layout.md) with `cycles`; `paper/stages`, `paper/source` without |
123
123
  | `submission/`, `SUBMIT-CHECKLIST.md` | 6 submit | you, while `submit-paper` and `submission-portal` do the submitting: forms, portal records, mail with the venue | [`cycle/evidence`](rules/cycle/evidence.md), for the files a closed cycle names |
124
124
  | `talk/` | 7 camera-ready | you | the `talk/*` rules ([talk](talk.md)) |
125
125
  | `extra/` | any | you | nothing, ever |
@@ -0,0 +1,62 @@
1
+ # cycle/camera-ready-source
2
+
3
+ **Level:** error · **Reads:** `paperlint.json` (`cycles`), `versions/<accepted cycle>/submitted.tex` and
4
+ `camera-ready.tex` · **Reported on:** the first line of `paper.tex`
5
+
6
+ ## What it catches
7
+
8
+ The last cycle is `accepted`, its camera-ready is frozen (`versions/<cycle>/camera-ready.tex`, written
9
+ by `npx paperlint submission update <paper> --save`), and that frozen source keeps less than half of the
10
+ lines of what was accepted, the cycle's frozen `submitted.tex`. The message names both files and the
11
+ count.
12
+
13
+ Until the camera-ready is frozen the rule is silent: while it is being edited, `paper.tex` differs from
14
+ what was accepted by design, since the reviewers' changes go in.
15
+
16
+ ## Why
17
+
18
+ The camera-ready is what the venue accepted plus the changes its reviewers asked for. Between the upload
19
+ and the decision `paper.tex` usually moved on (see [`cycle/draft-drift`](draft-drift.md)). Built from
20
+ that drifted draft, the camera-ready would send the venue a paper its reviewers never read.
21
+
22
+ A camera-ready differs from what was accepted, so any difference cannot be the test. The rule counts
23
+ the accepted source's lines the camera-ready keeps unchanged, with the same line diff as
24
+ `cycle/draft-drift`: one that keeps at least half started from the accepted source; one that keeps less
25
+ was built from something else.
26
+
27
+ ## Examples
28
+
29
+ Failing: cycle `open-2027` is accepted, and its frozen camera-ready was built from a draft that moved
30
+ on:
31
+
32
+ ```text
33
+ papers/my-paper/paper.tex
34
+ 1:1 error cycle «open-2027» is accepted, and its frozen camera-ready,
35
+ `versions/open-2027/camera-ready.tex`, keeps 3 of the 13 lines of what was accepted,
36
+ `versions/open-2027/submitted.tex` — under half, so it was not built from the accepted
37
+ source. … cycle/camera-ready-source
38
+ ```
39
+
40
+ Passing:
41
+
42
+ - an accepted cycle whose camera-ready is not frozen yet, however far `paper.tex` has moved;
43
+ - a frozen camera-ready that is the accepted source plus the requested changes.
44
+
45
+ ## Options / preset fields
46
+
47
+ No rule options.
48
+
49
+ ## What it does not check
50
+
51
+ - Whether the changes are the ones the reviewers asked for. Half of the lines kept says the
52
+ camera-ready started from the accepted source, not that every change in it was requested.
53
+ - `paper.tex` after the camera-ready is frozen. What the venue holds is fixed; the draft is free to head
54
+ for the next paper.
55
+ - A camera-ready PDF frozen without its source: there is nothing to compare, and
56
+ [`versions/layout`](../versions/layout.md) names the missing source.
57
+
58
+ ## How to fix
59
+
60
+ Copy `versions/<cycle>/submitted.tex` over `paper.tex`, make the changes the reviewers asked for, build,
61
+ and send again with `npx paperlint submission update <paper> --save`. That replaces the frozen
62
+ `camera-ready.{pdf,tex}`.
@@ -0,0 +1,47 @@
1
+ # cycle/draft-drift
2
+
3
+ **Level:** warn · **Reads:** `paper.tex` (with the files the last build spliced in), `paperlint.json`
4
+ (`cycles`), `versions/<open cycle>/submitted.tex` · **Reported on:** the first line of `paper.tex`
5
+
6
+ ## What it catches
7
+
8
+ The last cycle is open, its `submitted.tex` is frozen, and `paper.tex` differs from it. The message
9
+ says by how much: the lines added and removed, as a line diff counts them.
10
+
11
+ ## Why
12
+
13
+ `paper.tex` is always the draft; the frozen `submitted.tex` is what the venue's reviewers read. Between
14
+ the upload and the decision the draft keeps moving — towards the next venue, or answering what the
15
+ reviewers are likely to ask — and that is expected. What is not expected is to forget it: a reply to a
16
+ reviewer that quotes line numbers, or a rebuttal that quotes a sentence, has to quote the frozen file.
17
+ The warning keeps the distance visible. ESLint has no info level, so it is a warning: it prints and
18
+ does not fail the run.
19
+
20
+ ## Examples
21
+
22
+ Reported — the open cycle `open-2027` froze `submitted.tex`, and `paper.tex` changed since:
23
+
24
+ ```text
25
+ papers/my-paper/paper.tex
26
+ 1:1 warning paper.tex has moved on from `versions/open-2027/submitted.tex`, the version under
27
+ review at paperlint:acm-sigconf: 18 lines added, 18 removed — expected while the draft
28
+ heads for its next venue; … cycle/draft-drift
29
+ ```
30
+
31
+ Silent: `paper.tex` as frozen; no open cycle; or no frozen `submitted.tex` yet.
32
+
33
+ ## Options / preset fields
34
+
35
+ No rule options. A paper that does not want the reminder turns it off in its `paperlint.json`:
36
+ `"rules": { "cycle/draft-drift": "off" }`.
37
+
38
+ ## What it does not check
39
+
40
+ - Which of the changes matter: a changed number and a reflowed paragraph count the same.
41
+ - A paper split into several files is compared as the last build assembled it; when the build's record
42
+ is out of date (`paper/sources-fresh` says so), only `paper.tex` itself is compared.
43
+
44
+ ## How to fix
45
+
46
+ Nothing to fix while the draft heads elsewhere. To answer the reviewers of the open cycle, quote
47
+ `versions/<cycle>/submitted.tex`.
@@ -65,8 +65,8 @@ reads, and compares the way that rule does.
65
65
  are the format rules' to judge, and they judge again the moment the phase is dropped.
66
66
  - A cycle declared `porting` whose class line is still the old one: that is the state the phase
67
67
  describes.
68
- - A `porting` cycle that has already sent a PDF — [`cycle/stage`](stage.md) reports that on the
69
- scorecard.
68
+ - A `porting` cycle that has already sent a PDF — [`versions/layout`](../versions/layout.md) reports
69
+ a `submitted` or `camera-ready` PDF frozen in its folder.
70
70
 
71
71
  ## How to fix
72
72
 
@@ -144,7 +144,8 @@ scheduled workflow in paperlint's repository; your own preset may carry readings
144
144
  - Whether a deadline has passed, or how long is left.
145
145
  - A disagreement between sources — that is what the record holds; the earlier reading binds.
146
146
  - Whether an override's evidence exists — that is [`cycle/evidence`](evidence.md).
147
- - Whether the scorecard's stages name these cycles — that is [`cycle/stage`](stage.md).
147
+ - Whether `versions/` holds a folder per cycle and nothing else — that is
148
+ [`versions/layout`](../versions/layout.md).
148
149
 
149
150
  ## How to fix
150
151
 
@@ -0,0 +1,65 @@
1
+ # cycle/submitted-frozen
2
+
3
+ **Level:** error · **Reads:** the paper's `paperlint.json` (`cycles`) and its `versions/` folder ·
4
+ **Reported on:** the first line of `PIPELINE-STATUS.md`
5
+
6
+ ## What it catches
7
+
8
+ A cycle with `"submission": { "id": … }` — a PDF went to the venue's portal — and no
9
+ `versions/<cycle>/submitted.pdf`.
10
+
11
+ ## Why
12
+
13
+ What the reviewers read is the one version an author has to be able to show again: a reviewer's
14
+ page and line numbers point into it, and a camera-ready starts from its source. Before this rule a
15
+ paper could record a submission on an open cycle, keep no frozen copy, and `paperlint lint` said
16
+ nothing; the only record of the text under review was a branch commit, which squash merges and
17
+ `git gc` destroy.
18
+
19
+ Since this rule, `paperlint submission new --save` and `submission update --save` freeze the exact
20
+ PDF they send, with the source it was built from, so the rule fires only for an upload made another
21
+ way (by hand on the portal, or before paperlint froze on send).
22
+
23
+ ## Examples
24
+
25
+ Failing — `paperlint.json` has
26
+
27
+ ```json
28
+ {
29
+ "cycles": [
30
+ {
31
+ "id": "open-2027",
32
+ "venue": { "kind": "preset", "extends": "paperlint:acm-sigconf" },
33
+ "opened": "2026-09-09",
34
+ "submission": { "id": 7 }
35
+ }
36
+ ]
37
+ }
38
+ ```
39
+
40
+ and `versions/open-2027/` is empty:
41
+
42
+ ```text
43
+ papers/my-paper/PIPELINE-STATUS.md
44
+ 1:1 error cycle «open-2027» has submission 7 on the portal, and `versions/open-2027/submitted.pdf`
45
+ is not there: what the reviewers read is not frozen. … cycle/submitted-frozen
46
+ ```
47
+
48
+ Passing: the same cycle with `versions/open-2027/submitted.pdf` on disk; or a cycle with no
49
+ `submission` yet (still preparing).
50
+
51
+ ## Options / preset fields
52
+
53
+ No rule options.
54
+
55
+ ## What it does not check
56
+
57
+ - The source beside the PDF, the lock, the folder's names — [`versions/layout`](../versions/layout.md) does.
58
+ - That the frozen PDF is the one the portal holds: `paperlint submission show` compares sha256s.
59
+
60
+ ## How to fix
61
+
62
+ Send with `npx paperlint submission update <paper> --save`: it freezes what it sends. For an upload
63
+ made before, put the PDF the portal holds at `versions/<cycle>/submitted.pdf` (`submission show`
64
+ prints its size and sha256 to check against), the source it was built from as `submitted.tex`, and
65
+ run `npx paperlint versions lock <paper>`.
@@ -29,7 +29,7 @@ it: [the map](../../layout.md).
29
29
  ├── paper.pdf ← the built PDF
30
30
  ├── *.tex *.bib *.cls *.sty *.bst ← what TeX and bibtex look up beside paper.tex
31
31
  ├── _build/ ← the facts `paperlint build` writes
32
- ├── versions/ ← each stage's frozen PDF and source
32
+ ├── versions/ ← each stage's frozen PDF and source — per attempt, versions/<cycle>/, with lock.json
33
33
  ├── reviews/ ← dated reviews and pass reports
34
34
  ├── siblings/ ← the cards of competing papers
35
35
  ├── research/ ← dated research and decision records
@@ -0,0 +1,88 @@
1
+ # versions/layout
2
+
3
+ **Level:** error · **Reads:** the paper's `versions/` folder, `versions/lock.json` and `paperlint.json`
4
+ (`cycles`) · **Reported on:** the first line of `PIPELINE-STATUS.md`
5
+
6
+ ## What it catches
7
+
8
+ A paper that keeps `cycles` freezes each version it sends in one folder per attempt:
9
+
10
+ ```text
11
+ versions/
12
+ ├── alpha-2026/ ← a cycle id in paperlint.json
13
+ │ ├── submitted.pdf
14
+ │ └── submitted.tex
15
+ ├── beta-2027/
16
+ │ ├── submitted.pdf
17
+ │ ├── submitted.tex
18
+ │ └── camera-ready.pdf ← only after the attempt is accepted
19
+ └── lock.json ← written by paperlint: bytes, sha256, when each file was frozen
20
+ ```
21
+
22
+ The rule reports:
23
+
24
+ - an entry of `versions/` that is neither `lock.json` nor a folder: a dated file from the layout
25
+ before folders per attempt (`2026-07-22-submitted.pdf`) is named with the command that moves it;
26
+ - a folder that names no cycle in `paperlint.json`;
27
+ - a file in a cycle's folder that is not `<stage>.pdf` or `<stage>.tex`, the stage one of
28
+ `submitted`, `camera-ready`, `arxiv`;
29
+ - a frozen PDF without its source beside it, unless the lock records the source as lost;
30
+ - a `submitted` or `camera-ready` PDF in a cycle whose `phase` is still `porting`;
31
+ - frozen files and no lock, a lock that does not read, a frozen file the lock does not record, a file
32
+ whose bytes or digest are not the ones the lock recorded, and a lock entry whose file is gone.
33
+
34
+ Names and folders starting with `.` (`.gitkeep`) are not judged. A paper without `cycles` is not
35
+ judged either: its `stages:` list in `PIPELINE-STATUS.md` is still its record, checked by
36
+ `paper/stages` and `paper/source`.
37
+
38
+ ## Why
39
+
40
+ The folder and the file name are the record: which attempt a file belongs to, and at which stage it
41
+ was frozen. What state it is in follows from the attempt's `outcome` in `paperlint.json` — under
42
+ review while the cycle is open, historical once it is rejected or withdrawn, the camera-ready's base
43
+ once accepted. A name outside the convention is a version whose state nobody can read off.
44
+
45
+ A frozen file is never supposed to change. The lock records its bytes and digest when paperlint froze
46
+ it, so a file that was replaced afterwards — the PDF a reviewer's page numbers point into, the source
47
+ their line numbers point into — is caught instead of trusted. The lock is generated, never typed:
48
+ `paperlint submission new|update --save` writes it when it freezes what it sent, and
49
+ `paperlint versions lock` records a file put there by hand.
50
+
51
+ ## Examples
52
+
53
+ Failing — `paperlint.json` declares the cycle `open-2027`, and `versions/` still holds a file of the
54
+ old layout:
55
+
56
+ ```text
57
+ papers/my-paper/PIPELINE-STATUS.md
58
+ 1:1 error `versions/2026-07-22-submitted.pdf` is not part of the layout — a frozen version in the
59
+ layout before folders per attempt: `npx paperlint versions migrate <paper>` moves it
60
+ into versions/<cycle>/ versions/layout
61
+ ```
62
+
63
+ Passing: `versions/open-2027/submitted.pdf` and `submitted.tex`, both recorded in `versions/lock.json`
64
+ as they stand.
65
+
66
+ ## Options / preset fields
67
+
68
+ No rule options.
69
+
70
+ ## What it does not check
71
+
72
+ - That a cycle with a submission froze anything at all — [`cycle/submitted-frozen`](../cycle/submitted-frozen.md) does.
73
+ - That the frozen source builds the frozen PDF: the lock records what was frozen together, and
74
+ `submission --save` freezes the source only when the build's record says it is the one the PDF was
75
+ built from.
76
+ - A lost source: the lock records it (`"sourceLost": true`) and `paperlint versions` shows it; there is
77
+ nothing left to fix, so it is not reported.
78
+
79
+ ## How to fix
80
+
81
+ - A dated file: `npx paperlint versions migrate <paper>` moves every one named in `stages:` into its
82
+ cycle's folder, writes the lock and drops the list.
83
+ - A folder or file outside the convention: rename it to the cycle id or the stage, or move it to
84
+ `extra/`.
85
+ - A file put there by hand, or one replaced on purpose: `npx paperlint versions lock <paper>`.
86
+ - A file replaced by accident, or deleted: restore it from git.
87
+ - A PDF sent from a cycle still `porting`: the port is done — drop the phase
88
+ ([`cycle/port-done`](../cycle/port-done.md) says when).