Compare commits

..
4 Commits
Author SHA1 Message Date
alessandro barucci 1bc033c329 ff 2026-10-26 00:25:18 +01:00
alessandro barucci 1df884da84 Update build_container.sh 2026-10-18 18:34:42 +02:00
alessandro barucci 34213918fe varie 2026-10-11 23:49:35 +02:00
alessandro 23f276417f Create IDP-AgentPlatform.pdf 2026-09-27 11:23:06 +02:00
79 changed files with 4370 additions and 10 deletions
@@ -0,0 +1,154 @@
| | **Langflow** | **Kagent** |
| --------------- | -------------------------------------------- | --------------------------------------- |
| **Fase** | **Build time** | **Run time** |
| **Ruolo** | AI Agent Builder | AI Agent Runtime / Deployment |
| **Funzione** | Creare, configurare e testare agenti AI | Distribuire ed eseguire agenti AI |
| **Interfaccia** | Visuale / low-code | Kubernetes-native / YAML / CRD |
| **Deployment** | Può essere installato come servizio dell'IDP | È parte del runtime Kubernetes dell'IDP |
| **Output** | Flow/agent e relativo artefatto | Agent in esecuzione su Kubernetes |
Quindi:
### Langflow — Build Time
> **Software per progettare, costruire e testare AI Agent.**
Può essere installato **centralmente come servizio dell'IDP**, accessibile tramite browser dai developer.
Il developer può costruire, ad esempio:
```text
LLM
│
├── Knowledge Base
├── MCP Tool
├── API
└── Business Logic
│
▼
Agent
```
Il risultato può poi diventare un **artefatto software versionabile e deployabile**.
---
### Kagent — Run Time
> **Piattaforma Kubernetes-native per il deployment e l'esecuzione degli AI Agent.**
E qui la tua osservazione è importante: ci sono **due modalità**.
#### 1. Agent semplice definito direttamente
Per agenti relativamente semplici, la definizione può essere direttamente nella configurazione Kubernetes/Kagent:
```yaml
kind: Agent
metadata:
name: my-agent
spec:
model: ...
tools:
- ...
```
Quindi:
```text
YAML
↓
Kagent
↓
Agent Runtime
↓
Kubernetes
```
#### 2. Agent sviluppato con Langflow
Per agenti più complessi:
```text
Langflow
│
│ Build
▼
Agent Artifact
│
│ CI/CD
▼
Registry / Git
│
│ Deploy
▼
Kagent
│
▼
Kubernetes
│
▼
Running Agent
```
### La precisazione più importante
Direi però **"artefatti costruiti con Langflow"** piuttosto che:
> "Kagent esegue Langflow"
perché sono due cose diverse.
Kagent **non è il runtime di Langflow**. Il concetto corretto è:
> **Langflow è uno strumento di authoring/build; Kagent è uno strumento di deployment/runtime.**
L'artefatto prodotto da Langflow deve essere compatibile con il modello di deployment/runtime che hai scelto.
---
### Quindi il modello del tuo IDP diventa molto chiaro
```text
IDP
│
┌──────────┴──────────┐
│ │
BUILD TIME RUN TIME
│ │
┌─────────┐ ┌─────────┐
│Langflow │ │ Kagent │
│ │ │ │
│ Build │──────────►│ Deploy │
│ Test │ Artifact │ Execute │
└─────────┘ └────┬────┘
│
▼
Kubernetes
```
E aggiungerei un **terzo livello trasversale**:
```text
IDP GOVERNANCE
┌────────────────────────────┐
│ Security │
│ RBAC │
│ Secrets │
│ Network Policy │
│ Resource Quotas │
│ Approved Models │
│ Approved MCP │
│ Versioning │
│ CI/CD │
│ Observability │
│ Compliance │
└────────────────────────────┘
│
┌─────────┴─────────┐
▼ ▼
Langflow Kagent
Build Time Run Time
```
**Questa secondo me è la rappresentazione più corretta per il tuo progetto IDP:** Langflow e Kagent non sono concorrenti, ma **componenti di due fasi diverse del lifecycle dell'Agent**.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,146 @@
# =====================================================================
# ToolServer — Gitea MCP (ufficiale), token con permessi SOLO lettura
# =====================================================================
apiVersion: kagent.dev/v1alpha2
kind: ToolServer
metadata:
name: gitea-mcp
namespace: platform-architect
spec:
type: RemoteMCPServer
remoteMCPServer:
url: "http://gitea-mcp.platform-tools.svc.cluster.local:8080"
# Il token Gitea sottostante (configurato nel deployment di
# gitea-mcp, non qui) deve avere SOLO permesso di lettura
# (repo:read) sull'organizzazione idp-templates — l'agente non
# deve mai poter scrivere/modificare i template
---
# =====================================================================
# Agent dichiarativo — l'architetto esperto della IDP
# =====================================================================
apiVersion: kagent.dev/v1alpha2
kind: Agent
metadata:
name: idp-architect
namespace: platform-architect
spec:
description: >
Architetto esperto della Internal Developer Platform. Analizza le
specifiche applicative di un dev team e propone lo scenario
architetturale compliant, basandosi sul catalogo ufficiale dei
template della piattaforma.
type: Declarative
declarative:
modelConfig: gpt4o-config
a2aConfig:
skills:
- id: propose-architecture
name: Propose Architecture
description: >
Data una specifica applicativa, propone il template IDP
più adatto con motivazione e piano di customizzazione.
inputModes: ["text"]
outputModes: ["text"]
tags: ["architecture", "idp", "platform-engineering"]
systemMessage: |
# Ruolo
Sei l'architetto esperto della Internal Developer Platform (IDP)
di pigreco66.it. Il tuo compito è aiutare i dev team a scegliere
e adottare il template architetturale più corretto tra quelli
ufficialmente supportati dalla piattaforma, a fronte delle loro
specifiche applicative.
Il tuo obiettivo non è solo tecnico: sei anche lo strumento
principale di ADOPTION della piattaforma. Un dev team che parla
con te deve percepire che usare l'IDP è PIÙ VELOCE e PIÙ SICURO
che costruire da zero, non un vincolo burocratico.
# Fonte di verità — NON INVENTARE TEMPLATE
L'unica fonte di verità sui template disponibili è il catalogo
ufficiale, accessibile tramite i tool Gitea MCP:
1. Leggi SEMPRE prima l'indice aggregato:
repo "idp-templates-catalog", file "catalog-index.yaml"
2. Per i template candidati migliori (massimo 2-3), leggi il
dettaglio completo nel rispettivo repo, file
"idp-template.yaml", per verificare compliance, parametri e
vincoli (not_suitable_for) prima di confermare la proposta.
Non proporre MAI un'architettura o un componente che non esiste
nel catalogo. Se nessun template è adeguato, dillo esplicitamente
(vedi sezione "Quando non c'è un template adatto").
# Processo da seguire
1. **Raccogli le specifiche**: se l'input del dev team è
incompleto (manca almeno una di: tipo di workload,
persistenza dati, pattern di comunicazione, volumetria
attesa, classificazione dati/PII), fai domande di
chiarimento mirate PRIMA di proporre. Non indovinare requisiti
critici per sicurezza/compliance.
2. **Fai matching contro il catalogo** usando i tool Gitea MCP
come descritto sopra.
3. **Verifica i servizi cross della piattaforma**: controlla il
campo "cross_platform_services" del template candidato — se
il dev team descrive un bisogno già coperto da un servizio
condiviso (es. "mi serve una cache" quando esiste
shared-redis-cache), segnalalo esplicitamente: NON serve
che lo costruiscano, è già disponibile.
4. **Proponi**, con questa struttura:
- Template consigliato (id + nome + link al repo)
- Perché è compliant con le specifiche (motivazione tecnica)
- Eventuali servizi cross della piattaforma da riusare
- Parametri da personalizzare (dai file in env/)
- Eventuali gap o customizzazioni necessarie oltre al
template base
- Se pertinente, alternative scartate e perché
5. Se richiesto, genera il comando/istruzioni operative per
clonare/istanziare il template (basati sulla struttura reale
del repo: kubernetes/, containers/, src/, env/).
# Quando NON c'è un template adatto
Se nessun template del catalogo copre adeguatamente le
specifiche (es. gap reale, non solo mancanza di dettagli),
dillo chiaramente al dev team e segnala che si tratta di un
caso da portare al platform engineering team per valutare
l'introduzione di un nuovo template. NON improvvisare
un'architettura custom spacciandola per "compliant" — altrimenti
perdi la fiducia che è il tuo scopo costruire.
# Perimetro di azione — SOLO CONSULTIVO
In questa fase operi in modalità puramente consultiva: la tua
proposta è testuale e informativa, non esegui mai direttamente
azioni che modificano repository, pipeline o infrastruttura
(non crei repo, non apri PR, non modifichi file). Se il dev team
ti chiede di "farlo per loro" (es. "creamelo tu il repo"),
rispondi spiegando i passi operativi che devono seguire loro
stessi (clonare il template, personalizzare env/), senza
eseguirli al posto loro.
# Tono
Sei un collega esperto, non un gatekeeper burocratico. Spiega
il "perché" delle scelte, non solo il "cosa" — un dev team che
capisce il ragionamento adotta la piattaforma più volentieri di
uno a cui viene solo imposto un verdetto.
tools:
- type: McpServer
mcpServer:
name: gitea-mcp
kind: RemoteMCPServer
toolNames:
- get_file_content
- get_dir_content
- list_repo_commits
@@ -0,0 +1,22 @@
apiVersion: kagent.dev/v1alpha2
kind: AgentHarness
metadata:
name: idp-architect-slack
namespace: platform-architect
spec:
backend: hermes
agentRef:
name: idp-architect
namespace: platform-architect
channels:
- name: idp-architect-chat
type: slack
slack:
botToken:
valueFrom: { type: Secret, name: slack-tokens, key: bot-token }
appToken:
valueFrom: { type: Secret, name: slack-tokens, key: app-token }
# Canale dedicato consigliato: #idp-architect-support
# così tutti i dev team sanno dove chiedere, e le conversazioni
# restano visibili/searchable per il resto del team (effetto
# knowledge-sharing, non solo 1:1 privato)
@@ -0,0 +1,15 @@
# idp-templates-catalog/catalog-index.yaml
templates:
- id: tpl-java-springboot-rest-api
name: "Java Spring Boot — REST API sincrona"
workload_type: [sync-api, rest]
tags: [java, spring-boot, rest, sync, stateless]
status: active
repo_url: "https://gitea.pigreco66.it/idp-templates/java-springboot-rest-api"
- id: tpl-node-kafka-consumer
name: "Node.js Kafka Consumer"
workload_type: [async-consumer, event-driven]
tags: [nodejs, kafka, async]
status: active
repo_url: "..."
# ... altri 18
@@ -0,0 +1,86 @@
# idp-template.yaml — da posizionare nella root di ogni repo Gitea
# di tipo "template". Questo è l'esempio compilato per un template
# "REST API sincrona Java/Spring Boot".
id: tpl-java-springboot-rest-api
name: "Java Spring Boot — REST API sincrona"
version: "2.1.0"
status: active # active | beta | deprecated
last_updated: "2026-09-01"
maintainer: "platform-engineering@pigreco66.it"
repo_url: "https://gitea.pigreco66.it/idp-templates/java-springboot-rest-api"
# --- Classificazione, usata dall'agente per il primo filtro ---
category: api-service
workload_type:
- sync-api
- rest
tech_stack:
language: Java 21
framework: Spring Boot 3.3
build_tool: Maven
tags: ["java", "spring-boot", "rest", "sync", "stateless"]
description: >
Template per esporre una REST API sincrona stateless. Include
healthcheck, metrics endpoint Prometheus-ready, struttura a layer
controller/service/repository, logging strutturato JSON.
# --- Cosa contiene esattamente la struttura del repo ---
components_included:
kubernetes:
- Deployment (con probe readiness/liveness preconfigurate)
- Service (ClusterIP)
- HorizontalPodAutoscaler
- NetworkPolicy (default-deny + regole minime)
containers:
- Dockerfile multi-stage (build + runtime minimale distroless)
src:
- hello-world Spring Boot funzionante, con un endpoint /health e
un endpoint di esempio /api/v1/ping
env:
- application-dev.properties
- application-staging.properties
- application-prod.properties
# --- Integrazione con servizi cross della piattaforma ---
# Fondamentale: dice all'agente COSA NON riproporre perché già
# disponibile centralmente (evita duplicazione di servizi)
cross_platform_services:
- service: shared-postgres
how: "via env PLATFORM_DB_CONNECTION_STRING, iniettata dalla pipeline"
- service: shared-redis-cache
how: "opzionale, abilitabile via env ENABLE_CACHE=true"
- service: centralized-logging
how: "automatico, nessuna configurazione richiesta"
# --- Vincoli di compliance soddisfatti dal template ---
compliance:
data_classification_supported: ["public", "internal", "confidential"]
# NON supporta dati PII senza customizzazioni aggiuntive
pii_ready: false
network_policy_baseline: "default-deny"
security_scanning: "incluso in pipeline (SAST + container scan)"
scaling:
default_replicas: 2
autoscaling: hpa
min_replicas: 2
max_replicas: 10
# --- Parametri personalizzabili (quelli nei file in env/) ---
parameters:
- name: PLATFORM_DB_CONNECTION_STRING
required: false
description: "Richiesta solo se il servizio necessita persistenza"
- name: ENABLE_CACHE
required: false
default: "false"
# --- Quando NON usare questo template (altrettanto utile del 'quando sì') ---
not_suitable_for:
- "Workload asincroni basati su eventi (usa tpl-java-kafka-consumer)"
- "Batch job schedulati (usa tpl-batch-job)"
- "Dati classificati come PII senza data masking aggiuntivo"
@@ -0,0 +1,6 @@
STS Lab Srl — Spec-Driven Reader (skill per Claude Code)
Copyright (c) 2026 STS Lab Srl. Tutti i diritti riservati.
Questa skill (SKILL.md, references/, scripts/) è di proprietà di STS Lab Srl.
L'uso è riservato a STS Lab Srl, ai suoi collaboratori e ai clienti autorizzati.
Non è consentita la ridistribuzione o la modifica senza autorizzazione scritta di STS Lab Srl.
@@ -0,0 +1,248 @@
---
name: sts-spec-driven
description: "STS Lab Srl — Spec-Driven Reader. Legge e SCRIVE la documentazione spec-driven di un progetto (cartella docs/ con PROJECT_MEMORY, REQUIREMENTS REQ-*, specifiche SPEC-*, task T-*, decisioni D-*, difetti DIF-*, TRACEABILITY, TODO, DECISIONS, CHANGELOG, DEVELOPMENT_LOG). Lettura: trasforma la documentazione in un brief di implementazione prima del codice e chiude il ciclo dopo. Scrittura: se il progetto non ha documentazione la crea da zero facendo scegliere fra layout strutturato (00-INDEX + 01-product…08-project) e piatto, con modelli, intervista e controllo di coerenza; aggiunge nuove spec, requisiti e decisioni a una documentazione esistente. Usala all'inizio di ogni sessione di sviluppo, quando l'utente cita un ID (REQ-, SPEC-, T-, D-, DIF-), chiede di implementare una spec o un task, chiede «leggi la documentazione», «da dove riprendo», «che stato ha il progetto», «crea la documentazione», «imposta lo spec driven», «scrivi la spec di …», «nuovo progetto», o quando si inizia a sviluppare in un progetto senza docs/."
---
# Spec-Driven Reader — STS Lab Srl
> **STS Lab Srl** · Skill proprietaria per lo sviluppo software guidato da specifiche.
> © 2026 STS Lab Srl — tutti i diritti riservati.
Nei progetti STS Lab la documentazione **precede** il codice e ne è il contratto:
```
REQ-xxx cosa deve fare il prodotto (REQUIREMENTS)
▼
SPEC-xxx come lo fa: API, dati, regole, (03-specs/ o documenti di modulo)
│ errori, criteri di accettazione
▼
T-xxx unità di lavoro implementabile (TODO)
▼
codice + test
▼
TRACEABILITY chiude il cerchio: ogni REQ ha spec, task e test che passano
```
Ogni scelta strutturale è una decisione numerata `D-xxx`; ogni difetto noto è un `DIF-xx`.
Questa skill serve a **scrivere** quella catena quando manca, a **leggerla bene** prima di
toccare il codice, e a **richiuderla** dopo.
### Quale modalità
| Situazione | Modalità | Dove |
|---|---|---|
| Il progetto **non ha** documentazione spec-driven (o l'utente chiede di crearla) | **Scrittura** | §A qui sotto + [references/scrittura.md](references/scrittura.md) |
| C'è solo lo scheletro, con molti `DA COMPILARE` | **Scrittura** (completamento) | idem, dallo step in corso |
| Serve una spec, un requisito o una decisione **nuovi** | **Scrittura** (aggiunta) | [references/scrittura.md](references/scrittura.md) §6 |
| La documentazione c'è e si deve sviluppare | **Lettura** | §1–§8 |
---
## 0. Strumento: `scripts/spec_index.py`
Libreria standard Python 3.8+. Usalo **prima** di aprire i file grandi: DECISIONS e
DEVELOPMENT_LOG possono superare le 3–6 000 righe e non vanno mai letti interi.
```bash
S=~/.claude/skills/sts-spec-driven/scripts/spec_index.py
# lettura (non scrive nulla)
python $S overview <docs> # layout, documenti chiave, stato delle spec, segnaposto
python $S context <docs> SPEC-004 # definizione + ID collegati + TODO + TRACEABILITY
python $S find <docs> D-014 # dove è definito e chi lo cita
python $S check <docs> # coerenza: orfani, requisiti scoperti, derive di stato
python $S next-id <docs> SPEC|D|DIF|T|REQ [--area AUT] [--spec SPEC-004] [--prefisso W]
# scrittura (non sovrascrive mai un file esistente)
python $S scaffold <docs> --layout strutturato|piatto --progetto "Nome"
python $S new-spec <docs> --titolo "Titolo" [--prefisso W (solo layout piatto)]
```
Su Windows da PowerShell il `~` non viene espanso: usa `$HOME\.claude\skills\sts-spec-driven\scripts\spec_index.py`
(oppure lancialo dal tool Bash, dove `~` funziona).
- Per una SPEC, `context` stampa l'indice delle sezioni con i numeri di riga: leggi poi solo
le sezioni che servono con `Read offset/limit`.
- `check` esce con codice 1 se trova problemi; riporta all'utente cosa ha trovato.
Se Python non c'è, ripiega su `Grep` con il pattern
`REQ-[A-Z]+-\d+|SPEC-\d{3}|T-[A-Z]{0,2}\d+|D-\d{3}|DIF-\d+` e copia a mano i modelli da
`templates/`.
---
## A. Scrittura — creare la documentazione
Procedura completa in [references/scrittura.md](references/scrittura.md). In sintesi:
1. **Verifica** che non esista già (§1). Se esiste solo in parte, non ricrearla: completala.
2. **Fai scegliere il layout all'utente** con `AskUserQuestion`, mettendo per primo con
«(Consigliato)» quello adatto — *Strutturato* per progetti con più moduli, integrazioni,
riscritture, committente esterno; *Piatto* per progetti piccoli, prototipi, un solo modulo.
Chiedi anche dove crearla (default `docs/` alla radice del repository).
3. **Crea lo scheletro** con `scaffold`. I modelli stanno in `templates/strutturato/`,
`templates/piatto/`, `templates/spec.md`, `templates/modulo.md`.
4. **Raccogli le informazioni**: prima leggi README, manifest, codice; poi intervista l'utente
(al massimo 4 domande per giro, solo ciò che manca). Se c'è codice senza documenti,
ricostruisci i requisiti dal codice come tipo **P** con *Origine* `file:funzione`.
5. **Scrivi nell'ordine degli step** (visione → requisiti → architettura → spec → contratto e
dati → devops → qualità e backlog), con un riepilogo e la conferma dell'utente a fine step.
6. **Nuove spec** con `new-spec`: numerazione, blocco di task e lettera dei criteri sono
automatici. Nascono sempre «da approvare».
7. **Chiudi** con `check` e riporta l'esito.
Regole che non si derogano:
- **Non inventare**: ciò che non è noto resta `DA COMPILARE` o diventa una domanda.
- **Mai approvare da solo**: una spec diventa *Approvata* solo dopo un sì esplicito
dell'utente (procedura in scrittura.md §5).
- **Mai sovrascrivere** documenti esistenti; i contenuti sparsi si spostano nei documenti
giusti, con l'accordo dell'utente.
---
## 1. Trovare la documentazione
Cerca nell'ordine: `docs/`, `src/docs/`, `*/src/docs/`, `documentation/`. Una cartella è
spec-driven se contiene almeno `PROJECT_MEMORY.md` o `00-INDEX.md` più uno fra `DECISIONS.md`,
`TODO.md`, `REQUIREMENTS.md`. Leggi anche `CLAUDE.md`/`README.md` alla radice del repository:
possono indicare la cartella o regole locali.
**Se non la trovi**, non scrivere codice: di' all'utente che il progetto non ha documentazione
spec-driven e proponi di crearla (§A). Se l'utente preferisce procedere senza, rispetta la
scelta e annotala nella risposta.
Due layout ricorrenti (dettagli in [references/layout.md](references/layout.md)):
| Layout | Riconoscimento | Esempio |
|---|---|---|
| **Strutturato** | cartelle `01-product/ … 08-project/`, `00-INDEX.md`, `03-specs/SPEC-xxx-*.md` | `KadeonCad_v2/docs` |
| **Piatto** | tutti i `.md` in una cartella, specifiche come documenti di modulo (`CAD_MODULE.md`, `whatsapp/DESIGN.md`) | `Kadeon/kadeon/src/docs` |
---
## 2. Lettura di inizio sessione (sempre, prima del codice)
1. **`overview`** con lo script.
2. **`PROJECT_MEMORY.md`** — per intero. Contiene ciò che non è deducibile dal codice e che, se
dimenticato, fa ripetere errori: vincoli non negoziabili, azioni pendenti di sicurezza, «le
cose da non dimenticare mai». Tienile presenti per tutta la sessione.
3. **`00-INDEX.md`** (layout strutturato) — tabella degli step, stato dell'implementazione e i
riquadri `> **Titolo (data):**` in testa: sono le novità più recenti, in ordine inverso.
4. **Indice di `DECISIONS.md`** — solo la tabella iniziale; le singole decisioni si leggono
quando servono.
5. **`TODO.md`** — sezione «fuori sequenza / prima di tutto» e la fase corrente.
Non leggere CHANGELOG, DEVELOPMENT_LOG, openapi.yaml per intero: si interrogano per ID o data.
---
## 3. Prima di implementare: il cancello
Rispondi a queste domande **prima** di scrivere una riga. Se una risposta è «no», fermati e
dillo all'utente.
| # | Domanda | Dove si verifica | Se «no» |
|:-:|---|---|---|
| 1 | Il lavoro richiesto corrisponde a un `T-xxx`? | TODO, sezione *Task* della spec | Proponi il task e la spec di appartenenza |
| 2 | Il task appartiene a una `SPEC-xxx` in stato **Approvata**? | riga `Stato:` della spec, 00-INDEX | Non implementare: segnala lo stato (*da approvare*, 🟡) e chiedi |
| 3 | Le dipendenze del task (colonna *Dipende da*) sono ✅? | TODO, tabella task della spec | Segnala l'ordine corretto |
| 4 | Nessuna decisione `D-xxx` **accettata** lo vieta o lo vincola diversamente? | DECISIONS (per ID), PROJECT_MEMORY | Cita la decisione; una deviazione richiede una nuova `D-xxx` |
| 5 | Se tocca un requisito di **parità** (tipo `P`) cambia il comportamento? | REQUIREMENTS, colonna *Tipo* | È una decisione di prodotto: serve una `D-xxx` |
| 6 | Contratto API e schema dati sono già scritti? | `openapi.yaml` / API.md, DATABASE.md | Si scrive prima il contratto (contract-first) |
Eccezione legittima: richiesta esplicita dell'utente di procedere comunque. In quel caso
procedi, ma registra la deviazione (vedi §6) — è ciò che i progetti chiamano «inversione»
e che va lasciata scritta.
---
## 4. Leggere una spec
Struttura tipica di una `SPEC-xxx` (le sezioni possono variare):
1. Intestazione: `Stato`, versione, data, **requisiti coperti**, decisione di riferimento.
2. Contesto e obiettivo — il *perché*; spesso contiene un dato misurato che giustifica la scelta.
3. «Cosa cambia rispetto a ciò che è già scritto» — elenco dei punti fermi che la spec modifica.
4. Regole / algoritmo in **forma normativa**: le parole in grassetto e i «**non**» sono vincoli.
5. Contratto API: rotta, ingresso, uscita, **tabella degli esiti con codici HTTP**.
6. Errori.
7. **Criteri di accettazione** (`D1…D12`, `P1…P24`, …): ognuno diventa almeno un test.
8. **Task** con dipendenze.
Regole di lettura:
- Leggi la spec **intera** quando la implementi; usa l'indice di `context` per le altre.
- I paragrafi «è una decisione, non una dimenticanza» descrivono comportamenti voluti che
sembrano bug: non «correggerli».
- Sezioni con suffisso `bis`/`ter` (es. §5.3bis) sono aggiunte successive: leggile, spesso
modificano la regola base.
- Per ogni `REQ-` citato leggi la riga in REQUIREMENTS: colonna **Tipo** (P parità / C
correzione / N nuovo) e **Origine** (per i `P` la fonte di verità è il codice della versione
precedente indicato lì).
- Per ogni `D-` citata leggi **solo** la sezione `## D-xxx` (Contesto → Decisione →
Alternative → Conseguenze). Una decisione *Superata da D-yyy* non vale più: segui D-yyy.
- Il glossario (`GLOSSARY.md`) fissa i termini: usa gli stessi nomi nel codice e nei messaggi.
---
## 5. Il brief di implementazione
Prima di scrivere codice, presenta all'utente un brief compatto con il modello in
[references/brief-template.md](references/brief-template.md): obiettivo, stato della spec,
requisiti, regole normative, contratto, criteri di accettazione → test, task e dipendenze,
decisioni vincolanti, file di codice probabilmente toccati, **incoerenze trovate**.
Per un task piccolo il brief può stare in poche righe; non saltare mai le sezioni *criteri* e
*incoerenze*.
---
## 6. Dopo l'implementazione: chiudere il ciclo
Aggiorna la documentazione **nella stessa modifica del codice**, seguendo
[references/chiusura-ciclo.md](references/chiusura-ciclo.md). In sintesi:
| Documento | Cosa scrivere | Quando |
|---|---|---|
| TODO | stato del task (⬜ → 🟡 → ✅) | ✅ solo con test passanti |
| TRACEABILITY | stato della riga REQ, criteri coperti | **quando il test passa, non quando il codice è scritto** |
| spec | nuove sezioni `bis`, criteri e task aggiunti | se la regola cambia o si estende |
| DECISIONS | nuova `D-xxx` (mai modificarne una vecchia: la si *supera*) | ogni scelta strutturale o deviazione |
| CHANGELOG | cosa cambia **per chi usa il prodotto** | ogni modifica visibile |
| DEVELOPMENT_LOG | cosa si è scoperto e **perché** si è deciso così | scoperte, tentativi respinti, misure |
| PROJECT_MEMORY | solo ciò che, dimenticato, fa ripetere un errore | raramente |
| 00-INDEX | riquadro `> **Titolo (data):**` in testa + date «Ultimo aggiornamento» | ogni consegna rilevante |
Riporta sempre il numero di test eseguiti e il loro esito reale (es. «backend 614 (+3)»).
Non dichiarare ✅ ciò che non è stato verificato: scrivi 🟡 e cosa manca.
---
## 7. Incoerenze e fonti di verità
La documentazione viva va in deriva: indici non aggiornati, totali vecchi, spec marcata «da
approvare» ma task già ✅, ID citati e mai definiti (l'`overview` li elenca). Quando le trovi:
1. **Non correggere in silenzio e non sceglierne una a caso.** Elencale nel brief.
2. Applica la gerarchia delle fonti di verità dichiarata dal progetto. Default:
- contratto API → `openapi.yaml` (non API.md, non il codice);
- requisiti di parità → codice della versione precedente citato in *Origine*;
- stato «verificato» → test **eseguiti** (non la TRACEABILITY);
- decisioni → l'ultima `D-xxx` accettata che non è superata;
- stato di una spec → la sua riga `Stato:`; in assenza, l'indice.
3. Se l'incoerenza blocca il lavoro, chiedi all'utente; altrimenti prosegui e proponi la
correzione documentale insieme alla consegna.
---
## 8. Cose da non fare
- Implementare da una spec *da approvare* o da un'idea non scritta, senza dirlo.
- «Modernizzare» ciò che un requisito di parità o una decisione congela.
- Rinumerare o riusare un ID: gli ID sono **immutabili**; quelli eliminati si barrano (`~~T-005~~`).
- Cancellare una decisione o una voce di diario: si superano o si annotano.
- Leggere per intero file da migliaia di righe quando basta una sezione.
- Copiare segreti trovati nella documentazione in risposte, log o commit.
---
<sub>**STS Lab Srl** · Spec-Driven Reader v1.1 (lettura + scrittura) · 2026-10-16 · uso interno e per i clienti STS Lab Srl</sub>
@@ -0,0 +1,42 @@
# Modello — Brief di implementazione (STS Lab Srl)
Da presentare all'utente **prima** di scrivere codice. Ogni affermazione porta il riferimento
`file:riga` o l'ID da cui viene. Togli le sezioni vuote, mai *Criteri* e *Incoerenze*.
```markdown
## Brief · <T-xxx / SPEC-xxx> — <titolo breve>
*STS Lab Srl · Spec-Driven Reader*
**Obiettivo** — una frase: cosa cambia per chi usa il prodotto.
**Cancello**
| Verifica | Esito |
|---|---|
| Spec | SPEC-xxx · Stato: Approvata v1.0 (03-specs/SPEC-xxx.md:3) |
| Task | T-xxx · ⬜ · dipende da T-yyy ✅, T-zzz 🟡 |
| Decisioni vincolanti | D-0nn (sintesi in una riga) |
| Parità | REQ-ABC-01 è **P**: comportamento da riprodurre identico |
**Requisiti coperti** — REQ-… con il testo essenziale e il tipo (P/C/N).
**Regole normative** — punti elenco presi dalla spec, con i «non» in evidenza.
**Contratto**
- API: `METODO /percorso` → ingresso, uscita, esiti/codici (openapi.yaml:riga)
- Dati: tabelle/colonne/vincoli toccati (DATABASE.md §n)
- Stati: transizioni ammesse (DOMAIN_WORKFLOW.md §n)
**Criteri di accettazione → test**
| Criterio | Test previsto | Stato attuale (TRACEABILITY) |
|---|---|---|
| D1 | … | ✅ / 🟡 / ⬜ |
**Piano** — passi in ordine, file di codice probabilmente toccati.
**Incoerenze trovate** — documento A dice X, documento B dice Y; fonte di verità scelta e perché.
Se nessuna: «nessuna rilevata».
**Domande per l'utente** — solo quelle che bloccano.
```
<sub>STS Lab Srl · Spec-Driven Reader</sub>
@@ -0,0 +1,48 @@
# Chiusura del ciclo — STS Lab Srl
Checklist da seguire a lavoro finito, **nella stessa modifica del codice**. Ogni voce è
condizionale: aggiorna solo ciò che il lavoro ha davvero toccato.
## 1. Verifica prima di scrivere «fatto»
- [ ] I test dei criteri di accettazione del task esistono e sono stati **eseguiti**.
- [ ] Annotato il conteggio reale della suite (es. «backend 614 (+3), tutti passanti»).
- [ ] Se una verifica è stata fatta solo con test e non contro il sistema reale, scriverlo.
- [ ] Se qualcosa non è verificabile ora (cluster, casella reale, fornitore esterno), aprire
un task `T-xxx` per la verifica mancante invece di dichiarare ✅.
## 2. Documenti di tracciamento
- [ ] **TODO** — stato del task; nuovi task con l'ID successivo libero, la spec e lo stato.
Data «Ultimo aggiornamento» (e «precedente», se il file la usa).
- [ ] **TRACEABILITY** — righe REQ interessate: criteri coperti e stato. ✅ solo con test passanti.
Aggiornare anche la riga di copertura dell'area.
- [ ] **Spec** — se la regola è cambiata o estesa: nuova sezione `§n.bis`, nuovi criteri
(lettera della spec + numero successivo), nuovi task nella tabella *Task*.
- [ ] **REQUIREMENTS** — nuovo `REQ-<AREA>-<nn>` se nasce un comportamento nuovo; tipo N
con la `D-xxx` d'origine; nuovo `DIF-nn` se è emerso un difetto.
## 3. Decisioni e memoria
- [ ] **DECISIONS** — una `D-xxx` nuova per ogni scelta strutturale, deviazione da una spec o
cambio di un requisito di parità. Formato: Contesto → Decisione → Alternative valutate →
Conseguenze. Aggiungerla anche alla tabella indice. Se sostituisce una decisione,
marcare la vecchia «Superata da D-xxx» senza cancellarne il testo.
- [ ] **DEVELOPMENT_LOG** — voce in testa (ordine cronologico inverso): `## AAAA-MM-GG (n) · titolo`.
Cosa si è scoperto, cosa è stato tentato e respinto e **perché**, misure reali.
- [ ] **PROJECT_MEMORY** — solo se è emerso un vincolo che, dimenticato, farebbe ripetere un errore.
## 4. Comunicazione
- [ ] **CHANGELOG** — sezione *[Non rilasciato]*; linguaggio per l'utente del prodotto
(Aggiunto / Modificato / Corretto), non dettagli interni.
- [ ] **00-INDEX** — riquadro in testa `> **Titolo (AAAA-MM-GG):** …` con gli ID coinvolti
(`REQ-…`, `D-…`, `T-…`) e il conteggio dei test; aggiornare la data dell'indice e la
riga della spec nella mappa se ne cambia lo stato o il numero di criteri.
## 5. Coerenza finale
- [ ] Rieseguire `spec_index.py overview <docs>`: nessun nuovo ID «citato ma mai definito».
- [ ] Nessun segreto, password o chiave nei documenti aggiornati.
<sub>STS Lab Srl · Spec-Driven Reader</sub>
@@ -0,0 +1,91 @@
# Layout della documentazione — STS Lab Srl
Due forme ricorrenti nei progetti STS Lab Srl. La skill deve funzionare con entrambe e con
varianti intermedie. Per un progetto nuovo l'utente sceglie quale creare: i modelli sono in
`templates/strutturato/` e `templates/piatto/` (vedi [scrittura.md](scrittura.md)).
---
## A. Layout strutturato (es. `KadeonCad_v2/docs`)
```
docs/
├── 00-INDEX.md mappa, stato degli step, novità recenti in riquadri datati
├── 01-product/ COSA e PER CHI
│ ├── VISION.md perché esiste, perimetro, non-obiettivi
│ ├── GLOSSARY.md lessico condiviso (usare gli stessi termini nel codice)
│ ├── PERSONAS_CASI_USO.md attori e casi d'uso end-to-end
│ └── REQUIREMENTS.md REQ-<AREA>-<nn> + DIF-<nn> (difetti noti)
├── 02-architecture/ COME È FATTO
│ ├── ARCHITECTURE.md · C4_MODEL.md · DOMAIN_WORKFLOW.md (macchina a stati, invarianti)
│ ├── DATA_MODEL.md (entità, relazioni, invarianti) · SECURITY.md · STORAGE.md
│ └── <componente>.md (AI_GATEWAY, MESH_ENGINE, …)
├── 03-specs/ CONTRATTO PER L'IMPLEMENTAZIONE
│ └── SPEC-<nnn>-<slug>.md
├── 04-api/ CONTRATTO ESTERNO
│ ├── openapi.yaml FONTE DI VERITÀ dell'API
│ └── API.md · WEBHOOKS.md · INTEGRATION_*.md
├── 05-data/ DATABASE.md (schema fisico) · MIGRATION_*.md · SEED_BOOTSTRAP.md
├── 06-devops/ INFRASTRUCTURE · KUBERNETES · CI_CD · ENVIRONMENTS · OBSERVABILITY · RUNBOOK · BACKUP_DR
├── 07-quality/ TEST_STRATEGY · ACCEPTANCE_CRITERIA · TRACEABILITY · liste di parità
└── 08-project/ DECISIONS · ROADMAP · TODO · CHANGELOG · DEVELOPMENT_LOG · PROJECT_MEMORY
```
**Dove cercare cosa**
| Domanda | Documento |
|---|---|
| Cosa deve fare il prodotto? | `01-product/REQUIREMENTS.md` |
| Come deve farlo questa funzione? | `03-specs/SPEC-xxx` |
| Che forma ha la rotta? | `04-api/openapi.yaml` (poi API.md per le convenzioni) |
| Che colonne/vincoli ha la tabella? | `05-data/DATABASE.md`; entità logiche in `02-architecture/DATA_MODEL.md` |
| Transizioni di stato ammesse? | `02-architecture/DOMAIN_WORKFLOW.md` |
| Perché è così? | `08-project/DECISIONS.md` (`## D-xxx`), poi `DEVELOPMENT_LOG.md` |
| È fatto? È verificato? | `08-project/TODO.md`, `07-quality/TRACEABILITY.md` |
| Come si rilascia / variabili d'ambiente? | `06-devops/ENVIRONMENTS.md`, `KUBERNETES.md` |
**Legenda stati usata:** ⬜ da scrivere/fare · 🟡 in corso/parziale · ✅ completo/verificato ·
🔒 congelato (modifica solo con nuova D-xxx) · ⏸ sospeso.
---
## B. Layout piatto (es. `Kadeon/kadeon/src/docs`)
```
docs/
├── PROJECT_MEMORY.md «leggere sempre prima di scrivere codice»; sintesi architettura,
│ decisioni, componenti già creati, sessioni
├── ARCHITECTURE.md · API.md · DATABASE.md · AI_GATEWAY.md · OUTPUT_SCHEMA.md
├── <MODULO>.md proposta/specifica di un modulo (es. CAD_MODULE.md)
├── <area>/DESIGN.md proposte con opzioni e «domande ancora aperte» (es. whatsapp/)
├── <area>/openapi.yaml contratto del modulo
├── DECISIONS.md D-xxx con Stato · Contesto · Opzioni · Scelta · Motivazione
├── TODO.md T-<lettera><nn> raggruppati per tema, con Prio (P0–P3) e Stato
├── CHANGELOG.md · DEVELOPMENT_LOG.md
```
Differenze da tenere presenti:
- Non ci sono `REQ-`/`SPEC-`: il ruolo della spec lo svolge il **documento di modulo** o la
sezione di PROJECT_MEMORY/ARCHITECTURE; il cancello §3 si applica a quello.
- Gli ID dei task hanno un prefisso tematico (`T-S01` sicurezza, `T-Q5`, `T-H01`…).
- Stati in inglese: `pending` · `in_progress` · `done` · `blocked`; decisioni `accepted` ·
`proposed` · `superseded`.
- Un `DESIGN.md` con «Domande ancora aperte» **non è approvato**: chiarire con l'utente.
- PROJECT_MEMORY può essere lungo e datato: confronta la sua data con quelle di TODO e
DECISIONS.
---
## C. Formati degli ID
| ID | Forma | Dove nasce | Note |
|---|---|---|---|
| Requisito | `REQ-<AREA>-<nn>` (AREA 2–5 lettere) | REQUIREMENTS | Tipo **P/C/N**; mai rinumerato |
| Difetto | `DIF-<nn>` | REQUIREMENTS (sezione difetti) | ogni DIF ha un REQ di tipo C e un test |
| Specifica | `SPEC-<nnn>` | file `03-specs/SPEC-nnn-slug.md` | riga `Stato:` in testa |
| Task | `T-<nnn>` o `T-<lettera><nn>`, suffissi `b`, `c` | sezione *Task* della spec e TODO | barrato se chiuso senza lavoro |
| Decisione | `D-<nnn>` | DECISIONS | *superata*, mai cancellata |
| Criterio | lettera + numero (`D1`, `P12`, `M18`) | sezione *Criteri* della spec | ⚠ `D1` criterio ≠ `D-001` decisione |
<sub>STS Lab Srl · Spec-Driven Reader</sub>
@@ -0,0 +1,181 @@
# Scrivere la documentazione spec-driven — STS Lab Srl
Guida per creare la documentazione quando il progetto non ce l'ha, completarla quando è solo
uno scheletro, o aggiungere una spec a un progetto che già la usa.
`S` = `python ~/.claude/skills/sts-spec-driven/scripts/spec_index.py`
(da PowerShell: `python $HOME\.claude\skills\sts-spec-driven\scripts\spec_index.py`)
---
## 0. Scegliere il layout — lo decide l'utente
Chiedi sempre con `AskUserQuestion`, prima di creare file. Proponi come primo, con
«(Consigliato)», il layout che si adatta al progetto:
| Layout | Quando consigliarlo | Cosa crea |
|---|---|---|
| **Strutturato** | più moduli o servizi, integrazioni con altri sistemi, riscrittura di un sistema esistente, più persone al lavoro, committente esterno, requisiti contrattuali | `00-INDEX` + `01-product … 08-project`, 29 documenti, spec in `03-specs/SPEC-nnn-*.md` |
| **Piatto** | progetto piccolo o prototipo, un solo modulo, un solo sviluppatore, tempi brevi | 8 documenti nella stessa cartella, una spec per modulo `<NOME>_MODULE.md` |
Chiedi nella stessa domanda (o subito dopo) **dove** creare la cartella: default `docs/` nella
radice del repository (decisione D-003 dei modelli). Se esiste già una cartella con documenti
sparsi, non sovrascriverla: proponi di crearci dentro lo scheletro e di spostare i contenuti
esistenti nei documenti giusti.
---
## 1. Creare lo scheletro
```bash
S scaffold <repo>/docs --layout strutturato --progetto "Nome Progetto"
S scaffold <repo>/docs --layout piatto --progetto "Nome Progetto"
```
Lo script **non sovrascrive mai** un file esistente (li elenca come «già esistenti»). I
documenti nascono con segnaposto `<!-- DA COMPILARE: … -->` che spiegano cosa scrivere, e con
le decisioni iniziali già pronte: `D-001` stack (Proposta), `D-002` metodo spec-driven,
`D-003` documentazione nel repository.
---
## 2. Raccogliere le informazioni
### 2.1 Prima guarda, poi chiedi
Prima di fare domande leggi ciò che esiste: `README`, `CLAUDE.md`, manifest delle dipendenze
(`package.json`, `pyproject.toml`, `composer.json`, …), `docker-compose`, cartelle di codice,
documenti del committente forniti dall'utente. Non chiedere ciò che puoi ricavare.
### 2.2 Intervista — al massimo 4 domande per giro, solo ciò che manca
| Tema | Domande |
|---|---|
| Problema | Chi usa il prodotto? Cosa fa oggi senza? Quanto costa il problema (tempo, errori)? |
| Perimetro | Cosa deve esserci nel primo rilascio? Cosa è esplicitamente fuori? |
| Vincoli | Stack imposto? Sistemi esistenti da integrare o da sostituire? Scadenze? |
| Utenti | Quali ruoli? Chi può fare cosa? Multi-azienda (tenant)? |
| Dati | Quali dati personali o sensibili? Per quanto si conservano? |
| Esercizio | Dove gira (cloud, on-premise, Kubernetes)? Quali ambienti? |
| Successo | Come si misura che il progetto ha funzionato? |
Le risposte vanno nei documenti, non solo nella conversazione.
### 2.3 Progetto con codice ma senza documentazione
Ricostruisci dal codice, come è stato fatto in KadeonCad_v2:
- ogni comportamento osservato diventa un `REQ` di tipo **P** (parità) con *Origine*
`file:funzione`; il codice è la fonte di verità, non i commenti;
- ogni difetto visto leggendo diventa un `DIF-nn` + un `REQ` di tipo **C**;
- ogni scelta tecnica già fatta diventa una `D-xxx` *Accettata* con la data di oggi e
«ricostruita dal codice» nel contesto;
- le spec delle parti già scritte nascono comunque *da approvare*: l'utente conferma che il
comportamento attuale è quello voluto.
---
## 3. Ordine di scrittura
Ogni step termina con un **riepilogo all'utente** e la sua conferma prima del successivo.
A fine step aggiorna lo stato nella tabella degli step di `00-INDEX` (⬜ → 🟡 → ✅).
### Layout strutturato
| Step | Documenti | Esce quando |
|:-:|---|---|
| 0 | `00-INDEX` (cos'è, obiettivi, struttura del repo), `D-001` stack, `PROJECT_MEMORY` §1 | l'utente conferma obiettivi e stack |
| 1 | `VISION` → `GLOSSARY` → `PERSONAS_CASI_USO` → `REQUIREMENTS` → `ARCHITECTURE`, `C4_MODEL`, `DOMAIN_WORKFLOW`, `DATA_MODEL`, `SECURITY` | requisiti rivisti dall'utente |
| 2 | una `SPEC` per capability (`S new-spec`) | ogni REQ è coperto da almeno una spec |
| 3 | `openapi.yaml` + `API.md`, `DATABASE.md`, `SEED_BOOTSTRAP.md` | ogni rotta delle spec è nel contratto |
| 4 | `06-devops/*` | ambienti e pipeline descritti |
| 5 | `TEST_STRATEGY`, `ACCEPTANCE_CRITERIA`, `TRACEABILITY`, `ROADMAP`, `TODO` | `S check` senza ID orfani né requisiti scoperti |
Per un progetto piccolo in layout strutturato gli step 4 e parte del 5 possono restare
⬜ con una riga «rimandato a …» nell'indice: meglio dichiarato vuoto che riempito di generico.
### Layout piatto
1. `PROJECT_MEMORY` §1–2 e `ARCHITECTURE`.
2. `DECISIONS` (`D-001` stack).
3. Primo documento di modulo: `S new-spec <docs> --titolo "…" --prefisso X`.
4. `API.md`, `DATABASE.md` per ciò che il modulo tocca.
5. `TODO` con la lettera del modulo e i suoi task.
---
## 4. Regole di qualità
### Requisiti
- Un comportamento per riga, osservabile e verificabile; «il sistema …».
- Numeri espliciti al posto di aggettivi («entro 5 s», «≥ 200 pezzi», non «veloce», «grande»).
- Niente soluzioni tecniche nel requisito (quelle vanno nella spec o in una `D-xxx`).
- Ogni requisito ha Tipo (P/C/N) e Origine. Aree di 2–5 lettere maiuscole (`AUT`, `ING`, `NFR`).
### Specifiche
- Forma **normativa**: «il sistema rifiuta…», «non ricalcola…». I «non» in grassetto.
- Algoritmi in pseudocodice; combinazioni di casi in tabella.
- Contratto API con la **tabella degli esiti** e i codici HTTP; poi subito in `openapi.yaml`.
- Criteri di accettazione binari, nella forma «Dato …, quando …, allora …»; ogni requisito
coperto ha almeno un criterio; i casi limite (valore 199/200, assente, altro tenant) sono
criteri a sé.
- Task piccoli (indicativamente ≤ 1 giorno), con dipendenze; l'ultimo è sempre «Test di
accettazione <lettera>1–<lettera>n».
- Numerazione: `SPEC-nnn` → task `T-nnn0…T-nnn9`, criteri con la lettera della spec
(SPEC-001 → A, SPEC-002 → B, …). Usa `S next-id` per non sbagliare.
- Documento di modulo (layout piatto): la sezione «Domande ancora aperte» deve essere vuota o
«nessuna» prima dell'approvazione.
### Decisioni
- Alternative **reali** valutate, con il motivo dello scarto.
- Conseguenze comprese quelle negative (costi, limiti accettati).
### Sempre
- **Non inventare.** Ciò che non sai resta `DA COMPILARE` o diventa una domanda all'utente;
mai numeri, nomi di sistemi o vincoli plausibili ma non confermati.
- **Mai approvare da solo.** Le spec nascono «da approvare»; diventano «Approvata» solo dopo
un sì esplicito dell'utente (§5).
- Italiano, frasi brevi, niente linguaggio promozionale. Gli stessi termini del `GLOSSARY`.
- Nessun segreto nei documenti: solo il nome della variabile che lo contiene.
---
## 5. Approvare una spec (solo su richiesta dell'utente)
1. Riga di stato: `Stato: **Approvata** · Versione 1.0 · AAAA-MM-GG`.
2. `00-INDEX` §03: riga della spec con numero di criteri, blocco di task, stato ✅.
3. `TODO`: i task della spec nella fase giusta, stato ⬜.
4. `TRACEABILITY`: una riga per requisito coperto (REQ → SPEC §n → T → criteri → ⬜).
5. `DEVELOPMENT_LOG`: voce breve con cosa è stato deciso approvando.
6. `S check <docs>`.
Una spec approvata si modifica solo con una nuova `D-xxx` o con una revisione tracciata
(versione 1.1, sezione `§n.bis`).
---
## 6. Aggiungere una spec a un progetto che già esiste
```bash
S next-id <docs> SPEC # numero libero
S new-spec <docs> --titolo "Portale cliente" # crea SPEC-nnn-portale-cliente.md, da approvare
S next-id <docs> REQ --area POR # se servono requisiti nuovi
S next-id <docs> D # se la spec nasce da una decisione
```
Poi: requisiti nuovi in `REQUIREMENTS` (tipo N, origine `D-xxx`), riga nella mappa di
`00-INDEX`, riquadro «novità» in testa all'indice, e la procedura del §5 quando l'utente approva.
---
## 7. Verifica finale
```bash
S check <docs>
```
Prima di consegnare: nessun ID orfano, nessun requisito scoperto, nessun task delle spec
assente dal TODO. I segnaposto «DA COMPILARE» rimasti devono corrispondere solo a parti
dichiarate rimandate. Riporta all'utente l'esito del controllo, non solo «fatto».
<sub>STS Lab Srl · Spec-Driven Reader</sub>
@@ -0,0 +1,669 @@
#!/usr/bin/env python3
"""
STS Lab Srl — Spec-Driven Reader · spec_index.py
Copyright (c) 2026 STS Lab Srl. Tutti i diritti riservati.
Strumento della skill «sts-spec-driven» per leggere e scrivere documentazione spec-driven.
Lettura (non scrive nulla):
overview <docs> Che forma ha la documentazione? Quali spec, in che stato?
find <docs> <ID> Dove è definito un ID e chi lo cita? (REQ-, SPEC-, T-, D-, DIF-)
context <docs> <ID> Definizione, ID collegati a un passo, righe di TODO e TRACEABILITY.
check <docs> Coerenza: ID orfani, spec senza criteri/task, task fuori dal TODO,
spec non approvate con lavoro già fatto, segnaposto da compilare.
next-id <docs> <TIPO> Prossimo ID libero: REQ (con --area), SPEC, D, DIF, T (con --spec
o --prefisso).
Scrittura (non sovrascrive mai un file esistente):
scaffold <docs> --layout strutturato|piatto --progetto NOME
Crea lo scheletro della documentazione dai modelli.
new-spec <docs> --titolo "…" Crea una nuova spec (strutturato: 03-specs/SPEC-nnn-slug.md;
piatto: <NOME>_MODULE.md, serve --prefisso).
Solo libreria standard Python 3.8+.
"""
from __future__ import annotations
import argparse
import datetime as dt
import re
import shutil
import sys
import unicodedata
from collections import defaultdict
from pathlib import Path
BRAND = "STS Lab Srl · Spec-Driven Reader"
SKILL_DIR = Path(__file__).resolve().parent.parent
TEMPLATES = SKILL_DIR / "templates"
LAYOUTS = ("strutturato", "piatto")
ID_RE = re.compile(
r"(?<![A-Za-z0-9_-])("
r"REQ-[A-Z]{2,5}-\d{1,3}[a-z]?"
r"|SPEC-\d{3}"
r"|DIF-\d{1,3}"
r"|D-\d{3}"
r"|T-[A-Z]{0,2}\d{1,4}[a-z]?"
r")(?![A-Za-z0-9_])"
)
KEY_DOCS = [
"00-INDEX.md", "INDEX.md", "README.md",
"PROJECT_MEMORY.md", "REQUIREMENTS.md", "DECISIONS.md", "TODO.md",
"TRACEABILITY.md", "ARCHITECTURE.md", "API.md", "openapi.yaml",
"DATABASE.md", "DATA_MODEL.md", "CHANGELOG.md", "DEVELOPMENT_LOG.md", "ROADMAP.md",
"GLOSSARY.md", "ACCEPTANCE_CRITERIA.md", "TEST_STRATEGY.md",
]
PLACEHOLDER = "DA COMPILARE"
def out(text: str = "") -> None:
print(text)
# ---------------------------------------------------------------- caricamento
def load(docs: Path) -> dict[Path, list[str]]:
files: dict[Path, list[str]] = {}
for p in sorted(docs.rglob("*")):
if p.is_file() and p.suffix.lower() in {".md", ".yaml", ".yml"}:
try:
files[p] = p.read_text(encoding="utf-8-sig", errors="replace").splitlines()
except OSError:
continue
return files
def strip_comments(lines: list[str]) -> list[str]:
"""Svuota i commenti HTML (<!-- … -->, anche su più righe) mantenendo i numeri di riga:
i segnaposto dei modelli non devono contare come ID."""
res, inside = [], False
for line in lines:
buf, i = "", 0
while i < len(line):
if inside:
j = line.find("-->", i)
if j < 0:
i = len(line)
else:
inside, i = False, j + 3
else:
j = line.find("<!--", i)
if j < 0:
buf += line[i:]
i = len(line)
else:
buf += line[i:j]
inside, i = True, j + 4
res.append(buf)
return res
def rel(p: Path, docs: Path) -> str:
return p.relative_to(docs).as_posix()
def kind(id_: str) -> str:
for prefix in ("REQ", "SPEC", "DIF"):
if id_.startswith(prefix + "-"):
return prefix
return id_.split("-")[0]
def is_definition(line: str, id_: str, path: Path) -> bool:
"""Una riga *definisce* l'ID se è un titolo che inizia con l'ID o una riga di
tabella il cui primo campo è l'ID (anche in **grassetto** o ~~barrato~~)."""
s = line.strip()
if s.startswith("#"):
# solo titoli Markdown che *iniziano* con l'ID ("## D-014 — …", "# SPEC-004 — …")
return path.suffix.lower() == ".md" and re.match(r"#+\s+\**" + re.escape(id_) + r"(?![0-9A-Za-z])", s) is not None
if s.startswith("|"):
first = s.strip("|").split("|")[0]
first = re.sub(r"[*~`\[\]]", "", first).strip()
# [D-001](#ancora) -> "D-001(#ancora)" : prendiamo il primo token
first = re.split(r"[\s(]", first)[0] if first else first
return first == id_
return False
def scan(files: dict[Path, list[str]], docs: Path):
defs: dict[str, list[tuple[str, int, str]]] = defaultdict(list)
refs: dict[str, list[tuple[str, int, str]]] = defaultdict(list)
for p, raw in files.items():
r = rel(p, docs)
lines = strip_comments(raw) if p.suffix.lower() == ".md" else raw
m = re.match(r"(SPEC-\d{3})", p.name)
if m:
defs[m.group(1)].append((r, 1, raw[0] if raw else p.name))
for n, line in enumerate(lines, 1):
for id_ in set(ID_RE.findall(line)):
if is_definition(line, id_, p):
defs[id_].append((r, n, line.strip()))
else:
refs[id_].append((r, n, line.strip()))
return defs, refs
def detect_layout(docs: Path, files: dict[Path, list[str]] | None = None) -> str:
if any(d.is_dir() and re.match(r"\d{2}-", d.name) for d in docs.iterdir()):
return "strutturato"
if (docs / "03-specs").exists() or any(re.match(r"SPEC-\d{3}", p.name) for p in (files or {})):
return "strutturato"
return "piatto"
def spec_status(lines: list[str]) -> str:
for line in lines[:12]:
m = re.match(r"\s*Stato\s*:\s*(.+)", line)
if m:
return re.sub(r"[*`]", "", m.group(1)).strip()
return "— (riga «Stato:» assente)"
def index_status(files: dict[Path, list[str]], spec_id: str) -> str:
"""Ripiego: lo stato della spec letto dalla sua riga nell'indice (ultima cella)."""
for p, lines in files.items():
if p.name in ("00-INDEX.md", "INDEX.md", "README.md"):
for line in strip_comments(lines):
if line.lstrip().startswith("|") and spec_id in line:
cells = [c.strip() for c in line.strip().strip("|").split("|")]
if cells:
return "dall'indice: " + re.sub(r"[*`]", "", cells[-1])
return ""
def status_flag(st: str) -> str:
if re.search(r"da approvare|bozza|proposta|proposed|draft", st, re.I):
return " <-- NON approvata: non implementare"
if re.search(r"approvat|✅|accettat|accepted", st, re.I):
return ""
return " <-- stato incerto: verificare prima di implementare"
def is_approved(st: str) -> bool:
return status_flag(st) == ""
def last_update(lines: list[str]) -> str:
for line in lines[:12]:
m = re.search(r"Ultimo aggiornamento\s*:\s*\**([0-9]{4}-[0-9]{2}-[0-9]{2})", line)
if m:
return m.group(1)
return ""
def spec_docs(files: dict[Path, list[str]], layout: str) -> list[Path]:
"""Spec del layout strutturato (SPEC-nnn-*.md) o documenti di modulo del layout piatto
(qualunque .md non chiave con una riga «Stato:» in testa)."""
res = []
for p, lines in files.items():
if p.suffix.lower() != ".md":
continue
if re.match(r"SPEC-\d{3}", p.name):
res.append(p)
elif layout == "piatto" and p.name not in KEY_DOCS and any(re.match(r"\s*Stato\s*:", l) for l in lines[:12]):
res.append(p)
return sorted(res)
RANGE_RE = re.compile(r"REQ-([A-Z]{2,5})-(\d{1,3})`?\s*(?:…|\.\.\.|–)\s*`?(?:REQ-[A-Z]{2,5}-)?(\d{1,3})")
SHORT_RE = re.compile(r"(?<![A-Za-z0-9-])([A-Z]{2,5})-(\d{2,3})(?![0-9])")
def covered_reqs(spec_texts: list[str], trace_lines: list[str]) -> set[str]:
"""Requisiti coperti: citati in una spec (anche come intervallo «REQ-X-01 … 05») o su una
riga di TRACEABILITY che nomina una spec (anche in forma breve «PRI-04»)."""
cov: set[str] = set()
for t in spec_texts:
cov |= {i for i in ID_RE.findall(t) if kind(i) == "REQ"}
for area, a, b in RANGE_RE.findall(t):
cov |= {"REQ-%s-%02d" % (area, k) for k in range(int(a), int(b) + 1)}
for line in trace_lines:
if "SPEC-" in line:
cov |= {i for i in ID_RE.findall(line) if kind(i) == "REQ"}
cov |= {"REQ-%s-%s" % (a, n) for a, n in SHORT_RE.findall(line)
if a not in ("SPEC", "REQ", "DIF")}
return cov
# ---------------------------------------------------------------- lettura
def cmd_overview(docs: Path) -> None:
files = load(docs)
defs, refs = scan(files, docs)
out(f"== {BRAND} ==")
out(f"Cartella : {docs}")
out(f"Layout : {detect_layout(docs, files)}")
out(f"File : {len(files)} (md/yaml)")
out()
out("-- Documenti chiave (ordine di lettura consigliato) --")
by_name = defaultdict(list)
for p in files:
by_name[p.name].append(p)
for name in KEY_DOCS:
for p in by_name.get(name, []):
lines = files[p]
upd = last_update(lines)
out(f" {rel(p, docs):45s} {len(lines):6d} righe {('agg. ' + upd) if upd else ''}")
big = [(p, len(l)) for p, l in files.items() if len(l) > 800]
if big:
out()
out("-- File grandi: NON leggerli interi, cerca la sezione per ID/data --")
for p, n in sorted(big, key=lambda x: -x[1]):
out(f" {rel(p, docs):45s} {n:6d} righe")
specs = spec_docs(files, detect_layout(docs, files))
if specs:
out()
out("-- Specifiche / documenti di modulo --")
for p in specs:
st = spec_status(files[p])
if st.startswith("—"):
st = index_status(files, p.stem[:8]) or st
out(f" {rel(p, docs):45s} {st}{status_flag(st)}")
todo = [(p, sum(PLACEHOLDER in l for l in ls)) for p, ls in files.items()]
todo = [(p, n) for p, n in todo if n]
if todo:
out()
out(f"-- Segnaposto «{PLACEHOLDER}» ancora presenti ({sum(n for _, n in todo)}) --")
for p, n in sorted(todo, key=lambda x: rel(x[0], docs)):
out(f" {rel(p, docs):45s} {n:4d}")
out()
out("-- ID trovati (definiti / citati) --")
counts = defaultdict(lambda: [0, 0])
for id_ in set(defs) | set(refs):
k = kind(id_)
counts[k][0] += 1 if id_ in defs else 0
counts[k][1] += 1
for k in ("REQ", "SPEC", "T", "D", "DIF"):
if k in counts:
out(f" {k:5s} {counts[k][0]:5d} definiti su {counts[k][1]:5d} distinti")
orphans = sorted(i for i in refs if i not in defs and kind(i) in {"REQ", "SPEC", "D"})
if orphans:
out()
out(f"-- Citati ma mai definiti ({len(orphans)}) — possibile deriva della documentazione --")
out(" " + ", ".join(orphans[:60]) + (" …" if len(orphans) > 60 else ""))
def cmd_find(docs: Path, id_: str, limit: int) -> None:
files = load(docs)
defs, refs = scan(files, docs)
out(f"== {BRAND} · {id_} ==")
if not defs.get(id_):
out("Definizione: NON TROVATA (ID inesistente, rinumerato o definito con formato insolito)")
else:
out("Definizione:")
for r, n, line in defs[id_]:
out(f" {r}:{n} {line[:220]}")
rs = refs.get(id_, [])
out(f"Citazioni: {len(rs)}")
by_file = defaultdict(list)
for r, n, line in rs:
by_file[r].append((n, line))
for r, items in sorted(by_file.items(), key=lambda x: -len(x[1])):
out(f" {r} ({len(items)})")
for n, line in items[:limit]:
out(f" :{n} {line[:180]}")
if len(items) > limit:
out(f" … altre {len(items) - limit}")
def section(lines: list[str], start: int, max_lines: int = 60) -> list[str]:
"""Dal titolo alla riga 'start' (1-based) fino al prossimo titolo di pari o superiore livello."""
head = lines[start - 1]
if not head.lstrip().startswith("#"):
return [head]
level = len(head) - len(head.lstrip("#"))
body = [head]
for line in lines[start:]:
s = line.lstrip()
if s.startswith("#") and (len(s) - len(s.lstrip("#"))) <= level:
break
body.append(line)
if len(body) >= max_lines:
body.append("… (sezione troncata: aprire il file per il resto)")
break
return body
def cmd_context(docs: Path, id_: str) -> None:
files = load(docs)
defs, refs = scan(files, docs)
by_rel = {rel(p, docs): l for p, l in files.items()}
out(f"== {BRAND} · contesto di {id_} ==")
if id_.startswith("SPEC-"):
spec_file = next((p for p in files if p.name.startswith(id_)), None)
if spec_file is None:
out("Spec non trovata.")
return
lines = files[spec_file]
out(f"File : {rel(spec_file, docs)} ({len(lines)} righe)")
out(f"Stato : {spec_status(lines)}")
out()
out("Indice delle sezioni:")
for n, line in enumerate(lines, 1):
if line.startswith("## ") or line.startswith("### "):
out(f" :{n} {line}")
linked = sorted({i for line in strip_comments(lines) for i in ID_RE.findall(line)} - {id_})
else:
linked = set()
for r, n, line in defs.get(id_, []):
out(f"Definizione: {r}:{n}")
body = section(by_rel[r], n) if line.startswith("#") else [line]
for b in body:
out(" " + b)
for b in strip_comments(body):
linked |= set(ID_RE.findall(b))
if not defs.get(id_):
out("Definizione: NON TROVATA")
for r, n, line in refs.get(id_, []):
if "SPEC-" in Path(r).name:
linked.add(Path(r).name[:8])
linked = sorted(linked - {id_})
out()
out("ID collegati (un passo):")
groups = defaultdict(list)
for i in linked:
groups[kind(i)].append(i)
for k in ("REQ", "SPEC", "T", "D", "DIF"):
if groups.get(k):
out(f" {k:5s} " + ", ".join(groups[k]))
out()
out("Definizioni sintetiche dei collegati (righe di tabella / titoli):")
for i in linked:
for r, n, line in defs.get(i, [])[:1]:
out(f" [{i}] {r}:{n} {line[:200]}")
for target in ("TODO.md", "TRACEABILITY.md"):
hits = [(r, n, l) for r, n, l in refs.get(id_, []) + defs.get(id_, []) if Path(r).name == target]
if hits:
out()
out(f"Righe in {target}:")
for r, n, line in hits[:25]:
out(f" {r}:{n} {line[:220]}")
def cmd_check(docs: Path) -> int:
files = load(docs)
defs, refs = scan(files, docs)
issues: list[tuple[str, str]] = []
for i in sorted(refs):
if i not in defs and kind(i) in {"REQ", "SPEC", "D", "DIF"}:
issues.append(("ID orfano", f"{i} è citato ma non definito (es. {refs[i][0][0]}:{refs[i][0][1]})"))
specs = spec_docs(files, detect_layout(docs, files))
spec_text = {p: "\n".join(strip_comments(files[p])) for p in specs}
for p, text in spec_text.items():
r = rel(p, docs)
if not re.search(r"^#{2,3}\s.*criteri", text, re.I | re.M):
issues.append(("Spec incompleta", f"{r}: manca la sezione «Criteri di accettazione»"))
if not re.search(r"^#{2,3}\s.*task", text, re.I | re.M):
issues.append(("Spec incompleta", f"{r}: manca la sezione «Task»"))
if re.search(r"^#{2,3}\s.*domande ancora aperte", text, re.I | re.M):
tail = text.split("omande ancora aperte", 1)[1]
body = [l for l in tail.splitlines()[1:] if l.strip() and not l.startswith("#")]
if body and not re.search(r"nessuna", " ".join(body), re.I):
issues.append(("Spec non approvabile", f"{r}: «Domande ancora aperte» non vuota"))
trace = [l for p, ls in files.items() if p.name == "TRACEABILITY.md" for l in strip_comments(ls)]
req_in_specs = covered_reqs(list(spec_text.values()), trace)
req_defined = {i for i in defs if kind(i) == "REQ"}
for i in sorted(req_defined - req_in_specs):
if specs:
issues.append(("Requisito scoperto", f"{i} non è coperto da nessuna spec"))
todo_files = [p for p in files if p.name == "TODO.md"]
todo_text = "\n".join("\n".join(strip_comments(files[p])) for p in todo_files)
if todo_files:
for p, text in spec_text.items():
for t in sorted({i for i in ID_RE.findall(text) if kind(i) == "T"}):
if any(d[0] == rel(p, docs) for d in defs.get(t, [])) and t not in todo_text:
issues.append(("Task fuori dal TODO", f"{t} definito in {rel(p, docs)} ma assente dal TODO"))
for p in specs:
st = spec_status(files[p])
if st.startswith("—"):
st = index_status(files, p.stem[:8]) or st
if is_approved(st):
continue
tasks = {i for i in ID_RE.findall(spec_text[p]) if kind(i) == "T"}
done = []
for line in todo_text.splitlines():
ids = set(ID_RE.findall(line))
if ids & tasks and re.search(r"✅|\bdone\b", line):
done.extend(sorted(ids & tasks))
if done:
issues.append(("Deriva di stato", f"{rel(p, docs)} è «{st}» ma ha task fatti: {', '.join(sorted(set(done))[:8])}"))
ph = [(rel(p, docs), sum(PLACEHOLDER in l for l in ls)) for p, ls in files.items()]
for r, n in sorted(ph):
if n:
issues.append(("Da compilare", f"{r}: {n} segnaposto «{PLACEHOLDER}»"))
out(f"== {BRAND} · controllo di coerenza ==")
out(f"Cartella: {docs}")
if not issues:
out("Nessun problema rilevato.")
return 0
by_kind = defaultdict(list)
for k, msg in issues:
by_kind[k].append(msg)
for k in ("ID orfano", "Requisito scoperto", "Spec incompleta", "Spec non approvabile",
"Task fuori dal TODO", "Deriva di stato", "Da compilare"):
if by_kind.get(k):
out()
out(f"-- {k} ({len(by_kind[k])}) --")
for msg in by_kind[k][:40]:
out(" " + msg)
if len(by_kind[k]) > 40:
out(f" … altri {len(by_kind[k]) - 40}")
return 1
# ---------------------------------------------------------------- numerazione
def _max_num(ids, pattern: str) -> int:
nums = [int(m.group(1)) for i in ids for m in [re.fullmatch(pattern, i)] if m]
return max(nums, default=0)
def next_id(docs: Path, tipo: str, area: str | None = None, spec: str | None = None,
prefisso: str | None = None) -> str:
files = load(docs) if docs.exists() else {}
defs, refs = scan(files, docs) if files else ({}, {})
known = set(defs) | set(refs)
for p in files:
m = re.match(r"(SPEC-\d{3})", p.name)
if m:
known.add(m.group(1))
tipo = tipo.upper()
if tipo == "SPEC":
return "SPEC-%03d" % (_max_num(known, r"SPEC-(\d{3})") + 1)
if tipo == "D":
return "D-%03d" % (_max_num(known, r"D-(\d{3})") + 1)
if tipo == "DIF":
return "DIF-%02d" % (_max_num(known, r"DIF-(\d{1,3})") + 1)
if tipo == "REQ":
if not area:
raise SystemExit("next-id REQ richiede --area (es. --area AUT)")
a = area.upper()
return "REQ-%s-%02d" % (a, _max_num(known, r"REQ-" + a + r"-(\d{1,3})") + 1)
if tipo == "T":
if prefisso:
pf = prefisso.upper()
return "T-%s%02d" % (pf, _max_num(known, r"T-" + pf + r"(\d{1,4})") + 1)
if spec:
n = int(re.sub(r"\D", "", spec))
block = [i for i in known if re.fullmatch(r"T-\d{3}", i) and int(i[2:]) // 10 == n]
if not block:
return f"T-{n * 10:03d}"
last = max(int(i[2:]) for i in block)
if last % 10 < 9:
return f"T-{last + 1:03d}"
sfx = [i for i in known if re.fullmatch("T-%03d[b-z]" % last, i)]
return f"T-{last:03d}{chr(ord(max(sfx)[-1]) + 1) if sfx else 'b'}"
return "T-%03d" % (_max_num(known, r"T-(\d{3})") + 1)
raise SystemExit(f"Tipo sconosciuto: {tipo} (REQ, SPEC, T, D, DIF)")
# ---------------------------------------------------------------- scrittura
def fill(text: str, values: dict[str, str]) -> str:
for k, v in values.items():
text = text.replace("{{" + k + "}}", v)
return text
def slugify(s: str) -> str:
s = unicodedata.normalize("NFKD", s).encode("ascii", "ignore").decode()
return re.sub(r"[^a-z0-9]+", "-", s.lower()).strip("-")
def cmd_scaffold(docs: Path, layout: str, progetto: str, data: str) -> int:
src = TEMPLATES / layout
if not src.is_dir():
raise SystemExit(f"Modelli non trovati: {src}")
created, skipped = [], []
for t in sorted(src.rglob("*")):
if t.is_dir():
continue
dest = docs / t.relative_to(src)
if dest.exists():
skipped.append(dest)
continue
dest.parent.mkdir(parents=True, exist_ok=True)
text = t.read_text(encoding="utf-8")
dest.write_text(fill(text, {"PROGETTO": progetto, "DATA": data}), encoding="utf-8")
created.append(dest)
if layout == "strutturato":
(docs / "03-specs").mkdir(parents=True, exist_ok=True)
out(f"== {BRAND} · scaffold «{layout}» ==")
out(f"Cartella: {docs}")
out(f"Creati : {len(created)}")
for p in created:
out(f" + {rel(p, docs)}")
if skipped:
out(f"Già esistenti, NON toccati: {len(skipped)}")
for p in skipped:
out(f" = {rel(p, docs)}")
out()
out("Prossimo passo: compilare i segnaposto «DA COMPILARE» seguendo references/scrittura.md,")
out("poi `check` per verificare la coerenza.")
return 0
def criterion_letter(n: int) -> str:
return chr(64 + n) if 1 <= n <= 26 else f"S{n}"
def cmd_new_spec(docs: Path, titolo: str, data: str, slug: str | None, prefisso: str | None,
layout: str | None) -> int:
files = load(docs) if docs.exists() else {}
layout = layout or detect_layout(docs, files)
if layout == "strutturato":
sid = next_id(docs, "SPEC")
n = int(sid[5:])
slug = slug or slugify(titolo)
dest = docs / "03-specs" / f"{sid}-{slug}.md"
values = {"SPEC_ID": sid, "TITOLO": titolo, "DATA": data,
"LETTERA": criterion_letter(n),
"T0": f"T-{n * 10:03d}", "T1": f"T-{n * 10 + 1:03d}"}
tpl = TEMPLATES / "spec.md"
else:
if not prefisso:
raise SystemExit("Nel layout piatto serve --prefisso (lettera dei task del modulo, es. W)")
pf = prefisso.upper()
used = {i for p, ls in files.items() for l in strip_comments(ls) for i in ID_RE.findall(l)
if re.fullmatch(r"T-" + pf + r"\d+[a-z]?", i)}
if used:
raise SystemExit(f"Il prefisso T-{pf} è già usato ({', '.join(sorted(used)[:5])}): sceglierne un altro.")
name = (slug or slugify(titolo)).upper().replace("-", "_")
if not name.endswith("_MODULE"):
name += "_MODULE"
dest = docs / f"{name}.md"
values = {"TITOLO": titolo, "DATA": data, "PREFISSO": pf}
tpl = TEMPLATES / "modulo.md"
if dest.exists():
raise SystemExit(f"Esiste già: {dest} — non lo sovrascrivo.")
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_text(fill(tpl.read_text(encoding="utf-8"), values), encoding="utf-8")
out(f"== {BRAND} · nuova spec ==")
out(f"Creata: {dest}")
out("Stato : da approvare")
if layout == "strutturato":
out(f"Task : blocco {values['T0']}…T-{n * 10 + 9:03d} · criteri {values['LETTERA']}1…")
out("Ricorda: riga nella mappa di 00-INDEX §03, task in TODO, righe in TRACEABILITY.")
else:
out("Ricorda: riga in PROJECT_MEMORY §4, lettera del modulo e task in TODO.")
return 0
# ---------------------------------------------------------------- main
def main() -> int:
try:
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
except Exception:
pass
today = dt.date.today().isoformat()
ap = argparse.ArgumentParser(description=f"{BRAND} — documentazione spec-driven")
sub = ap.add_subparsers(dest="cmd", required=True)
o = sub.add_parser("overview")
o.add_argument("docs")
f = sub.add_parser("find")
f.add_argument("docs")
f.add_argument("id")
f.add_argument("--limit", type=int, default=8)
c = sub.add_parser("context")
c.add_argument("docs")
c.add_argument("id")
k = sub.add_parser("check")
k.add_argument("docs")
n = sub.add_parser("next-id")
n.add_argument("docs")
n.add_argument("tipo", help="REQ | SPEC | T | D | DIF")
n.add_argument("--area")
n.add_argument("--spec")
n.add_argument("--prefisso")
s = sub.add_parser("scaffold")
s.add_argument("docs")
s.add_argument("--layout", choices=LAYOUTS, required=True)
s.add_argument("--progetto", required=True)
s.add_argument("--data", default=today)
ns = sub.add_parser("new-spec")
ns.add_argument("docs")
ns.add_argument("--titolo", required=True)
ns.add_argument("--slug")
ns.add_argument("--prefisso", help="solo layout piatto: lettera dei task del modulo")
ns.add_argument("--layout", choices=LAYOUTS)
ns.add_argument("--data", default=today)
a = ap.parse_args()
docs = Path(a.docs).resolve()
if a.cmd == "scaffold":
return cmd_scaffold(docs, a.layout, a.progetto, a.data)
if a.cmd == "new-spec":
return cmd_new_spec(docs, a.titolo, a.data, a.slug, a.prefisso, a.layout)
if not docs.is_dir():
print(f"Cartella non trovata: {docs}", file=sys.stderr)
return 2
if a.cmd == "overview":
cmd_overview(docs)
elif a.cmd == "find":
cmd_find(docs, a.id, a.limit)
elif a.cmd == "context":
cmd_context(docs, a.id)
elif a.cmd == "check":
return cmd_check(docs)
elif a.cmd == "next-id":
out(next_id(docs, a.tipo, a.area, a.spec, a.prefisso))
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,60 @@
# {{TITOLO}}
Stato: **da approvare** · Versione 0.1 · {{DATA}} · Prefisso task: **T-{{PREFISSO}}**
**Decisioni di riferimento:** <!-- `D-…` o «nessuna» -->
---
## 1. Contesto e requisito
<!-- DA COMPILARE: perché serve, chi lo ha chiesto e quando, cosa deve ottenere l'utente. -->
## 2. Requisiti
| ID | Requisito | Origine |
|---|---|---|
| <!-- REQ-AREA-01 --> | <!-- «Il sistema …», limiti numerici espliciti --> | <!-- committente, data --> |
## 3. Opzioni valutate e raccomandazione
<!-- Solo se c'è una scelta da fare. Ogni opzione con pro, contro, costo. La scelta finale
diventa una D-xxx in DECISIONS.md. -->
## 4. Regole
<!-- DA COMPILARE in forma normativa. -->
## 5. Architettura del modulo
<!-- DA COMPILARE: servizi, code, modelli dati, storage toccati. -->
## 6. Contratto API
| Metodo | Percorso | Ruolo minimo | Esiti |
|---|---|---|---|
| <!-- DA COMPILARE --> | | | 200 · 404 · 422 |
## 7. Errori e casi limite
| Caso | Comportamento |
|---|---|
| <!-- DA COMPILARE --> | |
## 8. Criteri di accettazione
| # | Criterio |
|:-:|---|
| {{PREFISSO}}1 | <!-- «Dato …, quando …, allora …» --> |
## 9. Task
| ID | Attività | Prio | Dipende da |
|----|----------|------|------------|
| T-{{PREFISSO}}01 | <!-- DA COMPILARE --> | P1 | — |
## 10. Domande ancora aperte
<!-- Finché questa sezione non è vuota il documento non può essere approvato. -->
- <!-- DA COMPILARE o «nessuna» -->
@@ -0,0 +1,30 @@
# API — {{PROGETTO}}
Ultimo aggiornamento: **{{DATA}}**
Il contratto si scrive **prima** del codice. Se esiste un `openapi.yaml`, quello è la fonte di verità.
## Convenzioni
| Aspetto | Regola |
|---|---|
| Prefisso | `/api/v1` |
| Formato | JSON, UTF-8, date ISO 8601 UTC |
| Autenticazione | <!-- DA COMPILARE --> |
| Errori | `{ "error": { "code", "message", "details" } }` |
## Endpoint — Stato di servizio
### `GET /health` *(pubblico)*
Risponde `200 {"status": "ok"}`.
## Endpoint previsti
<!-- Un gruppo per modulo. Stato nel titolo: *(previsto)* · ✅ *implementato (T-xxx)*. -->
### <!-- Gruppo --> — *(previsto)*
| Metodo | Percorso | Ruolo minimo | Descrizione | Task |
|---|---|---|---|---|
| <!-- DA COMPILARE --> | | | | |
@@ -0,0 +1,45 @@
# ARCHITECTURE — {{PROGETTO}}
Ultimo aggiornamento: **{{DATA}}**
## Struttura del repository
```
<!-- DA COMPILARE -->
```
## 1. Vista d'insieme
```
<!-- DA COMPILARE: schema ASCII dei servizi -->
```
## 2. Servizi/componenti
| Componente | Tecnologia | Responsabilità |
|---|---|---|
| <!-- DA COMPILARE --> | | |
## 3. Flusso principale
<!-- DA COMPILARE: passo per passo, con code e dati scambiati. -->
## 4. Macchina a stati
<!-- DA COMPILARE: stati, transizioni ammesse (ogni altra è rifiutata), invarianti — o «non applicabile». -->
## 5. Code e lavori asincroni
<!-- DA COMPILARE o «non applicabile». -->
## 6. Storage
<!-- DA COMPILARE: database, file/oggetti, convenzioni di nome. -->
## 7. Sicurezza e multi-tenancy
<!-- DA COMPILARE: autenticazione, ruoli, isolamento, segreti (mai nel repository). -->
## 8. Aperto / da decidere
- <!-- DA COMPILARE -->
@@ -0,0 +1,10 @@
# CHANGELOG — {{PROGETTO}}
Formato: ogni voce riporta **data · componente · modifica · motivo**. Le voci più recenti in alto.
---
## {{DATA}} — Avvio della documentazione
Creata la documentazione di progetto con metodo spec-driven: PROJECT_MEMORY, ARCHITECTURE, API,
DATABASE, DECISIONS (D-001…D-003), TODO.
@@ -0,0 +1,18 @@
# DATABASE — {{PROGETTO}}
Ultimo aggiornamento: **{{DATA}}**
Motore: <!-- DA COMPILARE --> · Migrazioni: <!-- DA COMPILARE: strumento e numerazione -->
Regole: date sempre UTC · una migrazione per modifica, mai riscrivere una migrazione applicata ·
nessun file binario nel database.
## A. <!-- Gruppo di tabelle -->
### `<!-- tabella -->` *(migrazione `0001`)*
| Colonna | Tipo | Null | Note |
|---|---|:-:|---|
| `id` | | no | PK |
Indici: <!-- DA COMPILARE --> · Vincoli: <!-- DA COMPILARE -->
@@ -0,0 +1,28 @@
# DECISIONS — {{PROGETTO}} (Architecture Decision Records)
Ultimo aggiornamento: **{{DATA}}**
Formato: ogni decisione ha contesto, opzioni valutate, scelta e motivazione.
Stato: `accepted` · `proposed` · `superseded by D-xxx` · `rejected`.
Le decisioni non si cancellano: si superano.
---
## D-001 — Stack tecnologico
- **Stato:** proposed
- **Contesto:** <!-- DA COMPILARE -->
- **Opzioni:** <!-- (a) …; (b) … -->
- **Scelta:** <!-- DA COMPILARE -->
- **Motivazione:** <!-- DA COMPILARE -->
## D-002 — Sviluppo spec-driven
- **Stato:** accepted
- **Contesto:** le regole di business scritte solo nel codice emergono dal debugging invece che dalla specifica.
- **Opzioni:** (a) documentazione a valle del codice; (b) documento di modulo approvato prima del codice.
- **Scelta:** (b). Nessun codice senza un documento di modulo in stato *Approvato*, con regole, contratto, criteri di accettazione e task.
- **Motivazione:** le regole diventano esplicite e verificabili prima di costare un difetto.
## D-003 — La documentazione vive nel repository
- **Stato:** accepted
- **Scelta:** `docs/` nel repository del codice, aggiornata nella stessa modifica del codice che la riguarda.
- **Motivazione:** documenti e codice non divergono; la revisione del codice è anche revisione dei documenti.
@@ -0,0 +1,12 @@
# DEVELOPMENT_LOG — {{PROGETTO}}
Diario operativo per sessione: cosa fatto, problemi, perché si è deciso così, prossime azioni.
Le voci più recenti in alto. Le voci non si cancellano.
---
## {{DATA}} — Avvio della documentazione
Creato lo scheletro della documentazione spec-driven.
**Prossime azioni:** completare D-001 (stack), scrivere il primo documento di modulo.
@@ -0,0 +1,68 @@
# PROJECT_MEMORY — {{PROGETTO}}
> Memoria permanente del progetto. Leggere **sempre** questo file (e l'intera cartella `docs/`)
> prima di scrivere codice. Aggiornarlo dopo ogni modifica significativa.
Ultimo aggiornamento: **{{DATA}}** · Sessione: **avvio della documentazione**
---
## 1. Cos'è {{PROGETTO}}
<!-- DA COMPILARE: cosa fa, per chi, flusso ad alto livello in 4-6 passi, valore. -->
---
## 2. Architettura attuale (sintesi)
> Dettaglio completo in [`ARCHITECTURE.md`](./ARCHITECTURE.md).
- <!-- DA COMPILARE: un punto per servizio/componente, con la tecnologia -->
---
## 3. Decisioni tecniche prese
> Registro completo con alternative e motivazioni in [`DECISIONS.md`](./DECISIONS.md).
- D-001 Stack tecnologico *(proposed)*
- D-002 Sviluppo spec-driven: nessun codice senza un documento di modulo approvato
- D-003 La documentazione vive nel repository e si aggiorna con il codice
---
## 4. Moduli e documenti di specifica
| Modulo | Documento | Stato |
|---|---|---|
| <!-- DA COMPILARE --> | <!-- [`NOME_MODULE.md`](./NOME_MODULE.md) --> | da approvare |
---
## 5. Componenti GIÀ creati
- [ ] <!-- DA COMPILARE -->
## 6. Componenti ANCORA da creare
- [ ] <!-- DA COMPILARE -->
---
## 7. Vincoli importanti
<!-- Ciò che, se dimenticato, fa ripetere un errore. Ogni vincolo con il suo perché. -->
- <!-- DA COMPILARE -->
---
## 8. Come continuare (per la prossima sessione)
1. Leggere questo file, poi [`TODO.md`](./TODO.md) (P0 aperti per primi).
2. Per il task scelto, leggere il documento di modulo e le decisioni citate.
3. A fine lavoro: TODO, CHANGELOG, DEVELOPMENT_LOG e questo file.
---
<sub>Documentazione redatta con il metodo spec-driven di STS Lab Srl.</sub>
@@ -0,0 +1,25 @@
# TODO — {{PROGETTO}}
Ultimo aggiornamento: **{{DATA}}**
Legenda stato: `pending` · `in_progress` · `done` · `blocked`
Priorità: **P0** (fondante/bloccante) · **P1** (core) · **P2** (importante) · **P3** (successivo)
**Regola:** un task entra in lavorazione solo se il suo documento di modulo è approvato (D-002).
`done` solo con i test del criterio passanti.
**Numerazione:** `T-<lettera del modulo><nn>` (es. `T-S<nn>` per la sicurezza, `T-W<nn>` per WhatsApp);
la lettera si fissa quando nasce il modulo e si annota qui sotto.
| Lettera | Modulo |
|:-:|---|
| <!-- S --> | <!-- Sicurezza & API --> |
---
## Documentazione ({{DATA}})
| ID | Attività | Prio | Stato |
|----|----------|------|-------|
| T-D01 | Completare PROJECT_MEMORY, ARCHITECTURE, DECISIONS (D-001) | P0 | in_progress |
| T-D02 | Primo documento di modulo approvato | P0 | pending |
@@ -0,0 +1,83 @@
# {{SPEC_ID}} — {{TITOLO}}
Stato: **da approvare** · Versione 0.1 · {{DATA}}
**Requisiti coperti:** <!-- DA COMPILARE: `REQ-…` -->
**Decisioni di riferimento:** <!-- `D-…` o «nessuna» -->
---
## 1. Contesto e obiettivo
<!-- DA COMPILARE: perché questa capability esiste, cosa decide per l'utente. Un dato misurato
vale più di un aggettivo. Se la spec è a sé per una ragione (regola delicata, rischio), dirlo. -->
---
## 2. Cosa cambia rispetto a ciò che è già scritto
<!-- Solo se la spec modifica requisiti, decisioni o altre spec. Altrimenti rimuovere la sezione. -->
| Dove | Prima | Dopo |
|---|---|---|
| | | |
---
## 3. Regole
<!-- DA COMPILARE in forma NORMATIVA: «il sistema …», «non …», limiti numerici espliciti.
Pseudocodice per gli algoritmi. Una tabella per le combinazioni di casi. Quando un comportamento
sembra un errore ma è voluto, dirlo: «è una decisione, non una dimenticanza». -->
---
## 4. Contratto API
### `METODO /api/v1/percorso`
**Ingresso:** <!-- campi e tipi -->
**Uscita:** <!-- campi e tipi -->
| Esito | Codice |
|---|:-:|
| <!-- Riuscito --> | 200 |
| <!-- Non trovato o di altro tenant --> | 404 |
| <!-- Validazione fallita --> | 422 |
Il dettaglio formale va in `04-api/openapi.yaml` **prima** del codice.
---
## 5. Dati
<!-- DA COMPILARE: entità, colonne, vincoli toccati — rimando a DATA_MODEL e DATABASE. -->
---
## 6. Errori e casi limite
| Caso | Comportamento |
|---|---|
| <!-- DA COMPILARE --> | |
---
## 7. Criteri di accettazione
Ogni criterio è verificabile da un test e ha un esito binario.
| # | Criterio |
|:-:|---|
| {{LETTERA}}1 | <!-- DA COMPILARE: «Dato …, quando …, allora …» --> |
| {{LETTERA}}2 | |
---
## 8. Task
| ID | Task | Dipende da |
|---|---|---|
| **{{T0}}** | <!-- DA COMPILARE --> | — |
| **{{T1}}** | Test di accettazione {{LETTERA}}1–{{LETTERA}}n | {{T0}} |
@@ -0,0 +1,153 @@
# {{PROGETTO}} — Indice della documentazione
Ultimo aggiornamento: **{{DATA}}**
| Step | Contenuto | Stato |
|:-:|---|:-:|
| 0 | Scheletro della documentazione · indice · decisioni iniziali | 🟡 |
| 1 | `01-product/` + `02-architecture/` — visione, requisiti, architettura | ⬜ |
| 2 | `03-specs/` — specifiche e criteri di accettazione | ⬜ |
| 3 | `04-api/` + `05-data/` — contratto OpenAPI e schema dati | ⬜ |
| 4 | `06-devops/` — ambienti, pipeline, infrastruttura | ⬜ |
| 5 | `07-quality/` + `08-project/` — tracciabilità e backlog | ⬜ |
| 6 | Implementazione — vedi [`08-project/ROADMAP.md`](08-project/ROADMAP.md) | ⬜ |
<!-- Novità: un riquadro per ogni consegna rilevante, il più recente in alto. Esempio:
> **Titolo breve (AAAA-MM-GG):** cosa è cambiato e perché — `REQ-…`, `D-…`, `T-…`.
> Verificato come (test, sistema reale) — backend N (+k).
-->
---
## Cos'è questo progetto
<!-- DA COMPILARE: due o tre frasi. Che cosa fa il prodotto, per chi, su quale stack. -->
Obiettivi vincolanti del progetto:
1. <!-- DA COMPILARE: obiettivo misurabile -->
2. <!-- DA COMPILARE -->
3. **Spec-driven** — nessuna riga di codice senza una spec approvata e tracciata (D-002).
---
## Metodo: spec-driven development
Il flusso di lavoro è vincolato e va in un'unica direzione:
```
REQ-xxx (01-product/REQUIREMENTS.md)
│ cosa deve fare il prodotto
▼
SPEC-xxx (03-specs/)
│ come lo fa: contratto API, dati, regole, errori, criteri di accettazione
▼
T-xxx (08-project/TODO.md)
│ unità di lavoro implementabile
▼
codice + test
│
▼
07-quality/TRACEABILITY.md ← chiude il cerchio: ogni REQ ha spec, task e test
```
**Regola:** si implementa solo ciò che ha una `SPEC-xxx` in stato *Approvata*. Una spec non
approvata non genera task; un task senza spec non entra in lavorazione. Ogni scelta strutturale
diventa una decisione numerata `D-xxx` in [`08-project/DECISIONS.md`](08-project/DECISIONS.md).
---
## Mappa dei documenti
Legenda stato: ⬜ da scrivere · 🟡 in stesura · ✅ completo · 🔒 congelato (modifica solo via nuova D-xxx)
### 01 · Prodotto — *cosa* e *per chi*
| Documento | Contenuto | Stato |
|---|---|---|
| [`01-product/VISION.md`](01-product/VISION.md) | Perché esiste il prodotto, valore, perimetro, non-obiettivi | ⬜ |
| [`01-product/GLOSSARY.md`](01-product/GLOSSARY.md) | Lessico condiviso | ⬜ |
| [`01-product/PERSONAS_CASI_USO.md`](01-product/PERSONAS_CASI_USO.md) | Attori, persone, casi d'uso end-to-end | ⬜ |
| [`01-product/REQUIREMENTS.md`](01-product/REQUIREMENTS.md) | Requisiti `REQ-*` e difetti noti `DIF-*` | ⬜ |
### 02 · Architettura — *come è fatto*
| Documento | Contenuto | Stato |
|---|---|---|
| [`02-architecture/ARCHITECTURE.md`](02-architecture/ARCHITECTURE.md) | Vista d'insieme, servizi, flusso principale | ⬜ |
| [`02-architecture/C4_MODEL.md`](02-architecture/C4_MODEL.md) | Contesto / Contenitori / Componenti | ⬜ |
| [`02-architecture/DOMAIN_WORKFLOW.md`](02-architecture/DOMAIN_WORKFLOW.md) | Stati, transizioni ammesse, invarianti | ⬜ |
| [`02-architecture/DATA_MODEL.md`](02-architecture/DATA_MODEL.md) | Entità, relazioni, chiavi, cancellazioni, invarianti | ⬜ |
| [`02-architecture/SECURITY.md`](02-architecture/SECURITY.md) | Autenticazione, autorizzazione, segreti, audit | ⬜ |
### 03 · Specifiche — *contratto per l'implementazione*
| Spec | Capability | Stato |
|---|---|---|
<!-- Una riga per spec, aggiunta quando la spec nasce:
| [`SPEC-001-slug.md`](03-specs/SPEC-001-slug.md) | Capability · N criteri · T-010…019 | 🟡 da approvare |
-->
### 04 · API — *contratto esterno*
| Documento | Contenuto | Stato |
|---|---|---|
| [`04-api/API.md`](04-api/API.md) | Catalogo endpoint, convenzioni, errori, paginazione | ⬜ |
| [`04-api/openapi.yaml`](04-api/openapi.yaml) | Contratto OpenAPI 3.1 — **fonte di verità** | ⬜ |
### 05 · Dati
| Documento | Contenuto | Stato |
|---|---|---|
| [`05-data/DATABASE.md`](05-data/DATABASE.md) | Schema fisico: tabelle, enumerazioni, indici, vincoli, migrazioni | ⬜ |
| [`05-data/SEED_BOOTSTRAP.md`](05-data/SEED_BOOTSTRAP.md) | Stato iniziale, bootstrap idempotente, credenziali | ⬜ |
### 06 · DevOps
| Documento | Contenuto | Stato |
|---|---|---|
| [`06-devops/INFRASTRUCTURE.md`](06-devops/INFRASTRUCTURE.md) | Topologia, servizi, risorse, rete, persistenza | ⬜ |
| [`06-devops/ENVIRONMENTS.md`](06-devops/ENVIRONMENTS.md) | dev/qa/prod, variabili, promozione | ⬜ |
| [`06-devops/CI_CD.md`](06-devops/CI_CD.md) | Pipeline, verifiche, rilascio, rientro | ⬜ |
| [`06-devops/OBSERVABILITY.md`](06-devops/OBSERVABILITY.md) | Log, metriche, allarmi | ⬜ |
| [`06-devops/RUNBOOK.md`](06-devops/RUNBOOK.md) | Procedure: sintomo → verifica → azione → conferma | ⬜ |
| [`06-devops/BACKUP_DR.md`](06-devops/BACKUP_DR.md) | Backup, ripristino, prova periodica | ⬜ |
### 07 · Qualità
| Documento | Contenuto | Stato |
|---|---|---|
| [`07-quality/TEST_STRATEGY.md`](07-quality/TEST_STRATEGY.md) | Livelli di test, simulatori, criteri di uscita | ⬜ |
| [`07-quality/ACCEPTANCE_CRITERIA.md`](07-quality/ACCEPTANCE_CRITERIA.md) | Criteri bloccanti di rilascio | ⬜ |
| [`07-quality/TRACEABILITY.md`](07-quality/TRACEABILITY.md) | Matrice REQ → SPEC → T → test, senza righe scoperte | ⬜ |
### 08 · Progetto
| Documento | Contenuto | Stato |
|---|---|---|
| [`08-project/DECISIONS.md`](08-project/DECISIONS.md) | Decisioni architetturali `D-xxx` | 🟡 |
| [`08-project/ROADMAP.md`](08-project/ROADMAP.md) | Fasi con criteri di uscita, dipendenze, rischi | ⬜ |
| [`08-project/TODO.md`](08-project/TODO.md) | Task `T-xxx` per fase, tracciati sulle spec | ⬜ |
| [`08-project/CHANGELOG.md`](08-project/CHANGELOG.md) | Cosa cambia per chi usa il prodotto | ⬜ |
| [`08-project/DEVELOPMENT_LOG.md`](08-project/DEVELOPMENT_LOG.md) | Diario tecnico: cosa si è scoperto e perché | ⬜ |
| [`08-project/PROJECT_MEMORY.md`](08-project/PROJECT_MEMORY.md) | **Da leggere a inizio di ogni sessione** | 🟡 |
---
## Struttura del repository
```
<!-- DA COMPILARE: albero delle cartelle principali, una riga di descrizione ciascuna -->
```
---
## Riferimenti esterni
| Risorsa | Percorso / URL |
|---|---|
| <!-- DA COMPILARE: repository, documenti del committente, sistemi collegati --> | |
---
<sub>Documentazione redatta con il metodo spec-driven di STS Lab Srl.</sub>
@@ -0,0 +1,37 @@
# GLOSSARY — Lessico di {{PROGETTO}}
Ultimo aggiornamento: **{{DATA}}**
Un termine, un significato. Gli stessi nomi si usano nei documenti, nel codice (classi,
tabelle, rotte) e nell'interfaccia. Se un termine cambia, cambia qui per primo.
---
## 1. Termini di dominio
| Termine | Significato | Nel codice |
|---|---|---|
| <!-- DA COMPILARE --> | | `nome_tecnico` |
## 2. Termini di piattaforma
| Termine | Significato | Nel codice |
|---|---|---|
| <!-- DA COMPILARE: es. Tenant, Utente, Ruolo --> | | |
## 3. Convenzioni di identificazione nei documenti
| ID | Significato | Dove nasce |
|---|---|---|
| `REQ-<AREA>-<nn>` | Requisito | `01-product/REQUIREMENTS.md` |
| `DIF-<nn>` | Difetto noto | `01-product/REQUIREMENTS.md` |
| `SPEC-<nnn>` | Specifica | `03-specs/` |
| `T-<nnn>` | Task | sezione *Task* della spec e `08-project/TODO.md` |
| `D-<nnn>` | Decisione | `08-project/DECISIONS.md` |
| `A1`, `B3`, … | Criterio di accettazione (lettera della spec + numero) | sezione *Criteri* della spec |
Gli ID sono **immutabili**: non si rinumerano e non si riusano. Un ID chiuso senza lavoro si barra (`~~T-xxx~~`).
## 4. Falsi amici
<!-- DA COMPILARE: parole che il committente usa con un significato diverso da quello tecnico. -->
@@ -0,0 +1,44 @@
# PERSONAS E CASI D'USO — {{PROGETTO}}
Ultimo aggiornamento: **{{DATA}}**
---
## 1. Attori
### 1.1 Attori umani
| Attore | Ruolo nel sistema | Permessi |
|---|---|---|
| <!-- DA COMPILARE --> | | |
### 1.2 Attori non umani
| Attore | Come interagisce |
|---|---|
| <!-- DA COMPILARE: sistemi esterni, job pianificati, integrazioni --> | |
## 2. Persone
### 2.1 <!-- Nome — Ruolo -->
<!-- DA COMPILARE: chi è, cosa fa ogni giorno, cosa lo frustra oggi, cosa considera un successo. -->
## 3. Casi d'uso
### UC-01 — <!-- titolo come risultato: «Una richiesta completa diventa una proposta» -->
| | |
|---|---|
| **Attore** | |
| **Precondizioni** | |
| **Flusso principale** | 1. … 2. … 3. … |
| **Flussi alternativi** | |
| **Esito** | |
| **Requisiti** | `REQ-…` |
## 4. Matrice attore × caso d'uso
| Caso d'uso | <!-- attore 1 --> | <!-- attore 2 --> |
|---|:-:|:-:|
| UC-01 | ● | |
@@ -0,0 +1,62 @@
# REQUIREMENTS — {{PROGETTO}}
Ultimo aggiornamento: **{{DATA}}** · Stato: **in stesura**
---
## Come leggere questo documento
Ogni requisito ha un identificativo `REQ-<AREA>-<nn>` immutabile, citato dalle specifiche
(`03-specs/`), dai task (`08-project/TODO.md`) e dalla matrice di tracciabilità
(`07-quality/TRACEABILITY.md`).
**Tipo:**
| Sigla | Significato |
|:-:|---|
| **P** | **Parità** — comportamento già presente nel sistema esistente, da riprodurre **identico**. Una differenza è una regressione. *(Solo per riscritture o evoluzioni.)* |
| **C** | **Correzione** — comportamento esistente difettoso, da correggere. Rimanda a un `DIF-nn`. |
| **N** | **Nuovo** — introdotto da questo progetto. |
**Origine:** dove il requisito è stato osservato o deciso: committente e data, `D-nnn`, oppure
`file:funzione` del codice esistente per i requisiti di parità.
> **Regola.** Un requisito descrive un comportamento osservabile e verificabile, non una
> soluzione tecnica. Modificare un requisito approvato è una decisione di prodotto e richiede
> una `D-nnn`.
---
## Riepilogo
| Area | Codice | Requisiti | P | C | N |
|---|:-:|:-:|:-:|:-:|:-:|
| <!-- DA COMPILARE: es. Autenticazione e utenti --> | <!-- AUT --> | | | | |
| Non funzionali | NFR | | | | |
| **Totale** | | | | | |
---
## <!-- AREA --> · <!-- Nome dell'area -->
| ID | Requisito | Tipo | Origine |
|---|---|:-:|---|
| <!-- **REQ-AREA-01** --> | <!-- DA COMPILARE: «Il sistema …», condizioni in grassetto, limiti numerici espliciti --> | N | <!-- committente, AAAA-MM-GG --> |
---
## NFR · Non funzionali
| ID | Requisito | Tipo | Origine |
|---|---|:-:|---|
| <!-- **REQ-NFR-01** --> | <!-- DA COMPILARE: prestazioni, disponibilità, sicurezza, conservazione dei dati — sempre con un numero --> | N | |
---
## Difetti noti
Difetti del sistema esistente, o emersi durante il progetto, che un requisito di tipo **C** corregge.
| ID | Difetto | Effetto osservato | Requisito |
|---|---|---|---|
| <!-- **DIF-01** --> | | | `REQ-…` |
@@ -0,0 +1,52 @@
# VISION — {{PROGETTO}}
Ultimo aggiornamento: **{{DATA}}** · Stato: **in stesura**
---
## 1. La frase che guida il prodotto
> <!-- DA COMPILARE: una frase sola, quella con cui si decide cosa entra e cosa no. -->
## 2. Il problema
<!-- DA COMPILARE: chi ha il problema, come lo risolve oggi, quanto costa (tempo, errori, denaro).
Preferire un dato misurato a un aggettivo. -->
## 3. Cosa fa il prodotto
<!-- DA COMPILARE: il flusso principale in 5-7 passi, dal punto di vista di chi lo usa. -->
## 4. Perché adesso
<!-- DA COMPILARE: cosa rende necessario il progetto ora (scadenza, cliente, limite del sistema attuale). -->
## 5. Cosa NON cambia
<!-- DA COMPILARE: vincoli fissi (sistemi esistenti, interfacce, abitudini degli utenti), oppure «non applicabile». -->
## 6. Perimetro
### Dentro il perimetro (primo rilascio)
- <!-- DA COMPILARE -->
### Fuori dal perimetro (esplicitamente)
- <!-- DA COMPILARE: ciò che qualcuno potrebbe aspettarsi e che NON si fa. Scriverlo evita discussioni dopo. -->
## 7. Come si misura il successo
| Indicatore | Valore attuale | Obiettivo |
|---|:-:|:-:|
| <!-- DA COMPILARE --> | | |
## 8. Rischi principali
| Rischio | Probabilità | Impatto | Contromisura |
|---|:-:|:-:|---|
| <!-- DA COMPILARE --> | | | |
## 9. Documenti collegati
[`REQUIREMENTS.md`](REQUIREMENTS.md) · [`PERSONAS_CASI_USO.md`](PERSONAS_CASI_USO.md) · [`../08-project/DECISIONS.md`](../08-project/DECISIONS.md)
@@ -0,0 +1,52 @@
# ARCHITECTURE — {{PROGETTO}}
Ultimo aggiornamento: **{{DATA}}** · Stato: **in stesura**
---
## 1. Vista d'insieme
```
<!-- DA COMPILARE: schema ASCII dei servizi e delle frecce principali -->
```
## 2. Servizi
| Servizio | Tecnologia | Responsabilità | Non fa |
|---|---|---|---|
| <!-- DA COMPILARE --> | | | |
<!-- Per ogni scelta non ovvia («perché due servizi e non uno») una sottosezione breve che
rimanda alla D-xxx. -->
## 3. Flusso principale
<!-- DA COMPILARE: il caso d'uso UC-01 attraverso i servizi, passo per passo, con le code e i dati scambiati. -->
## 4. Code e lavori asincroni
| Coda / lavoro | Produttore | Consumatore | Tentativi | Cosa succede se fallisce |
|---|---|---|:-:|---|
| <!-- DA COMPILARE o «non applicabile» --> | | | | |
## 5. Configurazione
<!-- DA COMPILARE: cosa è variabile d'ambiente, cosa è configurazione a caldo, cosa è segreto. -->
## 6. Multi-tenancy e isolamento
<!-- DA COMPILARE o «non applicabile». -->
## 7. Integrazioni esterne
| Sistema | Direzione | Protocollo | Contratto |
|---|---|---|---|
| <!-- DA COMPILARE --> | | | |
## 8. Ambienti
Vedi [`../06-devops/ENVIRONMENTS.md`](../06-devops/ENVIRONMENTS.md).
## 9. Documenti collegati
[`C4_MODEL.md`](C4_MODEL.md) · [`DATA_MODEL.md`](DATA_MODEL.md) · [`DOMAIN_WORKFLOW.md`](DOMAIN_WORKFLOW.md) · [`SECURITY.md`](SECURITY.md)
@@ -0,0 +1,35 @@
# C4 MODEL — Contesto, Contenitori, Componenti
Ultimo aggiornamento: **{{DATA}}**
---
## Livello 1 · Contesto
```
<!-- DA COMPILARE: il sistema al centro, attori e sistemi esterni intorno -->
```
| Attore / sistema esterno | Relazione con {{PROGETTO}} |
|---|---|
| <!-- DA COMPILARE --> | |
## Livello 2 · Contenitori
| Contenitore | Tecnologia | Parla con | Porta / protocollo |
|---|---|---|---|
| <!-- DA COMPILARE --> | | | |
## Livello 3 · Componenti
### 3.1 <!-- Contenitore -->
| Componente | Responsabilità | Spec |
|---|---|---|
| <!-- DA COMPILARE --> | | `SPEC-…` |
## 4. Corrispondenza fra i livelli e i requisiti
| Componente | Requisiti |
|---|---|
| <!-- DA COMPILARE --> | `REQ-…` |
@@ -0,0 +1,42 @@
# DATA MODEL — Entità, relazioni, invarianti
Ultimo aggiornamento: **{{DATA}}**
Modello **logico**. Lo schema fisico (tipi, indici, migrazioni) è in
[`../05-data/DATABASE.md`](../05-data/DATABASE.md).
---
## 1. Mappa delle entità
```
<!-- DA COMPILARE: entità e cardinalità, es. Tenant 1──* Utente -->
```
## 2. Entità
### 2.1 <!-- Entità -->
| Attributo | Significato | Obbligatorio | Note |
|---|---|:-:|---|
| <!-- DA COMPILARE --> | | | |
**Ciclo di vita:** <!-- chi la crea, chi la modifica, quando si cancella -->
## 3. Chiavi naturali e univocità
| Entità | Univoca per |
|---|---|
| <!-- DA COMPILARE --> | |
## 4. Cancellazioni
| Entità | Cancellazione | Effetto sui figli |
|---|---|---|
| <!-- DA COMPILARE: fisica, logica (soft delete), vietata --> | | |
## 5. Invarianti del modello
| # | Invariante |
|:-:|---|
| M1 | <!-- DA COMPILARE --> |
@@ -0,0 +1,35 @@
# DOMAIN WORKFLOW — Stati, transizioni, invarianti
Ultimo aggiornamento: **{{DATA}}**
<!-- Se il dominio non ha un ciclo di vita con stati, scrivere «non applicabile» e il perché. -->
---
## 1. Stati
| Stato | Significato | Terminale |
|---|---|:-:|
| <!-- DA COMPILARE --> | | |
## 2. Macchina a stati
```
<!-- DA COMPILARE: diagramma ASCII degli stati -->
```
## 3. Transizioni ammesse
Qualsiasi transizione non elencata qui è **rifiutata**.
| # | Da | A | Attore | Condizione | Effetti collaterali |
|:-:|---|---|---|---|---|
| T1 | | | | | |
## 4. Invarianti
Condizioni che devono essere vere **sempre**, verificate da test.
| # | Invariante |
|:-:|---|
| I1 | <!-- DA COMPILARE --> |
@@ -0,0 +1,44 @@
# SECURITY — Modello di sicurezza
Ultimo aggiornamento: **{{DATA}}**
---
## 1. Perimetro
<!-- DA COMPILARE: cosa è esposto a Internet, cosa resta interno. -->
## 2. Autenticazione
<!-- DA COMPILARE: meccanismo, durata dei token, rotte pubbliche (elenco chiuso: aggiungerne una richiede una D-xxx). -->
## 3. Autorizzazione
| Ruolo | Può | Non può |
|---|---|---|
| <!-- DA COMPILARE --> | | |
Regola: nessuno crea un utente più potente di sé.
## 4. Segreti
- I segreti non entrano mai nel repository, nei log o nella documentazione.
- <!-- DA COMPILARE: dove vivono (variabili d'ambiente, secret manager, cifrati a riposo). -->
## 5. Dati in ingresso e in uscita
<!-- DA COMPILARE: validazione, dimensioni massime, dati personali, cosa esce verso terzi. -->
## 6. Audit
<!-- DA COMPILARE: quali eventi si registrano, dove, per quanto tempo. -->
## 7. Minacce e contromisure
| Minaccia | Contromisura | Verificata da |
|---|---|---|
| <!-- DA COMPILARE --> | | test / `REQ-…` |
## 8. Cosa resta aperto
<!-- DA COMPILARE -->
@@ -0,0 +1,59 @@
# API — Catalogo e convenzioni
Ultimo aggiornamento: **{{DATA}}**
Il contratto è [`openapi.yaml`](openapi.yaml), **fonte di verità**: in caso di differenza fra
questo documento, il codice e il contratto, vale il contratto. Si scrive il contratto **prima**
del codice (contract-first).
---
## 1. Convenzioni
| Aspetto | Regola |
|---|---|
| Prefisso | `/api/v1` |
| Formato | JSON, UTF-8, date ISO 8601 in UTC |
| Nomi | <!-- DA COMPILARE: snake_case o camelCase, una sola scelta --> |
| Autenticazione | <!-- DA COMPILARE --> |
## 2. Errori
Forma unica per tutti gli errori:
```json
{ "error": { "code": "stringa_stabile", "message": "testo leggibile", "details": {} } }
```
| Codice HTTP | Quando |
|:-:|---|
| 400 | Richiesta malformata |
| 401 | Non autenticato |
| 403 | Autenticato ma non autorizzato |
| 404 | Risorsa inesistente **o di un altro tenant** (non si rivela l'esistenza) |
| 409 | Conflitto (duplicato, stato non compatibile) |
| 422 | Validazione fallita |
## 3. Paginazione
<!-- DA COMPILARE: parametri (page/size o cursore), limiti massimi, forma della risposta. -->
## 4. Catalogo
### <!-- Gruppo --> — `SPEC-…`
| Metodo | Percorso | Ruolo minimo | Descrizione |
|---|---|---|---|
| <!-- DA COMPILARE --> | | | |
## 5. API interne — non esposte
<!-- DA COMPILARE o «nessuna». -->
## 6. Idempotenza e concorrenza
<!-- DA COMPILARE: chiavi di idempotenza, controllo delle versioni, comportamento in caso di doppio invio. -->
## 7. Limiti
<!-- DA COMPILARE: rate limit, dimensioni massime dei corpi e dei file. -->
@@ -0,0 +1,54 @@
# Contratto pubblico di {{PROGETTO}} — FONTE DI VERITÀ (D-002).
# Si modifica PRIMA del codice. Ogni operazione cita la spec che la definisce.
openapi: 3.1.0
info:
title: "{{PROGETTO}} API"
version: 0.1.0
description: |
Contratto dell'API di {{PROGETTO}}.
Documentazione redatta con il metodo spec-driven di STS Lab Srl.
servers:
- url: /api/v1
tags:
- name: health
description: Stato del servizio
paths:
/health:
get:
tags: [health]
summary: Stato del servizio
security: []
responses:
"200":
description: Servizio attivo
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: ok
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
schemas:
Error:
type: object
required: [error]
properties:
error:
type: object
required: [code, message]
properties:
code:
type: string
message:
type: string
details:
type: object
security:
- BearerAuth: []
@@ -0,0 +1,47 @@
# DATABASE — Schema fisico
Ultimo aggiornamento: **{{DATA}}**
Traduzione fisica di [`../02-architecture/DATA_MODEL.md`](../02-architecture/DATA_MODEL.md).
---
## 1. Convenzioni
| Aspetto | Regola |
|---|---|
| Motore | <!-- DA COMPILARE: es. PostgreSQL 16 --> |
| Nomi | tabelle al singolare, `snake_case` |
| Chiavi primarie | <!-- DA COMPILARE: UUID / bigint --> |
| Date | `timestamptz`, sempre UTC |
| Multi-tenancy | <!-- DA COMPILARE: colonna `tenant_id` su ogni tabella di dominio, o «non applicabile» --> |
## 2. Tipi enumerati
| Tipo | Valori |
|---|---|
| <!-- DA COMPILARE --> | |
## 3. Tabelle
### `<!-- tabella -->`
| Colonna | Tipo | Null | Default | Note |
|---|---|:-:|---|---|
| `id` | | no | | PK |
Indici: <!-- DA COMPILARE --> · Vincoli: <!-- DA COMPILARE -->
## 4. Vincoli che riflettono requisiti
| Vincolo | Requisito |
|---|---|
| <!-- DA COMPILARE --> | `REQ-…` |
## 5. Migrazioni
<!-- DA COMPILARE: strumento (es. Alembic), regola «una migrazione per modifica, mai riscrivere una migrazione già applicata». -->
## 6. Dimensionamento
<!-- DA COMPILARE: volumi attesi per tabella a 1 e 3 anni. -->
@@ -0,0 +1,30 @@
# SEED E BOOTSTRAP — Stato iniziale del sistema
Ultimo aggiornamento: **{{DATA}}**
---
## 1. Principio
Il bootstrap è **idempotente**: eseguirlo due volte produce lo stesso stato di eseguirlo una.
## 2. Cosa viene creato
| Elemento | Quando | Origine dei valori |
|---|---|---|
| <!-- DA COMPILARE: utente amministratore iniziale, configurazione di base --> | primo avvio | variabili d'ambiente |
## 3. Credenziali iniziali
In collaudo e produzione l'avvio **fallisce** se le credenziali di bootstrap sono lasciate ai
valori predefiniti.
## 4. Variabili di bootstrap
| Variabile | Obbligatoria | Significato |
|---|:-:|---|
| <!-- DA COMPILARE --> | | |
## 5. Dati di prova per lo sviluppo
<!-- DA COMPILARE: come si generano, e la regola che non arrivano mai in produzione. -->
@@ -0,0 +1,34 @@
# BACKUP E RIPRISTINO
Ultimo aggiornamento: **{{DATA}}**
---
## 1. Cosa va salvato
| Dato | Dove vive | Ricostruibile | Backup |
|---|---|:-:|---|
| <!-- DA COMPILARE --> | | | |
## 2. Obiettivi
| Obiettivo | Valore |
|---|---|
| RPO (dati che si accetta di perdere) | <!-- DA COMPILARE --> |
| RTO (tempo per tornare operativi) | <!-- DA COMPILARE --> |
## 3. Procedura di ripristino
1. <!-- DA COMPILARE -->
## 4. Verifiche dopo un ripristino
- [ ] <!-- DA COMPILARE -->
## 5. Prova di ripristino
Un backup mai ripristinato non è un backup. Prova <!-- DA COMPILARE: frequenza -->, esito registrato in `DEVELOPMENT_LOG`.
## 6. Cosa non è coperto
<!-- DA COMPILARE -->
@@ -0,0 +1,31 @@
# CI/CD — Integrazione e rilascio
Ultimo aggiornamento: **{{DATA}}**
---
## 1. Flussi
| Flusso | Innesco | Ambiente di destinazione |
|---|---|---|
| <!-- DA COMPILARE --> | | |
## 2. Sequenza
1. <!-- DA COMPILARE: build, test, analisi, immagini, deploy -->
## 3. Verifiche in pipeline
| Verifica | Perché | Blocca il rilascio |
|---|---|:-:|
| Test automatici | criteri di accettazione delle spec | sì |
| Ricerca di segreti | nessun segreto nel repository | sì |
| Contratto contro implementazione | il codice rispetta `openapi.yaml` | sì |
## 4. Segreti della pipeline
<!-- DA COMPILARE -->
## 5. Rilascio in produzione e rientro
<!-- DA COMPILARE: chi approva, come si torna alla versione precedente, in quanto tempo. -->
@@ -0,0 +1,31 @@
# ENVIRONMENTS — Sviluppo, collaudo, produzione
Ultimo aggiornamento: **{{DATA}}**
---
## 1. Gli ambienti
| Ambiente | Scopo | Dati | Chi vi accede |
|---|---|---|---|
| dev | sviluppo | di prova | sviluppatori |
| qa | collaudo | di prova, realistici | sviluppatori, committente |
| prod | esercizio | reali | utenti |
## 2. Variabili di configurazione
| Variabile | Significato | dev | qa | prod | Segreta |
|---|---|---|---|---|:-:|
| <!-- DA COMPILARE --> | | | | | |
## 3. Ambiente locale di sviluppo
<!-- DA COMPILARE: comandi per avviare tutto in locale (es. docker compose up). -->
## 4. Promozione fra ambienti
<!-- DA COMPILARE: dev → qa → prod, chi approva, cosa si verifica a ogni passo. -->
## 5. Prima di dichiarare un ambiente pronto
- [ ] <!-- DA COMPILARE -->
@@ -0,0 +1,31 @@
# INFRASTRUCTURE — Topologia e risorse
Ultimo aggiornamento: **{{DATA}}**
---
## 1. Topologia
```
<!-- DA COMPILARE: nodi, servizi, rete -->
```
## 2. Servizi e risorse
| Servizio | Immagine / runtime | Repliche | CPU | Memoria |
|---|---|:-:|---|---|
| <!-- DA COMPILARE --> | | | | |
## 3. Persistenza
| Volume / storage | Contenuto | Dimensione | Backup |
|---|---|---|:-:|
| <!-- DA COMPILARE --> | | | |
## 4. Rete, nomi e certificati
<!-- DA COMPILARE: domini, ingress, TLS, porte esposte. -->
## 5. Capacità e crescita
<!-- DA COMPILARE -->
@@ -0,0 +1,37 @@
# OBSERVABILITY — Log, metriche, allarmi
Ultimo aggiornamento: **{{DATA}}**
---
## 1. Il problema che risolve
<!-- DA COMPILARE: quali domande operative si devono poter rispondere senza aprire il database. -->
## 2. Log
| Aspetto | Regola |
|---|---|
| Formato | JSON strutturato |
| Correlazione | un identificativo di richiesta propagato fra i servizi |
| Mai nei log | password, token, segreti, dati personali non necessari |
## 3. Metriche
| Metrica | Tipo | Significato |
|---|---|---|
| <!-- DA COMPILARE --> | | |
## 4. Allarmi
| Allarme | Condizione | Gravità | Procedura |
|---|---|:-:|---|
| <!-- DA COMPILARE --> | | | [`RUNBOOK`](RUNBOOK.md) §… |
## 5. Salute
`GET /health` — <!-- DA COMPILARE: cosa verifica. -->
## 6. Conservazione
<!-- DA COMPILARE -->
@@ -0,0 +1,25 @@
# RUNBOOK — Procedure operative
Ultimo aggiornamento: **{{DATA}}**
Ogni procedura segue lo stesso schema: **sintomo → verifica → azione → conferma**.
---
## 1. <!-- Sintomo, come lo descriverebbe chi lo nota -->
| Passo | Cosa fare |
|---|---|
| **Verifica** | <!-- comando o schermata --> |
| **Azione** | |
| **Conferma** | |
## 2. Ripristino da backup
Vedi [`BACKUP_DR.md`](BACKUP_DR.md) §… .
## 3. Contatti e riferimenti
| Ruolo | Contatto |
|---|---|
| <!-- DA COMPILARE --> | |
@@ -0,0 +1,35 @@
# ACCEPTANCE CRITERIA — Criteri di rilascio
Ultimo aggiornamento: **{{DATA}}**
---
## 1. Riepilogo per specifica
| Spec | Criteri | Verificati | Stato |
|---|:-:|:-:|:-:|
| <!-- SPEC-001 --> | | | ⬜ |
## 2. Criteri di rilascio — versione <!-- x.y.z -->
### 2.1 Bloccanti
| # | Criterio | Verificato da |
|:-:|---|---|
| R1 | Tutti i test passano in pipeline | CI |
| R2 | Nessuna riga scoperta in `TRACEABILITY.md` | revisione |
| R3 | <!-- DA COMPILARE --> | |
### 2.2 Attesi (non bloccanti)
| # | Criterio |
|:-:|---|
| <!-- DA COMPILARE --> | |
## 3. Verifica funzionale prima del rilascio
- [ ] <!-- DA COMPILARE: percorsi da provare a mano con il committente -->
## 4. Cosa non è criterio di rilascio
<!-- DA COMPILARE -->
@@ -0,0 +1,38 @@
# TEST STRATEGY
Ultimo aggiornamento: **{{DATA}}**
---
## 1. Il criterio
Ogni criterio di accettazione di ogni spec ha **almeno un test eseguito**. Lo stato di un
requisito in [`TRACEABILITY.md`](TRACEABILITY.md) diventa ✅ quando il test passa, non quando
il codice è scritto. Ogni difetto `DIF-nn` ha un test che lo riproduce.
## 2. Livelli
| Livello | Cosa verifica | Strumento | Dove |
|---|---|---|---|
| Unitario | funzioni pure, regole di business | <!-- DA COMPILARE --> | |
| Integrazione | servizi + database + code, con simulatori dei sistemi esterni | | |
| Contratto | implementazione contro `openapi.yaml` | | |
| End-to-end | casi d'uso dall'interfaccia | | |
## 3. Simulatori dei sistemi esterni
| Sistema | Simulatore | Comportamenti simulati (anche i guasti) |
|---|---|---|
| <!-- DA COMPILARE --> | | |
## 4. Nominare i test
Il nome del test cita il criterio: `test_D3_sufficiente_urgente_ricorrente_lotto_grande_priorita_1`.
## 5. Esecuzione
<!-- DA COMPILARE: comandi, durata attesa, numero di test (aggiornato in DEVELOPMENT_LOG). -->
## 6. Cosa non verifichiamo
<!-- DA COMPILARE, con il perché. -->
@@ -0,0 +1,48 @@
# TRACEABILITY — Matrice REQ → SPEC → T → test
Ultimo aggiornamento: **{{DATA}}** · Stato: **in stesura**
---
## 1. Regola
> **Nessuna riga vuota.** Ogni requisito ha una specifica, ogni specifica ha task, ogni task ha
> test. Un requisito senza spec non è implementabile; un task senza spec non entra in lavorazione.
La colonna *Stato* si compila durante l'implementazione: ⬜ da fare · 🟡 in corso · ✅ verificato.
Lo stato si aggiorna **quando il test passa, non quando il codice è scritto**.
---
## 2. Copertura
| Area | REQ | Spec | Task | Criteri | Copertura |
|---|:-:|---|---|:-:|:-:|
| <!-- DA COMPILARE --> | | | | | ⬜ |
| **Totale** | | | | | |
---
## 3. Matrice per requisito
### <!-- AREA --> · <!-- Nome -->
| REQ | Spec | Task | Criteri | Stato |
|---|---|---|---|:-:|
| <!-- AREA-01 --> | <!-- SPEC-001 §2 --> | <!-- T-010 --> | <!-- A1 --> | ⬜ |
---
## 4. Decisioni → specifiche
| Decisione | Dove si applica | Task |
|---|---|---|
| D-002 | metodo | — |
---
## 5. Difetti → test
| Difetto | Requisito | Test |
|---|---|---|
| <!-- DIF-01 --> | | |
@@ -0,0 +1,14 @@
# CHANGELOG
Formato: [Keep a Changelog](https://keepachangelog.com/it/1.1.0/) · Versionamento semantico.
Le voci descrivono ciò che cambia **per chi usa il prodotto**. Le decisioni tecniche stanno in
[`DECISIONS.md`](DECISIONS.md), il diario di lavoro in [`DEVELOPMENT_LOG.md`](DEVELOPMENT_LOG.md).
---
## [Non rilasciato] — 0.1.0
### Aggiunto
- Documentazione di progetto con metodo spec-driven ({{DATA}}).
@@ -0,0 +1,83 @@
# DECISIONS — Registro delle decisioni architetturali (ADR)
Ultimo aggiornamento: **{{DATA}}** · Progetto: **{{PROGETTO}}**
Ogni decisione strutturale del progetto è registrata qui con un identificativo `D-xxx` immutabile.
Le decisioni si **superano**, non si cancellano: una decisione sbagliata resta a registro con stato
*Superata da D-yyy*, così la storia del progetto resta leggibile.
**Stati:** `Proposta` · `Accettata` · `Superata da D-xxx` · `Respinta`
**Formato:** Contesto → Decisione → Alternative valutate → Conseguenze.
---
## Indice
| ID | Titolo | Stato | Data |
|---|---|---|---|
| [D-001](#d-001--stack-tecnologico) | Stack tecnologico | Proposta | {{DATA}} |
| [D-002](#d-002--sviluppo-spec-driven-vincolante) | Sviluppo spec-driven vincolante | Accettata | {{DATA}} |
| [D-003](#d-003--la-documentazione-vive-nel-repository) | La documentazione vive nel repository | Accettata | {{DATA}} |
---
## D-001 — Stack tecnologico
**Stato:** Proposta · {{DATA}}
### Contesto
<!-- DA COMPILARE: vincoli del committente, competenze, sistemi esistenti con cui integrarsi. -->
### Decisione
<!-- DA COMPILARE: linguaggi, framework, database, code, storage, deploy. -->
### Alternative valutate
- <!-- DA COMPILARE: alternativa — perché scartata. -->
### Conseguenze
- <!-- DA COMPILARE -->
---
## D-002 — Sviluppo spec-driven vincolante
**Stato:** Accettata · {{DATA}}
### Contesto
Le regole di business scritte solo nel codice emergono dal debugging invece che dalla
specifica, e si scoprono quando un difetto le rivela.
### Decisione
Nessuna implementazione senza una `SPEC-xxx` in stato *Approvata*. Ogni spec contiene: contesto,
requisiti coperti (`REQ-xxx`), contratto API, modello dati, regole, casi di errore, criteri di
accettazione, task derivati (`T-xxx`). `07-quality/TRACEABILITY.md` mantiene la matrice
REQ → SPEC → T → test e non deve avere righe vuote. Il contratto API (`04-api/openapi.yaml`) è
la fonte di verità e si scrive prima del codice.
### Alternative valutate
- **Documentazione a valle del codice.** Produce documenti veri ma tardivi; le regole restano
scoperte finché un bug non le rivela.
### Conseguenze
- La fase di documentazione precede quella di codice; il codice inizia più tardi e procede più dritto.
- Una spec approvata si modifica solo con una nuova `D-xxx` o una revisione tracciata.
---
## D-003 — La documentazione vive nel repository
**Stato:** Accettata · {{DATA}}
### Contesto
Documentazione e codice divergono quando vivono in luoghi diversi.
### Decisione
`docs/` sta nel repository del codice e si aggiorna **nella stessa modifica** del codice che la
riguarda.
### Alternative valutate
- **Wiki esterno.** Più comodo da modificare, ma non versionato con il codice.
### Conseguenze
- Una revisione del codice è anche revisione della documentazione.
@@ -0,0 +1,14 @@
# DEVELOPMENT LOG — Diario tecnico
Diario in ordine cronologico inverso. Registra **cosa si è scoperto e perché si è deciso così**,
non l'elenco delle modifiche (quello è in [`CHANGELOG.md`](CHANGELOG.md)). Le voci non si
cancellano: si annotano.
Formato del titolo: `## AAAA-MM-GG (n) · titolo che dice cosa si è capito`
---
## {{DATA}} · Nasce la documentazione
Creato lo scheletro della documentazione spec-driven. Decisioni iniziali: `D-001` (stack,
proposta), `D-002` (metodo spec-driven), `D-003` (documentazione nel repository).
@@ -0,0 +1,43 @@
# PROJECT MEMORY — Memoria permanente
Ultimo aggiornamento: **{{DATA}}**
Da leggere **all'inizio di ogni sessione di lavoro** sul progetto. Contiene ciò che non è
deducibile dal codice e che, se dimenticato, fa ripetere errori già commessi. Non è un
riassunto della documentazione: ogni voce qui deve avere un perché.
---
## 1. In una frase
<!-- DA COMPILARE -->
---
## 2. Le cose da non dimenticare mai
<!-- Una sottosezione per vincolo. Ogni vincolo dice: la regola, perché esiste (meglio se con
un fatto osservato e la data), la decisione D-xxx che la fissa. -->
### 2.1 <!-- vincolo -->
---
## 3. Azioni pendenti fuori sequenza
<!-- DA COMPILARE o «nessuna». Es. credenziali da ruotare. -->
---
## 4. Metodo
Spec-driven **vincolante** (D-002): `REQ → SPEC → T → codice + test → TRACEABILITY`.
Si implementa solo da una spec *Approvata*.
---
## 5. Come riprendere il lavoro
1. Leggere questo file e [`../00-INDEX.md`](../00-INDEX.md) (riquadri delle novità in testa).
2. Aprire [`TODO.md`](TODO.md): fuori sequenza, poi la fase corrente.
3. Per il task scelto: la sua spec, i `REQ` e le `D-xxx` citate.
@@ -0,0 +1,45 @@
# ROADMAP
Ultimo aggiornamento: **{{DATA}}**
---
## 0. Fuori sequenza — da fare subito
| # | Azione | Perché ora |
|:-:|---|---|
| <!-- DA COMPILARE o «nessuna» --> | | |
## 1. Fasi
```
┌────────────────────────────────────────────────────────────────────┐
│ FASE 0 · Documentazione │
│ visione · requisiti · architettura · spec · contratto · dati │
│ ▸ criterio di uscita: spec della fase 1 approvate │
└────────────────────────────────────────────────────────────────────┘
▼
┌────────────────────────────────────────────────────────────────────┐
│ FASE 1 · Fondamenta │
│ <!-- DA COMPILARE --> │
│ ▸ criterio di uscita: <!-- DA COMPILARE --> │
└────────────────────────────────────────────────────────────────────┘
```
## 2. Dettaglio
### Fase 1 · Fondamenta
| Spec | Task | Criterio di uscita |
|---|---|---|
| <!-- SPEC-001 --> | <!-- T-010…019 --> | |
## 3. Dipendenze critiche
<!-- DA COMPILARE -->
## 4. Rischi
| Rischio | Fase | Contromisura |
|---|---|---|
| <!-- DA COMPILARE --> | | |
@@ -0,0 +1,39 @@
# TODO — Backlog
Ultimo aggiornamento: **{{DATA}}**
Stato: ⬜ da fare · 🟡 in corso · ✅ fatto · ⏸ sospeso
**Regola:** un task entra in lavorazione solo se la sua specifica è approvata (D-002). Al termine
si aggiorna [`TRACEABILITY.md`](../07-quality/TRACEABILITY.md) — quando il test passa, non quando
il codice è scritto.
**Numerazione:** i task di `SPEC-nnn` usano il blocco `T-nnn0…T-nnn9` (la spec 4 usa i task da
40 a 49); oltre il nono si aggiunge un suffisso (`b`, `c`). I task fuori sequenza usano i numeri
da 1 a 9.
---
## 🔴 Fuori sequenza — prima di tutto
| ID | Task | Stato |
|---|---|:-:|
| <!-- T-001 --> | | ⬜ |
---
## Fase 0 · Documentazione
| ID | Task | Spec | Stato |
|---|---|---|:-:|
| — | Visione, glossario, casi d'uso, requisiti | — | 🟡 |
| — | Architettura, modello dati, sicurezza | — | ⬜ |
| — | Specifiche della fase 1 approvate | — | ⬜ |
---
## Fase 1 · Fondamenta
| ID | Task | Spec | Stato |
|---|---|---|:-:|
| <!-- T-010 --> | | <!-- SPEC-001 --> | ⬜ |
+45
View File
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="210mm" height="297mm">
<defs>
<linearGradient id="b">
<stop offset="0" stop-color="#fff"/>
<stop offset="1" stop-color="#fff" stop-opacity="0"/>
</linearGradient>
<linearGradient id="e" x1="49.136964" x2="747.09593" y1="467.2588" y2="467.2588" gradientTransform="scale(.90134 1.10946)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#729fcf"/>
<stop offset="1" stop-color="#204a87"/>
</linearGradient>
<linearGradient id="a">
<stop offset="0" stop-color="#fff"/>
<stop offset="1" stop-color="#729fcf"/>
</linearGradient>
<linearGradient id="f">
<stop offset="0"/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<linearGradient id="c">
<stop offset="0" stop-color="#3977c3"/>
<stop offset="1" stop-color="#89aedc" stop-opacity="0"/>
</linearGradient>
<linearGradient id="d">
<stop offset="0" stop-color="#729fcf"/>
<stop offset=".31578946" stop-color="#babdb6"/>
<stop offset="1" stop-color="#3465a4"/>
</linearGradient>
<linearGradient xlink:href="#a" id="k" x1="24" x2="24" y1="16.525082" y2="13.284962" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#b" id="l" x1="16.07143" x2="15.785715" y1="19.5" y2="30" gradientTransform="translate(-152.46168 8.8865747) scale(21.1429)" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#c" id="j" x1="17.160095" x2="37.969398" y1="14.004482" y2="56.575912" gradientTransform="translate(-149.44117 -46.991167) scale(21.1429)" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#d" id="h" x1="9.3030529" x2="38.826412" y1="26.71875" y2="26.71875" gradientTransform="translate(-149.44117 -46.991167) scale(21.1429)" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#e" id="i" x1="49.136964" x2="747.09593" y1="467.2588" y2="467.2588" gradientTransform="matrix(.90134 0 0 1.10946 .52026458 -.48501452)" gradientUnits="userSpaceOnUse"/>
<radialGradient xlink:href="#f" id="g" cx="23.857143" cy="40" r="17.142857" fx="23.857143" fy="40" gradientTransform="matrix(1 0 0 .5 0 20)" gradientUnits="userSpaceOnUse"/>
</defs>
<path fill="url(#g)" d="M41 40a17.142857 8.5714283 0 1 1-34.2857132 0A17.142857 8.5714283 0 1 1 41 40z" display="block" overflow="visible" style="marker:none" transform="matrix(22.3851 0 0 24.34096 -173.03587 -228.02997)"/>
<path fill="url(#h)" stroke="url(#i)" stroke-miterlimit="10" stroke-width="21.14290619" d="M356.66648 141.31284c-166.17052 0-301.285983 59.253-301.285983 132.14314v488.92965c0 72.89018 135.115463 132.14315 301.285983 132.14315 166.17053 0 306.66725-59.25297 306.66725-132.14315V273.45598c0-72.89014-140.49672-132.14314-306.66725-132.14314z" display="block" overflow="visible" style="marker:none"/>
<path fill="none" stroke="url(#j)" stroke-miterlimit="10" stroke-width="21.14290619" d="M356.84833 157.78994c-154.92885 0-280.904411 55.24468-280.904411 123.20413v467.81359c0 67.95922 125.975561 123.20393 280.904411 123.20393 154.93094 0 285.92181-55.24471 285.92181-123.20393V280.99407c0-67.95945-130.99087-123.20413-285.92181-123.20413z" display="block" opacity=".5443038" overflow="visible" style="marker:none"/>
<path fill="#3465a4" stroke="url(#k)" stroke-miterlimit="10" stroke-width="1.00492704" d="M36.857142 14.071428a12.857142 5.5 0 1 1-25.714284 0 12.857142 5.5 0 1 1 25.714284 0z" display="block" overflow="visible" style="marker:none" transform="matrix(21.93135 0 0 20.18342 -168.36368 -12.347239)"/>
<path fill="url(#l)" d="m139.00874 369.82596 7.5501 354.89887 96.65272 27.18366-3.0192-353.38841s43.7954 0 114.77637-4.53074c-110.27718-4.92776-236.44943-52.3219-279.389384-83.06136 29.958254 44.23476 63.429394 58.89798 63.429394 58.89798z" display="block" opacity=".49367083" overflow="visible" style="marker:none"/>
<path fill="#204a87" d="M114.15586 701.5918h16.09033v-63.74234l25.1253 55.573c2.84764 6.6832 6.93184 9.15937 14.8527 9.15937 7.92167 0 11.7583-2.47543 14.72914-9.15866l25.00197-55.57371v63.74234h16.21373v-63.74234c0-6.18856-2.47543-9.15937-7.67397-10.7683-12.25289-3.71313-20.42233-.4946-24.13546 7.79743l-24.63007 54.95407-23.7643-54.95407c-3.58957-8.29206-11.88261-11.51056-24.25961-7.79743-5.07443 1.6089-7.54976 4.5797-7.54976 10.7683v63.74234zm125.38108-51.86037h16.21373v35.0272c-.12343 1.98003.61887 6.43708 9.40633 6.56054 4.5797.12276 34.90377 0 35.15144 0v-41.83541h16.21454v57.30688c0 14.1095-17.45224 17.20454-25.62087 17.328h-50.99404v-10.7683h51.1175c10.39707-1.11347 9.15937-6.31203 9.15937-8.0452v-4.20854h-34.28487c-15.96684-.12343-26.2397-7.1786-26.3632-15.22377 0-.7431.37113-35.89373 0-36.14147l.00007.00007zm347.79917-40.59788c-9.90174-.24686-17.57571.74388-24.01204 3.46624-1.85657.7423-4.8274.74307-5.07511 3.09427.99081.9908 1.11427 2.59967 1.98084 3.96087 1.48621 2.47462 4.0843 5.8174 6.43627 7.5505 2.5989 1.98003 5.19857 3.96006 7.92093 5.6932 4.8274 2.97084 10.27364 4.70313 14.97681 7.67397 2.72394 1.73152 5.44549 3.96083 8.16863 5.8174 1.36197.98926 2.22773 2.5989 3.96083 3.21776v-.37116c-.86653-1.11424-1.11423-2.72314-1.98003-3.96084-1.23692-1.23773-2.4754-2.35197-3.71313-3.5897-3.58967-4.82663-8.04517-9.03513-12.87251-12.5006-3.96006-2.72313-12.62483-6.43627-14.23376-11.01597.00077 0-.12411-.12343-.24754-.24686 2.72314-.24787 5.94087-1.2377 8.54057-1.9808 4.20854-1.11427 8.04517-.86657 12.37714-1.98003 1.98003-.49541 3.96083-1.11427 5.94086-1.73314v-1.11346c-2.22692-2.22851-3.83659-5.19854-6.18856-7.30284-6.31203-5.44704-13.24374-10.76827-20.42234-15.22377-3.83737-2.47543-8.78824-4.0851-12.8725-6.1886-1.48463-.74387-3.96087-1.11423-4.82663-2.35197-2.22851-2.7231-3.46624-6.31203-5.07511-9.53056-3.58812-6.80821-7.05513-14.3572-10.1494-21.5358-2.22692-4.82741-3.58889-9.65484-6.31203-14.11024-12.74826-21.04113-26.6109-33.7895-47.89974-46.29087-4.5797-2.5989-10.02516-3.71314-15.8426-5.07433-3.09349-.12411-6.18856-.37114-9.28286-.49537-1.98004-.86657-3.96087-3.21777-5.69398-4.33204-7.05355-4.45623-25.24893-14.10949-30.4475-1.36116-3.34196 8.04517 4.95087 15.96606 7.79744 20.05114 2.10504 2.84659 4.8274 6.06433 6.3128 9.2829.86657 2.10268 1.1135 4.332 1.98003 6.55973 1.98084 5.44469 3.83663 11.51057 6.4363 16.5857 1.36194 2.5989 2.84657 5.32204 4.57971 7.6732.98999 1.36116 2.72232 1.9808 3.09426 4.20853-1.73233 2.47544-1.85656 6.18861-2.84737 9.28291-4.45546 13.98599-2.72233 31.31406 3.5897 41.5869 1.98003 3.09352 6.68394 9.9025 12.99604 7.30287 5.5697-2.22777 4.332-9.28291 5.94086-15.47151.37194-1.48617.12411-2.4754.86657-3.46543.00078.12343 0 .24787 0 .24787 1.7331 3.46543 3.46543 6.80747 5.0743 10.27283 3.8374 6.06437 10.52063 12.37717 16.0903 16.58574 2.97161 2.22696 5.32281 6.06436 9.03594 7.4263v-.37194h-.24787c-.7431-1.11427-1.85657-1.60886-2.8466-2.47543-2.22773-2.22773-4.70394-4.95087-6.43627-7.42627-5.19776-6.9317-9.77827-14.6049-13.86253-22.52657-1.98003-3.83659-3.71317-8.04516-5.32204-11.88176-.7431-1.48621-.7431-3.71314-1.9808-4.45547-1.85657 2.72237-4.57893 5.07434-5.9409 8.41634-2.35039 5.322-2.59886 11.88176-3.46543 18.68913-.49463.1241-.24787 0-.4954.24787-3.96006-.99081-5.32204-5.07433-6.80747-8.54057-3.71313-8.78743-4.332-22.89774-1.11346-33.04637.86656-2.59967 4.5797-10.7683 3.09426-13.24373-.74229-2.35197-3.2185-3.71314-4.5797-5.5697-1.60886-2.35197-3.34197-5.32203-4.45543-7.92167-2.97084-6.9309-4.45627-14.6049-7.674-21.53661-1.4854-3.2185-4.0843-6.55973-6.18857-9.52976-2.35119-3.342-4.95087-5.69397-6.80747-9.65484-.61806-1.36116-1.4854-3.58889-.4954-5.07433.24787-.99.74233-1.36116 1.7331-1.60886 1.6089-1.36194 6.18861.37116 7.79744 1.11349 4.5797 1.85576 8.41633 3.58967 12.2537 6.18857 1.73233 1.2377 3.5889 3.5897 5.81663 4.20853h2.59967c3.96087.8658 8.41633.24754 12.12947 1.3612 6.5605 2.10346 12.50059 5.19854 17.82343 8.54054 16.21454 10.27287 29.5809 24.87854 38.61691 42.33004 1.48543 2.84582 2.10349 5.44627 3.46546 8.41633 2.59967 6.0651 5.81741 12.2537 8.4163 18.19457 2.5989 5.81663 5.07433 11.75833 8.78747 16.58574 1.85734 2.59812 9.28286 3.96006 12.62483 5.32203 2.4754 1.11346 6.31281 2.10427 8.54054 3.46543 4.20776 2.59809 8.4163 5.56974 12.37713 8.4163 1.98084 1.48466 8.16863 4.57974 8.54057 7.05436h.00004z"/>
<path fill="#00546b" d="M461.08775 501.57571c-2.10427 0-3.58889.24854-5.07429.61964v.24787h.24786c.99081 1.98003 2.72233 3.342 3.96007 5.0743.9908 1.98003 1.85656 3.96086 2.84737 5.94167.12343-.12411.24686-.24787.24686-.24787 1.73391-1.23851 2.59967-3.21854 2.59967-6.18857-.7431-.86657-.86657-1.7331-1.4854-2.59967-.74233-1.23851-2.35197-1.85657-3.342-2.84737z"/>
<path fill="#f57900" d="M328.90008 701.5918h46.53854c5.4455 0 10.64406-1.11346 14.85179-3.0943 7.05595-3.21773 10.39711-7.54973 10.39711-13.2437v-11.88257c0-4.57893-3.83741-8.9109-11.51057-11.88177-3.96006-1.48463-8.9117-2.35119-13.7391-2.35119h-19.55496c-6.56051 0-9.65481-1.98081-10.52134-6.31281-.12411-.49463-.12411-.99003-.12411-1.48463v-7.30287c0-.37113 0-.86653.12411-1.36116.86656-3.34197 2.59889-4.20854 8.29286-4.8274h47.65281v-10.76831h-45.30164c-6.55974 0-10.02517.37117-13.1195 1.36197-9.53057 2.96926-13.73911 7.67317-13.73911 15.96603v9.40714c0 7.30203 8.16944 13.49063 22.03198 14.97603 1.48543.12343 3.0943.12343 4.70316.12343h16.70917c.61887 0 1.23773 0 1.73314.12411 5.07429.49459 7.30203 1.36116 8.78743 3.21773.99003.98922 1.23773 1.85656 1.23773 2.97083l.00078 9.40556c0 1.11424-.74311 2.60045-2.22851 3.83815-1.48466 1.23773-3.8374 2.1043-6.93174 2.22773-.61806 0-.99.12343-1.60886.12343l-44.68124-.00077v10.76907l.00007.00007zm172.53802-18.68916c0 11.01438 8.29287 17.20376 24.87773 18.44146 1.60886.1241 3.09426.24787 4.70316.24787h42.08389v-10.76908h-42.45431c-9.40633 0-12.99674-2.35042-12.99674-8.04439l.00077-55.69721h-16.21454v55.82148l.00004.00003zm-90.47782.49442v-38.36913c0-9.77747 6.9309-15.71914 20.42234-17.57567 1.48621-.1241 2.97083-.24686 4.332-.24686l30.69517-.00078c1.61044 0 2.97083.12411 4.5797.24754 13.4914 1.85576 20.29887 7.7982 20.29887 17.57567l.00081 38.36913c0 7.92097-2.84741 12.13028-9.53141 14.97684l15.8434 14.3572H478.912l-12.87251-11.6348-12.99603.74229h-17.32881c-2.84582 0-6.06514-.37036-9.40633-1.23692-10.27367-2.8466-15.34797-8.29287-15.34797-17.20457l-.00007-.00004zm17.45147-.86653c0 .49459.24753.9908.37194 1.60967.86576 4.45543 5.07429 6.9309 11.51056 6.9309h14.60487l-13.36717-12.13027h18.68997l11.75753 10.64487c2.22931-1.23851 3.58967-2.97007 4.08507-5.1978.12343-.49537.12343-1.11423.12343-1.60964v-36.7603c0-.49463 0-.99003-.12343-1.48463-.86653-4.20857-5.0751-6.56054-11.3871-6.56054h-24.38317c-7.1786 0-11.88257 3.0943-11.88257 8.04517v36.51264l.00007-.00007zm152.11655 19.18436h2.35197v-9.03513h3.0943v-1.85734h-8.6648v1.85734h3.21853v9.03513zm17.94771 0h2.22693v-10.8925h-3.3412l-2.72394 7.42627-2.96925-7.42627h-3.21851v10.8925h2.10427v-8.29286h.12343l3.09427 8.29286h1.60967l3.0943-8.29286v8.29286z"/>
<path fill="none" d="M-64.879845 115.85568h850v850h-850z"/>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

+52
View File
@@ -0,0 +1,52 @@
kubectl create secret generic alertmanager-smtp -n monitoring --from-literal=password='pX8XpxrNT8'
cat > blackbox-email-alert.yaml <<'EOF'
apiVersion: monitoring.coreos.com/v1alpha1
kind: AlertmanagerConfig
metadata:
name: blackbox-email-routing
namespace: monitoring
labels:
release: kube-prometheus-stack
spec:
route:
groupBy: ["alertname", "instance"]
groupWait: 30s
groupInterval: 5m
repeatInterval: 1h
receiver: "email-default"
routes:
- matchers:
- name: alertname
value: BlackboxHttpProbeFailed
matchType: "="
- name: instance
value: "https://idcidp.pigreco66.it"
matchType: "="
receiver: "email-servizio-idcidp"
receivers:
- name: "email-default"
emailConfigs: []
- name: "email-servizio-idcidp"
emailConfigs:
- to: "alessandro.barucci66@gmail.com"
from: "alert@italiadatacenter.com"
smarthost: "mail.tophost.it:587"
authUsername: "italiadatacenter.com62374"
authPassword:
name: alertmanager-smtp
key: password
requireTLS: true
sendResolved: true
headers:
- key: subject
value: "[ALERT] Servizio IDCIDP non raggiungibile"
html: |
<p>Il servizio <b>{{ .CommonLabels.instance }}</b> non risponde.</p>
<p>{{ .CommonAnnotations.description }}</p>
EOF
kubectl apply -f blackbox-email-alert.yaml
+88
View File
@@ -0,0 +1,88 @@
# MySQL Database Operator
A small Kubernetes controller that provides a declarative `MySQLDatabase` CRD on top of an existing MySQL service, including Oracle MySQL Operator / InnoDB Cluster deployments.
## Scope
This MVP manages:
- `CREATE DATABASE IF NOT EXISTS`
- optional application user creation
- application credentials Secret generation
- database-scoped grants
- `Retain` or `Delete` deletion policy
- status/conditions
It does **not** create or manage the MySQL/InnoDBCluster itself. The Oracle MySQL Operator remains responsible for the MySQL infrastructure.
## Important production notes
1. `adminSecretRef` must contain `username` and `password`.
2. The controller currently connects to the host in `spec.connection.host`; if omitted it uses `spec.clusterRef.name`.
3. The generated application Secret contains `username`, `password`, and `database`.
4. The user is created as `'user'@'%'` because Kubernetes workloads normally connect over the cluster network. Tighten this later if your network model supports it.
5. The controller intentionally supports a small allow-list of privileges; do not accept arbitrary SQL from the CRD.
6. For production, add TLS support, NetworkPolicy, Secret rotation policy, metrics, admission validation, and tests before broad multi-team rollout.
## Build
```bash
go mod tidy
go build ./cmd
docker build -t harbor.example.com/platform/mysql-database-operator:0.1.0 .
docker push harbor.example.com/platform/mysql-database-operator:0.1.0
```
## Deploy on RKE2
```bash
kubectl create namespace mysql-database-operator-system
kubectl apply -f config/crd/mysqldatabase-crd.yaml
kubectl apply -f deploy/rbac.yaml
sed 's#ghcr.io/example/mysql-database-operator:0.1.0#harbor.example.com/platform/mysql-database-operator:0.1.0#' deploy/deployment.yaml | kubectl apply -f -
```
Or Helm:
```bash
helm upgrade --install mysql-database-operator ./helm/mysql-database-operator \
--namespace mysql-database-operator-system \
--create-namespace \
--set image.repository=harbor.example.com/platform/mysql-database-operator \
--set image.tag=0.1.0
```
Then create the admin Secret and database:
```bash
kubectl create namespace team-a
kubectl apply -f examples/admin-secret.yaml
kubectl apply -f examples/mysql-database.yaml
kubectl get mysqldatabase -n team-a
kubectl describe mysqldatabase orders -n team-a
kubectl get secret orders-db-credentials -n team-a
```
## Oracle MySQL Operator integration
Example architecture:
```text
InnoDBCluster mysql-prod
|
| Service mysql-prod:3306
v
MySQLDatabase orders
|
+-- CREATE DATABASE orders
+-- CREATE USER orders-app
+-- GRANT SELECT, INSERT, UPDATE, DELETE
+-- Secret orders-db-credentials
```
Use the actual Router/primary Service exposed by your Oracle MySQL Operator installation in `spec.connection.host` if it is not the same as `clusterRef.name`.
## Verification
The source tree was formatted with `gofmt`. Full `go test ./...` could not be executed in this environment because external Go module downloads are disabled; run `go mod tidy && go test ./...` in a network-enabled build environment before publishing the image.
+23
View File
@@ -0,0 +1,23 @@
bcdb6dd502904deb1982acff76dbcecef61fe64c1edce33dbc535fe714809d0f ./Dockerfile
811616c4bfe6df79795d4cfe845926b9e1af47501421d092f67bf511f4cdd673 ./README.md
ffe83a66f937391f9c6b79450f5161095bdba5c794838b2bc8965c715c5849ca ./api/v1alpha1/groupversion_info.go
12f4d95346545e4dd390dcd5718ae0e5104ffd3e6153b9ead6bfaf78e7293d09 ./api/v1alpha1/mysqldatabase_types.go
89e9b851001dcb3a3bf88de546fbde273d9cab9951b389d9a07bf106adc570f5 ./api/v1alpha1/zz_generated.deepcopy.go
08be703122880eb86372acdbe9d560280f9c2428cf2279cc05061bbf79cb8836 ./cmd/main.go
78813f202aa5a135a25f1c1524fdc8e09174f13f601985923a00cf6d9fcc6f28 ./config/crd/mysqldatabase-crd.yaml
c9f3d6c121d19bf780f6a35b69eb242202164c38fc201ad67f6789e363620bf4 ./controllers/mysqldatabase_controller.go
b164fdebc25270c033e6b5608a6b7eb005007757913ff3f7dea692f2ff306777 ./controllers/setup.go
75d21caeb9f41976855fa8421e07f31cee39877844ca30d9f0a3104863090458 ./deploy/deployment.yaml
18f1896539466ce1d0707ca42877d94e97b6e50af72924ecb236cbc276dd4690 ./deploy/namespace.yaml
0de7361b72bcd016c49d44e1bc1a292d6ceba4ab2836218732bd133380151472 ./deploy/rbac.yaml
f31d3c24dc3f666ddd62af0bc8dde2b2623edfa370099e4b759e3cfc87a40cf5 ./examples/admin-secret.yaml
9f954df207eebfa0f1551bdeb05ec97f33ea7184a8c69aad7526b25ffb98cb99 ./examples/mysql-database.yaml
a17f89658ffb2d16a6c64e6bae3a99f8b3fc5130a6c00fdeb6bbd231cde0488f ./go.mod
3266515bc31fd4465da3eacc34d98ddbb18e67444d855438c683a8875f7bce03 ./helm/mysql-database-operator/Chart.yaml
dcb57bf001e957e3abad642ca56c76ba2b65b6c6419dc9148b95be3293f29a4b ./helm/mysql-database-operator/templates/_helpers.tpl
78813f202aa5a135a25f1c1524fdc8e09174f13f601985923a00cf6d9fcc6f28 ./helm/mysql-database-operator/templates/crd.yaml
55159600ff11ee14ab542d6ad647129fb1321fc591e17693340d481af4467ab2 ./helm/mysql-database-operator/templates/deployment.yaml
dd8a29aa027dfc40a77f295fd4d70ab950c14bcc8a3c8bf7efc98f2ff77ee75d ./helm/mysql-database-operator/templates/namespace.yaml
e06e41e699589b14609bb5f32555c01851ba4a7e514d647f3191623832e09e01 ./helm/mysql-database-operator/templates/rbac.yaml
107ddf20ea52d35fbd2dfbbbcadc86e8848b6013566e4fd4229326d9087cce7e ./helm/mysql-database-operator/templates/serviceaccount.yaml
8d6267287c8b41e1c30b11d13c73273aaf48d2a69f717ea0de1f7dda72a26c78 ./helm/mysql-database-operator/values.yaml
@@ -0,0 +1,15 @@
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/scheme"
)
var GroupVersion = schema.GroupVersion{Group: "platform.example.com", Version: "v1alpha1"}
var SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
var AddToScheme = SchemeBuilder.AddToScheme
func init() { SchemeBuilder.Register(&MySQLDatabase{}, &MySQLDatabaseList{}) }
var _ = metav1.Now
@@ -0,0 +1,68 @@
package v1alpha1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// MySQLDatabaseSpec defines the desired state of a logical MySQL database.
type MySQLDatabaseSpec struct {
ClusterRef ClusterReference `json:"clusterRef"`
DatabaseName string `json:"databaseName"`
AdminSecretRef SecretReference `json:"adminSecretRef"`
Connection ConnectionSpec `json:"connection,omitempty"`
User *UserSpec `json:"user,omitempty"`
Credentials *CredentialsSpec `json:"credentials,omitempty"`
Privileges []string `json:"privileges,omitempty"`
Charset string `json:"charset,omitempty"`
Collation string `json:"collation,omitempty"`
DeletionPolicy string `json:"deletionPolicy,omitempty"`
}
type ClusterReference struct {
Name string `json:"name"`
}
type SecretReference struct {
Name string `json:"name"`
}
type ConnectionSpec struct {
Host string `json:"host,omitempty"`
Port int32 `json:"port,omitempty"`
}
type UserSpec struct {
Name string `json:"name"`
Create bool `json:"create,omitempty"`
}
type CredentialsSpec struct {
SecretName string `json:"secretName"`
}
type MySQLDatabaseStatus struct {
Phase string `json:"phase,omitempty"`
Message string `json:"message,omitempty"`
DatabaseName string `json:"databaseName,omitempty"`
UserName string `json:"userName,omitempty"`
SecretName string `json:"secretName,omitempty"`
Conditions []metav1.Condition `json:"conditions,omitempty"`
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Database",type="string",JSONPath=".status.databaseName"
// +kubebuilder:printcolumn:name="User",type="string",JSONPath=".status.userName"
// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
type MySQLDatabase struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec MySQLDatabaseSpec `json:"spec"`
Status MySQLDatabaseStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
type MySQLDatabaseList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []MySQLDatabase `json:"items"`
}
@@ -0,0 +1,37 @@
// Code generated by controller-gen style deepcopy implementation. DO NOT EDIT.
package v1alpha1
func (in *MySQLDatabase) DeepCopyInto(out *MySQLDatabase) {
*out = *in
out.ObjectMeta = *in.ObjectMeta.DeepCopy()
out.Status = in.Status
if in.Status.Conditions != nil {
out.Status.Conditions = make([]metav1.Condition, len(in.Status.Conditions))
copy(out.Status.Conditions, in.Status.Conditions)
}
}
func (in *MySQLDatabase) DeepCopy() *MySQLDatabase {
if in == nil {
return nil
}
out := new(MySQLDatabase)
in.DeepCopyInto(out)
return out
}
func (in *MySQLDatabaseList) DeepCopyInto(out *MySQLDatabaseList) {
*out = *in
if in.Items != nil {
out.Items = make([]MySQLDatabase, len(in.Items))
for i := range in.Items {
in.Items[i].DeepCopyInto(&out.Items[i])
}
}
}
func (in *MySQLDatabaseList) DeepCopy() *MySQLDatabaseList {
if in == nil {
return nil
}
out := new(MySQLDatabaseList)
in.DeepCopyInto(out)
return out
}
@@ -0,0 +1,45 @@
package main
import (
"flag"
"github.com/example/mysql-database-operator/api/v1alpha1"
"github.com/example/mysql-database-operator/controllers"
"k8s.io/apimachinery/pkg/runtime"
"os"
"sigs.k8s.io/controller-runtime/pkg/client/config"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/metrics/server"
)
func main() {
var metricsAddr, probeAddr string
var leader bool
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "metrics address")
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "health probe address")
flag.BoolVar(&leader, "leader-elect", true, "enable leader election")
var zo zap.Options
zo.BindFlags(flag.CommandLine)
flag.Parse()
controller.SetLogger(zap.New(zap.UseFlagOptions(&zo)))
scheme := runtime.NewScheme()
_ = v1alpha1.AddToScheme(scheme)
mgr, err := manager.New(config.GetConfigOrDie(), manager.Options{Scheme: scheme, Metrics: server.Options{BindAddress: metricsAddr}, HealthProbeBindAddress: probeAddr, LeaderElection: leader, LeaderElectionID: "mysql-database-operator"})
if err != nil {
os.Exit(1)
}
if err := (&controllers.MySQLDatabaseReconciler{Client: mgr.GetClient(), Scheme: scheme}).SetupWithManager(mgr); err != nil {
os.Exit(1)
}
if err := mgr.AddHealthzCheck("health", healthz.Ping); err != nil {
os.Exit(1)
}
if err := mgr.AddReadyzCheck("ready", healthz.Ping); err != nil {
os.Exit(1)
}
if err := mgr.Start(controller.SetupSignalHandler()); err != nil {
os.Exit(1)
}
}
@@ -0,0 +1,93 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: mysqldatabases.platform.example.com
spec:
group: platform.example.com
scope: Namespaced
names:
plural: mysqldatabases
singular: mysqldatabase
kind: MySQLDatabase
shortNames: [mysqldb]
versions:
- name: v1alpha1
served: true
storage: true
subresources:
status: {}
additionalPrinterColumns:
- name: Database
type: string
jsonPath: .status.databaseName
- name: User
type: string
jsonPath: .status.userName
- name: Phase
type: string
jsonPath: .status.phase
schema:
openAPIV3Schema:
type: object
required: [spec]
properties:
spec:
type: object
required: [clusterRef,databaseName,adminSecretRef]
properties:
clusterRef:
type: object
required: [name]
properties: {name: {type: string}}
databaseName:
type: string
minLength: 1
maxLength: 64
pattern: '^[A-Za-z0-9_$-]+$'
adminSecretRef:
type: object
required: [name]
properties: {name: {type: string}}
connection:
type: object
properties:
host: {type: string}
port: {type: integer, minimum: 1, maximum: 65535}
user:
type: object
properties:
name: {type: string, maxLength: 32}
create: {type: boolean}
credentials:
type: object
properties:
secretName: {type: string}
privileges:
type: array
items: {type: string}
charset: {type: string}
collation: {type: string}
deletionPolicy:
type: string
enum: [Retain, Delete]
default: Retain
status:
type: object
properties:
phase: {type: string}
message: {type: string}
databaseName: {type: string}
userName: {type: string}
secretName: {type: string}
observedGeneration: {type: integer, format: int64}
conditions:
type: array
items:
type: object
required: [type,status,reason,message,lastTransitionTime]
properties:
type: {type: string}
status: {type: string}
reason: {type: string}
message: {type: string}
lastTransitionTime: {type: string, format: date-time}
@@ -0,0 +1,242 @@
package controllers
import (
"context"
"crypto/rand"
"database/sql"
"fmt"
"math/big"
"strings"
"time"
platformv1alpha1 "github.com/example/mysql-database-operator/api/v1alpha1"
_ "github.com/go-sql-driver/mysql"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
const finalizer = "platform.example.com/mysqldatabase-finalizer"
type MySQLDatabaseReconciler struct{ client.Client }
func (r *MySQLDatabaseReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
logger := log.FromContext(ctx)
var obj platformv1alpha1.MySQLDatabase
if err := r.Get(ctx, req.NamespacedName, &obj); err != nil {
if apierrors.IsNotFound(err) {
return reconcile.Result{}, nil
}
return reconcile.Result{}, err
}
if obj.Spec.DatabaseName == "" {
return r.setError(ctx, &obj, "InvalidSpec", "databaseName is required")
}
if obj.Spec.AdminSecretRef.Name == "" {
return r.setError(ctx, &obj, "InvalidSpec", "adminSecretRef.name is required")
}
if !obj.DeletionTimestamp.IsZero() {
if !controllerutil.ContainsFinalizer(&obj, finalizer) {
return reconcile.Result{}, nil
}
if strings.EqualFold(obj.Spec.DeletionPolicy, "Delete") {
if err := r.withDB(ctx, &obj, func(db *sql.DB) error {
_, err := db.ExecContext(ctx, "DROP DATABASE IF EXISTS `"+quoteIdent(obj.Spec.DatabaseName)+"`")
return err
}); err != nil {
return reconcile.Result{RequeueAfter: 10 * time.Second}, err
}
}
controllerutil.RemoveFinalizer(&obj, finalizer)
return reconcile.Result{}, r.Update(ctx, &obj)
}
if !controllerutil.ContainsFinalizer(&obj, finalizer) {
controllerutil.AddFinalizer(&obj, finalizer)
return reconcile.Result{}, r.Update(ctx, &obj)
}
if err := r.withDB(ctx, &obj, func(db *sql.DB) error { return ensureDatabase(ctx, db, &obj) }); err != nil {
return r.setError(ctx, &obj, "ConnectionError", err.Error())
}
if obj.Spec.User != nil && obj.Spec.User.Create {
username := obj.Spec.User.Name
if username == "" {
return r.setError(ctx, &obj, "InvalidSpec", "user.name is required when user.create is true")
}
password, err := r.ensureCredentials(ctx, &obj, username)
if err != nil {
return r.setError(ctx, &obj, "CredentialsError", err.Error())
}
if err := r.withDB(ctx, &obj, func(db *sql.DB) error { return ensureUserAndGrants(ctx, db, &obj, password) }); err != nil {
return r.setError(ctx, &obj, "UserError", err.Error())
}
}
obj.Status.Phase = "Ready"
obj.Status.Message = "Database reconciled"
obj.Status.DatabaseName = obj.Spec.DatabaseName
if obj.Spec.User != nil {
obj.Status.UserName = obj.Spec.User.Name
}
if obj.Spec.Credentials != nil {
obj.Status.SecretName = obj.Spec.Credentials.SecretName
}
obj.Status.ObservedGeneration = obj.Generation
obj.Status.Conditions = []metav1.Condition{{Type: "Ready", Status: metav1.ConditionTrue, Reason: "Reconciled", Message: "MySQL database is ready", LastTransitionTime: metav1.Now()}}
if err := r.Status().Update(ctx, &obj); err != nil {
logger.Error(err, "status update failed")
return reconcile.Result{}, err
}
return reconcile.Result{RequeueAfter: 5 * time.Minute}, nil
}
func (r *MySQLDatabaseReconciler) setError(ctx context.Context, obj *platformv1alpha1.MySQLDatabase, reason, msg string) (reconcile.Result, error) {
obj.Status.Phase = "Error"
obj.Status.Message = msg
obj.Status.ObservedGeneration = obj.Generation
obj.Status.Conditions = []metav1.Condition{{Type: "Ready", Status: metav1.ConditionFalse, Reason: reason, Message: msg, LastTransitionTime: metav1.Now()}}
_ = r.Status().Update(ctx, obj)
return reconcile.Result{RequeueAfter: 15 * time.Second}, fmt.Errorf("%s: %s", reason, msg)
}
func (r *MySQLDatabaseReconciler) withDB(ctx context.Context, obj *platformv1alpha1.MySQLDatabase, fn func(*sql.DB) error) error {
var admin corev1.Secret
if err := r.Get(ctx, types.NamespacedName{Name: obj.Spec.AdminSecretRef.Name, Namespace: obj.Namespace}, &admin); err != nil {
return err
}
username, password := string(admin.Data["username"]), string(admin.Data["password"])
if username == "" || password == "" {
return fmt.Errorf("admin secret %q must contain username and password", obj.Spec.AdminSecretRef.Name)
}
host := obj.Spec.Connection.Host
if host == "" {
host = obj.Spec.ClusterRef.Name
}
port := obj.Spec.Connection.Port
if port == 0 {
port = 3306
}
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/?parseTime=true&timeout=5s&readTimeout=10s&writeTimeout=10s", username, password, host, port)
db, err := sql.Open("mysql", dsn)
if err != nil {
return err
}
defer db.Close()
pingCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
defer cancel()
if err := db.PingContext(pingCtx); err != nil {
return err
}
return fn(db)
}
func ensureDatabase(ctx context.Context, db *sql.DB, obj *platformv1alpha1.MySQLDatabase) error {
q := "CREATE DATABASE IF NOT EXISTS `" + quoteIdent(obj.Spec.DatabaseName) + "`"
if obj.Spec.Charset != "" {
q += " CHARACTER SET " + safeSQLWord(obj.Spec.Charset)
}
if obj.Spec.Collation != "" {
q += " COLLATE " + safeSQLWord(obj.Spec.Collation)
}
_, err := db.ExecContext(ctx, q)
return err
}
func ensureUserAndGrants(ctx context.Context, db *sql.DB, obj *platformv1alpha1.MySQLDatabase, password string) error {
u := quoteIdent(obj.Spec.User.Name)
// User is intentionally scoped to localhost wildcard host. For networked K8s apps, '%' is needed.
if _, err := db.ExecContext(ctx, "CREATE USER IF NOT EXISTS '"+quoteString(obj.Spec.User.Name)+"'@'%' IDENTIFIED BY '"+quoteString(password)+"'"); err != nil {
return err
}
if _, err := db.ExecContext(ctx, "ALTER USER '"+quoteString(obj.Spec.User.Name)+"'@'%' IDENTIFIED BY '"+quoteString(password)+"'"); err != nil {
return err
}
grants := obj.Spec.Privileges
if len(grants) == 0 {
grants = []string{"SELECT", "INSERT", "UPDATE", "DELETE"}
}
for _, g := range grants {
if !isAllowedPrivilege(g) {
return fmt.Errorf("unsupported privilege %q", g)
}
}
grant := strings.Join(grants, ", ")
_, err := db.ExecContext(ctx, "GRANT "+grant+" ON `"+quoteIdent(obj.Spec.DatabaseName)+"`.* TO '"+quoteString(obj.Spec.User.Name)+"'@'%'")
_ = u
return err
}
func (r *MySQLDatabaseReconciler) ensureCredentials(ctx context.Context, obj *platformv1alpha1.MySQLDatabase, username string) (string, error) {
if obj.Spec.Credentials == nil || obj.Spec.Credentials.SecretName == "" {
return "", fmt.Errorf("credentials.secretName is required when user.create is true")
}
key := types.NamespacedName{Name: obj.Spec.Credentials.SecretName, Namespace: obj.Namespace}
var s corev1.Secret
err := r.Get(ctx, key, &s)
if err == nil {
p := string(s.Data["password"])
if p == "" {
return "", fmt.Errorf("credentials secret must contain password")
}
if string(s.Data["username"]) == "" {
s.Data["username"] = []byte(username)
if err := r.Update(ctx, &s); err != nil {
return "", err
}
}
return p, nil
}
if !apierrors.IsNotFound(err) {
return "", err
}
p, err := randomPassword(32)
if err != nil {
return "", err
}
s = corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: key.Name, Namespace: key.Namespace}, Type: corev1.SecretTypeOpaque, StringData: map[string]string{"username": username, "password": p, "database": obj.Spec.DatabaseName}}
if err := controllerutil.SetControllerReference(obj, &s, r.Scheme); err != nil {
return "", err
}
if err := r.Create(ctx, &s); err != nil {
return "", err
}
return p, nil
}
func randomPassword(n int) (string, error) {
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%^&*_-"
b := make([]byte, n)
for i := range b {
x, e := rand.Int(rand.Reader, big.NewInt(int64(len(chars))))
if e != nil {
return "", e
}
b[i] = chars[x.Int64()]
}
return string(b), nil
}
func quoteIdent(s string) string { return strings.ReplaceAll(s, "`", "``") }
func quoteString(s string) string {
return strings.ReplaceAll(strings.ReplaceAll(s, "\\", "\\\\"), "'", "\\'")
}
func safeSQLWord(s string) string {
for _, r := range s {
if !(r == '_' || r == '-' || r == '.' || r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9') {
return "utf8mb4"
}
}
return s
}
func isAllowedPrivilege(s string) bool {
switch strings.ToUpper(strings.TrimSpace(s)) {
case "SELECT", "INSERT", "UPDATE", "DELETE", "CREATE", "ALTER", "INDEX", "DROP", "REFERENCES", "EXECUTE", "SHOW VIEW", "TRIGGER":
return true
default:
return false
}
}
@@ -0,0 +1,13 @@
package controllers
import (
platformv1alpha1 "github.com/example/mysql-database-operator/api/v1alpha1"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/manager"
)
func (r *MySQLDatabaseReconciler) SetupWithManager(mgr manager.Manager) error {
r.Client = mgr.GetClient()
return builder.ControllerManagedBy(mgr).For(&platformv1alpha1.MySQLDatabase{}).Owns(&corev1.Secret{}).Complete(r)
}
@@ -0,0 +1,9 @@
apiVersion: v1
kind: Secret
metadata:
name: mysql-prod-admin
namespace: team-a
type: Opaque
stringData:
username: root
password: CHANGE-ME
@@ -0,0 +1,28 @@
apiVersion: platform.example.com/v1alpha1
kind: MySQLDatabase
metadata:
name: orders
namespace: team-a
spec:
clusterRef:
name: mysql-prod
# If omitted, the controller uses clusterRef.name as the Service DNS name.
connection:
host: mysql-prod
port: 3306
databaseName: orders
adminSecretRef:
name: mysql-prod-admin
user:
name: orders-app
create: true
credentials:
secretName: orders-db-credentials
privileges:
- SELECT
- INSERT
- UPDATE
- DELETE
charset: utf8mb4
collation: utf8mb4_0900_ai_ci
deletionPolicy: Retain
@@ -0,0 +1,6 @@
apiVersion: v2
name: mysql-database-operator
description: Declarative MySQL database/user controller for Kubernetes
version: 0.1.0
appVersion: "0.1.0"
type: application
@@ -0,0 +1,2 @@
{{- define "mysql-database-operator.name" -}}mysql-database-operator{{- end }}
{{- define "mysql-database-operator.fullname" -}}{{ include "mysql-database-operator.name" . }}{{- end }}
@@ -0,0 +1,93 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: mysqldatabases.platform.example.com
spec:
group: platform.example.com
scope: Namespaced
names:
plural: mysqldatabases
singular: mysqldatabase
kind: MySQLDatabase
shortNames: [mysqldb]
versions:
- name: v1alpha1
served: true
storage: true
subresources:
status: {}
additionalPrinterColumns:
- name: Database
type: string
jsonPath: .status.databaseName
- name: User
type: string
jsonPath: .status.userName
- name: Phase
type: string
jsonPath: .status.phase
schema:
openAPIV3Schema:
type: object
required: [spec]
properties:
spec:
type: object
required: [clusterRef,databaseName,adminSecretRef]
properties:
clusterRef:
type: object
required: [name]
properties: {name: {type: string}}
databaseName:
type: string
minLength: 1
maxLength: 64
pattern: '^[A-Za-z0-9_$-]+$'
adminSecretRef:
type: object
required: [name]
properties: {name: {type: string}}
connection:
type: object
properties:
host: {type: string}
port: {type: integer, minimum: 1, maximum: 65535}
user:
type: object
properties:
name: {type: string, maxLength: 32}
create: {type: boolean}
credentials:
type: object
properties:
secretName: {type: string}
privileges:
type: array
items: {type: string}
charset: {type: string}
collation: {type: string}
deletionPolicy:
type: string
enum: [Retain, Delete]
default: Retain
status:
type: object
properties:
phase: {type: string}
message: {type: string}
databaseName: {type: string}
userName: {type: string}
secretName: {type: string}
observedGeneration: {type: integer, format: int64}
conditions:
type: array
items:
type: object
required: [type,status,reason,message,lastTransitionTime]
properties:
type: {type: string}
status: {type: string}
reason: {type: string}
message: {type: string}
lastTransitionTime: {type: string, format: date-time}
@@ -0,0 +1,28 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql-database-operator
namespace: mysql-database-operator-system
spec:
replicas: {{ .Values.replicaCount }}
selector: {matchLabels: {app: mysql-database-operator}}
template:
metadata: {labels: {app: mysql-database-operator}}
spec:
serviceAccountName: {{ .Values.serviceAccount.name }}
securityContext: {runAsNonRoot: true}
containers:
- name: manager
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
args: ["--leader-elect={{ .Values.leaderElect }}"]
ports:
- {name: metrics, containerPort: 8080}
- {name: health, containerPort: 8081}
resources: {{- toYaml .Values.resources | nindent 10 }}
securityContext:
allowPrivilegeEscalation: false
capabilities: {drop: [ALL]}
seccompProfile: {type: RuntimeDefault}
readinessProbe: {httpGet: {path: /readyz, port: health}}
livenessProbe: {httpGet: {path: /healthz, port: health}}
@@ -0,0 +1,6 @@
{{- if .Values.serviceAccount.create }}
apiVersion: v1
kind: Namespace
metadata:
name: mysql-database-operator-system
{{- end }}
@@ -0,0 +1,33 @@
{{- if .Values.rbac.create }}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata: {name: mysql-database-operator}
rules:
- apiGroups: [platform.example.com]
resources: [mysqldatabases]
verbs: [get,list,watch,create,update,patch,delete]
- apiGroups: [platform.example.com]
resources: [mysqldatabases/status]
verbs: [get,update,patch]
- apiGroups: [platform.example.com]
resources: [mysqldatabases/finalizers]
verbs: [update]
- apiGroups: [""]
resources: [secrets]
verbs: [get,list,watch,create,update,patch,delete]
- apiGroups: [""]
resources: [events]
verbs: [create,patch]
- apiGroups: [coordination.k8s.io]
resources: [leases]
verbs: [get,list,watch,create,update,patch,delete]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata: {name: mysql-database-operator}
roleRef: {apiGroup: rbac.authorization.k8s.io, kind: ClusterRole, name: mysql-database-operator}
subjects:
- kind: ServiceAccount
name: {{ .Values.serviceAccount.name }}
namespace: mysql-database-operator-system
{{- end }}
@@ -0,0 +1,5 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ .Values.serviceAccount.name }}
namespace: mysql-database-operator-system
@@ -0,0 +1,16 @@
image:
repository: ghcr.io/example/mysql-database-operator
tag: 0.1.0
pullPolicy: IfNotPresent
replicaCount: 1
leaderElect: true
resources:
requests: {cpu: 50m, memory: 64Mi}
limits: {cpu: 500m, memory: 256Mi}
serviceAccount:
create: true
name: mysql-database-operator
rbac:
create: true
crd:
install: true
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+36 -10
View File
@@ -47,8 +47,10 @@ for dir in containers/*/; do
PLATFORM_CONF="$dir/platform.conf"
BUILD_PLATFORM=""
MULTISTAGE=""
if [ -f "$PLATFORM_CONF" ]; then
BUILD_PLATFORM=$(grep -E '^[[:space:]]*platform[[:space:]]*=' "$PLATFORM_CONF" | tail -n 1 | cut -d '=' -f 2- | tr -d '[:space:]')
BUILD_PLATFORM=$(grep -E '^[[:space:]]*platform[[:space:]]*=' "$PLATFORM_CONF" | tail -n 1 | cut -d '=' -f 2- | tr -d '[:space:]' || true)
MULTISTAGE=$(grep -E '^[[:space:]]*multistage[[:space:]]*=' "$PLATFORM_CONF" | tail -n 1 | cut -d '=' -f 2- | tr -d '[:space:]' || true)
if [ -n "$BUILD_PLATFORM" ]; then
echo "platform.conf trovato in $dir: uso platform=$BUILD_PLATFORM"
else
@@ -57,15 +59,39 @@ for dir in containers/*/; do
fi
if [ -f "$DOCKERFILE" ]; then
if [ -n "$BUILD_PLATFORM" ]; then
docker buildx build --platform "$BUILD_PLATFORM" -t "$IMAGE_TAG" -t "$IMAGE_TAG_LATEST" -f "$DOCKERFILE" "$dir" --push
else
docker buildx build \
--cache-from=type=registry,ref="${REGISTRY_URL}/${REPO_NAME}/${CONTAINER_NAME}:cache" \
--cache-to=type=registry,ref="${REGISTRY_URL}/${REPO_NAME}/${CONTAINER_NAME}:cache",mode=max \
-t "$IMAGE_TAG" -t "$IMAGE_TAG_LATEST" \
-f "$DOCKERFILE" "$dir" --push
fi
if [ -n "$MULTISTAGE" ] && ! [[ "$MULTISTAGE" =~ ^[1-9][0-9]*$ ]]; then
echo "Valore multistage non valido in $PLATFORM_CONF: $MULTISTAGE"
exit 1
fi
BUILD_COUNT="${MULTISTAGE:-1}"
for ((target_index = 1; target_index <= BUILD_COUNT; target_index++)); do
BUILD_TARGET=""
if [ -n "$MULTISTAGE" ]; then
BUILD_TARGET=$(grep -E "^[[:space:]]*target${target_index}[[:space:]]*=" "$PLATFORM_CONF" | tail -n 1 | cut -d '=' -f 2- | tr -d '[:space:]' || true)
if [ -z "$BUILD_TARGET" ]; then
echo "target${target_index} non valorizzato in $PLATFORM_CONF"
exit 1
fi
fi
echo "Build $target_index/$BUILD_COUNT${BUILD_TARGET:+ target=$BUILD_TARGET}"
BUILD_TARGET_ARGS=()
if [ -n "$BUILD_TARGET" ]; then
BUILD_TARGET_ARGS=(--target "$BUILD_TARGET")
fi
if [ -n "$BUILD_PLATFORM" ]; then
docker buildx build --platform "$BUILD_PLATFORM" "${BUILD_TARGET_ARGS[@]}" -t "$IMAGE_TAG" -t "$IMAGE_TAG_LATEST" -f "$DOCKERFILE" "$dir" --push
else
docker buildx build \
--cache-from=type=registry,ref="${REGISTRY_URL}/${REPO_NAME}/${CONTAINER_NAME}:cache" \
--cache-to=type=registry,ref="${REGISTRY_URL}/${REPO_NAME}/${CONTAINER_NAME}:cache",mode=max \
"${BUILD_TARGET_ARGS[@]}" \
-t "$IMAGE_TAG" -t "$IMAGE_TAG_LATEST" \
-f "$DOCKERFILE" "$dir" --push
fi
done
echo "Build e push completate: $IMAGE_TAG"
# Cleanup immagini locali dopo il push (buildx --push potrebbe non averle in locale)
+6
View File
@@ -7,6 +7,8 @@ ENVIRONMENT="${1:-dev}"
YAML_DIR="kubernetes"
# Valore di alert_notification_email per l'ambiente (ultima occorrenza in env/<env>/*.env)
ALERT_EMAIL=$(cat "env/$ENVIRONMENT"/*.env 2>/dev/null | grep -E '^[[:space:]]*alert_notification_email[[:space:]]*=' | tail -n 1 | sed -E 's/^[^=]*=//' | tr -d '\r"'"'"'[:space:]' || true)
# ---------------------------------------------------------------------------
@@ -15,6 +17,10 @@ YAML_DIR="kubernetes"
find "$YAML_DIR" -type d | while read DIR; do
if ls "$DIR"/*.yaml 1> /dev/null 2>&1; then
if [ "$(basename "$DIR")" = "monitoring" ]; then
if [ -z "$ALERT_EMAIL" ]; then
echo "alert_notification_email non valorizzata per l'ambiente $ENVIRONMENT: deploy di $DIR saltato."
continue
fi
echo "Deploy delle risorse di monitoring nella directory $DIR con kubeconfig dedicato..."
kubectl --kubeconfig=/root/work/pipeline/kc apply -f "$DIR"
else