Arquitetura

Sugestão não é execução: a fronteira causal em sistemas com agentes

17 min

Autor: Paulinho Giovannini

Existe uma falha silenciosa que sistemas com integração de IA podem introduzir antes de perceber que ela existe.

Ela não aparece como bug de runtime. Não causa exceção. Não quebra nenhum teste. Ela aparece meses depois, quando alguém tenta auditar uma execução histórica e percebe que o snapshot contém um campo que não deveria ter autoridade causal: um identificador de ator, um timestamp de validação, uma referência ao agente que sugeriu a ação.

O replay falha — ou pior, passa a depender de campos que nunca deveriam influenciar a execução.

Não porque a lógica mudou. Mas porque a fronteira entre sugestão e execução nunca foi formalmente definida.

Hoje, esse problema costuma aparecer quando um LLM é introduzido no fluxo de ação. Mas ele não é um problema específico de LLMs.

Ele aparece sempre que uma camada propõe, interpreta ou recomenda ações antes de uma camada que precisa executar causalmente e responder historicamente pelo resultado que produziu.

A fronteira correta é simples, mas precisa ser estrutural: a camada cognitiva propõe; o Application Service resolve admissibilidade, autorização, idempotência e tradução contratual; o motor executa apenas causalidade de domínio; e o ledger registra identidade.

O problema que cresce devagar

Quando um agente entra no fluxo de ação de um sistema — seja um LLM, um workflow automatizado, um sistema de recomendação ou qualquer componente que proponha ações antes da execução causal — a integração parece natural.

O agente produz uma sugestão.
O sistema valida.
O efeito acontece.
O resultado é salvo.

O elo que parece proteger o sistema é a validação. Se a saída do agente passa por uma checagem rigorosa antes de chegar ao motor, o sistema parece seguro.

Não está.

O problema não está apenas na qualidade da validação. Está em quais tipos têm permissão de atravessar qual fronteira e no que essa permissão obriga o motor a saber.

Essa distinção importa porque o problema aqui não é AI safety, qualidade de prompt ou detecção de alucinação. Esses problemas existem, mas pertencem a outra superfície.

O problema aqui é integridade causal.

O contágio por proximidade

Uma saída cognitiva típica carrega mais do que intenção de ação.

Ela pode carregar identidade, justificativa, origem, contexto de raciocínio, referência ao agente, timestamp de validação, confidence score, histórico da conversa, chave de idempotência, capability/autorização operacional resolvida ou qualquer outro metadado útil para rastrear a proposta.

Esses campos podem ser legítimos. Alguns são essenciais para auditoria. Outros são úteis para investigação. O erro acontece quando eles atravessam a fronteira errada.

A razão dessa fronteira não é validação defensiva. É distinguir o que causou o resultado de quem pediu, sugeriu ou aprovou o resultado.

Quando o tipo que representa uma proposta de agente consegue chegar ao motor, mesmo após validação, três coisas começam a acontecer.

O motor passa a conhecer o conceito de proposta. Ele não deveria. O motor executa comandos causais. Propostas são contexto operacional de quem decidiu acionar o motor. Se o modelo cognitivo muda, o motor não deveria precisar mudar junto.

Metadados operacionais entram na superfície causal. proposed_by, actor_id, validated_at e proposal_reason_ref são campos legítimos no mundo cognitivo, operacional ou auditável. Dentro do motor, eles viram variáveis não causais com poder de contaminar replay.

O snapshot histórico perde pureza causal. Dois comandos causalmente equivalentes podem produzir snapshots distintos apenas porque foram sugeridos por agentes diferentes ou validados em momentos diferentes.

Esse é o pior tipo de erro: o sistema continua funcionando até o dia em que precisa provar o que fez.

A pergunta, portanto, deixa de ser:

A saída do agente foi validada?

E passa a ser:

O motor ainda consegue descrever sua própria causalidade sem conhecer o modelo cognitivo?

Validação não é fronteira

Esse é o erro mais comum.

