feat: Implement adaptive RRF weights and query intent detection

- Added integration tests for adaptive RRF weights in hybrid search.
- Enhanced query intent detection with new classifications: keyword, semantic, and mixed.
- Introduced symbol boosting in search results based on explicit symbol matches.
- Implemented embedding-based reranking with configurable options.
- Added global symbol index for efficient symbol lookups across projects.
- Improved file deletion handling on Windows to avoid permission errors.
- Updated chunk configuration to increase overlap for better context.
- Modified package.json test script to target specific test files.
- Created comprehensive writing style guidelines for documentation.
- Added TypeScript tests for query intent detection and adaptive weights.
- Established performance benchmarks for global symbol indexing.
This commit is contained in:
catlog22
2025-12-26 15:08:47 +08:00
parent ecd5085e51
commit 4061ae48c4
29 changed files with 2685 additions and 828 deletions

View File

@@ -5,6 +5,22 @@
> **模板参考**: [../templates/agent-base.md](../templates/agent-base.md)
> **规范参考**: [../specs/cpcc-requirements.md](../specs/cpcc-requirements.md)
## Agent 执行前置条件
**每个 Agent 必须首先读取以下规范文件**
```javascript
// Agent 启动时的第一步操作
const specs = {
cpcc: Read(`${skillRoot}/specs/cpcc-requirements.md`)
};
```
规范文件路径(相对于 skill 根目录):
- `specs/cpcc-requirements.md` - CPCC 软著申请规范要求
---
## Agent 配置
| Agent | 输出文件 | 章节 |
@@ -60,7 +76,13 @@ return { summaries, cross_notes: summaries.flatMap(s => s.cross_module_notes) };
```javascript
Task({
subagent_type: "cli-explore-agent",
run_in_background: false,
prompt: `
[SPEC]
首先读取规范文件:
- Read: ${skillRoot}/specs/cpcc-requirements.md
严格遵循 CPCC 软著申请规范要求。
[ROLE] 系统架构师,专注于分层设计和模块依赖。
[TASK]
@@ -113,7 +135,13 @@ graph TD
```javascript
Task({
subagent_type: "cli-explore-agent",
run_in_background: false,
prompt: `
[SPEC]
首先读取规范文件:
- Read: ${skillRoot}/specs/cpcc-requirements.md
严格遵循 CPCC 软著申请规范要求。
[ROLE] 功能分析师,专注于功能点识别和交互。
[TASK]
@@ -167,7 +195,13 @@ flowchart TD
```javascript
Task({
subagent_type: "cli-explore-agent",
run_in_background: false,
prompt: `
[SPEC]
首先读取规范文件:
- Read: ${skillRoot}/specs/cpcc-requirements.md
严格遵循 CPCC 软著申请规范要求。
[ROLE] 算法工程师,专注于核心逻辑和复杂度分析。
[TASK]
@@ -226,7 +260,13 @@ flowchart TD
```javascript
Task({
subagent_type: "cli-explore-agent",
run_in_background: false,
prompt: `
[SPEC]
首先读取规范文件:
- Read: ${skillRoot}/specs/cpcc-requirements.md
严格遵循 CPCC 软著申请规范要求。
[ROLE] 数据建模师,专注于实体关系和类型定义。
[TASK]
@@ -280,7 +320,13 @@ classDiagram
```javascript
Task({
subagent_type: "cli-explore-agent",
run_in_background: false,
prompt: `
[SPEC]
首先读取规范文件:
- Read: ${skillRoot}/specs/cpcc-requirements.md
严格遵循 CPCC 软著申请规范要求。
[ROLE] API设计师专注于接口契约和协议。
[TASK]
@@ -343,7 +389,13 @@ sequenceDiagram
```javascript
Task({
subagent_type: "cli-explore-agent",
run_in_background: false,
prompt: `
[SPEC]
首先读取规范文件:
- Read: ${skillRoot}/specs/cpcc-requirements.md
严格遵循 CPCC 软著申请规范要求。
[ROLE] 可靠性工程师,专注于异常处理和恢复策略。
[TASK]

View File

