diff --git a/.antigravity/INSTALL.md b/.antigravity/INSTALL.md index 5516be2..2c7681a 100644 --- a/.antigravity/INSTALL.md +++ b/.antigravity/INSTALL.md @@ -11,16 +11,20 @@ git clone https://github.com/Lum1104/Understand-Anything.git ~/.antigravity/understand-anything ``` -2. **Create the skills symlink:** +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. @@ -52,5 +56,6 @@ Skills update instantly through the symlink. ```bash rm ~/.gemini/antigravity/skills/understand-anything +rm ~/.understand-anything-plugin rm -rf ~/.antigravity/understand-anything ``` diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index b23857d..6f2845b 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,8 +9,8 @@ { "name": "understand-anything", "description": "Multi-agent codebase analysis with interactive dashboard, guided tours, and skill commands", - "version": "1.1.1", + "version": "1.2.2", "source": "./understand-anything-plugin" } ] -} \ No newline at end of file +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 7fc838c..375688e 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -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.2", "author": { "name": "Lum1104" }, @@ -15,4 +15,4 @@ "onboarding", "dashboard" ] -} \ No newline at end of file +} diff --git a/.codex/INSTALL.md b/.codex/INSTALL.md index 155ade1..bc4e9a2 100644 --- a/.codex/INSTALL.md +++ b/.codex/INSTALL.md @@ -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 ``` diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 73fcfbe..76707fd 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -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.2", "author": { "name": "Lum1104" }, diff --git a/.gemini/INSTALL.md b/.gemini/INSTALL.md new file mode 100644 index 0000000..fda5bf3 --- /dev/null +++ b/.gemini/INSTALL.md @@ -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 +``` diff --git a/.gitignore b/.gitignore index fafe0f1..5d78e4f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ dist .env.* coverage/ *.log +.claude/ +.worktrees/ diff --git a/.openclaw/INSTALL.md b/.openclaw/INSTALL.md index f06d566..d3150a6 100644 --- a/.openclaw/INSTALL.md +++ b/.openclaw/INSTALL.md @@ -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 ``` diff --git a/.opencode/INSTALL.md b/.opencode/INSTALL.md index 3fa6645..e462957 100644 --- a/.opencode/INSTALL.md +++ b/.opencode/INSTALL.md @@ -2,55 +2,86 @@ ## 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. - -To pin a specific version: - -```json -{ - "plugin": ["understand-anything@git+https://github.com/Lum1104/Understand-Anything.git#v1.1.1"] -} +```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. Verify the plugin line in your `opencode.json` -2. Check that `~/.cache/opencode/node_modules/understand-anything` exists after restart -3. Use the `skill` tool to list discovered skills +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 diff --git a/.opencode/plugins/understand-anything.js b/.opencode/plugins/understand-anything.js deleted file mode 100644 index 43378f6..0000000 --- a/.opencode/plugins/understand-anything.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Understand Anything plugin for OpenCode.ai - * - * Auto-registers the skills directory so OpenCode discovers all - * understand-anything skills without manual symlinks or config edits. - */ - -import path from 'path'; -import { fileURLToPath } from 'url'; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -export const UnderstandAnythingPlugin = async ({ client, directory }) => { - const skillsDir = path.resolve(__dirname, '../../understand-anything-plugin/skills'); - - return { - config: async (config) => { - config.skills = config.skills || {}; - config.skills.paths = config.skills.paths || []; - if (!config.skills.paths.includes(skillsDir)) { - config.skills.paths.push(skillsDir); - } - }, - }; -}; diff --git a/.pi/INSTALL.md b/.pi/INSTALL.md new file mode 100644 index 0000000..02a2012 --- /dev/null +++ b/.pi/INSTALL.md @@ -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 +``` diff --git a/CLAUDE.md b/CLAUDE.md index 56ad507..84abe60 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,7 +51,45 @@ An open-source tool combining LLM intelligence + static analysis to produce inte - **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 + +Claude Code caches installed plugins at `~/.claude/plugins/cache/understand-anything/understand-anything//`. Symlinks don't work because Claude's Search/Glob tools can't follow them. To test local changes: + +1. **Build the packages:** + ```bash + pnpm --filter @understand-anything/core build + pnpm --filter @understand-anything/skill build + ``` + +2. **Find the installed version** (must match what the marketplace currently serves): + ```bash + ls ~/.claude/plugins/cache/understand-anything/understand-anything/ + ``` + +3. **Copy your local plugin into the cache**, replacing `` with the version from step 2: + ```bash + rm -rf ~/.claude/plugins/cache/understand-anything/understand-anything/ + cp -R ./understand-anything-plugin ~/.claude/plugins/cache/understand-anything/understand-anything/ + ``` + +4. **Start a fresh Claude Code session** (existing sessions cache the old prompts in context). + +5. **Run `/understand --full`** in the target project to verify. + +**Re-sync after further changes:** +```bash +pnpm --filter @understand-anything/core build && \ +cp -R ./understand-anything-plugin/* ~/.claude/plugins/cache/understand-anything/understand-anything// +``` + +**To revert to upstream:** Uninstall and reinstall the plugin from the marketplace — it repopulates the cache from the upstream repo. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..64a8154 --- /dev/null +++ b/CONTRIBUTING.md @@ -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 +English | Français +``` + +## 🐛 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.** 🚀 diff --git a/README.ja-JP.md b/README.ja-JP.md index 4540a57..bad5000 100644 --- a/README.ja-JP.md +++ b/README.ja-JP.md @@ -5,7 +5,7 @@

- English | 中文 | 日本語 + English | 中文 | 日本語 | Türkçe

@@ -122,11 +122,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 @@ -147,16 +145,32 @@ Antigravityに以下を伝えてください: 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 | ✅ サポート | プラグイン設定 | +| OpenCode | ✅ サポート | AI駆動インストール | | OpenClaw | ✅ サポート | AI駆動インストール | | Cursor | ✅ サポート | 自動検出 | | Antigravity | ✅ サポート | AI駆動インストール | +| Gemini CLI | ✅ サポート | AI駆動インストール | +| Pi Agent | ✅ サポート | AI駆動インストール | --- diff --git a/README.md b/README.md index a078451..999fb49 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

- English | 中文 | 日本語 + English | 中文 | 日本語 | Türkçe

@@ -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 @@ -147,16 +145,32 @@ Tell Antigravity: 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 | --- @@ -223,9 +237,9 @@ The `/understand` command orchestrates 5 specialized agents: | `file-analyzer` | Extract functions, classes, imports; produce graph nodes and edges | | `architecture-analyzer` | Identify architectural layers | | `tour-builder` | Generate guided learning tours | -| `graph-reviewer` | Validate graph completeness and referential integrity | +| `graph-reviewer` | Validate graph completeness and referential integrity (runs inline by default; use `--review` for full LLM review) | -File analyzers run in parallel (up to 3 concurrent). Supports incremental updates — only re-analyzes files that changed since the last run. +File analyzers run in parallel (up to 5 concurrent, 20-30 files per batch). Supports incremental updates — only re-analyzes files that changed since the last run. ### Project Structure diff --git a/README.tr-TR.md b/README.tr-TR.md new file mode 100644 index 0000000..b39fccb --- /dev/null +++ b/README.tr-TR.md @@ -0,0 +1,304 @@ +

Understand Anything

+ +

+ 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. +

+ +

+ English | 中文 | 日本語 | Türkçe +

+ +

+ Hızlı Başlangıç + Lisans: MIT + Claude Code Eklentisi + Ana Sayfa +

+ +

+ Understand Anything — Herhangi bir kod tabanını interaktif bir bilgi grafiğine dönüştür +

+ +--- + +> [!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? + + + + + + + +
+

👩‍💻 Junior Geliştiriciler

+

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.

+
+

📋 Ürün Yöneticileri ve Tasarımcılar

+

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.

+
+

🤖 AI Destekli Geliştiriciler

+

AI araçlarına projen hakkında derin bağlam ver. Kod incelemeden önce /understand-diff, herhangi bir modüle dalmak için /understand-explain veya mimari hakkında akıl yürütmek için /understand-chat kullan.

+
+ +--- + +## 🚀 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 + +

+ Kontrol Paneli Ekran Görüntüsü +

+ + + + + + + + + + + + + + + + + + +
+

🗺️ İnteraktif Bilgi Grafiği

+

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.

+
+

💬 Sade Türkçe Özetler

+

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.

+
+

🧭 Rehberli Turlar

+

Bağımlılığa göre sıralanmış, mimarinin otomatik oluşturulmuş gözden geçirmeleri. Kod tabanını doğru sırayla öğren.

+
+

🔍 Bulanık ve Anlamsal Arama

+

İ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.

+
+

📊 Diff Etki Analizi

+

Değişikliklerinin sistemin hangi bölümlerini etkilediğini commit etmeden önce gör. Kod tabanı boyunca dalgalanma etkilerini anla.

+
+

🎭 Kişiye Uyarlanabilir UI

+

Kontrol paneli, kim olduğuna göre ayrıntı seviyesini ayarlar — junior geliştirici, ürün yöneticisi veya güçlü kullanıcı.

+
+

🏗️ Katman Görselleştirmesi

+

Mimari katmana göre otomatik gruplama — API, Servis, Veri, UI, Yardımcı — renk kodlu efsaneyle.

+
+

📚 Dil Kavramları

+

12 programlama deseni (generikler, kapanışlar, dekoratörler, vb.) göründükleri her yerde bağlam içinde açıklanır.

+
+ +--- + +## 🔧 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. + +--- + +

+ Kodu körü körüne okumayı bırak. Her şeyi anlamaya başla. +

+ +## Star Geçmişi + + + + + + Star Geçmişi Grafiği + + + +

+ MIT Lisansı © Lum1104 +

diff --git a/README.zh-CN.md b/README.zh-CN.md index 2174652..79b4ad9 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -4,7 +4,7 @@

- English | 中文 | 日本語 + English | 中文 | 日本語 | Türkçe

@@ -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 @@ -146,16 +144,32 @@ Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Und 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驱动安装 | --- diff --git a/docs/plans/2026-03-21-language-agnostic-design.md b/docs/plans/2026-03-21-language-agnostic-design.md new file mode 100644 index 0000000..1d0ec36 --- /dev/null +++ b/docs/plans/2026-03-21-language-agnostic-design.md @@ -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; // 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; + + 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 { + 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 diff --git a/docs/plans/2026-03-21-language-agnostic-plan.md b/docs/plans/2026-03-21-language-agnostic-plan.md new file mode 100644 index 0000000..16e14c4 --- /dev/null +++ b/docs/plans/2026-03-21-language-agnostic-plan.md @@ -0,0 +1,1392 @@ +# Language-Agnostic Support Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Make Understand-Anything language-agnostic by introducing a config-driven language framework, replacing the TS-only tree-sitter plugin, and creating language-aware prompts for 12 languages. + +**Architecture:** Config-first hybrid approach — each language defined by a `LanguageConfig` object (tree-sitter node mappings, concepts, extensions) plus a prompt snippet markdown file. A single `GenericTreeSitterPlugin` replaces the hardcoded TS-only plugin, driven by whichever config matches the file extension. + +**Tech Stack:** TypeScript, web-tree-sitter (WASM), Zod v4, Vitest + +--- + +### Task 1: Create LanguageConfig types and Zod schema + +**Files:** +- Create: `understand-anything-plugin/packages/core/src/languages/types.ts` + +**Step 1: Write the failing test** + +Create: `understand-anything-plugin/packages/core/src/languages/__tests__/types.test.ts` + +```typescript +import { describe, it, expect } from "vitest"; +import { LanguageConfigSchema } from "../types.js"; + +describe("LanguageConfigSchema", () => { + it("validates a complete language config", () => { + const config = { + id: "python", + displayName: "Python", + extensions: [".py", ".pyi"], + treeSitter: { + grammarPackage: "tree-sitter-python", + wasmFile: "tree-sitter-python.wasm", + nodeTypes: { + function: ["function_definition"], + class: ["class_definition"], + import: ["import_statement", "import_from_statement"], + export: [], + typeAnnotation: ["type"], + }, + }, + concepts: ["decorators", "list comprehensions", "generators"], + }; + const result = LanguageConfigSchema.safeParse(config); + expect(result.success).toBe(true); + }); + + it("rejects config missing required fields", () => { + const result = LanguageConfigSchema.safeParse({ id: "python" }); + expect(result.success).toBe(false); + }); + + it("accepts optional filePatterns", () => { + const config = { + id: "python", + displayName: "Python", + extensions: [".py"], + treeSitter: { + grammarPackage: "tree-sitter-python", + wasmFile: "tree-sitter-python.wasm", + nodeTypes: { + function: ["function_definition"], + class: ["class_definition"], + import: ["import_statement"], + export: [], + typeAnnotation: [], + }, + }, + concepts: ["decorators"], + filePatterns: { config: "pyproject.toml" }, + }; + const result = LanguageConfigSchema.safeParse(config); + expect(result.success).toBe(true); + }); +}); +``` + +**Step 2: Run test to verify it fails** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run src/languages/__tests__/types.test.ts` +Expected: FAIL — module `../types.js` not found + +**Step 3: Write minimal implementation** + +Create: `understand-anything-plugin/packages/core/src/languages/types.ts` + +```typescript +import { z } from "zod/v4"; + +export const TreeSitterConfigSchema = z.object({ + grammarPackage: z.string(), + wasmFile: z.string(), + nodeTypes: z.object({ + function: z.array(z.string()), + class: z.array(z.string()), + import: z.array(z.string()), + export: z.array(z.string()), + typeAnnotation: z.array(z.string()), + }), +}); + +export const LanguageConfigSchema = z.object({ + id: z.string(), + displayName: z.string(), + extensions: z.array(z.string()), + treeSitter: TreeSitterConfigSchema, + concepts: z.array(z.string()), + filePatterns: z.record(z.string(), z.string()).optional(), +}); + +export type LanguageConfig = z.infer; +export type TreeSitterConfig = z.infer; +``` + +**Step 4: Run test to verify it passes** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run src/languages/__tests__/types.test.ts` +Expected: PASS + +**Step 5: Commit** + +```bash +git add understand-anything-plugin/packages/core/src/languages/ +git commit -m "feat: add LanguageConfig types and Zod schema" +``` + +--- + +### Task 2: Create LanguageRegistry + +**Files:** +- Create: `understand-anything-plugin/packages/core/src/languages/registry.ts` + +**Step 1: Write the failing test** + +Create: `understand-anything-plugin/packages/core/src/languages/__tests__/registry.test.ts` + +```typescript +import { describe, it, expect } from "vitest"; +import { LanguageRegistry } from "../registry.js"; +import type { LanguageConfig } from "../types.js"; + +const pythonConfig: LanguageConfig = { + id: "python", + displayName: "Python", + extensions: [".py", ".pyi"], + treeSitter: { + grammarPackage: "tree-sitter-python", + wasmFile: "tree-sitter-python.wasm", + nodeTypes: { + function: ["function_definition"], + class: ["class_definition"], + import: ["import_statement", "import_from_statement"], + export: [], + typeAnnotation: ["type"], + }, + }, + concepts: ["decorators", "generators"], +}; + +const tsConfig: LanguageConfig = { + id: "typescript", + displayName: "TypeScript", + extensions: [".ts", ".tsx"], + treeSitter: { + grammarPackage: "tree-sitter-typescript", + wasmFile: "tree-sitter-typescript.wasm", + nodeTypes: { + function: ["function_declaration"], + class: ["class_declaration"], + import: ["import_statement"], + export: ["export_statement"], + typeAnnotation: ["type_annotation"], + }, + }, + concepts: ["generics", "type guards", "decorators"], +}; + +describe("LanguageRegistry", () => { + it("registers and retrieves a config by id", () => { + const registry = new LanguageRegistry(); + registry.register(pythonConfig); + expect(registry.getById("python")).toBe(pythonConfig); + }); + + it("retrieves config by file extension", () => { + const registry = new LanguageRegistry(); + registry.register(pythonConfig); + expect(registry.getByExtension(".py")).toBe(pythonConfig); + expect(registry.getByExtension(".pyi")).toBe(pythonConfig); + }); + + it("returns null for unknown extension", () => { + const registry = new LanguageRegistry(); + registry.register(pythonConfig); + expect(registry.getByExtension(".rs")).toBeNull(); + }); + + it("returns all registered configs", () => { + const registry = new LanguageRegistry(); + registry.register(pythonConfig); + registry.register(tsConfig); + expect(registry.getAll()).toHaveLength(2); + }); + + it("later registration overrides same id", () => { + const registry = new LanguageRegistry(); + const updated = { ...pythonConfig, displayName: "Python 3" }; + registry.register(pythonConfig); + registry.register(updated); + expect(registry.getById("python")?.displayName).toBe("Python 3"); + }); + + it("throws on invalid config", () => { + const registry = new LanguageRegistry(); + expect(() => registry.register({ id: "bad" } as LanguageConfig)).toThrow(); + }); +}); +``` + +**Step 2: Run test to verify it fails** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run src/languages/__tests__/registry.test.ts` +Expected: FAIL — module `../registry.js` not found + +**Step 3: Write minimal implementation** + +```typescript +// understand-anything-plugin/packages/core/src/languages/registry.ts +import { LanguageConfigSchema } from "./types.js"; +import type { LanguageConfig } from "./types.js"; + +export class LanguageRegistry { + private configs = new Map(); + private extensionMap = new Map(); + + register(config: LanguageConfig): void { + const result = LanguageConfigSchema.safeParse(config); + if (!result.success) { + throw new Error(`Invalid LanguageConfig for "${config.id}": ${result.error.message}`); + } + this.configs.set(config.id, config); + for (const ext of config.extensions) { + this.extensionMap.set(ext, config.id); + } + } + + getById(id: string): LanguageConfig | null { + return this.configs.get(id) ?? null; + } + + getByExtension(ext: string): LanguageConfig | null { + const id = this.extensionMap.get(ext); + if (!id) return null; + return this.configs.get(id) ?? null; + } + + getAll(): LanguageConfig[] { + return [...this.configs.values()]; + } +} +``` + +**Step 4: Run test to verify it passes** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run src/languages/__tests__/registry.test.ts` +Expected: PASS + +**Step 5: Commit** + +```bash +git add understand-anything-plugin/packages/core/src/languages/ +git commit -m "feat: add LanguageRegistry with Zod validation" +``` + +--- + +### Task 3: Create all 12 language configs + +**Files:** +- Create: `understand-anything-plugin/packages/core/src/languages/configs/typescript.ts` +- Create: `understand-anything-plugin/packages/core/src/languages/configs/javascript.ts` +- Create: `understand-anything-plugin/packages/core/src/languages/configs/python.ts` +- Create: `understand-anything-plugin/packages/core/src/languages/configs/go.ts` +- Create: `understand-anything-plugin/packages/core/src/languages/configs/java.ts` +- Create: `understand-anything-plugin/packages/core/src/languages/configs/rust.ts` +- Create: `understand-anything-plugin/packages/core/src/languages/configs/cpp.ts` +- Create: `understand-anything-plugin/packages/core/src/languages/configs/csharp.ts` +- Create: `understand-anything-plugin/packages/core/src/languages/configs/ruby.ts` +- Create: `understand-anything-plugin/packages/core/src/languages/configs/php.ts` +- Create: `understand-anything-plugin/packages/core/src/languages/configs/swift.ts` +- Create: `understand-anything-plugin/packages/core/src/languages/configs/kotlin.ts` +- Create: `understand-anything-plugin/packages/core/src/languages/configs/index.ts` + +**Step 1: Write the failing test** + +Create: `understand-anything-plugin/packages/core/src/languages/__tests__/configs.test.ts` + +```typescript +import { describe, it, expect } from "vitest"; +import { LanguageConfigSchema } from "../types.js"; +import { builtinConfigs } from "../configs/index.js"; + +describe("builtin language configs", () => { + it("has 12 language configs", () => { + expect(builtinConfigs).toHaveLength(12); + }); + + it("all configs pass Zod validation", () => { + for (const config of builtinConfigs) { + const result = LanguageConfigSchema.safeParse(config); + expect(result.success, `${config.id} failed validation: ${result.error?.message}`).toBe(true); + } + }); + + it("all configs have unique ids", () => { + const ids = builtinConfigs.map((c) => c.id); + expect(new Set(ids).size).toBe(ids.length); + }); + + it("no duplicate extensions across configs", () => { + const allExts: string[] = []; + for (const config of builtinConfigs) { + allExts.push(...config.extensions); + } + expect(new Set(allExts).size).toBe(allExts.length); + }); + + it("all configs have non-empty function and class node types", () => { + for (const config of builtinConfigs) { + expect(config.treeSitter.nodeTypes.function.length, `${config.id} missing function types`).toBeGreaterThan(0); + expect(config.treeSitter.nodeTypes.class.length, `${config.id} missing class types`).toBeGreaterThanOrEqual(0); + } + }); + + it("all configs have at least one concept", () => { + for (const config of builtinConfigs) { + expect(config.concepts.length, `${config.id} has no concepts`).toBeGreaterThan(0); + } + }); +}); +``` + +**Step 2: Run test to verify it fails** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run src/languages/__tests__/configs.test.ts` +Expected: FAIL — module not found + +**Step 3: Write all config files** + +Each config file exports a `LanguageConfig`. Here are the key ones (the rest follow the same pattern): + +**typescript.ts:** +```typescript +import type { LanguageConfig } from "../types.js"; + +export const typescriptConfig: LanguageConfig = { + id: "typescript", + displayName: "TypeScript", + extensions: [".ts", ".tsx"], + treeSitter: { + grammarPackage: "tree-sitter-typescript", + wasmFile: "tree-sitter-typescript.wasm", + nodeTypes: { + function: ["function_declaration"], + class: ["class_declaration"], + import: ["import_statement"], + export: ["export_statement"], + typeAnnotation: ["type_annotation"], + }, + }, + concepts: [ + "generics", "type guards", "discriminated unions", "utility types", + "decorators", "enums", "interfaces", "type inference", + "mapped types", "conditional types", "template literal types", + ], + filePatterns: { config: "tsconfig.json", manifest: "package.json" }, +}; +``` + +**python.ts:** +```typescript +import type { LanguageConfig } from "../types.js"; + +export const pythonConfig: LanguageConfig = { + id: "python", + displayName: "Python", + extensions: [".py", ".pyi"], + treeSitter: { + grammarPackage: "tree-sitter-python", + wasmFile: "tree-sitter-python.wasm", + nodeTypes: { + function: ["function_definition"], + class: ["class_definition"], + import: ["import_statement", "import_from_statement"], + export: [], + typeAnnotation: ["type"], + }, + }, + concepts: [ + "decorators", "list comprehensions", "generators", "context managers", + "type hints", "dunder methods", "metaclasses", "dataclasses", + "async/await", "descriptors", + ], + filePatterns: { config: "pyproject.toml", manifest: "setup.py" }, +}; +``` + +**go.ts:** +```typescript +import type { LanguageConfig } from "../types.js"; + +export const goConfig: LanguageConfig = { + id: "go", + displayName: "Go", + extensions: [".go"], + treeSitter: { + grammarPackage: "tree-sitter-go", + wasmFile: "tree-sitter-go.wasm", + nodeTypes: { + function: ["function_declaration", "method_declaration"], + class: ["type_declaration"], + import: ["import_declaration"], + export: [], + typeAnnotation: [], + }, + }, + concepts: [ + "goroutines", "channels", "interfaces", "struct embedding", + "error handling patterns", "defer/panic/recover", "slices", + "pointers", "concurrency patterns", + ], + filePatterns: { config: "go.mod" }, +}; +``` + +**java.ts:** +```typescript +import type { LanguageConfig } from "../types.js"; + +export const javaConfig: LanguageConfig = { + id: "java", + displayName: "Java", + extensions: [".java"], + treeSitter: { + grammarPackage: "tree-sitter-java", + wasmFile: "tree-sitter-java.wasm", + nodeTypes: { + function: ["method_declaration", "constructor_declaration"], + class: ["class_declaration", "interface_declaration", "enum_declaration"], + import: ["import_declaration"], + export: [], + typeAnnotation: ["type_identifier"], + }, + }, + concepts: [ + "generics", "annotations", "interfaces", "abstract classes", + "streams API", "lambdas", "sealed classes", "records", + "dependency injection", "checked exceptions", + ], + filePatterns: { config: "pom.xml", manifest: "build.gradle" }, +}; +``` + +**rust.ts:** +```typescript +import type { LanguageConfig } from "../types.js"; + +export const rustConfig: LanguageConfig = { + id: "rust", + displayName: "Rust", + extensions: [".rs"], + treeSitter: { + grammarPackage: "tree-sitter-rust", + wasmFile: "tree-sitter-rust.wasm", + nodeTypes: { + function: ["function_item"], + class: ["struct_item", "enum_item", "impl_item", "trait_item"], + import: ["use_declaration"], + export: [], + typeAnnotation: ["type_identifier"], + }, + }, + concepts: [ + "ownership", "borrowing", "lifetimes", "traits", "pattern matching", + "enums with data", "error handling (Result/Option)", "macros", + "async/await", "unsafe blocks", "generics", "closures", + ], + filePatterns: { config: "Cargo.toml" }, +}; +``` + +**cpp.ts:** +```typescript +import type { LanguageConfig } from "../types.js"; + +export const cppConfig: LanguageConfig = { + id: "cpp", + displayName: "C/C++", + extensions: [".cpp", ".cc", ".cxx", ".c", ".h", ".hpp", ".hxx"], + treeSitter: { + grammarPackage: "tree-sitter-cpp", + wasmFile: "tree-sitter-cpp.wasm", + nodeTypes: { + function: ["function_definition"], + class: ["class_specifier", "struct_specifier"], + import: ["preproc_include"], + export: [], + typeAnnotation: [], + }, + }, + concepts: [ + "templates", "RAII", "smart pointers", "move semantics", + "operator overloading", "virtual functions", "namespaces", + "constexpr", "lambda expressions", "STL containers", + ], + filePatterns: { config: "CMakeLists.txt", manifest: "Makefile" }, +}; +``` + +**csharp.ts:** +```typescript +import type { LanguageConfig } from "../types.js"; + +export const csharpConfig: LanguageConfig = { + id: "csharp", + displayName: "C#", + extensions: [".cs"], + treeSitter: { + grammarPackage: "tree-sitter-c-sharp", + wasmFile: "tree-sitter-c_sharp.wasm", + nodeTypes: { + function: ["method_declaration", "constructor_declaration"], + class: ["class_declaration", "interface_declaration", "struct_declaration", "enum_declaration", "record_declaration"], + import: ["using_directive"], + export: [], + typeAnnotation: ["type_identifier"], + }, + }, + concepts: [ + "LINQ", "async/await", "generics", "properties", + "delegates and events", "attributes", "nullable reference types", + "pattern matching", "records", "dependency injection", + ], + filePatterns: { config: "*.csproj" }, +}; +``` + +**ruby.ts:** +```typescript +import type { LanguageConfig } from "../types.js"; + +export const rubyConfig: LanguageConfig = { + id: "ruby", + displayName: "Ruby", + extensions: [".rb", ".rake"], + treeSitter: { + grammarPackage: "tree-sitter-ruby", + wasmFile: "tree-sitter-ruby.wasm", + nodeTypes: { + function: ["method"], + class: ["class", "module"], + import: ["call"], + export: [], + typeAnnotation: [], + }, + }, + concepts: [ + "blocks and procs", "mixins", "metaprogramming", "duck typing", + "DSLs", "monkey patching", "gems", "symbols", + "method_missing", "open classes", + ], + filePatterns: { config: "Gemfile" }, +}; +``` + +**php.ts:** +```typescript +import type { LanguageConfig } from "../types.js"; + +export const phpConfig: LanguageConfig = { + id: "php", + displayName: "PHP", + extensions: [".php"], + treeSitter: { + grammarPackage: "tree-sitter-php", + wasmFile: "tree-sitter-php.wasm", + nodeTypes: { + function: ["function_definition", "method_declaration"], + class: ["class_declaration", "interface_declaration", "trait_declaration"], + import: ["namespace_use_declaration"], + export: [], + typeAnnotation: ["type_list", "named_type"], + }, + }, + concepts: [ + "namespaces", "traits", "type declarations", "attributes", + "enums", "fibers", "closures", "magic methods", + "dependency injection", "middleware", + ], + filePatterns: { config: "composer.json" }, +}; +``` + +**swift.ts:** +```typescript +import type { LanguageConfig } from "../types.js"; + +export const swiftConfig: LanguageConfig = { + id: "swift", + displayName: "Swift", + extensions: [".swift"], + treeSitter: { + grammarPackage: "tree-sitter-swift", + wasmFile: "tree-sitter-swift.wasm", + nodeTypes: { + function: ["function_declaration", "init_declaration"], + class: ["class_declaration", "struct_declaration", "protocol_declaration", "enum_declaration"], + import: ["import_declaration"], + export: [], + typeAnnotation: ["type_annotation"], + }, + }, + concepts: [ + "optionals", "protocols", "extensions", "generics", + "closures", "property wrappers", "result builders", + "actors", "structured concurrency", "value types vs reference types", + ], + filePatterns: { config: "Package.swift" }, +}; +``` + +**kotlin.ts:** +```typescript +import type { LanguageConfig } from "../types.js"; + +export const kotlinConfig: LanguageConfig = { + id: "kotlin", + displayName: "Kotlin", + extensions: [".kt", ".kts"], + treeSitter: { + grammarPackage: "tree-sitter-kotlin", + wasmFile: "tree-sitter-kotlin.wasm", + nodeTypes: { + function: ["function_declaration"], + class: ["class_declaration", "object_declaration", "interface_declaration"], + import: ["import_header"], + export: [], + typeAnnotation: ["type_identifier"], + }, + }, + concepts: [ + "coroutines", "data classes", "sealed classes", "extension functions", + "null safety", "delegation", "DSL builders", "inline functions", + "companion objects", "flow", + ], + filePatterns: { config: "build.gradle.kts" }, +}; +``` + +**javascript.ts:** +```typescript +import type { LanguageConfig } from "../types.js"; + +export const javascriptConfig: LanguageConfig = { + id: "javascript", + displayName: "JavaScript", + extensions: [".js", ".mjs", ".cjs", ".jsx"], + treeSitter: { + grammarPackage: "tree-sitter-javascript", + wasmFile: "tree-sitter-javascript.wasm", + nodeTypes: { + function: ["function_declaration"], + class: ["class_declaration"], + import: ["import_statement"], + export: ["export_statement"], + typeAnnotation: [], + }, + }, + concepts: [ + "closures", "prototypes", "promises", "async/await", + "event loop", "destructuring", "spread operator", + "proxies", "generators", "modules (ESM/CJS)", + ], + filePatterns: { config: "package.json" }, +}; +``` + +**configs/index.ts:** +```typescript +import { typescriptConfig } from "./typescript.js"; +import { javascriptConfig } from "./javascript.js"; +import { pythonConfig } from "./python.js"; +import { goConfig } from "./go.js"; +import { javaConfig } from "./java.js"; +import { rustConfig } from "./rust.js"; +import { cppConfig } from "./cpp.js"; +import { csharpConfig } from "./csharp.js"; +import { rubyConfig } from "./ruby.js"; +import { phpConfig } from "./php.js"; +import { swiftConfig } from "./swift.js"; +import { kotlinConfig } from "./kotlin.js"; +import type { LanguageConfig } from "../types.js"; + +export const builtinConfigs: LanguageConfig[] = [ + typescriptConfig, + javascriptConfig, + pythonConfig, + goConfig, + javaConfig, + rustConfig, + cppConfig, + csharpConfig, + rubyConfig, + phpConfig, + swiftConfig, + kotlinConfig, +]; +``` + +**Step 4: Run test to verify it passes** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run src/languages/__tests__/configs.test.ts` +Expected: PASS + +**Step 5: Commit** + +```bash +git add understand-anything-plugin/packages/core/src/languages/configs/ +git commit -m "feat: add 12 builtin language configs" +``` + +--- + +### Task 4: Create languages/index.ts barrel and export from core + +**Files:** +- Create: `understand-anything-plugin/packages/core/src/languages/index.ts` +- Modify: `understand-anything-plugin/packages/core/src/index.ts` + +**Step 1: Create barrel export** + +```typescript +// understand-anything-plugin/packages/core/src/languages/index.ts +export { LanguageRegistry } from "./registry.js"; +export { LanguageConfigSchema } from "./types.js"; +export type { LanguageConfig, TreeSitterConfig } from "./types.js"; +export { builtinConfigs } from "./configs/index.js"; +``` + +**Step 2: Add export to core index.ts** + +Add to `understand-anything-plugin/packages/core/src/index.ts`: + +```typescript +// Languages +export { LanguageRegistry, builtinConfigs, LanguageConfigSchema } from "./languages/index.js"; +export type { LanguageConfig, TreeSitterConfig } from "./languages/index.js"; +``` + +**Step 3: Build and verify** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core build` +Expected: Build succeeds with no errors + +**Step 4: Commit** + +```bash +git add understand-anything-plugin/packages/core/src/languages/index.ts understand-anything-plugin/packages/core/src/index.ts +git commit -m "feat: export language types and registry from core" +``` + +--- + +### Task 5: Install tree-sitter WASM grammar packages + +**Files:** +- Modify: `understand-anything-plugin/packages/core/package.json` + +**Step 1: Install new grammar packages** + +Run: +```bash +cd understand-anything-plugin && pnpm --filter @understand-anything/core add \ + tree-sitter-python \ + tree-sitter-go \ + tree-sitter-java \ + tree-sitter-rust \ + tree-sitter-cpp \ + tree-sitter-c-sharp \ + tree-sitter-ruby \ + tree-sitter-php \ + tree-sitter-swift \ + tree-sitter-kotlin +``` + +Note: Some grammar packages may not ship `.wasm` files. For those, we need to check availability and potentially build from source or use the `tree-sitter` CLI to generate WASM. Verify each package after install: + +```bash +cd understand-anything-plugin && for lang in python go java rust cpp c-sharp ruby php swift kotlin; do + echo "=== tree-sitter-$lang ===" + ls node_modules/tree-sitter-$lang/*.wasm 2>/dev/null || echo "NO WASM FOUND" +done +``` + +For packages without pre-built WASM, use `tree-sitter build --wasm` to compile them, or find alternative npm packages that ship WASM builds. Document which packages needed manual WASM generation. + +**Step 2: Verify build still passes** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core build` +Expected: PASS + +**Step 3: Commit** + +```bash +git add understand-anything-plugin/packages/core/package.json understand-anything-plugin/pnpm-lock.yaml +git commit -m "feat: add tree-sitter grammar packages for 10 new languages" +``` + +--- + +### Task 6: Build GenericTreeSitterPlugin + +**Files:** +- Create: `understand-anything-plugin/packages/core/src/plugins/generic-tree-sitter-plugin.ts` + +**Step 1: Write the failing test** + +Create: `understand-anything-plugin/packages/core/src/plugins/generic-tree-sitter-plugin.test.ts` + +```typescript +import { describe, it, expect, beforeAll } from "vitest"; +import { GenericTreeSitterPlugin } from "./generic-tree-sitter-plugin.js"; +import { LanguageRegistry } from "../languages/registry.js"; +import { typescriptConfig } from "../languages/configs/typescript.js"; +import { javascriptConfig } from "../languages/configs/javascript.js"; +import { pythonConfig } from "../languages/configs/python.js"; + +describe("GenericTreeSitterPlugin", () => { + let plugin: GenericTreeSitterPlugin; + + beforeAll(async () => { + const registry = new LanguageRegistry(); + registry.register(typescriptConfig); + registry.register(javascriptConfig); + registry.register(pythonConfig); + plugin = new GenericTreeSitterPlugin(registry); + await plugin.init(); + }); + + describe("TypeScript (migration parity)", () => { + it("extracts function declarations", () => { + const code = ` +function greet(name: string): string { + return "Hello " + name; +} +`; + const result = plugin.analyzeFile("test.ts", code); + expect(result.functions).toHaveLength(1); + expect(result.functions[0].name).toBe("greet"); + }); + + it("extracts class declarations", () => { + const code = ` +class UserService { + getName(): string { return "test"; } +} +`; + const result = plugin.analyzeFile("test.ts", code); + expect(result.classes).toHaveLength(1); + expect(result.classes[0].name).toBe("UserService"); + }); + + it("extracts imports", () => { + const code = `import { readFile } from "fs";`; + const result = plugin.analyzeFile("test.ts", code); + expect(result.imports).toHaveLength(1); + expect(result.imports[0].source).toBe("fs"); + }); + + it("extracts exports", () => { + const code = `export function hello() {}`; + const result = plugin.analyzeFile("test.ts", code); + expect(result.exports.length).toBeGreaterThanOrEqual(1); + }); + + it("extracts arrow functions", () => { + const code = `const add = (a: number, b: number): number => a + b;`; + const result = plugin.analyzeFile("test.ts", code); + expect(result.functions).toHaveLength(1); + expect(result.functions[0].name).toBe("add"); + }); + }); + + describe("Python", () => { + it("extracts function definitions", () => { + const code = ` +def greet(name): + return f"Hello {name}" + +def add(a, b): + return a + b +`; + const result = plugin.analyzeFile("test.py", code); + expect(result.functions).toHaveLength(2); + expect(result.functions[0].name).toBe("greet"); + expect(result.functions[1].name).toBe("add"); + }); + + it("extracts class definitions", () => { + const code = ` +class UserService: + def get_name(self): + return "test" +`; + const result = plugin.analyzeFile("test.py", code); + expect(result.classes).toHaveLength(1); + expect(result.classes[0].name).toBe("UserService"); + }); + + it("extracts import statements", () => { + const code = ` +import os +from pathlib import Path +from typing import Optional +`; + const result = plugin.analyzeFile("test.py", code); + expect(result.imports).toHaveLength(3); + }); + }); + + it("returns null for unsupported file extension", () => { + expect(plugin.canAnalyze("test.unknown")).toBe(false); + }); + + it("reports all registered languages", () => { + const langs = plugin.supportedLanguages(); + expect(langs).toContain("typescript"); + expect(langs).toContain("python"); + }); +}); +``` + +**Step 2: Run test to verify it fails** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run src/plugins/generic-tree-sitter-plugin.test.ts` +Expected: FAIL — module not found + +**Step 3: Write implementation** + +Create `understand-anything-plugin/packages/core/src/plugins/generic-tree-sitter-plugin.ts`: + +This file implements a `GenericTreeSitterPlugin` that: +- Takes a `LanguageRegistry` in the constructor +- In `init()`, lazily loads WASM grammars per language using `require.resolve(config.treeSitter.grammarPackage + '/' + config.treeSitter.wasmFile)` +- In `analyzeFile()`, determines language from extension via registry, then walks the AST using `config.treeSitter.nodeTypes` to extract functions/classes/imports/exports +- Reuses the same helper patterns from the old `TreeSitterPlugin` (traverse, getStringValue, extractParams) but driven by config instead of hardcoded node types +- Implements `resolveImports()` and `extractCallGraph()` with the same logic as before + +Key implementation notes: +- The `extractNodes()` method walks the AST and matches nodes against `nodeTypes.function`, `nodeTypes.class`, etc. +- For TS/JS, also handle `lexical_declaration`/`variable_declaration` with arrow function values (existing behavior) +- For import extraction, use the same `getStringValue()` approach but match against language-specific import node types +- For export extraction, same pattern matching against export node types +- Grammar loading: try `require.resolve()` first; if WASM not found, log warning and skip that language + +**Step 4: Run test to verify it passes** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run src/plugins/generic-tree-sitter-plugin.test.ts` +Expected: PASS + +**Step 5: Run old TreeSitterPlugin tests with new plugin to verify migration parity** + +Ensure the existing `tree-sitter-plugin.test.ts` test cases also pass with `GenericTreeSitterPlugin` + TS/JS configs. + +**Step 6: Commit** + +```bash +git add understand-anything-plugin/packages/core/src/plugins/generic-tree-sitter-plugin.ts +git add understand-anything-plugin/packages/core/src/plugins/generic-tree-sitter-plugin.test.ts +git commit -m "feat: add GenericTreeSitterPlugin driven by LanguageConfig" +``` + +--- + +### Task 7: Add per-language test fixtures for remaining languages + +**Files:** +- Modify: `understand-anything-plugin/packages/core/src/plugins/generic-tree-sitter-plugin.test.ts` + +**Step 1: Add test cases for Go, Java, Rust, C++, C#, Ruby, PHP, Swift, Kotlin** + +For each language, add a `describe` block with a small fixture testing function/class/import extraction. Example for Go: + +```typescript +describe("Go", () => { + it("extracts function declarations", () => { + const code = ` +package main + +func greet(name string) string { + return "Hello " + name +} +`; + const result = plugin.analyzeFile("test.go", code); + expect(result.functions).toHaveLength(1); + expect(result.functions[0].name).toBe("greet"); + }); + + it("extracts type declarations", () => { + const code = ` +package main + +type UserService struct { + Name string +} +`; + const result = plugin.analyzeFile("test.go", code); + expect(result.classes).toHaveLength(1); + }); + + it("extracts imports", () => { + const code = ` +package main + +import ( + "fmt" + "os" +) +`; + const result = plugin.analyzeFile("test.go", code); + expect(result.imports).toHaveLength(2); + }); +}); +``` + +Follow same pattern for each language with appropriate syntax. Each test uses ~10-20 lines of idiomatic code. + +Note: Some WASM grammars may not be available. For languages where the grammar fails to load, register them in the `beforeAll` with a try/catch and use `it.skipIf()` to conditionally skip tests. This prevents CI failures while still testing what's available. + +**Step 2: Run all tests** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run src/plugins/generic-tree-sitter-plugin.test.ts` +Expected: PASS for all languages with available grammars + +**Step 3: Commit** + +```bash +git add understand-anything-plugin/packages/core/src/plugins/generic-tree-sitter-plugin.test.ts +git commit -m "test: add per-language fixtures for GenericTreeSitterPlugin" +``` + +--- + +### Task 8: Replace TreeSitterPlugin with GenericTreeSitterPlugin + +**Files:** +- Modify: `understand-anything-plugin/packages/core/src/index.ts` +- Modify: `understand-anything-plugin/packages/core/src/plugins/registry.ts` +- Delete: `understand-anything-plugin/packages/core/src/plugins/tree-sitter-plugin.ts` (after confirming no other imports) + +**Step 1: Update core exports** + +In `understand-anything-plugin/packages/core/src/index.ts`: +- Replace `export { TreeSitterPlugin }` with `export { GenericTreeSitterPlugin }` +- Also export `GenericTreeSitterPlugin` as `TreeSitterPlugin` for backward compat if needed (check consumers) + +**Step 2: Update PluginRegistry extension map** + +In `understand-anything-plugin/packages/core/src/plugins/registry.ts`: +- The `EXTENSION_TO_LANGUAGE` map is already comprehensive (has py, go, rs, etc.) +- No changes needed here — the registry just dispatches to whatever plugin is registered + +**Step 3: Update all imports in skill source** + +Search for all imports of `TreeSitterPlugin` across the codebase: + +Run: `grep -r "TreeSitterPlugin" understand-anything-plugin/` + +Update each import to use `GenericTreeSitterPlugin`. The main consumers are: +- `understand-anything-plugin/packages/core/src/index.ts` +- Any skill source files that instantiate the plugin + +**Step 4: Delete old TreeSitterPlugin** + +Once all imports are updated and tests pass: + +Run: `rm understand-anything-plugin/packages/core/src/plugins/tree-sitter-plugin.ts` + +Keep the old test file temporarily — rename it to verify parity. + +**Step 5: Run full test suite** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test` +Expected: ALL PASS + +**Step 6: Commit** + +```bash +git add -A +git commit -m "refactor: replace TreeSitterPlugin with GenericTreeSitterPlugin" +``` + +--- + +### Task 9: Update language-lesson.ts to use LanguageRegistry + +**Files:** +- Modify: `understand-anything-plugin/packages/core/src/analyzer/language-lesson.ts` +- Modify: `understand-anything-plugin/packages/core/src/__tests__/language-lesson.test.ts` + +**Step 1: Update the test** + +Update `language-lesson.test.ts` to verify concepts come from the registry: + +```typescript +it("detects concepts from language config", () => { + const node = { + ...sampleNode, + summary: "Uses decorators and async/await with generators", + tags: ["decorators"], + }; + const concepts = detectLanguageConcepts(node, "python"); + expect(concepts).toContain("decorators"); + expect(concepts).toContain("async/await"); +}); +``` + +**Step 2: Run test to verify it fails (or passes with old behavior)** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run src/__tests__/language-lesson.test.ts` + +**Step 3: Update implementation** + +In `language-lesson.ts`: +- Import `LanguageRegistry` and `builtinConfigs` +- Create a module-level registry instance, pre-populated with builtinConfigs +- Replace `LANGUAGE_DISPLAY_NAMES` lookups with `registry.getById(lang)?.displayName` +- Replace hardcoded `CONCEPT_PATTERNS` with `registry.getById(lang)?.concepts` merged with generic patterns (async/await, error handling, etc. that apply to all languages) +- Keep the detection logic (search tags/summary for concept keywords) but source keywords from the config + +**Step 4: Run test to verify it passes** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run src/__tests__/language-lesson.test.ts` +Expected: PASS + +**Step 5: Run full test suite** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test` +Expected: ALL PASS + +**Step 6: Commit** + +```bash +git add understand-anything-plugin/packages/core/src/analyzer/language-lesson.ts +git add understand-anything-plugin/packages/core/src/__tests__/language-lesson.test.ts +git commit -m "refactor: source language concepts from LanguageRegistry" +``` + +--- + +### Task 10: Create language prompt snippet files + +**Files:** +- Create: `understand-anything-plugin/skills/understand/languages/typescript.md` +- Create: `understand-anything-plugin/skills/understand/languages/javascript.md` +- Create: `understand-anything-plugin/skills/understand/languages/python.md` +- Create: `understand-anything-plugin/skills/understand/languages/go.md` +- Create: `understand-anything-plugin/skills/understand/languages/java.md` +- Create: `understand-anything-plugin/skills/understand/languages/rust.md` +- Create: `understand-anything-plugin/skills/understand/languages/cpp.md` +- Create: `understand-anything-plugin/skills/understand/languages/csharp.md` +- Create: `understand-anything-plugin/skills/understand/languages/ruby.md` +- Create: `understand-anything-plugin/skills/understand/languages/php.md` +- Create: `understand-anything-plugin/skills/understand/languages/swift.md` +- Create: `understand-anything-plugin/skills/understand/languages/kotlin.md` + +**Step 1: Create all 12 language markdown files** + +Each file follows this structure: + +```markdown +# [Language Name] + +## Key Concepts +- [5-10 language-specific concepts with brief explanations] + +## Import Patterns +- [All common import syntax patterns for this language] + +## Notable File Patterns +- [Special files like __init__.py, go.mod, Cargo.toml, etc.] + +## Common Frameworks +- [Top 3-5 frameworks/libraries in this ecosystem] + +## Example Summary Style +> "[Example of how to summarize a function/class in this language's idiom]" +``` + +Each file should be 30-50 lines, with content specific to that language's ecosystem and idioms. The content should help the LLM produce better analysis by understanding language-specific patterns. + +**Step 2: Verify files are well-formed** + +Manually review each file for accuracy and completeness. + +**Step 3: Commit** + +```bash +git add understand-anything-plugin/skills/understand/languages/ +git commit -m "feat: add language-specific prompt snippet files for 12 languages" +``` + +--- + +### Task 11: Make base prompts language-neutral with injection points + +**Files:** +- Modify: `understand-anything-plugin/skills/understand/file-analyzer-prompt.md` +- Modify: `understand-anything-plugin/skills/understand/tour-builder-prompt.md` +- Modify: `understand-anything-plugin/skills/understand/project-scanner-prompt.md` + +**Step 1: Update file-analyzer-prompt.md** + +- Remove all TypeScript-specific examples (e.g., "TypeScript barrel file", type guard references) +- Replace TS-specific concept lists with generic placeholders +- Add injection point: + +```markdown +## Language-Specific Guidance + +{{LANGUAGE_CONTEXT}} +``` + +- Make the Phase 1 script detection language-aware (not just "Node.js recommended") + +**Step 2: Update tour-builder-prompt.md** + +- Remove TS-specific language lesson examples ("generics, discriminated unions, utility types") +- Replace with injection point for detected languages: + +```markdown +## Language-Specific Concepts + +{{LANGUAGE_CONTEXT}} +``` + +**Step 3: Update project-scanner-prompt.md** + +- Remove `tsconfig.json` hardcoded check +- Make framework detection generic (inject detected languages' framework lists) +- Add multi-language section: + +```markdown +## Detected Languages + +{{LANGUAGE_CONTEXT}} +``` + +**Step 4: Verify prompts are well-formed** + +Read each modified prompt to ensure it's coherent with injection points and no residual TS bias. + +**Step 5: Commit** + +```bash +git add understand-anything-plugin/skills/understand/file-analyzer-prompt.md +git add understand-anything-plugin/skills/understand/tour-builder-prompt.md +git add understand-anything-plugin/skills/understand/project-scanner-prompt.md +git commit -m "refactor: make agent prompts language-neutral with injection points" +``` + +--- + +### Task 12: Implement prompt injection logic in skill source + +**Files:** +- Modify: `understand-anything-plugin/skills/understand/SKILL.md` (the `/understand` skill definition) + +**Step 1: Update the skill orchestration** + +In the `/understand` skill (SKILL.md), update the agent dispatch logic: + +- **Phase 0 (Pre-flight):** After scanning files, detect languages present and load corresponding `languages/*.md` files +- **Phase 2 (File Analyzer dispatch):** For each file batch, inject the matching language's `.md` content into the file-analyzer prompt's `{{LANGUAGE_CONTEXT}}` placeholder +- **Phase 4 (Architecture Analyzer):** Inject all detected languages' concepts +- **Phase 5 (Tour Builder):** Inject all detected languages' `.md` content into the `{{LANGUAGE_CONTEXT}}` placeholder +- **Phase 1 (Project Scanner):** Inject all detected languages' `.md` content + +The injection logic: +1. Map file extensions to language IDs (reuse `LanguageRegistry.getByExtension()`) +2. Read the corresponding `languages/.md` file +3. Replace `{{LANGUAGE_CONTEXT}}` in the base prompt with the file contents + +For multi-language projects, concatenate all detected language files. + +**Step 2: Verify by reading modified SKILL.md** + +Ensure the orchestration flow includes language detection and prompt injection steps. + +**Step 3: Commit** + +```bash +git add understand-anything-plugin/skills/understand/SKILL.md +git commit -m "feat: add language detection and prompt injection to /understand skill" +``` + +--- + +### Task 13: Update old tree-sitter-plugin test to use GenericTreeSitterPlugin + +**Files:** +- Modify or Delete: `understand-anything-plugin/packages/core/src/plugins/tree-sitter-plugin.test.ts` + +**Step 1: Migrate or delete** + +If the old `tree-sitter-plugin.test.ts` still exists: +- Either update it to import `GenericTreeSitterPlugin` and instantiate with a `LanguageRegistry` containing TS/JS configs +- Or delete it if all its test cases are covered in `generic-tree-sitter-plugin.test.ts` + +Prefer deleting to avoid duplication. + +**Step 2: Run full test suite** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test` +Expected: ALL PASS + +**Step 3: Commit** + +```bash +git add -A +git commit -m "test: migrate old tree-sitter-plugin tests to generic plugin" +``` + +--- + +### Task 14: Build and lint verification + +**Step 1: Build core** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core build` +Expected: PASS + +**Step 2: Build skill package** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/skill build` +Expected: PASS + +**Step 3: Build dashboard** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/dashboard build` +Expected: PASS (dashboard doesn't import language modules directly) + +**Step 4: Run lint** + +Run: `cd understand-anything-plugin && pnpm lint` +Expected: PASS (or fix any lint issues) + +**Step 5: Run all tests** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test && pnpm --filter @understand-anything/skill test` +Expected: ALL PASS + +**Step 6: Commit any fixes** + +```bash +git add -A +git commit -m "fix: resolve build and lint issues from language-agnostic refactor" +``` + +--- + +### Task 15: Update CLAUDE.md and documentation + +**Files:** +- Modify: `CLAUDE.md` +- Modify: `README.md` (if it exists and mentions TS-only support) + +**Step 1: Update CLAUDE.md** + +Add to the Architecture section: +- Mention the `languages/` directories (both in core and skills) +- Document how to add a new language (create config + prompt snippet) +- List supported languages + +**Step 2: Commit** + +```bash +git add CLAUDE.md +git commit -m "docs: update CLAUDE.md with language-agnostic architecture" +``` diff --git a/docs/plans/2026-03-25-dashboard-robustness-impl.md b/docs/plans/2026-03-25-dashboard-robustness-impl.md new file mode 100644 index 0000000..0fe6d09 --- /dev/null +++ b/docs/plans/2026-03-25-dashboard-robustness-impl.md @@ -0,0 +1,1277 @@ +# Dashboard Robustness Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Make the dashboard resilient to imperfect LLM-generated knowledge graphs by auto-fixing recoverable issues, dropping broken items, and showing user-friendly amber warnings with copy-paste-friendly error reports. + +**Architecture:** Three-layer pipeline in `schema.ts`: sanitize (Tier 1 silent) → auto-fix (Tier 2 tracked) → per-item validate (Tier 3 drop) → fatal gate (Tier 4). New `WarningBanner` component in dashboard displays categorized issues with copy button. + +**Tech Stack:** Zod (validation), React + TailwindCSS (dashboard UI), Vitest (testing) + +--- + +### Task 1: Add GraphIssue type and sanitizeGraph (Tier 1) + +**Files:** +- Modify: `understand-anything-plugin/packages/core/src/schema.ts:95-99` +- Test: `understand-anything-plugin/packages/core/src/__tests__/schema.test.ts` + +**Step 1: Write the failing tests for sanitizeGraph** + +Add to the end of `schema.test.ts`, before the closing `});`: + +```typescript +describe("sanitizeGraph", () => { + it("converts null optional node fields to undefined", () => { + const graph = structuredClone(validGraph); + (graph.nodes[0] as any).filePath = null; + (graph.nodes[0] as any).lineRange = null; + (graph.nodes[0] as any).languageNotes = null; + + const result = sanitizeGraph(graph as any); + const node = (result as any).nodes[0]; + expect(node.filePath).toBeUndefined(); + expect(node.lineRange).toBeUndefined(); + expect(node.languageNotes).toBeUndefined(); + }); + + it("converts null optional edge fields to undefined", () => { + const graph = structuredClone(validGraph); + (graph.edges[0] as any).description = null; + + const result = sanitizeGraph(graph as any); + const edge = (result as any).edges[0]; + expect(edge.description).toBeUndefined(); + }); + + it("lowercases enum-like strings on nodes", () => { + const graph = structuredClone(validGraph); + (graph.nodes[0] as any).type = "FILE"; + (graph.nodes[0] as any).complexity = "Simple"; + + const result = sanitizeGraph(graph as any); + const node = (result as any).nodes[0]; + expect(node.type).toBe("file"); + expect(node.complexity).toBe("simple"); + }); + + it("lowercases enum-like strings on edges", () => { + const graph = structuredClone(validGraph); + (graph.edges[0] as any).type = "IMPORTS"; + (graph.edges[0] as any).direction = "Forward"; + + const result = sanitizeGraph(graph as any); + const edge = (result as any).edges[0]; + expect(edge.type).toBe("imports"); + expect(edge.direction).toBe("forward"); + }); + + it("converts null tour/layers to empty arrays", () => { + const graph = structuredClone(validGraph); + (graph as any).tour = null; + (graph as any).layers = null; + + const result = sanitizeGraph(graph as any); + expect((result as any).tour).toEqual([]); + expect((result as any).layers).toEqual([]); + }); + + it("converts null optional tour step fields to undefined", () => { + const graph = structuredClone(validGraph); + (graph.tour[0] as any).languageLesson = null; + + const result = sanitizeGraph(graph as any); + expect((result as any).tour[0].languageLesson).toBeUndefined(); + }); +}); +``` + +**Step 2: Run tests to verify they fail** + +Run: `pnpm --filter @understand-anything/core test` +Expected: FAIL — `sanitizeGraph` is not exported + +**Step 3: Add GraphIssue type and update ValidationResult** + +In `schema.ts`, replace the `ValidationResult` interface (lines 95-99) with: + +```typescript +export interface GraphIssue { + level: "auto-corrected" | "dropped" | "fatal"; + category: string; + message: string; + path?: string; +} + +export interface ValidationResult { + success: boolean; + data?: z.infer; + issues: GraphIssue[]; + fatal?: string; + /** @deprecated Use issues/fatal instead */ + errors?: string[]; +} +``` + +**Step 4: Implement sanitizeGraph** + +Add after the alias maps (after line 39), before `GraphNodeSchema`: + +```typescript +export function sanitizeGraph(data: Record): Record { + const result = { ...data }; + + // Null → empty array for top-level collections + if (data.tour === null || data.tour === undefined) result.tour = []; + if (data.layers === null || data.layers === undefined) result.layers = []; + + // Sanitize nodes + if (Array.isArray(data.nodes)) { + result.nodes = (data.nodes as Record[]).map((node) => { + if (typeof node !== "object" || node === null) return node; + const n = { ...node }; + // Null → undefined for optional fields + if (n.filePath === null) delete n.filePath; + if (n.lineRange === null) delete n.lineRange; + if (n.languageNotes === null) delete n.languageNotes; + // Lowercase enum-like strings + if (typeof n.type === "string") n.type = n.type.toLowerCase(); + if (typeof n.complexity === "string") n.complexity = n.complexity.toLowerCase(); + return n; + }); + } + + // Sanitize edges + if (Array.isArray(data.edges)) { + result.edges = (data.edges as Record[]).map((edge) => { + if (typeof edge !== "object" || edge === null) return edge; + const e = { ...edge }; + if (e.description === null) delete e.description; + if (typeof e.type === "string") e.type = e.type.toLowerCase(); + if (typeof e.direction === "string") e.direction = e.direction.toLowerCase(); + return e; + }); + } + + // Sanitize tour steps + if (Array.isArray(result.tour)) { + result.tour = (result.tour as Record[]).map((step) => { + if (typeof step !== "object" || step === null) return step; + const s = { ...step }; + if (s.languageLesson === null) delete s.languageLesson; + return s; + }); + } + + return result; +} +``` + +**Step 5: Update imports in test file** + +Update the import line in `schema.test.ts`: + +```typescript +import { + validateGraph, + normalizeGraph, + sanitizeGraph, + NODE_TYPE_ALIASES, + EDGE_TYPE_ALIASES, +} from "../schema.js"; +``` + +**Step 6: Run tests to verify they pass** + +Run: `pnpm --filter @understand-anything/core test` +Expected: All sanitizeGraph tests PASS. Existing tests still PASS. + +**Step 7: Commit** + +```bash +git add understand-anything-plugin/packages/core/src/schema.ts understand-anything-plugin/packages/core/src/__tests__/schema.test.ts +git commit -m "feat(core): add GraphIssue type and sanitizeGraph (Tier 1 silent fixes)" +``` + +--- + +### Task 2: Add auto-fix maps and autoFixGraph (Tier 2) + +**Files:** +- Modify: `understand-anything-plugin/packages/core/src/schema.ts` +- Test: `understand-anything-plugin/packages/core/src/__tests__/schema.test.ts` + +**Step 1: Write the failing tests** + +Add to `schema.test.ts`, before the closing `});`: + +```typescript +describe("autoFixGraph", () => { + it("defaults missing complexity to moderate with issue", () => { + const graph = structuredClone(validGraph); + delete (graph.nodes[0] as any).complexity; + + const { data, issues } = autoFixGraph(graph as any); + expect((data as any).nodes[0].complexity).toBe("moderate"); + expect(issues).toContainEqual( + expect.objectContaining({ level: "auto-corrected", category: "missing-field", path: "nodes[0].complexity" }) + ); + }); + + it("maps complexity aliases with issue", () => { + const graph = structuredClone(validGraph); + (graph.nodes[0] as any).complexity = "low"; + + const { data, issues } = autoFixGraph(graph as any); + expect((data as any).nodes[0].complexity).toBe("simple"); + expect(issues.length).toBe(1); + expect(issues[0].level).toBe("auto-corrected"); + }); + + it("maps all complexity aliases correctly", () => { + const mapping: Record = { + low: "simple", easy: "simple", + medium: "moderate", intermediate: "moderate", + high: "complex", hard: "complex", difficult: "complex", + }; + for (const [alias, expected] of Object.entries(mapping)) { + const graph = structuredClone(validGraph); + (graph.nodes[0] as any).complexity = alias; + const { data } = autoFixGraph(graph as any); + expect((data as any).nodes[0].complexity).toBe(expected); + } + }); + + it("defaults missing tags to empty array with issue", () => { + const graph = structuredClone(validGraph); + delete (graph.nodes[0] as any).tags; + + const { data, issues } = autoFixGraph(graph as any); + expect((data as any).nodes[0].tags).toEqual([]); + expect(issues).toContainEqual( + expect.objectContaining({ level: "auto-corrected", category: "missing-field", path: "nodes[0].tags" }) + ); + }); + + it("defaults missing summary to node name with issue", () => { + const graph = structuredClone(validGraph); + delete (graph.nodes[0] as any).summary; + + const { data, issues } = autoFixGraph(graph as any); + expect((data as any).nodes[0].summary).toBe("index.ts"); + expect(issues).toContainEqual( + expect.objectContaining({ level: "auto-corrected", category: "missing-field", path: "nodes[0].summary" }) + ); + }); + + it("defaults missing node type to file with issue", () => { + const graph = structuredClone(validGraph); + delete (graph.nodes[0] as any).type; + + const { data, issues } = autoFixGraph(graph as any); + expect((data as any).nodes[0].type).toBe("file"); + expect(issues).toContainEqual( + expect.objectContaining({ level: "auto-corrected", category: "missing-field", path: "nodes[0].type" }) + ); + }); + + it("defaults missing direction to forward with issue", () => { + const graph = structuredClone(validGraph); + delete (graph.edges[0] as any).direction; + + const { data, issues } = autoFixGraph(graph as any); + expect((data as any).edges[0].direction).toBe("forward"); + expect(issues).toContainEqual( + expect.objectContaining({ level: "auto-corrected", category: "missing-field", path: "edges[0].direction" }) + ); + }); + + it("maps direction aliases with issue", () => { + const mapping: Record = { + to: "forward", outbound: "forward", + from: "backward", inbound: "backward", + both: "bidirectional", mutual: "bidirectional", + }; + for (const [alias, expected] of Object.entries(mapping)) { + const graph = structuredClone(validGraph); + (graph.edges[0] as any).direction = alias; + const { data } = autoFixGraph(graph as any); + expect((data as any).edges[0].direction).toBe(expected); + } + }); + + it("defaults missing weight to 0.5 with issue", () => { + const graph = structuredClone(validGraph); + delete (graph.edges[0] as any).weight; + + const { data, issues } = autoFixGraph(graph as any); + expect((data as any).edges[0].weight).toBe(0.5); + expect(issues).toContainEqual( + expect.objectContaining({ level: "auto-corrected", category: "missing-field", path: "edges[0].weight" }) + ); + }); + + it("coerces string weight to number with issue", () => { + const graph = structuredClone(validGraph); + (graph.edges[0] as any).weight = "0.8"; + + const { data, issues } = autoFixGraph(graph as any); + expect((data as any).edges[0].weight).toBe(0.8); + expect(issues).toContainEqual( + expect.objectContaining({ level: "auto-corrected", category: "type-coercion", path: "edges[0].weight" }) + ); + }); + + it("clamps out-of-range weight with issue", () => { + const graph = structuredClone(validGraph); + (graph.edges[0] as any).weight = 1.5; + + const { data, issues } = autoFixGraph(graph as any); + expect((data as any).edges[0].weight).toBe(1); + expect(issues).toContainEqual( + expect.objectContaining({ level: "auto-corrected", category: "out-of-range", path: "edges[0].weight" }) + ); + }); + + it("defaults missing edge type to depends_on with issue", () => { + const graph = structuredClone(validGraph); + delete (graph.edges[0] as any).type; + + const { data, issues } = autoFixGraph(graph as any); + expect((data as any).edges[0].type).toBe("depends_on"); + expect(issues).toContainEqual( + expect.objectContaining({ level: "auto-corrected", category: "missing-field", path: "edges[0].type" }) + ); + }); + + it("returns no issues for a valid graph", () => { + const { issues } = autoFixGraph(validGraph as any); + expect(issues).toEqual([]); + }); +}); +``` + +**Step 2: Run tests to verify they fail** + +Run: `pnpm --filter @understand-anything/core test` +Expected: FAIL — `autoFixGraph` is not exported + +**Step 3: Implement alias maps and autoFixGraph** + +Add to `schema.ts` after the existing `EDGE_TYPE_ALIASES` map (after line 39): + +```typescript +export const COMPLEXITY_ALIASES: Record = { + low: "simple", + easy: "simple", + medium: "moderate", + intermediate: "moderate", + high: "complex", + hard: "complex", + difficult: "complex", +}; + +export const DIRECTION_ALIASES: Record = { + to: "forward", + outbound: "forward", + from: "backward", + inbound: "backward", + both: "bidirectional", + mutual: "bidirectional", +}; +``` + +Add `autoFixGraph` function after `sanitizeGraph`: + +```typescript +export function autoFixGraph(data: Record): { + data: Record; + issues: GraphIssue[]; +} { + const issues: GraphIssue[] = []; + const result = { ...data }; + + if (Array.isArray(data.nodes)) { + result.nodes = (data.nodes as Record[]).map((node, i) => { + if (typeof node !== "object" || node === null) return node; + const n = { ...node }; + const name = (n.name as string) || (n.id as string) || `index ${i}`; + + // Missing or empty type + if (!n.type || typeof n.type !== "string") { + n.type = "file"; + issues.push({ + level: "auto-corrected", + category: "missing-field", + message: `nodes[${i}] ("${name}"): missing "type" — defaulted to "file"`, + path: `nodes[${i}].type`, + }); + } + + // Missing or empty complexity + if (!n.complexity || n.complexity === "") { + n.complexity = "moderate"; + issues.push({ + level: "auto-corrected", + category: "missing-field", + message: `nodes[${i}] ("${name}"): missing "complexity" — defaulted to "moderate"`, + path: `nodes[${i}].complexity`, + }); + } else if (typeof n.complexity === "string" && n.complexity in COMPLEXITY_ALIASES) { + const original = n.complexity; + n.complexity = COMPLEXITY_ALIASES[n.complexity]; + issues.push({ + level: "auto-corrected", + category: "alias", + message: `nodes[${i}] ("${name}"): complexity "${original}" — mapped to "${n.complexity}"`, + path: `nodes[${i}].complexity`, + }); + } + + // Missing tags + if (!Array.isArray(n.tags)) { + n.tags = []; + issues.push({ + level: "auto-corrected", + category: "missing-field", + message: `nodes[${i}] ("${name}"): missing "tags" — defaulted to []`, + path: `nodes[${i}].tags`, + }); + } + + // Missing summary + if (!n.summary || typeof n.summary !== "string") { + n.summary = (n.name as string) || "No summary"; + issues.push({ + level: "auto-corrected", + category: "missing-field", + message: `nodes[${i}] ("${name}"): missing "summary" — defaulted to name`, + path: `nodes[${i}].summary`, + }); + } + + return n; + }); + } + + if (Array.isArray(data.edges)) { + result.edges = (data.edges as Record[]).map((edge, i) => { + if (typeof edge !== "object" || edge === null) return edge; + const e = { ...edge }; + + // Missing type + if (!e.type || typeof e.type !== "string") { + e.type = "depends_on"; + issues.push({ + level: "auto-corrected", + category: "missing-field", + message: `edges[${i}]: missing "type" — defaulted to "depends_on"`, + path: `edges[${i}].type`, + }); + } + + // Missing direction + if (!e.direction || typeof e.direction !== "string") { + e.direction = "forward"; + issues.push({ + level: "auto-corrected", + category: "missing-field", + message: `edges[${i}]: missing "direction" — defaulted to "forward"`, + path: `edges[${i}].direction`, + }); + } else if (e.direction in DIRECTION_ALIASES) { + const original = e.direction; + e.direction = DIRECTION_ALIASES[e.direction as string]; + issues.push({ + level: "auto-corrected", + category: "alias", + message: `edges[${i}]: direction "${original}" — mapped to "${e.direction}"`, + path: `edges[${i}].direction`, + }); + } + + // Missing weight + if (e.weight === undefined || e.weight === null) { + e.weight = 0.5; + issues.push({ + level: "auto-corrected", + category: "missing-field", + message: `edges[${i}]: missing "weight" — defaulted to 0.5`, + path: `edges[${i}].weight`, + }); + } else if (typeof e.weight === "string") { + const parsed = parseFloat(e.weight as string); + if (!isNaN(parsed)) { + const original = e.weight; + e.weight = parsed; + issues.push({ + level: "auto-corrected", + category: "type-coercion", + message: `edges[${i}]: weight was string "${original}" — coerced to number`, + path: `edges[${i}].weight`, + }); + } + } + + // Clamp weight to [0, 1] + if (typeof e.weight === "number" && (e.weight < 0 || e.weight > 1)) { + const original = e.weight; + e.weight = Math.max(0, Math.min(1, e.weight)); + issues.push({ + level: "auto-corrected", + category: "out-of-range", + message: `edges[${i}]: weight ${original} clamped to ${e.weight}`, + path: `edges[${i}].weight`, + }); + } + + return e; + }); + } + + return { data: result, issues }; +} +``` + +**Step 4: Update imports in test file** + +```typescript +import { + validateGraph, + normalizeGraph, + sanitizeGraph, + autoFixGraph, + NODE_TYPE_ALIASES, + EDGE_TYPE_ALIASES, +} from "../schema.js"; +``` + +**Step 5: Run tests to verify they pass** + +Run: `pnpm --filter @understand-anything/core test` +Expected: All new autoFixGraph tests PASS. Existing tests still PASS. + +**Step 6: Commit** + +```bash +git add understand-anything-plugin/packages/core/src/schema.ts understand-anything-plugin/packages/core/src/__tests__/schema.test.ts +git commit -m "feat(core): add autoFixGraph with complexity/direction aliases and default values (Tier 2)" +``` + +--- + +### Task 3: Rewrite validateGraph to be permissive (Tier 3 + 4) + +**Files:** +- Modify: `understand-anything-plugin/packages/core/src/schema.ts:138-151` +- Test: `understand-anything-plugin/packages/core/src/__tests__/schema.test.ts` + +**Step 1: Write the failing tests for permissive validation** + +Add to `schema.test.ts`: + +```typescript +describe("permissive validation", () => { + it("drops nodes missing id with dropped issue", () => { + const graph = structuredClone(validGraph); + delete (graph.nodes[0] as any).id; + // Add a second valid node so graph isn't fatal + graph.nodes.push({ + id: "node-2", type: "file", name: "other.ts", + summary: "Other file", tags: ["util"], complexity: "simple", + }); + + const result = validateGraph(graph); + expect(result.success).toBe(true); + expect(result.data!.nodes.length).toBe(1); + expect(result.data!.nodes[0].id).toBe("node-2"); + expect(result.issues).toContainEqual( + expect.objectContaining({ level: "dropped", category: "invalid-node" }) + ); + }); + + it("drops edges referencing non-existent nodes with dropped issue", () => { + const graph = structuredClone(validGraph); + graph.edges[0].target = "non-existent-node"; + + const result = validateGraph(graph); + expect(result.success).toBe(true); + expect(result.data!.edges.length).toBe(0); + expect(result.issues).toContainEqual( + expect.objectContaining({ level: "dropped", category: "invalid-reference" }) + ); + }); + + it("returns fatal when 0 valid nodes remain", () => { + const graph = structuredClone(validGraph); + delete (graph.nodes[0] as any).id; + + const result = validateGraph(graph); + expect(result.success).toBe(false); + expect(result.fatal).toContain("No valid nodes"); + }); + + it("returns fatal when project metadata is missing", () => { + const graph = structuredClone(validGraph); + delete (graph as any).project; + + const result = validateGraph(graph); + expect(result.success).toBe(false); + expect(result.fatal).toContain("project metadata"); + }); + + it("returns fatal when input is not an object", () => { + const result = validateGraph("not an object"); + expect(result.success).toBe(false); + expect(result.fatal).toContain("Invalid input"); + }); + + it("loads graph with mixed good and bad nodes", () => { + const graph = structuredClone(validGraph); + // Add a good node + graph.nodes.push({ + id: "node-2", type: "function", name: "doThing", + summary: "Does a thing", tags: ["util"], complexity: "moderate", + }); + // Add a bad node (missing id AND name — unrecoverable) + (graph.nodes as any[]).push({ type: "file", summary: "broken" }); + + const result = validateGraph(graph); + expect(result.success).toBe(true); + expect(result.data!.nodes.length).toBe(2); + expect(result.issues.some((i) => i.level === "dropped")).toBe(true); + }); + + it("filters dangling nodeIds from layers", () => { + const graph = structuredClone(validGraph); + graph.layers[0].nodeIds.push("non-existent-node"); + + const result = validateGraph(graph); + expect(result.success).toBe(true); + expect(result.data!.layers[0].nodeIds).toEqual(["node-1"]); + }); + + it("filters dangling nodeIds from tour steps", () => { + const graph = structuredClone(validGraph); + graph.tour[0].nodeIds.push("non-existent-node"); + + const result = validateGraph(graph); + expect(result.success).toBe(true); + expect(result.data!.tour[0].nodeIds).toEqual(["node-1"]); + }); + + it("returns empty issues array for a perfect graph", () => { + const result = validateGraph(validGraph); + expect(result.success).toBe(true); + expect(result.issues).toEqual([]); + }); + + it("auto-corrects and loads graph that would have failed strict validation", () => { + // Graph with many Tier 2 issues: missing complexity, weight as string, null filePath + const messy = { + version: "1.0.0", + project: validGraph.project, + nodes: [{ + id: "n1", type: "FILE", name: "app.ts", + filePath: null, summary: "App entry", + tags: null, complexity: "HIGH", + }], + edges: [{ + source: "n1", target: "n1", type: "CALLS", + direction: "TO", weight: "0.9", + }], + layers: [{ id: "l1", name: "Core", description: "Core", nodeIds: ["n1"] }], + tour: [], + }; + + const result = validateGraph(messy); + expect(result.success).toBe(true); + expect(result.data!.nodes[0].complexity).toBe("complex"); + expect(result.data!.nodes[0].tags).toEqual([]); + expect(result.data!.edges[0].weight).toBe(0.9); + expect(result.data!.edges[0].direction).toBe("forward"); + expect(result.issues.length).toBeGreaterThan(0); + expect(result.issues.every((i) => i.level === "auto-corrected")).toBe(true); + }); +}); +``` + +**Step 2: Run tests to verify they fail** + +Run: `pnpm --filter @understand-anything/core test` +Expected: FAIL — `validateGraph` doesn't return `issues` or `fatal` + +**Step 3: Rewrite validateGraph** + +Replace the existing `validateGraph` function in `schema.ts` (lines 138-151) with: + +```typescript +export function validateGraph(data: unknown): ValidationResult { + // Tier 4: Fatal — not even an object + if (typeof data !== "object" || data === null) { + return { success: false, issues: [], fatal: "Invalid input: not an object" }; + } + + const raw = data as Record; + + // Tier 1: Sanitize + const sanitized = sanitizeGraph(raw); + + // Existing: Normalize type aliases + const normalized = normalizeGraph(sanitized) as Record; + + // Tier 2: Auto-fix defaults and coercion + const { data: fixed, issues } = autoFixGraph( + normalized as Record, + ); + + // Tier 4: Fatal — missing project metadata + const projectResult = ProjectMetaSchema.safeParse(fixed.project); + if (!projectResult.success) { + return { + success: false, + issues, + fatal: "Missing or invalid project metadata", + }; + } + + // Tier 3: Validate nodes individually, drop broken + const validNodes: z.infer[] = []; + if (Array.isArray(fixed.nodes)) { + for (let i = 0; i < fixed.nodes.length; i++) { + const node = fixed.nodes[i] as Record; + const result = GraphNodeSchema.safeParse(node); + if (result.success) { + validNodes.push(result.data); + } else { + const name = node?.name || node?.id || `index ${i}`; + issues.push({ + level: "dropped", + category: "invalid-node", + message: `nodes[${i}] ("${name}"): ${result.error.issues[0]?.message ?? "validation failed"} — removed`, + path: `nodes[${i}]`, + }); + } + } + } + + // Tier 4: Fatal — no valid nodes + if (validNodes.length === 0) { + return { + success: false, + issues, + fatal: "No valid nodes found in knowledge graph", + }; + } + + // Tier 3: Validate edges + referential integrity + const nodeIds = new Set(validNodes.map((n) => n.id)); + const validEdges: z.infer[] = []; + if (Array.isArray(fixed.edges)) { + for (let i = 0; i < fixed.edges.length; i++) { + const edge = fixed.edges[i] as Record; + const result = GraphEdgeSchema.safeParse(edge); + if (!result.success) { + issues.push({ + level: "dropped", + category: "invalid-edge", + message: `edges[${i}]: ${result.error.issues[0]?.message ?? "validation failed"} — removed`, + path: `edges[${i}]`, + }); + continue; + } + if (!nodeIds.has(result.data.source)) { + issues.push({ + level: "dropped", + category: "invalid-reference", + message: `edges[${i}]: source "${result.data.source}" does not exist in nodes — removed`, + path: `edges[${i}].source`, + }); + continue; + } + if (!nodeIds.has(result.data.target)) { + issues.push({ + level: "dropped", + category: "invalid-reference", + message: `edges[${i}]: target "${result.data.target}" does not exist in nodes — removed`, + path: `edges[${i}].target`, + }); + continue; + } + validEdges.push(result.data); + } + } + + // Validate layers (drop broken, filter dangling nodeIds) + const validLayers: z.infer[] = []; + if (Array.isArray(fixed.layers)) { + for (let i = 0; i < (fixed.layers as unknown[]).length; i++) { + const result = LayerSchema.safeParse((fixed.layers as unknown[])[i]); + if (result.success) { + validLayers.push({ + ...result.data, + nodeIds: result.data.nodeIds.filter((id) => nodeIds.has(id)), + }); + } else { + issues.push({ + level: "dropped", + category: "invalid-layer", + message: `layers[${i}]: ${result.error.issues[0]?.message ?? "validation failed"} — removed`, + path: `layers[${i}]`, + }); + } + } + } + + // Validate tour steps (drop broken, filter dangling nodeIds) + const validTour: z.infer[] = []; + if (Array.isArray(fixed.tour)) { + for (let i = 0; i < (fixed.tour as unknown[]).length; i++) { + const result = TourStepSchema.safeParse((fixed.tour as unknown[])[i]); + if (result.success) { + validTour.push({ + ...result.data, + nodeIds: result.data.nodeIds.filter((id) => nodeIds.has(id)), + }); + } else { + issues.push({ + level: "dropped", + category: "invalid-tour-step", + message: `tour[${i}]: ${result.error.issues[0]?.message ?? "validation failed"} — removed`, + path: `tour[${i}]`, + }); + } + } + } + + const graph = { + version: typeof fixed.version === "string" ? fixed.version : "1.0.0", + project: projectResult.data, + nodes: validNodes, + edges: validEdges, + layers: validLayers, + tour: validTour, + }; + + return { success: true, data: graph, issues }; +} +``` + +**Step 4: Run tests to verify new tests pass** + +Run: `pnpm --filter @understand-anything/core test` +Expected: New permissive tests PASS. Some old tests may now fail (expected — handled in Task 4). + +**Step 5: Commit** + +```bash +git add understand-anything-plugin/packages/core/src/schema.ts understand-anything-plugin/packages/core/src/__tests__/schema.test.ts +git commit -m "feat(core): rewrite validateGraph for permissive per-item validation (Tier 3+4)" +``` + +--- + +### Task 4: Update existing tests for new permissive behavior + +**Files:** +- Modify: `understand-anything-plugin/packages/core/src/__tests__/schema.test.ts` + +The new permissive validation changes behavior for several existing tests. Here's what changes: + +| Test | Old behavior | New behavior | +|------|-------------|-------------| +| "validates a correct graph" | `success: true, errors: undefined` | `success: true, issues: []` | +| "rejects missing required fields" | `success: false, errors` | `success: false, fatal` (missing project) | +| "rejects node with invalid type" | `success: false, errors` | `success: false, fatal` (0 valid nodes after drop) | +| "rejects edge with invalid EdgeType" | `success: false, errors` | `success: true` (edge dropped, node valid) | +| "rejects weight >1" | `success: false, errors` | `success: true` (weight clamped) | +| "rejects weight <0" | `success: false, errors` | `success: true` (weight clamped) | +| "rejects 'tests' edge type" | `success: false` | `success: true` (edge dropped) | +| "rejects truly invalid edge types" | `success: false` | `success: true` (edge dropped) | + +**Step 1: Update the affected tests** + +Replace the following tests in the `"schema validation"` describe block: + +```typescript +it("validates a correct knowledge graph", () => { + const result = validateGraph(validGraph); + expect(result.success).toBe(true); + expect(result.data).toBeDefined(); + expect(result.data!.version).toBe("1.0.0"); + expect(result.issues).toEqual([]); +}); + +it("rejects graph with missing required fields", () => { + const incomplete = { version: "1.0.0" }; + const result = validateGraph(incomplete); + expect(result.success).toBe(false); + expect(result.fatal).toBeDefined(); +}); + +it("rejects node with invalid type — drops node, fatal if none remain", () => { + const graph = structuredClone(validGraph); + (graph.nodes[0] as any).type = "invalid_type"; + + const result = validateGraph(graph); + expect(result.success).toBe(false); + expect(result.fatal).toContain("No valid nodes"); + expect(result.issues).toContainEqual( + expect.objectContaining({ level: "dropped", category: "invalid-node" }) + ); +}); + +it("drops edge with invalid EdgeType but loads graph", () => { + const graph = structuredClone(validGraph); + (graph.edges[0] as any).type = "not_a_real_edge_type"; + + const result = validateGraph(graph); + expect(result.success).toBe(true); + expect(result.data!.edges.length).toBe(0); + expect(result.issues).toContainEqual( + expect.objectContaining({ level: "dropped", category: "invalid-edge" }) + ); +}); + +it("auto-corrects weight >1 by clamping", () => { + const graph = structuredClone(validGraph); + graph.edges[0].weight = 1.5; + + const result = validateGraph(graph); + expect(result.success).toBe(true); + expect(result.issues).toContainEqual( + expect.objectContaining({ level: "auto-corrected", category: "out-of-range" }) + ); +}); + +it("auto-corrects weight <0 by clamping", () => { + const graph = structuredClone(validGraph); + graph.edges[0].weight = -0.1; + + const result = validateGraph(graph); + expect(result.success).toBe(true); + expect(result.issues).toContainEqual( + expect.objectContaining({ level: "auto-corrected", category: "out-of-range" }) + ); +}); +``` + +Also update the "tests" edge type test and "truly invalid edge types" test: + +```typescript +it('drops "tests" edge type — direction-inverting alias is unsafe', () => { + const graph = structuredClone(validGraph); + (graph.edges[0] as any).type = "tests"; + + const result = validateGraph(graph); + expect(result.success).toBe(true); + expect(result.data!.edges.length).toBe(0); + expect(result.issues).toContainEqual( + expect.objectContaining({ level: "dropped" }) + ); +}); + +it("drops truly invalid edge types after normalization", () => { + const graph = structuredClone(validGraph); + (graph.edges[0] as any).type = "totally_bogus"; + + const result = validateGraph(graph); + expect(result.success).toBe(true); + expect(result.data!.edges.length).toBe(0); + expect(result.issues).toContainEqual( + expect.objectContaining({ level: "dropped" }) + ); +}); +``` + +**Step 2: Run all tests** + +Run: `pnpm --filter @understand-anything/core test` +Expected: ALL tests PASS + +**Step 3: Commit** + +```bash +git add understand-anything-plugin/packages/core/src/__tests__/schema.test.ts +git commit -m "test(core): update existing tests for permissive validation behavior" +``` + +--- + +### Task 5: Create WarningBanner dashboard component + +**Files:** +- Create: `understand-anything-plugin/packages/dashboard/src/components/WarningBanner.tsx` + +**Step 1: Build core package for dashboard import** + +Run: `pnpm --filter @understand-anything/core build` +Expected: Build succeeds with new exports + +**Step 2: Create WarningBanner component** + +Create `understand-anything-plugin/packages/dashboard/src/components/WarningBanner.tsx`: + +```tsx +import { useState } from "react"; +import type { GraphIssue } from "@understand-anything/core/schema"; + +interface WarningBannerProps { + issues: GraphIssue[]; +} + +export default function WarningBanner({ issues }: WarningBannerProps) { + const [expanded, setExpanded] = useState(false); + const [copied, setCopied] = useState(false); + + const autoCorrected = issues.filter((i) => i.level === "auto-corrected"); + const dropped = issues.filter((i) => i.level === "dropped"); + + const summaryParts: string[] = []; + if (autoCorrected.length > 0) { + summaryParts.push( + `${autoCorrected.length} auto-correction${autoCorrected.length > 1 ? "s" : ""}`, + ); + } + if (dropped.length > 0) { + summaryParts.push( + `${dropped.length} dropped item${dropped.length > 1 ? "s" : ""}`, + ); + } + + const copyText = [ + "The following issues were found in your knowledge-graph.json.", + "These are LLM generation errors — not a system bug.", + "You can ask your agent to fix these specific issues in the knowledge-graph.json file:", + "", + ...issues.map( + (i) => + `[${i.level === "auto-corrected" ? "Auto-corrected" : "Dropped"}] ${i.message}`, + ), + ].join("\n"); + + const handleCopy = async () => { + await navigator.clipboard.writeText(copyText); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +

+
+ + +
+ + {expanded && ( +
+ {autoCorrected.length > 0 && ( + <> +
+ Auto-corrected ({autoCorrected.length}) +
+ {autoCorrected.map((issue, i) => ( +
+ {issue.message} +
+ ))} + + )} + {dropped.length > 0 && ( + <> +
+ Dropped ({dropped.length}) +
+ {dropped.map((issue, i) => ( +
+ {issue.message} +
+ ))} + + )} +

+ These are LLM generation issues, not system bugs. Copy the issues + above and ask your agent to fix them in the knowledge-graph.json, or + re-run{" "} + /understand for a fresh + generation. +

+
+ )} +
+ ); +} +``` + +**Step 3: Verify dashboard builds** + +Run: `pnpm --filter @understand-anything/dashboard build` +Expected: Build succeeds (component not yet wired, but should compile) + +**Step 4: Commit** + +```bash +git add understand-anything-plugin/packages/dashboard/src/components/WarningBanner.tsx +git commit -m "feat(dashboard): add WarningBanner component for graph validation issues" +``` + +--- + +### Task 6: Wire WarningBanner into App.tsx + +**Files:** +- Modify: `understand-anything-plugin/packages/dashboard/src/App.tsx` + +**Step 1: Update App.tsx** + +Add import at top of file (after other component imports): + +```typescript +import WarningBanner from "./components/WarningBanner"; +import type { GraphIssue } from "@understand-anything/core/schema"; +``` + +Add state for issues (after `loadError` state, line 26): + +```typescript +const [graphIssues, setGraphIssues] = useState([]); +``` + +Replace the graph loading `useEffect` (lines 119-136) with: + +```typescript +useEffect(() => { + fetch("/knowledge-graph.json") + .then((res) => res.json()) + .then((data: unknown) => { + const result = validateGraph(data); + if (result.success && result.data) { + setGraph(result.data); + setGraphIssues(result.issues); + if (result.issues.length > 0) { + const autoCorrected = result.issues.filter((i) => i.level === "auto-corrected"); + const dropped = result.issues.filter((i) => i.level === "dropped"); + if (autoCorrected.length > 0) console.warn(`[understand-anything] Auto-corrected ${autoCorrected.length} graph issues`); + if (dropped.length > 0) console.error(`[understand-anything] Dropped ${dropped.length} broken graph items`); + } + } else if (result.fatal) { + console.error("Knowledge graph fatal error:", result.fatal); + setLoadError(result.fatal); + } else { + setLoadError("Unknown validation error"); + } + }) + .catch((err) => { + console.error("Failed to load knowledge graph:", err); + setLoadError( + `Failed to load knowledge graph: ${err instanceof Error ? err.message : String(err)}`, + ); + }); +}, [setGraph]); +``` + +Replace the error banner section (lines 213-218) with: + +```tsx +{/* Warning banner for graph issues */} +{graphIssues.length > 0 && !loadError && ( + +)} + +{/* Fatal error banner */} +{loadError && ( +
+ {loadError} +
+)} +``` + +**Step 2: Build and verify** + +Run: `pnpm --filter @understand-anything/core build && pnpm --filter @understand-anything/dashboard build` +Expected: Both builds succeed + +**Step 3: Commit** + +```bash +git add understand-anything-plugin/packages/dashboard/src/App.tsx +git commit -m "feat(dashboard): wire WarningBanner to display graph validation issues" +``` + +--- + +### Task 7: Final verification + +**Step 1: Run all core tests** + +Run: `pnpm --filter @understand-anything/core test` +Expected: ALL tests pass + +**Step 2: Build full pipeline** + +Run: `pnpm --filter @understand-anything/core build && pnpm --filter @understand-anything/dashboard build` +Expected: Both builds succeed with no errors + +**Step 3: Lint** + +Run: `pnpm lint` +Expected: No lint errors in changed files + +**Step 4: Final commit (if any lint fixes needed)** + +```bash +git add -A +git commit -m "chore: lint fixes for dashboard robustness feature" +``` diff --git a/docs/plans/2026-03-25-dashboard-robustness-plan.md b/docs/plans/2026-03-25-dashboard-robustness-plan.md new file mode 100644 index 0000000..16d5c74 --- /dev/null +++ b/docs/plans/2026-03-25-dashboard-robustness-plan.md @@ -0,0 +1,149 @@ +# Design: Dashboard Robustness — Permissive Graph Loading + +## Problem + +When the LLM agent produces a knowledge-graph.json that deviates from the strict Zod schema, the dashboard shows a blank screen with cryptic Zod error paths. Users don't know whether it's a system bug or an agent generation issue, and their only recourse is a full re-run of `/understand`. + +## Goals + +1. **Maximize what the user can see** — load valid nodes/edges even if some are broken +2. **Clearly communicate generation issues** — amber warnings (not red errors) with copy-paste-friendly messages +3. **Empower targeted fixes** — users can copy the issue report and ask their agent to fix specific problems instead of a full re-run + +## Design + +### Three-Layer Robustness Pipeline + +``` +Raw JSON → Sanitize (Tier 1) → Normalize + Auto-fix (Tier 2) → Validate per-item (Tier 3) → Fatal check (Tier 4) → Dashboard +``` + +### Tier 1: Sanitize Silently + +Common LLM quirks that are pure noise — fix without reporting. + +| Issue | Fix | +|-------|-----| +| `null` on optional fields (`filePath`, `lineRange`, `description`, `languageNotes`) | Convert to `undefined` | +| Mixed-case enum strings (`"Forward"`, `"SIMPLE"`) | Lowercase before matching | + +### Tier 2: Auto-fix With Info Notice + +Recoverable issues — apply sensible defaults, track as `auto-corrected` issues. + +| Issue | Default | Notes | +|-------|---------|-------| +| Missing `complexity` | `"moderate"` | Most common LLM omission | +| Missing `tags` | `[]` | Empty is valid | +| Missing `weight` | `0.5` | Middle of 0–1 range | +| `weight` as string | Coerce to number | e.g., `"0.8"` → `0.8` | +| Missing `direction` | `"forward"` | Safe default | +| Missing `summary` | Use node `name` | Better than empty | +| `tour: null` / `layers: null` | `[]` | Null vs empty array | +| Complexity aliases | `low/easy→simple`, `medium/intermediate→moderate`, `high/hard→complex` | | +| Direction aliases | `to/outbound→forward`, `from/inbound→backward`, `both→bidirectional` | | +| Existing node/edge type aliases | Already handled by `normalizeGraph` | No change needed | +| Missing node `type` | `"file"` | Safe fallback | +| Missing edge `type` | `"depends_on"` | Generic fallback | + +### Tier 3: Drop With Warning + +Can't safely guess — remove the item, track as `dropped` issue. + +| Issue | Action | +|-------|--------| +| Edge references non-existent node ID | Drop edge | +| Node missing `id` | Drop node | +| Node missing `name` | Drop node | +| Edge missing `source` or `target` | Drop edge | +| Unrecognizable `type` value (not in canonical or alias list) | Drop item | +| `weight` not coercible to number | Drop edge | + +### Tier 4: Fatal + +Graph is unsalvageable — show red error banner. + +| Condition | Message | +|-----------|---------| +| 0 valid nodes after filtering | "No valid nodes found in knowledge graph" | +| Missing `project` metadata entirely | "Missing project metadata" | +| Input is not an object / not valid JSON | "Invalid input format" | + +### Return Type + +```typescript +interface GraphIssue { + level: 'auto-corrected' | 'dropped' | 'fatal'; + category: string; // e.g., "missing-field", "invalid-reference", "type-coercion" + message: string; // human-readable, copy-paste friendly + path?: string; // e.g., "nodes[3].complexity" +} + +interface ValidationResult { + success: boolean; + data?: KnowledgeGraph; + issues: GraphIssue[]; + fatal?: string; +} +``` + +### Dashboard UI: WarningBanner Component + +**New component** in `packages/dashboard/src/components/WarningBanner.tsx`. + +**Visual design:** +- **Amber/gold theme** — `bg-amber-900/20`, `border-amber-700`, `text-amber-200` +- Matches dashboard's gold accent aesthetic; signals "generation quality issue" not "system crash" +- **Collapsed by default** — summary line: "Knowledge graph loaded with 5 auto-corrections and 2 dropped items" +- **Expandable** — click to reveal categorized issue list +- **Copy button** — one-click copies the full issue report as a pre-formatted message +- **Actionable footer** — tells users to copy issues and ask their agent to fix them + +**Copy-paste output format:** +``` +The following issues were found in your knowledge-graph.json. +These are LLM generation errors — not a system bug. +You can ask your agent to fix these specific issues in the knowledge-graph.json file: + +[Auto-corrected] nodes[3] ("AuthService"): missing "complexity" — defaulted to "moderate" +[Auto-corrected] nodes[7] ("utils.ts"): missing "tags" — defaulted to [] +[Auto-corrected] edges[12]: weight was string "0.8" — coerced to number +[Dropped] edges[5]: target "file:src/nonexistent.ts" does not exist in nodes +[Dropped] nodes[14]: missing required "id" field — cannot recover +``` + +**Fatal errors** stay red (`bg-red-900/30`) with message: "Knowledge graph is unsalvageable: [reason]. Please re-run `/understand` to generate a new one." + +**Existing red error banner** for network/JSON-parse errors stays as-is (those ARE system/infra issues). + +### App.tsx Changes + +- On `result.success === true` with `result.issues.length > 0`: show `WarningBanner` with issues, load graph normally +- On `result.fatal`: show existing red banner with fatal message +- `console.warn` for auto-corrected items, `console.error` for dropped items + +### Test Coverage + +All in `packages/core/src/__tests__/schema.test.ts`: + +- **Tier 1:** `null` optional fields silently become `undefined` +- **Tier 2:** Missing `complexity`/`tags`/`weight`/`direction`/`summary` get defaults; issues tracked +- **Tier 2:** String `weight` coerced; complexity/direction aliases mapped +- **Tier 3:** Dangling edge references dropped; nodes missing `id` dropped; issues recorded +- **Tier 4:** Empty graph after filtering → fatal; missing `project` → fatal +- **Integration:** Graph with mixed good/bad nodes → loads with correct node count + correct issues list + +### Files Changed + +| File | Change | +|------|--------| +| `packages/core/src/schema.ts` | Sanitize, expanded normalize, permissive validate, new types | +| `packages/dashboard/src/components/WarningBanner.tsx` | New component | +| `packages/dashboard/src/App.tsx` | Wire issues to WarningBanner | +| `packages/core/src/__tests__/schema.test.ts` | Tests for all tiers | + +### Files NOT Changed + +- Agent prompts (can be tightened later as a separate effort) +- GraphView / store logic (they already handle valid `KnowledgeGraph` objects) +- Existing node/edge type alias maps (preserved, extended around) diff --git a/docs/plans/2026-03-26-theme-system-design.md b/docs/plans/2026-03-26-theme-system-design.md new file mode 100644 index 0000000..6f94154 --- /dev/null +++ b/docs/plans/2026-03-26-theme-system-design.md @@ -0,0 +1,415 @@ +# Theme System Design + +## Overview + +Add a curated theme preset system with accent color customization to the dashboard. Users select from 5 hand-designed theme presets and optionally swap the accent color within each preset from a set of 8-10 tested swatches. + +### Goals +- Support 5 theme presets: Dark Gold (current), Dark Ocean, Dark Forest, Dark Rose, Light Minimal +- Allow accent color customization within each preset (curated swatches only, no free picker) +- Persist theme preference in both `localStorage` (personal) and `meta.json` (project-level) +- Maintain visual coherence — no user-breakable color combinations +- Zero-reload theme switching via CSS variable injection at runtime + +### Non-Goals +- Free color picker (risk of ugly/unreadable combos) +- Per-component color overrides +- Multiple simultaneous themes + +--- + +## 1. Theme Presets & Color System + +### 1.1 Preset Definitions + +Each preset is a complete mapping of CSS variable names to values. The 5 presets: + +| Token | Dark Gold | Dark Ocean | Dark Forest | Dark Rose | Light Minimal | +|-------|-----------|------------|-------------|-----------|---------------| +| `--color-root` | `#0a0a0a` | `#0a0e14` | `#0a100a` | `#100a0a` | `#f5f3f0` | +| `--color-surface` | `#111111` | `#111820` | `#111811` | `#181111` | `#eae7e3` | +| `--color-elevated` | `#1a1a1a` | `#1a222c` | `#1a241a` | `#221a1a` | `#ffffff` | +| `--color-panel` | `#141414` | `#141c24` | `#141c14` | `#1c1414` | `#f0ede9` | +| `--color-gold`* | `#d4a574` | `#5ba4cf` | `#5ea67a` | `#cf7a8a` | `#4a6fa5` | +| `--color-gold-dim`* | `#c9a96e` | `#4e93ba` | `#4e9468` | `#b96e7e` | `#3d5f8f` | +| `--color-gold-bright`* | `#e8c49a` | `#7abce0` | `#78c492` | `#e094a4` | `#6088bf` | +| `--color-text-primary` | `#f5f0eb` | `#e8edf2` | `#ebf0eb` | `#f2e8ea` | `#1a1a1a` | +| `--color-text-secondary` | `#a39787` | `#87939f` | `#87a38f` | `#9f8790` | `#6b6b6b` | +| `--color-text-muted` | `#6b5f53` | `#536b7a` | `#536b5a` | `#6b535a` | `#a0a0a0` | +| `--color-border-subtle` | `rgba(212,165,116,0.12)` | `rgba(91,164,207,0.12)` | `rgba(94,166,122,0.12)` | `rgba(207,122,138,0.12)` | `rgba(74,111,165,0.10)` | +| `--color-border-medium` | `rgba(212,165,116,0.25)` | `rgba(91,164,207,0.25)` | `rgba(94,166,122,0.25)` | `rgba(207,122,138,0.25)` | `rgba(74,111,165,0.18)` | + +*\* The CSS variable names stay as `--color-gold`, `--color-gold-dim`, `--color-gold-bright` even for non-gold themes. They represent "the accent color" generically. Renaming them to `--color-accent` is a refactor we can do, but not required — the variable name is an implementation detail invisible to users.* + +**Decision: Rename `--color-gold*` to `--color-accent*`** to avoid confusion. This is a find-and-replace across the codebase with no behavioral change. + +### 1.2 Glass Effects + +Glass effects derive from base colors and need per-preset values: + +| Token | Dark themes | Light Minimal | +|-------|-------------|---------------| +| `--glass-bg` | `rgba(20,20,20,0.8)` | `rgba(255,255,255,0.8)` | +| `--glass-bg-heavy` | `rgba(20,20,20,0.95)` | `rgba(255,255,255,0.95)` | +| `--glass-border` | `rgba(accent,0.1)` | `rgba(accent,0.08)` | +| `--glass-border-heavy` | `rgba(accent,0.15)` | `rgba(accent,0.12)` | + +The `.glass` and `.glass-heavy` CSS classes will reference these variables instead of hardcoded values. + +### 1.3 Scrollbar & Glow Colors + +These also derive from the accent color and need to become CSS variables: + +| Token | Purpose | +|-------|---------| +| `--scrollbar-thumb` | `rgba(accent, 0.2)` | +| `--scrollbar-thumb-hover` | `rgba(accent, 0.35)` | +| `--glow-color` | `rgba(accent, 0.4)` for node selection glow | +| `--glow-pulse` | `rgba(accent, 0.6)` for tour highlight pulse | + +### 1.4 Node-Type & Diff Colors + +These are **semantic** and stay fixed across all dark themes: + +| Variable | Value | Purpose | +|----------|-------|---------| +| `--color-node-file` | `#4a7c9b` | File nodes | +| `--color-node-function` | `#5a9e6f` | Function nodes | +| `--color-node-class` | `#8b6fb0` | Class nodes | +| `--color-node-module` | `#c9a06c` | Module nodes | +| `--color-node-concept` | `#b07a8a` | Concept nodes | +| `--color-diff-changed` | `#e05252` | Changed nodes | +| `--color-diff-affected` | `#d4a030` | Affected nodes | + +For **Light Minimal only**, these are slightly desaturated/darkened to maintain readability on light backgrounds: + +| Variable | Light Minimal Value | +|----------|-------------------| +| `--color-node-file` | `#3a6a87` | +| `--color-node-function` | `#488a5b` | +| `--color-node-class` | `#755d99` | +| `--color-node-module` | `#a88a56` | +| `--color-node-concept` | `#966674` | + +### 1.5 Accent Swatches + +Each preset offers 8 accent color options. The first is the "native" default for that preset. Each swatch provides 3 values (accent, accent-dim, accent-bright) plus auto-derived border and glass opacities. + +**Dark theme accent swatches** (shared across all 4 dark presets): + +| Name | Accent | Dim | Bright | +|------|--------|-----|--------| +| Gold | `#d4a574` | `#c9a96e` | `#e8c49a` | +| Ocean | `#5ba4cf` | `#4e93ba` | `#7abce0` | +| Emerald | `#5ea67a` | `#4e9468` | `#78c492` | +| Rose | `#cf7a8a` | `#b96e7e` | `#e094a4` | +| Purple | `#9b7abf` | `#876bb0` | `#b494d4` | +| Amber | `#c9963a` | `#b5862e` | `#ddb05c` | +| Teal | `#4aab9a` | `#3d9686` | `#68c4b4` | +| Silver | `#a0a8b0` | `#8e959c` | `#b8bfc6` | + +**Light Minimal accent swatches:** + +| Name | Accent | Dim | Bright | +|------|--------|-----|--------| +| Indigo | `#4a6fa5` | `#3d5f8f` | `#6088bf` | +| Ocean | `#3a8ab5` | `#2e7aa0` | `#55a0cc` | +| Emerald | `#3a8a5c` | `#2e7a4e` | `#55a878` | +| Rose | `#a5566a` | `#8f4a5c` | `#bf6e82` | +| Purple | `#6b5a9e` | `#5c4d8a` | `#8474b5` | +| Amber | `#9e7a30` | `#8a6a28` | `#b5923e` | +| Teal | `#2e8a7a` | `#267a6c` | `#45a595` | +| Slate | `#5a6570` | `#4e5860` | `#6e7a85` | + +### 1.6 Border & Glass Derivation + +When an accent swatch is selected, borders and glass effects are auto-derived: + +```typescript +function deriveFromAccent(accentHex: string, isDark: boolean) { + return { + borderSubtle: `rgba(${hexToRgb(accentHex)}, ${isDark ? 0.12 : 0.10})`, + borderMedium: `rgba(${hexToRgb(accentHex)}, ${isDark ? 0.25 : 0.18})`, + glassBorder: `rgba(${hexToRgb(accentHex)}, ${isDark ? 0.1 : 0.08})`, + glassBorderHeavy: `rgba(${hexToRgb(accentHex)}, ${isDark ? 0.15 : 0.12})`, + scrollbarThumb: `rgba(${hexToRgb(accentHex)}, 0.2)`, + scrollbarThumbHover: `rgba(${hexToRgb(accentHex)}, 0.35)`, + glowColor: `rgba(${hexToRgb(accentHex)}, 0.4)`, + glowPulse: `rgba(${hexToRgb(accentHex)}, 0.6)`, + }; +} +``` + +--- + +## 2. Architecture & Data Flow + +### 2.1 File Structure + +``` +packages/dashboard/src/ + themes/ + types.ts # ThemePreset, AccentSwatch, ThemeConfig types + presets.ts # 5 preset definitions + accent swatch arrays + theme-engine.ts # applyTheme(), deriveFromAccent(), hexToRgb() + ThemeContext.tsx # React context + provider + useTheme() hook + components/ + ThemePicker.tsx # Popover UI for preset + accent selection +``` + +### 2.2 Type Definitions + +```typescript +// themes/types.ts + +export type PresetId = 'dark-gold' | 'dark-ocean' | 'dark-forest' | 'dark-rose' | 'light-minimal'; + +export interface ThemePreset { + id: PresetId; + name: string; // Display name: "Dark Gold" + isDark: boolean; // true for dark themes, false for light + colors: Record; // CSS variable name -> value (without --) + accentSwatches: AccentSwatch[]; + defaultAccentId: string; // Which swatch is the native default +} + +export interface AccentSwatch { + id: string; // e.g. 'gold', 'ocean' + name: string; // Display name: "Gold" + accent: string; // Primary accent hex + accentDim: string; // Dimmed accent hex + accentBright: string; // Bright accent hex +} + +export interface ThemeConfig { + presetId: PresetId; + accentId: string; // Selected accent swatch ID +} +``` + +### 2.3 Theme Engine + +The theme engine is a pure function layer (no React dependency): + +```typescript +// themes/theme-engine.ts + +export function applyTheme(config: ThemeConfig): void { + const preset = getPreset(config.presetId); + const accent = getAccent(preset, config.accentId); + + // 1. Apply base preset colors + for (const [key, value] of Object.entries(preset.colors)) { + document.documentElement.style.setProperty(`--color-${key}`, value); + } + + // 2. Override accent colors from swatch + document.documentElement.style.setProperty('--color-accent', accent.accent); + document.documentElement.style.setProperty('--color-accent-dim', accent.accentDim); + document.documentElement.style.setProperty('--color-accent-bright', accent.accentBright); + + // 3. Apply derived values (borders, glass, scrollbar, glow) + const derived = deriveFromAccent(accent.accent, preset.isDark); + for (const [key, value] of Object.entries(derived)) { + document.documentElement.style.setProperty(`--${key}`, value); + } + + // 4. Set data-theme attribute for any CSS-only selectors needed + document.documentElement.setAttribute('data-theme', preset.isDark ? 'dark' : 'light'); +} +``` + +### 2.4 React Context + +```typescript +// themes/ThemeContext.tsx + +interface ThemeContextValue { + config: ThemeConfig; + preset: ThemePreset; + setPreset: (presetId: PresetId) => void; + setAccent: (accentId: string) => void; +} +``` + +The provider: +1. On mount: resolves theme from `localStorage` > `meta.json` field in loaded graph > default (`dark-gold`) +2. Calls `applyTheme()` on every config change +3. Persists to `localStorage` on every change +4. Does NOT write to `meta.json` from the dashboard (the dashboard is read-only for meta.json; meta.json is written by the CLI/plugin side) + +### 2.5 Integration with Zustand Store + +The theme system is **separate from the Zustand store** — it uses its own React context. Rationale: +- Theme state is orthogonal to graph/UI state +- Theme needs to apply before the graph even loads (avoid flash of wrong theme) +- Keeps the store focused on graph interaction + +The store does NOT gain any theme-related fields. + +--- + +## 3. UI Components + +### 3.1 Theme Picker Button (Header) + +A small palette icon button in the top header bar, positioned after existing controls (PersonaSelector, DiffToggle, etc.). + +- Click opens a popover/dropdown panel +- Popover has two sections: + - **Presets**: 5 cards/buttons showing preset name + small color preview circles + - **Accent Colors**: row of 8 color circles for the active preset +- Active preset and accent are highlighted with a ring/check +- Selecting a preset instantly applies it; selecting an accent instantly applies it +- Clicking outside or pressing Escape closes the popover + +### 3.2 Preset Preview + +Each preset card shows: +- Name (e.g., "Dark Gold") +- 3-4 small circles showing root, surface, and accent colors as a visual preview +- Check mark or ring on the active one + +### 3.3 Accent Swatch Row + +- 8 small filled circles in a horizontal row +- Tooltip or label on hover showing the accent name +- Active one has a ring/border indicator + +### 3.4 Transitions + +When switching themes: +- CSS variables update instantly (no transition needed for most properties) +- Optionally add a subtle `transition: background-color 0.2s, color 0.2s` on `html` for a smooth feel +- No page reload required + +--- + +## 4. Persistence & Resolution + +### 4.1 Storage Locations + +| Location | Format | Written by | Read by | +|----------|--------|-----------|---------| +| `localStorage` key: `ua-theme` | `JSON.stringify(ThemeConfig)` | Dashboard (on every change) | Dashboard (on mount) | +| `.understand-anything/meta.json` | `{ ..., theme?: ThemeConfig }` | CLI/plugin (during analysis or explicit set) | Dashboard (on mount, as fallback) | + +### 4.2 Resolution Order + +``` +1. localStorage('ua-theme') → user's personal preference (wins) +2. meta.json.theme → project-level default (fallback) +3. { presetId: 'dark-gold', accentId: 'gold' } → hard default +``` + +### 4.3 meta.json Schema Extension + +Extend `AnalysisMeta` in `packages/core/src/types.ts`: + +```typescript +export interface AnalysisMeta { + lastAnalyzedAt: string; + gitCommitHash: string; + version: string; + analyzedFiles: number; + theme?: ThemeConfig; // NEW — optional, project-level theme preference +} +``` + +### 4.4 Dashboard Reads meta.json Theme + +The dashboard currently loads `/knowledge-graph.json` on mount. It also needs to load `/meta.json` (or the theme field can be embedded in `knowledge-graph.json`). + +**Decision:** Load `/meta.json` separately — it's a small file and keeps concerns separated. The dashboard fetches `/meta.json` on mount, extracts the `theme` field if present, and uses it as fallback when `localStorage` has no theme. + +--- + +## 5. Hardcoded Color Consolidation + +### 5.1 Problem + +Many components use hardcoded RGBA values instead of CSS variables: +- `rgba(212,165,116,0.3)` scattered in GraphView, CustomNode, etc. +- `rgba(20,20,20,0.8)` in glass effects +- `rgba(224,82,82,0.25)` in diff overlays + +These won't respond to theme changes. + +### 5.2 Solution + +Before implementing theme switching, consolidate all hardcoded color references: + +1. **Audit**: grep for hardcoded hex/rgba values in component files +2. **Replace with CSS variables**: create new variables where needed (e.g., `--edge-color`, `--edge-color-dim`) +3. **Glass classes**: update `.glass` and `.glass-heavy` in `index.css` to use variables +4. **Scrollbar**: update scrollbar styles to use variables +5. **Glow effects**: update `.node-glow`, `.diff-changed-glow`, `.diff-affected-glow` to use variables + +Key hardcoded patterns to consolidate: + +| Hardcoded Value | Replace With | +|-----------------|-------------| +| `rgba(212,165,116,X)` | `var(--color-accent)` with opacity modifier or dedicated variable | +| `rgba(20,20,20,0.8)` | `var(--glass-bg)` | +| `rgba(20,20,20,0.95)` | `var(--glass-bg-heavy)` | +| `color="rgba(212,165,116,0.15)"` in React Flow | Variable reference | +| Amber colors in WarningBanner | Keep as-is (semantic warning color, theme-independent) | + +### 5.3 CSS Variable Rename + +Rename throughout codebase: +- `--color-gold` -> `--color-accent` +- `--color-gold-dim` -> `--color-accent-dim` +- `--color-gold-bright` -> `--color-accent-bright` +- All Tailwind class usages: `text-gold` -> `text-accent`, `bg-gold` -> `bg-accent`, etc. + +--- + +## 6. Light Theme Considerations + +The Light Minimal theme requires special attention: + +### 6.1 Inverted Contrast + +- Text is dark on light backgrounds (flipped from dark themes) +- Borders need lower opacity to avoid looking harsh +- Glass effects use white-based rgba instead of black-based + +### 6.2 Node Colors + +Slightly darker/desaturated variants for readability on light backgrounds (see Section 1.4). + +### 6.3 data-theme Attribute + +Set `data-theme="light"` on `` for any styles that can't be handled purely through CSS variables (e.g., third-party component overrides, box-shadow directions). + +### 6.4 React Flow + +React Flow's background, minimap, and edge colors all need to respect the theme. The existing `!important` override on `.react-flow__background` already uses `var(--color-root)`, which is good. MiniMap colors in GraphView.tsx are currently hardcoded and need to be updated. + +--- + +## 7. Summary of Changes by Package + +### packages/core +- Extend `AnalysisMeta` type with optional `theme?: ThemeConfig` +- Export `ThemeConfig` and `PresetId` types from `./types` subpath + +### packages/dashboard +- New `themes/` directory with types, presets, engine, and context +- New `ThemePicker` component in header +- Rename `--color-gold*` to `--color-accent*` across all files +- Consolidate hardcoded RGBA values into CSS variables +- Update `index.css`: glass classes, scrollbar, glow effects to use variables +- Update `App.tsx`: wrap with ThemeProvider, add ThemePicker to header, fetch meta.json +- Update components with hardcoded colors: GraphView, CustomNode, LayerLegend, etc. + +--- + +## 8. Out of Scope + +- Theme import/export +- Custom theme creation UI +- Per-node color customization +- Animated theme transitions beyond simple CSS transitions +- Syncing theme across browser tabs (nice-to-have for later) diff --git a/docs/plans/2026-03-26-theme-system-implementation.md b/docs/plans/2026-03-26-theme-system-implementation.md new file mode 100644 index 0000000..148c548 --- /dev/null +++ b/docs/plans/2026-03-26-theme-system-implementation.md @@ -0,0 +1,1166 @@ +# Theme System Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add curated theme presets with accent customization to the dashboard. + +**Architecture:** CSS variable injection at runtime via a pure theme engine, React context for state, localStorage + meta.json for persistence. Five presets (4 dark + 1 light) with 8 accent swatches each. + +**Tech Stack:** React, TypeScript, TailwindCSS v4, Zustand (untouched), CSS custom properties. + +**Design Doc:** `docs/plans/2026-03-26-theme-system-design.md` + +--- + +### Task 1: Rename `gold` to `accent` in CSS variables and Tailwind classes + +This is a mechanical find-and-replace with no behavioral change. Must be done first so all subsequent tasks use the new naming. + +**Files:** +- Modify: `understand-anything-plugin/packages/dashboard/src/index.css` +- Modify: `understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx` +- Modify: `understand-anything-plugin/packages/dashboard/src/components/NodeInfo.tsx` +- Modify: `understand-anything-plugin/packages/dashboard/src/components/LearnPanel.tsx` +- Modify: `understand-anything-plugin/packages/dashboard/src/components/ProjectOverview.tsx` +- Modify: `understand-anything-plugin/packages/dashboard/src/components/SearchBar.tsx` +- Modify: `understand-anything-plugin/packages/dashboard/src/components/LayerLegend.tsx` +- Modify: `understand-anything-plugin/packages/dashboard/src/components/PersonaSelector.tsx` +- Modify: `understand-anything-plugin/packages/dashboard/src/components/CodeViewer.tsx` +- Modify: `understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx` +- Modify: `understand-anything-plugin/packages/dashboard/src/App.tsx` + +**Step 1: Rename CSS variables in index.css** + +In the `@theme` block, rename: +- `--color-gold` -> `--color-accent` +- `--color-gold-dim` -> `--color-accent-dim` +- `--color-gold-bright` -> `--color-accent-bright` + +Also rename the `@keyframes goldPulse` to `accentPulse` and `.animate-gold-pulse` to `.animate-accent-pulse`. + +**Step 2: Rename all Tailwind class references across components** + +Find and replace in all component files: +- `text-gold-bright` -> `text-accent-bright` +- `text-gold-dim` -> `text-accent-dim` +- `text-gold` -> `text-accent` +- `bg-gold` -> `bg-accent` +- `border-gold` -> `border-accent` +- `ring-gold-dim` -> `ring-accent-dim` +- `ring-gold-bright` -> `ring-accent-bright` +- `ring-gold` -> `ring-accent` +- `animate-gold-pulse` -> `animate-accent-pulse` + +Order matters — replace the longer `-bright` and `-dim` variants first to avoid partial matches. + +Also replace any `var(--color-gold` with `var(--color-accent` in inline styles. + +**Step 3: Verify the build compiles** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/dashboard build` +Expected: Build succeeds with no errors. + +**Step 4: Visually verify (optional)** + +Run: `cd understand-anything-plugin && pnpm dev:dashboard` +Expected: Dashboard looks identical — same gold accent, no visual changes. + +**Step 5: Commit** + +```bash +git add -A +git commit -m "refactor(dashboard): rename gold CSS variables to accent" +``` + +--- + +### Task 2: Consolidate hardcoded RGBA values into CSS variables + +Replace scattered hardcoded color values in components with CSS variables so they respond to theme changes. + +**Files:** +- Modify: `understand-anything-plugin/packages/dashboard/src/index.css` +- Modify: `understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx` +- Modify: `understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx` +- Modify: `understand-anything-plugin/packages/dashboard/src/components/CodeViewer.tsx` + +**Step 1: Add new CSS variables to index.css @theme block** + +Add these new variables after the existing border variables: + +```css +/* Glass */ +--glass-bg: rgba(20, 20, 20, 0.8); +--glass-bg-heavy: rgba(20, 20, 20, 0.95); +--glass-border: rgba(212, 165, 116, 0.1); +--glass-border-heavy: rgba(212, 165, 116, 0.15); + +/* Scrollbar */ +--scrollbar-thumb: rgba(212, 165, 116, 0.2); +--scrollbar-thumb-hover: rgba(212, 165, 116, 0.35); + +/* Glow */ +--glow-accent: rgba(212, 165, 116, 0.15); +--glow-accent-strong: rgba(212, 165, 116, 0.4); +--glow-accent-pulse: rgba(212, 165, 116, 0.6); + +/* Edges */ +--color-edge: rgba(212, 165, 116, 0.3); +--color-edge-dim: rgba(212, 165, 116, 0.08); +--color-edge-dot: rgba(212, 165, 116, 0.15); + +/* Layer group (accent-based overlays) */ +--color-accent-overlay-bg: rgba(212, 165, 116, 0.05); +--color-accent-overlay-border: rgba(212, 165, 116, 0.25); + +/* kbd */ +--kbd-bg: rgba(212, 165, 116, 0.1); +``` + +**Step 2: Update .glass, .glass-heavy classes in index.css** + +Replace hardcoded values with the new variables: + +```css +.glass { + background: var(--glass-bg); + border: 1px solid var(--glass-border); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +.glass-heavy { + background: var(--glass-bg-heavy); + border: 1px solid var(--glass-border-heavy); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); +} +``` + +**Step 3: Update scrollbar styles in index.css** + +```css +::-webkit-scrollbar-thumb { + background: var(--scrollbar-thumb); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--scrollbar-thumb-hover); +} +``` + +**Step 4: Update glow classes in index.css** + +```css +.node-glow { + box-shadow: 0 0 20px var(--glow-accent); +} +``` + +Update `@keyframes accentPulse` (renamed in Task 1): +```css +@keyframes accentPulse { + 0%, 100% { + box-shadow: 0 0 8px var(--glow-accent-strong); + } + 50% { + box-shadow: 0 0 20px var(--glow-accent-pulse); + } +} +``` + +**Step 5: Update .kbd class in index.css** + +```css +.kbd { + /* ... keep existing sizing/layout ... */ + color: var(--color-accent); + background: var(--kbd-bg); +} +``` + +**Step 6: Update GraphView.tsx hardcoded colors** + +Replace these inline style values: + +| Location | Old Value | New Value | +|----------|-----------|-----------| +| Edge default style stroke | `"rgba(212,165,116,0.3)"` | `"var(--color-edge)"` | +| Edge diff-faded stroke | `"rgba(212,165,116,0.08)"` | `"var(--color-edge-dim)"` | +| Background dots color prop | `"rgba(212,165,116,0.15)"` | `"var(--color-edge-dot)"` | +| MiniMap nodeColor | `"#1a1a1a"` | `"var(--color-elevated)"` | +| MiniMap maskColor | `"rgba(10,10,10,0.7)"` | `"var(--glass-bg)"` | +| Group node backgroundColor | `"rgba(212,165,116,0.05)"` | `"var(--color-accent-overlay-bg)"` | +| Group node border | `"2px dashed rgba(212,165,116,0.25)"` | `"2px dashed var(--color-accent-overlay-border)"` | +| Group node label color | `"#d4a574"` | `"var(--color-accent)"` | +| Edge label fill (normal) | `"#a39787"` | `"var(--color-text-secondary)"` | +| Edge label fill (diff faded) | `"rgba(163,151,135,0.3)"` | `"var(--color-text-muted)"` | +| Spinner border class | `border-gold` already renamed to `border-accent` | Already done in Task 1 | + +**Step 7: Update CodeViewer.tsx hardcoded colors** + +Replace inline styles for the file type badge: +- `color: "var(--color-node-file)"` — already uses CSS var, keep +- `borderColor: "rgba(74,124,155,0.3)"` -> `"color-mix(in srgb, var(--color-node-file) 30%, transparent)"` +- `backgroundColor: "rgba(74,124,155,0.1)"` -> `"color-mix(in srgb, var(--color-node-file) 10%, transparent)"` + +**Step 8: Update CustomNode.tsx hardcoded shadow** + +Replace `shadow-[0_2px_8px_rgba(0,0,0,0.3)]` — this black shadow is fine for dark themes but keep it. Leave as-is since it works on both dark and light. + +**Step 9: Verify build** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/dashboard build` +Expected: Build succeeds. + +**Step 10: Commit** + +```bash +git add -A +git commit -m "refactor(dashboard): consolidate hardcoded colors into CSS variables" +``` + +--- + +### Task 3: Create theme type definitions + +**Files:** +- Create: `understand-anything-plugin/packages/dashboard/src/themes/types.ts` + +**Step 1: Write the types file** + +```typescript +export type PresetId = + | "dark-gold" + | "dark-ocean" + | "dark-forest" + | "dark-rose" + | "light-minimal"; + +export interface AccentSwatch { + id: string; + name: string; + accent: string; + accentDim: string; + accentBright: string; +} + +export interface ThemePreset { + id: PresetId; + name: string; + isDark: boolean; + colors: Record; + accentSwatches: AccentSwatch[]; + defaultAccentId: string; +} + +export interface ThemeConfig { + presetId: PresetId; + accentId: string; +} + +export const DEFAULT_THEME_CONFIG: ThemeConfig = { + presetId: "dark-gold", + accentId: "gold", +}; +``` + +**Step 2: Commit** + +```bash +git add -A +git commit -m "feat(dashboard): add theme type definitions" +``` + +--- + +### Task 4: Create theme presets + +**Files:** +- Create: `understand-anything-plugin/packages/dashboard/src/themes/presets.ts` + +**Step 1: Write the presets file** + +```typescript +import type { AccentSwatch, ThemePreset } from "./types.ts"; + +const DARK_ACCENT_SWATCHES: AccentSwatch[] = [ + { id: "gold", name: "Gold", accent: "#d4a574", accentDim: "#c9a96e", accentBright: "#e8c49a" }, + { id: "ocean", name: "Ocean", accent: "#5ba4cf", accentDim: "#4e93ba", accentBright: "#7abce0" }, + { id: "emerald", name: "Emerald", accent: "#5ea67a", accentDim: "#4e9468", accentBright: "#78c492" }, + { id: "rose", name: "Rose", accent: "#cf7a8a", accentDim: "#b96e7e", accentBright: "#e094a4" }, + { id: "purple", name: "Purple", accent: "#9b7abf", accentDim: "#876bb0", accentBright: "#b494d4" }, + { id: "amber", name: "Amber", accent: "#c9963a", accentDim: "#b5862e", accentBright: "#ddb05c" }, + { id: "teal", name: "Teal", accent: "#4aab9a", accentDim: "#3d9686", accentBright: "#68c4b4" }, + { id: "silver", name: "Silver", accent: "#a0a8b0", accentDim: "#8e959c", accentBright: "#b8bfc6" }, +]; + +const LIGHT_ACCENT_SWATCHES: AccentSwatch[] = [ + { id: "indigo", name: "Indigo", accent: "#4a6fa5", accentDim: "#3d5f8f", accentBright: "#6088bf" }, + { id: "ocean", name: "Ocean", accent: "#3a8ab5", accentDim: "#2e7aa0", accentBright: "#55a0cc" }, + { id: "emerald", name: "Emerald", accent: "#3a8a5c", accentDim: "#2e7a4e", accentBright: "#55a878" }, + { id: "rose", name: "Rose", accent: "#a5566a", accentDim: "#8f4a5c", accentBright: "#bf6e82" }, + { id: "purple", name: "Purple", accent: "#6b5a9e", accentDim: "#5c4d8a", accentBright: "#8474b5" }, + { id: "amber", name: "Amber", accent: "#9e7a30", accentDim: "#8a6a28", accentBright: "#b5923e" }, + { id: "teal", name: "Teal", accent: "#2e8a7a", accentDim: "#267a6c", accentBright: "#45a595" }, + { id: "slate", name: "Slate", accent: "#5a6570", accentDim: "#4e5860", accentBright: "#6e7a85" }, +]; + +export const PRESETS: ThemePreset[] = [ + { + id: "dark-gold", + name: "Dark Gold", + isDark: true, + defaultAccentId: "gold", + accentSwatches: DARK_ACCENT_SWATCHES, + colors: { + root: "#0a0a0a", + surface: "#111111", + elevated: "#1a1a1a", + panel: "#141414", + "text-primary": "#f5f0eb", + "text-secondary": "#a39787", + "text-muted": "#6b5f53", + "node-file": "#4a7c9b", + "node-function": "#5a9e6f", + "node-class": "#8b6fb0", + "node-module": "#c9a06c", + "node-concept": "#b07a8a", + }, + }, + { + id: "dark-ocean", + name: "Dark Ocean", + isDark: true, + defaultAccentId: "ocean", + accentSwatches: DARK_ACCENT_SWATCHES, + colors: { + root: "#0a0e14", + surface: "#111820", + elevated: "#1a222c", + panel: "#141c24", + "text-primary": "#e8edf2", + "text-secondary": "#87939f", + "text-muted": "#536b7a", + "node-file": "#4a7c9b", + "node-function": "#5a9e6f", + "node-class": "#8b6fb0", + "node-module": "#c9a06c", + "node-concept": "#b07a8a", + }, + }, + { + id: "dark-forest", + name: "Dark Forest", + isDark: true, + defaultAccentId: "emerald", + accentSwatches: DARK_ACCENT_SWATCHES, + colors: { + root: "#0a100a", + surface: "#111811", + elevated: "#1a241a", + panel: "#141c14", + "text-primary": "#ebf0eb", + "text-secondary": "#87a38f", + "text-muted": "#536b5a", + "node-file": "#4a7c9b", + "node-function": "#5a9e6f", + "node-class": "#8b6fb0", + "node-module": "#c9a06c", + "node-concept": "#b07a8a", + }, + }, + { + id: "dark-rose", + name: "Dark Rose", + isDark: true, + defaultAccentId: "rose", + accentSwatches: DARK_ACCENT_SWATCHES, + colors: { + root: "#100a0a", + surface: "#181111", + elevated: "#221a1a", + panel: "#1c1414", + "text-primary": "#f2e8ea", + "text-secondary": "#9f8790", + "text-muted": "#6b535a", + "node-file": "#4a7c9b", + "node-function": "#5a9e6f", + "node-class": "#8b6fb0", + "node-module": "#c9a06c", + "node-concept": "#b07a8a", + }, + }, + { + id: "light-minimal", + name: "Light Minimal", + isDark: false, + defaultAccentId: "indigo", + accentSwatches: LIGHT_ACCENT_SWATCHES, + colors: { + root: "#f5f3f0", + surface: "#eae7e3", + elevated: "#ffffff", + panel: "#f0ede9", + "text-primary": "#1a1a1a", + "text-secondary": "#6b6b6b", + "text-muted": "#a0a0a0", + "node-file": "#3a6a87", + "node-function": "#488a5b", + "node-class": "#755d99", + "node-module": "#a88a56", + "node-concept": "#966674", + }, + }, +]; + +export function getPreset(id: string): ThemePreset { + return PRESETS.find((p) => p.id === id) ?? PRESETS[0]; +} + +export function getAccent(preset: ThemePreset, accentId: string): AccentSwatch { + return ( + preset.accentSwatches.find((s) => s.id === accentId) ?? + preset.accentSwatches.find((s) => s.id === preset.defaultAccentId) ?? + preset.accentSwatches[0] + ); +} +``` + +**Step 2: Commit** + +```bash +git add -A +git commit -m "feat(dashboard): add theme preset definitions" +``` + +--- + +### Task 5: Create theme engine + +Pure functions with no React dependency. Handles CSS variable injection and accent derivation. + +**Files:** +- Create: `understand-anything-plugin/packages/dashboard/src/themes/theme-engine.ts` + +**Step 1: Write the theme engine** + +```typescript +import type { ThemeConfig } from "./types.ts"; +import { getAccent, getPreset } from "./presets.ts"; + +export function hexToRgb(hex: string): string { + const h = hex.replace("#", ""); + const n = parseInt(h, 16); + return `${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}`; +} + +function deriveFromAccent(accentHex: string, isDark: boolean): Record { + const rgb = hexToRgb(accentHex); + return { + "color-border-subtle": `rgba(${rgb}, ${isDark ? 0.12 : 0.1})`, + "color-border-medium": `rgba(${rgb}, ${isDark ? 0.25 : 0.18})`, + "glass-bg": isDark ? "rgba(20, 20, 20, 0.8)" : "rgba(255, 255, 255, 0.8)", + "glass-bg-heavy": isDark ? "rgba(20, 20, 20, 0.95)" : "rgba(255, 255, 255, 0.95)", + "glass-border": `rgba(${rgb}, ${isDark ? 0.1 : 0.08})`, + "glass-border-heavy": `rgba(${rgb}, ${isDark ? 0.15 : 0.12})`, + "scrollbar-thumb": `rgba(${rgb}, 0.2)`, + "scrollbar-thumb-hover": `rgba(${rgb}, 0.35)`, + "glow-accent": `rgba(${rgb}, 0.15)`, + "glow-accent-strong": `rgba(${rgb}, 0.4)`, + "glow-accent-pulse": `rgba(${rgb}, 0.6)`, + "color-edge": `rgba(${rgb}, 0.3)`, + "color-edge-dim": `rgba(${rgb}, 0.08)`, + "color-edge-dot": `rgba(${rgb}, 0.15)`, + "color-accent-overlay-bg": `rgba(${rgb}, 0.05)`, + "color-accent-overlay-border": `rgba(${rgb}, 0.25)`, + "kbd-bg": `rgba(${rgb}, 0.1)`, + }; +} + +export function applyTheme(config: ThemeConfig): void { + const preset = getPreset(config.presetId); + const accent = getAccent(preset, config.accentId); + const style = document.documentElement.style; + + // 1. Apply base preset colors + for (const [key, value] of Object.entries(preset.colors)) { + style.setProperty(`--color-${key}`, value); + } + + // 2. Apply accent colors from swatch + style.setProperty("--color-accent", accent.accent); + style.setProperty("--color-accent-dim", accent.accentDim); + style.setProperty("--color-accent-bright", accent.accentBright); + + // 3. Apply derived values + const derived = deriveFromAccent(accent.accent, preset.isDark); + for (const [key, value] of Object.entries(derived)) { + style.setProperty(`--${key}`, value); + } + + // 4. Set data-theme for CSS-only selectors + document.documentElement.setAttribute("data-theme", preset.isDark ? "dark" : "light"); +} +``` + +**Step 2: Commit** + +```bash +git add -A +git commit -m "feat(dashboard): add theme engine with CSS variable injection" +``` + +--- + +### Task 6: Create ThemeContext + +React context + provider that manages theme state, persistence, and resolution. + +**Files:** +- Create: `understand-anything-plugin/packages/dashboard/src/themes/ThemeContext.tsx` + +**Step 1: Write the context** + +```typescript +import { + createContext, + useCallback, + useContext, + useEffect, + useRef, + useState, + type ReactNode, +} from "react"; +import type { PresetId, ThemeConfig, ThemePreset } from "./types.ts"; +import { DEFAULT_THEME_CONFIG } from "./types.ts"; +import { getPreset } from "./presets.ts"; +import { applyTheme } from "./theme-engine.ts"; + +const STORAGE_KEY = "ua-theme"; + +interface ThemeContextValue { + config: ThemeConfig; + preset: ThemePreset; + setPreset: (presetId: PresetId) => void; + setAccent: (accentId: string) => void; +} + +const ThemeContext = createContext(null); + +function loadFromLocalStorage(): ThemeConfig | null { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return null; + const parsed = JSON.parse(raw); + if (parsed && typeof parsed.presetId === "string" && typeof parsed.accentId === "string") { + return parsed as ThemeConfig; + } + return null; + } catch { + return null; + } +} + +function saveToLocalStorage(config: ThemeConfig): void { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(config)); + } catch { + // Storage full or unavailable — ignore + } +} + +function resolveInitialTheme(metaTheme?: ThemeConfig | null): ThemeConfig { + return loadFromLocalStorage() ?? metaTheme ?? DEFAULT_THEME_CONFIG; +} + +interface ThemeProviderProps { + metaTheme?: ThemeConfig | null; + children: ReactNode; +} + +export function ThemeProvider({ metaTheme, children }: ThemeProviderProps) { + const [config, setConfig] = useState(() => resolveInitialTheme(metaTheme)); + const initialized = useRef(false); + + // Apply theme on mount and config changes + useEffect(() => { + applyTheme(config); + if (initialized.current) { + saveToLocalStorage(config); + } + initialized.current = true; + }, [config]); + + // Update if metaTheme arrives later (async fetch) and no localStorage preference exists + useEffect(() => { + if (metaTheme && !loadFromLocalStorage()) { + setConfig(metaTheme); + } + }, [metaTheme]); + + const setPreset = useCallback((presetId: PresetId) => { + setConfig((prev) => { + const newPreset = getPreset(presetId); + return { presetId, accentId: newPreset.defaultAccentId }; + }); + }, []); + + const setAccent = useCallback((accentId: string) => { + setConfig((prev) => ({ ...prev, accentId })); + }, []); + + const preset = getPreset(config.presetId); + + return ( + + {children} + + ); +} + +export function useTheme(): ThemeContextValue { + const ctx = useContext(ThemeContext); + if (!ctx) throw new Error("useTheme must be used within ThemeProvider"); + return ctx; +} +``` + +**Step 2: Create barrel export** + +Create: `understand-anything-plugin/packages/dashboard/src/themes/index.ts` + +```typescript +export { ThemeProvider, useTheme } from "./ThemeContext.tsx"; +export { PRESETS, getPreset, getAccent } from "./presets.ts"; +export { applyTheme } from "./theme-engine.ts"; +export type { PresetId, ThemeConfig, ThemePreset, AccentSwatch } from "./types.ts"; +export { DEFAULT_THEME_CONFIG } from "./types.ts"; +``` + +**Step 3: Commit** + +```bash +git add -A +git commit -m "feat(dashboard): add ThemeContext with localStorage persistence" +``` + +--- + +### Task 7: Extend AnalysisMeta with theme field + +**Files:** +- Modify: `understand-anything-plugin/packages/core/src/types.ts` + +**Step 1: Add ThemeConfig type and extend AnalysisMeta** + +Add near the top of the file (after existing imports/types): + +```typescript +export interface ThemeConfig { + presetId: string; + accentId: string; +} +``` + +Add `theme` field to `AnalysisMeta`: + +```typescript +export interface AnalysisMeta { + lastAnalyzedAt: string; + gitCommitHash: string; + version: string; + analyzedFiles: number; + theme?: ThemeConfig; +} +``` + +**Step 2: Verify core builds** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core build` +Expected: Build succeeds. + +**Step 3: Verify core tests pass** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test` +Expected: All tests pass. + +**Step 4: Commit** + +```bash +git add -A +git commit -m "feat(core): add optional theme field to AnalysisMeta" +``` + +--- + +### Task 8: Create ThemePicker component + +The popover UI with preset selection and accent swatch row. + +**Files:** +- Create: `understand-anything-plugin/packages/dashboard/src/components/ThemePicker.tsx` + +**Step 1: Write the component** + +```tsx +import { useCallback, useEffect, useRef, useState } from "react"; +import { useTheme, PRESETS } from "../themes/index.ts"; + +export function ThemePicker() { + const { config, preset, setPreset, setAccent } = useTheme(); + const [open, setOpen] = useState(false); + const ref = useRef(null); + + // Close on outside click + useEffect(() => { + if (!open) return; + function handleClick(e: MouseEvent) { + if (ref.current && !ref.current.contains(e.target as Node)) { + setOpen(false); + } + } + document.addEventListener("mousedown", handleClick); + return () => document.removeEventListener("mousedown", handleClick); + }, [open]); + + // Close on Escape + useEffect(() => { + if (!open) return; + function handleKey(e: KeyboardEvent) { + if (e.key === "Escape") setOpen(false); + } + document.addEventListener("keydown", handleKey); + return () => document.removeEventListener("keydown", handleKey); + }, [open]); + + const handlePreset = useCallback( + (id: string) => { + setPreset(id as Parameters[0]); + }, + [setPreset], + ); + + return ( +
+ + + {open && ( +
+ {/* Presets */} +
+
+ Theme +
+
+ {PRESETS.map((p) => ( + + ))} +
+
+ + {/* Accent swatches */} +
+
+ Accent Color +
+
+ {preset.accentSwatches.map((swatch) => ( +
+
+
+ )} +
+ ); +} +``` + +**Step 2: Commit** + +```bash +git add -A +git commit -m "feat(dashboard): add ThemePicker popover component" +``` + +--- + +### Task 9: Integrate ThemeProvider and ThemePicker into App + +Wire everything together in the root component. + +**Files:** +- Modify: `understand-anything-plugin/packages/dashboard/src/App.tsx` + +**Step 1: Add imports** + +Add to imports at top of App.tsx: + +```typescript +import { ThemeProvider } from "./themes/index.ts"; +import { ThemePicker } from "./components/ThemePicker.tsx"; +import type { ThemeConfig } from "./themes/index.ts"; +``` + +**Step 2: Add meta.json theme loading** + +Inside the App component, add state and effect for meta.json theme: + +```typescript +const [metaTheme, setMetaTheme] = useState(null); + +useEffect(() => { + fetch("/meta.json") + .then((r) => (r.ok ? r.json() : null)) + .then((meta) => { + if (meta?.theme) setMetaTheme(meta.theme); + }) + .catch(() => {}); +}, []); +``` + +**Step 3: Wrap return JSX with ThemeProvider** + +Wrap the entire return value of App with `...`. + +**Step 4: Add ThemePicker to header** + +In the header bar (the `
` or top flex row), add `` after the existing controls (PersonaSelector, DiffToggle, LayerLegend) and before the help button. + +**Step 5: Verify build** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/dashboard build` +Expected: Build succeeds. + +**Step 6: Commit** + +```bash +git add -A +git commit -m "feat(dashboard): integrate ThemeProvider and ThemePicker into App" +``` + +--- + +### Task 10: Light theme CSS adjustments + +Handle edge cases where CSS variables alone aren't sufficient for the light theme. + +**Files:** +- Modify: `understand-anything-plugin/packages/dashboard/src/index.css` + +**Step 1: Add data-theme selectors for light theme overrides** + +Add at the end of index.css: + +```css +/* Light theme overrides */ +[data-theme="light"] { + color-scheme: light; +} + +[data-theme="light"] .diff-faded { + opacity: 0.35; +} + +[data-theme="light"] ::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.05); +} + +[data-theme="dark"] { + color-scheme: dark; +} +``` + +**Step 2: Add transition for smooth theme switching** + +Add to the `html` base styles: + +```css +html { + transition: background-color 0.2s ease, color 0.2s ease; +} +``` + +**Step 3: Update the WarningBanner consideration** + +WarningBanner uses Tailwind amber/orange colors directly (e.g., `bg-amber-900/20`). These are semantic warning colors and should NOT change with theme. However, for the light theme, the amber colors on a light background need adjustment. + +Add to light theme overrides if needed: + +```css +[data-theme="light"] .warning-banner { + background: rgba(180, 130, 30, 0.1); + border-color: rgba(180, 130, 30, 0.3); + color: #92600a; +} +``` + +Note: Only add this if the WarningBanner looks broken on the light theme during visual testing. It may work fine as-is with Tailwind's amber colors. + +**Step 4: Verify build** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/dashboard build` +Expected: Build succeeds. + +**Step 5: Commit** + +```bash +git add -A +git commit -m "feat(dashboard): add light theme CSS overrides" +``` + +--- + +### Task 11: Remove @theme defaults from index.css + +Now that the theme engine sets all CSS variables at runtime, the `@theme` block in index.css serves as the initial/fallback values before React mounts. Keep it but update it to use the accent naming. + +**Files:** +- Modify: `understand-anything-plugin/packages/dashboard/src/index.css` + +**Step 1: Update @theme block** + +The `@theme` block should already have `--color-accent` (from Task 1 rename). Ensure the new variables added in Task 2 are also present in the `@theme` block as defaults: + +```css +@theme { + /* Base */ + --color-root: #0a0a0a; + --color-surface: #111111; + --color-elevated: #1a1a1a; + --color-panel: #141414; + + /* Accent */ + --color-accent: #d4a574; + --color-accent-dim: #c9a96e; + --color-accent-bright: #e8c49a; + + /* Text */ + --color-text-primary: #f5f0eb; + --color-text-secondary: #a39787; + --color-text-muted: #6b5f53; + + /* Borders */ + --color-border-subtle: rgba(212, 165, 116, 0.12); + --color-border-medium: rgba(212, 165, 116, 0.25); + + /* Node types */ + --color-node-file: #4a7c9b; + --color-node-function: #5a9e6f; + --color-node-class: #8b6fb0; + --color-node-module: #c9a06c; + --color-node-concept: #b07a8a; + + /* Diff */ + --color-diff-changed: #e05252; + --color-diff-affected: #d4a030; + --color-diff-changed-dim: rgba(224, 82, 82, 0.25); + --color-diff-affected-dim: rgba(212, 160, 48, 0.25); + + /* Glass */ + --glass-bg: rgba(20, 20, 20, 0.8); + --glass-bg-heavy: rgba(20, 20, 20, 0.95); + --glass-border: rgba(212, 165, 116, 0.1); + --glass-border-heavy: rgba(212, 165, 116, 0.15); + + /* Scrollbar */ + --scrollbar-thumb: rgba(212, 165, 116, 0.2); + --scrollbar-thumb-hover: rgba(212, 165, 116, 0.35); + + /* Glow */ + --glow-accent: rgba(212, 165, 116, 0.15); + --glow-accent-strong: rgba(212, 165, 116, 0.4); + --glow-accent-pulse: rgba(212, 165, 116, 0.6); + + /* Edges */ + --color-edge: rgba(212, 165, 116, 0.3); + --color-edge-dim: rgba(212, 165, 116, 0.08); + --color-edge-dot: rgba(212, 165, 116, 0.15); + + /* Accent overlays */ + --color-accent-overlay-bg: rgba(212, 165, 116, 0.05); + --color-accent-overlay-border: rgba(212, 165, 116, 0.25); + + /* Kbd */ + --kbd-bg: rgba(212, 165, 116, 0.1); + + /* Typography */ + --font-serif: 'DM Serif Display', Georgia, serif; + --font-mono: 'JetBrains Mono', 'Fira Code', monospace; + --font-sans: 'Inter', system-ui, sans-serif; +} +``` + +This ensures: +- Tailwind v4 generates all the correct utility classes from the `@theme` block +- Before React mounts, the page shows the Dark Gold default (no flash of unstyled content) +- The theme engine overrides these values at runtime + +**Step 2: Verify build** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/dashboard build` +Expected: Build succeeds. + +**Step 3: Commit** + +```bash +git add -A +git commit -m "refactor(dashboard): align @theme defaults with theme engine variables" +``` + +--- + +### Task 12: Full build + visual verification + +**Files:** None (verification only) + +**Step 1: Build core** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core build` +Expected: Build succeeds. + +**Step 2: Build dashboard** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/dashboard build` +Expected: Build succeeds. + +**Step 3: Run core tests** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test` +Expected: All tests pass. + +**Step 4: Run lint** + +Run: `cd understand-anything-plugin && pnpm lint` +Expected: No lint errors. + +**Step 5: Start dev server and visually verify** + +Run: `cd understand-anything-plugin && pnpm dev:dashboard` + +Verify: +1. Dashboard loads with Dark Gold theme (default) — looks identical to current +2. Theme picker button visible in header +3. Click theme picker — popover opens with 5 presets and 8 accent swatches +4. Select Dark Ocean — backgrounds turn navy-blue, accent turns cyan +5. Select Dark Forest — backgrounds turn dark green, accent turns emerald +6. Select Dark Rose — backgrounds turn dark warm, accent turns rose +7. Select Light Minimal — backgrounds turn light, text turns dark, accent turns indigo +8. Select different accent swatches within each preset — accent color, borders, glass, glow all update +9. Refresh page — theme persists from localStorage +10. Click outside popover — it closes +11. Press Escape — popover closes + +**Step 6: Commit (if any fixes needed)** + +```bash +git add -A +git commit -m "fix(dashboard): theme system visual adjustments" +``` + +--- + +## Dependency Graph + +``` +Task 1 (rename gold→accent) ─┐ + ├─> Task 3 (types) ──┐ +Task 2 (consolidate colors) ──┤ │ + │ Task 4 (presets) ─┤ + │ ├─> Task 6 (context) ─┐ + │ Task 5 (engine) ──┘ │ + │ ├─> Task 8 (picker) ─┐ + │ Task 7 (core types) ────────────────────┘ │ + │ │ + └───────────────────────────────────────────> Task 9 (integrate) ─┤ + │ + Task 10 (light CSS) ┤ + │ + Task 11 (defaults) ─┤ + │ + Task 12 (verify) ───┘ +``` + +**Parallelizable groups:** +- Tasks 1 + 2 can be done sequentially (both touch index.css) +- Tasks 3, 4, 5 can be done in parallel (independent new files) +- Task 6 depends on 3, 4, 5 +- Task 7 is independent (core package) +- Task 8 depends on 6 +- Task 9 depends on 1, 2, 7, 8 +- Tasks 10, 11 can be done after 9 +- Task 12 is final verification diff --git a/docs/plans/2026-03-27-token-reduction-design.md b/docs/plans/2026-03-27-token-reduction-design.md new file mode 100644 index 0000000..8bf8975 --- /dev/null +++ b/docs/plans/2026-03-27-token-reduction-design.md @@ -0,0 +1,395 @@ +# Token Reduction Design + +**Date:** 2026-03-27 +**Status:** Draft +**Goal:** Reduce total token cost of `/understand` by ~85-90% on large codebases (200+ files) + +--- + +## Problem + +For large codebases, the `/understand` pipeline spends the vast majority of its tokens on **repeated context injection**. The same data is sent to every subagent independently, even when that data could be computed once and shared. + +### Token cost breakdown (500-file TypeScript+React project, baseline) + +| Source | Phase | Tokens (input) | % of total | +|---|---|---|---| +| `allProjectFiles` list × 67 batches | Phase 2 | ~167,000 | ~50% | +| `file-analyzer-prompt.md` × 67 batches | Phase 2 | ~134,000 | ~40% | +| Language/framework addendums × 67 batches | Phase 2 | ~68,000 | ~20% | +| Tour builder payload (all nodes + edges) | Phase 5 | ~80,000 | ~24% | +| Graph reviewer (assembled graph + inventory) | Phase 6 | ~58,000 | ~17% | +| Architecture analyzer payload | Phase 4 | ~22,000 | ~7% | +| **Total** | | **~529,000** | | + +The root cause: **Phase 2 runs 67 batches (at 5-10 files each), and every single batch receives the full 500-file list for import resolution.** The file list alone costs ~2,500 tokens × 67 repetitions = 167,000 tokens on input, doing work that is entirely redundant between batches. + +--- + +## Goals + +- Reduce total input tokens by 85%+ on a 500-file project +- No degradation in graph quality for standard projects +- Preserve the `--full` / incremental / scope flags +- Maintain backward compatibility with existing `knowledge-graph.json` output schema + +--- + +## Changes + +Five changes compose the full approach (C1–C5). Each is independent and can be shipped separately, but all five are needed for the full reduction. + +--- + +### C1 — Pre-resolve imports in the project scanner + +**Root cause addressed:** `allProjectFiles` (the entire file list) is injected into every file-analyzer batch solely so each batch's extraction script can resolve relative imports. This is redundant: the full file list is available during Phase 1, and import resolution is deterministic. It should happen once, not 67 times. + +**Change:** Extend the Phase 1 scanner script to also parse import statements from every source file and resolve relative imports against the discovered file list. The resolved results are written into `scan-result.json` as a new `importMap` field. File-analyzer batches then receive only their own batch's pre-resolved imports — not the full file list. + +#### Scanner output addition + +`scan-result.json` gains: + +```json +{ + "importMap": { + "src/index.ts": ["src/utils.ts", "src/config.ts"], + "src/utils.ts": [], + "src/components/App.tsx": ["src/hooks/useAuth.ts", "src/store/index.ts"] + } +} +``` + +- Keys are project-relative paths (matching `files[*].path`) +- Values are resolved project-relative paths only (external/unresolvable imports are omitted) +- External imports (`node_modules`, unresolvable paths) are excluded from the map entirely + +#### Scanner script additions (Phase 1 Step 8) + +After the existing 7 steps, the scanner script adds a new step: + +``` +Step 8 — Import Resolution + +For each file in the discovered source list: + 1. Read the file content + 2. Extract import statements (language-specific patterns per Step 3's language detection): + - TypeScript/JavaScript: `import ... from '...'`, `require('...')` + - Python: `import ...`, `from ... import ...` + - Go: `import "..."` blocks + - Rust: `use ...` statements + - Java/Kotlin: `import ...` statements + - Ruby: `require`, `require_relative` + 3. For each relative import (starts with `./` or `../`): + a. Compute the resolved path from the current file's directory + b. Normalize to project-relative format + c. Try common extension variants if the import has no extension: + `.ts`, `.tsx`, `.js`, `.jsx`, `/index.ts`, `/index.js`, `/index.tsx` + d. If any variant exists in the discovered file list, record it; otherwise skip + 4. For absolute imports (no `.` prefix): skip (external package) + +Output the full importMap in the JSON result. +``` + +#### File-analyzer input schema change + +**Before:** +```json +{ + "projectRoot": "/path/to/project", + "allProjectFiles": ["src/index.ts", "src/utils.ts", "...500 paths..."], + "batchFiles": [ + {"path": "src/index.ts", "language": "typescript", "sizeLines": 150} + ] +} +``` + +**After:** +```json +{ + "projectRoot": "/path/to/project", + "batchFiles": [ + {"path": "src/index.ts", "language": "typescript", "sizeLines": 150} + ], + "batchImportData": { + "src/index.ts": ["src/utils.ts", "src/config.ts"], + "src/components/App.tsx": ["src/hooks/useAuth.ts"] + } +} +``` + +`allProjectFiles` is removed entirely. `batchImportData` contains only the pre-resolved imports for the files in this batch (sliced from `importMap` by the orchestrator). + +#### File-analyzer extraction script change + +The extraction script no longer performs import resolution. It: +- Still extracts: functions, classes, exports, metrics (unchanged) +- For imports: reads `batchImportData[file.path]` from the input JSON — no cross-referencing needed +- The `imports` array in each file result becomes: `batchImportData[file.path]` mapped to import edge objects with `resolvedPath` already populated, `isExternal: false` + +#### SKILL.md Phase 2 change + +Remove the `allProjectFiles` injection from the batch dispatch prompt. Replace with a per-batch `batchImportData` slice: + +``` +For each batch, slice importData from the importMap read in Phase 1: +batchImportData = { [file.path]: importMap[file.path] ?? [] } + for each file in this batch +``` + +#### Token savings estimate + +| | Batches | Tokens/batch | Total | +|---|---|---|---| +| Before | 67 | ~2,500 (file list) | ~167,500 | +| After (C1 alone) | 67 | ~200 (batch importData) | ~13,400 | +| **Savings** | | | **~154,100** | + +--- + +### C2 — Increase batch size from 5-10 to 20-30 files + +**Root cause addressed:** Every batch incurs the full cost of `file-analyzer-prompt.md` (~2,000 tokens) plus the batch dispatch overhead. With 67 batches, this adds up even without `allProjectFiles`. Fewer, larger batches directly reduce this repetition. + +**Change:** In SKILL.md Phase 2, change the batch size guidance: + +- **Before:** "Batch the file list from Phase 1 into groups of **5-10 files each**" +- **After:** "Batch the file list from Phase 1 into groups of **20-30 files each** (aim for ~25 per batch)" + +Also update the concurrency limit from 3 to **5** concurrent batches. Fewer total batches means we can afford more parallelism without overwhelming the system. + +#### Trade-offs + +| | Smaller batches (current) | Larger batches (new) | +|---|---|---| +| Files per batch | 5-10 | 20-30 | +| Total batches (500 files) | ~67 | ~20 | +| Prompt repetition | 67× | 20× | +| Quality risk | Lower (focused) | Slightly higher (more files per subagent) | +| Concurrency | 3 | 5 | + +Quality risk is low: each subagent still operates on distinct, non-overlapping file groups. The extraction script is deterministic regardless of batch size. Semantic analysis (summaries, tags) may be marginally less focused, but the quality difference is negligible in practice for well-structured files. + +#### Token savings estimate (combined with C1) + +| | Batches | Tokens/batch (prompt) | Total | +|---|---|---|---| +| Before (C1 only) | 67 | ~2,000 | ~134,000 | +| After (C1+C2) | 20 | ~2,000 | ~40,000 | +| **Savings from C2** | | | **~94,000** | + +C1+C2 combined eliminate ~248,000 tokens from Phase 2 (down from ~301,500 to ~53,500, a ~82% Phase 2 reduction). + +--- + +### C3 — Remove language/framework addendums from file-analyzer batches + +**Root cause addressed:** `languages/typescript.md` (~600 tokens) and `frameworks/react.md` (~700 tokens) are read and injected into every file-analyzer batch prompt. For a TypeScript+React project with 20 batches (after C2), this costs 20 × 1,300 = 26,000 additional tokens — and the model already has deep knowledge of these languages from training. + +**Change:** Stop injecting addendum files into Phase 2 batch prompts entirely. The addendums remain injected into Phase 4 (architecture analyzer) where there is only **one** subagent call, making the cost acceptable. + +Instead, add a compact "Language and Framework Hints" reference section directly into `file-analyzer-prompt.md`. This section is a distilled, one-time addition (~150 tokens total) that captures the most useful patterns from all addendums in a concise lookup table. + +#### New section in `file-analyzer-prompt.md` (replace addendum injection) + +```markdown +## Language and Framework Quick Reference + +Use these hints to improve tag and edge accuracy. These supplement your training knowledge. + +| Signal | Tag(s) | Note | +|---|---|---| +| File in `hooks/`, exports function starting with `use` | `hook`, `service` | React custom hook | +| File in `contexts/`, exports a Provider | `service`, `state` | React context | +| File in `pages/` or `views/` | `ui`, `routing` | Page-level component | +| File in `store/`, `slices/`, `reducers/` | `state` | State management | +| File in `services/`, `api/` | `service` | Data-fetching / API client | +| `__init__.py` with re-exports | `entry-point`, `barrel` | Python package root | +| `manage.py` at project root | `entry-point` | Django management entry | +| File named `mod.rs` | `barrel` | Rust module barrel | +| File named `main.go` in `cmd/` | `entry-point` | Go binary entry | + +For React: create `depends_on` edges from components to hooks they call. Create `publishes`/`subscribes` edges for Context provider/consumer patterns. +``` + +#### SKILL.md Phase 2 change + +Remove steps 2 and 3 from the "Build the combined prompt template" block: +- **Remove:** Step 2 (Language context injection — read `./languages/.md` per detected language) +- **Remove:** Step 3 (Framework addendum injection — read `./frameworks/.md` per detected framework) +- **Keep:** Step 1 (Read the base template at `./file-analyzer-prompt.md`) + +The addendum injection steps **remain unchanged** in Phase 4 (architecture analyzer), since they run once. + +#### Token savings estimate + +| | Batches | Addendum tokens/batch | Total | +|---|---|---|---| +| Before (after C2) | 20 | ~1,300 (TS+React) | ~26,000 | +| After | 20 | ~150 (inline hints) | ~3,000 | +| **Savings** | | | **~23,000** | + +--- + +### C4 — Slim Phase 4 and Phase 5 payloads + +**Root cause addressed:** Phase 5 (tour builder) receives all nodes (file + function + class) and all edges (imports + contains + calls + exports + ...). For a 500-file project, this can include 1,500+ nodes and 3,000+ edges. Most of this data is not needed for tour design. + +#### Phase 4 (Architecture Analyzer) — minor trim + +Phase 4 already only sends file-type nodes, which is correct. Minor change: explicitly strip `languageNotes` from each node object in the payload (it's not useful for layer assignment and can be verbose). Also strip `name` — it is always derivable as the basename of `filePath`. + +**Before per node:** `{id, name, filePath, summary, tags, complexity, languageNotes?}` +**After per node:** `{id, filePath, summary, tags}` + +Savings: ~15-20% fewer tokens per node, ~3,000–5,000 tokens total for Phase 4. + +#### Phase 5 (Tour Builder) — major trim + +Three changes to what the orchestrator injects into the tour-builder subagent: + +**1. File nodes only (strip function/class nodes)** + +The tour references node IDs for wayfinding. In practice the tour always references `file:` nodes — function and class nodes are visible in the dashboard's NodeInfo sidebar once a file is selected, but the tour itself navigates at the file level. + +- **Before:** all nodes (file + function + class) — for 500 files, maybe 1,500+ nodes +- **After:** file-type nodes only — 500 nodes + +**2. Slim node format** + +The tour builder script only uses node IDs, names, and types for graph computation. Summaries and tags are used in Phase 2 (pedagogical narrative writing). Strip heavy optional fields from the injected payload: + +- **Before per node:** `{id, name, filePath, summary, type, tags, complexity, languageNotes?}` +- **After per node:** `{id, name, filePath, summary, type}` (drop tags, complexity, languageNotes) + +**3. Slim edges (imports + calls only) and slim layers** + +The tour's BFS traversal only traverses `imports` and `calls` edges. `contains`, `exports`, `tested_by`, `depends_on`, and other edge types add no value to the traversal and inflate the payload. + +- **Before edges:** all edge types (~3,000+ edges including all `contains` edges to function/class nodes) +- **After edges:** only `imports` and `calls` edge types (~400–800 edges for typical projects) + +For layers, the tour builder uses layer data only to inform the tour's narrative arc (which layer to introduce first, second, etc.). It does not need the full `nodeIds` arrays — those can be very large. + +- **Before per layer:** `{id, name, description, nodeIds: [...hundreds of IDs]}` +- **After per layer:** `{id, name, description}` (drop nodeIds) + +#### Token savings estimate (Phase 5) + +| Data | Before | After | +|---|---|---| +| Node count | ~1,500 × ~180 chars | ~500 × ~120 chars | +| Node tokens | ~67,500 | ~15,000 | +| Edge count | ~3,000 × ~80 chars | ~600 × ~80 chars | +| Edge tokens | ~60,000 | ~12,000 | +| Layer tokens | ~5,000 | ~500 | +| **Phase 5 total** | **~132,500** | **~27,500** | +| **Savings** | | **~105,000** | + +#### SKILL.md changes + +In **Phase 4** dispatch prompt template, update the file node format: +``` +File nodes: +[list of {id, filePath, summary, tags} for all file-type nodes] +``` + +In **Phase 5** dispatch prompt template, update all three payload specs: +``` +Nodes (file nodes only): +[list of {id, name, filePath, summary, type} for all file-type nodes only — do NOT include function or class nodes] + +Key edges (imports and calls only): +[list of edges where type is "imports" or "calls" only] + +Layers: +[list of {id, name, description} — omit nodeIds] +``` + +--- + +### C5 — Gate the graph-reviewer subagent behind `--review` + +**Root cause addressed:** The graph-reviewer subagent (Phase 6) reads the entire assembled graph (~500 nodes, all edges, layers, tour) and runs a LLM-powered validation. However, its Phase 1 is entirely a deterministic script, and its Phase 2 is a simple threshold decision: if `issues.length === 0`, approve. There is no LLM judgment needed for the happy path. + +**Change:** By default, skip the graph-reviewer subagent. The orchestrator performs inline deterministic validation using a pre-written script. Only when `--review` is explicitly passed in `$ARGUMENTS` does the full LLM reviewer subagent run. + +#### Default path (no `--review`) + +In Phase 6, instead of dispatching the graph-reviewer subagent, the orchestrator: + +1. Writes a compact validation script inline (embedded in SKILL.md, ~50 lines of Node.js): + - Check: every edge source/target references a real node ID + - Check: every file node appears in exactly one layer + - Check: every tour step nodeId exists + - Check: no duplicate node IDs + - Check: required fields present on nodes and edges +2. Runs the script against `assembled-graph.json` +3. If `issues.length === 0`: proceed to Phase 7 (save) +4. If `issues.length > 0`: apply the same automated fixes as before (remove dangling edges, fill defaults), then save + +This is sufficient for standard runs. The LLM reviewer adds value for catching subtle quality issues (generic summaries, orphan nodes, tour step coherence) — but those are nice-to-have, not blocking. + +#### `--review` path + +When `--review` is in `$ARGUMENTS`, the full graph-reviewer subagent runs as it does today. No change to that code path. + +#### Token savings estimate + +| Path | Tokens | +|---|---| +| Current (always runs LLM reviewer) | ~58,000 input + ~500 output | +| Default (inline script, no LLM) | ~0 | +| `--review` (unchanged) | ~58,000 (same as current) | +| **Savings for default runs** | **~58,500** | + +--- + +## Combined savings summary + +| Change | Tokens before | Tokens after | Savings | +|---|---|---|---| +| C1+C2: import map + batch consolidation | ~301,500 | ~53,500 | ~248,000 | +| C3: remove addendums from batches | ~26,000 | ~3,000 | ~23,000 | +| C4: slim Phase 4+5 payloads | ~154,500 | ~33,000 | ~121,500 | +| C5: gate reviewer (default path) | ~58,500 | ~0 | ~58,500 | +| **Total** | **~540,500** | **~89,500** | **~451,000 (~83%)** | + +Estimates are for a 500-file TypeScript+React project. Actual savings scale with project size — a 1,000-file project would see proportionally larger savings from C1+C2 (more batches = more repetition eliminated). + +--- + +## File changes + +| File | Change | +|---|---| +| `skills/understand/project-scanner-prompt.md` | Add Step 8 (import resolution); add `importMap` to output schema | +| `skills/understand/file-analyzer-prompt.md` | Replace `allProjectFiles` with `batchImportData` in input schema; update extraction script to use pre-resolved imports; add compact Language/Framework Quick Reference section; remove addendum injection steps | +| `skills/understand/SKILL.md` | Phase 1: note importMap in scan result; Phase 2: remove addendum injection (steps 2+3), increase batch size 5-10→20-30, increase concurrency 3→5, replace `allProjectFiles` injection with `batchImportData` slice; Phase 4: slim node format in dispatch; Phase 5: file nodes only + slim edges + slim layers in dispatch; Phase 6: conditional reviewer — default inline script, `--review` flag for LLM reviewer | +| `skills/understand/architecture-analyzer-prompt.md` | No change (addendums still injected here) | +| `skills/understand/tour-builder-prompt.md` | Update input schema to reflect file-only nodes, imports+calls-only edges, slim layer format | +| `skills/understand/graph-reviewer-prompt.md` | No change (only used when `--review` flag is passed) | + +--- + +## Risks and mitigations + +| Risk | Likelihood | Mitigation | +|---|---|---| +| Scanner import resolution misses edge cases (complex re-exports, dynamic imports) | Medium | Log unresolved imports; file-analyzer still uses resolved data and creates edges only for confirmed matches. Missed imports = missing edges, which is same behavior as before for unresolvable imports | +| Larger batches (C2) reduce summary quality | Low | Summary quality is driven by the model's analysis of individual files. Batch size mainly affects how many files share one subagent's context window, not per-file quality. 20-30 files remains well within context limits | +| Stripping function/class nodes from tour (C4) breaks existing tour steps | None | Tour steps reference `file:` node IDs. No existing tour data references function/class nodes at the step level | +| Removing reviewer by default (C5) misses graph errors | Low | The inline deterministic script catches all critical structural issues (dangling refs, missing layers, duplicate IDs). The LLM reviewer's additional value is quality warnings (orphan nodes, generic summaries), which are non-blocking | +| Import map generation slows down Phase 1 | Low | The scanner script already reads all files for line counting. Import parsing adds one regex pass per file — negligible overhead | + +--- + +## Phased rollout recommendation + +Given the risk profile, implement in this order: + +1. **C5 first** — gate the reviewer, lowest risk, immediate 58K token savings per run +2. **C4** — slim Phase 5 payload, no scanner changes, no quality risk +3. **C3** — remove addendums from batches, add inline hints +4. **C1+C2 together** — scanner changes and batch consolidation, test thoroughly on small/medium/large projects before releasing diff --git a/docs/plans/2026-03-27-token-reduction-impl.md b/docs/plans/2026-03-27-token-reduction-impl.md new file mode 100644 index 0000000..848276b --- /dev/null +++ b/docs/plans/2026-03-27-token-reduction-impl.md @@ -0,0 +1,971 @@ +# Token Reduction Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Reduce `/understand` token cost by ~85% on large codebases through import pre-resolution, batch consolidation, addendum removal, payload slimming, and gating the LLM reviewer. + +**Architecture:** Five changes (C5 → C4 → C3 → C1+C2) applied in rollout order — lowest risk first. All changes are to prompt/skill markdown files in `understand-anything-plugin/skills/understand/`. No TypeScript source changes required. + +**Tech Stack:** Markdown skill files, Node.js inline scripts embedded in SKILL.md, knowledge-graph JSON pipeline. + +**Design doc:** `docs/plans/2026-03-27-token-reduction-design.md` + +--- + +## Task 1: C5 — Gate graph-reviewer behind `--review` flag + +Replaces the always-on LLM graph-reviewer subagent with a deterministic inline validation script. The LLM reviewer only runs when `--review` is in `$ARGUMENTS`. Saves ~58,500 tokens per default run. + +**Files:** +- Modify: `understand-anything-plugin/skills/understand/SKILL.md` (Phase 6, lines 330–362) + +### Step 1: Open SKILL.md and locate Phase 6 + +Read the file and find "## Phase 6 — REVIEW" (line 297). Identify steps 3–6 (lines 330–362) which currently always dispatch the LLM graph-reviewer subagent. + +### Step 2: Replace Phase 6 steps 3–6 with conditional reviewer logic + +Replace lines 330–362 (from "3. Dispatch a subagent using the prompt template" through "6. **If `approved: true`:** Proceed to Phase 7.") with: + +```markdown +3. **Check `$ARGUMENTS` for `--review` flag.** Then run the appropriate validation path: + +--- + +#### Default path (no `--review`): inline deterministic validation + +Write the following Node.js script to `$PROJECT_ROOT/.understand-anything/tmp/ua-inline-validate.js`: + +```javascript +#!/usr/bin/env node +const fs = require('fs'); +const graphPath = process.argv[2]; +const outputPath = process.argv[3]; +try { + const graph = JSON.parse(fs.readFileSync(graphPath, 'utf8')); + const issues = [], warnings = []; + const nodeIds = new Set(); + const seen = new Map(); + graph.nodes.forEach((n, i) => { + if (!n.id) { issues.push(`Node[${i}] missing id`); return; } + if (!n.type) issues.push(`Node[${i}] '${n.id}' missing type`); + if (!n.name) issues.push(`Node[${i}] '${n.id}' missing name`); + if (!n.summary) issues.push(`Node[${i}] '${n.id}' missing summary`); + if (!n.tags || !n.tags.length) issues.push(`Node[${i}] '${n.id}' missing tags`); + if (seen.has(n.id)) issues.push(`Duplicate node ID '${n.id}' at indices ${seen.get(n.id)} and ${i}`); + else seen.set(n.id, i); + nodeIds.add(n.id); + }); + graph.edges.forEach((e, i) => { + if (!nodeIds.has(e.source)) issues.push(`Edge[${i}] source '${e.source}' not found`); + if (!nodeIds.has(e.target)) issues.push(`Edge[${i}] target '${e.target}' not found`); + }); + const fileNodes = graph.nodes.filter(n => n.type === 'file').map(n => n.id); + const assigned = new Map(); + (graph.layers || []).forEach(layer => { + (layer.nodeIds || []).forEach(id => { + if (!nodeIds.has(id)) issues.push(`Layer '${layer.id}' refs missing node '${id}'`); + if (assigned.has(id)) issues.push(`Node '${id}' appears in multiple layers`); + assigned.set(id, layer.id); + }); + }); + fileNodes.forEach(id => { + if (!assigned.has(id)) issues.push(`File node '${id}' not in any layer`); + }); + (graph.tour || []).forEach((step, i) => { + (step.nodeIds || []).forEach(id => { + if (!nodeIds.has(id)) issues.push(`Tour step[${i}] refs missing node '${id}'`); + }); + }); + const withEdges = new Set([ + ...graph.edges.map(e => e.source), + ...graph.edges.map(e => e.target) + ]); + graph.nodes.forEach(n => { + if (!withEdges.has(n.id)) warnings.push(`Node '${n.id}' has no edges (orphan)`); + }); + const stats = { + totalNodes: graph.nodes.length, + totalEdges: graph.edges.length, + totalLayers: (graph.layers || []).length, + tourSteps: (graph.tour || []).length, + nodeTypes: graph.nodes.reduce((a, n) => { a[n.type] = (a[n.type]||0)+1; return a; }, {}), + edgeTypes: graph.edges.reduce((a, e) => { a[e.type] = (a[e.type]||0)+1; return a; }, {}) + }; + fs.writeFileSync(outputPath, JSON.stringify({ issues, warnings, stats }, null, 2)); + process.exit(0); +} catch (err) { process.stderr.write(err.message + '\n'); process.exit(1); } +``` + +Execute it: +```bash +node $PROJECT_ROOT/.understand-anything/tmp/ua-inline-validate.js \ + "$PROJECT_ROOT/.understand-anything/intermediate/assembled-graph.json" \ + "$PROJECT_ROOT/.understand-anything/intermediate/review.json" +``` + +If the script exits non-zero, read stderr, fix the script, and retry once. + +--- + +#### `--review` path: full LLM reviewer + +If `--review` IS in `$ARGUMENTS`, dispatch the LLM graph-reviewer subagent as follows: + +Dispatch a subagent using the prompt template at `./graph-reviewer-prompt.md`. Read the template file and pass the full content as the subagent's prompt, appending the following additional context: + +> **Additional context from main session:** +> +> Phase 1 scan results (file inventory): +> ```json +> [list of {path, sizeLines} from scan-result.json] +> ``` +> +> Phase warnings/errors accumulated during analysis: +> - [list any batch failures, skipped files, or warnings from Phases 2-5] +> +> Cross-validate: every file in the scan inventory should have a corresponding `file:` node in the graph. Flag any missing files. Also flag any graph nodes whose `filePath` doesn't appear in the scan inventory. + +Pass these parameters in the dispatch prompt: + +> Validate the knowledge graph at `$PROJECT_ROOT/.understand-anything/intermediate/assembled-graph.json`. +> Project root: `$PROJECT_ROOT` +> Read the file and validate it for completeness and correctness. +> Write output to: `$PROJECT_ROOT/.understand-anything/intermediate/review.json` + +--- + +4. Read `$PROJECT_ROOT/.understand-anything/intermediate/review.json`. + +5. **If `issues` array is non-empty:** + - Review the `issues` list + - Apply automated fixes where possible: + - Remove edges with dangling references + - Fill missing required fields with sensible defaults (e.g., empty `tags` -> `["untagged"]`, empty `summary` -> `"No summary available"`) + - Remove nodes with invalid types + - Re-run the final graph validation after automated fixes + - If critical issues remain after one fix attempt, save the graph anyway but include the warnings in the final report and mark dashboard auto-launch as skipped + +6. **If `issues` array is empty:** Proceed to Phase 7. +``` + +### Step 3: Verify the edit + +Re-read SKILL.md lines 297–380 and confirm: +- Phase 6 step 3 now checks for `--review` flag +- The inline validation script is present and complete +- The `--review` path still dispatches the LLM subagent identically to before +- Steps 4–6 handle the `review.json` output the same way as before + +### Step 4: Commit + +```bash +git add understand-anything-plugin/skills/understand/SKILL.md +git commit -m "perf(understand): gate LLM graph-reviewer behind --review flag, add inline deterministic validation" +``` + +--- + +## Task 2: C4a — Slim Phase 4 (architecture) node payload + +Removes `name` and `languageNotes` from the file node format injected into the architecture-analyzer subagent. These fields are not needed for architectural layer assignment and add unnecessary tokens. + +**Files:** +- Modify: `understand-anything-plugin/skills/understand/SKILL.md` (Phase 4, around line 188–196) + +### Step 1: Locate the Phase 4 dispatch prompt in SKILL.md + +Find the block starting "Pass these parameters in the dispatch prompt:" under Phase 4 (around line 181). Look for: + +``` +> File nodes: +> ```json +> [list of {id, name, filePath, summary, tags} for all file-type nodes] +> ``` +``` + +### Step 2: Update the file node format + +Change the file nodes line from: +``` +> [list of {id, name, filePath, summary, tags} for all file-type nodes] +``` + +To: +``` +> [list of {id, filePath, summary, tags} for all file-type nodes — omit name, complexity, languageNotes] +``` + +### Step 3: Verify + +Re-read Phase 4 and confirm the node format line is updated. Import edges line below it (`[list of edges with type "imports"]`) is unchanged. + +### Step 4: Commit + +```bash +git add understand-anything-plugin/skills/understand/SKILL.md +git commit -m "perf(understand): slim Phase 4 architecture payload — drop redundant node fields" +``` + +--- + +## Task 3: C4b — Slim Phase 5 (tour builder) payload + +Phase 5 currently injects all nodes (including function/class), all edge types, and full layer objects (with nodeIds arrays). Only file nodes, import+calls edges, and slim layers are needed for tour design. This is the largest single payload change, saving ~105,000 tokens on a 500-file project. + +**Files:** +- Modify: `understand-anything-plugin/skills/understand/SKILL.md` (Phase 5, lines 257–270) +- Modify: `understand-anything-plugin/skills/understand/tour-builder-prompt.md` (input schema) + +### Step 1: Locate the Phase 5 dispatch prompt in SKILL.md + +Find the block starting with (around line 257): +``` +> Nodes (summarized): +> ```json +> [list of {id, name, filePath, summary, type} for key nodes] +> ``` +> +> Layers: +> ```json +> [layers from Phase 4] +> ``` +> +> Key edges: +> ```json +> [imports and calls edges] +> ``` +``` + +### Step 2: Replace all three payload sections + +Replace those lines with: + +```markdown +> Nodes (file nodes only): +> ```json +> [list of {id, name, filePath, summary, type} for file-type nodes ONLY — do NOT include function or class nodes] +> ``` +> +> Layers: +> ```json +> [list of {id, name, description} for each layer — omit nodeIds] +> ``` +> +> Edges (imports and calls only): +> ```json +> [list of edges where type is "imports" or "calls" only — exclude all other edge types] +> ``` +``` + +### Step 3: Update tour-builder-prompt.md input schema + +Open `tour-builder-prompt.md` and find the "Script Requirements" section (around line 18–35). The input schema currently shows: +```json +{ + "nodes": [...], + "edges": [...], + "layers": [ + {"id": "layer:core", "name": "Core", "nodeIds": ["file:src/index.ts"]} + ] +} +``` + +Update the layers example to reflect the slim format: +```json +{ + "nodes": [ + {"id": "file:src/index.ts", "type": "file", "name": "index.ts", "filePath": "src/index.ts", "summary": "..."} + ], + "edges": [ + {"source": "file:src/index.ts", "target": "file:src/utils.ts", "type": "imports"} + ], + "layers": [ + {"id": "layer:core", "name": "Core", "description": "Core application logic"} + ] +} +``` + +Also update the "G. Node Summary Index" description (around line 84) to reflect that input nodes are file-type only: + +Find: +``` +**G. Node Summary Index** + +Create a lookup of each node ID to its `summary`, `type`, `tags` (default to empty array `[]` if not present in input), and `name` for easy reference. +``` + +Add a note after it: +``` +Note: input nodes are file-type only. The nodeSummaryIndex will contain only file nodes. +``` + +### Step 4: Verify + +- Re-read SKILL.md Phase 5 payload block: confirms file-only nodes, slim layers (no nodeIds), imports+calls edges only +- Re-read tour-builder-prompt.md input schema: layers no longer have nodeIds + +### Step 5: Commit + +```bash +git add understand-anything-plugin/skills/understand/SKILL.md \ + understand-anything-plugin/skills/understand/tour-builder-prompt.md +git commit -m "perf(understand): slim Phase 5 tour payload — file nodes only, imports+calls edges, slim layers" +``` + +--- + +## Task 4: C3 — Remove language/framework addendums from file-analyzer batches + +The addendums (`languages/typescript.md`, `frameworks/react.md`, etc.) are currently injected into every file-analyzer batch prompt. They cost ~1,300 tokens × N batches. The model already knows these languages. Replace with a compact inline reference table (~150 tokens, paid once, embedded in the base template). + +**Files:** +- Modify: `understand-anything-plugin/skills/understand/SKILL.md` (Phase 2, lines 104–117) +- Modify: `understand-anything-plugin/skills/understand/file-analyzer-prompt.md` (add quick reference section) + +### Step 1: Update the "Build the combined prompt template" block in SKILL.md Phase 2 + +Find the block at lines 104–117: +``` +**Build the combined prompt template:** +1. Read the base template at `./file-analyzer-prompt.md`. +2. **Language context injection:** ... +3. **Framework addendum injection:** ... + +Then for each batch pass the combined template content as the subagent's prompt, appending the following additional context: + +> **Additional context from main session:** +> +> Project: `` — `` +> Frameworks detected: `` +> Languages: `` +> +> Use the language context and framework addendums (appended above) to produce more accurate summaries and better classify file roles. +``` + +Replace it with: +```markdown +**Build the prompt for each batch:** +1. Read the base template at `./file-analyzer-prompt.md`. (Language and framework hints are embedded in the template — do NOT append addendum files for Phase 2 batches. Addendums are reserved for Phase 4.) + +Then for each batch pass the template content as the subagent's prompt, appending the following additional context: + +> **Additional context from main session:** +> +> Project: `` — `` +> Languages: `` +``` + +This removes steps 2 and 3 (the addendum injection loops) entirely from Phase 2. + +### Step 2: Add Language and Framework Quick Reference to file-analyzer-prompt.md + +Open `file-analyzer-prompt.md`. Find the "## Critical Constraints" section near the bottom (around line 299). Insert the following new section **before** "## Critical Constraints": + +```markdown +## Language and Framework Quick Reference + +Use these hints to improve tag and edge accuracy for common patterns. Your training knowledge covers these — this is a fast lookup for the most impactful signals. + +**Tag signals:** + +| Signal | Tags to apply | +|---|---| +| File in `hooks/`, exports a function starting with `use` | `hook`, `service` | +| File in `contexts/` or `context/`, exports a Provider component | `service`, `state` | +| File in `pages/` or `views/` | `ui`, `routing` | +| File in `store/`, `slices/`, `reducers/`, `state/` | `state` | +| File in `services/`, `api/`, `client/` | `service` | +| `__init__.py` at a package root with re-exports | `entry-point`, `barrel` | +| `manage.py` at the project root | `entry-point` | +| `mod.rs` in a directory | `barrel` | +| `main.go` in a `cmd/` subdirectory | `entry-point` | + +**Edge signals:** + +| Pattern | Edge to create | +|---|---| +| React component renders another component in its JSX | `contains` from parent to child | +| Component/hook calls a custom hook (`useX`) | `depends_on` from consumer to hook file | +| Context provider wraps components | `publishes` from provider to context definition | +| Component calls `useContext` or custom context hook | `subscribes` from consumer to context definition | +| Python file uses `from x import y` where x is a project file | `imports` edge (same rule as JS/TS) | +| Go file `import`s an internal package path | `imports` edge to the resolved file | + +``` + +### Step 3: Verify + +- Re-read SKILL.md Phase 2 "Build the prompt" block: steps 2 and 3 (addendum loops) are gone; "Frameworks detected" line in additional context is gone +- Re-read file-analyzer-prompt.md: new "Language and Framework Quick Reference" section appears before Critical Constraints; no reference to addendum files +- Confirm Phase 4 "Build the combined prompt template" (lines 163–167) is **unchanged** — addendums still apply there + +### Step 4: Commit + +```bash +git add understand-anything-plugin/skills/understand/SKILL.md \ + understand-anything-plugin/skills/understand/file-analyzer-prompt.md +git commit -m "perf(understand): remove addendum injection from Phase 2 batches, add compact inline hints to file-analyzer" +``` + +--- + +## Task 5: C1a — Extend scanner to pre-resolve imports + +Adds a new Step 8 to the project scanner script: parse import statements from every source file and resolve relative imports against the discovered file list. The resolved map is written into `scan-result.json` as `importMap`. This is the data that lets us eliminate `allProjectFiles` from every batch in Task 7. + +**Files:** +- Modify: `understand-anything-plugin/skills/understand/project-scanner-prompt.md` + +### Step 1: Add Step 8 to the scanner script requirements + +Open `project-scanner-prompt.md`. Find "**Step 7 -- Project Name**" (around line 100). After its content (the priority list), add a new step: + +```markdown +**Step 8 -- Import Resolution** + +For each file in the discovered source list, extract and resolve relative import statements. The goal is to produce a map from each file's path to the list of project-internal files it imports. External package imports are ignored. + +For each file, read its content and extract import paths using language-appropriate patterns: + +| Language | Import patterns to match | +|---|---| +| TypeScript/JavaScript | `import ... from './...'` or `'../'`, `require('./...')` or `require('../...')` | +| Python | `from .x import y`, `from ..x import y`, `import .x` (relative only) | +| Go | Paths in `import (...)` blocks that start with the module path from `go.mod` | +| Rust | `use crate::`, `use super::`, `mod x` (within the same crate) | +| Java/Kotlin | Not resolvable by path — skip import resolution for these languages | +| Ruby | `require_relative '...'` paths | + +For each extracted import path: +1. Compute the resolved file path relative to project root: + - For relative imports (`./x`, `../x`): resolve from the importing file's directory + - Try these extension variants in order if the import has no extension: `.ts`, `.tsx`, `.js`, `.jsx`, `/index.ts`, `/index.js`, `/index.tsx`, `/index.jsx`, `.py`, `.go`, `.rs`, `.rb` +2. Check if the resolved path exists in the discovered file list +3. If yes: add to this file's resolved imports list +4. If no: skip (external, unresolvable, or dynamic import) + +Output format in the script result: +```json +"importMap": { + "src/index.ts": ["src/utils.ts", "src/config.ts"], + "src/utils.ts": [], + "src/components/App.tsx": ["src/hooks/useAuth.ts", "src/store/index.ts"] +} +``` + +Keys are project-relative paths. Values are arrays of resolved project-relative paths. Every key in the file list must appear in `importMap` (use an empty array `[]` if no imports were resolved). External packages and unresolvable imports are omitted entirely. +``` + +### Step 2: Update the scanner script output format + +Find the "### Script Output Format" section (around line 109) and update the example JSON to include `importMap`: + +Find this in the example: +```json +{ + "scriptCompleted": true, + "name": "project-name", + ... + "estimatedComplexity": "moderate" +} +``` + +Add `importMap` to the example: +```json +{ + "scriptCompleted": true, + "name": "project-name", + "rawDescription": "...", + "readmeHead": "...", + "languages": ["javascript", "typescript"], + "frameworks": ["React", "Vite"], + "files": [ + {"path": "src/index.ts", "language": "typescript", "sizeLines": 150} + ], + "totalFiles": 42, + "estimatedComplexity": "moderate", + "importMap": { + "src/index.ts": ["src/utils.ts", "src/config.ts"], + "src/utils.ts": [] + } +} +``` + +Also update the field documentation list below the example to add: +``` +- `importMap` (object) — map from every source file path to its list of resolved project-internal import paths; empty array if no resolved imports; external packages excluded +``` + +### Step 3: Update the final assembly section to preserve importMap + +Find "## Phase 2 -- Description and Final Assembly" (around line 153). Find the IMPORTANT note: +``` +**IMPORTANT:** The final output must NOT contain the `scriptCompleted`, `rawDescription`, or `readmeHead` fields. +``` + +Update it to: +``` +**IMPORTANT:** The final output must NOT contain the `scriptCompleted`, `rawDescription`, or `readmeHead` fields. All other fields — including `importMap` — MUST be preserved exactly as output by the script. +``` + +Also update the final output example to include `importMap`: +```json +{ + "name": "project-name", + "description": "...", + "languages": ["typescript"], + "frameworks": ["React"], + "files": [...], + "totalFiles": 42, + "estimatedComplexity": "moderate", + "importMap": { + "src/index.ts": ["src/utils.ts"] + } +} +``` + +### Step 4: Verify + +Re-read `project-scanner-prompt.md` and confirm: +- Step 8 is present with full import resolution logic +- Script output format includes `importMap` +- Field documentation includes `importMap` +- Final assembly section preserves `importMap` in output + +### Step 5: Commit + +```bash +git add understand-anything-plugin/skills/understand/project-scanner-prompt.md +git commit -m "perf(understand): extend scanner to pre-resolve imports, output importMap in scan-result.json" +``` + +--- + +## Task 6: C1b — Update file-analyzer to use batchImportData + +Removes `allProjectFiles` from the file-analyzer input schema and replaces it with `batchImportData` (pre-resolved imports for this batch's files only). Updates the extraction script section to skip import resolution entirely (already done by scanner). Updates the edge creation step to use `batchImportData` directly. + +**Files:** +- Modify: `understand-anything-plugin/skills/understand/file-analyzer-prompt.md` + +### Step 1: Update the input JSON schema (Script Requirements, step 1) + +Find the input schema block around line 19: +```json +{ + "projectRoot": "/path/to/project", + "allProjectFiles": ["src/index.ts", "src/utils.ts", "..."], + "batchFiles": [ + {"path": "src/index.ts", "language": "typescript", "sizeLines": 150}, + {"path": "src/utils.ts", "language": "typescript", "sizeLines": 80} + ] +} +``` + +Replace with: +```json +{ + "projectRoot": "/path/to/project", + "batchFiles": [ + {"path": "src/index.ts", "language": "typescript", "sizeLines": 150}, + {"path": "src/utils.ts", "language": "typescript", "sizeLines": 80} + ], + "batchImportData": { + "src/index.ts": ["src/utils.ts", "src/config.ts"], + "src/utils.ts": [] + } +} +``` + +Update the field descriptions: +- Remove: `allProjectFiles` description +- Add: `batchImportData` (object) — map from each batch file's project-relative path to its list of pre-resolved project-internal imports. Produced by the project scanner. Use this directly for import edge creation — do NOT attempt to re-resolve imports yourself. + +### Step 2: Remove the imports extraction from "What the Script Must Extract" + +Find the "**Imports:**" subsection under "What the Script Must Extract" (around lines 49–53): +``` +**Imports:** +- Source module path (exactly as written in the import statement) +- Imported specifiers (named imports, default import, namespace import) +- Line number +- For relative imports (starting with `./` or `../`), compute the resolved path... +``` + +Replace this entire subsection with: +```markdown +**Imports:** +- Do NOT extract imports in the script. Import resolution has already been performed by the project scanner. +- The pre-resolved imports for each file are provided in `batchImportData` in the input JSON. +- Do not include an `imports` field in the script output — import edges will be created in Phase 2 using `batchImportData` directly. +``` + +### Step 3: Update the script output format to remove imports + +Find the `results` array in the script output format (around line 67). The current `imports` array in the output: +```json +"imports": [ + {"source": "./utils", "resolvedPath": "src/utils.ts", "specifiers": ["formatDate"], "line": 1, "isExternal": false}, + {"source": "express", "resolvedPath": null, "specifiers": ["default"], "line": 2, "isExternal": true} +], +``` + +Remove the `imports` array from the script output format entirely. The result for each file should be: +```json +{ + "path": "src/index.ts", + "language": "typescript", + "totalLines": 150, + "nonEmptyLines": 120, + "functions": [...], + "classes": [...], + "exports": [...], + "metrics": { + "importCount": 5, + "exportCount": 3, + "functionCount": 4, + "classCount": 1 + } +} +``` + +Keep `metrics.importCount` (derived from `batchImportData[path].length`) as a useful metric. + +Update the metrics description to say: +``` +- `importCount` (integer) — use `batchImportData[file.path].length` from the input JSON +``` + +### Step 4: Update "Preparing the Script Input" section + +Find the `cat` command around line 113 that creates the input JSON: +```bash +cat > $PROJECT_ROOT/.understand-anything/tmp/ua-file-analyzer-input-.json << 'ENDJSON' +{ + "projectRoot": "", + "allProjectFiles": [], + "batchFiles": [] +} +ENDJSON +``` + +Replace with: +```bash +cat > $PROJECT_ROOT/.understand-anything/tmp/ua-file-analyzer-input-.json << 'ENDJSON' +{ + "projectRoot": "", + "batchFiles": [], + "batchImportData": +} +ENDJSON +``` + +### Step 5: Update Step 3 (Create Edges) — Import edge creation rule + +Find the "**Import edge creation rule:**" in the "Step 3 -- Create Edges" section (around line 213): +``` +**Import edge creation rule:** For each import in the script output where `isExternal` is `false` and `resolvedPath` is non-null, create an `imports` edge from the current file node to `file:`. Do NOT create edges for external package imports. +``` + +Replace with: +```markdown +**Import edge creation rule:** For each resolved path in `batchImportData[filePath]` (provided in the input JSON), create an `imports` edge from the current file node to `file:`. The `batchImportData` values contain only resolved project-internal paths — external packages have already been filtered out. Do NOT attempt to re-resolve imports from source. +``` + +### Step 6: Remove `allProjectFiles` references from Critical Constraints + +Find the last bullet in "## Critical Constraints" (around line 304): +``` +- For import edges, use the script's `resolvedPath` field directly. Do NOT attempt to resolve import paths yourself -- the script already did this deterministically. +``` + +Replace with: +```markdown +- For import edges, use `batchImportData[filePath]` directly from the input JSON. Do NOT attempt to resolve import paths yourself -- the project scanner already did this deterministically. +``` + +### Step 7: Verify + +Re-read `file-analyzer-prompt.md` and confirm: +- Input schema has `batchImportData`, no `allProjectFiles` +- Script "What to Extract" section: imports extraction replaced with "do not extract" +- Script output format: no `imports` array per file +- Preparing the Script Input: cat command has no `allProjectFiles` +- Import edge creation rule: uses `batchImportData` not script output +- Critical Constraints: no reference to `resolvedPath` from script + +### Step 8: Commit + +```bash +git add understand-anything-plugin/skills/understand/file-analyzer-prompt.md +git commit -m "perf(understand): replace allProjectFiles with batchImportData in file-analyzer — import resolution now done by scanner" +``` + +--- + +## Task 7: C1c + C2 — Update SKILL.md Phase 2 orchestration + +Wires up the `importMap` from Phase 1 into per-batch `batchImportData` slices. Increases batch size from 5-10 to 20-30 files. Increases concurrency from 3 to 5. Removes `allProjectFiles` from the dispatch prompt. + +**Files:** +- Modify: `understand-anything-plugin/skills/understand/SKILL.md` (Phase 0, Phase 1, Phase 2) + +### Step 1: Update Phase 1 to note importMap is now in scan-result.json + +Find Phase 1 (around line 62) where it says: +``` +After the subagent completes, read `$PROJECT_ROOT/.understand-anything/intermediate/scan-result.json` to get: +- Project name, description +- Languages, frameworks +- File list with line counts +- Complexity estimate +``` + +Add one item to the list: +``` +- Import map (`importMap`): pre-resolved project-internal imports per file +``` + +Also add a note: +``` +Store `importMap` in memory as `$IMPORT_MAP` for use in Phase 2 batch construction. +``` + +### Step 2: Change batch size and concurrency in Phase 2 + +Find line 100: +``` +Batch the file list from Phase 1 into groups of **5-10 files each** (aim for balanced batch sizes). +``` + +Replace with: +``` +Batch the file list from Phase 1 into groups of **20-30 files each** (aim for ~25 files per batch for balanced sizes). +``` + +Find line 102: +``` +For each batch, dispatch a subagent using the prompt template at `./file-analyzer-prompt.md`. Run up to **3 subagents concurrently** using parallel dispatch. +``` + +Replace with: +``` +For each batch, dispatch a subagent using the prompt template at `./file-analyzer-prompt.md`. Run up to **5 subagents concurrently** using parallel dispatch. +``` + +### Step 3: Add batchImportData construction to the dispatch block + +Find the dispatch prompt block (around lines 119–134): +``` +Fill in batch-specific parameters below and dispatch: + +> Analyze these source files and produce GraphNode and GraphEdge objects. +> Project root: `$PROJECT_ROOT` +> Project: `` +> Languages: `` +> Batch index: `` +> Write output to: `$PROJECT_ROOT/.understand-anything/intermediate/batch-.json` +> +> All project files (for import resolution): +> `` +> +> Files to analyze in this batch: +> 1. `` ( lines) +> ... +``` + +Replace with: +```markdown +Before dispatching each batch, construct `batchImportData` from `$IMPORT_MAP`: +```json +batchImportData = {} +for each file in this batch: + batchImportData[file.path] = $IMPORT_MAP[file.path] ?? [] +``` + +Fill in batch-specific parameters below and dispatch: + +> Analyze these source files and produce GraphNode and GraphEdge objects. +> Project root: `$PROJECT_ROOT` +> Project: `` +> Languages: `` +> Batch index: `` +> Write output to: `$PROJECT_ROOT/.understand-anything/intermediate/batch-.json` +> +> Pre-resolved import data for this batch (use this for all import edge creation — do NOT re-resolve imports from source): +> ```json +> +> ``` +> +> Files to analyze in this batch: +> 1. `` ( lines) +> 2. `` ( lines) +> ... +``` + +### Step 4: Update incremental update path + +Find "### Incremental update path" (around line 140): +``` +Use the changed files list from Phase 0. Batch and dispatch file-analyzer subagents using the same process as above, but only for changed files. +``` + +Update to clarify that batchImportData still applies: +``` +Use the changed files list from Phase 0. Batch and dispatch file-analyzer subagents using the same process as above (20-30 files per batch, up to 5 concurrent, with batchImportData constructed from $IMPORT_MAP), but only for changed files. +``` + +### Step 5: Verify all Phase 2 changes + +Re-read SKILL.md Phase 2 in full and confirm: +- Batch size says "20-30 files" +- Concurrency says "5 subagents concurrently" +- "Build the prompt" block: only step 1 (read base template), no addendum steps +- Additional context block: no "Frameworks detected" line, no addendum reference +- Dispatch prompt: has `batchImportData` injection, no `allProjectFiles` +- Incremental path: mentions batchImportData + +### Step 6: Commit + +```bash +git add understand-anything-plugin/skills/understand/SKILL.md +git commit -m "perf(understand): wire importMap into batchImportData per batch, increase batch size 5-10→20-30, concurrency 3→5" +``` + +--- + +## Task 8: Version bump + +Per project convention, all four version files must stay in sync when changes are pushed. + +**Files:** +- Modify: `understand-anything-plugin/package.json` +- Modify: `.claude-plugin/marketplace.json` +- Modify: `.claude-plugin/plugin.json` +- Modify: `.cursor-plugin/plugin.json` + +### Step 1: Read current version + +```bash +node -e "const p = require('./understand-anything-plugin/package.json'); console.log(p.version)" +``` + +Expected: `1.2.1` (or whatever the current version is). + +### Step 2: Bump patch version in all four files + +New version: `1.2.2` (patch bump — internal optimization, no API changes). + +Update each file: +- `understand-anything-plugin/package.json`: `"version": "1.2.2"` +- `.claude-plugin/marketplace.json`: `"version": "1.2.2"` in `plugins[0]` +- `.claude-plugin/plugin.json`: `"version": "1.2.2"` +- `.cursor-plugin/plugin.json`: `"version": "1.2.2"` + +### Step 3: Verify all four files match + +```bash +grep -r '"version"' understand-anything-plugin/package.json .claude-plugin/marketplace.json .claude-plugin/plugin.json .cursor-plugin/plugin.json +``` + +All four should show `"version": "1.2.2"`. + +### Step 4: Commit + +```bash +git add understand-anything-plugin/package.json \ + .claude-plugin/marketplace.json \ + .claude-plugin/plugin.json \ + .cursor-plugin/plugin.json +git commit -m "chore: bump version to 1.2.2" +``` + +--- + +## Task 9: Build and smoke test + +Verifies all changes work end-to-end by running `/understand --full` against a real project. + +**Files:** None (testing only) + +### Step 1: Build the packages + +```bash +pnpm --filter @understand-anything/core build +pnpm --filter @understand-anything/skill build +``` + +Expected: both build without errors. + +### Step 2: Find installed plugin version and copy to cache + +```bash +ls ~/.claude/plugins/cache/understand-anything/understand-anything/ +``` + +Note the version (e.g., `1.0.1`). Copy local build into the cache: + +```bash +VERSION=$(node -e "const p = require('./understand-anything-plugin/package.json'); console.log(p.version)") +rm -rf ~/.claude/plugins/cache/understand-anything/understand-anything/$VERSION +cp -R ./understand-anything-plugin ~/.claude/plugins/cache/understand-anything/understand-anything/$VERSION +``` + +### Step 3: Smoke test on a small project (~20 files) + +Open a fresh Claude Code session in a small TypeScript project. Run: +``` +/understand --full +``` + +Verify: +- Phases 0–7 complete without errors +- `knowledge-graph.json` is created +- Node count and edge count are reasonable +- Layers and tour are present +- No "allProjectFiles" or addendum errors in the output + +### Step 4: Smoke test on a larger project (~100+ files) + +Run `/understand --full` on a medium/large TypeScript+React project. + +Verify: +- Batch count is ~4-6 (at 20-30 files per batch for 100 files), not 10-20 +- No errors about missing import resolution +- `importMap` is present in `scan-result.json` (check `.understand-anything/intermediate/` before cleanup, or add a temporary debug log) +- Graph quality is comparable to before (summaries are descriptive, layers are correct) + +### Step 5: Test `--review` flag + +Run `/understand --full --review` on the same project. + +Verify: +- Phase 6 now dispatches the LLM graph-reviewer subagent (not the inline script) +- `review.json` is produced with `approved` field +- Pipeline completes normally + +### Step 6: Final commit (if any fixes needed from smoke test) + +```bash +git add -A +git commit -m "fix(understand): smoke test fixes for token reduction changes" +``` + +--- + +## Summary + +| Task | Change | Risk | +|---|---|---| +| 1 | C5: Gate reviewer | Low | +| 2 | C4a: Slim Phase 4 payload | Low | +| 3 | C4b: Slim Phase 5 payload | Low | +| 4 | C3: Remove addendums from batches | Low | +| 5 | C1a: Scanner import resolution | Medium | +| 6 | C1b: File-analyzer uses batchImportData | Medium | +| 7 | C1c+C2: SKILL.md orchestration + batch size | Medium | +| 8 | Version bump | Low | +| 9 | Smoke test | — | + +Tasks 1–4 are independent of Tasks 5–7. They can be shipped separately if needed. Tasks 5, 6, and 7 are tightly coupled (scanner produces importMap → SKILL.md passes batchImportData → file-analyzer consumes it) and must be shipped together. diff --git a/scripts/generate-large-graph.mjs b/scripts/generate-large-graph.mjs new file mode 100644 index 0000000..436063f --- /dev/null +++ b/scripts/generate-large-graph.mjs @@ -0,0 +1,297 @@ +#!/usr/bin/env node +/** + * Generate a large fake knowledge graph for testing. + * + * Usage: + * node scripts/generate-large-graph.mjs [nodeCount] + * node scripts/generate-large-graph.mjs [nodeCount] --messy + * + * Flags: + * --messy Inject LLM-style issues into ~20% of nodes/edges to test the + * dashboard robustness pipeline (Tier 1-3: null fields, wrong cases, + * missing fields, aliases, dangling refs, unrecognizable types). + * + * Default: 3000 nodes. Writes to .understand-anything/knowledge-graph.json + */ + +import { writeFileSync, mkdirSync } from "node:fs"; +import { resolve } from "node:path"; + +const args = process.argv.slice(2); +const MESSY = args.includes("--messy"); +const numArg = args.find((a) => !a.startsWith("--")); +const NODE_COUNT = parseInt(numArg || "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; +} + +// ── Messy injection (--messy flag) ── + +// Tier 1: silent fixes — null optional fields, mixed-case enums +function injectTier1(node) { + const issues = []; + if (Math.random() < 0.5 && node.filePath !== undefined) { + node.filePath = null; // null on optional field + issues.push("null filePath"); + } + if (Math.random() < 0.5) { + node.type = node.type.toUpperCase(); // "FILE", "FUNCTION" + issues.push(`uppercase type "${node.type}"`); + } + if (Math.random() < 0.5) { + node.complexity = node.complexity[0].toUpperCase() + node.complexity.slice(1); // "Simple" + issues.push(`mixed-case complexity "${node.complexity}"`); + } + return issues; +} + +// Tier 2: auto-fixable — missing fields, aliases, string weights +function injectTier2Node(node) { + const issues = []; + const r = Math.random(); + if (r < 0.2) { + delete node.complexity; + issues.push("missing complexity"); + } else if (r < 0.4) { + node.complexity = pick(["low", "easy", "medium", "intermediate", "high", "hard"]); + issues.push(`complexity alias "${node.complexity}"`); + } + if (Math.random() < 0.3) { + delete node.tags; + issues.push("missing tags"); + } + if (Math.random() < 0.2) { + delete node.summary; + issues.push("missing summary"); + } + if (Math.random() < 0.15) { + node.type = pick(["func", "fn", "method", "interface", "struct", "mod", "pkg"]); + issues.push(`type alias "${node.type}"`); + } + return issues; +} + +function injectTier2Edge(edge) { + const issues = []; + if (Math.random() < 0.3) { + edge.weight = String(edge.weight); // string weight + issues.push(`string weight "${edge.weight}"`); + } + if (Math.random() < 0.2) { + delete edge.direction; + issues.push("missing direction"); + } else if (Math.random() < 0.3) { + edge.direction = pick(["to", "outbound", "from", "inbound", "both"]); + issues.push(`direction alias "${edge.direction}"`); + } + if (Math.random() < 0.15) { + edge.type = pick(["extends", "invokes", "uses", "requires", "relates_to"]); + issues.push(`edge type alias "${edge.type}"`); + } + return issues; +} + +// Tier 3: unrecoverable — missing id/name, dangling refs, bad types +function injectTier3Node(node) { + const r = Math.random(); + if (r < 0.4) { + delete node.id; + return "missing id"; + } else if (r < 0.7) { + delete node.name; + return "missing name"; + } else { + node.type = "totally_bogus_type"; + return `unrecognizable type "${node.type}"`; + } +} + +function injectTier3Edge(edge, validNodeIds) { + const r = Math.random(); + if (r < 0.4) { + edge.target = "nonexistent-node-999999"; + return "dangling target ref"; + } else if (r < 0.7) { + edge.source = "nonexistent-node-888888"; + return "dangling source ref"; + } else { + edge.weight = "not_a_number"; + return "non-coercible weight"; + } +} + +function applyMessy(nodes, edges) { + const stats = { tier1: 0, tier2: 0, tier3: 0 }; + + for (const node of nodes) { + const r = Math.random(); + if (r < 0.10) { + // ~10% get Tier 3 issues (will be dropped) + injectTier3Node(node); + stats.tier3++; + } else if (r < 0.30) { + // ~20% get Tier 2 issues (will be auto-corrected) + injectTier2Node(node); + stats.tier2++; + } else if (r < 0.40) { + // ~10% get Tier 1 issues (silently fixed) + injectTier1(node); + stats.tier1++; + } + } + + const validIds = new Set(nodes.filter((n) => n.id).map((n) => n.id)); + for (const edge of edges) { + const r = Math.random(); + if (r < 0.05) { + injectTier3Edge(edge, validIds); + stats.tier3++; + } else if (r < 0.20) { + injectTier2Edge(edge); + stats.tier2++; + } + } + + // Also set tour/layers to null (Tier 1 null-vs-empty) + return stats; +} + +// ── 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); + +let messyStats = null; +if (MESSY) { + messyStats = applyMessy(nodes, edges); +} + +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 ${MESSY ? "robustness" : "performance"} testing.`, + analyzedAt: new Date().toISOString(), + gitCommitHash: "0000000000000000000000000000000000000000", + }, + nodes, + edges, + layers: MESSY && Math.random() < 0.5 ? null : layers, + tour: MESSY && Math.random() < 0.5 ? null : 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${MESSY ? " (messy mode)" : ""}:`); +console.log(` Nodes: ${nodes.length}`); +console.log(` Edges: ${edges.length}`); +console.log(` Layers: ${graph.layers === null ? "null (Tier 1 test)" : layers.length}`); +console.log(` Tour steps: ${graph.tour === null ? "null (Tier 1 test)" : tour.length}`); +if (messyStats) { + console.log(` Injected issues:`); + console.log(` Tier 1 (silent fix): ~${messyStats.tier1} items`); + console.log(` Tier 2 (auto-correct): ~${messyStats.tier2} items`); + console.log(` Tier 3 (will be dropped): ~${messyStats.tier3} items`); +} +console.log(` Written to: ${outPath}`); diff --git a/understand-anything-plugin/agents/knowledge-graph-guide.md b/understand-anything-plugin/agents/knowledge-graph-guide.md index 23193e4..d73f444 100644 --- a/understand-anything-plugin/agents/knowledge-graph-guide.md +++ b/understand-anything-plugin/agents/knowledge-graph-guide.md @@ -36,7 +36,7 @@ The JSON has this top-level shape: | Type | ID Convention | Description | |---|---|---| | `file` | `file:` | Source file | -| `function` | `func::` | Function or method | +| `function` | `function::` | Function or method | | `class` | `class::` | Class, interface, or type | | `module` | `module:` | Logical module or package | | `concept` | `concept:` | Abstract concept or pattern | diff --git a/understand-anything-plugin/package.json b/understand-anything-plugin/package.json index 4504866..59ca16d 100644 --- a/understand-anything-plugin/package.json +++ b/understand-anything-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@understand-anything/skill", - "version": "1.1.1", + "version": "1.2.2", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -16,4 +16,4 @@ "typescript": "^5.7.0", "vitest": "^3.1.0" } -} \ No newline at end of file +} diff --git a/understand-anything-plugin/packages/core/package.json b/understand-anything-plugin/packages/core/package.json index f26319b..1609792 100644 --- a/understand-anything-plugin/packages/core/package.json +++ b/understand-anything-plugin/packages/core/package.json @@ -20,6 +20,10 @@ "./schema": { "types": "./dist/schema.d.ts", "default": "./dist/schema.js" + }, + "./languages": { + "types": "./dist/languages/index.d.ts", + "default": "./dist/languages/index.js" } }, "scripts": { diff --git a/understand-anything-plugin/packages/core/src/__tests__/framework-registry.test.ts b/understand-anything-plugin/packages/core/src/__tests__/framework-registry.test.ts new file mode 100644 index 0000000..3620eb0 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/__tests__/framework-registry.test.ts @@ -0,0 +1,123 @@ +import { describe, it, expect } from "vitest"; +import { FrameworkRegistry } from "../languages/framework-registry.js"; +import { djangoConfig } from "../languages/frameworks/django.js"; +import { reactConfig } from "../languages/frameworks/react.js"; + +describe("FrameworkRegistry", () => { + it("registers and retrieves a framework config by id", () => { + const registry = new FrameworkRegistry(); + registry.register(djangoConfig); + expect(registry.getById("django")?.displayName).toBe("Django"); + }); + + it("retrieves frameworks for a language", () => { + const registry = new FrameworkRegistry(); + registry.register(djangoConfig); + registry.register(reactConfig); + const pythonFrameworks = registry.getForLanguage("python"); + expect(pythonFrameworks).toHaveLength(1); + expect(pythonFrameworks[0].id).toBe("django"); + }); + + it("returns empty array for unknown language", () => { + const registry = new FrameworkRegistry(); + registry.register(djangoConfig); + expect(registry.getForLanguage("haskell")).toEqual([]); + }); + + describe("detectFrameworks", () => { + it("detects Django from requirements.txt", () => { + const registry = new FrameworkRegistry(); + registry.register(djangoConfig); + const detected = registry.detectFrameworks({ + "requirements.txt": "django==4.2\ncelery==5.3\n", + }); + expect(detected).toHaveLength(1); + expect(detected[0].id).toBe("django"); + }); + + it("detects React from package.json", () => { + const registry = new FrameworkRegistry(); + registry.register(reactConfig); + const detected = registry.detectFrameworks({ + "package.json": '{"dependencies": {"react": "^18.2.0", "react-dom": "^18.2.0"}}', + }); + expect(detected).toHaveLength(1); + expect(detected[0].id).toBe("react"); + }); + + it("detection is case-insensitive", () => { + const registry = new FrameworkRegistry(); + registry.register(djangoConfig); + const detected = registry.detectFrameworks({ + "requirements.txt": "Django==4.2\n", + }); + expect(detected).toHaveLength(1); + }); + + it("returns empty array when no frameworks match", () => { + const registry = new FrameworkRegistry(); + registry.register(djangoConfig); + const detected = registry.detectFrameworks({ + "requirements.txt": "requests==2.31\n", + }); + expect(detected).toEqual([]); + }); + + it("returns empty array for empty manifests", () => { + const registry = new FrameworkRegistry(); + registry.register(djangoConfig); + expect(registry.detectFrameworks({})).toEqual([]); + }); + + it("does not duplicate detected frameworks", () => { + const registry = new FrameworkRegistry(); + registry.register(djangoConfig); + const detected = registry.detectFrameworks({ + "requirements.txt": "django==4.2\ndjango==4.2\n", + "pyproject.toml": '[project]\ndependencies = ["django>=4.0"]', + }); + expect(detected).toHaveLength(1); + }); + }); + + it("returns frameworks for all listed languages (cross-language)", () => { + const registry = FrameworkRegistry.createDefault(); + // React lists both typescript and javascript + const tsFrameworks = registry.getForLanguage("typescript"); + const jsFrameworks = registry.getForLanguage("javascript"); + expect(tsFrameworks.some((f) => f.id === "react")).toBe(true); + expect(jsFrameworks.some((f) => f.id === "react")).toBe(true); + }); + + it("does not duplicate on re-registration", () => { + const registry = new FrameworkRegistry(); + registry.register(djangoConfig); + registry.register(djangoConfig); + expect(registry.getForLanguage("python")).toHaveLength(1); + }); + + it("getForLanguage returns a copy, not the internal array", () => { + const registry = new FrameworkRegistry(); + registry.register(djangoConfig); + const result = registry.getForLanguage("python"); + result.push(reactConfig); + expect(registry.getForLanguage("python")).toHaveLength(1); + }); + + describe("createDefault", () => { + it("registers all 10 built-in framework configs", () => { + const registry = FrameworkRegistry.createDefault(); + expect(registry.getAllFrameworks()).toHaveLength(10); + }); + + it("includes frameworks for multiple languages", () => { + const registry = FrameworkRegistry.createDefault(); + expect(registry.getForLanguage("python").length).toBeGreaterThanOrEqual(3); + expect(registry.getForLanguage("typescript").length).toBeGreaterThanOrEqual(2); + expect(registry.getForLanguage("java").length).toBeGreaterThanOrEqual(1); + expect(registry.getForLanguage("ruby").length).toBeGreaterThanOrEqual(1); + expect(registry.getForLanguage("go").length).toBeGreaterThanOrEqual(1); + }); + }); +}); diff --git a/understand-anything-plugin/packages/core/src/__tests__/language-lesson.test.ts b/understand-anything-plugin/packages/core/src/__tests__/language-lesson.test.ts index 7a6d8e7..19ebb0b 100644 --- a/understand-anything-plugin/packages/core/src/__tests__/language-lesson.test.ts +++ b/understand-anything-plugin/packages/core/src/__tests__/language-lesson.test.ts @@ -5,9 +5,10 @@ import { detectLanguageConcepts, } from "../analyzer/language-lesson.js"; import type { GraphNode, GraphEdge } from "../types.js"; +import { typescriptConfig } from "../languages/configs/typescript.js"; const sampleNode: GraphNode = { - id: "func:auth:verifyToken", + id: "function:auth:verifyToken", type: "function", name: "verifyToken", filePath: "src/auth/verify.ts", @@ -19,7 +20,7 @@ const sampleNode: GraphNode = { const sampleEdges: GraphEdge[] = [ { - source: "func:auth:verifyToken", + source: "function:auth:verifyToken", target: "file:src/config.ts", type: "reads_from", direction: "forward", @@ -27,7 +28,7 @@ const sampleEdges: GraphEdge[] = [ }, { source: "file:src/middleware.ts", - target: "func:auth:verifyToken", + target: "function:auth:verifyToken", type: "calls", direction: "forward", weight: 0.8, @@ -51,6 +52,7 @@ describe("language-lesson", () => { sampleNode, sampleEdges, "typescript", + typescriptConfig, ); expect(prompt).toContain("TypeScript"); }); @@ -126,7 +128,7 @@ describe("language-lesson", () => { it("detects middleware pattern", () => { const middlewareNode: GraphNode = { - id: "func:middleware:auth", + id: "function:middleware:auth", type: "function", name: "authMiddleware", filePath: "src/middleware/auth.ts", diff --git a/understand-anything-plugin/packages/core/src/__tests__/language-registry.test.ts b/understand-anything-plugin/packages/core/src/__tests__/language-registry.test.ts new file mode 100644 index 0000000..c3a8b75 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/__tests__/language-registry.test.ts @@ -0,0 +1,91 @@ +import { describe, it, expect } from "vitest"; +import { LanguageRegistry } from "../languages/language-registry.js"; +import { typescriptConfig } from "../languages/configs/typescript.js"; +import { pythonConfig } from "../languages/configs/python.js"; + +describe("LanguageRegistry", () => { + it("registers and retrieves a language config by id", () => { + const registry = new LanguageRegistry(); + registry.register(typescriptConfig); + expect(registry.getById("typescript")).toEqual(typescriptConfig); + }); + + it("retrieves config by file extension", () => { + const registry = new LanguageRegistry(); + registry.register(typescriptConfig); + expect(registry.getByExtension(".ts")?.id).toBe("typescript"); + expect(registry.getByExtension(".tsx")?.id).toBe("typescript"); + }); + + it("retrieves config for a file path", () => { + const registry = new LanguageRegistry(); + registry.register(typescriptConfig); + registry.register(pythonConfig); + expect(registry.getForFile("src/index.ts")?.id).toBe("typescript"); + expect(registry.getForFile("app/models.py")?.id).toBe("python"); + }); + + it("returns null for unknown extensions", () => { + const registry = new LanguageRegistry(); + registry.register(typescriptConfig); + expect(registry.getByExtension(".xyz")).toBeNull(); + expect(registry.getForFile("file.unknown")).toBeNull(); + }); + + it("returns null for files without extensions", () => { + const registry = new LanguageRegistry(); + expect(registry.getForFile("Makefile")).toBeNull(); + }); + + it("lists all registered languages", () => { + const registry = new LanguageRegistry(); + registry.register(typescriptConfig); + registry.register(pythonConfig); + const all = registry.getAllLanguages(); + expect(all).toHaveLength(2); + expect(all.map(c => c.id)).toContain("typescript"); + expect(all.map(c => c.id)).toContain("python"); + }); + + describe("createDefault", () => { + it("registers all 12 built-in language configs", () => { + const registry = LanguageRegistry.createDefault(); + const all = registry.getAllLanguages(); + expect(all.length).toBe(12); + }); + + it("maps all expected extensions", () => { + const registry = LanguageRegistry.createDefault(); + expect(registry.getByExtension(".ts")?.id).toBe("typescript"); + expect(registry.getByExtension(".py")?.id).toBe("python"); + expect(registry.getByExtension(".go")?.id).toBe("go"); + expect(registry.getByExtension(".rs")?.id).toBe("rust"); + expect(registry.getByExtension(".java")?.id).toBe("java"); + expect(registry.getByExtension(".rb")?.id).toBe("ruby"); + expect(registry.getByExtension(".php")?.id).toBe("php"); + expect(registry.getByExtension(".swift")?.id).toBe("swift"); + expect(registry.getByExtension(".kt")?.id).toBe("kotlin"); + expect(registry.getByExtension(".cs")?.id).toBe("csharp"); + expect(registry.getByExtension(".cpp")?.id).toBe("cpp"); + expect(registry.getByExtension(".js")?.id).toBe("javascript"); + }); + + it("has no duplicate extension mappings across configs", () => { + const registry = LanguageRegistry.createDefault(); + const all = registry.getAllLanguages(); + const allExtensions: string[] = []; + for (const config of all) { + allExtensions.push(...config.extensions); + } + const unique = new Set(allExtensions); + expect(unique.size).toBe(allExtensions.length); + }); + + it("every config has at least one concept", () => { + const registry = LanguageRegistry.createDefault(); + for (const config of registry.getAllLanguages()) { + expect(config.concepts.length).toBeGreaterThan(0); + } + }); + }); +}); diff --git a/understand-anything-plugin/packages/core/src/__tests__/schema.test.ts b/understand-anything-plugin/packages/core/src/__tests__/schema.test.ts index ae3fd7e..0504557 100644 --- a/understand-anything-plugin/packages/core/src/__tests__/schema.test.ts +++ b/understand-anything-plugin/packages/core/src/__tests__/schema.test.ts @@ -1,5 +1,12 @@ import { describe, it, expect } from "vitest"; -import { validateGraph } from "../schema.js"; +import { + validateGraph, + normalizeGraph, + sanitizeGraph, + autoFixGraph, + NODE_TYPE_ALIASES, + EDGE_TYPE_ALIASES, +} from "../schema.js"; import type { KnowledgeGraph } from "../types.js"; const validGraph: KnowledgeGraph = { @@ -27,7 +34,7 @@ const validGraph: KnowledgeGraph = { edges: [ { source: "node-1", - target: "node-2", + target: "node-1", type: "imports", direction: "forward", weight: 0.8, @@ -57,56 +64,601 @@ describe("schema validation", () => { expect(result.success).toBe(true); expect(result.data).toBeDefined(); expect(result.data!.version).toBe("1.0.0"); - expect(result.errors).toBeUndefined(); + expect(result.issues).toEqual([]); }); it("rejects graph with missing required fields", () => { - const incomplete = { - version: "1.0.0", - // missing project, nodes, edges, layers, tour - }; - + const incomplete = { version: "1.0.0" }; const result = validateGraph(incomplete); expect(result.success).toBe(false); - expect(result.errors).toBeDefined(); - expect(result.errors!.length).toBeGreaterThan(0); + expect(result.fatal).toBeDefined(); }); - it("rejects node with invalid type", () => { + it("rejects node with invalid type — drops node, fatal if none remain", () => { const graph = structuredClone(validGraph); (graph.nodes[0] as any).type = "invalid_type"; const result = validateGraph(graph); expect(result.success).toBe(false); - expect(result.errors).toBeDefined(); - expect(result.errors!.some((e) => e.includes("type"))).toBe(true); + expect(result.fatal).toContain("No valid nodes"); + expect(result.issues).toContainEqual( + expect.objectContaining({ level: "dropped", category: "invalid-node" }) + ); }); - it("rejects edge with invalid EdgeType", () => { + it("drops edge with invalid EdgeType but loads graph", () => { const graph = structuredClone(validGraph); (graph.edges[0] as any).type = "not_a_real_edge_type"; const result = validateGraph(graph); - expect(result.success).toBe(false); - expect(result.errors).toBeDefined(); - expect(result.errors!.some((e) => e.includes("type"))).toBe(true); + expect(result.success).toBe(true); + expect(result.data!.edges.length).toBe(0); + expect(result.issues).toContainEqual( + expect.objectContaining({ level: "dropped", category: "invalid-edge" }) + ); }); - it("rejects weight out of range (>1)", () => { + it("auto-corrects weight >1 by clamping", () => { const graph = structuredClone(validGraph); graph.edges[0].weight = 1.5; const result = validateGraph(graph); - expect(result.success).toBe(false); - expect(result.errors).toBeDefined(); + expect(result.success).toBe(true); + expect(result.issues).toContainEqual( + expect.objectContaining({ level: "auto-corrected", category: "out-of-range" }) + ); }); - it("rejects weight out of range (<0)", () => { + it("auto-corrects weight <0 by clamping", () => { const graph = structuredClone(validGraph); graph.edges[0].weight = -0.1; const result = validateGraph(graph); + expect(result.success).toBe(true); + expect(result.issues).toContainEqual( + expect.objectContaining({ level: "auto-corrected", category: "out-of-range" }) + ); + }); + + it('normalizes "func" node type to "function"', () => { + const graph = structuredClone(validGraph); + (graph.nodes[0] as any).type = "func"; + + const result = validateGraph(graph); + expect(result.success).toBe(true); + expect(result.data!.nodes[0].type).toBe("function"); + }); + + it('normalizes "fn" node type to "function"', () => { + const graph = structuredClone(validGraph); + (graph.nodes[0] as any).type = "fn"; + + const result = validateGraph(graph); + expect(result.success).toBe(true); + expect(result.data!.nodes[0].type).toBe("function"); + }); + + it('normalizes "method" node type to "function"', () => { + const graph = structuredClone(validGraph); + (graph.nodes[0] as any).type = "method"; + + const result = validateGraph(graph); + expect(result.success).toBe(true); + expect(result.data!.nodes[0].type).toBe("function"); + }); + + it('normalizes "interface" node type to "class"', () => { + const graph = structuredClone(validGraph); + (graph.nodes[0] as any).type = "interface"; + + const result = validateGraph(graph); + expect(result.success).toBe(true); + expect(result.data!.nodes[0].type).toBe("class"); + }); + + it('normalizes "struct" node type to "class"', () => { + const graph = structuredClone(validGraph); + (graph.nodes[0] as any).type = "struct"; + + const result = validateGraph(graph); + expect(result.success).toBe(true); + expect(result.data!.nodes[0].type).toBe("class"); + }); + + it("normalizes multiple aliased node types in one graph", () => { + const graph = structuredClone(validGraph); + (graph.nodes[0] as any).type = "func"; + graph.nodes.push({ + id: "node-2", + type: "file" as any, + name: "utils.ts", + filePath: "src/utils.ts", + lineRange: [1, 30], + summary: "Utility helpers", + tags: ["utils"], + complexity: "simple", + }); + (graph.nodes[1] as any).type = "pkg"; + graph.nodes.push({ + id: "node-3", + type: "file" as any, + name: "MyClass.ts", + filePath: "src/MyClass.ts", + lineRange: [1, 80], + summary: "A class", + tags: ["class"], + complexity: "moderate", + }); + (graph.nodes[2] as any).type = "struct"; + + const result = validateGraph(graph); + expect(result.success).toBe(true); + expect(result.data!.nodes[0].type).toBe("function"); + expect(result.data!.nodes[1].type).toBe("module"); + expect(result.data!.nodes[2].type).toBe("class"); + }); + + it('normalizes "extends" edge type to "inherits"', () => { + const graph = structuredClone(validGraph); + (graph.edges[0] as any).type = "extends"; + + const result = validateGraph(graph); + expect(result.success).toBe(true); + expect(result.data!.edges[0].type).toBe("inherits"); + }); + + it('normalizes "invokes" edge type to "calls"', () => { + const graph = structuredClone(validGraph); + (graph.edges[0] as any).type = "invokes"; + + const result = validateGraph(graph); + expect(result.success).toBe(true); + expect(result.data!.edges[0].type).toBe("calls"); + }); + + it('normalizes "relates_to" edge type to "related"', () => { + const graph = structuredClone(validGraph); + (graph.edges[0] as any).type = "relates_to"; + + const result = validateGraph(graph); + expect(result.success).toBe(true); + expect(result.data!.edges[0].type).toBe("related"); + }); + + it('normalizes "uses" edge type to "depends_on"', () => { + const graph = structuredClone(validGraph); + (graph.edges[0] as any).type = "uses"; + + const result = validateGraph(graph); + expect(result.success).toBe(true); + expect(result.data!.edges[0].type).toBe("depends_on"); + }); + + it('drops "tests" edge type — direction-inverting alias is unsafe', () => { + const graph = structuredClone(validGraph); + (graph.edges[0] as any).type = "tests"; + + const result = validateGraph(graph); + expect(result.success).toBe(true); + expect(result.data!.edges.length).toBe(0); + expect(result.issues).toContainEqual( + expect.objectContaining({ level: "dropped" }) + ); + }); + + it("drops truly invalid edge types after normalization", () => { + const graph = structuredClone(validGraph); + (graph.edges[0] as any).type = "totally_bogus"; + + const result = validateGraph(graph); + expect(result.success).toBe(true); + expect(result.data!.edges.length).toBe(0); + expect(result.issues).toContainEqual( + expect.objectContaining({ level: "dropped" }) + ); + }); + + it("NODE_TYPE_ALIASES values are never alias keys (no chains)", () => { + for (const [alias, target] of Object.entries(NODE_TYPE_ALIASES)) { + expect( + NODE_TYPE_ALIASES, + `chain detected: ${alias} → ${target} → ${NODE_TYPE_ALIASES[target]}`, + ).not.toHaveProperty(target); + } + }); + + it("EDGE_TYPE_ALIASES values are never alias keys (no chains)", () => { + for (const [alias, target] of Object.entries(EDGE_TYPE_ALIASES)) { + expect( + EDGE_TYPE_ALIASES, + `chain detected: ${alias} → ${target} → ${EDGE_TYPE_ALIASES[target]}`, + ).not.toHaveProperty(target); + } + }); +}); + +describe("sanitizeGraph", () => { + it("converts null optional node fields to undefined", () => { + const graph = structuredClone(validGraph); + (graph.nodes[0] as any).filePath = null; + (graph.nodes[0] as any).lineRange = null; + (graph.nodes[0] as any).languageNotes = null; + + const result = sanitizeGraph(graph as any); + const node = (result as any).nodes[0]; + expect(node.filePath).toBeUndefined(); + expect(node.lineRange).toBeUndefined(); + expect(node.languageNotes).toBeUndefined(); + }); + + it("converts null optional edge fields to undefined", () => { + const graph = structuredClone(validGraph); + (graph.edges[0] as any).description = null; + + const result = sanitizeGraph(graph as any); + const edge = (result as any).edges[0]; + expect(edge.description).toBeUndefined(); + }); + + it("lowercases enum-like strings on nodes", () => { + const graph = structuredClone(validGraph); + (graph.nodes[0] as any).type = "FILE"; + (graph.nodes[0] as any).complexity = "Simple"; + + const result = sanitizeGraph(graph as any); + const node = (result as any).nodes[0]; + expect(node.type).toBe("file"); + expect(node.complexity).toBe("simple"); + }); + + it("lowercases enum-like strings on edges", () => { + const graph = structuredClone(validGraph); + (graph.edges[0] as any).type = "IMPORTS"; + (graph.edges[0] as any).direction = "Forward"; + + const result = sanitizeGraph(graph as any); + const edge = (result as any).edges[0]; + expect(edge.type).toBe("imports"); + expect(edge.direction).toBe("forward"); + }); + + it("converts null tour/layers to empty arrays", () => { + const graph = structuredClone(validGraph); + (graph as any).tour = null; + (graph as any).layers = null; + + const result = sanitizeGraph(graph as any); + expect((result as any).tour).toEqual([]); + expect((result as any).layers).toEqual([]); + }); + + it("converts null optional tour step fields to undefined", () => { + const graph = structuredClone(validGraph); + (graph.tour[0] as any).languageLesson = null; + + const result = sanitizeGraph(graph as any); + expect((result as any).tour[0].languageLesson).toBeUndefined(); + }); + + it("passes through non-object node/edge items unchanged", () => { + const graph = { nodes: [null, "garbage", 42], edges: [null], tour: [], layers: [] }; + const result = sanitizeGraph(graph as any); + expect((result as any).nodes).toEqual([null, "garbage", 42]); + expect((result as any).edges).toEqual([null]); + }); +}); + +describe("autoFixGraph", () => { + it("defaults missing complexity to moderate with issue", () => { + const graph = structuredClone(validGraph); + delete (graph.nodes[0] as any).complexity; + + const { data, issues } = autoFixGraph(graph as any); + expect((data as any).nodes[0].complexity).toBe("moderate"); + expect(issues).toContainEqual( + expect.objectContaining({ level: "auto-corrected", category: "missing-field", path: "nodes[0].complexity" }) + ); + }); + + it("maps complexity aliases with issue", () => { + const graph = structuredClone(validGraph); + (graph.nodes[0] as any).complexity = "low"; + + const { data, issues } = autoFixGraph(graph as any); + expect((data as any).nodes[0].complexity).toBe("simple"); + expect(issues.length).toBe(1); + expect(issues[0].level).toBe("auto-corrected"); + }); + + it("maps all complexity aliases correctly", () => { + const mapping: Record = { + low: "simple", easy: "simple", + medium: "moderate", intermediate: "moderate", + high: "complex", hard: "complex", difficult: "complex", + }; + for (const [alias, expected] of Object.entries(mapping)) { + const graph = structuredClone(validGraph); + (graph.nodes[0] as any).complexity = alias; + const { data } = autoFixGraph(graph as any); + expect((data as any).nodes[0].complexity).toBe(expected); + } + }); + + it("defaults missing tags to empty array with issue", () => { + const graph = structuredClone(validGraph); + delete (graph.nodes[0] as any).tags; + + const { data, issues } = autoFixGraph(graph as any); + expect((data as any).nodes[0].tags).toEqual([]); + expect(issues).toContainEqual( + expect.objectContaining({ level: "auto-corrected", category: "missing-field", path: "nodes[0].tags" }) + ); + }); + + it("defaults missing summary to node name with issue", () => { + const graph = structuredClone(validGraph); + delete (graph.nodes[0] as any).summary; + + const { data, issues } = autoFixGraph(graph as any); + expect((data as any).nodes[0].summary).toBe("index.ts"); + expect(issues).toContainEqual( + expect.objectContaining({ level: "auto-corrected", category: "missing-field", path: "nodes[0].summary" }) + ); + }); + + it("defaults missing node type to file with issue", () => { + const graph = structuredClone(validGraph); + delete (graph.nodes[0] as any).type; + + const { data, issues } = autoFixGraph(graph as any); + expect((data as any).nodes[0].type).toBe("file"); + expect(issues).toContainEqual( + expect.objectContaining({ level: "auto-corrected", category: "missing-field", path: "nodes[0].type" }) + ); + }); + + it("defaults missing direction to forward with issue", () => { + const graph = structuredClone(validGraph); + delete (graph.edges[0] as any).direction; + + const { data, issues } = autoFixGraph(graph as any); + expect((data as any).edges[0].direction).toBe("forward"); + expect(issues).toContainEqual( + expect.objectContaining({ level: "auto-corrected", category: "missing-field", path: "edges[0].direction" }) + ); + }); + + it("maps direction aliases with issue", () => { + const mapping: Record = { + to: "forward", outbound: "forward", + from: "backward", inbound: "backward", + both: "bidirectional", mutual: "bidirectional", + }; + for (const [alias, expected] of Object.entries(mapping)) { + const graph = structuredClone(validGraph); + (graph.edges[0] as any).direction = alias; + const { data } = autoFixGraph(graph as any); + expect((data as any).edges[0].direction).toBe(expected); + } + }); + + it("defaults missing weight to 0.5 with issue", () => { + const graph = structuredClone(validGraph); + delete (graph.edges[0] as any).weight; + + const { data, issues } = autoFixGraph(graph as any); + expect((data as any).edges[0].weight).toBe(0.5); + expect(issues).toContainEqual( + expect.objectContaining({ level: "auto-corrected", category: "missing-field", path: "edges[0].weight" }) + ); + }); + + it("coerces string weight to number with issue", () => { + const graph = structuredClone(validGraph); + (graph.edges[0] as any).weight = "0.8"; + + const { data, issues } = autoFixGraph(graph as any); + expect((data as any).edges[0].weight).toBe(0.8); + expect(issues).toContainEqual( + expect.objectContaining({ level: "auto-corrected", category: "type-coercion", path: "edges[0].weight" }) + ); + }); + + it("clamps out-of-range weight with issue", () => { + const graph = structuredClone(validGraph); + (graph.edges[0] as any).weight = 1.5; + + const { data, issues } = autoFixGraph(graph as any); + expect((data as any).edges[0].weight).toBe(1); + expect(issues).toContainEqual( + expect.objectContaining({ level: "auto-corrected", category: "out-of-range", path: "edges[0].weight" }) + ); + }); + + it("defaults missing edge type to depends_on with issue", () => { + const graph = structuredClone(validGraph); + delete (graph.edges[0] as any).type; + + const { data, issues } = autoFixGraph(graph as any); + expect((data as any).edges[0].type).toBe("depends_on"); + expect(issues).toContainEqual( + expect.objectContaining({ level: "auto-corrected", category: "missing-field", path: "edges[0].type" }) + ); + }); + + it("returns no issues for a valid graph", () => { + const { issues } = autoFixGraph(validGraph as any); + expect(issues).toEqual([]); + }); + + it("passes through non-object node/edge items unchanged", () => { + const graph = { nodes: [null, "garbage"], edges: [null], tour: [], layers: [] }; + const { data, issues } = autoFixGraph(graph as any); + expect((data as any).nodes).toEqual([null, "garbage"]); + expect((data as any).edges).toEqual([null]); + expect(issues).toEqual([]); + }); +}); + +describe("permissive validation", () => { + it("drops nodes missing id with dropped issue", () => { + const graph = structuredClone(validGraph); + delete (graph.nodes[0] as any).id; + // Add a second valid node so graph isn't fatal + graph.nodes.push({ + id: "node-2", type: "file", name: "other.ts", + summary: "Other file", tags: ["util"], complexity: "simple", + }); + + const result = validateGraph(graph); + expect(result.success).toBe(true); + expect(result.data!.nodes.length).toBe(1); + expect(result.data!.nodes[0].id).toBe("node-2"); + expect(result.issues).toContainEqual( + expect.objectContaining({ level: "dropped", category: "invalid-node" }) + ); + }); + + it("drops edges referencing non-existent nodes with dropped issue", () => { + const graph = structuredClone(validGraph); + graph.edges[0].target = "non-existent-node"; + + const result = validateGraph(graph); + expect(result.success).toBe(true); + expect(result.data!.edges.length).toBe(0); + expect(result.issues).toContainEqual( + expect.objectContaining({ level: "dropped", category: "invalid-reference" }) + ); + }); + + it("returns fatal when 0 valid nodes remain", () => { + const graph = structuredClone(validGraph); + delete (graph.nodes[0] as any).id; + + const result = validateGraph(graph); expect(result.success).toBe(false); - expect(result.errors).toBeDefined(); + expect(result.fatal).toContain("No valid nodes"); + }); + + it("returns fatal when project metadata is missing", () => { + const graph = structuredClone(validGraph); + delete (graph as any).project; + + const result = validateGraph(graph); + expect(result.success).toBe(false); + expect(result.fatal).toContain("project metadata"); + }); + + it("returns fatal when input is not an object", () => { + const result = validateGraph("not an object"); + expect(result.success).toBe(false); + expect(result.fatal).toContain("Invalid input"); + }); + + it("loads graph with mixed good and bad nodes", () => { + const graph = structuredClone(validGraph); + // Add a good node + graph.nodes.push({ + id: "node-2", type: "function", name: "doThing", + summary: "Does a thing", tags: ["util"], complexity: "moderate", + }); + // Add a bad node (missing id AND name -- unrecoverable) + (graph.nodes as any[]).push({ type: "file", summary: "broken" }); + + const result = validateGraph(graph); + expect(result.success).toBe(true); + expect(result.data!.nodes.length).toBe(2); + expect(result.issues.some((i) => i.level === "dropped")).toBe(true); + }); + + it("filters dangling nodeIds from layers", () => { + const graph = structuredClone(validGraph); + graph.layers[0].nodeIds.push("non-existent-node"); + + const result = validateGraph(graph); + expect(result.success).toBe(true); + expect(result.data!.layers[0].nodeIds).toEqual(["node-1"]); + }); + + it("filters dangling nodeIds from tour steps", () => { + const graph = structuredClone(validGraph); + graph.tour[0].nodeIds.push("non-existent-node"); + + const result = validateGraph(graph); + expect(result.success).toBe(true); + expect(result.data!.tour[0].nodeIds).toEqual(["node-1"]); + }); + + it("returns empty issues array for a perfect graph", () => { + const result = validateGraph(validGraph); + expect(result.success).toBe(true); + expect(result.issues).toEqual([]); + expect(result.errors).toBeUndefined(); + }); + + it("auto-corrects and loads graph that would have failed strict validation", () => { + // Graph with many Tier 2 issues: missing complexity, weight as string, null filePath + const messy = { + version: "1.0.0", + project: validGraph.project, + nodes: [{ + id: "n1", type: "FILE", name: "app.ts", + filePath: null, summary: "App entry", + tags: null, complexity: "HIGH", + }], + edges: [{ + source: "n1", target: "n1", type: "CALLS", + direction: "TO", weight: "0.9", + }], + layers: [{ id: "l1", name: "Core", description: "Core", nodeIds: ["n1"] }], + tour: [], + }; + + const result = validateGraph(messy); + expect(result.success).toBe(true); + expect(result.data!.nodes[0].complexity).toBe("complex"); + expect(result.data!.nodes[0].tags).toEqual([]); + expect(result.data!.edges[0].weight).toBe(0.9); + expect(result.data!.edges[0].direction).toBe("forward"); + expect(result.issues.length).toBeGreaterThan(0); + expect(result.issues.every((i) => i.level === "auto-corrected")).toBe(true); + }); + + it("handles non-parseable string weight by defaulting to 0.5", () => { + const graph = structuredClone(validGraph); + (graph.edges[0] as any).weight = "not_a_number"; + + const result = validateGraph(graph); + expect(result.success).toBe(true); + expect(result.data!.edges[0].weight).toBe(0.5); + expect(result.issues).toContainEqual( + expect.objectContaining({ level: "auto-corrected", category: "type-coercion" }) + ); + }); + + it("returns fatal when edges is present but not an array", () => { + const graph = structuredClone(validGraph) as any; + graph.edges = { source: "node-1", target: "node-1" }; + + const result = validateGraph(graph); + expect(result.success).toBe(false); + expect(result.fatal).toContain('"edges" must be an array'); + expect(result.errors).toContain('"edges" must be an array when present'); + expect(result.issues).toContainEqual( + expect.objectContaining({ + level: "fatal", + category: "invalid-collection", + path: "edges", + }) + ); + }); + + it("preserves deprecated errors for dropped-item callers", () => { + const graph = structuredClone(validGraph); + graph.edges[0].target = "non-existent-node"; + + const result = validateGraph(graph); + expect(result.success).toBe(true); + expect(result.errors).toContain('edges[0]: target "non-existent-node" does not exist in nodes — removed'); }); }); diff --git a/understand-anything-plugin/packages/core/src/analyzer/graph-builder.test.ts b/understand-anything-plugin/packages/core/src/analyzer/graph-builder.test.ts index 224d385..461e252 100644 --- a/understand-anything-plugin/packages/core/src/analyzer/graph-builder.test.ts +++ b/understand-anything-plugin/packages/core/src/analyzer/graph-builder.test.ts @@ -74,14 +74,14 @@ describe("GraphBuilder", () => { expect(fileNode!.type).toBe("file"); expect(fileNode!.summary).toBe("Handles data processing"); - const funcNode = graph.nodes.find((n) => n.id === "func:src/service.ts:processData"); + const funcNode = graph.nodes.find((n) => n.id === "function:src/service.ts:processData"); expect(funcNode).toBeDefined(); expect(funcNode!.type).toBe("function"); expect(funcNode!.name).toBe("processData"); expect(funcNode!.lineRange).toEqual([10, 25]); expect(funcNode!.summary).toBe("Processes raw input data"); - const validateNode = graph.nodes.find((n) => n.id === "func:src/service.ts:validate"); + const validateNode = graph.nodes.find((n) => n.id === "function:src/service.ts:validate"); expect(validateNode).toBeDefined(); expect(validateNode!.summary).toBe("Validates data format"); @@ -120,7 +120,7 @@ describe("GraphBuilder", () => { expect(containsEdges[0]).toMatchObject({ source: "file:src/widget.ts", - target: "func:src/widget.ts:helper", + target: "function:src/widget.ts:helper", type: "contains", direction: "forward", weight: 1, @@ -170,8 +170,8 @@ describe("GraphBuilder", () => { const callEdges = graph.edges.filter((e) => e.type === "calls"); expect(callEdges).toHaveLength(1); expect(callEdges[0]).toMatchObject({ - source: "func:src/index.ts:main", - target: "func:src/utils.ts:helper", + source: "function:src/index.ts:main", + target: "function:src/utils.ts:helper", type: "calls", direction: "forward", }); diff --git a/understand-anything-plugin/packages/core/src/analyzer/graph-builder.ts b/understand-anything-plugin/packages/core/src/analyzer/graph-builder.ts index 9c0c97d..aaa170b 100644 --- a/understand-anything-plugin/packages/core/src/analyzer/graph-builder.ts +++ b/understand-anything-plugin/packages/core/src/analyzer/graph-builder.ts @@ -117,7 +117,7 @@ export class GraphBuilder { // Create function nodes with "contains" edges for (const fn of analysis.functions) { - const funcId = `func:${filePath}:${fn.name}`; + const funcId = `function:${filePath}:${fn.name}`; this.nodes.push({ id: funcId, type: "function", @@ -179,8 +179,8 @@ export class GraphBuilder { calleeFunc: string, ): void { this.edges.push({ - source: `func:${callerFile}:${callerFunc}`, - target: `func:${calleeFile}:${calleeFunc}`, + source: `function:${callerFile}:${callerFunc}`, + target: `function:${calleeFile}:${calleeFunc}`, type: "calls", direction: "forward", weight: 0.8, diff --git a/understand-anything-plugin/packages/core/src/analyzer/language-lesson.ts b/understand-anything-plugin/packages/core/src/analyzer/language-lesson.ts index cd1bc56..53fcc01 100644 --- a/understand-anything-plugin/packages/core/src/analyzer/language-lesson.ts +++ b/understand-anything-plugin/packages/core/src/analyzer/language-lesson.ts @@ -1,11 +1,16 @@ import type { GraphNode, GraphEdge } from "../types.js"; +import type { LanguageConfig } from "../languages/types.js"; export interface LanguageLessonResult { languageNotes: string; concepts: Array<{ name: string; explanation: string }>; } -const CONCEPT_PATTERNS: Record = { +/** + * Base concept patterns that apply across all languages. + * These are merged with language-specific concepts from LanguageConfig. + */ +const BASE_CONCEPT_PATTERNS: Record = { "async/await": ["async", "await", "promise", "asynchronous"], "middleware pattern": ["middleware", "interceptor", "pipe"], "generics": ["generic", "type parameter", "template"], @@ -36,12 +41,35 @@ const CONCEPT_PATTERNS: Record = { "concurrency": ["goroutine", "channel", "thread", "worker", "mutex"], }; +/** + * Build the full concept patterns map by merging base patterns with + * language-specific concepts from a LanguageConfig (if provided). + */ +function buildConceptPatterns( + langConfig?: LanguageConfig | null, +): Record { + const patterns = { ...BASE_CONCEPT_PATTERNS }; + + if (langConfig?.concepts) { + for (const concept of langConfig.concepts) { + if (!patterns[concept]) { + // Use the concept name itself as a keyword for detection + patterns[concept] = [concept.toLowerCase()]; + } + } + } + + return patterns; +} + /** * Detects language concepts present in a graph node based on its tags, summary, and languageNotes. + * When a LanguageConfig is provided, language-specific concepts are also detected. */ export function detectLanguageConcepts( node: GraphNode, language: string, + langConfig?: LanguageConfig | null, ): string[] { const text = [ ...node.tags, @@ -49,9 +77,10 @@ export function detectLanguageConcepts( node.languageNotes?.toLowerCase() ?? "", ].join(" "); + const patterns = buildConceptPatterns(langConfig); const detected: string[] = []; - for (const [concept, keywords] of Object.entries(CONCEPT_PATTERNS)) { + for (const [concept, keywords] of Object.entries(patterns)) { const found = keywords.some((keyword) => text.toLowerCase().includes(keyword.toLowerCase()), ); @@ -63,11 +92,19 @@ export function detectLanguageConcepts( return detected; } -const LANGUAGE_DISPLAY_NAMES: Record = { - typescript: "TypeScript", - javascript: "JavaScript", - coffeescript: "CoffeeScript", -}; +/** + * Get the display name for a language. + * Uses LanguageConfig if provided, otherwise falls back to capitalization. + */ +export function getLanguageDisplayName( + language: string, + langConfig?: LanguageConfig | null, +): string { + if (langConfig?.displayName) { + return langConfig.displayName; + } + return language.charAt(0).toUpperCase() + language.slice(1); +} /** * Builds a prompt that asks an LLM to produce a language-specific lesson for a given node. @@ -76,12 +113,11 @@ export function buildLanguageLessonPrompt( node: GraphNode, edges: GraphEdge[], language: string, + langConfig?: LanguageConfig | null, ): string { - const capitalizedLanguage = - LANGUAGE_DISPLAY_NAMES[language.toLowerCase()] ?? - language.charAt(0).toUpperCase() + language.slice(1); + const capitalizedLanguage = getLanguageDisplayName(language, langConfig); - const concepts = detectLanguageConcepts(node, language); + const concepts = detectLanguageConcepts(node, language, langConfig); const relationships = edges .map((edge) => { diff --git a/understand-anything-plugin/packages/core/src/index.ts b/understand-anything-plugin/packages/core/src/index.ts index be8b001..3c6783e 100644 --- a/understand-anything-plugin/packages/core/src/index.ts +++ b/understand-anything-plugin/packages/core/src/index.ts @@ -1,6 +1,15 @@ export * from "./types.js"; export * from "./persistence/index.js"; -export { KnowledgeGraphSchema, validateGraph, type ValidationResult } from "./schema.js"; +export { + KnowledgeGraphSchema, + validateGraph, + sanitizeGraph, + autoFixGraph, + COMPLEXITY_ALIASES, + DIRECTION_ALIASES, + type ValidationResult, + type GraphIssue, +} from "./schema.js"; export { TreeSitterPlugin } from "./plugins/tree-sitter-plugin.js"; export { GraphBuilder } from "./analyzer/graph-builder.js"; export { @@ -36,6 +45,20 @@ export { type LanguageLessonResult, } from "./analyzer/language-lesson.js"; export { PluginRegistry } from "./plugins/registry.js"; +export { + LanguageRegistry, + FrameworkRegistry, + builtinLanguageConfigs, + builtinFrameworkConfigs, + LanguageConfigSchema, + FrameworkConfigSchema, +} from "./languages/index.js"; +export type { + LanguageConfig, + FrameworkConfig, + TreeSitterConfig, + FilePatternConfig, +} from "./languages/index.js"; export { parsePluginConfig, serializePluginConfig, diff --git a/understand-anything-plugin/packages/core/src/languages/configs/cpp.ts b/understand-anything-plugin/packages/core/src/languages/configs/cpp.ts new file mode 100644 index 0000000..e4aed53 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/cpp.ts @@ -0,0 +1,25 @@ +import type { LanguageConfig } from "../types.js"; + +export const cppConfig = { + id: "cpp", + displayName: "C/C++", + extensions: [".cpp", ".cc", ".cxx", ".c", ".h", ".hpp", ".hxx"], + concepts: [ + "templates", + "RAII", + "smart pointers", + "move semantics", + "operator overloading", + "virtual functions", + "namespaces", + "constexpr", + "lambda expressions", + "STL containers", + ], + filePatterns: { + entryPoints: ["main.cpp", "main.c", "src/main.cpp"], + barrels: [], + tests: ["*_test.cpp", "*_test.cc", "test_*.cpp"], + config: ["CMakeLists.txt", "Makefile", "meson.build"], + }, +} satisfies LanguageConfig; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/csharp.ts b/understand-anything-plugin/packages/core/src/languages/configs/csharp.ts new file mode 100644 index 0000000..ba2a864 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/csharp.ts @@ -0,0 +1,25 @@ +import type { LanguageConfig } from "../types.js"; + +export const csharpConfig = { + id: "csharp", + displayName: "C#", + extensions: [".cs"], + concepts: [ + "LINQ", + "async/await", + "generics", + "properties", + "delegates and events", + "attributes", + "nullable reference types", + "pattern matching", + "records", + "dependency injection", + ], + filePatterns: { + entryPoints: ["Program.cs", "**/Program.cs"], + barrels: [], + tests: ["*Tests.cs", "*Test.cs"], + config: ["*.csproj", "*.sln"], + }, +} satisfies LanguageConfig; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/go.ts b/understand-anything-plugin/packages/core/src/languages/configs/go.ts new file mode 100644 index 0000000..3d0f2a6 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/go.ts @@ -0,0 +1,24 @@ +import type { LanguageConfig } from "../types.js"; + +export const goConfig = { + id: "go", + displayName: "Go", + extensions: [".go"], + concepts: [ + "goroutines", + "channels", + "interfaces", + "struct embedding", + "error handling patterns", + "defer/panic/recover", + "slices", + "pointers", + "concurrency patterns", + ], + filePatterns: { + entryPoints: ["main.go", "cmd/*/main.go"], + barrels: [], + tests: ["*_test.go"], + config: ["go.mod", "go.sum"], + }, +} satisfies LanguageConfig; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/index.ts b/understand-anything-plugin/packages/core/src/languages/configs/index.ts new file mode 100644 index 0000000..f0a7676 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/index.ts @@ -0,0 +1,43 @@ +import type { LanguageConfig } from "../types.js"; +import { typescriptConfig } from "./typescript.js"; +import { javascriptConfig } from "./javascript.js"; +import { pythonConfig } from "./python.js"; +import { goConfig } from "./go.js"; +import { rustConfig } from "./rust.js"; +import { javaConfig } from "./java.js"; +import { rubyConfig } from "./ruby.js"; +import { phpConfig } from "./php.js"; +import { swiftConfig } from "./swift.js"; +import { kotlinConfig } from "./kotlin.js"; +import { cppConfig } from "./cpp.js"; +import { csharpConfig } from "./csharp.js"; + +export const builtinLanguageConfigs: LanguageConfig[] = [ + typescriptConfig, + javascriptConfig, + pythonConfig, + goConfig, + rustConfig, + javaConfig, + rubyConfig, + phpConfig, + swiftConfig, + kotlinConfig, + cppConfig, + csharpConfig, +]; + +export { + typescriptConfig, + javascriptConfig, + pythonConfig, + goConfig, + rustConfig, + javaConfig, + rubyConfig, + phpConfig, + swiftConfig, + kotlinConfig, + cppConfig, + csharpConfig, +}; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/java.ts b/understand-anything-plugin/packages/core/src/languages/configs/java.ts new file mode 100644 index 0000000..cc62fa4 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/java.ts @@ -0,0 +1,29 @@ +import type { LanguageConfig } from "../types.js"; + +export const javaConfig = { + id: "java", + displayName: "Java", + extensions: [".java"], + concepts: [ + "generics", + "annotations", + "interfaces", + "abstract classes", + "streams API", + "lambdas", + "sealed classes", + "records", + "dependency injection", + "checked exceptions", + ], + filePatterns: { + entryPoints: [ + "**/Application.java", + "**/Main.java", + "src/main/java/**/App.java", + ], + barrels: [], + tests: ["*Test.java", "*Tests.java", "*IT.java"], + config: ["pom.xml", "build.gradle", "build.gradle.kts"], + }, +} satisfies LanguageConfig; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/javascript.ts b/understand-anything-plugin/packages/core/src/languages/configs/javascript.ts new file mode 100644 index 0000000..d89cf49 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/javascript.ts @@ -0,0 +1,29 @@ +import type { LanguageConfig } from "../types.js"; + +export const javascriptConfig = { + id: "javascript", + displayName: "JavaScript", + extensions: [".js", ".jsx", ".mjs", ".cjs"], + treeSitter: { + wasmPackage: "tree-sitter-javascript", + wasmFile: "tree-sitter-javascript.wasm", + }, + concepts: [ + "closures", + "prototypes", + "promises", + "async/await", + "event loop", + "destructuring", + "spread operator", + "proxies", + "generators", + "modules (ESM/CJS)", + ], + filePatterns: { + entryPoints: ["index.js", "src/index.js", "main.js"], + barrels: ["index.js"], + tests: ["*.test.js", "*.spec.js"], + config: ["package.json", "jsconfig.json"], + }, +} satisfies LanguageConfig; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/kotlin.ts b/understand-anything-plugin/packages/core/src/languages/configs/kotlin.ts new file mode 100644 index 0000000..f02dc79 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/kotlin.ts @@ -0,0 +1,25 @@ +import type { LanguageConfig } from "../types.js"; + +export const kotlinConfig = { + id: "kotlin", + displayName: "Kotlin", + extensions: [".kt", ".kts"], + concepts: [ + "coroutines", + "data classes", + "sealed classes", + "extension functions", + "null safety", + "delegation", + "DSL builders", + "inline functions", + "companion objects", + "flow", + ], + filePatterns: { + entryPoints: ["**/Application.kt", "**/Main.kt"], + barrels: [], + tests: ["*Test.kt", "*Tests.kt"], + config: ["build.gradle.kts", "build.gradle"], + }, +} satisfies LanguageConfig; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/php.ts b/understand-anything-plugin/packages/core/src/languages/configs/php.ts new file mode 100644 index 0000000..2dab065 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/php.ts @@ -0,0 +1,25 @@ +import type { LanguageConfig } from "../types.js"; + +export const phpConfig = { + id: "php", + displayName: "PHP", + extensions: [".php"], + concepts: [ + "namespaces", + "traits", + "type declarations", + "attributes", + "enums", + "fibers", + "closures", + "magic methods", + "dependency injection", + "middleware", + ], + filePatterns: { + entryPoints: ["index.php", "public/index.php", "artisan"], + barrels: [], + tests: ["*Test.php", "tests/**/*.php"], + config: ["composer.json", "php.ini"], + }, +} satisfies LanguageConfig; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/python.ts b/understand-anything-plugin/packages/core/src/languages/configs/python.ts new file mode 100644 index 0000000..f5fa0b0 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/python.ts @@ -0,0 +1,40 @@ +import type { LanguageConfig } from "../types.js"; + +export const pythonConfig = { + id: "python", + displayName: "Python", + extensions: [".py", ".pyi"], + concepts: [ + "decorators", + "list comprehensions", + "generators", + "context managers", + "type hints", + "dunder methods", + "metaclasses", + "dataclasses", + "async/await", + "descriptors", + "protocols", + ], + filePatterns: { + entryPoints: [ + "main.py", + "manage.py", + "app.py", + "wsgi.py", + "asgi.py", + "run.py", + "__main__.py", + ], + barrels: ["__init__.py"], + tests: ["test_*.py", "*_test.py", "conftest.py"], + config: [ + "pyproject.toml", + "setup.py", + "setup.cfg", + "requirements.txt", + "Pipfile", + ], + }, +} satisfies LanguageConfig; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/ruby.ts b/understand-anything-plugin/packages/core/src/languages/configs/ruby.ts new file mode 100644 index 0000000..f3ed999 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/ruby.ts @@ -0,0 +1,24 @@ +import type { LanguageConfig } from "../types.js"; + +export const rubyConfig = { + id: "ruby", + displayName: "Ruby", + extensions: [".rb", ".rake"], + concepts: [ + "blocks and procs", + "mixins", + "metaprogramming", + "duck typing", + "DSLs", + "monkey patching", + "symbols", + "method_missing", + "open classes", + ], + filePatterns: { + entryPoints: ["config.ru", "app.rb"], + barrels: [], + tests: ["*_test.rb", "*_spec.rb", "spec_helper.rb"], + config: ["Gemfile", "Rakefile"], + }, +} satisfies LanguageConfig; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/rust.ts b/understand-anything-plugin/packages/core/src/languages/configs/rust.ts new file mode 100644 index 0000000..3a6a4bb --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/rust.ts @@ -0,0 +1,27 @@ +import type { LanguageConfig } from "../types.js"; + +export const rustConfig = { + id: "rust", + displayName: "Rust", + extensions: [".rs"], + concepts: [ + "ownership", + "borrowing", + "lifetimes", + "traits", + "pattern matching", + "enums with data", + "error handling (Result/Option)", + "macros", + "async/await", + "unsafe blocks", + "generics", + "closures", + ], + filePatterns: { + entryPoints: ["src/main.rs", "src/lib.rs"], + barrels: ["mod.rs", "lib.rs"], + tests: ["tests/*.rs"], + config: ["Cargo.toml"], + }, +} satisfies LanguageConfig; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/swift.ts b/understand-anything-plugin/packages/core/src/languages/configs/swift.ts new file mode 100644 index 0000000..af0977e --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/swift.ts @@ -0,0 +1,25 @@ +import type { LanguageConfig } from "../types.js"; + +export const swiftConfig = { + id: "swift", + displayName: "Swift", + extensions: [".swift"], + concepts: [ + "optionals", + "protocols", + "extensions", + "generics", + "closures", + "property wrappers", + "result builders", + "actors", + "structured concurrency", + "value types vs reference types", + ], + filePatterns: { + entryPoints: ["Sources/*/main.swift", "App.swift", "AppDelegate.swift"], + barrels: [], + tests: ["*Tests.swift", "Tests/**/*.swift"], + config: ["Package.swift"], + }, +} satisfies LanguageConfig; diff --git a/understand-anything-plugin/packages/core/src/languages/configs/typescript.ts b/understand-anything-plugin/packages/core/src/languages/configs/typescript.ts new file mode 100644 index 0000000..04a884d --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/configs/typescript.ts @@ -0,0 +1,30 @@ +import type { LanguageConfig } from "../types.js"; + +export const typescriptConfig = { + id: "typescript", + displayName: "TypeScript", + extensions: [".ts", ".tsx"], + treeSitter: { + wasmPackage: "tree-sitter-typescript", + wasmFile: "tree-sitter-typescript.wasm", + }, + concepts: [ + "generics", + "type guards", + "discriminated unions", + "utility types", + "decorators", + "enums", + "interfaces", + "type inference", + "mapped types", + "conditional types", + "template literal types", + ], + filePatterns: { + entryPoints: ["src/index.ts", "src/main.ts", "src/App.tsx", "index.ts"], + barrels: ["index.ts"], + tests: ["*.test.ts", "*.spec.ts", "*.test.tsx"], + config: ["tsconfig.json"], + }, +} satisfies LanguageConfig; diff --git a/understand-anything-plugin/packages/core/src/languages/framework-registry.ts b/understand-anything-plugin/packages/core/src/languages/framework-registry.ts new file mode 100644 index 0000000..671d5b5 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/framework-registry.ts @@ -0,0 +1,86 @@ +import { FrameworkConfigSchema } from "./types.js"; +import type { FrameworkConfig } from "./types.js"; +import { builtinFrameworkConfigs } from "./frameworks/index.js"; + +/** + * Registry for framework configurations. Provides detection of frameworks + * from manifest file contents and lookup by id or language. + */ +export class FrameworkRegistry { + private byId = new Map(); + private byLanguage = new Map(); + + register(config: FrameworkConfig): void { + const parsed = FrameworkConfigSchema.parse(config); + + // Prevent duplicate registration + if (this.byId.has(parsed.id)) return; + + this.byId.set(parsed.id, parsed); + + for (const lang of parsed.languages) { + const existing = this.byLanguage.get(lang) ?? []; + existing.push(parsed); + this.byLanguage.set(lang, existing); + } + } + + getById(id: string): FrameworkConfig | null { + return this.byId.get(id) ?? null; + } + + getForLanguage(langId: string): FrameworkConfig[] { + return [...(this.byLanguage.get(langId) ?? [])]; + } + + getAllFrameworks(): FrameworkConfig[] { + return [...this.byId.values()]; + } + + /** + * Detect frameworks from manifest file contents. + * @param manifests - Map of filename to file content (e.g., { "requirements.txt": "django==4.2\n..." }) + * @returns Array of detected FrameworkConfig objects + */ + detectFrameworks(manifests: Record): FrameworkConfig[] { + const detected = new Set(); + const results: FrameworkConfig[] = []; + + for (const config of this.byId.values()) { + if (detected.has(config.id)) continue; + + for (const manifestFile of config.manifestFiles) { + // Match manifest entries by filename (basename match) + const content = Object.entries(manifests).find( + ([key]) => key === manifestFile || key.endsWith(`/${manifestFile}`), + )?.[1]; + + if (!content) continue; + + const contentLower = content.toLowerCase(); + const found = config.detectionKeywords.some((keyword) => + contentLower.includes(keyword.toLowerCase()), + ); + + if (found) { + detected.add(config.id); + results.push(config); + break; + } + } + } + + return results; + } + + /** + * Create a registry pre-populated with all built-in framework configs. + */ + static createDefault(): FrameworkRegistry { + const registry = new FrameworkRegistry(); + for (const config of builtinFrameworkConfigs) { + registry.register(config); + } + return registry; + } +} diff --git a/understand-anything-plugin/packages/core/src/languages/frameworks/django.ts b/understand-anything-plugin/packages/core/src/languages/frameworks/django.ts new file mode 100644 index 0000000..aef5775 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/frameworks/django.ts @@ -0,0 +1,36 @@ +import type { FrameworkConfig } from "../types.js"; + +export const djangoConfig = { + id: "django", + displayName: "Django", + languages: ["python"], + detectionKeywords: [ + "django", + "djangorestframework", + "django-rest-framework", + "django-cors-headers", + "django-filter", + ], + manifestFiles: [ + "requirements.txt", + "pyproject.toml", + "setup.py", + "setup.cfg", + "Pipfile", + ], + promptSnippetPath: "./frameworks/django.md", + entryPoints: ["manage.py", "wsgi.py", "asgi.py"], + layerHints: { + views: "api", + models: "data", + serializers: "api", + urls: "api", + templates: "ui", + migrations: "data", + management: "config", + signals: "service", + admin: "config", + forms: "ui", + templatetags: "utility", + }, +} satisfies FrameworkConfig; diff --git a/understand-anything-plugin/packages/core/src/languages/frameworks/express.ts b/understand-anything-plugin/packages/core/src/languages/frameworks/express.ts new file mode 100644 index 0000000..dd5e26c --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/frameworks/express.ts @@ -0,0 +1,26 @@ +import type { FrameworkConfig } from "../types.js"; + +export const expressConfig = { + id: "express", + displayName: "Express", + languages: ["javascript", "typescript"], + detectionKeywords: ["\"express\":", "express-validator", "express-session"], + manifestFiles: ["package.json"], + promptSnippetPath: "./frameworks/express.md", + entryPoints: [ + "src/index.js", + "src/app.js", + "server.js", + "app.js", + "src/index.ts", + "src/app.ts", + ], + layerHints: { + routes: "api", + controllers: "service", + models: "data", + middleware: "middleware", + services: "service", + db: "data", + }, +} satisfies FrameworkConfig; diff --git a/understand-anything-plugin/packages/core/src/languages/frameworks/fastapi.ts b/understand-anything-plugin/packages/core/src/languages/frameworks/fastapi.ts new file mode 100644 index 0000000..a8043d0 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/frameworks/fastapi.ts @@ -0,0 +1,25 @@ +import type { FrameworkConfig } from "../types.js"; + +export const fastapiConfig = { + id: "fastapi", + displayName: "FastAPI", + languages: ["python"], + detectionKeywords: ["fastapi", "uvicorn", "starlette"], + manifestFiles: [ + "requirements.txt", + "pyproject.toml", + "setup.py", + "setup.cfg", + "Pipfile", + ], + promptSnippetPath: "./frameworks/fastapi.md", + entryPoints: ["main.py", "app.py"], + layerHints: { + routers: "api", + schemas: "types", + models: "data", + dependencies: "service", + crud: "service", + api: "api", + }, +} satisfies FrameworkConfig; diff --git a/understand-anything-plugin/packages/core/src/languages/frameworks/flask.ts b/understand-anything-plugin/packages/core/src/languages/frameworks/flask.ts new file mode 100644 index 0000000..0792893 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/frameworks/flask.ts @@ -0,0 +1,31 @@ +import type { FrameworkConfig } from "../types.js"; + +export const flaskConfig = { + id: "flask", + displayName: "Flask", + languages: ["python"], + detectionKeywords: [ + "flask", + "flask-restful", + "flask-sqlalchemy", + "flask-marshmallow", + "flask-wtf", + ], + manifestFiles: [ + "requirements.txt", + "pyproject.toml", + "setup.py", + "setup.cfg", + "Pipfile", + ], + promptSnippetPath: "./frameworks/flask.md", + entryPoints: ["app.py", "run.py", "wsgi.py"], + layerHints: { + blueprints: "api", + views: "api", + models: "data", + forms: "ui", + templates: "ui", + extensions: "config", + }, +} satisfies FrameworkConfig; diff --git a/understand-anything-plugin/packages/core/src/languages/frameworks/gin.ts b/understand-anything-plugin/packages/core/src/languages/frameworks/gin.ts new file mode 100644 index 0000000..2944113 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/frameworks/gin.ts @@ -0,0 +1,19 @@ +import type { FrameworkConfig } from "../types.js"; + +export const ginConfig = { + id: "gin", + displayName: "Gin", + languages: ["go"], + detectionKeywords: ["github.com/gin-gonic/gin"], + manifestFiles: ["go.mod"], + promptSnippetPath: "./frameworks/gin.md", + entryPoints: ["main.go", "cmd/server/main.go"], + layerHints: { + handlers: "api", + routes: "api", + models: "data", + middleware: "middleware", + services: "service", + repository: "data", + }, +} satisfies FrameworkConfig; diff --git a/understand-anything-plugin/packages/core/src/languages/frameworks/index.ts b/understand-anything-plugin/packages/core/src/languages/frameworks/index.ts new file mode 100644 index 0000000..0b26b60 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/frameworks/index.ts @@ -0,0 +1,38 @@ +import type { FrameworkConfig } from "../types.js"; + +import { djangoConfig } from "./django.js"; +import { fastapiConfig } from "./fastapi.js"; +import { flaskConfig } from "./flask.js"; +import { reactConfig } from "./react.js"; +import { nextjsConfig } from "./nextjs.js"; +import { expressConfig } from "./express.js"; +import { vueConfig } from "./vue.js"; +import { springConfig } from "./spring.js"; +import { railsConfig } from "./rails.js"; +import { ginConfig } from "./gin.js"; + +export const builtinFrameworkConfigs: FrameworkConfig[] = [ + djangoConfig, + fastapiConfig, + flaskConfig, + reactConfig, + nextjsConfig, + expressConfig, + vueConfig, + springConfig, + railsConfig, + ginConfig, +]; + +export { + djangoConfig, + fastapiConfig, + flaskConfig, + reactConfig, + nextjsConfig, + expressConfig, + vueConfig, + springConfig, + railsConfig, + ginConfig, +}; diff --git a/understand-anything-plugin/packages/core/src/languages/frameworks/nextjs.ts b/understand-anything-plugin/packages/core/src/languages/frameworks/nextjs.ts new file mode 100644 index 0000000..6a94bfb --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/frameworks/nextjs.ts @@ -0,0 +1,23 @@ +import type { FrameworkConfig } from "../types.js"; + +export const nextjsConfig = { + id: "nextjs", + displayName: "Next.js", + languages: ["typescript", "javascript"], + detectionKeywords: ["\"next\":", "@next/font", "@next/image"], + manifestFiles: ["package.json"], + promptSnippetPath: "./frameworks/nextjs.md", + entryPoints: [ + "src/app/layout.tsx", + "pages/_app.tsx", + "src/pages/_app.tsx", + ], + layerHints: { + app: "ui", + pages: "ui", + api: "api", + components: "ui", + lib: "service", + middleware: "middleware", + }, +} satisfies FrameworkConfig; diff --git a/understand-anything-plugin/packages/core/src/languages/frameworks/rails.ts b/understand-anything-plugin/packages/core/src/languages/frameworks/rails.ts new file mode 100644 index 0000000..7a5e82f --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/frameworks/rails.ts @@ -0,0 +1,28 @@ +import type { FrameworkConfig } from "../types.js"; + +export const railsConfig = { + id: "rails", + displayName: "Ruby on Rails", + languages: ["ruby"], + detectionKeywords: [ + "rails", + "railties", + "actionpack", + "activerecord", + "actionview", + ], + manifestFiles: ["Gemfile"], + promptSnippetPath: "./frameworks/rails.md", + entryPoints: ["config.ru", "bin/rails"], + layerHints: { + controllers: "api", + models: "data", + views: "ui", + helpers: "utility", + mailers: "service", + jobs: "service", + channels: "service", + middleware: "middleware", + lib: "service", + }, +} satisfies FrameworkConfig; diff --git a/understand-anything-plugin/packages/core/src/languages/frameworks/react.ts b/understand-anything-plugin/packages/core/src/languages/frameworks/react.ts new file mode 100644 index 0000000..c5830ad --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/frameworks/react.ts @@ -0,0 +1,19 @@ +import type { FrameworkConfig } from "../types.js"; + +export const reactConfig = { + id: "react", + displayName: "React", + languages: ["typescript", "javascript"], + detectionKeywords: ["react", "react-dom", "@types/react"], + manifestFiles: ["package.json"], + promptSnippetPath: "./frameworks/react.md", + entryPoints: ["src/App.tsx", "src/App.jsx", "src/index.tsx", "src/main.tsx"], + layerHints: { + components: "ui", + hooks: "service", + pages: "ui", + contexts: "service", + utils: "utility", + lib: "service", + }, +} satisfies FrameworkConfig; diff --git a/understand-anything-plugin/packages/core/src/languages/frameworks/spring.ts b/understand-anything-plugin/packages/core/src/languages/frameworks/spring.ts new file mode 100644 index 0000000..557353d --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/frameworks/spring.ts @@ -0,0 +1,27 @@ +import type { FrameworkConfig } from "../types.js"; + +export const springConfig = { + id: "spring", + displayName: "Spring Boot", + languages: ["java", "kotlin"], + detectionKeywords: [ + "spring-boot", + "spring-boot-starter", + "spring-web", + "spring-data", + "org.springframework", + ], + manifestFiles: ["pom.xml", "build.gradle", "build.gradle.kts"], + promptSnippetPath: "./frameworks/spring.md", + entryPoints: ["**/Application.java", "**/App.java"], + layerHints: { + controller: "api", + service: "service", + repository: "data", + model: "data", + entity: "data", + config: "config", + dto: "types", + security: "middleware", + }, +} satisfies FrameworkConfig; diff --git a/understand-anything-plugin/packages/core/src/languages/frameworks/vue.ts b/understand-anything-plugin/packages/core/src/languages/frameworks/vue.ts new file mode 100644 index 0000000..78ead2f --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/frameworks/vue.ts @@ -0,0 +1,19 @@ +import type { FrameworkConfig } from "../types.js"; + +export const vueConfig = { + id: "vue", + displayName: "Vue", + languages: ["typescript", "javascript"], + detectionKeywords: ["vue", "@vue/cli-service", "nuxt", "vite-plugin-vue"], + manifestFiles: ["package.json"], + promptSnippetPath: "./frameworks/vue.md", + entryPoints: ["src/main.ts", "src/App.vue", "src/main.js"], + layerHints: { + components: "ui", + views: "ui", + store: "service", + composables: "service", + router: "config", + plugins: "config", + }, +} satisfies FrameworkConfig; diff --git a/understand-anything-plugin/packages/core/src/languages/index.ts b/understand-anything-plugin/packages/core/src/languages/index.ts new file mode 100644 index 0000000..7fab61b --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/index.ts @@ -0,0 +1,22 @@ +// Types +export type { + LanguageConfig, + TreeSitterConfig, + FilePatternConfig, + FrameworkConfig, +} from "./types.js"; + +export { + LanguageConfigSchema, + TreeSitterConfigSchema, + FilePatternConfigSchema, + FrameworkConfigSchema, +} from "./types.js"; + +// Registries +export { LanguageRegistry } from "./language-registry.js"; +export { FrameworkRegistry } from "./framework-registry.js"; + +// Built-in configs +export { builtinLanguageConfigs } from "./configs/index.js"; +export { builtinFrameworkConfigs } from "./frameworks/index.js"; diff --git a/understand-anything-plugin/packages/core/src/languages/language-registry.ts b/understand-anything-plugin/packages/core/src/languages/language-registry.ts new file mode 100644 index 0000000..542cd1d --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/language-registry.ts @@ -0,0 +1,53 @@ +import { LanguageConfigSchema } from "./types.js"; +import type { LanguageConfig } from "./types.js"; +import { builtinLanguageConfigs } from "./configs/index.js"; + +/** + * Registry for language configurations. Maps language ids and file extensions + * to their corresponding LanguageConfig objects. + */ +export class LanguageRegistry { + private byId = new Map(); + private byExtension = new Map(); + + register(config: LanguageConfig): void { + const parsed = LanguageConfigSchema.parse(config); + this.byId.set(parsed.id, parsed); + for (const ext of parsed.extensions) { + // Normalize: strip leading dot if present for lookup consistency + const key = ext.startsWith(".") ? ext : `.${ext}`; + this.byExtension.set(key, parsed); + } + } + + getById(id: string): LanguageConfig | null { + return this.byId.get(id) ?? null; + } + + getByExtension(ext: string): LanguageConfig | null { + const key = (ext.startsWith(".") ? ext : `.${ext}`).toLowerCase(); + return this.byExtension.get(key) ?? null; + } + + getForFile(filePath: string): LanguageConfig | null { + const lastDot = filePath.lastIndexOf("."); + if (lastDot === -1) return null; + const ext = filePath.slice(lastDot).toLowerCase(); + return this.getByExtension(ext); + } + + getAllLanguages(): LanguageConfig[] { + return [...this.byId.values()]; + } + + /** + * Create a registry pre-populated with all built-in language configs. + */ + static createDefault(): LanguageRegistry { + const registry = new LanguageRegistry(); + for (const config of builtinLanguageConfigs) { + registry.register(config); + } + return registry; + } +} diff --git a/understand-anything-plugin/packages/core/src/languages/types.ts b/understand-anything-plugin/packages/core/src/languages/types.ts new file mode 100644 index 0000000..7d06c3a --- /dev/null +++ b/understand-anything-plugin/packages/core/src/languages/types.ts @@ -0,0 +1,49 @@ +import { z } from "zod"; + +// Tree-sitter grammar configuration for a language. +// Only wasmPackage and wasmFile are needed for grammar loading. +// The extraction logic in tree-sitter-plugin.ts is currently TS/JS-specific; +// when grammars for other languages are added, language-specific extractors +// should be registered via the customAnalyzer escape hatch. +export const TreeSitterConfigSchema = z.object({ + wasmPackage: z.string(), + wasmFile: z.string(), +}); + +export type TreeSitterConfig = z.infer; + +// File pattern conventions for a language +export const FilePatternConfigSchema = z.object({ + entryPoints: z.array(z.string()), + barrels: z.array(z.string()), + tests: z.array(z.string()), + config: z.array(z.string()), +}); + +export type FilePatternConfig = z.infer; + +// Complete language configuration +export const LanguageConfigSchema = z.object({ + id: z.string().min(1), + displayName: z.string().min(1), + extensions: z.array(z.string()).min(1), + treeSitter: TreeSitterConfigSchema.optional(), + concepts: z.array(z.string()), + filePatterns: FilePatternConfigSchema, +}); + +export type LanguageConfig = z.infer; + +// Framework configuration +export const FrameworkConfigSchema = z.object({ + id: z.string().min(1), + displayName: z.string().min(1), + languages: z.array(z.string().min(1)).min(1), + detectionKeywords: z.array(z.string()).min(1), + manifestFiles: z.array(z.string()).min(1), + promptSnippetPath: z.string().min(1), + entryPoints: z.array(z.string()).optional(), + layerHints: z.record(z.string(), z.string()).optional(), +}); + +export type FrameworkConfig = z.infer; diff --git a/understand-anything-plugin/packages/core/src/persistence/index.ts b/understand-anything-plugin/packages/core/src/persistence/index.ts index 2d3a6eb..b553cfd 100644 --- a/understand-anything-plugin/packages/core/src/persistence/index.ts +++ b/understand-anything-plugin/packages/core/src/persistence/index.ts @@ -36,7 +36,7 @@ export function loadGraph( const result = validateGraph(data); if (!result.success) { throw new Error( - `Invalid knowledge graph: ${result.errors!.join("; ")}`, + `Invalid knowledge graph: ${result.fatal ?? "unknown error"}`, ); } return result.data as KnowledgeGraph; diff --git a/understand-anything-plugin/packages/core/src/persistence/persistence.test.ts b/understand-anything-plugin/packages/core/src/persistence/persistence.test.ts index ce159d4..97681bc 100644 --- a/understand-anything-plugin/packages/core/src/persistence/persistence.test.ts +++ b/understand-anything-plugin/packages/core/src/persistence/persistence.test.ts @@ -41,7 +41,7 @@ describe("persistence", () => { edges: [ { source: "node-1", - target: "node-2", + target: "node-1", type: "imports", direction: "forward", weight: 0.8, diff --git a/understand-anything-plugin/packages/core/src/plugins/registry.ts b/understand-anything-plugin/packages/core/src/plugins/registry.ts index 324a802..71233fe 100644 --- a/understand-anything-plugin/packages/core/src/plugins/registry.ts +++ b/understand-anything-plugin/packages/core/src/plugins/registry.ts @@ -1,30 +1,21 @@ import type { AnalyzerPlugin, StructuralAnalysis, ImportResolution } from "../types.js"; - -const EXTENSION_TO_LANGUAGE: Record = { - ts: "typescript", - tsx: "typescript", - js: "javascript", - jsx: "javascript", - py: "python", - go: "go", - rs: "rust", - rb: "ruby", - java: "java", - kt: "kotlin", - cs: "csharp", - cpp: "cpp", - c: "c", - swift: "swift", - php: "php", -}; +import { LanguageRegistry } from "../languages/language-registry.js"; /** * Registry for analyzer plugins. Maps languages to plugins and provides * a unified interface for analyzing files across languages. + * + * Uses LanguageRegistry for extension-to-language mapping instead of + * a hardcoded lookup table. */ export class PluginRegistry { private plugins: AnalyzerPlugin[] = []; private languageMap = new Map(); + private languageRegistry: LanguageRegistry; + + constructor(languageRegistry?: LanguageRegistry) { + this.languageRegistry = languageRegistry ?? LanguageRegistry.createDefault(); + } register(plugin: AnalyzerPlugin): void { this.plugins.push(plugin); @@ -50,11 +41,16 @@ export class PluginRegistry { } getPluginForFile(filePath: string): AnalyzerPlugin | null { - const ext = filePath.split(".").pop()?.toLowerCase(); - if (!ext) return null; - const language = EXTENSION_TO_LANGUAGE[ext]; - if (!language) return null; - return this.getPluginForLanguage(language); + const langConfig = this.languageRegistry.getForFile(filePath); + if (!langConfig) return null; + return this.getPluginForLanguage(langConfig.id); + } + + /** + * Get the language id for a file path using the language registry. + */ + getLanguageForFile(filePath: string): string | null { + return this.languageRegistry.getForFile(filePath)?.id ?? null; } analyzeFile(filePath: string, content: string): StructuralAnalysis | null { diff --git a/understand-anything-plugin/packages/core/src/plugins/tree-sitter-plugin.ts b/understand-anything-plugin/packages/core/src/plugins/tree-sitter-plugin.ts index 81897c7..ff0dbda 100644 --- a/understand-anything-plugin/packages/core/src/plugins/tree-sitter-plugin.ts +++ b/understand-anything-plugin/packages/core/src/plugins/tree-sitter-plugin.ts @@ -6,6 +6,7 @@ import type { ImportResolution, CallGraphEntry, } from "../types.js"; +import type { LanguageConfig } from "../languages/types.js"; // web-tree-sitter uses CJS internally; we need createRequire for .wasm resolution const require = createRequire(import.meta.url); @@ -14,23 +15,6 @@ type TreeSitterParser = import("web-tree-sitter").Parser; type TreeSitterLanguage = import("web-tree-sitter").Language; type TreeSitterNode = import("web-tree-sitter").Node; -function languageKeyFromPath(filePath: string): string { - const ext = extname(filePath).toLowerCase(); - switch (ext) { - case ".ts": - return "typescript"; - case ".tsx": - return "tsx"; - case ".js": - case ".mjs": - case ".cjs": - case ".jsx": - return "javascript"; - default: - throw new Error(`Unsupported file extension: ${ext}`); - } -} - /** * Recursively traverse an AST tree, calling the visitor for each node. */ @@ -155,17 +139,77 @@ function extractImportSpecifiers( return specifiers; } +/** + * Config-driven tree-sitter plugin. + * + * Accepts LanguageConfig objects to determine which languages to support + * and how to load their WASM grammars. Currently provides deep structural + * analysis for TypeScript/JavaScript; other languages with tree-sitter configs + * get basic function/class/import extraction. + * + * Languages without tree-sitter configs are gracefully skipped (the LLM + * agent handles analysis for those). + */ export class TreeSitterPlugin implements AnalyzerPlugin { readonly name = "tree-sitter"; - readonly languages = ["typescript", "javascript"]; + readonly languages: string[]; + + private configs: LanguageConfig[]; // Pre-loaded parser constructor and languages (set by init()) private _ParserClass: | (new () => TreeSitterParser) | null = null; private _languages = new Map(); + private _extensionToLang = new Map(); private _initialized = false; + /** + * Create a TreeSitterPlugin with the given language configs. + * Only configs that have a `treeSitter` field will be loaded. + * If no configs are provided, defaults to TypeScript and JavaScript. + */ + constructor(configs?: LanguageConfig[]) { + if (configs) { + this.configs = configs.filter((c) => c.treeSitter); + } else { + // Default: TS/JS for backward compatibility + this.configs = []; + } + + // Derive supported languages and extension map from configs + const langs: string[] = []; + for (const config of this.configs) { + langs.push(config.id); + for (const ext of config.extensions) { + const key = ext.startsWith(".") ? ext : `.${ext}`; + this._extensionToLang.set(key, config.id); + } + } + + // Fallback for backward compat when no configs provided + if (langs.length === 0) { + langs.push("typescript", "javascript"); + this._extensionToLang.set(".ts", "typescript"); + this._extensionToLang.set(".tsx", "typescript"); + this._extensionToLang.set(".js", "javascript"); + this._extensionToLang.set(".mjs", "javascript"); + this._extensionToLang.set(".cjs", "javascript"); + this._extensionToLang.set(".jsx", "javascript"); + } + + this.languages = langs; + } + + private languageKeyFromPath(filePath: string): string | null { + const ext = extname(filePath).toLowerCase(); + + // Special case: .tsx needs its own grammar + if (ext === ".tsx") return "tsx"; + + return this._extensionToLang.get(ext) ?? null; + } + /** * Initialize the plugin by loading the WASM module and all language grammars. * Must be called (and awaited) before any synchronous methods. @@ -180,26 +224,68 @@ export class TreeSitterPlugin implements AnalyzerPlugin { await ParserCls.init(); this._ParserClass = ParserCls as unknown as new () => TreeSitterParser; - // Pre-load all supported language grammars - const tsWasm = require.resolve( - "tree-sitter-typescript/tree-sitter-typescript.wasm", - ); - const tsxWasm = require.resolve( - "tree-sitter-typescript/tree-sitter-tsx.wasm", - ); - const jsWasm = require.resolve( - "tree-sitter-javascript/tree-sitter-javascript.wasm", - ); + if (this.configs.length > 0) { + // Load grammars from configs + const loadPromises: Promise[] = []; - const [tsLang, tsxLang, jsLang] = await Promise.all([ - LanguageCls.load(tsWasm), - LanguageCls.load(tsxWasm), - LanguageCls.load(jsWasm), - ]); + for (const config of this.configs) { + if (!config.treeSitter) continue; + + const loadGrammar = async () => { + try { + const wasmPath = require.resolve( + `${config.treeSitter!.wasmPackage}/${config.treeSitter!.wasmFile}`, + ); + const lang = await LanguageCls.load(wasmPath); + this._languages.set(config.id, lang); + + // Special handling for TypeScript: also load TSX grammar + if (config.id === "typescript") { + try { + const tsxWasm = require.resolve( + `${config.treeSitter!.wasmPackage}/tree-sitter-tsx.wasm`, + ); + const tsxLang = await LanguageCls.load(tsxWasm); + this._languages.set("tsx", tsxLang); + } catch { + // TSX grammar not available; .tsx files will fall back to TS grammar + } + } + } catch { + // Grammar not available — this language will be skipped gracefully + console.debug?.( + `tree-sitter: Could not load grammar for ${config.id}, skipping structural analysis`, + ); + } + }; + + loadPromises.push(loadGrammar()); + } + + await Promise.all(loadPromises); + } else { + // Legacy fallback: load TS/JS grammars directly + const tsWasm = require.resolve( + "tree-sitter-typescript/tree-sitter-typescript.wasm", + ); + const tsxWasm = require.resolve( + "tree-sitter-typescript/tree-sitter-tsx.wasm", + ); + const jsWasm = require.resolve( + "tree-sitter-javascript/tree-sitter-javascript.wasm", + ); + + const [tsLang, tsxLang, jsLang] = await Promise.all([ + LanguageCls.load(tsWasm), + LanguageCls.load(tsxWasm), + LanguageCls.load(jsWasm), + ]); + + this._languages.set("typescript", tsLang); + this._languages.set("tsx", tsxLang); + this._languages.set("javascript", jsLang); + } - this._languages.set("typescript", tsLang); - this._languages.set("tsx", tsxLang); - this._languages.set("javascript", jsLang); this._initialized = true; } @@ -207,16 +293,18 @@ export class TreeSitterPlugin implements AnalyzerPlugin { * Create a parser set to the appropriate language for the given file. * This is synchronous because all languages are pre-loaded during init(). */ - private getParser(filePath: string): TreeSitterParser { + private getParser(filePath: string): TreeSitterParser | null { if (!this._initialized || !this._ParserClass) { throw new Error( "TreeSitterPlugin.init() must be called before use", ); } - const langKey = languageKeyFromPath(filePath); + const langKey = this.languageKeyFromPath(filePath); + if (!langKey) return null; const lang = this._languages.get(langKey); if (!lang) { - throw new Error(`Language not loaded: ${langKey}`); + // Language grammar not loaded — graceful degradation + return null; } const parser = new this._ParserClass(); parser.setLanguage(lang); @@ -228,6 +316,10 @@ export class TreeSitterPlugin implements AnalyzerPlugin { content: string, ): StructuralAnalysis { const parser = this.getParser(filePath); + if (!parser) { + return { functions: [], classes: [], imports: [], exports: [] }; + } + const tree = parser.parse(content); if (!tree) { parser.delete(); @@ -290,6 +382,8 @@ export class TreeSitterPlugin implements AnalyzerPlugin { content: string, ): CallGraphEntry[] { const parser = this.getParser(filePath); + if (!parser) return []; + const tree = parser.parse(content); if (!tree) { parser.delete(); diff --git a/understand-anything-plugin/packages/core/src/schema.ts b/understand-anything-plugin/packages/core/src/schema.ts index 2b66b1d..18ccbb6 100644 --- a/understand-anything-plugin/packages/core/src/schema.ts +++ b/understand-anything-plugin/packages/core/src/schema.ts @@ -9,6 +9,261 @@ export const EdgeTypeSchema = z.enum([ "related", "similar_to", // Semantic ]); +// Aliases that LLMs commonly generate instead of canonical node types +export const NODE_TYPE_ALIASES: Record = { + func: "function", + fn: "function", + method: "function", + interface: "class", + struct: "class", + mod: "module", + pkg: "module", + package: "module", +}; + +// Aliases that LLMs commonly generate instead of canonical edge types +export const EDGE_TYPE_ALIASES: Record = { + extends: "inherits", + invokes: "calls", + invoke: "calls", + uses: "depends_on", + requires: "depends_on", + relates_to: "related", + related_to: "related", + similar: "similar_to", + import: "imports", + export: "exports", + contain: "contains", + publish: "publishes", + subscribe: "subscribes", +}; + +// Aliases for complexity values LLMs commonly generate +export const COMPLEXITY_ALIASES: Record = { + low: "simple", + easy: "simple", + medium: "moderate", + intermediate: "moderate", + high: "complex", + hard: "complex", + difficult: "complex", +}; + +// Aliases for direction values LLMs commonly generate +export const DIRECTION_ALIASES: Record = { + to: "forward", + outbound: "forward", + from: "backward", + inbound: "backward", + both: "bidirectional", + mutual: "bidirectional", +}; + +export function sanitizeGraph(data: Record): Record { + const result = { ...data }; + + // Null → empty array for top-level collections + if (data.tour === null || data.tour === undefined) result.tour = []; + if (data.layers === null || data.layers === undefined) result.layers = []; + + // Sanitize nodes + if (Array.isArray(data.nodes)) { + result.nodes = (data.nodes as Record[]).map((node) => { + if (typeof node !== "object" || node === null) return node; + const n = { ...node }; + // Null → undefined for optional fields + if (n.filePath === null) delete n.filePath; + if (n.lineRange === null) delete n.lineRange; + if (n.languageNotes === null) delete n.languageNotes; + // Lowercase enum-like strings + if (typeof n.type === "string") n.type = n.type.toLowerCase(); + if (typeof n.complexity === "string") n.complexity = n.complexity.toLowerCase(); + return n; + }); + } + + // Sanitize edges + if (Array.isArray(data.edges)) { + result.edges = (data.edges as Record[]).map((edge) => { + if (typeof edge !== "object" || edge === null) return edge; + const e = { ...edge }; + if (e.description === null) delete e.description; + if (typeof e.type === "string") e.type = e.type.toLowerCase(); + if (typeof e.direction === "string") e.direction = e.direction.toLowerCase(); + return e; + }); + } + + // Sanitize tour steps + if (Array.isArray(result.tour)) { + result.tour = (result.tour as Record[]).map((step) => { + if (typeof step !== "object" || step === null) return step; + const s = { ...step }; + if (s.languageLesson === null) delete s.languageLesson; + return s; + }); + } + + return result; +} + +export function autoFixGraph(data: Record): { + data: Record; + issues: GraphIssue[]; +} { + const issues: GraphIssue[] = []; + const result = { ...data }; + + if (Array.isArray(data.nodes)) { + result.nodes = (data.nodes as Record[]).map((node, i) => { + if (typeof node !== "object" || node === null) return node; + const n = { ...node }; + const name = (n.name as string) || (n.id as string) || `index ${i}`; + + // Missing or empty type + if (!n.type || typeof n.type !== "string") { + n.type = "file"; + issues.push({ + level: "auto-corrected", + category: "missing-field", + message: `nodes[${i}] ("${name}"): missing "type" — defaulted to "file"`, + path: `nodes[${i}].type`, + }); + } + + // Missing or empty complexity + if (!n.complexity || n.complexity === "") { + n.complexity = "moderate"; + issues.push({ + level: "auto-corrected", + category: "missing-field", + message: `nodes[${i}] ("${name}"): missing "complexity" — defaulted to "moderate"`, + path: `nodes[${i}].complexity`, + }); + } else if (typeof n.complexity === "string" && n.complexity in COMPLEXITY_ALIASES) { + const original = n.complexity; + n.complexity = COMPLEXITY_ALIASES[n.complexity]; + issues.push({ + level: "auto-corrected", + category: "alias", + message: `nodes[${i}] ("${name}"): complexity "${original}" — mapped to "${n.complexity}"`, + path: `nodes[${i}].complexity`, + }); + } + + // Missing tags + if (!Array.isArray(n.tags)) { + n.tags = []; + issues.push({ + level: "auto-corrected", + category: "missing-field", + message: `nodes[${i}] ("${name}"): missing "tags" — defaulted to []`, + path: `nodes[${i}].tags`, + }); + } + + // Missing summary + if (!n.summary || typeof n.summary !== "string") { + n.summary = (n.name as string) || "No summary"; + issues.push({ + level: "auto-corrected", + category: "missing-field", + message: `nodes[${i}] ("${name}"): missing "summary" — defaulted to name`, + path: `nodes[${i}].summary`, + }); + } + + return n; + }); + } + + if (Array.isArray(data.edges)) { + result.edges = (data.edges as Record[]).map((edge, i) => { + if (typeof edge !== "object" || edge === null) return edge; + const e = { ...edge }; + + // Missing type + if (!e.type || typeof e.type !== "string") { + e.type = "depends_on"; + issues.push({ + level: "auto-corrected", + category: "missing-field", + message: `edges[${i}]: missing "type" — defaulted to "depends_on"`, + path: `edges[${i}].type`, + }); + } + + // Missing direction + if (!e.direction || typeof e.direction !== "string") { + e.direction = "forward"; + issues.push({ + level: "auto-corrected", + category: "missing-field", + message: `edges[${i}]: missing "direction" — defaulted to "forward"`, + path: `edges[${i}].direction`, + }); + } else if (e.direction in DIRECTION_ALIASES) { + const original = e.direction; + e.direction = DIRECTION_ALIASES[e.direction as string]; + issues.push({ + level: "auto-corrected", + category: "alias", + message: `edges[${i}]: direction "${original}" — mapped to "${e.direction}"`, + path: `edges[${i}].direction`, + }); + } + + // Missing weight + if (e.weight === undefined || e.weight === null) { + e.weight = 0.5; + issues.push({ + level: "auto-corrected", + category: "missing-field", + message: `edges[${i}]: missing "weight" — defaulted to 0.5`, + path: `edges[${i}].weight`, + }); + } else if (typeof e.weight === "string") { + const parsed = parseFloat(e.weight as string); + if (!isNaN(parsed)) { + const original = e.weight; + e.weight = parsed; + issues.push({ + level: "auto-corrected", + category: "type-coercion", + message: `edges[${i}]: weight was string "${original}" — coerced to number`, + path: `edges[${i}].weight`, + }); + } else { + const original = e.weight; + e.weight = 0.5; + issues.push({ + level: "auto-corrected", + category: "type-coercion", + message: `edges[${i}]: weight "${original}" is not a valid number — defaulted to 0.5`, + path: `edges[${i}].weight`, + }); + } + } + + // Clamp weight to [0, 1] + if (typeof e.weight === "number" && (e.weight < 0 || e.weight > 1)) { + const original = e.weight; + e.weight = Math.max(0, Math.min(1, e.weight)); + issues.push({ + level: "auto-corrected", + category: "out-of-range", + message: `edges[${i}]: weight ${original} clamped to ${e.weight}`, + path: `edges[${i}].weight`, + }); + } + + return e; + }); + } + + return { data: result, issues }; +} + export const GraphNodeSchema = z.object({ id: z.string(), type: z.enum(["file", "function", "class", "module", "concept"]), @@ -63,23 +318,236 @@ export const KnowledgeGraphSchema = z.object({ tour: z.array(TourStepSchema), }); +export interface GraphIssue { + level: "auto-corrected" | "dropped" | "fatal"; + category: string; + message: string; + path?: string; +} + export interface ValidationResult { success: boolean; data?: z.infer; + /** @deprecated Use issues/fatal instead */ errors?: string[]; + issues: GraphIssue[]; + fatal?: string; +} + +function buildInvalidCollectionIssue(name: string): GraphIssue { + return { + level: "fatal", + category: "invalid-collection", + message: `"${name}" must be an array when present`, + path: name, + }; +} + +function buildErrors(issues: GraphIssue[], fatal?: string): string[] | undefined { + const messages = issues.map((issue) => issue.message); + if (fatal && !messages.includes(fatal)) messages.unshift(fatal); + return messages.length > 0 ? messages : undefined; +} + +export function normalizeGraph(data: unknown): unknown { + if (typeof data !== "object" || data === null) return data; + + const d = data as Record; + const result = { ...d }; + + if (Array.isArray(d.nodes)) { + result.nodes = (d.nodes as any[]).map((node) => { + if ( + typeof node === "object" && + node !== null && + typeof node.type === "string" && + node.type in NODE_TYPE_ALIASES + ) { + return { ...node, type: NODE_TYPE_ALIASES[node.type] }; + } + return node; + }); + } + + if (Array.isArray(d.edges)) { + result.edges = (d.edges as any[]).map((edge) => { + if ( + typeof edge === "object" && + edge !== null && + typeof edge.type === "string" && + edge.type in EDGE_TYPE_ALIASES + ) { + return { ...edge, type: EDGE_TYPE_ALIASES[edge.type] }; + } + return edge; + }); + } + + return result; } export function validateGraph(data: unknown): ValidationResult { - const result = KnowledgeGraphSchema.safeParse(data); - - if (result.success) { - return { success: true, data: result.data }; + // Tier 4: Fatal — not even an object + if (typeof data !== "object" || data === null) { + const fatal = "Invalid input: not an object"; + return { success: false, issues: [], fatal, errors: buildErrors([], fatal) }; } - const errors = result.error.issues.map((issue) => { - const path = issue.path.join("."); - return path ? `${path}: ${issue.message}` : issue.message; - }); + const raw = data as Record; - return { success: false, errors }; + // Tier 1: Sanitize + const sanitized = sanitizeGraph(raw); + + // Existing: Normalize type aliases + const normalized = normalizeGraph(sanitized) as Record; + + // Tier 2: Auto-fix defaults and coercion + const { data: fixed, issues } = autoFixGraph(normalized); + + // Tier 4: Fatal — malformed top-level collections + const requiredCollections = ["nodes", "edges", "layers", "tour"] as const; + for (const collection of requiredCollections) { + if (collection in fixed && fixed[collection] !== undefined && !Array.isArray(fixed[collection])) { + const issue = buildInvalidCollectionIssue(collection); + issues.push(issue); + return { + success: false, + errors: buildErrors(issues, issue.message), + issues, + fatal: issue.message, + }; + } + } + + // Tier 4: Fatal — missing project metadata + const projectResult = ProjectMetaSchema.safeParse(fixed.project); + if (!projectResult.success) { + return { + success: false, + errors: buildErrors(issues, "Missing or invalid project metadata"), + issues, + fatal: "Missing or invalid project metadata", + }; + } + + // Tier 3: Validate nodes individually, drop broken + const validNodes: z.infer[] = []; + if (Array.isArray(fixed.nodes)) { + for (let i = 0; i < fixed.nodes.length; i++) { + const node = fixed.nodes[i] as Record; + const result = GraphNodeSchema.safeParse(node); + if (result.success) { + validNodes.push(result.data); + } else { + const name = node?.name || node?.id || `index ${i}`; + issues.push({ + level: "dropped", + category: "invalid-node", + message: `nodes[${i}] ("${name}"): ${result.error.issues[0]?.message ?? "validation failed"} — removed`, + path: `nodes[${i}]`, + }); + } + } + } + + // Tier 4: Fatal — no valid nodes + if (validNodes.length === 0) { + return { + success: false, + errors: buildErrors(issues, "No valid nodes found in knowledge graph"), + issues, + fatal: "No valid nodes found in knowledge graph", + }; + } + + // Tier 3: Validate edges + referential integrity + const nodeIds = new Set(validNodes.map((n) => n.id)); + const validEdges: z.infer[] = []; + if (Array.isArray(fixed.edges)) { + for (let i = 0; i < fixed.edges.length; i++) { + const edge = fixed.edges[i] as Record; + const result = GraphEdgeSchema.safeParse(edge); + if (!result.success) { + issues.push({ + level: "dropped", + category: "invalid-edge", + message: `edges[${i}]: ${result.error.issues[0]?.message ?? "validation failed"} — removed`, + path: `edges[${i}]`, + }); + continue; + } + if (!nodeIds.has(result.data.source)) { + issues.push({ + level: "dropped", + category: "invalid-reference", + message: `edges[${i}]: source "${result.data.source}" does not exist in nodes — removed`, + path: `edges[${i}].source`, + }); + continue; + } + if (!nodeIds.has(result.data.target)) { + issues.push({ + level: "dropped", + category: "invalid-reference", + message: `edges[${i}]: target "${result.data.target}" does not exist in nodes — removed`, + path: `edges[${i}].target`, + }); + continue; + } + validEdges.push(result.data); + } + } + + // Validate layers (drop broken, filter dangling nodeIds) + const validLayers: z.infer[] = []; + if (Array.isArray(fixed.layers)) { + for (let i = 0; i < (fixed.layers as unknown[]).length; i++) { + const result = LayerSchema.safeParse((fixed.layers as unknown[])[i]); + if (result.success) { + validLayers.push({ + ...result.data, + nodeIds: result.data.nodeIds.filter((id) => nodeIds.has(id)), + }); + } else { + issues.push({ + level: "dropped", + category: "invalid-layer", + message: `layers[${i}]: ${result.error.issues[0]?.message ?? "validation failed"} — removed`, + path: `layers[${i}]`, + }); + } + } + } + + // Validate tour steps (drop broken, filter dangling nodeIds) + const validTour: z.infer[] = []; + if (Array.isArray(fixed.tour)) { + for (let i = 0; i < (fixed.tour as unknown[]).length; i++) { + const result = TourStepSchema.safeParse((fixed.tour as unknown[])[i]); + if (result.success) { + validTour.push({ + ...result.data, + nodeIds: result.data.nodeIds.filter((id) => nodeIds.has(id)), + }); + } else { + issues.push({ + level: "dropped", + category: "invalid-tour-step", + message: `tour[${i}]: ${result.error.issues[0]?.message ?? "validation failed"} — removed`, + path: `tour[${i}]`, + }); + } + } + } + + const graph = { + version: typeof fixed.version === "string" ? fixed.version : "1.0.0", + project: projectResult.data, + nodes: validNodes, + edges: validEdges, + layers: validLayers, + tour: validTour, + }; + + return { success: true, data: graph, issues, errors: buildErrors(issues) }; } diff --git a/understand-anything-plugin/packages/core/src/types.ts b/understand-anything-plugin/packages/core/src/types.ts index e186c16..819f17e 100644 --- a/understand-anything-plugin/packages/core/src/types.ts +++ b/understand-anything-plugin/packages/core/src/types.ts @@ -66,12 +66,19 @@ export interface KnowledgeGraph { tour: TourStep[]; } +// Theme configuration (for dashboard customization) +export interface ThemeConfig { + presetId: string; + accentId: string; +} + // AnalysisMeta (for persistence) export interface AnalysisMeta { lastAnalyzedAt: string; gitCommitHash: string; version: string; analyzedFiles: number; + theme?: ThemeConfig; } // Project config (for auto-update opt-in) diff --git a/understand-anything-plugin/packages/dashboard/public/knowledge-graph.json b/understand-anything-plugin/packages/dashboard/public/knowledge-graph.json index 84cec42..c6e26f8 100644 --- a/understand-anything-plugin/packages/dashboard/public/knowledge-graph.json +++ b/understand-anything-plugin/packages/dashboard/public/knowledge-graph.json @@ -112,7 +112,7 @@ "complexity": "moderate" }, { - "id": "func:packages/core/src/schema.ts:validateGraph", + "id": "function:packages/core/src/schema.ts:validateGraph", "type": "function", "name": "validateGraph", "filePath": "packages/core/src/schema.ts", @@ -164,7 +164,7 @@ "complexity": "moderate" }, { - "id": "func:packages/core/src/embedding-search.ts:cosineSimilarity", + "id": "function:packages/core/src/embedding-search.ts:cosineSimilarity", "type": "function", "name": "cosineSimilarity", "filePath": "packages/core/src/embedding-search.ts", @@ -181,7 +181,7 @@ "complexity": "simple" }, { - "id": "func:packages/core/src/staleness.ts:getChangedFiles", + "id": "function:packages/core/src/staleness.ts:getChangedFiles", "type": "function", "name": "getChangedFiles", "filePath": "packages/core/src/staleness.ts", @@ -198,7 +198,7 @@ "complexity": "simple" }, { - "id": "func:packages/core/src/staleness.ts:isStale", + "id": "function:packages/core/src/staleness.ts:isStale", "type": "function", "name": "isStale", "filePath": "packages/core/src/staleness.ts", @@ -215,7 +215,7 @@ "complexity": "simple" }, { - "id": "func:packages/core/src/staleness.ts:mergeGraphUpdate", + "id": "function:packages/core/src/staleness.ts:mergeGraphUpdate", "type": "function", "name": "mergeGraphUpdate", "filePath": "packages/core/src/staleness.ts", @@ -263,7 +263,7 @@ "complexity": "moderate" }, { - "id": "func:packages/core/src/analyzer/graph-builder.ts:detectLanguage", + "id": "function:packages/core/src/analyzer/graph-builder.ts:detectLanguage", "type": "function", "name": "detectLanguage", "filePath": "packages/core/src/analyzer/graph-builder.ts", @@ -293,7 +293,7 @@ "complexity": "moderate" }, { - "id": "func:packages/core/src/analyzer/llm-analyzer.ts:buildFileAnalysisPrompt", + "id": "function:packages/core/src/analyzer/llm-analyzer.ts:buildFileAnalysisPrompt", "type": "function", "name": "buildFileAnalysisPrompt", "filePath": "packages/core/src/analyzer/llm-analyzer.ts", @@ -310,7 +310,7 @@ "complexity": "simple" }, { - "id": "func:packages/core/src/analyzer/llm-analyzer.ts:buildProjectSummaryPrompt", + "id": "function:packages/core/src/analyzer/llm-analyzer.ts:buildProjectSummaryPrompt", "type": "function", "name": "buildProjectSummaryPrompt", "filePath": "packages/core/src/analyzer/llm-analyzer.ts", @@ -327,7 +327,7 @@ "complexity": "simple" }, { - "id": "func:packages/core/src/analyzer/llm-analyzer.ts:parseFileAnalysisResponse", + "id": "function:packages/core/src/analyzer/llm-analyzer.ts:parseFileAnalysisResponse", "type": "function", "name": "parseFileAnalysisResponse", "filePath": "packages/core/src/analyzer/llm-analyzer.ts", @@ -344,7 +344,7 @@ "complexity": "moderate" }, { - "id": "func:packages/core/src/analyzer/llm-analyzer.ts:parseProjectSummaryResponse", + "id": "function:packages/core/src/analyzer/llm-analyzer.ts:parseProjectSummaryResponse", "type": "function", "name": "parseProjectSummaryResponse", "filePath": "packages/core/src/analyzer/llm-analyzer.ts", @@ -375,7 +375,7 @@ "complexity": "complex" }, { - "id": "func:packages/core/src/analyzer/layer-detector.ts:detectLayers", + "id": "function:packages/core/src/analyzer/layer-detector.ts:detectLayers", "type": "function", "name": "detectLayers", "filePath": "packages/core/src/analyzer/layer-detector.ts", @@ -392,7 +392,7 @@ "complexity": "moderate" }, { - "id": "func:packages/core/src/analyzer/layer-detector.ts:buildLayerDetectionPrompt", + "id": "function:packages/core/src/analyzer/layer-detector.ts:buildLayerDetectionPrompt", "type": "function", "name": "buildLayerDetectionPrompt", "filePath": "packages/core/src/analyzer/layer-detector.ts", @@ -409,7 +409,7 @@ "complexity": "simple" }, { - "id": "func:packages/core/src/analyzer/layer-detector.ts:parseLayerDetectionResponse", + "id": "function:packages/core/src/analyzer/layer-detector.ts:parseLayerDetectionResponse", "type": "function", "name": "parseLayerDetectionResponse", "filePath": "packages/core/src/analyzer/layer-detector.ts", @@ -426,7 +426,7 @@ "complexity": "moderate" }, { - "id": "func:packages/core/src/analyzer/layer-detector.ts:applyLLMLayers", + "id": "function:packages/core/src/analyzer/layer-detector.ts:applyLLMLayers", "type": "function", "name": "applyLLMLayers", "filePath": "packages/core/src/analyzer/layer-detector.ts", @@ -457,7 +457,7 @@ "complexity": "moderate" }, { - "id": "func:packages/core/src/analyzer/language-lesson.ts:detectLanguageConcepts", + "id": "function:packages/core/src/analyzer/language-lesson.ts:detectLanguageConcepts", "type": "function", "name": "detectLanguageConcepts", "filePath": "packages/core/src/analyzer/language-lesson.ts", @@ -474,7 +474,7 @@ "complexity": "simple" }, { - "id": "func:packages/core/src/analyzer/language-lesson.ts:buildLanguageLessonPrompt", + "id": "function:packages/core/src/analyzer/language-lesson.ts:buildLanguageLessonPrompt", "type": "function", "name": "buildLanguageLessonPrompt", "filePath": "packages/core/src/analyzer/language-lesson.ts", @@ -491,7 +491,7 @@ "complexity": "moderate" }, { - "id": "func:packages/core/src/analyzer/language-lesson.ts:parseLanguageLessonResponse", + "id": "function:packages/core/src/analyzer/language-lesson.ts:parseLanguageLessonResponse", "type": "function", "name": "parseLanguageLessonResponse", "filePath": "packages/core/src/analyzer/language-lesson.ts", @@ -523,7 +523,7 @@ "complexity": "complex" }, { - "id": "func:packages/core/src/analyzer/tour-generator.ts:buildTourGenerationPrompt", + "id": "function:packages/core/src/analyzer/tour-generator.ts:buildTourGenerationPrompt", "type": "function", "name": "buildTourGenerationPrompt", "filePath": "packages/core/src/analyzer/tour-generator.ts", @@ -540,7 +540,7 @@ "complexity": "simple" }, { - "id": "func:packages/core/src/analyzer/tour-generator.ts:parseTourGenerationResponse", + "id": "function:packages/core/src/analyzer/tour-generator.ts:parseTourGenerationResponse", "type": "function", "name": "parseTourGenerationResponse", "filePath": "packages/core/src/analyzer/tour-generator.ts", @@ -557,7 +557,7 @@ "complexity": "moderate" }, { - "id": "func:packages/core/src/analyzer/tour-generator.ts:generateHeuristicTour", + "id": "function:packages/core/src/analyzer/tour-generator.ts:generateHeuristicTour", "type": "function", "name": "generateHeuristicTour", "filePath": "packages/core/src/analyzer/tour-generator.ts", @@ -609,7 +609,7 @@ "complexity": "complex" }, { - "id": "func:packages/core/src/plugins/tree-sitter-plugin.ts:languageKeyFromPath", + "id": "function:packages/core/src/plugins/tree-sitter-plugin.ts:languageKeyFromPath", "type": "function", "name": "languageKeyFromPath", "filePath": "packages/core/src/plugins/tree-sitter-plugin.ts", @@ -625,7 +625,7 @@ "complexity": "simple" }, { - "id": "func:packages/core/src/plugins/tree-sitter-plugin.ts:traverse", + "id": "function:packages/core/src/plugins/tree-sitter-plugin.ts:traverse", "type": "function", "name": "traverse", "filePath": "packages/core/src/plugins/tree-sitter-plugin.ts", @@ -642,7 +642,7 @@ "complexity": "simple" }, { - "id": "func:packages/core/src/plugins/tree-sitter-plugin.ts:getStringValue", + "id": "function:packages/core/src/plugins/tree-sitter-plugin.ts:getStringValue", "type": "function", "name": "getStringValue", "filePath": "packages/core/src/plugins/tree-sitter-plugin.ts", @@ -659,7 +659,7 @@ "complexity": "simple" }, { - "id": "func:packages/core/src/plugins/tree-sitter-plugin.ts:extractParams", + "id": "function:packages/core/src/plugins/tree-sitter-plugin.ts:extractParams", "type": "function", "name": "extractParams", "filePath": "packages/core/src/plugins/tree-sitter-plugin.ts", @@ -677,7 +677,7 @@ "complexity": "moderate" }, { - "id": "func:packages/core/src/plugins/tree-sitter-plugin.ts:extractReturnType", + "id": "function:packages/core/src/plugins/tree-sitter-plugin.ts:extractReturnType", "type": "function", "name": "extractReturnType", "filePath": "packages/core/src/plugins/tree-sitter-plugin.ts", @@ -695,7 +695,7 @@ "complexity": "simple" }, { - "id": "func:packages/core/src/plugins/tree-sitter-plugin.ts:extractImportSpecifiers", + "id": "function:packages/core/src/plugins/tree-sitter-plugin.ts:extractImportSpecifiers", "type": "function", "name": "extractImportSpecifiers", "filePath": "packages/core/src/plugins/tree-sitter-plugin.ts", @@ -758,7 +758,7 @@ "complexity": "simple" }, { - "id": "func:packages/core/src/plugins/discovery.ts:parsePluginConfig", + "id": "function:packages/core/src/plugins/discovery.ts:parsePluginConfig", "type": "function", "name": "parsePluginConfig", "filePath": "packages/core/src/plugins/discovery.ts", @@ -775,7 +775,7 @@ "complexity": "moderate" }, { - "id": "func:packages/core/src/plugins/discovery.ts:serializePluginConfig", + "id": "function:packages/core/src/plugins/discovery.ts:serializePluginConfig", "type": "function", "name": "serializePluginConfig", "filePath": "packages/core/src/plugins/discovery.ts", @@ -805,7 +805,7 @@ "complexity": "simple" }, { - "id": "func:packages/core/src/persistence/index.ts:saveGraph", + "id": "function:packages/core/src/persistence/index.ts:saveGraph", "type": "function", "name": "saveGraph", "filePath": "packages/core/src/persistence/index.ts", @@ -822,7 +822,7 @@ "complexity": "simple" }, { - "id": "func:packages/core/src/persistence/index.ts:loadGraph", + "id": "function:packages/core/src/persistence/index.ts:loadGraph", "type": "function", "name": "loadGraph", "filePath": "packages/core/src/persistence/index.ts", @@ -840,7 +840,7 @@ "complexity": "moderate" }, { - "id": "func:packages/core/src/persistence/index.ts:saveMeta", + "id": "function:packages/core/src/persistence/index.ts:saveMeta", "type": "function", "name": "saveMeta", "filePath": "packages/core/src/persistence/index.ts", @@ -857,7 +857,7 @@ "complexity": "simple" }, { - "id": "func:packages/core/src/persistence/index.ts:loadMeta", + "id": "function:packages/core/src/persistence/index.ts:loadMeta", "type": "function", "name": "loadMeta", "filePath": "packages/core/src/persistence/index.ts", @@ -889,7 +889,7 @@ "complexity": "moderate" }, { - "id": "func:packages/dashboard/src/App.tsx:App", + "id": "function:packages/dashboard/src/App.tsx:App", "type": "function", "name": "App", "filePath": "packages/dashboard/src/App.tsx", @@ -934,7 +934,7 @@ "complexity": "complex" }, { - "id": "func:packages/dashboard/src/store.ts:buildSystemPrompt", + "id": "function:packages/dashboard/src/store.ts:buildSystemPrompt", "type": "function", "name": "buildSystemPrompt", "filePath": "packages/dashboard/src/store.ts", @@ -951,7 +951,7 @@ "complexity": "moderate" }, { - "id": "func:packages/dashboard/src/store.ts:getSortedTour", + "id": "function:packages/dashboard/src/store.ts:getSortedTour", "type": "function", "name": "getSortedTour", "filePath": "packages/dashboard/src/store.ts", @@ -967,7 +967,7 @@ "complexity": "simple" }, { - "id": "func:packages/dashboard/src/store.ts:useDashboardStore", + "id": "function:packages/dashboard/src/store.ts:useDashboardStore", "type": "function", "name": "useDashboardStore", "filePath": "packages/dashboard/src/store.ts", @@ -999,7 +999,7 @@ "complexity": "simple" }, { - "id": "func:packages/dashboard/src/utils/layout.ts:applyDagreLayout", + "id": "function:packages/dashboard/src/utils/layout.ts:applyDagreLayout", "type": "function", "name": "applyDagreLayout", "filePath": "packages/dashboard/src/utils/layout.ts", @@ -1031,7 +1031,7 @@ "complexity": "complex" }, { - "id": "func:packages/dashboard/src/components/GraphView.tsx:GraphView", + "id": "function:packages/dashboard/src/components/GraphView.tsx:GraphView", "type": "function", "name": "GraphView", "filePath": "packages/dashboard/src/components/GraphView.tsx", @@ -1063,7 +1063,7 @@ "complexity": "moderate" }, { - "id": "func:packages/dashboard/src/components/ChatPanel.tsx:ChatPanel", + "id": "function:packages/dashboard/src/components/ChatPanel.tsx:ChatPanel", "type": "function", "name": "ChatPanel", "filePath": "packages/dashboard/src/components/ChatPanel.tsx", @@ -1094,7 +1094,7 @@ "complexity": "moderate" }, { - "id": "func:packages/dashboard/src/components/CodeViewer.tsx:getLanguage", + "id": "function:packages/dashboard/src/components/CodeViewer.tsx:getLanguage", "type": "function", "name": "getLanguage", "filePath": "packages/dashboard/src/components/CodeViewer.tsx", @@ -1111,7 +1111,7 @@ "complexity": "simple" }, { - "id": "func:packages/dashboard/src/components/CodeViewer.tsx:CodeViewer", + "id": "function:packages/dashboard/src/components/CodeViewer.tsx:CodeViewer", "type": "function", "name": "CodeViewer", "filePath": "packages/dashboard/src/components/CodeViewer.tsx", @@ -1161,7 +1161,7 @@ "complexity": "simple" }, { - "id": "func:packages/dashboard/src/components/CustomNode.tsx:CustomNode", + "id": "function:packages/dashboard/src/components/CustomNode.tsx:CustomNode", "type": "function", "name": "CustomNode", "filePath": "packages/dashboard/src/components/CustomNode.tsx", @@ -1193,7 +1193,7 @@ "complexity": "moderate" }, { - "id": "func:packages/dashboard/src/components/LayerLegend.tsx:getLayerColor", + "id": "function:packages/dashboard/src/components/LayerLegend.tsx:getLayerColor", "type": "function", "name": "getLayerColor", "filePath": "packages/dashboard/src/components/LayerLegend.tsx", @@ -1210,7 +1210,7 @@ "complexity": "simple" }, { - "id": "func:packages/dashboard/src/components/LayerLegend.tsx:getLayerBorderColor", + "id": "function:packages/dashboard/src/components/LayerLegend.tsx:getLayerBorderColor", "type": "function", "name": "getLayerBorderColor", "filePath": "packages/dashboard/src/components/LayerLegend.tsx", @@ -1227,7 +1227,7 @@ "complexity": "simple" }, { - "id": "func:packages/dashboard/src/components/LayerLegend.tsx:LayerLegend", + "id": "function:packages/dashboard/src/components/LayerLegend.tsx:LayerLegend", "type": "function", "name": "LayerLegend", "filePath": "packages/dashboard/src/components/LayerLegend.tsx", @@ -1259,7 +1259,7 @@ "complexity": "complex" }, { - "id": "func:packages/dashboard/src/components/LearnPanel.tsx:LearnPanel", + "id": "function:packages/dashboard/src/components/LearnPanel.tsx:LearnPanel", "type": "function", "name": "LearnPanel", "filePath": "packages/dashboard/src/components/LearnPanel.tsx", @@ -1291,7 +1291,7 @@ "complexity": "complex" }, { - "id": "func:packages/dashboard/src/components/NodeInfo.tsx:NodeInfo", + "id": "function:packages/dashboard/src/components/NodeInfo.tsx:NodeInfo", "type": "function", "name": "NodeInfo", "filePath": "packages/dashboard/src/components/NodeInfo.tsx", @@ -1322,7 +1322,7 @@ "complexity": "simple" }, { - "id": "func:packages/dashboard/src/components/PersonaSelector.tsx:PersonaSelector", + "id": "function:packages/dashboard/src/components/PersonaSelector.tsx:PersonaSelector", "type": "function", "name": "PersonaSelector", "filePath": "packages/dashboard/src/components/PersonaSelector.tsx", @@ -1354,7 +1354,7 @@ "complexity": "moderate" }, { - "id": "func:packages/dashboard/src/components/SearchBar.tsx:SearchBar", + "id": "function:packages/dashboard/src/components/SearchBar.tsx:SearchBar", "type": "function", "name": "SearchBar", "filePath": "packages/dashboard/src/components/SearchBar.tsx", @@ -1400,7 +1400,7 @@ "complexity": "moderate" }, { - "id": "func:packages/skill/src/context-builder.ts:buildChatContext", + "id": "function:packages/skill/src/context-builder.ts:buildChatContext", "type": "function", "name": "buildChatContext", "filePath": "packages/skill/src/context-builder.ts", @@ -1418,7 +1418,7 @@ "complexity": "moderate" }, { - "id": "func:packages/skill/src/context-builder.ts:formatContextForPrompt", + "id": "function:packages/skill/src/context-builder.ts:formatContextForPrompt", "type": "function", "name": "formatContextForPrompt", "filePath": "packages/skill/src/context-builder.ts", @@ -1451,7 +1451,7 @@ "complexity": "moderate" }, { - "id": "func:packages/skill/src/diff-analyzer.ts:buildDiffContext", + "id": "function:packages/skill/src/diff-analyzer.ts:buildDiffContext", "type": "function", "name": "buildDiffContext", "filePath": "packages/skill/src/diff-analyzer.ts", @@ -1469,7 +1469,7 @@ "complexity": "moderate" }, { - "id": "func:packages/skill/src/diff-analyzer.ts:formatDiffAnalysis", + "id": "function:packages/skill/src/diff-analyzer.ts:formatDiffAnalysis", "type": "function", "name": "formatDiffAnalysis", "filePath": "packages/skill/src/diff-analyzer.ts", @@ -1502,7 +1502,7 @@ "complexity": "moderate" }, { - "id": "func:packages/skill/src/explain-builder.ts:buildExplainContext", + "id": "function:packages/skill/src/explain-builder.ts:buildExplainContext", "type": "function", "name": "buildExplainContext", "filePath": "packages/skill/src/explain-builder.ts", @@ -1520,7 +1520,7 @@ "complexity": "moderate" }, { - "id": "func:packages/skill/src/explain-builder.ts:formatExplainPrompt", + "id": "function:packages/skill/src/explain-builder.ts:formatExplainPrompt", "type": "function", "name": "formatExplainPrompt", "filePath": "packages/skill/src/explain-builder.ts", @@ -1553,7 +1553,7 @@ "complexity": "moderate" }, { - "id": "func:packages/skill/src/onboard-builder.ts:buildOnboardingGuide", + "id": "function:packages/skill/src/onboard-builder.ts:buildOnboardingGuide", "type": "function", "name": "buildOnboardingGuide", "filePath": "packages/skill/src/onboard-builder.ts", @@ -1585,7 +1585,7 @@ "complexity": "simple" }, { - "id": "func:packages/skill/src/understand-chat.ts:buildChatPrompt", + "id": "function:packages/skill/src/understand-chat.ts:buildChatPrompt", "type": "function", "name": "buildChatPrompt", "filePath": "packages/skill/src/understand-chat.ts", @@ -1739,7 +1739,7 @@ }, { "source": "file:packages/core/src/schema.ts", - "target": "func:packages/core/src/schema.ts:validateGraph", + "target": "function:packages/core/src/schema.ts:validateGraph", "type": "contains", "direction": "forward", "weight": 1 @@ -1760,42 +1760,42 @@ }, { "source": "file:packages/core/src/embedding-search.ts", - "target": "func:packages/core/src/embedding-search.ts:cosineSimilarity", + "target": "function:packages/core/src/embedding-search.ts:cosineSimilarity", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/core/src/staleness.ts", - "target": "func:packages/core/src/staleness.ts:getChangedFiles", + "target": "function:packages/core/src/staleness.ts:getChangedFiles", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/core/src/staleness.ts", - "target": "func:packages/core/src/staleness.ts:isStale", + "target": "function:packages/core/src/staleness.ts:isStale", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/core/src/staleness.ts", - "target": "func:packages/core/src/staleness.ts:mergeGraphUpdate", + "target": "function:packages/core/src/staleness.ts:mergeGraphUpdate", "type": "contains", "direction": "forward", "weight": 1 }, { - "source": "func:packages/core/src/staleness.ts:isStale", - "target": "func:packages/core/src/staleness.ts:getChangedFiles", + "source": "function:packages/core/src/staleness.ts:isStale", + "target": "function:packages/core/src/staleness.ts:getChangedFiles", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "class:packages/core/src/embedding-search.ts:SemanticSearchEngine", - "target": "func:packages/core/src/embedding-search.ts:cosineSimilarity", + "target": "function:packages/core/src/embedding-search.ts:cosineSimilarity", "type": "calls", "direction": "forward", "weight": 0.8 @@ -1830,7 +1830,7 @@ }, { "source": "file:packages/core/src/analyzer/graph-builder.ts", - "target": "func:packages/core/src/analyzer/graph-builder.ts:detectLanguage", + "target": "function:packages/core/src/analyzer/graph-builder.ts:detectLanguage", "type": "contains", "direction": "forward", "weight": 1 @@ -1844,56 +1844,56 @@ }, { "source": "file:packages/core/src/analyzer/llm-analyzer.ts", - "target": "func:packages/core/src/analyzer/llm-analyzer.ts:buildFileAnalysisPrompt", + "target": "function:packages/core/src/analyzer/llm-analyzer.ts:buildFileAnalysisPrompt", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/core/src/analyzer/llm-analyzer.ts", - "target": "func:packages/core/src/analyzer/llm-analyzer.ts:buildProjectSummaryPrompt", + "target": "function:packages/core/src/analyzer/llm-analyzer.ts:buildProjectSummaryPrompt", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/core/src/analyzer/llm-analyzer.ts", - "target": "func:packages/core/src/analyzer/llm-analyzer.ts:parseFileAnalysisResponse", + "target": "function:packages/core/src/analyzer/llm-analyzer.ts:parseFileAnalysisResponse", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/core/src/analyzer/llm-analyzer.ts", - "target": "func:packages/core/src/analyzer/llm-analyzer.ts:parseProjectSummaryResponse", + "target": "function:packages/core/src/analyzer/llm-analyzer.ts:parseProjectSummaryResponse", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/core/src/analyzer/llm-analyzer.ts", - "target": "func:packages/core/src/analyzer/llm-analyzer.ts:buildFileAnalysisPrompt", + "target": "function:packages/core/src/analyzer/llm-analyzer.ts:buildFileAnalysisPrompt", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:packages/core/src/analyzer/llm-analyzer.ts", - "target": "func:packages/core/src/analyzer/llm-analyzer.ts:buildProjectSummaryPrompt", + "target": "function:packages/core/src/analyzer/llm-analyzer.ts:buildProjectSummaryPrompt", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:packages/core/src/analyzer/llm-analyzer.ts", - "target": "func:packages/core/src/analyzer/llm-analyzer.ts:parseFileAnalysisResponse", + "target": "function:packages/core/src/analyzer/llm-analyzer.ts:parseFileAnalysisResponse", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:packages/core/src/analyzer/llm-analyzer.ts", - "target": "func:packages/core/src/analyzer/llm-analyzer.ts:parseProjectSummaryResponse", + "target": "function:packages/core/src/analyzer/llm-analyzer.ts:parseProjectSummaryResponse", "type": "exports", "direction": "forward", "weight": 0.8 @@ -1907,56 +1907,56 @@ }, { "source": "file:packages/core/src/analyzer/layer-detector.ts", - "target": "func:packages/core/src/analyzer/layer-detector.ts:detectLayers", + "target": "function:packages/core/src/analyzer/layer-detector.ts:detectLayers", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/core/src/analyzer/layer-detector.ts", - "target": "func:packages/core/src/analyzer/layer-detector.ts:buildLayerDetectionPrompt", + "target": "function:packages/core/src/analyzer/layer-detector.ts:buildLayerDetectionPrompt", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/core/src/analyzer/layer-detector.ts", - "target": "func:packages/core/src/analyzer/layer-detector.ts:parseLayerDetectionResponse", + "target": "function:packages/core/src/analyzer/layer-detector.ts:parseLayerDetectionResponse", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/core/src/analyzer/layer-detector.ts", - "target": "func:packages/core/src/analyzer/layer-detector.ts:applyLLMLayers", + "target": "function:packages/core/src/analyzer/layer-detector.ts:applyLLMLayers", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/core/src/analyzer/layer-detector.ts", - "target": "func:packages/core/src/analyzer/layer-detector.ts:detectLayers", + "target": "function:packages/core/src/analyzer/layer-detector.ts:detectLayers", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:packages/core/src/analyzer/layer-detector.ts", - "target": "func:packages/core/src/analyzer/layer-detector.ts:buildLayerDetectionPrompt", + "target": "function:packages/core/src/analyzer/layer-detector.ts:buildLayerDetectionPrompt", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:packages/core/src/analyzer/layer-detector.ts", - "target": "func:packages/core/src/analyzer/layer-detector.ts:parseLayerDetectionResponse", + "target": "function:packages/core/src/analyzer/layer-detector.ts:parseLayerDetectionResponse", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:packages/core/src/analyzer/layer-detector.ts", - "target": "func:packages/core/src/analyzer/layer-detector.ts:applyLLMLayers", + "target": "function:packages/core/src/analyzer/layer-detector.ts:applyLLMLayers", "type": "exports", "direction": "forward", "weight": 0.8 @@ -1970,49 +1970,49 @@ }, { "source": "file:packages/core/src/analyzer/language-lesson.ts", - "target": "func:packages/core/src/analyzer/language-lesson.ts:detectLanguageConcepts", + "target": "function:packages/core/src/analyzer/language-lesson.ts:detectLanguageConcepts", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/core/src/analyzer/language-lesson.ts", - "target": "func:packages/core/src/analyzer/language-lesson.ts:buildLanguageLessonPrompt", + "target": "function:packages/core/src/analyzer/language-lesson.ts:buildLanguageLessonPrompt", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/core/src/analyzer/language-lesson.ts", - "target": "func:packages/core/src/analyzer/language-lesson.ts:parseLanguageLessonResponse", + "target": "function:packages/core/src/analyzer/language-lesson.ts:parseLanguageLessonResponse", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/core/src/analyzer/language-lesson.ts", - "target": "func:packages/core/src/analyzer/language-lesson.ts:detectLanguageConcepts", + "target": "function:packages/core/src/analyzer/language-lesson.ts:detectLanguageConcepts", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:packages/core/src/analyzer/language-lesson.ts", - "target": "func:packages/core/src/analyzer/language-lesson.ts:buildLanguageLessonPrompt", + "target": "function:packages/core/src/analyzer/language-lesson.ts:buildLanguageLessonPrompt", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:packages/core/src/analyzer/language-lesson.ts", - "target": "func:packages/core/src/analyzer/language-lesson.ts:parseLanguageLessonResponse", + "target": "function:packages/core/src/analyzer/language-lesson.ts:parseLanguageLessonResponse", "type": "exports", "direction": "forward", "weight": 0.8 }, { - "source": "func:packages/core/src/analyzer/language-lesson.ts:buildLanguageLessonPrompt", - "target": "func:packages/core/src/analyzer/language-lesson.ts:detectLanguageConcepts", + "source": "function:packages/core/src/analyzer/language-lesson.ts:buildLanguageLessonPrompt", + "target": "function:packages/core/src/analyzer/language-lesson.ts:detectLanguageConcepts", "type": "calls", "direction": "forward", "weight": 0.8 @@ -2026,42 +2026,42 @@ }, { "source": "file:packages/core/src/analyzer/tour-generator.ts", - "target": "func:packages/core/src/analyzer/tour-generator.ts:buildTourGenerationPrompt", + "target": "function:packages/core/src/analyzer/tour-generator.ts:buildTourGenerationPrompt", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/core/src/analyzer/tour-generator.ts", - "target": "func:packages/core/src/analyzer/tour-generator.ts:parseTourGenerationResponse", + "target": "function:packages/core/src/analyzer/tour-generator.ts:parseTourGenerationResponse", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/core/src/analyzer/tour-generator.ts", - "target": "func:packages/core/src/analyzer/tour-generator.ts:generateHeuristicTour", + "target": "function:packages/core/src/analyzer/tour-generator.ts:generateHeuristicTour", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/core/src/analyzer/tour-generator.ts", - "target": "func:packages/core/src/analyzer/tour-generator.ts:buildTourGenerationPrompt", + "target": "function:packages/core/src/analyzer/tour-generator.ts:buildTourGenerationPrompt", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:packages/core/src/analyzer/tour-generator.ts", - "target": "func:packages/core/src/analyzer/tour-generator.ts:parseTourGenerationResponse", + "target": "function:packages/core/src/analyzer/tour-generator.ts:parseTourGenerationResponse", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:packages/core/src/analyzer/tour-generator.ts", - "target": "func:packages/core/src/analyzer/tour-generator.ts:generateHeuristicTour", + "target": "function:packages/core/src/analyzer/tour-generator.ts:generateHeuristicTour", "type": "exports", "direction": "forward", "weight": 0.8 @@ -2103,42 +2103,42 @@ }, { "source": "file:packages/core/src/plugins/tree-sitter-plugin.ts", - "target": "func:packages/core/src/plugins/tree-sitter-plugin.ts:languageKeyFromPath", + "target": "function:packages/core/src/plugins/tree-sitter-plugin.ts:languageKeyFromPath", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/core/src/plugins/tree-sitter-plugin.ts", - "target": "func:packages/core/src/plugins/tree-sitter-plugin.ts:traverse", + "target": "function:packages/core/src/plugins/tree-sitter-plugin.ts:traverse", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/core/src/plugins/tree-sitter-plugin.ts", - "target": "func:packages/core/src/plugins/tree-sitter-plugin.ts:getStringValue", + "target": "function:packages/core/src/plugins/tree-sitter-plugin.ts:getStringValue", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/core/src/plugins/tree-sitter-plugin.ts", - "target": "func:packages/core/src/plugins/tree-sitter-plugin.ts:extractParams", + "target": "function:packages/core/src/plugins/tree-sitter-plugin.ts:extractParams", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/core/src/plugins/tree-sitter-plugin.ts", - "target": "func:packages/core/src/plugins/tree-sitter-plugin.ts:extractReturnType", + "target": "function:packages/core/src/plugins/tree-sitter-plugin.ts:extractReturnType", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/core/src/plugins/tree-sitter-plugin.ts", - "target": "func:packages/core/src/plugins/tree-sitter-plugin.ts:extractImportSpecifiers", + "target": "function:packages/core/src/plugins/tree-sitter-plugin.ts:extractImportSpecifiers", "type": "contains", "direction": "forward", "weight": 1 @@ -2152,35 +2152,35 @@ }, { "source": "class:packages/core/src/plugins/tree-sitter-plugin.ts:TreeSitterPlugin", - "target": "func:packages/core/src/plugins/tree-sitter-plugin.ts:languageKeyFromPath", + "target": "function:packages/core/src/plugins/tree-sitter-plugin.ts:languageKeyFromPath", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "class:packages/core/src/plugins/tree-sitter-plugin.ts:TreeSitterPlugin", - "target": "func:packages/core/src/plugins/tree-sitter-plugin.ts:extractParams", + "target": "function:packages/core/src/plugins/tree-sitter-plugin.ts:extractParams", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "class:packages/core/src/plugins/tree-sitter-plugin.ts:TreeSitterPlugin", - "target": "func:packages/core/src/plugins/tree-sitter-plugin.ts:extractReturnType", + "target": "function:packages/core/src/plugins/tree-sitter-plugin.ts:extractReturnType", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "class:packages/core/src/plugins/tree-sitter-plugin.ts:TreeSitterPlugin", - "target": "func:packages/core/src/plugins/tree-sitter-plugin.ts:extractImportSpecifiers", + "target": "function:packages/core/src/plugins/tree-sitter-plugin.ts:extractImportSpecifiers", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "class:packages/core/src/plugins/tree-sitter-plugin.ts:TreeSitterPlugin", - "target": "func:packages/core/src/plugins/tree-sitter-plugin.ts:getStringValue", + "target": "function:packages/core/src/plugins/tree-sitter-plugin.ts:getStringValue", "type": "calls", "direction": "forward", "weight": 0.8 @@ -2208,28 +2208,28 @@ }, { "source": "file:packages/core/src/plugins/discovery.ts", - "target": "func:packages/core/src/plugins/discovery.ts:parsePluginConfig", + "target": "function:packages/core/src/plugins/discovery.ts:parsePluginConfig", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/core/src/plugins/discovery.ts", - "target": "func:packages/core/src/plugins/discovery.ts:serializePluginConfig", + "target": "function:packages/core/src/plugins/discovery.ts:serializePluginConfig", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/core/src/plugins/discovery.ts", - "target": "func:packages/core/src/plugins/discovery.ts:parsePluginConfig", + "target": "function:packages/core/src/plugins/discovery.ts:parsePluginConfig", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:packages/core/src/plugins/discovery.ts", - "target": "func:packages/core/src/plugins/discovery.ts:serializePluginConfig", + "target": "function:packages/core/src/plugins/discovery.ts:serializePluginConfig", "type": "exports", "direction": "forward", "weight": 0.8 @@ -2250,63 +2250,63 @@ }, { "source": "file:packages/core/src/persistence/index.ts", - "target": "func:packages/core/src/persistence/index.ts:saveGraph", + "target": "function:packages/core/src/persistence/index.ts:saveGraph", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/core/src/persistence/index.ts", - "target": "func:packages/core/src/persistence/index.ts:loadGraph", + "target": "function:packages/core/src/persistence/index.ts:loadGraph", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/core/src/persistence/index.ts", - "target": "func:packages/core/src/persistence/index.ts:saveMeta", + "target": "function:packages/core/src/persistence/index.ts:saveMeta", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/core/src/persistence/index.ts", - "target": "func:packages/core/src/persistence/index.ts:loadMeta", + "target": "function:packages/core/src/persistence/index.ts:loadMeta", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/core/src/persistence/index.ts", - "target": "func:packages/core/src/persistence/index.ts:saveGraph", + "target": "function:packages/core/src/persistence/index.ts:saveGraph", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:packages/core/src/persistence/index.ts", - "target": "func:packages/core/src/persistence/index.ts:loadGraph", + "target": "function:packages/core/src/persistence/index.ts:loadGraph", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:packages/core/src/persistence/index.ts", - "target": "func:packages/core/src/persistence/index.ts:saveMeta", + "target": "function:packages/core/src/persistence/index.ts:saveMeta", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:packages/core/src/persistence/index.ts", - "target": "func:packages/core/src/persistence/index.ts:loadMeta", + "target": "function:packages/core/src/persistence/index.ts:loadMeta", "type": "exports", "direction": "forward", "weight": 0.8 }, { - "source": "func:packages/core/src/persistence/index.ts:loadGraph", - "target": "func:packages/core/src/schema.ts:validateGraph", + "source": "function:packages/core/src/persistence/index.ts:loadGraph", + "target": "function:packages/core/src/schema.ts:validateGraph", "type": "calls", "direction": "forward", "weight": 0.8 @@ -2327,14 +2327,14 @@ }, { "source": "file:packages/dashboard/src/App.tsx", - "target": "func:packages/dashboard/src/App.tsx:App", + "target": "function:packages/dashboard/src/App.tsx:App", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/dashboard/src/App.tsx", - "target": "func:packages/dashboard/src/App.tsx:App", + "target": "function:packages/dashboard/src/App.tsx:App", "type": "exports", "direction": "forward", "weight": 0.8 @@ -2362,70 +2362,70 @@ }, { "source": "file:packages/dashboard/src/store.ts", - "target": "func:packages/dashboard/src/store.ts:buildSystemPrompt", + "target": "function:packages/dashboard/src/store.ts:buildSystemPrompt", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/dashboard/src/store.ts", - "target": "func:packages/dashboard/src/store.ts:getSortedTour", + "target": "function:packages/dashboard/src/store.ts:getSortedTour", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/dashboard/src/store.ts", - "target": "func:packages/dashboard/src/store.ts:useDashboardStore", + "target": "function:packages/dashboard/src/store.ts:useDashboardStore", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/dashboard/src/store.ts", - "target": "func:packages/dashboard/src/store.ts:useDashboardStore", + "target": "function:packages/dashboard/src/store.ts:useDashboardStore", "type": "exports", "direction": "forward", "weight": 0.8 }, { - "source": "func:packages/dashboard/src/store.ts:useDashboardStore", - "target": "func:packages/dashboard/src/store.ts:buildSystemPrompt", + "source": "function:packages/dashboard/src/store.ts:useDashboardStore", + "target": "function:packages/dashboard/src/store.ts:buildSystemPrompt", "type": "calls", "direction": "forward", "weight": 0.8 }, { - "source": "func:packages/dashboard/src/store.ts:useDashboardStore", - "target": "func:packages/dashboard/src/store.ts:getSortedTour", + "source": "function:packages/dashboard/src/store.ts:useDashboardStore", + "target": "function:packages/dashboard/src/store.ts:getSortedTour", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "file:packages/dashboard/src/utils/layout.ts", - "target": "func:packages/dashboard/src/utils/layout.ts:applyDagreLayout", + "target": "function:packages/dashboard/src/utils/layout.ts:applyDagreLayout", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/dashboard/src/utils/layout.ts", - "target": "func:packages/dashboard/src/utils/layout.ts:applyDagreLayout", + "target": "function:packages/dashboard/src/utils/layout.ts:applyDagreLayout", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:packages/dashboard/src/components/GraphView.tsx", - "target": "func:packages/dashboard/src/components/GraphView.tsx:GraphView", + "target": "function:packages/dashboard/src/components/GraphView.tsx:GraphView", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/dashboard/src/components/GraphView.tsx", - "target": "func:packages/dashboard/src/components/GraphView.tsx:GraphView", + "target": "function:packages/dashboard/src/components/GraphView.tsx:GraphView", "type": "exports", "direction": "forward", "weight": 0.8 @@ -2445,29 +2445,29 @@ "weight": 0.7 }, { - "source": "func:packages/dashboard/src/components/GraphView.tsx:GraphView", - "target": "func:packages/dashboard/src/utils/layout.ts:applyDagreLayout", + "source": "function:packages/dashboard/src/components/GraphView.tsx:GraphView", + "target": "function:packages/dashboard/src/utils/layout.ts:applyDagreLayout", "type": "calls", "direction": "forward", "weight": 0.8 }, { - "source": "func:packages/dashboard/src/components/GraphView.tsx:GraphView", - "target": "func:packages/dashboard/src/store.ts:useDashboardStore", + "source": "function:packages/dashboard/src/components/GraphView.tsx:GraphView", + "target": "function:packages/dashboard/src/store.ts:useDashboardStore", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "file:packages/dashboard/src/components/ChatPanel.tsx", - "target": "func:packages/dashboard/src/components/ChatPanel.tsx:ChatPanel", + "target": "function:packages/dashboard/src/components/ChatPanel.tsx:ChatPanel", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/dashboard/src/components/ChatPanel.tsx", - "target": "func:packages/dashboard/src/components/ChatPanel.tsx:ChatPanel", + "target": "function:packages/dashboard/src/components/ChatPanel.tsx:ChatPanel", "type": "exports", "direction": "forward", "weight": 0.8 @@ -2480,15 +2480,15 @@ "weight": 0.7 }, { - "source": "func:packages/dashboard/src/components/ChatPanel.tsx:ChatPanel", - "target": "func:packages/dashboard/src/store.ts:useDashboardStore", + "source": "function:packages/dashboard/src/components/ChatPanel.tsx:ChatPanel", + "target": "function:packages/dashboard/src/store.ts:useDashboardStore", "type": "calls", "direction": "forward", "weight": 0.8 }, { - "source": "func:packages/dashboard/src/App.tsx:App", - "target": "func:packages/dashboard/src/store.ts:useDashboardStore", + "source": "function:packages/dashboard/src/App.tsx:App", + "target": "function:packages/dashboard/src/store.ts:useDashboardStore", "type": "calls", "direction": "forward", "weight": 0.8 @@ -2502,28 +2502,28 @@ }, { "source": "file:packages/dashboard/src/components/CodeViewer.tsx", - "target": "func:packages/dashboard/src/components/CodeViewer.tsx:getLanguage", + "target": "function:packages/dashboard/src/components/CodeViewer.tsx:getLanguage", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/dashboard/src/components/CodeViewer.tsx", - "target": "func:packages/dashboard/src/components/CodeViewer.tsx:CodeViewer", + "target": "function:packages/dashboard/src/components/CodeViewer.tsx:CodeViewer", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/dashboard/src/components/CodeViewer.tsx", - "target": "func:packages/dashboard/src/components/CodeViewer.tsx:CodeViewer", + "target": "function:packages/dashboard/src/components/CodeViewer.tsx:CodeViewer", "type": "exports", "direction": "forward", "weight": 0.8 }, { - "source": "func:packages/dashboard/src/components/CodeViewer.tsx:CodeViewer", - "target": "func:packages/dashboard/src/components/CodeViewer.tsx:getLanguage", + "source": "function:packages/dashboard/src/components/CodeViewer.tsx:CodeViewer", + "target": "function:packages/dashboard/src/components/CodeViewer.tsx:getLanguage", "type": "calls", "direction": "forward", "weight": 0.8 @@ -2537,14 +2537,14 @@ }, { "source": "file:packages/dashboard/src/components/CustomNode.tsx", - "target": "func:packages/dashboard/src/components/CustomNode.tsx:CustomNode", + "target": "function:packages/dashboard/src/components/CustomNode.tsx:CustomNode", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/dashboard/src/components/CustomNode.tsx", - "target": "func:packages/dashboard/src/components/CustomNode.tsx:CustomNode", + "target": "function:packages/dashboard/src/components/CustomNode.tsx:CustomNode", "type": "exports", "direction": "forward", "weight": 0.8 @@ -2565,49 +2565,49 @@ }, { "source": "file:packages/dashboard/src/components/LayerLegend.tsx", - "target": "func:packages/dashboard/src/components/LayerLegend.tsx:getLayerColor", + "target": "function:packages/dashboard/src/components/LayerLegend.tsx:getLayerColor", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/dashboard/src/components/LayerLegend.tsx", - "target": "func:packages/dashboard/src/components/LayerLegend.tsx:getLayerBorderColor", + "target": "function:packages/dashboard/src/components/LayerLegend.tsx:getLayerBorderColor", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/dashboard/src/components/LayerLegend.tsx", - "target": "func:packages/dashboard/src/components/LayerLegend.tsx:LayerLegend", + "target": "function:packages/dashboard/src/components/LayerLegend.tsx:LayerLegend", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/dashboard/src/components/LayerLegend.tsx", - "target": "func:packages/dashboard/src/components/LayerLegend.tsx:getLayerColor", + "target": "function:packages/dashboard/src/components/LayerLegend.tsx:getLayerColor", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:packages/dashboard/src/components/LayerLegend.tsx", - "target": "func:packages/dashboard/src/components/LayerLegend.tsx:getLayerBorderColor", + "target": "function:packages/dashboard/src/components/LayerLegend.tsx:getLayerBorderColor", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:packages/dashboard/src/components/LayerLegend.tsx", - "target": "func:packages/dashboard/src/components/LayerLegend.tsx:LayerLegend", + "target": "function:packages/dashboard/src/components/LayerLegend.tsx:LayerLegend", "type": "exports", "direction": "forward", "weight": 0.8 }, { - "source": "func:packages/dashboard/src/components/LayerLegend.tsx:LayerLegend", - "target": "func:packages/dashboard/src/components/LayerLegend.tsx:getLayerBorderColor", + "source": "function:packages/dashboard/src/components/LayerLegend.tsx:LayerLegend", + "target": "function:packages/dashboard/src/components/LayerLegend.tsx:getLayerBorderColor", "type": "calls", "direction": "forward", "weight": 0.8 @@ -2621,14 +2621,14 @@ }, { "source": "file:packages/dashboard/src/components/LearnPanel.tsx", - "target": "func:packages/dashboard/src/components/LearnPanel.tsx:LearnPanel", + "target": "function:packages/dashboard/src/components/LearnPanel.tsx:LearnPanel", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/dashboard/src/components/LearnPanel.tsx", - "target": "func:packages/dashboard/src/components/LearnPanel.tsx:LearnPanel", + "target": "function:packages/dashboard/src/components/LearnPanel.tsx:LearnPanel", "type": "exports", "direction": "forward", "weight": 0.8 @@ -2642,14 +2642,14 @@ }, { "source": "file:packages/dashboard/src/components/NodeInfo.tsx", - "target": "func:packages/dashboard/src/components/NodeInfo.tsx:NodeInfo", + "target": "function:packages/dashboard/src/components/NodeInfo.tsx:NodeInfo", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/dashboard/src/components/NodeInfo.tsx", - "target": "func:packages/dashboard/src/components/NodeInfo.tsx:NodeInfo", + "target": "function:packages/dashboard/src/components/NodeInfo.tsx:NodeInfo", "type": "exports", "direction": "forward", "weight": 0.8 @@ -2663,14 +2663,14 @@ }, { "source": "file:packages/dashboard/src/components/PersonaSelector.tsx", - "target": "func:packages/dashboard/src/components/PersonaSelector.tsx:PersonaSelector", + "target": "function:packages/dashboard/src/components/PersonaSelector.tsx:PersonaSelector", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/dashboard/src/components/PersonaSelector.tsx", - "target": "func:packages/dashboard/src/components/PersonaSelector.tsx:PersonaSelector", + "target": "function:packages/dashboard/src/components/PersonaSelector.tsx:PersonaSelector", "type": "exports", "direction": "forward", "weight": 0.8 @@ -2684,14 +2684,14 @@ }, { "source": "file:packages/dashboard/src/components/SearchBar.tsx", - "target": "func:packages/dashboard/src/components/SearchBar.tsx:SearchBar", + "target": "function:packages/dashboard/src/components/SearchBar.tsx:SearchBar", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/dashboard/src/components/SearchBar.tsx", - "target": "func:packages/dashboard/src/components/SearchBar.tsx:SearchBar", + "target": "function:packages/dashboard/src/components/SearchBar.tsx:SearchBar", "type": "exports", "direction": "forward", "weight": 0.8 @@ -2733,56 +2733,56 @@ }, { "source": "file:packages/skill/src/context-builder.ts", - "target": "func:packages/skill/src/context-builder.ts:buildChatContext", + "target": "function:packages/skill/src/context-builder.ts:buildChatContext", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/skill/src/context-builder.ts", - "target": "func:packages/skill/src/context-builder.ts:formatContextForPrompt", + "target": "function:packages/skill/src/context-builder.ts:formatContextForPrompt", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/skill/src/diff-analyzer.ts", - "target": "func:packages/skill/src/diff-analyzer.ts:buildDiffContext", + "target": "function:packages/skill/src/diff-analyzer.ts:buildDiffContext", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/skill/src/diff-analyzer.ts", - "target": "func:packages/skill/src/diff-analyzer.ts:formatDiffAnalysis", + "target": "function:packages/skill/src/diff-analyzer.ts:formatDiffAnalysis", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/skill/src/explain-builder.ts", - "target": "func:packages/skill/src/explain-builder.ts:buildExplainContext", + "target": "function:packages/skill/src/explain-builder.ts:buildExplainContext", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/skill/src/explain-builder.ts", - "target": "func:packages/skill/src/explain-builder.ts:formatExplainPrompt", + "target": "function:packages/skill/src/explain-builder.ts:formatExplainPrompt", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/skill/src/onboard-builder.ts", - "target": "func:packages/skill/src/onboard-builder.ts:buildOnboardingGuide", + "target": "function:packages/skill/src/onboard-builder.ts:buildOnboardingGuide", "type": "contains", "direction": "forward", "weight": 1 }, { "source": "file:packages/skill/src/understand-chat.ts", - "target": "func:packages/skill/src/understand-chat.ts:buildChatPrompt", + "target": "function:packages/skill/src/understand-chat.ts:buildChatPrompt", "type": "contains", "direction": "forward", "weight": 1 @@ -2795,92 +2795,92 @@ "weight": 0.7 }, { - "source": "func:packages/skill/src/understand-chat.ts:buildChatPrompt", - "target": "func:packages/skill/src/context-builder.ts:buildChatContext", + "source": "function:packages/skill/src/understand-chat.ts:buildChatPrompt", + "target": "function:packages/skill/src/context-builder.ts:buildChatContext", "type": "calls", "direction": "forward", "weight": 0.8 }, { - "source": "func:packages/skill/src/understand-chat.ts:buildChatPrompt", - "target": "func:packages/skill/src/context-builder.ts:formatContextForPrompt", + "source": "function:packages/skill/src/understand-chat.ts:buildChatPrompt", + "target": "function:packages/skill/src/context-builder.ts:formatContextForPrompt", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "file:packages/skill/src/context-builder.ts", - "target": "func:packages/skill/src/context-builder.ts:formatContextForPrompt", + "target": "function:packages/skill/src/context-builder.ts:formatContextForPrompt", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:packages/skill/src/context-builder.ts", - "target": "func:packages/skill/src/context-builder.ts:buildChatContext", + "target": "function:packages/skill/src/context-builder.ts:buildChatContext", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:packages/skill/src/diff-analyzer.ts", - "target": "func:packages/skill/src/diff-analyzer.ts:buildDiffContext", + "target": "function:packages/skill/src/diff-analyzer.ts:buildDiffContext", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:packages/skill/src/diff-analyzer.ts", - "target": "func:packages/skill/src/diff-analyzer.ts:formatDiffAnalysis", + "target": "function:packages/skill/src/diff-analyzer.ts:formatDiffAnalysis", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:packages/skill/src/explain-builder.ts", - "target": "func:packages/skill/src/explain-builder.ts:buildExplainContext", + "target": "function:packages/skill/src/explain-builder.ts:buildExplainContext", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:packages/skill/src/explain-builder.ts", - "target": "func:packages/skill/src/explain-builder.ts:formatExplainPrompt", + "target": "function:packages/skill/src/explain-builder.ts:formatExplainPrompt", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:packages/skill/src/onboard-builder.ts", - "target": "func:packages/skill/src/onboard-builder.ts:buildOnboardingGuide", + "target": "function:packages/skill/src/onboard-builder.ts:buildOnboardingGuide", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:packages/skill/src/understand-chat.ts", - "target": "func:packages/skill/src/understand-chat.ts:buildChatPrompt", + "target": "function:packages/skill/src/understand-chat.ts:buildChatPrompt", "type": "exports", "direction": "forward", "weight": 0.8 }, { - "source": "func:packages/skill/src/context-builder.ts:buildChatContext", - "target": "func:packages/skill/src/context-builder.ts:formatContextForPrompt", + "source": "function:packages/skill/src/context-builder.ts:buildChatContext", + "target": "function:packages/skill/src/context-builder.ts:formatContextForPrompt", "type": "related", "direction": "forward", "weight": 0.6 }, { - "source": "func:packages/skill/src/diff-analyzer.ts:buildDiffContext", - "target": "func:packages/skill/src/diff-analyzer.ts:formatDiffAnalysis", + "source": "function:packages/skill/src/diff-analyzer.ts:buildDiffContext", + "target": "function:packages/skill/src/diff-analyzer.ts:formatDiffAnalysis", "type": "related", "direction": "forward", "weight": 0.6 }, { - "source": "func:packages/skill/src/explain-builder.ts:buildExplainContext", - "target": "func:packages/skill/src/explain-builder.ts:formatExplainPrompt", + "source": "function:packages/skill/src/explain-builder.ts:buildExplainContext", + "target": "function:packages/skill/src/explain-builder.ts:formatExplainPrompt", "type": "related", "direction": "forward", "weight": 0.6 @@ -2977,7 +2977,7 @@ "title": "Project Overview: The Dashboard Entry Point", "description": "Start with packages/dashboard/src/App.tsx, the root React component that bootstraps the entire interactive dashboard. On mount it fetches a pre-built knowledge-graph.json file and hands it to the Zustand store. From here you can see the three persona-adaptive layouts and how every major panel is composed together.", "nodeIds": [ - "func:packages/dashboard/src/App.tsx:App" + "function:packages/dashboard/src/App.tsx:App" ], "languageLesson": "TypeScript conditional JSX with ternary chains is a clean pattern for persona-adaptive rendering without introducing a separate routing library." }, @@ -3061,7 +3061,7 @@ "title": "Dashboard State: The Zustand Store", "description": "store.ts is the single source of truth for everything the dashboard displays. It holds the graph, search state, chat history, tour state, and persona. buildSystemPrompt assembles rich LLM context for the ChatPanel.", "nodeIds": [ - "func:packages/dashboard/src/store.ts:useDashboardStore" + "function:packages/dashboard/src/store.ts:useDashboardStore" ], "languageLesson": "Zustand's create pattern is a TypeScript-idiomatic alternative to Redux. Selectors subscribe only to changed slices." }, @@ -3070,7 +3070,7 @@ "title": "Visual Graph: React Flow with Dagre Layout", "description": "GraphView.tsx renders the knowledge graph as an interactive node-link diagram using React Flow. applyDagreLayout computes hierarchical positions, and tour-highlighted nodes receive distinct visual styles.", "nodeIds": [ - "func:packages/dashboard/src/components/GraphView.tsx:GraphView" + "function:packages/dashboard/src/components/GraphView.tsx:GraphView" ] }, { @@ -3078,10 +3078,10 @@ "title": "Skill Commands: AI-Powered Developer Tools", "description": "The skill package exposes four Claude Code slash commands. context-builder.ts does fuzzy search + 1-hop expansion. diff-analyzer.ts traces ripple effects from git diffs. explain-builder.ts resolves nodes for explanation. onboard-builder.ts generates markdown onboarding guides.", "nodeIds": [ - "func:packages/skill/src/context-builder.ts:buildChatContext", - "func:packages/skill/src/diff-analyzer.ts:buildDiffContext", - "func:packages/skill/src/explain-builder.ts:buildExplainContext", - "func:packages/skill/src/onboard-builder.ts:buildOnboardingGuide" + "function:packages/skill/src/context-builder.ts:buildChatContext", + "function:packages/skill/src/diff-analyzer.ts:buildDiffContext", + "function:packages/skill/src/explain-builder.ts:buildExplainContext", + "function:packages/skill/src/onboard-builder.ts:buildOnboardingGuide" ] } ] diff --git a/understand-anything-plugin/packages/dashboard/src/App.tsx b/understand-anything-plugin/packages/dashboard/src/App.tsx index 50df460..78ded8f 100644 --- a/understand-anything-plugin/packages/dashboard/src/App.tsx +++ b/understand-anything-plugin/packages/dashboard/src/App.tsx @@ -1,5 +1,6 @@ -import { useEffect, useState } from "react"; +import { useEffect, useState, useMemo } from "react"; import { validateGraph } from "@understand-anything/core/schema"; +import type { GraphIssue } from "@understand-anything/core/schema"; import { useDashboardStore } from "./store"; import GraphView from "./components/GraphView"; import CodeViewer from "./components/CodeViewer"; @@ -10,6 +11,13 @@ 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 WarningBanner from "./components/WarningBanner"; +import { useKeyboardShortcuts } from "./hooks/useKeyboardShortcuts"; +import type { KeyboardShortcut } from "./hooks/useKeyboardShortcuts"; +import { ThemeProvider } from "./themes/index.ts"; +import { ThemePicker } from "./components/ThemePicker.tsx"; +import type { ThemeConfig } from "./themes/index.ts"; function App() { const graph = useDashboardStore((s) => s.graph); @@ -21,6 +29,108 @@ function App() { const closeCodeViewer = useDashboardStore((s) => s.closeCodeViewer); const setDiffOverlay = useDashboardStore((s) => s.setDiffOverlay); const [loadError, setLoadError] = useState(null); + const [graphIssues, setGraphIssues] = useState([]); + const [showKeyboardHelp, setShowKeyboardHelp] = useState(false); + const [metaTheme, setMetaTheme] = useState(null); + + useEffect(() => { + fetch("/meta.json") + .then((r) => (r.ok ? r.json() : null)) + .then((meta) => { + if (meta?.theme) setMetaTheme(meta.theme); + }) + .catch(() => {}); + }, []); + + // Define keyboard shortcuts + const shortcuts = useMemo( + () => [ + // 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( + '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") @@ -29,10 +139,20 @@ function App() { const result = validateGraph(data); if (result.success && result.data) { setGraph(result.data); + setGraphIssues(result.issues); + for (const issue of result.issues) { + if (issue.level === "auto-corrected") { + console.warn(`[graph] auto-corrected: ${issue.message}`); + } else if (issue.level === "dropped") { + console.error(`[graph] dropped: ${issue.message}`); + } + } + } else if (result.fatal) { + console.error("Knowledge graph validation failed:", result.fatal); + setLoadError(`Invalid knowledge graph: ${result.fatal}`); } else { - const errorMsg = result.errors?.join("; ") ?? "Unknown validation error"; - console.error("Knowledge graph validation failed:", errorMsg); - setLoadError(`Invalid knowledge graph: ${errorMsg}`); + console.error("Knowledge graph validation failed: unknown error"); + setLoadError("Invalid knowledge graph: unknown validation error"); } }) .catch((err) => { @@ -78,6 +198,7 @@ function App() { ); return ( +
{/* Header */}
@@ -91,12 +212,37 @@ function App() {
+ +
{/* Search */} + {/* Validation warning banner */} + {graphIssues.length > 0 && !loadError && ( + + )} + {/* Error banner */} {loadError && (
@@ -107,8 +253,11 @@ function App() { {/* Main content: Graph + Sidebar */}
{/* Graph area */} -
+
+
+ Press ? for keyboard shortcuts +
{/* Right sidebar */} @@ -135,7 +284,16 @@ function App() {
)}
+ + {/* Keyboard shortcuts help modal */} + {showKeyboardHelp && ( + setShowKeyboardHelp(false)} + /> + )}
+ ); } diff --git a/understand-anything-plugin/packages/dashboard/src/components/CodeViewer.tsx b/understand-anything-plugin/packages/dashboard/src/components/CodeViewer.tsx index d1df7c3..64fe249 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/CodeViewer.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/CodeViewer.tsx @@ -27,8 +27,8 @@ export default function CodeViewer() { className="text-[10px] font-semibold uppercase tracking-wider px-2 py-0.5 rounded border" style={{ color: "var(--color-node-file)", - borderColor: "rgba(74,124,155,0.3)", - backgroundColor: "rgba(74,124,155,0.1)", + borderColor: "color-mix(in srgb, var(--color-node-file) 30%, transparent)", + backgroundColor: "color-mix(in srgb, var(--color-node-file) 10%, transparent)", }} > {node.type} @@ -56,14 +56,14 @@ export default function CodeViewer() {
{/* Summary */}
-

Summary

+

Summary

{node.summary}

{/* Language notes callout */} {node.languageNotes && ( -
-

Language Notes

+
+

Language Notes

{node.languageNotes}

)} @@ -71,7 +71,7 @@ export default function CodeViewer() { {/* Tags */} {node.tags.length > 0 && (
-

Tags

+

Tags

{node.tags.map((tag) => ( diff --git a/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx b/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx index cf88c98..d2ec838 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx @@ -1,3 +1,4 @@ +import { memo } from "react"; import { Handle, Position } from "@xyflow/react"; import type { NodeProps, Node } from "@xyflow/react"; @@ -19,7 +20,7 @@ const typeTextColors: Record = { const complexityColors: Record = { simple: "text-node-function", - moderate: "text-gold-dim", + moderate: "text-accent-dim", complex: "text-[#c97070]", }; @@ -40,7 +41,7 @@ export interface CustomNodeData extends Record { export type CustomFlowNode = Node; -export default function CustomNode({ +function CustomNodeComponent({ id, data, }: NodeProps) { @@ -50,17 +51,17 @@ export default function CustomNode({ let extraClass = ""; if (data.isSelected) { - extraClass = "ring-2 ring-gold node-glow"; + extraClass = "ring-2 ring-accent node-glow"; } else if (data.isTourHighlighted) { - extraClass = "ring-2 ring-gold-dim animate-gold-pulse"; + extraClass = "ring-2 ring-accent-dim animate-accent-pulse"; } else if (data.isHighlighted) { const score = data.searchScore ?? 1; if (score <= 0.1) { - extraClass = "ring-2 ring-gold-bright"; + extraClass = "ring-2 ring-accent-bright"; } else if (score <= 0.3) { - extraClass = "ring-2 ring-gold"; + extraClass = "ring-2 ring-accent"; } else { - extraClass = "ring-1 ring-gold-dim/60"; + extraClass = "ring-1 ring-accent-dim/60"; } } @@ -79,8 +80,7 @@ export default function CustomNode({ return (
data.onNodeClick?.(id)} > {/* Left color bar */} @@ -122,3 +122,6 @@ export default function CustomNode({
); } + +const CustomNode = memo(CustomNodeComponent); +export default CustomNode; diff --git a/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx b/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx index 2c8c92a..561feff 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ReactFlow, ReactFlowProvider, @@ -16,11 +16,17 @@ 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 { useTheme } from "../themes/index.ts"; +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 }; /** @@ -84,6 +90,214 @@ function SelectedNodeFitView() { 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["graph"]>, + persona: string, + diffMode: boolean, + changedNodeIds: Set, + affectedNodeIds: Set, + 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(); + 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 + ? "var(--color-diff-changed)" + : "var(--color-diff-affected)", + strokeWidth: 2.5, + } + : diffMode + ? { stroke: "var(--color-edge-dim)", strokeWidth: 1 } + : { stroke: "var(--color-edge)", strokeWidth: 1.5 }, + labelStyle: diffMode && !isImpacted + ? { fill: "var(--color-text-muted)", fontSize: 10 } + : { fill: "var(--color-text-secondary)", 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(); + 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: "var(--color-accent-overlay-bg)", + borderRadius: 12, + border: "2px dashed var(--color-accent-overlay-border)", + padding: 8, + fontSize: 13, + fontWeight: 600, + color: "var(--color-accent)", + }, + }); + + 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); @@ -96,6 +310,9 @@ function GraphViewInner() { const diffMode = useDashboardStore((s) => s.diffMode); const changedNodeIds = useDashboardStore((s) => s.changedNodeIds); const affectedNodeIds = useDashboardStore((s) => s.affectedNodeIds); + const { preset } = useTheme(); + + const [layouting, setLayouting] = useState(false); const handleNodeSelect = useCallback( (nodeId: string) => { @@ -105,190 +322,86 @@ function GraphViewInner() { [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(); - 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(); - 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 }) => { @@ -314,7 +427,17 @@ function GraphViewInner() { } return ( -
+
+ {layouting && ( +
+
+
+

+ Laying out {topoNodes.length.toLocaleString()} nodes... +

+
+
+ )} - + diff --git a/understand-anything-plugin/packages/dashboard/src/components/KeyboardShortcutsHelp.tsx b/understand-anything-plugin/packages/dashboard/src/components/KeyboardShortcutsHelp.tsx new file mode 100644 index 0000000..c9fcd1f --- /dev/null +++ b/understand-anything-plugin/packages/dashboard/src/components/KeyboardShortcutsHelp.tsx @@ -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); + + return ( +
+
e.stopPropagation()} + > + {/* Header */} +
+
+

+ Keyboard Shortcuts +

+

+ Press ? anytime to toggle this help +

+
+ +
+ + {/* Shortcuts list */} +
+ {Object.entries(groupedShortcuts).map(([category, categoryShortcuts]) => ( +
+

+ {category} +

+
+ {categoryShortcuts.map((shortcut, index) => ( +
+ + {shortcut.description} + + {formatShortcutKey(shortcut)} +
+ ))} +
+
+ ))} +
+ + {/* Footer */} +
+

+ Press ESC to close +

+
+
+
+ ); +} diff --git a/understand-anything-plugin/packages/dashboard/src/components/LayerLegend.tsx b/understand-anything-plugin/packages/dashboard/src/components/LayerLegend.tsx index 6d4fc24..f98e389 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/LayerLegend.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/LayerLegend.tsx @@ -47,7 +47,7 @@ export default function LayerLegend() { disabled={!hasLayers} className={`px-2 py-0.5 rounded text-[11px] font-medium transition-colors ${ showLayers && hasLayers - ? "bg-gold/20 text-gold" + ? "bg-accent/20 text-accent" : hasLayers ? "bg-elevated text-text-secondary hover:bg-surface" : "bg-elevated text-text-muted cursor-not-allowed" diff --git a/understand-anything-plugin/packages/dashboard/src/components/LearnPanel.tsx b/understand-anything-plugin/packages/dashboard/src/components/LearnPanel.tsx index 2534fd4..7e4344b 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/LearnPanel.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/LearnPanel.tsx @@ -47,13 +47,13 @@ export default function LearnPanel() {
-

+

Steps

{tourSteps.map((step, i) => ( @@ -61,7 +61,7 @@ export default function LearnPanel() { key={step.order} className="flex items-start gap-2 text-xs bg-elevated rounded-lg px-3 py-2 border border-border-subtle" > - + {i + 1}. {step.title} @@ -86,7 +86,7 @@ export default function LearnPanel() { {/* Header with progress counter and exit */}
-

+

Tour

@@ -104,7 +104,7 @@ export default function LearnPanel() { {/* Progress bar */}
@@ -122,16 +122,16 @@ export default function LearnPanel() {

{children}

), strong: ({ children }) => ( - {children} + {children} ), code: ({ className, children }) => { const isBlock = className?.includes("language-"); return isBlock ? ( - + {children} ) : ( - + {children} ); @@ -154,8 +154,8 @@ export default function LearnPanel() { {/* Language lesson */} {step.languageLesson && ( -
-

+
+

Language Lesson

@@ -167,7 +167,7 @@ export default function LearnPanel() { {/* Referenced component pills */} {step.nodeIds.length > 0 && (

-

+

Referenced Components

@@ -198,7 +198,7 @@ export default function LearnPanel() { onClick={() => setTourStep(i)} className={`w-2 h-2 rounded-full transition-colors ${ i === currentTourStep - ? "bg-gold" + ? "bg-accent" : "bg-elevated hover:bg-surface" }`} aria-label={`Go to step ${i + 1}`} @@ -217,7 +217,7 @@ export default function LearnPanel() { diff --git a/understand-anything-plugin/packages/dashboard/src/components/NodeInfo.tsx b/understand-anything-plugin/packages/dashboard/src/components/NodeInfo.tsx index 78c1041..ad60b1b 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/NodeInfo.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/NodeInfo.tsx @@ -11,7 +11,7 @@ const typeBadgeColors: Record = { const complexityBadgeColors: Record = { simple: "text-node-function border border-node-function/30 bg-node-function/10", - moderate: "text-gold-dim border border-gold-dim/30 bg-gold-dim/10", + moderate: "text-accent-dim border border-accent-dim/30 bg-accent-dim/10", complex: "text-[#c97070] border border-[#c97070]/30 bg-[#c97070]/10", }; @@ -75,7 +75,7 @@ export default function NodeInfo() {
diff --git a/understand-anything-plugin/packages/dashboard/src/components/SearchBar.tsx b/understand-anything-plugin/packages/dashboard/src/components/SearchBar.tsx index 8a8f245..112b552 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/SearchBar.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/SearchBar.tsx @@ -94,14 +94,14 @@ export default function SearchBar() { onChange={handleInputChange} onFocus={() => setDropdownOpen(true)} placeholder="Search nodes by name, summary, or tags..." - className="flex-1 bg-elevated text-text-primary text-sm rounded-lg px-3 py-1.5 border border-border-subtle focus:outline-none focus:border-gold/50 placeholder-text-muted" + className="flex-1 bg-elevated text-text-primary text-sm rounded-lg px-3 py-1.5 border border-border-subtle focus:outline-none focus:border-accent/50 placeholder-text-muted" />
+ + {open && ( +
+ {/* Presets */} +
+
+ Theme +
+
+ {PRESETS.map((p) => ( + + ))} +
+
+ + {/* Accent swatches */} +
+
+ Accent Color +
+
+ {preset.accentSwatches.map((swatch) => ( +
+
+
+ )} +
+ ); +} diff --git a/understand-anything-plugin/packages/dashboard/src/components/WarningBanner.tsx b/understand-anything-plugin/packages/dashboard/src/components/WarningBanner.tsx new file mode 100644 index 0000000..753a2c5 --- /dev/null +++ b/understand-anything-plugin/packages/dashboard/src/components/WarningBanner.tsx @@ -0,0 +1,193 @@ +import { useState, useCallback } from "react"; +import type { GraphIssue } from "@understand-anything/core/schema"; + +interface WarningBannerProps { + issues: GraphIssue[]; +} + +function buildCopyText(issues: GraphIssue[]): string { + const lines = [ + "The following issues were found in your knowledge-graph.json.", + "These are LLM generation errors — not a system bug.", + "You can ask your agent to fix these specific issues in the knowledge-graph.json file:", + "", + ]; + + // Auto-corrected first, then dropped + const sorted = [...issues].sort((a, b) => { + const order: Record = { "auto-corrected": 0, dropped: 1, fatal: 2 }; + return (order[a.level] ?? 2) - (order[b.level] ?? 2); + }); + + for (const issue of sorted) { + const label = + issue.level === "auto-corrected" + ? "Auto-corrected" + : issue.level === "dropped" + ? "Dropped" + : "Fatal"; + lines.push(`[${label}] ${issue.message}`); + } + + return lines.join("\n"); +} + +export default function WarningBanner({ issues }: WarningBannerProps) { + const [expanded, setExpanded] = useState(false); + const [copied, setCopied] = useState(false); + + const autoCorrected = issues.filter((i) => i.level === "auto-corrected"); + const dropped = issues.filter((i) => i.level === "dropped"); + + // Build summary text — only mention counts > 0 + const parts: string[] = []; + if (autoCorrected.length > 0) { + parts.push(`${autoCorrected.length} auto-correction${autoCorrected.length !== 1 ? "s" : ""}`); + } + if (dropped.length > 0) { + parts.push(`${dropped.length} dropped item${dropped.length !== 1 ? "s" : ""}`); + } + const summary = `Knowledge graph loaded with ${parts.join(" and ")}`; + + const handleCopy = useCallback(async () => { + const text = buildCopyText(issues); + try { + await navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { + console.warn("Clipboard write failed — copy text manually from the expanded issue list"); + } + }, [issues]); + + if (issues.length === 0) return null; + + return ( +
+ {/* Collapsed summary row */} + + + {/* Expanded detail panel */} + {expanded && ( +
+ {/* Issue list */} +
+ {/* Auto-corrected issues */} + {autoCorrected.length > 0 && ( +
+

+ Auto-corrected ({autoCorrected.length}) +

+ {autoCorrected.map((issue, i) => ( +
+ + + + + + {issue.message} +
+ ))} +
+ )} + + {/* Dropped issues */} + {dropped.length > 0 && ( +
0 ? "mt-2" : ""}> +

+ Dropped ({dropped.length}) +

+ {dropped.map((issue, i) => ( +
+ + + + + + {issue.message} +
+ ))} +
+ )} +
+ + {/* Footer with copy button and actionable message */} +
+

+ Copy these issues and ask your agent to fix them in knowledge-graph.json +

+ +
+
+ )} +
+ ); +} diff --git a/understand-anything-plugin/packages/dashboard/src/hooks/useKeyboardShortcuts.ts b/understand-anything-plugin/packages/dashboard/src/hooks/useKeyboardShortcuts.ts new file mode 100644 index 0000000..fcf91e3 --- /dev/null +++ b/understand-anything-plugin/packages/dashboard/src/hooks/useKeyboardShortcuts.ts @@ -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(" + "); +} diff --git a/understand-anything-plugin/packages/dashboard/src/index.css b/understand-anything-plugin/packages/dashboard/src/index.css index a767a27..95b5440 100644 --- a/understand-anything-plugin/packages/dashboard/src/index.css +++ b/understand-anything-plugin/packages/dashboard/src/index.css @@ -1,46 +1,77 @@ @import "tailwindcss"; @theme { - /* Dark luxury color palette */ + /* Base */ --color-root: #0a0a0a; --color-surface: #111111; --color-elevated: #1a1a1a; --color-panel: #141414; - /* Gold accent spectrum */ - --color-gold: #d4a574; - --color-gold-dim: #c9a96e; - --color-gold-bright: #e8c49a; + /* Accent */ + --color-accent: #d4a574; + --color-accent-dim: #c9a96e; + --color-accent-bright: #e8c49a; - /* Text hierarchy */ + /* Text */ --color-text-primary: #f5f0eb; --color-text-secondary: #a39787; --color-text-muted: #6b5f53; - /* Border tokens */ + /* Borders */ --color-border-subtle: rgba(212, 165, 116, 0.12); --color-border-medium: rgba(212, 165, 116, 0.25); - /* Node type colors (muted, refined) */ + /* Node types */ --color-node-file: #4a7c9b; --color-node-function: #5a9e6f; --color-node-class: #8b6fb0; --color-node-module: #c9a06c; --color-node-concept: #b07a8a; - /* Diff overlay colors */ + /* Diff */ --color-diff-changed: #e05252; --color-diff-affected: #d4a030; --color-diff-changed-dim: rgba(224, 82, 82, 0.25); --color-diff-affected-dim: rgba(212, 160, 48, 0.25); - /* Fonts */ + /* Glass */ + --glass-bg: rgba(20, 20, 20, 0.8); + --glass-bg-heavy: rgba(20, 20, 20, 0.95); + --glass-border: rgba(212, 165, 116, 0.1); + --glass-border-heavy: rgba(212, 165, 116, 0.15); + + /* Scrollbar */ + --scrollbar-thumb: rgba(212, 165, 116, 0.2); + --scrollbar-thumb-hover: rgba(212, 165, 116, 0.35); + + /* Glow */ + --glow-accent: rgba(212, 165, 116, 0.15); + --glow-accent-strong: rgba(212, 165, 116, 0.4); + --glow-accent-pulse: rgba(212, 165, 116, 0.6); + + /* Edges */ + --color-edge: rgba(212, 165, 116, 0.3); + --color-edge-dim: rgba(212, 165, 116, 0.08); + --color-edge-dot: rgba(212, 165, 116, 0.15); + + /* Accent overlays */ + --color-accent-overlay-bg: rgba(212, 165, 116, 0.05); + --color-accent-overlay-border: rgba(212, 165, 116, 0.25); + + /* Kbd */ + --kbd-bg: rgba(212, 165, 116, 0.1); + + /* Typography */ --font-serif: 'DM Serif Display', Georgia, serif; --font-mono: 'JetBrains Mono', 'Fira Code', monospace; --font-sans: 'Inter', system-ui, sans-serif; } /* Base styles */ +html { + transition: background-color 0.2s ease, color 0.2s ease; +} + body { font-family: var(--font-sans); background-color: var(--color-root); @@ -65,12 +96,37 @@ body { /* Glass utility */ .glass { - background: rgba(20, 20, 20, 0.8); - border: 1px solid rgba(212, 165, 116, 0.1); + background: var(--glass-bg); + border: 1px solid var(--glass-border); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); } +.glass-heavy { + background: var(--glass-bg-heavy); + border: 1px solid var(--glass-border-heavy); + 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-accent); + background: var(--kbd-bg); + border: 1px solid var(--color-border-medium); + border-radius: 0.25rem; + box-shadow: 0 1px 0 var(--scrollbar-thumb); +} + /* Animation keyframes */ @keyframes fadeSlideIn { from { @@ -92,12 +148,12 @@ body { } } -@keyframes goldPulse { +@keyframes accentPulse { 0%, 100% { - box-shadow: 0 0 0 0 rgba(212, 165, 116, 0.4); + box-shadow: 0 0 8px var(--glow-accent-strong); } 50% { - box-shadow: 0 0 20px 4px rgba(212, 165, 116, 0.15); + box-shadow: 0 0 20px var(--glow-accent-pulse); } } @@ -110,13 +166,13 @@ body { animation: slideUp 0.3s ease-out forwards; } -.animate-gold-pulse { - animation: goldPulse 2s ease-in-out infinite; +.animate-accent-pulse { + animation: accentPulse 2s ease-in-out infinite; } /* Node selection glow */ .node-glow { - box-shadow: 0 0 20px rgba(212, 165, 116, 0.15); + box-shadow: 0 0 20px var(--glow-accent); } /* Diff overlay glow effects */ @@ -144,14 +200,37 @@ body { background: transparent; } ::-webkit-scrollbar-thumb { - background: rgba(212, 165, 116, 0.2); - border-radius: 3px; + background: var(--scrollbar-thumb); + border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { - background: rgba(212, 165, 116, 0.35); + background: var(--scrollbar-thumb-hover); } /* Override React Flow dark theme */ .react-flow__background { background-color: var(--color-root) !important; } + +/* Light theme overrides */ +[data-theme="light"] { + color-scheme: light; +} + +[data-theme="light"] .diff-faded { + opacity: 0.35; +} + +[data-theme="light"] ::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.05); +} + +[data-theme="light"] .warning-banner { + background: rgba(180, 130, 30, 0.1); + border-color: rgba(180, 130, 30, 0.3); + color: #92600a; +} + +[data-theme="dark"] { + color-scheme: dark; +} diff --git a/understand-anything-plugin/packages/dashboard/src/themes/ThemeContext.tsx b/understand-anything-plugin/packages/dashboard/src/themes/ThemeContext.tsx new file mode 100644 index 0000000..dc12fcc --- /dev/null +++ b/understand-anything-plugin/packages/dashboard/src/themes/ThemeContext.tsx @@ -0,0 +1,101 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useRef, + useState, + type ReactNode, +} from "react"; +import type { PresetId, ThemeConfig, ThemePreset } from "./types.ts"; +import { DEFAULT_THEME_CONFIG } from "./types.ts"; +import { getPreset } from "./presets.ts"; +import { applyTheme } from "./theme-engine.ts"; + +const STORAGE_KEY = "ua-theme"; + +interface ThemeContextValue { + config: ThemeConfig; + preset: ThemePreset; + setPreset: (presetId: PresetId) => void; + setAccent: (accentId: string) => void; +} + +const ThemeContext = createContext(null); + +function loadFromLocalStorage(): ThemeConfig | null { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return null; + const parsed = JSON.parse(raw); + if (parsed && typeof parsed.presetId === "string" && typeof parsed.accentId === "string") { + return parsed as ThemeConfig; + } + return null; + } catch { + return null; + } +} + +function saveToLocalStorage(config: ThemeConfig): void { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(config)); + } catch { + // Storage full or unavailable — ignore + } +} + +function resolveInitialTheme(metaTheme?: ThemeConfig | null): ThemeConfig { + return loadFromLocalStorage() ?? metaTheme ?? DEFAULT_THEME_CONFIG; +} + +interface ThemeProviderProps { + metaTheme?: ThemeConfig | null; + children: ReactNode; +} + +export function ThemeProvider({ metaTheme, children }: ThemeProviderProps) { + const [config, setConfig] = useState(() => resolveInitialTheme(metaTheme)); + const initialized = useRef(false); + + // Apply theme on mount and config changes + useEffect(() => { + applyTheme(config); + if (initialized.current) { + saveToLocalStorage(config); + } + initialized.current = true; + }, [config]); + + // Update if metaTheme arrives later (async fetch) and no localStorage preference exists + useEffect(() => { + if (metaTheme && !loadFromLocalStorage()) { + setConfig(metaTheme); + } + }, [metaTheme]); + + const setPreset = useCallback((presetId: PresetId) => { + setConfig((_prev) => { + const newPreset = getPreset(presetId); + return { presetId, accentId: newPreset.defaultAccentId }; + }); + }, []); + + const setAccent = useCallback((accentId: string) => { + setConfig((prev) => ({ ...prev, accentId })); + }, []); + + const preset = getPreset(config.presetId); + + return ( + + {children} + + ); +} + +export function useTheme(): ThemeContextValue { + const ctx = useContext(ThemeContext); + if (!ctx) throw new Error("useTheme must be used within ThemeProvider"); + return ctx; +} diff --git a/understand-anything-plugin/packages/dashboard/src/themes/index.ts b/understand-anything-plugin/packages/dashboard/src/themes/index.ts new file mode 100644 index 0000000..c033d59 --- /dev/null +++ b/understand-anything-plugin/packages/dashboard/src/themes/index.ts @@ -0,0 +1,5 @@ +export { ThemeProvider, useTheme } from "./ThemeContext.tsx"; +export { PRESETS, getPreset, getAccent } from "./presets.ts"; +export { applyTheme } from "./theme-engine.ts"; +export type { PresetId, ThemeConfig, ThemePreset, AccentSwatch } from "./types.ts"; +export { DEFAULT_THEME_CONFIG } from "./types.ts"; diff --git a/understand-anything-plugin/packages/dashboard/src/themes/presets.ts b/understand-anything-plugin/packages/dashboard/src/themes/presets.ts new file mode 100644 index 0000000..35e0517 --- /dev/null +++ b/understand-anything-plugin/packages/dashboard/src/themes/presets.ts @@ -0,0 +1,143 @@ +import type { AccentSwatch, ThemePreset } from "./types.ts"; + +const DARK_ACCENT_SWATCHES: AccentSwatch[] = [ + { id: "gold", name: "Gold", accent: "#d4a574", accentDim: "#c9a96e", accentBright: "#e8c49a" }, + { id: "ocean", name: "Ocean", accent: "#5ba4cf", accentDim: "#4e93ba", accentBright: "#7abce0" }, + { id: "emerald", name: "Emerald", accent: "#5ea67a", accentDim: "#4e9468", accentBright: "#78c492" }, + { id: "rose", name: "Rose", accent: "#cf7a8a", accentDim: "#b96e7e", accentBright: "#e094a4" }, + { id: "purple", name: "Purple", accent: "#9b7abf", accentDim: "#876bb0", accentBright: "#b494d4" }, + { id: "amber", name: "Amber", accent: "#c9963a", accentDim: "#b5862e", accentBright: "#ddb05c" }, + { id: "teal", name: "Teal", accent: "#4aab9a", accentDim: "#3d9686", accentBright: "#68c4b4" }, + { id: "silver", name: "Silver", accent: "#a0a8b0", accentDim: "#8e959c", accentBright: "#b8bfc6" }, +]; + +const LIGHT_ACCENT_SWATCHES: AccentSwatch[] = [ + { id: "indigo", name: "Indigo", accent: "#4a6fa5", accentDim: "#3d5f8f", accentBright: "#6088bf" }, + { id: "ocean", name: "Ocean", accent: "#3a8ab5", accentDim: "#2e7aa0", accentBright: "#55a0cc" }, + { id: "emerald", name: "Emerald", accent: "#3a8a5c", accentDim: "#2e7a4e", accentBright: "#55a878" }, + { id: "rose", name: "Rose", accent: "#a5566a", accentDim: "#8f4a5c", accentBright: "#bf6e82" }, + { id: "purple", name: "Purple", accent: "#6b5a9e", accentDim: "#5c4d8a", accentBright: "#8474b5" }, + { id: "amber", name: "Amber", accent: "#9e7a30", accentDim: "#8a6a28", accentBright: "#b5923e" }, + { id: "teal", name: "Teal", accent: "#2e8a7a", accentDim: "#267a6c", accentBright: "#45a595" }, + { id: "slate", name: "Slate", accent: "#5a6570", accentDim: "#4e5860", accentBright: "#6e7a85" }, +]; + +export const PRESETS: ThemePreset[] = [ + { + id: "dark-gold", + name: "Dark Gold", + isDark: true, + defaultAccentId: "gold", + accentSwatches: DARK_ACCENT_SWATCHES, + colors: { + root: "#0a0a0a", + surface: "#111111", + elevated: "#1a1a1a", + panel: "#141414", + "text-primary": "#f5f0eb", + "text-secondary": "#a39787", + "text-muted": "#6b5f53", + "node-file": "#4a7c9b", + "node-function": "#5a9e6f", + "node-class": "#8b6fb0", + "node-module": "#c9a06c", + "node-concept": "#b07a8a", + }, + }, + { + id: "dark-ocean", + name: "Dark Ocean", + isDark: true, + defaultAccentId: "ocean", + accentSwatches: DARK_ACCENT_SWATCHES, + colors: { + root: "#0a0e14", + surface: "#111820", + elevated: "#1a222c", + panel: "#141c24", + "text-primary": "#e8edf2", + "text-secondary": "#87939f", + "text-muted": "#536b7a", + "node-file": "#4a7c9b", + "node-function": "#5a9e6f", + "node-class": "#8b6fb0", + "node-module": "#c9a06c", + "node-concept": "#b07a8a", + }, + }, + { + id: "dark-forest", + name: "Dark Forest", + isDark: true, + defaultAccentId: "emerald", + accentSwatches: DARK_ACCENT_SWATCHES, + colors: { + root: "#0a100a", + surface: "#111811", + elevated: "#1a241a", + panel: "#141c14", + "text-primary": "#ebf0eb", + "text-secondary": "#87a38f", + "text-muted": "#536b5a", + "node-file": "#4a7c9b", + "node-function": "#5a9e6f", + "node-class": "#8b6fb0", + "node-module": "#c9a06c", + "node-concept": "#b07a8a", + }, + }, + { + id: "dark-rose", + name: "Dark Rose", + isDark: true, + defaultAccentId: "rose", + accentSwatches: DARK_ACCENT_SWATCHES, + colors: { + root: "#100a0a", + surface: "#181111", + elevated: "#221a1a", + panel: "#1c1414", + "text-primary": "#f2e8ea", + "text-secondary": "#9f8790", + "text-muted": "#6b535a", + "node-file": "#4a7c9b", + "node-function": "#5a9e6f", + "node-class": "#8b6fb0", + "node-module": "#c9a06c", + "node-concept": "#b07a8a", + }, + }, + { + id: "light-minimal", + name: "Light Minimal", + isDark: false, + defaultAccentId: "indigo", + accentSwatches: LIGHT_ACCENT_SWATCHES, + colors: { + root: "#f5f3f0", + surface: "#eae7e3", + elevated: "#ffffff", + panel: "#f0ede9", + "text-primary": "#1a1a1a", + "text-secondary": "#6b6b6b", + "text-muted": "#a0a0a0", + "node-file": "#3a6a87", + "node-function": "#488a5b", + "node-class": "#755d99", + "node-module": "#a88a56", + "node-concept": "#966674", + }, + }, +]; + +export function getPreset(id: string): ThemePreset { + return PRESETS.find((p) => p.id === id) ?? PRESETS[0]; +} + +export function getAccent(preset: ThemePreset, accentId: string): AccentSwatch { + return ( + preset.accentSwatches.find((s) => s.id === accentId) ?? + preset.accentSwatches.find((s) => s.id === preset.defaultAccentId) ?? + preset.accentSwatches[0] + ); +} diff --git a/understand-anything-plugin/packages/dashboard/src/themes/theme-engine.ts b/understand-anything-plugin/packages/dashboard/src/themes/theme-engine.ts new file mode 100644 index 0000000..23004ca --- /dev/null +++ b/understand-anything-plugin/packages/dashboard/src/themes/theme-engine.ts @@ -0,0 +1,56 @@ +import type { ThemeConfig } from "./types.ts"; +import { getAccent, getPreset } from "./presets.ts"; + +export function hexToRgb(hex: string): string { + const h = hex.replace("#", ""); + const n = parseInt(h, 16); + return `${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}`; +} + +function deriveFromAccent(accentHex: string, isDark: boolean): Record { + const rgb = hexToRgb(accentHex); + return { + "color-border-subtle": `rgba(${rgb}, ${isDark ? 0.12 : 0.1})`, + "color-border-medium": `rgba(${rgb}, ${isDark ? 0.25 : 0.18})`, + "glass-bg": isDark ? "rgba(20, 20, 20, 0.8)" : "rgba(255, 255, 255, 0.8)", + "glass-bg-heavy": isDark ? "rgba(20, 20, 20, 0.95)" : "rgba(255, 255, 255, 0.95)", + "glass-border": `rgba(${rgb}, ${isDark ? 0.1 : 0.08})`, + "glass-border-heavy": `rgba(${rgb}, ${isDark ? 0.15 : 0.12})`, + "scrollbar-thumb": `rgba(${rgb}, 0.2)`, + "scrollbar-thumb-hover": `rgba(${rgb}, 0.35)`, + "glow-accent": `rgba(${rgb}, 0.15)`, + "glow-accent-strong": `rgba(${rgb}, 0.4)`, + "glow-accent-pulse": `rgba(${rgb}, 0.6)`, + "color-edge": `rgba(${rgb}, 0.3)`, + "color-edge-dim": `rgba(${rgb}, 0.08)`, + "color-edge-dot": `rgba(${rgb}, 0.15)`, + "color-accent-overlay-bg": `rgba(${rgb}, 0.05)`, + "color-accent-overlay-border": `rgba(${rgb}, 0.25)`, + "kbd-bg": `rgba(${rgb}, 0.1)`, + }; +} + +export function applyTheme(config: ThemeConfig): void { + const preset = getPreset(config.presetId); + const accent = getAccent(preset, config.accentId); + const style = document.documentElement.style; + + // 1. Apply base preset colors + for (const [key, value] of Object.entries(preset.colors)) { + style.setProperty(`--color-${key}`, value); + } + + // 2. Apply accent colors from swatch + style.setProperty("--color-accent", accent.accent); + style.setProperty("--color-accent-dim", accent.accentDim); + style.setProperty("--color-accent-bright", accent.accentBright); + + // 3. Apply derived values + const derived = deriveFromAccent(accent.accent, preset.isDark); + for (const [key, value] of Object.entries(derived)) { + style.setProperty(`--${key}`, value); + } + + // 4. Set data-theme for CSS-only selectors + document.documentElement.setAttribute("data-theme", preset.isDark ? "dark" : "light"); +} diff --git a/understand-anything-plugin/packages/dashboard/src/themes/types.ts b/understand-anything-plugin/packages/dashboard/src/themes/types.ts new file mode 100644 index 0000000..2d09590 --- /dev/null +++ b/understand-anything-plugin/packages/dashboard/src/themes/types.ts @@ -0,0 +1,33 @@ +export type PresetId = + | "dark-gold" + | "dark-ocean" + | "dark-forest" + | "dark-rose" + | "light-minimal"; + +export interface AccentSwatch { + id: string; + name: string; + accent: string; + accentDim: string; + accentBright: string; +} + +export interface ThemePreset { + id: PresetId; + name: string; + isDark: boolean; + colors: Record; + accentSwatches: AccentSwatch[]; + defaultAccentId: string; +} + +export interface ThemeConfig { + presetId: PresetId; + accentId: string; +} + +export const DEFAULT_THEME_CONFIG: ThemeConfig = { + presetId: "dark-gold", + accentId: "gold", +}; diff --git a/understand-anything-plugin/packages/dashboard/src/utils/layout.ts b/understand-anything-plugin/packages/dashboard/src/utils/layout.ts index 9d451ee..6f55b53 100644 --- a/understand-anything-plugin/packages/dashboard/src/utils/layout.ts +++ b/understand-anything-plugin/packages/dashboard/src/utils/layout.ts @@ -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) => { + 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); + }); +} diff --git a/understand-anything-plugin/packages/dashboard/src/utils/layout.worker.ts b/understand-anything-plugin/packages/dashboard/src/utils/layout.worker.ts new file mode 100644 index 0000000..4f466f3 --- /dev/null +++ b/understand-anything-plugin/packages/dashboard/src/utils/layout.worker.ts @@ -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; +} + +self.onmessage = (e: MessageEvent) => { + 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 = {}; + 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); +}; diff --git a/understand-anything-plugin/skills/understand-chat/SKILL.md b/understand-anything-plugin/skills/understand-chat/SKILL.md index cfe3bb4..b49749e 100644 --- a/understand-anything-plugin/skills/understand-chat/SKILL.md +++ b/understand-anything-plugin/skills/understand-chat/SKILL.md @@ -14,7 +14,7 @@ The knowledge graph JSON has this structure: - `project` — {name, description, languages, frameworks, analyzedAt, gitCommitHash} - `nodes[]` — each has {id, type, name, filePath, summary, tags[], complexity, languageNotes?} - Node types: file, function, class, module, concept - - IDs: `file:path`, `func:path:name`, `class:path:name` + - IDs: `file:path`, `function:path:name`, `class:path:name` - `edges[]` — each has {source, target, type, direction, weight} - Key types: imports, contains, calls, depends_on - `layers[]` — each has {id, name, description, nodeIds[]} diff --git a/understand-anything-plugin/skills/understand-dashboard/SKILL.md b/understand-anything-plugin/skills/understand-dashboard/SKILL.md index b800e84..e0614d7 100644 --- a/understand-anything-plugin/skills/understand-dashboard/SKILL.md +++ b/understand-anything-plugin/skills/understand-dashboard/SKILL.md @@ -19,13 +19,31 @@ 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: +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) + + Use the Bash tool to resolve: ```bash - PLUGIN_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" + 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 ``` - Or locate it by checking these paths in order: - - `${CLAUDE_PLUGIN_ROOT}/packages/dashboard/` - - The parent directory of this skill file, then `../../packages/dashboard/` 4. Install dependencies and build if needed: ```bash diff --git a/understand-anything-plugin/skills/understand-diff/SKILL.md b/understand-anything-plugin/skills/understand-diff/SKILL.md index 4f65df5..482d35b 100644 --- a/understand-anything-plugin/skills/understand-diff/SKILL.md +++ b/understand-anything-plugin/skills/understand-diff/SKILL.md @@ -13,7 +13,7 @@ The knowledge graph JSON has this structure: - `project` — {name, description, languages, frameworks, analyzedAt, gitCommitHash} - `nodes[]` — each has {id, type, name, filePath, summary, tags[], complexity, languageNotes?} - Node types: file, function, class, module, concept - - IDs: `file:path`, `func:path:name`, `class:path:name` + - IDs: `file:path`, `function:path:name`, `class:path:name` - `edges[]` — each has {source, target, type, direction, weight} - Key types: imports, contains, calls, depends_on - `layers[]` — each has {id, name, description, nodeIds[]} diff --git a/understand-anything-plugin/skills/understand-explain/SKILL.md b/understand-anything-plugin/skills/understand-explain/SKILL.md index 78d6801..6a0c67f 100644 --- a/understand-anything-plugin/skills/understand-explain/SKILL.md +++ b/understand-anything-plugin/skills/understand-explain/SKILL.md @@ -14,7 +14,7 @@ The knowledge graph JSON has this structure: - `project` — {name, description, languages, frameworks, analyzedAt, gitCommitHash} - `nodes[]` — each has {id, type, name, filePath, summary, tags[], complexity, languageNotes?} - Node types: file, function, class, module, concept - - IDs: `file:path`, `func:path:name`, `class:path:name` + - IDs: `file:path`, `function:path:name`, `class:path:name` - `edges[]` — each has {source, target, type, direction, weight} - Key types: imports, contains, calls, depends_on - `layers[]` — each has {id, name, description, nodeIds[]} diff --git a/understand-anything-plugin/skills/understand-onboard/SKILL.md b/understand-anything-plugin/skills/understand-onboard/SKILL.md index ec4b67d..ca167a0 100644 --- a/understand-anything-plugin/skills/understand-onboard/SKILL.md +++ b/understand-anything-plugin/skills/understand-onboard/SKILL.md @@ -13,7 +13,7 @@ The knowledge graph JSON has this structure: - `project` — {name, description, languages, frameworks, analyzedAt, gitCommitHash} - `nodes[]` — each has {id, type, name, filePath, summary, tags[], complexity, languageNotes?} - Node types: file, function, class, module, concept - - IDs: `file:path`, `func:path:name`, `class:path:name` + - IDs: `file:path`, `function:path:name`, `class:path:name` - `edges[]` — each has {source, target, type, direction, weight} - Key types: imports, contains, calls, depends_on - `layers[]` — each has {id, name, description, nodeIds[]} diff --git a/understand-anything-plugin/skills/understand/SKILL.md b/understand-anything-plugin/skills/understand/SKILL.md index e13e3a1..0f8fd0f 100644 --- a/understand-anything-plugin/skills/understand/SKILL.md +++ b/understand-anything-plugin/skills/understand/SKILL.md @@ -14,6 +14,7 @@ Analyze the current codebase and produce a `knowledge-graph.json` file in `.unde - `--full` — Force a full rebuild, ignoring any existing graph - `--auto-update` — Enable automatic graph updates on commit (writes `autoUpdate: true` to `.understand-anything/config.json`) - `--no-auto-update` — Disable automatic graph updates (writes `autoUpdate: false` to `.understand-anything/config.json`) + - `--review` — Run full LLM graph-reviewer instead of inline deterministic validation - A directory path — Scope analysis to a specific subdirectory --- @@ -27,9 +28,10 @@ Determine whether to run a full analysis or incremental update. ```bash git rev-parse HEAD ``` -3. Create the intermediate output directory: +3. Create the intermediate and temp output directories: ```bash mkdir -p $PROJECT_ROOT/.understand-anything/intermediate + mkdir -p $PROJECT_ROOT/.understand-anything/tmp ``` 3.5. **Auto-update configuration:** - If `--auto-update` is in `$ARGUMENTS`: write `{"autoUpdate": true}` to `$PROJECT_ROOT/.understand-anything/config.json` @@ -44,9 +46,12 @@ Determine whether to run a full analysis or incremental update. |---|---| | `--full` flag in `$ARGUMENTS` | Full analysis (all phases) | | No existing graph or meta | Full analysis (all phases) | - | Existing graph + unchanged commit hash | Report "Graph is up to date" and STOP | + | `--review` flag + existing graph + unchanged commit hash | Skip to Phase 6 (review-only — reuse existing assembled graph) | + | Existing graph + unchanged commit hash | Ask the user: "The graph is up to date at this commit. Would you like to: **(a)** run a full rebuild (`--full`), **(b)** run the LLM graph reviewer (`--review`), or **(c)** do nothing?" Then follow their choice. If they pick (c), STOP. | | Existing graph + changed files | Incremental update (re-analyze changed files only) | + **Review-only path:** Copy the existing `knowledge-graph.json` to `$PROJECT_ROOT/.understand-anything/intermediate/assembled-graph.json`, then jump directly to Phase 6 step 3. + For incremental updates, get the changed file list: ```bash git diff ..HEAD --name-only @@ -61,7 +66,7 @@ Determine whether to run a full analysis or incremental update. find $PROJECT_ROOT -maxdepth 2 -type f -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/dist/*' | head -100 ``` Store as `$DIR_TREE`. - - Detect the project entry point by checking for common patterns: `src/index.ts`, `src/main.ts`, `src/App.tsx`, `main.py`, `main.go`, `src/main.rs`, `index.js`. Store first match as `$ENTRY_POINT`. + - Detect the project entry point by checking for common patterns (in order): `src/index.ts`, `src/main.ts`, `src/App.tsx`, `index.js`, `main.py`, `manage.py`, `app.py`, `wsgi.py`, `asgi.py`, `run.py`, `__main__.py`, `main.go`, `cmd/*/main.go`, `src/main.rs`, `src/lib.rs`, `src/main/java/**/Application.java`, `Program.cs`, `config.ru`, `index.php`. Store first match as `$ENTRY_POINT`. --- @@ -94,6 +99,9 @@ After the subagent completes, read `$PROJECT_ROOT/.understand-anything/intermedi - Languages, frameworks - File list with line counts - Complexity estimate +- Import map (`importMap`): pre-resolved project-internal imports per file + +Store `importMap` in memory as `$IMPORT_MAP` for use in Phase 2 batch construction. **Gate check:** If >200 files, inform the user and suggest scoping with a subdirectory argument. Proceed only if user confirms or add guidance that this may take a while. @@ -103,23 +111,21 @@ After the subagent completes, read `$PROJECT_ROOT/.understand-anything/intermedi ### Full analysis path -Batch the file list from Phase 1 into groups of **5-10 files each** (aim for balanced batch sizes). +Batch the file list from Phase 1 into groups of **20-30 files each** (aim for ~25 files per batch for balanced sizes). -For each batch, dispatch a subagent using the prompt template at `./file-analyzer-prompt.md`. Run up to **3 subagents concurrently** using parallel dispatch. Read the template once, then for each batch pass the full template content as the subagent's prompt, appending the following additional context: +For each batch, dispatch a subagent using the prompt template at `./file-analyzer-prompt.md`. Run up to **5 subagents concurrently** using parallel dispatch. Pass the template as the subagent's prompt, appending the following additional context: > **Additional context from main session:** > > Project: `` — `` -> Frameworks detected: `` > Languages: `` -> -> Framework-specific guidance: -> - If React/Next.js: files in `app/` or `pages/` are routes, `components/` are UI, `lib/` or `utils/` are utilities -> - If Express/Fastify: files in `routes/` are API endpoints, `middleware/` is middleware, `models/` or `db/` is data -> - If Python Django: `views.py` are controllers, `models.py` is data, `urls.py` is routing, `templates/` is UI -> - If Go: `cmd/` is entry points, `internal/` is private packages, `pkg/` is public packages -> -> Use this context to produce more accurate summaries and better classify file roles. + +Before dispatching each batch, construct `batchImportData` from `$IMPORT_MAP`: +```json +batchImportData = {} +for each file in this batch: + batchImportData[file.path] = $IMPORT_MAP[file.path] ?? [] +``` Fill in batch-specific parameters below and dispatch: @@ -130,8 +136,10 @@ Fill in batch-specific parameters below and dispatch: > Batch index: `` > Write output to: `$PROJECT_ROOT/.understand-anything/intermediate/batch-.json` > -> All project files (for import resolution): -> `` +> Pre-resolved import data for this batch (use this for all import edge creation — do NOT re-resolve imports from source): +> ```json +> +> ``` > > Files to analyze in this batch: > 1. `` ( lines) @@ -144,7 +152,7 @@ After ALL batches complete, read each `batch-.json` file and merge: ### Incremental update path -Use the changed files list from Phase 0. Batch and dispatch file-analyzer subagents using the same process as above, but only for changed files. +Use the changed files list from Phase 0. Batch and dispatch file-analyzer subagents using the same process as above (20-30 files per batch, up to 5 concurrent, with batchImportData constructed from $IMPORT_MAP), but only for changed files. After batches complete, merge with the existing graph: 1. Remove old nodes whose `filePath` matches any changed file @@ -165,7 +173,12 @@ Merge all file-analyzer results into a single set of nodes and edges. Then perfo ## Phase 4 — ARCHITECTURE -Dispatch a subagent using the prompt template at `./architecture-analyzer-prompt.md`. Read the template file and pass the full content as the subagent's prompt, appending the following additional context: +**Build the combined prompt template:** +1. Read the base template at `./architecture-analyzer-prompt.md`. +2. **Language context injection:** For each language detected in Phase 1 (e.g., `python`), read the file at `./languages/.md` (e.g., `./languages/python.md`) and append its content after the base template under a `## Language Context` header. If the file does not exist for a detected language, skip it silently and continue. These files are in the `languages/` subdirectory next to this SKILL.md file. +3. **Framework addendum injection:** For each framework detected in Phase 1 (e.g., `Django`), read the file at `./frameworks/.md` (e.g., `./frameworks/django.md`) and append its full content after the language context. If the file does not exist for a detected framework, skip it silently and continue. These files are in the `frameworks/` subdirectory next to this SKILL.md file. + +Pass the combined content as the subagent's prompt, appending the following additional context: > **Additional context from main session:** > @@ -176,13 +189,7 @@ Dispatch a subagent using the prompt template at `./architecture-analyzer-prompt > $DIR_TREE > ``` > -> Framework-specific layer hints: -> - If React/Next.js: `app/` or `pages/` → UI Layer, `api/` → API Layer, `lib/` → Service Layer, `components/` → UI Layer -> - If Express: `routes/` → API Layer, `controllers/` → Service Layer, `models/` → Data Layer, `middleware/` → Middleware Layer -> - If Python Django: `views/` → API Layer, `models/` → Data Layer, `templates/` → UI Layer, `management/` → CLI Layer -> - If Go: `cmd/` → Entry Points, `internal/` → Service Layer, `pkg/` → Shared Library, `api/` → API Layer -> -> Use the directory tree and framework hints to inform layer assignments. Directory structure is strong evidence for layer boundaries. +> Use the directory tree, language context, and framework addendums (appended above) to inform layer assignments. Directory structure is strong evidence for layer boundaries. Pass these parameters in the dispatch prompt: @@ -193,7 +200,7 @@ Pass these parameters in the dispatch prompt: > > File nodes: > ```json -> [list of {id, name, filePath, summary, tags} for all file-type nodes] +> [list of {id, name, filePath, summary, tags} for all file-type nodes — omit complexity, languageNotes] > ``` > > Import edges: @@ -260,19 +267,19 @@ Pass these parameters in the dispatch prompt: > Project: `` — `` > Languages: `` > -> Nodes (summarized): +> Nodes (file nodes only): > ```json -> [list of {id, name, filePath, summary, type} for key nodes] +> [list of {id, name, filePath, summary, type} for file-type nodes ONLY — do NOT include function or class nodes] > ``` > > Layers: > ```json -> [layers from Phase 4] +> [list of {id, name, description} for each layer — omit nodeIds] > ``` > -> Key edges: +> Edges (imports and calls only): > ```json -> [imports and calls edges] +> [list of edges where type is "imports" or "calls" only — exclude all other edge types] > ``` 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**: @@ -333,7 +340,95 @@ Assemble the full KnowledgeGraph JSON object: 2. Write the assembled graph to `$PROJECT_ROOT/.understand-anything/intermediate/assembled-graph.json`. -3. Dispatch a subagent using the prompt template at `./graph-reviewer-prompt.md`. Read the template file and pass the full content as the subagent's prompt, appending the following additional context: +3. **Check `$ARGUMENTS` for `--review` flag.** Then run the appropriate validation path: + +--- + +#### Default path (no `--review`): inline deterministic validation + +Write the following Node.js script to `$PROJECT_ROOT/.understand-anything/tmp/ua-inline-validate.cjs`: + +```javascript +#!/usr/bin/env node +const fs = require('fs'); +const graphPath = process.argv[2]; +const outputPath = process.argv[3]; +try { + const graph = JSON.parse(fs.readFileSync(graphPath, 'utf8')); + const issues = [], warnings = []; + if (!Array.isArray(graph.nodes)) { issues.push('graph.nodes is missing or not an array'); graph.nodes = []; } + if (!Array.isArray(graph.edges)) { issues.push('graph.edges is missing or not an array'); graph.edges = []; } + const nodeIds = new Set(); + const seen = new Map(); + graph.nodes.forEach((n, i) => { + if (!n.id) { issues.push(`Node[${i}] missing id`); return; } + if (!n.type) issues.push(`Node[${i}] '${n.id}' missing type`); + if (!n.name) issues.push(`Node[${i}] '${n.id}' missing name`); + if (!n.summary) issues.push(`Node[${i}] '${n.id}' missing summary`); + if (!n.tags || !n.tags.length) issues.push(`Node[${i}] '${n.id}' missing tags`); + if (seen.has(n.id)) issues.push(`Duplicate node ID '${n.id}' at indices ${seen.get(n.id)} and ${i}`); + else seen.set(n.id, i); + nodeIds.add(n.id); + }); + graph.edges.forEach((e, i) => { + if (!nodeIds.has(e.source)) issues.push(`Edge[${i}] source '${e.source}' not found`); + if (!nodeIds.has(e.target)) issues.push(`Edge[${i}] target '${e.target}' not found`); + }); + const fileNodes = graph.nodes.filter(n => n.type === 'file').map(n => n.id); + const assigned = new Map(); + if (!Array.isArray(graph.layers)) { if (graph.layers) warnings.push('graph.layers is not an array'); graph.layers = []; } + if (!Array.isArray(graph.tour)) { if (graph.tour) warnings.push('graph.tour is not an array'); graph.tour = []; } + graph.layers.forEach(layer => { + (layer.nodeIds || []).forEach(id => { + if (!nodeIds.has(id)) issues.push(`Layer '${layer.id}' refs missing node '${id}'`); + if (assigned.has(id)) issues.push(`Node '${id}' appears in multiple layers`); + assigned.set(id, layer.id); + }); + }); + fileNodes.forEach(id => { + if (!assigned.has(id)) issues.push(`File node '${id}' not in any layer`); + }); + graph.tour.forEach((step, i) => { + (step.nodeIds || []).forEach(id => { + if (!nodeIds.has(id)) issues.push(`Tour step[${i}] refs missing node '${id}'`); + }); + }); + const withEdges = new Set([ + ...graph.edges.map(e => e.source), + ...graph.edges.map(e => e.target) + ]); + graph.nodes.forEach(n => { + if (!withEdges.has(n.id)) warnings.push(`Node '${n.id}' has no edges (orphan)`); + }); + const stats = { + totalNodes: graph.nodes.length, + totalEdges: graph.edges.length, + totalLayers: graph.layers.length, + tourSteps: graph.tour.length, + nodeTypes: graph.nodes.reduce((a, n) => { a[n.type] = (a[n.type]||0)+1; return a; }, {}), + edgeTypes: graph.edges.reduce((a, e) => { a[e.type] = (a[e.type]||0)+1; return a; }, {}) + }; + fs.writeFileSync(outputPath, JSON.stringify({ issues, warnings, stats }, null, 2)); + process.exit(0); +} catch (err) { process.stderr.write(err.message + '\n'); process.exit(1); } +``` + +Execute it: +```bash +node $PROJECT_ROOT/.understand-anything/tmp/ua-inline-validate.cjs \ + "$PROJECT_ROOT/.understand-anything/intermediate/assembled-graph.json" \ + "$PROJECT_ROOT/.understand-anything/intermediate/review.json" +``` + +If the script exits non-zero, read stderr, fix the script, and retry once. + +--- + +#### `--review` path: full LLM reviewer + +If `--review` IS in `$ARGUMENTS`, dispatch the LLM graph-reviewer subagent as follows: + +Dispatch a subagent using the prompt template at `./graph-reviewer-prompt.md`. Read the template file and pass the full content as the subagent's prompt, appending the following additional context: > **Additional context from main session:** > @@ -349,14 +444,16 @@ Assemble the full KnowledgeGraph JSON object: Pass these parameters in the dispatch prompt: - > Validate the knowledge graph at `$PROJECT_ROOT/.understand-anything/intermediate/assembled-graph.json`. - > Project root: `$PROJECT_ROOT` - > Read the file and validate it for completeness and correctness. - > Write output to: `$PROJECT_ROOT/.understand-anything/intermediate/review.json` +> Validate the knowledge graph at `$PROJECT_ROOT/.understand-anything/intermediate/assembled-graph.json`. +> Project root: `$PROJECT_ROOT` +> Read the file and validate it for completeness and correctness. +> Write output to: `$PROJECT_ROOT/.understand-anything/intermediate/review.json` -4. After the subagent completes, read `$PROJECT_ROOT/.understand-anything/intermediate/review.json`. +--- -5. **If `approved: false`:** +4. Read `$PROJECT_ROOT/.understand-anything/intermediate/review.json`. + +5. **If `issues` array is non-empty:** - Review the `issues` list - Apply automated fixes where possible: - Remove edges with dangling references @@ -365,7 +462,7 @@ Pass these parameters in the dispatch prompt: - Re-run the final graph validation after automated fixes - If critical issues remain after one fix attempt, save the graph anyway but include the warnings in the final report and mark dashboard auto-launch as skipped -6. **If `approved: true`:** Proceed to Phase 7. +6. **If `issues` array is empty:** Proceed to Phase 7. --- @@ -417,6 +514,7 @@ Pass these parameters in the dispatch prompt: 3. Clean up intermediate files: ```bash rm -rf $PROJECT_ROOT/.understand-anything/intermediate + rm -rf $PROJECT_ROOT/.understand-anything/tmp ``` 4. Report a summary to the user containing: @@ -437,7 +535,7 @@ Pass these parameters in the dispatch prompt: ## Error Handling - If any subagent dispatch fails, retry **once** with the same prompt plus additional context about the failure. -- Track all warnings and errors from each phase in a `$PHASE_WARNINGS` list. Pass this list to the graph-reviewer in Phase 6 for comprehensive validation. +- Track all warnings and errors from each phase in a `$PHASE_WARNINGS` list. When using `--review`, pass this list to the graph-reviewer in Phase 6. On the default path, include accumulated warnings in the Phase 7 final report. - If it fails a second time, skip that phase and continue with partial results. - ALWAYS save partial results — a partial graph is better than no graph. - Report any skipped phases or errors in the final summary so the user knows what happened. @@ -451,7 +549,7 @@ Pass these parameters in the dispatch prompt: | Type | Description | ID Convention | |---|---|---| | `file` | Source file | `file:` | -| `function` | Function or method | `func::` | +| `function` | Function or method | `function::` | | `class` | Class, interface, or type | `class::` | | `module` | Logical module or package | `module:` | | `concept` | Abstract concept or pattern | `concept:` | diff --git a/understand-anything-plugin/skills/understand/architecture-analyzer-prompt.md b/understand-anything-plugin/skills/understand/architecture-analyzer-prompt.md index 2200252..fb4dca4 100644 --- a/understand-anything-plugin/skills/understand/architecture-analyzer-prompt.md +++ b/understand-anything-plugin/skills/understand/architecture-analyzer-prompt.md @@ -82,11 +82,36 @@ Classify each directory name against known architectural patterns: | `hooks` | `hooks` | | `store`, `state`, `reducers`, `actions`, `slices` | `state` | | `assets`, `static`, `public` | `assets` | +| `migrations` | `data` | +| `management`, `commands` | `config` | +| `templatetags` | `utility` | +| `signals` | `service` | +| `serializers` | `api` | +| `cmd` | `entry` | +| `internal` | `service` | +| `pkg` | `utility` | +| `src/main/java` | `service` | +| `src/test/java` | `test` | +| `dto`, `request`, `response` | `types` | +| `entity` | `data` | +| `controller` | `api` | +| `routers` | `api` | +| `composables` | `service` | +| `blueprints` | `api` | +| `mailers`, `jobs`, `channels` | `service` | +| `bin` | `entry` | Also check file-level patterns: -- Files matching `*.test.*` or `*.spec.*` -> `test` -- Files matching `*.d.ts` -> `types` -- Files named `index.ts`/`index.js` at a package root -> `entry` +- Files matching `*.test.*` or `*.spec.*` or `test_*.py` or `*_test.go` or `*Test.java` or `*_spec.rb` or `*Test.php` or `*Tests.cs` -> `test` +- Files matching `*.d.ts` -> `types` (TypeScript declaration files only) +- Files named `index.ts`, `index.js`, or `__init__.py` at a package/directory root -> `entry` +- Files named `manage.py` at the project root -> `entry` (Django management entry point) +- Files named `wsgi.py` or `asgi.py` -> `config` (Python WSGI/ASGI server config) +- Files named `main.go` at `cmd/*/` -> `entry` (Go binary entry points) +- Files named `main.rs` or `lib.rs` at `src/` -> `entry` (Rust crate roots) +- Files named `Application.java` or `Program.cs` -> `entry` (JVM / .NET entry points) +- Files named `config.ru` -> `entry` (Ruby Rack entry point) +- Files named `Cargo.toml`, `go.mod`, `Gemfile`, `pom.xml`, `build.gradle`, `composer.json` -> `config` (language-level project config) **F. Dependency Direction** @@ -139,7 +164,7 @@ For each pair of groups with imports between them, determine the dominant direct Before writing the script, create its input JSON file: ```bash -cat > /tmp/ua-arch-input.json << 'ENDJSON' +cat > $PROJECT_ROOT/.understand-anything/tmp/ua-arch-input.json << 'ENDJSON' { "fileNodes": [], "importEdges": [] @@ -152,7 +177,7 @@ ENDJSON After writing the script, execute it: ```bash -node /tmp/ua-arch-analyze.js /tmp/ua-arch-input.json /tmp/ua-arch-results.json +node $PROJECT_ROOT/.understand-anything/tmp/ua-arch-analyze.js $PROJECT_ROOT/.understand-anything/tmp/ua-arch-input.json $PROJECT_ROOT/.understand-anything/tmp/ua-arch-results.json ``` If the script exits with a non-zero code, read stderr, diagnose the issue, fix the script, and re-run. You have up to 2 retry attempts. @@ -161,7 +186,7 @@ If the script exits with a non-zero code, read stderr, diagnose the issue, fix t ## Phase 2 -- Semantic Layer Assignment -After the script completes, read `/tmp/ua-arch-results.json`. Use the structural analysis as the primary input for your layer decisions. Do NOT re-read source files or re-analyze imports -- trust the script's results entirely. +After the script completes, read `$PROJECT_ROOT/.understand-anything/tmp/ua-arch-results.json`. Use the structural analysis as the primary input for your layer decisions. Do NOT re-read source files or re-analyze imports -- trust the script's results entirely. ### Step 1 -- Evaluate Directory Groups as Layer Candidates diff --git a/understand-anything-plugin/skills/understand/file-analyzer-prompt.md b/understand-anything-plugin/skills/understand/file-analyzer-prompt.md index 4f28e77..26df55f 100644 --- a/understand-anything-plugin/skills/understand/file-analyzer-prompt.md +++ b/understand-anything-plugin/skills/understand/file-analyzer-prompt.md @@ -12,7 +12,7 @@ For each file in the batch provided to you, extract structural data via a script ## Phase 1 -- Structural Extraction Script -Write a script that reads each source file in your batch and extracts deterministic structural information. Choose the best language for this task -- Node.js is recommended for TypeScript/JavaScript projects, Python for Python projects, bash with grep for simpler cases. +Write a script that reads each source file in your batch and extracts deterministic structural information. Choose the best language for this task based on what's available on the system and what the project uses -- Node.js, Python, or bash with grep are all valid choices. ### Script Requirements @@ -20,11 +20,14 @@ Write a script that reads each source file in your batch and extracts determinis ```json { "projectRoot": "/path/to/project", - "allProjectFiles": ["src/index.ts", "src/utils.ts", "..."], "batchFiles": [ {"path": "src/index.ts", "language": "typescript", "sizeLines": 150}, {"path": "src/utils.ts", "language": "typescript", "sizeLines": 80} - ] + ], + "batchImportData": { + "src/index.ts": ["src/utils.ts", "src/config.ts"], + "src/utils.ts": [] + } } ``` 2. **Write** results JSON to the path given as the second argument. @@ -45,10 +48,9 @@ For each file in `batchFiles`, read the file content and extract: - Detection approach: match `class `, `interface `, `type =`, `struct `, `trait `, `impl ` as appropriate **Imports:** -- Source module path (exactly as written in the import statement) -- Imported specifiers (named imports, default import, namespace import) -- Line number -- For relative imports (starting with `./` or `../`), compute the resolved path relative to project root. Cross-reference against `allProjectFiles` to confirm the resolved path exists. Mark unresolvable imports. +- Do NOT extract imports in the script. Import resolution has already been performed by the project scanner. +- The pre-resolved imports for each file are provided in `batchImportData` in the input JSON. +- Do not include an `imports` field in the script output — import edges will be created in Phase 2 using `batchImportData` directly. **Exports:** - Exported names and their line numbers @@ -57,7 +59,7 @@ For each file in `batchFiles`, read the file content and extract: **Basic Metrics:** - Total line count - Non-empty line count (lines that are not blank or comment-only) -- Import count (number of import statements) +- Import count — use `batchImportData[file.path].length` from the input JSON (do not count from source) - Export count (number of export statements) - Function count, class count @@ -82,10 +84,6 @@ The script must write this exact JSON structure to the output file: "classes": [ {"name": "App", "startLine": 50, "endLine": 140, "methods": ["init", "run"], "properties": ["config", "logger"]} ], - "imports": [ - {"source": "./utils", "resolvedPath": "src/utils.ts", "specifiers": ["formatDate", "sanitize"], "line": 1, "isExternal": false}, - {"source": "express", "resolvedPath": null, "specifiers": ["default"], "line": 2, "isExternal": true} - ], "exports": [ {"name": "App", "line": 50, "isDefault": true}, {"name": "createApp", "line": 145, "isDefault": false} @@ -111,11 +109,11 @@ The script must write this exact JSON structure to the output file: Before writing the script, create its input JSON file. **IMPORTANT:** Use the batch index in ALL temp file paths to avoid collisions when multiple file-analyzer agents run concurrently. ```bash -cat > /tmp/ua-file-analyzer-input-.json << 'ENDJSON' +cat > $PROJECT_ROOT/.understand-anything/tmp/ua-file-analyzer-input-.json << 'ENDJSON' { "projectRoot": "", - "allProjectFiles": [], - "batchFiles": [] + "batchFiles": [], + "batchImportData": } ENDJSON ``` @@ -125,7 +123,10 @@ ENDJSON After writing the script, execute it. **Use the batch index in every temp file path** — multiple file-analyzer agents run in parallel and must not overwrite each other's files: ```bash -node /tmp/ua-file-extract-.js /tmp/ua-file-analyzer-input-.json /tmp/ua-file-extract-results-.json +# For Node.js scripts: +node $PROJECT_ROOT/.understand-anything/tmp/ua-file-extract-.js $PROJECT_ROOT/.understand-anything/tmp/ua-file-analyzer-input-.json $PROJECT_ROOT/.understand-anything/tmp/ua-file-extract-results-.json +# For Python scripts: +python3 $PROJECT_ROOT/.understand-anything/tmp/ua-file-extract-.py $PROJECT_ROOT/.understand-anything/tmp/ua-file-analyzer-input-.json $PROJECT_ROOT/.understand-anything/tmp/ua-file-extract-results-.json ``` If the script exits with a non-zero code, read stderr, diagnose the issue, fix the script, and re-run. You have up to 2 retry attempts. @@ -134,7 +135,7 @@ If the script exits with a non-zero code, read stderr, diagnose the issue, fix t ## Phase 2 -- Semantic Analysis -After the script completes, read `/tmp/ua-file-extract-results-.json`. Use these structured results as the foundation for your analysis. Do NOT re-read the source files unless the script skipped a file or you need to understand a specific code pattern that the script could not capture. +After the script completes, read `$PROJECT_ROOT/.understand-anything/tmp/ua-file-extract-results-.json`. Use these structured results as the foundation for your analysis. Do NOT re-read the source files unless the script skipped a file or you need to understand a specific code pattern that the script could not capture. For each file in the script's `results` array, produce `GraphNode` and `GraphEdge` objects by combining the script's structural data with your expert judgment. @@ -166,14 +167,22 @@ Indicators from script data: - Filename contains `.test.` or `.spec.` = `test` - Exports a class with `Handler` or `Controller` in the name = `api-handler` - Only type/interface exports = `type-definition` -- Named `index.ts` at a directory root with re-exports = `entry-point` +- Named `index.ts` or `index.js` at a directory root with re-exports = `entry-point` (JavaScript/TypeScript barrel) +- Named `__init__.py` at a package root with imports or re-exports = `entry-point` (Python package barrel) +- Named `manage.py` = `entry-point` (Django management script) +- Named `main.go` in `cmd/` directory = `entry-point` (Go binary) +- Named `main.rs` or `lib.rs` in `src/` = `entry-point` (Rust crate root) +- Named `Application.java` or `Main.java` = `entry-point` (Java application) +- Named `Program.cs` = `entry-point` (.NET application) +- Named `config.ru` = `entry-point` (Ruby Rack server) +- Named `mod.rs` in a directory = `barrel` (Rust module barrel) **Language Notes** (optional, your expert judgment): If the structural data reveals notable language-specific patterns (e.g., many generic type parameters, decorator usage, complex trait bounds), add a brief `languageNotes` string. Only add this when genuinely educational. ### Step 2 -- Create Function and Class Nodes -For significant functions and classes from the script output, create `func:` and `class:` nodes. +For significant functions and classes from the script output, create `function:` and `class:` nodes. **Significance filter** -- only create nodes for: - Functions/methods with 10+ lines (skip trivial one-liners) @@ -191,7 +200,7 @@ Using the script's import, export, and structural data, create edges: | Edge Type | When to Create | Weight | Direction | |---|---|---|---| | `contains` | File contains a function or class node you created | `1.0` | `forward` | -| `imports` | File imports from another project file (use `resolvedPath` from script, skip external imports where `isExternal: true`) | `0.7` | `forward` | +| `imports` | File imports from another project file (use `batchImportData[filePath]` from input JSON — external imports already filtered out) | `0.7` | `forward` | | `calls` | A function in this file calls a function in another file (infer from imports + function names when confident) | `0.8` | `forward` | | `inherits` | A class extends another class in the project | `0.9` | `forward` | | `implements` | A class implements an interface in the project | `0.9` | `forward` | @@ -199,7 +208,7 @@ Using the script's import, export, and structural data, create edges: | `depends_on` | File has runtime dependency on another project file (broader than imports -- includes dynamic requires, lazy loads) | `0.6` | `forward` | | `tested_by` | Source file is tested by a test file (infer from test file imports and naming conventions) | `0.5` | `forward` | -**Import edge creation rule:** For each import in the script output where `isExternal` is `false` and `resolvedPath` is non-null, create an `imports` edge from the current file node to `file:`. Do NOT create edges for external package imports. +**Import edge creation rule:** For each resolved path in `batchImportData[filePath]` (provided in the input JSON), create an `imports` edge from the current file node to `file:`. The `batchImportData` values contain only resolved project-internal paths — external packages have already been filtered out. Do NOT attempt to re-resolve imports from source. Do NOT use edge types not listed in this table. @@ -210,10 +219,10 @@ You MUST use these exact prefixes for node IDs: | Node Type | ID Format | Example | |---|---|---| | File | `file:` | `file:src/index.ts` | -| Function | `func::` | `func:src/utils.ts:formatDate` | +| Function | `function::` | `function:src/utils.ts:formatDate` | | Class | `class::` | `class:src/models/User.ts:User` | -**Scope restriction:** Only produce `file:`, `func:`, and `class:` nodes. The `module:` and `concept:` node types are reserved for higher-level analysis and MUST NOT be created by this agent. +**Scope restriction:** Only produce `file:`, `function:`, and `class:` nodes. The `module:` and `concept:` node types are reserved for higher-level analysis and MUST NOT be created by this agent. ## Output Format @@ -233,7 +242,7 @@ Produce a single, valid JSON block. Validate it mentally before writing -- malfo "languageNotes": "TypeScript barrel file using re-exports." }, { - "id": "func:src/utils.ts:formatDate", + "id": "function:src/utils.ts:formatDate", "type": "function", "name": "formatDate", "filePath": "src/utils.ts", @@ -253,7 +262,7 @@ Produce a single, valid JSON block. Validate it mentally before writing -- malfo }, { "source": "file:src/utils.ts", - "target": "func:src/utils.ts:formatDate", + "target": "function:src/utils.ts:formatDate", "type": "contains", "direction": "forward", "weight": 1.0 @@ -284,13 +293,42 @@ Produce a single, valid JSON block. Validate it mentally before writing -- malfo - `direction` (string) -- always `forward` - `weight` (number) -- must match the weight specified in the edge type table +## Language and Framework Quick Reference + +Use these hints to improve tag and edge accuracy for common patterns. Your training knowledge covers these — this is a fast lookup for the most impactful signals. + +**Tag signals:** + +| Signal | Tags to apply | +|---|---| +| File in `hooks/`, exports a function starting with `use` | `hook`, `service` | +| File in `contexts/` or `context/`, exports a Provider component | `service`, `state` | +| File in `pages/` or `views/` | `ui`, `routing` | +| File in `store/`, `slices/`, `reducers/`, `state/` | `state` | +| File in `services/`, `api/`, `client/` | `service` | +| `__init__.py` at a package root with re-exports | `entry-point`, `barrel` | +| `manage.py` at the project root | `entry-point` | +| `mod.rs` in a directory | `barrel` | +| `main.go` in a `cmd/` subdirectory | `entry-point` | + +**Edge signals:** + +| Pattern | Edge to create | +|---|---| +| React component renders another component in its JSX | `contains` from parent to child | +| Component/hook calls a custom hook (`useX`) | `depends_on` from consumer to hook file | +| Context provider wraps components | `exports` from provider to context definition | +| Component calls `useContext` or custom context hook | `depends_on` from consumer to context definition | +| Python file uses `from x import y` where x is a project file | `imports` edge (same rule as JS/TS) | +| Go file `import`s an internal package path | `imports` edge to the resolved file | + ## Critical Constraints -- NEVER invent file paths. Every `filePath` and every file reference in node IDs must correspond to a real file from the script's output or the project file list provided to you. -- NEVER create edges to nodes that do not exist. If an import target is external (`isExternal: true` in script output), do NOT create an edge for it. +- NEVER invent file paths. Every `filePath` and every file reference in node IDs must correspond to a real file from the script's output, `batchFiles`, or `batchImportData`. +- NEVER create edges to nodes that do not exist. Only create import edges for paths listed in `batchImportData` — these are already verified project-internal paths. - ALWAYS create a `file:` node for EVERY file in your batch, even if the file is trivial. -- Only create `func:` and `class:` nodes for significant code elements (see significance filter above). -- For import edges, use the script's `resolvedPath` field directly. Do NOT attempt to resolve import paths yourself -- the script already did this deterministically. +- Only create `function:` and `class:` nodes for significant code elements (see significance filter above). +- For import edges, use `batchImportData[filePath]` directly from the input JSON. Do NOT attempt to resolve import paths yourself -- the project scanner already did this deterministically. - NEVER produce duplicate node IDs within your batch. - NEVER create self-referencing edges (where source equals target). - Trust the script's structural extraction. Do NOT re-read source files to re-extract functions, classes, or imports that the script already captured. Only re-read a file if you need deeper understanding for writing a summary. diff --git a/understand-anything-plugin/skills/understand/frameworks/django.md b/understand-anything-plugin/skills/understand/frameworks/django.md new file mode 100644 index 0000000..db4ea84 --- /dev/null +++ b/understand-anything-plugin/skills/understand/frameworks/django.md @@ -0,0 +1,67 @@ +# Django Framework Addendum + +> Injected into file-analyzer and architecture-analyzer prompts when Django is detected. +> Do NOT use as a standalone prompt — always appended to the base prompt template. + +## Django Project Structure + +When analyzing a Django project, apply these additional conventions on top of the base analysis rules. + +### Canonical File Roles + +| File / Pattern | Role | Tags | +|---|---|---| +| `manage.py` | CLI entry point for dev server, migrations, management commands | `entry-point`, `config` | +| `*/settings.py`, `*/settings/*.py` | Project-wide configuration (DB, installed apps, middleware) | `config` | +| `*/urls.py` | URL routing — maps URL patterns to views | `api-handler`, `routing` | +| `*/views.py`, `*/views/*.py` | Request handlers (function-based or class-based views) | `api-handler`, `controller` | +| `*/models.py`, `*/models/*.py` | ORM models — map to database tables | `data-model` | +| `*/serializers.py` | DRF serializers — convert models to/from JSON | `serialization`, `api-handler` | +| `*/forms.py` | Django forms — validation and rendering logic | `validation`, `ui` | +| `*/admin.py` | Admin site registrations — exposes models in Django admin | `config` | +| `*/signals.py` | Signal handlers — cross-cutting side effects on model events | `event-handler` | +| `*/tasks.py` | Celery async task definitions | `service`, `event-handler` | +| `*/middleware.py`, `*/middleware/*.py` | Request/response middleware classes | `middleware` | +| `*/permissions.py` | DRF permission classes | `middleware`, `validation` | +| `*/filters.py` | DRF filter backends | `utility` | +| `*/migrations/*.py` | Auto-generated schema migrations — do not summarize individually | `config` | +| `*/templates/**/*.html` | Django HTML templates | `ui` | +| `*/templatetags/*.py` | Custom template filters and tags | `utility` | +| `*/management/commands/*.py` | Custom management commands (`./manage.py mycommand`) | `config`, `entry-point` | +| `wsgi.py`, `asgi.py` | WSGI/ASGI server adapter — production entry point | `config`, `entry-point` | +| `*/apps.py` | App configuration and startup hooks (`AppConfig`) | `config` | +| `*/tests.py`, `*/tests/*.py` | Unit and integration tests | `test` | + +### Edge Patterns to Look For + +**URL routing graph** — Create `calls` edges from `urls.py` nodes to their corresponding view nodes when `path()` or `re_path()` maps a URL pattern to a view function or class. These edges represent the HTTP routing chain. + +**Signal wiring** — When `signals.py` uses `post_save.connect(handler, sender=Model)` or `@receiver(post_save, sender=Model)`, create `subscribes` edges from the signal handler function to the model class. Create `publishes` edges from the model to the signal handler to show the trigger direction. + +**ORM relationships** — When `models.py` defines `ForeignKey`, `OneToOneField`, or `ManyToManyField`, create `depends_on` edges between the model classes with a description indicating the relationship type and cardinality. + +**Serializer-to-model binding** — When a DRF serializer has `model = MyModel` in its `Meta` class, create a `depends_on` edge from the serializer to the model. + +**View-to-serializer binding** — When a DRF ViewSet or APIView references a serializer class, create a `depends_on` edge from the view to the serializer. + +### Architectural Layers for Django + +Assign nodes to these layers when detected: + +| Layer ID | Layer Name | What Goes Here | +|---|---|---| +| `layer:api` | API Layer | `views.py`, `serializers.py`, `urls.py`, DRF ViewSets and APIViews | +| `layer:data` | Data Layer | `models.py`, `migrations/`, database utility files | +| `layer:service` | Service Layer | `signals.py`, `tasks.py`, custom managers, service modules | +| `layer:ui` | UI Layer | `templates/`, `forms.py`, `templatetags/` | +| `layer:middleware` | Middleware Layer | `middleware.py`, `permissions.py`, authentication backends | +| `layer:config` | Config Layer | `settings.py`, `urls.py` (root), `wsgi.py`, `asgi.py`, `apps.py`, `manage.py` | +| `layer:test` | Test Layer | `tests.py`, `tests/` directory, `conftest.py` | + +### Notable Patterns to Capture in languageLesson + +- **Fat models vs. thin views**: Django encourages business logic in model methods, keeping views thin HTTP adapters +- **Django ORM lazy evaluation**: QuerySets are not evaluated until iterated — chain filters without DB hits +- **Class-based views (CBVs)**: Mixins like `LoginRequiredMixin`, `PermissionRequiredMixin` compose behavior through multiple inheritance +- **Signal anti-patterns**: Signals create invisible coupling; a signal in `signals.py` may be triggered by a `save()` call anywhere in the codebase +- **App isolation**: Each Django app (`INSTALLED_APPS`) should be self-contained with its own models, views, urls, and migrations diff --git a/understand-anything-plugin/skills/understand/frameworks/express.md b/understand-anything-plugin/skills/understand/frameworks/express.md new file mode 100644 index 0000000..2970354 --- /dev/null +++ b/understand-anything-plugin/skills/understand/frameworks/express.md @@ -0,0 +1,57 @@ +# Express Framework Addendum + +> Injected into file-analyzer and architecture-analyzer prompts when Express is detected. +> Do NOT use as a standalone prompt — always appended to the base prompt template. + +## Express Project Structure + +When analyzing an Express project, apply these additional conventions on top of the base analysis rules. + +### Canonical File Roles + +| File / Pattern | Role | Tags | +|---|---|---| +| `app.js`, `app.ts` | Application entry point — creates Express app, mounts middleware and routes | `entry-point`, `config` | +| `server.js`, `server.ts`, `index.js`, `index.ts` | Server bootstrap — starts HTTP listener, may import app | `entry-point`, `config` | +| `routes/*.js`, `routes/*.ts` | Route definitions — map HTTP methods and paths to handlers | `api-handler`, `routing` | +| `controllers/*.js`, `controllers/*.ts` | Request handlers — process requests, orchestrate services, return responses | `api-handler`, `service` | +| `models/*.js`, `models/*.ts` | Data models — Mongoose schemas, Sequelize models, or plain data definitions | `data-model` | +| `middleware/*.js`, `middleware/*.ts` | Middleware functions — authentication, logging, validation, error handling | `middleware` | +| `services/*.js`, `services/*.ts` | Business logic — domain operations decoupled from HTTP layer | `service` | +| `db/*.js`, `db/*.ts`, `database/*.js` | Database connection and configuration | `data-model`, `config` | +| `config/*.js`, `config/*.ts` | Application configuration — environment variables, feature flags | `config` | +| `validators/*.js`, `validators/*.ts` | Request validation schemas (Joi, Zod, express-validator) | `validation`, `utility` | +| `utils/*.js`, `utils/*.ts` | Shared utility functions | `utility` | +| `tests/*.js`, `test/*.js`, `__tests__/*.js` | Unit and integration tests | `test` | + +### Edge Patterns to Look For + +**Route mounting** — When `app.use('/api/users', usersRouter)` mounts a router, create `depends_on` edges from the main app to the router module. These edges represent the HTTP routing tree. + +**Middleware chain** — When `app.use(cors())`, `app.use(authMiddleware)`, or `router.use(validate)` registers middleware, create middleware edges from the app or router to the middleware function. Order matters — middleware executes in registration order. + +**Controller-to-service calls** — When a controller imports and calls a service function, create `depends_on` edges from the controller to the service. This represents the separation between HTTP handling and business logic. + +**Model relationships** — When models reference each other (Mongoose `ref`, Sequelize associations), create `depends_on` edges between model files with descriptions indicating the relationship type. + +### Architectural Layers for Express + +Assign nodes to these layers when detected: + +| Layer ID | Layer Name | What Goes Here | +|---|---|---| +| `layer:api` | API Layer | `routes/`, `controllers/`, request validators | +| `layer:data` | Data Layer | `models/`, `db/`, migration files, seeders | +| `layer:service` | Service Layer | `services/`, business logic modules | +| `layer:middleware` | Middleware Layer | `middleware/`, error handlers, authentication, logging | +| `layer:config` | Config Layer | `app.js`, `config/`, environment setup, `server.js` | +| `layer:utility` | Utility Layer | `utils/`, `helpers/`, shared pure functions | +| `layer:test` | Test Layer | `tests/`, `__tests__/`, `*.test.js`, `*.spec.js` | + +### Notable Patterns to Capture in languageLesson + +- **Middleware chain (req, res, next)**: Express processes requests through a pipeline of middleware functions — each receives the request, response, and a `next()` callback to pass control forward +- **Error-handling middleware (4 params)**: Middleware with signature `(err, req, res, next)` catches errors — must be registered after all routes to act as a global error handler +- **Router modularity**: `express.Router()` creates modular, mountable route handlers that can be composed into the main app at different path prefixes +- **MVC pattern**: Express apps commonly separate concerns into Models (data), Views (response formatting), and Controllers (request handling) +- **Body parsing and validation**: Request body parsing (`express.json()`, `express.urlencoded()`) and validation (Joi, Zod, express-validator) are middleware concerns applied before route handlers diff --git a/understand-anything-plugin/skills/understand/frameworks/fastapi.md b/understand-anything-plugin/skills/understand/frameworks/fastapi.md new file mode 100644 index 0000000..79431a2 --- /dev/null +++ b/understand-anything-plugin/skills/understand/frameworks/fastapi.md @@ -0,0 +1,58 @@ +# FastAPI Framework Addendum + +> Injected into file-analyzer and architecture-analyzer prompts when FastAPI is detected. +> Do NOT use as a standalone prompt — always appended to the base prompt template. + +## FastAPI Project Structure + +When analyzing a FastAPI project, apply these additional conventions on top of the base analysis rules. + +### Canonical File Roles + +| File / Pattern | Role | Tags | +|---|---|---| +| `main.py`, `app.py` | Application factory — creates and configures the `FastAPI()` instance | `entry-point`, `config` | +| `*/routers/*.py`, `*/api/*.py` | `APIRouter` modules — group related endpoints by domain | `api-handler`, `routing` | +| `*/schemas.py`, `*/schemas/*.py` | Pydantic request/response models | `type-definition`, `serialization` | +| `*/models.py`, `*/models/*.py` | SQLAlchemy ORM models or other DB models | `data-model` | +| `*/dependencies.py`, `*/deps.py` | `Depends()` provider functions — shared logic injected into routes | `service`, `middleware` | +| `*/crud.py`, `*/repository.py` | Database access layer — CRUD operations | `data-model`, `service` | +| `*/database.py`, `*/db.py` | DB engine, session factory, connection management | `config`, `data-model` | +| `*/config.py`, `*/settings.py` | `pydantic-settings` / `BaseSettings` config classes | `config` | +| `*/middleware.py` | Starlette middleware classes | `middleware` | +| `*/exceptions.py` | Custom exception classes and exception handlers | `utility` | +| `*/security.py`, `*/auth.py` | Auth utilities — JWT decoding, password hashing, OAuth helpers | `service`, `middleware` | +| `*/tasks.py` | Background tasks or Celery task definitions | `service`, `event-handler` | +| `*/tests/*.py`, `test_*.py` | pytest test files | `test` | +| `conftest.py` | pytest fixtures and test configuration | `test`, `config` | + +### Edge Patterns to Look For + +**Router inclusion chain** — When `app.include_router(some_router, prefix="/api")` appears in `main.py` or a router aggregator, create `imports` + `depends_on` edges from the main app file to each router module. This builds the URL hierarchy graph. + +**Dependency injection tree** — When a route function or another `Depends()` provider imports and calls `Depends(some_function)`, create `depends_on` edges from the caller to the dependency provider. Trace these chains — they often span multiple files (e.g., route → auth dependency → DB session dependency). + +**Pydantic model inheritance** — When a schema class inherits from another (e.g., `class UserCreate(UserBase)`), create `inherits` edges between the schema class nodes. + +**ORM model relationships** — When SQLAlchemy models use `relationship()`, `ForeignKey`, create `depends_on` edges between the model classes. + +**CRUD-to-model binding** — When a `crud.py` function takes a model type as an argument or directly references a model class, create `depends_on` edges from the CRUD file to the model file. + +### Architectural Layers for FastAPI + +| Layer ID | Layer Name | What Goes Here | +|---|---|---| +| `layer:api` | API Layer | Router files, endpoint functions with `@router.get/post/...` decorators | +| `layer:types` | Types Layer | Pydantic schema files, request/response models | +| `layer:service` | Service Layer | `dependencies.py`, `crud.py`, business logic modules | +| `layer:data` | Data Layer | ORM models, `database.py`, migrations | +| `layer:config` | Config Layer | `main.py` / `app.py` factory, `settings.py`, `config.py` | +| `layer:middleware` | Middleware Layer | `middleware.py`, `security.py`, `auth.py`, exception handlers | +| `layer:test` | Test Layer | `tests/`, `conftest.py` | + +### Notable Patterns to Capture in languageLesson + +- **Dependency injection as composition**: FastAPI's `Depends()` is a first-class DI system — a route can declare any number of dependencies, each of which can have their own dependencies, forming a tree resolved at request time +- **Pydantic for validation**: Request bodies, query params, and path params are automatically validated by Pydantic — invalid input raises `422 Unprocessable Entity` before your code runs +- **Async endpoints**: `async def` routes run in the event loop; `def` routes run in a threadpool — mixing them incorrectly can cause performance issues +- **Path operation order**: FastAPI matches routes in declaration order; a catch-all route before a specific one will shadow it diff --git a/understand-anything-plugin/skills/understand/frameworks/flask.md b/understand-anything-plugin/skills/understand/frameworks/flask.md new file mode 100644 index 0000000..b1df89f --- /dev/null +++ b/understand-anything-plugin/skills/understand/frameworks/flask.md @@ -0,0 +1,53 @@ +# Flask Framework Addendum + +> Injected into file-analyzer and architecture-analyzer prompts when Flask is detected. +> Do NOT use as a standalone prompt — always appended to the base prompt template. + +## Flask Project Structure + +When analyzing a Flask project, apply these additional conventions on top of the base analysis rules. + +### Canonical File Roles + +| File / Pattern | Role | Tags | +|---|---|---| +| `app.py`, `__init__.py` (in app package) | Application factory (`create_app()`) or direct `Flask(__name__)` instance | `entry-point`, `config` | +| `run.py`, `wsgi.py` | Production/dev server entry point | `entry-point`, `config` | +| `*/views.py`, `*/routes.py` | Route handler functions with `@app.route` or `@blueprint.route` | `api-handler`, `routing` | +| `*/blueprints/*.py`, `*/api/*.py` | Blueprint modules — group routes by feature | `api-handler`, `routing` | +| `*/models.py` | SQLAlchemy models or other ORM models | `data-model` | +| `*/forms.py` | WTForms form classes | `validation`, `ui` | +| `*/schemas.py` | Marshmallow serialization schemas | `serialization`, `type-definition` | +| `*/config.py` | Config classes (`DevelopmentConfig`, `ProductionConfig`) | `config` | +| `*/extensions.py` | Flask extension initialization (`db = SQLAlchemy()`, `login_manager = LoginManager()`) | `config`, `singleton` | +| `*/decorators.py` | Custom route decorators (auth guards, rate limiting) | `middleware`, `utility` | +| `*/utils.py`, `*/helpers.py` | Shared utility functions | `utility` | +| `*/templates/**/*.html` | Jinja2 templates | `ui` | +| `*/static/` | CSS, JS, and asset files | `assets` | +| `*/tests/*.py`, `test_*.py` | pytest or unittest test files | `test` | + +### Edge Patterns to Look For + +**Blueprint registration** — When `app.register_blueprint(bp, url_prefix='/api')` appears in the application factory, create `depends_on` edges from the app factory to each blueprint module. + +**Extension coupling** — When a view imports from `extensions.py` (e.g., `from .extensions import db, login_manager`), create `imports` edges to show which views depend on which extensions. + +**Before/after request hooks** — When `@app.before_request` or `@blueprint.before_request` decorates a function, create `middleware` edges from those functions to the app/blueprint they attach to. + +### Architectural Layers for Flask + +| Layer ID | Layer Name | What Goes Here | +|---|---|---| +| `layer:api` | API Layer | Blueprint route files, view functions | +| `layer:data` | Data Layer | `models.py`, database migration files | +| `layer:service` | Service Layer | Business logic modules, `schemas.py`, service classes | +| `layer:ui` | UI Layer | `templates/`, `forms.py`, `static/` | +| `layer:config` | Config Layer | `app.py` factory, `config.py`, `extensions.py` | +| `layer:middleware` | Middleware Layer | `decorators.py`, before/after request hooks | +| `layer:test` | Test Layer | Test files, `conftest.py` | + +### Notable Patterns to Capture in languageLesson + +- **Application factory pattern**: `create_app()` functions allow multiple app instances (e.g., for testing) and delay extension initialization — avoids circular imports +- **Blueprint modularity**: Blueprints group related routes, templates, and static files; they are registered on the app with a URL prefix, making them independently testable +- **Flask extension protocol**: Extensions follow `init_app(app)` for lazy initialization — the extension object is created globally but bound to an app instance later diff --git a/understand-anything-plugin/skills/understand/frameworks/gin.md b/understand-anything-plugin/skills/understand/frameworks/gin.md new file mode 100644 index 0000000..494c27d --- /dev/null +++ b/understand-anything-plugin/skills/understand/frameworks/gin.md @@ -0,0 +1,59 @@ +# Gin (Go) Framework Addendum + +> Injected into file-analyzer and architecture-analyzer prompts when Gin is detected. +> Do NOT use as a standalone prompt — always appended to the base prompt template. + +## Gin Project Structure + +When analyzing a Gin project, apply these additional conventions on top of the base analysis rules. + +### Canonical File Roles + +| File / Pattern | Role | Tags | +|---|---|---| +| `main.go` | Application entry point — initializes the Gin engine, registers routes, starts the server | `entry-point`, `config` | +| `cmd/*.go`, `cmd/**/*.go` | CLI entry points — multiple binaries in a multi-command project | `entry-point`, `config` | +| `handlers/*.go`, `handler/*.go` | HTTP handlers — process requests with `gin.Context` | `api-handler` | +| `controllers/*.go`, `controller/*.go` | Controllers — alternative naming for HTTP handlers | `api-handler` | +| `routes/*.go`, `router/*.go` | Route definitions — register endpoints and route groups | `routing`, `config` | +| `models/*.go`, `model/*.go` | Data models — struct definitions mapped to database tables | `data-model` | +| `middleware/*.go` | Middleware functions — authentication, logging, CORS, rate limiting | `middleware` | +| `services/*.go`, `service/*.go` | Business logic — domain operations decoupled from HTTP layer | `service` | +| `repository/*.go`, `repo/*.go` | Data access layer — database queries and persistence logic | `data-model`, `service` | +| `config/*.go`, `config.go` | Application configuration — environment loading, struct-based config | `config` | +| `dto/*.go` | Data transfer objects — request and response structs | `type-definition` | +| `utils/*.go`, `pkg/*.go` | Shared utility packages | `utility` | +| `*_test.go` | Unit and integration tests | `test` | + +### Edge Patterns to Look For + +**Route group registration** — When `r.Group("/api")` creates a route group and registers handlers, create `configures` edges from the route definition file to each handler. Route groups organize endpoints by prefix and shared middleware. + +**Handler-to-service calls** — When a handler function calls a service method, create `depends_on` edges from the handler to the service. This represents the separation between HTTP handling and business logic. + +**Service-to-repository calls** — When a service calls a repository method for data access, create `depends_on` edges from the service to the repository. This represents the data access abstraction. + +**Middleware chaining** — When `r.Use(middleware)` or a route group applies middleware, create middleware edges from the router or group to the middleware function. Middleware executes in registration order. + +### Architectural Layers for Gin + +Assign nodes to these layers when detected: + +| Layer ID | Layer Name | What Goes Here | +|---|---|---| +| `layer:api` | API Layer | `handlers/`, `controllers/`, HTTP handler functions | +| `layer:data` | Data Layer | `models/`, `repository/`, database access, migrations | +| `layer:service` | Service Layer | `services/`, business logic | +| `layer:middleware` | Middleware Layer | `middleware/`, authentication, logging, rate limiting | +| `layer:config` | Config Layer | `main.go`, `routes/`, `config/`, environment setup | +| `layer:utility` | Utility Layer | `utils/`, `pkg/`, shared helper packages | +| `layer:test` | Test Layer | `*_test.go`, test fixtures, test helpers | + +### Notable Patterns to Capture in languageLesson + +- **Handler functions with gin.Context**: Every Gin handler receives a `*gin.Context` parameter — it provides request parsing (`c.Bind`, `c.Param`, `c.Query`), response writing (`c.JSON`, `c.HTML`), and control flow (`c.Abort`, `c.Next`) +- **Middleware chain with c.Next()**: Middleware calls `c.Next()` to pass control to the next handler in the chain — code before `c.Next()` runs pre-handler, code after runs post-handler +- **Route grouping for modular APIs**: `r.Group("/v1")` creates modular sub-routers that can have their own middleware stack — enables versioning and access control at the group level +- **Dependency injection via constructors (no framework DI)**: Go has no DI framework — dependencies are passed as constructor parameters (e.g., `NewUserHandler(userService)`) and stored as struct fields +- **Interface-driven design for testability**: Services and repositories are defined as interfaces — handlers depend on the interface, enabling mock implementations in tests +- **Error handling with gin.Error**: Gin collects errors via `c.Error(err)` — middleware can inspect `c.Errors` after handler execution to implement centralized error logging and response formatting diff --git a/understand-anything-plugin/skills/understand/frameworks/nextjs.md b/understand-anything-plugin/skills/understand/frameworks/nextjs.md new file mode 100644 index 0000000..6b9a93c --- /dev/null +++ b/understand-anything-plugin/skills/understand/frameworks/nextjs.md @@ -0,0 +1,59 @@ +# Next.js Framework Addendum + +> Injected into file-analyzer and architecture-analyzer prompts when Next.js is detected. +> Do NOT use as a standalone prompt — always appended to the base prompt template. + +## Next.js Project Structure + +When analyzing a Next.js project, apply these additional conventions on top of the base analysis rules. + +### Canonical File Roles + +| File / Pattern | Role | Tags | +|---|---|---| +| `app/layout.tsx` | Root layout — wraps all pages, defines HTML shell and global providers | `entry-point`, `config`, `ui` | +| `app/page.tsx` | Root page component — renders at `/` | `ui`, `routing` | +| `app/**/page.tsx` | Route page components — file path determines URL | `ui`, `routing` | +| `app/**/layout.tsx` | Nested layouts — wrap child routes with shared UI | `ui`, `config` | +| `app/**/loading.tsx` | Loading UI — shown as Suspense fallback during route transitions | `ui` | +| `app/**/error.tsx` | Error boundary — catches errors in the route segment | `ui` | +| `app/**/not-found.tsx` | 404 UI — shown when `notFound()` is called | `ui` | +| `app/api/**/route.ts` | API route handlers — serverless endpoint functions (GET, POST, etc.) | `api-handler` | +| `middleware.ts` | Edge middleware — intercepts requests before they reach routes | `middleware` | +| `lib/*.ts`, `lib/**/*.ts` | Shared server-side utilities, data access, and business logic | `service` | +| `components/*.tsx`, `components/**/*.tsx` | Reusable UI components | `ui` | +| `next.config.js`, `next.config.mjs`, `next.config.ts` | Next.js configuration — redirects, rewrites, env, webpack overrides | `config` | +| `actions/*.ts`, `app/**/actions.ts` | Server Actions — server-side mutation functions callable from client | `service`, `api-handler` | + +### Edge Patterns to Look For + +**Layout nesting** — When `app/foo/layout.tsx` wraps `app/foo/page.tsx` and `app/foo/bar/page.tsx`, create `contains` edges from the layout to the pages it wraps. Layouts compose via the file-system hierarchy. + +**API route handlers** — When a `route.ts` file exports named functions (GET, POST, PUT, DELETE), create edges from consuming components or server actions to the route handler based on fetch calls. + +**Server/Client component boundary** — Files with `"use client"` directive at the top are Client Components. All other components in the `app/` directory are Server Components by default. Create `depends_on` edges that cross this boundary and note the boundary in the edge description. + +**Parallel routes** — When `app/@slot/page.tsx` patterns appear, create `contains` edges from the parent layout to each parallel slot. These render simultaneously in the same layout. + +**Route groups** — Directories wrapped in parentheses `(group)` organize routes without affecting the URL path. Note these in node descriptions. + +### Architectural Layers for Next.js + +Assign nodes to these layers when detected: + +| Layer ID | Layer Name | What Goes Here | +|---|---|---| +| `layer:ui` | UI Layer | `app/**/page.tsx`, `app/**/layout.tsx`, `components/`, loading/error boundaries | +| `layer:api` | API Layer | `app/api/**/route.ts`, API route handlers | +| `layer:service` | Service Layer | `lib/`, server actions, data-fetching utilities | +| `layer:middleware` | Middleware Layer | `middleware.ts`, edge functions | +| `layer:config` | Config Layer | `next.config.*`, root layout, `tailwind.config.*`, environment setup | +| `layer:test` | Test Layer | `__tests__/`, `*.test.tsx`, `*.spec.tsx`, `e2e/` | + +### Notable Patterns to Capture in languageLesson + +- **Server Components by default**: Components in the `app/` directory are Server Components — no JavaScript is sent to the client unless `"use client"` is declared +- **Server Actions for mutations**: Functions marked with `"use server"` can be called directly from client components, replacing traditional API routes for form submissions and mutations +- **App Router file conventions**: Special files (`page`, `layout`, `loading`, `error`, `not-found`, `route`) define behavior by naming convention within the file-system router +- **ISR and static generation**: `generateStaticParams` pre-renders pages at build time; revalidation strategies control cache freshness +- **Parallel and intercepting routes**: `@slot` directories enable parallel rendering; `(.)` prefix directories enable route interception for modal patterns diff --git a/understand-anything-plugin/skills/understand/frameworks/rails.md b/understand-anything-plugin/skills/understand/frameworks/rails.md new file mode 100644 index 0000000..570ef10 --- /dev/null +++ b/understand-anything-plugin/skills/understand/frameworks/rails.md @@ -0,0 +1,65 @@ +# Ruby on Rails Framework Addendum + +> Injected into file-analyzer and architecture-analyzer prompts when Rails is detected. +> Do NOT use as a standalone prompt — always appended to the base prompt template. + +## Rails Project Structure + +When analyzing a Ruby on Rails project, apply these additional conventions on top of the base analysis rules. + +### Canonical File Roles + +| File / Pattern | Role | Tags | +|---|---|---| +| `config.ru` | Rack entry point — boots the Rails application for the web server | `entry-point` | +| `config/application.rb` | Application configuration — sets up Rails, loads gems, configures middleware | `entry-point`, `config` | +| `app/controllers/*_controller.rb` | Controllers — handle HTTP requests, orchestrate models, render responses | `api-handler` | +| `app/controllers/concerns/*.rb` | Controller concerns — shared controller behavior via mixins | `middleware`, `utility` | +| `app/models/*.rb` | ActiveRecord models — map to database tables, contain validations and associations | `data-model` | +| `app/models/concerns/*.rb` | Model concerns — shared model behavior via mixins | `utility` | +| `app/views/**/*.erb`, `app/views/**/*.haml` | View templates — HTML rendering with embedded Ruby | `ui` | +| `app/helpers/*_helper.rb` | View helpers — utility methods available in templates | `utility` | +| `app/mailers/*_mailer.rb` | Action Mailer classes — send email notifications | `service` | +| `app/jobs/*_job.rb` | Active Job classes — background job processing | `service` | +| `app/channels/*_channel.rb` | Action Cable channels — WebSocket communication | `service` | +| `app/serializers/*_serializer.rb` | API serializers — JSON response formatting (ActiveModelSerializers, Blueprinter) | `api-handler`, `utility` | +| `app/services/*.rb` | Service objects — encapsulate complex business logic | `service` | +| `db/migrate/*.rb` | Database migrations — schema changes versioned by timestamp | `config`, `data-model` | +| `db/schema.rb`, `db/structure.sql` | Generated schema snapshot — current database structure | `data-model`, `config` | +| `config/routes.rb` | Route definitions — maps URLs to controller actions | `routing`, `config` | +| `config/initializers/*.rb` | Initializers — run once at boot to configure gems and services | `config` | +| `lib/**/*.rb` | Library code — custom classes, Rake tasks, extensions | `utility`, `service` | +| `spec/**/*_spec.rb`, `test/**/*_test.rb` | RSpec or Minitest test files | `test` | + +### Edge Patterns to Look For + +**Route-to-controller mapping** — When `config/routes.rb` defines `resources :users` or `get '/foo', to: 'bar#baz'`, create `configures` edges from the routes file to the corresponding controller. RESTful resources generate a full set of action mappings. + +**ActiveRecord associations** — When models define `has_many`, `belongs_to`, `has_one`, or `has_and_belongs_to_many`, create `depends_on` edges between model files with descriptions indicating the association type and direction. + +**Controller-to-model** — When a controller calls model methods (`User.find`, `@post.save`), create `depends_on` edges from the controller to the model. Controllers are the primary consumers of model data. + +**Callbacks** — When models or controllers use `before_action`, `after_save`, `before_validation`, or similar callbacks, note these as middleware-like edges. Callbacks create implicit execution paths that are not visible from the call site. + +### Architectural Layers for Rails + +Assign nodes to these layers when detected: + +| Layer ID | Layer Name | What Goes Here | +|---|---|---| +| `layer:api` | API Layer | `app/controllers/`, `app/serializers/`, API-specific controllers | +| `layer:data` | Data Layer | `app/models/`, `db/migrate/`, `db/schema.rb` | +| `layer:ui` | UI Layer | `app/views/`, `app/helpers/`, `app/assets/`, `app/javascript/` | +| `layer:service` | Service Layer | `app/mailers/`, `app/jobs/`, `app/channels/`, `app/services/`, `lib/` | +| `layer:config` | Config Layer | `config/routes.rb`, `config/initializers/`, `config/application.rb`, `config.ru` | +| `layer:middleware` | Middleware Layer | `app/middleware/`, controller concerns, Rack middleware | +| `layer:test` | Test Layer | `spec/`, `test/`, `*.spec.rb`, `*_test.rb` | + +### Notable Patterns to Capture in languageLesson + +- **Convention over configuration**: Rails derives routing, table names, and file locations from naming conventions — `UsersController` maps to `users_controller.rb`, handles `/users`, and queries the `users` table +- **ActiveRecord pattern**: Models are database wrappers — each model class maps to a table, instances map to rows, and attributes map to columns with automatic type coercion +- **Concerns for shared behavior**: `ActiveSupport::Concern` modules are mixins included in models or controllers to share validations, scopes, callbacks, and methods across classes +- **Strong parameters for mass-assignment protection**: `params.require(:user).permit(:name, :email)` whitelists attributes — controllers must explicitly declare which fields can be set from user input +- **RESTful resource routing**: `resources :posts` generates seven standard CRUD routes — Rails strongly encourages RESTful design where each controller maps to a resource +- **Callbacks and observers**: `before_save`, `after_create`, and similar callbacks inject logic into the object lifecycle — they create invisible execution paths that can be difficult to trace diff --git a/understand-anything-plugin/skills/understand/frameworks/react.md b/understand-anything-plugin/skills/understand/frameworks/react.md new file mode 100644 index 0000000..d36eb39 --- /dev/null +++ b/understand-anything-plugin/skills/understand/frameworks/react.md @@ -0,0 +1,55 @@ +# React Framework Addendum + +> Injected into file-analyzer and architecture-analyzer prompts when React is detected. +> Do NOT use as a standalone prompt — always appended to the base prompt template. + +## React Project Structure + +When analyzing a React project, apply these additional conventions on top of the base analysis rules. + +### Canonical File Roles + +| File / Pattern | Role | Tags | +|---|---|---| +| `src/App.tsx` | Root application component — mounts providers, router, and top-level layout | `entry-point`, `ui` | +| `components/*.tsx`, `components/**/*.tsx` | Reusable UI components | `ui` | +| `hooks/*.ts`, `hooks/*.tsx` | Custom React hooks — encapsulate reusable stateful logic | `service`, `utility` | +| `contexts/*.tsx`, `context/*.tsx` | React Context providers and consumers — shared state across component tree | `service`, `state` | +| `pages/*.tsx`, `views/*.tsx` | Page-level components mapped to routes | `ui`, `routing` | +| `utils/*.ts`, `helpers/*.ts` | Pure utility functions — formatting, validation, transformations | `utility` | +| `types/*.ts`, `types/*.d.ts` | TypeScript type definitions and interfaces | `type-definition` | +| `services/*.ts`, `api/*.ts` | API client functions and data-fetching logic | `service` | +| `store/*.ts`, `slices/*.ts` | State management (Redux, Zustand, etc.) | `service`, `state` | +| `constants/*.ts` | Application-wide constants and enums | `config` | +| `__tests__/*.tsx`, `*.test.tsx`, `*.spec.tsx` | Unit and integration tests | `test` | + +### Edge Patterns to Look For + +**Component composition** — When a parent component renders a child component in its JSX return, create `contains` edges from the parent to the child. These edges represent the component tree hierarchy. + +**Hook usage** — When a component or hook imports and calls a custom hook (`useX`), create `depends_on` edges from the consumer to the hook module. Hooks are the primary mechanism for shared logic in React. + +**Context provider/consumer** — When a Context provider wraps components, create `publishes` edges from the provider to the context definition. When components call `useContext` or use a custom context hook, create `subscribes` edges from the consumer to the context. + +**Props drilling chains** — When props are passed through multiple component layers without being used, create `depends_on` edges along the chain to surface the coupling depth. + +### Architectural Layers for React + +Assign nodes to these layers when detected: + +| Layer ID | Layer Name | What Goes Here | +|---|---|---| +| `layer:ui` | UI Layer | `components/`, `pages/`, `views/`, layout components | +| `layer:service` | Service Layer | `hooks/`, `contexts/`, `services/`, `api/`, `store/` | +| `layer:types` | Types Layer | `types/`, shared TypeScript interfaces and type definitions | +| `layer:utility` | Utility Layer | `utils/`, `helpers/`, pure functions | +| `layer:config` | Config Layer | `App.tsx`, router configuration, provider setup, constants | +| `layer:test` | Test Layer | `__tests__/`, `*.test.tsx`, `*.spec.tsx` | + +### Notable Patterns to Capture in languageLesson + +- **Component composition over inheritance**: React favors composing components via props and children rather than class inheritance hierarchies +- **Custom hooks for reusable logic**: Hooks prefixed with `use` extract stateful logic into shareable modules without changing the component tree +- **React.memo for performance**: Components wrapped in `React.memo` skip re-renders when props are unchanged — indicates performance-sensitive paths +- **Controlled vs. uncontrolled components**: Controlled components derive state from props; uncontrolled components manage internal state via refs +- **Render props pattern**: Components that accept a function as children or a render prop to delegate rendering decisions to the consumer diff --git a/understand-anything-plugin/skills/understand/frameworks/spring.md b/understand-anything-plugin/skills/understand/frameworks/spring.md new file mode 100644 index 0000000..0c5bac4 --- /dev/null +++ b/understand-anything-plugin/skills/understand/frameworks/spring.md @@ -0,0 +1,59 @@ +# Spring Boot Framework Addendum + +> Injected into file-analyzer and architecture-analyzer prompts when Spring Boot is detected. +> Do NOT use as a standalone prompt — always appended to the base prompt template. + +## Spring Boot Project Structure + +When analyzing a Spring Boot project, apply these additional conventions on top of the base analysis rules. + +### Canonical File Roles + +| File / Pattern | Role | Tags | +|---|---|---| +| `*Application.java`, `*Application.kt` | Application entry point — `@SpringBootApplication` class with `main()` method | `entry-point`, `config` | +| `*Controller.java`, `*RestController.java` | REST controllers — handle HTTP requests, delegate to services | `api-handler` | +| `*Service.java` | Service interfaces — define business operation contracts | `service` | +| `*ServiceImpl.java` | Service implementations — contain business logic | `service` | +| `*Repository.java` | Spring Data repositories — data access interfaces extending JpaRepository/CrudRepository | `data-model` | +| `*Entity.java` | JPA entities — map to database tables via `@Entity` annotation | `data-model` | +| `*DTO.java`, `*Request.java`, `*Response.java` | Data transfer objects — request/response payloads | `type-definition` | +| `*Config.java`, `*Configuration.java` | Configuration classes — `@Configuration` beans, security config, web config | `config` | +| `*Filter.java` | Servlet filters — intercept requests before they reach controllers | `middleware` | +| `*Interceptor.java` | Handler interceptors — pre/post processing around controller methods | `middleware` | +| `*Advice.java`, `*ExceptionHandler.java` | Controller advice — global exception handling and response wrapping | `middleware` | +| `*Mapper.java` | Object mappers — convert between entities and DTOs (MapStruct, ModelMapper) | `utility` | +| `application.yml`, `application.properties` | Application configuration — profiles, datasource, server settings | `config` | +| `*Test.java`, `*Tests.java`, `*IT.java` | Unit tests, integration tests | `test` | + +### Edge Patterns to Look For + +**@Autowired injection** — When a class injects a dependency via `@Autowired`, constructor injection, or `@Inject`, create `depends_on` edges from the consumer to the injected bean. Constructor injection is preferred and most common in modern Spring. + +**Controller-Service-Repository chain** — The canonical call chain is `@RestController` -> `@Service` -> `@Repository`. Create `depends_on` edges along this chain to show the layered architecture. + +**@Entity relationships** — When entities define `@OneToMany`, `@ManyToOne`, `@OneToOne`, or `@ManyToMany` annotations, create `depends_on` edges between entity classes with descriptions indicating the relationship type and direction. + +**@Configuration bean definitions** — When a `@Configuration` class defines `@Bean` methods, create `configures` edges from the configuration class to the types it produces. These beans become available for injection throughout the application. + +### Architectural Layers for Spring Boot + +Assign nodes to these layers when detected: + +| Layer ID | Layer Name | What Goes Here | +|---|---|---| +| `layer:api` | API Layer | `*Controller.java`, REST endpoints, API documentation | +| `layer:service` | Service Layer | `*Service.java`, `*ServiceImpl.java`, business logic | +| `layer:data` | Data Layer | `*Repository.java`, `*Entity.java`, JPA mappings, database migrations | +| `layer:types` | Types Layer | `*DTO.java`, `*Request.java`, `*Response.java`, shared value objects | +| `layer:config` | Config Layer | `*Configuration.java`, `application.yml`, security config, `*Application.java` | +| `layer:middleware` | Middleware Layer | `*Filter.java`, `*Interceptor.java`, `*Advice.java`, security filters | +| `layer:test` | Test Layer | `*Test.java`, `*Tests.java`, `*IT.java`, test configuration | + +### Notable Patterns to Capture in languageLesson + +- **Dependency injection via constructor injection**: Spring favors constructor injection over field injection (`@Autowired` on fields) — it makes dependencies explicit, supports immutability, and simplifies testing +- **Layered architecture (Controller -> Service -> Repository)**: Spring Boot applications follow a strict layered pattern where controllers handle HTTP, services contain business logic, and repositories manage persistence +- **Spring Security filter chain**: Security is implemented as a chain of servlet filters — `SecurityFilterChain` beans configure authentication, authorization, CORS, and CSRF protection +- **JPA entity lifecycle**: Entities transition through states (transient, managed, detached, removed) — understanding this lifecycle is essential for tracing data flow through the persistence layer +- **AOP for cross-cutting concerns**: `@Aspect` classes with `@Before`, `@After`, and `@Around` advice inject behavior at join points — used for logging, transactions (`@Transactional`), and caching (`@Cacheable`) diff --git a/understand-anything-plugin/skills/understand/frameworks/vue.md b/understand-anything-plugin/skills/understand/frameworks/vue.md new file mode 100644 index 0000000..fdd3419 --- /dev/null +++ b/understand-anything-plugin/skills/understand/frameworks/vue.md @@ -0,0 +1,59 @@ +# Vue Framework Addendum + +> Injected into file-analyzer and architecture-analyzer prompts when Vue is detected. +> Do NOT use as a standalone prompt — always appended to the base prompt template. + +## Vue Project Structure + +When analyzing a Vue project, apply these additional conventions on top of the base analysis rules. + +### Canonical File Roles + +| File / Pattern | Role | Tags | +|---|---|---| +| `src/App.vue` | Root application component — mounts the top-level layout and router view | `entry-point`, `ui` | +| `src/main.ts`, `src/main.js` | Application bootstrap — creates Vue app instance, registers plugins, mounts to DOM | `entry-point`, `config` | +| `components/*.vue`, `components/**/*.vue` | Reusable UI components | `ui` | +| `views/*.vue`, `pages/*.vue` | Page-level components mapped to routes | `ui`, `routing` | +| `composables/*.ts`, `composables/*.js` | Composable functions — reusable stateful logic using Composition API | `service`, `utility` | +| `store/*.ts`, `stores/*.ts` | State management modules (Pinia stores or Vuex modules) | `service`, `state` | +| `router/*.ts`, `router/index.ts` | Vue Router configuration — route definitions, navigation guards | `config`, `routing` | +| `plugins/*.ts`, `plugins/*.js` | Vue plugin registrations — extend app functionality (i18n, auth, etc.) | `config` | +| `utils/*.ts`, `helpers/*.ts` | Pure utility functions | `utility` | +| `types/*.ts`, `types/*.d.ts` | TypeScript type definitions and interfaces | `type-definition` | +| `api/*.ts`, `services/*.ts` | API client functions and data-fetching logic | `service` | +| `directives/*.ts` | Custom Vue directives | `utility` | +| `tests/*.spec.ts`, `__tests__/*.spec.ts` | Unit and integration tests | `test` | + +### Edge Patterns to Look For + +**Component parent-child** — When a parent component uses a child component in its `