Assistente indipendente per Pegaso, Mercatorum e San Raffaele
Snapshot di riferimento del capitolo
Questo capitolo usa il candidato PlumePilot 2.35.2, branchfeat/ui-ux-2.35.2, commit
f6fcb4c29c83ba9150df0be31da04fc91dffb634.
Per ricostruire la storia Git useremo anche release e pull request precedenti, in particolarev2.35.1e le PR #26, #31, #34, #39, #40, #41 e #42.
Un progetto software non vive soltanto nei file che vediamo nell’editor.
Esiste contemporaneamente come:
working tree
commit
branch
pull request
tag
release
ZIP
store submission
versione installata dall'utente
Queste cose sono collegate.
Ma non sono la stessa cosa.
Durante lo sviluppo di PlumePilot abbiamo incontrato un caso che rende questa distinzione particolarmente concreta.
Il branch candidato alla 2.35.2 aveva un commit:
170b21ec...
con questo tree:
992149b6...
Poi abbiamo ricostruito il branch sopra il vero commit di release 2.35.1 ottenendo:
4d32a8c2...
con:
lo stesso tree:
992149b6...
ma con parent diverso.
Quindi:
contenuto dei file identico
history diversa
commit SHA diverso
È difficile trovare un esempio più chiaro del fatto che:
Un commit Git non identifica soltanto “i file”. Identifica uno stato dei file inserito in una storia precisa.
Da qui partiremo per studiare Git non come insieme di comandi da memorizzare, ma come modello dati e sistema di release engineering.
Molti iniziano pensando:
Git =
cartella
+ copie precedenti dei file
Questo modello funziona finché usiamo soltanto:
git add
git commit
git push
Poi arrivano:
branch
merge
rebase
squash
revert
cherry-pick
tag
e tutto sembra arbitrario.
Il problema non sono i comandi.
È il modello.
Semplificando, Git ragiona con oggetti come:
blob
tree
commit
tag
Contenuto di un file.
Struttura di directory che collega:
path → blob/tree
Contiene almeno:
tree
parent(s)
author/committer
message
Può puntare a un commit o ad altro oggetto Git.
Per il nostro ragionamento, il punto più importante è:
commit
=
tree
+ parent
+ metadata
Un tree Git identifica lo stato della gerarchia di file.
Nel nostro caso:
170b21ec...
e:
4d32a8c2...
hanno entrambi:
tree 992149b68db9a5a388196ebc20679aaef59352fa
Quindi il checkout risultante è lo stesso.
Il vecchio commit aveva due parent:
2a97f9d8...
1db590c8...
Il nuovo commit ha:
parent a8007425...
Questa differenza cambia:
ancestry
merge-base
PR comparison
ahead/behind
future merge behavior
commit SHA
pur lasciando identici i file.
L’identità del commit dipende dal contenuto dell’oggetto commit.
Se cambiano:
parent
message
author metadata
tree
cambia l’hash.
Per questo:
stesso tree
≠
stesso commit
Due branch possono produrre:
esattamente lo stesso software
ma Git può considerarli:
divergenti
perché il percorso storico per arrivarci è diverso.
Questo è esattamente ciò che ci è successo con la 2.35.2.
Concettualmente un branch è soprattutto:
un ref
che punta a un commit.
Per esempio:
main
↓
a8007425
e:
feat/ui-ux-2.35.2
↓
f6fcb4c2
Quando committiamo sul feature branch:
il ref si sposta
A --- B --- C main
\
D --- E feature
main punta a C.
feature punta a E.
I commit precedenti non “appartengono” rigidamente a un branch.
Sono raggiungibili tramite la storia.
HEAD indica normalmente:
il branch attualmente checkout
che a sua volta punta al commit corrente.
HEAD
↓
feature
↓
E
In detached HEAD:
HEAD
↓
E
senza branch intermedio.
Quando modifichiamo un file:
working tree
Quando facciamo:
git add
la versione preparata entra nell’:
index / staging area
Quando facciamo:
git commit
creiamo un nuovo commit.
file modificato
↓ git add
index
↓ git commit
commit
Questa separazione permette commit selettivi.
main?Perché vogliamo poter avere:
main stabile
+
lavoro incompleto isolato
Esempio PlumePilot:
fix/autoplay-collapsed-followups
feature/epub-regional-preservation
feat/ui-ux-2.35.2
Ogni nome racconta:
intent
Un feature branch dice:
“questa sequenza di modifiche
potrebbe diventare parte del prodotto”
Non dice ancora:
“questa è una release”
Una pull request è una feature della piattaforma GitHub costruita sopra Git.
Contiene:
base branch
head branch
diff
discussion
review
checks
merge action
Possiamo leggerla così:
base state
↓
proposed changes
↓
candidate state
La PR #41 dice essenzialmente:
main
+
UI/UX 2.35.2
=
candidate 2.35.2
In una PR:
base = dove vogliamo arrivare
head = branch che propone modifiche
Per #41:
base: main
head: feat/ui-ux-2.35.2
Una stessa head confrontata con:
main
o con:
integration branch
può mostrare diff completamente diversi.
Per questo le stacked PR devono avere base scelta con attenzione.
La PR #40:
Improve EPUB math preservation...
non era basata direttamente su main.
Era:
base:
fix/autoplay-collapsed-followups
head:
feature/epub-regional-preservation
Quindi era stacked sopra il lavoro autoplay.
main
\
A --- B --- C autoplay branch
\
D --- E EPUB branch
PR #39:
A+B+C → main
PR #40:
D+E → autoplay branch
Quando una feature dipende da lavoro non ancora in main.
Vantaggi:
diff più piccola
review più focalizzata
dipendenza esplicita
La storia futura diventa più delicata se il branch base viene:
squashed
rebased
riscritto
Ed è proprio ciò che ci interessa.
Supponiamo:
A --- B --- C main
\
D --- E feature
Un merge può creare:
A --- B --- C ------- M
\ /
D -------- E
M ha:
due parent
Il merge commit conserva:
commit D
commit E
struttura del branch
punto di integrazione
Con squash:
D + E + F
vengono trasformati in un nuovo singolo commit:
S
applicato alla base.
A --- B --- C --- S
Il branch originale continua ad avere:
D --- E --- F
ma main non li contiene come ancestor.
La PR #39 conteneva:
20 commit
Il commit finale su main:
a8007425f81792e7570516dbdf1e7fb2cbda4d53
ha:
un solo parent
ed è diventato il commit della release 2.35.1.
Questo è coerente con uno squash merge.
Invece di vedere:
fix typo
fix tests
retry
actually fix retry
rename
oops
vediamo:
Release 2.35.1:
ordered autoplay and improved EPUB exports
La storia di main racconta unità logiche più grandi.
Squash interpreta:
una PR
=
una modifica concettuale
Questo è molto utile se i commit interni sono iterativi.
Immaginiamo che 2.35.2 sia stata costruita sopra:
D-E-F
della PR 2.35.1.
Dopo squash, main contiene:
S
che ha gli stessi effetti, ma non gli stessi commit.
Git vede:
D/E/F non sono ancestor di main
anche se il loro contenuto è già lì.
Una compare può dire:
feature branch:
molti commit avanti
alcuni commit indietro
e un merge può produrre conflitti.
Non perché il codice sia necessariamente incompatibile.
Perché la genealogia non è più allineata.
Il candidate branch aveva incorporato la linea originale 2.35.1.
Il commit:
170b21ec139c0f703a4f08809a9e51e479fc413c
aveva due parent:
2a97f9d8...
1db590c8...
e tree:
992149b6...
mainLa release 2.35.1 era invece:
a8007425...
con tree:
b3573ed8...
e una history squash.
Prima del fix:
feat/ui-ux-2.35.2
24 commit avanti
3 commit indietro
rispetto a main.
Ma parte di quella divergenza era history noise.
main nel branchNormalmente:
git checkout feature
git merge main
è un’ottima soluzione.
Preserva history e aggiorna il branch.
Git doveva riconciliare:
squash commit S
con:
commit originali D/E/F
che rappresentavano gran parte dello stesso cambiamento.
Il PR di sync risultava:
non mergeable
Un conflitto non è una richiesta di:
“scegli ours o theirs a caso”
È Git che dice:
non posso dedurre automaticamente
l'intento umano
Prima dobbiamo capire:
quali contenuti vogliamo preservare
Non:
come faccio a convincere Git a mergiare?
ma:
qual è il tree finale desiderato?
Nel nostro caso:
main 2.35.1 definitivo
+
soli file UI/UX 2.35.2
Partendo dal tree di:
a8007425
abbiamo sostituito soltanto i file della UI/UX candidate.
Risultato:
tree 992149b6...
Era esattamente lo stesso tree SHA del vecchio branch.
Quindi:
contenuto candidate corretto
history candidate problematica
Questo ci ha dato una prova molto forte.
Abbiamo creato:
4d32a8c2...
con:
tree:
992149b6...
parent:
a8007425...
Il candidate UI/UX è diventato:
main 2.35.1
↓
4d32a8c2 UI/UX
senza cambiare il tree del candidato.
L’obiettivo è quello di un rebase:
riporre il lavoro sopra una nuova base
Nel nostro intervento abbiamo costruito direttamente un commit:
tree noto
+
parent noto
anziché replayare automaticamente ogni commit.
È più simile a un:
content-preserving reparenting
o a una commit reconstruction.
Solo quando possiamo dimostrare quale tree vogliamo ottenere.
Non è una tecnica da usare alla cieca.
Il fatto che:
old tree == new tree
è stato il nostro controllo di sicurezza principale.
Abbiamo poi spostato:
feat/ui-ux-2.35.2
dal vecchio commit al nuovo.
Questo riscrive la history del branch.
Se altri developer hanno il branch checkout:
force push
può sorprenderli.
Per questo la history rewriting è molto più sicura su:
branch privato/personale
candidate controllato
che su:
main pubblico
branch condiviso da molte persone
Quando possibile usiamo un controllo tipo compare-and-swap:
aggiorna il ref
solo se punta ancora al commit che mi aspetto
Se nel frattempo qualcuno ha pushato:
fallisci
anziché sovrascrivere lavoro nuovo.
È l’equivalente Git di optimistic concurrency.
A --- B --- C main
\
D --- E feature
Dopo:
git rebase main
otteniamo:
A --- B --- C --- D' --- E'
Perché hanno:
parent diversi
quindi hash diversi.
Il contenuto delle patch può essere uguale.
La history no.
Molto utile per:
feature branch privati
pulire history prima della PR
riallineare una branch non condivisa
Se altre persone dipendono dai vecchi hash:
history rewrite
crea coordinamento extra.
Merge dice:
queste due storie sono esistite
e qui si sono integrate
Rebase dice:
riscriviamo la storia della feature
come se fosse partita dalla nuova base
Entrambi possono essere appropriati.
Cambia granularità:
N commit
→ 1 commit
Cambia ancestry:
parent vecchio
→ parent nuovo
mantenendo normalmente più commit.
È proprio la combinazione che può generare:
content already integrated
but ancestry not shared
git cherry-pick X prende:
la patch introdotta da X
e crea:
un nuovo commit
sul branch corrente.
X
e:
X'
possono introdurre lo stesso diff ma avere hash diversi.
Di nuovo:
same change
≠
same ancestry
Per:
hotfix selettivo
portare un singolo fix su release branch
git revert crea un nuovo commit che applica l’inverso di un cambiamento precedente.
A --- B --- C
↓ revert B
A --- B --- C --- R
B resta nella storia.
R ne annulla l’effetto.
Durante il lavoro verso 2.34.0:
integration/post-2.33.2
venne integrato prematuramente in main.
La PR #31 era esplicitamente:
restore main to 2.33.2
after accidental integration merge
main?Una possibilità tecnica sarebbe stata:
force reset main
Ma su una branch pubblica è spesso meglio lasciare una storia esplicita:
merge accidentale
↓
commit di ripristino
così gli eventi restano auditabili.
Un main con:
accidental merge
restore commit
racconta ciò che è successo.
Un force reset può far sembrare:
che non sia mai accaduto
pur avendo magari già triggerato:
CI
clone
store work
collaborator pull
Nel branch di integrazione ci fu anche:
revert branding
seguito da:
restore branding after mistaken revert
Questo mostra un’altra proprietà:
revert
non è magia.
È una nuova modifica che può a sua volta dover essere corretta.
Prima di 2.34.0:
integration/post-2.33.2
serviva a comporre:
supporto Multiversity
branding
EPUB fixes
quiz navigation
release docs
prima che tutto entrasse in main.
feature A ─┐
feature B ─┼→ integration → main
feature C ─┘
Possiamo testare:
l'insieme
senza destabilizzare main.
Se l’integration branch vive troppo a lungo:
drift
merge complexity
hidden dependencies
aumentano.
Branch molto brevi, integrazione frequente su main.
Feature accumulate prima della release.
Per release grandi:
integration
Per fix/release più circoscritte:
feature/candidate branch
→ PR → main
Non serve aderire dogmaticamente a una scuola.
Serve minimizzare:
rischio + coordinamento
main come promessaIdealmente:
stato integrato
che potremmo ragionevolmente trasformare in release
Non necessariamente:
versione già pubblicata
Il release flow spesso è:
candidate branch
↓
test
↓
version/release prep
↓
merge main
↓
tag
Quindi main cambia poco prima del tag.
feat/ui-ux-2.35.2 è diventato di fatto:
feature branch
→ integration candidate
→ release candidate
nel tempo.
Questo può succedere nei progetti piccoli.
Il nome del branch:
feat/ui-ux-2.35.2
non cambia automaticamente quando il suo ruolo diventa:
release candidate
Il nome è un’indicazione umana, non una garanzia semantica.
Draft comunica:
non considerare ancora questo lavoro pronto per merge
pur permettendo:
diff
review
checks
discussion
branch exists
↓
draft PR
↓
ready for review
↓
approved
↓
merged
Git conserva codice.
GitHub coordina il processo umano.
Commit piccoli sono utili:
fix checkbox color
fix spacing
add menu helper
add tests
per capire il lavoro.
Potremmo voler vedere:
Prepare 2.35.2 shared menu UX
come unità logica unica.
Da qui l’attrazione dello squash.
Preferibile quando:
branch history è significativa
stacked branches dipendono dagli hash
vogliamo preservare topology
Preferibile quando:
PR è l'unità logica
commit intermedi sono rumorosi
vogliamo main lineare
Preferibile quando:
commit sono già puliti
vogliamo linear history
ma preservare granularità
Ogni strategia ottimizza una cosa:
leggibilità
provenance
topology
bisectability
downstream branch compatibility
Se una PR contiene 20 commit, dopo squash:
git bisect su main
può dirci:
la regression è nella PR #39
ma non quale dei 20 step interni.
Se i 20 commit interni erano:
tentativi intermedi non validi
preservarli su main potrebbe peggiorare la bisectability.
Il commit ideale è:
piccolo
coerente
buildable
testable
ma nella pratica non sempre lo sviluppo procede così.
Un tag dà un nome stabile a un punto della storia.
Per esempio:
v2.35.1
↓
a8007425...
mainmain si muove.
Il tag dovrebbe rimanere fermo.
main:
a800 → future commits
v2.35.1:
sempre a800
Una GitHub Release aggiunge sopra il tag:
titolo
release notes
asset
pagina pubblica
Il tag è un concetto Git.
La Release è un concetto della piattaforma.
L’ultima release pubblicata al momento di questo capitolo è:
v2.35.1
con titolo:
PlumePilot 2.35.1 — Autoplay in ordine ed EPUB migliorati
Il tag punta esattamente a:
a8007425...
Possiamo avere:
manifest.version = 2.35.2
prima che esista:
tag v2.35.2
Questo è normale durante release prep.
Vogliamo idealmente:
manifest 2.35.2
tag v2.35.2
release title 2.35.2
ZIP names 2.35.2
AMO README 2.35.2
Novità 2.35.2
store submission 2.35.2
manifest 2.35.2
source README 2.35.1
ZIP plumepilot-v2.35.2
Novità 2.35.1
Il runtime può funzionare.
Ma la release è incoerente.
validate-release.mjs verifica anche:
versione
Novità
source README
archive names
Release engineering è parte del software.
PlumePilot usa versioni:
MAJOR.MINOR.PATCH
come:
2.35.2
Nel progetto:
2.35.0
introduce un set sostanziale di feature.
2.35.1
2.35.2
sono release incrementali/fix/miglioramenti all’interno della stessa linea.
Semantic Versioning dà una grammatica utile.
Ma la scelta:
minor o patch?
dipende da:
compatibilità
promessa agli utenti
granularità del prodotto
store behavior
Per una browser extension consumer non abbiamo una public API classica come una library.
La versione compare in:
manifest
store
browser extension manager
review notes
generated document metadata
Novità
support reports
Quindi è anche uno strumento diagnostico.
Possiamo preparare:
testi
screenshot
review notes
draft listing
prima che il candidate sia già su main.
Questo riduce il tempo finale.
Lo store draft non deve diventare una seconda source of truth.
Se il candidate cambia:
versione
feature
permission
privacy
dobbiamo riallineare la bozza.
Quando il codice entra in main.
Quando creiamo tag/release.
Quando:
submit
review
approve
publish
Possiamo avere:
v2.35.2 su GitHub
ma
Chrome ancora 2.35.1
oppure browser diversi su versioni differenti.
La release state è distribuita.
development
↓
candidate
↓
packaged
↓
submitted
↓
in review
↓
approved
↓
published
per ciascuno store.
Chrome: published
Edge: in review
Firefox: submitted
è uno stato perfettamente possibile.
“è uscita la 2.35.2”
potremmo intendere:
tag creato
GitHub release creata
store submitted
store approved
utenti possono installarla
Sono eventi diversi.
candidate
merged
tagged
submitted
approved
published
anziché un generico:
released
quando lo stato conta.
L’utente installa:
ZIP trasformato / package store
prodotto da:
build-release.mjs
commit
↓
build
↓
artifact
↓
store
↓
installed extension
Ogni freccia può introdurre un problema.
Identifica:
source/history state
Identifica:
bytes esatti distribuiti
Identifica:
release logica per utenti/store
Sono tre identity diverse.
da quale commit
è stato generato
questo ZIP
con quale builder
e quale checksum?
Questa è release provenance.
PlumePilot usa:
fixed ZIP timestamp
stable file ordering
fixed compression
local dependencies
per ridurre la variabilità dell’output.
Se tagghiamo:
v2.35.2
dovremmo poter rigenerare il package associato senza “indovinare” quali passaggi manuali erano stati fatti.
Se compiliamo da:
commit X
+
modifiche locali non committate
l’artefatto non corrisponde davvero a X.
Prima di una release:
git status
deve essere compreso.
Idealmente:
nothing to commit, working tree clean
merge
tag
checkout tag
build
candidate build
validate
merge exact tree
tag exact commit
rebuild/verify
Non il rituale.
La proprietà:
artifact pubblicato
deve essere riconducibile univocamente
al commit taggato
È spesso utile per:
smoke test
store validation
web-ext lint
Se il commit finale cambia:
anche solo metadata
dobbiamo assicurare che il package pubblicato venga dal commit finale, non da un vecchio candidate.
SHA256SUMS.txtLa release build genera un hash per:
Chrome ZIP
Firefox ZIP
Edge ZIP
source ZIP
candidate checksum
vs
final checksum
Se cambiano quando non dovrebbero:
investigare
Può descrivere:
master routing
cache invalidation
Firefox transform
Deve descrivere:
valore per l'utente
Devono descrivere:
come verificare la feature
Sono tre audience diverse.
master-guided two-pass discovery
ricerca più affidabile della prima attività incompleta
open demo course,
click Trova prima attività incompleta,
verify navigation
Partiamo da:
stesso cambiamento reale
e lo proiettiamo in:
developer language
user language
reviewer procedure
Il tempo di pubblicazione dipende da un sistema esterno.
Questo influenza la strategia di release.
A volte Firefox può rimanere:
una versione indietro
e la release successiva deve considerare:
upgrade path diverso
Novità cumulative
review notes diverse
Riducendo la dimensione del delta:
review più semplice
regression surface minore
rollback mentale più facile
Possono aumentare:
store overhead
review fatigue
version noise
Negli ultimi cicli:
2.35.0
2.35.1
2.35.2
le release hanno separato blocchi logici abbastanza chiari.
Supponiamo:
main = 2.36 development
store = 2.35.2
critical bug in 2.35.2
Non possiamo necessariamente prendere tutto main.
Potremmo creare:
release/2.35
dal tag 2.35.2:
git switch -c release/2.35 v2.35.2
applicare il fix e pubblicare:
2.35.3
Il fix deve anche entrare nella linea futura.
Possiamo:
merge
cherry-pick
reimplement
a seconda della situazione.
Fix su main, poi backport.
Buono se:
main è compatibile
Fix sul branch stabile, poi forward-port.
Buono se:
main è molto diverso
Non basta:
git reset --hard v2.35.1
Gli utenti hanno già:
2.35.2
e gli store richiedono versioni crescenti.
Spesso dobbiamo pubblicare:
2.35.3
che ripristina comportamento precedente.
È un roll-forward, non un vero rollback binario.
Gli update ragionano su:
versione nuova > versione installata
Quindi non possiamo generalmente “pubblicare di nuovo” 2.35.1 come se 2.35.2 non fosse esistita.
bad change in 2.35.2
↓
revert logic
↓
version 2.35.3
History e user version restano monotone.
Un commit message diventa:
git log
PR context
bisect clue
release archaeology
Fix autoplay recovery after collapsed accordion
fix
Il secondo trasferisce zero conoscenza.
a8007425Il messaggio:
Release 2.35.1:
ordered autoplay and improved EPUB exports
è appropriato per uno squash/release commit.
Rappresenta l’unità logica entrata in main.
Un titolo utile fra sei mesi:
Prepare 2.35.1:
ordered autoplay, incomplete discovery
and improved EPUB exports
è migliore di:
update
fix/
feature/
integration/
feat/
docs/
release/
aiuta a capire l’intento.
Non deve essere perfetto.
Deve essere utile.
Branch vecchi aumentano:
ambiguità
accidental base selection
UI clutter
Eliminare il ref non cancella la storia già raggiungibile da:
main
tag
PR refs
Questo può accadere quando:
lavoro è stato incorporato attraverso altre integration PR
ma la PR originale non viene chiusa.
La dashboard può suggerire:
work still pending
quando semanticamente è già shipping.
Una buona hygiene dovrebbe chiudere/annotare PR obsolete.
anche se la PR originaria:
è ancora open
perché il codice può essere entrato via:
integration branch
squash
cherry-pick
Di nuovo:
GitHub workflow state
≠
code state
Git trova due linee di storia che modificano una stessa regione e non sa quale risultato vogliamo.
Non significa automaticamente:
il codice è incompatibile
Può esistere anche senza conflict marker.
Esempio:
branch A cambia default a true
branch B assume ancora false
Git può mergiare perfettamente.
Il software no.
I test devono risolvere:
semantica
Per questo:
merge successful
non significa:
integration correct
Specialmente per:
UI
autoplay
storage
release metadata
non dimostrano che:
feature + latest main
sia verde.
Il target reale è il merge result.
Può costruire internamente un merge candidate per valutare:
mergeability
checks
Per questo una PR può avere uno SHA di merge virtuale diverso dall’head SHA.
Identifica:
ultimo commit del branch PR
Identifica:
come apparirebbe integrato
Questi concetti vanno distinti quando leggiamo CI o API GitHub.
Il confronto:
old tree == rebuilt tree
ci ha permesso di dire:
nessun byte del candidate UI/UX è cambiato
pur cambiando la history.
git diff vuoto?Sono concetti correlati.
Se due commit puntano allo stesso tree:
lo snapshot completo è identico
Non stiamo soltanto osservando:
nessuna patch mostrata nel confronto scelto
prove di equivalenza
history repair
release auditing
Perché perdiamo:
chi
quando
perché
parent
È un DAG:
Directed Acyclic Graph.
Ogni commit punta ai parent.
Il tempo logico va:
commit nuovo
→ parent vecchio
Non possiamo creare cicli.
Ha più parent.
Per questo il graph si riunisce.
Da backend developer possiamo pensare a un commit come un record immutabile:
id = hash(content + links + metadata)
Simile a:
alias "production-current"
→ immutable revision
Idealmente:
v2.35.1
→ revision specifica
e non viene spostato.
L’analogia con event sourcing è utile ma imperfetta.
Git conserva una storia di snapshot collegati, non un log di domain events.
Ma entrambi valorizzano:
immutabilità
history
derived current state
Squash assomiglia a una forma di compaction:
molte micro-transizioni
→ una transizione logica
Ma perdiamo dettaglio storico sulla mainline.
Cambiamo:
causal parent
delle modifiche.
Non stiamo mutando i vecchi commit.
Creiamo commit nuovi.
I vecchi possono rimanere raggiungibili altrove.
In realtà:
non modifichiamo commit X
creiamo X'
e spostiamo un ref
Questo è importante.
Non cambia gli oggetti.
Sposta il branch ref su una history differente.
Un collaboratore può avere:
local ref → old history
remote ref → new history
e dover riconciliare consapevolmente.
Su branch pubbliche/importanti:
no casual force push
e preferibilmente:
PR
checks
review
Serve a trasferire conoscenza:
perché questa scelta?
quale invariant?
quale fallback?
quale migration?
Con:
scope chiaro
description
test plan
screenshots quando UI
risk areas
Una PR con:
5000 linee non correlate
è difficile da verificare.
Gli errori si nascondono.
Cinque PR fortemente dipendenti possono essere peggio di una PR coerente.
La divisione deve seguire:
unità concettuali
Se B dipende da A:
base B = branch A
il graph lo dichiara.
Aspettare che A entri in main.
Più semplice.
Ma rallenta parallelismo.
[ ] feature scope frozen
[ ] branch synced with main
[ ] regression tests
[ ] browser smoke
[ ] version bump
[ ] changelog
[ ] Novità
[ ] reviewer docs
[ ] browser packages
[ ] validation
[ ] checksums
[ ] merge main
[ ] tag
[ ] GitHub Release
[ ] store submissions
A release prep iniziata, aggiungere:
“già che ci siamo”
aumenta il rischio.
Durante freeze accettiamo preferibilmente:
release blocker
regression fix
metadata fix
non nuove funzionalità non necessarie.
Se cambiamo il candidate dopo:
smoke test
web-ext lint
checksum
quell’evidenza non descrive più esattamente l’artefatto finale.
Ogni commit successivo può richiedere:
rebuild
revalidation
resmoke
proporzionalmente al rischio.
descrizione
screenshot
review steps
release notes
upload package
hash dichiarato
final version-specific source ZIP
se dipende dai bytes finali.
Non scriviamo soltanto:
v2.35.1
ma un titolo descrittivo.
Il repository diventa così anche una timeline del prodotto.
Un utente vuole sapere:
cosa cambia per me?
non leggere ogni refactor.
È utile per:
debug archaeology
reviewer
maintainer
future book
Proprio questo libro ne ha beneficiato.
Tag:
commit esatto
Release page:
cosa rappresenta quella versione
Insieme raccontano:
what + why
Possiamo:
delete feature branch
close obsolete PR
aggiornare roadmap
aprire next candidate
Riduce ambiguità.
Version | Chrome | Edge | Firefox
2.35.1 | live | live | live
2.35.2 | draft | draft| draft
Il repository tag da solo non basta per sapere cosa vede ogni utente.
Potremmo automatizzare un file:
release-status.json
o GitHub environment/deployment metadata.
Per un progetto piccolo può essere eccessivo.
Tutto ciò che è:
deterministico
ripetitivo
facile da dimenticare
browser manifests
ZIP
Firefox transforms
Edge locales
source archive
checksums
validation
scope
release timing
store copy
risk acceptance
manual UX judgment
merge strategy
Automatizzare una decisione ambigua non la rende corretta.
flowchart TD
A[Feature branch] --> B[Regression tests]
B --> C[Pull Request]
C --> D[Release candidate]
D --> E[Version + Changelog + Novità]
E --> F[Build Chrome / Edge / Firefox]
F --> G[Validate artifacts]
G --> H[Smoke test]
H --> I[Merge main]
I --> J[Tag]
J --> K[GitHub Release]
K --> L[Store submissions]
L --> M[Review]
M --> N[Published]
Possono iniziare mentre:
candidate ancora draft
flowchart LR
A[Candidate] --> B[Tests]
A --> C[Store draft texts]
A --> D[Screenshots]
B --> E[Final artifact]
C --> F[Submission]
D --> F
E --> F
Parallelizziamo attività senza confondere source of truth.
PR mostra commit non desiderati.
Feature non contiene ultimi fix di main.
Codice già incluso appare ancora nuovo.
Versioni/testi non allineati.
Artefatto contiene modifiche non committate.
Artifact non riproducibile.
Release label punta a source sbagliato.
Chrome ZIP inviato al target sbagliato o build precedente.
PR review
compare graph
sync/rebase
tests on final result
release-candidate tests
validate-release
naming
checksums
empty release dir
Se la directory contiene già:
plumepilot-v2.35.1-firefox.zip
potremmo confonderlo con output corrente.
Il builder quindi fallisce se trova artefatti precedenti.
Il build deve partire da:
output state noto
non da un mix di run precedenti.
A parità di:
source
environment compatibile
due build producono:
stessi bytes
Se checksum differisce:
qualche input è cambiato
Il problema diventa investigabile.
Meglio:
merge main into candidate
o:
rebase candidate onto main
?
Se la branch è condivisa:
merge
preserva history.
Se è privata e vogliamo lineare:
rebase
può essere più pulito.
Se squash downstream crea history anomala:
content-aware reconstruction
può essere più sicura, ma richiede competenza maggiore.
Possiamo tornare alla domanda fondamentale:
quale snapshot vogliamo?
e confrontare:
tree
diff
tests
La storia deve descrivere correttamente il contenuto.
Non dobbiamo sacrificare il contenuto per “far contento Git”.
byte
path
parent
“questo commit squash equivale
a quei 20 commit originali”
semanticamente.
Quella conoscenza è umana.
Se decidiamo:
tutte le feature PR vengono squashed
allora downstream stacked branch devono essere gestite sapendo questo.
Prima di squashare una base con branch dipendenti:
merge dependent branch first
o
prevedi rebase/reparent dopo squash
A → PR A
B based on A → PR B
quando A merge/squash:
rebase B onto main
force-with-lease B
update PR B base main
--force-with-leaseDa command line è preferibile a un force cieco:
git push --force-with-lease
perché controlla che il remote ref non sia cambiato inaspettatamente.
Concettualmente è lo stesso principio dell’expected SHA usato nel nostro riallineamento.
merge-baseGit usa il common ancestor per capire:
cosa è cambiato da entrambe le parti
Se squash/rebase cambiano ancestry:
merge-base cambia
e il diff può sembrare molto più grande del contenuto semanticamente nuovo.
git log --graphgit log --graph --oneline --decorate --all
rende visibile:
branch
merge
tag
divergenza
Spesso spiega un problema meglio di dieci comandi casuali.
git diffcosa cambia nei file?
git logDomanda diversa:
quali commit/history differiscono?
Possiamo avere:
diff vuoto
log diverso
come nel caso di tree identico/history diversa.
git statuscosa non è ancora registrato?
È uno dei comandi più importanti prima di:
switch
merge
build
release
git showche cosa rappresenta questo commit?
Mostra:
metadata
parents
patch
git cat-filePer studiare Git realmente:
git cat-file -p <commit>
possiamo vedere:
tree
parent
author
committer
message
Non è più:
“credo che un commit abbia un parent”
Lo vediamo nell’oggetto.
ahead / behindSono misure sul commit graph.
Non sulla somiglianza semantica dei file.
24 ahead / 3 behind
non significa:
24 feature nuove e 3 fix mancanti
Può riflettere:
squash/rebase/history divergence
Controlliamo:
merge-base
commit list
tree/diff
prima di concludere:
branch obsolete
Se:
v2.35.1
è già pubblico, altri possono averlo:
clonato
archiviato
referenziato
Spostarlo rompe provenance.
Meglio:
v2.35.2 / patch successiva
o un nuovo tag corretto secondo policy del progetto.
Un file con checksum pubblicato:
non viene sostituito silenziosamente
Se cambia un byte:
nuovo checksum
e, se distribuito, nuova versione
Può essere la source anchor.
Gli ZIP sono derivati.
Collega:
tag/source
a:
artifact exact bytes
checkout v2.35.2
run build
compare SHA256SUMS
Se coincide:
provenance molto più credibile
Possiamo fornire al reviewer:
expected SHA-256
così può verificare che il build riprodotto corrisponda all’upload.
Potremmo imporre:
PR required
checks required
no force push
review required
su main.
Più guardrail:
meno errori accidentali
ma anche:
più processo
Per un progetto piccolo scegliamo il livello che riduce davvero rischio.
Non serviva un bug Git.
Bastava:
clic/merge nel momento sbagliato
Possibili:
branch protection
draft PR
required checks
release checklist
naming
Software engineering include progettare contro i nostri stessi errori.
Prima:
identifica stato desiderato
Poi:
restore via commit/revert
Non iniziare con:
force push casuale
1. stop further merges
2. capture current SHAs
3. identify last good state
4. inspect who pulled/published
5. choose revert vs history rewrite
6. restore tests
7. document
Accumula:
code
metadata
tests
artifacts
prima del commit finale in main.
Durante giorni:
main cambia
store requirements cambiano
branch cambia
quindi dobbiamo riconciliare.
L’analogia è utile solo fino a un certo punto.
Idealmente:
code
+
test
+
docs necessari
insieme.
Un checkout di quel commit dovrebbe avere senso.
Questo migliora:
bisect
review
revert
Gli artifact di release non devono stare nel source history se sono:
derivabili
pesanti
binari
Meglio GitHub Release/store.
Il repository conserva:
recipe
ingredients
tests
la release conserva:
output
build-release.mjs deve essere versionato insieme al source.
Così il tag contiene:
codice
+
metodo per costruire il package
Release notes/reviewer docs utili possono stare nel repo.
Possono vivere solo nello store.
Il rischio è drift.
Per questo conviene almeno mantenere:
bozza canonica
nel progetto quando il costo è basso.
Non basta poter ricostruire i bytes.
Vogliamo poter ricostruire:
perché questa versione?
quali feature?
quali test?
quali target?
Git diff mostra:
cosa
Release note/changelog spiegano:
perché
Per questo hanno valore durante bug archaeology.
problem
↓
issue / note
↓
branch
↓
commits
↓
tests
↓
PR
Non ogni modifica ha una issue formale.
Questo è normale in un progetto piccolo.
L’importante è che:
reasoning critico
finisca almeno in:
PR
commit
test
changelog
user bug
→ PR
→ commit
→ test
→ release
support
debug
regression
audit
book writing
Una merge queue può garantire che ogni PR venga testata contro:
latest main + preceding queued changes
riducendo stale-green PR.
Sarebbe probabilmente eccessivo.
Ma il principio:
test final integration result
resta valido.
Controlliamo:
candidate contains latest required main
Se main contiene già lavoro per una versione futura non destinato alla release:
non dobbiamo inglobarlo automaticamente
Da qui l’utilità di release branch in progetti con sviluppo parallelo.
main
→ v2.35.1 baseline
feat/ui-ux-2.35.2
→ 2 commit sopra main
#41
→ open, draft, mergeable
Il secondo commit:
f6fcb4c2...
prepara:
version 2.35.2
Novità
AMO/release metadata
Non soltanto:
feature UI
ma:
UI
+
version
+
release packaging metadata
smoke test
artifact validation
release blockers resolved
Gli store hanno bisogno del package.
Quindi il codice può essere taggato/merged prima che l’approvazione avvenga.
Una sequenza pulita è:
1. final candidate tests
2. build candidate package
3. validate
4. merge #41 → main
5. confirm main tree
6. build/verify final from main
7. create v2.35.2 tag
8. GitHub Release
9. upload/submit stores
Se store draft richiede upload anticipato:
candidate package upload
può essere fatto, ma dopo merge conviene verificare che:
final tree/artifact
sia quello già caricato.
Potremmo confrontare:
candidate tree
main merged tree
Se sono uguali:
content invariant preserved
Poi rigeneriamo comunque gli artifact se release metadata/build input lo richiede.
Avremo:
main
↓
single 2.35.2 commit
e i due commit del candidate non diventeranno ancestor diretti.
Qualsiasi future branch basata su:
4d32 / f6fc
dovrà essere riallineata dopo squash.
La lezione della 2.35.1 va applicata subito.
Chiedi:
esistono branch future stacked sopra?
Se sì:
annota/rebase subito dopo merge
Dopo 2.35.2:
main
↓
new feature branches
evitando di costruire 2.35.3 sopra la vecchia candidate prima del merge, se non necessario.
feature boundaries
release boundaries
fixes
reverts
Eliminare ogni merge può nascondere relazioni reali.
Preservare ogni micro-commit può creare rumore.
È un design trade-off.
Riduce:
tempo per capire “cosa è uscito quando”
Può rimanere:
nella PR
anche se main riceve squash.
GitHub conserva discussione e commit della PR.
La PR conserva:
description
review
comments
screenshots
commit list
Quindi squash non cancella tutta la storia umana.
flowchart TD
A[User need / bug] --> B[Feature branch]
B --> C[Commits]
C --> D[Regression tests]
D --> E[Pull Request]
E --> F[Release candidate]
F --> G[Build script]
G --> H[Chrome ZIP]
G --> I[Firefox ZIP]
G --> J[Edge ZIP]
G --> K[AMO source ZIP]
H --> L[SHA256SUMS]
I --> L
J --> L
K --> L
F --> M[Merge main]
M --> N[Tag vX.Y.Z]
N --> O[GitHub Release]
H --> P[Chrome Store]
I --> Q[AMO]
J --> R[Edge Add-ons]
commit graph
v2.34.0
v2.35.0
v2.35.1
v2.35.2
quale versione è diventata live
su quale store
e quando
Questa distinzione è essenziale quando rispondiamo:
“quale codice sta usando questo utente?”
browser
installed version
store channel
Non basta:
“ho preso l'ultima”
perché i tre store possono essere in stati differenti.
Se la versione è pubblicata correttamente:
2.35.1
→ tag v2.35.1
→ commit a800...
Questo restringe immediatamente il codice da analizzare.
chrome.runtime.getManifest().versionPermette di mostrare/loggare:
versione installata
nel contesto dove l’API è disponibile.
Non va chiamata da un contesto page-world dove:
chrome.runtime
non esiste.
Anche release identity attraversa i boundary.
Se 2.35.2 contiene principalmente:
UI/UX alignment
e compare un autoplay bug:
sospettiamo meno direttamente il core autoplay
anche se non possiamo escludere effetti indiretti.
Release boundaries sono anche:
causal boundaries
imperfette ma utili.
È un obiettivo, non una legge fisica.
La domanda pratica:
quanto rapidamente rileviamo
e ripristiniamo?
riducono il Mean Time To Detect.
tests green
artifact validator green
prima del merge/tag.
Una UI può essere:
formalmente valida
ma confusa
Da qui lo smoke/manual review.
code
test
docs/changelog se necessario
candidate
validation
artifact
metadata
tag
store submission
La Definition of Done è diversa.
GitHub Actions potrebbe:
run tests on PR
build artifacts on tag
validate release
upload GitHub Release assets
Possibile in alcuni ecosistemi, ma introduce:
credentials
API policy
approval flow
risk
Per un progetto piccolo il manual upload può restare ragionevole.
Continuous Integration:
integra e verifica spesso
Può significare:
Continuous Delivery
o:
Continuous Deployment
La review esterna rende il continuous deployment meno diretto.
Possiamo automatizzare:
delivery candidate
ma la pubblicazione può richiedere review.
Include:
Git
tests
build
store
reviewer
maintainer
utente
Per esempio:
reviewer note chiara
è una feature del processo tanto quanto:
validator
commit
→ CI
→ Docker image
→ registry
→ deploy
commit
→ build
→ browser ZIP
→ store
→ review
→ client update
Docker digest e ZIP SHA-256 svolgono ruoli concettualmente simili:
identificare bytes esatti
Se cambia storage schema:
utenti hanno dati precedenti
come un database già popolato.
Il release deve supportare:
upgrade path
Dal Capitolo 6:
legacy Pegaso key
→ scoped platform key
è parte della release compatibility.
Una versione precedente reinstallata manualmente potrebbe:
non capirlo
Non sempre supportiamo downgrade.
Ma dobbiamo essere consapevoli.
Potremmo spiegare:
preferenza resettata
setting migrata
permission nuova
Una nuova permission può:
cambiare review
mostrare prompt
richiedere disclosure
Quindi non è una modifica come un CSS tweak.
copy
spacing
docs
UI logic
storage default
permissions
background lifecycle
autoplay navigation
test automation
export pipeline
migration
Non tutte le release richiedono la stessa intensità.
what
why
scope
tests
known limits
risk
Guardiamo:
candidate vs main
non soltanto i singoli commit.
Potremmo avere:
commit apparentemente corretti
ma file generato/stale rimasto nel tree
Lo snapshot finale conta.
Per una release UI:
content.js modificato?
background.js modificato?
Se non ce lo aspettiamo:
investigare
Una release candidate potrebbe avere una allowlist di file attesi.
Non sempre conviene.
Ma per hotfix molto stretto è utile.
debug.txt
screenshot.png
local config
entra nel package.
Artifact validator e file allowlist possono impedirlo.
.gitignore non basta.gitignoreimpedisce molti file non tracciati.
impediscono file tracciati ma non runtime:
tests
docs
scripts
Sono due boundary diversi.
Non dovrebbe diventare input involontario del prossimo build.
Da qui:
excluded release/
empty output guard
Il runtime ZIP esclude:
scripts/tests
Il source AMO li include.
Perché hanno scopi diversi.
Release engineering costruisce più view dello stesso repository.
È superset.
È subset + transforms.
È subset differente.
Il build è una projection.
Se aumentiamo versione all’inizio dello sviluppo:
molti commit intermedi sembrano già release
Se la cambiamo dopo aver creato package/testi:
drift
Nel candidate finale, vicino alla release prep.
È ragionevole.
Idealmente:
manifest.version
alimenta:
build filenames
validator expectations
runtime display
Quando dobbiamo duplicarla in:
README reviewer
aggiungiamo validator.
feature + release metadata
feature complete
↓
prepare version/release notes
Abbiamo due commit chiari:
4d32a8c2
Rebase UI/UX candidate onto released 2.35.1
f6fcb4c2
Prepare 2.35.2 version and shared menu release notes
Questa separazione rende evidente il confine:
content alignment
vs
release prep
Se vogliamo:
un solo release commit
squash è coerente.
rebase/merge potrebbe mantenere due commit.
La scelta va fatta prima di costruire future branch sopra questi hash.
Per release non banali potremmo salvare:
docs/releases/2.35.2.md
con:
scope
test plan
known limitations
store state
Alcune release precedenti già hanno documenti simili.
Fra mesi:
perché Firefox package era diverso?
troviamo la risposta vicino alla release.
PR base/head
merge SHA
branch SHA
commit parents
tree SHA
direttamente.
Questo è utile quando la UI GitHub semplifica troppo la storia.
weird ahead/behind
unexpected mergeability
tag mismatch
history rewrite
Git CLI:
log
show
diff
merge-base
status
è sufficiente.
Chiedi:
è condiviso?
rebase possibile
merge main spesso più sicuro
Chiedi:
il branch downstream contiene i vecchi commit?
Se sì:
rebase/content-aware cleanup
prima di un merge cieco.
Chiedi:
è già pubblico/condiviso?
Se sì:
preferisci revert/restore commit
a history rewrite, salvo casi controllati.
| Problema | Strumento principale |
|---|---|
| branch solo stale | merge/rebase |
| commit singolo da portare | cherry-pick |
| modifica da annullare pubblicamente | revert |
| history privata rumorosa | interactive rebase |
| squash ancestry mismatch | rebase/reparent/content reconstruction |
| wrong public main commit | revert/restore commit |
| wrong release artifact | rebuild + new version if published |
| wrong tag non pubblico | recreate carefully |
| wrong tag già pubblico | prefer new corrective release |
Hai:
main:
A-B-S
feature:
A-B-D-E-F-G
dove:
S = squash di D-E-F
G = nuova feature
Perché una merge può risultare rumorosa?
Disegna due possibili strategie per ottenere:
A-B-S-G'
Due commit hanno:
tree identico
parent diverso
Quali proprietà sono uguali?
Quali sono diverse?
La PR contiene 15 commit:
5 feature
4 fix dei fix
3 typo
3 test
Quando preferiresti:
squash
e quando no?
Hai già pubblicato:
v2.35.2
ma scopri un bug grave.
Perché spostare il tag a un commit corretto non risolve il problema degli utenti già aggiornati?
Store Chrome mostra:
2.35.1
GitHub Latest:
2.35.2
Firefox:
2.35.0
Quale versione sta usando “un utente PlumePilot”?
Risposta:
dipende dal browser e dal canale
La domanda era incompleta.
Il build genera un Firefox ZIP con checksum diverso dallo stesso commit di ieri.
Elenca input possibili da investigare:
Node version
builder change?
working tree dirty?
timestamp?
file ordering?
vendor file?
environment?
Una feature branch è condivisa da tre persone.
Vuoi riallinearla con main.
Perché:
```bashgit rebase main git push –force
richiede molta più cautela di un merge?
---
# Parte CXCIX — Esercizio 8
Una PR è mergeable e tutti i file si applicano senza conflitti.
Quali semantic conflict potrebbero comunque esistere?
---
# Parte CC — Concetti da portarsi dietro
## 354. Blob
Oggetto Git che contiene bytes di file.
---
## 355. Tree
Snapshot strutturale di file e directory.
---
## 356. Commit
Oggetto immutabile che collega:
```text
tree + parent(s) + metadata
Puntatore mobile a un oggetto Git, come un branch.
Riferimento alla posizione corrente del checkout.
Commit con più parent che integra storie.
Integrazione che comprime più commit in un nuovo commit singolo.
Ricostruzione di commit sopra una nuova base, creando nuovi hash.
Applicazione della modifica di un commit in un altro punto della storia tramite un nuovo commit.
Nuovo commit che annulla semanticamente un cambiamento precedente senza cancellarlo dalla history.
Ancestor comune usato per confrontare e integrare due linee di storia.
Due commit puntano allo stesso snapshot completo dei file anche se history e metadata differiscono.
Output costruito e distribuibile derivato dal repository.
Catena che collega:
source commit
→ build process
→ artifact
→ checksum
→ distribuzione
Stato considerato potenzialmente pubblicabile dopo le validazioni finali.
Processo che, a parità di input rilevanti, produce lo stesso output.
Sequenza delle versioni realmente pubblicate nei diversi canali, distinta dal commit graph.
Correzione di una release problematica tramite una nuova versione superiore anziché ripubblicare una versione precedente.
Differenza di ancestry fra branch, misurata in commit ahead/behind e non necessariamente equivalente a differenza semantica del codice.
Pull request costruita sopra un altro branch/PR non ancora integrato nel target finale.
Aggiornamento forzato di un branch che fallisce se il remote ref è cambiato rispetto a ciò che ci aspettiamo.
Git Book:
https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
https://git-scm.com/book/en/v2/Git-Internals-Git-References
Git Book:
https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging
https://git-scm.com/book/en/v2/Git-Branching-Rebasing
GitHub Docs:
https://docs.github.com/en/pull-requests
https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges
GitHub Docs:
https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/configuring-commit-squashing-for-pull-requests
GitHub Docs:
https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases
Git:
https://git-scm.com/book/en/v2/Git-Basics-Tagging
https://semver.org/
PlumePilot ci ha mostrato un caso concreto in cui:
same tree
≠
same commit
e questo singolo fatto apre quasi tutto il capitolo.
Git tiene insieme:
snapshot
causal history
branch pointers
release anchors
mentre GitHub aggiunge:
pull request
review
draft state
release pages
e il release system aggiunge ancora:
browser build
checksums
store submission
approval
publication
La pipeline completa è quindi:
idea
↓
branch
↓
commit
↓
test
↓
PR
↓
candidate
↓
build
↓
artifact
↓
merge
↓
tag
↓
GitHub Release
↓
store
↓
utente
Le lezioni principali sono:
Il branch è un puntatore, non un contenitore magico.
Il commit identifica uno snapshot dentro una storia, non soltanto i file.
Squash semplifica
main, ma cambia ancestry e quindi va considerato quando esistono branch stacked.
Un merge senza conflict non dimostra la correttezza semantica.
Il repository non è il prodotto distribuito: il prodotto è l’artefatto costruito e verificato.
Tag, GitHub Release e pubblicazione store sono tre eventi distinti.
E soprattutto:
Release engineering significa rendere riproducibile non soltanto il codice, ma il percorso con cui quel codice diventa software nelle mani degli utenti.
Abbiamo ormai studiato:
contexts
messaging
DOM
API
async
storage
cache
state machine
search algorithms
document generation
editor
UI
performance
cross-browser
testing
Git/release engineering
Il Capitolo 16 farà un passo indietro.
Non introdurrà una nuova feature.
Cercherà invece di rispondere:
quali pattern sono emersi spontaneamente
in tutto PlumePilot?
Parleremo di:
boundaries
identity
source of truth
anti-corruption layers
progressive refinement
fail-closed behavior
functional core / imperative shell
state machines
idempotence
graceful degradation
ownership
lifetime
observability
artifact thinking
e proveremo a ricostruire l’architettura che PlumePilot possiede oggi, anche quando non è stata progettata esplicitamente fin dall’inizio.
← 14 — Debug, test e regressioni · Indice · 16 — Lezioni architetturali →