b913a2bf90
Refs gitea:Adriano/AI-OS#310 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
121 lines
5.8 KiB
Markdown
121 lines
5.8 KiB
Markdown
### Task 3: Il radar, e come si mostra ciò che non si sa
|
|
|
|
**Files:**
|
|
- Create: `src/components/longevity/Radar.tsx`, `src/components/longevity/MacroScore.astro`, `src/pages/longevity/io.astro`
|
|
- Modify: `src/lib/longevity/vista.ts` — aggiungere `refertoDi`
|
|
- Test: `tests/longevity/referto.test.ts`
|
|
|
|
**Interfaces:**
|
|
- Consumes: `calcolaSessione`, `leggiScore` dal motore; `codicePerUtente` da `anagrafica.ts`
|
|
- Produces:
|
|
- `type AsseVista = { nome: string; valore: number | null; copertura: number; mancano: string[] }`
|
|
- `refertoDi(db, sessioneId): { assi: AsseVista[]; macro: {...}[]; fitnessAge: number | null }`
|
|
|
|
**Il punto del task.** Un asse senza dati sufficienti non porta un numero: porta **cosa manca per averlo**. `mancano` è l'elenco dei test del registro che alimenterebbero quell'asse e che quella sessione non ha — è ciò che trasforma un buco in un invito, ed è il gesto su cui si regge tutta l'interfaccia.
|
|
|
|
⚠️ **Le pagine non interrogano il database.** Tutto passa da `refertoDi`, che a sua volta usa `leggiScore`. Una `SELECT` scritta dentro una pagina riaprirebbe il buco che il tipo del motore chiude.
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
```ts
|
|
// tests/longevity/referto.test.ts
|
|
import { describe, it, expect } from 'vitest';
|
|
import { readFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { createLongevityDb } from '../../src/lib/longevity/db';
|
|
import { seedRegistro, seedPesi, MODEL_VERSION } from '../../src/lib/longevity/registro';
|
|
import { salvaCompilazione } from '../../src/lib/longevity/questionario';
|
|
import { calcolaSessione, salvaScore } from '../../src/lib/longevity/motore';
|
|
import { refertoDi } from '../../src/lib/longevity/vista';
|
|
|
|
function conCompilazione() {
|
|
const db = createLongevityDb(':memory:');
|
|
seedRegistro(db); seedPesi(db, MODEL_VERSION);
|
|
db.prepare(`INSERT INTO soggetti (client_code, sesso) VALUES ('ISL-0001','F')`).run();
|
|
const s = salvaCompilazione(db, {
|
|
client_code: 'ISL-0001', data: '2026-08-22', eta: 35,
|
|
risposte: { q_attivita: 5, q_alimentazione: 8, q_sigarette: 0, q_alcol_life: 2, q_luce: 2, q_schermi: 10 },
|
|
});
|
|
salvaScore(db, s, calcolaSessione(db, s));
|
|
return { db, s };
|
|
}
|
|
|
|
describe('il referto del cliente', () => {
|
|
it('restituisce i sette assi', () => {
|
|
const { db, s } = conCompilazione();
|
|
expect(refertoDi(db, s).assi.length).toBe(7);
|
|
});
|
|
|
|
it('un asse senza dati NON porta un numero', () => {
|
|
const { db, s } = conCompilazione();
|
|
const forza = refertoDi(db, s).assi.find((a) => a.nome === 'Forza & Struttura')!;
|
|
expect(forza.valore).toBeNull();
|
|
});
|
|
|
|
it('e dice cosa manca per completarlo, invece di lasciare un buco', () => {
|
|
const { db, s } = conCompilazione();
|
|
const forza = refertoDi(db, s).assi.find((a) => a.nome === 'Forza & Struttura')!;
|
|
expect(forza.mancano.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('un asse coperto porta il suo numero', () => {
|
|
const { db, s } = conCompilazione();
|
|
const stile = refertoDi(db, s).assi.find((a) => a.nome === 'Stile di Vita & Sonno')!;
|
|
expect(stile.valore).not.toBeNull();
|
|
expect(stile.mancano).toEqual([]);
|
|
});
|
|
|
|
it('legge i punteggi congelati, non li ricalcola al volo', () => {
|
|
const { db, s } = conCompilazione();
|
|
// se rileggesse le misure invece degli score salvati, cambiare i pesi cambierebbe il referto
|
|
const prima = JSON.stringify(refertoDi(db, s));
|
|
db.prepare(`UPDATE pesi SET peso = 0.99 WHERE model_version = ? AND elemento = 'questionario_lifestyle'`).run(MODEL_VERSION);
|
|
expect(JSON.stringify(refertoDi(db, s))).toBe(prima);
|
|
});
|
|
|
|
it('nessuna pagina interroga il database da sola', () => {
|
|
for (const f of ['src/pages/longevity/io.astro', 'src/pages/longevity/questionario.astro']) {
|
|
const src = readFileSync(join(process.cwd(), f), 'utf8');
|
|
expect(src, `${f} contiene SQL`).not.toMatch(/\bSELECT\b|\.prepare\(/i);
|
|
}
|
|
});
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Run test to verify it fails**
|
|
|
|
Run: `npm test -- tests/longevity/referto.test.ts`
|
|
Expected: FAIL — `refertoDi` non esiste
|
|
|
|
- [ ] **Step 3: Write minimal implementation**
|
|
|
|
`refertoDi` legge i punteggi congelati con `leggiScore` e, per ogni asse insufficiente, calcola `mancano`: i sotto-domini di quell'asse che non hanno nessuna misura nella sessione, tradotti nelle etichette leggibili del registro.
|
|
|
|
Il radar è un'isola React con `recharts`: due serie sovrapposte — una piena con i valori noti, una tratteggiata che chiude il perimetro dove il dato manca. Sotto, l'elenco di ciò che manca.
|
|
|
|
Le carte dei macro-score sono Astro. Un macro insufficiente mostra **un trattino**, non uno zero.
|
|
|
|
⚠️ I numeri usano `font-variant-numeric: tabular-nums`: in una pagina che confronta misure nel tempo, cifre che ballano sono un difetto di lettura.
|
|
|
|
- [ ] **Step 4: Run test to verify it passes**
|
|
|
|
Run: `npm test` — atteso 1 fallito pre-esistente. Poi `npx tsc --noEmit` pulito.
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/components/longevity/Radar.tsx src/components/longevity/MacroScore.astro \
|
|
src/pages/longevity/io.astro src/lib/longevity/vista.ts tests/longevity/referto.test.ts
|
|
git commit -m "longevity: il referto del cliente, e il radar che dice cosa non sa"
|
|
```
|
|
|
|
---
|
|
|
|
## Cosa esiste alla fine di questo piano
|
|
|
|
Un cliente entra, compila il questionario, e vede il proprio referto col radar. Il consenso ai dati sanitari si può **negare davvero**, e il rifiuto resta registrato.
|
|
|
|
**Non esiste ancora**: il gestionale del trainer, l'inserimento dei check-up, l'assorbimento delle sezioni HRV di Stress Index, la progressione nel tempo. Sono i piani successivi.
|
|
|
|
⚠️ **E non si deploya niente.** Il consenso al trattamento dei dati sanitari è una questione aperta che il cliente sta chiudendo con un consulente legale: si può costruire tutto, non si può accendere la raccolta su persone vere finché quella risposta non arriva.
|