Skip to main content
Glama
lucadevv

trazabilidad-mcp

by lucadevv
README.md
# Trazabilidad

Motor de trazabilidad de código full-stack: construye un **grafo de hechos** de tu app
(componentes → handlers → costuras HTTP → queries DB) y lo expone por **web**, **MCP** y **CLI**
desde una sola fuente de verdad. Local-first, sin telemetría, el código nunca sale de tu máquina.

> Estado: **v1 funcional**. El motor completo está implementado y verificado: extractor ts-morph,
> matcher de costuras HTTP, persistencia SQLite con migraciones, detección de doble-fuente, y las
> tres superficies vivas (CLI `trazar`, API REST en :8791, web en :5183, servidor MCP) y soporte
> **multi-proyecto**: un dueño apunta la herramienta a N repos reales.
> Las tres leen la MISMA query layer → una sola fuente de verdad.

## Onboarding de un comando

Requisitos: Docker + Docker Compose.

```bash
git clone <repo> && cd trazabilidad
docker compose -f docker-compose.local.yml up --build -d
```

- Web:  http://127.0.0.1:5183  (shell con first-run)
- API:  http://127.0.0.1:8791/health  (health honesto)

Ambos servicios se publican **solo en 127.0.0.1** — nunca expuestos a la LAN (decisión de
seguridad de la spec §5).

Para frenar todo:

```bash
docker compose -f docker-compose.local.yml down
```

## Ciclo de desarrollo (hot-reload)

El compose usa **bind-mount + `--watch`**, no bakea el código:

- `apps/api` corre con `bun --watch` → al guardar un `.ts`, el proceso reinicia solo.
- `apps/web` corre `vite` con `watch.usePolling` → HMR cruza el FS del contenedor en macOS.

Editás en el host, el cambio se ve sin reconstruir la imagen. Solo necesitás
`up --build` la primera vez (o tras tocar `package.json` / lockfile, para reinstalar deps).

Si cambiás dependencias: `docker compose -f docker-compose.local.yml up --build -d` recrea
los contenedores y reinstala dentro de ellos.

## Configuración de entorno

Convención de tres archivos en `env/`:

| Archivo | Tracked | Contenido |
|---|---|---|
| `env/local.env` | sí | config no-secreta (puertos, NODE_ENV) |
| `env/local.secret.env.example` | sí | **todas** las keys de secretos, sin valores |
| `env/local.secret.env` | **no** (gitignored) | tus valores reales |

**v1 no tiene secretos reales** (local-only, sin auth, sin servicios externos). El `.example`
existe para fijar la convención de cara a v2.

## Estructura (monorepo Bun + Turborepo)

```
packages/
  domain/    tipos puros del grafo (sin deps)
  core/      FactGraphBuilder + ExtractorPort (HEXAGONAL, no importa ts-morph)
  matcher/   SeamMatcher (STRATEGY, confianza descendente)
  db/        schema SQL + migraciones
  query/     query layer COMPARTIDA (fuente única que leen CLI, API y MCP)
apps/
  api/       Hono REST :8791 (bind 127.0.0.1) — /health, /index/status, /journeys, /seams, /findings
  web/       React + Vite + TanStack Router :5183 — canvas del grafo con polling de estado
  mcp/       servidor MCP stdio (SDK 1.29.0) — trace_flow, list_journeys, get_http_seams, index_status, ...
  cli/       `trazar` (index/add/projects/remove/overview/flow/journeys/seams/findings[+suppress/confirm]/serve)
examples/
  demo-login/  fixture de test: login React + Hono con plantados verificables
```

## CLI `trazar`

Todos los comandos van sobre la misma query layer que la web y el MCP:

