From 9f7931a1f9591ffde050c9e80976306c802d2183 Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Thu, 9 Apr 2026 22:56:00 +0800 Subject: [PATCH 01/15] docs: add /understand-knowledge design spec for personal knowledge base plugin New skill that takes markdown knowledge bases (Obsidian, Logseq, Dendron, Foam, Karpathy-style, Zettelkasten, plain) and produces interactive knowledge graphs with typed nodes/edges, auto-format detection, and dashboard visualization. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../2026-04-09-understand-knowledge-design.md | 335 ++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-09-understand-knowledge-design.md diff --git a/docs/superpowers/specs/2026-04-09-understand-knowledge-design.md b/docs/superpowers/specs/2026-04-09-understand-knowledge-design.md new file mode 100644 index 0000000..2c63252 --- /dev/null +++ b/docs/superpowers/specs/2026-04-09-understand-knowledge-design.md @@ -0,0 +1,335 @@ +# /understand-knowledge — Personal Knowledge Base Plugin Design + +## Overview + +A new `/understand-knowledge` skill within the existing Understand Anything plugin that takes any folder of markdown notes and produces an interactive knowledge graph visualized in the existing dashboard. + +Inspired by Andrej Karpathy's LLM Wiki pattern — where an LLM compiles and maintains a structured wiki from raw sources — this plugin goes further by adding typed relationship discovery and interactive graph visualization that tools like Obsidian and Logseq cannot provide. + +### Goals + +- Accept any markdown-based knowledge base (Obsidian vault, Logseq graph, Dendron workspace, Foam, Karpathy-style LLM wiki, Zettelkasten, or plain markdown) +- Auto-detect the format and adapt parsing accordingly +- Use LLM analysis to discover implicit relationships beyond explicit links +- Produce a knowledge graph with typed nodes and edges +- Visualize in the existing dashboard with knowledge-specific layout, sidebar, and reading mode + +### Non-Goals + +- Real-time sync with the knowledge base tool (Obsidian, Logseq, etc.) +- Replacing the user's existing PKM tool — this is a visualization/analysis layer on top +- Supporting non-markdown formats (PDFs, bookmarks) in v1 + +--- + +## Schema Extensions + +### New Node Types (5) + +Added to the existing `NodeType` union (currently 16 types): + +```typescript +export type NodeType = + // existing (16) + | "file" | "function" | "class" | "module" | "concept" + | "config" | "document" | "service" | "table" | "endpoint" + | "pipeline" | "schema" | "resource" + | "domain" | "flow" | "step" + // knowledge (5 new → 21 total) + | "article" | "entity" | "topic" | "claim" | "source"; +``` + +| Type | What it represents | Example | +|------|-------------------|---------| +| `article` | A wiki/note page — the primary content unit | "LLM Knowledge Bases.md" | +| `entity` | A named thing: person, tool, paper, org, project | "Andrej Karpathy", "Obsidian" | +| `topic` | A thematic cluster grouping related articles | "Personal Knowledge Management" | +| `claim` | A specific assertion, insight, or takeaway | "RAG loses context at chunk boundaries" | +| `source` | Raw/reference material that articles are compiled from | A paper URL, a raw PDF reference | + +### New Edge Types (6) + +Added to the existing `EdgeType` union (currently 29 types): + +```typescript +export type EdgeType = + // existing (29) + | ... + // knowledge (6 new → 35 total) + | "cites" | "contradicts" | "builds_on" + | "exemplifies" | "categorized_under" | "authored_by"; +``` + +| Type | Direction | Meaning | +|------|-----------|---------| +| `cites` | article → source | References or draws from | +| `contradicts` | claim → claim | Conflicts or disagrees with | +| `builds_on` | article → article | Extends, refines, or deepens | +| `exemplifies` | entity → concept/topic | Is a concrete example of | +| `categorized_under` | article/entity → topic | Belongs to this theme | +| `authored_by` | article → entity | Written or created by | + +### New Metadata Interface + +```typescript +export interface KnowledgeMeta { + format?: "obsidian" | "logseq" | "dendron" | "foam" | "karpathy" | "zettelkasten" | "plain"; + wikilinks?: string[]; + backlinks?: string[]; + frontmatter?: Record; + sourceUrl?: string; + confidence?: number; // 0-1, for LLM-inferred relationships +} +``` + +Added as an optional field on `GraphNode`: + +```typescript +export interface GraphNode { + // ...existing fields + knowledgeMeta?: KnowledgeMeta; +} +``` + +### Graph-Level Kind Flag + +```typescript +export interface KnowledgeGraph { + version: string; + kind: "codebase" | "knowledge"; // NEW + project: ProjectMeta; + nodes: GraphNode[]; + edges: GraphEdge[]; + layers: Layer[]; + tour: TourStep[]; +} +``` + +The `kind` field tells the dashboard which layout, sidebar, and visual styling to use. For backward compatibility, graphs without a `kind` field default to `"codebase"`. + +--- + +## Format Detection & Format Guides + +### Auto-Detection Logic + +Scans the target directory for signature files/patterns. Priority order (first match wins): + +| Priority | Signal | Detected Format | +|----------|--------|----------------| +| 1 | `.obsidian/` directory | Obsidian | +| 2 | `logseq/` + `pages/` directories | Logseq | +| 3 | `.dendron.yml` or `*.schema.yml` | Dendron | +| 4 | `.foam/` or `.vscode/foam.json` | Foam | +| 5 | `raw/` + `wiki/` + `index.md` | Karpathy | +| 6 | `[[wikilinks]]` + unique ID prefixes in filenames | Zettelkasten | +| 7 | Fallback | Plain markdown | + +### Format Guides + +Located at `skills/understand-knowledge/formats/`. Each guide tells the LLM agents how to parse that format: + +``` +skills/understand-knowledge/ + SKILL.md + formats/ + obsidian.md — [[wikilinks]], [[note|alias]], [[note#heading]], + #tags, YAML frontmatter, .obsidian/ config, + dataview annotations, canvas files + logseq.md — block-based outliner, ((block-refs)), + journals/YYYY_MM_DD.md, pages/, + property:: value syntax, TODO/DONE states + dendron.md — dot-delimited hierarchy (a.b.c.md), + .schema.yml for structure validation, + cross-vault links, refactoring rules + foam.md — [[wikilinks]] + link reference definitions + at file bottom, .foam/config, placeholder links + karpathy.md — raw/ → wiki/ pipeline, index.md master map, + log.md append-only record, _meta/ state, + LLM-maintained cross-references + zettelkasten.md — atomic notes, unique ID prefixes (timestamps), + typed semantic links, one idea per note + plain.md — standard [markdown](links), folder hierarchy, + heading structure, no special conventions +``` + +Each format guide covers: +- How to parse links (wikilinks vs standard vs block refs) +- Where metadata lives (frontmatter vs inline properties vs block properties) +- What the folder structure means (journals/ = daily notes, pages/ = permanent notes) +- What conventions to respect vs what to infer + +### Format Guide Authoring Process + +Format guides must be research-backed. During implementation, the agent building each format guide must: +1. Read the official documentation for that format (Obsidian Help, Logseq docs, Dendron wiki, Foam docs, etc.) +2. Study real-world examples of that format's structure +3. Write the guide based on verified behavior, not assumptions + +--- + +## Agent Pipeline + +``` +knowledge-scanner → format-detector → article-analyzer → relationship-builder → graph-reviewer +``` + +### Agent Definitions + +| Agent | Input | Output | Model | +|-------|-------|--------|-------| +| `knowledge-scanner` | Target directory path | File manifest: all `.md` files with paths, sizes, first 20 lines preview | `inherit` | +| `format-detector` | File manifest + directory structure | Detected format + format-specific parsing hints | `inherit` | +| `article-analyzer` | Individual `.md` file + format guide | Per-file nodes (article, entities, claims) + explicit edges (wikilinks, tags) | `inherit` | +| `relationship-builder` | All per-file results | Cross-file implicit edges (builds_on, contradicts, categorized_under) + topic clustering + layers | `inherit` | +| `graph-reviewer` | Assembled graph | Validated graph — deduped entities, consistent edge weights, orphan detection | `inherit` | + +### Key Differences from Codebase Pipeline + +- **No tree-sitter** — markdown parsing is simpler, mostly regex + LLM interpretation +- **format-detector** replaces framework detection — picks the right format guide +- **article-analyzer** replaces file-analyzer — extracts knowledge concepts instead of code structure +- **relationship-builder** is the heavy LLM step — discovers implicit connections across files that explicit links miss +- **graph-reviewer** stays similar — validates the assembled graph for consistency + +### Intermediate Files + +Same pattern as codebase analysis: + +``` +.understand-anything/intermediate/ + knowledge-manifest.json — scanner output + format-detection.json — detected format + hints + article-*.json — per-file analysis + relationships.json — cross-file edges + knowledge-graph.json — final assembled graph +``` + +Intermediate files are cleaned up after graph assembly (same as codebase flow). + +### Incremental Mode (`--ingest`) + +When the user runs `/understand-knowledge --ingest path/to/new-source.md`: + +1. **knowledge-scanner** — runs on just the new file(s) +2. **format-detector** — skipped (format already known from initial scan) +3. **article-analyzer** — processes only new/changed files +4. **relationship-builder** — runs on new nodes against the existing graph, finds connections to what's already there +5. **graph-reviewer** — validates the merged result + +Existing nodes are preserved; only new nodes/edges are added or updated. + +--- + +## Dashboard Changes + +All changes are scoped to graphs with `"kind": "knowledge"`. + +### Vertical Flow Layout + +- Default to top-down vertical layout (like existing domain/business flow view) +- Topics at top → articles in middle → entities/claims/sources at bottom +- Reads like a knowledge hierarchy: broad themes flow down into specifics +- User can still switch to horizontal or force-directed layout via controls + +### Knowledge Sidebar + +Replaces NodeInfo when a knowledge graph is loaded: + +| Selection | Sidebar Shows | +|-----------|---------------| +| Nothing selected | ProjectOverview: format detected, total articles/entities/topics/claims/sources | +| Article node | Title, summary, tags, frontmatter metadata, backlinks list (clickable), outgoing links, related topics | +| Entity node | Name, type (person/tool/paper/org), articles that mention it, relationships to other entities | +| Topic node | Description, child articles, child entities, cross-topic connections | +| Claim node | Assertion text, supporting articles, contradicting claims (if any), confidence score | +| Source node | Original URL/path, articles that cite it, ingestion date | + +### Reading Mode + +- Clicking an article node triggers a reading panel that slides up from the bottom (same pattern as current code viewer overlay) +- Shows the full compiled markdown rendered as HTML +- Includes a mini backlinks sidebar within the panel +- Clicking a `[[wikilink]]` or entity reference in the reading panel navigates the graph to that node + +### Node Visual Styling + +| Node Type | Shape | Color Accent | +|-----------|-------|-------------| +| `article` | Rounded rectangle | Warm amber | +| `entity` | Circle | Soft blue | +| `topic` | Large rounded rectangle | Muted gold | +| `claim` | Diamond | Green/red depending on contradictions | +| `source` | Small square | Gray | + +### Edge Visual Styling + +| Edge Type | Style | +|-----------|-------| +| `cites` | Dashed line | +| `contradicts` | Red line | +| `builds_on` | Solid with arrow | +| `categorized_under` | Thin gray | +| `authored_by` | Dotted blue | +| `exemplifies` | Dotted green | + +--- + +## Skill Interface + +### Usage + +```bash +# Full scan — first time or rescan +/understand-knowledge + +# Point at a specific directory +/understand-knowledge path/to/my-notes + +# Incremental ingest — add new sources to existing graph +/understand-knowledge --ingest path/to/new-note.md +/understand-knowledge --ingest path/to/new-folder/ +``` + +### Behavior + +1. Auto-detects format (Obsidian, Logseq, Karpathy, etc.) +2. Announces: "Detected Obsidian vault with 342 notes. Scanning..." +3. Runs the agent pipeline (scanner → detector → analyzer → relationship-builder → reviewer) +4. Writes `knowledge-graph.json` to `.understand-anything/` with `"kind": "knowledge"` +5. Auto-triggers `/understand-dashboard` after completion + +### File Structure + +``` +skills/understand-knowledge/ + SKILL.md — skill entry point, orchestration logic + formats/ + obsidian.md + logseq.md + dendron.md + foam.md + karpathy.md + zettelkasten.md + plain.md +``` + +### Coexistence with `/understand` + +- `/understand` produces `"kind": "codebase"` graphs +- `/understand-knowledge` produces `"kind": "knowledge"` graphs +- Both write to `.understand-anything/knowledge-graph.json` +- Running one replaces the other +- To scope knowledge analysis to a subdirectory (e.g., `docs/` within a code repo), use `/understand-knowledge path/to/docs` + +--- + +## What This Enables That Nothing Else Does + +| Existing Tools | Limitation | Our Advantage | +|---------------|-----------|---------------| +| Obsidian graph view | Untyped edges — all links look the same | Typed edges: cites, contradicts, builds_on | +| Logseq graph | Only shows explicit links | LLM discovers implicit relationships | +| All PKM tools | Single-format only | Cross-format support with auto-detection | +| Karpathy LLM Wiki | Flat text wiki, no visualization | Interactive graph dashboard with guided tours | +| None | No knowledge graph tours | Tour mode walks through a knowledge base step by step | From 7c999602bf0916e595fadcffceff98eb88ad65cf Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Thu, 9 Apr 2026 23:06:32 +0800 Subject: [PATCH 02/15] docs: add /understand-knowledge implementation plan (14 tasks) Detailed step-by-step plan covering core type extensions, schema validation, dashboard changes (CSS, store, sidebar, reading panel, edge styling, layout), agent definitions, format guides, and skill definition. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../plans/2026-04-09-understand-knowledge.md | 1740 +++++++++++++++++ 1 file changed, 1740 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-09-understand-knowledge.md diff --git a/docs/superpowers/plans/2026-04-09-understand-knowledge.md b/docs/superpowers/plans/2026-04-09-understand-knowledge.md new file mode 100644 index 0000000..4c0635f --- /dev/null +++ b/docs/superpowers/plans/2026-04-09-understand-knowledge.md @@ -0,0 +1,1740 @@ +# /understand-knowledge Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a `/understand-knowledge` skill that takes any folder of markdown notes (Obsidian, Logseq, Dendron, Foam, Karpathy-style, Zettelkasten, or plain) and produces an interactive knowledge graph with typed nodes, edges, and dashboard visualization. + +**Architecture:** Extends the existing schema with 5 knowledge node types and 6 knowledge edge types. A new 5-agent pipeline (knowledge-scanner → format-detector → article-analyzer → relationship-builder → graph-reviewer) processes markdown files. The dashboard renders knowledge graphs with vertical layout, a knowledge-specific sidebar, and a reading mode panel — all driven by a new `kind` field on the root graph object. + +**Tech Stack:** TypeScript, Zod (schema validation), React + ReactFlow (dashboard), dagre (layout), TailwindCSS v4, Vitest (testing) + +**Spec:** `docs/superpowers/specs/2026-04-09-understand-knowledge-design.md` + +--- + +## File Structure + +### Core package changes +- Modify: `understand-anything-plugin/packages/core/src/types.ts` — add 5 node types, 6 edge types, `KnowledgeMeta` interface, `kind` field +- Modify: `understand-anything-plugin/packages/core/src/schema.ts` — add new types to Zod schemas, add aliases +- Modify: `understand-anything-plugin/packages/core/src/types.test.ts` — add tests for new types +- Test: `understand-anything-plugin/packages/core/src/__tests__/knowledge-schema.test.ts` — validation tests for knowledge-specific schema + +### Dashboard changes +- Modify: `understand-anything-plugin/packages/dashboard/src/store.ts` — add knowledge node types, edge categories, `ViewMode`, node categories +- Modify: `understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx` — add colors for 5 new node types +- Modify: `understand-anything-plugin/packages/dashboard/src/components/NodeInfo.tsx` — add badge colors and edge labels for new types, add knowledge sidebar sections +- Modify: `understand-anything-plugin/packages/dashboard/src/components/ProjectOverview.tsx` — add knowledge-specific stats +- Modify: `understand-anything-plugin/packages/dashboard/src/index.css` — add CSS variables for 5 new node colors +- Modify: `understand-anything-plugin/packages/dashboard/src/App.tsx` — detect `kind` field, set view mode +- Create: `understand-anything-plugin/packages/dashboard/src/components/KnowledgeInfo.tsx` — knowledge-specific sidebar +- Create: `understand-anything-plugin/packages/dashboard/src/components/ReadingPanel.tsx` — full article reading overlay + +### Skill & agent definitions +- Create: `understand-anything-plugin/skills/understand-knowledge/SKILL.md` — skill entry point +- Create: `understand-anything-plugin/skills/understand-knowledge/formats/obsidian.md` +- Create: `understand-anything-plugin/skills/understand-knowledge/formats/logseq.md` +- Create: `understand-anything-plugin/skills/understand-knowledge/formats/dendron.md` +- Create: `understand-anything-plugin/skills/understand-knowledge/formats/foam.md` +- Create: `understand-anything-plugin/skills/understand-knowledge/formats/karpathy.md` +- Create: `understand-anything-plugin/skills/understand-knowledge/formats/zettelkasten.md` +- Create: `understand-anything-plugin/skills/understand-knowledge/formats/plain.md` +- Create: `understand-anything-plugin/agents/knowledge-scanner.md` +- Create: `understand-anything-plugin/agents/format-detector.md` +- Create: `understand-anything-plugin/agents/article-analyzer.md` +- Create: `understand-anything-plugin/agents/relationship-builder.md` + +Existing `graph-reviewer.md` agent is reused for the final validation step. + +--- + +## Task 1: Extend Core Types + +**Files:** +- Modify: `understand-anything-plugin/packages/core/src/types.ts` + +- [ ] **Step 1: Add knowledge node types to NodeType union** + +In `understand-anything-plugin/packages/core/src/types.ts`, add the 5 knowledge types after the domain types: + +```typescript +// Node types (21 total: 5 code + 8 non-code + 3 domain + 5 knowledge) +export type NodeType = + | "file" | "function" | "class" | "module" | "concept" + | "config" | "document" | "service" | "table" | "endpoint" + | "pipeline" | "schema" | "resource" + | "domain" | "flow" | "step" + | "article" | "entity" | "topic" | "claim" | "source"; +``` + +- [ ] **Step 2: Add knowledge edge types to EdgeType union** + +```typescript +// Edge types (35 total in 8 categories) +export type EdgeType = + | "imports" | "exports" | "contains" | "inherits" | "implements" + | "calls" | "subscribes" | "publishes" | "middleware" + | "reads_from" | "writes_to" | "transforms" | "validates" + | "depends_on" | "tested_by" | "configures" + | "related" | "similar_to" + | "deploys" | "serves" | "provisions" | "triggers" + | "migrates" | "documents" | "routes" | "defines_schema" + | "contains_flow" | "flow_step" | "cross_domain" + | "cites" | "contradicts" | "builds_on" | "exemplifies" | "categorized_under" | "authored_by"; +``` + +- [ ] **Step 3: Add KnowledgeMeta interface** + +Add after the `DomainMeta` interface: + +```typescript +// Optional knowledge metadata for article/entity/topic/claim/source nodes +export interface KnowledgeMeta { + format?: "obsidian" | "logseq" | "dendron" | "foam" | "karpathy" | "zettelkasten" | "plain"; + wikilinks?: string[]; + backlinks?: string[]; + frontmatter?: Record; + sourceUrl?: string; + confidence?: number; // 0-1, for LLM-inferred relationships +} +``` + +- [ ] **Step 4: Add knowledgeMeta to GraphNode** + +```typescript +export interface GraphNode { + id: string; + type: NodeType; + name: string; + filePath?: string; + lineRange?: [number, number]; + summary: string; + tags: string[]; + complexity: "simple" | "moderate" | "complex"; + languageNotes?: string; + domainMeta?: DomainMeta; + knowledgeMeta?: KnowledgeMeta; +} +``` + +- [ ] **Step 5: Add kind field to KnowledgeGraph** + +```typescript +export interface KnowledgeGraph { + version: string; + kind?: "codebase" | "knowledge"; // undefined defaults to "codebase" for backward compat + project: ProjectMeta; + nodes: GraphNode[]; + edges: GraphEdge[]; + layers: Layer[]; + tour: TourStep[]; +} +``` + +- [ ] **Step 6: Build core and verify no type errors** + +Run: `pnpm --filter @understand-anything/core build` +Expected: Clean build, no errors + +- [ ] **Step 7: Commit** + +```bash +git add understand-anything-plugin/packages/core/src/types.ts +git commit -m "feat(core): add knowledge node types, edge types, KnowledgeMeta, and graph kind field" +``` + +--- + +## Task 2: Extend Schema Validation + +**Files:** +- Modify: `understand-anything-plugin/packages/core/src/schema.ts` +- Create: `understand-anything-plugin/packages/core/src/__tests__/knowledge-schema.test.ts` + +- [ ] **Step 1: Add knowledge edge types to EdgeTypeSchema** + +In `understand-anything-plugin/packages/core/src/schema.ts`, update the `EdgeTypeSchema` z.enum to include the 6 new types: + +```typescript +export const EdgeTypeSchema = z.enum([ + "imports", "exports", "contains", "inherits", "implements", + "calls", "subscribes", "publishes", "middleware", + "reads_from", "writes_to", "transforms", "validates", + "depends_on", "tested_by", "configures", + "related", "similar_to", + "deploys", "serves", "provisions", "triggers", + "migrates", "documents", "routes", "defines_schema", + "contains_flow", "flow_step", "cross_domain", + // Knowledge + "cites", "contradicts", "builds_on", "exemplifies", "categorized_under", "authored_by", +]); +``` + +- [ ] **Step 2: Add knowledge node type aliases** + +Add to `NODE_TYPE_ALIASES`: + +```typescript + // Knowledge aliases + note: "article", + page: "article", + wiki_page: "article", + person: "entity", + tool: "entity", + paper: "entity", + organization: "entity", + org: "entity", + category: "topic", + theme: "topic", + tag_topic: "topic", + assertion: "claim", + insight: "claim", + takeaway: "claim", + reference: "source", + raw: "source", + citation: "source", +``` + +- [ ] **Step 3: Add knowledge edge type aliases** + +Add to `EDGE_TYPE_ALIASES`: + +```typescript + // Knowledge aliases + references: "cites", + cited_by: "cites", + sourced_from: "cites", + conflicts_with: "contradicts", + disagrees_with: "contradicts", + extends: "builds_on", // Note: "extends" was already mapped to "inherits" — knowledge context will use builds_on via the relationship-builder agent prompt, so keep "extends" → "inherits" for code + refines: "builds_on", + deepens: "builds_on", + example_of: "exemplifies", + instance_of: "exemplifies", + belongs_to: "categorized_under", + tagged_with: "categorized_under", + part_of: "categorized_under", + written_by: "authored_by", + created_by: "authored_by", +``` + +- [ ] **Step 4: Write the failing test for knowledge graph validation** + +Create `understand-anything-plugin/packages/core/src/__tests__/knowledge-schema.test.ts`: + +```typescript +import { describe, it, expect } from "vitest"; +import { validateGraph } from "../schema"; +import type { KnowledgeGraph } from "../types"; + +describe("knowledge graph schema validation", () => { + const minimalKnowledgeGraph: KnowledgeGraph = { + version: "1.0", + kind: "knowledge", + project: { + name: "Test KB", + languages: [], + frameworks: [], + description: "A test knowledge base", + analyzedAt: new Date().toISOString(), + gitCommitHash: "abc123", + }, + nodes: [ + { + id: "article:test-note", + type: "article", + name: "Test Note", + summary: "A test article node", + tags: ["test"], + complexity: "simple", + }, + { + id: "entity:karpathy", + type: "entity", + name: "Andrej Karpathy", + summary: "AI researcher", + tags: ["person", "ai"], + complexity: "simple", + }, + { + id: "topic:pkm", + type: "topic", + name: "Personal Knowledge Management", + summary: "Tools and methods for managing personal knowledge", + tags: ["knowledge", "productivity"], + complexity: "moderate", + }, + ], + edges: [ + { + source: "article:test-note", + target: "entity:karpathy", + type: "authored_by", + direction: "forward", + weight: 0.8, + }, + { + source: "article:test-note", + target: "topic:pkm", + type: "categorized_under", + direction: "forward", + weight: 0.7, + }, + ], + layers: [ + { + id: "layer:pkm", + name: "PKM", + description: "Personal Knowledge Management topic cluster", + nodeIds: ["article:test-note", "topic:pkm"], + }, + ], + tour: [], + }; + + it("validates a minimal knowledge graph", () => { + const result = validateGraph(minimalKnowledgeGraph); + const fatals = result.issues.filter((i) => i.level === "fatal"); + expect(fatals).toHaveLength(0); + }); + + it("accepts all knowledge node types", () => { + const graph = { + ...minimalKnowledgeGraph, + nodes: [ + ...minimalKnowledgeGraph.nodes, + { id: "claim:rag-bad", type: "claim", name: "RAG loses context", summary: "An assertion", tags: ["claim"], complexity: "simple" }, + { id: "source:paper1", type: "source", name: "Attention paper", summary: "A source", tags: ["paper"], complexity: "simple" }, + ], + }; + const result = validateGraph(graph); + const fatals = result.issues.filter((i) => i.level === "fatal"); + expect(fatals).toHaveLength(0); + }); + + it("accepts all knowledge edge types", () => { + const graph = { + ...minimalKnowledgeGraph, + nodes: [ + ...minimalKnowledgeGraph.nodes, + { id: "claim:c1", type: "claim", name: "Claim 1", summary: "c1", tags: [], complexity: "simple" }, + { id: "claim:c2", type: "claim", name: "Claim 2", summary: "c2", tags: [], complexity: "simple" }, + { id: "source:s1", type: "source", name: "Source 1", summary: "s1", tags: [], complexity: "simple" }, + { id: "article:a2", type: "article", name: "Article 2", summary: "a2", tags: [], complexity: "simple" }, + ], + edges: [ + ...minimalKnowledgeGraph.edges, + { source: "article:test-note", target: "source:s1", type: "cites", direction: "forward", weight: 0.7 }, + { source: "claim:c1", target: "claim:c2", type: "contradicts", direction: "forward", weight: 0.6 }, + { source: "article:a2", target: "article:test-note", type: "builds_on", direction: "forward", weight: 0.7 }, + { source: "entity:karpathy", target: "topic:pkm", type: "exemplifies", direction: "forward", weight: 0.5 }, + ], + }; + const result = validateGraph(graph); + const fatals = result.issues.filter((i) => i.level === "fatal"); + expect(fatals).toHaveLength(0); + }); + + it("resolves knowledge node type aliases", () => { + const graph = { + ...minimalKnowledgeGraph, + nodes: [ + { id: "note:n1", type: "note", name: "A Note", summary: "note alias", tags: [], complexity: "simple" }, + { id: "person:p1", type: "person", name: "A Person", summary: "person alias", tags: [], complexity: "simple" }, + ], + edges: [], + layers: [], + }; + const result = validateGraph(graph); + const noteNode = result.graph.nodes.find((n) => n.id === "note:n1"); + const personNode = result.graph.nodes.find((n) => n.id === "person:p1"); + expect(noteNode?.type).toBe("article"); + expect(personNode?.type).toBe("entity"); + }); + + it("resolves knowledge edge type aliases", () => { + const graph = { + ...minimalKnowledgeGraph, + edges: [ + { source: "article:test-note", target: "entity:karpathy", type: "written_by", direction: "forward", weight: 0.8 }, + ], + }; + const result = validateGraph(graph); + const edge = result.graph.edges.find((e) => e.source === "article:test-note" && e.target === "entity:karpathy"); + expect(edge?.type).toBe("authored_by"); + }); +}); +``` + +- [ ] **Step 5: Run tests to verify they fail** + +Run: `pnpm --filter @understand-anything/core test -- --run src/__tests__/knowledge-schema.test.ts` +Expected: Tests fail because EdgeTypeSchema doesn't include knowledge types yet (if schema.ts wasn't updated), or pass if Steps 1-3 were done correctly. + +- [ ] **Step 6: Run all core tests to verify nothing is broken** + +Run: `pnpm --filter @understand-anything/core test -- --run` +Expected: All existing tests pass, new knowledge tests pass + +- [ ] **Step 7: Commit** + +```bash +git add understand-anything-plugin/packages/core/src/schema.ts understand-anything-plugin/packages/core/src/__tests__/knowledge-schema.test.ts +git commit -m "feat(core): add knowledge types to schema validation with aliases and tests" +``` + +--- + +## Task 3: Dashboard — CSS Variables & Node Colors + +**Files:** +- Modify: `understand-anything-plugin/packages/dashboard/src/index.css` + +- [ ] **Step 1: Add CSS variables for 5 knowledge node types** + +In `understand-anything-plugin/packages/dashboard/src/index.css`, add after the existing `--color-node-resource` line: + +```css + /* Knowledge node colors */ + --color-node-article: #d4a574; /* warm amber */ + --color-node-entity: #7ba4c9; /* soft blue */ + --color-node-topic: #c9b06c; /* muted gold */ + --color-node-claim: #6fb07a; /* soft green */ + --color-node-source: #8a8a8a; /* gray */ +``` + +- [ ] **Step 2: Add Tailwind text-color utilities for knowledge nodes** + +Verify TailwindCSS v4 picks up the CSS variables automatically. If the existing pattern uses `text-node-*` classes defined elsewhere, add matching entries. Check if there's a Tailwind config or if the CSS variables are consumed directly. + +Look at how existing `text-node-file` etc. are defined — if they're in the CSS file as utility classes, add: + +```css + .text-node-article { color: var(--color-node-article); } + .text-node-entity { color: var(--color-node-entity); } + .text-node-topic { color: var(--color-node-topic); } + .text-node-claim { color: var(--color-node-claim); } + .text-node-source { color: var(--color-node-source); } +``` + +And corresponding `border-node-*` and `bg-node-*` variants if the pattern requires them. + +- [ ] **Step 3: Commit** + +```bash +git add understand-anything-plugin/packages/dashboard/src/index.css +git commit -m "feat(dashboard): add CSS variables and utility classes for knowledge node types" +``` + +--- + +## Task 4: Dashboard — Store & Type Maps + +**Files:** +- Modify: `understand-anything-plugin/packages/dashboard/src/store.ts` + +- [ ] **Step 1: Add knowledge types to NodeType union** + +Update the local `NodeType` in store.ts: + +```typescript +export type NodeType = "file" | "function" | "class" | "module" | "concept" | "config" | "document" | "service" | "table" | "endpoint" | "pipeline" | "schema" | "resource" | "domain" | "flow" | "step" | "article" | "entity" | "topic" | "claim" | "source"; +``` + +- [ ] **Step 2: Add knowledge edge category** + +Update `EdgeCategory` and `EDGE_CATEGORY_MAP`: + +```typescript +export type EdgeCategory = "structural" | "behavioral" | "data-flow" | "dependencies" | "semantic" | "infrastructure" | "domain" | "knowledge"; + +export const EDGE_CATEGORY_MAP: Record = { + structural: ["imports", "exports", "contains", "inherits", "implements"], + behavioral: ["calls", "subscribes", "publishes", "middleware"], + "data-flow": ["reads_from", "writes_to", "transforms", "validates"], + dependencies: ["depends_on", "tested_by", "configures"], + semantic: ["related", "similar_to"], + infrastructure: ["deploys", "serves", "provisions", "triggers"], + domain: ["contains_flow", "flow_step", "cross_domain"], + knowledge: ["cites", "contradicts", "builds_on", "exemplifies", "categorized_under", "authored_by"], +}; +``` + +- [ ] **Step 3: Add knowledge to ALL_NODE_TYPES and ALL_EDGE_CATEGORIES** + +```typescript +export const ALL_NODE_TYPES: NodeType[] = ["file", "function", "class", "module", "concept", "config", "document", "service", "table", "endpoint", "pipeline", "schema", "resource", "domain", "flow", "step", "article", "entity", "topic", "claim", "source"]; + +export const ALL_EDGE_CATEGORIES: EdgeCategory[] = ["structural", "behavioral", "data-flow", "dependencies", "semantic", "infrastructure", "domain", "knowledge"]; +``` + +- [ ] **Step 4: Add "knowledge" to ViewMode and NodeCategory** + +```typescript +export type ViewMode = "structural" | "domain" | "knowledge"; + +export type NodeCategory = "code" | "config" | "docs" | "infra" | "data" | "domain" | "knowledge"; +``` + +Update the `NODE_CATEGORY_MAP` (find where it maps node types to categories) to include: + +```typescript + article: "knowledge", + entity: "knowledge", + topic: "knowledge", + claim: "knowledge", + source: "knowledge", +``` + +- [ ] **Step 5: Add knowledge node type filter default** + +In the store's initial state `nodeTypeFilters`, add: + +```typescript +nodeTypeFilters: { code: true, config: true, docs: true, infra: true, data: true, domain: true, knowledge: true }, +``` + +- [ ] **Step 6: Build dashboard and verify no errors** + +Run: `pnpm --filter @understand-anything/dashboard build` +Expected: Clean build + +- [ ] **Step 7: Commit** + +```bash +git add understand-anything-plugin/packages/dashboard/src/store.ts +git commit -m "feat(dashboard): add knowledge types to store, edge categories, and view mode" +``` + +--- + +## Task 5: Dashboard — CustomNode & NodeInfo Type Maps + +**Files:** +- Modify: `understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx` +- Modify: `understand-anything-plugin/packages/dashboard/src/components/NodeInfo.tsx` + +- [ ] **Step 1: Add knowledge node colors to CustomNode.tsx** + +In `typeColors` map, add after the `step` entry: + +```typescript + // Knowledge + article: "var(--color-node-article)", + entity: "var(--color-node-entity)", + topic: "var(--color-node-topic)", + claim: "var(--color-node-claim)", + source: "var(--color-node-source)", +``` + +In `typeTextColors` map, add: + +```typescript + // Knowledge + article: "text-node-article", + entity: "text-node-entity", + topic: "text-node-topic", + claim: "text-node-claim", + source: "text-node-source", +``` + +- [ ] **Step 2: Add knowledge node badge colors to NodeInfo.tsx** + +In `typeBadgeColors` map, add: + +```typescript + // Knowledge + article: "text-node-article border border-node-article/30 bg-node-article/10", + entity: "text-node-entity border border-node-entity/30 bg-node-entity/10", + topic: "text-node-topic border border-node-topic/30 bg-node-topic/10", + claim: "text-node-claim border border-node-claim/30 bg-node-claim/10", + source: "text-node-source border border-node-source/30 bg-node-source/10", +``` + +- [ ] **Step 3: Add knowledge edge labels to NodeInfo.tsx** + +In `EDGE_LABELS` map, add: + +```typescript + // Knowledge + cites: { forward: "cites", backward: "cited by" }, + contradicts: { forward: "contradicts", backward: "contradicted by" }, + builds_on: { forward: "builds on", backward: "built upon by" }, + exemplifies: { forward: "exemplifies", backward: "exemplified by" }, + categorized_under: { forward: "categorized under", backward: "categorizes" }, + authored_by: { forward: "authored by", backward: "authored" }, +``` + +- [ ] **Step 4: Build dashboard and verify** + +Run: `pnpm --filter @understand-anything/dashboard build` +Expected: Clean build, no type errors + +- [ ] **Step 5: Commit** + +```bash +git add understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx understand-anything-plugin/packages/dashboard/src/components/NodeInfo.tsx +git commit -m "feat(dashboard): add knowledge node colors, badge colors, and edge labels" +``` + +--- + +## Task 6: Dashboard — Knowledge Sidebar Component + +**Files:** +- Create: `understand-anything-plugin/packages/dashboard/src/components/KnowledgeInfo.tsx` +- Modify: `understand-anything-plugin/packages/dashboard/src/App.tsx` + +- [ ] **Step 1: Create KnowledgeInfo.tsx** + +Create `understand-anything-plugin/packages/dashboard/src/components/KnowledgeInfo.tsx`: + +```tsx +import { useDashboardStore } from "../store"; +import type { GraphNode, GraphEdge, KnowledgeGraph } from "@understand-anything/core/types"; + +const KNOWLEDGE_NODE_TYPES = new Set(["article", "entity", "topic", "claim", "source"]); + +function getBacklinks(nodeId: string, edges: GraphEdge[]): string[] { + return edges + .filter((e) => e.target === nodeId) + .map((e) => e.source); +} + +function getOutgoingLinks(nodeId: string, edges: GraphEdge[]): string[] { + return edges + .filter((e) => e.source === nodeId) + .map((e) => e.target); +} + +function NodeLink({ nodeId, nodes, onNavigate }: { nodeId: string; nodes: GraphNode[]; onNavigate: (id: string) => void }) { + const node = nodes.find((n) => n.id === nodeId); + if (!node) return {nodeId}; + return ( + + ); +} + +export default function KnowledgeInfo() { + const graph = useDashboardStore((s) => s.graph); + const selectedNode = useDashboardStore((s) => s.selectedNode); + const setSelectedNode = useDashboardStore((s) => s.setSelectedNode); + + if (!graph || !selectedNode) return null; + + const node = graph.nodes.find((n) => n.id === selectedNode); + if (!node) return null; + + const backlinks = getBacklinks(node.id, graph.edges); + const outgoing = getOutgoingLinks(node.id, graph.edges); + const meta = node.knowledgeMeta; + + return ( +
+ {/* Header */} +
+
{node.type}
+