Times tratam validação como se ela fosse uma fronteira arquitetural. Não é.

Validação responde se um conteúdo é aceitável.
Fronteira responde se aquele conteúdo pode circular para determinado lugar.

Um payload de agente pode ser validado e ainda assim estar proibido de entrar no motor. Um campo pode ser verdadeiro e ainda assim não-causal. Um timestamp pode ser útil para auditoria e, ao mesmo tempo, destrutivo se entrar no cálculo do snapshot.

A fronteira não existe apenas para impedir dados inválidos.

Ela existe para impedir que dados de uma superfície errada adquiram autoridade causal.

As soluções que não resolvem

Esse problema normalmente gera quatro respostas. Todas parecem razoáveis. Todas preservam alguma forma de validação. E todas falham no mesmo ponto: permitem que o modelo errado continue próximo demais da superfície causal.

Tipo unificado com is_proposed

A primeira resposta costuma ser criar um único tipo de ação com uma flag:

type Action = {
	action_type: string;
	target: { entity: string; id: string };
	payload: Record<string, unknown>;
	is_proposed: boolean;
	proposed_by?: string;
	validated_at?: string;
};

Parece elegante. Um tipo. Um pipeline. Um campo indicando origem.

Na prática, o motor continua podendo receber o tipo completo — incluindo proposed_by, validated_at e qualquer campo futuro do modelo cognitivo. A flag não cria uma fronteira. Ela apenas coloca proposta e execução dentro do mesmo contrato circulável.

Pior: o motor agora conhece o conceito de proposta, um detalhe da superfície cognitiva que não deveria existir no vocabulário causal da execução.

Isso é contaminação conceitual. O problema não está em is_proposed existir. Está em campos causais e não causais coabitarem um tipo que pode chegar à superfície causal. O compilador não acusa o erro porque o erro foi codificado como parte do modelo.

Validação em runtime dentro do motor

Outra resposta é rejeitar campos cognitivos ou operacionais dentro do próprio motor.

Se receber algo com proposed_by, rejeita.

Isso inverte a responsabilidade.

Para rejeitar um campo cognitivo, operacional ou auditável, o motor precisa conhecer esse vocabulário. O contágio já ocorreu. A fronteira deixou de ser estrutural e virou uma inspeção defensiva em runtime.

Esse tipo de erro aparece tarde. Aparece em produção. Aparece depois que o tipo errado já atravessou a camada que deveria ser inviolável.

Conversão no controller

Controllers parecem um lugar conveniente para converter a sugestão em comando.

Não são.

Controllers conhecem transporte, validação superficial, parsing, extração de contexto autenticado e contexto da requisição. Eles não deveriam resolver capability/autorização operacional, idempotência, autorização por recurso, semântica de domínio ou contrato de execução.

Colocar a conversão ali produz uma de duas coisas:

  • um controller inflado com lógica de aplicação e domínio;

  • ou uma conversão rasa que parece segura porque transforma formato, mas não resolve autoridade.

Ambas degradam a arquitetura.

Payload genérico até o motor

A quarta resposta é manter o payload aberto por tempo demais:

payload: Record<string, unknown>;

O sistema ganha flexibilidade e perde quase tudo que tornava a fronteira verificável: rejeição de campos desconhecidos, verificação de exaustividade, rastreabilidade de novos tipos de ação e garantia de que cada payload foi interpretado por um resolver ou política explícita.

Um payload aberto é tolerável na borda cognitiva, porque ainda não tem autoridade causal. Ele se torna perigoso quando atravessa a fronteira e chega ao motor como se fosse comando de domínio.

Esse é o ponto que normalmente se perde: o problema não é Record<string, unknown> existir. O problema é ele chegar ao lugar errado.

Quatro artefatos, três superfícies

A solução correta separa quatro artefatos distribuídos em três superfícies de autoridade:

  • ProposedAction: proposta na superfície cognitiva;

  • DomainCommand: comando causal tipado;

  • EngineExecutionContext: contexto técnico causal da execução;

  • AuditContext: identidade, autorização, idempotência e rastreabilidade operacional.

