paperlint 10.1.1 → 10.2.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.
Files changed (70) hide show
  1. package/README.md +2 -0
  2. package/dist/adapters/study-registry/index.d.ts +6 -0
  3. package/dist/adapters/study-registry/index.d.ts.map +1 -0
  4. package/dist/adapters/study-registry/index.js +58 -0
  5. package/dist/adapters/study-registry/index.js.map +1 -0
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +40 -2
  8. package/dist/cli.js.map +1 -1
  9. package/dist/domain/repro-layout.d.ts +12 -4
  10. package/dist/domain/repro-layout.d.ts.map +1 -1
  11. package/dist/domain/repro-layout.js +49 -18
  12. package/dist/domain/repro-layout.js.map +1 -1
  13. package/dist/domain/scaffold-layout.d.ts +4 -3
  14. package/dist/domain/scaffold-layout.d.ts.map +1 -1
  15. package/dist/domain/scaffold-layout.js +7 -4
  16. package/dist/domain/scaffold-layout.js.map +1 -1
  17. package/dist/domain/study-registry.d.ts +176 -0
  18. package/dist/domain/study-registry.d.ts.map +1 -0
  19. package/dist/domain/study-registry.js +371 -0
  20. package/dist/domain/study-registry.js.map +1 -0
  21. package/dist/ports/study-registry-codec.d.ts +11 -0
  22. package/dist/ports/study-registry-codec.d.ts.map +1 -0
  23. package/dist/ports/study-registry-codec.js +2 -0
  24. package/dist/ports/study-registry-codec.js.map +1 -0
  25. package/dist/repro-layout-rule.d.ts.map +1 -1
  26. package/dist/repro-layout-rule.js +4 -3
  27. package/dist/repro-layout-rule.js.map +1 -1
  28. package/dist/studies.d.ts +27 -0
  29. package/dist/studies.d.ts.map +1 -0
  30. package/dist/studies.js +160 -0
  31. package/dist/studies.js.map +1 -0
  32. package/dist/study-registry-rule.d.ts +17 -0
  33. package/dist/study-registry-rule.d.ts.map +1 -0
  34. package/dist/study-registry-rule.js +115 -0
  35. package/dist/study-registry-rule.js.map +1 -0
  36. package/docs/design/study-registry.md +130 -0
  37. package/docs/layout.md +47 -44
  38. package/docs/rules/paper/allowed-files-only.md +3 -2
  39. package/docs/rules/repro/allowed-files-only.md +40 -30
  40. package/docs/rules/repro/study-registered.md +162 -0
  41. package/docs/rules/study/declare-sample.md +1 -1
  42. package/docs/rules.md +2 -1
  43. package/package.json +1 -1
  44. package/skills/build-benchmark/SKILL.md +20 -1
  45. package/skills/build-benchmark/SKILL.md.spec.ts +19 -0
  46. package/skills/build-label-page/SKILL.md +7 -4
  47. package/skills/build-label-page/SKILL.md.spec.ts +6 -3
  48. package/skills/paper-pipeline/references/artifact-checklist.md +2 -1
  49. package/skills/paper-pipeline/references/pipeline-status-template.md +5 -3
  50. package/skills/run-agent-grid/SKILL.md +8 -1
  51. package/skills/run-agent-grid/SKILL.md.spec.ts +7 -0
  52. package/src/adapters/study-registry/index.test.ts +144 -0
  53. package/src/adapters/study-registry/index.ts +70 -0
  54. package/src/cli.ts +51 -2
  55. package/src/domain/paper-layout.test.ts +1 -1
  56. package/src/domain/repro-layout.test.ts +18 -9
  57. package/src/domain/repro-layout.ts +58 -18
  58. package/src/domain/scaffold-layout.ts +7 -4
  59. package/src/domain/study-registry.test.ts +657 -0
  60. package/src/domain/study-registry.ts +598 -0
  61. package/src/folder-layout-hook.test.ts +10 -0
  62. package/src/new-paper.test.ts +28 -2
  63. package/src/ports/study-registry-codec.ts +11 -0
  64. package/src/repro-layout-rule.ts +4 -3
  65. package/src/rule-cases.test.ts +41 -0
  66. package/src/studies-cli.test.ts +113 -0
  67. package/src/studies.test.ts +353 -0
  68. package/src/studies.ts +259 -0
  69. package/src/study-registry-rule.test.ts +317 -0
  70. package/src/study-registry-rule.ts +172 -0
