paperlint 9.0.1 → 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/README.md +68 -128
- package/dist/build.d.ts +1 -1
- package/dist/build.js +1 -1
- 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/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/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/docs/ci.md +60 -0
- package/docs/configuration.md +2 -0
- package/docs/e2e.md +2 -2
- 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/prereg-coverage.test.ts +448 -0
- package/eslint-rules/prereg-coverage.ts +419 -0
- package/lib/skill-trigger-cases.harness.mjs +51 -5
- package/lib/skill-trigger-cases.mjs +53 -15
- package/package.json +1 -1
- package/plugin/hooks/hooks.json +9 -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/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/build.ts +1 -1
- 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/templates/paper/PIPELINE-STATUS.md +41 -7
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The paper folder's layout (`src/domain/paper-layout.ts`), judged on its root entries alone. Both
|
|
3
|
+
* halves: the scaffold `paperlint new` writes, and a folder holding every allowed entry, are clean; a
|
|
4
|
+
* stray file or folder at the root is reported once, by name, with where it belongs.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
import {
|
|
8
|
+
CURE_BY_KIND,
|
|
9
|
+
KNOWN_CURES,
|
|
10
|
+
NOTHING_DECLARED,
|
|
11
|
+
PAPER_LAYOUT_TREE,
|
|
12
|
+
ROOT_DIRS,
|
|
13
|
+
ROOT_FILES,
|
|
14
|
+
judgeLayout,
|
|
15
|
+
strayMessage,
|
|
16
|
+
type LayoutEntry,
|
|
17
|
+
} from "./paper-layout.ts";
|
|
18
|
+
|
|
19
|
+
const file = (name: string): LayoutEntry => ({ name, kind: "file" });
|
|
20
|
+
const dir = (name: string): LayoutEntry => ({ name, kind: "dir" });
|
|
21
|
+
|
|
22
|
+
describe("judgeLayout — silent", () => {
|
|
23
|
+
it("on the scaffold `paperlint new` writes", () => {
|
|
24
|
+
expect(
|
|
25
|
+
judgeLayout(
|
|
26
|
+
[file("PIPELINE-STATUS.md"), file("paper.tex"), file("paperlint.json")],
|
|
27
|
+
NOTHING_DECLARED,
|
|
28
|
+
),
|
|
29
|
+
).toEqual([]);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("judgeLayout — silent on the whole allowed set", () => {
|
|
34
|
+
it("on every allowed file and folder, the TeX inputs and the build's byproducts", () => {
|
|
35
|
+
const entries = [
|
|
36
|
+
...[
|
|
37
|
+
"PIPELINE-STATUS.md",
|
|
38
|
+
"paper.tex",
|
|
39
|
+
"paperlint.json",
|
|
40
|
+
"README.md",
|
|
41
|
+
"CLAUDE.md",
|
|
42
|
+
"CLAIMS.md",
|
|
43
|
+
"NOTES.md",
|
|
44
|
+
"SUBMIT-CHECKLIST.md",
|
|
45
|
+
".gitignore",
|
|
46
|
+
"paper.pdf",
|
|
47
|
+
"numbers.tex",
|
|
48
|
+
"refs.bib",
|
|
49
|
+
"acmart.cls",
|
|
50
|
+
"pbalance.sty",
|
|
51
|
+
"ACM-Reference-Format.bst",
|
|
52
|
+
"paper.aux",
|
|
53
|
+
"paper.log",
|
|
54
|
+
"paper.bbl",
|
|
55
|
+
"paper.blg",
|
|
56
|
+
"paper.out",
|
|
57
|
+
"paper.fls",
|
|
58
|
+
"paper.toc",
|
|
59
|
+
"paper.synctex.gz",
|
|
60
|
+
".DS_Store",
|
|
61
|
+
].map(file),
|
|
62
|
+
...[
|
|
63
|
+
"_build",
|
|
64
|
+
"versions",
|
|
65
|
+
"reviews",
|
|
66
|
+
"siblings",
|
|
67
|
+
"research",
|
|
68
|
+
"repro",
|
|
69
|
+
"figures",
|
|
70
|
+
"sections",
|
|
71
|
+
"talk",
|
|
72
|
+
"submission",
|
|
73
|
+
"extra",
|
|
74
|
+
].map(dir),
|
|
75
|
+
];
|
|
76
|
+
expect(judgeLayout(entries, NOTHING_DECLARED)).toEqual([]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("on what the paper's settings name at its root: another PDF, another talk folder, its own preset", () => {
|
|
80
|
+
expect(
|
|
81
|
+
judgeLayout([file("main.pdf"), dir("video"), file("venue.jsonc")], {
|
|
82
|
+
roots: ["main.pdf", "video", "venue.jsonc"],
|
|
83
|
+
}),
|
|
84
|
+
).toEqual([]);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("judgeLayout — reports", () => {
|
|
89
|
+
it("a known name with its one destination: the plan is a section of the status file", () => {
|
|
90
|
+
const [plan] = judgeLayout(
|
|
91
|
+
[file("paper.tex"), file("PLAN.md")],
|
|
92
|
+
NOTHING_DECLARED,
|
|
93
|
+
);
|
|
94
|
+
expect(plan?.name).toBe("PLAN.md");
|
|
95
|
+
expect(plan?.cure).toMatch(
|
|
96
|
+
/^a section of PIPELINE-STATUS\.md .*"Plan \/ now"/,
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it.each([
|
|
101
|
+
["DECISION-LOG.md", "file", /^research\//],
|
|
102
|
+
["EMAILS.md", "file", /^submission\//],
|
|
103
|
+
["data", "dir", /^repro\//],
|
|
104
|
+
["artifact", "dir", /^repro\//],
|
|
105
|
+
[
|
|
106
|
+
"archive",
|
|
107
|
+
"dir",
|
|
108
|
+
/^versions\/ for frozen PDFs, submission\/ .*, extra\/ for the rest$/,
|
|
109
|
+
],
|
|
110
|
+
[
|
|
111
|
+
"venue-corpus",
|
|
112
|
+
"dir",
|
|
113
|
+
/^<papers-root>\/venues\/<venue>\/, outside the paper folder/,
|
|
114
|
+
],
|
|
115
|
+
["plans", "dir", /^a section of PIPELINE-STATUS\.md/],
|
|
116
|
+
["Plan.md", "file", /^a section of PIPELINE-STATUS\.md/],
|
|
117
|
+
] as const)("%s (%s) goes to its destination", (name, kind, cure) => {
|
|
118
|
+
expect(judgeLayout([{ name, kind }], NOTHING_DECLARED)[0]?.cure).toMatch(
|
|
119
|
+
cure,
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("an unknown name gets the choice by what it is", () => {
|
|
124
|
+
expect(judgeLayout([dir("corpus")], NOTHING_DECLARED)).toEqual([
|
|
125
|
+
{ name: "corpus/", kind: "dir", cure: CURE_BY_KIND },
|
|
126
|
+
]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("a known file name used by a folder is not that file: the choice by kind", () => {
|
|
130
|
+
expect(judgeLayout([dir("PLAN.md")], NOTHING_DECLARED)[0]?.cure).toBe(
|
|
131
|
+
CURE_BY_KIND,
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("judgeLayout — the build's byproducts", () => {
|
|
137
|
+
// What pdflatex, bibtex, makeindex, biber, glossaries, beamer and latexmk write next to paper.tex.
|
|
138
|
+
const byproducts = [
|
|
139
|
+
"aux",
|
|
140
|
+
"log",
|
|
141
|
+
"out",
|
|
142
|
+
"toc",
|
|
143
|
+
"lof",
|
|
144
|
+
"lot",
|
|
145
|
+
"loa",
|
|
146
|
+
"lol",
|
|
147
|
+
"bbl",
|
|
148
|
+
"blg",
|
|
149
|
+
"idx",
|
|
150
|
+
"ind",
|
|
151
|
+
"ilg",
|
|
152
|
+
"glo",
|
|
153
|
+
"gls",
|
|
154
|
+
"glg",
|
|
155
|
+
"nlo",
|
|
156
|
+
"nls",
|
|
157
|
+
"fls",
|
|
158
|
+
"fdb_latexmk",
|
|
159
|
+
"synctex.gz",
|
|
160
|
+
"run.xml",
|
|
161
|
+
"bcf",
|
|
162
|
+
"nav",
|
|
163
|
+
"snm",
|
|
164
|
+
"vrb",
|
|
165
|
+
"xdv",
|
|
166
|
+
"dvi",
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
it("a root holding paper.idx, paper.ind, paper.ilg and the rest of the job's outputs reports nothing", () => {
|
|
170
|
+
expect(
|
|
171
|
+
judgeLayout(
|
|
172
|
+
byproducts.map((ext) => file(`paper.${ext}`)),
|
|
173
|
+
NOTHING_DECLARED,
|
|
174
|
+
),
|
|
175
|
+
).toEqual([]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("only under the paper's own job name: draft.idx is reported", () => {
|
|
179
|
+
expect(
|
|
180
|
+
judgeLayout([file("draft.idx"), file("paper.idx")], NOTHING_DECLARED).map(
|
|
181
|
+
(s) => s.name,
|
|
182
|
+
),
|
|
183
|
+
).toEqual(["draft.idx"]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("🔴 the .aux that \\include writes beside a root source: chapter.aux with chapter.tex is the build's; alone it is reported", () => {
|
|
187
|
+
expect(
|
|
188
|
+
judgeLayout(
|
|
189
|
+
[file("chapter.tex"), file("chapter.aux"), file("orphan.aux")],
|
|
190
|
+
NOTHING_DECLARED,
|
|
191
|
+
).map((s) => s.name),
|
|
192
|
+
).toEqual(["orphan.aux"]);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("judgeLayout — reports by name, exactly", () => {
|
|
197
|
+
it("a PDF other than the built one, a dotfile, another job's byproduct", () => {
|
|
198
|
+
expect(
|
|
199
|
+
judgeLayout(
|
|
200
|
+
[file("paper-2026-07-18.pdf"), file(".body-limit"), file("draft.aux")],
|
|
201
|
+
NOTHING_DECLARED,
|
|
202
|
+
).map((s) => s.name),
|
|
203
|
+
).toEqual(["paper-2026-07-18.pdf", ".body-limit", "draft.aux"]);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("an allowed folder's name used by a file, and an allowed file's name used by a folder", () => {
|
|
207
|
+
expect(
|
|
208
|
+
judgeLayout([file("reviews"), dir("README.md")], NOTHING_DECLARED).map(
|
|
209
|
+
(s) => s.name,
|
|
210
|
+
),
|
|
211
|
+
).toEqual(["reviews", "README.md/"]);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("names compared exactly: case matters", () => {
|
|
215
|
+
expect(
|
|
216
|
+
judgeLayout([dir("Reviews")], NOTHING_DECLARED).map((s) => s.name),
|
|
217
|
+
).toEqual(["Reviews/"]);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("each stray entry once, in the order given", () => {
|
|
221
|
+
expect(
|
|
222
|
+
judgeLayout(
|
|
223
|
+
[file("a.md"), file("paper.tex"), dir("data")],
|
|
224
|
+
NOTHING_DECLARED,
|
|
225
|
+
).map((s) => s.name),
|
|
226
|
+
).toEqual(["a.md", "data/"]);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("strayMessage — stands alone for a reader with no context", () => {
|
|
231
|
+
const [plan] = judgeLayout([file("PLAN.md")], NOTHING_DECLARED);
|
|
232
|
+
const text = plan === undefined ? "" : strayMessage(plan);
|
|
233
|
+
|
|
234
|
+
it("names the entry, where it goes, draws the whole layout, and links the rule's page", () => {
|
|
235
|
+
expect(text.split("\n")).toEqual([
|
|
236
|
+
`\`PLAN.md\` is at the root of a paper folder (the folder holding PIPELINE-STATUS.md), and that root holds only paperlint's layout. Where it goes: ${KNOWN_CURES[0]?.cure ?? ""}.`,
|
|
237
|
+
"The layout (extra/ takes anything and is never judged; the build's paper.* byproducts are never reported):",
|
|
238
|
+
...PAPER_LAYOUT_TREE.split("\n"),
|
|
239
|
+
"Rule paper/allowed-files-only: https://github.com/zernie/paperlint/blob/main/docs/rules/paper/allowed-files-only.md",
|
|
240
|
+
]);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("the layout it draws holds every allowed root entry, repro/'s own layout inside", () => {
|
|
244
|
+
const lines = PAPER_LAYOUT_TREE.split("\n");
|
|
245
|
+
expect(lines[0]).toBe("<paper>/");
|
|
246
|
+
const drawn = new Set(
|
|
247
|
+
lines.map((l) =>
|
|
248
|
+
l
|
|
249
|
+
.replace(/^[│ ]*[├└]── /u, "")
|
|
250
|
+
.split(" ", 1)
|
|
251
|
+
.join(""),
|
|
252
|
+
),
|
|
253
|
+
);
|
|
254
|
+
expect(
|
|
255
|
+
[...ROOT_FILES, ...ROOT_DIRS.map((d) => `${d}/`)].filter(
|
|
256
|
+
(name) => !drawn.has(name),
|
|
257
|
+
),
|
|
258
|
+
).toEqual([]);
|
|
259
|
+
expect(lines).toContain(
|
|
260
|
+
"│ └── <slug>-YYYY-MM-DD/ ← one folder per study or spike (<slug>-YYYY-MM for one that ran over weeks)",
|
|
261
|
+
);
|
|
262
|
+
expect(lines.at(-1)).toMatch(/^└── extra\/ +← anything; never judged$/);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE PAPER FOLDER'S LAYOUT — what may stand at the root of a paper folder, and nothing else.
|
|
3
|
+
*
|
|
4
|
+
* A paper folder collects files the way a desk does: plans, mails, three drafts of the PDF, a corpus,
|
|
5
|
+
* an old attempt's checklist. Each was put there for a reason, and after a few months nobody can say
|
|
6
|
+
* which file is current, which folder holds the evidence, or where a new thing goes. A strict root is
|
|
7
|
+
* the answer every build tool gives (Cargo's `src/`, a Maven layout): the root holds a small, known
|
|
8
|
+
* set, every known kind of material has one folder, and ONE escape hatch, `extra/`, takes the rest
|
|
9
|
+
* without judging it.
|
|
10
|
+
*
|
|
11
|
+
* ── WHERE THE SET COMES FROM ─────────────────────────────────────────────────────
|
|
12
|
+
* Each entry is a file or folder paperlint itself writes or reads, or one its skills name at the
|
|
13
|
+
* paper root (each carries its note below, which the drawing of the layout shows):
|
|
14
|
+
* - what `paperlint new` scaffolds: `PIPELINE-STATUS.md`, `paper.tex`, `paperlint.json`;
|
|
15
|
+
* - what the build writes: `paper.pdf` and `_build/`, and TeX's own byproducts beside `paper.tex`
|
|
16
|
+
* (pdflatex runs in the paper folder without `-output-directory`), which are never reported;
|
|
17
|
+
* - what the rules read: `versions/` (`paper/stages`), `reviews/` (`review/frontmatter`),
|
|
18
|
+
* `siblings/` (`sibling/frontmatter`), `talk/` (the `talk/` rules' default folder);
|
|
19
|
+
* - what the skills read or write at the root: `README.md`, `CLAUDE.md`, `CLAIMS.md`, `NOTES.md`,
|
|
20
|
+
* `SUBMIT-CHECKLIST.md`, `research/`, `repro/`, `figures/`, `submission/`;
|
|
21
|
+
* - where the paper's own body lives when it is split: `sections/` (README's `\input{sections/intro}`);
|
|
22
|
+
* - the files TeX and bibtex look up in the folder they run in: `*.tex`, `*.bib`, `*.cls`, `*.sty`,
|
|
23
|
+
* `*.bst`.
|
|
24
|
+
* A venue's corpus of accepted papers is NOT here: it is shared by every paper for that venue and
|
|
25
|
+
* lives outside paper folders, in `<papers>/venues/<venue>/`.
|
|
26
|
+
*
|
|
27
|
+
* ── WHAT THE PAPER DECLARES ──────────────────────────────────────────────────────
|
|
28
|
+
* A paper's settings may move three of these: `pdf` (the built PDF under another name), `talk.dir`
|
|
29
|
+
* (another talk folder) and a preset file of its own named by `extends`. What the settings name at the
|
|
30
|
+
* root is allowed beside the fixed set (`Declared.roots`); this module does not read settings.
|
|
31
|
+
*/
|
|
32
|
+
import { basename, extname } from "node:path";
|
|
33
|
+
import { leaf, renderTree, type TreeNode } from "./layout-tree.ts";
|
|
34
|
+
import { REPRO_DIR, REPRO_NODES } from "./repro-layout.ts";
|
|
35
|
+
|
|
36
|
+
/** One entry at the paper folder's root: its name, and whether it is a folder. */
|
|
37
|
+
export interface LayoutEntry {
|
|
38
|
+
readonly name: string;
|
|
39
|
+
readonly kind: "file" | "dir";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** What the paper's own settings name at its root (`pdf`, `talk.dir`, a preset of its own). */
|
|
43
|
+
export interface Declared {
|
|
44
|
+
readonly roots: readonly string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const NOTHING_DECLARED: Declared = { roots: [] };
|
|
48
|
+
|
|
49
|
+
/** An entry that is not part of the layout, with where such material belongs. */
|
|
50
|
+
export interface Stray {
|
|
51
|
+
/** The entry's name; a folder's ends in a slash. */
|
|
52
|
+
readonly name: string;
|
|
53
|
+
readonly kind: "file" | "dir";
|
|
54
|
+
/** Where it goes: one destination for a known name, else the choice by what it is. */
|
|
55
|
+
readonly cure: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The one folder whose content is not judged. */
|
|
59
|
+
export const ESCAPE_HATCH = "extra";
|
|
60
|
+
|
|
61
|
+
/** An allowed entry's name, with what it is for — the note the layout's drawing shows beside it. */
|
|
62
|
+
export interface Noted {
|
|
63
|
+
readonly name: string;
|
|
64
|
+
readonly note: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The files the root may hold, by exact name, each with what it is for. */
|
|
68
|
+
export const ROOT_FILE_NOTES: readonly Noted[] = [
|
|
69
|
+
{
|
|
70
|
+
name: "PIPELINE-STATUS.md",
|
|
71
|
+
note: "the paper's one live-state file: stages, plan, scorecard",
|
|
72
|
+
},
|
|
73
|
+
{ name: "paper.tex", note: "the source; `paperlint build` compiles it" },
|
|
74
|
+
{ name: "paperlint.json", note: "the paper's settings" },
|
|
75
|
+
{ name: "README.md", note: "what the paper is" },
|
|
76
|
+
{ name: "CLAUDE.md", note: "the paper's agent rules" },
|
|
77
|
+
{ name: "CLAIMS.md", note: "the claims ledger" },
|
|
78
|
+
{
|
|
79
|
+
name: "NOTES.md",
|
|
80
|
+
note: "each number in the paper → the data it comes from",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "SUBMIT-CHECKLIST.md",
|
|
84
|
+
note: "the venue's format and submission mechanics",
|
|
85
|
+
},
|
|
86
|
+
{ name: ".gitignore", note: "keeps the build's byproducts out of git" },
|
|
87
|
+
{ name: "paper.pdf", note: "the built PDF" },
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
/** The files the root may hold, by exact name. */
|
|
91
|
+
export const ROOT_FILES: readonly string[] = ROOT_FILE_NOTES.map((f) => f.name);
|
|
92
|
+
|
|
93
|
+
/** The folders the root may hold, by exact name, each with what it holds. */
|
|
94
|
+
export const ROOT_DIR_NOTES: readonly Noted[] = [
|
|
95
|
+
{ name: "_build", note: "the facts `paperlint build` writes" },
|
|
96
|
+
{ name: "versions", note: "each stage's frozen PDF and source" },
|
|
97
|
+
{ name: "reviews", note: "dated reviews and pass reports" },
|
|
98
|
+
{ name: "siblings", note: "the cards of competing papers" },
|
|
99
|
+
{ name: "research", note: "dated research and decision records" },
|
|
100
|
+
{ name: REPRO_DIR, note: "the reproduction artifact" },
|
|
101
|
+
{ name: "figures", note: "graphics and their sources" },
|
|
102
|
+
{ name: "sections", note: "the body, when it is split" },
|
|
103
|
+
{ name: "talk", note: "the talk's files" },
|
|
104
|
+
{
|
|
105
|
+
name: "submission",
|
|
106
|
+
note: "forms, portal records, mail with the venue, the anonymized upload",
|
|
107
|
+
},
|
|
108
|
+
{ name: ESCAPE_HATCH, note: "anything; never judged" },
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
/** The folders the root may hold, by exact name. */
|
|
112
|
+
export const ROOT_DIRS: readonly string[] = ROOT_DIR_NOTES.map((d) => d.name);
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The files TeX and bibtex look up in the folder they run in, which is the paper's root: a fragment
|
|
116
|
+
* `\input` names, a database, a class, a package, a bibliography style.
|
|
117
|
+
*/
|
|
118
|
+
export const TEX_INPUT_EXTENSIONS: readonly string[] = [
|
|
119
|
+
".tex",
|
|
120
|
+
".bib",
|
|
121
|
+
".cls",
|
|
122
|
+
".sty",
|
|
123
|
+
".bst",
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The extensions of what a TeX run writes next to its main file, named after the job. The list is
|
|
128
|
+
* the union of latexmk's clean lists (`@generated_exts`, `$clean_ext` and `$clean_full_ext` in the
|
|
129
|
+
* latexmk documentation, the files `latexmk -c` and `-C` delete) and the outputs of the tools
|
|
130
|
+
* `paperlint build` and a hand run use on the same job: pdflatex (`aux log out toc lof lot lol fls
|
|
131
|
+
* xdv dvi`, `synctex.gz` with `-synctex`), the algorithm packages' `\listofalgorithms` (`loa`),
|
|
132
|
+
* bibtex (`bbl blg`), makeindex (`idx ind ilg`), glossaries and nomencl (`glo gls glg acn acr alg ist
|
|
133
|
+
* nlo nls`), biber and biblatex (`bcf run.xml`), beamer (`nav snm vrb`), and latexmk's own
|
|
134
|
+
* `fdb_latexmk`. A missing extension is added here, not
|
|
135
|
+
* special-cased where the rule is judged.
|
|
136
|
+
*/
|
|
137
|
+
const BYPRODUCT_EXTENSIONS: readonly string[] = [
|
|
138
|
+
"aux",
|
|
139
|
+
"log",
|
|
140
|
+
"out",
|
|
141
|
+
"toc",
|
|
142
|
+
"lof",
|
|
143
|
+
"lot",
|
|
144
|
+
"lol",
|
|
145
|
+
"loa",
|
|
146
|
+
"fls",
|
|
147
|
+
"xdv",
|
|
148
|
+
"dvi",
|
|
149
|
+
"synctex.gz",
|
|
150
|
+
"bbl",
|
|
151
|
+
"blg",
|
|
152
|
+
"idx",
|
|
153
|
+
"ind",
|
|
154
|
+
"ilg",
|
|
155
|
+
"glo",
|
|
156
|
+
"gls",
|
|
157
|
+
"glg",
|
|
158
|
+
"acn",
|
|
159
|
+
"acr",
|
|
160
|
+
"alg",
|
|
161
|
+
"ist",
|
|
162
|
+
"nlo",
|
|
163
|
+
"nls",
|
|
164
|
+
"bcf",
|
|
165
|
+
"run.xml",
|
|
166
|
+
"nav",
|
|
167
|
+
"snm",
|
|
168
|
+
"vrb",
|
|
169
|
+
"fdb_latexmk",
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* What a `paperlint build` leaves beside `paper.tex`: pdflatex and bibtex run in the paper folder and
|
|
174
|
+
* name every output after the job, `paper`, so only `paper.<extension>` is the build's. Never
|
|
175
|
+
* reported — they are the build's, not the author's. Another job name (`draft.aux`) is the author's.
|
|
176
|
+
*/
|
|
177
|
+
export const BUILD_BYPRODUCTS: readonly string[] = BYPRODUCT_EXTENSIONS.map(
|
|
178
|
+
(ext) => `paper.${ext}`,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
/** Files the operating system writes into any folder it shows. Never reported. */
|
|
182
|
+
export const OS_FILES: readonly string[] = [".DS_Store", "Thumbs.db"];
|
|
183
|
+
|
|
184
|
+
/** The live state of a paper — its stages, its plan, its scorecard — is one file. */
|
|
185
|
+
const STATUS_SECTIONS =
|
|
186
|
+
'a section of PIPELINE-STATUS.md — the paper\'s one live-state file holds its stages, its plan ("Plan / now") and its scorecard; move what is still live there and delete the file';
|
|
187
|
+
|
|
188
|
+
/** The shared corpus of a venue's accepted papers. */
|
|
189
|
+
const VENUE_CORPUS =
|
|
190
|
+
"<papers-root>/venues/<venue>/, outside the paper folder: one corpus per venue, shared by every paper that targets it";
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Names met in real paper folders, each with its one destination. Matched ignoring case, a file's
|
|
194
|
+
* name against files and a folder's against folders.
|
|
195
|
+
*/
|
|
196
|
+
export const KNOWN_CURES: readonly {
|
|
197
|
+
readonly name: string;
|
|
198
|
+
readonly kind: "file" | "dir";
|
|
199
|
+
readonly cure: string;
|
|
200
|
+
}[] = [
|
|
201
|
+
{ name: "PLAN.md", kind: "file", cure: STATUS_SECTIONS },
|
|
202
|
+
{ name: "plans", kind: "dir", cure: STATUS_SECTIONS },
|
|
203
|
+
{ name: "DECISION-LOG.md", kind: "file", cure: "research/ (a dated record)" },
|
|
204
|
+
{
|
|
205
|
+
name: "EMAILS.md",
|
|
206
|
+
kind: "file",
|
|
207
|
+
cure: "submission/ (mail with the venue is about submitting)",
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: "data",
|
|
211
|
+
kind: "dir",
|
|
212
|
+
cure: "repro/data/ (the data behind the numbers)",
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: "artifact",
|
|
216
|
+
kind: "dir",
|
|
217
|
+
cure: "repro/ — its scripts in repro/tools/, its data in repro/data/; the anonymized copy uploaded for review is about submitting and goes in submission/",
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: "archive",
|
|
221
|
+
kind: "dir",
|
|
222
|
+
cure: "versions/ for frozen PDFs, submission/ for an earlier attempt's forms, mails and checklists, extra/ for the rest",
|
|
223
|
+
},
|
|
224
|
+
{ name: "venue-corpus", kind: "dir", cure: VENUE_CORPUS },
|
|
225
|
+
{ name: "venues", kind: "dir", cure: VENUE_CORPUS },
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
/** For a name nobody listed: the choice by what the material is. */
|
|
229
|
+
export const CURE_BY_KIND =
|
|
230
|
+
"decide by what it is — a record or research → research/; about submitting (forms, mails, checklists, portal records) → submission/; live state of the paper (plan, status) → a section of PIPELINE-STATUS.md; reproduction (scripts, data, the artifact) → repro/; a frozen PDF of a stage → versions/; unclear → extra/, which is never judged";
|
|
231
|
+
|
|
232
|
+
const isAllowedFile = (name: string, declared: Declared): boolean =>
|
|
233
|
+
ROOT_FILES.includes(name) ||
|
|
234
|
+
TEX_INPUT_EXTENSIONS.includes(extname(name)) ||
|
|
235
|
+
BUILD_BYPRODUCTS.includes(name) ||
|
|
236
|
+
OS_FILES.includes(name) ||
|
|
237
|
+
declared.roots.includes(name);
|
|
238
|
+
|
|
239
|
+
const isAllowed = (e: LayoutEntry, declared: Declared): boolean =>
|
|
240
|
+
e.kind === "dir"
|
|
241
|
+
? ROOT_DIRS.includes(e.name) || declared.roots.includes(e.name)
|
|
242
|
+
: isAllowedFile(e.name, declared);
|
|
243
|
+
|
|
244
|
+
/** Where material like `e` belongs: its listed destination, else the choice by what it is. */
|
|
245
|
+
function cureFor(e: LayoutEntry): string {
|
|
246
|
+
const known = KNOWN_CURES.find(
|
|
247
|
+
(c) => c.kind === e.kind && c.name.toLowerCase() === e.name.toLowerCase(),
|
|
248
|
+
);
|
|
249
|
+
return known === undefined ? CURE_BY_KIND : known.cure;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** The root entries that are not part of the layout, in the order given. Pure. */
|
|
253
|
+
export function judgeLayout(
|
|
254
|
+
entries: readonly LayoutEntry[],
|
|
255
|
+
declared: Declared,
|
|
256
|
+
): readonly Stray[] {
|
|
257
|
+
// `\include{chapter}` makes pdflatex write chapter.aux beside chapter.tex, under that file's own
|
|
258
|
+
// name, not the job's: the build's for every source at the root, the author's without one.
|
|
259
|
+
const includeAux = entries
|
|
260
|
+
.filter((e) => e.kind === "file" && extname(e.name) === ".tex")
|
|
261
|
+
.map((e) => `${basename(e.name, ".tex")}.aux`);
|
|
262
|
+
return entries
|
|
263
|
+
.filter(
|
|
264
|
+
(e) =>
|
|
265
|
+
!isAllowed(e, declared) &&
|
|
266
|
+
!(e.kind === "file" && includeAux.includes(e.name)),
|
|
267
|
+
)
|
|
268
|
+
.map((e) => ({
|
|
269
|
+
name: e.kind === "dir" ? `${e.name}/` : e.name,
|
|
270
|
+
kind: e.kind,
|
|
271
|
+
cure: cureFor(e),
|
|
272
|
+
}));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** The paper folder's layout, entry by entry, `repro/` drawn with its own layout inside. */
|
|
276
|
+
export const PAPER_NODES: readonly TreeNode[] = [
|
|
277
|
+
...ROOT_FILE_NOTES.map((f) => leaf(f.name, f.note)),
|
|
278
|
+
leaf(
|
|
279
|
+
TEX_INPUT_EXTENSIONS.map((e) => `*${e}`).join(" "),
|
|
280
|
+
"what TeX and bibtex look up beside paper.tex",
|
|
281
|
+
),
|
|
282
|
+
...ROOT_DIR_NOTES.map((d) => ({
|
|
283
|
+
name: `${d.name}/`,
|
|
284
|
+
note: d.note,
|
|
285
|
+
children: d.name === REPRO_DIR ? REPRO_NODES : [],
|
|
286
|
+
})),
|
|
287
|
+
];
|
|
288
|
+
|
|
289
|
+
/** The drawing of the paper folder's layout: the rule's message, the hook's refusal, the rule's page. */
|
|
290
|
+
export const PAPER_LAYOUT_TREE: string = renderTree("<paper>/", PAPER_NODES);
|
|
291
|
+
|
|
292
|
+
/** The folder beside the paper folders that holds one corpus of accepted papers per venue. */
|
|
293
|
+
export const VENUES_DIR = "venues";
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* The papers directory as the README and `docs/layout.md` draw it: the venues' corpora beside the
|
|
297
|
+
* paper folders, and each paper folder with its layout. Only the paper folder's part is enforced (by
|
|
298
|
+
* the two layout rules); `venues/` is drawn so a reader sees where a corpus goes, and no rule reads it.
|
|
299
|
+
*/
|
|
300
|
+
export const PAPERS_NODES: readonly TreeNode[] = [
|
|
301
|
+
{
|
|
302
|
+
name: `${VENUES_DIR}/`,
|
|
303
|
+
note: "outside every paper folder: one folder per venue",
|
|
304
|
+
children: [
|
|
305
|
+
leaf(
|
|
306
|
+
"<venue>/",
|
|
307
|
+
"that venue's accepted papers, shared by every paper aimed at it",
|
|
308
|
+
),
|
|
309
|
+
],
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
name: "<paper>/",
|
|
313
|
+
note: "one folder per paper, named after the work; its root holds only what is below",
|
|
314
|
+
children: PAPER_NODES,
|
|
315
|
+
},
|
|
316
|
+
];
|
|
317
|
+
|
|
318
|
+
/** The drawing of the papers directory: the README's and `docs/layout.md`'s map of where things live. */
|
|
319
|
+
export const PAPERS_LAYOUT_TREE: string = renderTree(
|
|
320
|
+
"<papers-root>/",
|
|
321
|
+
PAPERS_NODES,
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* The one text a stray entry is reported with, by the rule `paper/allowed-files-only` and by the hook that
|
|
326
|
+
* refuses to create such an entry. It stands alone: where the entry goes, and the whole layout drawn,
|
|
327
|
+
* for a reader who knows nothing of paperlint.
|
|
328
|
+
*/
|
|
329
|
+
export function strayMessage(s: Stray): string {
|
|
330
|
+
return [
|
|
331
|
+
`\`${s.name}\` is at the root of a paper folder (the folder holding PIPELINE-STATUS.md), and that root holds only paperlint's layout. Where it goes: ${s.cure}.`,
|
|
332
|
+
`The layout (${ESCAPE_HATCH}/ takes anything and is never judged; the build's paper.* byproducts are never reported):`,
|
|
333
|
+
PAPER_LAYOUT_TREE,
|
|
334
|
+
`Rule paper/allowed-files-only: https://github.com/zernie/paperlint/blob/main/docs/rules/paper/allowed-files-only.md`,
|
|
335
|
+
].join("\n");
|
|
336
|
+
}
|