sillyspec 3.20.0 → 3.20.2
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/package.json +1 -1
- package/src/brainstorm-postcheck.js +158 -0
- package/src/change-risk-profile.js +202 -17
- package/src/classify-change.js +73 -0
- package/src/index.js +7 -4
- package/src/run.js +270 -95
- package/src/scan-postcheck.js +15 -12
- package/src/stages/brainstorm-auto.js +229 -0
- package/src/stages/scan.js +20 -3
- package/test/run-scan-project-parse.test.mjs +166 -43
- package/test/scan-postcheck.test.mjs +18 -4
- package/.npmrc-tmp +0 -1
package/package.json
CHANGED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* brainstorm-postcheck.js — plan 阶段对 brainstorm 产物的校验
|
|
3
|
+
*
|
|
4
|
+
* 在 brainstorm → plan 之间执行,校验 brainstorm 产物完整性。
|
|
5
|
+
* PASS → 继续 plan
|
|
6
|
+
* WARN → 继续 plan,记录警告并分配补齐任务
|
|
7
|
+
* FAIL → 回退到 brainstorm 补齐(最多 2 次重试)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync } from 'fs'
|
|
11
|
+
import { join } from 'path'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 校验 brainstorm 产物完整性
|
|
15
|
+
* @param {string} changeDir - 变更目录路径(.sillyspec/changes/<change>)
|
|
16
|
+
* @returns {{ ok: boolean, level: 'PASS'|'WARN'|'FAIL', errors: string[], warnings: string[] }}
|
|
17
|
+
*/
|
|
18
|
+
export function checkBrainstormArtifacts(changeDir) {
|
|
19
|
+
const errors = []
|
|
20
|
+
const warnings = []
|
|
21
|
+
const brainstormDir = join(changeDir, 'brainstorm')
|
|
22
|
+
|
|
23
|
+
// ── 1. 产物完整性检查 ──
|
|
24
|
+
const requiredFiles = ['design.md', 'decisions.md', 'gaps.md', 'assumptions.md', 'next-action.json']
|
|
25
|
+
for (const file of requiredFiles) {
|
|
26
|
+
if (!existsSync(join(brainstormDir, file))) {
|
|
27
|
+
errors.push(`brainstorm/${file} 不存在`)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── 2. next-action.json 有效性检查 ──
|
|
32
|
+
const nextActionFile = join(brainstormDir, 'next-action.json')
|
|
33
|
+
if (existsSync(nextActionFile)) {
|
|
34
|
+
try {
|
|
35
|
+
const nextAction = JSON.parse(readFileSync(nextActionFile, 'utf8'))
|
|
36
|
+
|
|
37
|
+
// 必填字段
|
|
38
|
+
if (!['ready_for_plan', 'waiting_for_user'].includes(nextAction.status)) {
|
|
39
|
+
errors.push(`next-action.json.status 无效值: ${nextAction.status}`)
|
|
40
|
+
}
|
|
41
|
+
if (typeof nextAction.has_blocking_questions !== 'boolean') {
|
|
42
|
+
errors.push(`next-action.json.has_blocking_questions 不是布尔值`)
|
|
43
|
+
}
|
|
44
|
+
if (!['high', 'medium', 'low'].includes(nextAction.decision_level)) {
|
|
45
|
+
warnings.push(`next-action.json.decision_level 无效值: ${nextAction.decision_level}`)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 状态一致性
|
|
49
|
+
if (nextAction.status === 'waiting_for_user' && nextAction.has_blocking_questions !== true) {
|
|
50
|
+
errors.push(`next-action.json status=waiting_for_user 但 has_blocking_questions !== true`)
|
|
51
|
+
}
|
|
52
|
+
if (nextAction.status === 'ready_for_plan' && nextAction.has_blocking_questions === true) {
|
|
53
|
+
errors.push(`next-action.json status=ready_for_plan 但 has_blocking_questions === true`)
|
|
54
|
+
}
|
|
55
|
+
} catch (e) {
|
|
56
|
+
errors.push(`next-action.json 解析失败: ${e.message}`)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── 3. design.md 覆盖度检查 ──
|
|
61
|
+
const designFile = join(brainstormDir, 'design.md')
|
|
62
|
+
if (existsSync(designFile)) {
|
|
63
|
+
const designContent = readFileSync(designFile, 'utf8')
|
|
64
|
+
|
|
65
|
+
// 必须覆盖的维度
|
|
66
|
+
if (!/(?:目标|goal|objective|背景|background|问题|problem)/i.test(designContent)) {
|
|
67
|
+
warnings.push('design.md 缺少设计目标/背景描述')
|
|
68
|
+
}
|
|
69
|
+
if (!/(?:范围|scope|总体方案|方案|approach)/i.test(designContent)) {
|
|
70
|
+
warnings.push('design.md 缺少总体方案描述')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 弱覆盖检测
|
|
74
|
+
if (/影响模块.*TBD|文件变更.*TBD|待定/i.test(designContent)) {
|
|
75
|
+
warnings.push('design.md 包含 TBD/待定内容')
|
|
76
|
+
}
|
|
77
|
+
if (!/(?:验收|acceptance|完成标准)/i.test(designContent)) {
|
|
78
|
+
warnings.push('design.md 缺少验收标准')
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── 4. gaps.md 覆盖度检查 ──
|
|
83
|
+
const gapsFile = join(brainstormDir, 'gaps.md')
|
|
84
|
+
if (existsSync(gapsFile)) {
|
|
85
|
+
const gapsContent = readFileSync(gapsFile, 'utf8')
|
|
86
|
+
const hasBlocker = /BLOCKER/i.test(gapsContent)
|
|
87
|
+
if (hasBlocker) {
|
|
88
|
+
errors.push('gaps.md 包含 BLOCKER 级缺口,需要先解决')
|
|
89
|
+
}
|
|
90
|
+
// 检查是否为空
|
|
91
|
+
const nonEmpty = gapsContent.split('\n').filter(l => l.trim() && !l.startsWith('#')).length
|
|
92
|
+
if (nonEmpty === 0) {
|
|
93
|
+
warnings.push('gaps.md 为空(brainstorm 可能遗漏了缺口分析)')
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── 5. assumptions.md 风险检查 ──
|
|
98
|
+
const assumptionsFile = join(brainstormDir, 'assumptions.md')
|
|
99
|
+
if (existsSync(assumptionsFile)) {
|
|
100
|
+
const assumptionsContent = readFileSync(assumptionsFile, 'utf8')
|
|
101
|
+
if (/假设现有数据格式不变|假设.*API.*不变|假设.*性能.*可接受/i.test(assumptionsContent)) {
|
|
102
|
+
warnings.push('assumptions.md 包含高风险假设,需在 verify 阶段验证')
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── 6. decisions.md 一致性检查 ──
|
|
107
|
+
const decisionsFile = join(brainstormDir, 'decisions.md')
|
|
108
|
+
if (existsSync(decisionsFile)) {
|
|
109
|
+
const decisionsContent = readFileSync(decisionsFile, 'utf8')
|
|
110
|
+
const autoDecided = (decisionsContent.match(/AUTO_DECIDED/g) || []).length
|
|
111
|
+
const autoDecidedWithReason = (decisionsContent.match(/AUTO_DECIDED[\s\S]*?checklist/i) || []).length
|
|
112
|
+
if (autoDecided > 0 && autoDecidedWithReason < autoDecided) {
|
|
113
|
+
errors.push(`${autoDecided - autoDecidedWithReason} 个 AUTO_DECIDED 决策缺少 checklist 依据`)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── 判定结果 ──
|
|
118
|
+
let level
|
|
119
|
+
if (errors.length > 0) {
|
|
120
|
+
level = 'FAIL'
|
|
121
|
+
} else if (warnings.length > 0) {
|
|
122
|
+
level = 'WARN'
|
|
123
|
+
} else {
|
|
124
|
+
level = 'PASS'
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { ok: level !== 'FAIL', level, errors, warnings }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 执行 brainstorm postcheck(供 run.js 调用)
|
|
132
|
+
*/
|
|
133
|
+
export async function executeBrainstormPostcheck(cwd, platformOpts, changeName) {
|
|
134
|
+
const specBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
135
|
+
const changeDir = join(specBase, 'changes', changeName)
|
|
136
|
+
if (!existsSync(changeDir)) {
|
|
137
|
+
console.log('⚠️ brainstorm postcheck: 变更目录不存在,跳过')
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const result = checkBrainstormArtifacts(changeDir)
|
|
142
|
+
|
|
143
|
+
if (result.level === 'PASS') {
|
|
144
|
+
console.log('✅ brainstorm postcheck: PASS')
|
|
145
|
+
} else if (result.level === 'WARN') {
|
|
146
|
+
console.log('⚠️ brainstorm postcheck: WARN')
|
|
147
|
+
for (const w of result.warnings) {
|
|
148
|
+
console.log(` - ${w}`)
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
console.error('❌ brainstorm postcheck: FAIL')
|
|
152
|
+
for (const e of result.errors) {
|
|
153
|
+
console.error(` - ${e}`)
|
|
154
|
+
}
|
|
155
|
+
console.error('\\n 请修复以上问题后重试,或使用 --skip-approval 跳过。')
|
|
156
|
+
process.exit(1)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -1,21 +1,63 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* change-risk-profile.js — 变更风险分级检测
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 根据变更涉及的文件类型、关键词、git diff、brainstorm 产物,
|
|
5
|
+
* 自动判定 P0/P1/P2 风险等级,产出结构化 risk-profile.json。
|
|
6
|
+
*
|
|
7
|
+
* P0 = 阻塞确认(必须用户确认)
|
|
8
|
+
* P1 = 自动推进但记录
|
|
9
|
+
* P2 = 自动通过
|
|
5
10
|
*/
|
|
6
11
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
// ============ P0 触发规则 ============
|
|
13
|
+
|
|
14
|
+
const P0_FILE_PATTERNS = [
|
|
15
|
+
{ id: 'R-002', pattern: /(?:^|\/)(?:migrations?|migration)\/(?:.+)/i, desc: '数据库 migration' },
|
|
16
|
+
{ id: 'R-002', pattern: /(?:^|\/)schema\.(?:sql|prisma|ts|js)$/i, desc: '数据库 schema' },
|
|
17
|
+
{ id: 'R-003', pattern: /(?:^|\/)(?:auth|permission|role|login|token)\b/i, desc: '鉴权/权限' },
|
|
18
|
+
{ id: 'R-004', pattern: /(?:^|\/)(?:payment|billing|stripe|paypal)\b/i, desc: '支付/资金' },
|
|
19
|
+
{ id: 'R-005', pattern: /\.(?:env|env\.\w+)$/, desc: '环境配置' },
|
|
20
|
+
{ id: 'R-005', pattern: /(?:^|\/)(?:Dockerfile|docker-compose\.\w+)$/i, desc: '部署配置' },
|
|
21
|
+
{ id: 'R-005', pattern: /(?:^|\/)config\.(?:yaml|yml|json|toml)$/i, desc: '生产配置' },
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
const P0_CONTENT_PATTERNS = [
|
|
25
|
+
{ id: 'R-001', pattern: /\b(?:DROP\s+TABLE|TRUNCATE\s+TABLE|DELETE\s+FROM\s+\w+\s+(?!WHERE\s+.+\s+LIMIT))/i, desc: '删除数据' },
|
|
26
|
+
{ id: 'R-002', pattern: /\b(?:CREATE\s+TABLE|ALTER\s+TABLE|ADD\s+COLUMN|DROP\s+COLUMN|CREATE\s+INDEX)/i, desc: '数据库 migration' },
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
// ============ P1 触发规则 ============
|
|
30
|
+
|
|
31
|
+
const P1_FILE_PATTERNS = [
|
|
32
|
+
{ id: 'R-101', pattern: /(?:^|\/)(?:routes|controllers?|handlers?)\//i, desc: 'API 层' },
|
|
33
|
+
{ id: 'R-102', pattern: /(?:^|\/)(?:models|entities)\//i, desc: '数据模型层' },
|
|
34
|
+
{ id: 'R-104', pattern: /(?:^|\/)(?:services)\//i, desc: '业务逻辑层(跨模块)' },
|
|
35
|
+
{ id: 'R-106', pattern: /(?:^|\/)index\.(?:ts|js)$/i, desc: 're-export 入口' },
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
const P1_CONTENT_PATTERNS = [
|
|
39
|
+
{ id: 'R-101', pattern: /\b(?:daemon|backend|grpc|websocket|cross.?process|ipc|message.?queue)\b/i, desc: '跨进程通信' },
|
|
40
|
+
{ id: 'R-101', pattern: /\b(?:session|lease|agent.?run|lifecycle|state.?transition|claim|heartbeat)\b/i, desc: '状态机/生命周期' },
|
|
41
|
+
{ id: 'R-102', pattern: /\b(?:api|client|contract|dto)\b/i, desc: 'API contract' },
|
|
42
|
+
{ id: 'R-105', pattern: /(?:^|\/)(?:workflow|daemon|session|lifecycle|state-machine)\b/i, desc: '核心模块' },
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
// ============ P0/P1 命中文件名关键词(用于 git diff 检测) ============
|
|
46
|
+
|
|
47
|
+
const PROTECTED_FILE_NAMES = [
|
|
48
|
+
'.env', 'package.json', 'tsconfig.json', 'vite.config.ts', 'vite.config.js',
|
|
49
|
+
'prisma/schema.prisma', 'docker-compose.yml', 'docker-compose.yaml',
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
// ============ 向后兼容:旧的 INTEGRATION_CRITICAL_PATTERNS ============
|
|
53
|
+
|
|
54
|
+
export const INTEGRATION_CRITICAL_PATTERNS = [
|
|
12
55
|
/\bdaemon\b/i,
|
|
13
56
|
/\bbackend\b/i,
|
|
14
57
|
/\bclient.*api\b/i,
|
|
15
58
|
/\bgrpc\b/i,
|
|
16
59
|
/\bwebsocket\b/i,
|
|
17
60
|
/\bhttp.*client\b/i,
|
|
18
|
-
// 状态机 / 生命周期
|
|
19
61
|
/\bsession\b/i,
|
|
20
62
|
/\blease\b/i,
|
|
21
63
|
/\bagent.?run\b/i,
|
|
@@ -23,12 +65,10 @@ const INTEGRATION_CRITICAL_PATTERNS = [
|
|
|
23
65
|
/\bstate.?transition\b/i,
|
|
24
66
|
/\bclaim\b/i,
|
|
25
67
|
/\bheartbeat\b/i,
|
|
26
|
-
// 跨进程协议
|
|
27
68
|
/\bcross.?process\b/i,
|
|
28
69
|
/\bipc\b/i,
|
|
29
70
|
/\bmessage.?queue\b/i,
|
|
30
71
|
/\bpub.?sub\b/i,
|
|
31
|
-
// 部署/启动路径
|
|
32
72
|
/\bcli\.ts\b/i,
|
|
33
73
|
/\bmain\.ts\b/i,
|
|
34
74
|
/\bentrypoint\b/i,
|
|
@@ -38,10 +78,7 @@ const INTEGRATION_CRITICAL_PATTERNS = [
|
|
|
38
78
|
/\bdocker.?compose\b/i,
|
|
39
79
|
]
|
|
40
80
|
|
|
41
|
-
|
|
42
|
-
* 需要集成验证的文件路径关键词
|
|
43
|
-
*/
|
|
44
|
-
const INTEGRATION_FILE_PATTERNS = [
|
|
81
|
+
export const INTEGRATION_FILE_PATTERNS = [
|
|
45
82
|
/daemon/i,
|
|
46
83
|
/session.?manager/i,
|
|
47
84
|
/agent.?run/i,
|
|
@@ -55,12 +92,163 @@ const INTEGRATION_FILE_PATTERNS = [
|
|
|
55
92
|
/startup/i,
|
|
56
93
|
]
|
|
57
94
|
|
|
95
|
+
// ============ 核心检测函数 ============
|
|
96
|
+
|
|
58
97
|
/**
|
|
59
|
-
*
|
|
98
|
+
* 三级风险检测:P0 / P1 / P2
|
|
60
99
|
* @param {object} opts
|
|
61
100
|
* @param {string} [opts.designContent] - design.md 内容
|
|
62
101
|
* @param {string} [opts.planContent] - plan.md 内容
|
|
63
102
|
* @param {string[]} [opts.changedFiles] - 变更文件列表
|
|
103
|
+
* @param {string} [opts.diffContent] - git diff 内容(可选,用于更精确检测)
|
|
104
|
+
* @param {object} [opts.nextAction] - next-action.json 解析后的对象(可选)
|
|
105
|
+
* @param {string[]} [opts.protectedFiles] - 项目自定义 protected files 列表
|
|
106
|
+
* @returns {RiskProfile}
|
|
107
|
+
*
|
|
108
|
+
* @typedef {object} RiskProfile
|
|
109
|
+
* @property {string} level - 'P0' | 'P1' | 'P2'
|
|
110
|
+
* @property {string[]} triggers - 触发的规则 ID 列表
|
|
111
|
+
* @property {object[]} assessedFrom - 评估依据
|
|
112
|
+
* @property {string[]} applyBlockers - apply 阻塞原因
|
|
113
|
+
* @property {boolean} canAutoApply - 是否可自动 apply
|
|
114
|
+
* @property {string} applyReason - apply 决策原因
|
|
115
|
+
*/
|
|
116
|
+
export function detectRiskProfile({ designContent = '', planContent = '', changedFiles = [], diffContent = '', nextAction = null, protectedFiles = [] } = {}) {
|
|
117
|
+
const triggers = []
|
|
118
|
+
const assessedFrom = []
|
|
119
|
+
|
|
120
|
+
const combined = [designContent, planContent].join('\n')
|
|
121
|
+
|
|
122
|
+
// ── P0 检测(最高优先级)──
|
|
123
|
+
|
|
124
|
+
// 基于文件路径
|
|
125
|
+
for (const rule of P0_FILE_PATTERNS) {
|
|
126
|
+
for (const file of changedFiles) {
|
|
127
|
+
if (rule.pattern.test(file)) {
|
|
128
|
+
rule.pattern.lastIndex = 0
|
|
129
|
+
if (!triggers.includes(rule.id)) {
|
|
130
|
+
triggers.push(rule.id)
|
|
131
|
+
assessedFrom.push({ source: 'file_path', pattern: rule.desc, matchedFiles: [file] })
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 基于 diff 内容(P0 关键词)
|
|
138
|
+
const diffText = diffContent || combined
|
|
139
|
+
for (const rule of P0_CONTENT_PATTERNS) {
|
|
140
|
+
if (rule.pattern.test(diffText)) {
|
|
141
|
+
rule.pattern.lastIndex = 0
|
|
142
|
+
if (!triggers.includes(rule.id)) {
|
|
143
|
+
triggers.push(rule.id)
|
|
144
|
+
assessedFrom.push({ source: 'content', pattern: rule.desc })
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// protected files
|
|
150
|
+
for (const file of changedFiles) {
|
|
151
|
+
if (PROTECTED_FILE_NAMES.some(p => file === p || file.endsWith('/' + p))) {
|
|
152
|
+
const id = 'R-005'
|
|
153
|
+
if (!triggers.includes(id)) {
|
|
154
|
+
triggers.push(id)
|
|
155
|
+
assessedFrom.push({ source: 'protected_file', pattern: file })
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// 用户自定义 protected files
|
|
159
|
+
if (protectedFiles.includes(file)) {
|
|
160
|
+
const id = 'R-005'
|
|
161
|
+
if (!triggers.includes(id)) {
|
|
162
|
+
triggers.push(id)
|
|
163
|
+
assessedFrom.push({ source: 'protected_file', pattern: file })
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// next-action.json 有 blocking questions
|
|
169
|
+
if (nextAction && nextAction.has_blocking_questions === true) {
|
|
170
|
+
const id = 'R-009'
|
|
171
|
+
if (!triggers.includes(id)) {
|
|
172
|
+
triggers.push(id)
|
|
173
|
+
assessedFrom.push({ source: 'brainstorm', pattern: 'has_blocking_questions === true' })
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── P1 检测(只在无 P0 时检测)──
|
|
178
|
+
if (!triggers.some(t => t.startsWith('R-00'))) {
|
|
179
|
+
// 基于文件路径
|
|
180
|
+
for (const rule of P1_FILE_PATTERNS) {
|
|
181
|
+
for (const file of changedFiles) {
|
|
182
|
+
if (rule.pattern.test(file)) {
|
|
183
|
+
rule.pattern.lastIndex = 0
|
|
184
|
+
if (!triggers.includes(rule.id)) {
|
|
185
|
+
triggers.push(rule.id)
|
|
186
|
+
assessedFrom.push({ source: 'file_path', pattern: rule.desc, matchedFiles: [file] })
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 基于内容关键词
|
|
193
|
+
for (const rule of P1_CONTENT_PATTERNS) {
|
|
194
|
+
if (rule.pattern.test(combined)) {
|
|
195
|
+
rule.pattern.lastIndex = 0
|
|
196
|
+
if (!triggers.includes(rule.id)) {
|
|
197
|
+
triggers.push(rule.id)
|
|
198
|
+
assessedFrom.push({ source: 'content', pattern: rule.desc })
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 跨模块检测(> 3 个文件的变更)
|
|
204
|
+
if (changedFiles.length > 10) {
|
|
205
|
+
const id = 'R-101'
|
|
206
|
+
if (!triggers.includes(id)) {
|
|
207
|
+
triggers.push(id)
|
|
208
|
+
assessedFrom.push({ source: 'file_count', pattern: `${changedFiles.length} files changed` })
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// next-action.json 有 NEEDS_REVIEW 决策
|
|
213
|
+
if (nextAction && Array.isArray(nextAction.auto_decisions)) {
|
|
214
|
+
for (const d of nextAction.auto_decisions) {
|
|
215
|
+
if (d.status === 'NEEDS_REVIEW') {
|
|
216
|
+
const id = 'R-102'
|
|
217
|
+
if (!triggers.includes(id)) {
|
|
218
|
+
triggers.push(id)
|
|
219
|
+
assessedFrom.push({ source: 'brainstorm', pattern: `NEEDS_REVIEW: ${d.decision}` })
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ── 判定等级 ──
|
|
227
|
+
let level = 'P2'
|
|
228
|
+
if (triggers.some(t => t.match(/^R-0\d+$/))) {
|
|
229
|
+
level = 'P0'
|
|
230
|
+
} else if (triggers.some(t => t.match(/^R-1\d+$/))) {
|
|
231
|
+
level = 'P1'
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── apply 决策 ──
|
|
235
|
+
const applyBlockers = []
|
|
236
|
+
if (level === 'P0') {
|
|
237
|
+
applyBlockers.push('风险等级 P0:需要用户确认后才能 apply')
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const canAutoApply = level !== 'P0' && applyBlockers.length === 0
|
|
241
|
+
const applyReason = canAutoApply
|
|
242
|
+
? `风险等级 ${level},无 P0 触发,无 protected files 修改`
|
|
243
|
+
: applyBlockers.join('; ')
|
|
244
|
+
|
|
245
|
+
return { level, triggers, assessedFrom, applyBlockers, canAutoApply, applyReason }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ============ 向后兼容 ============
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* 旧的 detectChangeRisk 接口保持向后兼容
|
|
64
252
|
* @returns {{ level: string, triggers: string[], requiredVerification: string[] }}
|
|
65
253
|
*/
|
|
66
254
|
export function detectChangeRisk({ designContent = '', planContent = '', changedFiles = [] } = {}) {
|
|
@@ -114,9 +302,6 @@ export function detectChangeRisk({ designContent = '', planContent = '', changed
|
|
|
114
302
|
|
|
115
303
|
/**
|
|
116
304
|
* 检查 verify-result.md 是否包含集成验证证据
|
|
117
|
-
* @param {string} verifyContent
|
|
118
|
-
* @param {string[]} requiredVerification
|
|
119
|
-
* @returns {{ ok: boolean, errors: string[], warnings: string[] }}
|
|
120
305
|
*/
|
|
121
306
|
export function checkIntegrationEvidence(verifyContent, requiredVerification) {
|
|
122
307
|
const errors = []
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* classify-change.js — 变更规模分类器
|
|
3
|
+
*
|
|
4
|
+
* 将用户需求描述分类为 quick / auto / full,
|
|
5
|
+
* 供 auto 模式决定内部流程深度。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 用户显式指定的关键词 → 强制模式
|
|
10
|
+
*/
|
|
11
|
+
const FORCE_FULL_PATTERNS = [
|
|
12
|
+
/数据库|database|schema/i,
|
|
13
|
+
/迁移|migration|migrate/i,
|
|
14
|
+
/鉴权|权限|auth|permission|rbac/i,
|
|
15
|
+
/支付|payment|billing/i,
|
|
16
|
+
/重构|refactor.*architectur/i,
|
|
17
|
+
/微服务|microserv/i,
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
const FORCE_QUICK_PATTERNS = [
|
|
21
|
+
/fix typo/i,
|
|
22
|
+
/更新文案|改文案|文案修改/i,
|
|
23
|
+
/样式调整|style.*tweak/i,
|
|
24
|
+
/修复.*\s*\bbug\b.*\bfix\b/i,
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 分类变更规模
|
|
29
|
+
* @param {object} opts
|
|
30
|
+
* @param {string} opts.description - 用户需求描述
|
|
31
|
+
* @param {string} [opts.explicitMode] - 用户显式指定的模式(auto/quick/full)
|
|
32
|
+
* @param {object} [opts.localConfig] - local.yaml 中的 auto_mode 配置
|
|
33
|
+
* @returns {{ mode: 'quick'|'auto'|'full', reason: string }}
|
|
34
|
+
*/
|
|
35
|
+
export function classifyChange({ description = '', explicitMode, localConfig } = {}) {
|
|
36
|
+
// 1. 用户显式指定优先级最高
|
|
37
|
+
if (explicitMode && ['quick', 'auto', 'full'].includes(explicitMode)) {
|
|
38
|
+
return { mode: explicitMode, reason: '用户显式指定' }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 2. local.yaml force patterns
|
|
42
|
+
if (localConfig) {
|
|
43
|
+
const forceFullPatterns = localConfig.force_full_patterns || []
|
|
44
|
+
const forceQuickPatterns = localConfig.force_quick_patterns || []
|
|
45
|
+
|
|
46
|
+
for (const pattern of forceFullPatterns) {
|
|
47
|
+
if (new RegExp(pattern, 'i').test(description)) {
|
|
48
|
+
return { mode: 'full', reason: `local.yaml force_full_pattern 匹配: ${pattern}` }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
for (const pattern of forceQuickPatterns) {
|
|
52
|
+
if (new RegExp(pattern, 'i').test(description)) {
|
|
53
|
+
return { mode: 'quick', reason: `local.yaml force_quick_pattern 匹配: ${pattern}` }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 3. 默认关键词匹配
|
|
59
|
+
for (const pattern of FORCE_QUICK_PATTERNS) {
|
|
60
|
+
if (pattern.test(description)) {
|
|
61
|
+
return { mode: 'quick', reason: `命中 quick 关键词: ${pattern.source}` }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const pattern of FORCE_FULL_PATTERNS) {
|
|
66
|
+
if (pattern.test(description)) {
|
|
67
|
+
return { mode: 'full', reason: `命中 full 关键词: ${pattern.source}` }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 4. 默认 auto
|
|
72
|
+
return { mode: 'auto', reason: '默认模式' }
|
|
73
|
+
}
|
package/src/index.js
CHANGED
|
@@ -189,7 +189,7 @@ async function main() {
|
|
|
189
189
|
break;
|
|
190
190
|
case 'progress': {
|
|
191
191
|
const pm = new ProgressManager();
|
|
192
|
-
const progDir = resolveEffectiveDir(dir);
|
|
192
|
+
const progDir = specDir ? dir : resolveEffectiveDir(dir);
|
|
193
193
|
const subCommand = filteredArgs[1];
|
|
194
194
|
const stageIdx = filteredArgs.indexOf('--stage');
|
|
195
195
|
const stage = stageIdx >= 0 && filteredArgs[stageIdx + 1] ? filteredArgs[stageIdx + 1] : null;
|
|
@@ -293,7 +293,9 @@ async function main() {
|
|
|
293
293
|
}
|
|
294
294
|
case 'run': {
|
|
295
295
|
const { runCommand } = await import('./run.js')
|
|
296
|
-
|
|
296
|
+
// 平台模式(--spec-dir 已指定)时,--dir 是明确的 source_root,不应被 resolveEffectiveDir 纠正
|
|
297
|
+
const effectiveDir = specDir ? dir : resolveEffectiveDir(dir)
|
|
298
|
+
await runCommand(filteredArgs.slice(1), effectiveDir, specDir)
|
|
297
299
|
break
|
|
298
300
|
}
|
|
299
301
|
// task-10: 顶层命令别名,转发 runCommand,与 case 'run': 路径行为一致
|
|
@@ -308,12 +310,13 @@ async function main() {
|
|
|
308
310
|
case 'explore': {
|
|
309
311
|
const { runCommand } = await import('./run.js')
|
|
310
312
|
const stageArgs = [command, ...filteredArgs.slice(1)]
|
|
311
|
-
|
|
313
|
+
const effectiveDir = specDir ? dir : resolveEffectiveDir(dir)
|
|
314
|
+
await runCommand(stageArgs, effectiveDir, specDir)
|
|
312
315
|
break
|
|
313
316
|
}
|
|
314
317
|
case 'knowledge': {
|
|
315
318
|
const { cmdKnowledge } = await import('./stages/knowledge.js')
|
|
316
|
-
await cmdKnowledge(filteredArgs.slice(1), resolveEffectiveDir(dir), { specDir })
|
|
319
|
+
await cmdKnowledge(filteredArgs.slice(1), specDir ? dir : resolveEffectiveDir(dir), { specDir })
|
|
317
320
|
break
|
|
318
321
|
}
|
|
319
322
|
case 'dashboard': {
|