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,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)