sillyspec 3.19.2 → 3.20.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.
@@ -1,21 +1,63 @@
1
1
  /**
2
2
  * change-risk-profile.js — 变更风险分级检测
3
3
  *
4
- * 根据变更涉及的文件类型、关键词,自动判定 verify 所需的验收强度。
4
+ * 根据变更涉及的文件类型、关键词、git diff、brainstorm 产物,
5
+ * 自动判定 P0/P1/P2 风险等级,产出结构化 risk-profile.json。
6
+ *
7
+ * P0 = 阻塞确认(必须用户确认)
8
+ * P1 = 自动推进但记录
9
+ * P2 = 自动通过
5
10
  */
6
11
 
7
- /**
8
- * 触发 integration-critical 风险等级的关键词
9
- */
10
- const INTEGRATION_CRITICAL_PATTERNS = [
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
@@ -63,6 +63,13 @@ SillySpec CLI — 规范驱动开发工具包
63
63
 
64
64
  sillyspec docs migrate 迁移旧文档到统一结构
65
65
 
66
+ sillyspec knowledge <cmd> 知识库管理(agent-safe,输出 JSON)
67
+ search --query "<text>" --limit N 搜索知识库
68
+ inspect --id "<id>" 读取知识条目详情
69
+ validate 校验知识库健康度
70
+ refresh 从 scan 文档刷新自动知识(仅写 generated/)
71
+ propose --title "<title>" --category <name> 提议新知识(写入 proposed/)
72
+
66
73
  sillyspec platform <cmd> SillyHub 平台同步
67
74
  connect <url> [--token <t>] 连接平台
68
75
  disconnect 断开连接
@@ -304,6 +311,11 @@ async function main() {
304
311
  await runCommand(stageArgs, resolveEffectiveDir(dir), specDir)
305
312
  break
306
313
  }
314
+ case 'knowledge': {
315
+ const { cmdKnowledge } = await import('./stages/knowledge.js')
316
+ await cmdKnowledge(filteredArgs.slice(1), resolveEffectiveDir(dir), { specDir })
317
+ break
318
+ }
307
319
  case 'dashboard': {
308
320
  // Parse dashboard options
309
321
  let port = 3456;
@@ -359,6 +371,7 @@ SillySpec worktree — git worktree 隔离管理
359
371
  用法:
360
372
  sillyspec worktree create <change-name> [--base <branch>] 创建隔离 worktree
361
373
  sillyspec worktree apply <change-name> [--check-only] 校验并应用变更到主工作区
374
+ sillyspec worktree assess <change-name> 风险审计 + 自动 apply
362
375
  sillyspec worktree list 列出所有活跃 worktree
363
376
  sillyspec worktree cleanup <change-name> [--force] 强制清理 worktree
364
377
  sillyspec worktree doctor [--fix] [--stale-hours N] 健康检查 + 修复
@@ -434,6 +447,52 @@ SillySpec worktree — git worktree 隔离管理
434
447
  }
435
448
  break;
436
449
  }
450
+ case 'assess': {
451
+ if (!wtName) {
452
+ console.error('❌ 用法: sillyspec worktree assess <change-name>');
453
+ process.exit(1);
454
+ }
455
+ const { assessApplyRisk } = await import('./worktree-apply.js');
456
+ const assessment = assessApplyRisk(wtName, { cwd: dir });
457
+
458
+ const SEPARATOR = '─'.repeat(32);
459
+ console.log('Worktree Apply Decision');
460
+ console.log(SEPARATOR);
461
+ const decisionIcon = assessment.decision === 'SAFE' ? '✅' : assessment.decision === 'WARNING' ? '⚠️ ' : '🚫';
462
+ console.log(`Decision: ${decisionIcon} ${assessment.decision}`);
463
+ console.log(`Changed files: ${assessment.changedFiles.length}`);
464
+ if (assessment.stats.additions > 0 || assessment.stats.deletions > 0) {
465
+ console.log(`Additions: +${assessment.stats.additions} Deletions: -${assessment.stats.deletions}`);
466
+ }
467
+
468
+ if (assessment.reasons.length > 0) {
469
+ console.log('');
470
+ console.log('Blocked reasons:');
471
+ for (const r of assessment.reasons) r.split('\n').forEach(l => console.log(` ${l}`));
472
+ }
473
+ if (assessment.warnings.length > 0) {
474
+ console.log('');
475
+ console.log('Warnings:');
476
+ for (const w of assessment.warnings) console.log(` ⚠️ ${w}`);
477
+ }
478
+
479
+ console.log('');
480
+ if (assessment.decision === 'SAFE' || assessment.decision === 'WARNING') {
481
+ console.log('Action: auto-applying...');
482
+ const { applyWorktree } = await import('./worktree-apply.js');
483
+ const applyResult = applyWorktree(wtName, { cwd: dir });
484
+ if (applyResult.errors.length > 0) {
485
+ console.error('❌ apply 失败:', applyResult.errors.join('; '));
486
+ } else {
487
+ console.log(`✅ 已自动应用 ${applyResult.changedFiles.length} 个文件变更`);
488
+ }
489
+ } else {
490
+ console.log('Action: blocked');
491
+ console.log(' → 检查变更: sillyspec worktree diff ' + wtName);
492
+ console.log(' → 丢弃变更: sillyspec worktree cleanup ' + wtName);
493
+ }
494
+ break;
495
+ }
437
496
  case 'list': {
438
497
  const items = wm.list();
439
498
  if (items.length === 0) {