paperlint 10.1.0 → 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 (72) 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/render-paper/check-render.sh +9 -0
  51. package/skills/render-paper/render-paper.harness.mjs +102 -0
  52. package/skills/run-agent-grid/SKILL.md +8 -1
  53. package/skills/run-agent-grid/SKILL.md.spec.ts +7 -0
  54. package/src/adapters/study-registry/index.test.ts +144 -0
  55. package/src/adapters/study-registry/index.ts +70 -0
  56. package/src/cli.ts +51 -2
  57. package/src/domain/paper-layout.test.ts +1 -1
  58. package/src/domain/repro-layout.test.ts +18 -9
  59. package/src/domain/repro-layout.ts +58 -18
  60. package/src/domain/scaffold-layout.ts +7 -4
  61. package/src/domain/study-registry.test.ts +657 -0
  62. package/src/domain/study-registry.ts +598 -0
  63. package/src/folder-layout-hook.test.ts +10 -0
  64. package/src/new-paper.test.ts +28 -2
  65. package/src/ports/study-registry-codec.ts +11 -0
  66. package/src/repro-layout-rule.ts +4 -3
  67. package/src/rule-cases.test.ts +41 -0
  68. package/src/studies-cli.test.ts +113 -0
  69. package/src/studies.test.ts +353 -0
  70. package/src/studies.ts +259 -0
  71. package/src/study-registry-rule.test.ts +317 -0
  72. package/src/study-registry-rule.ts +172 -0
@@ -7,15 +7,21 @@
7
7
  * where a new run goes. The layout gives each kind of material one place:
8
8
  *
9
9
  * repro/
10
- * ├── README.md the index: one line per study
11
- * ├── <slug>-YYYY-MM-DD/ one folder per study or spike, with a README.md inside
10
+ * ├── README.md the index: one line per study, generated from studies.json
11
+ * ├── studies.json the study registry: one row per study (src/domain/study-registry.ts)
12
+ * ├── <id>-<slug>/ one folder per study or spike, named by its row, with a README.md inside
12
13
  * ├── tools/ scripts used across the paper (numbers, figures, anonymity check, harnesses)
13
14
  * └── data/ shared data (tables, caches)
14
15
  *
15
16
  * plus `.gitignore`. A loose note belongs in its study's folder or in the paper's `research/`.
16
17
  *
17
- * ── THE DATE IN A STUDY'S NAME ────────────────────────────────────────────────────
18
- * `<slug>-YYYY-MM-DD` is the day the study began. `<slug>-YYYY-MM` is accepted too: a study that ran
18
+ * ── A STUDY'S NAME ───────────────────────────────────────────────────────────────
19
+ * `<id>-<slug>` (`P1-S03-baseline`): the id of the study's row in `repro/studies.json` — the paper's
20
+ * prefix, `-S`, two or more digits — then the slug. Whether the id has a row, and the row this folder,
21
+ * is `repro/study-registered`'s question; this layout only accepts the shape.
22
+ *
23
+ * The older form, `<slug>-YYYY-MM-DD`, stays accepted, so a paper that turned the registry off is not
24
+ * broken: the date is the day the study began. `<slug>-YYYY-MM` is accepted too: a study that ran
19
25
  * over weeks (labelling, a reading of a corpus) has a month as its honest date, and both forms sort
20
26
  * by time. The date must be a real one (no month 13, no 30 February). The slug is lowercase letters
21
27
  * and digits in words joined by single hyphens.
@@ -32,6 +38,9 @@ import { leaf, renderTree, type TreeNode } from "./layout-tree.ts";
32
38
  /** The folder this layout governs, at the paper folder's root. */
33
39
  export const REPRO_DIR = "repro";
34
40
 
41
+ /** The study registry, at the top of `repro/` (src/domain/study-registry.ts). */
42
+ export const STUDIES_FILE = "studies.json";
43
+
35
44
  /** The index at the top of `repro/`, and the file every study folder holds. */
36
45
  export const README = "README.md";
37
46
 
@@ -40,7 +49,14 @@ export const REPRO_FILE_NOTES: readonly {
40
49
  readonly name: string;
41
50
  readonly note: string;
42
51
  }[] = [
43
- { name: README, note: "the index: one line per study" },
52
+ {
53
+ name: README,
54
+ note: "the index: one line per study, generated from studies.json",
55
+ },
56
+ {
57
+ name: STUDIES_FILE,
58
+ note: "the study registry: id, title, status and result of each study",
59
+ },
44
60
  { name: ".gitignore", note: "keeps the scripts' byproducts out of git" },
45
61
  ];
