Skip to content

fix(contracts): infer V from keyof S & string in parseContract to fix TS2345 in 15 edge functions#115

Merged
adm01-debug merged 1 commit into
mainfrom
fix/edge-functions-deno-typecheck
May 22, 2026
Merged

fix(contracts): infer V from keyof S & string in parseContract to fix TS2345 in 15 edge functions#115
adm01-debug merged 1 commit into
mainfrom
fix/edge-functions-deno-typecheck

Conversation

@adm01-debug
Copy link
Copy Markdown
Owner

@adm01-debug adm01-debug commented May 22, 2026

🐛 Bug

Job Edge Functions — Deno typecheck falha em 15 de 81 edge functions desde o merge do PR #87 (feat(contracts): migrate 13 edge functions to parseContract).

Funções afetadas: bi-copilot, block-ip-temporarily, e2e-cleanup, force-global-logout, kit-ai-builder, market-intelligence-insights, ownership-audit, ownership-repair, product-webhook, send-transactional-email, simulation-orchestrator, step-up-verify, sync-external-db, trends-insights, webhook-dispatcher.

Erro típico (em cada uma):

TS2345 [ERROR]: Argument of type '{ name: string; versions: { "1": ZodObject<...>; "2": ZodObject<...>; }; defaultVersion: "1"; ... }' 
is not assignable to parameter of type 'ContractSchemas<"1"> & { versions: { "1": ..., "2": ... } }'.
  The types returned by 'versions["1"].deepPartial()' are incompatible between these types.
    Type 'ZodOptional<ZodString>' is missing the following properties from type 'ZodString': _regex, _addCheck, email, url, and 39 more.

🔍 Causa-raiz

A signature anterior era:

export async function parseContract<
  V extends string,
  S extends Record<V, z.ZodTypeAny>,
>(
  req: Request,
  schemas: ContractSchemas<V> & { versions: S },
  ...
)

Cadeia de inferência problemática:

  1. Os schemas definem defaultVersion: "1" as const → TypeScript infere V = "1" (literal)
  2. ContractSchemas<V> exige versions: Record<V, z.ZodTypeAny> = Record<"1", ...>
  3. Mas versions: { "1": ..., "2": ... } tem mais chaves que o Record<"1", ...> exigido
  4. A intersecção & { versions: S } faz versions["1"] virar ZodTypeAny & ZodObject<...>
  5. Quando TypeScript chama .deepPartial() nesse tipo intersectado, devolve um tipo incompatível
  6. Resultado: TS2345 em cada call de parseContract()

✅ Fix

Inverte a ordem de inferência: S é o generic primário, e V é derivado de S via keyof S & string:

