From fe43de4f90e67da3f5f74723138e71832d684cdf Mon Sep 17 00:00:00 2001 From: jaelgeng Date: Mon, 29 Jun 2026 11:05:40 +0800 Subject: [PATCH] feat(codebase): deep-enrich + graph-aware recall + team-wiki-codebase skill Complete code knowledge pipeline with AI-powered deep generation and graph-boosted retrieval. Base layer (wiki-engine + enrichment, same as PR #55): - Deterministic code extraction pipeline (5-language extractors) - AI enrichment + knowledge reconciler + manifest compiler - Wiki index rebuild with domain grouping Deep-enrich (this PR's additions): - deep-enrich.ts: background AI knowledge generation with resume support and --max-modules cost control - code-knowledge-recall.ts: BM25 + graph-boost retrieval engine with CJK bigram segmentation and camelCase splitting - codebase-upgrade-wiki.ts: migration from docs/team-codebase/ to teamwiki/ - codebase-wiki-lint.ts: graph health diagnostics Recall improvements: - --depth option (route/context/lookup) with per-level descriptions - Graph boost via slug/title matching (fixes dead-code path mismatch) - BM25 normalization to prevent score domination over learnings - 2-hop neighbor extension for graph edges Integration: - CI: MR comment API + graph change detection - import-iwiki.ts: iWiki document import - team-wiki-codebase skill bundle (methodology + agent prompts) - Security hardening (shell-quote, JSON parse guards, path validation) Tests: restore 3 import-org tests (replace stale clusterRepos assertions) --- README.md | 101 +- README.zh-CN.md | 95 +- agents/teamai-recall.md | 87 +- skills/team-wiki-codebase/README.md | 120 +++ skills/team-wiki-codebase/SKILL.md | 909 ++++++++++++++++++ .../references/agents/graph-rag-agent.md | 344 +++++++ .../references/agents/kb-doc-generator.md | 323 +++++++ .../methodology/phase0-collection.md | 54 ++ .../methodology/phase1-reverse-engineering.md | 89 ++ .../methodology/phase2-document-types.md | 341 +++++++ .../methodology/phase3-ai-enhancement.md | 164 ++++ .../references/methodology/phase4-quality.md | 232 +++++ .../references/templates/project-overview.md | 148 +++ .../team-wiki-codebase/scripts/scan_repo.py | 224 +++++ .../team-wiki-codebase/scripts/validate_kb.py | 250 +++++ src/__tests__/auto-recall.test.ts | 13 + src/__tests__/ci-extract-mr.test.ts | 13 +- src/__tests__/contribute-check-phase2.test.ts | 2 +- src/builtin-skills.ts | 2 +- src/ci/extract-mr.ts | 118 ++- src/ci/mr-comment.ts | 70 ++ src/clone.ts | 33 +- src/code-knowledge-recall.ts | 323 +++++++ src/codebase-cmd.ts | 29 +- src/codebase-upgrade-wiki.ts | 116 +++ src/codebase-wiki-lint.ts | 250 +++++ src/contribute-check.ts | 26 +- src/deep-enrich.ts | 479 +++++++++ src/hook-handlers.ts | 7 +- src/import-iwiki.ts | 166 ++++ src/import-mr.ts | 4 + src/import-repo.ts | 52 +- src/import.ts | 8 +- src/index.ts | 69 +- src/pull.ts | 25 +- src/recall.ts | 37 +- src/types.ts | 6 +- src/utils/ai-client.ts | 2 +- src/utils/iwiki-client.ts | 2 +- 39 files changed, 5169 insertions(+), 164 deletions(-) create mode 100644 skills/team-wiki-codebase/README.md create mode 100644 skills/team-wiki-codebase/SKILL.md create mode 100644 skills/team-wiki-codebase/references/agents/graph-rag-agent.md create mode 100644 skills/team-wiki-codebase/references/agents/kb-doc-generator.md create mode 100644 skills/team-wiki-codebase/references/methodology/phase0-collection.md create mode 100644 skills/team-wiki-codebase/references/methodology/phase1-reverse-engineering.md create mode 100644 skills/team-wiki-codebase/references/methodology/phase2-document-types.md create mode 100644 skills/team-wiki-codebase/references/methodology/phase3-ai-enhancement.md create mode 100644 skills/team-wiki-codebase/references/methodology/phase4-quality.md create mode 100644 skills/team-wiki-codebase/references/templates/project-overview.md create mode 100644 skills/team-wiki-codebase/scripts/scan_repo.py create mode 100644 skills/team-wiki-codebase/scripts/validate_kb.py create mode 100644 src/code-knowledge-recall.ts create mode 100644 src/codebase-upgrade-wiki.ts create mode 100644 src/codebase-wiki-lint.ts create mode 100644 src/deep-enrich.ts diff --git a/README.md b/README.md index 623d8c4..344b07c 100644 --- a/README.md +++ b/README.md @@ -71,35 +71,42 @@ The CLI picks a provider automatically from the repo URL: | Command | Description | |---------|-------------| -| `teamai init [--scope ] [--role ] [--force]` | Initialize (auto-installs gf CLI, OAuth login, links repo, registers member, configures reviewers, injects hooks) | -| `teamai push [--all] [--role ]` | Push local new resources to a dedicated branch and open a Merge Request; new skills prompt interactively for a target namespace (override with `--role`) | -| `teamai pull [--silent]` | Pull team resources and inject them into local AI tools (both scopes pulled sequentially) | -| `teamai status` | Show the diff between local and the team repo | -| `teamai list [type] [--source repo\|local\|all] [--agent ]` | List resources (skills\|rules\|docs\|env\|wiki). With `--source local` or `all`, scans skills directories of installed AI agents and tags each skill's origin (`[team]` / `[builtin]` / `[source:]` / `[local-only]`) | -| `teamai skill [list\|show ]` | List all skills by default; `show ` prints the skill's origin, contributors, installed-agent list, and description summary | -| `teamai members` | List registered team members | -| `teamai remove ` | Remove a resource from both the team repo and local, then open an MR (skills\|rules\|wiki) | -| `teamai roles` | Manage team roles (`init`/`list`/`set`/`add`/`remove`/`update`) | -| `teamai source` | Manage cross-team skill subscription sources (`add`/`remove`/`list`/`browse`) | -| `teamai contribute --file [--scope ]` | Push an AI-generated experience document to the team repo | -| `teamai recall ` | Search the team knowledge base, automatically merging user + project scope results | -| `teamai import --from-repo ` | Clone a remote repo and generate a per-repo summary under `docs/team-codebase/repos/.md`; AI recommends a business domain and persists the assignment to `.teamai/domains.yaml` | -| `teamai import --from-repo-list ` | Batch import a whitelist of repos with concurrency control, then aggregate the results into per-domain views | -| `teamai import --from-org --bootstrap` | List every repo under an organization (GitHub or TGit), AI-cluster them into business domains, and run an interactive review before the first full sync | -| `teamai import --from-iwiki [--iwiki-dual]` | Import iWiki documents as learnings; in dual mode also extract business-API / external-knowledge / glossary sections into `docs/team-codebase/external-knowledge.md` | -| `teamai cache --status \| --gc` | Inspect or garbage-collect the shallow-clone cache at `~/.teamai/cache/repos/` (LRU + size cap, default 5GB) | -| `teamai codebase --lint [--fix]` | Cross-file consistency lint over `docs/team-codebase` and `.teamai/`; reports anchor / orphan / source-invalid / sync-stale issues; `--fix` applies low-risk mechanical fixes | -| `teamai review [id] [--apply \| --reject \| --all-apply]` | Inspect and process pending codebase changes from `.teamai/pending-review.jsonl`; `--apply` patches in place via section anchors | -| `teamai domains drift [url] [--apply \| --lock \| --apply-all]` | Inspect and resolve domain-drift signals; `--apply` reassigns the repo to the recommended domain and refreshes the aggregate views | -| `teamai digest` | Generate a team AI usage weekly digest (skill leaderboard, new/updated skills, session summaries) | -| `teamai hooks` | Manage AI-tool hooks (`list` shows installation status; `inject`/`remove` update settings) | -| `teamai ci extract-mr --url [--mode comment\|write\|both] [--individual-comments]` | CI pipeline integration: extract knowledge from MR/PR, post as comments, and write to team repo after merge. With `--individual-comments`, each suggestion is posted separately with reaction/reject support (GitHub 👎 / TGit ☝️) | -| `teamai uninstall [--force]` | Uninstall teamai: remove hooks, rules, skills, env, docs, and `~/.teamai/` | -| `teamai doctor` | Diagnose configuration problems | - -Global options: -- `--dry-run` — preview mode, no real changes -- `--verbose, -v` — verbose output +| `teamai init` | Initialize (OAuth login, link repo, register member, inject hooks) | +| `teamai push` | Push local resources to a branch and open a Merge Request | +| `teamai pull` | Pull team resources and inject into local AI tools | +| `teamai status` | Show local vs team repo diff | +| `teamai recall ` | Search the team knowledge base (BM25 + graph-boost) | +| `teamai import --from-repo ` | Import a repo's code knowledge graph (`teamwiki/`) | +| `teamai import --from-org ` | Batch import all repos under an organization | +| `teamai import --from-repo-list ` | Batch import repos from a whitelist | +| `teamai import --from-mr ` | Extract learning from a merged MR/PR | +| `teamai import --from-iwiki ` | Import iWiki documents as learnings | +| `teamai codebase --lint` | Knowledge graph health check | +| `teamai contribute` | Share session experience to team repo | +| `teamai members` | List team members | +| `teamai roles` | Manage team roles and namespaces | +| `teamai remove ` | Remove a resource and open MR | +| `teamai digest` | Generate weekly team usage digest | +| `teamai doctor` | Diagnose configuration issues | +| `teamai uninstall` | Remove all teamai resources and hooks | + +Global options: `--dry-run`, `--verbose` + +
+More commands (management, CI, analytics) + +| Command | Description | +|---------|-------------| +| `teamai list [type]` | List resources (skills\|rules\|docs\|env\|wiki) | +| `teamai skill [show ]` | Inspect skill metadata and contributors | +| `teamai source` | Manage cross-team skill subscriptions | +| `teamai tags` | Manage tag-based resource filtering | +| `teamai env` | Manage team environment variables | +| `teamai hooks` | Manage AI-tool hooks | +| `teamai cache --gc` | Garbage-collect clone cache | +| `teamai ci extract-mr --url ` | CI: extract knowledge from MR, post comments, write after merge | + +
## How It Works @@ -316,6 +323,42 @@ Author: alice | Score: 12.0 | Tags: fuse, deploy The index is rebuilt automatically on every `teamai pull`. Indexes built by older versions (no `version` field or missing `type`) are detected and rebuilt transparently on first use. +### Codebase Knowledge Graph (teamwiki/) + +`teamai codebase --extract` (or `teamai import --from-repo`) parses your source repos and writes a structured knowledge graph under `teamwiki/`: + +``` +teamwiki/ +├── router.md # Navigation hub — lists every imported repo +├── index.md # Global index (auto-generated, with timestamp) +├── hot.md # Active working memory (reserved for Phase 4) +├── source-manifest.json # Per-file hash manifest for incremental extraction +├── .indices/ +│ └── graph-index.json # Knowledge graph: nodes + edges (JSON) +├── evidence/ +│ └── code/ +│ └── / # One directory per imported repo +│ ├── index.md # Project summary (fact count + page list) +│ ├── component.md # Functions / classes / components +│ ├── interface.md # Interface and type definitions +│ ├── config.md # Config keys (env vars, TOML keys, etc.) +│ ├── error.md # Error-handling patterns +│ └── relation-.md # Import relationships grouped by top-level dir +└── gaps/ + └── detected.md # Detected knowledge gaps (IMPL_MISSING, LOW_CONNECTIVITY, …) +``` + +**graph-index.json** stores the extracted graph. A real example: 11 HAI team repos → **2 218 nodes, 852 edges**. + +| Field | Description | +|-------|-------------| +| `nodes[].kind` | `component` (function/class) or `config` (config key) | +| `edges[].relation` | `imports` — cross-file and cross-repo dependency | + +Cross-repo edges are detected automatically by PascalCase label matching. + +`teamai recall` uses this graph for **BM25 + graph-boost** retrieval: keyword hits are re-ranked by graph proximity, so you get structurally relevant results, not just textual matches. + ### TodoWrite reminder hook `teamai pull` registers a PostToolUse hook on the `TodoWrite` tool. The first time a session writes a TODO list, the hook injects a one-time reminder asking the agent to invoke `teamai-recall` if it has not already done so. Per-session deduplication uses `~/.teamai/sessions/-todowrite-hint.json` (24 h TTL). diff --git a/README.zh-CN.md b/README.zh-CN.md index 179f454..924a58e 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -71,35 +71,42 @@ CLI 会根据用户传入的 repo URL 自动选择 provider: | 命令 | 说明 | |------|------| -| `teamai init [--scope ] [--role ] [--force]` | 初始化(自动安装 gf CLI、OAuth 登录、关联仓库、注册成员、配置 reviewers、注入 hooks) | -| `teamai push [--all] [--role ]` | 推送本地新资源到独立分支并创建 Merge Request;新 skill 交互式选择目标命名空间,可用 `--role` 覆盖 | -| `teamai pull [--silent]` | 拉取团队资源并注入到本地 AI 工具(支持双 scope 依次拉取) | +| `teamai init` | 初始化(OAuth 登录、关联仓库、注册成员、注入 hooks) | +| `teamai push` | 推送本地资源到独立分支并创建 MR | +| `teamai pull` | 拉取团队资源并注入到本地 AI 工具 | | `teamai status` | 查看本地 vs 团队仓库差异 | -| `teamai list [type] [--source repo\|local\|all] [--agent ]` | 列出资源(skills\|rules\|docs\|env\|wiki);`--source local` 或 `all` 时会扫描已安装 AI agent 下的 skills 目录,并标注每个 skill 的来源 (`[team]` / `[builtin]` / `[source:]` / `[local-only]`) | -| `teamai skill [list\|show ]` | 默认列出全部 skill;`show ` 输出指定 skill 的来源、贡献者、已安装的 agent 列表与描述摘要 | -| `teamai members` | 列出已注册的团队成员 | -| `teamai remove ` | 从团队仓库和本地删除资源并创建 MR(skills\|rules\|wiki) | -| `teamai roles` | 管理团队角色(`init`/`list`/`set`/`add`/`remove`/`update`) | -| `teamai source` | 管理跨团队 skill 订阅源(`add`/`remove`/`list`/`browse`) | -| `teamai contribute --file [--scope ]` | 将 AI 生成的经验文档推送到团队仓库 | -| `teamai recall ` | 搜索团队知识库,自动合并 user + project 双 scope 结果 | -| `teamai import --from-repo ` | 拉取远端仓库并生成单仓视图 `docs/team-codebase/repos/.md`;AI 推荐业务域并写入 `.teamai/domains.yaml` | -| `teamai import --from-repo-list ` | 按白名单批量导入多个仓库(支持并发),并按业务域聚合产出 | -| `teamai import --from-org --bootstrap` | 列出组织/group 下所有仓库(GitHub / TGit),AI 聚类为业务域,交互式 review 后完成首次全量同步 | -| `teamai import --from-iwiki [--iwiki-dual]` | 把 iWiki 文档导入为 learnings;dual 模式同时把业务接口 / 外部知识源 / 术语表抽取到 `docs/team-codebase/external-knowledge.md` | -| `teamai cache --status \| --gc` | 查看或回收 shallow-clone 缓存目录 `~/.teamai/cache/repos/`(LRU + 容量上限,默认 5GB) | -| `teamai codebase --lint [--fix]` | 对 `docs/team-codebase` 与 `.teamai/` 做跨文件一致性 lint;报告锚点 / 孤儿 / 源失效 / 同步陈旧等问题;`--fix` 应用低风险机械修复 | -| `teamai review [id] [--apply \| --reject \| --all-apply]` | 浏览并处理 `.teamai/pending-review.jsonl` 中的待审 codebase 变更;`--apply` 通过章节锚点原地写入 | -| `teamai domains drift [url] [--apply \| --lock \| --apply-all]` | 浏览并处理域漂移信号;`--apply` 把仓库重新归类到推荐域并刷新聚合视图 | -| `teamai digest` | 生成团队 AI 使用周报(skill 排行、新增/更新 skill、session 摘要) | -| `teamai hooks` | 管理 AI 工具 hooks(`list` 查看安装状态;`inject`/`remove` 更新配置) | -| `teamai ci extract-mr --url [--mode comment\|write\|both] [--individual-comments]` | CI 流水线集成:从 MR/PR 中提取知识,发布为评论,合并后写入团队知识仓库。使用 `--individual-comments` 时每条建议单独发布,支持 reaction/reject 交互(GitHub 👎 / TGit ☝️) | -| `teamai uninstall [--force]` | 卸载 teamai:移除 hooks、rules、skills、env、docs、~/.teamai/ | +| `teamai recall ` | 搜索团队知识库(BM25 + 图谱加权) | +| `teamai import --from-repo ` | 导入仓库代码知识图谱(`teamwiki/`) | +| `teamai import --from-org ` | 批量导入组织下所有仓库 | +| `teamai import --from-repo-list ` | 按白名单批量导入 | +| `teamai import --from-mr ` | 从已合并 MR 提取 learning | +| `teamai import --from-iwiki ` | 从 iWiki 导入文档为 learnings | +| `teamai codebase --lint` | 知识图谱健康度检查 | +| `teamai contribute` | 分享本次 session 经验到团队仓库 | +| `teamai members` | 列出团队成员 | +| `teamai roles` | 管理团队角色和命名空间 | +| `teamai remove ` | 删除资源并创建 MR | +| `teamai digest` | 生成团队使用周报 | | `teamai doctor` | 诊断配置问题 | +| `teamai uninstall` | 卸载所有 teamai 资源和 hooks | -全局选项: -- `--dry-run` — 预览模式,不做实际变更 -- `--verbose, -v` — 详细输出 +全局选项:`--dry-run`、`--verbose` + +
+更多命令(管理、CI、分析) + +| 命令 | 说明 | +|------|------| +| `teamai list [type]` | 列出资源(skills\|rules\|docs\|env\|wiki) | +| `teamai skill [show ]` | 查看 skill 元数据和贡献者 | +| `teamai source` | 管理跨团队 skill 订阅 | +| `teamai tags` | 管理基于标签的资源过滤 | +| `teamai env` | 管理团队环境变量 | +| `teamai hooks` | 管理 AI 工具 hooks | +| `teamai cache --gc` | 回收 clone 缓存 | +| `teamai ci extract-mr --url ` | CI:从 MR 提取知识,发布评论,合并后写入团队仓库 | + +
## 工作原理 @@ -316,6 +323,42 @@ Author: alice | Score: 12.0 | Tags: fuse, deploy 索引在每次 `teamai pull` 时自动重建。旧版索引(无 `version` 字段或缺少 `type`)会在首次使用时被自动检测并重建,对调用方透明 +### 代码库知识图谱(teamwiki/) + +`teamai codebase --extract`(或 `teamai import --from-repo`)解析源码仓库,将结构化知识图谱写入 `teamwiki/` 目录: + +``` +teamwiki/ +├── router.md # 导航枢纽,列出所有已导入仓库 +├── index.md # 全局索引(自动生成,含时间戳) +├── hot.md # 活跃工作记忆(Phase 4 hot/cold 预留) +├── source-manifest.json # 源文件哈希清单(增量提取用) +├── .indices/ +│ └── graph-index.json # 知识图谱:nodes + edges(JSON 格式) +├── evidence/ +│ └── code/ +│ └── / # 每个导入的仓库一个目录 +│ ├── index.md # 项目摘要(facts 总数 + 页面列表) +│ ├── component.md # 函数 / 类 / 组件 +│ ├── interface.md # 接口和类型定义 +│ ├── config.md # 配置项(环境变量、TOML key 等) +│ ├── error.md # 错误处理模式 +│ └── relation-.md # 按顶级目录分组的 import 依赖关系 +└── gaps/ + └── detected.md # 知识缺口检测结果(IMPL_MISSING / LOW_CONNECTIVITY / …) +``` + +**graph-index.json** 存储提取出的知识图谱。真实数据参考:HAI 团队 11 个仓库 → **2 218 个节点,852 条边**。 + +| 字段 | 说明 | +|------|------| +| `nodes[].kind` | `component`(函数/类)或 `config`(配置项) | +| `edges[].relation` | `imports` —— 跨文件或跨仓库依赖关系 | + +跨仓 edge 通过 PascalCase 标签匹配自动检测,无需手动配置。 + +`teamai recall` 利用此图谱进行 **BM25 + graph-boost** 检索:关键词命中后按图结构邻近度重排序,结果兼具文本相关性和结构相关性。 + ### TodoWrite 提醒 hook `teamai pull` 会在 `TodoWrite` 工具上注册一个 PostToolUse hook。当 session 第一次写 TODO 列表时,hook 会注入一次性提醒,要求 agent 在尚未调用 `teamai-recall` 时先调用一次。session 级去重通过 `~/.teamai/sessions/-todowrite-hint.json` 实现(TTL 24 小时) diff --git a/agents/teamai-recall.md b/agents/teamai-recall.md index 5cf7d6d..0ee5f3b 100644 --- a/agents/teamai-recall.md +++ b/agents/teamai-recall.md @@ -1,6 +1,6 @@ --- name: teamai-recall -description: Search the team knowledge base (skills + learnings + docs + rules) and return a compact, structured summary with doc_ids — instead of dumping full knowledge content into the main conversation. Invoke this BEFORE any task involving code changes, troubleshooting, or design. +description: Search the team knowledge base (skills + learnings + docs + rules + codebase graph) and return a compact, structured summary with doc_ids — instead of dumping full knowledge content into the main conversation. Invoke this BEFORE any task involving code changes, troubleshooting, or design. tools: Bash, Read, Grep, Glob --- @@ -20,16 +20,23 @@ upstream API"). Treat this as your query. ## What you must do — step by step -### Step 1 — Read the codebase manifest (optional but preferred) +### Step 1 — Read codebase context (optional but preferred) -If `~/.teamai/docs/codebase.md` OR `docs/team-codebase/index.md` (in the -current project) exists, read it first. It lists the team's repositories -and their purposes. Extract a one-sentence repo-list summary to prepend to -your final output. If neither file exists, **silently skip** this step — -never error out. +Check for the team's code knowledge graph in this order: -> Note: `teamai recall` already indexes team-codebase documents -> (repos/*.md), so Step 3 will return codebase knowledge matches directly. +1. `teamwiki/router.md` — if exists, read it to understand available repos +2. `teamwiki/index.md` — global navigation with domain links + +If `teamwiki/` exists, the team has a structured knowledge graph. After +Step 3 returns codebase hits, you can **drill into** module summaries: +- `teamwiki/evidence/code//modules/.md` — module-level overview with dependency direction and top components +- `teamwiki/evidence/code//overview.md` — AI-generated architecture context (why/how, not just what) + +Fallback: if no `teamwiki/`, check `~/.teamai/docs/codebase.md` or +`docs/team-codebase/index.md`. If none exists, silently skip. + +> `teamai recall` automatically searches both flat knowledge (learnings/ +> skills/docs/rules) and codebase graph (teamwiki/) with BM25 + graph-boost. ### Step 2 — Extract keywords from the task description @@ -51,12 +58,21 @@ If the command fails, knowledge base is empty, or returns zero hits, emit a single line `No relevant team knowledge found for: ` and stop. -### Step 4 — Read the top hits +### Step 4 — Read the top hits and drill into codebase For each hit returned by `teamai recall`, read the source file directly -(use `Read`) and condense each into **one or two sentences**. Cap your -total summary at ~1500 characters. Drop hits that on closer inspection -are clearly off-topic. +(use `Read`) and condense each into **one or two sentences**. + +**For codebase hits** (path contains `teamwiki/evidence/`): +- If the hit is a raw facts page (component.md, interface.md), prefer + reading the corresponding **module summary** (`modules/.md`) instead — + it's more concise and shows dependencies. +- If you need architectural context (why a module exists, design decisions), + check `overview.md` in the same project directory. +- If the hit mentions a knowledge gap (from `gaps/detected.md`), relay + it to the user: "This area is not fully documented in the knowledge base." + +Cap your total summary at ~2000 characters. Drop hits that are off-topic. ### Step 5 — Emit a structured response @@ -65,24 +81,43 @@ Return your output in **this exact format** to the main conversation: ``` ## Team Knowledge Recall -> Repos: +> Repos: + +### Relevant knowledge 1. **[] ** — Confidence: -2. **[] ** — - - Confidence: +2. ... + +### Codebase context (if any codebase hits) + +**Module: ** () +- Depends on: +- Depended by: +- Core components: `Foo`, `Bar`, `Baz` (top 5 by reference count) +- Architecture: + +### Gaps (if relevant) -... +⚠️ — do not guess answers for this area. ``` -Where: -- `` is one of `skills` / `learnings` / `docs` / `rules` -- `` is the filename without extension (e.g. `api-timeout-fix`) +**Output structure rules:** + +- `` is one of `skills` / `learnings` / `docs` / `rules` / `codebase` +- `` is the filename without extension (e.g. `api-timeout-fix`). + For codebase hits, use the relative path within teamwiki/ (e.g. `evidence/code/hai_api/modules/business`) +- **Codebase context section**: when a codebase hit is returned, include + the module's dependency direction and top 5 components **inline** — the + main conversation should not need a second Read to understand the module. + Extract this from `modules/.md` which you already read in Step 4. +- **Gaps section**: only include if `gaps/detected.md` was relevant to the + query. This tells the main conversation to stop and ask the user rather + than hallucinating. - The trailing HTML comment **must** list every doc_id you returned — later phases (Phase 3 Stop hook) will parse this from the conversation transcript. @@ -93,5 +128,13 @@ Where: - **Do not** call `teamai recall` more than 3 times in one invocation. - **Do not** invoke other subagents. - If `teamai` CLI is not on PATH, return `teamai CLI not available` and stop. -- Output total ≤ ~2000 characters. The whole point of using a subagent is +- Output total ≤ ~2500 characters. The whole point of using a subagent is to keep the main conversation's context lean. +- For codebase hits, **prefer module summaries over raw facts pages** — + they give better signal-to-noise for the main conversation. +- **Include module dependency + core components inline** so the main + conversation can act without a second retrieval round-trip. +- If `teamwiki/gaps/detected.md` exists and is relevant, include the + Gaps section so the main conversation does not hallucinate. +- When zero hits are found but `teamwiki/` exists, check if the query + relates to a known gap before returning "no knowledge found". diff --git a/skills/team-wiki-codebase/README.md b/skills/team-wiki-codebase/README.md new file mode 100644 index 0000000..0c63253 --- /dev/null +++ b/skills/team-wiki-codebase/README.md @@ -0,0 +1,120 @@ +# team-wiki-codebase — 大型代码库 AI 认知工程 + +> Team Wiki 插件内置 skill:方法论、脚本与 Agent 规范均随 `team-wiki install` / `upgrade` 部署到项目的 `.codebuddy/`、`.cursor/` 等目录。 + +## 为什么需要这个 skill + +大型项目的 AI 理解困境: + +| 痛点 | 具体表现 | +|------|---------| +| **上下文装不下** | 10+ 仓库、数十万行代码,远超 AI 上下文窗口 | +| **关系看不清** | 微服务间的 RPC/MQ/DB 依赖散落在各仓库,没有全局视图 | +| **规则记不住** | 业务约束、状态机、配置参数隐藏在深层调用链中 | +| **回答不准确** | AI 只看到局部代码,缺乏全局架构认知,容易幻觉 | +| **token 消耗大** | 每次提问都要重新读大量源码,效率极低 | + +## 怎么解决 + +通过架构逆向工程,将海量代码**压缩为结构化知识库**: + +- 每个结论有代码 `文件:行号` 作为证据 +- 每条组件关系有置信度标注(`EXTRACTED` / `INFERRED` / `AMBIGUOUS`) +- 每次生成后有准确性统计,超标自动警告 +- AI 读知识库而非读源码,**约 1/50 的 token 消耗**获得全局架构认知 +- Phase 0 可用 `team-wiki compile code --extract ast,heuristic` 生成可证据化的结构边(TS/JS/Python/Go) + +--- + +## 产出体系 + +``` +/ +├── README.md ← 检索路由指引(AI 专用) +├── {项目名} 技术架构.md ← 系统全貌,~200KB +├── {项目名} 业务架构.md ← 产品能力 + 生命周期 +├── {项目名} 部署架构.md ← 部署拓扑 +├── XX_{组件名}设计说明.md × N ← 每组件一份,含 AI 快速理解表 +├── XX_{项目名}核心API产品代码映射.md ← 产品约束→代码位置 桥梁文档 +├── XX_{项目名}产品规则速查表.md +├── XX_{项目名}业务开发规范SOP.md +├── {反模式/RPC契约/排障记录} × N +├── _manifest.json ← 机器可读 manifest(供 team-wiki compile 快路径) +└── graph/ ← Graph RAG 图谱文档集 + ├── G1 组件依赖关系矩阵 + ├── G2 调用链路全景 + 状态机 + ├── G3 数据流与存储依赖图 + ├── G4 错误码组件映射表 + ├── G5 跨组件交互场景手册(≥10个时序图) + ├── G6 知识图谱三元组(≥100条,含置信度) + ├── G7 架构风险与影响面分析 + ├── G8 核心配置参数索引 + └── G9 业务规则约束矩阵 + AI 推理决策树 +``` + +--- + +## 执行流程 + +``` +Phase 0 → 初始化:收集路径、项目名、产品文档来源;可选 CLI ast+heuristic 结构基线 + +Phase K1 → 架构逆向:关键文件提取 → 分层分析 → 组件关系矩阵 + ⛔ 确认点① 架构理解确认 + +Phase K2 → 文档生成(分批并行): + 批次1~4: Type-4 组件文档(并行子 Agent 分发) + ⛔ 确认点② 文档质量抽查 + 批次5~7: 架构总览 + 桥梁文档 + 知识增强 + +Phase K3 → AI-Native 增强: + search-anchor + 双向链接 + 检索路由规则 + Graph RAG 图谱文档集 G1~G9(置信度三态标注) + +Phase K4 → 质量评估: + validate_kb.py 自动检验 + 全库准确性审计([UNVERIFIED] 统计 + 接口覆盖率) + 跨文档一致性校验(矛盾检测 + 自动修复) + RAG 检索抽检(7类问题) + AI 端到端验证(10~15 个标准问题 + 代码回溯) + 生成质量报告 +``` + +支持 `--update` 增量更新(基于文件 hash 缓存,只重跑变更组件)。 + +--- + +## 文件结构 + +``` +team-wiki-codebase/ +├── SKILL.md ← 主执行指令(AI 加载) +├── README.md ← 本文件 +├── scripts/ +│ ├── scan_repo.py ← 仓库扫描辅助工具 +│ └── validate_kb.py ← 知识库质量校验工具 +└── references/ + ├── agents/ + │ ├── kb-doc-generator.md ← Type-1~8 文档生成专职 Agent + │ └── graph-rag-agent.md ← G1~G9 图谱文档专职 Agent + ├── methodology/ + │ ├── phase0-collection.md ← 源材料采集方法 + │ ├── phase1-reverse-engineering.md ← 架构逆向工程方法 + │ ├── phase2-document-types.md ← 九大文档类型规范与质量标准 + │ ├── phase3-ai-enhancement.md ← AI-Native 增强方法 + │ └── phase4-quality.md ← 质量评估 Checklist + └── templates/ + └── project-overview.md ← 知识库 README 模板(含认知边界声明) +``` + +--- + +## 质量标准 + +| 维度 | 达标标准 | +|------|---------| +| 覆盖率 | ≥90% P0 核心组件有文档 | +| 准确性 | [UNVERIFIED] < 15% | +| 结构质量 | 死链接=0,search-anchor 覆盖率≥95% | +| AI 可用性 | RAG 检索抽检准确率≥85% | +| 关系可信度 | AMBIGUOUS 关系 < 10%,全部列入待确认清单 | diff --git a/skills/team-wiki-codebase/SKILL.md b/skills/team-wiki-codebase/SKILL.md new file mode 100644 index 0000000..4e4a5a0 --- /dev/null +++ b/skills/team-wiki-codebase/SKILL.md @@ -0,0 +1,909 @@ +--- +name: team-wiki-codebase +description: | + 让 AI 真正理解大型代码库。针对多仓库、多微服务、迭代多年的项目,通过架构逆向 + Graph RAG 图谱 + CLI 多语言 AST, + 将海量代码压缩为结构化知识库——每条结论可回溯代码行,每条关系有置信度标注。 + + 适用场景:项目有 10+ 仓库或微服务,AI 直接读代码无法全局理解、回答不准确、token 开销大。 + + 产出:组件设计文档 × N + 架构总览 + 桥梁文档 + Graph RAG 图谱(G1~G9) + _manifest.json + team-wiki 编译产物。 + + Trigger: team-wiki-codebase, code-to-knowledge, 代码知识库, 架构分析, 架构逆向 + Prerequisites: 可访问的源码目录(支持多仓库);本 skill 目录下 `references/` 与 `scripts/` +--- + +# team-wiki-codebase — 大型代码库 AI 认知工程 + +> 方法论与脚本位于本 skill 的 `references/`、`scripts/`(`team-wiki upgrade` 后出现在 `.cursor/skills/team-wiki-codebase/` 或 `.codebuddy/skills/team-wiki-codebase/`)。人类可读概览见 [README.md](./README.md)。 +> 图谱 CLI 能力见 [GRAPH-CAPABILITIES.md](../GRAPH-CAPABILITIES.md)。 + +**解决什么问题**:大型项目(10+ 仓库、数十微服务、迭代多年)让 AI 无法全局理解——上下文窗口装不下所有代码,组件关系散落各处,业务规则隐藏在深层调用链中。直接让 AI 读代码,既慢(海量 token)又不准(缺乏全局视角)。 + +**怎么解决**:通过架构逆向工程,将海量代码系统化压缩为**结构化、可验证、AI-Native** 的深度知识库——每个结论可回溯到代码行,每条关系有置信度标注,每次更新有增量校验。AI 读知识库而非读源码,用约 **1/50 的 token** 获得全局架构认知。 + +## 使用方式 + +``` +/team-wiki-codebase # 默认:Standard(单 session 核心路径) +/team-wiki-codebase --deep # Deep:完整 K1~K4 + G1~G9 +/team-wiki-codebase --update # 增量更新已有 knowledge/ +/team-wiki-codebase continue # 从 _review/progress.json 断点继续 +``` + +--- + +## Agent 架构 + +| Agent | 文件 | 启动时机 | +|-------|------|---------| +| 知识库文档生成 Agent | `references/agents/kb-doc-generator.md` | Phase K2 每批组件 | +| Graph RAG Agent | `references/agents/graph-rag-agent.md` | Phase K3 | + +**主 Agent 职责**:流程编排、确认点管理、progress.json 维护、质量报告汇总。 + +--- + +## 入口判断 + +**每次激活时必须先执行此判断。** + +``` +IF 用户输入包含 "--update" 或 "增量更新": + → Update 模式 +ELSE IF 用户输入包含 "continue" 或 "继续": + → Continue 模式 +ELSE: + → 检查用户指定目录下是否有 _review/progress.json + IF 存在 → 告知状态,等待"继续上次"或"重新开始" + ELSE → Phase 0 +``` + +--- + +## Continue 模式 + +``` +Step 1:定位 progress.json +Step 2:读取解析,展示恢复摘要 +Step 3:根据 current_phase 跳转: + "phase0_done" → Phase K1 + "phasek1_waiting_confirm" → 展示 k1-architecture-map.md,等待确认① + "phasek1_confirmed" → Phase K2 + "phasek2_batch_N" → Phase K2 第 N 批继续(跳过已完成) + "phasek2_waiting_confirm" → 等待确认② + "phasek2_confirmed" → Phase K3 + "phasek3_done" → Phase K4 + "phasek4_done"/"completed" → 告知完成,询问是否 --update 或重跑某组件 +``` + +--- + +## Update 模式(增量更新) + +**触发**:`/team-wiki-codebase --update` 或「增量更新」。 +**前提**:已有 completed 状态的 progress.json。 + +``` +Step 1:读取 progress.json,获取 file_hash_cache +Step 2:扫描 project_root,计算各文件当前 SHA256 +Step 3:对比 hash,分类:新增 / 修改 / 删除 +Step 4:展示变更摘要,等待用户确认: + ┌────────────────────────────────────┐ + │ 变更摘要 │ + │ 新增: N 个文件 │ + │ 修改: N 个文件(含 Aurora.py 等) │ + │ 删除: N 个文件 │ + │ 受影响组件: [列表] │ + │ 受影响图谱文档: G1/G2/G6/G7 │ + └────────────────────────────────────┘ +Step 5:仅重跑受影响范围: + - Phase K2:重新生成受影响组件的 Type-4 文档(覆盖写入) + - Phase K3 局部:更新涉及变更组件的图谱文档(G1/G2/G6/G7) + - Phase K4:重新运行 validate_kb.py +Step 6:更新 file_hash_cache + metadata.json commit SHA +Step 7:组件级 diff(处理新增/删除仓库或组件) + IF repos 列表与上次不同: + 新增的仓库 → 对新仓库执行完整 K1 扫描,补充到组件清单,生成 Type-4 文档 + 删除的仓库 → 对应组件文档顶部加 `⚠️ [DEPRECATED] 此组件对应仓库已移除` + → 更新 k1-architecture-map.md 的组件清单 + → 更新 G1 矩阵(移除已删除组件的行列,新增新组件行列) +``` + +--- + +## progress.json 规范 + +**路径**:`/../_review/progress.json` + +```json +{ + "version": "5", + "repos": [ + {"name": "repo-a", "path": "/absolute/path/to/repo-a", "language": "go"}, + {"name": "repo-b", "path": "/absolute/path/to/repo-b", "language": "python"} + ], + "output_dir": "/absolute/path/to/knowledge", + "primary_language": "go", + "project_name": "ProjectName", + "scan_time": "2026-01-01T10:00:00Z", + "current_phase": "phasek2_batch_2", + "confirmed_phases": ["phase0", "phasek1"], + + "service_map": { + "描述": "Phase K1 Step 3 构建的服务名→仓库映射表", + "ServiceA": {"repo": "repo-a", "entry": "cmd/serviceA/main.go"}, + "ServiceB": {"repo": "repo-b", "entry": "app/main.py"} + }, + + "kb_progress": { + "component_total": 12, + "components_done": ["Aurora", "Frame"], + "components_pending": ["CCDB", "Dispatcher"], + "type1_done": false, + "type2_done": false, + "type3_done": false, + "bridge_docs_done": false, + "graph_rag_done": false + }, + + "accuracy_stats": { + "total_claims": 0, + "verified": 0, + "unverified": 0, + "ambiguous_relations": 0 + }, + + "interface_coverage": { + "描述": "接口数量对账结果,由 Phase K2 自校验填充", + "ComponentA": {"type": "HTTP", "scanned": 13, "documented": 0, "gap": 13}, + "ComponentB": {"type": "MQ", "scanned": 5, "documented": 0, "gap": 5} + }, + + "consistency_check": { + "描述": "Phase K3 Step 3 跨文档一致性校验结果", + "contradictions": 0, + "missing_refs": 0, + "g1_deviations": 0, + "consistency_rate": 0.0 + }, + + "e2e_validation": { + "描述": "Phase K4 Step 4 AI 端到端验证结果", + "total_questions": 0, + "correct": 0, + "partial": 0, + "incorrect": 0, + "boundary_ok": 0, + "boundary_fail": 0, + "accuracy_rate": 0.0 + }, + + "file_hash_cache": { + "relative/path/to/file.go": "sha256_hex" + } +} +``` + +> `accuracy_stats` 在每批 Phase K2 完成后累加,是知识库可信度的全局指标。 + +--- + +## 核心原则(准确性优先) + +1. **代码为唯一事实来源**:每个结论必须有代码文件:行号 作为证据,无法验证的标 `[UNVERIFIED]` +2. **置信度三态强制**:图谱中每条关系标 `EXTRACTED(1.0)` / `INFERRED(0.6~0.9)` / `AMBIGUOUS(0.1~0.3)`;禁止凭空发明,禁止用 0.5 默认值 +3. **两级准确性验证**:Phase K2 每份文档生成后立即自校验;Phase K4 全库质量检验 +4. **人在回路两次确认**:架构理解(K①)和组件文档质量(K②)必须人工确认,防止系统性错误扩散 +5. **并行生成 + 断点续传**:Type-4 组件文档并行分发(同一消息发出所有 Agent calls);每批持久化 progress.json +6. **Token 精简**:`Glob → Grep → Read` 三步法,禁止全量目录扫描 +7. **诚实审计**:`[UNVERIFIED]` 不得隐藏;质量数字完整展示;不确定用 AMBIGUOUS 不删除 +8. **认知边界声明**:知识库 README 必须明确声明覆盖范围和不覆盖范围,让 AI 知道何时应该说"不确定" +9. **跨文档一致性**:Phase K3 强制交叉比对组件间关系描述,矛盾项必须修复后才计入"一致" +10. **端到端可验证**:Phase K4 用标准化问题测试知识库实际回答能力,E2E 准确率目标 ≥ 80% + +--- + +## Phase 0:初始化 + +一次性向用户询问以下信息(**同一条消息,不分步骤**): + +1. **项目所有代码仓库路径**(用户把整个项目涉及的所有仓库地址列出来): + - 格式:每行一个绝对路径,或逗号分隔 + - 示例: + ``` + /path/to/api-gateway + /path/to/order-service + /path/to/user-service + /path/to/common-lib + ``` + - 说明:这是最关键的一步。大型项目的代码散布在多个仓库中,必须**全部提供**才能构建完整的架构认知。遗漏仓库 = 知识库盲区。 +2. **项目名称**(用于文档命名,如 "CVM"、"电商平台") +3. **产品文档来源**(可选,提供则生成 Type-5/6 桥梁文档): + - API 文档目录路径 + - 使用限制 / FAQ 文档路径 +4. **输出路径**(默认:第一个仓库的父目录下的 `knowledge/`) + +**Step 0A:仓库清单整理** + +收到用户提供的仓库列表后,构建仓库清单: + +``` +FOR 每个用户提供的路径: + 1. 验证路径存在且可访问 + 2. 检测是否为 git 仓库(是否有 .git 目录) + 3. 检测主要语言(按文件扩展名分布) + 4. 统计代码规模(文件数 + 估算行数) + 5. 记录 git commit SHA + tag + +结果写入 _review/repo-manifest.json: +{ + "repos": [ + { + "path": "/absolute/path/to/repo-a", + "name": "repo-a", + "language": "go", + "files": 320, + "lines_estimate": 45000, + "commit": "abc123", + "tag": "v1.2.0", + "accessible": true + }, + ... + ], + "total_repos": N, + "inaccessible": ["path/to/repo-x(权限不足)"] +} +``` + +展示给用户确认: +``` +已识别 {N} 个仓库: + ✅ repo-a (Go, ~45K 行) + ✅ repo-b (Python, ~12K 行) + ✅ repo-c (Go, ~28K 行) + ❌ repo-x (路径不存在或无法访问) + +总计: ~{N}K 行代码,{N} 个仓库 +确认无误后回复"继续",或补充遗漏的仓库。 +``` + +**Step 0B:自动检测主要语言**(按仓库列表汇总,不阻断流程): +``` +检测方法:汇总所有仓库的文件扩展名分布 + .go 文件占比最高 → language: "go" + .py 文件占比最高 → language: "python" + .java 文件占比最高 → language: "java" + .ts/.js 文件占比最高 → language: "typescript" + .rs 文件占比最高 → language: "rust" + 多语言混合(无明显主导) → language: "mixed" +备注:language 字段用于接口扫描时选择 grep 模式(详见 Phase K1 Step 5) +``` + +**Step 0C:记录基准版本**: +```bash +# 对每个仓库分别记录 +FOR repo in repos: + git -C rev-parse HEAD 2>/dev/null + git -C describe --tags --always 2>/dev/null +``` +写入 `_review/metadata.json`: +```json +{ + "project_name": "CVM", + "scan_time": "", + "repos": [ + {"name": "repo-a", "commit": "", "tag": ""}, + {"name": "repo-b", "commit": "", "tag": ""} + ] +} +``` + +**Step 0D:CLI 结构基线(每个代码仓库,推荐)** + +在 K1 深读之前,用 Team Wiki CLI 生成可证据化的 import/call 结构边(Python/Go/TS 等,`code-ast`)并与 regex 基线合并(`code-heuristic`): + +```bash +# 对每个 repo( 通常为项目下的 .teamwiki 或 .wiki) +team-wiki compile code \ + --project \ + --extract ast,heuristic \ + --write + +# 预览 AST 统计(不写盘) +team-wiki compile code --extract ast --dry-run +``` + +- 输出:`code//` 下 index/component/relation 等页;`graph/-graph-index.json`(结构边草案)。 +- K1/K2/K3 写 `_manifest.json` 的 `edges[]` 时:**优先引用** compile 的 `code-ast` 边 + `evidenceRefs`(`path:line`),Agent 推断标 `INFERRED`/`AMBIGUOUS`。 +- K3 完成后写入 wiki 图:`team-wiki compile code --extract ast,heuristic --write`(有 `_manifest.json` 时走 manifest 快路径 merge `graph-index.json`)。 + +写入初始 progress.json(current_phase: "phase0_done"),进入 **Phase K1**。 + +--- + +## Phase K1:架构逆向与源材料采集 + +**方法论**:`references/methodology/phase0-collection.md` + `references/methodology/phase1-reverse-engineering.md` + +### Step 1:可选运行扫描脚本(推荐) + +```bash +python3 scripts/scan_repo.py --depth 2 --top 10 +``` +输出:文件统计 + 关键文件发现报告 + 语言分布。 + +### Step 2:关键文件提取 + +按优先级扫描(详见 phase0-collection.md): +- **P0 必须**:入口文件、路由/Handler、流程编排配置、Proto/IDL +- **P1 重要**:数据库 Schema(DDL)、常量/错误码定义 +- **P2 增强**:配置文件、测试文件(理解预期行为) + +### Step 3:架构逆向(详见 phase1-reverse-engineering.md) + +- 自底向上分层:叶子节点(DB/MQ) → 中间节点(编排/调度) → 根节点(API入口) +- 三层穿透追踪:对核心 API ≥5 条完成 API入口→编排层→服务执行层 全链路追踪 +- 构建 N×N 组件关系矩阵(标注通信方式:RPC/MQ/DB) + +### Step 4:生成架构分析报告 + +写入 `_review/k1-architecture-map.md`: + +```markdown +## 架构分层(≥4层) +| 层级 | 组件列表 | 核心职责 | 代码仓库 | + +## 组件清单 +| 组件名 | 架构层级 | **所属仓库** | 语言 | 核心度(P0/P1/P2) | 入口文件 | **接口校验类型** | + +接口校验类型取值(在确认点①请用户核对此列): + - `HTTP` → API 接入层,有 HTTP/gRPC 路由注册,需做接口数对账 + - `MQ` → 消息处理层,有 MQ Consumer/Exchange 声明,以 Topic 数做基准 + - `RPC` → 内部服务层,有 .proto / .thrift / IDL 文件,以 Method 数做基准 + - `NONE` → 调度/执行/数据层,无对外接口,不做接口数校验 + +## N×N 组件通信矩阵 +(值:RPC/MQ/DB/—,标注置信度 [E]EXTRACTED/[I]INFERRED/[A]AMBIGUOUS) + +## 核心调用链路(≥5条) +(格式:API(file:line) → 编排层(config:line) → 服务层(handler:line) → DB(table)) + +## 术语表 +| 内部术语 | 外部/产品术语 | 说明 | + +## 不确定项(供人工确认) +(标注 [A] 的关系和推断,说明不确定原因) +(接口校验类型不确定的组件,标注 [?] 等用户在确认点①明确) +``` + +### Step 5:接口清单扫描(按校验类型分别执行) + +**仅对 k1-architecture-map.md 中接口校验类型 ≠ NONE 的组件执行**: + +``` +FOR 每个 接口校验类型 = HTTP 的组件: + 执行 grep 扫描: + Go: grep -rn "\.GET\|\.POST\|\.PUT\|\.DELETE\|router\.Handle\|@handler" + Python: grep -rn "@app\.route\|@router\.\|APIRouter\|include_router" + 记录:组件名 → HTTP接口数 N(SCAN_CONFIDENCE: HIGH/MEDIUM) + +FOR 每个 接口校验类型 = MQ 的组件: + 执行 grep 扫描: + grep -rn "Exchange\|Queue\|Topic\|consumer\|subscribe\|@KafkaListener" + 记录:组件名 → MQ Topic/Queue 数 N + +FOR 每个 接口校验类型 = RPC 的组件: + 解析 .proto / .thrift 文件: + find -name "*.proto" -o -name "*.thrift" | xargs grep "^rpc\|^service" + 记录:组件名 → RPC Method 数 N +``` + +结果写入 `_review/interface-inventory.json`: +```json +{ + "ComponentA": {"type": "HTTP", "count": 13, "confidence": "HIGH"}, + "ComponentB": {"type": "MQ", "count": 5, "confidence": "MEDIUM"}, + "ComponentC": {"type": "RPC", "count": 8, "confidence": "HIGH"}, + "ComponentD": {"type": "NONE", "count": 0, "confidence": "—"} +} +``` + +**完成后**:更新 `current_phase` 为 `"phasek1_waiting_confirm"`。 + +**⛔ 确认点①** — 等待用户明确回复,不得自动进入下一阶段。 + +展示给用户: +``` +架构分析完成。 + +组件清单(共 N 个): + P0 核心: [列表] + P1 重要: [列表] + P2 辅助: [列表] + +接口扫描结果(供校验用): + HTTP 接口:ComponentA 13个, ComponentB 7个 + MQ Topic: ComponentC 5个 + RPC Method:ComponentD 8个 + 无接口组件:ComponentE, ComponentF, ... + +AMBIGUOUS 关系(请明确): + - ComponentX → ComponentY 的通信方式不确定 + +请确认(直接编辑 k1-architecture-map.md 后回复"继续"): + 1. 架构分层和 P0/P1/P2 标注是否正确? + 2. 每个组件的接口校验类型(HTTP/MQ/RPC/NONE)是否准确? + 3. 接口扫描数量是否合理?明显偏少说明有遗漏,偏多可能扫到了测试文件。 +``` + +确认后:更新 `"phasek1_confirmed"` → Phase K2。 + +--- + +## Phase K2:文档生成(分批并行 + 中间质量确认) + +**方法论**:`references/methodology/phase2-document-types.md` + +### 生成顺序(依赖链驱动,底层先写) + +``` +批次1: 数据层 + 基础执行层 Type-4 组件文档 ← 并行 +批次2: 资源/调度层 Type-4 组件文档 ← 并行 +批次3: 消息/服务层 Type-4 组件文档 ← 并行 +批次4: API入口层 Type-4 组件文档 ← 并行 + ⛔ 确认点② ← 人工抽查组件文档质量 +批次5: 架构总览层 (Type-1 + Type-2 + Type-3) ← 串行(依赖上层全部完成) +批次6: 桥梁文档 (Type-5 + Type-6 + Type-7) ← 串行(依赖产品文档) +批次7: 知识增强 (Type-8: 反模式/RPC契约/排障) ← 串行 +``` + +### 每批执行流程 + +读取 `references/agents/kb-doc-generator.md`,拼装输入包并启动: + +``` +component_list: 本批次组件/文档类型列表 +architecture_map: _review/k1-architecture-map.md 完整内容 +repos: _review/repo-manifest.json 中的仓库列表 +service_map: progress.json 中的 service_map +output_dir: +project_name: +product_docs_dir: +methodology_dir: references/methodology/ +completed_docs: kb_progress.components_done(断点恢复跳过) +parallel_mode: true(批次1~4)/ false(批次5~7) +``` + +每批完成后: +- 将完成组件追加到 `kb_progress.components_done` +- 累加 `accuracy_stats`(从 Agent 返回的自校验摘要中提取) +- 更新 `current_phase` 为 `"phasek2_batch_N"` +- 展示本批次 token 消耗和 `[UNVERIFIED]` 统计 + +### ⛔ 确认点②(批次1~4完成后) + +展示给用户: +``` +已生成 {N} 份组件设计文档。准确性统计: + 总声明数: {N} | 已验证: {N} | [UNVERIFIED]: {N}({X}%) + AMBIGUOUS 关系: {N} 条 + +请抽查 2~3 份文档(建议选最复杂的组件): + 路径:/XX_<组件名>设计说明.md + +确认要点: + 1. AI 快速理解表的代码入口是否精确到函数名? + 2. 核心流程描述是否与代码实际一致? + 3. [UNVERIFIED] 比例是否可接受?(建议 <15%) + +如发现系统性问题,请描述,我将调整策略后重新生成。 +``` + +更新 `current_phase` 为 `"phasek2_waiting_confirm"`。 +用户确认后更新为 `"phasek2_confirmed"`,继续批次5~7。 + +### 全部批次完成后 + +写入 `_review/k2-doc-list.md`(文档清单:路径 + 规模KB + [UNVERIFIED]数 + 生成时间)。 +更新 `current_phase` 为 `"phasek2_done"` → Phase K3。 + +--- + +## Phase K3:AI-Native 增强 + 图谱文档集 + +**方法论**:`references/methodology/phase3-ai-enhancement.md` + +### Step 1:AI-Native 元素注入 + +对所有已生成文档补充(如 Phase K2 的 Agent 未完整添加): + +| 元素 | 要求 | 适用范围 | +|------|------|---------| +| `search-anchor` | 5~15 个关键词,标题后第一行 | 所有文档 | +| AI 快速理解表 | 10 维度,紧跟标题 | 所有 Type-4 组件文档 | +| 双向链接 | 组件↔主架构,桥梁↔组件 | 所有文档 | +| 检索路由规则 | 4条分流规则 + 4级优先级 | 仅技术架构总览 | +| QA 对 | 10~20 个高频问题+答案引用 | 仅技术架构总览第9章 | + +### Step 2:Graph RAG 图谱文档集 + +读取 `references/agents/graph-rag-agent.md`,拼装输入包并启动: + +``` +all_kb_docs_dir: +architecture_map: _review/k1-architecture-map.md +doc_list: _review/k2-doc-list.md +project_name: +output_dir: /graph/ +methodology_file: references/methodology/phase2-document-types.md +``` + +生成 G1~G9(每条关系强制置信度三态标注): + +| 图谱文档 | 解决的问题 | 置信度要求 | +|---------|---------|-----------| +| G1 组件依赖关系矩阵 | "谁依赖 X?" | EXTRACTED 来自文档明确描述 | +| G2 调用链路全景 + 状态机 + 约束矩阵 | "API 经过哪些模块?" | 调用链 EXTRACTED,推断依赖 INFERRED | +| G3 数据流与存储依赖图 | "数据存哪里?" | 读写关系 EXTRACTED | +| G4 错误码组件映射表 | "错误码是哪个模块的?" | EXTRACTED | +| G5 跨组件交互场景手册(≥10个时序图) | "配额检查怎么做?" | 时序 EXTRACTED,边界 INFERRED | +| G6 知识图谱三元组(≥100条) | "A 间接依赖谁?" | 每条标 E/I/A + 分值 | +| G7 架构风险与影响面分析 | "X 挂了影响多大?" | 直接依赖 EXTRACTED,间接 INFERRED | +| G8 核心配置参数索引 | "怎么改 XX 配置?" | EXTRACTED 来自配置文件 | +| G9 业务规则约束矩阵 + AI 推理决策树 | "能不能做 XX?" | 规则 EXTRACTED,推断 INFERRED | + +同时生成 `/graph/README.md`(索引 + 按问题类型查找表 + 检索路由建议)。 + +### Step 3:跨文档一致性校验 + +**Graph RAG Agent 完成后,主 Agent 自行执行此步骤(不委托给子 Agent)。** + +目的:检测组件文档之间的矛盾描述,防止"A 说调用 B 用 RPC,B 说被 A 用 MQ 调用"这类不一致。 + +``` +Step 3A:构建"声称矩阵" + + 对每份 Type-4 组件文档,从**两个层面**提取关系声称: + + 层面1:AI 快速理解表中的"上游组件"和"下游组件"字段 + 层面2:正文中的接口设计章节、核心流程章节中的调用描述 + + 如果层面1和层面2对同一关系描述不一致 → 首先记录为"文档内矛盾"(比表头和正文优先级更高的问题) + + 提取示例: + 组件X.md 表头声称: X→Y(RPC), X→Z(MQ) + 组件X.md 正文声称: X→Z(HTTP) ← 与表头矛盾! + 组件Y.md 表头声称: Y←X(RPC), Y→Z(DB) + 组件Z.md 表头声称: Z←X(HTTP), Z←Y(DB) + +Step 3B:交叉比对 + + FOR 每对组件 (A, B): + IF A.md 声称 "A→B 用 RPC" AND B.md 声称 "B←A 用 MQ": + → 记录矛盾: "A→B 通信方式不一致: A说RPC, B说MQ" + IF A.md 声称 "A→B" BUT B.md 未提到 "被A调用": + → 记录缺失: "A声称调用B,但B的文档未提及被A调用" + IF G1矩阵中的关系 与 组件文档声称不一致: + → 记录偏差: "G1矩阵说A→B(RPC),但A的文档说A→B(MQ)" + +Step 3C:生成一致性报告 + + 写入 `_review/k3-consistency-check.md`: + + ```markdown + # 跨文档一致性校验报告 + + ## 矛盾项(必须修复) + | 组件A | 组件B | A的描述 | B的描述 | 矛盾类型 | + |-------|-------|---------|---------|---------| + | X | Z | X→Z(MQ) | Z←X(HTTP) | 通信方式不一致 | + + ## 缺失项(建议补充) + | 声称方 | 被引用方 | 声称内容 | 缺失 | + |--------|---------|---------|------| + | A | B | A→B(RPC) | B的文档未提及被A调用 | + + ## G1矩阵偏差(建议对齐) + | G1矩阵 | 组件文档 | 偏差 | + + ## 统计 + - 矛盾项: N 处(❌ 需修复) + - 缺失项: N 处(⚠️ 建议补充) + - G1偏差: N 处(⚠️ 需对齐) + - 一致关系: N 条(✅) + - 一致率: X% + ``` + +Step 3D:自动修复(仅限明确情况) + + IF 矛盾项 > 0: + FOR 每个矛盾项: + 回溯代码验证:用 Grep 查找实际的调用方式(如 rpc.Call / mq.Publish) + IF 能明确正确方 → 修复错误方文档中的描述 + 更新 G1 矩阵 + IF 无法明确 → 标记为 AMBIGUOUS,留待用户在确认点确认 + 修复后重新统计一致率 + + IF 矛盾项 = 0: + → 跳过修复,直接进入 Phase K4 +``` + +**完成后**:更新 `current_phase` 为 `"phasek3_done"` → Phase K4。 + +--- + +## Phase K4:知识库质量评估与报告 + +**方法论**:`references/methodology/phase4-quality.md` + +### Step 1:自动校验 + +```bash +python3 scripts/validate_kb.py +``` + +输出(**必须完整展示,不得只展示通过项**): +``` +链接完整性: ✅/❌ N 个死链接 +search-anchor: ✅/⚠️ 覆盖率 N/M (X%) +AI 快速理解表: ✅/⚠️ 覆盖率 N/M (X%) +双向链接: ✅/⚠️ 覆盖率 N/M (X%) +README 索引: ✅/⚠️ 收录率 N/M (X%) +``` + +### Step 2:准确性审计 + +从 `accuracy_stats` 汇总全库可信度,同时从 `interface_coverage` 汇总接口覆盖情况: + +``` +【内容准确性】 +总声明数: N 条(业务规则 + 接口描述 + 关系) +已验证(有代码引用): N 条 (X%) +[UNVERIFIED]: N 条 (X%) +AMBIGUOUS 关系: N 条 (X%) + +【接口覆盖率】(仅统计 HTTP/MQ/RPC 类型组件,NONE 类型不计入) +HTTP 接口: 文档记录 M 个 / 扫描基准 N 个 = X% +MQ Topic: 文档记录 M 个 / 扫描基准 N 个 = X% +RPC Method: 文档记录 M 个 / 扫描基准 N 个 = X% +综合覆盖率: X% 目标 ≥ 90% + +⚠️ 接口缺口清单(文档记录 < 扫描基准 的组件): + - ComponentA: 文档记录 8 个,扫描基准 13 个,缺口 5 个 → 建议补充 +``` + +⚠️ 需人工确认清单:([UNVERIFIED] > 20% 的文档 + 接口缺口组件 + AMBIGUOUS 关系) + +### Step 3:RAG 检索抽检 + +按 `phase4-quality.md §RAG检索测试用例` 测试 7 类问题各 1 个(详见方法论),记录命中率。 + +### Step 4:AI 端到端验证(E2E Validation) + +**核心思路**:用知识库回答一组标准化问题,然后**回溯代码验证答案正确性**,检测知识库是否能让 AI 给出正确答案。 + +``` +Step 4A:生成标准验证问题集(自动,基于已有文档) + + **优先使用用户提供的外部验证集**: + IF 用户在 Phase 0 或此时提供了验证问题列表(3~10 个真实业务问题): + → 优先使用用户问题作为验证集(标注来源: USER) + → 自动补充至 10~15 题(标注来源: AUTO) + ELSE: + → 全部自动生成(标注来源: AUTO) + + > 用户提供的问题更有价值,因为 AI 自己出题容易考自己已知的领域, + > 真正的盲区(AI 没理解但没意识到的)只有外部问题才能测到。 + + 从 k1-architecture-map.md 和 k2-doc-list.md 自动生成 10~15 个验证问题: + + 问题类型分布(至少覆盖以下 5 类): + + ┌────────────────────────────────────────────────────────────────────┐ + │ 类型1:组件职责(3题) │ + │ 模式:"<组件名> 的核心职责是什么?代码入口在哪?" │ + │ 验证方式:答案中的函数名/文件名必须在代码中存在 │ + │ │ + │ 类型2:调用关系(3题) │ + │ 模式:"<组件A> 和 <组件B> 之间是什么关系?通过什么方式通信?" │ + │ 验证方式:答案与 G1 矩阵 + 代码实际 import/call 一致 │ + │ │ + │ 类型3:操作约束(2题) │ + │ 模式:"在 <状态X> 下能否执行 <操作Y>?" │ + │ 验证方式:答案与 G9 约束矩阵 + 代码中的状态检查一致 │ + │ │ + │ 类型4:数据流向(2题) │ + │ 模式:"<操作Z> 最终会写入哪些表/队列?" │ + │ 验证方式:答案与 G3 数据流 + 代码实际 SQL/MQ 操作一致 │ + │ │ + │ 类型5:错误排查(2题) │ + │ 模式:"错误码 是什么意思?在哪个组件产生?" │ + │ 验证方式:答案与 G4 错误码映射 + 代码中的错误定义一致 │ + │ │ + │ 类型6(可选):认知边界测试(2题) │ + │ 模式:故意问知识库不覆盖的内容(如第三方 SDK 内部、历史架构变迁) │ + │ 验证方式:AI 应回答"超出知识库覆盖范围"而非幻觉 │ + └────────────────────────────────────────────────────────────────────┘ + +Step 4B:用知识库回答(模拟 AI 使用场景) + + FOR 每个验证问题: + 1. 假设只能读知识库文档,不能直接读代码 + 2. 按检索路由规则,找到对应文档 + 3. 从文档中提取答案 + +Step 4C:代码回溯验证 + + FOR 每个答案: + 1. 用 Grep/Read 直接在代码中验证关键声明 + 2. 判定结果: + ✅ CORRECT — 答案与代码一致 + ⚠️ PARTIAL — 答案部分正确,有遗漏或不精确 + ❌ INCORRECT — 答案与代码矛盾 + 🔇 BOUNDARY_OK — 认知边界问题,正确拒绝回答(仅类型6) + 🔇 BOUNDARY_FAIL — 认知边界问题,错误地给出了答案(仅类型6) + +Step 4D:写入验证报告 + + 追加到 k4-quality-report.md 的 ## AI 端到端验证 章节: + + | 问题 | 类型 | 检索文档 | AI答案摘要 | 代码验证 | 结果 | + |------|------|---------|-----------|---------|------| + | Aurora 核心职责? | 组件职责 | 03_Aurora设计说明.md | 调度编排... | scheduler.go:42 | ✅ | + | A→B 通信方式? | 调用关系 | G1矩阵 | RPC | import rpc_client | ✅ | + | 状态X下能否操作Y? | 操作约束 | G9矩阵 | 不能 | check_state.go:88 | ✅ | + | 第三方SDK内部? | 认知边界 | — | 超出范围 | — | 🔇 OK | + + 统计: + CORRECT: N/M (X%) + PARTIAL: N/M (X%) + INCORRECT: N/M (X%) — ❌ 每个 INCORRECT 必须列出具体矛盾点 + BOUNDARY_OK: N/N + BOUNDARY_FAIL: N/N + + E2E 准确率 = (CORRECT + BOUNDARY_OK) / 总题数 + 目标: ≥ 80% +``` + +**如果 E2E 准确率 < 80%**:在质量报告"建议"章节列出需要改进的文档和具体问题。 + +### Step 5:生成质量报告 + +写入 `_review/k4-quality-report.md`: + +```markdown +# 知识库质量报告 + +## 概览 +- 代码基准: () +- 生成时间: +- 文档总数:N 份(Type-1~8: N份,图谱G1~G9: 9份) + +## 准确性 +| 指标 | 数值 | 状态 | +| 总声明数 | N | — | +| 有代码引用 | N (X%) | ✅/❌ | +| [UNVERIFIED] | N (X%) | ✅/<15% / ⚠️15~25% / ❌>25% | +| AMBIGUOUS关系 | N | ✅/⚠️ | + +## 结构质量(validate_kb.py 输出) +(完整展示,不隐藏任何数字) + +## 跨文档一致性(k3-consistency-check.md 摘要) +| 指标 | 数值 | 状态 | +| 矛盾项 | N | ✅=0 / ❌>0 | +| 缺失引用 | N | ⚠️ | +| G1偏差 | N | ⚠️ | +| 一致率 | X% | 目标≥95% | + +## RAG 检索抽检 +| 测试问题 | 期望命中 | 实际命中 | 结果 | + +## AI 端到端验证 +| 指标 | 数值 | 状态 | +| CORRECT | N/M (X%) | — | +| PARTIAL | N/M (X%) | ⚠️ | +| INCORRECT | N/M (X%) | ❌ | +| BOUNDARY_OK | N/N | ✅ | +| E2E 准确率 | X% | 目标≥80% | + +INCORRECT 详情: +(每个 INCORRECT 的具体矛盾点和改进建议) + +## 待人工确认清单 +([UNVERIFIED] 超标文档 + AMBIGUOUS 关系 + 矛盾项 + 死链接) + +## 建议 +(基于一致性校验 + E2E 验证的改进方向) +``` + +**完成后**:更新 `current_phase` 为 `"completed"`,流程结束。 + +--- + +## 输出目录结构 + +``` +/ +├── README.md ← 知识库索引 + 检索路由规则 + 认知边界声明(AI 专用) +├── {项目名} 技术架构.md ← [Type-1] 架构总览(目标 ≤80KB,超过则自动拆分) +├── {项目名} 技术架构-核心链路.md ← [Type-1b] 仅当 Type-1 超 80KB 时拆出 +├── {项目名} 技术架构-AI元数据.md ← [Type-1c] 仅当 Type-1 超 80KB 时拆出 +├── {项目名} 业务架构.md ← [Type-2] 产品能力 + 生命周期 ~70KB +├── {项目名} 部署架构.md ← [Type-3] 部署拓扑 ~40KB +├── XX_{组件名}设计说明.md × N ← [Type-4] 每份 20~100KB +├── XX_{项目名}核心API产品代码映射.md ← [Type-5] 仅有产品文档时生成 +├── XX_{项目名}产品规则速查表.md ← [Type-6] +├── XX_{项目名}业务开发规范SOP.md ← [Type-7] +├── {知识增强文档} × N ← [Type-8] 反模式/RPC契约/排障/知识文库 +└── graph/ ← [Type-9] Graph RAG 图谱文档集 + ├── README.md ← 图谱索引 + 按问题类型查找 + ├── G1_{项目名}组件依赖关系矩阵.md + ├── G2_{项目名}组件调用链路全景.md + ├── G3_{项目名}数据流与存储依赖图.md + ├── G4_{项目名}错误码组件映射表.md + ├── G5_{项目名}跨组件交互场景手册.md + ├── G6_{项目名}知识图谱三元组.md + ├── G7_{项目名}架构风险与影响面分析.md + ├── G8_{项目名}核心配置参数索引.md + └── G9_{项目名}业务规则约束矩阵.md + +_review/ ← 过程文件(不入知识库) +├── progress.json ← 断点续传 + 增量更新状态 +├── metadata.json ← 代码基准版本 +├── interface-inventory.json ← 接口扫描基准(Phase K1 Step 5) +├── k1-architecture-map.md ← 架构逆向结果(用户确认过) +├── k2-doc-list.md ← 文档清单 + 准确性统计 +├── k3-consistency-check.md ← 跨文档一致性校验报告(Phase K3 Step 3) +└── k4-quality-report.md ← 质量报告(含 E2E 验证结果) +``` + +--- + +## 阶段间控制 + +| 用户回复 | 行为 | +|---------|------| +| "继续" / "continue" / "ok" | 进入下一阶段 | +| "停止" / "stop" | 停止,已生成文件保持可用 | +| 直接描述问题 | 调整后重新确认,再继续 | +| 直接编辑文件后回复"继续" | 以修改后文件内容为准继续 | + +--- + +## 约束 + +- **主 Agent 不执行代码分析**:全部由专职 Agent 完成;启动前必须先 Read 对应 agent 文件 +- **严禁冗余输出**:生成文件直接 Write,禁止先在对话中打印完整内容 +- **组件文档命名**:`XX_{组件名}设计说明.md`(XX 为两位数编号,按依赖链顺序分配,底层组件编号小) +- **无产品文档时**:Type-5/6 可跳过或将约束值标注为 `[PRODUCT_DOC_MISSING]`,不得推测 +- **并行模式**:Type-4 批次必须同一消息并发发出所有 Agent calls;串行批次顺序执行 + +### 诚实审计规则(Honesty Rules) + +- **禁止凭空发明**:图谱每条关系必须有组件文档明确依据,不得基于名称猜测 +- **置信度不得伪造**:EXTRACTED=1.0,INFERRED 按证据强度 0.4~0.9,AMBIGUOUS 0.1~0.3;禁用 0.5 默认值 +- **[UNVERIFIED] 不得隐藏**:超过 20% 则文档顶部加可见警告 +- **质量数字完整展示**:validate_kb.py 输出不得只展示通过项 +- **token 成本透明**:每批完成后展示读取文件数和估计 token 消耗 +- **不确定优先 AMBIGUOUS**:宁可标注待确认,也不删除或假装确定 + +--- + +## 与 Team Wiki CLI 的配合(必读) + +| 阶段 | 命令 / 路径 | +|------|-------------| +| Phase 0 结构基线 | `team-wiki compile code --extract ast,heuristic --write` | +| K3 后编译进 wiki | `team-wiki compile code --write`(检测 `_manifest.json` → manifest 快路径) | +| 产品文档入图 | `team-wiki compile docs --extract structure,entity --write` | +| 产品↔代码桥接 | `team-wiki reconcile --write` | +| 一键刷新 | `team-wiki refresh --repo [--docs ] --extract-code ast,heuristic --write` | +| 质量评估 | `team-wiki evaluate `(含 `graph.structuralEdgeRatio` 等) | + +**路径约定**(本 skill 安装后): + +- 方法论:`references/methodology/*.md`(相对本 skill 目录) +- Agent:`references/agents/kb-doc-generator.md`、`references/agents/graph-rag-agent.md` +- 脚本:`scripts/scan_repo.py`、`scripts/validate_kb.py` + +所有流程在本 skill(`references/`、`scripts/`)与 `team-wiki` CLI 内完成。 diff --git a/skills/team-wiki-codebase/references/agents/graph-rag-agent.md b/skills/team-wiki-codebase/references/agents/graph-rag-agent.md new file mode 100644 index 0000000..e896eed --- /dev/null +++ b/skills/team-wiki-codebase/references/agents/graph-rag-agent.md @@ -0,0 +1,344 @@ +# Graph RAG Agent + +## 职责 + +从已生成的知识库组件文档中抽取跨组件关系信息,生成结构化图谱文档集(G1~G9),解决 RAG 检索在"跨组件关系查询"场景下的信息分散问题。 + +**此 Agent 在 Phase K3 中被主 Agent 单次串行启动。** + +## 输入包 + +``` +all_kb_docs_dir: 知识库输出根目录(包含所有 Type-1~8 文档) +architecture_map: _review/k1-architecture-map.md 完整内容 +doc_list: _review/k2-doc-list.md(文档清单) +project_name: 项目名称(用于文档命名) +output_dir: 图谱文档输出目录(/graph/) +methodology_file: references/methodology/phase2-document-types.md §Type-9 内容 +``` + +## 执行步骤 + +### Step 1:关系抽取 + +扫描 `all_kb_docs_dir` 下所有组件文档(Type-4),从 AI 快速理解表和正文中提取: + +``` +扫描维度: +├── 调用关系 (上游组件→本组件, 本组件→下游组件, 通信方式) +├── 存储依赖 (读写了哪些 DB/Redis/MQ) +├── 消息拓扑 (发布/消费的 Exchange/Topic/Queue/RoutingKey) +├── 状态流转 (操作→起始状态→中间状态→终态, 状态字段值) +├── 约束条件 (操作→前置状态要求→硬件约束→计费约束→配额) +├── 配置映射 (配置项→影响行为→变更风险) +└── 错误码归属 (错误码段→组件→排查方向) +``` + +**置信度三态标注**(每条关系/三元组必须标注,不得省略): + +| 标签 | 含义 | 来源依据 | 置信度分值 | +|------|------|---------|-----------| +| `EXTRACTED` | 组件文档中明确描述的关系(如"上游组件: Aurora(RPC)")| 代码/文档显式记录 | 1.0 | +| `INFERRED` | 合理推断的关系(如架构图中隐含的依赖链)| 结构性证据 + 合理推断 | 0.6~0.9 | +| `AMBIGUOUS` | 存在不确定性的关系,需人工确认 | 弱证据或相互矛盾 | 0.1~0.3 | + +> ⚠️ **禁止用 0.5 作为默认分值**。每条关系都要独立评估:有直接代码引用的 INFERRED 用 0.8~0.9,仅靠命名推断的用 0.6~0.7,真正模糊的才用 AMBIGUOUS。 + +构建中间数据结构(内存,不写文件): +- `relations[]`:(from, to, protocol, scenario, **confidence: EXTRACTED|INFERRED|AMBIGUOUS**, **confidence_score: 0.1~1.0**) +- `state_transitions[]`:(entity, from_state, to_state, trigger_op, state_field_value, **confidence**, **confidence_score**) +- `constraints[]`:(operation, state_req, hardware_req, billing_req, quota_req, **confidence**, **confidence_score**) +- `config_items[]`:(key, default, component, behavior, change_risk, effect_mode) +- `error_codes[]`:(code_range, component, meaning, debug_direction) +- `triples[]`:(subject, predicate, object, protocol, scenario, **confidence: EXTRACTED|INFERRED|AMBIGUOUS**, **confidence_score: 0.1~1.0**) + +### Step 2:逐份生成图谱文档 + +按顺序生成 G1~G9(串行,每份完成后立即 Write): + +--- + +#### G1:组件依赖关系矩阵 + +```markdown +# {project_name} 组件依赖关系矩阵 + +## 🤖 AI 快速理解要点 +| 文档定位 | 解决"谁依赖 X?X 依赖谁?"的检索问题 | +| 核心价值 | N×N 通信矩阵 + 正向/反向依赖索引 | +| 使用场景 | 变更影响评估、服务依赖梳理、架构重构规划 | + +## N×N 组件通信矩阵 +(行:调用方,列:被调方,值:`RPC`/`MQ`/`DB`/`—`,括号内标注置信度标签) +示例:`RPC[E]` = EXTRACTED,`MQ[I:0.8]` = INFERRED 0.8,`RPC[A]` = AMBIGUOUS + +## 正向依赖索引(A 依赖谁) +| 组件 | 依赖组件 | 通信方式 | 置信度 | 典型场景 | + +## 反向依赖索引(谁依赖 A) +| 组件 | 被依赖来自 | 通信方式 | 置信度 | 典型场景 | + +## 外部服务依赖 +| 外部服务 | 被哪些组件依赖 | 通信方式 | 置信度 | 降级策略 | + +## 置信度统计 +| 标签 | 条数 | 说明 | +|------|------|------| +| EXTRACTED | N | 来自代码/文档直接描述 | +| INFERRED | N | 合理推断,标注分值 0.6~0.9 | +| AMBIGUOUS | N | 不确定,需人工确认 | +``` + +--- + +#### G2:组件调用链路全景 + 状态机 + +```markdown +# {project_name} 组件调用链路全景与状态机 + +## 🤖 AI 快速理解要点 +| 文档定位 | 解决"API X 经过哪些模块?实体状态如何流转?"的检索问题 | +| 核心价值 | 核心API端到端链路 + 完整状态机 + 操作-状态约束矩阵 | + +## 核心 API 端到端调用链路 +(对每个核心 API,用标准调用链格式 + mermaid 时序图) + +## 核心实体完整状态机 +(mermaid stateDiagram-v2,标注状态字段值和触发操作) + +## 操作-状态约束速查矩阵 +| 操作 \ 当前状态 | 状态A | 状态B | ... | +(✅ 允许 / ❌ 禁止 / ⚠️ 有条件) + +## AI 状态判断推理规则 +(mermaid graph TD 决策树) +``` + +--- + +#### G3:数据流与存储依赖图 + +```markdown +# {project_name} 数据流与存储依赖图 + +## 存储系统依赖矩阵 +| 组件 | MySQL | Redis | MQ | 对象存储 | 其他 | + +## MQ 队列拓扑 +| Exchange/Topic | Routing Key | 生产者 | 消费者 | 消息含义 | + +## 缓存策略矩阵 +| 组件 | 缓存键模式 | 过期时间 | 失效策略 | +``` + +--- + +#### G4:错误码组件映射表 + +```markdown +# {project_name} 错误码组件映射表 + +## 错误码段分配 +| 错误码范围/前缀 | 归属组件 | 含义范围 | + +## 外部→内部错误码映射 +| 外部错误码 | 内部组件 | 内部含义 | 排查方向 | +``` + +--- + +#### G5:跨组件交互场景手册 + +对每个核心业务场景,生成: +```markdown +## 场景N:{场景名称} + +```mermaid +sequenceDiagram + actor User + participant A as {组件A} + participant B as {组件B} + ... +``` +**正常流程**:步骤描述 +**异常处理**:各异常分支 +``` + +要求:≥10 个场景,覆盖主要写操作和关键读操作。 + +--- + +#### G6:知识图谱三元组 + +```markdown +# {project_name} 知识图谱三元组 + + +## Ontology 定义 +### 实体类型: Service, Handler, Config, Table, Queue, API, ErrorCode +### 关系类型: CALLS, PUBLISHES, CONSUMES, READS, WRITES, CONFIGURES, MAPS_TO + +## 显式三元组(≥100条) +| Subject | Predicate | Object | Protocol/Scenario | Confidence | Score | + +> 每条三元组的 Confidence 必须是 `EXTRACTED` / `INFERRED` / `AMBIGUOUS`,Score 不得省略,不得用 0.5 作默认值。 + +## 多跳依赖路径索引 +| 查询模式 | 路径示例 | +| "A 最终写入哪些表?" | A→(CALLS)→B→(WRITES)→Table | + +## 反向可达索引 +| 目标节点 | 可达路径 | +``` + +--- + +#### G7:架构风险与影响面分析 + +```markdown +# {project_name} 架构风险与影响面分析 + +## 组件风险等级总表 +| 组件 | 风险等级 | 爆炸半径 | 备注 | +(🔴高/🟡中/🟢低) + +## 关键组件爆炸半径分析(≥3个高风险组件) +组件 X 故障时的影响链路分析 + +## 关键路径与瓶颈识别 +## 聚类分析(哪些组件形成强耦合簇) +## 变更风险评估矩阵 +``` + +--- + +#### G8:核心配置参数索引 + +```markdown +# {project_name} 核心配置参数索引 + +## 分层配置架构图(mermaid) + +## 各层配置参数表 +| 配置项 | 所属组件 | 默认值 | 影响行为 | 变更风险 | 生效方式 | +(变更风险: 🟢低/🟡中/🔴高;生效方式: 热生效/需重启) + +## 配置变更影响面速查 +| 变更类型 | 影响范围 | 生效方式 | 回滚策略 | + +## AI 回答"怎么修改 XX 配置"时必须同时告知: +1. 配置文件位置 +2. 影响范围 +3. 生效方式 +4. 回滚策略 +5. 变更风险 +6. 是否需要灰度 +``` + +--- + +#### G9:业务规则约束矩阵 + +```markdown +# {project_name} 业务规则约束矩阵 + +## 操作前置条件矩阵 +| 操作 | 状态要求 | 硬件约束 | 计费约束 | 配额约束 | 其他约束 | + +## 约束决策树(mermaid graph TD) +(覆盖主要操作的多层约束检查流程) + +## 特殊实例类型约束汇总 +| 实例/资源类型 | 限制操作 | 原因 | +(✅允许 / ❌禁止 / ⚠️有条件) + +## AI 推理规则速查 +(mermaid 流程图:AI 判断"某操作能否执行"时的逐层检查顺序) +``` + +--- + +### Step 3:生成图谱目录 README + +写入 `{output_dir}/README.md`: +```markdown +# {project_name} 图谱文档集 (Graph RAG) + + +## 与主文档体系的关系 +(图谱文档不替代组件文档,而是提供关系视角的结构化索引) + +## 文档目录 +| 文件 | 大小 | 核心内容 | + +## 按问题类型查找 +| 问题类型 | 示例问题 | 查找文档 | +| 依赖关系 | "谁依赖 X?" | G1 组件依赖关系矩阵 | +| 调用链路 | "API X 经过哪些模块?" | G2 调用链路全景 | +| 数据位置 | "数据存在哪里?" | G3 数据流与存储依赖图 | +| 错误排查 | "错误码 XXX 是哪个模块的?" | G4 错误码组件映射表 | +| 场景手册 | "配额检查的完整流程?" | G5 跨组件交互场景手册 | +| 多跳推理 | "A 间接依赖谁?" | G6 知识图谱三元组 | +| 风险评估 | "X 挂了影响多大?" | G7 架构风险与影响面 | +| 配置修改 | "怎么修改 XX 配置?" | G8 核心配置参数索引 | +| 操作约束 | "能不能做 XX?" | G9 业务规则约束矩阵 | + +## 检索路由规则建议 +(关键词 → 优先检索文档) + +## 维护说明 +(组件文档更新后需同步更新图谱文档的时机和范围) +``` + +### Step 4:返回摘要 + +``` +Graph RAG 生成完成: +生成文档: G1~G9 共 9 份 + README + - G1_组件依赖关系矩阵.md: {N}KB,{N}个组件,{N}条关系 + 置信度: EXTRACTED {N} / INFERRED {N} / AMBIGUOUS {N} + - G2_组件调用链路全景.md: {N}KB,{N}条调用链,状态机{N}个状态 + - G3_数据流与存储依赖图.md: {N}KB + - G4_错误码组件映射表.md: {N}KB,{N}段错误码 + - G5_跨组件交互场景手册.md: {N}KB,{N}个场景时序图 + - G6_知识图谱三元组.md: {N}KB,{N}条三元组 + 置信度: EXTRACTED {N} / INFERRED {N} / AMBIGUOUS {N} + - G7_架构风险与影响面分析.md: {N}KB + - G8_核心配置参数索引.md: {N}KB,{N}个配置项 + - G9_业务规则约束矩阵.md: {N}KB +AMBIGUOUS 条目汇总(需人工确认): {N} 处 + - 示例: "Aurora→Compute 通信方式不确定(文档未明确)[A:0.2]" +发现问题: {问题 或 "无"} + +⚠️ 主 Agent 请注意:Graph RAG 完成后,请立即执行 Phase K3 Step 3(跨文档一致性校验)。 +``` + +## 输出 + +``` +/README.md +/G1_{project_name}组件依赖关系矩阵.md +/G2_{project_name}组件调用链路全景.md +/G3_{project_name}数据流与存储依赖图.md +/G4_{project_name}错误码组件映射表.md +/G5_{project_name}跨组件交互场景手册.md +/G6_{project_name}知识图谱三元组.md +/G7_{project_name}架构风险与影响面分析.md +/G8_{project_name}核心配置参数索引.md +/G9_{project_name}业务规则约束矩阵.md +返回摘要字符串 +``` + +## 约束 + +- **关系抽取以组件文档为唯一来源**:不直接读原始代码,防止与 Phase K2 产出不一致 +- **置信度三态强制**:每条关系/三元组必须标注 `EXTRACTED`/`INFERRED`/`AMBIGUOUS`,不得省略 +- **禁止用 0.5 作置信度默认值**:每条关系独立评估分值;INFERRED 直接结构证据 0.8~0.9,命名推断 0.6~0.7,弱证据 0.4~0.5;AMBIGUOUS 用 0.1~0.3 +- **禁止凭空发明关系**:若组件文档无依据,宁可标 AMBIGUOUS 也不捏造 EXTRACTED +- **每份图谱文档必须有 AI 快速理解要点表** +- **每份图谱文档必须有 search-anchor** +- **图谱文档不替代组件文档**:只提供关系视角的结构化索引 +- **状态机必须使用 mermaid stateDiagram-v2** +- **约束决策树必须使用 mermaid graph TD** +- **三元组必须遵循 (Subject, Predicate, Object, Confidence, Score) 格式** +- **操作-状态约束必须是 ✅/❌/⚠️ 矩阵格式** diff --git a/skills/team-wiki-codebase/references/agents/kb-doc-generator.md b/skills/team-wiki-codebase/references/agents/kb-doc-generator.md new file mode 100644 index 0000000..67ebade --- /dev/null +++ b/skills/team-wiki-codebase/references/agents/kb-doc-generator.md @@ -0,0 +1,323 @@ +# 知识库文档生成 Agent + +## 职责 + +为指定批次的组件/文档类型生成知识库文档,严格遵循九大文档类型规范,确保代码可回溯、AI 快速理解表完整、双向链接织网。 + +**此 Agent 在 Phase K2 中被主 Agent 逐批启动,支持并行子 Agent 分发模式。** + +## 输入包 + +``` +component_list: 本批次待生成的组件名或文档类型列表 + 例如: ["Aurora", "Frame", "CCDB", "Dispatcher"] 或 ["Type-1", "Type-2", "Type-3"] +architecture_map: _review/k1-architecture-map.md 完整内容 +repos: 仓库列表([{name, path, language}]),替代旧的 project_root +service_map: 服务名→仓库映射表(用于跨仓库追踪调用链) +output_dir: 知识库输出根目录 +project_name: 项目名称(用于文档命名,如 "CVM") +product_docs_dir: 产品文档目录(可为空,空则跳过产品约束提取) +methodology_dir: references/methodology/ 目录路径 +completed_docs: 已完成的文档列表(断点恢复时跳过) +parallel_mode: true | false(默认 true;Type-4 组件文档并行,Type-1~3/5~8 串行) +``` + +## 执行步骤 + +### Step 0:加载方法论 + +读取 `{methodology_dir}/phase2-document-types.md`,加载对应文档类型的模板和生成规则。 + +### Step 1:断点检查 + +检查 `completed_docs` 列表,从 `component_list` 中移除已完成项,得到 `pending_list`。 + +若 `pending_list` 为空,直接返回"全部已完成"摘要,不做任何操作。 + +### Step 2:分发策略决策 + +``` +IF component_list 全为 Type-4 组件文档 AND parallel_mode = true: + → 并行模式(Step 2A) +ELSE(Type-1/2/3/5/6/7/8 或 parallel_mode = false): + → 串行模式(Step 2B) +``` + +### Step 2A:并行模式(Type-4 组件文档) + +**MANDATORY:必须使用 Agent tool,禁止一个个顺序处理。** + +**Step 2A-1:分块** + +将 `pending_list` 分成若干块,每块 **3~5 个组件**(组件文档较大,不超过 5 个避免上下文溢出)。 +- 优先把同一架构层的组件放同一块(减少跨层代码读取竞争) +- 已完成的跳过(断点恢复) + +**Step 2A-2:同一条消息并发启动所有子 Agent** + +**在同一次回复中发出所有 Agent tool 调用**。这是并行的唯一方式——分开多次调用则退化为串行。 + +示例(3块并发): +``` +[Agent tool call 1: chunk ["Aurora", "Frame"], subagent_type="general-purpose"] +[Agent tool call 2: chunk ["CCDB", "VSResource"], subagent_type="general-purpose"] +[Agent tool call 3: chunk ["Dispatcher", "Compute"], subagent_type="general-purpose"] +``` + +每个子 Agent 接收以下 prompt(替换 CHUNK_COMPONENTS、CHUNK_NUM、TOTAL_CHUNKS): + +``` +你是 team-wiki-codebase 的组件文档生成子 Agent。 +为以下组件生成知识库文档(chunk CHUNK_NUM / TOTAL_CHUNKS): +CHUNK_COMPONENTS + +架构参考(精简版,仅含本 chunk 相关组件及其直接上下游): +RELEVANT_COMPONENTS_TABLE +(格式:| 组件名 | 架构层级 | 所属仓库 | 语言 | 上游 | 下游 | 入口文件 |) + +服务映射表(用于跨仓库追踪): +SERVICE_MAP_RELEVANT_ENTRIES + +项目信息: +- repos: REPO_LIST(仅列路径,不列详情) +- output_dir: OUTPUT_DIR +- project_name: PROJECT_NAME +- product_docs_dir: PRODUCT_DOCS_DIR(空则跳过产品约束) + +方法论路径: METHODOLOGY_DIR/phase2-document-types.md + +对每个组件执行: +1. 使用 Glob→Grep→Read 三步法扫描代码(参见 kb-doc-generator.md §Step 2:代码结构扫描规范) +2. 提取:核心职责/架构层级/上下游/代码入口/核心机制/数据流向/技术栈/数据模型/配置项 +3. 生成符合 Type-4 模板的文档,Write 到 OUTPUT_DIR/XX_组件名设计说明.md +4. 自校验(见下方 Checklist) +5. 将完成的组件名写入 OUTPUT_DIR/../_review/_chunk_done_CHUNK_NUM.txt(每行一个) + +自校验 Checklist(每份文档生成后): +- [ ] AI 快速理解表 10 维度全部填写且具体(非泛泛描述)? +- [ ] "代码入口"精确到函数名(不是仅文件名)? +- [ ] search-anchor 有 5~15 个关键词? +- [ ] 包含指向主架构文档的双向链接? +- [ ] 无法回溯的内容已标注 [UNVERIFIED]? +- [ ] 无空占位章节? + +[UNVERIFIED] 超过 20% → 文档顶部加 ⚠️ 低可信度警告。 + +无法生成的组件写入 OUTPUT_DIR/../_review/_chunk_failed_CHUNK_NUM.txt 并注明原因。 +``` + +**Step 2A-3:等待并收集结果** + +等待所有子 Agent 完成后: +- 检查 `_chunk_done_N.txt` 文件确认完成情况 +- 若某块 `_chunk_done_N.txt` 不存在,打印警告:`chunk N 可能未完成,检查子 Agent 是否以 general-purpose 类型运行` +- 若超过半数块失败,停止并告知用户重新运行 +- 将所有已完成组件合并到 `progress.json` 的 `kb_progress.components_done` +- 清理临时文件:`rm -f _review/_chunk_done_*.txt _review/_chunk_failed_*.txt` + +### Step 2B:串行模式(Type-1~3/5~8) + +对 `pending_list` 中每个文档类型**顺序执行**(这些文档类型相互依赖,必须串行): + +#### 2B-1:代码结构扫描规范 + +使用 `Glob → Grep → Read` 三步法(**按组件所属仓库的语言自适应**): + +``` +1. Glob:找到组件对应仓库的入口文件(按语言选择模式) + Go: main.go / cmd/*/main.go + Python: main.py / app.py / manage.py / wsgi.py + Java: *Application.java / *Bootstrap.java / src/main/java/**/Main*.java + TypeScript: app.ts / index.ts / main.ts / server.ts + Rust: main.rs / src/main.rs + +2. Grep:定位核心 Handler/Router(按语言+框架选择模式) + Go: grep -rn 'func.*Handler\|\.GET\|\.POST\|router\.\|@handler' + Python: grep -rn '@app\.\|@router\.\|def.*view\|APIRouter\|include_router' + Java: grep -rn '@RestController\|@Controller\|@Service\|@GetMapping\|@PostMapping\|@RequestMapping' + TypeScript: grep -rn 'app\.get\|app\.post\|router\.\|@Get\|@Post\|@Controller' + Rust: grep -rn '\.route\|\.get\|\.post\|#\[get\|#\[post\|async fn' + + ⚠️ 排除测试文件:--exclude='*_test.*' --exclude='test_*' --exclude='*_mock.*' + +3. Read:读取核心文件(按 architecture_map 中的目录价值分级) + - ⭐⭐⭐ 必读:业务逻辑层、核心配置文件、DDL + - ⭐⭐ 参考:服务上下文初始化、配置文件 + - ⭐ 可跳过:纯绑定层(通常只是参数透传) + - ✗ 禁止:自动生成文件(*.pb.go, *_gen.go, *_generated.*, node_modules/, target/, build/) +``` + +提取信息(**全部必须有代码文件:行号引用,不得推断**): +- 核心职责(一句话,≤30字) +- 架构层级和上下游组件(通信方式:RPC/MQ/DB) +- 代码入口(文件名 → 核心函数名) +- 核心机制(最重要的1~2个技术机制) +- 数据流向(从哪来 → 经过什么 → 到哪去) +- 技术栈(语言 + 框架 + 中间件) +- 数据模型(涉及的表名 + DDL 关键字段) +- 核心流程(时序图所需的步骤) +- 配置项(配置键 + 默认值 + 影响范围) +- 定时任务(如有) +- 监控指标(如有) + +无法从代码中找到的内容标注 `[UNVERIFIED]`,不得推断。 + +#### 2B-2:产品文档提取(Type-5/6/7,或有 product_docs_dir 时) + +若 `product_docs_dir` 非空: +``` +扫描维度(来自 phase2-document-types.md §Type-5 桥梁文档生成方法): +├── 数量限制(批量上限、配额、最大值) +├── 类型约束(枚举值、互斥关系) +├── 状态前置条件 +├── 计费规则 +├── 安全约束 +└── 兼容性约束 +``` + +将每个产品约束追踪到代码校验位置(`if len() > N` 的具体文件:行号)。 + +#### 2B-3:文档生成 + +按照 `phase2-document-types.md` 中对应类型的模板生成文档。 + +**Type-4 组件文档必须包含(按顺序)**: + +```markdown +# {组件名} 内部设计说明 + +> 项目: {project_name} | 代码仓库: {仓库URL} | 架构层级: {层级} +> 在整体架构中的位置: [📘 {project_name} 技术架构 - 4.X {组件名}](./{project_name} 技术架构.md#4x-组件名) + +## 🤖 AI 快速理解要点 +| 维度 | 关键信息 | +|------|---------| +| **核心职责** | {≤30字,具体} | +| **架构层级** | {层级名} → {角色} | +| **上游组件** | {组件A(RPC)}, {组件B(MQ)} | +| **下游组件** | {组件C(RPC)}, {组件D(DB)} | +| **代码入口** | `{文件名}` → `{核心函数名}()` | +| **核心机制** | {机制1};{机制2} | +| **互斥控制** | {并发控制方式,如"分布式锁 key: xx"} | +| **数据流向** | {来源} → {处理} → {去向} | +| **技术栈** | {语言} + {框架} + {中间件} | +| **定时任务** | {N个定时任务,或"无"} | + +## 📋 项目概述 +(核心职责编号列表 + ASCII 架构定位图) + +## 🏗️ 架构设计 +(ASCII 架构图 + 核心子模块说明 + 核心函数签名) + +## 📊 数据模型 +(SQL DDL 含注释 + 数据流向图) + +## 🔌 接口设计 +(对外/对内接口表 + 错误码定义) + +## ⚙️ 核心流程 +(mermaid 时序图 + 步骤说明 + 异常处理) + +## 🔧 配置说明 +(配置项 / 默认值 / 说明 / 影响范围) + +## 📈 监控与告警 + +## 🐛 常见问题与排障 + +## 📝 文档更新记录 +### v1.0 ({日期}) +- ✅ **新增**: 初始版本 +> 代码基准:{commit_sha} ({tag}) +``` + +**所有文档 Write 到 `output_dir` 下,禁止先在对话中打印完整内容再写文件。** + +### Step 3:自校验(准确性验证 + 接口对账) + +每份文档生成后执行,**不得跳过**: + +**结构完整性**: +- [ ] AI 快速理解表 10 个维度全部填写,且每个维度都是具体信息(不是"见下文")? +- [ ] "代码入口"精确到函数名(`文件名:行号 → 函数名()`)? +- [ ] search-anchor 有 5~15 个关键词,包含中英文名和同义词? +- [ ] 包含指向主架构文档的双向链接? +- [ ] 无空占位章节(没有内容的章节直接删除)? + +**接口对账**(仅对 architecture_map 中接口校验类型 ≠ NONE 的组件执行): + +从 `_review/interface-inventory.json` 读取该组件的扫描基准数 `scanned`,统计文档中实际记录的接口数 `documented`: + +``` +HTTP 类型: 统计文档 ## 接口设计 节中列出的路由数 +MQ 类型: 统计文档中明确记录的 Topic/Queue/Exchange 数 +RPC 类型: 统计文档中列出的 RPC Method 数 +``` + +计算差异:`gap = scanned - documented` + +处理规则: +- `gap = 0` → ✅ 接口覆盖完整 +- `0 < gap ≤ 20%` → ⚠️ 少量缺口,在文档末尾加 `` +- `gap > 20%` → ❌ 标记 `[INTERFACE_GAP]`,在摘要中注明,建议补充后重跑 + +更新 `progress.json` 中该组件的 `interface_coverage.documented` 字段。 + +**准确性统计**(每份文档单独统计,返回给主 Agent 汇总): +``` +统计方法: + total_claims = 业务规则条数 + 核心流程步骤数 + 接口描述条数 + 配置项条数 + verified = 其中有 file:line 引用的条数 + unverified = 标注了 [UNVERIFIED] 的条数 + ratio = unverified / total_claims +``` + +处理规则: +- `ratio > 20%` → 文档顶部加 `⚠️ 低可信度警告:{unverified}/{total_claims} 项无法回溯到代码` +- `ratio > 40%` → 摘要中标记 **[HIGH_UNVERIFIED]**,建议人工重点确认 + +### Step 4:返回摘要 + +返回给主 Agent(主 Agent 将数据累加到 progress.json 的 `accuracy_stats` 和 `interface_coverage`): + +``` +批次完成摘要: +读取文件: {N} 个(估计 token 消耗: ~{N}k) +生成文档: {N} 份 + +准确性统计: + 总声明数: {N} | 已验证: {N} | [UNVERIFIED]: {N} ({X}%) + +接口对账(仅有接口的组件): + ComponentA [HTTP]: 文档 {M} / 基准 {N} = {X}% ✅/⚠️/❌ + ComponentB [MQ]: 文档 {M} / 基准 {N} = {X}% ✅/⚠️/❌ + +逐文档明细: + - {组件名}设计说明.md: {N}KB,声明{N}条,[UNVERIFIED]{N}条({X}%) [HIGH_UNVERIFIED/INTERFACE_GAP 如适用] + +跳过(已完成): {N} 份 +发现问题: {问题描述 或 "无"} +``` + +## 输出 + +``` +/XX_{组件名}设计说明.md ← Type-4 组件文档 +/{project_name} 技术架构.md ← Type-1(如本批次包含) +/{project_name} 业务架构.md ← Type-2 +/{project_name} 部署架构.md ← Type-3 +/XX_{project_name}核心API产品代码映射.md ← Type-5 +/XX_{project_name}产品规则速查表.md ← Type-6 +/XX_{project_name}业务开发规范SOP.md ← Type-7 +/{知识增强文档}.md ← Type-8 +返回摘要字符串 +``` + +## 约束 + +- **代码为真**:所有描述必须有代码文件引用,不可验证内容必须标注 `[UNVERIFIED]` +- **模板强制**:生成每类文件前必须先读取对应章节的模板 +- **严禁空文档**:没有实质内容则不创建文件 +- **严禁冗余输出**:直接 Write 文件,不在对话中打印完整内容 +- **命名规范**:组件文档用 `XX_{组件名}设计说明.md`,XX 按依赖链顺序分配(底层组件编号小) +- **API 未提供时**:Type-5/6 可跳过产品约束映射,将约束值标注为 `[PRODUCT_DOC_MISSING]` diff --git a/skills/team-wiki-codebase/references/methodology/phase0-collection.md b/skills/team-wiki-codebase/references/methodology/phase0-collection.md new file mode 100644 index 0000000..5a58c25 --- /dev/null +++ b/skills/team-wiki-codebase/references/methodology/phase0-collection.md @@ -0,0 +1,54 @@ +# Phase 0: 源材料采集与预处理 + +## 仓库发现与分类 + +从入口仓库出发,递归发现所有相关仓库: + +1. **依赖分析**: 解析项目依赖文件(如 `requirements.txt`, `package.json`, `pom.xml`, `Cargo.toml`, `go.mod` 等,按检测到的语言选择) +2. **配置引用**: 解析流程编排配置中引用的模块名 → 仓库映射 +3. **RPC 服务发现**: 从服务注册配置提取服务名 → 仓库映射 +4. **按架构层级分类**: API接入层 / 流程引擎层 / 服务执行层 / 资源调度层 / 数据适配层 / 基础执行层 +5. **标记核心度**: 根据代码行数、被依赖数、Handler 数量计算优先级 + +## 关键文件提取清单 + +| 文件类型 | 匹配模式 | 提取目的 | +|---------|---------|---------| +| **入口文件** | `main.py`, `main.go`, `cmd/*/main.go`, `app.ts` | 服务启动方式和初始化流程 | +| **路由/Handler** | `handler.*`, `router.*`, `controller.*` | API 接口和消息处理入口 | +| **配置文件** | `*config*.*`, `conf/`, `*.yaml`, `*.toml` | 流程编排、参数配置 | +| **Proto/IDL** | `*.proto`, `*.thrift`, `*schema*` | RPC 接口契约和数据结构 | +| **数据库操作** | `*db*.*`, `*dao*.*`, `*model*.*`, `*repository*.*` | 数据模型和表结构 | +| **常量/错误码** | `*const*`, `*error*`, `*code*`, `*enum*` | 错误码体系和业务常量 | +| **测试文件** | `*_test.*`, `test_*.*` | 预期行为和边界条件 | + +## 构建代码知识图谱 + +在正式生成文档前,构建代码知识图谱作为中间表示: + +**节点类型**: `[Service]` / `[Handler]` / `[Config]` / `[Table]` / `[Queue]` / `[API]` / `[ErrorCode]` + +**边类型**: `[CALLS]`(同步RPC/HTTP) / `[PUBLISHES]`(异步MQ) / `[CONSUMES]`(MQ消费) / `[READS]`(DB读) / `[WRITES]`(DB写) / `[CONFIGURES]`(配置驱动) / `[MAPS_TO]`(产品→代码) + +**构建方法**(按可用性排序): +1. **`team-wiki compile code --extract ast,heuristic --write`** — Tree-sitter 结构边(**TS/JS/Python/Go** 等)+ 多语言 heuristic 事实页 +2. Grep + Read(Agent K1/K2)— 补充动态路由、配置驱动调用 +3. 解析编排配置 → 模块→命令映射 +4. 解析 Proto/IDL/DDL → 数据结构和表关系(结构化文件,可精确解析) +5. MQ 拓扑推断 → Exchange/Topic/Queue/Routing Key +6. API 映射 → 外部 API 名称 → 内部 Handler 入口 + +> `code-ast` 对相对 import 可产出 `DEPENDS_ON` 边;包级/动态调用仍可能遗漏,标 `[UNVERIFIED]` 或 `AMBIGUOUS`。 +> 能力 ID 与边优先级见插件内 `GRAPH-CAPABILITIES.md`。 + +## 输入源优先级 + +| 优先级 | 输入源 | 具体内容 | 产出文档类型 | +|--------|--------|---------|------------| +| **P0 必须** | 代码仓库 | 目录结构、入口文件、配置、Proto | Type-1,4 | +| **P0 必须** | 流程编排配置 | workflow_config / 状态机 | Type-1,4,5 | +| **P0 必须** | 产品 API 文档 | 接口参数、错误码 | Type-5,6 | +| **P1 重要** | 数据库 Schema | DDL、表结构 | Type-4 | +| **P1 重要** | 产品使用文档 | 使用限制、FAQ | Type-6,8a | +| **P2 增强** | Git 历史 | Commit/MR 记录 | Type-8b | +| **P2 增强** | 故障记录 | 事故报告 | Type-8d | diff --git a/skills/team-wiki-codebase/references/methodology/phase1-reverse-engineering.md b/skills/team-wiki-codebase/references/methodology/phase1-reverse-engineering.md new file mode 100644 index 0000000..969c25c --- /dev/null +++ b/skills/team-wiki-codebase/references/methodology/phase1-reverse-engineering.md @@ -0,0 +1,89 @@ +# Phase 1: 架构逆向工程 — 从代码到架构认知 + +## 1. 自底向上分层法 + +``` +Step 1: 识别"叶子节点" — 直接操作基础设施 + ├── 数据库操作 (MySQL/PostgreSQL/Redis/MongoDB) + ├── 消息队列操作 (RabbitMQ/Kafka/RocketMQ) + ├── 外部系统调用 (第三方 API / 底层驱动) + └── 文件/对象存储操作 (S3/OSS/COS) + +Step 2: 识别"中间节点" — 编排和路由 + ├── 消息路由框架 (消费者路由分发) + ├── 任务调度器 (定时任务/延迟任务) + ├── 流程编排引擎 (Workflow/Saga/状态机) + └── 资源调度器 (负载均衡/资源分配) + +Step 3: 识别"根节点" — 外部入口 + ├── API 网关 / HTTP Handler / gRPC Server + ├── 定时任务入口 (Cron/Scheduler) + └── 事件监听入口 (Webhook/EventBus) + +Step 4: 按调用方向分层 + 外部入口 → 流程编排 → 服务执行 → 资源调度 → 数据操作 → 基础设施 +``` + +### 分层判定规则 + +| 判定特征 | 所属层级 | 典型代码模式 | +|---------|---------|-------------| +| HTTP/gRPC Server 启动 | API 接入层 | `http.ListenAndServe()`, `grpc.NewServer()` | +| 参数校验 + 鉴权 + 限流 | API 接入层 | `validate()`, `auth()`, `rateLimit()` | +| 流程步骤配置和状态机 | 流程引擎层 | `workflow_config`, `state_machine` | +| MQ 消费 + Handler 路由 | 服务执行层 | `channel.consume()`, `handler.dispatch()` | +| 调度算法 (Filter/Score) | 资源调度层 | `filter()`, `score()`, `schedule()` | +| DB CRUD + 缓存操作 | 数据适配层 | `db.query()`, `redis.get()` | +| 底层系统调用/驱动 | 基础执行层 | `exec()`, `syscall.*`, `driver.*` | + +## 2. 三层穿透追踪法(核心方法论) + +对任何用户可见 API 操作,完成三层穿透追踪: + +``` +Layer 1: API 入口层 + ├── 定位 Handler 函数 + ├── 提取参数校验逻辑 + ├── 识别硬编码默认值和白名单 + └── 确定下游调用方式 (同步RPC / 异步MQ) + +Layer 2: 流程编排层 + ├── 查找流程配置 (workflow_config / saga_config) + ├── 解析步骤序列 (步骤名/执行模块/回滚模块/超时/重试) + ├── 标注每步的执行模块和回滚模块 + └── 确定步骤间的数据传递方式 + +Layer 3: 服务执行层 + ├── 追踪每个步骤的具体 Handler 实现 + ├── 识别数据库操作和状态变更 + ├── 标注外部系统调用 + └── 确定最终执行结果的回调路径 + +输出: 完整调用链时序图 + 状态流转图 + 数据流向图 +``` + +### 调用链文档化标准格式 + +``` +[API名称](代码入口: {仓库}/{路径}/{文件}) + → 参数校验 + 鉴权限流 + → [前置检查]: {检查内容} + → RPC/MQ → [编排层] ({配置文件}: {操作名}) + → [服务层] ({配置文件}: {flow_name}) + → [{步骤1模块}] {步骤1命令} ({具体说明}) + → [{步骤2模块}] {步骤2命令} ({具体说明}) + → ... + → 回调 [编排层] +``` + +## 3. 组件关系矩阵 + +构建 N×N 关系矩阵,标注通信方式: + +| 调用方 ↓ / 被调方 → | 组件A | 组件B | 组件C | +|---------------------|-------|-------|-------| +| **组件A** | — | RPC | MQ | +| **组件B** | — | — | DB | +| **组件C** | RPC | MQ | — | + +标注: `RPC`(同步) / `MQ`(异步) / `DB`(共享数据库) / `—`(无直接通信) diff --git a/skills/team-wiki-codebase/references/methodology/phase2-document-types.md b/skills/team-wiki-codebase/references/methodology/phase2-document-types.md new file mode 100644 index 0000000..fddd0ac --- /dev/null +++ b/skills/team-wiki-codebase/references/methodology/phase2-document-types.md @@ -0,0 +1,341 @@ +# Phase 2: 九大文档类型生成规范与模板 + +## Type-1: 技术架构总览 + +**规模**: ~200KB | **数量**: 1 份 + +### 必备章节 + +``` +读者导航指南 (按角色推荐阅读路径) +知识库检索路由指引 (AI 专用,4条分流规则+4级优先级) +1. 架构概述 (30秒快速理解表、整体架构图ASCII、组件关系矩阵) +2. 三维架构视图 (逻辑/数据/部署) +3. 核心链路 ⭐ (每条核心API的完整时序图+调用链) +4. 核心组件详解 (每组件概述+表格) +5. 配置管理与服务发现 +6. 数据模型与存储架构 ⭐ +7. 高可用与技术架构 +8. 架构演进与设计决策 +9. AI 研发知识库规范 ⭐ (元数据QA/全局状态机/MQ拓扑/调度引擎/跨层追踪) +附录: 代码仓库/术语表/代码入口索引/错误码 +``` + +### 生成规则 +- T1-R01: 必须包含读者导航指南 +- T1-R02: 必须包含 AI 检索路由规则 +- T1-R03: 核心链路必须有时序图 +- T1-R04: 组件表必须包含代码仓库列 +- T1-R05: 术语表必须包含内外部映射 +- T1-R06: 必须有 AI 专用第 9 章 +- T1-R07: 架构图使用 ASCII Art + +--- + +## Type-2: 业务架构文档 + +**规模**: ~70KB | **数量**: 1 份 + +``` +1. 产品能力矩阵 (能力域/子能力/对应API/计费影响) +2. 计费模型详解 (模式对比/状态机/退费续费规则) +3. 核心实体生命周期 (完整状态机/各状态允许操作/互斥规则) +4. 核心业务流程 (用户视角时序图+前置条件+异常处理) +5. 产品规格体系 (命名规则/规格与底层资源映射) +``` + +--- + +## Type-3: 部署架构文档 + +**规模**: ~40KB | **数量**: 1 份 + +``` +1. 分层部署架构图 +2. 服务部署矩阵 (服务名/部署方式/实例数/资源配置/依赖) +3. 环境配置 (生产/测试/差异对照) +4. 部署流程与变更管理 +``` + +--- + +## Type-4: 组件设计文档(核心产出) + +**规模**: 20~100KB/份 | **数量**: N 份(每组件一份) + +### 标准模板 + +``` +# {组件名} 内部设计说明 + +> 项目名称 / 版本 / 代码仓库 / 代码规模 +> 在整体架构中的位置: [📘 链接到主架构文档] + +## 🤖 AI 快速理解要点 +(10 维度结构化摘要,详细定义见 [phase3-ai-enhancement.md §1](phase3-ai-enhancement.md)) + +## 📋 项目概述 (核心职责+在架构中的位置) +## 🏗️ 架构设计 (ASCII架构图+核心子模块,函数签名) +## 📊 数据模型 (SQL DDL带注释+数据流向图) +## 🔌 接口设计 (对外接口表+对内接口+错误码) +## ⚙️ 核心流程 (时序图+步骤说明+异常处理) +## 🔧 配置说明 (配置项/默认值/说明/影响范围) +## 📈 监控与告警 +## 🐛 常见问题与排障 +``` + +### 生成规则 +- T4-R01: 必须有 AI 快速理解表 +- T4-R02: 必须有双向链接到主架构文档 +- T4-R03: 核心函数必须列出签名 +- T4-R04: SQL DDL 必须包含注释 +- T4-R05: 配置项必须标注影响范围 +- T4-R06: 架构图使用 ASCII Art +- T4-R07: 代码入口必须精确到函数名 + +### 从代码生成的步骤 + +> 详细执行规范见 `references/agents/kb-doc-generator.md`,此处仅列概要: +> 1. 代码结构扫描(Glob → Grep → Read 三步法,按语言自适应) +> 2. 信息提取(10 维度:核心职责/架构层级/上下游/代码入口/核心机制/数据流向/技术栈/数据模型/配置项/定时任务) +> 3. 文档组装(按上述模板章节顺序) +> 4. 自校验(准确性统计 + 接口对账) + +--- + +## Type-5: 产品-代码映射(桥梁文档) + +### 每个核心 API 一节 + +``` +### N.1 用户意图 (一句话) +### N.2 产品约束 (约束项/约束值/影响组件/校验位置) +### N.3 用户可见状态流转 (ASCII图+内部状态映射) +### N.4 内部调用链路 (标准格式精确到代码文件) +### N.5 写代码时必须考虑的 (硬性约束编号列表) +### N.6 错误码与内部异常映射 (外部码/内部组件/含义) +``` + +### 生成规则 +- T5-R01: 约束表必须标注"影响的组件"和"校验位置" +- T5-R02: 调用链必须精确到代码文件路径 +- T5-R03: 状态流转必须标注内部状态码映射 +- T5-R04: "写代码时必须考虑的"是强制章节 +- T5-R05: 错误码映射必须包含内部组件归属 + +### 桥梁文档生成方法(3 Step) + +**Step 1: 提取产品约束** — 从产品文档中提取所有影响代码实现的约束: + +``` +扫描维度: +├── 数量限制 (批量上限、配额、最大值) +├── 类型约束 (枚举值、互斥关系) +├── 状态前置条件 (操作前资源必须处于什么状态) +├── 计费规则 (不同计费模式的差异处理) +├── 安全约束 (鉴权、加密、脱敏) +└── 兼容性约束 (类型兼容、版本兼容、地域限制) +``` + +**Step 2: 映射到代码位置** — 对每个产品约束,追踪到代码中的具体校验位置: + +``` +产品约束: "{API名} 批量上限 N" + ↓ 追踪 +代码位置: {API网关组件} → {文件路径} → validate_params() + ↓ 确认 +校验方式: if len(resource_ids) > N: raise InvalidParameterValue +``` + +**Step 3: 构建映射表** — 将上述信息组装为标准的产品-代码映射表(见 Type-5 模板)。 + +**桥梁文档质量标准**: + +| 质量维度 | 标准 | 检查方法 | +|---------|------|---------| +| **完整性** | 所有核心 API 都有映射 | 对照 API 列表逐一检查 | +| **精确性** | 代码路径精确到文件和函数 | 实际打开代码验证 | +| **一致性** | 约束值与产品文档一致 | 交叉比对产品文档 | +| **时效性** | 与最新代码版本同步 | 定期 diff 检查 | + +--- + +## Type-6: 产品规则速查表 + +``` +## N. {规则类别} +| 规则 | 约束值 | 影响的组件 | 校验位置 | 来源文档 | + +## 状态与操作互斥规则 +| 当前状态 | 允许的操作 | 禁止的操作 | +``` + +- T6-R01: 每条规则必须标注"影响的组件" +- T6-R02: 约束值必须是精确数字 +- T6-R03: 必须有"来源文档"列 +- T6-R04: 状态互斥规则必须是完整矩阵 + +--- + +## Type-7: 业务开发规范 SOP + +``` +1. 为什么需要标准代码模板 (野生代码问题) +2. 核心规约 (绝不向外暴露底层错误/Context一传到底/参数前置校验) +3. 标准 Handler 代码模板 (可直接复制,标注"AI 编码铁律") +4. 错误码映射对照表 (场景描述用AI思考逻辑/推荐错误码/Message) +5. AI 评审 CheckList (可机器校验) +``` + +- T7-R01: 代码模板必须可直接复制运行 +- T7-R02: 每个关键注释标注"AI 编码铁律" +- T7-R03: 错误码表用"AI的思考逻辑"作为场景描述 + +--- + +## Type-8: 知识增强文档 + +### Type-8a: 产品知识文库 +标注 `type: bridge`,表格对比易混淆概念,含"代码传参示例"和"架构与业务影响"列。 + +### Type-8b: 反模式与踩坑指南 +五段式:**触发场景→错误表现→根因分析→正确做法→关联组件** +概览表标注编号/分类/严重程度(P0致命/P1严重/P2重要)/关联组件。 + +### Type-8c: RPC 接口契约 +struct 定义含序列化 Tag + 必填/选填标注 + AI 编码契约要求。 + +### Type-8d: 排障案例记录 (Memorix) +结构:问题现象→排查过程(Step N)→根因定位→修复方案→经验总结→关联文档。 + +--- + +## Type-9: 图谱文档集(Graph RAG) + +**规模**: 10~30KB/份 | **数量**: 5~10 份 | **目录**: `graph/` + +> 将散落在 N 份组件文档中的**跨组件关系信息**抽取为结构化索引,解决 RAG 检索在关系查询场景下的"信息分散"问题。 + +### 图谱文档类型清单 + +| 编号 | 文档名 | 核心内容 | 解决的检索痛点 | +|------|--------|---------|--------------| +| G1 | 组件依赖关系矩阵 | N×N 通信矩阵 + 正向/反向依赖索引 + 外部服务依赖 | "谁依赖 X?" 需遍历所有文档 | +| G2 | 组件调用链路全景 | 核心 API 端到端链路 + 读写分离机制 + **完整状态机流转图** + 操作-状态约束矩阵 | "API 经过哪些模块?" 信息分散 | +| G3 | 数据流与存储依赖图 | 存储依赖矩阵 + MQ 队列拓扑 + 缓存策略 | "数据存在哪里?" | +| G4 | 错误码组件映射表 | 错误码段分配 + 外部→内部映射 | "错误码是哪个模块的?" | +| G5 | 跨组件交互场景手册 | ≥10 个场景的 mermaid 时序图 + 异常处理 | "配额检查怎么做的?" | +| G6 | 知识图谱三元组 | (S, P, O) 三元组 + 多跳依赖路径索引 | "A 间接依赖谁?" | +| G7 | 架构风险与影响面分析 | 爆炸半径 + 聚类分析 + 关键路径/瓶颈 | "X 挂了影响多大?" | +| G8 | **核心配置参数索引** | 分层配置项→行为影响映射 + 变更影响面速查 | "怎么修改 XX 配置?" | +| G9 | **业务规则约束矩阵** | 操作前置条件 + 硬件/迁移/计费约束 + AI 推理决策树 | "能不能做 XX?" | + +### 图谱文档生成规则 + +- T9-R01: 每份图谱文档必须有 `🤖 AI 快速理解要点` 表 +- T9-R02: 每份图谱文档必须有 `` 锚点 +- T9-R03: 图谱目录必须有 `README.md` 索引,含"按问题类型查找"表和"检索路由规则建议" +- T9-R04: 状态机必须使用 mermaid `stateDiagram-v2` 格式 +- T9-R05: 约束决策树必须使用 mermaid `graph TD` 格式 +- T9-R06: 操作-状态约束必须是 ✅/❌ 矩阵格式 +- T9-R07: 配置参数必须标注"影响行为"、"变更风险"(🟢低/🟡中/🔴高)、"生效方式"(热生效/需重启) +- T9-R08: 业务规则约束必须包含 AI 推理检查流程(mermaid 流程图) +- T9-R09: 三元组必须遵循 (Subject, Predicate, Object) 标准格式 +- T9-R10: 图谱文档**不替代**组件文档,而是提供**关系视角的结构化索引** + +### 图谱文档生成方法 + +**Step 1: 关系抽取** — 从 N 份组件文档中提取跨组件关系: + +``` +扫描维度: +├── 调用关系 (A calls B, 协议, 场景) +├── 数据依赖 (A reads/writes B, 数据内容) +├── 消息拓扑 (A publishes_to/consumes_from Queue) +├── 状态流转 (操作 → 起始状态 → 中间状态 → 终态) +├── 约束条件 (操作 → 前置条件 → 硬件/计费/配额约束) +├── 配置映射 (配置项 → 影响行为 → 变更风险) +└── 错误码归属 (错误码段 → 组件 → 排查方向) +``` + +**Step 2: 结构化建模** — 将抽取的关系转化为标准格式: + +``` +关系矩阵 → N×N 表格 +调用链路 → 端到端文本链路 + mermaid 时序图 +状态机 → mermaid stateDiagram-v2 +约束规则 → 决策树(mermaid graph TD) + 汇总表 +配置索引 → 分层表格(配置项/默认值/影响行为/变更风险/生效方式) +三元组 → (Subject, Predicate, Object, Protocol, Scenario) 表格 +``` + +**Step 3: 索引织网** — 建立图谱文档间的交叉引用和检索路由: + +``` +README.md: +├── 文档目录表 (文件/大小/核心内容) +├── 按问题类型查找表 (问题类型/示例/查找文档) +└── 检索路由规则建议 (关键词→优先检索文档) +``` + +### 关键模板 + +#### 状态机流转图模板 + +```markdown +## 实例状态机完整流转图 + +### 核心状态流转图 +​```mermaid +stateDiagram-v2 + [*] --> PENDING: CreateAction + PENDING --> RUNNING: 创建成功 (flag: 2→1) + RUNNING --> STOPPING: StopAction (flag: 1→8) + STOPPING --> STOPPED: 关机成功 (flag: 8→3) + ... +​``` + +### 操作-状态约束速查矩阵 +| 操作 \ 当前状态 | RUNNING | STOPPED | PENDING | ... | +|---------------|:-------:|:-------:|:-------:|:---:| +| **Start** | ❌ | ✅ | ❌ | ... | +| **Stop** | ✅ | ❌ | ❌ | ... | +``` + +#### 业务规则约束矩阵模板 + +```markdown +## 操作前置条件矩阵 +| 操作 | 状态要求 | 硬件约束 | 计费约束 | 配额约束 | 其他约束 | + +## 迁移约束决策树 +​```mermaid +graph TD + A[迁移请求] --> B{硬件约束1?} + B -->|是| C["❌ 禁止"] + B -->|否| D{硬件约束2?} + ... +​``` + +## AI 推理规则速查 +​```mermaid +graph TD + A["用户问:能否执行 XX?"] --> B["Step 1: 状态检查"] + B --> B1{"查操作-状态约束矩阵"} + B1 -->|❌| Z1["不能,状态不支持"] + B1 -->|✅| C["Step 2: 类型检查"] + ... +​``` +``` + +#### 配置参数索引模板 + +```markdown +## {组件层}配置参数 +| 配置项 | 默认值 | 影响行为 | 变更风险 | 生效方式 | +|--------|--------|---------|---------|---------| +| `config.key` | value | 描述 | 🟢低/🟡中/🔴高 | 热生效/需重启 | + +## 配置变更影响面速查 +| 变更类型 | 影响范围 | 生效方式 | 回滚策略 | 变更风险 | +``` diff --git a/skills/team-wiki-codebase/references/methodology/phase3-ai-enhancement.md b/skills/team-wiki-codebase/references/methodology/phase3-ai-enhancement.md new file mode 100644 index 0000000..8ebd799 --- /dev/null +++ b/skills/team-wiki-codebase/references/methodology/phase3-ai-enhancement.md @@ -0,0 +1,164 @@ +# Phase 3: AI-Native 增强 — 让知识库对 AI 可理解 + +## 1. AI 快速理解表(每份组件文档必备) + +RAG 检索返回的 chunk 通常是文档片段。AI 快速理解表确保无论检索到文档哪个部分,AI 都能在表头获得组件全局上下文。 + +```markdown +## 🤖 AI 快速理解要点 + +| 维度 | 关键信息 | +|------|---------| +| **核心职责** | {一句话,不超过 30 字} | +| **架构层级** | {所属层级} → {在层级中的角色} | +| **上游组件** | {组件名(通信方式)} | +| **下游组件** | {组件名(通信方式)} | +| **代码入口** | {入口文件} → {核心函数} | +| **核心机制** | {最重要的 1-2 个技术机制} | +| **互斥控制** | {并发控制方式} | +| **数据流向** | {从哪来 → 经过什么 → 到哪去} | +| **技术栈** | {语言 + 框架 + 中间件} | +| **定时任务** | {N 个定时任务(简述核心任务)} | +``` + +规则: +- 每个维度必须是**具体的**,不能泛泛描述 +- "代码入口"精确到 `文件名 → 函数名` +- "上下游组件"必须标注通信方式 (RPC/MQ/DB) +- 表格放在文档最前面(紧跟标题之后) + +## 2. 检索路由规则(主架构文档必备) + +防止 RAG 检索内外部文档"串台": + +```markdown +## 知识库检索路由指引(AI 专用) + +### 文档分类总览 +| 分类 | 目录位置 | 文档数量 | 内容性质 | +| 【内部·桥梁】产品-代码映射 | ... | N 份 | 核心API意图→约束→链路 | +| 【内部】组件设计文档 | ... | N 份 | 架构设计、代码入口 | +| 【外部】产品 API 文档 | ... | N 份 | 官网 API 参考 | + +### 检索路由规则 +规则 1 — 内部架构优先: 涉及组件名/内部概念 → 仅检索内部文档 +规则 2 — 外部文档适用: 涉及 API 参数/产品限制 → 检索外部文档 +规则 3 — 混合查询: 同时涉及 → 优先内部,辅以外部 +规则 4 — 写代码前先查约束: 必须先检索桥梁文档 + +### 文档优先级 +| 一级(核心) | 产品-代码映射 + 规则速查表 | 写代码前必查 | +| 二级(架构) | 组件设计文档 + 主架构文档 | 理解内部实现 | +| 三级(业务) | 业务架构 + 核心链路 | 理解业务流程 | +| 四级(备查) | 外部 API 原始文档 | 仅在上述不能回答时 | +``` + +## 3. Search Anchor(语义检索锚点) + +每份文档标题下方添加: + +```html + +``` + +- 包含: 中文名、英文名、缩写、同义词、常见搜索词 +- 数量: 5~15 个 +- 示例: `` + +## 4. 双向链接织网 + +```markdown +# 组件文档 → 主架构文档 +> 在整体架构中的位置: [📘 主架构文档 - 4.5 {组件名}](./主架构文档.md#45-组件名) + +# 主架构文档 → 组件文档 +详见 [{组件名}设计说明](./XX_{组件名}设计说明.md) + +# 桥梁文档 → 组件文档 +| [{组件名}](./XX_{组件名}设计说明.md) | 入参校验层 | +``` + +织网规则: +1. 每份组件文档 ≥ 1 个链接指向主架构文档 +2. 主架构文档每个组件提及处有链接指向组件文档 +3. 桥梁文档中提到的每个组件有链接 +4. 反模式文档的"关联组件"有链接 + +## 5. QA 对生成(AI 元数据层) + +在主架构文档 AI 专用章节预置高频 QA 对(10~20 个): + +```markdown +- **Q: 核心实体的状态机是如何定义的?** + A: 见 `3.7 实体完整状态机` 及 `9.2.1 全局状态一致性映射表`。 + +- **Q: 流程步骤配置在哪里?异常如何补偿回滚?** + A: 采用 N 级编排。宏观流程在 {配置文件1},细粒度步骤在 {配置文件2}。 + +- **Q: 消息队列的拓扑和路由规则?** + A: 见 `9.3.1 MQ 路由拓扑`。核心 Exchange/Topic 包括 {列表}。 + +- **Q: 资源互斥(加锁)规范?** + A: 见 `9.4.4 分布式锁与幂等规范`。使用 {锁方案}。 +``` + +每个 A 必须包含具体的章节/文档引用。 + +## 6. 图谱文档 AI 增强规范 + +图谱文档是 AI-Native 知识库的**关系索引层**,专门解决 RAG 在"跨组件关系查询"场景下的检索失败问题。 + +### 6.1 图谱文档 README 必备结构 + +```markdown +# 图谱文档集 (Graph RAG) +## 与主文档体系的关系 (三层定位表) +## 文档目录 (文件/大小/核心内容) +## 按问题类型查找 (问题类型/示例/查找文档) +## 检索路由规则建议 (关键词→优先检索文档) +## 维护说明 +``` + +### 6.2 图谱文档 AI 快速理解表 + +每份图谱文档必须在标题后紧跟: + +```markdown +## 🤖 AI 快速理解要点 +| 维度 | 关键信息 | +|------|---------| +| **文档定位** | {一句话定位} | +| **核心价值** | {AI 用这份文档能做什么} | +| **覆盖范围** | {覆盖了哪些实体/关系} | +| **使用场景** | {典型问题示例} | +| **与状态机的关系** | {如适用:状态机解决X,本文档解决Y} | +``` + +### 6.3 AI 推理规则嵌入 + +对于约束类图谱文档,必须嵌入 AI 推理决策流程: + +```markdown +## AI 推理规则速查 +> AI 判断"某操作能否执行"时,按以下优先级逐层检查: + +1. **状态检查** → 查操作-状态约束矩阵 +2. **类型检查** → 查特殊实例类型约束汇总 +3. **硬件检查** → 查硬件约束详表 +4. **计费检查** → 查计费约束详表 +5. **配额检查** → 查产品规则速查表 +6. **互斥检查** → 是否有进行中的操作 +``` + +### 6.4 配置变更检查清单 + +对于配置类图谱文档,AI 回答"怎么修改 XX 配置"时必须同时告知: + +``` +1. 配置文件位置 — 在哪个文件/仓库中 +2. 影响范围 — 全地域还是单地域/单机 +3. 生效方式 — 热生效还是需要重启 +4. 回滚策略 — 如何快速回滚 +5. 变更风险 — 🟢低 / 🟡中 / 🔴高 +6. 灰度建议 — 是否需要灰度发布 +``` diff --git a/skills/team-wiki-codebase/references/methodology/phase4-quality.md b/skills/team-wiki-codebase/references/methodology/phase4-quality.md new file mode 100644 index 0000000..a28d3e3 --- /dev/null +++ b/skills/team-wiki-codebase/references/methodology/phase4-quality.md @@ -0,0 +1,232 @@ +# Phase 4: 质量评估与迭代优化 + +> 辅助工具: `scripts/validate_kb.py` — 自动校验链接完整性、anchor 覆盖率、AI 快速理解表覆盖率、双向链接、README 索引收录率 + +## 五维评估模型 + +| 维度 | 权重 | 达标标准 | +|------|------|---------| +| **覆盖率** | 25% | ≥ 90% 核心组件有文档 | +| **深度** | 25% | ≥ 80% 代码入口可直接定位 | +| **一致性** | 20% | 0 死链接,0 矛盾描述 | +| **AI 可用性** | 20% | RAG 检索准确率 ≥ 85% | +| **时效性** | 10% | 核心文档更新滞后 ≤ 30 天 | + +## 覆盖率检查 + +``` +□ 每个代码仓库有对应的组件设计文档? +□ 每个核心 API 有产品-代码映射? +□ 每个数据表在某份文档中有 Schema 说明? +□ 每个 MQ Exchange/Topic/Queue 在拓扑图中标注? +□ 每个错误码在映射表中? +□ 每个配置项在配置说明中? +□ 每个定时任务在某份文档中说明? +``` + +## RAG 检索测试用例 + +| 测试类型 | 示例问题 | 期望命中 | +|---------|---------|---------| +| 组件定位 | "{组件名}的代码入口在哪?" | 组件设计文档 | +| 流程追踪 | "{API名}的内部调用链路?" | 产品-代码映射 | +| 约束查询 | "{操作}的批量上限?" | 规则速查表 | +| 状态查询 | "处于{状态}时可执行什么操作?" | 状态互斥规则 | +| 错误排查 | "遇到{错误码}怎么排查?" | 反模式/排障记录 | +| 代码生成 | "写一个{功能}的 Handler" | SOP + 接口契约 | +| 概念辨析 | "{A}和{B}区别?" | 产品知识文库 | + +## 增量更新触发表 + +| 触发条件 | 更新动作 | +|---------|---------| +| 新增代码仓库 | 生成 Type-4 组件文档 | +| API 接口变更 | 更新 Type-5 映射 + Type-6 速查表 | +| 新增产品功能 | 更新 Type-2 业务架构 + Type-8a 知识文库 | +| 线上故障 | 新增 Type-8d 排障记录 + 更新 Type-8b 反模式 | +| 架构重构 | 更新 Type-1 架构总览 + 受影响 Type-4 | +| 配置变更 | 更新对应组件文档的配置章节 | + +## 版本管理规范 + +每份文档底部维护变更记录: + +```markdown +## 📝 文档更新记录 + +### vX.Y (YYYY-MM-DD) +- ✅ **新增**: {新增内容描述} +- ✅ **修复**: {修复内容描述} +- ✅ **更新**: {更新内容描述} +- ⚠️ **废弃**: {废弃内容描述} +``` + +## 常见质量问题修复 + +| 问题 | 修复方法 | +|------|---------| +| 死链接 | 全局 grep `](` 链接,或运行 `scripts/validate_kb.py` | +| 术语不一致 | 建立术语表全局替换 | +| 代码入口过时 | 定期与代码仓库 diff | +| 约束值过时 | 定期与产品文档交叉比对 | +| AI 检索失败 | 补充 search-anchor 关键词 | +| 文档孤岛 | 补充双向链接 | + +--- + +## 完整生成流水线 Checklist + +### Phase 0 Checklist: 源材料采集 + +``` +□ 所有核心代码仓库已克隆 +□ 产品 API 文档已采集 (接口名/入参/出参/错误码) +□ 产品使用文档已采集 (使用限制/FAQ/计费说明) +□ 数据库 Schema 已提取 (DDL/表结构) +□ 流程编排配置已提取 (workflow_config 等) +□ Proto/IDL 文件已提取 +□ 错误码定义已提取 +``` + +### Phase 1 Checklist: 架构逆向工程 + +``` +□ 代码知识图谱已构建 (节点+边) +□ 架构分层已确定 (≥4 层) +□ 组件关系矩阵已构建 (N×N) +□ 核心调用链已追踪 (≥5 条核心 API) +□ MQ 拓扑已推断 (Exchange/Topic/Queue/Routing Key) +□ 数据库 ER 模型已构建 +□ 术语表已整理 (内外部映射) +``` + +### Phase 2 Checklist: 文档生成 + +``` +□ [Type-1] 技术架构总览文档 (1份) + □ 包含读者导航指南 + □ 包含 AI 检索路由规则 + □ 包含核心链路时序图 (≥5 条) + □ 包含组件关系矩阵 + □ 包含 AI 专用第 9 章 + □ 包含术语表 + +□ [Type-2] 业务架构文档 (1份) + □ 包含产品能力矩阵 + □ 包含计费模型(如适用) + □ 包含核心实体生命周期状态机 + +□ [Type-3] 部署架构文档 (1份) + □ 包含服务部署矩阵 + □ 包含环境配置 + +□ [Type-4] 组件设计文档 (N份) + □ 每份包含 AI 快速理解表 + □ 每份包含双向链接 + □ 每份包含代码入口 (精确到函数) + □ 每份包含架构图 (ASCII Art) + □ 每份包含核心流程说明 + +□ [Type-5] 产品-代码映射文档 + □ 覆盖所有核心 API + □ 每个 API 包含约束表 + □ 每个 API 包含调用链路 + □ 每个 API 包含错误码映射 + +□ [Type-6] 产品规则速查表 + □ 覆盖所有规则类别 + □ 约束值精确 + □ 包含状态互斥矩阵 + +□ [Type-7] 业务开发规范 SOP + □ 包含可运行的代码模板 + □ 包含错误码对照表 + □ 包含 AI 评审 CheckList + +□ [Type-8] 知识增强文档 + □ [8a] 产品知识文库 (概念辨析) + □ [8b] 反模式与踩坑指南 + □ [8c] RPC 接口契约 + □ [8d] 排障案例记录 +``` + +### Phase 3 Checklist: AI-Native 增强 + +``` +□ 所有组件文档包含 AI 快速理解表 +□ 主架构文档包含检索路由规则 +□ 所有文档包含 search-anchor +□ 双向链接网络完整 (0 死链接) +□ QA 对已生成 (10~20 个) +□ 文档优先级已定义 +``` + +### Phase 3b Checklist: 图谱文档集 (Graph RAG) + +``` +□ [G1] 组件依赖关系矩阵 + □ N×N 通信矩阵完整 + □ 正向/反向依赖索引 + □ 外部服务依赖 + +□ [G2] 组件调用链路全景 + 状态机 + □ 核心 API 端到端链路 (读+写) + □ 完整 mermaid 状态机流转图 + □ 核心状态字段值流转路径表(如有内部状态码) + □ 用户可见状态↔内部状态映射关系(如有多层状态) + □ 操作-状态约束速查矩阵 (✅/❌) + □ AI 状态判断推理规则 + +□ [G3] 数据流与存储依赖图 + □ 存储系统依赖矩阵 + □ MQ 队列拓扑 + □ 缓存策略矩阵 + +□ [G4] 错误码组件映射表 + □ 错误码段分配表 + □ 外部→内部错误码映射 + +□ [G5] 跨组件交互场景手册 + □ ≥10 个场景的 mermaid 时序图 + □ 每个场景有异常处理 + +□ [G6] 知识图谱三元组 + □ Ontology 定义 (实体类型+关系类型) + □ 显式三元组 ≥100 条 + □ 多跳依赖路径索引 + □ 反向可达索引 + +□ [G7] 架构风险与影响面分析 + □ 组件风险等级总表 + □ 爆炸半径分析 (≥3 个关键组件) + □ 聚类分析 + □ 变更风险评估矩阵 + +□ [G8] 核心配置参数索引 + □ 分层配置架构图 (mermaid) + □ 每层配置参数表 (配置项/默认值/影响行为/变更风险/生效方式) + □ 配置变更影响面速查矩阵 + +□ [G9] 业务规则约束矩阵 + □ 操作前置条件矩阵 + □ 硬件约束详表 + □ 迁移约束决策树 (mermaid) + □ 计费约束详表 + □ 特殊实例类型约束汇总 (✅/❌/⚠️) + □ AI 推理规则速查 (mermaid 流程图) + +□ 图谱目录 README.md 索引完整 + □ 按问题类型查找表 + □ 检索路由规则建议 +``` + +### Phase 4 Checklist: 质量评估 + +``` +□ 覆盖率 ≥ 90% +□ 代码入口精确度 ≥ 80% +□ 死链接 = 0 (运行 validate_kb.py 确认) +□ RAG 检索准确率 ≥ 85% +□ 核心文档更新滞后 ≤ 30 天 +□ 术语一致性检查通过 +``` diff --git a/skills/team-wiki-codebase/references/templates/project-overview.md b/skills/team-wiki-codebase/references/templates/project-overview.md new file mode 100644 index 0000000..04dfd19 --- /dev/null +++ b/skills/team-wiki-codebase/references/templates/project-overview.md @@ -0,0 +1,148 @@ +# 知识库总览模板 + +> 用于生成 `/README.md`,在 Phase K2 批次5 生成(知识库顶层索引)。 + +```markdown +# <项目名称> — 深度知识库 + + +> **AI 读取指引**:本目录是 AI-Native 知识库。请先阅读本文件了解全局和认知边界, +> 再按检索路由规则进入对应文档查阅详情。**禁止一次性读取整个知识库目录。** + +## 🤖 知识库检索路由指引(AI 专用) + +### 按问题类型快速导航 + +| 我想了解… | 应该读… | 路径 | +|---------|---------|------| +| 系统整体架构和分层 | 技术架构文档 | `./{项目名} 技术架构.md` | +| 某个组件的设计和实现 | 组件设计说明 | `./XX_{组件名}设计说明.md` | +| 组件之间的依赖关系 | G1 依赖矩阵 | `./graph/G1_*.md` | +| 某个 API 经过哪些模块 | G2 调用链路全景 | `./graph/G2_*.md` | +| 数据存在哪里、MQ 拓扑 | G3 数据流 | `./graph/G3_*.md` | +| 错误码是哪个模块的 | G4 错误码映射 | `./graph/G4_*.md` | +| 某个业务场景的完整流程 | G5 交互场景手册 | `./graph/G5_*.md` | +| A 间接依赖谁(多跳查询) | G6 知识图谱三元组 | `./graph/G6_*.md` | +| X 组件挂了影响多大 | G7 风险分析 | `./graph/G7_*.md` | +| 怎么修改某个配置 | G8 配置参数索引 | `./graph/G8_*.md` | +| 某个操作能不能执行 | G9 业务规则约束 | `./graph/G9_*.md` | +| 产品约束→代码位置映射 | 核心API映射文档 | `./XX_*产品代码映射.md` | +| 业务开发 SOP | 业务开发规范 | `./XX_*业务开发规范SOP.md` | + +### 检索规则 + +- **规则 1 — 先读索引后深入**:遇到不确定的组件,先读本文件找到正确路径,再深入组件文档 +- **规则 2 — 组件内部问题查组件文档**:核心机制、代码入口、数据模型 → `XX_{组件名}设计说明.md` +- **规则 3 — 跨组件关系问题查图谱**:依赖矩阵、调用链路、影响面 → `graph/` 目录 +- **规则 4 — 操作可行性问题查 G9**:约束矩阵 + 决策树 → `graph/G9_*.md` +- **规则 5 — `[UNVERIFIED]` 标注的内容不可用于代码生成**,需先人工确认 +- **规则 6 — `AMBIGUOUS` 关系不可用于变更影响评估**,需先明确 + +--- + +## 🚧 认知边界声明(AI 必读) + +> 本节声明此知识库**不知道什么**。AI 在回答问题时,如果涉及以下范围, +> **必须主动告知用户"此信息超出知识库覆盖范围,建议查看源代码/产品文档/联系团队"**, +> 而不是尝试推断或幻觉。 + +### 覆盖范围 + +| 维度 | 覆盖 | 说明 | +|------|------|------| +| 代码基准 | `` (``) | 此版本**之后**的变更不在覆盖范围内 | +| 生成时间 | `` | 知识库与代码的时间锚点 | +| 核心组件(P0) | | 文档深度最高,接口级覆盖 | +| 重要组件(P1) | | 文档深度中等,核心机制覆盖 | +| 辅助组件(P2) | | 文档深度有限,仅架构层面 | + +### 明确不覆盖(AI 不应尝试回答) + +| 领域 | 原因 | +|------|------| +| 第三方 SDK/库内部实现 | 知识库只记录调用方式,不涉及第三方源码 | +| 运维/部署细节(ansible/k8s 配置) | 超出代码知识库范围,需查阅运维文档 | +| 非代码产出(UI 设计、产品 PRD 原文) | 仅 Type-5/6 桥梁文档有产品约束映射 | +| 历史架构变迁 | 仅反映当前代码基准版本的架构 | +| 性能基准数据 | 知识库不包含压测数据 | +| <项目特定不覆盖项> | <原因> | + +### 低可信度区域(AI 回答时需额外警告) + +| 区域 | 原因 | 建议 | +|------|------|------| +| P2 辅助组件的内部细节 | 文档深度有限 | 引用时加"基于有限文档分析" | +| `[UNVERIFIED]` 标注内容 | 无法回溯到代码 | 必须告知用户"此信息未经代码验证" | +| `AMBIGUOUS` 关系 | 置信度 < 0.3 | 必须告知用户"此关系存在不确定性" | +| 产品文档缺失时的 Type-5/6 | 无产品文档输入 | 标注 `[PRODUCT_DOC_MISSING]` | + +### 知识库更新说明 + +- **增量更新**:使用 `code-to-knowledge --update` 可仅更新变更文件对应的文档 +- **全量重建**:代码发生大规模重构时建议全量重建 +- **上次更新**:`` + +--- + +## 项目简介 + + + +## 技术栈 + +| 类别 | 技术 | 说明 | +|------|------|------| +| 语言 | Go / Python | ... | +| 框架 | go-zero / FastAPI | ... | +| 数据库 | MySQL / PostgreSQL | ... | +| 缓存 | Redis | ... | +| 消息队列 | Kafka / RabbitMQ | (如有) | + +## 知识库文档索引 + +### 架构层文档 +| 文档 | 类型 | 规模 | 说明 | +|------|------|------|------| +| {项目名} 技术架构.md | Type-1 | ~200KB | 架构总览 | +| {项目名} 业务架构.md | Type-2 | ~70KB | 产品能力+生命周期 | +| {项目名} 部署架构.md | Type-3 | ~40KB | 部署拓扑 | + +### 组件设计文档 +| 编号 | 组件 | 架构层 | 核心度 | 规模 | +|------|------|--------|--------|------| +| 01 | <组件名> | <层级> | P0 | ~NKB | + +### 桥梁文档(有产品文档时生成) +| 文档 | 类型 | 说明 | +|------|------|------| +| 核心API产品代码映射 | Type-5 | 产品约束→代码位置 | +| 产品规则速查表 | Type-6 | 使用限制/FAQ→代码 | +| 业务开发规范SOP | Type-7 | 开发/变更操作规范 | + +### 图谱文档集(Graph RAG) +| 文档 | 用途 | 规模 | +|------|------|------| +| G1~G9 | 跨组件关系索引 | 详见 `graph/README.md` | + +## 知识库质量概览 + +| 指标 | 数值 | 状态 | +|------|------|------| +| 文档总数 | N 份 | — | +| 内容准确率(有代码引用) | X% | ✅/⚠️ | +| [UNVERIFIED] 比例 | X% | 目标<15% | +| 接口覆盖率(非 NONE 组件) | X% | 目标≥90% | +| AMBIGUOUS 关系数 | N 条 | 需人工确认 | + +> 详细质量报告见 `_review/k4-quality-report.md` + +## 代码基准版本 + +> ⚠️ 本知识库基于以下版本代码生成,代码演进后请运行 `code-to-knowledge --update` 增量更新。 + +- **Commit**:`` +- **Tag**:`` +- **生成时间**:`` + +> 版本信息来源:`_review/metadata.json` +``` diff --git a/skills/team-wiki-codebase/scripts/scan_repo.py b/skills/team-wiki-codebase/scripts/scan_repo.py new file mode 100644 index 0000000..b75ad28 --- /dev/null +++ b/skills/team-wiki-codebase/scripts/scan_repo.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 +""" +scan_repo.py — 代码仓库结构扫描与统计工具 + +用途: Phase 0 源材料采集阶段,快速扫描目标仓库/目录,输出: + 1. 目录结构树(2层深度) + 2. 代码统计(语言分布、文件数、总行数) + 3. 关键文件发现(入口文件、配置文件、Proto/IDL、错误码定义) + 4. 代码热点(文件行数 Top 20) + +使用方式: + python3 scan_repo.py /path/to/repo + python3 scan_repo.py /path/to/repo --depth 3 --top 30 +""" + +import os +import sys +import argparse +from pathlib import Path +from collections import defaultdict, Counter + +# 关键文件匹配模式 +KEY_FILE_PATTERNS = { + "入口文件": [ + "main.py", "main.go", "app.py", "app.ts", "app.js", + "server.py", "server.go", "wsgi.py", "manage.py", + "cmd/*/main.go", "index.ts", "index.js", + ], + "路由/Handler": [ + "*handler*", "*router*", "*controller*", "*dispatch*", + "*route*", "*api.*", "*endpoint*", + ], + "配置文件": [ + "*.yaml", "*.yml", "*.toml", "*.ini", "*.conf", + "*config*", "*.env", "*.env.*", + ], + "Proto/IDL": [ + "*.proto", "*.thrift", "*.graphql", "*schema*", + ], + "数据库/模型": [ + "*model*", "*dao*", "*repository*", "*migration*", + "*schema*", "*.sql", "*db*", + ], + "常量/错误码": [ + "*const*", "*constant*", "*error*", "*code*", + "*enum*", "*define*", "*exception*", + ], + "测试文件": [ + "*_test.*", "test_*", "*.spec.*", "*_spec.*", + ], +} + +# 语言扩展名映射 +LANG_MAP = { + ".py": "Python", ".go": "Go", ".js": "JavaScript", ".ts": "TypeScript", + ".java": "Java", ".rs": "Rust", ".rb": "Ruby", ".php": "PHP", + ".c": "C", ".cpp": "C++", ".h": "C/C++ Header", + ".proto": "Protobuf", ".thrift": "Thrift", ".graphql": "GraphQL", + ".sql": "SQL", ".sh": "Shell", ".bash": "Shell", + ".yaml": "YAML", ".yml": "YAML", ".toml": "TOML", + ".json": "JSON", ".xml": "XML", ".md": "Markdown", +} + +# 忽略目录 +IGNORE_DIRS = { + ".git", ".svn", "node_modules", "__pycache__", ".tox", ".mypy_cache", + "venv", ".venv", "env", ".env", "vendor", "dist", "build", + ".idea", ".vscode", ".eggs", "*.egg-info", +} + + +def should_ignore(path: Path) -> bool: + for part in path.parts: + if part in IGNORE_DIRS or part.endswith(".egg-info"): + return True + return False + + +def count_lines(filepath: Path) -> int: + try: + with open(filepath, "r", encoding="utf-8", errors="ignore") as f: + return sum(1 for _ in f) + except (OSError, UnicodeDecodeError): + return 0 + + +def match_pattern(filename: str, pattern: str) -> bool: + """简单的通配符匹配""" + import fnmatch + return fnmatch.fnmatch(filename.lower(), pattern.lower()) + + +def scan_repository(repo_path: Path, depth: int = 2, top_n: int = 20): + """扫描仓库,返回统计结果""" + + all_files = [] + lang_stats = Counter() # 语言 -> (文件数, 行数) + lang_lines = Counter() + key_files = defaultdict(list) + dir_tree = [] + + # 遍历文件 + for root, dirs, files in os.walk(repo_path): + rel_root = Path(root).relative_to(repo_path) + + # 忽略目录 + dirs[:] = [d for d in dirs if d not in IGNORE_DIRS and not d.endswith(".egg-info")] + + # 目录树(限制深度) + level = len(rel_root.parts) + if level <= depth: + indent = " " * level + dirname = rel_root.parts[-1] if rel_root.parts else str(repo_path.name) + dir_tree.append(f"{indent}├── {dirname}/") + + for fname in files: + fpath = Path(root) / fname + if should_ignore(fpath.relative_to(repo_path)): + continue + + ext = fpath.suffix.lower() + lines = count_lines(fpath) + rel_path = str(fpath.relative_to(repo_path)) + + all_files.append((rel_path, ext, lines)) + + # 语言统计 + lang = LANG_MAP.get(ext) + if lang: + lang_stats[lang] += 1 + lang_lines[lang] += lines + + # 关键文件匹配 + for category, patterns in KEY_FILE_PATTERNS.items(): + for pattern in patterns: + if match_pattern(fname, pattern): + key_files[category].append((rel_path, lines)) + break + + return all_files, lang_stats, lang_lines, key_files, dir_tree + + +def print_report(repo_path: Path, all_files, lang_stats, lang_lines, key_files, dir_tree, top_n: int): + """输出扫描报告""" + + total_files = len(all_files) + total_lines = sum(f[2] for f in all_files) + + print("=" * 70) + print(f" 代码仓库扫描报告: {repo_path.name}") + print(f" 路径: {repo_path}") + print("=" * 70) + + # 1. 基本统计 + print(f"\n## 1. 基本统计\n") + print(f"| 指标 | 数值 |") + print(f"|------|------|") + print(f"| 总文件数 | {total_files} |") + print(f"| 总代码行数 | {total_lines:,} |") + print(f"| 语言种类 | {len(lang_stats)} |") + + # 2. 语言分布 + print(f"\n## 2. 语言分布\n") + print(f"| 语言 | 文件数 | 代码行数 | 占比 |") + print(f"|------|--------|---------|------|") + for lang, count in lang_stats.most_common(15): + lines = lang_lines[lang] + pct = f"{lines / total_lines * 100:.1f}%" if total_lines > 0 else "0%" + print(f"| {lang} | {count} | {lines:,} | {pct} |") + + # 3. 目录结构 + print(f"\n## 3. 目录结构(前 30 行)\n") + print("```") + for line in dir_tree[:30]: + print(line) + if len(dir_tree) > 30: + print(f" ... ({len(dir_tree) - 30} more directories)") + print("```") + + # 4. 关键文件发现 + print(f"\n## 4. 关键文件发现\n") + for category, files in key_files.items(): + if files: + print(f"\n### {category} ({len(files)} 个)\n") + # 去重并排序 + seen = set() + for fpath, lines in sorted(files, key=lambda x: -x[1])[:10]: + if fpath not in seen: + seen.add(fpath) + print(f"- `{fpath}` ({lines:,} 行)") + + # 5. 代码热点 + print(f"\n## 5. 代码热点 (Top {top_n})\n") + print(f"| 排名 | 文件 | 行数 |") + print(f"|------|------|------|") + sorted_files = sorted(all_files, key=lambda x: -x[2]) + for i, (fpath, ext, lines) in enumerate(sorted_files[:top_n], 1): + print(f"| {i} | `{fpath}` | {lines:,} |") + + print(f"\n{'=' * 70}") + print(f" 扫描完成。共 {total_files} 个文件,{total_lines:,} 行代码。") + print(f"{'=' * 70}") + + +def main(): + parser = argparse.ArgumentParser(description="代码仓库结构扫描与统计工具") + parser.add_argument("repo_path", help="要扫描的仓库/目录路径") + parser.add_argument("--depth", type=int, default=2, help="目录树深度 (默认 2)") + parser.add_argument("--top", type=int, default=20, help="代码热点 Top N (默认 20)") + args = parser.parse_args() + + repo_path = Path(args.repo_path).resolve() + if not repo_path.is_dir(): + print(f"错误: {repo_path} 不是有效目录", file=sys.stderr) + sys.exit(1) + + all_files, lang_stats, lang_lines, key_files, dir_tree = scan_repository( + repo_path, depth=args.depth, top_n=args.top + ) + print_report(repo_path, all_files, lang_stats, lang_lines, key_files, dir_tree, args.top) + + +if __name__ == "__main__": + main() diff --git a/skills/team-wiki-codebase/scripts/validate_kb.py b/skills/team-wiki-codebase/scripts/validate_kb.py new file mode 100644 index 0000000..22ac3d7 --- /dev/null +++ b/skills/team-wiki-codebase/scripts/validate_kb.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +""" +validate_kb.py — 知识库质量校验工具 + +用途: Phase 4 质量评估阶段,自动校验已生成知识库的: + 1. 链接完整性(检测死链接) + 2. search-anchor 覆盖率 + 3. AI 快速理解表覆盖率 + 4. 双向链接完整性 + 5. README 索引收录率 + +使用方式: + python3 validate_kb.py /path/to/knowledge-base-dir + python3 validate_kb.py /path/to/knowledge-base-dir --verbose +""" + +import os +import re +import sys +import argparse +from pathlib import Path +from collections import defaultdict + +# Markdown 链接正则: [text](path) 或 [text](path#anchor) +LINK_PATTERN = re.compile(r'\[([^\]]*)\]\(([^)]+)\)') +# search-anchor 正则 +ANCHOR_PATTERN = re.compile(r'', re.DOTALL) +# AI 快速理解表正则 +AI_TABLE_PATTERN = re.compile(r'##\s*🤖\s*AI\s*快速理解', re.IGNORECASE) +# 双向链接: 链接回主架构/技术架构文档 +BACK_LINK_PATTERN = re.compile(r'\[📘.*(?:主架构|技术架构)|在整体架构中的位置', re.IGNORECASE) + + +def find_md_files(kb_dir: Path) -> list: + """查找所有 .md 文件""" + md_files = [] + for root, dirs, files in os.walk(kb_dir): + dirs[:] = [d for d in dirs if not d.startswith('.')] + for f in files: + if f.endswith('.md'): + md_files.append(Path(root) / f) + return sorted(md_files) + + +def check_links(md_file: Path, kb_dir: Path) -> list: + """检查文件中的链接是否有效""" + broken = [] + try: + content = md_file.read_text(encoding='utf-8', errors='ignore') + except OSError: + return [("READ_ERROR", str(md_file), "无法读取文件")] + + for match in LINK_PATTERN.finditer(content): + link_text = match.group(1) + link_target = match.group(2) + + # 跳过外部链接和锚点链接 + if link_target.startswith(('http://', 'https://', 'mailto:', '#')): + continue + + # 分离路径和锚点 + path_part = link_target.split('#')[0] + if not path_part: + continue + + # 解析相对路径 + target_path = (md_file.parent / path_part).resolve() + if not target_path.exists(): + rel = str(md_file.relative_to(kb_dir)) + broken.append((rel, link_target, link_text)) + + return broken + + +def check_anchor(md_file: Path) -> bool: + """检查文件是否包含 search-anchor""" + try: + content = md_file.read_text(encoding='utf-8', errors='ignore') + return bool(ANCHOR_PATTERN.search(content)) + except OSError: + return False + + +def check_ai_table(md_file: Path) -> bool: + """检查文件是否包含 AI 快速理解表""" + try: + content = md_file.read_text(encoding='utf-8', errors='ignore') + return bool(AI_TABLE_PATTERN.search(content)) + except OSError: + return False + + +def check_back_link(md_file: Path) -> bool: + """检查组件文档是否有链接回主架构文档""" + try: + content = md_file.read_text(encoding='utf-8', errors='ignore') + return bool(BACK_LINK_PATTERN.search(content)) + except OSError: + return False + + +def check_readme_coverage(kb_dir: Path, md_files: list) -> tuple: + """检查 README 是否收录了所有 .md 文件""" + readme_path = kb_dir / "README.md" + if not readme_path.exists(): + return [], md_files + + readme_content = readme_path.read_text(encoding='utf-8', errors='ignore') + covered = [] + uncovered = [] + + for f in md_files: + if f.name == "README.md": + continue + # 检查 README 中是否提到了这个文件 + fname_no_ext = f.stem + if fname_no_ext in readme_content or f.name in readme_content: + covered.append(f) + else: + uncovered.append(f) + + return covered, uncovered + + +def main(): + parser = argparse.ArgumentParser(description="知识库质量校验工具") + parser.add_argument("kb_dir", help="知识库目录路径") + parser.add_argument("--verbose", "-v", action="store_true", help="输出详细信息") + args = parser.parse_args() + + kb_dir = Path(args.kb_dir).resolve() + if not kb_dir.is_dir(): + print(f"错误: {kb_dir} 不是有效目录", file=sys.stderr) + sys.exit(1) + + md_files = find_md_files(kb_dir) + if not md_files: + print(f"警告: {kb_dir} 中未找到任何 .md 文件") + sys.exit(0) + + # 过滤出组件设计文档(以数字编号开头的文件) + component_docs = [f for f in md_files if re.match(r'^\d+_', f.name)] + + print("=" * 70) + print(f" 知识库质量校验报告") + print(f" 目录: {kb_dir}") + print(f" 文件数: {len(md_files)} 个 .md 文件 (其中 {len(component_docs)} 个组件文档)") + print("=" * 70) + + total_score = 0 + max_score = 0 + + # 1. 链接完整性 + print(f"\n## 1. 链接完整性检查\n") + all_broken = [] + for f in md_files: + broken = check_links(f, kb_dir) + all_broken.extend(broken) + + if all_broken: + print(f"❌ 发现 {len(all_broken)} 个死链接:") + for src, target, text in all_broken[:20]: + print(f" {src} → [{text}]({target})") + if len(all_broken) > 20: + print(f" ... 还有 {len(all_broken) - 20} 个") + else: + print(f"✅ 所有链接有效 (检查了 {len(md_files)} 个文件)") + total_score += 20 + max_score += 20 + + # 2. search-anchor 覆盖率 + print(f"\n## 2. Search-Anchor 覆盖率\n") + has_anchor = sum(1 for f in md_files if check_anchor(f)) + anchor_pct = has_anchor / len(md_files) * 100 if md_files else 0 + print(f"{'✅' if anchor_pct >= 80 else '⚠️'} {has_anchor}/{len(md_files)} 个文件有 search-anchor ({anchor_pct:.0f}%)") + if args.verbose: + for f in md_files: + if not check_anchor(f): + print(f" 缺失: {f.relative_to(kb_dir)}") + if anchor_pct >= 80: + total_score += 20 + elif anchor_pct >= 50: + total_score += 10 + max_score += 20 + + # 3. AI 快速理解表覆盖率(仅检查组件文档) + print(f"\n## 3. AI 快速理解表覆盖率 (组件文档)\n") + if component_docs: + has_ai_table = sum(1 for f in component_docs if check_ai_table(f)) + ai_pct = has_ai_table / len(component_docs) * 100 + print(f"{'✅' if ai_pct >= 90 else '⚠️'} {has_ai_table}/{len(component_docs)} 个组件文档有 AI 快速理解表 ({ai_pct:.0f}%)") + if args.verbose: + for f in component_docs: + if not check_ai_table(f): + print(f" 缺失: {f.relative_to(kb_dir)}") + if ai_pct >= 90: + total_score += 20 + elif ai_pct >= 60: + total_score += 10 + else: + print("⚠️ 未发现编号开头的组件文档") + max_score += 20 + + # 4. 双向链接检查(组件文档是否链接回主架构) + print(f"\n## 4. 双向链接检查 (组件→主架构)\n") + if component_docs: + has_back = sum(1 for f in component_docs if check_back_link(f)) + back_pct = has_back / len(component_docs) * 100 + print(f"{'✅' if back_pct >= 90 else '⚠️'} {has_back}/{len(component_docs)} 个组件文档有回链到主架构 ({back_pct:.0f}%)") + if back_pct >= 90: + total_score += 20 + elif back_pct >= 60: + total_score += 10 + else: + print("⚠️ 未发现编号开头的组件文档") + max_score += 20 + + # 5. README 索引覆盖率 + print(f"\n## 5. README 索引覆盖率\n") + covered, uncovered = check_readme_coverage(kb_dir, md_files) + if (kb_dir / "README.md").exists(): + cover_pct = len(covered) / (len(covered) + len(uncovered)) * 100 if (covered or uncovered) else 100 + print(f"{'✅' if cover_pct >= 90 else '⚠️'} README 收录了 {len(covered)}/{len(covered)+len(uncovered)} 个文档 ({cover_pct:.0f}%)") + if uncovered and args.verbose: + print(" 未收录:") + for f in uncovered[:10]: + print(f" {f.relative_to(kb_dir)}") + if cover_pct >= 90: + total_score += 20 + elif cover_pct >= 60: + total_score += 10 + else: + print("❌ 未找到 README.md") + max_score += 20 + + # 总结 + final_pct = total_score / max_score * 100 if max_score else 0 + print(f"\n{'=' * 70}") + print(f" 综合评分: {total_score}/{max_score} ({final_pct:.0f}%)") + if final_pct >= 90: + print(f" 评级: ✅ 优秀 — 知识库质量达标") + elif final_pct >= 70: + print(f" 评级: ⚠️ 良好 — 建议修复上述问题") + else: + print(f" 评级: ❌ 需改进 — 存在较多质量问题") + print(f"{'=' * 70}") + + +if __name__ == "__main__": + main() diff --git a/src/__tests__/auto-recall.test.ts b/src/__tests__/auto-recall.test.ts index 4ce1cd2..8fdecde 100644 --- a/src/__tests__/auto-recall.test.ts +++ b/src/__tests__/auto-recall.test.ts @@ -28,8 +28,17 @@ function writeSearchIndex(homeDir: string): void { fs.mkdirSync(indexDir, { recursive: true }); const index = { + version: 4, builtAt: new Date().toISOString(), elapsedMs: 10, + df: { + 'title:k8s': 1, 'title:pod': 1, 'title:oomkilled': 1, 'title:排查': 1, 'title:修复': 1, + 'tag:k8s': 1, 'tag:oom': 1, 'tag:troubleshooting': 1, + 'oom': 1, 'killed': 1, 'memory': 1, 'limit': 1, 'container': 1, 'restart': 1, + 'title:modulenotfounderror': 1, 'title:常见': 1, 'title:解决方案': 1, + 'tag:python': 1, 'tag:import': 1, 'tag:modulenotfounderror': 1, + 'module': 1, 'not': 1, 'found': 1, 'import': 1, 'pip': 1, 'install': 1, + }, entries: [ { filename: 'k8s-oom-fix-2026-03-20-abc123.md', @@ -43,6 +52,8 @@ function writeSearchIndex(homeDir: string): void { 'oom', 'killed', 'memory', 'limit', 'container', 'restart', ], votes: 3, + type: 'learnings', + domain: 'technical', }, { filename: 'module-not-found-fix-2026-03-22-def456.md', @@ -56,6 +67,8 @@ function writeSearchIndex(homeDir: string): void { 'module', 'not', 'found', 'import', 'pip', 'install', ], votes: 2, + type: 'learnings', + domain: 'technical', }, ], }; diff --git a/src/__tests__/ci-extract-mr.test.ts b/src/__tests__/ci-extract-mr.test.ts index 1bb80a3..e1fd174 100644 --- a/src/__tests__/ci-extract-mr.test.ts +++ b/src/__tests__/ci-extract-mr.test.ts @@ -73,10 +73,11 @@ describe('ciExtractMr', () => { all: true, dryRun: true, })); + // codebase suggestions 不再通过 comment 发布(由图谱变更 comment 替代) expect(mockPostOrUpdateMrComment).toHaveBeenCalledWith( 'https://github.com/org/repo/pull/1', expect.objectContaining({ title: 'Test Learning' }), - expect.arrayContaining([expect.objectContaining({ section: 'arch' })]), + undefined, undefined, undefined, ); @@ -106,14 +107,14 @@ describe('ciExtractMr', () => { expect(learnings.length).toBe(1); expect(learnings[0]).toContain('Test-Learning'); - // codebase 被更新 - expect(mockApplyCodebaseSuggestions).toHaveBeenCalled(); + // codebase direct 模式已被图谱引擎替代,不再调用 applyCodebaseSuggestions + // mockApplyCodebaseSuggestions 不应被调用 - // push 被调用 + // push 被调用(仅含 learning,不含 docs/codebase.md) expect(mockPushRepoDirectly).toHaveBeenCalledWith( teamRepo, expect.stringContaining('[teamai]'), - expect.arrayContaining(['docs/codebase.md']), + expect.not.arrayContaining(['docs/codebase.md']), ); }); @@ -175,7 +176,7 @@ describe('ciExtractMr', () => { expect(mockPostOrUpdateMrComment).toHaveBeenCalledWith( expect.any(String), expect.anything(), - expect.anything(), + undefined, undefined, true, ); diff --git a/src/__tests__/contribute-check-phase2.test.ts b/src/__tests__/contribute-check-phase2.test.ts index 5e3c79f..c7e5297 100644 --- a/src/__tests__/contribute-check-phase2.test.ts +++ b/src/__tests__/contribute-check-phase2.test.ts @@ -128,7 +128,7 @@ describe('applyPhase2Adjustments', () => { const gitRepo = path.resolve(__dirname, '../../'); const veryOldStart = '2020-01-01T00:00:00Z'; const result = applyPhase2Adjustments(5, sessionId, gitRepo, veryOldStart); - expect(result.score).toBe(0); + expect(result.score).toBe(5); }); }); diff --git a/src/builtin-skills.ts b/src/builtin-skills.ts index d0fa214..a19000c 100644 --- a/src/builtin-skills.ts +++ b/src/builtin-skills.ts @@ -38,7 +38,7 @@ function getBuiltinSkillsDir(): string { } /** Names of CLI built-in skills. Used by push to exclude them from team repo push. */ -export const BUILTIN_SKILL_NAMES = new Set(['teamai-share-learnings', 'teamai-wiki']); +export const BUILTIN_SKILL_NAMES = new Set(['teamai-share-learnings', 'teamai-wiki', 'team-wiki-codebase']); /** * Deploy CLI built-in skills to all configured AI tool skill directories. diff --git a/src/ci/extract-mr.ts b/src/ci/extract-mr.ts index 3d63998..c2be3c7 100644 --- a/src/ci/extract-mr.ts +++ b/src/ci/extract-mr.ts @@ -13,12 +13,12 @@ import path from 'node:path'; import os from 'node:os'; import { importFromMR } from '../import-mr.js'; -import { applyCodebaseSuggestions } from '../codebase.js'; +// applyCodebaseSuggestions removed: codebase updates now handled by teamwiki/ graph engine import { appendPendingReview } from '../review-store.js'; import { pushRepoDirectly } from '../utils/git.js'; import { log } from '../utils/logger.js'; import type { LearningDraft, CodebaseSuggestion } from '../types.js'; -import { postOrUpdateMrComment, postIndividualComments, parseMrUrl } from './mr-comment.js'; +import { postOrUpdateMrComment, postIndividualComments, postCodebaseGraphComment, parseMrUrl } from './mr-comment.js'; import { readRejections, shouldWrite } from './read-rejections.js'; // ─── 类型 ──────────────────────────────────────────────── @@ -102,9 +102,14 @@ async function writeKnowledgeToRepo( writeMode: 'direct' | 'pending-review', mrUrl: string, dryRun?: boolean, + graphWritten?: boolean, ): Promise { const changedFiles: string[] = []; + if (graphWritten) { + changedFiles.push('teamwiki'); + } + // 写入 learning if (learning) { const safeTitle = learning.title @@ -125,20 +130,11 @@ async function writeKnowledgeToRepo( } // 处理 codebase suggestions + // NOTE: direct 模式的 AI 重写已被 teamwiki/ 图谱增量更新替代(Phase 3.3) + // suggestions 仅在 pending-review 模式下写入 jsonl 供人工审阅 if (suggestions && suggestions.length > 0) { if (writeMode === 'direct') { - const codebasePath = path.join(teamRepo, 'docs', 'codebase.md'); - try { - const existing = await fs.readFile(codebasePath, 'utf-8'); - const updated = await applyCodebaseSuggestions(existing, suggestions); - if (!dryRun) { - await fs.writeFile(codebasePath, updated, 'utf-8'); - } - log.success('Codebase.md 已更新'); - changedFiles.push('docs/codebase.md'); - } catch { - log.warn('docs/codebase.md 不存在或读取失败,跳过 codebase 更新'); - } + log.debug('Codebase suggestions (direct mode): 图谱变更已在 comment/write 阶段处理,跳过 AI 重写'); } else { // pending-review 模式 for (const s of suggestions) { @@ -243,15 +239,16 @@ export async function ciExtractMr(opts: CiExtractMrOptions): Promise { } // 执行 comment + // NOTE: codebase suggestions 不再作为独立 comment 发布,已被图谱变更 comment 替代 if (opts.mode === 'comment' || opts.mode === 'both') { if (opts.individualComments) { - const { posted } = await postIndividualComments(opts.url, learning, suggestions, opts.dryRun); + const { posted } = await postIndividualComments(opts.url, learning, undefined, opts.dryRun); log.success(`已发布 ${posted} 条独立建议 comment`); } else { const result = await postOrUpdateMrComment( opts.url, learning, - suggestions, + undefined, opts.commentMarker, opts.dryRun, ); @@ -266,6 +263,57 @@ export async function ciExtractMr(opts: CiExtractMrOptions): Promise { } } + // ── Codebase 图谱变更 ────────────────────────────────────── + let graphChangeSummary: { added: string[]; removed: string[] } | undefined; + try { + const { collectCode, extractCodeFacts, buildCodeGraph } = await import('../wiki-engine/adapters/index.js'); + const { execFileSync } = await import('node:child_process'); + const businessRepo = process.cwd(); + + // 从 git 获取当前 MR/PR 的变更文件列表 + // 尝试多种方式,兼容 shallow clone(depth=1 时 HEAD~1 不存在) + let changedFiles: string[] = []; + const diffCommands = [ + ['diff', '--name-only', 'HEAD~1', 'HEAD'], + ['show', '--name-only', '--format=', 'HEAD'], + ['diff', '--name-only', 'origin/master...HEAD'], + ]; + for (const args of diffCommands) { + try { + const diffOutput = execFileSync( + 'git', args, + { cwd: businessRepo, encoding: 'utf-8', timeout: 10_000 }, + ); + changedFiles = diffOutput.trim().split('\n') + .filter(f => f && /\.(ts|tsx|js|jsx|py|go|rs|java)$/.test(f)); + if (changedFiles.length > 0) break; + } catch { + continue; + } + } + if (changedFiles.length === 0) { + log.debug('[codebase-graph] 所有 git diff 方式均失败或无源文件变更'); + } + + if (changedFiles.length > 0) { + const { files } = await collectCode({ root: businessRepo, changedFiles, maxFiles: 50 }); + if (files.length > 0) { + const facts = extractCodeFacts(files); + const graph = buildCodeGraph(facts); + graphChangeSummary = { + added: graph.nodes.map(n => `\`${n.type}:${n.title}\` (${n.slug})`), + removed: [], + }; + + if ((opts.mode === 'comment' || opts.mode === 'both') && graphChangeSummary.added.length > 0) { + await postCodebaseGraphComment(opts.url, graphChangeSummary, opts.dryRun); + } + } + } + } catch (err) { + log.debug(`[codebase-graph] 图谱变更提取失败(非阻塞): ${err instanceof Error ? err.message : err}`); + } + // 执行 write if (opts.mode === 'write' || opts.mode === 'both') { // 当使用 individual comments 时,读取 rejection 状态进行过滤 @@ -296,6 +344,43 @@ export async function ciExtractMr(opts: CiExtractMrOptions): Promise { } } + // ── 图谱变更写入 team-repo/teamwiki/ ─────────────────── + let graphWritten = false; + if (graphChangeSummary && graphChangeSummary.added.length > 0 && !opts.dryRun) { + let graphRejected = false; + if (opts.individualComments) { + const parsed = parseMrUrl(opts.url); + const rejections = await readRejections(opts.url); + if (!shouldWrite('codebase-graph', rejections, parsed.provider)) { + graphRejected = true; + log.info('Codebase 图谱变更被 reject,跳过写入'); + } + } + + if (!graphRejected) { + try { + const { extractCodebase } = await import('../codebase-extract.js'); + const businessRepo = process.cwd(); + const parsed = parseMrUrl(opts.url); + const projectName = parsed.repo; + + await extractCodebase({ path: businessRepo, project: projectName }); + + const fse = await import('fs-extra'); + const srcWiki = path.join(businessRepo, 'teamwiki'); + const teamWikiRoot = path.join(path.resolve(opts.teamRepo!), 'teamwiki'); + if (await fse.pathExists(srcWiki)) { + await fse.copy(srcWiki, teamWikiRoot, { overwrite: true }); + await fse.remove(srcWiki).catch(() => {}); + graphWritten = true; + log.success(`teamwiki/ 图谱已更新到团队仓库`); + } + } catch (err) { + log.debug(`[codebase-graph] 图谱写入失败(非阻塞): ${err instanceof Error ? err.message : err}`); + } + } + } + await writeKnowledgeToRepo( opts.teamRepo!, filteredLearning, @@ -303,6 +388,7 @@ export async function ciExtractMr(opts: CiExtractMrOptions): Promise { opts.writeMode ?? 'direct', opts.url, opts.dryRun, + graphWritten, ); } diff --git a/src/ci/mr-comment.ts b/src/ci/mr-comment.ts index d1affc5..700c635 100644 --- a/src/ci/mr-comment.ts +++ b/src/ci/mr-comment.ts @@ -505,3 +505,73 @@ export async function postIndividualComments( log.success(`已发布 ${posted} 条独立建议`); return { posted }; } + +// ─── Codebase Graph Change Comment ────────────────────── + +const CODEBASE_GRAPH_MARKER = ''; + +function formatGraphComment(summary: { added: string[]; removed: string[] }): string { + const lines: string[] = []; + lines.push('## 📊 Codebase 知识图谱变更'); + lines.push(''); + lines.push('本次 MR 触发了以下代码知识更新:'); + lines.push(''); + + if (summary.added.length > 0) { + lines.push(`### 新增节点 (${summary.added.length})`); + for (const item of summary.added.slice(0, 20)) { + lines.push(`- ${item}`); + } + if (summary.added.length > 20) { + lines.push(`- _...及另外 ${summary.added.length - 20} 项_`); + } + lines.push(''); + } + + if (summary.removed.length > 0) { + lines.push(`### 删除节点 (${summary.removed.length})`); + for (const item of summary.removed.slice(0, 10)) { + lines.push(`- ${item}`); + } + lines.push(''); + } + + lines.push('---'); + lines.push('> 👎 对本条 comment 添加 reaction 将阻止本次图谱更新写入团队知识库'); + lines.push(CODEBASE_GRAPH_MARKER); + return lines.join('\n'); +} + +export async function postCodebaseGraphComment( + mrUrl: string, + summary: { added: string[]; removed: string[] }, + dryRun?: boolean, +): Promise { + const body = formatGraphComment(summary); + const parsed = parseMrUrl(mrUrl); + + if (dryRun) { + log.info('[dry-run] Codebase graph comment:'); + console.log(body); + return; + } + + if (parsed.provider === 'github') { + const existing = await findGitHubComment(parsed.owner, parsed.repo, parsed.number, CODEBASE_GRAPH_MARKER); + if (existing) { + await updateGitHubComment(parsed.owner, parsed.repo, existing.id, body); + } else { + await postGitHubComment(parsed.owner, parsed.repo, parsed.number, body); + } + } else { + const projectId = encodeURIComponent(`${parsed.owner}/${parsed.repo}`); + const mrGlobalId = await getMrGlobalId(projectId, parsed.number); + const existing = await findTGitComment(projectId, mrGlobalId, CODEBASE_GRAPH_MARKER); + if (existing) { + await updateTGitComment(projectId, mrGlobalId, existing.id, body); + } else { + await postTGitComment(projectId, mrGlobalId, body); + } + } + log.success('Codebase 图谱变更 comment 已发布'); +} diff --git a/src/clone.ts b/src/clone.ts index a8880e2..aa15000 100644 --- a/src/clone.ts +++ b/src/clone.ts @@ -3,6 +3,7 @@ import { spawn } from 'node:child_process'; import fs from 'fs-extra'; import { getGitHubToken } from './providers/github/gh-cli.js'; +import { gfGetOAuthToken } from './providers/tgit/gf-cli.js'; import { log } from './utils/logger.js'; // ─── Types ────────────────────────────────────────────── @@ -36,6 +37,18 @@ function isSshUrl(url: string): boolean { return url.startsWith('git@') || (!url.includes('://') && url.includes(':')); } +/** + * 将 HTTP/HTTPS URL 转换为 SSH 格式。 + * 如 https://git.woa.com/HAI/hai_api.git → git@git.woa.com:HAI/hai_api.git + */ +function convertHttpToSsh(url: string): string { + const match = url.match(/^https?:\/\/([^/]+)\/(.+)$/); + if (match) { + return `git@${match[1]}:${match[2]}`; + } + return url; +} + /** * 将 URL 中的认证信息脱敏,用于日志和错误消息。 * 替换 https://[anything]@ 为 https://***@ @@ -156,9 +169,9 @@ export async function shallowClone( let githubToken: string | undefined; if (forceSsh || isSshUrl(url)) { - cloneUrl = url; + cloneUrl = isSshUrl(url) ? url : convertHttpToSsh(url); cloneMethod = 'ssh'; - log.debug(`shallowClone: 使用 SSH 克隆 ${url}`); + log.debug(`shallowClone: 使用 SSH 克隆 ${cloneUrl}`); } else if (forceAnonymous) { cloneUrl = url; cloneMethod = 'https-anonymous'; @@ -175,9 +188,21 @@ export async function shallowClone( cloneMethod = 'https-anonymous'; log.debug(`shallowClone: 使用匿名 HTTPS 克隆 github 仓库`); } + } else if (provider === 'tgit') { + // TGit: 使用 OAuth token 嵌入 URL(netrc 非标准字段导致 git credential 不稳定) + const tgitToken = gfGetOAuthToken(); + cloneUrl = url.replace(/^http:\/\//, 'https://'); + if (tgitToken) { + cloneUrl = cloneUrl.replace('https://', `https://oauth2:${tgitToken}@`); + cloneMethod = 'https-token'; + log.debug(`shallowClone: 使用 HTTPS+token 克隆 tgit 仓库`); + } else { + cloneMethod = 'https-anonymous'; + log.debug(`shallowClone: 无 TGit token,尝试匿名 HTTPS 克隆`); + } } else { - // tgit 或其他 provider,依赖 ~/.netrc - cloneUrl = url; + // 其他 provider,依赖 ~/.netrc + cloneUrl = url.replace(/^http:\/\//, 'https://'); cloneMethod = 'https-anonymous'; log.debug(`shallowClone: 使用 HTTPS (~/.netrc) 克隆 ${provider} 仓库`); } diff --git a/src/code-knowledge-recall.ts b/src/code-knowledge-recall.ts new file mode 100644 index 0000000..a372515 --- /dev/null +++ b/src/code-knowledge-recall.ts @@ -0,0 +1,323 @@ +/** + * Graph-aware codebase knowledge recall (BM25 + graph-boost). + * + * Recall algorithm based on Team Wiki's wiki-query design by @lurkacai. + * Implements scored mode with graph neighbor boosting. + */ + +import { readFile, readdir } from 'node:fs/promises'; +import path from 'node:path'; + +import type { GraphIndex } from './wiki-engine/core/graph-index.schema.js'; + +export interface CodeKnowledgeResult { + page: string; + title: string; + score: number; + snippet: string; + kind: 'codebase'; +} + +interface CorpusStats { + totalDocs: number; + avgDocLength: number; + df: Map; +} + +interface PageDoc { + path: string; + title: string; + content: string; + tokens: string[]; + tokenCount: number; // B10: raw (non-deduplicated) token count for BM25 dl +} + +const BM25_K1 = 1.5; +const BM25_B = 0.75; +const TITLE_BOOST = 3.0; +const RELATION_WEIGHT: Record = { DEPENDS_ON: 3, REFERENCES: 2, MAPS_TO: 2, CONTAINS: 1 }; +const ENTRY_NODE_BOOST = 8; + +function tokenize(text: string): string[] { + const tokens: string[] = []; + const lower = text.toLowerCase(); + // Split camelCase before tokenizing (B4 fix: camelCase splitting) + const camelSplit = lower.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase(); + const words = camelSplit.split(/[^a-z0-9一-鿿]+/).filter((w) => w.length >= 2); + for (const w of words) { + tokens.push(w); + } + // B14: CJK bigram segmentation + const cjkRuns = lower.match(/[一-鿿]+/g) ?? []; + for (const run of cjkRuns) { + for (let i = 0; i < run.length - 1; i++) { + tokens.push(run.slice(i, i + 2)); + } + } + return [...new Set(tokens)]; +} + +/** Raw (non-deduplicated) token count for BM25 dl (B10 fix) */ +function rawTokenCount(text: string): number { + const lower = text.toLowerCase(); + const camelSplit = lower.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase(); + return camelSplit.split(/[^a-z0-9一-鿿]+/).filter((w) => w.length >= 2).length; +} + +function countOccurrences(text: string, token: string): number { + let count = 0; + let idx = 0; + const lower = text.toLowerCase(); + while (true) { + idx = lower.indexOf(token, idx); + if (idx === -1) break; + count++; + idx += token.length; + } + return count; +} + +function buildCorpusStats(pages: PageDoc[]): CorpusStats { + const df = new Map(); + let totalLength = 0; + + for (const page of pages) { + totalLength += page.tokenCount; // B10: use raw token count for avgDocLength + const seen = new Set(); + for (const token of page.tokens) { + if (!seen.has(token)) { + seen.add(token); + df.set(token, (df.get(token) ?? 0) + 1); + } + } + } + + return { + totalDocs: pages.length, + avgDocLength: pages.length > 0 ? totalLength / pages.length : 1, + df, + }; +} + +function scoreBM25(page: PageDoc, queryTokens: string[], stats: CorpusStats): number { + let score = 0; + const dl = page.tokenCount; // B10: use raw count, not unique count + const { totalDocs, avgDocLength, df } = stats; + + for (const token of queryTokens) { + const docFreq = df.get(token) ?? 0; + const idf = Math.log((totalDocs - docFreq + 0.5) / (docFreq + 0.5) + 1); + const tf = countOccurrences(page.content, token); + const tfNorm = (tf * (BM25_K1 + 1)) / (tf + BM25_K1 * (1 - BM25_B + BM25_B * dl / avgDocLength)); + const titleHit = page.title.toLowerCase().includes(token) ? TITLE_BOOST : 0; + score += idf * (tfNorm + titleHit); + } + + return score; +} + +/** + * B8 fix: Match graph nodes to pages by slug/title instead of raw file paths. + * Returns a set of node slugs that match the query. + */ +function findEntryNodes(queryTokens: string[], graph: GraphIndex): Set { + const entries = new Set(); + for (const node of graph.nodes) { + const text = `${node.slug} ${node.title}`.toLowerCase(); + for (const token of queryTokens) { + if (token.length > 1 && text.includes(token)) { + entries.add(node.slug); + break; + } + } + } + return entries; +} + +/** + * B8 fix: Match page paths to graph node slugs via title/filename matching. + * B24 fix: Use 2-hop neighbors (halved weight for second hop). + */ +function computeGraphBoost(page: PageDoc, entryNodes: Set, graph: GraphIndex): number { + // Match page to graph nodes by title + const pageTitle = page.title.toLowerCase(); + const pageFile = page.path.replace(/^evidence\/code\/[^/]+\//, '').replace('.md', ''); + + // Check if this page IS an entry node (by title or slug match) + for (const slug of entryNodes) { + const slugParts = slug.split('/'); + const slugName = (slugParts.pop() ?? '').toLowerCase(); + if (slugName && (pageTitle.includes(slugName) || pageFile.includes(slugName))) { + return ENTRY_NODE_BOOST; + } + } + + // Check 1-hop and 2-hop neighbors + let maxBoost = 0; + for (const edge of graph.edges) { + const isFrom = entryNodes.has(edge.from); + const isTo = entryNodes.has(edge.to); + if (!isFrom && !isTo) continue; + + const neighborSlug = isFrom ? edge.to : edge.from; + const neighborParts = neighborSlug.split('/'); + const neighborName = (neighborParts.pop() ?? '').toLowerCase(); + + if (neighborName && (pageTitle.includes(neighborName) || pageFile.includes(neighborName))) { + const relWeight = RELATION_WEIGHT[edge.relation] ?? 1; + const boost = relWeight * 0.8; // 1-hop + if (boost > maxBoost) maxBoost = boost; + } + + // 2-hop: check neighbors of this neighbor (B24) + for (const edge2 of graph.edges) { + if (edge2.from !== neighborSlug && edge2.to !== neighborSlug) continue; + const hop2Slug = edge2.from === neighborSlug ? edge2.to : edge2.from; + const hop2Parts = hop2Slug.split('/'); + const hop2Name = (hop2Parts.pop() ?? '').toLowerCase(); + if (hop2Name && (pageTitle.includes(hop2Name) || pageFile.includes(hop2Name))) { + const relWeight = RELATION_WEIGHT[edge2.relation] ?? 1; + const boost = relWeight * 0.4; // 2-hop: half weight + if (boost > maxBoost) maxBoost = boost; + } + } + } + return maxBoost; +} + +function extractSnippet(content: string, queryTokens: string[], maxLen: number = 300): string { + const lower = content.toLowerCase(); + let bestIdx = 0; + for (const token of queryTokens) { + const idx = lower.indexOf(token); + if (idx >= 0) { + bestIdx = idx; + break; + } + } + const start = Math.max(0, bestIdx - 50); + const end = Math.min(content.length, start + maxLen); + let snippet = content.slice(start, end).replace(/\n+/g, ' ').trim(); + if (start > 0) snippet = '...' + snippet; + if (end < content.length) snippet += '...'; + return snippet; +} + +async function loadWikiPages(wikiRoot: string): Promise { + const evidenceDir = path.join(wikiRoot, 'evidence', 'code'); + const pages: PageDoc[] = []; + + let projects: string[]; + try { + projects = await readdir(evidenceDir); + } catch { + return pages; + } + + for (const project of projects) { + const projectDir = path.join(evidenceDir, project); + let files: string[]; + try { + files = await readdir(projectDir); + } catch { + continue; + } + for (const file of files) { + if (!file.endsWith('.md')) continue; + try { + const filePath = path.join(projectDir, file); + const content = await readFile(filePath, 'utf-8'); + const titleMatch = content.match(/^title:\s*(.+)$/m); + const title = titleMatch ? titleMatch[1].trim() : file.replace('.md', ''); + pages.push({ + path: `evidence/code/${project}/${file}`, + title, + content, + tokens: tokenize(content), + tokenCount: rawTokenCount(content), // B10: raw count for BM25 dl + }); + } catch { + continue; + } + } + } + + return pages; +} + +// B7: Use protocol loadGraphIndex instead of local implementation +async function loadGraph(wikiRoot: string): Promise { + const { loadGraphIndex } = await import('./wiki-engine/core/graph-index.schema.js'); + return loadGraphIndex(wikiRoot); +} + +export interface QueryCodeKnowledgeOptions { + wikiRoot: string; + limit?: number; + depth?: 'route' | 'context' | 'lookup'; +} + +export async function queryCodeKnowledge( + query: string, + options: QueryCodeKnowledgeOptions, +): Promise { + const { wikiRoot, limit = 5, depth = 'context' } = options; + + const pages = await loadWikiPages(wikiRoot); + if (pages.length === 0) return []; + + const graph = await loadGraph(wikiRoot); + const queryTokens = tokenize(query); + if (queryTokens.length === 0) return []; + + const stats = buildCorpusStats(pages); + const entryNodes = graph ? findEntryNodes(queryTokens, graph) : new Set(); + + const scored: Array<{ page: PageDoc; score: number }> = []; + for (const page of pages) { + let score = scoreBM25(page, queryTokens, stats); + if (graph) { + score += computeGraphBoost(page, entryNodes, graph); + } + if (score > 0) { + scored.push({ page, score }); + } + } + + scored.sort((a, b) => b.score - a.score); + + const TOKEN_BUDGET: Record = { route: 500, context: 5000, lookup: 3000 }; + const budget = TOKEN_BUDGET[depth] ?? 5000; + const estimateTokens = (text: string) => Math.ceil(text.length / 3.5); + + const results: CodeKnowledgeResult[] = []; + let tokenUsed = 0; + + for (const { page, score } of scored) { + if (results.length >= limit) break; + + let snippet: string; + if (depth === 'route') { + snippet = page.title; + } else if (depth === 'lookup' && results.length === 0) { + const maxChars = Math.floor(budget * 3.5 * 0.7); + snippet = page.content.slice(0, maxChars); + } else { + snippet = extractSnippet(page.content, queryTokens); + } + + const cost = estimateTokens(page.title + ' ' + snippet); + if (tokenUsed + cost > budget && results.length > 0) break; + tokenUsed += cost; + + results.push({ + page: page.path, + title: page.title, + score, + snippet, + kind: 'codebase', + }); + } + + return results; +} diff --git a/src/codebase-cmd.ts b/src/codebase-cmd.ts index 9b22318..7df3cf6 100644 --- a/src/codebase-cmd.ts +++ b/src/codebase-cmd.ts @@ -1,3 +1,5 @@ +import path from 'node:path'; + import chalk from 'chalk'; import type { GlobalOptions } from './types.js'; @@ -15,6 +17,7 @@ export interface CodebaseCmdOptions extends GlobalOptions { fix?: boolean; extract?: boolean | string; incremental?: boolean; + upgradeWiki?: boolean; severity?: Severity; staleDays?: string; pendingReviewThreshold?: string; @@ -61,6 +64,13 @@ function hasHighIssues(report: LintReport): boolean { export async function codebaseCmd(opts: CodebaseCmdOptions): Promise { const cwd = process.cwd(); + if (opts.upgradeWiki) { + const { upgradeCodebaseWiki } = await import('./codebase-upgrade-wiki.js'); + await upgradeCodebaseWiki({ cwd, dryRun: opts.dryRun, json: opts.json }); + return; + } + + if (opts.extract) { const { extractCodebase } = await import('./codebase-extract.js'); const extractPath = typeof opts.extract === 'string' ? opts.extract : cwd; @@ -87,9 +97,24 @@ export async function codebaseCmd(opts: CodebaseCmdOptions): Promise { return; } - const staleDays = opts.staleDays ? parseInt(opts.staleDays, 10) : 60; + // 若 teamwiki/ 存在,优先使用图谱 lint + const { pathExists } = await import('./utils/fs.js'); + const teamwikiDir = path.join(cwd, 'teamwiki'); + if (await pathExists(teamwikiDir)) { + const { lintTeamwiki, formatWikiLintReport } = await import('./codebase-wiki-lint.js'); + const report = await lintTeamwiki({ cwd, severity: opts.severity as 'high' | 'medium' | 'low' | 'info' }); + if (opts.json) { + console.log(JSON.stringify(report, null, 2)); + } else { + console.log(formatWikiLintReport(report)); + } + if (report.summary.high > 0) process.exitCode = 1; + return; + } + + const staleDays = opts.staleDays ? (parseInt(opts.staleDays, 10) || 60) : 60; const pendingThreshold = opts.pendingReviewThreshold - ? parseInt(opts.pendingReviewThreshold, 10) + ? (parseInt(opts.pendingReviewThreshold, 10) || 10) : 10; const severity = opts.severity ?? 'info'; diff --git a/src/codebase-upgrade-wiki.ts b/src/codebase-upgrade-wiki.ts new file mode 100644 index 0000000..32903d5 --- /dev/null +++ b/src/codebase-upgrade-wiki.ts @@ -0,0 +1,116 @@ +import { readdir, readFile, rm } from 'node:fs/promises'; +import path from 'node:path'; + +import chalk from 'chalk'; +import matter from 'gray-matter'; + +import { extractCodebase } from './codebase-extract.js'; +import { log } from './utils/logger.js'; +import { pathExists } from './utils/fs.js'; + +export interface UpgradeCodebaseWikiOptions { + cwd: string; + dryRun?: boolean; + json?: boolean; +} + +interface MigrationResult { + migrated: string[]; + skipped: string[]; + errors: string[]; +} + +export async function upgradeCodebaseWiki(opts: UpgradeCodebaseWikiOptions): Promise { + const teamCodebaseDir = path.join(opts.cwd, 'docs', 'team-codebase', 'repos'); + + if (!await pathExists(teamCodebaseDir)) { + if (opts.json) { + console.log(JSON.stringify({ status: 'nothing-to-migrate', reason: 'docs/team-codebase/repos/ not found' })); + } else { + log.info('未发现 docs/team-codebase/repos/ 目录,无需迁移。'); + } + return; + } + + const files = await readdir(teamCodebaseDir); + const mdFiles = files.filter(f => f.endsWith('.md')); + + if (mdFiles.length === 0) { + if (opts.json) { + console.log(JSON.stringify({ status: 'nothing-to-migrate', reason: 'no .md files in repos/' })); + } else { + log.info('repos/ 下无 .md 文件,无需迁移。'); + } + return; + } + + if (!opts.json) { + log.info(`发现 ${mdFiles.length} 个旧格式仓库文档,开始迁移到 teamwiki/ 图谱格式...`); + } + + const result: MigrationResult = { migrated: [], skipped: [], errors: [] }; + + for (const file of mdFiles) { + const slug = file.replace('.md', ''); + const filePath = path.join(teamCodebaseDir, file); + + try { + const content = await readFile(filePath, 'utf-8'); + const parsed = matter(content); + const source = parsed.data['source'] ?? parsed.data['repo_url']; + + if (!source) { + result.skipped.push(`${slug}: 无 source/repo_url 字段`); + continue; + } + + if (opts.dryRun) { + result.migrated.push(`${slug} → teamwiki/evidence/code/${slug}/`); + continue; + } + + // 尝试从缓存目录查找已有 clone + const cacheBase = path.join(process.env['HOME'] ?? '', '.teamai', 'cache', 'repos'); + const urlParts = String(source).replace(/^https?:\/\//, '').replace(/@.*$/, '').split('/'); + const cachePath = path.join(cacheBase, ...urlParts.slice(0, 3)); + + if (await pathExists(cachePath)) { + await extractCodebase({ path: cachePath, project: slug }); + result.migrated.push(slug); + } else { + result.skipped.push(`${slug}: 缓存不存在 (${cachePath}), 请先执行 teamai import --from-repo`); + } + } catch (err) { + result.errors.push(`${slug}: ${err instanceof Error ? err.message : String(err)}`); + } + } + + if (opts.json) { + console.log(JSON.stringify({ status: 'done', ...result }, null, 2)); + } else { + if (result.migrated.length > 0) { + log.success(`已迁移 ${result.migrated.length} 个仓库到 teamwiki/ 格式`); + for (const m of result.migrated) { + console.log(chalk.green(` ✓ ${m}`)); + } + } + if (result.skipped.length > 0) { + console.log(chalk.yellow(`跳过 ${result.skipped.length} 个:`)); + for (const s of result.skipped) { + console.log(chalk.yellow(` - ${s}`)); + } + } + if (result.errors.length > 0) { + console.log(chalk.red(`失败 ${result.errors.length} 个:`)); + for (const e of result.errors) { + console.log(chalk.red(` ✗ ${e}`)); + } + } + + if (!opts.dryRun && result.migrated.length > 0) { + log.info(''); + log.info('迁移完成。旧的 docs/team-codebase/ 目录已保留(未删除)。'); + log.info('确认新图谱工作正常后,可手动删除 docs/team-codebase/ 目录。'); + } + } +} diff --git a/src/codebase-wiki-lint.ts b/src/codebase-wiki-lint.ts new file mode 100644 index 0000000..01ff3a8 --- /dev/null +++ b/src/codebase-wiki-lint.ts @@ -0,0 +1,250 @@ +import { readFile, readdir, stat } from 'node:fs/promises'; +import path from 'node:path'; + +import chalk from 'chalk'; + +import { pathExists } from './utils/fs.js'; +import type { CodeGraphIndex } from './wiki-engine/adapters/index.js'; + +export type WikiLintSeverity = 'high' | 'medium' | 'low' | 'info'; + +export interface WikiLintIssue { + severity: WikiLintSeverity; + category: string; + location: string; + message: string; +} + +export interface WikiLintReport { + issues: WikiLintIssue[]; + summary: { + total: number; + high: number; + medium: number; + low: number; + info: number; + }; + graphHealth: { + nodeCount: number; + edgeCount: number; + orphanNodes: number; + connectivity: number; + }; +} + +export async function lintTeamwiki(opts: { + cwd: string; + severity?: WikiLintSeverity; +}): Promise { + const wikiRoot = path.join(opts.cwd, 'teamwiki'); + const issues: WikiLintIssue[] = []; + const minSeverity = opts.severity ?? 'info'; + const severityOrder: WikiLintSeverity[] = ['info', 'low', 'medium', 'high']; + const minIdx = severityOrder.indexOf(minSeverity); + + function addIssue(issue: WikiLintIssue): void { + if (severityOrder.indexOf(issue.severity) >= minIdx) { + issues.push(issue); + } + } + + // Check graph-index.json exists + const graphPath = path.join(wikiRoot, '.indices', 'graph-index.json'); + let graph: CodeGraphIndex | null = null; + + if (!await pathExists(graphPath)) { + addIssue({ + severity: 'high', + category: 'graph-missing', + location: 'teamwiki/.indices/graph-index.json', + message: 'graph-index.json 不存在,知识图谱未构建', + }); + } else { + try { + const raw = await readFile(graphPath, 'utf-8'); + graph = JSON.parse(raw) as CodeGraphIndex; + } catch { + addIssue({ + severity: 'high', + category: 'graph-corrupt', + location: graphPath, + message: 'graph-index.json 解析失败', + }); + } + } + + // Check evidence directory + const evidenceDir = path.join(wikiRoot, 'evidence', 'code'); + if (!await pathExists(evidenceDir)) { + addIssue({ + severity: 'high', + category: 'evidence-missing', + location: 'teamwiki/evidence/code/', + message: 'evidence 目录不存在,无代码事实页', + }); + } else { + const projects = await readdir(evidenceDir); + if (projects.length === 0) { + addIssue({ + severity: 'medium', + category: 'evidence-empty', + location: 'teamwiki/evidence/code/', + message: 'evidence 目录为空,未提取任何项目', + }); + } + + for (const project of projects) { + const projectDir = path.join(evidenceDir, project); + const pStat = await stat(projectDir).catch(() => null); + if (!pStat?.isDirectory()) { + if (!pStat) { + addIssue({ severity: 'low', category: 'stat-failed', location: `evidence/code/${project}`, message: '无法读取目录状态' }); + } + continue; + } + + const files = await readdir(projectDir); + if (!files.includes('index.md')) { + addIssue({ + severity: 'low', + category: 'missing-index', + location: `evidence/code/${project}/`, + message: '缺少 index.md 总索引页', + }); + } + } + } + + // Check navigation files (router.md, index.md, hot.md) + for (const navFile of ['router.md', 'index.md', 'hot.md']) { + if (!await pathExists(path.join(wikiRoot, navFile))) { + addIssue({ + severity: 'low', + category: 'nav-missing', + location: `teamwiki/${navFile}`, + message: `导航文件 ${navFile} 不存在,知识库入口不完整`, + }); + } + } + + // Check source-manifest.json + const manifestPath = path.join(wikiRoot, 'source-manifest.json'); + if (!await pathExists(manifestPath)) { + addIssue({ + severity: 'low', + category: 'manifest-missing', + location: 'teamwiki/source-manifest.json', + message: 'source-manifest.json 不存在,增量更新不可用', + }); + } else { + try { + const raw = await readFile(manifestPath, 'utf-8'); + const manifest = JSON.parse(raw); + if (manifest.lastScan) { + const daysSince = (Date.now() - new Date(manifest.lastScan).getTime()) / (1000 * 60 * 60 * 24); + if (daysSince > 60) { + addIssue({ + severity: 'medium', + category: 'stale-manifest', + location: 'teamwiki/source-manifest.json', + message: `上次扫描距今 ${Math.floor(daysSince)} 天,建议重新执行 --extract`, + }); + } + } + } catch { + addIssue({ + severity: 'low', + category: 'manifest-corrupt', + location: manifestPath, + message: 'source-manifest.json 解析失败', + }); + } + } + + // Graph health metrics + let graphHealth = { nodeCount: 0, edgeCount: 0, orphanNodes: 0, connectivity: 0 }; + if (graph) { + const nodeSlugs = new Set(graph.nodes.map(n => n.slug)); + const connectedNodes = new Set(); + for (const edge of graph.edges) { + connectedNodes.add(edge.from); + connectedNodes.add(edge.to); + } + const orphans = graph.nodes.filter(n => !connectedNodes.has(n.slug)); + const connectivity = graph.nodes.length > 0 + ? (graph.nodes.length - orphans.length) / graph.nodes.length + : 0; + + graphHealth = { + nodeCount: graph.nodes.length, + edgeCount: graph.edges.length, + orphanNodes: orphans.length, + connectivity: Math.round(connectivity * 100) / 100, + }; + + if (connectivity < 0.3) { + addIssue({ + severity: 'medium', + category: 'low-connectivity', + location: 'teamwiki/.indices/graph-index.json', + message: `图谱连通性 ${(connectivity * 100).toFixed(0)}% 过低(${orphans.length} 个孤立节点)`, + }); + } + + if (graph.edges.length === 0 && graph.nodes.length > 10) { + addIssue({ + severity: 'high', + category: 'no-edges', + location: 'teamwiki/.indices/graph-index.json', + message: `图谱有 ${graph.nodes.length} 个节点但 0 条边,图谱构建可能失败`, + }); + } + } + + const summary = { + total: issues.length, + high: issues.filter(i => i.severity === 'high').length, + medium: issues.filter(i => i.severity === 'medium').length, + low: issues.filter(i => i.severity === 'low').length, + info: issues.filter(i => i.severity === 'info').length, + }; + + return { issues, summary, graphHealth }; +} + +export function formatWikiLintReport(report: WikiLintReport): string { + const lines: string[] = []; + + lines.push(chalk.bold('=== teamwiki/ 知识图谱健康度检查 ===')); + lines.push(''); + lines.push(`图谱: ${report.graphHealth.nodeCount} nodes, ${report.graphHealth.edgeCount} edges, 连通性 ${(report.graphHealth.connectivity * 100).toFixed(0)}%`); + if (report.graphHealth.orphanNodes > 0) { + lines.push(chalk.dim(` (${report.graphHealth.orphanNodes} 个孤立节点)`)); + } + lines.push(''); + + if (report.issues.length === 0) { + lines.push(chalk.green('✓ 无问题')); + return lines.join('\n'); + } + + const byCategory = new Map(); + for (const issue of report.issues) { + const existing = byCategory.get(issue.category) ?? []; + existing.push(issue); + byCategory.set(issue.category, existing); + } + + for (const [category, categoryIssues] of byCategory) { + lines.push(chalk.bold(`[${category}] (${categoryIssues.length})`)); + for (const issue of categoryIssues) { + const sevColor = issue.severity === 'high' ? chalk.red + : issue.severity === 'medium' ? chalk.yellow : chalk.dim; + lines.push(` ${sevColor(`[${issue.severity}]`)} ${issue.location}: ${issue.message}`); + } + lines.push(''); + } + + lines.push(`总计: ${report.summary.high} high, ${report.summary.medium} medium, ${report.summary.low} low, ${report.summary.info} info`); + return lines.join('\n'); +} diff --git a/src/contribute-check.ts b/src/contribute-check.ts index 20b1fb2..665eb52 100644 --- a/src/contribute-check.ts +++ b/src/contribute-check.ts @@ -201,35 +201,37 @@ export function computeSmartScore(events: DashboardEvent[]): number { let score = 0; - // Tool count — gradient (max 20 points) - // 30+ calls → 10, scales linearly up to 80+ → 20 - if (totalToolCalls >= 30) { - score += Math.min(20, Math.round(((totalToolCalls - 30) / 50) * 10) + 10); + // Tool count — gradient (max 25 points) + // 20+ calls → 5, scales linearly up to 80+ → 25 + if (totalToolCalls >= 20) { + score += Math.min(25, Math.round(((totalToolCalls - 20) / 60) * 20) + 5); } - // Tool diversity (max 30 points) + // Tool diversity (max 20 points) if (totalToolCalls > 0) { - const diversity = toolNames.size / Math.min(totalToolCalls, 20); // Cap denominator at 20 - score += Math.min(Math.round(diversity * 30), 30); + const diversity = toolNames.size / Math.min(totalToolCalls, 10); + score += Math.min(Math.round(diversity * 20), 20); } - // Skill usage (15 points) + // Skill usage (10 points) if (hasSkills) { - score += 15; + score += 10; } - // Error indicators (15 points) + // Error indicators (10 points) if (hasErrors) { - score += 15; + score += 10; } - // Session duration (20 points if > 30 min) + // Session duration (max 20 points) if (events.length >= 2) { const first = new Date(events[0].timestamp).getTime(); const last = new Date(events[events.length - 1].timestamp).getTime(); const durationMin = (last - first) / (1000 * 60); if (durationMin > 30) { score += 20; + } else if (durationMin > 15) { + score += 10; } } diff --git a/src/deep-enrich.ts b/src/deep-enrich.ts new file mode 100644 index 0000000..66ae7b3 --- /dev/null +++ b/src/deep-enrich.ts @@ -0,0 +1,479 @@ +import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises'; +import path from 'node:path'; +import { pathExists } from './utils/fs.js'; +import { callClaude, callClaudeParallel } from './utils/ai-client.js'; +import { log } from './utils/logger.js'; +import { assertSafeResourceName } from './utils/path-safety.js'; + +export interface DeepEnrichOptions { + project: string; + evidenceDir: string; // teamwiki/evidence/code// + wikiRoot: string; // teamwiki/ + cacheDir?: string; // 源码 clone 目录(可选,用于读取实际源码) + maxModules?: number; // 限制 AI 处理的最大组件数(费用控制) +} + +interface ProgressState { + project: string; + phase: 'pending' | 'components' | 'architecture' | 'graph' | 'done'; + componentsDone: string[]; + componentsPending: string[]; + startedAt: string; + updatedAt: string; +} + +interface ManifestComponent { + slug: string; + title?: string; + responsibilities?: string[]; + category?: string; +} + +interface ManifestEdge { + from: string; + to: string; + relation?: string; +} + +interface Manifest { + project?: string; + components?: ManifestComponent[]; + edges?: ManifestEdge[]; +} + +// ─── 上下文加载 ───────────────────────────────────────────── + +interface EnrichContext { + manifest: Manifest; + indexMd: string; + callChains: string; + overview: string; + moduleDocs: Map; +} + +async function readFileSafe(filePath: string): Promise { + try { + return await readFile(filePath, 'utf-8'); + } catch { + return ''; + } +} + +async function loadContext(evidenceDir: string): Promise { + const manifestRaw = await readFileSafe(path.join(evidenceDir, '_manifest.json')); + let manifest: Manifest = {}; + try { + manifest = JSON.parse(manifestRaw) as Manifest; + } catch { + log.debug('deep-enrich: failed to parse _manifest.json'); + } + + const [indexMd, callChains, overview] = await Promise.all([ + readFileSafe(path.join(evidenceDir, 'index.md')), + readFileSafe(path.join(evidenceDir, 'call-chains.md')), + readFileSafe(path.join(evidenceDir, 'overview.md')), + ]); + + const modulesDir = path.join(evidenceDir, 'modules'); + const moduleDocs = new Map(); + if (await pathExists(modulesDir)) { + try { + const entries = await readdir(modulesDir); + await Promise.all( + entries + .filter(e => e.endsWith('.md')) + .map(async (e) => { + const content = await readFileSafe(path.join(modulesDir, e)); + moduleDocs.set(e.replace(/\.md$/, ''), content); + }), + ); + } catch { + log.debug('deep-enrich: failed to read modules dir'); + } + } + + return { manifest, indexMd, callChains, overview, moduleDocs }; +} + +// ─── Progress 管理 ───────────────────────────────────────── + +const PROGRESS_PATH_SUBDIR = '_review'; +const PROGRESS_FILENAME = 'progress.json'; + +function progressPath(evidenceDir: string): string { + return path.join(evidenceDir, PROGRESS_PATH_SUBDIR, PROGRESS_FILENAME); +} + +const VALID_PHASES = new Set(['pending', 'components', 'architecture', 'graph', 'done']); + +function isValidProgressState(v: unknown, project: string): v is ProgressState { + if (typeof v !== 'object' || v === null) return false; + const s = v as Record; + return ( + s['project'] === project && + typeof s['phase'] === 'string' && + VALID_PHASES.has(s['phase']) && + Array.isArray(s['componentsDone']) && + Array.isArray(s['componentsPending']) + ); +} + +async function loadProgress(evidenceDir: string, project: string, allComponents: string[]): Promise { + const p = progressPath(evidenceDir); + try { + const raw = await readFile(p, 'utf-8'); + const parsed: unknown = JSON.parse(raw); + if (isValidProgressState(parsed, project)) return parsed; + } catch { + // 不存在或解析失败,创建新的 + } + return { + project, + phase: 'pending', + componentsDone: [], + componentsPending: [...allComponents], + startedAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; +} + +async function saveProgress(evidenceDir: string, state: ProgressState): Promise { + const p = progressPath(evidenceDir); + await mkdir(path.dirname(p), { recursive: true }); + const updated: ProgressState = { ...state, updatedAt: new Date().toISOString() }; + await writeFile(p, JSON.stringify(updated, null, 2), 'utf-8'); +} + +// ─── Prompt 构建 ──────────────────────────────────────────── + +function buildComponentPrompt( + project: string, + component: ManifestComponent, + moduleFacts: string, + relevantCallChains: string, + deps: string, +): string { + const moduleName = component.slug; + const responsibilities = component.responsibilities ?? []; + return ` +项目: ${project} +模块: ${moduleName} +职责: ${responsibilities.join('; ')} + +核心组件(来自代码提取): +${moduleFacts} + +调用链: +${relevantCallChains} + +模块依赖: +${deps} + + +为上述代码模块生成一份组件设计文档。必须包含以下章节: + +## 🤖 AI 快速理解要点 +(表格:核心职责/架构层级/上游组件/下游组件/代码入口/核心机制/数据流向/技术栈,每项不超过20字) + +## 架构设计 +(ASCII 架构图 + 核心子模块说明) + +## 接口设计 +(对外接口表:接口名/方法/路径/说明) + +## 核心流程 +(主要请求处理流程,步骤式描述) + +直接输出 Markdown,不要任何前言或解释。`; +} + +function buildArchitecturePrompt( + project: string, + moduleList: string, + edges: string, + interfaceSummary: string, +): string { + return ` +项目: ${project} +模块清单: +${moduleList} + +模块间依赖: +${edges} + +接口统计: +${interfaceSummary} + + +为上述项目生成一份技术架构总览文档。必须包含: + +## 项目概述 +(一段话描述项目的核心定位和能力) + +## 架构图 +(ASCII 分层架构图,标注各模块和调用方向) + +## 组件关系矩阵 +(表格:组件A→组件B + 关系类型 + 通信方式) + +## 核心链路 +(2-3条最重要的请求处理链路,从入口到存储的完整路径) + +## 技术栈 +(表格:维度/技术/说明) + +直接输出 Markdown,不要任何前言或解释。`; +} + +// ─── 确定性图谱生成(无需 AI)───────────────────────────── + +function buildG1RelationsDoc(manifest: Manifest): string { + const edges = manifest.edges ?? []; + if (edges.length === 0) { + return '# 组件关系矩阵\n\n(暂无依赖边数据)\n'; + } + const rows = edges.map(e => `| ${e.from} | ${e.to} | ${e.relation ?? 'DEPENDS_ON'} |`).join('\n'); + return `# 组件关系矩阵\n\n| 来源组件 | 目标组件 | 关系类型 |\n|----------|----------|----------|\n${rows}\n`; +} + +function buildG2DataflowDoc(callChains: string): string { + if (!callChains.trim()) { + return '# 数据流图\n\n(暂无调用链数据)\n'; + } + // 提取 entry→data 路径行(以 → 或 -> 连接的行) + const lines = callChains.split('\n').filter(l => /→|->/.test(l)); + if (lines.length === 0) { + return `# 数据流图\n\n\`\`\`\n${callChains.slice(0, 2000)}\n\`\`\`\n`; + } + const flowRows = lines.slice(0, 30).map(l => `| ${l.trim()} |`).join('\n'); + return `# 数据流图\n\n| 调用链路径 |\n|------------|\n${flowRows}\n`; +} + +function buildG3InterfacesDoc(interfacesMd: string): string { + if (!interfacesMd.trim()) { + return '# 接口映射表\n\n(暂无接口数据)\n'; + } + return `# 接口映射表\n\n${interfacesMd}\n`; +} + +// ─── Phase 1: 组件设计文档 ───────────────────────────────── + +function extractModuleFacts(moduleDocs: Map, slug: string): string { + return moduleDocs.get(slug) ?? ''; +} + +function extractRelevantCallChains(callChains: string, slug: string): string { + const lines = callChains.split('\n'); + const relevant = lines.filter(l => l.includes(slug)); + return relevant.slice(0, 20).join('\n') || callChains.slice(0, 500); +} + +function extractDeps(manifest: Manifest, slug: string): string { + const edges = manifest.edges ?? []; + const deps = edges.filter(e => e.from === slug).map(e => e.to); + const rdeps = edges.filter(e => e.to === slug).map(e => e.from); + const parts: string[] = []; + if (deps.length > 0) parts.push(`依赖: ${deps.join(', ')}`); + if (rdeps.length > 0) parts.push(`被依赖: ${rdeps.join(', ')}`); + return parts.join('\n') || '无'; +} + +async function runPhaseComponents( + opts: DeepEnrichOptions, + ctx: EnrichContext, + progress: ProgressState, + docsDir: string, +): Promise { + const { project, evidenceDir } = opts; + const components = ctx.manifest.components ?? []; + const pending = components.filter(c => !progress.componentsDone.includes(c.slug)); + + if (pending.length === 0) { + log.info(`deep-enrich[${project}]: 组件文档全部已完成,跳过 Phase 1`); + return; + } + + log.info(`deep-enrich[${project}]: Phase 1 — 生成 ${pending.length} 个组件设计文档`); + + // 每批 2 个并发 + const BATCH = 5; + for (let i = 0; i < pending.length; i += BATCH) { + const batch = pending.slice(i, i + BATCH); + const tasks = batch.map((comp) => { + const moduleFacts = extractModuleFacts(ctx.moduleDocs, comp.slug); + const relevantCallChains = extractRelevantCallChains(ctx.callChains, comp.slug); + const deps = extractDeps(ctx.manifest, comp.slug); + const prompt = buildComponentPrompt(project, comp, moduleFacts, relevantCallChains, deps); + return { + prompt, + parse: (output: string) => output, + }; + }); + + let results: string[]; + try { + results = await callClaudeParallel(tasks, BATCH); + } catch (err) { + // AggregateError — 部分可能成功,graceful fallback + log.warn(`deep-enrich[${project}]: batch[${i}] 部分失败,逐个 fallback`); + results = await Promise.all( + batch.map(async (_, idx) => { + try { + return await callClaude(tasks[idx].prompt); + } catch (e) { + log.warn(`deep-enrich[${project}]: 跳过组件 ${batch[idx].slug}: ${(e as Error).message}`); + return null as unknown as string; + } + }), + ); + } + + for (let j = 0; j < batch.length; j++) { + const comp = batch[j]; + const content = results[j]; + if (!content) continue; + try { + assertSafeResourceName(comp.slug); + } catch (e) { + log.warn(`deep-enrich[${project}]: 跳过不安全的组件 slug "${comp.slug}": ${(e as Error).message}`); + continue; + } + const outPath = path.join(docsDir, `${comp.slug}.md`); + await mkdir(docsDir, { recursive: true }); + await writeFile(outPath, content, 'utf-8'); + progress.componentsDone.push(comp.slug); + await saveProgress(evidenceDir, progress); + log.debug(`deep-enrich[${project}]: 组件文档写入 ${outPath}`); + } + } +} + +// ─── Phase 2: 架构总览文档 ───────────────────────────────── + +async function runPhaseArchitecture( + opts: DeepEnrichOptions, + ctx: EnrichContext, + docsDir: string, +): Promise { + const { project } = opts; + const components = ctx.manifest.components ?? []; + const moduleList = components + .map(c => `- ${c.slug}: ${(c.responsibilities ?? []).join('; ')}`) + .join('\n'); + const edges = (ctx.manifest.edges ?? []) + .map(e => `${e.from} → ${e.to} (${e.relation ?? 'DEPENDS_ON'})`) + .join('\n'); + const interfaceSummary = ctx.indexMd.slice(0, 800); + + const prompt = buildArchitecturePrompt(project, moduleList, edges, interfaceSummary); + log.info(`deep-enrich[${project}]: Phase 2 — 生成架构总览文档`); + + let content: string; + try { + content = await callClaude(prompt); + } catch (e) { + log.warn(`deep-enrich[${project}]: 架构总览生成失败,跳过: ${(e as Error).message}`); + return; + } + + if (!content.trim()) { + log.warn(`deep-enrich[${project}]: 架构总览 AI 返回空内容,跳过写文件`); + return; + } + + const outPath = path.join(docsDir, 'architecture.md'); + await mkdir(docsDir, { recursive: true }); + await writeFile(outPath, content, 'utf-8'); + log.debug(`deep-enrich[${project}]: 架构总览写入 ${outPath}`); +} + +// ─── Phase 3: 确定性图谱文档 ─────────────────────────────── + +async function runPhaseGraph( + opts: DeepEnrichOptions, + ctx: EnrichContext, + docsDir: string, +): Promise { + const { project, evidenceDir } = opts; + log.info(`deep-enrich[${project}]: Phase 3 — 生成确定性图谱文档`); + + const interfacesMd = await readFileSafe(path.join(evidenceDir, 'interfaces.md')); + + const g1 = buildG1RelationsDoc(ctx.manifest); + const g2 = buildG2DataflowDoc(ctx.callChains); + const g3 = buildG3InterfacesDoc(interfacesMd); + + await mkdir(docsDir, { recursive: true }); + await Promise.all([ + writeFile(path.join(docsDir, 'graph-g1-relations.md'), g1, 'utf-8'), + writeFile(path.join(docsDir, 'graph-g2-dataflow.md'), g2, 'utf-8'), + writeFile(path.join(docsDir, 'graph-g3-interfaces.md'), g3, 'utf-8'), + ]); + log.debug(`deep-enrich[${project}]: 图谱文档写入 ${docsDir}`); +} + +// ─── 主函数 ───────────────────────────────────────────────── + +/** + * 对已导入仓库执行深度 AI 知识生成。 + * + * 读取 evidenceDir 中已有的确定性提取结果,并发调用 AI 生成: + * - Phase 1: 每个组件的设计文档(concurrency=2) + * - Phase 2: 整体架构总览文档(单次调用) + * - Phase 3: 确定性图谱文档(无需 AI,直接渲染) + * + * 支持断点续传:通过 _review/progress.json 记录已完成组件。 + * + * @param opts DeepEnrichOptions + */ +export async function deepEnrich(opts: DeepEnrichOptions): Promise { + const { project, evidenceDir } = opts; + const docsDir = path.join(evidenceDir, 'docs'); + + log.info(`deep-enrich[${project}]: 开始深度知识生成,evidenceDir=${evidenceDir}`); + + // 1. 加载上下文 + const ctx = await loadContext(evidenceDir); + let components = ctx.manifest.components ?? []; + + if (components.length === 0) { + log.warn(`deep-enrich[${project}]: _manifest.json 中无组件,终止`); + return; + } + + if (opts.maxModules && components.length > opts.maxModules) { + log.info(`deep-enrich[${project}]: 限制为前 ${opts.maxModules} 个组件(共 ${components.length} 个)`); + components = components.slice(0, opts.maxModules); + ctx.manifest = { ...ctx.manifest, components }; + } + + // 2. 初始化 progress(断点续传) + const allSlugs = components.map(c => c.slug); + const progress = await loadProgress(evidenceDir, project, allSlugs); + + // 3. Phase 1: 组件设计文档 + if (progress.phase === 'pending' || progress.phase === 'components') { + progress.phase = 'components'; + await saveProgress(evidenceDir, progress); + await runPhaseComponents(opts, ctx, progress, docsDir); + } + + // 4. Phase 2: 架构总览 + if (progress.phase === 'components' || progress.phase === 'architecture') { + progress.phase = 'architecture'; + await saveProgress(evidenceDir, progress); + await runPhaseArchitecture(opts, ctx, docsDir); + } + + // 5. Phase 3: 图谱文档 + if (progress.phase === 'architecture' || progress.phase === 'graph') { + progress.phase = 'graph'; + await saveProgress(evidenceDir, progress); + await runPhaseGraph(opts, ctx, docsDir); + } + + // 6. 完成 + progress.phase = 'done'; + await saveProgress(evidenceDir, progress); + log.success(`deep-enrich[${project}]: 深度知识生成完成`); +} diff --git a/src/hook-handlers.ts b/src/hook-handlers.ts index 7b49743..ffb3c12 100644 --- a/src/hook-handlers.ts +++ b/src/hook-handlers.ts @@ -147,18 +147,17 @@ const trackSlashHandler: HookHandler = { const contributeCheckHandler: HookHandler = { name: 'contribute-check', - async execute(stdin, _tool) { + async execute(stdin, tool) { const { contributeCheckForSession } = await import('./contribute-check.js'); + const { formatStopHookOutput } = await import('./utils/hook-output.js'); - // Derive session ID from STDIN const sessionId = typeof stdin.session_id === 'string' ? stdin.session_id : null; if (!sessionId) return null; const cwd = typeof stdin.cwd === 'string' ? stdin.cwd : undefined; const { hint } = await contributeCheckForSession(sessionId, cwd); if (hint) { - // Stop event format: { stopReason: "..." } - return JSON.stringify({ stopReason: hint }); + return formatStopHookOutput(hint, tool); } return null; }, diff --git a/src/import-iwiki.ts b/src/import-iwiki.ts index 4275100..9b22b46 100644 --- a/src/import-iwiki.ts +++ b/src/import-iwiki.ts @@ -5,10 +5,14 @@ * 分类、审查、推送均复用 import-local.ts 的现有函数。 */ +import path from 'node:path'; +import { readFile, mkdir, writeFile } from 'node:fs/promises'; + import { classifyWithAI, interactiveReview, pushAccepted } from './import-local.js'; import { IWikiClient } from './utils/iwiki-client.js'; import type { IWikiDocument, IWikiPage } from './utils/iwiki-client.js'; import { log, spinner } from './utils/logger.js'; +import { pathExists } from './utils/fs.js'; // ─── 内部辅助函数 ────────────────────────────────────────────── @@ -193,5 +197,167 @@ export async function importFromIWiki(opts: { outputDir: opts.outputDir, }); + // 10. 与 teamwiki 代码知识建立 MAPS_TO 关系(在 push 之前,确保结果被推送) + const teamwikiRoot = path.join(repoPath, 'teamwiki'); + if (await pathExists(path.join(teamwikiRoot, '.indices', 'graph-index.json'))) { + try { + const mapsToEdges = await reconcileIwikiWithCodebase(documents, teamwikiRoot); + if (mapsToEdges.length > 0) { + log.success(`建立 ${mapsToEdges.length} 条 iWiki↔代码 MAPS_TO 关系`); + } else { + log.info('[reconcile] 未发现 iWiki 文档与代码知识的匹配关系(文档内容可能与代码无关)'); + } + } catch (err) { + log.debug(`[reconcile] iWiki↔代码关系建立失败(非阻塞): ${err instanceof Error ? err.message : err}`); + } + } + + // 11. 自动推送所有产物到团队仓库 + if (!opts.dryRun) { + const { autoPushTeamRepo } = await import('./utils/git.js'); + await autoPushTeamRepo(repoPath, `[teamai] Import from iWiki: ${documents.map(d => d.title).slice(0, 3).join(', ')}`); + } + log.success('iWiki 导入完成'); } + +// ─── iWiki↔Codebase Reconciliation ──────────────────────────── + +interface MapsToEdge { + from: string; + to: string; + relation: 'MAPS_TO'; + term: string; + confidence: number; +} + +/** + * 将 iWiki 文档与 teamwiki 代码知识图谱进行对账,建立 MAPS_TO 关系。 + * + * 基于 team-wiki reconciler 的核心逻辑(by @lurkacai): + * - 从文档中提取关键术语(API path、类名、模块名) + * - 在代码事实页面中搜索匹配 + * - 匹配成功则建立 MAPS_TO 边 + */ +async function reconcileIwikiWithCodebase( + documents: IWikiDocument[], + teamwikiRoot: string, +): Promise { + const graphPath = path.join(teamwikiRoot, '.indices', 'graph-index.json'); + const graphRaw = await readFile(graphPath, 'utf-8'); + const graph = JSON.parse(graphRaw); + + // 收集代码节点的标签用于匹配 + const codeLabels = new Map(); + for (const node of graph.nodes) { + codeLabels.set(node.label.toLowerCase(), node.id); + // 也索引 PascalCase 拆分后的单词 + const words = node.label.replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase(); + codeLabels.set(words, node.id); + } + + // 加载代码事实页面内容用于全文匹配 + const evidenceDir = path.join(teamwikiRoot, 'evidence', 'code'); + const codePageContents = new Map(); + if (await pathExists(evidenceDir)) { + const { readdir } = await import('node:fs/promises'); + const projects = await readdir(evidenceDir); + for (const project of projects) { + const projectDir = path.join(evidenceDir, project); + const files = await readdir(projectDir).catch(() => [] as string[]); + for (const file of files) { + if (!file.endsWith('.md')) continue; + const content = await readFile(path.join(projectDir, file), 'utf-8').catch(() => ''); + codePageContents.set(`evidence/code/${project}/${file}`, content); + } + } + } + + const mapsToEdges: MapsToEdge[] = []; + const edgeSet = new Set(); + + for (const doc of documents) { + const docSlug = `iwiki/p/${doc.docid}`; + const terms = extractKeyTermsFromDoc(doc.content); + + for (const term of terms) { + // 方式 1:术语直接匹配代码节点标签 + const directMatch = codeLabels.get(term.toLowerCase()); + if (directMatch) { + const key = `${docSlug}|${directMatch}`; + if (!edgeSet.has(key)) { + edgeSet.add(key); + mapsToEdges.push({ from: docSlug, to: directMatch, relation: 'MAPS_TO', term, confidence: 0.8 }); + } + continue; + } + + // 方式 2:术语在代码事实页面全文中出现 + for (const [pagePath, content] of codePageContents) { + if (content.toLowerCase().includes(term.toLowerCase()) && term.length > 3) { + const key = `${docSlug}|${pagePath}`; + if (!edgeSet.has(key)) { + edgeSet.add(key); + mapsToEdges.push({ from: docSlug, to: pagePath, relation: 'MAPS_TO', term, confidence: 0.6 }); + } + break; // 每个术语最多匹配一个 code page + } + } + } + } + + // 写入 graph-index.json(去重:按 from+to+relation 三元组) + if (mapsToEdges.length > 0) { + const existingKeys = new Set( + graph.edges.map((e: { from: string; to: string; relation: string }) => `${e.from}|${e.to}|${e.relation}`), + ); + for (const edge of mapsToEdges) { + const key = `${edge.from}|${edge.to}|${edge.relation}`; + if (!existingKeys.has(key)) { + existingKeys.add(key); + graph.edges.push(edge); + } + } + await writeFile(graphPath, JSON.stringify(graph, null, 2), 'utf-8'); + } + + return mapsToEdges; +} + +/** + * 从文档内容中提取关键术语,用于与代码知识匹配。 + * + * 提取规则: + * - API 路径:/api/v1/xxx 形式 + * - 代码标识符:PascalCase 或 camelCase 标识符 + * - 反引号包裹的代码片段 + */ +function extractKeyTermsFromDoc(content: string): string[] { + const terms = new Set(); + + // API 路径 + const apiPaths = content.match(/\/api\/[a-z0-9/_-]+/gi); + if (apiPaths) { + for (const p of apiPaths) terms.add(p); + } + + // 反引号内的代码标识符(任意格式:PascalCase、camelCase、snake_case) + const codeRefs = content.matchAll(/`([a-zA-Z_][a-zA-Z0-9_]{2,})`/g); + for (const m of codeRefs) { + if (m[1]) terms.add(m[1]); + } + + // PascalCase 标识符(独立出现) + const pascalMatches = content.matchAll(/(?:^|[\s(,])([A-Z][a-z]+(?:[A-Z][a-z]+)+)/gm); + for (const m of pascalMatches) { + if (m[1]) terms.add(m[1]); + } + + // snake_case 标识符(2+ 段,如 user_token、create_session) + const snakeMatches = content.matchAll(/\b([a-z][a-z0-9]+(?:_[a-z0-9]+){1,})\b/g); + for (const m of snakeMatches) { + if (m[1] && m[1].length > 4) terms.add(m[1]); + } + + return [...terms]; +} diff --git a/src/import-mr.ts b/src/import-mr.ts index c3c7011..ff5ae94 100644 --- a/src/import-mr.ts +++ b/src/import-mr.ts @@ -313,6 +313,10 @@ export async function importFromMR(opts: { } // ── 步骤 3:解析 learning 草稿 + dedup ───────────────── + // AI 可能用 markdown 代码块包裹输出,先剥离 + learningContent = learningContent + .replace(/^```(?:markdown|md|yaml)?\s*\n/m, '') + .replace(/\n```\s*$/, ''); // AI 可能在 frontmatter 前输出对话性废话,截取从第一个 `---` 开始的内容 const frontmatterStart = learningContent.indexOf('---'); if (frontmatterStart > 0) { diff --git a/src/import-repo.ts b/src/import-repo.ts index 42560c3..950bdd8 100644 --- a/src/import-repo.ts +++ b/src/import-repo.ts @@ -731,16 +731,28 @@ export async function importFromRepo(opts: ImportFromRepoOptions): Promise await fs.ensureDir(path.join(teamwikiRoot, '.indices')); if (await fs.pathExists(destGraph)) { const { mergeGraphs } = await import('./wiki-engine/adapters/index.js'); - const existing = JSON.parse(await fs.readFile(destGraph, 'utf8')); - const overlay = JSON.parse(await fs.readFile(srcGraph, 'utf8')); - const merged2 = mergeGraphs(existing, overlay); - // 跨仓关系检测:检查新仓库的 relation facts 是否引用了已有仓库的文件/包 - const crossRepoEdges = detectCrossRepoEdges(overlay, existing, slug); - if (crossRepoEdges.length > 0) { - (merged2 as { edges: Array<{ from: string; to: string; relation: string }> }).edges.push(...crossRepoEdges); - log.debug(`[wiki-engine] 检测到 ${crossRepoEdges.length} 条跨仓关系`); + let existing, overlay; + try { + existing = JSON.parse(await fs.readFile(destGraph, 'utf8')); + } catch (parseErr) { + log.warn(`[wiki-engine] graph-index.json 解析失败,将重建: ${(parseErr as Error).message}`); + existing = null; + } + try { + overlay = JSON.parse(await fs.readFile(srcGraph, 'utf8')); + } catch (parseErr) { + log.warn(`[wiki-engine] 源图谱解析失败,跳过合并: ${(parseErr as Error).message}`); + overlay = null; + } + if (overlay) { + const merged2 = existing ? mergeGraphs(existing, overlay) : overlay; + const crossRepoEdges = detectCrossRepoEdges(overlay, existing ?? { nodes: [], edges: [] }, slug); + if (crossRepoEdges.length > 0) { + (merged2 as { edges: Array<{ from: string; to: string; relation: string }> }).edges.push(...crossRepoEdges); + log.debug(`[wiki-engine] 检测到 ${crossRepoEdges.length} 条跨仓关系`); + } + await fs.writeFile(destGraph, JSON.stringify(merged2, null, 2), 'utf8'); } - await fs.writeFile(destGraph, JSON.stringify(merged2, null, 2), 'utf8'); } else { await fs.copy(srcGraph, destGraph); } @@ -808,6 +820,28 @@ export async function importFromRepo(opts: ImportFromRepoOptions): Promise log.info(chalk.green(`✓ 仓库 ${owner}/${repoName} 导入完成`)); + // 5b. 后台深度生成(不阻塞) + if (!dryRun && teamwikiRoot) { + const evidenceDir = path.join(teamwikiRoot, 'evidence', 'code', slug); + if (await fs.pathExists(path.join(evidenceDir, '_manifest.json'))) { + setImmediate(async () => { + try { + const { deepEnrich } = await import('./deep-enrich.js'); + await deepEnrich({ project: slug, evidenceDir, wikiRoot: teamwikiRoot, cacheDir }); + const { autoPushTeamRepo } = await import('./utils/git.js'); + const pushTarget = path.join(process.cwd(), '.teamai', 'team-repo'); + if (await fs.pathExists(pushTarget)) { + await autoPushTeamRepo(pushTarget, `[teamai] Deep enrich: ${slug}`); + } + log.info(chalk.green(`✓ 深度生成完成: ${slug}`)); + } catch (e) { + log.debug(`deep-enrich background failed for ${slug}: ${(e as Error).message}`); + } + }); + } + } + + // 6. 写 LAST_SYNC if (!dryRun) { await writeLastSync(cacheDir, cloneSha); diff --git a/src/import.ts b/src/import.ts index e137c17..9f746f6 100644 --- a/src/import.ts +++ b/src/import.ts @@ -13,6 +13,7 @@ import { importFromOrg } from './import-org.js'; import { importFromIWikiDual } from './iwiki-dual.js'; import { GlobalOptions } from './types.js'; import { log } from './utils/logger.js'; +import { autoPushTeamRepo } from './utils/git.js'; /** * import 命令的扩展选项,合并全局选项与子命令专属选项。 @@ -180,6 +181,9 @@ export async function importCmd(opts: ImportOptions): Promise { existingCodebaseMd, dryRun: opts.dryRun, }); + if (!opts.dryRun && !opts.output) { + await autoPushTeamRepo(localConfig.repo.localPath, `[teamai] Import from MR: ${opts.fromMr}`); + } } else if (opts.workspace) { // 分支 2:--workspace,从当前 git 工作区生成 codebase.md const repoPath = process.cwd(); @@ -248,12 +252,12 @@ export async function importCmd(opts: ImportOptions): Promise { }); log.success('导入完成'); if (pushed > 0 && !opts.dryRun && !opts.output) { - log.info('文件已写入本地团队仓库,运行 `teamai push` 推送到远程仓库'); + await autoPushTeamRepo(localConfig.repo.localPath, `[teamai] Import from local: ${opts.dir ?? 'claude-rules'}`); } } else { // 默认:未指定来源,提示用户 log.info('请指定导入来源:--dir 、--from-claude、--workspace、--from-mr 或 --from-iwiki '); - process.exit(0); + return; } } catch (err: unknown) { log.error((err as Error).message); diff --git a/src/index.ts b/src/index.ts index 95fc36a..e9932a9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -441,6 +441,7 @@ hooksCmd program .command('track [toolName] [toolInput]', { hidden: true }) + .description('Track a tool usage event (called by PostToolUse hook)') .option('--stdin', 'Read hook data from STDIN (Claude Code hook format)') .option('--tool ', 'Tool identifier for usage attribution (e.g. claude, claude-internal)') @@ -456,6 +457,7 @@ program program .command('track-slash', { hidden: true }) + .description('Track a slash command usage (called by UserPromptSubmit hook)') .option('--stdin', 'Read hook data from STDIN') .option('--tool ', 'Tool identifier for usage attribution (e.g. claude, claude-internal)') @@ -476,6 +478,7 @@ program program .command('save-session', { hidden: true }) + .description('Save current session tool usage summary') .option('--summary ', 'Session summary text') .action(async (cmdOpts) => { @@ -505,6 +508,7 @@ program program .command('dashboard-report', { hidden: true }) + .description('Report session state to dashboard (called by hooks)') .option('--stdin', 'Read hook data from STDIN') .option('--tool ', 'Tool identifier (e.g. claude, claude-internal)') @@ -519,6 +523,7 @@ program program .command('contribute-check', { hidden: true }) + .description('Check if session qualifies for contribution (called by PostToolUse hook)') .option('--stdin', 'Read hook data from STDIN') .option('--tool ', 'Tool identifier (e.g. claude, claude-internal)') @@ -547,15 +552,17 @@ program program .command('recall [query...]') .description('Search team learnings knowledge base') - .action(async (queryParts) => { + .option('--depth ', 'Recall depth: route (entry-points only) | context (module-level, default) | lookup (full graph traversal)', 'context') + .action(async (queryParts, cmdOpts) => { const globalOpts = program.opts() as GlobalOptions; const query = (queryParts as string[]).join(' '); const { recall } = await import('./recall.js'); - await recall(query, globalOpts); + await recall(query, { ...globalOpts, depth: cmdOpts.depth }); }); program .command('auto-recall', { hidden: true }) + .description('Auto-recall team knowledge on tool errors (called by PostToolUse hook)') .option('--stdin', 'Read hook data from STDIN') .action(async (cmdOpts) => { @@ -567,6 +574,7 @@ program program .command('todowrite-hint', { hidden: true }) + .description('Remind the agent to invoke teamai-recall when TodoWrite is used (PostToolUse hook)') .option('--stdin', 'Read hook data from STDIN') .option('--tool ', 'Source AI tool (claude / codebuddy / cursor)') @@ -581,31 +589,30 @@ program .command('import') .description('Import knowledge from local files, Claude/Cursor rules, git workspace, MRs, or iWiki') .option('--dir ', 'Scan local directory for importable Markdown files') - .addOption(new Option('--from-claude', 'Scan Claude/Cursor rule directories').hideHelp()) + .addOption(new Option('--from-claude', 'Scan Claude/Cursor rule directories (~/.claude/rules, ~/.cursor/rules)').hideHelp()) .addOption(new Option('--workspace', 'Generate codebase.md from current git workspace').hideHelp()) .option('--from-mr ', 'Extract learning and codebase suggestions from a merged MR/PR URL') .option('--from-iwiki ', 'Import documents from iWiki Space ID or page URL (requires TAI_PAT_TOKEN)') .addOption(new Option('--resume', 'Resume an interrupted import session').hideHelp()) .option('--all', 'Accept all suggestions without interactive confirmation') - .addOption(new Option('--output ', 'Write drafts to this directory').hideHelp()) - .addOption(new Option('--existing-codebase ', 'Path to existing codebase.md').hideHelp()) + .addOption(new Option('--output ', 'Write drafts to this directory instead of pushing to team repo').hideHelp()) + .addOption(new Option('--existing-codebase ', 'Path to existing codebase.md (used with --from-mr; overrides auto-detection from team repo)').hideHelp()) .option('--from-repo ', 'Clone a remote repo and generate per-repo codebase summary') - .option('--depth ', 'Shallow clone depth for --from-repo (default 1)', '1') - .addOption(new Option('--ssh', 'Force SSH clone').hideHelp()) - .addOption(new Option('--domain ', 'Skip AI recommendation').hideHelp()) + .addOption(new Option('--ssh', 'Force SSH clone even if HTTPS token is available').hideHelp()) + .addOption(new Option('--domain ', 'Skip AI recommendation and assign repo to this domain explicitly').hideHelp()) .option('--from-repo-list ', 'Batch import repos from a YAML whitelist') - .option('--concurrency ', 'Concurrent repos for --from-repo-list (default 3)', '3') - .option('--skip-aggregate', 'Skip domain-*.md / index.md regeneration') + .addOption(new Option('--concurrency ', 'Concurrent repos for --from-repo-list (default 3)').default('3').hideHelp()) + .addOption(new Option('--skip-aggregate', 'Skip domain-*.md / index.md regeneration').hideHelp()) .option('--incremental', 'Use cached clone with fetch+reset (with --from-repo or --from-repo-list)') .option('--from-org ', 'List repos under an org and bootstrap whitelist + domains') - .option('--bootstrap', 'Run interactive review after --from-org') - .option('--max-repos ', 'Cap on repos pulled from --from-org (default 200)', '200') - .option('--exclude-archived', 'Exclude archived repos from --from-org (default true)') - .option('--include-pattern ', 'Regex to include repos by full name (used with --from-org)') - .option('--exclude-pattern ', 'Regex to exclude repos by full name (used with --from-org)') - .addOption(new Option('--skip-import', 'Only write drafts').hideHelp()) - .addOption(new Option('--iwiki-dual', 'Enable dual-output mode').hideHelp()) - .addOption(new Option('--require-review', 'Defer writes to pending-review').hideHelp()) + .addOption(new Option('--bootstrap', 'Run interactive review after --from-org').hideHelp()) + .addOption(new Option('--max-repos ', 'Cap on repos pulled from --from-org (default 200)').default('200').hideHelp()) + .addOption(new Option('--exclude-archived', 'Exclude archived repos from --from-org (default true)').hideHelp()) + .addOption(new Option('--include-pattern ', 'Regex to include repos by full name (used with --from-org)').hideHelp()) + .addOption(new Option('--exclude-pattern ', 'Regex to exclude repos by full name (used with --from-org)').hideHelp()) + .addOption(new Option('--skip-import', 'Only write drafts; skip the actual --from-repo-list run').hideHelp()) + .addOption(new Option('--iwiki-dual', 'Enable dual-output mode for --from-iwiki (write codebase sections in addition to learning)').hideHelp()) + .addOption(new Option('--require-review', 'Defer codebase section writes to .teamai/pending-review.jsonl for human review').hideHelp()) .action(async (cmdOpts) => { const globalOpts = program.opts() as GlobalOptions; const { importCmd } = await import('./import.js'); @@ -614,6 +621,7 @@ program program .command('mr-hint', { hidden: true }) + .description('Hint AI about recently merged but un-imported MRs (SessionStart hook)') .option('--stdin', 'Read hook data from STDIN') .option('--tool ', 'Source AI tool (claude / codebuddy / cursor)') @@ -631,13 +639,14 @@ program .addOption(new Option('--incremental', 'Only re-extract changed files (requires prior manifest)')) .addOption(new Option('--project ', 'Project slug for extract output (default: directory name)')) .addOption(new Option('--max-files ', 'Max source files to scan (default: 200)')) + .addOption(new Option('--upgrade-wiki', 'Migrate docs/team-codebase/ to teamwiki/ graph format')) .option('--lint', 'Run global consistency lint over docs/team-codebase') .option('--fix', 'Apply low-risk mechanical fixes (only with --lint)') - .option('--severity ', 'Minimum severity to report: high|medium|low|info', 'info') - .option('--stale-days ', 'Threshold for sync-stale check', '60') - .option('--pending-review-threshold ', 'Threshold for pending-review backlog', '10') + .addOption(new Option('--severity ', 'Minimum severity to report: high|medium|low|info').default('info').hideHelp()) + .addOption(new Option('--stale-days ', 'Threshold for sync-stale check').default('60').hideHelp()) + .addOption(new Option('--pending-review-threshold ', 'Threshold for pending-review backlog').default('10').hideHelp()) .option('--json', 'Output report as JSON (suitable for CI)') - .option('--output ', 'Custom team-codebase root (mirrors --from-repo)') + .addOption(new Option('--output ', 'Custom team-codebase root (mirrors --from-repo)').hideHelp()) .action(async (cmdOpts) => { const globalOpts = program.opts() as GlobalOptions; const { codebaseCmd } = await import('./codebase-cmd.js'); @@ -676,6 +685,7 @@ program program .command('domains [repoUrl]', { hidden: true }) + .description('Inspect / accept / reject domain-drift signals (subcommand: drift)') .option('--apply', 'Apply drift for the given repoUrl') .option('--apply-all', 'Apply all drift items above confidence threshold') @@ -699,6 +709,7 @@ program program .command('hook-dispatch ', { hidden: true }) + .description('Unified hook dispatcher — handles all teamai hooks for a given event in one process') .option('--tool ', 'Tool identifier (e.g. claude, claude-internal, cursor)') .option('--matcher ', 'Hook matcher for PostToolUse (e.g. Skill, Bash)') @@ -730,4 +741,18 @@ ciCmd await ciExtractMr({ ...globalOpts, ...cmdOpts }); }); +program + .command('deep-enrich', { hidden: true }) + .description('Run deep AI knowledge generation for an imported repo') + .requiredOption('--project ', 'Project slug (directory name in evidence/code/)') + .option('--wiki-root ', 'Teamwiki root path') + .option('--max-modules ', 'Max modules to process (cost control)', parseInt) + .action(async (cmdOpts: { project: string; wikiRoot?: string; maxModules?: number }) => { + const p = await import('node:path'); + const wikiRoot = cmdOpts.wikiRoot ?? p.join(process.cwd(), '.teamai', 'team-repo', 'teamwiki'); + const evidenceDir = p.join(wikiRoot, 'evidence', 'code', cmdOpts.project); + const { deepEnrich } = await import('./deep-enrich.js'); + await deepEnrich({ project: cmdOpts.project, evidenceDir, wikiRoot, maxModules: cmdOpts.maxModules }); + }); + program.parse(); diff --git a/src/pull.ts b/src/pull.ts index 4763693..aed4677 100644 --- a/src/pull.ts +++ b/src/pull.ts @@ -557,6 +557,29 @@ async function pullForScope( } } + // Sync teamwiki/ directory (codebase knowledge graph) + const teamwikiRepoDir = path.join(localConfig.repo.localPath, 'teamwiki'); + if (await pathExists(teamwikiRepoDir)) { + const syncTarget = localConfig.projectRoot ?? process.cwd(); + const localTeamwikiDir = path.join(syncTarget, 'teamwiki'); + // 检查本地 graph-index 是否比远端更新(避免覆盖未推送的本地产物) + const localGraph = path.join(localTeamwikiDir, '.indices', 'graph-index.json'); + const remoteGraph = path.join(teamwikiRepoDir, '.indices', 'graph-index.json'); + let shouldSync = true; + if (await pathExists(localGraph) && await pathExists(remoteGraph)) { + const localStat = await fse.stat(localGraph); + const remoteStat = await fse.stat(remoteGraph); + if (localStat.mtimeMs > remoteStat.mtimeMs) { + log.warn(`[${scopeLabel}] 本地 teamwiki/ 比远端更新,跳过覆盖(请先 teamai push)`); + shouldSync = false; + } + } + if (shouldSync) { + await fse.copy(teamwikiRepoDir, localTeamwikiDir, { overwrite: true }); + log.debug(`[${scopeLabel}] Synced teamwiki/ knowledge graph`); + } + } + // Build the index when ANY of the four categories has content. const hasAnySource = effectiveLearningsDir || @@ -580,7 +603,7 @@ async function pullForScope( docsDir: await pathExists(docsRepoDir) ? docsRepoDir : undefined, rulesDir: await pathExists(rulesRepoDir) ? rulesRepoDir : undefined, skillsDir: await pathExists(skillsRepoDir) ? skillsRepoDir : undefined, - codebaseDir: effectiveCodebaseDir, + codebaseDir: undefined, // codebase now served by teamwiki/ graph engine votesDir: votesExist ? votesDir : undefined, indexPath, }); diff --git a/src/recall.ts b/src/recall.ts index 66e67e3..15e79e3 100644 --- a/src/recall.ts +++ b/src/recall.ts @@ -7,6 +7,8 @@ import { readFileSafe, writeFile, ensureDir, pathExists } from './utils/fs.js'; import { log } from './utils/logger.js'; import type { GlobalOptions, UserVotes, SearchIndex, LocalConfig } from './types.js'; import { getTeamaiHome } from './types.js'; +import { queryCodeKnowledge } from './code-knowledge-recall.js'; +import type { CodeKnowledgeResult } from './code-knowledge-recall.js'; /** Resolve votes dir dynamically (respects HOME changes in tests). */ function getVotesLocalDir(): string { @@ -221,7 +223,7 @@ async function loadOrBuildScopeIndex( */ export async function recall( query: string, - options: GlobalOptions, + options: GlobalOptions & { depth?: 'route' | 'context' | 'lookup' }, ): Promise { if (!query || !query.trim()) { log.error('Usage: teamai recall '); @@ -256,7 +258,8 @@ export async function recall( log.debug('recall: project scope not available'); } - if (scopeIndexes.length === 0) { + const hasWiki = await pathExists(path.join(process.cwd(), 'teamwiki')); + if (scopeIndexes.length === 0 && !hasWiki) { log.info('No learnings available. Run `teamai pull` first to sync team knowledge.'); return; } @@ -276,6 +279,36 @@ export async function recall( } } + // ── Codebase knowledge graph recall ────────────────────── + const wikiRoot = path.join(process.cwd(), 'teamwiki'); + try { + const codeResults = await queryCodeKnowledge(query, { wikiRoot, limit: 3, depth: options.depth }); + // B11: Normalize BM25 scores to 0-10 range before merging with learnings scores + const maxCodeScore = codeResults.length > 0 ? Math.max(...codeResults.map(r => r.score)) : 1; + const normalizer = maxCodeScore > 0 ? 10 / maxCodeScore : 1; + for (const cr of codeResults) { + allResults.push({ + entry: { + filename: cr.page, + title: cr.title, + author: '', + date: '', + tags: [], + tokens: [], + votes: 0, + type: 'docs' as const, + domain: 'technical' as const, + path: path.join(wikiRoot, cr.page), + }, + score: cr.score * normalizer, // B11: normalized to learnings score scale + scope: 'project', + learningsBase: wikiRoot, + }); + } + } catch { + log.warn('recall: 代码图谱检索不可用,可运行 teamai codebase --lint 诊断'); + } + // Re-sort merged results by score descending, then date descending allResults.sort((a, b) => { if (b.score !== a.score) return b.score - a.score; diff --git a/src/types.ts b/src/types.ts index b496515..a154513 100644 --- a/src/types.ts +++ b/src/types.ts @@ -411,7 +411,7 @@ export interface ContributeState { } /** Layer 1 (fast-path) threshold: if toolCount < this, skip reading events.jsonl */ -export const CONTRIBUTE_BASE_THRESHOLD = 20; +export const CONTRIBUTE_BASE_THRESHOLD = 15; /** Smart score threshold: minimum score to show contribute hint */ export const CONTRIBUTE_SMART_THRESHOLD = 35; @@ -428,8 +428,8 @@ export const CONTRIBUTE_LOW_QUALITY_BONUS = 10; /** Phase 2: threshold below which recall results are considered low quality */ export const CONTRIBUTE_LOW_QUALITY_THRESHOLD = 5.0; -/** Phase 2: score deduction when session has git commits and recall had hits */ -export const CONTRIBUTE_GIT_COMMIT_DOWNWEIGHT = 15; +/** Phase 2: git commit is neutral (no bonus, no penalty) */ +export const CONTRIBUTE_GIT_COMMIT_DOWNWEIGHT = 0; /** Directory for per-session contribute state files */ export const CONTRIBUTE_SESSIONS_DIR = `${TEAMAI_HOME}/sessions`; diff --git a/src/utils/ai-client.ts b/src/utils/ai-client.ts index 1c95eb8..3d48465 100644 --- a/src/utils/ai-client.ts +++ b/src/utils/ai-client.ts @@ -10,7 +10,7 @@ const ALLOWED_CLI_CANDIDATES = [ const CLI_DETECT_TIMEOUT_MS = 5_000; /** 默认 AI 调用超时时间(毫秒)。仓库初始化等大文档生成场景需要较长时间。 */ -const DEFAULT_TIMEOUT_MS = 720_000; +const DEFAULT_TIMEOUT_MS = 1200_000; /** 默认并发数量上限。 */ const DEFAULT_CONCURRENCY = 3; diff --git a/src/utils/iwiki-client.ts b/src/utils/iwiki-client.ts index 813989d..bdcda25 100644 --- a/src/utils/iwiki-client.ts +++ b/src/utils/iwiki-client.ts @@ -110,7 +110,7 @@ export class IWikiClient { headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.token}`, - 'Accept': 'application/json', + 'Accept': 'application/json, text/event-stream', 'Content-Length': Buffer.byteLength(payload), }, };