diff --git a/.fallowrc.json b/.fallowrc.json
index 80ce2306c..8c9d51fdd 100644
--- a/.fallowrc.json
+++ b/.fallowrc.json
@@ -66,7 +66,7 @@
},
{
"comment": "Tool config default exports, loaded by the tool rather than imported.",
- "file": "{tsdown.config.ts,website/rspress.config.ts,test/skillgym/skillgym.config.ts,test/skillgym/suites/*.ts}",
+ "file": "{tsdown.config.ts,vitest.mutation.config.ts,website/rspress.config.ts,test/skillgym/skillgym.config.ts,test/skillgym/suites/*.ts}",
"exports": [
"default"
]
@@ -81,6 +81,20 @@
"GatedKeysAreResolverKeys"
]
},
+ {
+ "comment": "Mutation-lane seam: readTestScope is consumed by vitest.mutation.config.ts, a tool config outside --production analysis.",
+ "file": "scripts/mutation/test-scope.ts",
+ "exports": [
+ "readTestScope"
+ ]
+ },
+ {
+ "comment": "Mutation-lane completeness gate: ownedTestFiles enumerates the whole derived ownership map, which only the assertion in ownership.test.ts consumes — a production consumer would defeat the point.",
+ "file": "scripts/mutation/ownership.ts",
+ "exports": [
+ "ownedTestFiles"
+ ]
+ },
{
"comment": "Help-benchmark conformance seams: helpTopicIds feeds the topic-coverage gate and PRIVATE_AX_RECOVERY_SAMPLE feeds the skillgym suite; both consumers are test-tree files outside --production analysis.",
"file": "{src/cli/parser/cli-help.ts,scripts/help-conformance-sample-outputs.mjs}",
diff --git a/.github/workflows/mutation-affected.yml b/.github/workflows/mutation-affected.yml
new file mode 100644
index 000000000..c12a3e28e
--- /dev/null
+++ b/.github/workflows/mutation-affected.yml
@@ -0,0 +1,178 @@
+name: Mutation Affected
+
+# PR-side half of the decision-kernel mutation lane (issue #1415).
+#
+# Graduation, not a flag day: this job runs on every PR that touches a kernel
+# module, but `scripts/mutation/run.ts` only exits non-zero once the committed
+# baseline reports `gating: true` — earned by two consecutive stable weekly
+# sweeps (mutation-weekly.yml). Until then it is a report with the same numbers,
+# so the gate's first failing day is not also its first running day.
+#
+# Cost control: before graduation an affected run is a report nobody acts on, so
+# `select` returns an empty matrix and no mutants run — except when the diff
+# touches the lane's own tooling, the one case where a pre-graduation run buys
+# something (the gate has to be proven before it can bite).
+#
+# Scope is the AFFECTED modules only, and affectedness is DERIVED from the import
+# graph (scripts/mutation/ownership.ts): a kernel source, or any test that reaches
+# one. Reaching a kernel is a superset of killing its mutants, so the `select` job
+# frequently returns several modules — sharded like the weekly sweep so the PR's
+# wall clock is one module, not their sum. The weekly run stays the full sweep.
+
+on:
+ pull_request:
+ paths:
+ # Kernel sources, every src test (ownership is derived, so any test may own
+ # a kernel — `select` decides, not this filter), and the lane's own tooling.
+ # scripts/mutation/workflow.test.ts asserts this covers the registry.
+ - "src/kernel/errors.ts"
+ - "src/daemon/ref-frame.ts"
+ - "src/commands/interaction/runtime/settle.ts"
+ - "src/utils/scroll-edge-state.ts"
+ - "src/selectors/**"
+ - "src/**/*.test.ts"
+ - "scripts/mutation/**"
+ - "scripts/lib/**"
+ - "stryker.config.json"
+ - "mutation-baselines/**"
+ - ".github/workflows/mutation-affected.yml"
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ci-${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ select:
+ name: Select affected kernels
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ outputs:
+ modules: ${{ steps.select.outputs.modules }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ fetch-depth: 0
+
+ - name: Setup toolchain
+ uses: ./.github/actions/setup-node-pnpm
+
+ - name: Ratchet self-test
+ run: pnpm mutation:test
+
+ - id: select
+ name: Derive the affected shard matrix
+ run: |
+ modules=$(pnpm --silent mutation:affected --list-affected \
+ --base "origin/${{ github.event.pull_request.base.ref }}" | tail -n1)
+ echo "modules=$modules" >> "$GITHUB_OUTPUT"
+ echo "Affected mutation shards: $modules" >> "$GITHUB_STEP_SUMMARY"
+
+ # The self-test and the derivation run before any mutant, so a failure here
+ # would leave the lane with no envelope at all (#1430).
+ - name: Record a failed lane envelope
+ if: failure()
+ run: |
+ pnpm mutation:run --affected --fail-envelope \
+ "affected selection failed before any mutant ran (run ${{ github.run_id }})" || true
+
+ - name: Upload selection envelope
+ if: failure()
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: mutation-affected-select
+ path: .tmp/mutation/lane-envelope.json
+ if-no-files-found: warn
+
+ mutants:
+ name: Mutants (${{ matrix.name }})
+ needs: select
+ if: needs.select.outputs.modules != '[]'
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ strategy:
+ fail-fast: false
+ matrix:
+ include: ${{ fromJSON(needs.select.outputs.modules) }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+
+ - name: Setup toolchain
+ uses: ./.github/actions/setup-node-pnpm
+
+ - name: Run mutants for ${{ matrix.name }}
+ run: |
+ pnpm mutation:run --modules ${{ matrix.module }} \
+ ${{ matrix.shard && format('--shard {0}', matrix.shard) || '' }}
+
+ - name: Record a failed shard envelope
+ if: failure()
+ run: |
+ pnpm mutation:run --affected --modules ${{ matrix.module }} --fail-envelope \
+ "shard ${{ matrix.name }} failed before producing a report (run ${{ github.run_id }})" || true
+
+ - name: Upload shard report
+ if: always()
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: mutation-affected-shard-${{ matrix.name }}
+ path: |
+ .tmp/mutation/mutation.json
+ .tmp/mutation/mutation.html
+ .tmp/mutation/lane-envelope.json
+ if-no-files-found: warn
+
+ ratchet:
+ name: Affected decision-kernel mutants
+ needs: [select, mutants]
+ if: always() && needs.select.result == 'success'
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ steps:
+ - name: Checkout
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ fetch-depth: 0
+
+ - name: Setup toolchain
+ uses: ./.github/actions/setup-node-pnpm
+
+ - name: Download shard reports
+ if: needs.select.outputs.modules != '[]'
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ pattern: mutation-affected-shard-*
+ path: .tmp/mutation/shards
+
+ # No shards means no affected kernel: run.ts reports "nothing to mutate"
+ # and still writes the envelope, so the lane is never silently absent.
+ - name: Ratchet the affected modules
+ run: |
+ if [ -d .tmp/mutation/shards ]; then
+ expected=$(echo '${{ needs.select.outputs.modules }}' | jq length)
+ pnpm mutation:check --report-dir .tmp/mutation/shards --affected \
+ --expect-shards "$expected" \
+ --base "origin/${{ github.event.pull_request.base.ref }}"
+ else
+ pnpm mutation:affected --base "origin/${{ github.event.pull_request.base.ref }}"
+ fi
+
+ - name: Record a failed lane envelope
+ if: failure()
+ run: |
+ pnpm mutation:run --affected --fail-envelope \
+ "affected ratchet failed before producing a verdict (run ${{ github.run_id }})" || true
+
+ - name: Upload mutation report
+ if: always()
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: mutation-affected
+ path: |
+ .tmp/mutation/shards
+ .tmp/mutation/lane-envelope.json
+ if-no-files-found: ignore
diff --git a/.github/workflows/mutation-weekly.yml b/.github/workflows/mutation-weekly.yml
new file mode 100644
index 000000000..99df7f329
--- /dev/null
+++ b/.github/workflows/mutation-weekly.yml
@@ -0,0 +1,153 @@
+name: Mutation Weekly
+
+# Weekly mutation sweep over the enumerated decision kernels (issue #1415).
+# Mutation score is the mechanical answer to "is this test load-bearing or
+# decorative"; a full-suite sweep is unaffordable, so the scope is the kernel
+# registry in scripts/mutation/modules.ts and nothing else.
+#
+# Sharded one job per module: the whole sweep is ~2,150 mutants, and the selector
+# module alone is ~1,280 of them, so a single job would sit near the 30-minute
+# acceptance budget on an ubuntu runner. The shards' JSON reports are merged into
+# one verdict by the ratchet job (`--report-dir`), so the ratchet still sees a
+# full sweep.
+#
+# The lane reports; it does not commit. The proposed baseline rides in the
+# artifact and applying it is a reviewed `pnpm mutation:baseline` commit — a score
+# can never lower itself. Gating (after two consecutive stable weekly runs) is
+# enforced on PRs by mutation-affected.yml.
+
+on:
+ schedule:
+ # Sundays 05:00 UTC — after the nightly lanes, before the working week.
+ - cron: "0 5 * * 0"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ci-${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ shard:
+ name: Mutants (${{ matrix.name }})
+ runs-on: ubuntu-latest
+ # The acceptance budget is 30 minutes of wall clock for the lane, and shards
+ # run in parallel, so the budget is per shard. The observed rate on a 2-core
+ # runner is ~3s/mutant, which is why selectors (~1,280 mutants) is sliced.
+ timeout-minutes: 30
+ strategy:
+ fail-fast: false
+ matrix:
+ # Kept in step with KERNEL_MODULES by the shard-coverage assertion in
+ # scripts/mutation/workflow.test.ts, which also pins --expect-shards to
+ # the number of entries here.
+ include:
+ - { name: kernel-errors, module: kernel-errors }
+ - { name: daemon-ref-frame, module: daemon-ref-frame }
+ - { name: interaction-settle, module: interaction-settle }
+ - { name: scroll-edge-state, module: scroll-edge-state }
+ - { name: selectors-1, module: selectors, shard: 1/4 }
+ - { name: selectors-2, module: selectors, shard: 2/4 }
+ - { name: selectors-3, module: selectors, shard: 3/4 }
+ - { name: selectors-4, module: selectors, shard: 4/4 }
+ steps:
+ - name: Checkout
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+
+ - name: Setup toolchain
+ uses: ./.github/actions/setup-node-pnpm
+
+ - name: Run mutants for ${{ matrix.name }}
+ run: |
+ pnpm mutation:run --modules ${{ matrix.module }} \
+ ${{ matrix.shard && format('--shard {0}', matrix.shard) || '' }}
+
+ # A shard can die before Stryker writes anything (install, config, crash);
+ # without this the artifact is absent and "shard failed" is indistinguishable
+ # from "lane never ran". --fail-envelope leaves a real verdict untouched.
+ - name: Record a failed shard envelope
+ if: failure()
+ run: |
+ pnpm mutation:run --modules ${{ matrix.module }} --fail-envelope \
+ "shard ${{ matrix.name }} failed before producing a report (run ${{ github.run_id }})" || true
+
+ - name: Upload shard report
+ if: always()
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: mutation-shard-${{ matrix.name }}
+ path: |
+ .tmp/mutation/mutation.json
+ .tmp/mutation/lane-envelope.json
+ if-no-files-found: warn
+
+ ratchet:
+ name: Mutation ratchet
+ runs-on: ubuntu-latest
+ needs: shard
+ if: always()
+ timeout-minutes: 15
+ steps:
+ - name: Checkout
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+
+ - name: Setup toolchain
+ uses: ./.github/actions/setup-node-pnpm
+
+ - name: Ratchet self-test
+ run: pnpm mutation:test
+
+ - name: Download shard reports
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ pattern: mutation-shard-*
+ path: .tmp/mutation/shards
+
+ # run.ts writes the markdown verdict to $GITHUB_STEP_SUMMARY when the
+ # runner exports it, so the summary and the artifact carry the same numbers.
+ - name: Ratchet the merged sweep and propose the next baseline
+ run: |
+ pnpm mutation:check --report-dir .tmp/mutation/shards --expect-shards 8 --update
+ cp mutation-baselines/decision-kernels.json .tmp/mutation/proposed-baseline.json
+ git checkout -- mutation-baselines/decision-kernels.json
+
+ # The self-test and the artifact download both run before the ratchet, so a
+ # failure there would otherwise leave the aggregate lane with no envelope.
+ - name: Record a failed lane envelope
+ if: failure()
+ run: |
+ pnpm mutation:run --fail-envelope \
+ "weekly ratchet job failed before producing a verdict (run ${{ github.run_id }})" || true
+
+ # Freshness/drift telemetry (#1430): the envelope states commit, Stryker
+ # version, config hash, duration and result, so a lane going dark or a tool
+ # bump is visible without reading these logs.
+ - name: Lane envelope
+ if: always()
+ run: |
+ if [ ! -f .tmp/mutation/lane-envelope.json ]; then
+ echo 'No lane envelope was written — the job died before it could run node.' \
+ >> "$GITHUB_STEP_SUMMARY"
+ exit 0
+ fi
+ {
+ echo 'Lane envelope (schema #1430)
'
+ echo
+ echo '```json'
+ cat .tmp/mutation/lane-envelope.json
+ echo '```'
+ echo ' '
+ } >> "$GITHUB_STEP_SUMMARY"
+
+ - name: Upload mutation report
+ if: always()
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: mutation-decision-kernels
+ path: |
+ .tmp/mutation/shards
+ .tmp/mutation/lane-envelope.json
+ .tmp/mutation/proposed-baseline.json
+ if-no-files-found: warn
diff --git a/docs/agents/testing.md b/docs/agents/testing.md
index 6e778d1b2..dd8780417 100644
--- a/docs/agents/testing.md
+++ b/docs/agents/testing.md
@@ -20,6 +20,7 @@ The mapping it encodes, for when you need to run a gate directly or reason about
| Help benchmark cases (`scripts/help-conformance-*.mjs`) | `pnpm exec vitest run scripts/__tests__` (deterministic gates); model-backed: `pnpm bench:help-conformance` (paid LLM calls, local only) |
| SkillGym prompts/assertions | `pnpm test:skillgym:case ` (broad: `pnpm test:skillgym`, filter with `-- --tag fixture-smoke` or `-- --tag skill-guidance`) — agentic routing + local-help-consumption proof only; command-planning knowledge checks belong in the help bench |
| Anything in `src/`, `test/`, `skills/` | `pnpm format` |
+| A decision kernel or its tests (`src/kernel/errors.ts`, `src/daemon/ref-frame.ts`, `src/commands/interaction/runtime/settle.ts`, `src/utils/scroll-edge-state.ts`, `src/selectors/`) | `pnpm mutation:affected --base origin/main` (minutes; GitHub runs it per PR — see the mutation ratchet section) |
Two traps worth naming:
@@ -119,6 +120,64 @@ The plan documents the rule and changed path behind every selected check.
Model and catalog live under `scripts/check-affected/`; the derivation is guarded
by `pnpm check:affected:test` (the `Affected-check Selector` CI job).
+## Mutation ratchet over decision kernels
+
+Mutation score is the mechanical answer to "is this test load-bearing or decorative". A full-suite
+sweep is unaffordable, so the scope is an enumerated list of pure decision kernels — modules where a
+surviving mutant means a silently wrong agent-facing decision. The registry
+(`scripts/mutation/modules.ts`) is the single source of truth: `stryker.config.json`'s `mutate` globs
+are asserted against it, and PR-affected selection maps changed files through it. Modules that spawn
+subprocesses or wait real time stay out by construction.
+
+```sh
+pnpm mutation:test # ratchet self-test (fast, no Stryker)
+pnpm mutation:run --modules selectors # one module locally (~7 min for selectors)
+pnpm mutation:check # ratchet an existing .tmp/mutation/mutation.json
+pnpm mutation:baseline # full sweep, then record it (reviewed commit)
+```
+
+- **Weekly full sweep** (`.github/workflows/mutation-weekly.yml`) runs `shardMatrix()` from the
+ registry: one job per module, except modules that declare a `shards` count and are sliced with
+ `--shard i/n` (selectors is ~1,280 mutants, well past the 30-minute budget in one job). The ratchet
+ merges the shard reports (`--report-dir`) for one verdict and requires the full set
+ (`--expect-shards`), so a dead shard fails the lane instead of scoring its module as 0. Results are
+ reported as a job summary plus an artifact. It never commits: the proposed baseline rides in the
+ artifact, and applying it is a reviewed `pnpm mutation:baseline` commit, so a score cannot lower
+ itself.
+- **PR lane** (`.github/workflows/mutation-affected.yml`) derives the affected shard matrix
+ (`--list-affected`) and merges the shards into one verdict. Before graduation the matrix is empty —
+ a report nobody acts on is not worth the runner minutes — unless the diff touches the lane's own
+ tooling, the one pre-graduation run that buys something: the gate has to be proven before it bites.
+ Lane sources own no kernel, so that exception adds `LANE_CANARY` (`kernel-errors`, the registry's
+ cheapest real sweep) to whatever the diff derives; otherwise it would select zero mutants and prove
+ nothing. `scripts/mutation/selection.test.ts` drives both halves of the rule through the real CLI
+ against a throwaway worktree commit.
+- **Ratchet**: scores may only rise. `mutation-baselines/decision-kernels.json` records the
+ high-water score per module plus the Stryker version and config content hash that produced it, so a
+ score change caused by a tool/config change is reported as provenance drift, never as a
+ test-strength regression.
+- **Graduation, not a flag day**: gating is off until two consecutive comparable weekly sweeps pass
+ (`stableRuns`/`requiredStableRuns` in the baseline); the PR job starts selecting modules — and
+ failing on them — once the committed baseline says `gating: true`. A regression or provenance drift
+ resets the counter.
+- **Test scope** is derived from Vitest's module graph (`vitest related` over the mutated files), the
+ same delegation `pnpm check:affected` uses; see `scripts/mutation/test-scope.ts` for the three
+ groups it drops and why dropping them cannot hide a surviving mutant.
+- **Test ownership is derived, never listed** (`scripts/mutation/ownership.ts`): a test owns every
+ kernel its imports reach, so `src/__tests__/daemon-error.test.ts` selects `kernel-errors` through
+ `src/daemon.ts` without naming it. A listed set of test files would silently omit exactly those
+ indirect tests and rot as tests are added — weakening one would skip the ratchet. Reaching a kernel
+ is a superset of killing its mutants, so the PR lane over-selects on purpose and shards the
+ selected modules; a false positive costs runner minutes, a false negative costs the gate. Non-kernel
+ *sources* are not owned: they can only move a score through those tests, and the weekly sweep
+ re-measures the whole surface.
+- **Lane envelope** (`scripts/lib/lane-envelope.ts`, issue #1430): every run writes
+ `.tmp/mutation/lane-envelope.json` — schema version, commit, Stryker version, config hash, seed
+ (`null`; the input is enumerated, not randomized), duration, result, stage, per-module scores — and
+ both workflows upload it, so lane freshness and tool drift are readable without parsing logs. It is
+ written on every exit path, including a crash before any mutant runs: an absent envelope would be
+ indistinguishable from a lane that never ran.
+
## Live web smoke
The live web platform smoke runs the public built CLI against a local fixture page through the managed web backend:
diff --git a/mutation-baselines/decision-kernels.json b/mutation-baselines/decision-kernels.json
new file mode 100644
index 000000000..357333b17
--- /dev/null
+++ b/mutation-baselines/decision-kernels.json
@@ -0,0 +1,53 @@
+{
+ "schemaVersion": 1,
+ "requiredStableRuns": 2,
+ "stableRuns": 0,
+ "gating": false,
+ "modules": {
+ "kernel-errors": {
+ "score": 55.19,
+ "killed": 101,
+ "survived": 82,
+ "total": 183,
+ "strykerVersion": "9.6.1",
+ "configHash": "sha256:806d9f2e657f",
+ "updatedAt": "2026-07-27T14:29:40.750Z"
+ },
+ "daemon-ref-frame": {
+ "score": 100,
+ "killed": 55,
+ "survived": 0,
+ "total": 55,
+ "strykerVersion": "9.6.1",
+ "configHash": "sha256:806d9f2e657f",
+ "updatedAt": "2026-07-27T14:29:40.750Z"
+ },
+ "interaction-settle": {
+ "score": 68.95,
+ "killed": 151,
+ "survived": 68,
+ "total": 219,
+ "strykerVersion": "9.6.1",
+ "configHash": "sha256:806d9f2e657f",
+ "updatedAt": "2026-07-27T14:29:40.750Z"
+ },
+ "scroll-edge-state": {
+ "score": 28.75,
+ "killed": 92,
+ "survived": 228,
+ "total": 320,
+ "strykerVersion": "9.6.1",
+ "configHash": "sha256:806d9f2e657f",
+ "updatedAt": "2026-07-27T14:29:40.750Z"
+ },
+ "selectors": {
+ "score": 70.56,
+ "killed": 901,
+ "survived": 376,
+ "total": 1277,
+ "strykerVersion": "9.6.1",
+ "configHash": "sha256:806d9f2e657f",
+ "updatedAt": "2026-07-27T14:29:40.750Z"
+ }
+ }
+}
diff --git a/package.json b/package.json
index 27a3e9c46..a59c8eb7f 100644
--- a/package.json
+++ b/package.json
@@ -108,9 +108,14 @@
"perf": "node --experimental-strip-types scripts/perf/run.ts",
"perf:ios": "node --experimental-strip-types scripts/perf/run.ts --platform ios",
"perf:android": "node --experimental-strip-types scripts/perf/run.ts --platform android",
+ "mutation:run": "node --experimental-strip-types scripts/mutation/run.ts",
+ "mutation:baseline": "node --experimental-strip-types scripts/mutation/run.ts --update",
+ "mutation:check": "node --experimental-strip-types scripts/mutation/run.ts --no-run",
+ "mutation:affected": "node --experimental-strip-types scripts/mutation/run.ts --affected",
+ "mutation:test": "node --experimental-strip-types --test scripts/mutation/*.test.ts",
"lint": "oxlint . --deny-warnings",
- "format": "node ./node_modules/oxfmt/bin/oxfmt --write src test skills scripts/help-conformance-bench.mjs scripts/help-conformance-case-checks.mjs scripts/help-conformance-command-validator.ts scripts/help-conformance-expectations.mjs scripts/help-conformance-plan-validator.mjs scripts/help-conformance-runner-output.mjs scripts/help-conformance-summary.mjs scripts/help-conformance-cases.mjs scripts/help-conformance-sample-outputs.mjs scripts/__tests__/help-conformance-bench.test.ts scripts/__tests__/help-conformance-sample-outputs.test.ts scripts/__tests__/help-conformance-topic-coverage.test.ts package.json tsconfig.json tsconfig.lib.json tsdown.config.ts vitest.config.ts .github/actions/setup-node-pnpm/action.yml .oxlintrc.json .oxfmtrc.json '!test/skillgym/.skillgym-results/**'",
- "format:check": "node ./node_modules/oxfmt/bin/oxfmt --check src test skills scripts/help-conformance-bench.mjs scripts/help-conformance-case-checks.mjs scripts/help-conformance-command-validator.ts scripts/help-conformance-expectations.mjs scripts/help-conformance-plan-validator.mjs scripts/help-conformance-runner-output.mjs scripts/help-conformance-summary.mjs scripts/help-conformance-cases.mjs scripts/help-conformance-sample-outputs.mjs scripts/__tests__/help-conformance-bench.test.ts scripts/__tests__/help-conformance-sample-outputs.test.ts scripts/__tests__/help-conformance-topic-coverage.test.ts package.json tsconfig.json tsconfig.lib.json tsdown.config.ts vitest.config.ts .github/actions/setup-node-pnpm/action.yml .oxlintrc.json .oxfmtrc.json '!test/skillgym/.skillgym-results/**'",
+ "format": "node ./node_modules/oxfmt/bin/oxfmt --write src test skills scripts/mutation scripts/lib scripts/help-conformance-bench.mjs scripts/help-conformance-case-checks.mjs scripts/help-conformance-command-validator.ts scripts/help-conformance-expectations.mjs scripts/help-conformance-plan-validator.mjs scripts/help-conformance-runner-output.mjs scripts/help-conformance-summary.mjs scripts/help-conformance-cases.mjs scripts/help-conformance-sample-outputs.mjs scripts/__tests__/help-conformance-bench.test.ts scripts/__tests__/help-conformance-sample-outputs.test.ts scripts/__tests__/help-conformance-topic-coverage.test.ts package.json tsconfig.json tsconfig.lib.json tsdown.config.ts vitest.config.ts vitest.mutation.config.ts .github/actions/setup-node-pnpm/action.yml .oxlintrc.json .oxfmtrc.json '!test/skillgym/.skillgym-results/**'",
+ "format:check": "node ./node_modules/oxfmt/bin/oxfmt --check src test skills scripts/mutation scripts/lib scripts/help-conformance-bench.mjs scripts/help-conformance-case-checks.mjs scripts/help-conformance-command-validator.ts scripts/help-conformance-expectations.mjs scripts/help-conformance-plan-validator.mjs scripts/help-conformance-runner-output.mjs scripts/help-conformance-summary.mjs scripts/help-conformance-cases.mjs scripts/help-conformance-sample-outputs.mjs scripts/__tests__/help-conformance-bench.test.ts scripts/__tests__/help-conformance-sample-outputs.test.ts scripts/__tests__/help-conformance-topic-coverage.test.ts package.json tsconfig.json tsconfig.lib.json tsdown.config.ts vitest.config.ts vitest.mutation.config.ts .github/actions/setup-node-pnpm/action.yml .oxlintrc.json .oxfmtrc.json '!test/skillgym/.skillgym-results/**'",
"fallow": "fallow audit --base origin/main",
"fallow:all": "fallow --summary",
"fallow:baseline": "(fallow dead-code --save-baseline fallow-baselines/dead-code.json --summary || true) && (fallow health --save-baseline fallow-baselines/health.json --summary || true)",
@@ -228,6 +233,8 @@
},
"devDependencies": {
"@chenglou/freerange": "^0.0.1",
+ "@stryker-mutator/core": "9.6.1",
+ "@stryker-mutator/vitest-runner": "9.6.1",
"@types/node": "^22.19.21",
"@vitest/coverage-v8": "4.1.8",
"fallow": "^2.95.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d4d402a87..7137e0dee 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -24,6 +24,12 @@ importers:
'@chenglou/freerange':
specifier: ^0.0.1
version: 0.0.1
+ '@stryker-mutator/core':
+ specifier: 9.6.1
+ version: 9.6.1(@types/node@22.19.21)
+ '@stryker-mutator/vitest-runner':
+ specifier: 9.6.1
+ version: 9.6.1(@stryker-mutator/core@9.6.1(@types/node@22.19.21))(vitest@4.1.8)
'@types/node':
specifier: ^22.19.21
version: 22.19.21
@@ -72,23 +78,176 @@ importers:
packages:
+ '@babel/code-frame@7.29.7':
+ resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/compat-data@7.29.7':
+ resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/core@7.29.7':
+ resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/generator@7.29.7':
+ resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-annotate-as-pure@7.29.7':
+ resolution: {integrity: sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-compilation-targets@7.29.7':
+ resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-create-class-features-plugin@7.29.7':
+ resolution: {integrity: sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-globals@7.29.7':
+ resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-member-expression-to-functions@7.29.7':
+ resolution: {integrity: sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-imports@7.29.7':
+ resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-transforms@7.29.7':
+ resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-optimise-call-expression@7.29.7':
+ resolution: {integrity: sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-plugin-utils@7.29.7':
+ resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-replace-supers@7.29.7':
+ resolution: {integrity: sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-skip-transparent-expression-wrappers@7.29.7':
+ resolution: {integrity: sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==}
+ engines: {node: '>=6.9.0'}
+
'@babel/helper-string-parser@7.27.1':
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
engines: {node: '>=6.9.0'}
+ '@babel/helper-string-parser@7.29.7':
+ resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
+ engines: {node: '>=6.9.0'}
+
'@babel/helper-validator-identifier@7.28.5':
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
engines: {node: '>=6.9.0'}
+ '@babel/helper-validator-identifier@7.29.7':
+ resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-option@7.29.7':
+ resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helpers@7.29.7':
+ resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==}
+ engines: {node: '>=6.9.0'}
+
'@babel/parser@7.29.3':
resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==}
engines: {node: '>=6.0.0'}
hasBin: true
+ '@babel/parser@7.29.7':
+ resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/plugin-proposal-decorators@7.29.7':
+ resolution: {integrity: sha512-EtU0Hi3GvrTqD56xKmZvV/uCXK2ZbwVNPNLAquVItcAZpUhkXwWlo3Fmj0c2LxgSf2I8IDULeAepwNP1OefLXg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-decorators@7.29.7':
+ resolution: {integrity: sha512-9MTTLbF39X6sqM92JPEsoI7++26hjZvzkxKZy64aMhWLH2mPkJ/Q3AV4QLmls3R14FpSpkOwQQfUh962JGQxxg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-jsx@7.29.7':
+ resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-typescript@7.29.7':
+ resolution: {integrity: sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-destructuring@7.29.7':
+ resolution: {integrity: sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-explicit-resource-management@7.29.7':
+ resolution: {integrity: sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-modules-commonjs@7.29.7':
+ resolution: {integrity: sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-typescript@7.29.7':
+ resolution: {integrity: sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/preset-typescript@7.28.5':
+ resolution: {integrity: sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/template@7.29.7':
+ resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/traverse@7.29.7':
+ resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==}
+ engines: {node: '>=6.9.0'}
+
'@babel/types@7.29.0':
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
engines: {node: '>=6.9.0'}
+ '@babel/types@7.29.7':
+ resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
+ engines: {node: '>=6.9.0'}
+
'@bcoe/v8-coverage@1.0.2':
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
engines: {node: '>=18'}
@@ -167,6 +326,146 @@ packages:
cpu: [x64]
os: [win32]
+ '@inquirer/ansi@2.0.7':
+ resolution: {integrity: sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==}
+ engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+
+ '@inquirer/checkbox@5.2.1':
+ resolution: {integrity: sha512-b6xmA/VlTe0ZgDQHDui+Nav470u7u49nRd8/iuhOcQPO9Ch7lGuogydhi2VOmNlZ+zXcM8IcPuNSwQcdJaF/kw==}
+ engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+ peerDependencies:
+ '@types/node': '>=18'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
+ '@inquirer/confirm@6.1.1':
+ resolution: {integrity: sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ==}
+ engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+ peerDependencies:
+ '@types/node': '>=18'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
+ '@inquirer/core@11.2.1':
+ resolution: {integrity: sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==}
+ engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+ peerDependencies:
+ '@types/node': '>=18'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
+ '@inquirer/editor@5.2.2':
+ resolution: {integrity: sha512-ZRVd/oD+sYsUd5zVm0NflqEzlqfYCyHNsqkHl2oWXEUHs12tCbcSFi+wVFEvD8+LGRaMUsVrE7qeo6lSG/S1Vg==}
+ engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+ peerDependencies:
+ '@types/node': '>=18'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
+ '@inquirer/expand@5.1.1':
+ resolution: {integrity: sha512-YmQpenjbFSHAK3sOd44puHh3V1KXXr+JiNpUztoSQ4drLh2rTVzTap/YtlAVu/5xavifIlBfNEzJ/neZJ1a/1g==}
+ engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+ peerDependencies:
+ '@types/node': '>=18'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
+ '@inquirer/external-editor@3.0.3':
+ resolution: {integrity: sha512-6thf5I8q7lZwzGLAxPaaGEREEkZ3nyePPDQ1oyobblxmEE8mqTLguScP7pDjUTAibiyb4hfXl+qjUEJ+di/aNA==}
+ engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+ peerDependencies:
+ '@types/node': '>=18'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
+ '@inquirer/figures@2.0.7':
+ resolution: {integrity: sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==}
+ engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+
+ '@inquirer/input@5.1.2':
+ resolution: {integrity: sha512-9K/DDBSQpOyZSkt6sOVP9Vo0TR7atX2kuILsUu0x3wVcVbe97lJwIJKMLdMw25tDYuXl/qp6erT0Xs1rfmcfZg==}
+ engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+ peerDependencies:
+ '@types/node': '>=18'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
+ '@inquirer/number@4.1.1':
+ resolution: {integrity: sha512-XF4IXAbPnGPgw0wsbC/i2tPcyfdZgDpUlhsqU0SfT4IRIGWha6Xm9VRgN5yYxJq+jnyXlfXI/nQ3ulfk0iEICA==}
+ engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+ peerDependencies:
+ '@types/node': '>=18'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
+ '@inquirer/password@5.1.1':
+ resolution: {integrity: sha512-3XBfF7DAsp5qeDsvN5Rd1HmbNokVvEQoUM0QLrRcybC9nX96w3Pbmu7qUsb3IT3J3jBvs2+mTXaKHOUsgHMLzg==}
+ engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+ peerDependencies:
+ '@types/node': '>=18'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
+ '@inquirer/prompts@8.5.2':
+ resolution: {integrity: sha512-IYR/3C/paEVVQYQvdDlFZVjRCJVYHHON0XXMH91KO9GSxs0TdKYWlUdvfQl2EfAHDxUaN3IBffkE/BDTh5nJ6g==}
+ engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+ peerDependencies:
+ '@types/node': '>=18'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
+ '@inquirer/rawlist@5.3.1':
+ resolution: {integrity: sha512-QqdTqQddL3qPX/PPrjobpsO25NZ4dWXgTLenrR445L2ptLEYE6Z+PD5c5CNDJNx4ugRgELAIpSIJxZaO2jJ2Og==}
+ engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+ peerDependencies:
+ '@types/node': '>=18'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
+ '@inquirer/search@4.2.1':
+ resolution: {integrity: sha512-xJj8QWKRSrfKoBIITLZK61dD3zwo0Rz11fgDImku30/Oe81zMdIdGgrLY2h6RkJ+KZ/GhNYIRMKnH/62qBTA5g==}
+ engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+ peerDependencies:
+ '@types/node': '>=18'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
+ '@inquirer/select@5.2.1':
+ resolution: {integrity: sha512-FlDndEUww8m7BfukO2nJa25vhD+H5jxxCv4oGioKqzyWz3nPHhhw4LKdYRSlXuAx7DsdWia7iyaBPKKS95Evfw==}
+ engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+ peerDependencies:
+ '@types/node': '>=18'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
+ '@inquirer/type@4.0.7':
+ resolution: {integrity: sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==}
+ engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
+ peerDependencies:
+ '@types/node': '>=18'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
+ '@jridgewell/gen-mapping@0.3.13':
+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+
+ '@jridgewell/remapping@2.3.5':
+ resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
+
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
@@ -883,6 +1182,9 @@ packages:
'@rspress/shared@2.0.12':
resolution: {integrity: sha512-YTzaeMvxQRiMwCt5pk7CwkSBenp8HS+t1E82jFDhLwXPMChk7LHYazPGIuaNAoDMN1axW5EHtMUdZm7wVI8EdQ==}
+ '@sec-ant/readable-stream@0.4.1':
+ resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
+
'@shikijs/core@4.0.2':
resolution: {integrity: sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==}
engines: {node: '>=20'}
@@ -918,9 +1220,36 @@ packages:
'@shikijs/vscode-textmate@10.0.2':
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
+ '@sindresorhus/merge-streams@4.0.0':
+ resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
+ engines: {node: '>=18'}
+
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
+ '@stryker-mutator/api@9.6.1':
+ resolution: {integrity: sha512-g8VNoFWQWbx0pdal3Vt8jVCZW+v3sc3gi94iI0GVtVgUGTqphAjJF6EAruPTx0lqvtonsaAxn5TD36hcG1d6Wg==}
+ engines: {node: '>=20.0.0'}
+
+ '@stryker-mutator/core@9.6.1':
+ resolution: {integrity: sha512-WMgnvf+Wyh/yiruhNZwc8w8DlzmmjXhPjSn5MR8RhAXzlnWji8TQrUYgBUkHk9bEgSaIlB3KZHm37iiU5Q2cLQ==}
+ engines: {node: '>=20.0.0'}
+ hasBin: true
+
+ '@stryker-mutator/instrumenter@9.6.1':
+ resolution: {integrity: sha512-5K8wH4Pthly25c2uKKik4Dfcoeou7sbJdFS6u3QIYHlulgFVDJwtEMWTZGkZfs7IiUEXIDNa0keRACq5jn5AvA==}
+ engines: {node: '>=20.0.0'}
+
+ '@stryker-mutator/util@9.6.1':
+ resolution: {integrity: sha512-Lk/ALVctJjFv1vvwR+CFoKzDCWvsBlq7flDUnmnpuwTrGbm156EdZD1Jjq4o8KdOap0ezUZqQNE9OAI1m2+pUQ==}
+
+ '@stryker-mutator/vitest-runner@9.6.1':
+ resolution: {integrity: sha512-eyUHTCf3Ui+SUn/tpFJwzw6MV391kyBLZk/cDHFUfKFELqKMLbvd7e81axArlApKqO6cOnLfrxlwED+2SRN0ow==}
+ engines: {node: '>=14.18.0'}
+ peerDependencies:
+ '@stryker-mutator/core': 9.6.1
+ vitest: '>=2.0.0'
+
'@swc/helpers@0.5.23':
resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==}
@@ -1304,6 +1633,13 @@ packages:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
+ ajv@8.18.0:
+ resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
+
+ angular-html-parser@10.4.0:
+ resolution: {integrity: sha512-++nLNyZwRfHqFh7akH5Gw/JYizoFlMRz0KRigfwfsLqV8ZqlcVRb1LkPEWdYvEKDnbktknM2J4BXaYUGrQZPww==}
+ engines: {node: '>= 14'}
+
ansi-regex@6.2.2:
resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
engines: {node: '>=12'}
@@ -1326,13 +1662,42 @@ packages:
bail@2.0.2:
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
+ balanced-match@4.0.4:
+ resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
+ engines: {node: 18 || 20 || >=22}
+
+ baseline-browser-mapping@2.11.4:
+ resolution: {integrity: sha512-s4+sLr9mZ/CyqeRritFeYV/Zx73OAtmaHn6kkBS1XRoJn1hrg3xIDUcpicAEX68tkcIN0iBCgti31C8zxtkhsQ==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
body-scroll-lock@4.0.0-beta.0:
resolution: {integrity: sha512-a7tP5+0Mw3YlUJcGAKUqIBkYYGlYxk2fnCasq/FUph1hadxlTRjF+gAcZksxANnaMnALjxEddmSi/H3OR8ugcQ==}
+ brace-expansion@5.0.8:
+ resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==}
+ engines: {node: 20 || >=22}
+
+ browserslist@4.28.7:
+ resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
cac@7.0.0:
resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==}
engines: {node: '>=20.19.0'}
+ call-bind-apply-helpers@1.0.2:
+ 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.30001806:
+ resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==}
+
ccount@2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
@@ -1340,6 +1705,10 @@ packages:
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
engines: {node: '>=18'}
+ chalk@5.6.2:
+ resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
+ engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
+
character-entities-html4@2.1.0:
resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
@@ -1352,10 +1721,17 @@ packages:
character-reference-invalid@2.0.1:
resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
+ chardet@2.2.0:
+ resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==}
+
cli-spinners@3.4.0:
resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==}
engines: {node: '>=18.20'}
+ cli-width@4.1.0:
+ resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
+ engines: {node: '>= 12'}
+
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
@@ -1370,6 +1746,10 @@ packages:
resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==}
engines: {node: '>=18'}
+ commander@14.0.3:
+ resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
+ engines: {node: '>=20'}
+
compute-scroll-into-view@3.1.1:
resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==}
@@ -1383,6 +1763,10 @@ packages:
copy-to-clipboard@3.3.3:
resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==}
+ cross-spawn@7.0.6:
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+ engines: {node: '>= 8'}
+
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
@@ -1405,6 +1789,9 @@ packages:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
+ des.js@1.1.0:
+ resolution: {integrity: sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==}
+
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
@@ -1412,6 +1799,9 @@ packages:
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==}
+
dts-resolver@3.0.0:
resolution: {integrity: sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q==}
engines: {node: ^22.18.0 || >=24.0.0}
@@ -1421,6 +1811,13 @@ packages:
oxc-resolver:
optional: true
+ dunder-proto@1.0.1:
+ resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+ engines: {node: '>= 0.4'}
+
+ electron-to-chromium@1.5.396:
+ resolution: {integrity: sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==}
+
emoji-regex@10.6.0:
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
@@ -1432,15 +1829,31 @@ packages:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'}
+ es-define-property@1.0.1:
+ resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
+ engines: {node: '>= 0.4'}
+
+ es-errors@1.3.0:
+ resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+ engines: {node: '>= 0.4'}
+
es-module-lexer@2.0.0:
resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==}
+ es-object-atoms@1.1.2:
+ resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==}
+ engines: {node: '>= 0.4'}
+
esast-util-from-estree@2.0.0:
resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==}
esast-util-from-js@2.0.1:
resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==}
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
escape-string-regexp@5.0.0:
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
engines: {node: '>=12'}
@@ -1474,6 +1887,10 @@ packages:
resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==}
engines: {node: '>=18.0.0'}
+ execa@9.6.1:
+ resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==}
+ engines: {node: ^18.19.0 || >=20.5.0}
+
expect-type@1.3.0:
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
engines: {node: '>=12.0.0'}
@@ -1490,6 +1907,21 @@ packages:
resolution: {integrity: sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg==}
engines: {node: '>=12.17.0'}
+ fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+ fast-string-truncated-width@3.0.3:
+ resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==}
+
+ fast-string-width@3.0.2:
+ resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==}
+
+ fast-uri@3.1.4:
+ resolution: {integrity: sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==}
+
+ fast-wrap-ansi@0.2.2:
+ resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==}
+
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
@@ -1499,6 +1931,10 @@ packages:
picomatch:
optional: true
+ figures@6.1.0:
+ resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==}
+ engines: {node: '>=18'}
+
flexsearch@0.8.212:
resolution: {integrity: sha512-wSyJr1GUWoOOIISRu+X2IXiOcVfg9qqBRyCPRUdLMIGJqPzMo+jMRlvE83t14v1j0dRMEaBbER/adQjp6Du2pw==}
@@ -1507,18 +1943,49 @@ packages:
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
+ 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-east-asian-width@1.5.0:
resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==}
engines: {node: '>=18'}
+ get-intrinsic@1.3.0:
+ resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
+ engines: {node: '>= 0.4'}
+
+ get-proto@1.0.1:
+ resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
+ engines: {node: '>= 0.4'}
+
+ get-stream@9.0.1:
+ resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==}
+ engines: {node: '>=18'}
+
get-tsconfig@5.0.0-beta.5:
resolution: {integrity: sha512-/6gFNr0N04nob252sTQxyFLi3eKFRqIg1I87YcqAMT1i6SQrSF6KujUEQrtrjMV0H/eejTCltLdDSTEMzHbnsQ==}
engines: {node: '>=20.20.0'}
+ gopd@1.2.0:
+ resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
+ engines: {node: '>= 0.4'}
+
has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
+ has-symbols@1.1.0:
+ resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
+ engines: {node: '>= 0.4'}
+
+ hasown@2.0.4:
+ resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
+ engines: {node: '>= 0.4'}
+
hast-util-from-parse5@8.0.3:
resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==}
@@ -1571,6 +2038,14 @@ packages:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'}
+ human-signals@8.0.1:
+ resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==}
+ engines: {node: '>=18.18.0'}
+
+ iconv-lite@0.7.3:
+ resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==}
+ engines: {node: '>=0.10.0'}
+
ignore@7.0.6:
resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==}
engines: {node: '>= 4'}
@@ -1579,6 +2054,9 @@ packages:
resolution: {integrity: sha512-NkJQA7oZ4YHQhd2+H3BoRFKF3d/XNsiKpHZCQEMH9pDX27hQQLsTyOocyRgaIVtf8gHX3Nt3LPkR4e5EdtPAGQ==}
engines: {node: ^22.18.0 || >=24.0.0}
+ inherits@2.0.4:
+ resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+
inline-style-parser@0.2.7:
resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==}
@@ -1602,6 +2080,17 @@ packages:
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
engines: {node: '>=12'}
+ is-stream@4.0.1:
+ resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==}
+ engines: {node: '>=18'}
+
+ is-unicode-supported@2.1.0:
+ resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
+ engines: {node: '>=18'}
+
+ isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
istanbul-lib-coverage@3.2.2:
resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
engines: {node: '>=8'}
@@ -1614,9 +2103,31 @@ packages:
resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
engines: {node: '>=8'}
+ js-md4@0.3.2:
+ resolution: {integrity: sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==}
+
js-tokens@10.0.0:
resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==}
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ jsesc@3.1.0:
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ json-rpc-2.0@1.7.1:
+ resolution: {integrity: sha512-JqZjhjAanbpkXIzFE7u8mE/iFblawwlXtONaCvRqI+pyABVz7B4M1EUNpyVW+dZjqgQ2L5HFmZCmOCgUKm00hg==}
+
+ 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
+
lightningcss-android-arm64@1.32.0:
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
engines: {node: '>= 12.0.0'}
@@ -1691,9 +2202,15 @@ packages:
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
engines: {node: '>= 12.0.0'}
+ lodash.groupby@4.6.0:
+ resolution: {integrity: sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==}
+
longest-streak@3.1.0:
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
+ 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==}
@@ -1711,6 +2228,10 @@ packages:
markdown-table@3.0.4:
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
+ math-intrinsics@1.1.0:
+ resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+ engines: {node: '>= 0.4'}
+
mdast-util-find-and-replace@3.0.2:
resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==}
@@ -1896,9 +2417,33 @@ packages:
micromark@4.0.2:
resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==}
+ minimalistic-assert@1.0.1:
+ resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
+
+ minimatch@10.2.5:
+ resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
+ engines: {node: 18 || 20 || >=22}
+
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+ mutation-server-protocol@0.4.1:
+ resolution: {integrity: sha512-SBGK0j8hLDne7bktgThKI8kGvGTx3rY3LAeQTmOKZ5bVnL/7TorLMvcVF7dIPJCu5RNUWhkkuF53kurygYVt3g==}
+ engines: {node: '>=18'}
+
+ mutation-testing-elements@3.7.3:
+ resolution: {integrity: sha512-SMeIPxngJpfjfNYctFpYQQtlBlZaVO0aoB3FKdwrI8Ee/2bkyUuCZzAOCLv1U9fnmfA37dPFq0Owduoxs2XgGQ==}
+
+ mutation-testing-metrics@3.7.3:
+ resolution: {integrity: sha512-B8QrP0ZomErzTPNlhrzKWPNBln+3afwBZPHv0Q7N8wZZTYxMptzb/Gdm3ExXVmioVYrtZAtsDs7W/T/b2AixOQ==}
+
+ mutation-testing-report-schema@3.7.3:
+ resolution: {integrity: sha512-BHm3MYq+ckO+t5CtlG8zpqxc75rdJCkxVlE+fGuGJM3F7tNCQ/OW2N+TQVHN3BHsYa84+BFc6g3AwDYkUsw2MA==}
+
+ mute-stream@3.0.0:
+ resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==}
+ engines: {node: ^20.17.0 || >=22.9.0}
+
nano-spawn@2.1.0:
resolution: {integrity: sha512-yTW+2okrElHiH4fsiz/+/zc0EDo9BDDoC3iKk8dpv1GeRc9nUWzUZHx6TofMWErchhUQR8hY9/Eu1Uja9x1nqA==}
engines: {node: '>=20.17'}
@@ -1908,9 +2453,21 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
+ node-releases@2.0.51:
+ resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==}
+ engines: {node: '>=18'}
+
+ npm-run-path@6.0.0:
+ resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==}
+ engines: {node: '>=18'}
+
nprogress@0.2.0:
resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==}
+ object-inspect@1.13.4:
+ resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
+ engines: {node: '>= 0.4'}
+
obug@2.1.1:
resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
@@ -1956,6 +2513,14 @@ packages:
parse5@7.3.0:
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
+ path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+
+ path-key@4.0.0:
+ resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
+ engines: {node: '>=12'}
+
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
@@ -1978,6 +2543,10 @@ packages:
resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==}
engines: {node: '>=18'}
+ 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==}
@@ -1988,6 +2557,10 @@ packages:
pure-rand@8.4.2:
resolution: {integrity: sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==}
+ qs@6.15.1:
+ resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==}
+ engines: {node: '>=0.6'}
+
quansync@1.0.0:
resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==}
@@ -2102,6 +2675,10 @@ packages:
remark-stringify@11.0.0:
resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
+ require-from-string@2.0.2:
+ resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
+ engines: {node: '>=0.10.0'}
+
resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
@@ -2142,12 +2719,22 @@ packages:
'@rsbuild/core':
optional: true
+ rxjs@7.8.2:
+ resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
+
+ safer-buffer@2.1.2:
+ resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+
scheduler@0.27.0:
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
scroll-into-view-if-needed@3.1.0:
resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==}
+ semver@6.3.1:
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
+ hasBin: true
+
semver@7.7.4:
resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
engines: {node: '>=10'}
@@ -2161,13 +2748,41 @@ packages:
set-cookie-parser@2.7.2:
resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
+ shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+
+ shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+
shiki@4.0.2:
resolution: {integrity: sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==}
engines: {node: '>=20'}
+ 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.1:
+ resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==}
+ engines: {node: '>= 0.4'}
+
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
+ signal-exit@4.1.0:
+ resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
+ engines: {node: '>=14'}
+
skillgym@0.9.1:
resolution: {integrity: sha512-vEFioLv7JQHWhPW7Cs2u7WIGl7GhE4D3K+IOAUEnuIewIdjAsmjnZXK0BS5oyY9TvKJECZh6a0XY46+GNaptpw==}
engines: {node: '>=22.18.0'}
@@ -2201,6 +2816,10 @@ packages:
resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==}
engines: {node: '>=12'}
+ strip-final-newline@4.0.0:
+ resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==}
+ engines: {node: '>=18'}
+
style-to-js@1.1.21:
resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==}
@@ -2288,6 +2907,18 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+ tunnel@0.0.6:
+ resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==}
+ engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'}
+
+ typed-inject@5.0.0:
+ resolution: {integrity: sha512-0Ql2ORqBORLMdAW89TQKZsb1PQkFGImFfVmncXWe7a+AA3+7dh7Se9exxZowH4kbnlvKEFkMxUYdHUpjYWFJaA==}
+ engines: {node: '>=18'}
+
+ typed-rest-client@2.3.1:
+ resolution: {integrity: sha512-k4kX5Up6qA68D0Cby2AK+6+vM5k3qTxe+/3FqhnHRExjY5cfbOnzjQZbP/LXleF8hVoDvDqxlgk9KK83HoBZlQ==}
+ engines: {node: '>= 16.0.0'}
+
typescript@6.0.3:
resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==}
engines: {node: '>=14.17'}
@@ -2301,6 +2932,9 @@ packages:
unconfig-core@7.5.0:
resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==}
+ underscore@1.13.8:
+ resolution: {integrity: sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==}
+
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
@@ -2311,6 +2945,10 @@ packages:
unhead@2.1.15:
resolution: {integrity: sha512-MCt5T90mCWyr3Z6pUCdM9lVRXoMoVBlL7z7U4CYVIiaDiuzad/UCfLuMqz5MeNmpZUgoBCQnrucJimU7EZR+XA==}
+ unicorn-magic@0.3.0:
+ resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
+ engines: {node: '>=18'}
+
unified@11.0.5:
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
@@ -2338,6 +2976,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-location@5.0.3:
resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
@@ -2431,9 +3075,17 @@ packages:
jsdom:
optional: true
+ weapon-regex@1.3.6:
+ resolution: {integrity: sha512-wsf1m1jmMrso5nhwVFJJHSubEBf3+pereGd7+nBKtYJ18KoB/PWJOHS3WRkwS04VrOU0iJr2bZU+l1QaTJ+9nA==}
+
web-namespaces@2.0.1:
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
+ which@2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+
why-is-node-running@2.3.0:
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
engines: {node: '>=8'}
@@ -2451,41 +3103,264 @@ packages:
utf-8-validate:
optional: true
+ yallist@3.1.1:
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+
yaml@2.9.0:
resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==}
engines: {node: '>= 14.6'}
hasBin: true
- yuku-ast@0.1.7:
- resolution: {integrity: sha512-2RiMEWv500TixY5rJy6OZd4fSy9WYZKWh6gGbIJ7y7vAGcuCugWOWwOLGaQcRZrXcPUfqtLtvpaJ3SdXtWlhKA==}
+ yoctocolors@2.2.0:
+ resolution: {integrity: sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==}
+ engines: {node: '>=18'}
+
+ yuku-ast@0.1.7:
+ resolution: {integrity: sha512-2RiMEWv500TixY5rJy6OZd4fSy9WYZKWh6gGbIJ7y7vAGcuCugWOWwOLGaQcRZrXcPUfqtLtvpaJ3SdXtWlhKA==}
+
+ yuku-codegen@0.5.44:
+ resolution: {integrity: sha512-0rhtgWGz+bR3Pe7xqJ5E4VqxI1vqNtkeJRkeXM0Qd3tgldYClQipxa9bRZyuNOOfk0Ri02scrjnkoAHM16/f2g==}
+
+ yuku-parser@0.5.44:
+ resolution: {integrity: sha512-mAhpQZ/bXjxZmKiGUqEWskC9mZTcTBv6/fdzVdzdjM6XuD1DP3IavdLVWdM39L9ewK9vS9OtJmaKNeWgRzpy0w==}
+
+ zod@4.3.6:
+ resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
+
+ zwitch@2.0.4:
+ resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
+
+snapshots:
+
+ '@babel/code-frame@7.29.7':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.29.7
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/compat-data@7.29.7': {}
+
+ '@babel/core@7.29.7':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/generator': 7.29.7
+ '@babel/helper-compilation-targets': 7.29.7
+ '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7)
+ '@babel/helpers': 7.29.7
+ '@babel/parser': 7.29.7
+ '@babel/template': 7.29.7
+ '@babel/traverse': 7.29.7
+ '@babel/types': 7.29.7
+ '@jridgewell/remapping': 2.3.5
+ 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.29.7':
+ dependencies:
+ '@babel/parser': 7.29.7
+ '@babel/types': 7.29.7
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+ jsesc: 3.1.0
+
+ '@babel/helper-annotate-as-pure@7.29.7':
+ dependencies:
+ '@babel/types': 7.29.7
+
+ '@babel/helper-compilation-targets@7.29.7':
+ dependencies:
+ '@babel/compat-data': 7.29.7
+ '@babel/helper-validator-option': 7.29.7
+ browserslist: 4.28.7
+ lru-cache: 5.1.1
+ semver: 6.3.1
+
+ '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7)':
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/helper-annotate-as-pure': 7.29.7
+ '@babel/helper-member-expression-to-functions': 7.29.7
+ '@babel/helper-optimise-call-expression': 7.29.7
+ '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7)
+ '@babel/helper-skip-transparent-expression-wrappers': 7.29.7
+ '@babel/traverse': 7.29.7
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-globals@7.29.7': {}
+
+ '@babel/helper-member-expression-to-functions@7.29.7':
+ dependencies:
+ '@babel/traverse': 7.29.7
+ '@babel/types': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-imports@7.29.7':
+ dependencies:
+ '@babel/traverse': 7.29.7
+ '@babel/types': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)':
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/helper-module-imports': 7.29.7
+ '@babel/helper-validator-identifier': 7.29.7
+ '@babel/traverse': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-optimise-call-expression@7.29.7':
+ dependencies:
+ '@babel/types': 7.29.7
+
+ '@babel/helper-plugin-utils@7.29.7': {}
+
+ '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7)':
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/helper-member-expression-to-functions': 7.29.7
+ '@babel/helper-optimise-call-expression': 7.29.7
+ '@babel/traverse': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-skip-transparent-expression-wrappers@7.29.7':
+ dependencies:
+ '@babel/traverse': 7.29.7
+ '@babel/types': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-string-parser@7.27.1': {}
+
+ '@babel/helper-string-parser@7.29.7': {}
+
+ '@babel/helper-validator-identifier@7.28.5': {}
+
+ '@babel/helper-validator-identifier@7.29.7': {}
+
+ '@babel/helper-validator-option@7.29.7': {}
+
+ '@babel/helpers@7.29.7':
+ dependencies:
+ '@babel/template': 7.29.7
+ '@babel/types': 7.29.7
+
+ '@babel/parser@7.29.3':
+ dependencies:
+ '@babel/types': 7.29.0
+
+ '@babel/parser@7.29.7':
+ dependencies:
+ '@babel/types': 7.29.7
+
+ '@babel/plugin-proposal-decorators@7.29.7(@babel/core@7.29.7)':
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7)
+ '@babel/helper-plugin-utils': 7.29.7
+ '@babel/plugin-syntax-decorators': 7.29.7(@babel/core@7.29.7)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-syntax-decorators@7.29.7(@babel/core@7.29.7)':
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/helper-plugin-utils': 7.29.7
- yuku-codegen@0.5.44:
- resolution: {integrity: sha512-0rhtgWGz+bR3Pe7xqJ5E4VqxI1vqNtkeJRkeXM0Qd3tgldYClQipxa9bRZyuNOOfk0Ri02scrjnkoAHM16/f2g==}
+ '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)':
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/helper-plugin-utils': 7.29.7
- yuku-parser@0.5.44:
- resolution: {integrity: sha512-mAhpQZ/bXjxZmKiGUqEWskC9mZTcTBv6/fdzVdzdjM6XuD1DP3IavdLVWdM39L9ewK9vS9OtJmaKNeWgRzpy0w==}
+ '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)':
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/helper-plugin-utils': 7.29.7
- zod@4.3.6:
- resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
+ '@babel/plugin-transform-destructuring@7.29.7(@babel/core@7.29.7)':
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/helper-plugin-utils': 7.29.7
+ '@babel/traverse': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
- zwitch@2.0.4:
- resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
+ '@babel/plugin-transform-explicit-resource-management@7.29.7(@babel/core@7.29.7)':
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/helper-plugin-utils': 7.29.7
+ '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7)
+ transitivePeerDependencies:
+ - supports-color
-snapshots:
+ '@babel/plugin-transform-modules-commonjs@7.29.7(@babel/core@7.29.7)':
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7)
+ '@babel/helper-plugin-utils': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
- '@babel/helper-string-parser@7.27.1': {}
+ '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7)':
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/helper-annotate-as-pure': 7.29.7
+ '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7)
+ '@babel/helper-plugin-utils': 7.29.7
+ '@babel/helper-skip-transparent-expression-wrappers': 7.29.7
+ '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7)
+ transitivePeerDependencies:
+ - supports-color
- '@babel/helper-validator-identifier@7.28.5': {}
+ '@babel/preset-typescript@7.28.5(@babel/core@7.29.7)':
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/helper-plugin-utils': 7.29.7
+ '@babel/helper-validator-option': 7.29.7
+ '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7)
+ '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7)
+ '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7)
+ transitivePeerDependencies:
+ - supports-color
- '@babel/parser@7.29.3':
+ '@babel/template@7.29.7':
dependencies:
- '@babel/types': 7.29.0
+ '@babel/code-frame': 7.29.7
+ '@babel/parser': 7.29.7
+ '@babel/types': 7.29.7
+
+ '@babel/traverse@7.29.7':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/generator': 7.29.7
+ '@babel/helper-globals': 7.29.7
+ '@babel/parser': 7.29.7
+ '@babel/template': 7.29.7
+ '@babel/types': 7.29.7
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
'@babel/types@7.29.0':
dependencies:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
+ '@babel/types@7.29.7':
+ dependencies:
+ '@babel/helper-string-parser': 7.29.7
+ '@babel/helper-validator-identifier': 7.29.7
+
'@bcoe/v8-coverage@1.0.2': {}
'@callstack/rspress-preset@0.6.6(@rsbuild/core@2.0.11)(@rspress/core@2.0.12(@rspack/core@2.0.6(@swc/helpers@0.5.23))(@types/mdast@4.0.4)(@types/react@19.2.13)(micromark-util-types@2.0.2)(micromark@4.0.2))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
@@ -2574,6 +3449,135 @@ snapshots:
'@fallow-cli/win32-x64-msvc@2.95.0':
optional: true
+ '@inquirer/ansi@2.0.7': {}
+
+ '@inquirer/checkbox@5.2.1(@types/node@22.19.21)':
+ dependencies:
+ '@inquirer/ansi': 2.0.7
+ '@inquirer/core': 11.2.1(@types/node@22.19.21)
+ '@inquirer/figures': 2.0.7
+ '@inquirer/type': 4.0.7(@types/node@22.19.21)
+ optionalDependencies:
+ '@types/node': 22.19.21
+
+ '@inquirer/confirm@6.1.1(@types/node@22.19.21)':
+ dependencies:
+ '@inquirer/core': 11.2.1(@types/node@22.19.21)
+ '@inquirer/type': 4.0.7(@types/node@22.19.21)
+ optionalDependencies:
+ '@types/node': 22.19.21
+
+ '@inquirer/core@11.2.1(@types/node@22.19.21)':
+ dependencies:
+ '@inquirer/ansi': 2.0.7
+ '@inquirer/figures': 2.0.7
+ '@inquirer/type': 4.0.7(@types/node@22.19.21)
+ cli-width: 4.1.0
+ fast-wrap-ansi: 0.2.2
+ mute-stream: 3.0.0
+ signal-exit: 4.1.0
+ optionalDependencies:
+ '@types/node': 22.19.21
+
+ '@inquirer/editor@5.2.2(@types/node@22.19.21)':
+ dependencies:
+ '@inquirer/core': 11.2.1(@types/node@22.19.21)
+ '@inquirer/external-editor': 3.0.3(@types/node@22.19.21)
+ '@inquirer/type': 4.0.7(@types/node@22.19.21)
+ optionalDependencies:
+ '@types/node': 22.19.21
+
+ '@inquirer/expand@5.1.1(@types/node@22.19.21)':
+ dependencies:
+ '@inquirer/core': 11.2.1(@types/node@22.19.21)
+ '@inquirer/type': 4.0.7(@types/node@22.19.21)
+ optionalDependencies:
+ '@types/node': 22.19.21
+
+ '@inquirer/external-editor@3.0.3(@types/node@22.19.21)':
+ dependencies:
+ chardet: 2.2.0
+ iconv-lite: 0.7.3
+ optionalDependencies:
+ '@types/node': 22.19.21
+
+ '@inquirer/figures@2.0.7': {}
+
+ '@inquirer/input@5.1.2(@types/node@22.19.21)':
+ dependencies:
+ '@inquirer/core': 11.2.1(@types/node@22.19.21)
+ '@inquirer/type': 4.0.7(@types/node@22.19.21)
+ optionalDependencies:
+ '@types/node': 22.19.21
+
+ '@inquirer/number@4.1.1(@types/node@22.19.21)':
+ dependencies:
+ '@inquirer/core': 11.2.1(@types/node@22.19.21)
+ '@inquirer/type': 4.0.7(@types/node@22.19.21)
+ optionalDependencies:
+ '@types/node': 22.19.21
+
+ '@inquirer/password@5.1.1(@types/node@22.19.21)':
+ dependencies:
+ '@inquirer/ansi': 2.0.7
+ '@inquirer/core': 11.2.1(@types/node@22.19.21)
+ '@inquirer/type': 4.0.7(@types/node@22.19.21)
+ optionalDependencies:
+ '@types/node': 22.19.21
+
+ '@inquirer/prompts@8.5.2(@types/node@22.19.21)':
+ dependencies:
+ '@inquirer/checkbox': 5.2.1(@types/node@22.19.21)
+ '@inquirer/confirm': 6.1.1(@types/node@22.19.21)
+ '@inquirer/editor': 5.2.2(@types/node@22.19.21)
+ '@inquirer/expand': 5.1.1(@types/node@22.19.21)
+ '@inquirer/input': 5.1.2(@types/node@22.19.21)
+ '@inquirer/number': 4.1.1(@types/node@22.19.21)
+ '@inquirer/password': 5.1.1(@types/node@22.19.21)
+ '@inquirer/rawlist': 5.3.1(@types/node@22.19.21)
+ '@inquirer/search': 4.2.1(@types/node@22.19.21)
+ '@inquirer/select': 5.2.1(@types/node@22.19.21)
+ optionalDependencies:
+ '@types/node': 22.19.21
+
+ '@inquirer/rawlist@5.3.1(@types/node@22.19.21)':
+ dependencies:
+ '@inquirer/core': 11.2.1(@types/node@22.19.21)
+ '@inquirer/type': 4.0.7(@types/node@22.19.21)
+ optionalDependencies:
+ '@types/node': 22.19.21
+
+ '@inquirer/search@4.2.1(@types/node@22.19.21)':
+ dependencies:
+ '@inquirer/core': 11.2.1(@types/node@22.19.21)
+ '@inquirer/figures': 2.0.7
+ '@inquirer/type': 4.0.7(@types/node@22.19.21)
+ optionalDependencies:
+ '@types/node': 22.19.21
+
+ '@inquirer/select@5.2.1(@types/node@22.19.21)':
+ dependencies:
+ '@inquirer/ansi': 2.0.7
+ '@inquirer/core': 11.2.1(@types/node@22.19.21)
+ '@inquirer/figures': 2.0.7
+ '@inquirer/type': 4.0.7(@types/node@22.19.21)
+ optionalDependencies:
+ '@types/node': 22.19.21
+
+ '@inquirer/type@4.0.7(@types/node@22.19.21)':
+ optionalDependencies:
+ '@types/node': 22.19.21
+
+ '@jridgewell/gen-mapping@0.3.13':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/remapping@2.3.5':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
@@ -3077,6 +4081,8 @@ snapshots:
- '@module-federation/runtime-tools'
- core-js
+ '@sec-ant/readable-stream@0.4.1': {}
+
'@shikijs/core@4.0.2':
dependencies:
'@shikijs/primitive': 4.0.2
@@ -3126,8 +4132,77 @@ snapshots:
'@shikijs/vscode-textmate@10.0.2': {}
+ '@sindresorhus/merge-streams@4.0.0': {}
+
'@standard-schema/spec@1.1.0': {}
+ '@stryker-mutator/api@9.6.1':
+ dependencies:
+ mutation-testing-metrics: 3.7.3
+ mutation-testing-report-schema: 3.7.3
+ tslib: 2.8.1
+ typed-inject: 5.0.0
+
+ '@stryker-mutator/core@9.6.1(@types/node@22.19.21)':
+ dependencies:
+ '@inquirer/prompts': 8.5.2(@types/node@22.19.21)
+ '@stryker-mutator/api': 9.6.1
+ '@stryker-mutator/instrumenter': 9.6.1
+ '@stryker-mutator/util': 9.6.1
+ ajv: 8.18.0
+ chalk: 5.6.2
+ commander: 14.0.3
+ diff-match-patch: 1.0.5
+ emoji-regex: 10.6.0
+ execa: 9.6.1
+ json-rpc-2.0: 1.7.1
+ lodash.groupby: 4.6.0
+ minimatch: 10.2.5
+ mutation-server-protocol: 0.4.1
+ mutation-testing-elements: 3.7.3
+ mutation-testing-metrics: 3.7.3
+ mutation-testing-report-schema: 3.7.3
+ npm-run-path: 6.0.0
+ progress: 2.0.3
+ rxjs: 7.8.2
+ semver: 7.8.5
+ source-map: 0.7.6
+ tree-kill: 1.2.2
+ tslib: 2.8.1
+ typed-inject: 5.0.0
+ typed-rest-client: 2.3.1
+ transitivePeerDependencies:
+ - '@types/node'
+ - supports-color
+
+ '@stryker-mutator/instrumenter@9.6.1':
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/generator': 7.29.7
+ '@babel/parser': 7.29.3
+ '@babel/plugin-proposal-decorators': 7.29.7(@babel/core@7.29.7)
+ '@babel/plugin-transform-explicit-resource-management': 7.29.7(@babel/core@7.29.7)
+ '@babel/preset-typescript': 7.28.5(@babel/core@7.29.7)
+ '@stryker-mutator/api': 9.6.1
+ '@stryker-mutator/util': 9.6.1
+ angular-html-parser: 10.4.0
+ semver: 7.7.4
+ tslib: 2.8.1
+ weapon-regex: 1.3.6
+ transitivePeerDependencies:
+ - supports-color
+
+ '@stryker-mutator/util@9.6.1': {}
+
+ '@stryker-mutator/vitest-runner@9.6.1(@stryker-mutator/core@9.6.1(@types/node@22.19.21))(vitest@4.1.8)':
+ dependencies:
+ '@stryker-mutator/api': 9.6.1
+ '@stryker-mutator/core': 9.6.1(@types/node@22.19.21)
+ '@stryker-mutator/util': 9.6.1
+ semver: 7.8.5
+ tslib: 2.8.1
+ vitest: 4.1.8(@types/node@22.19.21)(@vitest/coverage-v8@4.1.8)(vite@8.0.16(@types/node@22.19.21)(yaml@2.9.0))
+
'@swc/helpers@0.5.23':
dependencies:
tslib: 2.8.1
@@ -3384,6 +4459,15 @@ snapshots:
agent-base@7.1.4: {}
+ ajv@8.18.0:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-uri: 3.1.4
+ json-schema-traverse: 1.0.0
+ require-from-string: 2.0.2
+
+ angular-html-parser@10.4.0: {}
+
ansi-regex@6.2.2: {}
ansis@4.3.1: {}
@@ -3400,14 +4484,44 @@ snapshots:
bail@2.0.2: {}
+ balanced-match@4.0.4: {}
+
+ baseline-browser-mapping@2.11.4: {}
+
body-scroll-lock@4.0.0-beta.0: {}
+ brace-expansion@5.0.8:
+ dependencies:
+ balanced-match: 4.0.4
+
+ browserslist@4.28.7:
+ dependencies:
+ baseline-browser-mapping: 2.11.4
+ caniuse-lite: 1.0.30001806
+ electron-to-chromium: 1.5.396
+ node-releases: 2.0.51
+ update-browserslist-db: 1.2.3(browserslist@4.28.7)
+
cac@7.0.0: {}
+ call-bind-apply-helpers@1.0.2:
+ dependencies:
+ 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.30001806: {}
+
ccount@2.0.1: {}
chai@6.2.2: {}
+ chalk@5.6.2: {}
+
character-entities-html4@2.1.0: {}
character-entities-legacy@3.0.0: {}
@@ -3416,8 +4530,12 @@ snapshots:
character-reference-invalid@2.0.1: {}
+ chardet@2.2.0: {}
+
cli-spinners@3.4.0: {}
+ cli-width@4.1.0: {}
+
clsx@2.1.1: {}
collapse-white-space@2.1.0: {}
@@ -3426,6 +4544,8 @@ snapshots:
commander@13.1.0: {}
+ commander@14.0.3: {}
+
compute-scroll-into-view@3.1.1: {}
convert-source-map@2.0.0: {}
@@ -3436,6 +4556,12 @@ snapshots:
dependencies:
toggle-selection: 1.0.6
+ cross-spawn@7.0.6:
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+
csstype@3.2.3: {}
debug@4.4.3:
@@ -3450,22 +4576,45 @@ snapshots:
dequal@2.0.3: {}
+ des.js@1.1.0:
+ dependencies:
+ inherits: 2.0.4
+ minimalistic-assert: 1.0.1
+
detect-libc@2.1.2: {}
devlop@1.1.0:
dependencies:
dequal: 2.0.3
+ diff-match-patch@1.0.5: {}
+
dts-resolver@3.0.0: {}
+ dunder-proto@1.0.1:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
+ electron-to-chromium@1.5.396: {}
+
emoji-regex@10.6.0: {}
empathic@2.0.1: {}
entities@6.0.1: {}
+ es-define-property@1.0.1: {}
+
+ es-errors@1.3.0: {}
+
es-module-lexer@2.0.0: {}
+ es-object-atoms@1.1.2:
+ dependencies:
+ es-errors: 1.3.0
+
esast-util-from-estree@2.0.0:
dependencies:
'@types/estree-jsx': 1.0.5
@@ -3480,6 +4629,8 @@ snapshots:
esast-util-from-estree: 2.0.0
vfile-message: 4.0.3
+ escalade@3.2.0: {}
+
escape-string-regexp@5.0.0: {}
estree-util-attach-comments@3.0.0:
@@ -3521,6 +4672,21 @@ snapshots:
eventsource-parser@3.1.0: {}
+ execa@9.6.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.2.0
+
expect-type@1.3.0: {}
extend@3.0.2: {}
@@ -3542,23 +4708,76 @@ snapshots:
dependencies:
pure-rand: 8.4.2
+ fast-deep-equal@3.1.3: {}
+
+ fast-string-truncated-width@3.0.3: {}
+
+ fast-string-width@3.0.2:
+ dependencies:
+ fast-string-truncated-width: 3.0.3
+
+ fast-uri@3.1.4: {}
+
+ fast-wrap-ansi@0.2.2:
+ dependencies:
+ fast-string-width: 3.0.2
+
fdir@6.5.0(picomatch@4.0.4):
optionalDependencies:
picomatch: 4.0.4
+ figures@6.1.0:
+ dependencies:
+ is-unicode-supported: 2.1.0
+
flexsearch@0.8.212: {}
fsevents@2.3.3:
optional: true
+ function-bind@1.1.2: {}
+
+ gensync@1.0.0-beta.2: {}
+
get-east-asian-width@1.5.0: {}
+ get-intrinsic@1.3.0:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.2
+ function-bind: 1.1.2
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ has-symbols: 1.1.0
+ hasown: 2.0.4
+ math-intrinsics: 1.1.0
+
+ get-proto@1.0.1:
+ dependencies:
+ dunder-proto: 1.0.1
+ es-object-atoms: 1.1.2
+
+ get-stream@9.0.1:
+ dependencies:
+ '@sec-ant/readable-stream': 0.4.1
+ is-stream: 4.0.1
+
get-tsconfig@5.0.0-beta.5:
dependencies:
resolve-pkg-maps: 1.0.0
+ gopd@1.2.0: {}
+
has-flag@4.0.0: {}
+ has-symbols@1.1.0: {}
+
+ hasown@2.0.4:
+ dependencies:
+ function-bind: 1.1.2
+
hast-util-from-parse5@8.0.3:
dependencies:
'@types/hast': 3.0.4
@@ -3694,10 +4913,18 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ human-signals@8.0.1: {}
+
+ iconv-lite@0.7.3:
+ dependencies:
+ safer-buffer: 2.1.2
+
ignore@7.0.6: {}
import-without-cache@0.4.0: {}
+ inherits@2.0.4: {}
+
inline-style-parser@0.2.7: {}
is-absolute-url@4.0.1: {}
@@ -3715,6 +4942,12 @@ snapshots:
is-plain-obj@4.1.0: {}
+ is-stream@4.0.1: {}
+
+ is-unicode-supported@2.1.0: {}
+
+ isexe@2.0.0: {}
+
istanbul-lib-coverage@3.2.2: {}
istanbul-lib-report@3.0.1:
@@ -3728,8 +4961,20 @@ snapshots:
html-escaper: 2.0.2
istanbul-lib-report: 3.0.1
+ js-md4@0.3.2: {}
+
js-tokens@10.0.0: {}
+ js-tokens@4.0.0: {}
+
+ jsesc@3.1.0: {}
+
+ json-rpc-2.0@1.7.1: {}
+
+ json-schema-traverse@1.0.0: {}
+
+ json5@2.2.3: {}
+
lightningcss-android-arm64@1.32.0:
optional: true
@@ -3779,8 +5024,14 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.32.0
lightningcss-win32-x64-msvc: 1.32.0
+ lodash.groupby@4.6.0: {}
+
longest-streak@3.1.0: {}
+ lru-cache@5.1.1:
+ dependencies:
+ yallist: 3.1.1
+
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -3793,12 +5044,14 @@ snapshots:
make-dir@4.0.0:
dependencies:
- semver: 7.7.4
+ semver: 7.8.5
markdown-extensions@2.0.0: {}
markdown-table@3.0.4: {}
+ math-intrinsics@1.1.0: {}
+
mdast-util-find-and-replace@3.0.2:
dependencies:
'@types/mdast': 4.0.4
@@ -4260,14 +5513,43 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ minimalistic-assert@1.0.1: {}
+
+ minimatch@10.2.5:
+ dependencies:
+ brace-expansion: 5.0.8
+
ms@2.1.3: {}
+ mutation-server-protocol@0.4.1:
+ dependencies:
+ zod: 4.3.6
+
+ mutation-testing-elements@3.7.3: {}
+
+ mutation-testing-metrics@3.7.3:
+ dependencies:
+ mutation-testing-report-schema: 3.7.3
+
+ mutation-testing-report-schema@3.7.3: {}
+
+ mute-stream@3.0.0: {}
+
nano-spawn@2.1.0: {}
nanoid@3.3.12: {}
+ node-releases@2.0.51: {}
+
+ npm-run-path@6.0.0:
+ dependencies:
+ path-key: 4.0.0
+ unicorn-magic: 0.3.0
+
nprogress@0.2.0: {}
+ object-inspect@1.13.4: {}
+
obug@2.1.1: {}
obug@2.1.3: {}
@@ -4367,6 +5649,10 @@ snapshots:
dependencies:
entities: 6.0.1
+ path-key@3.1.1: {}
+
+ path-key@4.0.0: {}
+
pathe@2.0.3: {}
picocolors@1.1.1: {}
@@ -4385,12 +5671,18 @@ snapshots:
dependencies:
parse-ms: 4.0.0
+ progress@2.0.3: {}
+
property-information@7.1.0: {}
proxy-from-env@2.1.0: {}
pure-rand@8.4.2: {}
+ qs@6.15.1:
+ dependencies:
+ side-channel: 1.1.1
+
quansync@1.0.0: {}
react-dom@19.2.7(react@19.2.7):
@@ -4551,6 +5843,8 @@ snapshots:
mdast-util-to-markdown: 2.1.2
unified: 11.0.5
+ require-from-string@2.0.2: {}
+
resolve-pkg-maps@1.0.0: {}
rolldown-plugin-dts@0.27.1(rolldown@1.1.4)(typescript@7.0.2):
@@ -4613,18 +5907,32 @@ snapshots:
optionalDependencies:
'@rsbuild/core': 2.0.11
+ rxjs@7.8.2:
+ dependencies:
+ tslib: 2.8.1
+
+ safer-buffer@2.1.2: {}
+
scheduler@0.27.0: {}
scroll-into-view-if-needed@3.1.0:
dependencies:
compute-scroll-into-view: 3.1.1
+ semver@6.3.1: {}
+
semver@7.7.4: {}
semver@7.8.5: {}
set-cookie-parser@2.7.2: {}
+ shebang-command@2.0.0:
+ dependencies:
+ shebang-regex: 3.0.0
+
+ shebang-regex@3.0.0: {}
+
shiki@4.0.2:
dependencies:
'@shikijs/core': 4.0.2
@@ -4636,8 +5944,38 @@ 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.1:
+ 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: {}
+
skillgym@0.9.1:
dependencies:
'@types/node': 22.19.7
@@ -4673,6 +6011,8 @@ snapshots:
dependencies:
ansi-regex: 6.2.2
+ strip-final-newline@4.0.0: {}
+
style-to-js@1.1.21:
dependencies:
style-to-object: 1.0.14
@@ -4740,6 +6080,18 @@ snapshots:
tslib@2.8.1: {}
+ tunnel@0.0.6: {}
+
+ typed-inject@5.0.0: {}
+
+ typed-rest-client@2.3.1:
+ dependencies:
+ des.js: 1.1.0
+ js-md4: 0.3.2
+ qs: 6.15.1
+ tunnel: 0.0.6
+ underscore: 1.13.8
+
typescript@6.0.3: {}
typescript@7.0.2:
@@ -4770,6 +6122,8 @@ snapshots:
'@quansync/fs': 1.0.0
quansync: 1.0.0
+ underscore@1.13.8: {}
+
undici-types@6.21.0: {}
undici@7.28.0: {}
@@ -4778,6 +6132,8 @@ snapshots:
dependencies:
hookable: 6.1.0
+ unicorn-magic@0.3.0: {}
+
unified@11.0.5:
dependencies:
'@types/unist': 3.0.3
@@ -4825,6 +6181,12 @@ snapshots:
unist-util-is: 6.0.1
unist-util-visit-parents: 6.0.2
+ update-browserslist-db@1.2.3(browserslist@4.28.7):
+ dependencies:
+ browserslist: 4.28.7
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
vfile-location@5.0.3:
dependencies:
'@types/unist': 3.0.3
@@ -4880,8 +6242,14 @@ snapshots:
transitivePeerDependencies:
- msw
+ weapon-regex@1.3.6: {}
+
web-namespaces@2.0.1: {}
+ which@2.0.2:
+ dependencies:
+ isexe: 2.0.0
+
why-is-node-running@2.3.0:
dependencies:
siginfo: 2.0.0
@@ -4889,8 +6257,12 @@ snapshots:
ws@8.21.0: {}
+ yallist@3.1.1: {}
+
yaml@2.9.0: {}
+ yoctocolors@2.2.0: {}
+
yuku-ast@0.1.7:
dependencies:
'@yuku-toolchain/types': 0.5.43
diff --git a/scripts/check-affected/run.ts b/scripts/check-affected/run.ts
index 86d087497..2299ed50e 100644
--- a/scripts/check-affected/run.ts
+++ b/scripts/check-affected/run.ts
@@ -8,8 +8,8 @@
import fs from 'node:fs';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
-import { parseArgs as parseNodeArgs } from 'node:util';
import { runCmdStreaming, runCmdSync } from '../../src/utils/exec.ts';
+import { parseScriptArgs } from '../lib/cli-args.ts';
import {
assertCatalogComplete,
CHECK_CATALOG,
@@ -26,21 +26,12 @@ const repoRoot = runCmdSync('git', ['rev-parse', '--show-toplevel']).stdout.trim
const USAGE = 'Usage: pnpm check:affected [--base [] [--head ][] [--json] [--run]\n';
function parseArgs(argv: readonly string[]): Args {
- const { values } = parseNodeArgs({
- args: [...argv],
- options: {
- base: { type: 'string', default: 'origin/main' },
- head: { type: 'string', default: 'HEAD' },
- json: { type: 'boolean', default: false },
- run: { type: 'boolean', default: false },
- help: { type: 'boolean', short: 'h', default: false },
- },
- allowPositionals: false,
+ const values = parseScriptArgs(argv, USAGE, {
+ base: { type: 'string', default: 'origin/main' },
+ head: { type: 'string', default: 'HEAD' },
+ json: { type: 'boolean', default: false },
+ run: { type: 'boolean', default: false },
});
- if (values.help) {
- process.stdout.write(USAGE);
- process.exit(0);
- }
return {
base: values.base ?? 'origin/main',
head: values.head ?? 'HEAD',
diff --git a/scripts/lib/cli-args.ts b/scripts/lib/cli-args.ts
new file mode 100644
index 000000000..acd555ee5
--- /dev/null
+++ b/scripts/lib/cli-args.ts
@@ -0,0 +1,26 @@
+import { parseArgs, type ParseArgsConfig } from 'node:util';
+
+type Options = NonNullable;
+
+/**
+ * `parseArgs` plus the shared script `--help` contract: `-h`/`--help` prints the
+ * usage the caller owns and exits 0, so no script re-implements the flag.
+ */
+export function parseScriptArgs(
+ argv: readonly string[],
+ usage: string,
+ options: T,
+): ReturnType>['values'] {
+ const { values } = parseArgs({
+ args: [...argv],
+ options: { ...options, help: { type: 'boolean', short: 'h', default: false } },
+ allowPositionals: false,
+ });
+ if (values.help) {
+ process.stdout.write(usage);
+ process.exit(0);
+ }
+ return values as ReturnType<
+ typeof parseArgs<{ options: T & { help: { type: 'boolean' } } }>
+ >['values'];
+}
diff --git a/scripts/lib/lane-envelope.ts b/scripts/lib/lane-envelope.ts
new file mode 100644
index 000000000..8c0c24342
--- /dev/null
+++ b/scripts/lib/lane-envelope.ts
@@ -0,0 +1,69 @@
+// Standard artifact envelope for scheduled lanes (issue #1430).
+//
+// A scheduled lane can go dark or drift for weeks while PR CI stays green, so
+// every artifact it uploads must say — without a human reading logs — which
+// commit produced it, which tool/config version measured it, how long it took,
+// and whether it passed. Freshness monitoring reads `finishedAt`/`result`; drift
+// analysis reads `tool`/`configHash`.
+//
+// Lanes that adopt the envelope later should import this module rather than
+// re-deriving the field names.
+
+export const LANE_ENVELOPE_SCHEMA_VERSION = 1;
+
+export type LaneResult = 'pass' | 'fail';
+
+export type LaneEnvelope = {
+ schemaVersion: number;
+ /** Stable lane id — the metric key freshness/health jobs group by. */
+ lane: string;
+ commit: string;
+ ref: string | undefined;
+ runUrl: string | undefined;
+ /** Tool versions, or content hashes where a version is not enough. */
+ tool: Record;
+ configHash: string;
+ /** Only lanes with randomized input record a seed; `null` states "not applicable". */
+ seed: string | null;
+ startedAt: string;
+ finishedAt: string;
+ durationMs: number;
+ result: LaneResult;
+ /** Lane-specific payload; kept separate so the envelope shape stays stable. */
+ data: T;
+};
+
+export type LaneEnvelopeInput = {
+ lane: string;
+ commit: string;
+ tool: Record;
+ configHash: string;
+ startedAtMs: number;
+ result: LaneResult;
+ data: T;
+ seed?: string | null;
+ now?: number;
+};
+
+export function laneEnvelope(input: LaneEnvelopeInput): LaneEnvelope {
+ const finished = input.now ?? Date.now();
+ const { GITHUB_SERVER_URL, GITHUB_REPOSITORY, GITHUB_RUN_ID, GITHUB_REF_NAME } = process.env;
+ return {
+ schemaVersion: LANE_ENVELOPE_SCHEMA_VERSION,
+ lane: input.lane,
+ commit: input.commit,
+ ref: GITHUB_REF_NAME,
+ runUrl:
+ GITHUB_SERVER_URL && GITHUB_REPOSITORY && GITHUB_RUN_ID
+ ? `${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}`
+ : undefined,
+ tool: input.tool,
+ configHash: input.configHash,
+ seed: input.seed ?? null,
+ startedAt: new Date(input.startedAtMs).toISOString(),
+ finishedAt: new Date(finished).toISOString(),
+ durationMs: Math.max(0, finished - input.startedAtMs),
+ result: input.result,
+ data: input.data,
+ };
+}
diff --git a/scripts/mutation/config.test.ts b/scripts/mutation/config.test.ts
new file mode 100644
index 000000000..ff37d06b3
--- /dev/null
+++ b/scripts/mutation/config.test.ts
@@ -0,0 +1,41 @@
+// The Stryker config and the module registry are two views of one scope. If
+// they drift, the weekly sweep silently measures something other than the
+// enumerated decision kernels — the exact failure this lane exists to catch.
+
+import assert from 'node:assert/strict';
+import fs from 'node:fs';
+import path from 'node:path';
+import { test } from 'node:test';
+import { fileURLToPath } from 'node:url';
+import { mutateGlobs } from './modules.ts';
+import { configHash } from './run.ts';
+
+const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
+
+function readConfig(): Record {
+ const text = fs.readFileSync(path.join(repoRoot, 'stryker.config.json'), 'utf8');
+ return JSON.parse(text) as Record;
+}
+
+test('stryker mutate globs mirror the kernel-module registry', () => {
+ assert.deepEqual(
+ readConfig().mutate,
+ mutateGlobs(),
+ 'stryker.config.json `mutate` drifted from KERNEL_MODULES — update scripts/mutation/modules.ts and the config together.',
+ );
+});
+
+test('stryker owns no pass/fail threshold — the ratchet does', () => {
+ const thresholds = readConfig().thresholds as { break?: number | null } | undefined;
+ assert.equal(
+ thresholds?.break ?? null,
+ null,
+ "A Stryker `break` threshold would fail runs on an absolute score; gating is the ratchet's job (scripts/mutation/ratchet.ts).",
+ );
+});
+
+test('the config content hash is stable and content-addressed', () => {
+ assert.equal(configHash('a'), configHash('a'));
+ assert.notEqual(configHash('a'), configHash('b'));
+ assert.match(configHash('a'), /^sha256:[0-9a-f]{12}$/);
+});
diff --git a/scripts/mutation/envelope.test.ts b/scripts/mutation/envelope.test.ts
new file mode 100644
index 000000000..ef5e61568
--- /dev/null
+++ b/scripts/mutation/envelope.test.ts
@@ -0,0 +1,276 @@
+// The envelope is the only thing a downloaded scheduled-lane artifact can be
+// interpreted from months later (#1430), so its required fields are asserted
+// rather than assumed: a lane that stops emitting one of them makes freshness
+// and tool-drift monitoring silently useless.
+
+import assert from 'node:assert/strict';
+import fs from 'node:fs';
+import path from 'node:path';
+import { test } from 'node:test';
+import { runCmdSync } from '../../src/utils/exec.ts';
+import { laneEnvelope, LANE_ENVELOPE_SCHEMA_VERSION } from '../lib/lane-envelope.ts';
+
+const repoRoot = path.resolve(import.meta.dirname, '../..');
+
+function workflow(name: string): string {
+ return fs.readFileSync(path.join(repoRoot, '.github/workflows', name), 'utf8');
+}
+
+test('the envelope carries schema, commit, tool/config provenance, duration and result', () => {
+ const envelope = laneEnvelope({
+ lane: 'mutation-decision-kernels',
+ commit: 'a'.repeat(40),
+ tool: { stryker: '9.6.1' },
+ configHash: 'sha256:abcdef123456',
+ startedAtMs: 1_000,
+ now: 61_000,
+ result: 'pass',
+ data: { scope: 'full-sweep' },
+ });
+ assert.equal(envelope.schemaVersion, LANE_ENVELOPE_SCHEMA_VERSION);
+ assert.equal(envelope.lane, 'mutation-decision-kernels');
+ assert.equal(envelope.commit, 'a'.repeat(40));
+ assert.deepEqual(envelope.tool, { stryker: '9.6.1' });
+ assert.equal(envelope.configHash, 'sha256:abcdef123456');
+ // Mutation input is enumerated, not randomized: `null` is an explicit
+ // "not applicable", not a forgotten field.
+ assert.equal(envelope.seed, null);
+ assert.equal(envelope.durationMs, 60_000);
+ assert.equal(envelope.finishedAt, '1970-01-01T00:01:01.000Z');
+ assert.equal(envelope.result, 'pass');
+ assert.deepEqual(envelope.data, { scope: 'full-sweep' });
+});
+
+test('a failed ratchet is recorded as a failed lane run', () => {
+ const envelope = laneEnvelope({
+ lane: 'mutation-decision-kernels',
+ commit: 'b'.repeat(40),
+ tool: { stryker: '9.6.1' },
+ configHash: 'sha256:abcdef123456',
+ startedAtMs: 0,
+ now: 0,
+ result: 'fail',
+ data: {},
+ });
+ assert.equal(envelope.result, 'fail');
+ assert.equal(envelope.durationMs, 0);
+});
+
+// A lane that crashes before it can measure anything is the dark-lane case: an
+// absent envelope is indistinguishable from a lane that never ran, so the run
+// script must emit one from its failure path too.
+test('a crashed run still writes an envelope naming the stage it died in', () => {
+ const envelopePath = path.join(repoRoot, '.tmp/mutation/lane-envelope.json');
+ fs.rmSync(envelopePath, { force: true });
+ const result = runCmdSync(
+ 'node',
+ [
+ '--experimental-strip-types',
+ 'scripts/mutation/run.ts',
+ '--report',
+ '.tmp/mutation/absent-report.json',
+ '--modules',
+ 'kernel-errors',
+ ],
+ { cwd: repoRoot, allowFailure: true },
+ );
+ assert.notEqual(result.exitCode, 0, 'a missing report must fail the run');
+ assert.ok(fs.existsSync(envelopePath), 'no envelope written for a crashed run');
+ const envelope = JSON.parse(fs.readFileSync(envelopePath, 'utf8')) as {
+ result: string;
+ tool: Record;
+ configHash: string;
+ data: { stage: string; error: string | null };
+ };
+ assert.equal(envelope.result, 'fail');
+ assert.equal(envelope.data.stage, 'report');
+ assert.match(envelope.data.error ?? '', /absent-report\.json/);
+ // Provenance is read before the work, so a crash still reports its tool/config.
+ assert.ok(envelope.tool.stryker);
+ assert.match(envelope.configHash, /^sha256:/);
+});
+
+// Shard artifacts carry the envelope next to the report, so merging "every JSON
+// under the shard directory" fed the envelope to the report parser and crashed
+// the ratchet job after the mutants had already run.
+test('merging shard reports ignores the envelope sitting beside them', () => {
+ const shards = path.join(repoRoot, '.tmp/mutation/envelope-test-shards/shard-a');
+ fs.mkdirSync(shards, { recursive: true });
+ fs.writeFileSync(
+ path.join(shards, 'mutation.json'),
+ JSON.stringify({
+ files: {
+ 'src/kernel/errors.ts': { mutants: [{ status: 'Killed' }, { status: 'Survived' }] },
+ },
+ }),
+ );
+ fs.writeFileSync(
+ path.join(shards, 'lane-envelope.json'),
+ JSON.stringify(
+ laneEnvelope({
+ lane: 'mutation-decision-kernels',
+ commit: 'c'.repeat(40),
+ tool: { stryker: '9.6.1' },
+ configHash: 'sha256:abcdef123456',
+ startedAtMs: 0,
+ now: 0,
+ result: 'pass',
+ data: {},
+ }),
+ ),
+ );
+ const result = runCmdSync(
+ 'node',
+ [
+ '--experimental-strip-types',
+ 'scripts/mutation/run.ts',
+ '--report-dir',
+ '.tmp/mutation/envelope-test-shards',
+ '--modules',
+ 'kernel-errors',
+ ],
+ { cwd: repoRoot, allowFailure: true },
+ );
+ assert.match(result.stdout, /merging 1 shard report\(s\)/);
+ assert.match(result.stdout, /kernel-errors/);
+ assert.doesNotMatch(result.stderr, /Cannot convert undefined or null to object/);
+ fs.rmSync(path.join(repoRoot, '.tmp/mutation/envelope-test-shards'), {
+ recursive: true,
+ force: true,
+ });
+});
+
+function runMutation(args: readonly string[]): {
+ exitCode: number;
+ stdout: string;
+ stderr: string;
+} {
+ const result = runCmdSync(
+ 'node',
+ ['--experimental-strip-types', 'scripts/mutation/run.ts', ...args],
+ {
+ cwd: repoRoot,
+ allowFailure: true,
+ },
+ );
+ return { exitCode: result.exitCode ?? 1, stdout: result.stdout, stderr: result.stderr };
+}
+
+type Envelope = {
+ result: string;
+ data: { stage: string; error: string | null; modules: readonly { id: string }[] };
+};
+
+function readEnvelope(): Envelope {
+ return JSON.parse(
+ fs.readFileSync(path.join(repoRoot, '.tmp/mutation/lane-envelope.json'), 'utf8'),
+ ) as Envelope;
+}
+
+// A merged shard set is only a sweep if every requested module actually reported.
+// summarizeReport scores an absent module as 0, and while the lane is non-gating a
+// 0 only *reports* a regression — so a dead matrix shard would otherwise be
+// aggregated into a passing "complete" envelope claiming the sweep happened.
+test('an incomplete shard set fails instead of scoring the missing module as zero', () => {
+ const shards = path.join(repoRoot, '.tmp/mutation/partial-shards/shard-kernel-errors');
+ fs.mkdirSync(shards, { recursive: true });
+ fs.writeFileSync(
+ path.join(shards, 'mutation.json'),
+ JSON.stringify({
+ files: { 'src/kernel/errors.ts': { mutants: [{ status: 'Killed' }] } },
+ }),
+ );
+ const result = runMutation([
+ '--report-dir',
+ '.tmp/mutation/partial-shards',
+ '--modules',
+ 'kernel-errors,daemon-ref-frame',
+ ]);
+ assert.notEqual(result.exitCode, 0, 'a missing shard must fail the aggregate');
+ assert.match(result.stderr, /Incomplete shard set/);
+ assert.match(result.stderr, /daemon-ref-frame/);
+ const envelope = readEnvelope();
+ assert.equal(envelope.result, 'fail');
+ assert.equal(envelope.data.stage, 'ratchet');
+ fs.rmSync(path.join(repoRoot, '.tmp/mutation/partial-shards'), { recursive: true, force: true });
+});
+
+// Argument parsing and the provenance/baseline reads used to sit outside the
+// envelope boundary, so the lane could exit without declaring itself at all.
+test('a malformed invocation still writes an envelope', () => {
+ fs.rmSync(path.join(repoRoot, '.tmp/mutation/lane-envelope.json'), { force: true });
+ const result = runMutation(['--modules', 'not-a-kernel']);
+ assert.notEqual(result.exitCode, 0);
+ const envelope = readEnvelope();
+ assert.equal(envelope.result, 'fail');
+ assert.equal(envelope.data.stage, 'setup');
+ assert.match(envelope.data.error ?? '', /Unknown mutation module/);
+});
+
+// The weekly self-test and the affected-selection job run before any mutant, so
+// their failure has to be declared by the lane rather than only by the job log.
+test('--fail-envelope declares a step that failed before the sweep', () => {
+ fs.rmSync(path.join(repoRoot, '.tmp/mutation/lane-envelope.json'), { force: true });
+ const result = runMutation(['--fail-envelope', 'self-test failed']);
+ assert.notEqual(result.exitCode, 0, 'a pre-run failure must not report success');
+ const envelope = readEnvelope();
+ assert.equal(envelope.result, 'fail');
+ assert.equal(envelope.data.stage, 'setup');
+ assert.equal(envelope.data.error, 'self-test failed');
+});
+
+// The workflows run it from `if: failure()`, which also fires when the ratchet
+// itself failed — a generic reason must never displace the specific one.
+test('--fail-envelope keeps a failure the run already reported', () => {
+ fs.rmSync(path.join(repoRoot, '.tmp/mutation/lane-envelope.json'), { force: true });
+ runMutation(['--fail-envelope', 'the real failure']);
+ runMutation(['--fail-envelope', 'a later generic failure']);
+ assert.equal(readEnvelope().data.error, 'the real failure');
+});
+
+// A pass is not a verdict worth preserving: the weekly job copies the proposed
+// baseline and restores the committed one *after* the ratchet passed, so a failure
+// there would otherwise publish the failed scheduled job as a passing lane.
+test('--fail-envelope downgrades a passing envelope when a later step fails', () => {
+ const shards = path.join(repoRoot, '.tmp/mutation/pass-then-fail/shard-kernel-errors');
+ fs.mkdirSync(shards, { recursive: true });
+ // A perfect shard so the ratchet passes: the score can only rise from the
+ // committed kernel-errors baseline.
+ fs.writeFileSync(
+ path.join(shards, 'mutation.json'),
+ JSON.stringify({
+ files: { 'src/kernel/errors.ts': { mutants: [{ status: 'Killed' }, { status: 'Killed' }] } },
+ }),
+ );
+ const passing = runMutation([
+ '--report-dir',
+ '.tmp/mutation/pass-then-fail',
+ '--modules',
+ 'kernel-errors',
+ ]);
+ assert.equal(passing.exitCode, 0, passing.stderr);
+ assert.equal(readEnvelope().result, 'pass');
+
+ runMutation(['--fail-envelope', 'the baseline copy step failed']);
+ const envelope = readEnvelope();
+ assert.equal(envelope.result, 'fail', 'a failed job must not publish a passing envelope');
+ assert.equal(envelope.data.error, 'the baseline copy step failed');
+ fs.rmSync(path.join(repoRoot, '.tmp/mutation/pass-then-fail'), { recursive: true, force: true });
+});
+
+test('both mutation lanes record an envelope for failures before the sweep', () => {
+ for (const name of ['mutation-weekly.yml', 'mutation-affected.yml']) {
+ assert.match(workflow(name), /--fail-envelope/, `${name} can fail without an envelope`);
+ }
+});
+
+test('both mutation lanes publish the envelope', () => {
+ for (const name of ['mutation-weekly.yml', 'mutation-affected.yml']) {
+ assert.match(
+ workflow(name),
+ /\.tmp\/mutation\/lane-envelope\.json/,
+ `${name} does not upload the lane envelope`,
+ );
+ }
+ assert.match(workflow('mutation-weekly.yml'), /Lane envelope/);
+});
diff --git a/scripts/mutation/modules.test.ts b/scripts/mutation/modules.test.ts
new file mode 100644
index 000000000..046c04d2e
--- /dev/null
+++ b/scripts/mutation/modules.test.ts
@@ -0,0 +1,77 @@
+import assert from 'node:assert/strict';
+import fs from 'node:fs';
+import path from 'node:path';
+import { test } from 'node:test';
+import { fileURLToPath } from 'node:url';
+import {
+ affectedModules,
+ ALL_MODULE_IDS,
+ KERNEL_MODULES,
+ moduleForFile,
+ mutateGlobs,
+ shardMatrix,
+} from './modules.ts';
+
+const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
+
+test('every enumerated kernel path exists', () => {
+ for (const module of KERNEL_MODULES) {
+ for (const owned of module.owns) {
+ assert.ok(
+ fs.existsSync(path.join(repoRoot, owned)),
+ `${module.id} owns a path that no longer exists: ${owned}`,
+ );
+ }
+ }
+});
+
+test('changed sources map onto the module that owns them', () => {
+ assert.equal(moduleForFile('src/kernel/errors.ts'), 'kernel-errors');
+ assert.equal(moduleForFile('./src/daemon/ref-frame.ts'), 'daemon-ref-frame');
+ assert.equal(moduleForFile('src/selectors/parse.ts'), 'selectors');
+ // Selector tests live under the owned prefix; every other kernel's tests are
+ // attributed by ownership.ts, not by this path match.
+ assert.equal(moduleForFile('src/selectors/__tests__/resolve.test.ts'), 'selectors');
+ assert.equal(moduleForFile('src/kernel/rect.ts'), undefined);
+ assert.equal(moduleForFile('README.md'), undefined);
+});
+
+test('affected selection is deduplicated and registry-ordered', () => {
+ assert.deepEqual(
+ affectedModules([
+ 'src/selectors/parse.ts',
+ 'src/selectors/match.ts',
+ 'src/kernel/errors.ts',
+ 'docs/agents/testing.md',
+ ]),
+ ['kernel-errors', 'selectors'],
+ );
+ assert.deepEqual(affectedModules(['docs/agents/testing.md']), []);
+});
+
+test('mutate globs default to every module and narrow on request', () => {
+ assert.deepEqual(mutateGlobs(), mutateGlobs(ALL_MODULE_IDS));
+ assert.deepEqual(mutateGlobs(['kernel-errors']), ['src/kernel/errors.ts']);
+});
+
+// One job per module is only affordable while a module fits the lane's budget;
+// the shard count is registry data so the workflows and the runner agree on it.
+test('the shard matrix slices only the modules that declare shards', () => {
+ assert.deepEqual(shardMatrix(['kernel-errors']), [
+ { name: 'kernel-errors', module: 'kernel-errors' },
+ ]);
+ assert.deepEqual(shardMatrix(['selectors']), [
+ { name: 'selectors-1', module: 'selectors', shard: '1/4' },
+ { name: 'selectors-2', module: 'selectors', shard: '2/4' },
+ { name: 'selectors-3', module: 'selectors', shard: '3/4' },
+ { name: 'selectors-4', module: 'selectors', shard: '4/4' },
+ ]);
+ assert.equal(shardMatrix().length, ALL_MODULE_IDS.length + 3);
+ // Every registry module reaches the matrix: an unsharded sweep is not a sweep.
+ for (const id of ALL_MODULE_IDS) {
+ assert.ok(
+ shardMatrix().some((spec) => spec.module === id),
+ `${id} has no shard`,
+ );
+ }
+});
diff --git a/scripts/mutation/modules.ts b/scripts/mutation/modules.ts
new file mode 100644
index 000000000..df398b658
--- /dev/null
+++ b/scripts/mutation/modules.ts
@@ -0,0 +1,153 @@
+// Enumerated decision kernels the mutation lane measures (issue #1415).
+//
+// Mutation score is the only mechanical answer to "is this test load-bearing or
+// decorative", but a full-suite sweep is unaffordable. This registry is the
+// single source of truth for what Stryker mutates: `stryker.config.json`'s
+// `mutate` globs are asserted against it, and PR-affected gating maps changed
+// files onto modules through it.
+//
+// Membership rule: pure decision kernels only — a surviving mutant here means a
+// silently wrong agent-facing decision. Anything that spawns subprocesses or
+// waits real time is out of scope by construction (its mutants would be timeout
+// noise, not test-strength signal).
+
+export type ModuleId =
+ | 'kernel-errors'
+ | 'daemon-ref-frame'
+ | 'interaction-settle'
+ | 'scroll-edge-state'
+ | 'selectors';
+
+export type KernelModule = {
+ readonly id: ModuleId;
+ readonly label: string;
+ /** Globs handed to Stryker's `mutate`. */
+ readonly mutate: readonly string[];
+ /**
+ * Paths this module owns; a trailing `/` marks a directory prefix. Sources
+ * only — the tests whose strength the score measures are derived from the
+ * import graph in `ownership.ts`, never listed here.
+ */
+ readonly owns: readonly string[];
+ /**
+ * How many parallel jobs the module's mutants are sliced across. One job per
+ * module is the default; a module big enough to outrun the lane's 30-minute
+ * budget declares more (`--shard i/n` picks the slice).
+ */
+ readonly shards?: number;
+};
+
+export const KERNEL_MODULES: readonly KernelModule[] = [
+ {
+ id: 'kernel-errors',
+ label: 'Error retriability + hints',
+ mutate: ['src/kernel/errors.ts'],
+ owns: ['src/kernel/errors.ts'],
+ },
+ {
+ id: 'daemon-ref-frame',
+ label: 'Ref-frame admission matrix (ADR 0014)',
+ mutate: ['src/daemon/ref-frame.ts'],
+ owns: ['src/daemon/ref-frame.ts'],
+ },
+ {
+ id: 'interaction-settle',
+ label: 'Interaction settle decisions',
+ mutate: ['src/commands/interaction/runtime/settle.ts'],
+ owns: ['src/commands/interaction/runtime/settle.ts'],
+ },
+ {
+ id: 'scroll-edge-state',
+ label: 'Scroll edge-state detection',
+ mutate: ['src/utils/scroll-edge-state.ts'],
+ owns: ['src/utils/scroll-edge-state.ts'],
+ },
+ {
+ id: 'selectors',
+ label: 'Selector parsing + matching',
+ mutate: ['src/selectors/**/*.ts', '!src/selectors/**/*.test.ts', '!src/selectors/__tests__/**'],
+ // Selector tests live under the owned directory, so the prefix covers them.
+ owns: ['src/selectors/'],
+ // ~1,280 mutants at the observed ~3s/mutant on a 2-core runner is ~64
+ // minutes in one job — past the acceptance budget and its own timeout.
+ shards: 4,
+ },
+];
+
+export const ALL_MODULE_IDS: readonly ModuleId[] = KERNEL_MODULES.map((module) => module.id);
+
+export function moduleById(id: ModuleId): KernelModule {
+ const found = KERNEL_MODULES.find((module) => module.id === id);
+ if (!found) throw new Error(`Unknown mutation module: ${id}`);
+ return found;
+}
+
+export function isModuleId(value: string): value is ModuleId {
+ return ALL_MODULE_IDS.includes(value as ModuleId);
+}
+
+/** Mutate globs for a module subset, in registry order. */
+export function mutateGlobs(ids: readonly ModuleId[] = ALL_MODULE_IDS): string[] {
+ return KERNEL_MODULES.filter((module) => ids.includes(module.id)).flatMap((module) => [
+ ...module.mutate,
+ ]);
+}
+
+/**
+ * The module a lane-tooling change proves itself against before graduation.
+ * `kernel-errors` is the cheapest real sweep in the registry (one file, ~183
+ * mutants), so a change to the ratchet, the config, or the baseline runs actual
+ * mutants end to end without paying for the full sweep.
+ */
+export const LANE_CANARY: ModuleId = 'kernel-errors';
+
+/** One mutation job: a module, optionally one slice of it. */
+export type ShardSpec = { name: string; module: ModuleId; shard?: string };
+
+/**
+ * The jobs a module set expands into. Both workflows' matrices are this list, so
+ * a registry module (or a change to its shard count) can never leave the sweep
+ * without the workflow assertions noticing.
+ */
+export function shardMatrix(ids: readonly ModuleId[] = ALL_MODULE_IDS): ShardSpec[] {
+ return KERNEL_MODULES.filter((module) => ids.includes(module.id)).flatMap((module) => {
+ const count = module.shards ?? 1;
+ if (count === 1) return [{ name: module.id, module: module.id }];
+ return Array.from({ length: count }, (_unused, index) => ({
+ name: `${module.id}-${index + 1}`,
+ module: module.id,
+ shard: `${index + 1}/${count}`,
+ }));
+ });
+}
+
+export function normalizePath(filePath: string): string {
+ return filePath.replaceAll('\\', '/').replace(/^\.\//, '');
+}
+
+/**
+ * Which kernel module owns a repository-relative *source* path, if any.
+ *
+ * Test-file attribution is derived from the import graph — see
+ * `derivedAffectedModules` in `ownership.ts`, which is what the PR lane calls.
+ */
+export function moduleForFile(filePath: string): ModuleId | undefined {
+ const normalized = normalizePath(filePath);
+ for (const module of KERNEL_MODULES) {
+ for (const owned of module.owns) {
+ const match = owned.endsWith('/') ? normalized.startsWith(owned) : normalized === owned;
+ if (match) return module.id;
+ }
+ }
+ return undefined;
+}
+
+/** Kernel modules whose registry-owned paths a diff touches. */
+export function affectedModules(changedFiles: readonly string[]): ModuleId[] {
+ const ids = new Set();
+ for (const file of changedFiles) {
+ const id = moduleForFile(file);
+ if (id) ids.add(id);
+ }
+ return KERNEL_MODULES.filter((module) => ids.has(module.id)).map((module) => module.id);
+}
diff --git a/scripts/mutation/ownership.test.ts b/scripts/mutation/ownership.test.ts
new file mode 100644
index 000000000..096c54e85
--- /dev/null
+++ b/scripts/mutation/ownership.test.ts
@@ -0,0 +1,90 @@
+import assert from 'node:assert/strict';
+import path from 'node:path';
+import { test } from 'node:test';
+import { fileURLToPath } from 'node:url';
+import { KERNEL_MODULES } from './modules.ts';
+import {
+ derivedAffectedModules,
+ isTestFile,
+ mutatedSources,
+ ownedTestFiles,
+ ownershipDeriver,
+ reachableFrom,
+} from './ownership.ts';
+
+const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
+
+test('a kernel is owned by the mirrored test that imports it directly', () => {
+ const deriver = ownershipDeriver(repoRoot);
+ assert.deepEqual(deriver.ownersOf('src/kernel/__tests__/errors.test.ts'), ['kernel-errors']);
+ assert.ok(
+ deriver.ownersOf('src/daemon/__tests__/ref-frame.test.ts').includes('daemon-ref-frame'),
+ );
+ assert.ok(
+ deriver
+ .ownersOf('src/commands/interaction/runtime/settle.test.ts')
+ .includes('interaction-settle'),
+ );
+});
+
+// The omission that hand-listed ownership could not see: this test asserts over
+// normalizeError without importing src/kernel/errors.ts itself.
+test('a kernel is owned by tests that reach it indirectly', () => {
+ const deriver = ownershipDeriver(repoRoot);
+ assert.ok(
+ deriver.ownersOf('src/__tests__/daemon-error.test.ts').includes('kernel-errors'),
+ 'daemon-error.test.ts exercises normalizeError but does not own kernel-errors',
+ );
+ assert.ok(
+ deriver
+ .ownersOf('src/commands/interaction/runtime/gestures.test.ts')
+ .includes('scroll-edge-state'),
+ );
+});
+
+test('ownership is complete: every test reaching a kernel owns it', () => {
+ const owned = ownedTestFiles(repoRoot);
+ const cache = new Map();
+ for (const module of KERNEL_MODULES) {
+ const sources = mutatedSources(module, repoRoot);
+ assert.ok(sources.length > 0, `${module.id} mutates nothing`);
+ const files = owned.get(module.id) ?? [];
+ assert.ok(
+ files.length > 0,
+ `${module.id} has no owning tests — its score cannot be attributed`,
+ );
+ for (const testFile of files) {
+ const reachable = reachableFrom(testFile, repoRoot, cache);
+ assert.ok(
+ sources.some((source) => reachable.has(source)),
+ `${testFile} owns ${module.id} without reaching it`,
+ );
+ }
+ // The converse: a diff to an owning test selects the module on a PR.
+ assert.ok(
+ derivedAffectedModules([files[0]!], repoRoot).includes(module.id),
+ `changing ${files[0]} does not select ${module.id}`,
+ );
+ }
+});
+
+test('non-kernel sources and non-tests are not owned', () => {
+ assert.deepEqual(derivedAffectedModules(['README.md', 'src/kernel/rect.ts'], repoRoot), []);
+ assert.ok(!isTestFile('src/kernel/errors.ts'));
+ assert.ok(!isTestFile('scripts/mutation/ownership.test.ts'));
+ assert.ok(isTestFile('src/kernel/__tests__/errors.test.ts'));
+});
+
+test('derived selection unions source ownership with test reachability', () => {
+ assert.deepEqual(
+ derivedAffectedModules(
+ [
+ 'src/selectors/parse.ts',
+ 'src/daemon/__tests__/ref-frame.test.ts',
+ 'docs/agents/testing.md',
+ ],
+ repoRoot,
+ ).filter((id) => id === 'selectors' || id === 'daemon-ref-frame'),
+ ['daemon-ref-frame', 'selectors'],
+ );
+});
diff --git a/scripts/mutation/ownership.ts b/scripts/mutation/ownership.ts
new file mode 100644
index 000000000..571c71e1c
--- /dev/null
+++ b/scripts/mutation/ownership.ts
@@ -0,0 +1,136 @@
+// Which kernel module a changed file belongs to, DERIVED — never hand-listed.
+//
+// A mutation score is a statement about the tests that kill the mutants, so the
+// PR lane must re-measure a kernel whenever one of *those* tests changes. An
+// enumerated list of test files cannot state that: it silently omits tests that
+// exercise a kernel indirectly (`src/__tests__/daemon-error.test.ts` reaches
+// `normalizeError` through `src/daemon.ts`), and nothing fails when a new test
+// is added. So ownership is computed from the static import graph instead: a test
+// file owns every kernel module whose mutated sources it can reach.
+//
+// The derivation is deliberately a superset — reaching a kernel is cheaper to
+// prove than killing its mutants, so an unrelated diff can select a module and
+// pay for a report. False positives cost runner minutes; a false negative would
+// let a weakened test slip past the ratchet, which is the thing the lane exists
+// to catch.
+//
+// Non-test source changes outside the registry are NOT owned: they can only move
+// a score through the tests that reach the kernel, and the weekly full sweep is
+// what re-measures the whole surface. The PR lane's claim is narrower on purpose
+// — kernel sources plus the tests that exercise them.
+
+import fs from 'node:fs';
+import path from 'node:path';
+import { walkFiles } from '../lib/walk-files.ts';
+import {
+ affectedModules,
+ KERNEL_MODULES,
+ normalizePath,
+ type ModuleId,
+ type KernelModule,
+} from './modules.ts';
+import { expandMutateFiles } from './test-scope.ts';
+
+/** Test files the mutation lane can attribute to a kernel at all. */
+export function isTestFile(filePath: string): boolean {
+ const normalized = normalizePath(filePath);
+ return normalized.startsWith('src/') && normalized.endsWith('.test.ts');
+}
+
+/** Repository-relative modules a file imports, following relative specifiers only. */
+function importsOf(file: string, repoRoot: string, cache: Map): string[] {
+ const cached = cache.get(file);
+ if (cached) return cached;
+ const absolute = path.join(repoRoot, file);
+ const text = fs.existsSync(absolute) ? fs.readFileSync(absolute, 'utf8') : '';
+ const specifiers = [...text.matchAll(/(?:from|import)\s*\(?\s*'(?\.[^']+)'/g)].map(
+ (match) => match.groups!.spec,
+ );
+ const resolved = [
+ ...new Set(
+ specifiers.flatMap((specifier) => {
+ const base = path.posix.normalize(path.posix.join(path.posix.dirname(file), specifier));
+ return [base, `${base}.ts`, `${base}/index.ts`].filter((candidate) =>
+ fs.existsSync(path.join(repoRoot, candidate)),
+ );
+ }),
+ ),
+ ].filter((candidate) => candidate.endsWith('.ts'));
+ cache.set(file, resolved);
+ return resolved;
+}
+
+/** Every repository-relative module `file` reaches through the import graph. */
+export function reachableFrom(
+ file: string,
+ repoRoot: string,
+ cache: Map = new Map(),
+): Set {
+ const seen = new Set();
+ const queue = [normalizePath(file)];
+ while (queue.length > 0) {
+ const current = queue.shift()!;
+ if (seen.has(current)) continue;
+ seen.add(current);
+ queue.push(...importsOf(current, repoRoot, cache));
+ }
+ return seen;
+}
+
+/** The concrete sources Stryker mutates for a module. */
+export function mutatedSources(module: KernelModule, repoRoot: string): string[] {
+ return expandMutateFiles(module.mutate, repoRoot);
+}
+
+type Deriver = {
+ /** Kernel modules a single test file exercises, in registry order. */
+ ownersOf: (testFile: string) => ModuleId[];
+};
+
+/** A deriver with caches shared across files — one graph walk per module, not per query. */
+export function ownershipDeriver(repoRoot: string): Deriver {
+ const importCache = new Map();
+ const sources = KERNEL_MODULES.map((module) => ({
+ id: module.id,
+ sources: new Set(mutatedSources(module, repoRoot)),
+ }));
+ return {
+ ownersOf(testFile) {
+ const reachable = reachableFrom(testFile, repoRoot, importCache);
+ return sources
+ .filter((entry) => [...entry.sources].some((source) => reachable.has(source)))
+ .map((entry) => entry.id);
+ },
+ };
+}
+
+/**
+ * Kernel modules a diff affects: registry-owned paths plus every module the
+ * changed tests reach. Registry order, deduplicated.
+ */
+export function derivedAffectedModules(
+ changedFiles: readonly string[],
+ repoRoot: string,
+): ModuleId[] {
+ const ids = new Set(affectedModules(changedFiles));
+ const tests = changedFiles.filter(isTestFile).map(normalizePath);
+ if (tests.length > 0) {
+ const deriver = ownershipDeriver(repoRoot);
+ for (const testFile of tests) {
+ for (const id of deriver.ownersOf(testFile)) ids.add(id);
+ }
+ }
+ return KERNEL_MODULES.filter((module) => ids.has(module.id)).map((module) => module.id);
+}
+
+/** Every test file in the repository, per module that owns it — one graph walk. */
+export function ownedTestFiles(repoRoot: string): Map {
+ const deriver = ownershipDeriver(repoRoot);
+ const owned = new Map(KERNEL_MODULES.map((module) => [module.id, []]));
+ for (const file of walkFiles(path.join(repoRoot, 'src'), (file) => file.endsWith('.test.ts'))) {
+ const relative = normalizePath(path.relative(repoRoot, file));
+ for (const id of deriver.ownersOf(relative)) owned.get(id)!.push(relative);
+ }
+ for (const files of owned.values()) files.sort();
+ return owned;
+}
diff --git a/scripts/mutation/ratchet.test.ts b/scripts/mutation/ratchet.test.ts
new file mode 100644
index 000000000..40dd3a92b
--- /dev/null
+++ b/scripts/mutation/ratchet.test.ts
@@ -0,0 +1,240 @@
+import assert from 'node:assert/strict';
+import fs from 'node:fs';
+import path from 'node:path';
+import { test } from 'node:test';
+import { ALL_MODULE_IDS } from './modules.ts';
+import { renderReport } from './report.ts';
+import {
+ applyRun,
+ BASELINE_SCHEMA_VERSION,
+ DEFAULT_REQUIRED_STABLE_RUNS,
+ emptyBaseline,
+ evaluateRatchet,
+ type Baseline,
+ type ModuleBaseline,
+ type Provenance,
+} from './ratchet.ts';
+import { summarizeReport, type StrykerReport } from './score.ts';
+
+const PROVENANCE: Provenance = { strykerVersion: '9.6.1', configHash: 'sha256:abcdef123456' };
+const NOW = '2026-07-27T00:00:00.000Z';
+
+function mutants(statuses: readonly string[]): StrykerReport {
+ return {
+ files: {
+ 'src/kernel/errors.ts': {
+ mutants: statuses.map((status, index) => ({
+ status,
+ mutatorName: 'ConditionalExpression',
+ location: { start: { line: index + 1 } },
+ })),
+ },
+ },
+ };
+}
+
+function baselineWith(entry: Partial): Baseline {
+ return {
+ ...emptyBaseline(),
+ modules: {
+ 'kernel-errors': {
+ score: 75,
+ killed: 3,
+ survived: 1,
+ total: 4,
+ strykerVersion: PROVENANCE.strykerVersion,
+ configHash: PROVENANCE.configHash,
+ updatedAt: NOW,
+ ...entry,
+ },
+ },
+ };
+}
+
+test('scores count timeouts as killed and uncovered mutants as survived', () => {
+ const [score] = summarizeReport(mutants(['Killed', 'Timeout', 'Survived', 'NoCoverage']), [
+ 'kernel-errors',
+ ]);
+ assert.deepEqual(
+ { score: score?.score, killed: score?.killed, survived: score?.survived, total: score?.total },
+ { score: 50, killed: 2, survived: 2, total: 4 },
+ );
+});
+
+test('statuses outside the score (Ignored, CompileError) leave the denominator', () => {
+ const [score] = summarizeReport(mutants(['Killed', 'Ignored', 'CompileError']), [
+ 'kernel-errors',
+ ]);
+ assert.equal(score?.total, 1);
+ assert.equal(score?.score, 100);
+});
+
+test('a lowered score is a regression, and gating makes it fail with its survivors', () => {
+ const baseline = { ...baselineWith({ score: 100 }), stableRuns: 2, gating: true };
+ const scores = summarizeReport(mutants(['Killed', 'Survived']), ['kernel-errors']);
+ const result = evaluateRatchet(scores, baseline, PROVENANCE);
+
+ assert.equal(result.failed, true);
+ assert.deepEqual(
+ result.regressions.map((verdict) => verdict.module),
+ ['kernel-errors'],
+ );
+ assert.match(result.regressions[0]?.detail ?? '', /fell 100% -> 50%/);
+
+ const markdown = renderReport(result, baseline, PROVENANCE);
+ assert.match(markdown, /Surviving mutants:/);
+ assert.match(markdown, /`src\/kernel\/errors\.ts:2` ConditionalExpression/);
+ assert.match(markdown, /scores may only rise/);
+});
+
+test('a lowered score is reported but does not fail while the lane is non-gating', () => {
+ const scores = summarizeReport(mutants(['Killed', 'Survived']), ['kernel-errors']);
+ const baseline = baselineWith({ score: 100 });
+ const result = evaluateRatchet(scores, baseline, PROVENANCE);
+
+ assert.equal(result.regressions.length, 1);
+ assert.equal(result.failed, false);
+ assert.match(renderReport(result, baseline, PROVENANCE), /still non-gating/);
+});
+
+test('a regression never rewrites the baseline high-water mark', () => {
+ const baseline = baselineWith({ score: 100 });
+ const scores = summarizeReport(mutants(['Killed', 'Survived']), ['kernel-errors']);
+ const next = applyRun(baseline, scores, evaluateRatchet(scores, baseline, PROVENANCE), {
+ provenance: PROVENANCE,
+ now: NOW,
+ countsTowardGraduation: true,
+ });
+ assert.equal(next.modules['kernel-errors']?.score, 100);
+ assert.equal(next.stableRuns, 0);
+});
+
+test('a risen score is recorded with its provenance', () => {
+ const baseline = baselineWith({ score: 50 });
+ const scores = summarizeReport(mutants(['Killed', 'Killed']), ['kernel-errors']);
+ const result = evaluateRatchet(scores, baseline, PROVENANCE);
+ assert.equal(result.verdicts[0]?.status, 'improved');
+
+ const next = applyRun(baseline, scores, result, {
+ provenance: PROVENANCE,
+ now: NOW,
+ countsTowardGraduation: true,
+ });
+ assert.deepEqual(next.modules['kernel-errors'], {
+ score: 100,
+ killed: 2,
+ survived: 0,
+ total: 2,
+ strykerVersion: PROVENANCE.strykerVersion,
+ configHash: PROVENANCE.configHash,
+ updatedAt: NOW,
+ });
+});
+
+test('gating graduates after the required consecutive stable full sweeps', () => {
+ const scores = summarizeReport(mutants(['Killed', 'Killed']), ['kernel-errors']);
+ let baseline = baselineWith({ score: 100 });
+ assert.equal(baseline.gating, false);
+
+ for (let run = 1; run <= baseline.requiredStableRuns; run += 1) {
+ const result = evaluateRatchet(scores, baseline, PROVENANCE);
+ baseline = applyRun(baseline, scores, result, {
+ provenance: PROVENANCE,
+ now: NOW,
+ countsTowardGraduation: true,
+ });
+ assert.equal(baseline.stableRuns, run);
+ }
+ assert.equal(baseline.gating, true);
+
+ // A later regression resets graduation, so gating has to be re-earned.
+ const dropped = summarizeReport(mutants(['Killed', 'Survived']), ['kernel-errors']);
+ const regressed = evaluateRatchet(dropped, baseline, PROVENANCE);
+ assert.equal(regressed.failed, true);
+ const after = applyRun(baseline, dropped, regressed, {
+ provenance: PROVENANCE,
+ now: NOW,
+ countsTowardGraduation: true,
+ });
+ assert.equal(after.stableRuns, 0);
+ assert.equal(after.gating, false);
+});
+
+test('an affected PR run never advances graduation', () => {
+ const scores = summarizeReport(mutants(['Killed', 'Killed']), ['kernel-errors']);
+ const baseline = baselineWith({ score: 100 });
+ const next = applyRun(baseline, scores, evaluateRatchet(scores, baseline, PROVENANCE), {
+ provenance: PROVENANCE,
+ now: NOW,
+ countsTowardGraduation: false,
+ });
+ assert.equal(next.stableRuns, 0);
+});
+
+test('a tool or config change is provenance drift, not a test-strength regression', () => {
+ const scores = summarizeReport(mutants(['Killed', 'Survived']), ['kernel-errors']);
+ for (const drift of [{ strykerVersion: '9.5.0' }, { configHash: 'sha256:000000000000' }]) {
+ const baseline = { ...baselineWith({ score: 100, ...drift }), stableRuns: 2, gating: true };
+ const result = evaluateRatchet(scores, baseline, PROVENANCE);
+ assert.equal(result.regressions.length, 0);
+ assert.equal(result.failed, false);
+ assert.equal(result.comparable, false);
+ assert.match(result.drifted[0]?.detail ?? '', /not attributable to test strength/);
+
+ // Drift rebases onto the new tool/config and costs the graduation streak.
+ const next = applyRun(baseline, scores, result, {
+ provenance: PROVENANCE,
+ now: NOW,
+ countsTowardGraduation: true,
+ });
+ assert.equal(next.modules['kernel-errors']?.score, 50);
+ assert.equal(next.modules['kernel-errors']?.configHash, PROVENANCE.configHash);
+ assert.equal(next.stableRuns, 0);
+ }
+});
+
+test('a module with no baseline yet is new, recorded, and blocks graduation', () => {
+ const scores = summarizeReport(mutants(['Killed', 'Survived']), ['kernel-errors']);
+ const baseline = emptyBaseline();
+ const result = evaluateRatchet(scores, baseline, PROVENANCE);
+ assert.equal(result.verdicts[0]?.status, 'new');
+ assert.equal(result.comparable, false);
+
+ const next = applyRun(baseline, scores, result, {
+ provenance: PROVENANCE,
+ now: NOW,
+ countsTowardGraduation: true,
+ });
+ assert.equal(next.modules['kernel-errors']?.score, 50);
+ assert.equal(next.stableRuns, 0);
+});
+
+test('the report names the fix command and the graduation state', () => {
+ const scores = summarizeReport(mutants(['Killed', 'Killed']), ['kernel-errors']);
+ const baseline = { ...baselineWith({ score: 100 }), stableRuns: 1 };
+ const markdown = renderReport(
+ evaluateRatchet(scores, baseline, PROVENANCE),
+ baseline,
+ PROVENANCE,
+ );
+ assert.match(markdown, /gating \*\*off\*\* \(1\/2 stable weekly runs\)/);
+ assert.match(markdown, /Stryker `9\.6\.1` · config `sha256:abcdef123456`/);
+});
+
+test('the committed baseline is the shape the lane graduates from', () => {
+ const baseline = JSON.parse(
+ fs.readFileSync(
+ path.resolve(import.meta.dirname, '../../mutation-baselines/decision-kernels.json'),
+ 'utf8',
+ ),
+ ) as Baseline;
+ assert.equal(baseline.schemaVersion, BASELINE_SCHEMA_VERSION);
+ // Issue #1415's N: gating is earned by two consecutive stable weekly sweeps.
+ assert.equal(baseline.requiredStableRuns, DEFAULT_REQUIRED_STABLE_RUNS);
+ assert.deepEqual(Object.keys(baseline.modules), [...ALL_MODULE_IDS]);
+ for (const [id, module] of Object.entries(baseline.modules)) {
+ assert.ok(module.total > 0, `${id} has no mutants`);
+ assert.ok(module.strykerVersion.length > 0, `${id} has no Stryker version`);
+ assert.match(module.configHash, /^sha256:[0-9a-f]{12}$/);
+ }
+});
diff --git a/scripts/mutation/ratchet.ts b/scripts/mutation/ratchet.ts
new file mode 100644
index 000000000..63700d1df
--- /dev/null
+++ b/scripts/mutation/ratchet.ts
@@ -0,0 +1,208 @@
+// The ratchet itself: scores may only rise, gating is earned, and a score that
+// moved for tool/config reasons is never mistaken for test-strength change.
+//
+// Graduation (issue #1415): the lane starts non-gating. Each comparable weekly
+// full sweep with no regression increments `stableRuns`; once it reaches
+// `requiredStableRuns` the baseline flips to `gating: true`, after which a
+// regression fails the run — weekly as a full sweep, PRs for AFFECTED modules
+// only. A regression or a non-comparable run resets the counter to zero.
+
+import { roundScore, type ModuleScore, type SurvivingMutant } from './score.ts';
+import type { ModuleId } from './modules.ts';
+
+export const BASELINE_SCHEMA_VERSION = 1;
+export const DEFAULT_REQUIRED_STABLE_RUNS = 2;
+
+export type ModuleBaseline = {
+ score: number;
+ killed: number;
+ survived: number;
+ total: number;
+ // Provenance travels with every module baseline so a tool or config change is
+ // distinguishable from a test-strength change.
+ strykerVersion: string;
+ configHash: string;
+ updatedAt: string;
+};
+
+export type Baseline = {
+ schemaVersion: number;
+ requiredStableRuns: number;
+ stableRuns: number;
+ gating: boolean;
+ modules: Record;
+};
+
+export type Provenance = { readonly strykerVersion: string; readonly configHash: string };
+
+export type VerdictStatus = 'new' | 'improved' | 'held' | 'regressed' | 'provenance-drift';
+
+export type ModuleVerdict = {
+ readonly module: ModuleId;
+ readonly score: number;
+ readonly killed: number;
+ readonly total: number;
+ readonly baselineScore: number | undefined;
+ readonly delta: number | undefined;
+ readonly status: VerdictStatus;
+ readonly surviving: readonly SurvivingMutant[];
+ readonly detail: string;
+};
+
+export type RatchetResult = {
+ readonly verdicts: readonly ModuleVerdict[];
+ readonly regressions: readonly ModuleVerdict[];
+ readonly drifted: readonly ModuleVerdict[];
+ /** A run is comparable when every module has a same-provenance baseline. */
+ readonly comparable: boolean;
+ readonly gating: boolean;
+ /** True when the caller must exit non-zero. */
+ readonly failed: boolean;
+};
+
+export function emptyBaseline(requiredStableRuns: number = DEFAULT_REQUIRED_STABLE_RUNS): Baseline {
+ return {
+ schemaVersion: BASELINE_SCHEMA_VERSION,
+ requiredStableRuns,
+ stableRuns: 0,
+ gating: false,
+ modules: {},
+ };
+}
+
+function driftDetail(previous: ModuleBaseline, provenance: Provenance): string {
+ return (
+ `baseline recorded by stryker ${previous.strykerVersion} / config ${previous.configHash}, ` +
+ `this run used stryker ${provenance.strykerVersion} / config ${provenance.configHash}; ` +
+ 'the score change is not attributable to test strength — re-record with `pnpm mutation:baseline`'
+ );
+}
+
+function verdictFor(
+ score: ModuleScore,
+ previous: ModuleBaseline | undefined,
+ provenance: Provenance,
+): ModuleVerdict {
+ const base = {
+ module: score.module,
+ score: score.score,
+ killed: score.killed,
+ total: score.total,
+ surviving: score.surviving,
+ };
+ if (!previous) {
+ return {
+ ...base,
+ baselineScore: undefined,
+ delta: undefined,
+ status: 'new',
+ detail: `no baseline recorded yet (measured ${score.score}%)`,
+ };
+ }
+ const delta = roundScore(score.score - previous.score);
+ if (
+ previous.strykerVersion !== provenance.strykerVersion ||
+ previous.configHash !== provenance.configHash
+ ) {
+ return {
+ ...base,
+ baselineScore: previous.score,
+ delta,
+ status: 'provenance-drift',
+ detail: driftDetail(previous, provenance),
+ };
+ }
+ if (delta < 0) {
+ return {
+ ...base,
+ baselineScore: previous.score,
+ delta,
+ status: 'regressed',
+ detail:
+ `mutation score fell ${previous.score}% -> ${score.score}% ` +
+ `(${score.survived} surviving mutants)`,
+ };
+ }
+ return {
+ ...base,
+ baselineScore: previous.score,
+ delta,
+ status: delta > 0 ? 'improved' : 'held',
+ detail:
+ delta > 0
+ ? `mutation score rose ${previous.score}% -> ${score.score}%`
+ : `mutation score held at ${score.score}%`,
+ };
+}
+
+export function evaluateRatchet(
+ scores: readonly ModuleScore[],
+ baseline: Baseline,
+ provenance: Provenance,
+): RatchetResult {
+ const verdicts = scores.map((score) =>
+ verdictFor(score, baseline.modules[score.module], provenance),
+ );
+ const regressions = verdicts.filter((verdict) => verdict.status === 'regressed');
+ const drifted = verdicts.filter((verdict) => verdict.status === 'provenance-drift');
+ const comparable = drifted.length === 0 && verdicts.every((v) => v.status !== 'new');
+ return {
+ verdicts,
+ regressions,
+ drifted,
+ comparable,
+ gating: baseline.gating,
+ failed: baseline.gating && regressions.length > 0,
+ };
+}
+
+export type ApplyOptions = {
+ readonly provenance: Provenance;
+ readonly now: string;
+ /** Only the weekly full sweep may advance graduation; affected runs may not. */
+ readonly countsTowardGraduation: boolean;
+};
+
+/**
+ * Fold a run into the baseline: keep the high-water score per module (a
+ * regression never rewrites it, so the ratchet keeps failing until the tests are
+ * restored), rebase modules whose provenance drifted onto the new tool/config,
+ * then advance or reset graduation.
+ */
+export function applyRun(
+ baseline: Baseline,
+ scores: readonly ModuleScore[],
+ result: RatchetResult,
+ options: ApplyOptions,
+): Baseline {
+ const modules: Record = { ...baseline.modules };
+ for (const score of scores) {
+ const previous = modules[score.module];
+ const status = result.verdicts.find((verdict) => verdict.module === score.module)?.status;
+ const rebase = !previous || status === 'provenance-drift';
+ if (!rebase && score.score < previous.score) continue;
+ modules[score.module] = {
+ score: rebase ? score.score : Math.max(previous.score, score.score),
+ killed: score.killed,
+ survived: score.survived,
+ total: score.total,
+ strykerVersion: options.provenance.strykerVersion,
+ configHash: options.provenance.configHash,
+ updatedAt: options.now,
+ };
+ }
+
+ const stable = result.comparable && result.regressions.length === 0;
+ const stableRuns = !options.countsTowardGraduation
+ ? baseline.stableRuns
+ : stable
+ ? baseline.stableRuns + 1
+ : 0;
+ return {
+ ...baseline,
+ schemaVersion: BASELINE_SCHEMA_VERSION,
+ stableRuns,
+ gating: stableRuns >= baseline.requiredStableRuns,
+ modules,
+ };
+}
diff --git a/scripts/mutation/report.ts b/scripts/mutation/report.ts
new file mode 100644
index 000000000..31d761fcf
--- /dev/null
+++ b/scripts/mutation/report.ts
@@ -0,0 +1,81 @@
+// Markdown rendering for the mutation lane: GitHub job summary and terminal
+// output share one renderer, so the artifact and the console never disagree.
+
+import { moduleById } from './modules.ts';
+import type { Baseline, ModuleVerdict, Provenance, RatchetResult } from './ratchet.ts';
+
+const DEFAULT_MAX_SURVIVING_LISTED = 20;
+
+function formatDelta(delta: number | undefined): string {
+ if (delta === undefined) return '—';
+ return delta > 0 ? `+${delta}` : String(delta);
+}
+
+function renderRow(verdict: ModuleVerdict): string {
+ const module = moduleById(verdict.module);
+ const baseline = verdict.baselineScore === undefined ? '—' : `${verdict.baselineScore}%`;
+ return (
+ `| \`${verdict.module}\` — ${module.label} | ${verdict.score}% | ${baseline} | ` +
+ `${formatDelta(verdict.delta)} | ${verdict.killed}/${verdict.total} | ` +
+ `${verdict.surviving.length} | ${verdict.status} |`
+ );
+}
+
+function renderDetail(verdict: ModuleVerdict, maxListed: number): string[] {
+ const lines = ['', `### \`${verdict.module}\` — ${verdict.status}`, '', verdict.detail];
+ if (verdict.surviving.length > 0) {
+ lines.push('', 'Surviving mutants:', '');
+ for (const mutant of verdict.surviving.slice(0, maxListed)) {
+ lines.push(`- \`${mutant.file}:${mutant.line}\` ${mutant.mutator}`);
+ }
+ if (verdict.surviving.length > maxListed) {
+ lines.push(`- …and ${verdict.surviving.length - maxListed} more`);
+ }
+ }
+ return lines;
+}
+
+export type RenderOptions = {
+ readonly title?: string;
+ readonly maxSurvivingListed?: number;
+};
+
+export function renderReport(
+ result: RatchetResult,
+ baseline: Baseline,
+ provenance: Provenance,
+ options: RenderOptions = {},
+): string {
+ const maxListed = options.maxSurvivingListed ?? DEFAULT_MAX_SURVIVING_LISTED;
+ const lines: string[] = [
+ `## ${options.title ?? 'Mutation score — decision kernels'}`,
+ '',
+ `Stryker \`${provenance.strykerVersion}\` · config \`${provenance.configHash}\` · ` +
+ `gating **${baseline.gating ? 'on' : 'off'}** ` +
+ `(${baseline.stableRuns}/${baseline.requiredStableRuns} stable weekly runs)`,
+ '',
+ '| Module | Score | Baseline | Δ | Killed/Total | Surviving | Status |',
+ '| --- | --- | --- | --- | --- | --- | --- |',
+ ...result.verdicts.map(renderRow),
+ ];
+
+ for (const verdict of [...result.regressions, ...result.drifted]) {
+ lines.push(...renderDetail(verdict, maxListed));
+ }
+
+ if (result.failed) {
+ lines.push(
+ '',
+ 'Mutation ratchet failed: scores may only rise. Kill the surviving mutants listed above, ' +
+ 'then re-run `pnpm mutation:run` (full sweep) or `pnpm mutation:check --report ` ' +
+ 'against an existing Stryker report.',
+ );
+ } else if (result.regressions.length > 0) {
+ lines.push(
+ '',
+ 'Scores regressed while the lane is still non-gating — no failure recorded, but the ' +
+ 'surviving mutants above are the tests to strengthen before gating turns on.',
+ );
+ }
+ return `${lines.join('\n')}\n`;
+}
diff --git a/scripts/mutation/run.ts b/scripts/mutation/run.ts
new file mode 100644
index 000000000..db608ae0e
--- /dev/null
+++ b/scripts/mutation/run.ts
@@ -0,0 +1,559 @@
+// Entrypoint for the decision-kernel mutation lane (issue #1415).
+//
+// pnpm mutation:run full sweep + ratchet check
+// pnpm mutation:baseline full sweep, then record the scores
+// pnpm mutation:check --report ratchet an existing Stryker report
+// pnpm mutation:affected --base origin/main
+// PR lane: mutate only the kernel
+// modules the diff touches
+//
+// The weekly workflow runs the full sweep and writes the rendered report to the
+// job summary plus an artifact; the PR lane runs the affected subset and only
+// fails once the baseline has graduated to `gating: true`.
+
+import crypto from 'node:crypto';
+import fs from 'node:fs';
+import path from 'node:path';
+import { pathToFileURL } from 'node:url';
+import { runCmdStreaming, runCmdSync } from '../../src/utils/exec.ts';
+import { parseScriptArgs } from '../lib/cli-args.ts';
+import { laneEnvelope } from '../lib/lane-envelope.ts';
+import {
+ ALL_MODULE_IDS,
+ isModuleId,
+ LANE_CANARY,
+ mutateGlobs,
+ normalizePath,
+ shardMatrix,
+ type ModuleId,
+ type ShardSpec,
+} from './modules.ts';
+import { derivedAffectedModules } from './ownership.ts';
+import { renderReport } from './report.ts';
+import {
+ applyRun,
+ emptyBaseline,
+ evaluateRatchet,
+ type Baseline,
+ type Provenance,
+ type RatchetResult,
+} from './ratchet.ts';
+import { mergeReports, summarizeReport, type ModuleScore, type StrykerReport } from './score.ts';
+import {
+ expandMutateFiles,
+ relatedTestFiles,
+ TEST_SCOPE_ENV,
+ writeTestScope,
+} from './test-scope.ts';
+
+const repoRoot = runCmdSync('git', ['rev-parse', '--show-toplevel']).stdout.trim();
+
+const CONFIG_PATH = 'stryker.config.json';
+const BASELINE_PATH = 'mutation-baselines/decision-kernels.json';
+const DEFAULT_REPORT_PATH = '.tmp/mutation/mutation.json';
+const TEST_SCOPE_PATH = '.tmp/mutation/test-scope.json';
+const ENVELOPE_PATH = '.tmp/mutation/lane-envelope.json';
+const LANE_ID = 'mutation-decision-kernels';
+
+const USAGE = `Usage: pnpm mutation:run [options]
+
+ --modules Restrict to specific kernel modules (default: all)
+ --affected Restrict to the modules touched between --base and HEAD
+ --base ][ Base ref for --affected (default: origin/main)
+ --report Read an existing Stryker JSON report instead of running Stryker
+ --report-dir Merge every *.json Stryker report under (weekly shards)
+ --expect-shards
+ Fail unless --report-dir holds exactly n shard reports
+ --shard Mutate only the i-th of n balanced slices of the module's
+ sources (the big modules exceed one job's budget)
+ --update Record the run into the baseline (ratchet + graduation)
+ --summary Also write the markdown report to
+ --no-run Alias for --report with the default report path
+ --list-affected Print the PR lane's shard matrix as JSON and exit (empty
+ until the baseline graduates, unless the diff touches the
+ lane's own tooling)
+ --fail-envelope
+ Write a failed lane envelope for a step that ran before (or
+ instead of) the sweep, e.g. a self-test failure
+`;
+
+type Args = {
+ modules: readonly ModuleId[];
+ affected: boolean;
+ base: string;
+ report: string | undefined;
+ reportDir: string | undefined;
+ update: boolean;
+ summary: string | undefined;
+ listAffected: boolean;
+ failEnvelope: string | undefined;
+ shard: Shard | undefined;
+ expectShards: number | undefined;
+};
+
+/** One-based slice of a module's mutated sources: `--shard 2/4`. */
+type Shard = { index: number; count: number };
+
+function parseShard(value: string | undefined): Shard | undefined {
+ if (!value) return undefined;
+ const match = /^(\d+)\/(\d+)$/.exec(value.trim());
+ const index = Number(match?.[1]);
+ const count = Number(match?.[2]);
+ if (!match || index < 1 || index > count) {
+ throw new Error(`--shard expects i/n with 1 <= i <= n, got "${value}"`);
+ }
+ return { index, count };
+}
+
+function parseModules(value: string | undefined): readonly ModuleId[] {
+ if (!value) return ALL_MODULE_IDS;
+ const ids = value
+ .split(',')
+ .map((entry) => entry.trim())
+ .filter(Boolean);
+ const unknown = ids.filter((id) => !isModuleId(id));
+ if (unknown.length > 0) {
+ throw new Error(
+ `Unknown mutation module(s): ${unknown.join(', ')}. Known: ${ALL_MODULE_IDS.join(', ')}`,
+ );
+ }
+ return ids.filter(isModuleId);
+}
+
+function parseMutationArgs(argv: readonly string[]): Args {
+ const values = parseScriptArgs(argv, USAGE, {
+ modules: { type: 'string' },
+ affected: { type: 'boolean', default: false },
+ base: { type: 'string', default: 'origin/main' },
+ report: { type: 'string' },
+ 'report-dir': { type: 'string' },
+ update: { type: 'boolean', default: false },
+ summary: { type: 'string' },
+ 'no-run': { type: 'boolean', default: false },
+ 'list-affected': { type: 'boolean', default: false },
+ 'fail-envelope': { type: 'string' },
+ shard: { type: 'string' },
+ 'expect-shards': { type: 'string' },
+ });
+ return {
+ modules: parseModules(values.modules),
+ affected: Boolean(values.affected),
+ base: values.base ?? 'origin/main',
+ report: values.report ?? (values['no-run'] ? DEFAULT_REPORT_PATH : undefined),
+ reportDir: values['report-dir'],
+ update: Boolean(values.update),
+ summary: values.summary,
+ listAffected: Boolean(values['list-affected']),
+ failEnvelope: values['fail-envelope'],
+ shard: parseShard(values.shard),
+ expectShards: values['expect-shards'] ? Number(values['expect-shards']) : undefined,
+ };
+}
+
+/** Short, stable content hash of the Stryker config — half of a run's provenance. */
+export function configHash(configText: string): string {
+ return `sha256:${crypto.createHash('sha256').update(configText).digest('hex').slice(0, 12)}`;
+}
+
+function readProvenance(root: string = repoRoot): Provenance {
+ const pkgPath = path.join(root, 'node_modules/@stryker-mutator/core/package.json');
+ const version = fs.existsSync(pkgPath)
+ ? (JSON.parse(fs.readFileSync(pkgPath, 'utf8')) as { version: string }).version
+ : 'unknown';
+ return {
+ strykerVersion: version,
+ configHash: configHash(fs.readFileSync(path.join(root, CONFIG_PATH), 'utf8')),
+ };
+}
+
+function readBaseline(root: string = repoRoot): Baseline {
+ const file = path.join(root, BASELINE_PATH);
+ if (!fs.existsSync(file)) return emptyBaseline();
+ return JSON.parse(fs.readFileSync(file, 'utf8')) as Baseline;
+}
+
+function writeBaseline(baseline: Baseline, root: string = repoRoot): void {
+ const file = path.join(root, BASELINE_PATH);
+ fs.mkdirSync(path.dirname(file), { recursive: true });
+ fs.writeFileSync(file, `${JSON.stringify(baseline, null, 2)}\n`);
+}
+
+function changedFiles(base: string): string[] {
+ const result = runCmdSync('git', ['diff', '--name-only', '--merge-base', base, 'HEAD'], {
+ cwd: repoRoot,
+ allowFailure: true,
+ });
+ return result.stdout.split('\n').filter(Boolean);
+}
+
+// The JSON report path comes from the config (Stryker's CLI takes no nested
+// reporter options), so a run always writes DEFAULT_REPORT_PATH.
+/**
+ * Balances a module's sources across `count` jobs. Mutant count tracks file size
+ * closely enough that greedy longest-first packing keeps the slowest slice near
+ * the mean — the alternative is one 1,277-mutant selectors job that outruns both
+ * the 30-minute acceptance budget and its own timeout.
+ */
+function shardFiles(files: readonly string[], shard: Shard, root: string): string[] {
+ const bins: { size: number; files: string[] }[] = Array.from({ length: shard.count }, () => ({
+ size: 0,
+ files: [],
+ }));
+ const weighed = files
+ .map((file) => ({ file, size: fs.statSync(path.join(root, file)).size }))
+ .sort((a, b) => b.size - a.size || a.file.localeCompare(b.file));
+ for (const { file, size } of weighed) {
+ const bin = bins.reduce((smallest, next) => (next.size < smallest.size ? next : smallest));
+ bin.files.push(file);
+ bin.size += size;
+ }
+ return bins[shard.index - 1]!.files.sort();
+}
+
+async function runStryker(
+ modules: readonly ModuleId[],
+ reportPath: string,
+ shard: Shard | undefined,
+): Promise {
+ const absolute = path.isAbsolute(reportPath) ? reportPath : path.join(repoRoot, reportPath);
+ fs.mkdirSync(path.dirname(absolute), { recursive: true });
+ fs.rmSync(absolute, { force: true });
+ // The suite Stryker replays per mutant is derived from Vitest's module graph
+ // over the mutated files, not hand-listed; see scripts/mutation/test-scope.ts.
+ const globs = mutateGlobs(modules);
+ const all = expandMutateFiles(globs, repoRoot);
+ // A sharded job mutates concrete files, so the slice is exact rather than a
+ // glob the next contributor has to keep in step with the registry.
+ const mutate = shard ? shardFiles(all, shard, repoRoot) : globs;
+ const scopePath = path.join(repoRoot, TEST_SCOPE_PATH);
+ const testFiles = relatedTestFiles(shard ? mutate : all, repoRoot);
+ writeTestScope(testFiles, scopePath);
+ process.stdout.write(
+ `mutation: ${modules.join(', ')}${shard ? ` shard ${shard.index}/${shard.count}` : ''} -> ` +
+ `${shard ? mutate.length : all.length} source(s), ${testFiles.length} related test file(s)\n`,
+ );
+
+ // Stryker's `--mutate` takes one comma-separated value, not repeated args.
+ const args = ['exec', 'stryker', 'run', CONFIG_PATH, '--mutate', mutate.join(',')];
+ const result = await runCmdStreaming('pnpm', args, {
+ cwd: repoRoot,
+ allowFailure: true,
+ env: { ...process.env, [TEST_SCOPE_ENV]: scopePath },
+ onStdoutChunk: (chunk) => void process.stdout.write(chunk),
+ onStderrChunk: (chunk) => void process.stderr.write(chunk),
+ });
+ // Stryker exits non-zero on a low score too; the ratchet — not Stryker's own
+ // thresholds — owns the verdict, so only a missing report is fatal here.
+ if (!fs.existsSync(absolute)) {
+ throw new Error(
+ `Stryker produced no report at ${reportPath} (exit ${result.exitCode}). See output above.`,
+ );
+ }
+}
+
+function emit(markdown: string, summaryPath: string | undefined): void {
+ process.stdout.write(`\n${markdown}`);
+ const targets = [summaryPath, process.env.GITHUB_STEP_SUMMARY].filter(
+ (target): target is string => Boolean(target),
+ );
+ for (const target of targets) {
+ fs.mkdirSync(path.dirname(path.resolve(target)), { recursive: true });
+ fs.appendFileSync(target, markdown);
+ }
+}
+
+/**
+ * How far the lane got. A run that dies in `stryker` or `report` is exactly the
+ * failure the freshness monitor must see, so the stage rides in the envelope
+ * rather than only in the job log.
+ */
+type Stage = 'setup' | 'select' | 'stryker' | 'report' | 'ratchet' | 'complete';
+
+/**
+ * Provenance for a lane that died before it could read any: `unknown` is a
+ * reported fact, whereas skipping the envelope would be silence.
+ */
+const UNKNOWN_PROVENANCE: Provenance = { strykerVersion: 'unknown', configHash: 'unknown' };
+
+type LaneState = {
+ stage: Stage;
+ provenance: Provenance;
+ baseline: Baseline;
+ modules: readonly ModuleId[];
+ affected: boolean;
+ scores: readonly ModuleScore[];
+ result: RatchetResult | undefined;
+ error: string | undefined;
+ /**
+ * `--fail-envelope` keeps an existing *failure* (its reason is the specific
+ * one) but replaces an existing pass: a post-verdict step can fail after the
+ * ratchet passed, and publishing that job as passing is the bug the envelope
+ * exists to prevent.
+ */
+ recoveryOnly: boolean;
+};
+
+/** The result of an envelope already on disk, if there is a readable one. */
+function existingResult(file: string): string | undefined {
+ if (!fs.existsSync(file)) return undefined;
+ try {
+ return (JSON.parse(fs.readFileSync(file, 'utf8')) as { result?: string }).result;
+ } catch {
+ return undefined;
+ }
+}
+
+/**
+ * Scheduled-lane artifact envelope (#1430). Without it a downloaded report cannot
+ * say which commit or Stryker/config version produced it, how long the sweep took,
+ * or whether it passed — freshness and tool-drift monitoring would have to parse
+ * logs.
+ *
+ * Written on every exit path, including a crashed setup or a Stryker run that
+ * produced no report: a lane that fails before it can measure anything is the
+ * dark-lane case, and an absent envelope is indistinguishable from a lane that
+ * never ran.
+ */
+function writeEnvelope(state: LaneState, startedAtMs: number): void {
+ const target = path.join(repoRoot, ENVELOPE_PATH);
+ if (state.recoveryOnly && existingResult(target) === 'fail') {
+ process.stdout.write(`mutation: ${ENVELOPE_PATH} already reports a failure; left as is.\n`);
+ return;
+ }
+ const envelope = laneEnvelope({
+ lane: LANE_ID,
+ commit: runCmdSync('git', ['rev-parse', 'HEAD'], {
+ cwd: repoRoot,
+ allowFailure: true,
+ }).stdout.trim(),
+ tool: { stryker: state.provenance.strykerVersion },
+ configHash: state.provenance.configHash,
+ startedAtMs,
+ result: state.stage === 'complete' && !state.result?.failed ? 'pass' : 'fail',
+ data: {
+ scope: state.affected ? 'affected' : 'full-sweep',
+ stage: state.stage,
+ error: state.error ?? null,
+ modules: state.modules.map((id) => {
+ const score = state.scores.find((entry) => entry.module === id);
+ return {
+ id,
+ score: score?.score ?? null,
+ killed: score?.killed ?? null,
+ total: score?.total ?? null,
+ status: state.result?.verdicts.find((verdict) => verdict.module === id)?.status ?? null,
+ };
+ }),
+ gating: state.baseline.gating,
+ stableRuns: state.baseline.stableRuns,
+ requiredStableRuns: state.baseline.requiredStableRuns,
+ },
+ });
+ fs.mkdirSync(path.dirname(target), { recursive: true });
+ fs.writeFileSync(target, `${JSON.stringify(envelope, null, 2)}\n`);
+ process.stdout.write(
+ `\nLane envelope (${ENVELOPE_PATH}): ${envelope.result} at stage ${state.stage} in ` +
+ `${Math.round(envelope.durationMs / 1000)}s at ${envelope.commit.slice(0, 12)}.\n`,
+ );
+}
+
+function readReport(file: string): StrykerReport {
+ const absolute = path.isAbsolute(file) ? file : path.join(repoRoot, file);
+ return JSON.parse(fs.readFileSync(absolute, 'utf8')) as StrykerReport;
+}
+
+function readShardedReports(dir: string, expected: number | undefined): StrykerReport {
+ const root = path.isAbsolute(dir) ? dir : path.join(repoRoot, dir);
+ // Shard artifacts also carry the lane envelope and the derived test scope, so
+ // the report is selected by name rather than by "every .json here".
+ const files = fs
+ .globSync(`**/${path.basename(DEFAULT_REPORT_PATH)}`, { cwd: root })
+ .map((file) => path.join(root, file))
+ .sort();
+ if (files.length === 0) throw new Error(`No Stryker JSON reports found under ${dir}`);
+ // Sub-sharded modules make "every module has mutants" too weak on its own: the
+ // surviving slices would still cover the module, so the expected shard count is
+ // asserted as well.
+ if (expected !== undefined && files.length !== expected) {
+ throw new Error(
+ `Incomplete shard set from ${dir}: ${files.length} report(s), expected ${expected}. ` +
+ 'A shard job failed or its artifact is absent; the aggregate is not a sweep.',
+ );
+ }
+ process.stdout.write(`mutation: merging ${files.length} shard report(s) from ${dir}\n`);
+ return mergeReports(files.map(readReport));
+}
+
+async function produceReport(
+ modules: readonly ModuleId[],
+ reportPath: string | undefined,
+ shard: Shard | undefined,
+): Promise {
+ if (!reportPath) await runStryker(modules, DEFAULT_REPORT_PATH, shard);
+ return readReport(reportPath ?? DEFAULT_REPORT_PATH);
+}
+
+function recordRun(args: Args, state: LaneState, result: RatchetResult): void {
+ const next = applyRun(state.baseline, state.scores, result, {
+ provenance: state.provenance,
+ now: new Date().toISOString(),
+ // Only the full sweep proves stability; an affected subset says nothing
+ // about the modules it skipped.
+ countsTowardGraduation: !args.affected && state.modules.length === ALL_MODULE_IDS.length,
+ });
+ writeBaseline(next);
+ process.stdout.write(
+ `\nBaseline updated (${BASELINE_PATH}): ${next.stableRuns}/${next.requiredStableRuns} ` +
+ `stable runs, gating ${next.gating ? 'on' : 'off'}.\n`,
+ );
+}
+
+/**
+ * A merged shard set must cover every requested module. `summarizeReport` scores
+ * a module with no mutants as 0, and while the lane is non-gating a 0 is only
+ * *reported* as a regression — so a matrix shard that died would otherwise be
+ * aggregated into a `complete`/`pass` envelope claiming the sweep happened.
+ */
+function assertShardsCoverModules(state: LaneState, dir: string): void {
+ const missing = state.scores.filter((score) => score.total === 0).map((score) => score.module);
+ if (missing.length === 0) return;
+ throw new Error(
+ `Incomplete shard set from ${dir}: no mutants for ${missing.join(', ')}. ` +
+ 'A shard job failed or its artifact is absent; the aggregate is not a sweep.',
+ );
+}
+
+/** Reads shard reports, an existing report, or runs Stryker — and scores them. */
+async function scoreModules(args: Args, state: LaneState): Promise {
+ state.stage = args.reportDir || args.report ? 'report' : 'stryker';
+ const report = args.reportDir
+ ? readShardedReports(args.reportDir, args.expectShards)
+ : await produceReport(state.modules, args.report, args.shard);
+
+ state.stage = 'ratchet';
+ state.scores = summarizeReport(report, state.modules);
+ if (args.reportDir) assertShardsCoverModules(state, args.reportDir);
+}
+
+async function sweep(args: Args, state: LaneState): Promise {
+ state.stage = 'select';
+ // Test attribution is derived from the import graph, not a listed set of test
+ // files: see scripts/mutation/ownership.ts.
+ // Same selection the PR matrix uses, graduation rule included, so the ratchet
+ // job can never run mutants the `select` job decided not to spend.
+ if (args.affected) {
+ state.modules = [...new Set(affectedMatrix(args.base).map((entry) => entry.module))];
+ }
+ if (state.modules.length === 0) {
+ process.stdout.write('mutation: no decision-kernel modules affected — nothing to mutate.\n');
+ state.stage = 'complete';
+ return 0;
+ }
+
+ await scoreModules(args, state);
+ const result = evaluateRatchet(state.scores, state.baseline, state.provenance);
+ state.result = result;
+
+ const title = args.affected
+ ? 'Mutation score — affected decision kernels'
+ : 'Mutation score — decision kernels';
+ emit(renderReport(result, state.baseline, state.provenance, { title }), args.summary);
+
+ if (args.update) recordRun(args, state, result);
+ state.stage = 'complete';
+ return result.failed ? 1 : 0;
+}
+
+/** Sources of the lane itself: a change here must prove itself on real mutants. */
+const LANE_TOOLING = ['scripts/mutation/', 'scripts/lib/', 'stryker.config.json', 'mutation-'];
+
+/**
+ * The PR lane's matrix. Before graduation the affected run is a report nobody
+ * acts on, so it costs runner minutes for no verdict: it stays empty until the
+ * baseline reaches `gating: true`. The exception is a diff that changes the lane
+ * itself — that is the one case where the pre-graduation run buys something,
+ * because the gate has to be proven before it can bite.
+ */
+export function affectedMatrixFor(
+ changed: readonly string[],
+ gating: boolean,
+ root: string = repoRoot,
+): ShardSpec[] {
+ const touchesLane = changed.some((file) =>
+ LANE_TOOLING.some((prefix) => normalizePath(file).startsWith(prefix)),
+ );
+ if (!gating && !touchesLane) return [];
+ const modules = new Set(derivedAffectedModules(changed, root));
+ // Lane sources own no kernel, so a tooling-only diff derives nothing: without
+ // the canary the "prove the gate" exception would select zero mutants and
+ // prove nothing.
+ if (touchesLane) modules.add(LANE_CANARY);
+ return shardMatrix(ALL_MODULE_IDS.filter((id) => modules.has(id)));
+}
+
+function affectedMatrix(base: string): ShardSpec[] {
+ return affectedMatrixFor(changedFiles(base), readBaseline().gating);
+}
+
+/**
+ * Everything after `--help` runs inside the envelope boundary, argument parsing
+ * and provenance reads included: a lane that cannot even read its own config is
+ * the failure freshness monitoring most needs to see.
+ */
+async function run(argv: readonly string[], state: LaneState): Promise {
+ const args = parseMutationArgs(argv);
+ state.affected = args.affected;
+ state.provenance = readProvenance();
+ state.baseline = readBaseline();
+ state.modules = args.modules;
+ if (args.failEnvelope) {
+ // A step that ran before the sweep failed (the weekly self-test, setup): the
+ // lane produced no measurement, and that is what the envelope must say.
+ state.error = args.failEnvelope;
+ state.recoveryOnly = true;
+ return 1;
+ }
+ return await sweep(args, state);
+}
+
+async function main(argv = process.argv.slice(2)): Promise {
+ const startedAtMs = Date.now();
+ if (argv.includes('--list-affected')) {
+ // Selection only — no lane run, so no envelope. The shard jobs that consume
+ // this matrix each write their own.
+ const args = parseMutationArgs(argv);
+ process.stdout.write(`${JSON.stringify(affectedMatrix(args.base))}\n`);
+ return 0;
+ }
+ const state: LaneState = {
+ stage: 'setup',
+ provenance: UNKNOWN_PROVENANCE,
+ baseline: emptyBaseline(),
+ modules: [],
+ affected: false,
+ scores: [],
+ result: undefined,
+ error: undefined,
+ recoveryOnly: false,
+ };
+ try {
+ return await run(argv, state);
+ } catch (error: unknown) {
+ state.error = error instanceof Error ? error.message : String(error);
+ process.stderr.write(`mutation: ${state.error}\n`);
+ return 1;
+ } finally {
+ writeEnvelope(state, startedAtMs);
+ }
+}
+
+if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
+ main()
+ .then((code) => {
+ process.exitCode = code;
+ })
+ .catch((error: unknown) => {
+ process.stderr.write(`mutation: ${error instanceof Error ? error.message : String(error)}\n`);
+ process.exitCode = 1;
+ });
+}
diff --git a/scripts/mutation/score.ts b/scripts/mutation/score.ts
new file mode 100644
index 000000000..514f5ee4b
--- /dev/null
+++ b/scripts/mutation/score.ts
@@ -0,0 +1,103 @@
+// Per-module mutation scores derived from a Stryker JSON report.
+
+import { ALL_MODULE_IDS, moduleForFile, normalizePath, type ModuleId } from './modules.ts';
+
+/** The subset of Stryker's JSON report schema this lane reads. */
+export type StrykerMutant = {
+ readonly mutatorName?: string;
+ readonly status: string;
+ readonly location?: { readonly start?: { readonly line?: number } };
+};
+
+export type StrykerReport = {
+ readonly files: Record;
+};
+
+export type SurvivingMutant = {
+ readonly file: string;
+ readonly line: number;
+ readonly mutator: string;
+};
+
+export type ModuleScore = {
+ readonly module: ModuleId;
+ readonly score: number;
+ readonly killed: number;
+ readonly survived: number;
+ readonly total: number;
+ readonly surviving: readonly SurvivingMutant[];
+};
+
+// Stryker counts a timeout as killed. `NoCoverage` counts as survived here: an
+// uncovered mutant is precisely the "decorative test" signal this lane exists to
+// surface. Every other status (Ignored, CompileError, RuntimeError) leaves the
+// denominator, so tool-side noise cannot move the score.
+const KILLED_STATUSES = new Set(['Killed', 'Timeout']);
+const SURVIVED_STATUSES = new Set(['Survived', 'NoCoverage']);
+
+/**
+ * Merge sharded Stryker reports into one. The weekly sweep runs one shard per
+ * kernel module so no single job approaches its time budget; the ratchet still
+ * evaluates a single full-sweep report.
+ */
+export function mergeReports(reports: readonly StrykerReport[]): StrykerReport {
+ const files: Record = {};
+ for (const report of reports) {
+ for (const [file, entry] of Object.entries(report.files)) {
+ const existing = files[file];
+ if (existing) existing.mutants.push(...entry.mutants);
+ else files[file] = { mutants: [...entry.mutants] };
+ }
+ }
+ return { files };
+}
+
+export function roundScore(value: number): number {
+ return Math.round(value * 100) / 100;
+}
+
+function compareMutants(a: SurvivingMutant, b: SurvivingMutant): number {
+ return a.file.localeCompare(b.file) || a.line - b.line || a.mutator.localeCompare(b.mutator);
+}
+
+export function summarizeReport(
+ report: StrykerReport,
+ ids: readonly ModuleId[] = ALL_MODULE_IDS,
+): ModuleScore[] {
+ const buckets = new Map<
+ ModuleId,
+ { killed: number; survived: number; surviving: SurvivingMutant[] }
+ >();
+ for (const id of ids) buckets.set(id, { killed: 0, survived: 0, surviving: [] });
+
+ for (const [file, entry] of Object.entries(report.files)) {
+ const id = moduleForFile(file);
+ if (!id) continue;
+ const bucket = buckets.get(id);
+ if (!bucket) continue;
+ for (const mutant of entry.mutants) {
+ if (KILLED_STATUSES.has(mutant.status)) {
+ bucket.killed += 1;
+ } else if (SURVIVED_STATUSES.has(mutant.status)) {
+ bucket.survived += 1;
+ bucket.surviving.push({
+ file: normalizePath(file),
+ line: mutant.location?.start?.line ?? 0,
+ mutator: mutant.mutatorName ?? 'unknown',
+ });
+ }
+ }
+ }
+
+ return [...buckets].map(([module, bucket]) => {
+ const total = bucket.killed + bucket.survived;
+ return {
+ module,
+ score: total === 0 ? 0 : roundScore((bucket.killed / total) * 100),
+ killed: bucket.killed,
+ survived: bucket.survived,
+ total,
+ surviving: bucket.surviving.sort(compareMutants),
+ };
+ });
+}
diff --git a/scripts/mutation/selection.test.ts b/scripts/mutation/selection.test.ts
new file mode 100644
index 000000000..d6546f008
--- /dev/null
+++ b/scripts/mutation/selection.test.ts
@@ -0,0 +1,92 @@
+// Selection is what the PR lane spends money on, so both halves of the rule are
+// asserted end to end through the real CLI: nothing runs before graduation, and
+// the "prove the gate" exception for a lane-tooling diff selects real mutants
+// rather than an empty matrix that proves nothing.
+
+import assert from 'node:assert/strict';
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+import { after, test } from 'node:test';
+import { runCmdSync } from '../../src/utils/exec.ts';
+import { LANE_CANARY, shardMatrix, type ShardSpec } from './modules.ts';
+import { affectedMatrixFor } from './run.ts';
+
+const repoRoot = path.resolve(import.meta.dirname, '../..');
+const worktrees: string[] = [];
+
+after(() => {
+ for (const dir of worktrees) {
+ runCmdSync('git', ['worktree', 'remove', '--force', dir], { cwd: repoRoot });
+ }
+});
+
+/**
+ * A throwaway worktree holding one commit that touches only the given files, so
+ * `--list-affected` runs against a real `git diff` rather than a stubbed list.
+ */
+function worktreeWithCommit(name: string, files: readonly string[]): string {
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), `mutation-${name}-`));
+ fs.rmSync(dir, { recursive: true });
+ runCmdSync('git', ['worktree', 'add', '--detach', '--quiet', dir, 'HEAD'], { cwd: repoRoot });
+ worktrees.push(dir);
+ for (const file of files) {
+ fs.appendFileSync(path.join(dir, file), '\n');
+ }
+ runCmdSync('git', ['add', ...files], { cwd: dir });
+ // CI runners have no committer identity configured, and this commit is a
+ // fixture, so it carries its own rather than depending on the environment.
+ runCmdSync(
+ 'git',
+ [
+ '-c',
+ 'user.name=mutation-selection-test',
+ '-c',
+ 'user.email=mutation-selection-test@invalid',
+ 'commit',
+ '--quiet',
+ '--no-verify',
+ '-m',
+ `touch ${name}`,
+ ],
+ { cwd: dir },
+ );
+ return dir;
+}
+
+function listAffected(cwd: string): ShardSpec[] {
+ const result = runCmdSync(
+ 'node',
+ [
+ '--experimental-strip-types',
+ path.join(repoRoot, 'scripts/mutation/run.ts'),
+ '--list-affected',
+ '--base',
+ 'HEAD~1',
+ ],
+ { cwd },
+ );
+ assert.equal(result.exitCode, 0, result.stderr);
+ return JSON.parse(result.stdout.trim().split('\n').at(-1)!) as ShardSpec[];
+}
+
+test('a lane-tooling diff selects real mutants even before graduation', () => {
+ const dir = worktreeWithCommit('tooling', ['scripts/mutation/ratchet.ts']);
+ // The lane's own sources own no kernel, so derivation alone yields nothing:
+ // without the canary this exception would run zero mutants.
+ assert.deepEqual(listAffected(dir), shardMatrix([LANE_CANARY]));
+});
+
+test('a kernel diff selects nothing until the baseline graduates', () => {
+ const dir = worktreeWithCommit('kernel', ['src/utils/scroll-edge-state.ts']);
+ assert.deepEqual(listAffected(dir), []);
+ // …and the same diff selects that module once gating is on.
+ assert.deepEqual(
+ affectedMatrixFor(['src/utils/scroll-edge-state.ts'], true),
+ shardMatrix(['scroll-edge-state']),
+ );
+});
+
+test('a docs-only diff selects nothing even once gating is on', () => {
+ assert.deepEqual(affectedMatrixFor(['docs/agents/testing.md'], true), []);
+});
diff --git a/scripts/mutation/test-scope.test.ts b/scripts/mutation/test-scope.test.ts
new file mode 100644
index 000000000..271f9e645
--- /dev/null
+++ b/scripts/mutation/test-scope.test.ts
@@ -0,0 +1,28 @@
+import assert from 'node:assert/strict';
+import path from 'node:path';
+import { test } from 'node:test';
+import { expandMutateFiles, threadHostileTestFiles } from './test-scope.ts';
+import { mutateGlobs } from './modules.ts';
+
+const repoRoot = path.resolve(import.meta.dirname, '../..');
+
+test('mutate globs expand to real kernel sources and exclude selector tests', () => {
+ const files = expandMutateFiles(mutateGlobs(), repoRoot);
+ assert.ok(files.includes('src/kernel/errors.ts'));
+ assert.ok(files.some((file) => file.startsWith('src/selectors/')));
+ assert.deepEqual(
+ files.filter((file) => file.endsWith('.test.ts')),
+ [],
+ );
+});
+
+test('the thread-hostile exclusion is derived from imports, not listed', () => {
+ const files = threadHostileTestFiles(repoRoot);
+ // Both reasons are represented: process.chdir (CLI capture harness) and a
+ // worker inside a worker (the node:worker_threads PNG pipeline).
+ assert.ok(files.includes('src/__tests__/cli-help.test.ts'));
+ assert.ok(files.includes('src/utils/__tests__/png-worker.test.ts'));
+ // A pure decision-kernel test is never excluded — that would hide survivors.
+ assert.ok(!files.includes('src/daemon/__tests__/ref-frame.test.ts'));
+ assert.ok(files.every((file) => file.endsWith('.test.ts')));
+});
diff --git a/scripts/mutation/test-scope.ts b/scripts/mutation/test-scope.ts
new file mode 100644
index 000000000..c70177aa7
--- /dev/null
+++ b/scripts/mutation/test-scope.ts
@@ -0,0 +1,135 @@
+// Which tests the mutation lane runs, derived from Vitest's module graph.
+//
+// Stryker replays the configured suite for every mutant, so the suite it is
+// pointed at decides whether the weekly sweep fits its 30-minute budget. Pointing
+// it at the whole unit suite (487 files) makes the initial dry run alone cost
+// minutes; hand-listing per-kernel test files would be a second source of truth
+// that silently rots. So the scope is derived the same way `pnpm check:affected`
+// derives affected tests: `vitest related` over the mutated files, i.e. Vitest's
+// own static module graph.
+//
+// Two files are removed from whatever Vitest returns:
+// - the subprocess-stub group (it spawns stubbed binaries and waits real
+// subprocess/retry/poll time — out of scope by the issue's constraint, and
+// thousands of mutant runs would turn it into timeout noise);
+// - tests that cannot run in the thread pool Stryker's vitest runner forces:
+// the in-process CLI-capture tests (`process.chdir` throws in a worker
+// thread) and the `node:worker_threads` PNG pipeline tests (a worker inside
+// a worker raises uncaught MessagePort errors that kill the runner);
+// - anything outside `src/`: the unit suite also hosts the help-conformance
+// gates from `scripts/__tests__`, which assert over the repo's own registries
+// rather than over any decision kernel and own their CI job.
+//
+// Nothing here weakens the ratchet: a mutant only an excluded test could kill
+// shows up as a survivor — visible work, never a silent pass.
+
+import fs from 'node:fs';
+import path from 'node:path';
+import { runCmdSync } from '../../src/utils/exec.ts';
+import { walkFiles } from '../lib/walk-files.ts';
+import { normalizePath } from './modules.ts';
+
+/** Env var carrying the resolved scope file to `vitest.mutation.config.ts`. */
+export const TEST_SCOPE_ENV = 'AGENT_DEVICE_MUTATION_TEST_FILES';
+const CLI_CAPTURE_HARNESS = 'src/__tests__/cli-capture.ts';
+
+/** Source modules that own a `node:worker_threads` worker. */
+function workerThreadModules(repoRoot: string): string[] {
+ return walkFiles(
+ path.join(repoRoot, 'src'),
+ (file) => file.endsWith('.ts') && !file.endsWith('.test.ts'),
+ )
+ .filter((file) => fs.readFileSync(file, 'utf8').includes('node:worker_threads'))
+ .map((file) => path.basename(file, '.ts'));
+}
+
+/**
+ * Repo-relative test files that cannot survive Stryker's thread pool, derived
+ * from what they import rather than listed: the chdir-using CLI capture harness
+ * and any module that itself starts a worker thread.
+ */
+export function threadHostileTestFiles(repoRoot: string): string[] {
+ const modules = [path.basename(CLI_CAPTURE_HARNESS, '.ts'), ...workerThreadModules(repoRoot)];
+ const importsHostileModule = new RegExp(`from '[^']*/(${modules.join('|')})(\\.ts)?'`);
+ return walkFiles(path.join(repoRoot, 'src'), (file) => file.endsWith('.test.ts'))
+ .filter((file) => importsHostileModule.test(fs.readFileSync(file, 'utf8')))
+ .map((file) => normalizePath(path.relative(repoRoot, file)))
+ .sort();
+}
+
+/**
+ * Concrete source files behind a module's mutate globs. Stryker's `!`-prefixed
+ * exclusions are applied here too — `fs.globSync` has no notion of them, and
+ * dropping them would feed selector *tests* into the scope derivation.
+ */
+export function expandMutateFiles(globs: readonly string[], repoRoot: string): string[] {
+ const negated = globs.filter((glob) => glob.startsWith('!')).map((glob) => glob.slice(1));
+ const excluded = new Set(fs.globSync(negated, { cwd: repoRoot }).map(normalizePath));
+ return fs
+ .globSync(
+ globs.filter((glob) => !glob.startsWith('!')),
+ { cwd: repoRoot },
+ )
+ .map(normalizePath)
+ .filter((file) => !excluded.has(file))
+ .sort();
+}
+
+type VitestJsonReport = { testResults?: readonly { name: string }[] };
+
+/**
+ * Test files Vitest considers related to `sourceFiles`, minus the groups this
+ * lane cannot run. `vitest related` executes them once (seconds), which also
+ * proves the scope is green before Stryker's dry run depends on it.
+ */
+export function relatedTestFiles(
+ sourceFiles: readonly string[],
+ repoRoot: string,
+ excluded: readonly string[] = threadHostileTestFiles(repoRoot),
+): string[] {
+ const reportFile = path.join(repoRoot, '.tmp/mutation/related-tests.json');
+ fs.mkdirSync(path.dirname(reportFile), { recursive: true });
+ fs.rmSync(reportFile, { force: true });
+ runCmdSync(
+ 'pnpm',
+ [
+ 'exec',
+ 'vitest',
+ 'related',
+ ...sourceFiles,
+ '--project',
+ 'unit-core',
+ '--run',
+ '--reporter=json',
+ `--outputFile=${reportFile}`,
+ ],
+ { cwd: repoRoot, allowFailure: true },
+ );
+ if (!fs.existsSync(reportFile)) {
+ throw new Error(
+ `vitest related produced no report at ${reportFile} — cannot derive the mutation test scope.`,
+ );
+ }
+ const report = JSON.parse(fs.readFileSync(reportFile, 'utf8')) as VitestJsonReport;
+ const excludedSet = new Set(excluded);
+ return [
+ ...new Set(
+ (report.testResults ?? [])
+ .map((result) => normalizePath(path.relative(repoRoot, result.name)))
+ .filter((file) => file.startsWith('src/') && !excludedSet.has(file)),
+ ),
+ ].sort();
+}
+
+/** Read the scope Stryker was handed, or `undefined` for "whole unit suite". */
+export function readTestScope(): string[] | undefined {
+ const file = process.env[TEST_SCOPE_ENV];
+ if (!file || !fs.existsSync(file)) return undefined;
+ const files = JSON.parse(fs.readFileSync(file, 'utf8')) as string[];
+ return files.length > 0 ? files : undefined;
+}
+
+export function writeTestScope(files: readonly string[], file: string): void {
+ fs.mkdirSync(path.dirname(file), { recursive: true });
+ fs.writeFileSync(file, `${JSON.stringify(files, null, 2)}\n`);
+}
diff --git a/scripts/mutation/workflow.test.ts b/scripts/mutation/workflow.test.ts
new file mode 100644
index 000000000..2cbeb2788
--- /dev/null
+++ b/scripts/mutation/workflow.test.ts
@@ -0,0 +1,86 @@
+// The workflows' YAML cannot read the kernel registry, so these assertions keep
+// the two in step: a module added to KERNEL_MODULES that no weekly shard runs
+// would silently drop out of the sweep, and one no PR path filter selects would
+// silently stop gating once the ratchet graduates.
+
+import assert from 'node:assert/strict';
+import fs from 'node:fs';
+import path from 'node:path';
+import { test } from 'node:test';
+import { KERNEL_MODULES, shardMatrix } from './modules.ts';
+
+const repoRoot = path.resolve(import.meta.dirname, '../..');
+
+function workflow(name: string): string {
+ return fs.readFileSync(path.join(repoRoot, '.github/workflows', name), 'utf8');
+}
+
+test('the weekly sweep shards exactly the registry matrix', () => {
+ const yaml = workflow('mutation-weekly.yml');
+ const jobs = [...yaml.matchAll(/^ {10}- \{ (?[^}]+) \}$/gm)].map((match) =>
+ Object.fromEntries(
+ match
+ .groups!.entry.split(', ')
+ .map((pair) => pair.split(': ') as [string, string])
+ .map(([key, value]) => [key, value]),
+ ),
+ );
+ assert.deepEqual(
+ jobs,
+ shardMatrix().map((spec) =>
+ spec.shard ? { ...spec } : { name: spec.name, module: spec.module },
+ ),
+ );
+});
+
+test('the weekly sweep merges the shards into one ratcheted verdict', () => {
+ const yaml = workflow('mutation-weekly.yml');
+ assert.match(yaml, /pnpm mutation:check --report-dir/);
+ assert.match(yaml, /GITHUB_STEP_SUMMARY|\$GITHUB_STEP_SUMMARY/);
+ // A dead shard must not be merged into a verdict that looks like a sweep.
+ assert.match(
+ yaml,
+ new RegExp(`--expect-shards ${shardMatrix().length}\\b`),
+ 'the weekly ratchet does not require the full shard set',
+ );
+});
+
+// A shard that outruns the job timeout reports nothing, so the per-shard budget
+// is the acceptance criterion made mechanical.
+test('no mutation shard is allowed to exceed the 30-minute budget', () => {
+ for (const name of ['mutation-weekly.yml', 'mutation-affected.yml']) {
+ for (const [, minutes] of workflow(name).matchAll(/timeout-minutes: (\d+)/g)) {
+ assert.ok(Number(minutes) <= 30, `${name} declares a ${minutes}-minute job`);
+ }
+ }
+});
+
+test('every kernel path a PR can touch selects the affected mutation job', () => {
+ const paths = [...workflow('mutation-affected.yml').matchAll(/^ {6}- "(?[^"]+)"$/gm)].map(
+ (match) => match.groups!.glob,
+ );
+ for (const module of KERNEL_MODULES) {
+ for (const owned of module.owns) {
+ const selected = paths.some(
+ (glob) =>
+ glob === owned ||
+ glob === `${owned}**` ||
+ (glob.endsWith('/**') && owned.startsWith(glob.slice(0, -2))),
+ );
+ assert.ok(selected, `no path filter selects ${owned} (module ${module.id})`);
+ }
+ }
+ // Ownership is derived, so any test in src/ can own a kernel; the filter must
+ // let all of them through and leave the decision to the `select` job. A
+ // narrower filter is exactly the omission the derivation exists to prevent.
+ assert.ok(
+ paths.includes('src/**/*.test.ts'),
+ 'the PR lane must trigger on every src test, since test ownership is derived',
+ );
+ assert.match(workflow('mutation-affected.yml'), /mutation:affected --list-affected/);
+ // The lane's own sources fail open into it too: a ratchet or baseline edit must
+ // prove itself against real mutants, not against a stale report.
+ for (const own of ['scripts/mutation/**', 'stryker.config.json', 'mutation-baselines/**']) {
+ assert.ok(paths.includes(own), `missing path filter ${own}`);
+ }
+});
diff --git a/src/utils/__tests__/errors.test.ts b/src/kernel/__tests__/errors.test.ts
similarity index 98%
rename from src/utils/__tests__/errors.test.ts
rename to src/kernel/__tests__/errors.test.ts
index 6c1499aae..a92dfc802 100644
--- a/src/utils/__tests__/errors.test.ts
+++ b/src/kernel/__tests__/errors.test.ts
@@ -1,6 +1,6 @@
import { test } from 'vitest';
import assert from 'node:assert/strict';
-import { AppError, normalizeError, toAppErrorCode } from '../../kernel/errors.ts';
+import { AppError, normalizeError, toAppErrorCode } from '../errors.ts';
test('normalizeError adds default hint and strips diagnostic metadata from details', () => {
const err = new AppError('COMMAND_FAILED', 'runner failed', {
diff --git a/stryker.config.json b/stryker.config.json
new file mode 100644
index 000000000..8768e845e
--- /dev/null
+++ b/stryker.config.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json",
+ "_comment": "Decision-kernel mutation lane (issue #1415). The mutate list below MUST mirror KERNEL_MODULES in scripts/mutation/modules.ts — scripts/mutation/config.test.ts asserts it. Ratchet verdicts come from scripts/mutation/run.ts, never from Stryker's own thresholds. Any edit here changes the config content hash, which invalidates every recorded baseline as non-comparable until re-recorded with `pnpm mutation:baseline`.",
+ "packageManager": "pnpm",
+ "tsconfigFile_comment": "Points at a path that does not exist on purpose. Stryker's sandbox rewrites tsconfig `extends`/`references` through the classic TypeScript API, which the repo's TypeScript 7 native package no longer exposes (`ts.parseConfigFileTextToJson is not a function`). Our tsconfig has neither extends nor references, so there is nothing to rewrite — skipping the preprocessor is exact, not a workaround for a real dependency. The behaviour relied on is that the preprocessor no-ops when the file is absent: see packages/core/src/sandbox/ts-config-preprocessor.ts (https://github.com/stryker-mutator/stryker-js/blob/master/packages/core/src/sandbox/ts-config-preprocessor.ts) and https://stryker-mutator.io/docs/stryker-js/configuration/#tsconfigfile-string. Re-check it on every Stryker upgrade: if a future version errors on a missing tsconfig instead, point this at a real, extends-free tsconfig.",
+ "tsconfigFile": "tsconfig.stryker-absent.json",
+ "plugins_comment": "Named explicitly: the default `@stryker-mutator/*` glob does not resolve under pnpm's non-flat node_modules, so plugin discovery finds no test runner.",
+ "plugins": ["@stryker-mutator/vitest-runner"],
+ "testRunner": "vitest",
+ "vitest": {
+ "configFile": "vitest.mutation.config.ts",
+ "related": true
+ },
+ "reporters": ["progress", "clear-text", "json"],
+ "jsonReporter": {
+ "fileName": ".tmp/mutation/mutation.json"
+ },
+ "htmlReporter": {
+ "fileName": ".tmp/mutation/mutation.html"
+ },
+ "coverageAnalysis": "perTest",
+ "ignoreStatic": true,
+ "tempDirName": ".tmp/stryker",
+ "cleanTempDir": true,
+ "timeoutMS": 20000,
+ "timeoutFactor": 2,
+ "thresholds": {
+ "high": 90,
+ "low": 70,
+ "break": null
+ },
+ "mutate": [
+ "src/kernel/errors.ts",
+ "src/daemon/ref-frame.ts",
+ "src/commands/interaction/runtime/settle.ts",
+ "src/utils/scroll-edge-state.ts",
+ "src/selectors/**/*.ts",
+ "!src/selectors/**/*.test.ts",
+ "!src/selectors/__tests__/**"
+ ]
+}
diff --git a/vitest.config.ts b/vitest.config.ts
index 14db5736b..ab8ffb402 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -10,7 +10,7 @@ import slowTestGateReporter from './scripts/vitest-slow-test-reporter.ts';
// docs/agents/testing.md "tests must not wait real time"). Serialized below with
// per-file isolation so only one such file spawns stubs at a time, the same
// execution contract the pre-split android index.test.ts aggregation provided.
-const SUBPROCESS_STUB_TESTS = [
+export const SUBPROCESS_STUB_TESTS = [
'src/platforms/android/__tests__/{app-lifecycle-install,app-lifecycle-open,device-input-state,input-actions,notifications,settings}.test.ts',
'src/daemon/__tests__/runtime-hints.test.ts',
'src/platforms/apple/core/__tests__/index.test.ts',
diff --git a/vitest.mutation.config.ts b/vitest.mutation.config.ts
new file mode 100644
index 000000000..8f8cd7b87
--- /dev/null
+++ b/vitest.mutation.config.ts
@@ -0,0 +1,26 @@
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { defineConfig } from 'vitest/config';
+import { readTestScope, threadHostileTestFiles } from './scripts/mutation/test-scope.ts';
+import { SUBPROCESS_STUB_TESTS } from './vitest.config.ts';
+
+const repoRoot = path.dirname(fileURLToPath(import.meta.url));
+
+// Test scope for the decision-kernel mutation lane (issue #1415).
+//
+// `scripts/mutation/run.ts` derives the per-run scope from Vitest's module graph
+// (`vitest related` over the mutated files) and hands it over through
+// AGENT_DEVICE_MUTATION_TEST_FILES; the fallback is the deterministic unit suite,
+// which keeps `pnpm exec stryker run` usable by hand. Excluded either way: the
+// subprocess-stub group and the CLI-capture tests — see
+// scripts/mutation/test-scope.ts for why, and why excluding them cannot hide a
+// surviving mutant.
+const scope = readTestScope();
+
+export default defineConfig({
+ test: {
+ include: scope ?? ['src/**/*.test.ts'],
+ exclude: [...SUBPROCESS_STUB_TESTS, ...threadHostileTestFiles(repoRoot), '**/node_modules/**'],
+ setupFiles: ['src/__tests__/hermetic-env-setup.ts', 'src/__tests__/process-memo-setup.ts'],
+ },
+});
]