Mesmo quando dois deles parecem representar “a mesma ação”, eles não têm o mesmo direito de circular.

ProposedAction: saída cognitiva

ProposedAction é o que o agente entrega.

Ele pertence à superfície cognitiva: proposta, intenção, justificativa, origem e contexto.

interface ProposedAction {
	action_type: string;
	target: { entity: string; id: string };
	payload: Record<string, unknown>;

	proposal_id: string;
	proposed_by: string;
	proposal_reason_ref: string;
	confidence?: number;
}

action_type, target e payload podem ser abertos aqui porque ainda não têm autoridade causal. Eles são matéria-prima para mediação.

A regra é absoluta:

ProposedAction nunca entra no motor.

Nem por import direto.
Nem por exportação indireta em arquivo agregador.
Nem por alias de caminho.
Nem por conveniência temporária.

O motor não pode conhecer esse tipo.

DomainCommand: entrada causal do motor

DomainCommand é o que o motor recebe.

Sem identidade.
Sem justificativa cognitiva.
Sem referência ao agente.
Sem metadados de auditoria.

Ele é uma união discriminada por action_type, com payload estritamente tipado por ação.

type EntityTarget = {
	entity: 'entity';
	id: string;
};

type EntityExecuteCommand = {
	action_type: 'entity.execute';
	target: EntityTarget;
	payload: {
		operation: 'ACTIVATE' | 'SUSPEND';
	};
};

type EntityUpgradeCommand = {
	action_type: 'entity.upgrade';
	target: EntityTarget;
	payload: {
		target_model_version: string;
		effective_at: string;
		domain_upgrade_reason: 'CONTRACT_MIGRATION' | 'REGULATORY_CHANGE' | 'MODEL_DEPRECATION';
	};
};

type DomainCommand = EntityExecuteCommand | EntityUpgradeCommand;

function assertNeverDomainCommand(command: never): never {
	throw new Error(`Unhandled domain command: ${JSON.stringify(command)}`);
}

A vantagem da união discriminada não é estética. É restrição verificável.

Quando DomainCommand é uma união fechada, strict está ativo e o switch chama assertNeverDomainCommand(command) na branch default, novos comandos não tratados falham na compilação. Essa garantia desaparece com any, casts, payload genérico, fallback genérico ou alargamento de action_type para string.

Isso importa mais do que parece. Sistemas com agentes tendem a ganhar novos tipos de ação rapidamente. Sem verificação de exaustividade, cada nova ação vira um risco de caminho implícito de fallback.

Um campo como domain_upgrade_reason só pertence ao DomainCommand quando é causal: isto é, quando o domínio usa esse código para selecionar regra, contrato, transição permitida ou efeito. Se ele apenas explica por que a ação foi proposta ou aceita, pertence ao ledger, à auditoria ou à camada de explicabilidade, não ao comando causal.

EngineExecutionContext: contexto técnico causal

O motor também pode receber contexto técnico puro.

Mas esse contexto precisa obedecer a uma regra: só entra nele aquilo que pode afetar a execução causal ou parametrizar o motor sem trazer identidade viva.

interface EngineExecutionContext {
	mode: 'NORMATIVE' | 'EXPLORATORY';
	contract_versions: {
		engine: string;
		ruleset: string;
	};
}

Aqui, mode não é duplicado dentro do DomainCommand.payload. Ele pertence ao contexto técnico causal porque parametriza a execução do motor. Se o modo seleciona contrato, limiares, valores padrão, versões de regra ou classe de execução, ele precisa entrar no snapshot; mas continua separado do comando de domínio para não criar duas fontes de verdade.

Um modo exploratório pode existir, mas não pode ser um atalho para execução normativa com invariantes relaxadas. EXPLORATORY deve selecionar um contrato exploratório explícito, produzir artefato não autoritativo e não atualizar estado normativo. Caso contrário, mode deixa de ser contexto causal e vira uma porta lateral para comportamento implícito.

