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,7 +1,7 @@
|
|
|
1
1
|
/** `paperlint submission new`: what it reads and sends, the dry run, the save that records the id, the refusals. */
|
|
2
2
|
import { map } from "remeda";
|
|
3
3
|
import { expect, test } from "vitest";
|
|
4
|
-
import { builtPaper } from "../test/recorded-fixture.ts";
|
|
4
|
+
import { builtPaper, recordText } from "../test/recorded-fixture.ts";
|
|
5
5
|
import { latexReader } from "./adapters/latex/index.ts";
|
|
6
6
|
import { absolutePath } from "./domain/paths.ts";
|
|
7
7
|
import { err, ok } from "./domain/result.ts";
|
|
@@ -104,6 +104,16 @@ test("new --save in the cycles form: the id goes into the open attempt, the last
|
|
|
104
104
|
const fs = newFiles({
|
|
105
105
|
paper: { authors: AUTHORS, cycles: [closed, cycle("second", null)] },
|
|
106
106
|
});
|
|
107
|
+
// Built: a save in the cycles form freezes the PDF with the source the build read.
|
|
108
|
+
fs.writeAtomic(
|
|
109
|
+
absolutePath(`${PAPER}/_build/sources.json`),
|
|
110
|
+
new TextEncoder().encode(
|
|
111
|
+
recordText(
|
|
112
|
+
{ "paper.tex": DEFAULT_TEX },
|
|
113
|
+
{ inputs: [["paper.tex", "body"]] },
|
|
114
|
+
),
|
|
115
|
+
),
|
|
116
|
+
);
|
|
107
117
|
const r = await run({ sub: "new", save: true }, { files: fs });
|
|
108
118
|
expect(r.code, r.errs.join("\n")).toBe(0);
|
|
109
119
|
const written: unknown = JSON.parse(
|
package/src/submission.ts
CHANGED
|
@@ -82,6 +82,13 @@ import { originOf, type PaperSource } from "./domain/paper-source.ts";
|
|
|
82
82
|
import type { SourcesCodec } from "./ports/sources-codec.ts";
|
|
83
83
|
import { MAIN } from "./build.ts";
|
|
84
84
|
import { assessSent, venueMessages, type VenueMessage } from "./venue-rules.ts";
|
|
85
|
+
import {
|
|
86
|
+
freezePlan,
|
|
87
|
+
freezeSent,
|
|
88
|
+
NOT_FROZEN,
|
|
89
|
+
versionLines,
|
|
90
|
+
type FreezePlan,
|
|
91
|
+
} from "./versions.ts";
|
|
85
92
|
|
|
86
93
|
/** How to make and set the site's token: the page, the scopes, the variable. Pure. */
|
|
87
94
|
export function tokenSteps(
|
|
@@ -168,6 +175,8 @@ export interface SubmissionDeps {
|
|
|
168
175
|
file: string,
|
|
169
176
|
messages: readonly VenueMessage[],
|
|
170
177
|
) => Promise<string>;
|
|
178
|
+
/** The instant a frozen version's lock entry records (`versions/lock.json`), ISO 8601. */
|
|
179
|
+
readonly now: () => string;
|
|
171
180
|
readonly log: (line: string) => void;
|
|
172
181
|
readonly err: (line: string) => void;
|
|
173
182
|
}
|
|
@@ -468,7 +477,12 @@ async function runShow(
|
|
|
468
477
|
shown: shownPath(deps.cwd, pdfPath),
|
|
469
478
|
sha256: bytes === null ? null : sha256Hex(bytes),
|
|
470
479
|
};
|
|
471
|
-
|
|
480
|
+
// With `cycles`, the frozen versions beside the portal's copy: which is under review, which is history.
|
|
481
|
+
const versions = versionLines(t.paperDir, deps);
|
|
482
|
+
[
|
|
483
|
+
...showLines(t, view.value, local),
|
|
484
|
+
...(versions.ok ? ["", "versions", ...versions.value] : []),
|
|
485
|
+
].forEach((l) => {
|
|
472
486
|
deps.log(l);
|
|
473
487
|
});
|
|
474
488
|
return 0;
|
|
@@ -567,15 +581,71 @@ async function runUpdate(
|
|
|
567
581
|
const refused = await venueCheck(t, bytes, a.save, deps);
|
|
568
582
|
logAll(fieldLines(t.fields), deps);
|
|
569
583
|
if (refused) return 2;
|
|
584
|
+
const plan = planFor(t, a.save, deps);
|
|
585
|
+
if (!plan.ok) return (deps.err(plan.error), 2);
|
|
570
586
|
const change = changeOf(a, bytes, abstract.value, t.fields);
|
|
571
587
|
const r = await portal.update(t.id, change, { save: a.save });
|
|
572
588
|
if (!r.ok) return failed(r.error, deps);
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
589
|
+
const sent = { shown, bytes, save: a.save, plan: plan.value };
|
|
590
|
+
return afterUpdate(t, r.value, sent, deps);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/** The portal's answer to `update` printed, and what it kept frozen: the exit code. */
|
|
594
|
+
function afterUpdate(
|
|
595
|
+
t: SubmissionTarget,
|
|
596
|
+
o: UpdateOutcome,
|
|
597
|
+
sent: {
|
|
598
|
+
readonly shown: string;
|
|
599
|
+
readonly bytes: Uint8Array;
|
|
600
|
+
readonly save: boolean;
|
|
601
|
+
readonly plan: FreezePlan | null;
|
|
602
|
+
},
|
|
603
|
+
deps: RunDeps,
|
|
604
|
+
): number {
|
|
605
|
+
const local = { shown: sent.shown, sha256: sha256Hex(sent.bytes) };
|
|
606
|
+
const dropped = droppedLines(t.fields, t.venueFields, o);
|
|
607
|
+
logAll([...updateLines(o, local, sent.save), ...dropped], deps);
|
|
608
|
+
const fine = took(o);
|
|
609
|
+
const kept = fine && !o.dryRun ? sent.plan : null;
|
|
610
|
+
const frozen = freezeKept(t, kept, sent.bytes, deps);
|
|
611
|
+
return fine && dropped.length === 0 ? frozen : 1;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/** The portal answered valid, with a 2xx. */
|
|
615
|
+
const took = (o: {
|
|
616
|
+
readonly valid: boolean;
|
|
617
|
+
readonly httpStatus: number;
|
|
618
|
+
}): boolean => o.valid && o.httpStatus >= 200 && o.httpStatus < 300;
|
|
619
|
+
|
|
620
|
+
/** What a `--save` will freeze, decided before anything is sent; null for a dry run. */
|
|
621
|
+
const planFor = (
|
|
622
|
+
t: SubmissionTarget,
|
|
623
|
+
save: boolean,
|
|
624
|
+
deps: SubmissionDeps,
|
|
625
|
+
): Result<FreezePlan | null, string> =>
|
|
626
|
+
save ? freezePlan(t.paperDir, t.resolved.settings.cycles, deps) : ok(null);
|
|
627
|
+
|
|
628
|
+
/** The PDF a save sent, frozen with its source when there is a plan: 0, or 1 when the lock did not take it. */
|
|
629
|
+
function freezeKept(
|
|
630
|
+
t: SubmissionTarget,
|
|
631
|
+
plan: FreezePlan | null,
|
|
632
|
+
pdf: Uint8Array,
|
|
633
|
+
deps: RunDeps,
|
|
634
|
+
): number {
|
|
635
|
+
if (plan === null) return 0;
|
|
636
|
+
if (plan.kind === "flat") return (deps.log(NOT_FROZEN), 0);
|
|
637
|
+
const r = freezeSent(
|
|
638
|
+
{
|
|
639
|
+
paperDir: t.paperDir,
|
|
640
|
+
plan,
|
|
641
|
+
pdf,
|
|
642
|
+
shownPaper: shownPath(deps.cwd, t.paperDir),
|
|
643
|
+
},
|
|
644
|
+
deps,
|
|
645
|
+
);
|
|
646
|
+
if (!r.ok) return (deps.err(r.error), 1);
|
|
647
|
+
deps.log(r.value);
|
|
648
|
+
return 0;
|
|
579
649
|
}
|
|
580
650
|
|
|
581
651
|
// ── new ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -900,15 +970,16 @@ async function runNew(
|
|
|
900
970
|
const refused = await venueCheck(t, paper.pdf.bytes, a.save, deps);
|
|
901
971
|
logAll(newLines(paper), deps);
|
|
902
972
|
if (refused) return 2;
|
|
973
|
+
const plan = planFor(t, a.save, deps);
|
|
974
|
+
if (!plan.ok) return (deps.err(plan.error), 2);
|
|
903
975
|
const r = await portal.create(paper, { save: a.save });
|
|
904
976
|
if (!r.ok) return failed(r.error, deps);
|
|
905
977
|
const dropped = droppedLines(t.fields, t.venueFields, r.value);
|
|
906
978
|
logAll([...createLines(r.value, a.save), ...dropped], deps);
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
if (!fine || (a.save && r.value.id === null)) return 1;
|
|
979
|
+
if (!took(r.value) || (a.save && r.value.id === null)) return 1;
|
|
980
|
+
const frozen = freezeKept(t, plan.value, paper.pdf.bytes, deps);
|
|
910
981
|
const recorded = afterCreated(t, r.value.id, a, deps);
|
|
911
|
-
return dropped.length === 0 ? recorded : 1;
|
|
982
|
+
return dropped.length === 0 ? Math.max(recorded, frozen) : 1;
|
|
912
983
|
}
|
|
913
984
|
|
|
914
985
|
/** The subcommands that run before the paper has a submission id: `new` creates it, `topics` needs none. */
|
|
@@ -998,9 +998,12 @@ describe("otherVenues", () => {
|
|
|
998
998
|
if (!aidc.ok) return;
|
|
999
999
|
expect(otherVenues(aidc.value, deps).map((o) => o.label)).toEqual([
|
|
1000
1000
|
"acm-sigconf",
|
|
1001
|
+
"AGENT 2027",
|
|
1001
1002
|
"AgenticDev",
|
|
1003
|
+
"AGENTVERIFY",
|
|
1002
1004
|
"AISec",
|
|
1003
1005
|
"MSR",
|
|
1006
|
+
"NLBSE",
|
|
1004
1007
|
]);
|
|
1005
1008
|
});
|
|
1006
1009
|
});
|
package/src/venue-rules.test.ts
CHANGED
|
@@ -270,13 +270,13 @@ describe("pdf/profile — the declaration must resolve, or nothing is judged", (
|
|
|
270
270
|
"an unknown venue (a typo would silently disable every check)",
|
|
271
271
|
{ extends: "paperlint:agentic-dev", kind: "short" },
|
|
272
272
|
"preset",
|
|
273
|
-
/agenticdev, aidc, aisec, ieee-conference, msr, realm/,
|
|
273
|
+
/agent, agenticdev, agentverify, aidc, aisec, ieee-conference, msr, nlbse, realm/,
|
|
274
274
|
],
|
|
275
275
|
[
|
|
276
276
|
"the base TeX set is not a venue",
|
|
277
277
|
{ extends: "paperlint:tex-base" },
|
|
278
278
|
"preset",
|
|
279
|
-
/agenticdev, aidc, aisec, ieee-conference, msr, realm/,
|
|
279
|
+
/agent, agenticdev, agentverify, aidc, aisec, ieee-conference, msr, nlbse, realm/,
|
|
280
280
|
],
|
|
281
281
|
[
|
|
282
282
|
"paperlint.json that is not JSON",
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rules over a paper's frozen versions, each module called directly over in-memory files: the
|
|
3
|
+
* finding on a planted defect, silence on a clean folder, on another file, and on a paper without
|
|
4
|
+
* cycles. (`rule-cases.test.ts` runs each through real ESLint.)
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
import { latexReader } from "./adapters/latex/index.ts";
|
|
8
|
+
import { memoryFiles, memoryListDir } from "./adapters/memory/index.ts";
|
|
9
|
+
import { sourcesCodec } from "./adapters/sources-record/index.ts";
|
|
10
|
+
import { sha256Hex } from "./domain/sha256.ts";
|
|
11
|
+
import { lockText, type Lock } from "./domain/versions.ts";
|
|
12
|
+
import {
|
|
13
|
+
draftRules,
|
|
14
|
+
submittedFrozenRules,
|
|
15
|
+
versionLayoutRules,
|
|
16
|
+
} from "./version-rules.ts";
|
|
17
|
+
|
|
18
|
+
const DIR = "/w/papers/p";
|
|
19
|
+
const enc = (t: string) => new TextEncoder().encode(t);
|
|
20
|
+
const locked = (t: string) => ({
|
|
21
|
+
kind: "file" as const,
|
|
22
|
+
bytes: enc(t).length,
|
|
23
|
+
sha256: sha256Hex(enc(t)),
|
|
24
|
+
frozen: "2026-10-23T16:58:01.000Z",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const OPEN = {
|
|
28
|
+
id: "beta-2027",
|
|
29
|
+
venue: { kind: "named", name: "Beta 2027", url: "https://beta.example" },
|
|
30
|
+
opened: "2026-09-09",
|
|
31
|
+
submission: { id: 7 },
|
|
32
|
+
};
|
|
33
|
+
const ACCEPTED = {
|
|
34
|
+
...OPEN,
|
|
35
|
+
outcome: { kind: "accepted", date: "2026-12-01", evidence: "m.eml" },
|
|
36
|
+
};
|
|
37
|
+
const SOURCE =
|
|
38
|
+
"\\documentclass{article}\n\\begin{document}\nA.\n\\end{document}\n";
|
|
39
|
+
const DRIFTED = SOURCE.replace("A.", "A, then B.\nC.");
|
|
40
|
+
|
|
41
|
+
/** A paper folder: its settings, paper.tex, and `submitted.*` of beta-2027 frozen and locked. */
|
|
42
|
+
function tree(
|
|
43
|
+
o: {
|
|
44
|
+
readonly cycles?: readonly object[] | null;
|
|
45
|
+
readonly draft?: string;
|
|
46
|
+
readonly extra?: Readonly<Record<string, string>>;
|
|
47
|
+
readonly lock?: Lock;
|
|
48
|
+
} = {},
|
|
49
|
+
): Record<string, string> {
|
|
50
|
+
const pdf = "%PDF sent";
|
|
51
|
+
return {
|
|
52
|
+
[`${DIR}/paperlint.json`]: JSON.stringify(
|
|
53
|
+
o.cycles === null ? { extends: null } : { cycles: o.cycles ?? [OPEN] },
|
|
54
|
+
),
|
|
55
|
+
[`${DIR}/paper.tex`]: o.draft ?? SOURCE,
|
|
56
|
+
[`${DIR}/versions/beta-2027/submitted.pdf`]: pdf,
|
|
57
|
+
[`${DIR}/versions/beta-2027/submitted.tex`]: SOURCE,
|
|
58
|
+
[`${DIR}/versions/lock.json`]: lockText(
|
|
59
|
+
o.lock ?? {
|
|
60
|
+
"beta-2027/submitted.pdf": locked(pdf),
|
|
61
|
+
"beta-2027/submitted.tex": locked(SOURCE),
|
|
62
|
+
},
|
|
63
|
+
),
|
|
64
|
+
...o.extra,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface Seen {
|
|
69
|
+
readonly messageId: string;
|
|
70
|
+
readonly data: Readonly<Record<string, string | number>>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Every finding of the scorecard rules on `file` of the tree. */
|
|
74
|
+
function status(
|
|
75
|
+
t: Record<string, string>,
|
|
76
|
+
file = "PIPELINE-STATUS.md",
|
|
77
|
+
): Seen[] {
|
|
78
|
+
const files = memoryFiles(t);
|
|
79
|
+
const deps = { files, listDir: memoryListDir(files) };
|
|
80
|
+
const rules = { ...versionLayoutRules(deps), ...submittedFrozenRules(deps) };
|
|
81
|
+
const out: Seen[] = [];
|
|
82
|
+
Object.values(rules).forEach((rule) => {
|
|
83
|
+
rule
|
|
84
|
+
.create({
|
|
85
|
+
filename: `${DIR}/${file}`,
|
|
86
|
+
sourceCode: { getLocFromIndex: () => ({ line: 1, column: 0 }) },
|
|
87
|
+
report: (d) => out.push({ messageId: d.messageId, data: d.data }),
|
|
88
|
+
})
|
|
89
|
+
.root?.();
|
|
90
|
+
});
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Every finding of the draft rules on `file` (paper.tex by default), its text read off the tree. */
|
|
95
|
+
function draft(t: Record<string, string>, file = "paper.tex"): Seen[] {
|
|
96
|
+
const files = memoryFiles(t);
|
|
97
|
+
const rules = draftRules({ files, latex: latexReader, codec: sourcesCodec });
|
|
98
|
+
const out: Seen[] = [];
|
|
99
|
+
Object.values(rules).forEach((rule) => {
|
|
100
|
+
rule
|
|
101
|
+
.create({
|
|
102
|
+
filename: `${DIR}/${file}`,
|
|
103
|
+
sourceCode: {
|
|
104
|
+
text: t[`${DIR}/${file}`] ?? "",
|
|
105
|
+
getLocFromIndex: () => ({ line: 1, column: 0 }),
|
|
106
|
+
},
|
|
107
|
+
report: (d) => {
|
|
108
|
+
if ("messageId" in d)
|
|
109
|
+
out.push({ messageId: d.messageId, data: d.data ?? {} });
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
.root?.();
|
|
113
|
+
});
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
describe("versions/layout and cycle/submitted-frozen", () => {
|
|
118
|
+
it("a submission frozen, every file locked as it stands: silent", () => {
|
|
119
|
+
expect(status(tree())).toStrictEqual([]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("report on the scorecard only, and say nothing for a paper without cycles", () => {
|
|
123
|
+
const stray = { [`${DIR}/versions/notes.md`]: "x" };
|
|
124
|
+
expect(status(tree({ extra: stray }), "paper.tex")).toStrictEqual([]);
|
|
125
|
+
expect(status(tree({ cycles: null, extra: stray }))).toStrictEqual([]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("a stray file and a submission not frozen, each said by its rule", () => {
|
|
129
|
+
const t = tree({
|
|
130
|
+
cycles: [
|
|
131
|
+
{
|
|
132
|
+
...OPEN,
|
|
133
|
+
outcome: { kind: "rejected", date: "2026-10-01", evidence: "m.eml" },
|
|
134
|
+
},
|
|
135
|
+
{ ...OPEN, id: "gamma-2027", submission: { id: 9 } },
|
|
136
|
+
],
|
|
137
|
+
extra: { [`${DIR}/versions/notes.md`]: "x" },
|
|
138
|
+
});
|
|
139
|
+
expect(status(t).map((f) => f.messageId)).toStrictEqual([
|
|
140
|
+
"stray",
|
|
141
|
+
"unfrozen",
|
|
142
|
+
]);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("cycle/draft-drift and cycle/camera-ready-source", () => {
|
|
147
|
+
it("the draft as frozen: silent; paper.tex only", () => {
|
|
148
|
+
expect(draft(tree())).toStrictEqual([]);
|
|
149
|
+
expect(draft(tree({ draft: DRIFTED }), "notes.tex")).toStrictEqual([]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("an open cycle and a drifted draft: the drift, with the lines", () => {
|
|
153
|
+
expect(draft(tree({ draft: DRIFTED }))).toStrictEqual([
|
|
154
|
+
{
|
|
155
|
+
messageId: "drift",
|
|
156
|
+
data: {
|
|
157
|
+
key: "beta-2027/submitted.tex",
|
|
158
|
+
venue: "Beta 2027",
|
|
159
|
+
drift: "2 lines added, 1 removed",
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("no cycles, a cycle parked between venues, or no frozen source: nothing to measure", () => {
|
|
166
|
+
expect(draft(tree({ cycles: null, draft: DRIFTED }))).toStrictEqual([]);
|
|
167
|
+
const rejected = {
|
|
168
|
+
...OPEN,
|
|
169
|
+
outcome: { kind: "rejected", date: "2026-12-01", evidence: "m.eml" },
|
|
170
|
+
};
|
|
171
|
+
expect(draft(tree({ cycles: [rejected], draft: DRIFTED }))).toStrictEqual(
|
|
172
|
+
[],
|
|
173
|
+
);
|
|
174
|
+
const t = Object.fromEntries(
|
|
175
|
+
Object.entries(tree({ draft: DRIFTED })).filter(
|
|
176
|
+
([k]) => !k.endsWith("/submitted.tex"),
|
|
177
|
+
),
|
|
178
|
+
);
|
|
179
|
+
expect(draft(t)).toStrictEqual([]);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("cycle/camera-ready-source", () => {
|
|
184
|
+
it("an accepted cycle: silent while the camera-ready is edited, however far the draft moved", () => {
|
|
185
|
+
expect(draft(tree({ cycles: [ACCEPTED], draft: DRIFTED }))).toStrictEqual(
|
|
186
|
+
[],
|
|
187
|
+
);
|
|
188
|
+
// A PDF frozen without its source: nothing to compare (versions/layout names the missing source).
|
|
189
|
+
const pdfOnly = {
|
|
190
|
+
[`${DIR}/versions/beta-2027/camera-ready.pdf`]: "%PDF final",
|
|
191
|
+
};
|
|
192
|
+
expect(
|
|
193
|
+
draft(tree({ cycles: [ACCEPTED], draft: DRIFTED, extra: pdfOnly })),
|
|
194
|
+
).toStrictEqual([]);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("an accepted cycle with its camera-ready frozen: judged against what was accepted, the draft no more", () => {
|
|
198
|
+
const camera = (source: string) => ({
|
|
199
|
+
[`${DIR}/versions/beta-2027/camera-ready.pdf`]: "%PDF final",
|
|
200
|
+
[`${DIR}/versions/beta-2027/camera-ready.tex`]: source,
|
|
201
|
+
});
|
|
202
|
+
// The accepted source plus a change: it derives, and the draft moving on after it is free.
|
|
203
|
+
expect(
|
|
204
|
+
draft(
|
|
205
|
+
tree({ cycles: [ACCEPTED], draft: "Next.", extra: camera(DRIFTED) }),
|
|
206
|
+
),
|
|
207
|
+
).toStrictEqual([]);
|
|
208
|
+
expect(
|
|
209
|
+
draft(
|
|
210
|
+
tree({
|
|
211
|
+
cycles: [ACCEPTED],
|
|
212
|
+
extra: camera("\\documentclass{book}\nAnother paper.\n"),
|
|
213
|
+
}),
|
|
214
|
+
),
|
|
215
|
+
).toStrictEqual([
|
|
216
|
+
{
|
|
217
|
+
messageId: "base",
|
|
218
|
+
data: { id: "beta-2027", kept: "0", total: "4" },
|
|
219
|
+
},
|
|
220
|
+
]);
|
|
221
|
+
});
|
|
222
|
+
});
|