paperlint 7.1.0 → 8.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/.github/workflows/ci.yml +3 -0
- package/.github/workflows/deadlines.yml +100 -0
- package/README.md +6 -0
- package/dist/adapters/hotcrp/deadlines.d.ts +8 -0
- package/dist/adapters/hotcrp/deadlines.d.ts.map +1 -0
- package/dist/adapters/hotcrp/deadlines.io.d.ts +6 -0
- package/dist/adapters/hotcrp/deadlines.io.d.ts.map +1 -0
- package/dist/adapters/hotcrp/deadlines.io.js +26 -0
- package/dist/adapters/hotcrp/deadlines.io.js.map +1 -0
- package/dist/adapters/hotcrp/deadlines.js +134 -0
- package/dist/adapters/hotcrp/deadlines.js.map +1 -0
- package/dist/adapters/hotcrp/index.d.ts +2 -0
- package/dist/adapters/hotcrp/index.d.ts.map +1 -1
- package/dist/adapters/hotcrp/index.js +2 -0
- package/dist/adapters/hotcrp/index.js.map +1 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +1 -6
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +3 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +53 -47
- package/dist/cli.js.map +1 -1
- package/dist/cycle-rules.d.ts +24 -0
- package/dist/cycle-rules.d.ts.map +1 -0
- package/dist/cycle-rules.js +212 -0
- package/dist/cycle-rules.js.map +1 -0
- package/dist/domain/cycle.d.ts +148 -0
- package/dist/domain/cycle.d.ts.map +1 -0
- package/dist/domain/cycle.js +281 -0
- package/dist/domain/cycle.js.map +1 -0
- package/dist/domain/deadline.d.ts +128 -0
- package/dist/domain/deadline.d.ts.map +1 -0
- package/dist/domain/deadline.js +281 -0
- package/dist/domain/deadline.js.map +1 -0
- package/dist/eslint-rules/cycle-stage.d.ts +39 -0
- package/dist/eslint-rules/cycle-stage.d.ts.map +1 -0
- package/dist/eslint-rules/cycle-stage.js +119 -0
- package/dist/eslint-rules/cycle-stage.js.map +1 -0
- package/dist/eslint-rules/latex-language.d.ts.map +1 -1
- package/dist/eslint-rules/latex-language.js +10 -13
- package/dist/eslint-rules/latex-language.js.map +1 -1
- package/dist/eslint-rules/paper-context.js +2 -2
- package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
- package/dist/eslint-rules/paper-typography.js +22 -91
- package/dist/eslint-rules/paper-typography.js.map +1 -1
- package/dist/eslint-rules/papers.d.ts +1 -3
- package/dist/eslint-rules/papers.d.ts.map +1 -1
- package/dist/eslint-rules/papers.js +2 -4
- package/dist/eslint-rules/papers.js.map +1 -1
- package/dist/eslint-rules/tex-build.d.ts.map +1 -1
- package/dist/eslint-rules/tex-build.js +8 -25
- package/dist/eslint-rules/tex-build.js.map +1 -1
- package/dist/init.d.ts +1 -4
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +3 -9
- package/dist/init.js.map +1 -1
- package/dist/lib/markdown.d.ts +5 -81
- package/dist/lib/markdown.d.ts.map +1 -1
- package/dist/lib/markdown.js +5 -116
- package/dist/lib/markdown.js.map +1 -1
- package/dist/lib/paper-config.d.ts.map +1 -1
- package/dist/lib/paper-config.js +2 -2
- package/dist/lib/paper-config.js.map +1 -1
- package/dist/lib/skill-eval-fixture.d.ts +6 -4
- package/dist/lib/skill-eval-fixture.d.ts.map +1 -1
- package/dist/lib/skill-eval-fixture.js +25 -18
- package/dist/lib/skill-eval-fixture.js.map +1 -1
- package/dist/new-paper.d.ts +5 -11
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +12 -23
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-files.d.ts +4 -4
- package/dist/paper-prose.d.ts +17 -0
- package/dist/paper-prose.d.ts.map +1 -0
- package/dist/paper-prose.js +46 -0
- package/dist/paper-prose.js.map +1 -0
- package/dist/paper-settings.d.ts +7 -0
- package/dist/paper-settings.d.ts.map +1 -1
- package/dist/paper-settings.js +61 -8
- package/dist/paper-settings.js.map +1 -1
- package/dist/ports/venue-deadlines.d.ts +14 -0
- package/dist/ports/venue-deadlines.d.ts.map +1 -0
- package/dist/ports/venue-deadlines.js +2 -0
- package/dist/ports/venue-deadlines.js.map +1 -0
- package/dist/presets.d.ts +12 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +19 -11
- package/dist/presets.js.map +1 -1
- package/dist/structure.d.ts.map +1 -1
- package/dist/structure.js +4 -3
- package/dist/structure.js.map +1 -1
- package/dist/submission.d.ts +8 -0
- package/dist/submission.d.ts.map +1 -1
- package/dist/submission.js +41 -10
- package/dist/submission.js.map +1 -1
- package/dist/tex-requirements.d.ts +3 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +19 -0
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +19 -0
- package/dist/tex-venue-rules.d.ts.map +1 -1
- package/dist/tex-venue-rules.js +69 -0
- package/dist/tex-venue-rules.js.map +1 -1
- package/dist/types.d.ts +0 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts +10 -0
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +26 -10
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +146 -20
- package/docs/e2e.md +4 -6
- package/docs/prior-art/paper-folder-scaffolding.md +24 -29
- package/docs/rules/cycle/evidence.md +87 -0
- package/docs/rules/cycle/port-done.md +75 -0
- package/docs/rules/cycle/record.md +152 -0
- package/docs/rules/cycle/stage.md +85 -0
- package/docs/rules/tex/claim-provenance.md +0 -1
- package/docs/rules.md +48 -47
- package/docs/submission.md +5 -0
- package/eslint-rules/cycle-stage.test.mjs +131 -0
- package/eslint-rules/cycle-stage.ts +141 -0
- package/eslint-rules/latex-language.harness.mjs +3 -3
- package/eslint-rules/latex-language.ts +10 -13
- package/eslint-rules/paper-context.ts +2 -2
- package/eslint-rules/paper-typography.test.mjs +17 -85
- package/eslint-rules/paper-typography.ts +22 -98
- package/eslint-rules/papers.harness.mjs +1 -1
- package/eslint-rules/papers.ts +2 -4
- package/eslint-rules/tex-build.harness.mjs +2 -2
- package/eslint-rules/tex-build.ts +8 -25
- package/eslint.config.mjs +41 -2
- package/hooks/hooks.harness.ts +15 -15
- package/hooks/paper-edit-guard.hook.mjs +5 -8
- package/hooks/paper-edit-guard.test.ts +12 -3
- package/hooks/paper-skills-nudge.hook.mjs +2 -3
- package/hooks/paper-skills-nudge.test.ts +13 -3
- package/hooks/paper-status-gates.hook.mjs +2 -2
- package/hooks/paper-status-gates.test.ts +14 -2
- package/lib/markdown.test.mjs +5 -60
- package/lib/markdown.ts +9 -136
- package/lib/paper-config.ts +3 -2
- package/lib/skill-checks.test.mjs +4 -1
- package/lib/skill-eval-fixture.ts +25 -18
- package/package.json +1 -3
- package/presets/aidc.jsonc +24 -3
- package/presets/aidc.md +6 -3
- package/presets/msr.jsonc +25 -1
- package/presets/msr.md +5 -1
- package/presets/realm.jsonc +1 -1
- package/presets/venue-profile.schema.json +39 -0
- package/scripts/check.ts +4 -0
- package/scripts/harness-api.frozen.json +0 -5
- package/scripts/refresh-deadlines.test.ts +431 -0
- package/scripts/refresh-deadlines.ts +387 -0
- package/skills/cold-read-diff/SKILL.md +3 -3
- package/skills/cold-read-diff/SKILL.md.spec.ts +2 -2
- package/skills/grade-paper-writing/SKILL.md +14 -22
- package/skills/grade-paper-writing/SKILL.md.spec.ts +13 -21
- package/skills/grade-paper-writing/__snapshots__/prose-lint.test.ts.snap +40 -26
- package/skills/grade-paper-writing/prose-lint.mjs +108 -278
- package/skills/grade-paper-writing/prose-lint.test.ts +184 -81
- package/skills/paper-adversarial-review/SKILL.md +2 -2
- package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -1
- package/skills/paper-pipeline/SKILL.md +9 -12
- package/skills/paper-pipeline/SKILL.md.spec.ts +8 -11
- package/skills/paper-pipeline/description-language.eval.mjs +25 -15
- package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +25 -15
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +25 -15
- package/skills/paper-pipeline/pipeline-firing.eval.mjs +27 -16
- package/skills/paper-pipeline/pipeline-language.eval.mjs +25 -15
- package/skills/paper-pipeline/references/acceptance-gate.md +16 -20
- package/skills/paper-pipeline/references/artifact-checklist.md +4 -5
- package/skills/paper-pipeline/references/pipeline-status-template.md +1 -1
- package/skills/paper-pipeline/scripts/README.md +0 -4
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +8 -72
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +48 -156
- package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +81 -75
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +3 -3
- package/skills/paper-status/SKILL.md +2 -2
- package/skills/paper-status/SKILL.md.spec.ts +1 -1
- package/skills/render-paper/SKILL.md +2 -2
- package/skills/render-paper/SKILL.md.spec.ts +1 -1
- package/skills/render-paper/extract-pdf-facts.mjs +2 -2
- package/skills/tighten-paper/SKILL.md +29 -42
- package/skills/tighten-paper/SKILL.md.spec.ts +28 -41
- package/skills/verify-citations/scripts/bib-authors.check.test.mjs +0 -13
- package/skills/verify-citations/scripts/bib-authors.cli.test.ts +23 -11
- package/skills/verify-citations/scripts/bib-authors.mjs +8 -93
- package/skills/verify-citations/scripts/bib-authors.test.mjs +1 -44
- package/src/adapters/hotcrp/cassette/README.md +22 -0
- package/src/adapters/hotcrp/cassette/deadlines-aidc26.html +17 -0
- package/src/adapters/hotcrp/cassette/deadlines-fse2027.html +24 -0
- package/src/adapters/hotcrp/cassette/deadlines-icse2027.html +25 -0
- package/src/adapters/hotcrp/cassette/deadlines-msr2027.html +25 -0
- package/src/adapters/hotcrp/cassette/deadlines-nosuch.html +18 -0
- package/src/adapters/hotcrp/cassette/deadlines-sosp26.html +26 -0
- package/src/adapters/hotcrp/deadlines.io.test.ts +86 -0
- package/src/adapters/hotcrp/deadlines.io.ts +27 -0
- package/src/adapters/hotcrp/deadlines.test.ts +170 -0
- package/src/adapters/hotcrp/deadlines.ts +161 -0
- package/src/adapters/hotcrp/index.ts +2 -0
- package/src/build.harness.mjs +2 -2
- package/src/build.test.ts +1 -1
- package/src/build.ts +1 -6
- package/src/cli.harness.mjs +26 -46
- package/src/cli.test.ts +41 -40
- package/src/cli.ts +57 -58
- package/src/cycle-rules.test.ts +278 -0
- package/src/cycle-rules.ts +273 -0
- package/src/domain/cycle.test.ts +456 -0
- package/src/domain/cycle.ts +432 -0
- package/src/domain/deadline.test.ts +386 -0
- package/src/domain/deadline.ts +472 -0
- package/src/folder-venue-rule.test.ts +1 -1
- package/src/init.test.ts +6 -7
- package/src/init.ts +3 -22
- package/src/lint-fix.test.ts +10 -4
- package/src/new-paper.harness.mjs +9 -28
- package/src/new-paper.test.ts +7 -6
- package/src/new-paper.ts +11 -30
- package/src/paper-files.test.ts +5 -4
- package/src/paper-files.ts +4 -4
- package/src/paper-prose.test.ts +49 -0
- package/src/paper-prose.ts +67 -0
- package/src/paper-settings.test.ts +167 -0
- package/src/paper-settings.ts +85 -11
- package/src/parse-args.test.ts +0 -1
- package/src/ports/venue-deadlines.ts +14 -0
- package/src/presets.test.ts +105 -0
- package/src/presets.ts +43 -10
- package/src/rule-cases.test.ts +144 -0
- package/src/structure.harness.ts +5 -11
- package/src/structure.ts +4 -3
- package/src/submission.test.ts +105 -1
- package/src/submission.ts +73 -12
- package/src/tex-requirements.ts +31 -0
- package/src/tex-venue-rules.test.ts +76 -0
- package/src/tex-venue-rules.ts +85 -0
- package/src/types.ts +0 -2
- package/src/venue-rules.test.ts +82 -0
- package/src/venue-rules.ts +44 -12
- package/fixtures/real-markdown-paper/baseline.json +0 -22
- package/fixtures/real-markdown-paper/baseline.ts +0 -70
- package/skills/paper-pipeline/PIPELINE-MAP.md +0 -381
- package/skills/paper-pipeline/pipeline-firing.baseline.json +0 -270
- package/skills/paper-pipeline/references/sweep-2026-08-09-ideal-pipeline.md +0 -585
- package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +0 -492
- package/skills/paper-pipeline/scripts/artifact-coverage.mjs +0 -397
- package/skills/paper-pipeline/scripts/artifact-coverage.test.ts +0 -86
- package/skills/paper-pipeline/scripts/check-provenance.mjs +0 -184
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +0 -135
- package/skills/paper-pipeline/scripts/population-map.mjs +0 -188
- package/skills/paper-pipeline/scripts/population-map.test.mjs +0 -154
- package/skills/paper-pipeline/scripts/provenance.harness.mjs +0 -266
- package/skills/paper-pipeline/scripts/round-diff.harness.ts +0 -907
- package/skills/paper-pipeline/scripts/round-diff.mjs +0 -583
- package/skills/paper-pipeline/scripts/round-diff.test.mjs +0 -159
- package/skills/tighten-paper/__snapshots__/structure.test.ts.snap +0 -133
- package/skills/tighten-paper/structure.mjs +0 -364
- package/skills/tighten-paper/structure.test.ts +0 -117
- package/templates/paper/paper.md +0 -4
|
@@ -36,7 +36,6 @@ const ROOT = dirname(HERE);
|
|
|
36
36
|
const {
|
|
37
37
|
newPaper,
|
|
38
38
|
nameProblem,
|
|
39
|
-
wantedFiles,
|
|
40
39
|
PACKAGE_TEMPLATES,
|
|
41
40
|
OVERRIDE_DIR,
|
|
42
41
|
STATUS_FILE,
|
|
@@ -63,7 +62,7 @@ async function lint(dir) {
|
|
|
63
62
|
|
|
64
63
|
try {
|
|
65
64
|
// ── the templates are FILES in the package, one per required file ──────────────────────
|
|
66
|
-
for (const f of [STATUS_FILE, SOURCE_FILE
|
|
65
|
+
for (const f of [STATUS_FILE, SOURCE_FILE])
|
|
67
66
|
check(
|
|
68
67
|
`the package ships templates/paper/${f}`,
|
|
69
68
|
existsSync(join(PACKAGE_TEMPLATES, f)),
|
|
@@ -85,13 +84,13 @@ try {
|
|
|
85
84
|
);
|
|
86
85
|
check(
|
|
87
86
|
"a refused name creates nothing",
|
|
88
|
-
newPaper(work, "Bad",
|
|
87
|
+
newPaper(work, "Bad", { venues: [] }).ok === false &&
|
|
89
88
|
!existsSync(join(work, "Bad")),
|
|
90
89
|
);
|
|
91
90
|
|
|
92
91
|
// ── a fresh LaTeX paper ─────────────────────────────────────────────────────────────
|
|
93
92
|
const papers = join(work, "papers");
|
|
94
|
-
const r = newPaper(papers, "demo",
|
|
93
|
+
const r = newPaper(papers, "demo", { venues: [] });
|
|
95
94
|
check(
|
|
96
95
|
"a fresh folder gets the scorecard, a .tex source and paperlint.json, all from the package",
|
|
97
96
|
r.ok &&
|
|
@@ -129,36 +128,18 @@ try {
|
|
|
129
128
|
|
|
130
129
|
// ── never overwrite; on an existing folder only the missing files ─────────────────────
|
|
131
130
|
writeFileSync(join(papers, "demo", STATUS_FILE), "mine\n");
|
|
132
|
-
const again = newPaper(papers, "demo",
|
|
131
|
+
const again = newPaper(papers, "demo", { venues: [] });
|
|
133
132
|
check(
|
|
134
|
-
"🔴 a second run overwrites nothing
|
|
133
|
+
"🔴 a second run overwrites nothing",
|
|
135
134
|
again.ok &&
|
|
136
135
|
!again.fresh &&
|
|
137
136
|
read(papers, "demo", STATUS_FILE) === "mine\n" &&
|
|
138
|
-
!existsSync(join(papers, "demo", "paper.md")) &&
|
|
139
137
|
again.files.every((f) => f.status === "kept"),
|
|
140
138
|
);
|
|
141
|
-
mkdirSync(join(papers, "old"), { recursive: true });
|
|
142
|
-
writeFileSync(join(papers, "old", "paper.md"), "# Old\n");
|
|
143
|
-
const migrated = newPaper(papers, "old", "tex", { venues: [] });
|
|
144
|
-
check(
|
|
145
|
-
"an old folder with only paper.md gets ONLY the scorecard — that is the migration",
|
|
146
|
-
migrated.ok &&
|
|
147
|
-
existsSync(join(papers, "old", STATUS_FILE)) &&
|
|
148
|
-
!existsSync(join(papers, "old", "paper.tex")) &&
|
|
149
|
-
read(papers, "old", "paper.md") === "# Old\n",
|
|
150
|
-
);
|
|
151
|
-
check(
|
|
152
|
-
"wantedFiles asks for a source only when neither format is there",
|
|
153
|
-
wantedFiles(join(papers, "old"), "tex").join() ===
|
|
154
|
-
`${STATUS_FILE},paperlint.json` &&
|
|
155
|
-
wantedFiles(join(papers, "nowhere"), "md").join() ===
|
|
156
|
-
`${STATUS_FILE},paper.md,paperlint.json`,
|
|
157
|
-
);
|
|
158
139
|
writeFileSync(join(papers, "afile"), "x");
|
|
159
140
|
check(
|
|
160
141
|
"a name taken by a FILE is refused, not written through",
|
|
161
|
-
newPaper(papers, "afile",
|
|
142
|
+
newPaper(papers, "afile", { venues: [] }).ok === false,
|
|
162
143
|
);
|
|
163
144
|
|
|
164
145
|
// ── the project's template wins, file by file ─────────────────────────────────────────
|
|
@@ -167,7 +148,7 @@ try {
|
|
|
167
148
|
join(papers, OVERRIDE_DIR, STATUS_FILE),
|
|
168
149
|
"---\n---\n# House scorecard for {{name}}\n",
|
|
169
150
|
);
|
|
170
|
-
const own = newPaper(papers, "house",
|
|
151
|
+
const own = newPaper(papers, "house", { venues: [] });
|
|
171
152
|
// Guards: the override slot — `fromTemplate` in new-paper.ts reads `<papers>/.template/<file>`
|
|
172
153
|
// before the package's copy. Replace that lookup with the package's alone and this goes red.
|
|
173
154
|
check(
|
|
@@ -176,11 +157,11 @@ try {
|
|
|
176
157
|
read(papers, "house", STATUS_FILE) ===
|
|
177
158
|
"---\n---\n# House scorecard for house\n" &&
|
|
178
159
|
own.files.find((f) => f.file === STATUS_FILE)?.from === "project" &&
|
|
179
|
-
own.files.find((f) => f.file ===
|
|
160
|
+
own.files.find((f) => f.file === SOURCE_FILE)?.from === "package",
|
|
180
161
|
);
|
|
181
162
|
|
|
182
163
|
// ── a missing template is a broken install, not an empty file ─────────────────────────
|
|
183
|
-
const broken = newPaper(join(work, "b"), "x",
|
|
164
|
+
const broken = newPaper(join(work, "b"), "x", {
|
|
184
165
|
venues: [],
|
|
185
166
|
packageTemplates: join(work, "no-such-dir"),
|
|
186
167
|
});
|
package/src/new-paper.test.ts
CHANGED
|
@@ -41,7 +41,7 @@ const settingsOf = (dir: string) =>
|
|
|
41
41
|
describe("paperlint new — paperlint.json", () => {
|
|
42
42
|
it("is always written, from the package template: no venue chosen yet, and valid", () => {
|
|
43
43
|
const papers = join(tmp(), "papers");
|
|
44
|
-
const r = newPaper(papers, "demo",
|
|
44
|
+
const r = newPaper(papers, "demo", { venues: [] });
|
|
45
45
|
expect(r.ok && r.files.find((f) => f.file === "paperlint.json")).toEqual({
|
|
46
46
|
file: "paperlint.json",
|
|
47
47
|
status: "created",
|
|
@@ -60,6 +60,7 @@ describe("paperlint new — paperlint.json", () => {
|
|
|
60
60
|
identity: null,
|
|
61
61
|
talk: null,
|
|
62
62
|
submission: null,
|
|
63
|
+
cycles: null,
|
|
63
64
|
},
|
|
64
65
|
});
|
|
65
66
|
});
|
|
@@ -71,7 +72,7 @@ describe("paperlint new — paperlint.json", () => {
|
|
|
71
72
|
join(papers, OVERRIDE_DIR, "paperlint.json"),
|
|
72
73
|
'{ "extends": "paperlint:aisec", "kind": "research" }\n',
|
|
73
74
|
);
|
|
74
|
-
const r = newPaper(papers, "house",
|
|
75
|
+
const r = newPaper(papers, "house", { venues: [] });
|
|
75
76
|
expect(r.ok && r.files.find((f) => f.file === "paperlint.json")?.from).toBe(
|
|
76
77
|
"project",
|
|
77
78
|
);
|
|
@@ -88,7 +89,7 @@ describe("paperlint new — paperlint.json", () => {
|
|
|
88
89
|
join(papers, "p", "paperlint.json"),
|
|
89
90
|
'{"extends":"paperlint:aisec"}',
|
|
90
91
|
);
|
|
91
|
-
const r = newPaper(papers, "p",
|
|
92
|
+
const r = newPaper(papers, "p", { venues: [] });
|
|
92
93
|
expect(
|
|
93
94
|
r.ok && r.files.find((f) => f.file === "paperlint.json")?.status,
|
|
94
95
|
).toBe("kept");
|
|
@@ -105,7 +106,7 @@ describe("paperlint lint — a paper with no venue preset chosen", () => {
|
|
|
105
106
|
join(root, "package.json"),
|
|
106
107
|
JSON.stringify({ name: "c", private: true }),
|
|
107
108
|
);
|
|
108
|
-
newPaper(join(root, "papers"), "demo",
|
|
109
|
+
newPaper(join(root, "papers"), "demo", { venues: [] });
|
|
109
110
|
if (extendsValue !== null)
|
|
110
111
|
writeFileSync(
|
|
111
112
|
join(root, "papers", "demo", "paperlint.json"),
|
|
@@ -487,7 +488,7 @@ describe("a project's own template", () => {
|
|
|
487
488
|
mkdirSync(join(papers, OVERRIDE_DIR), { recursive: true });
|
|
488
489
|
const src = join(papers, OVERRIDE_DIR, "paperlint.json");
|
|
489
490
|
writeFileSync(src, '{\n // our house venue\n "extends": null\n}\n');
|
|
490
|
-
const r = newPaper(papers, "p",
|
|
491
|
+
const r = newPaper(papers, "p", {
|
|
491
492
|
venues: [],
|
|
492
493
|
venue: { extends: "paperlint:aisec", kind: null },
|
|
493
494
|
});
|
|
@@ -503,7 +504,7 @@ describe("a project's own template", () => {
|
|
|
503
504
|
mkdirSync(join(papers, OVERRIDE_DIR), { recursive: true });
|
|
504
505
|
writeFileSync(join(papers, OVERRIDE_DIR, "paperlint.json"), "{}\n");
|
|
505
506
|
const lines = reportNewPaper(
|
|
506
|
-
newPaper(papers, "p",
|
|
507
|
+
newPaper(papers, "p", { venues: [] }),
|
|
507
508
|
(p) => p,
|
|
508
509
|
);
|
|
509
510
|
expect(lines).toContain(
|
package/src/new-paper.ts
CHANGED
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
* Every rule below is taken from a tool that already solved it (docs/prior-art/paper-folder-scaffolding.md):
|
|
9
9
|
* - the template is a FILE with a lookup order — the project's `<papers>/.template/<file>` first,
|
|
10
10
|
* the package's `templates/paper/<file>` second (Hugo's archetypes);
|
|
11
|
-
* - it NEVER overwrites: on an existing folder it adds only the missing required files
|
|
12
|
-
* also the migration for an old folder that has only `paper.md` (`cargo init`);
|
|
11
|
+
* - it NEVER overwrites: on an existing folder it adds only the missing required files (`cargo init`);
|
|
13
12
|
* - the name is validated, because it becomes a path and, through the hooks, a shell argument.
|
|
14
13
|
*
|
|
15
14
|
* The only substitution is `{{name}}` → the folder name. Nothing else in a template is touched.
|
|
@@ -33,10 +32,6 @@ import {
|
|
|
33
32
|
type NamedVenue,
|
|
34
33
|
} from "./domain/venue-name.ts";
|
|
35
34
|
|
|
36
|
-
export type PaperFormat = "tex" | "md";
|
|
37
|
-
export const FORMATS: readonly PaperFormat[] = ["tex", "md"];
|
|
38
|
-
export const DEFAULT_FORMAT: PaperFormat = "tex";
|
|
39
|
-
|
|
40
35
|
/** The package's default templates. Read from disk, so they ship in the tarball (`files`). */
|
|
41
36
|
export const PACKAGE_TEMPLATES = fileURLToPath(
|
|
42
37
|
new URL("../templates/paper/", import.meta.url),
|
|
@@ -50,17 +45,12 @@ export const PACKAGE_TEMPLATES = fileURLToPath(
|
|
|
50
45
|
export const OVERRIDE_DIR = ".template";
|
|
51
46
|
|
|
52
47
|
export const STATUS_FILE = "PIPELINE-STATUS.md";
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
md: "paper.md",
|
|
56
|
-
};
|
|
48
|
+
/** A paper's source: LaTeX, one file at the paper's root. */
|
|
49
|
+
export const SOURCE_FILE = "paper.tex";
|
|
57
50
|
|
|
58
51
|
/** The flag that lets `paperlint new` create a folder whose name names a venue. */
|
|
59
52
|
export const ALLOW_VENUE_NAME = "--allow-venue-name";
|
|
60
53
|
|
|
61
|
-
export const isFormat = (v: unknown): v is PaperFormat =>
|
|
62
|
-
FORMATS.some((f) => f === v);
|
|
63
|
-
|
|
64
54
|
/**
|
|
65
55
|
* Why a name is refused, or null. `[a-z0-9._-]+` is the charset; a LEADING dot is refused on top
|
|
66
56
|
* of it, because discovery skips dot-directories — a paper named `.x` would be created and then
|
|
@@ -109,20 +99,12 @@ export type NewPaperResult =
|
|
|
109
99
|
}
|
|
110
100
|
| { readonly ok: false; readonly reason: string };
|
|
111
101
|
|
|
112
|
-
/**
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function scorecardAndSource(dir: string, format: PaperFormat): string[] {
|
|
121
|
-
const hasSource = Object.values(SOURCE_FILE).some((f) =>
|
|
122
|
-
existsSync(join(dir, f)),
|
|
123
|
-
);
|
|
124
|
-
return hasSource ? [STATUS_FILE] : [STATUS_FILE, SOURCE_FILE[format]];
|
|
125
|
-
}
|
|
102
|
+
/** The files a paper folder must end up with: the scorecard, the source and `paperlint.json`. */
|
|
103
|
+
export const WANTED_FILES: readonly string[] = [
|
|
104
|
+
STATUS_FILE,
|
|
105
|
+
SOURCE_FILE,
|
|
106
|
+
CONFIG_FILE,
|
|
107
|
+
];
|
|
126
108
|
|
|
127
109
|
/** The venue `paperlint new --venue` chose: what goes into the new paper's `paperlint.json`. */
|
|
128
110
|
export interface VenueSetting {
|
|
@@ -195,7 +177,7 @@ function fromTemplate(
|
|
|
195
177
|
return { ok: false, reason: `no template for ${file}: ${src} is missing` };
|
|
196
178
|
const text = readFileSync(src, "utf8").split("{{name}}").join(name);
|
|
197
179
|
const setClass = venue?.setClass ?? null;
|
|
198
|
-
if (file === SOURCE_FILE
|
|
180
|
+
if (file === SOURCE_FILE && setClass !== null)
|
|
199
181
|
return { ok: true, value: { file, text: setClass(text), from } };
|
|
200
182
|
if (file !== CONFIG_FILE || venue === null)
|
|
201
183
|
return { ok: true, value: { file, text, from } };
|
|
@@ -217,7 +199,6 @@ export interface NewPaperOptions {
|
|
|
217
199
|
export function newPaper(
|
|
218
200
|
papersRoot: string,
|
|
219
201
|
name: string,
|
|
220
|
-
format: PaperFormat,
|
|
221
202
|
opts: NewPaperOptions,
|
|
222
203
|
): NewPaperResult {
|
|
223
204
|
const { packageTemplates = PACKAGE_TEMPLATES, venue = null } = opts;
|
|
@@ -230,7 +211,7 @@ export function newPaper(
|
|
|
230
211
|
|
|
231
212
|
const files: FileOutcome[] = [];
|
|
232
213
|
const toWrite: Planned[] = [];
|
|
233
|
-
for (const file of
|
|
214
|
+
for (const file of WANTED_FILES) {
|
|
234
215
|
if (existsSync(join(dir, file))) {
|
|
235
216
|
files.push({ file, status: "kept" });
|
|
236
217
|
continue;
|
package/src/paper-files.test.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { tmpdir } from "node:os";
|
|
|
20
20
|
import { join } from "node:path";
|
|
21
21
|
import { afterEach, describe, expect, it } from "vitest";
|
|
22
22
|
import { buildConfig, eslintConfig, run } from "./cli.ts";
|
|
23
|
+
import { texLanguage } from "#eslint-rules/latex-language";
|
|
23
24
|
import {
|
|
24
25
|
narrowToOwners,
|
|
25
26
|
ownedPatterns,
|
|
@@ -247,7 +248,7 @@ describe("`rules` blocks — where the rule lands", () => {
|
|
|
247
248
|
},
|
|
248
249
|
],
|
|
249
250
|
},
|
|
250
|
-
|
|
251
|
+
texLanguage,
|
|
251
252
|
),
|
|
252
253
|
),
|
|
253
254
|
});
|
|
@@ -257,9 +258,9 @@ describe("`rules` blocks — where the rule lands", () => {
|
|
|
257
258
|
const status = await on("papers/a/PIPELINE-STATUS.md");
|
|
258
259
|
expect(status["paper/source"]).toEqual([1]);
|
|
259
260
|
expect(status["paper/section-word"]).toBeUndefined();
|
|
260
|
-
const
|
|
261
|
-
expect(
|
|
262
|
-
expect(
|
|
261
|
+
const source = await on("papers/a/paper.tex");
|
|
262
|
+
expect(source["paper/section-word"]).toEqual([0]);
|
|
263
|
+
expect(source["paper/source"]).toBeUndefined();
|
|
263
264
|
});
|
|
264
265
|
});
|
|
265
266
|
|
package/src/paper-files.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* THE FILES PAPERLINT OWNS — read off its own config, and made the only thing ESLint may reach.
|
|
3
3
|
*
|
|
4
|
-
* paperlint's blocks name the files they lint (`PIPELINE-STATUS.md`, `paper.
|
|
5
|
-
*
|
|
4
|
+
* paperlint's blocks name the files they lint (`PIPELINE-STATUS.md`, `paper.tex`, `reviews/*.md`,
|
|
5
|
+
* …). That list is the whole scope of `paperlint lint`, and this module turns it
|
|
6
6
|
* into two things ESLint understands:
|
|
7
7
|
*
|
|
8
8
|
* 1. **A global ignore of everything else** (`scopeToOwned`). Handing ESLint a directory is not
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* 2. **Each rule scoped to the files it was written for** (`ruleOwners` + `narrowToOwners`).
|
|
18
18
|
* One plugin NAME is bound to different plugin objects in different blocks: `paper` holds
|
|
19
19
|
* `stages`/`source`/`author-list` beside `PIPELINE-STATUS.md` and `typography` beside
|
|
20
|
-
* `paper.
|
|
20
|
+
* `paper.tex`. So a consumer block that named `paper/source`
|
|
21
21
|
* for every file under the papers directory reached `paper.tex`, where `paper` has no `source`
|
|
22
22
|
* — and ESLint threw `Could not find "source" in plugin "paper"` from inside `lintFiles`,
|
|
23
23
|
* after paperlint's own validation had (correctly) accepted the id. A block is therefore split
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* - a global-ignore pattern that matches a DIRECTORY hides everything inside it, so directories
|
|
37
37
|
* must be un-ignored (`!` + a pattern ending in a slash) before owned files can be;
|
|
38
38
|
* - that un-ignores `node_modules/` and `.git/` too — ESLint's own defaults — so they are
|
|
39
|
-
* re-ignored after, or a `paper.
|
|
39
|
+
* re-ignored after, or a `paper.tex` inside a dependency would be linted;
|
|
40
40
|
* - later global-ignore patterns win, across blocks, in config order;
|
|
41
41
|
* - `files: [["a", "b"]]` matches a file only when BOTH patterns match.
|
|
42
42
|
*/
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { expect, it } from "vitest";
|
|
2
|
+
import { latexReader } from "./adapters/latex/index.ts";
|
|
3
|
+
import { memoryFiles } from "./adapters/memory/index.ts";
|
|
4
|
+
import { paperProse } from "./paper-prose.ts";
|
|
5
|
+
|
|
6
|
+
const SRC = [
|
|
7
|
+
"\\documentclass{article}",
|
|
8
|
+
"\\usepackage{amsmath} % a preamble word count of its own",
|
|
9
|
+
"\\title{A Title}",
|
|
10
|
+
"\\begin{document}",
|
|
11
|
+
"\\maketitle",
|
|
12
|
+
"\\section{Introduction}",
|
|
13
|
+
"We measure it~\\cite{a}. % a comment",
|
|
14
|
+
"",
|
|
15
|
+
"A second paragraph \\input{body}",
|
|
16
|
+
"\\begin{filecontents*}{refs.bib}",
|
|
17
|
+
"@misc{a, author = {Ada Lovelace}, title = {Never Prose}}",
|
|
18
|
+
"\\end{filecontents*}",
|
|
19
|
+
"\\end{document}",
|
|
20
|
+
].join("\n");
|
|
21
|
+
|
|
22
|
+
it("the body's prose, a blank line between passages, includes spliced; the preamble, comments, the inline .bib and the title stay out", () => {
|
|
23
|
+
const files = memoryFiles({
|
|
24
|
+
"/p/paper.tex": SRC,
|
|
25
|
+
"/p/body.tex": "\\subsection{Method}\nends here.",
|
|
26
|
+
});
|
|
27
|
+
expect(
|
|
28
|
+
paperProse("/p/paper.tex", SRC, { files, latex: latexReader }),
|
|
29
|
+
).toEqual({
|
|
30
|
+
// A citation stands as `[1]`, where it is set; a heading — here an included one — ends a passage.
|
|
31
|
+
body: "We measure it [1].\n\nA second paragraph\n\nends here.",
|
|
32
|
+
headings: ["Introduction", "Method"],
|
|
33
|
+
files: [
|
|
34
|
+
{ file: "paper.tex", text: SRC },
|
|
35
|
+
{ file: "body.tex", text: "\\subsection{Method}\nends here." },
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("a citation stands as `[1]` and a cross-reference as `1`, so the sentence still ends on them", () => {
|
|
41
|
+
const src =
|
|
42
|
+
"\\documentclass{article}\n\\begin{document}\nIt holds~\\cite{a}. It is shown (Section~\\ref{s}).\n\\end{document}\n";
|
|
43
|
+
expect(
|
|
44
|
+
paperProse("/p/paper.tex", src, {
|
|
45
|
+
files: memoryFiles({ "/p/paper.tex": src }),
|
|
46
|
+
latex: latexReader,
|
|
47
|
+
}).body,
|
|
48
|
+
).toBe("It holds [1]. It is shown (Section 1).");
|
|
49
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE PROSE OF A paper.tex AS PLAIN TEXT — what `grade-paper-writing/prose-lint.mjs` measures.
|
|
3
|
+
*
|
|
4
|
+
* prose-lint counts sentences and words, and every metric it prints is a fraction of the word
|
|
5
|
+
* count. Read as raw text, a `.tex` puts the preamble, the `%` comments and an inline `filecontents`
|
|
6
|
+
* bibliography into that count (8127 "words" against about 4636 real ones on one paper), so every
|
|
7
|
+
* rate comes out roughly halved. This reads the paper the way the `tex/register` rule does: the
|
|
8
|
+
* includes spliced in (`readPaper`), then the body's prose as passages (`bodyProse` — the abstract
|
|
9
|
+
* to the back matter, without headings, captions, floats or code), each passage's sentences
|
|
10
|
+
* (`bodySentences`). A blank line separates two passages, so no sentence runs across them.
|
|
11
|
+
*
|
|
12
|
+
* A citation stands as `[1]` and a cross-reference as `1`, where they are set — not left out:
|
|
13
|
+
* prose-lint judges a sentence that ENDS on one (a wasted stress position) and counts references
|
|
14
|
+
* to other parts of the paper (`(Section 1)`), and both are invisible in a sentence the marks
|
|
15
|
+
* were cut out of. Math is left out; prose-lint counts no formula.
|
|
16
|
+
*/
|
|
17
|
+
import { bodySentences } from "./domain/register.ts";
|
|
18
|
+
import { CITATION, MATH, REFERENCE } from "./domain/sentences.ts";
|
|
19
|
+
import { runText } from "./domain/tex-document.ts";
|
|
20
|
+
import { readPaper, type PaperDeps } from "./tex-paper.ts";
|
|
21
|
+
|
|
22
|
+
/** A sentence's text with each mark as a reader would see it set, and math left out. */
|
|
23
|
+
const rendered = (raw: string): string =>
|
|
24
|
+
raw
|
|
25
|
+
.replaceAll(CITATION, "[1]")
|
|
26
|
+
.replaceAll(REFERENCE, "1")
|
|
27
|
+
.replaceAll(MATH, "")
|
|
28
|
+
.replace(/\s+/gu, " ")
|
|
29
|
+
.trim();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A paper's body prose, one passage per paragraph, its section titles in document order, and the
|
|
33
|
+
* text of each file it is made of — `paper.tex` and every file it includes, once, named as the
|
|
34
|
+
* include names it — for what the body leaves out, such as captions.
|
|
35
|
+
*/
|
|
36
|
+
export interface PaperProse {
|
|
37
|
+
readonly body: string;
|
|
38
|
+
readonly headings: readonly string[];
|
|
39
|
+
readonly files: readonly { readonly file: string; readonly text: string }[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The prose and the headings of the paper whose main file is `filename` with the text `src`. */
|
|
43
|
+
export function paperProse(
|
|
44
|
+
filename: string,
|
|
45
|
+
src: string,
|
|
46
|
+
deps: PaperDeps,
|
|
47
|
+
): PaperProse {
|
|
48
|
+
const { text, segments } = readPaper(filename, src, deps);
|
|
49
|
+
const passages = deps.latex
|
|
50
|
+
.bodyProse(text)
|
|
51
|
+
.map((p) =>
|
|
52
|
+
bodySentences([p])
|
|
53
|
+
.map((s) => rendered(s.raw))
|
|
54
|
+
.join(" "),
|
|
55
|
+
)
|
|
56
|
+
.filter((p) => p !== "");
|
|
57
|
+
return {
|
|
58
|
+
body: passages.join("\n\n"),
|
|
59
|
+
headings: deps.latex
|
|
60
|
+
.headings(text)
|
|
61
|
+
.filter((h) => h.command.kind === "heading" && h.argument === "title")
|
|
62
|
+
.map((h) => runText(h.title).replace(/\s+/g, " ").trim()),
|
|
63
|
+
files: [...new Map(segments.map((s) => [s.file, s.source])).entries()].map(
|
|
64
|
+
([file, text]) => ({ file, text }),
|
|
65
|
+
),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -33,6 +33,7 @@ describe("parsePaperSettings", () => {
|
|
|
33
33
|
identity: null,
|
|
34
34
|
talk: null,
|
|
35
35
|
submission: null,
|
|
36
|
+
cycles: null,
|
|
36
37
|
},
|
|
37
38
|
});
|
|
38
39
|
expect(parsePaperSettings({})).toEqual({
|
|
@@ -45,6 +46,7 @@ describe("parsePaperSettings", () => {
|
|
|
45
46
|
identity: null,
|
|
46
47
|
talk: null,
|
|
47
48
|
submission: null,
|
|
49
|
+
cycles: null,
|
|
48
50
|
},
|
|
49
51
|
});
|
|
50
52
|
});
|
|
@@ -60,6 +62,7 @@ describe("parsePaperSettings", () => {
|
|
|
60
62
|
identity: null,
|
|
61
63
|
talk: null,
|
|
62
64
|
submission: null,
|
|
65
|
+
cycles: null,
|
|
63
66
|
},
|
|
64
67
|
});
|
|
65
68
|
});
|
|
@@ -164,6 +167,7 @@ const UNSET = {
|
|
|
164
167
|
identity: null,
|
|
165
168
|
talk: null,
|
|
166
169
|
submission: null,
|
|
170
|
+
cycles: null,
|
|
167
171
|
};
|
|
168
172
|
|
|
169
173
|
describe("readPaperSettings — the root paperlint.json's defaults, the paper's file over them", () => {
|
|
@@ -220,8 +224,14 @@ describe("readPaperSettings — the root paperlint.json's defaults, the paper's
|
|
|
220
224
|
identity: null,
|
|
221
225
|
talk: null,
|
|
222
226
|
submission: null,
|
|
227
|
+
cycles: null,
|
|
223
228
|
});
|
|
224
229
|
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe("readPaperSettings — a project key in a paper's file", () => {
|
|
233
|
+
const withFiles = (f: Record<string, string>) =>
|
|
234
|
+
memoryFiles({ "/work/package.json": "{}", ...f });
|
|
225
235
|
|
|
226
236
|
it("papersDir in a paper's file: broken, naming the root file as its place", () => {
|
|
227
237
|
const r = readPaperSettings(
|
|
@@ -264,6 +274,7 @@ describe("paperRules — `rules` in paperlint.json", () => {
|
|
|
264
274
|
identity: null,
|
|
265
275
|
talk: null,
|
|
266
276
|
submission: null,
|
|
277
|
+
cycles: null,
|
|
267
278
|
});
|
|
268
279
|
|
|
269
280
|
it("no rules: none", () => {
|
|
@@ -352,3 +363,159 @@ describe("talk in a paper's paperlint.json", () => {
|
|
|
352
363
|
expect(r.ok ? "" : r.error).toMatch(/"talk.mode"/);
|
|
353
364
|
});
|
|
354
365
|
});
|
|
366
|
+
|
|
367
|
+
describe("parsePaperSettings — `cycles`: the current attempt supplies extends, kind and submission", () => {
|
|
368
|
+
const closed = {
|
|
369
|
+
id: "alpha-2026",
|
|
370
|
+
venue: { kind: "preset", extends: "paperlint:acm-sigconf" },
|
|
371
|
+
kind: "short",
|
|
372
|
+
opened: "2026-07-01",
|
|
373
|
+
outcome: {
|
|
374
|
+
kind: "rejected",
|
|
375
|
+
date: "2026-09-08",
|
|
376
|
+
desk: true,
|
|
377
|
+
evidence: "reviews/alpha.md",
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
const open = {
|
|
381
|
+
id: "beta-2027",
|
|
382
|
+
venue: { kind: "preset", extends: "paperlint:ieee-conference" },
|
|
383
|
+
kind: "technical",
|
|
384
|
+
opened: "2026-09-09",
|
|
385
|
+
submission: { id: 52 },
|
|
386
|
+
outcome: { kind: "open" },
|
|
387
|
+
};
|
|
388
|
+
const settingsOf = (json: unknown) => {
|
|
389
|
+
const r = parsePaperSettings(json);
|
|
390
|
+
if (!r.ok) throw new Error(r.error);
|
|
391
|
+
return r.value;
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
it("an open cycle: its venue, kind and submission are the paper's", () => {
|
|
395
|
+
const s = settingsOf({ cycles: [closed, open] });
|
|
396
|
+
expect(s.extends).toBe("paperlint:ieee-conference");
|
|
397
|
+
expect(s.kind).toBe("technical");
|
|
398
|
+
expect(s.submission).toEqual({ id: 52 });
|
|
399
|
+
expect(s.cycles?.current.kind).toBe("cycle");
|
|
400
|
+
expect(s.cycles?.list.map((c) => c.id)).toEqual([
|
|
401
|
+
"alpha-2026",
|
|
402
|
+
"beta-2027",
|
|
403
|
+
]);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it("parked (the last cycle closed) and none (`cycles: []`): no venue, as `extends: null` reads", () => {
|
|
407
|
+
const parked = settingsOf({ cycles: [closed] });
|
|
408
|
+
expect([parked.extends, parked.kind, parked.submission]).toEqual([
|
|
409
|
+
null,
|
|
410
|
+
null,
|
|
411
|
+
null,
|
|
412
|
+
]);
|
|
413
|
+
expect(parked.cycles?.current.kind).toBe("parked");
|
|
414
|
+
expect(settingsOf({ cycles: [] }).cycles?.current).toEqual({
|
|
415
|
+
kind: "none",
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
describe("parsePaperSettings — `cycles`, closed and named", () => {
|
|
421
|
+
const closed = {
|
|
422
|
+
id: "alpha-2026",
|
|
423
|
+
venue: { kind: "preset", extends: "paperlint:acm-sigconf" },
|
|
424
|
+
kind: "short",
|
|
425
|
+
opened: "2026-07-01",
|
|
426
|
+
outcome: { kind: "withdrawn", date: "2026-09-08", evidence: "n.md" },
|
|
427
|
+
};
|
|
428
|
+
const open = {
|
|
429
|
+
id: "beta-2027",
|
|
430
|
+
venue: { kind: "preset", extends: "paperlint:ieee-conference" },
|
|
431
|
+
kind: "technical",
|
|
432
|
+
opened: "2026-09-09",
|
|
433
|
+
};
|
|
434
|
+
const settingsOf = (json: unknown) => {
|
|
435
|
+
const r = parsePaperSettings(json);
|
|
436
|
+
if (!r.ok) throw new Error(r.error);
|
|
437
|
+
return r.value;
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
it("an accepted last cycle keeps its venue and kind: camera-ready and talk are judged against it", () => {
|
|
441
|
+
const accepted = { kind: "accepted", date: "2026-08-21", evidence: "r.md" };
|
|
442
|
+
const s = settingsOf({ cycles: [{ ...closed, outcome: accepted }] });
|
|
443
|
+
expect([s.extends, s.kind, s.cycles?.current.kind]).toEqual([
|
|
444
|
+
"paperlint:acm-sigconf",
|
|
445
|
+
"short",
|
|
446
|
+
"accepted",
|
|
447
|
+
]);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it("a named venue (no preset yet) resolves no preset: extends is null, the record stays", () => {
|
|
451
|
+
const venue = {
|
|
452
|
+
kind: "named",
|
|
453
|
+
name: "X '27",
|
|
454
|
+
url: "https://example.org/c",
|
|
455
|
+
};
|
|
456
|
+
const s = settingsOf({ cycles: [{ ...open, venue }] });
|
|
457
|
+
expect([s.extends, s.kind]).toEqual([null, "technical"]);
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
describe("parsePaperSettings — `cycles` refused", () => {
|
|
462
|
+
const open = {
|
|
463
|
+
id: "beta-2027",
|
|
464
|
+
venue: { kind: "preset", extends: "paperlint:ieee-conference" },
|
|
465
|
+
opened: "2026-09-09",
|
|
466
|
+
};
|
|
467
|
+
const alsoOpen = {
|
|
468
|
+
id: "alpha-2026",
|
|
469
|
+
venue: { kind: "preset", extends: "paperlint:acm-sigconf" },
|
|
470
|
+
opened: "2026-07-01",
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
it("one derived key beside `cycles` is refused in the singular", () => {
|
|
474
|
+
const error: unknown = expect.stringMatching(
|
|
475
|
+
/^"kind" beside "cycles": with cycles, it is the current attempt's/,
|
|
476
|
+
);
|
|
477
|
+
expect(parsePaperSettings({ kind: "short", cycles: [open] })).toEqual({
|
|
478
|
+
ok: false,
|
|
479
|
+
error,
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it("a derived key written beside `cycles` is refused, naming it — two sources for one fact", () => {
|
|
484
|
+
const error: unknown = expect.stringMatching(
|
|
485
|
+
/^"extends", "kind" beside "cycles": with cycles, they are the current attempt's/,
|
|
486
|
+
);
|
|
487
|
+
expect(
|
|
488
|
+
parsePaperSettings({
|
|
489
|
+
extends: "paperlint:acm-sigconf",
|
|
490
|
+
kind: "short",
|
|
491
|
+
cycles: [open],
|
|
492
|
+
}),
|
|
493
|
+
).toEqual({ ok: false, error });
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it("two open cycles is refused as the file's problem: dual submission has no current venue", () => {
|
|
497
|
+
const error: unknown = expect.stringMatching(
|
|
498
|
+
/are both open — a paper is on one attempt/,
|
|
499
|
+
);
|
|
500
|
+
expect(parsePaperSettings({ cycles: [alsoOpen, open] })).toEqual({
|
|
501
|
+
ok: false,
|
|
502
|
+
error,
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it("a malformed cycle is refused with the entry and key, through readPaperSettings as `broken`", () => {
|
|
507
|
+
const files = memoryFiles({
|
|
508
|
+
"/work/package.json": "{}",
|
|
509
|
+
[`${PAPER}/paperlint.json`]: JSON.stringify({
|
|
510
|
+
cycles: [{ ...open, opened: "soon" }],
|
|
511
|
+
}),
|
|
512
|
+
});
|
|
513
|
+
const why: unknown = expect.stringMatching(
|
|
514
|
+
/^cycles\[0\]: "opened" must be a date/,
|
|
515
|
+
);
|
|
516
|
+
expect(readPaperSettings(files, PAPER)).toEqual({
|
|
517
|
+
ok: false,
|
|
518
|
+
error: { kind: "broken", why },
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
});
|