mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-17 04:32:31 +03:00
Merge branch 'release/v3.8.49' into fix/port-issue-1809-mitm-dns-order
This commit is contained in:
@@ -505,7 +505,9 @@ NEXT_PUBLIC_CLOUD_URL=
|
||||
#OMNIROUTE_CROF_USAGE_URL=https://crof.ai/usage_api/
|
||||
#OMNIROUTE_CODEWHISPERER_BASE_URL=https://codewhisperer.us-east-1.amazonaws.com
|
||||
#OMNIROUTE_OPENCODE_QUOTA_URL=https://opencode.ai/zen/go/v1/quota
|
||||
#OMNIROUTE_OPENCODE_GO_QUOTA_URL=https://api.z.ai/api/monitor/usage/quota/limit
|
||||
# OpenCode Go has no public quota API — this has no default and stays
|
||||
# unset unless you explicitly opt in to a self-hosted/mirrored endpoint:
|
||||
#OMNIROUTE_OPENCODE_GO_QUOTA_URL=
|
||||
#OMNIROUTE_OPENCODE_GO_DASHBOARD_URL=https://opencode.ai/workspace
|
||||
#OMNIROUTE_OLLAMA_CLOUD_USAGE_URL=https://ollama.com/settings
|
||||
|
||||
|
||||
9
.env.homolog.example
Normal file
9
.env.homolog.example
Normal file
@@ -0,0 +1,9 @@
|
||||
# Homologação E2E real — copie para .env.homolog (NUNCA commitar o real)
|
||||
HOMOLOG_BASE_URL=http://192.168.0.15:20128
|
||||
# Senha de management do dashboard da VPS (a mesma do /login)
|
||||
HOMOLOG_ADMIN_PASSWORD=
|
||||
# Deixe vazio: a suíte cria uma API key efêmera via admin e revoga no fim.
|
||||
# Só preencha para depurar uma camada isolada com uma key fixa.
|
||||
HOMOLOG_API_KEY=
|
||||
# Tier crítico (chat real, max_tokens=5). Demais providers: só validação de catálogo.
|
||||
HOMOLOG_CRITICAL_PROVIDERS=openai,anthropic,gemini,codex,grok,glm,deepseek,openrouter
|
||||
9
.github/dependabot.yml
vendored
9
.github/dependabot.yml
vendored
@@ -24,6 +24,15 @@ updates:
|
||||
update-types: ["version-update:semver-major"]
|
||||
- dependency-name: "eslint-config-next"
|
||||
update-types: ["version-update:semver-major"]
|
||||
# typescript majors are peer-blocked by typescript-eslint, which pins a hard
|
||||
# upper bound (8.64.0 → peerDependencies.typescript ">=4.8.4 <6.1.0"). A TS 7
|
||||
# bump therefore violates the peer and takes down the whole toolchain at once —
|
||||
# #7068 grouped it with 6 harmless bumps and turned Build + Lint + Quality Ratchet
|
||||
# + Unit (6/8, 8/8) + Integration (1/2, 2/2) + dast-smoke red in one shot, blocking
|
||||
# the innocuous updates riding along with it. Un-ignore once typescript-eslint
|
||||
# widens the peer, and migrate TS majors intentionally (own PR, own CI run).
|
||||
- dependency-name: "typescript"
|
||||
update-types: ["version-update:semver-major"]
|
||||
# jscpd v5 is a Rust rewrite (native binary, no Node.js programmatic API).
|
||||
# scripts/check/check-duplication.mjs is deliberately pinned to jscpd@4 (it
|
||||
# parses jscpd-report.json against a frozen baseline). A v5 major would break
|
||||
|
||||
125
.github/workflows/ci.yml
vendored
125
.github/workflows/ci.yml
vendored
@@ -33,6 +33,7 @@ jobs:
|
||||
docs: ${{ steps.classify.outputs.docs }}
|
||||
i18n: ${{ steps.classify.outputs.i18n }}
|
||||
workflow: ${{ steps.classify.outputs.workflow }}
|
||||
testsOnly: ${{ steps.classify.outputs.testsOnly }}
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
with:
|
||||
@@ -124,11 +125,23 @@ jobs:
|
||||
- run: npm run check:route-guard-membership
|
||||
- run: npm run check:test-discovery
|
||||
- run: npm run check:tracked-artifacts
|
||||
# WS1.7 (v3.8.49 plan): Dockerfile lint (hadolint, pinned by digest).
|
||||
# failure-threshold=error keeps the 5 pre-existing warnings (DL3008/DL3003/
|
||||
# DL3016 version pinning / WORKDIR) visible without blocking; any ERROR fails.
|
||||
- name: hadolint (Dockerfile)
|
||||
run: docker run --rm -i hadolint/hadolint@sha256:27086352fd5e1907ea2b934eb1023f217c5ae087992eb59fde121dce9c9ff21e hadolint --failure-threshold error - < Dockerfile
|
||||
- run: npm run check:lockfile
|
||||
- run: npm run check:licenses
|
||||
# check:docs-sync is run by the docs-sync-strict job (via check:docs-all) and the
|
||||
# husky pre-commit hook; the standalone copy here was redundant (ROI dedup).
|
||||
- run: npm run typecheck:core
|
||||
# #7033: typecheck:core's curated file allowlist does not cover
|
||||
# src/app/(dashboard) TSX (and next.config.mjs sets ignoreBuildErrors:
|
||||
# true, so `next build` never type-checks it either) — orphaned
|
||||
# identifiers there (see #6625/#6909) were invisible to CI. This gate
|
||||
# runs tsc scoped to the dashboard tree against a frozen baseline of
|
||||
# pre-existing errors; only NEW errors fail it.
|
||||
- run: npm run check:dashboard-typecheck
|
||||
# typecheck:noimplicit:core dropped from this job (2026-07 optimize):
|
||||
# it was advisory (continue-on-error) and largely subsumed by the blocking
|
||||
# check:type-coverage ratchet in quality-gate. Local: npm run typecheck:noimplicit:core.
|
||||
@@ -148,7 +161,7 @@ jobs:
|
||||
# The coverage.* metrics degrade gracefully: the download is continue-on-error and
|
||||
# the ratchet runs with --allow-missing, so absent coverage is skipped, not failed.
|
||||
# Path filter: code-only — pure docs/i18n PRs have nothing for these ratchets to guard.
|
||||
if: ${{ !cancelled() && (github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.code == 'true' && (needs.lint.result == 'success' || needs.lint.result == 'failure'))) }}
|
||||
if: ${{ !cancelled() && !contains(github.event.pull_request.labels.*.name, 'hotfix') && (github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.code == 'true' && (needs.lint.result == 'success' || needs.lint.result == 'failure'))) }}
|
||||
# security-events: read lets the CodeQL ratchet read open code-scanning alerts
|
||||
# via `gh api .../code-scanning/alerts`. contents: read keeps checkout working.
|
||||
permissions:
|
||||
@@ -258,7 +271,7 @@ jobs:
|
||||
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
|
||||
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
|
||||
# Path filter: code-only (scanners/ratchets target production surface).
|
||||
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.code == 'true') }}
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'hotfix') && (github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.code == 'true')) }}
|
||||
steps:
|
||||
# fetch-depth: 0 — the OpenAPI breaking-change gate (oasdiff) reads the base
|
||||
# spec via `git show <base_ref>:docs/openapi.yaml`; a shallow clone
|
||||
@@ -609,12 +622,26 @@ jobs:
|
||||
- name: Assert dist/server.js exists
|
||||
run: test -f dist/server.js || (echo "dist/server.js missing — build:cli did not assemble correctly" && exit 1)
|
||||
- run: npm run check:pack-artifact
|
||||
# WS1.2 (#7065 class): pack the real tarball, install it into a clean prefix and
|
||||
# BOOT it to a healthy /api/monitoring/health — the gate that structure checks
|
||||
# cannot provide (3 releases shipped boot-crashing tarballs with green lists).
|
||||
- name: Boot-smoke the packed tarball
|
||||
run: npm run check:pack-boot
|
||||
|
||||
electron-package-smoke:
|
||||
name: Electron Package Smoke
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
name: Electron Package Smoke (${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 30
|
||||
needs: build
|
||||
# WS1.5 (v3.8.49 plan): the Electron rebuild/spawn path previously executed for
|
||||
# the FIRST time on the release tag — the v3.8.48 Windows bug (npx.cmd spawned
|
||||
# without shell, CVE-2024-27980 behavior change) could only surface at release.
|
||||
# windows-latest runs prepare:bundle (the ABI rebuild + spawn plan) per release
|
||||
# PR; ubuntu keeps the full pack + headless smoke.
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
env:
|
||||
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
||||
@@ -640,9 +667,21 @@ jobs:
|
||||
working-directory: electron
|
||||
run: npm install --no-audit --no-fund
|
||||
- name: Pack Electron app
|
||||
if: runner.os == 'Linux'
|
||||
working-directory: electron
|
||||
run: npm run pack
|
||||
# ADVISORY while the new Windows leg matures (repo convention, dast-smoke
|
||||
# precedent): its first-ever real run (2026-07-15, run 29457533565) died in
|
||||
# 0.7s with the error swallowed by pwsh — bash shell captures stderr and
|
||||
# continue-on-error keeps the heavy gate green while we harden it (#7336).
|
||||
- name: Prepare Electron standalone (Windows ABI rebuild + spawn path)
|
||||
if: runner.os == 'Windows'
|
||||
working-directory: electron
|
||||
continue-on-error: true
|
||||
shell: bash
|
||||
run: npm run prepare:bundle 2>&1
|
||||
- name: Smoke packaged Electron app
|
||||
if: runner.os == 'Linux'
|
||||
env:
|
||||
ELECTRON_SMOKE_TIMEOUT_MS: 60000
|
||||
run: xvfb-run -a npm run electron:smoke:packaged
|
||||
@@ -725,12 +764,23 @@ jobs:
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
# The second test runner (CLAUDE.md: "Both test runners must pass") — was never
|
||||
# wired into CI until the 2026-06-09 quality audit (Fase 6A.2).
|
||||
- run: npm run test:vitest
|
||||
# vitest:ui is RED today (14 fails — UI component drift accumulated while the
|
||||
# suite never ran in CI). Informational until the Fase 6A triage (2026-06-16+)
|
||||
# fixes the components/tests; then drop continue-on-error to make it blocking.
|
||||
- run: npm run test:vitest:ui
|
||||
# WS5.2/5.3 (v3.8.49 plan): JUnit output feeds Trunk Flaky Tests (advisory upload
|
||||
# below). node:test stays OUT of the first wave (fd1-sensitive reporter stream).
|
||||
- run: npm run test:vitest -- --reporter=default --reporter=junit --outputFile.junit=trunk-junit/vitest-mcp.xml
|
||||
# vitest:ui went back to 870/870 green in the v3.8.49 quality plan (WS6.1,
|
||||
# PR #7127 — 69 fails triaged: matchMedia polyfill, node:test→vitest migration,
|
||||
# CompareTab D22 cap). Promoted to BLOCKING per the plan's post-merge step.
|
||||
- run: npm run test:vitest:ui -- --reporter=default --reporter=junit --outputFile.junit=trunk-junit/vitest-ui.xml
|
||||
# Trunk Flaky Tests upload — advisory (never blocks), own-origin only (fork PRs
|
||||
# have no TRUNK_TOKEN). Pinned by SHA (tag v2.1.2).
|
||||
- name: Upload test results to Trunk (advisory)
|
||||
if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
|
||||
continue-on-error: true
|
||||
uses: trunk-io/analytics-uploader@385f1ccdf345b4532dc4b6c665dd432b702b8e28 # v2.1.2
|
||||
with:
|
||||
junit-paths: trunk-junit/**/*.xml
|
||||
org-slug: omniroute
|
||||
token: ${{ secrets.TRUNK_TOKEN }}
|
||||
|
||||
# Node 24/26 compatibility matrices moved to .github/workflows/nightly-compat.yml
|
||||
# (plano mestre testes+CI, Eixo D2 — they cost ~28% of every heavy run to catch a
|
||||
@@ -739,9 +789,12 @@ jobs:
|
||||
test-coverage:
|
||||
name: Coverage
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
# 10min was sized before #7114 added the lcov reporter (Codecov/Sonar need it);
|
||||
# merging 8 shard JSONs + text+json+lcov now takes ~10-12min — three consecutive
|
||||
# release-tip runs died at exactly 10m as job-timeout "cancelled" (2026-07-15/16).
|
||||
timeout-minutes: 20
|
||||
needs: test-unit
|
||||
if: ${{ !cancelled() && needs.test-unit.result == 'success' }}
|
||||
if: ${{ !cancelled() && needs.test-unit.result == 'success' && !contains(github.event.pull_request.labels.*.name, 'hotfix') }}
|
||||
env:
|
||||
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
|
||||
API_KEY_SECRET: ci-test-api-key-secret-long
|
||||
@@ -789,6 +842,7 @@ jobs:
|
||||
--merge-async \
|
||||
--reporter=text-summary \
|
||||
--reporter=json-summary \
|
||||
--reporter=lcov \
|
||||
--exclude=tests/** \
|
||||
--exclude=**/*.test.* \
|
||||
--check-coverage \
|
||||
@@ -810,6 +864,18 @@ jobs:
|
||||
> coverage/coverage-report.md
|
||||
fi
|
||||
cat coverage/coverage-report.md >> "$GITHUB_STEP_SUMMARY"
|
||||
# WS5.6 (D7, v3.8.49 plan): patch coverage on the PR diff via Codecov —
|
||||
# informational during calibration (codecov.yml sets informational: true);
|
||||
# promote to blocking only after ~2 weeks without false blocks. The lcov
|
||||
# reporter above also fixes coverage/lcov.info being silently absent
|
||||
# (if-no-files-found: warn) — Sonar consumes the same file.
|
||||
- name: Upload coverage to Codecov (informational)
|
||||
if: always()
|
||||
uses: codecov/codecov-action@04b047e8bb82a0c002c8312c1c880fbc6a999d45 # v5
|
||||
with:
|
||||
files: coverage/lcov.info
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
- name: Upload coverage artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
@@ -960,7 +1026,12 @@ jobs:
|
||||
# ~33%. Playwright browser is cached across runs (~1.5min saved per shard).
|
||||
# Heavy shard target: ≤20min (was ~40min). Timeout 45min to cover slow runners.
|
||||
timeout-minutes: 45
|
||||
needs: build
|
||||
needs: [build, changes]
|
||||
# WS3.1 hotfix fast-lane: the 9-shard E2E matrix is the CI critical path (~25min).
|
||||
# It skips for (a) PRs labeled `hotfix` (entry policy in docs/ops/RELEASE_CHECKLIST.md:
|
||||
# production-broken only, full-suite evidence from the previous green run linked in the
|
||||
# PR) and (b) tests-only diffs outside tests/e2e/ (cannot change the served app).
|
||||
if: ${{ needs.changes.outputs.testsOnly != 'true' && !contains(github.event.pull_request.labels.*.name, 'hotfix') }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -995,7 +1066,33 @@ jobs:
|
||||
- name: Extract Next.js build artifact
|
||||
run: |
|
||||
tar -xzf /tmp/e2e-build.tar.gz
|
||||
- run: npx playwright test tests/e2e/*.spec.ts --shard=${{ matrix.shard }}/9
|
||||
# WS4.1: duration-balanced shards (LPT over config/quality/e2e-timings.json).
|
||||
# Measured skew of plain --shard was 14× (24m47s vs 1m47s) — E2E was the CI
|
||||
# critical path. The balancer self-verifies completeness and exits non-zero on
|
||||
# any inconsistency, falling back to plain --shard (never fewer specs).
|
||||
- name: Run E2E tests (duration-balanced shard)
|
||||
env:
|
||||
SHARD: ${{ matrix.shard }}
|
||||
PLAYWRIGHT_JUNIT_OUTPUT_NAME: junit-e2e-results.xml
|
||||
run: |
|
||||
if FILES=$(node scripts/quality/balance-e2e-shards.mjs "$SHARD" 9); then
|
||||
if [ -z "$FILES" ]; then echo "[e2e-balance] shard $SHARD has no files"; exit 0; fi
|
||||
echo "[e2e-balance] shard $SHARD runs:"; echo "$FILES"
|
||||
# shellcheck disable=SC2086 — FILES is our own newline-separated path list
|
||||
npx playwright test $(echo "$FILES" | tr '\n' ' ') --reporter=line,junit
|
||||
else
|
||||
echo "[e2e-balance] balancer unavailable — plain --shard fallback"
|
||||
npx playwright test tests/e2e/*.spec.ts --shard="$SHARD"/9 --reporter=line,junit
|
||||
fi
|
||||
# WS5.2/5.3: Trunk Flaky Tests upload — advisory, own-origin only, SHA-pinned.
|
||||
- name: Upload test results to Trunk (advisory)
|
||||
if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
|
||||
continue-on-error: true
|
||||
uses: trunk-io/analytics-uploader@385f1ccdf345b4532dc4b6c665dd432b702b8e28 # v2.1.2
|
||||
with:
|
||||
junit-paths: junit-e2e-results.xml
|
||||
org-slug: omniroute
|
||||
token: ${{ secrets.TRUNK_TOKEN }}
|
||||
|
||||
test-integration:
|
||||
name: Integration Tests (${{ matrix.shard }}/2)
|
||||
|
||||
5
.github/workflows/dast-smoke.yml
vendored
5
.github/workflows/dast-smoke.yml
vendored
@@ -10,7 +10,10 @@ jobs:
|
||||
# ADVISORY while this new gate matures (repo convention: advisory -> blocking).
|
||||
# Flip to blocking (remove continue-on-error) once it's proven stable across a few PRs.
|
||||
continue-on-error: true
|
||||
timeout-minutes: 12
|
||||
# Build CLI bundle alone varies 6-11min on GitHub-hosted runners (3 consecutive
|
||||
# timeouts observed on 2026-07-14 with the old 12min cap killing schemathesis
|
||||
# mid-run) — 25min leaves real headroom for the actual DAST steps.
|
||||
timeout-minutes: 25
|
||||
env:
|
||||
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
|
||||
API_KEY_SECRET: ci-api-key-secret-with-sufficient-length-aaaa
|
||||
|
||||
178
.github/workflows/nightly-release-green.yml
vendored
178
.github/workflows/nightly-release-green.yml
vendored
@@ -1,11 +1,18 @@
|
||||
name: Nightly Release-Green
|
||||
name: Release-Green (continuous)
|
||||
|
||||
# Solution D — continuous, NON-BLOCKING drift signal for the active release branch.
|
||||
#
|
||||
# WHY: the full gate (ci.yml) only runs on the release PR (PR → main), so reds
|
||||
# accrue silently on release/** and explode — in layers — at release time. This
|
||||
# nightly reproduces the release-equivalent validation on the active release branch
|
||||
# HEAD and, when there are HARD failures, opens/updates a single tracking issue.
|
||||
# workflow reproduces the release-equivalent validation on the release branch and,
|
||||
# when there are HARD failures, opens/updates a single tracking issue.
|
||||
#
|
||||
# WS5.1 (v3.8.49 quality plan) — two modes:
|
||||
# push to release/v* (code paths) → --quick (fast HARD gates, ~5-8min). Catches the
|
||||
# captain's direct pushes (sync-back — the one ungated write path) AND the merged
|
||||
# COMBINATION right after every PR merge, attributing the offending push range in
|
||||
# the issue. Base-red MTTD drops from ≤24h to ≤~15min after the offending push.
|
||||
# schedule (3×/day) → full --with-build --full-ci (the deep sweep incl. build+suites).
|
||||
#
|
||||
# It is NOT a required status check and never touches a contributor PR — it only
|
||||
# reports. Ratchet drift (eslint warnings / cognitive-complexity / file-size) is
|
||||
@@ -14,8 +21,23 @@ name: Nightly Release-Green
|
||||
# package-artifact) flip the issue open.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["release/v*", "main"]
|
||||
paths:
|
||||
- "src/**"
|
||||
- "open-sse/**"
|
||||
- "bin/**"
|
||||
- "electron/**"
|
||||
- "scripts/**"
|
||||
- "tests/**"
|
||||
- "config/**"
|
||||
- "package.json"
|
||||
- "package-lock.json"
|
||||
- "tsconfig*.json"
|
||||
schedule:
|
||||
- cron: "23 5 * * *" # 05:23 UTC daily — off-peak, distinct from other nightlies
|
||||
- cron: "23 5 * * *" # full sweep — off-peak, distinct from other nightlies
|
||||
- cron: "23 12 * * *" # full sweep — midday (WS5.1: 3×/day instead of 1×)
|
||||
- cron: "23 18 * * *" # full sweep — evening
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
@@ -28,7 +50,9 @@ permissions:
|
||||
issues: write
|
||||
|
||||
concurrency:
|
||||
group: nightly-release-green
|
||||
# push storms during merge campaigns collapse to the newest commit per branch;
|
||||
# scheduled full sweeps keep their own single lane.
|
||||
group: release-green-${{ github.event_name }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
@@ -37,6 +61,9 @@ env:
|
||||
jobs:
|
||||
release-green:
|
||||
name: Validate active release branch
|
||||
# On a push, only run for release/* pushes — a push to main is handled by the
|
||||
# main-green job below. Schedule/dispatch always run (they validate the highest release).
|
||||
if: ${{ github.event_name != 'push' || startsWith(github.ref_name, 'release/') }}
|
||||
# Dynamic runner: with USE_VPS_RUNNER=true (release window / on-demand pre-flight)
|
||||
# this runs on the dedicated VPS runner — clean env (no operator OMNIROUTE_API_KEY,
|
||||
# no local noauth CLIs => zero machine-specific false positives) and no contention.
|
||||
@@ -56,10 +83,15 @@ jobs:
|
||||
id: branch
|
||||
env:
|
||||
INPUT_BRANCH: ${{ github.event.inputs.branch }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
PUSHED_REF: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -n "${INPUT_BRANCH:-}" ]; then
|
||||
TARGET="$INPUT_BRANCH"
|
||||
elif [ "$EVENT_NAME" = "push" ]; then
|
||||
# validate exactly what was pushed, not the highest branch
|
||||
TARGET="$PUSHED_REF"
|
||||
else
|
||||
# highest release/vX.Y.Z by semver among remote branches
|
||||
TARGET=$(git for-each-ref --format='%(refname:short)' 'refs/remotes/origin/release/v*' \
|
||||
@@ -93,16 +125,26 @@ jobs:
|
||||
|
||||
- name: Release-green validation (full)
|
||||
id: validate
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
run: |
|
||||
set +e
|
||||
# --hermetic: scrub live-test trigger vars (self-hosted runner may carry
|
||||
# operator env; hosted ignores the unknown flag before #6300 lands).
|
||||
# --full-ci: ALSO run every static gate from ci.yml's gate jobs (lint,
|
||||
# quality-gate, quality-extended, docs-sync-strict, pr-test-policy). PRs into
|
||||
# release/** only get the fast-gates, so these accrue silently and explode in
|
||||
# layers on the release PR (v3.8.46: 11 static base-reds leaked). Running them
|
||||
# nightly opens the tracking issue the moment one lands, not at release time.
|
||||
node scripts/quality/validate-release-green.mjs --json --with-build --hermetic --full-ci \
|
||||
# push → --quick: fast HARD gates only (~5-8min), per-merge signal.
|
||||
# schedule/dispatch → --with-build --full-ci: ALSO run every static gate from
|
||||
# ci.yml's gate jobs (lint, quality-gate, quality-extended, docs-sync-strict,
|
||||
# pr-test-policy) + build + full suites. PRs into release/** only get the
|
||||
# fast-gates, so these accrue silently and explode in layers on the release PR
|
||||
# (v3.8.46: 11 static base-reds leaked).
|
||||
if [ "$EVENT_NAME" = "push" ]; then
|
||||
MODE="--quick"
|
||||
else
|
||||
MODE="--with-build --full-ci"
|
||||
fi
|
||||
echo "[release-green] mode: $MODE (event: $EVENT_NAME)"
|
||||
# shellcheck disable=SC2086 — MODE is an intentional flag list
|
||||
node scripts/quality/validate-release-green.mjs --json --hermetic $MODE \
|
||||
1> release-green.json 2> release-green.log
|
||||
echo "exit=$?" >> "$GITHUB_OUTPUT"
|
||||
echo "------- report -------"
|
||||
@@ -114,15 +156,28 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TARGET: ${{ steps.branch.outputs.target }}
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
BEFORE_SHA: ${{ github.event.before }}
|
||||
AFTER_SHA: ${{ github.event.after }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TITLE="🔴 Release branch not green: ${TARGET}"
|
||||
{
|
||||
echo "The nightly **release-green** validation found HARD failures on \`${TARGET}\`."
|
||||
echo "The **release-green** validation found HARD failures on \`${TARGET}\`."
|
||||
echo "These are real defects that would block the release PR — fix them in the"
|
||||
echo "originating PR branch (via co-authorship), not by demanding it from contributors."
|
||||
echo ""
|
||||
echo "**Run:** ${RUN_URL}"
|
||||
echo "**Run:** ${RUN_URL} (mode: ${EVENT_NAME})"
|
||||
# WS5.1 attribution: on push events the offending change IS this push's range
|
||||
# (one merge per push in the normal queue), so name it — no bisect needed.
|
||||
if [ "$EVENT_NAME" = "push" ] && [ -n "${BEFORE_SHA:-}" ] && \
|
||||
git cat-file -e "$BEFORE_SHA" 2>/dev/null; then
|
||||
echo ""
|
||||
echo "**Offending push range** (\`${BEFORE_SHA:0:9}..${AFTER_SHA:0:9}\`):"
|
||||
echo '```'
|
||||
git log --no-decorate --oneline "${BEFORE_SHA}..${AFTER_SHA}" | head -20
|
||||
echo '```'
|
||||
fi
|
||||
echo ""
|
||||
echo '```'
|
||||
sed -n '/──────── verdict ────────/,$p' release-green.log || tail -40 release-green.log
|
||||
@@ -149,3 +204,100 @@ jobs:
|
||||
release-green.json
|
||||
release-green.log
|
||||
if-no-files-found: ignore
|
||||
|
||||
# Companion arm for `main`. Under the parallel-cycle model, main only receives merged
|
||||
# work at the release squash — so a gate/infra fix that lands only on release leaves
|
||||
# main red the whole cycle, and repo-wide gates (CodeQL alert count, ratchet baselines)
|
||||
# turn EVERY PR into main red on a check unrelated to its diff. This detects that and
|
||||
# opens a "🔴 main not green" tracking issue. The PREVENTION is the companion-PR reflex
|
||||
# (Hard Rule #21 area / _shared/merge-gates.md §8); this is the automated backstop.
|
||||
main-green:
|
||||
name: Validate main branch
|
||||
# On a push, only run for a push to main — a push to release/* is handled by
|
||||
# release-green above. Schedule/dispatch always run (they also sweep main).
|
||||
if: ${{ github.event_name != 'push' || github.ref_name == 'main' }}
|
||||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && fromJSON('["self-hosted","omni-release"]')) || 'ubuntu-latest' }}
|
||||
env:
|
||||
JWT_SECRET: ci-nightly-secret-with-sufficient-length-for-validation
|
||||
API_KEY_SECRET: ci-nightly-api-key-secret-long
|
||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: main # literal — no injection surface; scheduled runs default to the repo default branch (a release/v*), so pin main explicitly
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: npm
|
||||
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
|
||||
- name: Main-green validation
|
||||
id: validate
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
run: |
|
||||
set +e
|
||||
# push (a merge into main) → --quick fast HARD gates; schedule/dispatch → full sweep.
|
||||
if [ "$EVENT_NAME" = "push" ]; then
|
||||
MODE="--quick"
|
||||
else
|
||||
MODE="--with-build --full-ci"
|
||||
fi
|
||||
echo "[main-green] mode: $MODE (event: $EVENT_NAME)"
|
||||
# shellcheck disable=SC2086 — MODE is an intentional flag list
|
||||
node scripts/quality/validate-release-green.mjs --json --hermetic $MODE \
|
||||
1> main-green.json 2> main-green.log
|
||||
echo "exit=$?" >> "$GITHUB_OUTPUT"
|
||||
echo "------- report -------"
|
||||
cat main-green.log
|
||||
|
||||
- name: Open / update tracking issue on HARD failure
|
||||
if: steps.validate.outputs.exit != '0'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TITLE="🔴 main branch not green"
|
||||
{
|
||||
echo "The **main-green** validation found HARD failures on \`main\`."
|
||||
echo ""
|
||||
echo "Because \`main\` only receives merged work at the release squash, a gate/infra"
|
||||
echo "fix that landed only on the release branch leaves \`main\` broken for the whole"
|
||||
echo "cycle — and repo-wide gates (CodeQL alert count, ratchet baselines) then turn"
|
||||
echo "**every open PR into main** red on a check unrelated to its diff. The fix is a"
|
||||
echo "companion PR \`--base main\` carrying the release-side fix (see"
|
||||
echo "\`_shared/merge-gates.md\` §8), NOT chasing each contributor PR."
|
||||
echo ""
|
||||
echo "**Run:** ${RUN_URL} (mode: ${EVENT_NAME})"
|
||||
echo ""
|
||||
echo '```'
|
||||
sed -n '/──────── verdict ────────/,$p' main-green.log || tail -40 main-green.log
|
||||
echo '```'
|
||||
echo ""
|
||||
echo "_Ratchet drift (eslint warnings / cognitive-complexity / file-size) is expected mid-cycle and did NOT, on its own, open this issue._"
|
||||
} > issue-body.md
|
||||
|
||||
EXISTING=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open \
|
||||
--search "in:title $TITLE" --json number --jq '.[0].number' 2>/dev/null || echo "")
|
||||
if [ -n "$EXISTING" ]; then
|
||||
gh issue comment "$EXISTING" --repo "$GITHUB_REPOSITORY" --body-file issue-body.md
|
||||
echo "Updated existing issue #$EXISTING"
|
||||
else
|
||||
gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --body-file issue-body.md
|
||||
fi
|
||||
|
||||
- name: Upload report artifact
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: main-green-report
|
||||
path: |
|
||||
main-green.json
|
||||
main-green.log
|
||||
if-no-files-found: ignore
|
||||
|
||||
64
.github/workflows/npm-publish.yml
vendored
64
.github/workflows/npm-publish.yml
vendored
@@ -22,6 +22,14 @@ on:
|
||||
- latest
|
||||
- next
|
||||
- historic
|
||||
publish_mode:
|
||||
description: "staged = npm stage publish (owner approves with 2FA after the staged boot-verify); direct = legacy immediate publish (emergency fallback only)"
|
||||
required: false
|
||||
default: "staged"
|
||||
type: choice
|
||||
options:
|
||||
- staged
|
||||
- direct
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
@@ -166,8 +174,34 @@ jobs:
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: gh release upload "$TAG" sbom-npm.cdx.json --clobber
|
||||
|
||||
- name: Publish to npm
|
||||
# WS1.2/WS1.3 (#7065 class): the artifact that is about to be published must
|
||||
# BOOT. build:cli already assembled dist/ above; this packs+installs+boots the
|
||||
# real tarball and fails the publish before anything reaches the registry.
|
||||
- name: Boot-smoke the tarball before ANY publish
|
||||
if: steps.resolve.outputs.skip != 'true'
|
||||
run: npm run check:pack-boot
|
||||
|
||||
# WS1.3 (D2, v3.8.49 plan): STAGED publishing by default — `npm stage publish`
|
||||
# parks the exact bytes on the registry WITHOUT making them installable; the
|
||||
# owner then verifies and approves with 2FA (`npm stage approve`), moving the
|
||||
# human gate to AFTER the proof instead of before it. Requires npm >= 11.15
|
||||
# (staged publishing GA 2026-05-22). publish_mode=direct is the emergency
|
||||
# fallback (legacy immediate publish) via workflow_dispatch.
|
||||
- name: Ensure npm supports staged publishing
|
||||
if: steps.resolve.outputs.skip != 'true' && (github.event_name != 'workflow_dispatch' || inputs.publish_mode != 'direct')
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CUR=$(npm --version)
|
||||
if ! node -e "const [a,b]='$(npm --version)'.split('.').map(Number); process.exit(a>11||(a===11&&b>=15)?0:1)"; then
|
||||
# Pinned exact version (supply-chain: never float @latest in the publish
|
||||
# job); bump deliberately when a newer npm is required.
|
||||
echo "npm $CUR < 11.15 — installing pinned npm 11.15.0 for staged publishing"
|
||||
npm install -g --ignore-scripts npm@11.15.0
|
||||
fi
|
||||
npm --version
|
||||
|
||||
- name: Publish to npm (staged — owner approves with 2FA)
|
||||
if: steps.resolve.outputs.skip != 'true' && (github.event_name != 'workflow_dispatch' || inputs.publish_mode != 'direct')
|
||||
env:
|
||||
VERSION: ${{ steps.resolve.outputs.version }}
|
||||
TAG: ${{ steps.resolve.outputs.tag }}
|
||||
@@ -175,10 +209,32 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Always pass --tag explicitly. Defense in depth: even if VERSION is
|
||||
# accidentally an older release, `npm publish --tag historic` will
|
||||
# NOT promote it to `@latest`.
|
||||
# accidentally an older release, the historic tag will NOT claim `@latest`.
|
||||
npm stage publish --provenance --access public --tag "$TAG"
|
||||
{
|
||||
echo "## 📦 omniroute@$VERSION STAGED (not yet installable)"
|
||||
echo ""
|
||||
echo "The exact bytes are parked on the registry. To release them:"
|
||||
echo '```'
|
||||
echo "npm stage list omniroute # find the stage id"
|
||||
echo "npm stage approve <id> # owner 2FA — THE publish"
|
||||
echo '```'
|
||||
echo "To verify the staged bytes first: npm stage download <id> → run"
|
||||
echo "scripts/check/check-pack-boot.mjs against them (see RELEASE_CHECKLIST)."
|
||||
echo "To discard: npm stage reject <id>."
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "✅ Staged omniroute@$VERSION (dist-tag=$TAG) — awaiting owner 'npm stage approve'"
|
||||
|
||||
- name: Publish to npm (DIRECT — emergency fallback)
|
||||
if: steps.resolve.outputs.skip != 'true' && github.event_name == 'workflow_dispatch' && inputs.publish_mode == 'direct'
|
||||
env:
|
||||
VERSION: ${{ steps.resolve.outputs.version }}
|
||||
TAG: ${{ steps.resolve.outputs.tag }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npm publish --provenance --access public --tag "$TAG"
|
||||
echo "✅ Published omniroute@$VERSION (dist-tag=$TAG)"
|
||||
echo "✅ Published omniroute@$VERSION (dist-tag=$TAG) [DIRECT mode]"
|
||||
|
||||
- name: Publish to GitHub Packages
|
||||
if: steps.resolve.outputs.skip != 'true'
|
||||
|
||||
43
.github/workflows/quality.yml
vendored
43
.github/workflows/quality.yml
vendored
@@ -62,7 +62,7 @@ jobs:
|
||||
docs-gates:
|
||||
name: Docs Gates (fast-path)
|
||||
needs: changes
|
||||
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && (needs.changes.outputs.docs == 'true' || needs.changes.outputs.code == 'true')) }}
|
||||
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && (needs.changes.outputs.docs == 'true' || needs.changes.outputs.code == 'true')) }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
name: Fast Quality Gates
|
||||
needs: changes
|
||||
# Code surface only — pure docs/i18n PRs skip this bag (docs-gates covers docs).
|
||||
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.code == 'true') }}
|
||||
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
|
||||
# Dynamic runner (same rule as ci.yml): use the self-hosted VPS pool only when the
|
||||
# release captain has USE_VPS_RUNNER=true AND this is not a fork PR (own-origin
|
||||
# branches only — a fork PR must never execute on the LAN runner). Var unset/false
|
||||
@@ -143,6 +143,24 @@ jobs:
|
||||
- run: npm run check:complexity-ratchets
|
||||
- name: Typecheck (core)
|
||||
run: npm run typecheck:core
|
||||
# #7033: dashboard-scoped typecheck gate — src/app/(dashboard) TSX is not
|
||||
# covered by typecheck:core's curated allowlist. See check-dashboard-typecheck.mjs.
|
||||
- name: Typecheck (dashboard)
|
||||
run: npm run check:dashboard-typecheck
|
||||
# WS4.2 (v3.8.49 plan): TypeScript 7 native-compiler SHADOW — advisory only.
|
||||
# TS7 went GA 2026-07-08 with 8-12x type-check speedups; its Compiler API only
|
||||
# arrives in 7.1, so typescript-eslint / type-coverage / Stryker stay on 6.x
|
||||
# (the hybrid is the officially documented pattern). Isolated npx on purpose:
|
||||
# installing an alias package could collide node_modules/.bin/tsc with 6.x.
|
||||
# Promote to the blocking gate after ~1 week of parity with the step above.
|
||||
- name: Typecheck (core) — TS7 native shadow (advisory)
|
||||
continue-on-error: true
|
||||
run: |
|
||||
RC=0
|
||||
START=$(date +%s)
|
||||
npx -y -p typescript@7 tsc --pretty false -p tsconfig.typecheck-core.json || RC=$?
|
||||
echo "[ts7-shadow] exit=$RC elapsed=$(( $(date +%s) - START ))s — the 6.x step above stays authoritative"
|
||||
exit $RC
|
||||
# TIA: build the impact map at runtime (gitignored, ~21MB) and run only the
|
||||
# unit tests impacted by this PR's changed files. On hub/unmapped changes the
|
||||
# selector returns __RUN_ALL__ — full-suite authority is the parallel
|
||||
@@ -196,7 +214,7 @@ jobs:
|
||||
fast-vitest:
|
||||
name: Vitest (fast-path)
|
||||
needs: changes
|
||||
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.code == 'true') }}
|
||||
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
|
||||
# Dynamic runner — see fast-gates (own-origin + flag; fork/unset → ubuntu-latest).
|
||||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
|
||||
env:
|
||||
@@ -212,12 +230,23 @@ jobs:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run test:vitest
|
||||
# WS5.2/5.3: JUnit feeds Trunk Flaky Tests — the fast-path runs on EVERY PR,
|
||||
# which is where flaky-detection volume actually comes from (ci.yml's heavy
|
||||
# jobs only run on the release PR). Advisory upload, own-origin only.
|
||||
- run: npm run test:vitest -- --reporter=default --reporter=junit --outputFile.junit=trunk-junit/vitest-fastpath.xml
|
||||
- name: Upload test results to Trunk (advisory)
|
||||
if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
|
||||
continue-on-error: true
|
||||
uses: trunk-io/analytics-uploader@385f1ccdf345b4532dc4b6c665dd432b702b8e28 # v2.1.2
|
||||
with:
|
||||
junit-paths: trunk-junit/**/*.xml
|
||||
org-slug: omniroute
|
||||
token: ${{ secrets.TRUNK_TOKEN }}
|
||||
|
||||
fast-unit:
|
||||
name: Unit Tests fast-path (${{ matrix.shard }}/4)
|
||||
needs: changes
|
||||
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.code == 'true') }}
|
||||
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
|
||||
# Dynamic runner — see fast-gates (own-origin + flag; fork/unset → ubuntu-latest).
|
||||
# This is the heaviest fast-path job; 4-way sharding (was 2, #6781) halves the
|
||||
# critical path again (~8.5min → ~4.5min on ubuntu-latest; ~2min on the 8-slot
|
||||
@@ -263,7 +292,7 @@ jobs:
|
||||
lint-guard:
|
||||
name: No new ESLint warnings
|
||||
needs: changes
|
||||
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.code == 'true') }}
|
||||
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
|
||||
steps:
|
||||
@@ -302,7 +331,7 @@ jobs:
|
||||
merge-integrity:
|
||||
name: Merge integrity (changelog + generated skills)
|
||||
# Always on non-draft PRs — CHANGELOG/skills can break on docs-only merges too.
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
|
||||
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) }}
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
|
||||
env:
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -72,6 +72,7 @@ yarn-error.log*
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
!.env.example
|
||||
!.env.homolog.example
|
||||
# Provider API keys (never commit)
|
||||
*.api-key
|
||||
.nvidia-api-key
|
||||
@@ -242,3 +243,9 @@ _artifacts/
|
||||
|
||||
# CI/local quality artifacts (eslint-results.json, etc.)
|
||||
.artifacts/
|
||||
|
||||
# Homologation E2E suite (npm run homolog) — real-environment credentials + report output
|
||||
.env.homolog
|
||||
tests/homolog/.auth/
|
||||
tests/homolog/ui/.auth/
|
||||
homolog-report/
|
||||
|
||||
@@ -74,3 +74,16 @@
|
||||
# '''tests/unit/''',
|
||||
# ]
|
||||
#
|
||||
|
||||
[[rules]]
|
||||
# Falsos-positivos comprovados do generic-api-key — zerados em 2026-07-13 (WS6/D3,
|
||||
# plano v3.8.49). Revisar em v3.9.0. Nenhum é credencial: dois são NOMES DE CAMPO
|
||||
# de métricas de latência; o terceiro é o valor PÚBLICO de um beta header da API
|
||||
# da Anthropic (documentado publicamente, não é segredo).
|
||||
id = "generic-api-key"
|
||||
[rules.allowlist]
|
||||
description = "Field names + public Anthropic beta-header value (não são segredos)"
|
||||
regexes = [
|
||||
'''latencyP\d{2}Ms''',
|
||||
'''interleaved-thinking-2025-05-14''',
|
||||
]
|
||||
|
||||
55
.mergify.yml
Normal file
55
.mergify.yml
Normal file
@@ -0,0 +1,55 @@
|
||||
# Mergify merge queue — WS3.4/D5 of the v3.8.49 quality/velocity master plan.
|
||||
#
|
||||
# WHY: ~85-100 active PR authors/month and 300+ PRs/week peaks, all merged by ONE
|
||||
# identity. The manual merge-train validated batches by hand; this queue automates
|
||||
# it with batching + automatic batch bisection (a red batch of N costs ~log2(N)
|
||||
# revalidations instead of N). Mergify Open Source plan: free, unlimited, public repo.
|
||||
#
|
||||
# GOVERNANCE (non-negotiable, mirrors CLAUDE.md Hard Rules #21/#22 + the owner's
|
||||
# pre-merge ⭐ gate):
|
||||
# • A PR enters the queue ONLY via the `queue` label — applied by the owner (or a
|
||||
# session acting for the owner) AFTER the pre-merge ⭐ report/decision. The label
|
||||
# IS the merge approval; Mergify only executes it.
|
||||
# • During a release-freeze (open issue labeled `release-freeze`), do NOT label PRs
|
||||
# targeting the frozen branch — the freeze is a human-honored coordination signal
|
||||
# the queue cannot see. Retarget to the active release/vX+1 first (Hard Rule #21).
|
||||
# • Never label a PR another session is actively working (Hard Rule #22b).
|
||||
# • Fallback path if Mergify misbehaves or the OSS plan changes: the manual
|
||||
# merge-train runbook (docs/ops/MERGE_TRAIN.md) — remove labels, proceed by hand.
|
||||
|
||||
queue_rules:
|
||||
- name: release
|
||||
# Any current or future release branch — the reason GitHub's native queue was
|
||||
# rejected (no wildcard support on personal-account repos).
|
||||
queue_conditions:
|
||||
- base~=^release/v\d+\.\d+\.\d+$
|
||||
- label=queue
|
||||
- -draft
|
||||
- -conflict
|
||||
# "Everything that ran is green, nothing still running, AND the always-on
|
||||
# anchor check succeeded" — robust to the path-filtered fast-gates (docs-only
|
||||
# PRs skip code jobs; matrix shard names vary) while never fail-open: a PR with
|
||||
# zero checks cannot vacuously merge, because `Merge integrity` runs on EVERY
|
||||
# non-draft PR (quality.yml) and must be an affirmative success. Review approval
|
||||
# is intentionally NOT a condition here: the owner-applied `queue` label IS the
|
||||
# approval in this repo's single-maintainer model (see governance header).
|
||||
merge_conditions:
|
||||
- "#check-failure=0"
|
||||
- "#check-pending=0"
|
||||
- "#check-success>=1"
|
||||
- check-success=Merge integrity (changelog + generated skills)
|
||||
# Batching: validate up to 10 queued PRs together (the manual train's sweet spot);
|
||||
# don't hold a lone PR hostage waiting for siblings.
|
||||
batch_size: 10
|
||||
batch_max_wait_time: 5 min
|
||||
# Squash keeps the one-commit-per-PR history the CHANGELOG reconciliation expects.
|
||||
merge_method: squash
|
||||
|
||||
pull_request_rules:
|
||||
- name: clean up the queue label after merge
|
||||
conditions:
|
||||
- merged
|
||||
actions:
|
||||
label:
|
||||
remove:
|
||||
- queue
|
||||
1
changelog.d/features/homolog-e2e-suite.md
Normal file
1
changelog.d/features/homolog-e2e-suite.md
Normal file
@@ -0,0 +1 @@
|
||||
- **Homologation suite**: new `npm run homolog` runs the full release-homologation battery against the deployed VPS — health/version parity, API + real SSE streaming with an ephemeral API key (created and revoked by the run), minimal-cost real-provider smoke (promptfoo generated from the live catalog), and a Playwright sweep that loads every dashboard route and exercises the API-key UI flow — emitting a unified CTRF report that backs the release STOP #2 checklist
|
||||
1
changelog.d/fixes/5716-proxy-pool-relayauth-dropped.md
Normal file
1
changelog.d/fixes/5716-proxy-pool-relayauth-dropped.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(providers): preserve relayAuth for vercel/deno/cloudflare relay proxies referenced by-id from the no-auth-provider Proxy Pool dropdown (#5716)
|
||||
@@ -0,0 +1 @@
|
||||
- fix(sse): sanitize non-Latin1 characters before embedding combo diagnostics in HTTP headers, preventing a ByteString crash on quality-check failure (#6612)
|
||||
1
changelog.d/fixes/6699-jules-chat-executor-misroute.md
Normal file
1
changelog.d/fixes/6699-jules-chat-executor-misroute.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(providers): reject chat-completions requests for cloud-agent-only providers like jules instead of silently mis-routing them to OpenAI's endpoint (#6699)
|
||||
1
changelog.d/fixes/6835-db-oom-probe-cyclebreaker.md
Normal file
1
changelog.d/fixes/6835-db-oom-probe-cyclebreaker.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(db):** cap the sql.js OOM-during-probe path in `getDbInstance()` at 3 attempts with a terminal diagnostic — previously only the generic-corruption probe-failure path had a cycle-breaker (#6632), so a persistently OOMing `storage.sqlite` probe re-threw the identical error forever on every call from every background poller, hanging the app with "Internal Server Error" and no self-recovery (#6835).
|
||||
@@ -0,0 +1 @@
|
||||
- fix(providers): DuckDuckGo AI Chat executor propagates the real upstream status (429 rate limit with `Retry-After`) instead of misclassifying VQD-token acquisition failures as a hardcoded 503 (#6996)
|
||||
1
changelog.d/fixes/6998-opencode-oc-free-tier-catalog.md
Normal file
1
changelog.d/fixes/6998-opencode-oc-free-tier-catalog.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(providers): refresh OpenCode (`oc`) free-tier model catalog — 6 delisted IDs replaced with the 4 currently-live free models (#6998)
|
||||
1
changelog.d/fixes/7003-jetbrains-ai-loopback-connect.md
Normal file
1
changelog.d/fixes/7003-jetbrains-ai-loopback-connect.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(api): raise the main server's `keepAliveTimeout`/`headersTimeout` well above Node's 5s default so pooled keep-alive clients (e.g. JetBrains AI Assistant's JVM `HttpClient`) stop getting 0 bytes back on a reused connection (#7003)
|
||||
1
changelog.d/fixes/7005-adaptive-context-budget-dial.md
Normal file
1
changelog.d/fixes/7005-adaptive-context-budget-dial.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(compression): wire the adaptive context-budget "dial" (`contextBudget`) into the settings schema and DB so it can actually be persisted via `PUT /api/settings/compression`, instead of being silently rejected (#7005)
|
||||
1
changelog.d/fixes/7022-opencode-go-quota-url-zai.md
Normal file
1
changelog.d/fixes/7022-opencode-go-quota-url-zai.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(usage): stop opencode-go quota lookup from defaulting to an unrelated Z.AI endpoint (#7022)
|
||||
1
changelog.d/fixes/7033-dashboard-typecheck-gate.md
Normal file
1
changelog.d/fixes/7033-dashboard-typecheck-gate.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(ci): add a dashboard-scoped typecheck gate covering `src/app/(dashboard)` TSX, previously invisible to `typecheck:core` and `next build` (#7033)
|
||||
@@ -0,0 +1 @@
|
||||
- fix(build): extend the Turbopack `ignoreIssue` suppression to `open-sse/services/compression/**`, matching the `getModuleDir()` dynamic-path fs pattern already suppressed for `src/lib/agentSkills/**` in #6582, eliminating the remaining 610 "Overly broad patterns" warnings (#7051)
|
||||
1
changelog.d/fixes/7058-zai-web-proxy-connection-test.md
Normal file
1
changelog.d/fixes/7058-zai-web-proxy-connection-test.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(providers): web-cookie connection-test/cookie-validation probe (zai-web and every other registry-entry web-cookie provider) now honors the configured HTTP/SOCKS proxy — the `/models` probe routed through `directHttpsRequest`'s hardcoded native-fetch bypass, silently skipping proxy resolution even though the executor's actual chat traffic already respected it (#7058)
|
||||
1
changelog.d/fixes/7071-ollama-cloud-session-quota.md
Normal file
1
changelog.d/fixes/7071-ollama-cloud-session-quota.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(resilience): recognize Ollama Cloud's 5-hour session usage-limit 429 as quota-exhausted instead of a generic rate limit (#7071)
|
||||
1
changelog.d/fixes/7072-quota-card-grid-mobile.md
Normal file
1
changelog.d/fixes/7072-quota-card-grid-mobile.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(dashboard): restore mobile single-column fallback on the Provider Quota page card grid, fixing clipped labels/buttons on phone-width viewports (#7072)
|
||||
@@ -0,0 +1 @@
|
||||
- fix(dashboard): include proxyId when testing a saved registry proxy so SOCKS5/auth credentials are loaded (#7080)
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(sse):** claude-web now surfaces the real upstream error body for non-SSE 400/403/429/500 responses instead of reporting "no response body" — the streaming client was discarding the already-captured temp-file bytes and reading the native binding's empty in-memory body field instead (#7134).
|
||||
1
changelog.d/fixes/7149-combo-scope-proxy-dead.md
Normal file
1
changelog.d/fixes/7149-combo-scope-proxy-dead.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(db): honor combo-level proxy assignments from the registry when resolving a connection's proxy (#7149)
|
||||
1
changelog.d/fixes/7151-hermes-agent-model-aliases.md
Normal file
1
changelog.d/fixes/7151-hermes-agent-model-aliases.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(dashboard): wire modelAliases fetch into HermesAgentToolCard so OpenRouter and other passthrough providers appear in the Hermes Agent role picker (#7151)
|
||||
@@ -0,0 +1 @@
|
||||
- fix(dashboard): filter hidden custom models out of the legacy combo model picker (#7156)
|
||||
1
changelog.d/fixes/7157-agent-bridge-dns-405.md
Normal file
1
changelog.d/fixes/7157-agent-bridge-dns-405.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(dashboard): Agent Bridge DNS toggle now sends POST (was PUT), fixing HTTP 405 on Start/Stop DNS (#7157)
|
||||
1
changelog.d/fixes/7161-free-pool-handletogglesource.md
Normal file
1
changelog.d/fixes/7161-free-pool-handletogglesource.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(dashboard): implement missing `handleToggleSource` callback on the Free Pool tab so the page no longer crashes with a `ReferenceError` (#7161)
|
||||
1
changelog.d/fixes/7163-gemini-web-duplicated-text.md
Normal file
1
changelog.d/fixes/7163-gemini-web-duplicated-text.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(sse): stop duplicating text in Gemini Web streamed responses (#7163)
|
||||
1
changelog.d/fixes/pack-boot-runtimetimeouts-sibling.md
Normal file
1
changelog.d/fixes/pack-boot-runtimetimeouts-sibling.md
Normal file
@@ -0,0 +1 @@
|
||||
- **Build**: the packed tarball boots again — #7191's `../../src/…runtimeTimeouts.ts` import in `standalone-server-ws.mjs` escaped the package after the dist-root copy (`ERR_MODULE_NOT_FOUND` on every boot, #7065 class, caught live by the new `check:pack-boot` gate); the helper now lives in the shipped sibling `main-server-timeouts.mjs` (parity-tested against the canonical TS implementation) and the closure test bans package-escaping `../` imports in npm-shipped wrappers
|
||||
1
changelog.d/fixes/pack-boot-smoke-gate.md
Normal file
1
changelog.d/fixes/pack-boot-smoke-gate.md
Normal file
@@ -0,0 +1 @@
|
||||
- **Packaging**: new `check:pack-boot` gate packs the real npm tarball, installs it into a clean prefix (postinstall runs for real) and boots the installed CLI until `/api/monitoring/health` returns 200 with the packed version — the runtime gate that structure checks could not provide (3 releases shipped boot-crashing tarballs with green packaging lists: tls-options/3.8.41, #7040, #7065). Wired into the CI `package-artifact` job and `check:release-green --with-build`
|
||||
1
changelog.d/fixes/pack-entrypoint-closures.md
Normal file
1
changelog.d/fixes/pack-entrypoint-closures.md
Normal file
@@ -0,0 +1 @@
|
||||
- **Packaging**: pack-artifact closure tests now cover EVERY npm-shipped dist wrapper (derived from `EXTRA_MODULE_ENTRIES`) and the `bin/omniroute.mjs` CLI entry — including dynamic `import()` and `require()` forms the original server-ws test missed — requiring each local import in both the prepublish prune allowlist and `check:pack-artifact`; `bin/cli/data-dir.mjs` and `bin/cli/utils/storageKeyProvision.mjs` are now required tarball paths (#7065 class hardening, WS1.1)
|
||||
1
changelog.d/fixes/register-cli-skill-collector.md
Normal file
1
changelog.d/fixes/register-cli-skill-collector.md
Normal file
@@ -0,0 +1 @@
|
||||
- **Skills**: register `cli-skill-collector` in the agent-skills catalog (types union, curated entry, CLI id list) — #6294 shipped the `skills/cli-skill-collector/` directory without the catalog registration, so it was unreachable via the API and Integration CI failed on the catalog-integrity test; counts aligned (44 API+CLI, 45 with config)
|
||||
1
changelog.d/fixes/yuanbao-cookie-validation.md
Normal file
1
changelog.d/fixes/yuanbao-cookie-validation.md
Normal file
@@ -0,0 +1 @@
|
||||
- **Providers**: yuanbao-web no longer forwards a foreign single cookie pair upstream — `buildYuanbaoCookie` only trusts `hy_user`/`hy_token` extractions the input explicitly names, so a missing session token now fails fast with the local 401 guidance instead of a live Tencent round-trip
|
||||
@@ -0,0 +1 @@
|
||||
- **chore(ci):** stop dependabot from proposing `typescript` majors — `typescript-eslint` pins a hard peer upper bound (`>=4.8.4 <6.1.0`), so a TS 7 bump violates the peer and takes the whole toolchain red at once. #7068 grouped it with 6 harmless dev bumps and blocked all of them. TS majors now migrate intentionally, in their own PR.
|
||||
@@ -0,0 +1 @@
|
||||
- **test(dashboard):** restore dedicated regression coverage for #6815's `QuotaCardGrid` multi-column density guarantee, decoupled from the specific Tailwind token so it survives the #7027 auto-fit migration ([#7291](https://github.com/diegosouzapw/OmniRoute/pull/7291))
|
||||
1
changelog.d/maintenance/7307-rehome-open-prs-script.md
Normal file
1
changelog.d/maintenance/7307-rehome-open-prs-script.md
Normal file
@@ -0,0 +1 @@
|
||||
- **chore(release):** add `scripts/release/rehome-open-prs.mjs` — the Phase 0a.0b PR re-home, scripted with a read-back after every retarget. `gh pr edit --base` exits 0 without applying (v3.8.42), `gh pr list` silently caps at 30, and the v3.8.49 freeze had 148 open PRs to move — none of which a hand-run loop survives reliably.
|
||||
1
changelog.d/maintenance/codecov-patch-coverage.md
Normal file
1
changelog.d/maintenance/codecov-patch-coverage.md
Normal file
@@ -0,0 +1 @@
|
||||
- **CI**: Codecov patch-coverage on every PR diff (informational during calibration — `codecov.yml` sets nothing blocking; strict-patch/lenient-project philosophy on top of the existing 60% c8 floor + ratchet); the CI coverage job now actually emits `coverage/lcov.info` (the `lcov` reporter was missing, so the artifact silently skipped it — the same file Sonar consumes)
|
||||
1
changelog.d/maintenance/coverage-job-timeout.md
Normal file
1
changelog.d/maintenance/coverage-job-timeout.md
Normal file
@@ -0,0 +1 @@
|
||||
- **CI**: raise the Coverage job timeout 10→20min — the lcov reporter added for Codecov/Sonar (#7114) pushed the 8-shard report merge past the old cap, and three release-tip runs died at exactly 10min as job-timeout "cancelled"
|
||||
1
changelog.d/maintenance/dast-smoke-timeout.md
Normal file
1
changelog.d/maintenance/dast-smoke-timeout.md
Normal file
@@ -0,0 +1 @@
|
||||
- **CI**: raise the dast-smoke job timeout 12→25min — the CLI bundle build alone varies 6-11min on GitHub-hosted runners, so the old cap killed Schemathesis mid-run (3 consecutive false-negative timeouts on 2026-07-14)
|
||||
1
changelog.d/maintenance/e2e-shard-balance.md
Normal file
1
changelog.d/maintenance/e2e-shard-balance.md
Normal file
@@ -0,0 +1 @@
|
||||
- **CI**: E2E matrix shards are now duration-balanced (LPT bin-packing over `config/quality/e2e-timings.json`) instead of Playwright's count-based `--shard` — measured skew was 14× (24m47s vs 1m47s), making E2E the CI critical path; the balancer self-verifies that every spec lands in exactly one shard and falls back to plain `--shard` on any inconsistency
|
||||
1
changelog.d/maintenance/electron-win-advisory.md
Normal file
1
changelog.d/maintenance/electron-win-advisory.md
Normal file
@@ -0,0 +1 @@
|
||||
- **CI**: the new Electron Windows prepare-bundle leg (WS1.5) is advisory while it matures — its first real run failed with the error swallowed by pwsh; the step now runs under bash (stderr captured) with `continue-on-error`, tracked for promotion once green
|
||||
1
changelog.d/maintenance/electron-win-prepare-smoke.md
Normal file
1
changelog.d/maintenance/electron-win-prepare-smoke.md
Normal file
@@ -0,0 +1 @@
|
||||
- **CI**: the Electron Package Smoke job now runs a Windows leg that executes `prepare:bundle` (the native ABI rebuild + spawn plan) per release PR — the v3.8.48 Windows bug (`npx.cmd` spawned without shell) could previously only surface on the release tag, its first-ever execution
|
||||
@@ -0,0 +1 @@
|
||||
- **Quality gates hygiene (WS6/D3 + WS1.7)**: gitleaks baseline zeroed — the 3 frozen `generic-api-key` false positives (latency field names + the public Anthropic beta-header value) are allowlisted with justification, so any NEW secret finding now regresses the ratchet from 0; the orphaned `semgrepFindings` baseline metric was dropped (never wired to a gate; CodeQL covers the OWASP families); Dockerfile now has a hadolint gate in the lint job (digest-pinned, error-threshold — the 5 pre-existing warnings stay visible without blocking)
|
||||
1
changelog.d/maintenance/hotfix-fastlane.md
Normal file
1
changelog.d/maintenance/hotfix-fastlane.md
Normal file
@@ -0,0 +1 @@
|
||||
- **CI**: hotfix fast-lane — PRs labeled `hotfix` (owner-applied, production-broken only; entry policy in `docs/ops/RELEASE_CHECKLIST.md`) skip the 9-shard E2E matrix, coverage ratchet and extended gates while keeping build, unit/integration/vitest, lint/typecheck and the tarball boot-smoke (~15min instead of ~33min); tests-only diffs outside `tests/e2e/` skip the E2E matrix automatically via the new `testsOnly` change-classification output
|
||||
1
changelog.d/maintenance/mergify-queue-draft-ci.md
Normal file
1
changelog.d/maintenance/mergify-queue-draft-ci.md
Normal file
@@ -0,0 +1 @@
|
||||
- **CI**: quality.yml draft guards now also match Mergify speculative merge-queue PRs (`mergify/merge-queue/*` heads are drafts by design) — without this every queued batch failed its anchor check in 2s and dequeued
|
||||
1
changelog.d/maintenance/mergify-queue.md
Normal file
1
changelog.d/maintenance/mergify-queue.md
Normal file
@@ -0,0 +1 @@
|
||||
- **Merge queue (D5)**: reviewed PRs now merge through the Mergify queue (`.mergify.yml`, Open Source plan) — entry is the owner-applied `queue` label AFTER the pre-merge ⭐ gate; batches of up to 10 validate together with automatic bisection of red batches (~log2(N) instead of N revalidations); the manual merge-train is codified as the fallback runbook in `docs/ops/MERGE_TRAIN.md` with the freeze/cross-session guardrails
|
||||
1
changelog.d/maintenance/npm-staged-publishing.md
Normal file
1
changelog.d/maintenance/npm-staged-publishing.md
Normal file
@@ -0,0 +1 @@
|
||||
- **Release**: npm publishing is now STAGED by default — the workflow boots the packed tarball (`check:pack-boot`) and runs `npm stage publish` (bytes parked on the registry, not installable); the owner verifies the staged bytes and releases them with `npm stage approve` + 2FA, moving the human gate to after the proof (the structural fix for the #7065 broken-tarball class); `publish_mode=direct` remains as a documented emergency fallback
|
||||
1
changelog.d/maintenance/quality-policies-ws5.md
Normal file
1
changelog.d/maintenance/quality-policies-ws5.md
Normal file
@@ -0,0 +1 @@
|
||||
- **Docs**: `QUALITY_GATES.md` now codifies the per-runner test retry policy (Playwright 1 CI retry with trace; Vitest per-test explicit quarantine only; node:test never) with target flake SLOs, and the release-level ratchet-drift rule (combination drift on the pure tip is the release captain's to fix once on the branch — never pushed onto contributor PRs, never rebaselined per-PR)
|
||||
1
changelog.d/maintenance/release-green-continuous.md
Normal file
1
changelog.d/maintenance/release-green-continuous.md
Normal file
@@ -0,0 +1 @@
|
||||
- **CI**: release-green validation is now continuous — every code push to `release/v*` (including the captain's direct sync-back pushes, the one previously ungated write path) triggers a `--quick` HARD-gate run with per-branch superseded-run cancellation, and the tracking issue names the offending push range; the deep `--with-build --full-ci` sweep now runs 3×/day instead of nightly-only (base-red MTTD: ≤24h → ~15min after the offending push)
|
||||
1
changelog.d/maintenance/runner-janitor.md
Normal file
1
changelog.d/maintenance/runner-janitor.md
Normal file
@@ -0,0 +1 @@
|
||||
- **Ops**: `scripts/ops/runner-janitor.sh` + `docs/ops/RUNNER_BOX.md` codify the self-hosted runner box hygiene that was manual discipline — 30min cron sweeping stale runner temp dirs, alerting at ≥85% disk, and enforcing the proven 4-runner ceiling on the 16 GB box (8-wide OOM-killed jobs twice on the v3.8.47 release day)
|
||||
1
changelog.d/maintenance/selfref-6634-shallow.md
Normal file
1
changelog.d/maintenance/selfref-6634-shallow.md
Normal file
@@ -0,0 +1 @@
|
||||
- **Tests**: the #6634 self-reference test now fetches `origin/main` on demand and skips cleanly when the ref is unreachable — it failed as a false positive on shallow/single-ref checkouts (GitHub-hosted runners)
|
||||
1
changelog.d/maintenance/sync-back-green-gate.md
Normal file
1
changelog.d/maintenance/sync-back-green-gate.md
Normal file
@@ -0,0 +1 @@
|
||||
- **Release tooling**: `sync-next-cycle.mjs` now runs `validate-release-green --quick` on the merged tree BEFORE pushing the parallel-cycle sync-back — the one write path to the release branch that had no CI gate; a red merged tree stays local instead of turning the whole PR queue red (`--skip-green-gate` is the documented emergency hatch)
|
||||
1
changelog.d/maintenance/tighten-coverage-baseline.md
Normal file
1
changelog.d/maintenance/tighten-coverage-baseline.md
Normal file
@@ -0,0 +1 @@
|
||||
- **chore(quality):** tighten the coverage ratchet to the CI's real numbers (branches 73→78.1, statements/lines 76.5→80.8, functions 82→86.44, plus 7 per-module floors). The gate had been asking for this in plain text; the values come from the merged-coverage run on `main`, not a local run (local measures ~68% vs CI's ~80% — the baseline's own note warns about that gap).
|
||||
1
changelog.d/maintenance/trunk-flaky-uploads.md
Normal file
1
changelog.d/maintenance/trunk-flaky-uploads.md
Normal file
@@ -0,0 +1 @@
|
||||
- **CI**: Playwright E2E and both vitest suites now emit JUnit and upload to Trunk Flaky Tests (org `omniroute`) — advisory step, own-origin only, uploader action SHA-pinned (WS5.2/5.3 of the quality plan; node:test stays out of the first wave)
|
||||
1
changelog.d/maintenance/trunk-upload-fastpath.md
Normal file
1
changelog.d/maintenance/trunk-upload-fastpath.md
Normal file
@@ -0,0 +1 @@
|
||||
- **CI**: the fast-path Vitest job (every PR) now also emits JUnit and uploads to Trunk Flaky Tests — the heavy-gate uploads alone (release PR only) would never accumulate flaky-detection volume
|
||||
1
changelog.d/maintenance/ts7-shadow-typecheck.md
Normal file
1
changelog.d/maintenance/ts7-shadow-typecheck.md
Normal file
@@ -0,0 +1 @@
|
||||
- **CI**: TypeScript 7 (native compiler, GA 2026-07-08) now runs as an advisory SHADOW of the blocking `typecheck:core` gate on the fast path — same tsconfig, isolated `npx` (no dependency change; the Compiler API only lands in TS 7.1, so typescript-eslint/type-coverage/Stryker stay on 6.x). Local parity proven (0 errors on both, exit 0); promotion to the blocking gate after ~1 week of CI parity
|
||||
1
changelog.d/maintenance/verify-published.md
Normal file
1
changelog.d/maintenance/verify-published.md
Normal file
@@ -0,0 +1 @@
|
||||
- **Release tooling**: new `scripts/release/verify-published.mjs <version>` — post-publish net that installs the published version from the public registry inside a clean `node:24-slim` container and boots it until `/api/monitoring/health` reports the expected version (validates the exact bytes users install, on a machine with no repo/devbox state); wired into the release Phase 4 monitoring playbook
|
||||
1
changelog.d/maintenance/vitest-ui-blocking.md
Normal file
1
changelog.d/maintenance/vitest-ui-blocking.md
Normal file
@@ -0,0 +1 @@
|
||||
- **CI**: promote `test:vitest:ui` to a blocking gate — the suite is 870/870 green again after the WS6.1 triage (#7127), so `continue-on-error` is removed from the vitest job
|
||||
1
changelog.d/maintenance/vitest-ui-suite-green.md
Normal file
1
changelog.d/maintenance/vitest-ui-suite-green.md
Normal file
@@ -0,0 +1 @@
|
||||
- chore(tests): fix all 70 failing `test:vitest:ui` tests across 30 files (was advisory/parked) — root causes were 15 node:test-authored `.tsx` files never collected by vitest, a missing `window.matchMedia` jsdom polyfill, stale assertions against a redesigned BuildTab wizard / CompressionHub Phase-2 UI, and one obsolete test for a retired Plans screen; suite is now 158/158 files, 870/870 tests green (promotion to blocking is a follow-up)
|
||||
19
codecov.yml
Normal file
19
codecov.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
# Codecov — WS5.6/D7 of the v3.8.49 quality/velocity plan.
|
||||
# Philosophy: strict patch, lenient project — the project floor/ratchet already
|
||||
# lives in quality-baseline.json + the c8 60% gate; Codecov adds the DIFF view
|
||||
# ("new lines in this PR are covered"), which the global ratchet cannot see.
|
||||
# INFORMATIONAL during calibration: nothing here blocks a PR. Promote by flipping
|
||||
# informational to false after ~2 weeks without false blocks (owner decision).
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
informational: true
|
||||
patch:
|
||||
default:
|
||||
target: 70%
|
||||
informational: true
|
||||
comment:
|
||||
layout: "condensed_header, diff"
|
||||
behavior: default
|
||||
require_changes: true
|
||||
259
config/quality/dashboard-typecheck-baseline.json
Normal file
259
config/quality/dashboard-typecheck-baseline.json
Normal file
@@ -0,0 +1,259 @@
|
||||
{
|
||||
"open-sse/services/payloadRules.ts": {
|
||||
"TS2677": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/HomePageClient.tsx": {
|
||||
"TS2339": 16
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient.tsx": {
|
||||
"TS2503": 3
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/agent-skills/components/CoverageBar.tsx": {
|
||||
"TS2503": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/agent-skills/components/McpA2aLinksBar.tsx": {
|
||||
"TS2503": 2
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/agent-skills/components/SkillCard.tsx": {
|
||||
"TS2503": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/agent-skills/components/SkillPreviewPane.tsx": {
|
||||
"TS2503": 2
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/cache/__tests__/CachePage.test.tsx": {
|
||||
"TS2305": 3,
|
||||
"TS1117": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/cache/__tests__/CachePerformance.test.tsx": {
|
||||
"TS2305": 1,
|
||||
"TS2322": 2
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/cache/__tests__/CacheTrends.test.tsx": {
|
||||
"TS2305": 1,
|
||||
"TS2322": 6
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/cache/__tests__/IdempotencyLayer.test.tsx": {
|
||||
"TS2305": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/cache/__tests__/MemoryCards.test.tsx": {
|
||||
"TS2305": 1,
|
||||
"TS2322": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/cache/components/CachePerformance.tsx": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/cli-code/components/ClaudeToolCard.tsx": {
|
||||
"TS2339": 2
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/cli-code/components/CodexToolCard.tsx": {
|
||||
"TS2345": 3
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/cli-code/components/CustomCliCard.tsx": {
|
||||
"TS2345": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/cli-code/components/DroidToolCard.tsx": {
|
||||
"TS2554": 2
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/combos/page.tsx": {
|
||||
"TS2339": 4,
|
||||
"TS2345": 5,
|
||||
"TS2698": 1,
|
||||
"TS2322": 13
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/compression/studio/EncoderComparisonTable.tsx": {
|
||||
"TS2322": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx": {
|
||||
"TS2304": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/costs/quota-share/QuotaSharePageClient.tsx": {
|
||||
"TS2551": 7,
|
||||
"TS2322": 2,
|
||||
"TS2719": 2,
|
||||
"TS2739": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/costs/quota-share/components/StackedAllocationBar.tsx": {
|
||||
"TS2503": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/costs/quota-share/components/UsageLogCard.tsx": {
|
||||
"TS2869": 2
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/discovery/__tests__/DiscoveryPageClient.test.tsx": {
|
||||
"TS2305": 2
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx": {
|
||||
"TS2322": 18
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/memory/components/tabs/MemoriesTab.tsx": {
|
||||
"TS2322": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/omni-skills/OmniSkillsPageClient.tsx": {
|
||||
"TS2503": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/omni-skills/components/OmniExecutionsTab.tsx": {
|
||||
"TS2503": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/omni-skills/components/OmniMarketplaceTab.tsx": {
|
||||
"TS2503": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/omni-skills/components/OmniSandboxTab.tsx": {
|
||||
"TS2503": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/omni-skills/components/OmniSkillCard.tsx": {
|
||||
"TS2503": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/omni-skills/components/OmniSkillsList.tsx": {
|
||||
"TS2503": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/omni-skills/components/SkillInspectorPane.tsx": {
|
||||
"TS2503": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/playground/components/PresetPicker.tsx": {
|
||||
"TS2352": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/playground/components/ToolsBuilder.tsx": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx": {
|
||||
"TS2322": 4
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/__tests__/phase1e.test.tsx": {
|
||||
"TS2741": 2
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/components/CompatibleModelsSection.tsx": {
|
||||
"TS2741": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionRow.tsx": {
|
||||
"TS2345": 3,
|
||||
"TS2322": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionsListPanel.tsx": {
|
||||
"TS2322": 2
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/components/CustomModelsSection.tsx": {
|
||||
"TS2739": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/components/ModelCompatPopover.tsx": {
|
||||
"TS2304": 5
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/components/ProviderModalsPanel.tsx": {
|
||||
"TS2322": 3,
|
||||
"TS2739": 1,
|
||||
"TS2345": 3
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/components/ProviderModelsSection.tsx": {
|
||||
"TS2322": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/components/ProviderParamFilterSection.tsx": {
|
||||
"TS2339": 6
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/components/ProviderPlaygroundPanel.tsx": {
|
||||
"TS2503": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/components/__tests__/phase1d.test.tsx": {
|
||||
"TS2739": 2
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": {
|
||||
"TS2322": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelImportHandlers.ts": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelVisibilityHandlers.ts": {
|
||||
"TS2339": 15
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/providers/components/onboarding/providerOnboardingCatalog.ts": {
|
||||
"TS2339": 4,
|
||||
"TS2345": 2
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/providers/providerPageUtils.ts": {
|
||||
"TS2345": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/providers/services/components/CliproxyModelMappingEditor.tsx": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/quota/page.tsx": {
|
||||
"TS2339": 4
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": {
|
||||
"TS2304": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx": {
|
||||
"TS2339": 4
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/settings/components/RedisLauncherPanel.tsx": {
|
||||
"TS2345": 11
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/settings/components/SidebarTab.tsx": {
|
||||
"TS2322": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/settings/components/proxy/FreePoolTab.tsx": {
|
||||
"TS2304": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/tools/traffic-inspector/components/CustomHostsManager.tsx": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/tools/traffic-inspector/components/tabs/ResponseBodyTab.tsx": {
|
||||
"TS2339": 5
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/translator/components/MonitorTab.tsx": {
|
||||
"TS2345": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/translator/components/advanced/CompressionPreviewAccordion.tsx": {
|
||||
"TS4104": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/translator/components/advanced/StreamTransformerAccordion.tsx": {
|
||||
"TS2345": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx": {
|
||||
"TS2339": 2
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaEnvGroup.tsx": {
|
||||
"TS2739": 1
|
||||
},
|
||||
"src/lib/combos/builderDraft.ts": {
|
||||
"TS2741": 1
|
||||
},
|
||||
"src/lib/providers/codexFastTier.ts": {
|
||||
"TS2367": 1
|
||||
},
|
||||
"src/lib/services/htmlRewriter.ts": {
|
||||
"TS2322": 2,
|
||||
"TS2345": 2
|
||||
},
|
||||
"src/mitm/inspector/sseMerger.ts": {
|
||||
"TS2352": 1
|
||||
},
|
||||
"src/shared/components/Header.tsx": {
|
||||
"TS2353": 1
|
||||
},
|
||||
"src/shared/components/MonacoEditor.tsx": {
|
||||
"TS2307": 1
|
||||
},
|
||||
"src/shared/components/OAuthModal.tsx": {
|
||||
"TS2769": 4,
|
||||
"TS2345": 4
|
||||
},
|
||||
"src/shared/components/SkillsConceptCard.tsx": {
|
||||
"TS2503": 1
|
||||
},
|
||||
"src/shared/components/analytics/charts.tsx": {
|
||||
"TS2345": 1
|
||||
},
|
||||
"src/shared/components/analytics/rechartsDonuts.tsx": {
|
||||
"TS2739": 2
|
||||
},
|
||||
"src/shared/hooks/useElectron.ts": {
|
||||
"TS2339": 19
|
||||
},
|
||||
"src/shared/providers/webSessionCredentials.ts": {
|
||||
"TS2353": 1,
|
||||
"TS2322": 1
|
||||
},
|
||||
"src/shared/schemas/cliCatalog.ts": {
|
||||
"TS2554": 2
|
||||
},
|
||||
"src/shared/services/opencodeConfig.ts": {
|
||||
"TS2345": 1
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,7 @@
|
||||
"concurrently",
|
||||
"cross-env",
|
||||
"csv-stringify",
|
||||
"ctrf",
|
||||
"dompurify",
|
||||
"dpdm",
|
||||
"electron",
|
||||
@@ -63,6 +64,7 @@
|
||||
"glob",
|
||||
"http-proxy-middleware",
|
||||
"https-proxy-agent",
|
||||
"httpyac",
|
||||
"husky",
|
||||
"ink",
|
||||
"ink-spinner",
|
||||
@@ -74,6 +76,7 @@
|
||||
"jscpd",
|
||||
"jsdom",
|
||||
"jsonc-parser",
|
||||
"junit-to-ctrf",
|
||||
"keytar",
|
||||
"knip",
|
||||
"license-checker-rseidelsohn",
|
||||
@@ -99,7 +102,9 @@
|
||||
"pino-abstract-transport",
|
||||
"pino-pretty",
|
||||
"playwright",
|
||||
"playwright-ctrf-json-reporter",
|
||||
"prettier",
|
||||
"promptfoo",
|
||||
"react",
|
||||
"react-dom",
|
||||
"react-is",
|
||||
|
||||
39
config/quality/e2e-timings.json
Normal file
39
config/quality/e2e-timings.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"_meta": "Relative weights for scripts/quality/balance-e2e-shards.mjs (LPT shard packing). Unitless \u2014 only ratios matter. Seeded from spec LOC (proxy) on 2026-07-13; replace values with real per-file durations (seconds) from a full CI run's reports whenever convenient. New specs without an entry get the median weight.",
|
||||
"a11y-resilience.spec.ts": 59,
|
||||
"a11y.spec.ts": 281,
|
||||
"agent-bridge-traffic-cross.spec.ts": 155,
|
||||
"agent-bridge.spec.ts": 160,
|
||||
"agent-skills-page.spec.ts": 205,
|
||||
"analytics-tabs.spec.ts": 334,
|
||||
"api-keys-flow.spec.ts": 643,
|
||||
"api.spec.ts": 30,
|
||||
"combo-live-studio.spec.ts": 35,
|
||||
"combo-unification.spec.ts": 192,
|
||||
"combos-flow.spec.ts": 629,
|
||||
"compression-studio.spec.ts": 55,
|
||||
"error-pages.spec.ts": 101,
|
||||
"group-b-activity-feed.spec.ts": 96,
|
||||
"group-b-quota-plans-config.spec.ts": 154,
|
||||
"group-b-quota-share-pools.spec.ts": 98,
|
||||
"group-b-redirect-logs-activity.spec.ts": 56,
|
||||
"memory-engine.spec.ts": 607,
|
||||
"memory-qdrant-routes.spec.ts": 360,
|
||||
"memory-settings.spec.ts": 193,
|
||||
"navigation.spec.ts": 28,
|
||||
"playground-compare.spec.ts": 138,
|
||||
"playground-studio.spec.ts": 101,
|
||||
"protocol-visibility.spec.ts": 25,
|
||||
"providers-bailian-coding-plan.spec.ts": 240,
|
||||
"providers-management.spec.ts": 324,
|
||||
"proxy-registry.smoke.spec.ts": 218,
|
||||
"resilience-plan-alignment.spec.ts": 382,
|
||||
"responsive.spec.ts": 21,
|
||||
"search-tools-studio.spec.ts": 133,
|
||||
"settings-toggles.spec.ts": 127,
|
||||
"skills-marketplace.spec.ts": 209,
|
||||
"smoke.spec.ts": 33,
|
||||
"traffic-inspector.spec.ts": 212,
|
||||
"translator-friendly.spec.ts": 115,
|
||||
"visual-resilience-smoke.spec.ts": 20
|
||||
}
|
||||
@@ -27,22 +27,22 @@
|
||||
"eps": 0
|
||||
},
|
||||
"coverage.statements": {
|
||||
"value": 76.5,
|
||||
"value": 80.8,
|
||||
"direction": "up",
|
||||
"tightenSlack": 5
|
||||
},
|
||||
"coverage.lines": {
|
||||
"value": 76.5,
|
||||
"value": 80.8,
|
||||
"direction": "up",
|
||||
"tightenSlack": 5
|
||||
},
|
||||
"coverage.functions": {
|
||||
"value": 82,
|
||||
"value": 86.44,
|
||||
"direction": "up",
|
||||
"tightenSlack": 5
|
||||
},
|
||||
"coverage.branches": {
|
||||
"value": 73,
|
||||
"value": 78.1,
|
||||
"direction": "up",
|
||||
"eps": 1.5,
|
||||
"tightenSlack": 5
|
||||
@@ -54,49 +54,49 @@
|
||||
"tightenSlack": 10
|
||||
},
|
||||
"coverage.combo.lines": {
|
||||
"value": 80,
|
||||
"value": 85.42,
|
||||
"direction": "up",
|
||||
"eps": 1.5,
|
||||
"tightenSlack": 10
|
||||
},
|
||||
"coverage.accountFallback.lines": {
|
||||
"value": 88,
|
||||
"value": 96.78,
|
||||
"direction": "up",
|
||||
"eps": 1.5,
|
||||
"tightenSlack": 10
|
||||
},
|
||||
"coverage.auth.lines": {
|
||||
"value": 90,
|
||||
"value": 92.55,
|
||||
"direction": "up",
|
||||
"eps": 1.5,
|
||||
"tightenSlack": 10
|
||||
},
|
||||
"coverage.routeGuard.lines": {
|
||||
"value": 94,
|
||||
"value": 98.73,
|
||||
"direction": "up",
|
||||
"eps": 1.5,
|
||||
"tightenSlack": 10
|
||||
},
|
||||
"coverage.error.lines": {
|
||||
"value": 88,
|
||||
"value": 92.13,
|
||||
"direction": "up",
|
||||
"eps": 1.5,
|
||||
"tightenSlack": 10
|
||||
},
|
||||
"coverage.publicCreds.lines": {
|
||||
"value": 92,
|
||||
"value": 99.07,
|
||||
"direction": "up",
|
||||
"eps": 1.5,
|
||||
"tightenSlack": 10
|
||||
},
|
||||
"coverage.circuitBreaker.lines": {
|
||||
"value": 92,
|
||||
"value": 95.09,
|
||||
"direction": "up",
|
||||
"eps": 1.5,
|
||||
"tightenSlack": 10
|
||||
},
|
||||
"openapiCoverage.pct": {
|
||||
"value": 38.0,
|
||||
"value": 38,
|
||||
"direction": "up",
|
||||
"eps": 0.5,
|
||||
"_tighten_2026_07_04_v3844_release": "36.9 -> 39.3 (aperto exigido pelo --require-tighten no PR de release #5925). A cobertura OpenAPI melhorou no ciclo (9 rotas documentadas em 8fb020676 + as rotas novas de #5939/#5817/#6034/#5998 documentadas junto das features). 39.3 = valor medido pelo CI Quality Ratchet no run 28708141003 (tip 00c55afcb).",
|
||||
@@ -158,7 +158,8 @@
|
||||
"dedicatedGate": true
|
||||
},
|
||||
"secretFindings": {
|
||||
"value": 3,
|
||||
"_note": "Zeroed 2026-07-13 (WS6/D3): the 3 frozen generic-api-key FPs are allowlisted with justification in .gitleaks.toml — any NEW finding regresses the ratchet.",
|
||||
"value": 0,
|
||||
"direction": "down",
|
||||
"dedicatedGate": true
|
||||
},
|
||||
@@ -187,12 +188,6 @@
|
||||
"dedicatedGate": true,
|
||||
"_note": "oasdiff breaking-change gate (Fase 9 Onda 0). Blocks any breaking change vs base spec."
|
||||
},
|
||||
"semgrepFindings": {
|
||||
"value": 0,
|
||||
"direction": "down",
|
||||
"dedicatedGate": true,
|
||||
"_note": "semgrep owasp/secrets findings. ADVISORY until first CI value is frozen, then flip blocking (Fase 9)."
|
||||
},
|
||||
"mutationScore.src/sse/services/auth.ts": {
|
||||
"value": 52.57,
|
||||
"direction": "up",
|
||||
|
||||
@@ -46,6 +46,7 @@ Runs on every PR to `main`. Blocks merge on failure.
|
||||
| `check:docs-sync` | CHANGELOG version, OpenAPI version, and `llm.txt` are in sync | Yes |
|
||||
| `typecheck:core` | TypeScript compilation without errors (advisory warnings only) | Yes |
|
||||
| `typecheck:noimplicit:core` | Strict `noImplicitAny` — forward-looking; many pre-existing call sites still need annotations | **Advisory** (`continue-on-error: true`) |
|
||||
| `check:dashboard-typecheck` | `tsc` scoped to `src/app/(dashboard)/**` (#7033) — `typecheck:core`'s curated 27-file allowlist does not include any dashboard TSX, and `next build` never type-checks it either (`next.config.mjs` sets `ignoreBuildErrors: true`), so orphaned-identifier regressions there (#6625/#6909) were invisible to CI. Diffs against a frozen per-file/per-TS-code count baseline (`config/quality/dashboard-typecheck-baseline.json`, same stale-enforcement pattern as `check:known-symbols`) — only NEW errors beyond the baselined count fail the gate; ratchet down with `--update` when a pre-existing error is fixed. | Yes |
|
||||
|
||||
### Job: `quality-gate`
|
||||
|
||||
@@ -173,6 +174,31 @@ pending implementation).
|
||||
|
||||
---
|
||||
|
||||
## Test Retry Policy (WS5.4, v3.8.49)
|
||||
|
||||
Retry is per-runner, never a global blanket — a blanket retry converts real regressions
|
||||
into invisible flakes:
|
||||
|
||||
| Runner | Policy | Why |
|
||||
| --- | --- | --- |
|
||||
| Playwright (e2e) | `retries: 1` in CI only, with `trace: on-first-retry` | Browser/network timing is genuinely nondeterministic; one retry with a trace turns a flake into a diagnosable artifact |
|
||||
| Vitest | NO global retry. A proven-flaky test gets an explicit per-test retry (visible in the diff, reviewed in PR) | Keeps the quarantine list in the repo, never opaque |
|
||||
| node:test (unit) | NO retry, ever | A flaky unit test is a bug in the test — fix it, don't re-roll it |
|
||||
|
||||
Target SLOs once flake telemetry lands (WS5.2/5.3): <1% flake rate per test
|
||||
("fix now" threshold), ≥95% pass rate per pipeline. Industry reference values —
|
||||
recalibrate against our own measurements.
|
||||
|
||||
## Release-Level Ratchet Drift (WS5.5, v3.8.49)
|
||||
|
||||
When a ratchet (file-size, complexity, eslint warnings) regresses on the PURE release
|
||||
tip — i.e. the COMBINATION of merges regressed it, and no single PR reproduces the
|
||||
regression on its own branch — the fix belongs to the **release captain, once, on the
|
||||
release branch**: prefer extraction/refactor; rebaseline only with the documented
|
||||
justification entry. Never push combination drift onto a contributor PR, and never
|
||||
rebaseline per-PR (that hides real regressions). Discriminate first: reproduce the
|
||||
red against the pure tip in a probe worktree before assuming your PR caused it.
|
||||
|
||||
## Allowlist Policy
|
||||
|
||||
Every gate that cannot fail on pre-existing violations uses a frozen allowlist
|
||||
|
||||
@@ -278,7 +278,8 @@ Every compressed request includes stats in the server logs:
|
||||
| Phase 1 | Off, Lite | ✅ Shipped |
|
||||
| Phase 2 | Standard, Aggressive, Ultra | ✅ Shipped |
|
||||
| Phase 3 | RTK, Stacked, Compression Combos | ✅ Shipped |
|
||||
| Phase 4 | Output Styles, SLM-tier Ultra, adaptive context-budget, eval harness | ✅ Shipped |
|
||||
| Phase 4 | Output Styles, SLM-tier Ultra, eval harness | ✅ Shipped |
|
||||
| Phase 4C | Adaptive context-budget ("dial") — compute engine + API (`contextBudget` on `PUT /api/settings/compression`) | ✅ Shipped (API-configurable; dashboard controls not yet built, #7005) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -272,7 +272,7 @@ OmniRoute 提供两层防护:请求侧的注入扫描和响应侧的 PII 脱
|
||||
| `OMNIROUTE_PUBLIC_URL` | _(未设置)_ | `open-sse/utils/kieTask.ts` | 用于组合异步回调 URL 的公共源。kie.ai 回调的最低优先级回退;也用作其他中继的通用公共 URL。 |
|
||||
| `OMNIROUTE_CROF_USAGE_URL` | `https://crof.ai/usage_api/` | `open-sse/services/usage.ts` | Usage 页面使用的 CrofAI 配额查询端点。可覆盖为中继/测试固定件。 |
|
||||
| `OMNIROUTE_OPENCODE_QUOTA_URL` | `https://opencode.ai/zen/go/v1/quota` | `open-sse/services/opencodeQuotaFetcher.ts` | Usage 页面使用的 OpenCode (zen/go) 配额查询端点。可覆盖为中继/测试固定件。 |
|
||||
| `OMNIROUTE_OPENCODE_GO_QUOTA_URL` | `https://api.z.ai/api/monitor/usage/quota/limit` | `open-sse/services/usage.ts` | Usage 页面使用的 OpenCode Go 配额查询端点。可覆盖为中继/测试固定件。 |
|
||||
| `OMNIROUTE_OPENCODE_GO_QUOTA_URL` | _(未设置)_ | `open-sse/services/opencodeOllamaUsage.ts` | Usage 页面使用的 OpenCode Go 配额查询端点。OpenCode Go 没有公开的配额 API,因此没有默认值;除非运维人员显式设置该变量选择接入自建/镜像端点,否则不会发起网络请求。 |
|
||||
| `OMNIROUTE_OPENCODE_GO_DASHBOARD_URL` | `https://opencode.ai/workspace` | `open-sse/services/usage.ts` | 配置了 workspace ID 和 auth Cookie 时用于配额抓取的 OpenCode Go Dashboard 基础 URL。可覆盖为中继/测试固定件。 |
|
||||
| `OPENCODE_GO_WORKSPACE_ID` | _(未设置)_ | `open-sse/services/usage.ts` | 用于 Dashboard 配额抓取的 OpenCode Go workspace ID。配置多个账户时,推荐使用每个连接的 Dashboard 字段。 |
|
||||
| `OMNIROUTE_OPENCODE_GO_WORKSPACE_ID` | _(未设置)_ | `open-sse/services/usage.ts` | OpenCode Go workspace ID 环境变量的备选名,在较短的别名之前使用。配置多个账户时,推荐使用每个连接的 Dashboard 字段。 |
|
||||
|
||||
104
docs/ops/HOMOLOGATION.md
Normal file
104
docs/ops/HOMOLOGATION.md
Normal file
@@ -0,0 +1,104 @@
|
||||
---
|
||||
title: "Homologation Suite (npm run homolog)"
|
||||
version: 3.8.49
|
||||
lastUpdated: 2026-07-14
|
||||
---
|
||||
|
||||
# Homologation Suite (`npm run homolog`)
|
||||
|
||||
Real-environment E2E validation of the OmniRoute deploy running on the homologation VPS
|
||||
(`HOMOLOG_BASE_URL`, e.g. `http://192.168.0.15:20128`). One command replaces the manual
|
||||
release STOP #2 checklist with an automated, evidence-producing run.
|
||||
|
||||
## What it covers
|
||||
|
||||
| Layer | What it checks | Implementation |
|
||||
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
||||
| L0 — health/parity | `/api/monitoring/health` responds `200` with `status: "healthy"` and the expected version | `scripts/homolog/lib/parity.mjs` |
|
||||
| L1a — ephemeral key | Admin login → `POST /api/keys` creates a scoped API key for the run, revoked (`DELETE /api/keys/:id`) in a `finally` block regardless of outcome | `scripts/homolog/lib/adminClient.mjs` |
|
||||
| L1b — API surface | `/v1/models` catalog, a real non-streaming chat completion (tier-critical model, `max_tokens: 5`), an invalid-key `401`, and public `/api/monitoring/health` | `tests/homolog/api/core.http` (httpYac) |
|
||||
| L1c — SSE streaming | Real streaming chat completion; asserts `text/event-stream`, at least one content delta, and a `[DONE]` terminator | `scripts/homolog/lib/sseCheck.mjs` |
|
||||
| L2 — real providers | One minimal-cost chat request per critical provider present in the live `/v1/models` catalog, generated on the fly via promptfoo | `scripts/homolog/gen-promptfoo.mjs` + `scripts/homolog/lib/providerTiers.mjs` |
|
||||
| L4a — UI auth | Logs in once via the real login form and reuses the session (`storageState`) across the UI layer | `tests/homolog/ui/auth.setup.ts` |
|
||||
| L4b — UI routes | Every static `page.tsx` under `src/app/(dashboard)/dashboard` (discovered from the filesystem, dynamic `[param]` routes skipped) loads without an HTTP error, a page error, or the Next.js error boundary | `tests/homolog/ui/routes.spec.ts` |
|
||||
| L4c — UI critical flow | Creates an API key through the dashboard UI and revokes it again (leaves no residue on the VPS) | `tests/homolog/ui/api-key-flow.spec.ts` |
|
||||
| L5 — unified report | Merges httpYac (via `junit-to-ctrf`), the promptfoo→CTRF adapter, and the Playwright CTRF reporter into one `homolog-ctrf.json`, plus a human-readable `homolog-report/summary.md` | `scripts/homolog/run.mjs` |
|
||||
|
||||
Zero LLM involvement in the replay itself — this is a deterministic regression battery,
|
||||
not an eval. AI only enters in future maintenance work (see Roadmap below).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Copy `.env.homolog.example` to `.env.homolog` (gitignored — never commit it) and fill in:
|
||||
- `HOMOLOG_BASE_URL` — the target deploy, e.g. `http://192.168.0.15:20128`.
|
||||
- `HOMOLOG_ADMIN_PASSWORD` — the dashboard management password for that deploy.
|
||||
- `HOMOLOG_CRITICAL_PROVIDERS` — comma-separated provider prefixes that get a real
|
||||
smoke chat request (e.g. `openai,anthropic,gemini,codex,grok,glm,deepseek,openrouter`).
|
||||
- `HOMOLOG_API_KEY` — leave empty in normal runs; the suite creates and revokes its
|
||||
own ephemeral key. Only set this to debug a single layer in isolation.
|
||||
2. `npm install` in the repo (the suite's dependencies — `httpyac`, `promptfoo`,
|
||||
`playwright-ctrf-json-reporter`, `junit-to-ctrf`, `ctrf` — are regular devDependencies).
|
||||
3. `npx playwright install` if the browser binaries are not already present.
|
||||
|
||||
## How to run
|
||||
|
||||
```bash
|
||||
npm run homolog
|
||||
```
|
||||
|
||||
To validate against a deploy whose version does not match the local `package.json`
|
||||
(e.g. a homologation box still on a previous patch release), override the expected
|
||||
version explicitly:
|
||||
|
||||
```bash
|
||||
HOMOLOG_EXPECT_VERSION=3.8.47 npm run homolog
|
||||
```
|
||||
|
||||
The run exits non-zero if any layer fails, and always attempts to revoke the ephemeral
|
||||
API key it created, even on failure (`finally` block in `scripts/homolog/run.mjs`).
|
||||
|
||||
## Reading the report
|
||||
|
||||
All output lands in `homolog-report/` (gitignored):
|
||||
|
||||
- `summary.md` — the same table printed to stdout, one row per layer (✅/❌ + detail).
|
||||
- `homolog-ctrf.json` — the unified CTRF report (merge of API/SSE, provider-smoke, and
|
||||
UI results) — this is the artifact to attach to a release STOP #2 checklist.
|
||||
- `httpyac-junit.xml`, `api-ctrf.json`, `providers-ctrf.json`, `ui-ctrf.json` — the
|
||||
per-layer raw/intermediate reports.
|
||||
- `promptfooconfig.yaml`, `provider-misses.json` — the generated promptfoo config for
|
||||
the current run and any critical providers that were missing from the live catalog.
|
||||
|
||||
A failing L0 aborts immediately (no ephemeral key is created) since a version/health
|
||||
mismatch means every downstream layer would be validating the wrong deploy.
|
||||
|
||||
## Re-baselining when the UI changes legitimately
|
||||
|
||||
L4b (route smoke) and L4c (API-key UI flow) are driven by real DOM locators, not
|
||||
snapshots, so most legitimate UI changes do not require any suite update. When a change
|
||||
does break a locator (e.g. a renamed button label or a moved settings page):
|
||||
|
||||
1. Re-confirm the locator against the current source (the specs already document which
|
||||
file/line each locator was confirmed against — follow the same pattern, don't guess).
|
||||
2. Update the spec in `tests/homolog/ui/`.
|
||||
3. Re-run `npm run homolog` (or just the affected Playwright spec) against the VPS to
|
||||
confirm the fix, then commit.
|
||||
|
||||
There is no visual/pixel baseline in this suite (F1) — see Roadmap for that.
|
||||
|
||||
## Roadmap (F2 / F3)
|
||||
|
||||
Design and phased rollout live in the internal planning spec
|
||||
`_tasks/superpowers/specs/2026-07-13-homolog-e2e-suite-design.md` (not linked — internal
|
||||
`_tasks/` artifact, not part of this repo's tracked docs). Summary:
|
||||
|
||||
- **F2** — full walkthrough recording → Playwright Test Agents (`planner`/`generator`)
|
||||
turn it into flow specs (create combo, test provider, edit settings, MCP tools) +
|
||||
visual regression baseline (Lost Pixel) with masks over dynamic data (metrics,
|
||||
timestamps, logs) + a `healer` maintenance routine per release.
|
||||
- **F3** — resilience/contract/wiring coverage: toxiproxy + a fake OpenAI-compatible
|
||||
provider on the devbox, a `homolog-resilience` combo on the VPS pointed at it
|
||||
(injected timeout → assert fallback + circuit breaker open/close via
|
||||
`/api/monitoring/health`); gated Schemathesis contract testing against
|
||||
`docs/openapi.yaml` (low `--max-examples`, fixed seeds, non-LLM endpoints only); and
|
||||
wiring `npm run homolog` + its `summary.md` into the `/generate-release` STOP #2 phase.
|
||||
63
docs/ops/MERGE_TRAIN.md
Normal file
63
docs/ops/MERGE_TRAIN.md
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: Merge Queue & Manual Merge-Train Runbook
|
||||
---
|
||||
|
||||
# Merge Queue & Manual Merge-Train Runbook
|
||||
|
||||
Since v3.8.49 (WS3.2/WS3.4 of the quality/velocity plan) the default merge path for
|
||||
reviewed PRs into `release/vX.Y.Z` is the **Mergify merge queue** (`.mergify.yml`);
|
||||
the **manual merge-train** documented below is the FALLBACK — used during incidents,
|
||||
release freezes, or if the Mergify Open Source plan ever changes.
|
||||
|
||||
## Default path: the Mergify queue
|
||||
|
||||
1. PR is reviewed/greened by the campaigns and approved by the owner's pre-merge ⭐
|
||||
gate (the report + per-item decision — see `/merge-prs` Step 0.75).
|
||||
2. The owner (or the session acting on the owner's decision) applies the **`queue`**
|
||||
label. The label IS the merge approval; Mergify only executes it.
|
||||
3. Mergify batches up to 10 queued PRs, validates the batch against the fast-gates,
|
||||
and merges (squash). A red batch is **bisected automatically** — the offending PR
|
||||
is isolated in ~log2(N) revalidations and unqueued; the rest proceed.
|
||||
4. Post-merge, the continuous release-green workflow validates the new tip on push
|
||||
and opens an attribution issue if the combination regressed (never auto-revert).
|
||||
|
||||
Guardrails (mirror `CLAUDE.md` Hard Rules #21/#22):
|
||||
|
||||
- **Release freeze open** → do NOT label PRs targeting the frozen branch; retarget to
|
||||
the active `release/vX+1` first.
|
||||
- **Another session's in-flight PR** → never label it; only the owning session queues
|
||||
its own work.
|
||||
- Tests-only diffs and `hotfix`-labeled PRs already run reduced CI (see
|
||||
`RELEASE_CHECKLIST.md` → Hotfix Fast-Lane); the queue conditions accept whatever
|
||||
check set actually ran (`#check-failure=0` + `#check-pending=0`).
|
||||
|
||||
## Fallback: the manual merge-train
|
||||
|
||||
Used when the queue is unavailable. This codifies the practice that drained 33 PRs in
|
||||
one day during the v3.8.47 cycle:
|
||||
|
||||
1. **Assemble the batch** (~10–30 reviewed+approved PRs). Check `linked:` collisions
|
||||
(same `tap.testFiles`, same CHANGELOG hunks) and serialize those.
|
||||
2. **Validate ONCE**: in an isolated worktree off the release tip, merge all batch
|
||||
heads locally, then run the release-equivalent suite
|
||||
(`npm run check:release-green`, add `--with-build` before a release).
|
||||
3. **Green** → merge the PRs in sequence (re-checking `state,headRefOid` before each —
|
||||
a PR whose head moved re-enters review). Prove the net diff of each merge is the
|
||||
PR's own change (no auto-resolve reverts: audit `git diff --stat` for
|
||||
out-of-scope deletions).
|
||||
4. **Red** → bisect the batch by halves (validate each half) instead of re-validating
|
||||
one-by-one; drop the offending PR back to the review queue with the evidence.
|
||||
5. **Never**: merge during a freeze into the frozen branch; `git stash` anywhere;
|
||||
blanket-rerun CI hoping a red goes away (rule: a red is information).
|
||||
|
||||
## Tiering (why the queue is safe with fast-gates only)
|
||||
|
||||
- **Per PR** (quality.yml fast-gates): TIA-impacted tests + full unit 4-shard +
|
||||
vitest + lint bag + typecheck + docs/changelog integrity.
|
||||
- **Per batch/tip** (continuous release-green): `--quick` HARD gates on every push to
|
||||
the release branch; full `--with-build --full-ci` sweeps 3×/day.
|
||||
- **Per release** (ci.yml on the release PR): the complete matrix incl. E2E ×9,
|
||||
package-artifact + tarball boot-smoke, coverage/ratchets.
|
||||
|
||||
Nothing is validated less than before — the heavy surface just runs per batch/tip
|
||||
instead of per PR, which is what removes the O(N) round-trips.
|
||||
@@ -37,6 +37,56 @@ npm run test:e2e # optional but recommended
|
||||
/capture-release-evidences-cc
|
||||
```
|
||||
|
||||
## npm Staged Publishing (default since v3.8.49 — WS1.3/D2)
|
||||
|
||||
The npm-publish workflow no longer publishes directly: it boots the packed tarball
|
||||
(`check:pack-boot`) and then runs `npm stage publish` — the exact bytes are parked on
|
||||
the registry, **not installable** until the owner approves. The human 2FA gate moved
|
||||
to AFTER the proof, not before it.
|
||||
|
||||
**Owner flow after the workflow goes green:**
|
||||
|
||||
1. `npm stage list omniroute` — find the stage id (also printed in the workflow summary).
|
||||
2. Verify the staged bytes (recommended): `npm stage download <id>`, then install the
|
||||
downloaded tarball into a temp prefix and boot it (`npm run check:pack-boot` automates
|
||||
the same pack→install→boot verdict in CI).
|
||||
3. `npm stage approve <id>` — the 2FA prompt IS the publish. `npm stage reject <id>` discards.
|
||||
4. Post-publish net: the post-publish verifier (WS1.4 of the v3.8.49 plan) installs the
|
||||
published version from the public registry in a clean container and boots it.
|
||||
|
||||
**Emergency fallback:** `workflow_dispatch` with `publish_mode=direct` restores the
|
||||
legacy immediate `npm publish` (use only if staging itself misbehaves; record why).
|
||||
|
||||
**One-time hardening (owner, npmjs.com):** configure the Trusted Publisher for
|
||||
`omniroute` in stage-only mode so a leaked long-lived token cannot `npm publish`
|
||||
directly from anywhere — CI can only stage; only the owner's 2FA releases.
|
||||
|
||||
**Broken-artifact playbook (unchanged):** `npm deprecate omniroute@<bad> "<reason> — use <fixed>"`
|
||||
as the default reflex (minutes, reversible); `npm unpublish` only inside the 72h/no-dependents
|
||||
window and never as the first move. Docker: never rewrite a version tag — rollback is
|
||||
repointing `latest` to the last good digest.
|
||||
## Hotfix Fast-Lane (label `hotfix`)
|
||||
|
||||
A PR labeled `hotfix` skips the heavy CI matrix (9-shard E2E, coverage ratchet,
|
||||
quality-gate, quality-extended) and keeps the fast, high-signal gates: build,
|
||||
unit shards, integration, vitest, lint/typecheck, docs-sync, `check:pack-artifact`
|
||||
and the tarball boot-smoke (`check:pack-boot`). Target: green in ≤15min instead of ~33min.
|
||||
|
||||
**Entry policy — all four required (modeled on Chromium/VS Code/Node emergency lanes):**
|
||||
|
||||
1. **Severity**: production is broken — a published artifact crashes on boot / a
|
||||
security fix / every user of the release is affected. "Important" is not "broken".
|
||||
2. **Authority**: only the repository owner applies the `hotfix` label. The label IS
|
||||
the approval — never self-serve on a campaign PR.
|
||||
3. **Evidence**: the PR body links the previous fully-green heavy run (the suite the
|
||||
skipped jobs would re-validate) plus the fix's own failing-then-passing test.
|
||||
4. **Scope**: cherry-pick-only — the minimal fix, no refactors, no ride-alongs.
|
||||
|
||||
The skipped coverage/ratchet surface is re-validated by the next full run on the
|
||||
release branch (continuous release-green) — the lane skips WAITING, never validation.
|
||||
Tests-only diffs (all files under `tests/`, none under `tests/e2e/`) skip the E2E
|
||||
matrix automatically, without any label.
|
||||
|
||||
## Detailed Checklist
|
||||
|
||||
### Pre-release
|
||||
|
||||
35
docs/ops/RUNNER_BOX.md
Normal file
35
docs/ops/RUNNER_BOX.md
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
title: Self-Hosted Runner Box Operations
|
||||
---
|
||||
|
||||
# Self-Hosted Runner Box Operations (.113 pool)
|
||||
|
||||
The self-hosted pool (`self-hosted, omni-release` labels) runs on the 16 GB box at
|
||||
`192.168.0.113`. Two failure modes recurred on release days and were, until v3.8.49,
|
||||
manual discipline; the **janitor script codifies them** (WS3.3 of the quality plan):
|
||||
|
||||
1. **Orphaned temp/work dirs** filling the disk → disk-full SQLite errors mid-job.
|
||||
2. **>4 concurrent runners** → OOM-killed jobs (8-wide killed jobs twice on the
|
||||
v3.8.47 release day; 4-wide is the proven ceiling).
|
||||
|
||||
## Install the janitor (one-time, on the box)
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /opt/omniroute-ops
|
||||
sudo cp scripts/ops/runner-janitor.sh /opt/omniroute-ops/
|
||||
sudo chmod +x /opt/omniroute-ops/runner-janitor.sh
|
||||
( sudo crontab -l 2>/dev/null; echo '*/30 * * * * /opt/omniroute-ops/runner-janitor.sh >> /var/log/runner-janitor.log 2>&1' ) | sudo crontab -
|
||||
```
|
||||
|
||||
What it does every 30min: sweeps runner temp leftovers older than 24h, alerts at
|
||||
≥85% root-disk usage, and alerts when more than the runner ceiling (default 4, tunable
|
||||
via the script's own environment) of `Runner.Listener` processes are up. Alerts land in `/var/log/runner-janitor.log`
|
||||
with a non-zero exit (grep for `⚠`).
|
||||
|
||||
## Operating rules
|
||||
|
||||
- **Ceiling: 4 runners** on the 16 GB box. Runners 5–8 stay STOPPED except for
|
||||
explicit off-peak experiments — never during a release window.
|
||||
- Stopping a runner mid-job cancels the job (observed live): `systemctl stop`
|
||||
only when its runner is idle (`Runner.Listener` without a `Runner.Worker` child).
|
||||
- The `.15` VPS is homologation-only — never runs CI runners.
|
||||
@@ -280,7 +280,7 @@ OmniRoute provides a two-layer defense: request-side injection scanning and resp
|
||||
| `OMNIROUTE_PUBLIC_URL` | _(unset)_ | `open-sse/utils/kieTask.ts` | Public origin used to compose async callback URLs. Lowest-priority fallback for kie.ai callbacks; also used as a generic public URL for other relays. |
|
||||
| `OMNIROUTE_CROF_USAGE_URL` | `https://crof.ai/usage_api/` | `open-sse/services/usage.ts` | CrofAI quota lookup endpoint used by the Usage page. Override for relays / test fixtures. |
|
||||
| `OMNIROUTE_OPENCODE_QUOTA_URL` | `https://opencode.ai/zen/go/v1/quota` | `open-sse/services/opencodeQuotaFetcher.ts` | OpenCode (zen/go) quota lookup endpoint used by the Usage page. Override for relays / test fixtures. |
|
||||
| `OMNIROUTE_OPENCODE_GO_QUOTA_URL` | `https://api.z.ai/api/monitor/usage/quota/limit` | `open-sse/services/usage.ts` | OpenCode Go quota lookup endpoint used by the Usage page. Override for relays / test fixtures. |
|
||||
| `OMNIROUTE_OPENCODE_GO_QUOTA_URL` | _(unset)_ | `open-sse/services/opencodeOllamaUsage.ts` | OpenCode Go quota lookup endpoint used by the Usage page. OpenCode Go has no public quota API, so this has no default and the network call is skipped unless the operator opts in to a self-hosted/mirrored endpoint. |
|
||||
| `OMNIROUTE_OPENCODE_GO_DASHBOARD_URL` | `https://opencode.ai/workspace` | `open-sse/services/usage.ts` | OpenCode Go dashboard base URL used for quota scraping when a workspace ID and auth cookie are configured. Override for relays / test fixtures. |
|
||||
| `OPENCODE_GO_WORKSPACE_ID` | _(unset)_ | `open-sse/services/usage.ts` | OpenCode Go workspace ID used for dashboard quota scraping. Prefer the per-connection Dashboard field when multiple accounts are configured. |
|
||||
| `OMNIROUTE_OPENCODE_GO_WORKSPACE_ID` | _(unset)_ | `open-sse/services/usage.ts` | Alternate OpenCode Go workspace ID env var used before the shorter alias. Prefer the per-connection Dashboard field when multiple accounts are configured. |
|
||||
|
||||
@@ -128,11 +128,25 @@ const nextConfig = {
|
||||
// expected diagnostic — suppress it here rather than fight the analyzer,
|
||||
// mirroring the isNextIntlExtractorDynamicImportWarning precedent below
|
||||
// for the webpack path. (#6582)
|
||||
// open-sse/services/compression/ruleLoader.ts and
|
||||
// .../engines/rtk/filterLoader.ts both define an identical
|
||||
// getModuleDir() helper that walks up directories via
|
||||
// path.resolve(anchor) + fs.existsSync(...) in a loop with a
|
||||
// non-literal argument — the same dynamic-path fs access pattern as
|
||||
// the agentSkills case above, but not covered by that narrower
|
||||
// allowlist glob, so the "Overly broad patterns..." warning kept
|
||||
// firing (610 times, once per entry point transitively importing the
|
||||
// compression module). Same known-benign, bounded fs access;
|
||||
// suppressed here rather than fought. (#7051, follow-up to #6582)
|
||||
ignoreIssue: [
|
||||
{
|
||||
path: "**/src/lib/agentSkills/**",
|
||||
description: /Overly broad patterns can lead to build performance issues/,
|
||||
},
|
||||
{
|
||||
path: "**/open-sse/services/compression/**",
|
||||
description: /Overly broad patterns can lead to build performance issues/,
|
||||
},
|
||||
],
|
||||
},
|
||||
output: "standalone",
|
||||
|
||||
@@ -23,29 +23,15 @@ export const opencodeProvider: RegistryEntry = {
|
||||
interleavedField: "reasoning_content",
|
||||
},
|
||||
{ id: "deepseek-v4-flash-free", name: "DeepSeek V4 Flash Free", supportsReasoning: true },
|
||||
// #3110: MiniMax M3 free tier via OpenCode
|
||||
// #3328: MiniMax M3 is multimodal (verified: describes base64 images via the
|
||||
// opencode upstream) — flag it so vision requests aren't gated/stripped.
|
||||
{
|
||||
id: "minimax-m3-free",
|
||||
name: "MiniMax M3 Free",
|
||||
contextLength: 1048576,
|
||||
supportsVision: true,
|
||||
},
|
||||
{ id: "minimax-m2.5-free", name: "MiniMax M2.5 Free", contextLength: 204800 },
|
||||
{ id: "ling-2.6-1t-free", name: "Ling 2.6 Free", contextLength: 262000 },
|
||||
{
|
||||
id: "trinity-large-preview-free",
|
||||
name: "Trinity Large Preview Free",
|
||||
contextLength: 131000,
|
||||
},
|
||||
{ id: "nemotron-3-super-free", name: "Nemotron 3 Super Free", contextLength: 1000000 },
|
||||
{
|
||||
id: "qwen3.6-plus-free",
|
||||
name: "Qwen3.6 Plus Free",
|
||||
targetFormat: "claude",
|
||||
supportsVision: false,
|
||||
contextLength: 200000,
|
||||
},
|
||||
// #6998: 2026-07-14 refresh — the upstream free tier rotated its lineup;
|
||||
// minimax-m3-free, minimax-m2.5-free, ling-2.6-1t-free,
|
||||
// trinity-large-preview-free, nemotron-3-super-free and qwen3.6-plus-free
|
||||
// were delisted (401 "Model X is not supported") and replaced by the 4
|
||||
// entries below, confirmed live against
|
||||
// https://opencode.ai/zen/v1/chat/completions.
|
||||
{ id: "mimo-v2.5-free", name: "MiMo V2.5 Free", contextLength: 131000 },
|
||||
{ id: "hy3-free", name: "HY3 Free", contextLength: 131000 },
|
||||
{ id: "nemotron-3-ultra-free", name: "Nemotron 3 Ultra Free", contextLength: 1000000 },
|
||||
{ id: "north-mini-code-free", name: "North Mini Code Free", contextLength: 131000 },
|
||||
],
|
||||
};
|
||||
|
||||
@@ -64,11 +64,18 @@ function shouldUseBrowserBacked(): boolean {
|
||||
interface DuckDuckGoVqdHeaders {
|
||||
vqd4: string | null;
|
||||
vqdHash1: string | null;
|
||||
// #6996: the real upstream HTTP status of the VQD-acquisition attempt (null when
|
||||
// no request was made / a network error was thrown). Lets execute() distinguish a
|
||||
// retryable 429 rate-limit from a genuine 5xx instead of collapsing both to 503.
|
||||
status: number | null;
|
||||
retryAfter: string | null;
|
||||
}
|
||||
|
||||
interface DuckDuckGoAuthHeaders {
|
||||
vqd4: string | null;
|
||||
vqdHash1: string | null;
|
||||
status: number | null;
|
||||
retryAfter: string | null;
|
||||
}
|
||||
|
||||
interface DuckDuckGoModelCapabilities {
|
||||
@@ -369,10 +376,13 @@ export class DuckDuckGoWebExecutor extends BaseExecutor {
|
||||
const isStreaming = stream !== false;
|
||||
const upstreamHeaders = upstreamExtraHeaders || {};
|
||||
|
||||
const errorResponse = (status: number, message: string): Response =>
|
||||
const errorResponse = (status: number, message: string, retryAfter?: string | null): Response =>
|
||||
new Response(JSON.stringify({ error: { message } }), {
|
||||
status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(retryAfter ? { "Retry-After": retryAfter } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
if (messages.length === 0) {
|
||||
@@ -468,6 +478,19 @@ export class DuckDuckGoWebExecutor extends BaseExecutor {
|
||||
const vqdHeaders = await this.acquireAuthHeaders(mergedSignal);
|
||||
if (!vqdHeaders.vqd4 && !vqdHeaders.vqdHash1) {
|
||||
clearTimeout(timeout);
|
||||
// #6996: surface the real upstream status instead of a hardcoded 503 so a
|
||||
// 429 rate-limit gets a connection-cooldown, not a whole-provider circuit
|
||||
// breaker trip (see CLAUDE.md "Provider Circuit Breaker" — only
|
||||
// 408/500/502/503/504 should trip it, not 429). Any other non-2xx status
|
||||
// (403 anti-bot challenge, genuine 5xx, or a thrown network error where
|
||||
// status is null) keeps the existing 503 fallback.
|
||||
if (vqdHeaders.status === 429) {
|
||||
return errorResponse(
|
||||
429,
|
||||
"Failed to acquire VQD token: upstream rate limited",
|
||||
vqdHeaders.retryAfter
|
||||
);
|
||||
}
|
||||
return errorResponse(503, "Failed to acquire VQD token");
|
||||
}
|
||||
|
||||
@@ -555,16 +578,25 @@ export class DuckDuckGoWebExecutor extends BaseExecutor {
|
||||
});
|
||||
this.rememberResponseCookies(resp);
|
||||
|
||||
if (!resp.ok) return { vqd4: null, vqdHash1: null };
|
||||
if (!resp.ok) {
|
||||
return {
|
||||
vqd4: null,
|
||||
vqdHash1: null,
|
||||
status: resp.status,
|
||||
retryAfter: resp.headers.get("Retry-After"),
|
||||
};
|
||||
}
|
||||
return {
|
||||
vqd4: resp.headers.get("x-vqd-4"),
|
||||
vqdHash1: resp.headers.get("x-vqd-hash-1"),
|
||||
status: resp.status,
|
||||
retryAfter: null,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof DOMException && error.name === "AbortError") {
|
||||
throw error;
|
||||
}
|
||||
return { vqd4: null, vqdHash1: null };
|
||||
return { vqd4: null, vqdHash1: null, status: null, retryAfter: null };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -576,6 +608,8 @@ export class DuckDuckGoWebExecutor extends BaseExecutor {
|
||||
return {
|
||||
vqd4: null,
|
||||
vqdHash1: await solveDuckDuckGoChallenge(challenge, FAKE_HEADERS["User-Agent"]),
|
||||
status: null,
|
||||
retryAfter: null,
|
||||
};
|
||||
} catch (error) {
|
||||
void error;
|
||||
@@ -588,6 +622,8 @@ export class DuckDuckGoWebExecutor extends BaseExecutor {
|
||||
return {
|
||||
vqd4: headers.vqd4,
|
||||
vqdHash1: await solveDuckDuckGoChallenge(headers.vqdHash1, FAKE_HEADERS["User-Agent"]),
|
||||
status: headers.status,
|
||||
retryAfter: headers.retryAfter,
|
||||
};
|
||||
} catch (error) {
|
||||
void error;
|
||||
|
||||
@@ -112,12 +112,15 @@ function parseCookies(raw: string): Array<{ name: string; value: string }> {
|
||||
* [["wrb.fr", null, "<JSON string>"]]
|
||||
*
|
||||
* The JSON string contains nested array: inner[4][0][1] = ["text chunks"].
|
||||
* We concatenate text from every wrb.fr line because Gemini can split one
|
||||
* assistant answer across multiple StreamGenerate chunks.
|
||||
* Each wrb.fr line is a CUMULATIVE snapshot of the whole answer generated so
|
||||
* far (not an independent delta), so we keep only the text from the LAST
|
||||
* frame that yields non-empty text instead of concatenating every frame —
|
||||
* concatenating would reproduce the same growing text with each snapshot
|
||||
* (see #7163).
|
||||
*/
|
||||
export function parseStreamResponse(raw: string): string {
|
||||
const lines = raw.split("\n");
|
||||
const textChunks: string[] = [];
|
||||
let lastText = "";
|
||||
|
||||
for (const rawLine of lines) {
|
||||
const line = rawLine.trim();
|
||||
@@ -133,12 +136,12 @@ export function parseStreamResponse(raw: string): string {
|
||||
const responseArray = inner?.[4]?.[0]?.[1];
|
||||
if (!Array.isArray(responseArray)) continue;
|
||||
const text = responseArray.filter((c: unknown) => typeof c === "string").join("");
|
||||
if (text) textChunks.push(text);
|
||||
if (text) lastText = text;
|
||||
} catch {
|
||||
// Skip unparseable lines
|
||||
}
|
||||
}
|
||||
return textChunks.join("");
|
||||
return lastText;
|
||||
}
|
||||
|
||||
function readCredentialString(value: unknown): string {
|
||||
|
||||
@@ -170,8 +170,26 @@ const executors = {
|
||||
|
||||
const defaultCache = new Map();
|
||||
|
||||
// #6699 — providers that exist ONLY as Cloud Agent task-API entries
|
||||
// (CLOUD_AGENT_PROVIDERS / staticModels "Available Models" catalog) and have no
|
||||
// chat-completions REGISTRY entry anywhere in open-sse/. Without this guard,
|
||||
// getExecutor() silently falls through to DefaultExecutor's
|
||||
// `PROVIDERS[provider] || PROVIDERS.openai` fallback, sending the user's real
|
||||
// provider key to OpenAI's endpoint (mislabeled as coming from the provider the
|
||||
// user actually selected). Starting with just "jules" (the reported case);
|
||||
// "devin" and "codex-cloud" share the same structural gap and are left for a
|
||||
// follow-up once their own chat-routing behavior is confirmed.
|
||||
const CHAT_UNSUPPORTED_CLOUD_AGENT_PROVIDERS = new Set(["jules"]);
|
||||
|
||||
export function getExecutor(provider) {
|
||||
if (executors[provider]) return executors[provider];
|
||||
if (CHAT_UNSUPPORTED_CLOUD_AGENT_PROVIDERS.has(provider)) {
|
||||
const err = new Error(
|
||||
`Provider "${provider}" is a cloud-agent provider and does not support direct chat completions; use the Cloud Agents task API instead.`
|
||||
);
|
||||
(err as Error & { status?: number }).status = 400;
|
||||
throw err;
|
||||
}
|
||||
if (!defaultCache.has(provider)) defaultCache.set(provider, new DefaultExecutor(provider));
|
||||
return defaultCache.get(provider);
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ export interface AccountProxyConfig {
|
||||
port: number;
|
||||
username?: string;
|
||||
password?: string;
|
||||
relayAuth?: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface OpencodeAccountProxyConfig {
|
||||
port: number;
|
||||
username?: string;
|
||||
password?: string;
|
||||
relayAuth?: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -106,8 +106,13 @@ function buildPrompt(messages: Array<Record<string, unknown>>): string {
|
||||
/** Build the `hy_source=web; hy_user=...; hy_token=...` cookie from the pasted header. */
|
||||
function buildYuanbaoCookie(rawApiKey: string): { cookie: string; hasToken: boolean } {
|
||||
const raw = stripCookieInputPrefix(rawApiKey || "");
|
||||
const hyUser = extractCookieValue(raw, "hy_user");
|
||||
const hyToken = extractCookieValue(raw, "hy_token");
|
||||
// Guard the extractCookieValue bare-value fallback: for input that is a single
|
||||
// FOREIGN pair (e.g. "some_other=abc") the helper returns the whole string, which
|
||||
// used to fool this validation into forwarding garbage upstream (the request only
|
||||
// failed when Tencent replied 401 — a live-network dependency). Yuanbao needs the
|
||||
// two distinct cookies, so only trust an extraction the input explicitly names.
|
||||
const hyUser = raw.includes("hy_user=") ? extractCookieValue(raw, "hy_user") : null;
|
||||
const hyToken = raw.includes("hy_token=") ? extractCookieValue(raw, "hy_token") : null;
|
||||
|
||||
if (hyUser && hyToken) {
|
||||
return { cookie: `hy_source=web; hy_user=${hyUser}; hy_token=${hyToken}`, hasToken: true };
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
isSubscriptionQuotaText,
|
||||
buildSubscriptionQuotaFallback,
|
||||
buildWeeklyQuotaFallback,
|
||||
buildSessionQuotaFallback,
|
||||
} from "./quotaTextCooldowns.ts";
|
||||
import { parseDayGranularityResetMs, shouldPreserveQuotaSignals } from "./quotaResetParsing.ts";
|
||||
|
||||
@@ -1454,6 +1455,12 @@ export function checkFallbackError(
|
||||
}
|
||||
const weeklyResult = buildWeeklyQuotaFallback(errorStr);
|
||||
if (weeklyResult) return weeklyResult;
|
||||
// Issue #7071 (session usage cap) is the same sibling gap as #3709 above —
|
||||
// runs UNCONDITIONALLY for the same reason: apikey-category providers
|
||||
// like ollama-cloud are excluded from the oauth-only shouldUseQuotaSignal
|
||||
// gate.
|
||||
const sessionResult = buildSessionQuotaFallback(errorStr);
|
||||
if (sessionResult) return sessionResult;
|
||||
|
||||
const quotaResetHintMs = parseRetryFromErrorText(errorStr);
|
||||
if (
|
||||
|
||||
@@ -344,7 +344,19 @@ function toHeaders(raw: Record<string, string[]>): Headers {
|
||||
// to a file path, terminating when the upstream sends `streamOutputEOFSymbol`.
|
||||
// We tail the file from a worker and surface the bytes as a ReadableStream.
|
||||
|
||||
async function tlsFetchStreaming(
|
||||
// Cap for the bounded fallback read of a non-SSE error body straight from the
|
||||
// streaming temp file (mirrors the 2048-byte cap executors/claude-web.ts
|
||||
// already applies when reading error bodies) — avoids buffering an unbounded
|
||||
// error page into memory. See #7134.
|
||||
const MAX_ERROR_BODY_BYTES = 16 * 1024;
|
||||
|
||||
/**
|
||||
* Exported for tests (issue #7134): allows injecting a fake `client` so the
|
||||
* non-SSE error-body fallback path can be exercised without
|
||||
* `--experimental-test-module-mocks`, matching the DI pattern already used
|
||||
* by `__setTlsFetchOverrideForTesting` for the outer `tlsFetchClaude`.
|
||||
*/
|
||||
export async function tlsFetchStreaming(
|
||||
client: { request: (url: string, opts: Record<string, unknown>) => Promise<TlsResponseLike> },
|
||||
url: string,
|
||||
requestOptions: Record<string, unknown>,
|
||||
@@ -417,11 +429,22 @@ async function tlsFetchStreaming(
|
||||
const r = await requestPromise.catch(
|
||||
(e) => ({ status: 502, headers: {}, body: String(e) }) as TlsResponseLike
|
||||
);
|
||||
// tls-client-node's `streamOutputPath` mode writes the response body to
|
||||
// the temp file chunk-by-chunk and does NOT also populate the resolved
|
||||
// response's in-memory `body` field (confirmed against
|
||||
// node_modules/tls-client-node/dist/response.js) — so for every non-SSE,
|
||||
// non-2xx claude-web response (400/403/429/500 with a real JSON/HTML
|
||||
// error), `r.body` is empty even though the real bytes are sitting in
|
||||
// `path` (we just peeked them above). Prefer `r.body` when it IS
|
||||
// populated (some native-client modes do fill it in); otherwise fall
|
||||
// back to a bounded read of the temp file so the real upstream error
|
||||
// detail reaches the caller instead of being silently discarded. #7134
|
||||
const text = r.body || (await readFirstBytes(path, MAX_ERROR_BODY_BYTES).catch(() => ""));
|
||||
await cleanupTempPath(path);
|
||||
return {
|
||||
status: r.status,
|
||||
headers: toHeaders(r.headers),
|
||||
text: r.body,
|
||||
text,
|
||||
body: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,8 +13,13 @@ type UsageQuota = {
|
||||
currency?: string;
|
||||
};
|
||||
|
||||
const OPENCODE_GO_QUOTA_URL =
|
||||
process.env.OMNIROUTE_OPENCODE_GO_QUOTA_URL ?? "https://api.z.ai/api/monitor/usage/quota/limit";
|
||||
// OpenCode Go does not expose a public quota API. There is no working
|
||||
// opencode.ai endpoint to default to (see #7022) — the quota-by-API-key path
|
||||
// below is opt-in only and activates exclusively when the operator sets
|
||||
// OMNIROUTE_OPENCODE_GO_QUOTA_URL explicitly. Never hardcode a third-party
|
||||
// host here (a previous default silently sent the user's API key to an
|
||||
// unrelated Z.AI endpoint).
|
||||
const OPENCODE_GO_QUOTA_URL = process.env.OMNIROUTE_OPENCODE_GO_QUOTA_URL?.trim() || "";
|
||||
const OPENCODE_GO_DASHBOARD_BASE_URL =
|
||||
process.env.OMNIROUTE_OPENCODE_GO_DASHBOARD_URL ?? "https://opencode.ai/workspace";
|
||||
const OPENCODE_GO_QUOTA_TOTALS = { session: 12, weekly: 30, mcp_monthly: 60 } as const;
|
||||
@@ -335,6 +340,15 @@ export async function getOpenCodeGoUsage(apiKey: string, providerSpecificData?:
|
||||
};
|
||||
}
|
||||
|
||||
if (!OPENCODE_GO_QUOTA_URL) {
|
||||
return {
|
||||
message:
|
||||
"OpenCode Go does not expose a public quota API. " +
|
||||
"Set OPENCODE_GO_WORKSPACE_ID and OPENCODE_GO_AUTH_COOKIE to enable dashboard quota scraping, " +
|
||||
"or set OMNIROUTE_OPENCODE_GO_QUOTA_URL to opt in to an explicit quota endpoint.",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(OPENCODE_GO_QUOTA_URL, {
|
||||
headers: {
|
||||
@@ -348,7 +362,8 @@ export async function getOpenCodeGoUsage(apiKey: string, providerSpecificData?:
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
return {
|
||||
message:
|
||||
"OpenCode Go API key is valid for chat/models but cannot read quota from the Z.AI quota API. " +
|
||||
"OpenCode Go API key is valid for chat/models but cannot read quota from the configured " +
|
||||
"OMNIROUTE_OPENCODE_GO_QUOTA_URL endpoint. " +
|
||||
"Set OPENCODE_GO_WORKSPACE_ID and OPENCODE_GO_AUTH_COOKIE to enable dashboard quota scraping.",
|
||||
};
|
||||
}
|
||||
@@ -374,7 +389,8 @@ export async function getOpenCodeGoUsage(apiKey: string, providerSpecificData?:
|
||||
) {
|
||||
return {
|
||||
message:
|
||||
"OpenCode Go API key is valid for chat/models but cannot read quota from the Z.AI quota API. " +
|
||||
"OpenCode Go API key is valid for chat/models but cannot read quota from the configured " +
|
||||
"OMNIROUTE_OPENCODE_GO_QUOTA_URL endpoint. " +
|
||||
"Set OPENCODE_GO_WORKSPACE_ID and OPENCODE_GO_AUTH_COOKIE to enable dashboard quota scraping.",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -103,3 +103,37 @@ export function buildWeeklyQuotaFallback(errorStr: string): QuotaTextFallback |
|
||||
reason: RateLimitReason.QUOTA_EXHAUSTED,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Issue #7071 — Ollama Cloud 5-hour SESSION usage cap ───────────────────
|
||||
//
|
||||
// Ollama Cloud also enforces a rolling 5-hour "session" usage cap, sibling to
|
||||
// the weekly cap above (#3709/#6638). On cap the upstream returns 429 with a
|
||||
// body like "you (<account>) have reached your session usage limit". Same
|
||||
// root cause as the weekly gap: neither the generic subscription-quota-text
|
||||
// classifier nor the weekly one recognize "session" wording, so the account
|
||||
// fell through to the generic 429 backoff and got retried within the same
|
||||
// 5-hour window instead of cooling down for it — combo/LKGP routing cycled
|
||||
// back to the "exhausted" account instead of advancing to the next one.
|
||||
//
|
||||
// Patterns are scoped to "session ... usage limit" / "session limit reached"
|
||||
// / "reached your session ... usage limit" phrasing (not a bare "session"
|
||||
// match) so unrelated "session expired"/"session token invalid" auth errors
|
||||
// from other providers are not misclassified as quota-exhausted.
|
||||
const SESSION_QUOTA_COOLDOWN_MS = 5 * 60 * 60 * 1000; // 5 hours
|
||||
|
||||
export function isSessionUsageLimitText(lower: string): boolean {
|
||||
return (
|
||||
lower.includes("session usage limit") ||
|
||||
lower.includes("session limit reached") ||
|
||||
(lower.includes("reached your session") && lower.includes("usage limit"))
|
||||
);
|
||||
}
|
||||
|
||||
export function buildSessionQuotaFallback(errorStr: string): QuotaTextFallback | null {
|
||||
if (!isSessionUsageLimitText(errorStr.toLowerCase())) return null;
|
||||
return {
|
||||
shouldFallback: true,
|
||||
cooldownMs: SESSION_QUOTA_COOLDOWN_MS,
|
||||
reason: RateLimitReason.QUOTA_EXHAUSTED,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
stripEmptyOptionalToolArgs,
|
||||
normalizeOutputIndex,
|
||||
normalizeUpstreamFailure,
|
||||
extractResponsesReasoningSummaryText,
|
||||
getVisibleResponsesReasoningSummaryText,
|
||||
} from "./openai-responses/pureHelpers.ts";
|
||||
import { createEventEmitter } from "./openai-responses/eventEmitter.ts";
|
||||
|
||||
@@ -1070,7 +1070,11 @@ function openaiResponsesToOpenAIResponseStream(chunk, state) {
|
||||
!(state.reasoningItemsWithDelta instanceof Set && state.reasoningItemsWithDelta.size > 0);
|
||||
if (emittedForItem || emittedWithoutItemId) return null;
|
||||
|
||||
const summaryText = extractResponsesReasoningSummaryText(item);
|
||||
// #7095/#7176 reconciliation: computed WITHOUT mutating `item`, so an
|
||||
// encrypted-only reasoning item (and its `encrypted_content`) is never
|
||||
// rewritten with a fabricated `summary` — the placeholder only feeds this
|
||||
// synthetic client-facing delta chunk.
|
||||
const summaryText = getVisibleResponsesReasoningSummaryText(item);
|
||||
if (!summaryText) return null;
|
||||
return buildResponsesReasoningDeltaChunk(state, summaryText);
|
||||
}
|
||||
|
||||
@@ -163,3 +163,28 @@ export function extractResponsesReasoningSummaryText(item) {
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
// #7095/#7176 — when Codex exposes a reasoning item only as encrypted private
|
||||
// reasoning (no plaintext summary), chat clients would otherwise see nothing in
|
||||
// their thinking panel. Reconciles two goals that used to be in tension:
|
||||
// - #7095 wants a visible placeholder in the chat client.
|
||||
// - #7176 wants the upstream response item left untouched, so `encrypted_content`
|
||||
// (needed by Codex for subsequent requests) is never overwritten by a
|
||||
// fabricated `summary`.
|
||||
// This function computes the placeholder text WITHOUT mutating `item` — callers
|
||||
// use the returned text for synthetic client-facing events only.
|
||||
const ENCRYPTED_REASONING_PLACEHOLDER =
|
||||
"Codex is reasoning, but the upstream Responses API exposed this reasoning block only as encrypted private reasoning. OmniRoute cannot recover the plaintext.";
|
||||
|
||||
export function getVisibleResponsesReasoningSummaryText(item) {
|
||||
const existingSummary = extractResponsesReasoningSummaryText(item);
|
||||
if (existingSummary) return existingSummary;
|
||||
|
||||
const hasEncryptedReasoning =
|
||||
item &&
|
||||
item.type === "reasoning" &&
|
||||
typeof item.encrypted_content === "string" &&
|
||||
item.encrypted_content.length > 0;
|
||||
|
||||
return hasEncryptedReasoning ? ENCRYPTED_REASONING_PLACEHOLDER : "";
|
||||
}
|
||||
|
||||
@@ -145,6 +145,22 @@ function clampDiagStr(v: unknown, max = 128): string {
|
||||
return typeof v === "string" ? v.slice(0, max).replace(/[\r\n]+/g, " ") : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP header values must be Latin1/ByteString (undici throws a TypeError
|
||||
* otherwise — see #6612). Replace any codepoint outside the Latin1 range
|
||||
* (0-255) with "?" so header construction never throws. Only used for the
|
||||
* literal header value; the JSON body keeps the original, unsanitized
|
||||
* readable text via `sanitizeComboDiagnostics`.
|
||||
*/
|
||||
function toHeaderSafeAscii(v: string): string {
|
||||
let out = "";
|
||||
for (let i = 0; i < v.length; i++) {
|
||||
const code = v.charCodeAt(i);
|
||||
out += code > 255 ? "?" : v[i];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whitelist projection — guarantees only id/reason string primitives + integer
|
||||
* counts can escape, regardless of what the caller assembled. This is the secret
|
||||
@@ -186,10 +202,12 @@ export function errorResponseWithComboDiagnostics(
|
||||
if (opts.code) body.error.code = opts.code;
|
||||
if (opts.type) body.error.type = opts.type;
|
||||
body.diagnostics = safe;
|
||||
const excludedHeader = safe.excluded
|
||||
.map((e) => `${e.provider}${e.model ? `/${e.model}` : ""}:${e.reason}`)
|
||||
.join(",")
|
||||
.slice(0, 900);
|
||||
const excludedHeader = toHeaderSafeAscii(
|
||||
safe.excluded
|
||||
.map((e) => `${e.provider}${e.model ? `/${e.model}` : ""}:${e.reason}`)
|
||||
.join(",")
|
||||
.slice(0, 900)
|
||||
);
|
||||
return new Response(JSON.stringify(body), {
|
||||
status: statusCode,
|
||||
headers: {
|
||||
@@ -197,7 +215,7 @@ export function errorResponseWithComboDiagnostics(
|
||||
"x-omniroute-combo-pool-size": String(safe.poolSize),
|
||||
"x-omniroute-combo-attempted": String(safe.attempted),
|
||||
"x-omniroute-combo-excluded": excludedHeader,
|
||||
"x-omniroute-combo-terminal-reason": safe.terminalReason.slice(0, 200),
|
||||
"x-omniroute-combo-terminal-reason": toHeaderSafeAscii(safe.terminalReason.slice(0, 200)),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ export type PassthroughTailProcessorContext = {
|
||||
appendPassthroughReasoning: (value: string) => void;
|
||||
getResponsesReasoningKey: (payload: Record<string, unknown>) => string | null;
|
||||
markResponsesReasoningSummarySeen: (key: string) => void;
|
||||
ensureVisibleResponsesReasoningSummary: (payload: Record<string, unknown>) => boolean;
|
||||
emitSyntheticResponsesReasoningSummary: (payload: Record<string, unknown>) => void;
|
||||
passthroughResponsesOutputItems: unknown[];
|
||||
passthroughResponsesPendingFunctionCalls: Map<string, JsonRecord>;
|
||||
@@ -136,12 +135,8 @@ function handleResponsesTailPayload(
|
||||
}
|
||||
}
|
||||
if (parsed.type === "response.output_item.done" && parsed.item) {
|
||||
const reasoningSummaryInjected = context.ensureVisibleResponsesReasoningSummary(parsed);
|
||||
context.emitSyntheticResponsesReasoningSummary(parsed);
|
||||
pushUniqueResponsesOutputItems(context.passthroughResponsesOutputItems, [parsed.item]);
|
||||
if (reasoningSummaryInjected) {
|
||||
output = `data: ${JSON.stringify(parsed)}\n\n`;
|
||||
}
|
||||
const item = asRecord(parsed.item);
|
||||
if (item.type === "function_call") {
|
||||
const pendingKey = getFunctionCallPendingKey(item);
|
||||
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
stripResponsesLifecycleEcho,
|
||||
} from "./responsesStreamHelpers.ts";
|
||||
import { processBufferedPassthroughLine } from "./passthroughTailProcessor.ts";
|
||||
import { getVisibleResponsesReasoningSummaryText } from "../translator/response/openai-responses/pureHelpers.ts";
|
||||
import {
|
||||
getAnyReasoningValue,
|
||||
getReadableReasoningValue,
|
||||
@@ -1006,49 +1007,6 @@ export function createSSEStream(options: StreamOptions = {}) {
|
||||
return responseId !== null && outputIndex !== null ? `${responseId}:${outputIndex}` : null;
|
||||
};
|
||||
|
||||
const getResponsesReasoningSummaryText = (item: Record<string, unknown>): string => {
|
||||
return Array.isArray(item.summary)
|
||||
? item.summary
|
||||
.map((part) => {
|
||||
if (!part || typeof part !== "object" || Array.isArray(part)) {
|
||||
return "";
|
||||
}
|
||||
return typeof (part as Record<string, unknown>).text === "string"
|
||||
? ((part as Record<string, unknown>).text as string)
|
||||
: "";
|
||||
})
|
||||
.join("")
|
||||
: "";
|
||||
};
|
||||
|
||||
const ensureVisibleResponsesReasoningSummary = (payload: Record<string, unknown>): boolean => {
|
||||
const item =
|
||||
payload.item && typeof payload.item === "object" && !Array.isArray(payload.item)
|
||||
? (payload.item as Record<string, unknown>)
|
||||
: null;
|
||||
if (!item || item.type !== "reasoning") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (getResponsesReasoningSummaryText(item)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasEncryptedReasoning =
|
||||
typeof item.encrypted_content === "string" && item.encrypted_content.length > 0;
|
||||
if (!hasEncryptedReasoning) {
|
||||
return false;
|
||||
}
|
||||
|
||||
item.summary = [
|
||||
{
|
||||
type: "summary_text",
|
||||
text: "Codex is reasoning, but the upstream Responses API exposed this reasoning block only as encrypted state. OmniRoute cannot recover the private reasoning text.",
|
||||
},
|
||||
];
|
||||
return true;
|
||||
};
|
||||
|
||||
const emitSyntheticResponsesReasoningSummary = (
|
||||
controller: TransformStreamDefaultController,
|
||||
payload: Record<string, unknown>
|
||||
@@ -1061,8 +1019,10 @@ export function createSSEStream(options: StreamOptions = {}) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensureVisibleResponsesReasoningSummary(payload);
|
||||
const visibleSummary = getResponsesReasoningSummaryText(item);
|
||||
// #7095/#7176 reconciliation: compute the visible placeholder WITHOUT
|
||||
// mutating `item` — the encrypted reasoning item (and its `encrypted_content`,
|
||||
// required by Codex for subsequent requests) is forwarded to the client intact.
|
||||
const visibleSummary = getVisibleResponsesReasoningSummaryText(item);
|
||||
|
||||
if (!visibleSummary) {
|
||||
return;
|
||||
@@ -1485,13 +1445,8 @@ export function createSSEStream(options: StreamOptions = {}) {
|
||||
// response.completed snapshot can be backfilled when upstream
|
||||
// returns an empty `output` (happens with store: false).
|
||||
if (parsed.type === "response.output_item.done" && parsed.item) {
|
||||
const reasoningSummaryInjected = ensureVisibleResponsesReasoningSummary(parsed);
|
||||
emitSyntheticResponsesReasoningSummary(controller, parsed);
|
||||
pushUniqueResponsesOutputItems(passthroughResponsesOutputItems, [parsed.item]);
|
||||
if (reasoningSummaryInjected) {
|
||||
output = `data: ${JSON.stringify(parsed)}\n\n`;
|
||||
injectedUsage = true;
|
||||
}
|
||||
if (parsed.item?.type === "function_call") {
|
||||
const pendingKey =
|
||||
typeof parsed.item.id === "string"
|
||||
@@ -2181,7 +2136,6 @@ export function createSSEStream(options: StreamOptions = {}) {
|
||||
markResponsesReasoningSummarySeen: (key: string) => {
|
||||
passthroughResponsesReasoningSummarySeen.add(key);
|
||||
},
|
||||
ensureVisibleResponsesReasoningSummary,
|
||||
emitSyntheticResponsesReasoningSummary: (payload: Record<string, unknown>) =>
|
||||
emitSyntheticResponsesReasoningSummary(controller, payload),
|
||||
passthroughResponsesOutputItems,
|
||||
|
||||
9819
package-lock.json
generated
9819
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -87,6 +87,7 @@
|
||||
"build:release": "rm -rf .build dist && OMNIROUTE_BUILD_SHA=$(git rev-parse --short HEAD) npm run build && npm run build:cli && node scripts/build/write-build-sha.mjs",
|
||||
"build:native:tproxy": "cd src/mitm/tproxy/native && npx --yes node-gyp rebuild",
|
||||
"start": "node scripts/dev/run-next.mjs start",
|
||||
"homolog": "node scripts/homolog/run.mjs",
|
||||
"lint": "eslint . --cache --cache-location .eslintcache --suppressions-location config/quality/eslint-suppressions.json",
|
||||
"lint:json": "node scripts/quality/run-eslint-json.mjs",
|
||||
"lint:md": "npx --yes markdownlint-cli2 \"docs/**/*.md\" \"*.md\" \"!docs/i18n\" \"!docs/research\"",
|
||||
@@ -129,6 +130,7 @@
|
||||
"i18n:check-ui-coverage": "node scripts/i18n/check-ui-keys-coverage.mjs",
|
||||
"check:node-runtime": "node --import tsx scripts/check/check-supported-node-runtime.ts",
|
||||
"check:pack-artifact": "node --import tsx scripts/build/validate-pack-artifact.ts",
|
||||
"check:pack-boot": "node scripts/check/check-pack-boot.mjs",
|
||||
"check:pack-policy": "node --import tsx scripts/build/validate-pack-artifact.ts --policy-only",
|
||||
"check:cli-i18n": "node scripts/check/check-cli-i18n.mjs",
|
||||
"check:openapi-coverage": "node scripts/check/check-openapi-coverage.mjs",
|
||||
@@ -183,6 +185,7 @@
|
||||
"audit:electron": "npm --prefix electron audit --audit-level=critical && (npm --prefix electron audit --audit-level=high || echo '::warning::electron high-severity advisories present (non-blocking)')",
|
||||
"typecheck:core": "tsc --pretty false -p tsconfig.typecheck-core.json",
|
||||
"typecheck:noimplicit:core": "tsc --pretty false -p tsconfig.typecheck-noimplicit-core.json",
|
||||
"check:dashboard-typecheck": "node scripts/check/check-dashboard-typecheck.mjs",
|
||||
"backfill-aggregation": "node --import tsx src/scripts/backfillAggregation.ts",
|
||||
"env:sync": "node scripts/dev/sync-env.mjs",
|
||||
"test:integration": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 tests/integration/*.test.ts \"tests/integration/combo-matrix/*.test.ts\"",
|
||||
@@ -328,21 +331,26 @@
|
||||
"c8": "^11.0.0",
|
||||
"concurrently": "^10.0.3",
|
||||
"cross-env": "^10.1.0",
|
||||
"ctrf": "^0.2.1",
|
||||
"dpdm": "^4.2.0",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-config-next": "16.2.10",
|
||||
"eslint-plugin-sonarjs": "^4.1.0",
|
||||
"fast-check": "^4.8.0",
|
||||
"glob": "^13.0.6",
|
||||
"httpyac": "^6.16.7",
|
||||
"husky": "^9.1.7",
|
||||
"jscpd": "^4.2.5",
|
||||
"jsdom": "^29.1.1",
|
||||
"junit-to-ctrf": "^0.0.14",
|
||||
"knip": "^6.18.0",
|
||||
"license-checker-rseidelsohn": "^5.0.1",
|
||||
"lint-staged": "^17.0.8",
|
||||
"lockfile-lint": "^5.0.0",
|
||||
"node-loader": "^2.1.0",
|
||||
"playwright-ctrf-json-reporter": "^0.0.29",
|
||||
"prettier": "^3.8.3",
|
||||
"promptfoo": "^0.121.18",
|
||||
"size-limit": "^12.1.0",
|
||||
"tailwindcss": "^4.3.0",
|
||||
"type-coverage": "^2.29.7",
|
||||
|
||||
@@ -135,6 +135,11 @@ const EXTRA_MODULE_ENTRIES = [
|
||||
src: ["scripts", "dev", "peer-stamp.mjs"],
|
||||
dest: ["peer-stamp.mjs"],
|
||||
},
|
||||
{
|
||||
label: "main-server timeouts (server-ws.mjs dependency, #7003/#7065-class)",
|
||||
src: ["scripts", "dev", "main-server-timeouts.mjs"],
|
||||
dest: ["main-server-timeouts.mjs"],
|
||||
},
|
||||
{
|
||||
label: "HTTP method guard (server-ws.mjs dependency)",
|
||||
src: ["scripts", "dev", "http-method-guard.cjs"],
|
||||
|
||||
@@ -46,6 +46,7 @@ export const APP_STAGING_ALLOWED_EXACT_PATHS: string[] = [
|
||||
"open-sse/services/compression/engines/llmlingua/onnxWorker.js",
|
||||
"package.json",
|
||||
"peer-stamp.mjs",
|
||||
"main-server-timeouts.mjs",
|
||||
"responses-ws-proxy.mjs",
|
||||
"scripts/dev/sync-env.mjs",
|
||||
"scripts/dev/tls-options.mjs",
|
||||
@@ -152,6 +153,7 @@ export const PACK_ARTIFACT_REQUIRED_PATHS: string[] = [
|
||||
"dist/server-ws.mjs",
|
||||
"dist/responses-ws-proxy.mjs",
|
||||
"dist/peer-stamp.mjs",
|
||||
"dist/main-server-timeouts.mjs",
|
||||
"dist/http-method-guard.cjs",
|
||||
// #5452: regression guard — make check:pack-artifact fail loudly if the TLS
|
||||
// opt-in sidecar (imported by dist/server-ws.mjs) ever vanishes from the tarball.
|
||||
@@ -160,6 +162,12 @@ export const PACK_ARTIFACT_REQUIRED_PATHS: string[] = [
|
||||
"dist/head-response-guard.cjs",
|
||||
"dist/webdav-handler.mjs",
|
||||
"bin/cli/program.mjs",
|
||||
// Direct imports of bin/omniroute.mjs — bin/cli/ is only an allowlist PREFIX, so a
|
||||
// file vanishing from the tarball never fails the unexpected-paths check; only these
|
||||
// required entries make its absence loud (#7065 class; derived + enforced by
|
||||
// tests/unit/pack-artifact-entrypoint-closures.test.ts).
|
||||
"bin/cli/data-dir.mjs",
|
||||
"bin/cli/utils/storageKeyProvision.mjs",
|
||||
"bin/mcp-server.mjs",
|
||||
"bin/nodeRuntimeSupport.mjs",
|
||||
"bin/omniroute.mjs",
|
||||
|
||||
177
scripts/check/check-dashboard-typecheck.mjs
Normal file
177
scripts/check/check-dashboard-typecheck.mjs
Normal file
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env node
|
||||
// scripts/check/check-dashboard-typecheck.mjs
|
||||
// Dashboard-scoped typecheck gate (#7033).
|
||||
//
|
||||
// `typecheck:core` (the only blocking CI typecheck gate) runs against a curated
|
||||
// 27-file `"files"` allowlist in tsconfig.typecheck-core.json — none of it lives
|
||||
// under `src/app/(dashboard)`, and `next.config.mjs` sets
|
||||
// `typescript.ignoreBuildErrors: true`, so `next build` never type-checks either.
|
||||
// Net effect: orphaned-identifier regressions in dashboard TSX (deleted `useState`
|
||||
// decls with live usages left behind) are invisible to both CI type-check paths
|
||||
// and only surface at runtime — exactly what happened in #6625/#6909.
|
||||
//
|
||||
// This gate runs `tsc` scoped to `src/app/(dashboard)/**/*.{ts,tsx}` via
|
||||
// tsconfig.typecheck-dashboard.json and diffs the result against a frozen
|
||||
// per-file/per-TS-code count baseline (config/quality/dashboard-typecheck-baseline.json),
|
||||
// following this repo's stale-enforcement allowlist convention (see
|
||||
// scripts/check/check-known-symbols.ts). A live count that EXCEEDS the baselined
|
||||
// count for a given (file, TS code) pair is a regression and fails the gate; a
|
||||
// live count that is lower is an improvement and does not fail (use --update to
|
||||
// ratchet the baseline down).
|
||||
//
|
||||
// Run:
|
||||
// node scripts/check/check-dashboard-typecheck.mjs
|
||||
// node scripts/check/check-dashboard-typecheck.mjs --update # re-freeze baseline
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const ROOT = process.cwd();
|
||||
const TSCONFIG = path.join(ROOT, "tsconfig.typecheck-dashboard.json");
|
||||
const BASELINE_PATH = path.join(ROOT, "config/quality/dashboard-typecheck-baseline.json");
|
||||
const UPDATE = process.argv.includes("--update");
|
||||
|
||||
// Matches tsc --pretty false output lines, e.g.:
|
||||
// src/app/(dashboard)/dashboard/foo.tsx(12,7): error TS2304: Cannot find name 'bar'.
|
||||
const TSC_ERROR_LINE = /^(.+?)\((\d+),(\d+)\): error (TS\d+):/;
|
||||
|
||||
/**
|
||||
* Parses raw `tsc --pretty false` stdout into a nested count map:
|
||||
* { "<relative file path>": { "<TS code>": <count> } }
|
||||
*
|
||||
* Pure/exported for unit testing against synthetic tsc output — no child
|
||||
* process involved here.
|
||||
*/
|
||||
export function parseTscOutput(raw) {
|
||||
const counts = {};
|
||||
const lines = String(raw).split("\n");
|
||||
for (const line of lines) {
|
||||
const match = TSC_ERROR_LINE.exec(line);
|
||||
if (!match) continue;
|
||||
const [, file, , , code] = match;
|
||||
if (!counts[file]) counts[file] = {};
|
||||
counts[file][code] = (counts[file][code] || 0) + 1;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares live (file, TS code) error counts against a frozen baseline.
|
||||
* Returns `{ regressions, improvements }`:
|
||||
* - regressions: entries where live count > baselined count (or the pair is
|
||||
* entirely new/unbaselined) — these fail the gate.
|
||||
* - improvements: entries where live count < baselined count — informational,
|
||||
* do not fail (use --update to ratchet the baseline down).
|
||||
*
|
||||
* Exported for unit testing.
|
||||
*/
|
||||
export function diffAgainstBaseline(live, baseline) {
|
||||
const regressions = [];
|
||||
const improvements = [];
|
||||
|
||||
for (const [file, codes] of Object.entries(live)) {
|
||||
for (const [code, liveCount] of Object.entries(codes)) {
|
||||
const baselineCount = (baseline[file] && baseline[file][code]) || 0;
|
||||
if (liveCount > baselineCount) {
|
||||
regressions.push({ file, code, liveCount, baselineCount });
|
||||
} else if (liveCount < baselineCount) {
|
||||
improvements.push({ file, code, liveCount, baselineCount });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [file, codes] of Object.entries(baseline)) {
|
||||
for (const [code, baselineCount] of Object.entries(codes)) {
|
||||
const liveCount = (live[file] && live[file][code]) || 0;
|
||||
if (liveCount === 0 && baselineCount > 0) {
|
||||
improvements.push({ file, code, liveCount: 0, baselineCount });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { regressions, improvements };
|
||||
}
|
||||
|
||||
function runTsc() {
|
||||
try {
|
||||
const stdout = execFileSync(
|
||||
process.platform === "win32" ? "npx.cmd" : "npx",
|
||||
["tsc", "--pretty", "false", "--noEmit", "-p", TSCONFIG],
|
||||
{ encoding: "utf8", maxBuffer: 64 * 1024 * 1024, cwd: ROOT }
|
||||
);
|
||||
return stdout;
|
||||
} catch (err) {
|
||||
// tsc exits non-zero when there are type errors — stdout still has the report.
|
||||
if (err.stdout) return String(err.stdout);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function loadBaseline() {
|
||||
if (!fs.existsSync(BASELINE_PATH)) return {};
|
||||
return JSON.parse(fs.readFileSync(BASELINE_PATH, "utf8"));
|
||||
}
|
||||
|
||||
function writeBaseline(counts) {
|
||||
fs.writeFileSync(BASELINE_PATH, JSON.stringify(counts, null, 2) + "\n");
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (!fs.existsSync(TSCONFIG)) {
|
||||
process.stderr.write(`[dashboard-typecheck] FAIL — tsconfig not found at ${TSCONFIG}\n`);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
console.log("[dashboard-typecheck] Running tsc scoped to src/app/(dashboard)/**…");
|
||||
const stdout = runTsc();
|
||||
const live = parseTscOutput(stdout);
|
||||
const baseline = loadBaseline();
|
||||
const { regressions, improvements } = diffAgainstBaseline(live, baseline);
|
||||
|
||||
const liveErrorCount = Object.values(live).reduce(
|
||||
(sum, codes) => sum + Object.values(codes).reduce((s, c) => s + c, 0),
|
||||
0
|
||||
);
|
||||
console.log(`dashboardTypecheckErrors=${liveErrorCount}`);
|
||||
|
||||
if (UPDATE) {
|
||||
writeBaseline(live);
|
||||
console.log(`[dashboard-typecheck] baseline rewritten (${liveErrorCount} errors frozen).`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (improvements.length > 0) {
|
||||
console.log(
|
||||
`[dashboard-typecheck] ${improvements.length} baselined error(s) no longer present ` +
|
||||
`— run 'node scripts/check/check-dashboard-typecheck.mjs --update' to ratchet the baseline down:\n` +
|
||||
improvements
|
||||
.map((i) => ` - ${i.file} ${i.code} (baseline ${i.baselineCount} -> live ${i.liveCount})`)
|
||||
.join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
if (regressions.length > 0) {
|
||||
process.stderr.write(
|
||||
`[dashboard-typecheck] FAIL — ${regressions.length} new/regressed TypeScript error(s) ` +
|
||||
`under src/app/(dashboard)/ not covered by the frozen baseline:\n` +
|
||||
regressions
|
||||
.map((r) => ` ✗ ${r.file} ${r.code} (baseline ${r.baselineCount}, live ${r.liveCount})`)
|
||||
.join("\n") +
|
||||
`\n\nIf this is a genuine new dashboard TSX bug (e.g. an orphaned identifier), fix it.\n` +
|
||||
`If it's pre-existing type looseness you're intentionally not fixing in this PR,\n` +
|
||||
`do NOT widen the baseline for new regressions — that defeats the gate.\n`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[dashboard-typecheck] OK — ${liveErrorCount} pre-existing error(s), all within frozen baseline.`
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] || "").href) {
|
||||
main();
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user