diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index a38ba5b6..b1a3f3f0 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -26,6 +26,7 @@ Makefile は `nix develop --command bash -c "..."` でラップ済み。AI は | マイグレーション | `make migrate` / `make migrate-create MSG="..."` | | インフラ validate | `make infra-validate` | | コード重複検知 | `make dupe-check` (結果: `report/dupe/jscpd-report.json`) | +| ミューテーションテスト | `make mutation-backend` / `make mutation-web` (**長時間**。通常 CI には含まれない週次実行。詳細: 下記「ミューテーションテスト・Slack 通知」) | セットアップ詳細・各コマンドの目的は `docs/development.md` を参照。 @@ -102,6 +103,24 @@ nix develop --command bash -c "cd web && npm run test:e2e" CI 定義: `.github/workflows/ci.yml` +## ミューテーションテスト・Slack 通知(ADR-0017) + +テストの検出力(弱い assertion / 実装なぞり)を週次のミューテーションテストで可視化し、CI 結果は用途別 Slack チャンネルへ通知する。詳細(ローカル実行・レポート確認・Secrets 登録手順)は `docs/development.md`「ミューテーションテスト」「Slack 通知」節が正本。 + +- **ローカル実行**: `make mutation-backend`(mutmut)/ `make mutation-web`(Stryker)。**フル実行は長時間**のため、対象を絞る場合は `nix develop --command bash -c "cd backend && .venv/bin/python -m mutmut run 'app.services.shared.sort_utils*'"` / `nix develop --command bash -c "cd web && npx stryker run --mutate 'src/utils/text.ts'"` +- **対象スコープの正本**: backend = `backend/pyproject.toml` の `[tool.mutmut]`、web = `web/stryker.conf.json`。決定論的ビジネスロジックに限定(schemas / models / routers / 自動生成コード等は対象外) +- **CI**: `.github/workflows/mutation.yml`(週次 月曜 3:00 JST + workflow_dispatch。**PR/push では動かない・fail しない warn-only**) +- **pytest の `--cov` は addopts に戻さない**: mutmut 干渉回避のため Makefile / test.yml の呼び出し側で付与している(ADR-0017) + +| Slack Secret | 用途 | 送信元 workflow | +|---|---|---| +| `SLACK_WEBHOOK_URL_CI` | 通常 CI の失敗のみ | `notify.yml`(workflow_run) | +| `SLACK_WEBHOOK_URL_DEPLOY` | Cloud Run / Cloudflare Pages デプロイ結果(成功・失敗とも) | `notify.yml`(workflow_run) | +| `SLACK_WEBHOOK_URL_QUALITY` | ミューテーションテスト結果(score が閾値未満で ⚠️ 強調) | `mutation.yml` | +| `SLACK_WEBHOOK_URL_DEPS` | Renovate / Dependabot の PR 起票 | `deps-notify.yml`(pull_request_target) | + +Secrets は未登録の間は通知が静かに skip される(CI は green のまま)。チャンネル作成後の登録は `gh secret set SLACK_WEBHOOK_URL_CI --body "https://hooks.slack.com/services/..."`(4 つとも。手順詳細: `docs/development.md`)。 + ## 作業開始時のブランチ運用(デフォルト) **新しい作業に着手するときは、最初に main から作業ブランチを切る。** これはデフォルト挙動であり、合言葉や明示指示を待たない。 diff --git a/.github/workflows/deps-notify.yml b/.github/workflows/deps-notify.yml new file mode 100644 index 00000000..3cb55e97 --- /dev/null +++ b/.github/workflows/deps-notify.yml @@ -0,0 +1,70 @@ +# Renovate / Dependabot が依存更新 PR を起票したら SLACK_WEBHOOK_URL_DEPS へ通知する(ADR-0017)。 +# +# pull_request_target を使う理由: Dependabot が actor の pull_request イベントは +# Actions secrets ではなく Dependabot secrets しか参照できず Webhook が空になる。 +# pull_request_target は base ブランチのコンテキストで実行され通常の secrets を参照できる。 +# PR のコードは一切 checkout / 実行せず(イベントペイロードの文字列のみ扱う)、 +# actor をボットに限定しているため pull_request_target でも安全。 +# PR タイトル等の外部入力は run 内に直接式展開せず env 経由で渡す(インジェクション対策)。 +name: Deps Notify + +on: + pull_request_target: + types: [opened] + +permissions: + contents: read + +jobs: + notify-deps: + runs-on: ubuntu-latest + timeout-minutes: 5 + # github.actor は「最後にイベントを起こした actor」でスプーフ可能なため、 + # PR 作成者(pull_request.user.login)で判定する(zizmor bot-conditions) + if: github.event.pull_request.user.login == 'renovate[bot]' || github.event.pull_request.user.login == 'dependabot[bot]' + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL_DEPS }} + + steps: + - name: 通知メッセージを組み立て + id: compose + if: env.SLACK_WEBHOOK_URL != '' + env: + WF_NAME: ${{ github.workflow }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_URL: ${{ github.event.pull_request.html_url }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_BRANCH: ${{ github.event.pull_request.head.ref }} + PR_ACTOR: ${{ github.event.pull_request.user.login }} + run: | + summary="📦 依存更新 PR: #${PR_NUMBER} ${PR_TITLE}" + body=$(printf '%s\n' \ + "📦 *${WF_NAME}*: ${PR_ACTOR} が依存更新 PR を起票しました" \ + "> #${PR_NUMBER} ${PR_TITLE} (branch: \`${PR_BRANCH}\`)" \ + "<${PR_URL}|→ PR を開く>") + # untrusted 値(PR タイトル)が GITHUB_OUTPUT のデリミタ行と衝突しないよう run 毎にランダム生成する + delim="GHOUT_$(openssl rand -hex 8)" + { + echo "summary<<$delim"; echo "$summary"; echo "$delim" + echo "body<<$delim"; echo "$body"; echo "$delim" + } >> "$GITHUB_OUTPUT" + + - name: Slack 通知 (依存更新 PR) + if: env.SLACK_WEBHOOK_URL != '' + uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.0 + with: + webhook: ${{ env.SLACK_WEBHOOK_URL }} + webhook-type: incoming-webhook + payload: | + { + "text": ${{ toJSON(steps.compose.outputs.summary) }}, + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ${{ toJSON(steps.compose.outputs.body) }} + } + } + ] + } diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml new file mode 100644 index 00000000..f2d2925f --- /dev/null +++ b/.github/workflows/mutation.yml @@ -0,0 +1,273 @@ +# 週次ミューテーションテスト(ADR-0017)。 +# テストスイートの検出力(弱い assertion / 実装なぞりテスト)を可視化する。 +# フル実行が長時間かかるため通常 CI(ci.yml)には組み込まず、週次 schedule と +# 手動実行(workflow_dispatch)のみとする。結果は warn-only(fail させない)で、 +# SLACK_WEBHOOK_URL_QUALITY へ killed / survived / score を通知する。 +name: Mutation + +on: + schedule: + # 毎週月曜 3:00 JST(= 日曜 18:00 UTC) + - cron: "0 18 * * 0" + workflow_dispatch: + +permissions: + contents: read + +env: + # mutation score(%)がこの値未満のとき Slack 通知を ⚠️ で強調する + MUTATION_SCORE_THRESHOLD: "80" + +concurrency: + group: mutation-${{ github.ref }} + cancel-in-progress: false + +jobs: + # Backend: mutmut(対象: backend/pyproject.toml の [tool.mutmut] を参照) + mutation-backend: + runs-on: ubuntu-latest + timeout-minutes: 120 + outputs: + killed: ${{ steps.stats.outputs.killed }} + survived: ${{ steps.stats.outputs.survived }} + score: ${{ steps.stats.outputs.score }} + stats_ok: ${{ steps.stats.outputs.stats_ok }} + + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + # 読み取り専用ジョブのため checkout の認証情報をディスクに残さない(サプライチェーン保護)。 + persist-credentials: false + + - name: Install uv + uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + with: + version: "latest" + enable-cache: true + cache-dependency-glob: "backend/requirements.txt" + + - name: Setup Python + run: uv python install 3.13 + + - name: WeasyPrint 用システムライブラリのインストール + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + libpango-1.0-0 libpangoft2-1.0-0 libpangocairo-1.0-0 \ + libglib2.0-0 libgobject-2.0-0 libffi-dev libcairo2 + + - name: Install backend dependencies + working-directory: backend + run: uv pip install -r requirements.txt + + # 生存ミュータントがあっても集計と通知は行うため continue-on-error にする + - name: Run mutmut + id: run + working-directory: backend + continue-on-error: true + run: uv run mutmut run 2>&1 | tee mutmut-run.log + + # 生存ミュータントの一覧(調査用)。run が途中で落ちても best-effort で出力する + - name: 生存ミュータント一覧を出力 + working-directory: backend + continue-on-error: true + run: uv run mutmut results > mutmut-results.txt + + - name: 結果集計 + id: stats + working-directory: backend + run: | + # export-cicd-stats が mutants/mutmut-cicd-stats.json に統計を書き出す。 + # score の定義(ADR-0017 / Stryker と同基準): + # killed = killed + timeout(無限ループ化もテストによる検出とみなす) + # survived = survived + no_tests(どのテストにも触れられない = 検出不能) + # score = killed * 100 / (killed + survived + suspicious + segfault) + uv run mutmut export-cicd-stats || true + STATS=mutants/mutmut-cicd-stats.json + if [ -f "$STATS" ]; then + killed=$(jq '.killed + .timeout' "$STATS") + survived=$(jq '.survived + .no_tests' "$STATS") + denom=$(jq '.killed + .timeout + .survived + .no_tests + .suspicious + .segfault' "$STATS") + if [ "$denom" -gt 0 ]; then + score=$(( killed * 100 / denom )) + else + score=0 + fi + { + echo "killed=$killed" + echo "survived=$survived" + echo "score=$score" + echo "stats_ok=true" + } >> "$GITHUB_OUTPUT" + else + echo "::warning::mutmut の統計ファイルが見つかりません(run が途中で失敗した可能性)" + { + echo "killed=0" + echo "survived=0" + echo "score=0" + echo "stats_ok=false" + } >> "$GITHUB_OUTPUT" + fi + + - name: レポートを artifact 化 + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: mutmut-report + path: | + backend/mutmut-run.log + backend/mutmut-results.txt + backend/mutants/mutmut-cicd-stats.json + if-no-files-found: warn + retention-days: 14 + + # Frontend: Stryker(対象: web/stryker.conf.json を参照) + mutation-web: + runs-on: ubuntu-latest + timeout-minutes: 60 + outputs: + killed: ${{ steps.stats.outputs.killed }} + survived: ${{ steps.stats.outputs.survived }} + score: ${{ steps.stats.outputs.score }} + stats_ok: ${{ steps.stats.outputs.stats_ok }} + + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false + + - name: Setup Node + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version: "20" + cache: npm + cache-dependency-path: web/package-lock.json + + - name: Install dependencies + run: npm ci --prefix web + + # 生存ミュータントがあっても集計と通知は行うため continue-on-error にする + - name: Run Stryker + id: run + continue-on-error: true + run: npm run test:mutation --prefix web + + - name: 結果集計 + id: stats + run: | + # score の定義(ADR-0017 / mutmut 側と同基準): + # killed = Killed + Timeout + # survived = Survived + NoCoverage + # score = killed * 100 / (killed + survived) + REPORT=web/reports/mutation/mutation.json + if [ -f "$REPORT" ]; then + killed=$(jq '[.files[].mutants[].status | select(. == "Killed" or . == "Timeout")] | length' "$REPORT") + survived=$(jq '[.files[].mutants[].status | select(. == "Survived" or . == "NoCoverage")] | length' "$REPORT") + if [ $(( killed + survived )) -gt 0 ]; then + score=$(( killed * 100 / (killed + survived) )) + else + score=0 + fi + { + echo "killed=$killed" + echo "survived=$survived" + echo "score=$score" + echo "stats_ok=true" + } >> "$GITHUB_OUTPUT" + else + echo "::warning::Stryker のレポートが見つかりません(run が途中で失敗した可能性)" + { + echo "killed=0" + echo "survived=0" + echo "score=0" + echo "stats_ok=false" + } >> "$GITHUB_OUTPUT" + fi + + - name: レポートを artifact 化 + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: stryker-report + path: web/reports/mutation/ + if-no-files-found: warn + retention-days: 14 + + # 結果を SLACK_WEBHOOK_URL_QUALITY へ 1 メッセージで通知する。 + # secrets はジョブ/ステップの if で直接参照できないため、env 経由で空判定する + # (Secret 未登録の間は静かに skip され、workflow は green のまま)。 + notify-quality: + runs-on: ubuntu-latest + timeout-minutes: 5 + needs: [mutation-backend, mutation-web] + if: always() + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL_QUALITY }} + + steps: + - name: 通知メッセージを組み立て + id: compose + if: env.SLACK_WEBHOOK_URL != '' + env: + BE_RESULT: ${{ needs.mutation-backend.result }} + BE_STATS_OK: ${{ needs.mutation-backend.outputs.stats_ok }} + BE_KILLED: ${{ needs.mutation-backend.outputs.killed }} + BE_SURVIVED: ${{ needs.mutation-backend.outputs.survived }} + BE_SCORE: ${{ needs.mutation-backend.outputs.score }} + WEB_RESULT: ${{ needs.mutation-web.result }} + WEB_STATS_OK: ${{ needs.mutation-web.outputs.stats_ok }} + WEB_KILLED: ${{ needs.mutation-web.outputs.killed }} + WEB_SURVIVED: ${{ needs.mutation-web.outputs.survived }} + WEB_SCORE: ${{ needs.mutation-web.outputs.score }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + WORKFLOW: ${{ github.workflow }} + BRANCH: ${{ github.ref_name }} + run: | + threshold="$MUTATION_SCORE_THRESHOLD" + # 全体ステータス: ジョブ失敗/統計欠落 → ❌、いずれかの score が閾値未満 → ⚠️、それ以外 → ✅ + if [ "$BE_RESULT" != "success" ] || [ "$WEB_RESULT" != "success" ] || \ + [ "$BE_STATS_OK" != "true" ] || [ "$WEB_STATS_OK" != "true" ]; then + emoji="❌" + headline="Mutation testing がエラーで完走しませんでした" + elif [ "$BE_SCORE" -lt "$threshold" ] || [ "$WEB_SCORE" -lt "$threshold" ]; then + emoji="⚠️" + headline="*mutation score が閾値(${threshold}%)を下回っています*" + else + emoji="✅" + headline="mutation score は閾値(${threshold}%)以上です" + fi + body=$(printf '%s\n' \ + "${emoji} *${WORKFLOW}* (branch: \`${BRANCH}\`)" \ + "${headline}" \ + "• *Backend (mutmut)* — killed: ${BE_KILLED} / survived: ${BE_SURVIVED} / score: *${BE_SCORE}%*" \ + "• *Web (Stryker)* — killed: ${WEB_KILLED} / survived: ${WEB_SURVIVED} / score: *${WEB_SCORE}%*" \ + "<${RUN_URL}|→ 実行結果とレポート artifact を開く>") + summary="${emoji} Mutation weekly report (${BRANCH})" + # GITHUB_OUTPUT のデリミタ行と本文が衝突しないよう run 毎にランダム生成する(他通知 workflow と統一) + delim="GHOUT_$(openssl rand -hex 8)" + { + echo "summary<<$delim"; echo "$summary"; echo "$delim" + echo "body<<$delim"; echo "$body"; echo "$delim" + } >> "$GITHUB_OUTPUT" + + - name: Slack 通知 (QUALITY) + if: env.SLACK_WEBHOOK_URL != '' + uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.0 + with: + webhook: ${{ env.SLACK_WEBHOOK_URL }} + webhook-type: incoming-webhook + payload: | + { + "text": ${{ toJSON(steps.compose.outputs.summary) }}, + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ${{ toJSON(steps.compose.outputs.body) }} + } + } + ] + } diff --git a/.github/workflows/notify.yml b/.github/workflows/notify.yml new file mode 100644 index 00000000..fdc0381c --- /dev/null +++ b/.github/workflows/notify.yml @@ -0,0 +1,182 @@ +# CI ワークフローの完了を workflow_run で受けて Slack 通知する(ADR-0017)。 +# - notify-ci: CI 失敗時のみ SLACK_WEBHOOK_URL_CI へ通知(cancelled は +# concurrency の cancel-in-progress によるノイズのため通知しない) +# - notify-deploy: main/stg/dev への push で deploy ジョブが実行された場合に +# SLACK_WEBHOOK_URL_DEPLOY へ環境別の成功/失敗を通知(成功も通知する) +# workflow_run の payload にはジョブ単位の結果が含まれないため、jobs API +# (permissions: actions: read + runner 標準搭載の gh CLI)で補完する。 +# secrets はジョブ/ステップの if で直接参照できないため、env 経由で空判定する +# (Secret 未登録の間は静かに skip され、workflow は green のまま)。 +name: Notify + +on: + workflow_run: + workflows: ["CI"] + types: [completed] + +permissions: + contents: read + actions: read + +jobs: + notify-ci: + runs-on: ubuntu-latest + timeout-minutes: 5 + # timeout-minutes 到達時の conclusion は failure ではなく timed_out になるため両方拾う + if: contains(fromJSON('["failure", "timed_out"]'), github.event.workflow_run.conclusion) + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL_CI }} + + steps: + - name: 失敗ジョブ名を収集 + id: failed + if: env.SLACK_WEBHOOK_URL != '' + env: + GH_TOKEN: ${{ github.token }} + RUN_ID: ${{ github.event.workflow_run.id }} + REPO: ${{ github.repository }} + run: | + # gh api の失敗(rate limit / 一時障害)で通知が黙って欠落しないよう明示チェックする + if ! names=$(gh api "repos/${REPO}/actions/runs/${RUN_ID}/jobs?per_page=100" --paginate \ + --jq '[.jobs[] | select(.conclusion == "failure") | .name] | join(", ")'); then + echo "::warning::jobs API の取得に失敗しました(失敗ジョブ名なしで通知します)" + names="⚠️ 失敗ジョブ名の取得に失敗しました" + fi + echo "names=${names:-(ジョブ単位の失敗なし)}" >> "$GITHUB_OUTPUT" + + - name: 通知メッセージを組み立て + id: compose + if: env.SLACK_WEBHOOK_URL != '' + env: + # PR タイトル等の外部入力を含みうる値は run 内に直接式展開せず env 経由で渡す(インジェクション対策) + WF_NAME: ${{ github.event.workflow_run.name }} + TITLE: ${{ github.event.workflow_run.display_title }} + BRANCH: ${{ github.event.workflow_run.head_branch }} + EVENT: ${{ github.event.workflow_run.event }} + RUN_URL: ${{ github.event.workflow_run.html_url }} + FAILED_JOBS: ${{ steps.failed.outputs.names }} + run: | + summary="❌ ${WF_NAME} failed (${BRANCH})" + body=$(printf '%s\n' \ + "❌ *${WF_NAME}* failed (branch: \`${BRANCH}\` / event: ${EVENT})" \ + "> ${TITLE}" \ + "失敗ジョブ: ${FAILED_JOBS}" \ + "<${RUN_URL}|→ 失敗した Run を開く>") + # untrusted 値(display_title 等)が GITHUB_OUTPUT のデリミタ行と衝突しないよう run 毎にランダム生成する + delim="GHOUT_$(openssl rand -hex 8)" + { + echo "summary<<$delim"; echo "$summary"; echo "$delim" + echo "body<<$delim"; echo "$body"; echo "$delim" + } >> "$GITHUB_OUTPUT" + + - name: Slack 通知 (CI 失敗) + if: env.SLACK_WEBHOOK_URL != '' + uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.0 + with: + webhook: ${{ env.SLACK_WEBHOOK_URL }} + webhook-type: incoming-webhook + payload: | + { + "text": ${{ toJSON(steps.compose.outputs.summary) }}, + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ${{ toJSON(steps.compose.outputs.body) }} + } + } + ] + } + + notify-deploy: + runs-on: ubuntu-latest + timeout-minutes: 5 + # deploy ジョブ(ci.yml の deploy-dev / deploy-stg / deploy-prod)は + # main/stg/dev への push でのみ実行される。PR の run はここで除外する + if: > + github.event.workflow_run.event == 'push' && + contains(fromJSON('["main", "stg", "dev"]'), github.event.workflow_run.head_branch) + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL_DEPLOY }} + + steps: + - name: デプロイジョブの結果を収集 + id: deploys + if: env.SLACK_WEBHOOK_URL != '' + env: + GH_TOKEN: ${{ github.token }} + RUN_ID: ${{ github.event.workflow_run.id }} + REPO: ${{ github.repository }} + run: | + # 再利用ワークフロー(deploy.yml)内のジョブは "deploy-dev / deploy-web" の + # 形式で列挙される。"tests / deploy-gate" は startswith にマッチしない。 + # skipped(deploy_gate 不通過・変更なし等)は通知対象にしない。 + # gh api の失敗でデプロイ結果通知が黙ってスキップ(count=0)されないよう明示チェックする + if ! lines=$(gh api "repos/${REPO}/actions/runs/${RUN_ID}/jobs?per_page=100" --paginate \ + --jq '[.jobs[] | select(.name | startswith("deploy-")) + | select(.conclusion == "success" or .conclusion == "failure") + | "\(if .conclusion == "success" then "✅" else "❌" end) \(.name)"] | join("\n")'); then + echo "::warning::jobs API の取得に失敗しました(デプロイ結果を取得できず通知します)" + lines="⚠️ deploy ジョブ結果の取得に失敗しました" + fi + count=$(printf '%s' "$lines" | grep -c . || true) + # ジョブ名等が GITHUB_OUTPUT のデリミタ行と衝突しないよう run 毎にランダム生成する(他の出力と統一) + delim="GHOUT_$(openssl rand -hex 8)" + { + echo "count=$count" + echo "lines<<$delim" + echo "$lines" + echo "$delim" + } >> "$GITHUB_OUTPUT" + + - name: 通知メッセージを組み立て + id: compose + if: env.SLACK_WEBHOOK_URL != '' && steps.deploys.outputs.count != '0' + env: + WF_NAME: ${{ github.event.workflow_run.name }} + TITLE: ${{ github.event.workflow_run.display_title }} + BRANCH: ${{ github.event.workflow_run.head_branch }} + RUN_URL: ${{ github.event.workflow_run.html_url }} + DEPLOY_LINES: ${{ steps.deploys.outputs.lines }} + run: | + if printf '%s' "$DEPLOY_LINES" | grep -q '❌'; then + emoji="❌" + elif printf '%s' "$DEPLOY_LINES" | grep -q '⚠️'; then + emoji="⚠️" + else + emoji="✅" + fi + summary="${emoji} Deploy (${BRANCH})" + body=$(printf '%s\n' \ + "${emoji} *${WF_NAME}* deploy result (branch: \`${BRANCH}\`)" \ + "> ${TITLE}" \ + "$DEPLOY_LINES" \ + "<${RUN_URL}|→ Run を開く>") + # untrusted 値(display_title 等)が GITHUB_OUTPUT のデリミタ行と衝突しないよう run 毎にランダム生成する + delim="GHOUT_$(openssl rand -hex 8)" + { + echo "summary<<$delim"; echo "$summary"; echo "$delim" + echo "body<<$delim"; echo "$body"; echo "$delim" + } >> "$GITHUB_OUTPUT" + + # count=0(deploy ジョブが 1 つも実行されなかった run)は通知しない + - name: Slack 通知 (デプロイ結果) + if: env.SLACK_WEBHOOK_URL != '' && steps.deploys.outputs.count != '0' + uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.0 + with: + webhook: ${{ env.SLACK_WEBHOOK_URL }} + webhook-type: incoming-webhook + payload: | + { + "text": ${{ toJSON(steps.compose.outputs.summary) }}, + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ${{ toJSON(steps.compose.outputs.body) }} + } + } + ] + } diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 98b59e41..3efd7161 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -202,7 +202,8 @@ jobs: - name: Run backend tests working-directory: backend - run: uv run pytest -q tests + # --cov は pyproject の addopts ではなくここで付与する(mutmut 干渉回避 / ADR-0017) + run: uv run pytest -q --cov=app --cov-report=term-missing tests # OpenAPI → TypeScript 型のドリフト検知 (ADR-0007)。 # backend schema を変更したのに web/src/api/generated.ts を再生成して diff --git a/.gitignore b/.gitignore index b5547dd8..50325bc1 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,11 @@ backend/.env # AI レビュー出力(BE_refacter / WEB_refacter skill が書き出す) report/ +# Mutation testing の生成物(ADR-0017) +backend/mutants/ +web/reports/ +web/.stryker-tmp/ + # OS .DS_Store .direnv/ diff --git a/Makefile b/Makefile index 013f8f2a..0e845fb8 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: help \ setup install-hooks install-backend install-web generate-keys \ dev dev-build dev-down dev-amd64 dev-amd64-build dev-web preview-web dev-proxy dev-proxy-only stripe-webhook \ - test test-backend test-web \ + test test-backend test-web mutation-backend mutation-web \ lint lint-backend typecheck-backend lint-web lint-web-messages lint-env-keys lint-fix \ format format-check \ ci \ @@ -38,6 +38,8 @@ help: @echo " test 全テスト (backend + web)" @echo " test-backend Backend: pytest" @echo " test-web Frontend: vitest" + @echo " mutation-backend Backend: mutmut (長時間。結果閲覧: cd backend && .venv/bin/python -m mutmut browse)" + @echo " mutation-web Frontend: Stryker (長時間。レポート: web/reports/mutation/)" @echo " lint 全リント (backend + web)" @echo " lint-backend Backend: ruff check" @echo " typecheck-backend Backend: pyright 型チェック" @@ -136,12 +138,22 @@ ci: lint test build-web test: test-backend test-web +# --cov は pyproject の addopts ではなくここで付与する(mutmut 干渉回避 / ADR-0017) test-backend: - nix develop --command bash -c "cd backend && .venv/bin/python -m pytest -q tests" + nix develop --command bash -c "cd backend && .venv/bin/python -m pytest -q --cov=app --cov-report=term-missing tests" test-web: nix develop --command bash -c "cd web && npm test" +# ミューテーションテスト(ADR-0017)。フル実行は長時間かかるため通常 CI には含めず、 +# 週次の .github/workflows/mutation.yml で実行する。対象は pyproject [tool.mutmut] / +# web/stryker.conf.json を参照。 +mutation-backend: + nix develop --command bash -c "cd backend && .venv/bin/python -m mutmut run" + +mutation-web: + nix develop --command bash -c "cd web && npm run test:mutation" + lint: lint-backend typecheck-backend lint-web lint-web-messages lint-env-keys lint-backend: diff --git a/backend/pyproject.toml b/backend/pyproject.toml index d6d3bcff..4a1f9fc4 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -1,8 +1,29 @@ [tool.pytest.ini_options] pythonpath = ["."] -addopts = "--cov=app --cov-report=term-missing -p no:cacheprovider" +# --cov は Makefile / CI 側の pytest 呼び出しで付与する。addopts に置くと mutmut が +# mutants/ 内で pytest を内部実行する際にも全ミュータントへ coverage 計測が載り干渉するため(ADR-0017) +addopts = "-p no:cacheprovider" testpaths = ["tests"] +# ミューテーションテスト(週次 CI: .github/workflows/mutation.yml / ADR-0017) +# 対象は高価値な決定論的ビジネスロジックに限定する。source_paths の app 全体は +# mutants/ へコピーされる(import・JSON リソース解決のため)が、変異を加えるのは +# only_mutate のグロブ(fnmatch。"*" は "/" にもマッチ)に一致するファイルのみ。 +# schemas / models / routers / alembic_migrations / prompts / LLM クライアント等の +# I/O 層・宣言層は対象外(テストで殺せないミュータントがノイズになるため) +[tool.mutmut] +source_paths = ["app"] +only_mutate = [ + "app/services/blog/scorer.py", # ブログ技術記事スコアリング + "app/services/billing/pricing.py", # 課金レート表 + "app/services/billing/credit_service.py", # クレジット計算・残高 + "app/services/intelligence/skills/*", # スキル推論(aggregator/linguist/パーサ群) + "app/services/intelligence/github/repo_analyzer.py", # リポジトリ解析 + "app/services/intelligence/github/contributions.py", # コントリビューション集計 + "app/services/shared/*", # sort_utils / resume_format + "app/services/tasks/worker.py", # タスク状態遷移 +] + [tool.ruff] line-length = 100 target-version = "py313" diff --git a/backend/requirements.txt b/backend/requirements.txt index 7608a012..cb17f2cc 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -39,3 +39,5 @@ google-cloud-tasks==2.23.0 pyasn1==0.6.3 # CVE-2026-53538 / CVE-2026-53539 / CVE-2026-53540 対策で 0.0.31 へ更新 python-multipart==0.0.32 +# ミューテーションテスト(週次 CI: .github/workflows/mutation.yml / ADR-0017) +mutmut==3.6.0 diff --git a/backend/tests/test_error_code_sync.py b/backend/tests/test_error_code_sync.py index f49cb584..2cac1dad 100644 --- a/backend/tests/test_error_code_sync.py +++ b/backend/tests/test_error_code_sync.py @@ -12,9 +12,25 @@ from app.core.errors import ErrorCode -# backend/tests/ から見たリポジトリルート(monorepo 前提で frontend が隣接する) -_REPO_ROOT = Path(__file__).resolve().parents[2] -_ERROR_CODES_TS = _REPO_ROOT / "web" / "src" / "constants" / "errorCodes.ts" +# errorCodes.ts のリポジトリルートからの相対パス(monorepo 前提で frontend が隣接する) +_ERROR_CODES_TS_RELATIVE = Path("web") / "src" / "constants" / "errorCodes.ts" + + +def _locate_error_codes_ts() -> Path: + """祖先ディレクトリを遡って FE の ``errorCodes.ts`` を探す。 + + mutmut がテストを ``backend/mutants/tests/`` へコピーして実行する際は + 本ファイルの深さが 1 段増えるため、固定段数の ``parents[N]`` では + リポジトリルートを特定できない(ADR-0017)。 + """ + here = Path(__file__).resolve() + for candidate in here.parents: + target = candidate / _ERROR_CODES_TS_RELATIVE + if target.exists(): + return target + raise FileNotFoundError( + f"FE のエラーコード定義が祖先ディレクトリに見つからない: {_ERROR_CODES_TS_RELATIVE}" + ) # `export const ERROR_CODES = [ ... ] as const;` のブロックを取り出す _ARRAY_BLOCK = re.compile(r"ERROR_CODES\s*=\s*\[(.*?)\]\s*as\s+const", re.DOTALL) @@ -24,10 +40,7 @@ def _parse_frontend_error_codes() -> set[str]: """FE の ``errorCodes.ts`` から ``ERROR_CODES`` の値集合を抽出する。""" - assert _ERROR_CODES_TS.exists(), ( - f"FE のエラーコード定義が見つからない: {_ERROR_CODES_TS}" - ) - source = _ERROR_CODES_TS.read_text(encoding="utf-8") + source = _locate_error_codes_ts().read_text(encoding="utf-8") match = _ARRAY_BLOCK.search(source) assert match is not None, ( "errorCodes.ts に `ERROR_CODES = [...] as const` ブロックが見つからない" diff --git a/docs/adr/0017-mutation-testing-and-slack-notifications.md b/docs/adr/0017-mutation-testing-and-slack-notifications.md new file mode 100644 index 00000000..72b38aff --- /dev/null +++ b/docs/adr/0017-mutation-testing-and-slack-notifications.md @@ -0,0 +1,98 @@ +# ADR-0017: ミューテーションテスト週次実行と Slack 通知チャンネル分割 + +## ステータス + +Accepted + +## コンテキスト + +- backend のテストスイートが 500 件規模(51 ファイル・約 670 テスト)に成長し、行カバレッジ(86%)だけでは「実装をなぞっているだけのテスト」「assertion が弱いテスト」を検出できなくなった。テストの**検出力**そのものを測る仕組みが必要。 +- GitHub 通知(メール / Web)は流量が多くノイズに埋もれるため、Slack へ移行し通知の種類ごとにチャンネルを分けたい。 +- ミューテーションテストはフル実行に数十分〜時間単位かかるため、PR ごとの CI(ci.yml)に入れるとブロッキングチェックとして開発体験を大きく損なう。 + +## 決定内容 + +### ツール選定 + +| 領域 | ツール | 実行系 | +|---|---|---| +| backend | mutmut 3.x(`requirements.txt` で固定) | pytest を `mutants/` 内で in-process 実行 | +| web | Stryker(`@stryker-mutator/core` + `@stryker-mutator/vitest-runner`) | 既存 vitest(`vite.config.ts` 内蔵設定)をそのまま利用 | + +- **backend は uv 管理ではない**(pyproject に `[project]` なし・lockfile は `requirements.txt`)ため、mutmut も既存慣例どおり `requirements.txt` にバージョン固定で追加する。 +- Stryker の TS checker は使わない(tsconfig が project-references + noEmit 構成のため)。ランナーは vitest-runner(peer: `vitest >=2.0.0`、vitest 4 対応確認済み)。 + +### 対象スコープ(決定論的ビジネスロジックに限定) + +I/O 層・宣言層(schemas / models / routers / LLM クライアント / 自動生成コード / 定数カタログ)はテストで殺せないミュータントがノイズになるため対象外とし、テストで検証すべき純ロジックに絞る。 + +- backend(`pyproject.toml` の `[tool.mutmut]` が正本): blog スコアリング、billing(料金・クレジット計算)、skills 推論(aggregator / linguist / パーサ群)、repo_analyzer / contributions、shared(sort_utils / resume_format)、tasks/worker(状態遷移) +- web(`web/stryker.conf.json` が正本): `src/utils/**`、`formMappers.ts`、`payloadBuilders.ts`、`src/hooks/**`、store の slice、`constants/agentModels.ts` + +### 実行タイミングと fail ポリシー + +- 週次(毎週月曜 3:00 JST = cron `0 18 * * 0`)+ `workflow_dispatch` の専用ワークフロー `.github/workflows/mutation.yml`。backend / web は並列ジョブ。 +- **warn-only**(jscpd の Phase 1 と同じ方針): 生存ミュータントがあってもワークフローは fail させず、Slack 通知とレポート artifact(14 日保持)で可視化する。閾値ゲート化は baseline 確定後に検討。 + +### score の定義(backend / web 共通基準) + +``` +killed = killed + timeout # 無限ループ化もテストによる検出とみなす(Stryker と同基準) +survived = survived + no_tests # どのテストにも触れられない = 検出不能(Stryker の NoCoverage 相当) +score = killed * 100 / (killed + survived + suspicious + segfault) +``` + +閾値は `mutation.yml` の env `MUTATION_SCORE_THRESHOLD`(初期値 80%)。未満なら Slack 通知を ⚠️ で強調する。 + +### pytest-cov との干渉回避 + +mutmut は `mutants/` に app / tests / pyproject.toml をコピーし pytest を in-process 実行する。pyproject の `addopts` に `--cov` があると全ミュータント実行に coverage 計測が載り干渉するため、**`--cov=app --cov-report=term-missing` は addopts から外し、呼び出し側(Makefile `test-backend` / test.yml の pytest step)で付与する**。カバレッジ出力は従来どおり維持される。 + +### Slack 通知チャンネル分割(4 Webhook) + +| Secret 名 | 用途 | 送信元 workflow | 通知条件 | +|---|---|---|---| +| `SLACK_WEBHOOK_URL_CI` | 通常 CI の失敗 | `notify.yml` | CI conclusion = failure のみ(cancelled は concurrency キャンセルのノイズのため通知しない) | +| `SLACK_WEBHOOK_URL_DEPLOY` | Cloud Run / Cloudflare Pages デプロイ結果 | `notify.yml` | main/stg/dev への push で deploy ジョブが実行された run(成功・失敗とも) | +| `SLACK_WEBHOOK_URL_QUALITY` | ミューテーションテスト結果 | `mutation.yml` | 週次 / 手動実行の完了時(killed / survived / score%、閾値未満は ⚠️ 強調) | +| `SLACK_WEBHOOK_URL_DEPS` | 依存更新 PR の起票 | `deps-notify.yml` | renovate[bot] / dependabot[bot] の PR opened | + +- 送信は `slackapi/slack-github-action` v2(既存慣例どおり commit SHA ピン。Renovate `helpers:pinGitHubActionDigests` が以後管理)。 +- **Secrets はプレースホルダー**: Slack チャンネル未作成のため値は未登録。各通知ステップは `env` 経由の空判定で **Secret 未登録の間は静かに skip** され、ワークフローは green のまま(登録手順は `docs/development.md`)。 + +### 通知の実装方式 + +- **CI / デプロイ通知は `workflow_run`**(`workflows: ["CI"]` / `types: [completed]`)で受ける。ci.yml 側には一切手を入れず疎結合にする。workflow_run の payload にはジョブ単位の結果が無いため、jobs API(`permissions: actions: read` + runner 標準の `gh` CLI)で失敗ジョブ名・deploy ジョブ結果(再利用ワークフローは `deploy-dev / deploy-web` 形式で列挙)を補完する。 +- **依存更新通知は `pull_request_target` [opened]**: Dependabot actor の `pull_request` イベントは Actions secrets を参照できない(Dependabot secrets に分離される)ため。PR コードは checkout せず、actor をボットに限定しているため安全。 +- **インジェクション対策**: PR タイトル・display_title 等の外部入力は `run` スクリプトへ直接式展開せず `env` 経由で渡し、Slack payload へは `toJSON()` でエスケープして埋め込む。 + +## 代替案 + +- **mutmut ではなく cosmic-ray(backend)**: 並列実行は強いが設定が重く、pytest との統合と結果の可読性(`mutmut browse` / `export-cicd-stats`)で mutmut が優位。 +- **PR ごとの差分ミューテーション(incremental)**: 検出は早いが実行時間が PR CI を直撃する。まず週次で baseline を作る方が導入コストが低い。 +- **通知を ci.yml 内のジョブとして追加**: workflow_run より実装が単純だが、ci.yml / test.yml(再利用ワークフロー)に secrets 配線が増え、通知都合の変更が CI 本体の変更履歴に混ざる。疎結合な workflow_run を採用。 +- **DEPS 通知を `pull_request` トリガーで実装**: Dependabot の secrets 分離で Webhook が参照できず不成立。 +- **Slack App(chat.postMessage)方式**: チャンネル増減に強いが Bot トークン管理が必要。まず Incoming Webhook のチャンネル分割で開始し、必要になれば移行する。 + +## トレードオフ・既知のリスク + +- **実行時間**: backend は実 SQLite に当てる統合テスト主体のため、フル実行が 120 分の timeout に達する可能性がある。その場合は `[tool.mutmut]` の対象を削るか `mutate_only_covered_lines` を検討する。 +- **warn-only 期間はスコア悪化を強制できない**: ゲート化までは Slack の ⚠️ 通知と週次レビュー運用が頼り。 +- **`slackapi/slack-github-action` の SHA は初回実行で要検証**: 導入時の実行環境から外部リポジトリの SHA 解決ができず、`v2.1.0` の SHA は事後検証前提でピンしている。初回の workflow_dispatch で action 解決に失敗した場合は SHA を修正する(Renovate の `pinDigests` が以後のバージョン追従を管理)。 +- **workflow_run のジョブ名依存**: deploy 通知は「ジョブ名が `deploy-` で始まる」ことに依存する。ci.yml のジョブ名を変更する場合は notify.yml の jq フィルタも見直すこと。 +- **週次 cron は main ブランチの定義でのみ発火**: feature ブランチ上の mutation.yml は schedule では動かない(workflow_dispatch で検証する)。 + +## 将来の移行条件 + +- baseline が安定したら `MUTATION_SCORE_THRESHOLD` を実測に合わせて調整し、`thresholds.break`(Stryker)/ CI ゲート化(mutmut)へ移行する。 +- 通知チャンネルが増えたら Incoming Webhook 方式から Slack App(Bot トークン + チャンネル指定)へ移行する。 +- backend が uv 管理(PEP 621 + uv.lock)へ移行した場合、mutmut は dev dependency group(`uv add --dev`)へ移す。 + +## 関連リンク + +- `.github/workflows/mutation.yml` / `notify.yml` / `deps-notify.yml` +- `backend/pyproject.toml`(`[tool.mutmut]` / pytest addopts) +- `web/stryker.conf.json` +- `docs/development.md`「ミューテーションテスト」節(ローカル実行・Secrets 登録手順) +- ADR-0014(Renovate。Action の SHA ピン運用) +- mutmut: https://mutmut.readthedocs.io/ / Stryker: https://stryker-mutator.io/docs/ diff --git a/docs/development.md b/docs/development.md index 162f2a15..aeb96b11 100644 --- a/docs/development.md +++ b/docs/development.md @@ -159,3 +159,62 @@ make infra-validate # dev / stg / prod を順に validate ``` 詳細は [deployment.md](./deployment.md) の「インフラ構成(OpenTofu)」を参照。 + +### ミューテーションテスト(mutmut / Stryker)— 週次 + +テストスイートの検出力(弱い assertion / 実装なぞりテスト)を測る(設計判断: [ADR-0017](./adr/0017-mutation-testing-and-slack-notifications.md))。 +**フル実行は長時間かかるため通常 CI には含まれない。** 週次(毎週月曜 3:00 JST)の `.github/workflows/mutation.yml` が実行し、結果を Slack(`SLACK_WEBHOOK_URL_QUALITY`)へ通知する。手動実行は GitHub Actions の workflow_dispatch(`gh workflow run mutation.yml`)。 + +```bash +make mutation-backend # mutmut(対象: backend/pyproject.toml の [tool.mutmut]) +make mutation-web # Stryker(対象: web/stryker.conf.json) +``` + +ローカルで短時間だけ試す場合(対象を絞る): + +```bash +# backend: ミュータント名のグロブで絞る(モジュールパス + '*') +nix develop --command bash -c "cd backend && .venv/bin/python -m mutmut run 'app.services.shared.sort_utils*'" +# web: --mutate でファイルを絞る +nix develop --command bash -c "cd web && npx stryker run --mutate 'src/utils/text.ts'" +``` + +結果の確認: + +```bash +# backend: 生存ミュータント一覧 / TUI ブラウズ / CI 用 JSON(mutants/mutmut-cicd-stats.json) +nix develop --command bash -c "cd backend && .venv/bin/python -m mutmut results" +nix develop --command bash -c "cd backend && .venv/bin/python -m mutmut browse" +# web: HTML レポート +open web/reports/mutation/mutation.html +``` + +- 生成物(`backend/mutants/` / `web/reports/` / `web/.stryker-tmp/`)は gitignore 済み。 +- score が `MUTATION_SCORE_THRESHOLD`(初期値 80%。`mutation.yml` の env)未満だと Slack 通知が ⚠️ で強調される。Phase 1 は warn-only(fail しない)。 + +## Slack 通知(CI / デプロイ / 品質 / 依存更新) + +GitHub Actions の結果を用途別の Slack チャンネルへ Incoming Webhook で通知する([ADR-0017](./adr/0017-mutation-testing-and-slack-notifications.md))。 + +| GitHub Secret | 用途 | 送信元 workflow | +|---|---|---| +| `SLACK_WEBHOOK_URL_CI` | 通常 CI の失敗のみ | `notify.yml` | +| `SLACK_WEBHOOK_URL_DEPLOY` | Cloud Run / Cloudflare Pages デプロイ結果(成功・失敗とも) | `notify.yml` | +| `SLACK_WEBHOOK_URL_QUALITY` | 週次ミューテーションテスト結果(killed / survived / score) | `mutation.yml` | +| `SLACK_WEBHOOK_URL_DEPS` | Renovate / Dependabot の依存更新 PR 起票 | `deps-notify.yml` | + +### Secrets の登録手順(チャンネル作成後) + +1. Slack で通知先チャンネルを作成し、各チャンネルに [Incoming Webhook](https://api.slack.com/messaging/webhooks) を発行する(チャンネルごとに 1 つ) +2. 発行された Webhook URL を GitHub Secrets に登録する: + + ```bash + gh secret set SLACK_WEBHOOK_URL_CI --body "https://hooks.slack.com/services/..." + gh secret set SLACK_WEBHOOK_URL_DEPLOY --body "https://hooks.slack.com/services/..." + gh secret set SLACK_WEBHOOK_URL_QUALITY --body "https://hooks.slack.com/services/..." + gh secret set SLACK_WEBHOOK_URL_DEPS --body "https://hooks.slack.com/services/..." + ``` + +3. 動作確認: `gh workflow run mutation.yml` を手動実行し QUALITY チャンネルへの通知を確認する + +> **Secret 未登録の間の挙動**: 各通知ステップは Webhook が空だと静かに skip される(ワークフローは green のまま)。チャンネル作成前に導入しても CI は壊れない。 diff --git a/web/package-lock.json b/web/package-lock.json index 86600083..bc90b536 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -22,6 +22,8 @@ "devDependencies": { "@eslint/js": "^10.0.0", "@playwright/test": "^1.56.0", + "@stryker-mutator/core": "^9.6.1", + "@stryker-mutator/vitest-runner": "^9.6.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", @@ -180,6 +182,19 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-compilation-targets": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", @@ -197,6 +212,28 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@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/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/helper-globals": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", @@ -207,6 +244,20 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", @@ -239,6 +290,61 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", @@ -299,6 +405,163 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.7.tgz", + "integrity": "sha512-EtU0Hi3GvrTqD56xKmZvV/uCXK2ZbwVNPNLAquVItcAZpUhkXwWlo3Fmj0c2LxgSf2I8IDULeAepwNP1OefLXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-decorators": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.29.7.tgz", + "integrity": "sha512-9MTTLbF39X6sqM92JPEsoI7++26hjZvzkxKZy64aMhWLH2mPkJ/Q3AV4QLmls3R14FpSpkOwQQfUh962JGQxxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz", + "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.29.7.tgz", + "integrity": "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", + "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^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" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", + "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/runtime": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", @@ -1441,9 +1704,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1461,9 +1721,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1481,9 +1738,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1501,9 +1755,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1521,9 +1772,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1541,9 +1789,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1561,9 +1806,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1581,9 +1823,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1601,9 +1840,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1627,9 +1863,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1653,9 +1886,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1679,9 +1909,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1705,9 +1932,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1731,9 +1955,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1757,9 +1978,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1783,9 +2001,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1891,6 +2106,30 @@ "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" } }, + "node_modules/@inquirer/checkbox": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.1.tgz", + "integrity": "sha512-b6xmA/VlTe0ZgDQHDui+Nav470u7u49nRd8/iuhOcQPO9Ch7lGuogydhi2VOmNlZ+zXcM8IcPuNSwQcdJaF/kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@inquirer/confirm": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.1.1.tgz", @@ -1940,22 +2179,39 @@ } } }, - "node_modules/@inquirer/figures": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.7.tgz", - "integrity": "sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==", + "node_modules/@inquirer/editor": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.2.2.tgz", + "integrity": "sha512-ZRVd/oD+sYsUd5zVm0NflqEzlqfYCyHNsqkHl2oWXEUHs12tCbcSFi+wVFEvD8+LGRaMUsVrE7qeo6lSG/S1Vg==", "dev": true, "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/external-editor": "^3.0.3", + "@inquirer/type": "^4.0.7" + }, "engines": { "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@inquirer/type": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.7.tgz", - "integrity": "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==", + "node_modules/@inquirer/expand": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.1.tgz", + "integrity": "sha512-YmQpenjbFSHAK3sOd44puHh3V1KXXr+JiNpUztoSQ4drLh2rTVzTap/YtlAVu/5xavifIlBfNEzJ/neZJ1a/1g==", "dev": true, "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, "engines": { "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, @@ -1968,15 +2224,231 @@ } } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "node_modules/@inquirer/external-editor": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.3.tgz", + "integrity": "sha512-6thf5I8q7lZwzGLAxPaaGEREEkZ3nyePPDQ1oyobblxmEE8mqTLguScP7pDjUTAibiyb4hfXl+qjUEJ+di/aNA==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "chardet": "^2.1.1", + "iconv-lite": "^0.7.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.7.tgz", + "integrity": "sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/input": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.2.tgz", + "integrity": "sha512-9K/DDBSQpOyZSkt6sOVP9Vo0TR7atX2kuILsUu0x3wVcVbe97lJwIJKMLdMw25tDYuXl/qp6erT0Xs1rfmcfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.1.1.tgz", + "integrity": "sha512-XF4IXAbPnGPgw0wsbC/i2tPcyfdZgDpUlhsqU0SfT4IRIGWha6Xm9VRgN5yYxJq+jnyXlfXI/nQ3ulfk0iEICA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.1.1.tgz", + "integrity": "sha512-3XBfF7DAsp5qeDsvN5Rd1HmbNokVvEQoUM0QLrRcybC9nX96w3Pbmu7qUsb3IT3J3jBvs2+mTXaKHOUsgHMLzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.5.2.tgz", + "integrity": "sha512-IYR/3C/paEVVQYQvdDlFZVjRCJVYHHON0XXMH91KO9GSxs0TdKYWlUdvfQl2EfAHDxUaN3IBffkE/BDTh5nJ6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^5.2.1", + "@inquirer/confirm": "^6.1.1", + "@inquirer/editor": "^5.2.2", + "@inquirer/expand": "^5.1.1", + "@inquirer/input": "^5.1.2", + "@inquirer/number": "^4.1.1", + "@inquirer/password": "^5.1.1", + "@inquirer/rawlist": "^5.3.1", + "@inquirer/search": "^4.2.1", + "@inquirer/select": "^5.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.3.1.tgz", + "integrity": "sha512-QqdTqQddL3qPX/PPrjobpsO25NZ4dWXgTLenrR445L2ptLEYE6Z+PD5c5CNDJNx4ugRgELAIpSIJxZaO2jJ2Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.2.1.tgz", + "integrity": "sha512-xJj8QWKRSrfKoBIITLZK61dD3zwo0Rz11fgDImku30/Oe81zMdIdGgrLY2h6RkJ+KZ/GhNYIRMKnH/62qBTA5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.2.1.tgz", + "integrity": "sha512-FlDndEUww8m7BfukO2nJa25vhD+H5jxxCv4oGioKqzyWz3nPHhhw4LKdYRSlXuAx7DsdWia7iyaBPKKS95Evfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.7.tgz", + "integrity": "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, "node_modules/@jridgewell/remapping": { @@ -2160,9 +2632,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2183,9 +2652,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2206,9 +2672,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2229,9 +2692,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2252,9 +2712,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2609,9 +3066,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2629,9 +3083,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2649,9 +3100,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2669,9 +3117,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2689,9 +3134,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2709,9 +3151,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2798,6 +3237,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, "node_modules/@sindresorhus/is": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", @@ -2811,6 +3257,19 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@speed-highlight/core": { "version": "1.2.17", "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.17.tgz", @@ -2830,6 +3289,177 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, + "node_modules/@stryker-mutator/api": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@stryker-mutator/api/-/api-9.6.1.tgz", + "integrity": "sha512-g8VNoFWQWbx0pdal3Vt8jVCZW+v3sc3gi94iI0GVtVgUGTqphAjJF6EAruPTx0lqvtonsaAxn5TD36hcG1d6Wg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mutation-testing-metrics": "3.7.3", + "mutation-testing-report-schema": "3.7.3", + "tslib": "~2.8.0", + "typed-inject": "~5.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@stryker-mutator/core": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@stryker-mutator/core/-/core-9.6.1.tgz", + "integrity": "sha512-WMgnvf+Wyh/yiruhNZwc8w8DlzmmjXhPjSn5MR8RhAXzlnWji8TQrUYgBUkHk9bEgSaIlB3KZHm37iiU5Q2cLQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@inquirer/prompts": "^8.0.0", + "@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.0", + "commander": "~14.0.0", + "diff-match-patch": "1.0.5", + "emoji-regex": "~10.6.0", + "execa": "~9.6.0", + "json-rpc-2.0": "^1.7.0", + "lodash.groupby": "~4.6.0", + "minimatch": "~10.2.4", + "mutation-server-protocol": "~0.4.0", + "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.1", + "semver": "^7.6.3", + "source-map": "~0.7.4", + "tree-kill": "~1.2.2", + "tslib": "2.8.1", + "typed-inject": "~5.0.0", + "typed-rest-client": "~2.3.0" + }, + "bin": { + "stryker": "bin/stryker.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@stryker-mutator/core/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@stryker-mutator/core/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stryker-mutator/core/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@stryker-mutator/instrumenter": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@stryker-mutator/instrumenter/-/instrumenter-9.6.1.tgz", + "integrity": "sha512-5K8wH4Pthly25c2uKKik4Dfcoeou7sbJdFS6u3QIYHlulgFVDJwtEMWTZGkZfs7IiUEXIDNa0keRACq5jn5AvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/core": "~7.29.0", + "@babel/generator": "~7.29.0", + "@babel/parser": "~7.29.0", + "@babel/plugin-proposal-decorators": "~7.29.0", + "@babel/plugin-transform-explicit-resource-management": "^7.28.0", + "@babel/preset-typescript": "~7.28.0", + "@stryker-mutator/api": "9.6.1", + "@stryker-mutator/util": "9.6.1", + "angular-html-parser": "~10.4.0", + "semver": "~7.7.0", + "tslib": "2.8.1", + "weapon-regex": "~1.3.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@stryker-mutator/instrumenter/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@stryker-mutator/util": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@stryker-mutator/util/-/util-9.6.1.tgz", + "integrity": "sha512-Lk/ALVctJjFv1vvwR+CFoKzDCWvsBlq7flDUnmnpuwTrGbm156EdZD1Jjq4o8KdOap0ezUZqQNE9OAI1m2+pUQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@stryker-mutator/vitest-runner": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@stryker-mutator/vitest-runner/-/vitest-runner-9.6.1.tgz", + "integrity": "sha512-eyUHTCf3Ui+SUn/tpFJwzw6MV391kyBLZk/cDHFUfKFELqKMLbvd7e81axArlApKqO6cOnLfrxlwED+2SRN0ow==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stryker-mutator/api": "9.6.1", + "@stryker-mutator/util": "9.6.1", + "semver": "^7.7.4", + "tslib": "~2.8.0" + }, + "engines": { + "node": ">=14.18.0" + }, + "peerDependencies": { + "@stryker-mutator/core": "9.6.1", + "vitest": ">=2.0.0" + } + }, + "node_modules/@stryker-mutator/vitest-runner/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", @@ -3578,6 +4208,16 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/angular-html-parser": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/angular-html-parser/-/angular-html-parser-10.4.0.tgz", + "integrity": "sha512-++nLNyZwRfHqFh7akH5Gw/JYizoFlMRz0KRigfwfsLqV8ZqlcVRb1LkPEWdYvEKDnbktknM2J4BXaYUGrQZPww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -3889,6 +4529,13 @@ "dev": true, "license": "MIT" }, + "node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "dev": true, + "license": "MIT" + }, "node_modules/cli-width": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", @@ -3950,6 +4597,16 @@ "dev": true, "license": "MIT" }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/concurrently": { "version": "10.0.3", "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.3.tgz", @@ -4261,6 +4918,17 @@ "node": ">=6" } }, + "node_modules/des.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -4271,6 +4939,13 @@ "node": ">=8" } }, + "node_modules/diff-match-patch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", @@ -4706,6 +5381,33 @@ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "license": "MIT" }, + "node_modules/execa": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "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.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, "node_modules/expect-type": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", @@ -4798,6 +5500,23 @@ "fast-string-truncated-width": "^3.0.2" } }, + "node_modules/fast-uri": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fast-wrap-ansi": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", @@ -4808,6 +5527,22 @@ "fast-string-width": "^3.0.2" } }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -5011,6 +5746,23 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -5190,6 +5942,16 @@ "dev": true, "license": "MIT" }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/iconv-lite": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", @@ -5363,6 +6125,32 @@ "dev": true, "license": "MIT" }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -5432,6 +6220,13 @@ "node": ">=0.10.0" } }, + "node_modules/js-md4": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz", + "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==", + "dev": true, + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5532,6 +6327,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-rpc-2.0": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/json-rpc-2.0/-/json-rpc-2.0-1.7.1.tgz", + "integrity": "sha512-JqZjhjAanbpkXIzFE7u8mE/iFblawwlXtONaCvRqI+pyABVz7B4M1EUNpyVW+dZjqgQ2L5HFmZCmOCgUKm00hg==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -5736,9 +6538,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5760,9 +6559,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5784,9 +6580,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5808,9 +6601,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5882,6 +6672,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", + "dev": true, + "license": "MIT" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -6125,6 +6922,13 @@ "node": ">=22.0.0" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -6310,6 +7114,43 @@ "node": ">=12" } }, + "node_modules/mutation-server-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/mutation-server-protocol/-/mutation-server-protocol-0.4.1.tgz", + "integrity": "sha512-SBGK0j8hLDne7bktgThKI8kGvGTx3rY3LAeQTmOKZ5bVnL/7TorLMvcVF7dIPJCu5RNUWhkkuF53kurygYVt3g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "zod": "^4.1.12" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/mutation-testing-elements": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/mutation-testing-elements/-/mutation-testing-elements-3.7.3.tgz", + "integrity": "sha512-SMeIPxngJpfjfNYctFpYQQtlBlZaVO0aoB3FKdwrI8Ee/2bkyUuCZzAOCLv1U9fnmfA37dPFq0Owduoxs2XgGQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/mutation-testing-metrics": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/mutation-testing-metrics/-/mutation-testing-metrics-3.7.3.tgz", + "integrity": "sha512-B8QrP0ZomErzTPNlhrzKWPNBln+3afwBZPHv0Q7N8wZZTYxMptzb/Gdm3ExXVmioVYrtZAtsDs7W/T/b2AixOQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mutation-testing-report-schema": "3.7.3" + } + }, + "node_modules/mutation-testing-report-schema": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/mutation-testing-report-schema/-/mutation-testing-report-schema-3.7.3.tgz", + "integrity": "sha512-BHm3MYq+ckO+t5CtlG8zpqxc75rdJCkxVlE+fGuGJM3F7tNCQ/OW2N+TQVHN3BHsYa84+BFc6g3AwDYkUsw2MA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/mute-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", @@ -6366,6 +7207,36 @@ "node": ">=18" } }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -6525,6 +7396,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse5": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", @@ -6735,6 +7619,32 @@ "license": "MIT", "peer": true }, + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -7413,6 +8323,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -7501,6 +8421,19 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -7727,6 +8660,16 @@ "dev": true, "license": "0BSD" }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -7789,6 +8732,49 @@ "url": "https://opencollective.com/express" } }, + "node_modules/typed-inject": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/typed-inject/-/typed-inject-5.0.0.tgz", + "integrity": "sha512-0Ql2ORqBORLMdAW89TQKZsb1PQkFGImFfVmncXWe7a+AA3+7dh7Se9exxZowH4kbnlvKEFkMxUYdHUpjYWFJaA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/typed-rest-client": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-2.3.1.tgz", + "integrity": "sha512-k4kX5Up6qA68D0Cby2AK+6+vM5k3qTxe+/3FqhnHRExjY5cfbOnzjQZbP/LXleF8hVoDvDqxlgk9KK83HoBZlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "des.js": "^1.1.0", + "js-md4": "^0.3.2", + "qs": "6.15.1", + "tunnel": "0.0.6", + "underscore": "^1.13.8" + }, + "engines": { + "node": ">= 16.0.0" + } + }, + "node_modules/typed-rest-client/node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -7827,6 +8813,13 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true, + "license": "MIT" + }, "node_modules/undici": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", @@ -7854,6 +8847,19 @@ "pathe": "^2.0.3" } }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -8194,6 +9200,13 @@ "loose-envify": "^1.0.0" } }, + "node_modules/weapon-regex": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/weapon-regex/-/weapon-regex-1.3.6.tgz", + "integrity": "sha512-wsf1m1jmMrso5nhwVFJJHSubEBf3+pereGd7+nBKtYJ18KoB/PWJOHS3WRkwS04VrOU0iJr2bZU+l1QaTJ+9nA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", @@ -8496,6 +9509,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/youch": { "version": "4.1.0-beta.10", "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", diff --git a/web/package.json b/web/package.json index de9cda3e..1d52f9c5 100644 --- a/web/package.json +++ b/web/package.json @@ -11,6 +11,7 @@ "test:unit": "tsc --target ES2020 --module CommonJS --moduleResolution Node --strict --skipLibCheck --outDir .test-dist --rootDir src src/payloadBuilders.ts && node -e \"require('node:fs').writeFileSync('.test-dist/package.json', '{\\\"type\\\":\\\"commonjs\\\"}')\" && node --test tests/payloadBuilders.test.cjs", "test:vitest": "vitest run", "test:coverage": "vitest run --coverage", + "test:mutation": "stryker run", "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui", "lint": "eslint src/", @@ -34,6 +35,8 @@ "devDependencies": { "@eslint/js": "^10.0.0", "@playwright/test": "^1.56.0", + "@stryker-mutator/core": "^9.6.1", + "@stryker-mutator/vitest-runner": "^9.6.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", diff --git a/web/stryker.conf.json b/web/stryker.conf.json new file mode 100644 index 00000000..3dfd6f3a --- /dev/null +++ b/web/stryker.conf.json @@ -0,0 +1,34 @@ +{ + "$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json", + "_comment": "ミューテーションテスト設定(ADR-0017)。対象は純ロジック層(utils / hooks / store slice / フォーム変換)に限定し、自動生成コード(src/api/generated.ts)・定数カタログ(constants/messages 等)・UI シェル(main.tsx / App.tsx / components)・型定義のみのファイルは対象外。週次 CI(.github/workflows/mutation.yml)で実行され、Phase 1 は warn-only(break: null)。TS checker は project-references + noEmit 構成のため使用しない", + "testRunner": "vitest", + "vitest": { + "configFile": "vite.config.ts" + }, + "mutate": [ + "src/utils/**/*.ts", + "src/formMappers.ts", + "src/payloadBuilders.ts", + "src/hooks/**/*.ts", + "src/store/*Slice.ts", + "src/constants/agentModels.ts", + "!src/**/*.test.ts", + "!src/**/*.test.tsx", + "!src/**/*.d.ts", + "!src/utils/pdfjs.ts" + ], + "checkers": [], + "reporters": ["progress", "clear-text", "html", "json"], + "htmlReporter": { + "fileName": "reports/mutation/mutation.html" + }, + "jsonReporter": { + "fileName": "reports/mutation/mutation.json" + }, + "thresholds": { + "high": 90, + "low": 80, + "break": null + }, + "tempDirName": ".stryker-tmp" +}