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
package/src/hooks-settings.ts
CHANGED
|
@@ -28,7 +28,6 @@ import { basename, dirname, join, posix } from "node:path";
|
|
|
28
28
|
import { fileURLToPath } from "node:url";
|
|
29
29
|
import {
|
|
30
30
|
BIN_FILE,
|
|
31
|
-
LEGACY_PACKAGE_NAME,
|
|
32
31
|
PACKAGE_NAME,
|
|
33
32
|
} from "../skills/paper-pipeline/scripts/consumer.mjs";
|
|
34
33
|
|
|
@@ -47,9 +46,6 @@ export const WIRING_FILE = fileURLToPath(
|
|
|
47
46
|
*/
|
|
48
47
|
export const MANAGED_BY = `node_modules/${PACKAGE_NAME}/${BIN_FILE}`;
|
|
49
48
|
|
|
50
|
-
/** The names the package's directory under `node_modules` has had. */
|
|
51
|
-
const PKG_DIRS: readonly string[] = [PACKAGE_NAME, LEGACY_PACKAGE_NAME];
|
|
52
|
-
|
|
53
49
|
export interface HookCommand {
|
|
54
50
|
readonly type: string;
|
|
55
51
|
readonly command: string;
|
|
@@ -80,7 +76,6 @@ function bare(token: string): string {
|
|
|
80
76
|
interface Run {
|
|
81
77
|
readonly name: string;
|
|
82
78
|
readonly ours: boolean;
|
|
83
|
-
readonly legacy: boolean;
|
|
84
79
|
readonly path: string;
|
|
85
80
|
}
|
|
86
81
|
|
|
@@ -90,19 +85,11 @@ interface Run {
|
|
|
90
85
|
* Recognised spellings — each is ONE lexeme of a shell command, which is what a command is:
|
|
91
86
|
* `node <…>/node_modules/paperlint/bin/paperlint.mjs hook <name>`
|
|
92
87
|
* ours when the path is exactly MANAGED_BY
|
|
93
|
-
* `
|
|
94
|
-
*
|
|
95
|
-
* this version does not install — what an older `init` left behind
|
|
96
|
-
* (1.x: `research-paper-pipeline/bin/rpp.mjs`; 2.0.0: `paperlint/bin/rpp.mjs`)
|
|
97
|
-
* `npx paperlint hook <name>`, `node_modules/.bin/paperlint hook <name>` (or under the old package name),
|
|
98
|
-
* an absolute path into the package's `bin/`
|
|
88
|
+
* `npx paperlint hook <name>`, `node_modules/.bin/paperlint hook <name>`, an absolute path into
|
|
89
|
+
* the package's `bin/`
|
|
99
90
|
* another spelling
|
|
100
|
-
* `vigiles … run-program <…>/node_modules
|
|
91
|
+
* `vigiles … run-program <…>/node_modules/paperlint/hooks/<name>.hook.mjs`
|
|
101
92
|
* another spelling
|
|
102
|
-
* The last is how the one real consumer wired all three by hand before `init` could.
|
|
103
|
-
*
|
|
104
|
-
* `legacy` is decided by the SHAPE `init` writes, not by a list of past file names: any file in
|
|
105
|
-
* our `bin/` other than BIN_FILE is one that no longer ships, so a future rename is covered too.
|
|
106
93
|
*/
|
|
107
94
|
function parseRun(command: string): Run | null {
|
|
108
95
|
const tokens = command.trim().split(/\s+/).map(bare);
|
|
@@ -113,54 +100,37 @@ function parseRun(command: string): Run | null {
|
|
|
113
100
|
return null;
|
|
114
101
|
}
|
|
115
102
|
|
|
116
|
-
/** The path segments after `node_modules
|
|
117
|
-
function insidePackage(parts: readonly string[]): {
|
|
118
|
-
readonly inside: readonly string[];
|
|
119
|
-
readonly relative: boolean;
|
|
120
|
-
} {
|
|
103
|
+
/** The path segments after `node_modules/paperlint/`. */
|
|
104
|
+
function insidePackage(parts: readonly string[]): readonly string[] {
|
|
121
105
|
const at = parts.findIndex(
|
|
122
|
-
(p, j) => p === "node_modules" &&
|
|
106
|
+
(p, j) => p === "node_modules" && parts[j + 1] === PACKAGE_NAME,
|
|
123
107
|
);
|
|
124
|
-
return
|
|
125
|
-
inside: at === -1 ? [] : parts.slice(at + 2),
|
|
126
|
-
relative: at === 0,
|
|
127
|
-
};
|
|
108
|
+
return at === -1 ? [] : parts.slice(at + 2);
|
|
128
109
|
}
|
|
129
110
|
|
|
130
111
|
/** The run token `i` starts, if it names a paperlint hook. */
|
|
131
112
|
function runAt(tokens: readonly string[], i: number): Run | null {
|
|
132
113
|
const t = posix.normalize(tokens[i] ?? "");
|
|
133
|
-
const
|
|
114
|
+
const inside = insidePackage(t.split("/"));
|
|
134
115
|
const inBin = inside[0] === "bin" && inside.length === 2;
|
|
135
116
|
const name = tokens[i + 2];
|
|
136
117
|
if (
|
|
137
|
-
(inBin ||
|
|
118
|
+
(inBin || basename(t) === PACKAGE_NAME) &&
|
|
138
119
|
tokens[i + 1] === "hook" &&
|
|
139
120
|
name
|
|
140
121
|
)
|
|
141
|
-
return {
|
|
142
|
-
name,
|
|
143
|
-
ours: t === MANAGED_BY,
|
|
144
|
-
legacy: inBin && relative && t !== MANAGED_BY,
|
|
145
|
-
path: t,
|
|
146
|
-
};
|
|
122
|
+
return { name, ours: t === MANAGED_BY, path: t };
|
|
147
123
|
if (inside[0] === "hooks" && inside[1]?.endsWith(".hook.mjs"))
|
|
148
|
-
return {
|
|
149
|
-
name: basename(inside[1], ".hook.mjs"),
|
|
150
|
-
ours: false,
|
|
151
|
-
legacy: false,
|
|
152
|
-
path: t,
|
|
153
|
-
};
|
|
124
|
+
return { name: basename(inside[1], ".hook.mjs"), ours: false, path: t };
|
|
154
125
|
return null;
|
|
155
126
|
}
|
|
156
127
|
|
|
157
128
|
export function hookRun(command: string): {
|
|
158
129
|
readonly name: string;
|
|
159
130
|
readonly ours: boolean;
|
|
160
|
-
readonly legacy: boolean;
|
|
161
131
|
} | null {
|
|
162
132
|
const run = parseRun(command);
|
|
163
|
-
return run && { name: run.name, ours: run.ours
|
|
133
|
+
return run && { name: run.name, ours: run.ours };
|
|
164
134
|
}
|
|
165
135
|
|
|
166
136
|
/** Every command in a settings object, with the event it hangs off. */
|
|
@@ -205,69 +175,18 @@ export function shippedWiring(file: string = WIRING_FILE): Wiring {
|
|
|
205
175
|
export function wiredCounts(
|
|
206
176
|
settings: Settings,
|
|
207
177
|
names: readonly string[],
|
|
208
|
-
): Map<string, { ours: number;
|
|
209
|
-
const counts = new Map(
|
|
210
|
-
names.map((n) => [n, { ours: 0, legacy: 0, other: 0 }]),
|
|
211
|
-
);
|
|
178
|
+
): Map<string, { ours: number; other: number }> {
|
|
179
|
+
const counts = new Map(names.map((n) => [n, { ours: 0, other: 0 }]));
|
|
212
180
|
for (const { command } of commandsIn(settings)) {
|
|
213
181
|
const run = hookRun(command);
|
|
214
182
|
const slot = run ? counts.get(run.name) : undefined;
|
|
215
183
|
if (run && slot)
|
|
216
184
|
if (run.ours) slot.ours++;
|
|
217
|
-
else if (run.legacy) slot.legacy++;
|
|
218
185
|
else slot.other++;
|
|
219
186
|
}
|
|
220
187
|
return counts;
|
|
221
188
|
}
|
|
222
189
|
|
|
223
|
-
/**
|
|
224
|
-
* The settings with every command an install under the old package name wrote removed, and the
|
|
225
|
-
* matcher blocks and events left empty by that removal dropped. The user's own commands stay.
|
|
226
|
-
*/
|
|
227
|
-
export function withoutLegacy(settings: Settings): {
|
|
228
|
-
readonly settings: Settings;
|
|
229
|
-
readonly removed: number;
|
|
230
|
-
} {
|
|
231
|
-
const hooks = settings["hooks"];
|
|
232
|
-
if (!hooks || typeof hooks !== "object") return { settings, removed: 0 };
|
|
233
|
-
let removed = 0;
|
|
234
|
-
const next: Record<string, HookEntry[]> = {};
|
|
235
|
-
for (const [event, entries] of Object.entries(hooks as HooksMap)) {
|
|
236
|
-
const kept: HookEntry[] = [];
|
|
237
|
-
for (const entry of Array.isArray(entries) ? entries : []) {
|
|
238
|
-
const list: readonly HookCommand[] = Array.isArray(entry?.hooks)
|
|
239
|
-
? entry.hooks
|
|
240
|
-
: [];
|
|
241
|
-
const own = list.filter((h) => {
|
|
242
|
-
const legacy =
|
|
243
|
-
typeof h?.command === "string" && hookRun(h.command)?.legacy === true;
|
|
244
|
-
if (legacy) removed++;
|
|
245
|
-
return !legacy;
|
|
246
|
-
});
|
|
247
|
-
if (own.length > 0 || list.length === 0)
|
|
248
|
-
kept.push({ ...entry, hooks: own });
|
|
249
|
-
}
|
|
250
|
-
if (kept.length > 0) next[event] = kept;
|
|
251
|
-
}
|
|
252
|
-
return removed === 0
|
|
253
|
-
? { settings, removed }
|
|
254
|
-
: { settings: { ...settings, hooks: next }, removed };
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Plugin ids the PROJECT enables for this package. A user-scope plugin lives in the user's own
|
|
259
|
-
* settings, which this cannot see — callers must say so rather than print "not enabled".
|
|
260
|
-
*/
|
|
261
|
-
export function pluginEnabledHere(settings: Settings): string[] {
|
|
262
|
-
const enabled = settings["enabledPlugins"];
|
|
263
|
-
if (!enabled || typeof enabled !== "object") return [];
|
|
264
|
-
return Object.entries(enabled as Record<string, unknown>)
|
|
265
|
-
.filter(
|
|
266
|
-
([id, on]) => on === true && id.split("@")[0] === LEGACY_PACKAGE_NAME,
|
|
267
|
-
)
|
|
268
|
-
.map(([id]) => id);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
190
|
export type SettingsRead =
|
|
272
191
|
| { readonly status: "absent"; readonly path: string; readonly settings: {} }
|
|
273
192
|
| {
|
|
@@ -301,9 +220,6 @@ export type WireResult =
|
|
|
301
220
|
readonly status: "written" | "present";
|
|
302
221
|
readonly path: string;
|
|
303
222
|
readonly names: readonly string[];
|
|
304
|
-
readonly plugin: readonly string[];
|
|
305
|
-
/** Commands an install under the old package name wrote, removed before the merge. */
|
|
306
|
-
readonly replaced: number;
|
|
307
223
|
}
|
|
308
224
|
| {
|
|
309
225
|
readonly status: "foreign";
|
|
@@ -316,7 +232,6 @@ export type WireResult =
|
|
|
316
232
|
}[];
|
|
317
233
|
/** Shipped hooks that no command runs in ANY spelling — left unwired as well. */
|
|
318
234
|
readonly missing: readonly string[];
|
|
319
|
-
readonly plugin: readonly string[];
|
|
320
235
|
}
|
|
321
236
|
| {
|
|
322
237
|
readonly status: "unparsable";
|
|
@@ -342,11 +257,7 @@ export function wireHooks(
|
|
|
342
257
|
): WireResult {
|
|
343
258
|
const read = readSettings(root);
|
|
344
259
|
if (read.status === "unparsable") return read;
|
|
345
|
-
const { path } = read;
|
|
346
|
-
// Commands from an install under the old package name point at a directory that is gone after
|
|
347
|
-
// the upgrade. They are ours, so they are replaced rather than counted as someone else's.
|
|
348
|
-
const { settings, removed } = withoutLegacy(read.settings);
|
|
349
|
-
const plugin = pluginEnabledHere(settings);
|
|
260
|
+
const { path, settings } = read;
|
|
350
261
|
const found = commandsIn(settings)
|
|
351
262
|
.map(({ command }) => ({ command, run: hookRun(command) }))
|
|
352
263
|
.filter(({ run }) => run && !run.ours && wiring.names.includes(run.name))
|
|
@@ -356,63 +267,20 @@ export function wireHooks(
|
|
|
356
267
|
const missing = wiring.names.filter(
|
|
357
268
|
(n) => (counts.get(n)?.ours ?? 0) + (counts.get(n)?.other ?? 0) === 0,
|
|
358
269
|
);
|
|
359
|
-
return {
|
|
360
|
-
status: "foreign",
|
|
361
|
-
path,
|
|
362
|
-
names: wiring.names,
|
|
363
|
-
found,
|
|
364
|
-
missing,
|
|
365
|
-
plugin,
|
|
366
|
-
};
|
|
270
|
+
return { status: "foreign", path, names: wiring.names, found, missing };
|
|
367
271
|
}
|
|
368
272
|
|
|
369
273
|
const next = merge(settings, wiring.compiled, MANAGED_BY);
|
|
370
274
|
if (JSON.stringify(next) === JSON.stringify(read.settings))
|
|
371
|
-
return {
|
|
372
|
-
status: "present",
|
|
373
|
-
path,
|
|
374
|
-
names: wiring.names,
|
|
375
|
-
plugin,
|
|
376
|
-
replaced: 0,
|
|
377
|
-
};
|
|
275
|
+
return { status: "present", path, names: wiring.names };
|
|
378
276
|
mkdirSync(dirname(path), { recursive: true });
|
|
379
277
|
writeFileSync(path, JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
380
|
-
return {
|
|
381
|
-
status: "written",
|
|
382
|
-
path,
|
|
383
|
-
names: wiring.names,
|
|
384
|
-
plugin,
|
|
385
|
-
replaced: removed,
|
|
386
|
-
};
|
|
278
|
+
return { status: "written", path, names: wiring.names };
|
|
387
279
|
}
|
|
388
280
|
|
|
389
281
|
/** The consequence every report of a written hook must carry, in one place. */
|
|
390
282
|
export const FRESH_CLONE_NOTE = `the commands point into node_modules/${PACKAGE_NAME}/ — in a fresh clone they cannot run until \`npm install\` has run there`;
|
|
391
283
|
|
|
392
|
-
/** The instruction for a project that also enables the plugin (it only ever existed under the old name). */
|
|
393
|
-
export const UNINSTALL_PLUGIN = `/plugin uninstall ${LEGACY_PACKAGE_NAME}@${LEGACY_PACKAGE_NAME}`;
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* The doctor lines for hook commands an older install left behind: each names the file it runs,
|
|
397
|
-
* which this version does not install, so the hook silently does nothing.
|
|
398
|
-
*/
|
|
399
|
-
function legacyLines(settings: Settings): string[] {
|
|
400
|
-
const stale = new Map<string, number>();
|
|
401
|
-
for (const { command } of commandsIn(settings)) {
|
|
402
|
-
const run = parseRun(command);
|
|
403
|
-
if (run?.legacy) stale.set(run.path, (stale.get(run.path) ?? 0) + 1);
|
|
404
|
-
}
|
|
405
|
-
return stale.size === 0
|
|
406
|
-
? []
|
|
407
|
-
: [
|
|
408
|
-
...[...stale].map(
|
|
409
|
-
([path, n]) =>
|
|
410
|
-
` ⚠ ${String(n)} hook command(s) run ${path}, which this version does not install — they no longer run.`,
|
|
411
|
-
),
|
|
412
|
-
` \`npx ${PACKAGE_NAME} init\` replaces them`,
|
|
413
|
-
];
|
|
414
|
-
}
|
|
415
|
-
|
|
416
284
|
/**
|
|
417
285
|
* `paperlint doctor`'s section. ADVISORY — it never fails the run: the hooks are an in-editor guard,
|
|
418
286
|
* `--no-hooks` is a legitimate choice, and a doctor that exits non-zero on a choice gets muted.
|
|
@@ -461,17 +329,5 @@ export function doctorHooks(
|
|
|
461
329
|
` ✓ wired — ${wiring.names.join(", ")}, once each`,
|
|
462
330
|
` ${FRESH_CLONE_NOTE}`,
|
|
463
331
|
);
|
|
464
|
-
out.push(...legacyLines(read.settings));
|
|
465
|
-
const plugin = pluginEnabledHere(read.settings);
|
|
466
|
-
if (plugin.length > 0) {
|
|
467
|
-
out.push(
|
|
468
|
-
` ⚠ this project ALSO enables the plugin (${plugin.join(", ")}) — every hook would run twice.`,
|
|
469
|
-
` the plugin no longer carries the hooks: ${UNINSTALL_PLUGIN}`,
|
|
470
|
-
` and remove it from "enabledPlugins" in ${SETTINGS_PATH}`,
|
|
471
|
-
);
|
|
472
|
-
}
|
|
473
|
-
out.push(
|
|
474
|
-
` · a plugin installed at USER scope is not visible from here — \`/plugin\` inside Claude Code shows it`,
|
|
475
|
-
);
|
|
476
332
|
return out;
|
|
477
333
|
}
|
package/src/init.test.ts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `paperlint init` over a project that ALREADY declares its papers directory: the declaration is
|
|
3
|
+
* the answer, and init reports it — not a directory it discovered elsewhere and then did not use.
|
|
4
|
+
*
|
|
5
|
+
* Seen migrating a consumer project: init printed "papers directory ✓ eslint-rules/fixtures — 4
|
|
6
|
+
* candidates", then kept the declared papersDir. The line described a decision that was not made.
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
existsSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
mkdtempSync,
|
|
12
|
+
readFileSync,
|
|
13
|
+
realpathSync,
|
|
14
|
+
rmSync,
|
|
15
|
+
writeFileSync,
|
|
16
|
+
} from "node:fs";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
20
|
+
import { choosePapers, init } from "./init.ts";
|
|
21
|
+
|
|
22
|
+
const dirs: string[] = [];
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/** Declares `papersDir: "papers"`, and holds two OTHER directories that look like papers roots. */
|
|
28
|
+
function project(): string {
|
|
29
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-init-")));
|
|
30
|
+
dirs.push(root);
|
|
31
|
+
const files: Record<string, string> = {
|
|
32
|
+
"package.json": JSON.stringify({ name: "c", private: true }),
|
|
33
|
+
"paperlint.json": JSON.stringify({ papersDir: "papers" }),
|
|
34
|
+
"papers/a/PIPELINE-STATUS.md": "---\nstages: []\n---\n",
|
|
35
|
+
"eslint-rules/fixtures/x/PIPELINE-STATUS.md": "---\nstages: []\n---\n",
|
|
36
|
+
"eslint-rules/fixtures/y/paper.md": "# y\n",
|
|
37
|
+
"drafts/z/paper.md": "# z\n",
|
|
38
|
+
};
|
|
39
|
+
for (const [p, text] of Object.entries(files)) {
|
|
40
|
+
mkdirSync(join(root, p, ".."), { recursive: true });
|
|
41
|
+
writeFileSync(join(root, p), text);
|
|
42
|
+
}
|
|
43
|
+
return root;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe("paperlint init — a declared papers directory", () => {
|
|
47
|
+
it("🔴 is the one reported, and no discovered candidate is named", async () => {
|
|
48
|
+
const root = project();
|
|
49
|
+
const out: string[] = [];
|
|
50
|
+
const code = await init(root, {
|
|
51
|
+
cwd: root,
|
|
52
|
+
log: (s: string) => out.push(s),
|
|
53
|
+
err: (s: string) => out.push(s),
|
|
54
|
+
interactive: false,
|
|
55
|
+
hooks: false,
|
|
56
|
+
run: (() => ({ status: 0 })) as never,
|
|
57
|
+
link: () => ({ ok: false, error: "not linked in this test" }),
|
|
58
|
+
});
|
|
59
|
+
const text = out.join("\n");
|
|
60
|
+
const section = text.slice(
|
|
61
|
+
text.indexOf("papers directory"),
|
|
62
|
+
text.indexOf("settings"),
|
|
63
|
+
);
|
|
64
|
+
expect(code).toBe(0);
|
|
65
|
+
expect(section).toMatch(/✓ papers — declared in paperlint\.json/);
|
|
66
|
+
expect(section).not.toMatch(/candidates|eslint-rules|drafts/);
|
|
67
|
+
const cfg = JSON.parse(readFileSync(join(root, "paperlint.json"), "utf8"));
|
|
68
|
+
expect(cfg).toEqual({ papersDir: "papers" });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("is not asked about, even with a human at the terminal", async () => {
|
|
72
|
+
const root = project();
|
|
73
|
+
const asked: string[] = [];
|
|
74
|
+
await init(root, {
|
|
75
|
+
cwd: root,
|
|
76
|
+
log: () => {},
|
|
77
|
+
err: () => {},
|
|
78
|
+
interactive: true,
|
|
79
|
+
ask: async (q) => (asked.push(q), "n"),
|
|
80
|
+
hooks: false,
|
|
81
|
+
run: (() => ({ status: 0 })) as never,
|
|
82
|
+
link: () => ({ ok: false, error: "not linked in this test" }),
|
|
83
|
+
});
|
|
84
|
+
expect(asked.filter((q) => /papers roots/.test(q))).toEqual([]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("without a declaration, the candidates are still measured and offered", async () => {
|
|
88
|
+
const choice = await choosePapers(project(), { interactive: false });
|
|
89
|
+
expect(choice.how).toBe("not-asked");
|
|
90
|
+
expect(choice.candidates.length).toBeGreaterThan(1);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("paperlint init — writes paperlint.json only when something differs from the default", () => {
|
|
95
|
+
const quiet = {
|
|
96
|
+
log: () => {},
|
|
97
|
+
err: () => {},
|
|
98
|
+
interactive: false,
|
|
99
|
+
hooks: false,
|
|
100
|
+
run: (() => ({ status: 0 })) as never,
|
|
101
|
+
link: () => ({ ok: false as const, error: "not linked in this test" }),
|
|
102
|
+
};
|
|
103
|
+
const fresh = (files: Record<string, string>): string => {
|
|
104
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-init-")));
|
|
105
|
+
dirs.push(root);
|
|
106
|
+
for (const [p, text] of Object.entries(files)) {
|
|
107
|
+
mkdirSync(join(root, p, ".."), { recursive: true });
|
|
108
|
+
writeFileSync(join(root, p), text);
|
|
109
|
+
}
|
|
110
|
+
return root;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
it("🔴 papers/ is where the papers are: no paperlint.json is written", async () => {
|
|
114
|
+
const root = fresh({ "papers/a/paper.tex": "x" });
|
|
115
|
+
await init(root, { cwd: root, ...quiet });
|
|
116
|
+
expect(existsSync(join(root, "paperlint.json"))).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("nothing looks like papers: the default, and still no file", async () => {
|
|
120
|
+
const root = fresh({ "README.md": "x" });
|
|
121
|
+
await init(root, { cwd: root, ...quiet });
|
|
122
|
+
expect(existsSync(join(root, "paperlint.json"))).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("papers elsewhere: { papersDir } is written, and other keys already there are kept", async () => {
|
|
126
|
+
const root = fresh({
|
|
127
|
+
"docs/drafts/a/paper.tex": "x",
|
|
128
|
+
"paperlint.json": '{ "kind": "short" }\n',
|
|
129
|
+
});
|
|
130
|
+
await init(root, { cwd: root, ...quiet });
|
|
131
|
+
expect(
|
|
132
|
+
JSON.parse(readFileSync(join(root, "paperlint.json"), "utf8")),
|
|
133
|
+
).toEqual({ kind: "short", papersDir: "docs/drafts" });
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
/** Runs init in a fresh directory with the given TeX Live port; what it printed and asked. */
|
|
138
|
+
const texRun = async (
|
|
139
|
+
tex: { installed: () => boolean; install?: () => number },
|
|
140
|
+
{ interactive, answer = "" }: { interactive: boolean; answer?: string },
|
|
141
|
+
) => {
|
|
142
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-tex-")));
|
|
143
|
+
dirs.push(root);
|
|
144
|
+
const out: string[] = [];
|
|
145
|
+
const asked: string[] = [];
|
|
146
|
+
await init(root, {
|
|
147
|
+
cwd: root,
|
|
148
|
+
log: (s: string) => out.push(s),
|
|
149
|
+
err: () => {},
|
|
150
|
+
interactive,
|
|
151
|
+
ask: async (q) => (asked.push(q), /TeX Live/.test(q) ? answer : ""),
|
|
152
|
+
hooks: false,
|
|
153
|
+
run: (() => ({ status: 0 })) as never,
|
|
154
|
+
link: () => ({ ok: false as const, error: "not linked in this test" }),
|
|
155
|
+
tex,
|
|
156
|
+
});
|
|
157
|
+
return { text: out.join("\n"), asked };
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
describe("paperlint init — TeX Live is offered with its cost, never installed unasked", () => {
|
|
161
|
+
it("🔴 no terminal: nothing is asked or installed, and the command is the next step", async () => {
|
|
162
|
+
let installs = 0;
|
|
163
|
+
const r = await texRun(
|
|
164
|
+
{ installed: () => false, install: () => (installs++, 0) },
|
|
165
|
+
{ interactive: false },
|
|
166
|
+
);
|
|
167
|
+
expect(installs).toBe(0);
|
|
168
|
+
expect(r.asked.filter((q) => /TeX Live/.test(q))).toEqual([]);
|
|
169
|
+
expect(r.text).toMatch(
|
|
170
|
+
/npx paperlint toolchain {3}# ~270 MB, ~3 min, once/,
|
|
171
|
+
);
|
|
172
|
+
expect(r.text).toMatch(/next: .*\n.*npx paperlint toolchain/);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("a human is asked, with the size in the question; yes installs once", async () => {
|
|
176
|
+
let installs = 0;
|
|
177
|
+
const r = await texRun(
|
|
178
|
+
{ installed: () => false, install: () => (installs++, 0) },
|
|
179
|
+
{ interactive: true, answer: "y" },
|
|
180
|
+
);
|
|
181
|
+
expect(r.asked.find((q) => /TeX Live/.test(q))).toMatch(
|
|
182
|
+
/~270 MB, ~3 min, once\) \[y\/N\]/,
|
|
183
|
+
);
|
|
184
|
+
expect(installs).toBe(1);
|
|
185
|
+
expect(r.text).not.toMatch(/next: .*\n.*npx paperlint toolchain/);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("Enter is no — nothing installed", async () => {
|
|
189
|
+
let installs = 0;
|
|
190
|
+
const r = await texRun(
|
|
191
|
+
{ installed: () => false, install: () => (installs++, 0) },
|
|
192
|
+
{ interactive: true },
|
|
193
|
+
);
|
|
194
|
+
expect(installs).toBe(0);
|
|
195
|
+
expect(r.text).toMatch(/declined — nothing installed/);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("already installed: not asked about", async () => {
|
|
199
|
+
const r = await texRun(
|
|
200
|
+
{ installed: () => true, install: () => 0 },
|
|
201
|
+
{ interactive: true, answer: "y" },
|
|
202
|
+
);
|
|
203
|
+
expect(r.asked.filter((q) => /TeX Live/.test(q))).toEqual([]);
|
|
204
|
+
expect(r.text).toMatch(/✓ paperlint's TeX Live is installed/);
|
|
205
|
+
});
|
|
206
|
+
});
|