From 48b17ff2b7abec70d3833e2f8d4699601d192dae Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Fri, 7 Aug 2026 18:07:50 -0300 Subject: [PATCH] fix(build): remove misleading open-sse/package.json facade and add workspace typecheck gate (#8781) Co-authored-by: diegosouzapw --- .github/workflows/quality.yml | 4 + changelog.d/fixes/8781-fix.plan.md | 1 + .../quality/open-sse-typecheck-baseline.json | 176 ++++++++++++++++++ open-sse/package.json | 15 +- package.json | 1 + scripts/check/check-open-sse-typecheck.mjs | 174 +++++++++++++++++ 6 files changed, 358 insertions(+), 13 deletions(-) create mode 100644 changelog.d/fixes/8781-fix.plan.md create mode 100644 config/quality/open-sse-typecheck-baseline.json create mode 100644 scripts/check/check-open-sse-typecheck.mjs diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 7a167afcd0..76a116287e 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -271,6 +271,10 @@ jobs: # covered by typecheck:core's curated allowlist. See check-dashboard-typecheck.mjs. - name: Typecheck (dashboard) run: npm run check:dashboard-typecheck + # #8781: open-sse workspace typecheck gate — the workspace imports @/ which + # escapes to src/ via undeclared path aliases. See check-open-sse-typecheck.mjs. + - name: Typecheck (open-sse) + run: npm run check:open-sse-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 diff --git a/changelog.d/fixes/8781-fix.plan.md b/changelog.d/fixes/8781-fix.plan.md new file mode 100644 index 0000000000..ce761c1555 --- /dev/null +++ b/changelog.d/fixes/8781-fix.plan.md @@ -0,0 +1 @@ +- fix(build): remove misleading open-sse/package.json facade and add workspace typecheck gate (#8781) diff --git a/config/quality/open-sse-typecheck-baseline.json b/config/quality/open-sse-typecheck-baseline.json new file mode 100644 index 0000000000..dc91ce1890 --- /dev/null +++ b/config/quality/open-sse-typecheck-baseline.json @@ -0,0 +1,176 @@ +{ + "open-sse/executors/azure-openai.ts": { + "TS2345": 1 + }, + "open-sse/executors/chatgpt-web.ts": { + "TS2339": 1 + }, + "open-sse/executors/claude-web/stream.ts": { + "TS2322": 1, + "TS2345": 1 + }, + "open-sse/executors/copilot-web.ts": { + "TS2353": 1 + }, + "open-sse/executors/deepseek-web.ts": { + "TS2352": 1 + }, + "open-sse/executors/default.ts": { + "TS2352": 1 + }, + "open-sse/executors/duckduckgo-web.ts": { + "TS2345": 2 + }, + "open-sse/executors/duckduckgo-web/challenge.ts": { + "TS2304": 1 + }, + "open-sse/executors/edgeTts.ts": { + "TS2345": 1 + }, + "open-sse/executors/gemini-business.ts": { + "TS2339": 1 + }, + "open-sse/executors/ghe-copilot.ts": { + "TS2554": 1 + }, + "open-sse/executors/inner-ai.ts": { + "TS2352": 2 + }, + "open-sse/executors/theoldllm.ts": { + "TS2322": 1 + }, + "open-sse/executors/veoaifree-web.ts": { + "TS2322": 1 + }, + "open-sse/executors/windsurf.ts": { + "TS2322": 1 + }, + "open-sse/handlers/chatCore.ts": { + "TS2339": 30, + "TS2322": 1, + "TS2345": 11 + }, + "open-sse/handlers/chatCore/claudeUpstreamMessages.ts": { + "TS2345": 1 + }, + "open-sse/handlers/chatCore/clientUsageBuffer.ts": { + "TS2345": 1 + }, + "open-sse/handlers/chatCore/clineResponseEnvelope.ts": { + "TS2698": 1 + }, + "open-sse/handlers/chatCore/compressionAnalyticsWrite.ts": { + "TS2724": 1 + }, + "open-sse/handlers/chatCore/nonStreamingResponseHeaders.ts": { + "TS2322": 2 + }, + "open-sse/handlers/chatCore/sanitization.ts": { + "TS2339": 1, + "TS2537": 1 + }, + "open-sse/handlers/chatCore/semanticCacheStore.ts": { + "TS2345": 1 + }, + "open-sse/handlers/chatCore/streamingPipeline.ts": { + "TS2345": 2 + }, + "open-sse/handlers/chatCore/streamingSemanticCacheStore.ts": { + "TS2345": 1 + }, + "open-sse/handlers/chatCore/thinkingSignatureRecovery.ts": { + "TS2339": 2 + }, + "open-sse/handlers/imageGeneration.ts": { + "TS2554": 2 + }, + "open-sse/handlers/responsesHandler.ts": { + "TS2339": 1, + "TS2345": 1 + }, + "open-sse/handlers/sseParser.ts": { + "TS2322": 2 + }, + "open-sse/handlers/videoGeneration.ts": { + "TS2339": 2 + }, + "open-sse/mcp-server/tools/compressionTools.ts": { + "TS2339": 2 + }, + "open-sse/services/__tests__/specificityDetector.test.ts": { + "TS2353": 2 + }, + "open-sse/services/browserBackedChat.ts": { + "TS2322": 1, + "TS2794": 1 + }, + "open-sse/services/claudeAdaptiveThinking.ts": { + "TS2352": 2 + }, + "open-sse/services/comboManifestMetrics.ts": { + "TS2307": 1 + }, + "open-sse/services/compression/engines/ccr/index.ts": { + "TS2339": 1 + }, + "open-sse/services/payloadRules.ts": { + "TS2677": 1 + }, + "open-sse/services/tokenLimitCounter.ts": { + "TS2551": 1 + }, + "open-sse/transformer/responsesTransformer.ts": { + "TS2339": 1 + }, + "open-sse/utils/stream.ts": { + "TS2339": 7, + "TS2345": 1, + "TS2556": 1 + }, + "src/app/api/v1/_shared/mediaGenerationRoute.ts": { + "TS2339": 2 + }, + "src/app/api/v1/models/catalog.ts": { + "TS2345": 1 + }, + "src/app/api/v1/models/catalogVision.ts": { + "TS2322": 1 + }, + "src/app/api/v1/videos/generations/route.ts": { + "TS2322": 1, + "TS2345": 1 + }, + "src/lib/guardrails/visionBridge.ts": { + "TS2345": 1 + }, + "src/lib/providers/codexFastTier.ts": { + "TS2367": 1 + }, + "src/lib/skills/builtins.ts": { + "TS2322": 1 + }, + "src/lib/skills/injection.ts": { + "TS2339": 1 + }, + "src/lib/skills/webFetchExecution.ts": { + "TS2322": 1 + }, + "src/lib/streamingPiiTransform.ts": { + "TS2345": 1 + }, + "src/shared/providers/webSessionCredentials.ts": { + "TS2353": 1, + "TS2322": 1 + }, + "src/shared/validation/helpers.ts": { + "TS2339": 1 + }, + "src/sse/handlers/chat.ts": { + "TS2352": 1, + "TS2322": 2, + "TS2339": 1 + }, + "src/sse/services/model.ts": { + "TS2339": 4 + } +} diff --git a/open-sse/package.json b/open-sse/package.json index b2f90507e1..858e80d0c8 100644 --- a/open-sse/package.json +++ b/open-sse/package.json @@ -1,18 +1,7 @@ { "name": "@omniroute/open-sse", "version": "3.8.50", - "description": "Express SSE sidecar for OmniRoute — handles streaming, protocol translation, and provider orchestration", + "description": "OmniRoute streaming engine — handles provider dispatch, protocol translation, and SSE streaming", "type": "module", - "main": "index.js", - "types": "types.d.ts", - "private": true, - "exports": { - ".": "./index.js", - "./*": "./*" - }, - "dependencies": { - "@toon-format/toon": "^4.1.0", - "safe-regex": "^2.1.1", - "smol-toml": "1.7.1" - } + "private": true } diff --git a/package.json b/package.json index 954ee5871c..c703fbc90d 100644 --- a/package.json +++ b/package.json @@ -207,6 +207,7 @@ "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", + "check:open-sse-typecheck": "node scripts/check/check-open-sse-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\"", diff --git a/scripts/check/check-open-sse-typecheck.mjs b/scripts/check/check-open-sse-typecheck.mjs new file mode 100644 index 0000000000..d18c588538 --- /dev/null +++ b/scripts/check/check-open-sse-typecheck.mjs @@ -0,0 +1,174 @@ +#!/usr/bin/env node +// scripts/check/check-open-sse-typecheck.mjs +// open-sse workspace typecheck gate (#8781). +// +// The open-sse workspace declares path aliases (e.g. `@/*` → `../src/*`) in its own +// tsconfig.json, but those aliases are not resolvable by Node's bare module resolution — +// they only work because Next.js/Turbopack bundles the entire tree. Additionally, +// package.json historically declared `main`/`exports` entries that do not exist on disk. +// +// This gate runs `tsc -p open-sse/tsconfig.json` and diffs the result against a frozen +// per-file/per-TS-code count baseline (config/quality/open-sse-typecheck-baseline.json), +// following this repo's stale-enforcement allowlist convention. 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-open-sse-typecheck.mjs +// node scripts/check/check-open-sse-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, "open-sse", "tsconfig.json"); +const BASELINE_PATH = path.join(ROOT, "config/quality/open-sse-typecheck-baseline.json"); +const UPDATE = process.argv.includes("--update"); + +// Matches tsc --pretty false output lines, e.g.: +// src/app/api/v1/chat/route.ts(12,7): error TS2304: Cannot find name 'bar'. +// open-sse/handlers/chatCore.ts(45,3): error TS7053: Element implicitly has an 'any'... +const TSC_ERROR_LINE = /^(.+?)\((\d+),(\d+)\): error (TS\d+):/; + +/** + * Parses raw `tsc --pretty false` stdout into a nested count map: + * { "": { "": } } + * + * 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(`[open-sse-typecheck] FAIL — tsconfig not found at ${TSCONFIG}\n`); + process.exit(2); + } + + console.log("[open-sse-typecheck] Running tsc scoped to open-sse/ workspace…"); + 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(`openSseTypecheckErrors=${liveErrorCount}`); + + if (UPDATE) { + writeBaseline(live); + console.log(`[open-sse-typecheck] baseline rewritten (${liveErrorCount} errors frozen).`); + process.exit(0); + } + + if (improvements.length > 0) { + console.log( + `[open-sse-typecheck] ${improvements.length} baselined error(s) no longer present ` + + `— run 'node scripts/check/check-open-sse-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( + `[open-sse-typecheck] FAIL — ${regressions.length} new/regressed TypeScript error(s) ` + + `under open-sse/ workspace 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 open-sse type error (e.g. an undeclared @/ alias),\n` + + `fix it in the source, not in the baseline.\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( + `[open-sse-typecheck] OK — ${liveErrorCount} pre-existing error(s), all within frozen baseline.` + ); + process.exit(0); +} + +if (import.meta.url === pathToFileURL(process.argv[1] || "").href) { + main(); +}