Caderno operacional — descoberta e tracer bullet

Missão

Você recebeu uma fatia do sistema de manutenção: aprovar uma OS cara e sincronizar a aprovação com o ERP. Em vez de construir todas as telas, fará um tracer bullet vertical: uma operação pequena atravessa ator, autorização, regra, estado, persistência simulada, integração, teste e evidência. A munição traçante mostra o caminho; aqui, ela revela fronteiras arquiteturais sem fingir que o produto inteiro está pronto.

O experimento responde três perguntas:

  1. o pacote de requisitos contém informação suficiente para ser verificado?
  2. uma tentativa não autorizada deixa o estado intacto?
  3. se o ERP falhar, o retry entrega o efeito exatamente uma vez?

Hipótese: regras com dono, qualidades mensuráveis, fonte de verdade única, transições explícitas e rastreabilidade completa permitem localizar falhas antes da implementação ampla.

Duração: 90 a 120 minutos. Pré-requisito técnico: Python 3.10 ou superior; apenas biblioteca padrão. Segurança: use dados fictícios. Pasta de trabalho: crie uma pasta vazia fora de repositórios importantes.

Pré-mortem antes do código

Imagine que, daqui a seis meses, o sistema causou um incidente:

Risco imaginado Causa possível Controle Evidência esperada
aprovação entre unidades confiar no botão oculto autorização no servidor por papel e unidade teste negativo + contador de negações
baixa duplicada timeout seguido de retry sem identidade chave idempotente estável fake ERP recebe duas tentativas e aplica uma
aprovação perdida ERP indisponível outbox pendente e reconciliação profundidade da fila volta a zero
fechamento prematuro indicador recompensa quantidade fechada combinar tempo, reabertura e validação taxa de reabertura por equipe
regra muda sem controle limiar enterrado no código dono, fonte e vigência da regra revisão rastreável

O pré-mortem não prevê o futuro. Ele força a equipe a imaginar abuso, incentivo, indisponibilidade, duplicação e erro humano enquanto mudar o desenho ainda é barato.

Registro mínimo da descoberta

Antes de executar, escreva quatro linhas fora do código. Como fato observado, registre que o ERP pode ficar indisponível durante uma aprovação. Como regra declarada, registre que apenas supervisor da mesma unidade aprova, junto do responsável e da política que confirmam isso. Como hipótese, proponha que repetir a sincronização pode duplicar o efeito. Como decisão, declare que o aplicativo é dono da OS e o ERP é dono do saldo. Peça a um operador e a um supervisor que corrijam essas linhas.

Esse registro evita que o script transforme suposições em “verdades”. Se ninguém confirma a regra, marque-a como hipótese; se duas áreas reivindicam o saldo, interrompa o desenho e defina governança. O laboratório valida coerência interna e comportamento simulado, não a realidade organizacional. Guarde as correções dos participantes como evidência de descoberta, sem coletar dados pessoais desnecessários.

Passo 1 — crie o laboratório

Salve o bloco como requirements_lab.py:

from __future__ import annotations

import json
import sys
from copy import deepcopy
from pathlib import Path


BAD_PACKAGE = {
    "rules": [{"id": "BR-01", "text": "Custo alto exige supervisor"}],
    "states": ["PENDING", "APPROVED", "CLOSED"],
    "transitions": [["PENDING", "APPROVED"], ["CLOSED", "APPROVED"]],
    "data_ownership": {
        "inventory_balance": ["maintenance-app", "erp"]
    },
    "integrations": [{"id": "ERP", "timeout_s": 5}],
    "requirements": [
        {"id": "FR-01", "kind": "functional", "text": "Somente supervisor aprova"},
        {"id": "QR-01", "kind": "quality", "text": "A busca deve ser rápida"},
    ],
    "risks": [{"id": "RISK-AUTH"}],
    "tests": [],
    "telemetry": [],
    "trace": [{"requirement": "FR-01", "risk": "RISK-AUTH"}],
}


