← blog Read in English
IT 7 min read

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.

aiclaudetoolingtypescriptproductivity

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:

StepToolDimensione outputToken
Trova tutti i riferimenti a Taskgrep -rn "Task" src/6.989 char, 81 righe~1.747
Leggi la definizione del tipocat src/types/index.ts460 char~115
Leggi il consumer principalecat src/reducers/TasksReducer.ts4.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:

  1. Task è un’interfaccia definita in src/types/index.ts:1
  2. Il file viene importato esattamente da 4 file — questi sono tutti i posti che richiedono modifiche
  3. Nessuna ambiguità, nessun falso positivo dai match di stringa
Tool callToken
Senza sherpa3~2.897
Con sherpa0 (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.

RoundModificaTokenΔ
0Markdown originale (### header, grassetto, `backtick`)8.783
1Formato compatto — una riga per entry, nessun overhead di markup6.781−23%
2Filtra i file barrel index.ts e le costanti stringa-letterale dall’output6.015−11%
3Rimuovi le righe ridondanti + alias di percorso ($lib/ per prefissi lunghi)3.914−35%
4Filtra i file default-only dagli Exports + correggi perdite di percorso assoluto nelle firme3.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.md e .claude/manifest.cache.json al .gitignore — il manifest è solo locale, non viene mai committato
  • Installa un git post-commit hook che esegue sherpa generate dopo 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.