@jevaas/sdk
v0.3.3
Published
SDK TypeScript oficial do JEVaaS (jevaas.com.br) — julgamento tipado, roteado e auditável
Maintainers
Readme
@jevaas/sdk
SDK TypeScript oficial do JEVaaS (jev.vertikon.com.br) — o julgamento
tipado e roteado descrito no contrato de borda.
Zero dependências de runtime: só fetch nativo (Node 18+).
npm i @jevaas/sdkComeçando
import { Jevaas, JevaasError, isActionable } from '@jevaas/sdk';
const jevaas = new Jevaas({ apiKey: process.env.JEVAAAS_API_KEY! });
const decisao = await jevaas.judge({
contract: 'ticket-router', // usa a versão vigente
state: { ticket: { messages: [{ author: 'customer', text: '…' }] } },
state_version: 'run_184:step_7', // opcional, para rastreio
decision_id: 'tkt_9931', // opcional, idempotente no inquilino
});
if (isActionable(decisao)) {
// só aqui: route === 'auto' E enforced === true
await minhaPolitica.executar(decisao.allow!);
} else {
console.log(describeRoute(decisao.route, decisao.status));
}| opção | default | papel |
| --- | --- | --- |
| apiKey | — | obrigatória; precisa começar com jev_sk_ |
| baseUrl | https://jev.vertikon.com.br/v1 | borda da vertical; validada no construtor |
| allowPrivateEndpoints | false | aceita destino privado/loopback em baseUrl (dev local); nunca em produção |
| timeoutMs | 30000 | timeout por request |
| maxRetries | 3 (mínimo 1) | tentativas totais em 429/502/503/504 e falha de rede |
| onRequest | — | observabilidade: recebe cada tentativa concluída |
O serviço nunca executa nada. A resposta traz julgamento, rota e um allow
opaco; quem mapeia allow para permissão concreta é o motor de política do
consumidor. Em mode: shadow o julgamento vem com enforced: false — ele mede,
não autoriza.
Métodos
| método | rota | escopo |
| --- | --- | --- |
| judge(req) | POST /decisions/judge | read |
| fanout(req) | POST /decisions/fanout | read |
| contracts.list() | GET /contracts | read |
| contracts.get(id) | GET /contracts/{id} | read |
| contracts.create(input) | POST /contracts | write |
| contracts.update(id, input) | PUT /contracts/{id} | write |
| contracts.versions(id) | GET /contracts/{id}/versions | read |
| contracts.version(id, v) | GET /contracts/{id}/versions/{v} | read |
| contracts.promote(id, v) | POST /contracts/{id}/versions/{v}/promote | contracts |
| contracts.setMode(id, mode) | POST /contracts/{id}/mode | contracts |
| receipts.list(filter) | GET /receipts | read |
| receipts.get(id) | GET /receipts/{id} | read |
| receipts.label(id, l) | POST /receipts/{id}/label | write |
| receipts.outcome(id, o) | POST /receipts/{id}/outcome | write |
| calibration(contract, opts?) | GET /calibration/{contract} | read |
| runEval(contract, cases) | POST /evals/{contract}/run (corpo: casos) | contracts |
| me() · quota() · usage() | GET /auth/me · /quota · /usage | read |
fanout não tem contrato, regra nem barra: devolve só as respostas tipadas, e
status: 'decided' não autoriza nada.
Exemplos por recurso
// contratos: publicar mudança é criar versão nova — a anterior fica imutável, para poder reverter
const v1 = await jevaas.contracts.create({
id: 'ticket-router',
description: 'Roteia tickets de suporte',
owner: '[email protected]',
action_class: 'internal_write', // a barra pertence à classe, não ao modelo
primary_question: 'fila_principal',
questions: {
fila_principal: {
type: 'choice',
instructions: 'Qual equipe deve tratar a solicitação principal?',
criteria: { vendas: '…', suporte: '…', indeterminado: 'Evidência insuficiente.' }, // escape obrigatório
},
},
routes: [{ when: { question: 'fila_principal', equals: 'suporte' }, route: 'auto', allow: 'support:route' }],
default_route: 'human_review', // nunca 'auto' (§5)
});
await jevaas.contracts.promote('ticket-router', 2);
await jevaas.contracts.setMode('ticket-router', 'enforce');
// recibos: no modo sombra, devolver o desfecho real é o que permite medir divergência
const { receipts } = await jevaas.receipts.list({ contract: 'ticket-router', route: 'human_review', limit: 50 });
await jevaas.receipts.outcome(receipts[0].receipt_id, { production_answer: 'suporte', action_taken: true });
await jevaas.receipts.label(receipts[0].receipt_id, { human_label: 'suporte' });
// calibração: "as respostas de confiança alta são mesmo mais confiáveis nesta carga?"
const rel = await jevaas.calibration('ticket-router');
const golden = await jevaas.runEval('ticket-router', [
{ name: 'fatura', expected: 'financeiro', state: { ticket: { messages: [{ author: 'customer', text: 'fatura em dobro' }] } } },
]); // {total, correct, accuracy, by_band, failures}Erros e resiliência
Toda resposta ≥ 400 vira JevaasError com status, requestId, body e o
getter retryable:
try {
await jevaas.judge({ contract: 'ticket-router', state });
} catch (e) {
if (e instanceof JevaasError) {
console.error(`API ${e.status}: ${e.message} (request ${e.requestId})`);
if (e.retryable) await agenda.retentar();
}
throw e;
}retryableétruepara0(rede/timeout),429,502,503e504.- O retry é automático até
maxRetries, honrandoRetry-After(em segundos) quando presente e caindo em backoff exponencial com jitter (250ms × 2ⁿ⁻¹, teto de 8s) quando não. - A mensagem prefere
body.error, depoisbody.code, depois o status HTTP. 422 invalid_contractnão é retentado: é contrato reprovado, não indisponibilidade.409também não: publique outra versão.
Helpers
isActionable(res)—truesomente comroute === 'auto'eenforced === true. É o portão que separa julgamento de autorização: em sombra,collect_evidence,human_reviewouabstain, nunca aja.describeRoute(route, status?)— frase curta do que fazer com a decisão ("Busque evidência NOVA e rejulgue; não age."), considerando ostatusprimeiro (pending_reconciliationpede conferir consumo,stalepede rejulgar).isPublicEndpoint(baseUrl)—truequando a URL passa na guarda de destino do construtor (http/httpse host público). Mesma régua, sem lançar: confira a configuração antes de construir.
Destino (SSRF)
O construtor recusa baseUrl que não seja http/https e host público:
loopback, faixas privadas, link-local, reservadas e multicast (IPv4 e IPv6,
inclusive ::ffff:127.0.0.1 e CGNAT 100.64/10) e os nomes localhost,
*.localhost, *.local e *.internal. Nome DNS que não seja IP literal passa —
resolver está fora do escopo do SDK, quem resolve é o conector. Para apontar para
um servidor local (dev, teste), allowPrivateEndpoints: true — nunca em produção.
Tipos
QuestionKind, Question, QuestionCriteria, Answer, Route, Status,
ActionClass, Mode, Threshold, StateField, StateRole, Condition,
RouteRule, DecisionContract, ContractInput, Receipt,
ReceiptListFilter, ReceiptListResponse, JudgeRequest, JudgeResponse,
JudgeContractRef, FanoutRequest, FanoutResponse, CalibrationReport,
EvalReport, Usage, AccountInfo, Quota, UsageReport.
Os formatos seguem o contrato de borda §4. Onde o contrato fixa o significado
mas não os nomes dos campos (/calibration, /quota, /usage, /auth/me), o
tipo tem índice aberto: leia o que existe em vez de esperar o que não foi
prometido.
Mudanças na 0.3.3
- Escolha do modelo. O JEVaaS oferece o Jev (TypeSafe) e o Drex (nace.ai), com o mesmo preço por
token.
models()lista o que está disponível; escolha commodelnofanoutou no contrato (ContractInput.model). Contrato com modelo sem tarifa é recusado na publicação (422). No Drex, instrução e critérios precisam ser texto. Perfis medidos em português: jevaas.com.br/model#escolha.
Mudanças na 0.3.2
when.allnas regras do contrato: uma regra pode olhar várias perguntas ao mesmo tempo, com E entre elas (um nível só). As perguntas de um pedido não se veem, então a contradição entre elas vira rota — ex.: filasuportee urgência imediata →human_review.audit_sample_rateno contrato: fração das decisõesautosorteadas para auditoria. A rota não muda; a resposta e o recibo trazemaudit: true, ereceipts.list({ audit: true })devolve a fila. Rotule comreceipts.labelpara medir a taxa de falso-auto.- Boas práticas de decisão: texto julgado não confiável (injeção medida), falha fechada e limiar como custo.
Mudanças na 0.3.1
redact_identitynofanoute no contrato: o JEVaaS tira do estado as menções em que a pessoa declara raça, religião, orientação sexual, identidade de gênero, gênero, deficiência ou idade (PT e EN) antes de julgar. A resposta trazredaction: { mentions, kinds }(tipoRedactionReport). Medido em produção: o efeito de identidade na moderação caiu de até +12,6 pontos para perto de zero — viés e decisões reguladas.
Mudanças na 0.3.0
- Quebra (corrige):
runEval(contract, cases)envia os casos do golden set no corpo. Na 0.2.0 o método chamava sem corpo e a API sempre respondia 400. Para fixar a versão avaliada, informecontract_versionnos casos (oopts.versionsaiu: a API o ignorava). - Host padrão:
baseUrlpassa a serhttps://jev.api.br/v1. O host anterior (api.jev.vertikon.com.br) continua respondendo; quem o fixou não precisa mudar nada. - Escopo
contracts:runEval,contracts.setModeecontracts.promotepedemcontracts(ouadmin). No painel, marque "pode promover contratos" ao criar a chave. - Tipo
GoldenCaseexportado.
Mudanças na 0.2.0
Atualize manualmente se você declarou
^0.1.0. Em versões0.x, o^não atravessa minor (^0.1.0=>=0.1.0 <0.2.0): umnpm updatenão traz a 0.2.0, e a 0.1.0 é a versão em que o retry podia cobrar o mesmo julgamento duas vezes. Usenpm install @jevaas/sdk@^0.2.0(ou fixe a versão exata).
judge()geradecision_idquando você não passa — uma vez por chamada, então o retry automático reenvia a mesma chave e o serviço devolve o recibo em vez de julgar e cobrar de novo. Continue passando o id de negócio (tkt_…) quando tiver: ele é preservado e é o que protege entre processos.fanout()não repete mais em falha ambígua (rede/timeout, 504): o serviço não deduplica fanout, e a cobrança pode ter ocorrido. Trate status0/504como "pode ter sido cobrado". 429/502/503 continuam sendo repetidos.
Licença
MIT © Vertikon