Nunca:

interface ForbiddenEngineExecutionContext {
	actor_id: string;
	user_id: string;
	org_id: string;
	auth_context: unknown;
	jwt: string;
}

A regra normativa é simples:

Se um campo altera resultado causal — seleciona contrato, muda limiares, altera valores padrão, define vigência, escolhe versão de regra ou seleciona uma classe explícita de execução — ele deve entrar no snapshot e participar da identidade causal do snapshot histórico.

Se ele é apenas observacional — verbosidade de log, identificador de correlação, métrica, trace — ele não pode influenciar nenhum branch causal.

A distinção não é preferência. É consequência sobre replay.

AuditContext: identidade e rastreabilidade operacional

Identidade não desaparece. Ela só não entra no motor.

AuditContext pertence ao ledger.

interface AuditContext {
	command_id: string;
	capability_id: string;
	actor_id: string;
	idempotency_key: string;
	correlation_id?: string;
	confirmation_evidence?: {
		confirmed_at: string;
		mechanism: 'UI_CONFIRMATION' | 'SIGNED_APPROVAL' | 'OPERATOR_POLICY';
	};
}

Ele responde perguntas diferentes:

  • quem pediu;

  • sob qual capability/autorização operacional;

  • com qual chave de idempotência;

  • em qual correlação operacional;

  • qual comando auditável foi admitido.

Neste texto, capability significa uma autorização operacional resolvida: a capacidade concreta de executar uma ação específica sobre um recurso específico, sob uma política conhecida.

Essas perguntas são importantes. Só não são perguntas que o motor precisa responder para computar o resultado.

O erro não é registrar identidade. O erro é dar a ela autoridade causal.

A idempotency_key pode determinar se o Application Service executa um novo comando ou retorna uma execução já registrada. Isso afeta a orquestração da aplicação, mas não o cálculo do motor. Por isso ela pertence ao ledger e à política de orquestração, não à entrada causal do motor. A mesma lógica vale para capability_id: ela pode admitir ou bloquear execução, mas não deve moldar o comando causal sem que esse efeito apareça explicitamente no comando ou no contexto causal.

O Application Service como fronteira de mediação

A conversão de ProposedAction em DomainCommand + EngineExecutionContext + AuditContext acontece em um único lugar.

Não no controller.
Não no motor.
No Application Service.

const decision = await applicationPolicy.resolve({
	proposal,
	actor_id: actorId,
	capability_id: resolvedCapability,
	idempotency_key: idempotencyKey,
	command_id: commandId,
});

const command = commandResolver.toDomainCommand(proposal, {
	effective_at: decision.effective_at,
});

const execution_context = executionContextFactory.create({
	mode: decision.execution_mode,
	contract_versions: decision.contract_versions,
});

const execution_frame = buildExecutionFrame({
	command,
	causal_context: execution_context,
	causal_inputs: decision.causal_inputs,
});

const result = await motor.execute(execution_frame);

await auditService.record(
	buildAuditPayload({
		audit_context: auditContextFactory.create(decision),
		proposal_digest: digest(proposal),
		command_digest: digest(command),
		execution_context_digest: digest(execution_context),
		causal_inputs_digest: digest(execution_frame.causal_inputs),
		execution_frame_digest: digest(execution_frame),
		causal_result_digest: digest(result.causal_output),
	}),
	trx
);

Todos os digests devem ser calculados sobre representação canônica: ordenação estável de campos, normalização temporal, precisão numérica definida, ausência de campos observacionais e versão explícita do contrato de serialização.

O digest de resultado deve cobrir apenas a saída causal do motor. Logs, métricas, timestamps observacionais, warnings explicativos e traces pertencem ao ledger ou à camada de explicabilidade.

A conversão em toDomainCommand() não é genérica.

Ela é um switch(action_type) com schema estrito por tipo de ação, validação do envelope inteiro da proposta, validação do alvo, rejeição de campos desconhecidos e falha fechada. Campos cognitivos podem estar presentes na proposta original. Eles não passam para o comando causal.

