Assistente indipendente per Pegaso, Mercatorum e San Raffaele
Snapshot di riferimento del capitolo
Questo capitolo usa come baseline il candidato PlumePilot 2.35.2, branchfeat/ui-ux-2.35.2, commitf6fcb4c29c83ba9150df0be31da04fc91dffb634, ma sintetizza anche decisioni e regressioni osservate nei capitoli precedenti.
Fin qui abbiamo studiato PlumePilot per parti.
Abbiamo aperto il cofano e guardato:
manifest
content script
MAIN world
bridge
background
popup
floating menu
cache
storage
API
DOM
state machine
PDF
EPUB
HTML editor
build
store package
Nel farlo abbiamo seguito feature concrete.
Questo capitolo cambia prospettiva.
Non ci chiederemo più:
Come funziona autoplay?
oppure:
Come viene costruito un EPUB?
Ci chiederemo invece:
Quali principi architetturali continuano a riapparire in parti del progetto apparentemente diverse?
Quando una stessa idea compare in:
autoplay
export
commissioni
UI
cross-browser
release tooling
non siamo più davanti a un dettaglio locale.
Stiamo osservando l’architettura del sistema.
E la cosa interessante è che gran parte di questa architettura non è nata da un diagramma iniziale.
È emersa progressivamente da:
bug reali
vincoli del browser
comportamento del sito host
feedback utenti
store review
performance problems
Questo è uno dei motivi per cui un progetto reale è tanto istruttivo.
Se chiedessimo:
“qual è l’architettura di PlumePilot?”
una risposta superficiale potrebbe essere:
popup.js
background.js
content.js
bridge.js
floating-menu.js
...
Ma questo descrive soltanto la struttura fisica del repository.
L’architettura logica è più simile a:
host platform
DOM + API + session
│
┌───────────┴───────────┐
│ │
MAIN world isolated world
│ │
└──────── messaging ────┘
│
background
coordination layer
│
┌────────────────┼────────────────┐
│ │ │
popup floating menu builders
│ │ │
└────── shared persisted state ───┘
I file sono implementazione.
Le relazioni fra responsabilità sono architettura.
Un boundary è un punto in cui cambiano una o più proprietà fondamentali:
ownership
trust
lifetime
API disponibili
formato dati
source of truth
failure model
In PlumePilot ne incontriamo moltissimi.
Per esempio:
page world ↔ extension world
popup ↔ background
background ↔ storage
DOM ↔ API
raw platform response ↔ normalized model
source tree ↔ browser artifact
Molti bug non nascono dentro una funzione.
Nascono quando attraversiamo un boundary con un’assunzione sbagliata.
Esempi:
“questo oggetto DOM resterà valido dopo await”
“questa percentuale master è la verità”
“questa API esiste in tutti i browser”
“questo evento arriverà sicuramente al popup”
“questo ZIP rappresenta esattamente il repository”
Sono tutte assunzioni di boundary.
Un buon boundary non trasporta soltanto dati.
Li traduce.
Per esempio:
raw Multiversity response
↓
normalizeCourseOutline()
↓
PlumePilot course entries
Oppure:
browser-specific runtime
↓
build-release.mjs
↓
PlumePilot target artifact
La traduzione protegge il resto del sistema dalle stranezze esterne.
In Domain-Driven Design si usa spesso il termine anti-corruption layer per indicare un layer che impedisce al modello esterno di contaminare direttamente quello interno.
In PlumePilot vediamo forme leggere di questo pattern.
La piattaforma può usare:
lp_id
paragraph id
display_order
folder id
percentage
Il resto di PlumePilot preferisce concetti come:
lessonNumber
identity
playbackItems
test
objective
Quindi:
external schema
↓
normalizer
↓
internal model
Pegaso, Mercatorum e San Raffaele possono avere differenze nel DOM.
Invece di spargere:
if (pegaso) ...
if (mercatorum) ...
ovunque, funzioni come sections() cercano di produrre una rappresentazione comune.
Lo stesso principio ricompare nel Capitolo 13:
common source
↓
release adapter
↓
Chrome / Edge / Firefox artifacts
Tre problemi diversi.
Stesso pattern.
Una delle lezioni più profonde del progetto è nata dall’autoplay.
Un nodo DOM può:
sparire
essere ricreato
cambiare parent
venire nascosto
essere sostituito durante un await
Quindi non può essere l’identità logica di una lezione.
DOM node
=
rappresentazione momentanea
mentre:
course + section + chapter + lesson
=
identità logica
Questo porta al pattern:
resolve DOM
↓
extract logical identity
↓
await
↓
resolve DOM again
↓
validate identity
Quando un singolo campo non basta:
display_order = 1
perché si ripete in più sezioni, usiamo un’identità composta:
sectionText + chapterText
oppure, quando disponibili:
lpId + paragraphId
Usa come identity ciò che rimane stabile durante il lifetime del processo che devi completare.
Non ciò che è semplicemente comodo da leggere in quel momento.
exam_id = 7 non è necessariamente globale.
Potremmo avere:
Pegaso exam 7
Mercatorum exam 7
Quindi la vera identity è:
platform + exam_id
Lo stesso vale per i corsi:
COURSE123
può essere ambiguo fra piattaforme.
La chiave persistente diventa:
pegaso:COURSE123
mercatorum:COURSE123
Quando due entità collidono, la domanda non è soltanto:
“come aggiungo un prefisso?”
ma:
Quale parte del dominio mancava nella nostra definizione di identità?
PlumePilot vede spesso la stessa informazione da fonti diverse:
DOM
master API
lesson detail API
cache
storage
session state
Il problema non è soltanto scegliere una fonte.
È definire una gerarchia di evidenze.
La master call è molto utile per:
routing
candidate ordering
coarse progress
ma non sempre prova che:
tutte le attività siano realmente complete
Quindi è un buon:
index
non necessariamente la source of truth finale.
Una lesson detail response fornisce evidenza più precisa su:
Obiettivi
video
test
ma può essere incompleta o fallire.
Il DOM può essere:
stale
non renderizzato
accordion chiuso
ma può anche diventare l’unico fallback visuale disponibile.
coarse signal
↓
more precise signal
↓
visual fallback
↓
fail safe
Esempio:
DOM dice Obiettivi 0%
API fresca dice Obiettivi 100%
Se il DOM è noto stale, non deve sovrascrivere l’API fresca.
In forma generale:
fresh authoritative evidence
>
stale cache
>
heuristic hint
con eccezioni esplicite quando il provider stesso è noto imperfetto.
Molti problemi iniziano con:
complete = true / false
ma la realtà è:
complete
incomplete
unknown
Se la UI non mostra la percentuale:
undefined
non significa:
0%
né:
100%
Significa:
non sappiamo
Questo pattern appare in:
activity progress
API result
material link
commission state
cache freshness
Quando modelliamo esplicitamente unknown, possiamo decidere:
retry
fallback
visual verification
stop
invece di inventare un valore.
Se non sappiamo quale capitolo sia quello giusto, non dovremmo:
cliccarne uno a caso
Se non sappiamo se un test è incompleto, non dovremmo:
rispondere automaticamente
certainty sufficient?
├── yes → side effect
└── no → fallback / stop
no verified route
→ no skip
pattern inatteso
→ fail build
pending test
→ localizza
→ non risolvere
Molti sistemi sembrano “smart” perché prendono sempre una decisione.
Un sistema robusto sa anche dire:
non ho abbastanza informazioni
Per trovare una attività incompleta potremmo interrogare tutto.
Ma sarebbe costoso.
Quindi:
cheap coarse evidence
↓
selected candidate
↓
expensive precise verification
↓
fallback only if needed
text extraction
↓ insufficient
regional preservation
↓ insufficient
full visual raster
fresh cache
↓ miss/stale
API
↓ failure
DOM fallback
Possiamo astrarre:
fast path
│
├── sufficient evidence → done
│
└── uncertainty → slow path
Questo pattern tiene insieme performance e correttezza.
Map lookup
DOM query
network request
PDF parse
canvas raster
sono operazioni molto diverse.
Non necessariamente con numeri espliciti.
Ma con un ordine ragionato:
cheap
→ medium
→ expensive
Il first-incomplete engine assomiglia a un query planner:
index says likely row
↓
fetch detail
↓
verify exact predicate
Abbiamo visto quattro livelli:
in-memory
sessionStorage
chrome.storage.local
server/platform
Adatto a:
pending request
cache veloce
current operation helper
AbortController
quando la perdita al reload è accettabile o recuperabile.
sessionStorageAdatto a:
recovery di navigazione
reload attempt
short-lived continuity
chrome.storage.localAdatto a:
preferenze
operation recovery
achievement state
commission snapshots
Rimane authoritative per:
progress ufficiale
exam state
course structure
La persistenza deve essere abbastanza lunga per il dato, ma non più lunga del necessario.
Persistenza eccessiva crea:
stale state
migration cost
privacy surface
Una delle domande più utili è:
chi può modificarlo?
Il background coordina operazioni globali.
Quindi è un owner naturale di:
active operation
builder ownership
lease
Popup e floating menu leggono e riflettono:
preferenze
operation
presentation model
ma non dovrebbero inventare due verità indipendenti.
Se due componenti possono scrivere lo stesso dato senza protocollo:
lost update
è quasi inevitabile.
Dove possibile:
un coordinator scrive
più UI leggono
Le write serializzate dal background sono un esempio.
Preferenze utente possono essere modificate da:
popup
floating menu
Qui serve:
storage normalization
onChanged synchronization
Quando scriviamo:
chrome.runtime.sendMessage(...)
non stiamo chiamando una funzione normale.
Stiamo attraversando:
process/context boundary
type
payload
response
error semantics
correlation
lifetime
Un messaggio può dire:
accepted: true
ma il lavoro essere ancora:
running
Questo è lo stesso concetto di HTTP 202.
I message type sono stringhe:
PEGASO_START_EXPORT
PEGASO_GET_OPERATION
...
Se il protocollo cresce potrebbe essere utile:
schema condiviso
version
request/response types
Un framework interno di messaging può diventare più complesso del problema.
Il pattern giusto è:
formalizzare quando il numero di contratti lo giustifica
await divide il mondoPrima di:
await something();
abbiamo una fotografia.
Dopo:
DOM può cambiare
utente può navigare
setting può cambiare
video può cambiare
operation può essere cancellata
read
remember identity
await
re-read
validate
act
Prima di cliccare:
sono ancora nello stesso corso?
è ancora lo stesso player?
autoplay è ancora enabled?
la lesson identity coincide?
È simile a:
read version 12
compute
before write verify still version 12
La “version” può essere:
course code
generation token
current video identity
DOM identity
Usa:
smartResumeGeneration
per riconoscere una ricerca superata.
start operation → generation N
new operation → generation N+1
old continuation wakes up
if generation != N → cancel
Non ferma fisicamente ogni Promise.
Ma impedisce che una continuation vecchia produca side effect.
Una delle direzioni più promettenti del progetto è separare:
what should happen?
da:
perform browser effect
autoplayPresentation():
settings
↓
presentation model
senza toccare il DOM.
master + outline
↓
route map
è quasi logica pura.
Poi:
openSection
openChapter
clickRow
scrollIntoView
sono side effect browser.
Decision logic pura è:
più testabile
più riusabile
più comprensibile
Side effect restano concentrati.
Autoplay non è:
if video ended → click next
È:
state + event + guard + context + transition
Se vediamo molte variabili come:
enabled
busy
stopAtTests
autoCompleteTests
stoppedAtTestContext
thresholdBypassed
abbiamo già una macchina a stati, anche se non l’abbiamo chiamata così.
Possiamo chiedere:
quali transizioni esistono?
quali sono proibite?
quale state entra dopo cancel?
Non serve un unico enum.
Il vero state può essere il vettore:
(enabled,
busy,
stopAtTests,
autoCompleteTests,
thresholdBypassed,
currentContext)
Se ogni flag è indipendente:
2^N combinations
molte possono essere invalide.
Potremmo derivare più valori da uno stato più compatto o usare reducer/statechart per aree che continuano a crescere.
In sistemi distribuiti e browser extension:
message retry
double click
reconnect
reload
same event observed twice
succedono.
riconosci duplicato
→ non ripetere
ripetere l’operazione
→ stesso risultato finale
Un operationId permette di capire se:
builder request
appartiene a un lavoro già in corso.
Gli eventi sonori vengono ricordati per evitare doppia riproduzione/achievement.
Anche se l’API sembra semplice, conviene progettare come se alcuni eventi potessero essere:
ripetuti
ritardati
persi durante lifecycle
Per questo la UI non dovrebbe dipendere soltanto da:
un evento “operation started”
ma poter chiedere:
qual è lo stato corrente?
Se il popup si apre dopo l’evento:
non lo ha visto
Polling continuo:
spreca lavoro
initial snapshot
+
subsequent events
è un pattern molto più robusto.
Possono aggiornarsi in istanti leggermente diversi.
L’obiettivo non è:
same microsecond
ma:
convergere allo stesso state
Funge da canale reattivo di convergenza.
Una cache risponde:
ho una copia utile?
non:
questa è la realtà assoluta?
Una entry robusta contiene più del valore:
value
identity
fetchedAt
completeness
source
error/attempt metadata
Un dato può essere riutilizzato se:
completed = true
anche se vecchio, quando completion è monotona.
Mentre un dato:
progress = 40%
può diventare stale molto rapidamente.
Per una activity:
completed false → true
non dovrebbe tornare normalmente false.
Possiamo conservare:
max percentage
completed OR previousCompleted
quando identity è la stessa.
Se la route è cambiata:
non possiamo unire due activity diverse
anche se i campi sembrano compatibili.
Anche se il GC funziona perfettamente.
pageLessonSnapshotsHa limite:
50 entries
Conserva soltanto una finestra bounded/TTL.
Ogni struttura long-lived dovrebbe rispondere:
come viene rimossa una entry?
Se non abbiamo risposta, abbiamo un rischio.
Perché cancellazione reale richiede:
UI intent
↓
message
↓
operation state
↓
AbortController / generation
↓
loops observe cancellation
↓
cleanup
JavaScript non interrompe magicamente una funzione arbitraria.
Il lavoro deve controllare periodicamente:
signal.aborted?
Se controlliamo abort solo ogni 30 secondi:
cancellazione formalmente supportata
ma UX pessima
Per:
AbortController
canvas
PDF page
Blob URL
message listener
pending request Map entry
serve ownership chiara.
allocate
use
transfer ownership?
release
Un EPUB finale da:
40 MB
può richiedere molto più RAM durante la build.
PDF bytes
PDF.js structures
canvas RGBA
PNG bytes
ZIP entries
final buffer
possono coesistere.
Quindi:
process sequentially
cleanup page
null large buffers
dedupe assets
bound canvas resolution
sono decisioni architetturali.
Promise.all(100 PDFs)
può ridurre wall time ma esplodere:
network pressure
memory
CPU
Delay fra request è una forma semplice di backpressure.
Un concurrency limiter esplicito potrebbe controllare:
N in-flight tasks
meglio di delay statici in alcune pipeline.
Esempio audio:
Chromium → offscreen
Firefox → content tab
stessa semantica, adapter diverso.
text
regional
visual
sono livelli di degradazione controllata.
API link
DOM link
visual path
L’utente deve ricevere:
risultato alternativo
oppure errore comprensibile
non un output corrotto silenzioso.
Chrome ed Edge:
offscreen audio
Firefox:
tab audio
Semantica:
play requested notification
Popup e floating menu hanno DOM/lifecycle diversi.
Ma condividono:
presentation rules
settings semantics
operation semantics
Non cerchiamo:
implementation identity
ma:
observable contract identity
Non ha trasformato popup e floating menu in un unico mega-component.
Hanno:
different DOM roots
different lifecycle
different navigation constraints
different embedding
menu-ux.js
menu-ux.css
layout model
presentation logic
Condividi la semantica stabile, non forzare l’unificazione delle superfici che hanno lifecycle differenti.
Possiamo avere:
saved setting
+ override
+ operation state
+ course-specific bypass
raw state
↓
derive effective state
↓
derive labels/hints
↓
render
Popup e floating menu possono usare la stessa interpretazione.
Se possiamo calcolare:
effectiveStopAtTests
da:
saved test behavior
chapter limit
auto complete
non serve necessariamente persistirlo separatamente.
Se salviamo sia:
raw settings
sia:
derived effective state
possono divergere.
Esempi:
una operation attiva deve restare cancellabile
una identity ambigua non autorizza navigation
un hidden tab non provoca destructive reload
platform è parte della persistent identity
Patch:
aggiungi 200ms
Invariante:
non avanzare finché la nuova lesson non è verificata
La seconda sopravvive meglio ai refactor.
Il Capitolo 14 ha mostrato il passaggio:
bug
→ invariant
→ test
Questa è una delle principali forme di maturazione architetturale.
Per una extension:
source tree
↓
build
↓
Chrome ZIP
Firefox ZIP
Edge ZIP
Manifest specialization, vendor transformation, locales e checksums cambiano ciò che viene distribuito.
Serve anche:
artifact validation
common source
manifestFor(browser)
Firefox transforms
Edge locales
ZIP
validate-release.mjs
Questo modello mentale rende la release pipeline molto più comprensibile.
Spesso vogliamo fallback.
Se una precondizione deterministica è violata:
manifest sbagliato
checksum vendor cambiato
pattern transform ambiguo
meglio:
fail immediately
runtime uncertainty
→ degrade/recover
build invariant violation
→ stop
Un buon diagnostic log dovrebbe spiegare:
quale decisione
su quale identity
con quale evidence
master says chapter 5 <100%
→ detailed verification 5
è molto più utile di:
step 12
Per EPUB:
pages
assets
bytes
mode
elapsed
possono essere raccolti come struttura, non solo stampati.
Quindi non possiamo trattare logging come in un toy project.
Preferiamo:
course code
operation id
state name
error class
non:
token
password
raw private payload
Può cambiare:
DOM
API
route
response timing
Possono cambiare:
extension lifecycle
API support
manifest rules
Possono cambiare:
policy
review scanner
metadata requirements
Ogni integrazione esterna dovrebbe avere:
boundary
normalization
validation
fallback/test
Chiedi:
“come ce ne accorgeremo quando cambierà?”
contract tests
artifact validator
source assertions
live smoke test
clear error classification
PlumePilot deve affrontare:
multiple worlds
multiple browsers
multiple universities
asynchronous DOM
store policies
browser difference → build adapter
platform difference → DOM/API adapter
UI difference → controller
state persistence → storage helper
Quando la stessa eccezione appare in 10 file:
complessità diffusa
è molto più difficile da mantenere.
Senza pattern:
bug A → patch A
bug B → patch B
bug C → patch C
DOM can be stale after await
spiega e previene un’intera classe di bug.
Una piccola quantità di concetti che permette di ragionare su molti casi.
1. Boundary espliciti
2. Identity logica stabile
3. Source hierarchy
4. Unknown come stato
5. Fail closed sui side effect rischiosi
6. Progressive refinement
7. Lifetime coerente
8. Ownership chiara
9. Messaging come protocollo
10. Revalidation dopo await
11. State machines
12. Dedup/idempotence
13. Snapshot + event
14. Cache non authoritative
15. Bounded growth
16. Cooperative cancellation
17. Working-set awareness
18. Backpressure
19. Graceful degradation
20. Semantic parity
21. Shared semantics / separate adapters
22. Derived presentation state
23. Executable invariants
24. Artifact thinking
25. Contract drift preparedness
master index
→ source hierarchy
course/chapter identity
→ stable identity
API detail
→ progressive refinement
unknown percentage
→ ternary knowledge
ambiguous route
→ fail closed
generation token
→ async revalidation/cancel
Una sola feature usa quasi metà dei pattern.
PDF.js input
→ external boundary
page mode choice
→ progressive refinement
asset hash map
→ identity/dedup
canvas cleanup
→ ownership/lifetime
yieldToBrowser
→ cooperative scheduling
AbortSignal
→ cancellation
ZIP
→ artifact thinking
shared presentation rules
→ semantic parity
separate controllers
→ lifecycle-aware adapters
storage onChanged
→ snapshot + event
autoplayPresentation
→ derived state / functional core
active operation invariant
→ safety property
domain: play sound
→ shared semantics
offscreen/tab
→ compatibility adapter
manifest specialization
→ release compiler
replaceExactly
→ fail closed
checksums
→ artifact identity
Potremmo immaginare:
src/
├── domain/
│ ├── playback/
│ ├── discovery/
│ ├── operations/
│ └── achievements/
├── platform/
│ ├── multiversity-api/
│ ├── multiversity-dom/
│ └── browser-extension/
├── ui/
│ ├── shared/
│ ├── popup/
│ └── floating/
├── export/
│ ├── pdf/
│ ├── epub/
│ └── quiz/
└── infrastructure/
├── storage/
├── messaging/
└── diagnostics/
Non è una proposta da applicare subito.
È una mappa concettuale.
C’è un rischio opposto:
vedere pattern
→ creare 40 layer
→ rallentare ogni feature
Un modulo nuovo ha costo:
naming
imports
ownership
navigation
API interna
Astrarre quando una relazione è stabile e ripetuta, non quando possiamo immaginare che forse un giorno lo sarà.
PlumePilot deve realmente gestire:
host esterno
async DOM
browser extension contexts
cross-browser
Non possiamo eliminarli.
Possiamo invece ridurre:
duplicazione UI
state duplicato
browser conditions sparse
magic sleeps
ambiguous identity
manual packaging
Riduce accidental complexity senza fingere che essential complexity non esista.
Due componenti sono accoppiati se una modifica in uno richiede di conoscere dettagli dell’altro.
Se una funzione dipende da:
esatto nesting
classi visuali
accordion aperto
è fortemente accoppiata al sito.
content logic
→ sections()/chapterRows()
→ host DOM
Esempio ideale:
sound-settings.js
raccoglie normalizzazione e definizione delle preferenze audio.
content.jsÈ invece un file molto grande con molte responsabilità.
Questo non significa automaticamente:
bad code
ma segnala che il prossimo stadio di maturità potrebbe essere una decomposizione per coesione.
Un file lungo può essere leggibile.
API access
DOM parsing
autoplay
export
cache
discovery
recovery
molti test devono estrarre porzioni con source.slice().
Questo indica che responsabilità logiche già distinguibili potrebbero diventare moduli.
Quando per testare una funzione dobbiamo iniettare 25 global:
coupling high
input → output
il test è banale.
Ma usare la difficoltà di test come segnale di design.
DOM host
store policy
browser API detail
“trova prima attività incompleta” semantics
operation lifecycle
user preferences meaning
export intent
Le parti più instabili dovrebbero stare dietro boundary più chiari.
Meglio:
discovery algorithm
→ normalized course model
che:
discovery algorithm
→ querySelector con classi LMS
In un integration product:
API timeout
missing DOM
stale cache
unsupported API
sono normali stati operativi.
Il progetto migliora quando distingue:
retryable
fallback-able
cancelled
ambiguous
fatal
build invariant
Non da un generico:
catch { return null; }
null può voler dire troppe cosenot found
not ready
unknown
error
not applicable
Meglio usare risultati strutturati:
{ status: "target", ... }
{ status: "end" }
{ status: "cancelled" }
{ status: "fallback" }
JavaScript non impone union types, ma possiamo comunque modellare stati espliciti.
true/false è spesso troppo poveroPer una operation:
accepted
rejected
cancelled
duplicate
not owner
sono stati diversi.
{
accepted: false,
reason: "duplicate"
}
è molto più utile.
load state
→ apply state
→ attach listeners
oppure:
menu-ux.js
→ floating-menu.js
Con:
manifest order
test
API init function
non lasciarlo implicito.
Sono comode.
Ma possono rendere implicite dipendenze come:
function A requires variable busy
function B requires playbackContext
Raggruppare state per subsystem:
playbackState
exportState
commissionState
può rendere ownership e test più chiari.
Quando due funzioni cambiano sempre insieme:
probabile stessa responsabilità
Quando una parte del file cambia per motivi completamente diversi:
candidate module boundary
Se ogni fix autoplay modifica anche:
EPUB code
sarebbe un segnale preoccupante.
abbiamo una misura qualitativa di modularità.
accordion bug
→ DOM identity assumption
hidden tab bug
→ time assumption
master 100 bug
→ authority assumption
store rejection
→ artifact/source equivalence assumption
Ogni regressione importante ci dice quale dimensione architetturale non avevamo modellato esplicitamente.
Quando EPUB consuma troppa memoria, scopriamo che:
ownership dei buffer
lifetime dei canvas
parallelism
non erano dettagli.
readable vendor
source submission
build transform
validator
sono nati da un vincolo esterno.
Ma migliorano anche:
provenance
maintainability
reproducibility
Manifest V3, AMO e browser differences hanno costretto il progetto a esplicitare:
lifetime
packaging
permissions
adapter
Cache:
+ performance
- memory/staleness
Squash merge:
+ clean main
- ancestry compatibility
Shared UI:
+ consistency
- abstraction complexity
Non pretende:
“questa è la soluzione migliore in assoluto”
spacing
label
helper name
storage schema
extension ID
permissions
public URL
message protocol usato ovunque
È una regola pratica molto utile.
parallelizzare tutto
ottimizza wall time locale.
Ma può peggiorare:
memory
browser responsiveness
server pressure
Non soltanto alla funzione più veloce.
Se l’utente seleziona manualmente un’altra lezione:
manual intent wins
rispetto a una continuation vecchia dell’autoplay.
“Trova prima attività incompleta” serve a localizzare.
Non deve trasformarsi in:
risolvi automaticamente test
anche se auto-test è attivo altrove.
Quando automatizziamo, l’intento esplicito più recente dell’utente ha priorità sulle automazioni già pianificate.
È un assistant layer sopra una piattaforma che l’utente continua a controllare.
automation should assist
not seize control
Questo influenza:
cancel
manual navigation
stop rules
notifications
fallback UI
Leggere progress:
low risk
Cliccare una lezione:
medium risk
Inviare risposte a un test:
higher semantic risk
risk ↑
→ evidence requirement ↑
read-only action
→ heuristic may be enough
navigation
→ verify identity
submission
→ require authoritative context
Il debugging più efficace non parte da:
quale riga è sbagliata?
ma:
quale modello architetturale è stato violato?
Se un bug appare dopo await:
prima ipotesi:
stale state / temporal validity
non:
aggiungiamo un delay
“questo è un identity problem”
è più utile di:
“quel capitolo non funziona”
boundary
source of truth
identity
lifetime
ownership
state machine
fallback
invariant
adapter
artifact
comprime conversazioni lunghe.
Per decisioni importanti potremmo usare piccoli ADR:
docs/adr/001-course-identity.md
002-firefox-build-specialization.md
003-operation-ownership.md
Context
Decision
Alternatives
Consequences
Questo libro già sta svolgendo in parte quella funzione.
Ma ADR più piccoli vicino al repository renderebbero le decisioni operative più facili da trovare.
popup → background
non spiega perché.
popup è ephemeral
background coordina operation globali
storage persiste across lifecycle
Ora il diagramma diventa una decisione.
flowchart TB
USER[Utente]
LMS[Multiversity UI]
API[Multiversity API]
MAIN[MAIN-world integration]
ISO[Isolated content / bridge]
BG[Background coordinator]
STORE[(chrome.storage.local)]
POP[Popup]
FLOAT[Floating menu]
BUILDERS[PDF / EPUB / Quiz builders]
RELEASE[Release compiler]
USER --> POP
USER --> FLOAT
USER --> LMS
LMS <--> MAIN
API <--> MAIN
MAIN <--> ISO
ISO <--> BG
BG <--> STORE
POP <--> STORE
FLOAT <--> STORE
BG --> BUILDERS
BUILDERS --> USER
RELEASE --> CHROME[Chrome artifact]
RELEASE --> EDGE[Edge artifact]
RELEASE --> FIREFOX[Firefox artifact]
API/DOM
↓
normalization
↓
cache/index
↓
discovery decision
↓
verified target
↓
DOM effect
UI intent
↓
background coordinator
↓
persisted operation
↓
builder/source tab
↓
status events
↓
UI convergence
source commit
↓
target specialization
↓
artifact validation
↓
checksum
↓
store
where information moves
who decides what happens next
Il background spesso coordina control flow di operazioni.
Il MAIN/content layer coordina playback navigation.
Storage distribuisce data state.
Possiamo leggere:
background / settings / operation state
come una forma di control plane.
actual DOM navigation
PDF bytes
EPUB assets
API payloads
Ma aiuta a capire perché il background non deve fare DOM work.
content.js size
implicit global state
source-slicing tests
message types sparsi
multiple caches
manual release coordination
Significa sapere dove il prossimo aumento di complessità potrebbe costare di più.
Possiamo evolvere con piccoli passi:
extract pure helper
add result type
introduce adapter
centralize message constants
add test runner
Regression suite prima.
Refactor poi.
Una architecture fitness function verifica automaticamente una proprietà architetturale.
Firefox package has no offscreen
menu-ux loads before floating menu
cache remains bounded
active operation controls stay reachable
Potremmo aggiungere fitness function per:
no direct DOM selectors outside adapter
no new unbounded Map
all runtime message types declared centrally
solo se il beneficio supera il costo.
Non solo nei diagrammi.
Il test:
master 100% is not proof
è una regola architetturale sulla gerarchia delle fonti.
build-release.mjscodifica:
browser boundary
permission boundary
review boundary
platform:course
operation id
commission lease
achievement state
operation, lease, target, identity, snapshotnon sono cosmetici.
Rendono espliciti modelli.
Una lease dice:
questo tab possiede temporaneamente il diritto di fare il check
con:
expiresAt
Se il tab muore:
boolean true
potrebbe restare bloccato.
Una lease scade.
Browser extension + multiple tabs hanno già abbastanza concorrenza da rendere utile questo pattern.
tab hidden
page reload
service worker termination
network failure
Il progetto conserva abbastanza informazione per:
riprendere
oppure fermarsi in modo sicuro
A volte è meglio:
abort current attempt
let user retry
piuttosto che costruire un distributed transaction manager eccessivo.
state
migration
timeout
cleanup
edge cases
Export lungo:
più recovery/cancel support
Piccola UI transition:
può bastare retry locale
Non ha un backend developer-operated per gestire queste feature.
state locale
privacy surface ridotta
no remote job coordinator
browser memory limits
Cambierebbe radicalmente:
identity
auth
privacy
failure model
sync
cost
Quindi non è una semplice “feature tecnica”.
privacy
zero server maintenance
offline generated artifacts
low operational cost
browser resource limits
no cross-device sync
no central telemetry
harder long-running jobs
Influenza:
PDF generation location
EPUB generation location
notes persistence
logging
permissions
PlumePilot opera nella sessione dell’utente.
Questo significa che deve trattare:
token
page content
exam data
come dati sensibili.
Il bearer token intercettato resta in memoria invece di essere persistito.
È una scelta di lifetime + security.
Richiedere soltanto ciò che serve riduce:
attack surface
review complexity
user concern
Firefox non riceve offscreen se non la usa.
Questo è least privilege applicato al target.
Significa:
predictable change impact
clear boundaries
recoverable bugs
observable state
Ma oltre una certa pressione, boundaries fisici aiutano a preservare il modello.
make it work
fix real bugs
add recovery
identity
source hierarchy
state machine
cache semantics
extract modules
formalize protocols
fitness functions
PlumePilot oggi si trova fra fase 3 e fase 4 in diverse aree.
È molto esplicito:
build
validate
checksums
browser specialization
Ha molti pattern robusti ma ancora concentrati in un file grande.
La 2.35.2 ha iniziato a estrarre presentation logic condivisa.
Who owns state?
What is identity?
What is source of truth?
What is the failure model?
What can be retried?
What is persisted?
What is bounded?
How is it cancelled?
How is it tested?
Which boundary can drift?
MAIN/content playback layer.
Course + chapter + lesson context.
Player/DOM + API + cached progress.
DOM rerender, hidden tab, stale row, test boundary.
Enabled/busy/generation/manual selection.
Collapsed accordion, course order, skip API, hidden tab.
Background operation + builder page.
operationId + jobId.
Course/material API/DOM.
missing asset, PDF parse, abort, memory.
job handoff in local storage.
remove job, destroy PDF, revoke Blob URL.
Background/store coordination.
platform + exam id.
lease + serialized write.
local snapshots.
stale snapshot, duplicate check, tab closure.
Storage + local controller.
autoplayPresentation().
storage events + runtime operation query.
active operation remains visible/cancellable.
Build tooling.
commit SHA + artifact SHA + version.
browser/store.
manifest mismatch, vendor drift, review-sensitive code.
fail build for deterministic violations.
Prima di scrivere troppo codice, chiediamo:
1. Qual è l’identità delle entità?
2. Qual è la source of truth?
3. Quale dato è soltanto hint/cache?
4. Qual è il lifetime dello state?
5. Chi lo possiede?
6. Dove sono i boundary esterni?
7. Quali side effect sono rischiosi?
8. Cosa facciamo con unknown?
9. Qual è il fallback?
10. Come cancelliamo?
11. Come limitiamo crescita/memoria?
12. Come testiamo l’invariante?
Vogliamo aggiungere:
“Riprendi automaticamente l’ultima attività aperta ieri”
Definisci:
identity
storage lifetime
source of truth
staleness
manual override
Quali rischi crea una lesson identity basata soltanto sul testo?
Vogliamo introdurre una cache persistente delle lesson API.
Domande:
TTL?
monotonic fields?
platform scoping?
course version?
invalidation?
privacy?
size bound?
Quando chrome.storage.local diventerebbe peggiore della Map attuale?
Una nuova API browser permette una feature migliore soltanto su Chrome.
Progetta:
domain capability
Chrome adapter
Firefox fallback
manifest specialization
artifact test
senza introdurre if (chrome) in dieci file.
Il DOM host aggiunge una nuova university con struttura diversa.
Quali parti dovrebbero cambiare idealmente?
DOM adapter
platform config
maybe API normalizer
Quali invece non dovrebbero cambiare?
discovery semantics
operation model
export pipeline
Una feature richiede un side effect irreversibile.
Costruisci una policy:
what evidence level is required?
what ambiguity blocks execution?
what confirmation is needed?
what result is persisted?
Trovi una nuova Map globale.
Quali domande fai?
cache or registry?
key identity?
who deletes entries?
bound?
lifetime?
can service worker die?
Una funzione fa:
const row = currentLesson();
await fetchSomething();
click(row);
Individua il problema architetturale e riscrivilo come pattern, non come patch.
Un result ritorna:
false
per:
cancelled
not found
ambiguous
network error
Progetta un result type più utile.
Punto in cui cambiano ownership, trust, lifetime, API disponibili o modello dati.
Layer che traduce un modello esterno nel modello interno impedendo che dettagli del provider si diffondano nel dominio.
Identità che rimane valida durante il lifetime dell’operazione anche se la rappresentazione visuale o tecnica cambia.
Ordine esplicito di autorità fra fonti diverse dello stesso dato.
Modello in cui un fatto può essere true, false oppure unknown.
Strategia che parte da evidenza economica e aumenta il costo solo quando serve maggiore precisione.
Responsabilità di creare, modificare e rilasciare uno state o una risorsa.
Validità di un dato rispetto al tempo e ai punti di sospensione asincroni.
Rilettura/verifica dello state dopo un await prima di eseguire un side effect.
Numero/versione usato per invalidare continuazioni asincrone appartenenti a una precedente operazione.
Stesso comportamento osservabile ottenuto con meccanismi interni differenti.
State calcolabile deterministicamente da altre fonti e quindi spesso non necessario da persistere separatamente.
Proprietà per cui una struttura long-lived ha regole esplicite di limite, scadenza o eviction.
Cancellazione in cui il lavoro controlla periodicamente uno stato/AbortSignal e termina volontariamente.
Modello qualitativo o quantitativo del costo di operazioni alternative usato per scegliere fast e slow path.
Test o validator che verifica automaticamente una proprietà architetturale nel tempo.
Boundary progettato per isolare parti che cambiano spesso da parti semanticamente più stabili.
Automazione progettata per assistere l’utente mantenendo priorità all’intento manuale esplicito.
Dopo quindici capitoli possiamo descrivere PlumePilot senza nominare una singola feature specifica.
È un sistema che:
integra un host esterno instabile
attraversa più execution context
mantiene stato con lifetime differenti
coordina operazioni asincrone
usa fonti di dati con autorità differenti
protegge side effect con identity e revalidation
produce artefatti locali complessi
si adatta a più browser e store
La sua architettura reale non è un pattern unico.
È un insieme di idee ricorrenti:
boundary
identity
source of truth
unknown
progressive refinement
fail closed
ownership
lifetime
state machines
idempotence
snapshot + event
cache semantics
bounded growth
cancellation
backpressure
semantic parity
artifact thinking
executable invariants
La lezione più importante è forse questa:
L’architettura emerge dalle assunzioni che decidiamo di rendere esplicite.
Finché un’assunzione rimane nascosta:
“il DOM resterà lì”
“100% significa davvero completo”
“il background resterà vivo”
“il browser si comporterà uguale”
prima o poi diventa un bug.
Quando invece la trasformiamo in:
identity
protocol
state
invariant
adapter
test
entra nella struttura del sistema.
Ed è proprio questo il passaggio da:
codice che funziona oggi
a:
software che sappiamo far evolvere domani
Il capitolo finale sarà volutamente critico.
Non cercheremo errori da giudicare con il senno di poi.
Ci chiederemo invece:
se iniziassimo PlumePilot oggi,
con tutto ciò che abbiamo imparato,
quali decisioni prenderemmo diversamente?
Parleremo di:
modularizzazione di content.js
API interne tipizzate
message contracts
state management
DOM adapters
cache boundaries
worker/off-main-thread opportunities
test architecture
release automation
architecture documentation
ma anche di ciò che non rifaremmo:
alcune soluzioni imperfette sono state perfettamente ragionevoli per arrivare fin qui.
Il punto non sarà progettare un “PlumePilot ideale”.
Sarà capire come distinguere:
technical debt reale
da:
semplice complessità inevitabile di un prodotto vero
← 15 — Git, PR e release engineering · Indice · 17 — Cosa rifaremmo diversamente oggi? →