1

O JSON está válido. O prazo continua inventado.

A anotação diz “Possível lançamento na sexta-feira”. O resumo diz “Lançamento confirmado para sexta-feira”.

As duas frases cabem em um JSON perfeitamente válido. Um esquema pode verificar se date contém uma data. Ele não consegue dizer quem confirmou essa data.

Uma forma simples de tornar essa mudança visível é manter uma tabela revisada dos fatos ao lado do rascunho e comparar os campos de cada afirmação com essa tabela. Vamos apontar uma previsão que virou compromisso, uma pessoa que ganhou uma tarefa extra e uma exportação bem-sucedida confundida com uma importação bem-sucedida. Sem chamada a um modelo, chave de API ou instalação de pacotes.

Comece pelas anotações, não pelo resumo pronto

Estas cinco anotações descrevem o projeto:

  • S1: O rascunho da página de destino está pronto. Ainda não há aprovação do cliente.
  • S2: Ana deve enviar o rascunho ao cliente na terça-feira, 27 de outubro de 2026.
  • S3: A exportação da planilha passou em três verificações por amostragem. A importação no sistema seguinte ainda apresenta erro.
  • S4: Possível lançamento na sexta-feira, 30 de outubro. Data proposta, não confirmada.
  • S5: A página de ajuda precisa de um responsável. Ninguém foi designado.

Três detalhes precisam de campos próprios. O lançamento tem uma data e um grau de certeza. O responsável pela página de ajuda está explicitamente indefinido. Exportação e importação são etapas diferentes, mesmo quando usam o mesmo arquivo.

Primeiro, uma pessoa confere a tabela de fatos com essas anotações. Cada fato recebe um identificador, a referência da anotação, um assunto, um campo, um valor e um grau de certeza. recorded indica que o valor foi registrado na anotação; não significa que a tarefa terminou. Para a data do lançamento, usamos proposed. Para um responsável ainda não designado, usamos null.

Cada afirmação do rascunho recebe os mesmos campos e uma referência a um fato. Uma pessoa precisa conferir se esses campos representam o que a frase realmente diz. Se outro modelo preencher os campos, esse trabalho também precisa ser revisado. Caso contrário, estaríamos pedindo a um modelo sem revisão que certificasse o resultado de outro.

Dê ao código uma tarefa que ele consegue cumprir

A comparação é propositalmente literal. Ela verifica a referência e compara assunto, campo, valor e grau de certeza. Não reescreve o resumo, não inventa um responsável e não escolhe qual anotação é a mais recente. Um registro malformado gera um erro, em vez de parecer uma verificação bem-sucedida.

Salve o exemplo completo abaixo como demo.mjs e execute com Node.js 20 ou mais recente:

node demo.mjs

O código já inclui as anotações, a tabela de fatos, as afirmações, as verificações e o resultado em português. Os nomes dos campos e os códigos internos permanecem em inglês para que as duas versões usem o mesmo formato de dados.

// Compare afirmações estruturadas com uma tabela de fatos revisada por uma pessoa.
// O código não interpreta a redação nem decide se o resumo pode ser enviado.
const certainties = new Set(["recorded", "proposed", "confirmed"]);
const nonempty = (value) => typeof value === "string" && value.trim() !== "";
const scalar = (value) => value === null || typeof value === "string" ||
  typeof value === "boolean" || (typeof value === "number" && Number.isFinite(value));
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);

function validatePacket(packet) {
  if (!record(packet) || !record(packet.sources) ||
      !Array.isArray(packet.facts) || !Array.isArray(packet.claims)) {
    throw new TypeError("Expected sources, facts and claims.");
  }
  if (!Object.entries(packet.sources).every(([id, text]) => nonempty(id) && nonempty(text))) {
    throw new TypeError("Every source needs an ID and source text.");
  }
  for (const [kind, rows] of [["fact", packet.facts], ["claim", packet.claims]]) {
    const seen = new Set();
    for (const row of rows) {
      if (!record(row) || !["id", "subject", "field"].every((key) => nonempty(row[key])) ||
          !Object.hasOwn(row, "value") || !scalar(row.value) ||
          !certainties.has(row.certainty) || seen.has(row.id)) {
        throw new TypeError(`Invalid or duplicate ${kind} record.`);
      }
      seen.add(row.id);
      if (kind === "fact" && (!nonempty(row.sourceId) ||
          !Object.hasOwn(packet.sources, row.sourceId))) {
        throw new TypeError(`Unknown source for fact ${row.id}.`);
      }
      if (kind === "claim" && (!nonempty(row.text) ||
          !(row.evidenceId === null || nonempty(row.evidenceId)))) {
        throw new TypeError(`Claim ${row.id} needs text and an evidence ID or null.`);
      }
    }
  }
}

