# 20260923 - Audit e correzione accuratezza statistiche Sophia

**File:** `20260923_audit_statistics_accuracy.md`  
**Data:** 2026-09-23  
**Repository target:** `sophiadeveloper/mcp-servers`  
**Snapshot analizzato:** `c6f09d9c253a12b710c4a61bd0dc519585dc98e3` (`master`)  
**Tipo documento:** piano implementativo per agenti  
**Priorità:** alta  
**Stato:** pronto per implementazione incrementale

---

## 1. Obiettivo

Correggere i problemi di accuratezza e completezza della dashboard/statistica `analytics-node`, con attenzione specifica a:

1. differenze osservate tra Windows e macOS;
2. dati incompleti o apparentemente mancanti per Codex;
3. dati incompleti o sovrastimati per Claude Code;
4. incoerenze tra KPI aggregati, grafici temporali, sessioni e modelli;
5. difficoltà nel distinguere un dato realmente non esposto dal client da un dato perso dallo scanner/parser;
6. persistenza di dati già importati con una versione precedente e non più corretta dell'adapter.

Il risultato atteso non è una riscrittura di `analytics-node`, ma un hardening incrementale della pipeline:

```text
filesystem client
  -> source discovery
  -> parser/adaptor
  -> normalizzazione
  -> persistenza SQLite
  -> aggregazione
  -> API
  -> dashboard
```

La correzione deve rendere ogni livello verificabile e deve permettere di capire dove si perde informazione.

---

## 2. Sintesi della diagnosi

L'analisi non indica un unico "bug macOS". Il comportamento osservato può derivare da più problemi indipendenti che oggi vengono confusi nello stesso sintomo.

### 2.1 Classi di problema identificate

| Classe | Stato attuale | Impatto |
|---|---|---|
| Discovery dei file | parzialmente OS-aware | file reali possono non essere trovati |
| Risoluzione home/config custom | incompleta | installazioni non standard vengono ignorate |
| Parsing Codex | avanzato ma perde metadata utili | main/subagent e surface possono essere classificati male |
| Parsing Claude | troppo permissivo e whole-file | possibili duplicazioni token/messaggi e file grandi scartati |
| Dedupe record | robusto in alcune parti Codex, assente in Claude | sovrastima statistiche |
| Semantica temporale | non uniforme tra endpoint | KPI e grafici possono descrivere periodi diversi |
| Re-import dopo fix parser | non versionato | DB già importato resta sbagliato anche dopo la patch |
| Diagnostica copertura | insufficiente | "0" non distingue assenza reale da errore pipeline |
| Test OS | copertura limitata | regressioni macOS non intercettate |

### 2.2 Decisione architetturale

Non introdurre workaround specifici del tipo:

```text
if macOS -> usa logica diversa
```

salvo che il path nativo del client sia realmente diverso.

La strategia deve essere:

```text
stesso contratto logico
+ resolver path corretti
+ fixture equivalenti Windows/macOS
+ diagnostics espliciti
+ parser indipendente dall'OS
```

---

## 3. Evidenze tecniche dal repository

## 3.1 `DEFAULT_SCAN_SOURCES` ha già causato perdita silenziosa di dati

`analytics-node/src/sources.ts` contiene oggi:

```ts
export const ALL_SOURCES = [
  "codex",
  "copilot",
  "claude",
  "cursor",
  "antigravity",
  "hook_log"
];

export const DEFAULT_SCAN_SOURCES = [
  "codex",
  "copilot",
  "claude",
  "cursor",
  "antigravity",
  "hook_log"
];
```

Il README documenta che `claude` in passato era assente da `DEFAULT_SCAN_SOURCES`, quindi l'adapter funzionava ma la GUI non importava nulla per quella sorgente.

Questo è un precedente importante: il framework ha già avuto bug di accuratezza che non producevano errore, ma semplicemente statistiche vuote.

### Regola da introdurre

`DEFAULT_SCAN_SOURCES` non deve più essere mantenuto come lista indipendente se semanticamente equivale a tutte le sorgenti scansionabili.

Preferenza:

```ts
export const DEFAULT_SCAN_SOURCES: readonly AnalyticsSource[] = ALL_SOURCES;
```

oppure derivazione equivalente che impedisca drift.

Aggiungere un test che fallisce se una sorgente session/event-bearing viene resa disponibile ma non viene scansionata di default senza una scelta esplicita documentata.

---

## 3.2 Codex ignora `CODEX_HOME`

Il resolver attuale è:

```ts
export function resolveCodexHome(): string {
  return resolve(homedir(), ".codex");
}
```

Di conseguenza vengono sempre cercati:

```text
~/.codex/sessions
~/.codex/archived_sessions
~/.codex/session_index.jsonl
```

La configurazione corrente di Codex supporta invece `CODEX_HOME` come override della directory di stato.

### Problema

Una macchina che usa:

```text
CODEX_HOME=/custom/path/codex
```

può avere analytics vuoti o parziali anche se Codex funziona normalmente.

Questo può apparire come differenza Windows/macOS quando in realtà è una differenza di configurazione.

### Correzione richiesta

Centralizzare:

```ts
export function resolveCodexHome(env = process.env, home = homedir()): string {
  const configured = typeof env.CODEX_HOME === "string"
    ? env.CODEX_HOME.trim()
    : "";

  return configured
    ? resolve(configured)
    : resolve(home, ".codex");
}
```

Tutti i resolver Codex devono derivare da questa funzione, compreso `session_index.jsonl`.

Non leggere due home contemporaneamente in modo silenzioso. Se esiste `CODEX_HOME`, quello è il root autorevole.

---

## 3.3 Claude Code ignora `CLAUDE_CONFIG_DIR`

Il resolver attuale è:

```ts
return [resolve(homedir(), ".claude", "projects")];
```

Quindi `analytics-node` assume sempre:

```text
~/.claude/projects
```

ma Claude Code espone `CLAUDE_CONFIG_DIR` per spostare la directory di configurazione/stato.

### Correzione richiesta

Creare un resolver centrale, per esempio:

