⚠ DEV MODE — auth bypass ativo (FinID OIDC não configurado).

Demandas

O catálogo do dogfooding: o que a plataforma se pede para construir. Os casos de aceite são os RFs escritos de forma executável, por outro autor, e ficam FORA do prompt — é essa separação que faz o gate medir compreensão em vez de medir o próprio código do agente.

Leitor do Evidence Ledger para o índice de rotina de entrega

ledger-reader@finaya/delivery-metrics

O pacote @finaya/delivery-metrics calcula o índice de rotina (evento/estabilizando/rotina) a partir de RegistroDeEntrega[], mas nada o alimenta: o Evidence Ledger guarda as entradas delivery_attempt e delivery_review e não expõe leitura agregada por run. Sem esse leitor, a maturidade L4 não tem como ser medida — só afirmada.

escreve em packages/ts/ledger-reader · suíte importa de ./fulfillment/ledger-reader

Requisitos funcionais

o agente lê isto

  1. Sistema DEVE ler as entradas do evidence_ledger de kind delivery_attempt e delivery_review agrupadas por correlation_id
  2. Sistema DEVE converter cada correlation_id em um RegistroDeEntrega com attempts, verdict e code_persisted
  3. Sistema DEVE considerar attempts como o MAIOR número de tentativa registrado para aquele run
  4. Sistema DEVE tratar run sem entrada de review como verdict indefinido, nunca como APPROVED
  5. Sistema NÃO DEVE inventar repos_previstos ou repos_alterados quando o ledger não os registrou
Contrato

sem nome combinado, o caso não tem o que chamar

  • export type LedgerEntry = { kind: 'delivery_attempt'; correlation_id: string; attempt: number; repos_previstos?: string[] } | { kind: 'delivery_review'; correlation_id: string; verdict: 'APPROVED' | 'REJECTED'; code_persisted: boolean; repos_alterados?: string[] }
  • export function buildRegistros(entries: LedgerEntry[]): RegistroDeEntrega[]
  • RegistroDeEntrega: { correlation_id, attempts, verdict: Verdict | undefined, code_persisted, repos_previstos?, repos_alterados? }
Casos de aceite fora do prompt

o agente NÃO vê isto — é a leitura do requisito que ele não pode reinterpretar até caber no que escreveu

RFO que afirmaComo afirma
RF-1agrupa por correlation_id
const r = mod.buildRegistros([
  { kind: 'delivery_attempt', correlation_id: 'a', attempt: 1 },
  { kind: 'delivery_attempt', correlation_id: 'a', attempt: 2 },
  { kind: 'delivery_attempt', correlation_id: 'b', attempt: 1 },
]);
expect(r).toHaveLength(2);
expect(r.map((x: any) => x.correlation_id).sort()).toEqual(['a', 'b']);
RF-2converte em registro com attempts, verdict e code_persisted
const [r] = mod.buildRegistros([
  { kind: 'delivery_attempt', correlation_id: 'a', attempt: 1 },
  { kind: 'delivery_review', correlation_id: 'a', verdict: 'APPROVED', code_persisted: true },
]);
expect(r.attempts).toBe(1);
expect(r.verdict).toBe('APPROVED');
expect(r.code_persisted).toBe(true);
RF-3attempts é o MAIOR número, não a contagem de entradas
const [r] = mod.buildRegistros([
  { kind: 'delivery_attempt', correlation_id: 'a', attempt: 1 },
  { kind: 'delivery_attempt', correlation_id: 'a', attempt: 5 },
  { kind: 'delivery_attempt', correlation_id: 'a', attempt: 3 },
]);
expect(r.attempts).toBe(5);
RF-4run sem review fica com verdict indefinido, nunca APPROVED
const [r] = mod.buildRegistros([{ kind: 'delivery_attempt', correlation_id: 'a', attempt: 1 }]);
expect(r.verdict).toBeUndefined();
expect(r.verdict).not.toBe('APPROVED');
RF-5não inventa repos que o ledger não registrou
const [r] = mod.buildRegistros([{ kind: 'delivery_attempt', correlation_id: 'a', attempt: 1 }]);
expect(r.repos_previstos).toBeUndefined();
expect(r.repos_alterados).toBeUndefined();
simulate roda o loop inteiro sem escrever nada no mundo

