lovarch-cli 0.2.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.
- lovarch_cli/__init__.py +16 -0
- lovarch_cli/__main__.py +10 -0
- lovarch_cli/ai/__init__.py +21 -0
- lovarch_cli/ai/gateway.py +240 -0
- lovarch_cli/api.py +111 -0
- lovarch_cli/auth/__init__.py +32 -0
- lovarch_cli/auth/keyring_store.py +214 -0
- lovarch_cli/auth/local_server.py +165 -0
- lovarch_cli/auth/pkce.py +57 -0
- lovarch_cli/auth/session.py +189 -0
- lovarch_cli/cli.py +262 -0
- lovarch_cli/clients/__init__.py +33 -0
- lovarch_cli/clients/factory.py +54 -0
- lovarch_cli/clients/local_client.py +432 -0
- lovarch_cli/clients/lovarch_storage.py +174 -0
- lovarch_cli/clients/lovarch_supabase.py +295 -0
- lovarch_cli/clients/persistence.py +166 -0
- lovarch_cli/clients/storage.py +66 -0
- lovarch_cli/commands/__init__.py +10 -0
- lovarch_cli/commands/account.py +172 -0
- lovarch_cli/commands/audit.py +394 -0
- lovarch_cli/commands/config_cmd.py +80 -0
- lovarch_cli/commands/consolidate.py +217 -0
- lovarch_cli/commands/context_cmd.py +73 -0
- lovarch_cli/commands/dev.py +287 -0
- lovarch_cli/commands/do_cmd.py +120 -0
- lovarch_cli/commands/init.py +218 -0
- lovarch_cli/commands/jobs_cmd.py +95 -0
- lovarch_cli/commands/login.py +202 -0
- lovarch_cli/commands/mcp_cmd.py +26 -0
- lovarch_cli/commands/run.py +375 -0
- lovarch_cli/commands/signup.py +185 -0
- lovarch_cli/commands/status.py +243 -0
- lovarch_cli/commands/upgrade.py +108 -0
- lovarch_cli/commands/verifica_cmd.py +174 -0
- lovarch_cli/config.py +101 -0
- lovarch_cli/config_store.py +111 -0
- lovarch_cli/credits/__init__.py +35 -0
- lovarch_cli/credits/base.py +84 -0
- lovarch_cli/credits/factory.py +36 -0
- lovarch_cli/credits/local.py +34 -0
- lovarch_cli/credits/lovarch.py +56 -0
- lovarch_cli/i18n/__init__.py +27 -0
- lovarch_cli/i18n/loader.py +121 -0
- lovarch_cli/i18n/translations/en.json +168 -0
- lovarch_cli/i18n/translations/es.json +168 -0
- lovarch_cli/i18n/translations/it.json +168 -0
- lovarch_cli/i18n/translations/pt.json +168 -0
- lovarch_cli/mcp/__init__.py +9 -0
- lovarch_cli/mcp/server.py +199 -0
- lovarch_cli/mcp/tools.py +372 -0
- lovarch_cli/sample_downloader.py +255 -0
- lovarch_cli/squad/README.md +206 -0
- lovarch_cli/squad/agents/auditor-input.md +353 -0
- lovarch_cli/squad/agents/bim-engineer.md +404 -0
- lovarch_cli/squad/agents/briefing-architect.md +249 -0
- lovarch_cli/squad/agents/cad-engineer.md +278 -0
- lovarch_cli/squad/agents/capitolato-writer.md +256 -0
- lovarch_cli/squad/agents/computo-engineer.md +258 -0
- lovarch_cli/squad/agents/concept-designer.md +399 -0
- lovarch_cli/squad/agents/contratto-architect.md +243 -0
- lovarch_cli/squad/agents/deliverable-builder.md +253 -0
- lovarch_cli/squad/agents/energy-prelim.md +388 -0
- lovarch_cli/squad/agents/pratiche-it.md +251 -0
- lovarch_cli/squad/agents/progetto-chief.md +768 -0
- lovarch_cli/squad/agents/quality-dati.md +409 -0
- lovarch_cli/squad/agents/quality-misure.md +418 -0
- lovarch_cli/squad/agents/quality-normativa.md +417 -0
- lovarch_cli/squad/agents/quality-output.md +436 -0
- lovarch_cli/squad/agents/regolatorio-it.md +278 -0
- lovarch_cli/squad/checklists/handoff-quality-gate.md +232 -0
- lovarch_cli/squad/checklists/quality-dati-checklist.md +134 -0
- lovarch_cli/squad/checklists/quality-misure-checklist.md +139 -0
- lovarch_cli/squad/checklists/quality-normativa-checklist.md +121 -0
- lovarch_cli/squad/checklists/quality-output-checklist.md +116 -0
- lovarch_cli/squad/config.yaml +408 -0
- lovarch_cli/squad/data/CHANGELOG.md +272 -0
- lovarch_cli/squad/data/agents-prd.md +428 -0
- lovarch_cli/squad/data/architettura-progetto-rules.md +328 -0
- lovarch_cli/squad/data/handoff-card-template.md +231 -0
- lovarch_cli/squad/data/mocks/catasto-visura.json +72 -0
- lovarch_cli/squad/data/mocks/firma-envelope.json +43 -0
- lovarch_cli/squad/data/prezzario-lombardia-sample.json +312 -0
- lovarch_cli/squad/scripts/api_clients.py +206 -0
- lovarch_cli/squad/scripts/architect_profile.py +276 -0
- lovarch_cli/squad/scripts/deliverable_generators.py +844 -0
- lovarch_cli/squad/scripts/generate_attico_brera_dwg.py +369 -0
- lovarch_cli/squad/scripts/generate_chianti_dxf.py +368 -0
- lovarch_cli/squad/scripts/generate_chianti_images.py +223 -0
- lovarch_cli/squad/scripts/generate_real_sample_images.py +189 -0
- lovarch_cli/squad/scripts/generate_sample_assets.py +382 -0
- lovarch_cli/squad/scripts/lovarch_client.py +1046 -0
- lovarch_cli/squad/scripts/pipeline_runner.py +2095 -0
- lovarch_cli/squad/scripts/render_dxf_to_png.py +57 -0
- lovarch_cli/squad/scripts/run_palestra_demo.sh +277 -0
- lovarch_cli/squad/scripts/simulate_squad_execution.py +515 -0
- lovarch_cli/squad/scripts/validate-squad.py +383 -0
- lovarch_cli/squad/tasks/audit-input.md +146 -0
- lovarch_cli/squad/tasks/compute-metric.md +105 -0
- lovarch_cli/squad/tasks/consolidate-dossier.md +187 -0
- lovarch_cli/squad/tasks/generate-cad-plan.md +120 -0
- lovarch_cli/squad/tasks/generate-ifc-model.md +108 -0
- lovarch_cli/squad/tasks/write-capitolato.md +100 -0
- lovarch_cli/squad/templates/asseverazione-tecnica.md +126 -0
- lovarch_cli/squad/templates/capitolato-uni-11337.md +235 -0
- lovarch_cli/squad/templates/cila-comune-milano.md +177 -0
- lovarch_cli/squad/templates/contratto-cnappc.md +220 -0
- lovarch_cli/squad/workflows/dal-brief-al-cantiere.yaml +218 -0
- lovarch_cli/squad_loader.py +114 -0
- lovarch_cli/verify/__init__.py +15 -0
- lovarch_cli/verify/contratto.py +110 -0
- lovarch_cli/verify/dossier.py +97 -0
- lovarch_cli/verify/misure.py +83 -0
- lovarch_cli/verify/normativa.py +178 -0
- lovarch_cli/version.py +13 -0
- lovarch_cli/workflows/__init__.py +9 -0
- lovarch_cli/workflows/platform.py +212 -0
- lovarch_cli-0.2.1.dist-info/METADATA +232 -0
- lovarch_cli-0.2.1.dist-info/RECORD +122 -0
- lovarch_cli-0.2.1.dist-info/WHEEL +4 -0
- lovarch_cli-0.2.1.dist-info/entry_points.txt +3 -0
- lovarch_cli-0.2.1.dist-info/licenses/LICENSE +38 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simulate Squad Architettura-Progetto execution by inserting realistic steps
|
|
3
|
+
into Supabase tables progressively.
|
|
4
|
+
|
|
5
|
+
Use cases:
|
|
6
|
+
1. Test the /admin/squad-execution/:id/live page (Realtime)
|
|
7
|
+
2. Backup demo if real squad execution fails on stage
|
|
8
|
+
3. Demo previews / dev iteration
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
# 1. Set env vars or write secrets in ~/.lovarch/secrets.env:
|
|
12
|
+
# SUPABASE_URL=https://xxx.supabase.co
|
|
13
|
+
# SUPABASE_SERVICE_ROLE_KEY=eyJ...
|
|
14
|
+
#
|
|
15
|
+
# 2. Run:
|
|
16
|
+
python3 simulate_squad_execution.py
|
|
17
|
+
python3 simulate_squad_execution.py --speed 2.0 # 2x faster
|
|
18
|
+
python3 simulate_squad_execution.py --user-id <uuid>
|
|
19
|
+
python3 simulate_squad_execution.py --dry-run
|
|
20
|
+
|
|
21
|
+
The simulator creates 1 execution + ~30 steps + ~20 QA checks across 14 minutes
|
|
22
|
+
(or compressed by --speed factor). It mimics the dal-brief-al-cantiere workflow
|
|
23
|
+
including 1 deliberate QA REJECT + retry to demonstrate the loop.
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
import random
|
|
31
|
+
import sys
|
|
32
|
+
import time
|
|
33
|
+
import urllib.error
|
|
34
|
+
import urllib.request
|
|
35
|
+
import uuid
|
|
36
|
+
from dataclasses import dataclass, field
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from typing import Any, Dict, List, Optional
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
SECRETS_FILE = Path.home() / ".lovarch" / "secrets.env"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_secrets() -> Dict[str, str]:
|
|
45
|
+
secrets: Dict[str, str] = {}
|
|
46
|
+
if SECRETS_FILE.exists():
|
|
47
|
+
for line in SECRETS_FILE.read_text(encoding="utf-8").splitlines():
|
|
48
|
+
line = line.strip()
|
|
49
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
50
|
+
continue
|
|
51
|
+
k, v = line.split("=", 1)
|
|
52
|
+
secrets[k.strip()] = v.strip()
|
|
53
|
+
return secrets
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
_SECRETS = load_secrets()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_env(name: str, fallback: Optional[str] = None) -> Optional[str]:
|
|
60
|
+
return os.environ.get(name) or _SECRETS.get(name) or fallback
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
SUPABASE_URL = get_env("SUPABASE_URL")
|
|
64
|
+
SUPABASE_KEY = get_env("SUPABASE_SERVICE_ROLE_KEY") or get_env("SUPABASE_SERVICE_KEY")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ----------------------------------------------------------------------------
|
|
68
|
+
# Supabase REST client (no SDK · stdlib only)
|
|
69
|
+
# ----------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
class SupabaseRest:
|
|
72
|
+
def __init__(self, url: str, key: str):
|
|
73
|
+
self.url = url.rstrip("/")
|
|
74
|
+
self.headers = {
|
|
75
|
+
"apikey": key,
|
|
76
|
+
"Authorization": f"Bearer {key}",
|
|
77
|
+
"Content-Type": "application/json",
|
|
78
|
+
"Prefer": "return=representation",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
def insert(self, table: str, row: dict) -> dict:
|
|
82
|
+
return self._req("POST", f"/rest/v1/{table}", body=row)
|
|
83
|
+
|
|
84
|
+
def update(self, table: str, where: str, patch: dict) -> dict:
|
|
85
|
+
return self._req("PATCH", f"/rest/v1/{table}?{where}", body=patch)
|
|
86
|
+
|
|
87
|
+
def select(self, table: str, where: str = "select=*") -> list:
|
|
88
|
+
result = self._req("GET", f"/rest/v1/{table}?{where}")
|
|
89
|
+
return result if isinstance(result, list) else [result]
|
|
90
|
+
|
|
91
|
+
def _req(self, method: str, path: str, body: Optional[dict] = None) -> Any:
|
|
92
|
+
url = f"{self.url}{path}"
|
|
93
|
+
data = json.dumps(body).encode("utf-8") if body is not None else None
|
|
94
|
+
req = urllib.request.Request(url, data=data, method=method, headers=self.headers)
|
|
95
|
+
try:
|
|
96
|
+
with urllib.request.urlopen(req, timeout=15) as r:
|
|
97
|
+
raw = r.read().decode("utf-8") or "[]"
|
|
98
|
+
return json.loads(raw)
|
|
99
|
+
except urllib.error.HTTPError as e:
|
|
100
|
+
err = e.read().decode("utf-8", errors="replace")
|
|
101
|
+
raise RuntimeError(f"Supabase {method} {path} failed [{e.code}]: {err}") from e
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ----------------------------------------------------------------------------
|
|
105
|
+
# Step definitions · simulating the workflow
|
|
106
|
+
# ----------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
@dataclass
|
|
109
|
+
class StepDef:
|
|
110
|
+
agent: str
|
|
111
|
+
tier: int
|
|
112
|
+
step_type: str
|
|
113
|
+
duration: float
|
|
114
|
+
action: str
|
|
115
|
+
output_files: List[Dict[str, Any]] = field(default_factory=list)
|
|
116
|
+
qa_outcome: Optional[str] = None # "PASS" | "REJECT"
|
|
117
|
+
qa_checks: List[Dict[str, Any]] = field(default_factory=list)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _file(name: str, path: str, size_kb: int, mime: str = "application/pdf") -> dict:
|
|
121
|
+
return {
|
|
122
|
+
"name": name,
|
|
123
|
+
"path": path,
|
|
124
|
+
"size": size_kb * 1024,
|
|
125
|
+
"mime": mime,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# Workflow simulation · 28 steps + 1 QA REJECT + retry
|
|
130
|
+
WORKFLOW: List[StepDef] = [
|
|
131
|
+
# TIER 0 · ORCHESTRAZIONE
|
|
132
|
+
StepDef("@progetto-chief", 0, "orchestration", 4, "init: caricato briefing-cliente.md (1.4k parole)"),
|
|
133
|
+
StepDef("@auditor-input", 0, "orchestration", 12, "validazione 18 items input · PASS",
|
|
134
|
+
output_files=[_file("input_validation.json", "00-validation/", 2, "application/json")]),
|
|
135
|
+
|
|
136
|
+
# TIER 1 · ESECUZIONE PARALLELA
|
|
137
|
+
StepDef("@briefing-architect", 1, "execution", 28, "brief strutturato 12 sezioni UNI 11337",
|
|
138
|
+
output_files=[
|
|
139
|
+
_file("brief-strutturato.pdf", "01-briefing/", 412),
|
|
140
|
+
_file("requisiti.json", "01-briefing/", 4, "application/json"),
|
|
141
|
+
_file("programma-spaziale.xlsx", "01-briefing/", 22, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
|
|
142
|
+
]),
|
|
143
|
+
StepDef("@regolatorio-it", 1, "execution", 38, "tipo pratica = CILA · paesaggistica DPR 31/2017 procedura semplificata",
|
|
144
|
+
output_files=[
|
|
145
|
+
_file("analisi-regolamentare.pdf", "01-briefing/", 624),
|
|
146
|
+
_file("tipo-pratica.json", "01-briefing/", 3, "application/json"),
|
|
147
|
+
_file("vincoli.json", "01-briefing/", 2, "application/json"),
|
|
148
|
+
]),
|
|
149
|
+
StepDef("@cad-engineer", 1, "execution", 95, "pianta-progetto.dxf · 8 ambienti · cotazioni UNI ISO 5457",
|
|
150
|
+
output_files=[
|
|
151
|
+
_file("pianta-stato-attuale.dxf", "03-progetto-definitivo/", 78, "application/dxf"),
|
|
152
|
+
_file("pianta-stato-attuale.pdf", "03-progetto-definitivo/", 312),
|
|
153
|
+
_file("pianta-progetto.dxf", "03-progetto-definitivo/", 96, "application/dxf"),
|
|
154
|
+
_file("pianta-progetto.pdf", "03-progetto-definitivo/", 387),
|
|
155
|
+
_file("sezione-AA.pdf", "03-progetto-definitivo/", 218),
|
|
156
|
+
_file("prospetti.pdf", "03-progetto-definitivo/", 442),
|
|
157
|
+
_file("schema-quotato.json", "03-progetto-definitivo/", 5, "application/json"),
|
|
158
|
+
]),
|
|
159
|
+
StepDef("@bim-engineer", 1, "execution", 78, "modello.ifc LOD 300 · viewer APS embeddable",
|
|
160
|
+
output_files=[
|
|
161
|
+
_file("modello.ifc", "03-progetto-definitivo/", 1834, "application/x-step"),
|
|
162
|
+
_file("thumbnail-3d.png", "03-progetto-definitivo/", 412, "image/png"),
|
|
163
|
+
_file("quantitativi.json", "03-progetto-definitivo/", 8, "application/json"),
|
|
164
|
+
]),
|
|
165
|
+
StepDef("@concept-designer", 1, "execution", 168,
|
|
166
|
+
"moodboard 9 immagini + 6 render FLUX 1.1 Pro + palette + tipografia",
|
|
167
|
+
output_files=[
|
|
168
|
+
_file("moodboard.pdf", "02-concept/", 8420),
|
|
169
|
+
_file("living-moderno-a.png", "02-concept/renders/", 4280, "image/png"),
|
|
170
|
+
_file("living-moderno-b.png", "02-concept/renders/", 4156, "image/png"),
|
|
171
|
+
_file("cucina-moderna-a.png", "02-concept/renders/", 4350, "image/png"),
|
|
172
|
+
_file("cucina-moderna-b.png", "02-concept/renders/", 4120, "image/png"),
|
|
173
|
+
_file("camera-sofia-a.png", "02-concept/renders/", 3980, "image/png"),
|
|
174
|
+
_file("camera-sofia-b.png", "02-concept/renders/", 4042, "image/png"),
|
|
175
|
+
_file("palette-progetto.pdf", "02-concept/", 187),
|
|
176
|
+
_file("tipografia.pdf", "02-concept/", 142),
|
|
177
|
+
]),
|
|
178
|
+
StepDef("@computo-engineer", 1, "execution", 52,
|
|
179
|
+
"computo metrico estimativo · 124 voci Prezzario Lombardia · totale €180.000",
|
|
180
|
+
output_files=[
|
|
181
|
+
_file("computo-metrico.xlsx", "05-impresa/", 240, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
|
|
182
|
+
_file("computo-metrico.pdf", "05-impresa/", 524),
|
|
183
|
+
_file("quadro-economico.pdf", "05-impresa/", 142),
|
|
184
|
+
_file("lista-materiali-EPDs.xlsx", "05-impresa/", 98, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
|
|
185
|
+
]),
|
|
186
|
+
StepDef("@capitolato-writer", 1, "execution", 78,
|
|
187
|
+
"capitolato speciale 78 pp · UNI 11337-7 + CAM 2025 · cronoprogramma 90gg",
|
|
188
|
+
output_files=[
|
|
189
|
+
_file("capitolato-speciale.pdf", "05-impresa/", 3420),
|
|
190
|
+
_file("cronoprogramma-90gg.pdf", "05-impresa/", 412),
|
|
191
|
+
_file("lista-CAM-rispettati.xlsx", "05-impresa/", 76, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
|
|
192
|
+
]),
|
|
193
|
+
StepDef("@pratiche-it", 1, "execution", 42,
|
|
194
|
+
"CILA precompilata + asseverazione + paesaggistica + 6 elaborati",
|
|
195
|
+
output_files=[
|
|
196
|
+
_file("CILA-precompilata.pdf", "04-pratiche-comune/", 512),
|
|
197
|
+
_file("asseverazione-bozza.pdf", "04-pratiche-comune/", 189),
|
|
198
|
+
_file("paesaggistica-bozza.pdf", "04-pratiche-comune/", 734),
|
|
199
|
+
_file("relazione-paesaggistica.pdf", "04-pratiche-comune/", 412),
|
|
200
|
+
_file("documentazione-fotografica.pdf", "04-pratiche-comune/", 12340),
|
|
201
|
+
]),
|
|
202
|
+
StepDef("@contratto-architect", 1, "execution", 22,
|
|
203
|
+
"contratto CNAPPC + preventivo + privacy GDPR · onorari €22.000",
|
|
204
|
+
output_files=[
|
|
205
|
+
_file("contratto-servizi.pdf", "07-cliente/", 412),
|
|
206
|
+
_file("preventivo-onorari.pdf", "07-cliente/", 298),
|
|
207
|
+
_file("informativa-privacy-GDPR.pdf", "07-cliente/", 156),
|
|
208
|
+
]),
|
|
209
|
+
StepDef("@energy-prelim", 1, "execution", 36,
|
|
210
|
+
"APE preliminare · classe stimata B → A · LCA embodied carbon",
|
|
211
|
+
output_files=[
|
|
212
|
+
_file("APE-stima-preliminare.pdf", "06-ingegneri/", 682),
|
|
213
|
+
_file("LCA-embodied-carbon.pdf", "06-ingegneri/", 934),
|
|
214
|
+
_file("trasmittanze-pareti.xlsx", "06-ingegneri/", 42, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
|
|
215
|
+
]),
|
|
216
|
+
StepDef("@deliverable-builder", 1, "execution", 32,
|
|
217
|
+
"presentazione HTML V8 + portale cliente + DOSSIER.zip",
|
|
218
|
+
output_files=[
|
|
219
|
+
_file("presentazione-cliente.html", "07-cliente/", 1240, "text/html"),
|
|
220
|
+
_file("timeline-90gg-cliente.pdf", "07-cliente/", 187),
|
|
221
|
+
_file("DOSSIER-IMPRESA.zip", "05-impresa/", 15240, "application/zip"),
|
|
222
|
+
_file("scheda-progetto.json", "08-studio-interno/", 8, "application/json"),
|
|
223
|
+
_file("cash-flow-proiezione.xlsx", "08-studio-interno/", 42, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
|
|
224
|
+
_file("task-list-team.json", "08-studio-interno/", 12, "application/json"),
|
|
225
|
+
_file("social-instagram.json", "08-studio-interno/", 28, "application/json"),
|
|
226
|
+
]),
|
|
227
|
+
|
|
228
|
+
# TIER 2 · QA · 1 deliberate REJECT to show the loop
|
|
229
|
+
StepDef("@quality-misure", 2, "qa", 18, "verifica 24 items · 5/5 critici PASS · 9/11 secondari PASS",
|
|
230
|
+
qa_outcome="PASS",
|
|
231
|
+
qa_checks=[
|
|
232
|
+
{"check_id": "C1", "severity": "CRITICO", "description": "Somma quote orizzontali", "result": True},
|
|
233
|
+
{"check_id": "C2", "severity": "CRITICO", "description": "Somma quote verticali", "result": True},
|
|
234
|
+
{"check_id": "C3", "severity": "CRITICO", "description": "Sup utile = somma ambienti", "result": True},
|
|
235
|
+
{"check_id": "C4", "severity": "CRITICO", "description": "Sup lorda = utile + muratura", "result": True},
|
|
236
|
+
{"check_id": "C5", "severity": "CRITICO", "description": "Volume = sup × altezza", "result": True},
|
|
237
|
+
]),
|
|
238
|
+
StepDef("@quality-normativa", 2, "qa", 22, "verifica 18 items · DPR 380 art 6-bis OK · CAM 2025 89% rispettato",
|
|
239
|
+
qa_outcome="PASS",
|
|
240
|
+
qa_checks=[
|
|
241
|
+
{"check_id": "N-C1", "severity": "CRITICO", "description": "Tipo pratica corretto", "result": True},
|
|
242
|
+
{"check_id": "N-C2", "severity": "CRITICO", "description": "Articoli DPR 380 esistono", "result": True},
|
|
243
|
+
{"check_id": "N-C3", "severity": "CRITICO", "description": "Aut paesaggistica considerata", "result": True},
|
|
244
|
+
{"check_id": "N-C4", "severity": "CRITICO", "description": "CAM Edilizia 2025 ≥80%", "result": True},
|
|
245
|
+
]),
|
|
246
|
+
StepDef("@quality-dati", 2, "qa", 26,
|
|
247
|
+
"REJECT · Volume parete IFC 18.5 m² ≠ computo 19.2 m² (diff 3.7%)",
|
|
248
|
+
qa_outcome="REJECT",
|
|
249
|
+
qa_checks=[
|
|
250
|
+
{"check_id": "D-C1", "severity": "CRITICO", "description": "Sup lorda pianta=IFC=CILA", "result": True},
|
|
251
|
+
{"check_id": "D-C2", "severity": "CRITICO", "description": "Volumi parete IFC = computo", "result": False,
|
|
252
|
+
"expected_value": "18.5 m²", "actual_value": "19.2 m²", "diff": "+0.7 m² (3.7%)"},
|
|
253
|
+
{"check_id": "D-C3", "severity": "CRITICO", "description": "Totale computo=capitolato=contratto", "result": True},
|
|
254
|
+
]),
|
|
255
|
+
|
|
256
|
+
# RETRY · @computo-engineer corrige
|
|
257
|
+
StepDef("@computo-engineer", 1, "execution", 12, "RETRY 1/3 · correzione voce muratura demolizione (18.5 m²)",
|
|
258
|
+
output_files=[
|
|
259
|
+
_file("computo-metrico.xlsx", "05-impresa/", 240, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
|
|
260
|
+
]),
|
|
261
|
+
|
|
262
|
+
# QA RE-RUN
|
|
263
|
+
StepDef("@quality-dati", 2, "qa", 14, "RE-VERIFY · 16/16 items · PASS",
|
|
264
|
+
qa_outcome="PASS",
|
|
265
|
+
qa_checks=[
|
|
266
|
+
{"check_id": "D-C2", "severity": "CRITICO", "description": "Volumi parete IFC = computo (re-check)", "result": True,
|
|
267
|
+
"expected_value": "18.5 m²", "actual_value": "18.5 m²"},
|
|
268
|
+
]),
|
|
269
|
+
StepDef("@quality-output", 2, "qa", 16, "verifica 14 items · 27 deliverable · tutti i PDF aprono · upload Lovarch OK",
|
|
270
|
+
qa_outcome="PASS",
|
|
271
|
+
qa_checks=[
|
|
272
|
+
{"check_id": "O-C1", "severity": "CRITICO", "description": "25+ deliverable presenti", "result": True,
|
|
273
|
+
"actual_value": "27"},
|
|
274
|
+
{"check_id": "O-C2", "severity": "CRITICO", "description": "PDF aprono senza errore", "result": True,
|
|
275
|
+
"actual_value": "18/18"},
|
|
276
|
+
{"check_id": "O-C6", "severity": "CRITICO", "description": "File uploaded → Lovarch", "result": True,
|
|
277
|
+
"actual_value": "27/27"},
|
|
278
|
+
]),
|
|
279
|
+
|
|
280
|
+
# TIER 0 · CONSOLIDATION
|
|
281
|
+
StepDef("@progetto-chief", 0, "orchestration", 8, "consolidamento dossier · git commit · open Finder",
|
|
282
|
+
output_files=[
|
|
283
|
+
_file("README.md", "", 4, "text/markdown"),
|
|
284
|
+
_file("manifest.json", "", 6, "application/json"),
|
|
285
|
+
]),
|
|
286
|
+
]
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# ----------------------------------------------------------------------------
|
|
290
|
+
# Simulator
|
|
291
|
+
# ----------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
def run_simulation(
|
|
294
|
+
*,
|
|
295
|
+
user_id: str,
|
|
296
|
+
project_id: Optional[str] = None,
|
|
297
|
+
speed: float = 1.0,
|
|
298
|
+
dry_run: bool = False,
|
|
299
|
+
sb: Optional[SupabaseRest] = None,
|
|
300
|
+
) -> Dict[str, Any]:
|
|
301
|
+
"""Run the simulation, inserting rows progressively."""
|
|
302
|
+
execution_id = str(uuid.uuid4())
|
|
303
|
+
|
|
304
|
+
if dry_run:
|
|
305
|
+
print(f"[DRY-RUN] would create execution {execution_id} with {len(WORKFLOW)} steps")
|
|
306
|
+
print(f"[DRY-RUN] estimated total duration (real): {sum(s.duration for s in WORKFLOW):.0f}s "
|
|
307
|
+
f"({sum(s.duration for s in WORKFLOW) / speed:.0f}s with speed={speed})")
|
|
308
|
+
return {"execution_id": execution_id, "dry_run": True}
|
|
309
|
+
|
|
310
|
+
if sb is None:
|
|
311
|
+
raise ValueError("SupabaseRest client required when not dry_run")
|
|
312
|
+
|
|
313
|
+
# 1. Create execution
|
|
314
|
+
print(f"\n▶ Creating execution {execution_id[:8]}...")
|
|
315
|
+
exec_row = {
|
|
316
|
+
"id": execution_id,
|
|
317
|
+
"user_id": user_id,
|
|
318
|
+
"project_id": project_id,
|
|
319
|
+
"status": "running",
|
|
320
|
+
"metadata": {
|
|
321
|
+
"simulator": True,
|
|
322
|
+
"scenario": "Attico Brera demo",
|
|
323
|
+
"client": "Marco Rossini & Giulia Bianchi",
|
|
324
|
+
},
|
|
325
|
+
}
|
|
326
|
+
sb.insert("pm_squad_executions", exec_row)
|
|
327
|
+
|
|
328
|
+
# 2. AUTO-OPEN live tracking page in browser (so Pablo can watch real-time)
|
|
329
|
+
base_url = os.environ.get("LOVARCH_WEB_URL", "https://lovarch.com")
|
|
330
|
+
live_url = f"{base_url}/admin/squad-execution/{execution_id}/live"
|
|
331
|
+
print(f" Opening live tracking page: {live_url}")
|
|
332
|
+
try:
|
|
333
|
+
import webbrowser
|
|
334
|
+
webbrowser.open(live_url, new=2) # new=2 = new tab if possible
|
|
335
|
+
except Exception as e:
|
|
336
|
+
print(f" (could not auto-open browser: {e} · open manually)")
|
|
337
|
+
|
|
338
|
+
# 3. Iterate steps
|
|
339
|
+
total_steps = 0
|
|
340
|
+
total_qa_rejects = 0
|
|
341
|
+
total_retries = 0
|
|
342
|
+
|
|
343
|
+
for i, step_def in enumerate(WORKFLOW, start=1):
|
|
344
|
+
step_id = str(uuid.uuid4())
|
|
345
|
+
|
|
346
|
+
# Insert pending step
|
|
347
|
+
step_row = {
|
|
348
|
+
"id": step_id,
|
|
349
|
+
"execution_id": execution_id,
|
|
350
|
+
"agent_name": step_def.agent,
|
|
351
|
+
"tier": step_def.tier,
|
|
352
|
+
"step_type": step_def.step_type,
|
|
353
|
+
"status": "pending",
|
|
354
|
+
"action_description": step_def.action,
|
|
355
|
+
}
|
|
356
|
+
sb.insert("pm_squad_steps", step_row)
|
|
357
|
+
|
|
358
|
+
# Mark as running
|
|
359
|
+
time.sleep(0.5 / speed)
|
|
360
|
+
started_at_ms = int(time.time() * 1000)
|
|
361
|
+
sb.update(
|
|
362
|
+
"pm_squad_steps",
|
|
363
|
+
f"id=eq.{step_id}",
|
|
364
|
+
{
|
|
365
|
+
"status": "running",
|
|
366
|
+
"started_at": _ts(),
|
|
367
|
+
},
|
|
368
|
+
)
|
|
369
|
+
print(f" [{i:2d}/{len(WORKFLOW)}] {step_def.agent:<25} {step_def.action[:60]}...", end="", flush=True)
|
|
370
|
+
|
|
371
|
+
# Wait simulated duration
|
|
372
|
+
time.sleep(step_def.duration / speed)
|
|
373
|
+
|
|
374
|
+
# Determine final status
|
|
375
|
+
if step_def.qa_outcome == "REJECT":
|
|
376
|
+
final_status = "rejected"
|
|
377
|
+
total_qa_rejects += 1
|
|
378
|
+
else:
|
|
379
|
+
final_status = "done"
|
|
380
|
+
|
|
381
|
+
# If this is a retry step, mark with retry_count
|
|
382
|
+
retry_count = 1 if "RETRY" in step_def.action.upper() else 0
|
|
383
|
+
if retry_count:
|
|
384
|
+
total_retries += 1
|
|
385
|
+
|
|
386
|
+
# Compute duration
|
|
387
|
+
elapsed = (int(time.time() * 1000) - started_at_ms) / 1000
|
|
388
|
+
|
|
389
|
+
sb.update(
|
|
390
|
+
"pm_squad_steps",
|
|
391
|
+
f"id=eq.{step_id}",
|
|
392
|
+
{
|
|
393
|
+
"status": final_status,
|
|
394
|
+
"completed_at": _ts(),
|
|
395
|
+
"duration_seconds": int(elapsed),
|
|
396
|
+
"output_files": step_def.output_files,
|
|
397
|
+
"qa_report": {"verdict": step_def.qa_outcome} if step_def.qa_outcome else None,
|
|
398
|
+
"retry_count": retry_count,
|
|
399
|
+
},
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
# Insert QA checks if applicable
|
|
403
|
+
if step_def.qa_checks:
|
|
404
|
+
for chk in step_def.qa_checks:
|
|
405
|
+
sb.insert("pm_squad_qa_checks", {
|
|
406
|
+
"step_id": step_id,
|
|
407
|
+
"execution_id": execution_id,
|
|
408
|
+
"qa_agent": step_def.agent,
|
|
409
|
+
"check_id": chk["check_id"],
|
|
410
|
+
"check_description": chk["description"],
|
|
411
|
+
"severity": chk["severity"],
|
|
412
|
+
"result": chk["result"],
|
|
413
|
+
"expected_value": chk.get("expected_value"),
|
|
414
|
+
"actual_value": chk.get("actual_value"),
|
|
415
|
+
"diff": chk.get("diff"),
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
total_steps += 1
|
|
419
|
+
print(f" {final_status.upper()}")
|
|
420
|
+
|
|
421
|
+
# 4. Mark execution as completed
|
|
422
|
+
sb.update(
|
|
423
|
+
"pm_squad_executions",
|
|
424
|
+
f"id=eq.{execution_id}",
|
|
425
|
+
{
|
|
426
|
+
"status": "completed",
|
|
427
|
+
"completed_at": _ts(),
|
|
428
|
+
"total_steps": total_steps,
|
|
429
|
+
"total_qa_rejects": total_qa_rejects,
|
|
430
|
+
"total_retries": total_retries,
|
|
431
|
+
},
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
# 5. AUTO-OPEN dossier page (final deliverables) when complete
|
|
435
|
+
dossier_url = f"{base_url}/admin/squad-execution/{execution_id}/dossier"
|
|
436
|
+
print(f"\n✓ Execution complete: {execution_id}")
|
|
437
|
+
print(f" Total steps: {total_steps}")
|
|
438
|
+
print(f" QA rejects: {total_qa_rejects} (loop demonstrato)")
|
|
439
|
+
print(f" Retries: {total_retries}")
|
|
440
|
+
print(f"\n Opening dossier page: {dossier_url}")
|
|
441
|
+
try:
|
|
442
|
+
import webbrowser
|
|
443
|
+
webbrowser.open(dossier_url, new=2)
|
|
444
|
+
except Exception:
|
|
445
|
+
pass # already opened live, dossier is bonus
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
"execution_id": execution_id,
|
|
449
|
+
"user_id": user_id,
|
|
450
|
+
"total_steps": total_steps,
|
|
451
|
+
"total_qa_rejects": total_qa_rejects,
|
|
452
|
+
"total_retries": total_retries,
|
|
453
|
+
"live_url": f"/admin/squad-execution/{execution_id}/live",
|
|
454
|
+
"dossier_url": f"/admin/squad-execution/{execution_id}/dossier",
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _ts() -> str:
|
|
459
|
+
"""ISO timestamp in UTC."""
|
|
460
|
+
from datetime import datetime, timezone
|
|
461
|
+
return datetime.now(timezone.utc).isoformat()
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
# ----------------------------------------------------------------------------
|
|
465
|
+
# CLI
|
|
466
|
+
# ----------------------------------------------------------------------------
|
|
467
|
+
|
|
468
|
+
def main():
|
|
469
|
+
parser = argparse.ArgumentParser(description="Simulate squad execution")
|
|
470
|
+
parser.add_argument("--user-id", default=None, help="Supabase user UUID (defaults to env SUPABASE_USER_ID)")
|
|
471
|
+
parser.add_argument("--project-id", default=None, help="Project UUID (optional)")
|
|
472
|
+
parser.add_argument("--speed", type=float, default=1.0, help="Speed multiplier (1.0 = real time, 5.0 = 5× faster)")
|
|
473
|
+
parser.add_argument("--dry-run", action="store_true", help="Print plan without inserting")
|
|
474
|
+
args = parser.parse_args()
|
|
475
|
+
|
|
476
|
+
if args.dry_run:
|
|
477
|
+
result = run_simulation(
|
|
478
|
+
user_id=args.user_id or "00000000-0000-0000-0000-000000000000",
|
|
479
|
+
project_id=args.project_id,
|
|
480
|
+
speed=args.speed,
|
|
481
|
+
dry_run=True,
|
|
482
|
+
)
|
|
483
|
+
print(json.dumps(result, indent=2))
|
|
484
|
+
return
|
|
485
|
+
|
|
486
|
+
if not SUPABASE_URL or not SUPABASE_KEY:
|
|
487
|
+
sys.stderr.write(
|
|
488
|
+
"ERROR: SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY missing.\n"
|
|
489
|
+
"Add to ~/.lovarch/secrets.env or env vars.\n"
|
|
490
|
+
)
|
|
491
|
+
sys.exit(1)
|
|
492
|
+
|
|
493
|
+
user_id = args.user_id or get_env("SUPABASE_USER_ID")
|
|
494
|
+
if not user_id:
|
|
495
|
+
sys.stderr.write(
|
|
496
|
+
"ERROR: --user-id required (or set SUPABASE_USER_ID).\n"
|
|
497
|
+
"Get yours: SELECT id FROM auth.users WHERE email='pablo@archprime.io';\n"
|
|
498
|
+
)
|
|
499
|
+
sys.exit(1)
|
|
500
|
+
|
|
501
|
+
sb = SupabaseRest(SUPABASE_URL, SUPABASE_KEY)
|
|
502
|
+
result = run_simulation(
|
|
503
|
+
user_id=user_id,
|
|
504
|
+
project_id=args.project_id,
|
|
505
|
+
speed=args.speed,
|
|
506
|
+
sb=sb,
|
|
507
|
+
)
|
|
508
|
+
base_url = os.environ.get("LOVARCH_WEB_URL", "https://lovarch.com")
|
|
509
|
+
print(f"\nLive tracking: {base_url}{result['live_url']}")
|
|
510
|
+
print(f"Dossier: {base_url}{result['dossier_url']}")
|
|
511
|
+
print("\n(Browser was auto-opened on these pages during execution)")
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
if __name__ == "__main__":
|
|
515
|
+
main()
|