sillyspec 3.28.10 → 3.28.11
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/.claude/skills/sillyspec-execute/SKILL.md +223 -222
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-quick/SKILL.md +8 -10
- package/.claude/skills/sillyspec-verify/SKILL.md +91 -87
- package/package.json +1 -1
- package/src/change-risk-profile.js +51 -3
- package/src/config-schema.js +21 -0
- package/src/design-facts.js +46 -9
- package/src/docs-check.js +14 -10
- package/src/docs-debt.js +7 -2
- package/src/docs-gate.js +42 -15
- package/src/endpoint-extractor.js +50 -2
- package/src/foreign-declared.js +25 -16
- package/src/hooks/worktree-guard.js +75 -2
- package/src/index.js +129 -1
- package/src/machine-interface.js +26 -0
- package/src/probe7-anchor-check.js +20 -2
- package/src/progress/stage-machine.js +34 -5
- package/src/progress.js +1 -1
- package/src/quicklog.js +231 -1
- package/src/run/complete-handlers.js +172 -1
- package/src/run/complete.js +73 -3
- package/src/run/gate-snapshot.js +262 -0
- package/src/run/gates.js +41 -7
- package/src/run/prompt.js +79 -1
- package/src/run/shared.js +10 -0
- package/src/stage-contract-spec.js +14 -0
- package/src/stage-contract.js +314 -8
- package/src/stage-review-checklist.js +3 -1
- package/src/stage-review.js +137 -2
- package/src/stages/archive.js +9 -5
- package/src/stages/brainstorm.js +7 -1
- package/src/stages/execute.js +62 -8
- package/src/stages/plan-postcheck.js +42 -5
- package/src/stages/plan.js +8 -1
- package/src/stages/quick.js +5 -5
- package/src/stages/verify.js +3 -2
- package/src/sync.js +69 -0
- package/src/task-review.js +9 -3
- package/src/verify-facts-schema.js +78 -9
- package/src/verify-postcheck.js +260 -26
- package/src/verify-probes.js +780 -44
- package/src/worktree-apply.js +175 -3
- package/src/write-audit.js +55 -0
- package/.claude/full.json +0 -1
package/src/run/gates.js
CHANGED
|
@@ -919,9 +919,9 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
919
919
|
if (existsSync(p7ReportPath)) {
|
|
920
920
|
const p7Anchor = checkProbe7AnchorCoverage(readFileSync(p7ReportPath, 'utf8'))
|
|
921
921
|
if (p7Anchor.applicable && p7Anchor.missingAnchors.length > 0) {
|
|
922
|
-
console.warn(`\n⚠️ 探针7 锚点校验:${p7Anchor.missingAnchors.length}/${p7Anchor.coveredRows} 行判定为 covered 但证据列缺 file:line
|
|
922
|
+
console.warn(`\n⚠️ 探针7 锚点校验:${p7Anchor.missingAnchors.length}/${p7Anchor.coveredRows} 行判定为 covered 但证据列缺 file:line / \`.test.\` 文件名 / 反引号路径锚点(advisory 不阻断,审查会要求回补):`)
|
|
923
923
|
for (const m of p7Anchor.missingAnchors.slice(0, 10)) {
|
|
924
|
-
console.warn(` - ${m.task}:${m.acceptance || '(空 acceptance)'}——证据当前为「${m.evidence || '(空)'}」,应给 \`测试文件路径:行号\`
|
|
924
|
+
console.warn(` - ${m.task}:${m.acceptance || '(空 acceptance)'}——证据当前为「${m.evidence || '(空)'}」,应给 \`测试文件路径:行号\` 首命中锚点、\`.test.\` 测试文件名或反引号包裹的路径/测试名(行号可省)`)
|
|
925
925
|
}
|
|
926
926
|
if (p7Anchor.missingAnchors.length > 10) console.warn(` …还有 ${p7Anchor.missingAnchors.length - 10} 行`)
|
|
927
927
|
}
|
|
@@ -1051,8 +1051,14 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
1051
1051
|
// 不在自动刷新范围,照常 fail-closed。
|
|
1052
1052
|
const gated = validateStageReviewWithAutoRefresh({ stage: stageName, reviewType, runtimeRoot, reviewRunId, searchDirs, autoRefresh: true })
|
|
1053
1053
|
if (gated.autoRefreshed) {
|
|
1054
|
-
console.warn(`\n🔄 stage review docHash
|
|
1054
|
+
console.warn(`\n🔄 stage review docHash 失配已自动刷新(第 ${gated.refreshOrdinal || 1} 次;hash ${gated.hashDrift || 'n/a'};主文档改版后机械重算,verdict/checklist 保留——结论是否仍适用于新文档需人工确认)`)
|
|
1055
1055
|
console.warn(` 已就地更新: ${gated.refreshedPath}`)
|
|
1056
|
+
// C3 升级提醒(2026-09-15 wp EHS 实证:NEW: 批注/需求调整/Reverse Sync 多次走此通道):
|
|
1057
|
+
// 同一 review 被反复刷新 = 主文档在审查通过后反复变更,「审查后的变更不再见审查」的
|
|
1058
|
+
// 面积在累积——序数 ≥2 从一句注记升级为显式人工确认提醒(仍放行,不伪造重审)。
|
|
1059
|
+
if ((gated.refreshOrdinal || 1) >= 2) {
|
|
1060
|
+
console.warn(` ⚠️ 该 review 已第 ${gated.refreshOrdinal} 次刷新——主文档在审查后反复变更,建议人工复核 verdict 结论续用性(涉及设计要点/接口变更时应重审,勿把自动放行当「新文档已审过」)`)
|
|
1061
|
+
}
|
|
1056
1062
|
}
|
|
1057
1063
|
const reviewResult = gated.result
|
|
1058
1064
|
printStageReviewResult(reviewResult, { stage: stageName, reviewRunId, runtimeRoot, changeName })
|
|
@@ -1065,7 +1071,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
1065
1071
|
// channel 识别(2026-09-10 通道优先序批次):reviewer.channel 结构化落款优先,兼容
|
|
1066
1072
|
// reviewerNotes 首行「降级:」约定;platform 通道(P2 review-dispatch 落地后)ℹ️ 留痕
|
|
1067
1073
|
// missionId 供追溯,其余通道正常放行。
|
|
1068
|
-
const { classifyReviewerChannel } = await import('../stage-review.js')
|
|
1074
|
+
const { classifyReviewerChannel, hasDelegatedWriteDisclosure } = await import('../stage-review.js')
|
|
1069
1075
|
const reviewChannel = classifyReviewerChannel(reviewResult.review)
|
|
1070
1076
|
if (reviewChannel === 'self') {
|
|
1071
1077
|
console.warn(`\n⚠️ Stage Review 降级自审(tier=independent 但宿主无 Agent tool):${stageName} review 按 prompt 降级条款由当前 agent 自审产出——独立性折损已留痕(reviewerNotes 首行「降级:」),结论应附源码锚点补偿,建议人工抽查关键结论。`)
|
|
@@ -1073,6 +1079,12 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
1073
1079
|
const mid = reviewResult.review && reviewResult.review.reviewer && reviewResult.review.reviewer.missionId
|
|
1074
1080
|
console.log(`\nℹ️ Stage Review 平台派发(channel=platform${mid ? `,missionId=${mid}` : ',missionId 未落款'}):${stageName} review 由平台独立 worker 产出(独立进程/会话)。`)
|
|
1075
1081
|
}
|
|
1082
|
+
// C4 代落盘审计行(2026-09-15 wp EHS 实证:平台写通道故障下主流程代子代理落盘 review,
|
|
1083
|
+
// 独立性折损当时只在 notes 里、gate 不可见):reviewerNotes 首行「代落盘:」披露 → ⚠️ 留痕
|
|
1084
|
+
// (与降级自审同哲学:可见的折损优于静默伪装;首行约定识别,不阻断)。
|
|
1085
|
+
if (hasDelegatedWriteDisclosure(reviewResult.review)) {
|
|
1086
|
+
console.warn(`\n⚠️ Stage Review 代落盘披露:${stageName} review 的 reviewerNotes 首行标记「代落盘」——review.json 由主代理代(写通道故障的)审查者落盘,独立性依赖审查者结论回传的忠实性,建议人工抽查关键结论与回传文本一致性。`)
|
|
1087
|
+
}
|
|
1076
1088
|
}
|
|
1077
1089
|
} catch (e) {
|
|
1078
1090
|
// fail-closed:Gate 自身异常阻断完成,不静默放行(与 Task Review Gate 一致)
|
|
@@ -1653,11 +1665,33 @@ export function printReconcileTargetFilesCheck(r, envelope = buildReconcileTarge
|
|
|
1653
1665
|
// ③类(做了没声明=scope creep):WARNING 放行,suspectTask 尽力归因随行列出
|
|
1654
1666
|
if (r.status === 'undeclared') {
|
|
1655
1667
|
console.warn(`\n⚠️ target_files 对账发现 ${r.undeclared.length} 个实际改动未在任何 task 卡声明(③类 scope creep,advisory 不阻断):`)
|
|
1656
|
-
|
|
1668
|
+
if (r.undeclared.length <= 20) {
|
|
1669
|
+
for (const u of r.undeclared) {
|
|
1657
1670
|
console.warn(` - ${u.path}${u.suspectTask ? `(疑似 ${u.suspectTask} 的改动——该 task 的 review.json changedFiles 提及)` : ''}`)
|
|
1658
1671
|
}
|
|
1659
|
-
|
|
1660
|
-
|
|
1672
|
+
} else {
|
|
1673
|
+
// >20 条逐行刷屏改按顶层目录聚合(坑 reconcile-undeclared-noise,2026-09-15 wp EHS 会话
|
|
1674
|
+
// 实证:58 条③类多为 worktree baseline 拷入的环境文件——agent 需要的是形状不是清单):
|
|
1675
|
+
// 目录计数降序 + 每目录至多 2 个样例;完整清单随后由 writeReconcileRunResult 落盘
|
|
1676
|
+
// reconcile-result.json(gate 输出 📄 行含具体路径),suspectTask 归因以落盘为准。
|
|
1677
|
+
const byDir = new Map()
|
|
1678
|
+
for (const u of r.undeclared) {
|
|
1679
|
+
const p = String(u.path || '')
|
|
1680
|
+
const slash = p.indexOf('/')
|
|
1681
|
+
const dir = slash > 0 ? p.slice(0, slash + 1) : '(仓根)'
|
|
1682
|
+
const rec = byDir.get(dir) || { count: 0, samples: [] }
|
|
1683
|
+
rec.count++
|
|
1684
|
+
if (rec.samples.length < 2) rec.samples.push(p)
|
|
1685
|
+
byDir.set(dir, rec)
|
|
1686
|
+
}
|
|
1687
|
+
const dirs = [...byDir.entries()].sort((a, b) => b[1].count - a[1].count)
|
|
1688
|
+
for (const [dir, rec] of dirs.slice(0, 8)) {
|
|
1689
|
+
console.warn(` - ${dir} ×${rec.count}(样例:${rec.samples.join('、')})`)
|
|
1690
|
+
}
|
|
1691
|
+
if (dirs.length > 8) console.warn(` …另有 ${dirs.length - 8} 个目录`)
|
|
1692
|
+
console.warn(` 共 ${r.undeclared.length} 条,>20 触发按目录聚合;完整逐条清单(含疑似 task 归因)见对账落盘 reconcile-result.json(下方 📄 行)。`)
|
|
1693
|
+
}
|
|
1694
|
+
console.warn(` evidence: matched=${envelope.evidence.matched_count} missing=${envelope.evidence.missing_declared_count} undeclared=${envelope.evidence.undeclared_count}`)
|
|
1661
1695
|
for (const fix of envelope.supportedFixes) console.warn(` 提示:${fix}`)
|
|
1662
1696
|
console.warn(` 对账口径 actual=${(r.sources || []).join(' + ') || 'n/a'}(③类误报先核对 sources 与并行会话剔除提示)。`)
|
|
1663
1697
|
return false
|
package/src/run/prompt.js
CHANGED
|
@@ -1042,7 +1042,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1042
1042
|
if (['brainstorm', 'plan', 'execute'].includes(stageName) && promptText.includes('{REVIEW_TIER}')) {
|
|
1043
1043
|
try {
|
|
1044
1044
|
const { classifyReviewTier } = await import('../review-tier.js')
|
|
1045
|
-
const { generateStageReviewRunId, renderReviewJsonContract, stageReviewMarkerPath, readReviewChannelPriority } = await import('../stage-review.js')
|
|
1045
|
+
const { generateStageReviewRunId, renderReviewJsonContract, stageReviewMarkerPath, readReviewChannelPriority, getLatestStageReviewRunId, collectSameStagePriorReview, renderPriorRoundFindingsMd } = await import('../stage-review.js')
|
|
1046
1046
|
const tierSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
1047
1047
|
const tierChangeDir = changeName ? join(tierSpecBase, 'changes', changeName) : null
|
|
1048
1048
|
const designPath = tierChangeDir ? join(tierChangeDir, 'design.md') : null
|
|
@@ -1082,11 +1082,52 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1082
1082
|
// 头部渲染「审查执行通道」段(按用户配置序,见 local.yaml review_dispatch.channel_priority)。
|
|
1083
1083
|
const reviewChannelPriority = readReviewChannelPriority(cwd)
|
|
1084
1084
|
const reviewContractMd = renderReviewJsonContract({ stage: stageName, changeDir: tierChangeDir, reviewRunId, tier: tier.tier, channelPriority: reviewChannelPriority })
|
|
1085
|
+
// C2 前阶段实证清单机械附带(坑 review-subagent-redundant-verify,2026-09-15 wp EHS 实证:
|
|
1086
|
+
// plan 审查把 brainstorm 已两轮实证的三仓源码全量重验——94 分钟不收敛被用户三催。此前只有
|
|
1087
|
+
// prompt 散文说「前序实证勿重验」,子代理并不知道前序具体审过了什么;现把前序 stage review
|
|
1088
|
+
// 的 checklist pass 项机械注入派发 prompt(封顶 15 条防灌爆),审查子代理据此跳过已实证面。
|
|
1089
|
+
let priorFactsMd = ''
|
|
1090
|
+
if (tier.tier === 'independent') {
|
|
1091
|
+
try {
|
|
1092
|
+
const priorStages = stageName === 'plan' ? ['brainstorm'] : stageName === 'execute' ? ['brainstorm', 'plan'] : []
|
|
1093
|
+
const factLines = []
|
|
1094
|
+
for (const ps of priorStages) {
|
|
1095
|
+
let priorRunId = null
|
|
1096
|
+
try { priorRunId = getLatestStageReviewRunId(tierRuntimeRoot, ps, changeName) } catch {}
|
|
1097
|
+
if (!priorRunId) continue
|
|
1098
|
+
const priorReviewPath = join(tierRuntimeRoot, 'stage-reviews', `${ps}-${priorRunId}`, 'review.json')
|
|
1099
|
+
if (!existsSync(priorReviewPath)) continue
|
|
1100
|
+
try {
|
|
1101
|
+
const priorReview = JSON.parse(readFileSync(priorReviewPath, 'utf8'))
|
|
1102
|
+
for (const c of (Array.isArray(priorReview.checklist) ? priorReview.checklist : [])) {
|
|
1103
|
+
if (c && c.result === 'pass' && c.item) factLines.push(`- [${ps}] ${String(c.item).replace(/\s+/g, ' ').slice(0, 100)}`)
|
|
1104
|
+
}
|
|
1105
|
+
} catch {}
|
|
1106
|
+
}
|
|
1107
|
+
if (factLines.length > 0) {
|
|
1108
|
+
const shown = factLines.slice(0, 15)
|
|
1109
|
+
priorFactsMd = `\n**前序阶段独立审查已实证(pass)结论——直接引用勿重验**(展示 ${shown.length}/${factLines.length} 条;EHS 复盘实证:重验已实证面 = 94 分钟不收敛的同款浪费;本阶段只审自己特有面,结论存疑时才定向补证):\n${shown.join('\n')}\n`
|
|
1110
|
+
}
|
|
1111
|
+
} catch {}
|
|
1112
|
+
}
|
|
1113
|
+
// 同阶段上一轮复审基线(ql-20260916-021,Superpowers scoped re-review 采纳①):上一轮
|
|
1114
|
+
// FAIL 的 findings 与已实证 pass 面机械回灌给复审子代理——复审以增量为主(核验修复 +
|
|
1115
|
+
// 本次改版新增面),防全量重读与重复报告已修问题(独立复审子代理无对话历史,不知上一轮
|
|
1116
|
+
// 审过什么/报过什么)。骨架轮/他变更轮由采集端过滤;tier=self 不注入(当前 agent 自带
|
|
1117
|
+
// 上一轮上下文)。
|
|
1118
|
+
try {
|
|
1119
|
+
const priorRound = collectSameStagePriorReview(tierRuntimeRoot, stageName, changeName)
|
|
1120
|
+
if (priorRound) {
|
|
1121
|
+
const blockMd = renderPriorRoundFindingsMd(priorRound)
|
|
1122
|
+
priorFactsMd = priorFactsMd ? priorFactsMd + blockMd : blockMd
|
|
1123
|
+
}
|
|
1124
|
+
} catch {}
|
|
1085
1125
|
promptText = promptText
|
|
1086
1126
|
.split('{REVIEW_TIER}').join(tier.tier)
|
|
1087
1127
|
.split('{REVIEW_TIER_REASON}').join(tier.reason)
|
|
1088
1128
|
.split('{STAGE_REVIEW_RUN_ID}').join(reviewRunId)
|
|
1089
1129
|
.split('{REVIEW_JSON_CONTRACT}').join(reviewContractMd)
|
|
1130
|
+
.split('{PRIOR_REVIEW_FACTS}').join(priorFactsMd)
|
|
1090
1131
|
} catch (e) {
|
|
1091
1132
|
// 降级 self,避免 prompt 残留占位符
|
|
1092
1133
|
promptText = promptText
|
|
@@ -1094,6 +1135,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1094
1135
|
.split('{REVIEW_TIER_REASON}').join('分级异常降级 self: ' + e.message)
|
|
1095
1136
|
.split('{STAGE_REVIEW_RUN_ID}').join('review-unknown')
|
|
1096
1137
|
.split('{REVIEW_JSON_CONTRACT}').join('(review 契约注入失败,按 schemaVersion=1 + reviewType + verdicts∈pass/fail/cannot_verify + reviewedFiles + docHash=主文档 sha256 产出)')
|
|
1138
|
+
.split('{PRIOR_REVIEW_FACTS}').join('')
|
|
1097
1139
|
}
|
|
1098
1140
|
}
|
|
1099
1141
|
|
|
@@ -1270,6 +1312,42 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1270
1312
|
promptText = promptText.split('{SCOPE_AUDIT_TABLE}').join(scopeTable)
|
|
1271
1313
|
}
|
|
1272
1314
|
|
|
1315
|
+
// {HANDOVER_SUMMARY}(2026-09-17-pass-cap-semantics task-04,FR-06 注入态):archive「确认
|
|
1316
|
+
// 归档」步的移交项清单注入——读 changes/<name>/verify-facts.json 的 facts.handover(task-01
|
|
1317
|
+
// 产出 { count, items[] },items 行含 type/item/condition/severity),blocking 置顶 + severity
|
|
1318
|
+
// 标注 + 封顶渲染(maxRows 8,对齐 {SCOPE_AUDIT_TABLE} 60/量级收窄——移交项是裁决面不是
|
|
1319
|
+
// 对账面),表尾指路 verify-facts.json 看全量。fail-soft:handover 段缺失/零条目/facts.json
|
|
1320
|
+
// 缺失 → 空串(正常态零输出);读取异常降级单行指引——注入永不阻断归档 prompt 输出
|
|
1321
|
+
//(与门阻断语义分离,对齐 {SCOPE_AUDIT_TABLE} 注入先例)。
|
|
1322
|
+
if (stageName === 'archive' && promptText.includes('{HANDOVER_SUMMARY}')) {
|
|
1323
|
+
let handoverBlock = ''
|
|
1324
|
+
try {
|
|
1325
|
+
const hsSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
1326
|
+
const hsFactsPath = changeName ? join(hsSpecBase, 'changes', changeName, 'verify-facts.json') : null
|
|
1327
|
+
if (hsFactsPath && existsSync(hsFactsPath)) {
|
|
1328
|
+
const hsFacts = JSON.parse(readFileSync(hsFactsPath, 'utf8'))
|
|
1329
|
+
const hsItems = (hsFacts && hsFacts.handover && Array.isArray(hsFacts.handover.items)) ? hsFacts.handover.items : []
|
|
1330
|
+
if (hsItems.length > 0) {
|
|
1331
|
+
const HS_MAX_ROWS = 8
|
|
1332
|
+
const sevOf = (it) => (it && it.severity === 'blocking') ? 'blocking' : 'advisory'
|
|
1333
|
+
const sorted = [...hsItems].sort((a, b) => (sevOf(a) === 'blocking' ? 0 : 1) - (sevOf(b) === 'blocking' ? 0 : 1))
|
|
1334
|
+
const rows = sorted.slice(0, HS_MAX_ROWS).map((it) => {
|
|
1335
|
+
const cond = it && it.condition ? ` | 条件: ${it.condition}` : ''
|
|
1336
|
+
return `- [${sevOf(it)}] ${(it && it.type) || 'unknown'} | ${(it && it.item) || ''}${cond}`
|
|
1337
|
+
})
|
|
1338
|
+
handoverBlock = [
|
|
1339
|
+
`【移交项清单(CLI 机械注入,共 ${hsItems.length} 条,blocking 置顶)】`,
|
|
1340
|
+
...rows,
|
|
1341
|
+
...(sorted.length > HS_MAX_ROWS ? [`……(其余 ${sorted.length - HS_MAX_ROWS} 条略——全量见 verify-facts.json)`] : []),
|
|
1342
|
+
].join(String.fromCharCode(10))
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
} catch (e) {
|
|
1346
|
+
handoverBlock = '(移交项清单注入失败:' + (e && e.message ? e.message : e) + '——回退读 changes/<变更名>/verify-facts.json 的 handover 段核对后再确认归档,注入失败不阻断归档)'
|
|
1347
|
+
}
|
|
1348
|
+
promptText = promptText.split('{HANDOVER_SUMMARY}').join(handoverBlock)
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1273
1351
|
// 注入模块上下文(brainstorm/plan/execute 阶段全步 + quick 首步「理解任务」——刀①:quick
|
|
1274
1352
|
// step1 原让 agent cat module-map 再挑模块卡读,改为按任务描述匹配后注入,基于 Module Context Index)
|
|
1275
1353
|
const quickFirstStep = stageName === 'quick' && step && step.name === '理解任务'
|
package/src/run/shared.js
CHANGED
|
@@ -124,6 +124,12 @@ export function resolvePromptIncludes(text) {
|
|
|
124
124
|
* home 拒绝守卫:home 下的 .sillyspec 恒不命中——历史污染源(smoke 测试在 home 下临时目录
|
|
125
125
|
* 跑 CLI,_ensureDB 读路径即建库,~/.sillyspec 长出一整套平行进度库后,任何 home 子目录
|
|
126
126
|
* 跑命令都会向上撞它,污染自我延续)。撞 home 即停,回退 cwd/.sillyspec(子目录自建)。
|
|
127
|
+
* tmpdir 硬边界(坑 temp-root-stray-sillyspec,2026-09-16-friction5-hardening verify 移交项①
|
|
128
|
+
* 实证):某次 cwd=%TEMP% 直系的调用在 Temp 根遗留游离 .sillyspec 后,所有 temp 夹具的祖先
|
|
129
|
+
* 链都被它劫持(agent-automation-batch4『add frontend 建档』/feedback-batch2 两测试假败的真
|
|
130
|
+
* 根因——specBase 解析到 Temp\.sillyspec,夹具内断言读自身路径 ENOENT)。temp 下合法的
|
|
131
|
+
* .sillyspec 只会在夹具自身内部:起点在 tmpdir 子树内时,tmpdir 层及其上永不检查、走到即停
|
|
132
|
+
* (比 home 守卫更硬:home 是「跳过一层继续走」,tmp 是「整段封顶」——tmpdir 之上无合法项目根)。
|
|
127
133
|
* @param {string} cwd
|
|
128
134
|
* @param {object} [opts]
|
|
129
135
|
* @param {string} [opts.specDir] - 用户指定的 specDir(通过 --spec-dir 或 --spec-root)
|
|
@@ -132,11 +138,15 @@ export function resolvePromptIncludes(text) {
|
|
|
132
138
|
export function resolveSpecDir(cwd, opts = {}) {
|
|
133
139
|
if (opts.specDir) return resolve(opts.specDir)
|
|
134
140
|
const home = os.homedir()
|
|
141
|
+
const tmp = os.tmpdir()
|
|
135
142
|
const origin = resolve(cwd)
|
|
136
143
|
const originBelowHome = origin.startsWith(home + sep) // 严格子目录,不含 home 自身
|
|
144
|
+
const originInTmp = origin === tmp || origin.startsWith(tmp + sep) // 含 tmpdir 自身
|
|
137
145
|
let dir = origin
|
|
138
146
|
let passedHome = false
|
|
139
147
|
while (true) {
|
|
148
|
+
// tmpdir 硬边界:起点在 tmpdir 子树内时,走到 tmpdir 层即封顶(该层及其上是游离库温床,永不命中)
|
|
149
|
+
if (originInTmp && dir === tmp) break
|
|
140
150
|
// home 拒绝守卫:起点在 home 子树内时,home 层不检查 .sillyspec,
|
|
141
151
|
// 防止 home 子目录遍历经过 home 误命中 ~/.sillyspec;
|
|
142
152
|
// passedHome:经过 home 后不再检查(覆盖遍历到真实 home 的场景)。
|
|
@@ -274,6 +274,20 @@ const PLAN_RULES = [
|
|
|
274
274
|
spec: 'large 变更产出 `module-impact.md` 首版(plan review_plan 步生成;scale=small 豁免)',
|
|
275
275
|
failMessage: 'module-impact.md 缺失: ${path}(large 变更应在 plan 审查计划步生成模块影响分析首版)',
|
|
276
276
|
},
|
|
277
|
+
// 全局硬约束段存在性(ql-20260917-003,warning——ql-20260917-002 落地段的事后校验):plan_level=full
|
|
278
|
+
// 的 plan.md 应含「全局硬约束」段(design.md 跨 task 硬约束逐字抄录,执行期子代理只读自己的
|
|
279
|
+
// 任务卡+本段)。condition 生效依赖 validatePlanOutputs 把 plan.md frontmatter plan_level 传入
|
|
280
|
+
// ctx;读不到(无 frontmatter/旧格式/light)→ eq:'full' 不成立跳过(存量零误报 fail-safe)。
|
|
281
|
+
// 确无跨 task 硬约束时写一段「无」占位即可消警(段在场即过,不校验段体非空——空体由 execute
|
|
282
|
+
// 注入端容忍+审查目检)。
|
|
283
|
+
{
|
|
284
|
+
id: 'plan.global-constraints', stage: 'plan', source: 'validatePlanOutputs', severity: 'warning', kind: 'literal-any',
|
|
285
|
+
target: { root: 'change', path: 'plan.md', scope: 'full' },
|
|
286
|
+
data: { literals: ['全局硬约束'] },
|
|
287
|
+
condition: { ctxField: 'planLevel', eq: 'full' },
|
|
288
|
+
spec: 'plan_level=full 的 plan.md 含「全局硬约束」段(design.md 跨 task 硬约束逐字抄录;无跨 task 硬约束时写「无」占位消警)',
|
|
289
|
+
failMessage: 'plan.md 缺少「## 全局硬约束」段(plan_level=full:从 design.md 逐字抄录绑定所有 task 的硬约束,执行期子代理只读自己的任务卡+本段;确无跨 task 硬约束时写一段「无」占位消警)',
|
|
290
|
+
},
|
|
277
291
|
// entry-point-wiring(custom):design.md 命中入口实例化/启动路径模式时,被提到的入口文件
|
|
278
292
|
// (cli.ts/main.ts/server.(js|ts)/index.(js|ts))必须出现在某 task allowed_paths 或 plan.md
|
|
279
293
|
// 文件变更清单中,否则需 design.md 紧邻写明豁免。判定算法(多源 allowed_paths 收集 + 逐文件对账 +
|
package/src/stage-contract.js
CHANGED
|
@@ -17,6 +17,9 @@ import { evaluateRules } from './stage-contract-engine.js'
|
|
|
17
17
|
import { getRule } from './stage-contract-spec.js'
|
|
18
18
|
import { backfillFrontmatter } from './scan-postcheck.js'
|
|
19
19
|
import { parseAllowedPaths } from './stages/plan-postcheck.js'
|
|
20
|
+
// design.md 文件清单解析(2026-09-17-pass-cap-semantics task-02 事实③):change-list 是纯 fs
|
|
21
|
+
// 叶子模块(design-facts / scope-audit / verify-probes 等同款直连惯例),静态引入无环。
|
|
22
|
+
import { parseFileChangeListDetailed } from './change-list.js'
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
25
|
* 校验结果
|
|
@@ -403,8 +406,17 @@ function validatePlanOutputs(cwd, changeName, context = {}) {
|
|
|
403
406
|
const sm = fm && fm[1].match(/^scale:[ \t]*['"]?(\w+)/m)
|
|
404
407
|
if (sm) scale = sm[1]
|
|
405
408
|
}
|
|
409
|
+
// plan.md frontmatter plan_level 传入引擎 ctx(ql-20260917-003):plan.global-constraints warning
|
|
410
|
+
// 的 condition 用。读不到(无 frontmatter/旧格式)→ undefined → eq:'full' 不成立 → 规则跳过
|
|
411
|
+
// (存量零误报 fail-safe,同 scale 读取模式)。
|
|
412
|
+
let planLevel = null
|
|
413
|
+
if (existsSync(planFile)) {
|
|
414
|
+
const pfm = readFileSync(planFile, 'utf8').match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n/)
|
|
415
|
+
const plm = pfm && pfm[1].match(/^plan_level:[ \t]*['"]?(\w+)/m)
|
|
416
|
+
if (plm) planLevel = plm[1]
|
|
417
|
+
}
|
|
406
418
|
// plan.md 存在性由引擎消费 manifest。entryPoint/id-trace/decisions 为 custom kind(下方保留)。
|
|
407
|
-
const engineResult = evaluateRules('plan', { changeDir, scale })
|
|
419
|
+
const engineResult = evaluateRules('plan', { changeDir, scale, planLevel })
|
|
408
420
|
const errors = [...engineResult.errors]
|
|
409
421
|
const warnings = [...engineResult.warnings]
|
|
410
422
|
|
|
@@ -634,12 +646,23 @@ function validateVerifyOutputs(cwd, changeName, context = {}) {
|
|
|
634
646
|
`若属关键词误伤(实际未触碰 daemon/session/启动入口/跨进程),可在 design.md frontmatter 加 risk_level: <真实等级>(如 unit-sufficient)显式覆盖后重跑。`
|
|
635
647
|
)
|
|
636
648
|
}
|
|
637
|
-
// 显式 risk_level
|
|
638
|
-
//
|
|
639
|
-
//
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
649
|
+
// ── 显式 risk_level 豁免洞分层(D-002@v1,2026-09-17-pass-cap-semantics task-02)──
|
|
650
|
+
// ① explicit + 降级 unit-sufficient/contract-required:不进本 critical 块(上方 level 门),
|
|
651
|
+
// PASS WITH NOTES 维持免证据——关键词误伤逃生保留;
|
|
652
|
+
// ② explicit 且仍 integration/deployment-critical + PASS WITH NOTES:必须携带结构化 handover
|
|
653
|
+
// (facts.handover 有效行;blocking 级计入封顶口径同 validatePassEligibility 条件②)或齐全
|
|
654
|
+
// 集成证据,二选一——无 handover 即挂证据门(checkIntegrationEvidence 不过 → error);
|
|
655
|
+
// ③ 非显式(关键词判级)判定式一字不动(防 PASS WITH NOTES 绕证据门控)。
|
|
656
|
+
const notesExplicitCritical = conclusion === 'PASS WITH NOTES' && changeRiskProfile.explicit
|
|
657
|
+
const handoverRows = notesExplicitCritical ? countFactsHandoverItems(changeDir) : 0
|
|
658
|
+
const requiresEvidence = conclusion === 'PASS'
|
|
659
|
+
|| (conclusion === 'PASS WITH NOTES' && (!changeRiskProfile.explicit || handoverRows === 0))
|
|
660
|
+
if (notesExplicitCritical) {
|
|
661
|
+
if (handoverRows > 0) {
|
|
662
|
+
warnings.push(`[${changeRiskProfile.level}] 结论 PASS WITH NOTES:design frontmatter 显式声明 risk_level=${changeRiskProfile.level},缺口由结构化移交项承载(facts.handover ${handoverRows} 行,「## 移交项(结构化)」)——blocking 级行在结论=PASS 时按封顶口径拦截(validatePassEligibility 条件②),请如实分行勿漏报。`)
|
|
663
|
+
} else {
|
|
664
|
+
warnings.push(`[${changeRiskProfile.level}] 结论 PASS WITH NOTES:显式声明 risk_level=${changeRiskProfile.level} 且「## 移交项(结构化)」零有效行——按齐全集成证据口径校验(二选一:补结构化移交项承载缺口,或提供齐全集成证据;两者皆缺将 error)。`)
|
|
665
|
+
}
|
|
643
666
|
}
|
|
644
667
|
if (requiresEvidence) {
|
|
645
668
|
// CLI 回执注入(坑 verify-literal-evidence-mismatch,2026-08-22 实证:证据第一轮就齐
|
|
@@ -677,6 +700,11 @@ function validateVerifyOutputs(cwd, changeName, context = {}) {
|
|
|
677
700
|
extraEvidenceText: receiptText,
|
|
678
701
|
...(runtimeEvidence ? { runtimeEvidence } : {}),
|
|
679
702
|
...(context.verifyStartAt ? { verifyStartAt: context.verifyStartAt } : {}),
|
|
703
|
+
// 回执来源声明透传(X-10 / D-006,2026-09-17-pass-cap-semantics task-02):调用侧只透传
|
|
704
|
+
// context 携带的声明源(quality-scan 记录的命令 / verify_precedents 声明同思想),不在
|
|
705
|
+
// 本侧做任何分类——分类打标单点在 change-risk-profile(auditRuntimeReceipt 内按回执
|
|
706
|
+
// command 来源 classifyReceiptSourceTag);未透传时按各回执 command 就地分类。
|
|
707
|
+
...(context.receiptSourceTag ? { sourceTag: context.receiptSourceTag } : {}),
|
|
680
708
|
cwd,
|
|
681
709
|
specBase: context.specRoot || join(cwd, '.sillyspec'),
|
|
682
710
|
})
|
|
@@ -820,6 +848,12 @@ export function extractAcceptanceMatrixSlots(verifyMd) {
|
|
|
820
848
|
* - 段缺失:严格档(isIrStrictVerifyChange / IR_STRICT_SINCE 同源常量,与结论槽
|
|
821
849
|
* 「删槽回退已关闭」同口径)ERROR;非严格(存量变更)warning 提示补段不阻断
|
|
822
850
|
* - 段在场:unfilled / missingEvidence > 0 → ERROR 列出违规行(task+acceptance 截断 40 字)
|
|
851
|
+
* - 段在场:partial/uncovered 行 >0 且 facts.handover 零有效行(任意 severity)→ ERROR
|
|
852
|
+
* 「部分实现必须有移交去向」(D-003/FR-04,task-03;矩阵 MD 槽解析即防篡改锚点 X-05——
|
|
853
|
+
* 不读 facts.matrixPartialRows producer 快照,锚定当前文档实态;factsExpected=false 存量
|
|
854
|
+
* 零行为变化,factsExpected=true 而 facts 缺失 → fail-closed 按零有效行处理)
|
|
855
|
+
* - 同分支放行路径 advisory(R-05 第一版):handover 有效行在场时逐行核对行标识(task 锚 /
|
|
856
|
+
* acceptance 文本)↔ 条目 item/condition 文本命中,未命中 → console.warn 攒实证,不阻断
|
|
823
857
|
*/
|
|
824
858
|
function validateAcceptanceMatrix(cwd, changeName, context = {}) {
|
|
825
859
|
const { specRoot } = context
|
|
@@ -867,9 +901,279 @@ function validateAcceptanceMatrix(cwd, changeName, context = {}) {
|
|
|
867
901
|
`修复:在证据列补测试锚点(如 \`test/foo.test.mjs\` 或 \`src/x.js:42\`)或 non-testable 理由。`
|
|
868
902
|
)
|
|
869
903
|
}
|
|
904
|
+
|
|
905
|
+
// ── partial/uncovered × facts.handover 联动(D-003/FR-04,2026-09-17-pass-cap-semantics task-03)──
|
|
906
|
+
// 部分实现必须有移交去向:矩阵含 partial/uncovered 行且 facts.handover 零有效行(任意
|
|
907
|
+
// severity)→ error;有行 → 本分支放行,blocking 级是否封顶归 validatePassEligibility 条件②
|
|
908
|
+
// (④管「有去向」、②管「去向是否 blocking」)。handover 数据只读 facts.handover(task-01
|
|
909
|
+
// producer 四列产出;不在此重复解析 MD 移交项表——防篡改锚点由上方矩阵 MD 槽解析承担,X-05)。
|
|
910
|
+
// factsExpected 口径复用 task-02 判定式:false(存量未跑管线)→ 分支空转零行为变化;
|
|
911
|
+
// true 而 facts 缺失 → 按零有效行 fail-closed,文案附「重跑 verify-probes」出路。
|
|
912
|
+
// 放行路径上做逐行关联 advisory(R-05 第一版:验收项 ID ↔ handover 条目文本命中,只攒实证
|
|
913
|
+
// 不做硬门):行标识(task / acceptance 文本)未命中任何条目 → console.warn 提示,不阻断不进 errors。
|
|
914
|
+
const partialRows = matrix.rows.filter(r => r.verdict === 'partial' || r.verdict === 'uncovered')
|
|
915
|
+
if (partialRows.length > 0 && resolveFactsExpected(changeDir)) {
|
|
916
|
+
const facts = readFactsForEligibility(changeDir)
|
|
917
|
+
const factsMissing = facts === null
|
|
918
|
+
// 有效行口径同 countFactsHandoverItems(task-02):只计对象行;此处需逐条取文本故就地展开
|
|
919
|
+
const handoverItems = facts && facts.handover && Array.isArray(facts.handover.items)
|
|
920
|
+
? facts.handover.items.filter(it => it && typeof it === 'object') : []
|
|
921
|
+
if (handoverItems.length === 0) {
|
|
922
|
+
const list = partialRows.map(rowLabel).join(';')
|
|
923
|
+
errors.push(
|
|
924
|
+
`探针 7 验收×测试覆盖矩阵含 ${partialRows.length} 行 partial/uncovered 且「## 移交项(结构化)」零有效行(facts.handover)——部分实现必须有移交去向:${list}。` +
|
|
925
|
+
`修复:在 verify-result.md 补「## 移交项(结构化)」有效行(任意 severity 均可,blocking 级另受 PASS 封顶约束),或修正矩阵判定(实际已覆盖的行改 covered),或降级结论为 PASS WITH NOTES 承载。` +
|
|
926
|
+
(factsMissing
|
|
927
|
+
? `另:verify-facts.json 缺失/不可读而 factsExpected=true,fail-closed 按零有效行处理——重跑 verify-probes(\`sillyspec verify-probes --change ${changeName} --init\` 幂等刷新底稿)。`
|
|
928
|
+
: '')
|
|
929
|
+
)
|
|
930
|
+
} else {
|
|
931
|
+
// 逐行关联 advisory(R-05 第一版,D-003):行标识(task 锚 / acceptance 文本)命中任一
|
|
932
|
+
// handover 条目的 item/condition 文本即视为有去向;未命中 → console.warn 攒实证(文本
|
|
933
|
+
// 命中是弱关联,误报/漏报均可能——先 advisory 攒数据,硬门等实证后再议),不阻断。
|
|
934
|
+
const itemTexts = handoverItems.map(it => `${it.item || ''}|${it.condition || ''}`)
|
|
935
|
+
for (const r of partialRows) {
|
|
936
|
+
const keys = [r.task, r.acceptance].map(k => String(k || '').trim()).filter(Boolean)
|
|
937
|
+
const hit = keys.some(k => itemTexts.some(t => t.includes(k)))
|
|
938
|
+
if (!hit) {
|
|
939
|
+
console.warn(`ℹ️ [advisory] 验收项 ${rowLabel(r)} 的移交去向未在 handover 条目中命中——建议条目文本含该标识(advisory,攒实证,D-003/R-05;不阻断)`)
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
870
944
|
return { ok: errors.length === 0, errors, warnings }
|
|
871
945
|
}
|
|
872
946
|
|
|
947
|
+
// ============ PASS 封顶事实面 validator(2026-09-17-pass-cap-semantics task-02 / D-001@v2 / D-010 / D-011) ============
|
|
948
|
+
//
|
|
949
|
+
// 「要不要集成证据」(validateVerifyOutputs 的 requiresEvidence)与「能不能写 PASS」(本
|
|
950
|
+
// validator)解耦:结论=PASS 时四个已知未验证区任一在场即 error——
|
|
951
|
+
// ① 集成实测未跑(facts.integrationRan=not-ran,D-006 判定表)
|
|
952
|
+
// ② blocking 级移交项在场(facts.handover[].severity,D-005@v2——④管「有去向」、②管「去向是否 blocking」)
|
|
953
|
+
// ③ db 脚本未声明执行(verify 时点文件集 ∩ db/**/*.sql 对账 facts.dbScriptDeclarations,D-012)
|
|
954
|
+
// ④ 矩阵含 partial/uncovered 且移交项零有效行(facts.matrixPartialRows × facts.handover)
|
|
955
|
+
// 附加:runtimeEndpointExcluded=true 且判级 integration/deployment-critical 且 handover 零行
|
|
956
|
+
// → 计入①事实面(D-004/FR-03,仅判级时计入)。
|
|
957
|
+
//
|
|
958
|
+
// 分层纪律(D-011):事实生产走 facts 管线(verify-probes backfillFactsFromMdAndTests 首次
|
|
959
|
+
// backfill 主路径无条件产出,X-08 时序——gates 收尾前置 backfill 先于 runValidators);本壳只做
|
|
960
|
+
// 取数组装,判定是 evaluatePassEligibility 纯函数(零 MD 解析、零 IO——MD 锚点防篡改兜底并入
|
|
961
|
+
// checkProbeConsistency 抽查面属 task-03)。双源 fail-closed:factsExpected=true 而 facts 缺失
|
|
962
|
+
// (篡改/管线故障)→ 全条件按触发拦下;factsExpected=false(存量未跑 --init)→ 兼容 ok 不误伤。
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* 读 <changeDir>/verify-facts.json(缺失/不可解析/非对象 → null)——与 checkProbeConsistency
|
|
966
|
+
* 的 readVerifyFacts(verify-postcheck.js)同口径「有 facts」判别。不静态 import
|
|
967
|
+
* verify-postcheck/verify-probes(verify-probes 顶层 await 动态 import 回指本模块,静态依赖
|
|
968
|
+
* 会在其 TLA 上成环死锁;分层单向,全局硬约束 3),就地等价实现。
|
|
969
|
+
*/
|
|
970
|
+
function readFactsForEligibility(changeDir) {
|
|
971
|
+
const factsPath = join(changeDir, 'verify-facts.json')
|
|
972
|
+
if (!existsSync(factsPath)) return null
|
|
973
|
+
try {
|
|
974
|
+
const parsed = JSON.parse(readFileSync(factsPath, 'utf8'))
|
|
975
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null
|
|
976
|
+
} catch { return null }
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* factsExpected 判定(D-011 双源 fail-closed 的「应写」侧):isIrStrictVerifyChange(严格档
|
|
981
|
+
* 变更删 facts.json 另由 checkProbeConsistency error 级 MD 锚点兜底拦,通道 gates probeBlocked)
|
|
982
|
+
* || verify-facts.json 在场(readVerifyFacts 同口径,含探针子节判别的「有 facts」面)。
|
|
983
|
+
* factsExpected=false(存量未跑管线)→ 封顶兼容 skip 不误伤。
|
|
984
|
+
* @param {string} changeDir 变更目录
|
|
985
|
+
* @returns {boolean}
|
|
986
|
+
*/
|
|
987
|
+
export function resolveFactsExpected(changeDir) {
|
|
988
|
+
return isIrStrictVerifyChange(changeDir) || readFactsForEligibility(changeDir) !== null
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/** facts.handover 有效行数(producer parseHandoverRows 已滤占位/表头行;无 facts/handover → 0) */
|
|
992
|
+
function countFactsHandoverItems(changeDir) {
|
|
993
|
+
const facts = readFactsForEligibility(changeDir)
|
|
994
|
+
const items = facts && facts.handover && Array.isArray(facts.handover.items) ? facts.handover.items : []
|
|
995
|
+
return items.filter(it => it && typeof it === 'object').length
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* PASS 资格事实面封顶纯函数(X-09 双层形态的判定层:零 MD 解析、零 IO,消费 task-01 契约字段)。
|
|
1000
|
+
* @param {{
|
|
1001
|
+
* conclusion?: string|null,
|
|
1002
|
+
* facts?: object|null,
|
|
1003
|
+
* factsExpected?: boolean,
|
|
1004
|
+
* changeRiskProfile?: { level?: string }|null,
|
|
1005
|
+
* changeName?: string,
|
|
1006
|
+
* dbScriptCandidates?: string[],
|
|
1007
|
+
* }} [args] dbScriptCandidates = 壳内取数的 verify 时点文件集(design 清单 ∪ worktree 变更)∩ db 目录任意层级 .sql
|
|
1008
|
+
* @returns {{ ok: boolean, errors: string[], triggered: Array<{ fact: 'integration-not-run'|'blocking-handover-present'|'db-script-undeclared'|'matrix-partial-no-handover', detail: string }> }}
|
|
1009
|
+
*/
|
|
1010
|
+
export function evaluatePassEligibility(args) {
|
|
1011
|
+
const errors = []
|
|
1012
|
+
const triggered = []
|
|
1013
|
+
const a = args || {}
|
|
1014
|
+
// factsExpected=false(存量未跑管线)→ 沿用存量兼容口径 ok 不误伤(checkProbeConsistency 同款边界)
|
|
1015
|
+
if (!a.factsExpected) return { ok: true, errors, triggered }
|
|
1016
|
+
// 封顶只管 PASS:结论 ≠ PASS(FAIL / PASS WITH NOTES / 未识别未填)→ 直接 ok
|
|
1017
|
+
if (a.conclusion !== 'PASS') return { ok: true, errors, triggered }
|
|
1018
|
+
|
|
1019
|
+
const factsMissing = !a.facts || typeof a.facts !== 'object'
|
|
1020
|
+
const facts = factsMissing ? {} : a.facts
|
|
1021
|
+
const handoverItems = (facts.handover && Array.isArray(facts.handover.items)) ? facts.handover.items : []
|
|
1022
|
+
const blockingRows = handoverItems.filter(it => it && it.severity === 'blocking')
|
|
1023
|
+
const criticalLevel = a.changeRiskProfile && ['integration-critical', 'deployment-critical'].includes(a.changeRiskProfile.level)
|
|
1024
|
+
const NOTES_FIX = '改写结论为 PASS WITH NOTES 并在「## 移交项(结构化)」表格如实分行(类型枚举 env-blocked/manual-acceptance/db-script/other)'
|
|
1025
|
+
|
|
1026
|
+
// ── factsExpected=true 而 facts 缺失 → 双源 fail-closed:全条件按触发(D-011),附重跑 verify-probes 出路(R-07)──
|
|
1027
|
+
if (factsMissing) {
|
|
1028
|
+
errors.push(
|
|
1029
|
+
`[pass-eligibility] verify-facts.json 缺失/不可读但 factsExpected=true(严格档或底稿曾在场后消失——疑似删除/管线故障),按双源 fail-closed 全条件触发拦下。` +
|
|
1030
|
+
`出路:重跑 verify-probes(sillyspec verify-probes --change ${a.changeName || '<变更名>'} --init 幂等刷新底稿后重跑 --done 回填),或${NOTES_FIX}降级承载。`
|
|
1031
|
+
)
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// ① 集成实测未跑(时序口径 X-01:validator 时点只认已落盘 quality-scan 记录,被拦出路=重跑质量扫描步或降级 NOTES,不算失败)
|
|
1035
|
+
if (factsMissing || facts.integrationRan === 'not-ran') {
|
|
1036
|
+
triggered.push({ fact: 'integration-not-run', detail: factsMissing ? 'facts 缺失(fail-closed 按未跑处理)' : 'facts.integrationRan=not-ran' })
|
|
1037
|
+
errors.push(
|
|
1038
|
+
`[fact integration-not-run] 集成实测未跑(${factsMissing ? 'verify-facts.json 缺失,fail-closed 按未跑处理' : 'facts.integrationRan=not-ran'})——结论=PASS 须集成实测已跑(D-006 判定表:quality-scan 实测记录(skip 除外)或跨层回执条目)。` +
|
|
1039
|
+
`出路:重跑质量扫描步落实测记录,或提供跨层实测回执后重跑 verify-probes 刷新 facts;仍未跑则${NOTES_FIX}承载(不算失败)。`
|
|
1040
|
+
)
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// ② blocking 级移交项在场(blocking 移交项与结论 PASS 互斥)
|
|
1044
|
+
if (factsMissing || blockingRows.length > 0) {
|
|
1045
|
+
const list = blockingRows.map(it => `${it.type || '未知类型'}|${String(it.item || '').slice(0, 60)}`).join(';')
|
|
1046
|
+
triggered.push({ fact: 'blocking-handover-present', detail: factsMissing ? 'facts 缺失(fail-closed)' : `${blockingRows.length} 条 blocking 行:${list}` })
|
|
1047
|
+
errors.push(
|
|
1048
|
+
`[fact blocking-handover-present] ${factsMissing
|
|
1049
|
+
? '移交项 blocking 级行无法核对(verify-facts.json 缺失,fail-closed 按在场触发)'
|
|
1050
|
+
: `移交项含 ${blockingRows.length} 条 blocking 级行——承认有必须兜底的未竟事项即不是 PASS(④管「有去向」、②管「去向是否 blocking」)`}。` +
|
|
1051
|
+
`触发行:${factsMissing ? '(facts 缺失,无法核对——fail-closed)' : list}。修复:${NOTES_FIX};确属 advisory 的按降级文法「(降级:<理由>,依据 <file:line 或 D-xxx>)」显式降级。`
|
|
1052
|
+
)
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// ③ db 脚本未声明执行(D-012:verify 时点按声明面/diff 对账,apply/archive 兜底门归 task-04;零连库)
|
|
1056
|
+
const candidates = Array.isArray(a.dbScriptCandidates) ? a.dbScriptCandidates : []
|
|
1057
|
+
const declared = factsMissing ? [] : (Array.isArray(facts.dbScriptDeclarations) ? facts.dbScriptDeclarations : [])
|
|
1058
|
+
const undeclared = candidates.filter(f => !declared.includes(f))
|
|
1059
|
+
if (undeclared.length > 0) {
|
|
1060
|
+
triggered.push({ fact: 'db-script-undeclared', detail: `${undeclared.join('、')} 未进 facts.dbScriptDeclarations` })
|
|
1061
|
+
errors.push(
|
|
1062
|
+
`[fact db-script-undeclared] db 脚本在 verify 时点文件集(design 清单 ∪ worktree 变更)∩ db/**/*.sql 中但未声明执行:${undeclared.join('、')}。` +
|
|
1063
|
+
`修复:已对目标库执行则在 verify-result.md 补声明(「已对目标库执行:db/<file>.sql」或回执槽 command 含该文件)后重跑 verify-probes 刷新 facts;未执行则${NOTES_FIX}承载(db-script 类恒 blocking)。`
|
|
1064
|
+
)
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// ④ 矩阵含 partial/uncovered 且移交项零有效行(任意 severity——部分实现必须有移交去向)
|
|
1068
|
+
if (factsMissing || ((facts.matrixPartialRows || 0) > 0 && handoverItems.length === 0)) {
|
|
1069
|
+
triggered.push({ fact: 'matrix-partial-no-handover', detail: factsMissing ? 'facts 缺失(fail-closed)' : `matrixPartialRows=${facts.matrixPartialRows} 且 handover 零有效行` })
|
|
1070
|
+
errors.push(
|
|
1071
|
+
`[fact matrix-partial-no-handover] 验收×测试覆盖矩阵含 ${factsMissing ? '(facts 缺失,无法核对——fail-closed)' : `${facts.matrixPartialRows} 行 partial/uncovered`} 且「## 移交项(结构化)」零有效行——部分实现必须有移交去向。修复:${NOTES_FIX}承载(任意 severity 均可,blocking 级另受条件②约束)。`
|
|
1072
|
+
)
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// ── 附加条件(D-004/FR-03,仅判级 integration/deployment-critical 时计入①事实面):
|
|
1076
|
+
// Runtime Evidence「服务端点」行自声明「不涉及」(facts.runtimeEndpointExcluded,X-18 文法)
|
|
1077
|
+
// 且移交项零行——端点不得以「不涉及」免检,须真实回执或 handover 承载。
|
|
1078
|
+
if (criticalLevel && (factsMissing || facts.runtimeEndpointExcluded === true) && handoverItems.length === 0) {
|
|
1079
|
+
triggered.push({
|
|
1080
|
+
fact: 'integration-not-run',
|
|
1081
|
+
detail: factsMissing ? 'facts 缺失(fail-closed,runtimeEndpointExcluded 不可证伪)' : 'facts.runtimeEndpointExcluded=true 且 handover 零行',
|
|
1082
|
+
})
|
|
1083
|
+
errors.push(
|
|
1084
|
+
`[fact integration-not-run] 判级 ${a.changeRiskProfile.level} 且 Runtime Evidence 服务端点行自声明「不涉及」(facts.runtimeEndpointExcluded=true)且无移交项承载——端点不得以「不涉及」免检(D-004)。` +
|
|
1085
|
+
`出路:提供真实端点回执(跨层实测)后重跑 verify-probes,或${NOTES_FIX}承载。`
|
|
1086
|
+
)
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
return { ok: errors.length === 0, errors, triggered }
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* 事实③文件集取数(D-012,纯声明面/diff 对账零连库):design.md 文件清单
|
|
1094
|
+
* (change-list.parseFileChangeListDetailed——design-facts.validateDesignFileList 同源解析器)
|
|
1095
|
+
* ∪ worktree changed files(worktree meta baseHash..HEAD diff ∪ porcelain——checkExecuteCodeEvidence
|
|
1096
|
+
* 同款取数形态,与 task-review resolveAttributionDiffFiles 内 resolveVerifyChangedFiles 同源口径;
|
|
1097
|
+
* 本模块不静态 import verify-postcheck/verify-probes,TLA 动态环,就地等价实现)∩ db 目录任意层级 .sql。
|
|
1098
|
+
* @param {{ cwd: string, changeName: string, specRoot?: string|null, changeDir: string }} opts
|
|
1099
|
+
* @returns {string[]} 排序后的候选 db 脚本(posix 相对路径)
|
|
1100
|
+
*/
|
|
1101
|
+
function collectDbScriptCandidates({ cwd, changeName, specRoot, changeDir }) {
|
|
1102
|
+
const files = new Set()
|
|
1103
|
+
const add = (p) => {
|
|
1104
|
+
const n = String(p || '').trim().replace(/^"|"$/g, '').replace(/\\/g, '/')
|
|
1105
|
+
if (n && n !== '.sillyspec' && !n.startsWith('.sillyspec/')) files.add(n)
|
|
1106
|
+
}
|
|
1107
|
+
// design 清单半边(design.md 缺失/无清单段 → 空集,fail-soft,diff 半边仍覆盖)
|
|
1108
|
+
try {
|
|
1109
|
+
for (const entry of parseFileChangeListDetailed(join(changeDir, 'design.md'))) add(entry && entry.path)
|
|
1110
|
+
} catch { /* 解析异常退 diff 半边 */ }
|
|
1111
|
+
// worktree 变更半边:meta 双根候选(specBase 优先 + cwd/.sillyspec 兜底,_readWorktreeMeta 样板)
|
|
1112
|
+
try {
|
|
1113
|
+
const specBase = specRoot || join(cwd, '.sillyspec')
|
|
1114
|
+
const metaCandidates = [
|
|
1115
|
+
join(specBase, '.runtime', 'worktrees', changeName, 'meta.json'),
|
|
1116
|
+
join(cwd, '.sillyspec', '.runtime', 'worktrees', changeName, 'meta.json'),
|
|
1117
|
+
]
|
|
1118
|
+
let meta = null
|
|
1119
|
+
for (const p of metaCandidates) {
|
|
1120
|
+
if (existsSync(p)) {
|
|
1121
|
+
try { meta = JSON.parse(readFileSync(p, 'utf8')) } catch { meta = null }
|
|
1122
|
+
if (meta) break
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
if (meta && meta.baseHash) {
|
|
1126
|
+
const gitDir = (meta.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath))
|
|
1127
|
+
? meta.worktreePath
|
|
1128
|
+
: cwd
|
|
1129
|
+
const diff = gitTry(gitDir, ['diff', '--name-only', `${meta.baseHash}..HEAD`])
|
|
1130
|
+
if (diff.ok) for (const f of diff.out.split('\n')) add(f)
|
|
1131
|
+
const status = gitTry(gitDir, ['status', '--porcelain'])
|
|
1132
|
+
if (status.ok) {
|
|
1133
|
+
for (const line of status.out.split('\n')) {
|
|
1134
|
+
if (!line.trim()) continue
|
|
1135
|
+
add(line.slice(3).trim().split(' -> ').pop())
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
} catch { /* meta/git 取数失败退 design 清单半边(fail-soft) */ }
|
|
1140
|
+
return [...files].filter(p => /^db\/.+\.sql$/i.test(p)).sort()
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
* PASS 封顶 validator 注册壳(与 validateAcceptanceMatrix 同三参签名同构,D-010:注册进
|
|
1145
|
+
* contracts.verify.validators 即覆盖 gates / machine-interface 等全部 runValidators 调用方,
|
|
1146
|
+
* gates.js 零改动)。壳内取数组装:verify-result.md 结论(槽优先,legacy 回退)、
|
|
1147
|
+
* verify-facts.json(缺失容 null)、detectChangeRisk 判级、事实③文件集;判定调
|
|
1148
|
+
* evaluatePassEligibility 纯函数。
|
|
1149
|
+
*/
|
|
1150
|
+
export function validatePassEligibility(cwd, changeName, context = {}) {
|
|
1151
|
+
const { specRoot } = context
|
|
1152
|
+
const errors = []
|
|
1153
|
+
const warnings = []
|
|
1154
|
+
const changeDir = resolveChangeDir(cwd, changeName, specRoot)
|
|
1155
|
+
// 变更目录缺失 → 引擎 manifest 存在性规则兜底(validateVerifyOutputs 同名 error),此处 no-op
|
|
1156
|
+
if (!existsSync(changeDir)) return { ok: true, errors, warnings }
|
|
1157
|
+
// verify-result.md 中间步骤未落盘 → no-op(同 validateAcceptanceMatrix:存在性归引擎末步规则)
|
|
1158
|
+
const verifyResultPath = join(changeDir, 'verify-result.md')
|
|
1159
|
+
if (!existsSync(verifyResultPath)) return { ok: true, errors, warnings }
|
|
1160
|
+
|
|
1161
|
+
const conclusion = resolveVerifyConclusion(readFileSync(verifyResultPath, 'utf8'))
|
|
1162
|
+
const facts = readFactsForEligibility(changeDir)
|
|
1163
|
+
const result = evaluatePassEligibility({
|
|
1164
|
+
conclusion,
|
|
1165
|
+
facts,
|
|
1166
|
+
factsExpected: isIrStrictVerifyChange(changeDir) || facts !== null,
|
|
1167
|
+
changeRiskProfile: detectChangeRisk({
|
|
1168
|
+
designContent: readIfExists(join(changeDir, 'design.md')),
|
|
1169
|
+
planContent: readIfExists(join(changeDir, 'plan.md')),
|
|
1170
|
+
}),
|
|
1171
|
+
changeName,
|
|
1172
|
+
dbScriptCandidates: collectDbScriptCandidates({ cwd, changeName, specRoot, changeDir }),
|
|
1173
|
+
})
|
|
1174
|
+
return { ok: result.ok, errors: result.errors, warnings }
|
|
1175
|
+
}
|
|
1176
|
+
|
|
873
1177
|
/**
|
|
874
1178
|
* 计算归档目标目录名:保持原变更名不变,直接移入 archive/。
|
|
875
1179
|
*/
|
|
@@ -1064,7 +1368,9 @@ const contracts = {
|
|
|
1064
1368
|
allowedTo: ['archive'],
|
|
1065
1369
|
// validateAcceptanceMatrix:探针 7 矩阵槽 fail-closed(2026-09-14-acceptance-test-matrix FR-02),
|
|
1066
1370
|
// 注册进 validator 链即覆盖 gates / machine-interface 等全部 runValidators 调用方(gates.js 零改动)。
|
|
1067
|
-
|
|
1371
|
+
// validatePassEligibility:PASS 封顶事实面(2026-09-17-pass-cap-semantics task-02 / D-001@v2 /
|
|
1372
|
+
// D-010)——注册即覆盖同上;回退 = 移除本注册行(纯加法,未触发四条件行为零变化)。
|
|
1373
|
+
validators: [validateVerifyOutputs, validateAcceptanceMatrix, validatePassEligibility],
|
|
1068
1374
|
},
|
|
1069
1375
|
archive: {
|
|
1070
1376
|
stage: 'archive',
|