Classificador de banda do FFHI

ffhi-banda@finaya/ffhi-banda

O FFHI devolve um score 0-100 e as bandas (healthy/warning/critical) são recalculadas em cada consumidor, com limiares repetidos em código. Um classificador puro concentra a regra num lugar só — o mesmo motivo pelo qual a regra regulatória virou @finaya/manifests.

escreve em packages/ts/ffhi-banda

Requisitos funcionais

o agente lê isto

  1. Sistema DEVE classificar score >= 80 como healthy
  2. Sistema DEVE classificar score entre 60 e 79 como warning
  3. Sistema DEVE classificar score < 60 como critical
  4. Sistema DEVE recusar score fora de 0-100 com erro explícito, nunca clamp silencioso
  5. Sistema DEVE expor os limiares como constante exportada, para que quem exibe possa mostrá-los
Contrato

sem nome combinado, o caso não tem o que chamar

  • export type Banda = 'healthy' | 'warning' | 'critical'
  • export function classifyScore(score: number): Banda
  • export const BANDA_THRESHOLDS: { HEALTHY: number; WARNING: number }
Casos de aceite fora do prompt

o agente NÃO vê isto — é a leitura do requisito que ele não pode reinterpretar até caber no que escreveu

RFO que afirmaComo afirma
RF-1score no piso da banda e no topo da escala são healthy
expect(mod.classifyScore(80)).toBe('healthy');
expect(mod.classifyScore(100)).toBe('healthy');
RF-2as duas bordas da faixa warning
expect(mod.classifyScore(60)).toBe('warning');
expect(mod.classifyScore(79)).toBe('warning');
RF-3abaixo de 60 é critical, incluindo zero
expect(mod.classifyScore(59)).toBe('critical');
expect(mod.classifyScore(0)).toBe('critical');
RF-4fora de 0-100 lança, e dentro não
expect(() => mod.classifyScore(-1)).toThrow();
expect(() => mod.classifyScore(101)).toThrow();
expect(() => mod.classifyScore(50)).not.toThrow();
RF-5os limiares são exportados e batem com as bordas observadas
expect(mod.BANDA_THRESHOLDS.HEALTHY).toBe(80);
expect(mod.BANDA_THRESHOLDS.WARNING).toBe(60);
expect(mod.classifyScore(mod.BANDA_THRESHOLDS.HEALTHY)).toBe('healthy');
expect(mod.classifyScore(mod.BANDA_THRESHOLDS.WARNING)).toBe('warning');
simulate roda o loop inteiro sem escrever nada no mundo

Janela de retentativa com backoff exponencial e teto

janela-retentativa@finaya/janela-retentativa

Cada integração externa (Dock, StarkInfra, Tool Gateway) reimplementa sua própria espera entre tentativas, e as constantes divergem. Sem teto declarado, uma indisponibilidade longa vira espera de minutos que ninguém previu — e a decisão de desistir fica implícita no código de quem chamou, em vez de explícita na política.

escreve em packages/ts/janela-retentativa

Requisitos funcionais

o agente lê isto

  1. Sistema DEVE calcular o atraso da tentativa N como base * 2^(N-1)
  2. Sistema DEVE limitar o atraso ao teto declarado, nunca ultrapassá-lo
  3. Sistema DEVE aplicar jitter determinístico a partir de uma semente recebida, para que o mesmo run reproduza a mesma sequência
  4. Sistema DEVE recusar tentativa <= 0 com erro explícito, nunca tratar como primeira tentativa
  5. Sistema DEVE expor se a tentativa ultrapassou o limite de tentativas, sem decidir sozinho o que fazer a respeito
Contrato

sem nome combinado, o caso não tem o que chamar

  • export function calcularAtraso(args: { tentativa: number; base_ms: number; teto_ms: number; semente: string; max_tentativas: number }): { atraso_ms: number; atraso_base_ms: number; excedeu_limite: boolean }
  • atraso_base_ms = crescimento exponencial já limitado ao teto, SEM jitter
  • atraso_ms = atraso_base_ms com jitter determinístico aplicado, também limitado ao teto
Casos de aceite fora do prompt

o agente NÃO vê isto — é a leitura do requisito que ele não pode reinterpretar até caber no que escreveu