```ts
export function resolveClaudeHome(env = process.env, home = homedir()): string {
  const configured = typeof env.CLAUDE_CONFIG_DIR === "string"
    ? env.CLAUDE_CONFIG_DIR.trim()
    : "";

  return configured
    ? resolve(configured)
    : resolve(home, ".claude");
}
```

poi:

```ts
resolve(resolveClaudeHome(), "projects")
```

Anche qui non fare union silenziosa tra root default e custom.

### Nota

Il formato locale di Claude Code non deve essere trattato come API stabile. Il resolver del path può essere stabile e testato, mentre l'adapter deve continuare a degradare in modo sicuro quando incontra nuovi record.

---

## 3.4 Bug adiacente macOS: resolver Copilot non distingue Darwin da Linux

`resolveCopilotRoots()` oggi fa:

```ts
if (process.platform === "win32") {
  ...
}

return [
  ~/.config/Code/...
];
```

Questo tratta macOS come Linux.

Per VS Code su macOS il pattern già usato dal resto del repository è:

```text
~/Library/Application Support/Code/User/...
```

Anche se il problema segnalato riguarda soprattutto Codex e Claude, questo bug va corretto nello stesso audit perché dimostra che la copertura multipiattaforma di `analytics-node/src/sources.ts` non è uniforme.

### Correzione

Aggiungere branch esplicito:

```ts
if (platform === "darwin") {
  return [
    resolve(home, "Library", "Application Support", "Code", "User", "globalStorage"),
    resolve(home, "Library", "Application Support", "Code", "User", "workspaceStorage"),
    ...
  ];
}
```

Non duplicare i resolver già disponibili altrove se è possibile estrarre una utility condivisa senza refactor esteso.

---

## 3.5 Codex perde metadata sufficienti a classificare correttamente i subagenti

L'adapter Codex gestisce già record moderni come:

```text
session_meta
turn_context
token_usage_record
event_msg
response_item
```

ma da `session_meta` oggi conserva essenzialmente:

- `id`;
- `mode`;
- `cwd`.

I rollout recenti espongono metadata aggiuntivi utili, tra cui, a seconda della versione:

- `parent_thread_id`;
- `thread_source` / `source`;
- `agent_role`;
- `agent_nickname`;
- `agent_path`;
- `cli_version`;
- `model_provider`;
- metadata multi-agent.

Il codice attuale persiste:

```ts
client_surface = "cli"
session_kind = session.messageCount > 0 ? "main" : "unknown"
```

### Conseguenza

Un rollout Codex di subagente con messaggi può diventare:

```text
session_kind = main
```

quindi entra nei KPI conversazionali normali quando `include_technical=false`.

Questo altera:

- numero sessioni;
- numero messaggi;
- token;
- modello preferito;
- distribuzione modelli;
- medie per sessione.

### Correzione

Estendere lo stato `StreamSession` con metadata privacy-safe:

```ts
parentThreadIdPresent: boolean;
agentRole: string | null;
agentNicknamePresent: boolean;
threadSource: string | null;
clientVersion: string | null;
clientSurface: ClientSurface;
sessionKind: SessionKind;
```

Non è necessario salvare identificatori sensibili/raw.

Derivazione consigliata:

```text
parent_thread_id presente
OR agent_role presente
OR source/thread_source indica subagent
    -> session_kind=subagent

source/surface riconosciuta come desktop/app
    -> client_surface=desktop

altrimenti
    -> client_surface=cli
```

La classificazione deve essere conservativa e fixture-driven. In caso di metadata sconosciuti:

```text
session_kind=unknown
```

non `main` per default.

### Invariante

Una sessione può essere classificata `main` solo se esistono evidenze positive sufficienti, non semplicemente perché contiene messaggi.

---

## 3.6 Claude Code può sovrastimare messaggi e token per record duplicati

L'adapter Claude attuale percorre ogni record `user`/`assistant` e aggiunge sempre una nuova metrica:

```ts
if (parsed.type === "user" || parsed.type === "assistant") {
  ...
  agg.messages.push(...)
}
```

Non viene usato un identificatore logico come `message.id` per deduplicare record ripetuti.

I transcript moderni possono contenere più righe riferite allo stesso messaggio logico. Esistono segnalazioni upstream di record assistant ripetuti con lo stesso `message.id`, sufficienti a sovrastimare i token se ogni riga viene sommata.

### Correzione richiesta

Introdurre dedupe a livello di messaggio logico.

Chiave primaria quando disponibile:

```text
session + role + message.id
```

Fallback possibili, solo se supportati dal formato osservato:

```text
requestId
uuid
parentUuid + message.id
```

Non usare il contenuto conversazionale come chiave.

### Strategia winner

Se esistono più record con la stessa chiave:

1. preferire il record con usage completo;
2. se entrambi hanno usage, scegliere quello più completo secondo campi noti;
3. se identici, contarne uno;
4. registrare `duplicates_suppressed` nei diagnostics;
5. non sommare due snapshot dello stesso messaggio.

### Acceptance fixture

Tre righe `assistant` con stesso `message.id` e stessi token devono produrre:

```text
assistant_message_count = 1
input_tokens = valore singolo
output_tokens = valore singolo
duplicates_suppressed = 2
```

---

## 3.7 Claude Code legge il file intero in RAM e viene bloccato oltre 50 MB

L'adapter usa:

```ts
const raw = await readFile(absolutePath, "utf8");
const lines = raw.split(...);
```

Lo scanner applica inoltre:

```ts
MAX_SOURCE_FILE_BYTES = 50 * 1024 * 1024
```

ed esenta solo:

```text
cursor
codex
```

### Conseguenza

Un transcript Claude valido sopra 50 MB viene marcato:

```text
FILE_TOO_LARGE
```

mentre Codex è già gestito con parser streaming bounded.

La frequenza di file grandi può variare da macchina a macchina e quindi sembrare un problema specifico di macOS.

### Correzione

Portare Claude a streaming JSONL, riusando il pattern architetturale Codex ma senza condividere parser specifici.

Target:

```ts
async function* readJsonlLines(...)
```

con limite per singola riga, non per intero file.

