From e12f4e18f4bb3a5d769386edb0c56f5715b36095 Mon Sep 17 00:00:00 2001 From: AlexandreCamillo <43162926+AlexandreCamillo@users.noreply.github.com> Date: Fri, 29 May 2026 15:59:05 +0000 Subject: [PATCH 01/21] feat(quality-gate): add quality-gate.config.json --- quality-gate.config.json | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 quality-gate.config.json diff --git a/quality-gate.config.json b/quality-gate.config.json new file mode 100644 index 00000000..17c332d9 --- /dev/null +++ b/quality-gate.config.json @@ -0,0 +1,28 @@ +{ + "schema_version": 1, + "default_branch": "main", + "thresholds": { + "MAX_FILE_LINES": 500, + "MIN_NEW_FILE_COVERAGE": 60 + }, + "ratchet": { + "strict": false, + "epsilon": 0.1 + }, + "metrics": { + "coverage": { "enabled": true }, + "duplication": { "enabled": true }, + "lint": { "enabled": true }, + "file_size": { "enabled": true }, + "security": { + "enabled": true, + "block_severities": ["critical"], + "warn_severities": ["high"] + } + }, + "adapter": { + "command": "./.quality-gate/adapter.sh", + "name": "markup", + "version": "0.1.0" + } +} From 63de229862d84aecc8bfdfa8930c19a77fcc4a2b Mon Sep 17 00:00:00 2001 From: AlexandreCamillo <43162926+AlexandreCamillo@users.noreply.github.com> Date: Fri, 29 May 2026 16:02:48 +0000 Subject: [PATCH 02/21] feat(quality-gate): scaffold adapter directory + stub --- .gitignore | 7 +++++++ .quality-gate/README.md | 15 +++++++++++++++ .quality-gate/adapter.sh | 29 +++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 .quality-gate/README.md create mode 100755 .quality-gate/adapter.sh diff --git a/.gitignore b/.gitignore index 08fcafdb..617a2633 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,10 @@ public/_qa-mobile.html # landing-export build artifacts landing-export/.next/ landing-export/out/ + +# quality-gate adapter scratch files +.jscpd/ +.biome-report.json +.npm-audit.json +.pnpm-audit.json +/qg-output/ diff --git a/.quality-gate/README.md b/.quality-gate/README.md new file mode 100644 index 00000000..3303e122 --- /dev/null +++ b/.quality-gate/README.md @@ -0,0 +1,15 @@ +# Quality Gate adapter + +This directory holds the Markup-specific adapter for [`@quality-gate/core`](https://github.com/alkg-cloud/quality-gate). + +`adapter.sh` is invoked by both `quality-gate-pr.yml` and `quality-gate-main.yml`. It reads two env vars (`QG_OUTPUT_DIR`, `QG_CONFIG`) and writes six canonical JSON files into `$QG_OUTPUT_DIR`. The schemas it must satisfy live at `src/schemas/*.schema.json` in the upstream repo. + +To run locally: + +```bash +mkdir -p /tmp/qg +QG_OUTPUT_DIR=/tmp/qg QG_CONFIG=./quality-gate.config.json ./.quality-gate/adapter.sh +ls /tmp/qg +``` + +For the engine contract (PR-mode, main-mode, baseline format), see `docs/quality-gate.md`. diff --git a/.quality-gate/adapter.sh b/.quality-gate/adapter.sh new file mode 100755 index 00000000..ff6e4ece --- /dev/null +++ b/.quality-gate/adapter.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Quality Gate adapter for the Markup project. +# Stack: vitest (coverage) + biome (lint) + jscpd (duplication) + pnpm audit (security). +# +# Contract (from upstream src/schemas/*.schema.json): +# Inputs: $QG_OUTPUT_DIR (must exist, writable), $QG_CONFIG (path to quality-gate.config.json) +# Outputs (exactly 6 files in $QG_OUTPUT_DIR): +# coverage.json — { lines_pct, files:[{path,lines_pct}] } OR { _skipped } +# lint.json — { total, by_file:[{path,count}] } OR { _skipped } +# duplication.json — { pct, clones? } OR { _skipped } +# file_size.json — { max_lines, violations:[{path,lines}] } OR { _skipped } +# security.json — { critical, high, moderate, low } OR { _skipped } +# _meta.json — { adapter, adapter_version, tools[] } +# +# All paths in metric files are repo-relative (no leading "./" or absolute). +set -euo pipefail +: "${QG_OUTPUT_DIR:?must be set}" +: "${QG_CONFIG:?must be set}" + +mkdir -p "$QG_OUTPUT_DIR" +ROOT="$PWD" +MAX_FILE_LINES=$(jq -r '.thresholds.MAX_FILE_LINES' "$QG_CONFIG") + +# Each metric is filled in by a dedicated section below. +# Sections must produce a strictly schema-compliant file even on tool failure +# (use the {"_skipped":"…"} fallback to keep the gate green for that metric). + +# --- _meta.json (placeholder — final task overwrites with real tool list) --- +echo '{"adapter":"markup","adapter_version":"0.1.0","tools":[]}' > "$QG_OUTPUT_DIR/_meta.json" From 3687471078a541e8913fce534fd13d70464601d1 Mon Sep 17 00:00:00 2001 From: AlexandreCamillo <43162926+AlexandreCamillo@users.noreply.github.com> Date: Fri, 29 May 2026 16:06:40 +0000 Subject: [PATCH 03/21] =?UTF-8?q?feat(quality-gate):=20adapter=20=E2=80=94?= =?UTF-8?q?=20coverage=20section?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .quality-gate/adapter.sh | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.quality-gate/adapter.sh b/.quality-gate/adapter.sh index ff6e4ece..828fd63f 100755 --- a/.quality-gate/adapter.sh +++ b/.quality-gate/adapter.sh @@ -25,5 +25,27 @@ MAX_FILE_LINES=$(jq -r '.thresholds.MAX_FILE_LINES' "$QG_CONFIG") # Sections must produce a strictly schema-compliant file even on tool failure # (use the {"_skipped":"…"} fallback to keep the gate green for that metric). +# --- 1. Coverage (consumes coverage/coverage-summary.json produced by vitest) --- +if [ -f coverage/coverage-summary.json ]; then + jq --arg root "$ROOT/" ' + .total.lines.pct as $total + | { + lines_pct: ($total // 0), + files: [ + to_entries[] + | select(.key != "total") + | { + path: (.key | sub("^" + ($root | @text); "")), + lines_pct: (.value.lines.pct // 0) + } + ] + | sort_by(.path) + } + ' coverage/coverage-summary.json > "$QG_OUTPUT_DIR/coverage.json" +else + echo '{"_skipped":"coverage/coverage-summary.json missing — did the workflow run pnpm test --coverage?"}' \ + > "$QG_OUTPUT_DIR/coverage.json" +fi + # --- _meta.json (placeholder — final task overwrites with real tool list) --- echo '{"adapter":"markup","adapter_version":"0.1.0","tools":[]}' > "$QG_OUTPUT_DIR/_meta.json" From 548ab0895e6b20d023fbcd8523a71edc6f0270d6 Mon Sep 17 00:00:00 2001 From: AlexandreCamillo <43162926+AlexandreCamillo@users.noreply.github.com> Date: Fri, 29 May 2026 16:12:00 +0000 Subject: [PATCH 04/21] =?UTF-8?q?feat(quality-gate):=20adapter=20=E2=80=94?= =?UTF-8?q?=20lint=20section=20(biome)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .quality-gate/adapter.sh | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.quality-gate/adapter.sh b/.quality-gate/adapter.sh index 828fd63f..b3faa53a 100755 --- a/.quality-gate/adapter.sh +++ b/.quality-gate/adapter.sh @@ -47,5 +47,28 @@ else > "$QG_OUTPUT_DIR/coverage.json" fi +# --- 2. Lint (biome --reporter=json; biome exits 1 with violations, hence || true) --- +pnpm exec biome check . --reporter=json 2>/dev/null > .biome-report.json || true +if [ -s .biome-report.json ] && jq -e '.diagnostics' .biome-report.json > /dev/null 2>&1; then + jq --arg root "$ROOT/" ' + # Aggregate diagnostics by file path; biome paths can be absolute or relative. + # Biome 2.x emits .location.path as a string; older shapes used {file: "..."}. + # Handle both defensively. + reduce .diagnostics[] as $d ({}; + (($d.location.path | if type == "object" then .file else . end) // "unknown") as $raw + | ($raw | sub("^" + ($root | @text); "")) as $rel + | .[$rel] = ((.[$rel] // 0) + 1) + ) + | { + total: ([.[]] | add // 0), + by_file: [ + to_entries[] | { path: .key, count: .value } + ] | sort_by(.path) + } + ' .biome-report.json > "$QG_OUTPUT_DIR/lint.json" +else + echo '{"_skipped":"biome produced no parseable JSON report"}' > "$QG_OUTPUT_DIR/lint.json" +fi + # --- _meta.json (placeholder — final task overwrites with real tool list) --- echo '{"adapter":"markup","adapter_version":"0.1.0","tools":[]}' > "$QG_OUTPUT_DIR/_meta.json" From c5d3ce863dc6406a3815a3a4ded97ba40ab6f0d5 Mon Sep 17 00:00:00 2001 From: AlexandreCamillo <43162926+AlexandreCamillo@users.noreply.github.com> Date: Fri, 29 May 2026 16:18:31 +0000 Subject: [PATCH 05/21] =?UTF-8?q?feat(quality-gate):=20adapter=20=E2=80=94?= =?UTF-8?q?=20duplication=20section=20(jscpd)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .quality-gate/adapter.sh | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.quality-gate/adapter.sh b/.quality-gate/adapter.sh index b3faa53a..4c718308 100755 --- a/.quality-gate/adapter.sh +++ b/.quality-gate/adapter.sh @@ -70,5 +70,22 @@ else echo '{"_skipped":"biome produced no parseable JSON report"}' > "$QG_OUTPUT_DIR/lint.json" fi +# --- 3. Duplication (jscpd) --- +# Ignore paths beyond defaults: .next build cache, prisma generated client, +# vitest scratch dirs, the landing static export, and tests fixtures (binary zips). +rm -rf .jscpd && mkdir -p .jscpd +npx --yes jscpd@4 . --reporters json --output .jscpd \ + --ignore "**/node_modules/**,**/dist/**,**/coverage/**,**/.jscpd/**,**/.next/**,**/landing-export/**,**/prisma/migrations/**,**/tests/fixtures/**,**/.git/**" \ + --silent 2>/dev/null || true + +if [ -f .jscpd/jscpd-report.json ]; then + jq '{ + pct: (.statistics.total.percentage // 0), + clones: (.statistics.total.clones // 0) + }' .jscpd/jscpd-report.json > "$QG_OUTPUT_DIR/duplication.json" +else + echo '{"_skipped":"jscpd produced no report"}' > "$QG_OUTPUT_DIR/duplication.json" +fi + # --- _meta.json (placeholder — final task overwrites with real tool list) --- echo '{"adapter":"markup","adapter_version":"0.1.0","tools":[]}' > "$QG_OUTPUT_DIR/_meta.json" From fc12966a44e30745a682e9fa208581586c33bf04 Mon Sep 17 00:00:00 2001 From: AlexandreCamillo <43162926+AlexandreCamillo@users.noreply.github.com> Date: Fri, 29 May 2026 16:22:59 +0000 Subject: [PATCH 06/21] =?UTF-8?q?feat(quality-gate):=20adapter=20=E2=80=94?= =?UTF-8?q?=20file=5Fsize=20section?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .quality-gate/adapter.sh | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.quality-gate/adapter.sh b/.quality-gate/adapter.sh index 4c718308..e538aa18 100755 --- a/.quality-gate/adapter.sh +++ b/.quality-gate/adapter.sh @@ -87,5 +87,26 @@ else echo '{"_skipped":"jscpd produced no report"}' > "$QG_OUTPUT_DIR/duplication.json" fi +# --- 4. File size (walk src/ and tests/, count lines, emit violations) --- +{ + find src tests \ + -type f \ + \( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" -o -name "*.mjs" -o -name "*.cjs" \) \ + -not -path "src/app/landing/*" \ + -not -path "src/components/landing/*" \ + -not -path "*/landing-export/*" \ + -not -path "*/node_modules/*" \ + 2>/dev/null \ + || true +} | while read -r f; do + lines=$(wc -l < "$f") + if [ "$lines" -ge "$MAX_FILE_LINES" ]; then + printf '{"path":"%s","lines":%d}\n' "$f" "$lines" + fi + done | jq -s --argjson max "$MAX_FILE_LINES" '{ + max_lines: $max, + violations: (sort_by(.path)) + }' > "$QG_OUTPUT_DIR/file_size.json" + # --- _meta.json (placeholder — final task overwrites with real tool list) --- echo '{"adapter":"markup","adapter_version":"0.1.0","tools":[]}' > "$QG_OUTPUT_DIR/_meta.json" From 3d7ebc6d8040f9d6d73945d123911d132499134b Mon Sep 17 00:00:00 2001 From: AlexandreCamillo <43162926+AlexandreCamillo@users.noreply.github.com> Date: Fri, 29 May 2026 16:26:06 +0000 Subject: [PATCH 07/21] =?UTF-8?q?feat(quality-gate):=20adapter=20=E2=80=94?= =?UTF-8?q?=20security=20section=20(pnpm=20audit)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .quality-gate/adapter.sh | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.quality-gate/adapter.sh b/.quality-gate/adapter.sh index e538aa18..78c951b5 100755 --- a/.quality-gate/adapter.sh +++ b/.quality-gate/adapter.sh @@ -108,5 +108,18 @@ fi violations: (sort_by(.path)) }' > "$QG_OUTPUT_DIR/file_size.json" +# --- 5. Security (pnpm audit) --- +pnpm audit --json > .pnpm-audit.json 2>/dev/null || true +if [ -s .pnpm-audit.json ] && jq -e '.metadata.vulnerabilities' .pnpm-audit.json > /dev/null 2>&1; then + jq '.metadata.vulnerabilities | { + critical: (.critical // 0), + high: (.high // 0), + moderate: (.moderate // 0), + low: (.low // 0) + }' .pnpm-audit.json > "$QG_OUTPUT_DIR/security.json" +else + echo '{"_skipped":"pnpm audit produced no metadata.vulnerabilities"}' > "$QG_OUTPUT_DIR/security.json" +fi + # --- _meta.json (placeholder — final task overwrites with real tool list) --- echo '{"adapter":"markup","adapter_version":"0.1.0","tools":[]}' > "$QG_OUTPUT_DIR/_meta.json" From 5f60afa998f547cfbe92bf7feb494e709683246b Mon Sep 17 00:00:00 2001 From: AlexandreCamillo <43162926+AlexandreCamillo@users.noreply.github.com> Date: Fri, 29 May 2026 16:30:19 +0000 Subject: [PATCH 08/21] feat(quality-gate): finalize adapter _meta + verify schema compliance --- .quality-gate/adapter.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.quality-gate/adapter.sh b/.quality-gate/adapter.sh index 78c951b5..fc0e048b 100755 --- a/.quality-gate/adapter.sh +++ b/.quality-gate/adapter.sh @@ -121,5 +121,11 @@ else echo '{"_skipped":"pnpm audit produced no metadata.vulnerabilities"}' > "$QG_OUTPUT_DIR/security.json" fi -# --- _meta.json (placeholder — final task overwrites with real tool list) --- -echo '{"adapter":"markup","adapter_version":"0.1.0","tools":[]}' > "$QG_OUTPUT_DIR/_meta.json" +# --- 6. _meta (must list the tools the adapter actually invoked) --- +cat > "$QG_OUTPUT_DIR/_meta.json" <<'JSON' +{ + "adapter": "markup", + "adapter_version": "0.1.0", + "tools": ["vitest", "biome", "jscpd", "pnpm-audit"] +} +JSON From 64f201c8c25d7d623e73226387c53a87339a6fed Mon Sep 17 00:00:00 2001 From: AlexandreCamillo <43162926+AlexandreCamillo@users.noreply.github.com> Date: Fri, 29 May 2026 16:45:16 +0000 Subject: [PATCH 09/21] feat(quality-gate): add PR-mode workflow --- .github/workflows/quality-gate-pr.yml | 73 +++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 .github/workflows/quality-gate-pr.yml diff --git a/.github/workflows/quality-gate-pr.yml b/.github/workflows/quality-gate-pr.yml new file mode 100644 index 00000000..848ac8d3 --- /dev/null +++ b/.github/workflows/quality-gate-pr.yml @@ -0,0 +1,73 @@ +name: quality-gate +on: + pull_request: + branches: [main] + +permissions: + contents: read + pull-requests: write + +concurrency: + group: quality-gate-pr-${{ github.ref }} + cancel-in-progress: true + +jobs: + quality-gate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - run: pnpm install --frozen-lockfile + - run: pnpm prisma generate + - run: pnpm prisma migrate deploy + env: + DATABASE_URL: file:./prisma/test.db + - run: pnpm tsx tests/fixtures/build-fixtures.ts + + # Run vitest with coverage so the adapter has coverage-summary.json to consume. + - run: pnpm test --coverage + env: + DATABASE_URL: file:./prisma/test.db + + - name: Build quality-gate engine (upstream not on npm) + run: | + git clone --depth 1 https://github.com/alkg-cloud/quality-gate /tmp/qg-core + cd /tmp/qg-core + git fetch --depth 1 origin 192fcaf386cf5bbb464dca53a26949078240c100 + git checkout 192fcaf386cf5bbb464dca53a26949078240c100 + pnpm install --frozen-lockfile + pnpm run build + + - name: Run adapter + run: ./.quality-gate/adapter.sh + env: + QG_OUTPUT_DIR: ${{ runner.temp }}/qg + QG_MODE: pr + QG_CONFIG: ./quality-gate.config.json + + - name: Run quality gate + run: | + node /tmp/qg-core/dist/cli.js pr \ + --config ./quality-gate.config.json \ + --output-dir ${{ runner.temp }}/qg + + - name: Post sticky PR comment + if: always() + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: quality-gate-marker-v1 + path: ${{ runner.temp }}/qg/pr-comment.md + + - name: Upload artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: quality-gate-report + path: ${{ runner.temp }}/qg From 8dcf147f094f7051c559828f2252009b607a7d23 Mon Sep 17 00:00:00 2001 From: AlexandreCamillo <43162926+AlexandreCamillo@users.noreply.github.com> Date: Fri, 29 May 2026 16:47:31 +0000 Subject: [PATCH 10/21] feat(quality-gate): add main-mode baseline updater workflow --- .github/workflows/quality-gate-main.yml | 59 +++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 .github/workflows/quality-gate-main.yml diff --git a/.github/workflows/quality-gate-main.yml b/.github/workflows/quality-gate-main.yml new file mode 100644 index 00000000..2f98479c --- /dev/null +++ b/.github/workflows/quality-gate-main.yml @@ -0,0 +1,59 @@ +name: quality-gate-baseline +on: + push: + branches: [main] + +permissions: + contents: write + +concurrency: + group: quality-gate-main + cancel-in-progress: false + +jobs: + update-baseline: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - run: pnpm install --frozen-lockfile + - run: pnpm prisma generate + - run: pnpm prisma migrate deploy + env: + DATABASE_URL: file:./prisma/test.db + - run: pnpm tsx tests/fixtures/build-fixtures.ts + + - run: pnpm test --coverage + env: + DATABASE_URL: file:./prisma/test.db + + - name: Build quality-gate engine (upstream not on npm) + run: | + git clone --depth 1 https://github.com/alkg-cloud/quality-gate /tmp/qg-core + cd /tmp/qg-core + git fetch --depth 1 origin 192fcaf386cf5bbb464dca53a26949078240c100 + git checkout 192fcaf386cf5bbb464dca53a26949078240c100 + pnpm install --frozen-lockfile + pnpm run build + + - name: Run adapter + run: ./.quality-gate/adapter.sh + env: + QG_OUTPUT_DIR: ${{ runner.temp }}/qg + QG_MODE: main + QG_CONFIG: ./quality-gate.config.json + + - name: Update baseline on orphan branch + run: | + node /tmp/qg-core/dist/cli.js update-baseline \ + --config ./quality-gate.config.json \ + --output-dir ${{ runner.temp }}/qg + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From f02cf358a859033676016fe6e3a0da276b5734f8 Mon Sep 17 00:00:00 2001 From: AlexandreCamillo <43162926+AlexandreCamillo@users.noreply.github.com> Date: Fri, 29 May 2026 16:49:28 +0000 Subject: [PATCH 11/21] ci: remove bespoke coverage ratchet from test.yml (replaced by quality-gate) --- .github/workflows/test.yml | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b005dfa3..10a18b84 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,9 +49,6 @@ jobs: test-coverage: runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 @@ -68,32 +65,6 @@ jobs: - run: pnpm test --coverage env: DATABASE_URL: file:./prisma/test.db - - id: ratchet - run: pnpm exec tsx scripts/coverage-ratchet.ts - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - if: github.event_name == 'pull_request' && always() - uses: actions/github-script@v7 - env: - # Route the step output through an env var so the markdown content - # (which can contain backticks, ${...}, or other JS syntax) is read - # via process.env rather than interpolated into the script source. - # See: https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable - RATCHET_MARKDOWN: ${{ steps.ratchet.outputs.markdown }} - with: - script: | - const body = process.env.RATCHET_MARKDOWN ?? ''; - if (!body.trim()) return; - const { owner, repo } = context.repo; - const issue_number = context.issue.number; - const marker = ''; - const comments = await github.rest.issues.listComments({ owner, repo, issue_number }); - const existing = comments.data.find((c) => c.body && c.body.includes(marker)); - if (existing) { - await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }); - } else { - await github.rest.issues.createComment({ owner, repo, issue_number, body }); - } e2e: runs-on: ubuntu-latest From dbc60f0c56a51e02fe340b73957c50cb92147813 Mon Sep 17 00:00:00 2001 From: AlexandreCamillo <43162926+AlexandreCamillo@users.noreply.github.com> Date: Fri, 29 May 2026 16:53:21 +0000 Subject: [PATCH 12/21] chore: delete bespoke coverage-ratchet script + test (replaced by quality-gate) --- scripts/coverage-ratchet.ts | 144 -------------------- scripts/lib/coverage-compare.ts | 83 ----------- tests/unit/scripts/coverage-compare.test.ts | 71 ---------- 3 files changed, 298 deletions(-) delete mode 100644 scripts/coverage-ratchet.ts delete mode 100644 scripts/lib/coverage-compare.ts delete mode 100644 tests/unit/scripts/coverage-compare.test.ts diff --git a/scripts/coverage-ratchet.ts b/scripts/coverage-ratchet.ts deleted file mode 100644 index 236caec7..00000000 --- a/scripts/coverage-ratchet.ts +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env tsx -import { execSync } from 'node:child_process'; -import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; -import { join } from 'node:path'; -import { type CoverageMetrics, compareCoverage } from './lib/coverage-compare'; - -const DRIFT_TOLERANCE = 0.1; -const COVERAGE_BRANCH = 'coverage-data'; -const COVERAGE_DIR = 'coverage'; -const SCRATCH_DIR = '.coverage-data-clone'; - -type SummaryFile = { - total: { - lines: { pct: number }; - statements: { pct: number }; - functions: { pct: number }; - branches: { pct: number }; - }; -}; - -function sh(cmd: string, opts: { cwd?: string; allowFail?: boolean } = {}): string { - try { - return execSync(cmd, { cwd: opts.cwd, stdio: ['ignore', 'pipe', 'pipe'] }).toString(); - } catch (e) { - if (opts.allowFail) return ''; - throw e; - } -} - -function readCurrentMetrics(): CoverageMetrics { - const path = join(COVERAGE_DIR, 'coverage-summary.json'); - if (!existsSync(path)) throw new Error(`Missing ${path}. Did you run 'pnpm test --coverage'?`); - const summary = JSON.parse(readFileSync(path, 'utf8')) as SummaryFile; - return { - lines: summary.total.lines.pct, - statements: summary.total.statements.pct, - functions: summary.total.functions.pct, - branches: summary.total.branches.pct, - }; -} - -function cloneCoverageBranch(): { exists: boolean } { - rmSync(SCRATCH_DIR, { recursive: true, force: true }); - const out = sh(`git ls-remote --exit-code origin ${COVERAGE_BRANCH}`, { allowFail: true }); - if (!out.trim()) { - mkdirSync(SCRATCH_DIR, { recursive: true }); - sh(`git init -q`, { cwd: SCRATCH_DIR }); - sh(`git checkout --orphan ${COVERAGE_BRANCH}`, { cwd: SCRATCH_DIR }); - return { exists: false }; - } - sh( - `git clone --branch ${COVERAGE_BRANCH} --single-branch --depth 1 ${process.env.GITHUB_SERVER_URL ?? 'https://github.com'}/${process.env.GITHUB_REPOSITORY} ${SCRATCH_DIR}`, - ); - return { exists: true }; -} - -function readBaseline(): CoverageMetrics | null { - const path = join(SCRATCH_DIR, 'baseline.json'); - if (!existsSync(path)) return null; - return JSON.parse(readFileSync(path, 'utf8')) as CoverageMetrics; -} - -function writeArtifacts(current: CoverageMetrics, color: string): void { - writeFileSync(join(SCRATCH_DIR, 'baseline.json'), JSON.stringify(current, null, 2)); - writeFileSync( - join(SCRATCH_DIR, 'badge.json'), - JSON.stringify( - { schemaVersion: 1, label: 'coverage', message: `${Math.round(current.lines)}%`, color }, - null, - 2, - ), - ); - const reportDir = join(SCRATCH_DIR, 'report'); - rmSync(reportDir, { recursive: true, force: true }); - cpSync(COVERAGE_DIR, reportDir, { recursive: true }); - writeFileSync( - join(SCRATCH_DIR, 'README.md'), - '# coverage-data\n\nThis orphan branch holds coverage artifacts (baseline.json, badge.json, report/) for the Markup project. **Do not merge to main.** The branch is force-updated by CI on every push to main.\n', - ); -} - -function pushArtifacts(): void { - const token = process.env.GITHUB_TOKEN; - if (!token) throw new Error('GITHUB_TOKEN required to push coverage artifacts'); - const remote = `https://x-access-token:${token}@github.com/${process.env.GITHUB_REPOSITORY}.git`; - sh( - `git -C ${SCRATCH_DIR} config user.email "41898282+github-actions[bot]@users.noreply.github.com"`, - ); - sh(`git -C ${SCRATCH_DIR} config user.name "github-actions[bot]"`); - sh(`git -C ${SCRATCH_DIR} add -A`); - sh( - `git -C ${SCRATCH_DIR} commit -m "chore(coverage): update baseline + badge for ${process.env.GITHUB_SHA?.slice(0, 7) ?? 'unknown'}"`, - { allowFail: true }, - ); - sh(`git -C ${SCRATCH_DIR} push --force "${remote}" ${COVERAGE_BRANCH}`); -} - -class CoverageRegression extends Error { - constructor(failures: string[]) { - super(`Coverage regressed in: ${failures.join(', ')}`); - this.name = 'CoverageRegression'; - } -} - -async function main(): Promise { - try { - const current = readCurrentMetrics(); - const { exists } = cloneCoverageBranch(); - const baseline = exists ? readBaseline() : null; - const result = compareCoverage(current, baseline, DRIFT_TOLERANCE); - - console.log(result.markdown); - - const summaryPath = process.env.GITHUB_STEP_SUMMARY; - if (summaryPath) writeFileSync(summaryPath, result.markdown, { flag: 'a' }); - - const outputPath = process.env.GITHUB_OUTPUT; - if (outputPath) { - writeFileSync(outputPath, `markdown< { - if (e instanceof CoverageRegression) console.error(e.message); - else console.error(e); - process.exit(1); -}); diff --git a/scripts/lib/coverage-compare.ts b/scripts/lib/coverage-compare.ts deleted file mode 100644 index 16f48564..00000000 --- a/scripts/lib/coverage-compare.ts +++ /dev/null @@ -1,83 +0,0 @@ -export type CoverageMetrics = { - lines: number; - statements: number; - functions: number; - branches: number; -}; - -export type CompareResult = { - pass: boolean; - failures: string[]; - deltas: CoverageMetrics; - markdown: string; - color: string; -}; - -const METRIC_KEYS: (keyof CoverageMetrics)[] = ['lines', 'statements', 'functions', 'branches']; - -const LABEL: Record = { - lines: 'Lines', - statements: 'Statements', - functions: 'Functions', - branches: 'Branches', -}; - -export function pickColor(linesPct: number): string { - if (linesPct >= 80) return 'brightgreen'; - if (linesPct >= 70) return 'yellowgreen'; - if (linesPct >= 60) return 'yellow'; - if (linesPct >= 50) return 'orange'; - return 'red'; -} - -export function compareCoverage( - current: CoverageMetrics, - baseline: CoverageMetrics | null, - driftTolerance: number, -): CompareResult { - const effectiveBaseline: CoverageMetrics = baseline ?? { - lines: 0, - statements: 0, - functions: 0, - branches: 0, - }; - const deltas: CoverageMetrics = { - lines: round(current.lines - effectiveBaseline.lines), - statements: round(current.statements - effectiveBaseline.statements), - functions: round(current.functions - effectiveBaseline.functions), - branches: round(current.branches - effectiveBaseline.branches), - }; - const failures = METRIC_KEYS.filter((k) => deltas[k] < -driftTolerance); - - const rows = METRIC_KEYS.map((k) => { - const baseStr = baseline === null ? '—' : `${effectiveBaseline[k].toFixed(2)}%`; - const curStr = `${current[k].toFixed(2)}%`; - const delta = deltas[k]; - const sign = delta > 0 ? '+' : ''; - const flag = failures.includes(k) ? ' ❌' : ''; - return `| ${LABEL[k]} | ${baseStr} | ${curStr} | ${sign}${delta.toFixed(2)}${flag} |`; - }).join('\n'); - - const markdown = [ - '', - '', - '### Coverage report', - '', - '| Metric | Baseline (main) | This PR | Δ |', - '|---|---|---|---|', - rows, - '', - ].join('\n'); - - return { - pass: failures.length === 0, - failures, - deltas, - markdown, - color: pickColor(current.lines), - }; -} - -function round(n: number): number { - return Math.round(n * 100) / 100; -} diff --git a/tests/unit/scripts/coverage-compare.test.ts b/tests/unit/scripts/coverage-compare.test.ts deleted file mode 100644 index c59d5703..00000000 --- a/tests/unit/scripts/coverage-compare.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { type CoverageMetrics, compareCoverage } from '../../../scripts/lib/coverage-compare'; - -const DRIFT_TOLERANCE = 0.1; - -const make = ( - lines: number, - statements: number, - functions: number, - branches: number, -): CoverageMetrics => ({ - lines, - statements, - functions, - branches, -}); - -describe('compareCoverage', () => { - it('reports pass when current equals baseline', () => { - const r = compareCoverage(make(70, 70, 70, 70), make(70, 70, 70, 70), DRIFT_TOLERANCE); - expect(r.pass).toBe(true); - expect(r.failures).toEqual([]); - }); - - it('reports pass when current is above baseline', () => { - const r = compareCoverage(make(75, 75, 75, 75), make(70, 70, 70, 70), DRIFT_TOLERANCE); - expect(r.pass).toBe(true); - expect(r.deltas.lines).toBeCloseTo(5); - }); - - it('reports pass when current drops within tolerance', () => { - const r = compareCoverage(make(69.95, 70, 70, 70), make(70, 70, 70, 70), DRIFT_TOLERANCE); - expect(r.pass).toBe(true); - }); - - it('reports fail when any metric drops more than tolerance', () => { - const r = compareCoverage(make(69.5, 70, 70, 70), make(70, 70, 70, 70), DRIFT_TOLERANCE); - expect(r.pass).toBe(false); - expect(r.failures).toContain('lines'); - }); - - it('lists every failing metric', () => { - const r = compareCoverage(make(60, 60, 70, 70), make(70, 70, 70, 70), DRIFT_TOLERANCE); - expect(r.pass).toBe(false); - expect(r.failures.sort()).toEqual(['lines', 'statements']); - }); - - it('formats a markdown table', () => { - const r = compareCoverage( - make(73.5, 73.4, 68.1, 64.4), - make(73.42, 73.41, 68.1, 64.85), - DRIFT_TOLERANCE, - ); - expect(r.markdown).toContain('| Metric'); - expect(r.markdown).toContain('Lines'); - expect(r.markdown).toMatch(/-0\.45/); - }); - - it('treats a missing baseline as zero (first run)', () => { - const r = compareCoverage(make(50, 50, 50, 50), null, DRIFT_TOLERANCE); - expect(r.pass).toBe(true); - }); - - it('picks badge color from lines pct', () => { - expect(compareCoverage(make(85, 0, 0, 0), null, DRIFT_TOLERANCE).color).toBe('brightgreen'); - expect(compareCoverage(make(72, 0, 0, 0), null, DRIFT_TOLERANCE).color).toBe('yellowgreen'); - expect(compareCoverage(make(62, 0, 0, 0), null, DRIFT_TOLERANCE).color).toBe('yellow'); - expect(compareCoverage(make(52, 0, 0, 0), null, DRIFT_TOLERANCE).color).toBe('orange'); - expect(compareCoverage(make(40, 0, 0, 0), null, DRIFT_TOLERANCE).color).toBe('red'); - }); -}); From 837fef5a88921870d8534b6093de416a8f32f9ac Mon Sep 17 00:00:00 2001 From: AlexandreCamillo <43162926+AlexandreCamillo@users.noreply.github.com> Date: Fri, 29 May 2026 17:00:26 +0000 Subject: [PATCH 13/21] docs: document quality-gate and remove references to bespoke ratchet --- docs/INDEX.md | 1 + docs/ci.md | 16 +++------- docs/quality-gate.md | 74 ++++++++++++++++++++++++++++++++++++++++++++ docs/testing.md | 13 +------- 4 files changed, 81 insertions(+), 23 deletions(-) create mode 100644 docs/quality-gate.md diff --git a/docs/INDEX.md b/docs/INDEX.md index 7153ec1f..63f021de 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -6,6 +6,7 @@ Start here to find which docs apply to your task. If multiple docs are relevant, - [Task Rules](task-rules.md) — what to do before/after every task; the per-area checklist - [CI and coding rules](ci.md) — what fails CI, the pre-push checklist, and the conventions the agent must follow to keep `main` green +- [Quality Gate](quality-gate.md) — multi-metric ratchet (coverage, lint, duplication, file size, security) against the `quality-metrics` orphan branch - [Documentation Standards](doc-standards.md) — how to write and maintain docs (snapshot-only, declarative present tense) - [Git Conventions](git/conventions.md) — commit messages and workflow diff --git a/docs/ci.md b/docs/ci.md index 066be44e..ef149434 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -9,21 +9,15 @@ The single CI workflow runs five jobs on every push. Read this before changing a | `lint` | `pnpm exec biome check .` | Any lint error or format diff. Warnings do not fail CI; errors do. | | `typecheck` | `pnpm exec tsc --noEmit` | Any TS error. The pre-existing `baseUrl` deprecation warning is informational. | | `build` | `pnpm build` (Next 16 + Turbopack) | TS errors that only surface at build time, missing imports, or build-config issues. | -| `test-coverage` | `pnpm test --coverage` + `scripts/coverage-ratchet.ts` | Any failing assertion across unit + integration suites, OR a coverage drop of more than 0.10pp on any of lines/statements/functions/branches vs the baseline on the `coverage-data` branch. | +| `test-coverage` | `pnpm test --coverage` | Any failing assertion across unit + integration suites. Coverage drift is gated separately by the [Quality Gate](quality-gate.md). | | `e2e` | `pnpm test:e2e` (Playwright) | Any failing e2e assertion. The job installs Chromium via `playwright install --with-deps`. | +| `quality-gate` | `./.quality-gate/adapter.sh` + `node /tmp/qg-core/dist/cli.js pr` | Coverage drop > 0.10pp, lint count rise, duplication rise, file-size rise, or any `critical` audit vulnerability vs the baseline on the `quality-metrics` orphan branch. See [Quality Gate](quality-gate.md). | -All five jobs run in parallel from the same `actions/checkout` + `pnpm install` prelude. +All five jobs run in parallel from the same `actions/checkout` + `pnpm install` prelude. `quality-gate` runs in its own workflow but in parallel with the rest. -## Coverage +## Coverage and quality ratchet -`test-coverage` gates merges on a ratchet: each metric (lines, statements, functions, branches) must not drop more than 0.10pp below the baseline stored in the orphan branch `coverage-data`. On every `main` push, the job force-pushes the new baseline + a `shields.io`-shaped `badge.json` + the lcov HTML report to that branch. - -The README coverage badge reads `badge.json` via raw GitHub. Two implications: - -- The orphan branch never accumulates history (force-pushed). Fine for an artifact branch. -- The badge 404s until the first `main` push writes the `coverage-data` branch. - -See [`docs/testing.md`](testing.md) for the engineer-facing details. +Coverage is one of five ratcheted metrics. The full ratchet logic — including the baseline branch (`quality-metrics`), the bootstrap rule, and per-metric failure conditions — lives in [`docs/quality-gate.md`](quality-gate.md). The pre-push checklist below still runs `pnpm test` for fast feedback, but the merge gate is the `quality-gate / quality-gate` check. ## Pre-push checklist diff --git a/docs/quality-gate.md b/docs/quality-gate.md new file mode 100644 index 00000000..8b37f48e --- /dev/null +++ b/docs/quality-gate.md @@ -0,0 +1,74 @@ +# Quality Gate + +Merges to `main` are gated by `@quality-gate/core` (upstream: [`alkg-cloud/quality-gate`](https://github.com/alkg-cloud/quality-gate), pinned commit `192fcaf386cf5bbb464dca53a26949078240c100`). The gate ratchets five metrics against a baseline stored on the orphan branch `quality-metrics` in this repo. + +The upstream package is not published to npm. Both gate workflows clone the repo at the pinned commit and build the CLI on each run. To bump the pin, edit the SHA in both `.github/workflows/quality-gate-*.yml` files. + +## Metrics and rules + +| Metric | Source | Failure rule | +|---|---|---| +| `coverage` | `coverage/coverage-summary.json` from vitest (v8 provider) | Global `lines_pct` drops > `epsilon` (0.10pp) below baseline. New file with coverage below `MIN_NEW_FILE_COVERAGE` (60%) also blocks. | +| `lint` | `pnpm exec biome check . --reporter=json` | Total diagnostic count rises above baseline; OR any per-file count rises; OR any new file contributes ≥1 diagnostic. | +| `duplication` | `npx jscpd` | Global `pct` rises > `epsilon` above baseline. | +| `file_size` | `find src/ tests/` with line count | Existing file's line count rises above its baseline count (when already above `MAX_FILE_LINES`, currently 500). New file with `lines >= MAX_FILE_LINES` blocks. | +| `security` | `pnpm audit --json` | Any vulnerability in `block_severities` (currently `["critical"]`). `high` is a warning, not a block. | + +## Config + +`quality-gate.config.json` at repo root is the contract. Schema: [`config.schema.json`](https://raw.githubusercontent.com/alkg-cloud/quality-gate/main/src/schemas/config.schema.json). + +## Adapter + +`./.quality-gate/adapter.sh` produces the six canonical JSON files the engine consumes. It is project-specific (vitest + biome + jscpd + pnpm audit, not the upstream React template's jest + eslint + npm audit). Schemas at `https://github.com/alkg-cloud/quality-gate/tree/main/src/schemas`. + +To run the adapter locally: + +```bash +DATABASE_URL='file:./prisma/test.db' pnpm prisma migrate deploy +pnpm tsx tests/fixtures/build-fixtures.ts +pnpm test --coverage +mkdir -p /tmp/qg +QG_OUTPUT_DIR=/tmp/qg QG_CONFIG=./quality-gate.config.json ./.quality-gate/adapter.sh +ls /tmp/qg +``` + +To run the full engine locally (clone+build the upstream CLI yourself): + +```bash +git clone --depth 1 https://github.com/alkg-cloud/quality-gate /tmp/qg-core +cd /tmp/qg-core && git checkout 192fcaf386cf5bbb464dca53a26949078240c100 && pnpm install && pnpm run build +cd - +node /tmp/qg-core/dist/cli.js collect --input /tmp/qg --output /tmp/qg/metrics.json +node /tmp/qg-core/dist/cli.js compare --metrics /tmp/qg/metrics.json --baseline NONE --config ./quality-gate.config.json --output /tmp/qg/report.json +``` + +## Workflows + +- `.github/workflows/quality-gate-pr.yml` — runs on `pull_request` to `main`. Computes metrics, compares against baseline on `quality-metrics`, posts a sticky PR comment, fails the job on regression. +- `.github/workflows/quality-gate-main.yml` — runs on `push` to `main`. Recomputes metrics and force-pushes the new baseline + badges to the `quality-metrics` orphan branch. + +Both workflows clone and build the upstream engine at the pinned commit at the start of each run. + +The PR-mode workflow's job name is `quality-gate / quality-gate` — this is the required check for branch protection. + +## Orphan branch + +`quality-metrics` holds (force-pushed on every merge): +- `baseline.json` — the metrics snapshot the next PR is gated against. +- `badges/coverage.json`, `badges/quality.json` — shields.io endpoint format. +- `README.md` — auto-written by the engine; do not edit by hand. + +The branch never accumulates history. The badge endpoints in the root `README.md` reference these files via `raw.githubusercontent.com`. + +## Bootstrap + +The first PR runs without a baseline. The engine returns `bootstrap: true`, skips ratchet comparisons, and only blocks on `block_severities` security vulnerabilities. Merging that PR creates the baseline; subsequent PRs are ratcheted normally. + +## Tuning + +Edit `quality-gate.config.json`: +- `ratchet.epsilon` — tolerance in pp for coverage drop / duplication rise. Currently `0.1`. Set `strict: true, epsilon: 0` for zero-drift mode. +- `thresholds.MAX_FILE_LINES` — per-file size cap. Currently `500`. Lower in a follow-up PR after the codebase shrinks. +- `thresholds.MIN_NEW_FILE_COVERAGE` — floor (0-100) for any new file. Currently `60`. +- `metrics.security.block_severities` / `warn_severities` — graduate `high` from warn to block once the codebase is clean. diff --git a/docs/testing.md b/docs/testing.md index 14bb66f2..4e05ae3a 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -140,15 +140,4 @@ When a future test needs to exercise puppeteer, reuse the singleton from `src/li ## Coverage and the ratchet -CI runs `pnpm test --coverage` and feeds the summary into `scripts/coverage-ratchet.ts`. The script: - -1. Fetches `baseline.json` from the orphan branch `coverage-data`. -2. Compares each metric (lines, statements, functions, branches) against the baseline. -3. On PRs, fails the build if any metric drops more than 0.10pp below baseline. -4. On `main` push, force-writes the new baseline + `badge.json` + `report/` (lcov-html) back to `coverage-data`. - -The 0.10pp tolerance absorbs noise from denominator shifts when source files are added without proportional test additions. Real regressions surface quickly because the baseline tracks `main`. - -The lcov HTML report for the latest `main` is browsable at (the README "coverage" badge links there). To browse it locally, run `pnpm test --coverage` and open `coverage/index.html`. - -The orphan branch is force-pushed on every `main` run; it never accumulates history. If you need it locally: `git fetch origin coverage-data:coverage-data`. +CI runs `pnpm test --coverage` and the resulting `coverage/coverage-summary.json` is consumed by the Quality Gate adapter (`./.quality-gate/adapter.sh`). The merge gate is the `quality-gate / quality-gate` check defined in `.github/workflows/quality-gate-pr.yml`. See [`docs/quality-gate.md`](quality-gate.md) for the full ratchet rules and the orphan-branch contract. From 41ab30fe568c34927b88b4a09450012047a04cec Mon Sep 17 00:00:00 2001 From: AlexandreCamillo <43162926+AlexandreCamillo@users.noreply.github.com> Date: Fri, 29 May 2026 17:03:25 +0000 Subject: [PATCH 14/21] docs(readme): point coverage/quality badges at the quality-metrics orphan branch --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 74fb1e27..2e703f24 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,8 @@ Built with Next.js 16 Release CI - Coverage + coverage + quality

