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.
- package/CONTRIBUTING.md +2 -1
- package/dist/adapters/memory/index.d.ts +5 -0
- package/dist/adapters/memory/index.d.ts.map +1 -1
- package/dist/adapters/memory/index.js +16 -0
- package/dist/adapters/memory/index.js.map +1 -1
- package/dist/adapters/node/index.d.ts +1 -0
- package/dist/adapters/node/index.d.ts.map +1 -1
- package/dist/adapters/node/index.js +1 -0
- package/dist/adapters/node/index.js.map +1 -1
- package/dist/adapters/node/move-file.io.d.ts +4 -0
- package/dist/adapters/node/move-file.io.d.ts.map +1 -0
- package/dist/adapters/node/move-file.io.js +12 -0
- package/dist/adapters/node/move-file.io.js.map +1 -0
- package/dist/adapters/yaml/index.d.ts +3 -0
- package/dist/adapters/yaml/index.d.ts.map +1 -0
- package/dist/adapters/yaml/index.js +17 -0
- package/dist/adapters/yaml/index.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +48 -7
- package/dist/cli.js.map +1 -1
- package/dist/cycle-rules.d.ts +3 -3
- package/dist/cycle-rules.d.ts.map +1 -1
- package/dist/cycle-rules.js +1 -2
- package/dist/cycle-rules.js.map +1 -1
- package/dist/domain/cycle.d.ts +16 -2
- package/dist/domain/cycle.d.ts.map +1 -1
- package/dist/domain/cycle.js +21 -42
- package/dist/domain/cycle.js.map +1 -1
- package/dist/domain/paper-layout.d.ts.map +1 -1
- package/dist/domain/paper-layout.js +5 -2
- package/dist/domain/paper-layout.js.map +1 -1
- package/dist/domain/paths.d.ts +12 -0
- package/dist/domain/paths.d.ts.map +1 -1
- package/dist/domain/paths.js +22 -1
- package/dist/domain/paths.js.map +1 -1
- package/dist/domain/scaffold-layout.js +1 -1
- package/dist/domain/versions-migration.d.ts +51 -0
- package/dist/domain/versions-migration.d.ts.map +1 -0
- package/dist/domain/versions-migration.js +252 -0
- package/dist/domain/versions-migration.js.map +1 -0
- package/dist/domain/versions.d.ts +167 -0
- package/dist/domain/versions.d.ts.map +1 -0
- package/dist/domain/versions.js +457 -0
- package/dist/domain/versions.js.map +1 -0
- package/dist/eslint-rules/paper-stages.d.ts +7 -0
- package/dist/eslint-rules/paper-stages.d.ts.map +1 -1
- package/dist/eslint-rules/paper-stages.js +62 -7
- package/dist/eslint-rules/paper-stages.js.map +1 -1
- package/dist/eslint-rules/rule-context.d.ts +2 -0
- package/dist/eslint-rules/rule-context.d.ts.map +1 -1
- package/dist/ports/move-file.d.ts +7 -0
- package/dist/ports/move-file.d.ts.map +1 -0
- package/dist/ports/move-file.js +2 -0
- package/dist/ports/move-file.js.map +1 -0
- package/dist/ports/yaml.d.ts +5 -0
- package/dist/ports/yaml.d.ts.map +1 -0
- package/dist/ports/yaml.js +2 -0
- package/dist/ports/yaml.js.map +1 -0
- package/dist/submission.d.ts +2 -0
- package/dist/submission.d.ts.map +1 -1
- package/dist/submission.js +49 -9
- package/dist/submission.js.map +1 -1
- package/dist/version-rules.d.ts +16 -0
- package/dist/version-rules.d.ts.map +1 -0
- package/dist/version-rules.js +201 -0
- package/dist/version-rules.js.map +1 -0
- package/dist/versions-command.d.ts +37 -0
- package/dist/versions-command.d.ts.map +1 -0
- package/dist/versions-command.js +209 -0
- package/dist/versions-command.js.map +1 -0
- package/dist/versions.d.ts +76 -0
- package/dist/versions.d.ts.map +1 -0
- package/dist/versions.js +164 -0
- package/dist/versions.js.map +1 -0
- package/docs/configuration.md +76 -2
- package/docs/layout.md +5 -5
- package/docs/rules/cycle/camera-ready-source.md +62 -0
- package/docs/rules/cycle/draft-drift.md +47 -0
- package/docs/rules/cycle/port-done.md +2 -2
- package/docs/rules/cycle/record.md +2 -1
- package/docs/rules/cycle/submitted-frozen.md +65 -0
- package/docs/rules/paper/allowed-files-only.md +1 -1
- package/docs/rules/versions/layout.md +88 -0
- package/docs/rules.md +25 -11
- package/docs/submission.md +21 -3
- package/eslint-rules/paper-stages.test.mjs +62 -0
- package/eslint-rules/paper-stages.ts +67 -8
- package/eslint-rules/prereg-coverage.test.ts +1 -0
- package/eslint-rules/rule-context.ts +2 -0
- package/package.json +2 -1
- package/presets/agent.jsonc +75 -0
- package/presets/agent.md +70 -0
- package/presets/agentverify.jsonc +72 -0
- package/presets/agentverify.md +85 -0
- package/presets/ieee-conference.md +1 -1
- package/presets/nlbse.jsonc +76 -0
- package/presets/nlbse.md +85 -0
- package/scripts/coverage-config.test.ts +3 -0
- package/skills/CLAUDE.md +2 -2
- package/skills/CLAUDE.md.spec.ts +1 -1
- package/skills/paper-pipeline/references/pipeline-status-template.md +4 -1
- package/skills/submit-paper/SKILL.md +18 -10
- package/skills/submit-paper/SKILL.md.spec.ts +17 -9
- package/skills/submit-paper/references/publishers/acm.md +5 -3
- package/src/adapters/memory/index.test.ts +22 -0
- package/src/adapters/memory/index.ts +21 -0
- package/src/adapters/node/index.ts +1 -0
- package/src/adapters/node/move-file.io.ts +21 -0
- package/src/adapters/node/move-file.test.ts +51 -0
- package/src/adapters/yaml/index.test.ts +16 -0
- package/src/adapters/yaml/index.ts +17 -0
- package/src/cli.test.ts +43 -0
- package/src/cli.ts +58 -6
- package/src/cycle-rules.ts +4 -5
- package/src/domain/cycle.test.ts +27 -0
- package/src/domain/cycle.ts +38 -6
- package/src/domain/paper-layout.ts +5 -2
- package/src/domain/paths.test.ts +28 -1
- package/src/domain/paths.ts +34 -1
- package/src/domain/scaffold-layout.ts +1 -1
- package/src/domain/versions-migration.test.ts +410 -0
- package/src/domain/versions-migration.ts +429 -0
- package/src/domain/versions.test.ts +564 -0
- package/src/domain/versions.ts +706 -0
- package/src/paper-settings-commands.test.ts +1 -1
- package/src/ports/move-file.ts +8 -0
- package/src/ports/yaml.ts +5 -0
- package/src/preset-card-rule.test.ts +3 -0
- package/src/presets.test.ts +55 -31
- package/src/rule-cases.test.ts +157 -8
- package/src/submission-freeze.test.ts +320 -0
- package/src/submission-new.test.ts +11 -1
- package/src/submission.ts +82 -11
- package/src/tex-venue-rules.test.ts +3 -0
- package/src/venue-rules.test.ts +2 -2
- package/src/version-rules.test.ts +222 -0
- package/src/version-rules.ts +282 -0
- package/src/versions-command.test.ts +594 -0
- package/src/versions-command.ts +360 -0
- package/src/versions.ts +286 -0
- package/dist/eslint-rules/cycle-stage.d.ts +0 -39
- package/dist/eslint-rules/cycle-stage.d.ts.map +0 -1
- package/dist/eslint-rules/cycle-stage.js +0 -119
- package/dist/eslint-rules/cycle-stage.js.map +0 -1
- package/docs/rules/cycle/stage.md +0 -85
- package/eslint-rules/cycle-stage.test.mjs +0 -131
- 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
|
-
};
|