paperlint 9.0.0 → 10.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/CLAUDE.md +22 -0
- package/.github/CLAUDE.md.spec.ts +20 -0
- package/CLAUDE.md +59 -515
- package/CONTRIBUTING.md +2 -2
- package/README.md +68 -128
- package/dist/CLAUDE.md.spec.d.ts +3 -0
- package/dist/CLAUDE.md.spec.d.ts.map +1 -0
- package/dist/CLAUDE.md.spec.js +62 -0
- package/dist/CLAUDE.md.spec.js.map +1 -0
- package/dist/build.d.ts +1 -1
- package/dist/build.js +2 -2
- package/dist/cli.d.ts +5 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +52 -6
- package/dist/cli.js.map +1 -1
- package/dist/domain/layout-tree.d.ts +19 -0
- package/dist/domain/layout-tree.d.ts.map +1 -0
- package/dist/domain/layout-tree.js +32 -0
- package/dist/domain/layout-tree.js.map +1 -0
- package/dist/domain/lookup-cache.d.ts +2 -2
- package/dist/domain/lookup-cache.js +2 -2
- package/dist/domain/paper-layout.d.ts +81 -0
- package/dist/domain/paper-layout.d.ts.map +1 -0
- package/dist/domain/paper-layout.js +259 -0
- package/dist/domain/paper-layout.js.map +1 -0
- package/dist/domain/repro-layout.d.ts +80 -0
- package/dist/domain/repro-layout.d.ts.map +1 -0
- package/dist/domain/repro-layout.js +247 -0
- package/dist/domain/repro-layout.js.map +1 -0
- package/dist/domain/scaffold-layout.d.ts +28 -0
- package/dist/domain/scaffold-layout.d.ts.map +1 -0
- package/dist/domain/scaffold-layout.js +81 -0
- package/dist/domain/scaffold-layout.js.map +1 -0
- package/dist/eslint-rules/CLAUDE.md.spec.d.ts +3 -0
- package/dist/eslint-rules/CLAUDE.md.spec.d.ts.map +1 -0
- package/dist/eslint-rules/CLAUDE.md.spec.js +39 -0
- package/dist/eslint-rules/CLAUDE.md.spec.js.map +1 -0
- package/dist/eslint-rules/install-path-literals.d.ts +1 -1
- package/dist/eslint-rules/install-path-literals.js +4 -4
- package/dist/eslint-rules/install-path-literals.js.map +1 -1
- package/dist/eslint-rules/pdf-last-page-balance.js +1 -1
- package/dist/eslint-rules/prereg-coverage.d.ts +98 -0
- package/dist/eslint-rules/prereg-coverage.d.ts.map +1 -0
- package/dist/eslint-rules/prereg-coverage.js +274 -0
- package/dist/eslint-rules/prereg-coverage.js.map +1 -0
- package/dist/folder-layout-hook.d.ts +21 -0
- package/dist/folder-layout-hook.d.ts.map +1 -0
- package/dist/folder-layout-hook.js +117 -0
- package/dist/folder-layout-hook.js.map +1 -0
- package/dist/folder-layout-rule.d.ts +22 -0
- package/dist/folder-layout-rule.d.ts.map +1 -0
- package/dist/folder-layout-rule.js +107 -0
- package/dist/folder-layout-rule.js.map +1 -0
- package/dist/init.js +1 -1
- package/dist/init.js.map +1 -1
- package/dist/lib/agent-cli-version.js +1 -1
- package/dist/lib/agent-cli-version.js.map +1 -1
- package/dist/new-paper.d.ts +4 -1
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +40 -6
- package/dist/new-paper.js.map +1 -1
- package/dist/references.d.ts +1 -1
- package/dist/references.d.ts.map +1 -1
- package/dist/references.js +2 -2
- package/dist/references.js.map +1 -1
- package/dist/repro-layout-rule.d.ts +16 -0
- package/dist/repro-layout-rule.d.ts.map +1 -0
- package/dist/repro-layout-rule.js +81 -0
- package/dist/repro-layout-rule.js.map +1 -0
- package/dist/tex-requirements.js +1 -1
- package/dist/toolchain.js +1 -1
- package/docs/ci.md +60 -0
- package/docs/configuration.md +2 -0
- package/docs/e2e.md +3 -3
- package/docs/incidents.md +5 -5
- package/docs/install.md +20 -16
- package/docs/layout.md +154 -0
- package/docs/optional-rules.md +5 -4
- package/docs/references.md +5 -2
- package/docs/rules/paper/allowed-files-only.md +192 -0
- package/docs/rules/repro/allowed-files-only.md +128 -0
- package/docs/rules/study/declare-sample.md +146 -0
- package/docs/rules.md +53 -50
- package/docs/skills.md +6 -3
- package/docs/venues.md +94 -0
- package/eslint-rules/CLAUDE.md +44 -0
- package/eslint-rules/CLAUDE.md.spec.ts +43 -0
- package/eslint-rules/install-path-literals.harness.mjs +1 -1
- package/eslint-rules/install-path-literals.ts +4 -4
- package/eslint-rules/pdf-last-page-balance.ts +1 -1
- package/eslint-rules/prereg-coverage.test.ts +448 -0
- package/eslint-rules/prereg-coverage.ts +419 -0
- package/eslint.config.mjs +6 -3
- package/hooks/CLAUDE.md +34 -0
- package/hooks/CLAUDE.md.spec.ts +30 -0
- package/lib/agent-cli-version.ts +1 -1
- package/lib/skill-trigger-cases.harness.mjs +51 -5
- package/lib/skill-trigger-cases.mjs +53 -15
- package/package.json +2 -2
- package/plugin/CLAUDE.md +22 -0
- package/plugin/CLAUDE.md.spec.ts +22 -0
- package/plugin/hooks/hooks.json +9 -0
- package/scripts/coverage-config.test.ts +2 -1
- package/skills/CLAUDE.md +19 -0
- package/skills/CLAUDE.md.spec.ts +18 -0
- package/skills/analyze-sibling-paper/SKILL.md +2 -2
- package/skills/analyze-sibling-paper/SKILL.md.spec.ts +1 -1
- package/skills/build-benchmark/SKILL.md +14 -1
- package/skills/build-benchmark/SKILL.md.spec.ts +13 -0
- package/skills/build-label-page/SKILL.md +118 -0
- package/skills/build-label-page/SKILL.md.spec.ts +129 -0
- package/skills/build-label-page/build-label-page.eval.mjs +15 -0
- package/skills/build-label-page/build-label-page.test.ts +959 -0
- package/skills/build-label-page/references/checklist.md +148 -0
- package/skills/build-label-page/references/example-generator.py +803 -0
- package/skills/find-venue/SKILL.md +5 -3
- package/skills/find-venue/SKILL.md.spec.ts +4 -2
- package/skills/grade-paper-writing/SKILL.md +7 -7
- package/skills/grade-paper-writing/SKILL.md.spec.ts +6 -6
- package/skills/map-prior-work/SKILL.md +2 -2
- package/skills/map-prior-work/SKILL.md.spec.ts +1 -1
- package/skills/paper-pipeline/SKILL.md +8 -6
- package/skills/paper-pipeline/SKILL.md.spec.ts +7 -5
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +19 -9
- package/skills/paper-pipeline/references/acceptance-gate.md +19 -19
- package/skills/paper-pipeline/references/artifact-checklist.md +5 -0
- package/skills/paper-pipeline/references/pipeline-status-template.md +30 -1
- package/skills/paper-pipeline/repro/2026-08-07-fork-check.log +1 -1
- package/skills/paper-pipeline/repro/2026-08-07-fork-check2.log +1 -1
- package/skills/paper-pipeline/repro/2026-08-07-language-eval-pilot.log +1 -1
- package/skills/paper-pipeline/repro/2026-08-07-language-eval-raw.log +1 -1
- package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary-oracle.log +48 -48
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-A6-oracle.log +8 -8
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-oracle.log +48 -48
- package/skills/paper-pipeline/repro/2026-08-08-parent-replication-gpw.log +2 -2
- package/skills/paper-pipeline/repro/2026-08-08-parent-replication.mjs +3 -3
- package/skills/paper-pipeline/repro/2026-10-13-grade-paper-writing-ablation-preflight-after.log +81 -0
- package/skills/paper-pipeline/repro/2026-10-13-grade-paper-writing-ablation-preflight-before.log +81 -0
- package/skills/paper-pipeline/repro/2026-10-13-grade-paper-writing-ablation-setupdiff.log +59 -0
- package/skills/paper-pipeline/repro/2026-10-13-grade-paper-writing-trigger.jsonl +1 -0
- package/skills/paper-pipeline/repro/2026-10-13-grade-paper-writing-trigger.log +22 -0
- package/skills/paper-pipeline/repro/README.md +4 -0
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +2 -2
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +2 -2
- package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +4 -2
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +7 -2
- package/skills/paper-pipeline/scripts/generated-code.mjs +8 -4
- package/skills/paper-pipeline/scripts/generated-code.test.ts +15 -1
- package/skills/paper-status/SKILL.md +2 -2
- package/skills/paper-status/SKILL.md.spec.ts +1 -1
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +21 -102
- package/skills/study-accepted-papers/SKILL.md +163 -161
- package/skills/study-accepted-papers/SKILL.md.spec.ts +167 -165
- package/skills/study-accepted-papers/references/base-rates.md +69 -0
- package/skills/study-accepted-papers/references/mechanisms.md +181 -0
- package/skills/verify-citations/SKILL.md +2 -2
- package/skills/verify-citations/SKILL.md.spec.ts +1 -1
- package/src/CLAUDE.md +23 -1
- package/src/CLAUDE.md.spec.ts +72 -0
- package/src/build.ts +2 -2
- package/src/cli.harness.mjs +42 -0
- package/src/cli.test.ts +90 -6
- package/src/cli.ts +78 -5
- package/src/domain/layout-tree.test.ts +32 -0
- package/src/domain/layout-tree.ts +47 -0
- package/src/domain/lookup-cache.ts +2 -2
- package/src/domain/paper-layout.test.ts +264 -0
- package/src/domain/paper-layout.ts +336 -0
- package/src/domain/repro-layout.test.ts +222 -0
- package/src/domain/repro-layout.ts +311 -0
- package/src/domain/scaffold-layout.ts +104 -0
- package/src/folder-layout-hook.test.ts +214 -0
- package/src/folder-layout-hook.ts +141 -0
- package/src/folder-layout-rule.test.ts +235 -0
- package/src/folder-layout-rule.ts +151 -0
- package/src/hooks-settings.harness.ts +9 -8
- package/src/init.ts +1 -1
- package/src/layout-docs.test.ts +52 -0
- package/src/new-paper.harness.mjs +23 -1
- package/src/new-paper.test.ts +299 -6
- package/src/new-paper.ts +51 -5
- package/src/paper-files.test.ts +9 -6
- package/src/references.test.ts +5 -5
- package/src/references.ts +2 -2
- package/src/repro-layout-rule.test.ts +114 -0
- package/src/repro-layout-rule.ts +121 -0
- package/src/rule-cases.test.ts +98 -0
- package/src/tex-requirements.ts +1 -1
- package/src/toolchain.ts +1 -1
- package/templates/paper/PIPELINE-STATUS.md +41 -7
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* and a placeholder stage would be a red `paper/stages` on the very first run;
|
|
10
10
|
* - the scorecard carries the `**Readiness verdict:**` line the status hook surfaces on every
|
|
11
11
|
* paper edit — without it the hook reports that the paper cannot say whether it is ready;
|
|
12
|
+
* - a new folder also holds the layout (folders with a `.gitkeep`, `repro/README.md`), and lint
|
|
13
|
+
* still reports no finding on it: the rules that read those folders do not see the placeholders;
|
|
12
14
|
* - it never overwrites, and on an existing folder adds only what is missing;
|
|
13
15
|
* - the project's `<papers>/.template/` wins over the package, file by file.
|
|
14
16
|
*
|
|
@@ -82,6 +84,11 @@ try {
|
|
|
82
84
|
`\`${dot}\` is refused — discovery skips dot-directories, so it would never be linted`,
|
|
83
85
|
/starting with a dot/.test(nameProblem(dot, []) ?? ""),
|
|
84
86
|
);
|
|
87
|
+
for (const reserved of ["venues", "extra"])
|
|
88
|
+
check(
|
|
89
|
+
`\`${reserved}\` is refused — the papers directory's own folder, which no paper may take`,
|
|
90
|
+
/reserved/.test(nameProblem(reserved, []) ?? ""),
|
|
91
|
+
);
|
|
85
92
|
check(
|
|
86
93
|
"a refused name creates nothing",
|
|
87
94
|
newPaper(work, "Bad", { venues: [] }).ok === false &&
|
|
@@ -119,6 +126,20 @@ try {
|
|
|
119
126
|
/2 problems \(0 errors, 2 warnings\)/.test(l.text),
|
|
120
127
|
);
|
|
121
128
|
|
|
129
|
+
check(
|
|
130
|
+
"🔴 the layout is there: the five folders every paper uses, `repro/` with an index, tools/ and data/ — and none of the optional ones",
|
|
131
|
+
["reviews", "research", "figures", "versions", "submission"].every((d) =>
|
|
132
|
+
existsSync(join(papers, "demo", d, ".gitkeep")),
|
|
133
|
+
) &&
|
|
134
|
+
existsSync(join(papers, "demo", "repro", "README.md")) &&
|
|
135
|
+
existsSync(join(papers, "demo", "repro", "tools", ".gitkeep")) &&
|
|
136
|
+
existsSync(join(papers, "demo", "repro", "data", ".gitkeep")) &&
|
|
137
|
+
!existsSync(join(papers, "demo", "repro", ".gitkeep")) &&
|
|
138
|
+
["siblings", "sections", "talk", "extra", "_build"].every(
|
|
139
|
+
(d) => !existsSync(join(papers, "demo", d)),
|
|
140
|
+
),
|
|
141
|
+
);
|
|
142
|
+
|
|
122
143
|
// What the status hook reads on every paper edit: the verdict line.
|
|
123
144
|
check(
|
|
124
145
|
"the scorecard carries the `**Readiness verdict:**` line the status hook surfaces",
|
|
@@ -135,7 +156,8 @@ try {
|
|
|
135
156
|
again.ok &&
|
|
136
157
|
!again.fresh &&
|
|
137
158
|
read(papers, "demo", STATUS_FILE) === "mine\n" &&
|
|
138
|
-
again.files.every((f) => f.status === "kept")
|
|
159
|
+
again.files.every((f) => f.status === "kept") &&
|
|
160
|
+
again.layout.length === 0,
|
|
139
161
|
);
|
|
140
162
|
writeFileSync(join(papers, "afile"), "x");
|
|
141
163
|
check(
|
package/src/new-paper.test.ts
CHANGED
|
@@ -16,13 +16,28 @@ import { tmpdir } from "node:os";
|
|
|
16
16
|
import { join } from "node:path";
|
|
17
17
|
import { afterEach, describe, expect, it } from "vitest";
|
|
18
18
|
import { existsSync } from "node:fs";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
newPaper,
|
|
21
|
+
OVERRIDE_DIR,
|
|
22
|
+
PACKAGE_TEMPLATES,
|
|
23
|
+
reportNewPaper,
|
|
24
|
+
} from "./new-paper.ts";
|
|
20
25
|
import { parsePaperSettings } from "./paper-settings.ts";
|
|
21
26
|
import { chooseVenue, run } from "./cli.ts";
|
|
22
27
|
import { shippedPresets } from "./presets.ts";
|
|
23
28
|
import { presetsDir } from "./package-dirs.ts";
|
|
24
29
|
import { lintReport } from "../test/lint-report.ts";
|
|
25
30
|
import { z } from "zod";
|
|
31
|
+
import { judgeLayout } from "./domain/paper-layout.ts";
|
|
32
|
+
import {
|
|
33
|
+
GITKEEP,
|
|
34
|
+
NOT_SCAFFOLDED,
|
|
35
|
+
SCAFFOLD_DIRS,
|
|
36
|
+
SCAFFOLD_FILES,
|
|
37
|
+
} from "./domain/scaffold-layout.ts";
|
|
38
|
+
import { ROOT_DIRS } from "./domain/paper-layout.ts";
|
|
39
|
+
import { judgeRepro, REPRO_DIR } from "./domain/repro-layout.ts";
|
|
40
|
+
import { statSync } from "node:fs";
|
|
26
41
|
|
|
27
42
|
const dirs: string[] = [];
|
|
28
43
|
afterEach(() => {
|
|
@@ -395,11 +410,13 @@ describe("paperlint new --allow-venue-name", () => {
|
|
|
395
410
|
const r = await newIn(["--allow-venue-name"], {}, "realm-of-agents");
|
|
396
411
|
expect(r.code).toBe(0);
|
|
397
412
|
expect(r.out).not.toMatch(/venue really belongs|names a venue/);
|
|
398
|
-
expect(readdirSync(r.dir)
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
413
|
+
expect(readdirSync(r.dir)).toEqual(
|
|
414
|
+
expect.arrayContaining([
|
|
415
|
+
"PIPELINE-STATUS.md",
|
|
416
|
+
"paper.tex",
|
|
417
|
+
"paperlint.json",
|
|
418
|
+
]),
|
|
419
|
+
);
|
|
403
420
|
});
|
|
404
421
|
|
|
405
422
|
it.each<{
|
|
@@ -519,3 +536,279 @@ describe("a project's own template", () => {
|
|
|
519
536
|
).toBeGreaterThan(0);
|
|
520
537
|
});
|
|
521
538
|
});
|
|
539
|
+
|
|
540
|
+
describe("the PIPELINE-STATUS template — only stage tables are headed `id`", () => {
|
|
541
|
+
// A consumer's stage-table parser reads every table whose first column is `id` as stage rows, and a
|
|
542
|
+
// row under a heading that names no stage section is an error there: a fix-list table headed `id`
|
|
543
|
+
// gave a real paper two such errors (2026-10-12). Every other table starts with `#` or a word.
|
|
544
|
+
const SECTIONS = ["SETUP", "LOOP", "CONTINUOUS", "GATES", "AFTER"];
|
|
545
|
+
const text = readFileSync(
|
|
546
|
+
join(PACKAGE_TEMPLATES, "PIPELINE-STATUS.md"),
|
|
547
|
+
"utf8",
|
|
548
|
+
);
|
|
549
|
+
const idTablesUnder = text
|
|
550
|
+
.split("\n")
|
|
551
|
+
.reduce<{ heading: string; found: readonly string[] }>(
|
|
552
|
+
(acc, line) =>
|
|
553
|
+
line.startsWith("#")
|
|
554
|
+
? { ...acc, heading: line.replace(/^#+\s*/, "") }
|
|
555
|
+
: /^\|\s*id\s*\|/.test(line)
|
|
556
|
+
? { ...acc, found: [...acc.found, acc.heading] }
|
|
557
|
+
: acc,
|
|
558
|
+
{ heading: "", found: [] },
|
|
559
|
+
).found;
|
|
560
|
+
|
|
561
|
+
it("has stage tables at all", () => {
|
|
562
|
+
expect(idTablesUnder.length).toBeGreaterThanOrEqual(4);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it("puts each one under a stage section's heading", () => {
|
|
566
|
+
expect(
|
|
567
|
+
idTablesUnder.filter((h) => !SECTIONS.includes(h.split(/\s/)[0] ?? "")),
|
|
568
|
+
).toEqual([]);
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
/** Every file under `dir`, as a path from it, sorted. */
|
|
573
|
+
const filesUnder = (dir: string, at = ""): string[] =>
|
|
574
|
+
readdirSync(join(dir, at), { withFileTypes: true })
|
|
575
|
+
.flatMap((e) =>
|
|
576
|
+
e.isDirectory() ? filesUnder(dir, `${at}${e.name}/`) : [`${at}${e.name}`],
|
|
577
|
+
)
|
|
578
|
+
.sort();
|
|
579
|
+
|
|
580
|
+
describe("paperlint new — the layout is scaffolded", () => {
|
|
581
|
+
const EXPECTED = [
|
|
582
|
+
"PIPELINE-STATUS.md",
|
|
583
|
+
"figures/.gitkeep",
|
|
584
|
+
"paper.tex",
|
|
585
|
+
"paperlint.json",
|
|
586
|
+
"repro/README.md",
|
|
587
|
+
"repro/data/.gitkeep",
|
|
588
|
+
"repro/tools/.gitkeep",
|
|
589
|
+
"research/.gitkeep",
|
|
590
|
+
"reviews/.gitkeep",
|
|
591
|
+
"submission/.gitkeep",
|
|
592
|
+
"versions/.gitkeep",
|
|
593
|
+
];
|
|
594
|
+
|
|
595
|
+
it("a new paper has exactly the expected tree", () => {
|
|
596
|
+
const papers = join(tmp(), "papers");
|
|
597
|
+
const r = newPaper(papers, "demo", { venues: [] });
|
|
598
|
+
expect(r.ok && r.layout).toEqual(SCAFFOLD_FILES.map((f) => f.path));
|
|
599
|
+
expect(filesUnder(join(papers, "demo"))).toEqual(EXPECTED);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it("derives from the layout constants: every root folder is scaffolded or excused, and an excuse names a folder the layout has", () => {
|
|
603
|
+
const excused = NOT_SCAFFOLDED.map((n) => n.name);
|
|
604
|
+
expect([...SCAFFOLD_DIRS, ...excused].sort()).toEqual(
|
|
605
|
+
[...ROOT_DIRS].sort(),
|
|
606
|
+
);
|
|
607
|
+
expect(excused.filter((n) => !ROOT_DIRS.includes(n))).toEqual([]);
|
|
608
|
+
expect(NOT_SCAFFOLDED.filter((n) => n.why.length < 20)).toEqual([]);
|
|
609
|
+
// The optional folders are decided, not forgotten: these four are not made.
|
|
610
|
+
expect(excused).toEqual(
|
|
611
|
+
expect.arrayContaining(["siblings", "sections", "talk", "extra"]),
|
|
612
|
+
);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it(".gitkeep stands in an empty folder only, and no folder is empty but for it", () => {
|
|
616
|
+
const papers = join(tmp(), "papers");
|
|
617
|
+
newPaper(papers, "demo", { venues: [] });
|
|
618
|
+
const dir = join(papers, "demo");
|
|
619
|
+
const folders = new Set(
|
|
620
|
+
filesUnder(dir)
|
|
621
|
+
.filter((p) => p.includes("/"))
|
|
622
|
+
.map((p) => p.slice(0, p.lastIndexOf("/"))),
|
|
623
|
+
);
|
|
624
|
+
for (const f of folders) {
|
|
625
|
+
const names = readdirSync(join(dir, f)).filter((n) =>
|
|
626
|
+
statSync(join(dir, f, n)).isFile(),
|
|
627
|
+
);
|
|
628
|
+
const subdirs = readdirSync(join(dir, f)).length - names.length;
|
|
629
|
+
// A folder holds a .gitkeep alone, or real entries and no .gitkeep.
|
|
630
|
+
expect(
|
|
631
|
+
names.includes(GITKEEP) ? names.length === 1 && subdirs === 0 : true,
|
|
632
|
+
f,
|
|
633
|
+
).toBe(true);
|
|
634
|
+
}
|
|
635
|
+
// repro/ holds its index and folders, so it has no placeholder of its own.
|
|
636
|
+
expect(existsSync(join(dir, REPRO_DIR, GITKEEP))).toBe(false);
|
|
637
|
+
expect(existsSync(join(dir, GITKEEP))).toBe(false);
|
|
638
|
+
});
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
describe("paperlint new — what the scaffold writes fits the layout", () => {
|
|
642
|
+
it("the folders it makes are the layout's: nothing it writes is a stray, at the root or in repro/", () => {
|
|
643
|
+
const papers = join(tmp(), "papers");
|
|
644
|
+
newPaper(papers, "demo", { venues: [] });
|
|
645
|
+
const dir = join(papers, "demo");
|
|
646
|
+
const entries = (d: string) =>
|
|
647
|
+
readdirSync(d, { withFileTypes: true }).map((e) => ({
|
|
648
|
+
name: e.name,
|
|
649
|
+
kind: e.isDirectory() ? ("dir" as const) : ("file" as const),
|
|
650
|
+
}));
|
|
651
|
+
expect(judgeLayout(entries(dir), { roots: [] })).toEqual([]);
|
|
652
|
+
expect(
|
|
653
|
+
judgeRepro(
|
|
654
|
+
entries(join(dir, REPRO_DIR)).map((e) => ({
|
|
655
|
+
...e,
|
|
656
|
+
hasReadme: false,
|
|
657
|
+
})),
|
|
658
|
+
),
|
|
659
|
+
).toEqual([]);
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
it("repro/README.md is a stub index: one line saying one line per study", () => {
|
|
663
|
+
const papers = join(tmp(), "papers");
|
|
664
|
+
newPaper(papers, "demo", { venues: [] });
|
|
665
|
+
expect(
|
|
666
|
+
readFileSync(join(papers, "demo", "repro", "README.md"), "utf8"),
|
|
667
|
+
).toMatch(/One line per study/);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
it("the report names the folders in one line", () => {
|
|
671
|
+
const papers = join(tmp(), "papers");
|
|
672
|
+
const lines = reportNewPaper(
|
|
673
|
+
newPaper(papers, "demo", { venues: [] }),
|
|
674
|
+
(p) => p,
|
|
675
|
+
);
|
|
676
|
+
expect(lines.filter((l) => l.includes(GITKEEP))).toEqual([
|
|
677
|
+
" + versions/ reviews/ research/ repro/ repro/tools/ repro/data/ figures/ submission/ (the paper's layout; an empty folder holds a .gitkeep, so git keeps it)",
|
|
678
|
+
]);
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
describe("paperlint new — the scaffold on a folder that is not new", () => {
|
|
683
|
+
it("a second run changes nothing: all kept, no layout, same tree", () => {
|
|
684
|
+
const papers = join(tmp(), "papers");
|
|
685
|
+
newPaper(papers, "demo", { venues: [] });
|
|
686
|
+
const before = filesUnder(join(papers, "demo"));
|
|
687
|
+
const again = newPaper(papers, "demo", { venues: [] });
|
|
688
|
+
expect(again.ok && again.layout).toEqual([]);
|
|
689
|
+
expect(again.ok && again.files.every((f) => f.status === "kept")).toBe(
|
|
690
|
+
true,
|
|
691
|
+
);
|
|
692
|
+
expect(filesUnder(join(papers, "demo"))).toEqual(before);
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it("a folder that holds something gets only its missing three files, no layout", () => {
|
|
696
|
+
const papers = join(tmp(), "papers");
|
|
697
|
+
mkdirSync(join(papers, "old"), { recursive: true });
|
|
698
|
+
writeFileSync(join(papers, "old", "paper.tex"), "mine\n");
|
|
699
|
+
const r = newPaper(papers, "old", { venues: [] });
|
|
700
|
+
expect(r.ok && r.layout).toEqual([]);
|
|
701
|
+
expect(filesUnder(join(papers, "old"))).toEqual([
|
|
702
|
+
"PIPELINE-STATUS.md",
|
|
703
|
+
"paper.tex",
|
|
704
|
+
"paperlint.json",
|
|
705
|
+
]);
|
|
706
|
+
expect(readFileSync(join(papers, "old", "paper.tex"), "utf8")).toBe(
|
|
707
|
+
"mine\n",
|
|
708
|
+
);
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
it("an empty folder made by hand is new: it gets the layout", () => {
|
|
712
|
+
const papers = join(tmp(), "papers");
|
|
713
|
+
mkdirSync(join(papers, "made"), { recursive: true });
|
|
714
|
+
const r = newPaper(papers, "made", { venues: [] });
|
|
715
|
+
expect(r.ok && !r.fresh).toBe(true);
|
|
716
|
+
expect(filesUnder(join(papers, "made"))).toContain("reviews/.gitkeep");
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it("🔴 a folder the author emptied of its placeholder is not given it back, and a file in one stays", () => {
|
|
720
|
+
const papers = join(tmp(), "papers");
|
|
721
|
+
newPaper(papers, "demo", { venues: [] });
|
|
722
|
+
const dir = join(papers, "demo");
|
|
723
|
+
rmSync(join(dir, "reviews", GITKEEP));
|
|
724
|
+
writeFileSync(join(dir, "reviews", "2026-10-13-pass.md"), "x\n");
|
|
725
|
+
rmSync(join(dir, "figures"), { recursive: true });
|
|
726
|
+
newPaper(papers, "demo", { venues: [] });
|
|
727
|
+
expect(readdirSync(join(dir, "reviews"))).toEqual(["2026-10-13-pass.md"]);
|
|
728
|
+
expect(existsSync(join(dir, "figures"))).toBe(false);
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
it("an existing repro/README.md is never overwritten", () => {
|
|
732
|
+
const papers = join(tmp(), "papers");
|
|
733
|
+
mkdirSync(join(papers, "p", "repro"), { recursive: true });
|
|
734
|
+
writeFileSync(join(papers, "p", "repro", "README.md"), "mine\n");
|
|
735
|
+
newPaper(papers, "p", { venues: [] });
|
|
736
|
+
expect(readFileSync(join(papers, "p", "repro", "README.md"), "utf8")).toBe(
|
|
737
|
+
"mine\n",
|
|
738
|
+
);
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
/** A consumer project with one scaffolded paper and `added` files in it, linted: the rule ids found. */
|
|
743
|
+
async function lintScaffold(added: Record<string, string> = {}) {
|
|
744
|
+
const root = tmp();
|
|
745
|
+
writeFileSync(
|
|
746
|
+
join(root, "package.json"),
|
|
747
|
+
JSON.stringify({ name: "c", private: true }),
|
|
748
|
+
);
|
|
749
|
+
newPaper(join(root, "papers"), "demo", { venues: [] });
|
|
750
|
+
for (const [p, text] of Object.entries(added)) {
|
|
751
|
+
mkdirSync(join(root, "papers", "demo", p, ".."), { recursive: true });
|
|
752
|
+
writeFileSync(join(root, "papers", "demo", p), text);
|
|
753
|
+
}
|
|
754
|
+
const out: string[] = [];
|
|
755
|
+
const code = await run(["lint", "--json"], {
|
|
756
|
+
cwd: root,
|
|
757
|
+
log: (s: string) => out.push(s),
|
|
758
|
+
err: () => {},
|
|
759
|
+
});
|
|
760
|
+
return {
|
|
761
|
+
code,
|
|
762
|
+
ids: lintReport(out.join("\n")).flatMap((r) =>
|
|
763
|
+
r.messages.map((m) => m.ruleId),
|
|
764
|
+
),
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
describe("paperlint lint — a scaffolded paper has no layout finding", () => {
|
|
769
|
+
const LAYOUT_RULES = [
|
|
770
|
+
"paper/allowed-files-only",
|
|
771
|
+
"repro/allowed-files-only",
|
|
772
|
+
"review/frontmatter",
|
|
773
|
+
"sibling/frontmatter",
|
|
774
|
+
"paper/stages",
|
|
775
|
+
"study/declare-sample",
|
|
776
|
+
];
|
|
777
|
+
|
|
778
|
+
it("none of the rules that read the layout's folders reports it; only the not-built warnings remain", async () => {
|
|
779
|
+
const { code, ids } = await lintScaffold();
|
|
780
|
+
expect(ids.filter((id) => LAYOUT_RULES.includes(id ?? ""))).toEqual([]);
|
|
781
|
+
expect(ids).toEqual(["pdf/measured", "paper/sources-fresh"]);
|
|
782
|
+
expect(code).toBe(0);
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
it("🔴 a .gitkeep beside real entries, in any folder the rules read, is not reported either", async () => {
|
|
786
|
+
const { ids } = await lintScaffold({
|
|
787
|
+
"siblings/.gitkeep": "",
|
|
788
|
+
"talk/.gitkeep": "",
|
|
789
|
+
"sections/.gitkeep": "",
|
|
790
|
+
"repro/some-study-2026-10-13/README.md": "# s\n",
|
|
791
|
+
"repro/some-study-2026-10-13/.gitkeep": "",
|
|
792
|
+
});
|
|
793
|
+
expect(ids.filter((id) => LAYOUT_RULES.includes(id ?? ""))).toEqual([]);
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
describe("paperlint lint — extra/ is never read", () => {
|
|
798
|
+
// An archived snapshot of an older paper carries every marker a paper does. Under extra/ it is
|
|
799
|
+
// spillover the layout promises never to judge; anywhere else its folder reads as a paper root.
|
|
800
|
+
const SNAPSHOT = (dir: string) => ({
|
|
801
|
+
[`${dir}/PIPELINE-STATUS.md`]: "# Pipeline status\n",
|
|
802
|
+
[`${dir}/notes.txt`]: "x\n",
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
it("🔴 an archived paper under extra/ is never linted, by any rule", async () => {
|
|
806
|
+
const { ids } = await lintScaffold(SNAPSHOT("extra/old-2025"));
|
|
807
|
+
expect(ids).toEqual(["pdf/measured", "paper/sources-fresh"]);
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
it("the same snapshot outside extra/ is reported, so the test above can fail", async () => {
|
|
811
|
+
const { ids } = await lintScaffold(SNAPSHOT("research/old-2025"));
|
|
812
|
+
expect(ids).toContain("paper/allowed-files-only");
|
|
813
|
+
});
|
|
814
|
+
});
|
package/src/new-paper.ts
CHANGED
|
@@ -12,20 +12,34 @@
|
|
|
12
12
|
* - the name is validated, because it becomes a path and, through the hooks, a shell argument.
|
|
13
13
|
*
|
|
14
14
|
* The only substitution is `{{name}}` → the folder name. Nothing else in a template is touched.
|
|
15
|
+
*
|
|
16
|
+
* Beside the three files, a NEW paper folder gets the layout the rules enforce (`SCAFFOLD_FILES`,
|
|
17
|
+
* src/domain/scaffold-layout.ts): its folders, each with a `.gitkeep` or a first file, so the layout
|
|
18
|
+
* is visible from the first commit. That is for a folder that is absent or empty. On a folder that
|
|
19
|
+
* holds anything, `new` adds only the missing three files, as before: a paper that already has its
|
|
20
|
+
* own shape is not given folders it did not choose, and one whose author deleted an empty folder
|
|
21
|
+
* does not get it back.
|
|
15
22
|
*/
|
|
16
23
|
/* eslint-disable boundaries/dependencies -- legacy I/O, moves behind a port in #76 */
|
|
17
24
|
import {
|
|
18
25
|
existsSync,
|
|
19
26
|
mkdirSync,
|
|
27
|
+
readdirSync,
|
|
20
28
|
readFileSync,
|
|
21
29
|
statSync,
|
|
22
30
|
writeFileSync,
|
|
23
31
|
} from "node:fs";
|
|
24
32
|
/* eslint-enable boundaries/dependencies */
|
|
25
|
-
import { join } from "node:path";
|
|
33
|
+
import { dirname, join } from "node:path";
|
|
26
34
|
import { fileURLToPath } from "node:url";
|
|
27
35
|
import { CONFIG_FILE } from "#lib/paper-config";
|
|
28
36
|
import { isRecord } from "./domain/record.ts";
|
|
37
|
+
import { ESCAPE_HATCH, VENUES_DIR } from "./domain/paper-layout.ts";
|
|
38
|
+
import {
|
|
39
|
+
GITKEEP,
|
|
40
|
+
SCAFFOLD_FILES,
|
|
41
|
+
type ScaffoldFile,
|
|
42
|
+
} from "./domain/scaffold-layout.ts";
|
|
29
43
|
import {
|
|
30
44
|
venuesNamedBy,
|
|
31
45
|
withoutVenues,
|
|
@@ -51,10 +65,14 @@ export const SOURCE_FILE = "paper.tex";
|
|
|
51
65
|
/** The flag that lets `paperlint new` create a folder whose name names a venue. */
|
|
52
66
|
export const ALLOW_VENUE_NAME = "--allow-venue-name";
|
|
53
67
|
|
|
68
|
+
/** Names the papers directory's layout already gives a meaning to (src/domain/paper-layout.ts). */
|
|
69
|
+
const RESERVED_NAMES: readonly string[] = [VENUES_DIR, ESCAPE_HATCH];
|
|
70
|
+
|
|
54
71
|
/**
|
|
55
72
|
* Why a name is refused, or null. `[a-z0-9._-]+` is the charset; a LEADING dot is refused on top
|
|
56
73
|
* of it, because discovery skips dot-directories — a paper named `.x` would be created and then
|
|
57
|
-
* never linted, which is worse than a refusal.
|
|
74
|
+
* never linted, which is worse than a refusal. `venues` and `extra` are refused for the same reason:
|
|
75
|
+
* the first is the venues' corpus beside the papers, the second is never linted.
|
|
58
76
|
*
|
|
59
77
|
* A name that names a venue — one of `venues`' names as whole words (`aisec-2026`) — is refused
|
|
60
78
|
* too, the paper's own venue included: a paper that is rejected goes to another venue and keeps its
|
|
@@ -71,6 +89,8 @@ export function nameProblem(
|
|
|
71
89
|
return `\`${name}\` — a paper name may hold only a-z, 0-9, dot, underscore and hyphen (it becomes a path and a shell argument)`;
|
|
72
90
|
if (name.startsWith("."))
|
|
73
91
|
return `\`${name}\` — a name starting with a dot is skipped by paper discovery, so it would never be linted`;
|
|
92
|
+
if (RESERVED_NAMES.includes(name))
|
|
93
|
+
return `\`${name}\` — reserved: \`${VENUES_DIR}/\` beside the papers holds the venues' corpora, and lint never reads a folder named \`${ESCAPE_HATCH}/\`, so a paper by either name would be taken for something else or never linted`;
|
|
74
94
|
const named = venuesNamedBy(name, venues);
|
|
75
95
|
if (named.length === 0) return null;
|
|
76
96
|
const which = named.map((n) => `"${n.name}"`).join(", ");
|
|
@@ -96,6 +116,8 @@ export type NewPaperResult =
|
|
|
96
116
|
/** The folder did not exist before this run. */
|
|
97
117
|
readonly fresh: boolean;
|
|
98
118
|
readonly files: readonly FileOutcome[];
|
|
119
|
+
/** The layout files written into a folder that was absent or empty, relative to it; none otherwise. */
|
|
120
|
+
readonly layout: readonly string[];
|
|
99
121
|
}
|
|
100
122
|
| { readonly ok: false; readonly reason: string };
|
|
101
123
|
|
|
@@ -195,7 +217,19 @@ export interface NewPaperOptions {
|
|
|
195
217
|
readonly venues: readonly NamedVenue[];
|
|
196
218
|
}
|
|
197
219
|
|
|
198
|
-
|
|
220
|
+
/** The layout files `dir` is given: all of them when it holds nothing (absent, or made by hand and still empty), else none. */
|
|
221
|
+
const layoutFor = (dir: string, fresh: boolean): readonly ScaffoldFile[] =>
|
|
222
|
+
fresh || readdirSync(dir).length === 0 ? SCAFFOLD_FILES : [];
|
|
223
|
+
|
|
224
|
+
/** Writes the layout files under `dir`; `wx` fails rather than overwrite, as for the three files. */
|
|
225
|
+
const writeLayout = (dir: string, layout: readonly ScaffoldFile[]): void => {
|
|
226
|
+
layout.forEach(({ path, text }) => {
|
|
227
|
+
mkdirSync(dirname(join(dir, path)), { recursive: true });
|
|
228
|
+
writeFileSync(join(dir, path), text, { encoding: "utf8", flag: "wx" });
|
|
229
|
+
});
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// Documented in README.md#-getting-started — update it when this changes.
|
|
199
233
|
export function newPaper(
|
|
200
234
|
papersRoot: string,
|
|
201
235
|
name: string,
|
|
@@ -225,6 +259,7 @@ export function newPaper(
|
|
|
225
259
|
if (!planned.ok) return planned;
|
|
226
260
|
toWrite.push(planned.value);
|
|
227
261
|
}
|
|
262
|
+
const scaffold = layoutFor(dir, fresh);
|
|
228
263
|
// Read every template before writing any file, so a missing one leaves no half-made folder.
|
|
229
264
|
mkdirSync(dir, { recursive: true });
|
|
230
265
|
for (const { file, text, from } of toWrite) {
|
|
@@ -232,9 +267,15 @@ export function newPaper(
|
|
|
232
267
|
writeFileSync(join(dir, file), text, { encoding: "utf8", flag: "wx" });
|
|
233
268
|
files.push({ file, status: "created", from });
|
|
234
269
|
}
|
|
235
|
-
|
|
270
|
+
writeLayout(dir, scaffold);
|
|
271
|
+
return { ok: true, dir, fresh, files, layout: scaffold.map((f) => f.path) };
|
|
236
272
|
}
|
|
237
273
|
|
|
274
|
+
/** The folders the layout files sit in, each once, in order: `reviews/`, `repro/tools/`, … */
|
|
275
|
+
const layoutFolders = (paths: readonly string[]): readonly string[] => [
|
|
276
|
+
...new Set(paths.map((p) => `${dirname(p)}/`)),
|
|
277
|
+
];
|
|
278
|
+
|
|
238
279
|
/** The report lines, shared by `paperlint new` and the first-paper offer in `paperlint init`. */
|
|
239
280
|
export function reportNewPaper(
|
|
240
281
|
result: NewPaperResult,
|
|
@@ -250,5 +291,10 @@ export function reportNewPaper(
|
|
|
250
291
|
? ` + ${f.file} (from the ${f.from === "project" ? `project's ${OVERRIDE_DIR}/` : "package"} template)`
|
|
251
292
|
: ` = ${f.file} (kept — never overwritten)`,
|
|
252
293
|
);
|
|
253
|
-
return
|
|
294
|
+
return result.layout.length === 0
|
|
295
|
+
? out
|
|
296
|
+
: [
|
|
297
|
+
...out,
|
|
298
|
+
` + ${layoutFolders(result.layout).join(" ")} (the paper's layout; an empty folder holds a ${GITKEEP}, so git keeps it)`,
|
|
299
|
+
];
|
|
254
300
|
}
|
package/src/paper-files.test.ts
CHANGED
|
@@ -75,12 +75,15 @@ async function lint(
|
|
|
75
75
|
const linted = (stdout: string): string[] =>
|
|
76
76
|
lintReport(stdout).map((r) => r.filePath);
|
|
77
77
|
|
|
78
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* Vendored code under a paper: a syntax error, an unknown rule in a directive, plain CommonJS — in
|
|
80
|
+
* `repro/tools/`, where `repro/allowed-files-only` puts a paper's scripts.
|
|
81
|
+
*/
|
|
79
82
|
const VENDORED = {
|
|
80
|
-
"papers/a/repro/x.js": "return 1;\n",
|
|
81
|
-
"papers/a/repro/y.mjs":
|
|
83
|
+
"papers/a/repro/tools/x.js": "return 1;\n",
|
|
84
|
+
"papers/a/repro/tools/y.mjs":
|
|
82
85
|
"// eslint-disable-next-line import/no-nodejs-modules\nimport fs from 'node:fs';\n",
|
|
83
|
-
"papers/a/repro/lib/z.cjs": "module.exports = 1;\n",
|
|
86
|
+
"papers/a/repro/tools/lib/z.cjs": "module.exports = 1;\n",
|
|
84
87
|
};
|
|
85
88
|
|
|
86
89
|
describe("paperlint lint — lints only the files paperlint owns", () => {
|
|
@@ -125,10 +128,10 @@ describe("paperlint lint — lints only the files paperlint owns", () => {
|
|
|
125
128
|
|
|
126
129
|
it("a file named on the command line that paperlint does not lint is refused by name", async () => {
|
|
127
130
|
const root = project(VENDORED);
|
|
128
|
-
const r = await lint(root, ["papers/a/repro/x.js"]);
|
|
131
|
+
const r = await lint(root, ["papers/a/repro/tools/x.js"]);
|
|
129
132
|
expect(r.code).toBe(2);
|
|
130
133
|
expect(r.err).toMatch(
|
|
131
|
-
/papers\/a\/repro\/x\.js is not a file paperlint lints/,
|
|
134
|
+
/papers\/a\/repro\/tools\/x\.js is not a file paperlint lints/,
|
|
132
135
|
);
|
|
133
136
|
expect(r.err).toMatch(/PIPELINE-STATUS\.md/);
|
|
134
137
|
});
|
package/src/references.test.ts
CHANGED
|
@@ -303,13 +303,13 @@ const cacheText = (files: Files): string | null => {
|
|
|
303
303
|
return b === null ? null : new TextDecoder().decode(b);
|
|
304
304
|
};
|
|
305
305
|
|
|
306
|
-
describe("the lookup cache — <paper>/repro/references-cache.json", () => {
|
|
306
|
+
describe("the lookup cache — <paper>/repro/data/references-cache.json", () => {
|
|
307
307
|
it("a build that fetched writes the cache; the next build is handed it and writes nothing", async () => {
|
|
308
308
|
const f = fetching("crossref:doi:10.1/x");
|
|
309
309
|
const { files, out } = await build(ordinary(), f.check);
|
|
310
310
|
expect(out).toEqual({
|
|
311
311
|
ok: true,
|
|
312
|
-
note: "references: 1 checked, 0 failing → _build/references.json; 1 new answer → repro/references-cache.json",
|
|
312
|
+
note: "references: 1 checked, 0 failing → _build/references.json; 1 new answer → repro/data/references-cache.json",
|
|
313
313
|
});
|
|
314
314
|
const written = cacheText(files);
|
|
315
315
|
expect(written).toBe(
|
|
@@ -350,7 +350,7 @@ describe("the lookup cache — counting", () => {
|
|
|
350
350
|
const { out } = await build(ordinary(), two);
|
|
351
351
|
expect(out).toEqual({
|
|
352
352
|
ok: true,
|
|
353
|
-
note: "references: 1 checked, 0 failing → _build/references.json; 2 new answers → repro/references-cache.json",
|
|
353
|
+
note: "references: 1 checked, 0 failing → _build/references.json; 2 new answers → repro/data/references-cache.json",
|
|
354
354
|
});
|
|
355
355
|
});
|
|
356
356
|
|
|
@@ -381,7 +381,7 @@ describe("the lookup cache — counting", () => {
|
|
|
381
381
|
expect({ again, file: cacheText(files) }).toEqual({
|
|
382
382
|
again: {
|
|
383
383
|
ok: true,
|
|
384
|
-
note: "references: 1 checked, 0 failing → _build/references.json; 1 new answer → repro/references-cache.json",
|
|
384
|
+
note: "references: 1 checked, 0 failing → _build/references.json; 1 new answer → repro/data/references-cache.json",
|
|
385
385
|
},
|
|
386
386
|
file: serializeLookupCache({
|
|
387
387
|
citations: new Map([["a", ANSWER]]),
|
|
@@ -402,7 +402,7 @@ describe("the lookup cache — refusing", () => {
|
|
|
402
402
|
});
|
|
403
403
|
expect(out).toEqual({
|
|
404
404
|
ok: true,
|
|
405
|
-
note: "references NOT checked — repro/references-cache.json cannot be read (schema 7: this paperlint reads schema 1) — fix it, or delete it to ask every question again; lint will say so",
|
|
405
|
+
note: "references NOT checked — repro/data/references-cache.json cannot be read (schema 7: this paperlint reads schema 1) — fix it, or delete it to ask every question again; lint will say so",
|
|
406
406
|
});
|
|
407
407
|
expect(f.seen).toEqual([]);
|
|
408
408
|
expect(cacheText(files)).toBe('{"schema": 7}');
|
package/src/references.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* stale when it changes (`pdf_sha256`); these are about the databases and are stale when THEY
|
|
20
20
|
* change (`bib.sha256`). One staleness key per file, the way `pdf/fresh` already works.
|
|
21
21
|
*
|
|
22
|
-
* 🔴 WHAT THE CHECKERS ANSWERED IS KEPT, COMMITTED, IN `<paper>/repro/references-cache.json`
|
|
22
|
+
* 🔴 WHAT THE CHECKERS ANSWERED IS KEPT, COMMITTED, IN `<paper>/repro/data/references-cache.json`
|
|
23
23
|
* (`domain/lookup-cache.ts`). Loaded before the check and saved after it — only when something new
|
|
24
24
|
* was fetched, a refreshed answer included — so a build asks each question at most once every
|
|
25
25
|
* MAX_AGE_DAYS, not once per build (#107: 217 s for 27 references, every build). The verdicts above are NOT cached: they are derived from the cached
|
|
@@ -77,7 +77,7 @@ export const REFERENCES_SCHEMA = 3;
|
|
|
77
77
|
export const REFERENCES_FILE = "references.json";
|
|
78
78
|
|
|
79
79
|
/** The lookup cache's place in a paper: committed, beside the paper's other reproduction files. */
|
|
80
|
-
export const LOOKUP_CACHE_FILE = "repro/references-cache.json";
|
|
80
|
+
export const LOOKUP_CACHE_FILE = "repro/data/references-cache.json";
|
|
81
81
|
|
|
82
82
|
export const lookupCachePath = (paperDir: string): string =>
|
|
83
83
|
join(paperDir, LOOKUP_CACHE_FILE);
|