Dopo questa modifica:

```text
Claude va escluso dal whole-file MAX_SOURCE_FILE_BYTES
```

oppure il size guard deve diventare capability-based:

```ts
adapterCapabilities[source].wholeFileRead
```

Preferire quest'ultima soluzione se il cambiamento resta piccolo.

---

## 3.8 Discovery Claude troppo generica per i file `.json`

Per tutte le sorgenti non Cursor, lo scanner accetta oggi:

```ts
.json
.jsonl
```

sotto `~/.claude/projects`.

I layout recenti possono contenere metadata sidecar, inclusi file `.meta.json` relativi a subagenti.

Questi non devono essere trattati automaticamente come transcript.

### Correzione

Definire un discriminante specifico per Claude:

```text
transcript -> *.jsonl
metadata sidecar -> opzionale, solo se riconosciuto esplicitamente
```

Non inviare genericamente ogni `.json` all'adapter transcript.

Se si decide di usare `.meta.json`, deve esistere una funzione separata di metadata enrichment, non la pipeline principale del transcript.

---

## 3.9 Semantica temporale incoerente tra KPI e timeseries

Questo è un problema di accuratezza generale indipendente dal sistema operativo.

`analytics_summary` applica `date_from/date_to` a:

```sql
sessions.updated_at
```

poi somma tutte le righe `message_metrics` delle sessioni selezionate.

Quindi una sessione:

```text
01 settembre: 100k token
23 settembre: 2k token
```

con filtro:

```text
23 settembre
```

può contribuire al KPI con i token dell'intera sessione, non solo con quelli del 23 settembre.

`analytics_models` segue lo stesso schema: filtro sulla sessione, poi somma tutte le metriche della sessione.

Invece `analytics_timeseries` è già documentato come:

```text
sessions -> sessions.updated_at
messages/tokens -> message_metrics.created_at
events -> runtime_events.occurred_at
```

### Conseguenza GUI

Lo stesso filtro data alimenta:

- KPI `Observed Tokens` da `analytics_summary`;
- grafico attività da `analytics_timeseries`.

I due componenti possono quindi mostrare valori semanticamente diversi per lo stesso intervallo.

### Correzione richiesta

Formalizzare tre basi temporali:

```text
session time    = sessions.updated_at
activity time   = message_metrics.created_at
event time      = runtime_events.occurred_at
```

Per le metriche di utilizzo nel periodo:

- sessioni: `sessions.updated_at`;
- messaggi: `message_metrics.created_at`;
- token: `message_metrics.created_at`;
- modelli/token per modello: `message_metrics.created_at`;
- eventi: `runtime_events.occurred_at`.

### Compatibilità

Non cambiare silenziosamente il significato delle API esistenti senza contratto.

Opzione consigliata:

```json
{
  "time_basis": "activity"
}
```

come parametro opzionale inizialmente additivo per `analytics_summary` e `analytics_models`.

Valori:

```text
activity
session_updated
```

La GUI Sophia deve passare esplicitamente:

```text
time_basis=activity
```

Il legacy caller che omette il parametro può mantenere temporaneamente il comportamento attuale durante una finestra di compatibilità.

In una milestone successiva, dopo verifica dei consumer, `activity` può diventare default.

### Metadata di risposta

Ogni risposta filtrabile deve dichiarare la base temporale utilizzata:

```json
{
  "time_semantics": {
    "sessions": "sessions.updated_at",
    "messages": "message_metrics.created_at",
    "tokens": "message_metrics.created_at",
    "events": "runtime_events.occurred_at"
  }
}
```

---

## 3.10 `observed_tokens` non è un totale complessivo di tutti i token

Il contratto attuale definisce:

```text
observed_tokens = input_tokens + output_tokens
```

mentre mantiene separati:

- reasoning;
- cache read;
- cache write.

Questo è accettabile, ma la UI e la documentazione devono essere precise.

### Regola

Non rinominare o presentare `observed_tokens` come:

```text
Token totali
```

se include solo input+output.

Usare:

```text
Token osservati input+output
```

oppure mantenere `Observed Tokens` con tooltip esplicito.

Non stimare mai token non esposti dal client.

---

## 3.11 Un fix del parser oggi non corregge automaticamente il DB esistente

Lo scanner salta un file se:

```text
size uguale
mtime uguale
last_status=imported
```

Non considera la versione dell'adapter che ha prodotto le righe.

### Conseguenza

Dopo una correzione come:

- dedupe Claude;
- classificazione subagent Codex;
- nuovo mapping metadata;

un file già importato resta invariato e non viene riprocessato.

Quindi il codice può essere corretto ma la dashboard continua a mostrare dati storici sbagliati.

### Correzione obbligatoria

Versionare la proiezione/import per sorgente.

Modello consigliato:

```ts
const ADAPTER_VERSIONS = {
  codex: "...",
  claude: "...",
  ...
};
```

Aggiungere a `source_files`, tramite migration versionata:

```sql
adapter_version TEXT NULL
```

Lo skip incrementale diventa:

```text
same size
AND same mtime
AND last_status=imported
AND adapter_version=currentAdapterVersion
```

Se cambia versione:

```text
reimport automatico una tantum
```

### Invariante

`ADAPTER_VERSIONS` deve avere una sola fonte canonica, utilizzata sia da:

- `scan_runs.metadata_json`;
- `source_files.adapter_version`;
- diagnostica/status.

Non mantenere stringhe versione duplicate nello scanner.

---

## 3.12 Re-import Claude deve eliminare proiezioni stale dello stesso file

Codex conserva l'elenco delle sessioni precedentemente prodotte dal file e rimuove quelle non più prodotte dopo il re-import.

Claude deve offrire lo stesso contratto.

### Problema

Se cambia la normalizzazione degli ID sessione/subagent o un record viene correttamente scartato dopo la patch, una vecchia riga può restare nel DB se non viene esplicitamente ripulita.

### Correzione

All'interno della stessa transazione Claude:

1. acquisire gli ID sessione correnti associati a `source_file_id`;
2. tracciare gli ID prodotti dalla nuova importazione;
3. eliminare quelli stale;
4. commit atomico.

