sillyspec 3.29.4 → 3.29.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/assets/command-cards/run-archive.md +26 -0
- package/assets/command-cards/run-brainstorm.md +33 -0
- package/assets/command-cards/run-execute.md +29 -0
- package/assets/command-cards/run-plan.md +27 -0
- package/assets/command-cards/run-quick.md +34 -0
- package/assets/command-cards/run-verify.md +29 -0
- package/assets/command-cards/status.md +27 -0
- package/package.json +1 -1
- package/round4/account.mjs +85 -0
- package/round4/brief-autocompact.md +34 -0
- package/round4/brief-session-replay.md +50 -0
- package/round4/deep-read-synthesis.md +38 -0
- package/round4/extract-full.mjs +38 -0
- package/round4/extract-timeline.mjs +43 -0
- package/round4/find-sessions.mjs +13 -0
- package/round4/full-r4-l.md +4707 -0
- package/round4/full-r4-o-l.md +4300 -0
- package/round4/full-r4-os.md +1681 -0
- package/round4/full-r4-sf.md +4255 -0
- package/round4/full-r4-sq.md +1734 -0
- package/round4/gate-audit-relay-receipt.md +30 -0
- package/round4/gate-value-audit.md +52 -0
- package/round4/prereg.md +67 -0
- package/round4/prompt-R4-L.md +63 -0
- package/round4/prompt-R4-O-L.md +59 -0
- package/round4/prompt-R4-O-S.md +56 -0
- package/round4/prompt-R4-S-F.md +60 -0
- package/round4/prompt-R4-S-Q.md +66 -0
- package/round4/r4-os-timeline.txt +297 -0
- package/round4/r4-sf-timeline.txt +738 -0
- package/round4/r4-sq-timeline.txt +322 -0
- package/round4/r4l-timeline.txt +793 -0
- package/round4/r4ol-timeline.txt +729 -0
- package/round4/r4sf-timeline.txt +852 -0
- package/round4/result-S.md +171 -0
- package/round4/result-final.md +46 -0
- package/round4/result-quality.md +63 -0
- package/src/command-cards.js +73 -0
- package/src/contract-matrix.js +26 -5
- package/src/diagnostic-codes.js +2 -1
- package/src/index.js +33 -6
- package/src/init.js +25 -8
- package/src/machine-interface.js +88 -3
- package/src/module-impact.js +51 -22
- package/src/run/command.js +4 -0
- package/src/run/complete-handlers.js +36 -1
- package/src/run/complete.js +6 -3
- package/src/run/gate-snapshot.js +10 -4
- package/src/run/gates.js +114 -37
- package/src/run/quick-audit.js +28 -2
- package/src/run/shared.js +3 -1
- package/src/stages/execute.js +2 -2
- package/src/stages/plan-postcheck.js +33 -19
- package/src/taskcard-frontmatter.js +57 -0
- package/src/verify-postcheck.js +59 -1
- package/src/verify-probes.js +45 -23
package/src/run/gates.js
CHANGED
|
@@ -861,6 +861,47 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
861
861
|
} catch { /* advisory 失败不阻断完成(fail-soft,facade 预检先例同款) */ }
|
|
862
862
|
}
|
|
863
863
|
|
|
864
|
+
// ── fail-fast 前置:纯文档 blocking 检查先于 test/lint 实测门(R4-S-F 实证,2026-09-21)──
|
|
865
|
+
// module-impact 死信与预填注清零 error 门只读文档零外部执行,原先排在 9 分钟级 test+lint
|
|
866
|
+
// 实测门之后——文档未清时每轮 --done 都先白烧一轮门禁再被拦(R4-S-F 166min 的主要形状)。
|
|
867
|
+
// 前置到实测门之前:秒级失败秒级返工,贵的门只在文档面已清后才跑。
|
|
868
|
+
if (stageName === 'verify' && changeName) {
|
|
869
|
+
// module-impact 死信探针(blocking,债单 D-1/D-5):更新结果表 pending/待办行 → 阻断 verify。
|
|
870
|
+
// 与 archive 移动前校验(extractPendingDocSyncRows)同一口径,把死信号从 archive 提前到 verify:
|
|
871
|
+
// agent 在 verify 阶段就须完成文档同步并回填 done/skipped,而非拖到归档被拦。
|
|
872
|
+
const verifyChangeDir = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
|
|
873
|
+
if (verifyChangeDir) {
|
|
874
|
+
const impactPath = join(verifyChangeDir, 'module-impact.md')
|
|
875
|
+
if (existsSync(impactPath)) {
|
|
876
|
+
const { extractPendingDocSyncRows } = await import('./complete-handlers.js')
|
|
877
|
+
const pendingRows = extractPendingDocSyncRows(readFileSync(impactPath, 'utf8'))
|
|
878
|
+
if (pendingRows.length > 0) {
|
|
879
|
+
console.error(`\n❌ verify 阶段被阻断:module-impact.md「更新结果」表存在 ${pendingRows.length} 个未清 pending/待办项(死信)`)
|
|
880
|
+
for (const row of pendingRows) console.error(` - ${row}`)
|
|
881
|
+
console.error(' 文档同步是 verify 的收尾义务:请完成模块文档同步并回填状态为 done/skipped(说明原因),再重新完成 verify。')
|
|
882
|
+
return rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-contract' })
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
// 预填注清零 error 门(归档前最后一道;2026-09-18-artifact-prefill task-03 / FR-03 / D-003@v1
|
|
887
|
+
// 门禁梯度·error 档):白名单槽宿主文件(design.md + tasks/task-*.md)仍含未删预填注 →
|
|
888
|
+
// 阻断 verify 完成。brainstorm/plan --done 的 advisory 在此收口为硬门,注在场=预填未确认。
|
|
889
|
+
// 检测单点 = runProbe10PrefillNoteClearance(不二算)。已知误报面:散文引用注字面量会命中
|
|
890
|
+
// ——修法是核对后删注/改写散文措辞,不是绕门。
|
|
891
|
+
try {
|
|
892
|
+
const { runProbe10PrefillNoteClearance } = await import('../verify-probes.js')
|
|
893
|
+
const noteClearance = runProbe10PrefillNoteClearance({ specBase, changeName })
|
|
894
|
+
if (noteClearance.unclearedFiles.length > 0) {
|
|
895
|
+
console.error(`\n❌ verify 阶段被阻断:预填注清零校验未过(${noteClearance.unclearedFiles.length} 个文件的白名单槽仍含未删预填注——预填≠结论,注在场=未确认):`)
|
|
896
|
+
for (const f of noteClearance.unclearedFiles) console.error(` - ${f}`)
|
|
897
|
+
console.error(' 修复:逐槽核对预填值后删除行尾「(预填:核对后删本注)」注(删注=确认动作);散文含注字面量的命中改写该处措辞。清零后重新完成 verify(进度不丢)。')
|
|
898
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-contract' })
|
|
899
|
+
}
|
|
900
|
+
} catch (e) {
|
|
901
|
+
console.warn(`⚠️ 预填注清零校验异常(降级放行,fail-soft——异常不是未清零,ceremony 双跑同款处置): ${(e && e.message) || e}`)
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
864
905
|
// verify 产物校验通过 + 结论非 FAIL(否则上面已阻断)。
|
|
865
906
|
// 再由 CLI 亲自执行 local.yaml 的测试命令,与 verify-result.md 的自报告对账:
|
|
866
907
|
// 自报告 PASS 但实测失败 → 阻断(防止"文案通过"绕过验证)。
|
|
@@ -937,6 +978,20 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
937
978
|
}
|
|
938
979
|
} catch { /* fail-soft:回填失败不影响门禁 */ }
|
|
939
980
|
if (testCheck.status === 'failed') {
|
|
981
|
+
// ── 纯超时降档(R4 门禁价值考古 2026-09-21:227 跑 106 败里 36 次 600s 帽杀纯超时假拦
|
|
982
|
+
// =失败桶最大单一来源)。超时=未完成非测试挂——所有失败单元 reason 均含超时 → advisory
|
|
983
|
+
// 放行带处置指引;任一单元真实挂测/退出码 → 维持原硬拦(真拦 12 次的防线不动)。──
|
|
984
|
+
let timeoutOnly = false
|
|
985
|
+
try {
|
|
986
|
+
const { isTimeoutOnlyTestFailure } = await import('../verify-postcheck.js')
|
|
987
|
+
timeoutOnly = isTimeoutOnlyTestFailure(testCheck)
|
|
988
|
+
} catch { /* 判定链异常维持硬拦(保守) */ }
|
|
989
|
+
if (timeoutOnly) {
|
|
990
|
+
if (verifyGateSnap) { try { verifyGateSnap.cleanup() } catch {} ; verifyGateSnap = null }
|
|
991
|
+
console.warn('\n⚠️ Verify 实测门未在超时帽内完成(纯超时,无任何挂掉的测试)——降档 advisory 不阻断:')
|
|
992
|
+
console.warn(` ${testCheck.reason || ''}`)
|
|
993
|
+
console.warn(' 处置:① local.yaml 配置 modules: 块(name: { path, test })启用模块子集实测——触碰模块全套测试,时长与超时同步收窄;② 定向复跑触碰模块自证;③ 确需长跑设 SILLYSPEC_TEST_TIMEOUT_MS。')
|
|
994
|
+
} else {
|
|
940
995
|
if (verifyGateSnap) {
|
|
941
996
|
try { const { printSnapshotFailureHint } = await import('./gate-snapshot.js')
|
|
942
997
|
printSnapshotFailureHint(verifyGateSnap) } catch { /* 提示失败不影响阻断 */ }
|
|
@@ -953,6 +1008,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
953
1008
|
} catch { /* 归因提示失败不影响阻断语义 */ }
|
|
954
1009
|
console.error(' 请修复失败的测试并更新 verify-result.md 后重新完成此步骤。')
|
|
955
1010
|
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-test' })
|
|
1011
|
+
} // else(纯超时降档分支)结束——真实测试失败分支的 rollback 语义不变
|
|
956
1012
|
}
|
|
957
1013
|
// lint 对账(2026-08-21 审查 CLI-1):test 侧"自报告 PASS 但实测失败→阻断"已闭环,
|
|
958
1014
|
// lint 侧此前纯口头——CLI 亲自执行 commands.lint,advisory 起步(失败打印不阻断,观察期后升级)
|
|
@@ -983,6 +1039,22 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
983
1039
|
// 逃生 SILLYSPEC_VERIFY_LINT_GATE=advisory)
|
|
984
1040
|
const { shouldBlockVerifyLint } = await import('../verify-postcheck.js')
|
|
985
1041
|
if (shouldBlockVerifyLint(lintCheck)) {
|
|
1042
|
+
// ── lint 归属鉴定降档(R4-S-F 实证:verify 侧 lint 硬门 7 轮全是 HEAD 存量债——隔离
|
|
1043
|
+
// 快照含基线债文件时恒败,agent 被逼范围外清偿或逃生口)。失败输出提及文件 × 本变更
|
|
1044
|
+
// 文件集零交集 → 存量债 advisory 降档放行(债非本变更引入,硬拦只是制造范围外功);
|
|
1045
|
+
// 有交集 / 输出无可识别路径(unattributable,纯配置错误类)→ 维持硬拦(保守)。──
|
|
1046
|
+
let lintOwnership = null
|
|
1047
|
+
try {
|
|
1048
|
+
const { triageLintOwnership, resolveVerifyChangedFiles } = await import('../verify-postcheck.js')
|
|
1049
|
+
const lintScopeFiles = (resolveVerifyChangedFiles(cwd, changeName, null, { includeWorkingTree: true, specBase }) || [])
|
|
1050
|
+
.map(f => String(f).replace(/\\/g, '/')).filter(f => !f.startsWith('.sillyspec/'))
|
|
1051
|
+
lintOwnership = triageLintOwnership({ failureFiles: lintCheck.failureFiles || [], changeFiles: lintScopeFiles })
|
|
1052
|
+
} catch { /* 鉴定链异常维持硬拦(保守) */ }
|
|
1053
|
+
if (lintOwnership && lintOwnership.verdict === 'pre-existing') {
|
|
1054
|
+
console.warn('\n⚠️ Verify lint 实测失败,但归属鉴定为 HEAD 存量债(失败提及文件与本变更文件集零交集)——降档 advisory 不阻断 verify 完成:')
|
|
1055
|
+
console.warn(` 存量债文件:${(lintCheck.failureFiles || []).join('、') || '(见上方输出)'}`)
|
|
1056
|
+
console.warn(' 处置建议:单独 quick 机械清偿(顺手清债解锁全仓 lint 门),或本变更确认无关时设 SILLYSPEC_VERIFY_LINT_GATE=advisory(审计留痕)。债随基线存在,与本变更交付无关。')
|
|
1057
|
+
} else {
|
|
986
1058
|
console.error('\n❌ verify 阶段被阻断:CLI 亲自实测 lint 失败(agent 的 lint 自报告与实测不符时以实测为准)。')
|
|
987
1059
|
if (usedSnapInfo) {
|
|
988
1060
|
try { const { printSnapshotFailureHint } = await import('./gate-snapshot.js')
|
|
@@ -1006,6 +1078,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
1006
1078
|
}
|
|
1007
1079
|
console.error(' 修复后重跑 --done(进度不丢);确认要跳过实测请设 SILLYSPEC_VERIFY_LINT_GATE=advisory(审计留痕)。')
|
|
1008
1080
|
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-lint' })
|
|
1081
|
+
} // else(pre-existing 降档分支)结束——维持硬拦分支的 rollback 语义不变
|
|
1009
1082
|
}
|
|
1010
1083
|
// 契约 parity 对账:扫前端 API 调用 vs execute 提取的 provider endpoint artifact。
|
|
1011
1084
|
// 接线自 contract-matrix pipeline(verifyApiParity 的 CLI 入口)。
|
|
@@ -1076,6 +1149,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
1076
1149
|
// 放行态),fail-soft 不影响 gate 判定;②类阻断回执同样留档供平台/审计消费
|
|
1077
1150
|
writeReconcileRunResult({ runtimeRoot: reconcileRuntimeRoot, changeName, envelope: reconcileEnvelope, result: reconcileCheck })
|
|
1078
1151
|
if (reconcileBlocked) {
|
|
1152
|
+
writeVerifyGatePointer({ runtimeRoot: reconcileRuntimeRoot, changeName, blocked: true, note: 'target_files ②类(missing_declared)阻断' })
|
|
1079
1153
|
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-contract' })
|
|
1080
1154
|
}
|
|
1081
1155
|
// ── verify-result.md 探针预填段一致性抽查(task-03 / ir-stage-p3b D-002@v1 方案A)──
|
|
@@ -1122,6 +1196,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
1122
1196
|
// 两检查间耦合;各写各的回执、按文件名订阅更干净。fail-soft 不影响 gate 判定
|
|
1123
1197
|
writeProbeConsistencyRunResult({ runtimeRoot: reconcileRuntimeRoot, changeName, envelope: probeEnvelope, result: probeCheck })
|
|
1124
1198
|
if (probeBlocked) {
|
|
1199
|
+
writeVerifyGatePointer({ runtimeRoot: reconcileRuntimeRoot, changeName, blocked: true, note: '探针一致性抽查 error 阻断' })
|
|
1125
1200
|
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-contract' })
|
|
1126
1201
|
}
|
|
1127
1202
|
// ── ceremony 双跑收口·第一出口(2026-09-18-ceremony-risk-pricing task-04 / FR-03 / D-003;
|
|
@@ -1164,43 +1239,9 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
1164
1239
|
}
|
|
1165
1240
|
}
|
|
1166
1241
|
} catch { /* 自动重锚失败不影响收尾(docs gate 兜底) */ }
|
|
1167
|
-
// ── module-impact
|
|
1168
|
-
//
|
|
1169
|
-
//
|
|
1170
|
-
// 类「verify PASS → archive 才发现 pending」的时序漏洞)。
|
|
1171
|
-
const verifyChangeDir = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
|
|
1172
|
-
if (verifyChangeDir) {
|
|
1173
|
-
const impactPath = join(verifyChangeDir, 'module-impact.md')
|
|
1174
|
-
if (existsSync(impactPath)) {
|
|
1175
|
-
const { extractPendingDocSyncRows } = await import('./complete-handlers.js')
|
|
1176
|
-
const pendingRows = extractPendingDocSyncRows(readFileSync(impactPath, 'utf8'))
|
|
1177
|
-
if (pendingRows.length > 0) {
|
|
1178
|
-
console.error(`\n❌ verify 阶段被阻断:module-impact.md「更新结果」表存在 ${pendingRows.length} 个未清 pending/待办项(死信)`)
|
|
1179
|
-
for (const row of pendingRows) console.error(` - ${row}`)
|
|
1180
|
-
console.error(' 文档同步是 verify 的收尾义务:请完成模块文档同步并回填状态为 done/skipped(说明原因),再重新完成 verify。')
|
|
1181
|
-
return rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-contract' })
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
// ── 预填注清零 error 门(归档前最后一道;2026-09-18-artifact-prefill task-03 / FR-03 /
|
|
1186
|
-
// D-003@v1 门禁梯度·error 档):白名单槽宿主文件(design.md + tasks/task-*.md)仍含
|
|
1187
|
-
// 未删预填注 → 阻断 verify 完成(verify 完成是 archive 前置——「归档前注清零」的落点;
|
|
1188
|
-
// brainstorm/plan --done 的 advisory 在此收口为硬门,注在场=预填未确认)。检测单点 =
|
|
1189
|
-
// verify-probes 探针面 runProbe10PrefillNoteClearance(探针 10 --init 预填与本门同一
|
|
1190
|
-
// 实现,不二算)。已知误报面:散文引用注字面量会命中——修法是核对后删注/改写散文
|
|
1191
|
-
// 措辞,不是绕门。──
|
|
1192
|
-
try {
|
|
1193
|
-
const { runProbe10PrefillNoteClearance } = await import('../verify-probes.js')
|
|
1194
|
-
const noteClearance = runProbe10PrefillNoteClearance({ specBase, changeName })
|
|
1195
|
-
if (noteClearance.unclearedFiles.length > 0) {
|
|
1196
|
-
console.error(`\n❌ verify 阶段被阻断:预填注清零校验未过(${noteClearance.unclearedFiles.length} 个文件的白名单槽仍含未删预填注——预填≠结论,注在场=未确认):`)
|
|
1197
|
-
for (const f of noteClearance.unclearedFiles) console.error(` - ${f}`)
|
|
1198
|
-
console.error(' 修复:逐槽核对预填值后删除行尾「(预填:核对后删本注)」注(删注=确认动作);散文含注字面量的命中改写该处措辞。清零后重新完成 verify(进度不丢)。')
|
|
1199
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-contract' })
|
|
1200
|
-
}
|
|
1201
|
-
} catch (e) {
|
|
1202
|
-
console.warn(`⚠️ 预填注清零校验异常(降级放行,fail-soft——异常不是未清零,ceremony 双跑同款处置): ${(e && e.message) || e}`)
|
|
1203
|
-
}
|
|
1242
|
+
// ── module-impact 死信探针 + 预填注清零 error 门:已前移至 test/lint 实测门之前 ──
|
|
1243
|
+
// (fail-fast,R4-S-F 实证——纯文档 blocking 检查排在 9 分钟实测门后白烧门禁轮次;
|
|
1244
|
+
// 实现体见上方「fail-fast 前置」块,此处不再重复执行。)
|
|
1204
1245
|
// ── verify 服务进程回收(坑 verify-service-process-leak;坑 verify-pids-cross-session-kill
|
|
1205
1246
|
// 见 reapVerifyServices 注释)──
|
|
1206
1247
|
try {
|
|
@@ -1890,6 +1931,42 @@ export function writeReconcileRunResult({ runtimeRoot, changeName, envelope, res
|
|
|
1890
1931
|
}
|
|
1891
1932
|
}
|
|
1892
1933
|
|
|
1934
|
+
/**
|
|
1935
|
+
* verify --done 阻断明细稳定锚点(quick-D,2026-09-20 报告问题 D):ts 目录(verify-runs/<ts>/
|
|
1936
|
+
* reconcile-result.json 等)的可发现性依赖 stdout——宿主把长命令转后台截断输出后,逐条打印的
|
|
1937
|
+
* ②类明细实际取不回(实证:target_files 阻断明细靠推断绕 3 轮才定位)。稳定路径指针(每次
|
|
1938
|
+
* 覆盖、不随 ts 变)指向本轮最新 verify-runs 目录,agent 无 stdout 也能直读。fail-soft:
|
|
1939
|
+
* 写失败只留痕不阻断 gate(对齐 writeReconcileRunResult 口径)。
|
|
1940
|
+
* @param {{ runtimeRoot: string, changeName: string|null, blocked?: boolean, note?: string }} opts
|
|
1941
|
+
* @returns {string|null} 成功返回指针路径;失败返回 null
|
|
1942
|
+
*/
|
|
1943
|
+
export function writeVerifyGatePointer({ runtimeRoot, changeName, blocked = false, note = '' }) {
|
|
1944
|
+
try {
|
|
1945
|
+
const ts = new Date().toISOString().slice(0, 19).replace(/[-T:]/g, '')
|
|
1946
|
+
const runsDir = join(runtimeRoot, 'verify-runs')
|
|
1947
|
+
const pointerPath = join(runsDir, `gate-last-${changeName || 'change'}.json`)
|
|
1948
|
+
mkdirSync(runsDir, { recursive: true })
|
|
1949
|
+
writeFileSync(pointerPath, JSON.stringify({
|
|
1950
|
+
change: changeName,
|
|
1951
|
+
blocked,
|
|
1952
|
+
note,
|
|
1953
|
+
latest_run_dir: join(runsDir, ts),
|
|
1954
|
+
artifacts_hint: [
|
|
1955
|
+
`${ts}/reconcile-result.json(target_files ②类阻断明细在 missing 数组)`,
|
|
1956
|
+
`${ts}/probe-consistency-result.json(探针一致性抽查明细)`,
|
|
1957
|
+
],
|
|
1958
|
+
written_at: new Date().toISOString(),
|
|
1959
|
+
}, null, 2) + '\n')
|
|
1960
|
+
if (blocked) {
|
|
1961
|
+
console.error(`\n📄 verify 门失败明细稳定锚点(stdout 被截断时直读此文件): ${pointerPath}\n 逐条明细在指针文件指向的 verify-runs/<ts>/ 目录(reconcile-result.json 的 missing 数组等)`)
|
|
1962
|
+
}
|
|
1963
|
+
return pointerPath
|
|
1964
|
+
} catch (e) {
|
|
1965
|
+
console.error(`⚠️ verify 门稳定锚点写入失败(不影响 gate): ${e.message}`)
|
|
1966
|
+
return null
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1893
1970
|
/**
|
|
1894
1971
|
* 打印 target_files 对账结果(task-05 接线配套的打印层)。
|
|
1895
1972
|
*
|
package/src/run/quick-audit.js
CHANGED
|
@@ -472,8 +472,34 @@ export async function runQuickTestLintGate({ cwd, specBase, changedFiles = [], d
|
|
|
472
472
|
lint = runVerifyLintCheck({ cwd, specBase })
|
|
473
473
|
}
|
|
474
474
|
const failed = []
|
|
475
|
-
|
|
476
|
-
|
|
475
|
+
// 纯超时降档(R4 门禁价值考古:36/106 失败是 600s 帽杀纯超时假拦——超时=未完成非测试挂)。
|
|
476
|
+
// 所有失败单元 reason 均含超时 → advisory 不计入 failed;任一单元真实挂测 → 维持硬拦。
|
|
477
|
+
if (test.status === 'failed') {
|
|
478
|
+
const { isTimeoutOnlyTestFailure } = await import('../verify-postcheck.js')
|
|
479
|
+
if (isTimeoutOnlyTestFailure(test)) {
|
|
480
|
+
console.warn(`\n⚠️ quick test 实测纯超时(无任何挂掉的测试)——不拦完成:${test.reason || ''}`)
|
|
481
|
+
console.warn(' 处置:定向复跑触碰模块自证;常发超时建议 local.yaml 配 modules: 块启用模块子集实测。')
|
|
482
|
+
} else {
|
|
483
|
+
failed.push('test')
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// ── lint 归属鉴定降档(R4-S-F/R4-S-Q 同根因:隔离快照含 HEAD 存量债文件时 lint 恒败,
|
|
487
|
+
// agent 被逼范围外清偿才过门)。失败输出提及文件 × 本会话文件集零交集 → 存量债
|
|
488
|
+
// advisory 放行不拦完成;有交集 / 输出无可识别路径(unattributable)→ 维持硬拦。──
|
|
489
|
+
if (lint.status === 'failed') {
|
|
490
|
+
const { triageLintOwnership } = await import('../verify-postcheck.js')
|
|
491
|
+
const own = triageLintOwnership({
|
|
492
|
+
failureFiles: lint.failureFiles || [],
|
|
493
|
+
changeFiles: files.map(f => String(f).replace(/\\/g, '/')),
|
|
494
|
+
})
|
|
495
|
+
if (own.verdict === 'pre-existing') {
|
|
496
|
+
console.warn(`\n⚠️ quick lint 实测失败,但归属鉴定为 HEAD 存量债(失败提及文件与本会话文件集零交集)——不拦完成`)
|
|
497
|
+
console.warn(` 存量债文件:${(lint.failureFiles || []).join('、') || '(见上方输出)'}`)
|
|
498
|
+
console.warn(' 处置建议:单独 quick 机械清偿解锁全仓 lint 门(顺手清债);确与本会话无关可忽略本行。')
|
|
499
|
+
} else {
|
|
500
|
+
failed.push('lint')
|
|
501
|
+
}
|
|
502
|
+
}
|
|
477
503
|
if (failed.length > 0 && snapshot) {
|
|
478
504
|
try { const { printSnapshotFailureHint } = await import('./gate-snapshot.js')
|
|
479
505
|
printSnapshotFailureHint({ snapshotRoot: snapshot.snapshotRoot, changeFileCount: snapshot.overlaid, sourceRoot: snapshot.sourceRoot || null }, { offEnv: 'SILLYSPEC_QUICK_GATE_SNAPSHOT_OFF' })
|
package/src/run/shared.js
CHANGED
|
@@ -1291,7 +1291,9 @@ async function matchQuickModules(srcChanged, specBase, projectName) {
|
|
|
1291
1291
|
* @param {string} projectName 项目名(progress.project)
|
|
1292
1292
|
* @returns {Promise<object|null>} 模块索引(扁平 { id: {...} });不可得 → null
|
|
1293
1293
|
*/
|
|
1294
|
-
|
|
1294
|
+
// quick 资产尾②(changelog 边车)消费:complete-handlers 动态 import 解构取用——
|
|
1295
|
+
// 缺 export 曾致「loadQuickModuleIndex is not a function」fail-open 死路(R4-S-Q 实证)。
|
|
1296
|
+
export async function loadQuickModuleIndex(specBase, projectName) {
|
|
1295
1297
|
if (!specBase || !projectName) return null
|
|
1296
1298
|
try {
|
|
1297
1299
|
const mapPath = join(specBase, 'docs', projectName, 'modules', '_module-map.yaml')
|
package/src/stages/execute.js
CHANGED
|
@@ -1281,10 +1281,10 @@ ${workdirLines}
|
|
|
1281
1281
|
: `- 组内任意两个 task 之间无 provides / expects_from 契约链(契约 task 禁止同批——串行实现会读到半成品,契约 task 由独立子代理并行处理或落在不同 Wave)`
|
|
1282
1282
|
const roleItem1 = implicit
|
|
1283
1283
|
? `1. 为每个任务启动一个子代理(Agent tool),或按上述三条件把多个任务合并为一个 batch 子代理,逐个完成(串行——启动一个、等它完成并审查后再启动下一个,见「调度要求」串行铁律)`
|
|
1284
|
-
: `1. 为每个任务启动一个子代理(Agent tool),或按上述三条件把多个任务合并为一个 batch 子代理,同 Wave
|
|
1284
|
+
: `1. 为每个任务启动一个子代理(Agent tool),或按上述三条件把多个任务合并为一个 batch 子代理,同 Wave 内可并行——但同时在飞子代理 ≤3(见「调度要求」并发帽,超出的 batch 排队错峰)`
|
|
1285
1285
|
const scheduleItem1 = implicit
|
|
1286
1286
|
? `1. **隐式 Wave 串行铁律**:本 Wave 由 plan.md 无显式 Wave 划分时合成——任务一律逐个完成(单子代理串行逐个实现,或逐个启动子代理并等待其完成再启动下一个),**禁止并行启动**(未做过文件正交/契约链核查,并行不安全;需要并行收益请在 plan.md 显式划分 Wave)。`
|
|
1287
|
-
: `1. **同一 Wave 的多个子代理(独立或 batch
|
|
1287
|
+
: `1. **同一 Wave 的多个子代理(独立或 batch)并行启动、batch 内部串行,且同时在飞 ≤3**(batch 分组仅按文件正交 / 无契约链判定,不改变 Wave 依赖语义——Wave 定义=无依赖可并行;有依赖应在 plan.md 的不同 Wave 中。并发帽 3:Wave 内子代理超 3 个时分两批错峰——首批完成后回收槽位再派第二批;4 路齐发实证触发平台额度/限流耗尽,整 Wave 中断 17 分钟由用户手动恢复,得不偿失)。`
|
|
1288
1288
|
|
|
1289
1289
|
return `${waveHeader}
|
|
1290
1290
|
|
|
@@ -21,6 +21,7 @@ import { parseFileChangeList, pathMatches } from '../change-list.js'
|
|
|
21
21
|
import { getRule } from '../stage-contract-spec.js'
|
|
22
22
|
import { TASKCARD_PLACEHOLDERS } from '../taskcard-placeholders.js'
|
|
23
23
|
import { validateScriptCommands } from './cmd-existence.js'
|
|
24
|
+
import { parseTaskFrontmatter } from '../taskcard-frontmatter.js'
|
|
24
25
|
|
|
25
26
|
// ═══════════════════════════════════════════════════════════════
|
|
26
27
|
// 解析工具(从 plan.js 迁移)
|
|
@@ -319,21 +320,20 @@ function hasTddOrVerify(content) {
|
|
|
319
320
|
|
|
320
321
|
/**
|
|
321
322
|
* 解析 task-NN.md 的跨任务契约字段 provides / expects_from
|
|
322
|
-
*
|
|
323
|
-
*
|
|
323
|
+
* 解析归一 taskcard-frontmatter.js 单一源(2026-09-20-taskcard-yaml-hardgate:此前本函数与
|
|
324
|
+
* verify-probes 各自提取+吞错,坏 YAML 冒充「无契约字段」致 [plan.cross-task-contract] 空真过门)。
|
|
325
|
+
* provides/expects_from 为可选字段;缺失返回空(不阻断);frontmatter 非法 YAML 返回空 +
|
|
326
|
+
* yamlError 显式降级键(不冒充无字段——YAML 合法性由 validatePlanFeasibility 步骤 0b 硬拦,
|
|
327
|
+
* D-001@v2 单点拦截先例;yamlError 供独立消费方判读)。
|
|
324
328
|
* @param {string} content - task 文件内容
|
|
325
|
-
* @returns {{ provides: Array<{contract, fields}>, expectsFrom: Record<string, Array<{contract, needs}
|
|
329
|
+
* @returns {{ provides: Array<{contract, fields}>, expectsFrom: Record<string, Array<{contract, needs}>>,
|
|
330
|
+
* yamlError: {message: string, line: number, column: number}|null }}
|
|
326
331
|
*/
|
|
327
332
|
export function parseTaskContracts(content) {
|
|
328
|
-
const
|
|
329
|
-
if (!
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
fm = jsYaml.load(fmMatch[1]) || {}
|
|
333
|
-
} catch {
|
|
334
|
-
// frontmatter 非合法 YAML(老格式/手写不规范)—— 当作无契约字段,不阻断
|
|
335
|
-
return { provides: [], expectsFrom: {} }
|
|
336
|
-
}
|
|
333
|
+
const parsed = parseTaskFrontmatter(content)
|
|
334
|
+
if (!parsed.hasFrontmatter) return { provides: [], expectsFrom: {}, yamlError: null }
|
|
335
|
+
if (!parsed.ok) return { provides: [], expectsFrom: {}, yamlError: parsed.error }
|
|
336
|
+
const fm = parsed.fm
|
|
337
337
|
|
|
338
338
|
const provides = Array.isArray(fm.provides)
|
|
339
339
|
? fm.provides.map(p => ({
|
|
@@ -355,7 +355,7 @@ export function parseTaskContracts(content) {
|
|
|
355
355
|
}
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
-
return { provides, expectsFrom }
|
|
358
|
+
return { provides, expectsFrom, yamlError: null }
|
|
359
359
|
}
|
|
360
360
|
|
|
361
361
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -1300,6 +1300,21 @@ export function validatePlanFeasibility(changeDir, projectRoot = null) {
|
|
|
1300
1300
|
errors.push(`${dupTaskId || file}: frontmatter 顶层键 ${dup.key} 重复出现 ${dup.lines.length} 次(L${dup.lines.join('、L')})——骨架已自动反填的键(如 depends_on)勿重复手填,保留正确一处删除其余`)
|
|
1301
1301
|
}
|
|
1302
1302
|
|
|
1303
|
+
// 0b. frontmatter YAML 合法性硬校验(2026-09-20-taskcard-yaml-hardgate FR-02 / D-001@v2——
|
|
1304
|
+
// D-004@v1 重复键拦截的同类泛化:jsYaml 抛错族错误只能在 feasibility 入口单点拦截,
|
|
1305
|
+
// 否则下游 4 处 catch 静默降级令 [plan.cross-task-contract] 空真过门。双报豁免(双维度
|
|
1306
|
+
// 同信号须豁免惯例):message 含 duplicated mapping key 且 dupKeys 非空时跳过——上方 0
|
|
1307
|
+
// 已用键名+行号精确上报;嵌套重复键(detectDuplicateTopKeys 顶格正则够不到)仍由此兜底。
|
|
1308
|
+
const fmParse = parseTaskFrontmatter(content)
|
|
1309
|
+
if (!fmParse.ok) {
|
|
1310
|
+
const isDupReported = dupKeys.length > 0 && /duplicated mapping key/i.test(fmParse.error.message)
|
|
1311
|
+
if (!isDupReported) {
|
|
1312
|
+
errors.push(
|
|
1313
|
+
`${dupTaskId || file}: frontmatter 非法 YAML(${file}:${fmParse.error.line}:${fmParse.error.column} ${fmParse.error.message})——契约/验收/标量字段全链路不可读,plan 门禁拒绝放行;修复卡片 frontmatter(值含方括号/全角括号时加引号或改块式列表)`
|
|
1314
|
+
)
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1303
1318
|
// 1. 必要字段检查
|
|
1304
1319
|
const taskId = (fm.match(/^id:\s*(.+)/m)?.[1] || '').trim()
|
|
1305
1320
|
const title = (fm.match(/^title:\s*(.+)/m)?.[1] || '').trim()
|
|
@@ -1374,12 +1389,11 @@ export function validatePlanFeasibility(changeDir, projectRoot = null) {
|
|
|
1374
1389
|
// 不存在文件一律跳过,且至少读到一个源文件才比对(防全读不到 → 全标识符误报)。
|
|
1375
1390
|
if (projectRoot && hasAcceptance && allowedPaths.length > 0) {
|
|
1376
1391
|
let acceptanceText = ''
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
// frontmatter 非法 YAML(feasibility 未对 fm 做 YAML 解析,可能字面合法但语义复杂)→ 跳过 best-effort
|
|
1392
|
+
// 复用步骤 0b 的 fmParse(2026-09-20-taskcard-yaml-hardgate):不再重复 jsYaml.load;
|
|
1393
|
+
// 坏卡已被 0b 阻断,此处 ok=false 仅防御性跳过 best-effort(0 告警不误拦)
|
|
1394
|
+
if (fmParse.ok && fmParse.fm) {
|
|
1395
|
+
if (typeof fmParse.fm.acceptance === 'string') acceptanceText = fmParse.fm.acceptance
|
|
1396
|
+
else if (Array.isArray(fmParse.fm.acceptance)) acceptanceText = fmParse.fm.acceptance.join('\n')
|
|
1383
1397
|
}
|
|
1384
1398
|
if (acceptanceText) {
|
|
1385
1399
|
const IDENT_RE = /(?<![A-Za-z])[a-z]+(?:_[a-z]+)+|(?<![A-Za-z])[a-z]+(?:[A-Z][a-z]+)+/g
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* taskcard-frontmatter.js — task 卡 frontmatter 单一解析源(2026-09-20-taskcard-yaml-hardgate)
|
|
3
|
+
*
|
|
4
|
+
* 背景:坏 YAML 曾被三个消费点各自静默吞——plan-postcheck.js parseTaskContracts 的 catch 返回
|
|
5
|
+
* 空对象冒充「无契约字段」→ 契约门禁空真过门;verify-probes.js parseTaskAcceptance 的 catch
|
|
6
|
+
* 返回空数组 → 探针 7 假防御文案;且两侧 frontmatter 提取口径不一致(一侧 \r 容错一侧不容)。
|
|
7
|
+
* 本模块把「提取 + jsYaml 解析 + 错误定位(文件行:列)」收敛为一处,plan 与 verify 两侧共同消费。
|
|
8
|
+
*
|
|
9
|
+
* 防环铁律:本模块除 js-yaml 外零依赖(不 import 仓内任何模块)——plan-postcheck 与
|
|
10
|
+
* worktree-apply 存在既有依赖边(knowledge patterns),共享逻辑放新模块两侧 import。
|
|
11
|
+
*/
|
|
12
|
+
import jsYaml from 'js-yaml'
|
|
13
|
+
|
|
14
|
+
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 提取 task 卡 frontmatter。界定口径与 verify-probes.js parseTaskAcceptance 原实现同款
|
|
18
|
+
* (首行 --- 起、闭合 --- 行止,\r\n 容错——Windows 编辑器文本模式写卡不炸提取)。
|
|
19
|
+
* @param {string} content task 卡全文
|
|
20
|
+
* @returns {{ has: boolean, yamlText: string|null, yamlStartLine: number }}
|
|
21
|
+
* yamlStartLine = YAML 内容在文件中的起始行号(1 基):首行是 ---,YAML 自第 2 行起,恒为 2
|
|
22
|
+
*/
|
|
23
|
+
export function splitFrontmatter(content) {
|
|
24
|
+
const m = String(content ?? '').match(FRONTMATTER_RE)
|
|
25
|
+
if (!m) return { has: false, yamlText: null, yamlStartLine: 2 }
|
|
26
|
+
return { has: true, yamlText: m[1], yamlStartLine: 2 }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 解析 task 卡 frontmatter(单一真相源)。
|
|
31
|
+
* @param {string} content task 卡全文
|
|
32
|
+
* @returns {{ ok: boolean, hasFrontmatter: boolean, fm: object|null,
|
|
33
|
+
* error: { message: string, line: number, column: number }|null }}
|
|
34
|
+
* - 无 frontmatter:{ ok: true, hasFrontmatter: false, fm: null, error: null }——不是错误,
|
|
35
|
+
* 「无 frontmatter」由各消费方按自身语义处理(plan-postcheck 结构检查另有拦截)
|
|
36
|
+
* - jsYaml 抛错:{ ok: false, hasFrontmatter: true, fm: null, error: {...} }。
|
|
37
|
+
* error.line = e.mark.line + yamlStartLine:js-yaml mark.line 是 YAML 文本内 0 基行号,
|
|
38
|
+
* +2 后即文件 1 基行号(首行 ---,YAML 第 1 行 = 文件第 2 行);mark 缺席回退 1。
|
|
39
|
+
* error.column = e.mark.column + 1(0 基 → 1 基;mark 缺席回退 1)。
|
|
40
|
+
* error.message 取异常首行(js-yaml 原始 message 含多行上下文,单行化便于门禁文案)。
|
|
41
|
+
* - 合法:{ ok: true, hasFrontmatter: true, fm, error: null }(空文档回退 {})
|
|
42
|
+
*/
|
|
43
|
+
export function parseTaskFrontmatter(content) {
|
|
44
|
+
const { has, yamlText, yamlStartLine } = splitFrontmatter(content)
|
|
45
|
+
if (!has) return { ok: true, hasFrontmatter: false, fm: null, error: null }
|
|
46
|
+
let fm
|
|
47
|
+
try {
|
|
48
|
+
fm = jsYaml.load(yamlText) || {}
|
|
49
|
+
} catch (e) {
|
|
50
|
+
const mark = e && e.mark
|
|
51
|
+
const line = mark && typeof mark.line === 'number' ? mark.line + yamlStartLine : 1
|
|
52
|
+
const column = mark && typeof mark.column === 'number' ? mark.column + 1 : 1
|
|
53
|
+
const message = String((e && e.message) || e).split('\n')[0]
|
|
54
|
+
return { ok: false, hasFrontmatter: true, fm: null, error: { message, line, column } }
|
|
55
|
+
}
|
|
56
|
+
return { ok: true, hasFrontmatter: true, fm, error: null }
|
|
57
|
+
}
|
package/src/verify-postcheck.js
CHANGED
|
@@ -135,6 +135,44 @@ function extractLintCommand(yamlText) {
|
|
|
135
135
|
return null
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
/**
|
|
139
|
+
* lint 失败输出中提及的文件路径提取(lint 归属鉴定·R4-S-F 实证:verify 侧 lint 硬门 7 轮
|
|
140
|
+
* 全是 HEAD 存量债——快照含基线债文件时恒败,逼 agent 范围外清偿或逃生口)。
|
|
141
|
+
* 口径:按空白切 token,剥引号/行号尾缀(ruff/eslint 的 path:line:col 形态),保留带路径
|
|
142
|
+
* 分隔符且以代码扩展名结尾的 token(不带分隔符的普通单词不收,防误报)。去重保序。
|
|
143
|
+
*/
|
|
144
|
+
export function extractLintFailureFiles(output) {
|
|
145
|
+
if (!output || typeof output !== 'string') return []
|
|
146
|
+
const LINT_FILE_EXT_RE = /\.(?:js|mjs|cjs|ts|tsx|jsx|vue|svelte|py|pyw|go|rs|java|kt|kts|rb|php|cs|c|h|cpp|cc|hpp|swift|scala|css|scss)$/
|
|
147
|
+
const seen = new Set()
|
|
148
|
+
for (const raw of output.split(/\s+/)) {
|
|
149
|
+
let t = raw.replace(/^["'`(\[]+/, '').replace(/["'`),;]+$/, '')
|
|
150
|
+
t = t.replace(/:(?:\d+)(?::\d+)*$/, '') // 剥 :line / :line:col
|
|
151
|
+
if (!LINT_FILE_EXT_RE.test(t)) continue
|
|
152
|
+
if (!/[\\/]/.test(t)) continue
|
|
153
|
+
const norm = t.replace(/\\/g, '/').replace(/^\.\//, '')
|
|
154
|
+
if (norm.length > 1) seen.add(norm)
|
|
155
|
+
}
|
|
156
|
+
return [...seen]
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* lint 失败归属判定(纯函数):失败输出提及的文件 × 本变更文件集。
|
|
161
|
+
* - owned:有交集 → 失败可能由本变更引入 → 维持硬拦(保守)。
|
|
162
|
+
* - pre-existing:零交集 → 全是 HEAD 存量债 → 调用方降档 advisory(债不是本变更的错)。
|
|
163
|
+
* - unattributable:失败文件提取为空(输出无可识别路径,如纯配置错误)→ 维持硬拦(无法
|
|
164
|
+
* 排除本变更责任,宁可拦不可漏)。
|
|
165
|
+
* 匹配宽松双向 endsWith(输出可能是绝对路径/仓相对路径,两侧口径不一定同源)。
|
|
166
|
+
*/
|
|
167
|
+
export function triageLintOwnership({ failureFiles = [], changeFiles = [] } = {}) {
|
|
168
|
+
const norm = (p) => String(p).replace(/\\/g, '/').replace(/^\.\//, '').toLowerCase()
|
|
169
|
+
const ffs = (Array.isArray(failureFiles) ? failureFiles : []).map(norm).filter(Boolean)
|
|
170
|
+
if (ffs.length === 0) return { verdict: 'unattributable', overlap: [] }
|
|
171
|
+
const cfs = (Array.isArray(changeFiles) ? changeFiles : []).map(norm).filter(Boolean)
|
|
172
|
+
const overlap = ffs.filter(f => cfs.some(c => c === f || c.endsWith('/' + f) || f.endsWith('/' + c)))
|
|
173
|
+
return overlap.length > 0 ? { verdict: 'owned', overlap } : { verdict: 'pre-existing', overlap: [] }
|
|
174
|
+
}
|
|
175
|
+
|
|
138
176
|
/**
|
|
139
177
|
* lint advisory 观察期计数(刀③,2026-09-08):advisory 失败不阻断,但「观察期后升级硬门」
|
|
140
178
|
* 需要失败率数据支撑(更硬 vs 更吵的决策依据——硬门误伤纯文档/跨平台路径会制造新噪音)。
|
|
@@ -226,6 +264,7 @@ export function runVerifyLintCheck({ cwd, specBase, timeoutMs } = {}) {
|
|
|
226
264
|
outputTail,
|
|
227
265
|
reason: finalReason,
|
|
228
266
|
tally, // { failedRuns, totalRuns } | null——观察期失败率,升级硬门的决策依据
|
|
267
|
+
failureFiles: status === 'failed' ? extractLintFailureFiles(output) : [], // 归属鉴定输入(全文口径,outputTail 有截断盲区)
|
|
229
268
|
}
|
|
230
269
|
}
|
|
231
270
|
|
|
@@ -266,7 +305,7 @@ export function printVerifyLintCheck(result) {
|
|
|
266
305
|
console.error(`\n⚠️ Verify lint 实测失败(advisory 档,不阻断本次完成):\`${result.command}\` — ${result.reason}`)
|
|
267
306
|
console.error(' agent 的 lint 自报告与实测不符时以实测为准;请修复后重跑,避免格式债推迟到 commit 被 pre-commit hook 拦截。')
|
|
268
307
|
if (result.tally) {
|
|
269
|
-
console.error(` 📊 lint advisory
|
|
308
|
+
console.error(` 📊 lint advisory 观察期:实测 ${result.tally.totalRuns} 次中失败 ${result.tally.failedRuns} 次(数据落 .runtime/verify-lint-tally.json)。`)
|
|
270
309
|
}
|
|
271
310
|
}
|
|
272
311
|
if (result.outputTail) {
|
|
@@ -1027,6 +1066,22 @@ export function classifyTestFailureArtifact(result) {
|
|
|
1027
1066
|
* - 其余(full / module 无块 / module git 不可用 hitCount=-1)→ 'full'
|
|
1028
1067
|
* @returns {'skip'|'module-subset'|'module-zero-hit-skip'|'full'}
|
|
1029
1068
|
*/
|
|
1069
|
+
/**
|
|
1070
|
+
* 纯超时失败判定(R4 门禁价值考古 2026-09-21:227 跑 106 败里 36 次(34%)是 600s 帽杀的
|
|
1071
|
+
* 纯超时假拦——超时=未完成非测试失败,硬拦只是浪费一轮门禁)。所有失败单元的 reason 都
|
|
1072
|
+
* 含「超时」→ true(调用方降档 advisory);任一失败单元是真实退出码/挂测 → false(维持硬拦)。
|
|
1073
|
+
* 单元来源:module 模式读 result.modules(失败单元过滤),full 模式整体 reason。跨仓合并结果
|
|
1074
|
+
* 语义同 full(整体 reason 聚合)。
|
|
1075
|
+
*/
|
|
1076
|
+
export function isTimeoutOnlyTestFailure(testCheck) {
|
|
1077
|
+
if (!testCheck || testCheck.status !== 'failed') return false
|
|
1078
|
+
const units = Array.isArray(testCheck.modules) && testCheck.modules.length > 0
|
|
1079
|
+
? testCheck.modules.filter(u => u && u.status === 'failed')
|
|
1080
|
+
: [testCheck]
|
|
1081
|
+
if (units.length === 0) return false
|
|
1082
|
+
return units.every(u => /超时/.test(String(u.reason || '')))
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1030
1085
|
export function decideVerifyTestAction({ strategy, modulesPresent, hitCount }) {
|
|
1031
1086
|
if (strategy === 'skip') return 'skip'
|
|
1032
1087
|
if (strategy === 'module' && modulesPresent) {
|
|
@@ -2034,6 +2089,9 @@ function runModuleSubset({ cwd, specBase, changeName, hits, knownFailures = [],
|
|
|
2034
2089
|
// 每模块台账另见 extra.modules[].failure_remaining(带模块归属)
|
|
2035
2090
|
failureRemaining: perModule.flatMap(r => r.failureRemaining || []),
|
|
2036
2091
|
failureExempted: perModule.flatMap(r => r.failureExempted || []),
|
|
2092
|
+
// 逐模块结论(isTimeoutOnlyTestFailure 的单元口径;writeRunResult extra 之外再随结果
|
|
2093
|
+
// 返回,调用方可精确分单元判超时/真挂,不依赖聚合 reason 文本)
|
|
2094
|
+
modules: perModule.map(r => ({ name: r.name, status: r.status, reason: r.reason, durationMs: r.durationMs })),
|
|
2037
2095
|
}
|
|
2038
2096
|
|
|
2039
2097
|
writeRunResult({
|