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
package/src/cli.mutations.mjs
DELETED
|
@@ -1,414 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Battery for the utility. Three of the first five mutations reintroduce defects it ALREADY
|
|
3
|
-
* had, found by the first run rather than by reading — so without these assertions the
|
|
4
|
-
* regression would be silent.
|
|
5
|
-
*/
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { dirname, join, resolve } from "node:path";
|
|
8
|
-
import { runMutations } from "../lib/mutation-driver.mjs";
|
|
9
|
-
|
|
10
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
11
|
-
const ROOT = resolve(HERE, "..");
|
|
12
|
-
const CLI = join(HERE, "cli.ts");
|
|
13
|
-
const HARNESS = join(HERE, "cli.harness.mjs");
|
|
14
|
-
// The `bin/paperlint.mjs` shim — the package's EXECUTABLE file, and after the move to TypeScript the
|
|
15
|
-
// "was I run or was I imported" check lives right there: `dist/cli.js` is now always imported,
|
|
16
|
-
// so a mutation in it about the symlink proves nothing. The first run after the move showed
|
|
17
|
-
// this literally — the mutation SURVIVED, and it looked like a hole in the harness, when the
|
|
18
|
-
// hole was in what the mutation was aimed at.
|
|
19
|
-
const SHIM = join(HERE, "..", "bin", "paperlint.mjs");
|
|
20
|
-
// `init` stopped being twenty lines inside `cli.ts` and became its own module: it has four
|
|
21
|
-
// decisions, and each must be able to break in a way that EXACTLY its own assertion notices.
|
|
22
|
-
const INIT = join(HERE, "init.ts");
|
|
23
|
-
|
|
24
|
-
process.exit(
|
|
25
|
-
runMutations({
|
|
26
|
-
root: ROOT,
|
|
27
|
-
runner: "node",
|
|
28
|
-
cases: [
|
|
29
|
-
{
|
|
30
|
-
name: "the main-module check goes back to comparing STRINGS",
|
|
31
|
-
harness: HARNESS,
|
|
32
|
-
expect:
|
|
33
|
-
"through a SYMLINK the utility works, rather than silently exiting zero",
|
|
34
|
-
disables:
|
|
35
|
-
"the only way a consumer ever calls the utility. npm puts a SYMLINK in .bin, for " +
|
|
36
|
-
"which process.argv[1] and import.meta.url are different paths; comparing strings " +
|
|
37
|
-
"makes the condition false and the utility SILENTLY exits zero. A direct " +
|
|
38
|
-
"`node bin/paperlint.mjs` still works, meanwhile, so the defect is invisible in exactly the " +
|
|
39
|
-
"way it is normally checked",
|
|
40
|
-
edits: [
|
|
41
|
-
[
|
|
42
|
-
SHIM,
|
|
43
|
-
"if (isMain(import.meta.url))",
|
|
44
|
-
"if (import.meta.url === `file://${process.argv[1]}`)",
|
|
45
|
-
],
|
|
46
|
-
],
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
name: "argv[0] unconditionally becomes the command again",
|
|
50
|
-
harness: HARNESS,
|
|
51
|
-
expect: "`--help` as the first argument — is a FLAG, not a command",
|
|
52
|
-
disables:
|
|
53
|
-
"parsing a flag in the command position. The real defect: `paperlint --help` used to " +
|
|
54
|
-
'answer "unknown command `--help`" — i.e. the very first command a new user types ' +
|
|
55
|
-
"told them it did not exist",
|
|
56
|
-
edits: [
|
|
57
|
-
[
|
|
58
|
-
CLI,
|
|
59
|
-
'if (rest[0] && !rest[0].startsWith("-")) out.cmd = rest.shift() ?? null;',
|
|
60
|
-
"out.cmd = rest.shift() ?? null;",
|
|
61
|
-
],
|
|
62
|
-
],
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
name: "a valueless flag silently turns into a default again",
|
|
66
|
-
harness: HARNESS,
|
|
67
|
-
expect: "a flag with no value — a FAILURE, not a silent default",
|
|
68
|
-
disables:
|
|
69
|
-
'the distinction between "no config was set" and "a config was set, but its value ' +
|
|
70
|
-
'got lost". The latter happens from a typo and from a CI substitution collapsing to ' +
|
|
71
|
-
"empty, and without a failure the run falls into autodiscovery and lints the WRONG " +
|
|
72
|
-
"file without saying a word about it",
|
|
73
|
-
edits: [[CLI, " if (a.missingValue) {", " if (false) {"]],
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
name: "no papers where the config points reads as a clean run",
|
|
77
|
-
harness: HARNESS,
|
|
78
|
-
expect:
|
|
79
|
-
"`lint` with NO path, no config and no papers/ refuses and names BOTH ways out",
|
|
80
|
-
disables:
|
|
81
|
-
"the guard that makes the `papers` default safe. Without it, a project whose papers " +
|
|
82
|
-
"live elsewhere gets a run over an empty or missing directory instead of the sentence " +
|
|
83
|
-
"naming both ways out",
|
|
84
|
-
edits: [[CLI, " if (empty !== undefined) {", " if (false) {"]],
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
name: "the GUARD AGAINST A GREEN ZERO is removed",
|
|
88
|
-
harness: HARNESS,
|
|
89
|
-
expect: "an empty set — a FAILURE, not a green zero",
|
|
90
|
-
disables:
|
|
91
|
-
'the distinction between "no findings" and "not one rule got a single file". These ' +
|
|
92
|
-
"two states are byte-for-byte identical in the output, and the second reads as success",
|
|
93
|
-
edits: [[CLI, "if (results.length === 0) {", "if (false) {"]],
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
name: "ESLint's exception stops being caught again",
|
|
97
|
-
harness: HARNESS,
|
|
98
|
-
expect:
|
|
99
|
-
"the utility does NOT let an exception escape — a failure is declared by the exit code",
|
|
100
|
-
disables:
|
|
101
|
-
"the explainability of a failure. The real defect: on an empty set ESLint THROWS a " +
|
|
102
|
-
"NoFilesFoundError, the guard never lived long enough to reach its own check, and " +
|
|
103
|
-
"instead of a message a stack trace flew out of the depths of eslint-helpers.js",
|
|
104
|
-
edits: [
|
|
105
|
-
[
|
|
106
|
-
CLI,
|
|
107
|
-
" if (isEmptySet(e)) results = [];\n else throw e;",
|
|
108
|
-
" throw e;",
|
|
109
|
-
],
|
|
110
|
-
],
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
name: "the declaration stops reaching paperlint.json again",
|
|
114
|
-
harness: HARNESS,
|
|
115
|
-
expect:
|
|
116
|
-
"🔴 THE DECLARATION SHOWS UP IN paperlint.json — the file the hooks read",
|
|
117
|
-
disables:
|
|
118
|
-
"the whole reason this command was rewritten (#33). A hook does not import code and " +
|
|
119
|
-
"cannot walk up the tree — it reads a path it is able to name, and that path is the " +
|
|
120
|
-
"root paperlint.json. Without writing there, the install looks like it succeeded, " +
|
|
121
|
-
"while `paper-edit-guard` guards the default",
|
|
122
|
-
edits: [
|
|
123
|
-
[
|
|
124
|
-
INIT,
|
|
125
|
-
" writeFileSync(\n path,\n JSON.stringify(\n { ...settings, [PAPERS_DIR_FIELD]: choice.papers },",
|
|
126
|
-
" (() => {})(\n path,\n JSON.stringify(\n { ...settings, [PAPERS_DIR_FIELD]: choice.papers },",
|
|
127
|
-
],
|
|
128
|
-
],
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
name: "the papers directory is GUESSED again instead of measured",
|
|
132
|
-
harness: HARNESS,
|
|
133
|
-
expect:
|
|
134
|
-
"and it says HOW it was measured — otherwise a guess reads as a fact",
|
|
135
|
-
disables:
|
|
136
|
-
"measuring instead of guessing. The declaration still gets WRITTEN — i.e. the failure " +
|
|
137
|
-
"is one-sided and points toward a confident wrong answer: the file says `papers`, the " +
|
|
138
|
-
"papers live somewhere else, and both commands stay silent about it",
|
|
139
|
-
edits: [
|
|
140
|
-
[
|
|
141
|
-
INIT,
|
|
142
|
-
" const candidates = detectPapers(root);",
|
|
143
|
-
" const candidates = [];",
|
|
144
|
-
],
|
|
145
|
-
],
|
|
146
|
-
},
|
|
147
|
-
{
|
|
148
|
-
name: "someone else's value in the declaration gets overwritten again",
|
|
149
|
-
harness: HARNESS,
|
|
150
|
-
expect:
|
|
151
|
-
"🔴 an already-declared value stays intact byte for byte — silently replacing a setting is worse than doing nothing",
|
|
152
|
-
disables:
|
|
153
|
-
"the ban on silently replacing a consumer's setting. They keep trusting the old " +
|
|
154
|
-
"value, because nobody told them it changed",
|
|
155
|
-
edits: [
|
|
156
|
-
[
|
|
157
|
-
INIT,
|
|
158
|
-
' if (existing !== undefined)\n return { status: "kept"',
|
|
159
|
-
' if (false)\n return { status: "kept"',
|
|
160
|
-
],
|
|
161
|
-
],
|
|
162
|
-
},
|
|
163
|
-
{
|
|
164
|
-
name: "the default that was taken stops being named",
|
|
165
|
-
harness: HARNESS,
|
|
166
|
-
expect:
|
|
167
|
-
"🔴 not a terminal — the question is NOT asked, and the default taken is NAMED",
|
|
168
|
-
disables:
|
|
169
|
-
'half the "don\'t ask in CI" rule: not asking is not enough, it must say WHICH ' +
|
|
170
|
-
'default was taken. A silent skip reads as "there was never a question"',
|
|
171
|
-
edits: [
|
|
172
|
-
[
|
|
173
|
-
INIT,
|
|
174
|
-
" ` · ${why}, so nothing was asked. Default taken: NO file written.`,",
|
|
175
|
-
" ` · skipped`,",
|
|
176
|
-
],
|
|
177
|
-
],
|
|
178
|
-
},
|
|
179
|
-
{
|
|
180
|
-
name: "the answer to the question is ignored",
|
|
181
|
-
harness: HARNESS,
|
|
182
|
-
expect:
|
|
183
|
-
"🔴 the human's answer DECIDES, it does not just decorate the output",
|
|
184
|
-
disables:
|
|
185
|
-
"the point of the one question that gets asked. The prompt is printed, the answer is " +
|
|
186
|
-
"read and discarded — i.e. there is an interface with no decision behind it",
|
|
187
|
-
edits: [
|
|
188
|
-
[
|
|
189
|
-
INIT,
|
|
190
|
-
' const picked = candidates[Number((answer ?? "").trim()) - 1];',
|
|
191
|
-
" const picked = candidates[0];",
|
|
192
|
-
],
|
|
193
|
-
],
|
|
194
|
-
},
|
|
195
|
-
{
|
|
196
|
-
name: "an interrupted question crashes the command again",
|
|
197
|
-
harness: HARNESS,
|
|
198
|
-
expect:
|
|
199
|
-
"🔴 an interrupted question does NOT crash the command — it means the default",
|
|
200
|
-
disables:
|
|
201
|
-
"handling Ctrl+D. MEASURED 09-18 on a real pseudo-terminal: readline's `question()` " +
|
|
202
|
-
"REJECTS with `AbortError: Aborted with Ctrl+D`, and the exception used to escape " +
|
|
203
|
-
"AFTER the declaration had been written — the install both succeeded and looked like " +
|
|
204
|
-
"a crash",
|
|
205
|
-
edits: [
|
|
206
|
-
[
|
|
207
|
-
INIT,
|
|
208
|
-
" try {\n return await ask(question);\n } catch {\n return null;\n }",
|
|
209
|
-
" return await ask(question);",
|
|
210
|
-
],
|
|
211
|
-
],
|
|
212
|
-
},
|
|
213
|
-
{
|
|
214
|
-
name: "init stops ending with doctor",
|
|
215
|
-
harness: HARNESS,
|
|
216
|
-
expect:
|
|
217
|
-
"init ends with doctor's report: the install vouches for its OWN state",
|
|
218
|
-
disables:
|
|
219
|
-
'the one thing that distinguishes "installed" from "actually guarding": ' +
|
|
220
|
-
"`paper-edit-guard` is silent both when it works and when it guards nothing. Without " +
|
|
221
|
-
"the final doctor, init reports cheerfully on a state it never measured",
|
|
222
|
-
edits: [
|
|
223
|
-
[
|
|
224
|
-
INIT,
|
|
225
|
-
" const code = doctor({ log, cwd: root, projectDir: root, run, cliPapers });",
|
|
226
|
-
" const code = 0;",
|
|
227
|
-
],
|
|
228
|
-
],
|
|
229
|
-
},
|
|
230
|
-
{
|
|
231
|
-
name: "a paperlint.json is written for the default again",
|
|
232
|
-
harness: HARNESS,
|
|
233
|
-
expect:
|
|
234
|
-
"🔴 nothing to measure — the default is taken, and NO paperlint.json is written for it",
|
|
235
|
-
disables:
|
|
236
|
-
"the rule that a project on the defaults needs no config file. Every init would leave " +
|
|
237
|
-
"a file restating the default, and a reader could no longer tell a choice from a default",
|
|
238
|
-
edits: [
|
|
239
|
-
[
|
|
240
|
-
INIT,
|
|
241
|
-
' if (choice.papers === DEFAULT_PAPERS_ROOT)\n return { status: "default"',
|
|
242
|
-
' if (false)\n return { status: "default"',
|
|
243
|
-
],
|
|
244
|
-
],
|
|
245
|
-
},
|
|
246
|
-
{
|
|
247
|
-
name: "a missing program is named with no remedy",
|
|
248
|
-
harness: HARNESS,
|
|
249
|
-
expect:
|
|
250
|
-
"🔴 and it carries the INSTALL COMMAND — a remedy, not just a diagnosis",
|
|
251
|
-
disables:
|
|
252
|
-
'the second half of the "install nothing on the user\'s behalf" rule. A diagnosis ' +
|
|
253
|
-
"with no remedy leaves the person exactly where they stood: the program is missing, " +
|
|
254
|
-
"and what to type is unknown",
|
|
255
|
-
edits: [
|
|
256
|
-
[
|
|
257
|
-
INIT,
|
|
258
|
-
" for (const cmd of [...new Set(missing.map((p) => p.install))])\n log(` ${cmd}`);",
|
|
259
|
-
" void missing;",
|
|
260
|
-
],
|
|
261
|
-
],
|
|
262
|
-
},
|
|
263
|
-
{
|
|
264
|
-
name: "absences stop being named by name",
|
|
265
|
-
harness: HARNESS,
|
|
266
|
-
expect: "every absence is NAMED, and the count matches the names",
|
|
267
|
-
disables:
|
|
268
|
-
'the link between the NUMBER and the NAMES in one line. "Programs are missing" with ' +
|
|
269
|
-
"no names means going to hunt for them in another report, and a counter with no names " +
|
|
270
|
-
"is exactly the counter that promises coverage and renders no verdict",
|
|
271
|
-
edits: [
|
|
272
|
-
[
|
|
273
|
-
INIT,
|
|
274
|
-
' log(\n ` ✗ ${String(missing.length)} of ${String(PROGRAMS.length)} missing: ` +\n missing.map((p) => p.bin).join(", "),\n );',
|
|
275
|
-
" log(` ✗ some programs are missing`);",
|
|
276
|
-
],
|
|
277
|
-
],
|
|
278
|
-
},
|
|
279
|
-
{
|
|
280
|
-
name: "the utility stops reading paperlint.json",
|
|
281
|
-
harness: HARNESS,
|
|
282
|
-
expect:
|
|
283
|
-
"🔴 THE UTILITY READS THE DECLARATION FROM paperlint.json — the file `paperlint init` writes and the hooks read",
|
|
284
|
-
disables:
|
|
285
|
-
"the link between the install command and the check command. `init` writes the " +
|
|
286
|
-
"declaration into paperlint.json, while `lint` would run on the default — right after " +
|
|
287
|
-
"install it reports on a directory the papers are not in",
|
|
288
|
-
edits: [
|
|
289
|
-
[
|
|
290
|
-
CLI,
|
|
291
|
-
" const configPath = existsSync(found) ? found : null;",
|
|
292
|
-
" const configPath = existsSync(found) ? null : null;",
|
|
293
|
-
],
|
|
294
|
-
],
|
|
295
|
-
},
|
|
296
|
-
{
|
|
297
|
-
name: "init stops naming the skills it skipped",
|
|
298
|
-
harness: HARNESS,
|
|
299
|
-
expect: "a skipped skill is NAMED with what occupies it",
|
|
300
|
-
disables:
|
|
301
|
-
"the only way a consumer learns WHICH skill is missing from Claude Code. A count of " +
|
|
302
|
-
"skipped entries says something is wrong and not what, so nobody goes to fix it",
|
|
303
|
-
edits: [
|
|
304
|
-
[
|
|
305
|
-
INIT,
|
|
306
|
-
' for (const l of skipped)\n out.push(` ${l.name} — ${l.reason ?? "occupied"}`);',
|
|
307
|
-
"",
|
|
308
|
-
],
|
|
309
|
-
],
|
|
310
|
-
},
|
|
311
|
-
{
|
|
312
|
-
name: "a skipped skill link fails init",
|
|
313
|
-
harness: HARNESS,
|
|
314
|
-
expect: "a name init refused to take does not fail the install",
|
|
315
|
-
disables:
|
|
316
|
-
"init being safe to run on a real project. A consumer who keeps their own skill under " +
|
|
317
|
-
"the same name would get a red install for a decision init itself respected",
|
|
318
|
-
edits: [
|
|
319
|
-
[
|
|
320
|
-
INIT,
|
|
321
|
-
" for (const line of reportSkillLinks(link(root), here)) log(line);",
|
|
322
|
-
' const linked = link(root);\n for (const line of reportSkillLinks(linked, here)) log(line);\n if (linked.ok && linked.links.some((l) => l.status === "foreign")) return 2;',
|
|
323
|
-
],
|
|
324
|
-
],
|
|
325
|
-
},
|
|
326
|
-
|
|
327
|
-
{
|
|
328
|
-
name: "CI stops counting as 'no human'",
|
|
329
|
-
harness: HARNESS,
|
|
330
|
-
expect: "CI set → NOT interactive",
|
|
331
|
-
disables:
|
|
332
|
-
"the one signal a CI runner with a pseudo-terminal gives. init would print a question " +
|
|
333
|
-
"into a log nobody reads and wait for an answer that never comes",
|
|
334
|
-
edits: [
|
|
335
|
-
[
|
|
336
|
-
INIT,
|
|
337
|
-
' if (env["CI"]) return { interactive: false, why: "CI is set" };\n',
|
|
338
|
-
"",
|
|
339
|
-
],
|
|
340
|
-
],
|
|
341
|
-
},
|
|
342
|
-
{
|
|
343
|
-
name: "stdout is no longer consulted",
|
|
344
|
-
harness: HARNESS,
|
|
345
|
-
expect: "stdout piped",
|
|
346
|
-
disables:
|
|
347
|
-
"the agent case. An agent piping init's output has a terminal-less stdout; with only " +
|
|
348
|
-
"stdin checked it is asked a question it will never see",
|
|
349
|
-
edits: [
|
|
350
|
-
[
|
|
351
|
-
INIT,
|
|
352
|
-
' if (!stdoutTTY)\n return { interactive: false, why: "stdout is not a terminal" };\n',
|
|
353
|
-
"",
|
|
354
|
-
],
|
|
355
|
-
],
|
|
356
|
-
},
|
|
357
|
-
{
|
|
358
|
-
name: "without a human the hooks default to NO",
|
|
359
|
-
harness: HARNESS,
|
|
360
|
-
expect: "without a human the hooks default to YES",
|
|
361
|
-
disables:
|
|
362
|
-
"the install an agent performs. The agent cannot type /plugin and is never asked, so a " +
|
|
363
|
-
"NO default leaves every agent-made install without the edit guard, silently",
|
|
364
|
-
edits: [
|
|
365
|
-
[
|
|
366
|
-
INIT,
|
|
367
|
-
' if (!hooks) return { status: "skipped" };',
|
|
368
|
-
' if (!hooks || !interactive) return { status: "skipped" };',
|
|
369
|
-
],
|
|
370
|
-
],
|
|
371
|
-
},
|
|
372
|
-
{
|
|
373
|
-
name: "a human's no is ignored",
|
|
374
|
-
harness: HARNESS,
|
|
375
|
-
expect: "and a no is respected",
|
|
376
|
-
disables:
|
|
377
|
-
"the question itself. A prompt whose answer does not decide anything is decoration, " +
|
|
378
|
-
"and the file is written into a repository whose owner said no",
|
|
379
|
-
edits: [
|
|
380
|
-
[
|
|
381
|
-
INIT,
|
|
382
|
-
' if (answer === "n" || answer === "no") return { status: "declined" };',
|
|
383
|
-
' if (false) return { status: "declined" };',
|
|
384
|
-
],
|
|
385
|
-
],
|
|
386
|
-
},
|
|
387
|
-
{
|
|
388
|
-
name: "a first paper is offered where one already exists",
|
|
389
|
-
harness: HARNESS,
|
|
390
|
-
expect:
|
|
391
|
-
"a papers directory that already holds a paper is not offered another",
|
|
392
|
-
disables:
|
|
393
|
-
"the condition that makes the offer an offer. Every re-run of init on a working project " +
|
|
394
|
-
"would ask for a new paper",
|
|
395
|
-
edits: [
|
|
396
|
-
[
|
|
397
|
-
INIT,
|
|
398
|
-
" if (wanted === null && !hasPaper && interactive && createPaper) {",
|
|
399
|
-
" if (wanted === null && interactive && createPaper) {",
|
|
400
|
-
],
|
|
401
|
-
],
|
|
402
|
-
},
|
|
403
|
-
{
|
|
404
|
-
name: "the project's paper template is linted as a paper",
|
|
405
|
-
harness: HARNESS,
|
|
406
|
-
expect: "the project's paper TEMPLATE directory is ignored",
|
|
407
|
-
disables:
|
|
408
|
-
"the override slot `paperlint new` reads. Flat config does not skip dot-directories, so a " +
|
|
409
|
-
"richer house template — placeholder stages and all — would fail every `paperlint lint`",
|
|
410
|
-
edits: [[CLI, ' { ignores: ["**/.template/"] },\n', ""]],
|
|
411
|
-
},
|
|
412
|
-
],
|
|
413
|
-
}),
|
|
414
|
-
);
|
package/src/doctor.mutations.mjs
DELETED
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Battery for `paperlint doctor`.
|
|
3
|
-
*
|
|
4
|
-
* The subject here is unusual and therefore fragile: doctor is a check ABOUT A CHECK. Broken,
|
|
5
|
-
* it prints a column of checkmarks and exits zero — i.e. it fails in exactly the way it was
|
|
6
|
-
* written to catch. Every case below reintroduces one of those silent failures.
|
|
7
|
-
*/
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
9
|
-
import { dirname, join, resolve } from "node:path";
|
|
10
|
-
import { runMutations } from "../lib/mutation-driver.mjs";
|
|
11
|
-
|
|
12
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
13
|
-
const ROOT = resolve(HERE, "..");
|
|
14
|
-
const SRC = join(HERE, "doctor.ts");
|
|
15
|
-
const HARNESS = join(HERE, "doctor.harness.mjs");
|
|
16
|
-
|
|
17
|
-
process.exit(
|
|
18
|
-
runMutations({
|
|
19
|
-
root: ROOT,
|
|
20
|
-
runner: "node",
|
|
21
|
-
cases: [
|
|
22
|
-
{
|
|
23
|
-
name: "comparing the two roots stops meaning anything",
|
|
24
|
-
harness: HARNESS,
|
|
25
|
-
expect: "writes pass the guard unseen",
|
|
26
|
-
disables:
|
|
27
|
-
"the only reason this command exists. A mismatch between what the CLI lints and what " +
|
|
28
|
-
"the hook guards is a silent hole: both sides look installed and green, while writes " +
|
|
29
|
-
"into real papers pass the gate unseen",
|
|
30
|
-
edits: [
|
|
31
|
-
[
|
|
32
|
-
SRC,
|
|
33
|
-
" const same = resolve(root, cliPapers) === resolve(root, hookSays);",
|
|
34
|
-
" const same = true;",
|
|
35
|
-
],
|
|
36
|
-
],
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
name: "the hook's root is retold instead of asked for",
|
|
40
|
-
harness: HARNESS,
|
|
41
|
-
expect: "doctor prints EXACTLY what the hook returned",
|
|
42
|
-
disables:
|
|
43
|
-
"the defense against a SECOND COPY OF THE LOGIC — exactly the defect this command " +
|
|
44
|
-
"exists to report. A retelling drifts from the original silently and prints a " +
|
|
45
|
-
"confident wrong answer: here it loses the trailing-slash trim the hook does on purpose",
|
|
46
|
-
edits: [
|
|
47
|
-
[
|
|
48
|
-
SRC,
|
|
49
|
-
' const hookRoot = papersRoot(raw ?? "{}");',
|
|
50
|
-
' const hookRoot = raw === null ? "papers" : (() => { try { return JSON.parse(raw)?.papersDir ?? "papers"; } catch { return "papers"; } })();',
|
|
51
|
-
],
|
|
52
|
-
],
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
name: "a missing external program starts failing the run",
|
|
56
|
-
harness: HARNESS,
|
|
57
|
-
expect: "a missing tex install does NOT fail the run",
|
|
58
|
-
disables:
|
|
59
|
-
"the distinction between a fact and a verdict. Which programs are needed depends on " +
|
|
60
|
-
"which skills you use; a command that fails on advice gets piped into `|| true` or " +
|
|
61
|
-
"/dev/null entirely, taking the real binary findings it was written for down with it",
|
|
62
|
-
edits: [
|
|
63
|
-
[
|
|
64
|
-
SRC,
|
|
65
|
-
" out.push(` ${p.install}`);",
|
|
66
|
-
" out.push(` ${p.install}`);\n bad++;",
|
|
67
|
-
],
|
|
68
|
-
],
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
name: "detection loses the root and returns the papers themselves",
|
|
72
|
-
harness: HARNESS,
|
|
73
|
-
expect: "finds the root by a marker inside a subdirectory",
|
|
74
|
-
disables:
|
|
75
|
-
"the whole point of the hint. A config pointing at ONE document instead of a " +
|
|
76
|
-
"directory passes every check and lints one paper out of ten — while reporting clean",
|
|
77
|
-
edits: [
|
|
78
|
-
[
|
|
79
|
-
SRC,
|
|
80
|
-
" if (isPapersRoot) hits.push(relative(cwd, here));",
|
|
81
|
-
" if (isPapersRoot) hits.push(...children.filter((c) => c.isDirectory()).map((c) => relative(cwd, join(here, c.name))));",
|
|
82
|
-
],
|
|
83
|
-
],
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
name: "the papers themselves get mixed into the found root",
|
|
87
|
-
harness: HARNESS,
|
|
88
|
-
expect: "it is the ROOT, not the paper itself",
|
|
89
|
-
disables:
|
|
90
|
-
"the distinction between a papers directory and a paper. The root stays in the list, " +
|
|
91
|
-
'so the neighboring "found by marker" check notices nothing — while a config built ' +
|
|
92
|
-
"from such a hint points at one document and lints one paper out of ten, reporting clean",
|
|
93
|
-
edits: [
|
|
94
|
-
[
|
|
95
|
-
SRC,
|
|
96
|
-
" if (isPapersRoot) hits.push(relative(cwd, here));",
|
|
97
|
-
" if (isPapersRoot) hits.push(relative(cwd, here), ...children.filter((c) => c.isDirectory()).map((c) => relative(cwd, join(here, c.name))));",
|
|
98
|
-
],
|
|
99
|
-
],
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
name: "the walk stops skipping node_modules",
|
|
103
|
-
harness: HARNESS,
|
|
104
|
-
expect: "node_modules is not searched",
|
|
105
|
-
disables:
|
|
106
|
-
"the boundary between your own papers and someone else's. Any installed dependency " +
|
|
107
|
-
"carrying paper examples gets mistaken for the consumer's directory, and the hint " +
|
|
108
|
-
"points the config into node_modules",
|
|
109
|
-
edits: [
|
|
110
|
-
[
|
|
111
|
-
SRC,
|
|
112
|
-
'const skip = new Set(["node_modules", ".git", "dist", "_build", ".claude"]);',
|
|
113
|
-
"const skip = new Set();",
|
|
114
|
-
],
|
|
115
|
-
],
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
name: "an unparsable paperlint.json is reported as all-clear",
|
|
119
|
-
harness: HARNESS,
|
|
120
|
-
expect: "an unparsable paperlint.json — a FAILURE",
|
|
121
|
-
disables:
|
|
122
|
-
"the one failure left in the settings file: while it does not parse, the edit guard " +
|
|
123
|
-
"refuses every Bash command. A doctor that counts it as fine sends the reader looking " +
|
|
124
|
-
"everywhere but the file",
|
|
125
|
-
edits: [
|
|
126
|
-
[
|
|
127
|
-
SRC,
|
|
128
|
-
" ` the edit guard refuses every Bash command until it parses — fix it with Edit or Write`,\n ],\n bad: 1,",
|
|
129
|
-
" ` the edit guard refuses every Bash command until it parses — fix it with Edit or Write`,\n ],\n bad: 0,",
|
|
130
|
-
],
|
|
131
|
-
],
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
name: "an unlinked skill is folded into the checkmark",
|
|
135
|
-
harness: HARNESS,
|
|
136
|
-
expect: "an unlinked skill is NAMED",
|
|
137
|
-
disables:
|
|
138
|
-
"the one place a consumer learns that `/paper-pipeline` is missing. Claude Code does not " +
|
|
139
|
-
"look in node_modules, so an install without links has no skills at all and says nothing",
|
|
140
|
-
edits: [
|
|
141
|
-
[
|
|
142
|
-
SRC,
|
|
143
|
-
' const gaps = links.links.filter((l) => l.status !== "present");',
|
|
144
|
-
' const gaps = links.links.filter((l) => l.status === "foreign");',
|
|
145
|
-
],
|
|
146
|
-
],
|
|
147
|
-
},
|
|
148
|
-
{
|
|
149
|
-
name: "an unlinked skill starts failing the run",
|
|
150
|
-
harness: HARNESS,
|
|
151
|
-
expect: "an unlinked skill does NOT fail the run",
|
|
152
|
-
disables:
|
|
153
|
-
"the line between a fact and a verdict. A consumer who keeps their own skill under the " +
|
|
154
|
-
"same name would get a red doctor — and a red `init` — for a decision init itself respected",
|
|
155
|
-
edits: [
|
|
156
|
-
[
|
|
157
|
-
SRC,
|
|
158
|
-
" out.push(\n ` \\`npx paperlint init\\` links the missing ones; it never replaces an entry it did not make`,\n );",
|
|
159
|
-
" out.push(\n ` \\`npx paperlint init\\` links the missing ones; it never replaces an entry it did not make`,\n );\n bad++;",
|
|
160
|
-
],
|
|
161
|
-
],
|
|
162
|
-
},
|
|
163
|
-
{
|
|
164
|
-
name: "a .template child makes a papers root",
|
|
165
|
-
harness: HARNESS,
|
|
166
|
-
expect:
|
|
167
|
-
"a directory whose only marked child is .template/ is NOT a papers root",
|
|
168
|
-
disables:
|
|
169
|
-
"the override slot being invisible to discovery. `paperlint init` in a project that has only " +
|
|
170
|
-
"a house template would declare the template's parent as the papers directory, measured",
|
|
171
|
-
edits: [[SRC, ' !c.name.startsWith(".") &&\n', ""]],
|
|
172
|
-
},
|
|
173
|
-
],
|
|
174
|
-
}),
|
|
175
|
-
);
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Battery for `hooks-settings.ts`.
|
|
3
|
-
*
|
|
4
|
-
* Every case reintroduces a way the hooks end up wired wrong while every report stays green:
|
|
5
|
-
* twice (a guard that runs two times says nothing about it), rewritten on every run (a committed
|
|
6
|
-
* file that churns for nothing), or reported as fine when doctor was asked about the wrong thing.
|
|
7
|
-
*/
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
9
|
-
import { dirname, join, resolve } from "node:path";
|
|
10
|
-
import { runMutations } from "../lib/mutation-driver.mjs";
|
|
11
|
-
|
|
12
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
13
|
-
const ROOT = resolve(HERE, "..");
|
|
14
|
-
const SRC = join(HERE, "hooks-settings.ts");
|
|
15
|
-
const HARNESS = join(HERE, "hooks-settings.harness.mjs");
|
|
16
|
-
|
|
17
|
-
process.exit(
|
|
18
|
-
runMutations({
|
|
19
|
-
root: ROOT,
|
|
20
|
-
runner: "node",
|
|
21
|
-
cases: [
|
|
22
|
-
{
|
|
23
|
-
name: "every spelling of the bin counts as ours",
|
|
24
|
-
harness: HARNESS,
|
|
25
|
-
// The spelling truth table is the first owner: `npx paperlint hook …` must read as NOT ours,
|
|
26
|
-
// and that row is where it goes red.
|
|
27
|
-
expect: '→ {"name":"paper-skills-nudge","ours":false}',
|
|
28
|
-
disables:
|
|
29
|
-
"the duplicate guard for every bin spelling but ours. A hook wired by hand as " +
|
|
30
|
-
"`npx paperlint hook …` or through an absolute path to the bin reads as ours, init merges its own " +
|
|
31
|
-
"copy beside it, and the hook runs twice per event",
|
|
32
|
-
edits: [[SRC, "ours: t === MANAGED_BY,", "ours: true,"]],
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
name: "another spelling no longer stops the write",
|
|
36
|
-
harness: HARNESS,
|
|
37
|
-
expect: "a hook wired by hand under another spelling",
|
|
38
|
-
disables:
|
|
39
|
-
"the refusal itself: the foreign command is found and then written over anyway",
|
|
40
|
-
edits: [[SRC, " if (found.length > 0) {", " if (false) {"]],
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
name: "the settings file is rewritten even when nothing changed",
|
|
44
|
-
harness: HARNESS,
|
|
45
|
-
// The fresh project's second run is the first place a rewrite shows: `written` twice.
|
|
46
|
-
expect: "a second run changes NOTHING",
|
|
47
|
-
disables:
|
|
48
|
-
"idempotency as the user sees it. Every init would reformat a committed file the user " +
|
|
49
|
-
"wrote, and the diff would say nothing happened while showing that everything did",
|
|
50
|
-
edits: [
|
|
51
|
-
[
|
|
52
|
-
SRC,
|
|
53
|
-
" if (JSON.stringify(next) === JSON.stringify(read.settings))",
|
|
54
|
-
" if (false)",
|
|
55
|
-
],
|
|
56
|
-
],
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
name: "the project-dir prefix is not stripped",
|
|
60
|
-
harness: HARNESS,
|
|
61
|
-
expect: "hookRun(",
|
|
62
|
-
disables:
|
|
63
|
-
"recognising the spelling init itself writes. `${CLAUDE_PROJECT_DIR}/node_modules/…` " +
|
|
64
|
-
"stops matching, so a second init sees a foreign hook where its own stands",
|
|
65
|
-
edits: [
|
|
66
|
-
[
|
|
67
|
-
SRC,
|
|
68
|
-
' return unquoted.replace(/^\\$\\{?CLAUDE_PROJECT_DIR\\}?[/\\\\]/, "");',
|
|
69
|
-
" return unquoted;",
|
|
70
|
-
],
|
|
71
|
-
],
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
name: "doctor stops counting duplicates",
|
|
75
|
-
harness: HARNESS,
|
|
76
|
-
expect: "the same hook under two spellings",
|
|
77
|
-
disables:
|
|
78
|
-
"the only report of a guard that runs twice. Claude Code dedupes only identical " +
|
|
79
|
-
"handlers, so two spellings both run and nothing else would say so",
|
|
80
|
-
edits: [
|
|
81
|
-
[
|
|
82
|
-
SRC,
|
|
83
|
-
" const twice = wiring.names.filter((n) => total(n) > 1);",
|
|
84
|
-
" const twice = wiring.names.filter((n) => total(n) > 2);",
|
|
85
|
-
],
|
|
86
|
-
],
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
name: "doctor offers `paperlint init` even where init refuses to write",
|
|
90
|
-
harness: HARNESS,
|
|
91
|
-
expect: "partly wired BY HAND",
|
|
92
|
-
disables:
|
|
93
|
-
"a remedy that works. The reader runs init, init writes nothing, and doctor says the " +
|
|
94
|
-
"same thing again — a loop with no exit",
|
|
95
|
-
edits: [[SRC, " const remedy = handWired", " const remedy = false"]],
|
|
96
|
-
},
|
|
97
|
-
],
|
|
98
|
-
}),
|
|
99
|
-
);
|