feat(skill): scaffold skill package with /understand-chat command

Add the @understand-anything/skill package with context-builder (search +
1-hop expansion + layer resolution), chat prompt builder, skill definition
for Claude Code, and 14 passing tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lum1104
2026-03-14 19:00:54 +08:00
Unverified
parent 839166b229
commit b6ab0f8b32
8 changed files with 587 additions and 0 deletions
@@ -0,0 +1,18 @@
---
name: understand-chat
description: Ask questions about the current codebase using the knowledge graph
arguments: query
---
# /understand-chat
Answer questions about this codebase using the knowledge graph at `.understand-anything/knowledge-graph.json`.
## Instructions
1. Read the knowledge graph file at `.understand-anything/knowledge-graph.json` in the current project root
2. If the file doesn't exist, tell the user to run `/understand` first to analyze the project
3. Use the knowledge graph context to answer the user's query: "${ARGUMENTS}"
4. Reference specific files, functions, and relationships from the graph
5. If the project has layers defined, explain which layer(s) are relevant
6. Be concise but thorough -- link concepts to actual code locations
+19
View File
@@ -0,0 +1,19 @@
{
"name": "@understand-anything/skill",
"version": "0.1.0",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"test": "vitest run"
},
"dependencies": {
"@understand-anything/core": "workspace:*"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.7.0",
"vitest": "^3.1.0"
}
}
@@ -0,0 +1,234 @@
import { describe, it, expect } from "vitest";
import { buildChatContext, formatContextForPrompt } from "../context-builder.js";
import type { KnowledgeGraph, GraphNode, GraphEdge, Layer } from "@understand-anything/core";
const makeNode = (
overrides: Partial<GraphNode> & { id: string; name: string },
): GraphNode => ({
type: "file",
summary: "",
tags: [],
complexity: "simple",
...overrides,
});
const sampleNodes: GraphNode[] = [
makeNode({
id: "auth-ctrl",
name: "AuthenticationController",
type: "class",
filePath: "src/controllers/auth.ts",
summary: "Handles user login, logout, and session management",
tags: ["auth", "controller", "security"],
complexity: "complex",
languageNotes: "Uses Express middleware pattern",
}),
makeNode({
id: "db-pool",
name: "DatabasePool",
type: "class",
filePath: "src/db/pool.ts",
summary: "Manages PostgreSQL connection pooling",
tags: ["database", "connection"],
complexity: "moderate",
}),
makeNode({
id: "user-model",
name: "UserModel",
type: "class",
filePath: "src/models/user.ts",
summary: "ORM model for the users table",
tags: ["model", "database", "user"],
complexity: "moderate",
}),
makeNode({
id: "auth-middleware",
name: "authMiddleware",
type: "function",
filePath: "src/middleware/auth.ts",
summary: "Express middleware that validates JWT tokens for authentication",
tags: ["auth", "middleware", "security"],
complexity: "simple",
}),
makeNode({
id: "config",
name: "config.ts",
type: "file",
filePath: "src/config.ts",
summary: "Application configuration and environment variables",
tags: ["config", "env"],
complexity: "simple",
}),
];
const sampleEdges: GraphEdge[] = [
{
source: "auth-ctrl",
target: "user-model",
type: "depends_on",
direction: "forward",
description: "AuthenticationController uses UserModel for user lookup",
weight: 0.9,
},
{
source: "auth-ctrl",
target: "auth-middleware",
type: "calls",
direction: "forward",
description: "Controller registers auth middleware",
weight: 0.7,
},
{
source: "user-model",
target: "db-pool",
type: "depends_on",
direction: "forward",
description: "UserModel uses DatabasePool for queries",
weight: 0.8,
},
];
const sampleLayers: Layer[] = [
{
id: "layer-api",
name: "API Layer",
description: "HTTP controllers and middleware",
nodeIds: ["auth-ctrl", "auth-middleware"],
},
{
id: "layer-data",
name: "Data Layer",
description: "Database models and connections",
nodeIds: ["user-model", "db-pool"],
},
];
const sampleGraph: KnowledgeGraph = {
version: "1.0.0",
project: {
name: "test-project",
languages: ["TypeScript"],
frameworks: ["Express"],
description: "A test project for unit tests",
analyzedAt: "2026-03-14T00:00:00Z",
gitCommitHash: "abc123",
},
nodes: sampleNodes,
edges: sampleEdges,
layers: sampleLayers,
tour: [],
};
describe("buildChatContext", () => {
it("finds relevant nodes for a query", () => {
const ctx = buildChatContext(sampleGraph, "authentication");
expect(ctx.relevantNodes.length).toBeGreaterThan(0);
const nodeNames = ctx.relevantNodes.map((n) => n.name);
expect(nodeNames).toContain("AuthenticationController");
});
it("includes connected nodes via 1-hop expansion", () => {
// Searching for "authentication" should find auth-ctrl directly.
// auth-ctrl connects to user-model and auth-middleware via edges,
// so those should also appear in relevantNodes.
const ctx = buildChatContext(sampleGraph, "authentication");
const nodeIds = ctx.relevantNodes.map((n) => n.id);
// auth-ctrl is a direct match
expect(nodeIds).toContain("auth-ctrl");
// user-model and auth-middleware are 1-hop connected
expect(nodeIds).toContain("user-model");
expect(nodeIds).toContain("auth-middleware");
});
it("includes project metadata", () => {
const ctx = buildChatContext(sampleGraph, "database");
expect(ctx.projectName).toBe("test-project");
expect(ctx.projectDescription).toBe("A test project for unit tests");
expect(ctx.languages).toEqual(["TypeScript"]);
expect(ctx.frameworks).toEqual(["Express"]);
});
it("includes relevant layers containing matched nodes", () => {
const ctx = buildChatContext(sampleGraph, "authentication");
const layerNames = ctx.relevantLayers.map((l) => l.name);
// auth-ctrl is in API Layer
expect(layerNames).toContain("API Layer");
});
it("includes relevant edges between relevant nodes", () => {
const ctx = buildChatContext(sampleGraph, "authentication");
expect(ctx.relevantEdges.length).toBeGreaterThan(0);
// Should include the edge from auth-ctrl to user-model
const hasAuthToUser = ctx.relevantEdges.some(
(e) => e.source === "auth-ctrl" && e.target === "user-model",
);
expect(hasAuthToUser).toBe(true);
});
it("stores the original query", () => {
const ctx = buildChatContext(sampleGraph, "database pool");
expect(ctx.query).toBe("database pool");
});
it("respects maxNodes parameter", () => {
const ctx = buildChatContext(sampleGraph, "auth", 1);
// With maxNodes=1, only 1 search result (before expansion)
// Expansion may add connected nodes, but initial search is limited
expect(ctx.relevantNodes.length).toBeGreaterThanOrEqual(1);
// Should still be bounded reasonably
expect(ctx.relevantNodes.length).toBeLessThanOrEqual(sampleNodes.length);
});
it("returns empty relevantNodes for a query with no matches", () => {
const ctx = buildChatContext(sampleGraph, "xyznonexistent");
expect(ctx.relevantNodes.length).toBe(0);
expect(ctx.relevantEdges.length).toBe(0);
expect(ctx.relevantLayers.length).toBe(0);
});
});
describe("formatContextForPrompt", () => {
it("produces a string containing node names and summaries", () => {
const ctx = buildChatContext(sampleGraph, "authentication");
const formatted = formatContextForPrompt(ctx);
expect(formatted).toContain("AuthenticationController");
expect(formatted).toContain("Handles user login, logout, and session management");
});
it("includes project header information", () => {
const ctx = buildChatContext(sampleGraph, "authentication");
const formatted = formatContextForPrompt(ctx);
expect(formatted).toContain("test-project");
expect(formatted).toContain("TypeScript");
expect(formatted).toContain("Express");
});
it("includes edge/relationship descriptions", () => {
const ctx = buildChatContext(sampleGraph, "authentication");
const formatted = formatContextForPrompt(ctx);
// Should reference the relationship between auth-ctrl and user-model
expect(formatted).toContain("AuthenticationController");
expect(formatted).toContain("UserModel");
// Edge type or description should appear
expect(formatted).toContain("depends_on");
});
it("includes layer information when layers are present", () => {
const ctx = buildChatContext(sampleGraph, "authentication");
const formatted = formatContextForPrompt(ctx);
expect(formatted).toContain("API Layer");
});
it("includes file paths for nodes that have them", () => {
const ctx = buildChatContext(sampleGraph, "authentication");
const formatted = formatContextForPrompt(ctx);
expect(formatted).toContain("src/controllers/auth.ts");
});
it("includes complexity and type information", () => {
const ctx = buildChatContext(sampleGraph, "authentication");
const formatted = formatContextForPrompt(ctx);
expect(formatted).toContain("complex");
expect(formatted).toContain("class");
});
});
+147
View File
@@ -0,0 +1,147 @@
import { SearchEngine } from "@understand-anything/core";
import type {
KnowledgeGraph,
GraphNode,
GraphEdge,
Layer,
} from "@understand-anything/core";
export interface ChatContext {
projectName: string;
projectDescription: string;
languages: string[];
frameworks: string[];
relevantNodes: GraphNode[];
relevantEdges: GraphEdge[];
relevantLayers: Layer[];
query: string;
}
/**
* Build a ChatContext by searching the knowledge graph for nodes relevant
* to the user's query, expanding 1 hop via edges, and collecting the
* associated layers.
*/
export function buildChatContext(
graph: KnowledgeGraph,
query: string,
maxNodes?: number,
): ChatContext {
const limit = maxNodes ?? 15;
// 1. Use SearchEngine to find relevant nodes
const engine = new SearchEngine(graph.nodes);
const searchResults = engine.search(query, { limit });
// Build a set of matched node IDs
const matchedIds = new Set(searchResults.map((r) => r.nodeId));
// 2. Expand to connected nodes (1 hop via edges)
const expandedIds = new Set(matchedIds);
for (const edge of graph.edges) {
if (matchedIds.has(edge.source)) {
expandedIds.add(edge.target);
}
if (matchedIds.has(edge.target)) {
expandedIds.add(edge.source);
}
}
// Collect the actual node objects
const nodeMap = new Map(graph.nodes.map((n) => [n.id, n]));
const relevantNodes: GraphNode[] = [];
for (const id of expandedIds) {
const node = nodeMap.get(id);
if (node) {
relevantNodes.push(node);
}
}
// 3. Collect edges where both endpoints are in the relevant set
const relevantEdges = graph.edges.filter(
(e) => expandedIds.has(e.source) && expandedIds.has(e.target),
);
// 4. Find layers containing any relevant node
const relevantLayers = graph.layers.filter((layer) =>
layer.nodeIds.some((id) => expandedIds.has(id)),
);
return {
projectName: graph.project.name,
projectDescription: graph.project.description,
languages: graph.project.languages,
frameworks: graph.project.frameworks,
relevantNodes,
relevantEdges,
relevantLayers,
query,
};
}
/**
* Format the ChatContext as a readable markdown string for LLM consumption.
*/
export function formatContextForPrompt(context: ChatContext): string {
const lines: string[] = [];
// Project header
lines.push(`# Project: ${context.projectName}`);
lines.push("");
lines.push(context.projectDescription);
lines.push("");
lines.push(`**Languages:** ${context.languages.join(", ")}`);
lines.push(`**Frameworks:** ${context.frameworks.join(", ")}`);
lines.push("");
// Layers section
if (context.relevantLayers.length > 0) {
lines.push("## Relevant Layers");
lines.push("");
for (const layer of context.relevantLayers) {
lines.push(`### ${layer.name}`);
lines.push(layer.description);
lines.push("");
}
}
// Nodes section
if (context.relevantNodes.length > 0) {
lines.push("## Code Components");
lines.push("");
for (const node of context.relevantNodes) {
lines.push(`### ${node.name} (${node.type})`);
if (node.filePath) {
lines.push(`- **File:** ${node.filePath}`);
}
lines.push(`- **Complexity:** ${node.complexity}`);
lines.push(`- **Summary:** ${node.summary}`);
if (node.tags.length > 0) {
lines.push(`- **Tags:** ${node.tags.join(", ")}`);
}
if (node.languageNotes) {
lines.push(`- **Language Notes:** ${node.languageNotes}`);
}
lines.push("");
}
}
// Edges/relationships section
if (context.relevantEdges.length > 0) {
const nodeMap = new Map(context.relevantNodes.map((n) => [n.id, n]));
lines.push("## Relationships");
lines.push("");
for (const edge of context.relevantEdges) {
const sourceName = nodeMap.get(edge.source)?.name ?? edge.source;
const targetName = nodeMap.get(edge.target)?.name ?? edge.target;
let line = `- ${sourceName} --[${edge.type}]--> ${targetName}`;
if (edge.description) {
line += `: ${edge.description}`;
}
lines.push(line);
}
lines.push("");
}
return lines.join("\n");
}
+6
View File
@@ -0,0 +1,6 @@
export {
buildChatContext,
formatContextForPrompt,
type ChatContext,
} from "./context-builder.js";
export { buildChatPrompt } from "./understand-chat.js";
+29
View File
@@ -0,0 +1,29 @@
import type { KnowledgeGraph } from "@understand-anything/core";
import { buildChatContext, formatContextForPrompt } from "./context-builder.js";
/**
* Build a complete chat prompt by combining knowledge graph context
* with a system instruction for answering codebase questions.
*/
export function buildChatPrompt(
graph: KnowledgeGraph,
query: string,
): string {
const context = buildChatContext(graph, query);
const formattedContext = formatContextForPrompt(context);
return [
"You are a knowledgeable assistant that answers questions about a software codebase.",
"Use the following knowledge graph context to inform your answer.",
"Reference specific files, functions, classes, and relationships from the graph.",
"If layers are present, explain which architectural layer(s) are relevant.",
"Be concise but thorough — link concepts to actual code locations.",
"",
"---",
"",
formattedContext,
"---",
"",
`**User question:** ${query}`,
].join("\n");
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}
+126
View File
@@ -89,6 +89,22 @@ importers:
specifier: ^6.0.0
version: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)
packages/skill:
dependencies:
'@understand-anything/core':
specifier: workspace:*
version: link:../core
devDependencies:
'@types/node':
specifier: ^22.0.0
version: 22.19.15
typescript:
specifier: ^5.7.0
version: 5.9.3
vitest:
specifier: ^3.1.0
version: 3.2.4(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)
packages:
'@babel/code-frame@7.29.0':
@@ -775,6 +791,9 @@ packages:
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/node@22.19.15':
resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==}
'@types/node@25.5.0':
resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==}
@@ -1231,6 +1250,9 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
undici-types@7.18.2:
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
@@ -1907,6 +1929,10 @@ snapshots:
'@types/estree@1.0.8': {}
'@types/node@22.19.15':
dependencies:
undici-types: 6.21.0
'@types/node@25.5.0':
dependencies:
undici-types: 7.18.2
@@ -1942,6 +1968,14 @@ snapshots:
chai: 5.3.3
tinyrainbow: 2.0.0
'@vitest/mocker@3.2.4(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1))':
dependencies:
'@vitest/spy': 3.2.4
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)
'@vitest/mocker@3.2.4(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1))':
dependencies:
'@vitest/spy': 3.2.4
@@ -2362,6 +2396,8 @@ snapshots:
typescript@5.9.3: {}
undici-types@6.21.0: {}
undici-types@7.18.2: {}
update-browserslist-db@1.2.3(browserslist@4.28.1):
@@ -2374,6 +2410,27 @@ snapshots:
dependencies:
react: 19.2.4
vite-node@3.2.4(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1):
dependencies:
cac: 6.7.14
debug: 4.4.3
es-module-lexer: 1.7.0
pathe: 2.0.3
vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)
transitivePeerDependencies:
- '@types/node'
- jiti
- less
- lightningcss
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
- tsx
- yaml
vite-node@3.2.4(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1):
dependencies:
cac: 6.7.14
@@ -2395,6 +2452,20 @@ snapshots:
- tsx
- yaml
vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1):
dependencies:
esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
postcss: 8.5.8
rollup: 4.59.0
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 22.19.15
fsevents: 2.3.3
jiti: 2.6.1
lightningcss: 1.31.1
vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1):
dependencies:
esbuild: 0.25.12
@@ -2409,6 +2480,20 @@ snapshots:
jiti: 2.6.1
lightningcss: 1.31.1
vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1):
dependencies:
esbuild: 0.27.4
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
postcss: 8.5.8
rollup: 4.59.0
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 22.19.15
fsevents: 2.3.3
jiti: 2.6.1
lightningcss: 1.31.1
vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1):
dependencies:
esbuild: 0.27.4
@@ -2423,6 +2508,47 @@ snapshots:
jiti: 2.6.1
lightningcss: 1.31.1
vitest@3.2.4(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1):
dependencies:
'@types/chai': 5.2.3
'@vitest/expect': 3.2.4
'@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1))
'@vitest/pretty-format': 3.2.4
'@vitest/runner': 3.2.4
'@vitest/snapshot': 3.2.4
'@vitest/spy': 3.2.4
'@vitest/utils': 3.2.4
chai: 5.3.3
debug: 4.4.3
expect-type: 1.3.0
magic-string: 0.30.21
pathe: 2.0.3
picomatch: 4.0.3
std-env: 3.10.0
tinybench: 2.9.0
tinyexec: 0.3.2
tinyglobby: 0.2.15
tinypool: 1.1.1
tinyrainbow: 2.0.0
vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)
vite-node: 3.2.4(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 22.19.15
transitivePeerDependencies:
- jiti
- less
- lightningcss
- msw
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
- tsx
- yaml
vitest@3.2.4(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1):
dependencies:
'@types/chai': 5.2.3