paperlint 3.0.0 → 3.1.1
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 +28 -41
- package/.github/workflows/dependabot-automerge.yml +1 -1
- package/CLAUDE.md +51 -56
- package/CONTRIBUTING.md +30 -18
- package/README.md +179 -180
- package/bin/paperlint.test.mjs +45 -0
- package/dist/adapters/node/files.io.d.ts +2 -0
- package/dist/adapters/node/files.io.d.ts.map +1 -1
- package/dist/adapters/node/files.io.js +3 -2
- package/dist/adapters/node/files.io.js.map +1 -1
- package/dist/adapters/pdfjs/fill.d.ts +15 -4
- package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
- package/dist/adapters/pdfjs/fill.js +31 -21
- package/dist/adapters/pdfjs/fill.js.map +1 -1
- package/dist/adapters/references/index.d.ts.map +1 -1
- package/dist/adapters/references/index.js +14 -5
- package/dist/adapters/references/index.js.map +1 -1
- package/dist/adapters/references/pool.d.ts +8 -0
- package/dist/adapters/references/pool.d.ts.map +1 -0
- package/dist/adapters/references/pool.js +19 -0
- package/dist/adapters/references/pool.js.map +1 -0
- package/dist/build-engine.d.ts.map +1 -1
- package/dist/build-engine.js +6 -2
- package/dist/build-engine.js.map +1 -1
- package/dist/build.d.ts +10 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +29 -11
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +93 -6
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +296 -80
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts +27 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +4 -3
- package/dist/doctor.js.map +1 -1
- package/dist/domain/result.d.ts +0 -2
- package/dist/domain/result.d.ts.map +1 -1
- package/dist/domain/result.js +0 -4
- package/dist/domain/result.js.map +1 -1
- package/dist/engine.d.ts +9 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +27 -0
- package/dist/engine.js.map +1 -1
- package/dist/hooks-settings.d.ts +8 -5
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +32 -32
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +8 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +4 -4
- package/dist/init.js.map +1 -1
- package/dist/latex-log.d.ts +0 -2
- package/dist/latex-log.d.ts.map +1 -1
- package/dist/latex-log.js +7 -13
- package/dist/latex-log.js.map +1 -1
- package/dist/link-skills.d.ts +15 -2
- package/dist/link-skills.d.ts.map +1 -1
- package/dist/link-skills.js +36 -20
- package/dist/link-skills.js.map +1 -1
- package/dist/new-paper.d.ts +8 -1
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +50 -15
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-files.d.ts +30 -8
- package/dist/paper-files.d.ts.map +1 -1
- package/dist/paper-files.js +59 -30
- package/dist/paper-files.js.map +1 -1
- package/dist/pdf-facts.d.ts +11 -0
- package/dist/pdf-facts.d.ts.map +1 -1
- package/dist/pdf-facts.js +16 -18
- package/dist/pdf-facts.js.map +1 -1
- package/dist/pdf-geometry.js +1 -1
- package/dist/pdf-geometry.js.map +1 -1
- package/dist/presets.d.ts +2 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +3 -1
- package/dist/presets.js.map +1 -1
- package/dist/references.js +2 -2
- package/dist/references.js.map +1 -1
- package/dist/structure.js +1 -1
- package/dist/structure.js.map +1 -1
- package/dist/tex-requirements.d.ts +8 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +6 -5
- package/dist/tex-requirements.js.map +1 -1
- package/dist/toolchain.d.ts +8 -3
- package/dist/toolchain.d.ts.map +1 -1
- package/dist/toolchain.js +26 -10
- package/dist/toolchain.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +12 -10
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +48 -4
- package/docs/prior-art/README.md +1 -1
- package/docs/prior-art/test-tooling.md +7 -30
- package/docs/rules.md +29 -25
- package/docs/skills.md +80 -0
- package/eslint-rules/install-path-literals.harness.mjs +17 -1
- package/eslint-rules/latex-language.harness.mjs +3 -3
- package/eslint-rules/latex-language.mjs +12 -9
- package/eslint-rules/latex-language.test.mjs +200 -0
- package/eslint-rules/paper-research-question.harness.mjs +73 -7
- package/eslint-rules/paper-stages.harness.mjs +9 -14
- package/eslint-rules/paper-stages.test.mjs +135 -0
- package/eslint-rules/paper-typography.mjs +56 -9
- package/eslint-rules/paper-typography.test.mjs +186 -1
- package/eslint-rules/pdf-last-page-balance.harness.mjs +63 -7
- package/eslint-rules/review-frontmatter.mjs +2 -1
- package/eslint-rules/review-frontmatter.schema.json +1 -1
- package/eslint-rules/review-frontmatter.test.mjs +37 -1
- package/eslint-rules/temp-root-realpath.harness.mjs +49 -0
- package/eslint-rules/tex-build.mjs +13 -13
- package/eslint.config.mjs +6 -2
- package/fixtures/toolchain-mirror/catalog.txt +1 -0
- package/fixtures/toolchain-mirror/install-tl +4 -1
- package/fixtures/toolchain-mirror/stub-fmtutil-sys +4 -0
- package/fixtures/toolchain-mirror/stub-tlmgr +15 -0
- package/hooks/hooks.harness.mjs +6 -11
- package/hooks/paper-edit-guard.hook.mjs +2 -2
- package/hooks/paper-edit-guard.test.mjs +61 -0
- package/hooks/paper-skills-nudge.hook.mjs +2 -2
- package/hooks/paper-skills-nudge.test.mjs +38 -0
- package/hooks/paper-status-gates.test.mjs +42 -0
- package/lib/agent-cli-version.harness.mjs +7 -0
- package/lib/check.d.mts +17 -0
- package/lib/check.mjs +50 -0
- package/lib/check.test.ts +67 -0
- package/lib/markdown.mjs +16 -69
- package/lib/markdown.test.mjs +246 -0
- package/lib/paper-config.harness.mjs +28 -9
- package/lib/paper-config.mjs +7 -2
- package/lib/skill-checks.mjs +18 -46
- package/lib/skill-checks.test.mjs +297 -0
- package/lib/skill-corpus.mjs +11 -13
- package/lib/skill-corpus.test.mjs +54 -0
- package/lib/skill-trigger-cases.harness.mjs +57 -4
- package/package.json +6 -5
- package/scripts/check.harness.mjs +4 -10
- package/scripts/check.mjs +45 -42
- package/scripts/check.test.mjs +104 -0
- package/scripts/coverage-config.test.ts +406 -0
- package/scripts/eslint-report-guard.test.mjs +48 -0
- package/scripts/harness-api.frozen.json +0 -2
- package/scripts/layer-legacy-frozen.d.mts +9 -1
- package/scripts/layer-legacy-frozen.mjs +28 -11
- package/scripts/layer-legacy-frozen.test.ts +143 -1
- package/scripts/rules-are-content-only.harness.mjs +3 -11
- package/scripts/rules-are-content-only.mjs +29 -19
- package/scripts/rules-are-content-only.test.mjs +61 -0
- package/scripts/rules-see-files.mjs +11 -9
- package/scripts/rules-see-files.test.mjs +71 -0
- package/scripts/test.mjs +44 -0
- package/scripts/test.test.mjs +52 -0
- package/skills/grade-paper-writing/__snapshots__/prose-lint.test.mjs.snap +57 -0
- package/skills/grade-paper-writing/prose-lint.mjs +31 -12
- package/skills/grade-paper-writing/prose-lint.test.mjs +153 -0
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +3 -3
- package/skills/paper-pipeline/scripts/README.md +13 -132
- package/skills/paper-pipeline/scripts/announce.test.mjs +102 -0
- package/skills/paper-pipeline/scripts/artifact-coverage.test.mjs +86 -0
- package/skills/paper-pipeline/scripts/consumer.d.mts +8 -1
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +3 -3
- package/skills/paper-pipeline/scripts/consumer.mjs +13 -25
- package/skills/paper-pipeline/scripts/consumer.test.mjs +113 -0
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +9 -4
- package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +380 -0
- package/skills/paper-pipeline/scripts/fixtures/scorecard.mjs +71 -0
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +4 -4
- package/skills/paper-pipeline/scripts/generated-code.mjs +3 -2
- package/skills/paper-pipeline/scripts/generated-code.test.mjs +62 -0
- package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +302 -0
- package/skills/paper-pipeline/scripts/ledger.mjs +1 -1
- package/skills/paper-pipeline/scripts/{ledger.selftest.mjs → ledger.test.mjs} +44 -76
- package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +474 -0
- package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +2 -63
- package/skills/paper-pipeline/scripts/pipeline-check.mjs +37 -21
- package/skills/paper-pipeline/scripts/pipeline-edges.mjs +2 -2
- package/skills/paper-pipeline/scripts/pipeline-edges.test.mjs +73 -0
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +4 -47
- package/skills/paper-pipeline/scripts/population-map.mjs +12 -2
- package/skills/paper-pipeline/scripts/population-map.test.mjs +154 -0
- package/skills/paper-pipeline/scripts/provenance.harness.mjs +26 -0
- package/skills/paper-pipeline/scripts/round-diff.harness.mjs +1 -2
- package/skills/paper-pipeline/scripts/round-diff.mjs +13 -6
- package/skills/paper-pipeline/scripts/round-diff.test.mjs +159 -0
- package/skills/paper-pipeline/scripts/run-mechanical.mjs +30 -6
- package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +468 -0
- package/skills/paper-pipeline/scripts/status.mjs +2 -2
- package/skills/paper-pipeline/scripts/status.test.mjs +232 -0
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +2 -2
- package/skills/render-paper/extract-pdf-facts.harness.mjs +56 -7
- package/skills/tighten-paper/__snapshots__/structure.test.mjs.snap +133 -0
- package/skills/tighten-paper/structure.test.mjs +116 -0
- package/skills/verify-citations/SKILL.md +5 -5
- package/skills/verify-citations/SKILL.md.spec.ts +4 -4
- package/skills/verify-citations/scripts/__snapshots__/verify-cites.net.test.mjs.snap +235 -0
- package/skills/verify-citations/scripts/bib-authors.check.test.mjs +274 -0
- package/skills/verify-citations/scripts/bib-authors.cli.test.mjs +178 -0
- package/skills/verify-citations/scripts/bib-authors.mjs +2 -1
- package/skills/verify-citations/scripts/verify-cites.cli.test.mjs +160 -0
- package/skills/verify-citations/scripts/verify-cites.mjs +96 -96
- package/skills/verify-citations/scripts/verify-cites.net.test.mjs +287 -0
- package/skills/verify-citations/scripts/verify-cites.test.mjs +149 -5
- package/src/adapters/banal/failure.test.ts +70 -1
- package/src/adapters/banal/index.test.ts +16 -0
- package/src/adapters/banal/output.test.ts +5 -0
- package/src/adapters/curl/download.test.ts +43 -1
- package/src/adapters/memory/index.test.ts +25 -0
- package/src/adapters/node/files.io.ts +3 -2
- package/src/adapters/node/files.test.ts +13 -1
- package/src/adapters/node/process.test.ts +38 -0
- package/src/adapters/pdfjs/fill.test.ts +34 -0
- package/src/adapters/pdfjs/fill.ts +44 -25
- package/src/adapters/references/index.test.ts +228 -0
- package/src/adapters/references/index.ts +21 -6
- package/src/adapters/references/pool.test.ts +31 -0
- package/src/adapters/references/pool.ts +24 -0
- package/src/build-engine.harness.mjs +3 -7
- package/src/build-engine.test.ts +137 -0
- package/src/build-engine.ts +6 -1
- package/src/build.harness.mjs +51 -11
- package/src/build.test.ts +362 -0
- package/src/build.ts +40 -14
- package/src/cli.harness.mjs +12 -19
- package/src/cli.test.ts +584 -0
- package/src/cli.ts +409 -94
- package/src/doctor.harness.mjs +3 -8
- package/src/doctor.test.ts +64 -0
- package/src/doctor.ts +12 -4
- package/src/domain/result.test.ts +4 -0
- package/src/domain/result.ts +0 -5
- package/src/engine.harness.mjs +77 -7
- package/src/engine.ts +35 -0
- package/src/facts-file.test.ts +110 -0
- package/src/hooks-settings.harness.mjs +43 -13
- package/src/hooks-settings.ts +41 -35
- package/src/init.test.ts +308 -5
- package/src/init.ts +4 -4
- package/src/latex-log.harness.mjs +32 -7
- package/src/latex-log.ts +6 -13
- package/src/latex-loop.harness.mjs +8 -7
- package/src/link-skills.harness.mjs +3 -8
- package/src/link-skills.test.ts +105 -0
- package/src/link-skills.ts +75 -21
- package/src/linted-set.test.ts +118 -0
- package/src/new-paper.harness.mjs +5 -7
- package/src/new-paper.test.ts +203 -2
- package/src/new-paper.ts +84 -17
- package/src/paper-files.test.ts +122 -5
- package/src/paper-files.ts +99 -39
- package/src/paper-settings-commands.test.ts +41 -5
- package/src/paper-settings.test.ts +20 -0
- package/src/parse-args.test.ts +48 -0
- package/src/pdf-facts.harness.mjs +3 -6
- package/src/pdf-facts.test.ts +144 -0
- package/src/pdf-facts.ts +17 -19
- package/src/pdf-geometry.harness.mjs +3 -7
- package/src/pdf-geometry.ts +1 -1
- package/src/presets.test.ts +2 -0
- package/src/presets.ts +7 -1
- package/src/references.test.ts +97 -0
- package/src/references.ts +2 -2
- package/src/rules-config.harness.mjs +10 -8
- package/src/rules-off.test.ts +111 -0
- package/src/structure.harness.mjs +5 -9
- package/src/structure.ts +1 -1
- package/src/tex-requirements.harness.mjs +34 -7
- package/src/tex-requirements.ts +12 -6
- package/src/toolchain.harness.mjs +44 -13
- package/src/toolchain.test.ts +280 -0
- package/src/toolchain.ts +37 -12
- package/src/types.ts +6 -0
- package/src/venue-rules.test.ts +63 -0
- package/src/venue-rules.ts +14 -12
- package/templates/paper/paperlint.json +1 -1
- package/action.mutations.mjs +0 -162
- package/eslint-rules/install-path-literals.mutations.mjs +0 -62
- package/eslint-rules/latex-language.mutations.mjs +0 -196
- package/eslint-rules/paper-research-question.mutations.mjs +0 -127
- package/eslint-rules/paper-stages.mutations.mjs +0 -81
- package/eslint-rules/papers.mutations.mjs +0 -186
- package/eslint-rules/temp-root-realpath.mutations.mjs +0 -99
- package/eslint-rules/tex-build.mutations.mjs +0 -258
- package/hooks/hooks.mutations.mjs +0 -337
- package/lib/agent-cli-version.mutations.mjs +0 -109
- package/lib/mutation-driver.harness.mjs +0 -227
- package/lib/mutation-driver.mjs +0 -397
- package/lib/mutation-driver.mutations.mjs +0 -68
- package/lib/paper-config.mutations.mjs +0 -143
- package/lib/skill-trigger-cases.mutations.mjs +0 -65
- package/scripts/check.mutations.mjs +0 -110
- package/scripts/exclusive.mjs +0 -138
- package/scripts/mutation-batteries-frozen.harness.mjs +0 -204
- package/scripts/mutation-batteries-frozen.mjs +0 -238
- package/scripts/mutation-batteries.frozen.json +0 -108
- package/scripts/rules-are-content-only.mutations.mjs +0 -81
- package/scripts/rules-see-files.mutations.mjs +0 -131
- package/scripts/run-mutations.mjs +0 -100
- package/skills/paper-pipeline/scripts/artifact-coverage.mutations.mjs +0 -218
- package/skills/paper-pipeline/scripts/consumer.mutations.mjs +0 -190
- package/skills/paper-pipeline/scripts/extract-ref-facts.mutations.mjs +0 -54
- package/skills/paper-pipeline/scripts/generated-code.mutations.mjs +0 -254
- package/skills/paper-pipeline/scripts/pipeline-check.mutations.mjs +0 -54
- package/skills/paper-pipeline/scripts/population-map.mutations.mjs +0 -65
- package/skills/paper-pipeline/scripts/population-map.selftest.mjs +0 -122
- package/skills/paper-pipeline/scripts/provenance.mutations.mjs +0 -59
- package/skills/paper-pipeline/scripts/round-diff.mutations.mjs +0 -276
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +0 -65
- package/skills/skill-contract.mutations.mjs +0 -179
- package/src/build.mutations.mjs +0 -196
- package/src/cli.mutations.mjs +0 -414
- package/src/doctor.mutations.mjs +0 -175
- package/src/hooks-settings.mutations.mjs +0 -99
- package/src/link-skills.mutations.mjs +0 -140
- package/src/new-paper.mutations.mjs +0 -79
- package/src/structure.mutations.mjs +0 -83
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* link-skills.ts edges the harness does not reach: a resolver error without a code, a package that
|
|
3
|
+
* resolved outside every `node_modules` up the tree, a skills directory that is missing or holds a
|
|
4
|
+
* dangling link, a skills home that cannot be made, an entry that is a plain file, and — through
|
|
5
|
+
* the injected fs — an entry that cannot be resolved and a link that cannot be created.
|
|
6
|
+
*/
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import {
|
|
9
|
+
lstatSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
readlinkSync,
|
|
12
|
+
realpathSync,
|
|
13
|
+
symlinkSync,
|
|
14
|
+
} from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { test } from "vitest";
|
|
17
|
+
import { useTempDir, writeTree } from "../test/support.mjs";
|
|
18
|
+
import { linkSkills, locatePackage, type LinkFs } from "./link-skills.ts";
|
|
19
|
+
|
|
20
|
+
const root = useTempDir("paperlint-link-skills-");
|
|
21
|
+
const realFs: LinkFs = { lstatSync, realpathSync, readlinkSync, symlinkSync };
|
|
22
|
+
|
|
23
|
+
/** A package directory shipping skills `names` under `skills/`. */
|
|
24
|
+
function pkg(name: string, names: string[]): string {
|
|
25
|
+
const dir = join(root, name, "pkg");
|
|
26
|
+
mkdirSync(join(dir, "skills"), { recursive: true });
|
|
27
|
+
writeTree(
|
|
28
|
+
dir,
|
|
29
|
+
Object.fromEntries(names.map((n) => [`skills/${n}/SKILL.md`, "# s\n"])),
|
|
30
|
+
);
|
|
31
|
+
return dir;
|
|
32
|
+
}
|
|
33
|
+
const at = (dir: string) => () => ({ dir, spelled: dir });
|
|
34
|
+
|
|
35
|
+
test("locatePackage: a resolver error without a code; a package found outside every node_modules", () => {
|
|
36
|
+
assert.deepEqual(
|
|
37
|
+
locatePackage(root, {
|
|
38
|
+
resolveManifest: () => {
|
|
39
|
+
throw new Error("odd\nsecond line");
|
|
40
|
+
},
|
|
41
|
+
}),
|
|
42
|
+
{ error: "error: odd" },
|
|
43
|
+
);
|
|
44
|
+
const elsewhere = pkg("elsewhere", []);
|
|
45
|
+
assert.deepEqual(
|
|
46
|
+
locatePackage(root, {
|
|
47
|
+
resolveManifest: () => join(elsewhere, "package.json"),
|
|
48
|
+
}),
|
|
49
|
+
{ dir: elsewhere, spelled: elsewhere },
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("a package without a skills directory, or one holding a dangling link, is not linked", () => {
|
|
54
|
+
const bare = join(root, "bare");
|
|
55
|
+
mkdirSync(bare, { recursive: true });
|
|
56
|
+
const missing = linkSkills(join(root, "p1"), { locate: at(bare) });
|
|
57
|
+
assert.deepEqual(missing, {
|
|
58
|
+
ok: false,
|
|
59
|
+
error: `the package's skills directory is missing: ${join(bare, "skills")}`,
|
|
60
|
+
});
|
|
61
|
+
const dangling = pkg("dangling", ["a"]);
|
|
62
|
+
symlinkSync(join(root, "gone"), join(dangling, "skills", "b"));
|
|
63
|
+
const r = linkSkills(join(root, "p2"), { locate: at(dangling) });
|
|
64
|
+
assert.equal(r.ok, false);
|
|
65
|
+
assert.match(!r.ok ? r.error : "", /^1 skill link\(s\) in .* lead nowhere:/);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("a skills home that cannot be made is named", () => {
|
|
69
|
+
const project = writeTree(join(root, "p3"), {
|
|
70
|
+
".claude": "a file, not a directory",
|
|
71
|
+
});
|
|
72
|
+
const r = linkSkills(project, { locate: at(pkg("home", ["a"])) });
|
|
73
|
+
assert.equal(r.ok, false);
|
|
74
|
+
assert.match(!r.ok ? r.error : "", /^cannot create .*\.claude\/skills: /);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("entries that are someone else's: a plain file, one that cannot be resolved, a link that cannot be made", () => {
|
|
78
|
+
const shipped = pkg("entries", ["file", "unresolved", "refused"]);
|
|
79
|
+
const project = writeTree(join(root, "p4"), { ".claude/skills/file": "x" });
|
|
80
|
+
mkdirSync(join(project, ".claude", "skills", "unresolved"));
|
|
81
|
+
const fs: LinkFs = {
|
|
82
|
+
...realFs,
|
|
83
|
+
realpathSync: (p) => {
|
|
84
|
+
if (p.endsWith("unresolved")) throw new Error("ELOOP");
|
|
85
|
+
return realpathSync(p);
|
|
86
|
+
},
|
|
87
|
+
symlinkSync: () => {
|
|
88
|
+
throw new Error("EEXIST: raced");
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
const r = linkSkills(project, { locate: at(shipped), fs });
|
|
92
|
+
assert.deepEqual(r.ok ? r.links : [], [
|
|
93
|
+
{ name: "file", status: "foreign", reason: "a file" },
|
|
94
|
+
{
|
|
95
|
+
name: "refused",
|
|
96
|
+
status: "foreign",
|
|
97
|
+
reason: "could not create the link: EEXIST: raced",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "unresolved",
|
|
101
|
+
status: "foreign",
|
|
102
|
+
reason: "an entry that cannot be resolved",
|
|
103
|
+
},
|
|
104
|
+
]);
|
|
105
|
+
});
|
package/src/link-skills.ts
CHANGED
|
@@ -60,13 +60,18 @@ export type Located =
|
|
|
60
60
|
*
|
|
61
61
|
* @returns `dir` — the real directory; `spelled` — the same directory as the project names it.
|
|
62
62
|
*/
|
|
63
|
-
export function locatePackage(
|
|
63
|
+
export function locatePackage(
|
|
64
|
+
project: string,
|
|
65
|
+
{
|
|
66
|
+
resolveManifest = (from: string): string =>
|
|
67
|
+
createRequire(
|
|
68
|
+
pathToFileURL(join(from, "__paperlint_locate__.js")).href,
|
|
69
|
+
).resolve(`${PACKAGE_NAME}/package.json`),
|
|
70
|
+
}: { resolveManifest?: (project: string) => string } = {},
|
|
71
|
+
): Located {
|
|
64
72
|
let manifest: string;
|
|
65
73
|
try {
|
|
66
|
-
|
|
67
|
-
pathToFileURL(join(project, "__paperlint_locate__.js")).href,
|
|
68
|
-
);
|
|
69
|
-
manifest = req.resolve(`${PACKAGE_NAME}/package.json`);
|
|
74
|
+
manifest = resolveManifest(project);
|
|
70
75
|
} catch (e) {
|
|
71
76
|
return {
|
|
72
77
|
error: `${(e as NodeJS.ErrnoException).code ?? "error"}: ${(e as Error).message.split("\n")[0]}`,
|
|
@@ -126,10 +131,28 @@ export type LinkReport =
|
|
|
126
131
|
}
|
|
127
132
|
| { readonly ok: false; readonly error: string };
|
|
128
133
|
|
|
134
|
+
/** The filesystem calls linking makes — the disk by default; a test stages a race or a refusal. */
|
|
135
|
+
export interface LinkFs {
|
|
136
|
+
readonly lstatSync: (p: string) => {
|
|
137
|
+
isSymbolicLink(): boolean;
|
|
138
|
+
isDirectory(): boolean;
|
|
139
|
+
};
|
|
140
|
+
readonly realpathSync: (p: string) => string;
|
|
141
|
+
readonly readlinkSync: (p: string) => string;
|
|
142
|
+
readonly symlinkSync: (target: string, path: string, type: "dir") => void;
|
|
143
|
+
}
|
|
144
|
+
const nodeLinkFs: LinkFs = {
|
|
145
|
+
lstatSync,
|
|
146
|
+
realpathSync,
|
|
147
|
+
readlinkSync,
|
|
148
|
+
symlinkSync,
|
|
149
|
+
};
|
|
150
|
+
|
|
129
151
|
/** What occupies `entry`, judged against the directory it should lead to. */
|
|
130
152
|
function inspect(
|
|
131
153
|
entry: string,
|
|
132
154
|
want: string,
|
|
155
|
+
{ lstatSync, realpathSync, readlinkSync }: LinkFs,
|
|
133
156
|
): { status: "present" | "missing" | "foreign"; reason?: string } {
|
|
134
157
|
let st;
|
|
135
158
|
try {
|
|
@@ -163,15 +186,22 @@ function linkOne(
|
|
|
163
186
|
target,
|
|
164
187
|
want,
|
|
165
188
|
write,
|
|
166
|
-
|
|
189
|
+
fs,
|
|
190
|
+
}: {
|
|
191
|
+
entry: string;
|
|
192
|
+
target: string;
|
|
193
|
+
want: string;
|
|
194
|
+
write: boolean;
|
|
195
|
+
fs: LinkFs;
|
|
196
|
+
},
|
|
167
197
|
): SkillLink {
|
|
168
|
-
const seen = inspect(entry, want);
|
|
198
|
+
const seen = inspect(entry, want, fs);
|
|
169
199
|
if (!write || seen.status !== "missing")
|
|
170
200
|
return seen.reason
|
|
171
201
|
? { name, status: seen.status, reason: seen.reason }
|
|
172
202
|
: { name, status: seen.status };
|
|
173
203
|
try {
|
|
174
|
-
symlinkSync(target, entry, "dir");
|
|
204
|
+
fs.symlinkSync(target, entry, "dir");
|
|
175
205
|
return { name, status: "created" };
|
|
176
206
|
} catch (e) {
|
|
177
207
|
return {
|
|
@@ -182,6 +212,16 @@ function linkOne(
|
|
|
182
212
|
}
|
|
183
213
|
}
|
|
184
214
|
|
|
215
|
+
/** Create the skills home; the reason it could not be, or null. */
|
|
216
|
+
function makeHome(home: string): string | null {
|
|
217
|
+
try {
|
|
218
|
+
mkdirSync(home, { recursive: true });
|
|
219
|
+
return null;
|
|
220
|
+
} catch (e) {
|
|
221
|
+
return `cannot create ${home}: ${(e as Error).message}`;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
185
225
|
/**
|
|
186
226
|
* Link every shipped skill into `<project>/.claude/skills/`, or — with `write: false` — only
|
|
187
227
|
* report which ones are missing (that is what `paperlint doctor` asks).
|
|
@@ -191,7 +231,12 @@ export function linkSkills(
|
|
|
191
231
|
{
|
|
192
232
|
write = true,
|
|
193
233
|
locate = locatePackage,
|
|
194
|
-
|
|
234
|
+
fs = nodeLinkFs,
|
|
235
|
+
}: {
|
|
236
|
+
write?: boolean;
|
|
237
|
+
locate?: (p: string) => Located;
|
|
238
|
+
fs?: LinkFs;
|
|
239
|
+
} = {},
|
|
195
240
|
): LinkReport {
|
|
196
241
|
const pkg = locate(project);
|
|
197
242
|
if ("error" in pkg)
|
|
@@ -205,20 +250,28 @@ export function linkSkills(
|
|
|
205
250
|
const spelledSkills = join(pkg.spelled, relative(pkg.dir, shipped.skillsDir));
|
|
206
251
|
|
|
207
252
|
const home = join(project, SKILLS_HOME);
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
253
|
+
const made = write && shipped.names.length > 0 ? makeHome(home) : null;
|
|
254
|
+
if (made !== null) return { ok: false, error: made };
|
|
255
|
+
return {
|
|
256
|
+
ok: true,
|
|
257
|
+
home,
|
|
258
|
+
...linkAll(shipped, { spelledSkills, home, write, fs }),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Every shipped skill's link, and the first one's target as the report's example. */
|
|
263
|
+
function linkAll(
|
|
264
|
+
shipped: { readonly skillsDir: string; readonly names: readonly string[] },
|
|
265
|
+
{
|
|
266
|
+
spelledSkills,
|
|
267
|
+
home,
|
|
268
|
+
write,
|
|
269
|
+
fs,
|
|
270
|
+
}: { spelledSkills: string; home: string; write: boolean; fs: LinkFs },
|
|
271
|
+
): { example: string | null; links: SkillLink[] } {
|
|
218
272
|
// A relative target resolves against the directory that PHYSICALLY holds the link, so it is
|
|
219
273
|
// computed from the real one — `.claude` may itself be a symlink.
|
|
220
274
|
const physicalHome = existsSync(home) ? realpathSync(home) : resolve(home);
|
|
221
|
-
|
|
222
275
|
let example: string | null = null;
|
|
223
276
|
const links = shipped.names.map((name): SkillLink => {
|
|
224
277
|
const target = relative(physicalHome, join(spelledSkills, name));
|
|
@@ -228,7 +281,8 @@ export function linkSkills(
|
|
|
228
281
|
target,
|
|
229
282
|
want: realpathSync(join(shipped.skillsDir, name)),
|
|
230
283
|
write,
|
|
284
|
+
fs,
|
|
231
285
|
});
|
|
232
286
|
});
|
|
233
|
-
return {
|
|
287
|
+
return { example, links };
|
|
234
288
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE LINTED SET IS EXACTLY THE OWNED FILES — checked on the whole result, not one route in.
|
|
3
|
+
*
|
|
4
|
+
* Each test in `paper-files.test.ts` closes one way a stray file got into `paperlint lint`: vendored
|
|
5
|
+
* JavaScript under a paper (#99), a rule source that reached the siblings index (#101). This one
|
|
6
|
+
* does not care about the route. A paper with every kind of neighbour a real one has, settings from
|
|
7
|
+
* every level that generates blocks (a venue preset with `rules`, the root `rules`), and one
|
|
8
|
+
* assertion: the files ESLint returned are exactly the four paperlint owns — set equality, so a
|
|
9
|
+
* route nobody has thought of yet shows up as an extra member.
|
|
10
|
+
*/
|
|
11
|
+
import {
|
|
12
|
+
mkdirSync,
|
|
13
|
+
mkdtempSync,
|
|
14
|
+
realpathSync,
|
|
15
|
+
rmSync,
|
|
16
|
+
writeFileSync,
|
|
17
|
+
} from "node:fs";
|
|
18
|
+
import { tmpdir } from "node:os";
|
|
19
|
+
import { join, relative } from "node:path";
|
|
20
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
21
|
+
import { run } from "./cli.ts";
|
|
22
|
+
|
|
23
|
+
const dirs: string[] = [];
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const TEX = "\\documentclass{article}\n\\begin{document}x\\end{document}\n";
|
|
29
|
+
const JS = "export const x = 1;\n";
|
|
30
|
+
|
|
31
|
+
/** The files paperlint owns in the fixture paper. */
|
|
32
|
+
const OWNED = [
|
|
33
|
+
"papers/a/paper.tex",
|
|
34
|
+
"papers/a/PIPELINE-STATUS.md",
|
|
35
|
+
"papers/a/reviews/r1.md",
|
|
36
|
+
"papers/a/siblings/smith2025.md",
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/** Everything else a real paper folder and project carry. None of it may be linted. */
|
|
40
|
+
const NEIGHBOURS = {
|
|
41
|
+
"papers/a/siblings/README.md": "# Siblings\n\n- [Smith](smith2025.md)\n",
|
|
42
|
+
"papers/a/README.md": "# Paper a\n",
|
|
43
|
+
"papers/a/notes/idea.md": "# An idea\n",
|
|
44
|
+
"papers/a/repro/x.js": "return 1;\n",
|
|
45
|
+
"papers/a/repro/lib/y.mjs": JS,
|
|
46
|
+
"papers/a/repro/z.cjs": "module.exports = 1;\n",
|
|
47
|
+
"papers/a/repro/t.ts": "const t: number = 1;\n",
|
|
48
|
+
"script.js": JS,
|
|
49
|
+
"node_modules/pkg/index.js": JS,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function project(): string {
|
|
53
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-set-")));
|
|
54
|
+
dirs.push(root);
|
|
55
|
+
const all: Record<string, string> = {
|
|
56
|
+
"package.json": JSON.stringify({ name: "c", private: true }),
|
|
57
|
+
// A root `rules` block over every file of the project: the widest glob a consumer can write.
|
|
58
|
+
"paperlint.json": JSON.stringify({
|
|
59
|
+
rules: [
|
|
60
|
+
{
|
|
61
|
+
files: ["**/*.md", "**/*.tex", "**/*.js"],
|
|
62
|
+
rules: {
|
|
63
|
+
"pdf/last-page-balance": "warn",
|
|
64
|
+
"paper/section-word": "warn",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
}),
|
|
69
|
+
// A venue preset that carries `rules` of its own.
|
|
70
|
+
"papers/a/paperlint.json": JSON.stringify({
|
|
71
|
+
extends: "paperlint:agenticdev",
|
|
72
|
+
}),
|
|
73
|
+
"papers/a/paper.tex": TEX,
|
|
74
|
+
"papers/a/PIPELINE-STATUS.md": "---\nstages: []\n---\n",
|
|
75
|
+
"papers/a/reviews/r1.md": "# review\n",
|
|
76
|
+
"papers/a/siblings/smith2025.md": "---\nread: full\n---\n# Smith 2025\n",
|
|
77
|
+
...NEIGHBOURS,
|
|
78
|
+
};
|
|
79
|
+
for (const [p, text] of Object.entries(all)) {
|
|
80
|
+
mkdirSync(join(root, p, ".."), { recursive: true });
|
|
81
|
+
writeFileSync(join(root, p), text);
|
|
82
|
+
}
|
|
83
|
+
return root;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** `paperlint lint --json <args>` → the files it linted, relative to the project, sorted. */
|
|
87
|
+
async function lintedSet(root: string, args: string[]): Promise<string[]> {
|
|
88
|
+
const out: string[] = [];
|
|
89
|
+
await run(["lint", "--json", ...args], {
|
|
90
|
+
cwd: root,
|
|
91
|
+
log: (s: string) => out.push(s),
|
|
92
|
+
err: () => {},
|
|
93
|
+
});
|
|
94
|
+
type Result = { filePath: string; messages: { ruleId: string | null }[] };
|
|
95
|
+
return (
|
|
96
|
+
(JSON.parse(out.join("\n")) as Result[])
|
|
97
|
+
// A structure finding reports a MISSING file in the same shape; it was not linted.
|
|
98
|
+
.filter(
|
|
99
|
+
(r) => !r.messages.some((m) => m.ruleId === "structure/required-file"),
|
|
100
|
+
)
|
|
101
|
+
.map((r) => relative(root, r.filePath))
|
|
102
|
+
.sort()
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
describe("🔴 paperlint lint — the linted set is EXACTLY the owned files", () => {
|
|
107
|
+
it.each([
|
|
108
|
+
["the papers directory from the config", []],
|
|
109
|
+
["the paper named on the command line", ["papers/a"]],
|
|
110
|
+
// The widest scope: ESLint is handed the project root, so it enumerates `script.js` and
|
|
111
|
+
// `node_modules/` too, and only the scope keeps them out.
|
|
112
|
+
["the whole project named on the command line", ["."]],
|
|
113
|
+
])("%s", async (_, args) => {
|
|
114
|
+
// Guards: every route in at once — vendored JS/TS, a root script, node_modules, a README next
|
|
115
|
+
// to the paper, the siblings index, notes. An extra member names the file that got through.
|
|
116
|
+
expect(await lintedSet(project(), args)).toEqual([...OWNED].sort());
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
* ⚠️ Assertions at the TOP LEVEL: `vigiles test` imports the file and counts "did not throw"
|
|
17
17
|
* as a pass.
|
|
18
18
|
*/
|
|
19
|
-
import assert from "node:assert/strict";
|
|
20
19
|
import {
|
|
21
20
|
existsSync,
|
|
22
21
|
mkdirSync,
|
|
@@ -32,6 +31,7 @@ import { dirname, join } from "node:path";
|
|
|
32
31
|
import { fileURLToPath } from "node:url";
|
|
33
32
|
import { load } from "js-yaml";
|
|
34
33
|
import { frontmatterBlock } from "../lib/markdown.mjs";
|
|
34
|
+
import { createChecker } from "../lib/check.mjs";
|
|
35
35
|
|
|
36
36
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
37
37
|
const ROOT = dirname(HERE);
|
|
@@ -46,11 +46,7 @@ const {
|
|
|
46
46
|
} = await import(join(HERE, "new-paper.ts"));
|
|
47
47
|
const { run } = await import(join(HERE, "cli.ts"));
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
const check = (label, cond) => {
|
|
51
|
-
n++;
|
|
52
|
-
assert.ok(cond, label);
|
|
53
|
-
};
|
|
49
|
+
const check = createChecker();
|
|
54
50
|
|
|
55
51
|
const work = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-new-")));
|
|
56
52
|
const read = (...p) => readFileSync(join(...p), "utf8");
|
|
@@ -191,6 +187,8 @@ try {
|
|
|
191
187
|
"---\n---\n# House scorecard for {{name}}\n",
|
|
192
188
|
);
|
|
193
189
|
const own = newPaper(papers, "house", "md");
|
|
190
|
+
// Guards: the override slot — `fromTemplate` in new-paper.ts reads `<papers>/.template/<file>`
|
|
191
|
+
// before the package's copy. Replace that lookup with the package's alone and this goes red.
|
|
194
192
|
check(
|
|
195
193
|
"🔴 <papers>/.template/ is preferred for the file it holds, the package fills the rest",
|
|
196
194
|
own.ok &&
|
|
@@ -215,5 +213,5 @@ try {
|
|
|
215
213
|
}
|
|
216
214
|
|
|
217
215
|
console.log(
|
|
218
|
-
`✓ ${String(
|
|
216
|
+
`✓ ${String(check.count)} assertions passed — paperlint new scaffolds what paperlint lint accepts`,
|
|
219
217
|
);
|
package/src/new-paper.test.ts
CHANGED
|
@@ -14,9 +14,12 @@ import {
|
|
|
14
14
|
import { tmpdir } from "node:os";
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import { afterEach, describe, expect, it } from "vitest";
|
|
17
|
-
import {
|
|
17
|
+
import { existsSync } from "node:fs";
|
|
18
|
+
import { newPaper, OVERRIDE_DIR, reportNewPaper } from "./new-paper.ts";
|
|
18
19
|
import { parsePaperSettings } from "./paper-settings.ts";
|
|
19
|
-
import { run } from "./cli.ts";
|
|
20
|
+
import { chooseVenue, run } from "./cli.ts";
|
|
21
|
+
import { shippedPresets } from "./presets.ts";
|
|
22
|
+
import { packageVenuesDir } from "../skills/paper-pipeline/scripts/consumer.mjs";
|
|
20
23
|
|
|
21
24
|
const dirs: string[] = [];
|
|
22
25
|
afterEach(() => {
|
|
@@ -131,3 +134,201 @@ describe("paperlint lint — a paper with no venue preset chosen", () => {
|
|
|
131
134
|
);
|
|
132
135
|
});
|
|
133
136
|
});
|
|
137
|
+
|
|
138
|
+
/** `paperlint new <args>` in a fresh project (not a terminal: vitest's stdin is not a TTY). */
|
|
139
|
+
async function newIn(args: string[], files: Record<string, string> = {}) {
|
|
140
|
+
const root = tmp();
|
|
141
|
+
const all = {
|
|
142
|
+
"package.json": JSON.stringify({ name: "c", private: true }),
|
|
143
|
+
...files,
|
|
144
|
+
};
|
|
145
|
+
for (const [p, text] of Object.entries(all)) {
|
|
146
|
+
mkdirSync(join(root, p, ".."), { recursive: true });
|
|
147
|
+
writeFileSync(join(root, p), text);
|
|
148
|
+
}
|
|
149
|
+
const out: string[] = [];
|
|
150
|
+
const err: string[] = [];
|
|
151
|
+
const code = await run(["new", "demo", ...args], {
|
|
152
|
+
cwd: root,
|
|
153
|
+
log: (s: string) => out.push(s),
|
|
154
|
+
err: (s: string) => err.push(s),
|
|
155
|
+
});
|
|
156
|
+
const dir = join(root, "papers", "demo");
|
|
157
|
+
return {
|
|
158
|
+
code,
|
|
159
|
+
out: out.join("\n"),
|
|
160
|
+
err: err.join("\n"),
|
|
161
|
+
dir,
|
|
162
|
+
settings: () => settingsOf(dir),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
describe("paperlint new --venue / --kind", () => {
|
|
167
|
+
const PRESET = `{
|
|
168
|
+
"extends": "paperlint:acm-sigconf",
|
|
169
|
+
"format": { "kinds": { "short": { "body_pages_max": 4 } } },
|
|
170
|
+
}
|
|
171
|
+
`;
|
|
172
|
+
|
|
173
|
+
it("a shipped venue and its kind → extends and kind in paperlint.json, lint resolves it", async () => {
|
|
174
|
+
const r = await newIn(["--venue", "agenticdev", "--kind", "short"]);
|
|
175
|
+
expect(r.settings()).toEqual({
|
|
176
|
+
extends: "paperlint:agenticdev",
|
|
177
|
+
kind: "short",
|
|
178
|
+
});
|
|
179
|
+
expect(r.out).not.toMatch(/pdf\/profile/);
|
|
180
|
+
expect(r.code).toBe(0);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("🔴 a path is relative to where you run it, and written relative to the paper's file", async () => {
|
|
184
|
+
const r = await newIn(
|
|
185
|
+
["--venue", "./venues/my-workshop.jsonc", "--kind", "short"],
|
|
186
|
+
{
|
|
187
|
+
"venues/my-workshop.jsonc": PRESET,
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
// Guards: written as typed, `./venues/…` would name papers/demo/venues/…, which does not exist.
|
|
191
|
+
expect(r.settings()).toEqual({
|
|
192
|
+
extends: "../../venues/my-workshop.jsonc",
|
|
193
|
+
kind: "short",
|
|
194
|
+
});
|
|
195
|
+
expect(r.out).not.toMatch(/pdf\/profile/);
|
|
196
|
+
expect(r.code).toBe(0);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("an unknown venue exits 2, lists the shipped presets, and creates nothing", async () => {
|
|
200
|
+
const r = await newIn(["--venue", "icse"]);
|
|
201
|
+
expect(r.code).toBe(2);
|
|
202
|
+
// Guards: the list is read from the shipped presets, the same list lint prints.
|
|
203
|
+
expect(r.err).toContain(shippedPresets(packageVenuesDir()).join(", "));
|
|
204
|
+
expect(r.err).toMatch(/--venue icse: no such venue preset/);
|
|
205
|
+
expect(existsSync(r.dir)).toBe(false);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe("paperlint new --venue / --kind — refusals and defaults", () => {
|
|
210
|
+
it.each([
|
|
211
|
+
[
|
|
212
|
+
"a kind the preset lacks",
|
|
213
|
+
["--venue", "agenticdev", "--kind", "long"],
|
|
214
|
+
/`agenticdev` has no kind `long`; its kinds: short, full, demo/,
|
|
215
|
+
],
|
|
216
|
+
["a kind without a venue", ["--kind", "short"], /--kind needs --venue/],
|
|
217
|
+
[
|
|
218
|
+
"a kind for a preset with no kinds",
|
|
219
|
+
["--venue", "acm-sigconf", "--kind", "short"],
|
|
220
|
+
/`acm-sigconf` has no kinds/,
|
|
221
|
+
],
|
|
222
|
+
[
|
|
223
|
+
"a path that does not exist",
|
|
224
|
+
["--venue", "./venues/nope.jsonc"],
|
|
225
|
+
/no such file .* relative to where you run the command/,
|
|
226
|
+
],
|
|
227
|
+
])("%s → exit 2, nothing created", async (_, args, message) => {
|
|
228
|
+
const r = await newIn(args);
|
|
229
|
+
expect(r.err).toMatch(message);
|
|
230
|
+
expect(r.code).toBe(2);
|
|
231
|
+
expect(existsSync(r.dir)).toBe(false);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("a venue with kinds and no --kind: written, and new says what lint will report", async () => {
|
|
235
|
+
const r = await newIn(["--venue", "agenticdev"]);
|
|
236
|
+
expect(r.settings()).toEqual({ extends: "paperlint:agenticdev" });
|
|
237
|
+
expect(r.out).toMatch(
|
|
238
|
+
/kind: not set — `agenticdev` sets a page limit per kind/,
|
|
239
|
+
);
|
|
240
|
+
expect(r.out).toMatch(/pdf\/profile/);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("without a terminal and no --venue: as before, and the hint names --venue", async () => {
|
|
244
|
+
const r = await newIn([]);
|
|
245
|
+
expect(r.settings()["extends"]).toBeNull();
|
|
246
|
+
expect(r.out).toMatch(/venue: none yet .*--venue <preset>/);
|
|
247
|
+
expect(r.code).toBe(0);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("an existing paperlint.json is never overwritten: --venue is refused", async () => {
|
|
251
|
+
const r = await newIn(["--venue", "aisec"], {
|
|
252
|
+
"papers/demo/paperlint.json": '{ "extends": null }\n',
|
|
253
|
+
});
|
|
254
|
+
expect(r.code).toBe(2);
|
|
255
|
+
expect(r.err).toMatch(/already exists and is never overwritten/);
|
|
256
|
+
expect(r.settings()).toEqual({ extends: null });
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe("chooseVenue — on a terminal", () => {
|
|
261
|
+
const at = (answers: string[]) => {
|
|
262
|
+
const asked: string[] = [];
|
|
263
|
+
return {
|
|
264
|
+
asked,
|
|
265
|
+
opts: {
|
|
266
|
+
paperDir: "/p/papers/demo",
|
|
267
|
+
cwd: "/p",
|
|
268
|
+
interactive: true,
|
|
269
|
+
ask: async (q: string) => (asked.push(q), answers.shift() ?? ""),
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
it("asks the venue from the shipped list, then its kind", async () => {
|
|
275
|
+
const t = at(["agenticdev", "full"]);
|
|
276
|
+
const r = await chooseVenue({ venue: null, kind: null }, t.opts);
|
|
277
|
+
expect(r).toMatchObject({
|
|
278
|
+
ok: true,
|
|
279
|
+
value: { extends: "paperlint:agenticdev", kind: "full" },
|
|
280
|
+
});
|
|
281
|
+
expect(t.asked[0]).toContain(
|
|
282
|
+
[...shippedPresets(packageVenuesDir()), "none"].join(" / "),
|
|
283
|
+
);
|
|
284
|
+
expect(t.asked[1]).toMatch(/kind: short \/ full \/ demo \/ later/);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("the default answer is none: no venue written", async () => {
|
|
288
|
+
const t = at([""]);
|
|
289
|
+
expect(await chooseVenue({ venue: null, kind: null }, t.opts)).toEqual({
|
|
290
|
+
ok: true,
|
|
291
|
+
value: null,
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("without a terminal nothing is asked", async () => {
|
|
296
|
+
const t = at(["agenticdev"]);
|
|
297
|
+
const r = await chooseVenue(
|
|
298
|
+
{ venue: null, kind: null },
|
|
299
|
+
{ ...t.opts, interactive: false },
|
|
300
|
+
);
|
|
301
|
+
expect(r).toEqual({ ok: true, value: null });
|
|
302
|
+
expect(t.asked).toEqual([]);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe("a project's own template", () => {
|
|
307
|
+
it("🔴 a template paperlint.json with comments refuses --venue by name, and nothing is made", () => {
|
|
308
|
+
const papers = join(tmp(), "papers");
|
|
309
|
+
mkdirSync(join(papers, OVERRIDE_DIR), { recursive: true });
|
|
310
|
+
const src = join(papers, OVERRIDE_DIR, "paperlint.json");
|
|
311
|
+
writeFileSync(src, '{\n // our house venue\n "extends": null\n}\n');
|
|
312
|
+
const r = newPaper(papers, "p", "tex", {
|
|
313
|
+
venue: { extends: "paperlint:aisec", kind: null },
|
|
314
|
+
});
|
|
315
|
+
expect(r).toEqual({
|
|
316
|
+
ok: false,
|
|
317
|
+
reason: `--venue cannot be written into ${src}: it is not plain JSON — set "extends" by hand`,
|
|
318
|
+
});
|
|
319
|
+
expect(existsSync(join(papers, "p"))).toBe(false);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("the report names where each created file came from", () => {
|
|
323
|
+
const papers = join(tmp(), "papers");
|
|
324
|
+
mkdirSync(join(papers, OVERRIDE_DIR), { recursive: true });
|
|
325
|
+
writeFileSync(join(papers, OVERRIDE_DIR, "paperlint.json"), "{}\n");
|
|
326
|
+
const lines = reportNewPaper(newPaper(papers, "p", "tex"), (p) => p);
|
|
327
|
+
expect(lines).toContain(
|
|
328
|
+
" + paperlint.json (from the project's .template/ template)",
|
|
329
|
+
);
|
|
330
|
+
expect(
|
|
331
|
+
lines.filter((l) => l.endsWith("(from the package template)")).length,
|
|
332
|
+
).toBeGreaterThan(0);
|
|
333
|
+
});
|
|
334
|
+
});
|