Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion .fallowrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
Expand All @@ -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}",
Expand Down
46 changes: 46 additions & 0 deletions .github/workflows/lane-health.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Lane Health

# Watches the watchers (issue #1430): every `schedule:`-triggered workflow is
# checked for freshness and for two consecutive failures. Nightly and weekly lanes
# can go dark for weeks — a disabled schedule, a 60-day inactivity suspension, a
# rename — while PR CI stays green, so no other job can notice.
#
# The lane list is derived from .github/workflows/, not hand-maintained, so a lane
# added in the same PR as its first run is already watched.

on:
schedule:
# Daily 06:00 UTC — after the nightlies and the weekly sweep have reported.
- cron: "0 6 * * *"
workflow_dispatch:

permissions:
contents: read
actions: read
issues: write

concurrency:
group: ci-${{ github.workflow }}
cancel-in-progress: true

jobs:
lane-health:
name: Scheduled lane freshness
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Setup toolchain
uses: ./.github/actions/setup-node-pnpm

- name: Self-test
run: pnpm lane-health:test

# Reports to the job summary and pings one tracking issue; it never fails
# the build, because a second red lane nobody watches is not an alert.
- name: Check scheduled lane freshness
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: pnpm lane-health --alert
169 changes: 169 additions & 0 deletions .github/workflows/mutation-affected.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
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.
#
# 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 affected modules
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 decision kernels: $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.module }})
needs: select
if: needs.select.outputs.modules != '[]'
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
module: ${{ 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.module }}
run: pnpm mutation:run --modules ${{ matrix.module }}

- name: Record a failed shard envelope
if: failure()
run: |
pnpm mutation:run --affected --modules ${{ matrix.module }} --fail-envelope \
"shard ${{ matrix.module }} 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.module }}
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
pnpm mutation:check --report-dir .tmp/mutation/shards --affected \
--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
146 changes: 146 additions & 0 deletions .github/workflows/mutation-weekly.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
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.module }})
runs-on: ubuntu-latest
# Budget is 30 minutes per shard; the headroom covers checkout/install so a
# slow runner still reports instead of being killed mid-sweep.
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
# Kept in step with KERNEL_MODULES by the shard-coverage assertion in
# scripts/mutation/workflow.test.ts.
module:
- kernel-errors
- daemon-ref-frame
- interaction-settle
- scroll-edge-state
- selectors
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Setup toolchain
uses: ./.github/actions/setup-node-pnpm

- name: Run mutants for ${{ matrix.module }}
run: pnpm mutation:run --modules ${{ matrix.module }}

# 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.module }} 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.module }}
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 --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 '<details><summary>Lane envelope (schema #1430)</summary>'
echo
echo '```json'
cat .tmp/mutation/lane-envelope.json
echo '```'
echo '</details>'
} >> "$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
Loading
Loading