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
package/src/cli.ts
CHANGED
|
@@ -74,6 +74,8 @@ import {
|
|
|
74
74
|
import {
|
|
75
75
|
FORMAT_RULE_LEVELS,
|
|
76
76
|
formatRules,
|
|
77
|
+
CYCLE_PORT_RULE_LEVELS,
|
|
78
|
+
cyclePortRules,
|
|
77
79
|
TEX_VENUE_RULE_LEVELS,
|
|
78
80
|
texVenueRules,
|
|
79
81
|
} from "./tex-venue-rules.ts";
|
|
@@ -130,15 +132,7 @@ import {
|
|
|
130
132
|
init,
|
|
131
133
|
processInteractivity,
|
|
132
134
|
} from "./init.ts";
|
|
133
|
-
import {
|
|
134
|
-
DEFAULT_FORMAT,
|
|
135
|
-
FORMATS,
|
|
136
|
-
isFormat,
|
|
137
|
-
newPaper,
|
|
138
|
-
reportNewPaper,
|
|
139
|
-
type PaperFormat,
|
|
140
|
-
type VenueSetting,
|
|
141
|
-
} from "./new-paper.ts";
|
|
135
|
+
import { newPaper, reportNewPaper, type VenueSetting } from "./new-paper.ts";
|
|
142
136
|
// The one source for the consumer's config key lives in lib/ (the ESLint rules and the skill scripts
|
|
143
137
|
// import it too). It is imported compiled, from dist/, the one path that resolves the same from src/
|
|
144
138
|
// and from dist/ — see CONTRIBUTING.md.
|
|
@@ -164,6 +158,8 @@ export { init };
|
|
|
164
158
|
export { nextSteps } from "./init.ts";
|
|
165
159
|
|
|
166
160
|
import paperStages from "#eslint-rules/paper-stages";
|
|
161
|
+
import cycleStage from "#eslint-rules/cycle-stage";
|
|
162
|
+
import { CYCLE_RULE_LEVELS, cycleRules } from "./cycle-rules.ts";
|
|
167
163
|
import {
|
|
168
164
|
FOLDER_VENUE_RULE_LEVELS,
|
|
169
165
|
folderVenueRules,
|
|
@@ -185,7 +181,7 @@ const USAGE = `paperlint — machine-checkable gates for a paper kept in git
|
|
|
185
181
|
npx paperlint init [dir] set the project up: detect the papers directory, declare it
|
|
186
182
|
in package.json, link the skills, wire the hooks into
|
|
187
183
|
.claude/settings.json, offer the CI step, report what is missing
|
|
188
|
-
npx paperlint new <name> [--venue <preset>] [--kind <kind>]
|
|
184
|
+
npx paperlint new <name> [--venue <preset>] [--kind <kind>]
|
|
189
185
|
create <papers>/<name>/ from the template; never overwrites,
|
|
190
186
|
on an existing folder adds only the missing files, then lints it.
|
|
191
187
|
A <name> that names a venue (aisec-2026) is refused: name the
|
|
@@ -221,7 +217,6 @@ init:
|
|
|
221
217
|
or with CI set, nothing is asked either
|
|
222
218
|
--no-hooks do not wire the hooks (the default without a human is to wire them)
|
|
223
219
|
--paper <name> create this paper too (without a human, the only way init creates one)
|
|
224
|
-
--format tex|md the new paper's source format; default tex
|
|
225
220
|
|
|
226
221
|
new:
|
|
227
222
|
--venue <preset> the venue preset, written as "extends" into the paper's paperlint.json:
|
|
@@ -230,7 +225,6 @@ new:
|
|
|
230
225
|
On a terminal without --venue, new asks; "none" leaves it unset
|
|
231
226
|
--kind <kind> the paper's kind at that venue (its page limit), e.g. short — one of the
|
|
232
227
|
preset's kinds; needs --venue
|
|
233
|
-
--format tex|md the paper's source format; default tex
|
|
234
228
|
--allow-venue-name create the folder even though its name names a venue (a whole word such as
|
|
235
229
|
"realm" can be the work's own). Without it such a name is refused: a rejected
|
|
236
230
|
paper moves to another venue and keeps its folder
|
|
@@ -280,6 +274,24 @@ settings — paperlint.json, at two levels, one schema. Both are optional.
|
|
|
280
274
|
"extends" names a venue preset: paperlint:<name> (shipped: ${SHIPPED_VENUES().join(", ")})
|
|
281
275
|
or ./path.jsonc, relative to the paperlint.json. npm presets are not supported yet.
|
|
282
276
|
|
|
277
|
+
cycles and deadlines — a paper's attempts at venues, oldest first, instead of "extends",
|
|
278
|
+
"kind" and "submission" (those are then the current attempt's, and refused beside "cycles"):
|
|
279
|
+
|
|
280
|
+
{ "cycles": [ { "id": "msr-2027", "venue": { "kind": "preset", "extends": "paperlint:msr" },
|
|
281
|
+
"kind": "technical", "opened": "2026-10-05",
|
|
282
|
+
"deadlines": [ { "what": "submission",
|
|
283
|
+
"observed": [ { "at": "2026-10-23 AoE", "source": "call",
|
|
284
|
+
"url": "<the call>", "read": "2026-10-05" } ] } ] } ] }
|
|
285
|
+
|
|
286
|
+
A venue preset carries the portal's deadlines, read and dated; the package updates them and
|
|
287
|
+
your dependency update brings them. A paper adds what the call says ("source": "call") and,
|
|
288
|
+
for a deadline moved by the chairs, "override": { "at", "reason", "evidence" } — evidence a
|
|
289
|
+
file in the paper folder. The override wins; otherwise the earlier reading binds. "what":
|
|
290
|
+
registration (the call's abstract), submission (the full paper), resubmission, notification,
|
|
291
|
+
camera-ready. A venue without a preset: "venue": { "kind": "named", "name", "url" }, every
|
|
292
|
+
deadline typed from its call. Lint never reads the clock: it checks the record, not the time
|
|
293
|
+
left. docs/configuration.md#the-papers-cycles
|
|
294
|
+
|
|
283
295
|
"rules" is { "<rule>": "<severity>" } for every paper file in scope, or ESLint flat-config
|
|
284
296
|
blocks (files, ignores, rules) with globs relative to that file. Order, later wins: paperlint's
|
|
285
297
|
own, the venue preset's, the root file's, the paper's. Optional rules (off unless turned on):
|
|
@@ -303,6 +315,22 @@ const REGISTER_AND_CASE_RULES = {
|
|
|
303
315
|
"heading-case": headingCaseRule(TEX_RULE_DEPS),
|
|
304
316
|
};
|
|
305
317
|
|
|
318
|
+
/**
|
|
319
|
+
* The paper's attempts (`cycles` in paperlint.json), judged on `PIPELINE-STATUS.md`: the record itself
|
|
320
|
+
* and its evidence (src/cycle-rules.ts), and the stages that name a cycle (eslint-rules/cycle-stage.ts).
|
|
321
|
+
* The `cycle` plugin's other rule, `port-done`, reads `paper.tex` (src/tex-venue-rules.ts).
|
|
322
|
+
*/
|
|
323
|
+
/** The markdown language with YAML frontmatter: what every markdown block of the config lints as. */
|
|
324
|
+
const MARKDOWN_WITH_FRONTMATTER = {
|
|
325
|
+
language: "markdown/gfm",
|
|
326
|
+
languageOptions: { frontmatter: "yaml" },
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const CYCLE_STATUS_RULES = {
|
|
330
|
+
...cycleRules({ files: nodeFiles, venuesDir: presetsDir() }),
|
|
331
|
+
...cycleStage.rules,
|
|
332
|
+
};
|
|
333
|
+
|
|
306
334
|
/** The rules of the files `paper.tex` includes, each read on its own as a fragment (#144). */
|
|
307
335
|
const FRAGMENT_RULES = {
|
|
308
336
|
"future-promise": texBuild["future-promise"],
|
|
@@ -333,10 +361,7 @@ export function buildConfig(
|
|
|
333
361
|
"paper/section-word": "warn",
|
|
334
362
|
"paper/leading-zero": "warn",
|
|
335
363
|
};
|
|
336
|
-
const md =
|
|
337
|
-
language: "markdown/gfm",
|
|
338
|
-
languageOptions: { frontmatter: "yaml" },
|
|
339
|
-
};
|
|
364
|
+
const md = MARKDOWN_WITH_FRONTMATTER;
|
|
340
365
|
|
|
341
366
|
const cfg: ConfigBlock[] = [
|
|
342
367
|
// 🔴 THE PROJECT'S PAPER TEMPLATE IS NOT A PAPER. `paperlint new` reads `<papers>/.template/`, and
|
|
@@ -364,8 +389,8 @@ export function buildConfig(
|
|
|
364
389
|
},
|
|
365
390
|
{
|
|
366
391
|
files: ["**/PIPELINE-STATUS.md"],
|
|
367
|
-
// Every paper folder has one
|
|
368
|
-
//
|
|
392
|
+
// Every paper folder has one: the rules about the folder itself (its stages, its frozen
|
|
393
|
+
// sources, its name) are judged here, once per paper.
|
|
369
394
|
plugins: {
|
|
370
395
|
markdown,
|
|
371
396
|
paper: {
|
|
@@ -374,20 +399,16 @@ export function buildConfig(
|
|
|
374
399
|
...folderVenueRules({ files: nodeFiles, venuesDir: presetsDir() }),
|
|
375
400
|
},
|
|
376
401
|
},
|
|
402
|
+
cycle: { rules: CYCLE_STATUS_RULES },
|
|
377
403
|
},
|
|
378
404
|
...md,
|
|
379
405
|
rules: {
|
|
380
406
|
"paper/stages": "error",
|
|
381
407
|
"paper/source": "error",
|
|
382
408
|
...FOLDER_VENUE_RULE_LEVELS,
|
|
409
|
+
...CYCLE_RULE_LEVELS,
|
|
383
410
|
},
|
|
384
411
|
},
|
|
385
|
-
{
|
|
386
|
-
files: ["**/paper.md", "**/draft.md"],
|
|
387
|
-
plugins: { markdown, paper: { rules: paperRules } },
|
|
388
|
-
...md,
|
|
389
|
-
rules: prose,
|
|
390
|
-
},
|
|
391
412
|
{
|
|
392
413
|
files: ["**/reviews/*.md"],
|
|
393
414
|
plugins: { markdown, review: reviewFrontmatter },
|
|
@@ -432,6 +453,7 @@ export function buildConfig(
|
|
|
432
453
|
...pageLimitRules({ files: nodeFiles, venuesDir: presetsDir() }),
|
|
433
454
|
},
|
|
434
455
|
},
|
|
456
|
+
cycle: { rules: cyclePortRules(TEX_RULE_DEPS) },
|
|
435
457
|
},
|
|
436
458
|
language: "tex/latex",
|
|
437
459
|
rules: {
|
|
@@ -454,6 +476,7 @@ export function buildConfig(
|
|
|
454
476
|
...TEX_VENUE_RULE_LEVELS,
|
|
455
477
|
...FORMAT_RULE_LEVELS,
|
|
456
478
|
...PAGE_LIMIT_RULE_LEVELS,
|
|
479
|
+
...CYCLE_PORT_RULE_LEVELS,
|
|
457
480
|
// Silent for a paper that declares no `talk` (src/talk-rules.ts), but talk/undeclared.
|
|
458
481
|
...TALK_RULE_LEVELS,
|
|
459
482
|
},
|
|
@@ -546,8 +569,7 @@ export interface RulePlugin {
|
|
|
546
569
|
* file knows every rule id. `@eslint/markdown` is a dependency's plugin and is left out. The LaTeX
|
|
547
570
|
* language rides on `tex` when `texLanguage` is given, and that is how a consumer registers it: a
|
|
548
571
|
* second `tex` plugin of its own is refused by ESLint ("Cannot redefine plugin "tex"", measured
|
|
549
|
-
* with and without the language here). Without it, `tex` carries rules only
|
|
550
|
-
* lints markdown papers alone.
|
|
572
|
+
* with and without the language here). Without it, `tex` carries rules only.
|
|
551
573
|
*/
|
|
552
574
|
export function rulePlugins(texLanguage?: unknown): Record<string, RulePlugin> {
|
|
553
575
|
const out: Record<string, { rules: Record<string, unknown> }> = {};
|
|
@@ -849,7 +871,6 @@ export function parseArgs(argv: readonly string[]): Args {
|
|
|
849
871
|
noHooks: false,
|
|
850
872
|
allowVenueName: false,
|
|
851
873
|
paper: null,
|
|
852
|
-
format: null,
|
|
853
874
|
venue: null,
|
|
854
875
|
kind: null,
|
|
855
876
|
pdf: null,
|
|
@@ -948,10 +969,9 @@ const SWITCHES: ReadonlyMap<
|
|
|
948
969
|
/** The flags whose value is one string field of `Args`. */
|
|
949
970
|
const STRING_FLAGS: ReadonlyMap<
|
|
950
971
|
string,
|
|
951
|
-
"paper" | "
|
|
972
|
+
"paper" | "venue" | "kind" | "pdf" | "abstract"
|
|
952
973
|
> = new Map([
|
|
953
974
|
["--paper", "paper"],
|
|
954
|
-
["--format", "format"],
|
|
955
975
|
["--venue", "venue"],
|
|
956
976
|
["--kind", "kind"],
|
|
957
977
|
["--pdf", "pdf"],
|
|
@@ -1145,7 +1165,6 @@ export function runHook(
|
|
|
1145
1165
|
export async function createPaperAt(
|
|
1146
1166
|
papersRoot: string,
|
|
1147
1167
|
name: string,
|
|
1148
|
-
format: PaperFormat,
|
|
1149
1168
|
{
|
|
1150
1169
|
log,
|
|
1151
1170
|
err,
|
|
@@ -1162,7 +1181,7 @@ export async function createPaperAt(
|
|
|
1162
1181
|
allowVenueName?: boolean;
|
|
1163
1182
|
},
|
|
1164
1183
|
): Promise<number> {
|
|
1165
|
-
const result = newPaper(papersRoot, name,
|
|
1184
|
+
const result = newPaper(papersRoot, name, {
|
|
1166
1185
|
venue,
|
|
1167
1186
|
venues: allowVenueName ? [] : shippedVenueNames(PRESET_DEPS),
|
|
1168
1187
|
});
|
|
@@ -1327,25 +1346,10 @@ async function runNew(
|
|
|
1327
1346
|
const [name, ...extra] = a.paths;
|
|
1328
1347
|
if (!name || extra.length > 0) {
|
|
1329
1348
|
err(
|
|
1330
|
-
`\`new\` takes exactly one paper name: \`paperlint new my-paper [--venue <preset>] [--kind <kind>]
|
|
1349
|
+
`\`new\` takes exactly one paper name: \`paperlint new my-paper [--venue <preset>] [--kind <kind>]\``,
|
|
1331
1350
|
);
|
|
1332
1351
|
return 2;
|
|
1333
1352
|
}
|
|
1334
|
-
let format: PaperFormat = DEFAULT_FORMAT;
|
|
1335
|
-
if (a.format !== null) {
|
|
1336
|
-
if (!isFormat(a.format)) {
|
|
1337
|
-
err(
|
|
1338
|
-
`--format must be one of ${FORMATS.join(", ")} — got \`${a.format}\``,
|
|
1339
|
-
);
|
|
1340
|
-
return 2;
|
|
1341
|
-
}
|
|
1342
|
-
format = a.format;
|
|
1343
|
-
} else if (processInteractivity(a.yes).interactive) {
|
|
1344
|
-
const f = (
|
|
1345
|
-
await askOrDefault(ask, `format: tex / md [${DEFAULT_FORMAT}] `)
|
|
1346
|
-
)?.trim();
|
|
1347
|
-
if (isFormat(f)) format = f;
|
|
1348
|
-
}
|
|
1349
1353
|
const cfg = readConfig({ ...a, json: false }, { log: () => {}, err, cwd });
|
|
1350
1354
|
if (cfg.code !== undefined) return cfg.code;
|
|
1351
1355
|
const roots = papersRoots(cfg);
|
|
@@ -1378,7 +1382,7 @@ async function runNew(
|
|
|
1378
1382
|
ask,
|
|
1379
1383
|
});
|
|
1380
1384
|
if (!venue.ok) return (err(venue.error), 2);
|
|
1381
|
-
return createPaperAt(papersRoot, name,
|
|
1385
|
+
return createPaperAt(papersRoot, name, {
|
|
1382
1386
|
log,
|
|
1383
1387
|
err,
|
|
1384
1388
|
cwd,
|
|
@@ -1629,10 +1633,6 @@ async function runInit(
|
|
|
1629
1633
|
);
|
|
1630
1634
|
return 2;
|
|
1631
1635
|
}
|
|
1632
|
-
if (a.format !== null && !isFormat(a.format)) {
|
|
1633
|
-
err(`--format must be one of ${FORMATS.join(", ")} — got \`${a.format}\``);
|
|
1634
|
-
return 2;
|
|
1635
|
-
}
|
|
1636
1636
|
return await init(a.paths[0] ?? ".", {
|
|
1637
1637
|
log,
|
|
1638
1638
|
err,
|
|
@@ -1640,9 +1640,8 @@ async function runInit(
|
|
|
1640
1640
|
yes: a.yes,
|
|
1641
1641
|
hooks: !a.noHooks,
|
|
1642
1642
|
paper: a.paper,
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
createPaperAt(papersRoot, name, format, { log, err, cwd }),
|
|
1643
|
+
createPaper: (papersRoot, name) =>
|
|
1644
|
+
createPaperAt(papersRoot, name, { log, err, cwd }),
|
|
1646
1645
|
venues: shippedVenueNames(PRESET_DEPS),
|
|
1647
1646
|
tex: initTexLive(a, { log, err, cwd }),
|
|
1648
1647
|
resolveCliPapers: (root: string): string | null =>
|
|
@@ -1764,8 +1763,8 @@ export async function run(
|
|
|
1764
1763
|
};
|
|
1765
1764
|
|
|
1766
1765
|
// Loaded lazily because only `lint` needs it. If the module cannot be loaded (a broken install:
|
|
1767
|
-
// the file missing from the package), lint still runs over the
|
|
1768
|
-
// failing outright — the LaTeX rules simply have no language to run in.
|
|
1766
|
+
// the file missing from the package), lint still runs over the scorecards and the review notes
|
|
1767
|
+
// rather than failing outright — the LaTeX rules simply have no language to run in.
|
|
1769
1768
|
let texLanguage: unknown = null;
|
|
1770
1769
|
try {
|
|
1771
1770
|
({ texLanguage } = await import("#eslint-rules/latex-language"));
|
|
@@ -1831,7 +1830,7 @@ export async function run(
|
|
|
1831
1830
|
// is not invoked — and, not being invoked, it physically cannot report that.
|
|
1832
1831
|
if (results.length === 0) {
|
|
1833
1832
|
err(
|
|
1834
|
-
`nothing was linted under ${paths.map((x) => shown(cwd, x)).join(", ")} — no PIPELINE-STATUS.md, paper.
|
|
1833
|
+
`nothing was linted under ${paths.map((x) => shown(cwd, x)).join(", ")} — no PIPELINE-STATUS.md, paper.tex or reviews/ found there. A clean report over zero files is not a clean report.`,
|
|
1835
1834
|
);
|
|
1836
1835
|
return 1;
|
|
1837
1836
|
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cycle/record` and `cycle/evidence` on a paper folder's settings file: both halves of each — the
|
|
3
|
+
* finding on a planted defect, silence on a consistent record and on the flat form.
|
|
4
|
+
*/
|
|
5
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
import { memoryFiles } from "./adapters/memory/index.ts";
|
|
9
|
+
import { presetsDir } from "./package-dirs.ts";
|
|
10
|
+
import { cycleRules } from "./cycle-rules.ts";
|
|
11
|
+
|
|
12
|
+
const VENUES = presetsDir();
|
|
13
|
+
const DIR = "/work/papers/p";
|
|
14
|
+
/** The shipped presets, so a cycle naming `paperlint:msr` resolves through the memory files. */
|
|
15
|
+
const SHIPPED = Object.fromEntries(
|
|
16
|
+
readdirSync(VENUES)
|
|
17
|
+
.filter((f) => f.endsWith(".jsonc"))
|
|
18
|
+
.map((f) => [join(VENUES, f), readFileSync(join(VENUES, f), "utf8")]),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
interface Reported {
|
|
22
|
+
readonly rule: string;
|
|
23
|
+
readonly messageId: string;
|
|
24
|
+
readonly message: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const closed = {
|
|
28
|
+
id: "alpha-2026",
|
|
29
|
+
venue: { kind: "preset", extends: "paperlint:acm-sigconf" },
|
|
30
|
+
kind: "short",
|
|
31
|
+
opened: "2026-07-01",
|
|
32
|
+
outcome: {
|
|
33
|
+
kind: "rejected",
|
|
34
|
+
date: "2026-09-08",
|
|
35
|
+
desk: true,
|
|
36
|
+
evidence: "reviews/alpha-decision.md",
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
const open = {
|
|
40
|
+
id: "beta-2027",
|
|
41
|
+
venue: { kind: "preset", extends: "paperlint:ieee-conference" },
|
|
42
|
+
kind: "technical",
|
|
43
|
+
opened: "2026-09-09",
|
|
44
|
+
deadlines: [
|
|
45
|
+
{
|
|
46
|
+
what: "submission",
|
|
47
|
+
observed: [
|
|
48
|
+
{
|
|
49
|
+
at: "2026-10-20T04:00:00Z",
|
|
50
|
+
source: "portal",
|
|
51
|
+
url: "https://beta2027.example.org/deadlines",
|
|
52
|
+
read: "2026-09-09",
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
const CALL = {
|
|
59
|
+
source: "call",
|
|
60
|
+
url: "https://2027.msrconf.org/track/msr-2027-technical-papers",
|
|
61
|
+
read: "2026-10-05",
|
|
62
|
+
};
|
|
63
|
+
/** A cycle at the shipped `paperlint:msr`, whose preset carries the portal's readings. */
|
|
64
|
+
const msr = (deadlines: readonly object[]) => ({
|
|
65
|
+
id: "msr-2027",
|
|
66
|
+
venue: { kind: "preset", extends: "paperlint:msr" },
|
|
67
|
+
kind: "technical",
|
|
68
|
+
opened: "2026-10-05",
|
|
69
|
+
deadlines,
|
|
70
|
+
});
|
|
71
|
+
const OVERRIDE = {
|
|
72
|
+
at: "2026-10-27T04:00:00Z",
|
|
73
|
+
reason: "the chairs extended the deadline by mail",
|
|
74
|
+
evidence: "mail/extension.eml",
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** Every cycle rule over the paper folder holding `settings` (a string: the raw file) and `extra` files. */
|
|
78
|
+
function lint(
|
|
79
|
+
settings: object | string | undefined,
|
|
80
|
+
extra: Readonly<Record<string, string>> = {},
|
|
81
|
+
file = "PIPELINE-STATUS.md",
|
|
82
|
+
): Reported[] {
|
|
83
|
+
const files = memoryFiles({
|
|
84
|
+
...(settings === undefined
|
|
85
|
+
? {}
|
|
86
|
+
: {
|
|
87
|
+
[`${DIR}/paperlint.json`]:
|
|
88
|
+
typeof settings === "string" ? settings : JSON.stringify(settings),
|
|
89
|
+
}),
|
|
90
|
+
...SHIPPED,
|
|
91
|
+
...extra,
|
|
92
|
+
});
|
|
93
|
+
const out: Reported[] = [];
|
|
94
|
+
Object.entries(cycleRules({ files, venuesDir: VENUES })).forEach(
|
|
95
|
+
([name, rule]) => {
|
|
96
|
+
rule
|
|
97
|
+
.create({
|
|
98
|
+
filename: `${DIR}/${file}`,
|
|
99
|
+
sourceCode: { getLocFromIndex: () => ({ line: 1, column: 0 }) },
|
|
100
|
+
report: (d) => {
|
|
101
|
+
const template = rule.meta.messages[d.messageId] ?? "";
|
|
102
|
+
out.push({
|
|
103
|
+
rule: `cycle/${name}`,
|
|
104
|
+
messageId: d.messageId,
|
|
105
|
+
message: Object.entries(d.data).reduce(
|
|
106
|
+
(m, [k, v]) => m.split(`{{${k}}}`).join(String(v)),
|
|
107
|
+
template,
|
|
108
|
+
),
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
.root?.();
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const EVIDENCE = { [`${DIR}/reviews/alpha-decision.md`]: "# decision\n" };
|
|
119
|
+
|
|
120
|
+
describe("cycle/record", () => {
|
|
121
|
+
it("reports on PIPELINE-STATUS.md only — any other file in the folder gets nothing", () => {
|
|
122
|
+
expect(lint({ cycles: [open, open] }, {}, "paper.tex")).toEqual([]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("a consistent record with its evidence on disk: every cycle rule is silent", () => {
|
|
126
|
+
expect(lint({ cycles: [closed, open] }, EVIDENCE)).toEqual([]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("the flat form, no settings file, or a file that is not JSON: silent — not this rule's to say", () => {
|
|
130
|
+
expect(lint({ extends: "paperlint:aidc", kind: "regular" })).toEqual([]);
|
|
131
|
+
expect(lint(undefined)).toEqual([]);
|
|
132
|
+
expect(lint("{ not json")).toEqual([]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("two open cycles: `broken`, naming both and the cure", () => {
|
|
136
|
+
const [f] = lint({
|
|
137
|
+
cycles: [{ ...closed, outcome: { kind: "open" } }, open],
|
|
138
|
+
});
|
|
139
|
+
expect(f?.messageId).toBe("broken");
|
|
140
|
+
expect(f?.message).toMatch(
|
|
141
|
+
/^paperlint\.json: cycles «alpha-2026» and «beta-2027» are both open/,
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("a deadline without its zone: `broken`, naming the entry and the key", () => {
|
|
146
|
+
const bad = msr([
|
|
147
|
+
{ what: "submission", observed: [{ ...CALL, at: "2026-10-23" }] },
|
|
148
|
+
]);
|
|
149
|
+
const m: unknown = expect.stringMatching(
|
|
150
|
+
/^paperlint\.json: cycles\[0\]\.deadlines\[0\]\.observed\[0\]: "at" must be an instant with its zone/,
|
|
151
|
+
);
|
|
152
|
+
expect(lint({ cycles: [bad] }).map((f) => f.message)).toEqual([m]);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("cycle/record — the deadlines, over the venue preset's readings", () => {
|
|
157
|
+
it("MSR 2027 as a newcomer records the call: abstract → registration, paper → submission; the preset's portal readings are derived — silent", () => {
|
|
158
|
+
// The portal's submission (2026-10-20T04:00Z, from the preset) is earlier than the call's
|
|
159
|
+
// registration (the end of 2026-10-20 AoE): two sources disagreeing, not a typo.
|
|
160
|
+
const fs = lint({
|
|
161
|
+
cycles: [
|
|
162
|
+
msr([
|
|
163
|
+
{
|
|
164
|
+
what: "registration",
|
|
165
|
+
observed: [{ ...CALL, at: "2026-10-20 AoE" }],
|
|
166
|
+
},
|
|
167
|
+
{ what: "submission", observed: [{ ...CALL, at: "2026-10-23 AoE" }] },
|
|
168
|
+
]),
|
|
169
|
+
],
|
|
170
|
+
});
|
|
171
|
+
expect(fs).toEqual([]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("the call's own dates out of order: `order`, naming the cycle and the source", () => {
|
|
175
|
+
const fs = lint({
|
|
176
|
+
cycles: [
|
|
177
|
+
msr([
|
|
178
|
+
{
|
|
179
|
+
what: "registration",
|
|
180
|
+
observed: [{ ...CALL, at: "2026-10-25 AoE" }],
|
|
181
|
+
},
|
|
182
|
+
{ what: "submission", observed: [{ ...CALL, at: "2026-10-23 AoE" }] },
|
|
183
|
+
]),
|
|
184
|
+
],
|
|
185
|
+
});
|
|
186
|
+
expect(fs.map((f) => f.messageId)).toEqual(["order"]);
|
|
187
|
+
expect(fs[0]?.message).toBe(
|
|
188
|
+
'paperlint.json, cycle «msr-2027»: the call\'s "registration" (2026-10-26T11:59:59Z) is after its "submission" (2026-10-24T11:59:59Z) — a date typed wrong, or an AoE day miscounted (write a call\'s date as "YYYY-MM-DD AoE" and paperlint converts it)',
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("cycle/record — a portal reading beside the preset's", () => {
|
|
194
|
+
it("a portal reading of a deadline the venue's preset already carries: `presetCopy`, naming the preset's reading", () => {
|
|
195
|
+
const fs = lint({
|
|
196
|
+
cycles: [
|
|
197
|
+
msr([
|
|
198
|
+
{
|
|
199
|
+
what: "submission",
|
|
200
|
+
observed: [
|
|
201
|
+
{
|
|
202
|
+
at: "2026-10-20T04:00:00Z",
|
|
203
|
+
source: "portal",
|
|
204
|
+
url: "https://msr2027.hotcrp.com/deadlines",
|
|
205
|
+
read: "2026-10-01",
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
]),
|
|
210
|
+
],
|
|
211
|
+
});
|
|
212
|
+
expect(fs.map((f) => f.message)).toEqual([
|
|
213
|
+
"paperlint.json, cycle «msr-2027»: the \"submission\" deadline records a portal reading, and the preset paperlint:msr carries the portal's reading of it (2026-10-20T04:00:00Z, read 2026-10-05) — a second copy drifts when paperlint updates the preset: delete this one (the call's reading and an override stay)",
|
|
214
|
+
]);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("a portal reading of a venue whose preset carries none, or a named venue: the paper's to record — silent", () => {
|
|
218
|
+
expect(lint({ cycles: [open] })).toEqual([]);
|
|
219
|
+
expect(
|
|
220
|
+
lint({
|
|
221
|
+
cycles: [
|
|
222
|
+
{
|
|
223
|
+
...open,
|
|
224
|
+
venue: {
|
|
225
|
+
kind: "named",
|
|
226
|
+
name: "Beta '27",
|
|
227
|
+
url: "https://beta.example.org",
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
}),
|
|
232
|
+
).toEqual([]);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("a cycle whose preset does not resolve: its readings are judged alone (pdf/profile names the preset)", () => {
|
|
236
|
+
expect(
|
|
237
|
+
lint({
|
|
238
|
+
cycles: [
|
|
239
|
+
{ ...open, venue: { kind: "preset", extends: "paperlint:nosuch" } },
|
|
240
|
+
],
|
|
241
|
+
}),
|
|
242
|
+
).toEqual([]);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe("cycle/evidence", () => {
|
|
247
|
+
it("a closed cycle whose evidence file is not in the folder: `missing`, with the path", () => {
|
|
248
|
+
const fs = lint({ cycles: [closed, open] });
|
|
249
|
+
expect(fs.map((f) => f.rule)).toEqual(["cycle/evidence"]);
|
|
250
|
+
expect(fs[0]?.message).toMatch(
|
|
251
|
+
/^cycle «alpha-2026» is rejected \(2026-09-08\) with evidence `reviews\/alpha-decision\.md`, which is not in the paper folder/,
|
|
252
|
+
);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("an override whose evidence is not in the folder: `overrideMissing`; on disk: silent", () => {
|
|
256
|
+
const settings = {
|
|
257
|
+
cycles: [msr([{ what: "submission", override: OVERRIDE }])],
|
|
258
|
+
};
|
|
259
|
+
expect(lint(settings).map((f) => [f.rule, f.message])).toEqual([
|
|
260
|
+
[
|
|
261
|
+
"cycle/evidence",
|
|
262
|
+
'cycle «msr-2027»: the override of the "submission" deadline (2026-10-27T04:00:00Z) rests on `mail/extension.eml`, which is not in the paper folder — save the mail or the page that grants it there, or correct the path',
|
|
263
|
+
],
|
|
264
|
+
]);
|
|
265
|
+
expect(
|
|
266
|
+
lint(settings, { [`${DIR}/mail/extension.eml`]: "From: chairs\n" }),
|
|
267
|
+
).toEqual([]);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("an open cycle owes no evidence; a broken record is `cycle/record`'s alone", () => {
|
|
271
|
+
expect(lint({ cycles: [open] })).toEqual([]);
|
|
272
|
+
expect(
|
|
273
|
+
lint({ cycles: [{ ...closed, outcome: { kind: "open" } }, open] }).map(
|
|
274
|
+
(f) => f.rule,
|
|
275
|
+
),
|
|
276
|
+
).toEqual(["cycle/record"]);
|
|
277
|
+
});
|
|
278
|
+
});
|