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,218 @@
|
|
|
1
|
+
name: dal-brief-al-cantiere
|
|
2
|
+
version: 1.0.0
|
|
3
|
+
description: >
|
|
4
|
+
Workflow principale del squad architettura-progetto.
|
|
5
|
+
Esegue progetto end-to-end: dal briefing del cliente al dossier consegnabile all'impresa.
|
|
6
|
+
Target: 14 minuti, 27 deliverable, 4 destinatari (cliente, comune, impresa, studio).
|
|
7
|
+
|
|
8
|
+
# ============================================================================
|
|
9
|
+
# Phases
|
|
10
|
+
# ============================================================================
|
|
11
|
+
|
|
12
|
+
phases:
|
|
13
|
+
|
|
14
|
+
# --------------------------------------------------------------------------
|
|
15
|
+
# PHASE 0: Initialization
|
|
16
|
+
# --------------------------------------------------------------------------
|
|
17
|
+
- id: phase_0_init
|
|
18
|
+
name: Inizializzazione
|
|
19
|
+
duration_target_seconds: 30
|
|
20
|
+
agent: "@progetto-chief"
|
|
21
|
+
actions:
|
|
22
|
+
- create_pm_squad_executions_row
|
|
23
|
+
- load_input_files
|
|
24
|
+
- validate_environment # supabase service_role, file system access
|
|
25
|
+
blocks_next_phase: true
|
|
26
|
+
|
|
27
|
+
# --------------------------------------------------------------------------
|
|
28
|
+
# PHASE 1: Audit input
|
|
29
|
+
# --------------------------------------------------------------------------
|
|
30
|
+
- id: phase_1_audit
|
|
31
|
+
name: Audit input
|
|
32
|
+
duration_target_seconds: 90
|
|
33
|
+
agent: "@auditor-input"
|
|
34
|
+
blocks_next_phase: true
|
|
35
|
+
veto_conditions:
|
|
36
|
+
- input_validation_failed
|
|
37
|
+
- critical_data_missing
|
|
38
|
+
on_reject:
|
|
39
|
+
action: halt_and_ask_pablo
|
|
40
|
+
message: "Input incompleto: {missing_items}"
|
|
41
|
+
|
|
42
|
+
# --------------------------------------------------------------------------
|
|
43
|
+
# PHASE 2: Tier 1 esecuzione parallela (11 agenti)
|
|
44
|
+
# --------------------------------------------------------------------------
|
|
45
|
+
- id: phase_2_tier1_parallel
|
|
46
|
+
name: Esecuzione tecnica parallela
|
|
47
|
+
duration_target_seconds: 540 # 9 minuti
|
|
48
|
+
parallel: true
|
|
49
|
+
parallel_agents:
|
|
50
|
+
- id: a1
|
|
51
|
+
agent: "@briefing-architect"
|
|
52
|
+
depends_on_phase_1: true
|
|
53
|
+
depends_on_extracted_data: ["briefing"]
|
|
54
|
+
timeout_seconds: 90
|
|
55
|
+
|
|
56
|
+
- id: a2
|
|
57
|
+
agent: "@regolatorio-it"
|
|
58
|
+
depends_on: [a1] # ha bisogno di brief-strutturato.json
|
|
59
|
+
timeout_seconds: 120
|
|
60
|
+
|
|
61
|
+
- id: a3
|
|
62
|
+
agent: "@concept-designer"
|
|
63
|
+
depends_on: [a1]
|
|
64
|
+
timeout_seconds: 300 # render gargalo
|
|
65
|
+
|
|
66
|
+
- id: a4
|
|
67
|
+
agent: "@cad-engineer"
|
|
68
|
+
depends_on: [a1]
|
|
69
|
+
timeout_seconds: 180
|
|
70
|
+
|
|
71
|
+
- id: a5
|
|
72
|
+
agent: "@bim-engineer"
|
|
73
|
+
depends_on: [a4]
|
|
74
|
+
timeout_seconds: 180
|
|
75
|
+
|
|
76
|
+
- id: a6
|
|
77
|
+
agent: "@computo-engineer"
|
|
78
|
+
depends_on: [a5] # ha bisogno di quantitativi.json
|
|
79
|
+
timeout_seconds: 120
|
|
80
|
+
|
|
81
|
+
- id: a7
|
|
82
|
+
agent: "@capitolato-writer"
|
|
83
|
+
depends_on: [a6, a2]
|
|
84
|
+
timeout_seconds: 180
|
|
85
|
+
|
|
86
|
+
- id: a8
|
|
87
|
+
agent: "@pratiche-it"
|
|
88
|
+
depends_on: [a2, a4]
|
|
89
|
+
timeout_seconds: 120
|
|
90
|
+
|
|
91
|
+
- id: a9
|
|
92
|
+
agent: "@contratto-architect"
|
|
93
|
+
depends_on_phase_1: true
|
|
94
|
+
timeout_seconds: 90
|
|
95
|
+
|
|
96
|
+
- id: a10
|
|
97
|
+
agent: "@energy-prelim"
|
|
98
|
+
depends_on: [a4, a5, a6]
|
|
99
|
+
timeout_seconds: 120
|
|
100
|
+
|
|
101
|
+
- id: a11
|
|
102
|
+
agent: "@deliverable-builder"
|
|
103
|
+
depends_on: [a3, a4, a5, a6, a7, a8, a9, a10] # ultimo, consolidatore — attende TUTTI i produttori di file (concept, CAD, BIM/IFC, computo, capitolato, pratiche, contratto, APE)
|
|
104
|
+
timeout_seconds: 90
|
|
105
|
+
|
|
106
|
+
on_agent_timeout: retry_once_then_escalate
|
|
107
|
+
on_agent_error: retry_once_then_escalate
|
|
108
|
+
blocks_next_phase: true
|
|
109
|
+
|
|
110
|
+
# --------------------------------------------------------------------------
|
|
111
|
+
# PHASE 3: Tier 2 QA parallelo (4 agenti)
|
|
112
|
+
# --------------------------------------------------------------------------
|
|
113
|
+
- id: phase_3_qa_parallel
|
|
114
|
+
name: Conferenza qualità
|
|
115
|
+
duration_target_seconds: 90
|
|
116
|
+
parallel: true
|
|
117
|
+
parallel_agents:
|
|
118
|
+
- agent: "@quality-misure"
|
|
119
|
+
verifies: ["@cad-engineer", "@bim-engineer"]
|
|
120
|
+
|
|
121
|
+
- agent: "@quality-normativa"
|
|
122
|
+
verifies: ["@regolatorio-it", "@capitolato-writer", "@pratiche-it", "@contratto-architect"]
|
|
123
|
+
|
|
124
|
+
- agent: "@quality-dati"
|
|
125
|
+
verifies: all_tier_1_outputs
|
|
126
|
+
|
|
127
|
+
- agent: "@quality-output"
|
|
128
|
+
verifies: deliverables_completeness
|
|
129
|
+
|
|
130
|
+
on_qa_reject:
|
|
131
|
+
action: route_to_phase_2_retry
|
|
132
|
+
retry_logic:
|
|
133
|
+
max_retries: 3
|
|
134
|
+
per_agent: true
|
|
135
|
+
if_max_reached: escalate_to_pablo
|
|
136
|
+
blocks_next_phase: true
|
|
137
|
+
|
|
138
|
+
# --------------------------------------------------------------------------
|
|
139
|
+
# PHASE 4: Consolidation
|
|
140
|
+
# --------------------------------------------------------------------------
|
|
141
|
+
- id: phase_4_consolidate
|
|
142
|
+
name: Consolidamento dossier
|
|
143
|
+
duration_target_seconds: 60
|
|
144
|
+
agent: "@progetto-chief"
|
|
145
|
+
actions:
|
|
146
|
+
- generate_readme_index
|
|
147
|
+
- create_dossier_zip
|
|
148
|
+
- upload_all_to_lovarch_storage
|
|
149
|
+
- insert_pm_documents_rows
|
|
150
|
+
- generate_manifest_with_sha256
|
|
151
|
+
- git_commit_with_tag
|
|
152
|
+
- mark_pm_squad_executions_status_completed
|
|
153
|
+
- open_finder
|
|
154
|
+
- print_final_summary
|
|
155
|
+
|
|
156
|
+
# --------------------------------------------------------------------------
|
|
157
|
+
# PHASE 5: Reveal (per palestra)
|
|
158
|
+
# --------------------------------------------------------------------------
|
|
159
|
+
- id: phase_5_reveal
|
|
160
|
+
name: Reveal in palco
|
|
161
|
+
optional: true
|
|
162
|
+
duration_target_seconds: 30
|
|
163
|
+
actions:
|
|
164
|
+
- print_url_to_terminal: "https://lovarch.com/admin/squad-execution/{execution_id}/dossier"
|
|
165
|
+
- say_to_pablo: "Dossier pronto. URL aperto."
|
|
166
|
+
|
|
167
|
+
# ============================================================================
|
|
168
|
+
# Performance targets
|
|
169
|
+
# ============================================================================
|
|
170
|
+
|
|
171
|
+
targets:
|
|
172
|
+
total_duration_seconds: 840 # 14 min
|
|
173
|
+
parallel_max: 8
|
|
174
|
+
qa_retry_max: 3
|
|
175
|
+
deliverables_min: 25 # soglia minima; target tipico = 27 (config deliverables_count)
|
|
176
|
+
|
|
177
|
+
# ============================================================================
|
|
178
|
+
# Failure modes
|
|
179
|
+
# ============================================================================
|
|
180
|
+
|
|
181
|
+
failure_modes:
|
|
182
|
+
audit_reject:
|
|
183
|
+
severity: blocking
|
|
184
|
+
action: ask_pablo
|
|
185
|
+
|
|
186
|
+
agent_timeout_x3:
|
|
187
|
+
severity: blocking
|
|
188
|
+
action: escalate_to_pablo
|
|
189
|
+
|
|
190
|
+
qa_reject_x3:
|
|
191
|
+
severity: blocking
|
|
192
|
+
action: escalate_to_pablo
|
|
193
|
+
log: full_diff_history
|
|
194
|
+
|
|
195
|
+
storage_quota:
|
|
196
|
+
severity: critical
|
|
197
|
+
action: halt_immediately
|
|
198
|
+
notify: pablo
|
|
199
|
+
|
|
200
|
+
api_5xx_persistent:
|
|
201
|
+
severity: blocking
|
|
202
|
+
action: pause_60s_retry_3x_then_escalate
|
|
203
|
+
|
|
204
|
+
# ============================================================================
|
|
205
|
+
# Realtime tracking
|
|
206
|
+
# ============================================================================
|
|
207
|
+
|
|
208
|
+
tracking:
|
|
209
|
+
table: pm_squad_steps
|
|
210
|
+
insert_on:
|
|
211
|
+
- agent_start
|
|
212
|
+
- agent_complete
|
|
213
|
+
- qa_verdict
|
|
214
|
+
- retry_initiated
|
|
215
|
+
update_on:
|
|
216
|
+
- status_change
|
|
217
|
+
- output_files_added
|
|
218
|
+
publish_realtime: true
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Resolve which squad payload directory the CLI should use at runtime.
|
|
2
|
+
|
|
3
|
+
Three resolution sources, in priority order:
|
|
4
|
+
|
|
5
|
+
1. Explicit override (CLI flag `--squad-src`) — highest priority. Used for
|
|
6
|
+
ad-hoc one-off runs against a custom squad path.
|
|
7
|
+
2. `LOVARCH_SQUAD_SRC` environment variable — for the developer's daily
|
|
8
|
+
loop. Export once in `~/.zshrc` and every CLI invocation picks it up.
|
|
9
|
+
3. Bundled `lovarch_cli/squad/` — the vendored snapshot shipped with the
|
|
10
|
+
package. This is what `brew install` users get.
|
|
11
|
+
|
|
12
|
+
The override paths MUST point at a directory that looks like the squad
|
|
13
|
+
(must contain at minimum `scripts/pipeline_runner.py` and `agents/`).
|
|
14
|
+
We validate the shape early so the failure message is actionable instead
|
|
15
|
+
of a cryptic FileNotFoundError two layers deeper.
|
|
16
|
+
|
|
17
|
+
This module is the single source-of-truth for squad path resolution. All
|
|
18
|
+
commands that read squad assets (run, init --sample, future dev tooling)
|
|
19
|
+
go through `resolve_squad_root()`.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Environment variable name. Document this widely — it's the daily dev
|
|
28
|
+
# override most contributors will use.
|
|
29
|
+
ENV_VAR = "LOVARCH_SQUAD_SRC"
|
|
30
|
+
|
|
31
|
+
# Bundled vendor — shipped with the wheel via the build hook
|
|
32
|
+
# (scripts/sync_squad.py). See docs/squad-vendoring.md.
|
|
33
|
+
_BUNDLED_DIR: Path = Path(__file__).resolve().parent / "squad"
|
|
34
|
+
|
|
35
|
+
# Shape markers — files we expect to find in any valid squad payload.
|
|
36
|
+
# `scripts/pipeline_runner.py` is the orchestration entrypoint that
|
|
37
|
+
# `lovarch run` shells out to. `agents/` is the directory of agent prompts.
|
|
38
|
+
_REQUIRED_MARKERS: tuple[str, ...] = (
|
|
39
|
+
"scripts/pipeline_runner.py",
|
|
40
|
+
"agents",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SquadNotFoundError(RuntimeError):
|
|
45
|
+
"""The configured squad root does not exist or does not look like a squad."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _looks_like_squad(path: Path) -> bool:
|
|
49
|
+
"""Return True if every marker in _REQUIRED_MARKERS exists under path."""
|
|
50
|
+
return all((path / marker).exists() for marker in _REQUIRED_MARKERS)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _validate_or_raise(path: Path, source_label: str) -> Path:
|
|
54
|
+
"""Raise SquadNotFoundError unless `path` looks like a valid squad payload."""
|
|
55
|
+
if not path.is_dir():
|
|
56
|
+
raise SquadNotFoundError(
|
|
57
|
+
f"Squad path from {source_label} does not exist or is not a "
|
|
58
|
+
f"directory: {path}"
|
|
59
|
+
)
|
|
60
|
+
if not _looks_like_squad(path):
|
|
61
|
+
missing = [m for m in _REQUIRED_MARKERS if not (path / m).exists()]
|
|
62
|
+
raise SquadNotFoundError(
|
|
63
|
+
f"Squad path from {source_label} is missing required files: "
|
|
64
|
+
f"{', '.join(missing)} (looked under {path})"
|
|
65
|
+
)
|
|
66
|
+
return path
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def resolve_squad_root(override: str | os.PathLike[str] | None = None) -> Path:
|
|
70
|
+
"""Return the directory the CLI should treat as the squad payload root.
|
|
71
|
+
|
|
72
|
+
Priority:
|
|
73
|
+
1. `override` argument (typically wired from `--squad-src` flag)
|
|
74
|
+
2. `LOVARCH_SQUAD_SRC` env var
|
|
75
|
+
3. Bundled `lovarch_cli/squad/`
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
SquadNotFoundError if no source resolves to a valid squad shape.
|
|
79
|
+
"""
|
|
80
|
+
if override is not None:
|
|
81
|
+
candidate = Path(override).expanduser().resolve()
|
|
82
|
+
return _validate_or_raise(candidate, "--squad-src flag")
|
|
83
|
+
|
|
84
|
+
env_path = os.environ.get(ENV_VAR)
|
|
85
|
+
if env_path:
|
|
86
|
+
candidate = Path(env_path).expanduser().resolve()
|
|
87
|
+
return _validate_or_raise(candidate, f"${ENV_VAR}")
|
|
88
|
+
|
|
89
|
+
if _BUNDLED_DIR.is_dir() and any(_BUNDLED_DIR.iterdir()):
|
|
90
|
+
return _validate_or_raise(_BUNDLED_DIR, "bundled vendor")
|
|
91
|
+
|
|
92
|
+
raise SquadNotFoundError(
|
|
93
|
+
"No squad payload available. Bundled directory is empty and no "
|
|
94
|
+
f"override was provided. Set ${ENV_VAR} to a squad source path, "
|
|
95
|
+
"or pass --squad-src, or reinstall lovarch-cli."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def squad_source_label(squad_root: Path) -> str:
|
|
100
|
+
"""Return a short human-readable label for where this path came from.
|
|
101
|
+
|
|
102
|
+
Used by `lovarch dev show-squad-root` and run-summary panels.
|
|
103
|
+
"""
|
|
104
|
+
if squad_root == _BUNDLED_DIR.resolve():
|
|
105
|
+
return "bundled"
|
|
106
|
+
env_path = os.environ.get(ENV_VAR)
|
|
107
|
+
if env_path and Path(env_path).expanduser().resolve() == squad_root:
|
|
108
|
+
return f"override (${ENV_VAR})"
|
|
109
|
+
return "override (flag)"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def bundled_squad_dir() -> Path:
|
|
113
|
+
"""Public accessor for the bundled vendor path — used by refresh tooling."""
|
|
114
|
+
return _BUNDLED_DIR
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""`lovarch verifica` — data-checking workflows for architects, interior
|
|
2
|
+
designers, geometri and engineers.
|
|
3
|
+
|
|
4
|
+
Design (Onda 2 · F10): deterministic code first (cheap, exact — ezdxf/regex),
|
|
5
|
+
LLM only where judgment is needed, using the ADVERSARIAL two-model pattern:
|
|
6
|
+
Sonnet 5 (executor) extracts/structures → Opus 4.8 (verifier) tries to refute
|
|
7
|
+
each claim independently. Credits are debited per real tokens via cli-ai-text;
|
|
8
|
+
deterministic checks are free.
|
|
9
|
+
"""
|
|
10
|
+
from lovarch_cli.verify.contratto import verify_contratto
|
|
11
|
+
from lovarch_cli.verify.dossier import verify_dossier
|
|
12
|
+
from lovarch_cli.verify.misure import verify_misure
|
|
13
|
+
from lovarch_cli.verify.normativa import verify_normativa
|
|
14
|
+
|
|
15
|
+
__all__ = ["verify_contratto", "verify_dossier", "verify_misure", "verify_normativa"]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""verifica contratto — adversarial check of an architect's contract.
|
|
2
|
+
|
|
3
|
+
Same two-model pattern as normativa: Sonnet 5 (executor) extracts the
|
|
4
|
+
contract's structure, Opus 4.8 (verifier) adversarially checks it against the
|
|
5
|
+
CNAPPC standard (12 articles) and the compenso rules — including the QN_007
|
|
6
|
+
nuance: for a PRIVATE consumer client the DM 17/06/2016 parameters are
|
|
7
|
+
ORIENTATIVE (an unexplained gap is a CONCERN, never an illegality; L.49/2023
|
|
8
|
+
binds only strong counterparties such as PA/banks/large companies).
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from lovarch_cli.verify.normativa import NormativaError, _parse_json, extract_text
|
|
17
|
+
|
|
18
|
+
CNAPPC_ARTICLES = [
|
|
19
|
+
"oggetto dell'incarico", "prestazioni professionali", "compenso",
|
|
20
|
+
"modalità di pagamento", "spese e oneri", "tempi di esecuzione",
|
|
21
|
+
"recesso", "inadempimento", "proprietà intellettuale",
|
|
22
|
+
"privacy/GDPR", "controversie/foro", "firma delle parti",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
_EXTRACT_SYSTEM = (
|
|
26
|
+
"Sei un assistente per contratti di prestazione professionale di "
|
|
27
|
+
"architettura (standard CNAPPC). Estrai dal contratto la struttura: quali "
|
|
28
|
+
"delle 12 sezioni tipiche sono presenti, il tipo di committente "
|
|
29
|
+
"(privato consumatore | PA | impresa | banca/assicurazione), il compenso "
|
|
30
|
+
"pattuito e come è giustificato. Rispondi SOLO con JSON valido: "
|
|
31
|
+
'{"sections_present": ["..."], "sections_missing": ["..."], '
|
|
32
|
+
'"client_type": "privato|pa|impresa|banca|sconosciuto", '
|
|
33
|
+
'"compenso": {"amount": "...", "justification": "... o null"}, '
|
|
34
|
+
'"notes": ["..."]}'
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
_REFUTE_SYSTEM = (
|
|
38
|
+
"Sei un verificatore ADVERSARIALE di contratti CNAPPC. Controlla: "
|
|
39
|
+
"(1) completezza delle 12 sezioni tipiche; (2) coerenza del compenso. "
|
|
40
|
+
"REGOLA COMPENSO (QN_007): per un committente PRIVATO CONSUMATORE i "
|
|
41
|
+
"parametri DM 17/06/2016 sono ORIENTATIVI — uno scostamento non motivato è "
|
|
42
|
+
"un CONCERN (mai una violazione di legge, la L.49/2023 vincola solo "
|
|
43
|
+
"contraenti forti: PA, banche, assicurazioni, grandi imprese); per un "
|
|
44
|
+
"contraente FORTE lo scostamento sotto i parametri è invece violazione. "
|
|
45
|
+
"Rispondi SOLO con JSON valido: "
|
|
46
|
+
'{"findings": [{"area": "...", "severity": "critical" | "concern" | "info", '
|
|
47
|
+
'"reason": "..."}], "overall": "PASS" | "CONCERNS" | "REJECT"}'
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class ContrattoReport:
|
|
53
|
+
verdict: str
|
|
54
|
+
structure: dict = field(default_factory=dict)
|
|
55
|
+
findings: list[dict] = field(default_factory=list)
|
|
56
|
+
credits_charged: int = 0
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def verify_contratto(
|
|
60
|
+
gateway: Any,
|
|
61
|
+
document_path: str,
|
|
62
|
+
*,
|
|
63
|
+
language: str = "it",
|
|
64
|
+
max_chars: int = 60_000,
|
|
65
|
+
) -> ContrattoReport:
|
|
66
|
+
"""Adversarial CNAPPC contract check. Debits credits (2 calls)."""
|
|
67
|
+
text = extract_text(document_path)
|
|
68
|
+
if not text.strip():
|
|
69
|
+
raise NormativaError("documento vuoto o testo non estraibile.")
|
|
70
|
+
if len(text) > max_chars:
|
|
71
|
+
text = text[:max_chars]
|
|
72
|
+
|
|
73
|
+
credits = 0
|
|
74
|
+
extraction = await gateway.generate_text(
|
|
75
|
+
f"CONTRATTO:\n\n{text}",
|
|
76
|
+
role="executor",
|
|
77
|
+
system=_EXTRACT_SYSTEM,
|
|
78
|
+
max_tokens=1500,
|
|
79
|
+
language=language,
|
|
80
|
+
operation_type="verify:contratto:extract",
|
|
81
|
+
)
|
|
82
|
+
credits += extraction.credits_charged
|
|
83
|
+
structure = _parse_json(extraction.text)
|
|
84
|
+
|
|
85
|
+
refutation = await gateway.generate_text(
|
|
86
|
+
"STRUTTURA ESTRATTA DAL CONTRATTO:\n\n"
|
|
87
|
+
+ json.dumps(structure, ensure_ascii=False)
|
|
88
|
+
+ "\n\nSezioni CNAPPC attese: " + ", ".join(CNAPPC_ARTICLES),
|
|
89
|
+
role="verifier",
|
|
90
|
+
system=_REFUTE_SYSTEM,
|
|
91
|
+
max_tokens=2000,
|
|
92
|
+
language=language,
|
|
93
|
+
operation_type="verify:contratto:refute",
|
|
94
|
+
)
|
|
95
|
+
credits += refutation.credits_charged
|
|
96
|
+
judged = _parse_json(refutation.text)
|
|
97
|
+
findings = judged.get("findings") or []
|
|
98
|
+
|
|
99
|
+
overall = str(judged.get("overall", "")).upper()
|
|
100
|
+
if overall not in ("PASS", "CONCERNS", "REJECT"):
|
|
101
|
+
severities = [str(f.get("severity", "")).lower() for f in findings]
|
|
102
|
+
overall = ("REJECT" if "critical" in severities
|
|
103
|
+
else ("CONCERNS" if "concern" in severities else "PASS"))
|
|
104
|
+
|
|
105
|
+
return ContrattoReport(
|
|
106
|
+
verdict=overall,
|
|
107
|
+
structure=structure,
|
|
108
|
+
findings=findings,
|
|
109
|
+
credits_charged=credits,
|
|
110
|
+
)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""verifica dossier — run the full standalone QA over a deliverables folder.
|
|
2
|
+
|
|
3
|
+
Composes the individual verifiers by file type:
|
|
4
|
+
*.dxf → misure (deterministic, free)
|
|
5
|
+
contratto*.{pdf,md} → contratto (adversarial, credits)
|
|
6
|
+
other *.{pdf,md,txt} → normativa (adversarial, credits)
|
|
7
|
+
|
|
8
|
+
Verdict aggregation mirrors the squad's Tier-2 rule: any REJECT → REJECT;
|
|
9
|
+
any CONCERNS → CONCERNS; else PASS.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from lovarch_cli.verify.contratto import verify_contratto
|
|
18
|
+
from lovarch_cli.verify.misure import verify_misure
|
|
19
|
+
from lovarch_cli.verify.normativa import NormativaError, verify_normativa
|
|
20
|
+
|
|
21
|
+
_TEXT_SUFFIXES = {".pdf", ".md", ".txt"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class DossierReport:
|
|
26
|
+
verdict: str
|
|
27
|
+
files: list[dict] = field(default_factory=list) # per-file {name, kind, verdict, detail}
|
|
28
|
+
credits_charged: int = 0
|
|
29
|
+
skipped: list[str] = field(default_factory=list)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def verify_dossier(
|
|
33
|
+
gateway: Any,
|
|
34
|
+
directory: str | Path,
|
|
35
|
+
*,
|
|
36
|
+
language: str = "it",
|
|
37
|
+
max_llm_files: int = 8,
|
|
38
|
+
) -> DossierReport:
|
|
39
|
+
"""Verify every recognized deliverable in a folder.
|
|
40
|
+
|
|
41
|
+
``max_llm_files`` caps how many documents go through the paid adversarial
|
|
42
|
+
checks in one run (DXF checks are free and uncapped) — the cap and any
|
|
43
|
+
skipped files are reported explicitly, never silently.
|
|
44
|
+
"""
|
|
45
|
+
root = Path(directory).expanduser()
|
|
46
|
+
if not root.is_dir():
|
|
47
|
+
raise NormativaError(f"cartella non trovata: {root}")
|
|
48
|
+
|
|
49
|
+
files: list[dict] = []
|
|
50
|
+
skipped: list[str] = []
|
|
51
|
+
credits = 0
|
|
52
|
+
llm_used = 0
|
|
53
|
+
|
|
54
|
+
entries = sorted(p for p in root.rglob("*") if p.is_file())
|
|
55
|
+
for path in entries:
|
|
56
|
+
suffix = path.suffix.lower()
|
|
57
|
+
rel = str(path.relative_to(root))
|
|
58
|
+
if suffix == ".dxf":
|
|
59
|
+
report = verify_misure(path)
|
|
60
|
+
files.append({
|
|
61
|
+
"name": rel, "kind": "misure", "verdict": report.verdict,
|
|
62
|
+
"detail": "; ".join(report.findings) or "ok",
|
|
63
|
+
})
|
|
64
|
+
elif suffix in _TEXT_SUFFIXES:
|
|
65
|
+
if llm_used >= max_llm_files:
|
|
66
|
+
skipped.append(rel)
|
|
67
|
+
continue
|
|
68
|
+
llm_used += 1
|
|
69
|
+
is_contract = "contratto" in path.name.lower() or "contract" in path.name.lower()
|
|
70
|
+
try:
|
|
71
|
+
if is_contract:
|
|
72
|
+
rep = await verify_contratto(gateway, str(path), language=language)
|
|
73
|
+
detail = "; ".join(
|
|
74
|
+
f"[{f.get('severity')}] {f.get('area')}" for f in rep.findings[:4]
|
|
75
|
+
) or "ok"
|
|
76
|
+
else:
|
|
77
|
+
rep = await verify_normativa(gateway, str(path), language=language)
|
|
78
|
+
detail = "; ".join(
|
|
79
|
+
f"{v.get('reference')}: {v.get('status')}" for v in rep.verdicts[:4]
|
|
80
|
+
) or "; ".join(rep.notes) or "ok"
|
|
81
|
+
credits += rep.credits_charged
|
|
82
|
+
files.append({
|
|
83
|
+
"name": rel, "kind": "contratto" if is_contract else "normativa",
|
|
84
|
+
"verdict": rep.verdict, "detail": detail,
|
|
85
|
+
})
|
|
86
|
+
except NormativaError as exc:
|
|
87
|
+
files.append({"name": rel, "kind": "errore", "verdict": "CONCERNS",
|
|
88
|
+
"detail": str(exc)[:120]})
|
|
89
|
+
|
|
90
|
+
if not files:
|
|
91
|
+
raise NormativaError("nessun file verificabile trovato (.dxf/.pdf/.md/.txt).")
|
|
92
|
+
|
|
93
|
+
verdicts = [f["verdict"] for f in files]
|
|
94
|
+
overall = ("REJECT" if "REJECT" in verdicts
|
|
95
|
+
else ("CONCERNS" if "CONCERNS" in verdicts or skipped else "PASS"))
|
|
96
|
+
return DossierReport(verdict=overall, files=files,
|
|
97
|
+
credits_charged=credits, skipped=skipped)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""verifica misure — deterministic DXF checks (UNI ISO 5457 / CNAPPC).
|
|
2
|
+
|
|
3
|
+
Ported from the squad's Tier-2 @quality-misure verifier (pipeline_runner Q1) so
|
|
4
|
+
any professional can check a drawing standalone, without running the dossier.
|
|
5
|
+
Pure code — no credits are consumed.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
ISO_LAYERS = {
|
|
13
|
+
"CAD-A-WALL", "CAD-A-WALL-EXT", "CAD-A-DOOR", "CAD-A-WIND", "CAD-A-DIM",
|
|
14
|
+
"CAD-A-TEXT", "CAD-A-SYMB", "CAD-A-FURN", "CAD-A-CART",
|
|
15
|
+
}
|
|
16
|
+
ROOM_LABELS = {"INGRESSO", "SOGGIORNO", "CUCINA", "STUDIO", "CAMERA", "BAGNO", "LAVANDERIA"}
|
|
17
|
+
CARTIGLIO_REQUIRED = ["PROGETTO", "CLIENTE", "ARCHITETTO", "SCALA", "DATA"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class MisureReport:
|
|
22
|
+
verdict: str # PASS | CONCERNS | REJECT
|
|
23
|
+
findings: list[str] = field(default_factory=list)
|
|
24
|
+
stats: dict = field(default_factory=dict)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def verify_misure(dxf_path: str | Path) -> MisureReport:
|
|
28
|
+
"""Check a DXF for ISO layer compliance, room labels and CNAPPC cartiglio."""
|
|
29
|
+
path = Path(dxf_path).expanduser()
|
|
30
|
+
findings: list[str] = []
|
|
31
|
+
stats: dict = {}
|
|
32
|
+
|
|
33
|
+
if not path.is_file():
|
|
34
|
+
return MisureReport(verdict="REJECT", findings=[f"file non trovato: {path}"])
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
import ezdxf
|
|
38
|
+
doc = ezdxf.readfile(str(path))
|
|
39
|
+
except Exception as exc: # noqa: BLE001 — parse errors are the finding
|
|
40
|
+
return MisureReport(verdict="REJECT", findings=[f"DXF non leggibile: {str(exc)[:120]}"])
|
|
41
|
+
|
|
42
|
+
msp = doc.modelspace()
|
|
43
|
+
layers = {e.dxf.layer for e in msp}
|
|
44
|
+
stats["entities"] = len(msp)
|
|
45
|
+
stats["layers"] = sorted(layers)
|
|
46
|
+
|
|
47
|
+
# 1. ISO layer compliance (UNI ISO 5457 naming convention)
|
|
48
|
+
iso_ok = ISO_LAYERS & layers
|
|
49
|
+
stats["iso_layers_present"] = len(iso_ok)
|
|
50
|
+
if len(iso_ok) < 6:
|
|
51
|
+
findings.append(
|
|
52
|
+
f"conformità layer ISO: solo {len(iso_ok)}/9 layer standard presenti "
|
|
53
|
+
f"(attesi: {', '.join(sorted(ISO_LAYERS - layers))} mancanti)"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# 2. Room labels
|
|
57
|
+
texts = [e for e in msp if e.dxftype() in ("TEXT", "MTEXT")]
|
|
58
|
+
rooms: set[str] = set()
|
|
59
|
+
for e in texts:
|
|
60
|
+
try:
|
|
61
|
+
t = (e.dxf.text if e.dxftype() == "TEXT" else e.text).upper()
|
|
62
|
+
except Exception: # noqa: BLE001
|
|
63
|
+
continue
|
|
64
|
+
for room in ROOM_LABELS:
|
|
65
|
+
if room in t:
|
|
66
|
+
rooms.add(room)
|
|
67
|
+
stats["room_labels_found"] = sorted(rooms)
|
|
68
|
+
missing_rooms = ROOM_LABELS - rooms
|
|
69
|
+
if len(missing_rooms) > 1:
|
|
70
|
+
findings.append(f"etichette ambienti mancanti: {', '.join(sorted(missing_rooms))}")
|
|
71
|
+
|
|
72
|
+
# 3. Cartiglio CNAPPC
|
|
73
|
+
cart_text = " ".join(
|
|
74
|
+
(e.dxf.text if e.dxftype() == "TEXT" else e.text).upper()
|
|
75
|
+
for e in texts
|
|
76
|
+
if "CART" in str(e.dxf.layer).upper()
|
|
77
|
+
)
|
|
78
|
+
cart_missing = [k for k in CARTIGLIO_REQUIRED if k not in cart_text]
|
|
79
|
+
if cart_missing:
|
|
80
|
+
findings.append(f"cartiglio CNAPPC incompleto: mancano {', '.join(cart_missing)}")
|
|
81
|
+
|
|
82
|
+
verdict = "PASS" if not findings else ("CONCERNS" if len(findings) == 1 else "REJECT")
|
|
83
|
+
return MisureReport(verdict=verdict, findings=findings, stats=stats)
|