RFO que afirmaComo afirma
RF-1a tentativa N dobra a anterior
const a = (t: number) => mod.calcularAtraso({ tentativa: t, base_ms: 100, teto_ms: 1_000_000, semente: 's', max_tentativas: 10 });
expect(a(1).atraso_base_ms).toBe(100);
expect(a(4).atraso_base_ms).toBe(800);
RF-2o teto limita, e limita também o valor com jitter
const r = mod.calcularAtraso({ tentativa: 8, base_ms: 100, teto_ms: 500, semente: 's', max_tentativas: 10 });
expect(r.atraso_base_ms).toBe(500);
expect(r.atraso_ms).toBeLessThanOrEqual(500);
RF-3a mesma semente reproduz a mesma sequência
const seq = (semente: string) => [1, 2, 3, 4, 5].map((t) => mod.calcularAtraso({ tentativa: t, base_ms: 100, teto_ms: 1_000_000, semente, max_tentativas: 10 }).atraso_ms);
expect(seq('run-a')).toEqual(seq('run-a'));
expect(seq('run-a')).not.toEqual(seq('run-b'));
RF-4tentativa <= 0 lança, nunca vira primeira tentativa
const chamar = (tentativa: number) => () => mod.calcularAtraso({ tentativa, base_ms: 100, teto_ms: 1000, semente: 's', max_tentativas: 10 });
expect(chamar(0)).toThrow();
expect(chamar(-1)).toThrow();
expect(chamar(1)).not.toThrow();
RF-5informa que passou do limite sem decidir o que fazer
const args = { base_ms: 100, teto_ms: 1_000_000, semente: 's', max_tentativas: 5 };
expect(mod.calcularAtraso({ ...args, tentativa: 5 }).excedeu_limite).toBe(false);
const r = mod.calcularAtraso({ ...args, tentativa: 6 });
expect(r.excedeu_limite).toBe(true);
expect(r.atraso_ms).toBeGreaterThan(0);
simulate roda o loop inteiro sem escrever nada no mundo

Redator de segredos para texto que vai virar log

redacao-segredo@finaya/redacao-segredoduplicidade revisada

Esta semana vazaram senhas de Postgres e Redis por um erro de expansão de shell, e uma chamada à API do Dokploy despejou segredos de 17 projetos no transcript. Nos dois casos o valor chegou íntegro num texto que alguém ia ler. Um redator central resolve onde o problema mora — na fronteira da escrita — em vez de pedir cuidado a cada chamador.

escreve em packages/ts/redacao-segredo

Requisitos funcionais

o agente lê isto

  1. Sistema DEVE mascarar valores que casem com padrões de segredo declarados, devolvendo o texto com a mesma forma e sem o valor
  2. Sistema DEVE preservar um prefixo curto do valor mascarado, para permitir correlacionar duas ocorrências sem revelar o segredo
  3. Sistema DEVE mascarar também o valor que aparece dentro de URL de conexão, onde ele fica entre dois pontos e arroba
  4. Sistema DEVE recusar padrão vazio ou que case com qualquer coisa, com erro explícito — um redator que mascara tudo é indistinguível de um que apagou o log
  5. Sistema DEVE informar QUANTOS valores mascarou, para que quem chama saiba que houve redação em vez de supor que o texto estava limpo
Contrato

sem nome combinado, o caso não tem o que chamar

  • export function redigir(texto: string, padroes: RegExp[]): { texto: string; total: number }
  • texto = o mesmo texto, com os valores de segredo substituídos por uma marca
  • total = quantos valores foram mascarados nesta chamada
Casos de aceite fora do prompt

o agente NÃO vê isto — é a leitura do requisito que ele não pode reinterpretar até caber no que escreveu

