Merge branch 'main' into feat/understandignore

This commit is contained in:
Lum1104
2026-04-12 11:34:10 +08:00
25 changed files with 2497 additions and 45 deletions
+1
View File
@@ -0,0 +1 @@
patreon: Lum1104
+286
View File
@@ -0,0 +1,286 @@
<h1 align="center">Understand Anything</h1>
<p align="center">
<strong>Convierte cualquier código fuente, base de conocimiento o documentación en un grafo de conocimiento interactivo que puedes explorar, buscar y consultar.</strong>
<br />
<em>Compatible con Claude Code, Codex, Cursor, Copilot, Gemini CLI y más.</em>
</p>
<p align="center">
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ja-JP.md">日本語</a> | <a href="README.ko-KR.md">한국어</a> | <a href="README.es-ES.md">Español</a> | <a href="README.tr-TR.md">Türkçe</a>
</p>
<p align="center">
<a href="https://www.star-history.com/lum1104/understand-anything">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything" />
<img alt="Star History Rank" src="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything" />
</picture>
</a>
</p>
<p align="center">
<a href="#-inicio-rápido"><img src="https://img.shields.io/badge/Inicio_Rápido-blue" alt="Quick Start" /></a>
<a href="https://github.com/Lum1104/Understand-Anything/blob/main/LICENSE"><img src="https://img.shields.io/badge/Licencia-MIT-yellow" alt="License: MIT" /></a>
<a href="https://docs.anthropic.com/en/docs/claude-code"><img src="https://img.shields.io/badge/Claude_Code-8A2BE2" alt="Claude Code" /></a>
<a href="#codex"><img src="https://img.shields.io/badge/Codex-000000" alt="Codex" /></a>
<a href="#vs-code--github-copilot"><img src="https://img.shields.io/badge/Copilot-24292e" alt="Copilot" /></a>
<a href="#gemini-cli"><img src="https://img.shields.io/badge/Gemini_CLI-4285F4" alt="Gemini CLI" /></a>
<a href="#opencode"><img src="https://img.shields.io/badge/OpenCode-38bdf8" alt="OpenCode" /></a>
<a href="https://lum1104.github.io/Understand-Anything"><img src="https://img.shields.io/badge/Página_Principal-d4a574" alt="Homepage" /></a>
<a href="https://lum1104.github.io/Understand-Anything/demo/"><img src="https://img.shields.io/badge/Demo_en_Vivo-00c853" alt="Live Demo" /></a>
</p>
<p align="center">
<img src="assets/hero.jpg" alt="Understand Anything — Convierte cualquier código fuente en un grafo de conocimiento interactivo" width="800" />
</p>
---
> [!TIP]
> **¡Un enorme agradecimiento a la comunidad!** El apoyo a Understand-Anything ha sido increíble. Si esta herramienta te ahorra unos minutos de buscar entre la complejidad, eso es todo lo que quería. 🚀
**Acabas de unirte a un nuevo equipo. El código tiene 200,000 líneas. ¿Por dónde empiezas?**
Understand Anything es un plugin de [Claude Code](https://docs.anthropic.com/en/docs/claude-code) que analiza tu proyecto con un pipeline multi-agente, construye un grafo de conocimiento de cada archivo, función, clase y dependencia, y luego te ofrece un panel interactivo para explorarlo visualmente. Deja de leer código a ciegas. Empieza a ver el panorama completo.
---
## ✨ Características
### Explora el grafo estructural
Navega tu código como un grafo de conocimiento interactivo: cada archivo, función y clase es un nodo que puedes hacer clic, buscar y explorar. Selecciona cualquier nodo para ver resúmenes en lenguaje natural, relaciones y recorridos guiados.
<p align="center">
<img src="assets/overview-structural.gif" alt="Grafo estructural — explora archivos, funciones, clases y sus relaciones" width="750" />
</p>
### Comprende la lógica de negocio
Cambia a la vista de dominio y observa cómo tu código se mapea a procesos de negocio reales: dominios, flujos y pasos representados como un grafo horizontal.
<p align="center">
<img src="assets/overview-domain.gif" alt="Grafo de dominio — dominios de negocio, flujos y pasos de proceso" width="750" />
</p>
### Analiza bases de conocimiento
Apunta `/understand-knowledge` a un [wiki LLM con patrón Karpathy](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) y obtén un grafo de conocimiento dirigido por fuerzas con agrupación por comunidad. El parser determinístico extrae wikilinks y categorías de `index.md`, luego los agentes LLM descubren relaciones implícitas, extraen entidades y revelan afirmaciones, convirtiendo tu wiki en un grafo navegable de ideas interconectadas.
<table>
<tr>
<td width="50%" valign="top">
<h3>🧭 Recorridos Guiados</h3>
<p>Recorridos generados automáticamente de la arquitectura, ordenados por dependencia. Aprende el código en el orden correcto.</p>
</td>
<td width="50%" valign="top">
<h3>🔍 Búsqueda Difusa y Semántica</h3>
<p>Encuentra cualquier cosa por nombre o por significado. Busca "¿qué partes manejan la autenticación?" y obtén resultados relevantes en todo el grafo.</p>
</td>
</tr>
<tr>
<td width="50%" valign="top">
<h3>📊 Análisis de Impacto de Cambios</h3>
<p>Visualiza qué partes del sistema afectan tus cambios antes de hacer commit. Comprende los efectos en cascada a través del código.</p>
</td>
<td width="50%" valign="top">
<h3>🎭 Interfaz Adaptativa por Persona</h3>
<p>El panel ajusta su nivel de detalle según quién eres: desarrollador junior, PM o usuario avanzado.</p>
</td>
</tr>
<tr>
<td width="50%" valign="top">
<h3>🏗️ Visualización por Capas</h3>
<p>Agrupación automática por capa arquitectónica — API, Servicio, Datos, UI, Utilidades — con leyenda codificada por colores.</p>
</td>
<td width="50%" valign="top">
<h3>📚 Conceptos del Lenguaje</h3>
<p>12 patrones de programación (genéricos, closures, decoradores, etc.) explicados en contexto donde aparecen.</p>
</td>
</tr>
</table>
---
## 🚀 Inicio Rápido
### 1. Instala el plugin
```bash
/plugin marketplace add Lum1104/Understand-Anything
/plugin install understand-anything
```
### 2. Analiza tu código
```bash
/understand
```
Un pipeline multi-agente escanea tu proyecto, extrae cada archivo, función, clase y dependencia, y construye un grafo de conocimiento guardado en `.understand-anything/knowledge-graph.json`.
### 3. Explora el panel
```bash
/understand-dashboard
```
Se abre un panel web interactivo con tu código visualizado como un grafo, codificado por colores según la capa arquitectónica, con funciones de búsqueda y clic. Selecciona cualquier nodo para ver su código, relaciones y una explicación en lenguaje natural.
### 4. Sigue aprendiendo
```bash
# Pregunta cualquier cosa sobre el código
/understand-chat How does the payment flow work?
# Analiza el impacto de tus cambios actuales
/understand-diff
# Profundiza en un archivo o función específica
/understand-explain src/auth/login.ts
# Genera una guía de incorporación para nuevos miembros del equipo
/understand-onboard
# Extrae conocimiento de dominio de negocio (dominios, flujos, pasos)
/understand-domain
# Analiza un wiki LLM con patrón Karpathy
/understand-knowledge ~/path/to/wiki
```
---
## 🌐 Instalación Multiplataforma
Understand-Anything funciona en múltiples plataformas de codificación con IA.
### Claude Code (Nativo)
```bash
/plugin marketplace add Lum1104/Understand-Anything
/plugin install understand-anything
```
### Codex
Dile a Codex:
```
Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.codex/INSTALL.md
```
### OpenCode
Dile a OpenCode:
```
Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.opencode/INSTALL.md
```
### OpenClaw
Dile a OpenClaw:
```
Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.openclaw/INSTALL.md
```
### Cursor
Cursor detecta automáticamente el plugin a través de `.cursor-plugin/plugin.json` cuando se clona este repositorio. No requiere instalación manual: simplemente clona y abre en Cursor.
### VS Code + GitHub Copilot
VS Code con GitHub Copilot (v1.108+) detecta automáticamente el plugin a través de `.copilot-plugin/plugin.json` cuando se clona este repositorio. No requiere instalación manual: simplemente clona y abre en VS Code.
Para habilidades personales (disponibles en todos los proyectos), dile a GitHub Copilot:
```text
Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.vscode/INSTALL.md
```
### Antigravity
Dile a Antigravity:
```text
Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.antigravity/INSTALL.md
```
### Gemini CLI
Dile a Gemini CLI:
```text
Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.gemini/INSTALL.md
```
### Pi Agent
Dile a Pi Agent:
```text
Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.pi/INSTALL.md
```
### Compatibilidad de Plataformas
| Plataforma | Estado | Método de Instalación |
|----------|--------|----------------|
| Claude Code | ✅ Nativo | Marketplace de plugins |
| Codex | ✅ Soportado | Instalación guiada por IA |
| OpenCode | ✅ Soportado | Instalación guiada por IA |
| OpenClaw | ✅ Soportado | Instalación guiada por IA |
| Cursor | ✅ Soportado | Detección automática |
| VS Code + GitHub Copilot | ✅ Soportado | Detección automática |
| Antigravity | ✅ Soportado | Instalación guiada por IA |
| Gemini CLI | ✅ Soportado | Instalación guiada por IA |
| Pi Agent | ✅ Soportado | Instalación guiada por IA |
---
## 🔧 Bajo el Capó
### Pipeline Multi-Agente
El comando `/understand` orquesta 5 agentes especializados, y `/understand-domain` añade un sexto:
| Agente | Rol |
|-------|------|
| `project-scanner` | Descubre archivos, detecta lenguajes y frameworks |
| `file-analyzer` | Extrae funciones, clases e importaciones; produce nodos y aristas del grafo |
| `architecture-analyzer` | Identifica capas arquitectónicas |
| `tour-builder` | Genera recorridos de aprendizaje guiados |
| `graph-reviewer` | Valida la completitud y la integridad referencial del grafo (se ejecuta inline por defecto; usa `--review` para una revisión completa con LLM) |
| `domain-analyzer` | Extrae dominios de negocio, flujos y pasos de proceso (usado por `/understand-domain`) |
| `article-analyzer` | Extrae entidades, afirmaciones y relaciones implícitas de artículos wiki (usado por `/understand-knowledge`) |
Los analizadores de archivos se ejecutan en paralelo (hasta 5 concurrentes, 20-30 archivos por lote). Soporta actualizaciones incrementales: solo reanaliza los archivos que cambiaron desde la última ejecución.
---
## 🤝 Contribuir
¡Las contribuciones son bienvenidas! Así puedes empezar:
1. Haz fork del repositorio
2. Crea una rama de funcionalidad (`git checkout -b feature/my-feature`)
3. Ejecuta las pruebas (`pnpm --filter @understand-anything/core test`)
4. Haz commit de tus cambios y abre un pull request
Para cambios importantes, abre primero un issue para que podamos discutir el enfoque.
---
<p align="center">
<strong>Deja de leer código a ciegas. Empieza a entenderlo todo.</strong>
</p>
## Historial de Stars
<a href="https://www.star-history.com/?repos=Lum1104%2FUnderstand-Anything&type=date&legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/image?repos=Lum1104/Understand-Anything&type=date&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/image?repos=Lum1104/Understand-Anything&type=date&legend=top-left" />
<img alt="Star History Chart" src="https://api.star-history.com/image?repos=Lum1104/Understand-Anything&type=date&legend=top-left" />
</picture>
</a>
<p align="center">
Licencia MIT &copy; <a href="https://github.com/Lum1104">Lum1104</a>
</p>
+20 -2
View File
@@ -1,13 +1,23 @@
<h1 align="center">Understand Anything</h1>
<p align="center">
<strong>あらゆるコードベース、Dockerfile、ドキュメントを、探索・検索・質問ができるインタラクティブなナレッジグラフに変換します。</strong>
<strong>あらゆるコードベース、ナレッジベース、ドキュメントを、探索・検索・質問ができるインタラクティブなナレッジグラフに変換します。</strong>
<br />
<em>Claude Code、Codex、Cursor、Copilot、Gemini CLI など、マルチプラットフォーム対応。</em>
</p>
<p align="center">
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ja-JP.md">日本語</a> | <a href="README.tr-TR.md">Türkçe</a>
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ja-JP.md">日本語</a> | <a href="README.ko-KR.md">한국어</a> | <a href="README.es-ES.md">Español</a> | <a href="README.tr-TR.md">Türkçe</a>
</p>
<p align="center">
<a href="https://www.star-history.com/lum1104/understand-anything">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything" />
<img alt="Star History Rank" src="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything" />
</picture>
</a>
</p>
<p align="center">
@@ -55,6 +65,10 @@ Understand Anything は [Claude Code](https://docs.anthropic.com/en/docs/claude-
<img src="assets/overview-domain.gif" alt="ドメイングラフ——ビジネスドメイン、フロー、プロセスステップ" width="750" />
</p>
### ナレッジベースを分析
`/understand-knowledge` を [Karpathy パターンの LLM Wiki](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) に向けると、コミュニティクラスタリング付きのフォースディレクテッドナレッジグラフが生成されます。決定論的パーサーが `index.md` から wikilinks とカテゴリを抽出し、LLM エージェントが暗黙の関係を発見、エンティティを抽出、主張を浮き彫りにして、wiki をナビゲート可能な相互接続されたアイデアのグラフに変換します。
<table>
<tr>
<td width="50%" valign="top">
@@ -132,6 +146,9 @@ Understand Anything は [Claude Code](https://docs.anthropic.com/en/docs/claude-
# ビジネスドメイン知識を抽出(ドメイン、フロー、ステップ)
/understand-domain
# Karpathy パターンの LLM Wiki ナレッジベースを分析
/understand-knowledge ~/path/to/wiki
```
---
@@ -232,6 +249,7 @@ Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Und
| `tour-builder` | ガイド学習ツアーの生成 |
| `graph-reviewer` | グラフの完全性と参照整合性の検証 |
| `domain-analyzer` | ビジネスドメイン、フロー、処理ステップの抽出(`/understand-domain` で使用) |
| `article-analyzer` | wiki 記事からエンティティ、主張、暗黙の関係を抽出(`/understand-knowledge` で使用) |
ファイルアナライザーは並列実行されます(最大3つ同時)。インクリメンタル更新に対応しており、前回の実行から変更されたファイルのみを再分析します。
+286
View File
@@ -0,0 +1,286 @@
<h1 align="center">Understand Anything</h1>
<p align="center">
<strong>모든 코드베이스, 지식 베이스 또는 문서를 탐색, 검색, 질문할 수 있는 인터랙티브 지식 그래프로 변환합니다.</strong>
<br />
<em>Claude Code, Codex, Cursor, Copilot, Gemini CLI 등 다양한 플랫폼을 지원합니다.</em>
</p>
<p align="center">
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ja-JP.md">日本語</a> | <a href="README.ko-KR.md">한국어</a> | <a href="README.es-ES.md">Español</a> | <a href="README.tr-TR.md">Türkçe</a>
</p>
<p align="center">
<a href="https://www.star-history.com/lum1104/understand-anything">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything" />
<img alt="Star History Rank" src="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything" />
</picture>
</a>
</p>
<p align="center">
<a href="#-빠른-시작"><img src="https://img.shields.io/badge/빠른_시작-blue" alt="Quick Start" /></a>
<a href="https://github.com/Lum1104/Understand-Anything/blob/main/LICENSE"><img src="https://img.shields.io/badge/라이선스-MIT-yellow" alt="License: MIT" /></a>
<a href="https://docs.anthropic.com/en/docs/claude-code"><img src="https://img.shields.io/badge/Claude_Code-8A2BE2" alt="Claude Code" /></a>
<a href="#codex"><img src="https://img.shields.io/badge/Codex-000000" alt="Codex" /></a>
<a href="#vs-code--github-copilot"><img src="https://img.shields.io/badge/Copilot-24292e" alt="Copilot" /></a>
<a href="#gemini-cli"><img src="https://img.shields.io/badge/Gemini_CLI-4285F4" alt="Gemini CLI" /></a>
<a href="#opencode"><img src="https://img.shields.io/badge/OpenCode-38bdf8" alt="OpenCode" /></a>
<a href="https://lum1104.github.io/Understand-Anything"><img src="https://img.shields.io/badge/홈페이지-d4a574" alt="Homepage" /></a>
<a href="https://lum1104.github.io/Understand-Anything/demo/"><img src="https://img.shields.io/badge/라이브_데모-00c853" alt="Live Demo" /></a>
</p>
<p align="center">
<img src="assets/hero.jpg" alt="Understand Anything — 모든 코드베이스를 인터랙티브 지식 그래프로 변환" width="800" />
</p>
---
> [!TIP]
> **커뮤니티의 엄청난 응원에 감사드립니다!** Understand-Anything에 대한 관심이 정말 놀라웠습니다. 이 도구가 복잡한 코드를 파악하는 시간을 단 몇 분이라도 줄여드린다면, 그것만으로도 충분합니다. 🚀
**새 팀에 합류했습니다. 코드베이스가 20만 줄입니다. 어디서부터 시작하시겠습니까?**
Understand Anything은 [Claude Code](https://docs.anthropic.com/en/docs/claude-code) 플러그인으로, 멀티 에이전트 파이프라인을 통해 프로젝트를 분석하고, 모든 파일, 함수, 클래스, 의존성에 대한 지식 그래프를 구축한 뒤, 이를 시각적으로 탐색할 수 있는 인터랙티브 대시보드를 제공합니다. 더 이상 코드를 맹목적으로 읽지 마세요. 전체 그림을 파악하세요.
---
## ✨ 주요 기능
### 구조 그래프 탐색
코드베이스를 인터랙티브 지식 그래프로 탐색하세요. 모든 파일, 함수, 클래스가 클릭, 검색, 탐색 가능한 노드입니다. 노드를 선택하면 이해하기 쉬운 요약, 관계, 가이드 투어를 확인할 수 있습니다.
<p align="center">
<img src="assets/overview-structural.gif" alt="구조 그래프 — 파일, 함수, 클래스 및 관계 탐색" width="750" />
</p>
### 비즈니스 로직 이해
도메인 뷰로 전환하면 코드가 실제 비즈니스 프로세스에 어떻게 매핑되는지 확인할 수 있습니다. 도메인, 흐름, 단계가 수평 그래프로 표시됩니다.
<p align="center">
<img src="assets/overview-domain.gif" alt="도메인 그래프 — 비즈니스 도메인, 흐름 및 프로세스 단계" width="750" />
</p>
### 지식 베이스 분석
`/understand-knowledge`를 [Karpathy 패턴 LLM 위키](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f)에 연결하면 커뮤니티 클러스터링이 적용된 힘 기반 지식 그래프를 생성합니다. 결정론적 파서가 `index.md`에서 위키링크와 카테고리를 추출한 후, LLM 에이전트가 암묵적 관계를 발견하고, 엔티티를 추출하며, 주장을 도출하여 위키를 탐색 가능한 상호 연결된 아이디어 그래프로 변환합니다.
<table>
<tr>
<td width="50%" valign="top">
<h3>🧭 가이드 투어</h3>
<p>의존성 순서에 따라 자동 생성되는 아키텍처 워크스루입니다. 올바른 순서로 코드베이스를 학습하세요.</p>
</td>
<td width="50%" valign="top">
<h3>🔍 퍼지 및 시맨틱 검색</h3>
<p>이름 또는 의미로 무엇이든 검색하세요. "인증을 처리하는 부분은?" 같은 질문으로 그래프 전체에서 관련 결과를 얻을 수 있습니다.</p>
</td>
</tr>
<tr>
<td width="50%" valign="top">
<h3>📊 변경 영향 분석</h3>
<p>커밋 전에 변경 사항이 시스템의 어떤 부분에 영향을 미치는지 확인하세요. 코드베이스 전반의 파급 효과를 이해하세요.</p>
</td>
<td width="50%" valign="top">
<h3>🎭 페르소나 적응형 UI</h3>
<p>사용자 유형(주니어 개발자, PM, 파워 유저)에 따라 대시보드의 상세 수준이 자동으로 조정됩니다.</p>
</td>
</tr>
<tr>
<td width="50%" valign="top">
<h3>🏗️ 레이어 시각화</h3>
<p>아키텍처 레이어별 자동 그룹화 — API, 서비스, 데이터, UI, 유틸리티 — 색상 코드 범례가 함께 제공됩니다.</p>
</td>
<td width="50%" valign="top">
<h3>📚 프로그래밍 개념</h3>
<p>12가지 프로그래밍 패턴(제네릭, 클로저, 데코레이터 등)이 코드에 등장하는 맥락에서 설명됩니다.</p>
</td>
</tr>
</table>
---
## 🚀 빠른 시작
### 1. 플러그인 설치
```bash
/plugin marketplace add Lum1104/Understand-Anything
/plugin install understand-anything
```
### 2. 코드베이스 분석
```bash
/understand
```
멀티 에이전트 파이프라인이 프로젝트를 스캔하고, 모든 파일, 함수, 클래스, 의존성을 추출한 뒤, `.understand-anything/knowledge-graph.json`에 지식 그래프를 저장합니다.
### 3. 대시보드 탐색
```bash
/understand-dashboard
```
코드베이스가 그래프로 시각화된 인터랙티브 웹 대시보드가 열립니다. 아키텍처 레이어별로 색상이 구분되어 있으며, 검색과 클릭이 가능합니다. 노드를 선택하면 코드, 관계, 이해하기 쉬운 설명을 확인할 수 있습니다.
### 4. 더 깊이 탐구하기
```bash
# 코드베이스에 대해 무엇이든 질문하기
/understand-chat How does the payment flow work?
# 현재 변경 사항의 영향 분석
/understand-diff
# 특정 파일이나 함수를 심층 분석
/understand-explain src/auth/login.ts
# 새 팀원을 위한 온보딩 가이드 생성
/understand-onboard
# 비즈니스 도메인 지식 추출 (도메인, 흐름, 단계)
/understand-domain
# Karpathy 패턴 LLM 위키 지식 베이스 분석
/understand-knowledge ~/path/to/wiki
```
---
## 🌐 멀티 플랫폼 설치
Understand-Anything은 다양한 AI 코딩 플랫폼에서 사용할 수 있습니다.
### Claude Code (네이티브)
```bash
/plugin marketplace add Lum1104/Understand-Anything
/plugin install understand-anything
```
### Codex
Codex에 입력하세요:
```
Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.codex/INSTALL.md
```
### OpenCode
OpenCode에 입력하세요:
```
Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.opencode/INSTALL.md
```
### OpenClaw
OpenClaw에 입력하세요:
```
Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.openclaw/INSTALL.md
```
### Cursor
이 저장소를 클론하면 Cursor가 `.cursor-plugin/plugin.json`을 통해 플러그인을 자동으로 인식합니다. 수동 설치가 필요 없습니다. 클론 후 Cursor에서 열기만 하면 됩니다.
### VS Code + GitHub Copilot
GitHub Copilot(v1.108+)이 설치된 VS Code는 `.copilot-plugin/plugin.json`을 통해 플러그인을 자동으로 인식합니다. 수동 설치가 필요 없습니다. 클론 후 VS Code에서 열기만 하면 됩니다.
모든 프로젝트에서 사용하려면(개인 스킬) GitHub Copilot에 입력하세요:
```text
Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.vscode/INSTALL.md
```
### Antigravity
Antigravity에 입력하세요:
```text
Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.antigravity/INSTALL.md
```
### Gemini CLI
Gemini CLI에 입력하세요:
```text
Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.gemini/INSTALL.md
```
### Pi Agent
Pi Agent에 입력하세요:
```text
Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.pi/INSTALL.md
```
### 플랫폼 호환성
| 플랫폼 | 상태 | 설치 방법 |
|----------|--------|----------------|
| Claude Code | ✅ 네이티브 | 플러그인 마켓플레이스 |
| Codex | ✅ 지원 | AI 기반 설치 |
| OpenCode | ✅ 지원 | AI 기반 설치 |
| OpenClaw | ✅ 지원 | AI 기반 설치 |
| Cursor | ✅ 지원 | 자동 인식 |
| VS Code + GitHub Copilot | ✅ 지원 | 자동 인식 |
| Antigravity | ✅ 지원 | AI 기반 설치 |
| Gemini CLI | ✅ 지원 | AI 기반 설치 |
| Pi Agent | ✅ 지원 | AI 기반 설치 |
---
## 🔧 작동 원리
### 멀티 에이전트 파이프라인
`/understand` 명령은 5개의 전문 에이전트를 조율하며, `/understand-domain`은 6번째 에이전트를 추가합니다:
| 에이전트 | 역할 |
|-------|------|
| `project-scanner` | 파일 탐색, 언어 및 프레임워크 감지 |
| `file-analyzer` | 함수, 클래스, 임포트 추출; 그래프 노드 및 엣지 생성 |
| `architecture-analyzer` | 아키텍처 레이어 식별 |
| `tour-builder` | 가이드 학습 투어 생성 |
| `graph-reviewer` | 그래프 완전성 및 참조 무결성 검증 (기본적으로 인라인 실행; 전체 LLM 검토는 `--review` 사용) |
| `domain-analyzer` | 비즈니스 도메인, 흐름 및 프로세스 단계 추출 (`/understand-domain`에서 사용) |
| `article-analyzer` | 위키 문서에서 엔티티, 주장 및 암묵적 관계 추출 (`/understand-knowledge`에서 사용) |
파일 분석기는 병렬로 실행됩니다(최대 5개 동시, 배치당 20~30개 파일). 증분 업데이트를 지원하여 마지막 실행 이후 변경된 파일만 재분석합니다.
---
## 🤝 기여하기
기여를 환영합니다! 시작하는 방법은 다음과 같습니다:
1. 저장소를 Fork합니다
2. 기능 브랜치를 생성합니다 (`git checkout -b feature/my-feature`)
3. 테스트를 실행합니다 (`pnpm --filter @understand-anything/core test`)
4. 변경 사항을 커밋하고 Pull Request를 생성합니다
주요 변경 사항의 경우, 먼저 Issue를 열어 접근 방식을 논의해 주세요.
---
<p align="center">
<strong>더 이상 코드를 맹목적으로 읽지 마세요. 모든 것을 이해하세요.</strong>
</p>
## Star 히스토리
<a href="https://www.star-history.com/?repos=Lum1104%2FUnderstand-Anything&type=date&legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/image?repos=Lum1104/Understand-Anything&type=date&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/image?repos=Lum1104/Understand-Anything&type=date&legend=top-left" />
<img alt="Star History Chart" src="https://api.star-history.com/image?repos=Lum1104/Understand-Anything&type=date&legend=top-left" />
</picture>
</a>
<p align="center">
MIT 라이선스 &copy; <a href="https://github.com/Lum1104">Lum1104</a>
</p>
+20 -2
View File
@@ -1,13 +1,23 @@
<h1 align="center">Understand Anything</h1>
<p align="center">
<strong>Turn any codebase, Dockerfile, or docs into an interactive knowledge graph you can explore, search, and ask questions about.</strong>
<strong>Turn any codebase, knowledge base, or docs into an interactive knowledge graph you can explore, search, and ask questions about.</strong>
<br />
<em>Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, and more.</em>
</p>
<p align="center">
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ja-JP.md">日本語</a> | <a href="README.tr-TR.md">Türkçe</a>
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ja-JP.md">日本語</a> | <a href="README.ko-KR.md">한국어</a> | <a href="README.es-ES.md">Español</a> | <a href="README.tr-TR.md">Türkçe</a>
</p>
<p align="center">
<a href="https://www.star-history.com/lum1104/understand-anything">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything" />
<img alt="Star History Rank" src="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything" />
</picture>
</a>
</p>
<p align="center">
@@ -55,6 +65,10 @@ Switch to the domain view and see how your code maps to real business processes
<img src="assets/overview-domain.gif" alt="Domain graph — business domains, flows, and process steps" width="750" />
</p>
### Analyze knowledge bases
Point `/understand-knowledge` at a [Karpathy-pattern LLM wiki](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) and get a force-directed knowledge graph with community clustering. The deterministic parser extracts wikilinks and categories from `index.md`, then LLM agents discover implicit relationships, extract entities, and surface claims — turning your wiki into a navigable graph of interconnected ideas.
<table>
<tr>
<td width="50%" valign="top">
@@ -132,6 +146,9 @@ An interactive web dashboard opens with your codebase visualized as a graph —
# Extract business domain knowledge (domains, flows, steps)
/understand-domain
# Analyze a Karpathy-pattern LLM wiki knowledge base
/understand-knowledge ~/path/to/wiki
```
---
@@ -232,6 +249,7 @@ The `/understand` command orchestrates 5 specialized agents, and `/understand-do
| `tour-builder` | Generate guided learning tours |
| `graph-reviewer` | Validate graph completeness and referential integrity (runs inline by default; use `--review` for full LLM review) |
| `domain-analyzer` | Extract business domains, flows, and process steps (used by `/understand-domain`) |
| `article-analyzer` | Extract entities, claims, and implicit relationships from wiki articles (used by `/understand-knowledge`) |
File analyzers run in parallel (up to 5 concurrent, 20-30 files per batch). Supports incremental updates — only re-analyzes files that changed since the last run.
+20 -2
View File
@@ -1,13 +1,23 @@
<h1 align="center">Understand Anything</h1>
<p align="center">
<strong>Herhangi bir kod tabanını, Dockerfile'ı veya dokümantasyonu keşfedebileceğin, arayabileceğin ve hakkında sorular sorabileceğin interaktif bir bilgi grafiğine dönüştür.</strong>
<strong>Herhangi bir kod tabanını, bilgi tabanını veya dokümantasyonu keşfedebileceğin, arayabileceğin ve hakkında sorular sorabileceğin interaktif bir bilgi grafiğine dönüştür.</strong>
<br />
<em>Claude Code, Codex, Cursor, Copilot, Gemini CLI ve daha fazlasıyla çalışır.</em>
</p>
<p align="center">
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ja-JP.md">日本語</a> | <a href="README.tr-TR.md">Türkçe</a>
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ja-JP.md">日本語</a> | <a href="README.ko-KR.md">한국어</a> | <a href="README.es-ES.md">Español</a> | <a href="README.tr-TR.md">Türkçe</a>
</p>
<p align="center">
<a href="https://www.star-history.com/lum1104/understand-anything">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything" />
<img alt="Star History Rank" src="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything" />
</picture>
</a>
</p>
<p align="center">
@@ -55,6 +65,10 @@ Alan görünümüne geçin ve kodunuzun gerçek iş süreçleriyle nasıl eşle
<img src="assets/overview-domain.gif" alt="Alan grafiği — iş alanları, akışlar ve süreç adımları" width="750" />
</p>
### Bilgi tabanlarını analiz et
`/understand-knowledge` komutunu bir [Karpathy deseni LLM Wiki'sine](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) yönlendirin ve topluluk kümeleme ile kuvvet yönelimli bir bilgi grafiği elde edin. Deterministik ayrıştırıcı `index.md`'den wikilinkleri ve kategorileri çıkarır, ardından LLM ajanları örtük ilişkileri keşfeder, varlıkları çıkarır ve iddiaları ortaya çıkarır — wiki'nizi gezinilebilir, birbirine bağlı fikirler grafiğine dönüştürür.
<table>
<tr>
<td width="50%" valign="top">
@@ -132,6 +146,9 @@ Kod tabanın bir grafik olarak görselleştirilmiş, mimari katmana göre renkle
# İş alanı bilgisini çıkar (alanlar, akışlar, adımlar)
/understand-domain
# Karpathy deseni LLM Wiki bilgi tabanını analiz et
/understand-knowledge ~/path/to/wiki
```
---
@@ -232,6 +249,7 @@ Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Und
| `tour-builder` | Rehberli öğrenme turları oluştur |
| `graph-reviewer` | Grafik bütünlüğünü ve referans bütünlüğünü doğrula |
| `domain-analyzer` | İş alanları, akışlar ve işlem adımlarını çıkar (`/understand-domain` tarafından kullanılır) |
| `article-analyzer` | Wiki makalelerinden varlıkları, iddiaları ve örtük ilişkileri çıkar (`/understand-knowledge` tarafından kullanılır) |
Dosya analizörleri paralel çalışır (en fazla 3 eşzamanlı). Artımlı güncellemeleri destekler — yalnızca son çalıştırmadan bu yana değişen dosyaları yeniden analiz eder.
+20 -2
View File
@@ -1,12 +1,22 @@
<h1 align="center">Understand Anything</h1>
<p align="center">
<strong>将任意代码库、Dockerfile 或文档转化为可探索、可搜索、可对话的交互式知识图谱</strong>
<strong>将任意代码库、知识库或文档转化为可探索、可搜索、可对话的交互式知识图谱</strong>
<br />
<em>支持 Claude Code、Codex、Cursor、Copilot、Gemini CLI 等多平台。</em>
</p>
<p align="center">
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ja-JP.md">日本語</a> | <a href="README.tr-TR.md">Türkçe</a>
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ja-JP.md">日本語</a> | <a href="README.ko-KR.md">한국어</a> | <a href="README.es-ES.md">Español</a> | <a href="README.tr-TR.md">Türkçe</a>
</p>
<p align="center">
<a href="https://www.star-history.com/lum1104/understand-anything">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything" />
<img alt="Star History Rank" src="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything" />
</picture>
</a>
</p>
<p align="center">
@@ -54,6 +64,10 @@ Understand Anything 是一个基于 [Claude Code](https://docs.anthropic.com/en/
<img src="assets/overview-domain.gif" alt="领域图——业务领域、流程和处理步骤" width="750" />
</p>
### 分析知识库
`/understand-knowledge` 指向一个 [Karpathy 模式的 LLM Wiki](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f),即可获得带有社区聚类的力导向知识图谱。确定性解析器从 `index.md` 中提取 wikilinks 和分类,然后 LLM 代理发现隐式关系、提取实体并挖掘论断——将你的 wiki 转化为可导航的互联思想图谱。
<table>
<tr>
<td width="50%" valign="top">
@@ -131,6 +145,9 @@ Understand Anything 是一个基于 [Claude Code](https://docs.anthropic.com/en/
# 提取业务领域知识(领域、流程、步骤)
/understand-domain
# 分析 Karpathy 模式的 LLM Wiki 知识库
/understand-knowledge ~/path/to/wiki
```
---
@@ -231,6 +248,7 @@ Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Und
| `tour-builder` | 生成引导式学习路径 |
| `graph-reviewer` | 验证图的完整性和引用完整性 |
| `domain-analyzer` | 提取业务领域、流程和处理步骤(由 `/understand-domain` 使用) |
| `article-analyzer` | 从 wiki 文章中提取实体、论断和隐式关系(由 `/understand-knowledge` 使用) |
文件分析器并行运行(最多 3 个并发)。支持增量更新 — 仅重新分析自上次运行以来发生更改的文件。
+20 -2
View File
@@ -1,12 +1,22 @@
<h1 align="center">Understand Anything</h1>
<p align="center">
<strong>將任意程式碼庫、Dockerfile 或文件轉化為可探索、可搜尋、可對話的互動式知識圖譜</strong>
<strong>將任意程式碼庫、知識庫或文件轉化為可探索、可搜尋、可對話的互動式知識圖譜</strong>
<br />
<em>支援 Claude Code、Codex、Cursor、Copilot、Gemini CLI 等多平台。</em>
</p>
<p align="center">
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ja-JP.md">日本語</a> | <a href="README.tr-TR.md">Türkçe</a>
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ja-JP.md">日本語</a> | <a href="README.ko-KR.md">한국어</a> | <a href="README.es-ES.md">Español</a> | <a href="README.tr-TR.md">Türkçe</a>
</p>
<p align="center">
<a href="https://www.star-history.com/lum1104/understand-anything">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything" />
<img alt="Star History Rank" src="https://api.star-history.com/badge?repo=Lum1104/Understand-Anything" />
</picture>
</a>
</p>
<p align="center">
@@ -54,6 +64,10 @@ Understand Anything 是一個基於 [Claude Code](https://docs.anthropic.com/en/
<img src="assets/overview-domain.gif" alt="領域圖——業務領域、流程和處理步驟" width="750" />
</p>
### 分析知識庫
`/understand-knowledge` 指向一個 [Karpathy 模式的 LLM Wiki](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f),即可獲得帶有社群聚類的力導向知識圖譜。確定性解析器從 `index.md` 中提取 wikilinks 和分類,然後 LLM 代理發現隱式關係、提取實體並挖掘論斷——將你的 wiki 轉化為可導航的互聯思想圖譜。
<table>
<tr>
<td width="50%" valign="top">
@@ -131,6 +145,9 @@ Understand Anything 是一個基於 [Claude Code](https://docs.anthropic.com/en/
# 提取業務領域知識(領域、流程、步驟)
/understand-domain
# 分析 Karpathy 模式的 LLM Wiki 知識庫
/understand-knowledge ~/path/to/wiki
```
---
@@ -231,6 +248,7 @@ Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Und
| `tour-builder` | 產生引導式學習路徑 |
| `graph-reviewer` | 驗證圖的完整性和參考完整性 |
| `domain-analyzer` | 提取業務領域、流程和處理步驟(由 `/understand-domain` 使用) |
| `article-analyzer` | 從 wiki 文章中提取實體、論斷和隱式關係(由 `/understand-knowledge` 使用) |
檔案分析器並行執行(最多 3 個並發)。支援增量更新 — 僅重新分析自上次執行以來發生變更的檔案。
+28 -9
View File
@@ -85,6 +85,9 @@ importers:
'@xyflow/react':
specifier: ^12.0.0
version: 12.10.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
d3-force:
specifier: ^3.0.0
version: 3.0.0
devlop:
specifier: ^1.1.0
version: 1.1.0
@@ -107,6 +110,9 @@ importers:
'@tailwindcss/vite':
specifier: ^4.0.0
version: 4.2.1(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.3))
'@types/d3-force':
specifier: ^3.0.10
version: 3.0.10
'@types/react':
specifier: ^19.0.0
version: 19.2.14
@@ -1014,6 +1020,9 @@ packages:
'@types/d3-drag@3.0.7':
resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==}
'@types/d3-force@3.0.10':
resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==}
'@types/d3-interpolate@3.0.4':
resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
@@ -1320,10 +1329,18 @@ packages:
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
engines: {node: '>=12'}
d3-force@3.0.0:
resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==}
engines: {node: '>=12'}
d3-interpolate@3.0.1:
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
engines: {node: '>=12'}
d3-quadtree@3.0.1:
resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==}
engines: {node: '>=12'}
d3-selection@3.0.0:
resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
engines: {node: '>=12'}
@@ -3352,6 +3369,8 @@ snapshots:
dependencies:
'@types/d3-selection': 3.0.11
'@types/d3-force@3.0.10': {}
'@types/d3-interpolate@3.0.4':
dependencies:
'@types/d3-color': 3.1.3
@@ -3454,14 +3473,6 @@ snapshots:
chai: 5.3.3
tinyrainbow: 2.0.0
'@vitest/mocker@3.2.4(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.3))':
dependencies:
'@vitest/spy': 3.2.4
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.3)
'@vitest/mocker@3.2.4(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.3))':
dependencies:
'@vitest/spy': 3.2.4
@@ -3769,10 +3780,18 @@ snapshots:
d3-ease@3.0.1: {}
d3-force@3.0.0:
dependencies:
d3-dispatch: 3.0.1
d3-quadtree: 3.0.1
d3-timer: 3.0.1
d3-interpolate@3.0.1:
dependencies:
d3-color: 3.1.0
d3-quadtree@3.0.1: {}
d3-selection@3.0.0: {}
d3-timer@3.0.1: {}
@@ -5306,7 +5325,7 @@ snapshots:
dependencies:
'@types/chai': 5.2.3
'@vitest/expect': 3.2.4
'@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.3))
'@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.3))
'@vitest/pretty-format': 3.2.4
'@vitest/runner': 3.2.4
'@vitest/snapshot': 3.2.4
@@ -0,0 +1,93 @@
---
name: article-analyzer
description: |
Analyzes markdown files using pre-parsed structural data and LLM inference to extract knowledge graph nodes and edges (entities, claims, implicit relationships, topic clustering).
model: inherit
---
# Article Analyzer Agent
You are a knowledge graph extraction expert. Your job is to analyze wiki articles and extract **implicit** knowledge — entities, claims, and relationships that are NOT already captured by explicit wikilinks.
## Input
You will receive a batch of articles as a JSON array. Each article has:
- `id`: the article node ID (e.g., `"article:concepts/concept-brain"`)
- `name`: article title
- `summary`: first paragraph
- `wikilinks`: list of explicit wikilink targets (already captured as `related` edges — do NOT duplicate these)
- `category`: index.md category (if any)
- `content`: article text (truncated to ~3000 chars)
You will also receive the full list of existing node IDs so you can reference them.
## Task
For each article in the batch, extract:
### 1. Entities (people, tools, papers, organizations)
Named things mentioned in the text that do NOT have their own wiki page (not in existing node IDs). Create `entity` nodes.
- `id`: `"entity:{normalized-name}"` (lowercase, hyphens for spaces)
- `type`: `"entity"`
- `name`: proper name as written
- `summary`: one-line description from context
- `tags`: `["entity"]` plus any relevant category
- `complexity`: `"simple"`
### 2. Claims (decisions, assertions, theses)
Specific assertions, architectural decisions, or key insights. Create `claim` nodes.
- `id`: `"claim:{article-stem}:{short-slug}"` (e.g., `"claim:decision-typescript-python:ts-core-py-clones"`)
- `type`: `"claim"`
- `name`: short claim title
- `summary`: the assertion itself (1-2 sentences)
- `tags`: `["claim"]` plus category
- `complexity`: `"simple"`
### 3. Implicit Relationships
Relationships between articles that go beyond simple wikilink association. Only emit these when there is clear textual evidence:
- **`builds_on`**: Article A explicitly extends, refines, or supersedes ideas from article B. Weight: 0.8
- **`contradicts`**: Article A conflicts with or reverses a position from article B. Weight: 0.9
- **`exemplifies`**: An entity or article is a concrete example of a concept. Weight: 0.7
- **`authored_by`**: Article attributed to a specific entity (person/agent). Weight: 0.6
- **`cites`**: Article references a raw source document. Weight: 0.7
Edge format:
```json
{
"source": "article:...",
"target": "article:... or entity:... or claim:... or source:...",
"type": "builds_on",
"direction": "forward",
"weight": 0.8,
"description": "Brief reason for this relationship"
}
```
## Rules
1. **Do NOT duplicate wikilink edges.** The parse script already created `related` edges for every `[[wikilink]]`. Your job is to find what the wikilinks missed.
2. **Be conservative.** Only create edges with clear textual evidence. A vague thematic similarity is not enough.
3. **Deduplicate entities.** If the same person/tool appears in multiple articles, create the entity node once.
4. **Use existing IDs.** When creating edges to existing articles, use their exact `id` from the provided node list.
5. **Keep it small.** For a batch of 10-15 articles, expect ~5-15 entities, ~5-10 claims, and ~10-20 implicit edges. Don't over-extract.
## Output Format
Write a JSON file to `$INTERMEDIATE_DIR/analysis-batch-$BATCH_NUM.json`:
```json
{
"nodes": [
{ "id": "entity:...", "type": "entity", "name": "...", "summary": "...", "tags": [...], "complexity": "simple" },
{ "id": "claim:...", "type": "claim", "name": "...", "summary": "...", "tags": [...], "complexity": "simple" }
],
"edges": [
{ "source": "...", "target": "...", "type": "builds_on", "direction": "forward", "weight": 0.8, "description": "..." }
]
}
```
Do NOT include any article or topic nodes in your output — those already exist from the parse script. Only output NEW entity nodes, claim nodes, and implicit edges.
@@ -1,6 +1,6 @@
import { z } from "zod";
// Edge types (29 values across 7 categories)
// Edge types (35 values across 8 categories)
export const EdgeTypeSchema = z.enum([
"imports", "exports", "contains", "inherits", "implements", // Structural
"calls", "subscribes", "publishes", "middleware", // Behavioral
@@ -10,6 +10,7 @@ export const EdgeTypeSchema = z.enum([
"deploys", "serves", "provisions", "triggers", // Infrastructure
"migrates", "documents", "routes", "defines_schema", // Schema/Data
"contains_flow", "flow_step", "cross_domain", // Domain
"cites", "contradicts", "builds_on", "exemplifies", "categorized_under", "authored_by", // Knowledge
]);
// Aliases that LLMs commonly generate instead of canonical node types
@@ -55,6 +56,22 @@ export const NODE_TYPE_ALIASES: Record<string, string> = {
business_process: "flow",
task: "step",
business_step: "step",
// Knowledge aliases
note: "article",
page: "article",
wiki_page: "article",
person: "entity",
actor: "entity",
organization: "entity",
tag: "topic",
category: "topic",
theme: "topic",
assertion: "claim",
decision: "claim",
thesis: "claim",
reference: "source",
raw: "source",
paper: "source",
};
// Aliases that LLMs commonly generate instead of canonical edge types
@@ -88,6 +105,20 @@ export const EDGE_TYPE_ALIASES: Record<string, string> = {
has_flow: "contains_flow",
next_step: "flow_step",
interacts_with: "cross_domain",
// Knowledge aliases
references: "cites",
cites_source: "cites",
conflicts_with: "contradicts",
disagrees_with: "contradicts",
refines: "builds_on",
elaborates: "builds_on",
illustrates: "exemplifies",
instance_of: "exemplifies",
example_of: "exemplifies",
belongs_to: "categorized_under",
tagged_with: "categorized_under",
written_by: "authored_by",
created_by: "authored_by",
// Note: "implemented_by" is intentionally NOT aliased to "implements" —
// it inverts edge direction (see commit fd0df15). The LLM should use
// "implements" with correct source/target instead.
@@ -327,6 +358,13 @@ const DomainMetaSchema = z.object({
entryType: z.enum(["http", "cli", "event", "cron", "manual"]).optional(),
}).passthrough();
const KnowledgeMetaSchema = z.object({
wikilinks: z.array(z.string()).optional(),
backlinks: z.array(z.string()).optional(),
category: z.string().optional(),
content: z.string().optional(),
}).passthrough();
export const GraphNodeSchema = z.object({
id: z.string(),
type: z.enum([
@@ -334,6 +372,7 @@ export const GraphNodeSchema = z.object({
"config", "document", "service", "table", "endpoint",
"pipeline", "schema", "resource",
"domain", "flow", "step",
"article", "entity", "topic", "claim", "source",
]),
name: z.string(),
filePath: z.string().optional(),
@@ -343,6 +382,7 @@ export const GraphNodeSchema = z.object({
complexity: z.enum(["simple", "moderate", "complex"]),
languageNotes: z.string().optional(),
domainMeta: DomainMetaSchema.optional(),
knowledgeMeta: KnowledgeMetaSchema.optional(),
}).passthrough();
export const GraphEdgeSchema = z.object({
@@ -380,6 +420,7 @@ export const ProjectMetaSchema = z.object({
export const KnowledgeGraphSchema = z.object({
version: z.string(),
kind: z.enum(["codebase", "knowledge"]).optional(),
project: ProjectMetaSchema,
nodes: z.array(GraphNodeSchema),
edges: z.array(GraphEdgeSchema),
@@ -1,11 +1,12 @@
// Node types (16 total: 5 code + 8 non-code + 3 domain)
// Node types (21 total: 5 code + 8 non-code + 3 domain + 5 knowledge)
export type NodeType =
| "file" | "function" | "class" | "module" | "concept"
| "config" | "document" | "service" | "table" | "endpoint"
| "pipeline" | "schema" | "resource"
| "domain" | "flow" | "step";
| "domain" | "flow" | "step"
| "article" | "entity" | "topic" | "claim" | "source";
// Edge types (29 total in 7 categories: Structural, Behavioral, Data flow, Dependencies, Semantic, Infrastructure/Schema, Domain)
// Edge types (35 total in 8 categories: Structural, Behavioral, Data flow, Dependencies, Semantic, Infrastructure/Schema, Domain, Knowledge)
export type EdgeType =
| "imports" | "exports" | "contains" | "inherits" | "implements" // Structural
| "calls" | "subscribes" | "publishes" | "middleware" // Behavioral
@@ -14,7 +15,16 @@ export type EdgeType =
| "related" | "similar_to" // Semantic
| "deploys" | "serves" | "provisions" | "triggers" // Infrastructure
| "migrates" | "documents" | "routes" | "defines_schema" // Schema/Data
| "contains_flow" | "flow_step" | "cross_domain"; // Domain
| "contains_flow" | "flow_step" | "cross_domain" // Domain
| "cites" | "contradicts" | "builds_on" | "exemplifies" | "categorized_under" | "authored_by"; // Knowledge
// Optional knowledge metadata for article/entity/topic/claim/source nodes
export interface KnowledgeMeta {
wikilinks?: string[];
backlinks?: string[];
category?: string;
content?: string;
}
// Optional domain metadata for domain/flow/step nodes
export interface DomainMeta {
@@ -25,7 +35,7 @@ export interface DomainMeta {
entryType?: "http" | "cli" | "event" | "cron" | "manual";
}
// GraphNode with 16 types: 5 code + 8 non-code + 3 domain
// GraphNode with 21 types: 5 code + 8 non-code + 3 domain + 5 knowledge
export interface GraphNode {
id: string;
type: NodeType;
@@ -37,6 +47,7 @@ export interface GraphNode {
complexity: "simple" | "moderate" | "complex";
languageNotes?: string;
domainMeta?: DomainMeta;
knowledgeMeta?: KnowledgeMeta;
}
// GraphEdge with rich relationship modeling
@@ -79,6 +90,7 @@ export interface ProjectMeta {
// Root KnowledgeGraph
export interface KnowledgeGraph {
version: string;
kind?: "codebase" | "knowledge";
project: ProjectMeta;
nodes: GraphNode[];
edges: GraphEdge[];
@@ -13,15 +13,17 @@
"@dagrejs/dagre": "^2.0.4",
"@understand-anything/core": "workspace:*",
"@xyflow/react": "^12.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"d3-force": "^3.0.0",
"devlop": "^1.1.0",
"hast-util-to-jsx-runtime": "^2.3.6",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-markdown": "^10.1.0",
"zustand": "^5.0.0"
},
"devDependencies": {
"@tailwindcss/vite": "^4.0.0",
"@types/d3-force": "^3.0.10",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.3.0",
@@ -4,6 +4,7 @@ import type { GraphIssue } from "@understand-anything/core/schema";
import { useDashboardStore } from "./store";
import GraphView from "./components/GraphView";
import DomainGraphView from "./components/DomainGraphView";
import KnowledgeGraphView from "./components/KnowledgeGraphView";
import CodeViewer from "./components/CodeViewer";
import SearchBar from "./components/SearchBar";
import NodeInfo from "./components/NodeInfo";
@@ -104,6 +105,7 @@ function Dashboard({ accessToken }: { accessToken: string }) {
const [metaTheme, setMetaTheme] = useState<ThemeConfig | null>(null);
const viewMode = useDashboardStore((s) => s.viewMode);
const setViewMode = useDashboardStore((s) => s.setViewMode);
const isKnowledgeGraph = useDashboardStore((s) => s.isKnowledgeGraph);
const domainGraph = useDashboardStore((s) => s.domainGraph);
const setDomainGraph = useDashboardStore((s) => s.setDomainGraph);
@@ -240,6 +242,11 @@ function Dashboard({ accessToken }: { accessToken: string }) {
if (result.success && result.data) {
setGraph(result.data);
setGraphIssues(result.issues);
// Auto-detect knowledge graph kind
if ((data as Record<string, unknown>).kind === "knowledge") {
setViewMode("knowledge");
useDashboardStore.getState().setIsKnowledgeGraph(true);
}
for (const issue of result.issues) {
if (issue.level === "auto-corrected") {
console.warn(`[graph] auto-corrected: ${issue.message}`);
@@ -331,7 +338,7 @@ function Dashboard({ accessToken }: { accessToken: string }) {
</h1>
<div className="w-px h-5 bg-border-subtle" />
<PersonaSelector />
{graph && domainGraph && (
{graph && !isKnowledgeGraph && domainGraph && (
<>
<div className="w-px h-5 bg-border-subtle" />
<div className="flex items-center bg-elevated rounded-lg p-0.5">
@@ -369,14 +376,17 @@ function Dashboard({ accessToken }: { accessToken: string }) {
<div className="flex items-center gap-4 w-max">
<DiffToggle />
<div className="flex items-center gap-1">
{([
{ key: "code", label: "Code", color: "var(--color-node-file)" },
{ key: "config", label: "Config", color: "var(--color-node-config)" },
{ key: "docs", label: "Docs", color: "var(--color-node-document)" },
{ key: "infra", label: "Infra", color: "var(--color-node-service)" },
{ key: "data", label: "Data", color: "var(--color-node-table)" },
{ key: "domain", label: "Domain", color: "var(--color-node-concept)" },
] as const).map((cat) => (
{(isKnowledgeGraph ? [
{ key: "knowledge" as const, label: "All", color: "var(--color-node-article)" },
] : [
{ key: "code" as const, label: "Code", color: "var(--color-node-file)" },
{ key: "config" as const, label: "Config", color: "var(--color-node-config)" },
{ key: "docs" as const, label: "Docs", color: "var(--color-node-document)" },
{ key: "infra" as const, label: "Infra", color: "var(--color-node-service)" },
{ key: "data" as const, label: "Data", color: "var(--color-node-table)" },
{ key: "domain" as const, label: "Domain", color: "var(--color-node-concept)" },
{ key: "knowledge" as const, label: "Knowledge", color: "var(--color-node-article)" },
]).map((cat) => (
<button
key={cat.key}
onClick={() => toggleNodeTypeFilter(cat.key)}
@@ -468,7 +478,9 @@ function Dashboard({ accessToken }: { accessToken: string }) {
<div className="flex-1 flex min-h-0 relative">
{/* Graph area */}
<div className="flex-1 min-w-0 min-h-0 relative">
{viewMode === "domain" && domainGraph ? (
{viewMode === "knowledge" ? (
<KnowledgeGraphView />
) : viewMode === "domain" && domainGraph ? (
<DomainGraphView />
) : (
<GraphView />
@@ -21,6 +21,11 @@ const typeColors: Record<NodeType, string> = {
domain: "var(--color-node-concept)",
flow: "var(--color-node-pipeline)",
step: "var(--color-node-function)",
article: "var(--color-node-article)",
entity: "var(--color-node-entity)",
topic: "var(--color-node-topic)",
claim: "var(--color-node-claim)",
source: "var(--color-node-source)",
};
const typeTextColors: Record<NodeType, string> = {
@@ -40,6 +45,11 @@ const typeTextColors: Record<NodeType, string> = {
domain: "text-node-concept",
flow: "text-node-pipeline",
step: "text-node-function",
article: "text-node-article",
entity: "text-node-entity",
topic: "text-node-topic",
claim: "text-node-claim",
source: "text-node-source",
};
const complexityColors: Record<string, string> = {
@@ -57,6 +57,7 @@ const NODE_TYPE_TO_CATEGORY: Record<NodeType, NodeCategory> = {
service: "infra", resource: "infra", pipeline: "infra",
table: "data", endpoint: "data", schema: "data",
domain: "domain", flow: "domain", step: "domain",
article: "knowledge", entity: "knowledge", topic: "knowledge", claim: "knowledge", source: "knowledge",
} as const;
// ── Helper components that must live inside <ReactFlow> ────────────────
@@ -0,0 +1,292 @@
import { useMemo, useCallback } from "react";
import {
ReactFlow,
ReactFlowProvider,
Background,
BackgroundVariant,
Controls,
MiniMap,
} from "@xyflow/react";
import type { Edge, Node } from "@xyflow/react";
import "@xyflow/react/dist/style.css";
import CustomNode from "./CustomNode";
import type { CustomNodeData } from "./CustomNode";
import { useDashboardStore } from "../store";
import { applyForceLayout, NODE_WIDTH, NODE_HEIGHT } from "../utils/layout";
import type { KnowledgeGraph } from "@understand-anything/core/types";
const nodeTypes = {
custom: CustomNode,
};
/** Edge style presets by knowledge edge type. */
const EDGE_STYLES: Record<string, React.CSSProperties> = {
related: { stroke: "var(--color-border-medium)", strokeWidth: 0.5, opacity: 0.12 },
cites: { stroke: "var(--color-node-source)", strokeWidth: 1.5, strokeDasharray: "6 3" },
contradicts: { stroke: "#c97070", strokeWidth: 2 },
builds_on: { stroke: "var(--color-node-claim)", strokeWidth: 1.5 },
exemplifies: { stroke: "var(--color-node-entity)", strokeWidth: 1, strokeDasharray: "3 3" },
categorized_under: { stroke: "var(--color-border-medium)", strokeWidth: 0.5, opacity: 0.08 },
authored_by: { stroke: "var(--color-node-entity)", strokeWidth: 1, strokeDasharray: "4 4" },
implements: { stroke: "var(--color-node-function)", strokeWidth: 1, opacity: 0.4 },
depends_on: { stroke: "var(--color-node-module)", strokeWidth: 1, opacity: 0.4 },
};
/** Compute node size based on connection count. */
function getNodeDimensions(
edgeCount: number,
): { width: number; height: number } {
const scale = Math.min(1.5, Math.max(0.85, 0.85 + edgeCount * 0.03));
return {
width: Math.round(NODE_WIDTH * scale),
height: Math.round(NODE_HEIGHT * scale),
};
}
/**
* Compute the stable layout (positions) from graph topology.
* This only re-runs when the graph data or filters change, NOT on selection/search.
*/
function computeLayout(
graph: KnowledgeGraph,
): { positionMap: Map<string, { x: number; y: number }>; edgeCounts: Map<string, number>; communityMap: Map<string, number> } {
const edgeCounts = new Map<string, number>();
for (const edge of graph.edges) {
edgeCounts.set(edge.source, (edgeCounts.get(edge.source) ?? 0) + 1);
edgeCounts.set(edge.target, (edgeCounts.get(edge.target) ?? 0) + 1);
}
const communityMap = new Map<string, number>();
graph.layers.forEach((layer, i) => {
for (const nodeId of layer.nodeIds) {
communityMap.set(nodeId, i);
}
});
const dims = new Map<string, { width: number; height: number }>();
for (const node of graph.nodes) {
dims.set(node.id, getNodeDimensions(edgeCounts.get(node.id) ?? 0));
}
// Build temporary nodes/edges for layout computation only
const tmpNodes: Node[] = graph.nodes.map((node) => ({
id: node.id,
type: "custom" as const,
position: { x: 0, y: 0 },
data: {},
}));
const tmpEdges: Edge[] = graph.edges.map((e, i) => ({
id: `ke-${i}`,
source: e.source,
target: e.target,
}));
const { nodes: layoutedNodes } = applyForceLayout(tmpNodes, tmpEdges, dims, communityMap);
const positionMap = new Map<string, { x: number; y: number }>();
for (const n of layoutedNodes) {
positionMap.set(n.id, n.position);
}
return { positionMap, edgeCounts, communityMap };
}
function KnowledgeGraphViewInner() {
const graph = useDashboardStore((s) => s.graph);
const selectedNodeId = useDashboardStore((s) => s.selectedNodeId);
const focusNodeId = useDashboardStore((s) => s.focusNodeId);
const selectNode = useDashboardStore((s) => s.selectNode);
const searchResultsRaw = useDashboardStore((s) => s.searchResults);
const tourHighlightedNodeIds = useDashboardStore((s) => s.tourHighlightedNodeIds);
const nodeTypeFilters = useDashboardStore((s) => s.nodeTypeFilters);
const onNodeClick = useCallback(
(nodeId: string) => selectNode(nodeId),
[selectNode],
);
const searchResults = useMemo(
() => new Map(searchResultsRaw.map((r) => [r.nodeId, r.score])),
[searchResultsRaw],
);
const tourSet = useMemo(
() => new Set(tourHighlightedNodeIds),
[tourHighlightedNodeIds],
);
// Filter graph — only recompute when graph data or filters change
const filteredGraph = useMemo((): KnowledgeGraph | null => {
if (!graph) return null;
const filteredNodes = graph.nodes.filter((n) => {
if (["article", "entity", "topic", "claim", "source"].includes(n.type)) {
return nodeTypeFilters.knowledge !== false;
}
return true;
});
const filteredNodeIds = new Set(filteredNodes.map((n) => n.id));
const filteredEdges = graph.edges.filter(
(e) => filteredNodeIds.has(e.source) && filteredNodeIds.has(e.target),
);
return { ...graph, nodes: filteredNodes, edges: filteredEdges };
}, [graph, nodeTypeFilters]);
// Compute layout ONCE per graph/filter change — stable positions
const { positionMap, edgeCounts } = useMemo(() => {
if (!filteredGraph) return { positionMap: new Map(), edgeCounts: new Map() };
return computeLayout(filteredGraph);
}, [filteredGraph]);
// Build visual nodes/edges — recomputes on selection/search/tour WITHOUT re-layout
const { nodes, edges } = useMemo(() => {
if (!filteredGraph) return { nodes: [], edges: [] };
const neighborIds = new Set<string>();
if (focusNodeId || selectedNodeId) {
const focusId = focusNodeId ?? selectedNodeId;
for (const edge of filteredGraph.edges) {
if (edge.source === focusId) neighborIds.add(edge.target);
if (edge.target === focusId) neighborIds.add(edge.source);
}
}
const rfNodes: Node[] = filteredGraph.nodes.map((node) => {
const isSelected = node.id === selectedNodeId;
const isFocused = node.id === focusNodeId;
const isNeighbor = neighborIds.has(node.id);
const isSelectionFaded =
(focusNodeId || selectedNodeId) &&
!isSelected &&
!isFocused &&
!isNeighbor;
const searchScore = searchResults.get(node.id);
const isHighlighted = searchScore !== undefined;
const isTourHighlighted = tourSet.has(node.id);
const data: CustomNodeData = {
label: node.name,
nodeType: node.type,
summary: node.summary,
complexity: node.complexity,
isHighlighted,
searchScore,
isSelected,
isTourHighlighted,
isDiffChanged: false,
isDiffAffected: false,
isDiffFaded: false,
isNeighbor,
isSelectionFaded: !!isSelectionFaded,
onNodeClick,
incomingCount: edgeCounts.get(node.id) ?? 0,
tags: node.tags,
};
return {
id: node.id,
type: "custom" as const,
position: positionMap.get(node.id) ?? { x: 0, y: 0 },
data,
};
});
const activeId = focusNodeId ?? selectedNodeId;
const rfEdges: Edge[] = filteredGraph.edges.map((e) => {
const baseStyle = EDGE_STYLES[e.type] ?? EDGE_STYLES.related;
const isConnected = activeId && (e.source === activeId || e.target === activeId);
// When a node is selected: highlight connected edges, dim the rest
let style: React.CSSProperties;
if (activeId) {
if (isConnected) {
style = {
...baseStyle,
strokeWidth: Math.max(2, (baseStyle.strokeWidth as number ?? 1) * 1.5),
opacity: 1,
};
} else {
style = { ...baseStyle, opacity: 0.04 };
}
} else {
style = baseStyle;
}
return {
id: `ke-${e.source}-${e.target}-${e.type}`,
source: e.source,
target: e.target,
style,
animated: e.type === "contradicts" && (!activeId || !!isConnected),
label: isConnected && e.type !== "related" && e.type !== "categorized_under"
? e.type.replace(/_/g, " ")
: undefined,
labelStyle: { fill: "var(--color-text-muted)", fontSize: 9, opacity: 0.7 },
labelBgStyle: { fill: "var(--color-surface)", fillOpacity: 0.9 },
labelBgPadding: [4, 2] as [number, number],
labelBgBorderRadius: 3,
};
});
return { nodes: rfNodes, edges: rfEdges };
}, [filteredGraph, selectedNodeId, focusNodeId, searchResults, tourSet, onNodeClick, positionMap, edgeCounts]);
if (!graph) {
return (
<div className="h-full flex items-center justify-center text-text-muted text-sm">
No knowledge graph available. Run /understand-knowledge to generate one.
</div>
);
}
return (
<div className="h-full w-full relative">
<ReactFlow
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
fitView
fitViewOptions={{ padding: 0.15 }}
minZoom={0.05}
maxZoom={2}
proOptions={{ hideAttribution: true }}
>
<Background
variant={BackgroundVariant.Dots}
gap={20}
size={1}
color="var(--color-border-subtle)"
/>
<Controls />
<MiniMap
nodeColor={(n) => {
const data = n.data as CustomNodeData | undefined;
const type = data?.nodeType ?? "article";
const colorMap: Record<string, string> = {
article: "var(--color-node-article)",
entity: "var(--color-node-entity)",
topic: "var(--color-node-topic)",
claim: "var(--color-node-claim)",
source: "var(--color-node-source)",
};
return colorMap[type] ?? "var(--color-accent)";
}}
maskColor="var(--glass-bg)"
className="!bg-surface !border !border-border-subtle"
/>
</ReactFlow>
</div>
);
}
export default function KnowledgeGraphView() {
return (
<ReactFlowProvider>
<KnowledgeGraphViewInner />
</ReactFlowProvider>
);
}
@@ -20,6 +20,11 @@ const typeBadgeColors: Record<NodeType, string> = {
domain: "text-node-concept border border-node-concept/30 bg-node-concept/10",
flow: "text-node-pipeline border border-node-pipeline/30 bg-node-pipeline/10",
step: "text-node-function border border-node-function/30 bg-node-function/10",
article: "text-node-article border border-node-article/30 bg-node-article/10",
entity: "text-node-entity border border-node-entity/30 bg-node-entity/10",
topic: "text-node-topic border border-node-topic/30 bg-node-topic/10",
claim: "text-node-claim border border-node-claim/30 bg-node-claim/10",
source: "text-node-source border border-node-source/30 bg-node-source/10",
};
const complexityBadgeColors: Record<string, string> = {
@@ -62,6 +67,12 @@ const EDGE_LABELS: Record<EdgeType, { forward: string; backward: string }> = {
contains_flow: { forward: "contains flow", backward: "flow in" },
flow_step: { forward: "flow step", backward: "step of" },
cross_domain: { forward: "cross-domain to", backward: "cross-domain from" },
cites: { forward: "cites", backward: "cited by" },
contradicts: { forward: "contradicts", backward: "contradicted by" },
builds_on: { forward: "builds on", backward: "built upon by" },
exemplifies: { forward: "exemplifies", backward: "exemplified by" },
categorized_under: { forward: "categorized under", backward: "categorizes" },
authored_by: { forward: "authored by", backward: "authored" },
};
/**
@@ -78,6 +89,97 @@ function getDirectionalLabel(edgeType: string, isSource: boolean): string {
return isSource ? labels.forward : labels.backward;
}
function KnowledgeNodeDetails({ node, graph }: { node: GraphNode; graph: KnowledgeGraph }) {
const navigateToNode = useDashboardStore((s) => s.navigateToNode);
const meta = node.knowledgeMeta;
// Wikilinks (outgoing related edges)
const wikilinks = graph.edges
.filter((e) => e.type === "related" && e.source === node.id)
.map((e) => graph.nodes.find((n) => n.id === e.target))
.filter((n): n is GraphNode => n !== undefined);
// Backlinks (incoming related edges)
const backlinks = graph.edges
.filter((e) => e.type === "related" && e.target === node.id)
.map((e) => graph.nodes.find((n) => n.id === e.source))
.filter((n): n is GraphNode => n !== undefined);
// Category
const categoryEdge = graph.edges.find(
(e) => e.type === "categorized_under" && e.source === node.id
);
const categoryNode = categoryEdge
? graph.nodes.find((n) => n.id === categoryEdge.target)
: null;
return (
<div className="space-y-3">
{categoryNode && (
<div>
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">Category</h4>
<button
type="button"
onClick={() => navigateToNode(categoryNode.id)}
className="text-[11px] px-2 py-0.5 rounded bg-elevated text-accent hover:text-accent-bright transition-colors"
>
{categoryNode.name}
</button>
</div>
)}
{meta?.wikilinks && meta.wikilinks.length > 0 && (
<div>
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">
Wikilinks ({wikilinks.length})
</h4>
<div className="space-y-1 max-h-[200px] overflow-auto">
{wikilinks.map((n) => (
<button
key={n.id}
type="button"
onClick={() => navigateToNode(n.id)}
className="block w-full text-left px-2 py-1.5 rounded bg-elevated hover:bg-accent/10 text-[11px] text-text-secondary hover:text-accent transition-colors truncate"
>
{n.name}
</button>
))}
</div>
</div>
)}
{backlinks.length > 0 && (
<div>
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">
Backlinks ({backlinks.length})
</h4>
<div className="space-y-1 max-h-[200px] overflow-auto">
{backlinks.map((n) => (
<button
key={n.id}
type="button"
onClick={() => navigateToNode(n.id)}
className="block w-full text-left px-2 py-1.5 rounded bg-elevated hover:bg-accent/10 text-[11px] text-text-secondary hover:text-accent transition-colors truncate"
>
{n.name}
</button>
))}
</div>
</div>
)}
{meta?.content && (
<div>
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">Preview</h4>
<div className="text-[11px] text-text-secondary leading-relaxed bg-elevated rounded-lg p-3 max-h-[300px] overflow-auto whitespace-pre-wrap font-mono">
{meta.content.slice(0, 1500)}
{meta.content.length > 1500 && (
<span className="text-text-muted">... (truncated)</span>
)}
</div>
</div>
)}
</div>
);
}
function DomainNodeDetails({ node, graph }: { node: GraphNode; graph: KnowledgeGraph }) {
const navigateToDomain = useDashboardStore((s) => s.navigateToDomain);
const selectNode = useDashboardStore((s) => s.selectNode);
@@ -380,6 +482,11 @@ export default function NodeInfo() {
</div>
)}
{/* Knowledge-specific details */}
{activeGraph && node && (node.type === "article" || node.type === "entity" || node.type === "topic" || node.type === "claim" || node.type === "source") && (
<KnowledgeNodeDetails node={node} graph={activeGraph} />
)}
{/* Domain-specific details */}
{activeGraph && node && (node.type === "domain" || node.type === "flow" || node.type === "step") && (
<DomainNodeDetails node={node} graph={activeGraph} />
@@ -36,6 +36,13 @@
--color-node-schema: #fcd34d;
--color-node-resource: #a5b4fc;
/* Knowledge node types */
--color-node-article: #d4a574;
--color-node-entity: #7ba4c9;
--color-node-topic: #c9b06c;
--color-node-claim: #6fb07a;
--color-node-source: #8a8a8a;
/* Diff */
--color-diff-changed: #e05252;
--color-diff-affected: #d4a030;
@@ -9,10 +9,10 @@ import type { ReactFlowInstance } from "@xyflow/react";
export type Persona = "non-technical" | "junior" | "experienced";
export type NavigationLevel = "overview" | "layer-detail";
export type NodeType = "file" | "function" | "class" | "module" | "concept" | "config" | "document" | "service" | "table" | "endpoint" | "pipeline" | "schema" | "resource" | "domain" | "flow" | "step";
export type NodeType = "file" | "function" | "class" | "module" | "concept" | "config" | "document" | "service" | "table" | "endpoint" | "pipeline" | "schema" | "resource" | "domain" | "flow" | "step" | "article" | "entity" | "topic" | "claim" | "source";
export type Complexity = "simple" | "moderate" | "complex";
export type EdgeCategory = "structural" | "behavioral" | "data-flow" | "dependencies" | "semantic" | "infrastructure" | "domain";
export type ViewMode = "structural" | "domain";
export type EdgeCategory = "structural" | "behavioral" | "data-flow" | "dependencies" | "semantic" | "infrastructure" | "domain" | "knowledge";
export type ViewMode = "structural" | "domain" | "knowledge";
export interface FilterState {
nodeTypes: Set<NodeType>;
@@ -21,9 +21,9 @@ export interface FilterState {
edgeCategories: Set<EdgeCategory>;
}
export const ALL_NODE_TYPES: NodeType[] = ["file", "function", "class", "module", "concept", "config", "document", "service", "table", "endpoint", "pipeline", "schema", "resource", "domain", "flow", "step"];
export const ALL_NODE_TYPES: NodeType[] = ["file", "function", "class", "module", "concept", "config", "document", "service", "table", "endpoint", "pipeline", "schema", "resource", "domain", "flow", "step", "article", "entity", "topic", "claim", "source"];
export const ALL_COMPLEXITIES: Complexity[] = ["simple", "moderate", "complex"];
export const ALL_EDGE_CATEGORIES: EdgeCategory[] = ["structural", "behavioral", "data-flow", "dependencies", "semantic", "infrastructure", "domain"];
export const ALL_EDGE_CATEGORIES: EdgeCategory[] = ["structural", "behavioral", "data-flow", "dependencies", "semantic", "infrastructure", "domain", "knowledge"];
export const EDGE_CATEGORY_MAP: Record<EdgeCategory, string[]> = {
structural: ["imports", "exports", "contains", "inherits", "implements"],
@@ -33,6 +33,7 @@ export const EDGE_CATEGORY_MAP: Record<EdgeCategory, string[]> = {
semantic: ["related", "similar_to"],
infrastructure: ["deploys", "serves", "provisions", "triggers", "migrates", "documents", "routes", "defines_schema"],
domain: ["contains_flow", "flow_step", "cross_domain"],
knowledge: ["cites", "contradicts", "builds_on", "exemplifies", "categorized_under", "authored_by"],
};
export const DOMAIN_EDGE_TYPES = EDGE_CATEGORY_MAP.domain;
@@ -45,7 +46,7 @@ const DEFAULT_FILTERS: FilterState = {
};
/** Categories used for node type filter toggles. Single source of truth for NodeCategory. */
export type NodeCategory = "code" | "config" | "docs" | "infra" | "data" | "domain";
export type NodeCategory = "code" | "config" | "docs" | "infra" | "data" | "domain" | "knowledge";
/** Find which layer a node belongs to. Returns layerId or null. */
function findNodeLayer(graph: KnowledgeGraph, nodeId: string): string | null {
@@ -133,13 +134,15 @@ interface DashboardStore {
nextTourStep: () => void;
prevTourStep: () => void;
// Domain view
// View mode
viewMode: ViewMode;
isKnowledgeGraph: boolean;
domainGraph: KnowledgeGraph | null;
activeDomainId: string | null;
setDomainGraph: (graph: KnowledgeGraph) => void;
setViewMode: (mode: ViewMode) => void;
setIsKnowledgeGraph: (value: boolean) => void;
navigateToDomain: (domainId: string) => void;
clearActiveDomain: () => void;
}
@@ -197,7 +200,7 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
pathFinderOpen: false,
reactFlowInstance: null,
nodeTypeFilters: { code: true, config: true, docs: true, infra: true, data: true, domain: true },
nodeTypeFilters: { code: true, config: true, docs: true, infra: true, data: true, domain: true, knowledge: true },
toggleNodeTypeFilter: (category) =>
set((state) => ({
@@ -464,6 +467,7 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
},
viewMode: "structural",
isKnowledgeGraph: false,
domainGraph: null,
activeDomainId: null,
@@ -471,6 +475,10 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
set({ domainGraph: graph });
},
setIsKnowledgeGraph: (value) => {
set({ isKnowledgeGraph: value });
},
setViewMode: (mode) => {
set({
viewMode: mode,
@@ -1,4 +1,14 @@
import dagre from "@dagrejs/dagre";
import {
forceSimulation,
forceLink,
forceManyBody,
forceCenter,
forceCollide,
forceX,
forceY,
} from "d3-force";
import type { SimulationNodeDatum, SimulationLinkDatum } from "d3-force";
import type { Node, Edge } from "@xyflow/react";
export const NODE_WIDTH = 280;
@@ -62,4 +72,114 @@ export function applyDagreLayout(
return { nodes: layoutedNodes, edges };
}
// ---------------------------------------------------------------------------
// Force-directed layout (for knowledge graphs)
// ---------------------------------------------------------------------------
interface ForceNode extends SimulationNodeDatum {
id: string;
community?: number;
}
/**
* Force-directed layout using d3-force used for knowledge graphs.
* Optionally groups nodes by community (layer/category).
*/
export function applyForceLayout(
nodes: Node[],
edges: Edge[],
nodeDimensions?: Map<string, { width: number; height: number }>,
communityMap?: Map<string, number>,
): { nodes: Node[]; edges: Edge[] } {
if (nodes.length === 0) return { nodes, edges };
// Build simulation nodes with optional community assignment
const simNodes: ForceNode[] = nodes.map((n) => ({
id: n.id,
x: Math.random() * 800 - 400,
y: Math.random() * 800 - 400,
community: communityMap?.get(n.id),
}));
const nodeIdSet = new Set(simNodes.map((n) => n.id));
const simLinks: SimulationLinkDatum<ForceNode>[] = edges
.filter((e) => nodeIdSet.has(e.source as string) && nodeIdSet.has(e.target as string))
.map((e) => ({
source: e.source as string,
target: e.target as string,
}));
// Compute community centers for cluster attraction
const communityCount = communityMap
? Math.max(1, new Set(communityMap.values()).size)
: 1;
const communityAngle = (i: number) => (2 * Math.PI * i) / communityCount;
// Scale cluster radius with node count for better spread
const clusterRadius = Math.max(600, nodes.length * 5);
// Scale forces based on graph size
const isLarge = nodes.length > 100;
const chargeStrength = isLarge ? -600 : -350;
const linkDistance = isLarge ? 250 : 150;
const sim = forceSimulation<ForceNode>(simNodes)
.force(
"link",
forceLink<ForceNode, SimulationLinkDatum<ForceNode>>(simLinks)
.id((d) => d.id)
.distance(linkDistance)
.strength(0.2),
)
.force("charge", forceManyBody().strength(chargeStrength).distanceMax(1500))
.force("center", forceCenter(0, 0).strength(0.03))
.force(
"collide",
forceCollide<ForceNode>().radius((d) => {
const dims = nodeDimensions?.get(d.id);
return Math.max(20, ((dims?.width ?? NODE_WIDTH) + 40) / 2);
}).strength(0.8),
);
// Add community clustering force if communities are provided
if (communityMap && communityCount > 1) {
sim.force(
"clusterX",
forceX<ForceNode>((d) => {
const c = d.community ?? 0;
return Math.cos(communityAngle(c)) * clusterRadius;
}).strength(0.3),
);
sim.force(
"clusterY",
forceY<ForceNode>((d) => {
const c = d.community ?? 0;
return Math.sin(communityAngle(c)) * clusterRadius;
}).strength(0.3),
);
}
// Run to convergence synchronously
const ticks = Math.min(300, Math.max(100, nodes.length));
sim.tick(ticks);
sim.stop();
// Map positions back to xyflow nodes
const posMap = new Map(simNodes.map((n) => [n.id, { x: n.x ?? 0, y: n.y ?? 0 }]));
const layoutedNodes = nodes.map((node) => {
const pos = posMap.get(node.id) ?? { x: 0, y: 0 };
const dims = nodeDimensions?.get(node.id);
const w = dims?.width ?? NODE_WIDTH;
const h = dims?.height ?? NODE_HEIGHT;
return {
...node,
position: {
x: pos.x - w / 2,
y: pos.y - h / 2,
},
};
});
return { nodes: layoutedNodes, edges };
}
+27
View File
@@ -70,6 +70,9 @@ importers:
'@xyflow/react':
specifier: ^12.0.0
version: 12.10.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
d3-force:
specifier: ^3.0.0
version: 3.0.0
devlop:
specifier: ^1.1.0
version: 1.1.0
@@ -92,6 +95,9 @@ importers:
'@tailwindcss/vite':
specifier: ^4.0.0
version: 4.2.2(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))
'@types/d3-force':
specifier: ^3.0.10
version: 3.0.10
'@types/react':
specifier: ^19.0.0
version: 19.2.14
@@ -633,6 +639,9 @@ packages:
'@types/d3-drag@3.0.7':
resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==}
'@types/d3-force@3.0.10':
resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==}
'@types/d3-interpolate@3.0.4':
resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
@@ -861,10 +870,18 @@ packages:
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
engines: {node: '>=12'}
d3-force@3.0.0:
resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==}
engines: {node: '>=12'}
d3-interpolate@3.0.1:
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
engines: {node: '>=12'}
d3-quadtree@3.0.1:
resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==}
engines: {node: '>=12'}
d3-selection@3.0.0:
resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
engines: {node: '>=12'}
@@ -2085,6 +2102,8 @@ snapshots:
dependencies:
'@types/d3-selection': 3.0.11
'@types/d3-force@3.0.10': {}
'@types/d3-interpolate@3.0.4':
dependencies:
'@types/d3-color': 3.1.3
@@ -2345,10 +2364,18 @@ snapshots:
d3-ease@3.0.1: {}
d3-force@3.0.0:
dependencies:
d3-dispatch: 3.0.1
d3-quadtree: 3.0.1
d3-timer: 3.0.1
d3-interpolate@3.0.1:
dependencies:
d3-color: 3.1.0
d3-quadtree@3.0.1: {}
d3-selection@3.0.0: {}
d3-timer@3.0.1: {}
@@ -0,0 +1,132 @@
---
name: understand-knowledge
description: Analyze a Karpathy-pattern LLM wiki knowledge base and generate an interactive knowledge graph with entity extraction, implicit relationships, and topic clustering.
argument-hint: [wiki-directory]
---
# /understand-knowledge
Analyzes a Karpathy-pattern LLM wiki — a three-layer knowledge base with raw sources, wiki markdown, and a schema file — and produces an interactive knowledge graph dashboard.
## What It Detects
The **Karpathy LLM wiki pattern** (see https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f):
- **Raw sources** — immutable source documents (articles, papers, data files)
- **Wiki** — LLM-generated markdown files with wikilinks (`[[target]]` syntax)
- **Schema** — CLAUDE.md, AGENTS.md, or similar configuration file
- **index.md** — content catalog organized by categories
- **log.md** — chronological operation log
Detection signals: has `index.md` + multiple `.md` files with wikilinks. May have `raw/` directory and schema file.
## Instructions
### Phase 1: DETECT
1. Determine the target directory:
- If the user provided a path argument, use that
- Otherwise, use the current working directory
2. Run the format detection script bundled with this skill:
```
python3 <SKILL_DIR>/parse-knowledge-base.py <TARGET_DIR>
```
- If the script exits with an error, tell the user this doesn't appear to be a Karpathy-pattern wiki and explain what was expected
- If successful, proceed. The script writes `scan-manifest.json` to `<TARGET_DIR>/.understand-anything/intermediate/`
3. Read the scan-manifest.json and announce the results:
- "Detected Karpathy wiki: N articles, N sources, N topics, N wikilinks (N unresolved)"
- List the categories found from index.md
### Phase 2: SCAN (already done)
The parse script in Phase 1 already performed the deterministic scan. The scan-manifest.json contains:
- Article nodes (one per wiki .md file) with extracted wikilinks, headings, frontmatter
- Source nodes (one per raw/ file)
- Topic nodes (from index.md section headings)
- `related` edges (from wikilinks)
- `categorized_under` edges (from index.md sections)
No additional scanning is needed. Proceed to Phase 3.
### Phase 3: ANALYZE
Dispatch `article-analyzer` subagents to extract implicit knowledge:
1. Read the scan-manifest.json to get the article list
2. Prepare batches of 10-15 articles each, grouped by category when possible (articles in the same category are more likely to have implicit cross-references)
3. For each batch, dispatch an `article-analyzer` subagent with:
- The batch of articles (id, name, summary, wikilinks, category, content from knowledgeMeta)
- The full list of existing node IDs (so the agent can reference them)
- The batch number for output file naming
- The intermediate directory path: `$INTERMEDIATE_DIR = <TARGET_DIR>/.understand-anything/intermediate`
The agent will write `analysis-batch-{N}.json` to the intermediate directory.
4. Run up to 3 batches concurrently. Wait for all batches to complete.
5. If any batch fails, log a warning but continue — the scan-manifest provides a solid base graph even without LLM analysis.
### Phase 4: MERGE
1. Run the merge script bundled with this skill:
```
python3 <SKILL_DIR>/merge-knowledge-graph.py <TARGET_DIR>
```
2. The script:
- Combines scan-manifest.json + all analysis-batch-*.json files
- Deduplicates entities (case-insensitive name matching)
- Normalizes node/edge types via alias maps
- Builds layers from index.md categories
- Builds a tour from index.md section ordering
- Writes `assembled-graph.json` to the intermediate directory
3. Read the merge report from stderr and announce:
- Total nodes, edges, layers, tour steps
- How many entities/claims the LLM analysis added
### Phase 5: SAVE
1. Read the assembled-graph.json
2. Run basic validation:
- Every edge source/target must reference an existing node
- Every node must have: id, type, name, summary, tags, complexity
- Remove any edges with dangling references
3. Copy the validated graph to `<TARGET_DIR>/.understand-anything/knowledge-graph.json`
4. Write metadata to `<TARGET_DIR>/.understand-anything/meta.json`:
```json
{
"lastAnalyzedAt": "<ISO timestamp>",
"gitCommitHash": "<from git rev-parse HEAD or empty>",
"version": "1.0.0",
"analyzedFiles": <number of wiki articles>
}
```
5. Clean up intermediate files:
```
rm -rf <TARGET_DIR>/.understand-anything/intermediate
```
6. Report summary to the user:
- "Knowledge graph saved: N articles, N entities, N topics, N claims, N sources"
- "N edges (N wikilink, N categorized, N implicit)"
- "N layers, N tour steps"
7. Auto-trigger the dashboard:
```
/understand-dashboard <TARGET_DIR>
```
## Notes
- The parse script handles ALL deterministic extraction (wikilinks, headings, frontmatter, categories from index.md). The LLM agents only add implicit knowledge that requires inference.
- Categories and taxonomy come from index.md section headings, NOT from filename prefixes. The Karpathy spec is intentionally abstract about naming conventions.
- The graph uses `kind: "knowledge"` to signal the dashboard to use force-directed layout instead of hierarchical dagre.
- Source nodes from raw/ are lightweight (filename + size only) — we don't parse PDFs or binary files.
@@ -0,0 +1,397 @@
#!/usr/bin/env python3
"""
Merge script for Karpathy-pattern knowledge graphs.
Combines the deterministic scan-manifest.json with LLM analysis batches
(analysis-batch-*.json) into a final assembled knowledge graph.
Handles: entity deduplication, edge normalization, layer building from
index.md categories, tour generation from index.md section ordering.
Usage:
python merge-knowledge-graph.py <wiki-directory>
Output:
Writes assembled-graph.json to <wiki-directory>/.understand-anything/intermediate/
"""
import json
import os
import re
import sys
from datetime import datetime, timezone
from pathlib import Path
# ---------------------------------------------------------------------------
# Canonical type sets (must match core/src/types.ts)
# ---------------------------------------------------------------------------
VALID_NODE_TYPES = {
"article", "entity", "topic", "claim", "source",
# Codebase types (for cross-compatibility)
"file", "function", "class", "module", "concept",
"config", "document", "service", "table", "endpoint",
"pipeline", "schema", "resource", "domain", "flow", "step",
}
VALID_EDGE_TYPES = {
"cites", "contradicts", "builds_on", "exemplifies",
"categorized_under", "authored_by", "related", "similar_to",
# Codebase types
"imports", "exports", "contains", "inherits", "implements",
"calls", "subscribes", "publishes", "middleware",
"reads_from", "writes_to", "transforms", "validates",
"depends_on", "tested_by", "configures",
"deploys", "serves", "provisions", "triggers",
"migrates", "documents", "routes", "defines_schema",
"contains_flow", "flow_step", "cross_domain",
}
NODE_TYPE_ALIASES = {
"note": "article", "page": "article", "wiki_page": "article",
"person": "entity", "actor": "entity", "organization": "entity",
"tag": "topic", "category": "topic", "theme": "topic",
"assertion": "claim", "decision": "claim", "thesis": "claim",
"reference": "source", "raw": "source", "paper": "source",
}
EDGE_TYPE_ALIASES = {
"references": "cites", "cites_source": "cites",
"conflicts_with": "contradicts", "disagrees_with": "contradicts",
"refines": "builds_on", "elaborates": "builds_on",
"illustrates": "exemplifies", "instance_of": "exemplifies", "example_of": "exemplifies",
"belongs_to": "categorized_under", "tagged_with": "categorized_under",
"written_by": "authored_by", "created_by": "authored_by",
"relates_to": "related", "related_to": "related",
}
# ---------------------------------------------------------------------------
# Normalization
# ---------------------------------------------------------------------------
def normalize_node_type(t: str) -> str:
t = t.lower().strip()
return NODE_TYPE_ALIASES.get(t, t)
def normalize_edge_type(t: str) -> str:
t = t.lower().strip()
return EDGE_TYPE_ALIASES.get(t, t)
def normalize_entity_name(name: str) -> str:
"""Normalize entity names for deduplication."""
return re.sub(r'\s+', ' ', name.strip().lower())
# ---------------------------------------------------------------------------
# Merge pipeline
# ---------------------------------------------------------------------------
def merge(root: Path) -> dict:
intermediate = root / ".understand-anything" / "intermediate"
manifest_path = intermediate / "scan-manifest.json"
if not manifest_path.is_file():
print(f"Error: {manifest_path} not found. Run parse-knowledge-base.py first.",
file=sys.stderr)
sys.exit(1)
# Load scan manifest (deterministic base)
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
nodes = {n["id"]: n for n in manifest["nodes"]}
edges = list(manifest["edges"])
report = {"base_nodes": len(nodes), "base_edges": len(edges),
"batches": 0, "new_entities": 0, "new_claims": 0,
"new_edges": 0, "deduped_entities": 0, "dropped_edges": 0}
# Load analysis batches
batch_files = sorted(intermediate.glob("analysis-batch-*.json"))
entity_name_map: dict[str, str] = {} # normalized_name → entity_id
dedup_remap: dict[str, str] = {} # duplicate_id → canonical_id
for bf in batch_files:
report["batches"] += 1
try:
batch = json.loads(bf.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError) as e:
print(f"[merge] Warning: Failed to load {bf.name}: {e}", file=sys.stderr)
continue
# Process new nodes from LLM analysis
for node in batch.get("nodes", []):
node_type = normalize_node_type(node.get("type", ""))
if node_type not in VALID_NODE_TYPES:
print(f"[merge] Warning: Unknown node type '{node.get('type')}' — skipping",
file=sys.stderr)
continue
node["type"] = node_type
node_id = node.get("id", "")
# Entity deduplication — track remapping for edge fixup
if node_type == "entity":
norm_name = normalize_entity_name(node.get("name", ""))
if norm_name in entity_name_map:
# Map duplicate ID → canonical ID for edge remapping
dedup_remap[node_id] = entity_name_map[norm_name]
report["deduped_entities"] += 1
continue
entity_name_map[norm_name] = node_id
report["new_entities"] += 1
elif node_type == "claim":
report["new_claims"] += 1
# Ensure required fields
node.setdefault("summary", node.get("name", ""))
node.setdefault("tags", [])
node.setdefault("complexity", "simple")
nodes[node_id] = node
# Process new edges from LLM analysis
for edge in batch.get("edges", []):
edge_type = normalize_edge_type(edge.get("type", ""))
if edge_type not in VALID_EDGE_TYPES:
print(f"[merge] Warning: Unknown edge type '{edge.get('type')}'"
f"mapped to 'related'", file=sys.stderr)
edge_type = "related"
edge["type"] = edge_type
edge.setdefault("direction", "forward")
edge.setdefault("weight", 0.5)
# Remap deduped entity IDs, then validate source/target exist
src = dedup_remap.get(edge.get("source", ""), edge.get("source", ""))
tgt = dedup_remap.get(edge.get("target", ""), edge.get("target", ""))
edge["source"] = src
edge["target"] = tgt
if src in nodes and tgt in nodes:
edges.append(edge)
report["new_edges"] += 1
else:
report["dropped_edges"] += 1
# --- Deduplicate edges ---
seen: set[tuple[str, str, str]] = set()
final_edges = []
for edge in edges:
key = (edge["source"], edge["target"], edge["type"])
if key not in seen:
seen.add(key)
final_edges.append(edge)
# --- Build article→layer map from categories ---
categories = manifest.get("categories", [])
article_layer_map: dict[str, str] = {} # article_id → layer_id
layer_members: dict[str, list[str]] = {} # layer_id → [node_ids]
for cat in categories:
cat_name = cat["name"]
cat_slug = cat_name.lower().replace(" ", "-")
layer_id = f"layer:{cat_slug}"
topic_id = f"topic:{cat_slug}"
members = [e["source"] for e in final_edges
if e["type"] == "categorized_under" and e["target"] == topic_id]
if topic_id in nodes:
members.append(topic_id)
layer_members[layer_id] = members
for mid in members:
article_layer_map[mid] = layer_id
# --- Assign entity/claim nodes to their parent article's layer ---
# Step 1: Build entity/claim → article mapping from edges
child_to_article: dict[str, str] = {}
for edge in final_edges:
src_type = nodes.get(edge["source"], {}).get("type", "")
tgt_type = nodes.get(edge["target"], {}).get("type", "")
# If an article connects to an entity/claim, map the child to the article
if src_type == "article" and tgt_type in ("entity", "claim"):
child_to_article.setdefault(edge["target"], edge["source"])
elif tgt_type == "article" and src_type in ("entity", "claim"):
child_to_article.setdefault(edge["source"], edge["target"])
# Step 2: For orphan entities/claims, try to match by ID prefix
# Build a reverse lookup: bare article name → full article ID
# e.g., "concept-aaak-compression" → "article:concepts/concept-aaak-compression"
bare_to_article: dict[str, str] = {}
for nid in nodes:
if nid.startswith("article:"):
# Extract the bare filename from paths like "article:concepts/concept-foo"
bare = nid.split("/")[-1] if "/" in nid else nid.replace("article:", "")
bare_to_article[bare] = nid
for nid, node in nodes.items():
if node["type"] in ("entity", "claim") and nid not in child_to_article:
# e.g., "claim:concept-aaak-compression:not-zero-loss" → stem "concept-aaak-compression"
# e.g., "entity:brain" → stem "brain"
raw = nid.split(":", 1)[1] if ":" in nid else nid # "concept-aaak-compression:not-zero-loss"
stem = raw.split(":")[0] # "concept-aaak-compression"
# Try exact bare name match first
if stem in bare_to_article:
child_to_article[nid] = bare_to_article[stem]
else:
# Try suffix/substring match against bare names
# e.g., entity:brain → segment-brain, entity:mempalace → tool-mempalace
matched = False
for bare, aid in bare_to_article.items():
if stem in bare or bare in stem:
child_to_article[nid] = aid
matched = True
break
# Also try: bare ends with -stem (e.g., "segment-brain" ends with "-brain")
if bare.endswith(f"-{stem}") or bare.endswith(f"/{stem}"):
child_to_article[nid] = aid
matched = True
break
# Last resort: check if the node's name appears in any article's
# name OR content (knowledgeMeta.content)
if not matched and node.get("name"):
node_name_lower = node["name"].lower()
for aid, anode in nodes.items():
if not aid.startswith("article:"):
continue
# Match against article name
if node_name_lower in anode.get("name", "").lower():
child_to_article[nid] = aid
matched = True
break
# Match against article content (wikilinks or text)
meta = anode.get("knowledgeMeta", {})
content = (meta.get("content") or "").lower()
if len(node_name_lower) >= 3 and node_name_lower in content:
child_to_article[nid] = aid
matched = True
break
# Step 3: Place children into their parent article's layer
for child_id, article_id in child_to_article.items():
layer_id = article_layer_map.get(article_id)
if layer_id and layer_id in layer_members:
layer_members[layer_id].append(child_id)
article_layer_map[child_id] = layer_id
# --- Build layers ---
layers = []
for cat in categories:
cat_name = cat["name"]
cat_slug = cat_name.lower().replace(" ", "-")
layer_id = f"layer:{cat_slug}"
members = list(dict.fromkeys(layer_members.get(layer_id, []))) # Deduplicate preserving order
layers.append({
"id": layer_id,
"name": cat_name,
"description": f"{cat_name} ({len(members)} nodes)",
"nodeIds": members,
})
# Assign uncategorized nodes to an "Other" layer
categorized_ids = set()
for layer in layers:
categorized_ids.update(layer["nodeIds"])
uncategorized = [nid for nid in nodes if nid not in categorized_ids]
if uncategorized:
layers.append({
"id": "layer:other",
"name": "Other",
"description": f"Uncategorized nodes ({len(uncategorized)})",
"nodeIds": uncategorized,
})
# --- Build tour from index.md category ordering ---
tour = []
for i, cat in enumerate(categories):
cat_slug = cat["name"].lower().replace(" ", "-")
topic_id = f"topic:{cat_slug}"
# Pick representative articles (up to 3 per category)
members = [e["source"] for e in final_edges
if e["type"] == "categorized_under" and e["target"] == topic_id][:3]
if not members and topic_id in nodes:
members = [topic_id]
if members:
tour.append({
"order": i + 1,
"title": cat["name"],
"description": f"Explore the {cat['name']} section ({cat['count']} articles)",
"nodeIds": members,
})
# --- Detect project name ---
project_name = root.name
# Try to find a better name from index.md H1
index_path = root / "wiki" / "index.md"
if not index_path.is_file():
index_path = root / "index.md"
if index_path.is_file():
text = index_path.read_text(encoding="utf-8", errors="replace")
h1_match = re.search(r"^#\s+(.+)$", text, re.MULTILINE)
if h1_match:
project_name = h1_match.group(1).strip()
# --- Assemble final graph ---
graph = {
"version": "1.0.0",
"kind": "knowledge",
"project": {
"name": project_name,
"languages": ["markdown"],
"frameworks": ["karpathy-wiki"],
"description": f"Knowledge graph for {project_name}",
"analyzedAt": datetime.now(timezone.utc).isoformat(),
"gitCommitHash": "",
},
"nodes": list(nodes.values()),
"edges": final_edges,
"layers": layers,
"tour": tour,
}
# Try to get git commit hash
try:
import subprocess
result = subprocess.run(
["git", "rev-parse", "HEAD"],
capture_output=True, text=True, cwd=str(root), timeout=5
)
if result.returncode == 0:
graph["project"]["gitCommitHash"] = result.stdout.strip()
except (OSError, subprocess.TimeoutExpired):
pass
# Write output
out_path = intermediate / "assembled-graph.json"
out_path.write_text(json.dumps(graph, indent=2), encoding="utf-8")
# Report
print(f"[merge] Input: {report['base_nodes']} scan nodes, "
f"{report['base_edges']} scan edges, {report['batches']} analysis batches",
file=sys.stderr)
print(f"[merge] Added: {report['new_entities']} entities, "
f"{report['new_claims']} claims, {report['new_edges']} edges "
f"({report['deduped_entities']} deduped entities, "
f"{report['dropped_edges']} dropped dangling edges)", file=sys.stderr)
print(f"[merge] Output: {len(graph['nodes'])} nodes, {len(final_edges)} edges, "
f"{len(layers)} layers, {len(tour)} tour steps", file=sys.stderr)
print(f"[merge] Written: {out_path}", file=sys.stderr)
return graph
def main():
if len(sys.argv) < 2:
print("Usage: merge-knowledge-graph.py <wiki-directory>", file=sys.stderr)
sys.exit(1)
root = Path(sys.argv[1]).resolve()
if not root.is_dir():
print(f"Error: {root} is not a directory", file=sys.stderr)
sys.exit(1)
merge(root)
if __name__ == "__main__":
main()
@@ -0,0 +1,509 @@
#!/usr/bin/env python3
"""
Deterministic parser for Karpathy-pattern LLM wikis.
Detects the three-layer pattern (raw sources + wiki markdown + schema),
extracts structure from markdown files, resolves wikilinks, and derives
categories from index.md section headings.
Usage:
python parse-knowledge-base.py <wiki-directory>
Output:
Writes scan-manifest.json to <wiki-directory>/.understand-anything/intermediate/
"""
import json
import os
import re
import sys
from pathlib import Path
# ---------------------------------------------------------------------------
# Regex patterns
# ---------------------------------------------------------------------------
WIKILINK_RE = re.compile(r"\[\[([^\]|]+)(?:\|([^\]]+))?\]\]")
FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL)
CODE_BLOCK_RE = re.compile(r"```(\w*)")
HEADING_RE = re.compile(r"^(#{1,6})\s+(.+)$", re.MULTILINE)
INDEX_SECTION_RE = re.compile(r"^##\s+(.+)$", re.MULTILINE)
# Files that are part of wiki infrastructure, not content articles
INFRA_FILES = {"index.md", "log.md", "claude.md", "agents.md", "soul.md"}
# ---------------------------------------------------------------------------
# Detection: is this a Karpathy-pattern wiki?
# ---------------------------------------------------------------------------
def detect_format(root: Path) -> dict:
"""Detect if directory follows the Karpathy LLM wiki three-layer pattern."""
signals = {
"has_index": (root / "index.md").is_file() or (root / "wiki" / "index.md").is_file(),
"has_log": (root / "log.md").is_file() or (root / "wiki" / "log.md").is_file(),
"has_raw": (root / "raw").is_dir(),
"has_schema": any(
(root / f).is_file() or (root / "wiki" / f).is_file()
for f in ["CLAUDE.md", "AGENTS.md"]
),
}
# Find the wiki root — could be the directory itself or a wiki/ subdirectory
if (root / "wiki").is_dir():
wiki_root = root / "wiki"
else:
wiki_root = root
# Count markdown files in the wiki root
md_files = list(wiki_root.rglob("*.md"))
signals["md_count"] = len(md_files)
signals["wiki_root"] = str(wiki_root)
# Primary signal: has index.md + meaningful number of markdown files
if signals["has_index"] and signals["md_count"] >= 3:
signals["detected"] = True
signals["format"] = "karpathy"
else:
signals["detected"] = False
signals["format"] = "unknown"
return signals
# ---------------------------------------------------------------------------
# Markdown extraction helpers
# ---------------------------------------------------------------------------
def extract_frontmatter(text: str) -> dict:
"""Extract YAML frontmatter as a simple key-value dict."""
m = FRONTMATTER_RE.match(text)
if not m:
return {}
fm = {}
for line in m.group(1).split("\n"):
if ":" in line:
key, _, val = line.partition(":")
fm[key.strip()] = val.strip().strip('"').strip("'")
return fm
def extract_wikilinks(text: str) -> list[dict]:
"""Extract all [[target]] and [[target|display]] wikilinks."""
links = []
for m in WIKILINK_RE.finditer(text):
links.append({
"target": m.group(1).strip(),
"display": m.group(2).strip() if m.group(2) else None,
})
return links
def extract_headings(text: str) -> list[dict]:
"""Extract all markdown headings with level and text."""
return [
{"level": len(m.group(1)), "text": m.group(2).strip()}
for m in HEADING_RE.finditer(text)
]
def extract_code_blocks(text: str) -> list[str]:
"""Extract languages from fenced code blocks."""
return [m.group(1) for m in CODE_BLOCK_RE.finditer(text) if m.group(1)]
def extract_first_paragraph(text: str) -> str:
"""Extract the first non-empty paragraph after frontmatter and H1."""
# Strip frontmatter
stripped = FRONTMATTER_RE.sub("", text).strip()
if not stripped:
return ""
lines = stripped.split("\n")
def _collect_paragraph(start_lines: list[str]) -> str:
"""Collect the first paragraph from the given lines."""
para: list[str] = []
for s_raw in start_lines:
s = s_raw.strip()
if not s and not para:
continue # Skip leading blank lines
if not s and para:
break # End of paragraph
if s.startswith(">"):
continue # Skip blockquotes
if re.match(r"^[-*_]{3,}\s*$", s):
continue # Skip horizontal rules
if s.startswith("#"):
if para:
break # End paragraph at next heading
continue # Skip headings before paragraph
para.append(s)
return " ".join(para)
# Try: find first paragraph after H1
for i, line in enumerate(lines):
if line.strip().startswith("# "):
result = _collect_paragraph(lines[i + 1:])
if result:
if len(result) > 200:
return result[:197] + "..."
return result
# Fallback: no H1 found, take first paragraph from start
result = _collect_paragraph(lines)
if len(result) > 200:
result = result[:197] + "..."
return result or ""
def extract_h1(text: str) -> str:
"""Extract the first H1 heading."""
for m in HEADING_RE.finditer(text):
if len(m.group(1)) == 1:
# Strip trailing wiki-style decorations like " — subtitle"
return m.group(2).strip()
return ""
# ---------------------------------------------------------------------------
# Index.md parsing — categories come from section headings
# ---------------------------------------------------------------------------
def parse_index(index_path: Path) -> list[dict]:
"""Parse index.md to extract categories from ## headings and their wikilinks."""
if not index_path.is_file():
return []
text = index_path.read_text(encoding="utf-8", errors="replace")
categories = []
current_category = None
for line in text.split("\n"):
# Detect ## section heading
sec_match = re.match(r"^##\s+(.+)$", line)
if sec_match:
current_category = {
"name": sec_match.group(1).strip(),
"articles": [],
}
categories.append(current_category)
continue
# Collect wikilinks under current section
if current_category:
for wl in WIKILINK_RE.finditer(line):
current_category["articles"].append(wl.group(1).strip())
return categories
# ---------------------------------------------------------------------------
# Log.md parsing — extract operation timeline
# ---------------------------------------------------------------------------
def parse_log(log_path: Path) -> list[dict]:
"""Parse log.md to extract chronological entries."""
if not log_path.is_file():
return []
text = log_path.read_text(encoding="utf-8", errors="replace")
entries = []
log_entry_re = re.compile(
r"^##\s+\[(\d{4}-\d{2}-\d{2})\]\s+(\w+)\s*\|\s*(.+)$", re.MULTILINE
)
for m in log_entry_re.finditer(text):
entries.append({
"date": m.group(1),
"operation": m.group(2),
"title": m.group(3).strip(),
})
return entries
# ---------------------------------------------------------------------------
# Main pipeline
# ---------------------------------------------------------------------------
def build_name_to_stem_map(wiki_root: Path) -> dict[str, str]:
"""Build a case-insensitive map from filename stem to relative stem path.
Full relative paths always map uniquely. Bare basenames map only when
unambiguous duplicate basenames are removed so they don't silently
resolve to the wrong page.
"""
name_map: dict[str, str] = {}
# Track which bare basenames appear more than once
basename_counts: dict[str, int] = {}
for md_file in wiki_root.rglob("*.md"):
rel = md_file.relative_to(wiki_root)
stem = str(rel.with_suffix("")) # e.g., "decisions/decision-foo"
basename = md_file.stem # e.g., "decision-foo"
# Full relative path always maps uniquely
name_map[stem.lower()] = stem
# Track basename for ambiguity detection
key = basename.lower()
basename_counts[key] = basename_counts.get(key, 0) + 1
name_map[key] = stem
# Remove ambiguous basename entries (appear more than once)
for key, count in basename_counts.items():
if count > 1 and key in name_map:
del name_map[key]
return name_map
def resolve_wikilink(target: str, name_map: dict[str, str], node_ids: set[str] | None = None) -> str | None:
"""Resolve a wikilink target to an article node ID.
If node_ids is provided, only resolve to IDs that exist in the set.
"""
key = target.lower().strip()
# Skip targets that are clearly not page names (shell flags, etc.)
if key.startswith("-"):
return None
stem = name_map.get(key)
if stem:
candidate = f"article:{stem}"
# If we have a node set, verify the target exists
if node_ids is not None and candidate not in node_ids:
return None
return candidate
# Try without directory prefix
for stored_key, stored_stem in name_map.items():
if stored_key.endswith("/" + key) or stored_key == key:
candidate = f"article:{stored_stem}"
if node_ids is not None and candidate not in node_ids:
return None
return candidate
return None
def parse_wiki(root: Path) -> dict:
"""Parse a Karpathy-pattern wiki and produce the scan manifest."""
detection = detect_format(root)
if not detection["detected"]:
print(json.dumps({"error": "Not a Karpathy-pattern wiki", "detection": detection}),
file=sys.stderr)
sys.exit(1)
wiki_root = Path(detection["wiki_root"])
raw_root = root / "raw"
# Build name resolution map
name_map = build_name_to_stem_map(wiki_root)
# Find index.md and log.md
index_path = wiki_root / "index.md"
if not index_path.is_file():
index_path = root / "index.md"
log_path = wiki_root / "log.md"
if not log_path.is_file():
log_path = root / "log.md"
# Parse index for categories
categories = parse_index(index_path)
log_entries = parse_log(log_path)
# Build category lookup: wikilink target → category name
category_lookup: dict[str, str] = {}
for cat in categories:
for article_target in cat["articles"]:
category_lookup[article_target.lower()] = cat["name"]
# --- Pre-compute article IDs (for edge resolution validation) ---
# Only skip infra files at the wiki root level, not in subdirectories
# (e.g., wiki/index.md is infra, but wiki/concepts/index.md is content)
article_ids: set[str] = set()
for md_file in sorted(wiki_root.rglob("*.md")):
rel = md_file.relative_to(wiki_root)
stem = str(rel.with_suffix(""))
# Only filter infra files at root level (no parent directory)
if rel.parent == Path(".") and rel.name.lower() in INFRA_FILES:
continue
article_ids.add(f"article:{stem}")
# --- Build article nodes ---
nodes = []
edges = []
warnings = []
stats = {"articles": 0, "sources": 0, "topics": 0, "wikilinks": 0, "unresolved": 0}
for md_file in sorted(wiki_root.rglob("*.md")):
rel = md_file.relative_to(wiki_root)
stem = str(rel.with_suffix(""))
basename = md_file.stem
# Skip infrastructure files only at wiki root level
if rel.parent == Path(".") and rel.name.lower() in INFRA_FILES:
continue
text = md_file.read_text(encoding="utf-8", errors="replace")
h1 = extract_h1(text)
frontmatter = extract_frontmatter(text)
wikilinks = extract_wikilinks(text)
headings = extract_headings(text)
code_langs = extract_code_blocks(text)
summary = extract_first_paragraph(text)
line_count = text.count("\n") + 1
word_count = len(text.split())
# Derive category from index.md lookup
category = category_lookup.get(basename.lower(), "")
if not category:
# Try stem match
category = category_lookup.get(stem.lower(), "")
# Derive tags (deduplicated)
tag_set: set[str] = set()
if category:
tag_set.add(category.lower())
if rel.parent != Path("."):
tag_set.add(str(rel.parent))
fm_tags = frontmatter.get("tags", "")
if fm_tags:
tag_set.update(t.strip() for t in fm_tags.split(",") if t.strip())
tags = sorted(tag_set)
# Complexity from wikilink density
wl_count = len(wikilinks)
if wl_count > 15:
complexity = "complex"
elif wl_count > 5:
complexity = "moderate"
else:
complexity = "simple"
node_id = f"article:{stem}"
nodes.append({
"id": node_id,
"type": "article",
"name": h1 or basename,
"filePath": str(rel),
"summary": summary or f"Wiki article: {h1 or basename}",
"tags": tags,
"complexity": complexity,
"knowledgeMeta": {
"wikilinks": [wl["target"] for wl in wikilinks],
"category": category or None,
"content": text[:3000], # First 3000 chars for LLM analysis
},
})
stats["articles"] += 1
stats["wikilinks"] += wl_count
# Build edges from wikilinks (resolve against known article IDs)
for wl in wikilinks:
target_id = resolve_wikilink(wl["target"], name_map, article_ids)
if target_id and target_id != node_id:
edges.append({
"source": node_id,
"target": target_id,
"type": "related",
"direction": "forward",
"weight": 0.7,
})
elif not target_id:
warnings.append(f"Unresolved wikilink: [[{wl['target']}]] in {rel}")
stats["unresolved"] += 1
# --- Build topic nodes from index.md categories ---
for cat in categories:
topic_id = f"topic:{cat['name'].lower().replace(' ', '-')}"
nodes.append({
"id": topic_id,
"type": "topic",
"name": cat["name"],
"summary": f"Category from index: {cat['name']} ({len(cat['articles'])} articles)",
"tags": ["category"],
"complexity": "simple",
})
stats["topics"] += 1
# categorized_under edges (only resolve to known article nodes)
for article_target in cat["articles"]:
article_id = resolve_wikilink(article_target, name_map, article_ids)
if article_id:
edges.append({
"source": article_id,
"target": topic_id,
"type": "categorized_under",
"direction": "forward",
"weight": 0.6,
})
# --- Build source nodes from raw/ ---
if raw_root.is_dir():
for raw_file in sorted(raw_root.rglob("*")):
if raw_file.is_file() and not raw_file.name.startswith("."):
rel_raw = raw_file.relative_to(root)
ext = raw_file.suffix.lower()
size_kb = raw_file.stat().st_size / 1024
source_id = f"source:{raw_file.relative_to(raw_root).with_suffix('')}"
nodes.append({
"id": source_id,
"type": "source",
"name": raw_file.name,
"filePath": str(rel_raw),
"summary": f"Raw source ({ext or 'unknown'}, {size_kb:.0f} KB)",
"tags": ["raw", ext.lstrip(".") or "unknown"],
"complexity": "simple",
})
stats["sources"] += 1
# --- Compute backlinks ---
backlink_map: dict[str, list[str]] = {}
for edge in edges:
if edge["type"] == "related":
target = edge["target"]
source = edge["source"]
backlink_map.setdefault(target, []).append(source)
for node in nodes:
if node["type"] == "article" and "knowledgeMeta" in node:
bl = backlink_map.get(node["id"], [])
node["knowledgeMeta"]["backlinks"] = bl
# --- Deduplicate edges ---
seen_edges: set[tuple[str, str, str]] = set()
deduped_edges = []
for edge in edges:
key = (edge["source"], edge["target"], edge["type"])
if key not in seen_edges:
seen_edges.add(key)
deduped_edges.append(edge)
return {
"format": "karpathy",
"stats": stats,
"categories": [{"name": c["name"], "count": len(c["articles"])} for c in categories],
"logEntries": len(log_entries),
"nodes": nodes,
"edges": deduped_edges,
"warnings": warnings[:50], # Cap warnings
}
def main():
if len(sys.argv) < 2:
print("Usage: parse-knowledge-base.py <wiki-directory>", file=sys.stderr)
sys.exit(1)
root = Path(sys.argv[1]).resolve()
if not root.is_dir():
print(f"Error: {root} is not a directory", file=sys.stderr)
sys.exit(1)
manifest = parse_wiki(root)
# Write output
out_dir = root / ".understand-anything" / "intermediate"
out_dir.mkdir(parents=True, exist_ok=True)
out_path = out_dir / "scan-manifest.json"
out_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
# Report to stderr
s = manifest["stats"]
print(f"[parse] Karpathy wiki: {s['articles']} articles, {s['sources']} sources, "
f"{s['topics']} topics, {s['wikilinks']} wikilinks "
f"({s['unresolved']} unresolved)", file=sys.stderr)
print(f"[parse] Output: {out_path}", file=sys.stderr)
if __name__ == "__main__":
main()