# Analisi tecnica - docs-node roots e relocation

Data: 2026-09-01  
Repository: `sophiadeveloper/mcp-servers`  
Branch di sviluppo: `feat/docs-node-roots-relocation`  
Baseline: `master` @ `ed331f5cd16a6842b6c35f5471e614df67af0de6`

## Obiettivo

Rendere le roots documentali un concetto first-class in `docs-node`, consentire la migrazione non distruttiva degli scaffali legacy basati su `scan_source` di tipo `file`, e aggiungere operazioni sicure per spostare documenti o intere roots senza ricreare i documenti e senza perdere tag, mind-map, link documentali o link documento-codice.

## Vincoli di progetto applicati

- Compatibilita legacy preservata salvo esplicita migrazione richiesta dall'utente.
- Migrazioni storage versionate, incrementali e non distruttive.
- Schemi MCP stretti; gli array dichiarano sempre `items`.
- Pattern `action` conservato e nuove mutazioni separate dal tool storico `docs_management` per evitare ulteriore crescita del relativo schema.
- Nessuna nuova operazione nel protocollo `change_log`: gli spostamenti restano aggiornamenti della stessa entita e vengono sincronizzati come `upsert`.
- Modalita `remote-server` read-only preservata; `docs_roots` non viene esposto da remoto.
- Test locale/CI conforme al gate `npm run test:affected -- --strict --include test:docs`.

## Stato precedente

`scan_sources` supportava gia sorgenti `file` e `folder`; una sorgente `folder` era quindi gia, semanticamente, una root. Gli scaffali legacy venivano pero backfillati con una sorgente `file` per documento. Non esistevano API dedicate per:

- elencare le roots;
- migrare in modo esplicito un shelf file-only a roots folder-aware;
- spostare un documento mantenendo lo stesso `documents.id`/`uuid`;
- spostare o riassociare una root mantenendo lo stesso UUID della sorgente;
- osservare lo stato di una relocation in caso di errore tra filesystem e SQLite.

Una semplice riscan dopo uno spostamento fisico poteva creare un nuovo documento, perche l'upsert storico identifica il documento anche tramite `file_path`. Le relazioni, invece, sono ancorate agli ID/UUID del documento: mantenendo la stessa riga e aggiornando solo path/source si preservano tag, `document_links`, `concept_links` e `document_code_links`.

## Decisioni architetturali

### Root identity

La root e una `scan_source` attiva con `source_type='folder'`. Il suo `scan_sources.uuid` e il `root_uuid` stabile. Non viene introdotta una tabella roots ridondante e non vengono generati nuovi UUID per folder source esistenti.

### Tool dedicato

E introdotto il tool locale `docs_roots` con azioni:

- `list_roots`
- `add_root`
- `migrate_shelf_roots`
- `move_documents`
- `move_root`
- `rebind_root`
- `get_operation_status`

Le mutazioni sono dry-run-first: `apply=false` e il default.

### Risorse MCP

Le roots sono esposte come resource stabile `docs://root/{root_uuid}` e vengono aggiunte in modo additivo a `docs_management.list_shelves` tramite `root_count` e `roots`.

In `remote-server` i percorsi locali vengono redatti: la root resource non espone ne `local_path` ne `documents[].file_path`.

## Migrazione legacy shelf -> roots

`migrate_shelf_roots` richiede una o piu roots reali fornite esplicitamente dall'utente/agente. Il sistema non deduce automaticamente una root dal common ancestor, perche un antenato comune puo includere partizioni logicamente distinte.

Il dry-run classifica ogni documento come:

- `covered`: appartiene univocamente a una root richiesta;
- `already_root_managed`: e gia associato alla folder source corretta;
- `unmapped`: non appartiene ad alcuna root;
- `ambiguous`: appartiene a piu roots richieste.

L'apply e consentito solo con `unmapped=0` e `ambiguous=0`.

Durante l'apply:

1. le folder source vengono create o riattivate tramite il percorso gia esistente `ensureScanSource`;
2. i documenti vengono riassociati aggiornando `source_id` sulla stessa riga;
3. `syncDocumentEntityById` aggiorna metadata hash/versione senza cambiare UUID;
4. le vecchie file source vengono tombstonate solo se non piu referenziate;
5. tag e relazioni non vengono ricreati.

La migrazione gestisce anche shelf misti, quindi il criterio e per-documento e non un semplice flag shelf-level.

## Relocation documenti

`move_documents` supporta in questa milestone spostamenti nello stesso shelf verso una root registrata. Il dry-run valida:

- root disponibile;
- documenti scrivibili e file esistenti;
- destinazione dentro la root;
- escape tramite symlink;
- collisioni nel filesystem e nel batch;
- presenza di link Markdown relativi, segnalati come warning ma non riscritti automaticamente.

L'apply usa staging temporaneo per evitare collisioni/cicli nel batch, esegue rename sullo stesso filesystem, aggiorna `file_path` e `source_id` sulla stessa riga e poi sincronizza l'entita. La collision policy corrente e `fail`.

Gli spostamenti cross-device (`EXDEV`) non sono eseguiti automaticamente: non viene modificato il DB e viene indicato il percorso sicuro `spostamento esterno + rebind_root`.

## Relocation root

`move_root` sposta fisicamente l'intera directory e aggiorna la stessa folder source, preservando il suo UUID. I nuovi path documento sono calcolati mantenendo il path relativo alla root precedente.

