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,85 @@
1
+ ---
2
+ title: "AGENTVERIFY @ ICSE — venue card"
3
+ ---
4
+
5
+ # AGENTVERIFY @ ICSE
6
+
7
+ The card of the venue preset `paperlint:agentverify` ([`agentverify.jsonc`](agentverify.jsonc)):
8
+ what the venue asks for, in prose. The numbers a machine checks live in the preset; the section at
9
+ the end is generated from it. The `submit-paper` skill reads this card for the venue's specifics.
10
+
11
+ **Re-verify the call for papers every year**
12
+ ([agentverify2027.github.io/cfp.html](https://agentverify2027.github.io/cfp.html)). Sources read
13
+ 2026-10-26: that page, [the home page](https://agentverify2027.github.io/), the portal's
14
+ [deadlines page](https://icse2027-agentverify.hotcrp.com/deadlines), and the
15
+ [ICSE 2027 workshops page](https://conf.researchr.org/track/icse-2027/icse-2027-workshops). The
16
+ researchr page `conf.researchr.org/home/agentverify-2027` only redirects to the workshop's site.
17
+
18
+ ## The venue (2027 edition)
19
+
20
+ - **Venue**: Workshop on Infrastructure for Trustworthy Software Agents, an ICSE 2027 workshop,
21
+ Sun 25 April 2027, Dublin.
22
+ - **Portal**: HotCRP, `https://icse2027-agentverify.hotcrp.com/` («Submit Your Paper →»).
23
+ - **Format and review**: «Papers will be submitted via HotCRP and reviewed double-blind.
24
+ Submissions should follow the IEEE format in line with ICSE 2027 and present an original
25
+ contribution.» The portal's status data carries `"blind":true`. No class line is given, so the
26
+ preset keeps the family's `\documentclass[conference]{IEEEtran}`.
27
+ - **Publisher**: «Accepted papers will appear in the ICSE 2027 workshop proceedings by default.»
28
+ The ICSE workshops page: «The workshop papers will be published by the IEEE.» The CFP does not
29
+ say what a non-default route would be, or whether it is archival.
30
+ - **Presenting**: «At least one author of each accepted paper must register and present.»
31
+ - **Deadlines (AoE)**: submission 13 Nov 2026, reviews due 5 Dec, final notification 11 Dec 2026,
32
+ camera-ready 29 Jan 2027.
33
+
34
+ ## 🔴 The submission deadline moved
35
+
36
+ The organizers' email of 2026-10-23 (as received by an author; the email itself is not public):
37
+
38
+ > The submission deadline for AGENTVERIFY 2027 has been updated to November 13, 2026. We do not
39
+ > have a separate abstract submission deadline.
40
+
41
+ Source: **organizers' email, 2026-10-23**. On 2026-10-26 every public source agreed with it: the
42
+ CFP, the home page («Papers are due November 13th, 2026 (AoE)»), the ICSE workshops page («Workshop
43
+ Papers Submission Deadline for all Workshops: 13 November 2026») and the portal (`"sub":1794657599`,
44
+ 2026-11-14T11:59:59Z). None of the pages read showed 6 November. If a cached copy of any of them
45
+ does, the email and the live pages win.
46
+
47
+ ## Paper kinds and limits
48
+
49
+ | kind | pages | the call's words |
50
+ | ---------- | ----: | --------------------------------------------------------------------------------------------------------------------------------------- |
51
+ | `full` | 8 | «Full Papers: Maximum of 8 pages, including references.» |
52
+ | `short` | 4 | «Short & Demonstration Papers: Maximum of 4 pages, including references.» |
53
+ | `abstract` | 2 | «Extended Abstracts: Maximum of 2 pages, including references.» They «are free of APC charges and will be included in the proceedings.» |
54
+
55
+ 🔴 **Every limit is a total, references included.** `format/page-limit` can only compare banal's
56
+ body count with the number, so references standing alone on a page after a full body pass the rule
57
+ and are over the limit. Count the PDF's pages yourself before submitting.
58
+
59
+ ## What else the preset encodes
60
+
61
+ - **The IEEE template**, through `extends: paperlint:ieee-conference` ([`ieee-conference.md`](ieee-conference.md)).
62
+ - **Double-blind review** (`blind`), so `anonymity/identity` checks the PDF against the paper's
63
+ declared `identity`.
64
+ - **Portal and deadlines**: the submission reading is the portal's, refreshed by paperlint's
65
+ scheduled workflow; notification and camera-ready are the site's. No `registration`: there is no
66
+ abstract deadline.
67
+
68
+ ## Not verified
69
+
70
+ - **Remote presentation**: not stated (only «must register and present»).
71
+ - **Archival / non-archival choice**: «by default» suggests an alternative; none is described.
72
+ - **AI / LLM usage policy and required sections**: none stated.
73
+
74
+ ## What the preset resolves to
75
+
76
+ <!-- paperlint:preset-rules -->
77
+ <!-- prettier-ignore-start -->
78
+ <!-- Generated from agentverify.jsonc by `npx eslint --fix` (rule preset/card-rules). Edit the preset, not this section. -->
79
+
80
+ **`paperlint:agentverify`** — venue preset for AGENTVERIFY · call for papers: <https://agentverify2027.github.io/cfp.html> · extends `paperlint:ieee-conference`
81
+
82
+ No preset of this chain sets a rule: under it, every rule keeps paperlint's default.
83
+
84
+ <!-- prettier-ignore-end -->
85
+ <!-- /paperlint:preset-rules -->
@@ -30,7 +30,7 @@ versions. `compsoc` changes the text block and the headings, and nothing the `pd
30
30
  <!-- prettier-ignore-start -->
31
31
  <!-- Generated from ieee-conference.jsonc by `npx eslint --fix` (rule preset/card-rules). Edit the preset, not this section. -->
32
32
 
33
- **`paperlint:ieee-conference`** — template family, no venue of its own · extended by `paperlint:aidc`, `paperlint:msr`
33
+ **`paperlint:ieee-conference`** — template family, no venue of its own · extended by `paperlint:agent`, `paperlint:agentverify`, `paperlint:aidc`, `paperlint:msr`, `paperlint:nlbse`
34
34
 
35
35
  No preset of this chain sets a rule: under it, every rule keeps paperlint's default.
36
36
 
@@ -0,0 +1,76 @@
1
+ // NLBSE 2027 (the 6th Natural Language-based Software Engineering Workshop, an ICSE 2027 workshop)
2
+ // — a VENUE preset over the IEEE conference family. Everything the IEEEtran template decides comes
3
+ // from ieee-conference.jsonc; this file adds what NLBSE's call sets. Its card, in prose:
4
+ // `nlbse.md` beside this file.
5
+ //
6
+ // Verbatim from https://conf.researchr.org/home/icse-2027/nlbse-2027 (fetched 2026-10-26):
7
+ // «Full papers (maximum of 8 pages, including references).» · «Education tools and materials
8
+ // (maximum of 8 pages, including references).» · «Replication Studies and Negative results
9
+ // (maximum of 8 pages, including references).» · «Short and demonstration papers (maximum of 4
10
+ // pages, including references).» · «Position papers (maximum of 2 pages, including
11
+ // references).» · «Tool Competition entries (maximum of 4 pages, including references).»
12
+ // «All submissions must conform to the ICSE’27 formatting and submission instructions. All
13
+ // submissions must be anonymized, in PDF format and should be performed electronically through
14
+ // HotCRP: https://icse2027-nlbse.hotcrp.com/.»
15
+ // Dates: «Fri 13 Nov 2026 (updated) Paper submission / Fri 27 Nov 2026 (new) Tool
16
+ // competition/Research challenge paper submission / Fri 11 Dec 2026 Author notification / Fri
17
+ // 29 Jan 2027 Camera ready», «All dates are Anywhere on Earth (AoE).»
18
+ {
19
+ "type": "venue",
20
+ "name": "NLBSE",
21
+ // The call quoted above.
22
+ "url": "https://conf.researchr.org/home/icse-2027/nlbse-2027",
23
+ "extends": "paperlint:ieee-conference",
24
+
25
+ // «All submissions must be anonymized» (the call above), and `"blind":true` in the portal's
26
+ // status data (https://icse2027-nlbse.hotcrp.com/deadlines, read 2026-10-26). The call never
27
+ // says «double-blind»; anonymity/identity then requires the paper's `identity`.
28
+ "blind": true,
29
+
30
+ // «should be performed electronically through HotCRP» (the call above).
31
+ "portal": { "kind": "hotcrp", "url": "https://icse2027-nlbse.hotcrp.com" },
32
+
33
+ // 🔴 THE PORTAL HOLDS NO DEADLINE YET: on 2026-10-26 its status data read `"sub":{"open":false}`
34
+ // with no date, while the call lists 13 Nov. So these are the call's readings, in UTC at the end
35
+ // of the AoE day; the refresh adds the portal's the day it publishes one (the earlier binds).
36
+ // The competition papers' 27 Nov is in the card only: one reading per kind and source.
37
+ "deadlines": [
38
+ {
39
+ "what": "submission",
40
+ "at": "2026-11-14T11:59:59Z",
41
+ "source": "call",
42
+ "url": "https://conf.researchr.org/home/icse-2027/nlbse-2027",
43
+ "read": "2026-10-26",
44
+ },
45
+ {
46
+ "what": "notification",
47
+ "at": "2026-12-12T11:59:59Z",
48
+ "source": "call",
49
+ "url": "https://conf.researchr.org/home/icse-2027/nlbse-2027",
50
+ "read": "2026-10-26",
51
+ },
52
+ {
53
+ "what": "camera-ready",
54
+ "at": "2027-01-30T11:59:59Z",
55
+ "source": "call",
56
+ "url": "https://conf.researchr.org/home/icse-2027/nlbse-2027",
57
+ "read": "2026-10-26",
58
+ },
59
+ ],
60
+
61
+ "format": {
62
+ // 🔴 EVERY LIMIT INCLUDES THE REFERENCES, and no key gates a total: `body_pages_max` is banal's
63
+ // body count, never more than the total, so a paper over it is over the limit; references alone
64
+ // on a page after a full body pass this gate and are still over (nlbse.md). The competition
65
+ // papers' «2-4 pages» (Multitask Code Classification) does not say whether references count, so
66
+ // only the call's «Tool Competition entries» line is a kind; the research challenge has no limit.
67
+ "kinds": {
68
+ "full": { "body_pages_max": 8 },
69
+ "education": { "body_pages_max": 8 },
70
+ "replication": { "body_pages_max": 8 },
71
+ "short": { "body_pages_max": 4 },
72
+ "position": { "body_pages_max": 2 },
73
+ "tool-competition": { "body_pages_max": 4 },
74
+ },
75
+ },
76
+ }
@@ -0,0 +1,85 @@
1
+ ---
2
+ title: "NLBSE @ ICSE — venue card"
3
+ ---
4
+
5
+ # NLBSE @ ICSE
6
+
7
+ The card of the venue preset `paperlint:nlbse` ([`nlbse.jsonc`](nlbse.jsonc)): what the venue asks
8
+ for, in prose. The numbers a machine checks live in the preset; the section at the end is generated
9
+ from it. The `submit-paper` skill reads this card for the venue's specifics.
10
+
11
+ **Re-verify the call for papers every year**
12
+ ([conf.researchr.org/home/icse-2027/nlbse-2027](https://conf.researchr.org/home/icse-2027/nlbse-2027)).
13
+ Sources read 2026-10-26: that page, the portal's
14
+ [deadlines page](https://icse2027-nlbse.hotcrp.com/deadlines), and the
15
+ [ICSE 2027 workshops page](https://conf.researchr.org/track/icse-2027/icse-2027-workshops).
16
+
17
+ ## The venue (2027 edition)
18
+
19
+ - **Venue**: The 6th Natural Language-based Software Engineering Workshop, an ICSE 2027 workshop,
20
+ Sun 25 April 2027, Dublin. «NLBSE 2027 is sponsored by ACM SIGSOFT and IEEE-CS TCSE.»
21
+ - **Portal**: HotCRP, `https://icse2027-nlbse.hotcrp.com/`.
22
+ - **Format**: «All submissions must conform to the ICSE’27 formatting and submission instructions.»
23
+ No template or class line is named on the page; the ICSE 2027 workshops page says «The workshop
24
+ papers will be published by the IEEE.» and links the IEEE templates only for workshop proposals.
25
+ The preset keeps the family's `\documentclass[conference]{IEEEtran}`, an inference, not a quote.
26
+ - **Review**: «All submissions must be anonymized, in PDF format». The call never says single- or
27
+ double-blind; the portal's status data carries `"blind":true`, which the preset follows.
28
+ - **Publisher**: the call says only «The accepted submissions will be published in the workshop
29
+ proceedings.» (tool competitions) and «Accepted papers will appear in the NLBSE ’27 proceedings.»
30
+ (research challenge, skill classification). IEEE comes from the ICSE page above. No archival /
31
+ non-archival choice is mentioned.
32
+ - **Deadlines (AoE)**: paper submission 13 Nov 2026 (marked «updated»), tool competition and
33
+ research challenge papers **27 Nov 2026** (marked «new»), notification 11 Dec 2026, camera-ready
34
+ 29 Jan 2027. Only the 13 Nov line is in the preset.
35
+ - 🔴 **Disagreement**: the call lists 13 Nov, but the portal's status data on 2026-10-26 reads
36
+ `"sub":{"open":false}` with no date, i.e. it was closed and showed none. The preset therefore
37
+ carries the call's reading until the portal publishes one.
38
+
39
+ ## Paper kinds and limits
40
+
41
+ Every limit says «including references».
42
+
43
+ | kind | pages | the call's words |
44
+ | ------------------ | ----: | -------------------------------------------------------------------------------------- |
45
+ | `full` | 8 | «Full papers (maximum of 8 pages, including references).» |
46
+ | `education` | 8 | «Education tools and materials (maximum of 8 pages, including references).» |
47
+ | `replication` | 8 | «Replication Studies and Negative results (maximum of 8 pages, including references).» |
48
+ | `short` | 4 | «Short and demonstration papers (maximum of 4 pages, including references).» |
49
+ | `position` | 2 | «Position papers (maximum of 2 pages, including references).» |
50
+ | `tool-competition` | 4 | «Tool Competition entries (maximum of 4 pages, including references).» |
51
+
52
+ Two competition papers have no kind. Multitask Code Classification: «you must write a paper (2-4
53
+ pages)», references not mentioned. The research challenge states no limit.
54
+
55
+ 🔴 **The limits are totals.** `format/page-limit` can only compare banal's body count with the
56
+ number, so references standing alone on a page after a full body pass the rule and are over the
57
+ limit. Count the PDF's pages yourself before submitting.
58
+
59
+ ## What else the preset encodes
60
+
61
+ - **The IEEE template**, through `extends: paperlint:ieee-conference` ([`ieee-conference.md`](ieee-conference.md)).
62
+ - **Anonymous review** (`blind`), so `anonymity/identity` checks the PDF against the paper's
63
+ declared `identity`.
64
+ - **Portal and deadlines**: all three readings are the call's for now (see Disagreement above).
65
+
66
+ ## Not verified
67
+
68
+ - **Remote presentation**: not stated on the page.
69
+ - **AI / LLM usage policy**: not stated. Required content exists only for the competition papers
70
+ (model, pre-processing, tuning, validation results, code link; the challenge: research problem,
71
+ role of natural language, dataset version, methods, findings, reproducibility link).
72
+ - **The template's class line** and **whether a non-archival route exists**: not stated.
73
+
74
+ ## What the preset resolves to
75
+
76
+ <!-- paperlint:preset-rules -->
77
+ <!-- prettier-ignore-start -->
78
+ <!-- Generated from nlbse.jsonc by `npx eslint --fix` (rule preset/card-rules). Edit the preset, not this section. -->
79
+
80
+ **`paperlint:nlbse`** — venue preset for NLBSE · call for papers: <https://conf.researchr.org/home/icse-2027/nlbse-2027> · extends `paperlint:ieee-conference`
81
+
82
+ No preset of this chain sets a rule: under it, every rule keeps paperlint's default.
83
+
84
+ <!-- prettier-ignore-end -->
85
+ <!-- /paperlint:preset-rules -->
@@ -403,6 +403,9 @@ test("the exclude list is exactly the justified set", () => {
403
403
  // skill and CLAUDE.md sources vigiles compiles; a glob's `**` skips a dot directory
404
404
  "**/*.spec.ts",
405
405
  ".github/*.spec.ts",
406
+ // the same two kinds in a maintainer skill, which does not ship (`.claude/skills/<name>/`)
407
+ ".claude/skills/*/*.spec.ts",
408
+ ".claude/skills/*/*.eval.*",
406
409
  // type declarations
407
410
  "**/*.d.ts",
408
411
  "**/*.d.mts",
package/skills/CLAUDE.md CHANGED
@@ -1,4 +1,4 @@
1
- <!-- vigiles:sha256:e0aa2cec8926b04f compiled from skills/CLAUDE.md.spec.ts -->
1
+ <!-- vigiles:sha256:b9427668f4875ea5 compiled from skills/CLAUDE.md.spec.ts -->
2
2
 
3
3
  # CLAUDE.md
4
4
 
@@ -12,7 +12,7 @@ package's location come from `skills/paper-pipeline/scripts/consumer.mjs`; the l
12
12
 
13
13
  ### Skills Ship In Skills
14
14
 
15
- **Guidance only** — Skills ship in `skills/`, never `.claude/skills/`. `paperlint init` symlinks each one into the consumer's `.claude/skills/<name>`; the link's directory name is the skill's name. `skills/` is the npm layout: of 855 packages scanned, 336 shipped `skills/` against 15 `.claude/skills/`. Rejected: leaving the skills for Claude Code to find inside `node_modules` (`docs/install.md`); a symlink loads at startup, deterministically.
15
+ **Guidance only** — Skills ship in `skills/`, never `.claude/skills/`. `paperlint init` symlinks each one into the consumer's `.claude/skills/<name>`; the link's directory name is the skill's name. `skills/` is the npm layout: of 855 packages scanned, 336 shipped `skills/` against 15 `.claude/skills/`. Rejected: leaving the skills for Claude Code to find inside `node_modules` (`docs/install.md`); a symlink loads at startup, deterministically. A skill for working on paperlint itself (it edits this repository's presets, tests or source) does not ship: it lives in `.claude/skills/<name>/` as a real directory, outside `files`, and no shipped skill or user doc names it (`add-venue`).
16
16
 
17
17
  ### A Criterion That Judges Prose
18
18
 
@@ -9,7 +9,7 @@ ${file("src/link-skills.ts")}; install-specific paths are flagged by ${file("esl
9
9
  },
10
10
  rules: {
11
11
  "skills-ship-in-skills": guidance(
12
- "Skills ship in `skills/`, never `.claude/skills/`. `paperlint init` symlinks each one into the consumer's `.claude/skills/<name>`; the link's directory name is the skill's name. `skills/` is the npm layout: of 855 packages scanned, 336 shipped `skills/` against 15 `.claude/skills/`. Rejected: leaving the skills for Claude Code to find inside `node_modules` (`docs/install.md`); a symlink loads at startup, deterministically.",
12
+ "Skills ship in `skills/`, never `.claude/skills/`. `paperlint init` symlinks each one into the consumer's `.claude/skills/<name>`; the link's directory name is the skill's name. `skills/` is the npm layout: of 855 packages scanned, 336 shipped `skills/` against 15 `.claude/skills/`. Rejected: leaving the skills for Claude Code to find inside `node_modules` (`docs/install.md`); a symlink loads at startup, deterministically. A skill for working on paperlint itself (it edits this repository's presets, tests or source) does not ship: it lives in `.claude/skills/<name>/` as a real directory, outside `files`, and no shipped skill or user doc names it (`add-venue`).",
13
13
  ),
14
14
  "a-criterion-that-judges-prose": guidance(
15
15
  'It follows the five principles in `eslint-rules/CLAUDE.md` ("A rule that judges prose"). A skill does not grep prose for what a rule can count.',
@@ -11,7 +11,10 @@ the verdict line's wording and the `☐`/`☑` marks as they are, or the hook go
11
11
 
12
12
  ## The front matter
13
13
 
14
- Above the sections, the file carries YAML. The rules read its `stages` field:
14
+ Above the sections, the file carries YAML. In a paper WITHOUT `cycles` in its `paperlint.json`, the
15
+ rules read its `stages` field (a paper with `cycles` keeps no such list: its frozen versions are the
16
+ folders `versions/<cycle>/` and the generated `versions/lock.json`, and `paperlint versions migrate`
17
+ moves an old list there):
15
18
 
16
19
  ```yaml
17
20
  ---
@@ -4,7 +4,7 @@ description: End-to-end playbook for submitting a peer-reviewed paper to a doubl
4
4
  allowed-tools: [Read, Write, Edit, Grep, Glob, Bash, WebSearch, WebFetch, Skill]
5
5
  ---
6
6
 
7
- <!-- vigiles:sha256:81d5777962c2864a compiled from skills/submit-paper/SKILL.md.spec.ts -->
7
+ <!-- vigiles:sha256:567bfff9583abdf1 compiled from skills/submit-paper/SKILL.md.spec.ts -->
8
8
 
9
9
  # submit-paper — get a reviewed paper from "done" to "ready for review"
10
10
 
@@ -137,20 +137,28 @@ Many workshop HotCRPs have **no supplementary-upload field** — check first; if
137
137
 
138
138
  ## 7. Freeze the version you actually uploaded
139
139
 
140
- 🔴 **The moment the portal accepts the file, download it back and commit it.** The rendered PDF is the
141
- only artifact git cannot reconstruct — it was built outside the repo and lived on the portal.
140
+ 🔴 **What the reviewers read is the one version git cannot rebuild for you later** — it was built
141
+ outside the repo and lives on the portal. Freeze it at the send.
142
+
143
+ In a paper that keeps `cycles` in `paperlint.json`, the send freezes it:
144
+ `paperlint submission update <paper> --save` (and `new --save`) writes the exact PDF it sent and the
145
+ source the build read into
142
146
 
143
147
  ```
144
- <paper-dir>/versions/<ISO-date>-submitted.pdf
148
+ <paper-dir>/versions/<cycle id>/submitted.pdf
149
+ <paper-dir>/versions/<cycle id>/submitted.tex
145
150
  ```
146
151
 
147
- and in `PIPELINE-STATUS.md`, on the Submit row, the source that produced it: the literal word
148
- **`commit <hash>`**. Stages are `submitted` / `camera-ready` / `arxiv`; each one you *declare* in
149
- PIPELINE-STATUS needs its own frozen file.
152
+ and records their bytes and sha256 in `versions/lock.json` (generated; never edit it). After an
153
+ acceptance the same command freezes `camera-ready.{pdf,tex}`. An upload made on the portal by hand:
154
+ put that PDF and its source there under those names and run `npx paperlint versions lock <paper>`.
155
+ `npx paperlint versions` prints which version is under review and how far `paper.tex` has moved.
150
156
 
151
- **This is enforced, not advised** — `checkFrozenVersions()` in `.claude/hooks/paper-lint.mjs` reads the
152
- declared stages and reports a paper that declared one without freezing it. The convention itself is
153
- documented per-paper in `versions/README.md`.
157
+ **This is enforced, not advised** — `cycle/submitted-frozen` fails a cycle that has a submission and
158
+ no frozen `submitted.pdf`, and `versions/layout` fails a frozen file that no longer matches the lock.
159
+ A paper without `cycles` records each stage in `PIPELINE-STATUS.md`'s `stages:` list instead,
160
+ checked by `paper/stages` and `paper/source`; `npx paperlint versions migrate` moves it to the
161
+ folders once it keeps `cycles`.
154
162
 
155
163
  **Why it is a step and not hygiene:** `agenticdev-2026` kept a folder for exactly this for a month and
156
164
  the file inside was the wrong build (340 952 B vs the real 352 357 B). It surfaced only when a reviewer
@@ -153,20 +153,28 @@ Many workshop HotCRPs have **no supplementary-upload field** — check first; if
153
153
 
154
154
  ## 7. Freeze the version you actually uploaded
155
155
 
156
- 🔴 **The moment the portal accepts the file, download it back and commit it.** The rendered PDF is the
157
- only artifact git cannot reconstruct — it was built outside the repo and lived on the portal.
156
+ 🔴 **What the reviewers read is the one version git cannot rebuild for you later** — it was built
157
+ outside the repo and lives on the portal. Freeze it at the send.
158
+
159
+ In a paper that keeps \`cycles\` in \`paperlint.json\`, the send freezes it:
160
+ \`paperlint submission update <paper> --save\` (and \`new --save\`) writes the exact PDF it sent and the
161
+ source the build read into
158
162
 
159
163
  \`\`\`
160
- <paper-dir>/versions/<ISO-date>-submitted.pdf
164
+ <paper-dir>/versions/<cycle id>/submitted.pdf
165
+ <paper-dir>/versions/<cycle id>/submitted.tex
161
166
  \`\`\`
162
167
 
163
- and in \`PIPELINE-STATUS.md\`, on the Submit row, the source that produced it: the literal word
164
- **\`commit <hash>\`**. Stages are \`submitted\` / \`camera-ready\` / \`arxiv\`; each one you *declare* in
165
- PIPELINE-STATUS needs its own frozen file.
168
+ and records their bytes and sha256 in \`versions/lock.json\` (generated; never edit it). After an
169
+ acceptance the same command freezes \`camera-ready.{pdf,tex}\`. An upload made on the portal by hand:
170
+ put that PDF and its source there under those names and run \`npx paperlint versions lock <paper>\`.
171
+ \`npx paperlint versions\` prints which version is under review and how far \`paper.tex\` has moved.
166
172
 
167
- **This is enforced, not advised** — \`checkFrozenVersions()\` in \`.claude/hooks/paper-lint.mjs\` reads the
168
- declared stages and reports a paper that declared one without freezing it. The convention itself is
169
- documented per-paper in \`versions/README.md\`.
173
+ **This is enforced, not advised** — \`cycle/submitted-frozen\` fails a cycle that has a submission and
174
+ no frozen \`submitted.pdf\`, and \`versions/layout\` fails a frozen file that no longer matches the lock.
175
+ A paper without \`cycles\` records each stage in \`PIPELINE-STATUS.md\`'s \`stages:\` list instead,
176
+ checked by \`paper/stages\` and \`paper/source\`; \`npx paperlint versions migrate\` moves it to the
177
+ folders once it keeps \`cycles\`.
170
178
 
171
179
  **Why it is a step and not hygiene:** \`agenticdev-2026\` kept a folder for exactly this for a month and
172
180
  the file inside was the wrong build (340 952 B vs the real 352 357 B). It surfaced only when a reviewer
@@ -81,9 +81,11 @@ need to wait for them or make them up.
81
81
 
82
82
  ## The frozen version
83
83
 
84
- Camera-ready is a separate **stage** in the sense of `versions/` (see `checkFrozenVersions()` in
85
- `paper-lint.mjs`): the uploaded PDF is stored as `versions/<date>-camera-ready.pdf`, and the commit
86
- of its source is recorded in `PIPELINE-STATUS.md` as `commit <hash>`.
84
+ Camera-ready is a separate **stage** in `versions/`: once the cycle's `outcome` is `accepted`,
85
+ `paperlint submission update <paper> --save` freezes the PDF it sends and its source as
86
+ `versions/<cycle>/camera-ready.{pdf,tex}` and records them in `versions/lock.json`. A camera-ready
87
+ uploaded through the publisher's own system (not the review portal) goes there by hand, followed by
88
+ `npx paperlint versions lock <paper>`.
87
89
 
88
90
  **Why this isn't a formality:** the arXiv posting and the extension paper get diffed against
89
91
  camera-ready later, and reviewer references to line numbers only resolve against the exact render
@@ -6,9 +6,31 @@ import {
6
6
  fixedDownload,
7
7
  memoryFiles,
8
8
  memoryListEntries,
9
+ memoryCopyFile,
10
+ memoryMoveFile,
9
11
  memoryWorkspace,
10
12
  } from "./index.ts";
11
13
 
14
+ test("memoryMoveFile: the bytes move to the new path; a path holding nothing throws", () => {
15
+ const files = memoryFiles({ "/p/a.pdf": "x" });
16
+ const move = memoryMoveFile(files);
17
+ move(absolutePath("/p/a.pdf"), absolutePath("/p/v/b.pdf"));
18
+ assert.deepEqual([...files.map.keys()], ["/p/v/b.pdf"]);
19
+ assert.throws(() => {
20
+ move(absolutePath("/p/a.pdf"), absolutePath("/p/c.pdf"));
21
+ }, /ENOENT/);
22
+ });
23
+
24
+ test("memoryCopyFile: the bytes under the new path too; a path holding nothing throws", () => {
25
+ const files = memoryFiles({ "/p/a.tex": "x" });
26
+ const copy = memoryCopyFile(files);
27
+ copy(absolutePath("/p/a.tex"), absolutePath("/p/v/b.tex"));
28
+ assert.deepEqual([...files.map.keys()], ["/p/a.tex", "/p/v/b.tex"]);
29
+ assert.throws(() => {
30
+ copy(absolutePath("/p/none"), absolutePath("/p/c.tex"));
31
+ }, /ENOENT/);
32
+ });
33
+
12
34
  test("memoryWorkspace: a `within` that throws is recorded as `threw`, and the error goes on", () => {
13
35
  const ws = memoryWorkspace();
14
36
  assert.throws(
@@ -7,6 +7,7 @@ import type { Download } from "../../ports/download.ts";
7
7
  import type { Files } from "../../ports/files.ts";
8
8
  import type { ListDir } from "../../ports/talk-media.ts";
9
9
  import type { ListEntries } from "../../ports/list-entries.ts";
10
+ import type { CopyFile, MoveFile } from "../../ports/move-file.ts";
10
11
  import type { Command, ProcessExit, RunProcess } from "../../ports/process.ts";
11
12
  import type { NotAPromise, Scratch, Workspace } from "../../ports/workspace.ts";
12
13
  import { err, ok } from "../../domain/result.ts";
@@ -73,6 +74,26 @@ export const memoryListDir =
73
74
  ),
74
75
  ];
75
76
 
77
+ /** `MoveFile` over `memoryFiles`: the bytes under the new path, none under the old; nothing there throws. */
78
+ /** `CopyFile` over `memoryFiles`: the bytes under the new path too; nothing there throws. */
79
+ export const memoryCopyFile =
80
+ (files: MemoryFiles): CopyFile =>
81
+ (from, to) => {
82
+ const b = files.map.get(from);
83
+ if (b === undefined) throw new Error(`ENOENT: no file to copy at ${from}`);
84
+ files.writeAtomic(to, b);
85
+ };
86
+
87
+ export const memoryMoveFile =
88
+ (files: MemoryFiles): MoveFile =>
89
+ (from, to) => {
90
+ const b = files.map.get(from);
91
+ if (b === undefined) throw new Error(`ENOENT: no file to move at ${from}`);
92
+ files.writeAtomic(to, b);
93
+ // eslint-disable-next-line functional/immutable-data -- a test double's disk is the mutable map memoryFiles keeps
94
+ files.map.delete(from);
95
+ };
96
+
76
97
  /**
77
98
  * `ListEntries` over `memoryFiles`: each name directly under a directory, a file when it is one, else a
78
99
  * directory. Memory holds no links: the walk's refusal to follow one is the node adapter's to show.
@@ -36,3 +36,4 @@ export { spawnProcess, type SpawnSync } from "./process.io.ts";
36
36
  export { hostDirs } from "./host.io.ts";
37
37
  export { nodeListDir } from "./list-dir.io.ts";
38
38
  export { nodeListEntries } from "./list-entries.io.ts";
39
+ export { nodeCopyFile, nodeMoveFile } from "./move-file.io.ts";
@@ -0,0 +1,21 @@
1
+ /** `MoveFile` and `CopyFile` over `node:fs`: a rename or a copy, the directory made first. */
2
+ import { copyFileSync, mkdirSync, renameSync } from "node:fs";
3
+ import { dirname } from "node:path";
4
+ import type { AbsolutePath } from "../../domain/paths.ts";
5
+ import type { CopyFile, MoveFile } from "../../ports/move-file.ts";
6
+
7
+ export const nodeMoveFile: MoveFile = (
8
+ from: AbsolutePath,
9
+ to: AbsolutePath,
10
+ ): void => {
11
+ mkdirSync(dirname(to), { recursive: true });
12
+ renameSync(from, to);
13
+ };
14
+
15
+ export const nodeCopyFile: CopyFile = (
16
+ from: AbsolutePath,
17
+ to: AbsolutePath,
18
+ ): void => {
19
+ mkdirSync(dirname(to), { recursive: true });
20
+ copyFileSync(from, to);
21
+ };
@@ -0,0 +1,51 @@
1
+ /** `nodeMoveFile`: the file arrives with its bytes, into a folder made for it, and is gone from where it was. */
2
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { describe, expect, it } from "vitest";
5
+ import { absolutePath } from "../../domain/paths.ts";
6
+ import { useTempDir } from "../../../test/support.ts";
7
+ import { nodeCopyFile, nodeMoveFile } from "./move-file.io.ts";
8
+
9
+ const root = useTempDir("paperlint-move-file-");
10
+
11
+ describe("nodeMoveFile", () => {
12
+ it("moves a file into a folder that does not exist yet", () => {
13
+ writeFileSync(join(root, "a.pdf"), "bytes");
14
+ nodeMoveFile(
15
+ absolutePath(join(root, "a.pdf")),
16
+ absolutePath(join(root, "x", "y", "b.pdf")),
17
+ );
18
+ expect(existsSync(join(root, "a.pdf"))).toBe(false);
19
+ expect(readFileSync(join(root, "x", "y", "b.pdf"), "utf8")).toBe("bytes");
20
+ });
21
+
22
+ it("throws when there is nothing to move", () => {
23
+ expect(() => {
24
+ nodeMoveFile(
25
+ absolutePath(join(root, "none")),
26
+ absolutePath(join(root, "z")),
27
+ );
28
+ }).toThrow();
29
+ });
30
+ });
31
+
32
+ describe("nodeCopyFile", () => {
33
+ it("copies a file into a folder that does not exist yet, and leaves it where it was", () => {
34
+ writeFileSync(join(root, "s.tex"), "source");
35
+ nodeCopyFile(
36
+ absolutePath(join(root, "s.tex")),
37
+ absolutePath(join(root, "c", "d", "s.tex")),
38
+ );
39
+ expect(readFileSync(join(root, "s.tex"), "utf8")).toBe("source");
40
+ expect(readFileSync(join(root, "c", "d", "s.tex"), "utf8")).toBe("source");
41
+ });
42
+
43
+ it("throws when there is nothing to copy", () => {
44
+ expect(() => {
45
+ nodeCopyFile(
46
+ absolutePath(join(root, "none")),
47
+ absolutePath(join(root, "w")),
48
+ );
49
+ }).toThrow();
50
+ });
51
+ });
@@ -0,0 +1,16 @@
1
+ /** `parseYaml`: the value, a bare date as a `Date`, or the parser's own message. */
2
+ import { expect, test } from "vitest";
3
+ import { parseYaml } from "./index.ts";
4
+
5
+ test("parses a mapping, a date without quotes as a Date; refuses what is not YAML with the reason", () => {
6
+ expect(parseYaml("a: 1\nd: 2026-07-22\n")).toStrictEqual({
7
+ ok: true,
8
+ value: { a: 1, d: new Date("2026-07-22T00:00:00.000Z") },
9
+ });
10
+ const bad = parseYaml("a: [");
11
+ expect(bad.ok).toBe(false);
12
+ expect(bad).toStrictEqual({
13
+ ok: false,
14
+ error: "unexpected end of the stream within a flow collection (2:1)",
15
+ });
16
+ });
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `ParseYaml` over js-yaml: the one YAML reader of `src/`. A date without quotes reads as a `Date`.
3
+ * A refusal is the parser's first line — the reason and its line:column — without the excerpt it
4
+ * prints under it, so it fits in a one-line message.
5
+ */
6
+ import { load } from "js-yaml";
7
+ import { messageOf } from "../../domain/text.ts";
8
+ import { err, ok } from "../../domain/result.ts";
9
+ import type { ParseYaml } from "../../ports/yaml.ts";
10
+
11
+ export const parseYaml: ParseYaml = (text) => {
12
+ try {
13
+ return ok(load(text));
14
+ } catch (e) {
15
+ return err(messageOf(e).split("\n", 1).join(""));
16
+ }
17
+ };