46
62
  export const REPRO_FILES: readonly string[] = REPRO_FILE_NOTES.map(
@@ -64,8 +80,8 @@ export const REPRO_DIRS: readonly string[] = REPRO_DIR_NOTES.map((d) => d.name);
64
80
 
65
81
  /** The study folder as the layout draws it: the name pattern, and the README.md it must hold. */
66
82
  const STUDY_NODE: TreeNode = {
67
- name: "<slug>-YYYY-MM-DD/",
68
- note: "one folder per study or spike (<slug>-YYYY-MM for one that ran over weeks)",
83
+ name: "<id>-<slug>/",
84
+ note: "one folder per study or spike, named by its row in studies.json (P1-S03-baseline)",
69
85
  children: [
70
86
  leaf(README, "required: what it asks, how to run it, what it found"),
71
87
  ],
@@ -100,9 +116,20 @@ export const REPRO_BYPRODUCTS: readonly string[] = [
100
116
  "Thumbs.db",
101
117
  ];
102
118
 
103
- /** A study's name: a slug, then the year, the month, and optionally the day it began. */
119
+ /** A slug: lowercase letters and digits, in words joined by single hyphens. */
120
+ export const STUDY_SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
121
+
122
+ /** A dated study's name: a slug, then the year, the month, and optionally the day it began. */
104
123
  const STUDY_NAME = /^([a-z0-9]+(?:-[a-z0-9]+)*)-(\d{4})-(\d{2})(?:-(\d{2}))?$/u;
105
124
 
125
+ /** A registered study's name: the id (a prefix, `-S`, two or more digits), then the slug. */
126
+ const REGISTERED_STUDY_NAME =
127
+ /^[A-Z][A-Z0-9]*-S\d{2,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
128
+
129
+ /** Whether `name` is a registered study folder's: `<id>-<slug>`, as `P1-S03-baseline`. */
130
+ export const isRegisteredStudyName = (name: string): boolean =>
131
+ REGISTERED_STUDY_NAME.test(name);
132
+
106
133
  const isLeapYear = (y: number): boolean =>
107
134
  (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0;
108
135
 
@@ -110,8 +137,8 @@ const isLeapYear = (y: number): boolean =>
110
137
  const daysIn = (y: number, m: number): number =>
111
138
  m === 2 ? (isLeapYear(y) ? 29 : 28) : [4, 6, 9, 11].includes(m) ? 30 : 31;
112
139
 
113
- /** Whether `name` is a study folder's: `<slug>-YYYY-MM-DD` or `<slug>-YYYY-MM`, a real date. */
114
- export function isStudyName(name: string): boolean {
140
+ /** Whether `name` is a dated study folder's: `<slug>-YYYY-MM-DD` or `<slug>-YYYY-MM`, a real date. */
141
+ function isDatedStudyName(name: string): boolean {
115
142
  const m = STUDY_NAME.exec(name);
116
143
  if (m === null) return false;
117
144
  const year = Number(m[2]);
@@ -120,6 +147,10 @@ export function isStudyName(name: string): boolean {
120
147
  return month >= 1 && month <= 12 && day >= 1 && day <= daysIn(year, month);
121
148
  }
122
149
 
150
+ /** Whether `name` is a study folder's: `<id>-<slug>`, or the dated `<slug>-YYYY-MM-DD` / `<slug>-YYYY-MM`. */
151
+ export const isStudyName = (name: string): boolean =>
152
+ isRegisteredStudyName(name) || isDatedStudyName(name);
153
+
123
154
  /**
124
155
  * The study name to suggest for a folder named `name`: its name as a slug, with the date to fill in.
125
156
  * A name that becomes a study name once lowercased and hyphenated is suggested as it is.
@@ -129,9 +160,18 @@ export function suggestedStudyName(name: string): string {
129
160
  .toLowerCase()
130
161
  .replace(/[^a-z0-9]+/gu, "-")
131
162
  .replace(/^-+|-+$/gu, "");
132
- if (isStudyName(slug)) return slug;
133
- const base = slug.replace(/(?:^|-)\d{4}(?:-\d{1,2}){0,2}$/u, "");
134
- return `${base === "" ? "<slug>" : base}-YYYY-MM-DD`;
163
+ if (isDatedStudyName(slug)) return slug;
164
+ return `${suggestedSlug(name)}-YYYY-MM-DD`;
165
+ }
166
+
167
+ /** The slug to suggest for a folder named `name`: its name as a slug, any trailing date dropped. */
168
+ export function suggestedSlug(name: string): string {
169
+ const base = name
170
+ .toLowerCase()
171
+ .replace(/[^a-z0-9]+/gu, "-")
172
+ .replace(/^-+|-+$/gu, "")
173
+ .replace(/(?:^|-)\d{4}(?:-\d{1,2}){0,2}$/u, "");
174
+ return base === "" ? "<slug>" : base;
135
175
  }
136
176
 
137
177
  /** The extensions of a script: what a study or the paper's build runs. */
@@ -203,11 +243,11 @@ export const DATA_CURE =
203
243
 
204
244
  /** Where a note goes. */
205
245
  export const NOTE_CURE =
206
- "the dated folder of the study it is about (repro/<slug>-YYYY-MM-DD/), or the paper's research/ when it is research or a decision rather than the record of a run";
246
+ "the folder of the study it is about (repro/<id>-<slug>/), or the paper's research/ when it is research or a decision rather than the record of a run";
207
247
 
208
248
  /** Where a file of any other kind goes. */
209
249
  export const FILE_CURE_BY_KIND =
210
- "decide by what it is — a script → repro/tools/; data → repro/data/; a note → the dated folder of its study, or the paper's research/; what one study produced → that study's folder";
250
+ "decide by what it is — a script → repro/tools/; data → repro/data/; a note → the folder of its study, or the paper's research/; what one study produced → that study's folder";
211
251
 
212
252
  /** The files paperlint itself once kept at the top of `repro/`, each with where it is read now. */
213
253
  export const MOVED_FILES: readonly {
@@ -242,7 +282,7 @@ function fileCure(name: string): string {
242
282
 
243
283
  /** Where a folder with no study name goes: renamed as a study, or emptied into the shared folders. */
244
284
  const folderCure = (name: string): string =>
245
- `rename it to \`${suggestedStudyName(name)}\`, one folder per study or spike, named for what it is and the date it began — the date of its first commit is a fine default (\`git log --reverse --format=%as -- repro/${name} | head -1\`, run in the paper folder) — and give it a README.md; if it holds scripts or data shared across the paper instead, move them into repro/tools/ or repro/data/`;
285
+ `rename it to \`<id>-${suggestedSlug(name)}\` — the next free id in repro/${STUDIES_FILE}, then what it is — give it its row there and a README.md (\`npx paperlint studies new\` makes all three for a new study); in a paper with repro/study-registered off, \`${suggestedStudyName(name)}\` is accepted instead, dated by the day it began — its first commit is a fine default (\`git log --reverse --format=%as -- repro/${name} | head -1\`, run in the paper folder); if it holds scripts or data shared across the paper instead, move them into repro/tools/ or repro/data/`;
246
286
 
247
287
  /** An entry at the top of `repro/`: a file or a folder, and for a folder whether it holds a README.md. */
248
288
  export interface ReproEntry {
@@ -280,7 +320,7 @@ function judgeEntry(e: ReproEntry): ReproFinding | null {
280
320
  return {
281
321
  path,
282
322
  problem: "no-readme",
283
- cure: `add ${path}${README} — what the study asks, how to run it, what it found — and a line for it in repro/${README}`,
323
+ cure: `add ${path}${README} — what the study asks, how to run it, what it found — and give the study its row in repro/${STUDIES_FILE}`,
284
324
  };
285
325
  return null;
286
326
  }
@@ -300,7 +340,7 @@ export function judgeRepro(
300
340
  export function reproMessage(f: ReproFinding): string {
301
341
  const head =
302
342
  f.problem === "stray"
303
- ? `\`${f.path}\` is loose at the top of the paper's repro/ folder, which holds only an index, the shared tools and data, and one dated folder per study. Where it goes: ${f.cure}.`
343
+ ? `\`${f.path}\` is loose at the top of the paper's repro/ folder, which holds only an index, the study registry, the shared tools and data, and one folder per study. Where it goes: ${f.cure}.`
304
344
  : `\`${f.path}\` is a study folder without a ${README}, and every study in repro/ has one. What to do: ${f.cure}.`;
305
345
  return [
306
346
  head,
@@ -70,13 +70,16 @@ export const SCAFFOLD_DIRS: readonly string[] = ROOT_DIR_NOTES.map(
70
70
  ).filter((name) => !NOT_SCAFFOLDED.some((n) => n.name === name));
71
71
 
72
72
  /**
73
- * The stub index of `repro/`: it makes the folder non-empty (no `.gitkeep`) and says what the rule
74
- * asks of it. A line per study is added when the first study is.
73
+ * The stub index of `repro/`: it makes the folder non-empty (no `.gitkeep`) and says what the rules
74
+ * ask of it. The table of studies is generated into it by `paperlint studies new` and
75
+ * `paperlint studies index`, from `repro/studies.json`.
75
76
  */
76
77
  export const REPRO_INDEX_STUB = `# Reproduction index
77
78
 
78
- One line per study: \`<slug>-YYYY-MM-DD/\` — what it asks and what it found. Scripts used across the
79
- paper go in \`${TOOLS_DIR}/\`, shared data in \`${DATA_DIR}/\`.
79
+ One line per study, generated from \`studies.json\` — create a study with
80
+ \`npx paperlint studies new <paper-dir> "<title>"\`, and after editing its row run
81
+ \`npx paperlint studies index <paper-dir>\`. Scripts used across the paper go in \`${TOOLS_DIR}/\`,
82
+ shared data in \`${DATA_DIR}/\`.
80
83
  `;
81
84
 
82
85
  /** A file `paperlint new` writes into a new paper folder, with its path from that folder. */