```bash
trazar index <ruta>                 # indexa un proyecto y construye el grafo de hechos
trazar add <ruta> [--name=X]        # registra un proyecto en el registro (lo verás en la web)
trazar projects                     # lista los proyectos registrados (id, name, ruta, estado)
trazar remove <id>                  # quita un proyecto del registro
trazar overview [<ruta>]            # salud del proyecto (healthScore, capas, findings por severidad)
trazar journeys [<ruta>]            # lista los journeys (entry points de UI) detectados
trazar flow <journey> [--mermaid]   # camino completo botón→DB de un journey (texto o Mermaid)
trazar seams [<ruta>] [--level=L]   # costuras HTTP con confianza y file:line de ambos lados
trazar findings [<ruta>]            # findings de doble-fuente con evidencia y estado
trazar findings suppress <key> [--reason=...]   # suprime un finding (persiste entre re-index)
trazar findings confirm <key>                   # confirma un finding
trazar serve                        # cómo levantar API + web (modo Docker demo o host)
```

`add` y `index` son cosas **distintas**: `add` registra la ruta en el catálogo (para verla en la
web/MCP por id), `index` construye el grafo. `index` **no** auto-registra (deja un hint copiable);
`add` **no** indexa. Mirá la sección **Multi-proyecto** más abajo.

Honestidad por diseño: sin index → comando copiable; **stale** tras editar sin re-indexar → ⚠;
seams no resueltas → declaradas como huecos, nunca ocultadas. Exit codes: `0` ok · `2` uso · `1` error.

El proyecto objetivo de los comandos de lectura sale de: arg explícito > `TRAZABILIDAD_PROJECT` > cwd.

### Instalar `trazar` en el PATH

El `bin` del CLI apunta a `apps/cli/src/index.ts`, que ya tiene shebang `#!/usr/bin/env bun`.
**`bun link` NO sirve acá**: en un monorepo de workspaces solo registra el paquete para enlazarlo
en *otro* proyecto (`bun link @trazabilidad/cli`), **no** instala el binario `trazar` en el PATH
(verificado: tras `bun link`, `which trazar` → not found). Dos formas que sí funcionan:

**Opción A — symlink al bin dir de bun** (recomendada; `~/.bun/bin` ya está en tu PATH si usás bun):

```bash
chmod +x apps/cli/src/index.ts
ln -sf "$(pwd)/apps/cli/src/index.ts" ~/.bun/bin/trazar
trazar journeys examples/demo-login    # ✓ funciona por el shebang
```

**Opción B — alias/función en tu shell** (si no querés tocar `~/.bun/bin`):

```bash
# en ~/.zshrc o ~/.bashrc (ajustá la ruta absoluta del repo):
trazar() { bun "/ruta/al/repo/trazabilidad/apps/cli/src/index.ts" "$@"; }
```

Sin instalar nada, el CLI siempre corre con `bun apps/cli/src/index.ts <comando>` desde la raíz del
repo (lo que usan los ejemplos de este README).

## Multi-proyecto

Trazabilidad apunta a **N proyectos** desde un solo dueño. El catálogo de rutas vive en
`~/.trazabilidad/projects.json` (config, separado de la `.db` de cada proyecto). Cada superficie
—web, MCP, CLI— resuelve el proyecto de forma independiente; cuando no se especifica, cae al
**default** (env `TRAZABILIDAD_PROJECT`), así el demo dockerizado y los tests no se rompen.

### Dos modos de arranque

| Modo | Comando | Qué ve | Para qué |
|---|---|---|---|
| **Demo (Docker)** | `docker compose -f docker-compose.local.yml up --build -d` | solo el fixture montado en `/repo` | onboarding de un comando, conocer la herramienta |
| **Host (bun)** | `bun run api:host` + `bun run web:host` | **tus repos reales** (cualquier path absoluto del host) | trabajar sobre tu código de verdad |

La API en Docker está encajonada al volumen montado; para apuntar a tus repos reales usá el modo
host (la API corre con `bun` directo en loopback y ve todo el filesystem). Ambos publican **solo**
en `127.0.0.1`.

### Registrar y listar proyectos (CLI)