GOOD_PACKAGE = {
    "organization_goal": "reduzir risco e tempo parado sem perder controle de custo",
    "actors": ["requester", "technician", "supervisor", "erp"],
    "process": ["report", "triage", "assign", "execute", "approve", "sync_erp"],
    "rules": [{
        "id": "BR-01",
        "text": "OS com custo acima de 5000 exige supervisor da mesma unidade",
        "owner": "maintenance-manager",
        "source": "maintenance-policy-v3",
    }],
    "states": ["PENDING", "APPROVED", "CANCELLED"],
    "transitions": [["PENDING", "APPROVED"], ["PENDING", "CANCELLED"]],
    "data_ownership": {
        "work_order": ["maintenance-app"],
        "inventory_balance": ["erp"],
    },
    "integrations": [{
        "id": "ERP",
        "timeout_s": 5,
        "idempotency_key": "event_id",
        "reconciliation": "retry pending outbox and compare acknowledgements",
    }],
    "requirements": [
        {"id": "FR-01", "kind": "functional",
         "text": "Supervisor ativo aprova OS pendente da própria unidade"},
        {"id": "QR-01", "kind": "quality", "operation": "filtered-search",
         "metric": "p95_ms", "threshold": 800, "environment": "production-like",
         "load": "100 concurrent users; at most 100 returned items"},
        {"id": "IR-01", "kind": "integration",
         "text": "Aprovação pendente reconcilia no ERP sem efeito duplicado"},
    ],
    "risks": [
        {"id": "RISK-AUTH", "text": "aprovação indevida"},
        {"id": "RISK-DUP", "text": "efeito duplicado no ERP"},
        {"id": "RISK-LAT", "text": "triagem lenta"},
    ],
    "tests": [
        {"id": "TEST-AUTH-NEG", "kind": "negative"},
        {"id": "TEST-ERP-RETRY", "kind": "failure"},
        {"id": "TEST-PERF-P95", "kind": "quality"},
    ],
    "telemetry": [
        {"id": "METRIC-DENIED"}, {"id": "METRIC-OUTBOX"}, {"id": "METRIC-P95"}
    ],
    "trace": [
        {"requirement": "FR-01", "risk": "RISK-AUTH",
         "test": "TEST-AUTH-NEG", "telemetry": "METRIC-DENIED"},
        {"requirement": "IR-01", "risk": "RISK-DUP",
         "test": "TEST-ERP-RETRY", "telemetry": "METRIC-OUTBOX"},
        {"requirement": "QR-01", "risk": "RISK-LAT",
         "test": "TEST-PERF-P95", "telemetry": "METRIC-P95"},
    ],
}


def validate(package: dict) -> list[str]:
    findings: list[str] = []
    for rule in package.get("rules", []):
        if not rule.get("owner") or not rule.get("source"):
            findings.append(f"{rule['id']}:rule-without-owner-source")

    states = set(package.get("states", []))
    allowed = {("PENDING", "APPROVED"), ("PENDING", "CANCELLED")}
    for source, target in package.get("transitions", []):
        if source not in states or target not in states or (source, target) not in allowed:
            findings.append(f"invalid-transition:{source}->{target}")

    for datum, owners in package.get("data_ownership", {}).items():
        if len(owners) != 1:
            findings.append(f"{datum}:multiple-sources-of-truth")

    for integration in package.get("integrations", []):
        if not integration.get("idempotency_key") or not integration.get("reconciliation"):
            findings.append(f"{integration['id']}:missing-idempotency-reconciliation")

    requirement_ids = {item["id"] for item in package.get("requirements", [])}
    for item in package.get("requirements", []):
        if item.get("kind") == "quality":
            required = ("metric", "threshold", "environment", "load")
            if any(not item.get(field) for field in required):
                findings.append(f"{item['id']}:quality-not-measurable")

    risk_ids = {item["id"] for item in package.get("risks", [])}
    test_ids = {item["id"] for item in package.get("tests", [])}
    metric_ids = {item["id"] for item in package.get("telemetry", [])}
    traced = set()
    for row in package.get("trace", []):
        traced.add(row.get("requirement"))
        if (row.get("requirement") not in requirement_ids
                or row.get("risk") not in risk_ids
                or row.get("test") not in test_ids
                or row.get("telemetry") not in metric_ids):
            findings.append(f"{row.get('requirement')}:incomplete-trace")
    for requirement_id in sorted(requirement_ids - traced):
        findings.append(f"{requirement_id}:missing-trace")
    return sorted(set(findings))


class FakeERP:
    def __init__(self) -> None:
        self.available = True
        self.applied: set[str] = set()
        self.attempts = 0

    def send(self, event: dict) -> str:
        self.attempts += 1
        if not self.available:
            raise ConnectionError("ERP unavailable")
        event_id = event["event_id"]
        if event_id in self.applied:
            return "duplicate-ignored"
        self.applied.add(event_id)
        return "applied"


def approve(user: dict, order: dict, outbox: list[dict]) -> dict:
    before = deepcopy(order)
    if user["role"] != "supervisor" or user["unit"] != order["unit"]:
        return {"status": 403, "before": before, "after": deepcopy(order)}
    if order["state"] != "PENDING":
        return {"status": 409, "before": before, "after": deepcopy(order)}
    order["state"] = "APPROVED"
    order["version"] += 1
    event = {
        "event_id": f"approval:{order['id']}:{order['version']}",
        "type": "WorkOrderApproved",
        "order_id": order["id"],
    }
    outbox.append(event)
    return {"status": 200, "before": before, "after": deepcopy(order), "event": event}


