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 +65 -0
- actrova/__main__.py +392 -0
- actrova/_modelos.py +162 -0
- actrova/avaliador.py +172 -0
- actrova/conformidade.py +307 -0
- actrova/contrato.py +356 -0
- actrova/evento.py +183 -0
- actrova/guarda.py +736 -0
- actrova/maturidade.py +143 -0
- actrova/nucleo.py +1040 -0
- actrova/recibo.py +613 -0
- actrova/trabalhador.py +108 -0
- actrova/trava.py +76 -0
- actrova/verificacao.py +658 -0
- actrova-0.0.1.dist-info/METADATA +341 -0
- actrova-0.0.1.dist-info/RECORD +21 -0
- actrova-0.0.1.dist-info/WHEEL +5 -0
- actrova-0.0.1.dist-info/entry_points.txt +2 -0
- actrova-0.0.1.dist-info/licenses/LICENSE +202 -0
- actrova-0.0.1.dist-info/top_level.txt +2 -0
- escopo/__init__.py +55 -0
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
|
+
'''
|