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,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* skill-checks.mjs's two refusing halves, which the per-skill harnesses (all green on the real
|
|
3
|
+
* corpus) never reach: `checkSkill` on an unknown name and on a pipeline skill with a planted
|
|
4
|
+
* defect, and `checkProseSkill` — the check for skills WITHOUT the pipeline, which the package
|
|
5
|
+
* exports for a consumer's own skills and no harness here calls.
|
|
6
|
+
*
|
|
7
|
+
* The consumer is a temp root holding a COPY of the shipped skills, so a planted defect edits the
|
|
8
|
+
* copy, never the tracked files. It is set before import: the corpus resolves it at load time.
|
|
9
|
+
*/
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { cpSync, readFileSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { test } from "vitest";
|
|
15
|
+
import { useTempDir, writeTree } from "../test/support.mjs";
|
|
16
|
+
|
|
17
|
+
const SHIPPED = join(dirname(fileURLToPath(import.meta.url)), "..", "skills");
|
|
18
|
+
const consumer = useTempDir("skill-checks-");
|
|
19
|
+
const SKILLS = join(consumer, ".claude", "skills");
|
|
20
|
+
cpSync(SHIPPED, SKILLS, { recursive: true, verbatimSymlinks: true });
|
|
21
|
+
process.env.CLAUDE_PROJECT_DIR = consumer;
|
|
22
|
+
const { checkProseSkill, checkSkill } = await import("./skill-checks.mjs");
|
|
23
|
+
|
|
24
|
+
/** The message of what `fn` throws — an AggregateError's messages joined, one per line. */
|
|
25
|
+
async function thrown(fn) {
|
|
26
|
+
try {
|
|
27
|
+
await fn();
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return e instanceof AggregateError
|
|
30
|
+
? [e.message, ...e.errors.map((x) => x.message.split("\n")[0])]
|
|
31
|
+
: e.message.split("\n")[0];
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test("checkSkill passes a real shipped skill and refuses a name that is not a wired skill", async () => {
|
|
37
|
+
assert.deepEqual(
|
|
38
|
+
[
|
|
39
|
+
await checkSkill("cold-read-diff"),
|
|
40
|
+
await thrown(() => checkSkill("no-such-skill")),
|
|
41
|
+
],
|
|
42
|
+
[
|
|
43
|
+
1,
|
|
44
|
+
`"no-such-skill" is not a wired pipeline skill. Known: ${(await import("./skill-corpus.mjs")).SKILLS.join(", ")}`,
|
|
45
|
+
],
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("checkSkill reports EVERY finding of a broken pipeline skill at once", async () => {
|
|
50
|
+
const md = join(SKILLS, "cold-read-diff", "SKILL.md");
|
|
51
|
+
const clean = readFileSync(md, "utf8");
|
|
52
|
+
writeFileSync(
|
|
53
|
+
md,
|
|
54
|
+
clean
|
|
55
|
+
.replace("name: cold-read-diff", "name: renamed")
|
|
56
|
+
.replace("## Run me", "## Not run"),
|
|
57
|
+
);
|
|
58
|
+
try {
|
|
59
|
+
const first = await thrown(() => checkSkill("cold-read-diff"));
|
|
60
|
+
assert.equal(
|
|
61
|
+
first,
|
|
62
|
+
'2 assertion(s) failed for pipeline skill "cold-read-diff":',
|
|
63
|
+
);
|
|
64
|
+
} finally {
|
|
65
|
+
writeFileSync(md, clean);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("a pipeline skill whose record block is gone: ONE finding naming the heading — the ledger checks that read the block skip it", async () => {
|
|
70
|
+
const md = join(SKILLS, "cold-read-diff", "SKILL.md");
|
|
71
|
+
const clean = readFileSync(md, "utf8");
|
|
72
|
+
writeFileSync(md, clean.replaceAll("## Record the verdict", "## Afterwards"));
|
|
73
|
+
let message = "";
|
|
74
|
+
try {
|
|
75
|
+
await checkSkill("cold-read-diff");
|
|
76
|
+
} catch (e) {
|
|
77
|
+
message = e.message;
|
|
78
|
+
} finally {
|
|
79
|
+
writeFileSync(md, clean);
|
|
80
|
+
}
|
|
81
|
+
assert.equal(
|
|
82
|
+
message,
|
|
83
|
+
'1 assertion(s) failed for pipeline skill "cold-read-diff":\n\n' +
|
|
84
|
+
' [1] cold-read-diff: no "Record the verdict" heading. A run with an announce row and no verdict row is indistinguishable from a skill that started and died.',
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const PROSE = (extra = "") =>
|
|
89
|
+
[
|
|
90
|
+
"---",
|
|
91
|
+
"name: prose",
|
|
92
|
+
"description: a skill with no pipeline",
|
|
93
|
+
"allowed-tools: [Read, Write]",
|
|
94
|
+
"---",
|
|
95
|
+
"",
|
|
96
|
+
"<!-- vigiles:sha256:0000000000000000 compiled from SKILL.md.spec.ts -->",
|
|
97
|
+
"",
|
|
98
|
+
"Measured rule: always say which file. See [the notes](./notes.md) and [the web](https://x.org).",
|
|
99
|
+
extra,
|
|
100
|
+
"",
|
|
101
|
+
].join("\n");
|
|
102
|
+
writeTree(SKILLS, { "prose/SKILL.md": PROSE(), "prose/notes.md": "notes\n" });
|
|
103
|
+
|
|
104
|
+
test("checkProseSkill passes a clean prose skill", async () => {
|
|
105
|
+
assert.equal(await checkProseSkill("prose"), 1);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("checkProseSkill refuses an option it does not read, instead of silently skipping it", async () => {
|
|
109
|
+
assert.deepEqual(
|
|
110
|
+
await thrown(() =>
|
|
111
|
+
checkProseSkill("prose", { mustSay: [[/x/, "x"]], toolsNeeded: {} }),
|
|
112
|
+
),
|
|
113
|
+
"prose: unknown option(s) mustSay — checkProseSkill takes only toolsNeeded",
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("checkProseSkill collects every finding: a banned tool, a dangling link, no compile mark", async () => {
|
|
118
|
+
writeTree(SKILLS, {
|
|
119
|
+
"broken/SKILL.md": [
|
|
120
|
+
"---",
|
|
121
|
+
"name: broken",
|
|
122
|
+
"description: d",
|
|
123
|
+
"allowed-tools: [Read, Bash]",
|
|
124
|
+
"---",
|
|
125
|
+
"See [gone](./gone.md). never mind the file.",
|
|
126
|
+
"",
|
|
127
|
+
].join("\n"),
|
|
128
|
+
});
|
|
129
|
+
assert.deepEqual(await thrown(() => checkProseSkill("broken")), [
|
|
130
|
+
"broken: 3 findings",
|
|
131
|
+
"broken: Bash appeared in the contract. It was dropped deliberately; bringing it back restores a leg of the lethal trifecta (data + network + execution) on a skill that only writes text. If the tool IS needed IN SUBSTANCE — declare it in toolsNeeded WITH A REASON, not silently.",
|
|
132
|
+
"broken: dangling links → ./gone.md. A skill naming a path that does not exist is a dead instruction: the model reads it, does something adjacent and reports success.",
|
|
133
|
+
"broken: no compilation mark. That means SKILL.md was edited by hand rather than through .spec.ts — the next `vigiles compile` will overwrite the edit without warning.",
|
|
134
|
+
]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("checkProseSkill: a tool exemption needs a reason, and an exemption for a tool not in the contract is stale", async () => {
|
|
138
|
+
writeTree(SKILLS, {
|
|
139
|
+
"needs-bash/notes.md": "notes\n",
|
|
140
|
+
"needs-bash/SKILL.md": PROSE()
|
|
141
|
+
.replace("name: prose", "name: needs-bash")
|
|
142
|
+
.replace("[Read, Write]", "[Read, Bash]"),
|
|
143
|
+
});
|
|
144
|
+
assert.deepEqual(
|
|
145
|
+
[
|
|
146
|
+
await thrown(() =>
|
|
147
|
+
checkProseSkill("needs-bash", { toolsNeeded: { Bash: "short" } }),
|
|
148
|
+
),
|
|
149
|
+
await checkProseSkill("needs-bash", {
|
|
150
|
+
toolsNeeded: { Bash: "runs the measurement script it documents" },
|
|
151
|
+
}),
|
|
152
|
+
await thrown(() =>
|
|
153
|
+
checkProseSkill("prose", {
|
|
154
|
+
toolsNeeded: { Agent: "delegates the heavy read to a subagent" },
|
|
155
|
+
}),
|
|
156
|
+
),
|
|
157
|
+
],
|
|
158
|
+
[
|
|
159
|
+
"needs-bash: toolsNeeded.Bash is declared without an intelligible reason. An exemption must say WHY the tool is needed in substance — otherwise it is a silent disabling of the check.",
|
|
160
|
+
1,
|
|
161
|
+
[
|
|
162
|
+
"prose: 1 findings",
|
|
163
|
+
"prose: toolsNeeded.Agent is declared, but Agent itself is NOT in the contract. The exemption is stale — remove it, otherwise it describes a different skill.",
|
|
164
|
+
],
|
|
165
|
+
],
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("checkProseSkill refuses a skill with no SKILL.md, an empty contract, and one that does not parse", async () => {
|
|
170
|
+
writeTree(SKILLS, {
|
|
171
|
+
"empty-tools/notes.md": "notes\n",
|
|
172
|
+
"bad-yaml/notes.md": "notes\n",
|
|
173
|
+
"empty-tools/SKILL.md": PROSE()
|
|
174
|
+
.replace("name: prose", "name: empty-tools")
|
|
175
|
+
.replace("allowed-tools: [Read, Write]", "allowed-tools: []"),
|
|
176
|
+
"bad-yaml/SKILL.md": PROSE()
|
|
177
|
+
.replace("name: prose", "name: bad-yaml")
|
|
178
|
+
.replace("allowed-tools: [Read, Write]", "allowed-tools: [Read, Write"),
|
|
179
|
+
});
|
|
180
|
+
assert.deepEqual(
|
|
181
|
+
[
|
|
182
|
+
await thrown(() => checkProseSkill("absent")),
|
|
183
|
+
await thrown(() => checkProseSkill("empty-tools")),
|
|
184
|
+
(await thrown(() => checkProseSkill("bad-yaml")))[0],
|
|
185
|
+
],
|
|
186
|
+
[
|
|
187
|
+
`absent: no ${join(SKILLS, "absent", "SKILL.md")} — nothing to check`,
|
|
188
|
+
[
|
|
189
|
+
"empty-tools: 1 findings",
|
|
190
|
+
"empty-tools: empty allowed-tools — inherits everything",
|
|
191
|
+
],
|
|
192
|
+
"bad-yaml: 2 findings",
|
|
193
|
+
],
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
/** Rewrite a copied skill's SKILL.md with `edit`, run checkSkill, restore; the failure's first line. */
|
|
198
|
+
async function brokenBy(name, edit) {
|
|
199
|
+
const md = join(SKILLS, name, "SKILL.md");
|
|
200
|
+
const clean = readFileSync(md, "utf8");
|
|
201
|
+
writeFileSync(md, edit(clean));
|
|
202
|
+
try {
|
|
203
|
+
await checkSkill(name);
|
|
204
|
+
return null;
|
|
205
|
+
} catch (e) {
|
|
206
|
+
// The header, then the first 90 characters of each numbered finding.
|
|
207
|
+
const lines = e.message.split("\n");
|
|
208
|
+
return [
|
|
209
|
+
lines[0],
|
|
210
|
+
...lines
|
|
211
|
+
.filter((l) => /^ \[\d+\] /.test(l))
|
|
212
|
+
.map((l) => l.trim().slice(0, 90)),
|
|
213
|
+
];
|
|
214
|
+
} finally {
|
|
215
|
+
writeFileSync(md, clean);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const recordSection = /## Record the verdict[\s\S]*?(?=\n## |\n# |$)/;
|
|
219
|
+
|
|
220
|
+
test("checkSkill on the defects its own corpus never has: no description, no record block, a record block naming no kind", async () => {
|
|
221
|
+
assert.deepEqual(
|
|
222
|
+
[
|
|
223
|
+
await brokenBy("cold-read-diff", (s) =>
|
|
224
|
+
s.replace(/^description: .*\n/m, ""),
|
|
225
|
+
),
|
|
226
|
+
await brokenBy("argument-arc", (s) =>
|
|
227
|
+
s.replace(
|
|
228
|
+
recordSection,
|
|
229
|
+
"## Record the verdict\n\nNothing named here.\n",
|
|
230
|
+
),
|
|
231
|
+
),
|
|
232
|
+
await brokenBy("draft-paper", (s) =>
|
|
233
|
+
s.replace(
|
|
234
|
+
recordSection,
|
|
235
|
+
"## Record the verdict\n\n**ABSTAINED** — and no reason is named.\n",
|
|
236
|
+
),
|
|
237
|
+
),
|
|
238
|
+
],
|
|
239
|
+
[
|
|
240
|
+
[
|
|
241
|
+
'1 assertion(s) failed for pipeline skill "cold-read-diff":',
|
|
242
|
+
"[1] cold-read-diff: description is missing or trivially short (0 chars). The description i",
|
|
243
|
+
],
|
|
244
|
+
[
|
|
245
|
+
'3 assertion(s) failed for pipeline skill "argument-arc":',
|
|
246
|
+
'[1] argument-arc: the "Record the verdict" block instructs no `node .claude/skills/paper-p',
|
|
247
|
+
"[2] argument-arc: its record block names [nothing] and not FINDING, and does not admit tha",
|
|
248
|
+
"[3] argument-arc: its record block names no abstention reason from [started, no-witness, i",
|
|
249
|
+
],
|
|
250
|
+
[
|
|
251
|
+
'5 assertion(s) failed for pipeline skill "draft-paper":',
|
|
252
|
+
'[1] draft-paper: the "Record the verdict" block instructs no `node .claude/skills/paper-pi',
|
|
253
|
+
"[2] draft-paper: its record block names [ABSTAINED] and not FINDING, and does not admit th",
|
|
254
|
+
"[3] draft-paper: its record block names no abstention reason from [started, no-witness, in",
|
|
255
|
+
"[4] draft-paper: the honest admission is gone from its record block. It is the one skill i",
|
|
256
|
+
"[5] ABSTAINED needs a reason from started|no-witness|input-missing|blocked|crashed, got nu",
|
|
257
|
+
],
|
|
258
|
+
],
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("a skill may show `ledger.mjs status` — only a `record` call must file under the skill", async () => {
|
|
263
|
+
assert.equal(
|
|
264
|
+
await brokenBy("cold-read-diff", (s) =>
|
|
265
|
+
s.replace(
|
|
266
|
+
"node .claude/skills/paper-pipeline/scripts/announce.mjs cold-read-diff <paper-dir>",
|
|
267
|
+
"node .claude/skills/paper-pipeline/scripts/announce.mjs cold-read-diff <paper-dir>\n" +
|
|
268
|
+
"node .claude/skills/paper-pipeline/scripts/ledger.mjs status <paper-dir>",
|
|
269
|
+
),
|
|
270
|
+
),
|
|
271
|
+
null,
|
|
272
|
+
);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("a skill whose only ledger call is `status` records nothing, and is told so", async () => {
|
|
276
|
+
assert.deepEqual(
|
|
277
|
+
await brokenBy("cold-read-diff", (s) =>
|
|
278
|
+
s.replaceAll(
|
|
279
|
+
/ledger\.mjs record cold-read-diff <paper-dir> \w+[^\n]*/g,
|
|
280
|
+
"ledger.mjs status <paper-dir>",
|
|
281
|
+
),
|
|
282
|
+
),
|
|
283
|
+
[
|
|
284
|
+
'1 assertion(s) failed for pipeline skill "cold-read-diff":',
|
|
285
|
+
'[1] cold-read-diff: the "Record the verdict" block instructs no `node .claude/skills/paper',
|
|
286
|
+
],
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("the record CALL is the contract, not the heading: a renamed record section still passes", async () => {
|
|
291
|
+
assert.equal(
|
|
292
|
+
await brokenBy("camera-ready", (s) =>
|
|
293
|
+
s.replace("## Record the verdict", "## Something else"),
|
|
294
|
+
),
|
|
295
|
+
null,
|
|
296
|
+
);
|
|
297
|
+
});
|
package/lib/skill-corpus.mjs
CHANGED
|
@@ -86,9 +86,9 @@ export const GATE_SKILLS = new Set(
|
|
|
86
86
|
// command and is in neither list. That is DELIBERATE (see EXCLUDED), but nothing checked that the
|
|
87
87
|
// deliberateness was still true, and the old comment asserting it was the only thing holding it.
|
|
88
88
|
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
89
|
+
// A defect that removes a skill from EXPECTED_GATES while leaving it in the pinned array passed
|
|
90
|
+
// silently the whole time. A check proving a narrower property than its own error message claims
|
|
91
|
+
// is exactly the vacuity this file was built to hunt, sitting in this file.
|
|
92
92
|
//
|
|
93
93
|
// The old comment's objection to globbing was real and is answered rather than ignored: `.claude/skills/`
|
|
94
94
|
// does hold daily skills (search-krisha, food-log-entry, …) with no pipeline contract, so the filter
|
|
@@ -127,14 +127,15 @@ export const EXCLUDED = new Map([
|
|
|
127
127
|
// defects this file hunts, and a defect that shrinks the checked set is worse than the defect itself:
|
|
128
128
|
// the harness goes green by no longer looking.
|
|
129
129
|
//
|
|
130
|
-
// Both narrower versions were written and both
|
|
130
|
+
// Both narrower versions were written and both failed within minutes, on the same two planted
|
|
131
|
+
// defects:
|
|
131
132
|
//
|
|
132
|
-
// filter `announce.mjs <own name>` ->
|
|
133
|
+
// filter `announce.mjs <own name>` -> a typo in the announced NAME dropped harden-paper out
|
|
133
134
|
// of scope; harness printed "20 pipeline skills", said nothing
|
|
134
|
-
// filter `announce.mjs` path ->
|
|
135
|
+
// filter `announce.mjs` path -> a typo in the announce PATH dropped camera-ready the
|
|
135
136
|
// same way
|
|
136
137
|
//
|
|
137
|
-
// Each time, the
|
|
138
|
+
// Each time, the same planted defect that used to turn the harness red went GREEN because it
|
|
138
139
|
// erased its own subject. Scope decided by a property the defect corrupts is not scope.
|
|
139
140
|
//
|
|
140
141
|
// The union cannot be shrunk by a single-point edit: break the wiring and the gate table still holds
|
|
@@ -395,9 +396,6 @@ export const load = (name) => {
|
|
|
395
396
|
// the header of `skill-checks.mjs`). The real, declared carrier of this check is `checkSkill()`,
|
|
396
397
|
// which does it one skill at a time and with a legible message.
|
|
397
398
|
//
|
|
398
|
-
// `
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
throw new Error(
|
|
402
|
-
`PIPELINE_SKILL_ONLY="${ONLY}" is not a wired pipeline skill. Known: ${SKILLS.join(", ")}`,
|
|
403
|
-
);
|
|
399
|
+
// `PIPELINE_SKILL_ONLY` used to stay here, validated against SKILLS. Nothing read the variable
|
|
400
|
+
// any more — the validation was all that was left of the feature — so it was removed with it
|
|
401
|
+
// (2026-09-27).
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* skill-corpus.mjs's pure readers on the inputs the shipped corpus never contains: frontmatter
|
|
3
|
+
* that is not a mapping or not YAML, an `allowed-tools` that is absent, a skill with no record
|
|
4
|
+
* block and one whose record block ends the file.
|
|
5
|
+
*/
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { test } from "vitest";
|
|
8
|
+
import { parseFm, recordBlock, toolList } from "./skill-corpus.mjs";
|
|
9
|
+
|
|
10
|
+
const refusal = (fn) => {
|
|
11
|
+
try {
|
|
12
|
+
fn();
|
|
13
|
+
return null;
|
|
14
|
+
} catch (e) {
|
|
15
|
+
return e.message.split(". Almost")[0];
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
test("parseFm refuses a list, a bare string and broken YAML, naming where", () => {
|
|
20
|
+
assert.deepEqual(
|
|
21
|
+
[
|
|
22
|
+
refusal(() => parseFm("- a\n- b", "x/SKILL.md")),
|
|
23
|
+
refusal(() => parseFm("just words")),
|
|
24
|
+
refusal(() => parseFm("a: b: c")),
|
|
25
|
+
],
|
|
26
|
+
[
|
|
27
|
+
"x/SKILL.md: frontmatter is not valid YAML — parsed to a list, not a mapping",
|
|
28
|
+
"frontmatter: frontmatter is not valid YAML — parsed to string, not a mapping",
|
|
29
|
+
"frontmatter: frontmatter is not valid YAML — bad indentation of a mapping entry (1:5)",
|
|
30
|
+
],
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("toolList of nothing is an empty contract, and a flow list or a comma scalar is split", () => {
|
|
35
|
+
assert.deepEqual(
|
|
36
|
+
[
|
|
37
|
+
toolList(undefined),
|
|
38
|
+
toolList("[Bash, Read]"),
|
|
39
|
+
toolList(" Read , Write "),
|
|
40
|
+
toolList(["Read ", ""]),
|
|
41
|
+
],
|
|
42
|
+
[[], ["Bash", "Read"], ["Read", "Write"], ["Read"]],
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("recordBlock: none without the heading; to the end of the file when it is the last section", () => {
|
|
47
|
+
assert.deepEqual(
|
|
48
|
+
[
|
|
49
|
+
recordBlock("# S\n\n## Run me\n\nx\n"),
|
|
50
|
+
recordBlock("# S\n\n## Record the verdict\n\nlast\n"),
|
|
51
|
+
],
|
|
52
|
+
[null, "## Record the verdict\n\nlast\n"],
|
|
53
|
+
);
|
|
54
|
+
});
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
* be holding them itself".
|
|
15
15
|
*
|
|
16
16
|
* Run: `npx vigiles test lib/skill-trigger-cases.harness.mjs`
|
|
17
|
-
* Killed by: `lib/skill-trigger-cases.mutations.mjs`
|
|
18
17
|
*/
|
|
19
18
|
import assert from "node:assert/strict";
|
|
20
19
|
import { execFileSync } from "node:child_process";
|
|
@@ -41,10 +40,10 @@ const soft = (label, fn) => {
|
|
|
41
40
|
* (top-level await) — a second import in this process would be served from the module cache and
|
|
42
41
|
* would measure the first root again while appearing to measure the second.
|
|
43
42
|
*/
|
|
44
|
-
function loadAt(root) {
|
|
43
|
+
function loadAt(root, then = `console.log("OK " + m.CASES.length)`) {
|
|
45
44
|
const script =
|
|
46
45
|
`import("${join(HERE, "skill-trigger-cases.mjs").replaceAll("\\", "/")}")` +
|
|
47
|
-
`.then((m) =>
|
|
46
|
+
`.then((m) => { ${then}; })` +
|
|
48
47
|
`.catch((e) => { console.log("THROW " + String(e.message).split("\\n")[0]); });`;
|
|
49
48
|
return execFileSync(process.execPath, ["--input-type=module", "-e", script], {
|
|
50
49
|
cwd: root,
|
|
@@ -160,12 +159,66 @@ soft("II. a consumer may not redefine a case this package owns", () => {
|
|
|
160
159
|
);
|
|
161
160
|
});
|
|
162
161
|
|
|
162
|
+
soft(
|
|
163
|
+
"II. a declaration that is not a non-empty string THROWS, naming what it got",
|
|
164
|
+
() => {
|
|
165
|
+
for (const [declared, shown] of [
|
|
166
|
+
["", '""'],
|
|
167
|
+
[7, "7"],
|
|
168
|
+
]) {
|
|
169
|
+
const out = loadAt(fixture({ triggerCases: declared }, null));
|
|
170
|
+
assert.equal(
|
|
171
|
+
out,
|
|
172
|
+
`THROW paperlint: "triggerCases" must be a non-empty string, got ${shown}`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
soft(
|
|
179
|
+
"II. a default-location file that does not load THROWS, saying it is the default",
|
|
180
|
+
() => {
|
|
181
|
+
for (const broken of [
|
|
182
|
+
"export const CASES = [\n",
|
|
183
|
+
'throw "not an Error";\n',
|
|
184
|
+
]) {
|
|
185
|
+
const out = loadAt(fixture(null, broken));
|
|
186
|
+
assert.match(out, /^THROW .*could not load the consumer's trigger cases/);
|
|
187
|
+
assert.match(
|
|
188
|
+
out,
|
|
189
|
+
/\(the default location; declare another in paperlint\.json/,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
soft("II. a cases file whose CASES is not an array THROWS", () => {
|
|
196
|
+
const out = loadAt(fixture(null, "export const CASES = {};\n"));
|
|
197
|
+
assert.match(out, /^THROW .* must export an array named CASES$/);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
soft(
|
|
201
|
+
"II. irrelevantFor refuses an unknown skill, and a collider that names one",
|
|
202
|
+
() => {
|
|
203
|
+
assert.throws(
|
|
204
|
+
() => irrelevantFor("no-such-skill"),
|
|
205
|
+
/^Error: unknown skill: no-such-skill$/,
|
|
206
|
+
);
|
|
207
|
+
const ghost = `export const CASES = [{ skill: "local-x", why: "x", prompts: ["a","b","c","d"], collides: ["ghost"] }];\n`;
|
|
208
|
+
const out = loadAt(
|
|
209
|
+
fixture(null, ghost),
|
|
210
|
+
`m.irrelevantFor("local-x"); console.log("OK")`,
|
|
211
|
+
);
|
|
212
|
+
assert.equal(out, 'THROW local-x collides with unknown skill "ghost"');
|
|
213
|
+
},
|
|
214
|
+
);
|
|
215
|
+
|
|
163
216
|
if (failures.length) {
|
|
164
217
|
console.error(`${failures.length} assertion(s) failed:`);
|
|
165
218
|
for (const f of failures) console.error(` ✗ ${f}`);
|
|
166
219
|
process.exit(1);
|
|
167
220
|
}
|
|
168
221
|
console.log(
|
|
169
|
-
`trigger-case carrier: ${CASES.length} package case(s), consumer merge in
|
|
222
|
+
`trigger-case carrier: ${CASES.length} package case(s), consumer merge in every state ✓`,
|
|
170
223
|
);
|
|
171
224
|
recordCheck();
|
package/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paperlint",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"description": "A linter for scientific papers written in LaTeX: catches mistakes before you submit to a conference or journal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "tsc",
|
|
9
9
|
"prepare": "tsc",
|
|
10
|
-
"test": "node scripts/
|
|
11
|
-
"
|
|
10
|
+
"test": "node scripts/test.mjs",
|
|
11
|
+
"coverage": "NODE_OPTIONS=\"--import=./test/coverage-src.mjs $NODE_OPTIONS\" c8 --check-coverage npm test --",
|
|
12
|
+
"lint": "eslint .",
|
|
12
13
|
"check": "node scripts/check.mjs",
|
|
13
|
-
"test:e2e": "node
|
|
14
|
+
"test:e2e": "node test/e2e/install.mjs && node test/e2e/build.mjs",
|
|
14
15
|
"fmt": "prettier --write .",
|
|
15
16
|
"fmt:check": "prettier --check ."
|
|
16
17
|
},
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
"@semantic-release/commit-analyzer": "13.0.1",
|
|
34
35
|
"@semantic-release/release-notes-generator": "14.1.1",
|
|
35
36
|
"@types/node": "^26.6.1",
|
|
37
|
+
"c8": "^12.0.0",
|
|
36
38
|
"conventional-changelog-conventionalcommits": "9.3.1",
|
|
37
39
|
"eslint-plugin-boundaries": "^7.2.0",
|
|
38
40
|
"eslint-plugin-n": "^18.3.0",
|
|
@@ -75,7 +77,6 @@
|
|
|
75
77
|
"CLAUDE.md",
|
|
76
78
|
"CONTRIBUTING.md",
|
|
77
79
|
"action.harness.mjs",
|
|
78
|
-
"action.mutations.mjs",
|
|
79
80
|
"action.yml",
|
|
80
81
|
"bin",
|
|
81
82
|
"dist",
|
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
* ⚠️ Assertions at the TOP LEVEL: `vigiles test` imports the file and counts "did not throw"
|
|
15
15
|
* as a pass.
|
|
16
16
|
*/
|
|
17
|
-
import assert from "node:assert/strict";
|
|
18
17
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
19
18
|
import { dirname, join } from "node:path";
|
|
20
19
|
import { fileURLToPath } from "node:url";
|
|
21
20
|
import { load } from "js-yaml";
|
|
21
|
+
import { createChecker } from "../lib/check.mjs";
|
|
22
22
|
|
|
23
23
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
24
24
|
const ROOT = dirname(HERE);
|
|
@@ -26,11 +26,7 @@ const ROOT = dirname(HERE);
|
|
|
26
26
|
const { GATES, NOT_COVERED, outcome, SKIP_EXIT, commandOf } =
|
|
27
27
|
await import("./check.mjs");
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
const check = (label, cond) => {
|
|
31
|
-
n++;
|
|
32
|
-
assert.ok(cond, label);
|
|
33
|
-
};
|
|
29
|
+
const check = createChecker();
|
|
34
30
|
|
|
35
31
|
// ── the workflow is the ORACLE, not a copy of it ───────────────────────────────────────────
|
|
36
32
|
// A declared skip is a THIRD outcome. Folding it into "pass" printed "all gates passed" on a
|
|
@@ -135,9 +131,7 @@ for (const g of GATES) {
|
|
|
135
131
|
// every program other than `node` must be installed in node_modules/.bin.
|
|
136
132
|
const argv = commandOf(g);
|
|
137
133
|
const files = argv.filter((a) => /\.m?js$/.test(a));
|
|
138
|
-
const programs = argv
|
|
139
|
-
(a, i) => i === 0 || argv[i - 1] === "scripts/exclusive.mjs",
|
|
140
|
-
);
|
|
134
|
+
const programs = [argv[0]];
|
|
141
135
|
// Guards: a gate runs SOMETHING — a script of ours, or an installed program such as `tsc`.
|
|
142
136
|
check(
|
|
143
137
|
`gate «${g.name}» runs files that exist (${files.join(", ")})`,
|
|
@@ -172,6 +166,6 @@ check(
|
|
|
172
166
|
);
|
|
173
167
|
|
|
174
168
|
console.log(
|
|
175
|
-
`✓ ${String(
|
|
169
|
+
`✓ ${String(check.count)} assertions passed — npm run check: ${GATES.length} gates, ` +
|
|
176
170
|
`${ciJobs.length} CI job(s) all accounted for`,
|
|
177
171
|
);
|