-export interface ContractSchemas<V extends string = string> {
-  versions: Record<V, z.ZodTypeAny>;
+export interface ContractSchemas<
+  V extends string = string,
+  S extends Record<V, z.ZodTypeAny> = Record<V, z.ZodTypeAny>,
+> {
+  versions: S;
   defaultVersion: V;
   ...
 }

 export async function parseContract<
-  V extends string,
-  S extends Record<V, z.ZodTypeAny>,
+  S extends Record<string, z.ZodTypeAny>,
 >(
   req: Request,
-  schemas: ContractSchemas<V> & { versions: S },
+  schemas: ContractSchemas<keyof S & string, S>,
   ...
-): Promise<ParseResult<V, S>> {
+): Promise<ParseResult<keyof S & string, S>> {
   ...
+  type V = keyof S & string;

Por quê funciona: agora S é inferido livremente das chaves de versions (sem restrição prévia), e V é derivado. schema.versions["X"] passa a ser o ZodObject concreto (não a intersecção problemática). defaultVersion continua sendo tipado como V (qualquer chave válida de S).

🧪 Validação em lab

Container Linux limpo, Deno 2.8.0 (TypeScript 6.0.3), cache zerado:

Validação Antes Depois
node scripts/typecheck-edge-functions.mjs 15/81 ❌ 81/81 ✅
npx vitest run tests/contracts/ (92 testes) n/a 92/92 ✅
deno check supabase/functions/trends-insights/index.ts (isolado) TS2345 exit 0

🛡️ Análise de compatibilidade

  • ContractSchemas agora aceita 2 generics, mas o segundo (S) tem default = Record<V, z.ZodTypeAny> → chamadores que usam ContractSchemas<V> em 1 arg continuam funcionando.
  • Busca no codebase: ContractSchemas só aparece em 2 lugares — sua definição em parse.ts e o re-export em _shared/contracts/index.ts. Nenhum schema usa como anotação (todos usam inferência). Zero risco de quebrar terceiros.
  • Runtime: zero mudanças — só tipos. Os 92 testes de contracts confirmam o comportamento idêntico.

🤔 Por quê só agora?

PR #87 mergeou hoje cedo (06:42 -0300) com CI verde. O job ficou vermelho depois sem ninguém tocar nesses arquivos. Hipótese: Deno 2.x faz auto-update do toolchain TypeScript embarcado. Entre o merge e os runs subsequentes, a versão do TS ficou mais estrita na resolução de generics, expondo o bug que sempre esteve lá. O fix em si nunca foi compatível com a inferência rigorosa — só passou por sorte/cache.

🛣️ Plano "10/10" — progresso

🤖 PR proposto pelo Claude (Agente BPM/Tech Lead). Lab-test completo: 81/81 edge functions + 92/92 contract tests passam.


Summary by cubic

Fixes TS2345 type errors in 15 edge functions by changing parseContract to infer version V from keyof S & string. Restores Deno typecheck to 81/81 with no runtime changes.

  • Bug Fixes
    • Make S extends Record<string, z.ZodTypeAny> the primary generic and derive V as keyof S & string.
    • Update ContractSchemas to accept V and S (with defaults) and set versions: S; adjust parseContract signature and return type accordingly.
    • Validation: Edge Deno typecheck 81/81; contract tests 92/92 pass.

Written for commit b4c08b3. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

  • Refactor
    • Melhorada a modelagem genérica de esquemas de validação para aumentar a segurança de tipos e reduzir acoplamento entre versões suportadas e definições de schema. O fluxo de processamento e comportamento da aplicação permanecem inalterados.

Review Change Stack

…ix TS2345 in 15 edge functions

Bug: `Edge Functions — Deno typecheck` falhava em 15/81 edge functions desde
o merge do PR #87 (feat(contracts): migrate 13 edge functions to parseContract).
Funções afetadas: bi-copilot, block-ip-temporarily, e2e-cleanup,
force-global-logout, kit-ai-builder, market-intelligence-insights,
ownership-audit, ownership-repair, product-webhook, send-transactional-email,
simulation-orchestrator, step-up-verify, sync-external-db, trends-insights,
webhook-dispatcher.

Causa-raiz:
A signature anterior era:
  parseContract<V extends string, S extends Record<V, z.ZodTypeAny>>(
    req, schemas: ContractSchemas<V> & { versions: S }, opts
  )
TypeScript inferia V somente de `defaultVersion: "1" as const` (literal "1").
ContractSchemas<V> exigia `versions: Record<V, z.ZodTypeAny>` = `Record<"1", ...>`.
Mas `versions: { "1": ..., "2": ... }` tem mais chaves que `Record<"1", ...>`.
A intersecção `& { versions: S }` produzia `versions["1"]` com tipo
`ZodTypeAny & ZodObject<...>` — quando `.deepPartial()` era invocado, o tipo
retornado era incompatível (ZodOptional<ZodString> vs ZodString esperado),
gerando TS2345 em cada call de parseContract().

Fix:
Inverte a ordem de inferência. Agora `S extends Record<string, z.ZodTypeAny>`
é o generic primário (sem restrição prévia de V), e V é derivado de S via
`keyof S & string`. Isso permite que TypeScript infira corretamente todas as
chaves de versions, e o tipo de `schema.versions["X"]` é o ZodObject concreto
(não a intersecção problemática).

ContractSchemas também ganhou um segundo generic S (com default = Record<V, ...>)
para suportar a nova signature mantendo compat com chamadores que usam
ContractSchemas<V> em 1 arg (S vira o default).

Lab-validado:
- node scripts/typecheck-edge-functions.mjs → 81/81 ✅ (antes: 15/81 ❌)
- npx vitest run tests/contracts/ → 92/92 testes passam ✅
- Runtime: zero mudanças (só types)
- Pesquisa no codebase: ContractSchemas só é importado para re-export em
  _shared/contracts/index.ts; nenhum schema usa `: ContractSchemas<V>` como
  anotação. Mudança 100% backward-compat para chamadores existentes.

Causa do PR #87 ter passado e o main estar vermelho agora: Deno 2.x faz
auto-update do toolchain; entre o merge (06:42 -0300) e os runs subsequentes,
a versão de TypeScript embarcada mudou ou a resolução de tipos do esm.sh
ficou mais estrita. O diff em si nunca foi compatível com a inferência
TypeScript 6.x — só passou por sorte de cache no PR #87.

Plano "10/10": este é o Bug #2 de 4. Próximos: #3 (Test Coverage), #4 (quality Run tests).
Copilot AI review requested due to automatic review settings May 22, 2026 20:38
@vercel
Copy link
Copy Markdown

vercel Bot commented May 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
we-dream-big Ready Ready Preview, Comment May 22, 2026 8:38pm

@supabase
Copy link
Copy Markdown

supabase Bot commented May 22, 2026

This pull request has been ignored for the connected project doufsxqlfjyuvxuezpln due to reaching the limit of concurrent preview branches.
Go to Project Integrations Settings ↗︎ if you wish to update this limit.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 22, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9298ae4d-556c-45b3-9334-1f117f53664e

📥 Commits

Reviewing files that changed from the base of the PR and between 57d9f8f and b4c08b3.

📒 Files selected for processing (1)
  • supabase/functions/_shared/contracts/parse.ts

Walkthrough

A PR refatora a parametrização de genéricos em parseContract para melhorar inferência de tipos entre versões suportadas e schemas Zod. ContractSchemas agora aceita explicitamente o mapeamento versão→schema como segundo parâmetro, e parseContract usa esse tipo para retornar um resultado tipado corretamente sem necessidade de asserções.

Changes

Refatoração de Genéricos em parseContract

Layer / File(s) Summary
Parametrização dupla de ContractSchemas e parseContract
supabase/functions/_shared/contracts/parse.ts
ContractSchemas<V, S> passa a aceitar mapeamento versão→schema explicitamente; parseContract<S> trabalha com S extends Record<string, z.ZodTypeAny>, recebendo schemas: ContractSchemas<keyof S & string, S> e retornando ParseResult<keyof S & string, S> para melhor acoplamento de tipos sem alterar a execução interna.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutos

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed O título descreve precisamente a correção aplicada: ajuste da inferência de V a partir de keyof S & string para resolver o erro TS2345 em 15 funções edge.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/edge-functions-deno-typecheck

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes TypeScript inference issues in the shared Edge Function contract parser by changing parseContract to infer the supported version union (V) from the keys of the provided versions object, resolving TS2345 typecheck failures in multiple Edge Functions without changing runtime behavior.

Changes:

  • Extend ContractSchemas to parameterize versions as a generic S (defaulting to Record<V, ZodTypeAny>) instead of forcing Record<V, ...>.
  • Update parseContract to infer S first and derive V as keyof S & string, avoiding problematic intersections on versions["1"].
  • Adjust parseContract return type accordingly, preserving existing runtime logic.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 1 file

Re-trigger cubic

@adm01-debug adm01-debug merged commit 0c650ca into main May 22, 2026
29 of 34 checks passed
adm01-debug added a commit that referenced this pull request May 22, 2026
…ctedRoute redirect

Bug #3 do plano 10/10. O job CI "Test Coverage" (vitest run --coverage)
falhava em main com 1 test failing em 1845 total. Causa-raiz:

  ProtectedRoute.tsx:37 redireciona para /auth (canonico em prod):
    if (!user) return <Navigate to="/auth" .../>;

  Mas os testes admin/reduced-app-navigation.test.tsx e
  admin/route-no-error-element.test.tsx configuravam:
    <Route path="/login" element={<LoginStub />} />

  Logo o redirect cai num path sem element matching → renderiza vazio
  → screen.getByTestId("page-login") falha com:
    Unable to find an element by: [data-testid="page-login"]
    <body><div /></body>

Em produção src/routes/public-routes.tsx mantem AMBAS as rotas:
  <Route path="/auth" element={<Auth />} />
  <Route path="/login" element={<Auth />} />  (alias)

Mas TODOS os 3 redirects de guards (ProtectedRoute, AdminRoute,
DevRoute) apontam para /auth, que é a rota canonica. /login eh
apenas um alias defensivo.

Fix cirurgico: mudar path="/login" -> path="/auth" nos 2 testes
para alinhar com o destino real do Navigate.

Por que nao mudar o redirect para /login no codigo de producao?
  - Risco alto: OAuth callbacks usam /auth como retorno canonico
  - 3 arquivos de prod afetados vs 2 de teste

Por que nao adicionar AMBAS as rotas no teste?
  - Mais verbose; o teste deve refletir a realidade da prod
  - /login alias soh existe pra defensive coding em prod, nao deveria
    interessar pra um teste de redirect

Validacao em lab (clone fresco do main + npm ci + node v22):

  Antes: tests/admin/route-no-error-element.test.tsx > "anônimo em
         rota protegida → /login — árvore limpa"  -> FAIL (5s timeout
         em getByTestId)

  Depois:
    tests/admin/reduced-app-navigation.test.tsx (6 tests) ✅
    tests/admin/route-no-error-element.test.tsx (7 tests) ✅
    Subset broader: 314/314 tests passing em 21 files
    (admin/ + contracts/ + cloud-status + price-freshness)

  Em produção: zero mudancas. Apenas testes alinhados com Navigate.

Roadmap "10/10":
  ✅ Bug #1 Migrations sync guard (PR #111, 5f3ec9d)
  ✅ Bug #2 Edge Deno typecheck (PR #115, 0c650ca)
  🟢 Bug #3 Test Coverage (este PR)
  🟡 Bug #4 ESLint baseline gate (proximo - refinado: nao eh
            "Run tests" como pensavamos, eh ESLint regression)
