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 @@
-
+
+
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