paperlint 9.0.1 → 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/README.md +68 -128
- package/dist/build.d.ts +1 -1
- package/dist/build.js +1 -1
- 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/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/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/docs/ci.md +60 -0
- package/docs/configuration.md +2 -0
- package/docs/e2e.md +2 -2
- 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/prereg-coverage.test.ts +448 -0
- package/eslint-rules/prereg-coverage.ts +419 -0
- package/lib/skill-trigger-cases.harness.mjs +51 -5
- package/lib/skill-trigger-cases.mjs +53 -15
- package/package.json +1 -1
- package/plugin/hooks/hooks.json +9 -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/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/build.ts +1 -1
- 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/templates/paper/PIPELINE-STATUS.md +41 -7
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHAT `paperlint new` PUTS IN A NEW PAPER FOLDER BESIDE ITS THREE FILES — the layout, visible from the
|
|
3
|
+
* first commit.
|
|
4
|
+
*
|
|
5
|
+
* The rules `paper/allowed-files-only` and `repro/allowed-files-only` say where every kind of material goes. A
|
|
6
|
+
* paper that starts with three files shows none of it, and the first reviewer's report, the first
|
|
7
|
+
* figure and the first script each land loose at the root. So a new folder already holds the places.
|
|
8
|
+
*
|
|
9
|
+
* ── ONE SOURCE OF TRUTH ──────────────────────────────────────────────────────────
|
|
10
|
+
* Nothing here lists a folder by hand. The folders are `ROOT_DIR_NOTES` (paper-layout.ts) minus the
|
|
11
|
+
* ones named in `NOT_SCAFFOLDED`, each with its reason; `repro/` gets its own shared folders from
|
|
12
|
+
* `REPRO_DIR_NOTES`. A folder added to the layout is scaffolded unless someone writes down why not,
|
|
13
|
+
* and a test fails on an excluded name the layout no longer has.
|
|
14
|
+
*
|
|
15
|
+
* ── `.gitkeep` ───────────────────────────────────────────────────────────────────
|
|
16
|
+
* Git keeps files, not folders: an empty folder is not in the first commit, which is the point of
|
|
17
|
+
* scaffolding. A folder that holds nothing else gets a `.gitkeep`; a folder that holds a real file
|
|
18
|
+
* (`repro/` with its README.md) gets none. `.gitkeep` is the convention, not a paperlint file, and no
|
|
19
|
+
* rule reads it: `paper/stages` looks for `*.pdf` in `versions/`, the review and sibling rules lint
|
|
20
|
+
* `*.md`, `repro/allowed-files-only` judges the top of `repro/` only, and neither `tools/` nor `data/` is
|
|
21
|
+
* listed by any rule.
|
|
22
|
+
*
|
|
23
|
+
* Pure: a list of paths and texts, written by `new-paper.ts`.
|
|
24
|
+
*/
|
|
25
|
+
import { ROOT_DIR_NOTES } from "./paper-layout.js";
|
|
26
|
+
import { DATA_DIR, README, REPRO_DIR, REPRO_DIR_NOTES, TOOLS_DIR, } from "./repro-layout.js";
|
|
27
|
+
/** The placeholder git needs to keep an otherwise empty folder. */
|
|
28
|
+
export const GITKEEP = ".gitkeep";
|
|
29
|
+
/**
|
|
30
|
+
* The folders of the layout that `paperlint new` does not create, each with why. A folder here is
|
|
31
|
+
* allowed at the root all the same; it appears when the work needs it.
|
|
32
|
+
*/
|
|
33
|
+
export const NOT_SCAFFOLDED = [
|
|
34
|
+
{
|
|
35
|
+
name: "_build",
|
|
36
|
+
why: "`paperlint build` writes it, and an empty one would claim a build that never ran",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "siblings",
|
|
40
|
+
why: "a card is written when a close competitor turns up (skill analyze-sibling-paper, which also writes the folder's README.md index); a paper may have none, and an empty folder reads as an unfinished analysis",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "sections",
|
|
44
|
+
why: "the body is split only when it outgrows `paper.tex`; the stub is one file, and an empty `sections/` suggests the split is expected",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "talk",
|
|
48
|
+
why: "only a paper that declares a talk (`talk` in paperlint.json) has one, and its folder is the declared `talk.dir`, which need not be `talk/`",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "extra",
|
|
52
|
+
why: "the escape hatch for material nothing else takes; it is never judged, so it has no place in a layout meant to be filled by kind",
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
/** The folders at the paper's root that a new paper holds from the first commit, in layout order. */
|
|
56
|
+
export const SCAFFOLD_DIRS = ROOT_DIR_NOTES.map((d) => d.name).filter((name) => !NOT_SCAFFOLDED.some((n) => n.name === name));
|
|
57
|
+
/**
|
|
58
|
+
* The stub index of `repro/`: it makes the folder non-empty (no `.gitkeep`) and says what the rule
|
|
59
|
+
* asks of it. A line per study is added when the first study is.
|
|
60
|
+
*/
|
|
61
|
+
export const REPRO_INDEX_STUB = `# Reproduction index
|
|
62
|
+
|
|
63
|
+
One line per study: \`<slug>-YYYY-MM-DD/\` — what it asks and what it found. Scripts used across the
|
|
64
|
+
paper go in \`${TOOLS_DIR}/\`, shared data in \`${DATA_DIR}/\`.
|
|
65
|
+
`;
|
|
66
|
+
/**
|
|
67
|
+
* Every file of the layout scaffold, relative to the paper folder, in a fixed order: a `.gitkeep` in
|
|
68
|
+
* each empty folder, and `repro/README.md` where `repro/` would otherwise hold only folders.
|
|
69
|
+
*/
|
|
70
|
+
export const SCAFFOLD_FILES = [
|
|
71
|
+
...SCAFFOLD_DIRS.flatMap((dir) => dir === REPRO_DIR
|
|
72
|
+
? [
|
|
73
|
+
{ path: `${REPRO_DIR}/${README}`, text: REPRO_INDEX_STUB },
|
|
74
|
+
...REPRO_DIR_NOTES.map((d) => ({
|
|
75
|
+
path: `${REPRO_DIR}/${d.name}/${GITKEEP}`,
|
|
76
|
+
text: "",
|
|
77
|
+
})),
|
|
78
|
+
]
|
|
79
|
+
: [{ path: `${dir}/${GITKEEP}`, text: "" }]),
|
|
80
|
+
];
|
|
81
|
+
//# sourceMappingURL=scaffold-layout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scaffold-layout.js","sourceRoot":"","sources":["../../src/domain/scaffold-layout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EACL,QAAQ,EACR,MAAM,EACN,SAAS,EACT,eAAe,EACf,SAAS,GACV,MAAM,mBAAmB,CAAC;AAE3B,mEAAmE;AACnE,MAAM,CAAC,MAAM,OAAO,GAAG,UAAU,CAAC;AAElC;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAGrB;IACJ;QACE,IAAI,EAAE,QAAQ;QACd,GAAG,EAAE,kFAAkF;KACxF;IACD;QACE,IAAI,EAAE,UAAU;QAChB,GAAG,EAAE,8MAA8M;KACpN;IACD;QACE,IAAI,EAAE,UAAU;QAChB,GAAG,EAAE,oIAAoI;KAC1I;IACD;QACE,IAAI,EAAE,MAAM;QACZ,GAAG,EAAE,4IAA4I;KAClJ;IACD;QACE,IAAI,EAAE,OAAO;QACb,GAAG,EAAE,iIAAiI;KACvI;CACF,CAAC;AAEF,qGAAqG;AACrG,MAAM,CAAC,MAAM,aAAa,GAAsB,cAAc,CAAC,GAAG,CAChE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CACd,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC;AAEjE;;;GAGG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG;;;gBAGhB,SAAS,yBAAyB,QAAQ;CACzD,CAAC;AAQF;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAA4B;IACrD,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC,GAAG,EAA2B,EAAE,CACxD,GAAG,KAAK,SAAS;QACf,CAAC,CAAC;YACE,EAAE,IAAI,EAAE,GAAG,SAAS,IAAI,MAAM,EAAE,EAAE,IAAI,EAAE,gBAAgB,EAAE;YAC1D,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBAC7B,IAAI,EAAE,GAAG,SAAS,IAAI,CAAC,CAAC,IAAI,IAAI,OAAO,EAAE;gBACzC,IAAI,EAAE,EAAE;aACT,CAAC,CAAC;SACJ;QACH,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,GAAG,IAAI,OAAO,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAC9C;CACF,CAAC"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { RuleContext } from "./rule-context.ts";
|
|
3
|
+
/**
|
|
4
|
+
* The folder of a paper that holds its studies, the folders in it that are shared rather than a
|
|
5
|
+
* study's, and the file a study pre-registers in. The first two are `repro/allowed-files-only`'s
|
|
6
|
+
* (`src/domain/repro-layout.ts`, `REPRO_DIR` and `REPRO_DIRS`), copied because a module here is
|
|
7
|
+
* compiled before `src/`; the test beside this file holds the copies equal.
|
|
8
|
+
*/
|
|
9
|
+
export declare const REPRO_DIR = "repro";
|
|
10
|
+
export declare const SHARED_DIRS: readonly string[];
|
|
11
|
+
export declare const PREREG_FILE = "prereg.json";
|
|
12
|
+
/** The one sentence every finding ends with: why the fields exist. */
|
|
13
|
+
export declare const WHY = "Why: a study once screened its sample down to rules that are easy to judge \u2014 its labeller found 93 % of the items designed as borderline clear-cut \u2014 and nothing in its design made anyone state what share of the real population the sample covered or which screens removed which kinds of units.";
|
|
14
|
+
/** The answers to "is this what you meet in practice?"; any but the first needs a decision. */
|
|
15
|
+
export declare const VERDICTS: string[];
|
|
16
|
+
/** The declaration a `prereg.json` must carry. Every other key of the file is the author's. */
|
|
17
|
+
export declare const PREREG: z.ZodObject<{
|
|
18
|
+
population: z.ZodObject<{
|
|
19
|
+
about: z.ZodString;
|
|
20
|
+
source: z.ZodString;
|
|
21
|
+
size: z.ZodNumber;
|
|
22
|
+
}, z.core.$strip>;
|
|
23
|
+
strata: z.ZodObject<{
|
|
24
|
+
by: z.ZodString;
|
|
25
|
+
values: z.ZodArray<z.ZodString>;
|
|
26
|
+
}, z.core.$strip>;
|
|
27
|
+
screens: z.ZodArray<z.ZodObject<{
|
|
28
|
+
name: z.ZodString;
|
|
29
|
+
removed: z.ZodRecord<z.ZodString, z.ZodNumber>;
|
|
30
|
+
}, z.core.$strip>>;
|
|
31
|
+
coverage: z.ZodObject<{
|
|
32
|
+
share: z.ZodNullable<z.ZodNumber>;
|
|
33
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
34
|
+
unreachable: z.ZodArray<z.ZodString>;
|
|
35
|
+
}, z.core.$strip>;
|
|
36
|
+
author_check: z.ZodObject<{
|
|
37
|
+
who: z.ZodString;
|
|
38
|
+
when: z.ZodISODate;
|
|
39
|
+
units_seen: z.ZodNumber;
|
|
40
|
+
units_drawn: z.ZodNumber;
|
|
41
|
+
verdict: z.ZodEnum<{
|
|
42
|
+
[x: string]: string;
|
|
43
|
+
}>;
|
|
44
|
+
decision: z.ZodOptional<z.ZodObject<{
|
|
45
|
+
proceed: z.ZodBoolean;
|
|
46
|
+
by: z.ZodString;
|
|
47
|
+
why: z.ZodString;
|
|
48
|
+
}, z.core.$strip>>;
|
|
49
|
+
}, z.core.$strip>;
|
|
50
|
+
}, z.core.$strip>;
|
|
51
|
+
export type Prereg = z.infer<typeof PREREG>;
|
|
52
|
+
/** A zod path as it is written in the file: `screens[0].removed.vague`. */
|
|
53
|
+
export declare const pathText: (path: readonly PropertyKey[]) => string;
|
|
54
|
+
/** One problem: the field it is about, and what is wrong with it. */
|
|
55
|
+
export interface Problem {
|
|
56
|
+
readonly at: string;
|
|
57
|
+
readonly text: string;
|
|
58
|
+
}
|
|
59
|
+
/** What a schema of one field cannot see: how the fields of a parsed declaration relate. */
|
|
60
|
+
export declare function crossProblems(p: Readonly<Prereg>): readonly Problem[];
|
|
61
|
+
/** Everything wrong with the parsed JSON of one `prereg.json`, in the file's field order. */
|
|
62
|
+
export declare function judgePrereg(data: unknown): readonly Problem[];
|
|
63
|
+
/** The problems of one `prereg.json` whose text is `source`: a parse failure, or the judgement. */
|
|
64
|
+
export declare function preregProblems(source: string): readonly Problem[];
|
|
65
|
+
/** The one text a problem in the file `file` (relative to the paper) is reported with. */
|
|
66
|
+
export declare const problemMessage: (file: string, p: Problem) => string;
|
|
67
|
+
/**
|
|
68
|
+
* The `prereg.json` of every study folder at the top of `<paperDir>/repro/`, relative to the paper:
|
|
69
|
+
* every folder there but the shared ones, whatever its name — a study folder misnamed is
|
|
70
|
+
* `repro/allowed-files-only`'s finding, and its pre-registration is still judged here.
|
|
71
|
+
*/
|
|
72
|
+
export declare function preregFiles(paperDir: string): readonly string[];
|
|
73
|
+
/** Every finding for the paper folder `paperDir`, one text each. */
|
|
74
|
+
export declare const paperFindings: (paperDir: string) => readonly string[];
|
|
75
|
+
declare const _default: {
|
|
76
|
+
rules: {
|
|
77
|
+
"declare-sample": {
|
|
78
|
+
meta: {
|
|
79
|
+
type: "problem";
|
|
80
|
+
docs: {
|
|
81
|
+
description: string;
|
|
82
|
+
url: string;
|
|
83
|
+
};
|
|
84
|
+
schema: never[];
|
|
85
|
+
messages: {
|
|
86
|
+
coverage: string;
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
create(context: RuleContext): {
|
|
90
|
+
root?: never;
|
|
91
|
+
} | {
|
|
92
|
+
root(): void;
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
export default _default;
|
|
98
|
+
//# sourceMappingURL=prereg-coverage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prereg-coverage.d.ts","sourceRoot":"","sources":["../../eslint-rules/prereg-coverage.ts"],"names":[],"mappings":"AAqCA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD;;;;;GAKG;AACH,eAAO,MAAM,SAAS,UAAU,CAAC;AACjC,eAAO,MAAM,WAAW,EAAE,SAAS,MAAM,EAAsB,CAAC;AAChE,eAAO,MAAM,WAAW,gBAAgB,CAAC;AAKzC,sEAAsE;AACtE,eAAO,MAAM,GAAG,mTACwR,CAAC;AA2GzS,+FAA+F;AAC/F,eAAO,MAAM,QAAQ,UAAqD,CAAC;AAoC3E,+FAA+F;AAC/F,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAWlB,CAAC;AAEF,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,MAAM,CAAC,CAAC;AAE5C,2EAA2E;AAC3E,eAAO,MAAM,QAAQ,GAAI,MAAM,SAAS,WAAW,EAAE,KAAG,MAIhC,CAAC;AAEzB,qEAAqE;AACrE,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAkDD,4FAA4F;AAC5F,wBAAgB,aAAa,CAAC,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,SAAS,OAAO,EAAE,CAoBrE;AAED,6FAA6F;AAC7F,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,GAAG,SAAS,OAAO,EAAE,CAQ7D;AAOD,mGAAmG;AACnG,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,OAAO,EAAE,CAiBjE;AAED,0FAA0F;AAC1F,eAAO,MAAM,cAAc,GAAI,MAAM,MAAM,EAAE,GAAG,OAAO,KAAG,MAO7C,CAAC;AAEd;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAkB/D;AAUD,oEAAoE;AACpE,eAAO,MAAM,aAAa,GAAI,UAAU,MAAM,KAAG,SAAS,MAAM,EAO5D,CAAC;;;;;;;;;;;;;;;4BAeiB,WAAW;;;;;;;;AAbjC,wBA8BE"}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `study/declare-sample` — a study's pre-registration says which population the paper's claim is
|
|
3
|
+
* about, what the screens between that population and the sample removed, what share of it the
|
|
4
|
+
* sample can represent, and that the author looked at the drawn units before the run.
|
|
5
|
+
*
|
|
6
|
+
* ── THE FAILURE IT PREVENTS ──────────────────────────────────────────────────────
|
|
7
|
+
* A measurement study of how language models read coding rules drew its rules from three pools,
|
|
8
|
+
* kept only the rules "judgeable from one code excerpt of at most 40 lines", restricted one pool to a
|
|
9
|
+
* classifier's "enforceable" lane, and let a model pick short excerpts. Each screen was reasonable
|
|
10
|
+
* alone; together they left a sample of crisp rules. The labeller found 93 % of the items designed as
|
|
11
|
+
* borderline clear-cut, and the rules looked nothing like the ones met in practice. A spot-check of
|
|
12
|
+
* ten items flagged it (seven of ten clear-cut) and the run went ahead: nothing in the design made
|
|
13
|
+
* anyone state what share of the population the sample covered, or which screens removed which kinds
|
|
14
|
+
* of rules. The four fields below are those statements, and this rule makes them part of the record.
|
|
15
|
+
*
|
|
16
|
+
* ── WHERE IT LOOKS ───────────────────────────────────────────────────────────────
|
|
17
|
+
* A study lives in a folder at the top of the paper's `repro/` (`repro/allowed-files-only` names the
|
|
18
|
+
* layout); `tools/` and `data/` beside them are shared, not studies. Every study folder that holds a
|
|
19
|
+
* `prereg.json` is judged; a study without one owes nothing here. The findings are reported on the paper's `PIPELINE-STATUS.md`, first line, as the other rules
|
|
20
|
+
* about a paper's folders are: `prereg.json` is not a file paperlint lints, and every paper has a
|
|
21
|
+
* status file.
|
|
22
|
+
*
|
|
23
|
+
* ── THE FIELDS ───────────────────────────────────────────────────────────────────
|
|
24
|
+
* population { about, source, size } what the claim is about, where it comes from, how many
|
|
25
|
+
* strata { by, values } the one stratification the screens are counted by
|
|
26
|
+
* screens [{ name, removed: {value: n} }] each screen, and what it removed per stratum value
|
|
27
|
+
* coverage { share, reason?, unreachable } 0–1, or null with a reason; strata it cannot reach
|
|
28
|
+
* author_check { who, when, units_seen, units_drawn, verdict, decision? }
|
|
29
|
+
*
|
|
30
|
+
* A pre-registration holds much more (hypotheses, arms, models, budget); every other key is the
|
|
31
|
+
* author's and is not read. The shape is `PREREG` below — a zod schema whose every field carries the
|
|
32
|
+
* text a finding gives for it — and the relations between fields that a schema of one field cannot
|
|
33
|
+
* see (`removed` keyed by the declared strata, a null share with a reason, a doubtful verdict with a
|
|
34
|
+
* decision) are `crossProblems`.
|
|
35
|
+
*/
|
|
36
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
37
|
+
import { basename, dirname, join } from "node:path";
|
|
38
|
+
import { z } from "zod";
|
|
39
|
+
/**
|
|
40
|
+
* The folder of a paper that holds its studies, the folders in it that are shared rather than a
|
|
41
|
+
* study's, and the file a study pre-registers in. The first two are `repro/allowed-files-only`'s
|
|
42
|
+
* (`src/domain/repro-layout.ts`, `REPRO_DIR` and `REPRO_DIRS`), copied because a module here is
|
|
43
|
+
* compiled before `src/`; the test beside this file holds the copies equal.
|
|
44
|
+
*/
|
|
45
|
+
export const REPRO_DIR = "repro";
|
|
46
|
+
export const SHARED_DIRS = ["tools", "data"];
|
|
47
|
+
export const PREREG_FILE = "prereg.json";
|
|
48
|
+
const STATUS_FILE = "PIPELINE-STATUS.md";
|
|
49
|
+
const PAGE = "https://github.com/zernie/paperlint/blob/main/docs/rules/study/declare-sample.md";
|
|
50
|
+
/** The one sentence every finding ends with: why the fields exist. */
|
|
51
|
+
export const WHY = "Why: a study once screened its sample down to rules that are easy to judge — its labeller found 93 % of the items designed as borderline clear-cut — and nothing in its design made anyone state what share of the real population the sample covered or which screens removed which kinds of units.";
|
|
52
|
+
/** A zod error option: `what` is the text a finding gives for the field, missing or malformed. */
|
|
53
|
+
const field = (what) => ({
|
|
54
|
+
error: (iss) => iss.input === undefined
|
|
55
|
+
? `is missing — add ${what}`
|
|
56
|
+
: `is not valid — it must be ${what}`,
|
|
57
|
+
});
|
|
58
|
+
/** Text that says something: a string with more than white space in it. */
|
|
59
|
+
const text = (what) => z
|
|
60
|
+
.string(field(what))
|
|
61
|
+
.trim()
|
|
62
|
+
.min(1, { error: `is empty — write ${what}` });
|
|
63
|
+
/** A whole number above 0. */
|
|
64
|
+
const positive = (what) => z.number(field(what)).int(field(what)).positive(field(what));
|
|
65
|
+
const POPULATION = z.object({
|
|
66
|
+
about: text("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)"),
|
|
67
|
+
source: text("where the population's units come from — the census, crawl or registry, with its date"),
|
|
68
|
+
size: positive("the number of units in the population, a whole number above 0 (an estimate is fine; say how it was made in `source`)"),
|
|
69
|
+
}, field("`population`: what the paper's claim is about (`about`), where its units come from (`source`) and how many there are (`size`)"));
|
|
70
|
+
const STRATA = z.object({
|
|
71
|
+
by: text("the property the units are stratified by, e.g. rule type"),
|
|
72
|
+
values: z
|
|
73
|
+
.array(text("a value of the stratum"), field("the stratum's values, a list of names"))
|
|
74
|
+
.min(1, {
|
|
75
|
+
error: "is empty — list the stratum's values, at least one",
|
|
76
|
+
}),
|
|
77
|
+
}, field("`strata`: the property the screens are counted by (`by`, e.g. rule type) and its values (`values`)"));
|
|
78
|
+
const REMOVED_WHAT = "the number of candidates the screen removed in each declared stratum value, an object {<value>: <count>} with 0 where it removed none";
|
|
79
|
+
const SCREEN = z.object({
|
|
80
|
+
name: text("what the screen keeps, as it was applied (e.g. judgeable from one code excerpt of at most 40 lines)"),
|
|
81
|
+
removed: z.record(z.string(), z
|
|
82
|
+
.number(field("a count, a whole number from 0"))
|
|
83
|
+
.int(field("a count, a whole number from 0"))
|
|
84
|
+
.nonnegative(field("a count, a whole number from 0")), field(REMOVED_WHAT)),
|
|
85
|
+
}, field("a screen: `name` and `removed`"));
|
|
86
|
+
const SCREENS = z.array(SCREEN, field("`screens`: one entry per screen applied between the population and the sample, each with `name` and `removed` — [] when none was"));
|
|
87
|
+
const SHARE_WHAT = "the share of the population the sample can represent, a number from 0 to 1, or null with a `reason`";
|
|
88
|
+
const COVERAGE = z.object({
|
|
89
|
+
share: z
|
|
90
|
+
.number(field(SHARE_WHAT))
|
|
91
|
+
.min(0, field(SHARE_WHAT))
|
|
92
|
+
.max(1, field(SHARE_WHAT))
|
|
93
|
+
.nullable(),
|
|
94
|
+
reason: text("why the share cannot be stated").optional(),
|
|
95
|
+
unreachable: z.array(text("a stratum or kind of unit"), field("the strata (or kinds of units) the sample cannot reach, a list of names — [] only when it reaches them all")),
|
|
96
|
+
}, field("`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`)"));
|
|
97
|
+
/** The answers to "is this what you meet in practice?"; any but the first needs a decision. */
|
|
98
|
+
export const VERDICTS = ["representative", "partly", "not-representative"];
|
|
99
|
+
const DECISION = z.object({
|
|
100
|
+
proceed: z.boolean(field("whether the study runs on this sample, true or false")),
|
|
101
|
+
by: text("who decided"),
|
|
102
|
+
why: text("why, in a sentence"),
|
|
103
|
+
}, field("the decision taken after the verdict: `proceed` (true or false), `by` whom, and `why`"));
|
|
104
|
+
const AUTHOR_CHECK = z.object({
|
|
105
|
+
who: text("who looked at the drawn units (a name or a role)"),
|
|
106
|
+
when: z.iso.date(field("the date of the check, YYYY-MM-DD")),
|
|
107
|
+
units_seen: positive("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"),
|
|
108
|
+
units_drawn: positive("how many units the sample drew"),
|
|
109
|
+
verdict: z.enum(VERDICTS, field(`the author's answer to "is this what you meet in practice?" — one of ${VERDICTS.join(", ")}`)),
|
|
110
|
+
decision: DECISION.optional(),
|
|
111
|
+
}, field("`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`)"));
|
|
112
|
+
/** The declaration a `prereg.json` must carry. Every other key of the file is the author's. */
|
|
113
|
+
export const PREREG = z.object({
|
|
114
|
+
population: POPULATION,
|
|
115
|
+
strata: STRATA,
|
|
116
|
+
screens: SCREENS,
|
|
117
|
+
coverage: COVERAGE,
|
|
118
|
+
author_check: AUTHOR_CHECK,
|
|
119
|
+
}, field("a JSON object holding `population`, `strata`, `screens`, `coverage` and `author_check`"));
|
|
120
|
+
/** A zod path as it is written in the file: `screens[0].removed.vague`. */
|
|
121
|
+
export const pathText = (path) => path
|
|
122
|
+
.map((k) => (typeof k === "number" ? `[${String(k)}]` : `.${String(k)}`))
|
|
123
|
+
.join("")
|
|
124
|
+
.replace(/^\./u, "");
|
|
125
|
+
/** The values that occur more than once in `values`, each once. */
|
|
126
|
+
const repeated = (values) => [
|
|
127
|
+
...new Set(values.filter((v, i) => values.indexOf(v) !== i)),
|
|
128
|
+
];
|
|
129
|
+
/** What one screen's `removed` lacks and holds beyond the declared strata. */
|
|
130
|
+
const removedProblems = (removed, i, values) => [
|
|
131
|
+
...values
|
|
132
|
+
.filter((v) => !Object.hasOwn(removed, v))
|
|
133
|
+
.map((v) => ({
|
|
134
|
+
at: `screens[${String(i)}].removed.${v}`,
|
|
135
|
+
text: `is missing — every screen states how many candidates it removed in each declared stratum value (${values.join(", ")}), 0 where it removed none`,
|
|
136
|
+
})),
|
|
137
|
+
...Object.keys(removed)
|
|
138
|
+
.filter((k) => !values.includes(k))
|
|
139
|
+
.map((k) => ({
|
|
140
|
+
at: `screens[${String(i)}].removed.${k}`,
|
|
141
|
+
text: `is not a declared stratum value — \`strata.values\` declares ${values.join(", ")}; add it there or count it under one of them`,
|
|
142
|
+
})),
|
|
143
|
+
];
|
|
144
|
+
/** A problem when `when` holds, else none. */
|
|
145
|
+
const problemIf = (when, at, text) => (when ? [{ at, text }] : []);
|
|
146
|
+
/** The author's check against itself: no more units seen than drawn, a doubtful verdict decided. */
|
|
147
|
+
const checkProblems = (check) => [
|
|
148
|
+
...problemIf(check.units_seen > check.units_drawn, "author_check.units_seen", `is ${String(check.units_seen)}, more than the ${String(check.units_drawn)} units drawn (\`units_drawn\`)`),
|
|
149
|
+
...problemIf(check.verdict !== "representative" && check.decision === undefined, "author_check.decision", `is missing — the verdict is «${check.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\``),
|
|
150
|
+
];
|
|
151
|
+
/** What a schema of one field cannot see: how the fields of a parsed declaration relate. */
|
|
152
|
+
export function crossProblems(p) {
|
|
153
|
+
const values = p.strata.values;
|
|
154
|
+
return [
|
|
155
|
+
...repeated(values).map((v) => ({
|
|
156
|
+
at: "strata.values",
|
|
157
|
+
text: `names «${v}» more than once — each stratum value once`,
|
|
158
|
+
})),
|
|
159
|
+
...p.screens.flatMap((s, i) => removedProblems(s.removed, i, values)),
|
|
160
|
+
...problemIf(p.coverage.share === null && p.coverage.reason === undefined, "coverage.reason", "is missing — `share` is null, so say why the share cannot be stated"),
|
|
161
|
+
...problemIf(p.coverage.share === 1 && p.coverage.unreachable.length > 0, "coverage.unreachable", `contradicts \`share\` 1 — a sample that cannot reach ${p.coverage.unreachable.map((u) => `«${u}»`).join(", ")} does not represent the whole population: lower \`share\`, or empty the list`),
|
|
162
|
+
...checkProblems(p.author_check),
|
|
163
|
+
];
|
|
164
|
+
}
|
|
165
|
+
/** Everything wrong with the parsed JSON of one `prereg.json`, in the file's field order. */
|
|
166
|
+
export function judgePrereg(data) {
|
|
167
|
+
const parsed = PREREG.safeParse(data);
|
|
168
|
+
return parsed.success
|
|
169
|
+
? crossProblems(parsed.data)
|
|
170
|
+
: parsed.error.issues.map((i) => ({
|
|
171
|
+
at: pathText(i.path),
|
|
172
|
+
text: i.message,
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
/** What `JSON.parse` says about the text it refused. */
|
|
176
|
+
const Thrown = z
|
|
177
|
+
.object({ message: z.string() })
|
|
178
|
+
.catch({ message: "unparseable" });
|
|
179
|
+
/** The problems of one `prereg.json` whose text is `source`: a parse failure, or the judgement. */
|
|
180
|
+
export function preregProblems(source) {
|
|
181
|
+
const parsed = (() => {
|
|
182
|
+
try {
|
|
183
|
+
const data = JSON.parse(source);
|
|
184
|
+
return { data };
|
|
185
|
+
}
|
|
186
|
+
catch (e) {
|
|
187
|
+
return { why: Thrown.parse(e).message };
|
|
188
|
+
}
|
|
189
|
+
})();
|
|
190
|
+
return "why" in parsed
|
|
191
|
+
? [
|
|
192
|
+
{
|
|
193
|
+
at: "",
|
|
194
|
+
text: `does not parse as JSON (${parsed.why}) — there is no declaration to read`,
|
|
195
|
+
},
|
|
196
|
+
]
|
|
197
|
+
: judgePrereg(parsed.data);
|
|
198
|
+
}
|
|
199
|
+
/** The one text a problem in the file `file` (relative to the paper) is reported with. */
|
|
200
|
+
export const problemMessage = (file, p) => [
|
|
201
|
+
p.at === ""
|
|
202
|
+
? `\`${file}\` ${p.text}.`
|
|
203
|
+
: `\`${file}\`: \`${p.at}\` ${p.text}.`,
|
|
204
|
+
WHY,
|
|
205
|
+
`Rule study/declare-sample: ${PAGE}`,
|
|
206
|
+
].join(" ");
|
|
207
|
+
/**
|
|
208
|
+
* The `prereg.json` of every study folder at the top of `<paperDir>/repro/`, relative to the paper:
|
|
209
|
+
* every folder there but the shared ones, whatever its name — a study folder misnamed is
|
|
210
|
+
* `repro/allowed-files-only`'s finding, and its pre-registration is still judged here.
|
|
211
|
+
*/
|
|
212
|
+
export function preregFiles(paperDir) {
|
|
213
|
+
const repro = join(paperDir, REPRO_DIR);
|
|
214
|
+
return existsSync(repro) && statSync(repro).isDirectory()
|
|
215
|
+
? readdirSync(repro, { withFileTypes: true })
|
|
216
|
+
// stat, not the entry's own type: a study folder linked into repro/ is a folder to
|
|
217
|
+
// repro/allowed-files-only too, and must not escape the check by being a link
|
|
218
|
+
.filter((e) => !SHARED_DIRS.includes(e.name) &&
|
|
219
|
+
(statSync(join(repro, e.name), {
|
|
220
|
+
throwIfNoEntry: false,
|
|
221
|
+
})?.isDirectory() ??
|
|
222
|
+
false))
|
|
223
|
+
.map((e) => `${REPRO_DIR}/${e.name}/${PREREG_FILE}`)
|
|
224
|
+
.filter((rel) => existsSync(join(paperDir, rel)))
|
|
225
|
+
.sort()
|
|
226
|
+
: [];
|
|
227
|
+
}
|
|
228
|
+
/** A `prereg.json` that is not a regular file (a folder, a socket): reading it would abort the run. */
|
|
229
|
+
const NOT_A_FILE = [
|
|
230
|
+
{
|
|
231
|
+
at: "",
|
|
232
|
+
text: "is not a file — the declaration is one JSON file; move what the folder holds elsewhere",
|
|
233
|
+
},
|
|
234
|
+
];
|
|
235
|
+
/** Every finding for the paper folder `paperDir`, one text each. */
|
|
236
|
+
export const paperFindings = (paperDir) => preregFiles(paperDir).flatMap((rel) => {
|
|
237
|
+
const path = join(paperDir, rel);
|
|
238
|
+
const problems = statSync(path).isFile()
|
|
239
|
+
? preregProblems(readFileSync(path, "utf8"))
|
|
240
|
+
: NOT_A_FILE;
|
|
241
|
+
return problems.map((p) => problemMessage(rel, p));
|
|
242
|
+
});
|
|
243
|
+
export default {
|
|
244
|
+
rules: {
|
|
245
|
+
"declare-sample": {
|
|
246
|
+
meta: {
|
|
247
|
+
type: "problem",
|
|
248
|
+
docs: {
|
|
249
|
+
description: "every study's prereg.json declares its population, the screens and what each removed per stratum, the sample's coverage, and the author's check of the drawn units",
|
|
250
|
+
url: PAGE,
|
|
251
|
+
},
|
|
252
|
+
schema: [],
|
|
253
|
+
messages: { coverage: "{{text}}" },
|
|
254
|
+
},
|
|
255
|
+
create(context) {
|
|
256
|
+
if (basename(context.filename) !== STATUS_FILE)
|
|
257
|
+
return {};
|
|
258
|
+
return {
|
|
259
|
+
root() {
|
|
260
|
+
const at = context.sourceCode.getLocFromIndex(0);
|
|
261
|
+
paperFindings(dirname(context.filename)).forEach((t) => {
|
|
262
|
+
context.report({
|
|
263
|
+
loc: { start: at, end: at },
|
|
264
|
+
messageId: "coverage",
|
|
265
|
+
data: { text: t },
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
//# sourceMappingURL=prereg-coverage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prereg-coverage.js","sourceRoot":"","sources":["../../eslint-rules/prereg-coverage.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type FolderLayoutDeps } from "./folder-layout-rule.ts";
|
|
2
|
+
import type { Files } from "./ports/files.ts";
|
|
3
|
+
/** What Claude Code reads from a PreToolUse hook's stdout to refuse the call, with the reason. */
|
|
4
|
+
export interface HookOutput {
|
|
5
|
+
readonly hookSpecificOutput: {
|
|
6
|
+
readonly hookEventName: "PreToolUse";
|
|
7
|
+
readonly permissionDecision: "deny";
|
|
8
|
+
readonly permissionDecisionReason: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/** The target file's absolute path from the payload; null when there is none to read. */
|
|
12
|
+
export declare function writtenPath(payload: unknown): string | null;
|
|
13
|
+
/**
|
|
14
|
+
* The nearest folder above `file` holding `PIPELINE-STATUS.md`, up to the root; null when none, and
|
|
15
|
+
* null for anything under a folder named `extra/`, which lint never reads: an archived paper kept
|
|
16
|
+
* there carries its own status file and is not a paper to hold to the layout.
|
|
17
|
+
*/
|
|
18
|
+
export declare function paperFolderOf(file: string, files: Files): string | null;
|
|
19
|
+
/** The hook: the payload of an Edit or a Write in, Claude Code's refusal out, or null to allow. */
|
|
20
|
+
export declare function folderLayoutHook(payload: unknown, deps: FolderLayoutDeps): HookOutput | null;
|
|
21
|
+
//# sourceMappingURL=folder-layout-hook.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"folder-layout-hook.d.ts","sourceRoot":"","sources":["../src/folder-layout-hook.ts"],"names":[],"mappings":"AA8CA,OAAO,EAAc,KAAK,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAM9C,kGAAkG;AAClG,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,kBAAkB,EAAE;QAC3B,QAAQ,CAAC,aAAa,EAAE,YAAY,CAAC;QACrC,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;QACpC,QAAQ,CAAC,wBAAwB,EAAE,MAAM,CAAC;KAC3C,CAAC;CACH;AAED,yFAAyF;AACzF,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAO3D;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,GAAG,MAAM,GAAG,IAAI,CASvE;AAmCD,mGAAmG;AACnG,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,gBAAgB,GACrB,UAAU,GAAG,IAAI,CAenB"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `paper-folder-layout` hook — `paper/allowed-files-only` and `repro/allowed-files-only` BEFORE a file is
|
|
3
|
+
* created, not at the next lint. PreToolUse on `Edit|Write|MultiEdit` (`plugin/hooks/hooks.json`):
|
|
4
|
+
* when the file about to be written does not exist yet and would add an entry the layout does not
|
|
5
|
+
* allow — at a paper folder's root, or at the top of its `repro/` — the call is denied, and the
|
|
6
|
+
* reason is the rule's own message: where the entry goes, and the layout drawn. The agent writes it
|
|
7
|
+
* where it belongs instead, in the same turn.
|
|
8
|
+
*
|
|
9
|
+
* ── WHAT IT NEVER BLOCKS ─────────────────────────────────────────────────────────
|
|
10
|
+
* - A file that already exists. Editing is never refused, wherever the file is: a paper whose
|
|
11
|
+
* folder predates the layout keeps working, and the rule reports what is out of place.
|
|
12
|
+
* - A new file inside an entry that already exists, even one the layout does not allow: the entry is
|
|
13
|
+
* not new, and the rule reports it once, by name. Only a NEW entry is refused.
|
|
14
|
+
* - Anything outside a paper folder (no `PIPELINE-STATUS.md` above it).
|
|
15
|
+
* A new study folder in `repro/` is created with its `README.md` first: any other first file in it
|
|
16
|
+
* is refused with `repro/allowed-files-only`'s "without a README.md" message.
|
|
17
|
+
*
|
|
18
|
+
* ── WHAT IT CANNOT SEE ───────────────────────────────────────────────────────────
|
|
19
|
+
* A shell command (`mv`, `cp`, a script writing files) is not an Edit or a Write, and this hook never
|
|
20
|
+
* sees it. The guarantee is the lint: both rules fail `paperlint lint`, in CI, whatever wrote the
|
|
21
|
+
* entry. The hook only saves the round trip. It does not read the paper's `rules` either: a paper
|
|
22
|
+
* that switched a layout rule off is still refused a new stray entry.
|
|
23
|
+
*
|
|
24
|
+
* ── ONE FUNCTION PER RULE, NOT TWO LISTS ─────────────────────────────────────────
|
|
25
|
+
* The texts are `strayTexts` (`src/folder-layout-rule.ts`) and `reproTexts`
|
|
26
|
+
* (`src/repro-layout-rule.ts`), the functions the rules report with.
|
|
27
|
+
*
|
|
28
|
+
* ── WHY IT IS NOT A VIGILES PROGRAM LIKE ITS NEIGHBOURS (measured 2026-10-12, vigiles 38) ──────
|
|
29
|
+
* A shipped `.hook.mjs` may import nothing but `vigiles/hook` (`checkHookImports`), so it cannot call
|
|
30
|
+
* this package's function; its only way to a tool is `run()`, whose output reaches stderr and never
|
|
31
|
+
* the hook's answer (`paper-status-gates.hook.mjs` measured that); and a `provide()` command runs
|
|
32
|
+
* before the reaction without the event, so it cannot know which file is written. Each of the three
|
|
33
|
+
* would mean a second copy of the layout. So `paperlint hook paper-folder-layout` runs this module
|
|
34
|
+
* in-process (`runHook`, src/cli.ts), behind the same wiring command as the other hooks, which is
|
|
35
|
+
* what `paperlint init` merges and `paperlint doctor` reads.
|
|
36
|
+
*
|
|
37
|
+
* ── THE CHEAP FIRST STEP ─────────────────────────────────────────────────────────
|
|
38
|
+
* Before reading any settings or listing any folder, the hook looks for the nearest folder above the
|
|
39
|
+
* file that holds `PIPELINE-STATUS.md` — the file every paper has and the rules are reported on — with
|
|
40
|
+
* one `isFile` per ancestor and nothing else: no git, no lint. No such folder: no output.
|
|
41
|
+
*/
|
|
42
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
43
|
+
import { callerPath } from "./caller-path.js";
|
|
44
|
+
import { ESCAPE_HATCH } from "./domain/paper-layout.js";
|
|
45
|
+
import { README, REPRO_DIR, isStudyName } from "./domain/repro-layout.js";
|
|
46
|
+
import { fieldOf } from "./domain/record.js";
|
|
47
|
+
import { strayTexts } from "./folder-layout-rule.js";
|
|
48
|
+
import { reproTexts } from "./repro-layout-rule.js";
|
|
49
|
+
/** The file every paper folder has, and the rules are reported on. */
|
|
50
|
+
const STATUS_FILE = "PIPELINE-STATUS.md";
|
|
51
|
+
/** The target file's absolute path from the payload; null when there is none to read. */
|
|
52
|
+
export function writtenPath(payload) {
|
|
53
|
+
const input = fieldOf(payload, "tool_input");
|
|
54
|
+
const raw = fieldOf(input, "file_path") ?? fieldOf(input, "notebook_path");
|
|
55
|
+
if (typeof raw !== "string" || raw === "")
|
|
56
|
+
return null;
|
|
57
|
+
if (isAbsolute(raw))
|
|
58
|
+
return resolve(raw);
|
|
59
|
+
const cwd = fieldOf(payload, "cwd");
|
|
60
|
+
return typeof cwd === "string" && isAbsolute(cwd) ? resolve(cwd, raw) : null;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The nearest folder above `file` holding `PIPELINE-STATUS.md`, up to the root; null when none, and
|
|
64
|
+
* null for anything under a folder named `extra/`, which lint never reads: an archived paper kept
|
|
65
|
+
* there carries its own status file and is not a paper to hold to the layout.
|
|
66
|
+
*/
|
|
67
|
+
export function paperFolderOf(file, files) {
|
|
68
|
+
// The same reach as lint's `**/extra/` ignore: a folder named extra/ anywhere above the file.
|
|
69
|
+
if (dirname(file).split(sep).includes(ESCAPE_HATCH))
|
|
70
|
+
return null;
|
|
71
|
+
const up = (dir) => {
|
|
72
|
+
if (files.isFile(callerPath(join(dir, STATUS_FILE))))
|
|
73
|
+
return dir;
|
|
74
|
+
const parent = dirname(dir);
|
|
75
|
+
return parent === dir ? null : up(parent);
|
|
76
|
+
};
|
|
77
|
+
return up(dirname(file));
|
|
78
|
+
}
|
|
79
|
+
/** The entry `segments` would add under `dir`, as the layout judges it; null when it exists. */
|
|
80
|
+
const newEntry = (dir, [name = "", ...below], deps) => deps.listDir(callerPath(dir)).includes(name)
|
|
81
|
+
? null
|
|
82
|
+
: { name, kind: below.length > 0 ? "dir" : "file" };
|
|
83
|
+
/** Why creating `segments` (relative to the paper folder `dir`) breaks the layout; null when not. */
|
|
84
|
+
function refusal(dir, segments, deps) {
|
|
85
|
+
const atRoot = newEntry(dir, segments, deps);
|
|
86
|
+
const [rootText] = atRoot === null ? [] : strayTexts(dir, [atRoot], deps.files);
|
|
87
|
+
if (rootText !== undefined)
|
|
88
|
+
return rootText;
|
|
89
|
+
const [top, ...inRepro] = segments;
|
|
90
|
+
if (top !== REPRO_DIR || inRepro.length === 0)
|
|
91
|
+
return null;
|
|
92
|
+
const entry = newEntry(join(dir, REPRO_DIR), inRepro, deps);
|
|
93
|
+
// A new study folder starts with its README.md: that write is what makes it complete.
|
|
94
|
+
if (entry === null ||
|
|
95
|
+
(isStudyName(entry.name) && inRepro.slice(1).join("/") === README))
|
|
96
|
+
return null;
|
|
97
|
+
const [text] = reproTexts(dir, [entry], deps.files);
|
|
98
|
+
return text ?? null;
|
|
99
|
+
}
|
|
100
|
+
/** The hook: the payload of an Edit or a Write in, Claude Code's refusal out, or null to allow. */
|
|
101
|
+
export function folderLayoutHook(payload, deps) {
|
|
102
|
+
const file = writtenPath(payload);
|
|
103
|
+
const dir = file === null ? null : paperFolderOf(file, deps.files);
|
|
104
|
+
if (file === null || dir === null || deps.files.isFile(callerPath(file)))
|
|
105
|
+
return null;
|
|
106
|
+
const reason = refusal(dir, relative(dir, file).split(/[\\/]/), deps);
|
|
107
|
+
return reason === null
|
|
108
|
+
? null
|
|
109
|
+
: {
|
|
110
|
+
hookSpecificOutput: {
|
|
111
|
+
hookEventName: "PreToolUse",
|
|
112
|
+
permissionDecision: "deny",
|
|
113
|
+
permissionDecisionReason: reason,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=folder-layout-hook.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"folder-layout-hook.js","sourceRoot":"","sources":["../src/folder-layout-hook.ts"],"names":[],"mappings":"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"}
|