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.
Files changed (122) hide show
  1. lovarch_cli/__init__.py +16 -0
  2. lovarch_cli/__main__.py +10 -0
  3. lovarch_cli/ai/__init__.py +21 -0
  4. lovarch_cli/ai/gateway.py +240 -0
  5. lovarch_cli/api.py +111 -0
  6. lovarch_cli/auth/__init__.py +32 -0
  7. lovarch_cli/auth/keyring_store.py +214 -0
  8. lovarch_cli/auth/local_server.py +165 -0
  9. lovarch_cli/auth/pkce.py +57 -0
  10. lovarch_cli/auth/session.py +189 -0
  11. lovarch_cli/cli.py +262 -0
  12. lovarch_cli/clients/__init__.py +33 -0
  13. lovarch_cli/clients/factory.py +54 -0
  14. lovarch_cli/clients/local_client.py +432 -0
  15. lovarch_cli/clients/lovarch_storage.py +174 -0
  16. lovarch_cli/clients/lovarch_supabase.py +295 -0
  17. lovarch_cli/clients/persistence.py +166 -0
  18. lovarch_cli/clients/storage.py +66 -0
  19. lovarch_cli/commands/__init__.py +10 -0
  20. lovarch_cli/commands/account.py +172 -0
  21. lovarch_cli/commands/audit.py +394 -0
  22. lovarch_cli/commands/config_cmd.py +80 -0
  23. lovarch_cli/commands/consolidate.py +217 -0
  24. lovarch_cli/commands/context_cmd.py +73 -0
  25. lovarch_cli/commands/dev.py +287 -0
  26. lovarch_cli/commands/do_cmd.py +120 -0
  27. lovarch_cli/commands/init.py +218 -0
  28. lovarch_cli/commands/jobs_cmd.py +95 -0
  29. lovarch_cli/commands/login.py +202 -0
  30. lovarch_cli/commands/mcp_cmd.py +26 -0
  31. lovarch_cli/commands/run.py +375 -0
  32. lovarch_cli/commands/signup.py +185 -0
  33. lovarch_cli/commands/status.py +243 -0
  34. lovarch_cli/commands/upgrade.py +108 -0
  35. lovarch_cli/commands/verifica_cmd.py +174 -0
  36. lovarch_cli/config.py +101 -0
  37. lovarch_cli/config_store.py +111 -0
  38. lovarch_cli/credits/__init__.py +35 -0
  39. lovarch_cli/credits/base.py +84 -0
  40. lovarch_cli/credits/factory.py +36 -0
  41. lovarch_cli/credits/local.py +34 -0
  42. lovarch_cli/credits/lovarch.py +56 -0
  43. lovarch_cli/i18n/__init__.py +27 -0
  44. lovarch_cli/i18n/loader.py +121 -0
  45. lovarch_cli/i18n/translations/en.json +168 -0
  46. lovarch_cli/i18n/translations/es.json +168 -0
  47. lovarch_cli/i18n/translations/it.json +168 -0
  48. lovarch_cli/i18n/translations/pt.json +168 -0
  49. lovarch_cli/mcp/__init__.py +9 -0
  50. lovarch_cli/mcp/server.py +199 -0
  51. lovarch_cli/mcp/tools.py +372 -0
  52. lovarch_cli/sample_downloader.py +255 -0
  53. lovarch_cli/squad/README.md +206 -0
  54. lovarch_cli/squad/agents/auditor-input.md +353 -0
  55. lovarch_cli/squad/agents/bim-engineer.md +404 -0
  56. lovarch_cli/squad/agents/briefing-architect.md +249 -0
  57. lovarch_cli/squad/agents/cad-engineer.md +278 -0
  58. lovarch_cli/squad/agents/capitolato-writer.md +256 -0
  59. lovarch_cli/squad/agents/computo-engineer.md +258 -0
  60. lovarch_cli/squad/agents/concept-designer.md +399 -0
  61. lovarch_cli/squad/agents/contratto-architect.md +243 -0
  62. lovarch_cli/squad/agents/deliverable-builder.md +253 -0
  63. lovarch_cli/squad/agents/energy-prelim.md +388 -0
  64. lovarch_cli/squad/agents/pratiche-it.md +251 -0
  65. lovarch_cli/squad/agents/progetto-chief.md +768 -0
  66. lovarch_cli/squad/agents/quality-dati.md +409 -0
  67. lovarch_cli/squad/agents/quality-misure.md +418 -0
  68. lovarch_cli/squad/agents/quality-normativa.md +417 -0
  69. lovarch_cli/squad/agents/quality-output.md +436 -0
  70. lovarch_cli/squad/agents/regolatorio-it.md +278 -0
  71. lovarch_cli/squad/checklists/handoff-quality-gate.md +232 -0
  72. lovarch_cli/squad/checklists/quality-dati-checklist.md +134 -0
  73. lovarch_cli/squad/checklists/quality-misure-checklist.md +139 -0
  74. lovarch_cli/squad/checklists/quality-normativa-checklist.md +121 -0
  75. lovarch_cli/squad/checklists/quality-output-checklist.md +116 -0
  76. lovarch_cli/squad/config.yaml +408 -0
  77. lovarch_cli/squad/data/CHANGELOG.md +272 -0
  78. lovarch_cli/squad/data/agents-prd.md +428 -0
  79. lovarch_cli/squad/data/architettura-progetto-rules.md +328 -0
  80. lovarch_cli/squad/data/handoff-card-template.md +231 -0
  81. lovarch_cli/squad/data/mocks/catasto-visura.json +72 -0
  82. lovarch_cli/squad/data/mocks/firma-envelope.json +43 -0
  83. lovarch_cli/squad/data/prezzario-lombardia-sample.json +312 -0
  84. lovarch_cli/squad/scripts/api_clients.py +206 -0
  85. lovarch_cli/squad/scripts/architect_profile.py +276 -0
  86. lovarch_cli/squad/scripts/deliverable_generators.py +844 -0
  87. lovarch_cli/squad/scripts/generate_attico_brera_dwg.py +369 -0
  88. lovarch_cli/squad/scripts/generate_chianti_dxf.py +368 -0
  89. lovarch_cli/squad/scripts/generate_chianti_images.py +223 -0
  90. lovarch_cli/squad/scripts/generate_real_sample_images.py +189 -0
  91. lovarch_cli/squad/scripts/generate_sample_assets.py +382 -0
  92. lovarch_cli/squad/scripts/lovarch_client.py +1046 -0
  93. lovarch_cli/squad/scripts/pipeline_runner.py +2095 -0
  94. lovarch_cli/squad/scripts/render_dxf_to_png.py +57 -0
  95. lovarch_cli/squad/scripts/run_palestra_demo.sh +277 -0
  96. lovarch_cli/squad/scripts/simulate_squad_execution.py +515 -0
  97. lovarch_cli/squad/scripts/validate-squad.py +383 -0
  98. lovarch_cli/squad/tasks/audit-input.md +146 -0
  99. lovarch_cli/squad/tasks/compute-metric.md +105 -0
  100. lovarch_cli/squad/tasks/consolidate-dossier.md +187 -0
  101. lovarch_cli/squad/tasks/generate-cad-plan.md +120 -0
  102. lovarch_cli/squad/tasks/generate-ifc-model.md +108 -0
  103. lovarch_cli/squad/tasks/write-capitolato.md +100 -0
  104. lovarch_cli/squad/templates/asseverazione-tecnica.md +126 -0
  105. lovarch_cli/squad/templates/capitolato-uni-11337.md +235 -0
  106. lovarch_cli/squad/templates/cila-comune-milano.md +177 -0
  107. lovarch_cli/squad/templates/contratto-cnappc.md +220 -0
  108. lovarch_cli/squad/workflows/dal-brief-al-cantiere.yaml +218 -0
  109. lovarch_cli/squad_loader.py +114 -0
  110. lovarch_cli/verify/__init__.py +15 -0
  111. lovarch_cli/verify/contratto.py +110 -0
  112. lovarch_cli/verify/dossier.py +97 -0
  113. lovarch_cli/verify/misure.py +83 -0
  114. lovarch_cli/verify/normativa.py +178 -0
  115. lovarch_cli/version.py +13 -0
  116. lovarch_cli/workflows/__init__.py +9 -0
  117. lovarch_cli/workflows/platform.py +212 -0
  118. lovarch_cli-0.2.1.dist-info/METADATA +232 -0
  119. lovarch_cli-0.2.1.dist-info/RECORD +122 -0
  120. lovarch_cli-0.2.1.dist-info/WHEEL +4 -0
  121. lovarch_cli-0.2.1.dist-info/entry_points.txt +3 -0
  122. 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
+ """