b913a2bf90
Refs gitea:Adriano/AI-OS#310 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
250 lines
16 KiB
Markdown
250 lines
16 KiB
Markdown
# 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.
|