Nessuna cancellazione deve coinvolgere altri source file.

---

## 4. Principi implementativi obbligatori

1. **Privacy-first invariata.** Non persistono prompt, risposte, titoli conversazionali Claude o path raw.
2. **No stime.** Se token/modello non sono esposti, registrare indisponibilità, non dedurre valori.
3. **Cross-platform per contratto.** L'OS influenza solo path/surface realmente differenti.
4. **Adapter versionati.** Una modifica di normalizzazione deve poter correggere dati già importati.
5. **Dati diagnostici separati dai KPI.** Warning e coverage non devono diventare metriche di utilizzo.
6. **Fixture minimali.** Nessun transcript reale con contenuto utente deve finire nel repository.
7. **Schemi locali non ufficiali = parser difensivo.** Record nuovi vanno ignorati con warning aggregato quando possibile.
8. **Zero non significa unavailable.** UI e API devono distinguere esplicitamente i due casi.
9. **No refactor esteso.** Conservare `analytics-node`, SQLite e tool contract esistenti.
10. **Compatibilità progressiva.** Cambi di semantica aggregata devono essere additivi o versionati.

---

## 5. Fuori scope

Questa iterazione non deve:

- trasformare `analytics-node` in un servizio remoto;
- introdurre telemetry cloud;
- inviare transcript fuori macchina;
- importare Claude Desktop se il formato non è supportato in modo verificabile;
- stimare costi monetari;
- stimare token mancanti;
- analizzare contenuto semantico di prompt/risposte;
- riscrivere tutta la dashboard;
- introdurre un nuovo database;
- modificare formati upstream Codex/Claude.

---

# 6. Piano implementativo

## Fase 0 - Baseline e audit riproducibile

### 6.0.1 Creare un audit read-only delle sorgenti

Aggiungere, per esempio:

```text
scripts/audit-analytics-source-coverage.mjs
```

Il tool deve essere privacy-safe e non scrivere nel DB.

Input suggeriti:

```text
--source codex|claude|all
--home <path>
--json
```

Output per sorgente:

```json
{
  "source": "codex",
  "platform": "darwin",
  "arch": "arm64",
  "root_resolution": "CODEX_HOME|default_home",
  "root_exists": true,
  "candidate_files": 42,
  "candidate_bytes": 123456,
  "largest_file_bytes": 12345,
  "record_types": {},
  "field_presence": {},
  "duplicate_logical_records": 0,
  "unsupported_records": 0
}
```

### Privacy

Non stampare:

- path completi;
- session ID raw;
- cwd raw;
- prompt;
- risposta;
- tool arguments;
- thread title.

Usare path redatti, relativi o hash.

### 6.0.2 Golden baseline

Prima di modificare il parser, raccogliere su almeno:

| Client | Windows | macOS |
|---|---:|---:|
| Codex | sì | sì |
| Claude Code | sì | sì |

Per ogni macchina registrare:

- OS;
- architettura;
- versione client se rilevabile;
- effective root;
- file candidati;
- file importabili;
- file falliti;
- warning per codice;
- sessioni main;
- sessioni subagent;
- messaggi;
- token osservati;
- percentuale modello noto;
- duplicate records soppressi, dopo la patch.

Non confrontare semplicemente "Windows vs macOS": confrontare anche versione client e configurazione root.

---

## Fase 1 - Normalizzare i resolver multipiattaforma

### File principali

```text
analytics-node/src/sources.ts
analytics-node/README.md
tests/smoke/analytics-node.smoke.mjs
```

### Task

1. rendere `resolveCodexHome()` compatibile con `CODEX_HOME`;
2. far derivare sessioni, archivio e session index dallo stesso root;
3. introdurre `resolveClaudeHome()` con `CLAUDE_CONFIG_DIR`;
4. far derivare `~/.claude/projects` dal resolver;
5. correggere `resolveCopilotRoots()` per Darwin;
6. rendere platform/env/home iniettabili nei resolver o estrarre pure functions testabili;
7. non basare i test sulla sola `process.platform` della macchina CI.

### Test richiesti

```text
Codex/default/win32
Codex/default/darwin
Codex/CODEX_HOME/win32
Codex/CODEX_HOME/darwin
Claude/default/win32
Claude/default/darwin
Claude/CLAUDE_CONFIG_DIR/win32
Claude/CLAUDE_CONFIG_DIR/darwin
Copilot/win32
Copilot/darwin
Copilot/linux
```

### Acceptance

Dato lo stesso root logico, Windows e macOS devono produrre lo stesso set di file candidati a parità di fixture.

---

## Fase 2 - Adapter versioning e re-import correttivo

### File

```text
analytics-node/src/migrations.ts
analytics-node/src/scanner.ts
analytics-node/src/sources.ts o nuovo adapter-versions.ts
analytics-node/src/db.ts
analytics-node/README.md
tests/smoke/analytics-migrations-recovery.mjs
tests/smoke/analytics-node.smoke.mjs
```

### Task

1. introdurre mappa canonica `ADAPTER_VERSIONS`;
2. migration additiva `source_files.adapter_version`;
3. memorizzare la versione dopo import concluso con successo;
4. usare la stessa mappa in `scan_runs.metadata_json`;
5. cambiare condizione `unchanged` includendo versione;
6. se versione diversa, reimport anche con mtime/size invariati;
7. non forzare reimport di sorgenti il cui adapter non è cambiato;
8. testare upgrade da DB legacy con `adapter_version=NULL`.

### Acceptance

Dopo bump Claude/Codex, il primo scan riprocessa i file invariati interessati; il secondo scan li salta normalmente.

---

## Fase 3 - Hardening Codex

### File

```text
analytics-node/src/adapters/codex.ts
analytics-node/src/sources.ts
analytics-node/src/tools/sessions.ts
analytics-node/src/tools/summary.ts
analytics-node/src/tools/models.ts
tests/smoke/analytics-node.smoke.mjs
```

### 6.3.1 Session classification

Estendere `StreamSession` senza cambiare il contratto pubblico non necessario.

Classificare:

```text
main
subagent
unknown
```