adm01-debug added a commit that referenced this pull request May 23, 2026
)

* fix(tests): align /login → /auth in route guards tests to match ProtectedRoute redirect

Bug #3 do plano 10/10. O job CI "Test Coverage" (vitest run --coverage)
falhava em main com 1 test failing em 1845 total. Causa-raiz:

  ProtectedRoute.tsx:37 redireciona para /auth (canonico em prod):
    if (!user) return <Navigate to="/auth" .../>;

  Mas os testes admin/reduced-app-navigation.test.tsx e
  admin/route-no-error-element.test.tsx configuravam:
    <Route path="/login" element={<LoginStub />} />

  Logo o redirect cai num path sem element matching → renderiza vazio
  → screen.getByTestId("page-login") falha com:
    Unable to find an element by: [data-testid="page-login"]
    <body><div /></body>

Em produção src/routes/public-routes.tsx mantem AMBAS as rotas:
  <Route path="/auth" element={<Auth />} />
  <Route path="/login" element={<Auth />} />  (alias)

Mas TODOS os 3 redirects de guards (ProtectedRoute, AdminRoute,
DevRoute) apontam para /auth, que é a rota canonica. /login eh
apenas um alias defensivo.

Fix cirurgico: mudar path="/login" -> path="/auth" nos 2 testes
para alinhar com o destino real do Navigate.

