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
@@ -1,119 +0,0 @@
1
- /**
2
- * `cycle/stage` — a declared stage names the ATTEMPT it belongs to, and that attempt exists.
3
- *
4
- * With `cycles` in the paper's `paperlint.json`, a `stages` entry in the scorecard's frontmatter
5
- * says `cycle: <id>` instead of a free `venue:` string: the venue's name is then the cycle's
6
- * preset's, spelled once, and a consumer can tell which rows of the scorecard belong to which
7
- * attempt. Three things can be wrong, each an error:
8
- *
9
- * noCycle `cycles` is declared and a stage names no `cycle`
10
- * unknownCycle the stage's `cycle` is no declared cycle's id
11
- * porting a `submitted` or `camera-ready` stage in a cycle whose phase is still `porting` —
12
- * a PDF was sent while the record says the source is in the previous venue's template
13
- *
14
- * Without `cycles` (the flat form) the rule is silent: `venue:` stays what it was. The settings file
15
- * is read here only for the cycles' ids and phases; `cycle/record` (src/cycle-rules.ts) judges the
16
- * whole record.
17
- */
18
- import { existsSync, readFileSync } from "node:fs";
19
- import { dirname, join } from "node:path";
20
- import { load } from "js-yaml";
21
- import { z } from "zod";
22
- const SETTINGS_FILE = "paperlint.json";
23
- /** What this rule reads of a cycle: its id and phase. `cycle/record` parses the rest. */
24
- const CycleHead = z.looseObject({
25
- id: z.string(),
26
- phase: z.string().optional(),
27
- });
28
- const Settings = z.looseObject({
29
- cycles: z.array(CycleHead.catch({ id: "" })).optional(),
30
- });
31
- const Frontmatter = z.looseObject({ stages: z.unknown().optional() }).catch({});
32
- const Stage = z
33
- .looseObject({
34
- stage: z.unknown().optional(),
35
- cycle: z.unknown().optional(),
36
- date: z.unknown().optional(),
37
- })
38
- .catch({});
39
- /** The stages that send a PDF to the venue: a cycle still porting cannot have one. */
40
- const SENT = ["submitted", "camera-ready"];
41
- /** The declared cycles' heads, or null when the paper has no `cycles` (or no readable settings). */
42
- function cyclesOf(dir) {
43
- const file = join(dir, SETTINGS_FILE);
44
- if (!existsSync(file))
45
- return null;
46
- try {
47
- const s = Settings.safeParse(JSON.parse(readFileSync(file, "utf8")));
48
- return s.success && s.data.cycles !== undefined
49
- ? s.data.cycles.map((c) => ({ id: c.id, phase: c.phase ?? "prepared" }))
50
- : null;
51
- }
52
- catch {
53
- return null;
54
- }
55
- }
56
- const text = (v) => typeof v === "string" ? v : v === undefined ? "" : JSON.stringify(v);
57
- /** A YAML date without quotes is a `Date`; shown as the day it names, as `paper/stages` does. */
58
- const dateText = (v) => v instanceof Date ? v.toISOString().slice(0, 10) : text(v);
59
- /** Each stage against the declared cycles. Pure. */
60
- export function judgeStages(stages, cycles) {
61
- const known = cycles.map((c) => c.id).join(" · ") || "(none)";
62
- return stages.flatMap((entry) => {
63
- const s = Stage.parse(entry);
64
- const stage = text(s.stage);
65
- const date = dateText(s.date);
66
- if (s.cycle === undefined)
67
- return [{ messageId: "noCycle", data: { stage, date, known } }];
68
- const id = text(s.cycle);
69
- const cycle = cycles.find((c) => c.id === id);
70
- if (cycle === undefined)
71
- return [{ messageId: "unknownCycle", data: { stage, date, id, known } }];
72
- return SENT.includes(stage) && cycle.phase === "porting"
73
- ? [{ messageId: "porting", data: { stage, date, id } }]
74
- : [];
75
- });
76
- }
77
- export default {
78
- rules: {
79
- stage: {
80
- meta: {
81
- type: "problem",
82
- docs: {
83
- description: "with `cycles` declared, every stage names the cycle it belongs to, and no PDF is sent from a cycle still porting",
84
- url: "https://github.com/zernie/paperlint/blob/main/docs/rules/cycle/stage.md",
85
- },
86
- schema: [],
87
- messages: {
88
- noCycle: "the «{{stage}}» stage ({{date}}) names no `cycle` — this paper declares `cycles` in paperlint.json, so each stage says which attempt it belongs to: `cycle: <id>`, one of {{known}}",
89
- unknownCycle: "the «{{stage}}» stage ({{date}}) names the cycle «{{id}}», which paperlint.json does not declare — the declared ids: {{known}}",
90
- porting: "the «{{stage}}» stage ({{date}}) belongs to the cycle «{{id}}», whose phase is still `porting`: a PDF was sent while the record says the source is in the previous venue's template. Finish the port and drop the phase, or the stage is wrong",
91
- },
92
- },
93
- create(context) {
94
- const cycles = cyclesOf(dirname(context.filename));
95
- if (cycles === null)
96
- return {};
97
- return {
98
- yaml(node) {
99
- const data = (() => {
100
- try {
101
- return load(node.value);
102
- }
103
- catch {
104
- return undefined; // `paper/stages` reports the unreadable YAML
105
- }
106
- })();
107
- const stages = Frontmatter.parse(data).stages;
108
- if (!Array.isArray(stages))
109
- return;
110
- judgeStages(stages, cycles).forEach((f) => {
111
- context.report({ node, messageId: f.messageId, data: f.data });
112
- });
113
- },
114
- };
115
- },
116
- },
117
- },
118
- };
119
- //# sourceMappingURL=cycle-stage.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cycle-stage.js","sourceRoot":"","sources":["../../eslint-rules/cycle-stage.ts"],"names":[],"mappings":"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"}
@@ -1,85 +0,0 @@
1
- # cycle/stage
2
-
3
- **Level:** error · **Reads:** the paper's `PIPELINE-STATUS.md` frontmatter (`stages`) and its
4
- `paperlint.json` (`cycles`) · **Reported on:** the frontmatter of `PIPELINE-STATUS.md`
5
-
6
- ## What it catches
7
-
8
- With `cycles` declared in the paper's `paperlint.json`, a `stages` entry in the scorecard's
9
- frontmatter must say which attempt it belongs to, `cycle: <id>`, and that attempt must exist. Three
10
- things can be wrong:
11
-
12
- - a stage names no `cycle`;
13
- - a stage names a `cycle` no declared cycle has as its `id`;
14
- - a `submitted` or `camera-ready` stage belongs to a cycle whose `phase` is still `porting` — a PDF
15
- was sent to the venue while the record says the source is still in the previous venue's template.
16
-
17
- Without `cycles` (the flat form) the rule is silent and `venue:` stays what it was.
18
-
19
- ## Why
20
-
21
- A paper's attempts are recorded once, in `paperlint.json`; the scorecard's stages are the evidence
22
- — which PDF, how many bytes, when. A stage that names its cycle says which attempt the evidence
23
- belongs to, and the venue's name is then spelled once, in the preset, instead of as a free string
24
- retyped per stage (`REALM @ EMNLP 2026`, `AISec 2026 @ ACM CCS`, …). A stage whose cycle does not
25
- exist is evidence of nothing.
26
-
27
- The `porting` case is the other half of a declaration: `"phase": "porting"` silences the venue's
28
- format rules because the source is, by declaration, not yet in the venue's template. A PDF sent
29
- from such a cycle contradicts that declaration — either the port is done and the phase is stale, or
30
- the stage is wrong.
31
-
32
- ## Examples
33
-
34
- Failing — `paperlint.json` declares one cycle, `open-2027`, and `PIPELINE-STATUS.md` has
35
-
36
- ```yaml
37
- ---
38
- stages:
39
- - stage: submitted
40
- date: 2026-07-22
41
- pdf: versions/a.pdf
42
- bytes: 1
43
- ---
44
- ```
45
-
46
- ```text
47
- papers/my-paper/PIPELINE-STATUS.md
48
- 1:1 error the «submitted» stage (2026-07-22) names no `cycle` — this paper declares `cycles` in
49
- paperlint.json, so each stage says which attempt it belongs to: `cycle: <id>`, one of
50
- open-2027 cycle/stage
51
- ```
52
-
53
- Passing:
54
-
55
- ```yaml
56
- ---
57
- stages:
58
- - stage: submitted
59
- cycle: open-2027
60
- date: 2026-07-22
61
- pdf: versions/a.pdf
62
- bytes: 1
63
- ---
64
- ```
65
-
66
- Also passing: a paper without `cycles`, whatever its stages say.
67
-
68
- ## Options / preset fields
69
-
70
- No rule options.
71
-
72
- ## What it does not check
73
-
74
- - The PDF, its bytes, or the frozen source — [`paper/stages`](../../rules.md) and `paper/source`
75
- do.
76
- - Whether a stage's `date` falls inside its cycle (after `opened`, before the outcome's `date`).
77
- - Whether a cycle without any `submitted` stage was in fact submitted; that is the portal's record
78
- (`paperlint submission`), not the scorecard's.
79
- - The rest of the cycle record — [`cycle/record`](record.md) does.
80
-
81
- ## How to fix
82
-
83
- Add `cycle: <id>` to each stage, with the id of the cycle the PDF was sent in. For a stage whose
84
- cycle is still `porting`: if the port is done, remove `"phase": "porting"` from the cycle
85
- ([`cycle/port-done`](port-done.md) says when); if no PDF was sent, remove the stage.
@@ -1,131 +0,0 @@
1
- /**
2
- * `cycle/stage` on a scorecard's frontmatter beside a settings file: a stage names its cycle, the
3
- * cycle exists, and no PDF was sent from a cycle still porting. Silent on the flat form.
4
- */
5
- import assert from "node:assert/strict";
6
- import { join } from "node:path";
7
- import markdown from "@eslint/markdown";
8
- import { Linter } from "eslint";
9
- import { test } from "vitest";
10
- import { useTempDir, writeTree } from "../test/support.ts";
11
- import cycleStage from "./cycle-stage.ts";
12
-
13
- const root = useTempDir("cycle-stage-");
14
- const linter = new Linter({ cwd: root });
15
-
16
- const SETTINGS = JSON.stringify({
17
- cycles: [
18
- {
19
- id: "alpha-2026",
20
- venue: { kind: "preset", extends: "paperlint:acm-sigconf" },
21
- opened: "2026-07-01",
22
- outcome: { kind: "rejected", date: "2026-09-08", evidence: "r.md" },
23
- },
24
- {
25
- id: "beta-2027",
26
- venue: { kind: "preset", extends: "paperlint:ieee-conference" },
27
- opened: "2026-09-09",
28
- phase: "porting",
29
- },
30
- ],
31
- });
32
-
33
- /** The messages of the rule on a PIPELINE-STATUS.md with `frontmatter`, beside `settings`. */
34
- function lint(name, frontmatter, settings = SETTINGS) {
35
- const dir = writeTree(join(root, name), {
36
- ...(settings === null ? {} : { "paperlint.json": settings }),
37
- });
38
- const msgs = linter.verify(
39
- `---\n${frontmatter}\n---\n\n# Status\n`,
40
- [
41
- {
42
- files: ["**/*.md"],
43
- plugins: { markdown, cycle: cycleStage },
44
- language: "markdown/gfm",
45
- languageOptions: { frontmatter: "yaml" },
46
- rules: { "cycle/stage": "error" },
47
- },
48
- ],
49
- join(dir, "PIPELINE-STATUS.md"),
50
- );
51
- assert.deepEqual(
52
- msgs.filter((m) => m.fatal),
53
- [],
54
- `${name}: the rule threw`,
55
- );
56
- return msgs.map((m) => m.message);
57
- }
58
-
59
- const SUBMITTED = (cycle) =>
60
- `stages:\n - stage: submitted\n ${cycle}\n date: 2026-08-06\n pdf: versions/a.pdf\n bytes: 1`;
61
-
62
- test("a stage naming a declared cycle: silent", () => {
63
- assert.deepEqual(lint("ok", SUBMITTED("cycle: alpha-2026")), []);
64
- });
65
-
66
- test("the flat form (no `cycles`), no settings file, or no stages: silent", () => {
67
- const flat = JSON.stringify({ extends: "paperlint:aidc" });
68
- assert.deepEqual(lint("flat", SUBMITTED("venue: Somewhere"), flat), []);
69
- assert.deepEqual(lint("none", SUBMITTED("venue: Somewhere"), null), []);
70
- assert.deepEqual(lint("empty", "stages: []"), []);
71
- });
72
-
73
- test("a stage with no `cycle` names the declared ids; one naming an unknown id names it", () => {
74
- assert.deepEqual(lint("no-cycle", SUBMITTED("venue: Somewhere")), [
75
- "the «submitted» stage (2026-08-06) names no `cycle` — this paper declares `cycles` in paperlint.json, so each stage says which attempt it belongs to: `cycle: <id>`, one of alpha-2026 · beta-2027",
76
- ]);
77
- assert.deepEqual(lint("unknown", SUBMITTED("cycle: gamma-2028")), [
78
- "the «submitted» stage (2026-08-06) names the cycle «gamma-2028», which paperlint.json does not declare — the declared ids: alpha-2026 · beta-2027",
79
- ]);
80
- });
81
-
82
- test("a PDF sent from a cycle still porting is named; a stage that sends none is not", () => {
83
- assert.deepEqual(lint("porting", SUBMITTED("cycle: beta-2027")), [
84
- "the «submitted» stage (2026-08-06) belongs to the cycle «beta-2027», whose phase is still `porting`: a PDF was sent while the record says the source is in the previous venue's template. Finish the port and drop the phase, or the stage is wrong",
85
- ]);
86
- assert.deepEqual(
87
- lint(
88
- "arxiv-porting",
89
- SUBMITTED("cycle: beta-2027").replace("submitted", "arxiv"),
90
- ),
91
- [],
92
- );
93
- });
94
-
95
- test("a settings file that is not JSON, or frontmatter that is not YAML: silent — other rules report those", () => {
96
- assert.deepEqual(lint("not-json", SUBMITTED("venue: X"), "{ not json"), []);
97
- assert.deepEqual(lint("not-yaml", "stages: [unclosed"), []);
98
- });
99
-
100
- test("`cycles: []` declared: a stage without a cycle is told there are none to name", () => {
101
- const [msg] = lint(
102
- "no-cycles",
103
- SUBMITTED("venue: X"),
104
- JSON.stringify({ cycles: [] }),
105
- );
106
- assert.match(msg, /one of \(none\)$/);
107
- });
108
-
109
- test("a stage and a cycle that are not strings, and a quoted date, are shown as written", () => {
110
- const [msg] = lint(
111
- "numbers",
112
- 'stages:\n - stage: 5\n cycle: 7\n date: "2026-08-06"\n pdf: versions/a.pdf\n bytes: 1',
113
- );
114
- assert.match(msg, /the «5» stage \(2026-08-06\) names the cycle «7»/);
115
- });
116
-
117
- test("a stage entry with no `stage` key is shown as an empty name", () => {
118
- const [msg] = lint(
119
- "no-stage",
120
- "stages:\n - cycle: gamma-2028\n date: 2026-08-06\n pdf: versions/a.pdf\n bytes: 1",
121
- );
122
- assert.match(
123
- msg,
124
- /^the «» stage \(2026-08-06\) names the cycle «gamma-2028»/,
125
- );
126
- });
127
-
128
- test("a YAML date without quotes is still a date in the message", () => {
129
- const [msg] = lint("yaml-date", SUBMITTED("venue: X"));
130
- assert.match(msg, /\(2026-08-06\)/);
131
- });
@@ -1,141 +0,0 @@
1
- /**
2
- * `cycle/stage` — a declared stage names the ATTEMPT it belongs to, and that attempt exists.
3
- *
4
- * With `cycles` in the paper's `paperlint.json`, a `stages` entry in the scorecard's frontmatter
5
- * says `cycle: <id>` instead of a free `venue:` string: the venue's name is then the cycle's
6
- * preset's, spelled once, and a consumer can tell which rows of the scorecard belong to which
7
- * attempt. Three things can be wrong, each an error:
8
- *
9
- * noCycle `cycles` is declared and a stage names no `cycle`
10
- * unknownCycle the stage's `cycle` is no declared cycle's id
11
- * porting a `submitted` or `camera-ready` stage in a cycle whose phase is still `porting` —
12
- * a PDF was sent while the record says the source is in the previous venue's template
13
- *
14
- * Without `cycles` (the flat form) the rule is silent: `venue:` stays what it was. The settings file
15
- * is read here only for the cycles' ids and phases; `cycle/record` (src/cycle-rules.ts) judges the
16
- * whole record.
17
- */
18
- import { existsSync, readFileSync } from "node:fs";
19
- import { dirname, join } from "node:path";
20
- import { load } from "js-yaml";
21
- import { z } from "zod";
22
- import type { RuleContext } from "./rule-context.ts";
23
-
24
- const SETTINGS_FILE = "paperlint.json";
25
-
26
- /** What this rule reads of a cycle: its id and phase. `cycle/record` parses the rest. */
27
- const CycleHead = z.looseObject({
28
- id: z.string(),
29
- phase: z.string().optional(),
30
- });
31
- const Settings = z.looseObject({
32
- cycles: z.array(CycleHead.catch({ id: "" })).optional(),
33
- });
34
- const Frontmatter = z.looseObject({ stages: z.unknown().optional() }).catch({});
35
- const Stage = z
36
- .looseObject({
37
- stage: z.unknown().optional(),
38
- cycle: z.unknown().optional(),
39
- date: z.unknown().optional(),
40
- })
41
- .catch({});
42
-
43
- /** The stages that send a PDF to the venue: a cycle still porting cannot have one. */
44
- const SENT: readonly string[] = ["submitted", "camera-ready"];
45
-
46
- interface Head {
47
- readonly id: string;
48
- readonly phase: string;
49
- }
50
-
51
- /** The declared cycles' heads, or null when the paper has no `cycles` (or no readable settings). */
52
- function cyclesOf(dir: string): readonly Head[] | null {
53
- const file = join(dir, SETTINGS_FILE);
54
- if (!existsSync(file)) return null;
55
- try {
56
- const s = Settings.safeParse(JSON.parse(readFileSync(file, "utf8")));
57
- return s.success && s.data.cycles !== undefined
58
- ? s.data.cycles.map((c) => ({ id: c.id, phase: c.phase ?? "prepared" }))
59
- : null;
60
- } catch {
61
- return null;
62
- }
63
- }
64
-
65
- const text = (v: unknown): string =>
66
- typeof v === "string" ? v : v === undefined ? "" : JSON.stringify(v);
67
-
68
- /** A YAML date without quotes is a `Date`; shown as the day it names, as `paper/stages` does. */
69
- const dateText = (v: unknown): string =>
70
- v instanceof Date ? v.toISOString().slice(0, 10) : text(v);
71
-
72
- interface Finding {
73
- readonly messageId: "noCycle" | "unknownCycle" | "porting";
74
- readonly data: Readonly<Record<string, string>>;
75
- }
76
-
77
- /** Each stage against the declared cycles. Pure. */
78
- export function judgeStages(
79
- stages: readonly unknown[],
80
- cycles: readonly Head[],
81
- ): readonly Finding[] {
82
- const known = cycles.map((c) => c.id).join(" · ") || "(none)";
83
- return stages.flatMap((entry): readonly Finding[] => {
84
- const s = Stage.parse(entry);
85
- const stage = text(s.stage);
86
- const date = dateText(s.date);
87
- if (s.cycle === undefined)
88
- return [{ messageId: "noCycle", data: { stage, date, known } }];
89
- const id = text(s.cycle);
90
- const cycle = cycles.find((c) => c.id === id);
91
- if (cycle === undefined)
92
- return [{ messageId: "unknownCycle", data: { stage, date, id, known } }];
93
- return SENT.includes(stage) && cycle.phase === "porting"
94
- ? [{ messageId: "porting", data: { stage, date, id } }]
95
- : [];
96
- });
97
- }
98
-
99
- export default {
100
- rules: {
101
- stage: {
102
- meta: {
103
- type: "problem" as const,
104
- docs: {
105
- description:
106
- "with `cycles` declared, every stage names the cycle it belongs to, and no PDF is sent from a cycle still porting",
107
- url: "https://github.com/zernie/paperlint/blob/main/docs/rules/cycle/stage.md",
108
- },
109
- schema: [],
110
- messages: {
111
- noCycle:
112
- "the «{{stage}}» stage ({{date}}) names no `cycle` — this paper declares `cycles` in paperlint.json, so each stage says which attempt it belongs to: `cycle: <id>`, one of {{known}}",
113
- unknownCycle:
114
- "the «{{stage}}» stage ({{date}}) names the cycle «{{id}}», which paperlint.json does not declare — the declared ids: {{known}}",
115
- porting:
116
- "the «{{stage}}» stage ({{date}}) belongs to the cycle «{{id}}», whose phase is still `porting`: a PDF was sent while the record says the source is in the previous venue's template. Finish the port and drop the phase, or the stage is wrong",
117
- },
118
- },
119
- create(context: RuleContext) {
120
- const cycles = cyclesOf(dirname(context.filename));
121
- if (cycles === null) return {};
122
- return {
123
- yaml(node: { readonly value: string }) {
124
- const data = ((): unknown => {
125
- try {
126
- return load(node.value);
127
- } catch {
128
- return undefined; // `paper/stages` reports the unreadable YAML
129
- }
130
- })();
131
- const stages = Frontmatter.parse(data).stages;
132
- if (!Array.isArray(stages)) return;
133
- judgeStages(stages, cycles).forEach((f) => {
134
- context.report({ node, messageId: f.messageId, data: f.data });
135
- });
136
- },
137
- };
138
- },
139
- },
140
- },
141
- };