usando metadata strutturati di `session_meta` quando presenti.

Non usare:

```ts
messageCount > 0 ? "main" : "unknown"
```

come unica regola.

### 6.3.2 Client surface

Non hardcodare sempre:

```text
cli
```

se i metadata del rollout permettono di distinguere una surface nota.

Mappare solo valori verificati. Fallback:

```text
unknown
```

### 6.3.3 Metadata diagnostici

Persistire soltanto metadata non sensibili utili alla qualità:

```json
{
  "adapter_version": "...",
  "client_version": "...",
  "classification_reason": "parent_thread_id|agent_role|..."
}
```

Non salvare parent thread ID raw.

### 6.3.4 Lifecycle

Mantenere gli eventi:

```text
collab_agent_spawn_end
collab_close_end
```

come `agent_lifecycle`.

Non usarli da soli per contare il numero di sessioni subagent: sessioni e lifecycle sono metriche diverse.

### Acceptance fixture

Una root + due child deve produrre:

```text
main sessions = 1
subagent sessions = 2
```

Con `include_technical=false` deve contribuire ai KPI conversazionali solo la root.

---

## Fase 4 - Hardening Claude Code

### File

```text
analytics-node/src/adapters/claude.ts
analytics-node/src/scanner.ts
analytics-node/src/sources.ts
tests/smoke/analytics-node.smoke.mjs
```

### 6.4.1 Streaming JSONL

Sostituire whole-file `readFile` con parser streaming bounded.

Requisiti:

- limite per linea configurabile/costante;
- invalid JSON -> warning aggregato;
- una linea invalida non invalida l'intero file se esistono record supportati;
- no accumulo dell'intero transcript in RAM.

### 6.4.2 Candidate filtering

Per Claude:

```text
*.jsonl -> candidato transcript
*.json -> non transcript di default
```

Aggiungere metadata sidecar solo tramite path/schema espliciti se realmente necessari.

### 6.4.3 Logical-message dedupe

Aggiungere una struttura, per esempio:

```ts
Map<LogicalMessageKey, NormalizedMessage>
```

Chiave stabile senza contenuto conversazionale.

Contatori diagnostics:

```text
records_seen
logical_messages
records_deduplicated
token_records_available
```

### 6.4.4 MCP dedupe

Se il formato espone un identificatore stabile della tool invocation, deduplicare le osservazioni duplicate dello stesso call.

In assenza di ID stabile, mantenere comportamento conservativo e dichiarare il limite nei diagnostics.

### 6.4.5 Subagent classification

Continuare a usare `isSidechain` e `agentId` quando presenti.

Se la struttura directory o metadata sidecar espongono `agentType` / `spawnDepth`, usarli solo come enrichment opzionale verificato.

Non convertire ogni file sotto `subagents/` in main session.

### 6.4.6 Replace stale projections

Implementare il contratto `previousSessionIds/producedSessionIds` già presente nel pattern Codex.

### Acceptance

- transcript >50 MB simulato/generato non deve fallire solo per dimensione totale;
- record duplicati con stesso logical ID contati una volta;
- reimport idempotente;
- subagent non incluso nei KPI main con `include_technical=false`.

---

## Fase 5 - Correggere la semantica temporale

### File

```text
analytics-node/src/tools/summary.ts
analytics-node/src/tools/models.ts
analytics-node/src/tools/timeseries.ts
analytics-node/src/tools/sessions.ts
analytics-node/src/index.ts
scripts/gui-server.js
scripts/gui/analytics-dashboard.js
analytics-node/README.md
tests/smoke/analytics-node.smoke.mjs
tests/smoke/analytics-ui-audit.smoke.mjs
```

### 6.5.1 Introdurre `time_basis`

Schema additivo:

```json
{
  "time_basis": {
    "type": "string",
    "enum": ["activity", "session_updated"]
  }
}
```

Per la GUI usare sempre:

```text
activity
```

### 6.5.2 Query activity-based

Quando `time_basis=activity`:

- `message_metrics.created_at` delimita messaggi/token;
- modelli vengono aggregati sulle sole metriche nel periodo;
- il numero sessioni modello è `COUNT(DISTINCT session_id)` delle metriche nel periodo;
- eventi restano su `runtime_events.occurred_at`;
- sessioni recenti/lista possono continuare su `sessions.updated_at`.

### 6.5.3 Contratto GUI

Lo stesso periodo selezionato deve produrre:

```text
KPI token == somma token timeseries nello stesso periodo
KPI messaggi == somma messaggi timeseries nello stesso periodo
```

salvo filtri esplicitamente dichiarati diversi.

Aggiungere assert automatico su fixture deterministica.

### 6.5.4 Boundary fixture

Creare una sessione:

```text
2026-09-20: 100 input + 50 output
2026-09-23: 20 input + 10 output
```

Filtro:

```text
2026-09-23T00:00:00Z .. 2026-09-23T23:59:59Z
```

Con `activity` il risultato deve essere:

```text
observed_tokens = 30
```

non `180`.

---

## Fase 6 - Coverage e diagnostica esplicita

### Obiettivo

Trasformare:

```text
Claude = 0
```

in qualcosa di diagnosticabile:

```text
Claude:
  root found
  18 candidate files
  17 imported
  1 failed
  42 unsupported records
  5 duplicate logical messages suppressed
  token coverage 93%
```

senza esporre dati sensibili.

### 6.6.1 Nuovo blocco status/scan

Aggiungere output additivo privacy-safe, per esempio:

```json
{
  "source_diagnostics": {
    "claude": {
      "root_status": "found",
      "root_resolution": "default_home",
      "candidate_files": 18,
      "files_imported": 17,
      "files_failed": 1,
      "supported_records": 1200,
      "unsupported_records": 10,
      "duplicates_suppressed": 24,
      "token_coverage": {
        "available": 800,
        "missing": 100
      }
    }
  }
}
```

Non riportare path raw.

### 6.6.2 Distinguere stati

Standardizzare almeno:

```text
available
zero_activity
root_missing
no_candidates
partial
parser_unsupported
failed
```

### 6.6.3 UI

