@spardutti/claude-skills 2.23.0 → 2.25.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/README.md +1 -2
- package/lib/scaffold-stryker.mjs +40 -38
- package/lib/setup-hook.mjs +8 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -101,8 +101,7 @@ Portable slash commands installed to `.claude/commands/`. Some orchestrate paral
|
|
|
101
101
|
| Command | What it does |
|
|
102
102
|
|---------|--------------|
|
|
103
103
|
| `/ship` | Unified delivery pipeline — commit → gate → PR → merge → release. The gate is the one enforcement moment: it checks the diff's file lengths, audits it against the skills installed in the project, and mutation-tests the changed lines to prove the tests would catch a break — **fixing what it finds** rather than handing you a list. It runs before the **PR**, not before every commit: its scope is the whole branch, so gating each commit re-mutated every file the branch had ever touched. `--force` skips it. No argument steps through interactively; `/ship pr` runs through PR creation; `/ship release` runs the full pipeline |
|
|
104
|
-
| `/discover` | Find the right problem before deciding what to build — diverges first: generates competing framings through blind subagents under forced constraints, stress-tests the winner with a blind critic, and refuses to converge until every open question is answered or deferred. Run before `/
|
|
105
|
-
| `/preplan` | Resolve a fuzzy feature idea into concrete decisions — 6 fixed phases, one question at a time, ends with a decision log. Run before `/plan-feature` |
|
|
104
|
+
| `/discover` | Find the right problem before deciding what to build — diverges first: generates competing framings through blind subagents under forced constraints, stress-tests the winner with a blind critic, and refuses to converge until every open question is answered or deferred, then drafts scope, non-goals, edge cases and success criteria for you to correct. Run before `/plan-feature` |
|
|
106
105
|
| `/plan-feature` | Integration-first feature planning — 3 parallel subagents scan for reusable code, patterns, and touch points before producing a short plan |
|
|
107
106
|
| `/refactor` | Detect size / complexity / duplication / coupling issues via 4 parallel subagents, then refactor |
|
|
108
107
|
| `/deep-review` | Multi-agent deep code review — 5 parallel subagents catch guard bypasses, lost async state, wrong-table queries, dead references, protocol violations |
|
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
|