dev-playbooks-cn 1.4.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/devbooks.js
CHANGED
|
@@ -24,7 +24,7 @@ import path from 'path';
|
|
|
24
24
|
import os from 'os';
|
|
25
25
|
import { fileURLToPath } from 'url';
|
|
26
26
|
import { spawn } from 'child_process';
|
|
27
|
-
import { checkbox, confirm } from '@inquirer/prompts';
|
|
27
|
+
import { checkbox, confirm, select } from '@inquirer/prompts';
|
|
28
28
|
import chalk from 'chalk';
|
|
29
29
|
import ora from 'ora';
|
|
30
30
|
|
|
@@ -45,6 +45,15 @@ const SKILLS_SUPPORT = {
|
|
|
45
45
|
BASIC: 'basic' // 仅基础指令(无独立 Skills 概念)
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
+
// ============================================================================
|
|
49
|
+
// Skills 安装范围
|
|
50
|
+
// ============================================================================
|
|
51
|
+
|
|
52
|
+
const INSTALL_SCOPE = {
|
|
53
|
+
GLOBAL: 'global', // 全局安装(~/.claude/skills 等)
|
|
54
|
+
PROJECT: 'project' // 项目级安装(.claude/skills 等)
|
|
55
|
+
};
|
|
56
|
+
|
|
48
57
|
// ============================================================================
|
|
49
58
|
// AI 工具配置
|
|
50
59
|
// ============================================================================
|
|
@@ -540,12 +549,68 @@ async function promptToolSelection(projectDir) {
|
|
|
540
549
|
return selectedTools;
|
|
541
550
|
}
|
|
542
551
|
|
|
552
|
+
async function promptInstallScope(projectDir, selectedTools) {
|
|
553
|
+
// 检查是否有需要安装 Skills 的工具
|
|
554
|
+
const fullSupportTools = selectedTools.filter(id => {
|
|
555
|
+
const tool = AI_TOOLS.find(t => t.id === id);
|
|
556
|
+
return tool && tool.skillsSupport === SKILLS_SUPPORT.FULL;
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
if (fullSupportTools.length === 0) {
|
|
560
|
+
return INSTALL_SCOPE.PROJECT; // 没有完整 Skills 支持的工具,默认项目级
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// 读取已保存的配置
|
|
564
|
+
const config = loadConfig(projectDir);
|
|
565
|
+
const savedScope = config.installScope;
|
|
566
|
+
|
|
567
|
+
console.log();
|
|
568
|
+
console.log(chalk.bold('📦 Skills 安装位置'));
|
|
569
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
570
|
+
console.log();
|
|
571
|
+
|
|
572
|
+
const scope = await select({
|
|
573
|
+
message: 'Skills 安装到哪里?',
|
|
574
|
+
choices: [
|
|
575
|
+
{
|
|
576
|
+
name: `项目级 ${chalk.gray('(.claude/skills 等,仅当前项目可用)')}`,
|
|
577
|
+
value: INSTALL_SCOPE.PROJECT,
|
|
578
|
+
description: '推荐:Skills 随项目走,不影响其他项目'
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
name: `全局 ${chalk.gray('(~/.claude/skills 等,所有项目共享)')}`,
|
|
582
|
+
value: INSTALL_SCOPE.GLOBAL,
|
|
583
|
+
description: '所有项目共享同一套 Skills'
|
|
584
|
+
}
|
|
585
|
+
],
|
|
586
|
+
default: savedScope || INSTALL_SCOPE.PROJECT
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
return scope;
|
|
590
|
+
}
|
|
591
|
+
|
|
543
592
|
|
|
544
593
|
// ============================================================================
|
|
545
594
|
// 安装 Skills(Claude Code, Codex CLI, Qoder)
|
|
546
595
|
// ============================================================================
|
|
547
596
|
|
|
548
|
-
function
|
|
597
|
+
function getSkillsDestDir(tool, scope, projectDir) {
|
|
598
|
+
// 根据安装范围确定目标目录
|
|
599
|
+
if (scope === INSTALL_SCOPE.PROJECT) {
|
|
600
|
+
// 项目级安装:使用项目目录下的相对路径
|
|
601
|
+
if (tool.id === 'claude') {
|
|
602
|
+
return path.join(projectDir, '.claude', 'skills');
|
|
603
|
+
} else if (tool.id === 'codex') {
|
|
604
|
+
return path.join(projectDir, '.codex', 'skills');
|
|
605
|
+
} else if (tool.id === 'opencode') {
|
|
606
|
+
return path.join(projectDir, '.opencode', 'skill');
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
// 全局安装:使用工具定义的全局目录
|
|
610
|
+
return tool.skillsDir;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function installSkills(toolIds, projectDir, scope = INSTALL_SCOPE.GLOBAL, update = false) {
|
|
549
614
|
const results = [];
|
|
550
615
|
|
|
551
616
|
for (const toolId of toolIds) {
|
|
@@ -555,7 +620,7 @@ function installSkills(toolIds, update = false) {
|
|
|
555
620
|
// Claude Code / Codex CLI / OpenCode(含 oh-my-opencode)支持相同格式的 Skills
|
|
556
621
|
if ((toolId === 'claude' || toolId === 'codex' || toolId === 'opencode') && tool.skillsDir) {
|
|
557
622
|
const skillsSrcDir = path.join(__dirname, '..', 'skills');
|
|
558
|
-
const skillsDestDir = tool
|
|
623
|
+
const skillsDestDir = getSkillsDestDir(tool, scope, projectDir);
|
|
559
624
|
|
|
560
625
|
if (!fs.existsSync(skillsSrcDir)) continue;
|
|
561
626
|
|
|
@@ -601,7 +666,9 @@ function installSkills(toolIds, update = false) {
|
|
|
601
666
|
type: 'skills',
|
|
602
667
|
count: installedCount,
|
|
603
668
|
total: skillDirs.length,
|
|
604
|
-
removed: removedCount
|
|
669
|
+
removed: removedCount,
|
|
670
|
+
scope: scope,
|
|
671
|
+
path: skillsDestDir
|
|
605
672
|
});
|
|
606
673
|
}
|
|
607
674
|
|
|
@@ -1013,7 +1080,7 @@ function createProjectStructure(projectDir) {
|
|
|
1013
1080
|
// 保存配置
|
|
1014
1081
|
// ============================================================================
|
|
1015
1082
|
|
|
1016
|
-
function saveConfig(toolIds, projectDir) {
|
|
1083
|
+
function saveConfig(toolIds, projectDir, installScope = INSTALL_SCOPE.PROJECT) {
|
|
1017
1084
|
const configPath = path.join(projectDir, '.devbooks', 'config.yaml');
|
|
1018
1085
|
|
|
1019
1086
|
// 读取现有配置或创建新配置
|
|
@@ -1033,6 +1100,17 @@ function saveConfig(toolIds, projectDir) {
|
|
|
1033
1100
|
configContent = configContent.trimEnd() + '\n\n' + toolsYaml + '\n';
|
|
1034
1101
|
}
|
|
1035
1102
|
|
|
1103
|
+
// 更新 install_scope 部分
|
|
1104
|
+
const scopeYaml = `install_scope: ${installScope}`;
|
|
1105
|
+
|
|
1106
|
+
if (configContent.includes('install_scope:')) {
|
|
1107
|
+
// 替换现有的 install_scope 部分
|
|
1108
|
+
configContent = configContent.replace(/install_scope:.*/, scopeYaml);
|
|
1109
|
+
} else {
|
|
1110
|
+
// 追加 install_scope 部分
|
|
1111
|
+
configContent = configContent.trimEnd() + '\n\n' + scopeYaml + '\n';
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1036
1114
|
fs.writeFileSync(configPath, configContent);
|
|
1037
1115
|
}
|
|
1038
1116
|
|
|
@@ -1040,23 +1118,26 @@ function loadConfig(projectDir) {
|
|
|
1040
1118
|
const configPath = path.join(projectDir, '.devbooks', 'config.yaml');
|
|
1041
1119
|
|
|
1042
1120
|
if (!fs.existsSync(configPath)) {
|
|
1043
|
-
return { aiTools: [] };
|
|
1121
|
+
return { aiTools: [], installScope: null };
|
|
1044
1122
|
}
|
|
1045
1123
|
|
|
1046
1124
|
const content = fs.readFileSync(configPath, 'utf-8');
|
|
1047
|
-
const match = content.match(/ai_tools:\s*([\s\S]*?)(?=\n\w|\n$|$)/);
|
|
1048
|
-
|
|
1049
|
-
if (!match) {
|
|
1050
|
-
return { aiTools: [] };
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
const tools = match[1]
|
|
1054
|
-
.split('\n')
|
|
1055
|
-
.map(line => line.trim())
|
|
1056
|
-
.filter(line => line.startsWith('-'))
|
|
1057
|
-
.map(line => line.replace(/^-\s*/, '').trim());
|
|
1058
1125
|
|
|
1059
|
-
|
|
1126
|
+
// 解析 ai_tools
|
|
1127
|
+
const toolsMatch = content.match(/ai_tools:\s*([\s\S]*?)(?=\n\w|\n$|$)/);
|
|
1128
|
+
const tools = toolsMatch
|
|
1129
|
+
? toolsMatch[1]
|
|
1130
|
+
.split('\n')
|
|
1131
|
+
.map(line => line.trim())
|
|
1132
|
+
.filter(line => line.startsWith('-'))
|
|
1133
|
+
.map(line => line.replace(/^-\s*/, '').trim())
|
|
1134
|
+
: [];
|
|
1135
|
+
|
|
1136
|
+
// 解析 install_scope
|
|
1137
|
+
const scopeMatch = content.match(/install_scope:\s*(\w+)/);
|
|
1138
|
+
const installScope = scopeMatch ? scopeMatch[1] : null;
|
|
1139
|
+
|
|
1140
|
+
return { aiTools: tools, installScope };
|
|
1060
1141
|
}
|
|
1061
1142
|
|
|
1062
1143
|
// ============================================================================
|
|
@@ -1072,6 +1153,7 @@ async function initCommand(projectDir, options) {
|
|
|
1072
1153
|
|
|
1073
1154
|
// 确定选择的工具
|
|
1074
1155
|
let selectedTools;
|
|
1156
|
+
let installScope = INSTALL_SCOPE.PROJECT; // 默认项目级安装
|
|
1075
1157
|
|
|
1076
1158
|
if (options.tools) {
|
|
1077
1159
|
if (options.tools === 'all') {
|
|
@@ -1084,8 +1166,16 @@ async function initCommand(projectDir, options) {
|
|
|
1084
1166
|
);
|
|
1085
1167
|
}
|
|
1086
1168
|
console.log(chalk.blue('ℹ') + ` 非交互式模式:${selectedTools.length > 0 ? selectedTools.join(', ') : '无'}`);
|
|
1169
|
+
|
|
1170
|
+
// 非交互式模式下,检查 --scope 选项
|
|
1171
|
+
if (options.scope) {
|
|
1172
|
+
installScope = options.scope === 'global' ? INSTALL_SCOPE.GLOBAL : INSTALL_SCOPE.PROJECT;
|
|
1173
|
+
}
|
|
1087
1174
|
} else {
|
|
1088
1175
|
selectedTools = await promptToolSelection(projectDir);
|
|
1176
|
+
|
|
1177
|
+
// 交互式选择安装范围
|
|
1178
|
+
installScope = await promptInstallScope(projectDir, selectedTools);
|
|
1089
1179
|
}
|
|
1090
1180
|
|
|
1091
1181
|
// 创建项目结构
|
|
@@ -1093,8 +1183,8 @@ async function initCommand(projectDir, options) {
|
|
|
1093
1183
|
const templateCount = createProjectStructure(projectDir);
|
|
1094
1184
|
spinner.succeed(`创建了 ${templateCount} 个模板文件`);
|
|
1095
1185
|
|
|
1096
|
-
//
|
|
1097
|
-
saveConfig(selectedTools, projectDir);
|
|
1186
|
+
// 保存配置(包含安装范围)
|
|
1187
|
+
saveConfig(selectedTools, projectDir, installScope);
|
|
1098
1188
|
|
|
1099
1189
|
if (selectedTools.length === 0) {
|
|
1100
1190
|
console.log();
|
|
@@ -1111,12 +1201,16 @@ async function initCommand(projectDir, options) {
|
|
|
1111
1201
|
|
|
1112
1202
|
if (fullSupportTools.length > 0) {
|
|
1113
1203
|
const skillsSpinner = ora('安装 Skills...').start();
|
|
1114
|
-
const skillsResults = installSkills(fullSupportTools);
|
|
1204
|
+
const skillsResults = installSkills(fullSupportTools, projectDir, installScope);
|
|
1115
1205
|
skillsSpinner.succeed('Skills 安装完成');
|
|
1116
1206
|
|
|
1117
1207
|
for (const result of skillsResults) {
|
|
1118
1208
|
if (result.count > 0) {
|
|
1119
|
-
|
|
1209
|
+
const scopeLabel = result.scope === INSTALL_SCOPE.PROJECT ? '项目级' : '全局';
|
|
1210
|
+
console.log(chalk.gray(` └ ${result.tool}: ${result.count}/${result.total} 个 ${result.type} (${scopeLabel})`));
|
|
1211
|
+
if (result.path) {
|
|
1212
|
+
console.log(chalk.gray(` → ${result.path}`));
|
|
1213
|
+
}
|
|
1120
1214
|
} else if (result.note) {
|
|
1121
1215
|
console.log(chalk.gray(` └ ${result.tool}: ${result.note}`));
|
|
1122
1216
|
}
|
|
@@ -1217,6 +1311,7 @@ async function updateCommand(projectDir) {
|
|
|
1217
1311
|
// 加载配置
|
|
1218
1312
|
const config = loadConfig(projectDir);
|
|
1219
1313
|
const configuredTools = config.aiTools;
|
|
1314
|
+
const installScope = config.installScope || INSTALL_SCOPE.PROJECT;
|
|
1220
1315
|
|
|
1221
1316
|
if (configuredTools.length === 0) {
|
|
1222
1317
|
console.log(chalk.yellow('⚠') + ` 未配置任何 AI 工具。运行 \`${CLI_COMMAND} init\` 进行配置。`);
|
|
@@ -1227,13 +1322,17 @@ async function updateCommand(projectDir) {
|
|
|
1227
1322
|
const tool = AI_TOOLS.find(t => t.id === id);
|
|
1228
1323
|
return tool ? tool.name : id;
|
|
1229
1324
|
});
|
|
1230
|
-
|
|
1325
|
+
const scopeLabel = installScope === INSTALL_SCOPE.PROJECT ? '项目级' : '全局';
|
|
1326
|
+
console.log(chalk.blue('ℹ') + ` 检测到已配置的工具: ${toolNames.join(', ')} (${scopeLabel}安装)`);
|
|
1231
1327
|
|
|
1232
|
-
// 更新 Skills
|
|
1233
|
-
const skillsResults = installSkills(configuredTools, true);
|
|
1328
|
+
// 更新 Skills(使用配置中保存的安装范围)
|
|
1329
|
+
const skillsResults = installSkills(configuredTools, projectDir, installScope, true);
|
|
1234
1330
|
for (const result of skillsResults) {
|
|
1235
1331
|
if (result.count > 0) {
|
|
1236
1332
|
console.log(chalk.green('✓') + ` ${result.tool} ${result.type}: 更新了 ${result.count}/${result.total} 个`);
|
|
1333
|
+
if (result.path) {
|
|
1334
|
+
console.log(chalk.gray(` → ${result.path}`));
|
|
1335
|
+
}
|
|
1237
1336
|
}
|
|
1238
1337
|
if (result.removed && result.removed > 0) {
|
|
1239
1338
|
console.log(chalk.green('✓') + ` ${result.tool} ${result.type}: 清理了 ${result.removed} 个已删除的技能`);
|
|
@@ -1375,6 +1474,8 @@ function showHelp() {
|
|
|
1375
1474
|
console.log(chalk.cyan('选项:'));
|
|
1376
1475
|
console.log(' --tools <tools> 非交互式指定 AI 工具');
|
|
1377
1476
|
console.log(' 可用值: all, none, 或逗号分隔的工具 ID');
|
|
1477
|
+
console.log(' --scope <scope> Skills 安装位置 (非交互式模式)');
|
|
1478
|
+
console.log(' 可用值: project (默认), global');
|
|
1378
1479
|
console.log(' --from <framework> 迁移来源框架 (openspec, speckit)');
|
|
1379
1480
|
console.log(' --dry-run 模拟运行,不实际修改文件');
|
|
1380
1481
|
console.log(' --keep-old 迁移后保留原目录');
|
|
@@ -1419,7 +1520,8 @@ function showHelp() {
|
|
|
1419
1520
|
console.log(chalk.cyan('示例:'));
|
|
1420
1521
|
console.log(` ${CLI_COMMAND} init # 交互式初始化`);
|
|
1421
1522
|
console.log(` ${CLI_COMMAND} init my-project # 在 my-project 目录初始化`);
|
|
1422
|
-
console.log(` ${CLI_COMMAND} init --tools claude,cursor #
|
|
1523
|
+
console.log(` ${CLI_COMMAND} init --tools claude,cursor # 非交互式(默认项目级安装)`);
|
|
1524
|
+
console.log(` ${CLI_COMMAND} init --tools claude --scope global # 非交互式(全局安装)`);
|
|
1423
1525
|
console.log(` ${CLI_COMMAND} update # 更新已配置的工具`);
|
|
1424
1526
|
console.log(` ${CLI_COMMAND} migrate --from openspec # 从 OpenSpec 迁移`);
|
|
1425
1527
|
console.log(` ${CLI_COMMAND} migrate --from speckit # 从 spec-kit 迁移`);
|
|
@@ -1449,6 +1551,8 @@ async function main() {
|
|
|
1449
1551
|
process.exit(0);
|
|
1450
1552
|
} else if (arg === '--tools') {
|
|
1451
1553
|
options.tools = args[++i];
|
|
1554
|
+
} else if (arg === '--scope') {
|
|
1555
|
+
options.scope = args[++i];
|
|
1452
1556
|
} else if (arg === '--from') {
|
|
1453
1557
|
options.from = args[++i];
|
|
1454
1558
|
} else if (arg === '--dry-run') {
|
package/package.json
CHANGED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: devbooks-convergence-audit
|
|
3
|
+
description: devbooks-convergence-audit:以证据优先、声明存疑的原则评估 DevBooks 工作流收敛性,检测"西西弗斯反模式"和"假完成"。主动验证而非信任文档声明。用户说"评估收敛性/检查升级健康度/西西弗斯检测/工作流审计"等时使用。
|
|
4
|
+
allowed-tools:
|
|
5
|
+
- Glob
|
|
6
|
+
- Grep
|
|
7
|
+
- Read
|
|
8
|
+
- Bash
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# DevBooks:收敛性审计(Convergence Audit)
|
|
12
|
+
|
|
13
|
+
## 核心原则:反迷惑设计
|
|
14
|
+
|
|
15
|
+
> **黄金法则**:**证据 > 声明**。永远不要相信文档中的任何断言,必须通过可验证的证据确认。
|
|
16
|
+
|
|
17
|
+
### AI 容易被迷惑的场景(必须防范)
|
|
18
|
+
|
|
19
|
+
| 迷惑场景 | AI 错误行为 | 正确行为 |
|
|
20
|
+
|----------|-------------|----------|
|
|
21
|
+
| 文档写 `Status: Done` | 相信已完成 | 验证:测试是否真的全绿?证据是否存在? |
|
|
22
|
+
| AC 矩阵全部 `[x]` | 相信全覆盖 | 验证:每个 AC 对应的测试文件是否存在且通过? |
|
|
23
|
+
| 文档写"测试通过" | 相信通过 | 验证:实际运行测试或检查 CI 日志时间戳 |
|
|
24
|
+
| `evidence/` 目录存在 | 相信有证据 | 验证:目录非空?内容是否为有效测试日志? |
|
|
25
|
+
| tasks.md 全部 `[x]` | 相信已实现 | 验证:对应代码文件是否存在且有实质内容? |
|
|
26
|
+
| 提交信息说"修复了" | 相信已修复 | 验证:相关测试是否从红变绿? |
|
|
27
|
+
|
|
28
|
+
### 反迷惑三原则
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
1. 声明存疑(Distrust Declarations)
|
|
32
|
+
- 任何文档中的"完成/通过/覆盖"声明都是待验证的假设
|
|
33
|
+
- 默认立场:声明可能是错误的、过时的、或乐观的
|
|
34
|
+
|
|
35
|
+
2. 证据优先(Evidence First)
|
|
36
|
+
- 代码/测试结果是唯一真理
|
|
37
|
+
- 日志时间戳必须晚于最后一次代码修改
|
|
38
|
+
- 空目录/空文件 = 无证据
|
|
39
|
+
|
|
40
|
+
3. 交叉验证(Cross Validation)
|
|
41
|
+
- 声明 vs 证据:检查是否一致
|
|
42
|
+
- 代码 vs 测试:检查是否匹配
|
|
43
|
+
- 多个文档:检查是否矛盾
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 验证检查清单(逐项执行)
|
|
49
|
+
|
|
50
|
+
### 检查 1:Status 字段真实性验证
|
|
51
|
+
|
|
52
|
+
**文档声明**:`verification.md` 中 `Status: Done` 或 `Status: Verified`
|
|
53
|
+
|
|
54
|
+
**验证步骤**:
|
|
55
|
+
```bash
|
|
56
|
+
# 1. 检查 verification.md 是否存在
|
|
57
|
+
[[ -f "verification.md" ]] || echo "❌ verification.md 不存在"
|
|
58
|
+
|
|
59
|
+
# 2. 检查 evidence/green-final/ 是否有内容
|
|
60
|
+
if [[ -z "$(ls -A evidence/green-final/ 2>/dev/null)" ]]; then
|
|
61
|
+
echo "❌ Status 声称完成,但 evidence/green-final/ 为空"
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# 3. 检查证据时间戳是否晚于代码最后修改
|
|
65
|
+
code_mtime=$(stat -f %m src/ 2>/dev/null || stat -c %Y src/)
|
|
66
|
+
evidence_mtime=$(stat -f %m evidence/green-final/* 2>/dev/null | sort -n | tail -1)
|
|
67
|
+
if [[ $evidence_mtime -lt $code_mtime ]]; then
|
|
68
|
+
echo "❌ 证据时间早于代码修改,证据可能过时"
|
|
69
|
+
fi
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**迷惑检测**:
|
|
73
|
+
- ⚠️ Status=Done 但 evidence/ 为空 → **假完成**
|
|
74
|
+
- ⚠️ Status=Done 但证据时间戳过旧 → **过时证据**
|
|
75
|
+
- ⚠️ Status=Done 但测试实际运行失败 → **虚假状态**
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
### 检查 2:AC 覆盖矩阵真实性验证
|
|
80
|
+
|
|
81
|
+
**文档声明**:AC 矩阵中 `[x]` 表示已覆盖
|
|
82
|
+
|
|
83
|
+
**验证步骤**:
|
|
84
|
+
```bash
|
|
85
|
+
# 1. 提取所有声称已覆盖的 AC
|
|
86
|
+
grep -E '^\| AC-[0-9]+.*\[x\]' verification.md | while read line; do
|
|
87
|
+
ac_id=$(echo "$line" | grep -oE 'AC-[0-9]+')
|
|
88
|
+
test_id=$(echo "$line" | grep -oE 'T-[0-9]+')
|
|
89
|
+
|
|
90
|
+
# 2. 验证对应测试是否存在
|
|
91
|
+
if ! grep -rq "$test_id\|$ac_id" tests/; then
|
|
92
|
+
echo "❌ $ac_id 声称已覆盖,但找不到对应测试"
|
|
93
|
+
fi
|
|
94
|
+
done
|
|
95
|
+
|
|
96
|
+
# 3. 实际运行测试验证(最可靠)
|
|
97
|
+
npm test 2>&1 | tee /tmp/test-output.log
|
|
98
|
+
if grep -q "FAIL\|Error\|failed" /tmp/test-output.log; then
|
|
99
|
+
echo "❌ AC 声称全覆盖,但测试实际有失败"
|
|
100
|
+
fi
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**迷惑检测**:
|
|
104
|
+
- ⚠️ AC 打勾但对应测试文件不存在 → **虚假覆盖**
|
|
105
|
+
- ⚠️ AC 打勾但测试实际失败 → **假绿**
|
|
106
|
+
- ⚠️ AC 打勾但测试内容为空/占位符 → **占位符测试**
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
### 检查 3:tasks.md 完成度真实性验证
|
|
111
|
+
|
|
112
|
+
**文档声明**:tasks.md 中 `[x]` 表示已完成
|
|
113
|
+
|
|
114
|
+
**验证步骤**:
|
|
115
|
+
```bash
|
|
116
|
+
# 1. 提取所有声称已完成的任务
|
|
117
|
+
grep -E '^\- \[x\]' tasks.md | while read line; do
|
|
118
|
+
# 2. 提取任务描述中的关键词(函数名/文件名/功能)
|
|
119
|
+
keywords=$(echo "$line" | grep -oE '[A-Za-z]+[A-Za-z0-9]*' | head -5)
|
|
120
|
+
|
|
121
|
+
# 3. 验证代码中是否有对应实现
|
|
122
|
+
for kw in $keywords; do
|
|
123
|
+
if ! grep -rq "$kw" src/; then
|
|
124
|
+
echo "⚠️ 任务声称完成,但代码中找不到关键词: $kw"
|
|
125
|
+
fi
|
|
126
|
+
done
|
|
127
|
+
done
|
|
128
|
+
|
|
129
|
+
# 4. 检查是否有"骨架代码"(只有函数签名没有实现)
|
|
130
|
+
grep -rE 'throw new Error\(.*not implemented|TODO|FIXME|pass$|\.\.\.}' src/ && \
|
|
131
|
+
echo "⚠️ 发现未实现的占位符代码"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**迷惑检测**:
|
|
135
|
+
- ⚠️ 任务打勾但代码不存在 → **虚假完成**
|
|
136
|
+
- ⚠️ 任务打勾但代码是占位符 → **骨架代码**
|
|
137
|
+
- ⚠️ 任务打勾但功能不可调用 → **死代码**
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
### 检查 4:证据有效性验证
|
|
142
|
+
|
|
143
|
+
**文档声明**:`evidence/` 目录包含测试证据
|
|
144
|
+
|
|
145
|
+
**验证步骤**:
|
|
146
|
+
```bash
|
|
147
|
+
# 1. 检查目录是否存在且非空
|
|
148
|
+
if [[ ! -d "evidence" ]] || [[ -z "$(ls -A evidence/)" ]]; then
|
|
149
|
+
echo "❌ evidence/ 不存在或为空"
|
|
150
|
+
exit 1
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
# 2. 检查证据文件是否有实质内容
|
|
154
|
+
for f in evidence/**/*; do
|
|
155
|
+
if [[ -f "$f" ]]; then
|
|
156
|
+
lines=$(wc -l < "$f")
|
|
157
|
+
if [[ $lines -lt 5 ]]; then
|
|
158
|
+
echo "⚠️ 证据文件内容过少: $f ($lines 行)"
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
# 3. 检查是否为有效测试日志(包含测试框架输出特征)
|
|
162
|
+
if ! grep -qE 'PASS|FAIL|✓|✗|passed|failed|test|spec' "$f"; then
|
|
163
|
+
echo "⚠️ 证据文件不像测试日志: $f"
|
|
164
|
+
fi
|
|
165
|
+
fi
|
|
166
|
+
done
|
|
167
|
+
|
|
168
|
+
# 4. 检查 red-baseline 证据是否真的是红色(有失败)
|
|
169
|
+
if [[ -d "evidence/red-baseline" ]]; then
|
|
170
|
+
if ! grep -rqE 'FAIL|Error|✗|failed' evidence/red-baseline/; then
|
|
171
|
+
echo "❌ red-baseline 声称是红色,但没有失败记录"
|
|
172
|
+
fi
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
# 5. 检查 green-final 证据是否真的是绿色(全通过)
|
|
176
|
+
if [[ -d "evidence/green-final" ]]; then
|
|
177
|
+
if grep -rqE 'FAIL|Error|✗|failed' evidence/green-final/; then
|
|
178
|
+
echo "❌ green-final 声称是绿色,但包含失败记录"
|
|
179
|
+
fi
|
|
180
|
+
fi
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**迷惑检测**:
|
|
184
|
+
- ⚠️ evidence/ 存在但内容为空 → **空证据**
|
|
185
|
+
- ⚠️ 证据文件太小(< 5 行)→ **占位符证据**
|
|
186
|
+
- ⚠️ red-baseline 没有失败记录 → **伪造红色**
|
|
187
|
+
- ⚠️ green-final 包含失败记录 → **伪造绿色**
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
### 检查 5:Git 历史交叉验证
|
|
192
|
+
|
|
193
|
+
**原理**:Git 历史不会撒谎,用它来验证文档声明
|
|
194
|
+
|
|
195
|
+
**验证步骤**:
|
|
196
|
+
```bash
|
|
197
|
+
# 1. 检查声称完成的变更是否有对应的代码提交
|
|
198
|
+
change_id="xxx"
|
|
199
|
+
commits=$(git log --oneline --all --grep="$change_id" | wc -l)
|
|
200
|
+
if [[ $commits -eq 0 ]]; then
|
|
201
|
+
echo "❌ 变更 $change_id 声称完成,但 git 历史中没有相关提交"
|
|
202
|
+
fi
|
|
203
|
+
|
|
204
|
+
# 2. 检查测试文件是否在代码之后添加(TDD 违规检测)
|
|
205
|
+
for test_file in tests/**/*.test.*; do
|
|
206
|
+
test_added=$(git log --format=%at --follow -- "$test_file" | tail -1)
|
|
207
|
+
# 找到对应的源文件
|
|
208
|
+
src_file=$(echo "$test_file" | sed 's/tests/src/' | sed 's/.test//')
|
|
209
|
+
if [[ -f "$src_file" ]]; then
|
|
210
|
+
src_added=$(git log --format=%at --follow -- "$src_file" | tail -1)
|
|
211
|
+
if [[ $test_added -gt $src_added ]]; then
|
|
212
|
+
echo "⚠️ 测试后于代码添加(非 TDD): $test_file"
|
|
213
|
+
fi
|
|
214
|
+
fi
|
|
215
|
+
done
|
|
216
|
+
|
|
217
|
+
# 3. 检查是否有"一次性大提交"(可能是绕过流程)
|
|
218
|
+
git log --oneline -20 | while read line; do
|
|
219
|
+
commit=$(echo "$line" | cut -d' ' -f1)
|
|
220
|
+
files_changed=$(git show --stat "$commit" | grep -E '[0-9]+ file' | grep -oE '[0-9]+' | head -1)
|
|
221
|
+
if [[ $files_changed -gt 20 ]]; then
|
|
222
|
+
echo "⚠️ 大提交检测: $commit 修改了 $files_changed 个文件,可能绕过增量验证"
|
|
223
|
+
fi
|
|
224
|
+
done
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**迷惑检测**:
|
|
228
|
+
- ⚠️ 声称完成但无 git 提交 → **虚假变更**
|
|
229
|
+
- ⚠️ 测试后于代码添加 → **事后补测试**
|
|
230
|
+
- ⚠️ 大量文件一次提交 → **绕过增量验证**
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
### 检查 6:实时测试运行验证(最可靠)
|
|
235
|
+
|
|
236
|
+
**原理**:不信任任何日志,实际运行测试
|
|
237
|
+
|
|
238
|
+
**验证步骤**:
|
|
239
|
+
```bash
|
|
240
|
+
# 1. 运行完整测试
|
|
241
|
+
echo "=== 实时测试验证 ==="
|
|
242
|
+
npm test 2>&1 | tee /tmp/live-test.log
|
|
243
|
+
|
|
244
|
+
# 2. 检查结果
|
|
245
|
+
if grep -qE 'FAIL|Error|failed' /tmp/live-test.log; then
|
|
246
|
+
echo "❌ 实时测试失败,文档声明不可信"
|
|
247
|
+
grep -E 'FAIL|Error|failed' /tmp/live-test.log
|
|
248
|
+
else
|
|
249
|
+
echo "✅ 实时测试通过"
|
|
250
|
+
fi
|
|
251
|
+
|
|
252
|
+
# 3. 对比实时结果与证据文件
|
|
253
|
+
if [[ -f "evidence/green-final/latest.log" ]]; then
|
|
254
|
+
live_pass=$(grep -c 'PASS\|✓\|passed' /tmp/live-test.log)
|
|
255
|
+
evidence_pass=$(grep -c 'PASS\|✓\|passed' evidence/green-final/latest.log)
|
|
256
|
+
if [[ $live_pass -ne $evidence_pass ]]; then
|
|
257
|
+
echo "⚠️ 实时通过数 ($live_pass) ≠ 证据通过数 ($evidence_pass)"
|
|
258
|
+
fi
|
|
259
|
+
fi
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**迷惑检测**:
|
|
263
|
+
- ⚠️ 证据说绿色但实时运行失败 → **过时证据/假绿**
|
|
264
|
+
- ⚠️ 实时通过数与证据不符 → **证据伪造/环境差异**
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## 综合评分算法
|
|
269
|
+
|
|
270
|
+
### 可信度评分(0-100)
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
def calculate_trustworthiness(checks):
|
|
274
|
+
score = 100
|
|
275
|
+
|
|
276
|
+
# 严重问题(每个 -20 分)
|
|
277
|
+
critical = [
|
|
278
|
+
"证据为空",
|
|
279
|
+
"实时测试失败",
|
|
280
|
+
"Status 声称完成但测试失败",
|
|
281
|
+
"green-final 包含失败记录"
|
|
282
|
+
]
|
|
283
|
+
|
|
284
|
+
# 警告问题(每个 -10 分)
|
|
285
|
+
warnings = [
|
|
286
|
+
"证据时间戳过旧",
|
|
287
|
+
"AC 对应测试不存在",
|
|
288
|
+
"占位符代码",
|
|
289
|
+
"大提交检测"
|
|
290
|
+
]
|
|
291
|
+
|
|
292
|
+
# 轻微问题(每个 -5 分)
|
|
293
|
+
minor = [
|
|
294
|
+
"测试后于代码添加",
|
|
295
|
+
"证据文件过小"
|
|
296
|
+
]
|
|
297
|
+
|
|
298
|
+
for issue in checks.critical_issues:
|
|
299
|
+
score -= 20
|
|
300
|
+
for issue in checks.warnings:
|
|
301
|
+
score -= 10
|
|
302
|
+
for issue in checks.minor_issues:
|
|
303
|
+
score -= 5
|
|
304
|
+
|
|
305
|
+
return max(0, score)
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### 收敛性判定
|
|
309
|
+
|
|
310
|
+
| 可信度 | 判定 | 建议 |
|
|
311
|
+
|--------|------|------|
|
|
312
|
+
| 90-100 | ✅ 可信收敛 | 继续当前流程 |
|
|
313
|
+
| 70-89 | ⚠️ 部分可信 | 需要补充验证 |
|
|
314
|
+
| 50-69 | 🟠 存疑 | 需要返工部分环节 |
|
|
315
|
+
| < 50 | 🔴 不可信 | 西西弗斯困境,需要全面审查 |
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## 输出格式
|
|
320
|
+
|
|
321
|
+
```markdown
|
|
322
|
+
# DevBooks 收敛性审计报告(反迷惑版)
|
|
323
|
+
|
|
324
|
+
## 审计原则
|
|
325
|
+
本报告采用"证据优先、声明存疑"原则,所有结论基于可验证证据,而非文档声明。
|
|
326
|
+
|
|
327
|
+
## 声明 vs 证据对比
|
|
328
|
+
|
|
329
|
+
| 检查项 | 文档声明 | 实际验证 | 结论 |
|
|
330
|
+
|--------|----------|----------|------|
|
|
331
|
+
| Status | Done | 测试实际失败 | ❌ 假完成 |
|
|
332
|
+
| AC 覆盖 | 5/5 已打勾 | 2 个 AC 无对应测试 | ❌ 虚假覆盖 |
|
|
333
|
+
| 测试状态 | 全绿 | 实时运行 3 个失败 | ❌ 过时证据 |
|
|
334
|
+
| tasks.md | 10/10 完成 | 3 个任务代码不存在 | ❌ 虚假完成 |
|
|
335
|
+
| evidence/ | 存在 | 目录非空,内容有效 | ✅ 有效 |
|
|
336
|
+
|
|
337
|
+
## 可信度评分
|
|
338
|
+
|
|
339
|
+
**总分**:45/100 🔴 不可信
|
|
340
|
+
|
|
341
|
+
**扣分明细**:
|
|
342
|
+
- -20:Status=Done 但实时测试失败
|
|
343
|
+
- -20:AC 声称全覆盖但 2 个无测试
|
|
344
|
+
- -10:tasks.md 3 个任务无代码
|
|
345
|
+
- -5:证据时间戳早于代码修改
|
|
346
|
+
|
|
347
|
+
## 迷惑检测结果
|
|
348
|
+
|
|
349
|
+
### 🔴 检测到的假完成
|
|
350
|
+
1. `change-auth`:Status=Done,但 `npm test` 失败 3 个
|
|
351
|
+
2. `fix-cache`:AC-003 打勾,但 `tests/cache.test.ts` 不存在
|
|
352
|
+
|
|
353
|
+
### 🟡 可疑项
|
|
354
|
+
1. `refactor-api`:evidence/green-final/ 时间戳早于最后代码提交 2 天
|
|
355
|
+
2. `feature-login`:tasks.md 全部打勾,但 `src/login.ts` 包含 TODO
|
|
356
|
+
|
|
357
|
+
## 真实状态判定
|
|
358
|
+
|
|
359
|
+
| 变更包 | 声明状态 | 真实状态 | 差距 |
|
|
360
|
+
|--------|----------|----------|------|
|
|
361
|
+
| change-auth | Done | 测试失败 | 🔴 严重 |
|
|
362
|
+
| fix-cache | Verified | 覆盖不全 | 🟠 中等 |
|
|
363
|
+
| refactor-api | Ready | 证据过时 | 🟡 轻微 |
|
|
364
|
+
|
|
365
|
+
## 建议行动
|
|
366
|
+
|
|
367
|
+
### 立即行动
|
|
368
|
+
1. 将 `change-auth` 状态回退到 `In Progress`
|
|
369
|
+
2. 为 `fix-cache` 的 AC-003 补充测试
|
|
370
|
+
|
|
371
|
+
### 短期改进
|
|
372
|
+
1. 建立证据时效性检查(证据必须晚于代码)
|
|
373
|
+
2. AC 打勾前强制运行对应测试
|
|
374
|
+
|
|
375
|
+
### 流程改进
|
|
376
|
+
1. 禁止手动修改 Status,只能通过脚本验证后自动更新
|
|
377
|
+
2. CI 集成收敛性检查,阻止假完成合入
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## 完成状态
|
|
383
|
+
|
|
384
|
+
**状态**:✅ AUDIT_COMPLETED
|
|
385
|
+
|
|
386
|
+
**核心发现**:
|
|
387
|
+
- 文档声明可信度:X%
|
|
388
|
+
- 检测到的假完成:N 个
|
|
389
|
+
- 需要返工的变更:M 个
|
|
390
|
+
|
|
391
|
+
**下一步**:
|
|
392
|
+
- 假完成 → 立即回退状态,重新验证
|
|
393
|
+
- 存疑项 → 补充证据或重新运行测试
|
|
394
|
+
- 可信项 → 继续当前流程
|
|
@@ -28,7 +28,42 @@ allowed-tools:
|
|
|
28
28
|
|
|
29
29
|
## 产物落点
|
|
30
30
|
|
|
31
|
-
-
|
|
31
|
+
- **必须**写入:`<change-root>/<change-id>/proposal.md` 的 Impact 部分
|
|
32
|
+
- 备选:独立 `impact-analysis.md` 文件(后续回填到 proposal.md)
|
|
33
|
+
|
|
34
|
+
## 输出行为(关键约束)
|
|
35
|
+
|
|
36
|
+
> **黄金法则**:**直接写入文档,禁止输出到对话窗口**
|
|
37
|
+
|
|
38
|
+
### 必须遵守
|
|
39
|
+
|
|
40
|
+
1. **直接写入**:使用 `Edit` 或 `Write` 工具将分析结果直接写入目标文档
|
|
41
|
+
2. **禁止回显**:不要在对话中显示完整的分析内容
|
|
42
|
+
3. **简短通知**:完成后只需告知用户"影响分析已写入 `<文件路径>`"
|
|
43
|
+
|
|
44
|
+
### 正确行为 vs 错误行为
|
|
45
|
+
|
|
46
|
+
| 场景 | ❌ 错误行为 | ✅ 正确行为 |
|
|
47
|
+
|------|------------|------------|
|
|
48
|
+
| 分析完成 | 在对话中输出完整 Impact 表格 | 使用 Edit 工具写入 proposal.md |
|
|
49
|
+
| 通知用户 | 复述分析内容 | "影响分析已写入 `changes/xxx/proposal.md`" |
|
|
50
|
+
| 大量结果 | 分页输出到对话 | 全部写入文件,告知文件位置 |
|
|
51
|
+
|
|
52
|
+
### 示例对话
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
用户:分析一下修改 UserService 的影响
|
|
56
|
+
|
|
57
|
+
AI:[使用 Grep/CKB 分析引用]
|
|
58
|
+
[使用 Edit 工具写入 proposal.md]
|
|
59
|
+
|
|
60
|
+
影响分析已写入 `changes/refactor-user/proposal.md` 的 Impact 部分。
|
|
61
|
+
- 直接影响:8 个文件
|
|
62
|
+
- 间接影响:12 个文件
|
|
63
|
+
- 风险等级:中等
|
|
64
|
+
|
|
65
|
+
如需查看详情,请打开该文件。
|
|
66
|
+
```
|
|
32
67
|
|
|
33
68
|
## 执行方式
|
|
34
69
|
|