RFO que afirmaComo afirma
RF-1mascara o valor e devolve o texto ao redor intacto
const r = mod.redigir('Authorization: AKIAZZZ99911 fim', [/AKIA[A-Z0-9]+/]);
expect(r.texto).not.toContain('AKIAZZZ99911');
expect(r.texto).toContain('Authorization: ');
expect(r.texto).toContain(' fim');
RF-2duas ocorrências do mesmo valor ficam correlacionáveis; valores diferentes, distinguíveis
const p = [/[A-Z]{4}[0-9]{3}/g];
const mesmo = mod.redigir('ABCD111 e ABCD111', p).texto;
const [a, b] = mesmo.split(' e ');
expect(a).toBe(b);
const outro = mod.redigir('WXYZ222', p).texto;
expect(outro).not.toBe(a);
expect(mesmo).not.toContain('ABCD111');
RF-3mascara a senha da URL de conexão mesmo sem padrão que a descreva
const senhas = ['senha-que-ninguem-declarou', 'se[nha-com-colchete', 'p[a]ss', 's3nh@-nao', 'senha%20com%20escape', 'senha.com+sinais!'];
for (const senha of senhas) {
  const r = mod.redigir(`redis://:${senha}@redis:6379/0`, [/AKIA[A-Z0-9]+/]);
  expect(r.texto, `senha ${senha} sobreviveu`).not.toContain(senha);
  expect(r.texto).toContain('redis://');
  expect(r.texto).toContain('@redis:6379/0');
}
RF-4padrão vazio ou que casa com tudo é recusado, e o padrão válido não
expect(() => mod.redigir('texto', [new RegExp('')])).toThrow();
expect(() => mod.redigir('texto', [/.*/])).toThrow();
expect(() => mod.redigir('texto', [/.+/])).toThrow();
expect(() => mod.redigir('texto', [/[\s\S]+/])).toThrow();
expect(() => mod.redigir('texto', [/AKIA[A-Z0-9]+/])).not.toThrow();
RF-5informa quantos valores mascarou
expect(mod.redigir('AKIAAAA111 AKIABBB222', [/AKIA[A-Z0-9]+/g]).total).toBe(2);
expect(mod.redigir('nada aqui', [/AKIA[A-Z0-9]+/g]).total).toBe(0);
simulate roda o loop inteiro sem escrever nada no mundo

Valor monetário em unidade mínima, com repartição sem centavo perdido

dinheiro@finaya/dinheiroduplicidade revisada

A plataforma tem duas convenções de dinheiro convivendo e nenhuma aplicada: o `pricing-engine` declara `amount_usd: z.number()` — ponto flutuante — e o `marketplace.ts` diz `amount: number; // in cents`, uma convenção em COMENTÁRIO. Numa infraestrutura financeira, isso é a fonte clássica de diferença de centavo em conciliação: 0.1 + 0.2 não dá 0.3 em float, e repartir R$ 1,00 em três parcelas some com um centavo se ninguém disser para onde ele vai. Um tipo central resolve onde o problema mora — na representação — em vez de pedir cuidado a cada chamador.

escreve em packages/ts/dinheiro

Requisitos funcionais

o agente lê isto

  1. Sistema DEVE representar valor em unidade mínima INTEIRA (centavos) e recusar construção a partir de número com fração de centavo, com erro explícito
  2. Sistema DEVE recusar somar ou subtrair valores de MOEDAS DIFERENTES, com erro explícito, nunca convertendo por conta própria
  3. Sistema DEVE repartir um valor em N partes de modo que a SOMA das partes seja exatamente o valor original, sem centavo perdido nem criado
  4. Sistema DEVE multiplicar por fator decimal com arredondamento declarado (metade para cima), nunca truncando em silêncio
  5. Sistema DEVE formatar para exibição em pt-BR e expor os centavos separadamente, para que o texto formatado nunca volte a ser usado em conta
Contrato

sem nome combinado, o caso não tem o que chamar

  • export type Moeda = 'BRL' | 'USD'
  • export interface Dinheiro { centavos: number; moeda: Moeda }
  • export function emCentavos(centavos: number, moeda: Moeda): Dinheiro
  • export function somar(a: Dinheiro, b: Dinheiro): Dinheiro
  • export function subtrair(a: Dinheiro, b: Dinheiro): Dinheiro
  • export function repartir(d: Dinheiro, partes: number): Dinheiro[]
  • export function multiplicar(d: Dinheiro, fator: number): Dinheiro
  • export function formatar(d: Dinheiro): string
Casos de aceite fora do prompt

o agente NÃO vê isto — é a leitura do requisito que ele não pode reinterpretar até caber no que escreveu

