Sherpa: un indice di analisi statica che riduce il consumo di token AI del 60%
Come ho costruito un indice pre-calcolato del codebase che sostituisce le chiamate esplorative grep/file-read con un singolo manifest — riducendo il consumo di token del 60% sulle domande strutturali.
Ogni volta che uno strumento AI deve rispondere a una domanda strutturale — dove è definita questa funzione, chi la chiama, cosa esporta questo file — fa sempre la stessa cosa: fa una grep sul codebase, legge qualche file, ricostruisce la risposta dall’output. Tre, quattro, cinque tool call, qualche migliaio di token, e un risultato che qualsiasi compilatore TypeScript avrebbe prodotto in millisecondi.
Ho iniziato a notare questo pattern lavorando su un progetto React con circa 110 file TypeScript. Prima di toccare qualsiasi codice, Claude spendeva una porzione significativa del contesto di sessione solo a navigare — grep sui nomi delle funzioni, lettura delle definizioni dei tipi, tracciamento delle catene di import. Il lavoro di implementazione vero e proprio era più veloce dell’esplorazione che lo precedeva.
L’intuizione di fondo era semplice: questi sono lookup deterministici. Il compilatore sa già dove è definito ogni simbolo, cosa esporta, chi lo importa. Il problema è che questa conoscenza non è in un formato che l’AI può caricare come contesto.
Così ho costruito sherpa.
Cosa genera
sherpa è una CLI che esegue l’analisi statica su un codebase TypeScript o JavaScript e scrive .claude/manifest.md — un indice compatto e pre-calcolato in tre sezioni.
Exports — una riga per file, con tutto ciò che espone pubblicamente:
src/types/index.ts: Task DisplayState VolumeState ContextMenuOption Position
src/actions/tasks.ts: closeApp closeAllApps launchApp TaskActionTypes
src/reducers/index.ts: RootState default
Import Graph — chi importa ogni file, con → che indica la direzione:
src/types/index.ts → src/actions/tasks.ts src/reducers/TasksReducer.ts src/reducers/DisplayReducer.ts $lib/Common/ContextMenu/ContextMenu.tsx
src/actions/tasks.ts → src/App.tsx src/reducers/TasksReducer.ts $lib/AppDrawer/AppDrawer.tsx $lib/Desktop/Desktop.tsx ...
Symbols — una riga per simbolo esportato, con posizione nel file, tipo e firma TypeScript:
Task src/types/index.ts:1 interface
closeApp src/actions/tasks.ts:102 function (data: { _id: string }) => CloseAppAction
RootState src/reducers/index.ts:13 type
AppConfig src/config/apps.ts:5 interface
Il manifest viene caricato una volta per sessione tramite un riferimento in CLAUDE.md. Da quel momento in poi, qualsiasi domanda strutturale trova risposta leggendo poche righe da un file già in contesto — niente grep, niente letture di file.
La dimostrazione
Un esempio concreto. Task: “Voglio aggiungere una proprietà theme al tipo Task — dove devo intervenire?”
Senza sherpa — 3 tool call:
| Step | Tool | Dimensione output | Token |
|---|---|---|---|
Trova tutti i riferimenti a Task | grep -rn "Task" src/ | 6.989 char, 81 righe | ~1.747 |
| Leggi la definizione del tipo | cat src/types/index.ts | 460 char | ~115 |
| Leggi il consumer principale | cat src/reducers/TasksReducer.ts | 4.142 char | ~1.035 |
| Totale | ~2.897 |
E dopo tutto questo, la risposta richiede ancora di interpretare l’output della grep per identificare quali file usano davvero Task come tipo (rispetto ai file che contengono la stringa in un commento o in un nome di variabile).
Con sherpa — manifest già in contesto:
## Exports
src/types/index.ts: Task DisplayState VolumeState ...
## Import Graph
src/types/index.ts → src/actions/tasks.ts src/reducers/DisplayReducer.ts src/reducers/VolumeReducer.ts $lib/Common/ContextMenu/ContextMenu.tsx
## Symbols
Task src/types/index.ts:1 interface
Tre righe del manifest rispondono alla domanda completamente:
Taskè un’interfaccia definita insrc/types/index.ts:1- Il file viene importato esattamente da 4 file — questi sono tutti i posti che richiedono modifiche
- Nessuna ambiguità, nessun falso positivo dai match di stringa
| Tool call | Token | |
|---|---|---|
| Senza sherpa | 3 | ~2.897 |
| Con sherpa | 0 (manifest già caricato) | ~69 |
| Risparmio | −3 call | −97% |
Il manifest viene caricato una volta all’inizio della sessione, quindi le domande successive sullo stesso codebase non costano nulla in più.
Cinque round di ottimizzazione
La prima versione di sherpa generava un manifest in markdown standard — header per ogni file, label in grassetto, valori in backtick. Leggibile, ma costoso.
Su un progetto di ~110 file, il formato iniziale consumava 8.783 token. In cinque round di ottimizzazione, quel numero è sceso a 3.375.
| Round | Modifica | Token | Δ |
|---|---|---|---|
| 0 | Markdown originale (### header, grassetto, `backtick`) | 8.783 | — |
| 1 | Formato compatto — una riga per entry, nessun overhead di markup | 6.781 | −23% |
| 2 | Filtra i file barrel index.ts e le costanti stringa-letterale dall’output | 6.015 | −11% |
| 3 | Rimuovi le righe ← ridondanti + alias di percorso ($lib/ per prefissi lunghi) | 3.914 | −35% |
| 4 | Filtra i file default-only dagli Exports + correggi perdite di percorso assoluto nelle firme | 3.375 | −14% |
| Totale | −62% |
Alcune note su cosa ha eliminato ogni round:
Round 1 — Il formato originale aveva tre righe per simbolo (### name, - **file:**, - **kind:**, - **signature:**, riga vuota). Collassando ogni voce su una singola riga separata da spazi, il file è passato da 1.134 righe a 329.
Round 2 — I file barrel (index.ts che ri-esportano solo default) aggiungevano entry alla Export Map senza informazioni univoche — il Symbol Index già elencava il componente con il suo nome reale. Lo stesso per le costanti stringa delle action Redux ("CLOSE_APP") — erano nel Symbol Index come const con firma stringa letterale, ma il lettore non ne ricavava alcun valore.
Round 3 — L’Import Graph aveva sia ← (cosa importa un file) sia → (chi lo importa). Sono gli stessi dati da due prospettive — se A → B, allora B ← A. Eliminare ← ha dimezzato la sezione senza perdita di informazioni. L’alias di percorso ha sostituito il prefisso di 23 caratteri src/components/library/ (presente 276 volte) con $lib/.
Round 4 — I file che esportano solo default apparivano ancora nella Export Map anche quando non erano file barrel. Dato che il Symbol Index ha già il componente con il suo nome leggibile (es. Calculator invece di default), l’entry nella Export Map era ridondante. Corretta anche una particolarità del compilatore TypeScript dove i percorsi assoluti locali comparivano nelle firme: import("/percorso/assoluto/file").AppConfig[] diventava AppConfig[].
Come installarlo
sherpa è distribuito via GitHub — nessun registry npm necessario.
pnpm add -g github:Giovagni/sherpa
Poi, dalla root del progetto:
sherpa init
Questo fa quattro cose:
- Esegue un’analisi completa e scrive
.claude/manifest.md - Aggiunge
.claude/manifest.mde.claude/manifest.cache.jsonal.gitignore— il manifest è solo locale, non viene mai committato - Installa un git post-commit hook che esegue
sherpa generatedopo ogni commit - Stampa lo snippet da aggiungere al tuo
CLAUDE.md
Aggiungi lo snippet al CLAUDE.md:
## Codebase Index
See @.claude/manifest.md for symbol definitions, exports, and dependency graph.
Run `sherpa init` once to generate it locally (gitignored — each developer generates their own).
Il prefisso @ dice a Claude Code di caricare il file come contesto all’inizio della sessione.
Per le esecuzioni successive, l’analisi incrementale ri-analizza solo i file modificati:
sherpa generate # incrementale — ~20ms se nulla è cambiato, ~1–3s altrimenti
sherpa generate --full # forza una ri-analisi completa
sherpa watch # osserva i file e rigenera automaticamente ad ogni modifica
sherpa stats # mostra il conteggio dei token e la dimensione
Per definire alias di percorso personalizzati, crea sherpa.config.json nella root del progetto:
{
"aliases": {
"$lib/": "src/components/library/",
"$api/": "src/services/api/"
}
}
Senza file di configurazione, sherpa usa di default $lib/ per src/components/library/.
Limitazioni oneste
Alcune cose da sapere prima di adottarlo:
Solo TypeScript e JavaScript. Non c’è supporto per altri linguaggi. I file JavaScript funzionano ma senza informazioni di tipo — le firme degradano a tipi inferiti.
L’analisi incrementale costa 1–3 secondi sui file modificati. Il caso 0-modifiche è istantaneo (nessun compilatore TypeScript — pura lettura della cache). Ma quando i file cambiano, sherpa costruisce un mini progetto ts-morph per quei file e i loro import diretti. L’avvio del compilatore TypeScript è inevitabile in questa architettura.
sherpa watch ha limitazioni su Linux. Usa fs.watch({ recursive: true }), che funziona in modo affidabile su macOS e Windows ma ha problemi noti su Linux (inotify, nessun supporto NFS).
I pacchetti di terze parti non sono indicizzati. Vengono tracciati solo gli import locali. Le dipendenze npm non compaiono nel manifest.
Il sorgente è su github.com/Giovagni/sherpa.