Assistente indipendente per Pegaso, Mercatorum e San Raffaele
Una delle funzioni apparentemente più semplici di PlumePilot pone in realtà un problema algoritmico interessante:
Dato un corso composto da molti capitoli e molte attività, come troviamo la prima attività realmente incompleta senza aprire e controllare tutto?
La risposta ingenua è facile:
apri capitolo 1
controlla tutte le attività
se è tutto completo → capitolo 2
controlla tutte le attività
...
Funziona.
Ma può essere molto costoso.
Ogni capitolo può richiedere:
In un corso lungo, la domanda quindi non è soltanto:
Qual è il primo elemento con progresso < 100%?
È piuttosto:
Qual è il modo più economico per individuare il primo candidato corretto, sapendo che le nostre fonti possono essere incomplete, stale o ambigue?
Questo capitolo è uno studio di algoritmi applicati.
Non parleremo di quicksort o alberi bilanciati in astratto. Vedremo invece concetti come:
Supponiamo che un corso contenga N moduli.
Ogni modulo contiene attività ordinate:
Obiettivi
Video 1
Video 2
...
Test finale
Vogliamo trovare la prima attività incompleta secondo l’ordine del corso.
Formalmente, possiamo immaginare una sequenza:
A1, A2, A3, ..., Am
con una funzione:
complete(Ai) ∈ {true, false, unknown}
Il risultato che cerchiamo è:
min i tale che complete(Ai) = false
Il problema è che conoscere complete(Ai) non è gratis.
Per alcune attività possiamo avere un’indicazione aggregata economica.
Per altre dobbiamo interrogare un endpoint dettagliato.
Per altre ancora dobbiamo aprire il capitolo e osservare il DOM.
Quindi abbiamo un secondo problema:
In quale ordine conviene spendere il costo delle verifiche?
La soluzione più semplice è una scansione lineare.
Pseudo-codice:
for (const chapter of course) {
open(chapter);
for (const activity of chapter.activities) {
if (!isComplete(activity)) return activity;
}
}
Dal punto di vista algoritmico classico, la complessità è:
O(N)
se consideriamo ogni capitolo come una unità.
Ma questa notazione nasconde la parte realmente costosa.
Nel browser:
1 iterazione
non significa necessariamente:
1 operazione CPU economica
Può significare:
click DOM
→ rendering asincrono
→ attesa
→ richiesta di rete
→ parsing JSON
→ altra attesa
Per PlumePilot è quindi più utile distinguere:
costo CPU
costo DOM
costo rete
costo di sincronizzazione
Una scansione O(N) che esegue N chiamate HTTP può essere molto più lenta di una scansione O(N) che legge un singolo array già disponibile in memoria.
Questa distinzione tornerà spesso nel libro:
La Big-O descrive la crescita; non descrive da sola il costo reale delle operazioni.
Una versione iniziale del problema può essere risolta direttamente dal DOM.
Nel codice attuale esiste ancora una funzione che rappresenta bene questa idea:
function firstVideoInChapter(identity, incompleteOnly = false) {
const chapter = findChapter(identity);
if (!chapter) return null;
return (
chapterRows(chapter).find((row) => {
const name = lessonName(row).toLowerCase();
if (!name || name === "obiettivi") return false;
const progress = getProgress(row);
return !incompleteOnly || progress < 100;
}) || null
);
}
File: content.js, area ~1417.
La funzione usa Array.find().
Questa scelta è significativa.
Non vogliamo tutti i video incompleti.
Vogliamo il primo.
Perciò:
filter()
sarebbe concettualmente:
controlla tutto → costruisci un nuovo array → prendi il primo
mentre:
find()
fa short-circuit appena trova il primo match.
Schema:
video 1 → 100% → continua
video 2 → 100% → continua
video 3 → 42% → STOP
video 4 → non serve leggerlo
È una piccola ottimizzazione, ma esprime anche meglio l’intento dell’algoritmo.
La ricerca DOM ha però un limite strutturale.
Per leggere le righe di un capitolo, quel capitolo deve spesso essere disponibile/renderizzato.
Quindi una ricerca globale diventa:
capitolo 1
↓
apri
↓
render
↓
verifica
↓
capitolo 2
↓
apri
↓
render
↓
verifica
...
Se la prima attività incompleta è al capitolo 30, potremmo aver interagito inutilmente con 29 capitoli.
Questo fu esattamente uno dei tipi di regressione osservati nello sviluppo: la funzione poteva arrivare correttamente al target, ma aprendo capitoli intermedi che avrebbero dovuto essere saltati.
La correttezza del risultato non basta.
Conta anche il percorso usato per raggiungerlo.
Un database raramente legge tutte le righe di una tabella per rispondere a ogni query.
Usa indici.
Possiamo applicare lo stesso modello mentale.
Immaginiamo due livelli:
COURSE INDEX
--------------------------------
Modulo 1 100%
Modulo 2 100%
Modulo 3 80%
Modulo 4 100%
Modulo 5 20%
DETAIL
--------------------------------
Modulo 3
Obiettivi 100%
Video A 100%
Video B 42%
Test 0%
Il primo livello non ci dice necessariamente quale attività è incompleta.
Ma può dirci:
Dove conviene guardare per prima.
È esattamente il ruolo assunto dal course/master index di PlumePilot.
getPlaybackCourseIndex()Il codice normalizza l’indice in entry del tipo:
{
displayOrder,
percentage,
masterOrder,
folderId,
id,
lpId,
title
}
Una parte importante è:
const entries = response.data.entries
.map((entry) => {
const displayOrder = Number(entry?.displayOrder);
const percentage = Number(entry?.percentage);
if (!Number.isInteger(displayOrder) || displayOrder < 1) {
return null;
}
return {
displayOrder,
percentage: Number.isFinite(percentage) ? percentage : 0,
...
};
})
.filter(Boolean)
.sort((first, second) => first.displayOrder - second.displayOrder);
File: content.js, area ~5760–5822.
L’indice produce quindi una struttura ordinata e relativamente economica da consultare.
Per esempio:
[
{ displayOrder: 1, percentage: 100 },
{ displayOrder: 2, percentage: 100 },
{ displayOrder: 3, percentage: 82 },
{ displayOrder: 4, percentage: 100 },
{ displayOrder: 5, percentage: 35 }
]
A questo punto una domanda naturale è:
Perché non prendere semplicemente il primo
<100%e fermarsi?
Per un periodo l’algoritmo si è avvicinato molto a questo modello.
Poi l’esperienza reale ha mostrato che sarebbe stato troppo forte come assunzione.
Supponiamo che l’indice dica:
Modulo 1 → 100%
Modulo 2 → 100%
Modulo 3 → 80%
È ragionevole controllare prima il modulo 3.
Ma da questo non segue logicamente che:
Modulo 1 e 2 sono impossibili da considerare ancora
Perché il dato aggregato potrebbe essere:
Il modello corretto è quindi:
percentage < 100
↓
forte candidato
≠
prova definitiva
Questo porta a una distinzione fondamentale.
Fonte che consideriamo autorevole per una certa decisione.
Struttura che ci permette di trovare rapidamente dove cercare.
Segnale che migliora l’ordine della ricerca senza garantirne da solo il risultato.
Nel design attuale, la percentuale master agisce soprattutto come indice/euristica di prioritizzazione.
Nel codice corrente troviamo:
let discoveryOrder = outline
.map((_entry, index) => index)
.filter((index) =>
index >= firstIndex &&
!(options.verifiedCompleteChapters || []).some((identity) =>
sameChapterIdentity(outline[index].identity, identity)
)
);
Poi:
const suspectedIncomplete = (index) => {
const route = routeByLessonNumber.get(outline[index].lessonNumber);
return !route || Number(route.percentage) < 100;
};
discoveryOrder = [
...discoveryOrder.filter(suspectedIncomplete),
...discoveryOrder.filter((index) => !suspectedIncomplete(index)),
];
File: content.js, area ~5894–5911.
Questa operazione merita di essere letta con calma.
Partiamo da:
[1, 2, 3, 4, 5, 6]
Supponiamo:
1 → 100%
2 → 100%
3 → 80%
4 → 100%
5 → 40%
6 → 100%
Il nuovo ordine diventa:
[3, 5, 1, 2, 4, 6]
Non abbiamo eliminato i capitoli al 100%.
Li abbiamo spostati nella seconda passata.
Questo è concettualmente simile a una stable partition:
prima i candidati sospetti
poi gli altri
mantenendo però l’ordine originale all’interno dei due gruppi.
Quindi:
80% prima di 40%?
No.
La percentuale non diventa un ranking numerico.
Il criterio è soltanto:
sospetto incompleto?
poi viene mantenuto l’ordine del corso.
Questo è essenziale, perché cerchiamo la prima attività incompleta in ordine didattico, non quella con percentuale più bassa.
Potrebbe sembrare naturale fare:
chapters.sort((a, b) => a.percentage - b.percentage);
Ma sarebbe sbagliato.
Esempio:
Modulo 3 → 80%
Modulo 5 → 20%
La prima attività incompleta può essere nel modulo 3.
Se ordinassimo per percentuale visiteremmo prima il modulo 5, violando la semantica della funzione.
Il requisito non è:
Trova il capitolo meno completo.
È:
Trova la prima attività incompleta secondo l’ordine del corso.
Quindi l’indice serve a ridurre il lavoro, non a cambiare l’ordinamento semantico del dominio.
Il codice dice esplicitamente:
// Prefer the master's unfinished candidates, keeping course order within
// each pass. A second pass verifies 100% summaries if no candidate matches.
Questa frase descrive l’evoluzione architetturale dell’algoritmo.
Prima:
100% master → skip definitivo
può essere molto veloce.
Ma se il master è stale può produrre un false negative:
attività realmente incompleta
ma classificata come completa
→ mai controllata
La soluzione attuale è:
PASS 1
candidati <100% o senza mapping
se nessun target valido
↓
PASS 2
entry aggregate al 100%
Quindi l’indice riduce il tempo medio senza sacrificare la completezza nel caso peggiore.
Possiamo rappresentare la ricerca come una piramide.
DOM visuale
costo più elevato
▲
│
lesson detail API
▲
│
course/master index
▲
│
cache / stato locale
costo più economico
L’algoritmo cerca di salire nella piramide soltanto quando necessario.
Questo pattern può essere chiamato progressive refinement:
È molto simile a ciò che succede in molti sistemi reali:
indice DB → row lookup
cache → database
metadata → file completo
search index → document retrieval
bounding box → collision detection precisa
Per ogni candidato, PlumePilot può chiedere dati dettagliati:
const lesson = await requestLessonWithRetry(
courseCode,
entry.lessonNumber,
"playback",
null,
null,
lessonLpId,
false,
indexedChapter?.id || entry.lessonNumber,
true,
);
Poi esamina:
Obiettivi
video
progressItems
test
Il target può essere:
const unfinishedItem =
!options.forwardVideoSearch && objectivePending
? { ...objective, contentType: "intro" }
: playbackItems.find(
(item) =>
item.contentType === "video" &&
Number(item.percentage) < 100
);
Questa è una seconda ricerca lineare, ma su un insieme molto più piccolo: le attività del singolo modulo.
La struttura generale diventa:
course index
↓
chapter candidate
↓
lesson detail
↓
activity candidate
found/not foundUna funzione ingenua potrebbe restituire:
return activity || null;
L’algoritmo reale deve rappresentare più stati.
Per esempio:
target
end
cancelled
fallback
E un target può contenere metadati come:
{
status: "target",
entry,
unfinishedItem,
testTarget,
visualVerification,
verifiedActivities,
lessonLpId,
lessonParagraphId
}
Perché?
Perché:
abbiamo trovato il capitolo
non implica necessariamente:
sappiamo esattamente quale riga DOM cliccare
Il risultato dell’algoritmo trasporta quindi anche il grado di conoscenza disponibile.
In molti punti del progetto è utile pensare al progresso come:
complete
incomplete
unknown
non semplicemente:
true / false
Questa distinzione appare chiaramente in:
if (percentage === null || !Number.isFinite(percentage)) {
return { contentType: "unknown", title };
}
File: content.js, visibleIncompleteActivity().
Se una percentuale manca, non dobbiamo trasformarla automaticamente in:
0%
perché:
non so
non significa:
so che è incompleto
Questa è una lezione molto generale.
const percentage = value || 0;
Se value non è disponibile, stiamo convertendo l’assenza di informazione in una informazione precisa.
L’algoritmo attuale evita proprio questo in molti passaggi critici.
Due categorie di errore aiutano a ragionare sulla ricerca.
L’algoritmo dice:
questa attività è incompleta
ma in realtà è completa.
Effetto possibile:
apertura inutile
L’algoritmo dice:
questa attività/capitolo può essere saltato
ma contiene qualcosa di incompleto.
Effetto possibile:
la funzione restituisce un target successivo
oppure “tutto completo” erroneamente
Per Trova prima attività incompleta, il false negative è normalmente più grave.
Per questo la strategia attuale preferisce:
incertezza
→ verifica in più
piuttosto che:
incertezza
→ skip definitivo
Se una lesson response rimane incompleta dopo i retry, PlumePilot non ricomincia necessariamente da capo.
Nel codice:
if (!lesson.ok) {
return {
status: "target",
entry,
unfinishedItem: null,
visualVerification: true,
};
}
File: content.js, area ~5979–5995.
Questo è molto importante.
L’algoritmo ha già imparato qualcosa:
questo è il primo candidato che non posso escludere in sicurezza
Quindi non fa:
API fallita
→ cancella tutta la ricerca
→ riparti dal capitolo 1
Fa:
API incerta sul modulo 12
→ apri esattamente modulo 12
→ verifica visualmente
Questo è un targeted fallback.
errore modulo 12
↓
riparti da modulo 1
↓
ricontrolla 1...11
Costo alto e lavoro duplicato.
errore modulo 12
↓
apri modulo 12
↓
verifica
Il secondo approccio preserva il lavoro già svolto.
È lo stesso principio usato in sistemi distribuiti e pipeline robuste:
Quando una fase fallisce, conserva quanto più possibile dell’informazione già verificata.
verifiedCompleteChaptersSe la verifica visuale apre un candidato e scopre che in realtà è completo, non dobbiamo ricominciare da capo.
Nel codice:
verifiedCompleteChapters: [
...(options.verifiedCompleteChapters || []),
nextIdentity,
]
Poi la costruzione del discoveryOrder li esclude:
!(options.verifiedCompleteChapters || []).some((identity) =>
sameChapterIdentity(outline[index].identity, identity)
)
Questo è un piccolo esempio di memoization negativa nel workflow:
questo capitolo è già stato verificato completo
→ non spendere nuovamente il costo
Non è una cache globale persistente.
È memoria locale della singola ricerca.
Una volta trovato un candidato:
nextIdentity = apiDiscovery.entry.identity;
PlumePilot deve ancora trasformarlo in un’azione sulla pagina.
Il percorso è:
```textcandidate identity ↓ openChapter() ↓ wait rendering ↓ reacquire chapter ↓ select exact activity
Perché il DOM può cambiare tra un passo e l'altro.
Il codice infatti aspetta:
```js
let chapterReady = await waitFor(() => {
const fresh = findChapter(nextIdentity);
if (!fresh) return null;
const rows = chapterRows(fresh);
return rows.length > 0 ? fresh : null;
});
Questa parte collega direttamente il Capitolo 8 al Capitolo 3.
L’algoritmo di ricerca può essere perfetto, ma l’esecuzione finale deve comunque sincronizzarsi con una UI dinamica.
Durante il bookmark search il codice sceglie fra due strategie.
Se l’API ha identificato precisamente l’attività:
const activity = apiUnfinishedItem || ...;
Se invece serve verifica visuale:
visibleIncompleteActivity(nextIdentity, apiVerifiedActivities)
Questo produce una cosa interessante:
API
↓
fornisce evidenza
↓
DOM
↓
conferma / completa l'informazione
Il DOM non sostituisce necessariamente l’API.
E l’API non sostituisce necessariamente il DOM.
Le due fonti vengono combinate.
Il commento nel codice è particolarmente istruttivo:
// Missing sidebar percentages are unknown, not 0%. A verified API
// target must not be replaced by a completed Obiettivi row in the DOM.
Questo introduce un principio molto generale:
Quando combini fonti multiple, devi definire una gerarchia di affidabilità.
Esempio:
API verificata: Video B = 42%
DOM incompleto: percentuale mancante
Non dobbiamo trasformare la seconda informazione in:
DOM dice 0%, quindi scegli altro
né permettere a un elemento visuale ambiguo di cancellare un target già verificato.
Possiamo chiamarlo:
evidence precedence.
Il capitolo può contenere:
Obiettivi
video
Test
La ricerca deve rispettare le preferenze dell’utente.
Per il bookmark corrente, il test pending può diventare direttamente un target:
const testNeedsAttention =
Boolean(test) &&
(options.bookmarkSearch
? pendingTest
: ...);
Poi:
if (activity?.contentType === "test") {
const test = getEndOfLessonTest(chapterReady);
...
test.scrollIntoView(...);
return true;
}
Quindi il risultato non è sempre:
clicca un video
Può essere:
porta l'utente al test
Questo è un esempio di ricerca su elementi eterogenei.
La funzione cerca una proprietà comune:
richiede attenzione
ma l’azione finale dipende dal tipo.
All’avvio:
smartResumeRunning = true;
const generation = ++smartResumeGeneration;
Poi viene definita:
const isCancelled = () =>
generation !== smartResumeGeneration ||
courseCode !== courseCodeFromUrl() ||
!enabled ||
busy;
File: content.js, startFirstIncompleteDiscovery().
Questo è un generation token.
Supponiamo:
Ricerca A parte
↓
await API
↓
l'utente cambia stato
↓
parte Ricerca B
Quando A riprende, non deve più essere autorizzata a modificare la pagina.
Confronta:
generation di A
con:
generation corrente
Se differiscono:
A è stale
→ stop
Questo è molto simile a un optimistic concurrency token.
smartResumeRunningUn booleano dice:
c'è una ricerca in corso?
Ma non identifica quale ricerca.
Il generation counter aggiunge identità temporale:
run 17
run 18
run 19
Così una callback della run 17 non può accidentalmente diventare valida durante la run 19.
Schema:
boolean lock
→ evita duplicati simultanei
generation token
→ invalida continuazioni asincrone vecchie
Sono due problemi diversi.
Il changelog mostra una prima forma di smart resume:
outline cached
→ skip capitoli/video completi
→ considera Obiettivi
→ trova primo video <100%
Era già migliore di una navigazione completamente cieca.
Ma dipendeva ancora molto dalla sequenza di controlli dettagliati e dalla UI.
Arriva il course completion index.
L’idea:
indice aggregato
→ evita detail request per moduli sicuramente poco interessanti
Il changelog descrive:
Qui nasce chiaramente il modello:
index → candidate → detail/fallback
La funzione cambia semantica di prodotto.
Prima:
Inizia dalla prima attività incompleta
era una preferenza persistente.
Poi diventa:
Trova prima attività incompleta
un’azione esplicita.
Questo cambia anche il modello algoritmico.
Non è più:
policy automatica ad ogni startup
ma:
query dell'utente
→ cerca dall'inizio
→ porta esattamente al risultato
È quasi un bookmark intelligente.
Per ridurre ancora il lavoro, il master index viene promosso a strumento principale per individuare il candidato.
Concettualmente:
find first percentage < 100
→ map to visible chapter
→ open exact target
Molto efficiente.
Ma l’esperienza successiva porta a una correzione importante.
La versione corrente fa qualcosa di più prudente:
1. forza refresh del master
2. costruisce outline
3. crea discovery order
4. mette prima i <100%
5. verifica i candidati
6. se necessario controlla anche i 100%
7. usa detail API
8. usa targeted visual fallback quando serve
Questa è probabilmente la forma più interessante dal punto di vista didattico.
L’ottimizzazione non è stata rimossa.
È stata resa reversibile.
Se l’euristica funziona:
molto veloce
Se l’euristica sbaglia:
seconda passata
→ correttezza preservata
Con N capitoli:
N aperture DOM
N attese rendering
potenzialmente N detail request
Possiamo rappresentare il costo come:
T_naive ≈ N × (DOM + render + network)
Supponiamo:
N = 40 capitoli
K = 3 candidati <100%
Il master index costa circa una sola acquisizione.
Poi:
1 master request
+ fino a K detail verification
+ 1 apertura DOM target
Nel caso favorevole:
T ≈ index + K × detail + 1 × DOM
Il guadagno può essere enorme anche se, formalmente, costruire e filtrare gli array rimane O(N).
Perché abbiamo ridotto le operazioni costose.
Nel caso peggiore:
<100% risultano completi;Allora il costo può tornare vicino a una scansione completa.
Ma questa è una proprietà desiderabile.
L’algoritmo ottimizza:
common case
senza eliminare:
correctness path
async function findFirstIncomplete(course) {
const outline = await getOutline(course);
const master = await getFreshMasterIndex(course);
const candidates = partitionStable(
outline,
chapter => masterPercentage(chapter) < 100 || !isMapped(chapter)
);
const verifiedComplete = new Set();
for (const chapter of candidates) {
if (verifiedComplete.has(chapter.id)) continue;
const detail = await getLessonDetail(chapter);
if (!detail.isReliable) {
const visual = await verifyVisually(chapter);
if (visual.target) return visual.target;
verifiedComplete.add(chapter.id);
continue;
}
const target = firstIncompleteActivity(detail);
if (target) return target;
}
return null;
}
Non è il codice reale, ma cattura l’architettura.
Senza trasformare PlumePilot in un sistema di machine learning, possiamo prendere in prestito due concetti dall’information retrieval.
Fra i candidati che decidiamo di controllare, quanti sono davvero incompleti?
Fra tutti i capitoli realmente incompleti, quanti riusciamo a non perdere?
Un indice aggressivo potrebbe avere:
alta precision
basso costo
ma recall imperfetto
La seconda passata sui 100% aumenta la recall.
In altre parole:
master pass
→ velocità
verification pass
→ affidabilità
È un compromesso comune in sistemi di ricerca.
Non deve necessariamente contenere tutta la verità.
Il suo compito può essere semplicemente:
40 capitoli
↓
3 candidati da controllare subito
Non cerchiamo:
la percentuale più bassa
ma:
la prima attività incompleta
Quindi la prioritizzazione deve rispettare l’ordine didattico.
Uno dei bug più pericolosi nei sistemi reali nasce da trasformazioni come:
missing → false
missing → 0
missing → completed
Se non sappiamo qualcosa, spesso la rappresentazione più corretta è proprio:
unknown
Una buona ottimizzazione dice:
provo prima il percorso economico
non:
elimino per sempre il percorso costoso
Nel nostro caso:
master <100%
→ first pass
master 100%
→ second pass se serve
è più robusto di:
master 100%
→ mai più controllare
Quando il modulo 12 non è verificabile:
non ripartire dal modulo 1
Apri il 12.
Quando il 12 risulta completo:
memorizzalo nella ricerca corrente
Continua dal 13.
Questo riduce lavoro duplicato e rende il fallback locale.
La ricerca risponde:
dove devo andare?
L’interazione DOM risponde:
come ci arrivo?
Sono problemi diversi.
Nel codice sono ancora intrecciati in alcuni punti, ma concettualmente possiamo dividerli:
Discovery Engine
↓
Target
↓
Navigation / DOM Adapter
Questa separazione sarebbe una delle possibili evoluzioni architetturali future.
DiscoveryCandidatePotremmo immaginare un tipo esplicito:
type DiscoveryCandidate = {
chapter: ChapterIdentity;
lessonNumber: number;
masterPercentage: number | null;
confidence: "indexed" | "detailed" | "visual";
activity?: ActivityIdentity;
needsVisualVerification: boolean;
};
Questo renderebbe più esplicita la provenienza del dato.
Per esempio:
type DiscoveryResult =
| { type: "target"; candidate: DiscoveryCandidate }
| { type: "end" }
| { type: "cancelled" }
| { type: "unavailable"; reason: string };
Nel JavaScript attuale qualcosa di simile esiste già implicitamente tramite:
status: target
status: end
status: cancelled
status: fallback
La lezione non è “riscriviamolo in TypeScript”.
È:
Quando un algoritmo può terminare in modi semanticamente diversi, rappresentare esplicitamente quei risultati riduce ambiguità.
Una suite utile dovrebbe coprire almeno:
| Scenario | Master | Detail | DOM | Risultato atteso |
|---|---|---|---|---|
| primo modulo incompleto | <100 |
valido | — | target modulo 1 |
| primi 4 completi | 100,100,100,100,<100 |
valido | — | target modulo 5 |
| master stale | tutti 100 |
modulo 3 incompleto | — | target modulo 3 nella seconda passata |
| mapping mancante | entry senza route | — | valido | verifica visuale mirata |
| detail incompleto | <100 |
errore/retry esauriti | valido | verifica visuale target |
| candidato falso | <100 |
completo | completo | continua senza rescansione |
| percentuale DOM assente | <100 |
parziale | unknown | non inventare 0% |
| ricerca cancellata | qualsiasi | await in corso | — | nessuna navigazione |
| cambio corso | qualsiasi | await in corso | — | run precedente invalidata |
| test pending | <100 |
test pending | valido | evidenzia test |
Questa tabella è più utile di molti test basati soltanto sulla coverage di riga.
L’analogia più utile è una query che usa un indice.
Immagina una tabella enorme:
SELECT *
FROM activities
WHERE completed = false
ORDER BY course_order
LIMIT 1;
Se non esiste un indice adatto, il database può dover eseguire una scansione ampia.
PlumePilot affronta una situazione simile, ma con dati distribuiti fra:
master endpoint
lesson endpoint
cache locale
DOM
Il course index funziona come un coarse index.
Il detail endpoint assomiglia al fetch della row completa.
Il DOM è simile a un sistema legacy che dobbiamo interrogare come fallback quando i dati strutturati non sono sufficienti.
L’analogia non è perfetta: non abbiamo un query planner vero e proprio, né snapshot transactionally consistent.
Ma il principio è molto vicino:
Usa prima la rappresentazione più economica capace di restringere la ricerca; accedi ai dati costosi solo per i candidati.
Un indice può essere utilissimo anche se non è perfettamente autorevole.
prima controlla <100%
è più sicuro di:
non controllare mai 100%
Ridurre aperture DOM e chiamate HTTP può valere molto più di una micro-ottimizzazione su un loop JavaScript.
L’assenza di informazione deve restare rappresentabile.
Se fallisce il modulo 12, verifica il 12. Non ricominciare dal modulo 1.
verifiedCompleteChapters evita di ripetere lavoro già concluso.
Generation token e cancellation guard proteggono dalla continuazione di ricerche obsolete.
scopri target
→ naviga al target
è un modello più pulito di un’unica funzione che deve contemporaneamente decidere e manipolare la UI.
Dato:
const chapters = [
{ id: 1, percentage: 100 },
{ id: 2, percentage: 75 },
{ id: 3, percentage: 100 },
{ id: 4, percentage: 20 },
];
Costruisci senza ordinare numericamente:
[2, 4, 1, 3]
mantenendo l’ordine relativo dei due gruppi.
sort() può essere semanticamente sbagliato?Spiega perché:
chapters.sort((a, b) => a.percentage - b.percentage);
non risolve correttamente “prima attività incompleta”.
Modella il progresso come:
"complete" | "incomplete" | "unknown"
invece di un booleano.
Quali branch dell’algoritmo diventano più chiari?
Immagina 50 capitoli e un master index che riporta erroneamente tutto al 100%.
Quante detail verification può dover eseguire l’algoritmo per preservare la correttezza?
Perché questa degradazione è accettabile?
Disegna tre piani:
DOM-first
API-detail-first
master-index-first
Per ognuno annota:
Per i concetti JavaScript usati direttamente nel codice:
MDN — Array.prototype.find()
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find
MDN — Array.prototype.filter()
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
MDN — Map
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
Per il modello mentale di indice/query planning è utile studiare anche i concetti dei database relazionali, mantenendo però chiaro che PlumePilot non sta implementando un DBMS.
Ora sappiamo come PlumePilot decide quali dati recuperare e in quale ordine.
Nel Capitolo 09 cambieremo completamente tipo di problema:
Studieremo una pipeline che parte da URL e strutture del corso e arriva a file binari scaricabili:
raccolta
↓
normalizzazione
↓
fetch degli asset
↓
parsing
↓
trasformazione
↓
layout / reflow
↓
serializzazione
↓
Blob / download
e affronteremo una domanda che diventa molto concreta quando il browser deve elaborare decine o centinaia di megabyte:
Cosa significa usare il browser non come semplice interfaccia, ma come vero runtime di document generation?
← 07 — Autoplay come state machine · Indice · 09 — Generare PDF, HTML ed EPUB →