sillyspec 3.28.0 → 3.28.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/archive-delta.js +419 -0
- package/src/design-facts.js +354 -0
- package/src/docs-check.js +79 -8
- package/src/doctor-diagnostics.js +11 -2
- package/src/endpoint-baseline.js +109 -0
- package/src/git-helper.js +31 -0
- package/src/index.js +213 -4
- package/src/modules.js +41 -12
- package/src/progress/change-registry.js +20 -0
- package/src/progress/consistency-doctor.js +124 -5
- package/src/progress/stage-machine.js +13 -2
- package/src/progress.js +3 -0
- package/src/quicklog.js +51 -0
- package/src/run/command.js +70 -1
- package/src/run/complete-handlers.js +53 -4
- package/src/run/complete.js +46 -1
- package/src/run/gates.js +333 -2
- package/src/run/prompt.js +70 -0
- package/src/run/scan-profile.js +98 -6
- package/src/run/shared.js +12 -1
- package/src/run/stage.js +7 -1
- package/src/scan-diff.js +54 -1
- package/src/scan-facts.js +173 -0
- package/src/scan-postcheck.js +110 -3
- package/src/spec-sync.js +19 -2
- package/src/stage-review.js +62 -3
- package/src/stages/archive.js +5 -5
- package/src/stages/brainstorm.js +10 -10
- package/src/stages/execute.js +15 -13
- package/src/stages/plan-postcheck.js +194 -1
- package/src/stages/plan.js +6 -2
- package/src/stages/scan.js +44 -102
- package/src/stages/verify.js +9 -7
- package/src/taskcard.js +8 -1
- package/src/verify-postcheck.js +610 -2
- package/src/verify-probes.js +89 -10
- package/src/workflow.js +30 -0
- package/templates/agents-instruction.md +1 -1
- package/templates/prompts/taskcard-rules.md +5 -0
- package/templates/workflows/scan-docs.yaml +6 -0
package/src/run/gates.js
CHANGED
|
@@ -666,6 +666,56 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
666
666
|
const { runVerifyRequiredEvidenceCheck, printVerifyRequiredEvidenceCheck } = await import('../verify-postcheck.js')
|
|
667
667
|
const evidenceCheck = runVerifyRequiredEvidenceCheck({ cwd, specBase, changeName })
|
|
668
668
|
printVerifyRequiredEvidenceCheck(evidenceCheck)
|
|
669
|
+
// ── target_files 声明 ↔ 实际改动 对账(task-05 / ir-stage-p3a:②ERROR 阻断 / ③WARNING 放行)──
|
|
670
|
+
// 「计划落空」(task 卡声明没做)此前全盲区:review.json changedFiles 是 agent 手写不能当
|
|
671
|
+
// 事实源,execute 的 scope creep 只有 symbol-impact 试图抓。reconcileTargetFiles 用 git 三源
|
|
672
|
+
// 口径亲自取 actual,与 plan 阶段 task 卡 target_files 声明做三类差集:②类(声明未做)在场
|
|
673
|
+
// 即阻断——verify 的语义是「按计划交付且客观核验」,声明没做不能盖章归档;③类(做了没
|
|
674
|
+
// 声明=scope creep)启发式噪音面大(并行 WIP 剔除后仍可能有工具产物),WARNING 放行留审计;
|
|
675
|
+
// skipped/degraded(无 task 卡 / 全无声明 / git 不可用)降级不误红(存量变更零红门禁)。
|
|
676
|
+
// 阻断形态照 runVerifyTestCheck 先例:逐条列 task + path + 修复提示 + rollbackCompletionAndReturn。
|
|
677
|
+
const { reconcileTargetFiles } = await import('../verify-postcheck.js')
|
|
678
|
+
const reconcileRuntimeRoot = resolveRuntimeRoot(platformOpts, specBase)
|
|
679
|
+
const reconcileCheck = reconcileTargetFiles({
|
|
680
|
+
cwd, specBase, changeName,
|
|
681
|
+
// runtimeRoot 口径与上方 parity 对账同源(B3 apply-pathspec 兜底源在此根下;平台模式下
|
|
682
|
+
// specBase 与 .runtime 分离,不传会读不到兜底清单 → 假降级)
|
|
683
|
+
runtimeRoot: reconcileRuntimeRoot,
|
|
684
|
+
})
|
|
685
|
+
const reconcileEnvelope = buildReconcileTargetFilesEnvelope(reconcileCheck)
|
|
686
|
+
const reconcileBlocked = printReconcileTargetFilesCheck(reconcileCheck, reconcileEnvelope)
|
|
687
|
+
// 结果落盘(design Wave 2 承诺:对齐 .runtime/verify-runs/<ts>/ 先例)——五状态全落(含
|
|
688
|
+
// 放行态),fail-soft 不影响 gate 判定;②类阻断回执同样留档供平台/审计消费
|
|
689
|
+
writeReconcileRunResult({ runtimeRoot: reconcileRuntimeRoot, changeName, envelope: reconcileEnvelope, result: reconcileCheck })
|
|
690
|
+
if (reconcileBlocked) {
|
|
691
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
692
|
+
}
|
|
693
|
+
// ── verify-result.md 探针预填段一致性抽查(task-03 / ir-stage-p3b D-002@v1 方案A)──
|
|
694
|
+
// 「#### 探针 N」机械预填段(verify-probes --init 生成)长在 agent 可编辑的正文里,其防篡改
|
|
695
|
+
// 基准是正文锚点而非 verify-facts.json(删底稿绕不过防护)。checkProbeConsistency 重跑只读
|
|
696
|
+
// 探针与正文锚点对账,分级(D-003):probe1/probe6 不符 = ERROR → 阻断回滚(正文预填段疑似
|
|
697
|
+
// 被篡改);probe3/probe5 环境敏感维度 + R-06 HEAD 前进降级 = WARNING → 放行告警(drift);
|
|
698
|
+
// skipped(存量旧格式报告/无 changeName)/degraded(重跑异常 fail-soft)→ 放行提示。
|
|
699
|
+
// 接线形态照上方 P3a reconcile 先例:envelope + print(ERROR 时 console.error 逐条 mismatch +
|
|
700
|
+
// supportedFixes)+ verify-runs 落盘 + rollbackCompletionAndReturn。
|
|
701
|
+
const { checkProbeConsistency } = await import('../verify-postcheck.js')
|
|
702
|
+
const probeCheck = checkProbeConsistency({
|
|
703
|
+
// 四参与 reconcile 同源取值:cwd/specBase/changeName 原样透传;runtimeRoot 复用上方
|
|
704
|
+
// reconcileRuntimeRoot(resolveRuntimeRoot(platformOpts, specBase))——checkProbeConsistency
|
|
705
|
+
// 的锚点产物实际都在 changeDir,此参为统一传参签名对齐预留,口径一处定义不二算
|
|
706
|
+
cwd, specBase, changeName,
|
|
707
|
+
runtimeRoot: reconcileRuntimeRoot,
|
|
708
|
+
})
|
|
709
|
+
const probeEnvelope = buildProbeConsistencyEnvelope(probeCheck)
|
|
710
|
+
const probeBlocked = printProbeConsistencyCheck(probeCheck, probeEnvelope)
|
|
711
|
+
// 结果落盘(对齐 writeReconcileRunResult 先例):独立文件 probe-consistency-result.json——
|
|
712
|
+
// 与 reconcile-result.json 同 gate 运行同目录组织(verify-runs/<ts>/),但不并入后者:
|
|
713
|
+
// reconcile-result.json 的 schema 已被 P3a 消费方锁定,塞探针字段(读改写/扩参)只会引入
|
|
714
|
+
// 两检查间耦合;各写各的回执、按文件名订阅更干净。fail-soft 不影响 gate 判定
|
|
715
|
+
writeProbeConsistencyRunResult({ runtimeRoot: reconcileRuntimeRoot, changeName, envelope: probeEnvelope, result: probeCheck })
|
|
716
|
+
if (probeBlocked) {
|
|
717
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
718
|
+
}
|
|
669
719
|
// ── module-impact 死信探针(blocking,债单 D-1/D-5):更新结果表 pending/待办行 → 阻断 verify ──
|
|
670
720
|
// 与 archive 移动前校验(extractPendingDocSyncRows)同一口径,把死信号从 archive 提前到 verify:
|
|
671
721
|
// agent 在 verify 阶段就须完成文档同步并回填 done/skipped,而非拖到归档被拦(修复 perf-remediation
|
|
@@ -726,7 +776,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
726
776
|
if (['brainstorm', 'plan', 'execute'].includes(stageName)) {
|
|
727
777
|
try {
|
|
728
778
|
const { classifyReviewTier } = await import('../review-tier.js')
|
|
729
|
-
const {
|
|
779
|
+
const { validateStageReviewWithAutoRefresh, getLatestStageReviewRunId, printStageReviewResult, generateStageReviewRunId, stageReviewMarkerPath } = await import('../stage-review.js')
|
|
730
780
|
const effectiveSpecBase = platformOpts?.specRoot || specBase
|
|
731
781
|
const reviewChangeDir = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
|
|
732
782
|
const designPath = reviewChangeDir ? join(reviewChangeDir, 'design.md') : null
|
|
@@ -758,7 +808,16 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
758
808
|
: stageName === 'plan' ? 'plan'
|
|
759
809
|
: 'acceptance'
|
|
760
810
|
const searchDirs = [effectiveSpecBase, reviewChangeDir, cwd].filter(Boolean)
|
|
761
|
-
|
|
811
|
+
// docHash 失配自动刷新(坑 stage-review-refresh-hash-manual-forget,2026-09-04 ③):
|
|
812
|
+
// 主文档(design/plan.md)改版后忘跑 --refresh-hash 不再拦一轮——gate 机械重算放行
|
|
813
|
+
// (verdict 保留 + reviewerNotes 审计行 + 控制台留痕);主文档缺失/schema/verdict=fail
|
|
814
|
+
// 不在自动刷新范围,照常 fail-closed。
|
|
815
|
+
const gated = validateStageReviewWithAutoRefresh({ stage: stageName, reviewType, runtimeRoot, reviewRunId, searchDirs, autoRefresh: true })
|
|
816
|
+
if (gated.autoRefreshed) {
|
|
817
|
+
console.warn(`\n🔄 stage review docHash 失配已自动刷新(主文档改版后机械重算,verdict/checklist 保留——结论是否仍适用于新文档需人工确认)`)
|
|
818
|
+
console.warn(` 已就地更新: ${gated.refreshedPath}`)
|
|
819
|
+
}
|
|
820
|
+
const reviewResult = gated.result
|
|
762
821
|
printStageReviewResult(reviewResult, { stage: stageName, reviewRunId, runtimeRoot, changeName })
|
|
763
822
|
if (!reviewResult.ok) {
|
|
764
823
|
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
@@ -1169,6 +1228,278 @@ export function filterStaleBaselineOverlap(overlap, dirtySet, foreignDeclared) {
|
|
|
1169
1228
|
return (overlap || []).filter(f => dirtySet?.has(f) && !(foreignDeclared?.has(f)))
|
|
1170
1229
|
}
|
|
1171
1230
|
|
|
1231
|
+
/**
|
|
1232
|
+
* target_files 对账结果 → 诊断信封(task-05 接线配套)。
|
|
1233
|
+
*
|
|
1234
|
+
* verify 块既有五项检查是「单结果对象 + print 函数 + 状态判定」形态(runVerifyTestCheck 先例),
|
|
1235
|
+
* 无 scan-postcheck 那种 checks 数组聚合结构,故信封不进数组、字段命名对齐 scan-postcheck 的
|
|
1236
|
+
* checks 条目(name/severity/detail/evidence/supportedFixes)——机器可路由口径跨模块一致。
|
|
1237
|
+
* severity:②类阻断=error(产物级阻断而非流程崩溃,不取 critical);③类与降级=warning;ok=info。
|
|
1238
|
+
* evidence 携带三类清单计数 + actual 形态与源明细(③类误报排查先看 sources 归因)。
|
|
1239
|
+
* code:五状态→四 code 的机器路由键(reconcile_missing_declared / reconcile_undeclared_file /
|
|
1240
|
+
* reconcile_skipped / reconcile_ok,design 接口定义),写盘与平台消费按 code 分支。
|
|
1241
|
+
*
|
|
1242
|
+
* @param {object} r reconcileTargetFiles 返回值
|
|
1243
|
+
* @returns {{ code: string, name: string, severity: string, detail: string, evidence: object, supportedFixes: string[] }}
|
|
1244
|
+
*/
|
|
1245
|
+
export function buildReconcileTargetFilesEnvelope(r) {
|
|
1246
|
+
const severity = r.status === 'missing_declared' ? 'error'
|
|
1247
|
+
: (r.status === 'ok' ? 'info' : 'warning')
|
|
1248
|
+
// 修复提示按状态分级给「可执行」动作:②类=干活或改声明;③类=补声明或回退;降级=指引补声明
|
|
1249
|
+
const supportedFixes = []
|
|
1250
|
+
if (r.status === 'missing_declared') {
|
|
1251
|
+
supportedFixes.push('完成声明的工作:把上方清单中 task 卡 target_files 声明的文件真正改出来(NEW: 声明的新建文件要先创建)')
|
|
1252
|
+
supportedFixes.push('或修正声明:编辑 tasks/task-NN.md 的 target_files,移除不再交付的路径(改声明须与该 task 的 review 结论一致,不得为过门而删声明)')
|
|
1253
|
+
} else if (r.status === 'undeclared') {
|
|
1254
|
+
supportedFixes.push('补声明:把上方清单文件补进对应 task 卡的 target_files(存在的文件直接写仓根相对路径)')
|
|
1255
|
+
supportedFixes.push('或回退越权改动:不属于任何 task 交付的文件应 revert(并行会话声明文件已被剔除,剩余即本变更产出)')
|
|
1256
|
+
} else if (r.status === 'skipped' || r.status === 'degraded') {
|
|
1257
|
+
supportedFixes.push('新 task 卡请声明 target_files(每条为仓根相对精确文件路径,不存在的文件加 NEW: 前缀);存量卡无声明/跨仓卡跳过属预期,无需补')
|
|
1258
|
+
}
|
|
1259
|
+
return {
|
|
1260
|
+
// code 机器路由键(审查 gap 回流,design 接口定义):五状态→四 code(skipped/degraded 同
|
|
1261
|
+
// 一个 reconcile_skipped——两者对消费方语义一致:本次无对账结论),供平台/上游按 code 分支
|
|
1262
|
+
code: r.status === 'missing_declared' ? 'reconcile_missing_declared'
|
|
1263
|
+
: r.status === 'undeclared' ? 'reconcile_undeclared_file'
|
|
1264
|
+
: (r.status === 'ok' ? 'reconcile_ok' : 'reconcile_skipped'),
|
|
1265
|
+
name: 'target_files_reconcile',
|
|
1266
|
+
severity,
|
|
1267
|
+
detail: `target_files 声明 ↔ 实际改动对账:status=${r.status}` + (r.skipReason ? `(${r.skipReason})` : ''),
|
|
1268
|
+
evidence: {
|
|
1269
|
+
matched_count: r.matched.length,
|
|
1270
|
+
missing_declared_count: r.missing.length,
|
|
1271
|
+
undeclared_count: r.undeclared.length,
|
|
1272
|
+
form: r.form,
|
|
1273
|
+
sources: r.sources,
|
|
1274
|
+
},
|
|
1275
|
+
supportedFixes,
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
/**
|
|
1280
|
+
* target_files 对账结果落盘 .runtime/verify-runs/<ts>/reconcile-result.json(审查 gap 回流)。
|
|
1281
|
+
*
|
|
1282
|
+
* design「总体方案 Wave 2」承诺「结果落盘对齐 .runtime/verify-runs/<ts>/ 先例」——console 输出
|
|
1283
|
+
* 跨进程即失忆,落盘留审计痕迹供 SillyHub/平台消费。目录命名对齐 verify-postcheck.js
|
|
1284
|
+
* writeRunResult 先例(ts=UTC 紧凑 YYYYMMDDHHmmss,与 test-result.json 同目录组织)。
|
|
1285
|
+
* fail-soft:写失败只 console.error 留痕不阻断 gate(落盘是审计附件不是证据源,与
|
|
1286
|
+
* writeRunResult 的 best-effort 同口径);五状态全落(放行态也要留档——对账跳过/降级同样
|
|
1287
|
+
* 是审计事实,下次回看才知道为什么没有差集)。
|
|
1288
|
+
*
|
|
1289
|
+
* @param {{ runtimeRoot: string, changeName: string|null, envelope: object, result: object }} opts
|
|
1290
|
+
* @returns {string|null} 成功返回落盘路径;失败返回 null
|
|
1291
|
+
*/
|
|
1292
|
+
export function writeReconcileRunResult({ runtimeRoot, changeName, envelope, result }) {
|
|
1293
|
+
try {
|
|
1294
|
+
const ts = new Date().toISOString().slice(0, 19).replace(/[-T:]/g, '')
|
|
1295
|
+
const runDir = join(runtimeRoot, 'verify-runs', ts)
|
|
1296
|
+
mkdirSync(runDir, { recursive: true })
|
|
1297
|
+
const resultPath = join(runDir, 'reconcile-result.json')
|
|
1298
|
+
// 落盘字段 snake_case 对齐 writeRunResult 的 test-result.json 机器消费口径(信封内存字段
|
|
1299
|
+
// 保持 camelCase 不动,写盘这层做一次映射)
|
|
1300
|
+
writeFileSync(resultPath, JSON.stringify({
|
|
1301
|
+
change: changeName,
|
|
1302
|
+
code: envelope.code,
|
|
1303
|
+
severity: envelope.severity,
|
|
1304
|
+
status: result.status,
|
|
1305
|
+
evidence: envelope.evidence,
|
|
1306
|
+
supported_fixes: envelope.supportedFixes,
|
|
1307
|
+
matched: result.matched,
|
|
1308
|
+
missing: result.missing,
|
|
1309
|
+
undeclared: result.undeclared,
|
|
1310
|
+
notes: result.notes,
|
|
1311
|
+
form: result.form,
|
|
1312
|
+
sources: result.sources,
|
|
1313
|
+
ran_at: new Date().toISOString(),
|
|
1314
|
+
}, null, 2) + '\n')
|
|
1315
|
+
console.log(`📄 target_files 对账结果已写入: ${resultPath}`)
|
|
1316
|
+
return resultPath
|
|
1317
|
+
} catch (e) {
|
|
1318
|
+
console.error(`⚠️ target_files 对账结果落盘失败(不影响 verify gate): ${e.message}`)
|
|
1319
|
+
return null
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
/**
|
|
1324
|
+
* 打印 target_files 对账结果(task-05 接线配套的打印层)。
|
|
1325
|
+
*
|
|
1326
|
+
* reconcileTargetFiles 是纯函数无配套 print(verify-postcheck 导出消费、打印归接线方),本函数
|
|
1327
|
+
* 即其在 verify 块的打印层:②类阻断明细照 runVerifyTestCheck 的失败输出形态(状态行 + 逐条
|
|
1328
|
+
* task + path + 修复行),③类/skipped/degraded 走 console.warn(advisory 形态照
|
|
1329
|
+
* printVerifyRequiredEvidenceCheck 先例)。返回 true = ②类在场应阻断(调用方据此走
|
|
1330
|
+
* rollbackCompletionAndReturn,与 testCheck.status==='failed' 分支同形)。
|
|
1331
|
+
*
|
|
1332
|
+
* @param {object} r reconcileTargetFiles 返回值
|
|
1333
|
+
* @param {object} [envelope] 预构造信封(缺省现算;显式传入便于调用方先断言再打印)
|
|
1334
|
+
* @returns {boolean} true = 阻断(status='missing_declared')
|
|
1335
|
+
*/
|
|
1336
|
+
export function printReconcileTargetFilesCheck(r, envelope = buildReconcileTargetFilesEnvelope(r)) {
|
|
1337
|
+
// ②类(声明未做=计划落空):ERROR 阻断
|
|
1338
|
+
if (r.status === 'missing_declared') {
|
|
1339
|
+
console.error(`\n❌ target_files 对账阻断:${r.missing.length} 条「声明未做」(②类,计划落空)——声明的交付文件在实际改动中不存在。`)
|
|
1340
|
+
for (const m of r.missing) {
|
|
1341
|
+
console.error(` - ${m.task}: ${m.path}${m.isNew ? '(NEW: 声明新建,文件未创建)' : ''}`)
|
|
1342
|
+
}
|
|
1343
|
+
console.error(` evidence: matched=${envelope.evidence.matched_count} missing=${envelope.evidence.missing_declared_count} undeclared=${envelope.evidence.undeclared_count}`)
|
|
1344
|
+
for (const fix of envelope.supportedFixes) console.error(` 修复:${fix}`)
|
|
1345
|
+
console.error(` 对账口径 actual=${(r.sources || []).join(' + ') || 'n/a'};修复后重新完成 verify。`)
|
|
1346
|
+
return true
|
|
1347
|
+
}
|
|
1348
|
+
// ③类(做了没声明=scope creep):WARNING 放行,suspectTask 尽力归因随行列出
|
|
1349
|
+
if (r.status === 'undeclared') {
|
|
1350
|
+
console.warn(`\n⚠️ target_files 对账发现 ${r.undeclared.length} 个实际改动未在任何 task 卡声明(③类 scope creep,advisory 不阻断):`)
|
|
1351
|
+
for (const u of r.undeclared.slice(0, 20)) {
|
|
1352
|
+
console.warn(` - ${u.path}${u.suspectTask ? `(疑似 ${u.suspectTask} 的改动——该 task 的 review.json changedFiles 提及)` : ''}`)
|
|
1353
|
+
}
|
|
1354
|
+
if (r.undeclared.length > 20) console.warn(` …还有 ${r.undeclared.length - 20} 个`)
|
|
1355
|
+
console.warn(` evidence: matched=${envelope.evidence.matched_count} missing=${envelope.evidence.missing_declared_count} undeclared=${envelope.evidence.undeclared_count}`)
|
|
1356
|
+
for (const fix of envelope.supportedFixes) console.warn(` 提示:${fix}`)
|
|
1357
|
+
console.warn(` 对账口径 actual=${(r.sources || []).join(' + ') || 'n/a'}(③类误报先核对 sources 与并行会话剔除提示)。`)
|
|
1358
|
+
return false
|
|
1359
|
+
}
|
|
1360
|
+
// 降级(skipped/degraded):WARNING 放行,skipReason 一句话(存量卡/git 不可用,不误红)
|
|
1361
|
+
if (r.status === 'skipped' || r.status === 'degraded') {
|
|
1362
|
+
console.warn(`\n⚠️ target_files 对账${r.status === 'degraded' ? '降级' : '跳过'}(不阻断):${r.skipReason}`)
|
|
1363
|
+
return false
|
|
1364
|
+
}
|
|
1365
|
+
// ok:①交集全落地
|
|
1366
|
+
console.log(`\n✅ target_files 对账通过:${r.matched.length} 个声明文件全部落地${r.form ? `(actual 形态=${r.form})` : ''}`)
|
|
1367
|
+
for (const n of (r.notes || [])) console.log(` ℹ️ ${n}`)
|
|
1368
|
+
return false
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
/**
|
|
1372
|
+
* 探针一致性抽查结果 → 诊断信封(task-03 接线配套,形态对齐 buildReconcileTargetFilesEnvelope)。
|
|
1373
|
+
*
|
|
1374
|
+
* code 路由键(task-02 JSDoc 接线约定):mismatch+error → probe_consistency_mismatch(阻断,
|
|
1375
|
+
* 正文预填段疑似被篡改);mismatch+warning → probe_consistency_drift(probe3/probe5 环境敏感
|
|
1376
|
+
* 维度 + R-06 HEAD 前进降级,放行告警);skipped/degraded → probe_consistency_skipped(放行
|
|
1377
|
+
* 提示——两态对消费方语义一致:本次无对账结论,照 P3a 五状态→四 code 的合流先例);ok →
|
|
1378
|
+
* probe_consistency_ok。severity 分级照 reconcile 先例:阻断=error、非阻断非 ok=warning、ok=info。
|
|
1379
|
+
* evidence 携带 mismatch 计数(总/error/warning)+ subsections 在场性(task-02 返回值的诊断字段,
|
|
1380
|
+
* 判别子 D-003 排查先看它——「哪个探针子节在场」决定走逐探针对账还是 prefill 整段缺失分支)。
|
|
1381
|
+
*
|
|
1382
|
+
* @param {object} r checkProbeConsistency 返回值
|
|
1383
|
+
* @returns {{ code: string, name: string, severity: string, detail: string, evidence: object, supportedFixes: string[] }}
|
|
1384
|
+
*/
|
|
1385
|
+
export function buildProbeConsistencyEnvelope(r) {
|
|
1386
|
+
const blocked = r.status === 'mismatch' && r.severity === 'error'
|
|
1387
|
+
const drifted = r.status === 'mismatch' && !blocked
|
|
1388
|
+
const supportedFixes = []
|
|
1389
|
+
if (blocked) {
|
|
1390
|
+
supportedFixes.push('如实重生成:跑 `sillyspec verify-probes --change <变更名> --init` 重新落盘机械预填段,再基于新预填段如实补写正文结论(删改预填段过门是 Step7 防篡改纪律红线)')
|
|
1391
|
+
supportedFixes.push('若重生成后仍不符:核对 mismatch 的 expected(CLI 重跑指标)与 actual(正文锚点值)——确属环境变化(worktree 存活态/测试布局)时在 verify-result.md 注明差异原因')
|
|
1392
|
+
} else if (drifted) {
|
|
1393
|
+
supportedFixes.push('环境敏感维度漂移(probe3 测试布局 / probe5 扫描根 / probe6 HEAD 前进):跑 `sillyspec verify-probes --change <变更名> --init` 刷新预填段对齐当前环境(不阻断,留审计)')
|
|
1394
|
+
} else if (r.status === 'skipped' || r.status === 'degraded') {
|
|
1395
|
+
supportedFixes.push('新变更走 `sillyspec verify-probes --change <变更名> --init` 生成带探针预填段的骨架后再写结论;存量旧格式报告/无 changeName 跳过属预期,无需补')
|
|
1396
|
+
}
|
|
1397
|
+
return {
|
|
1398
|
+
// code 机器路由键(task-02 JSDoc):四状态→四 code,供平台/上游按 code 分支
|
|
1399
|
+
code: blocked ? 'probe_consistency_mismatch'
|
|
1400
|
+
: drifted ? 'probe_consistency_drift'
|
|
1401
|
+
: (r.status === 'ok' ? 'probe_consistency_ok' : 'probe_consistency_skipped'),
|
|
1402
|
+
name: 'probe_consistency',
|
|
1403
|
+
severity: blocked ? 'error' : (r.status === 'ok' ? 'info' : 'warning'),
|
|
1404
|
+
detail: `verify-result.md 探针预填段一致性抽查:status=${r.status}`
|
|
1405
|
+
+ (r.status === 'mismatch' ? `(${(r.mismatches || []).length} 项不符,severity=${r.severity})` : '')
|
|
1406
|
+
+ (r.skipReason ? `(${r.skipReason})` : ''),
|
|
1407
|
+
evidence: {
|
|
1408
|
+
mismatch_count: (r.mismatches || []).length,
|
|
1409
|
+
error_count: (r.mismatches || []).filter(m => m && m.severity === 'error').length,
|
|
1410
|
+
warning_count: (r.mismatches || []).filter(m => m && m.severity === 'warning').length,
|
|
1411
|
+
subsections: r.subsections ?? null,
|
|
1412
|
+
},
|
|
1413
|
+
supportedFixes,
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
/**
|
|
1418
|
+
* 探针一致性抽查结果落盘 .runtime/verify-runs/<ts>/probe-consistency-result.json(task-03)。
|
|
1419
|
+
*
|
|
1420
|
+
* 形态选型(二选一取独立文件):与 reconcile-result.json 同 gate 运行、同目录组织
|
|
1421
|
+
* (verify-runs/<ts>/,ts=UTC 紧凑 YYYYMMDDHHmmss 对齐 writeRunResult 先例),但不并入后者——
|
|
1422
|
+
* reconcile-result.json 的 schema 已被 P3a 消费方锁定,并入(读改写/扩参)引入两检查间耦合;
|
|
1423
|
+
* 独立文件各写各的回执,平台/审计按文件名订阅(与 test-result.json 同一目录约定)。
|
|
1424
|
+
* fail-soft:写失败只 console.error 留痕不阻断 gate(对齐 writeReconcileRunResult);四状态全落
|
|
1425
|
+
* (放行态同样留档——跳过/降级是审计事实,下次回看才知道为什么没有对账结论)。
|
|
1426
|
+
*
|
|
1427
|
+
* @param {{ runtimeRoot: string, changeName: string|null, envelope: object, result: object }} opts
|
|
1428
|
+
* @returns {string|null} 成功返回落盘路径;失败返回 null
|
|
1429
|
+
*/
|
|
1430
|
+
export function writeProbeConsistencyRunResult({ runtimeRoot, changeName, envelope, result }) {
|
|
1431
|
+
try {
|
|
1432
|
+
const ts = new Date().toISOString().slice(0, 19).replace(/[-T:]/g, '')
|
|
1433
|
+
const runDir = join(runtimeRoot, 'verify-runs', ts)
|
|
1434
|
+
mkdirSync(runDir, { recursive: true })
|
|
1435
|
+
const resultPath = join(runDir, 'probe-consistency-result.json')
|
|
1436
|
+
// 落盘字段 snake_case 对齐 reconcile-result.json 机器消费口径(信封内存字段保持 camelCase
|
|
1437
|
+
// 不动,写盘这层做一次映射);mismatches 逐条落盘供平台按 probe/severity 细读
|
|
1438
|
+
writeFileSync(resultPath, JSON.stringify({
|
|
1439
|
+
change: changeName,
|
|
1440
|
+
code: envelope.code,
|
|
1441
|
+
severity: envelope.severity,
|
|
1442
|
+
status: result.status,
|
|
1443
|
+
result_severity: result.severity ?? null,
|
|
1444
|
+
evidence: envelope.evidence,
|
|
1445
|
+
supported_fixes: envelope.supportedFixes,
|
|
1446
|
+
mismatches: result.mismatches || [],
|
|
1447
|
+
skip_reason: result.skipReason ?? null,
|
|
1448
|
+
subsections: result.subsections ?? null,
|
|
1449
|
+
ran_at: new Date().toISOString(),
|
|
1450
|
+
}, null, 2) + '\n')
|
|
1451
|
+
console.log(`📄 探针一致性抽查结果已写入: ${resultPath}`)
|
|
1452
|
+
return resultPath
|
|
1453
|
+
} catch (e) {
|
|
1454
|
+
console.error(`⚠️ 探针一致性抽查结果落盘失败(不影响 verify gate): ${e.message}`)
|
|
1455
|
+
return null
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
/**
|
|
1460
|
+
* 打印探针一致性抽查结果(task-03 接线配套的打印层,阻断语义同 printReconcileTargetFilesCheck)。
|
|
1461
|
+
*
|
|
1462
|
+
* ERROR 级 mismatch:console.error 状态行 + 逐条 mismatch(probe/expected/actual/note,reconcile
|
|
1463
|
+
* 先例同形)+ evidence 计数 + supportedFixes,返回 true(调用方据此走 rollbackCompletionAndReturn);
|
|
1464
|
+
* WARNING 级 mismatch:console.warn 同形明细返回 false 放行;skipped/degraded:一句 skipReason 放行
|
|
1465
|
+
* (对齐 reconcile 降级分支形态);ok:静默返回 false(task-02 JSDoc 接线约定「'ok' → 静默」——
|
|
1466
|
+
* 正文锚点全符是常态,不刷存在感)。
|
|
1467
|
+
*
|
|
1468
|
+
* @param {object} r checkProbeConsistency 返回值
|
|
1469
|
+
* @param {object} [envelope] 预构造信封(缺省现算;显式传入便于调用方先断言再打印)
|
|
1470
|
+
* @returns {boolean} true = 阻断(status='mismatch' 且 severity='error')
|
|
1471
|
+
*/
|
|
1472
|
+
export function printProbeConsistencyCheck(r, envelope = buildProbeConsistencyEnvelope(r)) {
|
|
1473
|
+
// ERROR 级 mismatch:正文预填段与 CLI 重跑事实不符,疑似篡改 → 阻断
|
|
1474
|
+
if (r.status === 'mismatch' && r.severity === 'error') {
|
|
1475
|
+
console.error(`\n❌ 探针一致性抽查阻断:${(r.mismatches || []).length} 项与 CLI 重跑事实不符(其中 ERROR 级 ${envelope.evidence.error_count} 项)——verify-result.md 探针预填段疑似被篡改。`)
|
|
1476
|
+
for (const m of (r.mismatches || [])) {
|
|
1477
|
+
console.error(` - ${m.probe}: 重跑 ${JSON.stringify(m.expected)} vs 正文 ${JSON.stringify(m.actual)}(${m.severity})——${m.note}`)
|
|
1478
|
+
}
|
|
1479
|
+
console.error(` evidence: mismatches=${envelope.evidence.mismatch_count}(error=${envelope.evidence.error_count} warning=${envelope.evidence.warning_count})subsections=${JSON.stringify(envelope.evidence.subsections)}`)
|
|
1480
|
+
for (const fix of envelope.supportedFixes) console.error(` 修复:${fix}`)
|
|
1481
|
+
console.error(` 对比基准是正文锚点(删 verify-facts.json 绕不过防护);如实重生成预填段后重新完成 verify。`)
|
|
1482
|
+
return true
|
|
1483
|
+
}
|
|
1484
|
+
// WARNING 级 mismatch:环境敏感维度漂移 → 放行告警
|
|
1485
|
+
if (r.status === 'mismatch') {
|
|
1486
|
+
console.warn(`\n⚠️ 探针一致性抽查发现 ${(r.mismatches || []).length} 项漂移(环境敏感维度,advisory 不阻断):`)
|
|
1487
|
+
for (const m of (r.mismatches || [])) {
|
|
1488
|
+
console.warn(` - ${m.probe}: 重跑 ${JSON.stringify(m.expected)} vs 正文 ${JSON.stringify(m.actual)}——${m.note}`)
|
|
1489
|
+
}
|
|
1490
|
+
console.warn(` evidence: mismatches=${envelope.evidence.mismatch_count}(error=${envelope.evidence.error_count} warning=${envelope.evidence.warning_count})`)
|
|
1491
|
+
for (const fix of envelope.supportedFixes) console.warn(` 提示:${fix}`)
|
|
1492
|
+
return false
|
|
1493
|
+
}
|
|
1494
|
+
// 降级(skipped/degraded):放行,skipReason 一句话(存量旧报告/重跑异常,不误红)
|
|
1495
|
+
if (r.status === 'skipped' || r.status === 'degraded') {
|
|
1496
|
+
console.warn(`\n⚠️ 探针一致性抽查${r.status === 'degraded' ? '降级' : '跳过'}(不阻断):${r.skipReason}`)
|
|
1497
|
+
return false
|
|
1498
|
+
}
|
|
1499
|
+
// ok:①正文锚点与重跑全符——静默放行
|
|
1500
|
+
return false
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1172
1503
|
/**
|
|
1173
1504
|
* verify 服务进程回收(verify --done 收尾调用)。
|
|
1174
1505
|
*
|
package/src/run/prompt.js
CHANGED
|
@@ -477,6 +477,35 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
477
477
|
}
|
|
478
478
|
}
|
|
479
479
|
|
|
480
|
+
// 机械事实底稿注入(IR P3c task-03,D-003@v1 三件套之三):brainstorm「加载项目上下文」Step2
|
|
481
|
+
// 的 {SCAN_FACTS} → docs/<project>/scan/_facts.md 全文(scan facts CLI 确定性抽取的端点/依赖/
|
|
482
|
+
// 规模底稿)+ 红线一句(措辞对齐 scan 子代理先例 stages/scan.js——禁止重新 grep 发现底稿覆盖
|
|
483
|
+
// 的机械事实,冲突以底稿为准),探索期直接消费省 token。>15KB 按 UTF-8 字节截断并提示跑
|
|
484
|
+
// `sillyspec scan facts` 刷新(design R-04);文件不存在/读取失败空注入(fail-soft,不留残留
|
|
485
|
+
// 占位符)。只挂 brainstorm:双重条件(stageName 判定 + 占位符仅存在于 brainstorm Step2
|
|
486
|
+
// prompt),与下方 buildModuleContextInjection(横跨 brainstorm/plan/execute 三阶段)相互独立。
|
|
487
|
+
if (stageName === 'brainstorm' && promptText.includes('{SCAN_FACTS}')) {
|
|
488
|
+
let factsInjected = ''
|
|
489
|
+
try {
|
|
490
|
+
const factsSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
491
|
+
const factsProjectName = dbProjectName || basename(cwd)
|
|
492
|
+
const factsPath = join(factsSpecBase, 'docs', factsProjectName, 'scan', '_facts.md')
|
|
493
|
+
if (existsSync(factsPath)) {
|
|
494
|
+
const factsBuf = readFileSync(factsPath) // Buffer:按 UTF-8 字节判 15KB 阈值
|
|
495
|
+
const FACTS_LIMIT = 15 * 1024
|
|
496
|
+
const factsBody = factsBuf.length > FACTS_LIMIT
|
|
497
|
+
? factsBuf.subarray(0, FACTS_LIMIT).toString('utf8') +
|
|
498
|
+
`\n\n(⚠️ 底稿超过 15KB 已截断——完整内容可读 \`${factsPath}\`;底稿已过期时可跑 \`sillyspec scan facts\` 刷新)`
|
|
499
|
+
: factsBuf.toString('utf8')
|
|
500
|
+
factsInjected =
|
|
501
|
+
'\n\n### 🧾 机械事实底稿(scan facts CLI 抽取,docs/' + factsProjectName + '/scan/_facts.md 全文注入)\n\n' +
|
|
502
|
+
factsBody +
|
|
503
|
+
'\n\n⛔ 红线(同 scan 子代理先例):禁止重新 grep 发现底稿覆盖的机械事实(端点/依赖/规模),冲突以底稿为准。\n'
|
|
504
|
+
}
|
|
505
|
+
} catch { /* fail-soft:读取失败空注入,不阻断 prompt 输出 */ }
|
|
506
|
+
promptText = promptText.replace(/\{SCAN_FACTS\}/g, factsInjected)
|
|
507
|
+
}
|
|
508
|
+
|
|
480
509
|
// 决策防复潮注入(W1.1 第 3 点,task-04,FR-05):brainstorm「加载项目上下文」Step2 的
|
|
481
510
|
// {DECISION_HITS} → matchKnowledge decisionHits——命中 rejected 条目时渲染「否决决策提示」段
|
|
482
511
|
// (ID/标题/否决理由/复潮条件),无命中替换为空串零输出(不留残留占位符);注入结果同步落
|
|
@@ -683,6 +712,47 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
683
712
|
}
|
|
684
713
|
}
|
|
685
714
|
|
|
715
|
+
// 三主阶段 Step1「进度确认」快照注入(2026-09-05 全流程审计):progress show 的核心信息
|
|
716
|
+
// (阶段/步骤位置/推进态)CLI 渲染 prompt 时已全部在手——注入替代 agent 跑一趟 CLI 的
|
|
717
|
+
// 工具往返(progress show 全量输出还很长)。fail-soft:异常降级为自查指引。
|
|
718
|
+
if (['brainstorm', 'execute', 'verify'].includes(stageName) && promptText.includes('{PROGRESS_SNAPSHOT}')) {
|
|
719
|
+
try {
|
|
720
|
+
const done = steps.filter(s => s.status === 'completed').length
|
|
721
|
+
const waiting = steps.filter(s => s.status === 'waiting').length
|
|
722
|
+
const snapshot =
|
|
723
|
+
`- 变更:${changeName || '(未知)'}\n` +
|
|
724
|
+
`- 阶段:${stageName}(CLI 已路由到本阶段,无需再跑 progress show 确认)\n` +
|
|
725
|
+
`- 步骤:${done}/${steps.length} 已完成${waiting > 0 ? `,${waiting} 个等待中` : ''};当前推进到第 ${stepIndex + 1} 步`
|
|
726
|
+
promptText = promptText.split('{PROGRESS_SNAPSHOT}').join(snapshot)
|
|
727
|
+
} catch (e) {
|
|
728
|
+
promptText = promptText.split('{PROGRESS_SNAPSHOT}').join('(快照注入失败:' + e.message + '——可运行 sillyspec progress show 自查)')
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// execute Step「确认 worktree 路径」meta 注入(2026-09-05 全流程审计):worktree meta 由 CLI
|
|
733
|
+
// 读出直接注入,agent 省一趟 worktree meta 工具往返;工具链可用性检查(真实工作)仍归 agent。
|
|
734
|
+
if (stageName === 'execute' && promptText.includes('{WORKTREE_META}')) {
|
|
735
|
+
let metaInfo
|
|
736
|
+
try {
|
|
737
|
+
const { WorktreeManager } = await import('../worktree.js')
|
|
738
|
+
const wm = new WorktreeManager({ cwd })
|
|
739
|
+
const meta = changeName ? wm.getMeta(changeName) : null
|
|
740
|
+
if (meta && meta.worktreePath) {
|
|
741
|
+
const dirOk = existsSync(meta.worktreePath)
|
|
742
|
+
metaInfo =
|
|
743
|
+
`- worktreePath:${meta.worktreePath}\n` +
|
|
744
|
+
`- branch:${meta.branch || '(meta 未记录)'}\n` +
|
|
745
|
+
`- mode:${meta.mode || 'worktree'}\n` +
|
|
746
|
+
`- worktree 目录存在:${dirOk ? '是' : '否——in-place 模式属正常;worktree/native-worktree 模式缺失则停止并报错'}`
|
|
747
|
+
} else {
|
|
748
|
+
metaInfo = '(worktree meta 不可读——meta.json 不存在说明创建失败,停止并报错;可运行 sillyspec worktree meta ' + (changeName || '<change-name>') + ' 自查)'
|
|
749
|
+
}
|
|
750
|
+
} catch (e) {
|
|
751
|
+
metaInfo = '(注入失败:' + e.message + '——运行 sillyspec worktree meta 自查,meta.json 不存在则停止并报错)'
|
|
752
|
+
}
|
|
753
|
+
promptText = promptText.split('{WORKTREE_META}').join(metaInfo)
|
|
754
|
+
}
|
|
755
|
+
|
|
686
756
|
// archive Step1「任务完成度检查」客观真相源注入:以 review.json verdict 算完成度,
|
|
687
757
|
// 替代「机械数 plan.md checkbox」(checkbox 依赖 autoCheckPlanFromReviews 回填,runId marker /
|
|
688
758
|
// review 缺失时回填静默 no-op,会停在未勾态导致完成度失真 → archive 误判「全未完成」)。
|
package/src/run/scan-profile.js
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* - executeScanPostcheck 原冗余动态 import('fs'/'path'/'child_process') 删除,改顶部静态(execSync 实际未直接用,纯遗留)
|
|
10
10
|
*/
|
|
11
11
|
import { join, basename, extname } from 'node:path'
|
|
12
|
-
import { existsSync, readdirSync, statSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
12
|
+
import { existsSync, readdirSync, statSync, mkdirSync, writeFileSync, unlinkSync } from 'node:fs'
|
|
13
13
|
import { safeGit } from './shared.js'
|
|
14
|
+
import { SCAN_REQUIRED_DOCS } from '../constants.js'
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* 估算源码规模(文件数 + 字节数)—— 跨平台 Node 遍历,替代 Unix `find`(原生 Windows 无 find,
|
|
@@ -130,19 +131,21 @@ export function buildQuickScanSteps() {
|
|
|
130
131
|
当前为 quick profile(小项目自动判定 或 用户显式 --quick),一次性生成核心文档用于快速接入。
|
|
131
132
|
|
|
132
133
|
### 操作
|
|
133
|
-
1.
|
|
134
|
-
2.
|
|
134
|
+
1. 先跑 \`sillyspec scan facts\`(CLI 确定性抽取端点/依赖/规模,产物 {DOCS_ROOT}/scan/_facts.md,零 token)——PROJECT/ARCHITECTURE 的技术栈与集成事实以底稿为准,不重复 grep 发现
|
|
135
|
+
2. 读取项目结构和关键文件(package.json / pyproject.toml / README / 入口文件)
|
|
136
|
+
3. 生成以下 4 份文档并写入 \`{DOCS_ROOT}/scan/\`:
|
|
135
137
|
- **PROJECT.md** — 项目简介、技术栈、模块划分
|
|
136
138
|
- **ARCHITECTURE.md** — 架构概览、模块关系、技术决策
|
|
137
139
|
- **CONVENTIONS.md** — 代码风格、框架隐形规则
|
|
138
140
|
- **STRUCTURE.md** — 目录树 + 模块说明
|
|
139
|
-
|
|
141
|
+
4. 如发现子项目,注册到 \`{PROJECTS_ROOT}/\` 下
|
|
140
142
|
|
|
141
|
-
|
|
143
|
+
每份文档正文第 1 行直接写中文标题;frontmatter(author / created_at / source_commit / updated_at / generator / scan_depth: quick——标记快速接入的浅层版本,后续深度扫描 --deep 会识别此标记并覆盖升级)**由 CLI 在终检时自动注入,你不需要写 frontmatter、也不要跑 git 取值**。
|
|
142
144
|
|
|
143
145
|
### ⛔ 硬约束
|
|
144
146
|
- **严禁使用子代理(Agent/Task 工具)。** 所有文档在一个 turn 内完成。
|
|
145
147
|
- 不要搜索 .sillyspec/ .claude/ .git/ node_modules/ dist/ build/
|
|
148
|
+
- 关键事实(模块职责/技术栈组件/外部集成)须带反引号 path:line 引用(如 \`src/db.js:42\`,真实可核验——CLI postcheck 逐条核验);不确定就不写行号,**禁止编造**
|
|
146
149
|
- --output 只需要列出生成的文件名,不要写长篇总结
|
|
147
150
|
|
|
148
151
|
### 输出
|
|
@@ -219,8 +222,11 @@ export async function executeScanPreflight(cwd, platformOpts, scanProfile) {
|
|
|
219
222
|
*/
|
|
220
223
|
export async function executeScanPostcheck(cwd, platformOpts, scanProfile) {
|
|
221
224
|
// scan-postcheck.js 在 src/,本模块在 src/run/ → 退一层(真环依赖,保留动态 import)
|
|
222
|
-
const { runScanPostCheck, printScanPostCheckResult } = await import('../scan-postcheck.js')
|
|
225
|
+
const { runScanPostCheck, printScanPostCheckResult, stampScanDocHeaders } = await import('../scan-postcheck.js')
|
|
223
226
|
const specDir = platformOpts?.specRoot || null
|
|
227
|
+
// ② frontmatter CLI 盖章(幂等,只补缺不覆盖):quick 档注入含 scan_depth: quick
|
|
228
|
+
const stamped = stampScanDocHeaders({ cwd, specDir, mode: scanProfile?.mode })
|
|
229
|
+
if (stamped.fixed.length > 0) console.log(` 📝 frontmatter 已由 CLI 注入 ${stamped.fixed.length} 份文档`)
|
|
224
230
|
const result = runScanPostCheck({
|
|
225
231
|
cwd,
|
|
226
232
|
specDir,
|
|
@@ -268,3 +274,89 @@ export async function executeScanPostcheck(cwd, platformOpts, scanProfile) {
|
|
|
268
274
|
}
|
|
269
275
|
}
|
|
270
276
|
}
|
|
277
|
+
|
|
278
|
+
// ─── noAI 化步骤动作(2026-09-05 ①:ls 级操作不值整轮 agent 往返) ─────────────
|
|
279
|
+
|
|
280
|
+
/** 已注册项目清单(projects/*.yaml 文件名去后缀);无注册时回退 [仓库根 basename](单项目口径) */
|
|
281
|
+
function listRegisteredProjects(specBase, cwd) {
|
|
282
|
+
const projectsRoot = join(specBase, 'projects')
|
|
283
|
+
try {
|
|
284
|
+
const names = readdirSync(projectsRoot).filter(f => f.endsWith('.yaml')).map(f => f.replace(/\.yaml$/, ''))
|
|
285
|
+
if (names.length > 0) return names
|
|
286
|
+
} catch { /* 无 projects 目录 → 回退单项目 */ }
|
|
287
|
+
return [basename(cwd)]
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* CLI-only: 子项目探测(scan 步骤 1 的 noAI 化)。
|
|
292
|
+
* 顶层目录 × 构建文件存在性 → 打印建议清单;**只建议不注册**(原红线语义不变,注册仍由用户确认)。
|
|
293
|
+
*/
|
|
294
|
+
export async function executeScanDetectProjects(cwd, platformOpts) {
|
|
295
|
+
const specBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
296
|
+
const projectsRoot = join(specBase, 'projects')
|
|
297
|
+
const buildFiles = [
|
|
298
|
+
['package.json', 'Node.js'], ['pom.xml', 'Java/Maven'], ['build.gradle', 'Java/Gradle'],
|
|
299
|
+
['pyproject.toml', 'Python'], ['requirements.txt', 'Python'], ['go.mod', 'Go'], ['Cargo.toml', 'Rust'],
|
|
300
|
+
]
|
|
301
|
+
const skip = new Set(['node_modules', '.git', '.sillyspec', 'dist', 'build', 'out', 'target', 'coverage', '.claude'])
|
|
302
|
+
console.log(' 📁 子项目探测(CLI,只建议不注册——注册需用户在后续步骤确认)')
|
|
303
|
+
let found = 0
|
|
304
|
+
try {
|
|
305
|
+
for (const e of readdirSync(cwd, { withFileTypes: true })) {
|
|
306
|
+
if (!e.isDirectory() || skip.has(e.name) || e.name.startsWith('.')) continue
|
|
307
|
+
const hit = buildFiles.find(([f]) => existsSync(join(cwd, e.name, f)))
|
|
308
|
+
if (!hit) continue
|
|
309
|
+
found++
|
|
310
|
+
const registered = existsSync(join(projectsRoot, `${e.name}.yaml`))
|
|
311
|
+
console.log(` - ${e.name}:${hit[1]} · ${registered ? '已注册' : '未注册(是否注册由用户确认)'}`)
|
|
312
|
+
}
|
|
313
|
+
} catch { /* 顶层目录读取失败 → 空清单 */ }
|
|
314
|
+
if (found === 0) console.log(' (未发现独立子项目,按单项目扫描)')
|
|
315
|
+
console.log('')
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* CLI-only: 断点续扫检测(scan 步骤 4 的 noAI 化)。existsSync 级检查,一次遍历全部已注册项目。
|
|
320
|
+
*/
|
|
321
|
+
export async function executeScanResumeCheck(cwd, platformOpts) {
|
|
322
|
+
const specBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
323
|
+
for (const p of listRegisteredProjects(specBase, cwd)) {
|
|
324
|
+
const scanDir = join(specBase, 'docs', p, 'scan')
|
|
325
|
+
const states = SCAN_REQUIRED_DOCS.map(d => existsSync(join(scanDir, d)) ? `✅${d.replace('.md', '')}` : `⬜${d.replace('.md', '')}`)
|
|
326
|
+
const facts = existsSync(join(scanDir, '_facts.md')) ? '· 底稿✅' : '· 底稿⬜'
|
|
327
|
+
console.log(` 📂 ${p}: ${states.join(' ')} ${facts}`)
|
|
328
|
+
}
|
|
329
|
+
console.log('')
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* CLI-only: scan 终检(步骤 11「自检和提交」的 noAI 化,含 ② frontmatter 盖章)。
|
|
334
|
+
* 顺序:frontmatter 注入(幂等)→ 清理 _env-detect.md → runScanPostCheck(本地轻量/平台严格)
|
|
335
|
+
* → failed_post_check 时 throw(step 保持 pending)→ 非平台 git add 扫描产物(不 commit)。
|
|
336
|
+
*/
|
|
337
|
+
export async function executeScanFinalize(cwd, platformOpts) {
|
|
338
|
+
const specDir = platformOpts?.specRoot || null
|
|
339
|
+
const specBase = specDir || join(cwd, '.sillyspec')
|
|
340
|
+
const { stampScanDocHeaders, runScanPostCheck, printScanPostCheckResult } = await import('../scan-postcheck.js')
|
|
341
|
+
|
|
342
|
+
const stamped = stampScanDocHeaders({ cwd, specDir })
|
|
343
|
+
if (stamped.fixed.length > 0) console.log(` 📝 frontmatter 已由 CLI 注入 ${stamped.fixed.length} 份文档(${stamped.stampedKeys} 个键,只补缺不覆盖)`)
|
|
344
|
+
|
|
345
|
+
for (const p of listRegisteredProjects(specBase, cwd)) {
|
|
346
|
+
const tmp = join(specBase, 'docs', p, 'scan', '_env-detect.md')
|
|
347
|
+
try { if (existsSync(tmp)) { unlinkSync(tmp); console.log(` 🧹 已清理 ${p}/scan/_env-detect.md`) } } catch { /* 清理失败不阻断 */ }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const result = runScanPostCheck({ cwd, specDir })
|
|
351
|
+
printScanPostCheckResult(result)
|
|
352
|
+
// 不 throw:平台模式的 manifest/指针/exit(1) 由 handleScanStageCompleted 在阶段完成时统一落
|
|
353
|
+
//(throw 会拦在完成前 → manifest.json 永不写入,破坏 SillyHub 消费契约,实测
|
|
354
|
+
// run-complete-step-scan-platform Case 1);本地模式的告警打印/postcheck-result.json 同理
|
|
355
|
+
// 由 complete-handlers 非平台收尾分支负责。本动作只负责:盖章 + 清理 + 即时报告 + 暂存。
|
|
356
|
+
|
|
357
|
+
if (!specDir) {
|
|
358
|
+
const r = safeGit(cwd, ['add', '.sillyspec/docs', '.sillyspec/knowledge'])
|
|
359
|
+
if (r && r.error) console.warn(` ⚠️ git add 扫描产物失败(可手动处理)`)
|
|
360
|
+
else console.log(' 📦 已暂存扫描产物 .sillyspec/docs 与 .sillyspec/knowledge(未提交,由统一提交工具处理)')
|
|
361
|
+
}
|
|
362
|
+
}
|
package/src/run/shared.js
CHANGED
|
@@ -614,13 +614,24 @@ export function resolveChangeDir(cwd, progress, specDir = null) {
|
|
|
614
614
|
// 仍在跑,平台可能已接受推送而客户端当作超时放弃(spec 树推送无 base_ts 自愈兜底)。
|
|
615
615
|
const SYNC_TOTAL_TIMEOUT_MS = 8_000
|
|
616
616
|
|
|
617
|
+
// 熔断总预算 env 开关(坑 spec-sync-aborted-looks-exception / 债 E22b 场景实证,2026-09-07):
|
|
618
|
+
// 真实平台执行环境后端偶发 >8s 响应,8s 熔断间歇触发(warn 文案见 spec-sync.js
|
|
619
|
+
// describeSyncError)。SILLYSPEC_SYNC_TIMEOUT_MS 允许平台环境放宽总预算;代价是 --done
|
|
620
|
+
// 最坏等待同比例变长(e2c9f90 治的体感 hang 需自行权衡)。整数毫秒,[1000, 120000],
|
|
621
|
+
// 非法/越界一律回退默认 8s。raceWithAbort 默认参数逐次求值——进程内改 env 即时生效。
|
|
622
|
+
export function resolveSyncTotalTimeoutMs() {
|
|
623
|
+
const raw = Number(process.env.SILLYSPEC_SYNC_TIMEOUT_MS)
|
|
624
|
+
if (Number.isInteger(raw) && raw >= 1_000 && raw <= 120_000) return raw
|
|
625
|
+
return SYNC_TOTAL_TIMEOUT_MS
|
|
626
|
+
}
|
|
627
|
+
|
|
617
628
|
/**
|
|
618
629
|
* 熔断 race + 确定性取消:超时触发 controller.abort() 并 resolve,在飞 fetch 收到
|
|
619
630
|
* AbortError 后自然失败(各调用方 catch 返回降级值,进程不挂起)。
|
|
620
631
|
* @param {() => Promise} op 接收 { signal } 的异步操作
|
|
621
632
|
* @param {number} timeoutMs 熔断时长(测试可缩短)
|
|
622
633
|
*/
|
|
623
|
-
async function raceWithAbort(op, timeoutMs =
|
|
634
|
+
async function raceWithAbort(op, timeoutMs = resolveSyncTotalTimeoutMs()) {
|
|
624
635
|
const controller = new AbortController()
|
|
625
636
|
let timer
|
|
626
637
|
try {
|
package/src/run/stage.js
CHANGED
|
@@ -20,7 +20,7 @@ import { join, dirname } from 'node:path'
|
|
|
20
20
|
import { existsSync, readdirSync, readFileSync, mkdirSync } from 'node:fs'
|
|
21
21
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
22
22
|
import { resolveSpecDir, resolveChangeDir, resolveRuntimeRoot, resolveQuickSessionsDir, triggerSync, safeGit, parsePorcelainPath, formatWaitOptions, checkApproval, getStageSteps, warnApprovalUnknown, predictProtectedQuickFiles, mergeQuickBoundaryFiles, detectEmptyShellQuickSessions } from './shared.js'
|
|
23
|
-
import { computeScanProfile, applyScanProfileSteps, executeScanPreflight, executeScanPostcheck } from './scan-profile.js'
|
|
23
|
+
import { computeScanProfile, applyScanProfileSteps, executeScanPreflight, executeScanPostcheck, executeScanDetectProjects, executeScanResumeCheck, executeScanFinalize } from './scan-profile.js'
|
|
24
24
|
import { outputStep, collectStageWaitHistory } from './prompt.js'
|
|
25
25
|
import { allocateQuicklogEntry, deriveTitleFromLinkedChange, sanitizeDesc } from '../quicklog.js'
|
|
26
26
|
import { createHash } from 'node:crypto'
|
|
@@ -529,6 +529,12 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
|
|
|
529
529
|
await executeScanPreflight(cwd, platformOpts, scanProfile)
|
|
530
530
|
} else if (cliAction === 'scanPostcheck') {
|
|
531
531
|
await executeScanPostcheck(cwd, platformOpts, scanProfile)
|
|
532
|
+
} else if (cliAction === 'scanDetectProjects') {
|
|
533
|
+
await executeScanDetectProjects(cwd, platformOpts)
|
|
534
|
+
} else if (cliAction === 'scanResumeCheck') {
|
|
535
|
+
await executeScanResumeCheck(cwd, platformOpts)
|
|
536
|
+
} else if (cliAction === 'scanFinalize') {
|
|
537
|
+
await executeScanFinalize(cwd, platformOpts)
|
|
532
538
|
} else if (cliAction === 'planPostcheck') {
|
|
533
539
|
await executePlanPostcheck(cwd, platformOpts, progress)
|
|
534
540
|
} else {
|