RFO que afirmaComo afirma
RF-1fração de centavo é recusada; centavo inteiro passa
expect(() => mod.emCentavos(10.5, 'BRL')).toThrow();
expect(() => mod.emCentavos(-0.01, 'BRL')).toThrow();
expect(mod.emCentavos(1050, 'BRL').centavos).toBe(1050);
RF-2moedas diferentes não se somam, e a mesma moeda soma
const brl = mod.emCentavos(100, 'BRL');
const usd = mod.emCentavos(100, 'USD');
expect(() => mod.somar(brl, usd)).toThrow();
expect(() => mod.subtrair(brl, usd)).toThrow();
expect(mod.somar(brl, mod.emCentavos(50, 'BRL')).centavos).toBe(150);
expect(mod.subtrair(brl, mod.emCentavos(50, 'BRL')).centavos).toBe(50);
RF-3repartir não perde nem cria centavo, em vários formatos
for (const [total, partes] of [[100, 3], [100, 4], [101, 3], [1, 3], [7, 2]] as const) {
  const ps = mod.repartir(mod.emCentavos(total, 'BRL'), partes);
  expect(ps, `${total}/${partes}`).toHaveLength(partes);
  const soma = ps.reduce((a: number, p: any) => a + p.centavos, 0);
  expect(soma, `soma de ${total}/${partes}`).toBe(total);
}
RF-4multiplicação arredonda metade para cima, não trunca
expect(mod.multiplicar(mod.emCentavos(1000, 'BRL'), 0.075).centavos).toBe(75);
// 101 * 0.5 = 50,5 — truncar daria 50; a regra declarada é metade para cima.
expect(mod.multiplicar(mod.emCentavos(101, 'BRL'), 0.5).centavos).toBe(51);
expect(mod.multiplicar(mod.emCentavos(100, 'BRL'), 3).centavos).toBe(300);
RF-5formata em pt-BR e mantém os centavos acessíveis
const d = mod.emCentavos(123456, 'BRL');
const txt = mod.formatar(d);
expect(txt).toContain('1.234,56');
expect(txt).toContain('R$');
expect(d.centavos).toBe(123456);
simulate roda o loop inteiro sem escrever nada no mundo

Validação de CPF e CNPJ por dígito verificador

documento-br@finaya/documento-brduplicidade revisada

O `agent-contract/invariant-validator.ts` detecta PII por FORMATO: `\d{3}\.\d{3}\.\d{3}-\d{2}` para CPF e `\b\d{14}\b` para CNPJ. Ou seja: quatorze dígitos quaisquer viram "CNPJ", e um CPF inválido como 111.111.111-11 é tratado como documento real. Numa plataforma de infra financeira isso erra dos dois lados — falso positivo enche o gate de ruído, e documento sem formatação passa batido. Não há validação de dígito verificador em lugar nenhum do repo.

escreve em packages/ts/documento-br

Requisitos funcionais

o agente lê isto

  1. Sistema DEVE validar CPF pelos dois dígitos verificadores e recusar os que não fecham
  2. Sistema DEVE recusar CPF com todos os dígitos iguais (111.111.111-11), que fecha na conta mas não existe
  3. Sistema DEVE validar CNPJ pelos dois dígitos verificadores, com os pesos 5432109876543210 na ordem correta
  4. Sistema DEVE aceitar entrada com ou sem máscara, e recusar entrada com quantidade errada de dígitos
  5. Sistema DEVE devolver o documento normalizado (só dígitos) junto com o veredito, nunca só um booleano
Contrato

sem nome combinado, o caso não tem o que chamar

  • export type TipoDocumento = 'cpf' | 'cnpj'
  • export interface Veredito { valido: boolean; digitos: string; motivo?: string }
  • export function validarCpf(entrada: string): Veredito
  • export function validarCnpj(entrada: string): Veredito
Casos de aceite fora do prompt

o agente NÃO vê isto — é a leitura do requisito que ele não pode reinterpretar até caber no que escreveu

