Skip to content
Merged
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
81 changes: 79 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ jobs:
filter: ${{ steps.scope.outputs.filter }}
standard-packages: ${{ steps.scope.outputs.standard-packages }}
coverage-other-packages: ${{ steps.scope.outputs.coverage-other-packages }}
mutation-targets: ${{ steps.scope.outputs.mutation-targets }}
sdk: ${{ steps.scope.outputs.sdk }}
did: ${{ steps.scope.outputs.did }}
wallet: ${{ steps.scope.outputs.wallet }}
Expand Down Expand Up @@ -103,16 +104,28 @@ jobs:
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE_SHA="${{ github.event.pull_request.base.sha }}"
CHANGED=$(git diff --name-only "${BASE_SHA}"...HEAD)
if echo "$CHANGED" | grep -Eq '^(package.json|pnpm-lock.yaml|pnpm-workspace.yaml|tsconfig.base.json|codecov.yml|sonar-project.properties|\.sonarcloud\.properties|\.github/workflows/(ci|property-tests)\.yml|governance/(repository-health|test-quality)/|scripts/(check-package-artifact|check-wallet-toolbox-platform|repository-health|run-ci-tests|normalize-lcov-paths|run-governed-test|test-governance|sync-service-(rate-limit|edge)-policy)\.(mjs|test\.mjs)$)'; then
if echo "$CHANGED" | grep -Eq '^(package.json|pnpm-lock.yaml|pnpm-workspace.yaml|tsconfig.base.json|codecov.yml|sonar-project.properties|\.sonarcloud\.properties|\.github/workflows/(ci|property-tests|mutation-tests)\.yml|governance/(repository-health|test-quality|mutation-testing)/|scripts/(check-package-artifact|check-wallet-toolbox-platform|repository-health|run-ci-tests|normalize-lcov-paths|run-governed-test|test-governance|mutation-testing|sync-service-(rate-limit|edge)-policy)\.(mjs|test\.mjs)$)'; then
FILTER=""
else
FILTER="...[${BASE_SHA}]"
fi
else
FILTER=""
BEFORE_SHA="${{ github.event.before }}"
if [ -z "$BEFORE_SHA" ] ||
[ "$BEFORE_SHA" = "0000000000000000000000000000000000000000" ] ||
! git cat-file -e "$BEFORE_SHA^{commit}" 2>/dev/null; then
CHANGED=$(git ls-files)
else
CHANGED=$(git diff --name-only "$BEFORE_SHA"...HEAD)
fi
fi

printf '%s\n' "$CHANGED" > .ci-changed-files.txt
echo "filter=$FILTER" >> "$GITHUB_OUTPUT"
MUTATION_TARGETS=$(node scripts/mutation-testing.mjs \
--affected-file .ci-changed-files.txt)
echo "mutation-targets=$MUTATION_TARGETS" >> "$GITHUB_OUTPUT"
if [ -n "$FILTER" ]; then
PROJECTS=$(pnpm -r --filter "$FILTER" list --depth -1 --json)
else
Expand Down Expand Up @@ -209,6 +222,67 @@ jobs:
if-no-files-found: error
retention-days: 1

