Definito in src/schema.js (BU.schema). Questo documento
spiega le scelte, non ripete il codice: se i due divergono, il codice ha
ragione.
{
id, nome, stato,
creata, modificata, campiModificatiIl,
campi: { identita: {...}, mercato: {...}, offerta: {...}, risorse: {...}, economia: {...}, pilota: {...}, test: {...} },
leve: [ ... ],
materiali: { <idGeneratore>: {...} },
risultati: { ... },
consegna: { <chiaveOutput>: { selezionato, nota } },
lancio: { <chiaveVerifica>: { selezionato, nota } },
decisione: null | 'continua' | 'modifica' | 'ferma',
noteDecisione: { motivazione, data }
}
stato è uno tra: idea, in_definizione, pronta_per_il_test,
test_attivo, da_modificare, validata, in_pausa, archiviata,
in_sviluppo. Cambia manualmente dall'header, o automaticamente quando si
registra una decisione in VALIDAZIONE (vedi sotto).
Ogni campo (BU.schema.CAMPI) appartiene a una sezione (identita,
mercato, offerta, risorse, economia, pilota, test) e ha un tipo:
testo— stringa libera.lista— array di stringhe, una voce per riga in COMPILA.durata—{ testo, dataFine }(usato datest.durata_testepilota.durata_pilota).scelta— stringa vincolata a un elenco di opzioni (opzioni+etichetteOpzioninella definizione del campo); in COMPILA è un<select>con "— non deciso —" come default. Usato oggi solo daidentita.apertura.
Ogni campo, qualunque il tipo, ha sempre le stesse due proprietà:
{ valore, stato }
stato è uno tra ipotesi | generato_da_ia | mandatorio:
ipotesi— non ancora verificato, può cambiare. Stato di default per ogni campo nuovo.generato_da_ia— indica provenienza, non affidabilità: il testo viene da una sessione assistita da IA (es. condensazione di un'intervista) e non è stato ancora riscritto/confermato a mano. Non dice se il contenuto è giusto o sbagliato, solo da dove viene.mandatorio— deciso, non negoziabile: non va cambiato senza una decisione esplicita a monte (es. un vincolo di prodotto o di prezzo già fissato altrove). Il valore enum restamandatorio(dati già salvati), ma l'etichetta visibile è "Confermato" — "mandatorio" in italiano suona come "obbligatorio", che non è il senso qui.
I campi con critico: true nella definizione sono quelli la cui assenza
finisce nella sezione "Cosa fermerebbe questa business unit" del generatore
BU One-Page.
Alcuni generatori — quelli "interni" (letti da chi lavora sulla BU, mai
spediti così a un cliente o pubblicati) — riportano lo stato accanto al
valore del campo, tramite render.testoCampoConStato(): es. "600€ al mese
🔒 Confermato" oppure "risolve X per Y 🤖 Generato da IA". Ogni stato ha
un'icona (💭 ipotesi, 🤖 generato da IA, 🔒 confermato) riconoscibile a colpo
d'occhio anche nel markdown grezzo, non renderizzato, dove il colore non è
disponibile; il badge `testo` (inline code) lo separa visivamente dalla
prosa circostante. Ogni materiale interno apre con una riga di legenda
(render.legendaStatiCampo()) che spiega le tre icone una volta sola, prima
di incontrarle nel testo. render.testoCampo() (senza stato) resta l'helper
per i materiali esterni (landing, presentazione commerciale, template
proposta economica, script discovery call): un cliente non deve mai vedere
un'annotazione interna come "🤖 Generato da IA" nel testo che riceve.
identita.apertura vale perdita o risultato e decide da quale lato
raccontare le leve. Cambia due cose, in modo verificabile:
- il blocco problema in landing: apre sul sintomo di oggi oppure sullo stato desiderato
- la slide problema della presentazione commerciale: idem
Il contenuto non cambia mai: cambia solo l'ordine in cui i due lati vengono raccontati. Un test verifica che landing e presentazione generate siano effettivamente diverse fra le due impostazioni — se qualcuno rende il campo inerte, la suite diventa rossa.
Come ogni altro campo ha stato, e nasce ipotesi. Non è una decisione da
prendere a tavolino: è una delle poche domande a cui un test di campagna
risponde bene e in fretta, mettendo in gara due angoli sullo stesso pubblico.
Diventa mandatorio quando il test lo dice.
Se il campo è vuoto i generatori assumono perdita e lo dichiarano nel testo
invece di nasconderlo.
Da 3 a 5 per business unit (limite morbido in UI, non imposto al salvataggio: una BU con 1 o 6 leve resta salvabile, l'interfaccia segnala solo quando sono meno di 3). Struttura:
{ id, fatto_osservabile, come_lo_chiama_lui, come_lo_chiami_tu, come_lo_elimini, stato }
Sono l'unità grezza da cui i generatori derivano blocco problema, riga di
contrasto, angolo di campagna, FAQ e ipotesi da testare. Non hanno una casa
in una sezione di campi perché non sono un "campo" nel senso sopra — sono
una lista propria (bu.leve).
stato usa lo stesso enum di campi (ipotesi | generato_da_ia | mandatorio, vedi sopra) — il concetto vale per una leva tanto quanto per
un campo: una leva è di per sé un'ipotesi finché il ciclo
ipotesi→materiali→test→decisione non la conferma. È uno stato per
l'intera leva, non uno per ciascuno dei suoi 4 campi: nascono e si
validano insieme (stessa intervista, stesso insight), separarli avrebbe
aggiunto granularità senza un uso reale — lo stesso ragionamento che tiene
decisione/noteDecisione a livello di BU e non di singolo risultato.
Come per i campi, compare come badge solo nei materiali interni
(render.badgeStatoLeva), mai in quelli destinati a un cliente.
C'era, nello schema v1, e non veniva letto da nessun generatore: cambiarlo produceva materiale identico byte per byte. Era decorativo.
Ma il difetto vero era di modello, non di implementazione: la leva contiene
già entrambi i lati. fatto_osservabile è la perdita che il cliente subisce,
come_lo_elimini è il risultato che otterrebbe. La tabella di contrasto li
mette già affiancati nelle sue due colonne. Un campo che sceglie fra i due
duplicava ciò che la struttura esprime da sola.
Quello che invece mancava è una decisione per business unit, non per leva:
da quale lato apre la comunicazione. È diventata identita.apertura.
costo_erogazione, capacita_erogazione, dimensione_mercato — tutti e
tre critici. Rispondono a una domanda che il resto dello schema non fa:
non "il mercato vuole questo servizio" ma "vale la pena costruirlo". Una BU
può avere offerta chiara, differenziazione netta e un test che converte, ed
essere comunque un cattivo investimento — perché il costo di erogarla
mangia il margine, perché il team regge un solo cliente alla volta, o
perché il mercato indirizzabile è troppo piccolo. Sezione a sé anziché
sparsi in Mercato/Offerta/Risorse (dove concettualmente potrebbero stare
uno alla volta) perché nell'output finiscono sempre insieme: alimentano il
generatore "Dimensionamento", che li mette fianco a fianco.
mercato.concorrenti_diretti e mercato.sinergia_altre_bu restano invece
in Mercato, e facoltativi: arricchiscono il quadro competitivo ma non sono
condizioni per procedere.
Sezione separata da offerta: servizio_pilota, prezzo_pilota,
durata_pilota, criteri_successo_pilota. Un pilota non è l'offerta
standard con uno sconto — è un servizio volutamente più piccolo, per
abbassare la soglia d'ingresso del primo cliente. Per questo ha un
servizio_pilota proprio, non solo un prezzo diverso.
Non va confuso con il "prezzo provvisorio" dell'offerta standard: quello
è semplicemente offerta.prezzo finché il suo stato resta ipotesi —
non serve un campo a parte, lo stato del campo lo dice già.
Nessun campo di questa sezione è critico: il pilota è un percorso opzionale, non una condizione per cui la business unit si ferma se manca.
Uno per generatore registrato, indicizzato per id del generatore:
{ stato: 'bozza' | 'da_revisionare' | 'approvato' | 'pubblicabile',
testo, generatoIl, modificatoAMano, risultatoPrompt }
Se manca la chiave, il materiale non è mai stato generato: la vista MATERIALI lo mostra come "Non ancora generato", non ricostruisce un oggetto vuoto solo per popolare la UI.
risultatoPrompt esiste solo per i 4 generatori con haPrompt: true
(Criteri di ricerca prospect, Proposta di valore, Landing, Dimensionamento —
ognuno finisce il proprio testo con un prompt da usare in uno strumento di
scrittura esterno). È il risultato incollato a mano di quel prompt,
indipendente dal testo generato: sopravvive alla rigenerazione del
materiale (rigenerare il testo non deve far sparire un risultato già
raccolto). Compare come campo interattivo in DOCUMENTO subito dopo il testo
del generatore — mai in MATERIALI, dove si vede il prompt ma non un posto
sensato per il suo risultato — e finisce anche nel file .md scaricato.
Ogni generatore ha una categoria obbligatoria (BU.registraGeneratore,
validata contro CATEGORIE in src/gen/_registry.js), che raggruppa i 16
generatori in "capitoli" nelle viste MATERIALI e DOCUMENTO — l'ordine di
lettura, non l'ordine dei file gen/NN-*.js (quello resta solo un id
interno, storico). BU.gen.elencaGeneratoriRaggruppati() restituisce i
gruppi nell'ordine fisso di CATEGORIE, con l'ordine di registrazione
preservato dentro ogni gruppo:
sintesi(BU One-Page) — nessun titolo di capitolo: è una fotografia di tutto il resto, resta da sola in apertura invece di aprire un gruppo.fondamenta— "Fondamenta strategiche"marketing— "Materiali di marketing"commerciale— "Processo commerciale"pilota_test— "Pilota, test e decisione"
I titoli di capitolo compaiono come "Parte N — Titolo" (numerati escludendo
sintesi), sia nell'export .md (render.documentoCompleto, stesso livello
# dei titoli dei singoli generatori — in Markdown grezzo non c'è un
livello sopra h1) sia nelle due viste, dove sono invece un elemento a sé
con uno stile deliberatamente più marcato, per non confondersi con un
titolo di generatore.
Ogni generatore è puro: genera(bu) legge campi/leve/risultati correnti e
restituisce Markdown, senza mai scrivere da solo in bu.materiali — la
scrittura avviene solo al click di "Genera"/"Rigenera" (o di "Rigenera
tutto"), mai automaticamente. Questo vuol dire che un materiale già
generato è un'istantanea: resta quello che era finché qualcuno non lo
rigenera esplicitamente, anche se i campi che lo alimentano cambiano dopo.
bu.campiModificatiIl traccia quando campi o leve sono stati modificati
l'ultima volta — non bu.modificata, che scatta anche per consegna/
lancio/decisione, cose che nessun generatore legge, e marcherebbe
obsoleto ogni materiale per modifiche che non lo riguardano.
schema.materialeObsoleto(bu, materiale) confronta questa soglia con
materiale.generatoIl: se il materiale è stato generato prima dell'ultima
modifica ai dati, è obsoleto. Le viste MATERIALI e DOCUMENTO lo segnalano
(bottone "Rigenera ⚠", banner con conteggio) senza mai bloccare né
rigenerare da sole — solo un click esplicito scrive.
"Rigenera tutto" (in entrambe le viste) itera tutti i generatori
registrati: genera chi non è mai stato generato, rigenera il resto. Un solo
confirm — non uno per materiale — compare solo se almeno un materiale è
stato modificato a mano, elencando quali verrebbero sovrascritti; se
nessuno lo è stato, parte senza chiedere nulla.
risultati (BU.schema.RISULTATI) è una lista piatta di metriche testuali,
non annidata per sezione. Alcune hanno decide: true: sono le uniche che
autorizzano una decisione (conversazioni arrivate al prezzo, preventivi,
vendite, angolo vincente) — le altre (contatti, tasso di risposta) misurano
solo il segnale di messaggio.
decisione e noteDecisione sono due proprietà separate della BU (non
annidate l'una nell'altra), per rispecchiare la richiesta originale del
modello dati. Le tre decisioni possibili aggiornano anche stato:
| decisione | stato BU risultante |
|---|---|
| continua | validata |
| modifica | da_modificare |
| ferma | archiviata |
Questa mappa è una scelta di design (vedi la sezione "Decisioni prese" nel
README), non parte del modello dati in senso stretto — vive in
BU.ui.MAPPA_DECISIONE_STATO (src/ui.js), non in schema.js.
consegna (BU.schema.OUTPUT_CREATIVI) è un catalogo fisso di 32 possibili
output che un art director o un copywriter potrebbero produrre a partire da
questa BU — non tutti servono per ogni BU. Ogni voce ha { selezionato, nota }: si spunta cosa serve davvero, il resto resta visibile come
promemoria di cosa esiste. A differenza di campi, non ha uno stato
ipotesi/generato-da-IA/mandatorio (non è un dato della BU, è uno strumento
di consegna) e non entra mai in completezza() o in "cosa fermerebbe
questa business unit": una BU non è "meno pronta" se non hai ancora deciso
quali output servono al team creativo.
lancio (BU.schema.CHECKLIST_LANCIO) è lo stesso meccanismo di
consegna — stessa forma { selezionato, nota }, stessa esclusione da
completezza/campi critici — ma per un contenuto diverso: 25 verifiche
tecniche/operative di lancio (setup domini/social, sito, tracking e
consenso, Meta), raggruppate in tre categorie (apertura, sito,
tracking_meta) invece delle due di consegna (testi, design). Il
contenuto viene da
docs/vademecum-nuova-bu.html, un documento
company-wide (uguale per ogni BU) linkato dalla sidebar — non duplicato
nei dati della BU: solo le 25 voci-checklist e il loro stato per-BU vivono
in schema.js, il resto (le regole, il perché, le tabelle di competenze e
deliverable) resta nel documento.
BU.schema.normalizzaBU(datiGrezzi) prende qualunque cosa — undefined,
un oggetto vuoto, una BU salvata con uno schema precedente — e restituisce
sempre una BU completa e valida secondo lo schema corrente. Regole:
- Ogni campo/sezione/leva/materiale mancante viene creato con i valori di
default (mai
undefinednei dati salvati). - I valori di tipo sbagliato vengono convertiti quando ha senso (es. una
lista salvata come stringa singola diventa un array di una riga; una
durata_testsalvata come stringa diventa `{ testo: quella stringa, dataFine: '' }), altrimenti scartati in favore del default. - Gli enum (
statodei campi,apertura,decisione, stato materiale) fuori dai valori validi ricadono sul default piuttosto che propagare un valore sconosciuto nell'interfaccia. - Niente viene mai scartato silenziosamente per il solo fatto di non essere nella forma attesa: viene recuperato quando possibile, altrimenti sostituito da un default esplicito.
BU.store.carica() chiama normalizzaBU su ogni BU letta da localStorage,
quindi la migrazione è automatica e trasparente: aggiungere un campo allo
schema non richiede uno script di migrazione separato, basta aggiungere la
definizione in CAMPI (o RISULTATI) — i dati vecchi la riceveranno vuota
al primo caricamento.