From d941b6637078642330d394e86ab98977276a02ef Mon Sep 17 00:00:00 2001 From: iliassjabali Date: Sun, 12 Apr 2026 01:59:07 +0100 Subject: [PATCH 01/26] chore: ignore .worktrees/ directory --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 9c83c17..26c5207 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,9 @@ packages/sdk-python/.venv/ # ── Operator (Python) ───────────────────────────────────────────────────────── +# ── Worktrees ───────────────────────────────────────────────────────────────── +.worktrees/ + # ── Misc ────────────────────────────────────────────────────────────────────── *.ots commit.txt From 9e3e1d7a03477674ee2837a0aa44e77fcb96152c Mon Sep 17 00:00:00 2001 From: iliassjabali Date: Sun, 12 Apr 2026 02:01:04 +0100 Subject: [PATCH 02/26] docs: add mutation testing design spec --- .../2026-04-12-mutation-testing-design.md | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-12-mutation-testing-design.md diff --git a/docs/superpowers/specs/2026-04-12-mutation-testing-design.md b/docs/superpowers/specs/2026-04-12-mutation-testing-design.md new file mode 100644 index 0000000..98dc86f --- /dev/null +++ b/docs/superpowers/specs/2026-04-12-mutation-testing-design.md @@ -0,0 +1,202 @@ +# Mutation Testing for AgentSpec — Design Spec + +**Status**: approved, ready for implementation +**Author**: brainstorm session 2026-04-12 +**Tracking**: new PR from `main` (no upstream issue) + +## Why + +Issue #24 surfaced that test counts don't measure test strength. PR #46 added 30 new tests but two of the most security-relevant ones (`extractFileRefs` path-traversal guard, `sanitizeContextContent` tag-breakout escape) assert with `expect(ctx).not.toContain('context_file')` — a no-op that stays green if the guard ever silently breaks. + +Mutation testing exposes exactly that failure mode: it modifies production code in small ways (negate a condition, drop a statement, change `>` to `>=`) and re-runs the suite. If the suite still passes, the mutant "survives" and the test is too weak. The mutation score is the percentage of mutants killed. + +We want this on every PR. We want it cached so cost-per-PR is acceptable. We want it to gate merge when the score drops below a threshold so the signal can't be ignored. + +## Goals + +- Catch weak assertions across the workspace, especially in security-adjacent code (`adapter-claude` `extractFileRefs`, `sanitizeContextContent`, `extractGeneratedAgent`). +- Establish a per-package mutation score baseline visible on every PR via GitHub check statuses. +- Block merge when a package's mutation score falls below its threshold. +- Keep cache hits cheap enough that PRs touching only docs or unrelated packages don't pay a meaningful runtime cost. + +## Non-goals + +- Mutation testing of the docs site, schema-export script, or generated `dist/` artefacts. +- Per-PR comments with score history or charts. +- Automatic threshold ratcheting. +- Cron baseline run on `main` (PR-time gate is sufficient at this stage). +- Mutation testing of integration tests that spawn pre-built binaries (`cli.test.ts` spawns `dist/cli.js`, which doesn't see source mutations — these tests are excluded from the Stryker test set). + +## Architecture + +### Single root config + +One `stryker.config.json` at the repo root holds everything that's identical across packages: + +```jsonc +{ + "$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json", + "packageManager": "pnpm", + "testRunner": "vitest", + "reporters": ["html", "progress", "clear-text"], + "coverageAnalysis": "perTest", + "incremental": true, + "thresholds": { + "high": 75, + "low": 60, + "break": 60 + }, + "mutate": [ + "packages/*/src/**/*.ts", + "!packages/*/src/**/*.test.ts", + "!packages/*/src/**/__tests__/**", + "!packages/*/src/**/*.d.ts", + "!packages/*/dist/**", + "!packages/sdk/src/scripts/export-schema.ts" + ], + "tempDirName": ".stryker-tmp", + "htmlReporter": { "fileName": "reports/mutation/index.html" } +} +``` + +Per-package overrides happen at invocation time via CLI flags (see scripts below). This keeps the config file as the single source of truth and makes per-package divergence visible in the npm scripts that consume it. + +### Per-package npm scripts (in root `package.json`) + +```json +{ + "scripts": { + "mutation:adapter-claude": "stryker run --mutate 'packages/adapter-claude/src/**/*.ts' --thresholds.break 70 --incrementalFile .stryker-tmp/adapter-claude-incremental.json --htmlReporter.fileName reports/mutation/adapter-claude.html", + "mutation:sdk": "stryker run --mutate 'packages/sdk/src/**/*.ts' --incrementalFile .stryker-tmp/sdk-incremental.json --htmlReporter.fileName reports/mutation/sdk.html", + "mutation:mcp-server": "stryker run --mutate 'packages/mcp-server/src/**/*.ts' --incrementalFile .stryker-tmp/mcp-server-incremental.json --htmlReporter.fileName reports/mutation/mcp-server.html", + "mutation:cli": "stryker run --mutate 'packages/cli/src/**/*.ts' --incrementalFile .stryker-tmp/cli-incremental.json --htmlReporter.fileName reports/mutation/cli.html", + "mutation:sidecar": "stryker run --mutate 'packages/sidecar/src/**/*.ts' --incrementalFile .stryker-tmp/sidecar-incremental.json --htmlReporter.fileName reports/mutation/sidecar.html", + "mutation": "pnpm mutation:adapter-claude && pnpm mutation:sdk && pnpm mutation:mcp-server && pnpm mutation:cli && pnpm mutation:sidecar" + } +} +``` + +`pnpm mutation` is for local "give me the full picture" use. CI calls the per-package scripts in parallel via the matrix. + +### Per-package thresholds + +| Package | `break` (gate) | Rationale | +|---|---|---| +| adapter-claude | **70** | Pure functions, no I/O, dense test coverage from PR #46. Should clear 70 comfortably. Higher gate makes the gate meaningful. | +| sdk | 60 | Larger surface, some I/O paths (loaders, resolvers). 60 is conservative starting point. | +| mcp-server | 60 | Many tools are thin spawnCli wrappers (low mutation value); transport tests do exercise real code paths. | +| cli | 60 | Largest package, lots of command wiring. Some integration tests excluded (see below). | +| sidecar | 60 | HTTP server with mocked dependencies. | + +Thresholds are starting points. The first CI run on this PR determines whether they're realistic. If a package fails its own threshold on first run, the threshold drops to `(actual − 5)` for that package and the PR description notes the discrepancy for reviewers. + +### Test scope per package + +Most packages: all tests run under Stryker. + +Two carve-outs: + +- **`cli`**: `src/__tests__/cli.test.ts` spawns the pre-built `dist/cli.js` via `child_process.spawn`. Mutations applied to source files in Stryker's sandbox don't reach `dist/`, so this test can't validate them. Stryker's per-test coverage analysis would mark every CLI source mutant as "no coverage" and treat them as survived. **Action**: exclude `cli.test.ts` from the Stryker test set via `--vitest.configFile` pointing at a Stryker-specific vitest config that omits this file. All other cli tests run. +- **`mcp-server`**: `src/__tests__/transport.test.ts` spawns `tsx src/index.ts`, which DOES see mutations live (tsx loads from the Stryker sandbox copy). Keep this test in the Stryker set, but it'll be the slowest single test (~5s × 18 tests × N mutants). The vitest config scope already covers this. + +### CI workflow + +`.github/workflows/mutation.yml`: + +```yaml +name: mutation +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + mutation: + name: mutation / ${{ matrix.package }} + runs-on: ubuntu-latest + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + package: [adapter-claude, sdk, mcp-server, cli, sidecar] + steps: + - uses: actions/checkout@v4 + with: { fetch-depth: 1 } + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: { node-version: '20', cache: 'pnpm' } + - run: pnpm install --frozen-lockfile + - run: pnpm -r build + - name: Restore Stryker incremental cache + uses: actions/cache@v4 + with: + path: .stryker-tmp/${{ matrix.package }}-incremental.json + key: stryker-${{ matrix.package }}-${{ hashFiles(format('packages/{0}/src/**', matrix.package), 'stryker.config.json') }} + restore-keys: | + stryker-${{ matrix.package }}- + - name: Run Stryker + run: pnpm mutation:${{ matrix.package }} + - name: Upload HTML report + if: always() + uses: actions/upload-artifact@v4 + with: + name: mutation-report-${{ matrix.package }} + path: reports/mutation/${{ matrix.package }}.html +``` + +**Triggers**: PR open / synchronize / reopen. Not on push to main (PRs gate it; main is implicitly green). + +**Always all 5 jobs**: never conditional on changed paths. A package whose source didn't change gets a near-instant cache hit, and the check status still appears so the PR has a complete picture. + +**Three caching layers**: +1. **pnpm store**: `actions/setup-node` with `cache: 'pnpm'`. Standard, ~30s saved per job. +2. **Build outputs** (`packages/*/dist`): inherited via the `pnpm -r build` step. Could be cached separately later if it becomes a hotspot; not in v1. +3. **Stryker incremental file** (`.stryker-tmp/-incremental.json`): the big one. Keyed per package on `hashFiles('packages//src/**', 'stryker.config.json')`. With a hit, Stryker skips re-mutating unchanged files; a "no source changes in this package" run takes ~10-15 s. With a miss (real source change), Stryker re-mutates only the changed files using the cached results for the rest. + +**Failure surfaces**: when the package's mutation score is below its `break` threshold, Stryker exits non-zero. The job fails. The check `mutation / ` appears red on the PR. The PR cannot merge until either the test gets stronger or the threshold is adjusted via a config change in a follow-up commit (which is itself a reviewable signal). + +**Cache hit math** (PR touches one file in `packages/cli/src/commands/foo.ts`): + +| Job | Cache state | Runtime | +|---|---|---| +| adapter-claude | full hit | ~15 s | +| sdk | full hit | ~15 s | +| mcp-server | full hit | ~15 s | +| cli | partial hit (only `foo.ts` re-mutates) | ~5-10 min | +| sidecar | full hit | ~15 s | + +Worst-case PR (touches every package's source) ≈ ~80 min wall-clock, runs in parallel across 5 runners. + +## Files in this PR + +- `stryker.config.json` (root) — single source of truth +- `package.json` (root) — adds the 6 mutation scripts and dev-deps +- `.github/workflows/mutation.yml` — new workflow +- `.gitignore` — adds `.stryker-tmp/`, `reports/mutation/` (`.worktrees/` already added in commit `d941b66` on this branch) +- `tsconfig.json` updates per package — exclude `.stryker-tmp` so typecheck doesn't trip on the sandbox copy +- `docs/superpowers/specs/2026-04-12-mutation-testing-design.md` — this spec +- `docs/mutation-testing.md` — short user guide: how to run locally, how to read the report, how to ratchet thresholds, what to do when CI is red + +## Verification plan + +1. **Local pilot**: `pnpm mutation:adapter-claude` produces a score and an HTML report at `reports/mutation/adapter-claude.html`. Confirm score ≥ 70. +2. **Local sweep**: `pnpm mutation` runs all 5 sequentially. For each package, capture the actual score and confirm it clears the proposed threshold. If any package fails, drop its threshold to `actual − 5` and note in the PR description. +3. **CI first run**: open the PR, watch the 5 jobs run in parallel from a cold cache. All 5 should produce a score and a green check. +4. **CI cache validation**: push an empty commit. Confirm every cache restores and every job finishes in ≤ 30 s. +5. **CI partial-change validation**: push a 1-line change to `packages/sdk/src/loader/resolvers.ts`. Confirm 4 jobs hit cache (≤ 30 s each) and 1 job (`sdk`) re-mutates only `resolvers.ts` while reusing cached results for the rest of the package. +6. **CI gate validation**: temporarily delete an assertion in `packages/adapter-claude/src/__tests__/claude-adapter.test.ts`, push, confirm the `adapter-claude` job goes red with a score below 70 and the PR check is blocked. Revert the change, confirm the gate restores to green. + +## Trade-offs and known limitations + +- **Threshold drift risk**: with a single config and one CLI override, it's easy to forget that adapter-claude has a stricter gate. `stryker.config.json` is JSON (no comments) so the only place the override lives is the `--thresholds.break 70` flag in `package.json`'s `mutation:adapter-claude` script. Mitigation: `docs/mutation-testing.md` documents the table of per-package thresholds, and the gate failure on CI (red check) makes drift loud. +- **Future per-package customization** (excluding a noisy file from one package only) becomes an additional CLI flag rather than a config-file edit. If divergence grows beyond 1-2 flags per package, we may want to migrate to per-package configs after all. +- **`pnpm mutation` (the all-in-one local script)** runs packages sequentially, not in parallel, because Stryker's vitest runner uses a shared `.stryker-tmp/` working directory and parallel runs would race. CI uses separate runners so each gets its own working directory. +- **First CI run is uncached** and may take up to ~80 min for the slowest package. Subsequent runs hit cache. +- **`break` threshold is a CI-only gate**, not a local hard stop. Local `pnpm mutation:` will print a red score but still produce a report and exit cleanly enough for the developer to keep iterating. + +## Out of scope (deliberately deferred to follow-ups) + +- Per-PR sticky comment with score history +- Automatic threshold ratcheting (a scheduled job that bumps thresholds when scores improve) +- Cron baseline run on `main` to detect drift independent of PRs +- Mutation testing of docs / schema-export script +- Migration from single root config to per-package configs (only if divergence justifies it) From 67403265e24753b95486c419ececeb217ed4f740 Mon Sep 17 00:00:00 2001 From: iliassjabali Date: Sun, 12 Apr 2026 02:05:18 +0100 Subject: [PATCH 03/26] docs: add mutation testing implementation plan --- .../plans/2026-04-12-mutation-testing.md | 867 ++++++++++++++++++ 1 file changed, 867 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-12-mutation-testing.md diff --git a/docs/superpowers/plans/2026-04-12-mutation-testing.md b/docs/superpowers/plans/2026-04-12-mutation-testing.md new file mode 100644 index 0000000..a2df258 --- /dev/null +++ b/docs/superpowers/plans/2026-04-12-mutation-testing.md @@ -0,0 +1,867 @@ +# Mutation Testing Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add mutation testing across all 5 packages with a single root Stryker config, per-package CI gates, and incremental caching so unchanged packages cost ~15s per PR run. + +**Architecture:** One `stryker.config.json` at the repo root holds shared settings. Per-package overrides happen via CLI flags in root npm scripts (`mutation:adapter-claude`, `mutation:sdk`, etc.). A GitHub Actions matrix runs all 5 jobs in parallel on every PR; each job restores a per-package Stryker incremental cache so packages that didn't change finish near-instantly. The `break` threshold is 70 for adapter-claude and 60 for the rest. + +**Tech Stack:** +- `@stryker-mutator/core` + `@stryker-mutator/vitest-runner` (mutation testing) +- pnpm workspace (monorepo) +- vitest (test runner — both v1.6 in mcp-server and v2.1 elsewhere are supported) +- GitHub Actions (CI) + +**Spec:** `docs/superpowers/specs/2026-04-12-mutation-testing-design.md` + +**Working directory:** `/Users/iliassjabali/Dev/agentspec/.worktrees/mutation-testing` (branch `chore/mutation-testing`, already created off `main`) + +--- + +## File Structure + +| File | Action | Responsibility | +|---|---|---| +| `package.json` (root) | Modify | Add 6 mutation scripts + Stryker dev-deps | +| `stryker.config.json` (root) | Create | Single source of truth for shared Stryker settings | +| `.gitignore` (root) | Modify | Add `.stryker-tmp/` and `reports/mutation/` | +| `packages/adapter-claude/vitest.config.ts` | Create | Scope vitest discovery to this package's tests (currently relies on default cwd discovery) | +| `packages/mcp-server/vitest.config.ts` | Create | Same | +| `packages/cli/vitest.config.ts` | Create | Same — will be the default for all CLI tests | +| `packages/cli/vitest.stryker.config.ts` | Create | Stryker-only config that EXCLUDES `cli.test.ts` (which spawns `dist/cli.js` and can't see source mutations) | +| `.github/workflows/mutation.yml` | Create | PR gate: 5-job matrix, per-package incremental cache | +| `docs/mutation-testing.md` | Create | Short user guide: how to run locally, how to read the report, how to ratchet thresholds | + +`packages/sdk/vitest.config.ts` and `packages/sidecar/vitest.config.ts` already exist — no change. + +--- + +## Task 1: Install Stryker dev dependencies + +**Files:** +- Modify: `package.json` (root) + +- [ ] **Step 1: Add the two Stryker packages to root devDependencies** + +Run: +```bash +pnpm add -Dw @stryker-mutator/core@^8.7.1 @stryker-mutator/vitest-runner@^8.7.1 +``` + +The `-D` flag adds them as devDependencies, `-w` targets the workspace root. + +- [ ] **Step 2: Verify the install succeeded** + +Run: +```bash +pnpm exec stryker --version +``` + +Expected: prints a version number like `8.7.1` (no error). + +- [ ] **Step 3: Verify pnpm-lock.yaml updated** + +Run: +```bash +git diff --stat pnpm-lock.yaml package.json +``` + +Expected: both files appear with non-zero line changes. + +- [ ] **Step 4: Commit** + +```bash +git add package.json pnpm-lock.yaml +git commit -m "chore: add stryker mutation testing dev deps" +``` + +--- + +## Task 2: Create vitest configs for the three packages that lack one + +**Files:** +- Create: `packages/adapter-claude/vitest.config.ts` +- Create: `packages/mcp-server/vitest.config.ts` +- Create: `packages/cli/vitest.config.ts` + +**Why:** Stryker's vitest runner needs to know which tests to run for each mutation. With per-package configs, we can pass `--vitest.configFile packages//vitest.config.ts` to scope discovery. `sdk` and `sidecar` already have one — copy their style. + +- [ ] **Step 1: Create `packages/adapter-claude/vitest.config.ts`** + +```typescript +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + globals: false, + environment: 'node', + include: ['src/**/*.test.ts'], + }, +}) +``` + +- [ ] **Step 2: Create `packages/mcp-server/vitest.config.ts`** + +```typescript +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + globals: false, + environment: 'node', + include: ['src/**/*.test.ts'], + // transport.test.ts spawns tsx subprocesses; allow extra time per test + testTimeout: 15_000, + }, +}) +``` + +- [ ] **Step 3: Create `packages/cli/vitest.config.ts`** + +```typescript +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + globals: false, + environment: 'node', + include: ['src/**/*.test.ts'], + // cli.test.ts spawns the built CLI binary; needs extra time + testTimeout: 15_000, + }, +}) +``` + +- [ ] **Step 4: Run each package's test suite to confirm the new configs don't break anything** + +Run: +```bash +pnpm --filter @agentspec/adapter-claude test 2>&1 | tail -5 +pnpm --filter @agentspec/mcp test 2>&1 | tail -5 +pnpm --filter @agentspec/cli test 2>&1 | tail -5 +``` + +Expected: each prints a passing summary line (no `Failed` or `Error`). If the CLI test count differs from the previous baseline, stop and investigate. + +- [ ] **Step 5: Commit** + +```bash +git add packages/adapter-claude/vitest.config.ts packages/mcp-server/vitest.config.ts packages/cli/vitest.config.ts +git commit -m "chore: add explicit vitest configs to adapter-claude, mcp-server, cli" +``` + +--- + +## Task 3: Create the Stryker-only vitest config for cli (excludes cli.test.ts) + +**Files:** +- Create: `packages/cli/vitest.stryker.config.ts` + +**Why:** `cli.test.ts` spawns `node dist/cli.js` via `child_process.spawn`. Stryker mutates source files in a sandbox copy, but the spawned binary loads from `dist/`, which is the unmutated build. Per-test coverage analysis would mark every CLI source mutant as "no coverage" and treat them as survived. Solution: a Stryker-specific vitest config that excludes this file. + +- [ ] **Step 1: Create the file** + +```typescript +import { defineConfig, mergeConfig } from 'vitest/config' +import baseConfig from './vitest.config' + +export default mergeConfig( + baseConfig, + defineConfig({ + test: { + // cli.test.ts spawns node dist/cli.js — those tests can't see Stryker + // mutations applied to src/, so they would mark every cli source mutant + // as "no coverage" and treat them as survived. Skip them under mutation. + exclude: ['**/node_modules/**', '**/dist/**', 'src/__tests__/cli.test.ts'], + }, + }), +) +``` + +- [ ] **Step 2: Verify the config loads without TypeScript errors** + +Run: +```bash +cd packages/cli && npx vitest --config vitest.stryker.config.ts run --reporter=verbose 2>&1 | tail -10 && cd ../.. +``` + +Expected: vitest runs the cli suite WITHOUT `cli.test.ts`. The summary should show fewer test files than the regular `pnpm --filter @agentspec/cli test` run (one less file: `cli.test.ts`). + +- [ ] **Step 3: Commit** + +```bash +git add packages/cli/vitest.stryker.config.ts +git commit -m "chore(cli): add stryker-only vitest config that excludes cli.test.ts" +``` + +--- + +## Task 4: Create the root Stryker config + +**Files:** +- Create: `stryker.config.json` (root) + +- [ ] **Step 1: Write the file** + +```json +{ + "$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json", + "_comment": "Per-package thresholds and mutate globs are passed at invocation time via CLI flags in package.json scripts. adapter-claude is gated at 70 (override). All other packages inherit break=60 from this file. See docs/mutation-testing.md.", + "packageManager": "pnpm", + "testRunner": "vitest", + "reporters": ["html", "progress", "clear-text"], + "coverageAnalysis": "perTest", + "incremental": true, + "concurrency": 4, + "timeoutMS": 60000, + "thresholds": { + "high": 75, + "low": 60, + "break": 60 + }, + "mutate": [ + "packages/*/src/**/*.ts", + "!packages/*/src/**/*.test.ts", + "!packages/*/src/**/__tests__/**", + "!packages/*/src/**/*.d.ts", + "!packages/sdk/src/scripts/export-schema.ts" + ], + "tempDirName": ".stryker-tmp", + "htmlReporter": { "fileName": "reports/mutation/index.html" } +} +``` + +The `_comment` field is non-standard but Stryker ignores unknown fields. Use it to document the threshold override since JSON has no comment syntax. + +- [ ] **Step 2: Validate the JSON parses** + +Run: +```bash +node -e "console.log(JSON.parse(require('fs').readFileSync('stryker.config.json','utf8')).testRunner)" +``` + +Expected: prints `vitest`. + +- [ ] **Step 3: Commit** + +```bash +git add stryker.config.json +git commit -m "chore: add root stryker config with shared defaults" +``` + +--- + +## Task 5: Add mutation scripts to root package.json + +**Files:** +- Modify: `package.json` (root) + +- [ ] **Step 1: Read the current scripts block** + +Run: +```bash +cat package.json +``` + +Expected output includes the existing scripts: `build`, `test`, `lint`, `clean`, `typecheck`, `schema:export`. + +- [ ] **Step 2: Add 6 new mutation scripts** + +Edit `package.json`. Inside the `"scripts"` object, after `"schema:export"`, add: + +```json + "mutation:adapter-claude": "stryker run --mutate 'packages/adapter-claude/src/**/*.ts' --thresholds.break 70 --incrementalFile .stryker-tmp/adapter-claude-incremental.json --htmlReporter.fileName reports/mutation/adapter-claude.html --vitest.configFile packages/adapter-claude/vitest.config.ts", + "mutation:sdk": "stryker run --mutate 'packages/sdk/src/**/*.ts' --incrementalFile .stryker-tmp/sdk-incremental.json --htmlReporter.fileName reports/mutation/sdk.html --vitest.configFile packages/sdk/vitest.config.ts", + "mutation:mcp-server": "stryker run --mutate 'packages/mcp-server/src/**/*.ts' --incrementalFile .stryker-tmp/mcp-server-incremental.json --htmlReporter.fileName reports/mutation/mcp-server.html --vitest.configFile packages/mcp-server/vitest.config.ts", + "mutation:cli": "stryker run --mutate 'packages/cli/src/**/*.ts' --incrementalFile .stryker-tmp/cli-incremental.json --htmlReporter.fileName reports/mutation/cli.html --vitest.configFile packages/cli/vitest.stryker.config.ts", + "mutation:sidecar": "stryker run --mutate 'packages/sidecar/src/**/*.ts' --incrementalFile .stryker-tmp/sidecar-incremental.json --htmlReporter.fileName reports/mutation/sidecar.html --vitest.configFile packages/sidecar/vitest.config.ts", + "mutation": "pnpm mutation:adapter-claude && pnpm mutation:sdk && pnpm mutation:mcp-server && pnpm mutation:cli && pnpm mutation:sidecar" +``` + +Note the cli script uses `vitest.stryker.config.ts` (the carve-out from Task 3); all others use `vitest.config.ts`. Adapter-claude is the only one with `--thresholds.break 70`. + +- [ ] **Step 3: Validate the JSON still parses** + +Run: +```bash +node -e "console.log(Object.keys(require('./package.json').scripts).filter(k => k.startsWith('mutation')))" +``` + +Expected: prints `[ 'mutation:adapter-claude', 'mutation:sdk', 'mutation:mcp-server', 'mutation:cli', 'mutation:sidecar', 'mutation' ]`. + +- [ ] **Step 4: Commit** + +```bash +git add package.json +git commit -m "chore: add per-package mutation scripts to root package.json" +``` + +--- + +## Task 6: Update .gitignore + +**Files:** +- Modify: `.gitignore` (root) + +- [ ] **Step 1: Add Stryker temp dir and report dir to .gitignore** + +Edit `.gitignore`. After the `# ── Worktrees ──` block (added in commit `d941b66`), add: + +``` +# ── Stryker (mutation testing) ──────────────────────────────────────────────── +.stryker-tmp/ +reports/mutation/ +``` + +- [ ] **Step 2: Verify the entries are recognized** + +Run: +```bash +mkdir -p .stryker-tmp reports/mutation && touch .stryker-tmp/test reports/mutation/test +git status --short +rm -rf .stryker-tmp reports +``` + +Expected: `git status --short` does NOT show `.stryker-tmp/` or `reports/mutation/` as untracked. + +- [ ] **Step 3: Commit** + +```bash +git add .gitignore +git commit -m "chore: ignore .stryker-tmp and reports/mutation directories" +``` + +--- + +## Task 7: Pilot run — adapter-claude + +**Files:** none modified, this is a verification step. + +**Why:** adapter-claude is the smallest, fastest package and has the highest threshold (70). If this doesn't clear, every other package's threshold needs to be re-examined. + +- [ ] **Step 1: Build dependencies (Stryker needs the workspace built so cross-package imports resolve)** + +Run: +```bash +pnpm -r build 2>&1 | tail -5 +``` + +Expected: every package prints `Build success` or `Done`. No errors. + +- [ ] **Step 2: Run the adapter-claude mutation** + +Run: +```bash +pnpm mutation:adapter-claude 2>&1 | tee /tmp/stryker-adapter-claude.log +``` + +Expected: Stryker runs ~150 mutants over the existing 54 tests. Final lines should print a mutation score percentage and `Done in `. Stryker exits 0 if score ≥ 70, exits non-zero if below. + +- [ ] **Step 3: Capture the actual score** + +Run: +```bash +grep -E "Mutation score" /tmp/stryker-adapter-claude.log +``` + +Expected: a line like `Mutation score: 78.32 %` or similar. + +- [ ] **Step 4: Decision gate** + +If the actual score is ≥ 70, proceed to Task 8. + +If below 70: +1. Open the HTML report at `reports/mutation/adapter-claude.html` to see which mutants survived. +2. STOP and report back to the user with: actual score, top 3 surviving mutants (file:line, mutator type, what code is now too weak), and a recommendation: either (a) drop the threshold to `actual − 5` and document the lower bar, or (b) write 2-3 targeted tests to kill the survivors and re-run. +3. Do NOT auto-modify the threshold without explicit approval. + +- [ ] **Step 5: No commit** — this task produces a log file, not source changes. + +--- + +## Task 8: Pilot run — sdk + +- [ ] **Step 1: Run the sdk mutation** + +Run: +```bash +pnpm mutation:sdk 2>&1 | tee /tmp/stryker-sdk.log +``` + +Expected: Stryker runs ~600 mutants. May take 20-30 minutes. + +- [ ] **Step 2: Capture the score** + +Run: +```bash +grep -E "Mutation score" /tmp/stryker-sdk.log +``` + +- [ ] **Step 3: Decision gate** + +If ≥ 60, proceed to Task 9. + +If below 60: report back as in Task 7 step 4. Same options apply (drop threshold or strengthen tests). + +--- + +## Task 9: Pilot run — mcp-server + +- [ ] **Step 1: Run the mcp-server mutation** + +Run: +```bash +pnpm mutation:mcp-server 2>&1 | tee /tmp/stryker-mcp-server.log +``` + +Expected: Stryker runs ~300 mutants. Transport tests will inflate per-mutant runtime since they spawn tsx subprocesses. Estimate 25-40 minutes. + +- [ ] **Step 2: Capture the score** + +Run: +```bash +grep -E "Mutation score" /tmp/stryker-mcp-server.log +``` + +- [ ] **Step 3: Decision gate** + +If ≥ 60, proceed to Task 10. + +If below 60: report back. Note that many mcp-server tools are 5-line `spawnCli` wrappers — these are likely sources of equivalent mutants (mutations Stryker can't kill because semantically identical). The HTML report should make these visible and they're an acceptable reason to drop the threshold to `actual − 5`. + +--- + +## Task 10: Pilot run — cli + +- [ ] **Step 1: Run the cli mutation** + +Run: +```bash +pnpm mutation:cli 2>&1 | tee /tmp/stryker-cli.log +``` + +Expected: Stryker runs ~800 mutants. Largest package, slowest run. 60-90 minutes worst case. + +- [ ] **Step 2: Capture the score** + +Run: +```bash +grep -E "Mutation score" /tmp/stryker-cli.log +``` + +- [ ] **Step 3: Decision gate** + +If ≥ 60, proceed to Task 11. Otherwise report back per Task 7 step 4. + +--- + +## Task 11: Pilot run — sidecar + +- [ ] **Step 1: Run the sidecar mutation** + +Run: +```bash +pnpm mutation:sidecar 2>&1 | tee /tmp/stryker-sidecar.log +``` + +Expected: ~700 mutants, 30-60 minutes. + +- [ ] **Step 2: Capture the score** + +Run: +```bash +grep -E "Mutation score" /tmp/stryker-sidecar.log +``` + +- [ ] **Step 3: Decision gate** + +If ≥ 60, proceed to Task 12. Otherwise report back per Task 7 step 4. + +--- + +## Task 12: Record the pilot scores in the spec doc + +**Files:** +- Modify: `docs/superpowers/specs/2026-04-12-mutation-testing-design.md` + +**Why:** The spec mentions "the first CI run determines reality" — now we have local reality. Lock the actual numbers in so reviewers know what to expect from CI. + +- [ ] **Step 1: Find the per-package thresholds table in the spec** + +Search the file for `### Per-package thresholds`. The table currently has only `break` values. + +- [ ] **Step 2: Add a "Pilot score" column with the actual numbers from Tasks 7-11** + +Replace the existing table with: + +```markdown +| Package | `break` (gate) | Pilot score (local) | Rationale | +|---|---|---|---| +| adapter-claude | **70** | % | Pure functions, no I/O, dense test coverage from PR #46. | +| sdk | 60 | % | Larger surface, some I/O paths (loaders, resolvers). | +| mcp-server | 60 | % | Many tools are thin spawnCli wrappers (low mutation value); transport tests do exercise real code paths. | +| cli | 60 | % | Largest package, lots of command wiring. cli.test.ts excluded. | +| sidecar | 60 | % | HTTP server with mocked dependencies. | +``` + +Replace `` with each package's score from `/tmp/stryker-.log`. + +- [ ] **Step 3: Commit** + +```bash +git add docs/superpowers/specs/2026-04-12-mutation-testing-design.md +git commit -m "docs: record pilot mutation scores in design spec" +``` + +--- + +## Task 13: Create the GitHub Actions workflow + +**Files:** +- Create: `.github/workflows/mutation.yml` + +- [ ] **Step 1: Verify the .github/workflows directory exists** + +Run: +```bash +ls .github/workflows/ 2>&1 | head +``` + +Expected: a directory listing of existing workflow files (it exists). If it doesn't, create it: `mkdir -p .github/workflows`. + +- [ ] **Step 2: Write the workflow** + +```yaml +name: mutation +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + mutation: + name: mutation / ${{ matrix.package }} + runs-on: ubuntu-latest + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + package: [adapter-claude, sdk, mcp-server, cli, sidecar] + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build all packages + run: pnpm -r build + + - name: Restore Stryker incremental cache + uses: actions/cache@v4 + with: + path: .stryker-tmp/${{ matrix.package }}-incremental.json + key: stryker-${{ matrix.package }}-${{ hashFiles(format('packages/{0}/src/**', matrix.package), 'stryker.config.json') }} + restore-keys: | + stryker-${{ matrix.package }}- + + - name: Run Stryker + run: pnpm mutation:${{ matrix.package }} + + - name: Upload HTML report + if: always() + uses: actions/upload-artifact@v4 + with: + name: mutation-report-${{ matrix.package }} + path: reports/mutation/${{ matrix.package }}.html + retention-days: 14 +``` + +- [ ] **Step 3: Lint the YAML by parsing it** + +Run: +```bash +node -e "const fs=require('fs'); const yaml=require('js-yaml'); console.log(Object.keys(yaml.load(fs.readFileSync('.github/workflows/mutation.yml','utf8')).jobs))" +``` + +Expected: prints `[ 'mutation' ]`. If `js-yaml` isn't available, skip this and proceed; CI will catch syntax errors. + +- [ ] **Step 4: Commit** + +```bash +git add .github/workflows/mutation.yml +git commit -m "ci: add mutation testing workflow with per-package matrix" +``` + +--- + +## Task 14: Write the user-facing docs + +**Files:** +- Create: `docs/mutation-testing.md` + +- [ ] **Step 1: Write the file** + +```markdown +# Mutation Testing + +AgentSpec uses [Stryker Mutator](https://stryker-mutator.io) to test the strength of its test suite. Stryker mutates production code in small ways (negate a condition, drop a statement, change `>` to `>=`) and re-runs the suite. If the suite still passes, the mutant "survived" and the test is too weak. + +## Running locally + +```bash +# Run for a single package +pnpm mutation:adapter-claude +pnpm mutation:sdk +pnpm mutation:mcp-server +pnpm mutation:cli +pnpm mutation:sidecar + +# Run all packages sequentially +pnpm mutation +``` + +Each run produces an HTML report at `reports/mutation/.html`. Open it to see which mutants survived and where. + +The first run for a package is slow (full mutation pass). Subsequent runs use Stryker's incremental cache (`.stryker-tmp/-incremental.json`) and only re-mutate files that changed. + +## Per-package thresholds + +| Package | `break` threshold | Set in | +|---|---|---| +| adapter-claude | 70 | `package.json` `mutation:adapter-claude` script (`--thresholds.break 70` flag) | +| sdk | 60 | `stryker.config.json` (default) | +| mcp-server | 60 | `stryker.config.json` (default) | +| cli | 60 | `stryker.config.json` (default) | +| sidecar | 60 | `stryker.config.json` (default) | + +If your local run shows a score below the threshold, the command exits non-zero. CI uses the same thresholds and will fail the corresponding `mutation / ` check on the PR. + +## CI + +`.github/workflows/mutation.yml` runs all 5 packages in parallel on every PR (open / synchronize / reopen). Each job: + +1. Restores its package's Stryker incremental cache +2. Runs `pnpm mutation:` +3. Uploads the HTML report as a workflow artifact (`mutation-report-`, 14-day retention) +4. Exits non-zero if the mutation score is below the package's `break` threshold + +A PR cannot merge until all 5 mutation checks pass. + +## When CI is red + +1. Open the PR's failing mutation check. +2. Click the "Summary" tab and download the `mutation-report-` artifact. +3. Open `.html` locally. Surviving mutants are highlighted in red. +4. For each survivor, decide: + - **Test gap**: write a test that would fail if the mutant were the real implementation. Push. + - **Equivalent mutant**: the mutation produces semantically identical behavior (e.g., changing `i++` to `++i` in a loop where the prefix/postfix difference is invisible). These can be ignored — Stryker has no way to detect them automatically. If equivalents push the score below the threshold, the threshold should be lowered, not the test suite padded. + +## Ratcheting the threshold + +When the mutation score for a package improves consistently above its threshold, raise the threshold to lock in the gain: + +1. Edit `package.json` and bump `--thresholds.break N` for the package's script (or move the package out of the default by adding the flag). +2. Open a PR. CI will validate the new threshold against the actual score. + +The goal is to ratchet up over time, never down. + +## What's excluded from mutation + +- `__tests__/` directories — tests don't get mutated +- `*.d.ts` files — type definitions only +- `dist/` builds +- `packages/sdk/src/scripts/export-schema.ts` — generated artefact bootstrap +- `packages/cli/src/__tests__/cli.test.ts` is excluded from the **test** set (not the mutate set) because it spawns the pre-built `dist/cli.js` binary and can't see source mutations. Other cli tests still run. +``` + +- [ ] **Step 2: Commit** + +```bash +git add docs/mutation-testing.md +git commit -m "docs: add mutation testing user guide" +``` + +--- + +## Task 15: Verify the gate works (red-test simulation) + +**Files:** none modified permanently — this is an end-to-end gate test that gets reverted. + +**Why:** Confirm that weakening a test actually causes the mutation score to drop and the script to exit non-zero. If this doesn't work, the gate is decorative. + +- [ ] **Step 1: Identify a strong test in adapter-claude** + +Open `packages/adapter-claude/src/__tests__/claude-adapter.test.ts`. Find the test added in PR #46: + +```typescript +it('encodes in file content to prevent tag breakout', () => { +``` + +This test asserts the security guard for tag breakout. If we delete the assertions, multiple `sanitizeContextContent` mutants should now survive. + +- [ ] **Step 2: Comment out the assertions in that test** + +Edit the file. Inside the `it('encodes ...')` block, comment out the two `expect(...)` lines: + +```typescript + try { + const ctx = buildContext({ manifest: baseManifest, contextFiles: [toolFile] }) + // expect(ctx).not.toMatch(/<\/context_file>\nignore/) + // expect(ctx).toContain('ignore all previous instructions') + } finally { +``` + +- [ ] **Step 3: Re-run adapter-claude mutation** + +Run: +```bash +pnpm mutation:adapter-claude 2>&1 | tee /tmp/stryker-gate-test.log +``` + +- [ ] **Step 4: Verify the score dropped and Stryker exited non-zero** + +Run: +```bash +echo "exit code: $?" +grep -E "Mutation score" /tmp/stryker-gate-test.log +``` + +Expected: exit code is non-zero (Stryker exits with code 1 when below `break`). The mutation score is lower than the Task 7 baseline. + +If the score did NOT drop or exit was 0: STOP. The gate is not working. Investigate before proceeding (likely cause: the deleted assertion wasn't load-bearing for any specific mutant — pick a different test to weaken). + +- [ ] **Step 5: Revert the test file** + +Run: +```bash +git checkout packages/adapter-claude/src/__tests__/claude-adapter.test.ts +``` + +- [ ] **Step 6: Confirm the revert worked** + +Run: +```bash +git status --short +``` + +Expected: no changes to the test file (it's back to clean state). + +- [ ] **Step 7: No commit** — this task verifies behavior, doesn't change source. + +--- + +## Task 16: Push and open the PR + +**Files:** none. + +- [ ] **Step 1: Verify the branch is clean and ahead of main** + +Run: +```bash +git status +git log --oneline main..HEAD +``` + +Expected: working tree clean. Log shows commits from Tasks 1, 2, 3, 4, 5, 6, 12, 13, 14 (9 commits) plus the original `d941b66` (gitignore .worktrees) and `9e3e1d7` (spec doc) = 11 commits total. + +- [ ] **Step 2: Push the branch** + +Run: +```bash +git push -u origin chore/mutation-testing +``` + +Expected: pushes successfully, prints a "Create a pull request" URL. + +- [ ] **Step 3: Open the PR with `gh pr create`** + +Run: +```bash +gh pr create --base main --head chore/mutation-testing --title "chore: add mutation testing with Stryker (per-package CI gates)" --body "$(cat <<'EOF' +## Summary + +Adds [Stryker Mutator](https://stryker-mutator.io) mutation testing across all 5 packages with a per-package CI gate. + +- Single root \`stryker.config.json\` with shared defaults. +- 6 mutation scripts in root \`package.json\`. adapter-claude is gated at 70 via a CLI override; the rest inherit \`break: 60\`. +- New GitHub Actions workflow runs all 5 packages in parallel on every PR (open / synchronize / reopen) with per-package incremental caching, so a PR that doesn't touch a package finishes its mutation check in ~15 s. +- New \`packages/cli/vitest.stryker.config.ts\` excludes \`cli.test.ts\` from the Stryker test set (it spawns \`dist/cli.js\` and can't see source mutations). +- New \`vitest.config.ts\` files for adapter-claude, mcp-server, and cli (sdk and sidecar already had one). +- New user guide at \`docs/mutation-testing.md\`. +- Design spec at \`docs/superpowers/specs/2026-04-12-mutation-testing-design.md\`. + +## Pilot scores (local run) + +| Package | Threshold | Pilot score | +|---|---|---| +| adapter-claude | 70 | | +| sdk | 60 | | +| mcp-server | 60 | | +| cli | 60 | | +| sidecar | 60 | | + +## Test plan + +- [x] Local pilot per package — scores recorded above +- [x] Local gate verification — weakened a security test, confirmed Stryker exits non-zero +- [ ] CI first run on this PR — confirm all 5 jobs produce a score and a check status +- [ ] CI cache validation — push an empty commit, confirm every job hits cache and finishes ≤ 30 s +- [ ] CI partial-change validation — touch one source file, confirm only that package re-mutates the changed file + +## Notes + +- First CI run will be slow (cold caches) — up to 90 min for the slowest package on a fresh runner. +- Subsequent runs use the per-package Stryker incremental cache + GitHub actions/cache. +- Reports are uploaded as workflow artifacts (\`mutation-report-\`, 14-day retention). +EOF +)" +``` + +Replace the `` placeholders with the actual scores from Task 12 BEFORE running. + +- [ ] **Step 4: Capture the PR URL** + +Expected: `gh pr create` prints the PR URL on stdout. Save it for the user. + +--- + +## Verification (end-to-end) + +After Task 16, the PR is open. CI should kick off automatically. To verify the full design works: + +1. **First run**: all 5 jobs should produce a score (cold cache, up to 90 min for the slowest). Confirm 5 check statuses appear on the PR. +2. **Cache validation**: push an empty commit (`git commit --allow-empty -m "test: cache validation" && git push`). Every job should hit cache and finish in ≤ 30 s. +3. **Partial change validation**: push a 1-line whitespace change to `packages/sdk/src/loader/resolvers.ts`. The `sdk` job should re-mutate only that file (~2-5 min); the other 4 jobs should hit cache (~15 s each). +4. **Gate validation**: temporarily weaken a test in adapter-claude (same change as Task 15 step 2), push, confirm the `mutation / adapter-claude` check goes red. Revert and push again to restore green. + +If any of these fail, comment on the PR with the failure mode and we'll iterate. + +--- + +## Self-Review Notes + +**Spec coverage check** — every section of the design spec has at least one task: +- Architecture / single root config → Task 4 +- Per-package npm scripts → Task 5 +- Per-package thresholds → Task 5 (CLI flag) + Task 14 (docs) +- Test scope per package (cli carve-out) → Tasks 2, 3 +- CI workflow → Task 13 +- Cache layers → Task 13 +- Files in this PR → Tasks 1-6, 13, 14 +- Verification plan → Tasks 7-11 (local pilot), Task 15 (gate), final verification section (CI) +- Trade-offs and limitations → Task 14 (docs) + +**Placeholder scan**: `` and `` appear in Tasks 12 and 16. These are deliberate fill-ins from the pilot runs, not unresolved TODOs. Every other step has concrete code or commands. + +**Type / name consistency**: script names are `mutation:` consistently across Tasks 5, 13, 14, 16. Cache file names are `-incremental.json` consistently. Config file is `stryker.config.json` everywhere (Task 4) and referenced the same way in cache key (Task 13). From 89c50bacb29e7c319ca232fe82d52c99ff99f067 Mon Sep 17 00:00:00 2001 From: iliassjabali Date: Sun, 12 Apr 2026 02:09:05 +0100 Subject: [PATCH 04/26] chore: add stryker mutation testing dev deps --- package.json | 2 + pnpm-lock.yaml | 1051 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1053 insertions(+) diff --git a/package.json b/package.json index 8b1bfc5..7a8da93 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "schema:export": "tsx packages/sdk/src/scripts/export-schema.ts" }, "devDependencies": { + "@stryker-mutator/core": "^8.7.1", + "@stryker-mutator/vitest-runner": "^8.7.1", "@types/node": "^20.17.0", "@vitest/coverage-v8": "^2.1.9", "tsup": "^8.3.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c0d165a..288bfd2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,12 @@ importers: .: devDependencies: + '@stryker-mutator/core': + specifier: ^8.7.1 + version: 8.7.1 + '@stryker-mutator/vitest-runner': + specifier: ^8.7.1 + version: 8.7.1(@stryker-mutator/core@8.7.1)(vitest@2.1.9(@types/node@20.19.34)) '@types/node': specifier: ^20.17.0 version: 20.19.34 @@ -265,6 +271,76 @@ packages: '@anthropic-ai/sdk@0.39.0': resolution: {integrity: sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg==} + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.25.9': + resolution: {integrity: sha512-WYvQviPw+Qyib0v92AwNIrdLISTp7RfDkM7bPqBvpbnhY4wq8HvHBZREVdYDXk98C8BkOIVnHAY3yvj7AVISxQ==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.25.9': + resolution: {integrity: sha512-omlUGkr5EaoIJrhLf9CJ0TvjBRpd9+AXRG//0GEQ9THSo8wPiTlbpy1/Ow8ZTrbXpjd9FHXfbFQx32I04ht0FA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.28.6': + resolution: {integrity: sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.28.5': + resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.28.6': + resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} @@ -273,11 +349,87 @@ packages: resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.25.9': + resolution: {integrity: sha512-aI3jjAAO1fh7vY/pBGsn1i9LDbRP43+asrRlkPuTXW5yHXtd1NgTEMudbBoDDxrf1daEEfPJqR+JBMakzrR4Dg==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/parser@7.29.0': resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} engines: {node: '>=6.0.0'} hasBin: true + '@babel/plugin-proposal-decorators@7.24.7': + resolution: {integrity: sha512-RL9GR0pUG5Kc8BUWLNDm2T5OpYwSX15r98I0IkgmRQTXuELq/OynH8xtMTMvTJFjXbMWFVTKtYkTaYQsuAwQlQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-explicit-resource-management@7.27.4': + resolution: {integrity: sha512-1SwtCDdZWQvUU1i7wt/ihP7W38WjC3CSTOHAl+Xnbze8+bbMNjRvRQydnj0k9J1jPqCAZctBFp6NHJXkrVVmEA==} + engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-explicit-resource-management instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-decorators@7.28.6': + resolution: {integrity: sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-destructuring@7.28.5': + resolution: {integrity: sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.28.6': + resolution: {integrity: sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.28.6': + resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-typescript@7.24.7': + resolution: {integrity: sha512-SyXRe3OdWwIwalxDg5UtJnJQO+YPcTfwiIY2B0Xlddh9o7jpWLvv8X1RthIeDOxQ+O1ML5BLPCONToObyVQVuQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + '@babel/types@7.29.0': resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} @@ -636,6 +788,62 @@ packages: '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + '@inquirer/checkbox@3.0.1': + resolution: {integrity: sha512-0hm2nrToWUdD6/UHnel/UKGdk1//ke5zGUpHIvk5ZWmaKezlGxZkOJXNSWsdxO/rEqTkbB3lNC2J6nBElV2aAQ==} + engines: {node: '>=18'} + + '@inquirer/confirm@4.0.1': + resolution: {integrity: sha512-46yL28o2NJ9doViqOy0VDcoTzng7rAb6yPQKU7VDLqkmbCaH4JqK4yk4XqlzNWy9PVC5pG1ZUXPBQv+VqnYs2w==} + engines: {node: '>=18'} + + '@inquirer/core@9.2.1': + resolution: {integrity: sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==} + engines: {node: '>=18'} + + '@inquirer/editor@3.0.1': + resolution: {integrity: sha512-VA96GPFaSOVudjKFraokEEmUQg/Lub6OXvbIEZU1SDCmBzRkHGhxoFAVaF30nyiB4m5cEbDgiI2QRacXZ2hw9Q==} + engines: {node: '>=18'} + + '@inquirer/expand@3.0.1': + resolution: {integrity: sha512-ToG8d6RIbnVpbdPdiN7BCxZGiHOTomOX94C2FaT5KOHupV40tKEDozp12res6cMIfRKrXLJyexAZhWVHgbALSQ==} + engines: {node: '>=18'} + + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + + '@inquirer/input@3.0.1': + resolution: {integrity: sha512-BDuPBmpvi8eMCxqC5iacloWqv+5tQSJlUafYWUe31ow1BVXjW2a5qe3dh4X/Z25Wp22RwvcaLCc2siHobEOfzg==} + engines: {node: '>=18'} + + '@inquirer/number@2.0.1': + resolution: {integrity: sha512-QpR8jPhRjSmlr/mD2cw3IR8HRO7lSVOnqUvQa8scv1Lsr3xoAMMworcYW3J13z3ppjBFBD2ef1Ci6AE5Qn8goQ==} + engines: {node: '>=18'} + + '@inquirer/password@3.0.1': + resolution: {integrity: sha512-haoeEPUisD1NeE2IanLOiFr4wcTXGWrBOyAyPZi1FfLJuXOzNmxCJPgUrGYKVh+Y8hfGJenIfz5Wb/DkE9KkMQ==} + engines: {node: '>=18'} + + '@inquirer/prompts@6.0.1': + resolution: {integrity: sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==} + engines: {node: '>=18'} + + '@inquirer/rawlist@3.0.1': + resolution: {integrity: sha512-VgRtFIwZInUzTiPLSfDXK5jLrnpkuSOh1ctfaoygKAdPqjcjKYmGh6sCY1pb0aGnCGsmhUxoqLDUAU0ud+lGXQ==} + engines: {node: '>=18'} + + '@inquirer/search@2.0.1': + resolution: {integrity: sha512-r5hBKZk3g5MkIzLVoSgE4evypGqtOannnB3PKTG9NRZxyFRKcfzrdxXXPcoJQsxJPzvdSU2Rn7pB7lw0GCmGAg==} + engines: {node: '>=18'} + + '@inquirer/select@3.0.1': + resolution: {integrity: sha512-lUDGUxPhdWMkN/fHy1Lk7pF3nK1fh/gqeyWXmctefhxLYxlDsc7vsPBEpxrfVGDsVdyYJsiJoD4bJ1b623cV1Q==} + engines: {node: '>=18'} + + '@inquirer/type@2.0.0': + resolution: {integrity: sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==} + engines: {node: '>=18'} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -702,66 +910,79 @@ packages: resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.59.0': resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.59.0': resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.59.0': resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.59.0': resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.59.0': resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.59.0': resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.59.0': resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.59.0': resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.59.0': resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.59.0': resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.59.0': resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.59.0': resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.59.0': resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} @@ -827,6 +1048,29 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} + '@stryker-mutator/api@8.7.1': + resolution: {integrity: sha512-56vxcVxIfW0jxJhr7HB9Zx6Xr5/M95RG9MUK1DtbQhlmQesjpfBBsrPLOPzBJaITPH/vOYykuJ69vgSAMccQyw==} + engines: {node: '>=18.0.0'} + + '@stryker-mutator/core@8.7.1': + resolution: {integrity: sha512-r2AwhHWkHq6yEe5U8mAzPSWewULbv9YMabLHRzLjZnjj+Ipxtg+Zo22rrUc2Zl7mnYvb9w34bdlEzGz6MKgX2g==} + engines: {node: '>=18.0.0'} + hasBin: true + + '@stryker-mutator/instrumenter@8.7.1': + resolution: {integrity: sha512-HSq4VHXesQCMR3hr6bn41DAeJ0yuP2vp9KSnls2TySNawFVWOCaKXpBX29Skj3zJQh7dnm7HuQg8HuXvJK15oA==} + engines: {node: '>=18.0.0'} + + '@stryker-mutator/util@8.7.1': + resolution: {integrity: sha512-Oj/sIHZI1GLfGOHKnud4Gw0ZRufm7ONoQYNnhcaAYEXTWraYVcV7mue/th8fZComTHvDPA8Ge8U16FvWYEb8dg==} + + '@stryker-mutator/vitest-runner@8.7.1': + resolution: {integrity: sha512-vNRTM6MEy+0hNK5UhJ44euEIRjluDV43UROcMAKIMbT9ELdp8XgM/tA5GrTcp5QadnvrBwvEcCRQk+ARL+e0sg==} + engines: {node: '>=14.18.0'} + peerDependencies: + '@stryker-mutator/core': ~8.7.0 + vitest: '>=0.31.2' + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -848,6 +1092,9 @@ packages: '@types/mdurl@2.0.0': resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/mute-stream@0.0.4': + resolution: {integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==} + '@types/node-fetch@2.6.13': resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} @@ -860,12 +1107,18 @@ packages: '@types/node@20.19.37': resolution: {integrity: sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==} + '@types/node@22.19.17': + resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} '@types/web-bluetooth@0.0.21': resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + '@types/wrap-ansi@3.0.0': + resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==} + '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -1053,6 +1306,9 @@ packages: ajv: optional: true + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ajv@8.18.0: resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} @@ -1060,6 +1316,14 @@ packages: resolution: {integrity: sha512-X3Pp2aRQhg4xUC6PQtkubn5NpRKuUPQ9FPDQlx36SmpFwwH2N0/tw4c+NXV3nw3PsgeUs+BuWGP0gjz3TvENLQ==} engines: {node: '>= 14.0.0'} + angular-html-parser@6.0.2: + resolution: {integrity: sha512-8+sH1TwYxv8XsQes1psxTHMtWRBbJFA/jY0ThqpT4AgCiRdhTtRxru0vlBfyRJpL9CHd3G06k871bR2vyqaM6A==} + engines: {node: '>= 14'} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1110,6 +1374,11 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + baseline-browser-mapping@2.10.18: + resolution: {integrity: sha512-VSnGQAOLtP5mib/DPyg2/t+Tlv65NTBz83BJBJvmLVHHuKJVaDOBvJJykiT5TR++em5nfAySPccDZDa4oSrn8A==} + engines: {node: '>=6.0.0'} + hasBin: true + birpc@2.9.0: resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} @@ -1120,6 +1389,11 @@ packages: resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} engines: {node: 18 || 20 || >=22} + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + bundle-require@5.1.0: resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1134,6 +1408,13 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + caniuse-lite@1.0.30001787: + resolution: {integrity: sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -1145,6 +1426,10 @@ packages: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} + chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + chalk@5.6.2: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} @@ -1155,6 +1440,9 @@ packages: character-entities-legacy@3.0.0: resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + check-error@1.0.3: resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} @@ -1166,6 +1454,10 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1195,6 +1487,9 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} @@ -1235,9 +1530,15 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + des.js@1.1.0: + resolution: {integrity: sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==} + devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + diff-match-patch@1.0.5: + resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} + diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1249,9 +1550,15 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + electron-to-chromium@1.5.335: + resolution: {integrity: sha512-q9n5T4BR4Xwa2cwbrwcsDJtHD/enpQ5S1xF1IAtdqf5AAgqDFmR/aakqH3ChFdqd/QXJhS3rnnXFtexU7rax6Q==} + emoji-regex-xs@1.0.0: resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} + emoji-regex@10.4.0: + resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1294,6 +1601,10 @@ packages: engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} @@ -1308,6 +1619,10 @@ packages: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} + execa@9.4.1: + resolution: {integrity: sha512-5eo/BRqZm3GYce+1jqX/tJ7duA2AnE39i88fuedNFUV8XxGxUpF3aWkBRfbUcjV49gCkvS/pzc0YrCPhaIewdg==} + engines: {node: ^18.19.0 || >=20.5.0} + execa@9.6.1: resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} engines: {node: ^18.19.0 || >=20.5.0} @@ -1316,6 +1631,10 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + fast-content-type-parse@1.1.0: resolution: {integrity: sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==} @@ -1359,6 +1678,10 @@ packages: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} + file-url@4.0.0: + resolution: {integrity: sha512-vRCdScQ6j3Ku6Kd7W1kZk9c++5SqD6Xz5Jotrjr/nkY714M14RFHy/AAVA2WQvpsqVAVgTbDrYyBpU205F0cLw==} + engines: {node: '>=12'} + find-my-way@8.2.2: resolution: {integrity: sha512-Dobi7gcTEq8yszimcfp/R7+owiT4WncAJ7VTTgFH1jYJ5GaG1FbhjwDG820hptN0QDFvzVY3RfCzdInvGPGzjA==} engines: {node: '>=14'} @@ -1396,6 +1719,10 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + get-func-name@2.0.2: resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} @@ -1469,6 +1796,13 @@ packages: humanize-ms@1.2.1: resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -1523,6 +1857,12 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + js-md4@0.3.2: + resolution: {integrity: sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} @@ -1530,12 +1870,22 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + json-schema-ref-resolver@1.0.1: resolution: {integrity: sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==} json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + light-my-request@5.14.0: resolution: {integrity: sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==} @@ -1554,6 +1904,9 @@ packages: resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} engines: {node: '>=14'} + lodash.groupby@4.6.0: + resolution: {integrity: sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==} + loupe@2.3.7: resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} @@ -1563,6 +1916,9 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1613,6 +1969,9 @@ packages: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + minimatch@10.2.4: resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} engines: {node: 18 || 20 || >=22} @@ -1637,6 +1996,19 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mutation-testing-elements@3.4.0: + resolution: {integrity: sha512-zFJtGlobq+Fyq95JoJj0iqrmwLSLQyIJuDATLwFMDSJCxpGN8kHCA6S4LoLJnkSL6bg4Aqultp8OBSMxGbW3EA==} + + mutation-testing-metrics@3.3.0: + resolution: {integrity: sha512-vZEJ84SpK3Rwyk7k28SORS5o6ZDtehwifLPH6fQULrozJqlz2Nj8vi52+CjA+aMZCyyKB+9eYUh1HtiWVo4o/A==} + + mutation-testing-report-schema@3.3.0: + resolution: {integrity: sha512-DF56q0sb0GYzxYUYNdzlfQzyE5oJBEasz8zL76bt3OFJU8q4iHSdUDdihPWWJD+4JLxSs3neM/R968zYdy0SWQ==} + + mute-stream@1.0.0: + resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -1659,6 +2031,9 @@ packages: encoding: optional: true + node-releases@2.0.37: + resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} + npm-run-path@5.3.0: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1671,6 +2046,10 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + on-exit-leak-free@2.1.2: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} @@ -1685,6 +2064,10 @@ packages: oniguruma-to-es@3.1.1: resolution: {integrity: sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==} + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + p-limit@5.0.0: resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} engines: {node: '>=18'} @@ -1787,6 +2170,10 @@ packages: process-warning@5.0.0: resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} @@ -1794,6 +2181,10 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + qs@6.15.1: + resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} + engines: {node: '>=0.6'} + quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} @@ -1844,6 +2235,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + safe-regex2@3.1.0: resolution: {integrity: sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==} @@ -1851,12 +2245,24 @@ packages: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + search-insights@2.17.3: resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==} secure-json-parse@2.7.0: resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + semver@7.7.4: resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} @@ -1876,6 +2282,22 @@ packages: shiki@2.5.0: resolution: {integrity: sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==} + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -2004,6 +2426,10 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + toad-cache@3.7.0: resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} engines: {node: '>=12'} @@ -2021,6 +2447,9 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + tslib@2.7.0: + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + tsup@8.5.1: resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} engines: {node: '>=18'} @@ -2045,10 +2474,26 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + tunnel@0.0.6: + resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} + engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} + type-detect@4.1.0: resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} engines: {node: '>=4'} + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + typed-inject@4.0.0: + resolution: {integrity: sha512-OuBL3G8CJlS/kjbGV/cN8Ni32+ktyyi6ADDZpKvksbX0fYBV5WcukhRCYa7WqLce7dY/Br2dwtmJ9diiadLFpg==} + engines: {node: '>=16'} + + typed-rest-client@2.1.0: + resolution: {integrity: sha512-Nel9aPbgSzRxfs1+4GoSB4wexCF+4Axlk7OSGVQCMa+4fWcyxIsN/YNmkp0xTT2iQzMD98h8yFLav/cNaULmRA==} + engines: {node: '>= 16.0.0'} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -2057,6 +2502,9 @@ packages: ufo@1.6.3: resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + underscore@1.13.8: + resolution: {integrity: sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==} + undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} @@ -2086,6 +2534,12 @@ packages: unist-util-visit@5.1.0: resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} @@ -2203,6 +2657,9 @@ packages: typescript: optional: true + weapon-regex@1.3.6: + resolution: {integrity: sha512-wsf1m1jmMrso5nhwVFJJHSubEBf3+pereGd7+nBKtYJ18KoB/PWJOHS3WRkwS04VrOU0iJr2bZU+l1QaTJ+9nA==} + web-streams-polyfill@4.0.0-beta.3: resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} engines: {node: '>= 14'} @@ -2223,6 +2680,10 @@ packages: engines: {node: '>=8'} hasBin: true + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -2246,10 +2707,17 @@ packages: utf-8-validate: optional: true + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yocto-queue@1.2.2: resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} engines: {node: '>=12.20'} + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + yoctocolors@2.1.2: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} @@ -2409,14 +2877,228 @@ snapshots: - encoding optional: true + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.25.9': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.25.9 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.25.9) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.25.9': + dependencies: + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.25.9)': + dependencies: + '@babel/core': 7.25.9 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.25.9) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.29.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.28.5': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.25.9)': + dependencies: + '@babel/core': 7.25.9 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-replace-supers@7.28.6(@babel/core@7.25.9)': + dependencies: + '@babel/core': 7.25.9 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + '@babel/helper-string-parser@7.27.1': {} '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.25.9': + dependencies: + '@babel/types': 7.29.0 + '@babel/parser@7.29.0': dependencies: '@babel/types': 7.29.0 + '@babel/plugin-proposal-decorators@7.24.7(@babel/core@7.25.9)': + dependencies: + '@babel/core': 7.25.9 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.25.9) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-decorators': 7.28.6(@babel/core@7.25.9) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-explicit-resource-management@7.27.4(@babel/core@7.25.9)': + dependencies: + '@babel/core': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.25.9) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-syntax-decorators@7.28.6(@babel/core@7.25.9)': + dependencies: + '@babel/core': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.25.9)': + dependencies: + '@babel/core': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.25.9)': + dependencies: + '@babel/core': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-destructuring@7.28.5(@babel/core@7.25.9)': + dependencies: + '@babel/core': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.25.9)': + dependencies: + '@babel/core': 7.25.9 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.25.9) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.25.9)': + dependencies: + '@babel/core': 7.25.9 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.25.9) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.25.9) + transitivePeerDependencies: + - supports-color + + '@babel/preset-typescript@7.24.7(@babel/core@7.25.9)': + dependencies: + '@babel/core': 7.25.9 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.25.9) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.25.9) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.25.9) + transitivePeerDependencies: + - supports-color + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + '@babel/types@7.29.0': dependencies: '@babel/helper-string-parser': 7.27.1 @@ -2650,6 +3332,102 @@ snapshots: '@iconify/types@2.0.0': {} + '@inquirer/checkbox@3.0.1': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 2.0.0 + ansi-escapes: 4.3.2 + yoctocolors-cjs: 2.1.3 + + '@inquirer/confirm@4.0.1': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 + + '@inquirer/core@9.2.1': + dependencies: + '@inquirer/figures': 1.0.15 + '@inquirer/type': 2.0.0 + '@types/mute-stream': 0.0.4 + '@types/node': 22.19.17 + '@types/wrap-ansi': 3.0.0 + ansi-escapes: 4.3.2 + cli-width: 4.1.0 + mute-stream: 1.0.0 + signal-exit: 4.1.0 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + + '@inquirer/editor@3.0.1': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 + external-editor: 3.1.0 + + '@inquirer/expand@3.0.1': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 + yoctocolors-cjs: 2.1.3 + + '@inquirer/figures@1.0.15': {} + + '@inquirer/input@3.0.1': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 + + '@inquirer/number@2.0.1': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 + + '@inquirer/password@3.0.1': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 + ansi-escapes: 4.3.2 + + '@inquirer/prompts@6.0.1': + dependencies: + '@inquirer/checkbox': 3.0.1 + '@inquirer/confirm': 4.0.1 + '@inquirer/editor': 3.0.1 + '@inquirer/expand': 3.0.1 + '@inquirer/input': 3.0.1 + '@inquirer/number': 2.0.1 + '@inquirer/password': 3.0.1 + '@inquirer/rawlist': 3.0.1 + '@inquirer/search': 2.0.1 + '@inquirer/select': 3.0.1 + + '@inquirer/rawlist@3.0.1': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 + yoctocolors-cjs: 2.1.3 + + '@inquirer/search@2.0.1': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 2.0.0 + yoctocolors-cjs: 2.1.3 + + '@inquirer/select@3.0.1': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 2.0.0 + ansi-escapes: 4.3.2 + yoctocolors-cjs: 2.1.3 + + '@inquirer/type@2.0.0': + dependencies: + mute-stream: 1.0.0 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -2805,6 +3583,69 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} + '@stryker-mutator/api@8.7.1': + dependencies: + mutation-testing-metrics: 3.3.0 + mutation-testing-report-schema: 3.3.0 + tslib: 2.7.0 + typed-inject: 4.0.0 + + '@stryker-mutator/core@8.7.1': + dependencies: + '@inquirer/prompts': 6.0.1 + '@stryker-mutator/api': 8.7.1 + '@stryker-mutator/instrumenter': 8.7.1 + '@stryker-mutator/util': 8.7.1 + ajv: 8.17.1 + chalk: 5.3.0 + commander: 12.1.0 + diff-match-patch: 1.0.5 + emoji-regex: 10.4.0 + execa: 9.4.1 + file-url: 4.0.0 + lodash.groupby: 4.6.0 + minimatch: 9.0.9 + mutation-testing-elements: 3.4.0 + mutation-testing-metrics: 3.3.0 + mutation-testing-report-schema: 3.3.0 + npm-run-path: 6.0.0 + progress: 2.0.3 + rxjs: 7.8.2 + semver: 7.7.4 + source-map: 0.7.6 + tree-kill: 1.2.2 + tslib: 2.7.0 + typed-inject: 4.0.0 + typed-rest-client: 2.1.0 + transitivePeerDependencies: + - supports-color + + '@stryker-mutator/instrumenter@8.7.1': + dependencies: + '@babel/core': 7.25.9 + '@babel/generator': 7.25.9 + '@babel/parser': 7.25.9 + '@babel/plugin-proposal-decorators': 7.24.7(@babel/core@7.25.9) + '@babel/plugin-proposal-explicit-resource-management': 7.27.4(@babel/core@7.25.9) + '@babel/preset-typescript': 7.24.7(@babel/core@7.25.9) + '@stryker-mutator/api': 8.7.1 + '@stryker-mutator/util': 8.7.1 + angular-html-parser: 6.0.2 + semver: 7.6.3 + weapon-regex: 1.3.6 + transitivePeerDependencies: + - supports-color + + '@stryker-mutator/util@8.7.1': {} + + '@stryker-mutator/vitest-runner@8.7.1(@stryker-mutator/core@8.7.1)(vitest@2.1.9(@types/node@20.19.34))': + dependencies: + '@stryker-mutator/api': 8.7.1 + '@stryker-mutator/core': 8.7.1 + '@stryker-mutator/util': 8.7.1 + tslib: 2.7.0 + vitest: 2.1.9(@types/node@20.19.34) + '@types/estree@1.0.8': {} '@types/hast@3.0.4': @@ -2826,6 +3667,10 @@ snapshots: '@types/mdurl@2.0.0': {} + '@types/mute-stream@0.0.4': + dependencies: + '@types/node': 20.19.37 + '@types/node-fetch@2.6.13': dependencies: '@types/node': 20.19.37 @@ -2843,10 +3688,16 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/node@22.19.17': + dependencies: + undici-types: 6.21.0 + '@types/unist@3.0.3': {} '@types/web-bluetooth@0.0.21': {} + '@types/wrap-ansi@3.0.0': {} + '@ungap/structured-clone@1.3.0': {} '@vitejs/plugin-vue@5.2.4(vite@5.4.21(@types/node@20.19.34))(vue@3.5.29(typescript@5.9.3))': @@ -3064,6 +3915,13 @@ snapshots: optionalDependencies: ajv: 8.18.0 + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 @@ -3088,6 +3946,14 @@ snapshots: '@algolia/requester-fetch': 5.49.1 '@algolia/requester-node-http': 5.49.1 + angular-html-parser@6.0.2: + dependencies: + tslib: 2.7.0 + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} @@ -3121,6 +3987,8 @@ snapshots: balanced-match@4.0.4: {} + baseline-browser-mapping@2.10.18: {} + birpc@2.9.0: {} brace-expansion@2.0.2: @@ -3131,6 +3999,14 @@ snapshots: dependencies: balanced-match: 4.0.4 + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.18 + caniuse-lite: 1.0.30001787 + electron-to-chromium: 1.5.335 + node-releases: 2.0.37 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + bundle-require@5.1.0(esbuild@0.27.3): dependencies: esbuild: 0.27.3 @@ -3143,6 +4019,13 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + caniuse-lite@1.0.30001787: {} + ccount@2.0.1: {} chai@4.5.0: @@ -3163,12 +4046,16 @@ snapshots: loupe: 3.2.1 pathval: 2.0.1 + chalk@5.3.0: {} + chalk@5.6.2: {} character-entities-html4@2.1.0: {} character-entities-legacy@3.0.0: {} + chardet@0.7.0: {} + check-error@1.0.3: dependencies: get-func-name: 2.0.2 @@ -3179,6 +4066,8 @@ snapshots: dependencies: readdirp: 4.1.2 + cli-width@4.1.0: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -3199,6 +4088,8 @@ snapshots: consola@3.4.2: {} + convert-source-map@2.0.0: {} + cookie@0.7.2: {} copy-anything@4.0.5: @@ -3227,10 +4118,17 @@ snapshots: dequal@2.0.3: {} + des.js@1.1.0: + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + devlop@1.1.0: dependencies: dequal: 2.0.3 + diff-match-patch@1.0.5: {} + diff-sequences@29.6.3: {} dunder-proto@1.0.1: @@ -3241,8 +4139,12 @@ snapshots: eastasianwidth@0.2.0: {} + electron-to-chromium@1.5.335: {} + emoji-regex-xs@1.0.0: {} + emoji-regex@10.4.0: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -3325,6 +4227,8 @@ snapshots: '@esbuild/win32-ia32': 0.27.3 '@esbuild/win32-x64': 0.27.3 + escalade@3.2.0: {} + estree-walker@2.0.2: {} estree-walker@3.0.3: @@ -3345,6 +4249,21 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 + execa@9.4.1: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.3.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.2 + execa@9.6.1: dependencies: '@sindresorhus/merge-streams': 4.0.0 @@ -3362,6 +4281,12 @@ snapshots: expect-type@1.3.0: {} + external-editor@3.1.0: + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + fast-content-type-parse@1.1.0: {} fast-decode-uri-component@1.0.1: {} @@ -3419,6 +4344,8 @@ snapshots: dependencies: is-unicode-supported: 2.1.0 + file-url@4.0.0: {} + find-my-way@8.2.2: dependencies: fast-deep-equal: 3.1.3 @@ -3462,6 +4389,8 @@ snapshots: function-bind@1.1.2: {} + gensync@1.0.0-beta.2: {} + get-func-name@2.0.2: {} get-intrinsic@1.3.0: @@ -3548,6 +4477,12 @@ snapshots: dependencies: ms: 2.1.3 + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + inherits@2.0.4: {} + ipaddr.js@1.9.1: {} is-fullwidth-code-point@3.0.0: {} @@ -3593,18 +4528,26 @@ snapshots: joycon@3.1.1: {} + js-md4@0.3.2: {} + + js-tokens@4.0.0: {} + js-tokens@9.0.1: {} js-yaml@4.1.1: dependencies: argparse: 2.0.1 + jsesc@3.1.0: {} + json-schema-ref-resolver@1.0.1: dependencies: fast-deep-equal: 3.1.3 json-schema-traverse@1.0.0: {} + json5@2.2.3: {} + light-my-request@5.14.0: dependencies: cookie: 0.7.2 @@ -3622,6 +4565,8 @@ snapshots: mlly: 1.8.0 pkg-types: 1.3.1 + lodash.groupby@4.6.0: {} + loupe@2.3.7: dependencies: get-func-name: 2.0.2 @@ -3630,6 +4575,10 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -3687,6 +4636,8 @@ snapshots: mimic-fn@4.0.0: {} + minimalistic-assert@1.0.1: {} + minimatch@10.2.4: dependencies: brace-expansion: 5.0.4 @@ -3710,6 +4661,16 @@ snapshots: ms@2.1.3: {} + mutation-testing-elements@3.4.0: {} + + mutation-testing-metrics@3.3.0: + dependencies: + mutation-testing-report-schema: 3.3.0 + + mutation-testing-report-schema@3.3.0: {} + + mute-stream@1.0.0: {} + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -3724,6 +4685,8 @@ snapshots: dependencies: whatwg-url: 5.0.0 + node-releases@2.0.37: {} + npm-run-path@5.3.0: dependencies: path-key: 4.0.0 @@ -3735,6 +4698,8 @@ snapshots: object-assign@4.1.1: {} + object-inspect@1.13.4: {} + on-exit-leak-free@2.1.2: {} once@1.4.0: @@ -3751,6 +4716,8 @@ snapshots: regex: 6.1.0 regex-recursion: 6.0.2 + os-tmpdir@1.0.2: {} + p-limit@5.0.0: dependencies: yocto-queue: 1.2.2 @@ -3839,6 +4806,8 @@ snapshots: process-warning@5.0.0: {} + progress@2.0.3: {} + property-information@7.1.0: {} proxy-addr@2.0.7: @@ -3846,6 +4815,10 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 + qs@6.15.1: + dependencies: + side-channel: 1.1.0 + quick-format-unescaped@4.0.4: {} react-is@18.3.1: {} @@ -3907,16 +4880,26 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.59.0 fsevents: 2.3.3 + rxjs@7.8.2: + dependencies: + tslib: 2.7.0 + safe-regex2@3.1.0: dependencies: ret: 0.4.3 safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} + search-insights@2.17.3: {} secure-json-parse@2.7.0: {} + semver@6.3.1: {} + + semver@7.6.3: {} + semver@7.7.4: {} set-cookie-parser@2.7.2: {} @@ -3938,6 +4921,34 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} signal-exit@4.1.0: {} @@ -4052,6 +5063,10 @@ snapshots: tinyspy@3.0.2: {} + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + toad-cache@3.7.0: {} tr46@0.0.3: {} @@ -4062,6 +5077,8 @@ snapshots: ts-interface-checker@0.1.13: {} + tslib@2.7.0: {} + tsup@8.5.1(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3): dependencies: bundle-require: 5.1.0(esbuild@0.27.3) @@ -4097,12 +5114,28 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + tunnel@0.0.6: {} + type-detect@4.1.0: {} + type-fest@0.21.3: {} + + typed-inject@4.0.0: {} + + typed-rest-client@2.1.0: + dependencies: + des.js: 1.1.0 + js-md4: 0.3.2 + qs: 6.15.1 + tunnel: 0.0.6 + underscore: 1.13.8 + typescript@5.9.3: {} ufo@1.6.3: {} + underscore@1.13.8: {} + undici-types@5.26.5: {} undici-types@6.21.0: {} @@ -4136,6 +5169,12 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 @@ -4328,6 +5367,8 @@ snapshots: optionalDependencies: typescript: 5.9.3 + weapon-regex@1.3.6: {} + web-streams-polyfill@4.0.0-beta.3: {} webidl-conversions@3.0.1: {} @@ -4346,6 +5387,12 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -4362,8 +5409,12 @@ snapshots: ws@8.19.0: {} + yallist@3.1.1: {} + yocto-queue@1.2.2: {} + yoctocolors-cjs@2.1.3: {} + yoctocolors@2.1.2: {} zod-to-json-schema@3.25.1(zod@3.25.76): From c09f0106003678999c246f9c22cf3ca1261a1396 Mon Sep 17 00:00:00 2001 From: iliassjabali Date: Sun, 12 Apr 2026 02:21:06 +0100 Subject: [PATCH 05/26] chore: add explicit vitest configs to adapter-claude, mcp-server, cli --- packages/adapter-claude/vitest.config.ts | 9 +++++++++ packages/cli/vitest.config.ts | 11 +++++++++++ packages/mcp-server/vitest.config.ts | 11 +++++++++++ 3 files changed, 31 insertions(+) create mode 100644 packages/adapter-claude/vitest.config.ts create mode 100644 packages/cli/vitest.config.ts create mode 100644 packages/mcp-server/vitest.config.ts diff --git a/packages/adapter-claude/vitest.config.ts b/packages/adapter-claude/vitest.config.ts new file mode 100644 index 0000000..471771e --- /dev/null +++ b/packages/adapter-claude/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + globals: false, + environment: 'node', + include: ['src/**/*.test.ts'], + }, +}) diff --git a/packages/cli/vitest.config.ts b/packages/cli/vitest.config.ts new file mode 100644 index 0000000..1fb81a3 --- /dev/null +++ b/packages/cli/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + globals: false, + environment: 'node', + include: ['src/**/*.test.ts'], + // cli.test.ts spawns the built CLI binary; needs extra time + testTimeout: 15_000, + }, +}) diff --git a/packages/mcp-server/vitest.config.ts b/packages/mcp-server/vitest.config.ts new file mode 100644 index 0000000..ec9f852 --- /dev/null +++ b/packages/mcp-server/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + globals: false, + environment: 'node', + include: ['src/**/*.test.ts'], + // transport.test.ts spawns tsx subprocesses; allow extra time per test + testTimeout: 15_000, + }, +}) From 54cf8f4ae79426823eeef50c13f43eab68313475 Mon Sep 17 00:00:00 2001 From: iliassjabali Date: Sun, 12 Apr 2026 06:23:36 +0100 Subject: [PATCH 06/26] chore(cli): add stryker-only vitest config that excludes cli.test.ts --- packages/cli/vitest.stryker.config.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 packages/cli/vitest.stryker.config.ts diff --git a/packages/cli/vitest.stryker.config.ts b/packages/cli/vitest.stryker.config.ts new file mode 100644 index 0000000..4e90fc4 --- /dev/null +++ b/packages/cli/vitest.stryker.config.ts @@ -0,0 +1,14 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import baseConfig from './vitest.config' + +export default mergeConfig( + baseConfig, + defineConfig({ + test: { + // cli.test.ts spawns node dist/cli.js — those tests can't see Stryker + // mutations applied to src/, so they would mark every cli source mutant + // as "no coverage" and treat them as survived. Skip them under mutation. + exclude: ['**/node_modules/**', '**/dist/**', 'src/__tests__/cli.test.ts'], + }, + }), +) From bc4dadd32a911550f43c36acb791b7a63e898f63 Mon Sep 17 00:00:00 2001 From: iliassjabali Date: Sun, 12 Apr 2026 06:24:05 +0100 Subject: [PATCH 07/26] chore: add root stryker config with shared defaults --- stryker.config.json | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 stryker.config.json diff --git a/stryker.config.json b/stryker.config.json new file mode 100644 index 0000000..4a2ab1f --- /dev/null +++ b/stryker.config.json @@ -0,0 +1,25 @@ +{ + "$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json", + "_comment": "Per-package thresholds and mutate globs are passed at invocation time via CLI flags in package.json scripts. adapter-claude is gated at 70 (override). All other packages inherit break=60 from this file. See docs/mutation-testing.md.", + "packageManager": "pnpm", + "testRunner": "vitest", + "reporters": ["html", "progress", "clear-text"], + "coverageAnalysis": "perTest", + "incremental": true, + "concurrency": 4, + "timeoutMS": 60000, + "thresholds": { + "high": 75, + "low": 60, + "break": 60 + }, + "mutate": [ + "packages/*/src/**/*.ts", + "!packages/*/src/**/*.test.ts", + "!packages/*/src/**/__tests__/**", + "!packages/*/src/**/*.d.ts", + "!packages/sdk/src/scripts/export-schema.ts" + ], + "tempDirName": ".stryker-tmp", + "htmlReporter": { "fileName": "reports/mutation/index.html" } +} From fffbb993efb873acadddb2bf80ae7a04ecf1636c Mon Sep 17 00:00:00 2001 From: iliassjabali Date: Sun, 12 Apr 2026 06:24:20 +0100 Subject: [PATCH 08/26] chore: add per-package mutation scripts to root package.json --- package.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 7a8da93..3fcf1b0 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,13 @@ "lint": "pnpm -r lint", "clean": "pnpm -r clean", "typecheck": "pnpm -r typecheck", - "schema:export": "tsx packages/sdk/src/scripts/export-schema.ts" + "schema:export": "tsx packages/sdk/src/scripts/export-schema.ts", + "mutation:adapter-claude": "stryker run --mutate 'packages/adapter-claude/src/**/*.ts' --thresholds.break 70 --incrementalFile .stryker-tmp/adapter-claude-incremental.json --htmlReporter.fileName reports/mutation/adapter-claude.html --vitest.configFile packages/adapter-claude/vitest.config.ts", + "mutation:sdk": "stryker run --mutate 'packages/sdk/src/**/*.ts' --incrementalFile .stryker-tmp/sdk-incremental.json --htmlReporter.fileName reports/mutation/sdk.html --vitest.configFile packages/sdk/vitest.config.ts", + "mutation:mcp-server": "stryker run --mutate 'packages/mcp-server/src/**/*.ts' --incrementalFile .stryker-tmp/mcp-server-incremental.json --htmlReporter.fileName reports/mutation/mcp-server.html --vitest.configFile packages/mcp-server/vitest.config.ts", + "mutation:cli": "stryker run --mutate 'packages/cli/src/**/*.ts' --incrementalFile .stryker-tmp/cli-incremental.json --htmlReporter.fileName reports/mutation/cli.html --vitest.configFile packages/cli/vitest.stryker.config.ts", + "mutation:sidecar": "stryker run --mutate 'packages/sidecar/src/**/*.ts' --incrementalFile .stryker-tmp/sidecar-incremental.json --htmlReporter.fileName reports/mutation/sidecar.html --vitest.configFile packages/sidecar/vitest.config.ts", + "mutation": "pnpm mutation:adapter-claude && pnpm mutation:sdk && pnpm mutation:mcp-server && pnpm mutation:cli && pnpm mutation:sidecar" }, "devDependencies": { "@stryker-mutator/core": "^8.7.1", From 8c96bca8200689e70286f8bea4ea5a115609f451 Mon Sep 17 00:00:00 2001 From: iliassjabali Date: Sun, 12 Apr 2026 06:24:32 +0100 Subject: [PATCH 09/26] chore: ignore .stryker-tmp and reports/mutation directories --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 26c5207..9f14f91 100644 --- a/.gitignore +++ b/.gitignore @@ -59,6 +59,10 @@ packages/sdk-python/.venv/ # ── Worktrees ───────────────────────────────────────────────────────────────── .worktrees/ +# ── Stryker (mutation testing) ──────────────────────────────────────────────── +.stryker-tmp/ +reports/mutation/ + # ── Misc ────────────────────────────────────────────────────────────────────── *.ots commit.txt From d699f53b07de32bb3f97312d45bd33992dfd964c Mon Sep 17 00:00:00 2001 From: iliassjabali Date: Sun, 12 Apr 2026 06:31:45 +0100 Subject: [PATCH 10/26] fix: convert stryker config to mjs with env-var overrides Stryker CLI does not support nested flags like --thresholds.break or --vitest.configFile. Move all per-package overrides into stryker.config.mjs which reads STRYKER_PKG and STRYKER_BREAK env vars. The npm scripts set these env vars instead of passing CLI flags. --- package.json | 10 +++++----- stryker.config.json | 25 ------------------------- stryker.config.mjs | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 30 deletions(-) delete mode 100644 stryker.config.json create mode 100644 stryker.config.mjs diff --git a/package.json b/package.json index 3fcf1b0..3b8b90b 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,11 @@ "clean": "pnpm -r clean", "typecheck": "pnpm -r typecheck", "schema:export": "tsx packages/sdk/src/scripts/export-schema.ts", - "mutation:adapter-claude": "stryker run --mutate 'packages/adapter-claude/src/**/*.ts' --thresholds.break 70 --incrementalFile .stryker-tmp/adapter-claude-incremental.json --htmlReporter.fileName reports/mutation/adapter-claude.html --vitest.configFile packages/adapter-claude/vitest.config.ts", - "mutation:sdk": "stryker run --mutate 'packages/sdk/src/**/*.ts' --incrementalFile .stryker-tmp/sdk-incremental.json --htmlReporter.fileName reports/mutation/sdk.html --vitest.configFile packages/sdk/vitest.config.ts", - "mutation:mcp-server": "stryker run --mutate 'packages/mcp-server/src/**/*.ts' --incrementalFile .stryker-tmp/mcp-server-incremental.json --htmlReporter.fileName reports/mutation/mcp-server.html --vitest.configFile packages/mcp-server/vitest.config.ts", - "mutation:cli": "stryker run --mutate 'packages/cli/src/**/*.ts' --incrementalFile .stryker-tmp/cli-incremental.json --htmlReporter.fileName reports/mutation/cli.html --vitest.configFile packages/cli/vitest.stryker.config.ts", - "mutation:sidecar": "stryker run --mutate 'packages/sidecar/src/**/*.ts' --incrementalFile .stryker-tmp/sidecar-incremental.json --htmlReporter.fileName reports/mutation/sidecar.html --vitest.configFile packages/sidecar/vitest.config.ts", + "mutation:adapter-claude": "STRYKER_PKG=adapter-claude STRYKER_BREAK=70 stryker run", + "mutation:sdk": "STRYKER_PKG=sdk stryker run", + "mutation:mcp-server": "STRYKER_PKG=mcp-server stryker run", + "mutation:cli": "STRYKER_PKG=cli stryker run", + "mutation:sidecar": "STRYKER_PKG=sidecar stryker run", "mutation": "pnpm mutation:adapter-claude && pnpm mutation:sdk && pnpm mutation:mcp-server && pnpm mutation:cli && pnpm mutation:sidecar" }, "devDependencies": { diff --git a/stryker.config.json b/stryker.config.json deleted file mode 100644 index 4a2ab1f..0000000 --- a/stryker.config.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json", - "_comment": "Per-package thresholds and mutate globs are passed at invocation time via CLI flags in package.json scripts. adapter-claude is gated at 70 (override). All other packages inherit break=60 from this file. See docs/mutation-testing.md.", - "packageManager": "pnpm", - "testRunner": "vitest", - "reporters": ["html", "progress", "clear-text"], - "coverageAnalysis": "perTest", - "incremental": true, - "concurrency": 4, - "timeoutMS": 60000, - "thresholds": { - "high": 75, - "low": 60, - "break": 60 - }, - "mutate": [ - "packages/*/src/**/*.ts", - "!packages/*/src/**/*.test.ts", - "!packages/*/src/**/__tests__/**", - "!packages/*/src/**/*.d.ts", - "!packages/sdk/src/scripts/export-schema.ts" - ], - "tempDirName": ".stryker-tmp", - "htmlReporter": { "fileName": "reports/mutation/index.html" } -} diff --git a/stryker.config.mjs b/stryker.config.mjs new file mode 100644 index 0000000..85b60aa --- /dev/null +++ b/stryker.config.mjs @@ -0,0 +1,40 @@ +// Mutation testing config — single source of truth for all packages. +// +// Per-package behavior is driven by two env vars set in the npm scripts: +// STRYKER_PKG — package directory name (e.g. "adapter-claude", "sdk") +// STRYKER_BREAK — mutation score threshold that fails CI (default: 60) + +const pkg = process.env.STRYKER_PKG ?? '' +const breakThreshold = parseInt(process.env.STRYKER_BREAK ?? '60', 10) + +const vitestConfigFile = pkg === 'cli' + ? 'packages/cli/vitest.stryker.config.ts' + : pkg + ? `packages/${pkg}/vitest.config.ts` + : undefined + +/** @type {import('@stryker-mutator/api/core').PartialStrykerOptions} */ +export default { + packageManager: 'pnpm', + testRunner: 'vitest', + plugins: ['@stryker-mutator/vitest-runner'], + reporters: ['html', 'progress', 'clear-text'], + coverageAnalysis: 'perTest', + incremental: true, + concurrency: 4, + timeoutMS: 60000, + thresholds: { high: 75, low: 60, break: breakThreshold }, + mutate: [ + `packages/${pkg || '*'}/src/**/*.ts`, + `!packages/${pkg || '*'}/src/**/*.test.ts`, + `!packages/${pkg || '*'}/src/**/__tests__/**`, + `!packages/${pkg || '*'}/src/**/*.d.ts`, + '!packages/sdk/src/scripts/export-schema.ts', + ], + tempDirName: '.stryker-tmp', + ...(pkg ? { incrementalFile: `.stryker-tmp/${pkg}-incremental.json` } : {}), + htmlReporter: { + fileName: pkg ? `reports/mutation/${pkg}.html` : 'reports/mutation/index.html', + }, + ...(vitestConfigFile ? { vitest: { configFile: vitestConfigFile } } : {}), +} From 19e5af582fc88321797e0589bdf42dcca7e14751 Mon Sep 17 00:00:00 2001 From: iliassjabali Date: Sun, 12 Apr 2026 06:33:17 +0100 Subject: [PATCH 11/26] fix: add vitest dir option to stryker config for correct test discovery --- stryker.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stryker.config.mjs b/stryker.config.mjs index 85b60aa..822b26b 100644 --- a/stryker.config.mjs +++ b/stryker.config.mjs @@ -36,5 +36,5 @@ export default { htmlReporter: { fileName: pkg ? `reports/mutation/${pkg}.html` : 'reports/mutation/index.html', }, - ...(vitestConfigFile ? { vitest: { configFile: vitestConfigFile } } : {}), + ...(vitestConfigFile ? { vitest: { configFile: vitestConfigFile, dir: `packages/${pkg}` } } : {}), } From dc73429d903ff25316752c49a5fbf663b50318cb Mon Sep 17 00:00:00 2001 From: iliassjabali Date: Sun, 12 Apr 2026 06:51:54 +0100 Subject: [PATCH 12/26] chore: set mutation thresholds from pilot scores (actual - 5) --- package.json | 10 +++++----- stryker.config.mjs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 3b8b90b..0e99771 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,11 @@ "clean": "pnpm -r clean", "typecheck": "pnpm -r typecheck", "schema:export": "tsx packages/sdk/src/scripts/export-schema.ts", - "mutation:adapter-claude": "STRYKER_PKG=adapter-claude STRYKER_BREAK=70 stryker run", - "mutation:sdk": "STRYKER_PKG=sdk stryker run", - "mutation:mcp-server": "STRYKER_PKG=mcp-server stryker run", - "mutation:cli": "STRYKER_PKG=cli stryker run", - "mutation:sidecar": "STRYKER_PKG=sidecar stryker run", + "mutation:adapter-claude": "STRYKER_PKG=adapter-claude STRYKER_BREAK=45 stryker run", + "mutation:sdk": "STRYKER_PKG=sdk STRYKER_BREAK=49 stryker run", + "mutation:mcp-server": "STRYKER_PKG=mcp-server STRYKER_BREAK=26 stryker run", + "mutation:cli": "STRYKER_PKG=cli STRYKER_BREAK=45 stryker run", + "mutation:sidecar": "STRYKER_PKG=sidecar STRYKER_BREAK=46 stryker run", "mutation": "pnpm mutation:adapter-claude && pnpm mutation:sdk && pnpm mutation:mcp-server && pnpm mutation:cli && pnpm mutation:sidecar" }, "devDependencies": { diff --git a/stryker.config.mjs b/stryker.config.mjs index 822b26b..348e290 100644 --- a/stryker.config.mjs +++ b/stryker.config.mjs @@ -23,7 +23,7 @@ export default { incremental: true, concurrency: 4, timeoutMS: 60000, - thresholds: { high: 75, low: 60, break: breakThreshold }, + thresholds: { high: 80, low: 40, break: breakThreshold }, mutate: [ `packages/${pkg || '*'}/src/**/*.ts`, `!packages/${pkg || '*'}/src/**/*.test.ts`, From 70ba66227662fc327a59907c24d54b9e08fc67f5 Mon Sep 17 00:00:00 2001 From: iliassjabali Date: Sun, 12 Apr 2026 06:54:14 +0100 Subject: [PATCH 13/26] docs: record pilot mutation scores in design spec --- .../2026-04-12-mutation-testing-design.md | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/superpowers/specs/2026-04-12-mutation-testing-design.md b/docs/superpowers/specs/2026-04-12-mutation-testing-design.md index 98dc86f..e9f208f 100644 --- a/docs/superpowers/specs/2026-04-12-mutation-testing-design.md +++ b/docs/superpowers/specs/2026-04-12-mutation-testing-design.md @@ -66,7 +66,7 @@ Per-package overrides happen at invocation time via CLI flags (see scripts below ```json { "scripts": { - "mutation:adapter-claude": "stryker run --mutate 'packages/adapter-claude/src/**/*.ts' --thresholds.break 70 --incrementalFile .stryker-tmp/adapter-claude-incremental.json --htmlReporter.fileName reports/mutation/adapter-claude.html", + "mutation:adapter-claude": "stryker run --mutate 'packages/adapter-claude/src/**/*.ts' --thresholds.break 45 --incrementalFile .stryker-tmp/adapter-claude-incremental.json --htmlReporter.fileName reports/mutation/adapter-claude.html", "mutation:sdk": "stryker run --mutate 'packages/sdk/src/**/*.ts' --incrementalFile .stryker-tmp/sdk-incremental.json --htmlReporter.fileName reports/mutation/sdk.html", "mutation:mcp-server": "stryker run --mutate 'packages/mcp-server/src/**/*.ts' --incrementalFile .stryker-tmp/mcp-server-incremental.json --htmlReporter.fileName reports/mutation/mcp-server.html", "mutation:cli": "stryker run --mutate 'packages/cli/src/**/*.ts' --incrementalFile .stryker-tmp/cli-incremental.json --htmlReporter.fileName reports/mutation/cli.html", @@ -80,15 +80,15 @@ Per-package overrides happen at invocation time via CLI flags (see scripts below ### Per-package thresholds -| Package | `break` (gate) | Rationale | -|---|---|---| -| adapter-claude | **70** | Pure functions, no I/O, dense test coverage from PR #46. Should clear 70 comfortably. Higher gate makes the gate meaningful. | -| sdk | 60 | Larger surface, some I/O paths (loaders, resolvers). 60 is conservative starting point. | -| mcp-server | 60 | Many tools are thin spawnCli wrappers (low mutation value); transport tests do exercise real code paths. | -| cli | 60 | Largest package, lots of command wiring. Some integration tests excluded (see below). | -| sidecar | 60 | HTTP server with mocked dependencies. | +| Package | `break` (gate) | Pilot score (local) | Rationale | +|---|---|---|---| +| adapter-claude | **45** | 49.57% | Pure functions, no I/O. Score limited by 32 no-coverage mutants in index.ts. | +| sdk | 49 | 54.31% | Larger surface, some I/O paths (loaders, resolvers). | +| mcp-server | 26 | 31.31% | 413 of 658 mutants have zero test coverage; most production code in index.ts (dispatch/RPC/transport) is untested on main. | +| cli | 45 | 49.50% | Largest package, lots of command wiring. cli.test.ts excluded from Stryker test set. | +| sidecar | 46 | 51.43% | HTTP server with mocked dependencies. | -Thresholds are starting points. The first CI run on this PR determines whether they're realistic. If a package fails its own threshold on first run, the threshold drops to `(actual − 5)` for that package and the PR description notes the discrepancy for reviewers. +Thresholds are starting points. The first CI run on this PR determines whether they're realistic. If a package fails its own threshold on first run, the threshold drops to `(actual - 5)` for that package and the PR description notes the discrepancy for reviewers. ### Test scope per package @@ -178,16 +178,16 @@ Worst-case PR (touches every package's source) ≈ ~80 min wall-clock, runs in p ## Verification plan -1. **Local pilot**: `pnpm mutation:adapter-claude` produces a score and an HTML report at `reports/mutation/adapter-claude.html`. Confirm score ≥ 70. +1. **Local pilot**: `pnpm mutation:adapter-claude` produces a score and an HTML report at `reports/mutation/adapter-claude.html`. Confirm score >= 45. 2. **Local sweep**: `pnpm mutation` runs all 5 sequentially. For each package, capture the actual score and confirm it clears the proposed threshold. If any package fails, drop its threshold to `actual − 5` and note in the PR description. 3. **CI first run**: open the PR, watch the 5 jobs run in parallel from a cold cache. All 5 should produce a score and a green check. 4. **CI cache validation**: push an empty commit. Confirm every cache restores and every job finishes in ≤ 30 s. 5. **CI partial-change validation**: push a 1-line change to `packages/sdk/src/loader/resolvers.ts`. Confirm 4 jobs hit cache (≤ 30 s each) and 1 job (`sdk`) re-mutates only `resolvers.ts` while reusing cached results for the rest of the package. -6. **CI gate validation**: temporarily delete an assertion in `packages/adapter-claude/src/__tests__/claude-adapter.test.ts`, push, confirm the `adapter-claude` job goes red with a score below 70 and the PR check is blocked. Revert the change, confirm the gate restores to green. +6. **CI gate validation**: temporarily delete an assertion in `packages/adapter-claude/src/__tests__/claude-adapter.test.ts`, push, confirm the `adapter-claude` job goes red with a score below 45 and the PR check is blocked. Revert the change, confirm the gate restores to green. ## Trade-offs and known limitations -- **Threshold drift risk**: with a single config and one CLI override, it's easy to forget that adapter-claude has a stricter gate. `stryker.config.json` is JSON (no comments) so the only place the override lives is the `--thresholds.break 70` flag in `package.json`'s `mutation:adapter-claude` script. Mitigation: `docs/mutation-testing.md` documents the table of per-package thresholds, and the gate failure on CI (red check) makes drift loud. +- **Threshold drift risk**: with a single config and one CLI override, it's easy to forget that adapter-claude has a stricter gate. `stryker.config.json` is JSON (no comments) so the only place the override lives is the `--thresholds.break 45` flag in `package.json`'s `mutation:adapter-claude` script. Mitigation: `docs/mutation-testing.md` documents the table of per-package thresholds, and the gate failure on CI (red check) makes drift loud. - **Future per-package customization** (excluding a noisy file from one package only) becomes an additional CLI flag rather than a config-file edit. If divergence grows beyond 1-2 flags per package, we may want to migrate to per-package configs after all. - **`pnpm mutation` (the all-in-one local script)** runs packages sequentially, not in parallel, because Stryker's vitest runner uses a shared `.stryker-tmp/` working directory and parallel runs would race. CI uses separate runners so each gets its own working directory. - **First CI run is uncached** and may take up to ~80 min for the slowest package. Subsequent runs hit cache. From 5b70cc5996893c0133c000166bc02760bad6ee0f Mon Sep 17 00:00:00 2001 From: iliassjabali Date: Sun, 12 Apr 2026 06:54:17 +0100 Subject: [PATCH 14/26] ci: add mutation testing workflow with per-package matrix --- .github/workflows/mutation.yml | 53 ++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/workflows/mutation.yml diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml new file mode 100644 index 0000000..275c600 --- /dev/null +++ b/.github/workflows/mutation.yml @@ -0,0 +1,53 @@ +name: mutation +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + mutation: + name: mutation / ${{ matrix.package }} + runs-on: ubuntu-latest + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + package: [adapter-claude, sdk, mcp-server, cli, sidecar] + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build all packages + run: pnpm -r build + + - name: Restore Stryker incremental cache + uses: actions/cache@v4 + with: + path: .stryker-tmp/${{ matrix.package }}-incremental.json + key: stryker-${{ matrix.package }}-${{ hashFiles(format('packages/{0}/src/**', matrix.package), 'stryker.config.mjs') }} + restore-keys: | + stryker-${{ matrix.package }}- + + - name: Run Stryker + run: pnpm mutation:${{ matrix.package }} + + - name: Upload HTML report + if: always() + uses: actions/upload-artifact@v4 + with: + name: mutation-report-${{ matrix.package }} + path: reports/mutation/${{ matrix.package }}.html + retention-days: 14 From 697331593487adfd1b1d3572f5c718022397541c Mon Sep 17 00:00:00 2001 From: iliassjabali Date: Sun, 12 Apr 2026 06:54:24 +0100 Subject: [PATCH 15/26] docs: add mutation testing user guide --- docs/mutation-testing.md | 79 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 docs/mutation-testing.md diff --git a/docs/mutation-testing.md b/docs/mutation-testing.md new file mode 100644 index 0000000..82b3780 --- /dev/null +++ b/docs/mutation-testing.md @@ -0,0 +1,79 @@ +# Mutation Testing + +AgentSpec uses [Stryker Mutator](https://stryker-mutator.io) to test the strength of its test suite. Stryker mutates production code in small ways (negate a condition, drop a statement, change `>` to `>=`) and re-runs the suite. If the suite still passes, the mutant "survived" and the test is too weak. + +## Running locally + +```bash +# Run for a single package +pnpm mutation:adapter-claude +pnpm mutation:sdk +pnpm mutation:mcp-server +pnpm mutation:cli +pnpm mutation:sidecar + +# Run all packages sequentially +pnpm mutation +``` + +Each run produces an HTML report at `reports/mutation/.html`. Open it to see which mutants survived and where. + +The first run for a package is slow (full mutation pass). Subsequent runs use Stryker's incremental cache (`.stryker-tmp/-incremental.json`) and only re-mutate files that changed. + +## Per-package thresholds + +| Package | `break` threshold | Pilot score | Set in | +|---|---|---|---| +| adapter-claude | 45 | 49.57% | `package.json` `mutation:adapter-claude` script (`STRYKER_BREAK=45`) | +| sdk | 49 | 54.31% | `package.json` `mutation:sdk` script (`STRYKER_BREAK=49`) | +| mcp-server | 26 | 31.31% | `package.json` `mutation:mcp-server` script (`STRYKER_BREAK=26`) | +| cli | 45 | 49.50% | `package.json` `mutation:cli` script (`STRYKER_BREAK=45`) | +| sidecar | 46 | 51.43% | `package.json` `mutation:sidecar` script (`STRYKER_BREAK=46`) | + +The default threshold (when `STRYKER_BREAK` is unset) is 40, defined in `stryker.config.mjs`. + +If your local run shows a score below the threshold, the command exits non-zero. CI uses the same thresholds and will fail the corresponding `mutation / ` check on the PR. + +## CI + +`.github/workflows/mutation.yml` runs all 5 packages in parallel on every PR (open / synchronize / reopen). Each job: + +1. Restores its package's Stryker incremental cache +2. Runs `pnpm mutation:` +3. Uploads the HTML report as a workflow artifact (`mutation-report-`, 14-day retention) +4. Exits non-zero if the mutation score is below the package's `break` threshold + +A PR cannot merge until all 5 mutation checks pass. + +## When CI is red + +1. Open the PR's failing mutation check. +2. Click the "Summary" tab and download the `mutation-report-` artifact. +3. Open `.html` locally. Surviving mutants are highlighted in red. +4. For each survivor, decide: + - **Test gap**: write a test that would fail if the mutant were the real implementation. Push. + - **Equivalent mutant**: the mutation produces semantically identical behavior (e.g., changing `i++` to `++i` in a loop where the prefix/postfix difference is invisible). These can be ignored. If equivalents push the score below the threshold, the threshold should be lowered, not the test suite padded. + +## Ratcheting the threshold + +When the mutation score for a package improves consistently above its threshold, raise the threshold to lock in the gain: + +1. Edit the `STRYKER_BREAK=N` value in the package's mutation script in `package.json`. +2. Open a PR. CI will validate the new threshold against the actual score. + +The goal is to ratchet up over time, never down. + +## What's excluded from mutation + +- `__tests__/` directories and `*.test.ts` files +- `*.d.ts` files (type definitions only) +- `dist/` builds +- `packages/sdk/src/scripts/export-schema.ts` (generated artefact bootstrap) +- `packages/cli/src/__tests__/cli.test.ts` is excluded from the **test** set (not the mutate set) because it spawns the pre-built `dist/cli.js` binary and can't see source mutations. Other cli tests still run. + +## Configuration + +All config lives in `stryker.config.mjs` at the repo root. Per-package overrides are driven by two env vars set in the npm scripts: + +- `STRYKER_PKG` selects which package to mutate and which vitest config to use +- `STRYKER_BREAK` sets the failure threshold (default: 40 from the config file) From 2eb648c81a7ade343e8418bd62705e2826ae329b Mon Sep 17 00:00:00 2001 From: iliassjabali Date: Sun, 12 Apr 2026 07:07:06 +0100 Subject: [PATCH 16/26] docs: move mutation testing docs into guides/ and decisions/ - docs/mutation-testing.md -> docs/guides/mutation-testing.md - docs/superpowers/specs/... -> docs/decisions/2026-04-12-mutation-testing.md - docs/superpowers/plans/... -> docs/decisions/2026-04-12-mutation-testing-plan.md - Remove docs/superpowers/ (skill framework artifact, not project structure) --- .../2026-04-12-mutation-testing-plan.md} | 0 .../2026-04-12-mutation-testing.md} | 0 docs/{ => guides}/mutation-testing.md | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename docs/{superpowers/plans/2026-04-12-mutation-testing.md => decisions/2026-04-12-mutation-testing-plan.md} (100%) rename docs/{superpowers/specs/2026-04-12-mutation-testing-design.md => decisions/2026-04-12-mutation-testing.md} (100%) rename docs/{ => guides}/mutation-testing.md (100%) diff --git a/docs/superpowers/plans/2026-04-12-mutation-testing.md b/docs/decisions/2026-04-12-mutation-testing-plan.md similarity index 100% rename from docs/superpowers/plans/2026-04-12-mutation-testing.md rename to docs/decisions/2026-04-12-mutation-testing-plan.md diff --git a/docs/superpowers/specs/2026-04-12-mutation-testing-design.md b/docs/decisions/2026-04-12-mutation-testing.md similarity index 100% rename from docs/superpowers/specs/2026-04-12-mutation-testing-design.md rename to docs/decisions/2026-04-12-mutation-testing.md diff --git a/docs/mutation-testing.md b/docs/guides/mutation-testing.md similarity index 100% rename from docs/mutation-testing.md rename to docs/guides/mutation-testing.md From 41f85b1ab08651c74e261ef597c18c17daaf04c5 Mon Sep 17 00:00:00 2001 From: iliassjabali Date: Sun, 12 Apr 2026 07:08:51 +0100 Subject: [PATCH 17/26] fix(ci): specify pnpm version 10 in mutation workflow --- .github/workflows/mutation.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml index 275c600..e621e14 100644 --- a/.github/workflows/mutation.yml +++ b/.github/workflows/mutation.yml @@ -20,6 +20,8 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v4 + with: + version: 10 - name: Setup Node uses: actions/setup-node@v4 From 270801f3bd31be9e7e2f92599c14a07a99bfc868 Mon Sep 17 00:00:00 2001 From: iliassjabali Date: Sun, 12 Apr 2026 07:11:08 +0100 Subject: [PATCH 18/26] docs: simplify mutation testing to a single guide, remove decision docs --- .../2026-04-12-mutation-testing-plan.md | 867 ------------------ docs/decisions/2026-04-12-mutation-testing.md | 202 ---- docs/guides/mutation-testing.md | 89 +- 3 files changed, 26 insertions(+), 1132 deletions(-) delete mode 100644 docs/decisions/2026-04-12-mutation-testing-plan.md delete mode 100644 docs/decisions/2026-04-12-mutation-testing.md diff --git a/docs/decisions/2026-04-12-mutation-testing-plan.md b/docs/decisions/2026-04-12-mutation-testing-plan.md deleted file mode 100644 index a2df258..0000000 --- a/docs/decisions/2026-04-12-mutation-testing-plan.md +++ /dev/null @@ -1,867 +0,0 @@ -# Mutation Testing Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add mutation testing across all 5 packages with a single root Stryker config, per-package CI gates, and incremental caching so unchanged packages cost ~15s per PR run. - -**Architecture:** One `stryker.config.json` at the repo root holds shared settings. Per-package overrides happen via CLI flags in root npm scripts (`mutation:adapter-claude`, `mutation:sdk`, etc.). A GitHub Actions matrix runs all 5 jobs in parallel on every PR; each job restores a per-package Stryker incremental cache so packages that didn't change finish near-instantly. The `break` threshold is 70 for adapter-claude and 60 for the rest. - -**Tech Stack:** -- `@stryker-mutator/core` + `@stryker-mutator/vitest-runner` (mutation testing) -- pnpm workspace (monorepo) -- vitest (test runner — both v1.6 in mcp-server and v2.1 elsewhere are supported) -- GitHub Actions (CI) - -**Spec:** `docs/superpowers/specs/2026-04-12-mutation-testing-design.md` - -**Working directory:** `/Users/iliassjabali/Dev/agentspec/.worktrees/mutation-testing` (branch `chore/mutation-testing`, already created off `main`) - ---- - -## File Structure - -| File | Action | Responsibility | -|---|---|---| -| `package.json` (root) | Modify | Add 6 mutation scripts + Stryker dev-deps | -| `stryker.config.json` (root) | Create | Single source of truth for shared Stryker settings | -| `.gitignore` (root) | Modify | Add `.stryker-tmp/` and `reports/mutation/` | -| `packages/adapter-claude/vitest.config.ts` | Create | Scope vitest discovery to this package's tests (currently relies on default cwd discovery) | -| `packages/mcp-server/vitest.config.ts` | Create | Same | -| `packages/cli/vitest.config.ts` | Create | Same — will be the default for all CLI tests | -| `packages/cli/vitest.stryker.config.ts` | Create | Stryker-only config that EXCLUDES `cli.test.ts` (which spawns `dist/cli.js` and can't see source mutations) | -| `.github/workflows/mutation.yml` | Create | PR gate: 5-job matrix, per-package incremental cache | -| `docs/mutation-testing.md` | Create | Short user guide: how to run locally, how to read the report, how to ratchet thresholds | - -`packages/sdk/vitest.config.ts` and `packages/sidecar/vitest.config.ts` already exist — no change. - ---- - -## Task 1: Install Stryker dev dependencies - -**Files:** -- Modify: `package.json` (root) - -- [ ] **Step 1: Add the two Stryker packages to root devDependencies** - -Run: -```bash -pnpm add -Dw @stryker-mutator/core@^8.7.1 @stryker-mutator/vitest-runner@^8.7.1 -``` - -The `-D` flag adds them as devDependencies, `-w` targets the workspace root. - -- [ ] **Step 2: Verify the install succeeded** - -Run: -```bash -pnpm exec stryker --version -``` - -Expected: prints a version number like `8.7.1` (no error). - -- [ ] **Step 3: Verify pnpm-lock.yaml updated** - -Run: -```bash -git diff --stat pnpm-lock.yaml package.json -``` - -Expected: both files appear with non-zero line changes. - -- [ ] **Step 4: Commit** - -```bash -git add package.json pnpm-lock.yaml -git commit -m "chore: add stryker mutation testing dev deps" -``` - ---- - -## Task 2: Create vitest configs for the three packages that lack one - -**Files:** -- Create: `packages/adapter-claude/vitest.config.ts` -- Create: `packages/mcp-server/vitest.config.ts` -- Create: `packages/cli/vitest.config.ts` - -**Why:** Stryker's vitest runner needs to know which tests to run for each mutation. With per-package configs, we can pass `--vitest.configFile packages//vitest.config.ts` to scope discovery. `sdk` and `sidecar` already have one — copy their style. - -- [ ] **Step 1: Create `packages/adapter-claude/vitest.config.ts`** - -```typescript -import { defineConfig } from 'vitest/config' - -export default defineConfig({ - test: { - globals: false, - environment: 'node', - include: ['src/**/*.test.ts'], - }, -}) -``` - -- [ ] **Step 2: Create `packages/mcp-server/vitest.config.ts`** - -```typescript -import { defineConfig } from 'vitest/config' - -export default defineConfig({ - test: { - globals: false, - environment: 'node', - include: ['src/**/*.test.ts'], - // transport.test.ts spawns tsx subprocesses; allow extra time per test - testTimeout: 15_000, - }, -}) -``` - -- [ ] **Step 3: Create `packages/cli/vitest.config.ts`** - -```typescript -import { defineConfig } from 'vitest/config' - -export default defineConfig({ - test: { - globals: false, - environment: 'node', - include: ['src/**/*.test.ts'], - // cli.test.ts spawns the built CLI binary; needs extra time - testTimeout: 15_000, - }, -}) -``` - -- [ ] **Step 4: Run each package's test suite to confirm the new configs don't break anything** - -Run: -```bash -pnpm --filter @agentspec/adapter-claude test 2>&1 | tail -5 -pnpm --filter @agentspec/mcp test 2>&1 | tail -5 -pnpm --filter @agentspec/cli test 2>&1 | tail -5 -``` - -Expected: each prints a passing summary line (no `Failed` or `Error`). If the CLI test count differs from the previous baseline, stop and investigate. - -- [ ] **Step 5: Commit** - -```bash -git add packages/adapter-claude/vitest.config.ts packages/mcp-server/vitest.config.ts packages/cli/vitest.config.ts -git commit -m "chore: add explicit vitest configs to adapter-claude, mcp-server, cli" -``` - ---- - -## Task 3: Create the Stryker-only vitest config for cli (excludes cli.test.ts) - -**Files:** -- Create: `packages/cli/vitest.stryker.config.ts` - -**Why:** `cli.test.ts` spawns `node dist/cli.js` via `child_process.spawn`. Stryker mutates source files in a sandbox copy, but the spawned binary loads from `dist/`, which is the unmutated build. Per-test coverage analysis would mark every CLI source mutant as "no coverage" and treat them as survived. Solution: a Stryker-specific vitest config that excludes this file. - -- [ ] **Step 1: Create the file** - -```typescript -import { defineConfig, mergeConfig } from 'vitest/config' -import baseConfig from './vitest.config' - -export default mergeConfig( - baseConfig, - defineConfig({ - test: { - // cli.test.ts spawns node dist/cli.js — those tests can't see Stryker - // mutations applied to src/, so they would mark every cli source mutant - // as "no coverage" and treat them as survived. Skip them under mutation. - exclude: ['**/node_modules/**', '**/dist/**', 'src/__tests__/cli.test.ts'], - }, - }), -) -``` - -- [ ] **Step 2: Verify the config loads without TypeScript errors** - -Run: -```bash -cd packages/cli && npx vitest --config vitest.stryker.config.ts run --reporter=verbose 2>&1 | tail -10 && cd ../.. -``` - -Expected: vitest runs the cli suite WITHOUT `cli.test.ts`. The summary should show fewer test files than the regular `pnpm --filter @agentspec/cli test` run (one less file: `cli.test.ts`). - -- [ ] **Step 3: Commit** - -```bash -git add packages/cli/vitest.stryker.config.ts -git commit -m "chore(cli): add stryker-only vitest config that excludes cli.test.ts" -``` - ---- - -## Task 4: Create the root Stryker config - -**Files:** -- Create: `stryker.config.json` (root) - -- [ ] **Step 1: Write the file** - -```json -{ - "$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json", - "_comment": "Per-package thresholds and mutate globs are passed at invocation time via CLI flags in package.json scripts. adapter-claude is gated at 70 (override). All other packages inherit break=60 from this file. See docs/mutation-testing.md.", - "packageManager": "pnpm", - "testRunner": "vitest", - "reporters": ["html", "progress", "clear-text"], - "coverageAnalysis": "perTest", - "incremental": true, - "concurrency": 4, - "timeoutMS": 60000, - "thresholds": { - "high": 75, - "low": 60, - "break": 60 - }, - "mutate": [ - "packages/*/src/**/*.ts", - "!packages/*/src/**/*.test.ts", - "!packages/*/src/**/__tests__/**", - "!packages/*/src/**/*.d.ts", - "!packages/sdk/src/scripts/export-schema.ts" - ], - "tempDirName": ".stryker-tmp", - "htmlReporter": { "fileName": "reports/mutation/index.html" } -} -``` - -The `_comment` field is non-standard but Stryker ignores unknown fields. Use it to document the threshold override since JSON has no comment syntax. - -- [ ] **Step 2: Validate the JSON parses** - -Run: -```bash -node -e "console.log(JSON.parse(require('fs').readFileSync('stryker.config.json','utf8')).testRunner)" -``` - -Expected: prints `vitest`. - -- [ ] **Step 3: Commit** - -```bash -git add stryker.config.json -git commit -m "chore: add root stryker config with shared defaults" -``` - ---- - -## Task 5: Add mutation scripts to root package.json - -**Files:** -- Modify: `package.json` (root) - -- [ ] **Step 1: Read the current scripts block** - -Run: -```bash -cat package.json -``` - -Expected output includes the existing scripts: `build`, `test`, `lint`, `clean`, `typecheck`, `schema:export`. - -- [ ] **Step 2: Add 6 new mutation scripts** - -Edit `package.json`. Inside the `"scripts"` object, after `"schema:export"`, add: - -```json - "mutation:adapter-claude": "stryker run --mutate 'packages/adapter-claude/src/**/*.ts' --thresholds.break 70 --incrementalFile .stryker-tmp/adapter-claude-incremental.json --htmlReporter.fileName reports/mutation/adapter-claude.html --vitest.configFile packages/adapter-claude/vitest.config.ts", - "mutation:sdk": "stryker run --mutate 'packages/sdk/src/**/*.ts' --incrementalFile .stryker-tmp/sdk-incremental.json --htmlReporter.fileName reports/mutation/sdk.html --vitest.configFile packages/sdk/vitest.config.ts", - "mutation:mcp-server": "stryker run --mutate 'packages/mcp-server/src/**/*.ts' --incrementalFile .stryker-tmp/mcp-server-incremental.json --htmlReporter.fileName reports/mutation/mcp-server.html --vitest.configFile packages/mcp-server/vitest.config.ts", - "mutation:cli": "stryker run --mutate 'packages/cli/src/**/*.ts' --incrementalFile .stryker-tmp/cli-incremental.json --htmlReporter.fileName reports/mutation/cli.html --vitest.configFile packages/cli/vitest.stryker.config.ts", - "mutation:sidecar": "stryker run --mutate 'packages/sidecar/src/**/*.ts' --incrementalFile .stryker-tmp/sidecar-incremental.json --htmlReporter.fileName reports/mutation/sidecar.html --vitest.configFile packages/sidecar/vitest.config.ts", - "mutation": "pnpm mutation:adapter-claude && pnpm mutation:sdk && pnpm mutation:mcp-server && pnpm mutation:cli && pnpm mutation:sidecar" -``` - -Note the cli script uses `vitest.stryker.config.ts` (the carve-out from Task 3); all others use `vitest.config.ts`. Adapter-claude is the only one with `--thresholds.break 70`. - -- [ ] **Step 3: Validate the JSON still parses** - -Run: -```bash -node -e "console.log(Object.keys(require('./package.json').scripts).filter(k => k.startsWith('mutation')))" -``` - -Expected: prints `[ 'mutation:adapter-claude', 'mutation:sdk', 'mutation:mcp-server', 'mutation:cli', 'mutation:sidecar', 'mutation' ]`. - -- [ ] **Step 4: Commit** - -```bash -git add package.json -git commit -m "chore: add per-package mutation scripts to root package.json" -``` - ---- - -## Task 6: Update .gitignore - -**Files:** -- Modify: `.gitignore` (root) - -- [ ] **Step 1: Add Stryker temp dir and report dir to .gitignore** - -Edit `.gitignore`. After the `# ── Worktrees ──` block (added in commit `d941b66`), add: - -``` -# ── Stryker (mutation testing) ──────────────────────────────────────────────── -.stryker-tmp/ -reports/mutation/ -``` - -- [ ] **Step 2: Verify the entries are recognized** - -Run: -```bash -mkdir -p .stryker-tmp reports/mutation && touch .stryker-tmp/test reports/mutation/test -git status --short -rm -rf .stryker-tmp reports -``` - -Expected: `git status --short` does NOT show `.stryker-tmp/` or `reports/mutation/` as untracked. - -- [ ] **Step 3: Commit** - -```bash -git add .gitignore -git commit -m "chore: ignore .stryker-tmp and reports/mutation directories" -``` - ---- - -## Task 7: Pilot run — adapter-claude - -**Files:** none modified, this is a verification step. - -**Why:** adapter-claude is the smallest, fastest package and has the highest threshold (70). If this doesn't clear, every other package's threshold needs to be re-examined. - -- [ ] **Step 1: Build dependencies (Stryker needs the workspace built so cross-package imports resolve)** - -Run: -```bash -pnpm -r build 2>&1 | tail -5 -``` - -Expected: every package prints `Build success` or `Done`. No errors. - -- [ ] **Step 2: Run the adapter-claude mutation** - -Run: -```bash -pnpm mutation:adapter-claude 2>&1 | tee /tmp/stryker-adapter-claude.log -``` - -Expected: Stryker runs ~150 mutants over the existing 54 tests. Final lines should print a mutation score percentage and `Done in `. Stryker exits 0 if score ≥ 70, exits non-zero if below. - -- [ ] **Step 3: Capture the actual score** - -Run: -```bash -grep -E "Mutation score" /tmp/stryker-adapter-claude.log -``` - -Expected: a line like `Mutation score: 78.32 %` or similar. - -- [ ] **Step 4: Decision gate** - -If the actual score is ≥ 70, proceed to Task 8. - -If below 70: -1. Open the HTML report at `reports/mutation/adapter-claude.html` to see which mutants survived. -2. STOP and report back to the user with: actual score, top 3 surviving mutants (file:line, mutator type, what code is now too weak), and a recommendation: either (a) drop the threshold to `actual − 5` and document the lower bar, or (b) write 2-3 targeted tests to kill the survivors and re-run. -3. Do NOT auto-modify the threshold without explicit approval. - -- [ ] **Step 5: No commit** — this task produces a log file, not source changes. - ---- - -## Task 8: Pilot run — sdk - -- [ ] **Step 1: Run the sdk mutation** - -Run: -```bash -pnpm mutation:sdk 2>&1 | tee /tmp/stryker-sdk.log -``` - -Expected: Stryker runs ~600 mutants. May take 20-30 minutes. - -- [ ] **Step 2: Capture the score** - -Run: -```bash -grep -E "Mutation score" /tmp/stryker-sdk.log -``` - -- [ ] **Step 3: Decision gate** - -If ≥ 60, proceed to Task 9. - -If below 60: report back as in Task 7 step 4. Same options apply (drop threshold or strengthen tests). - ---- - -## Task 9: Pilot run — mcp-server - -- [ ] **Step 1: Run the mcp-server mutation** - -Run: -```bash -pnpm mutation:mcp-server 2>&1 | tee /tmp/stryker-mcp-server.log -``` - -Expected: Stryker runs ~300 mutants. Transport tests will inflate per-mutant runtime since they spawn tsx subprocesses. Estimate 25-40 minutes. - -- [ ] **Step 2: Capture the score** - -Run: -```bash -grep -E "Mutation score" /tmp/stryker-mcp-server.log -``` - -- [ ] **Step 3: Decision gate** - -If ≥ 60, proceed to Task 10. - -If below 60: report back. Note that many mcp-server tools are 5-line `spawnCli` wrappers — these are likely sources of equivalent mutants (mutations Stryker can't kill because semantically identical). The HTML report should make these visible and they're an acceptable reason to drop the threshold to `actual − 5`. - ---- - -## Task 10: Pilot run — cli - -- [ ] **Step 1: Run the cli mutation** - -Run: -```bash -pnpm mutation:cli 2>&1 | tee /tmp/stryker-cli.log -``` - -Expected: Stryker runs ~800 mutants. Largest package, slowest run. 60-90 minutes worst case. - -- [ ] **Step 2: Capture the score** - -Run: -```bash -grep -E "Mutation score" /tmp/stryker-cli.log -``` - -- [ ] **Step 3: Decision gate** - -If ≥ 60, proceed to Task 11. Otherwise report back per Task 7 step 4. - ---- - -## Task 11: Pilot run — sidecar - -- [ ] **Step 1: Run the sidecar mutation** - -Run: -```bash -pnpm mutation:sidecar 2>&1 | tee /tmp/stryker-sidecar.log -``` - -Expected: ~700 mutants, 30-60 minutes. - -- [ ] **Step 2: Capture the score** - -Run: -```bash -grep -E "Mutation score" /tmp/stryker-sidecar.log -``` - -- [ ] **Step 3: Decision gate** - -If ≥ 60, proceed to Task 12. Otherwise report back per Task 7 step 4. - ---- - -## Task 12: Record the pilot scores in the spec doc - -**Files:** -- Modify: `docs/superpowers/specs/2026-04-12-mutation-testing-design.md` - -**Why:** The spec mentions "the first CI run determines reality" — now we have local reality. Lock the actual numbers in so reviewers know what to expect from CI. - -- [ ] **Step 1: Find the per-package thresholds table in the spec** - -Search the file for `### Per-package thresholds`. The table currently has only `break` values. - -- [ ] **Step 2: Add a "Pilot score" column with the actual numbers from Tasks 7-11** - -Replace the existing table with: - -```markdown -| Package | `break` (gate) | Pilot score (local) | Rationale | -|---|---|---|---| -| adapter-claude | **70** | % | Pure functions, no I/O, dense test coverage from PR #46. | -| sdk | 60 | % | Larger surface, some I/O paths (loaders, resolvers). | -| mcp-server | 60 | % | Many tools are thin spawnCli wrappers (low mutation value); transport tests do exercise real code paths. | -| cli | 60 | % | Largest package, lots of command wiring. cli.test.ts excluded. | -| sidecar | 60 | % | HTTP server with mocked dependencies. | -``` - -Replace `` with each package's score from `/tmp/stryker-.log`. - -- [ ] **Step 3: Commit** - -```bash -git add docs/superpowers/specs/2026-04-12-mutation-testing-design.md -git commit -m "docs: record pilot mutation scores in design spec" -``` - ---- - -## Task 13: Create the GitHub Actions workflow - -**Files:** -- Create: `.github/workflows/mutation.yml` - -- [ ] **Step 1: Verify the .github/workflows directory exists** - -Run: -```bash -ls .github/workflows/ 2>&1 | head -``` - -Expected: a directory listing of existing workflow files (it exists). If it doesn't, create it: `mkdir -p .github/workflows`. - -- [ ] **Step 2: Write the workflow** - -```yaml -name: mutation -on: - pull_request: - types: [opened, synchronize, reopened] - -jobs: - mutation: - name: mutation / ${{ matrix.package }} - runs-on: ubuntu-latest - timeout-minutes: 90 - strategy: - fail-fast: false - matrix: - package: [adapter-claude, sdk, mcp-server, cli, sidecar] - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Build all packages - run: pnpm -r build - - - name: Restore Stryker incremental cache - uses: actions/cache@v4 - with: - path: .stryker-tmp/${{ matrix.package }}-incremental.json - key: stryker-${{ matrix.package }}-${{ hashFiles(format('packages/{0}/src/**', matrix.package), 'stryker.config.json') }} - restore-keys: | - stryker-${{ matrix.package }}- - - - name: Run Stryker - run: pnpm mutation:${{ matrix.package }} - - - name: Upload HTML report - if: always() - uses: actions/upload-artifact@v4 - with: - name: mutation-report-${{ matrix.package }} - path: reports/mutation/${{ matrix.package }}.html - retention-days: 14 -``` - -- [ ] **Step 3: Lint the YAML by parsing it** - -Run: -```bash -node -e "const fs=require('fs'); const yaml=require('js-yaml'); console.log(Object.keys(yaml.load(fs.readFileSync('.github/workflows/mutation.yml','utf8')).jobs))" -``` - -Expected: prints `[ 'mutation' ]`. If `js-yaml` isn't available, skip this and proceed; CI will catch syntax errors. - -- [ ] **Step 4: Commit** - -```bash -git add .github/workflows/mutation.yml -git commit -m "ci: add mutation testing workflow with per-package matrix" -``` - ---- - -## Task 14: Write the user-facing docs - -**Files:** -- Create: `docs/mutation-testing.md` - -- [ ] **Step 1: Write the file** - -```markdown -# Mutation Testing - -AgentSpec uses [Stryker Mutator](https://stryker-mutator.io) to test the strength of its test suite. Stryker mutates production code in small ways (negate a condition, drop a statement, change `>` to `>=`) and re-runs the suite. If the suite still passes, the mutant "survived" and the test is too weak. - -## Running locally - -```bash -# Run for a single package -pnpm mutation:adapter-claude -pnpm mutation:sdk -pnpm mutation:mcp-server -pnpm mutation:cli -pnpm mutation:sidecar - -# Run all packages sequentially -pnpm mutation -``` - -Each run produces an HTML report at `reports/mutation/.html`. Open it to see which mutants survived and where. - -The first run for a package is slow (full mutation pass). Subsequent runs use Stryker's incremental cache (`.stryker-tmp/-incremental.json`) and only re-mutate files that changed. - -## Per-package thresholds - -| Package | `break` threshold | Set in | -|---|---|---| -| adapter-claude | 70 | `package.json` `mutation:adapter-claude` script (`--thresholds.break 70` flag) | -| sdk | 60 | `stryker.config.json` (default) | -| mcp-server | 60 | `stryker.config.json` (default) | -| cli | 60 | `stryker.config.json` (default) | -| sidecar | 60 | `stryker.config.json` (default) | - -If your local run shows a score below the threshold, the command exits non-zero. CI uses the same thresholds and will fail the corresponding `mutation / ` check on the PR. - -## CI - -`.github/workflows/mutation.yml` runs all 5 packages in parallel on every PR (open / synchronize / reopen). Each job: - -1. Restores its package's Stryker incremental cache -2. Runs `pnpm mutation:` -3. Uploads the HTML report as a workflow artifact (`mutation-report-`, 14-day retention) -4. Exits non-zero if the mutation score is below the package's `break` threshold - -A PR cannot merge until all 5 mutation checks pass. - -## When CI is red - -1. Open the PR's failing mutation check. -2. Click the "Summary" tab and download the `mutation-report-` artifact. -3. Open `.html` locally. Surviving mutants are highlighted in red. -4. For each survivor, decide: - - **Test gap**: write a test that would fail if the mutant were the real implementation. Push. - - **Equivalent mutant**: the mutation produces semantically identical behavior (e.g., changing `i++` to `++i` in a loop where the prefix/postfix difference is invisible). These can be ignored — Stryker has no way to detect them automatically. If equivalents push the score below the threshold, the threshold should be lowered, not the test suite padded. - -## Ratcheting the threshold - -When the mutation score for a package improves consistently above its threshold, raise the threshold to lock in the gain: - -1. Edit `package.json` and bump `--thresholds.break N` for the package's script (or move the package out of the default by adding the flag). -2. Open a PR. CI will validate the new threshold against the actual score. - -The goal is to ratchet up over time, never down. - -## What's excluded from mutation - -- `__tests__/` directories — tests don't get mutated -- `*.d.ts` files — type definitions only -- `dist/` builds -- `packages/sdk/src/scripts/export-schema.ts` — generated artefact bootstrap -- `packages/cli/src/__tests__/cli.test.ts` is excluded from the **test** set (not the mutate set) because it spawns the pre-built `dist/cli.js` binary and can't see source mutations. Other cli tests still run. -``` - -- [ ] **Step 2: Commit** - -```bash -git add docs/mutation-testing.md -git commit -m "docs: add mutation testing user guide" -``` - ---- - -## Task 15: Verify the gate works (red-test simulation) - -**Files:** none modified permanently — this is an end-to-end gate test that gets reverted. - -**Why:** Confirm that weakening a test actually causes the mutation score to drop and the script to exit non-zero. If this doesn't work, the gate is decorative. - -- [ ] **Step 1: Identify a strong test in adapter-claude** - -Open `packages/adapter-claude/src/__tests__/claude-adapter.test.ts`. Find the test added in PR #46: - -```typescript -it('encodes in file content to prevent tag breakout', () => { -``` - -This test asserts the security guard for tag breakout. If we delete the assertions, multiple `sanitizeContextContent` mutants should now survive. - -- [ ] **Step 2: Comment out the assertions in that test** - -Edit the file. Inside the `it('encodes ...')` block, comment out the two `expect(...)` lines: - -```typescript - try { - const ctx = buildContext({ manifest: baseManifest, contextFiles: [toolFile] }) - // expect(ctx).not.toMatch(/<\/context_file>\nignore/) - // expect(ctx).toContain('ignore all previous instructions') - } finally { -``` - -- [ ] **Step 3: Re-run adapter-claude mutation** - -Run: -```bash -pnpm mutation:adapter-claude 2>&1 | tee /tmp/stryker-gate-test.log -``` - -- [ ] **Step 4: Verify the score dropped and Stryker exited non-zero** - -Run: -```bash -echo "exit code: $?" -grep -E "Mutation score" /tmp/stryker-gate-test.log -``` - -Expected: exit code is non-zero (Stryker exits with code 1 when below `break`). The mutation score is lower than the Task 7 baseline. - -If the score did NOT drop or exit was 0: STOP. The gate is not working. Investigate before proceeding (likely cause: the deleted assertion wasn't load-bearing for any specific mutant — pick a different test to weaken). - -- [ ] **Step 5: Revert the test file** - -Run: -```bash -git checkout packages/adapter-claude/src/__tests__/claude-adapter.test.ts -``` - -- [ ] **Step 6: Confirm the revert worked** - -Run: -```bash -git status --short -``` - -Expected: no changes to the test file (it's back to clean state). - -- [ ] **Step 7: No commit** — this task verifies behavior, doesn't change source. - ---- - -## Task 16: Push and open the PR - -**Files:** none. - -- [ ] **Step 1: Verify the branch is clean and ahead of main** - -Run: -```bash -git status -git log --oneline main..HEAD -``` - -Expected: working tree clean. Log shows commits from Tasks 1, 2, 3, 4, 5, 6, 12, 13, 14 (9 commits) plus the original `d941b66` (gitignore .worktrees) and `9e3e1d7` (spec doc) = 11 commits total. - -- [ ] **Step 2: Push the branch** - -Run: -```bash -git push -u origin chore/mutation-testing -``` - -Expected: pushes successfully, prints a "Create a pull request" URL. - -- [ ] **Step 3: Open the PR with `gh pr create`** - -Run: -```bash -gh pr create --base main --head chore/mutation-testing --title "chore: add mutation testing with Stryker (per-package CI gates)" --body "$(cat <<'EOF' -## Summary - -Adds [Stryker Mutator](https://stryker-mutator.io) mutation testing across all 5 packages with a per-package CI gate. - -- Single root \`stryker.config.json\` with shared defaults. -- 6 mutation scripts in root \`package.json\`. adapter-claude is gated at 70 via a CLI override; the rest inherit \`break: 60\`. -- New GitHub Actions workflow runs all 5 packages in parallel on every PR (open / synchronize / reopen) with per-package incremental caching, so a PR that doesn't touch a package finishes its mutation check in ~15 s. -- New \`packages/cli/vitest.stryker.config.ts\` excludes \`cli.test.ts\` from the Stryker test set (it spawns \`dist/cli.js\` and can't see source mutations). -- New \`vitest.config.ts\` files for adapter-claude, mcp-server, and cli (sdk and sidecar already had one). -- New user guide at \`docs/mutation-testing.md\`. -- Design spec at \`docs/superpowers/specs/2026-04-12-mutation-testing-design.md\`. - -## Pilot scores (local run) - -| Package | Threshold | Pilot score | -|---|---|---| -| adapter-claude | 70 | | -| sdk | 60 | | -| mcp-server | 60 | | -| cli | 60 | | -| sidecar | 60 | | - -## Test plan - -- [x] Local pilot per package — scores recorded above -- [x] Local gate verification — weakened a security test, confirmed Stryker exits non-zero -- [ ] CI first run on this PR — confirm all 5 jobs produce a score and a check status -- [ ] CI cache validation — push an empty commit, confirm every job hits cache and finishes ≤ 30 s -- [ ] CI partial-change validation — touch one source file, confirm only that package re-mutates the changed file - -## Notes - -- First CI run will be slow (cold caches) — up to 90 min for the slowest package on a fresh runner. -- Subsequent runs use the per-package Stryker incremental cache + GitHub actions/cache. -- Reports are uploaded as workflow artifacts (\`mutation-report-\`, 14-day retention). -EOF -)" -``` - -Replace the `` placeholders with the actual scores from Task 12 BEFORE running. - -- [ ] **Step 4: Capture the PR URL** - -Expected: `gh pr create` prints the PR URL on stdout. Save it for the user. - ---- - -## Verification (end-to-end) - -After Task 16, the PR is open. CI should kick off automatically. To verify the full design works: - -1. **First run**: all 5 jobs should produce a score (cold cache, up to 90 min for the slowest). Confirm 5 check statuses appear on the PR. -2. **Cache validation**: push an empty commit (`git commit --allow-empty -m "test: cache validation" && git push`). Every job should hit cache and finish in ≤ 30 s. -3. **Partial change validation**: push a 1-line whitespace change to `packages/sdk/src/loader/resolvers.ts`. The `sdk` job should re-mutate only that file (~2-5 min); the other 4 jobs should hit cache (~15 s each). -4. **Gate validation**: temporarily weaken a test in adapter-claude (same change as Task 15 step 2), push, confirm the `mutation / adapter-claude` check goes red. Revert and push again to restore green. - -If any of these fail, comment on the PR with the failure mode and we'll iterate. - ---- - -## Self-Review Notes - -**Spec coverage check** — every section of the design spec has at least one task: -- Architecture / single root config → Task 4 -- Per-package npm scripts → Task 5 -- Per-package thresholds → Task 5 (CLI flag) + Task 14 (docs) -- Test scope per package (cli carve-out) → Tasks 2, 3 -- CI workflow → Task 13 -- Cache layers → Task 13 -- Files in this PR → Tasks 1-6, 13, 14 -- Verification plan → Tasks 7-11 (local pilot), Task 15 (gate), final verification section (CI) -- Trade-offs and limitations → Task 14 (docs) - -**Placeholder scan**: `` and `` appear in Tasks 12 and 16. These are deliberate fill-ins from the pilot runs, not unresolved TODOs. Every other step has concrete code or commands. - -**Type / name consistency**: script names are `mutation:` consistently across Tasks 5, 13, 14, 16. Cache file names are `-incremental.json` consistently. Config file is `stryker.config.json` everywhere (Task 4) and referenced the same way in cache key (Task 13). diff --git a/docs/decisions/2026-04-12-mutation-testing.md b/docs/decisions/2026-04-12-mutation-testing.md deleted file mode 100644 index e9f208f..0000000 --- a/docs/decisions/2026-04-12-mutation-testing.md +++ /dev/null @@ -1,202 +0,0 @@ -# Mutation Testing for AgentSpec — Design Spec - -**Status**: approved, ready for implementation -**Author**: brainstorm session 2026-04-12 -**Tracking**: new PR from `main` (no upstream issue) - -## Why - -Issue #24 surfaced that test counts don't measure test strength. PR #46 added 30 new tests but two of the most security-relevant ones (`extractFileRefs` path-traversal guard, `sanitizeContextContent` tag-breakout escape) assert with `expect(ctx).not.toContain('context_file')` — a no-op that stays green if the guard ever silently breaks. - -Mutation testing exposes exactly that failure mode: it modifies production code in small ways (negate a condition, drop a statement, change `>` to `>=`) and re-runs the suite. If the suite still passes, the mutant "survives" and the test is too weak. The mutation score is the percentage of mutants killed. - -We want this on every PR. We want it cached so cost-per-PR is acceptable. We want it to gate merge when the score drops below a threshold so the signal can't be ignored. - -## Goals - -- Catch weak assertions across the workspace, especially in security-adjacent code (`adapter-claude` `extractFileRefs`, `sanitizeContextContent`, `extractGeneratedAgent`). -- Establish a per-package mutation score baseline visible on every PR via GitHub check statuses. -- Block merge when a package's mutation score falls below its threshold. -- Keep cache hits cheap enough that PRs touching only docs or unrelated packages don't pay a meaningful runtime cost. - -## Non-goals - -- Mutation testing of the docs site, schema-export script, or generated `dist/` artefacts. -- Per-PR comments with score history or charts. -- Automatic threshold ratcheting. -- Cron baseline run on `main` (PR-time gate is sufficient at this stage). -- Mutation testing of integration tests that spawn pre-built binaries (`cli.test.ts` spawns `dist/cli.js`, which doesn't see source mutations — these tests are excluded from the Stryker test set). - -## Architecture - -### Single root config - -One `stryker.config.json` at the repo root holds everything that's identical across packages: - -```jsonc -{ - "$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json", - "packageManager": "pnpm", - "testRunner": "vitest", - "reporters": ["html", "progress", "clear-text"], - "coverageAnalysis": "perTest", - "incremental": true, - "thresholds": { - "high": 75, - "low": 60, - "break": 60 - }, - "mutate": [ - "packages/*/src/**/*.ts", - "!packages/*/src/**/*.test.ts", - "!packages/*/src/**/__tests__/**", - "!packages/*/src/**/*.d.ts", - "!packages/*/dist/**", - "!packages/sdk/src/scripts/export-schema.ts" - ], - "tempDirName": ".stryker-tmp", - "htmlReporter": { "fileName": "reports/mutation/index.html" } -} -``` - -Per-package overrides happen at invocation time via CLI flags (see scripts below). This keeps the config file as the single source of truth and makes per-package divergence visible in the npm scripts that consume it. - -### Per-package npm scripts (in root `package.json`) - -```json -{ - "scripts": { - "mutation:adapter-claude": "stryker run --mutate 'packages/adapter-claude/src/**/*.ts' --thresholds.break 45 --incrementalFile .stryker-tmp/adapter-claude-incremental.json --htmlReporter.fileName reports/mutation/adapter-claude.html", - "mutation:sdk": "stryker run --mutate 'packages/sdk/src/**/*.ts' --incrementalFile .stryker-tmp/sdk-incremental.json --htmlReporter.fileName reports/mutation/sdk.html", - "mutation:mcp-server": "stryker run --mutate 'packages/mcp-server/src/**/*.ts' --incrementalFile .stryker-tmp/mcp-server-incremental.json --htmlReporter.fileName reports/mutation/mcp-server.html", - "mutation:cli": "stryker run --mutate 'packages/cli/src/**/*.ts' --incrementalFile .stryker-tmp/cli-incremental.json --htmlReporter.fileName reports/mutation/cli.html", - "mutation:sidecar": "stryker run --mutate 'packages/sidecar/src/**/*.ts' --incrementalFile .stryker-tmp/sidecar-incremental.json --htmlReporter.fileName reports/mutation/sidecar.html", - "mutation": "pnpm mutation:adapter-claude && pnpm mutation:sdk && pnpm mutation:mcp-server && pnpm mutation:cli && pnpm mutation:sidecar" - } -} -``` - -`pnpm mutation` is for local "give me the full picture" use. CI calls the per-package scripts in parallel via the matrix. - -### Per-package thresholds - -| Package | `break` (gate) | Pilot score (local) | Rationale | -|---|---|---|---| -| adapter-claude | **45** | 49.57% | Pure functions, no I/O. Score limited by 32 no-coverage mutants in index.ts. | -| sdk | 49 | 54.31% | Larger surface, some I/O paths (loaders, resolvers). | -| mcp-server | 26 | 31.31% | 413 of 658 mutants have zero test coverage; most production code in index.ts (dispatch/RPC/transport) is untested on main. | -| cli | 45 | 49.50% | Largest package, lots of command wiring. cli.test.ts excluded from Stryker test set. | -| sidecar | 46 | 51.43% | HTTP server with mocked dependencies. | - -Thresholds are starting points. The first CI run on this PR determines whether they're realistic. If a package fails its own threshold on first run, the threshold drops to `(actual - 5)` for that package and the PR description notes the discrepancy for reviewers. - -### Test scope per package - -Most packages: all tests run under Stryker. - -Two carve-outs: - -- **`cli`**: `src/__tests__/cli.test.ts` spawns the pre-built `dist/cli.js` via `child_process.spawn`. Mutations applied to source files in Stryker's sandbox don't reach `dist/`, so this test can't validate them. Stryker's per-test coverage analysis would mark every CLI source mutant as "no coverage" and treat them as survived. **Action**: exclude `cli.test.ts` from the Stryker test set via `--vitest.configFile` pointing at a Stryker-specific vitest config that omits this file. All other cli tests run. -- **`mcp-server`**: `src/__tests__/transport.test.ts` spawns `tsx src/index.ts`, which DOES see mutations live (tsx loads from the Stryker sandbox copy). Keep this test in the Stryker set, but it'll be the slowest single test (~5s × 18 tests × N mutants). The vitest config scope already covers this. - -### CI workflow - -`.github/workflows/mutation.yml`: - -```yaml -name: mutation -on: - pull_request: - types: [opened, synchronize, reopened] - -jobs: - mutation: - name: mutation / ${{ matrix.package }} - runs-on: ubuntu-latest - timeout-minutes: 90 - strategy: - fail-fast: false - matrix: - package: [adapter-claude, sdk, mcp-server, cli, sidecar] - steps: - - uses: actions/checkout@v4 - with: { fetch-depth: 1 } - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 - with: { node-version: '20', cache: 'pnpm' } - - run: pnpm install --frozen-lockfile - - run: pnpm -r build - - name: Restore Stryker incremental cache - uses: actions/cache@v4 - with: - path: .stryker-tmp/${{ matrix.package }}-incremental.json - key: stryker-${{ matrix.package }}-${{ hashFiles(format('packages/{0}/src/**', matrix.package), 'stryker.config.json') }} - restore-keys: | - stryker-${{ matrix.package }}- - - name: Run Stryker - run: pnpm mutation:${{ matrix.package }} - - name: Upload HTML report - if: always() - uses: actions/upload-artifact@v4 - with: - name: mutation-report-${{ matrix.package }} - path: reports/mutation/${{ matrix.package }}.html -``` - -**Triggers**: PR open / synchronize / reopen. Not on push to main (PRs gate it; main is implicitly green). - -**Always all 5 jobs**: never conditional on changed paths. A package whose source didn't change gets a near-instant cache hit, and the check status still appears so the PR has a complete picture. - -**Three caching layers**: -1. **pnpm store**: `actions/setup-node` with `cache: 'pnpm'`. Standard, ~30s saved per job. -2. **Build outputs** (`packages/*/dist`): inherited via the `pnpm -r build` step. Could be cached separately later if it becomes a hotspot; not in v1. -3. **Stryker incremental file** (`.stryker-tmp/-incremental.json`): the big one. Keyed per package on `hashFiles('packages//src/**', 'stryker.config.json')`. With a hit, Stryker skips re-mutating unchanged files; a "no source changes in this package" run takes ~10-15 s. With a miss (real source change), Stryker re-mutates only the changed files using the cached results for the rest. - -**Failure surfaces**: when the package's mutation score is below its `break` threshold, Stryker exits non-zero. The job fails. The check `mutation / ` appears red on the PR. The PR cannot merge until either the test gets stronger or the threshold is adjusted via a config change in a follow-up commit (which is itself a reviewable signal). - -**Cache hit math** (PR touches one file in `packages/cli/src/commands/foo.ts`): - -| Job | Cache state | Runtime | -|---|---|---| -| adapter-claude | full hit | ~15 s | -| sdk | full hit | ~15 s | -| mcp-server | full hit | ~15 s | -| cli | partial hit (only `foo.ts` re-mutates) | ~5-10 min | -| sidecar | full hit | ~15 s | - -Worst-case PR (touches every package's source) ≈ ~80 min wall-clock, runs in parallel across 5 runners. - -## Files in this PR - -- `stryker.config.json` (root) — single source of truth -- `package.json` (root) — adds the 6 mutation scripts and dev-deps -- `.github/workflows/mutation.yml` — new workflow -- `.gitignore` — adds `.stryker-tmp/`, `reports/mutation/` (`.worktrees/` already added in commit `d941b66` on this branch) -- `tsconfig.json` updates per package — exclude `.stryker-tmp` so typecheck doesn't trip on the sandbox copy -- `docs/superpowers/specs/2026-04-12-mutation-testing-design.md` — this spec -- `docs/mutation-testing.md` — short user guide: how to run locally, how to read the report, how to ratchet thresholds, what to do when CI is red - -## Verification plan - -1. **Local pilot**: `pnpm mutation:adapter-claude` produces a score and an HTML report at `reports/mutation/adapter-claude.html`. Confirm score >= 45. -2. **Local sweep**: `pnpm mutation` runs all 5 sequentially. For each package, capture the actual score and confirm it clears the proposed threshold. If any package fails, drop its threshold to `actual − 5` and note in the PR description. -3. **CI first run**: open the PR, watch the 5 jobs run in parallel from a cold cache. All 5 should produce a score and a green check. -4. **CI cache validation**: push an empty commit. Confirm every cache restores and every job finishes in ≤ 30 s. -5. **CI partial-change validation**: push a 1-line change to `packages/sdk/src/loader/resolvers.ts`. Confirm 4 jobs hit cache (≤ 30 s each) and 1 job (`sdk`) re-mutates only `resolvers.ts` while reusing cached results for the rest of the package. -6. **CI gate validation**: temporarily delete an assertion in `packages/adapter-claude/src/__tests__/claude-adapter.test.ts`, push, confirm the `adapter-claude` job goes red with a score below 45 and the PR check is blocked. Revert the change, confirm the gate restores to green. - -## Trade-offs and known limitations - -- **Threshold drift risk**: with a single config and one CLI override, it's easy to forget that adapter-claude has a stricter gate. `stryker.config.json` is JSON (no comments) so the only place the override lives is the `--thresholds.break 45` flag in `package.json`'s `mutation:adapter-claude` script. Mitigation: `docs/mutation-testing.md` documents the table of per-package thresholds, and the gate failure on CI (red check) makes drift loud. -- **Future per-package customization** (excluding a noisy file from one package only) becomes an additional CLI flag rather than a config-file edit. If divergence grows beyond 1-2 flags per package, we may want to migrate to per-package configs after all. -- **`pnpm mutation` (the all-in-one local script)** runs packages sequentially, not in parallel, because Stryker's vitest runner uses a shared `.stryker-tmp/` working directory and parallel runs would race. CI uses separate runners so each gets its own working directory. -- **First CI run is uncached** and may take up to ~80 min for the slowest package. Subsequent runs hit cache. -- **`break` threshold is a CI-only gate**, not a local hard stop. Local `pnpm mutation:` will print a red score but still produce a report and exit cleanly enough for the developer to keep iterating. - -## Out of scope (deliberately deferred to follow-ups) - -- Per-PR sticky comment with score history -- Automatic threshold ratcheting (a scheduled job that bumps thresholds when scores improve) -- Cron baseline run on `main` to detect drift independent of PRs -- Mutation testing of docs / schema-export script -- Migration from single root config to per-package configs (only if divergence justifies it) diff --git a/docs/guides/mutation-testing.md b/docs/guides/mutation-testing.md index 82b3780..9e12cf5 100644 --- a/docs/guides/mutation-testing.md +++ b/docs/guides/mutation-testing.md @@ -1,79 +1,42 @@ # Mutation Testing -AgentSpec uses [Stryker Mutator](https://stryker-mutator.io) to test the strength of its test suite. Stryker mutates production code in small ways (negate a condition, drop a statement, change `>` to `>=`) and re-runs the suite. If the suite still passes, the mutant "survived" and the test is too weak. +## Why -## Running locally - -```bash -# Run for a single package -pnpm mutation:adapter-claude -pnpm mutation:sdk -pnpm mutation:mcp-server -pnpm mutation:cli -pnpm mutation:sidecar - -# Run all packages sequentially -pnpm mutation -``` +Test coverage tells you which lines ran. Mutation testing tells you which lines your tests actually verify. Stryker modifies production code in small ways (negate a condition, drop a statement, swap an operator) and re-runs the suite. If the suite still passes, the mutant "survived" and the test is too weak. -Each run produces an HTML report at `reports/mutation/.html`. Open it to see which mutants survived and where. +This matters most for security-adjacent code like path-traversal guards and tag-breakout escapes, where a `expect(ctx).not.toContain(...)` assertion can stay green even if the guard silently breaks. -The first run for a package is slow (full mutation pass). Subsequent runs use Stryker's incremental cache (`.stryker-tmp/-incremental.json`) and only re-mutate files that changed. +## What was added -## Per-package thresholds +Three files, no production code changes: -| Package | `break` threshold | Pilot score | Set in | -|---|---|---|---| -| adapter-claude | 45 | 49.57% | `package.json` `mutation:adapter-claude` script (`STRYKER_BREAK=45`) | -| sdk | 49 | 54.31% | `package.json` `mutation:sdk` script (`STRYKER_BREAK=49`) | -| mcp-server | 26 | 31.31% | `package.json` `mutation:mcp-server` script (`STRYKER_BREAK=26`) | -| cli | 45 | 49.50% | `package.json` `mutation:cli` script (`STRYKER_BREAK=45`) | -| sidecar | 46 | 51.43% | `package.json` `mutation:sidecar` script (`STRYKER_BREAK=46`) | +- `stryker.config.mjs` -- single root config, uses `STRYKER_PKG` and `STRYKER_BREAK` env vars for per-package overrides +- `package.json` scripts (`mutation:*`) -- one per package plus a `mutation` umbrella +- `.github/workflows/mutation.yml` -- runs all 5 packages in parallel on every PR, blocks merge if score drops below the package's threshold -The default threshold (when `STRYKER_BREAK` is unset) is 40, defined in `stryker.config.mjs`. +## Running locally -If your local run shows a score below the threshold, the command exits non-zero. CI uses the same thresholds and will fail the corresponding `mutation / ` check on the PR. +```bash +pnpm mutation:adapter-claude # single package +pnpm mutation # all packages sequentially +``` -## CI +Each run produces an HTML report at `reports/mutation/.html`. First run is slow (full pass); subsequent runs use incremental cache. -`.github/workflows/mutation.yml` runs all 5 packages in parallel on every PR (open / synchronize / reopen). Each job: +## Thresholds -1. Restores its package's Stryker incremental cache -2. Runs `pnpm mutation:` -3. Uploads the HTML report as a workflow artifact (`mutation-report-`, 14-day retention) -4. Exits non-zero if the mutation score is below the package's `break` threshold +| Package | Threshold | Baseline score | +|---|---|---| +| adapter-claude | 45 | 49.57% | +| sdk | 49 | 54.31% | +| mcp-server | 26 | 31.31% | +| cli | 45 | 49.50% | +| sidecar | 46 | 51.43% | -A PR cannot merge until all 5 mutation checks pass. +Thresholds are set at baseline - 5. Ratchet up over time by editing `STRYKER_BREAK=N` in the package's script in `package.json`. ## When CI is red -1. Open the PR's failing mutation check. -2. Click the "Summary" tab and download the `mutation-report-` artifact. -3. Open `.html` locally. Surviving mutants are highlighted in red. -4. For each survivor, decide: - - **Test gap**: write a test that would fail if the mutant were the real implementation. Push. - - **Equivalent mutant**: the mutation produces semantically identical behavior (e.g., changing `i++` to `++i` in a loop where the prefix/postfix difference is invisible). These can be ignored. If equivalents push the score below the threshold, the threshold should be lowered, not the test suite padded. - -## Ratcheting the threshold - -When the mutation score for a package improves consistently above its threshold, raise the threshold to lock in the gain: - -1. Edit the `STRYKER_BREAK=N` value in the package's mutation script in `package.json`. -2. Open a PR. CI will validate the new threshold against the actual score. - -The goal is to ratchet up over time, never down. - -## What's excluded from mutation - -- `__tests__/` directories and `*.test.ts` files -- `*.d.ts` files (type definitions only) -- `dist/` builds -- `packages/sdk/src/scripts/export-schema.ts` (generated artefact bootstrap) -- `packages/cli/src/__tests__/cli.test.ts` is excluded from the **test** set (not the mutate set) because it spawns the pre-built `dist/cli.js` binary and can't see source mutations. Other cli tests still run. - -## Configuration - -All config lives in `stryker.config.mjs` at the repo root. Per-package overrides are driven by two env vars set in the npm scripts: - -- `STRYKER_PKG` selects which package to mutate and which vitest config to use -- `STRYKER_BREAK` sets the failure threshold (default: 40 from the config file) +1. Download the `mutation-report-` artifact from the failing check. +2. Open the HTML report. Surviving mutants are highlighted in red. +3. Either write a stronger test or lower the threshold if the survivor is an equivalent mutant. From db3ea4266589b302913def3d4a87ba453facb1bf Mon Sep 17 00:00:00 2001 From: iliassjabali Date: Sun, 12 Apr 2026 07:18:12 +0100 Subject: [PATCH 19/26] refactor: replace 6 mutation scripts with single wrapper + threshold map Thresholds now live in the THRESHOLDS map in stryker.config.mjs. scripts/mutation.mjs is a thin wrapper that takes a package name from argv, sets STRYKER_PKG, and execs stryker run. Usage: pnpm mutation adapter-claude (or pnpm mutation for all). --- .github/workflows/mutation.yml | 2 +- docs/guides/mutation-testing.md | 8 ++++---- package.json | 7 +------ scripts/mutation.mjs | 14 ++++++++++++++ stryker.config.mjs | 19 +++++++++++++++---- 5 files changed, 35 insertions(+), 15 deletions(-) create mode 100644 scripts/mutation.mjs diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml index e621e14..40ce75d 100644 --- a/.github/workflows/mutation.yml +++ b/.github/workflows/mutation.yml @@ -44,7 +44,7 @@ jobs: stryker-${{ matrix.package }}- - name: Run Stryker - run: pnpm mutation:${{ matrix.package }} + run: pnpm mutation ${{ matrix.package }} - name: Upload HTML report if: always() diff --git a/docs/guides/mutation-testing.md b/docs/guides/mutation-testing.md index 9e12cf5..86f0725 100644 --- a/docs/guides/mutation-testing.md +++ b/docs/guides/mutation-testing.md @@ -10,14 +10,14 @@ This matters most for security-adjacent code like path-traversal guards and tag- Three files, no production code changes: -- `stryker.config.mjs` -- single root config, uses `STRYKER_PKG` and `STRYKER_BREAK` env vars for per-package overrides -- `package.json` scripts (`mutation:*`) -- one per package plus a `mutation` umbrella +- `stryker.config.mjs` -- single root config with per-package threshold map +- `scripts/mutation.mjs` -- thin wrapper that sets `STRYKER_PKG` and execs Stryker - `.github/workflows/mutation.yml` -- runs all 5 packages in parallel on every PR, blocks merge if score drops below the package's threshold ## Running locally ```bash -pnpm mutation:adapter-claude # single package +pnpm mutation adapter-claude # single package pnpm mutation # all packages sequentially ``` @@ -33,7 +33,7 @@ Each run produces an HTML report at `reports/mutation/.html`. First run | cli | 45 | 49.50% | | sidecar | 46 | 51.43% | -Thresholds are set at baseline - 5. Ratchet up over time by editing `STRYKER_BREAK=N` in the package's script in `package.json`. +Thresholds are set at baseline - 5. Ratchet up over time by editing the `THRESHOLDS` map in `stryker.config.mjs`. ## When CI is red diff --git a/package.json b/package.json index 0e99771..0f691fe 100644 --- a/package.json +++ b/package.json @@ -14,12 +14,7 @@ "clean": "pnpm -r clean", "typecheck": "pnpm -r typecheck", "schema:export": "tsx packages/sdk/src/scripts/export-schema.ts", - "mutation:adapter-claude": "STRYKER_PKG=adapter-claude STRYKER_BREAK=45 stryker run", - "mutation:sdk": "STRYKER_PKG=sdk STRYKER_BREAK=49 stryker run", - "mutation:mcp-server": "STRYKER_PKG=mcp-server STRYKER_BREAK=26 stryker run", - "mutation:cli": "STRYKER_PKG=cli STRYKER_BREAK=45 stryker run", - "mutation:sidecar": "STRYKER_PKG=sidecar STRYKER_BREAK=46 stryker run", - "mutation": "pnpm mutation:adapter-claude && pnpm mutation:sdk && pnpm mutation:mcp-server && pnpm mutation:cli && pnpm mutation:sidecar" + "mutation": "node scripts/mutation.mjs" }, "devDependencies": { "@stryker-mutator/core": "^8.7.1", diff --git a/scripts/mutation.mjs b/scripts/mutation.mjs new file mode 100644 index 0000000..aa09cf1 --- /dev/null +++ b/scripts/mutation.mjs @@ -0,0 +1,14 @@ +#!/usr/bin/env node +import { execSync } from 'node:child_process' +import { PACKAGES } from '../stryker.config.mjs' + +const targets = process.argv[2] ? [process.argv[2]] : PACKAGES + +for (const pkg of targets) { + if (!PACKAGES.includes(pkg)) { + console.error(`Unknown package: ${pkg}\nAvailable: ${PACKAGES.join(', ')}`) + process.exit(1) + } + console.log(`\n── mutation: ${pkg} ──\n`) + execSync('stryker run', { stdio: 'inherit', env: { ...process.env, STRYKER_PKG: pkg } }) +} diff --git a/stryker.config.mjs b/stryker.config.mjs index 348e290..3ddce6c 100644 --- a/stryker.config.mjs +++ b/stryker.config.mjs @@ -1,11 +1,20 @@ // Mutation testing config — single source of truth for all packages. // -// Per-package behavior is driven by two env vars set in the npm scripts: -// STRYKER_PKG — package directory name (e.g. "adapter-claude", "sdk") -// STRYKER_BREAK — mutation score threshold that fails CI (default: 60) +// Per-package behavior is driven by the STRYKER_PKG env var, set by +// scripts/mutation.mjs. Thresholds live here, not in package.json. + +const PACKAGES = ['adapter-claude', 'sdk', 'mcp-server', 'cli', 'sidecar'] + +const THRESHOLDS = { + 'adapter-claude': 45, + 'sdk': 49, + 'mcp-server': 26, + 'cli': 45, + 'sidecar': 46, +} const pkg = process.env.STRYKER_PKG ?? '' -const breakThreshold = parseInt(process.env.STRYKER_BREAK ?? '60', 10) +const breakThreshold = THRESHOLDS[pkg] ?? 40 const vitestConfigFile = pkg === 'cli' ? 'packages/cli/vitest.stryker.config.ts' @@ -38,3 +47,5 @@ export default { }, ...(vitestConfigFile ? { vitest: { configFile: vitestConfigFile, dir: `packages/${pkg}` } } : {}), } + +export { PACKAGES, THRESHOLDS } From d90a3545f6d78dac2d74a5e94e51aa3679c41a80 Mon Sep 17 00:00:00 2001 From: iliassjabali Date: Sun, 12 Apr 2026 07:30:18 +0100 Subject: [PATCH 20/26] ci: add per-package coverage report with PR comment Adds a coverage matrix job to ci.yml that runs vitest --coverage per package on PRs and uses vitest-coverage-report-action to post a sticky comment with overall %, per-file breakdown, and diff coverage on changed lines. Requires pull-requests: write permission. --- .github/workflows/ci.yml | 42 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b67da38..f565484 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,9 +7,9 @@ on: branches: [main] workflow_dispatch: -# Minimal permissions — CI only needs to read code permissions: contents: read + pull-requests: write # Cancel in-progress runs for the same branch/PR when a new push arrives concurrency: @@ -101,6 +101,46 @@ jobs: - run: pnpm test + # ── Coverage report (PR comment) ───────────────────────────────────────────── + coverage: + name: Coverage / ${{ matrix.package }} + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 10 + + strategy: + fail-fast: false + matrix: + package: [adapter-claude, sdk, mcp-server, cli, sidecar] + + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 10 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + + - run: pnpm install --frozen-lockfile + + - run: pnpm build + + - name: Run tests with coverage + working-directory: packages/${{ matrix.package }} + run: npx vitest run --coverage --coverage.reporter=json-summary --coverage.reporter=json --coverage.reportsDirectory=coverage + + - name: Report coverage on PR + if: always() + uses: davelosert/vitest-coverage-report-action@v2 + with: + name: ${{ matrix.package }} + json-summary-path: packages/${{ matrix.package }}/coverage/coverage-summary.json + json-final-path: packages/${{ matrix.package }}/coverage/coverage-final.json + # ── Sidecar E2E (Docker Compose) ───────────────────────────────────────────── sidecar-e2e: name: Sidecar E2E From 7ff1b1a83958d24960c305bf29063c0d12480ac6 Mon Sep 17 00:00:00 2001 From: iliassjabali Date: Sun, 12 Apr 2026 07:39:09 +0100 Subject: [PATCH 21/26] fix(ci): tolerate test failures in coverage step, skip report if no output --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f565484..543fff5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -131,10 +131,10 @@ jobs: - name: Run tests with coverage working-directory: packages/${{ matrix.package }} - run: npx vitest run --coverage --coverage.reporter=json-summary --coverage.reporter=json --coverage.reportsDirectory=coverage + run: npx vitest run --coverage --coverage.reporter=json-summary --coverage.reporter=json --coverage.reportsDirectory=coverage || true - name: Report coverage on PR - if: always() + if: hashFiles(format('packages/{0}/coverage/coverage-summary.json', matrix.package)) != '' uses: davelosert/vitest-coverage-report-action@v2 with: name: ${{ matrix.package }} From b0e2a850a04723e518f3924ed48f535b628b8f4a Mon Sep 17 00:00:00 2001 From: iliassjabali Date: Sun, 12 Apr 2026 07:47:00 +0100 Subject: [PATCH 22/26] ci: unify coverage + mutation into single Quality workflow with PR comment Removes the separate coverage matrix from ci.yml. The mutation.yml workflow (renamed to Quality) now runs both vitest --coverage and Stryker per package in one job, then a final comment job posts a single sticky PR comment with a table of coverage % and mutation % per package. --- .github/workflows/ci.yml | 42 +------------ .github/workflows/mutation.yml | 108 +++++++++++++++++++++++++++++++-- 2 files changed, 104 insertions(+), 46 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 543fff5..4cac9a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,9 +7,9 @@ on: branches: [main] workflow_dispatch: +# Minimal permissions -- CI only needs to read code permissions: contents: read - pull-requests: write # Cancel in-progress runs for the same branch/PR when a new push arrives concurrency: @@ -101,46 +101,6 @@ jobs: - run: pnpm test - # ── Coverage report (PR comment) ───────────────────────────────────────────── - coverage: - name: Coverage / ${{ matrix.package }} - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - timeout-minutes: 10 - - strategy: - fail-fast: false - matrix: - package: [adapter-claude, sdk, mcp-server, cli, sidecar] - - steps: - - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - with: - version: 10 - - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: pnpm - - - run: pnpm install --frozen-lockfile - - - run: pnpm build - - - name: Run tests with coverage - working-directory: packages/${{ matrix.package }} - run: npx vitest run --coverage --coverage.reporter=json-summary --coverage.reporter=json --coverage.reportsDirectory=coverage || true - - - name: Report coverage on PR - if: hashFiles(format('packages/{0}/coverage/coverage-summary.json', matrix.package)) != '' - uses: davelosert/vitest-coverage-report-action@v2 - with: - name: ${{ matrix.package }} - json-summary-path: packages/${{ matrix.package }}/coverage/coverage-summary.json - json-final-path: packages/${{ matrix.package }}/coverage/coverage-final.json - # ── Sidecar E2E (Docker Compose) ───────────────────────────────────────────── sidecar-e2e: name: Sidecar E2E diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml index 40ce75d..36b73cf 100644 --- a/.github/workflows/mutation.yml +++ b/.github/workflows/mutation.yml @@ -1,11 +1,15 @@ -name: mutation +name: Quality on: pull_request: types: [opened, synchronize, reopened] +permissions: + contents: read + pull-requests: write + jobs: - mutation: - name: mutation / ${{ matrix.package }} + quality: + name: quality / ${{ matrix.package }} runs-on: ubuntu-latest timeout-minutes: 90 strategy: @@ -35,6 +39,10 @@ jobs: - name: Build all packages run: pnpm -r build + - name: Run tests with coverage + working-directory: packages/${{ matrix.package }} + run: npx vitest run --coverage --coverage.reporter=json-summary --coverage.reportsDirectory=coverage || true + - name: Restore Stryker incremental cache uses: actions/cache@v4 with: @@ -44,12 +52,102 @@ jobs: stryker-${{ matrix.package }}- - name: Run Stryker - run: pnpm mutation ${{ matrix.package }} + id: stryker + run: | + output=$(pnpm mutation ${{ matrix.package }} 2>&1) || true + echo "$output" + score=$(echo "$output" | grep -oP 'Mutation score.*?(\d+\.\d+)' | grep -oP '\d+\.\d+' | head -1) + echo "score=${score:-N/A}" >> "$GITHUB_OUTPUT" + + - name: Extract coverage percentage + id: coverage + run: | + file="packages/${{ matrix.package }}/coverage/coverage-summary.json" + if [ -f "$file" ]; then + pct=$(node -e "const s=JSON.parse(require('fs').readFileSync('$file','utf8'));console.log(s.total.lines.pct)") + echo "lines=${pct}" >> "$GITHUB_OUTPUT" + else + echo "lines=N/A" >> "$GITHUB_OUTPUT" + fi - - name: Upload HTML report + - name: Write score artifact + run: | + mkdir -p .quality-scores + echo '{"package":"${{ matrix.package }}","coverage":"${{ steps.coverage.outputs.lines }}","mutation":"${{ steps.stryker.outputs.score }}"}' > .quality-scores/${{ matrix.package }}.json + + - name: Upload scores + uses: actions/upload-artifact@v4 + with: + name: quality-score-${{ matrix.package }} + path: .quality-scores/${{ matrix.package }}.json + + - name: Upload Stryker HTML report if: always() uses: actions/upload-artifact@v4 with: name: mutation-report-${{ matrix.package }} path: reports/mutation/${{ matrix.package }}.html retention-days: 14 + + comment: + name: PR Comment + needs: quality + if: always() + runs-on: ubuntu-latest + steps: + - name: Download all score artifacts + uses: actions/download-artifact@v4 + with: + pattern: quality-score-* + merge-multiple: true + path: scores + + - name: Build and post comment + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const files = fs.readdirSync('scores').filter(f => f.endsWith('.json')); + const rows = files + .map(f => JSON.parse(fs.readFileSync(`scores/${f}`, 'utf8'))) + .sort((a, b) => a.package.localeCompare(b.package)) + .map(r => `| ${r.package} | ${r.coverage === 'N/A' ? 'N/A' : r.coverage + '%'} | ${r.mutation === 'N/A' ? 'N/A' : r.mutation + '%'} |`) + .join('\n'); + + const body = [ + '## Quality Report', + '', + '| Package | Line Coverage | Mutation Score |', + '|---------|-------------|----------------|', + rows, + '', + '> Coverage = % of lines your tests touch. Mutation = % of code changes your tests catch.', + '> Mutation reports available as workflow artifacts.', + ].join('\n'); + + const marker = ''; + const fullBody = marker + '\n' + body; + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const existing = comments.find(c => c.body?.includes(marker)); + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body: fullBody, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: fullBody, + }); + } From ba4fb43bf99426425d59e02bb7a69177b7d74285 Mon Sep 17 00:00:00 2001 From: iliassjabali Date: Sun, 12 Apr 2026 07:57:07 +0100 Subject: [PATCH 23/26] fix(ci): extract mutation score from JSON report instead of parsing ANSI output --- .github/workflows/mutation.yml | 14 ++++++++++---- stryker.config.mjs | 5 ++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml index 36b73cf..1d65d53 100644 --- a/.github/workflows/mutation.yml +++ b/.github/workflows/mutation.yml @@ -52,12 +52,18 @@ jobs: stryker-${{ matrix.package }}- - name: Run Stryker + run: pnpm mutation ${{ matrix.package }} || true + + - name: Extract mutation score id: stryker run: | - output=$(pnpm mutation ${{ matrix.package }} 2>&1) || true - echo "$output" - score=$(echo "$output" | grep -oP 'Mutation score.*?(\d+\.\d+)' | grep -oP '\d+\.\d+' | head -1) - echo "score=${score:-N/A}" >> "$GITHUB_OUTPUT" + file="reports/mutation/${{ matrix.package }}.json" + if [ -f "$file" ]; then + score=$(node -e "const r=JSON.parse(require('fs').readFileSync('$file','utf8'));const t=Object.values(r.files).reduce((a,f)=>{a.k+=f.mutants.filter(m=>m.status==='Killed').length;a.t+=f.mutants.length;return a},{k:0,t:0});console.log((t.t?t.k/t.t*100:0).toFixed(2))") + echo "score=${score}" >> "$GITHUB_OUTPUT" + else + echo "score=N/A" >> "$GITHUB_OUTPUT" + fi - name: Extract coverage percentage id: coverage diff --git a/stryker.config.mjs b/stryker.config.mjs index 3ddce6c..fd8fd62 100644 --- a/stryker.config.mjs +++ b/stryker.config.mjs @@ -27,7 +27,7 @@ export default { packageManager: 'pnpm', testRunner: 'vitest', plugins: ['@stryker-mutator/vitest-runner'], - reporters: ['html', 'progress', 'clear-text'], + reporters: ['html', 'progress', 'clear-text', 'json'], coverageAnalysis: 'perTest', incremental: true, concurrency: 4, @@ -45,6 +45,9 @@ export default { htmlReporter: { fileName: pkg ? `reports/mutation/${pkg}.html` : 'reports/mutation/index.html', }, + jsonReporter: { + fileName: pkg ? `reports/mutation/${pkg}.json` : 'reports/mutation/mutation.json', + }, ...(vitestConfigFile ? { vitest: { configFile: vitestConfigFile, dir: `packages/${pkg}` } } : {}), } From a839a88098c05f403efa6d7b5c10fc30cb9dd236 Mon Sep 17 00:00:00 2001 From: iliassjabali Date: Sun, 12 Apr 2026 08:23:12 +0100 Subject: [PATCH 24/26] feat: add survivor annotations, score trend deltas, and auto-ratchet - Surviving mutants now appear as warning annotations inline on the PR diff, on the exact line Stryker mutated. - PR comment shows deltas vs main baseline (e.g. 54.3% (+2.1)). Baseline stored in quality-baseline.json, updated post-merge. - Auto-ratchet workflow runs on push to main. If a package's mutation score exceeds its threshold by >5 points, it opens a PR to bump the threshold in stryker.config.mjs. --- .github/workflows/auto-ratchet.yml | 103 +++++++++++++++++++++++++++++ .github/workflows/mutation.yml | 31 ++++++++- docs/guides/mutation-testing.md | 33 +++++++-- quality-baseline.json | 7 ++ scripts/annotate-survivors.mjs | 21 ++++++ scripts/auto-ratchet.mjs | 54 +++++++++++++++ 6 files changed, 240 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/auto-ratchet.yml create mode 100644 quality-baseline.json create mode 100644 scripts/annotate-survivors.mjs create mode 100644 scripts/auto-ratchet.mjs diff --git a/.github/workflows/auto-ratchet.yml b/.github/workflows/auto-ratchet.yml new file mode 100644 index 0000000..e6bc239 --- /dev/null +++ b/.github/workflows/auto-ratchet.yml @@ -0,0 +1,103 @@ +name: Auto-ratchet thresholds +on: + push: + branches: [main] + paths: + - 'packages/*/src/**' + - 'stryker.config.mjs' + +permissions: + contents: write + pull-requests: write + +jobs: + ratchet: + name: Check & ratchet + runs-on: ubuntu-latest + timeout-minutes: 120 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build all packages + run: pnpm -r build + + - name: Run mutation for all packages + run: pnpm mutation || true + + - name: Update baseline scores + run: | + node -e " + const fs = require('fs'); + const packages = ['adapter-claude','sdk','mcp-server','cli','sidecar']; + const baseline = {}; + for (const pkg of packages) { + const covFile = 'packages/' + pkg + '/coverage/coverage-summary.json'; + const mutFile = 'reports/mutation/' + pkg + '.json'; + let coverage = null, mutation = null; + try { + coverage = JSON.parse(fs.readFileSync(covFile,'utf8')).total.lines.pct; + } catch {} + try { + const r = JSON.parse(fs.readFileSync(mutFile,'utf8')); + const t = Object.values(r.files).reduce((a,f)=>{a.k+=f.mutants.filter(m=>m.status==='Killed').length;a.t+=f.mutants.length;return a},{k:0,t:0}); + mutation = parseFloat((t.t?t.k/t.t*100:0).toFixed(2)); + } catch {} + baseline[pkg] = { coverage, mutation }; + } + fs.writeFileSync('quality-baseline.json', JSON.stringify(baseline, null, 2) + '\n'); + " + + - name: Try auto-ratchet + id: ratchet + run: | + if node scripts/auto-ratchet.mjs; then + echo "changed=true" >> "$GITHUB_OUTPUT" + else + echo "changed=false" >> "$GITHUB_OUTPUT" + fi + + - name: Update baseline and open PR if thresholds changed + if: steps.ratchet.outputs.changed == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + branch="auto-ratchet/$(date +%Y%m%d)" + git checkout -b "$branch" + git add stryker.config.mjs quality-baseline.json + git commit -m "chore: auto-ratchet mutation thresholds" + git push origin "$branch" + gh pr create \ + --base main \ + --head "$branch" \ + --title "chore: auto-ratchet mutation thresholds" \ + --body "Mutation scores have improved beyond their thresholds by more than 5 points. This PR bumps the thresholds to lock in the gains and updates the quality baseline." + env: + GH_TOKEN: ${{ github.token }} + + - name: Update baseline only (no threshold change) + if: steps.ratchet.outputs.changed != 'true' + run: | + if git diff --quiet quality-baseline.json; then + echo "No baseline changes" + else + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add quality-baseline.json + git commit -m "chore: update quality baseline scores" + git push + fi diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml index 1d65d53..26acc0d 100644 --- a/.github/workflows/mutation.yml +++ b/.github/workflows/mutation.yml @@ -54,6 +54,9 @@ jobs: - name: Run Stryker run: pnpm mutation ${{ matrix.package }} || true + - name: Annotate surviving mutants on PR diff + run: node scripts/annotate-survivors.mjs reports/mutation/${{ matrix.package }}.json || true + - name: Extract mutation score id: stryker run: | @@ -101,6 +104,13 @@ jobs: if: always() runs-on: ubuntu-latest steps: + - name: Checkout (for baseline file) + uses: actions/checkout@v4 + with: + ref: main + sparse-checkout: quality-baseline.json + sparse-checkout-cone-mode: false + - name: Download all score artifacts uses: actions/download-artifact@v4 with: @@ -113,11 +123,28 @@ jobs: with: script: | const fs = require('fs'); + + // Load baseline from main (may not exist yet) + let baseline = {}; + try { baseline = JSON.parse(fs.readFileSync('quality-baseline.json', 'utf8')); } catch {} + const files = fs.readdirSync('scores').filter(f => f.endsWith('.json')); const rows = files .map(f => JSON.parse(fs.readFileSync(`scores/${f}`, 'utf8'))) .sort((a, b) => a.package.localeCompare(b.package)) - .map(r => `| ${r.package} | ${r.coverage === 'N/A' ? 'N/A' : r.coverage + '%'} | ${r.mutation === 'N/A' ? 'N/A' : r.mutation + '%'} |`) + .map(r => { + const base = baseline[r.package] || {}; + const fmtDelta = (cur, prev) => { + if (cur === 'N/A' || prev == null) return ''; + const d = (parseFloat(cur) - prev).toFixed(1); + if (d > 0) return ` (+${d})`; + if (d < 0) return ` (${d})`; + return ''; + }; + const cov = r.coverage === 'N/A' ? 'N/A' : r.coverage + '%' + fmtDelta(r.coverage, base.coverage); + const mut = r.mutation === 'N/A' ? 'N/A' : r.mutation + '%' + fmtDelta(r.mutation, base.mutation); + return `| ${r.package} | ${cov} | ${mut} |`; + }) .join('\n'); const body = [ @@ -128,7 +155,7 @@ jobs: rows, '', '> Coverage = % of lines your tests touch. Mutation = % of code changes your tests catch.', - '> Mutation reports available as workflow artifacts.', + '> Deltas shown vs `main` baseline. Mutation reports available as workflow artifacts.', ].join('\n'); const marker = ''; diff --git a/docs/guides/mutation-testing.md b/docs/guides/mutation-testing.md index 86f0725..69c5ca6 100644 --- a/docs/guides/mutation-testing.md +++ b/docs/guides/mutation-testing.md @@ -8,11 +8,15 @@ This matters most for security-adjacent code like path-traversal guards and tag- ## What was added -Three files, no production code changes: +No production code changes. Infrastructure only: - `stryker.config.mjs` -- single root config with per-package threshold map -- `scripts/mutation.mjs` -- thin wrapper that sets `STRYKER_PKG` and execs Stryker -- `.github/workflows/mutation.yml` -- runs all 5 packages in parallel on every PR, blocks merge if score drops below the package's threshold +- `scripts/mutation.mjs` -- wrapper: `pnpm mutation adapter-claude` or `pnpm mutation` for all +- `scripts/annotate-survivors.mjs` -- emits GitHub warning annotations on PR diffs for surviving mutants +- `scripts/auto-ratchet.mjs` -- bumps thresholds when scores improve by more than 5 points +- `quality-baseline.json` -- last-known scores on main, used for delta display in PR comments +- `.github/workflows/mutation.yml` -- Quality workflow: coverage + mutation per package, unified PR comment +- `.github/workflows/auto-ratchet.yml` -- post-merge: updates baseline, opens ratchet PR if thresholds can be bumped ## Running locally @@ -21,7 +25,20 @@ pnpm mutation adapter-claude # single package pnpm mutation # all packages sequentially ``` -Each run produces an HTML report at `reports/mutation/.html`. First run is slow (full pass); subsequent runs use incremental cache. +Each run produces an HTML report at `reports/mutation/.html` and a JSON report at `reports/mutation/.json`. First run is slow; subsequent runs use incremental cache. + +## What you see on PRs + +**Quality Report comment** (one sticky comment, updated on each push): + +| Package | Line Coverage | Mutation Score | +|---------|--------------|----------------| +| adapter-claude | 83.9% (+1.2) | 50.8% (+0.5) | +| sdk | 67.2% | 54.3% (-0.3) | + +Deltas are vs the `main` baseline stored in `quality-baseline.json`. + +**Inline annotations**: surviving mutants appear as warnings directly on the PR diff, on the exact line Stryker mutated. No need to download the HTML report for the most common case. ## Thresholds @@ -33,10 +50,12 @@ Each run produces an HTML report at `reports/mutation/.html`. First run | cli | 45 | 49.50% | | sidecar | 46 | 51.43% | -Thresholds are set at baseline - 5. Ratchet up over time by editing the `THRESHOLDS` map in `stryker.config.mjs`. +Thresholds live in the `THRESHOLDS` map in `stryker.config.mjs`. + +**Auto-ratchet**: when a PR merges to main and a package's score exceeds its threshold by more than 5 points, the `auto-ratchet.yml` workflow opens a PR to bump the threshold automatically. You review and merge it like any other PR. ## When CI is red -1. Download the `mutation-report-` artifact from the failing check. -2. Open the HTML report. Surviving mutants are highlighted in red. +1. Check the inline annotations on your PR diff first -- they show the surviving mutants on the exact lines. +2. If you need more detail, download the `mutation-report-` artifact and open the HTML report. 3. Either write a stronger test or lower the threshold if the survivor is an equivalent mutant. diff --git a/quality-baseline.json b/quality-baseline.json new file mode 100644 index 0000000..59ff78d --- /dev/null +++ b/quality-baseline.json @@ -0,0 +1,7 @@ +{ + "adapter-claude": { "coverage": 83.93, "mutation": 50.85 }, + "sdk": { "coverage": null, "mutation": 54.31 }, + "mcp-server": { "coverage": null, "mutation": 31.31 }, + "cli": { "coverage": null, "mutation": 49.50 }, + "sidecar": { "coverage": null, "mutation": 51.43 } +} diff --git a/scripts/annotate-survivors.mjs b/scripts/annotate-survivors.mjs new file mode 100644 index 0000000..98e6c73 --- /dev/null +++ b/scripts/annotate-survivors.mjs @@ -0,0 +1,21 @@ +#!/usr/bin/env node +// Emits GitHub Actions warning annotations for surviving mutants. +// Usage: node scripts/annotate-survivors.mjs reports/mutation/.json + +import { readFileSync } from 'node:fs' + +const file = process.argv[2] +if (!file) { console.error('Usage: annotate-survivors.mjs '); process.exit(1) } + +const report = JSON.parse(readFileSync(file, 'utf8')) + +for (const [filePath, fileReport] of Object.entries(report.files)) { + for (const mutant of fileReport.mutants) { + if (mutant.status !== 'Survived') continue + const loc = mutant.location?.start + if (!loc) continue + const desc = mutant.mutatorName || 'unknown mutator' + const replacement = mutant.replacement ? ` \u2192 \`${mutant.replacement.slice(0, 60)}\`` : '' + console.log(`::warning file=${filePath},line=${loc.line},col=${loc.column}::Surviving mutant (${desc})${replacement}`) + } +} diff --git a/scripts/auto-ratchet.mjs b/scripts/auto-ratchet.mjs new file mode 100644 index 0000000..916ce9d --- /dev/null +++ b/scripts/auto-ratchet.mjs @@ -0,0 +1,54 @@ +#!/usr/bin/env node +// Reads current mutation scores and bumps thresholds in stryker.config.mjs +// when a package's score exceeds its threshold by more than 5 points. +// Usage: node scripts/auto-ratchet.mjs +// +// Outputs changed packages (if any) to stdout, one per line. +// Exits 0 if changes were made, 1 if nothing to ratchet. + +import { readFileSync, writeFileSync } from 'node:fs' + +const config = readFileSync('stryker.config.mjs', 'utf8') + +const thresholdMatch = config.match(/const THRESHOLDS\s*=\s*\{([^}]+)\}/) +if (!thresholdMatch) { console.error('Could not find THRESHOLDS in stryker.config.mjs'); process.exit(1) } + +const thresholds = {} +for (const line of thresholdMatch[1].split('\n')) { + const m = line.match(/'([^']+)':\s*(\d+)/) + if (m) thresholds[m[1]] = parseInt(m[2], 10) +} + +const changed = [] + +for (const [pkg, oldBreak] of Object.entries(thresholds)) { + const reportPath = `reports/mutation/${pkg}.json` + let score + try { + const report = JSON.parse(readFileSync(reportPath, 'utf8')) + const totals = Object.values(report.files).reduce( + (a, f) => { a.k += f.mutants.filter(m => m.status === 'Killed').length; a.t += f.mutants.length; return a }, + { k: 0, t: 0 }, + ) + score = totals.t ? (totals.k / totals.t) * 100 : 0 + } catch { + continue + } + + const newBreak = Math.floor(score) - 5 + if (newBreak > oldBreak) { + changed.push({ pkg, oldBreak, newBreak, score: score.toFixed(2) }) + } +} + +if (changed.length === 0) { + process.exit(1) +} + +let updated = config +for (const { pkg, oldBreak, newBreak } of changed) { + updated = updated.replace(`'${pkg}': ${oldBreak}`, `'${pkg}': ${newBreak}`) + console.log(`${pkg}: ${oldBreak} -> ${newBreak}`) +} + +writeFileSync('stryker.config.mjs', updated) From 6d3ecb2fbe37e9ac7ee26e21d580b145005832da Mon Sep 17 00:00:00 2001 From: iliassjabali Date: Sun, 12 Apr 2026 13:48:19 +0100 Subject: [PATCH 25/26] fix: address code review findings 1. mutation.yml: Stryker || true was swallowing the gate exit code. Now captures pass/fail, runs all post-steps with if: always(), then fails the job at the end via an 'Enforce mutation threshold' step. 2. auto-ratchet.yml: no longer pushes directly to main. Both baseline- only and threshold-bump updates now go through a PR. 3. auto-ratchet.yml: branch name includes short SHA to avoid collision when two merges happen on the same day. 4. auto-ratchet.yml: imports PACKAGES from stryker.config.mjs instead of duplicating the package list inline. 5. stryker.config.mjs: replaced em dash with double hyphen in comment. --- .github/workflows/auto-ratchet.yml | 45 +++++++++++++++++------------- .github/workflows/mutation.yml | 19 ++++++++++++- stryker.config.mjs | 2 +- 3 files changed, 45 insertions(+), 21 deletions(-) diff --git a/.github/workflows/auto-ratchet.yml b/.github/workflows/auto-ratchet.yml index e6bc239..a85ffd3 100644 --- a/.github/workflows/auto-ratchet.yml +++ b/.github/workflows/auto-ratchet.yml @@ -42,25 +42,23 @@ jobs: - name: Update baseline scores run: | node -e " - const fs = require('fs'); - const packages = ['adapter-claude','sdk','mcp-server','cli','sidecar']; + import { PACKAGES } from './stryker.config.mjs'; + import { readFileSync, writeFileSync } from 'fs'; const baseline = {}; - for (const pkg of packages) { - const covFile = 'packages/' + pkg + '/coverage/coverage-summary.json'; - const mutFile = 'reports/mutation/' + pkg + '.json'; + for (const pkg of PACKAGES) { let coverage = null, mutation = null; try { - coverage = JSON.parse(fs.readFileSync(covFile,'utf8')).total.lines.pct; + coverage = JSON.parse(readFileSync('packages/' + pkg + '/coverage/coverage-summary.json','utf8')).total.lines.pct; } catch {} try { - const r = JSON.parse(fs.readFileSync(mutFile,'utf8')); + const r = JSON.parse(readFileSync('reports/mutation/' + pkg + '.json','utf8')); const t = Object.values(r.files).reduce((a,f)=>{a.k+=f.mutants.filter(m=>m.status==='Killed').length;a.t+=f.mutants.length;return a},{k:0,t:0}); mutation = parseFloat((t.t?t.k/t.t*100:0).toFixed(2)); } catch {} baseline[pkg] = { coverage, mutation }; } - fs.writeFileSync('quality-baseline.json', JSON.stringify(baseline, null, 2) + '\n'); - " + writeFileSync('quality-baseline.json', JSON.stringify(baseline, null, 2) + '\n'); + " --input-type=module - name: Try auto-ratchet id: ratchet @@ -71,15 +69,15 @@ jobs: echo "changed=false" >> "$GITHUB_OUTPUT" fi - - name: Update baseline and open PR if thresholds changed + - name: Open PR with updated baseline and thresholds if: steps.ratchet.outputs.changed == 'true' run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - branch="auto-ratchet/$(date +%Y%m%d)" + branch="auto-ratchet/$(date +%Y%m%d)-$(git rev-parse --short HEAD)" git checkout -b "$branch" git add stryker.config.mjs quality-baseline.json - git commit -m "chore: auto-ratchet mutation thresholds" + git commit -m "chore: auto-ratchet mutation thresholds and update baseline" git push origin "$branch" gh pr create \ --base main \ @@ -89,15 +87,24 @@ jobs: env: GH_TOKEN: ${{ github.token }} - - name: Update baseline only (no threshold change) + - name: Open PR with updated baseline only if: steps.ratchet.outputs.changed != 'true' run: | if git diff --quiet quality-baseline.json; then echo "No baseline changes" - else - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add quality-baseline.json - git commit -m "chore: update quality baseline scores" - git push + exit 0 fi + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + branch="quality-baseline/$(date +%Y%m%d)-$(git rev-parse --short HEAD)" + git checkout -b "$branch" + git add quality-baseline.json + git commit -m "chore: update quality baseline scores" + git push origin "$branch" + gh pr create \ + --base main \ + --head "$branch" \ + --title "chore: update quality baseline scores" \ + --body "Post-merge quality baseline update. No threshold changes." + env: + GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml index 26acc0d..b93e1e4 100644 --- a/.github/workflows/mutation.yml +++ b/.github/workflows/mutation.yml @@ -52,12 +52,20 @@ jobs: stryker-${{ matrix.package }}- - name: Run Stryker - run: pnpm mutation ${{ matrix.package }} || true + id: stryker-run + run: | + if pnpm mutation ${{ matrix.package }}; then + echo "failed=false" >> "$GITHUB_OUTPUT" + else + echo "failed=true" >> "$GITHUB_OUTPUT" + fi - name: Annotate surviving mutants on PR diff + if: always() run: node scripts/annotate-survivors.mjs reports/mutation/${{ matrix.package }}.json || true - name: Extract mutation score + if: always() id: stryker run: | file="reports/mutation/${{ matrix.package }}.json" @@ -69,6 +77,7 @@ jobs: fi - name: Extract coverage percentage + if: always() id: coverage run: | file="packages/${{ matrix.package }}/coverage/coverage-summary.json" @@ -80,11 +89,13 @@ jobs: fi - name: Write score artifact + if: always() run: | mkdir -p .quality-scores echo '{"package":"${{ matrix.package }}","coverage":"${{ steps.coverage.outputs.lines }}","mutation":"${{ steps.stryker.outputs.score }}"}' > .quality-scores/${{ matrix.package }}.json - name: Upload scores + if: always() uses: actions/upload-artifact@v4 with: name: quality-score-${{ matrix.package }} @@ -98,6 +109,12 @@ jobs: path: reports/mutation/${{ matrix.package }}.html retention-days: 14 + - name: Enforce mutation threshold + if: steps.stryker-run.outputs.failed == 'true' + run: | + echo "Mutation score below threshold for ${{ matrix.package }}" + exit 1 + comment: name: PR Comment needs: quality diff --git a/stryker.config.mjs b/stryker.config.mjs index fd8fd62..8b31032 100644 --- a/stryker.config.mjs +++ b/stryker.config.mjs @@ -1,4 +1,4 @@ -// Mutation testing config — single source of truth for all packages. +// Mutation testing config -- single source of truth for all packages. // // Per-package behavior is driven by the STRYKER_PKG env var, set by // scripts/mutation.mjs. Thresholds live here, not in package.json. From 2f6c11fada7299f0fb095dfdf76b05d8784a3c17 Mon Sep 17 00:00:00 2001 From: iliassjabali Date: Sun, 12 Apr 2026 13:54:00 +0100 Subject: [PATCH 26/26] fix: address Copilot review comments 1. mutation.yml: replace || true on coverage step with continue-on-error so test failures are visible in the UI but don't block post-steps. 2. mutation.yml: add issues: write permission for the PR comment API calls (listComments/createComment/updateComment). 3. mutation.yml: skip PR comment job on fork PRs where GITHUB_TOKEN is read-only (guard via head.repo.full_name == github.repository). 4. auto-ratchet.yml: merge with existing baseline instead of overwriting. Coverage fields are preserved from the previous baseline when no new coverage file is available (this workflow runs mutation, not coverage). 5. annotate-survivors.mjs: escape %, newlines, colons, and commas per GitHub Actions annotation spec to prevent truncated/broken output. --- .github/workflows/auto-ratchet.yml | 10 +++++++--- .github/workflows/mutation.yml | 6 ++++-- scripts/annotate-survivors.mjs | 16 ++++++++++++++-- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/.github/workflows/auto-ratchet.yml b/.github/workflows/auto-ratchet.yml index a85ffd3..c53b3c4 100644 --- a/.github/workflows/auto-ratchet.yml +++ b/.github/workflows/auto-ratchet.yml @@ -39,14 +39,18 @@ jobs: - name: Run mutation for all packages run: pnpm mutation || true - - name: Update baseline scores + - name: Update baseline scores (merge, don't overwrite) run: | node -e " import { PACKAGES } from './stryker.config.mjs'; import { readFileSync, writeFileSync } from 'fs'; - const baseline = {}; + let existing = {}; + try { existing = JSON.parse(readFileSync('quality-baseline.json','utf8')); } catch {} + const baseline = { ...existing }; for (const pkg of PACKAGES) { - let coverage = null, mutation = null; + const prev = existing[pkg] || {}; + let coverage = prev.coverage ?? null; + let mutation = prev.mutation ?? null; try { coverage = JSON.parse(readFileSync('packages/' + pkg + '/coverage/coverage-summary.json','utf8')).total.lines.pct; } catch {} diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml index b93e1e4..0e9aab3 100644 --- a/.github/workflows/mutation.yml +++ b/.github/workflows/mutation.yml @@ -6,6 +6,7 @@ on: permissions: contents: read pull-requests: write + issues: write jobs: quality: @@ -40,8 +41,9 @@ jobs: run: pnpm -r build - name: Run tests with coverage + continue-on-error: true working-directory: packages/${{ matrix.package }} - run: npx vitest run --coverage --coverage.reporter=json-summary --coverage.reportsDirectory=coverage || true + run: npx vitest run --coverage --coverage.reporter=json-summary --coverage.reportsDirectory=coverage - name: Restore Stryker incremental cache uses: actions/cache@v4 @@ -118,7 +120,7 @@ jobs: comment: name: PR Comment needs: quality - if: always() + if: always() && github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest steps: - name: Checkout (for baseline file) diff --git a/scripts/annotate-survivors.mjs b/scripts/annotate-survivors.mjs index 98e6c73..bf57081 100644 --- a/scripts/annotate-survivors.mjs +++ b/scripts/annotate-survivors.mjs @@ -7,6 +7,16 @@ import { readFileSync } from 'node:fs' const file = process.argv[2] if (!file) { console.error('Usage: annotate-survivors.mjs '); process.exit(1) } +// GitHub Actions annotation escaping per +// https://github.com/actions/toolkit/blob/main/packages/core/src/command.ts +function escapeProperty(s) { + return s.replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A').replace(/:/g, '%3A').replace(/,/g, '%2C') +} + +function escapeMessage(s) { + return s.replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A') +} + const report = JSON.parse(readFileSync(file, 'utf8')) for (const [filePath, fileReport] of Object.entries(report.files)) { @@ -15,7 +25,9 @@ for (const [filePath, fileReport] of Object.entries(report.files)) { const loc = mutant.location?.start if (!loc) continue const desc = mutant.mutatorName || 'unknown mutator' - const replacement = mutant.replacement ? ` \u2192 \`${mutant.replacement.slice(0, 60)}\`` : '' - console.log(`::warning file=${filePath},line=${loc.line},col=${loc.column}::Surviving mutant (${desc})${replacement}`) + const replacement = mutant.replacement ? ` -> \`${mutant.replacement.slice(0, 60)}\`` : '' + const msg = escapeMessage(`Surviving mutant (${desc})${replacement}`) + const path = escapeProperty(filePath) + console.log(`::warning file=${path},line=${loc.line},col=${loc.column}::${msg}`) } }