{node.name}

+
+ + {/* Summary */} +

{node.summary}

+ + {/* Tags */} + {node.tags.length > 0 && ( +
+ {node.tags.map((tag) => ( + + {tag} + + ))} +
+ )} + + {/* Knowledge-specific metadata */} + {meta?.sourceUrl && ( +
+
Source
+ {meta.sourceUrl} +
+ )} + + {meta?.confidence !== undefined && ( +
+
Confidence
+
+
+
+
+ {Math.round(meta.confidence * 100)}% +
+
+ )} + + {/* Frontmatter */} + {meta?.frontmatter && Object.keys(meta.frontmatter).length > 0 && ( +
+
Frontmatter
+
+ {Object.entries(meta.frontmatter).map(([key, value]) => ( +
+ {key}:{" "} + {String(value)} +
+ ))} +
+
+ )} + + {/* Backlinks */} + {backlinks.length > 0 && ( +
+
+ Backlinks ({backlinks.length}) +
+
+ {backlinks.map((id) => ( + + ))} +
+
+ )} + + {/* Outgoing */} + {outgoing.length > 0 && ( +
+
+ Outgoing Links ({outgoing.length}) +
+
+ {outgoing.map((id) => ( + + ))} +
+
+ )} +
+ ); +} +``` + +- [ ] **Step 2: Integrate KnowledgeInfo into App.tsx sidebar rendering** + +In `understand-anything-plugin/packages/dashboard/src/App.tsx`, find where the sidebar renders `NodeInfo` and add a condition: if `graph.kind === "knowledge"` and a node is selected, render `KnowledgeInfo` instead of `NodeInfo`. + +Import at top: +```typescript +import KnowledgeInfo from "./components/KnowledgeInfo"; +``` + +In the sidebar section, wrap the existing NodeInfo render: +```tsx +{graph?.kind === "knowledge" ? : } +``` + +- [ ] **Step 3: Build dashboard and verify** + +Run: `pnpm --filter @understand-anything/dashboard build` +Expected: Clean build + +- [ ] **Step 4: Commit** + +```bash +git add understand-anything-plugin/packages/dashboard/src/components/KnowledgeInfo.tsx understand-anything-plugin/packages/dashboard/src/App.tsx +git commit -m "feat(dashboard): add KnowledgeInfo sidebar component for knowledge graphs" +``` + +--- + +## Task 7: Dashboard — Reading Panel + +**Files:** +- Create: `understand-anything-plugin/packages/dashboard/src/components/ReadingPanel.tsx` +- Modify: `understand-anything-plugin/packages/dashboard/src/App.tsx` + +- [ ] **Step 1: Create ReadingPanel.tsx** + +Create `understand-anything-plugin/packages/dashboard/src/components/ReadingPanel.tsx`: + +```tsx +import { useState } from "react"; +import { useDashboardStore } from "../store"; + +export default function ReadingPanel() { + const graph = useDashboardStore((s) => s.graph); + const selectedNode = useDashboardStore((s) => s.selectedNode); + const setSelectedNode = useDashboardStore((s) => s.setSelectedNode); + const [isExpanded, setIsExpanded] = useState(false); + + if (!graph || graph.kind !== "knowledge" || !selectedNode) return null; + + const node = graph.nodes.find((n) => n.id === selectedNode); + if (!node || node.type !== "article") return null; + + // Get backlinks for this article + const backlinks = graph.edges + .filter((e) => e.target === node.id) + .map((e) => { + const sourceNode = graph.nodes.find((n) => n.id === e.source); + return sourceNode ? { id: sourceNode.id, name: sourceNode.name, type: sourceNode.type } : null; + }) + .filter(Boolean) as { id: string; name: string; type: string }[]; + + return ( +
+ {/* Header bar */} +
+
+ Reading + {node.name} +
+
+ + +
+
+ +
+ {/* Main content */} +
+
+

{node.name}

+ + {/* Tags */} + {node.tags.length > 0 && ( +
+ {node.tags.map((tag) => ( + + {tag} + + ))} +
+ )} + + {/* Article content (summary for now — full markdown rendering is a future enhancement) */} +
+

{node.summary}

+
+ + {/* Frontmatter metadata */} + {node.knowledgeMeta?.frontmatter && Object.keys(node.knowledgeMeta.frontmatter).length > 0 && ( +
+
Metadata
+ {Object.entries(node.knowledgeMeta.frontmatter).map(([key, value]) => ( +
+ {key}:{" "} + {String(value)} +
+ ))} +
+ )} +
+
+ + {/* Backlinks sidebar */} + {backlinks.length > 0 && ( +
+
+ Backlinks ({backlinks.length}) +
+
+ {backlinks.map((link) => ( + + ))} +
+
+ )} +
+
+ ); +} +``` + +- [ ] **Step 2: Add ReadingPanel to App.tsx** + +Import and render `ReadingPanel` in the main dashboard layout, positioned at the bottom: + +```typescript +import ReadingPanel from "./components/ReadingPanel"; +``` + +Add `` inside the dashboard container, after the graph view area. + +- [ ] **Step 3: Build and verify** + +Run: `pnpm --filter @understand-anything/dashboard build` +Expected: Clean build + +- [ ] **Step 4: Commit** + +```bash +git add understand-anything-plugin/packages/dashboard/src/components/ReadingPanel.tsx understand-anything-plugin/packages/dashboard/src/App.tsx +git commit -m "feat(dashboard): add ReadingPanel for article reading mode in knowledge graphs" +``` + +--- + +## Task 8: Dashboard — Vertical Layout for Knowledge Graphs + +**Files:** +- Modify: `understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx` +- Modify: `understand-anything-plugin/packages/dashboard/src/utils/layout.ts` (if direction isn't already configurable) + +- [ ] **Step 1: Check how layout direction is passed to dagre** + +Read `GraphView.tsx` to find where `applyDagreLayout` is called. The layout.ts `applyDagreLayout` already accepts a `direction: "TB" | "LR"` parameter (default `"TB"`). + +Find where GraphView calls this function and check what direction it passes. + +- [ ] **Step 2: Pass graph kind to layout decision** + +In `GraphView.tsx`, where the layout is applied, check the graph's `kind` field. If `kind === "knowledge"`, use `"TB"` (top-to-bottom). If `kind === "codebase"` or undefined, keep the existing default. + +The graph object is available via the store. Add: + +```typescript +const graphKind = useDashboardStore((s) => s.graph?.kind); +const layoutDirection = graphKind === "knowledge" ? "TB" : "LR"; +``` + +Pass `layoutDirection` to the layout call. + +- [ ] **Step 3: Build and verify** + +Run: `pnpm --filter @understand-anything/dashboard build` +Expected: Clean build + +- [ ] **Step 4: Commit** + +```bash +git add understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx +git commit -m "feat(dashboard): use vertical top-down layout for knowledge graphs" +``` + +--- + +## Task 9: Dashboard — Knowledge Edge Styling + +**Files:** +- Modify: `understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx` + +- [ ] **Step 1: Add knowledge edge style map** + +In `GraphView.tsx`, add a style map for knowledge edge types. Follow the existing pattern from `DomainGraphView.tsx` which uses ReactFlow's `style` prop: + +```typescript +const KNOWLEDGE_EDGE_STYLES: Record = { + cites: { strokeDasharray: "6 3", strokeWidth: 1.5 }, + contradicts: { stroke: "#c97070", strokeWidth: 2 }, + builds_on: { stroke: "var(--color-accent)", strokeWidth: 2 }, + categorized_under: { stroke: "rgba(150,150,150,0.5)", strokeWidth: 1 }, + authored_by: { strokeDasharray: "3 3", stroke: "var(--color-node-entity)", strokeWidth: 1.5 }, + exemplifies: { strokeDasharray: "3 3", stroke: "var(--color-node-claim)", strokeWidth: 1.5 }, +}; +``` + +- [ ] **Step 2: Apply styles when building ReactFlow edges** + +Where edges are converted to ReactFlow format, check if the graph is `kind === "knowledge"` and the edge type has a knowledge style. Merge the style: + +```typescript +const knowledgeStyle = graph?.kind === "knowledge" ? KNOWLEDGE_EDGE_STYLES[edge.type] : undefined; +// Merge with existing edge style +const style = { ...baseEdgeStyle, ...knowledgeStyle }; +``` + +- [ ] **Step 3: Build and verify** + +Run: `pnpm --filter @understand-anything/dashboard build` +Expected: Clean build + +- [ ] **Step 4: Commit** + +```bash +git add understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx +git commit -m "feat(dashboard): add distinct edge styles for knowledge relationship types" +``` + +--- + +## Task 10: Dashboard — Knowledge-Aware ProjectOverview + +**Files:** +- Modify: `understand-anything-plugin/packages/dashboard/src/components/ProjectOverview.tsx` + +- [ ] **Step 1: Add knowledge-specific stats** + +In `ProjectOverview.tsx`, detect `graph.kind === "knowledge"` and show knowledge-specific stats: + +- Total articles, entities, topics, claims, sources (instead of "code, config, docs, infra, data") +- Detected format (from the first node's `knowledgeMeta.format`) +- Remove "Languages" and "Frameworks" sections for knowledge graphs (they'll be empty) + +Add after the existing stats grid: + +```tsx +{graph.kind === "knowledge" && ( +
+
Knowledge Stats
+
+ n.type === "article").length} /> + n.type === "entity").length} /> + n.type === "topic").length} /> + n.type === "claim").length} /> + n.type === "source").length} /> +
+
+)} +``` + +Reuse or create a `StatBox` component matching the existing style. + +- [ ] **Step 2: Conditionally hide code-specific sections** + +Wrap the "Languages", "Frameworks", and code-specific file type breakdown sections in a condition: + +```tsx +{graph.kind !== "knowledge" && ( + <> + {/* existing languages/frameworks/file-types sections */} + +)} +``` + +- [ ] **Step 3: Build and verify** + +Run: `pnpm --filter @understand-anything/dashboard build` +Expected: Clean build + +- [ ] **Step 4: Commit** + +```bash +git add understand-anything-plugin/packages/dashboard/src/components/ProjectOverview.tsx +git commit -m "feat(dashboard): add knowledge-specific stats to ProjectOverview" +``` + +--- + +## Task 11: Create Agent Definitions + +**Files:** +- Create: `understand-anything-plugin/agents/knowledge-scanner.md` +- Create: `understand-anything-plugin/agents/format-detector.md` +- Create: `understand-anything-plugin/agents/article-analyzer.md` +- Create: `understand-anything-plugin/agents/relationship-builder.md` + +- [ ] **Step 1: Create knowledge-scanner agent** + +Create `understand-anything-plugin/agents/knowledge-scanner.md`: + +```markdown +--- +name: knowledge-scanner +description: Scans a directory for markdown files and produces a file manifest for knowledge base analysis +model: inherit +--- + +# Knowledge Scanner Agent + +You scan a target directory to discover all markdown files for knowledge base analysis. + +## Input + +You receive a JSON block with: +- `targetDir` — absolute path to the knowledge base directory + +## Task + +1. Use Glob/Bash to find all `.md` files in the target directory (recursive) +2. Exclude common non-content directories: `.obsidian/`, `logseq/`, `.foam/`, `_meta/`, `node_modules/`, `.git/` +3. For each file, capture: + - `path` — relative path from targetDir + - `sizeLines` — number of lines + - `preview` — first 20 lines of content +4. Detect directory structure signatures: + - Check for `.obsidian/` directory + - Check for `logseq/` + `pages/` directories + - Check for `.dendron.yml` or `*.schema.yml` + - Check for `.foam/` or `.vscode/foam.json` + - Check for `raw/` + `wiki/` + `index.md` + - Scan a sample of files for `[[wikilinks]]` and unique ID prefixes +5. Write results to `$PROJECT_ROOT/.understand-anything/intermediate/knowledge-manifest.json` + +## Output Schema + +```json +{ + "targetDir": "/absolute/path", + "totalFiles": 342, + "directorySignatures": { + "hasObsidianDir": true, + "hasLogseqDir": false, + "hasDendronConfig": false, + "hasFoamConfig": false, + "hasKarpathyStructure": false, + "hasWikilinks": true, + "hasUniqueIdPrefixes": false + }, + "files": [ + { + "path": "notes/topic.md", + "sizeLines": 45, + "preview": "---\ntags: [ai, ml]\n---\n# Topic Name\n..." + } + ] +} +``` + +## Rules + +- Do NOT read file contents beyond the 20-line preview +- Sort files by path alphabetically +- Report total count prominently +- Write output to `.understand-anything/intermediate/knowledge-manifest.json` +``` + +- [ ] **Step 2: Create format-detector agent** + +Create `understand-anything-plugin/agents/format-detector.md`: + +```markdown +--- +name: format-detector +description: Detects the knowledge base format from directory signatures and file samples +model: inherit +--- + +# Format Detector Agent + +You analyze the knowledge-manifest.json to determine which knowledge base format is being used. + +## Input + +Read `.understand-anything/intermediate/knowledge-manifest.json` produced by the knowledge-scanner. + +## Detection Priority + +Apply these rules in order (first match wins): + +| Priority | Signal | Format | +|----------|--------|--------| +| 1 | `hasObsidianDir === true` | `obsidian` | +| 2 | `hasLogseqDir === true` | `logseq` | +| 3 | `hasDendronConfig === true` | `dendron` | +| 4 | `hasFoamConfig === true` | `foam` | +| 5 | `hasKarpathyStructure === true` | `karpathy` | +| 6 | `hasWikilinks === true` AND `hasUniqueIdPrefixes === true` | `zettelkasten` | +| 7 | fallback | `plain` | + +## Output + +Write to `.understand-anything/intermediate/format-detection.json`: + +```json +{ + "format": "obsidian", + "confidence": 0.95, + "parsingHints": { + "linkStyle": "wikilink", + "metadataLocation": "yaml-frontmatter", + "folderSemantics": "none", + "specialFiles": [".obsidian/app.json"], + "tagSyntax": "hashtag-inline" + } +} +``` + +## Rules + +- Always produce exactly one format +- Set confidence based on how many signals matched +- Include parsing hints that will help the article-analyzer +``` + +- [ ] **Step 3: Create article-analyzer agent** + +Create `understand-anything-plugin/agents/article-analyzer.md`: + +```markdown +--- +name: article-analyzer +description: Analyzes individual markdown files to extract knowledge nodes and explicit edges +model: inherit +--- + +# Article Analyzer Agent + +You analyze batches of markdown files from a knowledge base to extract structured knowledge graph data. + +## Input + +You receive a JSON block with: +- `projectRoot` — absolute path to the knowledge base +- `batchFiles` — array of file objects from the manifest (path, sizeLines, preview) +- `format` — detected format from format-detection.json +- `parsingHints` — format-specific parsing guidance + +You also receive a **format guide** (injected by the skill) that describes how to parse this specific format. + +## Task + +For each file in the batch: + +### 1. Read the full file content + +### 2. Extract the article node + +- **id**: `article:` (e.g., `article:notes/topic`) +- **type**: `article` +- **name**: First heading, or frontmatter title, or filename +- **filePath**: relative path +- **summary**: 2-3 sentence summary of the article content +- **tags**: from frontmatter tags, inline #tags, or inferred from content (3-5 tags) +- **complexity**: `simple` (<50 lines), `moderate` (50-200 lines), `complex` (>200 lines) +- **knowledgeMeta**: `{ format, wikilinks, frontmatter }` + +### 3. Extract entity nodes + +Identify named entities mentioned in the article: +- People, organizations, tools, papers, projects, datasets +- **id**: `entity:` (e.g., `entity:andrej-karpathy`) +- **type**: `entity` +- **summary**: one-sentence description based on context in the article +- **tags**: entity category tags like `person`, `tool`, `paper`, `organization` + +### 4. Extract claim nodes (for articles with strong assertions) + +- Only extract claims that are significant takeaways or insights +- **id**: `claim::` (e.g., `claim:notes/topic:rag-loses-context`) +- **type**: `claim` +- **summary**: the assertion itself + +### 5. Extract source nodes (for cited references) + +- External URLs, paper references, book citations +- **id**: `source:` +- **type**: `source` +- **knowledgeMeta**: `{ sourceUrl }` + +### 6. Extract explicit edges + +- `[[wikilinks]]` → find target article, create `related` edge +- Frontmatter references → `categorized_under` or `related` edges +- Inline citations/URLs → `cites` edges to source nodes +- Author mentions → `authored_by` edges + +## Node ID Conventions + +``` +article: +entity: +topic: +claim:: +source: +``` + +Normalize: lowercase, replace spaces with hyphens, remove special characters. + +**Deduplicate entities**: If the same entity appears across multiple files in the batch, emit it only once. Use the most informative summary. + +## Edge Weight Conventions + +``` +contains: 1.0 +authored_by: 0.9 +cites: 0.8 +categorized_under: 0.7 +builds_on: 0.7 +related: 0.5 +exemplifies: 0.5 +contradicts: 0.6 +``` + +## Output + +Write per-batch results to `.understand-anything/intermediate/article-batch-.json`: + +```json +{ + "nodes": [...], + "edges": [...] +} +``` + +## Rules + +- One article node per file (always) +- Entity nodes only for clearly named entities (not generic concepts) +- Claim nodes only for significant assertions (not every sentence) +- Source nodes only for explicit external references +- Deduplicate entities within the batch +- Respect the format guide for parsing links and metadata +``` + +- [ ] **Step 4: Create relationship-builder agent** + +Create `understand-anything-plugin/agents/relationship-builder.md`: + +```markdown +--- +name: relationship-builder +description: Discovers implicit cross-file relationships and builds topic clusters from analyzed knowledge nodes +model: inherit +--- + +# Relationship Builder Agent + +You analyze all extracted nodes and edges to discover implicit relationships that explicit links missed. + +## Input + +Read all `article-batch-*.json` files from `.understand-anything/intermediate/`. Merge all nodes and edges. + +## Task + +### 1. Deduplicate entities globally + +Multiple batches may have emitted the same entity. Merge them: +- Keep the most detailed summary +- Union all tags +- Collapse duplicate IDs + +### 2. Discover implicit relationships + +For each pair of articles/entities, determine if there's an implicit relationship: + +- **builds_on**: Article A extends or deepens ideas from Article B (similar topics, references same entities, but goes further) +- **contradicts**: Article A makes claims that conflict with Article B +- **categorized_under**: Group articles into topic clusters +- **exemplifies**: An entity is a concrete example of a concept/topic +- **related**: Articles share significant thematic overlap but aren't explicitly linked + +Set `confidence` in knowledgeMeta for LLM-inferred edges (0.0-1.0). + +### 3. Build topic nodes + +Identify thematic clusters across all articles: +- **id**: `topic:` +- **type**: `topic` +- **summary**: description of what this topic covers +- Create `categorized_under` edges from articles/entities to their topics + +### 4. Build layers + +Group nodes into layers by topic: +- Each topic becomes a layer +- Articles, entities, claims, and sources are assigned to their primary topic's layer +- Nodes not clearly belonging to any topic go into an "Uncategorized" layer + +### 5. Build tour + +Create a guided tour through the knowledge base: +- Start with the broadest topic overview +- Walk through key articles in a logical learning order +- Each step covers 1-3 related nodes +- 5-10 tour steps total + +## Output + +Write to `.understand-anything/intermediate/relationships.json`: + +```json +{ + "nodes": [...], + "edges": [...], + "layers": [...], + "tour": [...] +} +``` + +## Rules + +- Only add edges with confidence > 0.4 +- Don't duplicate edges that already exist from article-analyzer +- Topics should be meaningful clusters (3+ articles), not one-off categories +- Tour should be navigable by someone new to the knowledge base +- Keep layers balanced — no layer with 50%+ of all nodes +``` + +- [ ] **Step 5: Commit** + +```bash +git add understand-anything-plugin/agents/knowledge-scanner.md understand-anything-plugin/agents/format-detector.md understand-anything-plugin/agents/article-analyzer.md understand-anything-plugin/agents/relationship-builder.md +git commit -m "feat(agents): add knowledge-scanner, format-detector, article-analyzer, and relationship-builder agents" +``` + +--- + +## Task 12: Create Format Guides + +**Files:** +- Create: `understand-anything-plugin/skills/understand-knowledge/formats/obsidian.md` +- Create: `understand-anything-plugin/skills/understand-knowledge/formats/logseq.md` +- Create: `understand-anything-plugin/skills/understand-knowledge/formats/dendron.md` +- Create: `understand-anything-plugin/skills/understand-knowledge/formats/foam.md` +- Create: `understand-anything-plugin/skills/understand-knowledge/formats/karpathy.md` +- Create: `understand-anything-plugin/skills/understand-knowledge/formats/zettelkasten.md` +- Create: `understand-anything-plugin/skills/understand-knowledge/formats/plain.md` + +**IMPORTANT**: Each format guide must be **research-backed**. The implementing agent MUST: +1. Use WebSearch and WebFetch to read the **official documentation** for each format +2. Study the actual parsing rules, not assumptions +3. Include specific syntax examples from real documentation + +- [ ] **Step 1: Create obsidian.md format guide** + +Research Obsidian's official docs (https://help.obsidian.md/) and create `understand-anything-plugin/skills/understand-knowledge/formats/obsidian.md`: + +The guide must cover: +- Detection: `.obsidian/` directory exists +- Link syntax: `[[wikilink]]`, `[[note|alias]]`, `[[note#heading]]`, `![[embed]]` +- Metadata: YAML frontmatter between `---` delimiters +- Tags: `#tag` inline, `tags:` in frontmatter (both array and space-separated) +- Properties: Obsidian Properties (frontmatter fields rendered in UI) +- Folder semantics: Obsidian doesn't assign folder meaning by default +- Special files: `.obsidian/app.json`, `.obsidian/workspace.json` (ignore these) +- Canvas: `.canvas` files (JSON format, describe spatial layouts — extract card references) +- Dataview: inline fields `key:: value`, `[key:: value]` + +- [ ] **Step 2: Create logseq.md format guide** + +Research Logseq docs (https://docs.logseq.com/) and create `understand-anything-plugin/skills/understand-knowledge/formats/logseq.md`: + +Cover: +- Detection: `logseq/` + `pages/` directories +- Structure: `journals/YYYY_MM_DD.md` (daily notes), `pages/*.md` (named pages) +- Link syntax: `[[wikilinks]]`, `((block-references))` by UUID +- Block-based: Content is organized as bullet-point outlines +- Properties: `key:: value` syntax on blocks +- Tags: `#tag` inline, page tags via properties +- Special: `logseq/config.edn` for configuration + +- [ ] **Step 3: Create dendron.md format guide** + +Research Dendron wiki (https://wiki.dendron.so/) and create `understand-anything-plugin/skills/understand-knowledge/formats/dendron.md`: + +Cover: +- Detection: `.dendron.yml` or `*.schema.yml` files +- Hierarchy: dot-delimited filenames (`a.b.c.md`) +- Link syntax: `[[wikilinks]]` with hierarchy awareness +- Schemas: `.schema.yml` files define expected hierarchy structure +- Frontmatter: YAML with required `id` and `title` fields +- Stubs: auto-created intermediate hierarchy files + +- [ ] **Step 4: Create foam.md format guide** + +Research Foam docs (https://foambubble.github.io/foam/) and create `understand-anything-plugin/skills/understand-knowledge/formats/foam.md`: + +Cover: +- Detection: `.foam/` directory or `.vscode/foam.json` +- Link syntax: `[[wikilinks]]` plus link reference definitions at file bottom +- Placeholder links: links to non-existent files +- Frontmatter: standard YAML +- Auto-linking: Foam auto-updates links on file rename/move + +- [ ] **Step 5: Create karpathy.md format guide** + +Research Karpathy's gist (https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) and create `understand-anything-plugin/skills/understand-knowledge/formats/karpathy.md`: + +Cover: +- Detection: `raw/` + `wiki/` directories + `index.md` +- Structure: `raw/` (immutable sources), `wiki/` (compiled articles), `_meta/` (state) +- Special files: `index.md` (master page list), `log.md` (append-only operations log) +- Link style: standard markdown links (not wikilinks) +- Log parsing: `## [YYYY-MM-DD] operation | Title` entries +- Wiki articles: LLM-compiled, may have cross-references and backlinks + +- [ ] **Step 6: Create zettelkasten.md format guide** + +Research zettelkasten.de and create `understand-anything-plugin/skills/understand-knowledge/formats/zettelkasten.md`: + +Cover: +- Detection: `[[wikilinks]]` + unique ID prefixes in filenames (timestamps like `202604091234`) +- Atomic notes: one idea per note +- Unique IDs: timestamp or alphanumeric prefix in filename +- Links: `[[wikilinks]]` with optional typed links +- Frontmatter: YAML with tags, creation date +- No folder hierarchy: flat structure, connections via links only + +- [ ] **Step 7: Create plain.md format guide** + +Create `understand-anything-plugin/skills/understand-knowledge/formats/plain.md`: + +Cover: +- Detection: fallback when no other format detected +- Links: standard markdown `[text](relative/path.md)` links +- Structure: folder hierarchy provides categorization +- Headings: `#` hierarchy provides structure within files +- No special metadata expectations +- Tags: none expected (LLM infers topics) + +- [ ] **Step 8: Commit** + +```bash +git add understand-anything-plugin/skills/understand-knowledge/formats/ +git commit -m "feat(skill): add 7 research-backed format guides for knowledge base parsing" +``` + +--- + +## Task 13: Create SKILL.md + +**Files:** +- Create: `understand-anything-plugin/skills/understand-knowledge/SKILL.md` + +- [ ] **Step 1: Create the skill definition** + +Create `understand-anything-plugin/skills/understand-knowledge/SKILL.md`: + +```markdown +--- +name: understand-knowledge +description: Analyze a markdown knowledge base (Obsidian, Logseq, Dendron, Foam, Karpathy-style, Zettelkasten, or plain) to produce an interactive knowledge graph with typed relationships +argument-hint: [path/to/notes] [--ingest ] +--- + +# /understand-knowledge + +Analyze a personal knowledge base of markdown files and produce an interactive knowledge graph. + +## Arguments + +- `path/to/notes` — (optional) directory containing markdown files. Defaults to current working directory. +- `--ingest ` — (optional) incrementally add new file(s) to an existing knowledge graph. + +## Phase 0: Pre-flight + +1. Determine the target directory: + - If a path argument is provided, use it + - Otherwise use the current working directory +2. Create `.understand-anything/` and `.understand-anything/intermediate/` directories if they don't exist +3. If `--ingest` flag is present: + - Verify `.understand-anything/knowledge-graph.json` exists (error if not — must run full scan first) + - Read the existing graph + - Skip to Phase 2 with only the new/changed files +4. Get the current git commit hash (if in a git repo, otherwise use "no-git") + +## Phase 1: SCAN + +Dispatch the **knowledge-scanner** agent: + +```json +{ + "targetDir": "" +} +``` + +Wait for the agent to write `.understand-anything/intermediate/knowledge-manifest.json`. + +Report: "Scanned {totalFiles} markdown files." + +## Phase 2: FORMAT DETECTION + +Dispatch the **format-detector** agent. + +Wait for `.understand-anything/intermediate/format-detection.json`. + +Report: "Detected format: {format} (confidence: {confidence})" + +## Phase 3: ANALYZE + +Read the format detection result. Load the corresponding format guide: + +- `obsidian` → inject `skills/understand-knowledge/formats/obsidian.md` +- `logseq` → inject `skills/understand-knowledge/formats/logseq.md` +- `dendron` → inject `skills/understand-knowledge/formats/dendron.md` +- `foam` → inject `skills/understand-knowledge/formats/foam.md` +- `karpathy` → inject `skills/understand-knowledge/formats/karpathy.md` +- `zettelkasten` → inject `skills/understand-knowledge/formats/zettelkasten.md` +- `plain` → inject `skills/understand-knowledge/formats/plain.md` + +Batch the files from the manifest into groups of 15-25 files each. + +For each batch, dispatch an **article-analyzer** agent with: + +```json +{ + "projectRoot": "", + "batchFiles": [...], + "format": "", + "parsingHints": {...} +} +``` + +Inject the format guide content into each agent's context. + +Run up to 5 batches concurrently. + +Wait for all `article-batch-*.json` files. + +Report: "Analyzed {totalFiles} files across {batchCount} batches." + +## Phase 4: RELATIONSHIPS + +Dispatch the **relationship-builder** agent. + +Wait for `.understand-anything/intermediate/relationships.json`. + +Report: "Discovered {topicCount} topics, {implicitEdgeCount} implicit relationships." + +## Phase 5: ASSEMBLE + +Merge all intermediate results into a single knowledge graph: + +1. Read all `article-batch-*.json` files — collect all nodes and edges +2. Read `relationships.json` — merge in topic nodes, implicit edges, layers, and tour +3. Deduplicate nodes by ID (keep the most complete version) +4. Deduplicate edges by source+target+type +5. Assemble into `KnowledgeGraph` format: + +```json +{ + "version": "1.0", + "kind": "knowledge", + "project": { + "name": "", + "languages": [], + "frameworks": [], + "description": "Knowledge base analyzed from format", + "analyzedAt": "", + "gitCommitHash": "" + }, + "nodes": [...], + "edges": [...], + "layers": [...], + "tour": [...] +} +``` + +## Phase 6: REVIEW + +Dispatch the existing **graph-reviewer** agent to validate: +- All edge source/target IDs reference existing nodes +- No orphan nodes (nodes with zero edges) +- No duplicate node IDs +- All layers reference existing nodes +- Tour steps reference existing nodes + +Apply fixes from the reviewer. + +## Phase 7: SAVE + +1. Write `.understand-anything/knowledge-graph.json` +2. Write `.understand-anything/meta.json`: + ```json + { + "lastAnalyzedAt": "", + "gitCommitHash": "", + "version": "1.0", + "analyzedFiles": , + "knowledgeFormat": "" + } + ``` +3. Clean up `.understand-anything/intermediate/` directory +4. Report: "Knowledge graph saved with {nodeCount} nodes and {edgeCount} edges." + +## Phase 8: DASHBOARD + +Auto-trigger `/understand-dashboard` to launch the visualization. + +## Incremental Mode (--ingest) + +When `--ingest ` is specified: + +1. Read the existing `knowledge-graph.json` +2. Scan only the specified file(s) or folder +3. Skip format detection (reuse format from existing graph's metadata) +4. Run article-analyzer on only the new/changed files +5. Run relationship-builder on new nodes against the full existing graph +6. Merge new nodes/edges into the existing graph +7. Re-run graph-reviewer +8. Save updated graph +``` + +- [ ] **Step 2: Commit** + +```bash +git add understand-anything-plugin/skills/understand-knowledge/SKILL.md +git commit -m "feat(skill): add /understand-knowledge skill definition with 8-phase pipeline" +``` + +--- + +## Task 14: Build, Test & Verify End-to-End + +**Files:** +- All modified files + +- [ ] **Step 1: Build core package** + +Run: `pnpm --filter @understand-anything/core build` +Expected: Clean build, no errors + +- [ ] **Step 2: Run core tests** + +Run: `pnpm --filter @understand-anything/core test -- --run` +Expected: All tests pass, including new knowledge-schema tests + +- [ ] **Step 3: Build dashboard** + +Run: `pnpm --filter @understand-anything/dashboard build` +Expected: Clean build, no errors + +- [ ] **Step 4: Run lint** + +Run: `pnpm lint` +Expected: No lint errors + +- [ ] **Step 5: Verify skill is discoverable** + +Check that the skill file exists and has valid frontmatter: + +Run: `head -5 understand-anything-plugin/skills/understand-knowledge/SKILL.md` +Expected: Valid `---` delimited YAML with name, description, argument-hint + +- [ ] **Step 6: Verify all agents are present** + +Run: `ls understand-anything-plugin/agents/ | grep knowledge\|format\|article\|relationship` +Expected: `knowledge-scanner.md`, `format-detector.md`, `article-analyzer.md`, `relationship-builder.md` + +- [ ] **Step 7: Verify all format guides are present** + +Run: `ls understand-anything-plugin/skills/understand-knowledge/formats/` +Expected: `obsidian.md`, `logseq.md`, `dendron.md`, `foam.md`, `karpathy.md`, `zettelkasten.md`, `plain.md` + +- [ ] **Step 8: Final commit** + +```bash +git add -A +git commit -m "feat: complete /understand-knowledge implementation — knowledge base analysis skill" +``` From b3acd5b1d2b0d5ae89c7ddd337a363161a1b1da5 Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Thu, 9 Apr 2026 23:11:44 +0800 Subject: [PATCH 03/15] docs: reorganize docs/ into superpowers/specs/ and superpowers/plans/ Move all design docs to docs/superpowers/specs/ and all implementation plans to docs/superpowers/plans/ for consistent organization. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/{ => superpowers}/plans/2026-03-14-phase1-implementation.md | 0 docs/{ => superpowers}/plans/2026-03-14-phase2-implementation.md | 0 docs/{ => superpowers}/plans/2026-03-14-phase3-implementation.md | 0 docs/{ => superpowers}/plans/2026-03-14-phase4-implementation.md | 0 .../{ => superpowers}/plans/2026-03-15-homepage-implementation.md | 0 .../plans/2026-03-18-multi-platform-simple-implementation.md | 0 docs/{ => superpowers}/plans/2026-03-21-language-agnostic-plan.md | 0 .../plans/2026-03-25-dashboard-robustness-impl.md | 0 .../plans/2026-03-25-dashboard-robustness-plan.md | 0 .../plans/2026-03-26-theme-system-implementation.md | 0 docs/{ => superpowers}/plans/2026-03-27-token-reduction-impl.md | 0 .../plans/2026-03-28-understand-anything-extension-impl.md | 0 docs/{ => superpowers}/plans/2026-03-29-homepage-update-impl.md | 0 .../plans/2026-04-01-business-domain-knowledge-impl.md | 0 .../specs}/2026-03-14-understand-anything-design.md | 0 docs/{plans => superpowers/specs}/2026-03-15-homepage-design.md | 0 .../specs}/2026-03-18-multi-platform-simple-design.md | 0 .../specs}/2026-03-21-language-agnostic-design.md | 0 .../specs}/2026-03-26-theme-system-design.md | 0 .../specs}/2026-03-27-token-reduction-design.md | 0 .../specs}/2026-03-28-understand-anything-extension-design.md | 0 .../specs}/2026-03-29-homepage-update-design.md | 0 .../specs}/2026-04-01-business-domain-knowledge-design.md | 0 23 files changed, 0 insertions(+), 0 deletions(-) rename docs/{ => superpowers}/plans/2026-03-14-phase1-implementation.md (100%) rename docs/{ => superpowers}/plans/2026-03-14-phase2-implementation.md (100%) rename docs/{ => superpowers}/plans/2026-03-14-phase3-implementation.md (100%) rename docs/{ => superpowers}/plans/2026-03-14-phase4-implementation.md (100%) rename docs/{ => superpowers}/plans/2026-03-15-homepage-implementation.md (100%) rename docs/{ => superpowers}/plans/2026-03-18-multi-platform-simple-implementation.md (100%) rename docs/{ => superpowers}/plans/2026-03-21-language-agnostic-plan.md (100%) rename docs/{ => superpowers}/plans/2026-03-25-dashboard-robustness-impl.md (100%) rename docs/{ => superpowers}/plans/2026-03-25-dashboard-robustness-plan.md (100%) rename docs/{ => superpowers}/plans/2026-03-26-theme-system-implementation.md (100%) rename docs/{ => superpowers}/plans/2026-03-27-token-reduction-impl.md (100%) rename docs/{ => superpowers}/plans/2026-03-28-understand-anything-extension-impl.md (100%) rename docs/{ => superpowers}/plans/2026-03-29-homepage-update-impl.md (100%) rename docs/{ => superpowers}/plans/2026-04-01-business-domain-knowledge-impl.md (100%) rename docs/{plans => superpowers/specs}/2026-03-14-understand-anything-design.md (100%) rename docs/{plans => superpowers/specs}/2026-03-15-homepage-design.md (100%) rename docs/{plans => superpowers/specs}/2026-03-18-multi-platform-simple-design.md (100%) rename docs/{plans => superpowers/specs}/2026-03-21-language-agnostic-design.md (100%) rename docs/{plans => superpowers/specs}/2026-03-26-theme-system-design.md (100%) rename docs/{plans => superpowers/specs}/2026-03-27-token-reduction-design.md (100%) rename docs/{plans => superpowers/specs}/2026-03-28-understand-anything-extension-design.md (100%) rename docs/{plans => superpowers/specs}/2026-03-29-homepage-update-design.md (100%) rename docs/{plans => superpowers/specs}/2026-04-01-business-domain-knowledge-design.md (100%) diff --git a/docs/plans/2026-03-14-phase1-implementation.md b/docs/superpowers/plans/2026-03-14-phase1-implementation.md similarity index 100% rename from docs/plans/2026-03-14-phase1-implementation.md rename to docs/superpowers/plans/2026-03-14-phase1-implementation.md diff --git a/docs/plans/2026-03-14-phase2-implementation.md b/docs/superpowers/plans/2026-03-14-phase2-implementation.md similarity index 100% rename from docs/plans/2026-03-14-phase2-implementation.md rename to docs/superpowers/plans/2026-03-14-phase2-implementation.md diff --git a/docs/plans/2026-03-14-phase3-implementation.md b/docs/superpowers/plans/2026-03-14-phase3-implementation.md similarity index 100% rename from docs/plans/2026-03-14-phase3-implementation.md rename to docs/superpowers/plans/2026-03-14-phase3-implementation.md diff --git a/docs/plans/2026-03-14-phase4-implementation.md b/docs/superpowers/plans/2026-03-14-phase4-implementation.md similarity index 100% rename from docs/plans/2026-03-14-phase4-implementation.md rename to docs/superpowers/plans/2026-03-14-phase4-implementation.md diff --git a/docs/plans/2026-03-15-homepage-implementation.md b/docs/superpowers/plans/2026-03-15-homepage-implementation.md similarity index 100% rename from docs/plans/2026-03-15-homepage-implementation.md rename to docs/superpowers/plans/2026-03-15-homepage-implementation.md diff --git a/docs/plans/2026-03-18-multi-platform-simple-implementation.md b/docs/superpowers/plans/2026-03-18-multi-platform-simple-implementation.md similarity index 100% rename from docs/plans/2026-03-18-multi-platform-simple-implementation.md rename to docs/superpowers/plans/2026-03-18-multi-platform-simple-implementation.md diff --git a/docs/plans/2026-03-21-language-agnostic-plan.md b/docs/superpowers/plans/2026-03-21-language-agnostic-plan.md similarity index 100% rename from docs/plans/2026-03-21-language-agnostic-plan.md rename to docs/superpowers/plans/2026-03-21-language-agnostic-plan.md diff --git a/docs/plans/2026-03-25-dashboard-robustness-impl.md b/docs/superpowers/plans/2026-03-25-dashboard-robustness-impl.md similarity index 100% rename from docs/plans/2026-03-25-dashboard-robustness-impl.md rename to docs/superpowers/plans/2026-03-25-dashboard-robustness-impl.md diff --git a/docs/plans/2026-03-25-dashboard-robustness-plan.md b/docs/superpowers/plans/2026-03-25-dashboard-robustness-plan.md similarity index 100% rename from docs/plans/2026-03-25-dashboard-robustness-plan.md rename to docs/superpowers/plans/2026-03-25-dashboard-robustness-plan.md diff --git a/docs/plans/2026-03-26-theme-system-implementation.md b/docs/superpowers/plans/2026-03-26-theme-system-implementation.md similarity index 100% rename from docs/plans/2026-03-26-theme-system-implementation.md rename to docs/superpowers/plans/2026-03-26-theme-system-implementation.md diff --git a/docs/plans/2026-03-27-token-reduction-impl.md b/docs/superpowers/plans/2026-03-27-token-reduction-impl.md similarity index 100% rename from docs/plans/2026-03-27-token-reduction-impl.md rename to docs/superpowers/plans/2026-03-27-token-reduction-impl.md diff --git a/docs/plans/2026-03-28-understand-anything-extension-impl.md b/docs/superpowers/plans/2026-03-28-understand-anything-extension-impl.md similarity index 100% rename from docs/plans/2026-03-28-understand-anything-extension-impl.md rename to docs/superpowers/plans/2026-03-28-understand-anything-extension-impl.md diff --git a/docs/plans/2026-03-29-homepage-update-impl.md b/docs/superpowers/plans/2026-03-29-homepage-update-impl.md similarity index 100% rename from docs/plans/2026-03-29-homepage-update-impl.md rename to docs/superpowers/plans/2026-03-29-homepage-update-impl.md diff --git a/docs/plans/2026-04-01-business-domain-knowledge-impl.md b/docs/superpowers/plans/2026-04-01-business-domain-knowledge-impl.md similarity index 100% rename from docs/plans/2026-04-01-business-domain-knowledge-impl.md rename to docs/superpowers/plans/2026-04-01-business-domain-knowledge-impl.md diff --git a/docs/plans/2026-03-14-understand-anything-design.md b/docs/superpowers/specs/2026-03-14-understand-anything-design.md similarity index 100% rename from docs/plans/2026-03-14-understand-anything-design.md rename to docs/superpowers/specs/2026-03-14-understand-anything-design.md diff --git a/docs/plans/2026-03-15-homepage-design.md b/docs/superpowers/specs/2026-03-15-homepage-design.md similarity index 100% rename from docs/plans/2026-03-15-homepage-design.md rename to docs/superpowers/specs/2026-03-15-homepage-design.md diff --git a/docs/plans/2026-03-18-multi-platform-simple-design.md b/docs/superpowers/specs/2026-03-18-multi-platform-simple-design.md similarity index 100% rename from docs/plans/2026-03-18-multi-platform-simple-design.md rename to docs/superpowers/specs/2026-03-18-multi-platform-simple-design.md diff --git a/docs/plans/2026-03-21-language-agnostic-design.md b/docs/superpowers/specs/2026-03-21-language-agnostic-design.md similarity index 100% rename from docs/plans/2026-03-21-language-agnostic-design.md rename to docs/superpowers/specs/2026-03-21-language-agnostic-design.md diff --git a/docs/plans/2026-03-26-theme-system-design.md b/docs/superpowers/specs/2026-03-26-theme-system-design.md similarity index 100% rename from docs/plans/2026-03-26-theme-system-design.md rename to docs/superpowers/specs/2026-03-26-theme-system-design.md diff --git a/docs/plans/2026-03-27-token-reduction-design.md b/docs/superpowers/specs/2026-03-27-token-reduction-design.md similarity index 100% rename from docs/plans/2026-03-27-token-reduction-design.md rename to docs/superpowers/specs/2026-03-27-token-reduction-design.md diff --git a/docs/plans/2026-03-28-understand-anything-extension-design.md b/docs/superpowers/specs/2026-03-28-understand-anything-extension-design.md similarity index 100% rename from docs/plans/2026-03-28-understand-anything-extension-design.md rename to docs/superpowers/specs/2026-03-28-understand-anything-extension-design.md diff --git a/docs/plans/2026-03-29-homepage-update-design.md b/docs/superpowers/specs/2026-03-29-homepage-update-design.md similarity index 100% rename from docs/plans/2026-03-29-homepage-update-design.md rename to docs/superpowers/specs/2026-03-29-homepage-update-design.md diff --git a/docs/plans/2026-04-01-business-domain-knowledge-design.md b/docs/superpowers/specs/2026-04-01-business-domain-knowledge-design.md similarity index 100% rename from docs/plans/2026-04-01-business-domain-knowledge-design.md rename to docs/superpowers/specs/2026-04-01-business-domain-knowledge-design.md From ce25b61e365b1139059ad1c339a05af385f9444e Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Fri, 10 Apr 2026 11:24:49 +0800 Subject: [PATCH 04/15] docs: add .understandignore design spec User-configurable file exclusion using .gitignore syntax, with hardcoded defaults, auto-generated starter file, and pre-analysis review pause. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../2026-04-10-understandignore-design.md | 258 ++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-10-understandignore-design.md diff --git a/docs/superpowers/specs/2026-04-10-understandignore-design.md b/docs/superpowers/specs/2026-04-10-understandignore-design.md new file mode 100644 index 0000000..2d71f51 --- /dev/null +++ b/docs/superpowers/specs/2026-04-10-understandignore-design.md @@ -0,0 +1,258 @@ +# .understandignore Design Spec + +## Overview + +Add user-configurable file exclusion via `.understandignore` files, using `.gitignore` syntax. This makes analysis faster by skipping irrelevant files (vendor code, generated output, test fixtures) without modifying hardcoded defaults. + +## Goals + +- Let users exclude files/directories from analysis via `.understandignore` +- Use `.gitignore` syntax (familiar, no learning curve) +- Keep hardcoded defaults as built-in — `.understandignore` adds patterns on top +- Allow `!` negation to force-include files excluded by defaults +- Auto-generate a commented-out starter file on first run (deterministic code, not LLM) +- Pause before analysis to let user review the ignore file + +## Non-Goals + +- Replacing `.gitignore` — this is analysis-specific +- Per-directory `.understandignore` files (project root and `.understand-anything/` only) +- GUI for editing ignore patterns + +--- + +## IgnoreFilter Module + +New file: `packages/core/src/ignore-filter.ts` + +Uses the [`ignore`](https://www.npmjs.com/package/ignore) npm package for gitignore-compatible pattern matching. + +### API + +```typescript +export interface IgnoreFilter { + isIgnored(relativePath: string): boolean; +} + +export function createIgnoreFilter(projectRoot: string): IgnoreFilter; +``` + +### Behavior + +`createIgnoreFilter` loads patterns in this order (later entries can override earlier ones): + +1. **Hardcoded defaults** — the existing exclusion patterns from project-scanner (node_modules/, .git/, dist/, build/, bin/, obj/, *.lock, *.min.js, etc.) +2. **`.understand-anything/.understandignore`** — project-level, lives alongside the output +3. **`.understandignore`** at project root — alternative location for visibility + +Patterns merge additively. `!` negation in user files can override hardcoded defaults (e.g., `!dist/` force-includes dist/). + +### Hardcoded Default Patterns + +These are the built-in defaults (matching current project-scanner behavior, plus bin/obj for .NET): + +``` +# Dependency directories +node_modules/ +.git/ +vendor/ +venv/ +.venv/ +__pycache__/ + +# Build output +dist/ +build/ +out/ +coverage/ +.next/ +.cache/ +.turbo/ +target/ +bin/ +obj/ + +# Lock files +*.lock +package-lock.json +yarn.lock +pnpm-lock.yaml + +# Binary/asset files +*.png +*.jpg +*.jpeg +*.gif +*.svg +*.ico +*.woff +*.woff2 +*.ttf +*.eot +*.mp3 +*.mp4 +*.pdf +*.zip +*.tar +*.gz + +# Generated files +*.min.js +*.min.css +*.map +*.generated.* + +# IDE/editor +.idea/ +.vscode/ + +# Misc +LICENSE +.gitignore +.editorconfig +.prettierrc +.eslintrc* +*.log +``` + +--- + +## Starter File Generator + +New file: `packages/core/src/ignore-generator.ts` + +### API + +```typescript +export function generateStarterIgnoreFile(projectRoot: string): string; +``` + +### Behavior + +- Deterministic code — scans the project directory for common patterns +- Returns the file content as a string (caller writes it to disk) +- All suggestions are **commented out** — user must uncomment to activate +- Header comment explains the file, syntax, and built-in defaults + +### Detection Logic + +| If exists | Suggest | +|-----------|---------| +| `__tests__/` or `*.test.*` files | `# __tests__/`, `# *.test.*`, `# *.spec.*` | +| `fixtures/` or `testdata/` | `# fixtures/`, `# testdata/` | +| `test/` or `tests/` | `# test/`, `# tests/` | +| `.storybook/` | `# .storybook/` | +| `docs/` | `# docs/` | +| `examples/` | `# examples/` | +| `scripts/` | `# scripts/` | +| `migrations/` | `# migrations/` | +| `*.snap` files | `# *.snap` | +| `bin/` (non-.NET, i.e. shell scripts) | `# bin/` | +| `obj/` | `# obj/` | + +### Generated File Format + +``` +# .understandignore — patterns for files/dirs to exclude from analysis +# Syntax: same as .gitignore (globs, # comments, ! negation, trailing / for dirs) +# Lines below are suggestions — uncomment to activate. +# Use ! prefix to force-include something excluded by defaults. +# +# Built-in defaults (always excluded unless negated): +# node_modules/, .git/, dist/, build/, bin/, obj/, *.lock, *.min.js, etc. +# + +# --- Suggested exclusions (uncomment to activate) --- + +# Test files +# __tests__/ +# *.test.* +# *.spec.* + +# Test data +# fixtures/ +# testdata/ + +# Documentation +# docs/ + +# ... (more suggestions based on detection) +``` + +Only generated if `.understand-anything/.understandignore` doesn't already exist. + +--- + +## Skill Integration + +### Phase 0.5: Ignore Setup (new phase in SKILL.md) + +Added between Pre-flight (Phase 0) and SCAN (Phase 1): + +1. Check if `.understand-anything/.understandignore` exists +2. If not, run `generateStarterIgnoreFile(projectRoot)` and write the result to `.understand-anything/.understandignore` +3. Report to user: + - **First run:** "Generated `.understand-anything/.understandignore` with suggested exclusions. Please review it and uncomment any patterns you'd like to exclude. When ready, confirm to continue." + - **Subsequent runs:** "Found `.understand-anything/.understandignore`. Review it if needed, then confirm to continue." +4. Wait for user confirmation before proceeding + +### Phase 1: SCAN changes + +The `project-scanner` agent's scan script is updated to: + +1. Collect files via `git ls-files` (or fallback) +2. Apply agent's hardcoded pattern filter (Layer 1 — existing behavior) +3. Apply `IgnoreFilter` from core (Layer 2 — user patterns) +4. Add `filteredByIgnore` count to scan output +5. Report: "Scanned {totalFiles} files ({filteredByIgnore} excluded by .understandignore)" + +Two-layer filtering: +- **Layer 1:** Agent's hardcoded patterns in the prompt (fast, coarse filter) +- **Layer 2:** `IgnoreFilter` from core (deterministic code, user-configurable) + +--- + +## Project Scanner Agent Update + +Changes to `understand-anything-plugin/agents/project-scanner.md`: + +- After the file list is built and Layer 1 filtering is applied, the agent runs a Node.js script that imports `createIgnoreFilter` from `@understand-anything/core` and filters the remaining paths +- The scan result JSON includes a new `filteredByIgnore: number` field +- Existing hardcoded exclusion patterns in the agent prompt remain for backward compatibility + +--- + +## Testing + +### `packages/core/src/__tests__/ignore-filter.test.ts` + +- Parses basic glob patterns (`*.log`, `dist/`) +- Handles `#` comments and blank lines +- Handles `!` negation (force-include) +- Handles `**/` recursive matching +- Handles trailing `/` for directory-only patterns +- Merges defaults + user patterns correctly +- `!` in user file overrides hardcoded defaults +- Returns `false` for paths not matching any pattern + +### `packages/core/src/__tests__/ignore-generator.test.ts` + +- Generates starter file with header comment +- Detects existing directories and suggests relevant patterns +- All suggestions are commented out (prefixed with `# `) +- Doesn't overwrite existing file +- Includes bin/obj suggestions when relevant + +--- + +## File Structure + +| File | Purpose | +|------|---------| +| `packages/core/src/ignore-filter.ts` | Parse .understandignore, merge with defaults, filter paths | +| `packages/core/src/ignore-generator.ts` | Generate starter file by scanning project structure | +| `packages/core/src/__tests__/ignore-filter.test.ts` | Filter logic tests | +| `packages/core/src/__tests__/ignore-generator.test.ts` | Generator tests | +| `agents/project-scanner.md` | Add Layer 2 filtering via IgnoreFilter | +| `skills/understand/SKILL.md` | Add Phase 0.5 (generate + pause for review) | +| `packages/core/package.json` | Add `ignore` npm dependency | From c0776233898fbbb9f8ee21c63c72d66d18703bb7 Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Fri, 10 Apr 2026 11:27:47 +0800 Subject: [PATCH 05/15] docs: add .understandignore implementation plan (7 tasks) TDD-driven plan covering IgnoreFilter module, IgnoreGenerator, core exports, project-scanner agent update, and skill Phase 0.5 integration. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../plans/2026-04-10-understandignore-impl.md | 776 ++++++++++++++++++ 1 file changed, 776 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-10-understandignore-impl.md diff --git a/docs/superpowers/plans/2026-04-10-understandignore-impl.md b/docs/superpowers/plans/2026-04-10-understandignore-impl.md new file mode 100644 index 0000000..4bc782d --- /dev/null +++ b/docs/superpowers/plans/2026-04-10-understandignore-impl.md @@ -0,0 +1,776 @@ +# .understandignore Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add user-configurable file exclusion via `.understandignore` files using `.gitignore` syntax, with auto-generated starter files and a pre-analysis review pause. + +**Architecture:** An `IgnoreFilter` module in `packages/core` uses the `ignore` npm package to parse `.understandignore` files and filter paths. A companion `IgnoreGenerator` scans the project for common patterns and produces a commented-out starter file. The `project-scanner` agent applies the filter as a second pass after its existing hardcoded exclusions. The `/understand` skill adds a Phase 0.5 that generates the starter file and pauses for user review. + +**Tech Stack:** TypeScript, `ignore` npm package, Vitest + +**Spec:** `docs/superpowers/specs/2026-04-10-understandignore-design.md` + +--- + +## File Structure + +### Core package +- Create: `understand-anything-plugin/packages/core/src/ignore-filter.ts` — parse .understandignore, merge with defaults, filter paths +- Create: `understand-anything-plugin/packages/core/src/ignore-generator.ts` — generate starter .understandignore by scanning project +- Create: `understand-anything-plugin/packages/core/src/__tests__/ignore-filter.test.ts` — filter tests +- Create: `understand-anything-plugin/packages/core/src/__tests__/ignore-generator.test.ts` — generator tests +- Modify: `understand-anything-plugin/packages/core/src/index.ts` — export new modules +- Modify: `understand-anything-plugin/packages/core/package.json` — add `ignore` dependency + +### Agents & skills +- Modify: `understand-anything-plugin/agents/project-scanner.md` — add Layer 2 filtering step +- Modify: `understand-anything-plugin/skills/understand/SKILL.md` — add Phase 0.5 + +--- + +## Task 1: Add `ignore` dependency + +**Files:** +- Modify: `understand-anything-plugin/packages/core/package.json` + +- [ ] **Step 1: Install the `ignore` npm package** + +Run: +```bash +cd understand-anything-plugin && pnpm add --filter @understand-anything/core ignore +``` + +- [ ] **Step 2: Verify it was added** + +Run: `grep ignore understand-anything-plugin/packages/core/package.json` +Expected: `"ignore": "^7.x.x"` (or similar) in dependencies + +- [ ] **Step 3: Commit** + +```bash +git add understand-anything-plugin/packages/core/package.json understand-anything-plugin/pnpm-lock.yaml +git commit -m "chore(core): add ignore package for .understandignore support" +``` + +--- + +## Task 2: Create IgnoreFilter module with tests (TDD) + +**Files:** +- Create: `understand-anything-plugin/packages/core/src/ignore-filter.ts` +- Create: `understand-anything-plugin/packages/core/src/__tests__/ignore-filter.test.ts` + +- [ ] **Step 1: Write the failing tests** + +Create `understand-anything-plugin/packages/core/src/__tests__/ignore-filter.test.ts`: + +```typescript +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { createIgnoreFilter, DEFAULT_IGNORE_PATTERNS } from "../ignore-filter"; +import { mkdirSync, writeFileSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +describe("IgnoreFilter", () => { + let testDir: string; + + beforeEach(() => { + testDir = join(tmpdir(), `ignore-filter-test-${Date.now()}`); + mkdirSync(testDir, { recursive: true }); + mkdirSync(join(testDir, ".understand-anything"), { recursive: true }); + }); + + afterEach(() => { + rmSync(testDir, { recursive: true, force: true }); + }); + + describe("DEFAULT_IGNORE_PATTERNS", () => { + it("contains node_modules", () => { + expect(DEFAULT_IGNORE_PATTERNS).toContain("node_modules/"); + }); + + it("contains .git", () => { + expect(DEFAULT_IGNORE_PATTERNS).toContain(".git/"); + }); + + it("contains bin and obj for .NET", () => { + expect(DEFAULT_IGNORE_PATTERNS).toContain("bin/"); + expect(DEFAULT_IGNORE_PATTERNS).toContain("obj/"); + }); + + it("contains build output directories", () => { + expect(DEFAULT_IGNORE_PATTERNS).toContain("dist/"); + expect(DEFAULT_IGNORE_PATTERNS).toContain("build/"); + expect(DEFAULT_IGNORE_PATTERNS).toContain("out/"); + expect(DEFAULT_IGNORE_PATTERNS).toContain("coverage/"); + }); + }); + + describe("createIgnoreFilter with no user file", () => { + it("ignores files matching default patterns", () => { + const filter = createIgnoreFilter(testDir); + expect(filter.isIgnored("node_modules/foo/bar.js")).toBe(true); + expect(filter.isIgnored("dist/index.js")).toBe(true); + expect(filter.isIgnored(".git/config")).toBe(true); + expect(filter.isIgnored("bin/Debug/app.dll")).toBe(true); + expect(filter.isIgnored("obj/Release/net8.0/app.dll")).toBe(true); + }); + + it("does not ignore source files", () => { + const filter = createIgnoreFilter(testDir); + expect(filter.isIgnored("src/index.ts")).toBe(false); + expect(filter.isIgnored("README.md")).toBe(false); + expect(filter.isIgnored("package.json")).toBe(false); + }); + + it("ignores lock files", () => { + const filter = createIgnoreFilter(testDir); + expect(filter.isIgnored("pnpm-lock.yaml")).toBe(true); + expect(filter.isIgnored("package-lock.json")).toBe(true); + expect(filter.isIgnored("yarn.lock")).toBe(true); + }); + + it("ignores binary/asset files", () => { + const filter = createIgnoreFilter(testDir); + expect(filter.isIgnored("logo.png")).toBe(true); + expect(filter.isIgnored("font.woff2")).toBe(true); + expect(filter.isIgnored("doc.pdf")).toBe(true); + }); + + it("ignores generated files", () => { + const filter = createIgnoreFilter(testDir); + expect(filter.isIgnored("bundle.min.js")).toBe(true); + expect(filter.isIgnored("style.min.css")).toBe(true); + expect(filter.isIgnored("source.map")).toBe(true); + }); + + it("ignores IDE directories", () => { + const filter = createIgnoreFilter(testDir); + expect(filter.isIgnored(".idea/workspace.xml")).toBe(true); + expect(filter.isIgnored(".vscode/settings.json")).toBe(true); + }); + }); + + describe("createIgnoreFilter with user .understandignore", () => { + it("reads patterns from .understand-anything/.understandignore", () => { + writeFileSync( + join(testDir, ".understand-anything", ".understandignore"), + "# Exclude tests\n__tests__/\n*.test.ts\n" + ); + const filter = createIgnoreFilter(testDir); + expect(filter.isIgnored("__tests__/foo.test.ts")).toBe(true); + expect(filter.isIgnored("src/utils.test.ts")).toBe(true); + expect(filter.isIgnored("src/utils.ts")).toBe(false); + }); + + it("reads patterns from project root .understandignore", () => { + writeFileSync( + join(testDir, ".understandignore"), + "docs/\n" + ); + const filter = createIgnoreFilter(testDir); + expect(filter.isIgnored("docs/README.md")).toBe(true); + expect(filter.isIgnored("src/index.ts")).toBe(false); + }); + + it("handles # comments and blank lines", () => { + writeFileSync( + join(testDir, ".understand-anything", ".understandignore"), + "# This is a comment\n\n\nfixtures/\n\n# Another comment\n" + ); + const filter = createIgnoreFilter(testDir); + expect(filter.isIgnored("fixtures/data.json")).toBe(true); + expect(filter.isIgnored("src/index.ts")).toBe(false); + }); + + it("supports ! negation to override defaults", () => { + writeFileSync( + join(testDir, ".understand-anything", ".understandignore"), + "!dist/\n" + ); + const filter = createIgnoreFilter(testDir); + // dist/ is in defaults but negated by user + expect(filter.isIgnored("dist/index.js")).toBe(false); + }); + + it("supports ** recursive matching", () => { + writeFileSync( + join(testDir, ".understand-anything", ".understandignore"), + "**/snapshots/\n" + ); + const filter = createIgnoreFilter(testDir); + expect(filter.isIgnored("src/components/snapshots/Button.snap")).toBe(true); + expect(filter.isIgnored("snapshots/foo.snap")).toBe(true); + }); + + it("merges .understand-anything/ and root .understandignore", () => { + writeFileSync( + join(testDir, ".understand-anything", ".understandignore"), + "__tests__/\n" + ); + writeFileSync( + join(testDir, ".understandignore"), + "fixtures/\n" + ); + const filter = createIgnoreFilter(testDir); + expect(filter.isIgnored("__tests__/foo.ts")).toBe(true); + expect(filter.isIgnored("fixtures/data.json")).toBe(true); + expect(filter.isIgnored("src/index.ts")).toBe(false); + }); + }); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `pnpm --filter @understand-anything/core test -- --run src/__tests__/ignore-filter.test.ts` +Expected: FAIL — module not found + +- [ ] **Step 3: Implement IgnoreFilter** + +Create `understand-anything-plugin/packages/core/src/ignore-filter.ts`: + +```typescript +import ignore, { type Ignore } from "ignore"; +import { readFileSync, existsSync } from "node:fs"; +import { join } from "node:path"; + +/** + * Hardcoded default ignore patterns matching the project-scanner agent's + * exclusion rules, plus bin/obj for .NET projects. + */ +export const DEFAULT_IGNORE_PATTERNS: string[] = [ + // Dependency directories + "node_modules/", + ".git/", + "vendor/", + "venv/", + ".venv/", + "__pycache__/", + + // Build output + "dist/", + "build/", + "out/", + "coverage/", + ".next/", + ".cache/", + ".turbo/", + "target/", + "bin/", + "obj/", + + // Lock files + "*.lock", + "package-lock.json", + "yarn.lock", + "pnpm-lock.yaml", + + // Binary/asset files + "*.png", + "*.jpg", + "*.jpeg", + "*.gif", + "*.svg", + "*.ico", + "*.woff", + "*.woff2", + "*.ttf", + "*.eot", + "*.mp3", + "*.mp4", + "*.pdf", + "*.zip", + "*.tar", + "*.gz", + + // Generated files + "*.min.js", + "*.min.css", + "*.map", + "*.generated.*", + + // IDE/editor + ".idea/", + ".vscode/", + + // Misc + "LICENSE", + ".gitignore", + ".editorconfig", + ".prettierrc", + ".eslintrc*", + "*.log", +]; + +export interface IgnoreFilter { + /** Returns true if the given relative path should be excluded from analysis. */ + isIgnored(relativePath: string): boolean; +} + +/** + * Creates an IgnoreFilter that merges hardcoded defaults with user-defined + * patterns from .understandignore files. + * + * Pattern load order (later entries can override earlier ones via ! negation): + * 1. Hardcoded defaults + * 2. .understand-anything/.understandignore (if exists) + * 3. .understandignore at project root (if exists) + */ +export function createIgnoreFilter(projectRoot: string): IgnoreFilter { + const ig: Ignore = ignore(); + + // Layer 1: hardcoded defaults + ig.add(DEFAULT_IGNORE_PATTERNS); + + // Layer 2: .understand-anything/.understandignore + const projectIgnorePath = join(projectRoot, ".understand-anything", ".understandignore"); + if (existsSync(projectIgnorePath)) { + const content = readFileSync(projectIgnorePath, "utf-8"); + ig.add(content); + } + + // Layer 3: .understandignore at project root + const rootIgnorePath = join(projectRoot, ".understandignore"); + if (existsSync(rootIgnorePath)) { + const content = readFileSync(rootIgnorePath, "utf-8"); + ig.add(content); + } + + return { + isIgnored(relativePath: string): boolean { + return ig.ignores(relativePath); + }, + }; +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `pnpm --filter @understand-anything/core test -- --run src/__tests__/ignore-filter.test.ts` +Expected: All tests PASS + +- [ ] **Step 5: Build to verify no type errors** + +Run: `pnpm --filter @understand-anything/core build` +Expected: Clean build + +- [ ] **Step 6: Commit** + +```bash +git add understand-anything-plugin/packages/core/src/ignore-filter.ts understand-anything-plugin/packages/core/src/__tests__/ignore-filter.test.ts +git commit -m "feat(core): add IgnoreFilter module with .understandignore parsing and tests" +``` + +--- + +## Task 3: Create IgnoreGenerator module with tests (TDD) + +**Files:** +- Create: `understand-anything-plugin/packages/core/src/ignore-generator.ts` +- Create: `understand-anything-plugin/packages/core/src/__tests__/ignore-generator.test.ts` + +- [ ] **Step 1: Write the failing tests** + +Create `understand-anything-plugin/packages/core/src/__tests__/ignore-generator.test.ts`: + +```typescript +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { generateStarterIgnoreFile } from "../ignore-generator"; +import { mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +describe("generateStarterIgnoreFile", () => { + let testDir: string; + + beforeEach(() => { + testDir = join(tmpdir(), `ignore-gen-test-${Date.now()}`); + mkdirSync(testDir, { recursive: true }); + }); + + afterEach(() => { + rmSync(testDir, { recursive: true, force: true }); + }); + + it("includes a header comment explaining the file", () => { + const content = generateStarterIgnoreFile(testDir); + expect(content).toContain(".understandignore"); + expect(content).toContain("same as .gitignore"); + expect(content).toContain("Built-in defaults"); + }); + + it("all suggestions are commented out", () => { + // Create some directories to trigger suggestions + mkdirSync(join(testDir, "__tests__"), { recursive: true }); + mkdirSync(join(testDir, "docs"), { recursive: true }); + const content = generateStarterIgnoreFile(testDir); + const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#")); + // No active (uncommented) patterns + expect(lines).toHaveLength(0); + }); + + it("suggests __tests__ when __tests__ directory exists", () => { + mkdirSync(join(testDir, "__tests__"), { recursive: true }); + const content = generateStarterIgnoreFile(testDir); + expect(content).toContain("# __tests__/"); + }); + + it("suggests docs when docs directory exists", () => { + mkdirSync(join(testDir, "docs"), { recursive: true }); + const content = generateStarterIgnoreFile(testDir); + expect(content).toContain("# docs/"); + }); + + it("suggests test directories when they exist", () => { + mkdirSync(join(testDir, "test"), { recursive: true }); + mkdirSync(join(testDir, "tests"), { recursive: true }); + const content = generateStarterIgnoreFile(testDir); + expect(content).toContain("# test/"); + expect(content).toContain("# tests/"); + }); + + it("suggests fixtures when fixtures directory exists", () => { + mkdirSync(join(testDir, "fixtures"), { recursive: true }); + const content = generateStarterIgnoreFile(testDir); + expect(content).toContain("# fixtures/"); + }); + + it("suggests examples when examples directory exists", () => { + mkdirSync(join(testDir, "examples"), { recursive: true }); + const content = generateStarterIgnoreFile(testDir); + expect(content).toContain("# examples/"); + }); + + it("suggests .storybook when .storybook directory exists", () => { + mkdirSync(join(testDir, ".storybook"), { recursive: true }); + const content = generateStarterIgnoreFile(testDir); + expect(content).toContain("# .storybook/"); + }); + + it("suggests migrations when migrations directory exists", () => { + mkdirSync(join(testDir, "migrations"), { recursive: true }); + const content = generateStarterIgnoreFile(testDir); + expect(content).toContain("# migrations/"); + }); + + it("suggests scripts when scripts directory exists", () => { + mkdirSync(join(testDir, "scripts"), { recursive: true }); + const content = generateStarterIgnoreFile(testDir); + expect(content).toContain("# scripts/"); + }); + + it("always includes generic suggestions", () => { + const content = generateStarterIgnoreFile(testDir); + expect(content).toContain("# *.snap"); + expect(content).toContain("# *.test.*"); + expect(content).toContain("# *.spec.*"); + }); + + it("does not suggest directories that don't exist", () => { + const content = generateStarterIgnoreFile(testDir); + // __tests__ doesn't exist, so it shouldn't be in directory suggestions + // (it may still be in generic test file patterns) + expect(content).not.toContain("# __tests__/"); + expect(content).not.toContain("# .storybook/"); + }); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `pnpm --filter @understand-anything/core test -- --run src/__tests__/ignore-generator.test.ts` +Expected: FAIL — module not found + +- [ ] **Step 3: Implement IgnoreGenerator** + +Create `understand-anything-plugin/packages/core/src/ignore-generator.ts`: + +```typescript +import { existsSync } from "node:fs"; +import { join } from "node:path"; + +const HEADER = `# .understandignore — patterns for files/dirs to exclude from analysis +# Syntax: same as .gitignore (globs, # comments, ! negation, trailing / for dirs) +# Lines below are suggestions — uncomment to activate. +# Use ! prefix to force-include something excluded by defaults. +# +# Built-in defaults (always excluded unless negated): +# node_modules/, .git/, dist/, build/, bin/, obj/, *.lock, *.min.js, etc. +# +`; + +/** Directories to check for and suggest excluding. */ +const DETECTABLE_DIRS = [ + { dir: "__tests__", pattern: "__tests__/" }, + { dir: "test", pattern: "test/" }, + { dir: "tests", pattern: "tests/" }, + { dir: "fixtures", pattern: "fixtures/" }, + { dir: "testdata", pattern: "testdata/" }, + { dir: "docs", pattern: "docs/" }, + { dir: "examples", pattern: "examples/" }, + { dir: "scripts", pattern: "scripts/" }, + { dir: "migrations", pattern: "migrations/" }, + { dir: ".storybook", pattern: ".storybook/" }, +]; + +/** Always-included generic suggestions. */ +const GENERIC_SUGGESTIONS = [ + "*.test.*", + "*.spec.*", + "*.snap", +]; + +/** + * Generates a starter .understandignore file by scanning the project root + * for common directories and suggesting them as commented-out exclusions. + * + * All suggestions are commented out — the user must uncomment to activate. + * Returns the file content as a string. + */ +export function generateStarterIgnoreFile(projectRoot: string): string { + const sections: string[] = [HEADER]; + + // Detected directory suggestions + const detected: string[] = []; + for (const { dir, pattern } of DETECTABLE_DIRS) { + if (existsSync(join(projectRoot, dir))) { + detected.push(pattern); + } + } + + if (detected.length > 0) { + sections.push("# --- Detected directories (uncomment to exclude) ---\n"); + for (const pattern of detected) { + sections.push(`# ${pattern}`); + } + sections.push(""); + } + + // Generic suggestions (always included) + sections.push("# --- Test file patterns (uncomment to exclude) ---\n"); + for (const pattern of GENERIC_SUGGESTIONS) { + sections.push(`# ${pattern}`); + } + sections.push(""); + + return sections.join("\n"); +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `pnpm --filter @understand-anything/core test -- --run src/__tests__/ignore-generator.test.ts` +Expected: All tests PASS + +- [ ] **Step 5: Build** + +Run: `pnpm --filter @understand-anything/core build` +Expected: Clean build + +- [ ] **Step 6: Commit** + +```bash +git add understand-anything-plugin/packages/core/src/ignore-generator.ts understand-anything-plugin/packages/core/src/__tests__/ignore-generator.test.ts +git commit -m "feat(core): add IgnoreGenerator for starter .understandignore file creation" +``` + +--- + +## Task 4: Export new modules from core + +**Files:** +- Modify: `understand-anything-plugin/packages/core/src/index.ts` + +- [ ] **Step 1: Add exports** + +Add to the end of `understand-anything-plugin/packages/core/src/index.ts`: + +```typescript +export { + createIgnoreFilter, + DEFAULT_IGNORE_PATTERNS, + type IgnoreFilter, +} from "./ignore-filter.js"; +export { generateStarterIgnoreFile } from "./ignore-generator.js"; +``` + +- [ ] **Step 2: Build and run all tests** + +Run: `pnpm --filter @understand-anything/core build && pnpm --filter @understand-anything/core test -- --run` +Expected: Clean build, all tests pass + +- [ ] **Step 3: Commit** + +```bash +git add understand-anything-plugin/packages/core/src/index.ts +git commit -m "feat(core): export IgnoreFilter and IgnoreGenerator from core index" +``` + +--- + +## Task 5: Update project-scanner agent + +**Files:** +- Modify: `understand-anything-plugin/agents/project-scanner.md` + +- [ ] **Step 1: Read the current project-scanner.md** + +Read `understand-anything-plugin/agents/project-scanner.md` to understand the current structure. + +- [ ] **Step 2: Add bin/ and obj/ to hardcoded exclusions** + +In Step 2 (Exclusion Filtering), add `bin/` and `obj/` to the "Build output" line: + +Change: +``` +- **Build output:** paths with a directory segment matching `dist/`, `build/`, `out/`, `coverage/`, `.next/`, `.cache/`, `.turbo/`, `target/` (Rust) +``` + +To: +``` +- **Build output:** paths with a directory segment matching `dist/`, `build/`, `out/`, `coverage/`, `.next/`, `.cache/`, `.turbo/`, `target/` (Rust), `bin/` (.NET), `obj/` (.NET) +``` + +- [ ] **Step 3: Add Layer 2 filtering step** + +After Step 2 (Exclusion Filtering), add a new step: + +```markdown +**Step 2.5 -- User-Configured Filtering (.understandignore)** + +After applying the hardcoded exclusion filters above, apply user-configured patterns from `.understandignore`: + +1. Check if `.understand-anything/.understandignore` exists in the project root. If so, read it. +2. Check if `.understandignore` exists in the project root. If so, read it. +3. Parse both files using `.gitignore` syntax (glob patterns, `#` comments, blank lines ignored, `!` prefix for negation, trailing `/` for directories, `**/` for recursive matching). +4. Filter the remaining file list through these patterns. Files matching any pattern are excluded. +5. `!` negation patterns override the hardcoded exclusions from Step 2 (e.g., `!dist/` force-includes dist/). +6. Track the count of files removed by this step as `filteredByIgnore`. + +This filtering must be deterministic (not LLM-based). Use a Node.js script with the `ignore` npm package if implementing programmatically, or apply the patterns manually if the file list is small. +``` + +- [ ] **Step 4: Update scan output schema** + +Find the output JSON schema section and add `filteredByIgnore` field: + +```json +{ + "name": "...", + "description": "...", + "languages": ["..."], + "frameworks": ["..."], + "files": [...], + "totalFiles": 123, + "filteredByIgnore": 5, + "estimatedComplexity": "moderate", + "importMap": {} +} +``` + +- [ ] **Step 5: Commit** + +```bash +git add understand-anything-plugin/agents/project-scanner.md +git commit -m "feat(agent): add .understandignore support and bin/obj exclusions to project-scanner" +``` + +--- + +## Task 6: Update /understand skill with Phase 0.5 + +**Files:** +- Modify: `understand-anything-plugin/skills/understand/SKILL.md` + +- [ ] **Step 1: Read the current SKILL.md Phase 0 section** + +Read `understand-anything-plugin/skills/understand/SKILL.md` lines 22-80 to understand Phase 0. + +- [ ] **Step 2: Add Phase 0.5 after Phase 0** + +After the Phase 0 section (after the `---` separator before Phase 1), insert: + +```markdown +## Phase 0.5 — Ignore Configuration + +Set up and verify the `.understandignore` file before scanning. + +1. Check if `$PROJECT_ROOT/.understand-anything/.understandignore` exists. +2. **If it does NOT exist**, generate a starter file: + - Run a Node.js script (or inline logic) that scans `$PROJECT_ROOT` for common directories (`__tests__/`, `test/`, `tests/`, `fixtures/`, `testdata/`, `docs/`, `examples/`, `scripts/`, `migrations/`, `.storybook/`) and generates a `.understandignore` file with commented-out suggestions. + - Write the generated content to `$PROJECT_ROOT/.understand-anything/.understandignore`. + - Report to the user: + > "Generated `.understand-anything/.understandignore` with suggested exclusions based on your project structure. Please review it and uncomment any patterns you'd like to exclude from analysis. When ready, confirm to continue." + - **Wait for user confirmation before proceeding.** +3. **If it already exists**, report: + > "Found `.understand-anything/.understandignore`. Review it if needed, then confirm to continue." + - **Wait for user confirmation before proceeding.** +4. After confirmation, proceed to Phase 1. + +**Note:** The `.understandignore` file uses `.gitignore` syntax. The user can add patterns to exclude files from analysis, or use `!` prefix to force-include files excluded by built-in defaults (e.g., `!dist/` to analyze dist/ files). + +--- +``` + +- [ ] **Step 3: Update Phase 1 reporting** + +In the Phase 1 section, after the gate check (~line 114), add a note about reporting ignore stats: + +```markdown +After scanning, if the scan result includes `filteredByIgnore > 0`, report: +> "Scanned {totalFiles} files ({filteredByIgnore} excluded by .understandignore)" +``` + +- [ ] **Step 4: Commit** + +```bash +git add understand-anything-plugin/skills/understand/SKILL.md +git commit -m "feat(skill): add Phase 0.5 for .understandignore setup and review pause" +``` + +--- + +## Task 7: Build, test, and verify end-to-end + +**Files:** +- All modified files + +- [ ] **Step 1: Build core** + +Run: `pnpm --filter @understand-anything/core build` +Expected: Clean build + +- [ ] **Step 2: Run all core tests** + +Run: `pnpm --filter @understand-anything/core test -- --run` +Expected: All tests pass (existing + new ignore-filter + ignore-generator tests) + +- [ ] **Step 3: Build skill package** + +Run: `pnpm --filter @understand-anything/skill build` +Expected: Clean build + +- [ ] **Step 4: Verify files exist** + +Run: +```bash +ls understand-anything-plugin/packages/core/src/ignore-filter.ts understand-anything-plugin/packages/core/src/ignore-generator.ts +``` +Expected: Both files listed + +- [ ] **Step 5: Verify exports work** + +Run: +```bash +node -e "import('@understand-anything/core').then(m => { console.log('IgnoreFilter:', typeof m.createIgnoreFilter); console.log('Generator:', typeof m.generateStarterIgnoreFile); })" +``` +Expected: Both show `function` + +- [ ] **Step 6: Final commit (if any unstaged changes)** + +```bash +git status +# If clean, skip. If changes exist: +git add -A && git commit -m "chore: final verification for .understandignore support" +``` From 07b02d18ae3f8a348a3cde4fcabdf1394f3cf5a4 Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Fri, 10 Apr 2026 11:30:52 +0800 Subject: [PATCH 06/15] chore(core): add ignore package for .understandignore support --- .../packages/core/package.json | 1 + understand-anything-plugin/pnpm-lock.yaml | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/understand-anything-plugin/packages/core/package.json b/understand-anything-plugin/packages/core/package.json index 457aae8..2318ddc 100644 --- a/understand-anything-plugin/packages/core/package.json +++ b/understand-anything-plugin/packages/core/package.json @@ -38,6 +38,7 @@ }, "dependencies": { "fuse.js": "^7.1.0", + "ignore": "^7.0.5", "tree-sitter-javascript": "^0.25.0", "tree-sitter-typescript": "^0.23.2", "web-tree-sitter": "^0.26.6", diff --git a/understand-anything-plugin/pnpm-lock.yaml b/understand-anything-plugin/pnpm-lock.yaml index 0134903..01e7e07 100644 --- a/understand-anything-plugin/pnpm-lock.yaml +++ b/understand-anything-plugin/pnpm-lock.yaml @@ -27,6 +27,9 @@ importers: fuse.js: specifier: ^7.1.0 version: 7.1.0 + ignore: + specifier: ^7.0.5 + version: 7.0.5 tree-sitter-javascript: specifier: ^0.25.0 version: 0.25.0 @@ -998,6 +1001,10 @@ packages: html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} @@ -2184,6 +2191,14 @@ snapshots: optionalDependencies: vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) + '@vitest/mocker@3.2.4(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) + '@vitest/pretty-format@3.2.4': dependencies: tinyrainbow: 2.0.0 @@ -2486,6 +2501,8 @@ snapshots: html-url-attributes@3.0.1: {} + ignore@7.0.5: {} + inline-style-parser@0.2.7: {} is-alphabetical@2.0.1: {} @@ -3256,7 +3273,7 @@ snapshots: dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)) + '@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 From 5e86254b7775b9759dbaa05b667e77241885b1bc Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Fri, 10 Apr 2026 11:33:10 +0800 Subject: [PATCH 07/15] feat(core): add IgnoreFilter module with .understandignore parsing and tests Co-Authored-By: Claude Opus 4.6 (1M context) --- .../core/src/__tests__/ignore-filter.test.ts | 153 ++++++++++++++++++ .../packages/core/src/ignore-filter.ts | 112 +++++++++++++ 2 files changed, 265 insertions(+) create mode 100644 understand-anything-plugin/packages/core/src/__tests__/ignore-filter.test.ts create mode 100644 understand-anything-plugin/packages/core/src/ignore-filter.ts diff --git a/understand-anything-plugin/packages/core/src/__tests__/ignore-filter.test.ts b/understand-anything-plugin/packages/core/src/__tests__/ignore-filter.test.ts new file mode 100644 index 0000000..28c8c7e --- /dev/null +++ b/understand-anything-plugin/packages/core/src/__tests__/ignore-filter.test.ts @@ -0,0 +1,153 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { createIgnoreFilter, DEFAULT_IGNORE_PATTERNS } from "../ignore-filter"; +import { mkdirSync, writeFileSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +describe("IgnoreFilter", () => { + let testDir: string; + + beforeEach(() => { + testDir = join(tmpdir(), `ignore-filter-test-${Date.now()}`); + mkdirSync(testDir, { recursive: true }); + mkdirSync(join(testDir, ".understand-anything"), { recursive: true }); + }); + + afterEach(() => { + rmSync(testDir, { recursive: true, force: true }); + }); + + describe("DEFAULT_IGNORE_PATTERNS", () => { + it("contains node_modules", () => { + expect(DEFAULT_IGNORE_PATTERNS).toContain("node_modules/"); + }); + + it("contains .git", () => { + expect(DEFAULT_IGNORE_PATTERNS).toContain(".git/"); + }); + + it("contains bin and obj for .NET", () => { + expect(DEFAULT_IGNORE_PATTERNS).toContain("bin/"); + expect(DEFAULT_IGNORE_PATTERNS).toContain("obj/"); + }); + + it("contains build output directories", () => { + expect(DEFAULT_IGNORE_PATTERNS).toContain("dist/"); + expect(DEFAULT_IGNORE_PATTERNS).toContain("build/"); + expect(DEFAULT_IGNORE_PATTERNS).toContain("out/"); + expect(DEFAULT_IGNORE_PATTERNS).toContain("coverage/"); + }); + }); + + describe("createIgnoreFilter with no user file", () => { + it("ignores files matching default patterns", () => { + const filter = createIgnoreFilter(testDir); + expect(filter.isIgnored("node_modules/foo/bar.js")).toBe(true); + expect(filter.isIgnored("dist/index.js")).toBe(true); + expect(filter.isIgnored(".git/config")).toBe(true); + expect(filter.isIgnored("bin/Debug/app.dll")).toBe(true); + expect(filter.isIgnored("obj/Release/net8.0/app.dll")).toBe(true); + }); + + it("does not ignore source files", () => { + const filter = createIgnoreFilter(testDir); + expect(filter.isIgnored("src/index.ts")).toBe(false); + expect(filter.isIgnored("README.md")).toBe(false); + expect(filter.isIgnored("package.json")).toBe(false); + }); + + it("ignores lock files", () => { + const filter = createIgnoreFilter(testDir); + expect(filter.isIgnored("pnpm-lock.yaml")).toBe(true); + expect(filter.isIgnored("package-lock.json")).toBe(true); + expect(filter.isIgnored("yarn.lock")).toBe(true); + }); + + it("ignores binary/asset files", () => { + const filter = createIgnoreFilter(testDir); + expect(filter.isIgnored("logo.png")).toBe(true); + expect(filter.isIgnored("font.woff2")).toBe(true); + expect(filter.isIgnored("doc.pdf")).toBe(true); + }); + + it("ignores generated files", () => { + const filter = createIgnoreFilter(testDir); + expect(filter.isIgnored("bundle.min.js")).toBe(true); + expect(filter.isIgnored("style.min.css")).toBe(true); + expect(filter.isIgnored("source.map")).toBe(true); + }); + + it("ignores IDE directories", () => { + const filter = createIgnoreFilter(testDir); + expect(filter.isIgnored(".idea/workspace.xml")).toBe(true); + expect(filter.isIgnored(".vscode/settings.json")).toBe(true); + }); + }); + + describe("createIgnoreFilter with user .understandignore", () => { + it("reads patterns from .understand-anything/.understandignore", () => { + writeFileSync( + join(testDir, ".understand-anything", ".understandignore"), + "# Exclude tests\n__tests__/\n*.test.ts\n" + ); + const filter = createIgnoreFilter(testDir); + expect(filter.isIgnored("__tests__/foo.test.ts")).toBe(true); + expect(filter.isIgnored("src/utils.test.ts")).toBe(true); + expect(filter.isIgnored("src/utils.ts")).toBe(false); + }); + + it("reads patterns from project root .understandignore", () => { + writeFileSync( + join(testDir, ".understandignore"), + "docs/\n" + ); + const filter = createIgnoreFilter(testDir); + expect(filter.isIgnored("docs/README.md")).toBe(true); + expect(filter.isIgnored("src/index.ts")).toBe(false); + }); + + it("handles # comments and blank lines", () => { + writeFileSync( + join(testDir, ".understand-anything", ".understandignore"), + "# This is a comment\n\n\nfixtures/\n\n# Another comment\n" + ); + const filter = createIgnoreFilter(testDir); + expect(filter.isIgnored("fixtures/data.json")).toBe(true); + expect(filter.isIgnored("src/index.ts")).toBe(false); + }); + + it("supports ! negation to override defaults", () => { + writeFileSync( + join(testDir, ".understand-anything", ".understandignore"), + "!dist/\n" + ); + const filter = createIgnoreFilter(testDir); + expect(filter.isIgnored("dist/index.js")).toBe(false); + }); + + it("supports ** recursive matching", () => { + writeFileSync( + join(testDir, ".understand-anything", ".understandignore"), + "**/snapshots/\n" + ); + const filter = createIgnoreFilter(testDir); + expect(filter.isIgnored("src/components/snapshots/Button.snap")).toBe(true); + expect(filter.isIgnored("snapshots/foo.snap")).toBe(true); + }); + + it("merges .understand-anything/ and root .understandignore", () => { + writeFileSync( + join(testDir, ".understand-anything", ".understandignore"), + "__tests__/\n" + ); + writeFileSync( + join(testDir, ".understandignore"), + "fixtures/\n" + ); + const filter = createIgnoreFilter(testDir); + expect(filter.isIgnored("__tests__/foo.ts")).toBe(true); + expect(filter.isIgnored("fixtures/data.json")).toBe(true); + expect(filter.isIgnored("src/index.ts")).toBe(false); + }); + }); +}); diff --git a/understand-anything-plugin/packages/core/src/ignore-filter.ts b/understand-anything-plugin/packages/core/src/ignore-filter.ts new file mode 100644 index 0000000..88a65b9 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/ignore-filter.ts @@ -0,0 +1,112 @@ +import ignore, { type Ignore } from "ignore"; +import { readFileSync, existsSync } from "node:fs"; +import { join } from "node:path"; + +/** + * Hardcoded default ignore patterns matching the project-scanner agent's + * exclusion rules, plus bin/obj for .NET projects. + */ +export const DEFAULT_IGNORE_PATTERNS: string[] = [ + // Dependency directories + "node_modules/", + ".git/", + "vendor/", + "venv/", + ".venv/", + "__pycache__/", + + // Build output + "dist/", + "build/", + "out/", + "coverage/", + ".next/", + ".cache/", + ".turbo/", + "target/", + "bin/", + "obj/", + + // Lock files + "*.lock", + "package-lock.json", + "yarn.lock", + "pnpm-lock.yaml", + + // Binary/asset files + "*.png", + "*.jpg", + "*.jpeg", + "*.gif", + "*.svg", + "*.ico", + "*.woff", + "*.woff2", + "*.ttf", + "*.eot", + "*.mp3", + "*.mp4", + "*.pdf", + "*.zip", + "*.tar", + "*.gz", + + // Generated files + "*.min.js", + "*.min.css", + "*.map", + "*.generated.*", + + // IDE/editor + ".idea/", + ".vscode/", + + // Misc + "LICENSE", + ".gitignore", + ".editorconfig", + ".prettierrc", + ".eslintrc*", + "*.log", +]; + +export interface IgnoreFilter { + /** Returns true if the given relative path should be excluded from analysis. */ + isIgnored(relativePath: string): boolean; +} + +/** + * Creates an IgnoreFilter that merges hardcoded defaults with user-defined + * patterns from .understandignore files. + * + * Pattern load order (later entries can override earlier ones via ! negation): + * 1. Hardcoded defaults + * 2. .understand-anything/.understandignore (if exists) + * 3. .understandignore at project root (if exists) + */ +export function createIgnoreFilter(projectRoot: string): IgnoreFilter { + const ig: Ignore = ignore(); + + // Layer 1: hardcoded defaults + ig.add(DEFAULT_IGNORE_PATTERNS); + + // Layer 2: .understand-anything/.understandignore + const projectIgnorePath = join(projectRoot, ".understand-anything", ".understandignore"); + if (existsSync(projectIgnorePath)) { + const content = readFileSync(projectIgnorePath, "utf-8"); + ig.add(content); + } + + // Layer 3: .understandignore at project root + const rootIgnorePath = join(projectRoot, ".understandignore"); + if (existsSync(rootIgnorePath)) { + const content = readFileSync(rootIgnorePath, "utf-8"); + ig.add(content); + } + + return { + isIgnored(relativePath: string): boolean { + return ig.ignores(relativePath); + }, + }; +} From 6f8270fb378a418f7c965627346eaeafbfdf3212 Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Fri, 10 Apr 2026 11:36:23 +0800 Subject: [PATCH 08/15] feat(core): add IgnoreGenerator for starter .understandignore file creation Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/__tests__/ignore-generator.test.ts | 97 +++++++++++++++++++ .../packages/core/src/ignore-generator.ts | 62 ++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 understand-anything-plugin/packages/core/src/__tests__/ignore-generator.test.ts create mode 100644 understand-anything-plugin/packages/core/src/ignore-generator.ts diff --git a/understand-anything-plugin/packages/core/src/__tests__/ignore-generator.test.ts b/understand-anything-plugin/packages/core/src/__tests__/ignore-generator.test.ts new file mode 100644 index 0000000..8c2189b --- /dev/null +++ b/understand-anything-plugin/packages/core/src/__tests__/ignore-generator.test.ts @@ -0,0 +1,97 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { generateStarterIgnoreFile } from "../ignore-generator"; +import { mkdirSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +describe("generateStarterIgnoreFile", () => { + let testDir: string; + + beforeEach(() => { + testDir = join(tmpdir(), `ignore-gen-test-${Date.now()}`); + mkdirSync(testDir, { recursive: true }); + }); + + afterEach(() => { + rmSync(testDir, { recursive: true, force: true }); + }); + + it("includes a header comment explaining the file", () => { + const content = generateStarterIgnoreFile(testDir); + expect(content).toContain(".understandignore"); + expect(content).toContain("same as .gitignore"); + expect(content).toContain("Built-in defaults"); + }); + + it("all suggestions are commented out", () => { + mkdirSync(join(testDir, "__tests__"), { recursive: true }); + mkdirSync(join(testDir, "docs"), { recursive: true }); + const content = generateStarterIgnoreFile(testDir); + const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#")); + expect(lines).toHaveLength(0); + }); + + it("suggests __tests__ when directory exists", () => { + mkdirSync(join(testDir, "__tests__"), { recursive: true }); + const content = generateStarterIgnoreFile(testDir); + expect(content).toContain("# __tests__/"); + }); + + it("suggests docs when directory exists", () => { + mkdirSync(join(testDir, "docs"), { recursive: true }); + const content = generateStarterIgnoreFile(testDir); + expect(content).toContain("# docs/"); + }); + + it("suggests test and tests when they exist", () => { + mkdirSync(join(testDir, "test"), { recursive: true }); + mkdirSync(join(testDir, "tests"), { recursive: true }); + const content = generateStarterIgnoreFile(testDir); + expect(content).toContain("# test/"); + expect(content).toContain("# tests/"); + }); + + it("suggests fixtures when directory exists", () => { + mkdirSync(join(testDir, "fixtures"), { recursive: true }); + const content = generateStarterIgnoreFile(testDir); + expect(content).toContain("# fixtures/"); + }); + + it("suggests examples when directory exists", () => { + mkdirSync(join(testDir, "examples"), { recursive: true }); + const content = generateStarterIgnoreFile(testDir); + expect(content).toContain("# examples/"); + }); + + it("suggests .storybook when directory exists", () => { + mkdirSync(join(testDir, ".storybook"), { recursive: true }); + const content = generateStarterIgnoreFile(testDir); + expect(content).toContain("# .storybook/"); + }); + + it("suggests migrations when directory exists", () => { + mkdirSync(join(testDir, "migrations"), { recursive: true }); + const content = generateStarterIgnoreFile(testDir); + expect(content).toContain("# migrations/"); + }); + + it("suggests scripts when directory exists", () => { + mkdirSync(join(testDir, "scripts"), { recursive: true }); + const content = generateStarterIgnoreFile(testDir); + expect(content).toContain("# scripts/"); + }); + + it("always includes generic test file suggestions", () => { + const content = generateStarterIgnoreFile(testDir); + expect(content).toContain("# *.snap"); + expect(content).toContain("# *.test.*"); + expect(content).toContain("# *.spec.*"); + }); + + it("does not suggest directories that don't exist", () => { + const content = generateStarterIgnoreFile(testDir); + expect(content).not.toContain("# __tests__/"); + expect(content).not.toContain("# .storybook/"); + expect(content).not.toContain("# fixtures/"); + }); +}); diff --git a/understand-anything-plugin/packages/core/src/ignore-generator.ts b/understand-anything-plugin/packages/core/src/ignore-generator.ts new file mode 100644 index 0000000..021e170 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/ignore-generator.ts @@ -0,0 +1,62 @@ +import { existsSync } from "node:fs"; +import { join } from "node:path"; + +const HEADER = `# .understandignore — patterns for files/dirs to exclude from analysis +# Syntax: same as .gitignore (globs, # comments, ! negation, trailing / for dirs) +# Lines below are suggestions — uncomment to activate. +# Use ! prefix to force-include something excluded by defaults. +# +# Built-in defaults (always excluded unless negated): +# node_modules/, .git/, dist/, build/, bin/, obj/, *.lock, *.min.js, etc. +# +`; + +const DETECTABLE_DIRS = [ + { dir: "__tests__", pattern: "__tests__/" }, + { dir: "test", pattern: "test/" }, + { dir: "tests", pattern: "tests/" }, + { dir: "fixtures", pattern: "fixtures/" }, + { dir: "testdata", pattern: "testdata/" }, + { dir: "docs", pattern: "docs/" }, + { dir: "examples", pattern: "examples/" }, + { dir: "scripts", pattern: "scripts/" }, + { dir: "migrations", pattern: "migrations/" }, + { dir: ".storybook", pattern: ".storybook/" }, +]; + +const GENERIC_SUGGESTIONS = [ + "*.test.*", + "*.spec.*", + "*.snap", +]; + +/** + * Generates a starter .understandignore file content by scanning the project + * for common directories. All suggestions are commented out. + */ +export function generateStarterIgnoreFile(projectRoot: string): string { + const sections: string[] = [HEADER]; + + const detected: string[] = []; + for (const { dir, pattern } of DETECTABLE_DIRS) { + if (existsSync(join(projectRoot, dir))) { + detected.push(pattern); + } + } + + if (detected.length > 0) { + sections.push("# --- Detected directories (uncomment to exclude) ---\n"); + for (const pattern of detected) { + sections.push(`# ${pattern}`); + } + sections.push(""); + } + + sections.push("# --- Test file patterns (uncomment to exclude) ---\n"); + for (const pattern of GENERIC_SUGGESTIONS) { + sections.push(`# ${pattern}`); + } + sections.push(""); + + return sections.join("\n"); +} From 4201cbcb9e20b48056f22b5012d5a245a0aee622 Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Fri, 10 Apr 2026 11:36:53 +0800 Subject: [PATCH 09/15] feat(core): export IgnoreFilter and IgnoreGenerator from core index --- understand-anything-plugin/packages/core/src/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/understand-anything-plugin/packages/core/src/index.ts b/understand-anything-plugin/packages/core/src/index.ts index 756284d..0375dad 100644 --- a/understand-anything-plugin/packages/core/src/index.ts +++ b/understand-anything-plugin/packages/core/src/index.ts @@ -114,3 +114,9 @@ export { ShellParser, registerAllParsers, } from "./plugins/parsers/index.js"; +export { + createIgnoreFilter, + DEFAULT_IGNORE_PATTERNS, + type IgnoreFilter, +} from "./ignore-filter.js"; +export { generateStarterIgnoreFile } from "./ignore-generator.js"; From 3e27a4a8d448b81a78eda1ab8c9e9454d7128265 Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Fri, 10 Apr 2026 11:38:23 +0800 Subject: [PATCH 10/15] feat(agent): add .understandignore support and bin/obj exclusions to project-scanner Co-Authored-By: Claude Opus 4.6 (1M context) --- .../agents/project-scanner.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/understand-anything-plugin/agents/project-scanner.md b/understand-anything-plugin/agents/project-scanner.md index e8814f9..090c281 100644 --- a/understand-anything-plugin/agents/project-scanner.md +++ b/understand-anything-plugin/agents/project-scanner.md @@ -39,7 +39,7 @@ Discover all tracked files. In order of preference: Remove ALL files matching these patterns: - **Dependency directories:** paths containing `node_modules/`, `.git/`, `vendor/`, `venv/`, `.venv/`, `__pycache__/` -- **Build output:** paths with a directory segment matching `dist/`, `build/`, `out/`, `coverage/`, `.next/`, `.cache/`, `.turbo/`, `target/` (Rust) — match full directory segments only, not substrings (e.g., `buildSrc/` should NOT be excluded) +- **Build output:** paths with a directory segment matching `dist/`, `build/`, `out/`, `coverage/`, `.next/`, `.cache/`, `.turbo/`, `target/` (Rust), `bin/` (.NET), `obj/` (.NET) — match full directory segments only, not substrings (e.g., `buildSrc/` should NOT be excluded) - **Lock files:** `*.lock`, `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml` - **Binary/asset files:** `.png`, `.jpg`, `.jpeg`, `.gif`, `.svg`, `.ico`, `.woff`, `.woff2`, `.ttf`, `.eot`, `.mp3`, `.mp4`, `.pdf`, `.zip`, `.tar`, `.gz` - **Generated files:** `*.min.js`, `*.min.css`, `*.map`, `*.generated.*` (note: do NOT exclude `*.d.ts` — many projects have hand-written declaration files) @@ -58,6 +58,19 @@ Remove ALL files matching these patterns: **Note on package manifests:** Config files read for framework detection (`package.json`, `tsconfig.json`, `Cargo.toml`, `go.mod`, `pyproject.toml`, etc.) should also appear in the file list with `fileCategory: "config"`. +**Step 2.5 -- User-Configured Filtering (.understandignore)** + +After applying the hardcoded exclusion filters above, apply user-configured patterns from `.understandignore`: + +1. Check if `$PROJECT_ROOT/.understand-anything/.understandignore` exists. If so, read it. +2. Check if `$PROJECT_ROOT/.understandignore` exists. If so, read it. +3. Parse both files using `.gitignore` syntax (glob patterns, `#` comments, blank lines ignored, `!` prefix for negation, trailing `/` for directories, `**/` for recursive matching). +4. Filter the remaining file list through these patterns. Files matching any pattern are excluded. +5. `!` negation patterns override the hardcoded exclusions from Step 2 (e.g., `!dist/` force-includes files from dist/). +6. Track the count of files removed by this step as `filteredByIgnore`. + +This filtering must be deterministic (not LLM-based). Use a Node.js script with the `ignore` npm package from `@understand-anything/core`, or apply the patterns manually if the file list is small. + **Step 3 -- Language Detection** Map file extensions to language identifiers: @@ -217,6 +230,7 @@ The script must write this exact JSON structure to the output file: {"path": "package.json", "language": "json", "sizeLines": 35, "fileCategory": "config"} ], "totalFiles": 42, + "filteredByIgnore": 0, "estimatedComplexity": "moderate", "importMap": { "src/index.ts": ["src/utils.ts", "src/config.ts"], @@ -237,6 +251,7 @@ The script must write this exact JSON structure to the output file: - `files` (object[]) -- every discovered file, sorted by `path` alphabetically - `files[].fileCategory` (string) -- one of: `code`, `config`, `docs`, `infra`, `data`, `script`, `markup` - `totalFiles` (integer) -- must equal `files.length` +- `filteredByIgnore` (integer) -- count of files removed by `.understandignore` patterns in Step 2.5; 0 if no `.understandignore` file exists - `estimatedComplexity` (string) -- one of `small`, `moderate`, `large`, `very-large` - `importMap` (object) -- map from every file path to its list of resolved project-internal import paths; empty array for non-code files and files with no resolved imports; external packages excluded @@ -281,6 +296,7 @@ Then assemble the final output JSON: {"path": "Dockerfile", "language": "dockerfile", "sizeLines": 22, "fileCategory": "infra"} ], "totalFiles": 42, + "filteredByIgnore": 0, "estimatedComplexity": "moderate", "importMap": { "src/index.ts": ["src/utils.ts"] @@ -295,6 +311,7 @@ Then assemble the final output JSON: - `frameworks` (string[]): directly from script output - `files` (object[]): directly from script output, including `fileCategory` per file - `totalFiles` (integer): directly from script output +- `filteredByIgnore` (integer): directly from script output - `estimatedComplexity` (string): directly from script output - `importMap` (object): directly from script output From 59a6f56bfea046fb0b766916ba5fa09a8b6fd5f6 Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Fri, 10 Apr 2026 11:39:45 +0800 Subject: [PATCH 11/15] feat(skill): add Phase 0.5 for .understandignore setup and review pause Co-Authored-By: Claude Opus 4.6 (1M context) --- .../skills/understand/SKILL.md | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/understand-anything-plugin/skills/understand/SKILL.md b/understand-anything-plugin/skills/understand/SKILL.md index be26f59..3ad7fcf 100644 --- a/understand-anything-plugin/skills/understand/SKILL.md +++ b/understand-anything-plugin/skills/understand/SKILL.md @@ -77,6 +77,37 @@ Determine whether to run a full analysis or incremental update. --- +## Phase 0.5 — Ignore Configuration + +Set up and verify the `.understandignore` file before scanning. + +1. Check if `$PROJECT_ROOT/.understand-anything/.understandignore` exists. +2. **If it does NOT exist**, generate a starter file: + - Run the following Node.js one-liner in `$PROJECT_ROOT`: + ```bash + node -e " + const fs = require('fs'); + const path = require('path'); + const root = process.cwd(); + const dirs = ['__tests__','test','tests','fixtures','testdata','docs','examples','scripts','migrations','.storybook']; + const header = '# .understandignore — patterns for files/dirs to exclude from analysis\n# Syntax: same as .gitignore (globs, # comments, ! negation, trailing / for dirs)\n# Lines below are suggestions — uncomment to activate.\n# Use ! prefix to force-include something excluded by defaults.\n#\n# Built-in defaults (always excluded unless negated):\n# node_modules/, .git/, dist/, build/, bin/, obj/, *.lock, *.min.js, etc.\n#\n'; + let body = ''; + const found = dirs.filter(d => fs.existsSync(path.join(root, d))); + if (found.length) { body += '# --- Detected directories (uncomment to exclude) ---\n\n' + found.map(d => '# ' + d + '/').join('\n') + '\n\n'; } + body += '# --- Test file patterns (uncomment to exclude) ---\n\n# *.test.*\n# *.spec.*\n# *.snap\n'; + fs.writeFileSync(path.join(root, '.understand-anything', '.understandignore'), header + body); + " + ``` + - Report to the user: + > Generated `.understand-anything/.understandignore` with suggested exclusions based on your project structure. Please review it and uncomment any patterns you'd like to exclude from analysis. When ready, confirm to continue. + - **Wait for user confirmation before proceeding.** +3. **If it already exists**, report: + > Found `.understand-anything/.understandignore`. Review it if needed, then confirm to continue. + - **Wait for user confirmation before proceeding.** +4. After confirmation, proceed to Phase 1. + +--- + ## Phase 1 — SCAN (Full analysis only) Dispatch a subagent using the `project-scanner` agent definition (at `agents/project-scanner.md`). Append the following additional context: @@ -113,6 +144,9 @@ Store the file list as `$FILE_LIST` with `fileCategory` metadata for use in Phas **Gate check:** If >100 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. +If the scan result includes `filteredByIgnore > 0`, report: +> Excluded {filteredByIgnore} files via `.understandignore`. + --- ## Phase 2 — ANALYZE From 2104501bc8456ca96a88b919b0bb2aef73482fcf Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Fri, 10 Apr 2026 11:45:05 +0800 Subject: [PATCH 12/15] fix: update pnpm-lock.yaml with ignore dependency --- pnpm-lock.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 28627d0..90c1160 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,6 +42,9 @@ importers: fuse.js: specifier: ^7.1.0 version: 7.1.0 + ignore: + specifier: ^7.0.5 + version: 7.0.5 tree-sitter-javascript: specifier: ^0.25.0 version: 0.25.0 @@ -1569,6 +1572,10 @@ packages: http-cache-semantics@4.2.0: resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} @@ -4102,6 +4109,8 @@ snapshots: http-cache-semantics@4.2.0: {} + ignore@7.0.5: {} + inline-style-parser@0.2.7: {} iron-webcrypto@1.2.1: {} From d3de6dc1fd2b8bd1cab1b9138d8c927eb8893128 Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Fri, 10 Apr 2026 11:51:59 +0800 Subject: [PATCH 13/15] =?UTF-8?q?fix:=20address=20code=20review=20?= =?UTF-8?q?=E2=80=94=20remove=20bin/=20from=20defaults,=20fix=20negation?= =?UTF-8?q?=20override=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove bin/ from DEFAULT_IGNORE_PATTERNS (Node/Ruby CLI launchers use bin/) - .NET users can add bin/ to .understandignore manually - Fix project-scanner Step 2.5 to re-filter from original file list when .understandignore exists, ensuring ! negation correctly overrides defaults Co-Authored-By: Claude Opus 4.6 (1M context) --- .../agents/project-scanner.md | 13 ++++++------- .../core/src/__tests__/ignore-filter.test.ts | 8 +++++--- .../packages/core/src/ignore-filter.ts | 1 - 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/understand-anything-plugin/agents/project-scanner.md b/understand-anything-plugin/agents/project-scanner.md index 090c281..2ccc2fe 100644 --- a/understand-anything-plugin/agents/project-scanner.md +++ b/understand-anything-plugin/agents/project-scanner.md @@ -39,7 +39,7 @@ Discover all tracked files. In order of preference: Remove ALL files matching these patterns: - **Dependency directories:** paths containing `node_modules/`, `.git/`, `vendor/`, `venv/`, `.venv/`, `__pycache__/` -- **Build output:** paths with a directory segment matching `dist/`, `build/`, `out/`, `coverage/`, `.next/`, `.cache/`, `.turbo/`, `target/` (Rust), `bin/` (.NET), `obj/` (.NET) — match full directory segments only, not substrings (e.g., `buildSrc/` should NOT be excluded) +- **Build output:** paths with a directory segment matching `dist/`, `build/`, `out/`, `coverage/`, `.next/`, `.cache/`, `.turbo/`, `target/` (Rust), `obj/` (.NET) — match full directory segments only, not substrings (e.g., `buildSrc/` should NOT be excluded). Note: `bin/` is NOT excluded by default because Node.js and Ruby projects use `bin/` for CLI launchers; .NET users can add `bin/` to `.understandignore`. - **Lock files:** `*.lock`, `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml` - **Binary/asset files:** `.png`, `.jpg`, `.jpeg`, `.gif`, `.svg`, `.ico`, `.woff`, `.woff2`, `.ttf`, `.eot`, `.mp3`, `.mp4`, `.pdf`, `.zip`, `.tar`, `.gz` - **Generated files:** `*.min.js`, `*.min.css`, `*.map`, `*.generated.*` (note: do NOT exclude `*.d.ts` — many projects have hand-written declaration files) @@ -60,16 +60,15 @@ Remove ALL files matching these patterns: **Step 2.5 -- User-Configured Filtering (.understandignore)** -After applying the hardcoded exclusion filters above, apply user-configured patterns from `.understandignore`: +When `.understandignore` files exist, **replace** Step 2's hardcoded filtering with a unified filter that combines defaults and user patterns in a single pass. This ensures `!` negation patterns can override defaults. 1. Check if `$PROJECT_ROOT/.understand-anything/.understandignore` exists. If so, read it. 2. Check if `$PROJECT_ROOT/.understandignore` exists. If so, read it. -3. Parse both files using `.gitignore` syntax (glob patterns, `#` comments, blank lines ignored, `!` prefix for negation, trailing `/` for directories, `**/` for recursive matching). -4. Filter the remaining file list through these patterns. Files matching any pattern are excluded. -5. `!` negation patterns override the hardcoded exclusions from Step 2 (e.g., `!dist/` force-includes files from dist/). -6. Track the count of files removed by this step as `filteredByIgnore`. +3. If neither file exists, skip this step entirely — Step 2's hardcoded filtering is sufficient. +4. If at least one file exists, re-filter the **original file list from Step 1** (not the Step 2 output) using the `createIgnoreFilter` function from `@understand-anything/core`, which merges hardcoded defaults and user patterns into a single `.gitignore`-compatible matcher. This ensures `!` negation in user files can override hardcoded defaults (e.g., `!dist/` force-includes dist/ files). +5. Track the count of additional files removed beyond Step 2's baseline as `filteredByIgnore`. -This filtering must be deterministic (not LLM-based). Use a Node.js script with the `ignore` npm package from `@understand-anything/core`, or apply the patterns manually if the file list is small. +This filtering must be deterministic (not LLM-based). Use a Node.js script with the `ignore` npm package from `@understand-anything/core`. **Step 3 -- Language Detection** diff --git a/understand-anything-plugin/packages/core/src/__tests__/ignore-filter.test.ts b/understand-anything-plugin/packages/core/src/__tests__/ignore-filter.test.ts index 28c8c7e..d7f9626 100644 --- a/understand-anything-plugin/packages/core/src/__tests__/ignore-filter.test.ts +++ b/understand-anything-plugin/packages/core/src/__tests__/ignore-filter.test.ts @@ -26,11 +26,14 @@ describe("IgnoreFilter", () => { expect(DEFAULT_IGNORE_PATTERNS).toContain(".git/"); }); - it("contains bin and obj for .NET", () => { - expect(DEFAULT_IGNORE_PATTERNS).toContain("bin/"); + it("contains obj for .NET", () => { expect(DEFAULT_IGNORE_PATTERNS).toContain("obj/"); }); + it("does not contain bin (used by Node/Ruby CLI launchers)", () => { + expect(DEFAULT_IGNORE_PATTERNS).not.toContain("bin/"); + }); + it("contains build output directories", () => { expect(DEFAULT_IGNORE_PATTERNS).toContain("dist/"); expect(DEFAULT_IGNORE_PATTERNS).toContain("build/"); @@ -45,7 +48,6 @@ describe("IgnoreFilter", () => { expect(filter.isIgnored("node_modules/foo/bar.js")).toBe(true); expect(filter.isIgnored("dist/index.js")).toBe(true); expect(filter.isIgnored(".git/config")).toBe(true); - expect(filter.isIgnored("bin/Debug/app.dll")).toBe(true); expect(filter.isIgnored("obj/Release/net8.0/app.dll")).toBe(true); }); diff --git a/understand-anything-plugin/packages/core/src/ignore-filter.ts b/understand-anything-plugin/packages/core/src/ignore-filter.ts index 88a65b9..a56d2e3 100644 --- a/understand-anything-plugin/packages/core/src/ignore-filter.ts +++ b/understand-anything-plugin/packages/core/src/ignore-filter.ts @@ -24,7 +24,6 @@ export const DEFAULT_IGNORE_PATTERNS: string[] = [ ".cache/", ".turbo/", "target/", - "bin/", "obj/", // Lock files From 6ef1a21315be673f0377d0ded67cab98d476c84c Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Sun, 12 Apr 2026 11:39:31 +0800 Subject: [PATCH 14/15] feat: seed .understandignore with .gitignore patterns on first generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Read the project's .gitignore at starter-file generation time and include non-default patterns as commented suggestions in .understandignore. Patterns already covered by hardcoded defaults are deduplicated (with trailing-slash normalization). This is a one-time inclusion — users can later remove patterns for files they want analyzed without the filter re-reading .gitignore. Also fixes the starter header listing bin/ as a built-in default when it is intentionally excluded from DEFAULT_IGNORE_PATTERNS (bin/ is used by Node/Ruby CLI launchers). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/__tests__/ignore-generator.test.ts | 67 ++++++++++++++++++- .../packages/core/src/ignore-generator.ts | 46 ++++++++++++- 2 files changed, 109 insertions(+), 4 deletions(-) diff --git a/understand-anything-plugin/packages/core/src/__tests__/ignore-generator.test.ts b/understand-anything-plugin/packages/core/src/__tests__/ignore-generator.test.ts index 8c2189b..5d47140 100644 --- a/understand-anything-plugin/packages/core/src/__tests__/ignore-generator.test.ts +++ b/understand-anything-plugin/packages/core/src/__tests__/ignore-generator.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { generateStarterIgnoreFile } from "../ignore-generator"; -import { mkdirSync, rmSync } from "node:fs"; +import { mkdirSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; @@ -94,4 +94,69 @@ describe("generateStarterIgnoreFile", () => { expect(content).not.toContain("# .storybook/"); expect(content).not.toContain("# fixtures/"); }); + + describe(".gitignore integration", () => { + it("includes .gitignore patterns not covered by defaults", () => { + writeFileSync(join(testDir, ".gitignore"), ".env\nsecrets/\n*.pyc\n"); + const content = generateStarterIgnoreFile(testDir); + expect(content).toContain("From .gitignore"); + expect(content).toContain("# .env"); + expect(content).toContain("# secrets/"); + expect(content).toContain("# *.pyc"); + }); + + it("excludes .gitignore patterns already in defaults", () => { + writeFileSync(join(testDir, ".gitignore"), "node_modules/\ndist/\n.env\n"); + const content = generateStarterIgnoreFile(testDir); + // .env is not in defaults, should appear + expect(content).toContain("# .env"); + // node_modules/ and dist/ are in defaults, should not appear in .gitignore section + const gitignoreSection = content.split("From .gitignore")[1]?.split("---")[0] ?? ""; + expect(gitignoreSection).not.toContain("node_modules"); + expect(gitignoreSection).not.toContain("dist"); + }); + + it("skips .gitignore comments and blank lines", () => { + writeFileSync(join(testDir, ".gitignore"), "# a comment\n\n.env\n \n"); + const content = generateStarterIgnoreFile(testDir); + expect(content).toContain("# .env"); + // Should not include the original comment as a pattern + const gitignoreSection = content.split("From .gitignore")[1]?.split("---")[0] ?? ""; + expect(gitignoreSection).not.toContain("a comment"); + }); + + it("handles .gitignore with trailing-slash normalization for defaults", () => { + // "dist" without trailing slash should still match "dist/" default + writeFileSync(join(testDir, ".gitignore"), "dist\ncoverage\n.env\n"); + const content = generateStarterIgnoreFile(testDir); + expect(content).toContain("From .gitignore"); + // Extract lines between the .gitignore header and the next section header + const lines = content.split("\n"); + const headerIdx = lines.findIndex((l) => l.includes("From .gitignore")); + const nextSectionIdx = lines.findIndex((l, i) => i > headerIdx && l.startsWith("# ---")); + const sectionLines = lines.slice(headerIdx + 1, nextSectionIdx === -1 ? undefined : nextSectionIdx); + const patterns = sectionLines.filter((l) => l.startsWith("# ") && !l.startsWith("# ---")).map((l) => l.slice(2)); + expect(patterns).toContain(".env"); + expect(patterns).not.toContain("dist"); + expect(patterns).not.toContain("coverage"); + }); + + it("omits .gitignore section when no .gitignore exists", () => { + const content = generateStarterIgnoreFile(testDir); + expect(content).not.toContain("From .gitignore"); + }); + + it("omits .gitignore section when all patterns are covered by defaults", () => { + writeFileSync(join(testDir, ".gitignore"), "node_modules/\ndist/\n*.lock\n"); + const content = generateStarterIgnoreFile(testDir); + expect(content).not.toContain("From .gitignore"); + }); + + it("all .gitignore suggestions are commented out", () => { + writeFileSync(join(testDir, ".gitignore"), ".env\nsecrets/\n*.pyc\n"); + const content = generateStarterIgnoreFile(testDir); + const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#")); + expect(lines).toHaveLength(0); + }); + }); }); diff --git a/understand-anything-plugin/packages/core/src/ignore-generator.ts b/understand-anything-plugin/packages/core/src/ignore-generator.ts index 021e170..f0e49ac 100644 --- a/understand-anything-plugin/packages/core/src/ignore-generator.ts +++ b/understand-anything-plugin/packages/core/src/ignore-generator.ts @@ -1,5 +1,6 @@ -import { existsSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; +import { DEFAULT_IGNORE_PATTERNS } from "./ignore-filter.js"; const HEADER = `# .understandignore — patterns for files/dirs to exclude from analysis # Syntax: same as .gitignore (globs, # comments, ! negation, trailing / for dirs) @@ -7,7 +8,7 @@ const HEADER = `# .understandignore — patterns for files/dirs to exclude from # Use ! prefix to force-include something excluded by defaults. # # Built-in defaults (always excluded unless negated): -# node_modules/, .git/, dist/, build/, bin/, obj/, *.lock, *.min.js, etc. +# node_modules/, .git/, dist/, build/, obj/, *.lock, *.min.js, etc. # `; @@ -30,13 +31,51 @@ const GENERIC_SUGGESTIONS = [ "*.snap", ]; +/** + * Parses a .gitignore file and returns active patterns (no comments, no blanks). + */ +function parseGitignorePatterns(gitignorePath: string): string[] { + if (!existsSync(gitignorePath)) return []; + const content = readFileSync(gitignorePath, "utf-8"); + return content + .split("\n") + .map((line) => line.trim()) + .filter((line) => line.length > 0 && !line.startsWith("#")); +} + +/** + * Returns true if a gitignore pattern is already covered by the hardcoded defaults. + * Normalizes trailing slashes for comparison. + */ +function isCoveredByDefaults(pattern: string): boolean { + const normalize = (p: string) => p.replace(/\/+$/, ""); + const normalized = normalize(pattern); + return DEFAULT_IGNORE_PATTERNS.some((d) => normalize(d) === normalized); +} + /** * Generates a starter .understandignore file content by scanning the project - * for common directories. All suggestions are commented out. + * for common directories and reading .gitignore patterns. + * All suggestions are commented out — this is a one-time generation. */ export function generateStarterIgnoreFile(projectRoot: string): string { const sections: string[] = [HEADER]; + // Section 1: patterns from .gitignore not already in defaults + const gitignorePath = join(projectRoot, ".gitignore"); + const gitignorePatterns = parseGitignorePatterns(gitignorePath).filter( + (p) => !isCoveredByDefaults(p), + ); + + if (gitignorePatterns.length > 0) { + sections.push("# --- From .gitignore (uncomment to exclude) ---\n"); + for (const pattern of gitignorePatterns) { + sections.push(`# ${pattern}`); + } + sections.push(""); + } + + // Section 2: detected directories const detected: string[] = []; for (const { dir, pattern } of DETECTABLE_DIRS) { if (existsSync(join(projectRoot, dir))) { @@ -52,6 +91,7 @@ export function generateStarterIgnoreFile(projectRoot: string): string { sections.push(""); } + // Section 3: generic test patterns sections.push("# --- Test file patterns (uncomment to exclude) ---\n"); for (const pattern of GENERIC_SUGGESTIONS) { sections.push(`# ${pattern}`); From 55fc2ac8488caae5a46d458a042ab27710d8794d Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Sun, 12 Apr 2026 11:47:55 +0800 Subject: [PATCH 15/15] fix: sync Phase 0.5 inline script with .gitignore integration and header fix The skill's inline Node.js generator was duplicated from core and missed the .gitignore seeding and bin/ header fix. Updated the one-liner to read .gitignore patterns, deduplicate against defaults with trailing-slash normalization, and remove bin/ from the built-in defaults header comment. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../skills/understand/SKILL.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/understand-anything-plugin/skills/understand/SKILL.md b/understand-anything-plugin/skills/understand/SKILL.md index 3ad7fcf..efebf1e 100644 --- a/understand-anything-plugin/skills/understand/SKILL.md +++ b/understand-anything-plugin/skills/understand/SKILL.md @@ -83,19 +83,29 @@ Set up and verify the `.understandignore` file before scanning. 1. Check if `$PROJECT_ROOT/.understand-anything/.understandignore` exists. 2. **If it does NOT exist**, generate a starter file: - - Run the following Node.js one-liner in `$PROJECT_ROOT`: + - Run the following Node.js one-liner in `$PROJECT_ROOT` (reads `.gitignore` and deduplicates against built-in defaults): ```bash node -e " const fs = require('fs'); const path = require('path'); const root = process.cwd(); - const dirs = ['__tests__','test','tests','fixtures','testdata','docs','examples','scripts','migrations','.storybook']; - const header = '# .understandignore — patterns for files/dirs to exclude from analysis\n# Syntax: same as .gitignore (globs, # comments, ! negation, trailing / for dirs)\n# Lines below are suggestions — uncomment to activate.\n# Use ! prefix to force-include something excluded by defaults.\n#\n# Built-in defaults (always excluded unless negated):\n# node_modules/, .git/, dist/, build/, bin/, obj/, *.lock, *.min.js, etc.\n#\n'; + const defaults = ['node_modules/','node_modules','.git/','vendor/','venv/','.venv/','__pycache__/','dist/','dist','build/','build','out/','coverage/','coverage','.next/','.cache/','.turbo/','target/','obj/','*.lock','package-lock.json','yarn.lock','pnpm-lock.yaml','*.png','*.jpg','*.jpeg','*.gif','*.svg','*.ico','*.woff','*.woff2','*.ttf','*.eot','*.mp3','*.mp4','*.pdf','*.zip','*.tar','*.gz','*.min.js','*.min.css','*.map','*.generated.*','.idea/','.vscode/','LICENSE','.gitignore','.editorconfig','.prettierrc','.eslintrc*','*.log']; + const norm = p => p.replace(/\/+$/, ''); + const defaultSet = new Set(defaults.map(norm)); + const header = '# .understandignore — patterns for files/dirs to exclude from analysis\n# Syntax: same as .gitignore (globs, # comments, ! negation, trailing / for dirs)\n# Lines below are suggestions — uncomment to activate.\n# Use ! prefix to force-include something excluded by defaults.\n#\n# Built-in defaults (always excluded unless negated):\n# node_modules/, .git/, dist/, build/, obj/, *.lock, *.min.js, etc.\n#\n'; let body = ''; + const gitignorePath = path.join(root, '.gitignore'); + if (fs.existsSync(gitignorePath)) { + const gi = fs.readFileSync(gitignorePath, 'utf-8').split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#')).filter(p => !defaultSet.has(norm(p))); + if (gi.length) { body += '# --- From .gitignore (uncomment to exclude) ---\n\n' + gi.map(p => '# ' + p).join('\n') + '\n\n'; } + } + const dirs = ['__tests__','test','tests','fixtures','testdata','docs','examples','scripts','migrations','.storybook']; const found = dirs.filter(d => fs.existsSync(path.join(root, d))); if (found.length) { body += '# --- Detected directories (uncomment to exclude) ---\n\n' + found.map(d => '# ' + d + '/').join('\n') + '\n\n'; } body += '# --- Test file patterns (uncomment to exclude) ---\n\n# *.test.*\n# *.spec.*\n# *.snap\n'; - fs.writeFileSync(path.join(root, '.understand-anything', '.understandignore'), header + body); + const outDir = path.join(root, '.understand-anything'); + if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true }); + fs.writeFileSync(path.join(outDir, '.understandignore'), header + body); " ``` - Report to the user: