paperlint 10.3.0 → 10.4.1

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 (187) hide show
  1. package/.github/workflows/ci.yml +15 -4
  2. package/README.md +1 -0
  3. package/dist/CLAUDE.md.spec.d.ts.map +1 -1
  4. package/dist/CLAUDE.md.spec.js +6 -3
  5. package/dist/CLAUDE.md.spec.js.map +1 -1
  6. package/dist/adapters/git/index.d.ts +10 -0
  7. package/dist/adapters/git/index.d.ts.map +1 -0
  8. package/dist/adapters/git/index.js +151 -0
  9. package/dist/adapters/git/index.js.map +1 -0
  10. package/dist/adapters/latex/captions.d.ts +9 -0
  11. package/dist/adapters/latex/captions.d.ts.map +1 -0
  12. package/dist/adapters/latex/captions.js +40 -0
  13. package/dist/adapters/latex/captions.js.map +1 -0
  14. package/dist/adapters/latex/index.d.ts +1 -0
  15. package/dist/adapters/latex/index.d.ts.map +1 -1
  16. package/dist/adapters/latex/index.js +1 -0
  17. package/dist/adapters/latex/index.js.map +1 -1
  18. package/dist/adapters/latex/parse.d.ts.map +1 -1
  19. package/dist/adapters/latex/parse.js +6 -0
  20. package/dist/adapters/latex/parse.js.map +1 -1
  21. package/dist/adapters/memory/index.d.ts +6 -0
  22. package/dist/adapters/memory/index.d.ts.map +1 -1
  23. package/dist/adapters/memory/index.js +8 -0
  24. package/dist/adapters/memory/index.js.map +1 -1
  25. package/dist/adapters/node/index.d.ts +1 -0
  26. package/dist/adapters/node/index.d.ts.map +1 -1
  27. package/dist/adapters/node/index.js +1 -0
  28. package/dist/adapters/node/index.js.map +1 -1
  29. package/dist/adapters/node/list-entries.io.d.ts +3 -0
  30. package/dist/adapters/node/list-entries.io.d.ts.map +1 -0
  31. package/dist/adapters/node/list-entries.io.js +26 -0
  32. package/dist/adapters/node/list-entries.io.js.map +1 -0
  33. package/dist/adapters/study-registry/index.d.ts +70 -10
  34. package/dist/adapters/study-registry/index.d.ts.map +1 -1
  35. package/dist/adapters/study-registry/index.js +84 -10
  36. package/dist/adapters/study-registry/index.js.map +1 -1
  37. package/dist/cli.d.ts.map +1 -1
  38. package/dist/cli.js +33 -3
  39. package/dist/cli.js.map +1 -1
  40. package/dist/domain/decide-first.d.ts +110 -0
  41. package/dist/domain/decide-first.d.ts.map +1 -0
  42. package/dist/domain/decide-first.js +108 -0
  43. package/dist/domain/decide-first.js.map +1 -0
  44. package/dist/domain/index-columns.d.ts +45 -0
  45. package/dist/domain/index-columns.d.ts.map +1 -0
  46. package/dist/domain/index-columns.js +64 -0
  47. package/dist/domain/index-columns.js.map +1 -0
  48. package/dist/domain/study-mode.d.ts +37 -0
  49. package/dist/domain/study-mode.d.ts.map +1 -0
  50. package/dist/domain/study-mode.js +45 -0
  51. package/dist/domain/study-mode.js.map +1 -0
  52. package/dist/domain/study-registry.d.ts +116 -12
  53. package/dist/domain/study-registry.d.ts.map +1 -1
  54. package/dist/domain/study-registry.js +164 -44
  55. package/dist/domain/study-registry.js.map +1 -1
  56. package/dist/outputs-before-decision-rule.d.ts +17 -0
  57. package/dist/outputs-before-decision-rule.d.ts.map +1 -0
  58. package/dist/outputs-before-decision-rule.js +94 -0
  59. package/dist/outputs-before-decision-rule.js.map +1 -0
  60. package/dist/ports/git-history.d.ts +44 -0
  61. package/dist/ports/git-history.d.ts.map +1 -0
  62. package/dist/ports/git-history.js +2 -0
  63. package/dist/ports/git-history.js.map +1 -0
  64. package/dist/ports/list-entries.d.ts +20 -0
  65. package/dist/ports/list-entries.d.ts.map +1 -0
  66. package/dist/ports/list-entries.js +2 -0
  67. package/dist/ports/list-entries.js.map +1 -0
  68. package/dist/ports/study-deps.d.ts +14 -0
  69. package/dist/ports/study-deps.d.ts.map +1 -0
  70. package/dist/ports/study-deps.js +2 -0
  71. package/dist/ports/study-deps.js.map +1 -0
  72. package/dist/ports/study-folder.d.ts +43 -0
  73. package/dist/ports/study-folder.d.ts.map +1 -0
  74. package/dist/ports/study-folder.js +2 -0
  75. package/dist/ports/study-folder.js.map +1 -0
  76. package/dist/ports/study-registry-codec.d.ts +6 -3
  77. package/dist/ports/study-registry-codec.d.ts.map +1 -1
  78. package/dist/presets.d.ts +1 -2
  79. package/dist/presets.d.ts.map +1 -1
  80. package/dist/presets.js +1 -2
  81. package/dist/presets.js.map +1 -1
  82. package/dist/studies.d.ts +4 -0
  83. package/dist/studies.d.ts.map +1 -1
  84. package/dist/studies.js +52 -21
  85. package/dist/studies.js.map +1 -1
  86. package/dist/study-folder.d.ts +14 -0
  87. package/dist/study-folder.d.ts.map +1 -0
  88. package/dist/study-folder.js +123 -0
  89. package/dist/study-folder.js.map +1 -0
  90. package/dist/study-gate.d.ts +14 -0
  91. package/dist/study-gate.d.ts.map +1 -0
  92. package/dist/study-gate.js +132 -0
  93. package/dist/study-gate.js.map +1 -0
  94. package/dist/study-index-input.d.ts +5 -0
  95. package/dist/study-index-input.d.ts.map +1 -0
  96. package/dist/study-index-input.js +18 -0
  97. package/dist/study-index-input.js.map +1 -0
  98. package/dist/study-modes.d.ts +56 -0
  99. package/dist/study-modes.d.ts.map +1 -0
  100. package/dist/study-modes.js +59 -0
  101. package/dist/study-modes.js.map +1 -0
  102. package/dist/study-registry-rule.d.ts +13 -1
  103. package/dist/study-registry-rule.d.ts.map +1 -1
  104. package/dist/study-registry-rule.js +33 -21
  105. package/dist/study-registry-rule.js.map +1 -1
  106. package/dist/study-run.d.ts +19 -0
  107. package/dist/study-run.d.ts.map +1 -0
  108. package/dist/study-run.js +9 -0
  109. package/dist/study-run.js.map +1 -0
  110. package/docs/design/study-modes.md +265 -0
  111. package/docs/design/study-registry.md +6 -0
  112. package/docs/e2e.md +8 -4
  113. package/docs/layout.md +22 -19
  114. package/docs/rules/repro/outputs-before-decision.md +122 -0
  115. package/docs/rules/repro/study-registered.md +49 -15
  116. package/docs/rules/tex/heading-case.md +1 -1
  117. package/docs/rules.md +54 -53
  118. package/docs/testing.md +1 -0
  119. package/eslint.config.mjs +36 -24
  120. package/package.json +2 -1
  121. package/scripts/check.ts +6 -0
  122. package/skills/camera-ready/SKILL.md +6 -1
  123. package/skills/camera-ready/SKILL.md.spec.ts +5 -0
  124. package/skills/draft-paper/SKILL.md +11 -3
  125. package/skills/draft-paper/SKILL.md.spec.ts +10 -2
  126. package/skills/grade-paper-writing/SKILL.md +4 -2
  127. package/skills/grade-paper-writing/SKILL.md.spec.ts +3 -1
  128. package/skills/grade-paper-writing/__snapshots__/prose-lint.test.ts.snap +16 -3
  129. package/skills/grade-paper-writing/prose-lint.mjs +127 -95
  130. package/skills/grade-paper-writing/prose-lint.test.ts +119 -10
  131. package/skills/harden-paper/SKILL.md +12 -2
  132. package/skills/harden-paper/SKILL.md.spec.ts +11 -1
  133. package/skills/osf-artifact-upload/SKILL.md +7 -1
  134. package/skills/osf-artifact-upload/SKILL.md.spec.ts +6 -0
  135. package/skills/pc-panel-review/SKILL.md +10 -3
  136. package/skills/pc-panel-review/SKILL.md.spec.ts +9 -2
  137. package/skills/tighten-paper/SKILL.md +5 -1
  138. package/skills/tighten-paper/SKILL.md.spec.ts +4 -0
  139. package/src/CLAUDE.md +7 -4
  140. package/src/CLAUDE.md.spec.ts +6 -3
  141. package/src/adapters/git/history.test.ts +121 -0
  142. package/src/adapters/git/index.test.ts +268 -0
  143. package/src/adapters/git/index.ts +227 -0
  144. package/src/adapters/latex/captions.test.ts +57 -0
  145. package/src/adapters/latex/captions.ts +56 -0
  146. package/src/adapters/latex/index.ts +1 -0
  147. package/src/adapters/latex/parse.ts +7 -0
  148. package/src/adapters/memory/index.test.ts +15 -1
  149. package/src/adapters/memory/index.ts +13 -0
  150. package/src/adapters/node/index.ts +1 -0
  151. package/src/adapters/node/list-entries.io.ts +27 -0
  152. package/src/adapters/node/list-entries.test.ts +61 -0
  153. package/src/adapters/study-registry/index.test.ts +12 -9
  154. package/src/adapters/study-registry/index.ts +130 -24
  155. package/src/adapters/study-registry/mode.test.ts +162 -0
  156. package/src/cli.ts +47 -3
  157. package/src/domain/decide-first.test.ts +291 -0
  158. package/src/domain/decide-first.ts +258 -0
  159. package/src/domain/index-columns.test.ts +261 -0
  160. package/src/domain/index-columns.ts +107 -0
  161. package/src/domain/study-measurements.test.ts +16 -8
  162. package/src/domain/study-mode.test.ts +55 -0
  163. package/src/domain/study-mode.ts +78 -0
  164. package/src/domain/study-registry.test.ts +72 -37
  165. package/src/domain/study-registry.ts +281 -50
  166. package/src/new-paper.test.ts +5 -1
  167. package/src/outputs-before-decision-rule.test.ts +251 -0
  168. package/src/outputs-before-decision-rule.ts +133 -0
  169. package/src/ports/git-history.ts +44 -0
  170. package/src/ports/list-entries.ts +21 -0
  171. package/src/ports/study-deps.ts +14 -0
  172. package/src/ports/study-folder.ts +38 -0
  173. package/src/ports/study-registry-codec.ts +10 -3
  174. package/src/presets.ts +1 -2
  175. package/src/rule-cases.test.ts +78 -3
  176. package/src/studies-index-modes.test.ts +129 -0
  177. package/src/studies.test.ts +35 -11
  178. package/src/studies.ts +96 -26
  179. package/src/study-folder.test.ts +237 -0
  180. package/src/study-folder.ts +176 -0
  181. package/src/study-gate.test.ts +467 -0
  182. package/src/study-gate.ts +234 -0
  183. package/src/study-index-input.ts +31 -0
  184. package/src/study-modes.ts +107 -0
  185. package/src/study-registry-rule.test.ts +285 -11
  186. package/src/study-registry-rule.ts +64 -35
  187. package/src/study-run.ts +21 -0
