paperlint 10.2.0 → 10.3.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/dist/adapters/study-registry/index.d.ts +352 -1
- package/dist/adapters/study-registry/index.d.ts.map +1 -1
- package/dist/adapters/study-registry/index.js +171 -8
- package/dist/adapters/study-registry/index.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -4
- package/dist/cli.js.map +1 -1
- package/dist/domain/study-registry.d.ts +201 -5
- package/dist/domain/study-registry.d.ts.map +1 -1
- package/dist/domain/study-registry.js +224 -2
- package/dist/domain/study-registry.js.map +1 -1
- package/dist/ports/study-registry-codec.d.ts +6 -2
- package/dist/ports/study-registry-codec.d.ts.map +1 -1
- package/dist/studies.d.ts.map +1 -1
- package/dist/studies.js +7 -4
- package/dist/studies.js.map +1 -1
- package/dist/study-registry-rule.d.ts +12 -5
- package/dist/study-registry-rule.d.ts.map +1 -1
- package/dist/study-registry-rule.js +67 -37
- package/dist/study-registry-rule.js.map +1 -1
- package/docs/design/study-registry.md +131 -15
- package/docs/layout.md +23 -21
- package/docs/rules/repro/study-plan-name.md +70 -0
- package/docs/rules/repro/study-registered.md +164 -6
- package/docs/rules.md +53 -52
- package/package.json +1 -1
- package/skills/build-benchmark/SKILL.md +24 -12
- package/skills/build-benchmark/SKILL.md.spec.ts +23 -11
- package/skills/build-label-page/SKILL.md +9 -7
- package/skills/build-label-page/SKILL.md.spec.ts +8 -6
- package/skills/run-agent-grid/SKILL.md +10 -5
- package/skills/run-agent-grid/SKILL.md.spec.ts +9 -4
- package/src/adapters/study-registry/index.test.ts +340 -0
- package/src/adapters/study-registry/index.ts +268 -10
- package/src/cli.ts +6 -4
- package/src/domain/study-measurements.test.ts +629 -0
- package/src/domain/study-registry.test.ts +10 -1
- package/src/domain/study-registry.ts +456 -10
- package/src/new-paper.test.ts +1 -0
- package/src/ports/study-registry-codec.ts +6 -2
- package/src/rule-cases.test.ts +27 -0
- package/src/studies-cli.test.ts +2 -2
- package/src/studies.test.ts +6 -0
- package/src/studies.ts +12 -3
- package/src/study-registry-rule.test.ts +140 -1
- package/src/study-registry-rule.ts +106 -39
package/src/studies.test.ts
CHANGED
|
@@ -9,6 +9,8 @@ import { memoryFiles, memoryListDir } from "./adapters/memory/index.ts";
|
|
|
9
9
|
import { studyRegistryCodec } from "./adapters/study-registry/index.ts";
|
|
10
10
|
import {
|
|
11
11
|
indexBlock,
|
|
12
|
+
planTemplate,
|
|
13
|
+
planText,
|
|
12
14
|
registryText,
|
|
13
15
|
studyReadme,
|
|
14
16
|
withIndex,
|
|
@@ -101,17 +103,21 @@ describe("studies new", () => {
|
|
|
101
103
|
status: "designing",
|
|
102
104
|
date: "2026-10-15",
|
|
103
105
|
in_paper: "no",
|
|
106
|
+
plan: "plan.json",
|
|
104
107
|
};
|
|
105
108
|
const r = P1(study(), added);
|
|
106
109
|
expect({ code, err }).toEqual({ code: 0, err: [] });
|
|
107
110
|
expect(after).toEqual({
|
|
108
111
|
...before,
|
|
109
112
|
[`${REPRO}/P1-S02-ablation-of-the-guard/README.md`]: studyReadme(added),
|
|
113
|
+
[`${REPRO}/P1-S02-ablation-of-the-guard/plan.json`]:
|
|
114
|
+
planText(planTemplate()),
|
|
110
115
|
[`${REPRO}/studies.json`]: registryText(r),
|
|
111
116
|
[`${REPRO}/README.md`]: `# Reproduction index\n\nIntro.\n\n${indexBlock(r)}\n`,
|
|
112
117
|
});
|
|
113
118
|
expect(log).toEqual([
|
|
114
119
|
"created papers/example/repro/P1-S02-ablation-of-the-guard/README.md",
|
|
120
|
+
"created papers/example/repro/P1-S02-ablation-of-the-guard/plan.json, the study's plan: its fields are there to fill in before the first run",
|
|
115
121
|
"added P1-S02 to papers/example/repro/studies.json: kind spike, status designing, 2026-10-15",
|
|
116
122
|
"wrote the index of 2 studies in papers/example/repro/README.md",
|
|
117
123
|
"Next: write the study's one-sentence question in its row (`question`) and in its README.md. When it ends, set `status`, `result`, `result_file` and `in_paper` in its row, then run `npx paperlint studies index papers/example`.",
|
package/src/studies.ts
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
* `repro/studies.json`, and its index in `repro/README.md` (docs/design/study-registry.md).
|
|
4
4
|
*
|
|
5
5
|
* new <paper-dir> "<title>" [--kind <kind>]
|
|
6
|
-
* the next id; `repro/<id>-<slug>/README.md` with the id and title in its frontmatter
|
|
7
|
-
* row, `status: designing`, today's date
|
|
6
|
+
* the next id; `repro/<id>-<slug>/README.md` with the id and title in its frontmatter, and
|
|
7
|
+
* `plan.json`, the plan's template; the row, `status: designing`, today's date, its `plan`;
|
|
8
|
+
* the index regenerated
|
|
8
9
|
* index <paper-dir>
|
|
9
10
|
* the index block of `repro/README.md`, regenerated from the registry
|
|
10
11
|
*
|
|
@@ -18,11 +19,14 @@ import type { AbsolutePath } from "./domain/paths.ts";
|
|
|
18
19
|
import { README, REPRO_DIR } from "./domain/repro-layout.ts";
|
|
19
20
|
import {
|
|
20
21
|
DEFAULT_KIND,
|
|
22
|
+
PLAN_FILE,
|
|
21
23
|
STUDIES_FILE,
|
|
22
24
|
STUDY_KINDS,
|
|
23
25
|
addStudy,
|
|
24
26
|
indexInSync,
|
|
25
27
|
isStudyKind,
|
|
28
|
+
planTemplate,
|
|
29
|
+
planText,
|
|
26
30
|
registryText,
|
|
27
31
|
studyReadme,
|
|
28
32
|
withIndex,
|
|
@@ -178,7 +182,7 @@ function newArgs(
|
|
|
178
182
|
);
|
|
179
183
|
}
|
|
180
184
|
|
|
181
|
-
/** Write a new study's README.md, the registry, and the index; the lines that say so. */
|
|
185
|
+
/** Write a new study's README.md and plan.json, the registry, and the index; the lines that say so. */
|
|
182
186
|
function writeStudy(
|
|
183
187
|
p: Paper,
|
|
184
188
|
added: { readonly registry: StudyRegistry; readonly study: Study },
|
|
@@ -188,11 +192,16 @@ function writeStudy(
|
|
|
188
192
|
const at = (...names: readonly string[]): AbsolutePath =>
|
|
189
193
|
callerPath(join(p.dir, REPRO_DIR, ...names));
|
|
190
194
|
files.writeAtomic(at(study.folder, README), text(studyReadme(study)));
|
|
195
|
+
files.writeAtomic(
|
|
196
|
+
at(study.folder, PLAN_FILE),
|
|
197
|
+
text(planText(planTemplate())),
|
|
198
|
+
);
|
|
191
199
|
files.writeAtomic(at(STUDIES_FILE), text(registryText(registry)));
|
|
192
200
|
writeIndex(p.dir, registry, files);
|
|
193
201
|
const repro = `${p.shown}/${REPRO_DIR}`;
|
|
194
202
|
return [
|
|
195
203
|
`created ${repro}/${study.folder}/${README}`,
|
|
204
|
+
`created ${repro}/${study.folder}/${PLAN_FILE}, the study's plan: its fields are there to fill in before the first run`,
|
|
196
205
|
`added ${study.id} to ${repro}/${STUDIES_FILE}: kind ${study.kind}, status ${study.status}, ${study.date}`,
|
|
197
206
|
`wrote the index of ${studiesCount(registry)} in ${repro}/${README}`,
|
|
198
207
|
`Next: write the study's one-sentence question in its row (\`question\`) and in its README.md. When it ends, set \`status\`, \`result\`, \`result_file\` and \`in_paper\` in its row, then run \`npx paperlint studies index ${p.shown}\`.`,
|
|
@@ -9,6 +9,8 @@ import { describe, expect, it } from "vitest";
|
|
|
9
9
|
import { memoryFiles, memoryListDir } from "./adapters/memory/index.ts";
|
|
10
10
|
import { studyRegistryCodec } from "./adapters/study-registry/index.ts";
|
|
11
11
|
import {
|
|
12
|
+
planTemplate,
|
|
13
|
+
planText,
|
|
12
14
|
registryText,
|
|
13
15
|
studyMessage,
|
|
14
16
|
withIndex,
|
|
@@ -65,13 +67,14 @@ interface Reported {
|
|
|
65
67
|
function lint(
|
|
66
68
|
tree: Readonly<Record<string, string>>,
|
|
67
69
|
file = `${DIR}/PIPELINE-STATUS.md`,
|
|
70
|
+
name: "study-registered" | "study-plan-name" = "study-registered",
|
|
68
71
|
): Reported[] {
|
|
69
72
|
const files = memoryFiles(tree);
|
|
70
73
|
const rule = studyRegistryRules({
|
|
71
74
|
files,
|
|
72
75
|
listDir: memoryListDir(files),
|
|
73
76
|
codec: studyRegistryCodec,
|
|
74
|
-
})[
|
|
77
|
+
})[name];
|
|
75
78
|
const out: Reported[] = [];
|
|
76
79
|
const visitor = rule.create({
|
|
77
80
|
filename: file,
|
|
@@ -271,6 +274,7 @@ describe("repro/study-registered — level", () => {
|
|
|
271
274
|
it("is an error from the start", () => {
|
|
272
275
|
expect(STUDY_REGISTRY_RULE_LEVELS).toEqual({
|
|
273
276
|
"repro/study-registered": "error",
|
|
277
|
+
"repro/study-plan-name": "warn",
|
|
274
278
|
});
|
|
275
279
|
});
|
|
276
280
|
});
|
|
@@ -315,3 +319,138 @@ describe("repro/study-registered — a result_file the disk cannot hold", () =>
|
|
|
315
319
|
).toEqual(["result-file"]);
|
|
316
320
|
});
|
|
317
321
|
});
|
|
322
|
+
|
|
323
|
+
/** A plan as a study would hold it once the author has checked its drawn units. */
|
|
324
|
+
const FILLED = planText({
|
|
325
|
+
question: "How many rules can a linter check?",
|
|
326
|
+
population: {
|
|
327
|
+
source: "the crawl of 2026-10-01",
|
|
328
|
+
draw: { kind: "all", method: "census" },
|
|
329
|
+
},
|
|
330
|
+
arms: [],
|
|
331
|
+
primary_outcome: "the share of rules a linter can check",
|
|
332
|
+
author_check: { by: "A. Author", date: "2026-10-14", quote: "Go." },
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
/** A registry with a headline measurement of one running study whose row names `plan`. */
|
|
336
|
+
const HEADLINE = (plan: string): StudyRegistry => ({
|
|
337
|
+
prefix: "P1",
|
|
338
|
+
author: "A. Author",
|
|
339
|
+
measurements: [
|
|
340
|
+
{
|
|
341
|
+
id: "M1",
|
|
342
|
+
title: "Rule census",
|
|
343
|
+
role: "headline",
|
|
344
|
+
approval: { by: "A. Author", date: "2026-10-12", quote: "Yes." },
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
studies: [study({ status: "running", measurement: "M1", plan })],
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
describe("repro/study-registered — the plan a row names, read from its folder", () => {
|
|
351
|
+
it("is silent on a plan.json the author checked", () => {
|
|
352
|
+
expect(
|
|
353
|
+
lint({
|
|
354
|
+
...registered(HEADLINE("plan.json")),
|
|
355
|
+
[`${DIR}/repro/P1-S01-baseline/plan.json`]: FILLED,
|
|
356
|
+
}),
|
|
357
|
+
).toEqual([]);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("reports a plan that is not there", () => {
|
|
361
|
+
expect(messages(registered(HEADLINE("plan.json")))).toEqual([
|
|
362
|
+
studyMessage({
|
|
363
|
+
problem: "bad-plan",
|
|
364
|
+
id: "P1-S01",
|
|
365
|
+
path: "plan.json",
|
|
366
|
+
why: "missing",
|
|
367
|
+
}),
|
|
368
|
+
]);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("reports a plan that is not JSON, with the reason", () => {
|
|
372
|
+
expect(
|
|
373
|
+
messages({
|
|
374
|
+
...registered(HEADLINE("plan.json")),
|
|
375
|
+
[`${DIR}/repro/P1-S01-baseline/plan.json`]: "{",
|
|
376
|
+
}),
|
|
377
|
+
).toEqual([
|
|
378
|
+
studyMessage({
|
|
379
|
+
problem: "bad-plan",
|
|
380
|
+
id: "P1-S01",
|
|
381
|
+
path: "plan.json",
|
|
382
|
+
why: "unreadable",
|
|
383
|
+
reasons: "not JSON",
|
|
384
|
+
}),
|
|
385
|
+
]);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("reports a running study of the headline whose plan is still the template", () => {
|
|
389
|
+
expect(
|
|
390
|
+
lint({
|
|
391
|
+
...registered(HEADLINE("plan.json")),
|
|
392
|
+
[`${DIR}/repro/P1-S01-baseline/plan.json`]: planText(planTemplate()),
|
|
393
|
+
}).map((r) => [r.messageId, r.line]),
|
|
394
|
+
).toEqual([
|
|
395
|
+
["bad-plan", 1],
|
|
396
|
+
["author-check", 1],
|
|
397
|
+
]);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
describe("repro/study-registered — a plan the disk cannot hold", () => {
|
|
402
|
+
it("reads as missing, and the disk is never asked", () => {
|
|
403
|
+
const r = HEADLINE("a\u0000.json");
|
|
404
|
+
const files = memoryFiles(registered(r));
|
|
405
|
+
const probe = {
|
|
406
|
+
...files,
|
|
407
|
+
isFile: (p: Parameters<typeof files.isFile>[0]): boolean => {
|
|
408
|
+
if (p.includes("\0")) throw new Error("ERR_INVALID_ARG_VALUE");
|
|
409
|
+
return files.isFile(p);
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
expect(
|
|
413
|
+
studyFindings(DIR, {
|
|
414
|
+
files: probe,
|
|
415
|
+
listDir: memoryListDir(files),
|
|
416
|
+
codec: studyRegistryCodec,
|
|
417
|
+
}).map((f) => f.problem),
|
|
418
|
+
).toEqual(["bad-plan"]);
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
describe("repro/study-plan-name — a plan under its old name", () => {
|
|
423
|
+
const OLD = {
|
|
424
|
+
...registered(HEADLINE("prereg.json")),
|
|
425
|
+
[`${DIR}/repro/P1-S01-baseline/prereg.json`]: FILLED,
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
it("is read as the plan: repro/study-registered is silent", () => {
|
|
429
|
+
expect(lint(OLD)).toEqual([]);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("is reported by repro/study-plan-name, naming plan.json", () => {
|
|
433
|
+
expect(lint(OLD, `${DIR}/PIPELINE-STATUS.md`, "study-plan-name")).toEqual([
|
|
434
|
+
{
|
|
435
|
+
messageId: "legacy-plan",
|
|
436
|
+
message: studyMessage({
|
|
437
|
+
problem: "legacy-plan",
|
|
438
|
+
id: "P1-S01",
|
|
439
|
+
path: "prereg.json",
|
|
440
|
+
}),
|
|
441
|
+
line: 1,
|
|
442
|
+
},
|
|
443
|
+
]);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("is silent on plan.json, and on any file but PIPELINE-STATUS.md", () => {
|
|
447
|
+
const tree = {
|
|
448
|
+
...registered(HEADLINE("plan.json")),
|
|
449
|
+
[`${DIR}/repro/P1-S01-baseline/plan.json`]: FILLED,
|
|
450
|
+
};
|
|
451
|
+
expect([
|
|
452
|
+
lint(tree, `${DIR}/PIPELINE-STATUS.md`, "study-plan-name"),
|
|
453
|
+
lint(OLD, `${DIR}/paper.tex`, "study-plan-name"),
|
|
454
|
+
]).toEqual([[], []]);
|
|
455
|
+
});
|
|
456
|
+
});
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `repro/study-registered` — every study folder at the top of a paper's `repro/` has exactly one row in
|
|
3
|
-
* `repro/studies.json`, every row names a folder that exists,
|
|
4
|
-
* one the registry generates.
|
|
3
|
+
* `repro/studies.json`, every row names a folder that exists, a row's measurement and plan hold
|
|
4
|
+
* together, and the index in `repro/README.md` is the one the registry generates. Beside it,
|
|
5
|
+
* `repro/study-plan-name`, a warning: a plan still named `prereg.json`.
|
|
5
6
|
*
|
|
6
7
|
* What is judged, and why, is `src/domain/study-registry.ts` and docs/rules/repro/study-registered.md.
|
|
7
|
-
* This file lists `repro/`, reads the registry, the README
|
|
8
|
+
* This file lists `repro/`, reads the registry, the README, the result files and the plans, and reports.
|
|
8
9
|
*
|
|
9
10
|
* ── WHY A RULE OF ITS OWN, NOT MORE OF `repro/allowed-files-only` ─────────────────────
|
|
10
11
|
* `repro/allowed-files-only` judges what may stand at the top of `repro/`, by name, and accepts the
|
|
@@ -16,7 +17,7 @@
|
|
|
16
17
|
* `PIPELINE-STATUS.md`, first line, like the layout rules: every paper has it, and ESLint lints no
|
|
17
18
|
* file in `repro/`.
|
|
18
19
|
*/
|
|
19
|
-
import { basename, dirname, join } from "node:path";
|
|
20
|
+
import { basename, dirname, join, posix } from "node:path";
|
|
20
21
|
import { callerPath } from "./caller-path.ts";
|
|
21
22
|
import {
|
|
22
23
|
README,
|
|
@@ -25,9 +26,13 @@ import {
|
|
|
25
26
|
REPRO_DIRS,
|
|
26
27
|
} from "./domain/repro-layout.ts";
|
|
27
28
|
import {
|
|
29
|
+
LEGACY_PLAN_FILE,
|
|
30
|
+
PLAN_NAME_PROBLEMS,
|
|
28
31
|
STUDIES_FILE,
|
|
29
32
|
judgeStudies,
|
|
33
|
+
planKeys,
|
|
30
34
|
studyMessage,
|
|
35
|
+
type PlanState,
|
|
31
36
|
type RegistryState,
|
|
32
37
|
type StudyFinding,
|
|
33
38
|
} from "./domain/study-registry.ts";
|
|
@@ -97,7 +102,31 @@ const existingResults = (
|
|
|
97
102
|
),
|
|
98
103
|
);
|
|
99
104
|
|
|
100
|
-
/**
|
|
105
|
+
/**
|
|
106
|
+
* The plan each row of `r` names, read and parsed, by `<folder>/<plan>`; a plan not there is left out.
|
|
107
|
+
* A plan still named `prereg.json` is parsed by the loose schema of its old name.
|
|
108
|
+
*/
|
|
109
|
+
const plansOf = (
|
|
110
|
+
dir: string,
|
|
111
|
+
r: RegistryState,
|
|
112
|
+
deps: Pick<StudyRegistryDeps, "files" | "codec">,
|
|
113
|
+
): ReadonlyMap<string, PlanState> =>
|
|
114
|
+
new Map(
|
|
115
|
+
(r.kind === "read" ? planKeys(r.registry) : [])
|
|
116
|
+
// A NUL is no path the disk can hold, and probing one throws: such a plan reads as missing.
|
|
117
|
+
.filter((key) => !key.includes("\0"))
|
|
118
|
+
.flatMap((key): readonly (readonly [string, PlanState])[] => {
|
|
119
|
+
const text = textAt(deps.files, join(dir, REPRO_DIR, key));
|
|
120
|
+
if (text === null) return [];
|
|
121
|
+
const parse =
|
|
122
|
+
posix.basename(key) === LEGACY_PLAN_FILE
|
|
123
|
+
? deps.codec.parseLegacyPlan
|
|
124
|
+
: deps.codec.parsePlan;
|
|
125
|
+
return [[key, parse(text)]];
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
/** Every finding of the paper folder `dir`'s study registry — THE function behind both rules. */
|
|
101
130
|
export function studyFindings(
|
|
102
131
|
dir: string,
|
|
103
132
|
deps: StudyRegistryDeps,
|
|
@@ -108,10 +137,11 @@ export function studyFindings(
|
|
|
108
137
|
registry,
|
|
109
138
|
readme: textAt(deps.files, join(dir, REPRO_DIR, README)),
|
|
110
139
|
existingResults: existingResults(dir, registry, deps.files),
|
|
140
|
+
plans: plansOf(dir, registry, deps),
|
|
111
141
|
});
|
|
112
142
|
}
|
|
113
143
|
|
|
114
|
-
/** The problems
|
|
144
|
+
/** The problems `repro/study-registered` reports, each its own message id; the text is `studyMessage`. */
|
|
115
145
|
const PROBLEMS: readonly StudyFinding["problem"][] = [
|
|
116
146
|
"no-registry",
|
|
117
147
|
"unreadable",
|
|
@@ -126,47 +156,84 @@ const PROBLEMS: readonly StudyFinding["problem"][] = [
|
|
|
126
156
|
"result-file",
|
|
127
157
|
"no-successor",
|
|
128
158
|
"stale-index",
|
|
159
|
+
"unknown-measurement",
|
|
160
|
+
"empty-measurement",
|
|
161
|
+
"unknown-requirement",
|
|
162
|
+
"headline-approval",
|
|
163
|
+
"bad-plan",
|
|
164
|
+
"author-check",
|
|
129
165
|
];
|
|
130
166
|
|
|
131
|
-
|
|
167
|
+
/** A rule's meta: its page, and a message id per problem, the whole text being `studyMessage`. */
|
|
168
|
+
const metaOf = (
|
|
169
|
+
name: string,
|
|
170
|
+
description: string,
|
|
171
|
+
problems: readonly StudyFinding["problem"][],
|
|
172
|
+
): FolderRuleModule["meta"] => ({
|
|
132
173
|
type: "suggestion",
|
|
133
|
-
docs: {
|
|
134
|
-
description:
|
|
135
|
-
"every study folder at the top of a paper's repro/ has exactly one row in repro/studies.json, every row names a folder that exists, and repro/README.md's index is the one the registry generates",
|
|
136
|
-
url: rulePageUrl("repro/study-registered"),
|
|
137
|
-
},
|
|
174
|
+
docs: { description, url: rulePageUrl(name) },
|
|
138
175
|
schema: [],
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
};
|
|
176
|
+
messages: Object.fromEntries(problems.map((p) => [p, "{{text}}"])),
|
|
177
|
+
});
|
|
142
178
|
|
|
143
|
-
/**
|
|
144
|
-
|
|
179
|
+
/** A rule that reports the findings of `problems`, reading the folder through `deps`. */
|
|
180
|
+
const ruleOf = (
|
|
181
|
+
meta: FolderRuleModule["meta"],
|
|
182
|
+
problems: readonly StudyFinding["problem"][],
|
|
145
183
|
deps: StudyRegistryDeps,
|
|
146
|
-
):
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
});
|
|
184
|
+
): FolderRuleModule => ({
|
|
185
|
+
meta,
|
|
186
|
+
create(context: FolderRuleContext) {
|
|
187
|
+
if (basename(context.filename) !== STATUS_FILE) return {};
|
|
188
|
+
return {
|
|
189
|
+
root() {
|
|
190
|
+
const at = context.sourceCode.getLocFromIndex(0);
|
|
191
|
+
studyFindings(dirname(context.filename), deps)
|
|
192
|
+
.filter((f) => problems.includes(f.problem))
|
|
193
|
+
.forEach((f) => {
|
|
194
|
+
context.report({
|
|
195
|
+
loc: { start: at, end: at },
|
|
196
|
+
messageId: f.problem,
|
|
197
|
+
data: { text: studyMessage(f) },
|
|
161
198
|
});
|
|
162
|
-
}
|
|
163
|
-
};
|
|
199
|
+
});
|
|
164
200
|
},
|
|
165
|
-
}
|
|
201
|
+
};
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The two rules, reading the folder through `deps`: `study-registered`, the registry's errors, and
|
|
207
|
+
* `study-plan-name`, a warning for a plan still named `prereg.json` — read as the plan, so a paper
|
|
208
|
+
* that wrote one before `plan.json` existed is not broken, and told the name to move to.
|
|
209
|
+
*/
|
|
210
|
+
export function studyRegistryRules(
|
|
211
|
+
deps: StudyRegistryDeps,
|
|
212
|
+
): Readonly<Record<"study-registered" | "study-plan-name", FolderRuleModule>> {
|
|
213
|
+
return {
|
|
214
|
+
"study-registered": ruleOf(
|
|
215
|
+
metaOf(
|
|
216
|
+
"repro/study-registered",
|
|
217
|
+
"every study folder at the top of a paper's repro/ has exactly one row in repro/studies.json, every row names a folder that exists, its measurement and its plan hold together, and repro/README.md's index is the one the registry generates",
|
|
218
|
+
PROBLEMS,
|
|
219
|
+
),
|
|
220
|
+
PROBLEMS,
|
|
221
|
+
deps,
|
|
222
|
+
),
|
|
223
|
+
"study-plan-name": ruleOf(
|
|
224
|
+
metaOf(
|
|
225
|
+
"repro/study-plan-name",
|
|
226
|
+
"a study's plan is named plan.json, not prereg.json",
|
|
227
|
+
PLAN_NAME_PROBLEMS,
|
|
228
|
+
),
|
|
229
|
+
PLAN_NAME_PROBLEMS,
|
|
230
|
+
deps,
|
|
231
|
+
),
|
|
166
232
|
};
|
|
167
233
|
}
|
|
168
234
|
|
|
169
|
-
/** The
|
|
170
|
-
export const STUDY_REGISTRY_RULE_LEVELS: Readonly<
|
|
171
|
-
|
|
172
|
-
|
|
235
|
+
/** The levels they are on at in paperlint's own config, for every paper's `PIPELINE-STATUS.md`. */
|
|
236
|
+
export const STUDY_REGISTRY_RULE_LEVELS: Readonly<{
|
|
237
|
+
"repro/study-registered": "error";
|
|
238
|
+
"repro/study-plan-name": "warn";
|
|
239
|
+
}> = { "repro/study-registered": "error", "repro/study-plan-name": "warn" };
|