@@ -1,14 +1,23 @@
# Phase 2.5: Consolidation Agent
汇总所有分析 Agent 的产出,识别跨模块问题,生成汇总报告
汇总所有分析 Agent 的产出,生成设计综述,为 Phase 4 索引文档提供内容
> **规范参考**: [../specs/cpcc-requirements.md](../specs/cpcc-requirements.md)
## 核心职责
1. **设计综述**:生成 synthesis软件整体设计思路
2. **章节摘要**:生成 section_summaries导航表格内容
3. **跨模块分析**:识别问题和关联
4. **质量检查**:验证 CPCC 合规性
## 输入
```typescript
interface ConsolidationInput {
output_dir: string;
agent_summaries: AgentReturn[]; // 6个Agent的简要返回
cross_module_notes: string[]; // 所有跨模块备注
agent_summaries: AgentReturn[];
cross_module_notes: string[];
metadata: ProjectMetadata;
}
```
@@ -18,74 +27,94 @@ interface ConsolidationInput {
```javascript
Task({
subagent_type: "cli-explore-agent",
run_in_background: false,
prompt: `
## 规范前置
首先读取规范文件:
- Read: ${skillRoot}/specs/cpcc-requirements.md
严格遵循 CPCC 软著申请规范要求。
## 任务
作为汇总 Agent读取所有章节文件执行跨模块分析,生成汇总报告。
作为汇总 Agent读取所有章节文件生成设计综述和跨模块分析报告。
## 输入
- 章节文件: {output_dir}/sections/section-*.md
- 章节文件: ${outputDir}/sections/section-*.md
- Agent 摘要: ${JSON.stringify(agent_summaries)}
- 跨模块备注: ${JSON.stringify(cross_module_notes)}
- 软件信息: ${JSON.stringify(metadata)}
## 分析维度
## 核心产出
### 1. 一致性检查
- 术语一致性:同一概念是否使用相同名称
- 命名规范:中英文命名是否统一
- 图表编号:是否连续且正确
### 1. 设计综述 (synthesis)
用 2-3 段落描述软件整体设计思路:
- 第一段:软件定位与核心设计理念
- 第二段:模块划分与协作机制
- 第三段:技术选型与设计特点
### 2. 完整性检查
- 功能覆盖section-3的功能是否都有对应接口(section-6)
- 算法覆盖section-4的算法是否在功能中被引用
- 异常覆盖section-7的异常是否覆盖所有接口
### 2. 章节摘要 (section_summaries)
为每个章节提取一句话说明,用于导航表格:
### 3. 关联性检查
- 模块依赖:架构图(section-2)的依赖是否与实际一致
- 数据流向:数据结构(section-5)是否与接口(section-6)匹配
- 跨模块调用:是否存在未记录的跨模块依赖
| 章节 | 文件 | 一句话说明 |
|------|------|------------|
| 2. 系统架构设计 | section-2-architecture.md | ... |
| 3. 功能模块设计 | section-3-functions.md | ... |
| 4. 核心算法与流程 | section-4-algorithms.md | ... |
| 5. 数据结构设计 | section-5-data-structures.md | ... |
| 6. 接口设计 | section-6-interfaces.md | ... |
| 7. 异常处理设计 | section-7-exceptions.md | ... |
### 4. 质量检查
- 图表语法Mermaid 语法是否正确
- 内容深度:各章节是否有足够的详细说明
- 代码引用:是否包含具体的文件路径和行号
### 3. 跨模块分析
- 一致性:术语、命名规范
- 完整性:功能-接口对应、异常覆盖
- 关联性:模块依赖、数据流向
## 输出文件
写入: {output_dir}/cross-module-summary.md
写入: ${outputDir}/cross-module-summary.md
### 文件格式
\`\`\`markdown
# 跨模块分析报告
## 1. 文档统计
## 设计综述
| 章节 | 图表数 | 子章节数 | 字数 |
|------|--------|----------|------|
| 系统架构图 | 1 | 2 | ~500 |
| ... | ... | ... | ... |
[2-3 段落的软件设计思路描述]
## 2. 发现的问题
## 章节摘要
### 2.1 严重问题 (必须修复)
| 章节 | 文件 | 说明 |
|------|------|------|
| 2. 系统架构设计 | section-2-architecture.md | 一句话说明 |
| ... | ... | ... |
## 文档统计
| 章节 | 图表数 | 字数 |
|------|--------|------|
| ... | ... | ... |
## 发现的问题
### 严重问题 (必须修复)
| ID | 类型 | 位置 | 描述 | 建议 |
|----|------|------|------|------|
| E001 | 缺失 | section-6 | 缺少权限管理接口 | 补充 /api/permissions 相关接口 |
| E001 | ... | ... | ... | ... |
### 2.2 警告 (建议修复)
### 警告 (建议修复)
| ID | 类型 | 位置 | 描述 | 建议 |
|----|------|------|------|------|
| W001 | 一致性 | section-3/6 | 功能名"用户管理"与接口名"UserModule"不一致 | 统一使用中文或英文 |
| W001 | ... | ... | ... | ... |
### 2.3 提示 (可选修复)
### 提示 (可选修复)
| ID | 类型 | 位置 | 描述 |
|----|------|------|------|
| I001 | 增强 | section-4 | 建议为复杂算法添加复杂度分析 |
| I001 | ... | ... | ... |
## 3. 跨模块关联图
## 跨模块关联图
\`\`\`mermaid
graph LR
@@ -94,21 +123,11 @@ graph LR
S3 --> S6[接口]
S5[数据结构] --> S6
S6 --> S7[异常]
S3 -.->|缺少关联| S5
\`\`\`
## 4. 修复建议优先级
## 修复建议优先级
1. **E001**: 补充权限管理接口 (阻塞合规)
2. **W001**: 统一术语命名 (影响一致性)
3. ...
## 5. 下一步行动
- [ ] 修复 E001-E00x 严重问题
- [ ] 处理 W001-W00x 警告
- [ ] 可选处理 I001-I00x 提示
[按优先级排序的建议,段落式描述]
\`\`\`
## 返回格式 (JSON)
@@ -116,21 +135,28 @@ graph LR
{
"status": "completed",
"output_file": "cross-module-summary.md",
// Phase 4 索引文档所需
"synthesis": "2-3 段落的设计综述文本",
"section_summaries": [
{"file": "section-2-architecture.md", "title": "2. 系统架构设计", "summary": "一句话说明"},
{"file": "section-3-functions.md", "title": "3. 功能模块设计", "summary": "一句话说明"},
{"file": "section-4-algorithms.md", "title": "4. 核心算法与流程", "summary": "一句话说明"},
{"file": "section-5-data-structures.md", "title": "5. 数据结构设计", "summary": "一句话说明"},
{"file": "section-6-interfaces.md", "title": "6. 接口设计", "summary": "一句话说明"},
{"file": "section-7-exceptions.md", "title": "7. 异常处理设计", "summary": "一句话说明"}
],
// 质量信息
"stats": {
"total_sections": 6,
"total_diagrams": 8,
"total_words": 3500
},
"issues": {
"errors": [
{"id": "E001", "type": "missing", "section": "section-6", "desc": "缺少权限管理接口"}
],
"warnings": [
{"id": "W001", "type": "inconsistency", "sections": ["section-3", "section-6"], "desc": "术语不一致"}
],
"info": [
{"id": "I001", "type": "enhancement", "section": "section-4", "desc": "建议添加复杂度分析"}
]
"errors": [...],
"warnings": [...],
"info": [...]
},
"cross_refs": {
"found": 12,
@@ -151,34 +177,16 @@ graph LR
## 问题类型
| 类型 | 说明 | 检查范围 |
|------|------|----------|
| missing | 缺失内容 | 功能-接口对应、异常覆盖 |
| inconsistency | 不一致 | 术语、命名、编号 |
| circular | 循环依赖 | 模块依赖关系 |
| orphan | 孤立内容 | 未被引用的功能/数据结构 |
| syntax | 语法错误 | Mermaid 图表 |
| enhancement | 增强建议 | 内容深度、引用 |
## 与 Phase 4 的集成
```javascript
// Phase 4 装配时读取汇总报告
const summary = JSON.parse(Read(`${outputDir}/cross-module-summary.md`));
if (summary.issues.errors.length > 0) {
// 阻止装配,先修复严重问题
return {
action: "fix_required",
errors: summary.issues.errors
};
}
// 在最终文档中插入跨模块说明
insertCrossModuleSummary(document, summary);
```
| 类型 | 说明 |
|------|------|
| missing | 缺失内容功能-接口对应、异常覆盖|
| inconsistency | 不一致术语、命名、编号|
| circular | 循环依赖 |
| orphan | 孤立内容未被引用|
| syntax | Mermaid 语法错误 |
| enhancement | 增强建议 |
## Output
- 文件: `cross-module-summary.md`
- 返回: 问题列表 + 统计信息
- **文件**: `cross-module-summary.md`(完整汇总报告)
- **返回**: JSON 包含 Phase 4 所需的 synthesis 和 section_summaries

View File

@@ -1,9 +1,16 @@
# Phase 4: Document Assembly
合并所有章节文件,生成最终 CPCC 合规文档
生成索引式文档,通过 markdown 链接引用章节文件
> **规范参考**: [../specs/cpcc-requirements.md](../specs/cpcc-requirements.md)
## 设计原则
1. **引用而非嵌入**:主文档通过链接引用章节,不复制内容
2. **索引 + 综述**:主文档提供导航和软件概述
3. **CPCC 合规**:保持章节编号符合软著申请要求
4. **独立可读**:各章节文件可单独阅读
## 输入
```typescript
@@ -11,6 +18,12 @@ interface AssemblyInput {
output_dir: string;
metadata: ProjectMetadata;
consolidation: {
synthesis: string; // 跨章节综合分析
section_summaries: Array<{
file: string;
title: string;
summary: string;
}>;
issues: { errors: Issue[], warnings: Issue[], info: Issue[] };
stats: { total_sections: number, total_diagrams: number };
};
@@ -22,8 +35,7 @@ interface AssemblyInput {
```javascript
// 1. 检查是否有阻塞性问题
if (consolidation.issues.errors.length > 0) {
// 阻止装配,先修复
return AskUserQuestion({
const response = await AskUserQuestion({
questions: [{
question: `发现 ${consolidation.issues.errors.length} 个严重问题,如何处理?`,
header: "阻塞问题",
@@ -35,33 +47,26 @@ if (consolidation.issues.errors.length > 0) {
]
}]
});
if (response === "查看并修复") {
return { action: "fix_required", errors: consolidation.issues.errors };
}
if (response === "终止") {
return { action: "abort" };
}
}
// 2. 读取章节文件
const sections = [
Read(`${outputDir}/sections/section-2-architecture.md`),
Read(`${outputDir}/sections/section-3-functions.md`),
Read(`${outputDir}/sections/section-4-algorithms.md`),
Read(`${outputDir}/sections/section-5-data-structures.md`),
Read(`${outputDir}/sections/section-6-interfaces.md`),
Read(`${outputDir}/sections/section-7-exceptions.md`)
];
// 2. 生成索引式文档(不读取章节内容)
const doc = generateIndexDocument(metadata, consolidation);
// 3. 读取汇总报告
const crossModuleSummary = Read(`${outputDir}/cross-module-summary.md`);
// 4. 装配文档
const finalDoc = assembleDocument(metadata, sections, crossModuleSummary);
// 5. 写入最终文件
Write(`${outputDir}/${metadata.software_name}-软件设计说明书.md`, finalDoc);
// 3. 写入最终文件
Write(`${outputDir}/${metadata.software_name}-软件设计说明书.md`, doc);
```
## 文档结构
## 文档模板
```markdown
<!-- 页眉:{软件名称} - 版本号:{版本号} -->
<!-- 注:最终文档页码位于每页右上角 -->
# {软件名称} 软件设计说明书
@@ -78,109 +83,56 @@ Write(`${outputDir}/${metadata.software_name}-软件设计说明书.md`, finalDo
## 1. 软件概述
### 1.1 软件背景与用途
{从 metadata 生成}
[从 metadata 生成的软件背景描述]
### 1.2 开发目标与特点
{从 metadata 生成}
[从 metadata 生成的目标和特点]
### 1.3 运行环境与技术架构
{从 metadata.tech_stack 生成}
[从 metadata.tech_stack 生成]
---
<!-- 以下章节直接引用 Agent 产出 -->
## 文档导航
{section-2-architecture.md 内容}
{consolidation.synthesis - 软件整体设计思路综述}
| 章节 | 说明 | 详情 |
|------|------|------|
| 2. 系统架构设计 | {summary} | [查看](./sections/section-2-architecture.md) |
| 3. 功能模块设计 | {summary} | [查看](./sections/section-3-functions.md) |
| 4. 核心算法与流程 | {summary} | [查看](./sections/section-4-algorithms.md) |
| 5. 数据结构设计 | {summary} | [查看](./sections/section-5-data-structures.md) |
| 6. 接口设计 | {summary} | [查看](./sections/section-6-interfaces.md) |
| 7. 异常处理设计 | {summary} | [查看](./sections/section-7-exceptions.md) |
---
{section-3-functions.md 内容}
## 附录
---
{section-4-algorithms.md 内容}
---
{section-5-data-structures.md 内容}
---
{section-6-interfaces.md 内容}
---
{section-7-exceptions.md 内容}
---
## 附录A跨模块分析
{cross-module-summary.md 的问题列表和关联图}
---
## 附录B文档统计
| 章节 | 图表数 | 字数 |
|------|--------|------|
| ... | ... | ... |
- [跨模块分析报告](./cross-module-summary.md)
- [章节文件目录](./sections/)
---
<!-- 页脚:生成时间 {timestamp} -->
```
## Section 1 生成
## 生成函数
```javascript
function generateSection1(metadata) {
const categoryDescriptions = {
"命令行工具 (CLI)": "提供命令行界面,用户通过终端命令与系统交互",
"后端服务/API": "提供 RESTful/GraphQL API 接口,支持前端或其他服务调用",
"SDK/库": "提供可复用的代码库,供其他项目集成使用",
"数据处理系统": "处理数据导入、转换、分析和导出",
"自动化脚本": "自动执行重复性任务,提高工作效率"
};
function generateIndexDocument(metadata, consolidation) {
const date = new Date().toLocaleDateString('zh-CN');
return `
## 1. 软件概述
// 章节导航表格
const sectionTable = consolidation.section_summaries
.map(s => `| ${s.title} | ${s.summary} | [查看](./sections/${s.file}) |`)
.join('\n');
### 1.1 软件背景与用途
${metadata.software_name}是一款${metadata.category}软件。${categoryDescriptions[metadata.category]}
本软件基于${metadata.tech_stack.language}语言开发,运行于${metadata.tech_stack.runtime}环境,采用${metadata.tech_stack.framework || '原生'}框架实现核心功能。
### 1.2 开发目标与特点
**开发目标**
${generateObjectives(metadata)}
**技术特点**
${generateFeatures(metadata)}
### 1.3 运行环境与技术架构
**运行环境**
- 操作系统:${metadata.os || 'Windows/Linux/macOS'}
- 运行时:${metadata.tech_stack.runtime}
- 依赖环境:${metadata.tech_stack.dependencies?.join(', ') || '无'}
**技术架构**
- 架构模式:${metadata.architecture_pattern || '分层架构'}
- 核心框架:${metadata.tech_stack.framework || '原生实现'}
- 主要模块:${metadata.main_modules?.join(', ') || '见第2章'}
`;
}
```
## 装配函数
```javascript
function assembleDocument(metadata, sections, crossModuleSummary) {
const header = `<!-- 页眉:${metadata.software_name} - 版本号:${metadata.version} -->
<!-- 注:最终文档页码位于每页右上角 -->
return `<!-- 页眉:${metadata.software_name} - 版本号:${metadata.version} -->
# ${metadata.software_name} 软件设计说明书
@@ -190,87 +142,120 @@ function assembleDocument(metadata, sections, crossModuleSummary) {
|------|------|
| 软件名称 | ${metadata.software_name} |
| 版本号 | ${metadata.version} |
| 生成日期 | ${new Date().toLocaleDateString('zh-CN')} |
| 生成日期 | ${date} |
---
`;
## 1. 软件概述
const section1 = generateSection1(metadata);
### 1.1 软件背景与用途
// 合并章节 (sections 已经是完整的 MD 内容)
const mainContent = sections.join('\n\n---\n\n');
${generateBackground(metadata)}
// 提取跨模块问题作为附录
const appendixA = extractAppendixA(crossModuleSummary);
### 1.2 开发目标与特点
// 生成统计
const appendixB = generateStats(sections);
${generateObjectives(metadata)}
### 1.3 运行环境与技术架构
${generateTechStack(metadata)}
---
## 设计综述
${consolidation.synthesis}
---
## 文档导航
| 章节 | 说明 | 详情 |
|------|------|------|
${sectionTable}
---
## 附录
- [跨模块分析报告](./cross-module-summary.md)
- [章节文件目录](./sections/)
const footer = `
---
<!-- 页脚:生成时间 ${new Date().toISOString()} -->
`;
}
return header + section1 + '\n\n---\n\n' + mainContent + '\n\n' + appendixA + '\n\n' + appendixB + footer;
function generateBackground(metadata) {
const categoryDescriptions = {
"命令行工具 (CLI)": "提供命令行界面,用户通过终端命令与系统交互",
"后端服务/API": "提供 RESTful/GraphQL API 接口,支持前端或其他服务调用",
"SDK/库": "提供可复用的代码库,供其他项目集成使用",
"数据处理系统": "处理数据导入、转换、分析和导出",
"自动化脚本": "自动执行重复性任务,提高工作效率"
};
return `${metadata.software_name}是一款${metadata.category}软件。${categoryDescriptions[metadata.category] || ''}
本软件基于${metadata.tech_stack.language}语言开发,运行于${metadata.tech_stack.runtime}环境,采用${metadata.tech_stack.framework || '原生'}框架实现核心功能。`;
}
function generateObjectives(metadata) {
return `本软件旨在${metadata.purpose || '解决特定领域的技术问题'}
主要技术特点包括${metadata.tech_stack.framework ? `采用 ${metadata.tech_stack.framework} 框架` : '模块化设计'},具备良好的可扩展性和可维护性。`;
}
function generateTechStack(metadata) {
return `**运行环境**
- 操作系统:${metadata.os || 'Windows/Linux/macOS'}
- 运行时:${metadata.tech_stack.runtime}
- 依赖环境:${metadata.tech_stack.dependencies?.join(', ') || '无特殊依赖'}
**技术架构**
- 架构模式:${metadata.architecture_pattern || '分层架构'}
- 核心框架:${metadata.tech_stack.framework || '原生实现'}
- 主要模块详见第2章系统架构设计`;
}
```
## 附录生成
## 输出结构
### 附录A跨模块分析
```
.workflow/.scratchpad/copyright-{timestamp}/
├── sections/ # 独立章节Phase 2 产出)
│ ├── section-2-architecture.md
│ ├── section-3-functions.md
│ └── ...
├── cross-module-summary.md # 跨模块报告Phase 2.5 产出)
└── {软件名称}-软件设计说明书.md # 索引文档(本阶段产出)
```
```javascript
function extractAppendixA(crossModuleSummary) {
// 从 cross-module-summary.md 提取关键内容
return `
## 附录A跨模块分析
## 与 Phase 2.5 的协作
本附录汇总了文档生成过程中发现的跨模块问题和关联关系。
Phase 2.5 consolidation agent 需要提供:
${extractSection(crossModuleSummary, '## 2. 发现的问题')}
${extractSection(crossModuleSummary, '## 3. 跨模块关联图')}
`;
```typescript
interface ConsolidationOutput {
synthesis: string; // 设计思路综述2-3 段落)
section_summaries: Array<{
file: string; // 文件名
title: string; // 章节标题(如"2. 系统架构设计"
summary: string; // 一句话说明
}>;
issues: {...};
stats: {...};
}
```
### 附录B文档统计
## 关键变更
```javascript
function generateStats(sections) {
const stats = sections.map((content, idx) => {
const diagrams = (content.match(/```mermaid/g) || []).length;
const words = content.length;
const sectionNames = ['系统架构图', '功能模块设计', '核心算法与流程',
'数据结构设计', '接口设计', '异常处理设计'];
return { name: sectionNames[idx], diagrams, words };
});
let table = `
## 附录B文档统计
| 章节 | 图表数 | 字数 |
|------|--------|------|
`;
stats.forEach(s => {
table += `| ${s.name} | ${s.diagrams} | ${s.words} |\n`;
});
const total = stats.reduce((acc, s) => ({
diagrams: acc.diagrams + s.diagrams,
words: acc.words + s.words
}), { diagrams: 0, words: 0 });
table += `| **总计** | **${total.diagrams}** | **${total.words}** |\n`;
return table;
}
```
## 输出
- 最终文档: `{软件名称}-软件设计说明书.md`
- 保留原始章节文件供追溯
| 原设计 | 新设计 |
|--------|--------|
| 读取章节内容并拼接 | 链接引用,不读取内容 |
| 嵌入完整章节 | 仅提供导航索引 |
| 重复生成统计 | 引用 cross-module-summary.md |
| 大文件 | 精简索引文档 |

View File

@@ -37,6 +37,7 @@ Generate comprehensive project analysis reports through multi-phase iterative wo
2. **简要返回**: Agent 只返回路径+摘要,不返回完整内容
3. **汇总 Agent**: 独立 Agent 负责跨章节问题检测和质量评分
4. **引用合并**: Phase 4 读取文件合并,不在上下文中传递
5. **段落式描述**: 禁止清单罗列,层层递进,客观学术表达
## Execution Flow
@@ -157,4 +158,5 @@ Bash(`mkdir "${dir}\\iterations"`);
| [phases/04-report-generation.md](phases/04-report-generation.md) | Report assembly |
| [phases/05-iterative-refinement.md](phases/05-iterative-refinement.md) | Quality refinement |
| [specs/quality-standards.md](specs/quality-standards.md) | Quality gates, standards |
| [specs/writing-style.md](specs/writing-style.md) | 段落式学术写作规范 |
| [../_shared/mermaid-utils.md](../_shared/mermaid-utils.md) | Shared Mermaid utilities |

View File

@@ -24,6 +24,7 @@ For each angle, launch an exploration agent:
```javascript
Task({
subagent_type: "cli-explore-agent",
run_in_background: false,
description: `Explore: ${angle}`,
prompt: `
## Exploration Objective

View File

@@ -1,44 +1,77 @@
# Phase 3: Deep Analysis
并行 Agent 直接写入 MD 章节文件,返回简要信息。
并行 Agent 撰写设计报告章节,返回简要信息。
> **规范参考**: [../specs/quality-standards.md](../specs/quality-standards.md)
> **写作风格**: [../specs/writing-style.md](../specs/writing-style.md)
## Agent 配置
## Agent 执行前置条件
根据报告类型,使用不同的 Agent 配置
**每个 Agent 必须首先读取以下规范文件**
### Architecture Report Agents
```javascript
// Agent 启动时的第一步操作
const specs = {
quality: Read(`${skillRoot}/specs/quality-standards.md`),
style: Read(`${skillRoot}/specs/writing-style.md`)
};
```
| Agent | 输出文件 | 章节 |
|-------|----------|------|
| overview | sections/section-overview.md | System Overview |
| layers | sections/section-layers.md | Layer Analysis |
| dependencies | sections/section-dependencies.md | Module Dependencies |
| dataflow | sections/section-dataflow.md | Data Flow |
| entrypoints | sections/section-entrypoints.md | Entry Points & Critical Paths |
### Design Report Agents
| Agent | 输出文件 | 章节 |
|-------|----------|------|
| patterns | sections/section-patterns.md | Design Patterns Used |
| classes | sections/section-classes.md | Class Relationships |
| interfaces | sections/section-interfaces.md | Interface Contracts |
| state | sections/section-state.md | State Management |
### Methods Report Agents
| Agent | 输出文件 | 章节 |
|-------|----------|------|
| algorithms | sections/section-algorithms.md | Core Algorithms |
| paths | sections/section-paths.md | Critical Code Paths |
| apis | sections/section-apis.md | Public API Reference |
| logic | sections/section-logic.md | Complex Logic Breakdown |
规范文件路径(相对于 skill 根目录):
- `specs/quality-standards.md` - 质量标准和检查清单
- `specs/writing-style.md` - 段落式写作规范
---
## 通用 Agent 返回格式
## 通用写作规范(所有 Agent 共用)
```
[STYLE]
- **语言规范**:使用严谨、专业的中文进行技术写作。仅专业术语(如 Singleton, Middleware, ORM保留英文原文。
- **叙述视角**:采用完全客观的第三人称视角("上帝视角")。严禁使用"我们"、"开发者"、"用户"、"你"或"我"。主语应为"系统"、"模块"、"设计"、"架构"或"该层"。
- **段落结构**
- 禁止使用无序列表作为主要叙述方式,必须将观点融合在连贯的段落中。
- 采用"论点-论据-结论"的逻辑结构。
- 善用逻辑连接词("因此"、"然而"、"鉴于"、"进而")来体现设计思路的推演过程。
- **内容深度**
- 抽象化:描述"做什么"和"为什么这么做",而不是"怎么写的"。
- 方法论:强调设计模式、架构原则(如 SOLID、高内聚低耦合的应用。
- 非代码化:除非定义关键接口,否则不直接引用代码。文件引用仅作为括号内的来源标注 (参考: path/to/file)。
```
## Agent 配置
### Architecture Report Agents
| Agent | 输出文件 | 关注点 |
|-------|----------|--------|
| overview | section-overview.md | 顶层架构、技术决策、设计哲学 |
| layers | section-layers.md | 逻辑分层、职责边界、隔离策略 |
| dependencies | section-dependencies.md | 依赖治理、集成拓扑、风险控制 |
| dataflow | section-dataflow.md | 数据流向、转换机制、一致性保障 |
| entrypoints | section-entrypoints.md | 入口设计、调用链、异常传播 |
### Design Report Agents
| Agent | 输出文件 | 关注点 |
|-------|----------|--------|
| patterns | section-patterns.md | 架构模式、通信机制、横切关注点 |
| classes | section-classes.md | 类型体系、继承策略、职责划分 |
| interfaces | section-interfaces.md | 契约设计、抽象层次、扩展机制 |
| state | section-state.md | 状态模型、生命周期、并发控制 |
### Methods Report Agents
| Agent | 输出文件 | 关注点 |
|-------|----------|--------|
| algorithms | section-algorithms.md | 核心算法思想、复杂度权衡、优化策略 |
| paths | section-paths.md | 关键路径设计、性能敏感点、瓶颈分析 |
| apis | section-apis.md | API 设计规范、版本策略、兼容性 |
| logic | section-logic.md | 业务逻辑建模、决策机制、边界处理 |
---
## Agent 返回格式
```typescript
interface AgentReturn {
@@ -46,111 +79,92 @@ interface AgentReturn {
output_file: string;
summary: string; // 50字以内
cross_module_notes: string[]; // 跨模块发现
stats: {
diagrams: number;
code_refs: number;
};
stats: { diagrams: number; };
}
```
---
## Agent 提示词模板
## Agent 提示词
### Overview Agent (Architecture)
### Overview Agent
```javascript
Task({
subagent_type: "cli-explore-agent",
run_in_background: false,
prompt: `
[ROLE] 系统架构师,专注于系统全貌和核心组件。
[SPEC]
首先读取规范文件:
- Read: ${skillRoot}/specs/quality-standards.md
- Read: ${skillRoot}/specs/writing-style.md
严格遵循规范中的质量标准和段落式写作要求。
[ROLE] 首席系统架构师
[TASK]
分析 ${config.scope},生成 System Overview 章节
基于代码库的全貌,撰写《系统架构设计报告》的"总体架构"章节。透过代码表象,洞察系统的核心价值主张和顶层技术决策
输出: ${outDir}/sections/section-overview.md
[QUALITY_SPEC]
- 内容基于代码分析,无臆测
- 代码引用格式: \`file:line\`
- 每个子章节 ≥100字
- 包含至少1个 Mermaid 图表
[TEMPLATE]
## System Overview
### Project Summary
{项目概述,技术栈,核心功能}
### Architecture Diagram
\`\`\`mermaid
graph TD
subgraph Core["核心层"]
A[组件A]
end
\`\`\`
### Key Components
| 组件 | 职责 | 文件 |
|------|------|------|
### Technology Stack
| 技术 | 用途 | 版本 |
|------|------|------|
[STYLE]
- 严谨专业的中文技术写作,专业术语保留英文
- 完全客观的第三人称视角,严禁"我们"、"开发者"
- 段落式叙述,采用"论点-论据-结论"结构
- 善用逻辑连接词体现设计推演过程
- 描述"做什么"和"为什么",非"怎么写的"
- 不直接引用代码,文件仅作来源标注
[FOCUS]
1. 项目定位和核心功能
2. 技术栈和依赖
3. 核心组件及职责
4. 整体架构模式
- 领域边界与定位:系统旨在解决什么核心业务问题?其在更大的技术生态中处于什么位置?
- 架构范式:采用何种架构风格(分层、六边形、微服务、事件驱动等)?选择该范式的根本原因是什么?
- 核心技术决策:关键技术栈的选型依据,这些选型如何支撑系统的非功能性需求(性能、扩展性、维护性)
- 顶层模块划分:系统在最高层级被划分为哪些逻辑单元?它们之间的高层协作机制是怎样的?
[CONSTRAINT]
- 避免罗列目录结构
- 重点阐述"设计意图"而非"现有功能"
- 包含至少1个 Mermaid 架构图辅助说明
[RETURN JSON]
{"status":"completed","output_file":"section-overview.md","summary":"<50字>","cross_module_notes":[],"stats":{"diagrams":1,"code_refs":5}}
{"status":"completed","output_file":"section-overview.md","summary":"<50字>","cross_module_notes":[],"stats":{"diagrams":1}}
`
})
```
### Layers Agent (Architecture)
### Layers Agent
```javascript
Task({
subagent_type: "cli-explore-agent",
run_in_background: false,
prompt: `
[ROLE] 架构分析师,专注于分层结构。
[SPEC]
首先读取规范文件:
- Read: ${skillRoot}/specs/quality-standards.md
- Read: ${skillRoot}/specs/writing-style.md
严格遵循规范中的质量标准和段落式写作要求。
[ROLE] 资深软件设计师
[TASK]
分析 ${config.scope},生成 Layer Analysis 章节
分析系统的逻辑分层结构,撰写《系统架构设计报告》的"逻辑视点与分层架构"章节。重点揭示系统如何通过分层来隔离关注点
输出: ${outDir}/sections/section-layers.md
[QUALITY_SPEC]
- 内容基于代码分析,无臆测
- 代码引用格式: \`file:line\`
- 每个子章节 ≥100字
[TEMPLATE]
## Layer Analysis
### Layer Overview
\`\`\`mermaid
graph TD
L1[表现层] --> L2[业务层]
L2 --> L3[数据层]
\`\`\`
### Layer Details
| 层级 | 目录 | 职责 | 组件数 |
|------|------|------|--------|
### Layer Interactions
{层间交互说明}
### Violations & Recommendations
{违反分层的情况和建议}
[STYLE]
- 严谨专业的中文技术写作
- 客观第三人称视角,主语为"系统"、"该层"、"设计"
- 段落式叙述,禁止无序列表作为主体
- 强调方法论和架构原则的应用
[FOCUS]
1. 识别代码分层(按目录/命名空间)
2. 每层职责和边界
3. 层间依赖方向
4. 违反分层原则的情况
- 职责分配体系:系统被划分为哪几个逻辑层级?每一层的核心职责和输入输出是什么?
- 数据流向与约束:数据在各层之间是如何流动的?是否存在严格的单向依赖规则?
- 边界隔离策略各层之间通过何种方式解耦接口抽象、DTO转换、依赖注入如何防止下层实现细节泄露到上层
- 异常处理流:异常信息如何在分层结构中传递和转化?
[CONSTRAINT]
- 不要列举具体的文件名列表
- 关注"层级间的契约"和"隔离的艺术"
[RETURN JSON]
{"status":"completed","output_file":"section-layers.md","summary":"<50字>","cross_module_notes":[],"stats":{}}
@@ -158,90 +172,79 @@ graph TD
})
```
### Dependencies Agent (Architecture)
### Dependencies Agent
```javascript
Task({
subagent_type: "cli-explore-agent",
run_in_background: false,
prompt: `
[ROLE] 依赖分析师,专注于模块依赖关系。
[SPEC]
首先读取规范文件:
- Read: ${skillRoot}/specs/quality-standards.md
- Read: ${skillRoot}/specs/writing-style.md
严格遵循规范中的质量标准和段落式写作要求。
[ROLE] 集成架构专家
[TASK]
分析 ${config.scope},生成 Module Dependencies 章节。
审视系统的外部连接与内部耦合情况,撰写《系统架构设计报告》的"依赖管理与生态集成"章节。
输出: ${outDir}/sections/section-dependencies.md
[TEMPLATE]
## Module Dependencies
### Dependency Graph
\`\`\`mermaid
graph LR
A[ModuleA] --> B[ModuleB]
A --> C[ModuleC]
B --> D[ModuleD]
\`\`\`
### Module List
| 模块 | 路径 | 依赖数 | 被依赖数 |
|------|------|--------|----------|
### Critical Dependencies
{核心依赖说明}
### Circular Dependencies
{循环依赖检测结果}
[STYLE]
- 严谨专业的中文技术写作
- 客观第三人称视角
- 段落式叙述,逻辑连贯
[FOCUS]
1. 模块间依赖关系
2. 依赖方向(单向/双向)
3. 循环依赖检测
4. 核心模块识别
- 外部集成拓扑系统如何与外部世界第三方API、数据库、中间件交互采用了何种适配器或防腐层设计来隔离外部变化
- 核心依赖分析:区分"核心业务依赖"与"基础设施依赖"。系统对关键框架的依赖程度如何?是否存在被锁定的风险?
- 依赖注入与控制反转:系统内部模块间的组装方式是什么?是否实现了依赖倒置原则以支持可测试性?
- 供应链安全与治理:对于复杂的依赖树,系统采用了何种策略来管理版本和兼容性?
[CONSTRAINT]
- 禁止简单列出依赖配置文件的内容
- 必须分析依赖背后的"集成策略"和"风险控制模型"
[RETURN JSON]
{"status":"completed","output_file":"section-dependencies.md","summary":"<50字>","cross_module_notes":["发现循环依赖: A <-> B"],"stats":{}}
{"status":"completed","output_file":"section-dependencies.md","summary":"<50字>","cross_module_notes":[],"stats":{}}
`
})
```
### Patterns Agent (Design)
### Patterns Agent
```javascript
Task({
subagent_type: "cli-explore-agent",
run_in_background: false,
prompt: `
[ROLE] 设计模式专家,专注于识别和分析设计模式。
[SPEC]
首先读取规范文件:
- Read: ${skillRoot}/specs/quality-standards.md
- Read: ${skillRoot}/specs/writing-style.md
严格遵循规范中的质量标准和段落式写作要求。
[ROLE] 核心开发规范制定者
[TASK]
分析 ${config.scope},生成 Design Patterns 章节。
挖掘代码中的复用机制和标准化实践,撰写《系统架构设计报告》的"设计模式与工程规范"章节。
输出: ${outDir}/sections/section-patterns.md
[TEMPLATE]
## Design Patterns Used
### Pattern Summary
| 模式 | 类型 | 实现位置 | 说明 |
|------|------|----------|------|
### Pattern Details
#### Singleton Pattern
**位置**: \`src/core/config.ts:15\`
**说明**: {实现说明}
\`\`\`mermaid
classDiagram
class Singleton {
-instance: Singleton
+getInstance()
}
\`\`\`
### Pattern Recommendations
{模式使用建议}
[STYLE]
- 严谨专业的中文技术写作
- 客观第三人称视角
- 段落式叙述,结合项目上下文
[FOCUS]
1. 识别使用的设计模式(GoF 23种 + 其他)
2. 模式实现质量评估
3. 模式使用建议
- 架构级模式:识别系统中广泛使用的架构模式(CQRS、Event Sourcing、Repository Pattern、Unit of Work。阐述引入这些模式解决了什么特定难题
- 通信与并发模式:分析组件间的通信机制(同步/异步、观察者模式、发布订阅)以及并发控制策略
- 横切关注点实现系统如何统一处理日志、鉴权、缓存、事务管理等横切逻辑AOP、中间件管道、装饰器
- 抽象与复用策略:分析基类、泛型、工具类的设计思想,系统如何通过抽象来减少重复代码并提高一致性?
[CONSTRAINT]
- 避免教科书式地解释设计模式定义,必须结合当前项目上下文说明其应用场景
- 关注"解决类问题的通用机制"
[RETURN JSON]
{"status":"completed","output_file":"section-patterns.md","summary":"<50字>","cross_module_notes":[],"stats":{}}
@@ -249,47 +252,239 @@ classDiagram
})
```
### Algorithms Agent (Methods)
### DataFlow Agent
```javascript
Task({
subagent_type: "cli-explore-agent",
run_in_background: false,
prompt: `
[ROLE] 算法分析师,专注于核心算法和复杂度。
[SPEC]
首先读取规范文件:
- Read: ${skillRoot}/specs/quality-standards.md
- Read: ${skillRoot}/specs/writing-style.md
严格遵循规范中的质量标准和段落式写作要求。
[ROLE] 数据架构师
[TASK]
分析 ${config.scope},生成 Core Algorithms 章节。
输出: ${outDir}/sections/section-algorithms.md
追踪系统的数据流转机制,撰写《系统架构设计报告》的"数据流与状态管理"章节。
输出: ${outDir}/sections/section-dataflow.md
[TEMPLATE]
## Core Algorithms
### Algorithm Inventory
| 算法 | 文件 | 复杂度 | 说明 |
|------|------|--------|------|
### Algorithm Details
#### {算法名称}
**位置**: \`src/algo/xxx.ts:42\`
**复杂度**: 时间 O(n log n), 空间 O(n)
\`\`\`mermaid
flowchart TD
Start([开始]) --> Process[处理]
Process --> End([结束])
\`\`\`
**说明**: {算法说明≥100字}
### Optimization Suggestions
{优化建议}
[STYLE]
- 严谨专业的中文技术写作
- 客观第三人称视角
- 段落式叙述
[FOCUS]
1. 核心业务算法
2. 复杂度分析
3. 算法流程图
4. 优化建议
- 数据入口与出口:数据从何处进入系统,最终流向何处?边界处的数据校验和转换策略是什么?
- 数据转换管道:数据在各层/模块间经历了怎样的形态变化DTO、Entity、VO 等数据对象的职责边界如何划分?
- 持久化策略:系统如何设计数据存储方案?采用了何种 ORM 策略或数据访问模式?
- 一致性保障:系统如何处理事务边界?分布式场景下如何保证数据一致性?
[CONSTRAINT]
- 关注数据的"生命周期"和"形态演变"
- 不要罗列数据库表结构
[RETURN JSON]
{"status":"completed","output_file":"section-dataflow.md","summary":"<50字>","cross_module_notes":[],"stats":{}}
`
})
```
### EntryPoints Agent
```javascript
Task({
subagent_type: "cli-explore-agent",
run_in_background: false,
prompt: `
[SPEC]
首先读取规范文件:
- Read: ${skillRoot}/specs/quality-standards.md
- Read: ${skillRoot}/specs/writing-style.md
严格遵循规范中的质量标准和段落式写作要求。
[ROLE] 系统边界分析师
[TASK]
识别系统的入口设计和关键路径,撰写《系统架构设计报告》的"系统入口与调用链"章节。
输出: ${outDir}/sections/section-entrypoints.md
[STYLE]
- 严谨专业的中文技术写作
- 客观第三人称视角
- 段落式叙述
[FOCUS]
- 入口类型与职责系统提供了哪些类型的入口REST API、CLI、消息队列消费者、定时任务各入口的设计目的和适用场景是什么
- 请求处理管道:从入口到核心逻辑,请求经过了怎样的处理管道?中间件/拦截器的编排逻辑是什么?
- 关键业务路径:最重要的几条业务流程的调用链是怎样的?关键节点的设计考量是什么?
- 异常与边界处理:系统如何统一处理异常?异常信息如何传播和转化?
[CONSTRAINT]
- 关注"入口的设计哲学"而非 API 清单
- 不要逐个列举所有端点
[RETURN JSON]
{"status":"completed","output_file":"section-entrypoints.md","summary":"<50字>","cross_module_notes":[],"stats":{}}
`
})
```
### Classes Agent
```javascript
Task({
subagent_type: "cli-explore-agent",
run_in_background: false,
prompt: `
[SPEC]
首先读取规范文件:
- Read: ${skillRoot}/specs/quality-standards.md
- Read: ${skillRoot}/specs/writing-style.md
严格遵循规范中的质量标准和段落式写作要求。
[ROLE] 领域模型设计师
[TASK]
分析系统的类型体系和领域模型,撰写《系统架构设计报告》的"类型体系与领域建模"章节。
输出: ${outDir}/sections/section-classes.md
[STYLE]
- 严谨专业的中文技术写作
- 客观第三人称视角
- 段落式叙述
[FOCUS]
- 领域模型设计:系统的核心领域概念有哪些?它们之间的关系如何建模(聚合、实体、值对象)?
- 继承与组合策略:系统倾向于使用继承还是组合?基类/接口的设计意图是什么?
- 职责分配原则:类的职责划分遵循了什么原则?是否体现了单一职责原则?
- 类型安全与约束:系统如何利用类型系统来表达业务约束和不变量?
[CONSTRAINT]
- 关注"建模思想"而非类的属性列表
- 用 UML 类图辅助说明核心关系
[RETURN JSON]
{"status":"completed","output_file":"section-classes.md","summary":"<50字>","cross_module_notes":[],"stats":{}}
`
})
```
### Interfaces Agent
```javascript
Task({
subagent_type: "cli-explore-agent",
run_in_background: false,
prompt: `
[SPEC]
首先读取规范文件:
- Read: ${skillRoot}/specs/quality-standards.md
- Read: ${skillRoot}/specs/writing-style.md
严格遵循规范中的质量标准和段落式写作要求。
[ROLE] 契约设计专家
[TASK]
分析系统的接口设计和抽象层次,撰写《系统架构设计报告》的"接口契约与抽象设计"章节。
输出: ${outDir}/sections/section-interfaces.md
[STYLE]
- 严谨专业的中文技术写作
- 客观第三人称视角
- 段落式叙述
[FOCUS]
- 抽象层次设计:系统定义了哪些核心接口/抽象类?这些抽象的设计意图和职责边界是什么?
- 契约与实现分离:接口如何隔离契约与实现?多态机制如何被运用?
- 扩展点设计:系统预留了哪些扩展点?如何在不修改核心代码的情况下扩展功能?
- 版本演进策略:接口如何支持版本演进?向后兼容性如何保障?
[CONSTRAINT]
- 关注"接口的设计哲学"
- 不要逐个列举接口方法签名
[RETURN JSON]
{"status":"completed","output_file":"section-interfaces.md","summary":"<50字>","cross_module_notes":[],"stats":{}}
`
})
```
### State Agent
```javascript
Task({
subagent_type: "cli-explore-agent",
run_in_background: false,
prompt: `
[SPEC]
首先读取规范文件:
- Read: ${skillRoot}/specs/quality-standards.md
- Read: ${skillRoot}/specs/writing-style.md
严格遵循规范中的质量标准和段落式写作要求。
[ROLE] 状态管理架构师
[TASK]
分析系统的状态管理机制,撰写《系统架构设计报告》的"状态管理与生命周期"章节。
输出: ${outDir}/sections/section-state.md
[STYLE]
- 严谨专业的中文技术写作
- 客观第三人称视角
- 段落式叙述
[FOCUS]
- 状态模型设计:系统需要管理哪些类型的状态(会话状态、应用状态、领域状态)?状态的存储位置和作用域是什么?
- 状态生命周期:状态如何创建、更新、销毁?生命周期管理的机制是什么?
- 并发与一致性:多线程/多实例场景下,状态如何保持一致?采用了何种并发控制策略?
- 状态恢复与容错:系统如何处理状态丢失或损坏?是否有状态恢复机制?
[CONSTRAINT]
- 关注"状态管理的设计决策"
- 不要列举具体的变量名
[RETURN JSON]
{"status":"completed","output_file":"section-state.md","summary":"<50字>","cross_module_notes":[],"stats":{}}
`
})
```
### Algorithms Agent
```javascript
Task({
subagent_type: "cli-explore-agent",
run_in_background: false,
prompt: `
[SPEC]
首先读取规范文件:
- Read: ${skillRoot}/specs/quality-standards.md
- Read: ${skillRoot}/specs/writing-style.md
严格遵循规范中的质量标准和段落式写作要求。
[ROLE] 算法架构师
[TASK]
分析系统的核心算法设计,撰写《系统架构设计报告》的"核心算法与计算模型"章节。
输出: ${outDir}/sections/section-algorithms.md
[STYLE]
- 严谨专业的中文技术写作
- 客观第三人称视角
- 段落式叙述
[FOCUS]
- 算法选型与权衡:系统的核心业务逻辑采用了哪些关键算法?选择这些算法的考量因素是什么(时间复杂度、空间复杂度、可维护性)?
- 计算模型设计复杂计算如何被分解和组织是否采用了流水线、Map-Reduce 等计算模式?
- 性能与可扩展性:算法设计如何考虑性能和可扩展性?是否有针对大数据量的优化策略?
- 正确性保障:关键算法的正确性如何保障?是否有边界条件的特殊处理?
[CONSTRAINT]
- 关注"算法思想"而非具体实现代码
- 用流程图辅助说明复杂逻辑
[RETURN JSON]
{"status":"completed","output_file":"section-algorithms.md","summary":"<50字>","cross_module_notes":[],"stats":{}}
@@ -297,6 +492,126 @@ flowchart TD
})
```
### Paths Agent
```javascript
Task({
subagent_type: "cli-explore-agent",
run_in_background: false,
prompt: `
[SPEC]
首先读取规范文件:
- Read: ${skillRoot}/specs/quality-standards.md
- Read: ${skillRoot}/specs/writing-style.md
严格遵循规范中的质量标准和段落式写作要求。
[ROLE] 性能架构师
[TASK]
分析系统的关键执行路径,撰写《系统架构设计报告》的"关键路径与性能设计"章节。
输出: ${outDir}/sections/section-paths.md
[STYLE]
- 严谨专业的中文技术写作
- 客观第三人称视角
- 段落式叙述
[FOCUS]
- 关键业务路径:系统中最重要的几条业务执行路径是什么?这些路径的设计目标和约束是什么?
- 性能敏感区域:哪些环节是性能敏感的?系统采用了何种优化策略(缓存、异步、批处理)?
- 瓶颈识别与缓解:潜在的性能瓶颈在哪里?设计中是否预留了扩展空间?
- 降级与熔断:在高负载或故障场景下,系统如何保护关键路径?
[CONSTRAINT]
- 关注"路径设计的战略考量"
- 不要罗列所有代码执行步骤
[RETURN JSON]
{"status":"completed","output_file":"section-paths.md","summary":"<50字>","cross_module_notes":[],"stats":{}}
`
})
```
### APIs Agent
```javascript
Task({
subagent_type: "cli-explore-agent",
run_in_background: false,
prompt: `
[SPEC]
首先读取规范文件:
- Read: ${skillRoot}/specs/quality-standards.md
- Read: ${skillRoot}/specs/writing-style.md
严格遵循规范中的质量标准和段落式写作要求。
[ROLE] API 设计规范专家
[TASK]
分析系统的对外接口设计规范,撰写《系统架构设计报告》的"API 设计与规范"章节。
输出: ${outDir}/sections/section-apis.md
[STYLE]
- 严谨专业的中文技术写作
- 客观第三人称视角
- 段落式叙述
[FOCUS]
- API 设计风格:系统采用了何种 API 设计风格RESTful、GraphQL、RPC选择该风格的原因是什么
- 命名与结构规范API 的命名、路径结构、参数设计遵循了什么规范?是否有一致性保障机制?
- 版本管理策略API 如何支持版本演进?向后兼容性策略是什么?
- 错误处理规范API 错误响应的设计规范是什么?错误码体系如何组织?
[CONSTRAINT]
- 关注"设计规范和一致性"
- 不要逐个列举所有 API 端点
[RETURN JSON]
{"status":"completed","output_file":"section-apis.md","summary":"<50字>","cross_module_notes":[],"stats":{}}
`
})
```
### Logic Agent
```javascript
Task({
subagent_type: "cli-explore-agent",
run_in_background: false,
prompt: `
[SPEC]
首先读取规范文件:
- Read: ${skillRoot}/specs/quality-standards.md
- Read: ${skillRoot}/specs/writing-style.md
严格遵循规范中的质量标准和段落式写作要求。
[ROLE] 业务逻辑架构师
[TASK]
分析系统的业务逻辑建模,撰写《系统架构设计报告》的"业务逻辑与规则引擎"章节。
输出: ${outDir}/sections/section-logic.md
[STYLE]
- 严谨专业的中文技术写作
- 客观第三人称视角
- 段落式叙述
[FOCUS]
- 业务规则建模:核心业务规则如何被表达和组织?是否采用了规则引擎或策略模式?
- 决策点设计:系统中的关键决策点有哪些?决策逻辑如何被封装和测试?
- 边界条件处理:系统如何处理边界条件和异常情况?是否有防御性编程措施?
- 业务流程编排:复杂业务流程如何被编排?是否采用了工作流引擎或状态机?
[CONSTRAINT]
- 关注"业务逻辑的组织方式"
- 不要逐行解释代码逻辑
[RETURN JSON]
{"status":"completed","output_file":"section-logic.md","summary":"<50字>","cross_module_notes":[],"stats":{}}
`
})
```
---
## 执行流程
@@ -306,7 +621,7 @@ flowchart TD
const agentConfigs = getAgentConfigs(config.type);
// 2. 准备目录
Bash(`mkdir -p ${outputDir}/sections`);
Bash(`mkdir "${outputDir}\\sections"`);
// 3. 并行启动所有 Agent
const results = await Promise.all(
@@ -317,10 +632,7 @@ const results = await Promise.all(
const summaries = results.map(r => JSON.parse(r));
// 5. 传递给 Phase 3.5 汇总 Agent
return {
summaries,
cross_notes: summaries.flatMap(s => s.cross_module_notes)
};
return { summaries, cross_notes: summaries.flatMap(s => s.cross_module_notes) };
```
## Output

View File

@@ -1,6 +1,15 @@
# Phase 3.5: Consolidation Agent
汇总所有分析 Agent 的产出,识别跨章节问题,生成汇总报告
汇总所有分析 Agent 的产出,生成跨章节综合分析,为 Phase 4 索引报告提供内容
> **写作规范**: [../specs/writing-style.md](../specs/writing-style.md)
## 核心职责
1. **跨章节综合分析**:生成 synthesis报告综述
2. **章节摘要提取**:生成 section_summaries索引表格内容
3. **质量检查**:识别问题并评分
4. **建议汇总**:生成 recommendations优先级排序
## 输入
@@ -18,9 +27,16 @@ interface ConsolidationInput {
```javascript
Task({
subagent_type: "cli-explore-agent",
run_in_background: false,
prompt: `
## 规范前置
首先读取规范文件:
- Read: ${skillRoot}/specs/quality-standards.md
- Read: ${skillRoot}/specs/writing-style.md
严格遵循规范中的质量标准和段落式写作要求。
## 任务
作为汇总 Agent读取所有章节文件执行跨章节分析生成汇总报告。
作为汇总 Agent读取所有章节文件执行跨章节分析生成汇总报告和索引内容
## 输入
- 章节文件: ${outputDir}/sections/section-*.md
@@ -28,27 +44,39 @@ Task({
- 跨模块备注: ${JSON.stringify(cross_module_notes)}
- 报告类型: ${config.type}
## 分析维度
## 核心产出
### 1. 一致性检查
### 1. 综合分析 (synthesis)
阅读所有章节,用 2-3 段落描述项目全貌:
- 第一段:项目定位与核心架构特征
- 第二段:关键设计决策与技术选型
- 第三段:整体质量评价与显著特点
### 2. 章节摘要 (section_summaries)
为每个章节提取一句话核心发现,用于索引表格。
### 3. 架构洞察 (cross_analysis)
描述章节间的关联性,如:
- 模块间的依赖关系如何体现在各章节
- 设计决策如何贯穿多个层面
- 潜在的一致性或冲突
### 4. 建议汇总 (recommendations)
按优先级整理各章节的建议,段落式描述。
## 质量检查维度
### 一致性检查
- 术语一致性:同一概念是否使用相同名称
- 代码引用file:line 格式是否正确
- 图表编号:是否连续且正确
### 2. 完整性检查
### 完整性检查
- 章节覆盖:是否涵盖所有必需章节
- 内容深度:每章节是否达到 ${config.depth} 级别
- 图表数量:是否每章节包含图表
### 3. 质量检查
### 质量检查
- Mermaid 语法:图表是否可渲染
- 代码引用有效性:引用的文件是否存在
- 建议可行性:推荐是否具体可操作
### 4. 关联性检查
- 章节间引用:是否有交叉引用
- 逻辑连贯:内容是否逻辑一致
- 重复内容:是否有冗余描述
- 段落式写作:是否符合写作规范(禁止清单罗列)
## 输出文件
@@ -59,58 +87,62 @@ Task({
\`\`\`markdown
# 分析汇总报告
## 1. 章节统计
## 综合分析
| 章节 | 状态 | 图表数 | 代码引用数 | 字数 |
|------|------|--------|------------|------|
| System Overview | completed | 1 | 5 | 450 |
| ... | ... | ... | ... | ... |
[2-3 段落的项目全貌描述,段落式写作]
## 2. 发现的问题
## 章节摘要
### 2.1 严重问题 (阻塞报告生成)
| 章节 | 文件 | 核心发现 |
|------|------|----------|
| 系统概述 | section-overview.md | 一句话描述 |
| 层次分析 | section-layers.md | 一句话描述 |
| ... | ... | ... |
| ID | 类型 | 位置 | 描述 | 建议 |
|----|------|------|------|------|
| E001 | missing | section-dataflow | 缺少数据流章节 | 补充数据流分析 |
## 架构洞察
### 2.2 警告 (影响报告质量)
[跨章节关联分析,段落式描述]
| ID | 类型 | 位置 | 描述 | 建议 |
|----|------|------|------|------|
| W001 | inconsistency | section-overview/layers | 架构描述不一致 | 统一术语 |
## 建议汇总
### 2.3 提示 (可选优化)
[优先级排序的建议,段落式描述]
| ID | 类型 | 位置 | 描述 |
|----|------|------|------|
| I001 | enhancement | section-algorithms | 建议添加复杂度分析 |
---
## 3. 跨章节关联
## 质量评估
\`\`\`mermaid
graph LR
S1[Overview] --> S2[Layers]
S2 --> S3[Dependencies]
S3 --> S4[DataFlow]
S2 -.->|不一致| S1
\`\`\`
## 4. 报告质量评分
### 评分
| 维度 | 得分 | 说明 |
|------|------|------|
| 完整性 | 85% | 缺少1个章节 |
| 一致性 | 90% | 2处术语不一致 |
| 深度 | 95% | 符合 ${config.depth} 级别 |
| 可读性 | 88% | 图表清晰 |
| 完整性 | 85% | ... |
| 一致性 | 90% | ... |
| 深度 | 95% | ... |
| 可读性 | 88% | ... |
| 综合 | 89% | ... |
## 5. 推荐操作
### 发现的问题
1. **[必须]** 补充 section-dataflow 章节
2. **[建议]** 统一 Overview 和 Layers 中的架构术语
3. **[可选]** 为算法章节添加复杂度分析
#### 严重问题
| ID | 类型 | 位置 | 描述 |
|----|------|------|------|
| E001 | ... | ... | ... |
#### 警告
| ID | 类型 | 位置 | 描述 |
|----|------|------|------|
| W001 | ... | ... | ... |
#### 提示
| ID | 类型 | 位置 | 描述 |
|----|------|------|------|
| I001 | ... | ... | ... |
### 统计
- 章节数: X
- 图表数: X
- 总字数: X
\`\`\`
## 返回格式 (JSON)
@@ -118,6 +150,17 @@ graph LR
{
"status": "completed",
"output_file": "consolidation-summary.md",
// Phase 4 索引报告所需
"synthesis": "2-3 段落的综合分析文本",
"cross_analysis": "跨章节关联分析文本",
"recommendations": "优先级排序的建议文本",
"section_summaries": [
{"file": "section-overview.md", "title": "系统概述", "summary": "一句话核心发现"},
{"file": "section-layers.md", "title": "层次分析", "summary": "一句话核心发现"}
],
// 质量信息
"quality_score": {
"completeness": 85,
"consistency": 90,
@@ -126,20 +169,13 @@ graph LR
"overall": 89
},
"issues": {
"errors": [
{"id": "E001", "type": "missing", "section": "section-dataflow", "desc": "缺少数据流章节"}
],
"warnings": [
{"id": "W001", "type": "inconsistency", "sections": ["section-overview", "section-layers"], "desc": "架构描述不一致"}
],
"info": [
{"id": "I001", "type": "enhancement", "section": "section-algorithms", "desc": "建议添加复杂度分析"}
]
"errors": [...],
"warnings": [...],
"info": [...]
},
"stats": {
"total_sections": 5,
"total_diagrams": 8,
"total_code_refs": 42,
"total_words": 3500
}
}
@@ -157,42 +193,16 @@ graph LR
## 问题类型
| 类型 | 说明 | 检查范围 |
|------|------|----------|
| missing | 缺失章节 | 必需章节列表 |
| inconsistency | 不一致 | 术语、命名、描述 |
| invalid_ref | 无效引用 | 代码引用、图表引用 |
| syntax | 语法错误 | Mermaid 图表 |
| shallow | 内容过浅 | 深度级别要求 |
| enhancement | 增强建议 | 最佳实践 |
## 与 Phase 4 的集成
```javascript
// Phase 4 装配时读取汇总报告
const summary = JSON.parse(Read(`${outputDir}/consolidation-summary.md`));
if (summary.issues.errors.length > 0) {
// 阻止装配,提示用户
return AskUserQuestion({
questions: [{
question: `发现 ${summary.issues.errors.length} 个严重问题,如何处理?`,
header: "质量检查",
multiSelect: false,
options: [
{label: "查看并修复", description: "显示问题,手动修复"},
{label: "忽略继续", description: "跳过检查,继续装配"},
{label: "终止", description: "停止报告生成"}
]
}]
});
}
// 在报告中插入质量评分
insertQualityScore(report, summary.quality_score);
```
| 类型 | 说明 |
|------|------|
| missing | 缺失章节 |
| inconsistency | 术语/描述不一致 |
| invalid_ref | 无效代码引用 |
| syntax | Mermaid 语法错误 |
| shallow | 内容过浅 |
| list_style | 违反段落式写作规范 |
## Output
- 文件: `consolidation-summary.md`
- 返回: 质量评分 + 问题列表 + 统计信息
- **文件**: `consolidation-summary.md`(完整汇总报告)
- **返回**: JSON 包含 Phase 4 所需的所有字段

View File

@@ -1,9 +1,16 @@
# Phase 4: Report Generation
合并所有章节文件,生成最终分析报告
生成索引式报告,通过 markdown 链接引用章节文件
> **规范参考**: [../specs/quality-standards.md](../specs/quality-standards.md)
## 设计原则
1. **引用而非嵌入**:主报告通过链接引用章节,不复制内容
2. **索引 + 综述**:主报告提供导航和高阶分析
3. **避免重复**:综述来自 consolidation不重新生成
4. **独立可读**:各章节文件可单独阅读
## 输入
```typescript
@@ -14,6 +21,8 @@ interface ReportInput {
quality_score: QualityScore;
issues: { errors: Issue[], warnings: Issue[], info: Issue[] };
stats: Stats;
synthesis: string; // consolidation agent 的综合分析
section_summaries: Array<{file: string, summary: string}>;
};
}
```
@@ -21,7 +30,7 @@ interface ReportInput {
## 执行流程
```javascript
// 1. 检查质量门禁
// 1. 质量门禁检查
if (consolidation.issues.errors.length > 0) {
const response = await AskUserQuestion({
questions: [{
@@ -44,259 +53,165 @@ if (consolidation.issues.errors.length > 0) {
}
}
// 2. 读取章节文件
const sectionFiles = Glob(`${outputDir}/sections/section-*.md`);
const sections = sectionFiles.map(f => Read(f));
// 2. 生成索引式报告(不读取章节内容)
const report = generateIndexReport(config, consolidation);
// 3. 读取汇总报告
const summary = Read(`${outputDir}/consolidation-summary.md`);
// 4. 装配报告
const report = assembleReport(config, sections, summary);
// 5. 写入最终文件
// 3. 写入最终文件
const fileName = `${config.type.toUpperCase()}-REPORT.md`;
Write(`${outputDir}/${fileName}`, report);
```
## 报告结构
## 报告模板
### Architecture Report
### 通用结构
```markdown
# Architecture Report
# {报告标题}
> Generated: {date}
> Scope: {config.scope}
> Quality Score: {overall}%
## Executive Summary
{3-5 key takeaways from consolidation-summary}
> 生成日期:{date}
> 分析范围:{scope}
> 分析深度:{depth}
> 质量评分:{overall}%
---
{section-overview.md 内容}
## 报告综述
{consolidation.synthesis - 来自汇总 Agent 的跨章节综合分析}
---
{section-layers.md 内容}
## 章节索引
| 章节 | 核心发现 | 详情 |
|------|----------|------|
{section_summaries 生成的表格行}
---
{section-dependencies.md 内容}
## 架构洞察
{从 consolidation 提取的跨模块关联分析}
---
{section-dataflow.md 内容}
## 建议与展望
{consolidation.recommendations - 优先级排序的综合建议}
---
{section-entrypoints.md 内容}
**附录**
---
## Recommendations
{从各章节汇总的建议}
---
## Appendix: Quality Report
{consolidation-summary.md 的质量评分和问题列表}
- [质量报告](./consolidation-summary.md)
- [章节文件目录](./sections/)
```
### Design Report
### 报告标题映射
```markdown
# Design Report
| 类型 | 标题 |
|------|------|
| architecture | 项目架构设计报告 |
| design | 项目设计模式报告 |
| methods | 项目核心方法报告 |
| comprehensive | 项目综合分析报告 |
> Generated: {date}
> Scope: {config.scope}
> Quality Score: {overall}%
## Executive Summary
{3-5 key takeaways}
---
{section-patterns.md 内容}
---
{section-classes.md 内容}
---
{section-interfaces.md 内容}
---
{section-state.md 内容}
---
## Design Recommendations
{汇总的设计建议}
---
## Appendix: Quality Report
{质量评分}
```
### Methods Report
```markdown
# Key Methods Report
> Generated: {date}
> Scope: {config.scope}
> Quality Score: {overall}%
## Executive Summary
{3-5 key takeaways}
---
{section-algorithms.md 内容}
---
{section-paths.md 内容}
---
{section-apis.md 内容}
---
{section-logic.md 内容}
---
## Optimization Suggestions
{汇总的优化建议}
---
## Appendix: Quality Report
{质量评分}
```
## 装配函数
## 生成函数
```javascript
function assembleReport(config, sections, summary) {
const reportTitles = {
architecture: "Architecture Report",
design: "Design Report",
methods: "Key Methods Report",
comprehensive: "Comprehensive Project Analysis"
function generateIndexReport(config, consolidation) {
const titles = {
architecture: "项目架构设计报告",
design: "项目设计模式报告",
methods: "项目核心方法报告",
comprehensive: "项目综合分析报告"
};
const header = `# ${reportTitles[config.type]}
const date = new Date().toLocaleDateString('zh-CN');
> Generated: ${new Date().toLocaleDateString('zh-CN')}
> Scope: ${config.scope}
> Depth: ${config.depth}
> Quality Score: ${summary.quality_score?.overall || 'N/A'}%
// 章节索引表格
const sectionTable = consolidation.section_summaries
.map(s => `| ${s.title} | ${s.summary} | [查看详情](./sections/${s.file}) |`)
.join('\n');
return `# ${titles[config.type]}
> 生成日期:${date}
> 分析范围:${config.scope}
> 分析深度:${config.depth}
> 质量评分:${consolidation.quality_score.overall}%
---
## 报告综述
${consolidation.synthesis}
---
## 章节索引
| 章节 | 核心发现 | 详情 |
|------|----------|------|
${sectionTable}
---
## 架构洞察
${consolidation.cross_analysis || '详见各章节分析。'}
---
## 建议与展望
${consolidation.recommendations || '详见质量报告中的改进建议。'}
---
**附录**
- [质量报告](./consolidation-summary.md)
- [章节文件目录](./sections/)
`;
// Executive Summary from consolidation
const execSummary = generateExecutiveSummary(summary, config.type);
// Merge sections
const mainContent = sections.join('\n\n---\n\n');
// Recommendations from sections
const recommendations = extractRecommendations(sections, config.type);
// Quality appendix
const appendix = generateQualityAppendix(summary);
return header + execSummary + '\n\n---\n\n' + mainContent + '\n\n' + recommendations + '\n\n' + appendix;
}
function generateExecutiveSummary(summary, type) {
return `## Executive Summary
### Key Findings
${summary.key_findings?.map(f => `- ${f}`).join('\n') || '- See detailed sections below'}
### Quality Overview
| Dimension | Score |
|-----------|-------|
| Completeness | ${summary.quality_score?.completeness || 'N/A'}% |
| Consistency | ${summary.quality_score?.consistency || 'N/A'}% |
| Depth | ${summary.quality_score?.depth || 'N/A'}% |
### Issues Summary
- Errors: ${summary.issues?.errors?.length || 0}
- Warnings: ${summary.issues?.warnings?.length || 0}
- Suggestions: ${summary.issues?.info?.length || 0}
`;
}
function extractRecommendations(sections, type) {
const recommendationTitles = {
architecture: "## Architectural Recommendations",
design: "## Design Recommendations",
methods: "## Optimization Suggestions",
comprehensive: "## Recommendations & Next Steps"
};
// Extract recommendation sections from each section file
let recommendations = `${recommendationTitles[type]}\n\n`;
// Aggregate from sections
recommendations += "Based on the analysis, the following recommendations are prioritized:\n\n";
recommendations += "1. **High Priority**: Address critical issues identified in the quality report\n";
recommendations += "2. **Medium Priority**: Resolve warnings to improve code quality\n";
recommendations += "3. **Low Priority**: Consider enhancement suggestions for future iterations\n";
return recommendations;
}
function generateQualityAppendix(summary) {
return `---
## Appendix: Quality Report
### Overall Score: ${summary.quality_score?.overall || 'N/A'}%
| Dimension | Score | Status |
|-----------|-------|--------|
| Completeness | ${summary.quality_score?.completeness || 'N/A'}% | ${getStatus(summary.quality_score?.completeness)} |
| Consistency | ${summary.quality_score?.consistency || 'N/A'}% | ${getStatus(summary.quality_score?.consistency)} |
| Depth | ${summary.quality_score?.depth || 'N/A'}% | ${getStatus(summary.quality_score?.depth)} |
| Readability | ${summary.quality_score?.readability || 'N/A'}% | ${getStatus(summary.quality_score?.readability)} |
### Statistics
- Total Sections: ${summary.stats?.total_sections || 'N/A'}
- Total Diagrams: ${summary.stats?.total_diagrams || 'N/A'}
- Total Code References: ${summary.stats?.total_code_refs || 'N/A'}
- Total Words: ${summary.stats?.total_words || 'N/A'}
`;
}
function getStatus(score) {
if (!score) return '?';
if (score >= 90) return 'PASS';
if (score >= 70) return 'WARNING';
return 'FAIL';
}
```
## Output
## 输出结构
- 最终报告: `{TYPE}-REPORT.md`
- 保留原始章节文件供追溯
```
.workflow/.scratchpad/analyze-{timestamp}/
├── sections/ # 独立章节Phase 3 产出)
│ ├── section-overview.md
│ ├── section-layers.md
│ └── ...
├── consolidation-summary.md # 质量报告Phase 3.5 产出)
└── {TYPE}-REPORT.md # 索引报告(本阶段产出)
```
## 与 Phase 3.5 的协作
Phase 3.5 consolidation agent 需要提供:
```typescript
interface ConsolidationOutput {
// ... 原有字段
synthesis: string; // 跨章节综合分析2-3 段落)
cross_analysis: string; // 架构级关联洞察
recommendations: string; // 优先级排序的建议
section_summaries: Array<{
file: string; // 文件名
title: string; // 章节标题
summary: string; // 一句话核心发现
}>;
}
```
## 关键变更
| 原设计 | 新设计 |
|--------|--------|
| 读取章节内容并拼接 | 链接引用,不读取内容 |
| 重新生成 Executive Summary | 直接使用 consolidation.synthesis |
| 嵌入质量评分表格 | 链接引用 consolidation-summary.md |
| 主报告包含全部内容 | 主报告仅为索引 + 综述 |

View File

@@ -0,0 +1,152 @@
# 写作风格规范
## 核心原则
**段落式描述,层层递进,禁止清单罗列。**
## 禁止的写作模式
```markdown
<!-- 禁止:清单罗列 -->
### 模块列表
- 用户模块:处理用户相关功能
- 订单模块:处理订单相关功能
- 支付模块:处理支付相关功能
### 依赖关系
| 模块 | 依赖 | 说明 |
|------|------|------|
| A | B | xxx |
```
## 推荐的写作模式
```markdown
<!-- 推荐:段落式描述 -->
### 模块架构设计
系统采用分层模块化架构,核心业务逻辑围绕用户、订单、支付三大领域展开。
用户模块作为系统的入口层,承担身份认证与权限管理职责,为下游模块提供
统一的用户上下文。订单模块位于业务核心层,依赖用户模块获取会话信息,
并协调支付模块完成交易闭环。
值得注意的是,支付模块采用策略模式实现多渠道支付,通过接口抽象与
具体支付网关解耦。这一设计使得新增支付渠道时,仅需实现相应策略类,
无需修改核心订单逻辑,体现了开闭原则的应用。
从依赖方向分析,系统呈现清晰的单向依赖:表现层依赖业务层,业务层
依赖数据层,未发现循环依赖。这一架构特征确保了模块的独立可测试性,
同时为后续微服务拆分奠定了基础。
```
## 写作策略
### 策略一:主语转换
将主语从开发者视角转移到系统/代码本身:
| 禁止 | 推荐 |
|------|------|
| 我们设计了... | 系统采用... |
| 开发者实现了... | 该模块通过... |
| 代码中使用了... | 架构设计体现了... |
### 策略二:逻辑连接
使用连接词确保段落递进:
- **承接**:此外、进一步、在此基础上
- **转折**:然而、值得注意的是、不同于
- **因果**:因此、这一设计使得、由此可见
- **总结**:综上所述、从整体来看、概言之
### 策略三:深度阐释
每个技术点需包含:
1. **是什么**:客观描述技术实现
2. **为什么**:阐释设计意图和考量
3. **影响**:说明对系统的影响和价值
```markdown
<!-- 示例 -->
系统采用依赖注入模式管理组件生命周期(是什么)。这一选择源于
对可测试性和松耦合的追求(为什么)。通过将依赖关系外置于
配置层,各模块可独立进行单元测试,同时为运行时替换实现
提供了可能(影响)。
```
## 章节模板
### 架构概述(段落式)
```markdown
## 系统架构概述
{项目名称}采用{架构模式}架构,整体设计围绕{核心理念}展开。
从宏观视角审视,系统可划分为{N}个主要层次,各层职责明确,
边界清晰。
{表现层/入口层}作为系统与外部交互的唯一入口,承担请求解析、
参数校验、响应封装等职责。该层通过{框架/技术}实现,遵循
{设计原则},确保接口的一致性与可维护性。
{业务层}是系统的核心所在,封装了全部业务逻辑。该层采用
{模式/策略}组织代码,将复杂业务拆解为{N}个领域模块。
值得注意的是,{关键设计决策}体现了对{质量属性}的重视。
{数据层}负责持久化与数据访问,通过{技术/框架}实现。
该层与业务层通过{接口/抽象}解耦,使得数据源的替换
不影响上层逻辑,体现了依赖倒置原则的应用。
```
### 设计模式分析(段落式)
```markdown
## 设计模式应用
代码库中可识别出{模式1}、{模式2}等设计模式的应用,
这些模式的选择与系统的{核心需求}密切相关。
{模式1}主要应用于{场景/模块}。具体实现位于
`{文件路径}`,通过{实现方式}达成{目标}。
这一模式的引入有效解决了{问题},使得{效果}。
在{另一场景}中,系统采用{模式2}应对{挑战}。
不同于{模式1}的{特点}{模式2}更侧重于{关注点}。
`{文件路径}`的实现可以看出,设计者通过
{具体实现}实现了{目标}。
综合来看,模式的选择体现了对{原则}的遵循,
为系统的{质量属性}提供了有力支撑。
```
### 算法流程分析(段落式)
```markdown
## 核心算法设计
{算法名称}是系统处理{业务场景}的核心逻辑,
其实现位于`{文件路径}`
从算法流程来看,整体可分为{N}个阶段。首先,
{第一阶段描述},这一步骤的目的在于{目的}。
随后,算法进入{第二阶段},通过{方法}实现{目标}。
最终,{结果处理}完成整个处理流程。
在复杂度方面,该算法的时间复杂度为{O(x)}
空间复杂度为{O(y)}。这一复杂度特征源于
{原因},在{数据规模}场景下表现良好。
值得关注的是,{算法名称}采用了{优化策略}
相较于朴素实现,{具体优化点}。这一设计决策
使得{性能提升/效果}。
```
## 质量检查清单
- [ ] 无清单罗列(禁止 `-``|` 表格作为主体内容)
- [ ] 段落完整(每段 3-5 句,逻辑闭环)
- [ ] 逻辑递进(有连接词串联)
- [ ] 客观表达(无"我们"、"开发者"等主观主语)
- [ ] 深度阐释(包含是什么/为什么/影响)
- [ ] 代码引用(关键点附文件路径)

View File

@@ -24,6 +24,39 @@ import {
import type { ProgressInfo } from './codex-lens.js';
import { getProjectRoot } from '../utils/path-validator.js';
// Timing utilities for performance analysis
const TIMING_ENABLED = process.env.SMART_SEARCH_TIMING === '1' || process.env.DEBUG?.includes('timing');
interface TimingData {
[key: string]: number;
}
function createTimer(): { mark: (name: string) => void; getTimings: () => TimingData; log: () => void } {
const startTime = performance.now();
const marks: { name: string; time: number }[] = [];
let lastMark = startTime;
return {
mark(name: string) {
const now = performance.now();
marks.push({ name, time: now - lastMark });
lastMark = now;
},
getTimings(): TimingData {
const timings: TimingData = {};
marks.forEach(m => { timings[m.name] = Math.round(m.time * 100) / 100; });
timings['_total'] = Math.round((performance.now() - startTime) * 100) / 100;
return timings;
},
log() {
if (TIMING_ENABLED) {
const timings = this.getTimings();
console.error(`[TIMING] smart-search: ${JSON.stringify(timings)}`);
}
}
};
}
// Define Zod schema for validation
const ParamsSchema = z.object({
// Action: search (content), find_files (path/name pattern), init, status
@@ -48,6 +81,9 @@ const ParamsSchema = z.object({
regex: z.boolean().default(true), // Use regex pattern matching (default: enabled)
caseSensitive: z.boolean().default(true), // Case sensitivity (default: case-sensitive)
tokenize: z.boolean().default(true), // Tokenize multi-word queries for OR matching (default: enabled)
// File type filtering
excludeExtensions: z.array(z.string()).optional().describe('File extensions to exclude from results (e.g., ["md", "txt"])'),
codeOnly: z.boolean().default(false).describe('Only return code files (excludes md, txt, json, yaml, xml, etc.)'),
// Fuzzy matching is implicit in hybrid mode (RRF fusion)
});
@@ -254,6 +290,8 @@ interface SearchMetadata {
tokenized?: boolean; // Whether tokenization was applied
// Pagination metadata
pagination?: PaginationInfo;
// Performance timing data (when SMART_SEARCH_TIMING=1 or DEBUG includes 'timing')
timing?: TimingData;
// Init action specific
action?: string;
path?: string;
@@ -1086,7 +1124,8 @@ async function executeCodexLensExactMode(params: Params): Promise<SearchResult>
* Requires index with embeddings
*/
async function executeHybridMode(params: Params): Promise<SearchResult> {
const { query, path = '.', maxResults = 5, extraFilesCount = 10, maxContentLength = 200, enrich = false } = params;
const timer = createTimer();
const { query, path = '.', maxResults = 5, extraFilesCount = 10, maxContentLength = 200, enrich = false, excludeExtensions, codeOnly = false } = params;
if (!query) {
return {
@@ -1097,6 +1136,7 @@ async function executeHybridMode(params: Params): Promise<SearchResult> {
// Check CodexLens availability
const readyStatus = await ensureCodexLensReady();
timer.mark('codexlens_ready_check');
if (!readyStatus.ready) {
return {
success: false,
@@ -1106,6 +1146,7 @@ async function executeHybridMode(params: Params): Promise<SearchResult> {
// Check index status
const indexStatus = await checkIndexStatus(path);
timer.mark('index_status_check');
// Request more results to support split (full content + extra files)
const totalToFetch = maxResults + extraFilesCount;
@@ -1114,8 +1155,10 @@ async function executeHybridMode(params: Params): Promise<SearchResult> {
args.push('--enrich');
}
const result = await executeCodexLens(args, { cwd: path });
timer.mark('codexlens_search');
if (!result.success) {
timer.log();
return {
success: false,
error: result.error,
@@ -1150,6 +1193,7 @@ async function executeHybridMode(params: Params): Promise<SearchResult> {
symbol: item.symbol || null,
};
});
timer.mark('parse_results');
initialCount = allResults.length;
@@ -1159,14 +1203,15 @@ async function executeHybridMode(params: Params): Promise<SearchResult> {
allResults = baselineResult.filteredResults;
baselineInfo = baselineResult.baselineInfo;
// 1. Filter noisy files (coverage, node_modules, etc.)
allResults = filterNoisyFiles(allResults);
// 1. Filter noisy files (coverage, node_modules, etc.) and excluded extensions
allResults = filterNoisyFiles(allResults, { excludeExtensions, codeOnly });
// 2. Boost results containing query keywords
allResults = applyKeywordBoosting(allResults, query);
// 3. Enforce score diversity (penalize identical scores)
allResults = enforceScoreDiversity(allResults);
// 4. Re-sort by adjusted scores
allResults.sort((a, b) => b.score - a.score);
timer.mark('post_processing');
} catch {
return {
success: true,
@@ -1184,6 +1229,7 @@ async function executeHybridMode(params: Params): Promise<SearchResult> {
// Split results: first N with full content, rest as file paths only
const { results, extra_files } = splitResultsWithExtraFiles(allResults, maxResults, extraFilesCount);
timer.mark('split_results');
// Build metadata with baseline info if detected
let note = 'Hybrid mode uses RRF fusion (exact + fuzzy + vector) for best results';
@@ -1191,6 +1237,10 @@ async function executeHybridMode(params: Params): Promise<SearchResult> {
note += ` | Filtered ${initialCount - allResults.length} hot-spot results with baseline score ~${baselineInfo.score.toFixed(4)}`;
}
// Log timing data
timer.log();
const timings = timer.getTimings();
return {
success: true,
results,
@@ -1203,22 +1253,82 @@ async function executeHybridMode(params: Params): Promise<SearchResult> {
note,
warning: indexStatus.warning,
suggested_weights: getRRFWeights(query),
timing: TIMING_ENABLED ? timings : undefined,
},
};
}
const RRF_WEIGHTS = {
code: { exact: 0.7, fuzzy: 0.2, vector: 0.1 },
natural: { exact: 0.4, fuzzy: 0.2, vector: 0.4 },
default: { exact: 0.5, fuzzy: 0.2, vector: 0.3 },
};
/**
* Query intent used to adapt RRF weights (Python parity).
*
* Keep this logic aligned with CodexLens Python hybrid search:
* `codex-lens/src/codexlens/search/hybrid_search.py`
*/
export type QueryIntent = 'keyword' | 'semantic' | 'mixed';
function getRRFWeights(query: string): Record<string, number> {
const isCode = looksLikeCodeQuery(query);
const isNatural = detectNaturalLanguage(query);
if (isCode) return RRF_WEIGHTS.code;
if (isNatural) return RRF_WEIGHTS.natural;
return RRF_WEIGHTS.default;
// Python default: vector 60%, exact 30%, fuzzy 10%
const DEFAULT_RRF_WEIGHTS = {
exact: 0.3,
fuzzy: 0.1,
vector: 0.6,
} as const;
function normalizeWeights(weights: Record<string, number>): Record<string, number> {
const sum = Object.values(weights).reduce((acc, v) => acc + v, 0);
if (!Number.isFinite(sum) || sum <= 0) return { ...weights };
return Object.fromEntries(Object.entries(weights).map(([k, v]) => [k, v / sum]));
}
/**
* Detect query intent using the same heuristic signals as Python:
* - Code patterns: `.`, `::`, `->`, CamelCase, snake_case, common code keywords
* - Natural language patterns: >5 words, question marks, interrogatives, common verbs
*/
export function detectQueryIntent(query: string): QueryIntent {
const trimmed = query.trim();
if (!trimmed) return 'mixed';
const lower = trimmed.toLowerCase();
const wordCount = trimmed.split(/\s+/).filter(Boolean).length;
const hasCodeSignals =
/(::|->|\.)/.test(trimmed) ||
/[A-Z][a-z]+[A-Z]/.test(trimmed) ||
/\b\w+_\w+\b/.test(trimmed) ||
/\b(def|class|function|const|let|var|import|from|return|async|await|interface|type)\b/i.test(lower);
const hasNaturalSignals =
wordCount > 5 ||
/\?/.test(trimmed) ||
/\b(how|what|why|when|where)\b/i.test(trimmed) ||
/\b(handle|explain|fix|implement|create|build|use|find|search|convert|parse|generate|support)\b/i.test(trimmed);
if (hasCodeSignals && hasNaturalSignals) return 'mixed';
if (hasCodeSignals) return 'keyword';
if (hasNaturalSignals) return 'semantic';
return 'mixed';
}
/**
* Intent → weights mapping (Python parity).
* - keyword: exact-heavy
* - semantic: vector-heavy
* - mixed: keep defaults
*/
export function adjustWeightsByIntent(
intent: QueryIntent,
baseWeights: Record<string, number>,
): Record<string, number> {
if (intent === 'keyword') return normalizeWeights({ exact: 0.5, fuzzy: 0.1, vector: 0.4 });
if (intent === 'semantic') return normalizeWeights({ exact: 0.2, fuzzy: 0.1, vector: 0.7 });
return normalizeWeights({ ...baseWeights });
}
export function getRRFWeights(
query: string,
baseWeights: Record<string, number> = DEFAULT_RRF_WEIGHTS,
): Record<string, number> {
return adjustWeightsByIntent(detectQueryIntent(query), baseWeights);
}
/**
@@ -1231,7 +1341,29 @@ const FILE_EXCLUDE_REGEXES = [...FILTER_CONFIG.exclude_files].map(pattern =>
new RegExp('^' + pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\\\*/g, '.*') + '$')
);
function filterNoisyFiles(results: SemanticMatch[]): SemanticMatch[] {
// Non-code file extensions (for codeOnly filter)
const NON_CODE_EXTENSIONS = new Set([
'md', 'txt', 'json', 'yaml', 'yml', 'xml', 'csv', 'log',
'ini', 'cfg', 'conf', 'toml', 'env', 'properties',
'html', 'htm', 'svg', 'png', 'jpg', 'jpeg', 'gif', 'ico', 'webp',
'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
'lock', 'sum', 'mod',
]);
interface FilterOptions {
excludeExtensions?: string[];
codeOnly?: boolean;
}
function filterNoisyFiles(results: SemanticMatch[], options: FilterOptions = {}): SemanticMatch[] {
const { excludeExtensions = [], codeOnly = false } = options;
// Build extension filter set
const excludedExtSet = new Set(excludeExtensions.map(ext => ext.toLowerCase().replace(/^\./, '')));
if (codeOnly) {
NON_CODE_EXTENSIONS.forEach(ext => excludedExtSet.add(ext));
}
return results.filter(r => {
const filePath = r.file || '';
if (!filePath) return true;
@@ -1249,6 +1381,14 @@ function filterNoisyFiles(results: SemanticMatch[]): SemanticMatch[] {
return false;
}
// Extension filter check
if (excludedExtSet.size > 0) {
const ext = filename.split('.').pop()?.toLowerCase() || '';
if (excludedExtSet.has(ext)) {
return false;
}
}
return true;
});
}
@@ -1396,10 +1536,11 @@ function filterDominantBaselineScores(
*/
function applyRRFFusion(
resultsMap: Map<string, any[]>,
weights: Record<string, number>,
weightsOrQuery: Record<string, number> | string,
limit: number,
k: number = 60,
): any[] {
const weights = typeof weightsOrQuery === 'string' ? getRRFWeights(weightsOrQuery) : weightsOrQuery;
const pathScores = new Map<string, { score: number; result: any; sources: string[] }>();
resultsMap.forEach((results, source) => {

View File

@@ -147,9 +147,9 @@ export { initApp, processData, Application };
assert.ok('success' in result, 'Result should have success property');
if (result.success) {
// Check that .codexlens directory was created
const codexlensDir = join(testDir, '.codexlens');
assert.ok(existsSync(codexlensDir), '.codexlens directory should exist');
// CodexLens stores indexes in the global data directory (e.g. ~/.codexlens/indexes)
// rather than creating a per-project ".codexlens" folder.
assert.ok(true);
}
});

View File

@@ -16,8 +16,8 @@ import assert from 'node:assert';
import { createServer } from 'http';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs';
import { homedir } from 'os';
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs';
import { homedir, tmpdir } from 'os';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -382,36 +382,53 @@ describe('CodexLens Error Handling', async () => {
assert.ok(typeof result === 'object', 'Result should be an object');
});
it('should handle missing files parameter for update action', async () => {
it('should support update action without files parameter', async () => {
if (!codexLensModule) {
console.log('Skipping: codex-lens module not available');
return;
}
const result = await codexLensModule.codexLensTool.execute({
action: 'update'
// files is missing
});
assert.ok(typeof result === 'object', 'Result should be an object');
assert.strictEqual(result.success, false, 'Should return success: false');
assert.ok(result.error, 'Should have error message');
assert.ok(result.error.includes('files'), 'Error should mention files parameter');
});
it('should handle empty files array for update action', async () => {
if (!codexLensModule) {
console.log('Skipping: codex-lens module not available');
const checkResult = await codexLensModule.checkVenvStatus();
if (!checkResult.ready) {
console.log('Skipping: CodexLens not installed');
return;
}
const updateRoot = mkdtempSync(join(tmpdir(), 'ccw-codexlens-update-'));
writeFileSync(join(updateRoot, 'main.py'), 'def hello():\n return 1\n', 'utf8');
const result = await codexLensModule.codexLensTool.execute({
action: 'update',
path: updateRoot,
});
assert.ok(typeof result === 'object', 'Result should be an object');
assert.ok('success' in result, 'Result should have success property');
});
it('should ignore extraneous files parameter for update action', async () => {
if (!codexLensModule) {
console.log('Skipping: codex-lens module not available');
return;
}
const checkResult = await codexLensModule.checkVenvStatus();
if (!checkResult.ready) {
console.log('Skipping: CodexLens not installed');
return;
}
const updateRoot = mkdtempSync(join(tmpdir(), 'ccw-codexlens-update-'));
writeFileSync(join(updateRoot, 'main.py'), 'def hello():\n return 1\n', 'utf8');
const result = await codexLensModule.codexLensTool.execute({
action: 'update',
path: updateRoot,
files: []
});
assert.ok(typeof result === 'object', 'Result should be an object');
assert.strictEqual(result.success, false, 'Should return success: false');
assert.ok('success' in result, 'Result should have success property');
});
});

View File

@@ -77,7 +77,7 @@ describe('MCP Server', () => {
const toolNames = response.result.tools.map(t => t.name);
assert(toolNames.includes('edit_file'));
assert(toolNames.includes('write_file'));
assert(toolNames.includes('codex_lens'));
assert(toolNames.includes('smart_search'));
});
it('should respond to tools/call request', async () => {

View File

@@ -0,0 +1,122 @@
/**
* Tests for query intent detection + adaptive RRF weights (TypeScript/Python parity).
*
* References:
* - `ccw/src/tools/smart-search.ts` (detectQueryIntent, adjustWeightsByIntent, getRRFWeights)
* - `codex-lens/src/codexlens/search/hybrid_search.py` (weight intent concept + defaults)
*/
import { describe, it, before } from 'node:test';
import assert from 'node:assert';
const smartSearchPath = new URL('../dist/tools/smart-search.js', import.meta.url).href;
describe('Smart Search - Query Intent + RRF Weights', async () => {
/** @type {any} */
let smartSearchModule;
before(async () => {
try {
smartSearchModule = await import(smartSearchPath);
} catch (err) {
// Keep tests non-blocking for environments that haven't built `ccw/dist` yet.
console.log('Note: smart-search module import skipped:', err.message);
}
});
describe('detectQueryIntent', () => {
it('classifies "def authenticate" as keyword', () => {
if (!smartSearchModule) return;
assert.strictEqual(smartSearchModule.detectQueryIntent('def authenticate'), 'keyword');
});
it('classifies CamelCase identifiers as keyword', () => {
if (!smartSearchModule) return;
assert.strictEqual(smartSearchModule.detectQueryIntent('MyClass'), 'keyword');
});
it('classifies snake_case identifiers as keyword', () => {
if (!smartSearchModule) return;
assert.strictEqual(smartSearchModule.detectQueryIntent('user_id'), 'keyword');
});
it('classifies namespace separators "::" as keyword', () => {
if (!smartSearchModule) return;
assert.strictEqual(smartSearchModule.detectQueryIntent('UserService::authenticate'), 'keyword');
});
it('classifies pointer arrows "->" as keyword', () => {
if (!smartSearchModule) return;
assert.strictEqual(smartSearchModule.detectQueryIntent('ptr->next'), 'keyword');
});
it('classifies dotted member access as keyword', () => {
if (!smartSearchModule) return;
assert.strictEqual(smartSearchModule.detectQueryIntent('foo.bar'), 'keyword');
});
it('classifies natural language questions as semantic', () => {
if (!smartSearchModule) return;
assert.strictEqual(smartSearchModule.detectQueryIntent('how to handle user login'), 'semantic');
});
it('classifies interrogatives with question marks as semantic', () => {
if (!smartSearchModule) return;
assert.strictEqual(smartSearchModule.detectQueryIntent('what is authentication?'), 'semantic');
});
it('classifies queries with both code + NL signals as mixed', () => {
if (!smartSearchModule) return;
assert.strictEqual(smartSearchModule.detectQueryIntent('why does FooBar crash?'), 'mixed');
});
it('classifies long NL queries containing identifiers as mixed', () => {
if (!smartSearchModule) return;
assert.strictEqual(smartSearchModule.detectQueryIntent('how to use user_id in query'), 'mixed');
});
});
describe('adjustWeightsByIntent', () => {
it('maps keyword intent to exact-heavy weights', () => {
if (!smartSearchModule) return;
const weights = smartSearchModule.adjustWeightsByIntent('keyword', { exact: 0.3, fuzzy: 0.1, vector: 0.6 });
assert.deepStrictEqual(weights, { exact: 0.5, fuzzy: 0.1, vector: 0.4 });
});
});
describe('getRRFWeights parity set', () => {
it('produces stable weights for 20 representative queries', () => {
if (!smartSearchModule) return;
const base = { exact: 0.3, fuzzy: 0.1, vector: 0.6 };
const expected = [
['def authenticate', { exact: 0.5, fuzzy: 0.1, vector: 0.4 }],
['class UserService', { exact: 0.5, fuzzy: 0.1, vector: 0.4 }],
['user_id', { exact: 0.5, fuzzy: 0.1, vector: 0.4 }],
['MyClass', { exact: 0.5, fuzzy: 0.1, vector: 0.4 }],
['Foo::Bar', { exact: 0.5, fuzzy: 0.1, vector: 0.4 }],
['ptr->next', { exact: 0.5, fuzzy: 0.1, vector: 0.4 }],
['foo.bar', { exact: 0.5, fuzzy: 0.1, vector: 0.4 }],
['import os', { exact: 0.5, fuzzy: 0.1, vector: 0.4 }],
['how to handle user login', { exact: 0.2, fuzzy: 0.1, vector: 0.7 }],
['what is the best way to search?', { exact: 0.2, fuzzy: 0.1, vector: 0.7 }],
['explain the authentication flow', { exact: 0.2, fuzzy: 0.1, vector: 0.7 }],
['generate embeddings for this repo', { exact: 0.2, fuzzy: 0.1, vector: 0.7 }],
['how does FooBar work', base],
['user_id how to handle', base],
['Find UserService::authenticate method', base],
['where is foo.bar used', base],
['parse_json function', { exact: 0.5, fuzzy: 0.1, vector: 0.4 }],
['How to parse_json output?', base],
['', base],
['authentication', base],
];
for (const [query, expectedWeights] of expected) {
const actual = smartSearchModule.getRRFWeights(query, base);
assert.deepStrictEqual(actual, expectedWeights, `unexpected weights for query: ${JSON.stringify(query)}`);
}
});
});
});

View File

@@ -0,0 +1,71 @@
/**
* TypeScript parity tests for query intent detection + adaptive RRF weights.
*
* Notes:
* - These tests target the runtime implementation shipped in `ccw/dist`.
* - Keep logic aligned with Python: `codex-lens/src/codexlens/search/ranking.py`.
*/
import { before, describe, it } from 'node:test';
import assert from 'node:assert';
const smartSearchPath = new URL('../dist/tools/smart-search.js', import.meta.url).href;
describe('Smart Search (TS) - Query Intent + RRF Weights', async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let smartSearchModule: any;
before(async () => {
try {
smartSearchModule = await import(smartSearchPath);
} catch (err: any) {
// Keep tests non-blocking for environments that haven't built `ccw/dist` yet.
console.log('Note: smart-search module import skipped:', err?.message ?? String(err));
}
});
describe('detectQueryIntent parity (10 cases)', () => {
const cases: Array<[string, 'keyword' | 'semantic' | 'mixed']> = [
['def authenticate', 'keyword'],
['MyClass', 'keyword'],
['user_id', 'keyword'],
['UserService::authenticate', 'keyword'],
['ptr->next', 'keyword'],
['how to handle user login', 'semantic'],
['what is authentication?', 'semantic'],
['where is this used?', 'semantic'],
['why does FooBar crash?', 'mixed'],
['how to use user_id in query', 'mixed'],
];
for (const [query, expected] of cases) {
it(`classifies ${JSON.stringify(query)} as ${expected}`, () => {
if (!smartSearchModule) return;
assert.strictEqual(smartSearchModule.detectQueryIntent(query), expected);
});
}
});
describe('adaptive weights (Python parity thresholds)', () => {
it('uses exact-heavy weights for code-like queries (exact > 0.4)', () => {
if (!smartSearchModule) return;
const weights = smartSearchModule.getRRFWeights('def authenticate', {
exact: 0.3,
fuzzy: 0.1,
vector: 0.6,
});
assert.ok(weights.exact > 0.4);
});
it('uses vector-heavy weights for NL queries (vector > 0.6)', () => {
if (!smartSearchModule) return;
const weights = smartSearchModule.getRRFWeights('how to handle user login', {
exact: 0.3,
fuzzy: 0.1,
vector: 0.6,
});
assert.ok(weights.vector > 0.6);
});
});
});

Binary file not shown.

41
codex-lens/CHANGELOG.md Normal file
View File

@@ -0,0 +1,41 @@
# CodexLens Optimization Plan Changelog
This changelog tracks the **CodexLens optimization plan** milestones (not the Python package version in `pyproject.toml`).
## v1.0 (Optimization) 2025-12-26
### Optimizations
1. **P0: Context-aware hybrid chunking**
- Docstrings are extracted into dedicated chunks and excluded from code chunks.
- Docstring chunks include `parent_symbol` metadata when the docstring belongs to a function/class/method.
- Sliding-window chunk boundaries are deterministic for identical input.
2. **P1: Adaptive RRF weights (QueryIntent)**
- Query intent is classified as `keyword` / `semantic` / `mixed`.
- RRF weights adapt to intent:
- `keyword`: exact-heavy (favors lexical matches)
- `semantic`: vector-heavy (favors semantic matches)
- `mixed`: keeps base/default weights
3. **P2: Symbol boost**
- Fused results with an explicit symbol match (`symbol_name`) receive a multiplicative boost (default `1.5x`).
4. **P2: Embedding-based re-ranking (optional)**
- A second-stage ranker can reorder top results by semantic similarity.
- Re-ranking runs only when `Config.enable_reranking=True`.
5. **P3: Global symbol index (incremental + fast path)**
- `GlobalSymbolIndex` stores project-wide symbols in one SQLite DB for fast symbol lookups.
- `ChainSearchEngine.search_symbols()` uses the global index fast path when enabled.
### Migration Notes
- **Reindexing (recommended)**: deterministic chunking and docstring metadata affect stored chunks. For best results, regenerate indexes/embeddings after upgrading:
- Rebuild indexes and/or re-run embedding generation for existing projects.
- **New config flags**:
- `Config.enable_reranking` (default `False`)
- `Config.reranking_top_k` (default `50`)
- `Config.symbol_boost_factor` (default `1.5`)
- `Config.global_symbol_index_enabled` (default `True`)
- **Breaking changes**: none (behavioral improvements only).

View File

@@ -103,6 +103,11 @@ class Config:
# Indexing/search optimizations
global_symbol_index_enabled: bool = True # Enable project-wide symbol index fast path
# Optional search reranking (disabled by default)
enable_reranking: bool = False
reranking_top_k: int = 50
symbol_boost_factor: float = 1.5
# Multi-endpoint configuration for litellm backend
embedding_endpoints: List[Dict[str, Any]] = field(default_factory=list)
# List of endpoint configs: [{"model": "...", "api_key": "...", "api_base": "...", "weight": 1.0}]

View File

@@ -7,12 +7,38 @@ results via Reciprocal Rank Fusion (RRF) algorithm.
from __future__ import annotations
import logging
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from contextlib import contextmanager
from pathlib import Path
from typing import Dict, List, Optional
from typing import Any, Dict, List, Optional
@contextmanager
def timer(name: str, logger: logging.Logger, level: int = logging.DEBUG):
"""Context manager for timing code blocks.
Args:
name: Name of the operation being timed
logger: Logger instance to use
level: Logging level (default DEBUG)
"""
start = time.perf_counter()
try:
yield
finally:
elapsed_ms = (time.perf_counter() - start) * 1000
logger.log(level, "[TIMING] %s: %.2fms", name, elapsed_ms)
from codexlens.config import Config
from codexlens.entities import SearchResult
from codexlens.search.ranking import reciprocal_rank_fusion, tag_search_source
from codexlens.search.ranking import (
apply_symbol_boost,
get_rrf_weights,
reciprocal_rank_fusion,
rerank_results,
tag_search_source,
)
from codexlens.storage.dir_index import DirIndexStore
@@ -34,14 +60,23 @@ class HybridSearchEngine:
"vector": 0.6,
}
def __init__(self, weights: Optional[Dict[str, float]] = None):
def __init__(
self,
weights: Optional[Dict[str, float]] = None,
config: Optional[Config] = None,
embedder: Any = None,
):
"""Initialize hybrid search engine.
Args:
weights: Optional custom RRF weights (default: DEFAULT_WEIGHTS)
config: Optional runtime config (enables optional reranking features)
embedder: Optional embedder instance for embedding-based reranking
"""
self.logger = logging.getLogger(__name__)
self.weights = weights or self.DEFAULT_WEIGHTS.copy()
self._config = config
self.embedder = embedder
def search(
self,
@@ -101,7 +136,8 @@ class HybridSearchEngine:
backends["vector"] = True
# Execute parallel searches
results_map = self._search_parallel(index_path, query, backends, limit)
with timer("parallel_search_total", self.logger):
results_map = self._search_parallel(index_path, query, backends, limit)
# Provide helpful message if pure-vector mode returns no results
if pure_vector and enable_vector and len(results_map.get("vector", [])) == 0:
@@ -120,11 +156,72 @@ class HybridSearchEngine:
if source in results_map
}
fused_results = reciprocal_rank_fusion(results_map, active_weights)
with timer("rrf_fusion", self.logger):
adaptive_weights = get_rrf_weights(query, active_weights)
fused_results = reciprocal_rank_fusion(results_map, adaptive_weights)
# Optional: boost results that include explicit symbol matches
boost_factor = (
self._config.symbol_boost_factor
if self._config is not None
else 1.5
)
with timer("symbol_boost", self.logger):
fused_results = apply_symbol_boost(
fused_results, boost_factor=boost_factor
)
# Optional: embedding-based reranking on top results
if self._config is not None and self._config.enable_reranking:
with timer("reranking", self.logger):
if self.embedder is None:
self.embedder = self._get_reranking_embedder()
fused_results = rerank_results(
query,
fused_results[:100],
self.embedder,
top_k=self._config.reranking_top_k,
)
# Apply final limit
return fused_results[:limit]
def _get_reranking_embedder(self) -> Any:
"""Create an embedder for reranking based on Config embedding settings."""
if self._config is None:
return None
try:
from codexlens.semantic.factory import get_embedder
except Exception as exc:
self.logger.debug("Reranking embedder unavailable: %s", exc)
return None
try:
if self._config.embedding_backend == "fastembed":
return get_embedder(
backend="fastembed",
profile=self._config.embedding_model,
use_gpu=self._config.embedding_use_gpu,
)
if self._config.embedding_backend == "litellm":
return get_embedder(
backend="litellm",
model=self._config.embedding_model,
endpoints=self._config.embedding_endpoints,
strategy=self._config.embedding_strategy,
cooldown=self._config.embedding_cooldown,
)
except Exception as exc:
self.logger.debug("Failed to initialize reranking embedder: %s", exc)
return None
self.logger.debug(
"Unknown embedding backend for reranking: %s",
self._config.embedding_backend,
)
return None
def _search_parallel(
self,
index_path: Path,
@@ -144,25 +241,30 @@ class HybridSearchEngine:
Dictionary mapping source name to results list
"""
results_map: Dict[str, List[SearchResult]] = {}
timing_data: Dict[str, float] = {}
# Use ThreadPoolExecutor for parallel I/O-bound searches
with ThreadPoolExecutor(max_workers=len(backends)) as executor:
# Submit search tasks
# Submit search tasks with timing
future_to_source = {}
submit_times = {}
if backends.get("exact"):
submit_times["exact"] = time.perf_counter()
future = executor.submit(
self._search_exact, index_path, query, limit
)
future_to_source[future] = "exact"
if backends.get("fuzzy"):
submit_times["fuzzy"] = time.perf_counter()
future = executor.submit(
self._search_fuzzy, index_path, query, limit
)
future_to_source[future] = "fuzzy"
if backends.get("vector"):
submit_times["vector"] = time.perf_counter()
future = executor.submit(
self._search_vector, index_path, query, limit
)
@@ -171,18 +273,26 @@ class HybridSearchEngine:
# Collect results as they complete
for future in as_completed(future_to_source):
source = future_to_source[future]
elapsed_ms = (time.perf_counter() - submit_times[source]) * 1000
timing_data[source] = elapsed_ms
try:
results = future.result()
# Tag results with source for debugging
tagged_results = tag_search_source(results, source)
results_map[source] = tagged_results
self.logger.debug(
"Got %d results from %s search", len(results), source
"[TIMING] %s_search: %.2fms (%d results)",
source, elapsed_ms, len(results)
)
except Exception as exc:
self.logger.error("Search failed for %s: %s", source, exc)
results_map[source] = []
# Log timing summary
if timing_data:
timing_str = ", ".join(f"{k}={v:.1f}ms" for k, v in timing_data.items())
self.logger.debug("[TIMING] search_backends: {%s}", timing_str)
return results_map
def _search_exact(
@@ -245,6 +355,8 @@ class HybridSearchEngine:
try:
# Check if semantic chunks table exists
import sqlite3
start_check = time.perf_counter()
try:
with sqlite3.connect(index_path) as conn:
cursor = conn.execute(
@@ -254,6 +366,10 @@ class HybridSearchEngine:
except sqlite3.Error as e:
self.logger.error("Database check failed in vector search: %s", e)
return []
self.logger.debug(
"[TIMING] vector_table_check: %.2fms",
(time.perf_counter() - start_check) * 1000
)
if not has_semantic_table:
self.logger.info(
@@ -267,7 +383,12 @@ class HybridSearchEngine:
from codexlens.semantic.factory import get_embedder
from codexlens.semantic.vector_store import VectorStore
start_init = time.perf_counter()
vector_store = VectorStore(index_path)
self.logger.debug(
"[TIMING] vector_store_init: %.2fms",
(time.perf_counter() - start_init) * 1000
)
# Check if vector store has data
if vector_store.count_chunks() == 0:
@@ -279,6 +400,7 @@ class HybridSearchEngine:
return []
# Get stored model configuration (preferred) or auto-detect from dimension
start_embedder = time.perf_counter()
model_config = vector_store.get_model_config()
if model_config:
backend = model_config.get("backend", "fastembed")
@@ -324,21 +446,32 @@ class HybridSearchEngine:
detected_dim
)
embedder = get_embedder(backend="fastembed", profile="code")
self.logger.debug(
"[TIMING] embedder_init: %.2fms",
(time.perf_counter() - start_embedder) * 1000
)
# Generate query embedding
start_embed = time.perf_counter()
query_embedding = embedder.embed_single(query)
self.logger.debug(
"[TIMING] query_embedding: %.2fms",
(time.perf_counter() - start_embed) * 1000
)
# Search for similar chunks
start_search = time.perf_counter()
results = vector_store.search_similar(
query_embedding=query_embedding,
top_k=limit,
min_score=0.0, # Return all results, let RRF handle filtering
return_full_content=True,
)
self.logger.debug(
"[TIMING] vector_similarity_search: %.2fms (%d results)",
(time.perf_counter() - start_search) * 1000, len(results)
)
self.logger.debug("Vector search found %d results", len(results))
return results
except ImportError as exc:

View File

@@ -6,12 +6,98 @@ for combining results from heterogeneous search backends (exact FTS, fuzzy FTS,
from __future__ import annotations
import re
import math
from typing import Dict, List
from enum import Enum
from typing import Any, Dict, List
from codexlens.entities import SearchResult, AdditionalLocation
class QueryIntent(str, Enum):
"""Query intent for adaptive RRF weights (Python/TypeScript parity)."""
KEYWORD = "keyword"
SEMANTIC = "semantic"
MIXED = "mixed"
def normalize_weights(weights: Dict[str, float]) -> Dict[str, float]:
"""Normalize weights to sum to 1.0 (best-effort)."""
total = sum(float(v) for v in weights.values() if v is not None)
if not math.isfinite(total) or total <= 0:
return {k: float(v) for k, v in weights.items()}
return {k: float(v) / total for k, v in weights.items()}
def detect_query_intent(query: str) -> QueryIntent:
"""Detect whether a query is code-like, natural-language, or mixed.
Heuristic signals kept aligned with `ccw/src/tools/smart-search.ts`.
"""
trimmed = (query or "").strip()
if not trimmed:
return QueryIntent.MIXED
lower = trimmed.lower()
word_count = len([w for w in re.split(r"\s+", trimmed) if w])
has_code_signals = bool(
re.search(r"(::|->|\.)", trimmed)
or re.search(r"[A-Z][a-z]+[A-Z]", trimmed)
or re.search(r"\b\w+_\w+\b", trimmed)
or re.search(
r"\b(def|class|function|const|let|var|import|from|return|async|await|interface|type)\b",
lower,
flags=re.IGNORECASE,
)
)
has_natural_signals = bool(
word_count > 5
or "?" in trimmed
or re.search(r"\b(how|what|why|when|where)\b", trimmed, flags=re.IGNORECASE)
or re.search(
r"\b(handle|explain|fix|implement|create|build|use|find|search|convert|parse|generate|support)\b",
trimmed,
flags=re.IGNORECASE,
)
)
if has_code_signals and has_natural_signals:
return QueryIntent.MIXED
if has_code_signals:
return QueryIntent.KEYWORD
if has_natural_signals:
return QueryIntent.SEMANTIC
return QueryIntent.MIXED
def adjust_weights_by_intent(
intent: QueryIntent,
base_weights: Dict[str, float],
) -> Dict[str, float]:
"""Map intent → weights (kept aligned with TypeScript mapping)."""
if intent == QueryIntent.KEYWORD:
target = {"exact": 0.5, "fuzzy": 0.1, "vector": 0.4}
elif intent == QueryIntent.SEMANTIC:
target = {"exact": 0.2, "fuzzy": 0.1, "vector": 0.7}
else:
target = dict(base_weights)
# Preserve only keys that are present in base_weights (active backends).
keys = list(base_weights.keys())
filtered = {k: float(target.get(k, 0.0)) for k in keys}
return normalize_weights(filtered)
def get_rrf_weights(
query: str,
base_weights: Dict[str, float],
) -> Dict[str, float]:
"""Compute adaptive RRF weights from query intent."""
return adjust_weights_by_intent(detect_query_intent(query), base_weights)
def reciprocal_rank_fusion(
results_map: Dict[str, List[SearchResult]],
weights: Dict[str, float] = None,
@@ -102,6 +188,186 @@ def reciprocal_rank_fusion(
return fused_results
def apply_symbol_boost(
results: List[SearchResult],
boost_factor: float = 1.5,
) -> List[SearchResult]:
"""Boost fused scores for results that include an explicit symbol match.
The boost is multiplicative on the current result.score (typically the RRF fusion score).
When boosted, the original score is preserved in metadata["original_fusion_score"] and
metadata["boosted"] is set to True.
"""
if not results:
return []
if boost_factor <= 1.0:
# Still return new objects to follow immutable transformation pattern.
return [
SearchResult(
path=r.path,
score=r.score,
excerpt=r.excerpt,
content=r.content,
symbol=r.symbol,
chunk=r.chunk,
metadata={**r.metadata},
start_line=r.start_line,
end_line=r.end_line,
symbol_name=r.symbol_name,
symbol_kind=r.symbol_kind,
additional_locations=list(r.additional_locations),
)
for r in results
]
boosted_results: List[SearchResult] = []
for result in results:
has_symbol = bool(result.symbol_name)
original_score = float(result.score)
boosted_score = original_score * boost_factor if has_symbol else original_score
metadata = {**result.metadata}
if has_symbol:
metadata.setdefault("original_fusion_score", metadata.get("fusion_score", original_score))
metadata["boosted"] = True
metadata["symbol_boost_factor"] = boost_factor
boosted_results.append(
SearchResult(
path=result.path,
score=boosted_score,
excerpt=result.excerpt,
content=result.content,
symbol=result.symbol,
chunk=result.chunk,
metadata=metadata,
start_line=result.start_line,
end_line=result.end_line,
symbol_name=result.symbol_name,
symbol_kind=result.symbol_kind,
additional_locations=list(result.additional_locations),
)
)
boosted_results.sort(key=lambda r: r.score, reverse=True)
return boosted_results
def rerank_results(
query: str,
results: List[SearchResult],
embedder: Any,
top_k: int = 50,
) -> List[SearchResult]:
"""Re-rank results with embedding cosine similarity, combined with current score.
Combined score formula:
0.5 * rrf_score + 0.5 * cosine_similarity
If embedder is None or embedding fails, returns results as-is.
"""
if not results:
return []
if embedder is None or top_k <= 0:
return results
rerank_count = min(int(top_k), len(results))
def cosine_similarity(vec_a: List[float], vec_b: List[float]) -> float:
# Defensive: handle mismatched lengths and zero vectors.
n = min(len(vec_a), len(vec_b))
if n == 0:
return 0.0
dot = 0.0
norm_a = 0.0
norm_b = 0.0
for i in range(n):
a = float(vec_a[i])
b = float(vec_b[i])
dot += a * b
norm_a += a * a
norm_b += b * b
if norm_a <= 0.0 or norm_b <= 0.0:
return 0.0
sim = dot / (math.sqrt(norm_a) * math.sqrt(norm_b))
# SearchResult.score requires non-negative scores; clamp cosine similarity to [0, 1].
return max(0.0, min(1.0, sim))
def text_for_embedding(r: SearchResult) -> str:
if r.excerpt and r.excerpt.strip():
return r.excerpt
if r.content and r.content.strip():
return r.content
if r.chunk and r.chunk.content and r.chunk.content.strip():
return r.chunk.content
# Fallback: stable, non-empty text.
return r.symbol_name or r.path
try:
if hasattr(embedder, "embed_single"):
query_vec = embedder.embed_single(query)
else:
query_vec = embedder.embed(query)[0]
doc_texts = [text_for_embedding(r) for r in results[:rerank_count]]
doc_vecs = embedder.embed(doc_texts)
except Exception:
return results
reranked_results: List[SearchResult] = []
for idx, result in enumerate(results):
if idx < rerank_count:
rrf_score = float(result.score)
sim = cosine_similarity(query_vec, doc_vecs[idx])
combined_score = 0.5 * rrf_score + 0.5 * sim
reranked_results.append(
SearchResult(
path=result.path,
score=combined_score,
excerpt=result.excerpt,
content=result.content,
symbol=result.symbol,
chunk=result.chunk,
metadata={
**result.metadata,
"rrf_score": rrf_score,
"cosine_similarity": sim,
"reranked": True,
},
start_line=result.start_line,
end_line=result.end_line,
symbol_name=result.symbol_name,
symbol_kind=result.symbol_kind,
additional_locations=list(result.additional_locations),
)
)
else:
# Preserve remaining results without re-ranking, but keep immutability.
reranked_results.append(
SearchResult(
path=result.path,
score=result.score,
excerpt=result.excerpt,
content=result.content,
symbol=result.symbol,
chunk=result.chunk,
metadata={**result.metadata},
start_line=result.start_line,
end_line=result.end_line,
symbol_name=result.symbol_name,
symbol_kind=result.symbol_kind,
additional_locations=list(result.additional_locations),
)
)
reranked_results.sort(key=lambda r: r.score, reverse=True)
return reranked_results
def normalize_bm25_score(score: float) -> float:
"""Normalize BM25 scores from SQLite FTS5 to 0-1 range.

View File

@@ -392,6 +392,22 @@ class HybridChunker:
filtered.append(symbol)
return filtered
def _find_parent_symbol(
self,
start_line: int,
end_line: int,
symbols: List[Symbol],
) -> Optional[Symbol]:
"""Find the smallest symbol range that fully contains a docstring span."""
candidates: List[Symbol] = []
for symbol in symbols:
sym_start, sym_end = symbol.range
if sym_start <= start_line and end_line <= sym_end:
candidates.append(symbol)
if not candidates:
return None
return min(candidates, key=lambda s: (s.range[1] - s.range[0], s.range[0]))
def chunk_file(
self,
content: str,
@@ -414,24 +430,53 @@ class HybridChunker:
chunks: List[SemanticChunk] = []
# Step 1: Extract docstrings as dedicated chunks
docstrings = self.docstring_extractor.extract_docstrings(content, language)
docstrings: List[Tuple[str, int, int]] = []
if language == "python":
# Fast path: avoid expensive docstring extraction if delimiters are absent.
if '"""' in content or "'''" in content:
docstrings = self.docstring_extractor.extract_docstrings(content, language)
elif language in {"javascript", "typescript"}:
if "/**" in content:
docstrings = self.docstring_extractor.extract_docstrings(content, language)
else:
docstrings = self.docstring_extractor.extract_docstrings(content, language)
# Fast path: no docstrings -> delegate to base chunker directly.
if not docstrings:
if symbols:
base_chunks = self.base_chunker.chunk_by_symbol(
content, symbols, file_path, language, symbol_token_counts
)
else:
base_chunks = self.base_chunker.chunk_sliding_window(content, file_path, language)
for chunk in base_chunks:
chunk.metadata["strategy"] = "hybrid"
chunk.metadata["chunk_type"] = "code"
return base_chunks
for docstring_content, start_line, end_line in docstrings:
if len(docstring_content.strip()) >= self.config.min_chunk_size:
parent_symbol = self._find_parent_symbol(start_line, end_line, symbols)
# Use base chunker's token estimation method
token_count = self.base_chunker._estimate_token_count(docstring_content)
metadata = {
"file": str(file_path),
"language": language,
"chunk_type": "docstring",
"start_line": start_line,
"end_line": end_line,
"strategy": "hybrid",
"token_count": token_count,
}
if parent_symbol is not None:
metadata["parent_symbol"] = parent_symbol.name
metadata["parent_symbol_kind"] = parent_symbol.kind
metadata["parent_symbol_range"] = parent_symbol.range
chunks.append(SemanticChunk(
content=docstring_content,
embedding=None,
metadata={
"file": str(file_path),
"language": language,
"chunk_type": "docstring",
"start_line": start_line,
"end_line": end_line,
"strategy": "hybrid",
"token_count": token_count,
}
metadata=metadata
))
# Step 2: Get line ranges occupied by docstrings

View File

@@ -0,0 +1,293 @@
import sqlite3
import tempfile
import time
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from unittest.mock import MagicMock
import pytest
from codexlens.config import Config
from codexlens.entities import Symbol
from codexlens.errors import StorageError
from codexlens.search.chain_search import ChainSearchEngine
from codexlens.storage.global_index import GlobalSymbolIndex
from codexlens.storage.path_mapper import PathMapper
from codexlens.storage.registry import RegistryStore
@pytest.fixture()
def temp_paths():
tmpdir = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)
root = Path(tmpdir.name)
yield root
try:
tmpdir.cleanup()
except (PermissionError, OSError):
pass
def test_add_symbol(temp_paths: Path):
db_path = temp_paths / "indexes" / "_global_symbols.db"
index_path = temp_paths / "indexes" / "_index.db"
file_path = temp_paths / "src" / "a.py"
index_path.parent.mkdir(parents=True, exist_ok=True)
index_path.write_text("", encoding="utf-8")
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text("class AuthManager:\n pass\n", encoding="utf-8")
with GlobalSymbolIndex(db_path, project_id=1) as store:
store.add_symbol(
Symbol(name="AuthManager", kind="class", range=(1, 2)),
file_path=file_path,
index_path=index_path,
)
matches = store.search("AuthManager", kind="class", limit=10, prefix_mode=True)
assert len(matches) == 1
assert matches[0].name == "AuthManager"
assert matches[0].file == str(file_path.resolve())
# Schema version safety: newer schema versions should be rejected.
bad_db = temp_paths / "indexes" / "_global_symbols_bad.db"
bad_db.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(bad_db)
conn.execute("PRAGMA user_version = 999")
conn.close()
with pytest.raises(StorageError):
GlobalSymbolIndex(bad_db, project_id=1).initialize()
def test_search_symbols(temp_paths: Path):
db_path = temp_paths / "indexes" / "_global_symbols.db"
index_path = temp_paths / "indexes" / "_index.db"
file_path = temp_paths / "src" / "mod.py"
index_path.parent.mkdir(parents=True, exist_ok=True)
index_path.write_text("", encoding="utf-8")
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text("def authenticate():\n pass\n", encoding="utf-8")
with GlobalSymbolIndex(db_path, project_id=7) as store:
store.add_symbol(
Symbol(name="authenticate", kind="function", range=(1, 2)),
file_path=file_path,
index_path=index_path,
)
locations = store.search_symbols("auth", kind="function", limit=10, prefix_mode=True)
assert locations
assert any(p.endswith("mod.py") for p, _ in locations)
assert any(rng == (1, 2) for _, rng in locations)
def test_update_file_symbols(temp_paths: Path):
db_path = temp_paths / "indexes" / "_global_symbols.db"
file_path = temp_paths / "src" / "mod.py"
index_path = temp_paths / "indexes" / "_index.db"
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text("def a():\n pass\n", encoding="utf-8")
index_path.parent.mkdir(parents=True, exist_ok=True)
index_path.write_text("", encoding="utf-8")
with GlobalSymbolIndex(db_path, project_id=7) as store:
store.update_file_symbols(
file_path=file_path,
symbols=[
Symbol(name="old_func", kind="function", range=(1, 2)),
Symbol(name="Other", kind="class", range=(10, 20)),
],
index_path=index_path,
)
assert any(s.name == "old_func" for s in store.search("old_", prefix_mode=True))
store.update_file_symbols(
file_path=file_path,
symbols=[Symbol(name="new_func", kind="function", range=(3, 4))],
index_path=index_path,
)
assert not any(s.name == "old_func" for s in store.search("old_", prefix_mode=True))
assert any(s.name == "new_func" for s in store.search("new_", prefix_mode=True))
# Backward-compatible path: index_path can be omitted after it's been established.
store.update_file_symbols(
file_path=file_path,
symbols=[Symbol(name="new_func2", kind="function", range=(5, 6))],
index_path=None,
)
assert any(s.name == "new_func2" for s in store.search("new_func2", prefix_mode=True))
# New file + symbols without index_path should raise.
missing_index_file = temp_paths / "src" / "new_file.py"
with pytest.raises(StorageError):
store.update_file_symbols(
file_path=missing_index_file,
symbols=[Symbol(name="must_fail", kind="function", range=(1, 1))],
index_path=None,
)
deleted = store.delete_file_symbols(file_path)
assert deleted > 0
def test_incremental_updates(temp_paths: Path, monkeypatch):
db_path = temp_paths / "indexes" / "_global_symbols.db"
file_path = temp_paths / "src" / "same.py"
idx_a = temp_paths / "indexes" / "a" / "_index.db"
idx_b = temp_paths / "indexes" / "b" / "_index.db"
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text("class AuthManager:\n pass\n", encoding="utf-8")
idx_a.parent.mkdir(parents=True, exist_ok=True)
idx_a.write_text("", encoding="utf-8")
idx_b.parent.mkdir(parents=True, exist_ok=True)
idx_b.write_text("", encoding="utf-8")
with GlobalSymbolIndex(db_path, project_id=42) as store:
sym = Symbol(name="AuthManager", kind="class", range=(1, 2))
store.add_symbol(sym, file_path=file_path, index_path=idx_a)
store.add_symbol(sym, file_path=file_path, index_path=idx_b)
# prefix_mode=False exercises substring matching.
assert store.search("Manager", prefix_mode=False)
conn = sqlite3.connect(db_path)
row = conn.execute(
"""
SELECT index_path
FROM global_symbols
WHERE project_id=? AND symbol_name=? AND symbol_kind=? AND file_path=?
""",
(42, "AuthManager", "class", str(file_path.resolve())),
).fetchone()
conn.close()
assert row is not None
assert str(Path(row[0]).resolve()) == str(idx_b.resolve())
# Migration path coverage: simulate a future schema version and an older DB version.
migrating_db = temp_paths / "indexes" / "_global_symbols_migrate.db"
migrating_db.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(migrating_db)
conn.execute("PRAGMA user_version = 1")
conn.close()
monkeypatch.setattr(GlobalSymbolIndex, "SCHEMA_VERSION", 2)
GlobalSymbolIndex(migrating_db, project_id=1).initialize()
def test_concurrent_access(temp_paths: Path):
db_path = temp_paths / "indexes" / "_global_symbols.db"
index_path = temp_paths / "indexes" / "_index.db"
file_path = temp_paths / "src" / "a.py"
index_path.parent.mkdir(parents=True, exist_ok=True)
index_path.write_text("", encoding="utf-8")
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text("class A:\n pass\n", encoding="utf-8")
with GlobalSymbolIndex(db_path, project_id=1) as store:
def add_many(worker_id: int):
for i in range(50):
store.add_symbol(
Symbol(name=f"Sym{worker_id}_{i}", kind="class", range=(1, 2)),
file_path=file_path,
index_path=index_path,
)
with ThreadPoolExecutor(max_workers=8) as ex:
list(ex.map(add_many, range(8)))
matches = store.search("Sym", kind="class", limit=1000, prefix_mode=True)
assert len(matches) >= 200
def test_chain_search_integration(temp_paths: Path):
project_root = temp_paths / "project"
project_root.mkdir(parents=True, exist_ok=True)
index_root = temp_paths / "indexes"
mapper = PathMapper(index_root=index_root)
index_db_path = mapper.source_to_index_db(project_root)
index_db_path.parent.mkdir(parents=True, exist_ok=True)
index_db_path.write_text("", encoding="utf-8")
registry = RegistryStore(db_path=temp_paths / "registry.db")
registry.initialize()
project_info = registry.register_project(project_root, mapper.source_to_index_dir(project_root))
global_db_path = project_info.index_root / GlobalSymbolIndex.DEFAULT_DB_NAME
with GlobalSymbolIndex(global_db_path, project_id=project_info.id) as global_index:
file_path = project_root / "auth.py"
global_index.update_file_symbols(
file_path=file_path,
symbols=[
Symbol(name="AuthManager", kind="class", range=(1, 10)),
Symbol(name="authenticate", kind="function", range=(12, 20)),
],
index_path=index_db_path,
)
config = Config(data_dir=temp_paths / "data", global_symbol_index_enabled=True)
engine = ChainSearchEngine(registry, mapper, config=config)
engine._search_symbols_parallel = MagicMock(side_effect=AssertionError("should not traverse chain"))
symbols = engine.search_symbols("Auth", project_root)
assert any(s.name == "AuthManager" for s in symbols)
registry.close()
def test_disabled_fallback(temp_paths: Path):
project_root = temp_paths / "project"
project_root.mkdir(parents=True, exist_ok=True)
index_root = temp_paths / "indexes"
mapper = PathMapper(index_root=index_root)
index_db_path = mapper.source_to_index_db(project_root)
index_db_path.parent.mkdir(parents=True, exist_ok=True)
index_db_path.write_text("", encoding="utf-8")
registry = RegistryStore(db_path=temp_paths / "registry.db")
registry.initialize()
registry.register_project(project_root, mapper.source_to_index_dir(project_root))
config = Config(data_dir=temp_paths / "data", global_symbol_index_enabled=False)
engine = ChainSearchEngine(registry, mapper, config=config)
engine._collect_index_paths = MagicMock(return_value=[index_db_path])
engine._search_symbols_parallel = MagicMock(
return_value=[Symbol(name="FallbackSymbol", kind="function", range=(1, 2))]
)
symbols = engine.search_symbols("Fallback", project_root)
assert any(s.name == "FallbackSymbol" for s in symbols)
assert engine._search_symbols_parallel.called
registry.close()
def test_performance_benchmark(temp_paths: Path):
db_path = temp_paths / "indexes" / "_global_symbols.db"
index_path = temp_paths / "indexes" / "_index.db"
file_path = temp_paths / "src" / "perf.py"
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text("class AuthManager:\n pass\n", encoding="utf-8")
index_path.parent.mkdir(parents=True, exist_ok=True)
index_path.write_text("", encoding="utf-8")
with GlobalSymbolIndex(db_path, project_id=1) as store:
for i in range(500):
store.add_symbol(
Symbol(name=f"AuthManager{i}", kind="class", range=(1, 2)),
file_path=file_path,
index_path=index_path,
)
start = time.perf_counter()
results = store.search("AuthManager", kind="class", limit=50, prefix_mode=True)
elapsed_ms = (time.perf_counter() - start) * 1000
assert elapsed_ms < 100.0
assert results

View File

@@ -551,3 +551,72 @@ class UserProfile:
# Verify <15% overhead (reasonable threshold for performance tests with system variance)
assert overhead < 15.0, f"Overhead {overhead:.2f}% exceeds 15% threshold (base={base_time:.4f}s, hybrid={hybrid_time:.4f}s)"
class TestHybridChunkerV1Optimizations:
"""Tests for v1.0 optimization behaviors (parent metadata + determinism)."""
def test_merged_docstring_metadata(self):
"""Docstring chunks include parent_symbol metadata when applicable."""
config = ChunkConfig(min_chunk_size=1)
chunker = HybridChunker(config=config)
content = '''"""Module docstring."""
def hello():
"""Function docstring."""
return 1
'''
symbols = [Symbol(name="hello", kind="function", range=(3, 5))]
chunks = chunker.chunk_file(content, symbols, "m.py", "python")
func_doc_chunks = [
c for c in chunks
if c.metadata.get("chunk_type") == "docstring" and c.metadata.get("start_line") == 4
]
assert len(func_doc_chunks) == 1
assert func_doc_chunks[0].metadata.get("parent_symbol") == "hello"
assert func_doc_chunks[0].metadata.get("parent_symbol_kind") == "function"
def test_deterministic_chunk_boundaries(self):
"""Chunk boundaries are stable across repeated runs on identical input."""
config = ChunkConfig(max_chunk_size=80, overlap=10, min_chunk_size=1)
chunker = HybridChunker(config=config)
# No docstrings, no symbols -> sliding window path.
content = "\n".join([f"line {i}: x = {i}" for i in range(1, 200)]) + "\n"
boundaries = []
for _ in range(3):
chunks = chunker.chunk_file(content, [], "deterministic.py", "python")
boundaries.append([
(c.metadata.get("start_line"), c.metadata.get("end_line"))
for c in chunks
if c.metadata.get("chunk_type") == "code"
])
assert boundaries[0] == boundaries[1] == boundaries[2]
def test_orphan_docstrings(self):
"""Module-level docstrings remain standalone (no parent_symbol assigned)."""
config = ChunkConfig(min_chunk_size=1)
chunker = HybridChunker(config=config)
content = '''"""Module-level docstring."""
def hello():
"""Function docstring."""
return 1
'''
symbols = [Symbol(name="hello", kind="function", range=(3, 5))]
chunks = chunker.chunk_file(content, symbols, "orphan.py", "python")
module_doc = [
c for c in chunks
if c.metadata.get("chunk_type") == "docstring" and c.metadata.get("start_line") == 1
]
assert len(module_doc) == 1
assert module_doc[0].metadata.get("parent_symbol") is None
code_chunks = [c for c in chunks if c.metadata.get("chunk_type") == "code"]
assert code_chunks, "Expected at least one code chunk"
assert all("Module-level docstring" not in c.content for c in code_chunks)

View File

@@ -10,6 +10,7 @@ from pathlib import Path
import pytest
from codexlens.config import Config
from codexlens.entities import SearchResult
from codexlens.search.hybrid_search import HybridSearchEngine
from codexlens.storage.dir_index import DirIndexStore
@@ -774,3 +775,97 @@ class TestHybridSearchWithVectorMock:
assert hasattr(result, 'score')
assert result.score > 0 # RRF fusion scores are positive
class TestHybridSearchAdaptiveWeights:
"""Integration tests for adaptive RRF weights + reranking gating."""
def test_adaptive_weights_code_query(self):
"""Exact weight should dominate for code-like queries."""
from unittest.mock import patch
engine = HybridSearchEngine()
results_map = {
"exact": [SearchResult(path="a.py", score=10.0, excerpt="a")],
"fuzzy": [SearchResult(path="b.py", score=9.0, excerpt="b")],
"vector": [SearchResult(path="c.py", score=0.9, excerpt="c")],
}
captured = {}
from codexlens.search import ranking as ranking_module
def capture_rrf(map_in, weights_in, k=60):
captured["weights"] = dict(weights_in)
return ranking_module.reciprocal_rank_fusion(map_in, weights_in, k=k)
with patch.object(HybridSearchEngine, "_search_parallel", return_value=results_map), patch(
"codexlens.search.hybrid_search.reciprocal_rank_fusion",
side_effect=capture_rrf,
):
engine.search(Path("dummy.db"), "def authenticate", enable_vector=True)
assert captured["weights"]["exact"] > 0.4
def test_adaptive_weights_nl_query(self):
"""Vector weight should dominate for natural-language queries."""
from unittest.mock import patch
engine = HybridSearchEngine()
results_map = {
"exact": [SearchResult(path="a.py", score=10.0, excerpt="a")],
"fuzzy": [SearchResult(path="b.py", score=9.0, excerpt="b")],
"vector": [SearchResult(path="c.py", score=0.9, excerpt="c")],
}
captured = {}
from codexlens.search import ranking as ranking_module
def capture_rrf(map_in, weights_in, k=60):
captured["weights"] = dict(weights_in)
return ranking_module.reciprocal_rank_fusion(map_in, weights_in, k=k)
with patch.object(HybridSearchEngine, "_search_parallel", return_value=results_map), patch(
"codexlens.search.hybrid_search.reciprocal_rank_fusion",
side_effect=capture_rrf,
):
engine.search(Path("dummy.db"), "how to handle user login", enable_vector=True)
assert captured["weights"]["vector"] > 0.6
def test_reranking_enabled(self, tmp_path):
"""Reranking runs only when explicitly enabled via config."""
from unittest.mock import patch
results_map = {
"exact": [SearchResult(path="a.py", score=10.0, excerpt="a")],
"fuzzy": [SearchResult(path="b.py", score=9.0, excerpt="b")],
"vector": [SearchResult(path="c.py", score=0.9, excerpt="c")],
}
class DummyEmbedder:
def embed(self, texts):
if isinstance(texts, str):
texts = [texts]
return [[1.0, 0.0] for _ in texts]
# Disabled: should not invoke rerank_results
config_off = Config(data_dir=tmp_path / "off", enable_reranking=False)
engine_off = HybridSearchEngine(config=config_off, embedder=DummyEmbedder())
with patch.object(HybridSearchEngine, "_search_parallel", return_value=results_map), patch(
"codexlens.search.hybrid_search.rerank_results"
) as rerank_mock:
engine_off.search(Path("dummy.db"), "query", enable_vector=True)
rerank_mock.assert_not_called()
# Enabled: should invoke rerank_results once
config_on = Config(data_dir=tmp_path / "on", enable_reranking=True, reranking_top_k=10)
engine_on = HybridSearchEngine(config=config_on, embedder=DummyEmbedder())
with patch.object(HybridSearchEngine, "_search_parallel", return_value=results_map), patch(
"codexlens.search.hybrid_search.rerank_results",
side_effect=lambda q, r, e, top_k=50: r,
) as rerank_mock:
engine_on.search(Path("dummy.db"), "query", enable_vector=True)
assert rerank_mock.call_count == 1

View File

@@ -3,6 +3,7 @@
import pytest
import sqlite3
import tempfile
import time
from pathlib import Path
from codexlens.search.hybrid_search import HybridSearchEngine
@@ -16,6 +17,22 @@ except ImportError:
SEMANTIC_DEPS_AVAILABLE = False
def _safe_unlink(path: Path, retries: int = 5, delay_s: float = 0.05) -> None:
"""Best-effort unlink for Windows where SQLite can keep files locked briefly."""
for attempt in range(retries):
try:
path.unlink()
return
except FileNotFoundError:
return
except PermissionError:
time.sleep(delay_s * (attempt + 1))
try:
path.unlink(missing_ok=True)
except (PermissionError, OSError):
pass
class TestPureVectorSearch:
"""Tests for pure vector search mode."""
@@ -48,7 +65,7 @@ class TestPureVectorSearch:
store.close()
if db_path.exists():
db_path.unlink()
_safe_unlink(db_path)
def test_pure_vector_without_embeddings(self, sample_db):
"""Test pure_vector mode returns empty when no embeddings exist."""
@@ -200,12 +217,8 @@ def login_handler(credentials: dict) -> bool:
yield db_path
store.close()
# Ignore file deletion errors on Windows (SQLite file lock)
try:
if db_path.exists():
db_path.unlink()
except PermissionError:
pass # Ignore Windows file lock errors
if db_path.exists():
_safe_unlink(db_path)
def test_pure_vector_with_embeddings(self, db_with_embeddings):
"""Test pure vector search returns results when embeddings exist."""
@@ -289,7 +302,7 @@ class TestSearchModeComparison:
store.close()
if db_path.exists():
db_path.unlink()
_safe_unlink(db_path)
def test_mode_comparison_without_embeddings(self, comparison_db):
"""Compare all search modes without embeddings."""

View File

@@ -7,8 +7,12 @@ import pytest
from codexlens.entities import SearchResult
from codexlens.search.ranking import (
apply_symbol_boost,
QueryIntent,
detect_query_intent,
normalize_bm25_score,
reciprocal_rank_fusion,
rerank_results,
tag_search_source,
)
@@ -342,6 +346,62 @@ class TestTagSearchSource:
assert tagged[0].symbol_kind == "function"
class TestSymbolBoost:
"""Tests for apply_symbol_boost function."""
def test_symbol_boost(self):
results = [
SearchResult(path="a.py", score=0.2, excerpt="...", symbol_name="foo"),
SearchResult(path="b.py", score=0.21, excerpt="..."),
]
boosted = apply_symbol_boost(results, boost_factor=1.5)
assert boosted[0].path == "a.py"
assert boosted[0].score == pytest.approx(0.2 * 1.5)
assert boosted[0].metadata["boosted"] is True
assert boosted[0].metadata["original_fusion_score"] == pytest.approx(0.2)
assert boosted[1].path == "b.py"
assert boosted[1].score == pytest.approx(0.21)
assert "boosted" not in boosted[1].metadata
class TestEmbeddingReranking:
"""Tests for rerank_results embedding-based similarity."""
def test_rerank_embedding_similarity(self):
class DummyEmbedder:
def embed(self, texts):
if isinstance(texts, str):
texts = [texts]
mapping = {
"query": [1.0, 0.0],
"doc1": [1.0, 0.0],
"doc2": [0.0, 1.0],
}
return [mapping[t] for t in texts]
results = [
SearchResult(path="a.py", score=0.2, excerpt="doc1"),
SearchResult(path="b.py", score=0.9, excerpt="doc2"),
]
reranked = rerank_results("query", results, DummyEmbedder(), top_k=2)
assert reranked[0].path == "a.py"
assert reranked[0].metadata["reranked"] is True
assert reranked[0].metadata["rrf_score"] == pytest.approx(0.2)
assert reranked[0].metadata["cosine_similarity"] == pytest.approx(1.0)
assert reranked[0].score == pytest.approx(0.5 * 0.2 + 0.5 * 1.0)
assert reranked[1].path == "b.py"
assert reranked[1].metadata["reranked"] is True
assert reranked[1].metadata["rrf_score"] == pytest.approx(0.9)
assert reranked[1].metadata["cosine_similarity"] == pytest.approx(0.0)
assert reranked[1].score == pytest.approx(0.5 * 0.9 + 0.5 * 0.0)
@pytest.mark.parametrize("k_value", [30, 60, 100])
class TestRRFParameterized:
"""Parameterized tests for RRF with different k values."""
@@ -419,3 +479,41 @@ class TestRRFEdgeCases:
# Should work with normalization
assert len(fused) == 1 # Deduplicated
assert fused[0].score > 0
class TestSymbolBoostAndIntentV1:
"""Tests for symbol boosting and query intent detection (v1.0)."""
def test_symbol_boost_application(self):
"""Results with symbol_name receive a multiplicative boost (default 1.5x)."""
results = [
SearchResult(path="a.py", score=0.4, excerpt="...", symbol_name="AuthManager"),
SearchResult(path="b.py", score=0.41, excerpt="..."),
]
boosted = apply_symbol_boost(results, boost_factor=1.5)
assert boosted[0].score == pytest.approx(0.4 * 1.5)
assert boosted[0].metadata["boosted"] is True
assert boosted[0].metadata["original_fusion_score"] == pytest.approx(0.4)
assert boosted[1].score == pytest.approx(0.41)
assert "boosted" not in boosted[1].metadata
@pytest.mark.parametrize(
("query", "expected"),
[
("def authenticate", QueryIntent.KEYWORD),
("MyClass", QueryIntent.KEYWORD),
("user_id", QueryIntent.KEYWORD),
("UserService::authenticate", QueryIntent.KEYWORD),
("ptr->next", QueryIntent.KEYWORD),
("how to handle user login", QueryIntent.SEMANTIC),
("what is authentication?", QueryIntent.SEMANTIC),
("where is this used?", QueryIntent.SEMANTIC),
("why does FooBar crash?", QueryIntent.MIXED),
("how to use user_id in query", QueryIntent.MIXED),
],
)
def test_query_intent_detection(self, query, expected):
"""Detect intent for representative queries (Python/TypeScript parity)."""
assert detect_query_intent(query) == expected

View File

@@ -466,7 +466,18 @@ class TestDiagnostics:
yield db_path
if db_path.exists():
db_path.unlink()
for attempt in range(5):
try:
db_path.unlink()
break
except PermissionError:
time.sleep(0.05 * (attempt + 1))
else:
# Best-effort cleanup (Windows SQLite locks can linger briefly).
try:
db_path.unlink(missing_ok=True)
except (PermissionError, OSError):
pass
def test_diagnose_empty_database(self, empty_db):
"""Diagnose behavior with empty database."""

View File

@@ -13,7 +13,7 @@ class TestChunkConfig:
"""Test default configuration values."""
config = ChunkConfig()
assert config.max_chunk_size == 1000
assert config.overlap == 100
assert config.overlap == 200
assert config.min_chunk_size == 50
def test_custom_config(self):

View File

@@ -11,7 +11,7 @@
"scripts": {
"build": "tsc -p ccw/tsconfig.json",
"start": "node ccw/bin/ccw.js",
"test": "node --test",
"test": "node --test ccw/tests/*.test.js",
"prepublishOnly": "npm run build && echo 'Ready to publish @dyw/claude-code-workflow'"
},
"keywords": [