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 @@ Built with Next.js 16 Release CI - Coverage + coverage + quality

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'); - }); -});