@@ -19,14 +19,19 @@ import {
19
19
  studyIndex,
20
20
  studyMessage,
21
21
  studyReadme,
22
+ studyById,
22
23
  studySlug,
23
24
  withIndex,
24
25
  type RegistryState,
25
26
  type Study,
26
27
  type StudyInput,
27
28
  type StudyRegistry,
29
+ type IndexInput,
28
30
  } from "./study-registry.ts";
29
31
 
32
+ /** No plan and no history: the index with every mode column `—`. */
33
+ const NO_FACTS: IndexInput = { plans: new Map(), history: new Map() };
34
+
30
35
  const study = (over: Partial<Study> = {}): Study => ({
31
36
  id: "P1-S01",
32
37
  folder: "P1-S01-baseline",
@@ -56,8 +61,8 @@ const input = (
56
61
  ): StudyInput => ({
57
62
  folders: r.studies.map((s) => s.folder),
58
63
  registry: read(r),
59
- readme: withIndex("# Reproduction index\n", r),
60
- existingResults: new Set(),
64
+ readme: withIndex("# Reproduction index\n", r, NO_FACTS),
65
+ results: new Map(),
61
66
  plans: new Map(),
62
67
  ...over,
63
68
  });
@@ -84,7 +89,9 @@ describe("judgeStudies — silent", () => {
84
89
  expect(
85
90
  judgeStudies(
86
91
  input(r, {
87
- existingResults: new Set(["P1-S02-ablation/out/results.tsv"]),
92
+ results: new Map([
93
+ ["P1-S02-ablation/out/results.tsv", { kind: "file" as const }],
94
+ ]),
88
95
  }),
89
96
  ),
90
97
  ).toEqual([]);
@@ -96,7 +103,7 @@ describe("judgeStudies — silent", () => {
96
103
  folders: [],
97
104
  registry: { kind: "absent" },
98
105
  readme: null,
99
- existingResults: new Set(),
106
+ results: new Map(),
100
107
  plans: new Map(),
101
108
  }),
102
109
  ).toEqual([]);
@@ -125,7 +132,7 @@ describe("judgeStudies — the registry itself", () => {
125
132
  folders: ["a-2026-10-01", "b-2026-10-02"],
126
133
  registry: { kind: "absent" },
127
134
  readme: null,
128
- existingResults: new Set(),
135
+ results: new Map(),
129
136
  plans: new Map(),
130
137
  }),
131
138
  ).toEqual([
@@ -139,7 +146,7 @@ describe("judgeStudies — the registry itself", () => {
139
146
  folders: ["a-2026-10-01"],
140
147
  registry: { kind: "unreadable", why: "prefix: required" },
141
148
  readme: null,
142
- existingResults: new Set(),
149
+ results: new Map(),
143
150
  plans: new Map(),
144
151
  }),
145
152
  ).toEqual([{ problem: "unreadable", why: "prefix: required" }]);
@@ -255,7 +262,11 @@ describe("judgeStudies — status", () => {
255
262
  const r = reg(study({ status: "running", result_file: "r.tsv" }));
256
263
  expect(
257
264
  judgeStudies(
258
- input(r, { existingResults: new Set(["P1-S01-baseline/r.tsv"]) }),
265
+ input(r, {
266
+ results: new Map([
267
+ ["P1-S01-baseline/r.tsv", { kind: "file" as const }],
268
+ ]),
269
+ }),
259
270
  ),
260
271
  ).toEqual([]);
261
272
  });
@@ -310,16 +321,16 @@ describe("judgeStudies — the index", () => {
310
321
 
311
322
  it("a table prettier padded by display width, with wide characters, is in sync", async () => {
312
323
  const wide = reg(study({ title: "測定 A" }));
313
- const readme = await format(withIndex("# index\n", wide), {
324
+ const readme = await format(withIndex("# index\n", wide, NO_FACTS), {
314
325
  parser: "markdown",
315
326
  });
316
- expect(readme).not.toBe(withIndex("# index\n", wide));
327
+ expect(readme).not.toBe(withIndex("# index\n", wide, NO_FACTS));
317
328
  expect(judgeStudies(input(wide, { readme }))).toEqual([]);
318
329
  });
319
330
 
320
331
  it("a title with a backslash before a pipe stays one cell through prettier", async () => {
321
332
  const bs = reg(study({ title: "foo\\|bar" }));
322
- const readme = await format(withIndex("# index\n", bs), {
333
+ const readme = await format(withIndex("# index\n", bs, NO_FACTS), {
323
334
  parser: "markdown",
324
335
  });
325
336
  expect(judgeStudies(input(bs, { readme }))).toEqual([]);
@@ -328,22 +339,25 @@ describe("judgeStudies — the index", () => {
328
339
 
329
340
  it("a title carrying the end marker's text: in sync, and regenerating changes nothing", () => {
330
341
  const m = reg(study({ title: `x ${INDEX_END} y` }));
331
- const readme = withIndex("# index\n", m);
342
+ const readme = withIndex("# index\n", m, NO_FACTS);
332
343
  expect(judgeStudies(input(m, { readme }))).toEqual([]);
333
- expect(withIndex(readme, m)).toBe(readme);
344
+ expect(withIndex(readme, m, NO_FACTS)).toBe(readme);
334
345
  });
335
346
 
336
347
  it("prose that mentions the begin marker is kept, and the real block is the one judged", () => {
337
348
  const r1 = reg(study());
338
349
  const prose = `# index\n\nThe table between \`${INDEX_BEGIN}\` and its end marker is generated.\n`;
339
- const readme = withIndex(prose, r1);
350
+ const readme = withIndex(prose, r1, NO_FACTS);
340
351
  expect(judgeStudies(input(r1, { readme }))).toEqual([]);
341
- expect(withIndex(readme, r1)).toBe(readme);
352
+ expect(withIndex(readme, r1, NO_FACTS)).toBe(readme);
342
353
  expect(readme.startsWith(prose.trimEnd())).toBe(true);
343
354
  });
344
355
 
345
356
  it("a block that differs from the registry by one character", () => {
346
- const readme = withIndex("# index\n", r).replace("designing", "running");
357
+ const readme = withIndex("# index\n", r, NO_FACTS).replace(
358
+ "designing",
359
+ "running",
360
+ );
347
361
  expect(judgeStudies(input(r, { readme }))).toEqual([
348
362
  { problem: "stale-index", why: "differs" },
349
363
  ]);
@@ -363,12 +377,12 @@ describe("the index", () => {
363
377
  in_paper: "yes",
364
378
  }),
365
379
  );
366
- expect(studyIndex(r)).toBe(
380
+ expect(studyIndex(r, NO_FACTS)).toBe(
367
381
  [
368
- "| id | title | status | result | in paper | folder |",
369
- "| ------ | ------------------- | --------- | -------- | -------- | ------------------------------------ |",
370
- "| P1-S01 | Baseline pass rate | designing | — | no | [P1-S01-baseline/](P1-S01-baseline/) |",
371
- "| P1-S02 | Pipes \\| and breaks | done | 41 of 60 | yes | [P1-S02-a-b/](P1-S02-a-b/) |",
382
+ "| id | title | status | result | in paper | mode | decision | first output | folder |",
383
+ "| ------ | ------------------- | --------- | -------- | -------- | ---- | -------- | ------------ | ------------------------------------ |",
384
+ "| P1-S01 | Baseline pass rate | designing | — | no | — | — | — | [P1-S01-baseline/](P1-S01-baseline/) |",
385
+ "| P1-S02 | Pipes \\| and breaks | done | 41 of 60 | yes | — | — | — | [P1-S02-a-b/](P1-S02-a-b/) |",
372
386
  ].join("\n"),
373
387
  );
374
388
  });
@@ -390,30 +404,30 @@ describe("the index", () => {
390
404
  }),
391
405
  ].slice(0, n),
392
406
  );
393
- const md = withIndex("# Reproduction index\n\nSome text.\n", r);
407
+ const md = withIndex("# Reproduction index\n\nSome text.\n", r, NO_FACTS);
394
408
  expect(await format(md, { parser: "markdown" })).toBe(md);
395
409
  });
396
410
  });
397
411
 
398
412
  describe("the index of an empty registry, and its markers", () => {
399
413
  it("of an empty registry is the header alone", () => {
400
- expect(studyIndex(reg())).toBe(
414
+ expect(studyIndex(reg(), NO_FACTS)).toBe(
401
415
  [
402
- "| id | title | status | result | in paper | folder |",
403
- "| --- | ----- | ------ | ------ | -------- | ------ |",
416
+ "| id | title | status | result | in paper | mode | decision | first output | folder |",
417
+ "| --- | ----- | ------ | ------ | -------- | ---- | -------- | ------------ | ------ |",
404
418
  ].join("\n"),
405
419
  );
406
420
  });
407
421
 
408
422
  it("sits between the two markers, a blank line on each side", () => {
409
- expect(indexBlock(reg())).toBe(
410
- `${INDEX_BEGIN}\n\n${studyIndex(reg())}\n\n${INDEX_END}`,
423
+ expect(indexBlock(reg(), NO_FACTS)).toBe(
424
+ `${INDEX_BEGIN}\n\n${studyIndex(reg(), NO_FACTS)}\n\n${INDEX_END}`,
411
425
  );
412
426
  });
413
427
 
414
428
  it("is read back from a README as the text between the markers, or null", () => {
415
- expect(embeddedIndex(`# x\n\n${indexBlock(reg())}\n`)).toBe(
416
- `\n\n${studyIndex(reg())}\n\n`,
429
+ expect(embeddedIndex(`# x\n\n${indexBlock(reg(), NO_FACTS)}\n`)).toBe(
430
+ `\n\n${studyIndex(reg(), NO_FACTS)}\n\n`,
417
431
  );
418
432
  expect(embeddedIndex("# x\n")).toBeNull();
419
433
  expect(embeddedIndex(`${INDEX_END}\n${INDEX_BEGIN}\n`)).toBeNull();
@@ -424,27 +438,27 @@ describe("withIndex", () => {
424
438
  const r = reg(study());
425
439
 
426
440
  it("replaces the block in place and keeps the rest", () => {
427
- const before = `# Index\n\nintro\n\n${indexBlock(reg())}\n\ntail\n`;
428
- expect(withIndex(before, r)).toBe(
429
- `# Index\n\nintro\n\n${indexBlock(r)}\n\ntail\n`,
441
+ const before = `# Index\n\nintro\n\n${indexBlock(reg(), NO_FACTS)}\n\ntail\n`;
442
+ expect(withIndex(before, r, NO_FACTS)).toBe(
443
+ `# Index\n\nintro\n\n${indexBlock(r, NO_FACTS)}\n\ntail\n`,
430
444
  );
431
445
  });
432
446
 
433
447
  it("appends the block to a README without markers", () => {
434
- expect(withIndex("# Index\n\nintro", r)).toBe(
435
- `# Index\n\nintro\n\n${indexBlock(r)}\n`,
448
+ expect(withIndex("# Index\n\nintro", r, NO_FACTS)).toBe(
449
+ `# Index\n\nintro\n\n${indexBlock(r, NO_FACTS)}\n`,
436
450
  );
437
451
  });
438
452
 
439
453
  it("writes a README when there is none", () => {
440
- expect(withIndex(null, r)).toBe(
441
- `# Reproduction index\n\n${indexBlock(r)}\n`,
454
+ expect(withIndex(null, r, NO_FACTS)).toBe(
455
+ `# Reproduction index\n\n${indexBlock(r, NO_FACTS)}\n`,
442
456
  );
443
457
  });
444
458
 
445
459
  it("is a fixed point: a second run changes nothing", () => {
446
- const once = withIndex("# Index\n", r);
447
- expect(withIndex(once, r)).toBe(once);
460
+ const once = withIndex("# Index\n", r, NO_FACTS);
461
+ expect(withIndex(once, r, NO_FACTS)).toBe(once);
448
462
  });
449
463
  });
450
464
 
@@ -664,3 +678,24 @@ describe("studyReadme", () => {
664
678
  );
665
679
  });
666
680
  });
681
+
682
+ describe("studyById — the one way to address a study by its id", () => {
683
+ const a = study({ id: "P1-S01", folder: "P1-S01-a" });
684
+ const b = study({ id: "P1-S01", folder: "P1-S01-b" });
685
+ const c = study({ id: "P1-S02", folder: "P1-S02-c" });
686
+
687
+ it("exactly one row: that row; no row: none; two or more: all of them, in registry order", () => {
688
+ // Guards: a caller can take a row only from `one`, never "the first" of an ambiguous id.
689
+ expect([
690
+ studyById([a, c], "P1-S01"),
691
+ studyById([a, c], "P1-S09"),
692
+ studyById([], "P1-S01"),
693
+ studyById([a, c, b], "P1-S01"),
694
+ ]).toEqual([
695
+ { kind: "one", row: a },
696
+ { kind: "none" },
697
+ { kind: "none" },
698
+ { kind: "ambiguous", rows: [a, b] },
699
+ ]);
700
+ });
701
+ });