--- name: preventivo description: Lavorare sulla pipeline che genera i preventivi di ClientHub (src/lib/proposal/) senza romperla. Da usare quando si modifica il prompt, lo schema Zod, il montaggio o la resa di un preventivo, quando una generazione fallisce con "Contenuto AI non valido", quando una sezione del documento esce vuota, o prima di pubblicare una proposta a un cliente. --- # Preventivo — la catena e come non spezzarla Il preventivo lo genera **l'app**, da `/admin/preventivi/genera`. Questa skill non contiene un prompt di generazione e non ne va aggiunto uno: il prompt vive in `agent.ts` ed e' l'unico. Un secondo prompt qui resterebbe indietro rispetto a quello vero senza che nessuno se ne accorga. ## La catena: quattro file che devono dire la stessa cosa ``` src/lib/proposal/agent.ts il prompt chiede N campi ─┐ src/lib/proposal/schema.ts lo Zod pretende quegli N campi │ se non src/lib/proposal/assemble.ts li impacchetta con prezzi+profilo │ coincidono, src/components/public/proposal/ ProposalDeck + sections/ li resa ─┘ si rompe ``` **Si tocca il prompt, si riapre lo schema. Sempre.** I due fallimenti hanno forma diversa e solo uno si vede subito: | Cosa hai fatto | Cosa succede | |---|---| | Campo aggiunto allo **schema**, non al prompt | `ProposalContentSchema.safeParse` fallisce → **ogni** generazione muore con «Contenuto AI non valido». Rumoroso, si scopre subito | | Campo aggiunto al **prompt**, non allo schema | Zod lo scarta in silenzio. Il documento esce senza quella parte, e te ne accorgi davanti al cliente | | Campo nello schema, nessuna `
` che lo legge | Generazione verde, sezione assente. Il piu' subdolo: niente segnala l'errore | Quando aggiungi un campo, il giro completo e' **quattro file**: prompt in `buildUserPrompt`, schema Zod, eventuale passaggio in `assembleProposal`, e la sezione in `src/components/public/proposal/sections/` piu' la riga in `ProposalDeck.tsx` che la monta. I vincoli di cardinalita' stanno nello schema (`.min(3).max(5)` sui problemi, `.length(5)` sui nodi del diagramma, `.min(4).max(10)` sulla matrice): se li cambi li' e non nel prompt, il modello continua a produrre il numero vecchio e Zod lo rifiuta. ## Prezzi: cosa puo' vedere il cliente Vincolo **LOCKED #2** (`../../CLAUDE.md`): al cliente vanno i totali, mai le righe di prezzo. `PricingSection.tsx` renderizza `tier.publicPrice ?? tier.servicesTotal` e il **nome** dei servizi — corretto. Ma `assembleProposal` mette in `content.offer.tiers[].services[].unitPrice` anche il prezzo unitario, e `ProposalDeck` e' `"use client"`: riceve l'intero `proposal` come prop da un server component, quindi **tutto** l'oggetto finisce serializzato nel payload RSC della pagina, renderizzato o no. E' la stessa trappola del gate OTP annotata in `STATUS.md` — *sparire a schermo non e' sparire*. Prima di toccare la pagina pubblica, e prima di mandare un preventivo a un cliente che potrebbe aprire il sorgente: ```bash curl -s https:///preventivo/ > /tmp/p.html LC_ALL=C grep -c unitPrice /tmp/p.html # atteso a regime: 0 ``` Se e' > 0, la correzione non e' nascondere la sezione: e' una proiezione client-safe in `assemble.ts` o al confine del componente, come gia' fa `src/lib/client-view.ts` per il portale. ## Prima di pubblicare 1. **Preflight sui dati finti** — `./check-profilo.sh` da questa cartella. `profile.ts` e' uno **snapshot**: quello che c'e' dentro al momento della generazione finisce in `proposals.content` e ci resta anche se poi correggi il file. 2. **Le citazioni sono verbatim o non sono.** Il prompt lo impone; verificarlo a campione contro il transcript e' il controllo che nessuna macchina fa al posto tuo. Una citazione inventata in un preventivo e' peggio di un preventivo senza citazioni. 3. **Copy** — italiano, registro di `../../../../brand/voce.md`, e **nessun numero che non stia in `../../../../brand/prove.md` marcato divulgabile** (`../../../../.claude/rules/lingua-e-tono.md`). ⚠️ `brand/` sta **fuori dal repo hub**: su un clone senza il workspace questo passo si salta dichiarandolo, non si finge di averlo fatto. 4. **Stato** — `draft` non e' visibile (la pagina risponde «non ancora disponibile»). Pubblicare = `publishProposal` in `src/app/admin/preventivi/actions.ts`. ## Come si prova davvero - `npm run build` e' **la verifica di riferimento**: non esiste test suite, e il build fa il typecheck. Verde qui significa «i tipi tornano», non «il documento e' giusto». - Per vedere l'output serve una **generazione vera** dall'admin, con un'offerta e almeno un transcript. In locale oggi **non si puo'**: `.env.local` non autentica piu' contro il DB (dal 2026-08-21, vedi `STATUS.md`). Si guarda in produzione. - Un preventivo si legge a schermo prima di mandarlo. «Buildato» non e' «funziona», e «generato» non e' «verificato». ## Note sulla chiamata al modello `agent.ts` usa `max_tokens: 8192` per uno schema che chiede fino a 5 problemi + 5 soluzioni + matrice. Se la risposta viene troncata, il fallimento **non** dice «troncata»: dice «L'AI ha prodotto JSON non valido», e manda a cercare nel posto sbagliato. Se capita, controllare `message.stop_reason === "max_tokens"` prima di dare la colpa al parsing.