La GUI non deve mostrare lo stesso rendering per:

```text
0 attività reale
```

e:

```text
sorgente non importabile
```

Aggiungere note/chip diagnostiche leggere, senza trasformare la dashboard in una console tecnica.

### 6.6.4 Model/token coverage

Mantenere gli attuali concetti:

```text
known_observed_tokens
unknown_observed_tokens
coverage available/missing/partial
```

ma aggiungere source-level diagnostics quando il dato è incompleto per parser/import.

---

## Fase 7 - Test multipiattaforma e parità

## 6.7.1 Fixture sintetiche OS-neutral

Le stesse fixture logiche devono essere eseguite con resolver simulati per:

```text
win32
linux
darwin
```

L'output normalizzato deve essere identico salvo:

```text
platform
root_resolution/path hash
client surface se realmente diversa
```

## 6.7.2 Real-machine validation

Eseguire almeno una volta:

### Codex

```text
Windows x64
macOS arm64 o x64 disponibile
```

### Claude Code

```text
Windows x64
macOS arm64 o x64 disponibile
```

Raccogliere solo audit privacy-safe.

### Confronto

Per sessione/campione noto confrontare:

| Campo | Windows | macOS | Parità attesa |
|---|---:|---:|---|
| file candidati | | | sì, a parità storico |
| session_kind | | | sì |
| user messages | | | sì |
| assistant messages | | | sì |
| input tokens | | | sì, se esposti |
| output tokens | | | sì, se esposti |
| reasoning tokens | | | sì, se esposti |
| model | | | sì, se esposto |
| MCP calls | | | sì, a parità sessione |
| warning unsupported | | | dipende da versione client |

La differenza di versione client deve essere riportata prima di attribuire una divergenza all'OS.

---

# 7. Test automatici minimi da aggiungere

## Resolver

- `codex_default_home_win32`
- `codex_default_home_darwin`
- `codex_home_override`
- `claude_default_home_win32`
- `claude_default_home_darwin`
- `claude_config_dir_override`
- `copilot_roots_win32`
- `copilot_roots_darwin`
- `copilot_roots_linux`

## Codex adapter

- main session classificata main;
- child con `parent_thread_id` classificato subagent;
- metadata sconosciuti -> unknown, non main arbitrario;
- root+child non contaminano KPI main;
- `session_index` usa effective `CODEX_HOME`;
- token reconciliation esistente non regredisce.

## Claude adapter

- duplicate `message.id` dedupe;
- record diversi non deduplicati;
- subagent sidechain separato;
- large JSONL streaming;
- malformed line recuperabile;
- `.meta.json` non scansionato come transcript;
- reimport elimina stale rows;
- force import e normal import convergono allo stesso DB state.

## Aggregazioni

- sessione cross-day;
- activity filter su message timestamp;
- legacy `session_updated` conservato durante transizione;
- summary/timeseries token parity;
- summary/timeseries message parity;
- models usa solo metriche nel periodo activity;
- reasoning/cache restano separati da `observed_tokens`.

## Adapter versioning

- legacy `adapter_version=NULL` -> reimport;
- versione vecchia -> reimport;
- versione corrente -> skip;
- file failed -> retry anche se invariato;
- reimport di Claude/Codex non duplica righe.

## Diagnostics

- root missing != zero activity;
- no candidates != parser unsupported;
- partial import espone conteggi privacy-safe;
- diagnostics non contengono path/session ID/contenuto conversazionale raw.

---

# 8. File interessati

## Core

```text
analytics-node/src/sources.ts
analytics-node/src/scanner.ts
analytics-node/src/migrations.ts
analytics-node/src/adapters/codex.ts
analytics-node/src/adapters/claude.ts
analytics-node/src/tools/status.ts
analytics-node/src/tools/summary.ts
analytics-node/src/tools/models.ts
analytics-node/src/tools/sessions.ts
analytics-node/src/tools/timeseries.ts
analytics-node/src/index.ts
```

Possibile nuovo file:

```text
analytics-node/src/adapter-versions.ts
```

## GUI

```text
scripts/gui-server.js
scripts/gui/analytics-dashboard.js
scripts/gui/index.html
```

## Test

```text
tests/smoke/analytics-node.smoke.mjs
tests/smoke/analytics-migrations-recovery.mjs
tests/smoke/analytics-ui-audit.smoke.mjs
```

Possibile nuovo audit:

```text
scripts/audit-analytics-source-coverage.mjs
```

## Documentazione

```text
analytics-node/README.md
docs/server-capability-matrix.md
docs/mcp-skills-agents-development-guide.md
.agents/skills/mcp-runtime-integrator/references/pitfalls-found-in-production.md
```

Aggiornare anche `CHANGELOG.md` se previsto dalla convenzione della PR.

---

# 9. Sequenza PR consigliata

Per ridurre il rischio, non implementare tutto in una singola PR.

## PR 1 - Source resolution parity

Scope:

- `CODEX_HOME`;
- `CLAUDE_CONFIG_DIR`;
- Copilot macOS roots;
- test resolver multipiattaforma;
- audit source coverage iniziale.

Non cambiare parser o aggregazioni.

## PR 2 - Adapter projection versioning

Scope:

- `ADAPTER_VERSIONS` canonico;
- migration `source_files.adapter_version`;
- reimport automatico su bump;
- test upgrade/skip/retry.

## PR 3 - Codex classification accuracy

Scope:

- metadata `session_meta`;
- main/subagent/unknown;
- surface;
- diagnostics;
- fixture root/child;
- bump versione adapter Codex.

## PR 4 - Claude parser accuracy

Scope:

- streaming;
- transcript filtering;
- logical message dedupe;
- subagent enrichment;
- stale projection cleanup;
- bump versione adapter Claude.

## PR 5 - Time semantics consistency

Scope:

- `time_basis`;
- summary/models activity query;
- GUI usa `activity`;
- summary/timeseries parity;
- documentazione contratto.

## PR 6 - Diagnostics e macOS acceptance

Scope:

- source diagnostics API/UI;
- zero vs unavailable;
- audit privacy-safe;
- golden parity Windows/macOS;
- README operativo.