function compareClaimFields(packet) {
  validatePacket(packet);
  const facts = new Map(packet.facts.map((fact) => [fact.id, fact]));
  return packet.claims.map((claim) => {
    const fact = facts.get(claim.evidenceId);
    const issues = [];
    if (claim.evidenceId === null) {
      issues.push("MISSING_EVIDENCE");
    } else if (!fact) {
      issues.push("UNKNOWN_EVIDENCE");
    } else {
      if (claim.subject !== fact.subject) issues.push("SUBJECT_MISMATCH");
      if (claim.field !== fact.field) issues.push("FIELD_MISMATCH");
      if (claim.value !== fact.value) issues.push("VALUE_MISMATCH");
      if (claim.certainty !== fact.certainty) issues.push("CERTAINTY_MISMATCH");
    }
    return {
      id: claim.id,
      status: issues.length ? "needs_review" : "fields_match",
      issues,
      sourceId: fact?.sourceId ?? null,
      proseReviewRequired: true,
    };
  });
}

function summaryRows(packet, report, language = "en") {
  const pt = language === "pt-BR";
  const messages = pt ? {
    MISSING_EVIDENCE: "falta uma referência",
    UNKNOWN_EVIDENCE: "referência desconhecida",
    SUBJECT_MISMATCH: "o assunto é outro",
    FIELD_MISMATCH: "o campo é outro",
    VALUE_MISMATCH: "o valor é diferente",
    CERTAINTY_MISMATCH: "o grau de certeza mudou",
  } : {
    MISSING_EVIDENCE: "missing evidence",
    UNKNOWN_EVIDENCE: "unknown evidence",
    SUBJECT_MISMATCH: "different subject",
    FIELD_MISMATCH: "different field",
    VALUE_MISMATCH: "different value",
    CERTAINTY_MISMATCH: "certainty changed",
  };
  return report.map((result, index) => ({
    id: result.id,
    text: packet.claims[index].text,
    result: result.status === "fields_match"
      ? (pt ? "CAMPOS CONFEREM" : "FIELDS MATCH")
      : (pt ? "REVISAR" : "REVIEW"),
    reason: result.issues.length
      ? result.issues.map((code) => messages[code]).join("; ")
      : (pt ? "a redação ainda precisa de revisão" : "prose still needs review"),
  }));
}

const packet = {
  sources: {
    "S1": "O rascunho da página de destino está pronto. Ainda não há aprovação do cliente.",
    "S2": "Ana deve enviar o rascunho ao cliente na terça-feira, 27 de outubro de 2026.",
    "S3": "A exportação da planilha passou em três verificações por amostragem. A importação no sistema seguinte ainda apresenta erro.",
    "S4": "Possível lançamento na sexta-feira, 30 de outubro. Data proposta, não confirmada.",
    "S5": "A página de ajuda precisa de um responsável. Ninguém foi designado.",
  },
  facts: [
    {"id": "F1", "sourceId": "S1", "subject": "landing-page", "field": "client_approval", "value": "not_received", "certainty": "recorded"},
    {"id": "F2", "sourceId": "S2", "subject": "send-draft", "field": "owner", "value": "Ana", "certainty": "recorded"},
    {"id": "F3", "sourceId": "S3", "subject": "spreadsheet-export", "field": "sample_checks_passed", "value": 3, "certainty": "recorded"},
    {"id": "F4", "sourceId": "S3", "subject": "next-system-import", "field": "status", "value": "error", "certainty": "recorded"},
    {"id": "F5", "sourceId": "S4", "subject": "launch", "field": "date", "value": "2026-10-30", "certainty": "proposed"},
    {"id": "F6", "sourceId": "S5", "subject": "help-page", "field": "owner", "value": null, "certainty": "recorded"},
  ],
  claims: [
    {"id": "C1", "text": "O lançamento está confirmado para 30 de outubro.", "subject": "launch", "field": "date", "value": "2026-10-30", "certainty": "confirmed", "evidenceId": "F5"},
    {"id": "C2", "text": "Ana é a responsável pela página de ajuda.", "subject": "help-page", "field": "owner", "value": "Ana", "certainty": "recorded", "evidenceId": "F6"},
    {"id": "C3", "text": "A importação passou em três verificações por amostragem.", "subject": "next-system-import", "field": "sample_checks_passed", "value": 3, "certainty": "recorded", "evidenceId": "F3"},
    {"id": "C4", "text": "O cliente aprovou a página de destino.", "subject": "landing-page", "field": "client_approval", "value": "received", "certainty": "recorded", "evidenceId": "F1"},
    {"id": "C5", "text": "30 de outubro é uma data proposta para o lançamento.", "subject": "launch", "field": "date", "value": "2026-10-30", "certainty": "proposed", "evidenceId": "F5"},
    {"id": "C6", "text": "A página de ajuda não tem responsável designado.", "subject": "help-page", "field": "owner", "value": null, "certainty": "recorded", "evidenceId": "F6"},
    {"id": "C7", "text": "A exportação passou em três verificações por amostragem.", "subject": "spreadsheet-export", "field": "sample_checks_passed", "value": 3, "certainty": "recorded", "evidenceId": "F3"},
    {"id": "C8", "text": "A importação no sistema seguinte ainda apresenta erro.", "subject": "next-system-import", "field": "status", "value": "error", "certainty": "recorded", "evidenceId": "F4"},
    {"id": "C9", "text": "Ana ficou encarregada de enviar o rascunho.", "subject": "send-draft", "field": "owner", "value": "Ana", "certainty": "recorded", "evidenceId": "F2"},
  ],
};
console.table(summaryRows(packet, compareClaimFields(packet), "pt-BR"));