RFO que afirmaComo afirma
RF-1CPF com dígito verificador correto passa; alterado reprova
expect(mod.validarCpf('529.982.247-25').valido).toBe(true);
expect(mod.validarCpf('529.982.247-26').valido).toBe(false);
expect(mod.validarCpf('123.456.789-00').valido).toBe(false);
RF-2CPF de dígitos repetidos reprova, apesar de fechar na conta
for (const d of ['111.111.111-11', '000.000.000-00', '999.999.999-99']) {
  expect(mod.validarCpf(d).valido, d).toBe(false);
}
RF-3CNPJ com dígito verificador correto passa; alterado reprova
expect(mod.validarCnpj('11.222.333/0001-81').valido).toBe(true);
expect(mod.validarCnpj('11.222.333/0001-82').valido).toBe(false);
RF-4com e sem máscara dão o mesmo veredito; tamanho errado reprova
expect(mod.validarCpf('52998224725').valido).toBe(true);
expect(mod.validarCnpj('11222333000181').valido).toBe(true);
expect(mod.validarCpf('5299822472').valido).toBe(false);
expect(mod.validarCnpj('112223330001').valido).toBe(false);
RF-5o veredito carrega os dígitos normalizados
const v = mod.validarCpf('529.982.247-25');
expect(v.digitos).toBe('52998224725');
expect(mod.validarCnpj('11.222.333/0001-81').digitos).toBe('11222333000181');
simulate roda o loop inteiro sem escrever nada no mundo

Janela operacional em horário de Brasília

janela-operacional@finaya/janela-operacionalduplicidade revisada

O repositório tem 131 lugares manipulando data e NENHUM declara fuso: nem `America/Sao_Paulo`, nem `timeZone`. Toda conta de "é o mesmo dia?" e "passou do corte?" roda no fuso de quem executa — o que dá respostas diferentes no laptop do dev e no container em UTC. Numa infra financeira, o corte é o que separa D+0 de D+1.

escreve em packages/ts/janela-operacional

Requisitos funcionais

o agente lê isto

  1. Sistema DEVE decidir o dia operacional de um instante segundo o horário de Brasília, e não o fuso do processo
  2. Sistema DEVE tratar um instante que cai depois do horário de corte como pertencente ao próximo dia operacional
  3. Sistema DEVE considerar sábado e domingo fora da janela, devolvendo o próximo dia útil
  4. Sistema DEVE recusar horário de corte inválido, com erro explícito, em vez de assumir um padrão
  5. Sistema DEVE devolver o dia operacional como data ISO (AAAA-MM-DD), sem hora, para servir de chave
Contrato

sem nome combinado, o caso não tem o que chamar

  • export interface Corte { hora: number; minuto: number }
  • export function diaOperacional(instante: Date, corte: Corte): string
  • export function dentroDaJanela(instante: Date, corte: Corte): boolean
Casos de aceite fora do prompt

o agente NÃO vê isto — é a leitura do requisito que ele não pode reinterpretar até caber no que escreveu

RFO que afirmaComo afirma
RF-1o fuso de Brasília manda, não o do processo
const corte = { hora: 23, minuto: 59 };
expect(mod.diaOperacional(new Date('2026-03-10T02:00:00Z'), corte)).toBe('2026-03-09');
RF-2depois do corte, o dia operacional é o seguinte
const corte = { hora: 16, minuto: 0 };
// 2026-03-10T18:00:00Z = 15:00 em Brasília, antes do corte.
expect(mod.diaOperacional(new Date('2026-03-10T18:00:00Z'), corte)).toBe('2026-03-10');
// 2026-03-10T20:00:00Z = 17:00 em Brasília, depois do corte.
expect(mod.diaOperacional(new Date('2026-03-10T20:00:00Z'), corte)).toBe('2026-03-11');
RF-3fim de semana devolve o próximo dia útil
const corte = { hora: 16, minuto: 0 };
// 2026-03-14 é sábado; 2026-03-15, domingo.
expect(mod.diaOperacional(new Date('2026-03-14T13:00:00Z'), corte)).toBe('2026-03-16');
expect(mod.diaOperacional(new Date('2026-03-15T13:00:00Z'), corte)).toBe('2026-03-16');
expect(mod.dentroDaJanela(new Date('2026-03-14T13:00:00Z'), corte)).toBe(false);
RF-4corte inválido lança em vez de assumir padrão
expect(() => mod.diaOperacional(new Date('2026-03-10T13:00:00Z'), { hora: 24, minuto: 0 })).toThrow();
expect(() => mod.diaOperacional(new Date('2026-03-10T13:00:00Z'), { hora: 10, minuto: 60 })).toThrow();
expect(() => mod.diaOperacional(new Date('2026-03-10T13:00:00Z'), { hora: -1, minuto: 0 })).toThrow();
RF-5a saída é data ISO pura, servível como chave
const d = mod.diaOperacional(new Date('2026-03-10T13:00:00Z'), { hora: 16, minuto: 0 });
expect(d).toMatch(/^\d{4}-\d{2}-\d{2}$/);
expect(d).not.toContain('T');
simulate roda o loop inteiro sem escrever nada no mundo

