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,295 @@
|
|
|
1
|
+
"""LovarchSupabaseClient — DataPersistenceClient via Supabase REST API.
|
|
2
|
+
|
|
3
|
+
Mirrors LocalSqliteClient but talks to the Lovarch Supabase via PostgREST
|
|
4
|
+
(/rest/v1/{table}) using a user Bearer token managed by LovarchSession
|
|
5
|
+
(auto-refresh on 401).
|
|
6
|
+
|
|
7
|
+
Tables:
|
|
8
|
+
- pm_squad_executions
|
|
9
|
+
- pm_squad_steps
|
|
10
|
+
- pm_squad_qa_checks
|
|
11
|
+
|
|
12
|
+
These are the same tables the Lovarch web app writes to — admin pages at
|
|
13
|
+
/admin/squad-execution/{id}/live read them in real time (Story 1.5 will
|
|
14
|
+
verify CLI executions appear in the dashboard alongside web ones).
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import uuid
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from lovarch_cli.auth.session import LovarchSession, LovarchSessionError
|
|
24
|
+
from lovarch_cli.clients.persistence import (
|
|
25
|
+
DataPersistenceClient,
|
|
26
|
+
Execution,
|
|
27
|
+
ExecutionMode,
|
|
28
|
+
QaCheck,
|
|
29
|
+
QaVerdict,
|
|
30
|
+
Step,
|
|
31
|
+
StepStatus,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _parse_dt(value: str | None) -> datetime | None:
|
|
36
|
+
if not value:
|
|
37
|
+
return None
|
|
38
|
+
# Supabase returns "2026-05-10T12:00:00.123456+00:00" — Python 3.11+ parses
|
|
39
|
+
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _parse_metadata(value: Any) -> dict[str, Any]:
|
|
43
|
+
if not value:
|
|
44
|
+
return {}
|
|
45
|
+
if isinstance(value, dict):
|
|
46
|
+
return value
|
|
47
|
+
if isinstance(value, str):
|
|
48
|
+
try:
|
|
49
|
+
return json.loads(value)
|
|
50
|
+
except json.JSONDecodeError:
|
|
51
|
+
return {}
|
|
52
|
+
return {}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class LovarchSupabaseClient(DataPersistenceClient):
|
|
56
|
+
"""Premium-mode persistence backend via Supabase REST."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, session: LovarchSession) -> None:
|
|
59
|
+
self._session = session
|
|
60
|
+
|
|
61
|
+
# ─── Helper ───────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
async def _post(self, table: str, body: dict[str, Any]) -> dict[str, Any]:
|
|
64
|
+
path = f"/rest/v1/{table}"
|
|
65
|
+
response = await self._session.request(
|
|
66
|
+
"POST",
|
|
67
|
+
path,
|
|
68
|
+
json=body,
|
|
69
|
+
headers={
|
|
70
|
+
"Content-Type": "application/json",
|
|
71
|
+
"Prefer": "return=representation",
|
|
72
|
+
},
|
|
73
|
+
)
|
|
74
|
+
if response.status_code >= 400:
|
|
75
|
+
msg = (
|
|
76
|
+
f"Supabase POST {table} failed: HTTP {response.status_code} "
|
|
77
|
+
f"{response.text[:300]}"
|
|
78
|
+
)
|
|
79
|
+
raise LovarchSessionError(msg)
|
|
80
|
+
data = response.json()
|
|
81
|
+
if isinstance(data, list):
|
|
82
|
+
return data[0] if data else {}
|
|
83
|
+
return data
|
|
84
|
+
|
|
85
|
+
async def _patch(
|
|
86
|
+
self, table: str, filters: dict[str, str], body: dict[str, Any]
|
|
87
|
+
) -> None:
|
|
88
|
+
path = f"/rest/v1/{table}"
|
|
89
|
+
response = await self._session.request(
|
|
90
|
+
"PATCH",
|
|
91
|
+
path,
|
|
92
|
+
params=filters,
|
|
93
|
+
json=body,
|
|
94
|
+
headers={"Content-Type": "application/json"},
|
|
95
|
+
)
|
|
96
|
+
if response.status_code >= 400:
|
|
97
|
+
msg = (
|
|
98
|
+
f"Supabase PATCH {table} failed: HTTP {response.status_code} "
|
|
99
|
+
f"{response.text[:300]}"
|
|
100
|
+
)
|
|
101
|
+
raise LovarchSessionError(msg)
|
|
102
|
+
|
|
103
|
+
async def _get(
|
|
104
|
+
self, table: str, params: dict[str, Any]
|
|
105
|
+
) -> list[dict[str, Any]]:
|
|
106
|
+
path = f"/rest/v1/{table}"
|
|
107
|
+
response = await self._session.request("GET", path, params=params)
|
|
108
|
+
if response.status_code >= 400:
|
|
109
|
+
msg = (
|
|
110
|
+
f"Supabase GET {table} failed: HTTP {response.status_code} "
|
|
111
|
+
f"{response.text[:300]}"
|
|
112
|
+
)
|
|
113
|
+
raise LovarchSessionError(msg)
|
|
114
|
+
data = response.json()
|
|
115
|
+
return data if isinstance(data, list) else []
|
|
116
|
+
|
|
117
|
+
# ─── DataPersistenceClient impl ───────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
async def create_execution(
|
|
120
|
+
self,
|
|
121
|
+
project_id: str,
|
|
122
|
+
workflow: str,
|
|
123
|
+
mode: ExecutionMode,
|
|
124
|
+
metadata: dict[str, Any] | None = None,
|
|
125
|
+
) -> str:
|
|
126
|
+
execution_id = str(uuid.uuid4())
|
|
127
|
+
# Schema reference: pm_squad_executions has columns id, project_id,
|
|
128
|
+
# workflow, status, started_at, completed_at, metadata. CLI rows
|
|
129
|
+
# tagged with metadata.source='cli-{free,premium}' for filtering.
|
|
130
|
+
meta = dict(metadata or {})
|
|
131
|
+
meta.setdefault("source", f"cli-{mode.value}")
|
|
132
|
+
meta.setdefault("cli_user_id", self._session.user_id)
|
|
133
|
+
|
|
134
|
+
await self._post(
|
|
135
|
+
"pm_squad_executions",
|
|
136
|
+
{
|
|
137
|
+
"id": execution_id,
|
|
138
|
+
"project_id": project_id,
|
|
139
|
+
"workflow": workflow,
|
|
140
|
+
"status": "pending",
|
|
141
|
+
"metadata": meta,
|
|
142
|
+
},
|
|
143
|
+
)
|
|
144
|
+
return execution_id
|
|
145
|
+
|
|
146
|
+
async def update_execution(
|
|
147
|
+
self,
|
|
148
|
+
execution_id: str,
|
|
149
|
+
status: str,
|
|
150
|
+
completed_at: datetime | None = None,
|
|
151
|
+
metadata: dict[str, Any] | None = None,
|
|
152
|
+
) -> None:
|
|
153
|
+
body: dict[str, Any] = {"status": status}
|
|
154
|
+
if completed_at is not None:
|
|
155
|
+
body["completed_at"] = completed_at.isoformat(timespec="seconds")
|
|
156
|
+
if metadata is not None:
|
|
157
|
+
body["metadata"] = metadata
|
|
158
|
+
await self._patch(
|
|
159
|
+
"pm_squad_executions",
|
|
160
|
+
{"id": f"eq.{execution_id}"},
|
|
161
|
+
body,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
async def insert_step(
|
|
165
|
+
self,
|
|
166
|
+
execution_id: str,
|
|
167
|
+
agent: str,
|
|
168
|
+
task: str | None,
|
|
169
|
+
status: StepStatus = StepStatus.TRIAGED,
|
|
170
|
+
) -> str:
|
|
171
|
+
step_id = str(uuid.uuid4())
|
|
172
|
+
await self._post(
|
|
173
|
+
"pm_squad_steps",
|
|
174
|
+
{
|
|
175
|
+
"id": step_id,
|
|
176
|
+
"execution_id": execution_id,
|
|
177
|
+
"agent": agent,
|
|
178
|
+
"task": task,
|
|
179
|
+
"status": status.value,
|
|
180
|
+
},
|
|
181
|
+
)
|
|
182
|
+
return step_id
|
|
183
|
+
|
|
184
|
+
async def update_step(
|
|
185
|
+
self,
|
|
186
|
+
step_id: str,
|
|
187
|
+
status: StepStatus,
|
|
188
|
+
artifact_url: str | None = None,
|
|
189
|
+
error: str | None = None,
|
|
190
|
+
metadata: dict[str, Any] | None = None,
|
|
191
|
+
) -> None:
|
|
192
|
+
body: dict[str, Any] = {"status": status.value}
|
|
193
|
+
if status in {StepStatus.DONE, StepStatus.QA_PASS, StepStatus.VALIDATED}:
|
|
194
|
+
body["completed_at"] = datetime.utcnow().isoformat(timespec="seconds")
|
|
195
|
+
if artifact_url is not None:
|
|
196
|
+
body["artifact_url"] = artifact_url
|
|
197
|
+
if error is not None:
|
|
198
|
+
body["error"] = error
|
|
199
|
+
if metadata is not None:
|
|
200
|
+
body["metadata"] = metadata
|
|
201
|
+
await self._patch("pm_squad_steps", {"id": f"eq.{step_id}"}, body)
|
|
202
|
+
|
|
203
|
+
async def insert_qa_check(
|
|
204
|
+
self,
|
|
205
|
+
execution_id: str,
|
|
206
|
+
qa_agent: str,
|
|
207
|
+
target_step_id: str | None,
|
|
208
|
+
verdict: QaVerdict,
|
|
209
|
+
findings: str | None = None,
|
|
210
|
+
) -> str:
|
|
211
|
+
qa_id = str(uuid.uuid4())
|
|
212
|
+
await self._post(
|
|
213
|
+
"pm_squad_qa_checks",
|
|
214
|
+
{
|
|
215
|
+
"id": qa_id,
|
|
216
|
+
"execution_id": execution_id,
|
|
217
|
+
"qa_agent": qa_agent,
|
|
218
|
+
"target_step_id": target_step_id,
|
|
219
|
+
"verdict": verdict.value,
|
|
220
|
+
"findings": findings,
|
|
221
|
+
},
|
|
222
|
+
)
|
|
223
|
+
return qa_id
|
|
224
|
+
|
|
225
|
+
async def get_execution(self, execution_id: str) -> Execution | None:
|
|
226
|
+
rows = await self._get(
|
|
227
|
+
"pm_squad_executions",
|
|
228
|
+
{"id": f"eq.{execution_id}", "select": "*"},
|
|
229
|
+
)
|
|
230
|
+
if not rows:
|
|
231
|
+
return None
|
|
232
|
+
r = rows[0]
|
|
233
|
+
meta = _parse_metadata(r.get("metadata"))
|
|
234
|
+
# Mode is implied by metadata.source='cli-{free,premium}'; default premium
|
|
235
|
+
source = str(meta.get("source", "cli-premium"))
|
|
236
|
+
mode = (
|
|
237
|
+
ExecutionMode.FREE if source.endswith("free") else ExecutionMode.PREMIUM
|
|
238
|
+
)
|
|
239
|
+
return Execution(
|
|
240
|
+
id=r["id"],
|
|
241
|
+
project_id=r["project_id"],
|
|
242
|
+
workflow=r["workflow"],
|
|
243
|
+
mode=mode,
|
|
244
|
+
status=r["status"],
|
|
245
|
+
started_at=_parse_dt(r.get("started_at")) or datetime.utcnow(),
|
|
246
|
+
completed_at=_parse_dt(r.get("completed_at")),
|
|
247
|
+
metadata=meta,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
async def list_steps(self, execution_id: str) -> list[Step]:
|
|
251
|
+
rows = await self._get(
|
|
252
|
+
"pm_squad_steps",
|
|
253
|
+
{
|
|
254
|
+
"execution_id": f"eq.{execution_id}",
|
|
255
|
+
"select": "*",
|
|
256
|
+
"order": "started_at.asc",
|
|
257
|
+
},
|
|
258
|
+
)
|
|
259
|
+
return [
|
|
260
|
+
Step(
|
|
261
|
+
id=r["id"],
|
|
262
|
+
execution_id=r["execution_id"],
|
|
263
|
+
agent=r["agent"],
|
|
264
|
+
task=r.get("task"),
|
|
265
|
+
status=StepStatus(r["status"]),
|
|
266
|
+
started_at=_parse_dt(r.get("started_at")) or datetime.utcnow(),
|
|
267
|
+
completed_at=_parse_dt(r.get("completed_at")),
|
|
268
|
+
artifact_url=r.get("artifact_url"),
|
|
269
|
+
error=r.get("error"),
|
|
270
|
+
metadata=_parse_metadata(r.get("metadata")),
|
|
271
|
+
)
|
|
272
|
+
for r in rows
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
async def list_qa_checks(self, execution_id: str) -> list[QaCheck]:
|
|
276
|
+
rows = await self._get(
|
|
277
|
+
"pm_squad_qa_checks",
|
|
278
|
+
{
|
|
279
|
+
"execution_id": f"eq.{execution_id}",
|
|
280
|
+
"select": "*",
|
|
281
|
+
"order": "checked_at.asc",
|
|
282
|
+
},
|
|
283
|
+
)
|
|
284
|
+
return [
|
|
285
|
+
QaCheck(
|
|
286
|
+
id=r["id"],
|
|
287
|
+
execution_id=r["execution_id"],
|
|
288
|
+
qa_agent=r["qa_agent"],
|
|
289
|
+
target_step_id=r.get("target_step_id"),
|
|
290
|
+
verdict=QaVerdict(r["verdict"]),
|
|
291
|
+
findings=r.get("findings"),
|
|
292
|
+
checked_at=_parse_dt(r.get("checked_at")) or datetime.utcnow(),
|
|
293
|
+
)
|
|
294
|
+
for r in rows
|
|
295
|
+
]
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""DataPersistenceClient — abstract interface for execution tracking.
|
|
2
|
+
|
|
3
|
+
Mirrors the 3 Lovarch tables (pm_squad_executions, pm_squad_steps,
|
|
4
|
+
pm_squad_qa_checks) but lives behind an ABC so the CLI can run against:
|
|
5
|
+
- Free mode: SQLite local (~/.lovarch/local.db)
|
|
6
|
+
- Premium mode: Lovarch Supabase (Epic 3)
|
|
7
|
+
|
|
8
|
+
The methods declared here are the MINIMUM surface needed for the squad
|
|
9
|
+
pipeline to track executions. Lead/CRM/billing methods from the original
|
|
10
|
+
LovarchClient (1.000+ lines, 25 methods) are NOT abstracted — they belong
|
|
11
|
+
to premium mode only and live in lovarch_supabase_extras.py (Epic 3).
|
|
12
|
+
|
|
13
|
+
Status state machine for steps mirrors the squad workflow:
|
|
14
|
+
Triaged → Routed → InProgress → Returned → QA_Pending
|
|
15
|
+
QA_Pending → QA_Pass | QA_Reject
|
|
16
|
+
QA_Pass → Validated → Done
|
|
17
|
+
QA_Reject → InProgress (retry)
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from abc import ABC, abstractmethod
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from datetime import datetime
|
|
24
|
+
from enum import Enum
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class StepStatus(str, Enum):
|
|
29
|
+
"""Step lifecycle states (matches squad workflow contract)."""
|
|
30
|
+
|
|
31
|
+
TRIAGED = "Triaged"
|
|
32
|
+
ROUTED = "Routed"
|
|
33
|
+
IN_PROGRESS = "InProgress"
|
|
34
|
+
RETURNED = "Returned"
|
|
35
|
+
QA_PENDING = "QA_Pending"
|
|
36
|
+
QA_PASS = "QA_Pass"
|
|
37
|
+
QA_REJECT = "QA_Reject"
|
|
38
|
+
VALIDATED = "Validated"
|
|
39
|
+
DONE = "Done"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ExecutionMode(str, Enum):
|
|
43
|
+
"""Backend mode."""
|
|
44
|
+
|
|
45
|
+
FREE = "free"
|
|
46
|
+
PREMIUM = "premium"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class QaVerdict(str, Enum):
|
|
50
|
+
"""QA gate verdicts (mirrors Lovarch convention)."""
|
|
51
|
+
|
|
52
|
+
PASS = "PASS"
|
|
53
|
+
CONCERNS = "CONCERNS"
|
|
54
|
+
FAIL = "FAIL"
|
|
55
|
+
WAIVED = "WAIVED"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class Execution:
|
|
60
|
+
"""Squad execution metadata."""
|
|
61
|
+
|
|
62
|
+
id: str
|
|
63
|
+
project_id: str
|
|
64
|
+
workflow: str
|
|
65
|
+
mode: ExecutionMode
|
|
66
|
+
status: str
|
|
67
|
+
started_at: datetime
|
|
68
|
+
completed_at: datetime | None = None
|
|
69
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class Step:
|
|
74
|
+
"""Single agent step within an execution."""
|
|
75
|
+
|
|
76
|
+
id: str
|
|
77
|
+
execution_id: str
|
|
78
|
+
agent: str
|
|
79
|
+
task: str | None
|
|
80
|
+
status: StepStatus
|
|
81
|
+
started_at: datetime
|
|
82
|
+
completed_at: datetime | None = None
|
|
83
|
+
artifact_url: str | None = None
|
|
84
|
+
error: str | None = None
|
|
85
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True)
|
|
89
|
+
class QaCheck:
|
|
90
|
+
"""QA gate verdict on a step."""
|
|
91
|
+
|
|
92
|
+
id: str
|
|
93
|
+
execution_id: str
|
|
94
|
+
qa_agent: str
|
|
95
|
+
target_step_id: str | None
|
|
96
|
+
verdict: QaVerdict
|
|
97
|
+
findings: str | None
|
|
98
|
+
checked_at: datetime
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class DataPersistenceClient(ABC):
|
|
102
|
+
"""Abstract backend for execution + steps + QA tracking."""
|
|
103
|
+
|
|
104
|
+
@abstractmethod
|
|
105
|
+
async def create_execution(
|
|
106
|
+
self,
|
|
107
|
+
project_id: str,
|
|
108
|
+
workflow: str,
|
|
109
|
+
mode: ExecutionMode,
|
|
110
|
+
metadata: dict[str, Any] | None = None,
|
|
111
|
+
) -> str:
|
|
112
|
+
"""Create an execution row, return its id."""
|
|
113
|
+
|
|
114
|
+
@abstractmethod
|
|
115
|
+
async def update_execution(
|
|
116
|
+
self,
|
|
117
|
+
execution_id: str,
|
|
118
|
+
status: str,
|
|
119
|
+
completed_at: datetime | None = None,
|
|
120
|
+
metadata: dict[str, Any] | None = None,
|
|
121
|
+
) -> None:
|
|
122
|
+
"""Update execution status / completion / metadata."""
|
|
123
|
+
|
|
124
|
+
@abstractmethod
|
|
125
|
+
async def insert_step(
|
|
126
|
+
self,
|
|
127
|
+
execution_id: str,
|
|
128
|
+
agent: str,
|
|
129
|
+
task: str | None,
|
|
130
|
+
status: StepStatus = StepStatus.TRIAGED,
|
|
131
|
+
) -> str:
|
|
132
|
+
"""Insert a new step, return its id."""
|
|
133
|
+
|
|
134
|
+
@abstractmethod
|
|
135
|
+
async def update_step(
|
|
136
|
+
self,
|
|
137
|
+
step_id: str,
|
|
138
|
+
status: StepStatus,
|
|
139
|
+
artifact_url: str | None = None,
|
|
140
|
+
error: str | None = None,
|
|
141
|
+
metadata: dict[str, Any] | None = None,
|
|
142
|
+
) -> None:
|
|
143
|
+
"""Update step status / artifact / error / metadata."""
|
|
144
|
+
|
|
145
|
+
@abstractmethod
|
|
146
|
+
async def insert_qa_check(
|
|
147
|
+
self,
|
|
148
|
+
execution_id: str,
|
|
149
|
+
qa_agent: str,
|
|
150
|
+
target_step_id: str | None,
|
|
151
|
+
verdict: QaVerdict,
|
|
152
|
+
findings: str | None = None,
|
|
153
|
+
) -> str:
|
|
154
|
+
"""Record a QA gate verdict, return its id."""
|
|
155
|
+
|
|
156
|
+
@abstractmethod
|
|
157
|
+
async def get_execution(self, execution_id: str) -> Execution | None:
|
|
158
|
+
"""Fetch execution by id."""
|
|
159
|
+
|
|
160
|
+
@abstractmethod
|
|
161
|
+
async def list_steps(self, execution_id: str) -> list[Step]:
|
|
162
|
+
"""List all steps for an execution, ordered by started_at."""
|
|
163
|
+
|
|
164
|
+
@abstractmethod
|
|
165
|
+
async def list_qa_checks(self, execution_id: str) -> list[QaCheck]:
|
|
166
|
+
"""List all QA verdicts for an execution."""
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""StorageClient — abstract interface for artifact persistence.
|
|
2
|
+
|
|
3
|
+
Two implementations:
|
|
4
|
+
- Free: LocalFilesystemStorage → ~/.lovarch/projects/{execution_id}/
|
|
5
|
+
- Premium: LovarchS3Storage → Lovarch S3 buckets (Epic 3)
|
|
6
|
+
(user-assets, render-images, moodboard-sources)
|
|
7
|
+
|
|
8
|
+
The squad generates 27 deliverables per execution (DXF, IFC, PDF, XLSX, JSON,
|
|
9
|
+
HTML, ZIP). All go through this interface so the same pipeline_runner code
|
|
10
|
+
works in both modes.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from abc import ABC, abstractmethod
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class StoredArtifact:
|
|
22
|
+
"""Reference to a stored artifact."""
|
|
23
|
+
|
|
24
|
+
id: str
|
|
25
|
+
execution_id: str
|
|
26
|
+
filename: str
|
|
27
|
+
url: str
|
|
28
|
+
mime_type: str | None
|
|
29
|
+
size_bytes: int
|
|
30
|
+
created_at: datetime
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class StorageClient(ABC):
|
|
34
|
+
"""Abstract backend for artifact storage (deliverables)."""
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
async def save_artifact(
|
|
38
|
+
self,
|
|
39
|
+
execution_id: str,
|
|
40
|
+
filename: str,
|
|
41
|
+
data: bytes | Path,
|
|
42
|
+
mime_type: str | None = None,
|
|
43
|
+
) -> StoredArtifact:
|
|
44
|
+
"""Save artifact bytes (or copy from Path) and return reference.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
execution_id: scope artifact to this run
|
|
48
|
+
filename: target filename (e.g. 'pianta-progetto.dxf')
|
|
49
|
+
data: raw bytes OR Path to source file
|
|
50
|
+
mime_type: optional MIME type (defaults to guess by extension)
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
async def read_artifact(self, artifact_id_or_url: str) -> bytes:
|
|
55
|
+
"""Read an artifact by id or url and return its bytes."""
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
async def list_artifacts(self, execution_id: str) -> list[StoredArtifact]:
|
|
59
|
+
"""List all artifacts for an execution."""
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
async def delete_artifacts(self, execution_id: str) -> int:
|
|
63
|
+
"""Delete all artifacts for an execution (GDPR right-to-erasure).
|
|
64
|
+
|
|
65
|
+
Returns number of artifacts deleted.
|
|
66
|
+
"""
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""lovarch-cli subcommands.
|
|
2
|
+
|
|
3
|
+
Each command lives in its own module and exposes either:
|
|
4
|
+
- a `cmd()` function (single-action subcommand), OR
|
|
5
|
+
- an `app: typer.Typer` instance (multi-subcommand group)
|
|
6
|
+
|
|
7
|
+
The main CLI (lovarch_cli/cli.py) registers each via add_typer or command.
|
|
8
|
+
|
|
9
|
+
Lazy imports here so heavy commands (audit, run) don't load at startup.
|
|
10
|
+
"""
|