```bash
# 1) registrar una o más rutas reales (idempotente; imprime el id estable)
trazar add ~/code/mi-api          # → ✓ registrado "mi-api" — id mi-api-3f9a2c
trazar add ~/code/mi-web --name="Front"

# 2) listar el registro con estado (id, name, ruta, ✓/✗ index, ⚠ si la ruta no existe)
trazar projects

# 3) construir el grafo de cada uno (index ≠ add; corré index aparte)
trazar index ~/code/mi-api

# 4) quitar uno del registro (no borra la .db ni el código)
trazar remove mi-api-3f9a2c
```

El **`id`** es estable y se deriva del path **absoluto** (`slug(basename)-hash6`), no del basename:
dos repos `api` en carpetas distintas obtienen ids distintos, sin colisión.

> **El registro es CLI-only.** No hay `POST /add` ni `POST /index` en la API: registrar e indexar
> se hacen por el CLI en el host (decisión v1 — la API es de solo lectura sobre el grafo). La web y
> el MCP **leen** el catálogo, pero quien lo escribe es `trazar add` / `trazar remove`.

### Selector en la web

En modo host (`bun run web:host`), el sidebar muestra un **dropdown** que lista `GET /projects`. El
proyecto activo vive en la **URL** como segmento (`/p/<id>/overview`, deep-linkeable). Al elegir otro
proyecto, todas las vistas (overview/journeys/seams/findings/status) se re-scopean a ese proyecto.
Si un proyecto está sin indexar o su ruta ya no existe, aparece con un punto rojo y un estado honesto.
Sin proyectos registrados → estado vacío con CTAs copiables (`trazar add` / `trazar index`).

### El param `project` del MCP

Cada tool del MCP acepta un `project` opcional (el `id` del registro). Con `project=<id>` responde
sobre **ese** proyecto; sin el param, sobre el default (`TRAZABILIDAD_PROJECT`). La tool
`list_projects` devuelve el catálogo para que el agente descubra los ids.

```jsonc
// un agente consulta dos proyectos en la MISMA sesión, sin reconectar:
{ "tool": "overview",      "arguments": { "project": "mi-api-3f9a2c" } }
{ "tool": "list_journeys", "arguments": { "project": "mi-web-b71e04" } }
{ "tool": "overview",      "arguments": {} }   // ← default (back-compat)
```

Las tres superficies pueden operar sobre **proyectos distintos a la vez** sin interferir: son procesos
independientes que leen `.db`s distintas, y cada uno cae al default cuando no se especifica.

## Flujo completo con el demo

```bash
bun install

# 1) indexar el fixture → 22 nodos, 14 aristas, 3 seams (1 finding de doble-fuente)
bun apps/cli/src/index.ts index examples/demo-login

# 2) ver los journeys de UI detectados (onSubmit, signOut)
bun apps/cli/src/index.ts journeys examples/demo-login

# 3) trazar el journey de login botón→DB (con el puente honesto del extractor)
bun apps/cli/src/index.ts flow onSubmit examples/demo-login

# 4) ver el finding estrella: doble fuente de verdad de `user`
bun apps/cli/src/index.ts findings examples/demo-login

# 5) levantar API + web (consumen la MISMA verdad que el CLI)
TRAZABILIDAD_PROJECT="$(pwd)/examples/demo-login" bun apps/api/src/index.ts   # :8791
cd apps/web && bun run dev                                                    # :5183
```

Si editás un `.tsx` del demo sin re-indexar, todas las superficies (CLI, `/index/status`, `/health`,
web) reportan **stale** con ⚠ — la verdad sigue siendo honesta hasta que vuelvas a `trazar index`.

## Conectar el MCP a Claude Code / Claude Desktop / Cursor

El servidor MCP (`apps/mcp`) expone el grafo de hechos por **stdio** (`trace_flow`, `list_journeys`,
`get_http_seams`, `index_status`, `find_data_providers`, `list_findings`, `overview` y `list_projects`)
leyendo la MISMA query layer que el CLI y la API. Cada tool acepta un `project` opcional (el `id` del
registro) para apuntar a un proyecto puntual; sin ese param usa la env `TRAZABILIDAD_PROJECT` (back-compat).
Ver la sección **Multi-proyecto** arriba. El MCP solo LEE — indexá antes con `trazar index <ruta>`.

