# Prova: Observabilidade GenAI

> Escopo: Gate 3 (avaliador independente). Não é dimensão do score composto — impõe **teto** em
> Qualidade de Código (`evals/rubrics/code-quality.md`, seção "Teto de prova de observabilidade
> GenAI"). Aplica-se **somente** a feature que chama LLM; feature sem LLM é `not_evaluated`, e
> `not_evaluated` **não** aplica teto.

## O que esta prova responde

Uma feature que chama um modelo gasta dinheiro, pode falhar de formas que nenhum teste cobre, e
produz texto que ninguém revisou. A pergunta que importa depois do incidente é sempre a mesma, e
raramente tem resposta:

**Dá para reconstruir o que foi ao provedor, e o que ele respondeu, neste turno?**

Repare no que a pergunta **não** é. Ela não é "tem span OpenTelemetry?". Essa formulação foi medida
contra o acervo e reprova **5 de 5** repositórios — e, pior, daria a mesma nota a um projeto cuja
observabilidade inteira é a mensagem da exceção e a outro que tem hash por bloco de prompt, vocabulário
fechado de desfecho de tool, veredito ternário de guard, sete testes provando que a telemetria é
inerte, e a invariante escrita de que o texto enviado é derivado sem cache *"para a telemetria não
poder mentir"*. **Régua que pune o melhor aluno não é régua.**

Span com as convenções GenAI é a forma **recomendada** (`ai-agents-observability-patterns`) e alcança
a faixa mais alta. Telemetria própria também alcança — desde que exista **teste de fidelidade**, isto
é, algo que prove que o registrado é o enviado. O que a rubrica cobra é o **resultado**; o standard é
quem recomenda o meio.

### Aplicabilidade — filtro mecânico, e depois uma determinação nomeada

**Esta rubrica NÃO se declara determinística, e a razão é medida:** nenhuma leitura de `tasks.json`
ou do `mandate.md` mede a propriedade que importa — *esta feature entrega código que chama um
modelo?*. A versão anterior deste bloco prometia determinismo e **errou no primeiro cliente que
encontrou**: a própria feature que criou esta rubrica satisfazia as duas condições (tasks com
`group: "tests"`, mandate citando `ai-agents` dez vezes) e não chama LLM nenhum.

São **duas etapas**, e a segunda não é opcional:

**1. Filtro mecânico, de alta recall** (barato, lido do disco):

> o `mandate.md` cita ao menos um standard `ai-agents`.

Falso ⇒ `not_evaluated`, sem mais discussão. É deliberadamente **largo**: erra para o lado de
convocar a determinação, não para o de dispensá-la.

**2. Determinação, escrita pelo avaliador.** Passado o filtro, a pergunta é
*a feature entrega código que chama um modelo?* — e o relatório **registra a resposta com o motivo**:

```markdown
- Aplicabilidade: LLM SIM — T4 acrescenta `TriagemAgent` sobre `IChatClient` (`AgentCatalog.cs:31`).
- Aplicabilidade: `not_evaluated` — o mandate cita `ai-agents`, mas o diff é 100% markdown de
  standard; nenhuma chamada a modelo entra no produto.
```

**Sinal auxiliar, não determinação:** toda task com `group` em `{docs, tests}` é indício **contra**
código de produção que chama modelo. Indício, porque um harness de agente pode viver inteiro numa
task `tests` — por isso ele informa a determinação em vez de substituí-la.

**Um `not_evaluated` sem motivo escrito não é dispensa: é o escape que o standard
`ai-agents-guardrails` proíbe.** O motivo é o que torna a decisão auditável na revisão seguinte, e é o que impede que
`not_evaluated` vire a saída fácil.

## Bandas 0-10

| 0-3 | 4-6 | 7-8 | 9-10 |
|---|---|---|---|
| A feature chama LLM e **nada** permite saber o que foi enviado, quanto custou, ou se falhou. Observabilidade é a mensagem da exceção | Chamada de LLM registrada **só por log de texto**: sem contagem de tokens, sem correlação com a feature ou a requisição, sem forma de amarrar o registro ao turno | Instrumentação presente e correta — mas a evidência no relatório é a **linha de wiring**, não a saída. O avaliador leu o `AddSource`, não leu um span | Span por chamada com `gen_ai.operation.name` e `gen_ai.provider.name`, tokens registrados, correlação com a feature, e sensível desligado fora de dev — **ou** telemetria própria equivalente cuja fidelidade tem teste. Nos dois casos, o relatório **mostra um span ou registro capturado** |

A fronteira que decide 7-8 × 9-10 é uma só, e é a mesma de toda rubrica desta classe: **evidência
capturada, não evidência declarada.** Ler o código que instrumenta prova que alguém escreveu o
wiring; ler um span prova que ele emite.

## Anti-padrões literais

- **Pontuar o wiring como se fosse a saída** — `builder.AddSource(...)` no diff é 7-8, nunca 9-10.
  Para subir, capture: rode o caminho, pegue **um** span ou **uma** linha de registro, e cole no
  relatório com os atributos que ela carrega.
- **Aceitar "temos logs"** — log de texto sem tokens e sem correlação é banda 4-6. A pergunta
  operacional é "quanto custou este turno e o que foi enviado nele"; um log que não responde nenhuma
  das duas não é observabilidade, é rastro.
- **Reprovar telemetria própria por não ser OpenTelemetry** — a rubrica cobra a propriedade. Um
  registro próprio com **teste de fidelidade** vale 9-10. Sem teste de fidelidade, não vale: é uma
  afirmação sobre o que o código faz, feita pelo mesmo código.
- **Aceitar hash sem a invariante** — um hash do prompt só prova algo se o texto de que ele é hash
  **é** o texto enviado. Se existe cache entre a montagem e o envio, telemetria e requisição podem
  divergir em silêncio. Procure a derivação sem cache; sem ela, o hash é decoração e a nota não passa
  de 6.
- **Prompt cru em produção contado como ponto a favor** — é o oposto: dado sensível persistido é
  achado, não evidência. Cru só em dev ou harness, com opt-in; em produção, hash.
- **Confundir "não tem LLM" com "tem e não instrumentou"** — o primeiro é `not_evaluated` e **não**
  aplica teto; o segundo é 0-3. Colapsar os dois num booleano é o mesmo defeito que o veredito
  ternário de `ai-agents-guardrails` existe para impedir.
- **Aplicar teto sem ter avaliado** — sem a linha da chave `obs` no `**Proof-Scores:**` não há teto a
  aplicar. Um teto inventado a partir de impressão é pior que teto nenhum.
- **Pontuar telemetria de turno cancelado como cobertura** — registro de algo que não aconteceu é
  achado, não evidência.

## Como registrar no relatório

Uma linha dizendo o que foi capturado e como, e — quando a nota for 9-10 — o artefato capturado:

```markdown
- Aplicabilidade: filtro passou (mandate cita `ai-agents-cost-and-budget`); determinação: LLM SIM — T4 acrescenta `TriagemAgent` sobre `IChatClient` (`AgentCatalog.cs:31`).
- Capturado 1 span de `Morph.Agents.Triagem` rodando o caminho real com o fake:
  `chat gpt-5-mini` · gen_ai.operation.name=chat · gen_ai.provider.name=openai ·
  gen_ai.usage.input_tokens=1841 · output_tokens=212 · morph.feature=triagem-lead
- Fidelidade: `PromptDecompositionTests.TextoEnviadoEhAConcatenacaoDosBlocos` prova que o hash
  registrado é o do texto enviado (derivação sem cache).
- EnableSensitiveData=false confirmado fora de dev (`appsettings.Production.json`).
```

E no `**Proof-Scores:**`, a chave `obs`.

Quando não se aplica, uma linha basta — e ela precisa dizer **o que** foi determinado, e por quê. Um `not_evaluated` mudo não é dispensa:

```markdown
- Observabilidade GenAI: `not_evaluated` — o filtro passou (o mandate cita `ai-agents`), mas o
  diff é 100% markdown de standard: nenhuma chamada a modelo entra no produto. Sem teto sobre
  code-quality.
```
