diff --git a/.git-ai/lancedb.tar.gz b/.git-ai/lancedb.tar.gz index 40f8fef..d8f647f 100644 --- a/.git-ai/lancedb.tar.gz +++ b/.git-ai/lancedb.tar.gz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:602322a04f5a62790ca9009b06b1a426dc1043c886b4b6549b3b8e1e6b4b74d2 -size 84364 +oid sha256:96a602309d85c1540e6113ecd0a728ce2122360a6519c43d67e0bdcac1dff935 +size 177893 diff --git a/README.md b/README.md index dbaa236..b3e99f4 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,8 @@ Supports multiple mainstream programming languages: | Go | `.go` | | Rust | `.rs` | | C | `.c`, `.h` | +| Markdown | `.md`, `.mdx` | +| YAML | `.yml`, `.yaml` | --- @@ -168,23 +170,22 @@ Claude will automatically invoke git-ai tools to provide deep analysis. *Enablin ```mermaid graph TB - A[Git Repository] -->|On Commit| B["DSR (Deterministic Semantic Record)"] - B --> C[".git-ai/dsr/.json
Semantic Snapshot"] - C -->|Index Rebuild| D[LanceDB Vector DB] - C -->|Index Rebuild| E[CozoDB Graph DB] + A[Git Repository] -->|On Commit| B[DSR\nDeterministic Semantic Record] + B --> C[.git-ai/dsr/commit.json\nSemantic Snapshot] + C -->|Index Rebuild| D[LanceDB\nVector Database] + C -->|Index Rebuild| E[CozoDB\nGraph Database] D --> F[MCP Server] E --> F - F -->|Tool Call| G["AI Agent
Claude Desktop / Trae"] + F -->|Tool Call| G[AI Agent\nClaude Desktop / Trae] F -->|CLI| H[Developer] - C -->|Cross-Version| I{"Semantic Timeline
Traceable, Comparable, Evolvable"} - - style B fill:#e1f5ff - style C fill:#e8f5e9 - style D fill:#fff4e1 - style E fill:#fff4e1 - style F fill:#e8f5e9 - style G fill:#f3e5f5 - style I fill:#fce4ec + C -->|Cross-Version| I[Semantic Timeline\nTraceable · Comparable · Evolvable] + style B fill:#e1f5ff,stroke:#333 + style C fill:#e8f5e9,stroke:#333 + style D fill:#fff4e1,stroke:#333 + style E fill:#fff4e1,stroke:#333 + style F fill:#e8f5e9,stroke:#333 + style G fill:#f3e5f5,stroke:#333 + style I fill:#fce4ec,stroke:#333 ``` **Core Components**: @@ -296,6 +297,8 @@ We provide carefully designed Agent templates to help AI use git-ai better: - [Skill Template](./templates/agents/common/skills/git-ai-mcp/SKILL.md): Guides Agents on how to use tools - [Rule Template](./templates/agents/common/rules/git-ai-mcp/RULE.md): Constrains Agent behavior +Skills/Rules docs (Markdown/YAML) are indexed as part of semantic search, so agents can retrieve MCP guidance via `semantic_search`. + One-click install to your project: ```bash diff --git a/README.zh-CN.md b/README.zh-CN.md index d6dc99c..3179dff 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -80,6 +80,8 @@ git-ai ai dsr context | Go | `.go` | | Rust | `.rs` | | C | `.c`, `.h` | +| Markdown | `.md`, `.mdx` | +| YAML | `.yml`, `.yaml` | --- @@ -296,6 +298,8 @@ Claude 会自动调用 git-ai 的工具,为你提供深入的分析。 - [Skill 模版](./templates/agents/common/skills/git-ai-mcp/SKILL.md):指导 Agent 如何使用工具 - [Rule 模版](./templates/agents/common/rules/git-ai-mcp/RULE.md):约束 Agent 的行为 +Skills/Rules 文档(Markdown/YAML)会被纳入语义索引,便于通过 `semantic_search` 检索 MCP 指南。 + 一键安装到你的项目: ```bash diff --git a/docs/zh-CN/mcp.md b/docs/zh-CN/mcp.md index 30ab23e..30237e3 100644 --- a/docs/zh-CN/mcp.md +++ b/docs/zh-CN/mcp.md @@ -24,8 +24,8 @@ git-ai ai serve - `unpack_index({ path })`:解包索引归档 ### 检索 -- `search_symbols({ path, query, mode?, case_insensitive?, max_candidates?, limit?, lang?, with_repo_map?, repo_map_max_files?, repo_map_max_symbols?, wiki_dir? })`:符号检索(lang: auto/all/java/ts;可选附带 repo_map) -- `semantic_search({ path, query, topk?, lang?, with_repo_map?, repo_map_max_files?, repo_map_max_symbols?, wiki_dir? })`:基于 LanceDB + SQ8 的语义检索(lang: auto/all/java/ts;可选附带 repo_map) +- `search_symbols({ path, query, mode?, case_insensitive?, max_candidates?, limit?, lang?, with_repo_map?, repo_map_max_files?, repo_map_max_symbols?, wiki_dir? })`:符号检索(lang: auto/all/java/ts/python/go/rust/c/markdown/yaml;可选附带 repo_map) +- `semantic_search({ path, query, topk?, lang?, with_repo_map?, repo_map_max_files?, repo_map_max_symbols?, wiki_dir? })`:基于 LanceDB + SQ8 的语义检索(lang: auto/all/java/ts/python/go/rust/c/markdown/yaml;可选附带 repo_map) - `repo_map({ path, max_files?, max_symbols?, wiki_dir? })`:生成 repo map(重要文件/符号排名、引导 Wiki 阅读) - `ast_graph_find({ path, prefix, limit?, lang? })`:按名字前缀查找符号定义(大小写不敏感;lang: auto/all/java/ts) - `ast_graph_children({ path, id, as_file? })`:列出包含关系的直接子节点(文件→顶层符号、类→方法等) @@ -122,6 +122,8 @@ semantic_search({ path: "/ABS/PATH/TO/REPO", query: "where is auth handled", top 本仓库提供了 Agent 可直接复用的 Skill/Rule 模板,旨在让 Agent 能够遵循最佳实践来使用上述工具。 +这些模板文档(Markdown/YAML)会被索引,便于 Agent 通过 `semantic_search` 检索 MCP 规则与技能说明。 + ### YAML 格式模板 - **Skill**: [`templates/agents/common/skills/git-ai/skill.yaml`](../../templates/agents/common/skills/git-ai/skill.yaml) - 指导 Agent 如何使用 git-ai 的 Git-native 语义体系(包含 DSR 约束)与 MCP 工具 @@ -174,6 +176,9 @@ git-ai ai agent install --agent trae - **use_dsr_for_history**: 追溯历史必须使用 DSR 工具 - **respect_dsr_risk**: 重视 DSR 报告的 high 风险变更 +文档/规则检索建议: +- 当问题涉及 MCP/Skill/Rule 时,先用 `semantic_search` 定位对应文档片段,再给出结论。 + 禁止事项包括: - 假设符号位置而不搜索 - 直接修改未读过的文件 diff --git a/package-lock.json b/package-lock.json index cf210ac..0469085 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "git-ai", - "version": "2.0.0", + "version": "2.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "git-ai", - "version": "2.0.0", + "version": "2.1.0", "license": "MIT", "dependencies": { "@lancedb/lancedb": "0.22.3", diff --git a/package.json b/package.json index c09d4b4..40c40d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "git-ai", - "version": "2.0.0", + "version": "2.1.0", "main": "dist/index.js", "bin": { "git-ai": "dist/bin/git-ai.js" diff --git a/src/commands/graph.ts b/src/commands/graph.ts index ea26d42..fbecb83 100644 --- a/src/commands/graph.ts +++ b/src/commands/graph.ts @@ -31,7 +31,7 @@ export const graphCommand = new Command('graph') .description('Find symbols by name prefix') .argument('', 'Name prefix (case-insensitive)') .option('-p, --path ', 'Path inside the repository', '.') - .option('--lang ', 'Language: auto|all|java|ts|python|go|rust|c', 'auto') + .option('--lang ', 'Language: auto|all|java|ts|python|go|rust|c|markdown|yaml', 'auto') .action(async (prefix, options) => { const log = createLogger({ component: 'cli', cmd: 'ai graph find' }); const startedAt = Date.now(); @@ -77,7 +77,7 @@ export const graphCommand = new Command('graph') .argument('', 'Symbol name') .option('-p, --path ', 'Path inside the repository', '.') .option('--limit ', 'Limit results', '200') - .option('--lang ', 'Language: auto|all|java|ts|python|go|rust|c', 'auto') + .option('--lang ', 'Language: auto|all|java|ts|python|go|rust|c|markdown|yaml', 'auto') .action(async (name, options) => { const log = createLogger({ component: 'cli', cmd: 'ai graph refs' }); const startedAt = Date.now(); @@ -108,7 +108,7 @@ export const graphCommand = new Command('graph') .argument('', 'Callee name') .option('-p, --path ', 'Path inside the repository', '.') .option('--limit ', 'Limit results', '200') - .option('--lang ', 'Language: auto|all|java|ts|python|go|rust|c', 'auto') + .option('--lang ', 'Language: auto|all|java|ts|python|go|rust|c|markdown|yaml', 'auto') .action(async (name, options) => { const log = createLogger({ component: 'cli', cmd: 'ai graph callers' }); const startedAt = Date.now(); @@ -139,7 +139,7 @@ export const graphCommand = new Command('graph') .argument('', 'Caller name') .option('-p, --path ', 'Path inside the repository', '.') .option('--limit ', 'Limit results', '200') - .option('--lang ', 'Language: auto|all|java|ts|python|go|rust|c', 'auto') + .option('--lang ', 'Language: auto|all|java|ts|python|go|rust|c|markdown|yaml', 'auto') .action(async (name, options) => { const log = createLogger({ component: 'cli', cmd: 'ai graph callees' }); const startedAt = Date.now(); @@ -173,7 +173,7 @@ export const graphCommand = new Command('graph') .option('--depth ', 'Max depth', '3') .option('--limit ', 'Limit results', '500') .option('--min-name-len ', 'Filter out edges with very short names (default: 1)', '1') - .option('--lang ', 'Language: auto|all|java|ts|python|go|rust|c', 'auto') + .option('--lang ', 'Language: auto|all|java|ts|python|go|rust|c|markdown|yaml', 'auto') .action(async (name, options) => { const log = createLogger({ component: 'cli', cmd: 'ai graph chain' }); const startedAt = Date.now(); diff --git a/src/commands/query.ts b/src/commands/query.ts index b6f7047..f311f0f 100644 --- a/src/commands/query.ts +++ b/src/commands/query.ts @@ -17,7 +17,7 @@ export const queryCommand = new Command('query') .option('--mode ', 'Mode: substring|prefix|wildcard|regex|fuzzy (default: auto)') .option('--case-insensitive', 'Case-insensitive matching', false) .option('--max-candidates ', 'Max candidates to fetch before filtering', '1000') - .option('--lang ', 'Language: auto|all|java|ts|python|go|rust|c', 'auto') + .option('--lang ', 'Language: auto|all|java|ts|python|go|rust|c|markdown|yaml', 'auto') .option('--with-repo-map', 'Attach a lightweight repo map (ranked files + top symbols + wiki links)', false) .option('--repo-map-files ', 'Max repo map files', '20') .option('--repo-map-symbols ', 'Max repo map symbols per file', '5') @@ -110,6 +110,8 @@ function resolveWikiDir(repoRoot: string, wikiOpt: string): string { function inferLangFromFile(file: string): IndexLang { const f = String(file); + if (f.endsWith('.md') || f.endsWith('.mdx')) return 'markdown'; + if (f.endsWith('.yml') || f.endsWith('.yaml')) return 'yaml'; if (f.endsWith('.java')) return 'java'; if (f.endsWith('.c') || f.endsWith('.h')) return 'c'; if (f.endsWith('.go')) return 'go'; diff --git a/src/commands/semantic.ts b/src/commands/semantic.ts index e79cc79..2b8bff8 100644 --- a/src/commands/semantic.ts +++ b/src/commands/semantic.ts @@ -13,7 +13,7 @@ export const semanticCommand = new Command('semantic') .argument('', 'Query text') .option('-p, --path ', 'Path inside the repository', '.') .option('-k, --topk ', 'Top K results', '10') - .option('--lang ', 'Language: auto|all|java|ts|python|go|rust|c', 'auto') + .option('--lang ', 'Language: auto|all|java|ts|python|go|rust|c|markdown|yaml', 'auto') .option('--with-repo-map', 'Attach a lightweight repo map (ranked files + top symbols + wiki links)', false) .option('--repo-map-files ', 'Max repo map files', '20') .option('--repo-map-symbols ', 'Max repo map symbols per file', '5') diff --git a/src/core/dsr/snapshotParser.ts b/src/core/dsr/snapshotParser.ts index 982c281..97bb4dd 100644 --- a/src/core/dsr/snapshotParser.ts +++ b/src/core/dsr/snapshotParser.ts @@ -7,6 +7,8 @@ import { CAdapter } from '../parser/c'; import { GoAdapter } from '../parser/go'; import { PythonAdapter } from '../parser/python'; import { RustAdapter } from '../parser/rust'; +import { parseMarkdown } from '../parser/markdown'; +import { parseYaml } from '../parser/yaml'; export interface ParsedSymbolSnapshot { symbol: SymbolInfo; @@ -31,6 +33,8 @@ export class SnapshotCodeParser { } parseContent(filePath: string, content: string): ParseResult { + if (isMarkdownFile(filePath)) return parseMarkdown(content, filePath); + if (isYamlFile(filePath)) return parseYaml(content, filePath); const adapter = this.pickAdapter(filePath); if (!adapter) return { symbols: [], refs: [] }; try { @@ -59,3 +63,11 @@ export class SnapshotCodeParser { return null; } } + +function isMarkdownFile(filePath: string): boolean { + return filePath.endsWith('.md') || filePath.endsWith('.mdx'); +} + +function isYamlFile(filePath: string): boolean { + return filePath.endsWith('.yml') || filePath.endsWith('.yaml'); +} diff --git a/src/core/indexCheck.ts b/src/core/indexCheck.ts index c915ca3..482639d 100644 --- a/src/core/indexCheck.ts +++ b/src/core/indexCheck.ts @@ -44,7 +44,7 @@ export function resolveLangs(meta: IndexMetaV21 | null, selector: LangSelector): const available = filtered.length > 0 ? filtered : (['java', 'ts'] as IndexLang[]); if (selector === 'all') return available; if (selector !== 'auto') return available.includes(selector) ? [selector] : []; - const preferred: IndexLang[] = ['java', 'ts', 'python', 'go', 'rust', 'c']; + const preferred: IndexLang[] = ['java', 'ts', 'python', 'go', 'rust', 'c', 'markdown', 'yaml']; for (const lang of preferred) { if (available.includes(lang)) return [lang]; } diff --git a/src/core/indexer.ts b/src/core/indexer.ts index d8296ae..fb079bd 100644 --- a/src/core/indexer.ts +++ b/src/core/indexer.ts @@ -41,6 +41,8 @@ function buildChunkText(file: string, symbol: { name: string; kind: string; sign } function inferIndexLang(file: string): IndexLang { + if (file.endsWith('.md') || file.endsWith('.mdx')) return 'markdown'; + if (file.endsWith('.yml') || file.endsWith('.yaml')) return 'yaml'; if (file.endsWith('.java')) return 'java'; if (file.endsWith('.c') || file.endsWith('.h')) return 'c'; if (file.endsWith('.go')) return 'go'; @@ -73,7 +75,7 @@ export class IndexerV2 { const aiIgnore = await loadIgnorePatterns(this.repoRoot, '.aiignore'); const gitIgnore = await loadIgnorePatterns(this.repoRoot, '.gitignore'); - const files = await glob('**/*.{ts,tsx,js,jsx,java,c,h,go,py,rs}', { + const files = await glob('**/*.{ts,tsx,js,jsx,java,c,h,go,py,rs,md,mdx,yml,yaml}', { cwd: this.scanRoot, nodir: true, ignore: [ diff --git a/src/core/indexerIncremental.ts b/src/core/indexerIncremental.ts index e1fd29c..a156757 100644 --- a/src/core/indexerIncremental.ts +++ b/src/core/indexerIncremental.ts @@ -25,6 +25,8 @@ function buildChunkText(file: string, symbol: { name: string; kind: string; sign } function inferIndexLang(file: string): IndexLang { + if (file.endsWith('.md') || file.endsWith('.mdx')) return 'markdown'; + if (file.endsWith('.yml') || file.endsWith('.yaml')) return 'yaml'; if (file.endsWith('.java')) return 'java'; if (file.endsWith('.c') || file.endsWith('.h')) return 'c'; if (file.endsWith('.go')) return 'go'; @@ -34,7 +36,7 @@ function inferIndexLang(file: string): IndexLang { } function isIndexableFile(file: string): boolean { - return /\.(ts|tsx|js|jsx|java|c|h|go|py|rs)$/i.test(file); + return /\.(ts|tsx|js|jsx|java|c|h|go|py|rs|md|mdx|yml|yaml)$/i.test(file); } function escapeQuotes(s: string): string { diff --git a/src/core/lancedb.ts b/src/core/lancedb.ts index 75cef46..bda24da 100644 --- a/src/core/lancedb.ts +++ b/src/core/lancedb.ts @@ -3,9 +3,9 @@ import { Field, Float32, Int32, Schema, Utf8 } from 'apache-arrow'; import fs from 'fs-extra'; import path from 'path'; -export type IndexLang = 'java' | 'ts' | 'c' | 'go' | 'python' | 'rust'; +export type IndexLang = 'java' | 'ts' | 'c' | 'go' | 'python' | 'rust' | 'markdown' | 'yaml'; -export const ALL_INDEX_LANGS: IndexLang[] = ['java', 'ts', 'c', 'go', 'python', 'rust']; +export const ALL_INDEX_LANGS: IndexLang[] = ['java', 'ts', 'c', 'go', 'python', 'rust', 'markdown', 'yaml']; export interface LanceTables { db: lancedb.Connection; diff --git a/src/core/parser.ts b/src/core/parser.ts index 556e9db..68feabd 100644 --- a/src/core/parser.ts +++ b/src/core/parser.ts @@ -8,6 +8,8 @@ import { CAdapter } from './parser/c'; import { GoAdapter } from './parser/go'; import { PythonAdapter } from './parser/python'; import { RustAdapter } from './parser/rust'; +import { parseMarkdown } from './parser/markdown'; +import { parseYaml } from './parser/yaml'; export class CodeParser { private parser: Parser; @@ -28,6 +30,8 @@ export class CodeParser { async parseFile(filePath: string): Promise { const content = await fs.readFile(filePath, 'utf-8'); + if (isMarkdownFile(filePath)) return parseMarkdown(content, filePath); + if (isYamlFile(filePath)) return parseYaml(content, filePath); const adapter = this.pickAdapter(filePath); if (!adapter) return { symbols: [], refs: [] }; @@ -58,3 +62,11 @@ export class CodeParser { return null; } } + +function isMarkdownFile(filePath: string): boolean { + return filePath.endsWith('.md') || filePath.endsWith('.mdx'); +} + +function isYamlFile(filePath: string): boolean { + return filePath.endsWith('.yml') || filePath.endsWith('.yaml'); +} diff --git a/src/core/parser/markdown.ts b/src/core/parser/markdown.ts new file mode 100644 index 0000000..e897214 --- /dev/null +++ b/src/core/parser/markdown.ts @@ -0,0 +1,87 @@ +import { ParseResult, SymbolInfo } from '../types'; + +const headerPattern = /^(#{1,6})\s+(.+?)\s*#*\s*$/; + +type Section = { + level: number; + name: string; + startLine: number; + endLine: number; + parent?: Section; +}; + +function buildPath(current: Section): string { + const parts: string[] = []; + let cursor: Section | undefined = current; + while (cursor) { + parts.unshift(cursor.name); + cursor = cursor.parent; + } + return parts.join(' > '); +} + +export function parseMarkdown(content: string, filePath: string): ParseResult { + const lines = content.split(/\r?\n/); + const sections: Section[] = []; + const stack: Section[] = []; + + const pushSection = (level: number, name: string, startLine: number) => { + while (stack.length > 0 && stack[stack.length - 1].level >= level) { + const last = stack.pop(); + if (last) last.endLine = startLine - 1; + } + const parent = stack[stack.length - 1]; + const section: Section = { level, name, startLine, endLine: lines.length, parent }; + sections.push(section); + stack.push(section); + }; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const match = headerPattern.exec(line); + if (!match) continue; + const level = match[1].length; + const name = match[2].trim(); + if (!name) continue; + pushSection(level, name, i + 1); + } + + while (stack.length > 0) { + const last = stack.pop(); + if (last) last.endLine = lines.length; + } + + const symbols: SymbolInfo[] = sections.map((section) => { + const signature = buildPath(section) || section.name; + const container = section.parent + ? { + name: section.parent.name, + kind: 'section' as const, + startLine: section.parent.startLine, + endLine: section.parent.endLine, + signature: buildPath(section.parent) || section.parent.name, + } + : undefined; + return { + name: section.name, + kind: 'section', + startLine: section.startLine, + endLine: section.endLine, + signature, + ...(container ? { container } : {}), + }; + }); + + if (symbols.length === 0) { + const name = filePath.split(/[\\/]/).pop() ?? filePath; + symbols.push({ + name, + kind: 'document', + startLine: 1, + endLine: Math.max(1, lines.length), + signature: name, + }); + } + + return { symbols, refs: [] }; +} diff --git a/src/core/parser/yaml.ts b/src/core/parser/yaml.ts new file mode 100644 index 0000000..97aee21 --- /dev/null +++ b/src/core/parser/yaml.ts @@ -0,0 +1,61 @@ +import { ParseResult, SymbolInfo } from '../types'; + +const keyPattern = /^\s*([A-Za-z0-9_.-]+)\s*:/; + +type YamlNode = { + name: string; + startLine: number; + endLine: number; +}; + +function isConfigPath(filePath: string): boolean { + const p = filePath.replace(/\\/g, '/'); + return p.includes('/.agents/') || p.includes('/templates/agents/') || p.includes('/rules/') || p.includes('/skills/'); +} + +function fileBaseName(filePath: string): string { + const parts = filePath.split(/[\\/]/); + return parts[parts.length - 1] ?? filePath; +} + +export function parseYaml(content: string, filePath: string): ParseResult { + const lines = content.split(/\r?\n/); + const nodes: YamlNode[] = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (!line || line.trimStart().startsWith('#')) continue; + const match = keyPattern.exec(line); + if (!match) continue; + const name = match[1].trim(); + if (!name) continue; + if (line.trimStart().startsWith('-')) continue; + nodes.push({ name, startLine: i + 1, endLine: lines.length }); + } + + for (let i = 0; i < nodes.length; i++) { + const next = nodes[i + 1]; + if (next) nodes[i].endLine = Math.max(nodes[i].startLine, next.startLine - 1); + } + + const symbols: SymbolInfo[] = nodes.map((node) => ({ + name: node.name, + kind: 'node', + startLine: node.startLine, + endLine: node.endLine, + signature: node.name, + })); + + if (symbols.length === 0) { + const name = fileBaseName(filePath); + symbols.push({ + name, + kind: isConfigPath(filePath) ? 'document' : 'node', + startLine: 1, + endLine: Math.max(1, lines.length), + signature: name, + }); + } + + return { symbols, refs: [] }; +} diff --git a/src/core/types.ts b/src/core/types.ts index 91b23b8..9bad95b 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -1,12 +1,14 @@ +export type SymbolKind = 'function' | 'class' | 'method' | 'section' | 'document' | 'node'; + export interface SymbolInfo { name: string; - kind: 'function' | 'class' | 'method'; + kind: SymbolKind; startLine: number; endLine: number; signature: string; container?: { name: string; - kind: 'function' | 'class' | 'method'; + kind: SymbolKind; startLine: number; endLine: number; signature: string; diff --git a/templates/agents/common/rules/git-ai-mcp/RULE.md b/templates/agents/common/rules/git-ai-mcp/RULE.md index 1ad7eba..60ba5c8 100644 --- a/templates/agents/common/rules/git-ai-mcp/RULE.md +++ b/templates/agents/common/rules/git-ai-mcp/RULE.md @@ -12,6 +12,10 @@ Agent 使用 git-ai MCP 工具的行为约束。 - **级别**: error - **规则**: 使用 `search_symbols`、`semantic_search`、`ast_graph_*` 前,必须先调用 `check_index` 确认索引就绪。索引不兼容时必须重建,禁止在索引缺失时进行符号搜索。 +### doc_index_scope +- **级别**: warning +- **规则**: 文档与规则模板已纳入索引(Markdown/YAML)。涉及 MCP、Skill、Rule 等问题时,优先使用 `semantic_search` 检索相关文档,再给出结论。 + ### understand_before_modify - **级别**: error - **规则**: 修改代码前必须先理解现有实现。流程:`search_symbols` 定位 → `read_file` 精读 → `ast_graph_callers` 确认影响范围 → 修改。禁止直接修改未读过的文件。 diff --git a/templates/agents/common/skills/git-ai-mcp/SKILL.md b/templates/agents/common/skills/git-ai-mcp/SKILL.md index 29c2307..25a0096 100644 --- a/templates/agents/common/skills/git-ai-mcp/SKILL.md +++ b/templates/agents/common/skills/git-ai-mcp/SKILL.md @@ -14,6 +14,7 @@ 4. **变更历史追溯** - 使用 `dsr_symbol_evolution` 追踪符号演变 5. **代码精读** - 使用 `read_file` 深入理解关键代码片段 6. **索引管理** - 按需重建索引,确保检索准确性 +7. **文档/规则检索** - 索引 Markdown/YAML 文档(含 skills/rules 模板)并支持语义检索 ## 推荐工作流 @@ -49,6 +50,12 @@ search_symbols({ path: "/ABS/PATH/TO/REPO", query: "handleRequest", mode: "subst semantic_search({ path: "/ABS/PATH/TO/REPO", query: "用户认证逻辑在哪里", topk: 10 }) ``` +查找文档/规则说明: + +```js +semantic_search({ path: "/ABS/PATH/TO/REPO", query: "MCP 规则约束", topk: 10, lang: "markdown" }) +``` + ### 4. 理解代码关系 查找调用者: @@ -100,6 +107,7 @@ read_file({ path: "/ABS/PATH/TO/REPO", file: "src/auth.ts", start_line: 1, end_l | 了解项目结构 | `repo_map` | `path`, `max_files`, `max_symbols` | | 按名称查找符号 | `search_symbols` | `path`, `query`, `mode`, `limit` | | 按语义查找代码 | `semantic_search` | `path`, `query`, `topk` | +| 查找文档/规则 | `semantic_search` | `path`, `query`, `topk`, `lang: "markdown"|"yaml"` | | 查找调用者 | `ast_graph_callers` | `path`, `name`, `limit` | | 查找被调用者 | `ast_graph_callees` | `path`, `name`, `limit` | | 追踪调用链 | `ast_graph_chain` | `path`, `name`, `direction`, `max_depth` | diff --git a/test/verify_parsing.ts b/test/verify_parsing.ts index 0344615..fedac61 100644 --- a/test/verify_parsing.ts +++ b/test/verify_parsing.ts @@ -1,14 +1,13 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import path from 'path'; -import { fileURLToPath } from 'node:url'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore dist module has no typings import { CodeParser } from '../dist/src/core/parser.js'; test('parser can parse polyglot examples', async () => { const parser = new CodeParser(); - const __filename = fileURLToPath(import.meta.url); - const __dirname = path.dirname(__filename); - const repo = path.join(__dirname, '../examples/polyglot-repo'); + const repo = path.resolve('examples/polyglot-repo'); const files = ['main.c', 'main.go', 'main.py', 'main.rs']; for (const f of files) { const res = await parser.parseFile(path.join(repo, f));