Refs gitea:Adriano/AI-OS#310 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
16 KiB
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.
anagrafica.tsespone una funzione che risolve il codice cliente aprendo lei ciò che serve; l'endpoint apre solo longevity.- 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.