Leia o resultado como uma comparação, não como autorização para enviar

As quatro primeiras afirmações precisam de revisão. As cinco seguintes correspondem aos campos da tabela de fatos.

AfirmaçãoResultadoO que mudou ou foi preservado
C1: lançamento confirmadoREVISARproposed virou confirmed
C2: Ana é responsável pela ajudaREVISARnull virou Ana
C3: importação passou em três verificaçõesREVISARA referência descreve a exportação
C4: cliente aprovou a páginaREVISARA aprovação passou de ausente para recebida
C5: data proposta para o lançamentoCAMPOS CONFEREMA data e a incerteza foram preservadas
C6: ajuda sem responsávelCAMPOS CONFEREMNinguém foi designado
C7: exportação passou em três verificaçõesCAMPOS CONFEREMO número pertence à exportação
C8: importação ainda apresenta erroCAMPOS CONFEREMA etapa pendente continua visível
C9: Ana deve enviar o rascunhoCAMPOS CONFEREMA atribuição continua ligada à tarefa certa

Repare no que não aparece: nenhum resultado diz approved. Todos mantêm proseReviewRequired: true, indicando que a redação ainda precisa de revisão.

Quando a comparação falhar, corrija o rascunho ou investigue a anotação. Não altere a tabela só para fazer o texto passar. Se o plano mudou de verdade, confira a nova anotação e atualize a tabela com base nela. Em um sistema de trabalho, a tabela também precisa de uma data ou versão. Uma decisão antiga, mesmo copiada corretamente, pode ser a resposta errada hoje.

Agora faça o teste falhar de outro jeito

Em C5, troque apenas a frase por “O lançamento está confirmado para 30 de outubro”. Deixe o campo certainty como proposed.

O resultado continuará sendo CAMPOS CONFEREM.

Esse é o limite da verificação. Os campos correspondem à tabela, mas a frase contradiz os próprios campos. O código não interpreta text para decidir se ele é verdadeiro. Uma pessoa precisa conferir essa ligação, verificar se a tabela está atualizada e perceber se alguma informação importante ficou de fora. Cada registro deve representar uma afirmação. Separe frases com várias afirmações antes de verificá-las.

Faça outra mudança: use F2 como referência de C2. Esse fato fala de Ana enviando o rascunho. O nome agora corresponde, mas o assunto é outro. Ter a tarefa de enviar um documento não torna Ana responsável pela página de ajuda.

Esse tipo de verificação pode ficar ao lado de uma etapa de redação com IA, de um formulário ou de uma exportação de planilha. Sua contribuição é uma lista curta de divergências que você consegue inspecionar. Quem revisa o resumo pode se concentrar na decisão, em vez de procurar de onde saiu uma palavra alterada.

Para praticar mais, o exercício em inglês com cinco afirmações e uma ficha editável usa as mesmas anotações. Comece pela frase que alguém da equipe provavelmente vai usar para agir. Confira se a data, o responsável e o grau de certeza têm respaldo nas anotações.

Carregando publicação patrocinada...
1

esse é um bom exemplo de por que schema valida estrutura, não verdade. Eu acrescentaria uma checagem de proveniência por afirmação: qual anotação sustenta o campo e qual grau de certeza ela permite. E a própria tabela precisa de versão ou validade; senão, dá para produzir um resumo fiel a uma decisão velha. Você chegou a testar como sinalizar conflito entre duas anotações?

1

No exemplo publicado, esse conflito ainda fica fora da checagem. A ligação evidenceId → sourceId e o campo certainty já permitem rastrear a fonte e comparar o grau de certeza, mas cada afirmação é comparada com um único fato. Ela pode bater com esse fato enquanto outra anotação o contradiz.

Eu acrescentaria uma checagem antes dessa comparação: reunir as anotações vigentes sobre o mesmo assunto e campo, no mesmo contexto. Se uma confirma o lançamento para 30/10 e outra para 02/11, sem indicar que substitui a primeira, o resultado deveria ser needs_review, mostrando as duas fontes. O código não escolheria uma data.

Uma atualização explícita é diferente de um conflito pendente. Eu registraria qual anotação substitui qual, além da versão da tabela usada na revisão. Só pegar a mais recente não basta: ela pode ser uma proposta, não uma decisão.

Esse seria um bom teste adicional: a afirmação corresponde perfeitamente a uma fonte, mas existe outra fonte vigente incompatível. Nesse caso, fields_match sozinho esconde o problema.