paperlint 10.2.0 → 10.4.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/.github/workflows/ci.yml +15 -4
- package/README.md +1 -0
- package/dist/adapters/git/index.d.ts +10 -0
- package/dist/adapters/git/index.d.ts.map +1 -0
- package/dist/adapters/git/index.js +151 -0
- package/dist/adapters/git/index.js.map +1 -0
- package/dist/adapters/memory/index.d.ts +6 -0
- package/dist/adapters/memory/index.d.ts.map +1 -1
- package/dist/adapters/memory/index.js +8 -0
- package/dist/adapters/memory/index.js.map +1 -1
- package/dist/adapters/node/index.d.ts +1 -0
- package/dist/adapters/node/index.d.ts.map +1 -1
- package/dist/adapters/node/index.js +1 -0
- package/dist/adapters/node/index.js.map +1 -1
- package/dist/adapters/node/list-entries.io.d.ts +3 -0
- package/dist/adapters/node/list-entries.io.d.ts.map +1 -0
- package/dist/adapters/node/list-entries.io.js +24 -0
- package/dist/adapters/node/list-entries.io.js.map +1 -0
- package/dist/adapters/study-registry/index.d.ts +412 -1
- package/dist/adapters/study-registry/index.d.ts.map +1 -1
- package/dist/adapters/study-registry/index.js +245 -8
- package/dist/adapters/study-registry/index.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +38 -6
- package/dist/cli.js.map +1 -1
- package/dist/domain/decide-first.d.ts +100 -0
- package/dist/domain/decide-first.d.ts.map +1 -0
- package/dist/domain/decide-first.js +102 -0
- package/dist/domain/decide-first.js.map +1 -0
- package/dist/domain/index-columns.d.ts +45 -0
- package/dist/domain/index-columns.d.ts.map +1 -0
- package/dist/domain/index-columns.js +62 -0
- package/dist/domain/index-columns.js.map +1 -0
- package/dist/domain/study-mode.d.ts +37 -0
- package/dist/domain/study-mode.d.ts.map +1 -0
- package/dist/domain/study-mode.js +45 -0
- package/dist/domain/study-mode.js.map +1 -0
- package/dist/domain/study-registry.d.ts +253 -10
- package/dist/domain/study-registry.d.ts.map +1 -1
- package/dist/domain/study-registry.js +325 -27
- package/dist/domain/study-registry.js.map +1 -1
- package/dist/outputs-before-decision-rule.d.ts +17 -0
- package/dist/outputs-before-decision-rule.d.ts.map +1 -0
- package/dist/outputs-before-decision-rule.js +90 -0
- package/dist/outputs-before-decision-rule.js.map +1 -0
- package/dist/ports/git-history.d.ts +44 -0
- package/dist/ports/git-history.d.ts.map +1 -0
- package/dist/ports/git-history.js +2 -0
- package/dist/ports/git-history.js.map +1 -0
- package/dist/ports/list-entries.d.ts +17 -0
- package/dist/ports/list-entries.d.ts.map +1 -0
- package/dist/ports/list-entries.js +2 -0
- package/dist/ports/list-entries.js.map +1 -0
- package/dist/ports/study-registry-codec.d.ts +9 -2
- package/dist/ports/study-registry-codec.d.ts.map +1 -1
- package/dist/studies.d.ts +4 -0
- package/dist/studies.d.ts.map +1 -1
- package/dist/studies.js +55 -24
- package/dist/studies.js.map +1 -1
- package/dist/study-gate.d.ts +14 -0
- package/dist/study-gate.d.ts.map +1 -0
- package/dist/study-gate.js +95 -0
- package/dist/study-gate.js.map +1 -0
- package/dist/study-index-input.d.ts +5 -0
- package/dist/study-index-input.d.ts.map +1 -0
- package/dist/study-index-input.js +13 -0
- package/dist/study-index-input.js.map +1 -0
- package/dist/study-modes.d.ts +45 -0
- package/dist/study-modes.d.ts.map +1 -0
- package/dist/study-modes.js +70 -0
- package/dist/study-modes.js.map +1 -0
- package/dist/study-registry-rule.d.ts +29 -6
- package/dist/study-registry-rule.d.ts.map +1 -1
- package/dist/study-registry-rule.js +91 -40
- package/dist/study-registry-rule.js.map +1 -1
- package/dist/study-run.d.ts +19 -0
- package/dist/study-run.d.ts.map +1 -0
- package/dist/study-run.js +9 -0
- package/dist/study-run.js.map +1 -0
- package/docs/design/study-modes.md +247 -0
- package/docs/design/study-registry.md +137 -15
- package/docs/e2e.md +8 -4
- package/docs/layout.md +26 -21
- package/docs/rules/repro/outputs-before-decision.md +119 -0
- package/docs/rules/repro/study-plan-name.md +70 -0
- package/docs/rules/repro/study-registered.md +206 -15
- package/docs/rules.md +54 -52
- package/docs/testing.md +1 -0
- package/package.json +2 -1
- package/scripts/check.ts +6 -0
- 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/camera-ready/SKILL.md +6 -1
- package/skills/camera-ready/SKILL.md.spec.ts +5 -0
- package/skills/draft-paper/SKILL.md +11 -3
- package/skills/draft-paper/SKILL.md.spec.ts +10 -2
- package/skills/harden-paper/SKILL.md +12 -2
- package/skills/harden-paper/SKILL.md.spec.ts +11 -1
- package/skills/osf-artifact-upload/SKILL.md +7 -1
- package/skills/osf-artifact-upload/SKILL.md.spec.ts +6 -0
- package/skills/pc-panel-review/SKILL.md +10 -3
- package/skills/pc-panel-review/SKILL.md.spec.ts +9 -2
- package/skills/run-agent-grid/SKILL.md +10 -5
- package/skills/run-agent-grid/SKILL.md.spec.ts +9 -4
- package/skills/tighten-paper/SKILL.md +5 -1
- package/skills/tighten-paper/SKILL.md.spec.ts +4 -0
- package/src/adapters/git/history.test.ts +121 -0
- package/src/adapters/git/index.test.ts +268 -0
- package/src/adapters/git/index.ts +227 -0
- package/src/adapters/memory/index.test.ts +15 -1
- package/src/adapters/memory/index.ts +13 -0
- package/src/adapters/node/index.ts +1 -0
- package/src/adapters/node/list-entries.io.ts +26 -0
- package/src/adapters/node/list-entries.test.ts +43 -0
- package/src/adapters/study-registry/index.test.ts +343 -0
- package/src/adapters/study-registry/index.ts +374 -10
- package/src/adapters/study-registry/mode.test.ts +162 -0
- package/src/cli.ts +52 -6
- package/src/domain/decide-first.test.ts +258 -0
- package/src/domain/decide-first.ts +243 -0
- package/src/domain/index-columns.test.ts +216 -0
- package/src/domain/index-columns.ts +105 -0
- package/src/domain/study-measurements.test.ts +633 -0
- package/src/domain/study-mode.test.ts +55 -0
- package/src/domain/study-mode.ts +78 -0
- package/src/domain/study-registry.test.ts +48 -32
- package/src/domain/study-registry.ts +621 -36
- package/src/new-paper.test.ts +6 -1
- package/src/outputs-before-decision-rule.test.ts +206 -0
- package/src/outputs-before-decision-rule.ts +124 -0
- package/src/ports/git-history.ts +44 -0
- package/src/ports/list-entries.ts +18 -0
- package/src/ports/study-registry-codec.ts +13 -2
- package/src/rule-cases.test.ts +103 -1
- package/src/studies-cli.test.ts +2 -2
- package/src/studies-index-modes.test.ts +129 -0
- package/src/studies.test.ts +39 -9
- package/src/studies.ts +99 -29
- package/src/study-gate.test.ts +316 -0
- package/src/study-gate.ts +161 -0
- package/src/study-index-input.ts +23 -0
- package/src/study-modes.ts +120 -0
- package/src/study-registry-rule.test.ts +333 -7
- package/src/study-registry-rule.ts +145 -41
- package/src/study-run.ts +21 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A STUDY'S MODE — how its outcomes are kept from the decisions about its inputs. A measurement study
|
|
3
|
+
* declares exactly one (docs/design/study-modes.md):
|
|
4
|
+
*
|
|
5
|
+
* decide_before_run the default: the author's decisions on the inputs (exclusions, veto, the
|
|
6
|
+
* sample's approval) are committed BEFORE any output exists. No key, no extra
|
|
7
|
+
* tool: git's history is the proof of order, and
|
|
8
|
+
* `repro/outputs-before-decision` re-checks it on every lint.
|
|
9
|
+
* exploratory no gate: the study is exploratory and is reported as such, with its reason.
|
|
10
|
+
*
|
|
11
|
+
* "None" and "two at once" cannot be written: the plan holds one `mode`, a union on its `kind`, and a
|
|
12
|
+
* measurement study's plan is not ready without it (the parser, `src/adapters/study-registry/`).
|
|
13
|
+
*
|
|
14
|
+
* Paths: `outputs` are globs relative to the study's folder (the study's results live there); the
|
|
15
|
+
* decision's `file` is relative to the PAPER's folder, so a decision may live beside the study or
|
|
16
|
+
* anywhere in the paper (`research/decisions.md`). Pure.
|
|
17
|
+
*/
|
|
18
|
+
import { posix } from "node:path";
|
|
19
|
+
import { REPRO_DIR } from "./repro-layout.ts";
|
|
20
|
+
import type { StudyKind } from "./study-registry.ts";
|
|
21
|
+
|
|
22
|
+
/** A decision recorded in a file: where, and what it decides. */
|
|
23
|
+
export interface Decision {
|
|
24
|
+
/** Relative to the paper's folder. */
|
|
25
|
+
readonly file: string;
|
|
26
|
+
readonly note: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Decide first: the decision on the inputs is committed before any output exists. */
|
|
30
|
+
export interface DecideFirst {
|
|
31
|
+
readonly kind: "decide_before_run";
|
|
32
|
+
/** Globs relative to the study's folder: the files the run produces. */
|
|
33
|
+
readonly outputs: readonly string[];
|
|
34
|
+
readonly decision: Decision;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Exploratory: no gate, and reported as exploratory. */
|
|
38
|
+
export interface Exploratory {
|
|
39
|
+
readonly kind: "exploratory";
|
|
40
|
+
readonly reason: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type StudyMode = DecideFirst | Exploratory;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The kinds of study whose plan must declare a mode: the ones that measure. A spike, a tool, a data
|
|
47
|
+
* set, an artifact or a salvage may omit it.
|
|
48
|
+
*/
|
|
49
|
+
export const MODE_REQUIRED: readonly StudyKind[] = ["measurement"];
|
|
50
|
+
|
|
51
|
+
/** Whether a study of `kind` must declare its mode. */
|
|
52
|
+
export const needsMode = (kind: StudyKind): boolean =>
|
|
53
|
+
MODE_REQUIRED.includes(kind);
|
|
54
|
+
|
|
55
|
+
/** `paperPath` relative to the study folder `folder`, when it lies inside it; else null. */
|
|
56
|
+
export const inStudy = (folder: string, paperPath: string): string | null => {
|
|
57
|
+
const prefix = `${REPRO_DIR}/${folder}/`;
|
|
58
|
+
const n = posix.normalize(paperPath);
|
|
59
|
+
return n.startsWith(prefix) ? n.slice(prefix.length) : null;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Whether the file at `path` (relative to the study's folder) is one of `outputs`. */
|
|
63
|
+
export const isOutput = (outputs: readonly string[], path: string): boolean =>
|
|
64
|
+
outputs.some((g) => posix.matchesGlob(path, g));
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The output glob that matches the study's own decision file, when the file lies in the study's
|
|
68
|
+
* folder: the decision would be "produced" by the run it gates.
|
|
69
|
+
*/
|
|
70
|
+
export const decisionAsOutput = (
|
|
71
|
+
folder: string,
|
|
72
|
+
m: DecideFirst,
|
|
73
|
+
): string | null => {
|
|
74
|
+
const rel = inStudy(folder, m.decision.file);
|
|
75
|
+
return rel === null
|
|
76
|
+
? null
|
|
77
|
+
: (m.outputs.find((g) => posix.matchesGlob(rel, g)) ?? null);
|
|
78
|
+
};
|
|
@@ -25,8 +25,12 @@ import {
|
|
|
25
25
|
type Study,
|
|
26
26
|
type StudyInput,
|
|
27
27
|
type StudyRegistry,
|
|
28
|
+
type IndexInput,
|
|
28
29
|
} from "./study-registry.ts";
|
|
29
30
|
|
|
31
|
+
/** No plan and no history: the index with every mode column `—`. */
|
|
32
|
+
const NO_FACTS: IndexInput = { plans: new Map(), history: new Map() };
|
|
33
|
+
|
|
30
34
|
const study = (over: Partial<Study> = {}): Study => ({
|
|
31
35
|
id: "P1-S01",
|
|
32
36
|
folder: "P1-S01-baseline",
|
|
@@ -56,8 +60,9 @@ const input = (
|
|
|
56
60
|
): StudyInput => ({
|
|
57
61
|
folders: r.studies.map((s) => s.folder),
|
|
58
62
|
registry: read(r),
|
|
59
|
-
readme: withIndex("# Reproduction index\n", r),
|
|
63
|
+
readme: withIndex("# Reproduction index\n", r, NO_FACTS),
|
|
60
64
|
existingResults: new Set(),
|
|
65
|
+
plans: new Map(),
|
|
61
66
|
...over,
|
|
62
67
|
});
|
|
63
68
|
|
|
@@ -96,6 +101,7 @@ describe("judgeStudies — silent", () => {
|
|
|
96
101
|
registry: { kind: "absent" },
|
|
97
102
|
readme: null,
|
|
98
103
|
existingResults: new Set(),
|
|
104
|
+
plans: new Map(),
|
|
99
105
|
}),
|
|
100
106
|
).toEqual([]);
|
|
101
107
|
});
|
|
@@ -124,6 +130,7 @@ describe("judgeStudies — the registry itself", () => {
|
|
|
124
130
|
registry: { kind: "absent" },
|
|
125
131
|
readme: null,
|
|
126
132
|
existingResults: new Set(),
|
|
133
|
+
plans: new Map(),
|
|
127
134
|
}),
|
|
128
135
|
).toEqual([
|
|
129
136
|
{ problem: "no-registry", folders: ["a-2026-10-01", "b-2026-10-02"] },
|
|
@@ -137,6 +144,7 @@ describe("judgeStudies — the registry itself", () => {
|
|
|
137
144
|
registry: { kind: "unreadable", why: "prefix: required" },
|
|
138
145
|
readme: null,
|
|
139
146
|
existingResults: new Set(),
|
|
147
|
+
plans: new Map(),
|
|
140
148
|
}),
|
|
141
149
|
).toEqual([{ problem: "unreadable", why: "prefix: required" }]);
|
|
142
150
|
});
|
|
@@ -306,16 +314,16 @@ describe("judgeStudies — the index", () => {
|
|
|
306
314
|
|
|
307
315
|
it("a table prettier padded by display width, with wide characters, is in sync", async () => {
|
|
308
316
|
const wide = reg(study({ title: "測定 A" }));
|
|
309
|
-
const readme = await format(withIndex("# index\n", wide), {
|
|
317
|
+
const readme = await format(withIndex("# index\n", wide, NO_FACTS), {
|
|
310
318
|
parser: "markdown",
|
|
311
319
|
});
|
|
312
|
-
expect(readme).not.toBe(withIndex("# index\n", wide));
|
|
320
|
+
expect(readme).not.toBe(withIndex("# index\n", wide, NO_FACTS));
|
|
313
321
|
expect(judgeStudies(input(wide, { readme }))).toEqual([]);
|
|
314
322
|
});
|
|
315
323
|
|
|
316
324
|
it("a title with a backslash before a pipe stays one cell through prettier", async () => {
|
|
317
325
|
const bs = reg(study({ title: "foo\\|bar" }));
|
|
318
|
-
const readme = await format(withIndex("# index\n", bs), {
|
|
326
|
+
const readme = await format(withIndex("# index\n", bs, NO_FACTS), {
|
|
319
327
|
parser: "markdown",
|
|
320
328
|
});
|
|
321
329
|
expect(judgeStudies(input(bs, { readme }))).toEqual([]);
|
|
@@ -324,22 +332,25 @@ describe("judgeStudies — the index", () => {
|
|
|
324
332
|
|
|
325
333
|
it("a title carrying the end marker's text: in sync, and regenerating changes nothing", () => {
|
|
326
334
|
const m = reg(study({ title: `x ${INDEX_END} y` }));
|
|
327
|
-
const readme = withIndex("# index\n", m);
|
|
335
|
+
const readme = withIndex("# index\n", m, NO_FACTS);
|
|
328
336
|
expect(judgeStudies(input(m, { readme }))).toEqual([]);
|
|
329
|
-
expect(withIndex(readme, m)).toBe(readme);
|
|
337
|
+
expect(withIndex(readme, m, NO_FACTS)).toBe(readme);
|
|
330
338
|
});
|
|
331
339
|
|
|
332
340
|
it("prose that mentions the begin marker is kept, and the real block is the one judged", () => {
|
|
333
341
|
const r1 = reg(study());
|
|
334
342
|
const prose = `# index\n\nThe table between \`${INDEX_BEGIN}\` and its end marker is generated.\n`;
|
|
335
|
-
const readme = withIndex(prose, r1);
|
|
343
|
+
const readme = withIndex(prose, r1, NO_FACTS);
|
|
336
344
|
expect(judgeStudies(input(r1, { readme }))).toEqual([]);
|
|
337
|
-
expect(withIndex(readme, r1)).toBe(readme);
|
|
345
|
+
expect(withIndex(readme, r1, NO_FACTS)).toBe(readme);
|
|
338
346
|
expect(readme.startsWith(prose.trimEnd())).toBe(true);
|
|
339
347
|
});
|
|
340
348
|
|
|
341
349
|
it("a block that differs from the registry by one character", () => {
|
|
342
|
-
const readme = withIndex("# index\n", r).replace(
|
|
350
|
+
const readme = withIndex("# index\n", r, NO_FACTS).replace(
|
|
351
|
+
"designing",
|
|
352
|
+
"running",
|
|
353
|
+
);
|
|
343
354
|
expect(judgeStudies(input(r, { readme }))).toEqual([
|
|
344
355
|
{ problem: "stale-index", why: "differs" },
|
|
345
356
|
]);
|
|
@@ -359,12 +370,12 @@ describe("the index", () => {
|
|
|
359
370
|
in_paper: "yes",
|
|
360
371
|
}),
|
|
361
372
|
);
|
|
362
|
-
expect(studyIndex(r)).toBe(
|
|
373
|
+
expect(studyIndex(r, NO_FACTS)).toBe(
|
|
363
374
|
[
|
|
364
|
-
"| id | title | status | result | in paper | folder |",
|
|
365
|
-
"| ------ | ------------------- | --------- | -------- | -------- | ------------------------------------ |",
|
|
366
|
-
"| P1-S01 | Baseline pass rate | designing | — | no | [P1-S01-baseline/](P1-S01-baseline/) |",
|
|
367
|
-
"| P1-S02 | Pipes \\| and breaks | done | 41 of 60 | yes | [P1-S02-a-b/](P1-S02-a-b/) |",
|
|
375
|
+
"| id | title | status | result | in paper | mode | decision | first output | folder |",
|
|
376
|
+
"| ------ | ------------------- | --------- | -------- | -------- | ---- | -------- | ------------ | ------------------------------------ |",
|
|
377
|
+
"| P1-S01 | Baseline pass rate | designing | — | no | — | — | — | [P1-S01-baseline/](P1-S01-baseline/) |",
|
|
378
|
+
"| P1-S02 | Pipes \\| and breaks | done | 41 of 60 | yes | — | — | — | [P1-S02-a-b/](P1-S02-a-b/) |",
|
|
368
379
|
].join("\n"),
|
|
369
380
|
);
|
|
370
381
|
});
|
|
@@ -386,30 +397,30 @@ describe("the index", () => {
|
|
|
386
397
|
}),
|
|
387
398
|
].slice(0, n),
|
|
388
399
|
);
|
|
389
|
-
const md = withIndex("# Reproduction index\n\nSome text.\n", r);
|
|
400
|
+
const md = withIndex("# Reproduction index\n\nSome text.\n", r, NO_FACTS);
|
|
390
401
|
expect(await format(md, { parser: "markdown" })).toBe(md);
|
|
391
402
|
});
|
|
392
403
|
});
|
|
393
404
|
|
|
394
405
|
describe("the index of an empty registry, and its markers", () => {
|
|
395
406
|
it("of an empty registry is the header alone", () => {
|
|
396
|
-
expect(studyIndex(reg())).toBe(
|
|
407
|
+
expect(studyIndex(reg(), NO_FACTS)).toBe(
|
|
397
408
|
[
|
|
398
|
-
"| id | title | status | result | in paper | folder |",
|
|
399
|
-
"| --- | ----- | ------ | ------ | -------- | ------ |",
|
|
409
|
+
"| id | title | status | result | in paper | mode | decision | first output | folder |",
|
|
410
|
+
"| --- | ----- | ------ | ------ | -------- | ---- | -------- | ------------ | ------ |",
|
|
400
411
|
].join("\n"),
|
|
401
412
|
);
|
|
402
413
|
});
|
|
403
414
|
|
|
404
415
|
it("sits between the two markers, a blank line on each side", () => {
|
|
405
|
-
expect(indexBlock(reg())).toBe(
|
|
406
|
-
`${INDEX_BEGIN}\n\n${studyIndex(reg())}\n\n${INDEX_END}`,
|
|
416
|
+
expect(indexBlock(reg(), NO_FACTS)).toBe(
|
|
417
|
+
`${INDEX_BEGIN}\n\n${studyIndex(reg(), NO_FACTS)}\n\n${INDEX_END}`,
|
|
407
418
|
);
|
|
408
419
|
});
|
|
409
420
|
|
|
410
421
|
it("is read back from a README as the text between the markers, or null", () => {
|
|
411
|
-
expect(embeddedIndex(`# x\n\n${indexBlock(reg())}\n`)).toBe(
|
|
412
|
-
`\n\n${studyIndex(reg())}\n\n`,
|
|
422
|
+
expect(embeddedIndex(`# x\n\n${indexBlock(reg(), NO_FACTS)}\n`)).toBe(
|
|
423
|
+
`\n\n${studyIndex(reg(), NO_FACTS)}\n\n`,
|
|
413
424
|
);
|
|
414
425
|
expect(embeddedIndex("# x\n")).toBeNull();
|
|
415
426
|
expect(embeddedIndex(`${INDEX_END}\n${INDEX_BEGIN}\n`)).toBeNull();
|
|
@@ -420,27 +431,27 @@ describe("withIndex", () => {
|
|
|
420
431
|
const r = reg(study());
|
|
421
432
|
|
|
422
433
|
it("replaces the block in place and keeps the rest", () => {
|
|
423
|
-
const before = `# Index\n\nintro\n\n${indexBlock(reg())}\n\ntail\n`;
|
|
424
|
-
expect(withIndex(before, r)).toBe(
|
|
425
|
-
`# Index\n\nintro\n\n${indexBlock(r)}\n\ntail\n`,
|
|
434
|
+
const before = `# Index\n\nintro\n\n${indexBlock(reg(), NO_FACTS)}\n\ntail\n`;
|
|
435
|
+
expect(withIndex(before, r, NO_FACTS)).toBe(
|
|
436
|
+
`# Index\n\nintro\n\n${indexBlock(r, NO_FACTS)}\n\ntail\n`,
|
|
426
437
|
);
|
|
427
438
|
});
|
|
428
439
|
|
|
429
440
|
it("appends the block to a README without markers", () => {
|
|
430
|
-
expect(withIndex("# Index\n\nintro", r)).toBe(
|
|
431
|
-
`# Index\n\nintro\n\n${indexBlock(r)}\n`,
|
|
441
|
+
expect(withIndex("# Index\n\nintro", r, NO_FACTS)).toBe(
|
|
442
|
+
`# Index\n\nintro\n\n${indexBlock(r, NO_FACTS)}\n`,
|
|
432
443
|
);
|
|
433
444
|
});
|
|
434
445
|
|
|
435
446
|
it("writes a README when there is none", () => {
|
|
436
|
-
expect(withIndex(null, r)).toBe(
|
|
437
|
-
`# Reproduction index\n\n${indexBlock(r)}\n`,
|
|
447
|
+
expect(withIndex(null, r, NO_FACTS)).toBe(
|
|
448
|
+
`# Reproduction index\n\n${indexBlock(r, NO_FACTS)}\n`,
|
|
438
449
|
);
|
|
439
450
|
});
|
|
440
451
|
|
|
441
452
|
it("is a fixed point: a second run changes nothing", () => {
|
|
442
|
-
const once = withIndex("# Index\n", r);
|
|
443
|
-
expect(withIndex(once, r)).toBe(once);
|
|
453
|
+
const once = withIndex("# Index\n", r, NO_FACTS);
|
|
454
|
+
expect(withIndex(once, r, NO_FACTS)).toBe(once);
|
|
444
455
|
});
|
|
445
456
|
});
|
|
446
457
|
|
|
@@ -583,7 +594,7 @@ describe("addStudy", () => {
|
|
|
583
594
|
date: "2026-10-15",
|
|
584
595
|
} as const;
|
|
585
596
|
|
|
586
|
-
it("appends the next id, its folder, designing, not in the paper, an empty question", () => {
|
|
597
|
+
it("appends the next id, its folder, designing, not in the paper, an empty question, its plan", () => {
|
|
587
598
|
const r = reg(study({ id: "P1-S04", folder: "P1-S04-x", foo: 1 }));
|
|
588
599
|
const out = addStudy(r, { ...NEW, title: " Baseline\n pass rate " });
|
|
589
600
|
const added: Study = {
|
|
@@ -595,6 +606,7 @@ describe("addStudy", () => {
|
|
|
595
606
|
status: "designing",
|
|
596
607
|
date: "2026-10-15",
|
|
597
608
|
in_paper: "no",
|
|
609
|
+
plan: "plan.json",
|
|
598
610
|
};
|
|
599
611
|
expect(out).toEqual({
|
|
600
612
|
ok: true,
|
|
@@ -645,6 +657,10 @@ describe("studyReadme", () => {
|
|
|
645
657
|
"Its row in `../studies.json` holds its status. When the study ends, set `status`, `result`,",
|
|
646
658
|
"`result_file` and `in_paper` there, then run `npx paperlint studies index <paper-dir>`.",
|
|
647
659
|
"",
|
|
660
|
+
"Its plan is `plan.json`: what it measures, the population and the draw, the arms and the primary",
|
|
661
|
+
"outcome, filled in before the first run; the author's check of the drawn units goes in its",
|
|
662
|
+
"`author_check`.",
|
|
663
|
+
"",
|
|
648
664
|
"## Question",
|
|
649
665
|
"",
|
|
650
666
|
"## How to run",
|