# Audit 05 - Accuratezza statistiche: contro-analisi e stato

**Documento verificato:** [`20260923_audit_05_statistics_accuracy.md`](20260923_audit_05_statistics_accuracy.md)
**Baseline Git:** `a217141eddc1380461a27803512449e8f4e1e839`
**Data verifica:** 2026-09-23. Evidenze raccolte:

- Windows x64, circa 547 rollout Codex (cli_version da 0.100 a 0.155) e 205 transcript Claude Code (dati locali reali);
- conteggi aggregati e privacy-safe, senza contenuti, id o path;
- validazione finale su una **copia** del DB analytics reale (il DB reale non è stato modificato).

## 1. Affermazioni verificate

| # | Affermazione dell'audit | Esito | Evidenza |
|---|---|---|---|
| 3.1 | `DEFAULT_SCAN_SOURCES` è una lista duplicata | **Confermata** | Lista indipendente con una nota "tenere allineata" |
| 3.2 | Codex ignora `CODEX_HOME` | **Confermata** | `resolve(homedir(), ".codex")`, anche per `session_index.jsonl` |
| 3.3 | Claude ignora `CLAUDE_CONFIG_DIR` | **Confermata** | `~/.claude/projects` fisso |
| 3.4 | Copilot tratta macOS come Linux | **Confermata** | Nessun branch `darwin`, al contrario di Cursor |
| 3.5 | Subagenti Codex classificati male | **Confermata, con impatto maggiore del previsto** | <ul><li>414 dei 544 rollout con metadata sono subagenti (`source.subagent` thread_spawn o guardian).</li><li>La regola `messageCount > 0 ? main : unknown` li rendeva `unknown` (senza messaggi utente) o `main`.</li><li>`client_surface` era sempre `cli`, ma `originator` vale "Codex Desktop" in 465 rollout e `codex_vscode` in 79.</li></ul> |
| 3.6 | Record Claude duplicati sovrastimano i token | **Confermata e misurata** | <ul><li>22.595 record assistant per 10.594 `message.id` unici.</li><li>Token output 21,6M contro 10,0M dopo la deduplica (2,2×), input 2,5M contro 0,8M (3×).</li><li>Il duplicato tipico è un record per content block con la stessa usage.</li></ul> |
| 3.7 | Claude letto per intero e bloccato oltre 50 MB | **Confermata** | Un transcript reale da 52,5 MB era in `FILE_TOO_LARGE` |
| 3.8 | Discovery Claude accetta ogni `.json` | **Confermata** | 180 sidecar `.meta.json` su disco. Nel DB c'erano 220 righe `failed / SOURCE_SCHEMA_UNSUPPORTED`, per cui ogni scan risultava `partial` |
| 3.9 | KPI e timeseries usano basi temporali diverse | **Confermata** | summary e models filtrano `sessions.updated_at`, timeseries `message_metrics.created_at` |
| 3.10 | `observed_tokens` è solo input + output | **Confermata, con un difetto in più (N3)** | |
| 3.11 | Un fix del parser non corregge il DB | **Confermata, con effetto visibile** | <ul><li>Nel DB reale c'erano 10.157 sessioni Codex "main" provenienti da 529 file, fino a 18 per file.</li><li>Sono pseudo-sessioni di un vecchio bug del parser, già corretto nel codice ma mai reimportato perché i file non erano cambiati.</li><li>9.193 di queste sessioni non hanno modello.</li></ul> |
| 3.12 | Claude non rimuove le proiezioni stale | **Confermata** | Nessun contratto `previous` / `produced` |

## 2. Finding nuovi