def reconcile(outbox: list[dict], erp: FakeERP) -> list[str]:
    results = []
    for event in list(outbox):
        try:
            results.append(erp.send(event))
            outbox.remove(event)
        except ConnectionError:
            results.append("pending")
    return results


def prove() -> dict:
    assert validate(GOOD_PACKAGE) == []
    order = {"id": "OS-1042", "unit": "NORTH", "state": "PENDING", "version": 0}
    outbox: list[dict] = []

    denied = approve({"role": "technician", "unit": "NORTH"}, order, outbox)
    assert denied["status"] == 403 and denied["before"] == denied["after"]
    assert order["state"] == "PENDING" and outbox == []

    accepted = approve({"role": "supervisor", "unit": "NORTH"}, order, outbox)
    assert accepted["status"] == 200 and order["state"] == "APPROVED"
    assert len(outbox) == 1

    erp = FakeERP()
    erp.available = False
    first = reconcile(outbox, erp)
    assert first == ["pending"] and len(outbox) == 1

    erp.available = True
    second = reconcile(outbox, erp)
    replay = erp.send(accepted["event"])
    assert second == ["applied"] and replay == "duplicate-ignored"
    assert len(outbox) == 0 and len(erp.applied) == 1

    return {
        "package_valid": True,
        "unauthorized": {"status": 403, "state_unchanged": True},
        "authorized": {"status": 200, "final_state": order["state"]},
        "erp_failure": {"first_attempt": first[0], "pending_after_failure": 1},
        "reconciliation": {
            "retry": second[0], "replay": replay,
            "effects_applied": len(erp.applied), "outbox_remaining": len(outbox),
        },
    }


def main() -> int:
    mode = sys.argv[1] if len(sys.argv) > 1 else "prove"
    if mode == "bad":
        findings = validate(BAD_PACKAGE)
        print(json.dumps({"valid": not findings, "findings": findings}, indent=2))
        return 2 if findings else 0
    if mode == "prove":
        evidence = prove()
        output = Path("lab-output") / "evidence.json"
        output.parent.mkdir(exist_ok=True)
        output.write_text(json.dumps(evidence, indent=2), encoding="utf-8")
        print(json.dumps(evidence, indent=2))
        print(f"evidence={output}")
        return 0
    print("usage: python requirements_lab.py [bad|prove]", file=sys.stderr)
    return 64


if __name__ == "__main__":
    raise SystemExit(main())

O validador é didático, não um verificador de conformidade ISO. Ele torna explícitas algumas propriedades ensinadas neste capítulo; uma especificação real exige revisão humana, validação com stakeholders e controles próprios do domínio.

Passo 2 — provoque a falha

Execute:

python requirements_lab.py bad
$LASTEXITCODE

Resultado esperado: um JSON com valid: false, seis achados e código de saída 2. A ordem dos achados pode variar na apresentação, mas devem aparecer estas classes:

BR-01:rule-without-owner-source
FR-01:incomplete-trace
QR-01:missing-trace
QR-01:quality-not-measurable
ERP:missing-idempotency-reconciliation
inventory_balance:multiple-sources-of-truth
invalid-transition:CLOSED->APPROVED

Há sete linhas porque uma especificação vaga pode violar mais de uma propriedade: QR-01 não é mensurável e também não foi rastreado. O código de saída diferente de zero permite que CI, hook ou agente pare em vez de declarar sucesso.

Diagnóstico por fronteira

  • Regra: BR-01 não diz quem responde por ela nem qual documento sustenta a afirmação.
  • Estado: fechar e depois aprovar contradiz a máquina definida para o exemplo.
  • Dados: aplicativo e ERP se declaram autoridades do mesmo saldo; um conflito não tem desempate.
  • Integração: timeout existe, mas retry não tem identidade nem plano de reconciliação.
  • Qualidade: “rápida” não informa operação, carga, ambiente, métrica ou limiar.
  • Evidência: FR-01 aponta um risco, porém não possui teste negativo nem telemetria; QR-01 nem entra na matriz.

Se o modo defeituoso retornar zero, não prossiga: confirme que executou bad, que o arquivo foi salvo completo e que não alterou BAD_PACKAGE acidentalmente.

Passo 3 — execute a fatia vertical

Agora rode:

python requirements_lab.py prove
$LASTEXITCODE
Get-Content -LiteralPath .\lab-output\evidence.json

Resultado esperado: código 0 e evidência contendo:

