actrova 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
actrova/__init__.py ADDED
@@ -0,0 +1,65 @@
1
+ # -*- coding: utf-8 -*-
2
+ """ACTROVA — assurance de execução para agentes.
3
+
4
+ Autorize a ação. Prove o resultado.
5
+
6
+ O que esta biblioteca faz, em uma frase: separa **o que o agente disse que
7
+ fez** de **o que a fonte de verdade confirma que aconteceu**, e guarda a
8
+ diferença numa cadeia de recibos em que **toda alteração deixa marca**.
9
+
10
+ ⚠️ "Deixa marca" e não "é impossível": ver o limite honesto em `recibo.py`.
11
+
12
+ from actrova import Escopo
13
+
14
+ escopo = Escopo(politicas="politicas", dados="dados")
15
+
16
+ @escopo.guarda(agente="jarvis.ceo", acao="source.disable", alvos="fontes")
17
+ def desabilitar(fontes): ...
18
+
19
+ ⚠️ Padrão é `observe`: registra tudo, não interrompe nada.
20
+ """
21
+ from .nucleo import (AcaoBloqueada, Alcance, Assegura, CanarioInterrompido,
22
+ Cobertura, Contagem, comparar_cobertura,
23
+ Custo, Decisao, Divergencia, Efeito, EscopoDaAfirmacao,
24
+ Estado, Evidencia, Finalidade, IndependenciaDaFonte,
25
+ Intencao, Modo, Prova, Relacao, SemEfeito, TipoCusto,
26
+ Veredito, comparar_efeito, impressao,
27
+ nova_execucao, normalizar_custo,
28
+ normalizar_evidencias)
29
+ from .contrato import (Contrato, ContratoInvalido, Execucao,
30
+ EvidenciaExigida, carregar, carregar_diretorio)
31
+ from .avaliador import Avaliador, AvaliadorSimples, validar_ordem
32
+ from .recibo import CadeiaQuebrada, Livro, Recibo, renderizar
33
+ from .verificacao import (Fila, PlanoDeVerificacao, Registro,
34
+ Verificador)
35
+ from .guarda import OMITIDO, Escopo, portao_de_evidencia
36
+ from .maturidade import prontidao, relatorio
37
+ from .evento import (Evento, EventSink, SinkDeArquivos, SinkEmMemoria,
38
+ novo_id)
39
+ from .trabalhador import Trabalhador
40
+ from .conformidade import CENARIOS, Relatorio, conformar
41
+
42
+ __version__ = "0.0.1"
43
+
44
+ __all__ = [
45
+ "Escopo", "AcaoBloqueada", "CanarioInterrompido", "Decisao", "Efeito",
46
+ "Estado", "Evidencia", "Execucao", "Intencao", "SemEfeito", "Contagem",
47
+ "Custo", "TipoCusto", "normalizar_custo",
48
+ "Relacao", "Divergencia", "comparar_efeito", "nova_execucao",
49
+ # os três eixos de QUANTO a prova prova — ver `Assegura`
50
+ "Assegura", "EscopoDaAfirmacao", "IndependenciaDaFonte",
51
+ "Finalidade",
52
+ # prova de QUÊ, e o que ficou fora — ver `Alcance`
53
+ "Cobertura", "Alcance", "comparar_cobertura",
54
+ "Modo", "Prova", "Veredito", "Contrato", "ContratoInvalido",
55
+ "EvidenciaExigida", "carregar", "carregar_diretorio", "Avaliador",
56
+ "AvaliadorSimples", "validar_ordem", "CadeiaQuebrada", "Livro", "Recibo",
57
+ "renderizar", "Fila", "PlanoDeVerificacao", "Registro",
58
+ "Verificador", "portao_de_evidencia",
59
+ "impressao", "normalizar_evidencias", "prontidao", "relatorio",
60
+ "conformar", "CENARIOS", "Relatorio",
61
+ "Evento", "EventSink", "SinkEmMemoria", "SinkDeArquivos", "novo_id",
62
+ "Trabalhador",
63
+ "OMITIDO",
64
+ "__version__",
65
+ ]
actrova/__main__.py ADDED
@@ -0,0 +1,392 @@
1
+ # -*- coding: utf-8 -*-
2
+ """__main__.py -- a suíte de conformidade na mão de quem não escreveu a Actrova.
3
+
4
+ ⚠️ POR QUE ISTO EXISTE.
5
+
6
+ A `conformar()` já sabia testar o verificador de um estranho. Só que para
7
+ CHEGAR nela o estranho precisava importar o pacote, instanciar a classe certa,
8
+ montar um dicionário de cenários aninhado e descobrir sozinho que
9
+ `Estado.INVERIFICAVEL` é o que se espera quando a fonte cai.
10
+
11
+ 📌 **Isso é uma barreira de dez minutos antes do primeiro resultado, e a
12
+ pergunta que a Actrova precisa responder — "outro desenvolvedor de agentes tem
13
+ esse problema?" — morre nesses dez minutos.** Ninguém escreve um arquivo de
14
+ fixtures para um produto que ainda não sabe se serve.
15
+
16
+ O comando existe para inverter a ordem: **primeiro o resultado sobre o código
17
+ DELE, depois o convite.**
18
+
19
+ python3 -m actrova conformar meu_modulo.py:MeuVerificador
20
+
21
+ ⚠️ E rodar SEM cenário nenhum é um caminho de primeira classe, não um erro de
22
+ uso. A saída mostra o que já dá para afirmar sem fixture (as invariantes do
23
+ caminho de erro), mostra o que falta, e **se recusa a certificar** — porque a
24
+ pergunta que decide tudo é a única que a suíte não consegue simular:
25
+
26
+ o seu `_consultar` LEVANTA quando a fonte cai, ou devolve {}?
27
+ """
28
+ from __future__ import annotations
29
+
30
+ import importlib
31
+ import importlib.util
32
+ import json
33
+ import sys
34
+ from pathlib import Path
35
+
36
+ from .conformidade import CENARIOS, OBRIGATORIO, conformar
37
+
38
+ # ── códigos de saída ──────────────────────────────────────────────────────
39
+ # ⚠️ 1 e 2 são DIFERENTES de propósito. "violou uma invariante" e "não foi
40
+ # perguntado" são coisas distintas, e a CI de quem usa isto precisa poder
41
+ # tratá-las diferente. Colapsar as duas em "falhou" repete, no shell, o mesmo
42
+ # achatamento que o projeto inteiro existe para desfazer.
43
+ SAIDA_CONFORME = 0
44
+ SAIDA_NAO_CONFORME = 1
45
+ SAIDA_SEM_CERTIFICADO = 2
46
+ SAIDA_ERRO_DE_USO = 3
47
+
48
+ USO = """\
49
+ actrova — assurance de execução para agentes
50
+
51
+ python3 -m actrova iniciar NOME esqueleto rodável em 10 minutos
52
+ python3 -m actrova conformar ALVO [--cenarios FONTE]
53
+ python3 -m actrova exemplo [> cenarios.py]
54
+
55
+ ALVO onde mora o seu verificador, no formato `onde:Nome`
56
+ meu_modulo.py:MeuVerificador caminho de arquivo
57
+ meu_pacote.verificadores:Stripe módulo importável
58
+ `Nome` pode ser a classe (instanciada sem argumentos), uma
59
+ instância já pronta, ou uma função que devolve uma.
60
+
61
+ --cenarios as fixtures que só você consegue montar, porque só você sabe que
62
+ contexto faz a SUA fonte responder cada coisa.
63
+ cenarios.py:CENARIOS dicionário Python (recomendado)
64
+ cenarios.json se os contextos forem só JSON
65
+
66
+ Sem `--cenarios` o comando roda assim mesmo e diz o que já dá para afirmar.
67
+ Ele só não emite certificado — `não testado não é aprovado`.
68
+
69
+ saída 0 conforme sob as fixtures fornecidas
70
+ 1 NÃO conforme — alguma invariante foi violada
71
+ 2 sem certificado — algo não foi testado
72
+ 3 erro de uso
73
+ """
74
+
75
+ MODELO = '''\
76
+ # -*- coding: utf-8 -*-
77
+ """cenarios.py -- as três situações que só VOCÊ consegue montar.
78
+
79
+ A suíte testa sozinha o caminho de erro (exceção, tipo errado, asserção
80
+ vazia). O que ela não consegue descobrir de fora é como a SUA fonte se
81
+ comporta — e é aí que mora o bug que originou a Actrova.
82
+
83
+ Cada cenário é o par `{contexto, espera}` que você passaria ao verificador:
84
+ `contexto` é o que o executor sabe sobre a ação, `espera` é o que o
85
+ contrato afirmou que deveria ser verdade.
86
+ """
87
+
88
+ CENARIOS = {
89
+ # a fonte confirma que a ação aconteceu → VERIFIED (ou PARTIAL)
90
+ "sucesso": {
91
+ "contexto": {},
92
+ "espera": {},
93
+ },
94
+
95
+ # a fonte responde, e diz que NÃO aconteceu → FAILED (ou PARTIAL)
96
+ "divergente": {
97
+ "contexto": {},
98
+ "espera": {},
99
+ },
100
+
101
+ # ⚠️ O QUE DECIDE TUDO. A fonte está fora: rede caída, 500, timeout.
102
+ #
103
+ # Aponte o contexto para algo que faça o SEU `_consultar` falhar de
104
+ # verdade: host inexistente, porta fechada, token vazio.
105
+ #
106
+ # ⚠️ Não simule devolvendo {} — o ponto é justamente descobrir se o
107
+ # seu código distingue "não consegui consultar" de "consultei e não
108
+ # tem nada". Fingir a queda da fonte é testar a fixture, não o código.
109
+ #
110
+ # Esperado: UNVERIFIABLE. Nunca VERIFIED, nunca FAILED.
111
+ "fonte_fora": {
112
+ "contexto": {},
113
+ "espera": {},
114
+ },
115
+ }
116
+ '''
117
+
118
+
119
+ def _iniciar(argumentos: list) -> int:
120
+ """Escreve um projeto que RODA antes de ser editado.
121
+
122
+ ⚠️ POR QUE A ORDEM IMPORTA. Dois pilotos disseram sim e ficaram parados
123
+ porque o primeiro passo era *"escreva um `Verificador`"* — e ninguém
124
+ escreve uma classe abstrata de um produto que ainda não viu funcionar.
125
+ **Pedia-se fé antes de resultado.**
126
+
127
+ 📌 O esqueleto usa uma fonte de mentira de propósito: a pessoa vê a FORMA
128
+ do recibo, com cadeia real, antes de ligar no sistema dela. Depois troca
129
+ um método só."""
130
+ from . import _modelos
131
+
132
+ nome = (argumentos[0] if argumentos else "verificador").strip()
133
+ if not nome.replace("_", "").isalnum() or nome[0].isdigit():
134
+ return _erro(f"{nome!r} não serve como nome de módulo Python — "
135
+ f"use letras, números e `_`, sem começar com número")
136
+ resto = argumentos[1:]
137
+ destino = Path(resto[1]).expanduser() if len(resto) > 1 and \
138
+ resto[0] == "--em" else Path.cwd()
139
+
140
+ classe = "Verificador" + "".join(
141
+ p.capitalize() for p in nome.split("_") if p) or "VerificadorNovo"
142
+
143
+ arquivos = {
144
+ destino / "politicas" / "contrato.yaml": _modelos.CONTRATO,
145
+ destino / f"{nome}.py": _modelos.VERIFICADOR.format(
146
+ nome=nome, classe=classe),
147
+ destino / "app.py": _modelos.APP.format(nome=nome, classe=classe),
148
+ }
149
+
150
+ # ⚠️ NÃO SOBRESCREVE NADA. Escrever por cima do código de alguém para
151
+ # "facilitar o começo" é exatamente o tipo de ajuda que ninguém pediu.
152
+ existentes = [a for a in arquivos if a.exists()]
153
+ if existentes:
154
+ return _erro("estes arquivos já existem e eu não vou por cima:\n "
155
+ + "\n ".join(str(a) for a in existentes))
156
+
157
+ for caminho, conteudo in arquivos.items():
158
+ caminho.parent.mkdir(parents=True, exist_ok=True)
159
+ caminho.write_text(conteudo, encoding="utf-8")
160
+
161
+ print(f"""✅ escrito em {destino}
162
+
163
+ politicas/contrato.yaml o que a ação pode fazer, e como provar
164
+ {nome}.py{' ' * max(1, 25 - len(nome) - 3)}o verificador — troque o `_consultar`
165
+ app.py ⚠️ RODE ISTO AGORA, antes de editar
166
+
167
+ python3 app.py
168
+
169
+ Ele usa uma fonte de mentira de propósito. Você vê a forma do recibo,
170
+ com cadeia real, antes de ligar no seu sistema.
171
+
172
+ 📌 Depois de trocar o `_consultar` pela sua fonte:
173
+
174
+ python3 -m actrova conformar {nome}.py:{classe}
175
+
176
+ Isso diz se o seu código transforma "a fonte caiu" em "a fonte disse
177
+ que não" — que é o bug que este produto existe para pegar.
178
+ """)
179
+ return SAIDA_CONFORME
180
+
181
+
182
+ def _erro(msg: str) -> int:
183
+ print(f"❌ {msg}", file=sys.stderr)
184
+ return SAIDA_ERRO_DE_USO
185
+
186
+
187
+ def _carregar(alvo: str):
188
+ """`arquivo.py:Nome` ou `pacote.modulo:Nome` → o objeto.
189
+
190
+ ⚠️ O caminho de arquivo existe porque é o que a pessoa tem na mão. Exigir
191
+ módulo importável é exigir que ela entenda `sys.path` antes de ver
192
+ qualquer resultado, e ninguém paga esse pedágio por curiosidade.
193
+ """
194
+ if ":" not in alvo:
195
+ raise ValueError(
196
+ f"falta o `:Nome` em {alvo!r} — o formato é `onde:Nome`, por "
197
+ f"exemplo `{alvo}:MeuVerificador`")
198
+ onde, _, nome = alvo.rpartition(":")
199
+ if not onde or not nome:
200
+ raise ValueError(f"não entendi {alvo!r} — o formato é `onde:Nome`")
201
+
202
+ if onde.endswith(".py") or "/" in onde or "\\" in onde:
203
+ caminho = Path(onde).expanduser().resolve()
204
+ if not caminho.is_file():
205
+ raise ValueError(f"não achei o arquivo {caminho}")
206
+ # ⚠️ O diretório do arquivo entra no path ANTES de importar: um
207
+ # verificador quase sempre importa vizinhos dele.
208
+ sys.path.insert(0, str(caminho.parent))
209
+ spec = importlib.util.spec_from_file_location(caminho.stem, caminho)
210
+ modulo = importlib.util.module_from_spec(spec)
211
+ sys.modules[caminho.stem] = modulo
212
+ spec.loader.exec_module(modulo)
213
+ else:
214
+ modulo = importlib.import_module(onde)
215
+
216
+ if not hasattr(modulo, nome):
217
+ publicos = [n for n in vars(modulo) if not n.startswith("_")]
218
+ raise ValueError(
219
+ f"`{nome}` não existe em {onde}. Achei: "
220
+ + (", ".join(sorted(publicos)[:12]) or "nada público"))
221
+ return getattr(modulo, nome)
222
+
223
+
224
+ def _instanciar(obj, alvo: str):
225
+ """Classe → instância; instância → ela mesma; fábrica → o que devolver."""
226
+ if isinstance(obj, type):
227
+ try:
228
+ return obj()
229
+ except TypeError as e:
230
+ raise ValueError(
231
+ f"`{alvo}` é uma classe que não instancia sem argumentos "
232
+ f"({e}). Exponha uma instância já pronta ou uma função sem "
233
+ f"argumentos que devolva uma, e aponte para ela.") from e
234
+ if callable(obj) and not hasattr(obj, "verificar"):
235
+ return obj()
236
+ return obj
237
+
238
+
239
+ def _cenarios(fonte: str) -> dict:
240
+ if fonte.endswith(".json"):
241
+ caminho = Path(fonte).expanduser().resolve()
242
+ if not caminho.is_file():
243
+ raise ValueError(f"não achei o arquivo {caminho}")
244
+ dados = json.loads(caminho.read_text(encoding="utf-8"))
245
+ else:
246
+ dados = _carregar(fonte)
247
+ if callable(dados):
248
+ dados = dados()
249
+
250
+ if not isinstance(dados, dict):
251
+ raise ValueError(
252
+ f"os cenários precisam ser um dicionário "
253
+ f"`{{nome: {{contexto, espera}}}}`, e vieram {type(dados).__name__}")
254
+
255
+ # ⚠️ NOME ERRADO TEM QUE GRITAR. `"fonte-fora"` com hífen, ou
256
+ # `"fonte_fora "` com espaço, hoje cairia silenciosamente em "não
257
+ # fornecido" — e a pessoa leria `SEM CERTIFICADO` achando que escreveu a
258
+ # fixture mais importante. Ausência disfarçada de presença é, de novo, a
259
+ # doença da casa.
260
+ desconhecidos = sorted(set(dados) - set(CENARIOS))
261
+ if desconhecidos:
262
+ raise ValueError(
263
+ f"cenário(s) que a suíte não conhece: "
264
+ f"{', '.join(repr(d) for d in desconhecidos)}. "
265
+ f"Os nomes são exatamente: {', '.join(sorted(CENARIOS))}")
266
+
267
+ for nome, c in dados.items():
268
+ if not isinstance(c, dict) or not set(c) <= {"contexto", "espera"}:
269
+ raise ValueError(
270
+ f"o cenário {nome!r} precisa ser "
271
+ f"`{{'contexto': {{...}}, 'espera': {{...}}}}`")
272
+ return dados
273
+
274
+
275
+ def _conselho(rel, fornecidos) -> list:
276
+ """O que fazer a seguir, em vez de só o veredito.
277
+
278
+ ⚠️ Um relatório que diz `NÃO CONFORME` e para por aí transfere para quem
279
+ está de fora o trabalho de descobrir o que a Actrova já sabe.
280
+ """
281
+ linhas = []
282
+ faltando = [n for n in CENARIOS if n not in fornecidos]
283
+ if OBRIGATORIO in faltando:
284
+ linhas += [
285
+ "",
286
+ " 📌 PRÓXIMO PASSO — a pergunta que decide tudo ainda não foi feita:",
287
+ "",
288
+ " o seu `_consultar` LEVANTA quando a fonte cai,",
289
+ " ou devolve {} ?",
290
+ "",
291
+ " De fora, um `{}` de falha é idêntico a um `{}` de fonte que",
292
+ " respondeu vazio. Essa confusão exata é o bug que originou",
293
+ " este projeto, e é a única coisa que a suíte não consegue",
294
+ " descobrir sozinha.",
295
+ "",
296
+ " python3 -m actrova exemplo > cenarios.py",
297
+ " python3 -m actrova conformar ALVO --cenarios cenarios.py:CENARIOS",
298
+ ]
299
+ elif faltando:
300
+ linhas += [
301
+ "",
302
+ f" 📌 falta(m) o(s) cenário(s): {', '.join(faltando)}",
303
+ ]
304
+ if rel.reprovados:
305
+ linhas += [
306
+ "",
307
+ " 📌 cada ❌ acima é um caminho pelo qual o seu verificador pode",
308
+ " afirmar mais do que a fonte disse. Não é estilo — é a",
309
+ " diferença entre um recibo que vale e um que mente.",
310
+ ]
311
+ return linhas
312
+
313
+
314
+ def _conformar(argumentos: list) -> int:
315
+ if not argumentos:
316
+ return _erro("falta o ALVO. Exemplo:\n"
317
+ " python3 -m actrova conformar meu_modulo.py:MeuVerificador")
318
+
319
+ alvo, fonte = argumentos[0], None
320
+ resto = argumentos[1:]
321
+ while resto:
322
+ if resto[0] in ("--cenarios", "--cenários"):
323
+ if len(resto) < 2:
324
+ return _erro("`--cenarios` precisa de um valor "
325
+ "(`cenarios.py:CENARIOS` ou `cenarios.json`)")
326
+ fonte, resto = resto[1], resto[2:]
327
+ else:
328
+ return _erro(f"não conheço a opção {resto[0]!r}")
329
+
330
+ try:
331
+ verificador = _instanciar(_carregar(alvo), alvo)
332
+ except ValueError as e:
333
+ return _erro(str(e))
334
+ except Exception as e: # noqa: BLE001
335
+ return _erro(f"não consegui carregar {alvo!r} — "
336
+ f"{type(e).__name__}: {e}")
337
+
338
+ if not hasattr(verificador, "verificar"):
339
+ return _erro(
340
+ f"`{alvo}` não parece um Verificador: não tem `verificar()`.\n"
341
+ f" Ele precisa herdar de `actrova.Verificador` — é a classe base "
342
+ f"que impõe\n a invariante, e herdar dela é metade do que esta "
343
+ f"suíte confere.")
344
+
345
+ try:
346
+ fixtures = _cenarios(fonte) if fonte else {}
347
+ except ValueError as e:
348
+ return _erro(str(e))
349
+ except Exception as e: # noqa: BLE001
350
+ return _erro(f"não consegui ler os cenários — {type(e).__name__}: {e}")
351
+
352
+ relatorio = conformar(verificador, fixtures)
353
+ print(relatorio.texto())
354
+ for linha in _conselho(relatorio, set(fixtures)):
355
+ print(linha)
356
+ print()
357
+
358
+ if relatorio.conforme:
359
+ return SAIDA_CONFORME
360
+ if relatorio.reprovados:
361
+ return SAIDA_NAO_CONFORME
362
+ return SAIDA_SEM_CERTIFICADO
363
+
364
+
365
+ def principal(argumentos: list | None = None) -> int:
366
+ argumentos = list(sys.argv[1:] if argumentos is None else argumentos)
367
+
368
+ # ⚠️ O diretório de onde a pessoa chamou entra no path. Sem isto,
369
+ # `meu_pacote.verificadores:X` só funciona para quem já instalou o
370
+ # próprio projeto — e a primeira tentativa de todo mundo é de dentro da
371
+ # pasta do projeto, sem instalar nada.
372
+ if "" not in sys.path and str(Path.cwd()) not in sys.path:
373
+ sys.path.insert(0, str(Path.cwd()))
374
+
375
+ if not argumentos or argumentos[0] in ("-h", "--help", "ajuda"):
376
+ print(USO)
377
+ return SAIDA_CONFORME
378
+
379
+ comando, resto = argumentos[0], argumentos[1:]
380
+ if comando == "iniciar":
381
+ return _iniciar(resto)
382
+ if comando == "conformar":
383
+ return _conformar(resto)
384
+ if comando == "exemplo":
385
+ print(MODELO)
386
+ return SAIDA_CONFORME
387
+ return _erro(f"não conheço o comando {comando!r}. "
388
+ f"São três: `iniciar`, `conformar` e `exemplo`.")
389
+
390
+
391
+ if __name__ == "__main__":
392
+ sys.exit(principal())
actrova/_modelos.py ADDED
@@ -0,0 +1,162 @@
1
+ # -*- coding: utf-8 -*-
2
+ """_modelos.py -- o esqueleto que `actrova iniciar` escreve.
3
+
4
+ ⚠️ POR QUE ISTO EXISTE, E POR QUE ELE RODA ANTES DE SER EDITADO.
5
+
6
+ Dois pilotos disseram sim e ficaram parados porque o primeiro passo era
7
+ *"escreva um `Verificador`"* — e ninguém escreve uma classe abstrata de um
8
+ produto que ainda não viu funcionar. **A ordem estava invertida: pedia-se fé
9
+ antes de resultado.**
10
+
11
+ 📌 Por isso o esqueleto é RODÁVEL no instante em que é escrito. Ele produz um
12
+ recibo de verdade, com cadeia de verdade, contra uma fonte de mentira — e a
13
+ pessoa vê a forma da coisa antes de ligar no sistema dela. Depois ela troca a
14
+ fonte de mentira pela real, que é um método só.
15
+ """
16
+
17
+ CONTRATO = """\
18
+ # O contrato declara O QUE a ação pode fazer e COMO provar que fez.
19
+ # ⚠️ Ele é lido por quem NÃO escreveu o código — por isso YAML, e não Python.
20
+
21
+ agente: meu.agente
22
+ acao: pedido.cancelar
23
+ versao: v1
24
+ autoridade: L2
25
+
26
+ # ⚠️ `observe` NÃO bloqueia nada. Registra o veredito e deixa a ação seguir.
27
+ # É assim que se descobre quais políticas importam antes de deixar uma delas
28
+ # parar a operação de alguém. Só vire `enforce` depois de semanas de recibo.
29
+ modo: observe
30
+
31
+ regras:
32
+ - id: lote_grande
33
+ se: {campo: quantidade, op: ">", valor: 10}
34
+ entao: HOLD
35
+ motivo: >-
36
+ cancelar mais de 10 pedidos de uma vez precisa de gente olhando.
37
+
38
+ verificacao:
39
+ verificador: meu.verificador
40
+ # ⚠️ Quem confere, contra o quê, e por quanto tempo insiste. Sem isto,
41
+ # `PENDING` é uma promessa que ninguém consegue cobrar.
42
+ espera:
43
+ status: cancelado
44
+ tentativas: 3
45
+ # ⚠️ A PRIMEIRA ESPERA É 0 de propósito: tenta na hora, e só então recua.
46
+ # Verificação quase nunca é síncrona — reembolso liquida depois, e-mail
47
+ # entrega depois — mas insistir com espera crescente só faz sentido DEPOIS
48
+ # de uma primeira tentativa que pode muito bem já responder.
49
+ espera_segundos: [0, 30, 120]
50
+ """
51
+
52
+ VERIFICADOR = '''\
53
+ # -*- coding: utf-8 -*-
54
+ """{nome}.py -- prova que a ação aconteceu, perguntando a quem sabe.
55
+
56
+ ⚠️ A ÚNICA REGRA QUE IMPORTA AQUI, e ela é a razão de este produto existir:
57
+
58
+ `_consultar` DEVE LEVANTAR quando não conseguir falar com a fonte.
59
+
60
+ Devolver `{{}}` numa falha faz "não consegui perguntar" virar "perguntei e não
61
+ tem nada" — e essas duas coisas levam a decisões opostas. Foi assim que um
62
+ agente real desabilitou 36 fontes de conteúdo porque uma consulta ao banco
63
+ falhou e o código leu vazio como zero.
64
+
65
+ 📌 Se você levantar, a classe base devolve INVERIFICAVEL. Se você engolir,
66
+ ela não tem como saber, e vai devolver FALHOU com toda a confiança do mundo.
67
+ """
68
+ from actrova import Verificador
69
+
70
+
71
+ class FonteIndisponivel(Exception):
72
+ """Não deu para falar com a fonte de verdade."""
73
+
74
+
75
+ class {classe}(Verificador):
76
+ nome = "meu.verificador"
77
+
78
+ def _consultar(self, contexto: dict) -> dict:
79
+ """Vá à fonte REAL e devolva o que ela disse.
80
+
81
+ ⚠️ TROQUE ESTE CORPO pelo seu banco, sua API, seu CRM. O que não muda
82
+ é o `raise`: qualquer falha de comunicação tem que subir como exceção.
83
+
84
+ resposta = requests.get(..., timeout=10)
85
+ resposta.raise_for_status() # ← NÃO engula isto
86
+ return resposta.json()
87
+ """
88
+ # --- fonte de mentira, só para o esqueleto rodar. Troque. ---
89
+ #
90
+ # 📌 `contexto` é o que o executor sabe sobre ESTA ação. Por padrão ele
91
+ # traz `alvos` — a lista que a sua função recebeu. É com ela que você
92
+ # vai perguntar à fonte real sobre as entidades CERTAS.
93
+ alvos = contexto.get("alvos") or []
94
+ if not alvos:
95
+ # ⚠️ Repare que isto LEVANTA em vez de devolver vazio. Sem alvo
96
+ # não há o que perguntar, e responder mesmo assim seria inventar.
97
+ raise FonteIndisponivel(
98
+ "o contexto não trouxe `alvos` — sem eles não há o que "
99
+ "perguntar à fonte")
100
+ return {{"pedidos": list(alvos), "status": "cancelado"}}
101
+
102
+ # ⚠️ OPCIONAL: implemente `_contar` se a sua ação mexe em LOTE. Quem
103
+ # implementa ganha `PARCIAL` de graça — "3 de 5 confirmados" — e o estado
104
+ # é DERIVADO dos números, nunca escolhido à mão.
105
+ #
106
+ # def _contar(self, observado, espera):
107
+ # from actrova import Contagem
108
+ # return Contagem(pedidos=..., confirmados=..., falhos=...)
109
+
110
+ # ⚠️ OPCIONAL: implemente `_efeito_observado` para responder a pergunta
111
+ # seguinte — "aconteceu exatamente o que foi AUTORIZADO?". Devolva
112
+ # (dicionário canônico, "complete" | "partial").
113
+ #
114
+ # `complete` significa "esta é a lista INTEIRA do que existe". Diga
115
+ # `partial` se a fonte paginou ou filtrou: é isso que decide se a AUSÊNCIA
116
+ # de um alvo pode ser afirmada.
117
+ #
118
+ # def _efeito_observado(self, observado, contexto):
119
+ # return {{"pedidos": observado["cancelados"]}}, "complete"
120
+ '''
121
+
122
+ APP = '''\
123
+ # -*- coding: utf-8 -*-
124
+ """app.py -- rode isto AGORA, antes de editar qualquer coisa.
125
+
126
+ python3 app.py
127
+
128
+ ⚠️ Ele usa uma fonte de mentira de propósito. A ideia é você ver a FORMA do
129
+ recibo antes de ligar no seu sistema — e só depois trocar o `_consultar`.
130
+ """
131
+ from actrova import Escopo, renderizar
132
+ from {nome} import {classe}
133
+
134
+ escopo = Escopo(politicas="politicas", dados="dados")
135
+ escopo.registrar_verificador({classe}())
136
+
137
+
138
+ @escopo.guarda(agente="meu.agente", acao="pedido.cancelar", alvos="pedidos")
139
+ def cancelar(pedidos):
140
+ """A SUA função. A ACTROVA não muda o que ela faz — só registra."""
141
+ return {{"ok": True, "cancelados": pedidos}}
142
+
143
+
144
+ if __name__ == "__main__":
145
+ cancelar(["pedido-8821"])
146
+
147
+ # ⚠️ A verificação NÃO acontece junto com a ação, e isso é deliberado:
148
+ # reembolso leva tempo para liquidar, e-mail leva tempo para entregar.
149
+ # Num sistema real, isto roda num laço, de tempos em tempos.
150
+ escopo.processar_verificacoes()
151
+
152
+ for recibo in escopo.livro.ler():
153
+ print(renderizar(recibo))
154
+ print("-" * 70)
155
+
156
+ integra, problemas = escopo.livro.verificar_cadeia()
157
+ print(f"cadeia íntegra: {{integra}} {{problemas or ''}}")
158
+ print()
159
+ print("📌 Agora troque o `_consultar` de {nome}.py pela sua fonte real.")
160
+ print(" E rode `python3 -m actrova conformar {nome}.py:{classe}`")
161
+ print(" para conferir se ele mente quando a fonte cai.")
162
+ '''