---
name: build_obsidian_graph
kind: function
lang: py
domain: obsidian
version: "1.0.0"
purity: impure
signature: "def build_obsidian_graph(vault_dir: str, include_dangling: bool = True) -> dict"
description: "Construye el grafo agregado (nodos + aristas) de un vault de Obsidian leyendo todas sus notas .md. Cada nota es un nodo tipado (tipo por carpeta o frontmatter, id = slug, label = frontmatter['nombre'] o slug) y cada wikilink [[...]] del cuerpo es una arista dirigida resuelta por slug del ultimo segmento del destino. Los wikilinks rotos se incluyen como nodos fantasma dangling o se descartan segun include_dangling. Compone list_obsidian_notes, read_obsidian_note y slugify_obsidian_name del grupo obsidian. Es la pieza que cierra la frontera 'el grupo obsidian no indexa el grafo agregado'. Base de la vista grafo (sigma.js) de la app osint_web."
tags: [obsidian, graph, vault, nodes, edges, wikilinks, sigma, osint]
uses_functions: ["list_obsidian_notes_py_obsidian", "read_obsidian_note_py_obsidian", "slugify_obsidian_name_py_obsidian"]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: ["os", "re"]
params:
  - name: vault_dir
    desc: "ruta (absoluta o relativa) a la raiz del vault de Obsidian a indexar; se excluyen .obsidian/ y .trash/"
  - name: include_dangling
    desc: "si True (por defecto) los wikilinks que no resuelven a ninguna nota generan un nodo fantasma con dangling=True y su arista; si False, esos enlaces rotos se descartan"
output: "dict con 'nodes' (lista de {id: slug, tipo: str, label: str, frontmatter: dict}; los fantasma anaden dangling=True y frontmatter vacio) y 'edges' (lista de {source: slug, target: slug, kind: str} deduplicada; kind es relacion/lugar/documento segun la seccion ## donde aparece el wikilink, o 'wikilink' por defecto)"
tested: true
tests:
  - "golden grafo del mini-vault con nodos y aristas esperados"
  - "resuelve wikilink con acentos maria del mar al slug"
  - "el kind de la arista sale de la seccion del cuerpo"
  - "dangling marcado con true y excluido con false"
  - "el tipo cae a la carpeta si falta en frontmatter"
  - "wikilink sintacticamente roto no tumba el grafo"
  - "vault inexistente lanza filenotfounderror"
test_file_path: "python/functions/obsidian/build_obsidian_graph_test.py"
file_path: "python/functions/obsidian/build_obsidian_graph.py"
---

## Ejemplo

```python
import sys, os
sys.path.insert(0, os.path.join("python", "functions"))
from obsidian.build_obsidian_graph import build_obsidian_graph

graph = build_obsidian_graph("/home/enmanuel/Obsidian/osint")
print(len(graph["nodes"]), "nodos,", len(graph["edges"]), "aristas")
# 1199 nodos (984 reales + 215 fantasma), 618 aristas

# Un nodo tipado listo para sigma.js: color por tipo, label visible.
n = next(x for x in graph["nodes"] if x["tipo"] == "persona")
print(n["id"], n["label"], n["tipo"])   # ana-gomez Ana Gómez persona

# Aristas con su kind (relacion / lugar / documento / wikilink).
for e in graph["edges"][:3]:
    print(e["source"], "->", e["target"], f"({e['kind']})")
```

Lanzable directo sobre el vault real (imprime conteo de nodos/aristas/dangling):

```bash
cd /home/enmanuel/fn_registry
PYTHONPATH=python/functions python/.venv/bin/python3 \
  python/functions/obsidian/build_obsidian_graph.py /home/enmanuel/Obsidian/osint
```

## Cuando usarla

Cuando necesites el grafo entero de un vault de Obsidian de una sola pasada: para pintar una vista de nodos navegable (sigma.js / graphology), para detectar objetivos OSINT aun sin fichar (nodos `dangling`), o para alimentar el endpoint `/api/graph` de una app que lee el vault sin BD intermedia. Es el agregado que las funciones atomicas del grupo `obsidian` (`list_obsidian_notes`, `read_obsidian_note`) no daban por si solas.

## Gotchas

- **Lee todo el vault de disco** (I/O impuro): el grafo refleja el estado de los `.md` en ese instante; vuelve a llamar para refrescar tras editar notas.
- **El `id` de un nodo es el slug = nombre de archivo sin `.md`**. Si dos notas en carpetas distintas comparten ese nombre (caso real en el vault osint: `dni.md`, `fotos.md`, `certificado-digital.md` repetidos dentro de cada subcarpeta `personas/<slug>/`), colapsan al **mismo nodo** y solo la primera en orden alfabetico sobrevive. Por eso el vault con 1022 `.md` produce 984 nodos reales (13 grupos de slugs colisionan). Para evitarlo habria que usar el path relativo como id — se dejo el slug por compatibilidad con la resolucion de wikilinks `[[slug]]`.
- **Resolucion de wikilinks por ultimo segmento**: `[[organizaciones/acme-sl|Acme SL]]` resuelve a la nota cuyo slug es `acme-sl`; el alias (`|...`) y el ancla (`#...`) se ignoran. Nombres con acentos/mayusculas (`[[María del Mar]]`) se slugifican con `slugify_obsidian_name` antes de buscar, asi que resuelven igual que `[[maria-del-mar]]`.
- **`kind` por seccion es heuristico**: depende del texto del encabezado `## ...` mas cercano por encima del wikilink (`Relaciones`/`Relacionado` -> `relacion`, `Lugares` -> `lugar`, `Documentos` -> `documento`, resto -> `wikilink`). Un wikilink fuera de cualquier seccion conocida es `wikilink`.
- **Auto-enlaces ignorados**: si una nota se enlaza a si misma, esa arista no se emite.
- **Nodos fantasma** (`dangling: true`) llevan `tipo: "desconocido"` y `frontmatter` vacio; no representan un `.md` en disco. Con `include_dangling=False` no aparecen ni ellos ni sus aristas.
- Lanza `FileNotFoundError` si `vault_dir` no existe y `NotADirectoryError` si no es un directorio (heredado de `list_obsidian_notes`). Una nota individual ilegible se omite sin tumbar el grafo.