{
  "package_valid": true,
  "unauthorized": {"status": 403, "state_unchanged": true},
  "authorized": {"status": 200, "final_state": "APPROVED"},
  "erp_failure": {"first_attempt": "pending", "pending_after_failure": 1},
  "reconciliation": {
    "retry": "applied",
    "replay": "duplicate-ignored",
    "effects_applied": 1,
    "outbox_remaining": 0
  }
}

Leia causalmente. A tentativa do técnico devolve 403 e o before é igual ao after: autorização não depende da interface. A aprovação válida muda o estado e coloca um evento na outbox. Com ERP indisponível, o evento permanece. No retorno, a reconciliação aplica o evento; uma repetição intencional usa o mesmo event_id e é ignorada. O fake não prova o ERP real, a rede real ou desempenho; prova a regra no limite didático e revela o contrato que depois exigirá teste de integração.

Critérios de aceite do laboratório

  • o modo bad retorna 2 e localiza regra, qualidade, estado, propriedade, integração e rastreabilidade;
  • o modo prove retorna 0 sem pacotes externos;
  • uma pessoa sem papel de supervisor recebe 403 e não muda estado, versão nem outbox;
  • supervisor da mesma unidade leva PENDING a APPROVED;
  • falha do ERP preserva um evento pendente;
  • retry e replay produzem somente um efeito no fake ERP;
  • evidence.json registra os resultados relevantes;
  • cada requisito em GOOD_PACKAGE liga risco, teste e telemetria existentes.

Guarde como evidência o comando, versão do Python, saídas e arquivo JSON. Em uma equipe, acrescente hash do commit e ambiente. Não use apenas uma captura de tela: texto estruturado pode ser comparado e automatizado.

Falhas comuns e recuperação

Sintoma Causa provável Ação
python não encontrado runtime ausente ou fora do PATH instale Python 3.10+ e confirme python --version
bad retorna 0 pacote defeituoso foi alterado restaure BAD_PACKAGE e execute novamente
prove acusa incomplete-trace ID divergente entre catálogos compare requisito, risco, teste e telemetria
replay gera segundo efeito chave muda entre tentativas derive chave da intenção estável, não do horário do retry
evento some na falha foi removido antes da confirmação remova da outbox somente após aceite do destino
teste negativo muda a OS autorização ocorreu depois da mutação autorize antes de qualquer efeito

Exercícios de mutação

  1. Troque a unidade do supervisor para SOUTH. Escreva a asserção que prova 403 e estado intacto.
  2. Remova environment de QR-01; preveja o achado antes de executar.
  3. Faça event_id incluir a hora de cada tentativa. Observe como o replay deixa de ser reconhecido e explique por que “ID único” não significa “chave idempotente correta”.
  4. Adicione o estado CANCELLED e prove que CANCELLED → APPROVED falha.
  5. Acrescente RISK-INCENTIVE: técnicos fecham OS cedo para melhorar indicador. Proponha requisito, teste analítico e telemetria que combine taxa de reabertura com tempo de resolução.
  6. Expanda o tracer bullet com um contrato HTTP mínimo OpenAPI. Defina 200, 403, 409 e uma chave idempotente; não implemente outra tela.

Recuperação ativa

Feche o arquivo e responda: por que este é um tracer bullet vertical? O que o pré-mortem mudou no desenho? Por que “somente supervisor” está incompleto sem teste negativo e telemetria? Qual evidência demonstra reconciliação e qual parte ainda depende de teste contra o ERP real?

Limpeza e conclusão

Quando terminar, remova apenas os artefatos criados por você nesta pasta de laboratório:

Remove-Item -LiteralPath .\lab-output -Recurse
Remove-Item -LiteralPath .\requirements_lab.py

Verifique o caminho antes de executar. Se quiser preservar a evidência para portfólio, mova-a conscientemente e registre ambiente e versão em vez de apagar.

O resultado importante não é o script. É a cadeia explícita: organização e processo fornecem contexto; regras e estados limitam comportamento; fonte de verdade organiza dados; requisitos tornam expectativas verificáveis; riscos exigem controles; testes e telemetria produzem evidência. Essa cadeia é aplicável a uma planilha, API, ERP ou agente de IA.

Teste de fixação

Comprove o que você aprendeu

Responda todas as questões. O gabarito comentado só aparece depois do envio.

1. Qual entrega caracteriza o tracer bullet pedido no caderno?
2. No pré-mortem “o sistema causou um incidente em seis meses”, qual uso é produtivo?
3. O requisito diz “somente supervisor aprova”, mas não existe teste negativo nem telemetria. Qual lacuna permanece?

Consulta universal

O que você quer encontrar?

Títulos, capítulos, conceitos, termos, laboratórios e ferramentas em uma única busca.

Digite pelo menos dois caracteres.