paperlint 11.1.0 → 11.1.2
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/dependabot-automerge.yml +20 -6
- package/.github/workflows/release.yml +10 -0
- package/dist/cli.d.ts +21 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +83 -7
- package/dist/cli.js.map +1 -1
- package/dist/submission-confirm-hook.d.ts +59 -0
- package/dist/submission-confirm-hook.d.ts.map +1 -0
- package/dist/submission-confirm-hook.js +230 -0
- package/dist/submission-confirm-hook.js.map +1 -0
- package/dist/submission.d.ts +13 -1
- package/dist/submission.d.ts.map +1 -1
- package/dist/submission.js +55 -7
- package/dist/submission.js.map +1 -1
- package/dist/venue-rules.d.ts +33 -0
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +84 -9
- package/dist/venue-rules.js.map +1 -1
- package/docs/install.md +4 -4
- package/docs/layout.md +1 -0
- package/docs/submission.md +33 -1
- package/package.json +1 -1
- package/plugin/hooks/hooks.json +4 -0
- package/scripts/refresh-deadlines/main.test.ts +5 -2
- package/scripts/refresh-deadlines/refresh.test.ts +15 -14
- package/skills/submission-portal/SKILL.md +11 -10
- package/skills/submission-portal/SKILL.md.spec.ts +10 -9
- package/src/cli.test.ts +56 -0
- package/src/cli.ts +100 -6
- package/src/cycle-rules.test.ts +14 -6
- package/src/hooks-settings.harness.ts +6 -6
- package/src/presets.test.ts +34 -24
- package/src/submission-confirm-hook.test.ts +399 -0
- package/src/submission-confirm-hook.ts +330 -0
- package/src/submission-fields-run.test.ts +1 -1
- package/src/submission-new.test.ts +29 -2
- package/src/submission-show.test.ts +9 -0
- package/src/submission-update.test.ts +153 -3
- package/src/submission.ts +85 -10
- package/src/venue-rules.test.ts +15 -0
- package/src/venue-rules.ts +126 -10
package/src/cli.test.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
parseSettings,
|
|
26
26
|
run,
|
|
27
27
|
runHook,
|
|
28
|
+
portalHosts,
|
|
28
29
|
silentOptionalRules,
|
|
29
30
|
toolchainTex,
|
|
30
31
|
} from "./cli.ts";
|
|
@@ -241,6 +242,40 @@ test("runHook paper-folder-layout: in-process, on the real disk — a refusal on
|
|
|
241
242
|
);
|
|
242
243
|
});
|
|
243
244
|
|
|
245
|
+
test("runHook submission-confirm: in-process — an ask on stdout for a send, nothing for anything else, and an ask when its input cannot be read", () => {
|
|
246
|
+
const hook = (stdin: () => string) => {
|
|
247
|
+
const out: string[] = [];
|
|
248
|
+
const code = runHook("submission-confirm", {
|
|
249
|
+
stdin,
|
|
250
|
+
out: (t) => out.push(t),
|
|
251
|
+
run: () => {
|
|
252
|
+
throw new Error("an in-process hook spawns nothing");
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
return { code, out };
|
|
256
|
+
};
|
|
257
|
+
const command = (c: string) => () =>
|
|
258
|
+
JSON.stringify({ tool_input: { command: c }, cwd: root });
|
|
259
|
+
const send = hook(command("npx paperlint submission update nope --save"));
|
|
260
|
+
assert.equal(send.code, 0);
|
|
261
|
+
assert.match(
|
|
262
|
+
send.out.join(""),
|
|
263
|
+
/^\{"hookSpecificOutput":\{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"This sends the paper to the venue's portal/,
|
|
264
|
+
);
|
|
265
|
+
assert.deepEqual(hook(command("npx paperlint submission show nope")), {
|
|
266
|
+
code: 0,
|
|
267
|
+
out: [],
|
|
268
|
+
});
|
|
269
|
+
const unreadable = hook(() => {
|
|
270
|
+
throw new Error("EAGAIN");
|
|
271
|
+
});
|
|
272
|
+
assert.equal(unreadable.code, 0);
|
|
273
|
+
assert.match(
|
|
274
|
+
unreadable.out.join(""),
|
|
275
|
+
/"permissionDecision":"ask","permissionDecisionReason":"paperlint could not read this command/,
|
|
276
|
+
);
|
|
277
|
+
});
|
|
278
|
+
|
|
244
279
|
test("runHook paper-folder-layout: by default its output goes to stdout, where Claude Code reads it", () => {
|
|
245
280
|
const dir = join(root, "layout-hook-stdout");
|
|
246
281
|
writeTree(dir, { "papers/p/PIPELINE-STATUS.md": "# status\n" });
|
|
@@ -450,6 +485,27 @@ test("build and toolchain over a broken or empty declaration", async () => {
|
|
|
450
485
|
assert.deepEqual(toolchainTex(broken), toolchainTex(empty));
|
|
451
486
|
});
|
|
452
487
|
|
|
488
|
+
test("portalHosts: the hosts of the shipped presets' portals, and of the venues the project's papers extend", () => {
|
|
489
|
+
const dir = writeTree(join(root, "portal-hosts"), {
|
|
490
|
+
"package.json": "{}",
|
|
491
|
+
"venue.jsonc": JSON.stringify({
|
|
492
|
+
type: "venue",
|
|
493
|
+
name: "Own Venue",
|
|
494
|
+
url: "https://own.example.org/cfp",
|
|
495
|
+
deadlines: [],
|
|
496
|
+
extends: "paperlint:ieee-conference",
|
|
497
|
+
portal: { kind: "hotcrp", url: "https://submit.own-venue.example.org" },
|
|
498
|
+
}),
|
|
499
|
+
"papers/p/PIPELINE-STATUS.md": "# status\n",
|
|
500
|
+
"papers/p/paper.tex": "x",
|
|
501
|
+
"papers/p/paperlint.json": JSON.stringify({ extends: "../../venue.jsonc" }),
|
|
502
|
+
});
|
|
503
|
+
const hosts = portalHosts(dir);
|
|
504
|
+
assert.ok(hosts.includes("aidc.submit.acsac.org"), hosts.join());
|
|
505
|
+
assert.ok(hosts.includes("msr2027.hotcrp.com"), hosts.join());
|
|
506
|
+
assert.ok(hosts.includes("submit.own-venue.example.org"), hosts.join());
|
|
507
|
+
});
|
|
508
|
+
|
|
453
509
|
test("build --dry-run on a paper with a venue plans for the preset's packages", async () => {
|
|
454
510
|
const dir = writeTree(join(root, "build-venue"), {
|
|
455
511
|
"package.json": "{}",
|
package/src/cli.ts
CHANGED
|
@@ -70,6 +70,7 @@ import {
|
|
|
70
70
|
pageLimitRules,
|
|
71
71
|
VENUE_RULE_LEVELS,
|
|
72
72
|
venueRules,
|
|
73
|
+
type VenueMessage,
|
|
73
74
|
} from "./venue-rules.ts";
|
|
74
75
|
import {
|
|
75
76
|
FORMAT_RULE_LEVELS,
|
|
@@ -116,11 +117,13 @@ import {
|
|
|
116
117
|
settingsProblemLine,
|
|
117
118
|
presetProblemText,
|
|
118
119
|
resolvePreset,
|
|
120
|
+
resolveShipped,
|
|
119
121
|
shippedPresets,
|
|
120
122
|
shippedSpec,
|
|
121
123
|
shippedVenueNames,
|
|
122
124
|
SHIPPED_PREFIX,
|
|
123
125
|
type PaperPreset,
|
|
126
|
+
type Preset,
|
|
124
127
|
type PresetTemplate,
|
|
125
128
|
} from "./presets.ts";
|
|
126
129
|
import type { ToolInstaller } from "./ports/tool-installer.ts";
|
|
@@ -181,6 +184,11 @@ import {
|
|
|
181
184
|
folderLayoutRules,
|
|
182
185
|
} from "./folder-layout-rule.ts";
|
|
183
186
|
import { folderLayoutHook } from "./folder-layout-hook.ts";
|
|
187
|
+
import {
|
|
188
|
+
submissionConfirm,
|
|
189
|
+
UNREADABLE_INPUT,
|
|
190
|
+
} from "./submission-confirm-hook.ts";
|
|
191
|
+
import { leafCommandsNormalized } from "vigiles/hook";
|
|
184
192
|
import {
|
|
185
193
|
REPRO_LAYOUT_RULE_LEVELS,
|
|
186
194
|
reproLayoutRules,
|
|
@@ -238,7 +246,7 @@ const USAGE = `paperlint — machine-checkable gates for a paper kept in git
|
|
|
238
246
|
create the paper's submission on the venue's portal: the title
|
|
239
247
|
and abstract from the build, the authors from paperlint.json,
|
|
240
248
|
the PDF. A DRY RUN unless --save; a save records the new id in
|
|
241
|
-
paperlint.json
|
|
249
|
+
paperlint.json. Prints the venue check first, as update does
|
|
242
250
|
npx paperlint submission show [paper]
|
|
243
251
|
the paper's submission on the venue's portal: status, title,
|
|
244
252
|
type, topics, abstract length, the PDF the portal holds, and
|
|
@@ -246,7 +254,9 @@ const USAGE = `paperlint — machine-checkable gates for a paper kept in git
|
|
|
246
254
|
npx paperlint submission update [paper] [--pdf <file>] [--abstract <file>] [--submit] [--save]
|
|
247
255
|
send the PDF (default: the paper's own), an abstract, or
|
|
248
256
|
"submitted". A DRY RUN unless --save: the portal checks the
|
|
249
|
-
change and keeps nothing
|
|
257
|
+
change and keeps nothing. First prints what the venue rules find
|
|
258
|
+
in that exact PDF, as lint does; --save refuses a PDF the last
|
|
259
|
+
build did not measure (exit 2)
|
|
250
260
|
npx paperlint submission topics [paper]
|
|
251
261
|
the topics the venue's portal offers, against the ones its
|
|
252
262
|
preset records; exit 1 when they differ
|
|
@@ -1237,8 +1247,42 @@ const IN_PROCESS_HOOKS: Readonly<Record<string, (io: HookIo) => number>> = {
|
|
|
1237
1247
|
}
|
|
1238
1248
|
return 0;
|
|
1239
1249
|
},
|
|
1250
|
+
// Never advisory-silent: an input it cannot read is asked about, not let through.
|
|
1251
|
+
"submission-confirm": ({ stdin, out }) => {
|
|
1252
|
+
const said = ((): ReturnType<typeof submissionConfirm> => {
|
|
1253
|
+
try {
|
|
1254
|
+
return submissionConfirm(stdin(), {
|
|
1255
|
+
...PRESET_DEPS,
|
|
1256
|
+
portalHosts,
|
|
1257
|
+
shellLeaves: leafCommandsNormalized,
|
|
1258
|
+
readSend: submissionSend,
|
|
1259
|
+
});
|
|
1260
|
+
} catch {
|
|
1261
|
+
return UNREADABLE_INPUT;
|
|
1262
|
+
}
|
|
1263
|
+
})();
|
|
1264
|
+
if (said !== null) out(`${JSON.stringify(said)}\n`);
|
|
1265
|
+
return 0;
|
|
1266
|
+
},
|
|
1240
1267
|
};
|
|
1241
1268
|
|
|
1269
|
+
/**
|
|
1270
|
+
* The words after `submission` read by this CLI's own parser, as the command reads them: the paper
|
|
1271
|
+
* and `--pdf` a send names, null for their defaults — or null when the words would not run cleanly
|
|
1272
|
+
* (a flag missing its value, an unknown flag, a stray operand), so `submission-confirm` says it
|
|
1273
|
+
* cannot tell rather than guess.
|
|
1274
|
+
*/
|
|
1275
|
+
export function submissionSend(
|
|
1276
|
+
words: readonly string[],
|
|
1277
|
+
): { readonly paper: string | null; readonly pdf: string | null } | null {
|
|
1278
|
+
const a = parseArgs(["submission", ...words]);
|
|
1279
|
+
const clean =
|
|
1280
|
+
a.missingValue === undefined &&
|
|
1281
|
+
a.unknownFlag === undefined &&
|
|
1282
|
+
a.paths.length <= 2;
|
|
1283
|
+
return clean ? { paper: a.paths[1] ?? null, pdf: a.pdf } : null;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1242
1286
|
/**
|
|
1243
1287
|
* `paperlint hook <name>` — run an editor hook. It exists for ONE thing: so that the wiring does not
|
|
1244
1288
|
* address the runtime from the project root.
|
|
@@ -1653,17 +1697,40 @@ function paperRequirements(dir: string): TexRequirements {
|
|
|
1653
1697
|
* package, so the shipped union alone would not see it.
|
|
1654
1698
|
*/
|
|
1655
1699
|
export function toolchainTex(cwd: string): TexRequirements {
|
|
1700
|
+
const chains = projectPresets(cwd).map((p) => p.tex);
|
|
1701
|
+
return declaredUnion(undefined, chains).tex;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
/** The resolved venue preset of every paper under the project's papers directories, seen from `cwd`. */
|
|
1705
|
+
function projectPresets(cwd: string): readonly Preset[] {
|
|
1656
1706
|
const cfg = readConfig(parseArgs(["toolchain"]), {
|
|
1657
1707
|
log: () => {},
|
|
1658
1708
|
err: () => {},
|
|
1659
1709
|
cwd,
|
|
1660
1710
|
});
|
|
1661
1711
|
const roots = cfg.code === undefined ? papersRoots(cfg) : [];
|
|
1662
|
-
|
|
1712
|
+
return roots
|
|
1663
1713
|
.flatMap((r) => papersIn(r))
|
|
1664
1714
|
.map((dir) => paperPreset(dir, PRESET_DEPS))
|
|
1665
|
-
.flatMap((p) => (p.kind === "resolved" ? [p.preset
|
|
1666
|
-
|
|
1715
|
+
.flatMap((p) => (p.kind === "resolved" ? [p.preset] : []));
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
/**
|
|
1719
|
+
* The host of every venue portal paperlint knows of from `cwd` — the shipped presets', and those the
|
|
1720
|
+
* project's papers extend: what `submission-confirm` asks about when a request goes straight there.
|
|
1721
|
+
*/
|
|
1722
|
+
export function portalHosts(cwd: string): readonly string[] {
|
|
1723
|
+
return [...resolveShipped(PRESET_DEPS), ...projectPresets(cwd)].flatMap(
|
|
1724
|
+
(p) =>
|
|
1725
|
+
p.portal === null
|
|
1726
|
+
? []
|
|
1727
|
+
: [
|
|
1728
|
+
p.portal.url
|
|
1729
|
+
.replace(/^[a-z][a-z0-9+.-]*:\/\//iu, "")
|
|
1730
|
+
.split("/", 1)
|
|
1731
|
+
.join(""),
|
|
1732
|
+
],
|
|
1733
|
+
);
|
|
1667
1734
|
}
|
|
1668
1735
|
|
|
1669
1736
|
/**
|
|
@@ -1755,6 +1822,7 @@ const SIMPLE: Readonly<
|
|
|
1755
1822
|
site: portalSite,
|
|
1756
1823
|
latex: latexReader,
|
|
1757
1824
|
codec: sourcesCodec,
|
|
1825
|
+
lintText,
|
|
1758
1826
|
log,
|
|
1759
1827
|
err,
|
|
1760
1828
|
},
|
|
@@ -2082,6 +2150,32 @@ const isEmptySet = (e: unknown): boolean => {
|
|
|
2082
2150
|
);
|
|
2083
2151
|
};
|
|
2084
2152
|
|
|
2153
|
+
/**
|
|
2154
|
+
* Findings on one file, printed by the formatter `paperlint lint` prints with — what `paperlint
|
|
2155
|
+
* submission` shows of the venue rules before a send, so the two read the same.
|
|
2156
|
+
*/
|
|
2157
|
+
export async function lintText(
|
|
2158
|
+
file: string,
|
|
2159
|
+
messages: readonly VenueMessage[],
|
|
2160
|
+
): Promise<string> {
|
|
2161
|
+
const errorCount = messages.filter((m) => m.severity === 2).length;
|
|
2162
|
+
const result: Readonly<ESLint.LintResult> = {
|
|
2163
|
+
filePath: file,
|
|
2164
|
+
messages: [...messages],
|
|
2165
|
+
suppressedMessages: [],
|
|
2166
|
+
errorCount,
|
|
2167
|
+
fatalErrorCount: 0,
|
|
2168
|
+
warningCount: messages.length - errorCount,
|
|
2169
|
+
fixableErrorCount: 0,
|
|
2170
|
+
fixableWarningCount: 0,
|
|
2171
|
+
usedDeprecatedRules: [],
|
|
2172
|
+
};
|
|
2173
|
+
return (await new ESLint().loadFormatter(LINT_FORMAT)).format([result]);
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
/** The formatter `paperlint lint` prints with. */
|
|
2177
|
+
const LINT_FORMAT = "stylish";
|
|
2178
|
+
|
|
2085
2179
|
/**
|
|
2086
2180
|
* The end of `paperlint lint`: refuse an optional rule that reached no paper, print the findings, and
|
|
2087
2181
|
* decide the exit code. Pulled out of `run` so each question has its own function.
|
|
@@ -2125,7 +2219,7 @@ async function reportLint(
|
|
|
2125
2219
|
// Missing things are printed FIRST: they explain why the report below may be suspiciously
|
|
2126
2220
|
// short. The reverse order would read as "all clean — oh, and also this".
|
|
2127
2221
|
if (structure.length > 0) log(formatStructure(structure));
|
|
2128
|
-
const out = await (await eslint.loadFormatter(
|
|
2222
|
+
const out = await (await eslint.loadFormatter(LINT_FORMAT)).format(results);
|
|
2129
2223
|
log(
|
|
2130
2224
|
out.trim() ||
|
|
2131
2225
|
(structure.length > 0
|
package/src/cycle-rules.test.ts
CHANGED
|
@@ -8,15 +8,23 @@ import { describe, expect, it } from "vitest";
|
|
|
8
8
|
import { memoryFiles } from "./adapters/memory/index.ts";
|
|
9
9
|
import { presetsDir } from "./package-dirs.ts";
|
|
10
10
|
import { cycleRules } from "./cycle-rules.ts";
|
|
11
|
+
import { frozenText } from "../test/frozen-presets.ts";
|
|
11
12
|
|
|
12
13
|
const VENUES = presetsDir();
|
|
13
14
|
const DIR = "/work/papers/p";
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
/**
|
|
16
|
+
* The shipped presets, so a cycle naming `paperlint:msr` resolves through the memory files — msr
|
|
17
|
+
* FROZEN (test/frozen-presets.ts): its deadlines are rewritten by the scheduled refresh, and the
|
|
18
|
+
* tests below assert the readings they sit beside.
|
|
19
|
+
*/
|
|
20
|
+
const SHIPPED = {
|
|
21
|
+
...Object.fromEntries(
|
|
22
|
+
readdirSync(VENUES)
|
|
23
|
+
.filter((f) => f.endsWith(".jsonc"))
|
|
24
|
+
.map((f) => [join(VENUES, f), readFileSync(join(VENUES, f), "utf8")]),
|
|
25
|
+
),
|
|
26
|
+
[join(VENUES, "msr.jsonc")]: frozenText("msr.jsonc"),
|
|
27
|
+
};
|
|
20
28
|
|
|
21
29
|
interface Reported {
|
|
22
30
|
readonly rule: string;
|
|
@@ -65,9 +65,9 @@ check(
|
|
|
65
65
|
// ── the one source: plugin/hooks/hooks.json ─────────────────────────────────────────────
|
|
66
66
|
const wiring = shippedWiring();
|
|
67
67
|
check(
|
|
68
|
-
"the hook names are DERIVED from hooks.json —
|
|
68
|
+
"the hook names are DERIVED from hooks.json — five, in its order",
|
|
69
69
|
wiring.names.join() ===
|
|
70
|
-
"paper-edit-guard,paper-folder-layout,paper-skills-nudge,paper-status-gates",
|
|
70
|
+
"paper-edit-guard,submission-confirm,paper-folder-layout,paper-skills-nudge,paper-status-gates",
|
|
71
71
|
);
|
|
72
72
|
// ── settings written by hand: shapes Claude Code would ignore are skipped, not crashed on ──
|
|
73
73
|
check(
|
|
@@ -284,7 +284,7 @@ try {
|
|
|
284
284
|
"and names the hooks that are not wired in ANY form",
|
|
285
285
|
r.status === "foreign" &&
|
|
286
286
|
r.missing.join() ===
|
|
287
|
-
"paper-folder-layout,paper-skills-nudge,paper-status-gates",
|
|
287
|
+
"submission-confirm,paper-folder-layout,paper-skills-nudge,paper-status-gates",
|
|
288
288
|
);
|
|
289
289
|
// The control: without the check, vigiles' merge WOULD add a second copy. This is the
|
|
290
290
|
// reason the check exists, measured rather than asserted.
|
|
@@ -320,13 +320,13 @@ try {
|
|
|
320
320
|
const wiredOnce = merge({}, wiring.compiled, MANAGED_BY);
|
|
321
321
|
check(
|
|
322
322
|
"wired once each → ✓ wired, with the fresh-clone consequence",
|
|
323
|
-
/✓ wired — paper-edit-guard, paper-folder-layout, paper-skills-nudge, paper-status-gates, once each/.test(
|
|
323
|
+
/✓ wired — paper-edit-guard, submission-confirm, paper-folder-layout, paper-skills-nudge, paper-status-gates, once each/.test(
|
|
324
324
|
doc(wiredOnce),
|
|
325
325
|
) && /cannot run until `npm install`/.test(doc(wiredOnce)),
|
|
326
326
|
);
|
|
327
327
|
check(
|
|
328
328
|
"nothing wired → ⚠ not wired, with the remedy",
|
|
329
|
-
/⚠ not wired — none of the
|
|
329
|
+
/⚠ not wired — none of the 5 hooks/.test(doc({})),
|
|
330
330
|
);
|
|
331
331
|
const twice = PreToolUseSettings.parse(structuredClone(wiredOnce));
|
|
332
332
|
twice.hooks.PreToolUse[0]?.hooks.push({
|
|
@@ -354,7 +354,7 @@ try {
|
|
|
354
354
|
});
|
|
355
355
|
check(
|
|
356
356
|
"🔴 partly wired BY HAND → the remedy is NOT `npx paperlint init`, which would refuse to write",
|
|
357
|
-
/partly wired — missing: paper-folder-layout, paper-skills-nudge, paper-status-gates/.test(
|
|
357
|
+
/partly wired — missing: submission-confirm, paper-folder-layout, paper-skills-nudge, paper-status-gates/.test(
|
|
358
358
|
handPartial,
|
|
359
359
|
) && /writes nothing then/.test(handPartial),
|
|
360
360
|
);
|
package/src/presets.test.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { readdirSync, readFileSync } from "node:fs";
|
|
|
10
10
|
import { join, relative } from "node:path";
|
|
11
11
|
import { describe, expect, it } from "vitest";
|
|
12
12
|
import { memoryFiles } from "./adapters/memory/index.ts";
|
|
13
|
+
import { frozenText } from "../test/frozen-presets.ts";
|
|
13
14
|
import { presetsDir } from "./package-dirs.ts";
|
|
14
15
|
import { parsePreset } from "./tex-requirements.ts";
|
|
15
16
|
import {
|
|
@@ -23,18 +24,27 @@ import {
|
|
|
23
24
|
} from "./presets.ts";
|
|
24
25
|
|
|
25
26
|
const VENUES = presetsDir();
|
|
26
|
-
const
|
|
27
|
+
const live = Object.fromEntries(
|
|
27
28
|
readdirSync(VENUES)
|
|
28
29
|
.filter((f) => f.endsWith(".jsonc") || f.endsWith(".json"))
|
|
29
30
|
.map((f) => [join(VENUES, f), readFileSync(join(VENUES, f))]),
|
|
30
31
|
);
|
|
32
|
+
// The shipped msr.jsonc's deadlines are rewritten by the scheduled refresh, so the tests of LOGIC
|
|
33
|
+
// read it FROZEN (test/frozen-presets.ts); only the shape test below reads the shipped file.
|
|
34
|
+
const shipped = {
|
|
35
|
+
...live,
|
|
36
|
+
[join(VENUES, "msr.jsonc")]: frozenText("msr.jsonc"),
|
|
37
|
+
};
|
|
31
38
|
const PAPER_FILE = "/work/papers/p/paperlint.json";
|
|
32
|
-
const deps = (extra: Record<string, string> = {}) => ({
|
|
33
|
-
files: memoryFiles({ ...
|
|
39
|
+
const deps = (extra: Record<string, string> = {}, base = shipped) => ({
|
|
40
|
+
files: memoryFiles({ ...base, ...extra }),
|
|
34
41
|
venuesDir: VENUES,
|
|
35
42
|
});
|
|
36
|
-
const resolve = (
|
|
37
|
-
|
|
43
|
+
const resolve = (
|
|
44
|
+
spec: string,
|
|
45
|
+
extra: Record<string, string> = {},
|
|
46
|
+
base = shipped,
|
|
47
|
+
) => resolvePreset(spec, PAPER_FILE, deps(extra, base));
|
|
38
48
|
/** A venue preset of a project's own: the identity every venue declares, then `body`. */
|
|
39
49
|
const venue = (body: Record<string, unknown>, name = "Our Workshop"): string =>
|
|
40
50
|
JSON.stringify({
|
|
@@ -80,7 +90,7 @@ describe("shipped presets", () => {
|
|
|
80
90
|
|
|
81
91
|
describe("shipped presets: paperlint:msr", () => {
|
|
82
92
|
it("paperlint:msr → msr over ieee-conference: one kind, appendices counted as body, compsoc forbidden", () => {
|
|
83
|
-
const r = resolve("paperlint:msr");
|
|
93
|
+
const r = resolve("paperlint:msr", {}, live);
|
|
84
94
|
expect(r.ok).toBe(true);
|
|
85
95
|
if (!r.ok) return;
|
|
86
96
|
expect(r.value.chain.map((f) => f.split("/").pop())).toEqual([
|
|
@@ -103,26 +113,26 @@ describe("shipped presets: paperlint:msr", () => {
|
|
|
103
113
|
kind: "hotcrp",
|
|
104
114
|
url: "https://msr2027.hotcrp.com",
|
|
105
115
|
});
|
|
106
|
-
// The
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
source:
|
|
114
|
-
url:
|
|
115
|
-
read:
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
116
|
+
// The deadlines' SHAPE, never their dates: the scheduled refresh rewrites those whenever the
|
|
117
|
+
// portal moves them (deadlines.yml), and a date asserted here turns its pull request red.
|
|
118
|
+
// Guards: the shipped preset keeps a submission deadline, every reading is the portal's own
|
|
119
|
+
// from the venue's deadlines page, with a read day and an instant that carries its zone.
|
|
120
|
+
expect(r.value.deadlines.map((d) => d.what)).toContain("submission");
|
|
121
|
+
expect(
|
|
122
|
+
r.value.deadlines.map((d) => ({
|
|
123
|
+
source: d.source,
|
|
124
|
+
url: d.url,
|
|
125
|
+
read: /^\d{4}-\d{2}-\d{2}$/.test(d.read),
|
|
126
|
+
at: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(d.at),
|
|
127
|
+
})),
|
|
128
|
+
).toEqual(
|
|
129
|
+
r.value.deadlines.map(() => ({
|
|
120
130
|
source: "portal",
|
|
121
131
|
url: "https://msr2027.hotcrp.com/deadlines",
|
|
122
|
-
read:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
132
|
+
read: true,
|
|
133
|
+
at: true,
|
|
134
|
+
})),
|
|
135
|
+
);
|
|
126
136
|
// No rule turned on: the IEEE capitalization rule has not been read and quoted.
|
|
127
137
|
expect(r.value.rules).toEqual({});
|
|
128
138
|
expect(r.value.registerAnchors).toEqual([]);
|