const ProposedActionEnvelopeSchema = z.strictObject({
	action_type: z.string(),
	target: z.unknown(),
	payload: z.record(z.string(), z.unknown()),
	proposal_id: z.string(),
	proposed_by: z.string(),
	proposal_reason_ref: z.string(),
	confidence: z.number().optional(),
});

const EntityTargetSchema = z.strictObject({
	entity: z.literal('entity'),
	id: EntityIdSchema,
});

const EntityExecuteSchema = z.strictObject({
	operation: z.enum(['ACTIVATE', 'SUSPEND']),
});

const EntityUpgradeSchema = z.strictObject({
	target_model_version: z.string(),
	domain_upgrade_reason: z.enum(['CONTRACT_MIGRATION', 'REGULATORY_CHANGE', 'MODEL_DEPRECATION']),
});

function toDomainCommand(proposal: unknown, ctx: CommandResolutionContext): DomainCommand {
	const parsedProposal = ProposedActionEnvelopeSchema.parse(proposal);
	const target = EntityTargetSchema.parse(parsedProposal.target);

	switch (parsedProposal.action_type) {
		case 'entity.execute': {
			const payload = EntityExecuteSchema.parse(parsedProposal.payload);

			return {
				action_type: 'entity.execute',
				target,
				payload,
			};
		}

		case 'entity.upgrade': {
			const payload = EntityUpgradeSchema.parse(parsedProposal.payload);

			return {
				action_type: 'entity.upgrade',
				target,
				payload: {
					...payload,
					effective_at: ctx.effective_at,
				},
			};
		}

		default:
			return rejectUnsupportedActionType(parsedProposal.action_type);
	}
}

function rejectUnsupportedActionType(actionType: string): never {
	throw new Error(`Unsupported action_type: ${actionType}`);
}

parse() só é aceitável se o schema for estrito. Sanitizar removendo campos desconhecidos não é equivalente a rejeitar campos desconhecidos, porque pode mascarar uma tentativa de atravessar a fronteira.

Schema estrito garante forma; não substitui política de domínio, autorização, idempotência ou resolução de contrato.

O envelope cognitivo aceita action_type como string porque essa borda ainda não tem autoridade causal. A promoção para comando acontece apenas nos casos suportados pelo switch; todo o resto falha fechado.

Resolver versões de contrato também é parte da fronteira. A resolução pode depender do estado atual no momento da decisão, mas o resultado resolvido deve ser materializado antes da execução, persistido no snapshot e usado no replay. O replay nunca deve recalcular versões a partir do estado atual de um registro mutável.

O mesmo vale para estado de domínio. Um target.id identifica o alvo, mas não congela o estado usado na execução. Se o motor carrega estado mutável a partir desse identificador, o snapshot histórico passa a depender do estado atual do sistema. Uma execução auditável precisa registrar o estado causal usado pelo motor: snapshot materializado ou referência historicamente resolvível, acompanhados de digest de integridade.

Entradas temporais causais devem ser canonicalizadas antes de entrar no comando: timezone explícito, formato normalizado, precisão definida e nenhuma dependência do clock local do motor.

O motor executa sobre um frame causal de execução, não sobre uma proposta traduzida mais contexto solto.

interface VersionedCausalInputs {
	input_contract_version: string;

	state_snapshot_ref: string;
	state_digest: string;

	ruleset_ref: string;
	ruleset_digest: string;
}

interface EngineExecutionFrame {
	command: DomainCommand;
	causal_context: EngineExecutionContext;
	causal_inputs: VersionedCausalInputs;
}

O digest verifica integridade. A referência historicamente resolvível permite recuperar o artefato causal. Sem os dois, replay vira suposição.

A assinatura do motor permanece única:

interface Engine {
	execute(frame: EngineExecutionFrame): Promise<EngineResult>;
}

Criar uma assinatura sobrecarregada que aceite AuthContext, ActorContext, JWT, RequestContext ou qualquer outra forma de identidade viva como conveniência temporária é uma violação arquitetural.