Chave de idempotência derivada do conteúdo

chave-idempotencia@finaya/chave-idempotenciaduplicidade revisada

Idempotência aparece em `agent-runtime/ports.ts`, no gerador de contratos e no de OpenAPI — sempre como CAMPO a ser preenchido, nunca como derivação. Cada chamador inventa a chave do seu jeito, e duas requisições iguais viram duas operações quando o jeito diverge. Derivar do conteúdo torna a igualdade uma propriedade do dado, não da disciplina de quem chama.

escreve em packages/ts/chave-idempotencia

Requisitos funcionais

o agente lê isto

  1. Para qualquer par de requisições com o mesmo conteúdo, a chave derivada DEVE ser a mesma, independente da ordem das chaves do objeto
  2. Para qualquer par de requisições que difiram em qualquer valor, as chaves derivadas DEVEM ser diferentes
  3. A chave DEVE ser estável entre execuções: a mesma entrada hoje e amanhã produz a mesma saída
  4. Campos declarados como voláteis (timestamp, id de correlação) NÃO DEVEM influenciar a chave
  5. A chave DEVE ser opaca e de tamanho fixo, sem revelar o conteúdo que a originou
Contrato

sem nome combinado, o caso não tem o que chamar

  • export function derivarChave(conteudo: Record<string, unknown>, volateis?: string[]): string
Casos de aceite fora do prompt

o agente NÃO vê isto — é a leitura do requisito que ele não pode reinterpretar até caber no que escreveu

RFO que afirmaComo afirma
RF-1ordem das chaves não muda a derivação
const a = mod.derivarChave({ valor: 100, moeda: 'BRL', destino: 'x' });
const b = mod.derivarChave({ destino: 'x', moeda: 'BRL', valor: 100 });
expect(a).toBe(b);
expect(mod.derivarChave({ a: { x: 1, y: 2 } })).toBe(mod.derivarChave({ a: { y: 2, x: 1 } }));
RF-2qualquer diferença de valor muda a chave
const base = { valor: 100, moeda: 'BRL' };
const variacoes = [{ valor: 101, moeda: 'BRL' }, { valor: 100, moeda: 'USD' }, { valor: '100', moeda: 'BRL' }];
const k = mod.derivarChave(base);
for (const v of variacoes) expect(mod.derivarChave(v), JSON.stringify(v)).not.toBe(k);
RF-3a mesma entrada dá a mesma saída em chamadas repetidas
const e = { valor: 100, moeda: 'BRL' };
const chaves = Array.from({ length: 5 }, () => mod.derivarChave(e));
expect(new Set(chaves).size).toBe(1);
RF-4campo declarado volátil não entra na conta
const v = ['timestamp', 'correlation_id'];
const a = mod.derivarChave({ valor: 100, timestamp: 1, correlation_id: 'a' }, v);
const b = mod.derivarChave({ valor: 100, timestamp: 2, correlation_id: 'b' }, v);
expect(a).toBe(b);
// sem declarar, eles VOLTAM a contar — ignorar por conta própria seria decidir sozinho.
expect(mod.derivarChave({ valor: 100, timestamp: 1 })).not.toBe(mod.derivarChave({ valor: 100, timestamp: 2 }));
RF-5a chave é opaca e de tamanho fixo
const curta = mod.derivarChave({ a: 1 });
const longa = mod.derivarChave({ a: 'x'.repeat(5000), b: 2, c: 3 });
expect(curta.length).toBe(longa.length);
expect(longa).not.toContain('xxxx');
simulate roda o loop inteiro sem escrever nada no mundo