@spardutti/claude-skills 2.23.0 → 2.24.0
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/lib/scaffold-stryker.mjs +40 -38
- package/lib/setup-hook.mjs +8 -1
- package/package.json +1 -1
package/lib/scaffold-stryker.mjs
CHANGED
|
@@ -14,36 +14,31 @@
|
|
|
14
14
|
import { readFile, writeFile, access } from "node:fs/promises";
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
|
|
17
|
+
// ignoreStatic is the whole reason a first run is survivable. A string in a
|
|
18
|
+
// module-level array or object is a static mutant, evaluated once at file load,
|
|
19
|
+
// and Stryker's docs say testing one "often requires running all tests". A table
|
|
20
|
+
// of 11 headings and 16 term rows produced 57 survivors and a gate that timed
|
|
21
|
+
// out past 600s; with this set the same gate ran in 1m33s and the survivors were
|
|
22
|
+
// gone without a single test written for them. It requires perTest coverage
|
|
23
|
+
// analysis, which is why the two belong together.
|
|
24
|
+
//
|
|
25
|
+
// A shipped attempt to solve this in the ignorer — skip string mutants inside a
|
|
26
|
+
// module-level array or object literal — was exactly the definition of a static
|
|
27
|
+
// mutant, so it did the same job worse. It is gone; this option replaces it.
|
|
17
28
|
const CONFIG = {
|
|
18
29
|
$schema: "./node_modules/@stryker-mutator/core/schema/stryker-schema.json",
|
|
19
30
|
testRunner: "vitest",
|
|
20
31
|
plugins: ["@stryker-mutator/vitest-runner", "./stryker-classname-ignorer.mjs"],
|
|
21
32
|
ignorers: ["tailwind-classnames"],
|
|
22
33
|
coverageAnalysis: "perTest",
|
|
34
|
+
ignoreStatic: true,
|
|
23
35
|
tempDirName: ".stryker-tmp",
|
|
24
36
|
};
|
|
25
37
|
|
|
26
38
|
// Bumped whenever IGNORER changes. A project scaffolded before a rule existed
|
|
27
39
|
// has no other way to learn about it, and the file is generated, not authored —
|
|
28
40
|
// so a missing marker means overwrite, not keep.
|
|
29
|
-
const IGNORER_VERSION = "
|
|
30
|
-
|
|
31
|
-
// Exported and then serialised into the generated ignorer below, rather than
|
|
32
|
-
// written twice. A copy in the template and a copy under test would agree with
|
|
33
|
-
// each other and disagree with the shipped file.
|
|
34
|
-
export function inDataTable(path) {
|
|
35
|
-
let found = false;
|
|
36
|
-
for (let p = path; p; p = p.parentPath) {
|
|
37
|
-
if (p.isFunction()) return false;
|
|
38
|
-
if (!found && p.isVariableDeclarator()) {
|
|
39
|
-
const init = p.node.init;
|
|
40
|
-
if (!init) return false;
|
|
41
|
-
if (init.type !== "ArrayExpression" && init.type !== "ObjectExpression") return false;
|
|
42
|
-
found = true;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return found;
|
|
46
|
-
}
|
|
41
|
+
const IGNORER_VERSION = "v3";
|
|
47
42
|
|
|
48
43
|
// .mjs, not .js: an app without "type": "module" in its package.json cannot
|
|
49
44
|
// load ESM from a .js file, and the plugin is ESM.
|
|
@@ -53,19 +48,10 @@ const IGNORER = `// Generated by @spardutti/claude-skills — ignorer ${IGNORER_
|
|
|
53
48
|
// Ignore by WHERE the mutant sits, not by what kind it is. Disabling the
|
|
54
49
|
// StringLiteral mutator globally would also stop mutating labels, keys and
|
|
55
50
|
// separators — strings whose value is behaviour.
|
|
56
|
-
import { PluginKind, declareValuePlugin } from "@stryker-mutator/api/plugin";
|
|
57
|
-
|
|
58
|
-
// A string in a module-level array or object literal is copy: a heading, a
|
|
59
|
-
// label, a term row. Mutating it produces 'Pizzas' -> 'Stryker was here!',
|
|
60
|
-
// which can only be killed by asserting the table against a copy of itself —
|
|
61
|
-
// the tautology the testing skill exists to stop. One table of 11 headings and
|
|
62
|
-
// 16 rows produced 57 such survivors and a fifteen-minute gate.
|
|
63
51
|
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
// behaviour even when it sits in data.
|
|
68
|
-
${inDataTable.toString()}
|
|
52
|
+
// Content in a module-level table is NOT handled here. Those are static mutants,
|
|
53
|
+
// and \`ignoreStatic\` in stryker.config.json covers them for less.
|
|
54
|
+
import { PluginKind, declareValuePlugin } from "@stryker-mutator/api/plugin";
|
|
69
55
|
|
|
70
56
|
export const strykerPlugins = [
|
|
71
57
|
declareValuePlugin(PluginKind.Ignore, "tailwind-classnames", {
|
|
@@ -74,8 +60,6 @@ export const strykerPlugins = [
|
|
|
74
60
|
return "styling, not behaviour";
|
|
75
61
|
if (path.find((p) => p.isCallExpression() && p.node.callee.name === "cn"))
|
|
76
62
|
return "styling, not behaviour";
|
|
77
|
-
if (path.isStringLiteral() && inDataTable(path))
|
|
78
|
-
return "copy in a data table, not behaviour";
|
|
79
63
|
},
|
|
80
64
|
}),
|
|
81
65
|
];
|
|
@@ -128,13 +112,31 @@ export async function scaffoldStryker(projectDir, rel) {
|
|
|
128
112
|
}
|
|
129
113
|
if (cfg === null) {
|
|
130
114
|
unreadable.push(`${at}stryker.config.json`);
|
|
131
|
-
} else if ((cfg.ignorers ?? []).includes("tailwind-classnames")) {
|
|
132
|
-
kept.push(`${at}stryker.config.json`);
|
|
133
115
|
} else {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
116
|
+
const needsIgnorer = !(cfg.ignorers ?? []).includes("tailwind-classnames");
|
|
117
|
+
|
|
118
|
+
// ignoreStatic is only correct under perTest coverage analysis. Absent, we
|
|
119
|
+
// set both. Already perTest, we add the flag. Anything else was a
|
|
120
|
+
// deliberate choice and stays — silently switching it would change which
|
|
121
|
+
// mutants the project reports, which is not ours to decide.
|
|
122
|
+
const coverage = cfg.coverageAnalysis;
|
|
123
|
+
const canSetStatic = coverage === undefined || coverage === "perTest";
|
|
124
|
+
const needsStatic = cfg.ignoreStatic !== true && canSetStatic;
|
|
125
|
+
|
|
126
|
+
if (!needsIgnorer && !needsStatic) {
|
|
127
|
+
kept.push(`${at}stryker.config.json`);
|
|
128
|
+
} else {
|
|
129
|
+
if (needsIgnorer) {
|
|
130
|
+
cfg.plugins = [...new Set([...(cfg.plugins ?? []), "./stryker-classname-ignorer.mjs"])];
|
|
131
|
+
cfg.ignorers = [...new Set([...(cfg.ignorers ?? []), "tailwind-classnames"])];
|
|
132
|
+
}
|
|
133
|
+
if (needsStatic) {
|
|
134
|
+
cfg.coverageAnalysis = "perTest";
|
|
135
|
+
cfg.ignoreStatic = true;
|
|
136
|
+
}
|
|
137
|
+
await writeFile(configPath, JSON.stringify(cfg, null, 2) + "\n");
|
|
138
|
+
patched.push(`${at}stryker.config.json`);
|
|
139
|
+
}
|
|
138
140
|
}
|
|
139
141
|
} else {
|
|
140
142
|
await writeFile(configPath, JSON.stringify(CONFIG, null, 2) + "\n");
|
package/lib/setup-hook.mjs
CHANGED
|
@@ -663,6 +663,13 @@ GAUNTLET_MUTATE=""
|
|
|
663
663
|
GAUNTLET_SOURCE_EXT="ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|rb|php|c|h|cpp|hpp|cs|swift|gd"
|
|
664
664
|
GAUNTLET_IGNORE_EXT="md|mdx|txt|rst|adoc|json|ya?ml|toml|lock|cfg|ini|env|csv|tsv|sql|html|css|scss|svg|png|jpg|jpeg|gif|webp|ico|pdf|woff2?|ttf|otf|mp3|mp4|wav|zip|gz|tres|tscn|import|godot"
|
|
665
665
|
GAUNTLET_IGNORE_FILES="*.gen.ts *.gen.tsx *.generated.* */migrations/*.py */alembic/versions/*.py */components/ui/*.tsx */*.config.*"
|
|
666
|
+
# Length-checked and skill-audited like everything else, but not mutated.
|
|
667
|
+
# Mutation pays on logic and burns time on presentation: a component's mutants
|
|
668
|
+
# are class names, copy and JSX shape, none of which is behaviour, and one
|
|
669
|
+
# session spent an evening killing them. Logic that lives inside a component is
|
|
670
|
+
# logic that wants extracting — which the 200-line rule already pushes toward.
|
|
671
|
+
# Override in .claude/gauntlet.conf to mutate components too.
|
|
672
|
+
GAUNTLET_NO_MUTATE='\\.(tsx|jsx)$'
|
|
666
673
|
[ -n "\${HOME:-}" ] && [ -f "$HOME/.claude/gauntlet.conf" ] && . "$HOME/.claude/gauntlet.conf"
|
|
667
674
|
[ -f ".claude/gauntlet.conf" ] && . ".claude/gauntlet.conf"
|
|
668
675
|
|
|
@@ -825,7 +832,7 @@ for owner in $OWNERS; do
|
|
|
825
832
|
[ "$owner" = "." ] && label="<repo root>" || label="$owner/"
|
|
826
833
|
OWNED=$(while IFS= read -r f; do
|
|
827
834
|
[ "$(owner_of "$f")" = "$owner" ] && printf '%s\\n' "$f"
|
|
828
|
-
done <<< "$FILES")
|
|
835
|
+
done <<< "$FILES" | grep -vE "$GAUNTLET_NO_MUTATE" || true)
|
|
829
836
|
[ -z "$OWNED" ] && continue
|
|
830
837
|
|
|
831
838
|
# ONE comma-joined --mutate value, never a flag per range. Stryker's CLI
|