@antoneeo/agentic-sdlc-skill 1.3.1 → 1.5.0

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.
@@ -0,0 +1,474 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Validatore meccanico per la skill Agentic SDLC.
4
+
5
+ Comandi:
6
+ check gate unico di chiusura: validate + stale in un solo comando (exit 1 se uno dei due fallisce)
7
+ validate verifica la coerenza strutturale di ai_docs/ (exit 1 se errori)
8
+ index rigenera ai_docs/strategic/features_history.md dai frontmatter delle ANALYSIS_*.md
9
+ stale elenca le aree modificate dopo l'ultima analisi registrata in audit_plan.md (exit 1 se presenti)
10
+ mark registra percorsi come ANALYZED con riferimento corrente (hash git, altrimenti timestamp UTC)
11
+ gate hook PreToolUse: blocca scritture su percorsi protetti senza ANALYSIS IN_PROGRESS (exit 2)
12
+
13
+ Solo libreria standard (Python >= 3.8). Compatibile Windows e POSIX.
14
+ """
15
+ import argparse
16
+ import json
17
+ import os
18
+ import re
19
+ import subprocess
20
+ import sys
21
+ from datetime import datetime, timedelta, timezone
22
+ from pathlib import Path
23
+
24
+ VALID_STATES = {"PLANNED", "IN_PROGRESS", "COMPLETED", "CANCELLED"}
25
+ VALID_LEVELS = {"L1", "L2", "L3", "SPIKE"}
26
+ VISION_FILES = ("project_vision.md", "roadmap.md", "principles.md")
27
+ SKIP_DIRS = {".git", ".hg", ".svn", "node_modules", "__pycache__", ".venv", "venv",
28
+ "dist", "build", ".idea", ".vs", "ai_docs"}
29
+ INDEX_HEADER = ("<!-- GENERATO da sdlc_check.py index - non modificare a mano. "
30
+ "Fonte di verita': frontmatter dei file ANALYSIS_*.md -->")
31
+ MTIME_GRACE = timedelta(seconds=2)
32
+
33
+ try:
34
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
35
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
36
+ except Exception:
37
+ pass
38
+
39
+
40
+ # ----------------------------------------------------------------- utilità
41
+
42
+ def utc_now_iso():
43
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
44
+
45
+
46
+ def find_project_root(start=None):
47
+ cur = Path(start or os.getcwd()).resolve()
48
+ for p in [cur] + list(cur.parents):
49
+ if (p / "ai_docs").is_dir():
50
+ return p
51
+ return cur
52
+
53
+
54
+ def read_text(path):
55
+ return path.read_text(encoding="utf-8", errors="replace")
56
+
57
+
58
+ def parse_iso(value):
59
+ if not value:
60
+ return None
61
+ try:
62
+ dt = datetime.fromisoformat(value.strip().replace("Z", "+00:00"))
63
+ if dt.tzinfo is None:
64
+ dt = dt.replace(tzinfo=timezone.utc)
65
+ return dt
66
+ except ValueError:
67
+ return None
68
+
69
+
70
+ def norm_text(s):
71
+ return "\n".join(line.rstrip() for line in s.strip().splitlines())
72
+
73
+
74
+ def load_frontmatter(lines):
75
+ meta = {}
76
+ if not lines or lines[0].strip() != "---":
77
+ return meta
78
+ for line in lines[1:60]:
79
+ if line.strip() == "---":
80
+ break
81
+ m = re.match(r"^([A-Za-z_][\w-]*):\s*(.*)$", line)
82
+ if m:
83
+ meta[m.group(1).strip().lower()] = m.group(2).strip()
84
+ return meta
85
+
86
+
87
+ def list_analyses(root):
88
+ """Ritorna [(path, frontmatter, testo)] per le ANALYSIS_*.md (shadow escluse)."""
89
+ sol = root / "ai_docs" / "solutions"
90
+ out = []
91
+ if not sol.is_dir():
92
+ return out
93
+ for p in sorted(sol.glob("ANALYSIS_*.md")):
94
+ text = read_text(p)
95
+ if "SHADOW" in text[:200]:
96
+ continue
97
+ out.append((p, load_frontmatter(text.splitlines()), text))
98
+ return out
99
+
100
+
101
+ def iter_files(target):
102
+ if target.is_file():
103
+ yield target
104
+ return
105
+ for dirpath, dirnames, filenames in os.walk(target):
106
+ dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS and not d.startswith(".")]
107
+ for name in filenames:
108
+ yield Path(dirpath) / name
109
+
110
+
111
+ # ---------------------------------------------------------------------- git
112
+
113
+ def git_available(root):
114
+ try:
115
+ r = subprocess.run(["git", "rev-parse", "--is-inside-work-tree"],
116
+ cwd=str(root), capture_output=True, text=True, timeout=10)
117
+ return r.returncode == 0 and r.stdout.strip() == "true"
118
+ except Exception:
119
+ return False
120
+
121
+
122
+ def git_head(root):
123
+ try:
124
+ r = subprocess.run(["git", "rev-parse", "--short=12", "HEAD"],
125
+ cwd=str(root), capture_output=True, text=True, timeout=10)
126
+ return r.stdout.strip() if r.returncode == 0 else ""
127
+ except Exception:
128
+ return ""
129
+
130
+
131
+ def git_has_changes(root, rel_path):
132
+ """True se ci sono modifiche tracked/untracked sotto rel_path."""
133
+ try:
134
+ rel = rel_path.replace("\\", "/")
135
+ r = subprocess.run(["git", "status", "--porcelain", "--", rel],
136
+ cwd=str(root), capture_output=True, text=True, timeout=30)
137
+ return r.returncode == 0 and bool(r.stdout.strip())
138
+ except Exception:
139
+ return False
140
+
141
+
142
+ def git_changed_since(root, ref, rel_path):
143
+ """File cambiati (tracked + untracked) sotto rel_path rispetto a ref. None se ref non risolvibile."""
144
+ try:
145
+ r = subprocess.run(["git", "diff", "--name-only", ref, "--", rel_path],
146
+ cwd=str(root), capture_output=True, text=True, timeout=30)
147
+ if r.returncode != 0:
148
+ return None
149
+ changed = [l.strip() for l in r.stdout.splitlines() if l.strip()]
150
+ r2 = subprocess.run(["git", "ls-files", "--others", "--exclude-standard", "--", rel_path],
151
+ cwd=str(root), capture_output=True, text=True, timeout=30)
152
+ if r2.returncode == 0:
153
+ changed += [l.strip() for l in r2.stdout.splitlines() if l.strip()]
154
+ return sorted(set(changed))
155
+ except Exception:
156
+ return None
157
+
158
+
159
+ # -------------------------------------------------------------------- index
160
+
161
+ def build_index(root):
162
+ rows = []
163
+ for p, meta, _ in list_analyses(root):
164
+ rows.append((
165
+ meta.get("id", "?"),
166
+ meta.get("feature", p.stem.replace("ANALYSIS_", "")),
167
+ meta.get("livello", ""),
168
+ meta.get("stato", "?"),
169
+ meta.get("data_inizio", ""),
170
+ meta.get("data_fine", ""),
171
+ "solutions/" + p.name,
172
+ ))
173
+ rows.sort(key=lambda r: r[0])
174
+ lines = [INDEX_HEADER,
175
+ "# Storico Funzionalita' (generato)",
176
+ "",
177
+ "| ID | Feature | Livello | Stato | Inizio | Fine | Doc |",
178
+ "|---|---|---|---|---|---|---|"]
179
+ for r in rows:
180
+ lines.append("| " + " | ".join(r) + " |")
181
+ return "\n".join(lines) + "\n"
182
+
183
+
184
+ def cmd_index(root):
185
+ target = root / "ai_docs" / "strategic" / "features_history.md"
186
+ target.parent.mkdir(parents=True, exist_ok=True)
187
+ target.write_text(build_index(root), encoding="utf-8")
188
+ print(f"[ok] indice rigenerato: {target}")
189
+ return 0
190
+
191
+
192
+ # ----------------------------------------------------------------- validate
193
+
194
+ def cmd_validate(root):
195
+ errors, warnings = [], []
196
+ ai = root / "ai_docs"
197
+ if not ai.is_dir():
198
+ print(f"[info] {ai} non esiste: nulla da validare (progetto senza documentazione SDLC).")
199
+ return 0
200
+
201
+ # Vision: presenza e dichiarazione di stato
202
+ for name in VISION_FILES:
203
+ f = ai / "vision" / name
204
+ if not f.is_file():
205
+ warnings.append(f"vision/{name} mancante")
206
+ continue
207
+ head = "\n".join(read_text(f).splitlines()[:12])
208
+ m = re.search(r"Stato:\s*(DRAFT|APPROVED)", head)
209
+ if not m:
210
+ errors.append(f"vision/{name}: manca 'Stato: DRAFT|APPROVED' nelle prime righe")
211
+ elif m.group(1) == "DRAFT":
212
+ warnings.append(f"vision/{name} in stato DRAFT: non e' autorita' di gating, da far validare all'utente")
213
+
214
+ # ANALYSIS: frontmatter e sezioni obbligatorie
215
+ seen_ids = {}
216
+ analyses = list_analyses(root)
217
+ for p, meta, text in analyses:
218
+ rel = "solutions/" + p.name
219
+ if not meta:
220
+ errors.append(f"{rel}: frontmatter assente")
221
+ continue
222
+ fid = meta.get("id")
223
+ if not fid:
224
+ errors.append(f"{rel}: campo 'id' mancante")
225
+ elif fid in seen_ids:
226
+ errors.append(f"{rel}: id '{fid}' duplicato (gia' usato in {seen_ids[fid]})")
227
+ else:
228
+ seen_ids[fid] = rel
229
+ stato = meta.get("stato", "")
230
+ if stato not in VALID_STATES:
231
+ errors.append(f"{rel}: stato '{stato}' non valido ({'/'.join(sorted(VALID_STATES))})")
232
+ if not meta.get("data_inizio"):
233
+ errors.append(f"{rel}: 'data_inizio' mancante")
234
+ if stato == "COMPLETED" and not meta.get("data_fine"):
235
+ errors.append(f"{rel}: COMPLETED senza 'data_fine'")
236
+ livello = meta.get("livello")
237
+ if livello and livello.upper() not in VALID_LEVELS:
238
+ warnings.append(f"{rel}: livello '{livello}' non riconosciuto ({'/'.join(sorted(VALID_LEVELS))})")
239
+ if "## Sicurezza" not in text:
240
+ errors.append(f"{rel}: sezione '## Sicurezza e Threat Model' mancante (obbligatoria)")
241
+ for sec in ("## Obiettivo", "## Vision della Feature", "## Impatto", "## Piano d'Azione", "## Strategia di Test", "## Diario"):
242
+ if sec not in text:
243
+ warnings.append(f"{rel}: sezione '{sec}' mancante")
244
+
245
+ # Indice generato allineato
246
+ hist = ai / "strategic" / "features_history.md"
247
+ if analyses:
248
+ if not hist.is_file():
249
+ errors.append("strategic/features_history.md mancante: esegui 'sdlc_check.py index'")
250
+ elif norm_text(read_text(hist)) != norm_text(build_index(root)):
251
+ errors.append("strategic/features_history.md non allineato alle ANALYSIS: esegui 'sdlc_check.py index'")
252
+
253
+ # Handoff: intestazione e freschezza
254
+ hand = ai / "audit" / "handoff.md"
255
+ if hand.is_file():
256
+ m = re.search(r"Data:\s*(\d{4}-\d{2}-\d{2})", read_text(hand))
257
+ if not m:
258
+ warnings.append("audit/handoff.md senza intestazione 'Data: YYYY-MM-DD'")
259
+ else:
260
+ try:
261
+ stamp = datetime.strptime(m.group(1), "%Y-%m-%d").replace(tzinfo=timezone.utc)
262
+ age = (datetime.now(timezone.utc) - stamp).days
263
+ if age > 14:
264
+ warnings.append(f"audit/handoff.md ha {age} giorni: trattarlo come storico, non come stato corrente")
265
+ except ValueError:
266
+ warnings.append("audit/handoff.md: data non interpretabile")
267
+
268
+ for w in warnings:
269
+ print(f"[warn] {w}")
270
+ for e in errors:
271
+ print(f"[ERROR] {e}")
272
+ print(f"\nValidazione: {len(errors)} errori, {len(warnings)} avvisi.")
273
+ return 1 if errors else 0
274
+
275
+
276
+ # ------------------------------------------------------------- audit_plan
277
+
278
+ def parse_audit_plan(root):
279
+ f = root / "ai_docs" / "audit" / "audit_plan.md"
280
+ rows, lines = [], []
281
+ if f.is_file():
282
+ lines = read_text(f).splitlines()
283
+ for i, line in enumerate(lines):
284
+ if not line.strip().startswith("|"):
285
+ continue
286
+ cells = [c.strip() for c in line.strip().strip("|").split("|")]
287
+ if len(cells) < 2:
288
+ continue
289
+ if cells[0].lower() == "percorso" or set(cells[0]) <= set("-: "):
290
+ continue
291
+ rows.append({
292
+ "line": i,
293
+ "path": cells[0],
294
+ "stato": cells[1].upper(),
295
+ "ref": cells[2] if len(cells) > 2 else "",
296
+ "note": cells[3] if len(cells) > 3 else "",
297
+ })
298
+ return f, lines, rows
299
+
300
+
301
+ def cmd_stale(root):
302
+ f, _, rows = parse_audit_plan(root)
303
+ if not rows:
304
+ print(f"[info] nessuna riga in {f}: niente da controllare "
305
+ "(audit non inizializzato, oppure modalita' Hybrid dove la mappatura e' delegata a devPNT).")
306
+ return 0
307
+ use_git = git_available(root)
308
+ stale = []
309
+ for row in rows:
310
+ if row["stato"] != "ANALYZED":
311
+ continue
312
+ rel, ref = row["path"], row["ref"]
313
+ target = root / rel
314
+ if not target.exists():
315
+ print(f"[warn] {rel}: percorso inesistente")
316
+ continue
317
+ changed = []
318
+ if use_git and re.fullmatch(r"[0-9a-fA-F]{7,40}", ref or ""):
319
+ res = git_changed_since(root, ref, rel.replace("\\", "/"))
320
+ if res is None:
321
+ print(f"[warn] {rel}: ref git '{ref}' non risolvibile, impossibile valutare")
322
+ continue
323
+ changed = res
324
+ else:
325
+ ts = parse_iso(ref)
326
+ if ts is None:
327
+ print(f"[warn] {rel}: riferimento '{ref}' non interpretabile (ne' hash git ne' ISO UTC)")
328
+ continue
329
+ for fp in iter_files(target):
330
+ mtime = datetime.fromtimestamp(fp.stat().st_mtime, tz=timezone.utc)
331
+ if mtime > ts + MTIME_GRACE:
332
+ changed.append(str(fp.relative_to(root)).replace("\\", "/"))
333
+ if changed:
334
+ stale.append((rel, changed))
335
+
336
+ if not stale:
337
+ print("[ok] nessuna area analizzata risulta modificata dopo l'ultima analisi.")
338
+ return 0
339
+ print("Aree modificate dopo l'ultima analisi registrata:")
340
+ for rel, changed in stale:
341
+ print(f" {rel} ({len(changed)} file)")
342
+ for c in changed[:10]:
343
+ print(f" - {c}")
344
+ if len(changed) > 10:
345
+ print(f" ... e altri {len(changed) - 10}")
346
+ print("\nDopo la ri-analisi, registra con: sdlc_check.py mark <percorso>")
347
+ return 1
348
+
349
+
350
+ def cmd_mark(root, paths):
351
+ f, lines, rows = parse_audit_plan(root)
352
+ use_git_ref = git_available(root) and not any(
353
+ git_has_changes(root, raw.replace("\\", "/").rstrip("/")) for raw in paths
354
+ )
355
+ ref = git_head(root) if use_git_ref else utc_now_iso()
356
+ by_path = {r["path"].replace("\\", "/").rstrip("/"): r for r in rows}
357
+
358
+ if not lines:
359
+ lines = ["# Piano di Audit", "",
360
+ "| Percorso | Stato | Riferimento | Note |",
361
+ "|---|---|---|---|"]
362
+ rows = []
363
+
364
+ def row_text(path, note):
365
+ return f"| {path} | ANALYZED | {ref} | {note} |"
366
+
367
+ appended = []
368
+ for raw in paths:
369
+ key = raw.replace("\\", "/").rstrip("/")
370
+ display = key + ("/" if (root / key).is_dir() else "")
371
+ existing = by_path.get(key)
372
+ if existing:
373
+ lines[existing["line"]] = row_text(existing["path"], existing["note"])
374
+ print(f"[ok] {existing['path']} -> ANALYZED ({ref})")
375
+ else:
376
+ appended.append(row_text(display, ""))
377
+ print(f"[ok] {display} aggiunto come ANALYZED ({ref})")
378
+
379
+ if appended:
380
+ insert_at = (max(r["line"] for r in rows) + 1) if rows else len(lines)
381
+ lines[insert_at:insert_at] = appended
382
+
383
+ f.parent.mkdir(parents=True, exist_ok=True)
384
+ f.write_text("\n".join(lines) + "\n", encoding="utf-8")
385
+ return 0
386
+
387
+
388
+ def cmd_check(root):
389
+ print("===== validate =====")
390
+ rc_v = cmd_validate(root)
391
+ print("\n===== stale =====")
392
+ rc_s = cmd_stale(root)
393
+ print(f"\ncheck: {'PULITO' if not (rc_v or rc_s) else 'NON PULITO'} "
394
+ f"(validate rc={rc_v}, stale rc={rc_s})")
395
+ return 1 if (rc_v or rc_s) else 0
396
+
397
+
398
+ # --------------------------------------------------------------------- gate
399
+
400
+ def cmd_gate(args):
401
+ file_path = args.file or ""
402
+ if args.hook:
403
+ try:
404
+ # bytes -> utf-8-sig: il payload degli hook e' JSON UTF-8 a prescindere
405
+ # dalla code page della console; '-sig' scarta il BOM (pipe da PowerShell)
406
+ raw = sys.stdin.buffer.read().decode("utf-8-sig", errors="replace")
407
+ payload = json.loads(raw)
408
+ file_path = (payload.get("tool_input") or {}).get("file_path") or ""
409
+ except Exception:
410
+ return 0 # input non interpretabile: non bloccare
411
+ if not file_path:
412
+ return 0
413
+ root = Path(args.root).resolve() if args.root else find_project_root()
414
+ try:
415
+ rel = str(Path(file_path).resolve().relative_to(root)).replace("\\", "/")
416
+ except ValueError:
417
+ return 0 # fuori dal progetto: non di competenza del gate
418
+ if rel.startswith(("ai_docs/", "tests/", "test/")):
419
+ return 0
420
+ protected = [p.strip().replace("\\", "/").rstrip("/")
421
+ for p in (args.protected or "").split(";") if p.strip()]
422
+ if not protected:
423
+ return 0
424
+ if not any(rel == p or rel.startswith(p + "/") for p in protected):
425
+ return 0
426
+ for _, meta, _ in list_analyses(root):
427
+ if meta.get("stato") == "IN_PROGRESS":
428
+ return 0
429
+ sys.stderr.write(
430
+ f"[sdlc gate] '{rel}' e' in un percorso protetto ma nessuna ANALYSIS_*.md e' IN_PROGRESS. "
431
+ "Crea o riattiva l'analisi (Fase 3 di agentic-sdlc) prima di modificare questo file.\n")
432
+ return 2
433
+
434
+
435
+ # --------------------------------------------------------------------- main
436
+
437
+ def main(argv=None):
438
+ common = argparse.ArgumentParser(add_help=False)
439
+ common.add_argument("--root", help="radice del progetto (default: risale fino a trovare ai_docs/)")
440
+
441
+ ap = argparse.ArgumentParser(prog="sdlc_check.py",
442
+ description="Validatore meccanico per Agentic SDLC")
443
+ sub = ap.add_subparsers(dest="cmd", required=True)
444
+ sub.add_parser("check", parents=[common], help="gate di chiusura: validate + stale in un solo comando")
445
+ sub.add_parser("validate", parents=[common], help="verifica coerenza di ai_docs/")
446
+ sub.add_parser("index", parents=[common], help="rigenera features_history.md")
447
+ sub.add_parser("stale", parents=[common], help="aree modificate dopo l'ultima analisi")
448
+ mp = sub.add_parser("mark", parents=[common], help="registra percorsi come ANALYZED")
449
+ mp.add_argument("paths", nargs="+", help="percorsi relativi alla radice del progetto")
450
+ gp = sub.add_parser("gate", parents=[common], help="hook PreToolUse (exit 2 = blocca)")
451
+ gp.add_argument("--hook", action="store_true", help="leggi il payload JSON dell'hook da stdin")
452
+ gp.add_argument("--file", help="percorso file da valutare (alternativa a --hook)")
453
+ gp.add_argument("--protected", default="", help="prefissi protetti separati da ';' (es. \"src/auth;src/crypto\")")
454
+
455
+ args = ap.parse_args(argv)
456
+ if args.cmd == "gate":
457
+ return cmd_gate(args)
458
+
459
+ root = Path(args.root).resolve() if args.root else find_project_root()
460
+ if args.cmd == "check":
461
+ return cmd_check(root)
462
+ if args.cmd == "validate":
463
+ return cmd_validate(root)
464
+ if args.cmd == "index":
465
+ return cmd_index(root)
466
+ if args.cmd == "stale":
467
+ return cmd_stale(root)
468
+ if args.cmd == "mark":
469
+ return cmd_mark(root, args.paths)
470
+ return 0
471
+
472
+
473
+ if __name__ == "__main__":
474
+ sys.exit(main())
@@ -0,0 +1,169 @@
1
+ # Template dei documenti — Agentic SDLC
2
+
3
+ Regole generali:
4
+ - Documenti concisi: ≤ ~80 righe ciascuno (handoff ≤ 20). Se un documento cresce oltre, va diviso, non gonfiato.
5
+ - La conformità al template non è l'obiettivo: se una sezione non ha contenuto reale, scrivi esplicitamente perché non si applica. Mai testo riempitivo.
6
+ - Date sempre assolute, in UTC dove indicato.
7
+
8
+ ## ai_docs/vision/project_vision.md
9
+
10
+ ```markdown
11
+ # Vision del Progetto
12
+ Stato: DRAFT
13
+ <!-- Stato: DRAFT (ricostruita dall'agente, NON è autorità di gating)
14
+ oppure APPROVED (da <chi>, <data>) — solo dopo conferma esplicita dell'utente -->
15
+
16
+ ## North Star
17
+ ## Utenti Target
18
+ ## Problema Centrale
19
+ ## Obiettivi
20
+ ## Non-Obiettivi
21
+ ## Segnali di Successo
22
+ ```
23
+
24
+ ## ai_docs/vision/roadmap.md
25
+
26
+ ```markdown
27
+ # Roadmap
28
+ Stato: DRAFT
29
+
30
+ ## Milestone
31
+ <!-- per ciascuna: beneficio atteso, priorità, indicatore di avanzamento -->
32
+ ```
33
+
34
+ ## ai_docs/vision/principles.md
35
+
36
+ ```markdown
37
+ # Principi Decisionali
38
+ Stato: DRAFT
39
+
40
+ <!-- elenco puntato dei principi stabili che guidano trade-off e scope, dal più critico -->
41
+ ```
42
+
43
+ ## ai_docs/vision/features/VISION_[nome_feature].md
44
+
45
+ Solo per feature che attraversano più ANALYSIS o più milestone: negli altri casi la vision di feature vive nella sezione `## Vision della Feature` dell'ANALYSIS.
46
+
47
+ ```markdown
48
+ # Vision Feature: [Nome]
49
+
50
+ ## Problema
51
+ ## Beneficio Atteso
52
+ ## Utenti o Stakeholder
53
+ ## Segnali di Successo
54
+ ## Non-Obiettivi / Fuori Scope
55
+ ## Vincoli e Principi Collegati
56
+ ```
57
+
58
+ ## ai_docs/solutions/ANALYSIS_[nome_feature].md
59
+
60
+ Il frontmatter è la fonte di verità dello stato della feature (l'indice `features_history.md` si genera da qui).
61
+
62
+ ```markdown
63
+ ---
64
+ id: F-001
65
+ feature: Nome Feature
66
+ stato: PLANNED
67
+ livello: L3
68
+ data_inizio: 2026-06-11
69
+ data_fine:
70
+ ---
71
+ # Analisi della Feature: [Nome]
72
+
73
+ ## Obiettivo
74
+ <!-- cosa si vuole ottenere e quali problemi risolve -->
75
+
76
+ ## Vision della Feature
77
+ <!-- beneficio atteso e problema risolto; allineamento alla vision di progetto
78
+ (citare il documento e il suo stato DRAFT/APPROVED); non-obiettivi/fuori scope
79
+ di questa feature; segnali di successo; stakeholder solo se non ovvi.
80
+ È l'unico posto della vision di feature: il file separato VISION_[feature].md
81
+ si crea solo se la feature attraversa più ANALYSIS o più milestone. -->
82
+
83
+ ## Impatto
84
+ <!-- file esistenti toccati, API/contratti, performance, nuove dipendenze -->
85
+
86
+ ## Sicurezza e Threat Model
87
+ <!-- SEMPRE obbligatoria, anche in standalone.
88
+ Superfici toccate: input esterni, authN/authZ, crittografia, rete, dati personali, filesystem.
89
+ Minacce principali e mitigazioni. "Nessun impatto di sicurezza" va motivato, non dichiarato. -->
90
+
91
+ ## Piano d'Azione
92
+ - [ ] ...
93
+
94
+ ## Strategia di Test
95
+ <!-- unit AAA, integrazione, esempi. Se l'ambiente non è eseguibile (firmware/HIL):
96
+ verifica alternativa esplicita e motivo. -->
97
+
98
+ ## Diario / Stato Corrente
99
+ <!-- aggiornato a ogni milestone: dove sono, ultimo problema, prossimo passo.
100
+ È la fonte dell'handoff per questa feature. -->
101
+ ```
102
+
103
+ Stati ammessi nel frontmatter: `PLANNED` | `IN_PROGRESS` | `COMPLETED` | `CANCELLED`. `COMPLETED` richiede `data_fine`.
104
+
105
+ ## ai_docs/solutions/SPIKE_[tema].md
106
+
107
+ ```markdown
108
+ # Spike: [tema]
109
+
110
+ ## Domanda da rispondere
111
+ ## Time-box
112
+ ## Cosa è stato provato
113
+ ## Risposta / Esito
114
+ ## Conseguenze
115
+ <!-- max 1 pagina. Il codice dello spike NON è mergiabile: per produzione riclassificare L2/L3. -->
116
+ ```
117
+
118
+ ## ai_docs/audit/audit_plan.md (solo modalità Standalone)
119
+
120
+ Il campo `Riferimento` (hash git o timestamp ISO UTC) è gestito da `sdlc_check.py mark` — non compilarlo a mano. La freschezza si verifica con `sdlc_check.py stale`.
121
+
122
+ ```markdown
123
+ # Piano di Audit
124
+
125
+ Stati: PENDING (da analizzare) | ANALYZED (analizzato, con riferimento) | SKIPPED (con motivo).
126
+
127
+ | Percorso | Stato | Riferimento | Note |
128
+ |---|---|---|---|
129
+ | src/core/ | PENDING | - | |
130
+ | vendor/ | SKIPPED | - | codice vendored |
131
+ ```
132
+
133
+ ## ai_docs/audit/handoff.md
134
+
135
+ Solo un puntatore, ≤ 20 righe. Il dettaglio vive nel Diario di ciascuna ANALYSIS.
136
+
137
+ ```markdown
138
+ # Handoff
139
+ Data: 2026-06-11 (UTC)
140
+ Branch: feature/sso-login
141
+ Agente: Claude
142
+
143
+ ## Feature attive
144
+ - F-001 — vedi solutions/ANALYSIS_login_sso.md (sezione Diario)
145
+
146
+ ## Prossimo passo
147
+ <!-- una riga -->
148
+
149
+ ## Note di sessione
150
+ <!-- vision lette in questa sessione? draft da far validare? -->
151
+ ```
152
+
153
+ ## ai_docs/strategic/architecture.md e existing_features.md
154
+
155
+ Invariati rispetto alla v1:
156
+
157
+ ```markdown
158
+ # Architettura del Progetto
159
+ ## Stack Tecnologico
160
+ ## Struttura delle Directory
161
+ ## Pattern Architetturali
162
+ ```
163
+
164
+ ```markdown
165
+ # Funzionalità Esistenti
166
+ - [ID] **Nome Feature**: Descrizione
167
+ ```
168
+
169
+ `ai_docs/strategic/features_history.md` NON ha template: è generato da `sdlc_check.py index`.