Por que nao mudar o redirect para /login no codigo de producao?
  - Risco alto: OAuth callbacks usam /auth como retorno canonico
  - 3 arquivos de prod afetados vs 2 de teste

Por que nao adicionar AMBAS as rotas no teste?
  - Mais verbose; o teste deve refletir a realidade da prod
  - /login alias soh existe pra defensive coding em prod, nao deveria
    interessar pra um teste de redirect

Validacao em lab (clone fresco do main + npm ci + node v22):

  Antes: tests/admin/route-no-error-element.test.tsx > "anônimo em
         rota protegida → /login — árvore limpa"  -> FAIL (5s timeout
         em getByTestId)

  Depois:
    tests/admin/reduced-app-navigation.test.tsx (6 tests) ✅
    tests/admin/route-no-error-element.test.tsx (7 tests) ✅
    Subset broader: 314/314 tests passing em 21 files
    (admin/ + contracts/ + cloud-status + price-freshness)

  Em produção: zero mudancas. Apenas testes alinhados com Navigate.

Roadmap "10/10":
  ✅ Bug #1 Migrations sync guard (PR #111, 5f3ec9d)
  ✅ Bug #2 Edge Deno typecheck (PR #115, 0c650ca)
  🟢 Bug #3 Test Coverage (este PR)
  🟡 Bug #4 ESLint baseline gate (proximo - refinado: nao eh
            "Run tests" como pensavamos, eh ESLint regression)

* fix(tests): align /login → /auth in route-no-error-element test

Parte 2/2 do Bug #3 do plano 10/10 (mesmo PR).

Mesma causa-raiz que reduced-app-navigation.test.tsx: o teste
configurava <Route path="/login" element={<LoginStub />} /> mas o
ProtectedRoute.tsx:37 redireciona para /auth.

Test afetado:
  "anônimo em rota protegida → /login — árvore limpa" (linha 317)

Antes: getByTestId("page-login") timeout 5s (body vazio porque
       /auth não tinha route matching no MemoryRouter do test)

Depois: ✅ 7/7 tests passing

Diff: 1 linha (path="/login" → path="/auth").
@coderabbitai coderabbitai Bot mentioned this pull request May 23, 2026
33 tasks
@adm01-debug adm01-debug deleted the fix/edge-functions-deno-typecheck branch May 24, 2026 18:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants