chore(claude): architettura base .claude — skill preventivo e audit, hook di guardia, piani nel repo
La cartella aveva dentro solo rules/ e i settings: nessun posto dove mettere una skill, un hook o un piano. Ora ha lo scheletro completo e un .claude/CLAUDE.md che spiega cosa va dove — non duplica CLAUDE.md di progetto, che resta quello che comanda. Due skill locali (le altre restano globali in ~/.claude/skills/): - /preventivo — la catena agent.ts → schema.ts → assemble.ts → ProposalDeck e i tre modi di romperla, di cui uno solo fa rumore. Nessun prompt di generazione qui dentro: quello vive in agent.ts ed e' l'unico. Porta check-profilo.sh. - /audit — guida scripts/audit-fonti.ts, nuovo, che mette in moto le cinque fonti di src/lib/audit/sources/, in prod dal 2026-08-19 ma mai chiamate da nessuno. Provate su giojello.com: 5 su 5, 42,7 s, PageSpeed mobile 58 / desktop 93. Due hook, provati a mano (6 casi il primo, 5 il secondo): - guardia-migration.sh BLOCCA l'SQL distruttivo sulle entita' protette — il vincolo Data Safety (LOCKED) fatto rispettare dalla macchina invece che dalla memoria. - guardia-token.sh AVVISA sulle classi Tailwind grezze. Non blocca: con ~450 occorrenze di debito, bloccare lo renderebbe un ostacolo da disattivare. I tre piani di v2.5 entrano nel repo: stavano solo in ~/.claude/plans/ e STATE.md avvertiva che senza quelli la milestone non era ricostruibile. Passati al setaccio per credenziali prima di committarli. Corretta in rules/memory-discipline.md la chiave della memoria persistente: e' …-Vault-IAMCAVALLI-hub, non quella del workspace. Sedici file stavano nella prima, la regola indicava la seconda. Impeccable resta abilitato solo a livello globale: fuori da settings.json locale. Nessun tocco al prodotto. Build e lint verdi, lint identico al baseline. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
---
|
||||
name: audit
|
||||
description: Far girare le fonti di rilevazione dell'audit sito di ClientHub su un URL e capire cosa e' stato misurato e cosa no. Da usare quando si lavora al motore audit v2.5, quando serve una rilevazione su un sito reale, quando si vuole sapere se una fonte risponde, o prima di scrivere qualsiasi pezzo del documento di audit.
|
||||
---
|
||||
|
||||
# Audit — le rilevazioni, e cosa vale come misura
|
||||
|
||||
Il motore v2.5 **non e' scritto**. Quello che esiste, e che questa skill mette in moto,
|
||||
sono le fonti di rilevazione: nessun LLM, nessun database, nessun browser headless.
|
||||
|
||||
## Due attrezzi, due domande diverse
|
||||
|
||||
| Comando | Cosa fa | Quando |
|
||||
|---|---|---|
|
||||
| `npx tsx scripts/audit-fonti.ts <url>` | Le **cinque fonti** di `src/lib/audit/sources/` — pagine, CrUX, PageSpeed, storico Wayback, segnali. Puramente meccanico | «Cosa si riesce a misurare su questo sito?» |
|
||||
| `npx tsx scripts/spike-audit.ts <url>` | Verifica le **264 voci di checklist** con Sonnet 5, poi sintetizza con Opus 5. Volutamente **isolato** da `src/`: non importa nulla, non tocca il DB | «Cosa c'e' che non va, in parole?» |
|
||||
|
||||
Opzioni comuni: `--profilo=servizi|ecommerce`, `--no-psi` (salta PageSpeed, che da solo
|
||||
vale meta' del tempo). Riferimento misurato il 2026-08-26 su `giojello.com`: cinque fonti
|
||||
su cinque, **42,7 s** in tutto, PageSpeed 34,2 s. Se una fonte tace, **quella e' la notizia** —
|
||||
va riportata, non aggirata.
|
||||
|
||||
`PAGESPEED_API_KEY` viene letta dall'ambiente o da `.env.local`. Senza, PageSpeed e CrUX
|
||||
rispondono a vuoto **senza errore**: lo script lo dice in testa, leggerlo.
|
||||
|
||||
Il grezzo finisce in `audit-fonti-<dominio>.json` (gitignorato). E' li' che ogni numero del
|
||||
documento dovra' essere rintracciabile.
|
||||
|
||||
## Le tre regole che il motore erediterà
|
||||
|
||||
Non sono preferenze. Sono gia' costate, e stanno per esteso in `STATUS.md` § Lezioni operative.
|
||||
|
||||
1. **Un numero entra solo se e' stato misurato.** Rintracciabile nel grezzo, e nel motore in
|
||||
`audit_runs.raw`. Nessun numero dedotto, arrotondato o ricordato.
|
||||
|
||||
2. **Laboratorio e campo non si fondono — la differenza *e'* il risultato.** Su giojello.com
|
||||
Lighthouse dava `server-response-time` **7 ms** e CrUX TTFB p75 **3.553 ms con l'1% nel
|
||||
verde**: il server risponde in fretta al datacenter Google e lento a tutti gli altri. Due
|
||||
numeri con lo stesso nome verrebbero fusi in uno solo — per questo il campo si chiama
|
||||
`risposta_server_ms` e non `ttfb_ms`.
|
||||
|
||||
3. **Un punteggio va sempre con la sua data.** Performance mobile 52, poi 64 sullo stesso sito
|
||||
mezz'ora dopo. Mai presentato come una costante del sito.
|
||||
|
||||
E una regola di metodo che vale per ogni fonte nuova: **ogni estrazione da un'API di terzi va
|
||||
vista funzionare, non dedotta dai docs.** Scrivendo `sources/`, due estrazioni prese dalla
|
||||
documentazione hanno restituito valori vuoti *senza errore* — `largest-contentful-paint-element`
|
||||
non esiste piu' e `configSettings.screenEmulation` non esiste affatto nelle risposte pubbliche.
|
||||
Trovate solo perche' le fonti sono state fatte girare su un sito vero.
|
||||
|
||||
## Perche' niente browser headless
|
||||
|
||||
Misurato il 2026-08-17 sul VPS: **1,5 GB di RAM liberi su 3,8**, Chromium ne prende 500 MB–1 GB
|
||||
→ OOM kill sui servizi con dati reali. Verificato il 2026-08-18 che non serve comunque: 153 voci
|
||||
si verificano sul DOM renderizzato che PageSpeed restituisce, e lo screenshot buono e'
|
||||
`final-screenshot`, non `fullPageScreenshot`.
|
||||
|
||||
## Lo stato di v2.5, per non ripartire dal posto sbagliato
|
||||
|
||||
**In pausa dal 2026-08-19** per scelta: prima le modifiche all'hub, poi il motore.
|
||||
|
||||
| Pezzo | Dove | Stato |
|
||||
|---|---|---|
|
||||
| Schema, 7 tabelle + rubrica 264 voci | `0017_audits.sql`, `checklist_items` | in produzione |
|
||||
| Le cinque fonti | `src/lib/audit/sources/` | **in prod ma inerti**: nessuna route le chiama |
|
||||
| Agent, sintetizzatore, pipeline, editor, pagina | `src/lib/audit/`, `src/app/{admin/audit,audit}` | **da scrivere** |
|
||||
|
||||
I piani stanno in [`../../plans/`](../../plans/): `v2.5-audit-documento.md` (i tre livelli
|
||||
Radiografia / Prima-Dopo / Rotta come configurazioni di **un unico** documento) e
|
||||
`v2.5-audit-motore.md` (raccolta in parallelo → quattro sub-agent → sintetizzatore che
|
||||
**incrocia** le osservazioni in massimo 10 finding).
|
||||
|
||||
## L'agente, quando lo progetteremo
|
||||
|
||||
Va in `.claude/agents/audit-*.md`, non qui. Non esiste ancora e non va abbozzato: il design
|
||||
dei quattro sub-agent cambiera' scrivendo il motore. Quando nascera' eredita le tre regole
|
||||
qui sopra — sono il perimetro, non il contorno.
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
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 `<Section>` 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://<host>/preventivo/<slug> > /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.
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
# Preflight: src/lib/proposal/profile.ts contiene dati placeholder?
|
||||
#
|
||||
# profile.ts e' uno SNAPSHOT: finisce dentro proposals.content al momento della
|
||||
# generazione e ci resta. Un placeholder spedito una volta non si corregge piu'
|
||||
# modificando il file — va rigenerato il preventivo.
|
||||
#
|
||||
# exit 0 = pulito · exit 1 = placeholder trovati
|
||||
set -uo pipefail
|
||||
|
||||
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)
|
||||
F="$ROOT/src/lib/proposal/profile.ts"
|
||||
[ -f "$F" ] || { echo "profile.ts non trovato in $F" >&2; exit 1; }
|
||||
|
||||
# Ogni riga: etichetta|regex. Sono i segni concreti di "da riempire".
|
||||
PATTERNS=(
|
||||
'testimonianza segnaposto|→ Aggiungere'
|
||||
'nome cliente finto|"Cliente [0-9]"'
|
||||
'ruolo generico|role: "Professione"'
|
||||
'campo da compilare|// → '
|
||||
'dominio sbagliato (il brand e iamcavalli.net)|iamcavalli\.com'
|
||||
)
|
||||
|
||||
TROVATI=0
|
||||
for p in "${PATTERNS[@]}"; do
|
||||
ETICHETTA="${p%%|*}"; RE="${p#*|}"
|
||||
N=$(grep -cE "$RE" "$F" || true)
|
||||
if [ "$N" -gt 0 ]; then
|
||||
printf ' ✗ %-48s %s occorrenz%s\n' "$ETICHETTA" "$N" "$([ "$N" = 1 ] && echo a || echo e)"
|
||||
TROVATI=$((TROVATI + N))
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$TROVATI" -gt 0 ]; then
|
||||
cat >&2 <<'MSG'
|
||||
|
||||
BLOCCO — profile.ts spedisce dati placeholder in ogni preventivo generato.
|
||||
|
||||
Non si riempiono a occhio: i numeri divulgabili stanno in brand/prove.md, che oggi
|
||||
e' ancora `stato: scheletro-intervista`. Finche' non sono verificati, la mossa giusta
|
||||
non e' inventare — e' non renderizzare il blocco.
|
||||
|
||||
Regola: ../../../.claude/rules/lingua-e-tono.md — «un numero affermato con sicurezza e
|
||||
mai verificato e' il modo piu' veloce per bruciare la credibilita' che il tono costruisce».
|
||||
MSG
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo " ✓ profile.ts pulito"
|
||||
Reference in New Issue
Block a user