- **N1 - I rollout Codex forkati attribuivano i token del figlio al parent.** In 25 rollout la seconda `session_meta` (righe 2-3) è la copia di quella del parent. L'adapter spostava la sessione attiva sul parent, e tutti i `token_count` del figlio finivano sulla sessione parent, sovrascrivendone la proiezione. Ora la prima `session_meta` è la proprietaria del file e la copia del parent è contesto (`CODEX_FORK_CONTEXT_META_IGNORED`).
- **N2 - I tool result di Claude contavano come messaggi utente.** 12.776 dei 13.287 record `user` sono solo `tool_result`, quindi i messaggi utente "main" erano 6.303 invece di 438. Codex conta solo i veri turni utente: Claude ora usa la stessa semantica (esclusi anche i record `isMeta`).
- **N3 - Codex conteggiava la cache come input.** L'usage OpenAI include `cached_input_tokens` dentro `input_tokens`, mentre Claude la riporta separata. Sui dati reali: input Codex main 2,24 miliardi, di cui 2,12 miliardi di cache. Il KPI "Token osservati" confrontava quindi grandezze diverse e contava la cache Codex due volte (dentro l'input e nel `cache_read` separato). Ora l'input Codex esclude la cache, limitato a 0, e `cache_read_tokens` resta separato, come da contratto.
- **N4 - I `source_files` non più candidati rendevano una sorgente `partial` per sempre.** I vecchi sidecar non vengono più scoperti ma restavano registrati come falliti. Lo scan ora registra `full_discovery` e la diagnostica esclude i file non visti dall'ultima discovery completa (riportati come `files_no_longer_candidates`).

## 3. Implementazione

