diff --git a/.github/actions/quality-gate-prepare/action.yml b/.github/actions/quality-gate-prepare/action.yml
new file mode 100644
index 00000000..35803c3a
--- /dev/null
+++ b/.github/actions/quality-gate-prepare/action.yml
@@ -0,0 +1,71 @@
+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 the runner temp dir (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 clone --depth 1 https://github.com/alkg-cloud/quality-gate /tmp/qg-core
+ cd /tmp/qg-core
+ 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
new file mode 100644
index 00000000..7c52ed65
--- /dev/null
+++ b/.github/workflows/quality-gate-main.yml
@@ -0,0 +1,29 @@
+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: ./.github/actions/quality-gate-prepare
+
+ - 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 }}
diff --git a/.github/workflows/quality-gate-pr.yml b/.github/workflows/quality-gate-pr.yml
new file mode 100644
index 00000000..e583d0c8
--- /dev/null
+++ b/.github/workflows/quality-gate-pr.yml
@@ -0,0 +1,55 @@
+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: ./.github/actions/quality-gate-prepare
+
+ - name: Run quality gate
+ run: |
+ node /tmp/qg-core/dist/cli.js pr \
+ --config ./quality-gate.config.json \
+ --output-dir ${{ runner.temp }}/qg
+
+ # 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
+ 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
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
diff --git a/.gitignore b/.gitignore
index 08fcafdb..011c851b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,3 +41,6 @@ public/_qa-mobile.html
# landing-export build artifacts
landing-export/.next/
landing-export/out/
+
+# quality-gate adapter local output dir
+/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..35a355a0
--- /dev/null
+++ b/.quality-gate/adapter.sh
@@ -0,0 +1,137 @@
+#!/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")
+
+# 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
+ 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
+
+# --- 2. Lint (biome --reporter=json; biome exits 1 with violations, hence || true) ---
+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: "..."}.
+ # 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)
+ }
+ ' "$WORK/biome-report.json" > "$QG_OUTPUT_DIR/lint.json"
+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).
+# 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.
+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 "$WORK/jscpd/jscpd-report.json" ]; then
+ jq '{
+ pct: (.statistics.total.percentage // 0),
+ clones: (.statistics.total.clones // 0)
+ }' "$WORK/jscpd/jscpd-report.json" > "$QG_OUTPUT_DIR/duplication.json"
+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=$(awk 'END { print NR }' "$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"
+
+# --- 5. Security (pnpm audit) ---
+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)
+ }' "$WORK/pnpm-audit.json" > "$QG_OUTPUT_DIR/security.json"
+else
+ echo '{"_skipped":"pnpm audit produced no metadata.vulnerabilities"}' > "$QG_OUTPUT_DIR/security.json"
+fi
+
+# --- 6. _meta (must list the tools the adapter actually invoked) ---
+# 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"
diff --git a/README.md b/README.md
index 74fb1e27..2e703f24 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,8 @@
-
+
+
diff --git a/docs/INDEX.md b/docs/INDEX.md
index 7153ec1f..1a32e827 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
@@ -42,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 066be44e..9f093171 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
@@ -81,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/quality-gate.md b/docs/quality-gate.md
new file mode 100644
index 00000000..a4bf5fdf
--- /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 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
+
+| 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` | `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. |
+
+## 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 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.
+
+## 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/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 14bb66f2..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
@@ -140,15 +132,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.
diff --git a/package.json b/package.json
index dedb141b..4b5b9ddd 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"
},
@@ -68,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",
@@ -80,8 +80,7 @@
"better-sqlite3",
"esbuild",
"prisma",
- "puppeteer",
- "sharp"
+ "puppeteer"
]
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 07b0ddf9..d71a07d7 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
@@ -123,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)
@@ -271,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'}
@@ -679,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==}
@@ -747,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==}
@@ -1341,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==}
@@ -1385,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'}
@@ -1414,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'}
@@ -1441,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:
@@ -1518,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==}
@@ -1528,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:
@@ -1536,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'}
@@ -1547,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'}
@@ -1563,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==}
@@ -1580,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==}
@@ -1683,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==}
@@ -1690,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==}
@@ -1718,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'}
@@ -1751,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'}
@@ -1783,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==}
@@ -1804,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'}
@@ -1811,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}
@@ -1821,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==}
@@ -1828,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'}
@@ -1835,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'}
@@ -1853,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==}
@@ -1866,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==}
@@ -1895,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'}
@@ -1922,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==}
@@ -1961,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==}
@@ -1971,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}
@@ -1986,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==}
@@ -2098,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'}
@@ -2162,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==}
@@ -2175,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'}
@@ -2201,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==}
@@ -2213,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'}
@@ -2289,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==}
@@ -2299,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==}
@@ -2318,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==}
@@ -2393,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'}
@@ -2408,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==}
@@ -2505,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'}
@@ -2539,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'}
@@ -2564,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==}
@@ -2615,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'}
@@ -2652,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'}
@@ -2770,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'}
@@ -2799,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'}
@@ -2940,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)':
@@ -3093,7 +3441,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:
@@ -3198,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)':
@@ -3236,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': {}
@@ -3798,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
@@ -3861,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:
@@ -3888,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:
@@ -3906,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:
@@ -3969,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: {}
@@ -3978,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
@@ -3995,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
@@ -4017,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:
@@ -4033,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: {}
@@ -4114,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
@@ -4141,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
@@ -4192,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: {}
@@ -4224,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
@@ -4238,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
@@ -4245,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
@@ -4281,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: {}
@@ -4289,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: {}
@@ -4322,6 +4859,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ human-signals@1.1.1: {}
+
iconv-lite@0.7.2:
dependencies:
safer-buffer: 2.1.2
@@ -4343,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: {}
@@ -4372,6 +4939,8 @@ snapshots:
joycon@3.1.1: {}
+ js-stringify@1.0.2: {}
+
js-tokens@10.0.0: {}
js-tokens@4.0.0: {}
@@ -4380,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
@@ -4410,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
@@ -4496,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: {}
@@ -4559,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: {}
@@ -4569,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
@@ -4606,6 +5236,8 @@ snapshots:
path-key@3.1.1: {}
+ path-parse@1.0.7: {}
+
pathe@2.0.3: {}
pend@1.2.0: {}
@@ -4614,6 +5246,8 @@ snapshots:
picocolors@1.1.1: {}
+ picomatch@2.3.2: {}
+
picomatch@4.0.4: {}
pino-abstract-transport@3.0.0:
@@ -4725,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
@@ -4746,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
@@ -4789,6 +5494,8 @@ snapshots:
pure-rand@6.1.0: {}
+ queue-microtask@1.2.3: {}
+
quick-format-unescaped@4.0.4: {}
rc9@3.0.1:
@@ -4865,6 +5572,8 @@ snapshots:
remeda@2.33.4: {}
+ repeat-string@1.6.1: {}
+
require-directory@2.1.1: {}
require-from-string@2.0.2: {}
@@ -4873,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
@@ -4896,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: {}
@@ -4950,6 +5672,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:
@@ -4995,6 +5718,8 @@ snapshots:
source-map@0.6.1:
optional: true
+ spark-md5@3.0.2: {}
+
split2@4.2.0: {}
sqlstring@2.3.3: {}
@@ -5032,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: {}
@@ -5045,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:
@@ -5123,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
@@ -5154,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
@@ -5222,6 +5959,8 @@ snapshots:
transitivePeerDependencies:
- msw
+ void-elements@3.1.0: {}
+
w3c-xmlserializer@5.0.0:
dependencies:
xml-name-validator: 5.0.0
@@ -5249,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
diff --git a/quality-gate.config.json b/quality-gate.config.json
new file mode 100644
index 00000000..6abb96dc
--- /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"
+ }
+}
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/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);
- });
-});
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');
- });
-});