**Claude Code** — un comando (verificá tu sintaxis con `claude mcp add --help`):

```bash
claude mcp add trazabilidad \
  -e TRAZABILIDAD_PROJECT="$(pwd)/examples/demo-login" \
  -- bun "$(pwd)/apps/mcp/src/index.ts"
```

Comprobá la conexión con `claude mcp get trazabilidad` (debe decir `Status: ✓ Connected`).
Quitalo con `claude mcp remove trazabilidad`. Usá `-s project` para escribirlo en un `.mcp.json`
versionable del repo, o `-s user` para que esté disponible en todos tus proyectos.

**JSON equivalente** (`.mcp.json` en la raíz del repo, o el bloque `mcpServers` de la config de
Claude Desktop / Cursor). Usá **rutas absolutas** — `${PWD}` lo expande Claude Code; otros clientes
pueden no hacerlo, poné la ruta completa:

```json
{
  "mcpServers": {
    "trazabilidad": {
      "type": "stdio",
      "command": "bun",
      "args": ["/ruta/absoluta/al/repo/apps/mcp/src/index.ts"],
      "env": {
        "TRAZABILIDAD_PROJECT": "/ruta/absoluta/al/repo/examples/demo-login"
      }
    }
  }
}
```

## El fixture: `examples/demo-login`

App de login completa (React + Hono) que es el **contrato de test** del producto. Planta a
propósito tres situaciones que los lotes 2 y 5 deben detectar (marcadas con `// FIXTURE:`):

1. **Doble fuente de verdad de `user`**: `Dashboard.tsx` lo lee de `AuthContext`,
   `Profile.tsx` lo lee directo de `localStorage`. → Finding `double-source` (LOTE 5).
2. **Fetch dinámico** (`fetchProfile`, URL template en runtime) → seam **no-resuelta** visible (LOTE 2).
3. **Fetch literal** `fetch('/api/auth/login')` → costura `literal` con el handler Hono (LOTE 2).

Typecheck del fixture:

```bash
bun install
bun run --filter '@trazabilidad/example-demo-login' typecheck
```

## Limitaciones conocidas v1

Trazabilidad declara sus huecos en vez de ocultarlos. Lo que **v1 todavía NO cubre**:

- **`sessionStorage` y claves dinámicas no se detectan como fuente de estado.** El extractor
  reconoce `localStorage` con clave literal y el Context explícito; una doble-fuente sobre
  `sessionStorage` (o `localStorage` con una clave armada en runtime) **no** genera finding y
  `find_data_providers` devuelve 0 proveedores. Cobertura ampliable en v2.
- **`finding_key` vs renames.** La identidad de un finding (para que la supresión persista entre
  re-index) se deriva del contenido del hecho; si renombrás el símbolo/archivo involucrado, la key
  cambia y una supresión previa **no** se reasocia automáticamente al finding "equivalente".
- **Puente del flujo botón→DB es heurístico.** Cuando el extractor no puede unir dos tramos con
  certeza (p. ej. una URL armada en runtime), declara la costura como **no-resuelta** (hueco visible)
  en lugar de inventar una arista — preferimos un hueco honesto a una conexión falsa.
- **La API es de SOLO LECTURA sobre el grafo + supresión/confirmación de findings.** El indexado se
  dispara únicamente por CLI (`trazar index`); no hay `POST /index` en v1 (cola + progreso es backlog v2).
- **DB local single-project, single-writer.** La base SQLite usa `journal_mode=DELETE` (no WAL): se
  comparte host↔contenedor por bind-mount sobre VirtioFS, donde el `-shm` de WAL no es coherente y
  causaba "disk I/O error" intermitentes y estados divergentes. DELETE serializa escritor↔lectores,
  aceptable para un dev local; no es una DB pensada para concurrencia multi-cliente.
- **Sin auth ni multi-tenant.** Local-first, bind 127.0.0.1, sin telemetría; multi-usuario/remoto es
  la visión SaaS de v2, no v1.