`rebind_root` non sposta file: valida che il contenuto atteso esista nella nuova posizione e riallinea `scan_sources.source_path` e `documents.file_path`. E il percorso di recovery previsto per spostamenti effettuati da IDE, Git, strumenti esterni o tra filesystem diversi.

Sono bloccate roots sovrapposte/nidificate e roots che contengono documenti attivi non ancora associati alla root: in tale caso va eseguita prima `migrate_shelf_roots`.

## Consistenza tra filesystem e SQLite

Filesystem e SQLite non possono essere atomici nello stesso commit. Per questo lo schema viene portato a v12 aggiungendo:

- `storage_operations`
- `storage_operation_items`

Stati principali:

- `planned`
- `staging`
- `filesystem_applied`
- `completed`
- `rolled_back`
- `recovery_required`

Se il filesystem viene modificato ma la transazione DB fallisce, il servizio tenta un rollback compensativo. Se il DB e gia stato committato e fallisce solo la finalizzazione del journal, non viene eseguito un rollback fisico incoerente: l'operazione resta applicata e viene marcata `recovery_required`.

`get_operation_status` espone journal e items per diagnosi/recovery.

## Schema v12 e compatibilita

La migrazione v12 e additiva e non modifica UUID, path o relazioni dei dati esistenti. Crea soltanto le tabelle di journal e relativi indici. La rappresentazione delle roots riusa le folder `scan_sources` gia presenti nelle versioni precedenti.

Restano invariati:

- Manifest V1/V2 import legacy;
- Manifest V3 corrente;
- URI shelf/document legacy e UUID-based;
- protocollo sync `upsert/delete`;
- read-only remote replicas.

Dopo l'aggiornamento e richiesto il riavvio di docs-node affinche la migrazione venga applicata all'avvio.

## Sicurezza e path handling

L'implementazione normalizza i path e usa confronto case-insensitive su Windows. Una root gia registrata viene riusata anche quando cambia soltanto il casing del path.

Le destinazioni vengono validate rispetto alla real root per impedire path traversal e symlink escape. Le roots sovrapposte vengono rifiutate per evitare ownership ambigua dei documenti. Il controllo di containment distingue correttamente il segmento padre `..` da nomi validi che iniziano con due punti, ad esempio `..valid`.

## Test introdotti e regressione

E stato aggiunto `tests/smoke/docs-node-storage-relocation.smoke.mjs` e registrato in `test:docs`.

Copertura osservata:

- shelf legacy file-only -> root-aware, dry-run e apply;
- tombstone delle file source legacy non piu referenziate;
- esposizione root in `list_shelves`;
- move documento con identita/tag/link invariati;
- warning link Markdown relativi;
- move root;
- rebind root dopo spostamento esterno;
- resource `docs://root/{uuid}`;
- operation journal;
- symlink escape;
- directory figlia valida con nome che inizia per `..`;
- failure forzato di finalizzazione journal dopo commit DB;
- schema migration v12 su DB vuoto e legacy;
- remote-server root resource senza leak di path locali;
- regressione dell'intera suite `test:docs` inclusi sync, replica, remote push, mind-map, concept graph, codebase inventory e stale docs.

Il gate eseguito piu volte durante sviluppo e review e stato:

```text
npm run test:affected -- --strict --include test:docs
```

Tutti i round finali hanno completato con successo la suite `test:docs`.

## Review e findings risolti

La review e stata eseguita sul delta `master...feat/docs-node-roots-relocation` seguendo il workflow della skill `mcp-code-reviewer`.

Finding corretti:

1. **Recovery dopo COMMIT DB**: evitato rollback filesystem quando il DB e gia committato; il journal passa a `recovery_required`.
2. **Symlink/path safety**: aggiunti controlli per impedire destinazioni risolte fuori dalla root.
3. **Remote path disclosure**: la root resource in remote-server redige sia `local_path` sia `documents[].file_path` e il comportamento e coperto da smoke test.
4. **Windows casing**: `add_root` riusa una root esistente con confronto path case-insensitive coerente con il resto di docs-node.
5. **Containment di path validi `..name`**: corretto il test che confondeva qualunque prefisso `..` con un parent traversal; aggiunta copertura smoke dedicata.

### Verdetto finale review

`OK`

Scope verificato: diff completo `master...feat/docs-node-roots-relocation`, migrazione schema v12, API/resource MCP, compatibilita legacy/sync, remote-server, path handling, relocation/recovery, smoke test e documentazione tecnica.

Correzioni richieste residue: nessuna.

## Limiti deliberati della milestone

- `move_documents` non supporta cross-shelf.
- `move_documents` e `move_root` non fanno automaticamente copy/delete cross-device.
- I link Markdown relativi non vengono riscritti automaticamente.
- La correlazione/mind-map strutturale resta invariata; eventuali segnali euristici derivati dalla prossimita del path possono richiedere una successiva rivalutazione.
- Non viene introdotto un Manifest V4 o una separazione formale tra root logica e binding locale; rimane un possibile sviluppo futuro se diventasse necessario rendere i binding di root portabili tra macchine.

## Criterio di accettazione raggiunto

Uno shelf legacy composto esclusivamente da file-level `scan_sources` puo essere convertito esplicitamente a uno shelf con una o piu folder roots tramite operazione dry-run-first, senza cambiare UUID di shelf/documenti e senza perdita o ricreazione di tag, document links, concept links o document-code links. Successivamente documenti e roots possono essere spostati/rebindati preservando la stessa identita logica e con journal di recovery per le operazioni fisiche.