---

# 10. Criteri di accettazione complessivi

La milestone è completata quando:

1. Codex usa `CODEX_HOME` quando configurato.
2. Claude usa `CLAUDE_CONFIG_DIR` quando configurato.
3. Il resolver Copilot usa path macOS nativi.
4. La stessa fixture Codex produce la stessa normalizzazione su Windows e macOS.
5. La stessa fixture Claude produce la stessa normalizzazione su Windows e macOS.
6. I child Codex non vengono più classificati automaticamente come main solo perché contengono messaggi.
7. I duplicate logical messages Claude non vengono conteggiati più volte.
8. Claude non fallisce solo perché il transcript totale supera 50 MB.
9. File `.json` Claude non transcript non vengono inviati al parser JSONL principale.
10. Un bump adapter forza il reimport automatico una sola volta.
11. Il secondo scan dopo il reimport torna incrementale.
12. `activity` filtra messaggi/token su `message_metrics.created_at`.
13. KPI messaggi/token e timeseries sono coerenti sullo stesso range.
14. `observed_tokens` resta input+output e non ingloba reasoning/cache in modo implicito.
15. La GUI distingue zero attività da sorgente mancante/fallita.
16. Diagnostics non espongono prompt, risposta, path raw o session ID raw.
17. I dati legacy restano migrabili e nessuna migration è distruttiva.
18. Real-machine validation è documentata per Codex Windows/macOS e Claude Code Windows/macOS.
19. Nessuna modifica richiede cancellazione manuale del DB per diventare effettiva.
20. Il gate test conclusivo previsto da `AGENTS.md` è verde.

---

# 11. KPI tecnici before/after

Per ogni sorgente misurare:

| KPI | Before | After |
|---|---:|---:|
| candidate files | | |
| imported files | | |
| failed files | | |
| unsupported records | | |
| duplicate records suppressed | n/a | |
| main sessions | | |
| subagent sessions | | |
| logical user messages | | |
| logical assistant messages | | |
| observed input tokens | | |
| observed output tokens | | |
| model known % | | |
| token coverage % | | |
| summary/timeseries delta | | |

### Target

Per fixture deterministiche:

```text
summary/timeseries delta = 0
cross-platform normalized delta = 0
```

Per dati reali:

- ogni divergenza deve avere un reason code;
- nessun valore mancante deve essere attribuito genericamente a macOS senza prova.

---

# 12. Reason code consigliati

Evitare diagnostica solo testuale.

Possibili codici:

```text
SOURCE_ROOT_MISSING
SOURCE_ROOT_OVERRIDE_ACTIVE
NO_CANDIDATE_FILES
FILE_TOO_LARGE
INVALID_JSON_LINE_SKIPPED
RECORD_SCHEMA_UNSUPPORTED
MODEL_FIELD_MISSING
MODEL_FIELD_INVALID
TOKEN_DATA_NOT_EXPOSED
DUPLICATE_LOGICAL_MESSAGE_SUPPRESSED
DUPLICATE_TOOL_CALL_SUPPRESSED
SUBAGENT_CLASSIFICATION_UNCERTAIN
ADAPTER_VERSION_REIMPORT
SOURCE_PARTIAL_IMPORT
```

Riutilizzare codici esistenti quando semanticamente equivalenti invece di duplicarli.

---

# 13. Migrazione dei dati esistenti

## 13.1 Non chiedere all'utente di cancellare il DB

La nuova versione deve correggere i dati attraverso reimport versionato.

### Sequenza

```text
migration schema
  -> adapter_version legacy/null
  -> scan
  -> rileva mismatch
  -> reimport transazionale
  -> aggiorna adapter_version
  -> query aggiornate
```

## 13.2 Rollback

Le migration devono restare non distruttive secondo le regole repository.

Il rollback applicativo può consistere nel ripristino del codice precedente mantenendo la colonna additiva inutilizzata.

Non introdurre downgrade SQL distruttivo.

---

# 14. Rischi e mitigazioni

## Rischio A - Schema upstream non stabile

### Mitigazione

- parser permissivo per record aggiuntivi;
- classificazione solo su campi osservati e testati;
- reason code per unknown;
- adapter version bump quando cambia normalizzazione.

## Rischio B - Dedupe troppo aggressivo

### Mitigazione

- dedupe solo su stable logical ID;
- niente hash del contenuto conversazionale;
- fixture con due messaggi diversi ma token identici.

## Rischio C - Cambio time semantics rompe consumer legacy

### Mitigazione

- parametro additivo `time_basis`;
- GUI passa esplicitamente `activity`;
- deprecazione documentata prima di cambiare default.

## Rischio D - Reimport costoso

### Mitigazione

- bump solo adapter interessato;
- reimport una tantum;
- scanner streaming;
- skip normale dal secondo run.

## Rischio E - Path custom configurato ma non valido

### Mitigazione

- root autorevole = env override;
- diagnostics `SOURCE_ROOT_MISSING`;
- niente fallback silenzioso al default perché potrebbe unire due storici diversi.

## Rischio F - macOS sembra diverso per versione client, non per OS

### Mitigazione

Ogni report di parità deve includere:

```text
platform
arch
client version
adapter version
root resolution
```

prima di formulare la root cause.

---

# 15. Verifiche da eseguire durante l'implementazione

Seguire `AGENTS.md` del repository.

Prima dello sviluppo:

```bash
git rev-parse HEAD
```

memorizzare il baseline.

Durante lo sviluppo usare test mirati solo quando consentito dal workflow del repository, ma il gate conclusivo deve restare una singola invocazione del planner affected-tests prevista dalle regole correnti.

Gate finale:

```bash
npm run test:affected -- --strict --base <baseline>
```

Se il planner include `test:analytics`, non eseguire preventivamente un secondo full gate equivalente solo per sicurezza.

Per le prove Windows/macOS real-machine usare l'audit privacy-safe e allegare soltanto output aggregati/redatti.

---

# 16. Prompt operativo per gli agenti implementatori

