Un confine che non fai rispettare è solo un desiderio

Il mio ultimo articolo sosteneva che le decisioni che non scrivi diventano debito. Lo confermo. Ma pochi giorni dopo ho aperto il mio repository e mi sono imbattuto nel rovescio della stessa medaglia, la parte che è facile non vedere:

Scrivere un confine non è la stessa cosa che farlo rispettare.

Cosa ho trovato nel mio repo

Il mio workspace è un monorepo Nx: un'applicazione e qualche decina di librerie UI condivise. Il linter aveva già la regola che dovrebbe sorvegliare chi può dipendere da cosa: @nx/enforce-module-boundaries. Era così:

depConstraints: [
  { sourceTag: '*', onlyDependOnLibsWithTags: ['*'] },
]

Leggila piano. Qualsiasi cosa può dipendere da qualsiasi cosa. La regola c'era, configurata e verde in CI, e non faceva rispettare assolutamente nulla. In più, ognuno dei miei ~60 progetti portava un tags: [] vuoto.

Avevo scritto il confine. Non avevo costruito un muro.

Il nome di quella lacuna

Una architecture fitness function è un check automatico che fa fallire la build quando il sistema si allontana da una proprietà a cui tieni: una regola di layering, una direzione di dipendenza, un budget di performance. Il termine è di Neal Ford e Rebecca Parsons; l'idea è più vecchia della definizione. Il punto è netto: una caratteristica architetturale che non puoi testare è una caratteristica che stai soltanto sperando di avere.

Perché ha smesso di essere opzionale

Per anni potevi cavartela coi confini-come-convenzione, perché chi li attraversava erano umani che avevano partecipato alla code review e assorbito le regole non scritte. Quell'era sta finendo.

Quando il tuo teammate più veloce è un assistente AI, attraverserà un confine che esiste solo come convenzione (con sicurezza, in modo plausibile, coi test ancora verdi) perché ottimizza per "fai compilare e passare", non per "rispetta un'intenzione architetturale che nessuno ha codificato". Una convenzione vive nella testa delle persone. L'AI non era nella stanza quando vi siete messi d'accordo.

Quindi il confine deve vivere in un posto che l'AI non può aggirare a parole: la build.

Dare i denti alla regola

Ho dato un tag a ogni progetto. Due categorie sono bastate a rendere l'idea:

  • type:app: l'applicazione.
  • type:ui: le librerie di componenti condivise.

E ho sostituito la regola che non faceva nulla con due vincoli veri:

depConstraints: [
  { sourceTag: 'type:app', onlyDependOnLibsWithTags: ['type:app', 'type:ui'] },
  { sourceTag: 'type:ui',  onlyDependOnLibsWithTags: ['type:ui'] },
]

In parole povere: l'app può usare la UI condivisa; la UI condivisa può comporre altra UI condivisa, ma non deve mai risalire dentro l'applicazione. Quest'ultima regola è quella che ha valore: una libreria condivisa che dipende dall'app che dovrebbe servire è un nodo circolare pronto a stringersi.

Guardarla lavorare

La cosa migliore di una fitness function è che puoi vederla mordere.

Sul codice reale è passata al primo colpo: il confine che avevo appena dichiarato era già rispettato, quindi non si è rotto nulla. È importante: un buon vincolo ratifica l'architettura che hai già, e da lì tiene la linea.

Poi ho aggiunto una sola riga a un componente button condiviso: un import che risaliva dentro l'app. La build è diventata rossa:

error  Projects cannot be imported by a relative or absolute path,
       and must begin with a npm scope   @nx/enforce-module-boundaries

Nessuna discussione in una pull request. Nessun reviewer costretto a ricordarsi la regola alle 18 di venerdì. Nessuna fiducia nel fatto che l'assistente abbia letto il documento di architettura. Il muro si è semplicemente rifiutato di far passare l'import. Ho tolto la riga; di nuovo verde.

L'intera modifica sono stati qualche tag e quattro righe di configurazione: un pomeriggio, non un progetto.

Dove questo incontra "scrivi la decisione"

Qui la fitness function si unisce all'argomento che avevo fatto la volta scorsa.

Una decisione documentata, "la UI condivisa non deve dipendere dall'app", è necessaria, perché cattura il perché: il ragionamento, il compromesso, il fallimento che previene. Un Architecture Decision Record è il posto giusto per questo.

Ma la documentazione è un messaggio a umani che scelgono di leggerla. Una fitness function è la stessa decisione compilata in qualcosa che non può essere ignorato: né da un collega di fretta, né da un'AI, né da me-tra-sei-mesi che ha dimenticato la conversazione.

L'ADR spiega il confine. La fitness function è il confine.

Scritto e fatto-rispettare sono due lavori diversi, e i sistemi maturi fanno entrambi: il record perché le persone capiscano l'intenzione, il check perché nessuno, e niente, possa attraversarlo di nascosto.

La regola che ti lascio

Un confine che non fai rispettare è solo un desiderio. E nell'era dell'AI, un desiderio è esattamente ciò che i tuoi strumenti scavalcheranno.

Gli invarianti che proteggono denaro, fiducia o un'architettura pulita non appartengono solo a un documento che un modello può parafrasare. Appartengono alla build, dove attraversare la linea fa diventare qualcosa di rosso.

Dove, nel tuo codice, c'è un confine che tutti "conoscono" ma che niente fa davvero rispettare? Quella è la tua prima fitness function.