From 19911ca6dfce996c4ce58b64f99e54b97925cbdc Mon Sep 17 00:00:00 2001 From: AlexandreCamillo <43162926+AlexandreCamillo@users.noreply.github.com> Date: Fri, 29 May 2026 17:23:31 +0000 Subject: [PATCH 15/21] simplify(quality-gate): cache engine build, derive _meta from config, drop unused QG_MODE - Cache /tmp/qg-core keyed on the pinned upstream SHA so subsequent CI runs skip the ~25s clone+install+build on hit. - _meta.json reads adapter.name / adapter.version from quality-gate.config.json (jq) so the snapshot the engine stores in baseline.json cannot drift from what the adapter reports. - Drop QG_MODE env from both workflows: the adapter never reads it and the contract didn't document it. --- .github/workflows/quality-gate-main.yml | 9 ++++++++- .github/workflows/quality-gate-pr.yml | 9 ++++++++- .quality-gate/adapter.sh | 14 +++++++------- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/.github/workflows/quality-gate-main.yml b/.github/workflows/quality-gate-main.yml index 2f98479c..b5239dd9 100644 --- a/.github/workflows/quality-gate-main.yml +++ b/.github/workflows/quality-gate-main.yml @@ -34,7 +34,15 @@ jobs: env: DATABASE_URL: file:./prisma/test.db + - name: Cache quality-gate engine build + id: cache-qg + uses: actions/cache@v4 + with: + path: /tmp/qg-core + key: qg-core-192fcaf386cf5bbb464dca53a26949078240c100 + - name: Build quality-gate engine (upstream not on npm) + if: steps.cache-qg.outputs.cache-hit != 'true' run: | git clone --depth 1 https://github.com/alkg-cloud/quality-gate /tmp/qg-core cd /tmp/qg-core @@ -47,7 +55,6 @@ jobs: run: ./.quality-gate/adapter.sh env: QG_OUTPUT_DIR: ${{ runner.temp }}/qg - QG_MODE: main QG_CONFIG: ./quality-gate.config.json - name: Update baseline on orphan branch diff --git a/.github/workflows/quality-gate-pr.yml b/.github/workflows/quality-gate-pr.yml index 848ac8d3..5bb72cb9 100644 --- a/.github/workflows/quality-gate-pr.yml +++ b/.github/workflows/quality-gate-pr.yml @@ -36,7 +36,15 @@ jobs: env: DATABASE_URL: file:./prisma/test.db + - name: Cache quality-gate engine build + id: cache-qg + uses: actions/cache@v4 + with: + path: /tmp/qg-core + key: qg-core-192fcaf386cf5bbb464dca53a26949078240c100 + - name: Build quality-gate engine (upstream not on npm) + if: steps.cache-qg.outputs.cache-hit != 'true' run: | git clone --depth 1 https://github.com/alkg-cloud/quality-gate /tmp/qg-core cd /tmp/qg-core @@ -49,7 +57,6 @@ jobs: run: ./.quality-gate/adapter.sh env: QG_OUTPUT_DIR: ${{ runner.temp }}/qg - QG_MODE: pr QG_CONFIG: ./quality-gate.config.json - name: Run quality gate diff --git a/.quality-gate/adapter.sh b/.quality-gate/adapter.sh index fc0e048b..a105f2d5 100755 --- a/.quality-gate/adapter.sh +++ b/.quality-gate/adapter.sh @@ -122,10 +122,10 @@ else fi # --- 6. _meta (must list the tools the adapter actually invoked) --- -cat > "$QG_OUTPUT_DIR/_meta.json" <<'JSON' -{ - "adapter": "markup", - "adapter_version": "0.1.0", - "tools": ["vitest", "biome", "jscpd", "pnpm-audit"] -} -JSON +# adapter/version derive from quality-gate.config.json so the _meta the engine +# ingests cannot drift from the config_snapshot it stores in baseline.json. +jq '{ + adapter: .adapter.name, + adapter_version: .adapter.version, + tools: ["vitest", "biome", "jscpd", "pnpm-audit"] +}' "$QG_CONFIG" > "$QG_OUTPUT_DIR/_meta.json" From a9e1586c7e339e0436683221a98be36b6d0a4150 Mon Sep 17 00:00:00 2001 From: AlexandreCamillo <43162926+AlexandreCamillo@users.noreply.github.com> Date: Fri, 29 May 2026 18:29:02 +0000 Subject: [PATCH 16/21] fix(quality-gate): harden engine cache + exclude landing from duplication - Split actions/cache into restore + conditional save so a crashed install/build never persists a partial /tmp/qg-core (missing dist/cli.js) under the fixed-SHA key. With the prior single-action cache, the post-step saved on job failure too, poisoning the cache for every later run until manual eviction or a pin bump. - Add src/app/landing + src/components/landing to the jscpd --ignore list so the duplication metric matches the coverage and file_size exclusions. Landing is a presentation-heavy marketing surface whose repeated section markup inflated the metric without signalling real duplication debt. --- .github/workflows/quality-gate-main.yml | 14 ++++++++++++-- .github/workflows/quality-gate-pr.yml | 14 ++++++++++++-- .quality-gate/adapter.sh | 5 ++++- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/.github/workflows/quality-gate-main.yml b/.github/workflows/quality-gate-main.yml index b5239dd9..e853fc19 100644 --- a/.github/workflows/quality-gate-main.yml +++ b/.github/workflows/quality-gate-main.yml @@ -34,9 +34,9 @@ jobs: env: DATABASE_URL: file:./prisma/test.db - - name: Cache quality-gate engine build + - name: Restore quality-gate engine build id: cache-qg - uses: actions/cache@v4 + uses: actions/cache/restore@v4 with: path: /tmp/qg-core key: qg-core-192fcaf386cf5bbb464dca53a26949078240c100 @@ -51,6 +51,16 @@ jobs: pnpm install --frozen-lockfile pnpm run build + # Save only after a clean build, so a crashed install/build never caches a + # partial /tmp/qg-core (missing dist/cli.js) under the fixed-SHA key — which + # would make every later run hit the poisoned cache and skip the rebuild. + - name: Save quality-gate engine build + if: steps.cache-qg.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: /tmp/qg-core + key: qg-core-192fcaf386cf5bbb464dca53a26949078240c100 + - name: Run adapter run: ./.quality-gate/adapter.sh env: diff --git a/.github/workflows/quality-gate-pr.yml b/.github/workflows/quality-gate-pr.yml index 5bb72cb9..3a038e42 100644 --- a/.github/workflows/quality-gate-pr.yml +++ b/.github/workflows/quality-gate-pr.yml @@ -36,9 +36,9 @@ jobs: env: DATABASE_URL: file:./prisma/test.db - - name: Cache quality-gate engine build + - name: Restore quality-gate engine build id: cache-qg - uses: actions/cache@v4 + uses: actions/cache/restore@v4 with: path: /tmp/qg-core key: qg-core-192fcaf386cf5bbb464dca53a26949078240c100 @@ -53,6 +53,16 @@ jobs: pnpm install --frozen-lockfile pnpm run build + # Save only after a clean build, so a crashed install/build never caches a + # partial /tmp/qg-core (missing dist/cli.js) under the fixed-SHA key — which + # would make every later run hit the poisoned cache and skip the rebuild. + - name: Save quality-gate engine build + if: steps.cache-qg.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: /tmp/qg-core + key: qg-core-192fcaf386cf5bbb464dca53a26949078240c100 + - name: Run adapter run: ./.quality-gate/adapter.sh env: diff --git a/.quality-gate/adapter.sh b/.quality-gate/adapter.sh index a105f2d5..d5164142 100755 --- a/.quality-gate/adapter.sh +++ b/.quality-gate/adapter.sh @@ -73,9 +73,12 @@ fi # --- 3. Duplication (jscpd) --- # Ignore paths beyond defaults: .next build cache, prisma generated client, # vitest scratch dirs, the landing static export, and tests fixtures (binary zips). +# The landing source (src/app/landing, src/components/landing) is excluded to match +# the coverage + file_size metrics: it is a presentation-heavy marketing surface +# whose repeated section markup would inflate duplication without signalling real debt. rm -rf .jscpd && mkdir -p .jscpd npx --yes jscpd@4 . --reporters json --output .jscpd \ - --ignore "**/node_modules/**,**/dist/**,**/coverage/**,**/.jscpd/**,**/.next/**,**/landing-export/**,**/prisma/migrations/**,**/tests/fixtures/**,**/.git/**" \ + --ignore "**/node_modules/**,**/dist/**,**/coverage/**,**/.jscpd/**,**/.next/**,**/landing-export/**,**/src/app/landing/**,**/src/components/landing/**,**/prisma/migrations/**,**/tests/fixtures/**,**/.git/**" \ --silent 2>/dev/null || true if [ -f .jscpd/jscpd-report.json ]; then From f413acba62537f39654578c019655b234c21682c Mon Sep 17 00:00:00 2001 From: AlexandreCamillo <43162926+AlexandreCamillo@users.noreply.github.com> Date: Thu, 28 May 2026 09:46:41 +0000 Subject: [PATCH 17/21] chore: remove GET /api/annotations/[id]/region endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The endpoint cropped a screenshot using pinCoords. After the comment-only annotation flow landed in PR #44, no new annotation row has either field set — the endpoint was unreachable for anything created after the cutover. Removing the route, the sharp-backed cropRegion helper, the unit test, sharp's pnpm "built-dependencies" entry, and every doc cross-reference (agent-loop endpoints + INDEX, api INDEX, feature catalog, stack, storage, routes, ci, frontend INDEX, task-rules, testing). Legacy annotation rows with screenshotPath + pinCoords populated will no longer be reachable through this URL — orchestrators relying on it should fetch the full screenshot via /api/annotations/[id]/screenshot. --- docs/INDEX.md | 2 +- docs/agent-loop/INDEX.md | 2 +- docs/agent-loop/endpoints.md | 27 ---------- docs/api/INDEX.md | 1 - docs/api/routes.md | 2 +- docs/api/storage.md | 7 +-- docs/ci.md | 2 +- docs/feature-catalog.md | 1 - docs/frontend/INDEX.md | 1 - docs/stack.md | 6 +-- docs/task-rules.md | 2 +- docs/testing.md | 10 +--- package.json | 4 +- pnpm-lock.yaml | 7 ++- src/app/api/annotations/[id]/region/route.ts | 55 -------------------- src/lib/region/crop.ts | 25 --------- tests/unit/lib/region/crop.test.ts | 47 ----------------- 17 files changed, 13 insertions(+), 188 deletions(-) delete mode 100644 src/app/api/annotations/[id]/region/route.ts delete mode 100644 src/lib/region/crop.ts delete mode 100644 tests/unit/lib/region/crop.test.ts diff --git a/docs/INDEX.md b/docs/INDEX.md index 63f021de..1a32e827 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -43,7 +43,7 @@ Start here to find which docs apply to your task. If multiple docs are relevant, - [Agent-loop INDEX](agent-loop/INDEX.md) — overview + endpoint map - [Overview](agent-loop/overview.md) — the user→agent→user cycle -- [Endpoints](agent-loop/endpoints.md) — `/intent`, `/context`, `/version-patch`, `/region`, `/diff` +- [Endpoints](agent-loop/endpoints.md) — `/intent`, `/context`, `/version-patch`, `/diff` - [Intent payload](agent-loop/intent-payload.md) — what `/intent` returns, sidecar caching, invalidation - [Patch format](agent-loop/patch-format.md) — unified-diff conventions for `/version-patch` - [Chips](agent-loop/chips.md) — G1 intent vocabulary (`visual` / `copy` / `behavior` / `other`) diff --git a/docs/agent-loop/INDEX.md b/docs/agent-loop/INDEX.md index b3f35d5d..bc0b223f 100644 --- a/docs/agent-loop/INDEX.md +++ b/docs/agent-loop/INDEX.md @@ -9,7 +9,7 @@ Silent drift in any of these endpoints breaks consumers. See the [agent-loop rul ## Read first - [Overview](overview.md) — the cycle end-to-end with byte costs -- [Endpoints](endpoints.md) — `/context`, `/version-patch`, `/region`, `/diff` +- [Endpoints](endpoints.md) — `/context`, `/version-patch`, `/diff` - [Uploads](uploads.md) — `POST /api/mockups`, `POST /api/mockups/[id]/version` (raw HTML + zip, size cap) - [Patch format](patch-format.md) — unified-diff conventions for `/version-patch` diff --git a/docs/agent-loop/endpoints.md b/docs/agent-loop/endpoints.md index 4cef78d7..23da8f97 100644 --- a/docs/agent-loop/endpoints.md +++ b/docs/agent-loop/endpoints.md @@ -185,33 +185,6 @@ Step 4 is orchestrator-decided. Most fix cycles do not close the mockup (more an **Rename caveat:** `name` changes the slug (the canonical URL). The owner-or-admin gate means agents can only rename mockups they themselves uploaded. If the slug changes, existing orchestrator bookmarks to `/projects//…` break. -## `GET /api/annotations/[id]/region` - -Bbox-cropped PNG of the annotation's screenshot. Sidecar-cached. - -**Auth:** cookie OR Bearer. - -**Response 200:** -- `Content-Type: image/png` -- `Cache-Control: private, max-age=300` -- Body: cropped PNG (typically 5–50 KB vs 200–700 KB for the full screenshot) - -**Errors:** - -| Status | `error` | When | -|---|---|---| -| 401 | `unauthorized` | No identity | -| 404 | `not_found` | Annotation row doesn't exist | -| 404 | `no_pin_coords` | Annotation has `pinCoords: null` (no drawn shapes) | -| 404 | `screenshot_missing` | Filesystem state corrupted | -| 500 | `invalid_pin_coords` | Stored `pinCoords` JSON is malformed | - -**Bbox source:** `Annotation.pinCoords.{bboxX, bboxY, bboxW, bboxH}`, with a fixed 20px padding around the bbox clamped at image edges. - -**No query params:** the bbox is fully derived from the stored pin coords. A future `?bbox=x,y,w,h` override would let agents request a different crop, but adding it would mean splitting the cache key — out of scope for v1.3. - -**Caching:** sidecar `region.png`. Regenerated when `screenshot.png`'s mtime is newer than `region.png`'s. Edits to `pinCoords` (none today; pinCoords are immutable per annotation) would need a cache-key extension. - ## `GET /api/mockups/[id]/diff` Text-mode unified diff between two versions of a mockup. diff --git a/docs/api/INDEX.md b/docs/api/INDEX.md index 0e9ae16c..c60ecd14 100644 --- a/docs/api/INDEX.md +++ b/docs/api/INDEX.md @@ -80,7 +80,6 @@ The Markup API is a set of Next.js App Router route handlers under `src/app/api/ | `POST` | `/api/mockups/[id]/annotations` | Create (JSON: body + anchors + colorIndex) | | `GET` | `/api/annotations/[id]` | Single annotation metadata | | `GET` | `/api/annotations/[id]/screenshot` | Full PNG screenshot | -| `GET` | `/api/annotations/[id]/region` | Bbox-cropped PNG (sidecar-cached) | | `GET` | `/api/annotations/[id]/detail` | Aggregator for `/annotations/[id]` — annotation + screenshot dims + thread + names + mockup blurb + viewerHref | ### Agent diff --git a/docs/api/routes.md b/docs/api/routes.md index 36fca7ee..774e56d7 100644 --- a/docs/api/routes.md +++ b/docs/api/routes.md @@ -233,7 +233,7 @@ Currently no route streams. When a route needs to stream a large payload (e.g. a Routes that produce expensive payloads use either: -- **Sidecar files** on disk (e.g. `intent.json`, `region.png`) keyed by `(input_mtime, current_version_id)` — see [Storage](storage.md) +- **Sidecar files** on disk (e.g. `intent.json`) keyed by `(input_mtime, current_version_id)` — see [Storage](storage.md) - **ETag headers** for in-memory aggregations (e.g. `/agent/context`) Don't add HTTP `Cache-Control: max-age=…` to mutable resources without thinking through the invalidation path; sidecars + ETag give the same effect with explicit invalidation hooks. diff --git a/docs/api/storage.md b/docs/api/storage.md index c15396cc..83324e20 100644 --- a/docs/api/storage.md +++ b/docs/api/storage.md @@ -17,8 +17,7 @@ ${DATA_DIR}/ │ └── annotations/ │ └── / │ ├── screenshot.png # base capture (immutable per annotation) -│ ├── intent.json # sidecar cache (regenerated on read) -│ └── region.png # bbox crop (regenerated on read) +│ └── intent.json # sidecar cache (regenerated on read) └── tmp/ └── version-.zip # short-lived patch composition staging ``` @@ -42,10 +41,6 @@ Routes and services compose paths via these helpers — never hardcode the layou Files derived from the primary blobs are stored as **sidecars** in the same directory. Conventions: -| Sidecar | Source | Cache key | Invalidator | -|---|---|---|---| -| `region.png` | `screenshot.png` + the annotation's `pinCoords` | `screenshot_mtime` (compared against `region.png`'s mtime) | regenerated when `screenshot.png` is newer than `region.png` | - The sidecar wrapping format for JSON caches is: ```json diff --git a/docs/ci.md b/docs/ci.md index ef149434..9f093171 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -75,7 +75,7 @@ These rules are enforced by biome + tsc + the test suite. Violating them turns t ### Agent-loop endpoints 1. **Auth via `identify(req)`** — accepts cookie OR Bearer; returns `{kind: 'user', userId} | {kind: 'agent', tokenId}` or `null`. Never re-implement auth in a route. -2. **Sidecar files are atomic-write candidates.** Writes to `intent.json` and `region.png` go directly to disk; if a future change needs concurrency safety, write to `*.tmp` and rename. +2. **Sidecar files are atomic-write candidates.** Writes to `intent.json` go directly to disk; if a future change needs concurrency safety, write to `*.tmp` and rename. 3. **Cache invalidation runs BEFORE the new write.** When a route mutates a primary blob that has derived sidecars, it deletes the stale sidecars before writing the new blob so a concurrent reader never pairs a fresh primary with a stale sidecar. 4. **The `/context` aggregator delegates to `/intent`** by importing the GET handler directly — no HTTP loopback. This keeps tests deterministic and avoids depending on `APP_URL` being reachable from the server. diff --git a/docs/feature-catalog.md b/docs/feature-catalog.md index 3424d7cf..e6faa4a7 100644 --- a/docs/feature-catalog.md +++ b/docs/feature-catalog.md @@ -831,7 +831,6 @@ Surfaces that compose the agent automation cycle. These are API-driven but have | `agent-context-read` | Single-call context aggregator: annotation + thread + inline source + diff_since_creation + project + folder_path. ETag for short-circuit | N/A (agent-only) | `GET /api/agent/context/[annotationId]` | | `agent-version-patch` | Diff-based version update with `base_version_id`. Binary files reused by reference. 409 on conflict (stale base) | new version in `mockup-viewer-versions` | `PATCH /api/mockups/[id]/version-patch` | | `agent-mockup-patch` | Mockup-metadata mutation. All fields (`name`, `status`, `projectId`, `folderId`, `position`) are gated by `requireOwnerOrAdmin`: the caller must be the recorded `(createdBy, createdByType)` of the mockup OR an admin. Agents can rename/move/status-change mockups they uploaded; they receive 403 `forbidden_owner` on mockups created by others. Optional close-out step after the last thread on a mockup is resolved. | `mockup-status-pill`, `mockup-actions-menu` (existing UI surfaces) | `PATCH /api/mockups/[id]` | -| `agent-region-crop` | Bbox-cropped screenshot (sidecar-cached) | N/A (agent-only) | `GET /api/annotations/[id]/region` | | `agent-diff-text` | Text-mode unified or JSON diff between versions | used by `diff-viewer` | `GET /api/mockups/[id]/diff` | | `agent-thread-reply` | Agent replies in thread (`authorType: 'agent'`) | `thread-timeline-message` | `POST /api/threads/[id]/reply` | | `agent-thread-resolve` | Thread resolution | `thread-timeline-resolve-btn` | `POST /api/threads/[id]/resolve` | diff --git a/docs/frontend/INDEX.md b/docs/frontend/INDEX.md index f31445a9..7d550dbf 100644 --- a/docs/frontend/INDEX.md +++ b/docs/frontend/INDEX.md @@ -46,7 +46,6 @@ The route group `(app)` mounts `AppShell` once (via `(app)/layout.tsx`) so the s - Mockup card thumbnails are served from `/api/mockups/[id]/thumbnail`. The route serves the file when ≥ 64 bytes and a valid PNG; smaller / corrupt files trigger a 404 and the card falls back to a deterministic monogram (palette-cycled hue from a 6-entry list keyed off the mockup id) - Annotation screenshots come from `/api/annotations/[id]/screenshot` — full PNG, no transformation -- Bbox-cropped screenshots come from `/api/annotations/[id]/region` — see [`docs/agent-loop/endpoints.md`](../agent-loop/endpoints.md) ## State ownership diff --git a/docs/stack.md b/docs/stack.md index 7081fee6..70ef9fb2 100644 --- a/docs/stack.md +++ b/docs/stack.md @@ -30,7 +30,6 @@ Markup is a single-process Next.js application served from a Docker container. T ## Server-side image + DOM -- **`sharp`** for PNG cropping (`/api/annotations/[id]/region`) - **`puppeteer`** (with bundled chromium, ~150 MB) for server-side DOM resolution at the bbox the user drew (`/api/annotations/[id]/intent`) - **`diff`** + **`@types/diff`** for unified-diff apply/render (`/api/mockups/[id]/version-patch`, `/api/mockups/[id]/diff`) - **`jszip`** for in-memory zip composition when applying patches @@ -67,7 +66,7 @@ src/ app/ # Next.js App Router api/ # API routes (route.ts files) agent/context/[annotationId]/route.ts - annotations/[id]/{intent,region,screenshot,messages}/route.ts + annotations/[id]/{intent,screenshot,messages}/route.ts mockups/[id]/{version,version-patch,diff,thumbnail,annotations,versions/[vid]/{source,promote}}/route.ts threads/[id]/{reply,resolve,reopen}/route.ts auth/{login,logout,setup}/route.ts @@ -89,7 +88,6 @@ src/ diff/ # apply-unified, render-unified intent/ # parser, contrast, cache, puppeteer singleton mockup/ # service, storage, zip-extractor - region/crop.ts # sharp-based bbox crop boot.ts, env.ts, logger.ts, prisma.ts styles/tokens.css prisma/ @@ -98,7 +96,7 @@ prisma/ scripts/ # one-shot maintenance scripts (tsx-run) tests/ integration/{annotation,api,auth,lib,mockup}/*.test.ts - unit/lib/{intent,diff,region,…}/*.test.ts + unit/lib/{intent,diff,…}/*.test.ts fixtures/mockups/*.zip setup.ts docs/ # this directory diff --git a/docs/task-rules.md b/docs/task-rules.md index aac825fa..d070bc01 100644 --- a/docs/task-rules.md +++ b/docs/task-rules.md @@ -4,7 +4,7 @@ Read [`docs/INDEX.md`](INDEX.md) to find which docs apply to your task. If multiple docs are relevant, read all of them before starting. This is non-negotiable regardless of how simple the change appears. -If the change touches an agent-loop endpoint (`/intent`, `/context`, `/version-patch`, `/region`, `/diff`, or `POST /annotations`), consult [`docs/agent-loop/`](agent-loop/INDEX.md) **before** writing code. See the [agent-loop rule](../CLAUDE.md#agent-loop-rule-strict--non-negotiable). +If the change touches an agent-loop endpoint (`/intent`, `/context`, `/version-patch`, `/diff`, or `POST /annotations`), consult [`docs/agent-loop/`](agent-loop/INDEX.md) **before** writing code. See the [agent-loop rule](../CLAUDE.md#agent-loop-rule-strict--non-negotiable). If the change replicates a `tests/fixtures/mockups/.zip` fixture (lumen-coffee, helio-pricing, drone-console), follow the [mockup-replication rule](../CLAUDE.md#mockup-replication-rule-when-the-user-points-at-a-fixture). diff --git a/docs/testing.md b/docs/testing.md index 4e05ae3a..1bfae766 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -86,7 +86,7 @@ return token; App Router routes are imported and invoked as functions: ```ts -import { GET } from '@/app/api/annotations/[id]/region/route'; +import { GET } from '@/app/api/annotations/[id]/detail/route'; import { POST as createMockupRoute } from '@/app/api/mockups/route'; const res = await GET( @@ -118,14 +118,6 @@ const png = Buffer.from([ ]); ``` -For tests that need real images (`/region.png` crop, puppeteer rendering), use `sharp` to generate a buffer: - -```ts -const png = await sharp({ - create: { width: 200, height: 200, channels: 4, background: { r: 100, g: 200, b: 100, alpha: 1 } }, -}).png().toBuffer(); -``` - ## Fixtures - **`tests/fixtures/mockups/valid-simple.zip`** — 28-byte `` for tests that just need a valid zip diff --git a/package.json b/package.json index dedb141b..e40dcdae 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,6 @@ "react-dom": "latest", "react-icons": "^5.6.0", "server-only": "^0.0.1", - "sharp": "^0.34.5", "yauzl": "latest", "zod": "latest" }, @@ -80,8 +79,7 @@ "better-sqlite3", "esbuild", "prisma", - "puppeteer", - "sharp" + "puppeteer" ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 07b0ddf9..df31513f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,9 +83,6 @@ importers: server-only: specifier: ^0.0.1 version: 0.0.1 - sharp: - specifier: ^0.34.5 - version: 0.34.5 yauzl: specifier: latest version: 3.3.0 @@ -3093,7 +3090,8 @@ snapshots: dependencies: hono: 4.12.18 - '@img/colour@1.1.0': {} + '@img/colour@1.1.0': + optional: true '@img/sharp-darwin-arm64@0.34.5': optionalDependencies: @@ -4950,6 +4948,7 @@ snapshots: '@img/sharp-win32-arm64': 0.34.5 '@img/sharp-win32-ia32': 0.34.5 '@img/sharp-win32-x64': 0.34.5 + optional: true shebang-command@2.0.0: dependencies: diff --git a/src/app/api/annotations/[id]/region/route.ts b/src/app/api/annotations/[id]/region/route.ts deleted file mode 100644 index 9588d142..00000000 --- a/src/app/api/annotations/[id]/region/route.ts +++ /dev/null @@ -1,55 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import { NextResponse } from 'next/server'; -import { parsePinCoords } from '@/lib/annotation/pin-coords'; -import { identify } from '@/lib/auth/identify'; -import { env } from '@/lib/env'; -import { prisma } from '@/lib/prisma'; -import { cropRegion } from '@/lib/region/crop'; - -const PADDING = 20; - -export async function GET(req: Request, ctx: { params: Promise<{ id: string }> }) { - const ident = await identify(req); - if (!ident) return NextResponse.json({ error: 'unauthorized' }, { status: 401 }); - const { id } = await ctx.params; - const annotation = await prisma.annotation.findUnique({ where: { id } }); - if (!annotation) return NextResponse.json({ error: 'not_found' }, { status: 404 }); - if (!annotation.pinCoords) { - return NextResponse.json({ error: 'no_pin_coords' }, { status: 404 }); - } - const pin = parsePinCoords(annotation.pinCoords); - if (!pin) return NextResponse.json({ error: 'invalid_pin_coords' }, { status: 500 }); - - const screenshotAbs = path.join(env().DATA_DIR, annotation.screenshotPath); - if (!fs.existsSync(screenshotAbs)) { - return NextResponse.json({ error: 'screenshot_missing' }, { status: 404 }); - } - const annDir = path.dirname(screenshotAbs); - const sidecarPath = path.join(annDir, 'region.png'); - const screenshotMtime = fs.statSync(screenshotAbs).mtimeMs; - const sidecarMtime = fs.existsSync(sidecarPath) ? fs.statSync(sidecarPath).mtimeMs : 0; - - let body: Buffer; - if (sidecarMtime >= screenshotMtime && sidecarMtime > 0) { - body = fs.readFileSync(sidecarPath); - } else { - const src = fs.readFileSync(screenshotAbs); - body = await cropRegion(src, { - x: pin.bboxX, - y: pin.bboxY, - w: pin.bboxW, - h: pin.bboxH, - padding: PADDING, - }); - fs.writeFileSync(sidecarPath, body); - } - return new NextResponse(body as unknown as BodyInit, { - headers: { - 'Content-Type': 'image/png', - 'Cache-Control': 'private, max-age=300', - }, - }); -} - -export const dynamic = 'force-dynamic'; diff --git a/src/lib/region/crop.ts b/src/lib/region/crop.ts deleted file mode 100644 index 8307d591..00000000 --- a/src/lib/region/crop.ts +++ /dev/null @@ -1,25 +0,0 @@ -import 'server-only'; - -import sharp from 'sharp'; - -interface CropInput { - x: number; - y: number; - w: number; - h: number; - padding?: number; -} - -export async function cropRegion(src: Buffer, input: CropInput): Promise { - const padding = input.padding ?? 0; - const meta = await sharp(src).metadata(); - const imgW = meta.width ?? 0; - const imgH = meta.height ?? 0; - const left = Math.max(0, Math.floor(input.x - padding)); - const top = Math.max(0, Math.floor(input.y - padding)); - const right = Math.min(imgW, Math.ceil(input.x + input.w + padding)); - const bottom = Math.min(imgH, Math.ceil(input.y + input.h + padding)); - const width = Math.max(1, right - left); - const height = Math.max(1, bottom - top); - return sharp(src).extract({ left, top, width, height }).png().toBuffer(); -} diff --git a/tests/unit/lib/region/crop.test.ts b/tests/unit/lib/region/crop.test.ts deleted file mode 100644 index 749eaa47..00000000 --- a/tests/unit/lib/region/crop.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import sharp from 'sharp'; -import { describe, expect, it } from 'vitest'; -import { cropRegion } from '@/lib/region/crop'; - -async function makeRedSquarePng(w: number, h: number): Promise { - return sharp({ - create: { width: w, height: h, channels: 4, background: { r: 255, g: 0, b: 0, alpha: 1 } }, - }) - .png() - .toBuffer(); -} - -describe('cropRegion', () => { - it('crops the requested bbox with optional padding', async () => { - const src = await makeRedSquarePng(200, 200); - const out = await cropRegion(src, { x: 50, y: 50, w: 100, h: 100, padding: 10 }); - const meta = await sharp(out).metadata(); - expect(meta.width).toBe(120); // 100 + 10*2 - expect(meta.height).toBe(120); - }); - - it('clamps padding at left/top edges', async () => { - const src = await makeRedSquarePng(200, 200); - const out = await cropRegion(src, { x: 0, y: 0, w: 100, h: 100, padding: 50 }); - const meta = await sharp(out).metadata(); - // Left edge clamped to 0; right gets full padding. So width = 100 + 50 = 150. - expect(meta.width).toBe(150); - expect(meta.height).toBe(150); - }); - - it('returns the entire image when bbox + padding cover it', async () => { - const src = await makeRedSquarePng(200, 200); - const out = await cropRegion(src, { x: 0, y: 0, w: 200, h: 200, padding: 0 }); - const meta = await sharp(out).metadata(); - expect(meta.width).toBe(200); - expect(meta.height).toBe(200); - }); - - it('handles bbox at right edge clamping to image width', async () => { - const src = await makeRedSquarePng(200, 200); - const out = await cropRegion(src, { x: 150, y: 50, w: 100, h: 100, padding: 0 }); - const meta = await sharp(out).metadata(); - // Right side clamped: x=150, w=100 -> should clamp to 200 width = 50px wide - expect(meta.width).toBe(50); - expect(meta.height).toBe(100); - }); -}); From 1ea2a682a570882485e07777233869e8517e64f5 Mon Sep 17 00:00:00 2001 From: AlexandreCamillo <43162926+AlexandreCamillo@users.noreply.github.com> Date: Fri, 29 May 2026 18:50:40 +0000 Subject: [PATCH 18/21] style: biome-format quality-gate.config.json --- quality-gate.config.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/quality-gate.config.json b/quality-gate.config.json index 17c332d9..6abb96dc 100644 --- a/quality-gate.config.json +++ b/quality-gate.config.json @@ -10,11 +10,11 @@ "epsilon": 0.1 }, "metrics": { - "coverage": { "enabled": true }, + "coverage": { "enabled": true }, "duplication": { "enabled": true }, - "lint": { "enabled": true }, - "file_size": { "enabled": true }, - "security": { + "lint": { "enabled": true }, + "file_size": { "enabled": true }, + "security": { "enabled": true, "block_severities": ["critical"], "warn_severities": ["high"] From f98d75764ded4acb19044baea55777817ab17086 Mon Sep 17 00:00:00 2001 From: AlexandreCamillo <43162926+AlexandreCamillo@users.noreply.github.com> Date: Fri, 29 May 2026 20:08:01 +0000 Subject: [PATCH 19/21] refactor(quality-gate): dedupe workflows into composite action; pin jscpd Extract the shared prelude + engine clone/build into .github/actions/quality-gate-prepare so the PR and baseline workflows no longer duplicate ~50 lines and the engine SHA lives in one place. Run jscpd from a pinned devDependency instead of re-downloading via npx each run, write adapter scratch files to a mktemp dir (keeping the repo clean and out of jscpd's own scan), and count file lines with awk so a missing trailing newline can't slip a file past the size gate. --- .../actions/quality-gate-prepare/action.yml | 72 ++ .github/workflows/quality-gate-main.yml | 49 +- .github/workflows/quality-gate-pr.yml | 50 +- .gitignore | 6 +- .quality-gate/adapter.sh | 31 +- docs/quality-gate.md | 6 +- package.json | 1 + pnpm-lock.yaml | 747 ++++++++++++++++++ 8 files changed, 843 insertions(+), 119 deletions(-) create mode 100644 .github/actions/quality-gate-prepare/action.yml diff --git a/.github/actions/quality-gate-prepare/action.yml b/.github/actions/quality-gate-prepare/action.yml new file mode 100644 index 00000000..452fe275 --- /dev/null +++ b/.github/actions/quality-gate-prepare/action.yml @@ -0,0 +1,72 @@ +name: Quality Gate prepare +description: >- + Set up the toolchain, run the test suite with coverage, build the pinned + quality-gate engine, and run the adapter. Shared by the PR and baseline + workflows so the prelude and engine build stay in one place. The adapter + writes its six metric files to ${{ runner.temp }}/qg. + +inputs: + engine-sha: + description: Commit of alkg-cloud/quality-gate to build the engine from. + default: 192fcaf386cf5bbb464dca53a26949078240c100 + +runs: + using: composite + steps: + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - run: pnpm install --frozen-lockfile + shell: bash + - run: pnpm prisma generate + shell: bash + - run: pnpm prisma migrate deploy + shell: bash + env: + DATABASE_URL: file:./prisma/test.db + - run: pnpm tsx tests/fixtures/build-fixtures.ts + shell: bash + + - run: pnpm test --coverage + shell: bash + env: + DATABASE_URL: file:./prisma/test.db + + - name: Restore quality-gate engine build + id: cache-qg + uses: actions/cache/restore@v4 + with: + path: /tmp/qg-core + key: qg-core-${{ inputs.engine-sha }} + + - name: Build quality-gate engine (upstream not on npm) + if: steps.cache-qg.outputs.cache-hit != 'true' + shell: bash + run: | + git init /tmp/qg-core + cd /tmp/qg-core + git remote add origin https://github.com/alkg-cloud/quality-gate + git fetch --depth 1 origin ${{ inputs.engine-sha }} + git checkout ${{ inputs.engine-sha }} + pnpm install --frozen-lockfile + pnpm run build + + # Save only after a clean build, so a crashed install/build never caches a + # partial /tmp/qg-core (missing dist/cli.js) under the fixed-SHA key — which + # would make every later run hit the poisoned cache and skip the rebuild. + - name: Save quality-gate engine build + if: steps.cache-qg.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: /tmp/qg-core + key: qg-core-${{ inputs.engine-sha }} + + - name: Run adapter + shell: bash + run: ./.quality-gate/adapter.sh + env: + QG_OUTPUT_DIR: ${{ runner.temp }}/qg + QG_CONFIG: ./quality-gate.config.json diff --git a/.github/workflows/quality-gate-main.yml b/.github/workflows/quality-gate-main.yml index e853fc19..7c52ed65 100644 --- a/.github/workflows/quality-gate-main.yml +++ b/.github/workflows/quality-gate-main.yml @@ -17,55 +17,8 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: pnpm - - - run: pnpm install --frozen-lockfile - - run: pnpm prisma generate - - run: pnpm prisma migrate deploy - env: - DATABASE_URL: file:./prisma/test.db - - run: pnpm tsx tests/fixtures/build-fixtures.ts - - - run: pnpm test --coverage - env: - DATABASE_URL: file:./prisma/test.db - - - name: Restore quality-gate engine build - id: cache-qg - uses: actions/cache/restore@v4 - with: - path: /tmp/qg-core - key: qg-core-192fcaf386cf5bbb464dca53a26949078240c100 - - - name: Build quality-gate engine (upstream not on npm) - if: steps.cache-qg.outputs.cache-hit != 'true' - run: | - git clone --depth 1 https://github.com/alkg-cloud/quality-gate /tmp/qg-core - cd /tmp/qg-core - git fetch --depth 1 origin 192fcaf386cf5bbb464dca53a26949078240c100 - git checkout 192fcaf386cf5bbb464dca53a26949078240c100 - pnpm install --frozen-lockfile - pnpm run build - # Save only after a clean build, so a crashed install/build never caches a - # partial /tmp/qg-core (missing dist/cli.js) under the fixed-SHA key — which - # would make every later run hit the poisoned cache and skip the rebuild. - - name: Save quality-gate engine build - if: steps.cache-qg.outputs.cache-hit != 'true' - uses: actions/cache/save@v4 - with: - path: /tmp/qg-core - key: qg-core-192fcaf386cf5bbb464dca53a26949078240c100 - - - name: Run adapter - run: ./.quality-gate/adapter.sh - env: - QG_OUTPUT_DIR: ${{ runner.temp }}/qg - QG_CONFIG: ./quality-gate.config.json + - uses: ./.github/actions/quality-gate-prepare - name: Update baseline on orphan branch run: | diff --git a/.github/workflows/quality-gate-pr.yml b/.github/workflows/quality-gate-pr.yml index 3a038e42..44121de8 100644 --- a/.github/workflows/quality-gate-pr.yml +++ b/.github/workflows/quality-gate-pr.yml @@ -18,56 +18,8 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: pnpm - - - run: pnpm install --frozen-lockfile - - run: pnpm prisma generate - - run: pnpm prisma migrate deploy - env: - DATABASE_URL: file:./prisma/test.db - - run: pnpm tsx tests/fixtures/build-fixtures.ts - - # Run vitest with coverage so the adapter has coverage-summary.json to consume. - - run: pnpm test --coverage - env: - DATABASE_URL: file:./prisma/test.db - - - name: Restore quality-gate engine build - id: cache-qg - uses: actions/cache/restore@v4 - with: - path: /tmp/qg-core - key: qg-core-192fcaf386cf5bbb464dca53a26949078240c100 - - - name: Build quality-gate engine (upstream not on npm) - if: steps.cache-qg.outputs.cache-hit != 'true' - run: | - git clone --depth 1 https://github.com/alkg-cloud/quality-gate /tmp/qg-core - cd /tmp/qg-core - git fetch --depth 1 origin 192fcaf386cf5bbb464dca53a26949078240c100 - git checkout 192fcaf386cf5bbb464dca53a26949078240c100 - pnpm install --frozen-lockfile - pnpm run build - - # Save only after a clean build, so a crashed install/build never caches a - # partial /tmp/qg-core (missing dist/cli.js) under the fixed-SHA key — which - # would make every later run hit the poisoned cache and skip the rebuild. - - name: Save quality-gate engine build - if: steps.cache-qg.outputs.cache-hit != 'true' - uses: actions/cache/save@v4 - with: - path: /tmp/qg-core - key: qg-core-192fcaf386cf5bbb464dca53a26949078240c100 - - name: Run adapter - run: ./.quality-gate/adapter.sh - env: - QG_OUTPUT_DIR: ${{ runner.temp }}/qg - QG_CONFIG: ./quality-gate.config.json + - uses: ./.github/actions/quality-gate-prepare - name: Run quality gate run: | diff --git a/.gitignore b/.gitignore index 617a2633..011c851b 100644 --- a/.gitignore +++ b/.gitignore @@ -42,9 +42,5 @@ public/_qa-mobile.html landing-export/.next/ landing-export/out/ -# quality-gate adapter scratch files -.jscpd/ -.biome-report.json -.npm-audit.json -.pnpm-audit.json +# quality-gate adapter local output dir /qg-output/ diff --git a/.quality-gate/adapter.sh b/.quality-gate/adapter.sh index d5164142..35a355a0 100755 --- a/.quality-gate/adapter.sh +++ b/.quality-gate/adapter.sh @@ -21,9 +21,13 @@ mkdir -p "$QG_OUTPUT_DIR" ROOT="$PWD" MAX_FILE_LINES=$(jq -r '.thresholds.MAX_FILE_LINES' "$QG_CONFIG") -# Each metric is filled in by a dedicated section below. -# Sections must produce a strictly schema-compliant file even on tool failure -# (use the {"_skipped":"…"} fallback to keep the gate green for that metric). +# Intermediate tool reports live outside the repo tree so they never pollute the +# working copy nor get picked up by jscpd's own scan of ".". +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +# Each metric section must produce a strictly schema-compliant file even on tool +# failure — use the {"_skipped":"…"} fallback to keep the gate green for that metric. # --- 1. Coverage (consumes coverage/coverage-summary.json produced by vitest) --- if [ -f coverage/coverage-summary.json ]; then @@ -48,8 +52,8 @@ else fi # --- 2. Lint (biome --reporter=json; biome exits 1 with violations, hence || true) --- -pnpm exec biome check . --reporter=json 2>/dev/null > .biome-report.json || true -if [ -s .biome-report.json ] && jq -e '.diagnostics' .biome-report.json > /dev/null 2>&1; then +pnpm exec biome check . --reporter=json 2>/dev/null > "$WORK/biome-report.json" || true +if [ -s "$WORK/biome-report.json" ] && jq -e '.diagnostics' "$WORK/biome-report.json" > /dev/null 2>&1; then jq --arg root "$ROOT/" ' # Aggregate diagnostics by file path; biome paths can be absolute or relative. # Biome 2.x emits .location.path as a string; older shapes used {file: "..."}. @@ -65,7 +69,7 @@ if [ -s .biome-report.json ] && jq -e '.diagnostics' .biome-report.json > /dev/n to_entries[] | { path: .key, count: .value } ] | sort_by(.path) } - ' .biome-report.json > "$QG_OUTPUT_DIR/lint.json" + ' "$WORK/biome-report.json" > "$QG_OUTPUT_DIR/lint.json" else echo '{"_skipped":"biome produced no parseable JSON report"}' > "$QG_OUTPUT_DIR/lint.json" fi @@ -76,16 +80,15 @@ fi # The landing source (src/app/landing, src/components/landing) is excluded to match # the coverage + file_size metrics: it is a presentation-heavy marketing surface # whose repeated section markup would inflate duplication without signalling real debt. -rm -rf .jscpd && mkdir -p .jscpd -npx --yes jscpd@4 . --reporters json --output .jscpd \ +pnpm exec jscpd . --reporters json --output "$WORK/jscpd" \ --ignore "**/node_modules/**,**/dist/**,**/coverage/**,**/.jscpd/**,**/.next/**,**/landing-export/**,**/src/app/landing/**,**/src/components/landing/**,**/prisma/migrations/**,**/tests/fixtures/**,**/.git/**" \ --silent 2>/dev/null || true -if [ -f .jscpd/jscpd-report.json ]; then +if [ -f "$WORK/jscpd/jscpd-report.json" ]; then jq '{ pct: (.statistics.total.percentage // 0), clones: (.statistics.total.clones // 0) - }' .jscpd/jscpd-report.json > "$QG_OUTPUT_DIR/duplication.json" + }' "$WORK/jscpd/jscpd-report.json" > "$QG_OUTPUT_DIR/duplication.json" else echo '{"_skipped":"jscpd produced no report"}' > "$QG_OUTPUT_DIR/duplication.json" fi @@ -102,7 +105,7 @@ fi 2>/dev/null \ || true } | while read -r f; do - lines=$(wc -l < "$f") + lines=$(awk 'END { print NR }' "$f") if [ "$lines" -ge "$MAX_FILE_LINES" ]; then printf '{"path":"%s","lines":%d}\n' "$f" "$lines" fi @@ -112,14 +115,14 @@ fi }' > "$QG_OUTPUT_DIR/file_size.json" # --- 5. Security (pnpm audit) --- -pnpm audit --json > .pnpm-audit.json 2>/dev/null || true -if [ -s .pnpm-audit.json ] && jq -e '.metadata.vulnerabilities' .pnpm-audit.json > /dev/null 2>&1; then +pnpm audit --json > "$WORK/pnpm-audit.json" 2>/dev/null || true +if [ -s "$WORK/pnpm-audit.json" ] && jq -e '.metadata.vulnerabilities' "$WORK/pnpm-audit.json" > /dev/null 2>&1; then jq '.metadata.vulnerabilities | { critical: (.critical // 0), high: (.high // 0), moderate: (.moderate // 0), low: (.low // 0) - }' .pnpm-audit.json > "$QG_OUTPUT_DIR/security.json" + }' "$WORK/pnpm-audit.json" > "$QG_OUTPUT_DIR/security.json" else echo '{"_skipped":"pnpm audit produced no metadata.vulnerabilities"}' > "$QG_OUTPUT_DIR/security.json" fi diff --git a/docs/quality-gate.md b/docs/quality-gate.md index 8b37f48e..a4bf5fdf 100644 --- a/docs/quality-gate.md +++ b/docs/quality-gate.md @@ -2,7 +2,7 @@ Merges to `main` are gated by `@quality-gate/core` (upstream: [`alkg-cloud/quality-gate`](https://github.com/alkg-cloud/quality-gate), pinned commit `192fcaf386cf5bbb464dca53a26949078240c100`). The gate ratchets five metrics against a baseline stored on the orphan branch `quality-metrics` in this repo. -The upstream package is not published to npm. Both gate workflows clone the repo at the pinned commit and build the CLI on each run. To bump the pin, edit the SHA in both `.github/workflows/quality-gate-*.yml` files. +The upstream package is not published to npm. Both gate workflows share the composite action `.github/actions/quality-gate-prepare`, which clones the repo at the pinned commit and builds the CLI on each run (the build is cached on the SHA). To bump the pin, edit the `engine-sha` default in that action. ## Metrics and rules @@ -10,7 +10,7 @@ The upstream package is not published to npm. Both gate workflows clone the repo |---|---|---| | `coverage` | `coverage/coverage-summary.json` from vitest (v8 provider) | Global `lines_pct` drops > `epsilon` (0.10pp) below baseline. New file with coverage below `MIN_NEW_FILE_COVERAGE` (60%) also blocks. | | `lint` | `pnpm exec biome check . --reporter=json` | Total diagnostic count rises above baseline; OR any per-file count rises; OR any new file contributes ≥1 diagnostic. | -| `duplication` | `npx jscpd` | Global `pct` rises > `epsilon` above baseline. | +| `duplication` | `pnpm exec jscpd` (pinned devDependency) | Global `pct` rises > `epsilon` above baseline. | | `file_size` | `find src/ tests/` with line count | Existing file's line count rises above its baseline count (when already above `MAX_FILE_LINES`, currently 500). New file with `lines >= MAX_FILE_LINES` blocks. | | `security` | `pnpm audit --json` | Any vulnerability in `block_severities` (currently `["critical"]`). `high` is a warning, not a block. | @@ -48,7 +48,7 @@ node /tmp/qg-core/dist/cli.js compare --metrics /tmp/qg/metrics.json --baseline - `.github/workflows/quality-gate-pr.yml` — runs on `pull_request` to `main`. Computes metrics, compares against baseline on `quality-metrics`, posts a sticky PR comment, fails the job on regression. - `.github/workflows/quality-gate-main.yml` — runs on `push` to `main`. Recomputes metrics and force-pushes the new baseline + badges to the `quality-metrics` orphan branch. -Both workflows clone and build the upstream engine at the pinned commit at the start of each run. +Both workflows run the shared `.github/actions/quality-gate-prepare` composite action, which sets up the toolchain, runs the suite with coverage, builds the pinned upstream engine, and runs the adapter. The PR-mode workflow's job name is `quality-gate / quality-gate` — this is the required check for branch protection. diff --git a/package.json b/package.json index e40dcdae..4b5b9ddd 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "@types/yauzl": "latest", "@types/yazl": "^3.3.1", "@vitest/coverage-v8": "latest", + "jscpd": "^4.2.4", "jsdom": "^29.1.1", "tsx": "latest", "typescript": "latest", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index df31513f..d71a07d7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -120,6 +120,9 @@ importers: '@vitest/coverage-v8': specifier: latest version: 4.1.5(vitest@4.1.5) + jscpd: + specifier: ^4.2.4 + version: 4.2.4 jsdom: specifier: ^29.1.1 version: 29.1.1(@noble/hashes@1.8.0) @@ -268,6 +271,10 @@ packages: resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} hasBin: true + '@colors/colors@1.5.0': + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + '@csstools/color-helpers@6.0.2': resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} engines: {node: '>=20.19.0'} @@ -676,6 +683,21 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jscpd/badge-reporter@4.2.4': + resolution: {integrity: sha512-g5vu05u0lX9rcHA0k3CptLfpOiuMzxh5+mUe2iYRAznTwH3ks6JAVAf9aPi5mBFttMCRiJh2zSt3xnSadHtMGg==} + + '@jscpd/core@4.2.4': + resolution: {integrity: sha512-9V9YzmmhYg9682kFqi+n0KGOhXNSoqxHbuIP3i/l/oSd6upBOnnSeBWDZMGOenQRQnyKEtCIbnS9YFz+3B+siQ==} + + '@jscpd/finder@4.2.4': + resolution: {integrity: sha512-4LLEuAAmAraud/TAAlB5BByVdWfy7SYiPKacj5yEggpkNs0qsw2kiZ5EyU3LonB+/vntJJEDDpJMmvOeS58e0A==} + + '@jscpd/html-reporter@4.2.4': + resolution: {integrity: sha512-6UljCTVGf7O+o6D6fs1zNBG+vR1PTn47W2mSgb5hzSrvNw60rLrVoAMZMnr/TeIEdd/OEgAu+icbdvvVBfnvJw==} + + '@jscpd/tokenizer@4.2.4': + resolution: {integrity: sha512-nM4kGyDvpcevt8t0zOsMQ82ShSc65c3LIQUHClTYwraiOGOmWgUQyen+JIiFCNF8eDCGR2Qa5iI5XBfGWYQzIg==} + '@kurkle/color@0.3.4': resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} @@ -744,6 +766,18 @@ packages: resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + '@oxc-project/types@0.128.0': resolution: {integrity: sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ==} @@ -1338,6 +1372,9 @@ packages: '@types/react@19.2.15': resolution: {integrity: sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==} + '@types/sarif@2.1.7': + resolution: {integrity: sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==} + '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} @@ -1382,6 +1419,11 @@ packages: '@vitest/utils@4.1.5': resolution: {integrity: sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==} + acorn@7.4.1: + resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} + engines: {node: '>=0.4.0'} + hasBin: true + agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -1411,6 +1453,12 @@ packages: aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + + assert-never@1.4.0: + resolution: {integrity: sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -1438,6 +1486,13 @@ packages: react-native-b4a: optional: true + babel-walk@3.0.0-canary-5: + resolution: {integrity: sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==} + engines: {node: '>= 10.0.0'} + + badgen@3.3.2: + resolution: {integrity: sha512-fbQwK9norfdzbdsoPwbLIAmgBXDGEme3jeIyqPAH7o6vp9lmuLHS7uXULvOiQ6XnMLkYNG4gDjILf74hgtTAug==} + bare-events@2.8.2: resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==} peerDependencies: @@ -1515,6 +1570,14 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + blamer@1.0.7: + resolution: {integrity: sha512-GbBStl/EVlSWkiJQBZps3H1iARBrC7vt++Jb/TTmCNu/jZ04VW7tSN1nScbFXBUy1AN+jzeL7Zep9sbQxLhXKA==} + engines: {node: '>=8.9'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} @@ -1525,6 +1588,10 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + c12@3.3.4: resolution: {integrity: sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==} peerDependencies: @@ -1533,6 +1600,14 @@ packages: magicast: optional: true + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -1544,6 +1619,9 @@ packages: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} + character-parser@2.2.0: + resolution: {integrity: sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==} + chart.js@4.5.1: resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==} engines: {pnpm: '>=8'} @@ -1560,6 +1638,10 @@ packages: peerDependencies: devtools-protocol: '*' + cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} + client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} @@ -1577,9 +1659,20 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + colors@1.4.0: + resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==} + engines: {node: '>=0.1.90'} + + commander@5.1.0: + resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} + engines: {node: '>= 6'} + confbox@0.2.4: resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + constantinople@4.0.1: + resolution: {integrity: sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -1680,6 +1773,9 @@ packages: resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==} engines: {node: '>=0.3.1'} + doctypes@1.1.0: + resolution: {integrity: sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==} + dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} @@ -1687,6 +1783,10 @@ packages: resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} engines: {node: '>=12'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + effect@3.20.0: resolution: {integrity: sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==} @@ -1715,9 +1815,21 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@2.1.0: resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + esbuild@0.27.7: resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} engines: {node: '>=18'} @@ -1748,9 +1860,16 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + events-universal@1.0.1: resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + execa@4.1.0: + resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} + engines: {node: '>=10'} + expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} @@ -1780,12 +1899,19 @@ packages: fast-fifo@1.3.2: resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} fast-uri@3.1.2: resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} @@ -1801,6 +1927,10 @@ packages: file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -1808,6 +1938,10 @@ packages: fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fs-extra@11.3.5: + resolution: {integrity: sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==} + engines: {node: '>=14.14'} + fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1818,6 +1952,9 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + generate-function@2.3.1: resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} @@ -1825,6 +1962,10 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + get-nonce@1.0.1: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} @@ -1832,6 +1973,10 @@ packages: get-port-please@3.2.0: resolution: {integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==} + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-stream@5.2.0: resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} engines: {node: '>=8'} @@ -1850,6 +1995,14 @@ packages: github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -1863,6 +2016,18 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + help-me@5.0.0: resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} @@ -1892,6 +2057,10 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + human-signals@1.1.1: + resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} + engines: {node: '>=8.12.0'} + iconv-lite@0.7.2: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} @@ -1919,16 +2088,46 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-expression@4.0.0: + resolution: {integrity: sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@2.2.2: + resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} + is-property@1.0.2: resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -1958,6 +2157,9 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + js-stringify@1.0.2: + resolution: {integrity: sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==} + js-tokens@10.0.0: resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} @@ -1968,6 +2170,13 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + jscpd-sarif-reporter@4.2.4: + resolution: {integrity: sha512-JtX79kFSyAhqJh5TdLUcvtYJtJd1F8UW8b4Miaga+EIgUn2/nR0N2zWL9mH5cRXgbzLuQbbsw9kReUVIECApwQ==} + + jscpd@4.2.4: + resolution: {integrity: sha512-PSo2U0G8OxULayGyQMv7T/0ZQ+c3PPltdMOz/57v9Xnmq5xSIhh4cnZ0oYZPKqejy10aFwAbMVxqAlo24+PQ3g==} + hasBin: true + jsdom@29.1.1: resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==} engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} @@ -1983,6 +2192,12 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + jsonfile@6.2.1: + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + + jstransformer@1.0.0: + resolution: {integrity: sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==} + jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} @@ -2095,9 +2310,31 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} + markdown-table@2.0.0: + resolution: {integrity: sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + mdn-data@2.27.1: resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} @@ -2159,6 +2396,18 @@ packages: resolution: {integrity: sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==} engines: {node: '>=10'} + node-sarif-builder@3.4.0: + resolution: {integrity: sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg==} + engines: {node: '>=20'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} @@ -2172,6 +2421,10 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + pac-proxy-agent@7.2.0: resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} engines: {node: '>= 14'} @@ -2198,6 +2451,9 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -2210,6 +2466,10 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + picomatch@4.0.4: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} @@ -2286,6 +2546,9 @@ packages: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} + promise@7.3.1: + resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} + proper-lockfile@4.1.2: resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} @@ -2296,6 +2559,42 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + pug-attrs@3.0.0: + resolution: {integrity: sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==} + + pug-code-gen@3.0.4: + resolution: {integrity: sha512-6okWYIKdasTyXICyEtvobmTZAVX57JkzgzIi4iRJlin8kmhG+Xry2dsus+Mun/nGCn6F2U49haHI5mkELXB14g==} + + pug-error@2.1.0: + resolution: {integrity: sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==} + + pug-filters@4.0.0: + resolution: {integrity: sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==} + + pug-lexer@5.0.1: + resolution: {integrity: sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==} + + pug-linker@4.0.0: + resolution: {integrity: sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==} + + pug-load@3.0.0: + resolution: {integrity: sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==} + + pug-parser@6.0.0: + resolution: {integrity: sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==} + + pug-runtime@3.0.1: + resolution: {integrity: sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==} + + pug-strip-comments@2.0.0: + resolution: {integrity: sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==} + + pug-walk@2.0.0: + resolution: {integrity: sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==} + + pug@3.0.4: + resolution: {integrity: sha512-kFfq5mMzrS7+wrl5pLJzZEzemx34OQ0w4SARfhy/3yxTlhbstsudDwJzhf1hP02yHzbjoVMSXUj/Sz6RNfMyXg==} + pump@3.0.4: resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} @@ -2315,6 +2614,9 @@ packages: pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} @@ -2390,6 +2692,10 @@ packages: remeda@2.33.4: resolution: {integrity: sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==} + repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -2405,15 +2711,27 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + retry@0.12.0: resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} engines: {node: '>= 4'} + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rolldown@1.0.0-rc.18: resolution: {integrity: sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} @@ -2502,6 +2820,9 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + spark-md5@3.0.2: + resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==} + split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} @@ -2536,6 +2857,10 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + strip-json-comments@2.0.1: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} @@ -2561,6 +2886,10 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -2612,6 +2941,13 @@ packages: resolution: {integrity: sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==} hasBin: true + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + token-stream@1.0.0: + resolution: {integrity: sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==} + tough-cookie@6.0.1: resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} engines: {node: '>=16'} @@ -2649,6 +2985,10 @@ packages: resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} engines: {node: '>=20.18.1'} + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + use-callback-ref@1.3.3: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} @@ -2767,6 +3107,10 @@ packages: jsdom: optional: true + void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + w3c-xmlserializer@5.0.0: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} @@ -2796,6 +3140,10 @@ packages: engines: {node: '>=8'} hasBin: true + with@7.0.2: + resolution: {integrity: sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==} + engines: {node: '>= 10.0.0'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -2937,6 +3285,9 @@ snapshots: dependencies: css-tree: 3.2.1 + '@colors/colors@1.5.0': + optional: true + '@csstools/color-helpers@6.0.2': {} '@csstools/css-calc@3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': @@ -3196,6 +3547,40 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@jscpd/badge-reporter@4.2.4': + dependencies: + badgen: 3.3.2 + colors: 1.4.0 + fs-extra: 11.3.5 + + '@jscpd/core@4.2.4': + dependencies: + eventemitter3: 5.0.4 + + '@jscpd/finder@4.2.4': + dependencies: + '@jscpd/core': 4.2.4 + '@jscpd/tokenizer': 4.2.4 + blamer: 1.0.7 + bytes: 3.1.2 + cli-table3: 0.6.5 + colors: 1.4.0 + fast-glob: 3.3.3 + fs-extra: 11.3.5 + markdown-table: 2.0.0 + pug: 3.0.4 + + '@jscpd/html-reporter@4.2.4': + dependencies: + colors: 1.4.0 + fs-extra: 11.3.5 + pug: 3.0.4 + + '@jscpd/tokenizer@4.2.4': + dependencies: + '@jscpd/core': 4.2.4 + spark-md5: 3.0.2 + '@kurkle/color@0.3.4': {} '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': @@ -3234,6 +3619,18 @@ snapshots: '@noble/hashes@1.8.0': optional: true + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + '@oxc-project/types@0.128.0': {} '@pinojs/redact@0.4.0': {} @@ -3796,6 +4193,8 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/sarif@2.1.7': {} + '@types/yauzl@2.10.3': dependencies: '@types/node': 25.6.2 @@ -3859,6 +4258,8 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 + acorn@7.4.1: {} + agent-base@7.1.4: {} ajv@8.20.0: @@ -3886,6 +4287,10 @@ snapshots: dependencies: dequal: 2.0.3 + asap@2.0.6: {} + + assert-never@1.4.0: {} + assertion-error@2.0.1: {} ast-types@0.13.4: @@ -3904,6 +4309,12 @@ snapshots: b4a@1.8.1: {} + babel-walk@3.0.0-canary-5: + dependencies: + '@babel/types': 7.29.0 + + badgen@3.3.2: {} + bare-events@2.8.2: {} bare-fs@4.7.1: @@ -3967,6 +4378,15 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + blamer@1.0.7: + dependencies: + execa: 4.1.0 + which: 2.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + buffer-crc32@0.2.13: {} buffer-crc32@1.0.0: {} @@ -3976,6 +4396,8 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + bytes@3.1.2: {} + c12@3.3.4(magicast@0.5.2): dependencies: chokidar: 5.0.0 @@ -3993,12 +4415,26 @@ snapshots: optionalDependencies: magicast: 0.5.2 + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + callsites@3.1.0: {} caniuse-lite@1.0.30001792: {} chai@6.2.2: {} + character-parser@2.2.0: + dependencies: + is-regex: 1.2.1 + chart.js@4.5.1: dependencies: '@kurkle/color': 0.3.4 @@ -4015,6 +4451,12 @@ snapshots: mitt: 3.0.1 zod: 3.25.76 + cli-table3@0.6.5: + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + client-only@0.0.1: {} cliui@8.0.1: @@ -4031,8 +4473,17 @@ snapshots: colorette@2.0.20: {} + colors@1.4.0: {} + + commander@5.1.0: {} + confbox@0.2.4: {} + constantinople@4.0.1: + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + convert-source-map@2.0.0: {} core-util-is@1.0.3: {} @@ -4112,10 +4563,18 @@ snapshots: diff@9.0.0: {} + doctypes@1.1.0: {} + dom-accessibility-api@0.5.16: {} dotenv@17.4.2: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + effect@3.20.0: dependencies: '@standard-schema/spec': 1.1.0 @@ -4139,8 +4598,16 @@ snapshots: dependencies: is-arrayish: 0.2.1 + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@2.1.0: {} + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + esbuild@0.27.7: optionalDependencies: '@esbuild/aix-ppc64': 0.27.7 @@ -4190,12 +4657,26 @@ snapshots: esutils@2.0.3: {} + eventemitter3@5.0.4: {} + events-universal@1.0.1: dependencies: bare-events: 2.8.2 transitivePeerDependencies: - bare-abort-controller + execa@4.1.0: + dependencies: + cross-spawn: 7.0.6 + get-stream: 5.2.0 + human-signals: 1.1.1 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + expand-template@2.0.3: {} expect-type@1.3.0: {} @@ -4222,10 +4703,22 @@ snapshots: fast-fifo@1.3.2: {} + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + fast-safe-stringify@2.1.1: {} fast-uri@3.1.2: {} + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + fd-slicer@1.1.0: dependencies: pend: 1.2.0 @@ -4236,6 +4729,10 @@ snapshots: file-uri-to-path@1.0.0: {} + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 @@ -4243,22 +4740,48 @@ snapshots: fs-constants@1.0.0: {} + fs-extra@11.3.5: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + fsevents@2.3.2: optional: true fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + generate-function@2.3.1: dependencies: is-property: 1.0.2 get-caller-file@2.0.5: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + get-nonce@1.0.1: {} get-port-please@3.2.0: {} + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + get-stream@5.2.0: dependencies: pump: 3.0.4 @@ -4279,6 +4802,12 @@ snapshots: github-from-package@0.0.0: {} + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + gopd@1.2.0: {} + graceful-fs@4.2.11: {} grammex@3.1.12: {} @@ -4287,6 +4816,16 @@ snapshots: has-flag@4.0.0: {} + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + help-me@5.0.0: {} hono@4.12.18: {} @@ -4320,6 +4859,8 @@ snapshots: transitivePeerDependencies: - supports-color + human-signals@1.1.1: {} + iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -4341,12 +4882,40 @@ snapshots: is-arrayish@0.2.1: {} + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + + is-expression@4.0.0: + dependencies: + acorn: 7.4.1 + object-assign: 4.1.1 + + is-extglob@2.1.1: {} + is-fullwidth-code-point@3.0.0: {} + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + is-potential-custom-element-name@1.0.1: {} + is-promise@2.2.2: {} + is-property@1.0.2: {} + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + is-stream@2.0.1: {} + isarray@1.0.0: {} isexe@2.0.0: {} @@ -4370,6 +4939,8 @@ snapshots: joycon@3.1.1: {} + js-stringify@1.0.2: {} + js-tokens@10.0.0: {} js-tokens@4.0.0: {} @@ -4378,6 +4949,24 @@ snapshots: dependencies: argparse: 2.0.1 + jscpd-sarif-reporter@4.2.4: + dependencies: + colors: 1.4.0 + fs-extra: 11.3.5 + node-sarif-builder: 3.4.0 + + jscpd@4.2.4: + dependencies: + '@jscpd/badge-reporter': 4.2.4 + '@jscpd/core': 4.2.4 + '@jscpd/finder': 4.2.4 + '@jscpd/html-reporter': 4.2.4 + '@jscpd/tokenizer': 4.2.4 + colors: 1.4.0 + commander: 5.1.0 + fs-extra: 11.3.5 + jscpd-sarif-reporter: 4.2.4 + jsdom@29.1.1(@noble/hashes@1.8.0): dependencies: '@asamuzakjp/css-color': 5.1.11 @@ -4408,6 +4997,17 @@ snapshots: json-schema-traverse@1.0.0: {} + jsonfile@6.2.1: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jstransformer@1.0.0: + dependencies: + is-promise: 2.2.2 + promise: 7.3.1 + jszip@3.10.1: dependencies: lie: 3.3.0 @@ -4494,8 +5094,25 @@ snapshots: dependencies: semver: 7.7.4 + markdown-table@2.0.0: + dependencies: + repeat-string: 1.6.1 + + math-intrinsics@1.1.0: {} + mdn-data@2.27.1: {} + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mimic-fn@2.1.0: {} + mimic-response@3.1.0: {} minimist@1.2.8: {} @@ -4557,6 +5174,17 @@ snapshots: dependencies: semver: 7.7.4 + node-sarif-builder@3.4.0: + dependencies: + '@types/sarif': 2.1.7 + fs-extra: 11.3.5 + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + object-assign@4.1.1: {} + obug@2.1.1: {} ohash@2.0.11: {} @@ -4567,6 +5195,10 @@ snapshots: dependencies: wrappy: 1.0.2 + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + pac-proxy-agent@7.2.0: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 @@ -4604,6 +5236,8 @@ snapshots: path-key@3.1.1: {} + path-parse@1.0.7: {} + pathe@2.0.3: {} pend@1.2.0: {} @@ -4612,6 +5246,8 @@ snapshots: picocolors@1.1.1: {} + picomatch@2.3.2: {} + picomatch@4.0.4: {} pino-abstract-transport@3.0.0: @@ -4723,6 +5359,10 @@ snapshots: progress@2.0.3: {} + promise@7.3.1: + dependencies: + asap: 2.0.6 + proper-lockfile@4.1.2: dependencies: graceful-fs: 4.2.11 @@ -4744,6 +5384,73 @@ snapshots: proxy-from-env@1.1.0: {} + pug-attrs@3.0.0: + dependencies: + constantinople: 4.0.1 + js-stringify: 1.0.2 + pug-runtime: 3.0.1 + + pug-code-gen@3.0.4: + dependencies: + constantinople: 4.0.1 + doctypes: 1.1.0 + js-stringify: 1.0.2 + pug-attrs: 3.0.0 + pug-error: 2.1.0 + pug-runtime: 3.0.1 + void-elements: 3.1.0 + with: 7.0.2 + + pug-error@2.1.0: {} + + pug-filters@4.0.0: + dependencies: + constantinople: 4.0.1 + jstransformer: 1.0.0 + pug-error: 2.1.0 + pug-walk: 2.0.0 + resolve: 1.22.12 + + pug-lexer@5.0.1: + dependencies: + character-parser: 2.2.0 + is-expression: 4.0.0 + pug-error: 2.1.0 + + pug-linker@4.0.0: + dependencies: + pug-error: 2.1.0 + pug-walk: 2.0.0 + + pug-load@3.0.0: + dependencies: + object-assign: 4.1.1 + pug-walk: 2.0.0 + + pug-parser@6.0.0: + dependencies: + pug-error: 2.1.0 + token-stream: 1.0.0 + + pug-runtime@3.0.1: {} + + pug-strip-comments@2.0.0: + dependencies: + pug-error: 2.1.0 + + pug-walk@2.0.0: {} + + pug@3.0.4: + dependencies: + pug-code-gen: 3.0.4 + pug-filters: 4.0.0 + pug-lexer: 5.0.1 + pug-linker: 4.0.0 + pug-load: 3.0.0 + pug-parser: 6.0.0 + pug-runtime: 3.0.1 + pug-strip-comments: 2.0.0 + pump@3.0.4: dependencies: end-of-stream: 1.4.5 @@ -4787,6 +5494,8 @@ snapshots: pure-rand@6.1.0: {} + queue-microtask@1.2.3: {} + quick-format-unescaped@4.0.4: {} rc9@3.0.1: @@ -4863,6 +5572,8 @@ snapshots: remeda@2.33.4: {} + repeat-string@1.6.1: {} + require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -4871,8 +5582,17 @@ snapshots: resolve-pkg-maps@1.0.0: {} + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + retry@0.12.0: {} + reusify@1.1.0: {} + rolldown@1.0.0-rc.18: dependencies: '@oxc-project/types': 0.128.0 @@ -4894,6 +5614,10 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.18 '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.18 + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + safe-buffer@5.1.2: {} safe-buffer@5.2.1: {} @@ -4994,6 +5718,8 @@ snapshots: source-map@0.6.1: optional: true + spark-md5@3.0.2: {} + split2@4.2.0: {} sqlstring@2.3.3: {} @@ -5031,6 +5757,8 @@ snapshots: dependencies: ansi-regex: 5.0.1 + strip-final-newline@2.0.0: {} + strip-json-comments@2.0.1: {} strip-json-comments@5.0.3: {} @@ -5044,6 +5772,8 @@ snapshots: dependencies: has-flag: 4.0.0 + supports-preserve-symlinks-flag@1.0.0: {} + symbol-tree@3.2.4: {} tar-fs@2.1.4: @@ -5122,6 +5852,12 @@ snapshots: dependencies: tldts-core: 7.0.30 + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + token-stream@1.0.0: {} + tough-cookie@6.0.1: dependencies: tldts: 7.0.30 @@ -5153,6 +5889,8 @@ snapshots: undici@7.25.0: {} + universalify@2.0.1: {} + use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.6): dependencies: react: 19.2.6 @@ -5221,6 +5959,8 @@ snapshots: transitivePeerDependencies: - msw + void-elements@3.1.0: {} + w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 @@ -5248,6 +5988,13 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + with@7.0.2: + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + assert-never: 1.4.0 + babel-walk: 3.0.0-canary-5 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 From 0b4737ed74323fbda6f8e640f5b1209902205f89 Mon Sep 17 00:00:00 2001 From: AlexandreCamillo <43162926+AlexandreCamillo@users.noreply.github.com> Date: Fri, 29 May 2026 20:30:30 +0000 Subject: [PATCH 20/21] fix(quality-gate): drop runner.temp expression from action description; restore git clone for engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub validates ${{ }} expressions everywhere in an action manifest, including the top-level description, where the runner context is not available — this failed the workflow at load time. State the path in plain prose instead. Also revert the engine fetch to the proven `git clone --depth 1` form rather than `git init` + fetch-by-SHA, which is not equivalent on a cold runner. --- .github/actions/quality-gate-prepare/action.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/actions/quality-gate-prepare/action.yml b/.github/actions/quality-gate-prepare/action.yml index 452fe275..35803c3a 100644 --- a/.github/actions/quality-gate-prepare/action.yml +++ b/.github/actions/quality-gate-prepare/action.yml @@ -3,7 +3,7 @@ description: >- Set up the toolchain, run the test suite with coverage, build the pinned quality-gate engine, and run the adapter. Shared by the PR and baseline workflows so the prelude and engine build stay in one place. The adapter - writes its six metric files to ${{ runner.temp }}/qg. + writes its six metric files to the runner temp dir (runner.temp/qg). inputs: engine-sha: @@ -46,9 +46,8 @@ runs: if: steps.cache-qg.outputs.cache-hit != 'true' shell: bash run: | - git init /tmp/qg-core + git clone --depth 1 https://github.com/alkg-cloud/quality-gate /tmp/qg-core cd /tmp/qg-core - git remote add origin https://github.com/alkg-cloud/quality-gate git fetch --depth 1 origin ${{ inputs.engine-sha }} git checkout ${{ inputs.engine-sha }} pnpm install --frozen-lockfile From cf7c672cf34802e9cbfc6a084ce28ed17d89c373 Mon Sep 17 00:00:00 2001 From: AlexandreCamillo <43162926+AlexandreCamillo@users.noreply.github.com> Date: Fri, 29 May 2026 20:36:53 +0000 Subject: [PATCH 21/21] fix(quality-gate): skip sticky PR comment when the gate produced no comment file If the suite or engine build fails, the adapter never writes pr-comment.md, and the always() comment step would itself fail and bury the real error. Gate it on an explicit existence check instead. --- .github/workflows/quality-gate-pr.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/quality-gate-pr.yml b/.github/workflows/quality-gate-pr.yml index 44121de8..e583d0c8 100644 --- a/.github/workflows/quality-gate-pr.yml +++ b/.github/workflows/quality-gate-pr.yml @@ -27,8 +27,21 @@ jobs: --config ./quality-gate.config.json \ --output-dir ${{ runner.temp }}/qg - - name: Post sticky PR comment + # The comment file only exists once the engine has run; on an earlier + # failure (e.g. the suite or engine build) it is absent, so skip the + # comment step rather than letting it fail and bury the real error. + - name: Check for gate comment + id: gate-comment if: always() + run: | + if [ -f "${{ runner.temp }}/qg/pr-comment.md" ]; then + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + + - name: Post sticky PR comment + if: always() && steps.gate-comment.outputs.exists == 'true' uses: marocchino/sticky-pull-request-comment@v2 with: header: quality-gate-marker-v1