```text
Nel repository `sophiadeveloper/mcp-servers`, implementa in modo incrementale il piano `20260923_audit_statistics_accuracy.md` per correggere accuratezza, completezza e parita' multipiattaforma di `analytics-node`.

Prima di modificare codice:
1. leggi `AGENTS.md` e le regole locali applicabili;
2. usa la skill repository-only `.agents/skills/mcp-runtime-integrator` per i bug di scanning/parsing runtime;
3. acquisisci il baseline Git richiesto da AGENTS.md;
4. non modificare `graphify-out/`;
5. non usare transcript reali con contenuto utente come fixture.

Principi vincolanti:
- privacy-first invariata;
- nessun prompt/risposta/path raw/session id raw nel DB o nei diagnostics;
- non stimare token o modelli non esposti;
- non introdurre workaround macOS generici: correggi resolver/path reali e parser OS-neutral;
- modifiche piccole e reviewable;
- migration additive e non distruttive;
- nessuna cancellazione manuale del DB richiesta all'utente;
- ogni cambio di normalizzazione parser deve avere adapter version bump e reimport automatico dei file gia' importati.

Implementa per PR/fase separata.

FASE 1 - Resolver e audit sorgenti
- `CODEX_HOME` deve essere il root Codex autorevole quando presente.
- `CLAUDE_CONFIG_DIR` deve essere il root Claude autorevole quando presente.
- Correggi `resolveCopilotRoots()` per macOS (`~/Library/Application Support/...`).
- Rendi resolver testabili con platform/env/home iniettati, senza dipendere esclusivamente dalla macchina CI.
- Aggiungi `scripts/audit-analytics-source-coverage.mjs` read-only e privacy-safe.

FASE 2 - Adapter versioning
- Introduci una sola mappa canonica `ADAPTER_VERSIONS`.
- Aggiungi migration `source_files.adapter_version`.
- Un file invariato e importato e' skippabile solo se la versione adapter coincide.
- Un bump adapter deve provocare un solo reimport correttivo.
- `scan_runs.metadata_json` deve usare la stessa mappa.

FASE 3 - Codex
- Estendi parsing `session_meta` ai metadata strutturati utili alla classificazione quando presenti.
- Non classificare una sessione come main soltanto per `message_count > 0`.
- Classifica child/subagent usando evidenze come parent-thread/agent metadata disponibili.
- In caso incerto usa `unknown`.
- Non hardcodare `client_surface=cli` quando la source espone una surface verificabile.
- Conserva token reconciliation corrente e aggiungi regression test.
- Bump adapter Codex.

FASE 4 - Claude Code
- Converti l'adapter a streaming JSONL bounded.
- Non trattare genericamente ogni `.json` sotto `.claude/projects` come transcript.
- Deduplica logical messages usando `message.id` o equivalente stabile quando disponibile; non usare contenuto conversazionale.
- Mantieni contatore `duplicates_suppressed` privacy-safe.
- Evita doppio conteggio delle tool call quando esiste un ID stabile.
- Mantieni subagent classification via `isSidechain`/agent metadata verificati.
- Allinea cleanup stale projections al pattern Codex.
- Bump adapter Claude.

FASE 5 - Semantica temporale
- Aggiungi parametro additivo `time_basis=activity|session_updated` a summary/models.
- La GUI deve passare `activity`.
- In `activity`: messaggi/token/modelli usano `message_metrics.created_at`; eventi usano `runtime_events.occurred_at`; session listing resta su `sessions.updated_at`.
- Esponi `time_semantics` nella risposta.
- Mantieni percorso legacy durante la transizione.
- Aggiungi fixture cross-day che dimostri che i KPI del giorno non includono token storici esterni al range.
- Verifica parita' KPI/timeseries sullo stesso filtro.

FASE 6 - Diagnostics
- Esponi stati separati: available, zero_activity, root_missing, no_candidates, partial, parser_unsupported, failed.
- Aggiungi conteggi privacy-safe per candidate/import/fail/unsupported/duplicates/token coverage.
- La GUI deve distinguere zero reale da dato non disponibile.
- `observed_tokens` resta input+output; reasoning/cache restano separati e vanno etichettati correttamente.

Test obbligatori da implementare:
- resolver win32/darwin/linux e env override;
- Codex main/subagent/unknown;
- Claude duplicate message id;
- Claude large JSONL streaming;
- Claude malformed line recovery;
- Claude `.meta.json` escluso come transcript;
- adapter-version reimport e successivo skip;
- stale projection cleanup;
- activity vs session_updated boundary;
- summary/timeseries parity;
- diagnostics privacy-safe;
- stessa fixture normalizzata con platform win32 e darwin produce stessi dati funzionali.

Real-machine acceptance:
- Codex Windows e macOS;
- Claude Code Windows e macOS;
- registra platform, arch, client version, adapter version e root resolution prima di attribuire differenze all'OS.

Non modificare direttamente dati utente reali durante l'audit. Lo scanner puo' aggiornare il DB analytics solo durante i normali test/scan previsti e mai i transcript sorgente.

A chiusura esegui il solo gate conclusivo previsto da AGENTS.md:
`npm run test:affected -- --strict --base <baseline>`

Riporta:
- file modificati;
- migration introdotte;
- adapter version bump;
- test selezionati dal planner e risultato;
- risultati di parita' Windows/macOS disponibili;
- eventuali formati upstream ancora non verificabili;
- rischi residui.
```

---

# 17. Risultato atteso

Dopo l'intervento la dashboard non deve più limitarsi a mostrare un numero apparentemente autorevole.

Per ogni statistica deve essere possibile distinguere:

```text
misurato correttamente
misurato parzialmente
dato non esposto dal client
sorgente non trovata
formato non supportato
record duplicato soppresso
```

Il target finale è che una differenza tra Windows e macOS sia attribuibile a un fatto osservabile, per esempio:

```text
client version differente
root custom differente
surface differente
schema record differente
```

non a una generica categoria "macOS perde dettagli".

La pipeline analytics deve diventare verificabile end-to-end e auto-correttiva quando cambia una normalizzazione dell'adapter, senza richiedere cancellazione manuale del database o force scan permanente.