Não existe atalho documentado.

O resolver também precisa ser contido

Mover a conversão para o Application Service não torna o resolver uma zona livre.

O Application Service pode ler contexto cognitivo, operacional e auditável para registrar auditoria, resolver capability/autorização operacional, aplicar idempotência ou rejeitar uma proposta. O que ele não pode fazer é deixar esses campos decidirem a forma causal do comando.

Se proposed_by, proposal_reason_ref, confidence, validated_at, actor_id ou capability_id alteram o DomainCommand, então a contaminação apenas mudou de lugar.

A regra é simples: se um campo deve afetar a execução, ele precisa ser promovido a entrada causal explícita, tipada, validada e persistida no snapshot. Caso contrário, só pode influenciar auditoria, rejeição ou roteamento operacional, nunca o comando causal.

Essa é a parte que costuma faltar em integrações com agentes. O time cria uma boa fronteira no motor, mas permite que a mediação tome decisões causais a partir de metadados cognitivos ou operacionais. A fronteira parece limpa por fora e já está contaminada por dentro.

Timestamps não são todos iguais

validated_at parece inofensivo. Frequentemente não é causal. Em geral, ele descreve quando uma proposta foi validada por uma camada de aplicação ou por um humano.

Esse timestamp pertence à auditoria.

Mas isso não significa que o tempo nunca seja causal.

Um effective_at, upgraded_at ou valid_from pode ser parte da regra de domínio. Se o domínio usa aquele instante para escolher contrato, aplicar regra, calcular efeito ou determinar vigência, ele é causal. Nesse caso, deve estar no comando, no snapshot e no digest/hash causal.

A diferença não está no tipo primitivo. Ambos podem ser strings ISO 8601. A diferença está na pergunta que o campo responde.

validated_at responde:

quando a proposta foi validada?

effective_at responde:

qual instante participa da regra executada?

Um é histórico-operacional. O outro pode ser causal.

Confundir os dois é uma das formas mais discretas de corromper replay.

O motor também não deve ler tempo corrente, aleatoriedade ou configuração ambiental invisível. Se tempo, seed, versão de regra ou configuração alteram o resultado, eles precisam entrar como entrada causal explícita. Caso contrário, duas execuções com o mesmo comando podem divergir sem que o snapshot explique por quê.

Garantia executável que não depende de memória coletiva

Separar os artefatos resolve o problema conceitualmente. Mas arquitetura sem garantia executável é intenção.

A fronteira precisa quebrar a compilação quando for violada.

A primeira garantia deve ser estrutural: o motor não pode importar tipos cognitivos.

engine/src
  pode depender de:
    - domain-command
    - engine-context
    - pure-domain-types

  não pode depender de:
    - cognitive
    - auth
    - rbac
    - transport
    - application-service

Uma barreira por grafo de dependências deve impedir que qualquer arquivo dentro do motor importe ou exporte módulos de caminhos proibidos.

A lista de negação de tokens vem depois. Ela não é a fronteira. É um sensor.

[
	{ "pattern": "ProposedAction", "code": "COGNITIVE_TYPE_IN_ENGINE" },
	{ "pattern": "proposal_reason_ref", "code": "COGNITIVE_FIELD_IN_ENGINE" },
	{ "pattern": "proposed_by", "code": "COGNITIVE_FIELD_IN_ENGINE" },
	{ "pattern": "validated_at", "code": "NON_CAUSAL_TIMESTAMP_IN_ENGINE" },
	{ "pattern": "applied_by", "code": "IDENTITY_IN_ENGINE" },
	{ "pattern": "AuthContext", "code": "IDENTITY_IN_ENGINE" },
	{ "pattern": "JWT", "code": "IDENTITY_IN_ENGINE" },
	{ "pattern": "RBAC", "code": "IDENTITY_IN_ENGINE" }
]

Ela captura violações óbvias e torna o erro legível. Mas não substitui análise por AST, grafo de dependências ou regra de fronteira na compilação.

