paperlint 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +4 -4
- package/CLAUDE.md +3 -3
- package/CONTRIBUTING.md +21 -0
- package/LICENSE +1 -1
- package/README.md +81 -24
- package/action.harness.mjs +10 -10
- package/action.mutations.mjs +3 -3
- package/action.yml +11 -11
- package/bin/{rpp.mjs → paperlint.mjs} +2 -2
- package/dist/adapters/banal/invocation.js +3 -3
- package/dist/adapters/banal/invocation.js.map +1 -1
- package/dist/adapters/banal/run.d.ts +1 -1
- package/dist/adapters/banal/run.js +1 -1
- package/dist/adapters/banal/run.js.map +1 -1
- package/dist/adapters/banal/settings.d.ts +4 -4
- package/dist/adapters/banal/settings.d.ts.map +1 -1
- package/dist/adapters/banal/settings.js +4 -4
- package/dist/adapters/banal/settings.js.map +1 -1
- package/dist/adapters/banal/xml.js +1 -1
- package/dist/adapters/banal/xml.js.map +1 -1
- package/dist/adapters/curl/download.io.d.ts.map +1 -1
- package/dist/adapters/curl/download.io.js +2 -2
- package/dist/adapters/curl/download.io.js.map +1 -1
- package/dist/build-engine.d.ts +1 -1
- package/dist/build-engine.d.ts.map +1 -1
- package/dist/build-engine.js +6 -3
- package/dist/build-engine.js.map +1 -1
- package/dist/build.d.ts +1 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +5 -4
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +29 -30
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +174 -98
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +21 -6
- package/dist/doctor.js.map +1 -1
- package/dist/engine.d.ts +2 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +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 -18
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +70 -42
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +13 -10
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +74 -42
- package/dist/init.js.map +1 -1
- package/dist/link-skills.js +2 -2
- 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 +17 -2
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-settings.d.ts +68 -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/presets.d.ts +84 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +186 -0
- package/dist/presets.js.map +1 -0
- package/dist/rules-config.d.ts +8 -1
- package/dist/rules-config.d.ts.map +1 -1
- package/dist/rules-config.js +13 -8
- package/dist/rules-config.js.map +1 -1
- package/dist/structure.d.ts.map +1 -1
- package/dist/structure.js +2 -1
- 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/toolchain.d.ts +2 -2
- package/dist/toolchain.d.ts.map +1 -1
- package/dist/toolchain.js +8 -8
- package/dist/toolchain.js.map +1 -1
- package/dist/types.d.ts +4 -4
- 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 +387 -0
- package/dist/venue-rules.js.map +1 -0
- package/docs/configuration.md +73 -17
- package/docs/e2e.md +3 -4
- package/docs/install.md +11 -3
- package/docs/optional-rules.md +26 -25
- package/docs/rules.md +132 -3
- package/docs/toolchain.md +5 -5
- package/eslint-rules/paper-typography.mjs +1 -1
- package/eslint-rules/paper-typography.mutations.mjs +1 -1
- package/eslint-rules/papers.mjs +6 -2
- package/eslint-rules/pdf-last-page-balance.harness.mjs +5 -3
- package/eslint-rules/pdf-last-page-balance.mjs +23 -2
- package/fixtures/build-e2e/acmart/paperlint.json +1 -0
- package/fixtures/build-e2e/guards/paper.tex +1 -1
- package/fixtures/pdf-facts/README.md +1 -1
- package/fixtures/real-markdown-paper/baseline.json +1 -1
- package/fixtures/real-markdown-paper/baseline.mjs +2 -2
- package/fixtures/toolchain-mirror/install-tl +2 -2
- package/fixtures/toolchain-mirror/stub-pdflatex +1 -1
- package/fixtures/toolchain-mirror/stub-tlmgr +1 -1
- package/hooks/hooks.harness.mjs +1 -1
- package/lib/paper-config.d.mts +7 -0
- package/lib/paper-config.harness.mjs +3 -3
- package/lib/paper-config.mjs +34 -3
- package/lib/skill-trigger-cases.harness.mjs +1 -1
- package/package.json +4 -4
- package/plugin/hooks/hooks.json +3 -3
- package/scripts/check.mjs +1 -1
- package/scripts/harness-api.frozen.json +1 -1
- package/scripts/harness-api.test.ts +2 -2
- package/scripts/layer-legacy.frozen.json +1 -1
- package/scripts/mutation-batteries-frozen.harness.mjs +1 -1
- package/scripts/mutation-batteries.frozen.json +2 -2
- package/scripts/release-config.test.ts +1 -1
- package/skills/find-venue/SKILL.md +5 -1
- package/skills/find-venue/SKILL.md.spec.ts +4 -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/description-language.eval.mjs +1 -1
- package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +1 -1
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +2 -2
- package/skills/paper-pipeline/pipeline-firing.eval.mjs +1 -1
- package/skills/paper-pipeline/pipeline-language.eval.mjs +1 -1
- package/skills/paper-pipeline/scripts/consumer.d.mts +1 -0
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +10 -4
- package/skills/paper-pipeline/scripts/consumer.mjs +8 -1
- package/skills/paper-pipeline/scripts/consumer.mutations.mjs +1 -1
- package/skills/render-paper/SKILL.md +3 -3
- package/skills/render-paper/SKILL.md.spec.ts +2 -2
- package/skills/render-paper/extract-pdf-facts.harness.mjs +6 -6
- package/skills/render-paper/extract-pdf-facts.mjs +4 -4
- 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 +120 -30
- package/src/adapters/banal/index.test.ts +1 -1
- package/src/adapters/banal/invocation.test.ts +3 -3
- package/src/adapters/banal/invocation.ts +3 -3
- package/src/adapters/banal/locate.test.ts +4 -4
- package/src/adapters/banal/run.ts +2 -2
- package/src/adapters/banal/settings.test.ts +12 -6
- package/src/adapters/banal/settings.ts +9 -5
- package/src/adapters/banal/xml.test.ts +1 -1
- package/src/adapters/banal/xml.ts +1 -1
- package/src/adapters/curl/download.io.ts +4 -2
- package/src/adapters/curl/download.test.ts +3 -1
- package/src/adapters/node/files.test.ts +1 -1
- package/src/adapters/node/process.test.ts +2 -2
- package/src/adapters/node/workspace.test.ts +4 -4
- package/src/build-engine.harness.mjs +2 -2
- package/src/build-engine.ts +11 -3
- package/src/build.harness.mjs +27 -22
- package/src/build.mutations.mjs +3 -2
- package/src/build.ts +6 -5
- package/src/cli.harness.mjs +72 -131
- package/src/cli.mutations.mjs +7 -32
- package/src/cli.ts +204 -121
- package/src/doctor.harness.mjs +9 -25
- package/src/doctor.ts +28 -5
- package/src/engine.harness.mjs +4 -2
- package/src/engine.ts +2 -2
- package/src/facts-file.test.ts +54 -7
- package/src/facts-file.ts +145 -24
- package/src/hooks-settings.harness.mjs +24 -15
- package/src/hooks-settings.mutations.mjs +2 -4
- package/src/hooks-settings.ts +92 -49
- package/src/init.ts +88 -52
- package/src/latex-log.harness.mjs +1 -1
- package/src/link-skills.harness.mjs +3 -1
- package/src/link-skills.mutations.mjs +1 -1
- package/src/link-skills.ts +2 -2
- package/src/new-paper.harness.mjs +10 -7
- package/src/new-paper.test.ts +145 -0
- package/src/new-paper.ts +22 -2
- package/src/paper-settings-commands.test.ts +311 -0
- package/src/paper-settings.test.ts +220 -0
- package/src/paper-settings.ts +206 -0
- package/src/pdf-facts.harness.mjs +1 -1
- package/src/presets.test.ts +236 -0
- package/src/presets.ts +307 -0
- package/src/rules-config.ts +14 -9
- package/src/structure.harness.mjs +4 -4
- package/src/structure.ts +2 -1
- package/src/tex-requirements.harness.mjs +12 -18
- package/src/tex-requirements.ts +179 -29
- package/src/toolchain.harness.mjs +34 -25
- package/src/toolchain.ts +14 -8
- package/src/types.ts +4 -4
- package/src/venue-rules.test.ts +545 -0
- package/src/venue-rules.ts +573 -0
- package/templates/paper/paperlint.json +4 -0
- package/fixtures/build-e2e/acmart/venue.json +0 -1
- package/plugin/.claude-plugin/plugin.json +0 -8
package/src/hooks-settings.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
27
27
|
import { basename, dirname, join, posix } from "node:path";
|
|
28
28
|
import { fileURLToPath } from "node:url";
|
|
29
29
|
import {
|
|
30
|
+
BIN_FILE,
|
|
30
31
|
LEGACY_PACKAGE_NAME,
|
|
31
32
|
PACKAGE_NAME,
|
|
32
33
|
} from "../skills/paper-pipeline/scripts/consumer.mjs";
|
|
@@ -44,13 +45,7 @@ export const WIRING_FILE = fileURLToPath(
|
|
|
44
45
|
* and `${CLAUDE_PROJECT_DIR}/` before comparing, so this is the same file as the spelling in
|
|
45
46
|
* `hooks.json`, and nothing machine-specific lands in a committed file.
|
|
46
47
|
*/
|
|
47
|
-
export const MANAGED_BY = `node_modules/${PACKAGE_NAME}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* The same token as an install under the package's old name wrote it. A command spelled this way
|
|
51
|
-
* is ours from before the rename: `init` removes it and writes the current one, `doctor` names it.
|
|
52
|
-
*/
|
|
53
|
-
export const LEGACY_MANAGED_BY = `node_modules/${LEGACY_PACKAGE_NAME}/bin/rpp.mjs`;
|
|
48
|
+
export const MANAGED_BY = `node_modules/${PACKAGE_NAME}/${BIN_FILE}`;
|
|
54
49
|
|
|
55
50
|
/** The names the package's directory under `node_modules` has had. */
|
|
56
51
|
const PKG_DIRS: readonly string[] = [PACKAGE_NAME, LEGACY_PACKAGE_NAME];
|
|
@@ -81,50 +76,91 @@ function bare(token: string): string {
|
|
|
81
76
|
return unquoted.replace(/^\$\{?CLAUDE_PROJECT_DIR\}?[/\\]/, "");
|
|
82
77
|
}
|
|
83
78
|
|
|
79
|
+
/** A hook command reduced to what it runs: the hook, and the path token that runs it. */
|
|
80
|
+
interface Run {
|
|
81
|
+
readonly name: string;
|
|
82
|
+
readonly ours: boolean;
|
|
83
|
+
readonly legacy: boolean;
|
|
84
|
+
readonly path: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
84
87
|
/**
|
|
85
88
|
* Which paperlint hook a command runs, if any, and whether it is spelled the way `init` writes it.
|
|
86
89
|
*
|
|
87
90
|
* Recognised spellings — each is ONE lexeme of a shell command, which is what a command is:
|
|
88
|
-
* `node <…>/node_modules/paperlint/bin/
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
91
|
+
* `node <…>/node_modules/paperlint/bin/paperlint.mjs hook <name>`
|
|
92
|
+
* ours when the path is exactly MANAGED_BY
|
|
93
|
+
* `node node_modules/<pkg>/bin/<any other file> hook <name>`, <pkg> this name or the old one
|
|
94
|
+
* `legacy`: the project-relative form `init` writes, pointing at a file
|
|
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/`
|
|
99
|
+
* another spelling
|
|
93
100
|
* `vigiles … run-program <…>/node_modules/<pkg>/hooks/<name>.hook.mjs`
|
|
94
|
-
*
|
|
101
|
+
* another spelling
|
|
95
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.
|
|
96
106
|
*/
|
|
107
|
+
function parseRun(command: string): Run | null {
|
|
108
|
+
const tokens = command.trim().split(/\s+/).map(bare);
|
|
109
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
110
|
+
const run = runAt(tokens, i);
|
|
111
|
+
if (run) return run;
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The path segments after `node_modules/<pkg>/`, and whether the token starts there. */
|
|
117
|
+
function insidePackage(parts: readonly string[]): {
|
|
118
|
+
readonly inside: readonly string[];
|
|
119
|
+
readonly relative: boolean;
|
|
120
|
+
} {
|
|
121
|
+
const at = parts.findIndex(
|
|
122
|
+
(p, j) => p === "node_modules" && PKG_DIRS.includes(parts[j + 1] ?? ""),
|
|
123
|
+
);
|
|
124
|
+
return {
|
|
125
|
+
inside: at === -1 ? [] : parts.slice(at + 2),
|
|
126
|
+
relative: at === 0,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The run token `i` starts, if it names a paperlint hook. */
|
|
131
|
+
function runAt(tokens: readonly string[], i: number): Run | null {
|
|
132
|
+
const t = posix.normalize(tokens[i] ?? "");
|
|
133
|
+
const { inside, relative } = insidePackage(t.split("/"));
|
|
134
|
+
const inBin = inside[0] === "bin" && inside.length === 2;
|
|
135
|
+
const name = tokens[i + 2];
|
|
136
|
+
if (
|
|
137
|
+
(inBin || PKG_DIRS.includes(basename(t))) &&
|
|
138
|
+
tokens[i + 1] === "hook" &&
|
|
139
|
+
name
|
|
140
|
+
)
|
|
141
|
+
return {
|
|
142
|
+
name,
|
|
143
|
+
ours: t === MANAGED_BY,
|
|
144
|
+
legacy: inBin && relative && t !== MANAGED_BY,
|
|
145
|
+
path: t,
|
|
146
|
+
};
|
|
147
|
+
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
|
+
};
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
97
157
|
export function hookRun(command: string): {
|
|
98
158
|
readonly name: string;
|
|
99
159
|
readonly ours: boolean;
|
|
100
160
|
readonly legacy: boolean;
|
|
101
161
|
} | null {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
const t = posix.normalize(tokens[i] ?? "");
|
|
105
|
-
const parts = t.split("/");
|
|
106
|
-
const at = parts.findIndex(
|
|
107
|
-
(p, j) => p === "node_modules" && PKG_DIRS.includes(parts[j + 1] ?? ""),
|
|
108
|
-
);
|
|
109
|
-
const inside = at === -1 ? [] : parts.slice(at + 2);
|
|
110
|
-
const isBin =
|
|
111
|
-
(inside[0] === "bin" && inside[1] === "rpp.mjs") ||
|
|
112
|
-
["rpp", ...PKG_DIRS].includes(basename(t));
|
|
113
|
-
const name = tokens[i + 2];
|
|
114
|
-
if (isBin && tokens[i + 1] === "hook" && name)
|
|
115
|
-
return {
|
|
116
|
-
name,
|
|
117
|
-
ours: t === MANAGED_BY,
|
|
118
|
-
legacy: t === LEGACY_MANAGED_BY,
|
|
119
|
-
};
|
|
120
|
-
if (inside[0] === "hooks" && inside[1]?.endsWith(".hook.mjs"))
|
|
121
|
-
return {
|
|
122
|
-
name: basename(inside[1], ".hook.mjs"),
|
|
123
|
-
ours: false,
|
|
124
|
-
legacy: false,
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
return null;
|
|
162
|
+
const run = parseRun(command);
|
|
163
|
+
return run && { name: run.name, ours: run.ours, legacy: run.legacy };
|
|
128
164
|
}
|
|
129
165
|
|
|
130
166
|
/** Every command in a settings object, with the event it hangs off. */
|
|
@@ -356,16 +392,23 @@ export const FRESH_CLONE_NOTE = `the commands point into node_modules/${PACKAGE_
|
|
|
356
392
|
/** The instruction for a project that also enables the plugin (it only ever existed under the old name). */
|
|
357
393
|
export const UNINSTALL_PLUGIN = `/plugin uninstall ${LEGACY_PACKAGE_NAME}@${LEGACY_PACKAGE_NAME}`;
|
|
358
394
|
|
|
359
|
-
/**
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
|
366
406
|
? []
|
|
367
407
|
: [
|
|
368
|
-
|
|
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
|
+
),
|
|
369
412
|
` \`npx ${PACKAGE_NAME} init\` replaces them`,
|
|
370
413
|
];
|
|
371
414
|
}
|
|
@@ -400,7 +443,7 @@ export function doctorHooks(
|
|
|
400
443
|
out.push(` ⚠ wired TWICE — each of these runs more than once per event:`);
|
|
401
444
|
for (const n of twice) out.push(` ${n} ×${String(total(n))}`);
|
|
402
445
|
out.push(
|
|
403
|
-
` keep one command per hook; \`npx paperlint init\` writes the
|
|
446
|
+
` keep one command per hook; \`npx paperlint init\` writes the ${MANAGED_BY} form`,
|
|
404
447
|
);
|
|
405
448
|
}
|
|
406
449
|
if (missing.length === wiring.names.length)
|
|
@@ -418,7 +461,7 @@ export function doctorHooks(
|
|
|
418
461
|
` ✓ wired — ${wiring.names.join(", ")}, once each`,
|
|
419
462
|
` ${FRESH_CLONE_NOTE}`,
|
|
420
463
|
);
|
|
421
|
-
out.push(...legacyLines(
|
|
464
|
+
out.push(...legacyLines(read.settings));
|
|
422
465
|
const plugin = pluginEnabledHere(read.settings);
|
|
423
466
|
if (plugin.length > 0) {
|
|
424
467
|
out.push(
|
package/src/init.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* `paperlint init` — the whole install, in the terminal it was typed in.
|
|
3
3
|
*
|
|
4
4
|
* 🔴 WHAT THIS COMMAND USED TO DO, AND WHY THAT WAS A DEFECT RATHER THAN A SHORTFALL. It wrote
|
|
5
|
-
*
|
|
5
|
+
* a separate config file with a GUESSED `"papers": "papers"` and never touched `package.json`. The three hooks
|
|
6
6
|
* read the papers directory out of `package.json` and nothing else, so a consumer who followed the
|
|
7
7
|
* documented install got a `paper-edit-guard` watching a directory that did not exist — and a guard
|
|
8
8
|
* watching nothing is byte-identical, from outside, to a guard that is working (issue #33).
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* carry a paper marker. Several hits is the only case a human is asked about.
|
|
16
16
|
* declaration WRITTEN into `package.json`, merged, never overwriting a value that is
|
|
17
17
|
* already there. Prior art: husky's `init` edits the consumer's package.json
|
|
18
|
-
* to add `prepare`.
|
|
18
|
+
* to add `prepare`. No second config file is created.
|
|
19
19
|
* skills LINKED — one relative symlink per shipped skill into `.claude/skills/`, the
|
|
20
20
|
* only place Claude Code looks for project skills (`link-skills.ts`). An
|
|
21
21
|
* entry of the same name that paperlint did not make is reported, never replaced.
|
|
@@ -35,8 +35,14 @@
|
|
|
35
35
|
* an answer nobody gave. So the non-interactive path takes the stated default and SAYS which
|
|
36
36
|
* default it took and why nothing was asked, rather than pretending it asked.
|
|
37
37
|
*/
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
import {
|
|
39
|
+
existsSync,
|
|
40
|
+
mkdirSync,
|
|
41
|
+
readFileSync,
|
|
42
|
+
unlinkSync,
|
|
43
|
+
writeFileSync,
|
|
44
|
+
// eslint-disable-next-line boundaries/dependencies -- legacy I/O, moves behind a port in #76
|
|
45
|
+
} from "node:fs";
|
|
40
46
|
// eslint-disable-next-line boundaries/dependencies -- legacy I/O, moves behind a port in #76
|
|
41
47
|
import { spawnSync } from "node:child_process";
|
|
42
48
|
import { dirname, join, relative, resolve } from "node:path";
|
|
@@ -66,10 +72,13 @@ import {
|
|
|
66
72
|
CONFIG_KEY,
|
|
67
73
|
DEFAULT_PAPERS_ROOT,
|
|
68
74
|
LEGACY_CONFIG_KEY,
|
|
75
|
+
LEGACY_PAPER_SETTINGS_FILE,
|
|
69
76
|
PAPERS_DIR_FIELD,
|
|
77
|
+
PAPER_SETTINGS_FILE,
|
|
70
78
|
declaredSettings,
|
|
71
79
|
renamedFieldMessage,
|
|
72
80
|
} from "../lib/paper-config.mjs";
|
|
81
|
+
import { migrationOf } from "./paper-settings.ts";
|
|
73
82
|
|
|
74
83
|
/** How the papers directory was arrived at. Printed, because a guess must not read as a fact. */
|
|
75
84
|
export type PapersHow =
|
|
@@ -204,8 +213,8 @@ export type DeclarationResult =
|
|
|
204
213
|
*
|
|
205
214
|
* 🔴 A HOOK CANNOT IMPORT CODE AND CANNOT WALK UP A TREE LOOKING FOR A CONFIG. It can read a path
|
|
206
215
|
* it is able to spell, and the only path it can always spell is the project's own `package.json`.
|
|
207
|
-
* That asymmetry is the whole reason the declaration
|
|
208
|
-
*
|
|
216
|
+
* That asymmetry is the whole reason the declaration lives here rather than in a file of its own:
|
|
217
|
+
* many readers against one (`docs/install.md`, "One declaration").
|
|
209
218
|
*
|
|
210
219
|
* ⚠️ Merged, not rewritten, and never over a value the consumer set — an `init` that silently
|
|
211
220
|
* replaces a setting is worse than an `init` that does nothing, because the consumer keeps
|
|
@@ -259,35 +268,6 @@ function renameKey(
|
|
|
259
268
|
);
|
|
260
269
|
}
|
|
261
270
|
|
|
262
|
-
export type RppJsonResult = "absent" | "kept" | "filled" | "unparsable";
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* `rpp.json` is no longer CREATED — but a consumer who already has one keeps it working, and it
|
|
266
|
-
* gets the same `papers` value rather than being left to disagree with `package.json` in silence.
|
|
267
|
-
* Two declarations that disagree is the defect `paperlint doctor` was written to catch; writing the
|
|
268
|
-
* second one on purpose would be handing it new work.
|
|
269
|
-
*/
|
|
270
|
-
export function syncRppJson(root: string, papers: string): RppJsonResult {
|
|
271
|
-
const path = join(root, "rpp.json");
|
|
272
|
-
if (!existsSync(path)) return "absent";
|
|
273
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- #49: replace with a real type
|
|
274
|
-
let cfg: Record<string, any>;
|
|
275
|
-
const raw = readFileSync(path, "utf8");
|
|
276
|
-
try {
|
|
277
|
-
cfg = JSON.parse(raw);
|
|
278
|
-
} catch {
|
|
279
|
-
return "unparsable";
|
|
280
|
-
}
|
|
281
|
-
if (cfg?.[PAPERS_DIR_FIELD] !== undefined) return "kept";
|
|
282
|
-
cfg[PAPERS_DIR_FIELD] = papers;
|
|
283
|
-
writeFileSync(
|
|
284
|
-
path,
|
|
285
|
-
JSON.stringify(cfg, null, 2) + (raw.endsWith("\n") ? "\n" : ""),
|
|
286
|
-
"utf8",
|
|
287
|
-
);
|
|
288
|
-
return "filled";
|
|
289
|
-
}
|
|
290
|
-
|
|
291
271
|
export const WORKFLOW_PATH = join(".github", "workflows", "papers.yml");
|
|
292
272
|
|
|
293
273
|
/** Where the action is pinned when no release tag is known — obviously a placeholder. */
|
|
@@ -310,7 +290,7 @@ export function workflowYaml(papers: string, ref: string | null): string {
|
|
|
310
290
|
` runs-on: ubuntu-latest`,
|
|
311
291
|
` steps:`,
|
|
312
292
|
` - uses: actions/checkout@v4`,
|
|
313
|
-
` - uses: zernie/
|
|
293
|
+
` - uses: zernie/paperlint@${ref ?? UNPINNED_REF}`,
|
|
314
294
|
` with:`,
|
|
315
295
|
` paths: ${papers}`,
|
|
316
296
|
``,
|
|
@@ -380,7 +360,7 @@ export function reportWorkflow(
|
|
|
380
360
|
if (wf !== "written" && wf !== "kept")
|
|
381
361
|
out.push(
|
|
382
362
|
` to run the same checks in CI, add this step to a workflow:`,
|
|
383
|
-
` - uses: zernie/
|
|
363
|
+
` - uses: zernie/paperlint@${ref ?? UNPINNED_REF}`,
|
|
384
364
|
` with:`,
|
|
385
365
|
` paths: ${papersDir}`,
|
|
386
366
|
);
|
|
@@ -638,6 +618,73 @@ export interface InitOptions {
|
|
|
638
618
|
}
|
|
639
619
|
|
|
640
620
|
/** Reads one line from a real terminal. Kept out of `init` so the command stays testable. */
|
|
621
|
+
/**
|
|
622
|
+
* Move every paper's pre-2.1.0 `venue.json` to `paperlint.json`, the way the old package.json key
|
|
623
|
+
* is moved, and in the same step `"venue": "aisec"` becomes `"extends": "paperlint:aisec"`: written
|
|
624
|
+
* as `paperlint.json` when it is alone, removed when `paperlint.json` already says the same, and
|
|
625
|
+
* REFUSED — both files left as they are — when they differ, since there is no way to know which
|
|
626
|
+
* one the author means. `code` is 2 when anything was refused.
|
|
627
|
+
*/
|
|
628
|
+
export function migratePaperSettings(papersAbs: string): {
|
|
629
|
+
readonly code: number;
|
|
630
|
+
readonly lines: readonly string[];
|
|
631
|
+
} {
|
|
632
|
+
const lines: string[] = [];
|
|
633
|
+
let code = 0;
|
|
634
|
+
for (const dir of papersIn(papersAbs, [
|
|
635
|
+
...PAPER_MARKERS,
|
|
636
|
+
LEGACY_PAPER_SETTINGS_FILE,
|
|
637
|
+
])) {
|
|
638
|
+
const r = migrateOne(dir, (p) => relative(papersAbs, p));
|
|
639
|
+
if (r === null) continue;
|
|
640
|
+
lines.push(r.line);
|
|
641
|
+
if (r.refused) code = 2;
|
|
642
|
+
}
|
|
643
|
+
return {
|
|
644
|
+
code,
|
|
645
|
+
lines: lines.length ? ["", "paper settings", ...lines] : [],
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/** One paper's move: done, and the line that says so — or null when there is nothing to move. */
|
|
650
|
+
function migrateOne(
|
|
651
|
+
dir: string,
|
|
652
|
+
shown: (p: string) => string,
|
|
653
|
+
): { readonly line: string; readonly refused: boolean } | null {
|
|
654
|
+
const [from, to] = [LEGACY_PAPER_SETTINGS_FILE, PAPER_SETTINGS_FILE].map(
|
|
655
|
+
(f) => join(dir, f),
|
|
656
|
+
) as [string, string];
|
|
657
|
+
const read = (p: string) => (existsSync(p) ? readFileSync(p) : null);
|
|
658
|
+
const plan = migrationOf(read(from), read(to));
|
|
659
|
+
switch (plan.kind) {
|
|
660
|
+
case "none":
|
|
661
|
+
return null;
|
|
662
|
+
case "move":
|
|
663
|
+
writeFileSync(to, plan.text);
|
|
664
|
+
unlinkSync(from);
|
|
665
|
+
return {
|
|
666
|
+
line: ` ✓ ${shown(from)} → ${shown(to)} ("venue" is now "extends": "paperlint:<name>"; renamed in paperlint 2.1.0)`,
|
|
667
|
+
refused: false,
|
|
668
|
+
};
|
|
669
|
+
case "drop-legacy":
|
|
670
|
+
unlinkSync(from);
|
|
671
|
+
return {
|
|
672
|
+
line: ` ✓ ${shown(from)} removed — ${shown(to)} already says the same`,
|
|
673
|
+
refused: false,
|
|
674
|
+
};
|
|
675
|
+
case "conflict":
|
|
676
|
+
return {
|
|
677
|
+
line: ` ✗ ${shown(from)} and ${shown(to)} both exist and differ — nothing was moved. Keep ${PAPER_SETTINGS_FILE}, copy what you need from ${LEGACY_PAPER_SETTINGS_FILE} into it (its "venue": "x" is "extends": "paperlint:x"), delete ${LEGACY_PAPER_SETTINGS_FILE}, then run init again`,
|
|
678
|
+
refused: true,
|
|
679
|
+
};
|
|
680
|
+
case "broken":
|
|
681
|
+
return {
|
|
682
|
+
line: ` ✗ ${shown(from)} was not moved: ${plan.why}`,
|
|
683
|
+
refused: true,
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
641
688
|
export async function askOnTerminal(question: string): Promise<string> {
|
|
642
689
|
// eslint-disable-next-line boundaries/dependencies -- legacy I/O, moves behind a port in #76
|
|
643
690
|
const { createInterface } = await import("node:readline/promises");
|
|
@@ -772,23 +819,12 @@ export async function init(
|
|
|
772
819
|
` one declaration — the hooks, the rules and the CLI all read this one key`,
|
|
773
820
|
);
|
|
774
821
|
// Every step below uses the DECLARED directory. A kept declaration outranks what init
|
|
775
|
-
// measured or guessed: otherwise the first paper
|
|
822
|
+
// measured or guessed: otherwise the first paper and the workflow would land in the
|
|
776
823
|
// guessed directory while lint and the hooks keep reading the declared one.
|
|
777
824
|
const papersDir =
|
|
778
825
|
decl.status === "kept" ? (decl.papers as string) : choice.papers;
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
log(
|
|
782
|
-
` ⚠ rpp.json was already here — gave it the same ${PAPERS_DIR_FIELD} value; it is deprecated`,
|
|
783
|
-
);
|
|
784
|
-
else if (rpp === "kept")
|
|
785
|
-
log(
|
|
786
|
-
` ⚠ rpp.json was already here and already declares ${PAPERS_DIR_FIELD} — left untouched; it is deprecated`,
|
|
787
|
-
);
|
|
788
|
-
else if (rpp === "unparsable")
|
|
789
|
-
log(
|
|
790
|
-
` ⚠ rpp.json is here and does not parse — left untouched; it is deprecated, delete it`,
|
|
791
|
-
);
|
|
826
|
+
const moved = migratePaperSettings(resolve(root, papersDir));
|
|
827
|
+
for (const line of moved.lines) (moved.code ? err : log)(line);
|
|
792
828
|
|
|
793
829
|
// ── 3. the skills, linked where Claude Code looks for them ─────────────────────────────
|
|
794
830
|
for (const line of reportSkillLinks(link(root), here)) log(line);
|
|
@@ -896,5 +932,5 @@ export async function init(
|
|
|
896
932
|
`doctor exits ${String(code)} — the install is NOT finished. The lines marked ✗ above say what is\n` +
|
|
897
933
|
`left; re-run \`npx paperlint doctor\` once you have done them.`,
|
|
898
934
|
);
|
|
899
|
-
return paperCode
|
|
935
|
+
return paperCode || moved.code || code;
|
|
900
936
|
}
|
|
@@ -112,7 +112,7 @@ const undefinedCs = [
|
|
|
112
112
|
"Here is how much of TeX's memory you used:",
|
|
113
113
|
];
|
|
114
114
|
const ex = errorExcerpt(undefinedCs);
|
|
115
|
-
// Guards: the spelling
|
|
115
|
+
// Guards: the spelling paperlint's own flags produce for most errors — with only the `!` form recognised,
|
|
116
116
|
// an undefined control sequence fails with no error line quoted.
|
|
117
117
|
check(
|
|
118
118
|
"-file-line-error form: the error line, the l.NNN line and the rest of the source line",
|
|
@@ -50,7 +50,9 @@ const check = (label, cond) => {
|
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
const SKILLS = ["alpha", "beta", "gamma"];
|
|
53
|
-
const work = realpathSync(
|
|
53
|
+
const work = realpathSync(
|
|
54
|
+
mkdtempSync(join(tmpdir(), "paperlint-link-skills-")),
|
|
55
|
+
);
|
|
54
56
|
|
|
55
57
|
/** A package as a manager would unpack it: manifest, skills, one non-skill. */
|
|
56
58
|
function writePackage(dir) {
|
|
@@ -27,7 +27,7 @@ process.exit(
|
|
|
27
27
|
runner: "node",
|
|
28
28
|
cases: [
|
|
29
29
|
{
|
|
30
|
-
// The definition lives in the port since
|
|
30
|
+
// The definition lives in the port since paperlint#62 (`installedSkills` in consumer.mjs), so
|
|
31
31
|
// the defect is planted THERE and must still die in THIS harness — the evidence that the
|
|
32
32
|
// linker reads it through the shared function rather than through a copy of its own.
|
|
33
33
|
name: "any directory counts as a skill",
|
package/src/link-skills.ts
CHANGED
|
@@ -66,7 +66,7 @@ export function locatePackage(project: string): Located {
|
|
|
66
66
|
let manifest: string;
|
|
67
67
|
try {
|
|
68
68
|
const req = createRequire(
|
|
69
|
-
pathToFileURL(join(project, "
|
|
69
|
+
pathToFileURL(join(project, "__paperlint_locate__.js")).href,
|
|
70
70
|
);
|
|
71
71
|
manifest = req.resolve(`${PACKAGE_NAME}/package.json`);
|
|
72
72
|
} catch (e) {
|
|
@@ -100,7 +100,7 @@ export function shippedSkills(
|
|
|
100
100
|
// clean run.
|
|
101
101
|
if (!existsSync(skillsDir))
|
|
102
102
|
return { error: `the package's skills directory is missing: ${skillsDir}` };
|
|
103
|
-
// The same function every reader of an installed skills directory calls (
|
|
103
|
+
// The same function every reader of an installed skills directory calls (paperlint#62): the writer
|
|
104
104
|
// and the readers of this fact share one definition of "a skill is here", links included.
|
|
105
105
|
try {
|
|
106
106
|
return { skillsDir, names: installedSkills(skillsDir) };
|
|
@@ -52,7 +52,7 @@ const check = (label, cond) => {
|
|
|
52
52
|
assert.ok(cond, label);
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
-
const work = realpathSync(mkdtempSync(join(tmpdir(), "
|
|
55
|
+
const work = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-new-")));
|
|
56
56
|
const read = (...p) => readFileSync(join(...p), "utf8");
|
|
57
57
|
const front = (text) => load(frontmatterBlock(text) ?? "") ?? {};
|
|
58
58
|
|
|
@@ -98,11 +98,11 @@ try {
|
|
|
98
98
|
const papers = join(work, "papers");
|
|
99
99
|
const r = newPaper(papers, "demo", "tex");
|
|
100
100
|
check(
|
|
101
|
-
"a fresh folder gets the scorecard
|
|
101
|
+
"a fresh folder gets the scorecard, a .tex source and paperlint.json, all from the package",
|
|
102
102
|
r.ok &&
|
|
103
103
|
r.fresh &&
|
|
104
104
|
r.files.map((f) => `${f.file}:${f.status}:${f.from}`).join(" ") ===
|
|
105
|
-
"PIPELINE-STATUS.md:created:package paper.tex:created:package",
|
|
105
|
+
"PIPELINE-STATUS.md:created:package paper.tex:created:package paperlint.json:created:package",
|
|
106
106
|
);
|
|
107
107
|
const status = read(papers, "demo", STATUS_FILE);
|
|
108
108
|
const fm = front(status);
|
|
@@ -122,8 +122,10 @@ try {
|
|
|
122
122
|
);
|
|
123
123
|
const l = await lint(join(papers, "demo"));
|
|
124
124
|
check(
|
|
125
|
-
"🔴 `paperlint lint` passes the scaffold
|
|
126
|
-
l.code === 0 &&
|
|
125
|
+
"🔴 `paperlint lint` passes the scaffold — exit 0, not a missing-file error — with the one warning that no venue is chosen yet",
|
|
126
|
+
l.code === 0 &&
|
|
127
|
+
/names no venue preset yet/.test(l.text) &&
|
|
128
|
+
/1 problem \(0 errors, 1 warning\)/.test(l.text),
|
|
127
129
|
);
|
|
128
130
|
|
|
129
131
|
// The format contract of the skills' checker: four sections, parsed.
|
|
@@ -171,9 +173,10 @@ try {
|
|
|
171
173
|
);
|
|
172
174
|
check(
|
|
173
175
|
"wantedFiles asks for a source only when neither format is there",
|
|
174
|
-
wantedFiles(join(papers, "old"), "tex").join() ===
|
|
176
|
+
wantedFiles(join(papers, "old"), "tex").join() ===
|
|
177
|
+
`${STATUS_FILE},paperlint.json` &&
|
|
175
178
|
wantedFiles(join(papers, "nowhere"), "md").join() ===
|
|
176
|
-
`${STATUS_FILE},paper.md`,
|
|
179
|
+
`${STATUS_FILE},paper.md,paperlint.json`,
|
|
177
180
|
);
|
|
178
181
|
writeFileSync(join(papers, "afile"), "x");
|
|
179
182
|
check(
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `paperlint new` writes `<paper>/paperlint.json` — from the package's template, or from the
|
|
3
|
+
* project's `<papers>/.template/` when it has one — with no venue chosen yet, which lint then names
|
|
4
|
+
* in one warning instead of staying silent.
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
mkdirSync,
|
|
8
|
+
mkdtempSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
realpathSync,
|
|
11
|
+
rmSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from "node:fs";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
17
|
+
import { newPaper, OVERRIDE_DIR } from "./new-paper.ts";
|
|
18
|
+
import { parsePaperSettings } from "./paper-settings.ts";
|
|
19
|
+
import { run } from "./cli.ts";
|
|
20
|
+
|
|
21
|
+
const dirs: string[] = [];
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true });
|
|
24
|
+
});
|
|
25
|
+
const tmp = () => {
|
|
26
|
+
const d = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-new-")));
|
|
27
|
+
dirs.push(d);
|
|
28
|
+
return d;
|
|
29
|
+
};
|
|
30
|
+
const settingsOf = (dir: string) =>
|
|
31
|
+
JSON.parse(readFileSync(join(dir, "paperlint.json"), "utf8")) as Record<
|
|
32
|
+
string,
|
|
33
|
+
unknown
|
|
34
|
+
>;
|
|
35
|
+
|
|
36
|
+
describe("paperlint new — paperlint.json", () => {
|
|
37
|
+
it("is always written, from the package template: no venue chosen yet, and valid", () => {
|
|
38
|
+
const papers = join(tmp(), "papers");
|
|
39
|
+
const r = newPaper(papers, "demo", "tex");
|
|
40
|
+
expect(r.ok && r.files.find((f) => f.file === "paperlint.json")).toEqual({
|
|
41
|
+
file: "paperlint.json",
|
|
42
|
+
status: "created",
|
|
43
|
+
from: "package",
|
|
44
|
+
});
|
|
45
|
+
const s = settingsOf(join(papers, "demo"));
|
|
46
|
+
expect(s["extends"]).toBeNull();
|
|
47
|
+
expect(String(s["$comment"])).toMatch(/paperlint:agenticdev/);
|
|
48
|
+
expect(parsePaperSettings(s)).toEqual({
|
|
49
|
+
ok: true,
|
|
50
|
+
value: { extends: null, kind: null, pdf: null, rules: null },
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("comes from the project's <papers>/.template/ when it has one", () => {
|
|
55
|
+
const papers = join(tmp(), "papers");
|
|
56
|
+
mkdirSync(join(papers, OVERRIDE_DIR), { recursive: true });
|
|
57
|
+
writeFileSync(
|
|
58
|
+
join(papers, OVERRIDE_DIR, "paperlint.json"),
|
|
59
|
+
'{ "extends": "paperlint:aisec", "kind": "research" }\n',
|
|
60
|
+
);
|
|
61
|
+
const r = newPaper(papers, "house", "tex");
|
|
62
|
+
expect(r.ok && r.files.find((f) => f.file === "paperlint.json")?.from).toBe(
|
|
63
|
+
"project",
|
|
64
|
+
);
|
|
65
|
+
expect(settingsOf(join(papers, "house"))).toEqual({
|
|
66
|
+
extends: "paperlint:aisec",
|
|
67
|
+
kind: "research",
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("is never overwritten", () => {
|
|
72
|
+
const papers = join(tmp(), "papers");
|
|
73
|
+
mkdirSync(join(papers, "p"), { recursive: true });
|
|
74
|
+
writeFileSync(
|
|
75
|
+
join(papers, "p", "paperlint.json"),
|
|
76
|
+
'{"extends":"paperlint:aisec"}',
|
|
77
|
+
);
|
|
78
|
+
const r = newPaper(papers, "p", "tex");
|
|
79
|
+
expect(
|
|
80
|
+
r.ok && r.files.find((f) => f.file === "paperlint.json")?.status,
|
|
81
|
+
).toBe("kept");
|
|
82
|
+
expect(settingsOf(join(papers, "p"))).toEqual({
|
|
83
|
+
extends: "paperlint:aisec",
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("is not added beside a pre-2.1.0 venue.json — `paperlint init` moves that one", () => {
|
|
88
|
+
const papers = join(tmp(), "papers");
|
|
89
|
+
mkdirSync(join(papers, "old"), { recursive: true });
|
|
90
|
+
writeFileSync(join(papers, "old", "venue.json"), '{"venue":"aisec"}');
|
|
91
|
+
const r = newPaper(papers, "old", "tex");
|
|
92
|
+
expect(r.ok && r.files.map((f) => f.file)).not.toContain("paperlint.json");
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("paperlint lint — a paper with no venue preset chosen", () => {
|
|
97
|
+
async function lintNew(extendsValue: string | null) {
|
|
98
|
+
const root = tmp();
|
|
99
|
+
writeFileSync(
|
|
100
|
+
join(root, "package.json"),
|
|
101
|
+
JSON.stringify({
|
|
102
|
+
name: "c",
|
|
103
|
+
private: true,
|
|
104
|
+
paperlint: { papersDir: "papers" },
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
newPaper(join(root, "papers"), "demo", "tex");
|
|
108
|
+
if (extendsValue !== null)
|
|
109
|
+
writeFileSync(
|
|
110
|
+
join(root, "papers", "demo", "paperlint.json"),
|
|
111
|
+
JSON.stringify({ extends: extendsValue, kind: "short" }),
|
|
112
|
+
);
|
|
113
|
+
const out: string[] = [];
|
|
114
|
+
const code = await run(["lint", "--json"], {
|
|
115
|
+
cwd: root,
|
|
116
|
+
log: (s: string) => out.push(s),
|
|
117
|
+
err: () => {},
|
|
118
|
+
});
|
|
119
|
+
const messages = (
|
|
120
|
+
JSON.parse(out.join("\n")) as {
|
|
121
|
+
messages: { ruleId: string; severity: number; message: string }[];
|
|
122
|
+
}[]
|
|
123
|
+
).flatMap((r) => r.messages);
|
|
124
|
+
return { code, messages };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
it("gets exactly one warning naming the file to set, and exits 0", async () => {
|
|
128
|
+
const { code, messages } = await lintNew(null);
|
|
129
|
+
expect(code).toBe(0);
|
|
130
|
+
expect(messages).toHaveLength(1);
|
|
131
|
+
expect(messages[0]?.ruleId).toBe("pdf/measured");
|
|
132
|
+
expect(messages[0]?.severity).toBe(1);
|
|
133
|
+
expect(messages[0]?.message).toMatch(/names no venue preset yet/);
|
|
134
|
+
expect(messages[0]?.message).toMatch(
|
|
135
|
+
/set "extends" in .*papers\/demo\/paperlint\.json/,
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("a paper with a real extends does not get it", async () => {
|
|
140
|
+
const { messages } = await lintNew("paperlint:agenticdev");
|
|
141
|
+
expect(messages.map((m) => m.message).join("\n")).not.toMatch(
|
|
142
|
+
/names no venue preset yet/,
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
});
|