mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
Merge branch 'main' into feat/language-agnostic-understanding
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
# Installing Understand-Anything for Antigravity
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Git
|
||||
|
||||
## Installation
|
||||
|
||||
1. **Clone the repository:**
|
||||
```bash
|
||||
git clone https://github.com/Lum1104/Understand-Anything.git ~/.antigravity/understand-anything
|
||||
```
|
||||
|
||||
2. **Create the skills symlinks:**
|
||||
```bash
|
||||
mkdir -p ~/.gemini/antigravity/skills
|
||||
ln -s ~/.antigravity/understand-anything/understand-anything-plugin/skills ~/.gemini/antigravity/skills/understand-anything
|
||||
# Universal plugin root symlink — lets the dashboard skill find packages/dashboard/
|
||||
# Skip if already exists (e.g. another platform was installed first)
|
||||
[ -e ~/.understand-anything-plugin ] || [ -L ~/.understand-anything-plugin ] || ln -s ~/.antigravity/understand-anything/understand-anything-plugin ~/.understand-anything-plugin
|
||||
```
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.gemini\antigravity\skills"
|
||||
cmd /c mklink /J "$env:USERPROFILE\.gemini\antigravity\skills\understand-anything" "$env:USERPROFILE\.antigravity\understand-anything\understand-anything-plugin\skills"
|
||||
cmd /c mklink /J "$env:USERPROFILE\.understand-anything-plugin" "$env:USERPROFILE\.antigravity\understand-anything\understand-anything-plugin"
|
||||
```
|
||||
|
||||
3. **Restart the chat or IDE** so Antigravity can discover the skills.
|
||||
|
||||
## Verify
|
||||
|
||||
```bash
|
||||
ls -la ~/.gemini/antigravity/skills/understand-anything
|
||||
```
|
||||
|
||||
You should see a symlink pointing to the skills directory in the cloned repo.
|
||||
|
||||
## Usage
|
||||
|
||||
Skills activate automatically when relevant. You can also invoke directly by saying:
|
||||
- "Run the understand skill to analyze this codebase"
|
||||
- "Use the understand-dashboard skill to view the architecture map"
|
||||
- "Use understand-chat to answer a question about the graph"
|
||||
|
||||
## Updating
|
||||
|
||||
```bash
|
||||
cd ~/.antigravity/understand-anything && git pull
|
||||
```
|
||||
|
||||
Skills update instantly through the symlink.
|
||||
|
||||
## Uninstalling
|
||||
|
||||
```bash
|
||||
rm ~/.gemini/antigravity/skills/understand-anything
|
||||
rm ~/.understand-anything-plugin
|
||||
rm -rf ~/.antigravity/understand-anything
|
||||
```
|
||||
@@ -9,7 +9,7 @@
|
||||
{
|
||||
"name": "understand-anything",
|
||||
"description": "Multi-agent codebase analysis with interactive dashboard, guided tours, and skill commands",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"source": "./understand-anything-plugin"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "understand-anything",
|
||||
"description": "AI-powered codebase understanding — analyze, visualize, and explain any project",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"author": {
|
||||
"name": "Lum1104"
|
||||
},
|
||||
|
||||
+23
-7
@@ -11,16 +11,29 @@
|
||||
git clone https://github.com/Lum1104/Understand-Anything.git ~/.codex/understand-anything
|
||||
```
|
||||
|
||||
2. **Create the skills symlink:**
|
||||
2. **Create the skills symlinks:**
|
||||
```bash
|
||||
mkdir -p ~/.agents/skills
|
||||
ln -s ~/.codex/understand-anything/understand-anything-plugin/skills ~/.agents/skills/understand-anything
|
||||
# Note: if OpenCode's Understand-Anything is already installed, these symlinks
|
||||
# already exist and the ln commands will safely fail — that is fine, the
|
||||
# existing symlinks work for Codex too.
|
||||
for skill in understand understand-chat understand-dashboard understand-diff understand-explain understand-onboard; do
|
||||
ln -sf ~/.codex/understand-anything/understand-anything-plugin/skills/$skill ~/.agents/skills/$skill
|
||||
done
|
||||
# Universal plugin root symlink — lets the dashboard skill find packages/dashboard/
|
||||
# Skip if already exists (e.g. another platform was installed first)
|
||||
[ -e ~/.understand-anything-plugin ] || [ -L ~/.understand-anything-plugin ] || ln -s ~/.codex/understand-anything/understand-anything-plugin ~/.understand-anything-plugin
|
||||
```
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.agents\skills"
|
||||
cmd /c mklink /J "$env:USERPROFILE\.agents\skills\understand-anything" "$env:USERPROFILE\.codex\understand-anything\understand-anything-plugin\skills"
|
||||
$skills = @("understand","understand-chat","understand-dashboard","understand-diff","understand-explain","understand-onboard")
|
||||
foreach ($skill in $skills) {
|
||||
cmd /c mklink /J "$env:USERPROFILE\.agents\skills\$skill" "$env:USERPROFILE\.codex\understand-anything\understand-anything-plugin\skills\$skill"
|
||||
}
|
||||
# Universal plugin root symlink
|
||||
cmd /c mklink /J "$env:USERPROFILE\.understand-anything-plugin" "$env:USERPROFILE\.codex\understand-anything\understand-anything-plugin"
|
||||
```
|
||||
|
||||
3. **Restart Codex** to discover the skills.
|
||||
@@ -28,10 +41,10 @@
|
||||
## Verify
|
||||
|
||||
```bash
|
||||
ls -la ~/.agents/skills/understand-anything
|
||||
ls -la ~/.agents/skills/ | grep understand
|
||||
```
|
||||
|
||||
You should see a symlink pointing to the skills directory.
|
||||
You should see symlinks for each skill pointing into the cloned repository.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -45,11 +58,14 @@ Skills activate automatically when relevant. You can also invoke directly:
|
||||
cd ~/.codex/understand-anything && git pull
|
||||
```
|
||||
|
||||
Skills update instantly through the symlink.
|
||||
Skills update instantly through the symlinks.
|
||||
|
||||
## Uninstalling
|
||||
|
||||
```bash
|
||||
rm ~/.agents/skills/understand-anything
|
||||
for skill in understand understand-chat understand-dashboard understand-diff understand-explain understand-onboard; do
|
||||
rm -f ~/.agents/skills/$skill
|
||||
done
|
||||
rm ~/.understand-anything-plugin
|
||||
rm -rf ~/.codex/understand-anything
|
||||
```
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "understand-anything",
|
||||
"displayName": "Understand Anything",
|
||||
"description": "AI-powered codebase understanding — analyze, visualize, and explain any project",
|
||||
"version": "1.0.5",
|
||||
"version": "1.2.0",
|
||||
"author": {
|
||||
"name": "Lum1104"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# Installing Understand-Anything for Gemini CLI
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Git
|
||||
- [Gemini CLI](https://github.com/google-gemini/gemini-cli) installed
|
||||
|
||||
## Installation
|
||||
|
||||
1. **Clone the repository:**
|
||||
```bash
|
||||
git clone https://github.com/Lum1104/Understand-Anything.git ~/.gemini/understand-anything
|
||||
```
|
||||
|
||||
2. **Create the skills symlinks:**
|
||||
```bash
|
||||
mkdir -p ~/.agents/skills
|
||||
# Note: if another platform's Understand-Anything is already installed, these symlinks
|
||||
# already exist and the ln commands will safely fail — that is fine, the
|
||||
# existing symlinks work for Gemini CLI too.
|
||||
for skill in understand understand-chat understand-dashboard understand-diff understand-explain understand-onboard; do
|
||||
ln -sf ~/.gemini/understand-anything/understand-anything-plugin/skills/$skill ~/.agents/skills/$skill
|
||||
done
|
||||
# Universal plugin root symlink — lets the dashboard skill find packages/dashboard/
|
||||
# Skip if already exists (e.g. another platform was installed first)
|
||||
[ -e ~/.understand-anything-plugin ] || [ -L ~/.understand-anything-plugin ] || ln -s ~/.gemini/understand-anything/understand-anything-plugin ~/.understand-anything-plugin
|
||||
```
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.agents\skills"
|
||||
$skills = @("understand","understand-chat","understand-dashboard","understand-diff","understand-explain","understand-onboard")
|
||||
foreach ($skill in $skills) {
|
||||
cmd /c mklink /J "$env:USERPROFILE\.agents\skills\$skill" "$env:USERPROFILE\.gemini\understand-anything\understand-anything-plugin\skills\$skill"
|
||||
}
|
||||
# Universal plugin root symlink
|
||||
cmd /c mklink /J "$env:USERPROFILE\.understand-anything-plugin" "$env:USERPROFILE\.gemini\understand-anything\understand-anything-plugin"
|
||||
```
|
||||
|
||||
3. **Restart Gemini CLI** to discover the skills.
|
||||
|
||||
## Verify
|
||||
|
||||
```bash
|
||||
ls -la ~/.agents/skills/ | grep understand
|
||||
```
|
||||
|
||||
You should see symlinks for each skill pointing into the cloned repository.
|
||||
|
||||
## Usage
|
||||
|
||||
Skills activate automatically when relevant. You can also invoke directly:
|
||||
- "Analyze this codebase and build a knowledge graph"
|
||||
- "Help me understand this project's architecture"
|
||||
|
||||
## Updating
|
||||
|
||||
```bash
|
||||
cd ~/.gemini/understand-anything && git pull
|
||||
```
|
||||
|
||||
Skills update instantly through the symlinks.
|
||||
|
||||
## Uninstalling
|
||||
|
||||
```bash
|
||||
for skill in understand understand-chat understand-dashboard understand-diff understand-explain understand-onboard; do
|
||||
rm -f ~/.agents/skills/$skill
|
||||
done
|
||||
rm ~/.understand-anything-plugin
|
||||
rm -rf ~/.gemini/understand-anything
|
||||
```
|
||||
@@ -11,16 +11,20 @@
|
||||
git clone https://github.com/Lum1104/Understand-Anything.git ~/.openclaw/understand-anything
|
||||
```
|
||||
|
||||
2. **Create the skills symlink:**
|
||||
2. **Create the skills symlinks:**
|
||||
```bash
|
||||
mkdir -p ~/.openclaw/skills
|
||||
ln -s ~/.openclaw/understand-anything/understand-anything-plugin/skills ~/.openclaw/skills/understand-anything
|
||||
# Universal plugin root symlink — lets the dashboard skill find packages/dashboard/
|
||||
# Skip if already exists (e.g. another platform was installed first)
|
||||
[ -e ~/.understand-anything-plugin ] || [ -L ~/.understand-anything-plugin ] || ln -s ~/.openclaw/understand-anything/understand-anything-plugin ~/.understand-anything-plugin
|
||||
```
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.openclaw\skills"
|
||||
cmd /c mklink /J "$env:USERPROFILE\.openclaw\skills\understand-anything" "$env:USERPROFILE\.openclaw\understand-anything\understand-anything-plugin\skills"
|
||||
cmd /c mklink /J "$env:USERPROFILE\.understand-anything-plugin" "$env:USERPROFILE\.openclaw\understand-anything\understand-anything-plugin"
|
||||
```
|
||||
|
||||
3. **Restart OpenClaw** to discover the skills.
|
||||
@@ -41,5 +45,6 @@ cd ~/.openclaw/understand-anything && git pull
|
||||
|
||||
```bash
|
||||
rm ~/.openclaw/skills/understand-anything
|
||||
rm ~/.understand-anything-plugin
|
||||
rm -rf ~/.openclaw/understand-anything
|
||||
```
|
||||
|
||||
+67
-12
@@ -2,36 +2,91 @@
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [OpenCode.ai](https://opencode.ai) installed
|
||||
- Git
|
||||
- [OpenCode](https://opencode.ai) installed
|
||||
|
||||
## Installation
|
||||
|
||||
Add understand-anything to the `plugin` array in your `opencode.json` (global or project-level):
|
||||
1. **Clone the repository:**
|
||||
```bash
|
||||
git clone https://github.com/Lum1104/Understand-Anything.git ~/.opencode/understand-anything
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"plugin": ["understand-anything@git+https://github.com/Lum1104/Understand-Anything.git"]
|
||||
}
|
||||
```
|
||||
2. **Create the skills symlinks:**
|
||||
```bash
|
||||
mkdir -p ~/.agents/skills
|
||||
# Note: if Codex's Understand-Anything is already installed, these symlinks
|
||||
# already exist and the ln commands will safely fail — that is fine, the
|
||||
# existing symlinks work for OpenCode too.
|
||||
for skill in understand understand-chat understand-dashboard understand-diff understand-explain understand-onboard; do
|
||||
ln -sf ~/.opencode/understand-anything/understand-anything-plugin/skills/$skill ~/.agents/skills/$skill
|
||||
done
|
||||
# Universal plugin root symlink — lets the dashboard skill find packages/dashboard/
|
||||
# Skip if already exists (e.g. another platform was installed first)
|
||||
[ -e ~/.understand-anything-plugin ] || [ -L ~/.understand-anything-plugin ] || ln -s ~/.opencode/understand-anything/understand-anything-plugin ~/.understand-anything-plugin
|
||||
```
|
||||
|
||||
Restart OpenCode. The plugin auto-installs and registers all skills.
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.agents\skills"
|
||||
$skills = @("understand","understand-chat","understand-dashboard","understand-diff","understand-explain","understand-onboard")
|
||||
foreach ($skill in $skills) {
|
||||
cmd /c mklink /J "$env:USERPROFILE\.agents\skills\$skill" "$env:USERPROFILE\.opencode\understand-anything\understand-anything-plugin\skills\$skill"
|
||||
}
|
||||
# Universal plugin root symlink
|
||||
cmd /c mklink /J "$env:USERPROFILE\.understand-anything-plugin" "$env:USERPROFILE\.opencode\understand-anything\understand-anything-plugin"
|
||||
```
|
||||
|
||||
3. **Restart OpenCode** to discover the skills.
|
||||
|
||||
## Verify
|
||||
|
||||
Ask: "List available skills" — you should see understand, understand-chat, understand-dashboard, etc.
|
||||
```bash
|
||||
ls -la ~/.agents/skills/ | grep understand
|
||||
```
|
||||
|
||||
You should see symlinks for each skill pointing into the cloned repository.
|
||||
|
||||
## Usage
|
||||
|
||||
Skills activate automatically when relevant. You can also invoke directly:
|
||||
|
||||
```
|
||||
use skill tool to load understand-anything/understand
|
||||
use skill tool to load understand
|
||||
```
|
||||
|
||||
Or just ask: "Analyze this codebase and build a knowledge graph"
|
||||
|
||||
## Updating
|
||||
|
||||
Restart OpenCode — the plugin re-installs from git automatically.
|
||||
```bash
|
||||
cd ~/.opencode/understand-anything && git pull
|
||||
```
|
||||
|
||||
Skills update instantly through the symlinks.
|
||||
|
||||
## Uninstalling
|
||||
|
||||
Remove the plugin line from `opencode.json` and restart.
|
||||
```bash
|
||||
for skill in understand understand-chat understand-dashboard understand-diff understand-explain understand-onboard; do
|
||||
rm -f ~/.agents/skills/$skill
|
||||
done
|
||||
rm ~/.understand-anything-plugin
|
||||
rm -rf ~/.opencode/understand-anything
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Skills not found
|
||||
|
||||
1. Check that the symlinks exist: `ls -la ~/.agents/skills/ | grep understand`
|
||||
2. Verify the clone succeeded: `ls ~/.opencode/understand-anything/understand-anything-plugin/skills/`
|
||||
3. Restart OpenCode
|
||||
|
||||
### Tool mapping
|
||||
|
||||
When skills reference Claude Code tools:
|
||||
- `TodoWrite` → `todowrite`
|
||||
- `Task` with subagents → `@mention` syntax
|
||||
- `Skill` tool → OpenCode's native `skill` tool
|
||||
- File operations → your native tools
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# Installing Understand-Anything for Pi Agent
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Git
|
||||
- [Pi Agent](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent) installed
|
||||
|
||||
## Installation
|
||||
|
||||
1. **Clone the repository:**
|
||||
```bash
|
||||
git clone https://github.com/Lum1104/Understand-Anything.git ~/.pi/understand-anything
|
||||
```
|
||||
|
||||
2. **Create the skills symlinks:**
|
||||
```bash
|
||||
mkdir -p ~/.agents/skills
|
||||
# Note: if another platform's Understand-Anything is already installed, these symlinks
|
||||
# already exist and the ln commands will safely fail — that is fine, the
|
||||
# existing symlinks work for Pi Agent too.
|
||||
for skill in understand understand-chat understand-dashboard understand-diff understand-explain understand-onboard; do
|
||||
ln -sf ~/.pi/understand-anything/understand-anything-plugin/skills/$skill ~/.agents/skills/$skill
|
||||
done
|
||||
# Universal plugin root symlink — lets the dashboard skill find packages/dashboard/
|
||||
# Skip if already exists (e.g. another platform was installed first)
|
||||
[ -e ~/.understand-anything-plugin ] || [ -L ~/.understand-anything-plugin ] || ln -s ~/.pi/understand-anything/understand-anything-plugin ~/.understand-anything-plugin
|
||||
```
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.agents\skills"
|
||||
$skills = @("understand","understand-chat","understand-dashboard","understand-diff","understand-explain","understand-onboard")
|
||||
foreach ($skill in $skills) {
|
||||
cmd /c mklink /J "$env:USERPROFILE\.agents\skills\$skill" "$env:USERPROFILE\.pi\understand-anything\understand-anything-plugin\skills\$skill"
|
||||
}
|
||||
# Universal plugin root symlink
|
||||
cmd /c mklink /J "$env:USERPROFILE\.understand-anything-plugin" "$env:USERPROFILE\.pi\understand-anything\understand-anything-plugin"
|
||||
```
|
||||
|
||||
3. **Restart Pi Agent** to discover the skills.
|
||||
|
||||
## Verify
|
||||
|
||||
```bash
|
||||
ls -la ~/.agents/skills/ | grep understand
|
||||
```
|
||||
|
||||
You should see symlinks for each skill pointing into the cloned repository.
|
||||
|
||||
## Usage
|
||||
|
||||
Skills activate automatically when relevant. You can also invoke directly:
|
||||
- "Analyze this codebase and build a knowledge graph"
|
||||
- "Help me understand this project's architecture"
|
||||
|
||||
## Updating
|
||||
|
||||
```bash
|
||||
cd ~/.pi/understand-anything && git pull
|
||||
```
|
||||
|
||||
Skills update instantly through the symlinks.
|
||||
|
||||
## Uninstalling
|
||||
|
||||
```bash
|
||||
for skill in understand understand-chat understand-dashboard understand-diff understand-explain understand-onboard; do
|
||||
rm -f ~/.agents/skills/$skill
|
||||
done
|
||||
rm ~/.understand-anything-plugin
|
||||
rm -rf ~/.pi/understand-anything
|
||||
```
|
||||
@@ -3,6 +3,10 @@
|
||||
## Project Overview
|
||||
An open-source tool combining LLM intelligence + static analysis to produce interactive dashboards for understanding codebases.
|
||||
|
||||
## Prerequisites
|
||||
- Node.js >= 22 (developed on v24)
|
||||
- pnpm >= 10 (pinned via `packageManager` field in root `package.json`)
|
||||
|
||||
## Architecture
|
||||
- **Monorepo** with pnpm workspaces
|
||||
- **understand-anything-plugin/** — Claude Code plugin containing all source code:
|
||||
@@ -34,6 +38,7 @@ An open-source tool combining LLM intelligence + static analysis to produce inte
|
||||
- `pnpm --filter @understand-anything/skill test` — Run plugin tests
|
||||
- `pnpm --filter @understand-anything/dashboard build` — Build the dashboard
|
||||
- `pnpm dev:dashboard` — Start dashboard dev server
|
||||
- `pnpm lint` — Run ESLint across the project
|
||||
|
||||
## Conventions
|
||||
- TypeScript strict mode everywhere
|
||||
@@ -42,10 +47,19 @@ An open-source tool combining LLM intelligence + static analysis to produce inte
|
||||
- Knowledge graph JSON lives in `.understand-anything/` directory of analyzed projects
|
||||
- Core uses subpath exports (`./search`, `./types`, `./schema`) to avoid pulling Node.js modules into browser
|
||||
|
||||
## Gotchas
|
||||
- **tree-sitter**: Uses `web-tree-sitter` (WASM) instead of native `tree-sitter` — native bindings fail on darwin/arm64 + Node 24
|
||||
- **Dashboard imports**: Dashboard must only import from core's browser-safe subpath exports (`./search`, `./types`, `./schema`), never the main entry point which pulls in Node.js modules
|
||||
|
||||
## Scripts
|
||||
- `scripts/generate-large-graph.mjs` — Generates a fake knowledge graph for performance testing (e.g. large-graph layout). Writes to `.understand-anything/knowledge-graph.json`. Usage: `node scripts/generate-large-graph.mjs [nodeCount]` (default: 3000 nodes). Not part of the production pipeline.
|
||||
|
||||
## Versioning
|
||||
When pushing to remote, bump the version in **both** of these files (keep them in sync):
|
||||
When pushing to remote, bump the version in **all four** of these files (keep them in sync):
|
||||
- `understand-anything-plugin/package.json` → `"version"` field
|
||||
- `.claude-plugin/marketplace.json` → `plugins[0].version` field
|
||||
- `.claude-plugin/plugin.json` → `"version"` field
|
||||
- `.cursor-plugin/plugin.json` → `"version"` field
|
||||
|
||||
## Testing Local Plugin Changes
|
||||
|
||||
|
||||
+270
@@ -0,0 +1,270 @@
|
||||
# Contributing to Understand Anything
|
||||
|
||||
Thank you for your interest in contributing to Understand Anything! This document provides guidelines and instructions for contributing to the project.
|
||||
|
||||
## 🌟 Ways to Contribute
|
||||
|
||||
- **Bug Reports**: Found a bug? Open an issue with detailed reproduction steps
|
||||
- **Feature Requests**: Have an idea? Share it in the issues section
|
||||
- **Documentation**: Improve or translate documentation
|
||||
- **Code**: Fix bugs, add features, or improve performance
|
||||
- **Testing**: Write tests to improve code coverage
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js >= 22 (developed on v24)
|
||||
- pnpm >= 10 (pinned via `packageManager` field in root `package.json`)
|
||||
- Git for version control
|
||||
|
||||
### Setup
|
||||
|
||||
1. **Fork and Clone**
|
||||
```bash
|
||||
git clone https://github.com/YOUR_USERNAME/Understand-Anything.git
|
||||
cd Understand-Anything
|
||||
```
|
||||
|
||||
2. **Install Dependencies**
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
3. **Build Core Package**
|
||||
```bash
|
||||
pnpm --filter @understand-anything/core build
|
||||
```
|
||||
|
||||
4. **Run Tests**
|
||||
```bash
|
||||
pnpm --filter @understand-anything/core test
|
||||
pnpm --filter @understand-anything/skill test
|
||||
```
|
||||
|
||||
5. **Start Dashboard (Optional)**
|
||||
```bash
|
||||
pnpm dev:dashboard
|
||||
```
|
||||
|
||||
## 📝 Development Workflow
|
||||
|
||||
### 1. Create a Branch
|
||||
|
||||
Create a descriptive branch name:
|
||||
```bash
|
||||
git checkout -b feat/my-feature # For new features
|
||||
git checkout -b fix/bug-description # For bug fixes
|
||||
git checkout -b docs/update-readme # For documentation
|
||||
```
|
||||
|
||||
### 2. Make Changes
|
||||
|
||||
- Write clean, readable code
|
||||
- Follow existing code style and conventions
|
||||
- Add tests for new functionality
|
||||
- Update documentation as needed
|
||||
|
||||
### 3. Test Your Changes
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
pnpm --filter @understand-anything/core test
|
||||
pnpm --filter @understand-anything/skill test
|
||||
|
||||
# Run linter
|
||||
pnpm lint
|
||||
|
||||
# Build packages
|
||||
pnpm build
|
||||
```
|
||||
|
||||
### 4. Commit Your Changes
|
||||
|
||||
Write clear, descriptive commit messages:
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "feat: add keyboard shortcuts to dashboard"
|
||||
```
|
||||
|
||||
**Commit Message Convention:**
|
||||
- `feat:` - New feature
|
||||
- `fix:` - Bug fix
|
||||
- `docs:` - Documentation changes
|
||||
- `style:` - Code style changes (formatting, etc.)
|
||||
- `refactor:` - Code refactoring
|
||||
- `test:` - Adding or updating tests
|
||||
- `chore:` - Maintenance tasks
|
||||
|
||||
### 5. Push and Create Pull Request
|
||||
|
||||
```bash
|
||||
git push origin your-branch-name
|
||||
```
|
||||
|
||||
Then open a Pull Request on GitHub with:
|
||||
- Clear title describing the change
|
||||
- Detailed description of what changed and why
|
||||
- Link to related issues (if any)
|
||||
- Screenshots (for UI changes)
|
||||
|
||||
## 🧪 Testing Guidelines
|
||||
|
||||
### Writing Tests
|
||||
|
||||
- Use Vitest for testing
|
||||
- Place tests in `__tests__` directories or `*.test.ts` files
|
||||
- Aim for high test coverage for new features
|
||||
- Test edge cases and error conditions
|
||||
|
||||
Example test structure:
|
||||
```typescript
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('MyFeature', () => {
|
||||
it('should do something', () => {
|
||||
// Arrange
|
||||
const input = 'test';
|
||||
|
||||
// Act
|
||||
const result = myFunction(input);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe('expected');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
pnpm test
|
||||
|
||||
# Run tests for specific package
|
||||
pnpm --filter @understand-anything/core test
|
||||
|
||||
# Run tests in watch mode
|
||||
pnpm --filter @understand-anything/core test --watch
|
||||
```
|
||||
|
||||
## 📚 Code Style Guidelines
|
||||
|
||||
### TypeScript
|
||||
|
||||
- Use TypeScript strict mode
|
||||
- Define explicit types for function parameters and return values
|
||||
- Avoid `any` type - use `unknown` if type is truly unknown
|
||||
- Use interfaces for object shapes
|
||||
- Use type aliases for unions and complex types
|
||||
|
||||
### Formatting
|
||||
|
||||
- The project uses ESLint for code quality
|
||||
- Consistent indentation (2 spaces)
|
||||
- Use meaningful variable and function names
|
||||
- Keep functions small and focused
|
||||
|
||||
### React/Dashboard
|
||||
|
||||
- Use functional components with hooks
|
||||
- Keep components focused and single-purpose
|
||||
- Use Zustand for state management
|
||||
- Follow the existing component structure
|
||||
|
||||
### File Organization
|
||||
|
||||
```
|
||||
understand-anything-plugin/
|
||||
├── packages/
|
||||
│ ├── core/ # Core analysis engine
|
||||
│ │ ├── src/
|
||||
│ │ └── package.json
|
||||
│ └── dashboard/ # React dashboard
|
||||
│ ├── src/
|
||||
│ │ ├── components/
|
||||
│ │ ├── utils/
|
||||
│ │ └── store.ts
|
||||
│ └── package.json
|
||||
├── src/ # Plugin skills implementation
|
||||
├── agents/ # AI agent prompts
|
||||
└── skills/ # Skill definitions
|
||||
```
|
||||
|
||||
## 🌍 Translation Guidelines
|
||||
|
||||
### Adding a New Language
|
||||
|
||||
1. Create `README.{language-code}.md` (e.g., `README.fr-FR.md`)
|
||||
2. Translate all sections while maintaining formatting
|
||||
3. Update main `README.md` to include language link
|
||||
4. Keep technical terms in English where appropriate
|
||||
5. Ensure all links still work
|
||||
|
||||
Example:
|
||||
```markdown
|
||||
<a href="README.md">English</a> | <a href="README.fr-FR.md">Français</a>
|
||||
```
|
||||
|
||||
## 🐛 Bug Reports
|
||||
|
||||
When reporting bugs, include:
|
||||
|
||||
- **Description**: Clear description of the issue
|
||||
- **Steps to Reproduce**: Detailed steps to reproduce the bug
|
||||
- **Expected Behavior**: What you expected to happen
|
||||
- **Actual Behavior**: What actually happened
|
||||
- **Environment**: OS, Node version, pnpm version
|
||||
- **Screenshots**: If applicable
|
||||
- **Error Messages**: Full error output
|
||||
|
||||
## 💡 Feature Requests
|
||||
|
||||
When requesting features:
|
||||
|
||||
- **Use Case**: Describe the problem you're trying to solve
|
||||
- **Proposed Solution**: How you envision the feature working
|
||||
- **Alternatives**: Other solutions you've considered
|
||||
- **Additional Context**: Any other relevant information
|
||||
|
||||
## 📋 Pull Request Checklist
|
||||
|
||||
Before submitting a PR, ensure:
|
||||
|
||||
- [ ] Code follows the project's style guidelines
|
||||
- [ ] All tests pass (`pnpm test`)
|
||||
- [ ] New code has test coverage
|
||||
- [ ] Documentation is updated (if needed)
|
||||
- [ ] Commit messages follow convention
|
||||
- [ ] PR description clearly explains changes
|
||||
- [ ] No console.log or debug code left behind
|
||||
- [ ] Branch is up to date with main
|
||||
|
||||
## 🤝 Code Review Process
|
||||
|
||||
1. **Automated Checks**: CI runs tests and linting
|
||||
2. **Maintainer Review**: Project maintainers review the code
|
||||
3. **Feedback**: Address any requested changes
|
||||
4. **Approval**: Once approved, PR will be merged
|
||||
5. **Cleanup**: Delete your branch after merge
|
||||
|
||||
## 📞 Getting Help
|
||||
|
||||
- **Issues**: For bugs and feature requests
|
||||
- **Discussions**: For questions and general discussion
|
||||
- **Documentation**: Check existing docs first
|
||||
|
||||
## 📄 License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under the MIT License.
|
||||
|
||||
## 🙏 Recognition
|
||||
|
||||
Contributors will be recognized in:
|
||||
- GitHub contributors list
|
||||
- Release notes (for significant contributions)
|
||||
- Special mentions for exceptional contributions
|
||||
|
||||
---
|
||||
|
||||
**Thank you for contributing to Understand Anything! Your contributions help make code understanding accessible to everyone.** 🚀
|
||||
+304
@@ -0,0 +1,304 @@
|
||||
<h1 align="center">Understand Anything</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>あらゆるコードベースを、探索・検索・質問ができるインタラクティブなナレッジグラフに変換します。</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">中文</a> | <a href="README.ja-JP.md">日本語</a> | <a href="README.tr-TR.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#-クイックスタート"><img src="https://img.shields.io/badge/Quick_Start-blue?style=for-the-badge" alt="クイックスタート" /></a>
|
||||
<a href="https://github.com/Lum1104/Understand-Anything/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow?style=for-the-badge" alt="License: MIT" /></a>
|
||||
<a href="https://docs.anthropic.com/en/docs/claude-code"><img src="https://img.shields.io/badge/Claude_Code-Plugin-8A2BE2?style=for-the-badge" alt="Claude Code Plugin" /></a>
|
||||
<a href="https://lum1104.github.io/Understand-Anything"><img src="https://img.shields.io/badge/Homepage-d4a574?style=for-the-badge" alt="ホームページ" /></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) プラグインです。マルチエージェントパイプラインでプロジェクトを分析し、すべてのファイル・関数・クラス・依存関係のナレッジグラフを構築して、インタラクティブなダッシュボードで視覚的に探索できるようにします。コードを闇雲に読むのはやめて、全体像を把握しましょう。
|
||||
|
||||
---
|
||||
|
||||
## 🤔 なぜ必要なのか?
|
||||
|
||||
コードを読むのは大変です。コードベース全体を理解するのはさらに大変です。ドキュメントは常に古く、オンボーディングには数週間かかり、新機能の開発はまるで考古学のようです。
|
||||
|
||||
Understand Anything は、**LLMの知能**と**静的解析**を組み合わせることでこの問題を解決します。プロジェクトの生きた探索可能なマップを生成し、すべてに平易な日本語の説明が付きます。
|
||||
|
||||
---
|
||||
|
||||
## 🎯 誰のためのツール?
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="33%" valign="top">
|
||||
<h3>👩💻 ジュニア開発者</h3>
|
||||
<p>不慣れなコードに溺れるのはもう終わり。アーキテクチャをステップバイステップで案内するガイドツアーで、すべての関数やクラスが平易な言葉で説明されます。</p>
|
||||
</td>
|
||||
<td width="33%" valign="top">
|
||||
<h3>📋 プロダクトマネージャー&デザイナー</h3>
|
||||
<p>コードを読まなくても、システムが実際にどう動くかを理解できます。「認証はどう動いているの?」のような質問をすれば、実際のコードベースに基づいた明確な回答が得られます。</p>
|
||||
</td>
|
||||
<td width="33%" valign="top">
|
||||
<h3>🤖 AI活用開発者</h3>
|
||||
<p>AIツールにプロジェクトの深いコンテキストを与えましょう。コードレビュー前に <code>/understand-diff</code>、モジュールの詳細調査に <code>/understand-explain</code>、アーキテクチャの推論に <code>/understand-chat</code> を使えます。</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
|
||||
```
|
||||
|
||||
インタラクティブなWebダッシュボードが開き、コードベースがグラフとして可視化されます。アーキテクチャ層ごとに色分けされ、検索やクリックが可能です。ノードを選択すると、コード・関連関係・平易な説明が表示されます。
|
||||
|
||||
### 4. さらに学ぶ
|
||||
|
||||
```bash
|
||||
# コードベースについて何でも質問
|
||||
/understand-chat 支払いフローはどう動いているの?
|
||||
|
||||
# 現在の変更の影響を分析
|
||||
/understand-diff
|
||||
|
||||
# 特定のファイルや関数を詳しく調べる
|
||||
/understand-explain src/auth/login.ts
|
||||
|
||||
# 新メンバー向けのオンボーディングガイドを生成
|
||||
/understand-onboard
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌐 マルチプラットフォームインストール
|
||||
|
||||
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で開くだけです。
|
||||
|
||||
### 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 | ✅ サポート | 自動検出 |
|
||||
| Antigravity | ✅ サポート | AI駆動インストール |
|
||||
| Gemini CLI | ✅ サポート | AI駆動インストール |
|
||||
| Pi Agent | ✅ サポート | AI駆動インストール |
|
||||
|
||||
---
|
||||
|
||||
## ✨ 機能
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/overview.png" alt="ダッシュボードスクリーンショット" width="800" />
|
||||
</p>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="50%" valign="top">
|
||||
<h3>🗺️ インタラクティブナレッジグラフ</h3>
|
||||
<p>ファイル・関数・クラスとそれらの関係をReact Flowで可視化。ノードをクリックするとコードと接続関係が表示されます。</p>
|
||||
</td>
|
||||
<td width="50%" valign="top">
|
||||
<h3>💬 平易な言葉での説明</h3>
|
||||
<p>すべてのノードがLLMによって説明されるため、技術者でなくても、それが何をしているのか、なぜ存在するのかを理解できます。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<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・Service・Data・UI・Utilityなどのアーキテクチャ層ごとに自動グループ化。色分けされた凡例付き。</p>
|
||||
</td>
|
||||
<td width="50%" valign="top">
|
||||
<h3>📚 言語コンセプト</h3>
|
||||
<p>ジェネリクス・クロージャ・デコレータなど12のプログラミングパターンが、出現箇所のコンテキストで説明されます。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
---
|
||||
|
||||
## 🔧 内部の仕組み
|
||||
|
||||
### マルチエージェントパイプライン
|
||||
|
||||
`/understand` コマンドは5つの専門エージェントをオーケストレーションします:
|
||||
|
||||
| エージェント | 役割 |
|
||||
|-------|------|
|
||||
| `project-scanner` | ファイルの検出、言語やフレームワークの検出 |
|
||||
| `file-analyzer` | 関数・クラス・インポートの抽出、グラフノードとエッジの生成 |
|
||||
| `architecture-analyzer` | アーキテクチャ層の特定 |
|
||||
| `tour-builder` | ガイド学習ツアーの生成 |
|
||||
| `graph-reviewer` | グラフの完全性と参照整合性の検証 |
|
||||
|
||||
ファイルアナライザーは並列実行されます(最大3つ同時)。インクリメンタル更新に対応しており、前回の実行から変更されたファイルのみを再分析します。
|
||||
|
||||
### プロジェクト構成
|
||||
|
||||
```
|
||||
understand-anything-plugin/
|
||||
.claude-plugin/ — プラグインマニフェスト
|
||||
agents/ — 専門AIエージェント
|
||||
skills/ — スキル定義(/understand、/understand-chatなど)
|
||||
src/ — TypeScriptソース(context-builder、diff-analyzerなど)
|
||||
packages/
|
||||
core/ — 分析エンジン(types、persistence、tree-sitter、search、schema、tours)
|
||||
dashboard/ — React + TypeScript Webダッシュボード
|
||||
```
|
||||
|
||||
### 技術スタック
|
||||
|
||||
TypeScript、pnpm workspaces、React 18、Vite、TailwindCSS v4、React Flow、Zustand、web-tree-sitter、Fuse.js、Zod、Dagre
|
||||
|
||||
### 開発コマンド
|
||||
|
||||
| コマンド | 説明 |
|
||||
|---------|-------------|
|
||||
| `pnpm install` | すべての依存関係をインストール |
|
||||
| `pnpm --filter @understand-anything/core build` | coreパッケージをビルド |
|
||||
| `pnpm --filter @understand-anything/core test` | coreテストを実行 |
|
||||
| `pnpm --filter @understand-anything/skill build` | プラグインパッケージをビルド |
|
||||
| `pnpm --filter @understand-anything/skill test` | プラグインテストを実行 |
|
||||
| `pnpm --filter @understand-anything/dashboard build` | ダッシュボードをビルド |
|
||||
| `pnpm dev:dashboard` | ダッシュボード開発サーバーを起動 |
|
||||
|
||||
---
|
||||
|
||||
## 🤝 コントリビュート
|
||||
|
||||
コントリビュートを歓迎します!始め方は以下の通りです:
|
||||
|
||||
1. リポジトリをフォーク
|
||||
2. フィーチャーブランチを作成(`git checkout -b feature/my-feature`)
|
||||
3. テストを実行(`pnpm --filter @understand-anything/core test`)
|
||||
4. 変更をコミットしてプルリクエストを作成
|
||||
|
||||
大きな変更については、まずIssueを作成してアプローチを議論してください。
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<strong>コードを闇雲に読むのはやめよう。すべてを理解しよう。</strong>
|
||||
</p>
|
||||
|
||||
## Star History
|
||||
|
||||
<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 License © <a href="https://github.com/Lum1104">Lum1104</a>
|
||||
</p>
|
||||
@@ -5,7 +5,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">中文</a>
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">中文</a> | <a href="README.ja-JP.md">日本語</a> | <a href="README.tr-TR.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -122,11 +122,9 @@ Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Und
|
||||
|
||||
### OpenCode
|
||||
|
||||
Add to your `opencode.json`:
|
||||
```json
|
||||
{
|
||||
"plugin": ["understand-anything@git+https://github.com/Lum1104/Understand-Anything.git"]
|
||||
}
|
||||
Tell OpenCode:
|
||||
```
|
||||
Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.opencode/INSTALL.md
|
||||
```
|
||||
|
||||
### OpenClaw
|
||||
@@ -140,15 +138,39 @@ Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Und
|
||||
|
||||
Cursor auto-discovers the plugin via `.cursor-plugin/plugin.json` when this repo is cloned. No manual installation needed — just clone and open in Cursor.
|
||||
|
||||
### Antigravity
|
||||
|
||||
Tell Antigravity:
|
||||
```text
|
||||
Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.antigravity/INSTALL.md
|
||||
```
|
||||
|
||||
### Gemini CLI
|
||||
|
||||
Tell Gemini CLI:
|
||||
```text
|
||||
Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.gemini/INSTALL.md
|
||||
```
|
||||
|
||||
### Pi Agent
|
||||
|
||||
Tell Pi Agent:
|
||||
```text
|
||||
Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.pi/INSTALL.md
|
||||
```
|
||||
|
||||
### Platform Compatibility
|
||||
|
||||
| Platform | Status | Install Method |
|
||||
|----------|--------|----------------|
|
||||
| Claude Code | ✅ Native | Plugin marketplace |
|
||||
| Codex | ✅ Supported | AI-driven install |
|
||||
| OpenCode | ✅ Supported | Plugin config |
|
||||
| OpenCode | ✅ Supported | AI-driven install |
|
||||
| OpenClaw | ✅ Supported | AI-driven install |
|
||||
| Cursor | ✅ Supported | Auto-discovery |
|
||||
| Antigravity | ✅ Supported | AI-driven install |
|
||||
| Gemini CLI | ✅ Supported | AI-driven install |
|
||||
| Pi Agent | ✅ Supported | AI-driven install |
|
||||
|
||||
---
|
||||
|
||||
|
||||
+304
@@ -0,0 +1,304 @@
|
||||
<h1 align="center">Understand Anything</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>Herhangi bir kod tabanını keşfedebileceğin, arayabileceğin ve hakkında sorular sorabileceğin interaktif bir bilgi grafiğine dönüştür.</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">中文</a> | <a href="README.ja-JP.md">日本語</a> | <a href="README.tr-TR.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#-hızlı-başlangıç"><img src="https://img.shields.io/badge/Hızlı_Başlangıç-blue?style=for-the-badge" alt="Hızlı Başlangıç" /></a>
|
||||
<a href="https://github.com/Lum1104/Understand-Anything/blob/main/LICENSE"><img src="https://img.shields.io/badge/Lisans-MIT-yellow?style=for-the-badge" alt="Lisans: MIT" /></a>
|
||||
<a href="https://docs.anthropic.com/en/docs/claude-code"><img src="https://img.shields.io/badge/Claude_Code-Plugin-8A2BE2?style=for-the-badge" alt="Claude Code Eklentisi" /></a>
|
||||
<a href="https://lum1104.github.io/Understand-Anything"><img src="https://img.shields.io/badge/Ana_Sayfa-d4a574?style=for-the-badge" alt="Ana Sayfa" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/hero.jpg" alt="Understand Anything — Herhangi bir kod tabanını interaktif bir bilgi grafiğine dönüştür" width="800" />
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
> [!TIP]
|
||||
> **Topluluğa çok teşekkürler!** Understand-Anything'e gösterilen destek inanılmaz oldu. Bu araç sana karmaşıklığı anlamak için birkaç dakika kazandırıyorsa, istediğim tek şey buydu. 🚀
|
||||
|
||||
**Yeni bir ekibe katıldın. Kod tabanı 200.000 satır kod. Nereden başlayacaksın bile bilemiyorsun?**
|
||||
|
||||
Understand Anything, projenizi çok-ajan hattıyla analiz eden, her dosya, fonksiyon, sınıf ve bağımlılığın bilgi grafiğini oluşturan ve hepsini görsel olarak keşfetmen için interaktif bir kontrol paneli sunan bir [Claude Code](https://docs.anthropic.com/en/docs/claude-code) eklentisidir. Kodu körü körüne okumayı bırak. Büyük resmi görmeye başla.
|
||||
|
||||
---
|
||||
|
||||
## 🤔 Neden?
|
||||
|
||||
Kod okumak zor. Bütün bir kod tabanını anlamak daha da zor. Dokümantasyon her zaman güncel değil, işe alıştırma haftalar alıyor ve her yeni özellik arkeoloji gibi hissettiriyor.
|
||||
|
||||
Understand Anything bunu **LLM zekası** ile **statik analizi** birleştirerek çözüyor ve projenin canlı, keşfedilebilir bir haritasını üretiyor — her şey için sade Türkçe açıklamalarla.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Kimler için?
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="33%" valign="top">
|
||||
<h3>👩💻 Junior Geliştiriciler</h3>
|
||||
<p>Tanımadığın kodda boğulmayı bırak. Her fonksiyon ve sınıfın sade Türkçe açıklandığı, mimariyi adım adım anlatan rehberli turlar al.</p>
|
||||
</td>
|
||||
<td width="33%" valign="top">
|
||||
<h3>📋 Ürün Yöneticileri ve Tasarımcılar</h3>
|
||||
<p>Kod okumadan sistemin gerçekte nasıl çalıştığını nihayet anla. "Kimlik doğrulama nasıl çalışır?" gibi sorular sor ve gerçek kod tabanına dayalı net cevaplar al.</p>
|
||||
</td>
|
||||
<td width="33%" valign="top">
|
||||
<h3>🤖 AI Destekli Geliştiriciler</h3>
|
||||
<p>AI araçlarına projen hakkında derin bağlam ver. Kod incelemeden önce <code>/understand-diff</code>, herhangi bir modüle dalmak için <code>/understand-explain</code> veya mimari hakkında akıl yürütmek için <code>/understand-chat</code> kullan.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Hızlı Başlangıç
|
||||
|
||||
### 1. Eklentiyi yükle
|
||||
|
||||
```bash
|
||||
/plugin marketplace add Lum1104/Understand-Anything
|
||||
/plugin install understand-anything
|
||||
```
|
||||
|
||||
### 2. Kod tabanını analiz et
|
||||
|
||||
```bash
|
||||
/understand
|
||||
```
|
||||
|
||||
Çok-ajan hattı projenizi tarar, her dosya, fonksiyon, sınıf ve bağımlılığı çıkarır, ardından `.understand-anything/knowledge-graph.json` dosyasına kaydedilen bir bilgi grafiği oluşturur.
|
||||
|
||||
### 3. Kontrol panelini keşfet
|
||||
|
||||
```bash
|
||||
/understand-dashboard
|
||||
```
|
||||
|
||||
Kod tabanın bir grafik olarak görselleştirilmiş, mimari katmana göre renklendirilmiş, aranabilir ve tıklanabilir interaktif bir web kontrol paneli açılır. Kodunu, ilişkilerini ve sade Türkçe açıklamasını görmek için herhangi bir düğüm seç.
|
||||
|
||||
### 4. Öğrenmeye devam et
|
||||
|
||||
```bash
|
||||
# Kod tabanı hakkında her şeyi sor
|
||||
/understand-chat Ödeme akışı nasıl çalışır?
|
||||
|
||||
# Mevcut değişikliklerinin etkisini analiz et
|
||||
/understand-diff
|
||||
|
||||
# Belirli bir dosya veya fonksiyona derinlemesine dal
|
||||
/understand-explain src/auth/login.ts
|
||||
|
||||
# Yeni ekip üyeleri için bir işe alıştırma rehberi oluştur
|
||||
/understand-onboard
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Çoklu Platform Kurulumu
|
||||
|
||||
Understand-Anything birden fazla AI kodlama platformunda çalışır.
|
||||
|
||||
### Claude Code (Yerli)
|
||||
|
||||
```bash
|
||||
/plugin marketplace add Lum1104/Understand-Anything
|
||||
/plugin install understand-anything
|
||||
```
|
||||
|
||||
### Codex
|
||||
|
||||
Codex'e söyle:
|
||||
```
|
||||
Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.codex/INSTALL.md
|
||||
```
|
||||
|
||||
### OpenCode
|
||||
|
||||
OpenCode'a söyle:
|
||||
```
|
||||
Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.opencode/INSTALL.md
|
||||
```
|
||||
|
||||
### OpenClaw
|
||||
|
||||
OpenClaw'a söyle:
|
||||
```
|
||||
Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.openclaw/INSTALL.md
|
||||
```
|
||||
|
||||
### Cursor
|
||||
|
||||
Bu depo klonlandığında Cursor, eklentiyi `.cursor-plugin/plugin.json` aracılığıyla otomatik olarak keşfeder. Manuel kurulum gerekmez — sadece klonla ve Cursor'da aç.
|
||||
|
||||
### Antigravity
|
||||
|
||||
Antigravity'e söyle:
|
||||
```text
|
||||
Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.antigravity/INSTALL.md
|
||||
```
|
||||
|
||||
### Gemini CLI
|
||||
|
||||
Gemini CLI'a söyle:
|
||||
```text
|
||||
Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.gemini/INSTALL.md
|
||||
```
|
||||
|
||||
### Pi Agent
|
||||
|
||||
Pi Agent'a söyle:
|
||||
```text
|
||||
Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.pi/INSTALL.md
|
||||
```
|
||||
|
||||
### Platform Uyumluluğu
|
||||
|
||||
| Platform | Durum | Kurulum Yöntemi |
|
||||
|----------|--------|----------------|
|
||||
| Claude Code | ✅ Yerli | Eklenti pazarı |
|
||||
| Codex | ✅ Destekleniyor | AI güdümlü kurulum |
|
||||
| OpenCode | ✅ Destekleniyor | AI güdümlü kurulum |
|
||||
| OpenClaw | ✅ Destekleniyor | AI güdümlü kurulum |
|
||||
| Cursor | ✅ Destekleniyor | Otomatik keşif |
|
||||
| Antigravity | ✅ Destekleniyor | AI güdümlü kurulum |
|
||||
| Gemini CLI | ✅ Destekleniyor | AI güdümlü kurulum |
|
||||
| Pi Agent | ✅ Destekleniyor | AI güdümlü kurulum |
|
||||
|
||||
---
|
||||
|
||||
## ✨ Özellikler
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/overview.png" alt="Kontrol Paneli Ekran Görüntüsü" width="800" />
|
||||
</p>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="50%" valign="top">
|
||||
<h3>🗺️ İnteraktif Bilgi Grafiği</h3>
|
||||
<p>Dosyalar, fonksiyonlar, sınıflar ve ilişkileri React Flow ile görselleştirildi. Kodunu ve bağlantılarını görmek için herhangi bir düğüme tıkla.</p>
|
||||
</td>
|
||||
<td width="50%" valign="top">
|
||||
<h3>💬 Sade Türkçe Özetler</h3>
|
||||
<p>Her düğüm bir LLM tarafından açıklanır, böylece herkes — teknik olsun ya da olmasın — ne yaptığını ve neden var olduğunu anlayabilir.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%" valign="top">
|
||||
<h3>🧭 Rehberli Turlar</h3>
|
||||
<p>Bağımlılığa göre sıralanmış, mimarinin otomatik oluşturulmuş gözden geçirmeleri. Kod tabanını doğru sırayla öğren.</p>
|
||||
</td>
|
||||
<td width="50%" valign="top">
|
||||
<h3>🔍 Bulanık ve Anlamsal Arama</h3>
|
||||
<p>İsme veya anlamına göre her şeyi bul. "Kimlik doğrulamayı hangi parçalar yönetiyor?" ara ve grafik boyunca ilgili sonuçları al.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%" valign="top">
|
||||
<h3>📊 Diff Etki Analizi</h3>
|
||||
<p>Değişikliklerinin sistemin hangi bölümlerini etkilediğini commit etmeden önce gör. Kod tabanı boyunca dalgalanma etkilerini anla.</p>
|
||||
</td>
|
||||
<td width="50%" valign="top">
|
||||
<h3>🎭 Kişiye Uyarlanabilir UI</h3>
|
||||
<p>Kontrol paneli, kim olduğuna göre ayrıntı seviyesini ayarlar — junior geliştirici, ürün yöneticisi veya güçlü kullanıcı.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%" valign="top">
|
||||
<h3>🏗️ Katman Görselleştirmesi</h3>
|
||||
<p>Mimari katmana göre otomatik gruplama — API, Servis, Veri, UI, Yardımcı — renk kodlu efsaneyle.</p>
|
||||
</td>
|
||||
<td width="50%" valign="top">
|
||||
<h3>📚 Dil Kavramları</h3>
|
||||
<p>12 programlama deseni (generikler, kapanışlar, dekoratörler, vb.) göründükleri her yerde bağlam içinde açıklanır.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Kaputun Altında
|
||||
|
||||
### Çok-Ajan Hattı
|
||||
|
||||
`/understand` komutu 5 özel ajan düzenler:
|
||||
|
||||
| Ajan | Rol |
|
||||
|-------|------|
|
||||
| `project-scanner` | Dosyaları keşfet, dilleri ve çerçeveleri tespit et |
|
||||
| `file-analyzer` | Fonksiyonları, sınıfları, içe aktarmaları çıkar; grafik düğümleri ve kenarları üret |
|
||||
| `architecture-analyzer` | Mimari katmanları tanımla |
|
||||
| `tour-builder` | Rehberli öğrenme turları oluştur |
|
||||
| `graph-reviewer` | Grafik bütünlüğünü ve referans bütünlüğünü doğrula |
|
||||
|
||||
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.
|
||||
|
||||
### Proje Yapısı
|
||||
|
||||
```
|
||||
understand-anything-plugin/
|
||||
.claude-plugin/ — Eklenti manifestosu
|
||||
agents/ — Özel AI ajanları
|
||||
skills/ — Yetenek tanımları (/understand, /understand-chat, vb.)
|
||||
src/ — TypeScript kaynağı (context-builder, diff-analyzer, vb.)
|
||||
packages/
|
||||
core/ — Analiz motoru (tipler, kalıcılık, tree-sitter, arama, şema, turlar)
|
||||
dashboard/ — React + TypeScript web kontrol paneli
|
||||
```
|
||||
|
||||
### Teknoloji Yığını
|
||||
|
||||
TypeScript, pnpm workspaces, React 18, Vite, TailwindCSS v4, React Flow, Zustand, web-tree-sitter, Fuse.js, Zod, Dagre
|
||||
|
||||
### Geliştirme Komutları
|
||||
|
||||
| Komut | Açıklama |
|
||||
|---------|-------------|
|
||||
| `pnpm install` | Tüm bağımlılıkları yükle |
|
||||
| `pnpm --filter @understand-anything/core build` | Core paketini derle |
|
||||
| `pnpm --filter @understand-anything/core test` | Core testlerini çalıştır |
|
||||
| `pnpm --filter @understand-anything/skill build` | Eklenti paketini derle |
|
||||
| `pnpm --filter @understand-anything/skill test` | Eklenti testlerini çalıştır |
|
||||
| `pnpm --filter @understand-anything/dashboard build` | Kontrol panelini derle |
|
||||
| `pnpm dev:dashboard` | Kontrol paneli geliştirme sunucusunu başlat |
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Katkıda Bulunma
|
||||
|
||||
Katkılar memnuniyetle karşılanır! Başlamak için:
|
||||
|
||||
1. Depoyu fork'la
|
||||
2. Bir özellik dalı oluştur (`git checkout -b feature/benim-ozellligim`)
|
||||
3. Testleri çalıştır (`pnpm --filter @understand-anything/core test`)
|
||||
4. Değişikliklerini commit et ve bir pull request aç
|
||||
|
||||
Büyük değişiklikler için lütfen önce bir issue aç ki yaklaşımı tartışalım.
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<strong>Kodu körü körüne okumayı bırak. Her şeyi anlamaya başla.</strong>
|
||||
</p>
|
||||
|
||||
## Star Geçmişi
|
||||
|
||||
<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 Geçmişi Grafiği" src="https://api.star-history.com/image?repos=Lum1104/Understand-Anything&type=date&legend=top-left" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
<p align="center">
|
||||
MIT Lisansı © <a href="https://github.com/Lum1104">Lum1104</a>
|
||||
</p>
|
||||
+29
-7
@@ -4,7 +4,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">中文</a>
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">中文</a> | <a href="README.ja-JP.md">日本語</a> | <a href="README.tr-TR.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -121,11 +121,9 @@ Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Und
|
||||
|
||||
### OpenCode
|
||||
|
||||
添加到你的 `opencode.json` 文件:
|
||||
```json
|
||||
{
|
||||
"plugin": ["understand-anything@git+https://github.com/Lum1104/Understand-Anything.git"]
|
||||
}
|
||||
告诉 OpenCode:
|
||||
```
|
||||
Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.opencode/INSTALL.md
|
||||
```
|
||||
|
||||
### OpenClaw
|
||||
@@ -139,15 +137,39 @@ Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Und
|
||||
|
||||
克隆此仓库后,Cursor 会自动通过 `.cursor-plugin/plugin.json`文件发现插件。无需手动安装 — 只需克隆并在 Cursor 中打开即可。
|
||||
|
||||
### 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 | ✅ Native | 插件市场 |
|
||||
| Codex | ✅ 支持 | AI驱动安装 |
|
||||
| OpenCode | ✅ 支持 | 插件配置 |
|
||||
| OpenCode | ✅ 支持 | AI驱动安装 |
|
||||
| OpenClaw | ✅ 支持 | AI驱动安装 |
|
||||
| Cursor | ✅ 支持 | 自动发现 |
|
||||
| Antigravity | ✅ 支持 | AI驱动安装 |
|
||||
| Gemini CLI | ✅ 支持 | AI驱动安装 |
|
||||
| Pi Agent | ✅ 支持 | AI驱动安装 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
# Language-Agnostic Support Design
|
||||
|
||||
**Date:** 2026-03-21
|
||||
**Status:** Approved
|
||||
**Issue:** Make Understand-Anything codebase-aware and language-agnostic instead of TypeScript-heavy
|
||||
|
||||
## Problem
|
||||
|
||||
The tool's agent prompts, tree-sitter plugin, and language lesson system are heavily biased toward TypeScript/JavaScript. Non-TS codebases get degraded analysis because:
|
||||
|
||||
1. Agent prompts use TS-specific examples and concepts (e.g., "barrel files", "type guards", "generics")
|
||||
2. Tree-sitter plugin only ships TS/JS grammar support — structural analysis silently fails for other languages
|
||||
3. Language lesson detection hardcodes TS-specific concept patterns and display names
|
||||
|
||||
The architecture (PluginRegistry, GraphBuilder, dashboard, search) is already language-neutral. The bias is in shipped content, not the framework.
|
||||
|
||||
## Decisions
|
||||
|
||||
- **Scope:** All three layers — prompts, tree-sitter plugins, language framework
|
||||
- **Languages (v1):** TypeScript, JavaScript, Python, Go, Java, Rust, C/C++, C#, Ruby, PHP, Swift, Kotlin
|
||||
- **Architecture:** Config-first with code escape hatch (hybrid)
|
||||
- **Prompt strategy:** Base prompt + per-language markdown snippet files in a `languages/` folder
|
||||
- **Config location:** Prompt snippets in `skills/understand/languages/`, tree-sitter configs in `packages/core/src/languages/`
|
||||
- **Multi-language projects:** Per-file language analysis + project-level multi-language summary
|
||||
- **Language detection:** Auto-detect from file extensions only (no manual override for v1)
|
||||
|
||||
## Design
|
||||
|
||||
### 1. LanguageConfig Type & Registry
|
||||
|
||||
#### LanguageConfig Interface
|
||||
|
||||
```typescript
|
||||
// packages/core/src/languages/types.ts
|
||||
interface LanguageConfig {
|
||||
id: string; // e.g., "python"
|
||||
displayName: string; // e.g., "Python"
|
||||
extensions: string[]; // e.g., [".py", ".pyi"]
|
||||
treeSitter: {
|
||||
grammarPackage: string; // npm package name
|
||||
nodeTypes: {
|
||||
function: string[]; // e.g., ["function_definition"]
|
||||
class: string[]; // e.g., ["class_definition"]
|
||||
import: string[]; // e.g., ["import_statement", "import_from_statement"]
|
||||
export: string[]; // e.g., ["export_statement"] or [] for languages without exports
|
||||
typeAnnotation: string[]; // e.g., ["type"] for Python type hints
|
||||
};
|
||||
};
|
||||
concepts: string[]; // e.g., ["decorators", "list comprehensions", "generators"]
|
||||
filePatterns?: Record<string, string>; // special files, e.g., {"config": "pyproject.toml"}
|
||||
customAnalyzer?: (node: SyntaxNode) => AnalysisResult; // escape hatch for unusual AST shapes
|
||||
}
|
||||
```
|
||||
|
||||
#### Language Registry
|
||||
|
||||
```typescript
|
||||
// packages/core/src/languages/registry.ts
|
||||
class LanguageRegistry {
|
||||
private configs: Map<string, LanguageConfig>;
|
||||
|
||||
register(config: LanguageConfig): void;
|
||||
getByExtension(ext: string): LanguageConfig | null;
|
||||
getById(id: string): LanguageConfig;
|
||||
getAll(): LanguageConfig[];
|
||||
}
|
||||
```
|
||||
|
||||
#### File Structure
|
||||
|
||||
```
|
||||
packages/core/src/languages/
|
||||
├── types.ts
|
||||
├── registry.ts
|
||||
├── index.ts
|
||||
├── configs/
|
||||
│ ├── typescript.ts
|
||||
│ ├── javascript.ts
|
||||
│ ├── python.ts
|
||||
│ ├── go.ts
|
||||
│ ├── java.ts
|
||||
│ ├── rust.ts
|
||||
│ ├── cpp.ts
|
||||
│ ├── csharp.ts
|
||||
│ ├── ruby.ts
|
||||
│ ├── php.ts
|
||||
│ ├── swift.ts
|
||||
│ └── kotlin.ts
|
||||
```
|
||||
|
||||
All built-in configs auto-registered on import.
|
||||
|
||||
### 2. GenericTreeSitterPlugin
|
||||
|
||||
Replaces the current TS-only `TreeSitterPlugin` with a config-driven version.
|
||||
|
||||
```typescript
|
||||
// packages/core/src/plugins/generic-tree-sitter-plugin.ts
|
||||
class GenericTreeSitterPlugin implements AnalyzerPlugin {
|
||||
private registry: LanguageRegistry;
|
||||
|
||||
canAnalyze(filePath: string): boolean {
|
||||
return this.registry.getByExtension(path.extname(filePath)) !== null;
|
||||
}
|
||||
|
||||
async analyzeFile(filePath: string, content: string): Promise<FileAnalysis> {
|
||||
const config = this.registry.getByExtension(path.extname(filePath));
|
||||
|
||||
// Custom analyzer escape hatch
|
||||
if (config.customAnalyzer) {
|
||||
return config.customAnalyzer(tree.rootNode);
|
||||
}
|
||||
|
||||
// Generic extraction driven by config.treeSitter.nodeTypes
|
||||
const functions = this.extractNodes(tree, config.treeSitter.nodeTypes.function);
|
||||
const classes = this.extractNodes(tree, config.treeSitter.nodeTypes.class);
|
||||
const imports = this.extractNodes(tree, config.treeSitter.nodeTypes.import);
|
||||
const exports = this.extractNodes(tree, config.treeSitter.nodeTypes.export);
|
||||
// ...
|
||||
}
|
||||
|
||||
private extractNodes(tree: Tree, nodeTypes: string[]): NodeInfo[] {
|
||||
// Walk AST, collect all nodes matching any of the given types
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Migration
|
||||
|
||||
- Current `TreeSitterPlugin` deleted, replaced by `GenericTreeSitterPlugin` + TS/JS configs
|
||||
- `PluginRegistry` unchanged
|
||||
- Existing tests updated to use new plugin
|
||||
|
||||
#### WASM Grammar Loading
|
||||
|
||||
- Each grammar loaded lazily on first use and cached
|
||||
- WASM files bundled in `packages/core/src/languages/grammars/` or fetched from tree-sitter's official WASM builds
|
||||
|
||||
### 3. Language-Aware Prompts
|
||||
|
||||
#### File Structure
|
||||
|
||||
```
|
||||
skills/understand/
|
||||
├── file-analyzer-prompt.md # Base prompt (language-neutral)
|
||||
├── tour-builder-prompt.md
|
||||
├── project-scanner-prompt.md
|
||||
├── languages/
|
||||
│ ├── typescript.md
|
||||
│ ├── javascript.md
|
||||
│ ├── python.md
|
||||
│ ├── go.md
|
||||
│ ├── java.md
|
||||
│ ├── rust.md
|
||||
│ ├── cpp.md
|
||||
│ ├── csharp.md
|
||||
│ ├── ruby.md
|
||||
│ ├── php.md
|
||||
│ ├── swift.md
|
||||
│ └── kotlin.md
|
||||
```
|
||||
|
||||
#### Base Prompt Changes
|
||||
|
||||
All TS-specific examples removed from base prompts. Replaced with injection point:
|
||||
|
||||
```markdown
|
||||
## Language-Specific Guidance
|
||||
|
||||
{{LANGUAGE_CONTEXT}}
|
||||
```
|
||||
|
||||
#### Language Markdown Format
|
||||
|
||||
Each language file contains:
|
||||
|
||||
```markdown
|
||||
# Python
|
||||
|
||||
## Key Concepts
|
||||
- Decorators, comprehensions, generators, context managers, type hints, dunder methods
|
||||
|
||||
## Import Patterns
|
||||
- `import module`, `from module import name`, relative imports
|
||||
|
||||
## Notable File Patterns
|
||||
- `__init__.py` (package initializer), `conftest.py` (pytest), `pyproject.toml` (config)
|
||||
|
||||
## Example Summary Style
|
||||
> "FastAPI route handler that accepts a Pydantic model, validates input..."
|
||||
```
|
||||
|
||||
#### Injection Logic
|
||||
|
||||
1. Project scanner detects languages present in the codebase
|
||||
2. File-analyzer: inject matching language `.md` for that file's language
|
||||
3. Tour-builder: inject all detected languages' `.md` files
|
||||
4. Project-scanner: inject all detected languages' key concepts for project-level summary
|
||||
|
||||
#### Multi-Language Projects
|
||||
|
||||
Project-scanner prompt gets a combined section listing all detected languages with their key concepts.
|
||||
|
||||
### 4. Language Lesson Updates
|
||||
|
||||
- Delete `LANGUAGE_DISPLAY_NAMES` — use `LanguageRegistry.getById(id).displayName`
|
||||
- Delete hardcoded concept patterns — use `LanguageConfig.concepts` from registry
|
||||
- Language lesson generation becomes config-driven
|
||||
|
||||
### 5. Testing Strategy
|
||||
|
||||
#### Unit Tests
|
||||
|
||||
1. **LanguageConfig validation** — Each config has all required fields, non-empty nodeTypes
|
||||
2. **LanguageRegistry** — Registration, lookup by extension/id, duplicate handling
|
||||
3. **GenericTreeSitterPlugin per language** — Small fixture file per language verifying function/class/import extraction
|
||||
4. **Language lesson generation** — Concepts sourced from config
|
||||
|
||||
#### Integration Tests
|
||||
|
||||
5. **Multi-language project** — Mixed TS + Python fixture, verify graph contains nodes from both languages
|
||||
6. **Prompt injection** — Correct language `.md` injected based on detected language
|
||||
|
||||
#### Migration Tests
|
||||
|
||||
- Current tree-sitter-plugin tests rewritten for GenericTreeSitterPlugin with TS config
|
||||
- Must produce identical results to validate non-breaking migration
|
||||
|
||||
### 6. Error Handling & Graceful Degradation
|
||||
|
||||
#### Key Principle
|
||||
|
||||
**Every file always gets analyzed.** Tree-sitter is an enhancement, not a gate. The LLM is the primary analyzer; structural analysis enriches it.
|
||||
|
||||
#### Unknown Language
|
||||
|
||||
- Tree-sitter skipped (returns `null`)
|
||||
- LLM analysis still runs — file gets summary, tags, graph node
|
||||
- Debug log: `"No language config for .xyz, skipping structural analysis"`
|
||||
|
||||
#### Missing WASM Grammar
|
||||
|
||||
- Warning logged, that language degrades to LLM-only
|
||||
- Other languages unaffected
|
||||
|
||||
#### Malformed Language Config
|
||||
|
||||
- Validated at registration time via Zod schema
|
||||
- Invalid config throws at startup — fail fast
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
||||
"name": "understand-anything",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": ".opencode/plugins/understand-anything.js",
|
||||
"packageManager": "pnpm@10.6.2+sha512.47870716bea1572b53df34ad8647b42962bc790ce2bf4562ba0f643237d7302a3d6a8ecef9e4bdfc01d23af1969aa90485d4cebb0b9638fa5ef1daef656f6c1b",
|
||||
"scripts": {
|
||||
"prepare": "pnpm --filter @understand-anything/core build",
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Generate a large fake knowledge graph for testing PR #18
|
||||
* (Web Worker layout for large graphs).
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/generate-large-graph.mjs [nodeCount]
|
||||
*
|
||||
* Default: 3000 nodes. Writes to .understand-anything/knowledge-graph.json
|
||||
*/
|
||||
|
||||
import { writeFileSync, mkdirSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const NODE_COUNT = parseInt(process.argv[2] || "3000", 10);
|
||||
const EDGE_RATIO = 1.7; // edges per node (realistic for codebases)
|
||||
|
||||
const nodeTypes = ["file", "function", "class", "module", "concept"];
|
||||
const edgeTypes = [
|
||||
"imports", "exports", "contains", "inherits", "implements",
|
||||
"calls", "subscribes", "publishes", "middleware",
|
||||
"reads_from", "writes_to", "transforms", "validates",
|
||||
"depends_on", "tested_by", "configures",
|
||||
"related", "similar_to",
|
||||
];
|
||||
const complexities = ["simple", "moderate", "complex"];
|
||||
const languages = ["TypeScript", "JavaScript", "Python", "Go", "Rust"];
|
||||
const frameworks = ["React", "Express", "FastAPI", "Gin", "Actix"];
|
||||
|
||||
function pick(arr) {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
function generateNodes(count) {
|
||||
const nodes = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const type = pick(nodeTypes);
|
||||
const name = `${type}_${i}`;
|
||||
nodes.push({
|
||||
id: `node-${i}`,
|
||||
type,
|
||||
name,
|
||||
filePath: type === "file" ? `src/${name}.ts` : undefined,
|
||||
summary: `Auto-generated ${type} node #${i} for performance testing.`,
|
||||
tags: [type, `group-${i % 20}`],
|
||||
complexity: pick(complexities),
|
||||
});
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
function generateEdges(nodes, edgeCount) {
|
||||
const edges = [];
|
||||
const seen = new Set();
|
||||
const n = nodes.length;
|
||||
|
||||
for (let i = 0; i < edgeCount; i++) {
|
||||
let src, tgt;
|
||||
// Forward-only edges to avoid cycles (dagre blows the stack on large cyclic graphs)
|
||||
do {
|
||||
src = Math.floor(Math.random() * (n - 1));
|
||||
const offset = Math.floor(Math.random() * Math.min(50, n - src - 1)) + 1;
|
||||
tgt = src + offset;
|
||||
} while (tgt >= n || src === tgt || seen.has(`${src}-${tgt}`));
|
||||
|
||||
seen.add(`${src}-${tgt}`);
|
||||
edges.push({
|
||||
source: nodes[src].id,
|
||||
target: nodes[tgt].id,
|
||||
type: pick(edgeTypes),
|
||||
direction: "forward",
|
||||
weight: Math.round(Math.random() * 100) / 100,
|
||||
});
|
||||
}
|
||||
return edges;
|
||||
}
|
||||
|
||||
function generateLayers(nodes) {
|
||||
const layers = [];
|
||||
const layerNames = [
|
||||
"Presentation", "Application", "Domain", "Infrastructure",
|
||||
"API Gateway", "Data Access", "Utilities", "Testing",
|
||||
];
|
||||
|
||||
for (let i = 0; i < layerNames.length; i++) {
|
||||
const start = Math.floor((i / layerNames.length) * nodes.length);
|
||||
const end = Math.floor(((i + 1) / layerNames.length) * nodes.length);
|
||||
layers.push({
|
||||
id: `layer-${i}`,
|
||||
name: layerNames[i],
|
||||
description: `${layerNames[i]} layer (auto-generated)`,
|
||||
nodeIds: nodes.slice(start, end).map((n) => n.id),
|
||||
});
|
||||
}
|
||||
return layers;
|
||||
}
|
||||
|
||||
function generateTour(nodes) {
|
||||
const steps = [];
|
||||
const stepCount = Math.min(8, Math.floor(nodes.length / 100));
|
||||
for (let i = 0; i < stepCount; i++) {
|
||||
const idx = Math.floor((i / stepCount) * nodes.length);
|
||||
steps.push({
|
||||
order: i + 1,
|
||||
title: `Step ${i + 1}: Explore ${nodes[idx].name}`,
|
||||
description: `This tour step highlights node **${nodes[idx].name}** and its surrounding context.`,
|
||||
nodeIds: [nodes[idx].id, nodes[Math.min(idx + 1, nodes.length - 1)].id],
|
||||
});
|
||||
}
|
||||
return steps;
|
||||
}
|
||||
|
||||
// ── Generate ──
|
||||
|
||||
const nodes = generateNodes(NODE_COUNT);
|
||||
const edgeCount = Math.floor(NODE_COUNT * EDGE_RATIO);
|
||||
const edges = generateEdges(nodes, edgeCount);
|
||||
const layers = generateLayers(nodes);
|
||||
const tour = generateTour(nodes);
|
||||
|
||||
const graph = {
|
||||
version: "1.0",
|
||||
project: {
|
||||
name: "large-test-project",
|
||||
languages: languages.slice(0, 3),
|
||||
frameworks: frameworks.slice(0, 2),
|
||||
description: `Auto-generated project with ${NODE_COUNT} nodes for performance testing.`,
|
||||
analyzedAt: new Date().toISOString(),
|
||||
gitCommitHash: "0000000000000000000000000000000000000000",
|
||||
},
|
||||
nodes,
|
||||
edges,
|
||||
layers,
|
||||
tour,
|
||||
};
|
||||
|
||||
const outDir = resolve(process.cwd(), ".understand-anything");
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
const outPath = resolve(outDir, "knowledge-graph.json");
|
||||
writeFileSync(outPath, JSON.stringify(graph, null, 2));
|
||||
|
||||
console.log(`Generated knowledge graph:`);
|
||||
console.log(` Nodes: ${nodes.length}`);
|
||||
console.log(` Edges: ${edges.length}`);
|
||||
console.log(` Layers: ${layers.length}`);
|
||||
console.log(` Tour steps: ${tour.length}`);
|
||||
console.log(` Written to: ${outPath}`);
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@understand-anything/skill",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import { validateGraph } from "@understand-anything/core/schema";
|
||||
import { useDashboardStore } from "./store";
|
||||
import GraphView from "./components/GraphView";
|
||||
@@ -10,6 +10,9 @@ import DiffToggle from "./components/DiffToggle";
|
||||
import LearnPanel from "./components/LearnPanel";
|
||||
import PersonaSelector from "./components/PersonaSelector";
|
||||
import ProjectOverview from "./components/ProjectOverview";
|
||||
import KeyboardShortcutsHelp from "./components/KeyboardShortcutsHelp";
|
||||
import { useKeyboardShortcuts } from "./hooks/useKeyboardShortcuts";
|
||||
import type { KeyboardShortcut } from "./hooks/useKeyboardShortcuts";
|
||||
|
||||
function App() {
|
||||
const graph = useDashboardStore((s) => s.graph);
|
||||
@@ -21,6 +24,97 @@ function App() {
|
||||
const closeCodeViewer = useDashboardStore((s) => s.closeCodeViewer);
|
||||
const setDiffOverlay = useDashboardStore((s) => s.setDiffOverlay);
|
||||
const [loadError, setLoadError] = useState<string | null>(null);
|
||||
const [showKeyboardHelp, setShowKeyboardHelp] = useState(false);
|
||||
|
||||
// Define keyboard shortcuts
|
||||
const shortcuts = useMemo<KeyboardShortcut[]>(
|
||||
() => [
|
||||
// Help
|
||||
{
|
||||
key: "?",
|
||||
shiftKey: true,
|
||||
description: "Show keyboard shortcuts",
|
||||
action: () => setShowKeyboardHelp((prev) => !prev),
|
||||
category: "General",
|
||||
},
|
||||
// Navigation
|
||||
{
|
||||
key: "Escape",
|
||||
description: "Close panels and modals",
|
||||
action: () => {
|
||||
// Read from store at invocation time to avoid stale closures
|
||||
const state = useDashboardStore.getState();
|
||||
if (state.codeViewerOpen) {
|
||||
state.closeCodeViewer();
|
||||
} else if (state.selectedNodeId) {
|
||||
state.selectNode(null);
|
||||
} else if (state.tourActive) {
|
||||
state.stopTour();
|
||||
} else {
|
||||
setShowKeyboardHelp(false);
|
||||
}
|
||||
},
|
||||
category: "Navigation",
|
||||
},
|
||||
{
|
||||
key: "/",
|
||||
description: "Focus search bar",
|
||||
action: () => {
|
||||
const searchInput = document.querySelector<HTMLInputElement>(
|
||||
'input[placeholder*="Search"]'
|
||||
);
|
||||
searchInput?.focus();
|
||||
},
|
||||
category: "Navigation",
|
||||
},
|
||||
// Tour controls
|
||||
{
|
||||
key: "ArrowRight",
|
||||
description: "Next tour step",
|
||||
action: () => {
|
||||
const state = useDashboardStore.getState();
|
||||
if (state.tourActive) {
|
||||
state.nextTourStep();
|
||||
}
|
||||
},
|
||||
category: "Tour",
|
||||
},
|
||||
{
|
||||
key: "ArrowLeft",
|
||||
description: "Previous tour step",
|
||||
action: () => {
|
||||
const state = useDashboardStore.getState();
|
||||
if (state.tourActive) {
|
||||
state.prevTourStep();
|
||||
}
|
||||
},
|
||||
category: "Tour",
|
||||
},
|
||||
// View toggles
|
||||
{
|
||||
key: "l",
|
||||
description: "Toggle layer visualization",
|
||||
action: () => {
|
||||
const state = useDashboardStore.getState();
|
||||
state.toggleLayers();
|
||||
},
|
||||
category: "View",
|
||||
},
|
||||
{
|
||||
key: "d",
|
||||
description: "Toggle diff mode",
|
||||
action: () => {
|
||||
const state = useDashboardStore.getState();
|
||||
state.toggleDiffMode();
|
||||
},
|
||||
category: "View",
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
// Register keyboard shortcuts
|
||||
useKeyboardShortcuts(shortcuts);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/knowledge-graph.json")
|
||||
@@ -91,6 +185,25 @@ function App() {
|
||||
<div className="flex items-center gap-4">
|
||||
<DiffToggle />
|
||||
<LayerLegend />
|
||||
<button
|
||||
onClick={() => setShowKeyboardHelp(true)}
|
||||
className="text-text-muted hover:text-gold transition-colors"
|
||||
title="Keyboard shortcuts (Shift + ?)"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -107,8 +220,11 @@ function App() {
|
||||
{/* Main content: Graph + Sidebar */}
|
||||
<div className="flex-1 flex min-h-0 relative">
|
||||
{/* Graph area */}
|
||||
<div className="flex-1 min-w-0 min-h-0">
|
||||
<div className="flex-1 min-w-0 min-h-0 relative">
|
||||
<GraphView />
|
||||
<div className="absolute top-3 right-3 text-sm text-text-muted/60 pointer-events-none select-none">
|
||||
Press <kbd className="kbd">?</kbd> for keyboard shortcuts
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right sidebar */}
|
||||
@@ -135,6 +251,14 @@ function App() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Keyboard shortcuts help modal */}
|
||||
{showKeyboardHelp && (
|
||||
<KeyboardShortcutsHelp
|
||||
shortcuts={shortcuts}
|
||||
onClose={() => setShowKeyboardHelp(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { memo } from "react";
|
||||
import { Handle, Position } from "@xyflow/react";
|
||||
import type { NodeProps, Node } from "@xyflow/react";
|
||||
|
||||
@@ -40,7 +41,7 @@ export interface CustomNodeData extends Record<string, unknown> {
|
||||
|
||||
export type CustomFlowNode = Node<CustomNodeData, "custom">;
|
||||
|
||||
export default function CustomNode({
|
||||
function CustomNodeComponent({
|
||||
id,
|
||||
data,
|
||||
}: NodeProps<CustomFlowNode>) {
|
||||
@@ -79,8 +80,7 @@ export default function CustomNode({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative rounded-lg bg-elevated border border-border-subtle ${extraClass} min-w-[180px] max-w-[220px] overflow-hidden transition-all duration-200 cursor-pointer`}
|
||||
style={{ boxShadow: '0 2px 8px rgba(0,0,0,0.3)' }}
|
||||
className={`relative rounded-lg bg-elevated border border-border-subtle ${extraClass} min-w-[180px] max-w-[220px] overflow-hidden transition-[box-shadow,outline,opacity,filter] duration-200 cursor-pointer shadow-[0_2px_8px_rgba(0,0,0,0.3)]`}
|
||||
onClick={() => data.onNodeClick?.(id)}
|
||||
>
|
||||
{/* Left color bar */}
|
||||
@@ -122,3 +122,6 @@ export default function CustomNode({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const CustomNode = memo(CustomNodeComponent);
|
||||
export default CustomNode;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
ReactFlow,
|
||||
ReactFlowProvider,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
useReactFlow,
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
Controls,
|
||||
@@ -14,14 +16,288 @@ import "@xyflow/react/dist/style.css";
|
||||
import CustomNode from "./CustomNode";
|
||||
import type { CustomFlowNode } from "./CustomNode";
|
||||
import { useDashboardStore } from "../store";
|
||||
import { applyDagreLayout, NODE_WIDTH, NODE_HEIGHT } from "../utils/layout";
|
||||
// Layer colors are hardcoded to gold-tinted values in the group node styles
|
||||
import { applyDagreLayout, applyDagreLayoutAsync, NODE_WIDTH, NODE_HEIGHT } from "../utils/layout";
|
||||
|
||||
const LAYER_PADDING = 40;
|
||||
|
||||
/**
|
||||
* Node count above which layout runs in a Web Worker
|
||||
* to avoid blocking the main thread.
|
||||
*/
|
||||
const ASYNC_LAYOUT_THRESHOLD = 200;
|
||||
|
||||
const nodeTypes = { custom: CustomNode };
|
||||
|
||||
export default function GraphView() {
|
||||
/**
|
||||
* Inner component that pans/zooms to tour-highlighted nodes.
|
||||
* Must be rendered inside <ReactFlow> so useReactFlow() works.
|
||||
*/
|
||||
function TourFitView() {
|
||||
const tourHighlightedNodeIds = useDashboardStore((s) => s.tourHighlightedNodeIds);
|
||||
const { fitView } = useReactFlow();
|
||||
const prevRef = useRef<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const prev = prevRef.current;
|
||||
const changed =
|
||||
tourHighlightedNodeIds.length > 0 &&
|
||||
(tourHighlightedNodeIds.length !== prev.length ||
|
||||
tourHighlightedNodeIds.some((id, i) => id !== prev[i]));
|
||||
prevRef.current = tourHighlightedNodeIds;
|
||||
|
||||
if (changed) {
|
||||
// Small delay to ensure nodes are rendered before fitting
|
||||
requestAnimationFrame(() => {
|
||||
fitView({
|
||||
nodes: tourHighlightedNodeIds.map((id) => ({ id })),
|
||||
duration: 500,
|
||||
padding: 0.3,
|
||||
maxZoom: 1.2,
|
||||
minZoom: 0.01,
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [tourHighlightedNodeIds, fitView]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Centers the graph on the selected node (e.g. from search).
|
||||
* Must be rendered inside <ReactFlow> so useReactFlow() works.
|
||||
*/
|
||||
function SelectedNodeFitView() {
|
||||
const selectedNodeId = useDashboardStore((s) => s.selectedNodeId);
|
||||
const { fitView } = useReactFlow();
|
||||
const prevRef = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedNodeId && selectedNodeId !== prevRef.current) {
|
||||
requestAnimationFrame(() => {
|
||||
fitView({
|
||||
nodes: [{ id: selectedNodeId }],
|
||||
duration: 500,
|
||||
padding: 0.3,
|
||||
maxZoom: 1.2,
|
||||
minZoom: 0.01,
|
||||
});
|
||||
});
|
||||
}
|
||||
prevRef.current = selectedNodeId;
|
||||
}, [selectedNodeId, fitView]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build topology-only flow data: nodes and edges without visual-only state
|
||||
* (selection, tour highlights, search results). This output drives dagre
|
||||
* layout and should only recompute when the graph structure changes.
|
||||
*/
|
||||
function buildTopologyData(
|
||||
graph: NonNullable<ReturnType<typeof useDashboardStore.getState>["graph"]>,
|
||||
persona: string,
|
||||
diffMode: boolean,
|
||||
changedNodeIds: Set<string>,
|
||||
affectedNodeIds: Set<string>,
|
||||
handleNodeSelect: (nodeId: string) => void,
|
||||
) {
|
||||
const filteredGraphNodes =
|
||||
persona === "non-technical"
|
||||
? graph.nodes.filter(
|
||||
(n) =>
|
||||
n.type === "concept" || n.type === "module" || n.type === "file",
|
||||
)
|
||||
: graph.nodes;
|
||||
|
||||
const filteredNodeIds = new Set(filteredGraphNodes.map((n) => n.id));
|
||||
const filteredGraphEdges =
|
||||
persona === "non-technical"
|
||||
? graph.edges.filter(
|
||||
(e) => filteredNodeIds.has(e.source) && filteredNodeIds.has(e.target),
|
||||
)
|
||||
: graph.edges;
|
||||
|
||||
const flowNodes: CustomFlowNode[] = filteredGraphNodes.map((node) => ({
|
||||
id: node.id,
|
||||
type: "custom" as const,
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: node.name ?? node.filePath?.split("/").pop() ?? node.id,
|
||||
nodeType: node.type,
|
||||
summary: node.summary,
|
||||
complexity: node.complexity,
|
||||
isHighlighted: false,
|
||||
searchScore: undefined,
|
||||
isSelected: false,
|
||||
isTourHighlighted: false,
|
||||
isDiffChanged: diffMode && changedNodeIds.has(node.id),
|
||||
isDiffAffected: diffMode && affectedNodeIds.has(node.id),
|
||||
isDiffFaded: diffMode && !changedNodeIds.has(node.id) && !affectedNodeIds.has(node.id),
|
||||
onNodeClick: handleNodeSelect,
|
||||
},
|
||||
}));
|
||||
|
||||
const diffNodeIds = diffMode ? new Set([...changedNodeIds, ...affectedNodeIds]) : new Set<string>();
|
||||
const flowEdges: Edge[] = filteredGraphEdges.map((edge, i) => {
|
||||
const sourceInDiff = diffMode && diffNodeIds.has(edge.source);
|
||||
const targetInDiff = diffMode && diffNodeIds.has(edge.target);
|
||||
const isImpacted = diffMode && (sourceInDiff || targetInDiff);
|
||||
|
||||
return {
|
||||
id: `e-${i}`,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
label: edge.type,
|
||||
animated: edge.type === "calls" || isImpacted,
|
||||
style: isImpacted
|
||||
? {
|
||||
stroke: sourceInDiff && targetInDiff
|
||||
? "rgba(224, 82, 82, 0.7)"
|
||||
: "rgba(212, 160, 48, 0.5)",
|
||||
strokeWidth: 2.5,
|
||||
}
|
||||
: diffMode
|
||||
? { stroke: "rgba(212,165,116,0.08)", strokeWidth: 1 }
|
||||
: { stroke: "rgba(212,165,116,0.3)", strokeWidth: 1.5 },
|
||||
labelStyle: diffMode && !isImpacted
|
||||
? { fill: "rgba(163,151,135,0.3)", fontSize: 10 }
|
||||
: { fill: "#a39787", fontSize: 10 },
|
||||
};
|
||||
});
|
||||
|
||||
return { flowNodes, flowEdges };
|
||||
}
|
||||
|
||||
/**
|
||||
* Lightweight overlay of visual-only state onto already-positioned nodes.
|
||||
* This is O(n) object spreads — cheap even for thousands of nodes — and
|
||||
* avoids triggering a dagre relayout when selection/highlight/search changes.
|
||||
*/
|
||||
function applyVisualState(
|
||||
nodes: (CustomFlowNode | Node)[],
|
||||
selectedNodeId: string | null,
|
||||
tourHighlightedNodeIds: string[],
|
||||
searchResults: Array<{ nodeId: string; score: number }>,
|
||||
): (CustomFlowNode | Node)[] {
|
||||
const searchMap = new Map(searchResults.map((r) => [r.nodeId, r.score]));
|
||||
const tourSet = new Set(tourHighlightedNodeIds);
|
||||
|
||||
return nodes.map((node) => {
|
||||
// Skip group nodes (layer containers) — they have no CustomNodeData
|
||||
if (node.type === "group") return node;
|
||||
|
||||
const searchScore = searchMap.get(node.id);
|
||||
const isHighlighted = searchScore !== undefined;
|
||||
const isSelected = selectedNodeId === node.id;
|
||||
const isTourHighlighted = tourSet.has(node.id);
|
||||
|
||||
const data = node.data as CustomFlowNode["data"];
|
||||
|
||||
// Skip creating a new object if nothing visual changed
|
||||
if (
|
||||
data.isHighlighted === isHighlighted &&
|
||||
data.searchScore === searchScore &&
|
||||
data.isSelected === isSelected &&
|
||||
data.isTourHighlighted === isTourHighlighted
|
||||
) {
|
||||
return node;
|
||||
}
|
||||
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...data,
|
||||
isHighlighted,
|
||||
searchScore,
|
||||
isSelected,
|
||||
isTourHighlighted,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function applyLayerGroups(
|
||||
laidNodes: CustomFlowNode[],
|
||||
edges: Edge[],
|
||||
layers: Array<{ id: string; name: string; nodeIds: string[] }>,
|
||||
showLayers: boolean,
|
||||
): { initialNodes: (CustomFlowNode | Node)[]; initialEdges: Edge[] } {
|
||||
if (!showLayers || layers.length === 0) {
|
||||
return { initialNodes: laidNodes, initialEdges: edges };
|
||||
}
|
||||
|
||||
const nodeToLayer = new Map<string, string>();
|
||||
for (const layer of layers) {
|
||||
for (const nodeId of layer.nodeIds) {
|
||||
nodeToLayer.set(nodeId, layer.id);
|
||||
}
|
||||
}
|
||||
|
||||
const groupNodes: Node[] = [];
|
||||
const adjustedNodes: (CustomFlowNode | Node)[] = [];
|
||||
|
||||
for (const layer of layers) {
|
||||
const memberNodes = laidNodes.filter((n) => layer.nodeIds.includes(n.id));
|
||||
if (memberNodes.length === 0) continue;
|
||||
|
||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||
for (const node of memberNodes) {
|
||||
minX = Math.min(minX, node.position.x);
|
||||
minY = Math.min(minY, node.position.y);
|
||||
maxX = Math.max(maxX, node.position.x + NODE_WIDTH);
|
||||
maxY = Math.max(maxY, node.position.y + NODE_HEIGHT);
|
||||
}
|
||||
|
||||
const groupX = minX - LAYER_PADDING;
|
||||
const groupY = minY - LAYER_PADDING - 24;
|
||||
const groupWidth = maxX - minX + LAYER_PADDING * 2;
|
||||
const groupHeight = maxY - minY + LAYER_PADDING * 2 + 24;
|
||||
|
||||
groupNodes.push({
|
||||
id: layer.id,
|
||||
type: "group",
|
||||
position: { x: groupX, y: groupY },
|
||||
data: { label: layer.name },
|
||||
style: {
|
||||
width: groupWidth,
|
||||
height: groupHeight,
|
||||
backgroundColor: "rgba(212,165,116,0.05)",
|
||||
borderRadius: 12,
|
||||
border: "2px dashed rgba(212,165,116,0.25)",
|
||||
padding: 8,
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: "#d4a574",
|
||||
},
|
||||
});
|
||||
|
||||
for (const node of memberNodes) {
|
||||
adjustedNodes.push({
|
||||
...node,
|
||||
parentId: layer.id,
|
||||
extent: "parent" as const,
|
||||
position: {
|
||||
x: node.position.x - groupX,
|
||||
y: node.position.y - groupY,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const node of laidNodes) {
|
||||
if (!nodeToLayer.has(node.id)) {
|
||||
adjustedNodes.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
initialNodes: [...groupNodes, ...adjustedNodes],
|
||||
initialEdges: edges,
|
||||
};
|
||||
}
|
||||
|
||||
function GraphViewInner() {
|
||||
const graph = useDashboardStore((s) => s.graph);
|
||||
const selectedNodeId = useDashboardStore((s) => s.selectedNodeId);
|
||||
const searchResults = useDashboardStore((s) => s.searchResults);
|
||||
@@ -34,6 +310,8 @@ export default function GraphView() {
|
||||
const changedNodeIds = useDashboardStore((s) => s.changedNodeIds);
|
||||
const affectedNodeIds = useDashboardStore((s) => s.affectedNodeIds);
|
||||
|
||||
const [layouting, setLayouting] = useState(false);
|
||||
|
||||
const handleNodeSelect = useCallback(
|
||||
(nodeId: string) => {
|
||||
selectNode(nodeId);
|
||||
@@ -42,190 +320,86 @@ export default function GraphView() {
|
||||
[selectNode, openCodeViewer],
|
||||
);
|
||||
|
||||
const { initialNodes, initialEdges } = useMemo(() => {
|
||||
if (!graph)
|
||||
return {
|
||||
initialNodes: [] as (CustomFlowNode | Node)[],
|
||||
initialEdges: [] as Edge[],
|
||||
};
|
||||
// ── Topology memo: only recomputes when graph structure changes ──
|
||||
// Does NOT depend on selectedNodeId, tourHighlightedNodeIds, or searchResults.
|
||||
const { topoNodes, topoEdges, needsAsyncLayout } = useMemo(() => {
|
||||
if (!graph) {
|
||||
return { topoNodes: [] as CustomFlowNode[], topoEdges: [] as Edge[], needsAsyncLayout: false };
|
||||
}
|
||||
const { flowNodes, flowEdges } = buildTopologyData(
|
||||
graph, persona, diffMode, changedNodeIds, affectedNodeIds,
|
||||
handleNodeSelect,
|
||||
);
|
||||
return { topoNodes: flowNodes, topoEdges: flowEdges, needsAsyncLayout: flowNodes.length > ASYNC_LAYOUT_THRESHOLD };
|
||||
}, [graph, persona, handleNodeSelect, diffMode, changedNodeIds, affectedNodeIds]);
|
||||
|
||||
// Filter nodes and edges based on persona
|
||||
const filteredGraphNodes =
|
||||
persona === "non-technical"
|
||||
? graph.nodes.filter(
|
||||
(n) =>
|
||||
n.type === "concept" || n.type === "module" || n.type === "file",
|
||||
)
|
||||
: graph.nodes;
|
||||
|
||||
const filteredNodeIds = new Set(filteredGraphNodes.map((n) => n.id));
|
||||
const filteredGraphEdges =
|
||||
persona === "non-technical"
|
||||
? graph.edges.filter(
|
||||
(e) => filteredNodeIds.has(e.source) && filteredNodeIds.has(e.target),
|
||||
)
|
||||
: graph.edges;
|
||||
|
||||
const flowNodes: CustomFlowNode[] = filteredGraphNodes.map((node) => {
|
||||
const matchResult = searchResults.find((r) => r.nodeId === node.id);
|
||||
return {
|
||||
id: node.id,
|
||||
type: "custom" as const,
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: node.name ?? node.filePath?.split("/").pop() ?? node.id,
|
||||
nodeType: node.type,
|
||||
summary: node.summary,
|
||||
complexity: node.complexity,
|
||||
isHighlighted: !!matchResult,
|
||||
searchScore: matchResult?.score,
|
||||
isSelected: selectedNodeId === node.id,
|
||||
isTourHighlighted: tourHighlightedNodeIds.includes(node.id),
|
||||
isDiffChanged: diffMode && changedNodeIds.has(node.id),
|
||||
isDiffAffected: diffMode && affectedNodeIds.has(node.id),
|
||||
isDiffFaded: diffMode && !changedNodeIds.has(node.id) && !affectedNodeIds.has(node.id),
|
||||
onNodeClick: handleNodeSelect,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const diffNodeIds = diffMode ? new Set([...changedNodeIds, ...affectedNodeIds]) : new Set<string>();
|
||||
const flowEdges: Edge[] = filteredGraphEdges.map((edge, i) => {
|
||||
const sourceInDiff = diffMode && diffNodeIds.has(edge.source);
|
||||
const targetInDiff = diffMode && diffNodeIds.has(edge.target);
|
||||
const isImpacted = diffMode && (sourceInDiff || targetInDiff);
|
||||
|
||||
return {
|
||||
id: `e-${i}`,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
label: edge.type,
|
||||
animated: edge.type === "calls" || isImpacted,
|
||||
style: isImpacted
|
||||
? {
|
||||
stroke: sourceInDiff && targetInDiff
|
||||
? "rgba(224, 82, 82, 0.7)"
|
||||
: "rgba(212, 160, 48, 0.5)",
|
||||
strokeWidth: 2.5,
|
||||
}
|
||||
: diffMode
|
||||
? { stroke: "rgba(212,165,116,0.08)", strokeWidth: 1 }
|
||||
: { stroke: "rgba(212,165,116,0.3)", strokeWidth: 1.5 },
|
||||
labelStyle: diffMode && !isImpacted
|
||||
? { fill: "rgba(163,151,135,0.3)", fontSize: 10 }
|
||||
: { fill: "#a39787", fontSize: 10 },
|
||||
};
|
||||
});
|
||||
|
||||
// Run dagre layout on all nodes (without groups)
|
||||
const laid = applyDagreLayout(flowNodes, flowEdges);
|
||||
const laidNodes = laid.nodes as CustomFlowNode[];
|
||||
// ── Laid-out nodes from the last completed layout pass ──
|
||||
// Stored in a ref so layout results persist across visual-state changes.
|
||||
const laidOutRef = useRef<{ initialNodes: (CustomFlowNode | Node)[]; initialEdges: Edge[] } | null>(null);
|
||||
|
||||
// ── Sync layout: for small graphs, run dagre on the main thread ──
|
||||
const syncResult = useMemo(() => {
|
||||
if (!graph || needsAsyncLayout || topoNodes.length === 0) return null;
|
||||
const laid = applyDagreLayout(topoNodes, topoEdges);
|
||||
const layers = graph.layers ?? [];
|
||||
if (!showLayers || layers.length === 0) {
|
||||
return { initialNodes: laidNodes, initialEdges: laid.edges };
|
||||
}
|
||||
return applyLayerGroups(laid.nodes as CustomFlowNode[], laid.edges, layers, showLayers);
|
||||
}, [graph, topoNodes, topoEdges, needsAsyncLayout, showLayers]);
|
||||
|
||||
// Build a map of nodeId -> layer for quick lookup
|
||||
const nodeToLayer = new Map<string, string>();
|
||||
for (const layer of layers) {
|
||||
for (const nodeId of layer.nodeIds) {
|
||||
nodeToLayer.set(nodeId, layer.id);
|
||||
}
|
||||
}
|
||||
// Keep laidOutRef in sync with sync layout results
|
||||
if (syncResult) {
|
||||
laidOutRef.current = syncResult;
|
||||
}
|
||||
|
||||
// Create group nodes and adjust member positions
|
||||
const groupNodes: Node[] = [];
|
||||
const adjustedNodes: (CustomFlowNode | Node)[] = [];
|
||||
// ── Visual memo: cheap overlay of selection/highlight/search state ──
|
||||
const visualNodes = useMemo(() => {
|
||||
const base = laidOutRef.current;
|
||||
if (!base) return [] as (CustomFlowNode | Node)[];
|
||||
return applyVisualState(base.initialNodes, selectedNodeId, tourHighlightedNodeIds, searchResults);
|
||||
}, [laidOutRef.current, selectedNodeId, tourHighlightedNodeIds, searchResults]);
|
||||
|
||||
for (let layerIdx = 0; layerIdx < layers.length; layerIdx++) {
|
||||
const layer = layers[layerIdx];
|
||||
const memberNodes = laidNodes.filter((n) =>
|
||||
layer.nodeIds.includes(n.id),
|
||||
);
|
||||
|
||||
if (memberNodes.length === 0) continue;
|
||||
|
||||
// Compute bounding box around member nodes
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
|
||||
for (const node of memberNodes) {
|
||||
const x = node.position.x;
|
||||
const y = node.position.y;
|
||||
minX = Math.min(minX, x);
|
||||
minY = Math.min(minY, y);
|
||||
maxX = Math.max(maxX, x + NODE_WIDTH);
|
||||
maxY = Math.max(maxY, y + NODE_HEIGHT);
|
||||
}
|
||||
|
||||
// Group node position = top-left with padding
|
||||
const groupX = minX - LAYER_PADDING;
|
||||
const groupY = minY - LAYER_PADDING - 24; // extra space for label
|
||||
const groupWidth = maxX - minX + LAYER_PADDING * 2;
|
||||
const groupHeight = maxY - minY + LAYER_PADDING * 2 + 24;
|
||||
|
||||
// Create the group node
|
||||
groupNodes.push({
|
||||
id: layer.id,
|
||||
type: "group",
|
||||
position: { x: groupX, y: groupY },
|
||||
data: { label: layer.name },
|
||||
style: {
|
||||
width: groupWidth,
|
||||
height: groupHeight,
|
||||
backgroundColor: "rgba(212,165,116,0.05)",
|
||||
borderRadius: 12,
|
||||
border: `2px dashed rgba(212,165,116,0.25)`,
|
||||
padding: 8,
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: "#d4a574",
|
||||
},
|
||||
});
|
||||
|
||||
// Adjust member node positions to be relative to the group
|
||||
for (const node of memberNodes) {
|
||||
adjustedNodes.push({
|
||||
...node,
|
||||
parentId: layer.id,
|
||||
extent: "parent" as const,
|
||||
position: {
|
||||
x: node.position.x - groupX,
|
||||
y: node.position.y - groupY,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add nodes that are not in any layer (keep original positions)
|
||||
for (const node of laidNodes) {
|
||||
if (!nodeToLayer.has(node.id)) {
|
||||
adjustedNodes.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
// Group nodes must come before their children in the array
|
||||
const allNodes: (CustomFlowNode | Node)[] = [
|
||||
...groupNodes,
|
||||
...adjustedNodes,
|
||||
];
|
||||
|
||||
return { initialNodes: allNodes, initialEdges: laid.edges };
|
||||
}, [graph, searchResults, selectedNodeId, showLayers, tourHighlightedNodeIds, persona, handleNodeSelect, diffMode, changedNodeIds, affectedNodeIds]);
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(visualNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(laidOutRef.current?.initialEdges ?? []);
|
||||
|
||||
// ── Push sync layout + visual state to ReactFlow ──
|
||||
useEffect(() => {
|
||||
setNodes(initialNodes);
|
||||
}, [initialNodes, setNodes]);
|
||||
if (syncResult) {
|
||||
const withVisual = applyVisualState(syncResult.initialNodes, selectedNodeId, tourHighlightedNodeIds, searchResults);
|
||||
setNodes(withVisual);
|
||||
setEdges(syncResult.initialEdges);
|
||||
}
|
||||
}, [syncResult, selectedNodeId, tourHighlightedNodeIds, searchResults, setNodes, setEdges]);
|
||||
|
||||
// ── Push visual-only changes (no relayout) ──
|
||||
useEffect(() => {
|
||||
setEdges(initialEdges);
|
||||
}, [initialEdges, setEdges]);
|
||||
if (laidOutRef.current && !layouting) {
|
||||
const withVisual = applyVisualState(laidOutRef.current.initialNodes, selectedNodeId, tourHighlightedNodeIds, searchResults);
|
||||
setNodes(withVisual);
|
||||
}
|
||||
}, [selectedNodeId, tourHighlightedNodeIds, searchResults, setNodes, layouting]);
|
||||
|
||||
// ── Async layout: for large graphs, run dagre in a Web Worker ──
|
||||
useEffect(() => {
|
||||
if (!graph || !needsAsyncLayout || topoNodes.length === 0) return;
|
||||
|
||||
let cancelled = false;
|
||||
setLayouting(true);
|
||||
|
||||
applyDagreLayoutAsync(topoNodes, topoEdges).then((laid) => {
|
||||
if (cancelled) return;
|
||||
const layers = graph.layers ?? [];
|
||||
const result = applyLayerGroups(laid.nodes as CustomFlowNode[], laid.edges, layers, showLayers);
|
||||
laidOutRef.current = result;
|
||||
const withVisual = applyVisualState(result.initialNodes, selectedNodeId, tourHighlightedNodeIds, searchResults);
|
||||
setNodes(withVisual);
|
||||
setEdges(result.initialEdges);
|
||||
setLayouting(false);
|
||||
}).catch(() => {
|
||||
if (cancelled) return;
|
||||
setLayouting(false);
|
||||
});
|
||||
|
||||
return () => { cancelled = true; setLayouting(false); };
|
||||
}, [graph, topoNodes, topoEdges, needsAsyncLayout, showLayers, setNodes, setEdges]);
|
||||
|
||||
const onNodeClick = useCallback(
|
||||
(_: React.MouseEvent, node: { id: string }) => {
|
||||
@@ -251,7 +425,17 @@ export default function GraphView() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<div className="h-full w-full relative">
|
||||
{layouting && (
|
||||
<div className="absolute inset-0 z-50 flex items-center justify-center bg-root/80 rounded-lg">
|
||||
<div className="text-center">
|
||||
<div className="inline-block w-8 h-8 border-2 border-gold border-t-transparent rounded-full animate-spin mb-3" />
|
||||
<p className="text-text-secondary text-sm">
|
||||
Laying out {topoNodes.length.toLocaleString()} nodes...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
@@ -260,7 +444,15 @@ export default function GraphView() {
|
||||
onNodeClick={onNodeClick}
|
||||
onPaneClick={onPaneClick}
|
||||
nodeTypes={nodeTypes}
|
||||
nodesDraggable={false}
|
||||
nodesConnectable={false}
|
||||
edgesFocusable={false}
|
||||
edgesReconnectable={false}
|
||||
elementsSelectable={false}
|
||||
fitView
|
||||
fitViewOptions={{ minZoom: 0.01, padding: 0.1 }}
|
||||
minZoom={0.01}
|
||||
maxZoom={2}
|
||||
colorMode="dark"
|
||||
>
|
||||
<Background variant={BackgroundVariant.Dots} color="rgba(212,165,116,0.15)" gap={20} size={1} />
|
||||
@@ -270,7 +462,17 @@ export default function GraphView() {
|
||||
maskColor="rgba(10,10,10,0.7)"
|
||||
className="!bg-surface !border !border-border-subtle"
|
||||
/>
|
||||
<TourFitView />
|
||||
<SelectedNodeFitView />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function GraphView() {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<GraphViewInner />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
}
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
import type { KeyboardShortcut } from "../hooks/useKeyboardShortcuts";
|
||||
import { formatShortcutKey } from "../hooks/useKeyboardShortcuts";
|
||||
|
||||
interface KeyboardShortcutsHelpProps {
|
||||
shortcuts: KeyboardShortcut[];
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function KeyboardShortcutsHelp({
|
||||
shortcuts,
|
||||
onClose,
|
||||
}: KeyboardShortcutsHelpProps) {
|
||||
// Group shortcuts by category
|
||||
const groupedShortcuts = shortcuts.reduce((acc, shortcut) => {
|
||||
if (!acc[shortcut.category]) {
|
||||
acc[shortcut.category] = [];
|
||||
}
|
||||
acc[shortcut.category].push(shortcut);
|
||||
return acc;
|
||||
}, {} as Record<string, KeyboardShortcut[]>);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="glass rounded-lg shadow-2xl max-w-2xl w-full max-h-[80vh] overflow-auto m-4"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="sticky top-0 glass-heavy border-b border-border-subtle px-6 py-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-serif text-text-primary">
|
||||
Keyboard Shortcuts
|
||||
</h2>
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
Press <kbd className="kbd">?</kbd> anytime to toggle this help
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-text-muted hover:text-text-primary transition-colors"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Shortcuts list */}
|
||||
<div className="p-6 space-y-6">
|
||||
{Object.entries(groupedShortcuts).map(([category, categoryShortcuts]) => (
|
||||
<div key={category}>
|
||||
<h3 className="text-sm font-semibold text-gold uppercase tracking-wider mb-3">
|
||||
{category}
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{categoryShortcuts.map((shortcut, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between py-2 px-3 rounded hover:bg-elevated transition-colors"
|
||||
>
|
||||
<span className="text-sm text-text-secondary">
|
||||
{shortcut.description}
|
||||
</span>
|
||||
<kbd className="kbd">{formatShortcutKey(shortcut)}</kbd>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="sticky bottom-0 glass-heavy border-t border-border-subtle px-6 py-3 text-center">
|
||||
<p className="text-xs text-text-muted">
|
||||
Press <kbd className="kbd">ESC</kbd> to close
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
export interface KeyboardShortcut {
|
||||
key: string;
|
||||
ctrlKey?: boolean;
|
||||
shiftKey?: boolean;
|
||||
altKey?: boolean;
|
||||
metaKey?: boolean;
|
||||
description: string;
|
||||
action: () => void;
|
||||
category: string;
|
||||
}
|
||||
|
||||
export function useKeyboardShortcuts(
|
||||
shortcuts: KeyboardShortcut[],
|
||||
enabled = true
|
||||
) {
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
// Prevent shortcuts from firing when typing in input fields
|
||||
const target = event.target as HTMLElement;
|
||||
const tagName = target.tagName.toLowerCase();
|
||||
if (tagName === 'input' || tagName === 'textarea' || target.isContentEditable) {
|
||||
if (event.key !== 'Escape') return;
|
||||
}
|
||||
|
||||
for (const shortcut of shortcuts) {
|
||||
const keyMatches = event.key.toLowerCase() === shortcut.key.toLowerCase();
|
||||
const ctrlMatches = shortcut.ctrlKey ? event.ctrlKey : !event.ctrlKey;
|
||||
const shiftMatches = shortcut.shiftKey ? event.shiftKey : !event.shiftKey;
|
||||
const altMatches = shortcut.altKey ? event.altKey : !event.altKey;
|
||||
const metaMatches = shortcut.metaKey ? event.metaKey : !event.metaKey;
|
||||
|
||||
if (keyMatches && ctrlMatches && shiftMatches && altMatches && metaMatches) {
|
||||
// Prevent default for shortcuts that might conflict with browser
|
||||
if (event.ctrlKey || event.metaKey || event.altKey) {
|
||||
event.preventDefault();
|
||||
}
|
||||
shortcut.action();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [shortcuts, enabled]);
|
||||
}
|
||||
|
||||
export function formatShortcutKey(shortcut: KeyboardShortcut): string {
|
||||
const keys: string[] = [];
|
||||
|
||||
// Use userAgentData with fallback to navigator.platform
|
||||
const isMac = (navigator as Navigator & { userAgentData?: { platform: string } }).userAgentData?.platform
|
||||
? (navigator as Navigator & { userAgentData: { platform: string } }).userAgentData.platform === 'macOS'
|
||||
: navigator.platform.includes("Mac");
|
||||
|
||||
if (shortcut.ctrlKey || shortcut.metaKey) {
|
||||
keys.push(isMac ? "⌘" : "Ctrl");
|
||||
}
|
||||
// Don't show ⇧ for keys that inherently require Shift (e.g. ?, !, @)
|
||||
const isShiftedPunctuation = shortcut.key.length === 1 && /[^a-zA-Z0-9]/.test(shortcut.key);
|
||||
if (shortcut.shiftKey && !isShiftedPunctuation) keys.push("⇧");
|
||||
if (shortcut.altKey) keys.push(isMac ? "⌥" : "Alt");
|
||||
|
||||
keys.push(isShiftedPunctuation ? shortcut.key : shortcut.key.toUpperCase());
|
||||
|
||||
return keys.join(" + ");
|
||||
}
|
||||
@@ -71,6 +71,31 @@ body {
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.glass-heavy {
|
||||
background: rgba(20, 20, 20, 0.95);
|
||||
border: 1px solid rgba(212, 165, 116, 0.15);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
}
|
||||
|
||||
/* Keyboard shortcut key styling */
|
||||
.kbd {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
padding: 0 0.5rem;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-gold);
|
||||
background: rgba(212, 165, 116, 0.1);
|
||||
border: 1px solid rgba(212, 165, 116, 0.3);
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 0 1px 0 rgba(212, 165, 116, 0.2);
|
||||
}
|
||||
|
||||
/* Animation keyframes */
|
||||
@keyframes fadeSlideIn {
|
||||
from {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import dagre from "@dagrejs/dagre";
|
||||
import type { Node, Edge } from "@xyflow/react";
|
||||
import type { LayoutMessage, LayoutResult } from "./layout.worker";
|
||||
|
||||
export const NODE_WIDTH = 280;
|
||||
export const NODE_HEIGHT = 120;
|
||||
|
||||
/**
|
||||
* Synchronous dagre layout — used for small graphs.
|
||||
*/
|
||||
export function applyDagreLayout(
|
||||
nodes: Node[],
|
||||
edges: Edge[],
|
||||
@@ -43,3 +47,79 @@ export function applyDagreLayout(
|
||||
|
||||
return { nodes: layoutedNodes, edges };
|
||||
}
|
||||
|
||||
let _worker: Worker | null = null;
|
||||
let _nextRequestId = 0;
|
||||
let _latestRequestId = -1;
|
||||
const _pending = new Map<
|
||||
number,
|
||||
{
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
resolve: (v: { nodes: Node[]; edges: Edge[] }) => void;
|
||||
reject: (reason?: unknown) => void;
|
||||
}
|
||||
>();
|
||||
|
||||
function getWorker(): Worker {
|
||||
if (!_worker) {
|
||||
_worker = new Worker(
|
||||
new URL("./layout.worker.ts", import.meta.url),
|
||||
{ type: "module" },
|
||||
);
|
||||
|
||||
_worker.onmessage = (e: MessageEvent<LayoutResult>) => {
|
||||
const { requestId, positions } = e.data;
|
||||
const entry = _pending.get(requestId);
|
||||
_pending.delete(requestId);
|
||||
|
||||
// S1: Discard stale results — only honour the latest request.
|
||||
if (!entry || requestId !== _latestRequestId) return;
|
||||
|
||||
const layoutedNodes = entry.nodes.map((node) => ({
|
||||
...node,
|
||||
position: positions[node.id] ?? { x: 0, y: 0 },
|
||||
}));
|
||||
|
||||
entry.resolve({ nodes: layoutedNodes, edges: entry.edges });
|
||||
};
|
||||
|
||||
_worker.onerror = (err: ErrorEvent) => {
|
||||
for (const [, entry] of _pending) {
|
||||
entry.reject(err);
|
||||
}
|
||||
_pending.clear();
|
||||
};
|
||||
}
|
||||
return _worker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Async dagre layout via Web Worker — used for large graphs.
|
||||
* Keeps the main thread responsive while dagre computes positions.
|
||||
*
|
||||
* Uses request-ID correlation so concurrent calls never cross-wire,
|
||||
* and only the latest request's result is honoured (stale ones are discarded).
|
||||
*/
|
||||
export function applyDagreLayoutAsync(
|
||||
nodes: Node[],
|
||||
edges: Edge[],
|
||||
direction: "TB" | "LR" = "TB",
|
||||
): Promise<{ nodes: Node[]; edges: Edge[] }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const worker = getWorker();
|
||||
const requestId = _nextRequestId++;
|
||||
_latestRequestId = requestId;
|
||||
|
||||
_pending.set(requestId, { nodes, edges, resolve, reject });
|
||||
|
||||
const msg: LayoutMessage = {
|
||||
requestId,
|
||||
nodes: nodes.map((n) => ({ id: n.id, width: NODE_WIDTH, height: NODE_HEIGHT })),
|
||||
edges: edges.map((e) => ({ source: e.source, target: e.target })),
|
||||
direction,
|
||||
};
|
||||
|
||||
worker.postMessage(msg);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import dagre from "@dagrejs/dagre";
|
||||
|
||||
export interface LayoutMessage {
|
||||
requestId: number;
|
||||
nodes: Array<{ id: string; width: number; height: number }>;
|
||||
edges: Array<{ source: string; target: string }>;
|
||||
direction: "TB" | "LR";
|
||||
}
|
||||
|
||||
export interface LayoutResult {
|
||||
requestId: number;
|
||||
positions: Record<string, { x: number; y: number }>;
|
||||
}
|
||||
|
||||
self.onmessage = (e: MessageEvent<LayoutMessage>) => {
|
||||
const { requestId, nodes, edges, direction } = e.data;
|
||||
|
||||
const g = new dagre.graphlib.Graph();
|
||||
g.setDefaultEdgeLabel(() => ({}));
|
||||
g.setGraph({
|
||||
rankdir: direction,
|
||||
nodesep: 60,
|
||||
ranksep: 80,
|
||||
marginx: 20,
|
||||
marginy: 20,
|
||||
});
|
||||
|
||||
for (const node of nodes) {
|
||||
g.setNode(node.id, { width: node.width, height: node.height });
|
||||
}
|
||||
|
||||
for (const edge of edges) {
|
||||
g.setEdge(edge.source, edge.target);
|
||||
}
|
||||
|
||||
dagre.layout(g);
|
||||
|
||||
const positions: Record<string, { x: number; y: number }> = {};
|
||||
for (const node of nodes) {
|
||||
const pos = g.node(node.id);
|
||||
positions[node.id] = pos
|
||||
? { x: pos.x - node.width / 2, y: pos.y - node.height / 2 }
|
||||
: { x: 0, y: 0 };
|
||||
}
|
||||
|
||||
self.postMessage({ requestId, positions } satisfies LayoutResult);
|
||||
};
|
||||
@@ -19,18 +19,40 @@ Start the Understand Anything dashboard to visualize the knowledge graph for the
|
||||
No knowledge graph found. Run /understand first to analyze this project.
|
||||
```
|
||||
|
||||
3. Find the dashboard code. The dashboard is at `packages/dashboard/` relative to this plugin's root directory. Use the Bash tool to resolve the path:
|
||||
```bash
|
||||
PLUGIN_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
```
|
||||
Or locate it by checking these paths in order:
|
||||
- `${CLAUDE_PLUGIN_ROOT}/packages/dashboard/`
|
||||
- The parent directory of this skill file, then `../../packages/dashboard/`
|
||||
3. Find the dashboard code. The dashboard is at `packages/dashboard/` relative to this plugin's root directory. Check these paths in order and use the first that exists:
|
||||
- `~/.understand-anything-plugin/packages/dashboard/` (universal symlink, all installs)
|
||||
- `${CLAUDE_PLUGIN_ROOT}/packages/dashboard/` (Claude Code plugin)
|
||||
- Two levels up from this skill file's real path: `../../packages/dashboard/` (self-relative fallback)
|
||||
|
||||
4. Install dependencies if needed:
|
||||
Use the Bash tool to resolve:
|
||||
```bash
|
||||
SKILL_REAL=$(realpath ~/.agents/skills/understand-dashboard 2>/dev/null || readlink -f ~/.agents/skills/understand-dashboard 2>/dev/null || echo "")
|
||||
SELF_RELATIVE=$([ -n "$SKILL_REAL" ] && cd "$SKILL_REAL/../.." 2>/dev/null && pwd || echo "")
|
||||
|
||||
PLUGIN_ROOT=""
|
||||
for candidate in \
|
||||
"$HOME/.understand-anything-plugin" \
|
||||
"${CLAUDE_PLUGIN_ROOT}" \
|
||||
"$SELF_RELATIVE"; do
|
||||
if [ -n "$candidate" ] && [ -d "$candidate/packages/dashboard" ]; then
|
||||
PLUGIN_ROOT="$candidate"; break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$PLUGIN_ROOT" ]; then
|
||||
echo "Error: Cannot find the understand-anything plugin root. Make sure you followed the installation instructions and that ~/.understand-anything-plugin exists."
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
4. Install dependencies and build if needed:
|
||||
```bash
|
||||
cd <dashboard-dir> && pnpm install --frozen-lockfile 2>/dev/null || pnpm install
|
||||
```
|
||||
Then ensure the core package is built (the dashboard depends on it):
|
||||
```bash
|
||||
cd <plugin-root> && pnpm --filter @understand-anything/core build
|
||||
```
|
||||
|
||||
5. Start the Vite dev server pointing at the project's knowledge graph:
|
||||
```bash
|
||||
|
||||
@@ -195,13 +195,15 @@ Pass these parameters in the dispatch prompt:
|
||||
> [list of edges with type "imports"]
|
||||
> ```
|
||||
|
||||
After the subagent completes, read `$PROJECT_ROOT/.understand-anything/intermediate/layers.json` to get the layer assignments.
|
||||
After the subagent completes, read `$PROJECT_ROOT/.understand-anything/intermediate/layers.json` and normalize it into a final `layers` array. Apply these steps **in order**:
|
||||
|
||||
`layers.json` may be either:
|
||||
- a top-level JSON array of layer objects, or
|
||||
- an envelope object such as `{ "layers": [...] }` from the current prompt/template output
|
||||
1. **Unwrap envelope:** If the file contains `{ "layers": [...] }` instead of a plain array, extract the inner array. (The prompt requests a plain array, but LLMs may still produce an envelope.)
|
||||
2. **Rename legacy fields:** If any layer object has a `nodes` field instead of `nodeIds`, rename `nodes` → `nodeIds`. If `nodes` entries are objects with an `id` field rather than plain strings, extract just the `id` values into `nodeIds`.
|
||||
3. **Synthesize missing IDs:** If any layer is missing an `id`, generate one as `layer:<kebab-case-name>`.
|
||||
4. **Convert file paths:** If `nodeIds` entries are raw file paths (not prefixed with `file:`), convert them to `file:<relative-path>`.
|
||||
5. **Drop dangling refs:** Remove any `nodeIds` entries that do not exist in the merged node set.
|
||||
|
||||
Normalize either form into a final top-level `layers` array before assembling the graph. Each final saved layer object MUST match this exact shape:
|
||||
Each element of the final `layers` array MUST have this shape:
|
||||
|
||||
```json
|
||||
[
|
||||
@@ -214,13 +216,7 @@ Normalize either form into a final top-level `layers` array before assembling th
|
||||
]
|
||||
```
|
||||
|
||||
Rules:
|
||||
- `id` is required and must be unique
|
||||
- `nodeIds` is required and must contain graph node IDs, not raw file paths
|
||||
- If the intermediate output is an envelope object, unwrap its `layers` array before any other normalization
|
||||
- If the subagent returns file paths, convert them to file node IDs before assembling the final graph
|
||||
- Drop any `nodeIds` that do not exist in the merged node set
|
||||
- Do not use a `nodes` field in the final saved layer objects
|
||||
All four fields (`id`, `name`, `description`, `nodeIds`) are required.
|
||||
|
||||
**For incremental updates:** Always re-run architecture analysis on the full merged node set, since layer assignments may shift when files change.
|
||||
|
||||
@@ -273,13 +269,15 @@ Pass these parameters in the dispatch prompt:
|
||||
> [imports and calls edges]
|
||||
> ```
|
||||
|
||||
After the subagent completes, read `$PROJECT_ROOT/.understand-anything/intermediate/tour.json` to get the tour steps.
|
||||
After the subagent completes, read `$PROJECT_ROOT/.understand-anything/intermediate/tour.json` and normalize it into a final `tour` array. Apply these steps **in order**:
|
||||
|
||||
`tour.json` may be either:
|
||||
- a top-level JSON array of tour step objects, or
|
||||
- an envelope object such as `{ "steps": [...] }` from the current prompt/template output
|
||||
1. **Unwrap envelope:** If the file contains `{ "steps": [...] }` instead of a plain array, extract the inner array. (The prompt requests a plain array, but LLMs may still produce an envelope.)
|
||||
2. **Rename legacy fields:** If any step has `nodesToInspect` instead of `nodeIds`, rename it → `nodeIds`. If any step has `whyItMatters` instead of `description`, rename it → `description`.
|
||||
3. **Convert file paths:** If `nodeIds` entries are raw file paths, convert them to `file:<relative-path>`.
|
||||
4. **Drop dangling refs:** Remove any `nodeIds` entries that do not exist in the merged node set.
|
||||
5. **Sort** by `order` before saving.
|
||||
|
||||
Normalize either form into a final top-level `tour` array before assembling the graph. Each final saved tour step object MUST match this exact shape:
|
||||
Each element of the final `tour` array MUST have this shape:
|
||||
|
||||
```json
|
||||
[
|
||||
@@ -292,31 +290,7 @@ Normalize either form into a final top-level `tour` array before assembling the
|
||||
]
|
||||
```
|
||||
|
||||
Rules:
|
||||
- If the intermediate output is an envelope object, unwrap its `steps` array before any other normalization
|
||||
- `description` is required; do not use `whyItMatters` in the final saved tour steps
|
||||
- `nodeIds` is required; do not use `nodesToInspect` in the final saved tour steps
|
||||
- `nodeIds` must reference existing graph node IDs
|
||||
- Preserve optional `languageLesson` when present
|
||||
- Sort by `order` before saving
|
||||
|
||||
---
|
||||
|
||||
## Phase 5.5 — NORMALIZE
|
||||
|
||||
Before assembling the final graph:
|
||||
|
||||
- Unwrap legacy or prompt-shaped envelopes before field renaming:
|
||||
- `{ "layers": [...] }` -> use the contained array as the working `layers` value
|
||||
- `{ "steps": [...] }` -> use the contained array as the working `tour` value
|
||||
- Convert any layer `nodes` field to `nodeIds`
|
||||
- Convert any tour `nodesToInspect` field to `nodeIds`
|
||||
- Convert any tour `whyItMatters` field to `description`
|
||||
- If layers or tour reference file paths, map them to file node IDs using the `file:<relative-path>` convention
|
||||
- Synthesize missing layer IDs as `layer:<kebab-case-name>`
|
||||
- Drop unresolved layer and tour node references
|
||||
- Ensure the final `layers` value is an array of `{ id, name, description, nodeIds }`
|
||||
- Ensure the final `tour` value is an array of `{ order, title, description, nodeIds }`, preserving optional `languageLesson`
|
||||
Required fields: `order`, `title`, `description`, `nodeIds`. Preserve optional `languageLesson` when present.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user