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
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* consumer.mutations.mjs — plant each defect `consumer.harness.mjs` claims to catch, and require
|
|
3
|
-
* it to die BY ITS OWN assertion.
|
|
4
|
-
*
|
|
5
|
-
* Run: `node skills/paper-pipeline/scripts/consumer.mutations.mjs` (it rewrites `consumer.mjs` and
|
|
6
|
-
* restores it, so it is not part of the plain test run).
|
|
7
|
-
*
|
|
8
|
-
* 🔴 WHY THIS ONE MATTERS MORE THAN MOST. Every resolution in `consumer.mjs` fails toward SILENCE:
|
|
9
|
-
* a ledger under `node_modules` still accepts appends, a main guard that never fires still exits 0,
|
|
10
|
-
* a root that does not exist still hashes to a stable value. That is exactly the shape a harness
|
|
11
|
-
* cannot be seen catching — so the four cases below are the only evidence that its assertions can
|
|
12
|
-
* fail at all. Each `expect` is the harness's OWN message for that defect, not a generic failure
|
|
13
|
-
* string: a case killed by somebody else's assertion says nothing about the defect it planted.
|
|
14
|
-
*/
|
|
15
|
-
import { dirname, join } from "node:path";
|
|
16
|
-
import { fileURLToPath } from "node:url";
|
|
17
|
-
import { runMutations } from "../../../lib/mutation-driver.mjs";
|
|
18
|
-
import { consumerRoot } from "./consumer.mjs";
|
|
19
|
-
|
|
20
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
21
|
-
const SRC = join(HERE, "consumer.mjs");
|
|
22
|
-
const HARNESS = join(HERE, "consumer.harness.mjs");
|
|
23
|
-
|
|
24
|
-
const MUTATIONS = [
|
|
25
|
-
[
|
|
26
|
-
"a declared `null` is read as an absence",
|
|
27
|
-
[
|
|
28
|
-
[
|
|
29
|
-
SRC,
|
|
30
|
-
"if (declared !== undefined) {",
|
|
31
|
-
"if (declared !== undefined && declared !== null) {",
|
|
32
|
-
],
|
|
33
|
-
],
|
|
34
|
-
'a `"ledger": null` was accepted',
|
|
35
|
-
"the absence/keystroke distinction — a typed null silently picks a different file",
|
|
36
|
-
],
|
|
37
|
-
[
|
|
38
|
-
"`node_modules` is detected by substring instead of by path segment",
|
|
39
|
-
[
|
|
40
|
-
[
|
|
41
|
-
SRC,
|
|
42
|
-
'return dir.split(sep).includes("node_modules");',
|
|
43
|
-
'return dir.includes("node_modules");',
|
|
44
|
-
],
|
|
45
|
-
],
|
|
46
|
-
"naming coincidence",
|
|
47
|
-
"a consumer whose own directory merely contains the word is refused its ledger",
|
|
48
|
-
],
|
|
49
|
-
[
|
|
50
|
-
"the ledger default inside node_modules is no longer refused",
|
|
51
|
-
[
|
|
52
|
-
[
|
|
53
|
-
SRC,
|
|
54
|
-
" if (insideNodeModules(hereDir))",
|
|
55
|
-
" if (false && insideNodeModules(hereDir))",
|
|
56
|
-
],
|
|
57
|
-
],
|
|
58
|
-
"ledgerPath RETURNED A PATH INSIDE node_modules",
|
|
59
|
-
"the one guard against silent data loss: appends succeed until `npm ci` deletes them",
|
|
60
|
-
],
|
|
61
|
-
[
|
|
62
|
-
"the main guard stops resolving the entry point's realpath",
|
|
63
|
-
[[SRC, " real = realpathSync(entry);", " real = entry;"]],
|
|
64
|
-
"isMain is false through a symlink",
|
|
65
|
-
"every CLI in this directory when a consumer runs it at the path its skills document",
|
|
66
|
-
],
|
|
67
|
-
|
|
68
|
-
// ── the fourth carrier. Its defects are worse than the three above: the value is a PREFIX a
|
|
69
|
-
// caller filters prose with, so a wrong one matches nothing and every check built on it
|
|
70
|
-
// reports zero findings — the green zero this package has a dedicated guard against.
|
|
71
|
-
[
|
|
72
|
-
"a declared `null` scripts path is read as an absence",
|
|
73
|
-
[
|
|
74
|
-
[
|
|
75
|
-
SRC,
|
|
76
|
-
"const rel = declared === undefined ? DEFAULT_SCRIPTS_ROOT : declared;",
|
|
77
|
-
"const rel = declared ?? DEFAULT_SCRIPTS_ROOT;",
|
|
78
|
-
],
|
|
79
|
-
],
|
|
80
|
-
'a `"scripts": null` was accepted',
|
|
81
|
-
"the absence/keystroke distinction — a typed null is ignored and the default silently wins",
|
|
82
|
-
],
|
|
83
|
-
[
|
|
84
|
-
"a scripts path inside node_modules is no longer refused",
|
|
85
|
-
[
|
|
86
|
-
[
|
|
87
|
-
SRC,
|
|
88
|
-
" if (insideNodeModules(abs))",
|
|
89
|
-
" if (false && insideNodeModules(abs))",
|
|
90
|
-
],
|
|
91
|
-
],
|
|
92
|
-
"scriptsRoot ACCEPTED a path inside node_modules",
|
|
93
|
-
"the guard against naming a path `npm ci` deletes and git does not track",
|
|
94
|
-
],
|
|
95
|
-
[
|
|
96
|
-
"a scripts path that is not on disk is no longer refused",
|
|
97
|
-
[[SRC, " if (!existsSync(abs))", " if (false && !existsSync(abs))"]],
|
|
98
|
-
"scriptsRoot accepted a path that does not exist",
|
|
99
|
-
"the only net under the wrong prefix, whose symptom is zero findings and exit 0",
|
|
100
|
-
],
|
|
101
|
-
[
|
|
102
|
-
"the scripts root is returned resolved instead of as declared",
|
|
103
|
-
[[SRC, " return rel;", " return abs;"]],
|
|
104
|
-
"scriptsRoot returned an ABSOLUTE path",
|
|
105
|
-
"prose comparison — every `startsWith` against a SKILL.md becomes false",
|
|
106
|
-
],
|
|
107
|
-
[
|
|
108
|
-
"a declared trailing slash is no longer normalised",
|
|
109
|
-
[[SRC, 'const base = root.replace(/\\/+$/, "");', "const base = root;"]],
|
|
110
|
-
"a declared trailing slash doubled the separator",
|
|
111
|
-
"the one input a consumer types by habit, whose doubled prefix matches nothing",
|
|
112
|
-
],
|
|
113
|
-
[
|
|
114
|
-
"the prefix loses its trailing separator",
|
|
115
|
-
[[SRC, " prefix: `${base}/`,", " prefix: `${base}`,"]],
|
|
116
|
-
"the prefix matched a sibling directory sharing the root's name",
|
|
117
|
-
"the boundary between the pipeline's scripts and a directory merely named like them",
|
|
118
|
-
],
|
|
119
|
-
|
|
120
|
-
// ── `installedSkills` (paperlint#62). Its defects fail toward a WRONG ROSTER, not an error: a reader
|
|
121
|
-
// that does not follow links sees zero skills in every consumer; one that skips a dangling link
|
|
122
|
-
// reports a broken install as "not installed"; one that counts it hands out an unreadable skill.
|
|
123
|
-
[
|
|
124
|
-
"installed skills are listed by `Dirent.isDirectory()` again, which does not follow links",
|
|
125
|
-
[
|
|
126
|
-
[
|
|
127
|
-
SRC,
|
|
128
|
-
" for (const name of readdirSync(dir)) {",
|
|
129
|
-
" for (const dirent of readdirSync(dir, { withFileTypes: true })) {\n const name = dirent.name;",
|
|
130
|
-
],
|
|
131
|
-
[
|
|
132
|
-
SRC,
|
|
133
|
-
' if (st.isDirectory() && existsSync(join(entry, "SKILL.md")))',
|
|
134
|
-
' if (dirent.isDirectory() && existsSync(join(entry, "SKILL.md")))',
|
|
135
|
-
],
|
|
136
|
-
],
|
|
137
|
-
"a SYMLINKED skill was not counted as installed",
|
|
138
|
-
"every installed skill in every consumer — `paperlint init` makes links, and the evals refuse to start",
|
|
139
|
-
],
|
|
140
|
-
[
|
|
141
|
-
"a dangling skill link is skipped silently",
|
|
142
|
-
[
|
|
143
|
-
[
|
|
144
|
-
SRC,
|
|
145
|
-
' dangling.push({\n name,\n target: readlinkOr(entry),\n cause: e.code ?? "error",\n });',
|
|
146
|
-
"",
|
|
147
|
-
],
|
|
148
|
-
],
|
|
149
|
-
"a DANGLING skill link was silently accepted",
|
|
150
|
-
"the report of a broken install — a retired skill's stale link reads as one never installed",
|
|
151
|
-
],
|
|
152
|
-
[
|
|
153
|
-
"a dangling skill link is counted as installed",
|
|
154
|
-
[
|
|
155
|
-
[
|
|
156
|
-
SRC,
|
|
157
|
-
' dangling.push({\n name,\n target: readlinkOr(entry),\n cause: e.code ?? "error",\n });',
|
|
158
|
-
" names.push(name);",
|
|
159
|
-
],
|
|
160
|
-
],
|
|
161
|
-
"a DANGLING skill link was silently accepted",
|
|
162
|
-
"the guarantee that every name returned has a SKILL.md a caller can open",
|
|
163
|
-
],
|
|
164
|
-
[
|
|
165
|
-
"any directory counts as an installed skill",
|
|
166
|
-
[
|
|
167
|
-
[
|
|
168
|
-
SRC,
|
|
169
|
-
' if (st.isDirectory() && existsSync(join(entry, "SKILL.md")))',
|
|
170
|
-
" if (st.isDirectory())",
|
|
171
|
-
],
|
|
172
|
-
],
|
|
173
|
-
"installedSkills must return exactly the entries that lead to a directory holding SKILL.md",
|
|
174
|
-
"the definition of a skill — a helper directory beside the skills joins the roster",
|
|
175
|
-
],
|
|
176
|
-
];
|
|
177
|
-
|
|
178
|
-
process.exit(
|
|
179
|
-
runMutations({
|
|
180
|
-
root: consumerRoot(),
|
|
181
|
-
runner: "vigiles",
|
|
182
|
-
cases: MUTATIONS.map(([name, edits, expect, disables]) => ({
|
|
183
|
-
name,
|
|
184
|
-
disables,
|
|
185
|
-
edits,
|
|
186
|
-
harness: HARNESS,
|
|
187
|
-
expect,
|
|
188
|
-
})),
|
|
189
|
-
}),
|
|
190
|
-
);
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* extract-ref-facts.mutations.mjs — plant the defect `extract-ref-facts.harness.mjs` claims to catch, and require it to die.
|
|
3
|
-
*
|
|
4
|
-
* Run: `node skills/paper-pipeline/scripts/extract-ref-facts.mutations.mjs` (it rewrites the source and restores
|
|
5
|
-
* it, so it is not part of the plain test run).
|
|
6
|
-
*
|
|
7
|
-
* WHY IT EXISTS. A harness whose success state is silence cannot be observed working: «it passed»
|
|
8
|
-
* and «it cannot fail» produce identical output. Written 2026-09-12, when the mechanism moved into
|
|
9
|
-
* this package and `scripts/run-mutations.mjs` reported this harness as one nothing could kill.
|
|
10
|
-
*
|
|
11
|
-
* ⚠️ HONEST SCOPE: ONE case. It kills the checker outright — the shape that makes every other
|
|
12
|
-
* assertion in the harness vacuous at once — and that is the property worth holding first, not the
|
|
13
|
-
* only one worth holding. Narrower cases were probed and left out DELIBERATELY: the assertions
|
|
14
|
-
* they trip carry no message of their own (a bare deepEqual diff), so `expect` could only match
|
|
15
|
-
* text any unrelated failure also prints, and a case killed by someone else's assertion is not
|
|
16
|
-
* evidence about its own defect. Adding one means giving that assertion a message first.
|
|
17
|
-
*/
|
|
18
|
-
import { dirname, join } from "node:path";
|
|
19
|
-
import { fileURLToPath } from "node:url";
|
|
20
|
-
import { runMutations } from "../../../lib/mutation-driver.mjs";
|
|
21
|
-
import { consumerRoot } from "./consumer.mjs";
|
|
22
|
-
|
|
23
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
24
|
-
const SRC = join(HERE, "extract-ref-facts.mjs");
|
|
25
|
-
const HARNESS = join(HERE, "extract-ref-facts.harness.mjs");
|
|
26
|
-
|
|
27
|
-
const MUTATIONS = [
|
|
28
|
-
[
|
|
29
|
-
"the markdown reference list is never parsed",
|
|
30
|
-
[
|
|
31
|
-
[
|
|
32
|
-
SRC,
|
|
33
|
-
"export function parseMarkdownRefs(text) {",
|
|
34
|
-
"export function parseMarkdownRefs(text) {\n if (1) return [];",
|
|
35
|
-
],
|
|
36
|
-
],
|
|
37
|
-
"heading must close the reference list",
|
|
38
|
-
"the markdown leg entirely — a paper whose bibliography is prose yields zero refs and no complaint",
|
|
39
|
-
],
|
|
40
|
-
];
|
|
41
|
-
|
|
42
|
-
process.exit(
|
|
43
|
-
runMutations({
|
|
44
|
-
root: consumerRoot(),
|
|
45
|
-
runner: "vigiles",
|
|
46
|
-
cases: MUTATIONS.map(([name, edits, expect, disables]) => ({
|
|
47
|
-
name,
|
|
48
|
-
disables,
|
|
49
|
-
edits,
|
|
50
|
-
harness: HARNESS,
|
|
51
|
-
expect,
|
|
52
|
-
})),
|
|
53
|
-
}),
|
|
54
|
-
);
|
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* generated-code.mutations.mjs — proof that every rule and every guard in `generated-code.mjs` is
|
|
3
|
-
* watched FAILING. Run: `node .claude/skills/paper-pipeline/scripts/generated-code.mutations.mjs`
|
|
4
|
-
*
|
|
5
|
-
* This is not a test. It is the evidence behind the claim that the tests test something.
|
|
6
|
-
*
|
|
7
|
-
* WHY IT MATTERS MORE HERE THAN FOR A NORMAL CHECKER. Half of what this file asserts is that the
|
|
8
|
-
* checker STAYS QUIET — on a seeded script, on a lowercase route, on a reused variable name. A quiet
|
|
9
|
-
* assertion is the easiest kind to write vacuously: delete the rule it is guarding and the fixture
|
|
10
|
-
* still passes, because a checker that does nothing is quiet about everything. Only a mutation can
|
|
11
|
-
* tell those apart, and the ignore set has the same shape: an over-wide allow rule and a working one
|
|
12
|
-
* produce identical output.
|
|
13
|
-
*
|
|
14
|
-
* WHAT IT DOES. For each rule, it neuters exactly that rule in the source, runs the harness, and
|
|
15
|
-
* requires the harness to go RED at the named assertion. Then it restores the file.
|
|
16
|
-
*
|
|
17
|
-
* 🔴 The quiet cases need INVERTED mutations. Disabling a rule cannot make a "stays quiet" case go
|
|
18
|
-
* red — it makes it quieter. So those rows LOOSEN the guard instead (drop the anchor, add the `i`
|
|
19
|
-
* flag, accept any file mode), which is the mistake a future editor would actually make.
|
|
20
|
-
*
|
|
21
|
-
* The source is restored in a `finally`, and the run refuses to start on a dirty working tree.
|
|
22
|
-
*/
|
|
23
|
-
import { runMutations } from "../../../lib/mutation-driver.mjs";
|
|
24
|
-
import { dirname, join } from "node:path";
|
|
25
|
-
import { fileURLToPath } from "node:url";
|
|
26
|
-
import { consumerRoot } from "./consumer.mjs";
|
|
27
|
-
|
|
28
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
29
|
-
const ROOT = consumerRoot();
|
|
30
|
-
const SRC = join(HERE, "generated-code.mjs");
|
|
31
|
-
const HARNESS = join(HERE, "generated-code.harness.mjs");
|
|
32
|
-
|
|
33
|
-
/** [name, what it disables or loosens, exact source substring, replacement, expected red assertion] */
|
|
34
|
-
const MUTATIONS = [
|
|
35
|
-
// ── the three rules fire ──
|
|
36
|
-
[
|
|
37
|
-
"no-seed/rule",
|
|
38
|
-
"the unseeded-randomness rule",
|
|
39
|
-
" if (DRAWS.test(code) && !SEEDS.test(src))",
|
|
40
|
-
" if (false)",
|
|
41
|
-
"a sampling script with no seed passed",
|
|
42
|
-
],
|
|
43
|
-
[
|
|
44
|
-
"no-seed/python-draws",
|
|
45
|
-
"recognising `random.sample` as a draw",
|
|
46
|
-
"\\brandom\\.(?:random|sample|",
|
|
47
|
-
"\\brandom\\.(?:random|nope|",
|
|
48
|
-
"a sampling script with no seed passed",
|
|
49
|
-
],
|
|
50
|
-
[
|
|
51
|
-
"abs/rule",
|
|
52
|
-
"the hard-coded-absolute-path rule",
|
|
53
|
-
" const abs = code.match(\n new RegExp(ABS.source",
|
|
54
|
-
" const abs = false && code.match(\n new RegExp(ABS.source",
|
|
55
|
-
"a script that only runs on its author's laptop",
|
|
56
|
-
],
|
|
57
|
-
[
|
|
58
|
-
"abs/home",
|
|
59
|
-
"recognising /home/ as an absolute path",
|
|
60
|
-
"(?:\\/home\\/|\\/Users\\/",
|
|
61
|
-
"(?:\\/nope\\/|\\/Users\\/",
|
|
62
|
-
"a script that only runs on its author's laptop",
|
|
63
|
-
],
|
|
64
|
-
[
|
|
65
|
-
"inplace/rule",
|
|
66
|
-
"the in-place-overwrite rule",
|
|
67
|
-
" const both = [...operands(code, READS)].filter((p) =>\n operands(code, WRITES).has(p),\n );",
|
|
68
|
-
" const both = [];",
|
|
69
|
-
"a script that clobbers its own input passed",
|
|
70
|
-
],
|
|
71
|
-
[
|
|
72
|
-
"inplace/argv",
|
|
73
|
-
"the argv form of an in-place rewrite",
|
|
74
|
-
" /open\\(\\s*((?:sys\\.argv|process\\.argv)\\[\\d\\])\\s*\\)/g,",
|
|
75
|
-
" /never-matches-anything-x/g,",
|
|
76
|
-
"argv form of an in-place rewrite was not caught",
|
|
77
|
-
],
|
|
78
|
-
|
|
79
|
-
// ── the guards that keep it quiet on correct code (LOOSENED, not disabled) ──
|
|
80
|
-
[
|
|
81
|
-
"quiet/seed-recognised",
|
|
82
|
-
"recognising that a seed was set at all",
|
|
83
|
-
" if (DRAWS.test(code) && !SEEDS.test(src))",
|
|
84
|
-
" if (DRAWS.test(code))",
|
|
85
|
-
"a properly seeded script (explicit) was reported as unseeded",
|
|
86
|
-
],
|
|
87
|
-
// Disables BOTH alternations that could credit a seed threaded through argparse. Naming only
|
|
88
|
-
// `--seed` left the row alive on `\bSEED\b`, and a partial disable that another alternation
|
|
89
|
-
// covers is the same as no disable at all.
|
|
90
|
-
[
|
|
91
|
-
"quiet/seed-argparse",
|
|
92
|
-
"recognising a seed threaded through argparse (the leading-\\b regression)",
|
|
93
|
-
"--seed|\\bSEED\\b|Random",
|
|
94
|
-
"\\b--seed|\\bNOSUCHSEED\\b|Random",
|
|
95
|
-
"a properly seeded script (argparse) was reported as unseeded",
|
|
96
|
-
],
|
|
97
|
-
[
|
|
98
|
-
"quiet/abs-anchor",
|
|
99
|
-
"the left anchor that keeps a RELATIVE third-party path out",
|
|
100
|
-
"const ABS =\n /(?<![\\w./-])(?:",
|
|
101
|
-
"const ABS =\n /(?:",
|
|
102
|
-
"reported as absolute local paths",
|
|
103
|
-
],
|
|
104
|
-
[
|
|
105
|
-
"quiet/abs-case",
|
|
106
|
-
"case sensitivity, so an Express route /users/ stops matching /Users/",
|
|
107
|
-
"|[A-Z]:\\\\\\\\?[A-Za-z])/;",
|
|
108
|
-
"|[A-Z]:\\\\\\\\?[A-Za-z])/i;", // ← only bites since ABS.flags is threaded through
|
|
109
|
-
"reported as absolute local paths",
|
|
110
|
-
],
|
|
111
|
-
[
|
|
112
|
-
"quiet/read-mode",
|
|
113
|
-
"requiring a READ to not be a write (the prototype's vacuity bug)",
|
|
114
|
-
" /open\\(\\s*(['\"])([^'\"]+)\\1\\s*(?:,\\s*(['\"])r[b+]*\\3\\s*)?\\)/g,",
|
|
115
|
-
" /open\\(\\s*(['\"])([^'\"]+)\\1\\s*(?:,[^)]*)?\\)/g,",
|
|
116
|
-
"was reported as clobbering its input",
|
|
117
|
-
],
|
|
118
|
-
// 🔴 SPANS BOTH ARRAYS ON PURPOSE. Loosening only the READ side survived: the WRITE side still
|
|
119
|
-
// demanded a quoted literal, so no operand could ever pair and the verdict did not move. A
|
|
120
|
-
// mutation that cannot change the answer is not a mutation, and this is the second one this file
|
|
121
|
-
// caught in itself.
|
|
122
|
-
[
|
|
123
|
-
"quiet/literal-only",
|
|
124
|
-
"matching literals rather than operand expressions (17 false positives)",
|
|
125
|
-
" /readFileSync\\(\\s*(['\"])([^'\"]+)\\1/g,\n /Path\\(\\s*(['\"])([^'\"]+)\\1\\s*\\)\\s*\\.read/g,\n /open\\(\\s*((?:sys\\.argv|process\\.argv)\\[\\d\\])\\s*\\)/g,\n];\nconst WRITES = [\n /open\\(\\s*(['\"])([^'\"]+)\\1\\s*,\\s*['\"][wa]/g,\n /writeFileSync\\(\\s*(['\"])([^'\"]+)\\1/g,",
|
|
126
|
-
" /readFileSync\\(\\s*([^,)]+?)\\s*[,)]/g,\n /Path\\(\\s*(['\"])([^'\"]+)\\1\\s*\\)\\s*\\.read/g,\n /open\\(\\s*((?:sys\\.argv|process\\.argv)\\[\\d\\])\\s*\\)/g,\n];\nconst WRITES = [\n /open\\(\\s*(['\"])([^'\"]+)\\1\\s*,\\s*['\"][wa]/g,\n /writeFileSync\\(\\s*([^,)]+?)\\s*,/g,",
|
|
127
|
-
"reused across two loops was mistaken for dataflow",
|
|
128
|
-
],
|
|
129
|
-
|
|
130
|
-
// ── the boundary with check-anon.sh ──
|
|
131
|
-
[
|
|
132
|
-
"bundle/excluded",
|
|
133
|
-
"excluding the released bundle, which check-anon.sh cat. 5 owns",
|
|
134
|
-
" if (depth === 1 && name === bundleName) {\n swallowed.get(RULE[0]).push(name);\n continue;\n }",
|
|
135
|
-
" if (false) {\n swallowed.get(RULE[0]).push(name);\n continue;\n }",
|
|
136
|
-
"reported an absolute path inside the RELEASED bundle",
|
|
137
|
-
], // case 8, its own assertion
|
|
138
|
-
[
|
|
139
|
-
"bundle/named-in-ledger",
|
|
140
|
-
"naming that exclusion in the ledger",
|
|
141
|
-
' [\n "the released bundle — `check-anon.sh` cat. 5 owns it, and it asks a different question there " +\n "(does it leak an identity, not will it run elsewhere)",\n [],\n ],',
|
|
142
|
-
' [\n "a tree we do not enter",\n [],\n ],',
|
|
143
|
-
"the boundary invisible",
|
|
144
|
-
],
|
|
145
|
-
|
|
146
|
-
// ── the ignore set ──
|
|
147
|
-
[
|
|
148
|
-
"allow/applied",
|
|
149
|
-
"applying an allow row at all",
|
|
150
|
-
" if (reason !== undefined) {\n ignored.push(",
|
|
151
|
-
" if (false) {\n ignored.push(",
|
|
152
|
-
"the allowed kind still fired",
|
|
153
|
-
],
|
|
154
|
-
[
|
|
155
|
-
"allow/per-kind",
|
|
156
|
-
"scoping an allowance to ONE KIND rather than to the whole file",
|
|
157
|
-
" const reason = allowed.get(key) ?? allowed.get(`${relative(repro, f)}\\t*`);",
|
|
158
|
-
" const reason = allowed.get(key) ?? [...allowed.entries()].find(([k]) => k.startsWith(relative(repro, f)))?.[1];",
|
|
159
|
-
"also silenced its unseeded sampling",
|
|
160
|
-
],
|
|
161
|
-
[
|
|
162
|
-
"allow/reason-printed",
|
|
163
|
-
"printing each allowance's reason",
|
|
164
|
-
"for (const [key, reason] of ignored) ledger.push(` ${key} → ${reason}`);",
|
|
165
|
-
"for (const [key, reason] of ignored) ledger.push(` ${key}`);",
|
|
166
|
-
"its reason was not printed",
|
|
167
|
-
],
|
|
168
|
-
[
|
|
169
|
-
"allow/stale",
|
|
170
|
-
"reporting a row that names no script",
|
|
171
|
-
" if (!live.has(key))",
|
|
172
|
-
" if (false)",
|
|
173
|
-
"sat unreported, so the list never shrinks",
|
|
174
|
-
],
|
|
175
|
-
[
|
|
176
|
-
"allow/empty-said",
|
|
177
|
-
"saying so when nothing is being waved through",
|
|
178
|
-
'if (!ignored.length) ledger.push(" (nothing is being waved through)");',
|
|
179
|
-
'if (false) ledger.push(" (nothing is being waved through)");',
|
|
180
|
-
"an empty allow file printed nothing",
|
|
181
|
-
],
|
|
182
|
-
|
|
183
|
-
// ── WHERE repro/ IS, and what silence is allowed to mean ──
|
|
184
|
-
// 🔴 These three guard a defect that was LIVE until 2026-08-26 and produced no symptom: pointed
|
|
185
|
-
// at `<paper-e>`, the check exited 0 with no output while 18 findings sat in
|
|
186
|
-
// `typed-shell/repro/`. A missed directory is worse than a missed finding, because it is
|
|
187
|
-
// reported in exactly the words of a clean paper.
|
|
188
|
-
[
|
|
189
|
-
"scope/nested-root",
|
|
190
|
-
"looking for repro/ one level below the paper directory",
|
|
191
|
-
' .map((e) => join(dir, e.name, "repro"))',
|
|
192
|
-
' .map((e) => join(dir, e.name, "nope"))',
|
|
193
|
-
"a repro/ one level below the paper directory was not scanned",
|
|
194
|
-
],
|
|
195
|
-
[
|
|
196
|
-
"scope/loud-empty",
|
|
197
|
-
"saying so when there is no repro/ to scan (silence would read as clean)",
|
|
198
|
-
" if (!flagsOnly)\n console.log(\n `🧪 generated-code — 0 finding(s) in ${dir}\\n NOTHING WAS SCANNED:",
|
|
199
|
-
" if (false)\n console.log(\n `🧪 generated-code — 0 finding(s) in ${dir}\\n NOTHING WAS SCANNED:",
|
|
200
|
-
"produced no output at all",
|
|
201
|
-
],
|
|
202
|
-
[
|
|
203
|
-
"scope/second-root-named",
|
|
204
|
-
"naming a SECOND repro/ root in the ledger instead of dropping it",
|
|
205
|
-
"for (const other of roots.slice(1))\n swallowed.get(RULE[3]).push(relative(dir, other));",
|
|
206
|
-
"for (const other of [])\n swallowed.get(RULE[3]).push(relative(dir, other));",
|
|
207
|
-
"was dropped without being named",
|
|
208
|
-
],
|
|
209
|
-
|
|
210
|
-
// ── the machine-readable contract with run-mechanical.mjs ──
|
|
211
|
-
[
|
|
212
|
-
"out/stated-count",
|
|
213
|
-
"the stated finding count in the header",
|
|
214
|
-
"const header = `🧪 generated-code — ${findings.length} finding(s) in ${dir}`;",
|
|
215
|
-
"const header = `🧪 generated-code — ${dir}`;",
|
|
216
|
-
"does not state `— 0 finding(s)`",
|
|
217
|
-
],
|
|
218
|
-
[
|
|
219
|
-
"out/ledger-on-clean",
|
|
220
|
-
"printing the ignore ledger when there are no findings",
|
|
221
|
-
' console.log(\n " every analysis script seeds its randomness, names only relative paths, and does not clobber its input",\n );\n ledger.forEach((l) => console.log(l));',
|
|
222
|
-
' console.log(\n " every analysis script seeds its randomness, names only relative paths, and does not clobber its input",\n );',
|
|
223
|
-
// Killed by case 8's ledger assertion rather than case 11's: case 8 is also a 0-finding
|
|
224
|
-
// report-mode run and it runs first. Both assert the property; the row records WHICH one caught
|
|
225
|
-
// it so this is not misread as an off-target kill.
|
|
226
|
-
"the boundary invisible",
|
|
227
|
-
],
|
|
228
|
-
[
|
|
229
|
-
"out/flags-quiet",
|
|
230
|
-
"the silence of --flags-only on a clean tree",
|
|
231
|
-
"if (!flagsOnly) {\n console.log(header);",
|
|
232
|
-
"if (true) {\n console.log(header);",
|
|
233
|
-
"--flags-only printed something about a clean tree",
|
|
234
|
-
],
|
|
235
|
-
];
|
|
236
|
-
|
|
237
|
-
// The table above is DATA about this checker; the engine below is shared with the other mutation
|
|
238
|
-
// files (`mutation-driver.mjs`). The split gave this file two protections it never had: the no-op
|
|
239
|
-
// guard (a replacement equal to the original leaves a green harness proving nothing) and one retry
|
|
240
|
-
// on a non-kill (a flake reported as a survivor sends the next reader to rewrite a working
|
|
241
|
-
// assertion). It also refuses to start when a named harness does not exist — the defect that made
|
|
242
|
-
// three files report SURVIVED for every case after the 2026-08-11 colocation.
|
|
243
|
-
process.exit(
|
|
244
|
-
runMutations({
|
|
245
|
-
root: ROOT,
|
|
246
|
-
cases: MUTATIONS.map(([name, disables, find, replace, expect]) => ({
|
|
247
|
-
name,
|
|
248
|
-
disables,
|
|
249
|
-
edits: [[SRC, find, replace]],
|
|
250
|
-
harness: HARNESS,
|
|
251
|
-
expect,
|
|
252
|
-
})),
|
|
253
|
-
}),
|
|
254
|
-
);
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pipeline-check.mutations.mjs — plant the defect `pipeline-check.harness.mjs` claims to catch, and require it to die.
|
|
3
|
-
*
|
|
4
|
-
* Run: `node skills/paper-pipeline/scripts/pipeline-check.mutations.mjs` (it rewrites the source and restores
|
|
5
|
-
* it, so it is not part of the plain test run).
|
|
6
|
-
*
|
|
7
|
-
* WHY IT EXISTS. A harness whose success state is silence cannot be observed working: «it passed»
|
|
8
|
-
* and «it cannot fail» produce identical output. Written 2026-09-12, when the mechanism moved into
|
|
9
|
-
* this package and `scripts/run-mutations.mjs` reported this harness as one nothing could kill.
|
|
10
|
-
*
|
|
11
|
-
* ⚠️ HONEST SCOPE: ONE case. It kills the checker outright — the shape that makes every other
|
|
12
|
-
* assertion in the harness vacuous at once — and that is the property worth holding first, not the
|
|
13
|
-
* only one worth holding. Narrower cases were probed and left out DELIBERATELY: the assertions
|
|
14
|
-
* they trip carry no message of their own (a bare deepEqual diff), so `expect` could only match
|
|
15
|
-
* text any unrelated failure also prints, and a case killed by someone else's assertion is not
|
|
16
|
-
* evidence about its own defect. Adding one means giving that assertion a message first.
|
|
17
|
-
*/
|
|
18
|
-
import { dirname, join } from "node:path";
|
|
19
|
-
import { fileURLToPath } from "node:url";
|
|
20
|
-
import { runMutations } from "../../../lib/mutation-driver.mjs";
|
|
21
|
-
import { consumerRoot } from "./consumer.mjs";
|
|
22
|
-
|
|
23
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
24
|
-
const SRC = join(HERE, "pipeline-check.mjs");
|
|
25
|
-
const HARNESS = join(HERE, "pipeline-check.harness.mjs");
|
|
26
|
-
|
|
27
|
-
const MUTATIONS = [
|
|
28
|
-
[
|
|
29
|
-
"check() goes blind",
|
|
30
|
-
[
|
|
31
|
-
[
|
|
32
|
-
SRC,
|
|
33
|
-
"export function check({ sections, header }, { sourceDate, today, dir }) {",
|
|
34
|
-
"export function check({ sections, header }, { sourceDate, today, dir }) {\n if (1) return [];",
|
|
35
|
-
],
|
|
36
|
-
],
|
|
37
|
-
"stale-continuous: wrong finding set",
|
|
38
|
-
"every gate-staleness finding: a PIPELINE-STATUS that reports nothing reads as a clean one",
|
|
39
|
-
],
|
|
40
|
-
];
|
|
41
|
-
|
|
42
|
-
process.exit(
|
|
43
|
-
runMutations({
|
|
44
|
-
root: consumerRoot(),
|
|
45
|
-
runner: "vigiles",
|
|
46
|
-
cases: MUTATIONS.map(([name, edits, expect, disables]) => ({
|
|
47
|
-
name,
|
|
48
|
-
disables,
|
|
49
|
-
edits,
|
|
50
|
-
harness: HARNESS,
|
|
51
|
-
expect,
|
|
52
|
-
})),
|
|
53
|
-
}),
|
|
54
|
-
);
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* population-map.mutations.mjs — plant each defect `population-map.harness.mjs` claims to catch, and require it to die.
|
|
3
|
-
*
|
|
4
|
-
* Run: `node skills/paper-pipeline/scripts/population-map.mutations.mjs` (~1 min; it rewrites the source and
|
|
5
|
-
* restores it, so it is not part of the plain test run).
|
|
6
|
-
*
|
|
7
|
-
* WHY IT EXISTS. A harness whose success state is silence cannot be observed working: «it passed»
|
|
8
|
-
* and «it cannot fail» produce identical output. This battery is the difference. It was written on
|
|
9
|
-
* 2026-09-12, when the mechanism moved into this package and `scripts/run-mutations.mjs` reported
|
|
10
|
-
* this harness as one nothing could kill — a gap that had been open for as long as the harness had
|
|
11
|
-
* existed, invisible because nothing asked the question.
|
|
12
|
-
*
|
|
13
|
-
* Every replacement below differs from the text it replaces (the driver refuses a no-op, which
|
|
14
|
-
* would leave a green harness proving nothing), every case names the assertion that must fire, and
|
|
15
|
-
* the driver verifies the patch actually landed before believing a result.
|
|
16
|
-
*/
|
|
17
|
-
import { dirname, join } from "node:path";
|
|
18
|
-
import { fileURLToPath } from "node:url";
|
|
19
|
-
import { runMutations } from "../../../lib/mutation-driver.mjs";
|
|
20
|
-
import { consumerRoot } from "./consumer.mjs";
|
|
21
|
-
|
|
22
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
23
|
-
const SRC = join(HERE, "population-map.mjs");
|
|
24
|
-
const HARNESS = join(HERE, "population-map.harness.mjs");
|
|
25
|
-
|
|
26
|
-
const MUTATIONS = [
|
|
27
|
-
[
|
|
28
|
-
"findings() goes blind",
|
|
29
|
-
[
|
|
30
|
-
[
|
|
31
|
-
SRC,
|
|
32
|
-
"export function findings(md, tsv) {",
|
|
33
|
-
"export function findings(md, tsv) {\n if (1) return [];",
|
|
34
|
-
],
|
|
35
|
-
],
|
|
36
|
-
"population-map.selftest.mjs failed",
|
|
37
|
-
"the whole checker: a registry map that reports nothing reads exactly like a clean paper",
|
|
38
|
-
],
|
|
39
|
-
[
|
|
40
|
-
"the registry is never parsed",
|
|
41
|
-
[
|
|
42
|
-
[
|
|
43
|
-
SRC,
|
|
44
|
-
"export function parseRegistry(tsv) {",
|
|
45
|
-
"export function parseRegistry(tsv) {\n if (1) return [];",
|
|
46
|
-
],
|
|
47
|
-
],
|
|
48
|
-
"population-map.selftest.mjs failed",
|
|
49
|
-
"every row-level finding — an empty registry makes stale rows and dangling refs unrepresentable",
|
|
50
|
-
],
|
|
51
|
-
];
|
|
52
|
-
|
|
53
|
-
process.exit(
|
|
54
|
-
runMutations({
|
|
55
|
-
root: consumerRoot(),
|
|
56
|
-
runner: "vigiles",
|
|
57
|
-
cases: MUTATIONS.map(([name, edits, expect, disables]) => ({
|
|
58
|
-
name,
|
|
59
|
-
disables,
|
|
60
|
-
edits,
|
|
61
|
-
harness: HARNESS,
|
|
62
|
-
expect,
|
|
63
|
-
})),
|
|
64
|
-
}),
|
|
65
|
-
);
|