@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 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 `/preplan` |
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 |
@@ -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 = "v2";
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
- // Any function between the string and the top disqualifies it: that is code
65
- // building a value, not a table of content. A non-string mutant in the same
66
- // table is left alone, because a number or a flag in a config object is
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
- cfg.plugins = [...new Set([...(cfg.plugins ?? []), "./stryker-classname-ignorer.mjs"])];
135
- cfg.ignorers = [...new Set([...(cfg.ignorers ?? []), "tailwind-classnames"])];
136
- await writeFile(configPath, JSON.stringify(cfg, null, 2) + "\n");
137
- patched.push(`${at}stryker.config.json`);
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");
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spardutti/claude-skills",
3
- "version": "2.23.0",
3
+ "version": "2.25.0",
4
4
  "description": "CLI to install Claude Code skills from the claude-skills collection",
5
5
  "type": "module",
6
6
  "repository": {