Caderno operacional — descoberta e tracer bullet
Este laboratório transforma requisitos em um pacote executável para a aprovação de uma ordem de manutenção integrada ao ERP. Primeiro, o modo defeituoso provoca seis classes de falha: qualidade vaga, regra sem dono, fontes de verdade concorrentes, transição inválida, integração sem idempotência e rastreabilidade sem teste ou telemetria. Em seguida, o modo de prova valida um pacote corrigido e executa um tracer bullet vertical: supervisor autorizado aprova, técnico é negado sem alterar estado e uma indisponibilidade do ERP deixa evento pendente para reconciliação sem duplicidade. O resultado é uma evidência JSON inspecionável, acompanhada de pré-mortem, diagnóstico, critérios de aceite e exercícios de mutação.
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:
- o pacote de requisitos contém informação suficiente para ser verificado?
- uma tentativa não autorizada deixa o estado intacto?
- 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
$LASTEXITCODEResultado 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->APPROVEDHá 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-01nã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-01aponta um risco, porém não possui teste negativo nem telemetria;QR-01nem 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.jsonResultado 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
badretorna2e localiza regra, qualidade, estado, propriedade, integração e rastreabilidade; - o modo
proveretorna0sem pacotes externos; - uma pessoa sem papel de supervisor recebe 403 e não muda estado, versão nem outbox;
- supervisor da mesma unidade leva
PENDINGaAPPROVED; - falha do ERP preserva um evento pendente;
- retry e replay produzem somente um efeito no fake ERP;
evidence.jsonregistra os resultados relevantes;- cada requisito em
GOOD_PACKAGEliga 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
- Troque a unidade do supervisor para
SOUTH. Escreva a asserção que prova 403 e estado intacto. - Remova
environmentdeQR-01; preveja o achado antes de executar. - Faça
event_idincluir 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”. - Adicione o estado
CANCELLEDe prove queCANCELLED → APPROVEDfalha. - 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. - 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.pyVerifique 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.
Comprove o que você aprendeu
Responda todas as questões. O gabarito comentado só aparece depois do envio.