paperlint 3.1.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/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.d.ts +10 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +7 -8
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +27 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +71 -55
- 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/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 +3 -3
- 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/paper-files.d.ts.map +1 -1
- package/dist/paper-files.js +6 -4
- 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/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 +3 -2
- package/dist/toolchain.d.ts.map +1 -1
- package/dist/toolchain.js +4 -5
- package/dist/toolchain.js.map +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +11 -9
- package/dist/venue-rules.js.map +1 -1
- package/docs/prior-art/README.md +1 -1
- package/docs/prior-art/test-tooling.md +7 -30
- 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 +9 -2
- package/eslint-rules/paper-typography.test.mjs +146 -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.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/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.harness.mjs +5 -9
- package/src/build.test.ts +362 -0
- package/src/build.ts +13 -9
- package/src/cli.harness.mjs +10 -17
- package/src/cli.test.ts +584 -0
- package/src/cli.ts +112 -66
- 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 +27 -7
- 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 +3 -3
- 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/new-paper.harness.mjs +3 -7
- package/src/new-paper.test.ts +31 -1
- package/src/paper-files.test.ts +12 -0
- package/src/paper-files.ts +9 -5
- package/src/paper-settings.test.ts +20 -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/structure.harness.mjs +3 -7
- package/src/tex-requirements.harness.mjs +34 -7
- package/src/tex-requirements.ts +12 -6
- package/src/toolchain.harness.mjs +3 -7
- package/src/toolchain.test.ts +280 -0
- package/src/toolchain.ts +8 -7
- package/src/venue-rules.test.ts +60 -0
- package/src/venue-rules.ts +13 -11
- 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 -65
- package/src/structure.mutations.mjs +0 -83
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* markdown.mjs — the markup readers every script shares, on the shapes their callers rely on:
|
|
3
|
+
* frontmatter blanked (offsets kept) or cut, headings from the parser (not from a `#` inside a
|
|
4
|
+
* fence), sections by level, fences, paragraphs and tables. And the one thing a test run never
|
|
5
|
+
* sees by accident: markdown-it failing to resolve, where every reader must degrade to its
|
|
6
|
+
* documented empty value and `requireMarkdown` must refuse with exit 2.
|
|
7
|
+
*/
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import { copyFileSync, mkdirSync } from "node:fs";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { test } from "vitest";
|
|
13
|
+
import { runNode, useTempDir, writeTree } from "../test/support.mjs";
|
|
14
|
+
import {
|
|
15
|
+
blankFrontmatter,
|
|
16
|
+
fences,
|
|
17
|
+
frontmatterBlock,
|
|
18
|
+
headings,
|
|
19
|
+
requireMarkdown,
|
|
20
|
+
paragraphs,
|
|
21
|
+
splitSections,
|
|
22
|
+
stripFences,
|
|
23
|
+
stripFrontmatter,
|
|
24
|
+
tables,
|
|
25
|
+
} from "./markdown.mjs";
|
|
26
|
+
|
|
27
|
+
const DOC = [
|
|
28
|
+
"---", // 0
|
|
29
|
+
"title: T", // 1
|
|
30
|
+
" ## not a heading, inside the frontmatter", // 2
|
|
31
|
+
"---", // 3
|
|
32
|
+
"# Title", // 4
|
|
33
|
+
"", // 5
|
|
34
|
+
"Intro para.", // 6
|
|
35
|
+
"", // 7
|
|
36
|
+
"## One", // 8
|
|
37
|
+
"Body one.", // 9
|
|
38
|
+
"", // 10
|
|
39
|
+
"---", // 11
|
|
40
|
+
"", // 12
|
|
41
|
+
"after the rule", // 13
|
|
42
|
+
"", // 14
|
|
43
|
+
"```", // 15
|
|
44
|
+
"## a heading in a fence", // 16
|
|
45
|
+
"```", // 17
|
|
46
|
+
"", // 18
|
|
47
|
+
" indented code", // 19
|
|
48
|
+
"", // 20
|
|
49
|
+
"### One.a", // 21
|
|
50
|
+
"| a | b |", // 22
|
|
51
|
+
"|---|:-:|", // 23
|
|
52
|
+
"| 1 | 2 |", // 24
|
|
53
|
+
"", // 25
|
|
54
|
+
"## Two", // 26
|
|
55
|
+
"last", // 27
|
|
56
|
+
].join("\n");
|
|
57
|
+
|
|
58
|
+
test("frontmatter: blanked with offsets kept, its body, cut; untouched when there is none", () => {
|
|
59
|
+
assert.deepEqual(
|
|
60
|
+
{
|
|
61
|
+
blank: blankFrontmatter("---\na: 1\n---\nx").split("\n"),
|
|
62
|
+
body: frontmatterBlock(DOC),
|
|
63
|
+
none: [
|
|
64
|
+
frontmatterBlock("x"),
|
|
65
|
+
blankFrontmatter("x"),
|
|
66
|
+
stripFrontmatter("x"),
|
|
67
|
+
],
|
|
68
|
+
cut: stripFrontmatter("---\na: 1\n---\n\nx"),
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
blank: [" ", " ", " ", "x"],
|
|
72
|
+
body: "title: T\n ## not a heading, inside the frontmatter",
|
|
73
|
+
none: [null, "x", "x"],
|
|
74
|
+
cut: "\nx",
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("headings come from the parser: not from the frontmatter, not from a fence, not setext", () => {
|
|
80
|
+
const offset = (line) => DOC.split("\n").slice(0, line).join("\n").length + 1;
|
|
81
|
+
assert.deepEqual(headings(DOC + "\n\nSetext\n======\n"), [
|
|
82
|
+
{ depth: 1, text: "Title", line: 4, offset: offset(4) },
|
|
83
|
+
{ depth: 2, text: "One", line: 8, offset: offset(8) },
|
|
84
|
+
{ depth: 3, text: "One.a", line: 21, offset: offset(21) },
|
|
85
|
+
{ depth: 2, text: "Two", line: 26, offset: offset(26) },
|
|
86
|
+
]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("an empty ATX heading has empty text", () => {
|
|
90
|
+
assert.deepEqual(
|
|
91
|
+
headings("#\n").map((h) => [h.depth, h.text]),
|
|
92
|
+
[[1, ""]],
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("splitSections: a preamble always first, then each heading of the level range", () => {
|
|
97
|
+
assert.deepEqual(
|
|
98
|
+
splitSections(DOC, { min: 2, max: 2 }).map((s) => [
|
|
99
|
+
s.heading?.text ?? null,
|
|
100
|
+
s.body.split("\n")[0],
|
|
101
|
+
]),
|
|
102
|
+
[
|
|
103
|
+
[null, "---"],
|
|
104
|
+
["One", "Body one."],
|
|
105
|
+
["Two", "last"],
|
|
106
|
+
],
|
|
107
|
+
);
|
|
108
|
+
assert.deepEqual(
|
|
109
|
+
splitSections("## A").map((s) => [s.heading?.text ?? null, s.body]),
|
|
110
|
+
[
|
|
111
|
+
[null, ""],
|
|
112
|
+
["A", ""],
|
|
113
|
+
],
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("fences and indented code, as content; stripped as lines, or blanked in place", () => {
|
|
118
|
+
const src = "a\n```\nx\n```\n\n y\nb";
|
|
119
|
+
assert.deepEqual(
|
|
120
|
+
{
|
|
121
|
+
fences: fences(src),
|
|
122
|
+
stripped: stripFences(src),
|
|
123
|
+
blanked: stripFences(src, { blank: true }),
|
|
124
|
+
},
|
|
125
|
+
{ fences: ["x\n", "y\n"], stripped: "a\n\nb", blanked: "a\n\n\n\n\n\nb" },
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("paragraphs are line ranges; tables are rows of cells, the alignment row skipped", () => {
|
|
130
|
+
assert.deepEqual(
|
|
131
|
+
{ paragraphs: paragraphs(DOC), tables: tables(DOC) },
|
|
132
|
+
{
|
|
133
|
+
paragraphs: [
|
|
134
|
+
{ line: 6, endLine: 7 },
|
|
135
|
+
{ line: 9, endLine: 10 },
|
|
136
|
+
{ line: 13, endLine: 14 },
|
|
137
|
+
{ line: 27, endLine: 28 },
|
|
138
|
+
],
|
|
139
|
+
tables: [
|
|
140
|
+
{
|
|
141
|
+
line: 22,
|
|
142
|
+
endLine: 25,
|
|
143
|
+
rows: [
|
|
144
|
+
["a", "b"],
|
|
145
|
+
["1", "2"],
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const root = useTempDir("markdown-no-parser-");
|
|
154
|
+
writeTree(root, {
|
|
155
|
+
// Registered with --import: resolving `markdown-it` fails, as it does with no node_modules.
|
|
156
|
+
"no-markdown-it.mjs":
|
|
157
|
+
"import { register } from 'node:module';\n" +
|
|
158
|
+
"register('data:text/javascript,' + encodeURIComponent(\"export async function resolve(s, c, next) { if (s === 'markdown-it') throw Object.assign(new Error('gone'), { code: 'ERR_MODULE_NOT_FOUND' }); return next(s, c); }\"));\n",
|
|
159
|
+
"probe.mjs":
|
|
160
|
+
`const m = await import(${JSON.stringify(join(dirname(fileURLToPath(import.meta.url)), "markdown.mjs"))});\n` +
|
|
161
|
+
"const t = '# H\\n\\n```\\nx\\n```\\n';\n" +
|
|
162
|
+
"console.log(JSON.stringify([m.MD_AVAILABLE(), m.headings(t), m.fences(t), m.stripFences(t), m.paragraphs(t), m.tables(t)]));\n" +
|
|
163
|
+
"m.requireMarkdown();\n" +
|
|
164
|
+
"console.log('not reached');\n",
|
|
165
|
+
"probe-plugin.mjs":
|
|
166
|
+
`const m = await import(${JSON.stringify(join(dirname(fileURLToPath(import.meta.url)), "markdown.mjs"))});\n` +
|
|
167
|
+
'm.requireMarkdown({ from: "file:///home/u/.claude/plugins/cache/paperlint/lib/markdown.mjs" });\n',
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("this file, told it runs from a plugin cache, sends its user to the npm package", () => {
|
|
171
|
+
const r = runNode(join(root, "probe-plugin.mjs"), [], {
|
|
172
|
+
nodeArgs: ["--import", join(root, "no-markdown-it.mjs")],
|
|
173
|
+
});
|
|
174
|
+
assert.deepEqual(
|
|
175
|
+
[r.status, r.stderr.split("\n").slice(1, 2)],
|
|
176
|
+
[
|
|
177
|
+
2,
|
|
178
|
+
[
|
|
179
|
+
" Running from a PLUGIN copy, which is installed without dependencies. Install the npm",
|
|
180
|
+
],
|
|
181
|
+
],
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("with no markdown-it, every reader degrades to its empty value and requireMarkdown exits 2", () => {
|
|
186
|
+
const r = runNode(join(root, "probe.mjs"), [], {
|
|
187
|
+
nodeArgs: ["--import", join(root, "no-markdown-it.mjs")],
|
|
188
|
+
});
|
|
189
|
+
assert.deepEqual(r, {
|
|
190
|
+
status: 2,
|
|
191
|
+
stdout:
|
|
192
|
+
JSON.stringify([false, [], [], "# H\n\n```\nx\n```\n", [], []]) + "\n",
|
|
193
|
+
stderr:
|
|
194
|
+
"markdown-it does not resolve — this script parses markup with a parser and without it would produce a confident zero instead of an error.\n" +
|
|
195
|
+
" Cured by `npm i` at the repo root.\n",
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("splitSections of a document with no heading is the preamble alone; requireMarkdown is silent with a parser", () => {
|
|
200
|
+
assert.deepEqual(
|
|
201
|
+
[
|
|
202
|
+
splitSections("just text").map((s) => [s.heading, s.body]),
|
|
203
|
+
requireMarkdown(),
|
|
204
|
+
],
|
|
205
|
+
[[[null, "just text"]], undefined],
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("headings read a token stream that breaks markdown-it's shape as empty text and offset 0", () => {
|
|
210
|
+
// A heading not followed by its inline token, mapped past the end of the text.
|
|
211
|
+
const parse = () => [
|
|
212
|
+
{ type: "heading_open", map: [9, 10], markup: "##", tag: "h2" },
|
|
213
|
+
{ type: "paragraph_open" },
|
|
214
|
+
];
|
|
215
|
+
assert.deepEqual(headings("x", { parse }), [
|
|
216
|
+
{ depth: 2, text: "", line: 9, offset: 0 },
|
|
217
|
+
]);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("a copy running from a plugin cache without markdown-it is told to install the npm package", () => {
|
|
221
|
+
// A plugin copy is installed without dependencies: here, a copy under `.claude/plugins/` with
|
|
222
|
+
// no node_modules above it, so `markdown-it` does not resolve for real.
|
|
223
|
+
const lib = join(root, ".claude", "plugins", "paperlint", "lib");
|
|
224
|
+
mkdirSync(lib, { recursive: true });
|
|
225
|
+
copyFileSync(
|
|
226
|
+
join(dirname(fileURLToPath(import.meta.url)), "markdown.mjs"),
|
|
227
|
+
join(lib, "markdown.mjs"),
|
|
228
|
+
);
|
|
229
|
+
writeTree(lib, {
|
|
230
|
+
"probe.mjs":
|
|
231
|
+
'const m = await import("./markdown.mjs");\nm.requireMarkdown();\n',
|
|
232
|
+
});
|
|
233
|
+
const r = runNode(join(lib, "probe.mjs"), []);
|
|
234
|
+
assert.deepEqual(
|
|
235
|
+
[r.status, r.stderr.split("\n").slice(1)],
|
|
236
|
+
[
|
|
237
|
+
2,
|
|
238
|
+
[
|
|
239
|
+
" Running from a PLUGIN copy, which is installed without dependencies. Install the npm",
|
|
240
|
+
" package in the project as well (`npm i paperlint`) and call it through",
|
|
241
|
+
" `npx paperlint`, which resolves from the project's own tree.",
|
|
242
|
+
"",
|
|
243
|
+
],
|
|
244
|
+
],
|
|
245
|
+
);
|
|
246
|
+
});
|
|
@@ -23,21 +23,16 @@
|
|
|
23
23
|
* while the value itself is still taken by import.
|
|
24
24
|
*
|
|
25
25
|
* Run: npx vigiles test lib/paper-config.harness.mjs
|
|
26
|
-
* Killed by: lib/paper-config.mutations.mjs
|
|
27
26
|
*/
|
|
28
|
-
import assert from "node:assert/strict";
|
|
29
27
|
import { readFileSync, readdirSync } from "node:fs";
|
|
30
28
|
import { dirname, join, relative, resolve } from "node:path";
|
|
31
29
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
30
|
+
import { createChecker } from "./check.mjs";
|
|
32
31
|
|
|
33
32
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
34
33
|
const ROOT = resolve(HERE, "..");
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
const check = (label, cond) => {
|
|
38
|
-
n++;
|
|
39
|
-
assert.ok(cond, label);
|
|
40
|
-
};
|
|
35
|
+
const check = createChecker();
|
|
41
36
|
|
|
42
37
|
/**
|
|
43
38
|
* Every place that publishes at least one of the two constants. The three hooks duplicate them
|
|
@@ -181,7 +176,7 @@ const loaded = await Promise.all(
|
|
|
181
176
|
if (e.isDirectory()) walk(p);
|
|
182
177
|
else if (
|
|
183
178
|
/\.(mjs|ts)$/.test(e.name) &&
|
|
184
|
-
!/\.
|
|
179
|
+
!/\.harness\.(mjs|ts)$/.test(e.name)
|
|
185
180
|
) {
|
|
186
181
|
if (declares.test(readFileSync(p, "utf8")))
|
|
187
182
|
found.push(relative(ROOT, p));
|
|
@@ -264,6 +259,30 @@ const loaded = await Promise.all(
|
|
|
264
259
|
);
|
|
265
260
|
}
|
|
266
261
|
|
|
262
|
+
// ── the two readers' fallbacks: no marker anywhere up the tree, and JSON that is not an object ──
|
|
263
|
+
{
|
|
264
|
+
const { findProjectRoot, settingsOf } = await import(
|
|
265
|
+
join(HERE, "paper-config.mjs")
|
|
266
|
+
);
|
|
267
|
+
check(
|
|
268
|
+
"findProjectRoot: no paperlint.json and no package.json up to / — the start directory itself",
|
|
269
|
+
findProjectRoot("/nowhere/deep", () => false) === "/nowhere/deep",
|
|
270
|
+
);
|
|
271
|
+
const { mkdtempSync, realpathSync, rmSync, writeFileSync } =
|
|
272
|
+
await import("node:fs");
|
|
273
|
+
const { tmpdir } = await import("node:os");
|
|
274
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "paper-config-")));
|
|
275
|
+
try {
|
|
276
|
+
writeFileSync(join(dir, "paperlint.json"), "[]\n");
|
|
277
|
+
check(
|
|
278
|
+
"settingsOf: a paperlint.json that is JSON but not an object is no settings",
|
|
279
|
+
settingsOf(dir) === undefined,
|
|
280
|
+
);
|
|
281
|
+
} finally {
|
|
282
|
+
rmSync(dir, { recursive: true, force: true });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
267
286
|
console.log(
|
|
268
|
-
`✓ ${
|
|
287
|
+
`✓ ${check.count} assertions passed — one source for the consumer's config key`,
|
|
269
288
|
);
|
package/lib/paper-config.mjs
CHANGED
|
@@ -64,11 +64,16 @@ export const PAPER_MARKERS = Object.freeze([
|
|
|
64
64
|
export function findProjectRoot(startDir, isFile = existsSync) {
|
|
65
65
|
const start = resolve(startDir);
|
|
66
66
|
const isPaper = (d) => PAPER_MARKERS.some((m) => isFile(join(d, m)));
|
|
67
|
+
// Up from `start` to the first directory `found` accepts; null past the filesystem root. A
|
|
68
|
+
// loop with an exit condition rather than `for (;;)`: the code after an endless loop is a block
|
|
69
|
+
// that can never run, and coverage counts it as one that did not.
|
|
67
70
|
const walk = (found) => {
|
|
68
|
-
|
|
69
|
-
|
|
71
|
+
let d = start;
|
|
72
|
+
while (!found(d)) {
|
|
70
73
|
if (dirname(d) === d) return null;
|
|
74
|
+
d = dirname(d);
|
|
71
75
|
}
|
|
76
|
+
return d;
|
|
72
77
|
};
|
|
73
78
|
return (
|
|
74
79
|
walk((d) => isFile(join(d, CONFIG_FILE)) && !isPaper(d)) ??
|
package/lib/skill-checks.mjs
CHANGED
|
@@ -137,8 +137,8 @@ export async function checkSkill(name) {
|
|
|
137
137
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
138
138
|
// 1. GONE, AND THAT IS THE RESULT (2026-08-19). This slot held a strict-YAML assertion whose whole
|
|
139
139
|
// job was to police the gap between how the corpus was READ here (a hand-rolled lenient reader)
|
|
140
|
-
// and how every other consumer reads it (a real YAML parser). The corpus owner, on being shown the
|
|
141
|
-
//
|
|
140
|
+
// and how every other consumer reads it (a real YAML parser). The corpus owner, on being shown the gap:
|
|
141
|
+
// "sounds like a hack… let's think about how to do it properly". The right fix was not a better
|
|
142
142
|
// check — it was to stop being lenient: the two remaining unparseable descriptions were quoted and
|
|
143
143
|
// `parseFm` in skill-corpus.mjs now throws on anything a real parser rejects.
|
|
144
144
|
//
|
|
@@ -213,7 +213,12 @@ export async function checkSkill(name) {
|
|
|
213
213
|
["announce", PIPELINE.announce, "Run me"],
|
|
214
214
|
["record", PIPELINE.ledger, "Record the verdict"],
|
|
215
215
|
]) {
|
|
216
|
-
|
|
216
|
+
// For the ledger, only a `record` call is the wired call: `ledger.mjs status <dir>` is a
|
|
217
|
+
// read, files under nothing, and until 2026-09-27 was misreported as "files under status".
|
|
218
|
+
const hits = cmds.filter(
|
|
219
|
+
(c) =>
|
|
220
|
+
c.script === script && (label !== "record" || c.args[0] === "record"),
|
|
221
|
+
);
|
|
217
222
|
soft(() =>
|
|
218
223
|
assert.ok(
|
|
219
224
|
hits.length > 0,
|
|
@@ -231,12 +236,7 @@ export async function checkSkill(name) {
|
|
|
231
236
|
),
|
|
232
237
|
);
|
|
233
238
|
// arg[0] for announce is the skill; for ledger.mjs it is the literal `record`, then the skill.
|
|
234
|
-
const passed =
|
|
235
|
-
label === "announce"
|
|
236
|
-
? h.args[0]
|
|
237
|
-
: h.args[0] === "record"
|
|
238
|
-
? h.args[1]
|
|
239
|
-
: h.args[0];
|
|
239
|
+
const passed = label === "announce" ? h.args[0] : h.args[1];
|
|
240
240
|
soft(() =>
|
|
241
241
|
assert.equal(
|
|
242
242
|
passed,
|
|
@@ -551,10 +551,7 @@ function danglingLinks(src, dir) {
|
|
|
551
551
|
* Check the prose skill `name`.
|
|
552
552
|
*
|
|
553
553
|
* @param {string} name — the directory name in `.claude/skills/`
|
|
554
|
-
* @param {{
|
|
555
|
-
* toolsNeeded?: Record<string,string>}} opts
|
|
556
|
-
* `mustSay` — that whose DISAPPEARANCE is a regression (a rule obtained by measurement).
|
|
557
|
-
* `mustNotSay` — that whose APPEARANCE is a regression (a refuted claim).
|
|
554
|
+
* @param {{toolsNeeded?: Record<string,string>}} opts
|
|
558
555
|
* `toolsNeeded` — {tool: REASON}. Lifts the ban on Bash/Agent/Task for a skill that
|
|
559
556
|
* needs the tool IN SUBSTANCE. The reason is mandatory and non-empty.
|
|
560
557
|
*
|
|
@@ -583,6 +580,14 @@ export async function checkProseSkill(name, opts = {}) {
|
|
|
583
580
|
}
|
|
584
581
|
};
|
|
585
582
|
|
|
583
|
+
// Any other option is refused by name: an option this function no longer reads would be a
|
|
584
|
+
// check the caller believes runs and that does nothing.
|
|
585
|
+
const unknown = Object.keys(opts).filter((k) => k !== "toolsNeeded");
|
|
586
|
+
assert.deepEqual(
|
|
587
|
+
unknown,
|
|
588
|
+
[],
|
|
589
|
+
`${name}: unknown option(s) ${unknown.join(", ")} — checkProseSkill takes only toolsNeeded`,
|
|
590
|
+
);
|
|
586
591
|
assert.ok(existsSync(md), `${name}: no ${md} — nothing to check`);
|
|
587
592
|
const src = readFileSync(md, "utf8");
|
|
588
593
|
|
|
@@ -650,12 +655,6 @@ export async function checkProseSkill(name, opts = {}) {
|
|
|
650
655
|
`is a dead instruction: the model reads it, does something adjacent and reports success.`,
|
|
651
656
|
),
|
|
652
657
|
);
|
|
653
|
-
// mutation: a planted broken link must be caught
|
|
654
|
-
assert.equal(
|
|
655
|
-
danglingLinks(src + "\n[x](./net-takogo-fayla.md)\n", dir).length,
|
|
656
|
-
dangling.length + 1,
|
|
657
|
-
`${name}: the link check did not fire on a planted broken link — it is dead`,
|
|
658
|
-
);
|
|
659
658
|
|
|
660
659
|
// 3. Whether it is compiled. Editing the markdown by hand desynchronises the sha and gets
|
|
661
660
|
// quietly rolled back by the next `vigiles compile`.
|
|
@@ -667,33 +666,6 @@ export async function checkProseSkill(name, opts = {}) {
|
|
|
667
666
|
),
|
|
668
667
|
);
|
|
669
668
|
|
|
670
|
-
// 4-5. Substantive anchors, each with BOTH halves.
|
|
671
|
-
for (const [re, why] of opts.mustSay || []) {
|
|
672
|
-
soft(() => assert.ok(re.test(src), `${name}: "${why}" is gone (${re})`));
|
|
673
|
-
// Mutation: without the matched fragment the predicate must fail. The replacement is GLOBAL —
|
|
674
|
-
// `replace` with a non-global regex removes only the first occurrence, and an anchor occurring
|
|
675
|
-
// twice (for example both in the description and in the body) would "pass" on the mutated text
|
|
676
|
-
// too. Then this assertion would fail rather than the anchor itself — that is, the message
|
|
677
|
-
// would point in the wrong direction.
|
|
678
|
-
const g = new RegExp(
|
|
679
|
-
re.source,
|
|
680
|
-
re.flags.includes("g") ? re.flags : re.flags + "g",
|
|
681
|
-
);
|
|
682
|
-
assert.equal(
|
|
683
|
-
re.test(src.replace(g, "")),
|
|
684
|
-
false,
|
|
685
|
-
`${name}: the anchor ${re} passes even on text WITHOUT it — the check pins nothing down`,
|
|
686
|
-
);
|
|
687
|
-
}
|
|
688
|
-
for (const [re, why] of opts.mustNotSay || []) {
|
|
689
|
-
soft(() => assert.ok(!re.test(src), `${name}: "${why}" came back (${re})`));
|
|
690
|
-
assert.equal(
|
|
691
|
-
re.test(src + "\n" + why + "\n"),
|
|
692
|
-
true,
|
|
693
|
-
`${name}: the ban ${re} does not fire on the planted line "${why}" — the check is dead`,
|
|
694
|
-
);
|
|
695
|
-
}
|
|
696
|
-
|
|
697
669
|
if (deferred.length)
|
|
698
670
|
throw new AggregateError(deferred, `${name}: ${deferred.length} findings`);
|
|
699
671
|
recordCheck();
|