paperlint 2.0.1 → 3.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/CLAUDE.md +6 -1
- package/CONTRIBUTING.md +21 -0
- package/README.md +231 -180
- package/action.yml +4 -6
- package/dist/adapters/references/index.d.ts +13 -0
- package/dist/adapters/references/index.d.ts.map +1 -0
- package/dist/adapters/references/index.js +55 -0
- package/dist/adapters/references/index.js.map +1 -0
- package/dist/adapters/references/reach.io.d.ts +3 -0
- package/dist/adapters/references/reach.io.d.ts.map +1 -0
- package/dist/adapters/references/reach.io.js +14 -0
- package/dist/adapters/references/reach.io.js.map +1 -0
- package/dist/build-engine.d.ts.map +1 -1
- package/dist/build-engine.js +5 -2
- package/dist/build-engine.js.map +1 -1
- package/dist/build.d.ts +16 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +40 -5
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +37 -20
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +377 -251
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +37 -52
- package/dist/doctor.js.map +1 -1
- package/dist/facts-file.d.ts +33 -9
- package/dist/facts-file.d.ts.map +1 -1
- package/dist/facts-file.js +117 -17
- package/dist/facts-file.js.map +1 -1
- package/dist/hooks-settings.d.ts +0 -21
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +17 -135
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +40 -19
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +144 -109
- package/dist/init.js.map +1 -1
- package/dist/link-skills.d.ts +1 -1
- package/dist/link-skills.d.ts.map +1 -1
- package/dist/link-skills.js +11 -29
- package/dist/link-skills.js.map +1 -1
- package/dist/new-paper.d.ts +2 -2
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +6 -2
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-files.d.ts +63 -0
- package/dist/paper-files.d.ts.map +1 -0
- package/dist/paper-files.js +69 -0
- package/dist/paper-files.js.map +1 -0
- package/dist/paper-settings.d.ts +40 -0
- package/dist/paper-settings.d.ts.map +1 -0
- package/dist/paper-settings.js +144 -0
- package/dist/paper-settings.js.map +1 -0
- package/dist/ports/check-references.d.ts +31 -0
- package/dist/ports/check-references.d.ts.map +1 -0
- package/dist/ports/check-references.js +7 -0
- package/dist/ports/check-references.js.map +1 -0
- package/dist/presets.d.ts +84 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +184 -0
- package/dist/presets.js.map +1 -0
- package/dist/reference-rules.d.ts +47 -0
- package/dist/reference-rules.d.ts.map +1 -0
- package/dist/reference-rules.js +156 -0
- package/dist/reference-rules.js.map +1 -0
- package/dist/references.d.ts +36 -0
- package/dist/references.d.ts.map +1 -0
- package/dist/references.js +91 -0
- package/dist/references.js.map +1 -0
- package/dist/rules-config.d.ts +11 -2
- package/dist/rules-config.d.ts.map +1 -1
- package/dist/rules-config.js +25 -11
- package/dist/rules-config.js.map +1 -1
- package/dist/structure.d.ts.map +1 -1
- package/dist/structure.js +3 -2
- package/dist/structure.js.map +1 -1
- package/dist/tex-requirements.d.ts +63 -7
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +91 -24
- package/dist/tex-requirements.js.map +1 -1
- package/dist/types.d.ts +20 -18
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts +109 -0
- package/dist/venue-rules.d.ts.map +1 -0
- package/dist/venue-rules.js +389 -0
- package/dist/venue-rules.js.map +1 -0
- package/docs/configuration.md +152 -77
- package/docs/e2e.md +1 -1
- package/docs/install.md +25 -40
- package/docs/optional-rules.md +26 -24
- package/docs/rules.md +181 -15
- package/eslint-rules/bib-reachable-entry.mjs +75 -0
- package/eslint-rules/latex-language.mjs +29 -1
- package/eslint-rules/paper-stages.harness.mjs +1 -117
- package/eslint-rules/paper-stages.mjs +0 -120
- package/eslint-rules/paper-stages.mutations.mjs +0 -76
- package/eslint-rules/paper-typography.mjs +267 -192
- package/eslint-rules/paper-typography.test.mjs +297 -0
- package/eslint-rules/papers.harness.mjs +8 -12
- package/eslint-rules/papers.mjs +17 -27
- package/eslint-rules/pdf-last-page-balance.mjs +23 -5
- package/eslint-rules/review-frontmatter.mjs +118 -0
- package/eslint-rules/review-frontmatter.schema.json +36 -0
- package/eslint-rules/review-frontmatter.test.mjs +118 -0
- package/eslint-rules/sibling-frontmatter.mjs +19 -0
- package/eslint-rules/sibling-frontmatter.schema.json +14 -0
- package/eslint.config.mjs +0 -15
- package/fixtures/build-e2e/acmart/paperlint.json +1 -0
- package/fixtures/real-markdown-paper/baseline.json +6 -7
- package/hooks/hooks.harness.mjs +39 -98
- package/hooks/hooks.mutations.mjs +6 -6
- package/hooks/paper-edit-guard.hook.d.mts +2 -3
- package/hooks/paper-edit-guard.hook.mjs +31 -42
- package/hooks/paper-skills-nudge.hook.mjs +21 -19
- package/hooks/paper-status-gates.hook.mjs +21 -19
- package/hooks/paper-status-gates.sh +8 -13
- package/lib/paper-config.d.mts +15 -18
- package/lib/paper-config.harness.mjs +18 -35
- package/lib/paper-config.mjs +90 -81
- package/lib/paper-config.mutations.mjs +2 -2
- package/lib/skill-checks.mjs +1 -1
- package/lib/skill-trigger-cases.harness.mjs +9 -8
- package/lib/skill-trigger-cases.mjs +7 -15
- package/package.json +1 -1
- package/scripts/eslint-report-guard.mjs +1 -1
- package/scripts/harness-api.frozen.json +0 -3
- package/scripts/mutation-batteries.frozen.json +4 -13
- package/skills/analyze-sibling-paper/SKILL.md +16 -12
- package/skills/analyze-sibling-paper/SKILL.md.spec.ts +15 -11
- package/skills/cold-read-diff/SKILL.md +20 -1
- package/skills/cold-read-diff/SKILL.md.spec.ts +19 -0
- package/skills/find-venue/SKILL.md +5 -1
- package/skills/find-venue/SKILL.md.spec.ts +4 -0
- package/skills/grade-paper-writing/SKILL.md +20 -1
- package/skills/grade-paper-writing/SKILL.md.spec.ts +19 -0
- package/skills/paper-pipeline/PIPELINE-MAP.md +14 -4
- package/skills/paper-pipeline/SKILL.md +4 -3
- package/skills/paper-pipeline/SKILL.md.spec.ts +3 -2
- package/skills/paper-pipeline/scripts/consumer.d.mts +0 -1
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +8 -12
- package/skills/paper-pipeline/scripts/consumer.mjs +19 -33
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +6 -4
- package/skills/paper-pipeline/scripts/run-mechanical.mjs +2 -11
- package/skills/plan-paper-timeline/SKILL.md +2 -2
- package/skills/plan-paper-timeline/SKILL.md.spec.ts +1 -1
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +1 -1
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +2 -2
- package/skills/render-paper/check-render.sh +2 -3
- package/skills/render-paper/extract-pdf-facts.harness.mjs +5 -5
- package/skills/render-paper/extract-pdf-facts.mjs +4 -4
- package/skills/render-paper/render-paper.harness.mjs +2 -2
- package/skills/study-accepted-papers/SKILL.md +2 -1
- package/skills/study-accepted-papers/SKILL.md.spec.ts +1 -0
- package/skills/submit-paper/SKILL.md +9 -1
- package/skills/submit-paper/SKILL.md.spec.ts +8 -0
- package/skills/submit-paper/references/venues/acm-sigconf.jsonc +86 -0
- package/skills/submit-paper/references/venues/agenticdev.jsonc +34 -102
- package/skills/submit-paper/references/venues/aisec.jsonc +21 -82
- package/skills/submit-paper/references/venues/realm.jsonc +45 -43
- package/skills/submit-paper/references/venues/tex-base.jsonc +1 -1
- package/skills/submit-paper/references/venues/venue-profile.schema.json +119 -29
- package/skills/verify-citations/SKILL.md +10 -1
- package/skills/verify-citations/SKILL.md.spec.ts +9 -0
- package/skills/verify-citations/scripts/bib-authors.mjs +48 -26
- package/src/adapters/references/index.ts +102 -0
- package/src/adapters/references/reach.io.ts +12 -0
- package/src/build-engine.ts +10 -2
- package/src/build.harness.mjs +31 -22
- package/src/build.ts +65 -8
- package/src/cli.harness.mjs +151 -294
- package/src/cli.mutations.mjs +35 -44
- package/src/cli.ts +457 -288
- package/src/doctor.harness.mjs +41 -84
- package/src/doctor.mutations.mjs +9 -9
- package/src/doctor.ts +41 -61
- package/src/facts-file.test.ts +47 -7
- package/src/facts-file.ts +145 -24
- package/src/hooks-settings.harness.mjs +7 -92
- package/src/hooks-settings.mutations.mjs +2 -17
- package/src/hooks-settings.ts +18 -162
- package/src/init.test.ts +206 -0
- package/src/init.ts +193 -159
- package/src/link-skills.harness.mjs +1 -38
- package/src/link-skills.mutations.mjs +9 -5
- package/src/link-skills.ts +10 -34
- package/src/lint-fix.test.ts +120 -0
- package/src/new-paper.harness.mjs +9 -6
- package/src/new-paper.test.ts +133 -0
- package/src/new-paper.ts +7 -2
- package/src/paper-files.test.ts +290 -0
- package/src/paper-files.ts +144 -0
- package/src/paper-settings-commands.test.ts +274 -0
- package/src/paper-settings.test.ts +249 -0
- package/src/paper-settings.ts +208 -0
- package/src/ports/check-references.ts +28 -0
- package/src/presets.test.ts +236 -0
- package/src/presets.ts +297 -0
- package/src/reference-rules.ts +229 -0
- package/src/references.test.ts +209 -0
- package/src/references.ts +138 -0
- package/src/rules-config.harness.mjs +24 -8
- package/src/rules-config.ts +30 -13
- package/src/structure.harness.mjs +6 -4
- package/src/structure.ts +3 -2
- package/src/tex-requirements.harness.mjs +11 -17
- package/src/tex-requirements.ts +179 -29
- package/src/types.ts +30 -18
- package/src/venue-rules.test.ts +554 -0
- package/src/venue-rules.ts +571 -0
- package/templates/paper/paperlint.json +4 -0
- package/eslint-rules/doc-fields.harness.mjs +0 -336
- package/eslint-rules/doc-fields.mjs +0 -186
- package/eslint-rules/doc-fields.mutations.mjs +0 -96
- package/eslint-rules/paper-typography.harness.mjs +0 -291
- package/eslint-rules/paper-typography.mutations.mjs +0 -131
- package/eslint-rules/review-findings-cause.harness.mjs +0 -228
- package/eslint-rules/review-findings-cause.mjs +0 -135
- package/eslint-rules/review-findings-cause.mutations.mjs +0 -72
- package/fixtures/build-e2e/acmart/venue.json +0 -1
- package/fixtures/paper-stages/authors-ran/PIPELINE-STATUS.md +0 -16
- package/fixtures/paper-stages/marker-in-prose/PIPELINE-STATUS.md +0 -17
- package/fixtures/review-findings-cause/clean.md +0 -17
- package/fixtures/review-findings-cause/defect.md +0 -14
- package/fixtures/review-findings-cause/old-debt.md +0 -14
- package/fixtures/review-findings-cause/quiet-in-fence.md +0 -16
|
@@ -43,13 +43,13 @@
|
|
|
43
43
|
* this cannot — there is only one file.
|
|
44
44
|
*
|
|
45
45
|
* ── WHERE THE CONSUMER'S PAPERS ARE ─────────────────────────────────────────
|
|
46
|
-
* One declaration, `
|
|
47
|
-
* read anything at all: `
|
|
46
|
+
* One declaration, `{ "papersDir": "…" }` in the root `paperlint.json` (optional; default
|
|
47
|
+
* `papers`), read the way a hook is able to read anything at all: a `provide` that cats the file.
|
|
48
48
|
* The alternatives were measured and killed in `eslint-rules/papers.mjs` — an env var cannot be
|
|
49
49
|
* read from a hook at all (no imports), and a symlinked root makes ESLint report zero files.
|
|
50
50
|
*
|
|
51
51
|
* 🔴 THE PATH IS ANCHORED, AND THAT IS NOT DECORATION. vigiles runs a provider "via execSync in
|
|
52
|
-
* the hook's cwd", so a bare `cat
|
|
52
|
+
* the hook's cwd", so a bare `cat paperlint.json` resolves against whatever directory the hook
|
|
53
53
|
* PROCESS happens to have — which is set by the consumer's wiring, a string this hook cannot
|
|
54
54
|
* see and does not control. With the wiring this package documents (`cd "$CLAUDE_PROJECT_DIR"
|
|
55
55
|
* && node …`) the bare read is correct; without it, any tool cwd outside the project root makes
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
* when the variable is absent, so this is strictly wider than what it replaces.
|
|
64
64
|
*
|
|
65
65
|
* 🔴 AN UNREADABLE DECLARATION DENIES, IT DOES NOT DEFAULT. Measured 2026-09-12: when the
|
|
66
|
-
* provider's command fails —
|
|
67
|
-
* arrives as the EMPTY STRING and the runtime says nothing. A `try { JSON.parse } catch { use the
|
|
66
|
+
* provider's command fails — an unreadable file, or one with conflict markers in it — `e.ctx.config`
|
|
67
|
+
* arrives as the EMPTY STRING (or text that is not JSON) and the runtime says nothing. A `try { JSON.parse } catch { use the
|
|
68
68
|
* default }` therefore turns a broken declaration into a silently permissive gate: the prefix
|
|
69
69
|
* quietly becomes `papers`, nothing under the real root matches, and every Bash write to a paper
|
|
70
70
|
* is allowed by a gate that still looks installed and green. So parse failure is a `deny` that
|
|
@@ -107,19 +107,29 @@ import {
|
|
|
107
107
|
deny,
|
|
108
108
|
} from "vigiles/hook";
|
|
109
109
|
|
|
110
|
-
/** The
|
|
110
|
+
/** The tool's name — the package this hook resolves its scripts through. */
|
|
111
111
|
export const CONFIG_KEY = "paperlint";
|
|
112
|
-
/** The
|
|
113
|
-
export const
|
|
112
|
+
/** The settings file at the project root. Optional: absent means every default. */
|
|
113
|
+
export const CONFIG_FILE = "paperlint.json";
|
|
114
114
|
/** The default. A consumer that declares nothing is assumed to keep papers in `papers/`. */
|
|
115
115
|
export const DEFAULT_PAPERS_ROOT = "papers";
|
|
116
116
|
/**
|
|
117
|
-
* The field
|
|
117
|
+
* The field of the root `paperlint.json` that names the papers directory. These are copies of the
|
|
118
118
|
* constants in `lib/paper-config.mjs` (a hook may import nothing but `vigiles/hook`);
|
|
119
119
|
* `lib/paper-config.harness.mjs` checks that the copies match.
|
|
120
120
|
*/
|
|
121
121
|
export const PAPERS_DIR_FIELD = "papersDir";
|
|
122
|
-
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The provider: the root `paperlint.json`, or `{}` when the project has none — so an ABSENT file
|
|
125
|
+
* (every default) and an UNREADABLE one (the empty string the runtime gives a failed provider)
|
|
126
|
+
* stay two different inputs.
|
|
127
|
+
*
|
|
128
|
+
* 🔴 THE PATH IS ANCHORED TO THE PROJECT ROOT. vigiles runs a provider "via execSync in the hook's
|
|
129
|
+
* cwd", which is the consumer's wiring — a string this hook cannot see. A bare relative path reads
|
|
130
|
+
* from whatever directory the Bash tool last moved to.
|
|
131
|
+
*/
|
|
132
|
+
const READ_CONFIG = `f="\${CLAUDE_PROJECT_DIR:-.}/${CONFIG_FILE}"; if [ -e "$f" ]; then cat "$f"; else echo '{}'; fi`;
|
|
123
133
|
|
|
124
134
|
/**
|
|
125
135
|
* The declared papers root, or a `deny` explaining why there is not one.
|
|
@@ -140,51 +150,30 @@ export const OLD_PAPERS_DIR_FIELD = "papers";
|
|
|
140
150
|
* doesn't restrict exporting outward, and the reverse direction, CLI → hook, is free.
|
|
141
151
|
* Returns the root string, or a rejection object: the caller tells them apart by `typeof`.
|
|
142
152
|
*/
|
|
143
|
-
export const papersRoot = (
|
|
144
|
-
let
|
|
153
|
+
export const papersRoot = (rawConfig) => {
|
|
154
|
+
let config;
|
|
145
155
|
try {
|
|
146
|
-
|
|
156
|
+
config = JSON.parse(rawConfig);
|
|
147
157
|
} catch {
|
|
148
158
|
return deny(
|
|
149
|
-
`${CONFIG_KEY}: this gate could not read the
|
|
159
|
+
`${CONFIG_KEY}: this gate could not read the project's ${CONFIG_FILE}, so it does not know ` +
|
|
150
160
|
`where papers live and is refusing rather than guessing.\n` +
|
|
151
161
|
`The declaration it needs is:\n` +
|
|
152
|
-
`
|
|
153
|
-
`(no
|
|
154
|
-
`If
|
|
162
|
+
` { "${PAPERS_DIR_FIELD}": "path/to/papers" }\n` +
|
|
163
|
+
`(no file at all is fine too — the default is "${DEFAULT_PAPERS_ROOT}").\n` +
|
|
164
|
+
`If ${CONFIG_FILE} is mid-merge or otherwise broken, fix it with Edit or Write: file ` +
|
|
155
165
|
`tools do not pass through this gate, so that path out is always open.\n` +
|
|
156
166
|
`This refuses rather than falling back on purpose. A gate whose prefix quietly became ` +
|
|
157
167
|
`the default would allow every Bash write to the real papers root while still looking ` +
|
|
158
168
|
`installed and green.`,
|
|
159
169
|
);
|
|
160
170
|
}
|
|
161
|
-
|
|
162
|
-
// refused: the gate cannot know which one the author means.
|
|
163
|
-
const current = pkg?.[CONFIG_KEY];
|
|
164
|
-
const legacy = pkg?.[LEGACY_CONFIG_KEY];
|
|
165
|
-
if (
|
|
166
|
-
current !== undefined &&
|
|
167
|
-
legacy !== undefined &&
|
|
168
|
-
JSON.stringify(current) !== JSON.stringify(legacy)
|
|
169
|
-
)
|
|
170
|
-
return deny(
|
|
171
|
-
`package.json has both "${CONFIG_KEY}" and "${LEGACY_CONFIG_KEY}", and they differ. ` +
|
|
172
|
-
`Keep "${CONFIG_KEY}" and delete "${LEGACY_CONFIG_KEY}" (its old name). ` +
|
|
173
|
-
`Fix it with Edit or Write: file tools do not pass through this gate.`,
|
|
174
|
-
);
|
|
175
|
-
const settings = current ?? legacy;
|
|
176
|
-
// The old field name is refused, not read as a fallback: this gate would otherwise guard the
|
|
177
|
-
// default directory while the consumer believes it guards the one they declared.
|
|
178
|
-
if (settings && Object.hasOwn(settings, OLD_PAPERS_DIR_FIELD))
|
|
179
|
-
return deny(
|
|
180
|
-
`"${OLD_PAPERS_DIR_FIELD}" was renamed to "${PAPERS_DIR_FIELD}" in package.json → "${CONFIG_KEY}"`,
|
|
181
|
-
);
|
|
182
|
-
const declared = settings?.[PAPERS_DIR_FIELD];
|
|
171
|
+
const declared = config?.[PAPERS_DIR_FIELD];
|
|
183
172
|
const root = declared === undefined ? DEFAULT_PAPERS_ROOT : declared;
|
|
184
173
|
if (typeof root !== "string" || root.length === 0)
|
|
185
174
|
return deny(
|
|
186
175
|
`${CONFIG_KEY}: "${PAPERS_DIR_FIELD}" must be a non-empty string, got ${JSON.stringify(root)}.\n` +
|
|
187
|
-
`Fix it in the
|
|
176
|
+
`Fix it in the project's ${CONFIG_FILE}. This gate refuses while the value is unusable: ` +
|
|
188
177
|
`an empty prefix matches nothing, so the gate would pass every paper write in silence.`,
|
|
189
178
|
);
|
|
190
179
|
// One trailing slash at most, whatever the consumer typed: `"papersDir": "docs/papers/"` reaches
|
|
@@ -425,9 +414,9 @@ const namesPaperSource = (raw, prefixes) =>
|
|
|
425
414
|
export default experimental_defineHook({
|
|
426
415
|
on: "PreToolUse",
|
|
427
416
|
match: tools("Bash"),
|
|
428
|
-
needs: [provide("
|
|
417
|
+
needs: [provide("config", READ_CONFIG)],
|
|
429
418
|
decide: (e) => {
|
|
430
|
-
const root = papersRoot(e.ctx.
|
|
419
|
+
const root = papersRoot(e.ctx.config);
|
|
431
420
|
// A `deny` object rather than a string means the root could not be established. Returning it
|
|
432
421
|
// unchanged is deliberate: there is no root to continue with, so there is nothing to decide.
|
|
433
422
|
if (typeof root !== "string") return root;
|
|
@@ -38,19 +38,29 @@ import {
|
|
|
38
38
|
nothing,
|
|
39
39
|
} from "vigiles/hook";
|
|
40
40
|
|
|
41
|
-
/** The
|
|
41
|
+
/** The tool's name — the package this hook resolves its scripts through. */
|
|
42
42
|
export const CONFIG_KEY = "paperlint";
|
|
43
|
-
/** The
|
|
44
|
-
export const
|
|
43
|
+
/** The settings file at the project root. Optional: absent means every default. */
|
|
44
|
+
export const CONFIG_FILE = "paperlint.json";
|
|
45
45
|
/** The default. A consumer that declares nothing is assumed to keep papers in `papers/`. */
|
|
46
46
|
export const DEFAULT_PAPERS_ROOT = "papers";
|
|
47
47
|
/**
|
|
48
|
-
* The field
|
|
48
|
+
* The field of the root `paperlint.json` that names the papers directory. These are copies of the
|
|
49
49
|
* constants in `lib/paper-config.mjs` (a hook may import nothing but `vigiles/hook`);
|
|
50
50
|
* `lib/paper-config.harness.mjs` checks that the copies match.
|
|
51
51
|
*/
|
|
52
52
|
export const PAPERS_DIR_FIELD = "papersDir";
|
|
53
|
-
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The provider: the root `paperlint.json`, or `{}` when the project has none — so an ABSENT file
|
|
56
|
+
* (every default) and an UNREADABLE one (the empty string the runtime gives a failed provider)
|
|
57
|
+
* stay two different inputs.
|
|
58
|
+
*
|
|
59
|
+
* 🔴 THE PATH IS ANCHORED TO THE PROJECT ROOT. vigiles runs a provider "via execSync in the hook's
|
|
60
|
+
* cwd", which is the consumer's wiring — a string this hook cannot see. A bare relative path reads
|
|
61
|
+
* from whatever directory the Bash tool last moved to.
|
|
62
|
+
*/
|
|
63
|
+
const READ_CONFIG = `f="\${CLAUDE_PROJECT_DIR:-.}/${CONFIG_FILE}"; if [ -e "$f" ]; then cat "$f"; else echo '{}'; fi`;
|
|
54
64
|
|
|
55
65
|
/**
|
|
56
66
|
* The declared papers root, or `null` when it is unusable.
|
|
@@ -74,14 +84,10 @@ export const OLD_PAPERS_DIR_FIELD = "papers";
|
|
|
74
84
|
* 🔴 `declared === undefined`, NOT `declared ?? DEFAULT` — `"papersDir": null` is a keystroke, not
|
|
75
85
|
* an absence. Same distinction as every other carrier in this package.
|
|
76
86
|
*/
|
|
77
|
-
const papersRoot = (
|
|
87
|
+
const papersRoot = (rawConfig) => {
|
|
78
88
|
let declared;
|
|
79
89
|
try {
|
|
80
|
-
|
|
81
|
-
const settings = pkg?.[CONFIG_KEY] ?? pkg?.[LEGACY_CONFIG_KEY];
|
|
82
|
-
// Old field name: stay silent rather than fall back to the default directory.
|
|
83
|
-
if (settings && Object.hasOwn(settings, OLD_PAPERS_DIR_FIELD)) return null;
|
|
84
|
-
declared = settings?.[PAPERS_DIR_FIELD];
|
|
90
|
+
declared = JSON.parse(rawConfig)?.[PAPERS_DIR_FIELD];
|
|
85
91
|
} catch {
|
|
86
92
|
return null;
|
|
87
93
|
}
|
|
@@ -113,21 +119,17 @@ RULES earned the hard way:
|
|
|
113
119
|
export default experimental_defineReact({
|
|
114
120
|
on: "PostToolUse",
|
|
115
121
|
match: tools("Edit", "Write", "MultiEdit"),
|
|
116
|
-
//
|
|
117
|
-
// provider "via execSync in the hook's cwd", and the hook process's cwd is the consumer's
|
|
118
|
-
// own wiring — a string this hook cannot see. A bare `cat package.json` therefore reads from
|
|
119
|
-
// whatever directory the Bash tool last moved to, and a `cd` into a subdirectory with no
|
|
120
|
-
// manifest breaks the read.
|
|
122
|
+
// The provider is anchored to the project root (`READ_CONFIG`, above).
|
|
121
123
|
//
|
|
122
124
|
// ⚠️ THE FAILURE HERE IS SILENT, which makes it more dangerous than its neighbor's.
|
|
123
125
|
// `paper-edit-guard` on PreToolUse DENIES loudly and visibly when the declaration is
|
|
124
126
|
// unreadable. This hook on PostToolUse just returns `nothing()`, i.e. simply stops firing:
|
|
125
|
-
// the `cat`
|
|
127
|
+
// the `cat` fails → empty string → `JSON.parse("")` throws → `papersRoot` returns null
|
|
126
128
|
// → silence. And silence is exactly what a nudge's success state looks like, so a dead hook
|
|
127
129
|
// is indistinguishable from a working one.
|
|
128
|
-
needs: [provide("
|
|
130
|
+
needs: [provide("config", READ_CONFIG)],
|
|
129
131
|
react: (e) => {
|
|
130
|
-
const root = papersRoot(e.ctx.
|
|
132
|
+
const root = papersRoot(e.ctx.config);
|
|
131
133
|
if (root === null) return nothing();
|
|
132
134
|
return e.path.under([root]) && isPaperSource(e.path.raw)
|
|
133
135
|
? notice(CHECKLIST)
|
|
@@ -63,19 +63,29 @@ import {
|
|
|
63
63
|
nothing,
|
|
64
64
|
} from "vigiles/hook";
|
|
65
65
|
|
|
66
|
-
/** The
|
|
66
|
+
/** The tool's name — the package this hook resolves its scripts through. */
|
|
67
67
|
export const CONFIG_KEY = "paperlint";
|
|
68
|
-
/** The
|
|
69
|
-
export const
|
|
68
|
+
/** The settings file at the project root. Optional: absent means every default. */
|
|
69
|
+
export const CONFIG_FILE = "paperlint.json";
|
|
70
70
|
/** The default. A consumer that declares nothing is assumed to keep papers in `papers/`. */
|
|
71
71
|
export const DEFAULT_PAPERS_ROOT = "papers";
|
|
72
72
|
/**
|
|
73
|
-
* The field
|
|
73
|
+
* The field of the root `paperlint.json` that names the papers directory. These are copies of the
|
|
74
74
|
* constants in `lib/paper-config.mjs` (a hook may import nothing but `vigiles/hook`);
|
|
75
75
|
* `lib/paper-config.harness.mjs` checks that the copies match.
|
|
76
76
|
*/
|
|
77
77
|
export const PAPERS_DIR_FIELD = "papersDir";
|
|
78
|
-
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The provider: the root `paperlint.json`, or `{}` when the project has none — so an ABSENT file
|
|
81
|
+
* (every default) and an UNREADABLE one (the empty string the runtime gives a failed provider)
|
|
82
|
+
* stay two different inputs.
|
|
83
|
+
*
|
|
84
|
+
* 🔴 THE PATH IS ANCHORED TO THE PROJECT ROOT. vigiles runs a provider "via execSync in the hook's
|
|
85
|
+
* cwd", which is the consumer's wiring — a string this hook cannot see. A bare relative path reads
|
|
86
|
+
* from whatever directory the Bash tool last moved to.
|
|
87
|
+
*/
|
|
88
|
+
const READ_CONFIG = `f="\${CLAUDE_PROJECT_DIR:-.}/${CONFIG_FILE}"; if [ -e "$f" ]; then cat "$f"; else echo '{}'; fi`;
|
|
79
89
|
|
|
80
90
|
/**
|
|
81
91
|
* The declared papers root, or `null` when it is unusable.
|
|
@@ -88,14 +98,10 @@ export const OLD_PAPERS_DIR_FIELD = "papers";
|
|
|
88
98
|
* 🔴 `declared === undefined`, NOT `declared ?? DEFAULT` — `"papersDir": null` is a keystroke, not
|
|
89
99
|
* an absence.
|
|
90
100
|
*/
|
|
91
|
-
const papersRoot = (
|
|
101
|
+
const papersRoot = (rawConfig) => {
|
|
92
102
|
let declared;
|
|
93
103
|
try {
|
|
94
|
-
|
|
95
|
-
const settings = pkg?.[CONFIG_KEY] ?? pkg?.[LEGACY_CONFIG_KEY];
|
|
96
|
-
// Old field name: stay silent rather than fall back to the default directory.
|
|
97
|
-
if (settings && Object.hasOwn(settings, OLD_PAPERS_DIR_FIELD)) return null;
|
|
98
|
-
declared = settings?.[PAPERS_DIR_FIELD];
|
|
104
|
+
declared = JSON.parse(rawConfig)?.[PAPERS_DIR_FIELD];
|
|
99
105
|
} catch {
|
|
100
106
|
return null;
|
|
101
107
|
}
|
|
@@ -133,21 +139,17 @@ const surface = (dir) =>
|
|
|
133
139
|
export default experimental_defineReact({
|
|
134
140
|
on: "PostToolUse",
|
|
135
141
|
match: tools("Edit", "Write", "MultiEdit"),
|
|
136
|
-
//
|
|
137
|
-
// provider "via execSync in the hook's cwd", and the hook process's cwd is the consumer's
|
|
138
|
-
// own wiring — a string this hook cannot see. A bare `cat package.json` therefore reads from
|
|
139
|
-
// whatever directory the Bash tool last moved to, and a `cd` into a subdirectory with no
|
|
140
|
-
// manifest breaks the read.
|
|
142
|
+
// The provider is anchored to the project root (`READ_CONFIG`, above).
|
|
141
143
|
//
|
|
142
144
|
// ⚠️ THE FAILURE HERE IS SILENT, which makes it more dangerous than its neighbor's.
|
|
143
145
|
// `paper-edit-guard` on PreToolUse DENIES loudly and visibly when the declaration is
|
|
144
146
|
// unreadable. This hook on PostToolUse just returns `nothing()`, i.e. simply stops firing:
|
|
145
|
-
// the `cat`
|
|
147
|
+
// the `cat` fails → empty string → `JSON.parse("")` throws → `papersRoot` returns null
|
|
146
148
|
// → silence. And silence is exactly what a nudge's success state looks like, so a dead hook
|
|
147
149
|
// is indistinguishable from a working one.
|
|
148
|
-
needs: [provide("
|
|
150
|
+
needs: [provide("config", READ_CONFIG)],
|
|
149
151
|
react: (e) => {
|
|
150
|
-
const root = papersRoot(e.ctx.
|
|
152
|
+
const root = papersRoot(e.ctx.config);
|
|
151
153
|
if (root === null) return nothing();
|
|
152
154
|
const m = paperSourceRe(root).exec(e.path.raw.replace(/^\.\//, ""));
|
|
153
155
|
if (m === null) return nothing();
|
|
@@ -27,33 +27,28 @@ case "$pdir_name" in
|
|
|
27
27
|
*[!A-Za-z0-9._-]* | "" | . | ..) exit 0 ;;
|
|
28
28
|
esac
|
|
29
29
|
|
|
30
|
-
# ── WHERE THE CONSUMER'S THINGS ARE —
|
|
31
|
-
#
|
|
32
|
-
#
|
|
30
|
+
# ── WHERE THE CONSUMER'S THINGS ARE — READ, NOT ASSUMED ─────────────────────
|
|
31
|
+
# Both paths come from the one declaration every other carrier of this package reads, the
|
|
32
|
+
# project's root paperlint.json (optional — absent means every default):
|
|
33
33
|
#
|
|
34
|
-
#
|
|
34
|
+
# { "papersDir": "docs/papers", "scripts": "tools/pipeline" }
|
|
35
35
|
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
# ⚠️ `node -p` RATHER THAN grep/sed ON package.json. A JSON value is not a line of text: it can
|
|
36
|
+
# ⚠️ `node -p` RATHER THAN grep/sed ON THE FILE. A JSON value is not a line of text: it can
|
|
39
37
|
# be quoted, escaped, or spread across lines, and a pattern that gets it right today gets it
|
|
40
38
|
# wrong on the first reformat — silently, by producing a root that matches nothing. Node is
|
|
41
39
|
# already a hard dependency of the checker this script runs, so there is no new requirement.
|
|
42
40
|
ROOT_DIR="${CLAUDE_PROJECT_DIR:-.}"
|
|
43
41
|
read_key() { # $1 = key, $2 = default
|
|
44
|
-
node -p "((
|
|
42
|
+
node -p "((c) => c['$1'] ?? '$2')((() => { try { return require('$ROOT_DIR/paperlint.json') } catch { return {} } })())" \
|
|
45
43
|
2>/dev/null || echo "$2"
|
|
46
44
|
}
|
|
47
|
-
# The field used to be called "papers". Every other reader refuses the old name; this script only
|
|
48
|
-
# gives advice, so it stays silent instead of falling back to the default directory.
|
|
49
|
-
[ -z "$(read_key papers '')" ] || exit 0
|
|
50
45
|
papers_root="$(read_key papersDir papers)"
|
|
51
46
|
scripts_root="$(read_key scripts .claude/skills/paper-pipeline/scripts)"
|
|
52
47
|
|
|
53
48
|
# 🔴 AN UNUSABLE ROOT EXITS, IT DOES NOT BUILD A PATH FROM IT. `node -p` prints `undefined` for a
|
|
54
49
|
# key whose value is literally `null` (`?? ` only catches null/undefined AFTER the object lookup,
|
|
55
|
-
# and a `null` value reaches the default — but a malformed
|
|
56
|
-
#
|
|
50
|
+
# and a `null` value reaches the default — but a malformed paperlint.json reads as {} and
|
|
51
|
+
# hands back the default). Either way, an empty or nonsense value would
|
|
57
52
|
# produce `/<paper>/PIPELINE-STATUS.md` rooted at the filesystem, which does not exist, and this
|
|
58
53
|
# script would exit 0 looking like a paper with nothing to report. Named rather than risked.
|
|
59
54
|
case "$papers_root" in
|
package/lib/paper-config.d.mts
CHANGED
|
@@ -5,30 +5,27 @@
|
|
|
5
5
|
// The field names are typed as `string`, not as their values, so that the value is written
|
|
6
6
|
// once, in paper-config.mjs.
|
|
7
7
|
|
|
8
|
+
/** The tool's name — its settings file is named after it. */
|
|
8
9
|
export declare const CONFIG_KEY: string;
|
|
9
|
-
|
|
10
|
-
export declare const
|
|
10
|
+
/** `paperlint.json`: the settings file, at the project root and in each paper. */
|
|
11
|
+
export declare const CONFIG_FILE: string;
|
|
12
|
+
/** The files that make a directory a paper. */
|
|
13
|
+
export declare const PAPER_MARKERS: readonly string[];
|
|
11
14
|
|
|
12
|
-
/**
|
|
13
|
-
export declare function
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
};
|
|
15
|
+
/** The project root, walking up from `startDir` (see the module). */
|
|
16
|
+
export declare function findProjectRoot(
|
|
17
|
+
startDir: string,
|
|
18
|
+
isFile?: (path: string) => boolean,
|
|
19
|
+
): string;
|
|
18
20
|
|
|
19
|
-
/** The
|
|
21
|
+
/** The root `paperlint.json` of `root` — undefined when absent, unparsable or not an object. */
|
|
20
22
|
export declare function settingsOf(
|
|
21
|
-
|
|
23
|
+
root: string,
|
|
22
24
|
): Readonly<Record<string, unknown>> | undefined;
|
|
23
25
|
export declare const DEFAULT_PAPERS_ROOT: string;
|
|
24
26
|
export declare const PAPERS_DIR_FIELD: string;
|
|
25
|
-
export declare const OLD_PAPERS_DIR_FIELD: string;
|
|
26
27
|
|
|
27
|
-
/**
|
|
28
|
-
export declare function renamedFieldMessage(
|
|
29
|
-
settings: unknown,
|
|
30
|
-
where?: string,
|
|
31
|
-
): string | null;
|
|
32
|
-
|
|
33
|
-
/** Every key the settings object may hold, mapped to who reads it. */
|
|
28
|
+
/** Every key a `paperlint.json` may hold, at either level, mapped to who reads it. */
|
|
34
29
|
export declare const SETTINGS_KEYS: Readonly<Record<string, string>>;
|
|
30
|
+
/** The keys refused in a paper's `paperlint.json`. */
|
|
31
|
+
export declare const ROOT_ONLY_KEYS: readonly string[];
|
|
@@ -62,8 +62,7 @@ const loaded = await Promise.all(
|
|
|
62
62
|
key: mod.CONFIG_KEY,
|
|
63
63
|
def: mod.DEFAULT_PAPERS_ROOT,
|
|
64
64
|
field: mod.PAPERS_DIR_FIELD,
|
|
65
|
-
|
|
66
|
-
legacyKey: mod.LEGACY_CONFIG_KEY,
|
|
65
|
+
file: mod.CONFIG_FILE,
|
|
67
66
|
};
|
|
68
67
|
}),
|
|
69
68
|
);
|
|
@@ -98,30 +97,34 @@ const loaded = await Promise.all(
|
|
|
98
97
|
`all carriers agree on DEFAULT_PAPERS_ROOT (${[...defs].join(" / ")})`,
|
|
99
98
|
defs.size === 1,
|
|
100
99
|
);
|
|
101
|
-
// The papers-directory field name
|
|
100
|
+
// The papers-directory field name. Only the source and the three hooks
|
|
102
101
|
// (which cannot import it) declare these; the value itself is pinned nowhere but the source,
|
|
103
102
|
// so a rename stays a one-line change there plus the hook copies this checks.
|
|
104
103
|
const fields = new Set(
|
|
105
104
|
loaded.map((c) => c.field).filter((v) => v !== undefined),
|
|
106
105
|
);
|
|
107
|
-
const olds = new Set(loaded.map((c) => c.old).filter((v) => v !== undefined));
|
|
108
106
|
check(
|
|
109
107
|
`all carriers agree on PAPERS_DIR_FIELD (${[...fields].join(" / ")})`,
|
|
110
108
|
fields.size === 1,
|
|
111
109
|
);
|
|
110
|
+
const files = new Set(
|
|
111
|
+
loaded.map((c) => c.file).filter((v) => v !== undefined),
|
|
112
|
+
);
|
|
112
113
|
check(
|
|
113
|
-
`all carriers agree on
|
|
114
|
-
|
|
114
|
+
`all carriers agree on CONFIG_FILE (${[...files].join(" / ")})`,
|
|
115
|
+
files.size === 1,
|
|
115
116
|
);
|
|
116
117
|
check(
|
|
117
|
-
"every hook declares
|
|
118
|
+
"every hook declares the file name — its provider reads that file",
|
|
118
119
|
loaded
|
|
119
120
|
.filter((c) => c.rel.endsWith(".hook.mjs"))
|
|
120
|
-
.every((c) => typeof c.
|
|
121
|
+
.every((c) => typeof c.file === "string"),
|
|
121
122
|
);
|
|
122
123
|
check(
|
|
123
|
-
"
|
|
124
|
-
loaded
|
|
124
|
+
"every hook declares the field name — a hook without it would read an undefined field",
|
|
125
|
+
loaded
|
|
126
|
+
.filter((c) => c.rel.endsWith(".hook.mjs"))
|
|
127
|
+
.every((c) => typeof c.field === "string"),
|
|
125
128
|
);
|
|
126
129
|
check(
|
|
127
130
|
"and no carrier is silent about both at once — such an entry is dead weight on the list",
|
|
@@ -135,23 +138,6 @@ const loaded = await Promise.all(
|
|
|
135
138
|
loaded[0].key ===
|
|
136
139
|
JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")).name,
|
|
137
140
|
);
|
|
138
|
-
// The old key is still read — by the source and by each hook, which keep their own copy.
|
|
139
|
-
const legacyKeys = new Set(
|
|
140
|
-
loaded.map((c) => c.legacyKey).filter((v) => v !== undefined),
|
|
141
|
-
);
|
|
142
|
-
const { LEGACY_PACKAGE_NAME } = await import(
|
|
143
|
-
pathToFileURL(join(ROOT, "skills/paper-pipeline/scripts/consumer.mjs")).href
|
|
144
|
-
);
|
|
145
|
-
check(
|
|
146
|
-
`all carriers agree on LEGACY_CONFIG_KEY, the package's old name (${[...legacyKeys].join(" / ")})`,
|
|
147
|
-
legacyKeys.size === 1 && legacyKeys.has(LEGACY_PACKAGE_NAME),
|
|
148
|
-
);
|
|
149
|
-
check(
|
|
150
|
-
"and every hook carries it — a hook without it would ignore settings under the old key",
|
|
151
|
-
loaded
|
|
152
|
-
.filter((c) => c.rel.endsWith(".hook.mjs"))
|
|
153
|
-
.every((c) => c.legacyKey !== undefined),
|
|
154
|
-
);
|
|
155
141
|
}
|
|
156
142
|
|
|
157
143
|
// ── III. THE HOOKS DUPLICATE, AND THAT IS CHECKED SEPARATELY ───────────────────────────────
|
|
@@ -186,7 +172,7 @@ const loaded = await Promise.all(
|
|
|
186
172
|
"repro",
|
|
187
173
|
]);
|
|
188
174
|
const declares =
|
|
189
|
-
/^(?:export )?const (?:CONFIG_KEY|DEFAULT_PAPERS_ROOT|PAPERS_DIR_FIELD
|
|
175
|
+
/^(?:export )?const (?:CONFIG_KEY|CONFIG_FILE|DEFAULT_PAPERS_ROOT|PAPERS_DIR_FIELD)\s*=/m;
|
|
190
176
|
const found = [];
|
|
191
177
|
const walk = (dir) => {
|
|
192
178
|
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
@@ -218,19 +204,16 @@ const loaded = await Promise.all(
|
|
|
218
204
|
// The settings object has readers all over the package, each reading its own keys, and the CLI
|
|
219
205
|
// refuses any key not in SETTINGS_KEYS. So the list must hold every key some reader reads — or a
|
|
220
206
|
// consumer's valid key is refused — and nothing no one reads. The readers are FOUND, by the AST:
|
|
221
|
-
// every `
|
|
207
|
+
// every `settingsOf(root)?.<key>` (and the older `[CONFIG_KEY]?.<key>` shape), plus every field of
|
|
222
208
|
// `PaperlintConfig`, the CLI's typed view of the same object.
|
|
223
209
|
{
|
|
224
210
|
const { SETTINGS_KEYS } = await import(join(HERE, "paper-config.mjs"));
|
|
225
211
|
const ts = (await import("typescript")).default;
|
|
226
212
|
const readKeys = new Set();
|
|
227
213
|
const isConfigKey = (e) =>
|
|
228
|
-
(ts.isIdentifier(e) &&
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
(e.text === "paperlint" || e.text === "research-paper-pipeline"));
|
|
232
|
-
// A read is `pkg[CONFIG_KEY]?.<key>` or `settingsOf(pkg)?.<key>` — the helper that also reads
|
|
233
|
-
// the old key.
|
|
214
|
+
(ts.isIdentifier(e) && e.text === "CONFIG_KEY") ||
|
|
215
|
+
(ts.isStringLiteral(e) && e.text === "paperlint");
|
|
216
|
+
// A read is `settingsOf(root)?.<key>` (or `x[CONFIG_KEY]?.<key>`).
|
|
234
217
|
const isSettings = (e) =>
|
|
235
218
|
(ts.isElementAccessExpression(e) && isConfigKey(e.argumentExpression)) ||
|
|
236
219
|
(ts.isCallExpression(e) &&
|