| Fase audit | Stato | Note |
|---|---|---|
| 1 - Resolver | **Fatto** | Resolver puri con env, home e piattaforma iniettabili: <ul><li>`CODEX_HOME` e `CLAUDE_CONFIG_DIR` sono autorevoli, senza unione con la home di default;</li><li>ramo macOS per Copilot, root VS Code condivisa con Cursor;</li><li>`DEFAULT_SCAN_SOURCES = ALL_SOURCES`;</li><li>`sourceRootResolution` distingue `env_override` e `default_home`.</li></ul> |
| 2 - Versioning adapter | **Fatto** | `src/adapter-versions.ts` è l'unica fonte delle versioni. Migration 17 (additiva e idempotente) aggiunge `source_files.adapter_version` e valorizza le righe esistenti con le versioni legacy registrate in `scan_runs`. Lo skip richiede anche la stessa versione, e `files_reprojected` conta i reimport. Si riprocessano solo Codex e Claude (versioni aggiornate), non Copilot, Cursor o Antigravity |
| 3 - Codex | **Fatto** | <ul><li>Classificazione da `session_meta`, esportata come funzione pura: subagente se c'è `source.subagent`, `thread_source` subagent o guardian_review, `parent_thread_id` o `agent_role`; `main` solo con evidenza di radice e almeno un messaggio; altrimenti `unknown`.</li><li>Surface ricavata dai valori `originator` verificati, altrimenti `unknown`.</li><li>`metadata_json` contiene solo `classification_reason` e `client_version`, unito a `thread_name` senza sovrascriverlo.</li><li>Gestione dei fork (N1) e normalizzazione della cache (N3).</li></ul> |
| 4 - Claude | **Fatto** | <ul><li>Streaming JSONL con limite per riga (lettore limitato condiviso con Codex, parser separati) e nessun limite di 50 MB (guard basato sulla capacità di lettura limitata dell'adapter).</li><li>Solo file `*.jsonl`.</li><li>Deduplica per `message.id` tenendo la usage più completa, più deduplica per `uuid`; tool result esclusi dai messaggi utente (N2).</li><li>Rimozione delle sessioni stale.</li><li>Contatori per file in `source_files.metadata_json`.</li></ul> |
| 5 - Semantica temporale | **Fatto** | `time_basis` additivo su summary e models: `activity` filtra per `message_metrics.created_at`; il default `session_updated` conserva il comportamento legacy. Le risposte includono `time_basis` e `time_semantics`, e la GUI usa sempre `activity`. Test: KPI token e messaggi uguali alla somma dei bucket della timeseries; boundary fixture 30 contro 180 |
| 6 - Diagnostica | **Fatto** | `analytics_status.source_diagnostics` riporta per sorgente: <ul><li>stato: available, zero_activity, root_missing, no_candidates, partial, parser_unsupported o failed;</li><li>root e tipo di risoluzione;</li><li>esiti dei file e codici di errore;</li><li>file in attesa di ricalcolo e file non più candidati;</li><li>copertura token e contatori del parser.</li></ul> Non contiene path né id. La GUI mostra una nota quando una sorgente è incompleta. Tempo di risposta di `status` sul DB reale da 450 MB: 58 ms |
| 7 - Test cross-platform | **Fatto (sintetico)** | Stessa radice logica su win32, darwin e linux; branch per OS di Copilot e Cursor; fixture Codex (una radice, un figlio forkato, un guardian) e fixture Claude (duplicati, tool result, sidecar, file oltre 51 MB, rinomina della sessione, riproiezione, migration legacy) |

Validazione sulla copia del DB reale:

| Grandezza | Prima | Dopo |
|---|---|---|
| Sessioni Codex | 10.157 main + 476 unknown | 124 main (77 desktop, 47 vscode) + 388 subagenti |
| Messaggi utente Claude main | 6.303 | 438 |
| KPI token osservati (tutto il periodo) | 381,7M | 135,8M |

Nel KPI finale Codex vale 128,7M (input senza cache) e Claude 7,1M. Il primo scan riproietta 632 file in circa 50 secondi; il secondo li salta.

## 4. Deviazioni motivate

- **Nessuno script `scripts/audit-analytics-source-coverage.mjs` (Fase 0).** Un nuovo file nella root di `scripts/` sarebbe un'area scoperta per il gate `test:affected --strict`. La baseline è stata raccolta con analisi locali in sola lettura (risultati sopra), mentre la diagnostica permanente è in `analytics_status.source_diagnostics`.
- **Il default di `time_basis` resta `session_updated`.** Il cambio è additivo, come previsto dall'audit. La GUI usa `activity`; il passaggio del default a `activity` va fatto in una milestone successiva, dopo aver verificato gli altri consumer.
- **Il guardian Codex è classificato `subagent`** (motivo `source_subagent_guardian`) e non `task`. È comunque escluso dai KPI conversazionali e resta distinguibile dal motivo.
- **Deduplica MCP di Claude (6.4.4) invariata.** Gli eventi MCP sono già idempotenti per (evento, istante, sessione), e le righe `uuid` duplicate ora vengono scartate prima dell'estrazione. Il formato non espone un id stabile della chiamata tool a livello di record.
- **Validazione macOS reale (6.7.2) non eseguita.** Non c'è una macchina macOS disponibile. La parità è verificata con resolver simulati; il confronto reale Windows/macOS resta da fare con `analytics_status.source_diagnostics`, che ora riporta root e tipo di risoluzione.
- **Normalizzazione della cache Codex (N3) fuori dal perimetro esplicito dell'audit.** È però richiesta dal contratto 3.10 (`observed = input + output`, cache separata). Cambia i totali Codex storici al primo scan, grazie al versioning; l'effetto è documentato nel README di analytics.

## 5. Azioni per l'utente

- Riavviare il server MCP `analytics-mcp-server` e la GUI: al primo avvio la migration 17 aggiunge `adapter_version` al DB reale.
- Eseguire una normale sincronizzazione (`analytics_scan` o il pulsante "Sincronizza"), senza `force` e senza cancellare il DB. I file Codex e Claude già importati vengono ricalcolati una volta (circa 1 minuto su questa macchina); le sessioni fantasma spariscono e i token vengono ricalcolati.
- I totali cambiano in modo atteso: meno sessioni e messaggi Claude "main", KPI Codex senza token di cache, subagenti Codex esclusi dai KPI conversazionali.