@@ -0,0 +1,160 @@
1
+ /**
2
+ * `paperlint studies new|index <paper-dir>` — the two commands that write a paper's study registry,
3
+ * `repro/studies.json`, and its index in `repro/README.md` (docs/design/study-registry.md).
4
+ *
5
+ * new <paper-dir> "<title>" [--kind <kind>]
6
+ * the next id; `repro/<id>-<slug>/README.md` with the id and title in its frontmatter; the
7
+ * row, `status: designing`, today's date; the index regenerated
8
+ * index <paper-dir>
9
+ * the index block of `repro/README.md`, regenerated from the registry
10
+ *
11
+ * Nothing else changes a row: a study's status, result and `in_paper` are edited in `studies.json`, and
12
+ * `repro/study-registered` checks them. The prefix is the paper's own choice, so a paper without a
13
+ * registry is refused with the file to write rather than given a guessed one.
14
+ */
15
+ import { join, relative, resolve } from "node:path";
16
+ import { callerPath } from "./caller-path.js";
17
+ import { README, REPRO_DIR } from "./domain/repro-layout.js";
18
+ import { DEFAULT_KIND, STUDIES_FILE, STUDY_KINDS, addStudy, indexInSync, isStudyKind, registryText, studyReadme, withIndex, } from "./domain/study-registry.js";
19
+ import { registryOf } from "./study-registry-rule.js";
20
+ const NEW_USAGE = 'npx paperlint studies new <paper-dir> "<title>" [--kind <kind>]';
21
+ const INDEX_USAGE = "npx paperlint studies index <paper-dir>";
22
+ const SUBS = `new or index — ${NEW_USAGE} · ${INDEX_USAGE}`;
23
+ const STATUS_FILE = "PIPELINE-STATUS.md";
24
+ const refuse = (code, line) => ({ code, line });
25
+ /** The paper folder's registry, or the refusal naming why there is none to write to. */
26
+ function registryIn(p, deps) {
27
+ const r = registryOf(p.dir, deps);
28
+ const file = `${p.shown}/${REPRO_DIR}/${STUDIES_FILE}`;
29
+ switch (r.kind) {
30
+ case "absent":
31
+ return refuse(1, `${file} does not exist. Its prefix is the paper's own choice — a capital letter, then capitals or digits, one per paper (P1) — so write the file, then run this again:\n\n{ "prefix": "P1", "studies": [] }`);
32
+ case "unreadable":
33
+ return refuse(1, `${file} is not a study registry: ${r.why}`);
34
+ case "read":
35
+ return r.registry;
36
+ }
37
+ }
38
+ const isRefusal = (v) => typeof v === "object" && v !== null && "code" in v && "line" in v;
39
+ const text = (s) => new TextEncoder().encode(s);
40
+ /** The README's text; null when there is none. */
41
+ const readmeOf = (dir, files) => {
42
+ const b = files.readBytes(callerPath(join(dir, REPRO_DIR, README)));
43
+ return b === null ? null : new TextDecoder().decode(b);
44
+ };
45
+ const studiesCount = (r) => `${String(r.studies.length)} ${r.studies.length === 1 ? "study" : "studies"}`;
46
+ /**
47
+ * Write the index of `r` into the README; whether it wrote. A block already in sync is left as it
48
+ * is, so the padding a formatter gave the table is never undone.
49
+ */
50
+ function writeIndex(dir, r, files) {
51
+ const before = readmeOf(dir, files);
52
+ if (indexInSync(before, r))
53
+ return false;
54
+ files.writeAtomic(callerPath(join(dir, REPRO_DIR, README)), text(withIndex(before, r)));
55
+ return true;
56
+ }
57
+ /** The refusal when something that is not a file sits where `repro/README.md` goes; null when none. */
58
+ function readmeBlocked(p, deps) {
59
+ const listed = deps.listDir(callerPath(join(p.dir, REPRO_DIR)));
60
+ const path = callerPath(join(p.dir, REPRO_DIR, README));
61
+ return listed.includes(README) && !deps.files.isFile(path)
62
+ ? refuse(1, `${p.shown}/${REPRO_DIR}/${README} is not a file, so the index cannot be written there. Move it, then run this again.`)
63
+ : null;
64
+ }
65
+ /** `studies index`: the README's block regenerated from the registry. */
66
+ function runIndex(p, a, deps) {
67
+ const [surplus] = [...(a.title === undefined ? [] : [a.title]), ...a.extra];
68
+ if (surplus !== undefined)
69
+ return refuse(2, `studies index takes only the paper's folder — \`${surplus}\` is one argument too many`);
70
+ const r = registryIn(p, deps);
71
+ if (isRefusal(r))
72
+ return r;
73
+ const blocked = readmeBlocked(p, deps);
74
+ if (blocked !== null)
75
+ return blocked;
76
+ const readme = `${p.shown}/${REPRO_DIR}/${README}`;
77
+ return [
78
+ writeIndex(p.dir, r, deps.files)
79
+ ? `wrote the index of ${studiesCount(r)} in ${readme}`
80
+ : `the index in ${readme} is in sync with ${studiesCount(r)}`,
81
+ ];
82
+ }
83
+ /** `new`'s title and kind from the command line, or the refusal. */
84
+ function newArgs(a) {
85
+ if (a.title === undefined)
86
+ return refuse(2, `studies new needs the study's title: ${NEW_USAGE}`);
87
+ const [surplus] = a.extra;
88
+ if (surplus !== undefined)
89
+ return refuse(2, `studies new takes the paper's folder and one title — \`${surplus}\` is one argument too many (quote a title of several words)`);
90
+ const kind = a.kind ?? DEFAULT_KIND;
91
+ return isStudyKind(kind)
92
+ ? { title: a.title, kind }
93
+ : refuse(2, `--kind ${kind} is not a kind of study: one of ${STUDY_KINDS.join(", ")}`);
94
+ }
95
+ /** Write a new study's README.md, the registry, and the index; the lines that say so. */
96
+ function writeStudy(p, added, files) {
97
+ const { study, registry } = added;
98
+ const at = (...names) => callerPath(join(p.dir, REPRO_DIR, ...names));
99
+ files.writeAtomic(at(study.folder, README), text(studyReadme(study)));
100
+ files.writeAtomic(at(STUDIES_FILE), text(registryText(registry)));
101
+ writeIndex(p.dir, registry, files);
102
+ const repro = `${p.shown}/${REPRO_DIR}`;
103
+ return [
104
+ `created ${repro}/${study.folder}/${README}`,
105
+ `added ${study.id} to ${repro}/${STUDIES_FILE}: kind ${study.kind}, status ${study.status}, ${study.date}`,
106
+ `wrote the index of ${studiesCount(registry)} in ${repro}/${README}`,
107
+ `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}\`.`,
108
+ ];
109
+ }
110
+ /** `studies new`: the folder, its README.md, the row, the index. */
111
+ function runNew(p, a, deps) {
112
+ const args = newArgs(a);
113
+ if (isRefusal(args))
114
+ return args;
115
+ const r = registryIn(p, deps);
116
+ if (isRefusal(r))
117
+ return r;
118
+ const blocked = readmeBlocked(p, deps);
119
+ if (blocked !== null)
120
+ return blocked;
121
+ const added = addStudy(r, { ...args, date: deps.today });
122
+ if (!added.ok)
123
+ return refuse(2, added.why);
124
+ const folder = added.study.folder;
125
+ return deps.listDir(callerPath(join(p.dir, REPRO_DIR))).includes(folder)
126
+ ? refuse(1, `${p.shown}/${REPRO_DIR}/${folder}/ already exists; it is never overwritten. Register it by hand, or remove it and run this again.`)
127
+ : writeStudy(p, added, deps.files);
128
+ }
129
+ /** The paper folder the command line names, or the refusal. */
130
+ function paperOf(a, deps) {
131
+ if (a.sub === undefined)
132
+ return refuse(2, `studies needs a subcommand: ${SUBS}`);
133
+ if (a.sub !== "new" && a.sub !== "index")
134
+ return refuse(2, `unknown subcommand \`studies ${a.sub}\`: ${SUBS}`);
135
+ if (a.paperDir === undefined)
136
+ return refuse(2, `studies ${a.sub} needs the paper's folder: ${a.sub === "new" ? NEW_USAGE : INDEX_USAGE}`);
137
+ const dir = resolve(deps.cwd, a.paperDir);
138
+ const shown = relative(deps.cwd, dir) || ".";
139
+ return deps.files.isFile(callerPath(join(dir, STATUS_FILE)))
140
+ ? { dir, shown }
141
+ : refuse(2, `${shown} is not a paper folder: it has no ${STATUS_FILE}`);
142
+ }
143
+ /** The run: 0 done, 1 refused by the paper's state, 2 refused by the command line. */
144
+ export function runStudies(a, deps) {
145
+ const p = paperOf(a, deps);
146
+ const out = isRefusal(p)
147
+ ? p
148
+ : a.sub === "new"
149
+ ? runNew(p, a, deps)
150
+ : runIndex(p, a, deps);
151
+ if (isRefusal(out)) {
152
+ deps.err(out.line);
153
+ return out.code;
154
+ }
155
+ out.forEach((line) => {
156
+ deps.log(line);
157
+ });
158
+ return 0;
159
+ }
160
+ //# sourceMappingURL=studies.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"studies.js","sourceRoot":"","sources":["../src/studies.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import { type RegistryState, type StudyFinding } from "./domain/study-registry.ts";
2
+ import type { FolderLayoutDeps } from "./folder-layout-rule.ts";
3
+ import type { FolderRuleModule } from "./folder-venue-rule.ts";
4
+ import type { StudyRegistryCodec } from "./ports/study-registry-codec.ts";
5
+ /** What the rule reads with: the disk, its listings, and the registry's parser. */
6
+ export interface StudyRegistryDeps extends FolderLayoutDeps {
7
+ readonly codec: StudyRegistryCodec;
8
+ }
9
+ /** The registry of the paper folder `dir`: absent, unreadable with why, or read. */
10
+ export declare function registryOf(dir: string, deps: Pick<StudyRegistryDeps, "files" | "codec">): RegistryState;
11
+ /** Every finding of the paper folder `dir`'s study registry — THE function behind the rule. */
12
+ export declare function studyFindings(dir: string, deps: StudyRegistryDeps): readonly StudyFinding[];
13
+ /** The rule, reading the folder through `deps`. */
14
+ export declare function studyRegistryRules(deps: StudyRegistryDeps): Readonly<Record<"study-registered", FolderRuleModule>>;
15
+ /** The level it is on at in paperlint's own config, for every paper's `PIPELINE-STATUS.md`. */
16
+ export declare const STUDY_REGISTRY_RULE_LEVELS: Readonly<Record<"repro/study-registered", "error">>;
17
+ //# sourceMappingURL=study-registry-rule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"study-registry-rule.d.ts","sourceRoot":"","sources":["../src/study-registry-rule.ts"],"names":[],"mappings":"AA0BA,OAAO,EAIL,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,KAAK,EAEV,gBAAgB,EACjB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAM1E,mFAAmF;AACnF,MAAM,WAAW,iBAAkB,SAAQ,gBAAgB;IACzD,QAAQ,CAAC,KAAK,EAAE,kBAAkB,CAAC;CACpC;AAQD,oFAAoF;AACpF,wBAAgB,UAAU,CACxB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,IAAI,CAAC,iBAAiB,EAAE,OAAO,GAAG,OAAO,CAAC,GAC/C,aAAa,CAOf;AAgCD,+FAA+F;AAC/F,wBAAgB,aAAa,CAC3B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,iBAAiB,GACtB,SAAS,YAAY,EAAE,CAQzB;AA+BD,mDAAmD;AACnD,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,iBAAiB,GACtB,QAAQ,CAAC,MAAM,CAAC,kBAAkB,EAAE,gBAAgB,CAAC,CAAC,CAqBxD;AAED,+FAA+F;AAC/F,eAAO,MAAM,0BAA0B,EAAE,QAAQ,CAC/C,MAAM,CAAC,wBAAwB,EAAE,OAAO,CAAC,CACF,CAAC"}
@@ -0,0 +1,115 @@
1
+ /**
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, and the index in `repro/README.md` is the
4
+ * one the registry generates.
5
+ *
6
+ * 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 and the result files, and reports.
8
+ *
9
+ * ── WHY A RULE OF ITS OWN, NOT MORE OF `repro/allowed-files-only` ─────────────────────
10
+ * `repro/allowed-files-only` judges what may stand at the top of `repro/`, by name, and accepts the
11
+ * dated `<slug>-YYYY-MM-DD` folders of a paper that predates the registry. This rule judges the
12
+ * registry against the folders. A paper that keeps its dated folders turns this one off and keeps
13
+ * its layout judged.
14
+ *
15
+ * ── WHICH FILE IT REPORTS ON ─────────────────────────────────────────────────────
16
+ * `PIPELINE-STATUS.md`, first line, like the layout rules: every paper has it, and ESLint lints no
17
+ * file in `repro/`.
18
+ */
19
+ import { basename, dirname, join } from "node:path";
20
+ import { callerPath } from "./caller-path.js";
21
+ import { README, REPRO_BYPRODUCTS, REPRO_DIR, REPRO_DIRS, } from "./domain/repro-layout.js";
22
+ import { STUDIES_FILE, judgeStudies, studyMessage, } from "./domain/study-registry.js";
23
+ import { reproEntriesOf } from "./repro-layout-rule.js";
24
+ import { rulePageUrl } from "./venue-rules.js";
25
+ const STATUS_FILE = "PIPELINE-STATUS.md";
26
+ /** The text of the file at `p`; null when there is none, or a directory sits there. */
27
+ const textAt = (files, p) => {
28
+ const b = files.isFile(callerPath(p)) ? files.readBytes(callerPath(p)) : null;
29
+ return b === null ? null : new TextDecoder().decode(b);
30
+ };
31
+ /** The registry of the paper folder `dir`: absent, unreadable with why, or read. */
32
+ export function registryOf(dir, deps) {
33
+ const text = textAt(deps.files, join(dir, REPRO_DIR, STUDIES_FILE));
34
+ if (text === null)
35
+ return { kind: "absent" };
36
+ const parsed = deps.codec.parse(text);
37
+ return parsed.ok
38
+ ? { kind: "read", registry: parsed.registry }
39
+ : { kind: "unreadable", why: parsed.why };
40
+ }
41
+ /** The study folders at the top of `<dir>/repro/`: every folder but the shared ones and the byproducts. */
42
+ const studyFolders = (dir, deps) => reproEntriesOf(dir, deps)
43
+ .filter((e) => e.kind === "dir" &&
44
+ !REPRO_DIRS.includes(e.name) &&
45
+ !REPRO_BYPRODUCTS.includes(e.name))
46
+ .map((e) => e.name);
47
+ /** Each `<folder>/<result_file>` of `r` that is a file. */
48
+ const existingResults = (dir, r, files) => new Set((r.kind === "read" ? r.registry.studies : [])
49
+ .flatMap((s) => s.result_file === undefined ? [] : [`${s.folder}/${s.result_file}`])
50
+ // A NUL is no path the disk can hold, and probing one throws: such a row reads as missing.
51
+ .filter((p) => !p.includes("\0") &&
52
+ files.isFile(callerPath(join(dir, REPRO_DIR, p)))));
53
+ /** Every finding of the paper folder `dir`'s study registry — THE function behind the rule. */
54
+ export function studyFindings(dir, deps) {
55
+ const registry = registryOf(dir, deps);
56
+ return judgeStudies({
57
+ folders: studyFolders(dir, deps),
58
+ registry,
59
+ readme: textAt(deps.files, join(dir, REPRO_DIR, README)),
60
+ existingResults: existingResults(dir, registry, deps.files),
61
+ });
62
+ }
63
+ /** The problems a finding can be, each its own message id; the text is `studyMessage`. */
64
+ const PROBLEMS = [
65
+ "no-registry",
66
+ "unreadable",
67
+ "unregistered",
68
+ "no-folder",
69
+ "two-rows",
70
+ "bad-id",
71
+ "duplicate-id",
72
+ "folder-name",
73
+ "long-title",
74
+ "no-result",
75
+ "result-file",
76
+ "no-successor",
77
+ "stale-index",
78
+ ];
79
+ const META = {
80
+ type: "suggestion",
81
+ docs: {
82
+ description: "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",
83
+ url: rulePageUrl("repro/study-registered"),
84
+ },
85
+ schema: [],
86
+ // The whole text is `studyMessage` (src/domain/study-registry.ts).
87
+ messages: Object.fromEntries(PROBLEMS.map((p) => [p, "{{text}}"])),
88
+ };
89
+ /** The rule, reading the folder through `deps`. */
90
+ export function studyRegistryRules(deps) {
91
+ return {
92
+ "study-registered": {
93
+ meta: META,
94
+ create(context) {
95
+ if (basename(context.filename) !== STATUS_FILE)
96
+ return {};
97
+ return {
98
+ root() {
99
+ const at = context.sourceCode.getLocFromIndex(0);
100
+ studyFindings(dirname(context.filename), deps).forEach((f) => {
101
+ context.report({
102
+ loc: { start: at, end: at },
103
+ messageId: f.problem,
104
+ data: { text: studyMessage(f) },
105
+ });
106
+ });
107
+ },
108
+ };
109
+ },
110
+ },
111
+ };
112
+ }
113
+ /** The level it is on at in paperlint's own config, for every paper's `PIPELINE-STATUS.md`. */
114
+ export const STUDY_REGISTRY_RULE_LEVELS = { "repro/study-registered": "error" };
115
+ //# sourceMappingURL=study-registry-rule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"study-registry-rule.js","sourceRoot":"","sources":["../src/study-registry-rule.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,130 @@
1
+ # The study registry — design
2
+
3
+ A paper's `repro/` holds one folder per study. Named `<slug>-YYYY-MM-DD`, they sort by date and say
4
+ nothing else: a real paper reached 79 study folders, and nobody could tell which one a number in the
5
+ paper came from, which ones had ended, or which were current. This note fixes the shape of the
6
+ registry that gives every study a stable number and a plain name, the lint rule that holds it, and the
7
+ two commands that write it.
8
+
9
+ ## What is decided
10
+
11
+ 1. **One registry, `repro/studies.json`, JSON with a schema.** Never markdown: a table in a README is
12
+ what people edit by hand and what a parser has to guess at. The schema (zod, in
13
+ `src/adapters/study-registry/`) is checked where the file is read; nothing after that re-validates.
14
+
15
+ ```json
16
+ {
17
+ "prefix": "P1",
18
+ "studies": [
19
+ {
20
+ "id": "P1-S01",
21
+ "folder": "P1-S01-baseline-pass-rate",
22
+ "title": "Baseline pass rate",
23
+ "question": "How often does the unmodified agent pass the task?",
24
+ "kind": "measurement",
25
+ "status": "done",
26
+ "date": "2026-10-15",
27
+ "result": "41 of 60 runs pass",
28
+ "result_file": "results.tsv",
29
+ "in_paper": "yes"
30
+ }
31
+ ]
32
+ }
33
+ ```
34
+
35
+ | field | values |
36
+ | --------------- | ------------------------------------------------------------------------- |
37
+ | `prefix` | the paper's prefix: a capital letter, then capitals or digits (`P1`) |
38
+ | `id` | `<prefix>-S<two or more digits>`, unique |
39
+ | `folder` | `<id>-<slug>`: the study's folder at the top of `repro/` |
40
+ | `title` | a plain name, at most 8 words |
41
+ | `question` | one sentence: what the study asks |
42
+ | `kind` | `measurement`, `spike`, `tooling`, `data`, `artifact`, `salvage` |
43
+ | `status` | `designing`, `running`, `done`, `stopped`, `superseded`, `dropped` |
44
+ | `date` | `YYYY-MM-DD`, the day it was registered |
45
+ | `result` | optional; one line. Required when `done` |
46
+ | `result_file` | optional; a path inside the folder. Required, and must exist, when `done` |
47
+ | `superseded_by` | optional; an id in the registry. Required when `superseded` |
48
+ | `in_paper` | `yes`, `no`, `registry` (listed only in the registry, not in the text) |
49
+
50
+ **A row may carry more fields**, and they are kept: a paper merges its own measurement metadata
51
+ into the rows (`role`, `frame`, `method`, `outcome`, …). The top level is closed: a misspelt
52
+ `prefix` is an error, not an ignored key.
53
+
54
+ 2. **The rule `repro/study-registered`, an error,** reported on the first line of
55
+ `PIPELINE-STATUS.md` like `repro/allowed-files-only`, once per finding:
56
+
57
+ - every folder at the top of `repro/` that is not `tools/`, `data/` or a byproduct the layout
58
+ ignores (`__pycache__/`, `node_modules/`, …) has exactly one row, and every row's folder exists;
59
+ - the folder is `<id>-<slug>`, the id is `<prefix>-S<digits>`, ids are unique;
60
+ - `done` has a `result` and an existing `result_file`; `superseded` names an existing id in
61
+ `superseded_by`; a title has at most 8 words;
62
+ - **the index is in sync:** the block of `repro/README.md` between the two markers below equals the
63
+ text generated from `studies.json`. The comparison is of strings. The markdown is output only and
64
+ is never parsed.
65
+
66
+ ```
67
+ <!-- studies:begin — generated from studies.json by `paperlint studies index`; do not edit -->
68
+ <!-- studies:end -->
69
+ ```
70
+
71
+ - a paper with study folders and no `repro/studies.json` gets one finding naming the file.
72
+ - a `studies.json` that is not JSON, or not of the schema, gets one finding with the reasons.
73
+
74
+ 3. **Two commands**, in `src/studies.ts`, wired in `src/cli.ts`:
75
+
76
+ - `paperlint studies new <paper-dir> "<title>" [--kind measurement]` takes the next id, creates
77
+ `repro/<id>-<slug>/README.md` (frontmatter `id:` and `title:`), appends the row with
78
+ `status: designing`, `in_paper: no` and today's date, and regenerates the index block. It refuses a
79
+ title over 8 words, a title with no letter or digit to make a slug from, an unknown kind, a folder
80
+ that already exists, and a paper with no `studies.json` (the prefix is the author's choice, so the
81
+ refusal names the file to write and does not guess one).
82
+ - `paperlint studies index <paper-dir>` regenerates the index block: a table with the columns id ·
83
+ title · status · result · in paper · folder. With no markers in `repro/README.md` the block is
84
+ appended; with no `repro/README.md` the file is written.
85
+
86
+ 4. **The layout accepts `<id>-<slug>` beside `<slug>-YYYY-MM-DD`.** `repro/allowed-files-only` keeps
87
+ accepting the dated form, so a paper that turns the registry off is not broken by this change; it
88
+ also allows `studies.json` at the top of `repro/`. A folder in the dated form is still reported by
89
+ `repro/study-registered` as unregistered — that is the point of the registry — and the paper decides
90
+ whether to register it or turn the rule off.
91
+
92
+ 5. **Skills.** `build-benchmark`, `run-agent-grid` and `build-label-page` create a study only with
93
+ `paperlint studies new`, and update its row (status, result, result file, in paper) when it ends,
94
+ then run `paperlint studies index`.
95
+
96
+ ## Where each part lives
97
+
98
+ | part | file | layer |
99
+ | --------------------------------------- | ----------------------------------- | ------- |
100
+ | the types, the judge, the index, ids | `src/domain/study-registry.ts` | domain |
101
+ | the schema (zod), parsing the text | `src/adapters/study-registry/` | adapter |
102
+ | what the app asks of a parser | `src/ports/study-registry-codec.ts` | port |
103
+ | the rule: list the folder, read, report | `src/study-registry-rule.ts` | app |
104
+ | the two commands | `src/studies.ts` | app |
105
+ | wiring, the clock for today's date | `src/cli.ts` | root |
106
+
107
+ The judge is pure: it takes the folder names, the parsed registry (or why it is absent or unreadable),
108
+ the README's text, and which result files exist, and returns findings. The rule and the commands are
109
+ the only places that touch the disk, through the `Files` and `ListDir` ports.
110
+
111
+ ## Generated index, and prettier
112
+
113
+ The index is a table whose columns are padded to their widest cell, the way prettier writes a
114
+ markdown table, so formatting `repro/README.md` with prettier does not make the block stale. A `|` in
115
+ a cell is escaped and a line break becomes a space.
116
+
117
+ ## Rejected, by the author on 2026-10-15
118
+
119
+ - Commands that change a study's status: the row is edited in `studies.json`, the rule checks it.
120
+ - Detecting automatically whether a study is cited in the paper: `in_paper` is declared.
121
+ - An adopt or migration command for existing folders: there are no other users; the folders are
122
+ renamed by hand once.
123
+ - A PreToolUse hook for the registry: the lint is the guarantee, and the layout hook already refuses
124
+ a loose entry.
125
+
126
+ ## What it does not check
127
+
128
+ - Whether the `question` is one sentence, or the `result` one line, beyond their being strings.
129
+ - What a study's `README.md` says, or whether its frontmatter matches the row.
130
+ - Files inside a study folder other than `result_file`.