A garantia mínima precisa cobrir quatro barreiras:

  1. fronteira por grafo de dependências: o motor não importa cognitive, auth, transport, application, rbac, billing ou identity;

  2. fronteira de API pública: o motor exporta apenas Engine, DomainCommand, EngineExecutionContext, EngineResult e tipos puros;

  3. símbolos proibidos por AST ou tipo: ProposedAction, AuthContext, ActorContext, JWT, RequestContext, confidence, proposed_by, validated_at e equivalentes não entram no motor;

  4. testes contratuais e golden tests de replay: snapshots históricos são reexecutáveis sem ledger, sem ator, sem proposta cognitiva e sem a requisição original.

Um grep simples pode pegar imports diretos:

grep -RIn --include='*.ts' "from .*cognitive" engine/src && exit 1 || true

Mas isso não cobre exportações agregadoras, alias de caminho ou exportações indiretas. Esses casos precisam de garantia por grafo de dependências.

A transparência sobre esse limite é parte do modelo. O que ainda não está automatizado deve aparecer como risco explícito, não como suposição invisível.

Esse é o ponto em que a diretriz vira fronteira executável.

O teste decisivo é o replay. Se uma execução histórica exige actor_id, proposal_id, validated_at, confidence, AuthContext ou qualquer dado cognitivo para ser reproduzida, a fronteira já foi violada.

A propriedade que isso preserva

Com a fronteira formal, o sistema preserva uma propriedade difícil de adicionar depois:

o registro histórico da execução é causal, não identitário.

Daqui a dois anos, quando alguém auditar uma execução e perguntar o que o sistema computou, a resposta virá de um snapshot que contém apenas o que foi causalmente relevante.

A identidade de quem pediu estará no ledger de auditoria — estruturada, vinculada ao mesmo momento por hash ou referência forte, mas separada do registro causal.

A separação também aparece nos digests. O digest causal deve incluir apenas entradas causais: comando, estado versionado, contratos, regras, parâmetros técnicos e timestamps causais. Proposta cognitiva, score de confiança, ator, capability, evidência de confirmação e trilha de justificativa não entram na identidade causal do snapshot. Eles podem estar vinculados por hash forte no ledger ou na camada de explicabilidade, mas não podem alterar o cálculo.

Explicação também não é causalidade. Uma camada de explicabilidade pode explicar por que uma proposta foi aceita, rejeitada ou executada, mas não pode retroativamente se tornar parte do cálculo, salvo quando um campo é promovido a entrada causal explícita antes da execução.

Essa separação permite que o motor evolua — novos formatos, novos contratos, novas regras — sem que a leitura de snapshots históricos exija conhecimento sobre quem eram os atores no momento da execução.

E permite algo ainda mais importante: que agentes cognitivos participem do sistema sem degradar as garantias do motor.

Nenhum processo precisa saber mais do que sua responsabilidade exige.

Esse é o insight real do padrão. Não se trata de colocar a IA atrás de uma validação. Trata-se de impedir que o modelo cognitivo se torne parte do vocabulário do motor.

Quando esse padrão é necessário

O sinal não é complexidade do sistema. É a presença de dois requisitos simultâneos.

Requisito 1: o sistema integra algum tipo de camada cognitiva ou semiautônoma que propõe ações.

Requisito 2: o sistema tem um motor que produz resultados que precisam ser auditáveis, reproduzíveis, ou ambos.

Se os dois requisitos estiverem presentes, a fronteira entre sugestão e execução não pode ser implícita.

Cada dia que o sistema opera sem ela é um dia em que a contaminação pode acontecer — e provavelmente acontece devagar, em campos que parecem inofensivos no momento.

O único momento realmente caro para implementar essa separação é depois que o sistema já está em produção: múltiplos serviços usando um tipo unificado, snapshots históricos misturando causalidade e identidade, testes que não distinguem os dois mundos.

Antes disso, parece apenas a decisão de definir artefatos separados. Depois, vira migração histórica, correção de snapshots e perda de confiança no replay.