sillyspec 3.24.3 → 3.24.5
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/machine-interface.js +19 -2
- package/src/run.js +20 -4
- package/src/verify-postcheck.js +81 -8
package/package.json
CHANGED
package/src/machine-interface.js
CHANGED
|
@@ -217,16 +217,27 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot }
|
|
|
217
217
|
// ── d. verify 阶段追加 verify-test(CLI 实测 local.yaml commands.test)──
|
|
218
218
|
if (stage === 'verify') {
|
|
219
219
|
const vt = runVerifyTestCheck({ cwd, specBase: specRoot, changeName });
|
|
220
|
+
const vtWarnings = [];
|
|
221
|
+
if (vt.status === 'skipped') {
|
|
222
|
+
vtWarnings.push('⚠️ verify-test SKIPPED — gate 未核验测试(local.yaml 未配置 commands.test 或显式无测试)。本次 gate 结论不含测试客观核验,driver 不应据 exit 0 判定测试通过;integration-critical 变更应在 verify 阶段降级 FAIL');
|
|
223
|
+
}
|
|
224
|
+
// 全量 fallback 明示:跑了全量 commands.test 但非变更范围子集,失败可能含未变更
|
|
225
|
+
// 模块的预存错误——driver 不应据 exit 0 判定本次变更范围已客观测试(见 3.24 verify 坑1)。
|
|
226
|
+
if (vt.status !== 'skipped' && vt.mode === 'full' && vt.fallbackReason) {
|
|
227
|
+
vtWarnings.push(`⚠️ verify-test 跑的是全量 commands.test(${vt.fallbackReason});失败可能含未变更模块的预存错误,driver 不应据 exit 0 判定本次变更范围已测`);
|
|
228
|
+
}
|
|
220
229
|
checks.push({
|
|
221
230
|
id: 'verify-test',
|
|
222
231
|
ok: vt.status !== 'failed',
|
|
223
232
|
errors: vt.status === 'failed' ? [`测试失败: ${vt.reason || ''}`] : [],
|
|
224
|
-
warnings:
|
|
233
|
+
warnings: vtWarnings,
|
|
225
234
|
data: {
|
|
226
235
|
status: vt.status,
|
|
227
236
|
exitCode: vt.exitCode,
|
|
228
237
|
durationMs: vt.durationMs,
|
|
229
238
|
resultPath: vt.resultPath,
|
|
239
|
+
mode: vt.mode ?? null,
|
|
240
|
+
fallbackReason: vt.fallbackReason ?? null,
|
|
230
241
|
},
|
|
231
242
|
});
|
|
232
243
|
}
|
|
@@ -351,10 +362,16 @@ export async function runDerive(facet, changeName, { cwd, specBase, runtimeRoot
|
|
|
351
362
|
exitCode: vt.exitCode,
|
|
352
363
|
durationMs: vt.durationMs,
|
|
353
364
|
resultPath: vt.resultPath,
|
|
365
|
+
mode: vt.mode ?? null,
|
|
366
|
+
fallbackReason: vt.fallbackReason ?? null,
|
|
354
367
|
};
|
|
355
368
|
ok = vt.status !== 'failed';
|
|
356
369
|
errors = vt.status === 'failed' ? [`测试失败: ${vt.reason || ''}`] : [];
|
|
357
|
-
warnings = vt.status === 'skipped'
|
|
370
|
+
warnings = vt.status === 'skipped'
|
|
371
|
+
? ['测试被跳过']
|
|
372
|
+
: (vt.mode === 'full' && vt.fallbackReason)
|
|
373
|
+
? [`⚠️ verify-test 跑的是全量 commands.test(${vt.fallbackReason});失败可能含未变更模块的预存错误`]
|
|
374
|
+
: [];
|
|
358
375
|
exitCode = ok ? EXIT_OK : EXIT_BLOCKED;
|
|
359
376
|
break;
|
|
360
377
|
}
|
package/src/run.js
CHANGED
|
@@ -632,11 +632,26 @@ async function getStageSteps(stageName, cwd, progress, specDir = null) {
|
|
|
632
632
|
if (stageName === 'execute') {
|
|
633
633
|
const changeDir = resolveChangeDir(cwd, progress, specDir)
|
|
634
634
|
let planFile = null
|
|
635
|
+
let worktreePath = null
|
|
635
636
|
if (changeDir) {
|
|
636
637
|
const p = join(changeDir, 'plan.md')
|
|
637
638
|
if (existsSync(p)) planFile = p
|
|
639
|
+
// 自动检测 worktree 路径,注入 Wave prompt 的 workdir 指令
|
|
640
|
+
// 修复:之前未传 worktreePath 给 buildExecuteSteps,导致 Wave prompt 缺失工作目录指令,
|
|
641
|
+
// 子代理可能把文件写到主工作区而非 worktree 内,破坏隔离。
|
|
642
|
+
try {
|
|
643
|
+
const changeName = basename(changeDir)
|
|
644
|
+
const { WorktreeManager } = await import('./worktree.js')
|
|
645
|
+
const wm = new WorktreeManager({ cwd })
|
|
646
|
+
const meta = wm.getMeta(changeName)
|
|
647
|
+
if (meta?.worktreePath && existsSync(meta.worktreePath)) {
|
|
648
|
+
worktreePath = meta.worktreePath
|
|
649
|
+
}
|
|
650
|
+
} catch {
|
|
651
|
+
// 无 worktree meta 不阻断——可能是首次启动或 in-place 模式
|
|
652
|
+
}
|
|
638
653
|
}
|
|
639
|
-
return buildExecuteSteps(planFile)
|
|
654
|
+
return buildExecuteSteps(planFile, { worktreePath })
|
|
640
655
|
}
|
|
641
656
|
if (stageName === 'plan') {
|
|
642
657
|
const changeDir = resolveChangeDir(cwd, progress, specDir)
|
|
@@ -3207,11 +3222,12 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
3207
3222
|
const resultCheck = validateQuickResult(outputText)
|
|
3208
3223
|
if (!resultCheck.ok) {
|
|
3209
3224
|
console.error(`\n❌ quick 结果摘要结构不完整:缺少字段 ${resultCheck.missing.join('、')}`)
|
|
3210
|
-
console.error(` --output
|
|
3211
|
-
console.error(`
|
|
3225
|
+
console.error(` --output 是 QUICKLOG「结果:」归档的唯一来源,四个标签必须放在 --output 里(不是 --input)。`)
|
|
3226
|
+
console.error(` 补全后重跑 --done(不丢进度),直接照抄此模板:`)
|
|
3227
|
+
console.error(` sillyspec run quick --done --change <changeName> --output "需求:用户/任务要什么`)
|
|
3212
3228
|
console.error(` 根因:为什么这样改(纯新增/样式则写「无,纯新增/纯样式」)`)
|
|
3213
3229
|
console.error(` 方案:怎么改的`)
|
|
3214
|
-
console.error(` 结果:验证情况(测试数 / lint / typecheck /
|
|
3230
|
+
console.error(` 结果:验证情况(测试数 / lint / typecheck / 部署状态)"`)
|
|
3215
3231
|
steps[currentIdx].status = 'pending'
|
|
3216
3232
|
steps[currentIdx].completedAt = null
|
|
3217
3233
|
if (outputText) steps[currentIdx].output = null
|
package/src/verify-postcheck.js
CHANGED
|
@@ -61,6 +61,47 @@ export function extractTestStrategy(yamlText) {
|
|
|
61
61
|
return null
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* 计算全量 fallback 的原因文本(供 printVerifyTestCheck / facet 给 agent/daemon 明示)。
|
|
66
|
+
*
|
|
67
|
+
* 返回 null 表示不需要 hint:显式 test_strategy:full(用户有意跑全量),
|
|
68
|
+
* 或 test_strategy:module 已成功命中模块子集(该情况不会走到全量路径)。
|
|
69
|
+
* 其余走全量路径的情况都返回原因字符串,让结果可正确解读——
|
|
70
|
+
* 否则 agent/daemon 会把"全量 commands.test"当成"按变更范围测的",误把
|
|
71
|
+
* 未变更模块的预存错误归因到本次变更(见 3.24 verify 坑1)。
|
|
72
|
+
*
|
|
73
|
+
* 语义对齐 runVerifyTestCheck 的分支:
|
|
74
|
+
* - strategy==='full' → null
|
|
75
|
+
* - strategy==='module' 但无有效 modules 块 → hint
|
|
76
|
+
* - strategy==='module' 有块但 git 不可用(hitCount=-1)→ hint
|
|
77
|
+
* - strategy==='module' 有块但 0 命中(hitCount=0)→ hint
|
|
78
|
+
* - strategy==='module' 有块且命中(hitCount>0)→ null(走子集)
|
|
79
|
+
* - strategy===null(缺省)→ hint(默认全量)
|
|
80
|
+
*
|
|
81
|
+
* @param {object} ctx
|
|
82
|
+
* @param {'full'|'module'|null} ctx.strategy
|
|
83
|
+
* @param {boolean} ctx.modulesPresent - extractModules 是否返回有效映射
|
|
84
|
+
* @param {number} ctx.hitCount - git diff 命中的模块数;-1 表示 git 不可用/非仓库
|
|
85
|
+
* @returns {string|null}
|
|
86
|
+
*/
|
|
87
|
+
export function computeFullFallbackReason({ strategy, modulesPresent, hitCount }) {
|
|
88
|
+
if (strategy === 'full') return null
|
|
89
|
+
if (strategy === 'module') {
|
|
90
|
+
if (!modulesPresent) {
|
|
91
|
+
return 'test_strategy: module 但 local.yaml 未配置有效的 modules: 块(需 inline flow: name: { path, test }),回退全量'
|
|
92
|
+
}
|
|
93
|
+
if (hitCount < 0) {
|
|
94
|
+
return 'test_strategy: module 但 git 不可用/非 git 仓库,无法判定命中模块,回退全量'
|
|
95
|
+
}
|
|
96
|
+
if (hitCount === 0) {
|
|
97
|
+
return 'test_strategy: module 但本次 git diff 未命中任何已配置 modules,回退全量'
|
|
98
|
+
}
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
// strategy === null(缺省 → 默认全量)
|
|
102
|
+
return 'local.yaml 未配置 test_strategy(默认全量 commands.test,未按变更范围收窄)'
|
|
103
|
+
}
|
|
104
|
+
|
|
64
105
|
/**
|
|
65
106
|
* 从 local.yaml 文本解析 modules 映射块。
|
|
66
107
|
* 设计约定(见 change 2026-07-10-tooling-followups design.md #2 方案 A):
|
|
@@ -258,29 +299,42 @@ export function runVerifyTestCheck({ cwd, specBase, changeName = null }) {
|
|
|
258
299
|
const localYamlPath = join(specBase, 'local.yaml')
|
|
259
300
|
const yamlText = existsSync(localYamlPath) ? readFileSync(localYamlPath, 'utf8') : null
|
|
260
301
|
|
|
261
|
-
// —— 模块子集路径(test_strategy: module)——
|
|
262
302
|
const strategy = extractTestStrategy(yamlText)
|
|
303
|
+
|
|
304
|
+
// —— 模块子集路径(test_strategy: module)——
|
|
305
|
+
// 算出 modulesPresent / hitCount 供 fallback hint 判定(computeFullFallbackReason);
|
|
306
|
+
// 命中即走子集。gitChangedFiles 返回 null 表示 git 不可用 → hitCount=-1(与 0 命中区分)。
|
|
307
|
+
let modulesPresent = false
|
|
308
|
+
let hitCount = 0
|
|
263
309
|
if (strategy === 'module') {
|
|
264
310
|
const modules = extractModules(yamlText)
|
|
265
311
|
if (modules) {
|
|
312
|
+
modulesPresent = true
|
|
266
313
|
const changedFiles = gitChangedFiles(cwd)
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
314
|
+
if (changedFiles === null) {
|
|
315
|
+
hitCount = -1 // git 不可用 / 非仓库
|
|
316
|
+
} else {
|
|
317
|
+
const hits = pickHitModules(changedFiles, modules)
|
|
318
|
+
hitCount = hits.length
|
|
319
|
+
if (hitCount > 0) {
|
|
320
|
+
return runModuleSubset({ cwd, specBase, changeName, hits })
|
|
321
|
+
}
|
|
322
|
+
// 有 modules 配置但无命中 → fallback commands.test(D-002@v1 向后兼容)
|
|
270
323
|
}
|
|
271
|
-
// 有 modules 配置但无命中 / git 不可用 → fallback commands.test(D-002@v1 向后兼容)
|
|
272
324
|
}
|
|
273
325
|
// test_strategy:module 但无 modules 配置 → fallback commands.test(brownfield 友好)
|
|
274
326
|
}
|
|
275
327
|
|
|
276
328
|
// —— 全量路径(默认 full / fallback)——
|
|
277
|
-
|
|
329
|
+
// fallbackReason 非 null 表示本次全量是"非显式"的(缺省/配置不全/未命中),需明示。
|
|
330
|
+
const fallbackReason = computeFullFallbackReason({ strategy, modulesPresent, hitCount })
|
|
331
|
+
return runFullCommand({ yamlText, localYamlPath, cwd, specBase, changeName, fallbackReason })
|
|
278
332
|
}
|
|
279
333
|
|
|
280
334
|
/**
|
|
281
335
|
* 全量跑 commands.test(现有逻辑,brownfield 行为不变)。
|
|
282
336
|
*/
|
|
283
|
-
function runFullCommand({ yamlText, localYamlPath, cwd, specBase, changeName }) {
|
|
337
|
+
function runFullCommand({ yamlText, localYamlPath, cwd, specBase, changeName, fallbackReason = null }) {
|
|
284
338
|
const command = extractTestCommand(yamlText)
|
|
285
339
|
|
|
286
340
|
if (!command) {
|
|
@@ -294,6 +348,8 @@ function runFullCommand({ yamlText, localYamlPath, cwd, specBase, changeName })
|
|
|
294
348
|
? 'local.yaml 未配置 commands.test(或标记 unavailable)'
|
|
295
349
|
: `local.yaml 不存在(${localYamlPath})`,
|
|
296
350
|
resultPath: null,
|
|
351
|
+
mode: 'full',
|
|
352
|
+
fallbackReason,
|
|
297
353
|
}
|
|
298
354
|
}
|
|
299
355
|
|
|
@@ -327,9 +383,11 @@ function runFullCommand({ yamlText, localYamlPath, cwd, specBase, changeName })
|
|
|
327
383
|
outputTail,
|
|
328
384
|
reason,
|
|
329
385
|
resultPath: null,
|
|
386
|
+
mode: 'full',
|
|
387
|
+
fallbackReason,
|
|
330
388
|
}
|
|
331
389
|
|
|
332
|
-
writeRunResult({ specBase, changeName, result })
|
|
390
|
+
writeRunResult({ specBase, changeName, result, extra: fallbackReason ? { fallback_reason: fallbackReason } : {} })
|
|
333
391
|
return result
|
|
334
392
|
}
|
|
335
393
|
|
|
@@ -362,6 +420,8 @@ function runModuleSubset({ cwd, specBase, changeName, hits }) {
|
|
|
362
420
|
outputTail: outputTail.length > OUTPUT_TAIL_CHARS ? '…' + outputTail.slice(-OUTPUT_TAIL_CHARS) : outputTail,
|
|
363
421
|
reason,
|
|
364
422
|
resultPath: null,
|
|
423
|
+
mode: 'module-subset',
|
|
424
|
+
fallbackReason: null,
|
|
365
425
|
}
|
|
366
426
|
|
|
367
427
|
writeRunResult({
|
|
@@ -427,6 +487,19 @@ export function printVerifyTestCheck(result) {
|
|
|
427
487
|
for (const line of tail.split('\n')) console.error(` | ${line}`)
|
|
428
488
|
}
|
|
429
489
|
}
|
|
490
|
+
// —— 全量 fallback 明示(skipped 已提前 return,此处仅 passed/failed)——
|
|
491
|
+
// 让 agent 知道本次跑的是全量 commands.test、非变更范围子集;失败可能含未变更模块
|
|
492
|
+
// 的预存错误,需先核对用例归属再归因到本次变更(见 3.24 verify 坑1)。
|
|
493
|
+
if (result.mode === 'full' && result.fallbackReason) {
|
|
494
|
+
if (result.status === 'failed') {
|
|
495
|
+
console.warn(` ⚠️ 本次跑的是 commands.test 全量(${result.fallbackReason})。`)
|
|
496
|
+
console.warn(' 失败可能含与本变更无关的预存错误——先核对失败用例是否属于你的变更范围;')
|
|
497
|
+
console.warn(' 或在 local.yaml 配置 test_strategy: module + modules: 块以收窄到变更模块。')
|
|
498
|
+
} else {
|
|
499
|
+
console.warn(` ⚠️ 本次跑的是 commands.test 全量(${result.fallbackReason})。`)
|
|
500
|
+
console.warn(' 如耗时过长,可在 local.yaml 配置 test_strategy: module + modules: 块按模块收窄。')
|
|
501
|
+
}
|
|
502
|
+
}
|
|
430
503
|
if (result.resultPath) {
|
|
431
504
|
console.log(`📄 实测结果已写入: ${result.resultPath}`)
|
|
432
505
|
}
|