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-metricsO 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
o agente lê isto
- Sistema DEVE ler as entradas do evidence_ledger de kind delivery_attempt e delivery_review agrupadas por correlation_id
- Sistema DEVE converter cada correlation_id em um RegistroDeEntrega com attempts, verdict e code_persisted
- Sistema DEVE considerar attempts como o MAIOR número de tentativa registrado para aquele run
- Sistema DEVE tratar run sem entrada de review como verdict indefinido, nunca como APPROVED
- Sistema NÃO DEVE inventar repos_previstos ou repos_alterados quando o ledger não os registrou
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? }
o agente NÃO vê isto — é a leitura do requisito que ele não pode reinterpretar até caber no que escreveu
| RF | O que afirma | Como afirma |
|---|---|---|
RF-1 | agrupa 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-2 | converte 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-3 | attempts é 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-4 | run 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-5 | nã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(); |
Classificador de banda do FFHI
ffhi-banda@finaya/ffhi-bandaO 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
o agente lê isto
- Sistema DEVE classificar score >= 80 como healthy
- Sistema DEVE classificar score entre 60 e 79 como warning
- Sistema DEVE classificar score < 60 como critical
- Sistema DEVE recusar score fora de 0-100 com erro explícito, nunca clamp silencioso
- Sistema DEVE expor os limiares como constante exportada, para que quem exibe possa mostrá-los
sem nome combinado, o caso não tem o que chamar
export type Banda = 'healthy' | 'warning' | 'critical'export function classifyScore(score: number): Bandaexport const BANDA_THRESHOLDS: { HEALTHY: number; WARNING: number }
o agente NÃO vê isto — é a leitura do requisito que ele não pode reinterpretar até caber no que escreveu
| RF | O que afirma | Como afirma |
|---|---|---|
RF-1 | score 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-2 | as duas bordas da faixa warning | expect(mod.classifyScore(60)).toBe('warning');
expect(mod.classifyScore(79)).toBe('warning'); |
RF-3 | abaixo de 60 é critical, incluindo zero | expect(mod.classifyScore(59)).toBe('critical');
expect(mod.classifyScore(0)).toBe('critical'); |
RF-4 | fora 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-5 | os 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'); |
Janela de retentativa com backoff exponencial e teto
janela-retentativa@finaya/janela-retentativaCada 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
o agente lê isto
- Sistema DEVE calcular o atraso da tentativa N como base * 2^(N-1)
- Sistema DEVE limitar o atraso ao teto declarado, nunca ultrapassá-lo
- Sistema DEVE aplicar jitter determinístico a partir de uma semente recebida, para que o mesmo run reproduza a mesma sequência
- Sistema DEVE recusar tentativa <= 0 com erro explícito, nunca tratar como primeira tentativa
- Sistema DEVE expor se a tentativa ultrapassou o limite de tentativas, sem decidir sozinho o que fazer a respeito
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 jitteratraso_ms = atraso_base_ms com jitter determinístico aplicado, também limitado ao teto
o agente NÃO vê isto — é a leitura do requisito que ele não pode reinterpretar até caber no que escreveu
| RF | O que afirma | Como afirma |
|---|---|---|
RF-1 | a 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-2 | o 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-3 | a 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-4 | tentativa <= 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-5 | informa 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); |
Redator de segredos para texto que vai virar log
redacao-segredo@finaya/redacao-segredoduplicidade revisadaEsta 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
o agente lê isto
- Sistema DEVE mascarar valores que casem com padrões de segredo declarados, devolvendo o texto com a mesma forma e sem o valor
- Sistema DEVE preservar um prefixo curto do valor mascarado, para permitir correlacionar duas ocorrências sem revelar o segredo
- Sistema DEVE mascarar também o valor que aparece dentro de URL de conexão, onde ele fica entre dois pontos e arroba
- 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
- Sistema DEVE informar QUANTOS valores mascarou, para que quem chama saiba que houve redação em vez de supor que o texto estava limpo
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 marcatotal = quantos valores foram mascarados nesta chamada
o agente NÃO vê isto — é a leitura do requisito que ele não pode reinterpretar até caber no que escreveu
| RF | O que afirma | Como afirma |
|---|---|---|
RF-1 | mascara 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-2 | duas 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-3 | mascara 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-4 | padrã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-5 | informa 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); |
Valor monetário em unidade mínima, com repartição sem centavo perdido
dinheiro@finaya/dinheiroduplicidade revisadaA 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
o agente lê isto
- 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
- Sistema DEVE recusar somar ou subtrair valores de MOEDAS DIFERENTES, com erro explícito, nunca convertendo por conta própria
- 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
- Sistema DEVE multiplicar por fator decimal com arredondamento declarado (metade para cima), nunca truncando em silêncio
- 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
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): Dinheiroexport function somar(a: Dinheiro, b: Dinheiro): Dinheiroexport function subtrair(a: Dinheiro, b: Dinheiro): Dinheiroexport function repartir(d: Dinheiro, partes: number): Dinheiro[]export function multiplicar(d: Dinheiro, fator: number): Dinheiroexport function formatar(d: Dinheiro): string
o agente NÃO vê isto — é a leitura do requisito que ele não pode reinterpretar até caber no que escreveu
| RF | O que afirma | Como afirma |
|---|---|---|
RF-1 | fraçã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-2 | moedas 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-3 | repartir 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-4 | multiplicaçã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-5 | formata 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); |
Validação de CPF e CNPJ por dígito verificador
documento-br@finaya/documento-brduplicidade revisadaO `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
o agente lê isto
- Sistema DEVE validar CPF pelos dois dígitos verificadores e recusar os que não fecham
- Sistema DEVE recusar CPF com todos os dígitos iguais (111.111.111-11), que fecha na conta mas não existe
- Sistema DEVE validar CNPJ pelos dois dígitos verificadores, com os pesos 5432109876543210 na ordem correta
- Sistema DEVE aceitar entrada com ou sem máscara, e recusar entrada com quantidade errada de dígitos
- Sistema DEVE devolver o documento normalizado (só dígitos) junto com o veredito, nunca só um booleano
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): Vereditoexport function validarCnpj(entrada: string): Veredito
o agente NÃO vê isto — é a leitura do requisito que ele não pode reinterpretar até caber no que escreveu
| RF | O que afirma | Como afirma |
|---|---|---|
RF-1 | CPF 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-2 | CPF 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-3 | CNPJ 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-4 | com 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-5 | o 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'); |
Janela operacional em horário de Brasília
janela-operacional@finaya/janela-operacionalduplicidade revisadaO 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
o agente lê isto
- Sistema DEVE decidir o dia operacional de um instante segundo o horário de Brasília, e não o fuso do processo
- Sistema DEVE tratar um instante que cai depois do horário de corte como pertencente ao próximo dia operacional
- Sistema DEVE considerar sábado e domingo fora da janela, devolvendo o próximo dia útil
- Sistema DEVE recusar horário de corte inválido, com erro explícito, em vez de assumir um padrão
- Sistema DEVE devolver o dia operacional como data ISO (AAAA-MM-DD), sem hora, para servir de chave
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): stringexport function dentroDaJanela(instante: Date, corte: Corte): boolean
o agente NÃO vê isto — é a leitura do requisito que ele não pode reinterpretar até caber no que escreveu
| RF | O que afirma | Como afirma |
|---|---|---|
RF-1 | o 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-2 | depois 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-3 | fim 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-4 | corte 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-5 | a 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'); |
Chave de idempotência derivada do conteúdo
chave-idempotencia@finaya/chave-idempotenciaduplicidade revisadaIdempotê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
o agente lê isto
- 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
- Para qualquer par de requisições que difiram em qualquer valor, as chaves derivadas DEVEM ser diferentes
- A chave DEVE ser estável entre execuções: a mesma entrada hoje e amanhã produz a mesma saída
- Campos declarados como voláteis (timestamp, id de correlação) NÃO DEVEM influenciar a chave
- A chave DEVE ser opaca e de tamanho fixo, sem revelar o conteúdo que a originou
sem nome combinado, o caso não tem o que chamar
export function derivarChave(conteudo: Record<string, unknown>, volateis?: string[]): string
o agente NÃO vê isto — é a leitura do requisito que ele não pode reinterpretar até caber no que escreveu
| RF | O que afirma | Como afirma |
|---|---|---|
RF-1 | ordem 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-2 | qualquer 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-3 | a 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-4 | campo 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-5 | a 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'); |