mutation-tests:
name: Mutation / ${{ matrix.target }}
if: needs.prepare.outputs.mutation-targets != '[]'
needs: prepare
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
strategy:
fail-fast: false
max-parallel: 6
matrix:
target: ${{ fromJSON(needs.prepare.outputs.mutation-targets) }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: pnpm
- name: Install frozen dependencies
run: pnpm install --frozen-lockfile --ignore-scripts
- name: Rebuild the audited workspace build tool
run: pnpm rebuild esbuild
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: build-outputs
path: .ci-artifacts
- name: Restore build outputs
run: tar --extract --gzip --file .ci-artifacts/build-outputs.tar.gz
- name: Enforce the affected mutation-quality ratchet
run: node scripts/mutation-testing.mjs --target "${{ matrix.target }}"
- name: Preserve the machine-readable mutation report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: mutation-${{ matrix.target }}
path: artifacts/mutation/${{ matrix.target }}/mutation.json
if-no-files-found: ignore
retention-days: 30

mutation-quality:
name: Mutation quality
if: always()
needs:
- prepare
- mutation-tests
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Verify the affected mutation targets
env:
PREPARE_RESULT: ${{ needs.prepare.result }}
MUTATION_RESULT: ${{ needs.mutation-tests.result }}
run: |
if [[ "$PREPARE_RESULT" != "success" ||
( "$MUTATION_RESULT" != "success" && "$MUTATION_RESULT" != "skipped" ) ]]; then
echo "::error::Mutation quality failed: prepare=$PREPARE_RESULT mutation=$MUTATION_RESULT"
exit 1
fi

standard-tests:
name: Tests / non-coverage packages
needs: prepare
Expand Down Expand Up @@ -897,6 +971,7 @@ jobs:
needs:
- repository-health
- build-and-test
- mutation-quality
- infra
- docs-validate
- conformance
Expand All @@ -908,17 +983,19 @@ jobs:
env:
HEALTH_RESULT: ${{ needs.repository-health.result }}
BUILD_RESULT: ${{ needs.build-and-test.result }}
MUTATION_RESULT: ${{ needs.mutation-quality.result }}
INFRA_RESULT: ${{ needs.infra.result }}
DOCS_RESULT: ${{ needs.docs-validate.result }}
CONFORMANCE_RESULT: ${{ needs.conformance.result }}
DEPENDENCY_RESULT: ${{ needs.dependency-review.result }}
run: |
if [[ "$HEALTH_RESULT" != "success" ||
"$BUILD_RESULT" != "success" ||
"$MUTATION_RESULT" != "success" ||
"$INFRA_RESULT" != "success" ||
"$DOCS_RESULT" != "success" ||
"$CONFORMANCE_RESULT" != "success" ]]; then
echo "::error::Required CI failed: health=$HEALTH_RESULT build=$BUILD_RESULT infra=$INFRA_RESULT docs=$DOCS_RESULT conformance=$CONFORMANCE_RESULT"
echo "::error::Required CI failed: health=$HEALTH_RESULT build=$BUILD_RESULT mutation=$MUTATION_RESULT infra=$INFRA_RESULT docs=$DOCS_RESULT conformance=$CONFORMANCE_RESULT"
exit 1
fi
if [[ "$DEPENDENCY_RESULT" != "success" && "$DEPENDENCY_RESULT" != "skipped" ]]; then
Expand Down
140 changes: 140 additions & 0 deletions .github/workflows/mutation-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
name: Mutation quality

on:
schedule:
- cron: '17 8 * * 0'
workflow_dispatch:
inputs:
target:
description: Optional exact governed target ID; blank runs all 25
required: false
type: string

permissions: {}

concurrency:
group: mutation-quality-${{ github.ref }}
cancel-in-progress: false

jobs:
prepare:
name: Build mutation campaign
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
outputs:
targets: ${{ steps.targets.outputs.targets }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: pnpm

- name: Install frozen dependencies
run: pnpm install --frozen-lockfile --ignore-scripts

- name: Rebuild the audited workspace build tool
run: pnpm rebuild esbuild

- name: Select the full campaign or one exact target
id: targets
env:
REQUESTED_TARGET: ${{ inputs.target || '' }}
run: |
GOVERNED_TARGETS=$(node scripts/mutation-testing.mjs --list)
if [ -n "$REQUESTED_TARGET" ]; then
if ! grep -Fxq "$REQUESTED_TARGET" <<<"$GOVERNED_TARGETS"; then
echo "::error::Unknown mutation target: $REQUESTED_TARGET"
exit 1
fi
TARGETS=$(jq -cn --arg target "$REQUESTED_TARGET" '[$target]')
else
TARGETS=$(jq -R -s -c 'split("\n") | map(select(length > 0))' <<<"$GOVERNED_TARGETS")
fi
echo "targets=$TARGETS" >> "$GITHUB_OUTPUT"

- name: Build the governed package graph
run: pnpm build:property

- name: Archive build outputs
run: |
{
find packages -type d \( -name dist -o -name out \) -prune -print0
find packages -type f -name '*.tsbuildinfo' -print0
} | tar --null --create --gzip --file build-outputs.tar.gz --files-from=-

- name: Share build outputs
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: mutation-build-outputs
path: build-outputs.tar.gz
if-no-files-found: error
retention-days: 1

mutation-tests:
name: Mutation / ${{ matrix.target }}
needs: prepare
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
strategy:
fail-fast: false
max-parallel: 6
matrix:
target: ${{ fromJSON(needs.prepare.outputs.targets) }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: pnpm

- name: Install frozen dependencies
run: pnpm install --frozen-lockfile --ignore-scripts

- name: Rebuild the audited workspace build tool
run: pnpm rebuild esbuild

- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: mutation-build-outputs
path: .ci-artifacts

- name: Restore build outputs
run: tar --extract --gzip --file .ci-artifacts/build-outputs.tar.gz

- name: Enforce the governed mutation-quality ratchet
run: node scripts/mutation-testing.mjs --target "${{ matrix.target }}"

- name: Preserve the machine-readable mutation report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: mutation-${{ matrix.target }}-${{ github.run_id }}
path: artifacts/mutation/${{ matrix.target }}/mutation.json
if-no-files-found: ignore
retention-days: 30

mutation-quality:
name: Full mutation campaign
if: always()
needs:
- prepare
- mutation-tests
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Verify the campaign
env:
PREPARE_RESULT: ${{ needs.prepare.result }}
MUTATION_RESULT: ${{ needs.mutation-tests.result }}
run: |
if [[ "$PREPARE_RESULT" != "success" || "$MUTATION_RESULT" != "success" ]]; then
echo "::error::Mutation campaign failed: prepare=$PREPARE_RESULT mutation=$MUTATION_RESULT"
exit 1
fi
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ build/
!.env.example
coverage/
.nyc_output/
.stryker-tmp/
artifacts/mutation/
*.log
.DS_Store
.venv/
Expand Down
21 changes: 20 additions & 1 deletion docs/about/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,21 @@ resource-intensive, and intended conformance gaps follow the
Run `pnpm test:governance` before submitting test-classification changes. New
untrusted parsers, codecs, serializers, signature framing, or network
destination logic should include a registered property suite; use
`pnpm test:property` to run the complete reproducible campaign.
`pnpm test:property` to run the complete reproducible campaign. Every property
suite also owns a focused mutation target so its assertions are proven capable
of detecting semantic defects:

```bash
# List the governed targets
pnpm test:mutation --list

# Run the target for the boundary being changed
pnpm test:mutation --target p2p-messages
```

When a mutant survives, add the missing observable assertion or correlated
input case when it represents a real behavior gap. Do not add broad mutation
exclusions or weaken the ratchet to make a pull request pass.

### Lint Code

Expand Down Expand Up @@ -282,13 +296,18 @@ Run before submitting:

```bash
pnpm test
pnpm test:property
pnpm conformance
pnpm lint
pnpm format:check
pnpm health:check
pnpm typecheck
```

Run the affected mutation target while iterating. Root toolchain, SDK,
governance, and CI changes require the complete `pnpm test:mutation --all`
campaign; CI selects this automatically.

## Reporting Issues

When reporting a bug, include:
Expand Down
13 changes: 12 additions & 1 deletion docs/reference/repository-health.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
id: repository-health
title: 'Repository Health Controls'
kind: reference
version: '1.2.0'
version: '1.3.0'
last_updated: '2026-07-26'
last_verified: '2026-07-26'
review_cadence_days: 30
Expand Down Expand Up @@ -79,6 +79,9 @@ contract inventories required skips, classifies every manual/live/resource
suite, rejects empty tests, ratchets all conformance skip groups, and requires
every property-fuzz suite to declare its package, trust boundary, concrete
invariants, minimum PR budget, and reproducible long-run campaign.
`governance/mutation-testing/policy.json` then pairs all 25 property suites with
focused implementation mutation targets, per-boundary score ratchets, zero
uncovered mutants, and zero invalid mutants.

## Commands

Expand All @@ -88,6 +91,7 @@ Run the blocking control and its unit tests:
pnpm health:check
pnpm test:governance
pnpm test:property
pnpm test:mutation --all
pnpm typecheck
```

Expand Down Expand Up @@ -157,6 +161,13 @@ checks:
6. current package-contract findings exactly match the ratcheted snapshot; and
7. the health implementation’s unit tests pass.

Affected package changes also select their matching mutation targets. The
parallel mutation lane restores the shared workspace build, executes only the
focused property and regression tests for each selected implementation
boundary, retains its JSON report, and feeds the required merge gate. SDK,
toolchain, CI, and governance changes deliberately fan out to the full target
registry. A separate scheduled workflow validates all targets weekly.

The job writes a rule-by-rule and project-by-project report to the GitHub Actions
step summary and feeds the required `merge-gate`. Known findings stay visible
until their remediation PR removes them from both the code and baseline.
Expand Down
Loading
Loading