From 028ed4ea7b3ef5a8dab43826e31c2bcc13a1817c Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Wed, 24 Jun 2026 23:51:07 -0300 Subject: [PATCH] =?UTF-8?q?fix(quality):=20resolve=20base-reds=20da=20rele?= =?UTF-8?q?ase=20=E2=80=94=20db-rules=20allowlist=20+=20task-aware=20route?= =?UTF-8?q?r=20precedence=20(#4973)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dois base-reds pré-existentes que reprovavam o CI da release v3.8.36 (Fast Quality Gates + Unit Tests fast-path), independentes de qualquer feature em voo: 1. check:db-rules / allowlist: os módulos db-internal caseMapping (#4947) e schemaColumns (#4948), extraídos de db/core.ts e importados só por ele, não estavam em INTENTIONALLY_INTERNAL. Registrados na allowlist (correção canônica — são internos legítimos, não re-exportados pelo localDb). 2. auto-strategy honra LKGP/cost (combo-routing-engine.test.ts, 2 testes): o task-aware reordering (#4945, reorderByTaskWeight) roda para strategy "auto" e era aplicado DEPOIS do router explícito (selectWithStrategy: lkgp/cost), sobrescrevendo o orderedTargets[0] que o operador escolheu. Instrumentação provou: post-filter [0]=claude (LKGP) → post-task [0]=gpt-oss. Correção: quando o auto usa router explícito, preserva o [0] dele e deixa o task-aware refinar só a cauda de fallback. gpt-oss-120b PERMANECE tool-capable (não é mudança de catálogo; o model-capabilities-registry test segue verde). Validado: 121 testes (combo-routing-engine + combo-task-aware + registry) verdes, red-check confirmado, db-rules/file-size/typecheck/lint/prettier OK. Co-authored-by: Diego Rodrigues de Sa e Souza --- config/quality/file-size-baseline.json | 3 ++- open-sse/services/combo.ts | 21 ++++++++++++++++--- scripts/check/check-db-rules.mjs | 2 ++ .../check-db-rules-classification.test.ts | 4 +++- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 50dab3ac5d..404ab51e3f 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -1,5 +1,6 @@ { "_comment": "Catraca de tamanho (check-file-size.mjs). frozen so pode encolher; arquivos novos <= cap. --update ratcheta.", + "_rebaseline_2026_06_24_4945_task_aware_router_precedence": "Base-red fix: the #4945 task-aware reordering (reorderByTaskWeight) runs for strategy \"auto\" and was applied AFTER the auto router (selectWithStrategy: lkgp/cost/etc) had already pinned orderedTargets[0], silently defeating the operator's explicit LKGP/cost choice (proven: tests/unit/combo-routing-engine.test.ts 'honors LKGP after filtering to tool-capable models' + 'falls back to the full pool when tool filtering empties candidates' selected gpt-oss-120b — the 1st target — instead of the LKGP/cost winner; instrumentation showed orderedTargets[0]=claude post-filter then gpt-oss post-task-reorder). open-sse/services/combo.ts 3306->3321 (+15 = a one-line `autoUsedExplicitRouter` flag set when the explicit router succeeds, and at the task-aware step: when that flag is set, pin orderedTargets[0] and let task-aware refine only the fallback tail — reorderByTaskWeight returns the same objects so identity filtering is safe). gpt-oss-120b correctly REMAINS tool-capable (model-capabilities-registry.test.ts asserts true), so this is NOT a tool-capability change. Irreducible chokepoint wiring at the existing auto-route boundary; not extractable. Covered by the two combo-routing-engine.test.ts cases above + combo-task-aware.test.ts (task-aware still active for non-router strategies).", "_rebaseline_2026_06_24_per_connection_max_concurrent": "Feature (per-connection max_concurrent enforcement, issue #9 follow-up) own growth: open-sse/services/combo.ts 3225->3238 (+13 net). Provider connections already carry a maxConcurrent ceiling (provider_connections.max_concurrent, migration 029, set in the UI/API/DB) but routing ignored it, so low-concurrency subscription accounts (GLM/MiniMax ~1) got flooded -> 429 + cooldown, worse under quota-share where keys share one account. The new wiring lives at the two combo dispatch chokepoints: (1) the strategy===\"quota-share\" branch resolves each target connection's cap and passes it into selectQuotaShareTarget so an at-cap connection is deprioritized (fail-open, never hard-blocked); (2) handleRoundRobinCombo resolves the target connection's cap and uses it as the semaphore maxConcurrency (fallback = combo-level concurrency). ALL extractable logic was moved into the new pure leaf open-sse/services/combo/concurrencyCaps.ts (resolveMaxConcurrentByConnection + makeConnectionConcurrencyResolver, DB reads deduped per connectionId, fail-open) and the pure effectiveMaxConcurrency resolver into combo/comboPredicates.ts; combo.ts keeps only the irreducible chokepoint wiring (a 5-line import + the two call-site bindings). Cohesive at the existing dispatch boundaries, not further extractable. Structural shrink of combo.ts tracked in #3501. Covered by tests/unit/quota-share-strategy.test.ts (per-connection maxConcurrent gating cases) + tests/unit/combo/effective-max-concurrency.test.ts.", "_rebaseline_2026_06_24_combo_cooldown_wait_quota_share": "Feature quota-share combo cooldown-aware retry (Variante A) own growth: open-sse/services/combo.ts 3225->3293 (+68 = the cooldown-wait wrap inside handleComboChat's quota-share path. The existing setTry loop body is LEFT at its original indentation: instead of an outer `while (true)` (which would re-indent ~1600 lines and bloat the review), the setTry loop is hoisted into a small recursive closure `dispatchWithCooldownRetry`, and a wait+redispatch is a tail `return dispatchWithCooldownRetry()` — re-running ONLY the set loop (exactly the prior continue-to-top-of-set-loop semantics) while selection/shadow-routing/setup above stay untouched. At the 429 crystallization point the lock reason is resolved via getModelLockoutInfo, the decision via the new pure resolveComboCooldownWaitDecision, then await waitForCooldownAwareRetry (499 on abort), decrement the budget, recurse. Gated to strategy==='quota-share' && comboCooldownWait.enabled. `git diff` == `git diff -w` for combo.ts (zero re-indentation noise). The gating policy + reason resolution are extracted to the new pure leaf open-sse/services/combo/comboCooldownRetry.ts (1098 (+115 = the ComboCooldownWaitCard exposing enabled/maxWaitMs/maxAttempts/budgetMs in Settings > Resilience, mirroring WaitForCooldownCard; wired through GET/PATCH in src/app/api/resilience/route.ts + comboCooldownWaitSettingsSchema in src/shared/validation/schemas/settings.ts; new UI labels use the t(key)||English-fallback pattern, en-only). Structural shrink of combo.ts + ResilienceTab tracked in #3501.", "_rebaseline_2026_06_23_4774_combo_legacy_strip": "PR #4774 (KooshaPari, #4382 round-trip) own growth: src/app/(dashboard)/dashboard/combos/page.tsx 4434->4456 (+22 = the client-side LEGACY_COMBO_RESILIENCE_KEYS Set gains queueTimeoutMs + the 12 v3.8.31-era removed keys (queueDepth/fallbackDelayMs/handoffProviders/maxComboDepth/manifestRouting/complexityAwareRouting/pipeline_enabled/pipelineConcurrency/shadowRouting/evalRouting/resetAwareEnabled/resetAwareWindow) with explanatory comments, mirroring the server-side strip list in src/app/api/combos/[id]/route.ts so the modal never re-introduces removed keys on Save). Functional strip list, not a movable block; combos/page.tsx structural shrink tracked in #3501. Covered by tests/unit/combo-config.test.ts (auto-promote + passthrough + legacy-key round-trip).", @@ -145,7 +146,7 @@ "_rebaseline_2026_06_24_headroom_strategy": "Headroom-aware connection selection (dario technique): combo.ts 3168->3180 (+12 = a new `else if (strategy === \"headroom\")` dispatch branch in handleComboChat that delegates to orderTargetsByHeadroom + its log line, plus the import). The actual logic lives OUT of the god-file: the pure ranker rankByHeadroom/computeHeadroom is the new leaf open-sse/services/combo/headroomRanking.ts (91 LOC, 3190 (+10 = one new `else if (strategy === \"quota-share\")` dispatch branch in handleComboChat that delegates 100% to selectQuotaShareTarget + its log line, plus the import). All the new logic lives OUT of the god-file in two new leaves under open-sse/services/combo/: quotaShareInflight.ts (in-flight counter with TTL/lease, ~150 LOC 3225 (+35) = one new `else if (strategy === \"task-aware\")` dispatch branch delegating 100% to selectTaskAwareTarget + its imports/log lines. All scoring/classification logic lives OUT of the god-file in the new leaf open-sse/services/taskAwareRouting.ts (553 LOC 0; let eligibleTargets = [...orderedTargets]; @@ -1332,6 +1337,7 @@ export async function handleComboChat({ selectedProvider = decision.provider; selectedModel = decision.model; selectionReason = decision.reason; + autoUsedExplicitRouter = true; } catch (err) { log.warn( "COMBO", @@ -1604,17 +1610,26 @@ export async function handleComboChat({ const task = classifyTask(body); const conversationCacheKey = getConversationCacheKey(body); const taskReordered = reorderByTaskWeight(orderedTargets, task); - if (taskReordered[0]?.modelStr !== orderedTargets[0]?.modelStr) { + // #4945 regression guard: when an explicit auto router (lkgp/cost/…) pinned + // orderedTargets[0], keep that primary choice and let task-aware refine only + // the fallback tail — otherwise task weighting silently defeats the operator's + // chosen LKGP/cost selection. reorderByTaskWeight returns the same target + // objects (no clone), so identity filtering is safe. + const pinnedFirst = autoUsedExplicitRouter ? orderedTargets[0] : undefined; + const nextOrder = pinnedFirst + ? [pinnedFirst, ...taskReordered.filter((t) => t !== pinnedFirst)] + : taskReordered; + if (nextOrder[0]?.modelStr !== orderedTargets[0]?.modelStr) { const reasons = Array.isArray(task.reasons) && task.reasons.length > 0 ? ` (${task.reasons.join(",")})` : ""; log.info( "COMBO", - `task-route task=${task.level}${reasons} cacheKey=${conversationCacheKey ?? "none"} → ${taskReordered[0]?.modelStr}` + `task-route task=${task.level}${reasons} cacheKey=${conversationCacheKey ?? "none"} → ${nextOrder[0]?.modelStr}` ); } - orderedTargets = taskReordered; + orderedTargets = nextOrder; } // Parallel pre-screen: check provider profiles and model availability for all targets diff --git a/scripts/check/check-db-rules.mjs b/scripts/check/check-db-rules.mjs index 3ec8b1e2be..e948bd90e4 100644 --- a/scripts/check/check-db-rules.mjs +++ b/scripts/check/check-db-rules.mjs @@ -42,6 +42,7 @@ export const INTENTIONALLY_INTERNAL = new Set([ "accessTokens", // intentionally-internal: 4 rotas /api/cli/* (connect, whoami, tokens, tokens/[id]) + server/authz/accessTokenAuth.ts via import direto "@/lib/db/accessTokens" (Rule #2) "apiKeyColumnFallbacks", // db-internal: importado só por db/apiKeys.ts (API_KEY_COLUMN_FALLBACKS — fallbacks de coluna split do apiKeys.ts) "apiKeyUsageLimitFields", // db-internal: importado só por db/apiKeys.ts (helpers de campo de limite de uso split do apiKeys.ts; mig 101) + "caseMapping", // db-internal: importado só por db/core.ts (toSnakeCase/toCamelCase/objToSnake — column-mapping snake↔camel split do core.ts, #4947) "cleanup", // intentionally-internal: 3 API routes (purge-quota-snapshots, purge-call-logs, purge-detailed-logs) "cliToolState", // intentionally-internal: 14+ API routes em /api/cli-tools/*-settings "comboForecast", // intentionally-internal: src/lib/usage/comboForecast.ts @@ -63,6 +64,7 @@ export const INTENTIONALLY_INTERNAL = new Set([ "providerNodeSelect", // db-internal: importado só por db/providers.ts (selectProviderNodeForConnection — lógica pura de seleção de provider node split do providers.ts, #4421) "providerStats", // intentionally-internal: src/app/api/provider-stats/route.ts "recovery", // intentionally-internal: bin/cli/runtime.mjs (import() dinâmico) + tests + "schemaColumns", // db-internal: importado só por db/core.ts (ensureProviderConnections/UsageHistory/CallLogsColumns + hasColumn/hasTable/getTableColumns — schema-column reconciliation split do core.ts, #4948) "secrets", // intentionally-internal: src/instrumentation-node.ts (import() dinâmico na inicialização) "serviceModels", // intentionally-internal: 3 callers (services/modelSync, services/bootstrap, /api/services/9router/models) "stateReset", // db-internal: 3 callers dentro de src/lib/db/ (core, backup, apiKeys) para coordenação de reset diff --git a/tests/unit/check-db-rules-classification.test.ts b/tests/unit/check-db-rules-classification.test.ts index a25c180bc7..2c131bc18d 100644 --- a/tests/unit/check-db-rules-classification.test.ts +++ b/tests/unit/check-db-rules-classification.test.ts @@ -121,12 +121,13 @@ test("INTENTIONALLY_INTERNAL is exported from check-db-rules.mjs", () => { assert.ok(INTENTIONALLY_INTERNAL.size > 0, "INTENTIONALLY_INTERNAL must not be empty"); }); -test("INTENTIONALLY_INTERNAL contains the expected 30 audited modules", () => { +test("INTENTIONALLY_INTERNAL contains the expected 32 audited modules", () => { const expected = [ "_rowTypes", "accessTokens", "apiKeyColumnFallbacks", "apiKeyUsageLimitFields", + "caseMapping", "cleanup", "cliToolState", "comboForecast", @@ -147,6 +148,7 @@ test("INTENTIONALLY_INTERNAL contains the expected 30 audited modules", () => { "providerNodeSelect", "providerStats", "recovery", + "schemaColumns", "secrets", "serviceModels", "stateReset",