Files
InsanityLab-web/.superpowers/sdd/2026-08-22-longevity-interfaccia/progress.md
T
Adriano b913a2bf90 registri dei piani in git (AI-OS#309)
Refs gitea:Adriano/AI-OS#310

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 10:36:26 +02:00

250 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SDD ledger — plan: docs/plans/2026-08-22-longevity-interfaccia.md
Spec: docs/specs/2026-08-21-longevity-design.md
Repo: InsanityLab-web — branch `feat/longevity` (main fermo a ecab678)
Piani precedenti eseguiti: strato dati (2026-08-21), motore (2026-08-22)
## Scansione pre-volo
| Task A | Task B | Produce → Consuma | Esito |
|---|---|---|---|
| 1 guscio | 2, 3 | layout `Longevity.astro`, classi `.lg-*` | OK |
| 2 vista.ts | 3 | il file viene creato da 2 ed esteso da 3 | ⚠️ vedi ruling |
| 2 endpoint | 3 | nessuna dipendenza diretta | OK |
| 3 refertoDi | nessuno | ultimo task del piano | OK |
| Task | Coerenza interna | Esito |
|---|---|---|
| 1 | 5 test, tutti su file e contenuto — nessuno esegue una pagina | ⚠️ vedi ruling |
| 2 | 7 test; tre riguardano il consenso, che è il punto del task | OK |
| 3 | 6 test; quello sul congelamento discrimina cambiando i pesi | OK |
## Ruling: `vista.ts` è creato dal Task 2 ed esteso dal Task 3
Non è un conflitto — i task sono sequenziali e nessuno gira in parallelo — ma va detto:
il Task 3 **aggiunge** `refertoDi` a un file che il Task 2 ha già scritto. Chi esegue il 3
non deve riscrivere il file da zero.
## Ruling: i test del Task 1 controllano il sorgente, non il comportamento
Astro non si esegue in vitest senza montare la pipeline di Vite. I test del guscio quindi
leggono i file e ne verificano proprietà — nessun header pubblico, classi prefissate, niente
semaforo. È una rete debole ma vera: intercetta la regressione che conta (qualcuno che
aggiunge un colore «buono»/«cattivo» ai punteggi). Il comportamento vero delle pagine si
vede col piano successivo, quando ci sarà una sessione reale da mostrare.
Costo se sbagliato: una pagina può rompersi senza che un test se ne accorga. Mitigazione:
`npx tsc --noEmit` deve restare pulito, ed è un vincolo globale del piano.
## Modelli scelti
Implementer: sonnet — c'è React, Astro e integrazione con tre moduli esistenti.
Reviewer: sonnet. Review finale: opus.
## Task 1 — il test anti-semaforo protegge il nome, non l'intento (sesta volta)
Il revisore non si è fidato: ha **simulato tre regressioni** sul CSS vero e ha guardato quali
passano.
| Regressione simulata | Il test se ne accorge? |
|---|---|
| `.lg-score--alto { color: #6f8f6a; }` (verde scritto in esadecimale, lo stesso di Stress Index) | **no** |
| `.lg-score--alto { color: var(--lg-buona-salute); }` (variabile con altro nome) | **no** |
| `--lg-verde-scuro: #6f8f6a;` (contiene una delle tre parole vietate) | sì |
Il test cerca tre nomi letterali. Chiunque reintroducesse il semaforo con un colore scritto
direttamente, o con una variabile battezzata diversamente, passerebbe indisturbato — ed è
proprio il modo più naturale di farlo.
📌 **È la sesta volta in questa sessione** che compare lo stesso difetto, e ancora una volta
il test veniva dal brief che ho scritto io. La forma è sempre quella: **proteggo il nome di
una cosa invece della cosa**. Qui: i tre nomi vietati invece del colore.
**Ruling: rendere il controllo strutturale.** Non un elenco di nomi proibiti, ma il verso
opposto — nelle regole che riguardano i punteggi possono comparire **solo** i colori
ammessi (l'accento del sito, il tratteggio, il fuori range). Tutto il resto è una
regressione, che si chiami come si chiami.
Costo se sbagliato: un test più severo può dare falsi allarmi quando il foglio cresce; è
preferibile a una rete che lascia passare esattamente ciò per cui esiste.
Task 1: implementato (commit 34c66ca) + fix round 1/5 (test reso strutturale; commit 2ea822a)
Task 1: verificato in prima persona — iniettando un verde esadecimale su una classe di punteggio il test nuovo diventa rosso, mentre il vecchio lo lasciava passare
Task 1: l'implementer ha tenuto ANCHE il vecchio controllo sui nomi, notando che intercetta un caso che il nuovo non vede: un token dichiarato e mai usato. Osservazione giusta, i due controlli sono complementari.
Task 1: complete (commits 779aef3..2ea822a, review clean dopo 1 round)
## Task 2 — la garanzia di separazione è stata aggirata al PRIMO endpoint
L'implementer ha aggiunto in `db.ts` due funzioni comode, `getLongevityDb()` e
`getIdentityDb()` (stesso pattern del `getDb()` del sito), e l'endpoint del questionario le
importa **entrambe**. È un secondo punto di giunzione fuori da `anagrafica.ts`, cioè
esattamente ciò che la spec §4 vieta.
⚠️ **E il test che dovrebbe impedirlo non se ne accorge**, perché cerca i nomi
`createIdentityDb` e `createLongevityDb`: i nomi nuovi non sono sorvegliati da nessuno.
📌 Era stato **previsto**. La revisione del Task 4 dello strato dati diceva: «il test rileva
solo la violazione più ingenua — chi chiama direttamente le due factory — non quella che
imita il modulo autorizzato». Allora l'avevo parcheggiata come improbabile. Si è avverata al
**primo endpoint scritto**, tre piani dopo.
Va detto che l'uso è corretto nella sostanza: l'endpoint chiama `codicePerUtente`, che sta
in `anagrafica.ts`, quindi la **logica** di giunzione è al posto giusto. È l'**apertura**
delle due connessioni a essere fuori posto — e il rischio non è oggi, è il giorno in cui
qualcuno, da quell'endpoint che già ha entrambe le connessioni in mano, scrive una query che
le incrocia.
**Ruling: due correzioni, e la seconda vale più della prima.**
1. `anagrafica.ts` espone una funzione che risolve il codice cliente aprendo lei ciò che
serve; l'endpoint apre **solo** longevity.
2. Il test anti-giunzione va esteso ai nomi nuovi — e scritto in modo che **qualunque**
coppia di accessi ai due database venga vista, non tre nomi elencati a mano. È la stessa
lezione del semaforo del Task 1: si protegge la cosa, non il nome della cosa.
Costo se sbagliato: una funzione in più in anagrafica.ts. Il costo di non farlo è che la
garanzia centrale del progetto resta scritta nella spec e assente dal codice.
Task 2: implementato (commit f5ca841) + fix round 1/5 (endpoint apre solo longevity, test anti-giunzione strutturale; commit f0629db)
Task 2: verificato in prima persona con una TERZA coppia di nomi, diversa sia dalla vecchia sia dalla nuova: il test la rileva e la nomina. Protegge la cosa, non i nomi.
Task 2: in revisione
## Task 2 — il test anti-giunzione NON basta, e la cura non è un test migliore
Avevo chiesto al revisore: «il vincolo regge perché il test lo sorveglia, o perché nessuno
l'ha ancora usato male?». Risposta: **la seconda**. E l'ha dimostrata, con un pattern che
non ha niente di malizioso: tre file, una connessione riesportata da un modulo e usata
insieme all'altra in un terzo. **Nessuno dei tre contiene entrambe le parole**, quindi il
test resta verde mentre la giunzione esiste davvero.
📌 **Settima volta** che protegge il nome invece della cosa — e stavolta la mia correzione
precedente (cercare le due stringhe invece dei due nomi di funzione) era ancora dello stesso
tipo, solo un gradino più su. Un controllo testuale file per file non può vedere una
giunzione che attraversa due file.
**Ruling: si smette di sorvegliare e si rende impossibile.**
La connessione a `identity` non deve essere ottenibile da nessuna parte tranne che dentro
`anagrafica.ts`. Oggi `db.ts` la espone a chiunque con `getIdentityDb()`: quella funzione
esce da lì e diventa **privata** al modulo autorizzato. Chi ha bisogno di risolvere un
codice cliente chiama una funzione di `anagrafica.ts` — che è già ciò che fa l'endpoint dopo
il fix precedente.
Così il vincolo non dipende più solo da un guardiano aggirabile: la via ovvia — il singleton
condiviso — non è più raggiungibile.
⚠️ **CORREZIONE (revisione finale):** avevo scritto «non esiste il modo di avere entrambe le
connessioni». **È troppo forte, ed è falso.** `createIdentityDb` resta esportata e senza
argomenti apre lo **stesso file** del singleton: il bypass a tre file funziona ancora, compila
e la suite resta verde. L'attrito è aumentato, l'impossibilità no. Va detto com'è. Il test resta come
rete secondaria, con il suo limite scritto accanto.
⚠️ `createIdentityDb` resta esportata: serve ai test per creare database in memoria, e lì la
giunzione è voluta e visibile. È il **singleton condiviso** il problema, non la fabbrica.
Costo se sbagliato: se un domani servisse leggere identity da un altro punto legittimo,
bisognerà aggiungere una funzione ad `anagrafica.ts` invece di aprire una connessione. È
esattamente l'attrito che vogliamo.
Task 2: fix round 2/5 (getIdentityDb privata in anagrafica.ts, try/catch nell'endpoint, campi testuali; commit 8c98075)
Task 2: RIPROVATO io il bypass a tre file che prima passava indisturbato — ora non compila:
"'db' has no exported member named 'getIdentityDb'". Il test resta verde (e dichiara il
proprio limite), ma la difesa vera è che il codice che aggira la garanzia non esiste più.
📌 La lezione di tutta la sessione, in una riga: quando una regola conta, non la si sorveglia —
si costruisce il codice in modo che violarla non sia un'opzione. Stessa forma della soluzione
nel motore, dove il punteggio insufficiente ha smesso di AVERE un campo valore.
Task 2: complete (commits 2ea822a..8c98075, review clean dopo 2 round)
Task 2: minor (deferred): il test sulla nota di consenso verifica l'esito ma non che vi compaiano versione e data, che il codice scrive comunque
## Task 3 — l'ottava volta, e stavolta l'ha trovata l'implementer
Il test sul congelamento che avevo scritto **non poteva funzionare**, e la ragione è
algebrica, non di programmazione: la mutazione che avevo scelto cambia il peso di
`questionario_lifestyle`, che è l'**unico** sotto-dominio del suo asse. In una media pesata
con un elemento solo il peso si semplifica — `(v × p) / p = v` per il valore, `p / p = 1`
per la copertura — quindi il risultato è identico con qualunque peso, letto o ricalcolato.
Verificato:
peso 1.00 -> valore 72, copertura 1
peso 0.99 -> valore 72, copertura 1
peso 0.50 -> valore 72, copertura 1
Il test passava per un'identità algebrica, non perché il congelamento funzionasse.
L'implementer l'ha scoperto **facendo la prova che gli avevo chiesto**: ha rotto `refertoDi`
perché ricalcolasse invece di leggere, e ha visto il test restare verde. Poi invece di
girarci intorno ha trovato la mutazione che discrimina davvero — il peso di un sotto-dominio
**assente** in un asse a più elementi con copertura parziale — e ha verificato che lì il test
rotto diventa rosso. Ha lasciato il test del brief com'era, perché così diceva l'istruzione,
e ha scritto la falla nel rapporto con la causa esatta.
📌 **Ottava volta**, e la forma è sempre quella: scelgo un caso di prova su cui
l'implementazione giusta e quella sbagliata danno lo stesso risultato. Qui non per
distrazione ma per un motivo che si vede solo facendo il conto — ed è esattamente perché la
prova di rottura va **eseguita**, non immaginata.
**Ruling: si sostituisce la mutazione con quella discriminante.** Il test deve fallire se il
referto ricalcola invece di leggere; oggi non lo fa.
Task 3: implementato (commit 7f01ee0) + fix round 1/5 (mutazione discriminante; commit 6630be7)
Task 3: riprovato io il sabotaggio sul test corretto — forzando il ricalcolo diventa rosso e nomina il problema. Ora protegge.
Task 3: nota tecnica dell'implementer da tenere — `mancano` non poteva venire dalla colonna `asse` del registro, perché lì oggi ci sono solo i 20 campi del questionario e nessuna riga per i sotto-domini fisici. L'ha costruito confrontando i sotto-domini PESATI di un asse con quelli effettivamente misurati.
Task 3: in revisione
## Task 3 — nona volta, ed è colpa della MIA correzione precedente
Il test anti-semaforo, che nel Task 1 avevo fatto irrobustire, filtra le regole CSS su
`lg-score`. Ma questo task ha aggiunto `.lg-radar*` e `.lg-macro*`, che quella stringa non
la contengono: **restano fuori dal controllo**. Il revisore l'ha dimostrato iniettando
`color: #ff0000` in `.lg-radar__voce-nome` — sei test verdi, violazione invisibile.
📌 La forma è sempre la stessa, e stavolta l'ho introdotta io correggendo: sono passato da
**un elenco di nomi vietati** a **un elenco di prefissi da controllare**. Un elenco resta un
elenco, e invecchia appena qualcuno aggiunge una classe.
**Ruling: si gira di nuovo, e stavolta fino in fondo.** Il foglio è interamente prefissato
`lg-` per costruzione — quindi il controllo non deve scegliere *quali* regole guardare, ma
guardarle **tutte** e dichiarare le eccezioni. Una regola nuova entra sotto controllo per il
solo fatto di esistere, invece che per essersi ricordati di aggiungerla a un elenco.
Costo se sbagliato: qualche eccezione da dichiarare quando il foglio crescerà — ed è
l'attrito giusto, perché dichiarare un'eccezione è una decisione visibile, dimenticare di
aggiungere un prefisso non lo è.
Task 3: fix round 2/5 (il controllo guarda tutto il foglio, non un elenco di prefissi; commit a56398e)
Task 3: prova definitiva fatta da me — una classe INVENTATA sul momento, che non esisteva quando il
test è stato scritto, con un verde dentro: il test la vede e la nomina. Non protegge più un
elenco, protegge il foglio.
Task 3: complete (commits 8c98075..a56398e, review clean dopo 2 round)
Task 3: minor (deferred): `mancano` usa la MODEL_VERSION corrente invece di quella congelata sulla riga di score — ininfluente oggi (ne esiste una sola), rilevante quando il modello cambierà
TUTTI E TRE I TASK DELL'INTERFACCIA COMPLETI. Suite 327 verdi, typecheck pulito.
## REVISIONE FINALE — verdetto: NO, e il difetto è nel piano che ho scritto io
**Nessuno chiama il motore.** Il Task 2 salva la compilazione, il Task 3 legge i punteggi, e
**fra i due non c'è niente**: il piano non prevedeva il passo che li calcola. Verificato in
prima persona eseguendo il flusso vero con tutte e venti le risposte compilate:
righe in score dopo la compilazione: 0
assi che mostrano un numero: 0 su 7
Il cliente compilerebbe venti domande e vedrebbe sette assi vuoti, tre trattini e
«copertura dati 0%». Non è un referto parziale che si spiega: **è una pagina che dichiara il
falso sui dati che ha appena ricevuto**.
📌 È il difetto della composizione allo stato puro: ogni task corretto e revisionato, il
prodotto non funziona. Nessuna revisione per task poteva vederlo, perché i test del Task 3
chiamano `salvaScore` dentro la propria fixture — cioè fanno a mano ciò che in produzione non
fa nessuno.
### Altri quattro test che non proteggono niente (mutazioni eseguite dal revisore)
| # | Mutazione | Esito |
|---|---|---|
| 10 | il `client_code` preso dal corpo della richiesta | **verde** — il test elenca `body.`/`corpo.`, ma il file chiama il corpo `data`: **vacuo per costruzione** |
| 11 | rimossa la registrazione del consenso **accettato** | **verde** — è coperto solo il ramo del rifiuto |
| 12 | una pagina nuova con `SELECT` dentro | **verde** — il test elenca due nomi di file |
| 13 | giunzione a tre file via `createIdentityDb()` | **verde**, e compila |
La #10 è la decima ricorrenza della stessa forma, ed è la peggiore: la regola §8 —
*«il client_code si ricava dalla sessione, mai dalla richiesta»* — **oggi non è sorvegliata
da niente**, e un cliente potrebbe scrivere nel fascicolo di un altro.
### Un canale di re-identificazione aperto proprio da questo piano
`profilo_note` accoglie testo libero, e questo è il primo lavoro in cui **una persona ci
scrive dentro**. Un cliente che scrive «sono Mario Rossi, prendo…» mette il proprio nome in
`longevity.db`, che la §7 dichiara **essere** l'export per le statistiche. Lo strato dati ha
costruito la tabella, l'interfaccia ha aperto il rubinetto: nessuna revisione per task poteva
vederlo.