paperlint 9.0.0 → 10.0.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/CLAUDE.md +22 -0
- package/.github/CLAUDE.md.spec.ts +20 -0
- package/CLAUDE.md +59 -515
- package/CONTRIBUTING.md +2 -2
- package/README.md +68 -128
- package/dist/CLAUDE.md.spec.d.ts +3 -0
- package/dist/CLAUDE.md.spec.d.ts.map +1 -0
- package/dist/CLAUDE.md.spec.js +62 -0
- package/dist/CLAUDE.md.spec.js.map +1 -0
- package/dist/build.d.ts +1 -1
- package/dist/build.js +2 -2
- package/dist/cli.d.ts +5 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +52 -6
- package/dist/cli.js.map +1 -1
- package/dist/domain/layout-tree.d.ts +19 -0
- package/dist/domain/layout-tree.d.ts.map +1 -0
- package/dist/domain/layout-tree.js +32 -0
- package/dist/domain/layout-tree.js.map +1 -0
- package/dist/domain/lookup-cache.d.ts +2 -2
- package/dist/domain/lookup-cache.js +2 -2
- package/dist/domain/paper-layout.d.ts +81 -0
- package/dist/domain/paper-layout.d.ts.map +1 -0
- package/dist/domain/paper-layout.js +259 -0
- package/dist/domain/paper-layout.js.map +1 -0
- package/dist/domain/repro-layout.d.ts +80 -0
- package/dist/domain/repro-layout.d.ts.map +1 -0
- package/dist/domain/repro-layout.js +247 -0
- package/dist/domain/repro-layout.js.map +1 -0
- package/dist/domain/scaffold-layout.d.ts +28 -0
- package/dist/domain/scaffold-layout.d.ts.map +1 -0
- package/dist/domain/scaffold-layout.js +81 -0
- package/dist/domain/scaffold-layout.js.map +1 -0
- package/dist/eslint-rules/CLAUDE.md.spec.d.ts +3 -0
- package/dist/eslint-rules/CLAUDE.md.spec.d.ts.map +1 -0
- package/dist/eslint-rules/CLAUDE.md.spec.js +39 -0
- package/dist/eslint-rules/CLAUDE.md.spec.js.map +1 -0
- package/dist/eslint-rules/install-path-literals.d.ts +1 -1
- package/dist/eslint-rules/install-path-literals.js +4 -4
- package/dist/eslint-rules/install-path-literals.js.map +1 -1
- package/dist/eslint-rules/pdf-last-page-balance.js +1 -1
- package/dist/eslint-rules/prereg-coverage.d.ts +98 -0
- package/dist/eslint-rules/prereg-coverage.d.ts.map +1 -0
- package/dist/eslint-rules/prereg-coverage.js +274 -0
- package/dist/eslint-rules/prereg-coverage.js.map +1 -0
- package/dist/folder-layout-hook.d.ts +21 -0
- package/dist/folder-layout-hook.d.ts.map +1 -0
- package/dist/folder-layout-hook.js +117 -0
- package/dist/folder-layout-hook.js.map +1 -0
- package/dist/folder-layout-rule.d.ts +22 -0
- package/dist/folder-layout-rule.d.ts.map +1 -0
- package/dist/folder-layout-rule.js +107 -0
- package/dist/folder-layout-rule.js.map +1 -0
- package/dist/init.js +1 -1
- package/dist/init.js.map +1 -1
- package/dist/lib/agent-cli-version.js +1 -1
- package/dist/lib/agent-cli-version.js.map +1 -1
- package/dist/new-paper.d.ts +4 -1
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +40 -6
- package/dist/new-paper.js.map +1 -1
- package/dist/references.d.ts +1 -1
- package/dist/references.d.ts.map +1 -1
- package/dist/references.js +2 -2
- package/dist/references.js.map +1 -1
- package/dist/repro-layout-rule.d.ts +16 -0
- package/dist/repro-layout-rule.d.ts.map +1 -0
- package/dist/repro-layout-rule.js +81 -0
- package/dist/repro-layout-rule.js.map +1 -0
- package/dist/tex-requirements.js +1 -1
- package/dist/toolchain.js +1 -1
- package/docs/ci.md +60 -0
- package/docs/configuration.md +2 -0
- package/docs/e2e.md +3 -3
- package/docs/incidents.md +5 -5
- package/docs/install.md +20 -16
- package/docs/layout.md +154 -0
- package/docs/optional-rules.md +5 -4
- package/docs/references.md +5 -2
- package/docs/rules/paper/allowed-files-only.md +192 -0
- package/docs/rules/repro/allowed-files-only.md +128 -0
- package/docs/rules/study/declare-sample.md +146 -0
- package/docs/rules.md +53 -50
- package/docs/skills.md +6 -3
- package/docs/venues.md +94 -0
- package/eslint-rules/CLAUDE.md +44 -0
- package/eslint-rules/CLAUDE.md.spec.ts +43 -0
- package/eslint-rules/install-path-literals.harness.mjs +1 -1
- package/eslint-rules/install-path-literals.ts +4 -4
- package/eslint-rules/pdf-last-page-balance.ts +1 -1
- package/eslint-rules/prereg-coverage.test.ts +448 -0
- package/eslint-rules/prereg-coverage.ts +419 -0
- package/eslint.config.mjs +6 -3
- package/hooks/CLAUDE.md +34 -0
- package/hooks/CLAUDE.md.spec.ts +30 -0
- package/lib/agent-cli-version.ts +1 -1
- package/lib/skill-trigger-cases.harness.mjs +51 -5
- package/lib/skill-trigger-cases.mjs +53 -15
- package/package.json +2 -2
- package/plugin/CLAUDE.md +22 -0
- package/plugin/CLAUDE.md.spec.ts +22 -0
- package/plugin/hooks/hooks.json +9 -0
- package/scripts/coverage-config.test.ts +2 -1
- package/skills/CLAUDE.md +19 -0
- package/skills/CLAUDE.md.spec.ts +18 -0
- package/skills/analyze-sibling-paper/SKILL.md +2 -2
- package/skills/analyze-sibling-paper/SKILL.md.spec.ts +1 -1
- package/skills/build-benchmark/SKILL.md +14 -1
- package/skills/build-benchmark/SKILL.md.spec.ts +13 -0
- package/skills/build-label-page/SKILL.md +118 -0
- package/skills/build-label-page/SKILL.md.spec.ts +129 -0
- package/skills/build-label-page/build-label-page.eval.mjs +15 -0
- package/skills/build-label-page/build-label-page.test.ts +959 -0
- package/skills/build-label-page/references/checklist.md +148 -0
- package/skills/build-label-page/references/example-generator.py +803 -0
- package/skills/find-venue/SKILL.md +5 -3
- package/skills/find-venue/SKILL.md.spec.ts +4 -2
- package/skills/grade-paper-writing/SKILL.md +7 -7
- package/skills/grade-paper-writing/SKILL.md.spec.ts +6 -6
- package/skills/map-prior-work/SKILL.md +2 -2
- package/skills/map-prior-work/SKILL.md.spec.ts +1 -1
- package/skills/paper-pipeline/SKILL.md +8 -6
- package/skills/paper-pipeline/SKILL.md.spec.ts +7 -5
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +19 -9
- package/skills/paper-pipeline/references/acceptance-gate.md +19 -19
- package/skills/paper-pipeline/references/artifact-checklist.md +5 -0
- package/skills/paper-pipeline/references/pipeline-status-template.md +30 -1
- package/skills/paper-pipeline/repro/2026-08-07-fork-check.log +1 -1
- package/skills/paper-pipeline/repro/2026-08-07-fork-check2.log +1 -1
- package/skills/paper-pipeline/repro/2026-08-07-language-eval-pilot.log +1 -1
- package/skills/paper-pipeline/repro/2026-08-07-language-eval-raw.log +1 -1
- package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary-oracle.log +48 -48
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-A6-oracle.log +8 -8
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-oracle.log +48 -48
- package/skills/paper-pipeline/repro/2026-08-08-parent-replication-gpw.log +2 -2
- package/skills/paper-pipeline/repro/2026-08-08-parent-replication.mjs +3 -3
- package/skills/paper-pipeline/repro/2026-10-13-grade-paper-writing-ablation-preflight-after.log +81 -0
- package/skills/paper-pipeline/repro/2026-10-13-grade-paper-writing-ablation-preflight-before.log +81 -0
- package/skills/paper-pipeline/repro/2026-10-13-grade-paper-writing-ablation-setupdiff.log +59 -0
- package/skills/paper-pipeline/repro/2026-10-13-grade-paper-writing-trigger.jsonl +1 -0
- package/skills/paper-pipeline/repro/2026-10-13-grade-paper-writing-trigger.log +22 -0
- package/skills/paper-pipeline/repro/README.md +4 -0
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +2 -2
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +2 -2
- package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +4 -2
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +7 -2
- package/skills/paper-pipeline/scripts/generated-code.mjs +8 -4
- package/skills/paper-pipeline/scripts/generated-code.test.ts +15 -1
- package/skills/paper-status/SKILL.md +2 -2
- package/skills/paper-status/SKILL.md.spec.ts +1 -1
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +21 -102
- package/skills/study-accepted-papers/SKILL.md +163 -161
- package/skills/study-accepted-papers/SKILL.md.spec.ts +167 -165
- package/skills/study-accepted-papers/references/base-rates.md +69 -0
- package/skills/study-accepted-papers/references/mechanisms.md +181 -0
- package/skills/verify-citations/SKILL.md +2 -2
- package/skills/verify-citations/SKILL.md.spec.ts +1 -1
- package/src/CLAUDE.md +23 -1
- package/src/CLAUDE.md.spec.ts +72 -0
- package/src/build.ts +2 -2
- package/src/cli.harness.mjs +42 -0
- package/src/cli.test.ts +90 -6
- package/src/cli.ts +78 -5
- package/src/domain/layout-tree.test.ts +32 -0
- package/src/domain/layout-tree.ts +47 -0
- package/src/domain/lookup-cache.ts +2 -2
- package/src/domain/paper-layout.test.ts +264 -0
- package/src/domain/paper-layout.ts +336 -0
- package/src/domain/repro-layout.test.ts +222 -0
- package/src/domain/repro-layout.ts +311 -0
- package/src/domain/scaffold-layout.ts +104 -0
- package/src/folder-layout-hook.test.ts +214 -0
- package/src/folder-layout-hook.ts +141 -0
- package/src/folder-layout-rule.test.ts +235 -0
- package/src/folder-layout-rule.ts +151 -0
- package/src/hooks-settings.harness.ts +9 -8
- package/src/init.ts +1 -1
- package/src/layout-docs.test.ts +52 -0
- package/src/new-paper.harness.mjs +23 -1
- package/src/new-paper.test.ts +299 -6
- package/src/new-paper.ts +51 -5
- package/src/paper-files.test.ts +9 -6
- package/src/references.test.ts +5 -5
- package/src/references.ts +2 -2
- package/src/repro-layout-rule.test.ts +114 -0
- package/src/repro-layout-rule.ts +121 -0
- package/src/rule-cases.test.ts +98 -0
- package/src/tex-requirements.ts +1 -1
- package/src/toolchain.ts +1 -1
- package/templates/paper/PIPELINE-STATUS.md +41 -7
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `study/declare-sample`: the declaration a study's `prereg.json` carries — population, strata,
|
|
3
|
+
* screens, coverage, the author's check — judged field by field, then across fields, then through
|
|
4
|
+
* ESLint on a paper's `PIPELINE-STATUS.md` with study folders beside it.
|
|
5
|
+
*/
|
|
6
|
+
import { symlinkSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { describe, expect, it } from "vitest";
|
|
9
|
+
import {
|
|
10
|
+
REPRO_DIR as LAYOUT_REPRO_DIR,
|
|
11
|
+
REPRO_DIRS,
|
|
12
|
+
} from "../src/domain/repro-layout.ts";
|
|
13
|
+
import { useTempDir, writeTree } from "../test/support.ts";
|
|
14
|
+
import preregCoverage, {
|
|
15
|
+
REPRO_DIR,
|
|
16
|
+
SHARED_DIRS,
|
|
17
|
+
WHY,
|
|
18
|
+
judgePrereg,
|
|
19
|
+
pathText,
|
|
20
|
+
paperFindings,
|
|
21
|
+
preregFiles,
|
|
22
|
+
preregProblems,
|
|
23
|
+
problemMessage,
|
|
24
|
+
} from "./prereg-coverage.ts";
|
|
25
|
+
|
|
26
|
+
const PAGE =
|
|
27
|
+
"https://github.com/zernie/paperlint/blob/main/docs/rules/study/declare-sample.md";
|
|
28
|
+
|
|
29
|
+
/** A declaration that passes: a sample screened down, saying so, and checked by its author. */
|
|
30
|
+
const GOOD = {
|
|
31
|
+
study: "the author's own keys are not read",
|
|
32
|
+
population: {
|
|
33
|
+
about: "rules written in agent instruction files of public repositories",
|
|
34
|
+
source: "a census of instruction files, crawled 2026-07-28",
|
|
35
|
+
size: 4210,
|
|
36
|
+
},
|
|
37
|
+
strata: { by: "rule type", values: ["crisp", "parameterised", "vague"] },
|
|
38
|
+
screens: [
|
|
39
|
+
{
|
|
40
|
+
name: "judgeable from one code excerpt of at most 40 lines",
|
|
41
|
+
removed: { crisp: 120, parameterised: 410, vague: 1630 },
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
coverage: {
|
|
45
|
+
share: 0.31,
|
|
46
|
+
unreachable: ["vague", "rules about process rather than code"],
|
|
47
|
+
},
|
|
48
|
+
author_check: {
|
|
49
|
+
who: "first author",
|
|
50
|
+
when: "2026-10-12",
|
|
51
|
+
units_seen: 24,
|
|
52
|
+
units_drawn: 24,
|
|
53
|
+
verdict: "representative",
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** `GOOD` with `patch` merged into one of its object fields. */
|
|
58
|
+
const withField = (
|
|
59
|
+
key: "coverage" | "author_check",
|
|
60
|
+
patch: object,
|
|
61
|
+
): object => ({ ...GOOD, [key]: { ...GOOD[key], ...patch } });
|
|
62
|
+
|
|
63
|
+
/** The problems as `at: text`, the whole list. */
|
|
64
|
+
const judged = (data: unknown): string[] =>
|
|
65
|
+
judgePrereg(data).map((p) => `${p.at}: ${p.text}`);
|
|
66
|
+
|
|
67
|
+
describe("judgePrereg", () => {
|
|
68
|
+
it("a complete declaration, and keys the rule does not read: silent", () => {
|
|
69
|
+
expect(judged(GOOD)).toEqual([]);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("judgePrereg: the fields, missing", () => {
|
|
74
|
+
it("an empty object: each of the five fields, with what to add", () => {
|
|
75
|
+
expect(judged({})).toEqual([
|
|
76
|
+
"population: is missing — add `population`: what the paper's claim is about (`about`), where its units come from (`source`) and how many there are (`size`)",
|
|
77
|
+
"strata: is missing — add `strata`: the property the screens are counted by (`by`, e.g. rule type) and its values (`values`)",
|
|
78
|
+
"screens: is missing — add `screens`: one entry per screen applied between the population and the sample, each with `name` and `removed` — [] when none was",
|
|
79
|
+
"coverage: is missing — add `coverage`: the share of the population the sample can represent (`share`, 0 to 1, or null with a `reason`) and the strata it cannot reach (`unreachable`)",
|
|
80
|
+
"author_check: is missing — add `author_check`: who looked at the drawn units before the run (`who`), when (`when`), how many of how many (`units_seen`, `units_drawn`), and what they said (`verdict`)",
|
|
81
|
+
]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("not an object at all: one problem about the file", () => {
|
|
85
|
+
expect(judged([1])).toEqual([
|
|
86
|
+
": is not valid — it must be a JSON object holding `population`, `strata`, `screens`, `coverage` and `author_check`",
|
|
87
|
+
]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("the fields inside each part, missing", () => {
|
|
91
|
+
expect(
|
|
92
|
+
judged({
|
|
93
|
+
...GOOD,
|
|
94
|
+
population: {},
|
|
95
|
+
strata: {},
|
|
96
|
+
screens: [{}],
|
|
97
|
+
coverage: {},
|
|
98
|
+
author_check: {},
|
|
99
|
+
}),
|
|
100
|
+
).toEqual([
|
|
101
|
+
"population.about: is missing — add what the paper's claim is about, in the paper's own words (e.g. the rules written in agent instruction files of public repositories)",
|
|
102
|
+
"population.source: is missing — add where the population's units come from — the census, crawl or registry, with its date",
|
|
103
|
+
"population.size: is missing — add the number of units in the population, a whole number above 0 (an estimate is fine; say how it was made in `source`)",
|
|
104
|
+
"strata.by: is missing — add the property the units are stratified by, e.g. rule type",
|
|
105
|
+
"strata.values: is missing — add the stratum's values, a list of names",
|
|
106
|
+
"screens[0].name: is missing — add what the screen keeps, as it was applied (e.g. judgeable from one code excerpt of at most 40 lines)",
|
|
107
|
+
"screens[0].removed: is missing — add the number of candidates the screen removed in each declared stratum value, an object {<value>: <count>} with 0 where it removed none",
|
|
108
|
+
"coverage.share: is missing — add the share of the population the sample can represent, a number from 0 to 1, or null with a `reason`",
|
|
109
|
+
"coverage.unreachable: is missing — add the strata (or kinds of units) the sample cannot reach, a list of names — [] only when it reaches them all",
|
|
110
|
+
"author_check.who: is missing — add who looked at the drawn units (a name or a role)",
|
|
111
|
+
"author_check.when: is missing — add the date of the check, YYYY-MM-DD",
|
|
112
|
+
"author_check.units_seen: is missing — add how many of the drawn units — the rules or cases drawn, not the items made from them — the author looked at, a whole number above 0",
|
|
113
|
+
"author_check.units_drawn: is missing — add how many units the sample drew",
|
|
114
|
+
'author_check.verdict: is missing — add the author\'s answer to "is this what you meet in practice?" — one of representative, partly, not-representative',
|
|
115
|
+
]);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("judgePrereg: the fields, malformed", () => {
|
|
120
|
+
it("the fields inside each part, malformed", () => {
|
|
121
|
+
expect(
|
|
122
|
+
judged({
|
|
123
|
+
...GOOD,
|
|
124
|
+
population: { about: " ", source: 3, size: 0.5 },
|
|
125
|
+
strata: { by: "rule type", values: [] },
|
|
126
|
+
screens: [
|
|
127
|
+
"a screen in a sentence",
|
|
128
|
+
{ name: "short", removed: { crisp: -1 } },
|
|
129
|
+
],
|
|
130
|
+
coverage: { share: 1.5, reason: "", unreachable: "vague" },
|
|
131
|
+
author_check: {
|
|
132
|
+
who: "first author",
|
|
133
|
+
when: "2026-02-30",
|
|
134
|
+
units_seen: 0,
|
|
135
|
+
units_drawn: 10,
|
|
136
|
+
verdict: "fine",
|
|
137
|
+
decision: { proceed: "yes" },
|
|
138
|
+
},
|
|
139
|
+
}),
|
|
140
|
+
).toEqual([
|
|
141
|
+
"population.about: is empty — write what the paper's claim is about, in the paper's own words (e.g. the rules written in agent instruction files of public repositories)",
|
|
142
|
+
"population.source: is not valid — it must be where the population's units come from — the census, crawl or registry, with its date",
|
|
143
|
+
"population.size: is not valid — it must be the number of units in the population, a whole number above 0 (an estimate is fine; say how it was made in `source`)",
|
|
144
|
+
"strata.values: is empty — list the stratum's values, at least one",
|
|
145
|
+
"screens[0]: is not valid — it must be a screen: `name` and `removed`",
|
|
146
|
+
"screens[1].removed.crisp: is not valid — it must be a count, a whole number from 0",
|
|
147
|
+
"coverage.share: is not valid — it must be the share of the population the sample can represent, a number from 0 to 1, or null with a `reason`",
|
|
148
|
+
"coverage.reason: is empty — write why the share cannot be stated",
|
|
149
|
+
"coverage.unreachable: is not valid — it must be the strata (or kinds of units) the sample cannot reach, a list of names — [] only when it reaches them all",
|
|
150
|
+
"author_check.when: is not valid — it must be the date of the check, YYYY-MM-DD",
|
|
151
|
+
"author_check.units_seen: is not valid — it must be how many of the drawn units — the rules or cases drawn, not the items made from them — the author looked at, a whole number above 0",
|
|
152
|
+
'author_check.verdict: is not valid — it must be the author\'s answer to "is this what you meet in practice?" — one of representative, partly, not-representative',
|
|
153
|
+
"author_check.decision.proceed: is not valid — it must be whether the study runs on this sample, true or false",
|
|
154
|
+
"author_check.decision.by: is missing — add who decided",
|
|
155
|
+
"author_check.decision.why: is missing — add why, in a sentence",
|
|
156
|
+
]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("a decision that is not an object names the three fields it holds", () => {
|
|
160
|
+
expect(
|
|
161
|
+
judged(withField("author_check", { verdict: "partly", decision: "go" })),
|
|
162
|
+
).toEqual([
|
|
163
|
+
"author_check.decision: is not valid — it must be the decision taken after the verdict: `proceed` (true or false), `by` whom, and `why`",
|
|
164
|
+
]);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("judgePrereg: across fields", () => {
|
|
169
|
+
it("`removed` keyed by other than the declared strata: each missing value and each unknown key", () => {
|
|
170
|
+
expect(
|
|
171
|
+
judged({
|
|
172
|
+
...GOOD,
|
|
173
|
+
screens: [
|
|
174
|
+
{ name: "R1", removed: { crisp: 1, vague: 2, procedural: 3 } },
|
|
175
|
+
{ name: "R2", removed: {} },
|
|
176
|
+
],
|
|
177
|
+
}),
|
|
178
|
+
).toEqual([
|
|
179
|
+
"screens[0].removed.parameterised: is missing — every screen states how many candidates it removed in each declared stratum value (crisp, parameterised, vague), 0 where it removed none",
|
|
180
|
+
"screens[0].removed.procedural: is not a declared stratum value — `strata.values` declares crisp, parameterised, vague; add it there or count it under one of them",
|
|
181
|
+
"screens[1].removed.crisp: is missing — every screen states how many candidates it removed in each declared stratum value (crisp, parameterised, vague), 0 where it removed none",
|
|
182
|
+
"screens[1].removed.parameterised: is missing — every screen states how many candidates it removed in each declared stratum value (crisp, parameterised, vague), 0 where it removed none",
|
|
183
|
+
"screens[1].removed.vague: is missing — every screen states how many candidates it removed in each declared stratum value (crisp, parameterised, vague), 0 where it removed none",
|
|
184
|
+
]);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("no screens at all is a declaration, not an omission: silent", () => {
|
|
188
|
+
expect(judged({ ...GOOD, screens: [] })).toEqual([]);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("🔴 a stratum value that Object.prototype also has must be counted in `removed` itself", () => {
|
|
192
|
+
expect(
|
|
193
|
+
judged({
|
|
194
|
+
...GOOD,
|
|
195
|
+
strata: { by: "kind", values: ["constructor", "toString"] },
|
|
196
|
+
screens: [{ name: "R1", removed: {} }],
|
|
197
|
+
}),
|
|
198
|
+
).toEqual(
|
|
199
|
+
["constructor", "toString"].map(
|
|
200
|
+
(v) =>
|
|
201
|
+
`screens[0].removed.${v}: is missing — every screen states how many candidates it removed in each declared stratum value (constructor, toString), 0 where it removed none`,
|
|
202
|
+
),
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe("judgePrereg: the coverage", () => {
|
|
208
|
+
it("a null share needs its reason; with one it is silent", () => {
|
|
209
|
+
expect(judged(withField("coverage", { share: null }))).toEqual([
|
|
210
|
+
"coverage.reason: is missing — `share` is null, so say why the share cannot be stated",
|
|
211
|
+
]);
|
|
212
|
+
expect(
|
|
213
|
+
judged(
|
|
214
|
+
withField("coverage", {
|
|
215
|
+
share: null,
|
|
216
|
+
reason: "the census has no rule-type labels",
|
|
217
|
+
}),
|
|
218
|
+
),
|
|
219
|
+
).toEqual([]);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("a share of 1 with strata it cannot reach contradicts itself; a share below 1 may list them", () => {
|
|
223
|
+
expect(
|
|
224
|
+
judged(withField("coverage", { share: 1, unreachable: ["vague"] })),
|
|
225
|
+
).toEqual([
|
|
226
|
+
"coverage.unreachable: contradicts `share` 1 — a sample that cannot reach «vague» does not represent the whole population: lower `share`, or empty the list",
|
|
227
|
+
]);
|
|
228
|
+
expect(
|
|
229
|
+
judged(withField("coverage", { share: 0.7, unreachable: ["vague"] })),
|
|
230
|
+
).toEqual([]);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe("judgePrereg: within the strata and the check", () => {
|
|
235
|
+
it("a stratum value named twice", () => {
|
|
236
|
+
expect(
|
|
237
|
+
judged({
|
|
238
|
+
...GOOD,
|
|
239
|
+
strata: { by: "rule type", values: ["crisp", "vague", "crisp"] },
|
|
240
|
+
screens: [],
|
|
241
|
+
}),
|
|
242
|
+
).toEqual([
|
|
243
|
+
"strata.values: names «crisp» more than once — each stratum value once",
|
|
244
|
+
]);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("more units seen than drawn", () => {
|
|
248
|
+
expect(judged(withField("author_check", { units_seen: 30 }))).toEqual([
|
|
249
|
+
"author_check.units_seen: is 30, more than the 24 units drawn (`units_drawn`)",
|
|
250
|
+
]);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("a doubtful verdict without a decision is an error; with one, either way, it is silent", () => {
|
|
254
|
+
const missing = (verdict: string): string =>
|
|
255
|
+
`author_check.decision: is missing — the verdict is «${verdict}», and a doubtful verdict with no recorded decision is how a sample the author doubted gets run anyway: add \`proceed\` (true or false), \`by\` whom and \`why\``;
|
|
256
|
+
expect(judged(withField("author_check", { verdict: "partly" }))).toEqual([
|
|
257
|
+
missing("partly"),
|
|
258
|
+
]);
|
|
259
|
+
expect(
|
|
260
|
+
judged(withField("author_check", { verdict: "not-representative" })),
|
|
261
|
+
).toEqual([missing("not-representative")]);
|
|
262
|
+
const decided = (proceed: boolean): string[] =>
|
|
263
|
+
judged(
|
|
264
|
+
withField("author_check", {
|
|
265
|
+
verdict: "not-representative",
|
|
266
|
+
decision: {
|
|
267
|
+
proceed,
|
|
268
|
+
by: "first author",
|
|
269
|
+
why: "redrawn from the census",
|
|
270
|
+
},
|
|
271
|
+
}),
|
|
272
|
+
);
|
|
273
|
+
expect(decided(true)).toEqual([]);
|
|
274
|
+
expect(decided(false)).toEqual([]);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe("preregProblems and the message", () => {
|
|
279
|
+
it("text that is not JSON: one problem, with the parser's reason", () => {
|
|
280
|
+
const problems = preregProblems("{ not json");
|
|
281
|
+
expect(problems.map((p) => p.at)).toEqual([""]);
|
|
282
|
+
expect(problems[0]?.text).toMatch(
|
|
283
|
+
/^does not parse as JSON \(.+\) — there is no declaration to read$/u,
|
|
284
|
+
);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("JSON text is judged", () => {
|
|
288
|
+
expect(preregProblems(JSON.stringify(GOOD))).toEqual([]);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("a path as written in the file", () => {
|
|
292
|
+
expect(pathText(["screens", 0, "removed", "vague"])).toBe(
|
|
293
|
+
"screens[0].removed.vague",
|
|
294
|
+
);
|
|
295
|
+
expect(pathText([])).toBe("");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("a message names the file and the field, then why, then the rule's page", () => {
|
|
299
|
+
expect(
|
|
300
|
+
problemMessage("repro/s-2026-10-12/prereg.json", {
|
|
301
|
+
at: "coverage",
|
|
302
|
+
text: "is missing — add it",
|
|
303
|
+
}),
|
|
304
|
+
).toBe(
|
|
305
|
+
`\`repro/s-2026-10-12/prereg.json\`: \`coverage\` is missing — add it. ${WHY} Rule study/declare-sample: ${PAGE}`,
|
|
306
|
+
);
|
|
307
|
+
expect(
|
|
308
|
+
problemMessage("repro/s/prereg.json", { at: "", text: "is broken" }),
|
|
309
|
+
).toBe(
|
|
310
|
+
`\`repro/s/prereg.json\` is broken. ${WHY} Rule study/declare-sample: ${PAGE}`,
|
|
311
|
+
);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const root = useTempDir("prereg-coverage-");
|
|
316
|
+
|
|
317
|
+
describe("which files are judged", () => {
|
|
318
|
+
it("repro/ and its shared folders are the ones repro/allowed-files-only names", () => {
|
|
319
|
+
expect(REPRO_DIR).toBe(LAYOUT_REPRO_DIR);
|
|
320
|
+
expect(SHARED_DIRS).toEqual(REPRO_DIRS);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("every study folder at the top of repro/ that holds a prereg.json, sorted; never tools/ or data/", () => {
|
|
324
|
+
const dir = writeTree(join(root, "files"), {
|
|
325
|
+
"repro/tools/prereg.json": "{}",
|
|
326
|
+
"repro/data/prereg.json": "{}",
|
|
327
|
+
"repro/Misnamed Study/prereg.json": "{}",
|
|
328
|
+
"repro/b-2026-10-12/prereg.json": "{}",
|
|
329
|
+
"repro/a-2026-10/prereg.json": "{}",
|
|
330
|
+
"repro/no-prereg-2026-10-01/README.md": "# s\n",
|
|
331
|
+
"repro/README.md": "# repro\n",
|
|
332
|
+
"repro/a-2026-10/deeper/prereg.json": "{}",
|
|
333
|
+
});
|
|
334
|
+
expect(preregFiles(dir)).toEqual([
|
|
335
|
+
"repro/Misnamed Study/prereg.json",
|
|
336
|
+
"repro/a-2026-10/prereg.json",
|
|
337
|
+
"repro/b-2026-10-12/prereg.json",
|
|
338
|
+
]);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("no repro/, or a file named repro: nothing", () => {
|
|
342
|
+
expect(preregFiles(writeTree(join(root, "none"), { "x.md": "" }))).toEqual(
|
|
343
|
+
[],
|
|
344
|
+
);
|
|
345
|
+
expect(
|
|
346
|
+
preregFiles(writeTree(join(root, "file"), { repro: "not a folder" })),
|
|
347
|
+
).toEqual([]);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("the findings of a paper: each file's problems, each as its message", () => {
|
|
351
|
+
const dir = writeTree(join(root, "findings"), {
|
|
352
|
+
"repro/ok-2026-10-01/prereg.json": JSON.stringify(GOOD),
|
|
353
|
+
"repro/bad-2026-10-02/prereg.json": JSON.stringify(
|
|
354
|
+
withField("coverage", { share: null }),
|
|
355
|
+
),
|
|
356
|
+
});
|
|
357
|
+
expect(paperFindings(dir)).toEqual([
|
|
358
|
+
`\`repro/bad-2026-10-02/prereg.json\`: \`coverage.reason\` is missing — \`share\` is null, so say why the share cannot be stated. ${WHY} Rule study/declare-sample: ${PAGE}`,
|
|
359
|
+
]);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("🔴 a prereg.json that is a folder is a finding, not a crash of the whole lint run", () => {
|
|
363
|
+
const dir = writeTree(join(root, "folder-prereg"), {
|
|
364
|
+
"repro/s-2026-10-01/prereg.json/x.txt": "",
|
|
365
|
+
});
|
|
366
|
+
expect(paperFindings(dir)).toEqual([
|
|
367
|
+
`\`repro/s-2026-10-01/prereg.json\` is not a file — the declaration is one JSON file; move what the folder holds elsewhere. ${WHY} Rule study/declare-sample: ${PAGE}`,
|
|
368
|
+
]);
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
describe("which files are judged: through a link", () => {
|
|
373
|
+
it("🔴 a study folder linked into repro/ is judged like any other: the layout rule follows the link too", () => {
|
|
374
|
+
const dir = writeTree(join(root, "linked"), {
|
|
375
|
+
"elsewhere/s-2026-10-01/prereg.json": "{}",
|
|
376
|
+
"repro/README.md": "# repro\n",
|
|
377
|
+
});
|
|
378
|
+
symlinkSync(
|
|
379
|
+
join(dir, "elsewhere", "s-2026-10-01"),
|
|
380
|
+
join(dir, "repro", "s-2026-10-01"),
|
|
381
|
+
);
|
|
382
|
+
expect(preregFiles(dir)).toEqual(["repro/s-2026-10-01/prereg.json"]);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("a link in repro/ to nothing is passed over, not a crash", () => {
|
|
386
|
+
const dir = writeTree(join(root, "dangling"), {
|
|
387
|
+
"repro/README.md": "# repro\n",
|
|
388
|
+
});
|
|
389
|
+
symlinkSync(join(dir, "gone"), join(dir, "repro", "s-2026-10-02"));
|
|
390
|
+
expect(preregFiles(dir)).toEqual([]);
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
describe("the rule, on the status file of a paper", () => {
|
|
395
|
+
const rule = preregCoverage.rules["declare-sample"];
|
|
396
|
+
/** What the rule reports for `file` in `dir`, each finding with its line and its whole text. */
|
|
397
|
+
const lint = (dir: string, file = "PIPELINE-STATUS.md") => {
|
|
398
|
+
const out: { line: number; messageId: string; message: string }[] = [];
|
|
399
|
+
rule
|
|
400
|
+
.create({
|
|
401
|
+
filename: join(dir, file),
|
|
402
|
+
options: [],
|
|
403
|
+
settings: {},
|
|
404
|
+
sourceCode: {
|
|
405
|
+
text: "# Status\n",
|
|
406
|
+
getLocFromIndex: (i) => ({ line: i + 1, column: 0 }),
|
|
407
|
+
getText: () => "",
|
|
408
|
+
},
|
|
409
|
+
report: (d) => {
|
|
410
|
+
out.push({
|
|
411
|
+
line: d.loc?.start.line ?? 0,
|
|
412
|
+
messageId: d.messageId,
|
|
413
|
+
message: rule.meta.messages.coverage.replace(
|
|
414
|
+
"{{text}}",
|
|
415
|
+
String(d.data?.["text"]),
|
|
416
|
+
),
|
|
417
|
+
});
|
|
418
|
+
},
|
|
419
|
+
})
|
|
420
|
+
.root?.();
|
|
421
|
+
return out;
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
it("reports each problem on the status file's first line", () => {
|
|
425
|
+
const dir = writeTree(join(root, "lint-bad"), {
|
|
426
|
+
"repro/s-2026-10-12/prereg.json": "[]",
|
|
427
|
+
});
|
|
428
|
+
expect(lint(dir)).toEqual([
|
|
429
|
+
{
|
|
430
|
+
line: 1,
|
|
431
|
+
messageId: "coverage",
|
|
432
|
+
message: `\`repro/s-2026-10-12/prereg.json\` is not valid — it must be a JSON object holding \`population\`, \`strata\`, \`screens\`, \`coverage\` and \`author_check\`. ${WHY} Rule study/declare-sample: ${PAGE}`,
|
|
433
|
+
},
|
|
434
|
+
]);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it("silent on a complete declaration, and on any file but the status file", () => {
|
|
438
|
+
const dir = writeTree(join(root, "lint-good"), {
|
|
439
|
+
"repro/s-2026-10-12/prereg.json": JSON.stringify(GOOD),
|
|
440
|
+
"repro/t-2026-10-12/prereg.json": "{}",
|
|
441
|
+
});
|
|
442
|
+
expect(lint(dir, "NOTES.md")).toEqual([]);
|
|
443
|
+
const good = writeTree(join(root, "lint-good-only"), {
|
|
444
|
+
"repro/s-2026-10-12/prereg.json": JSON.stringify(GOOD),
|
|
445
|
+
});
|
|
446
|
+
expect(lint(good)).toEqual([]);
|
|
447
|
+
});
|
|
448
|
+
});
|