sillyspec 3.31.0 → 3.32.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/sillyspec-auto/SKILL.md +1 -1
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +2 -2
- package/.claude/skills/sillyspec-commit/SKILL.md +1 -1
- package/.claude/skills/sillyspec-flow/SKILL.md +10 -6
- package/.claude/skills/sillyspec-quick/SKILL.md +105 -7
- package/.zcodeignore +66 -0
- package/AGENTS.md +16 -16
- package/assets/command-cards/flow.md +9 -3
- package/assets/command-cards/run-quick.md +4 -1
- package/package.json +2 -2
- package/src/agent-session-log.js +27 -5
- package/src/config-schema.js +21 -0
- package/src/datetime.js +51 -0
- package/src/db.js +10 -3
- package/src/decision-distill.js +21 -12
- package/src/design-facts.js +46 -6
- package/src/flow-draft.js +468 -95
- package/src/flow-parity.js +199 -5
- package/src/flow-review.js +136 -6
- package/src/flow.js +786 -145
- package/src/foreign-declared.js +75 -3
- package/src/fr-index.js +148 -162
- package/src/git-helper.js +16 -3
- package/src/hooks/worktree-guard.js +16 -5
- package/src/hunk-attribution.js +28 -5
- package/src/index.js +186 -19
- package/src/knowledge-classify.js +3 -1
- package/src/knowledge-digest.js +55 -26
- package/src/knowledge-match.js +135 -7
- package/src/knowledge-stats.js +105 -4
- package/src/knowledge-vector.js +160 -0
- package/src/machine-interface.js +90 -2
- package/src/module-impact.js +22 -1
- package/src/modules.js +9 -0
- package/src/progress/change-registry.js +20 -0
- package/src/progress/stage-machine.js +4 -7
- package/src/progress.js +8 -1
- package/src/quicklog.js +20 -0
- package/src/redomain.js +23 -7
- package/src/route-hindsight.js +276 -0
- package/src/run/archive-distill.js +3 -1
- package/src/run/command.js +65 -12
- package/src/run/complete-handlers.js +219 -5
- package/src/run/complete.js +99 -22
- package/src/run/gate-snapshot.js +15 -0
- package/src/run/gates.js +60 -4
- package/src/run/prompt.js +159 -31
- package/src/run/quick-audit.js +66 -7
- package/src/run/shared.js +6 -11
- package/src/run/stage.js +5 -1
- package/src/run/verify-quality-scan.js +84 -0
- package/src/scan-facts.js +2 -1
- package/src/scope-audit.js +138 -5
- package/src/sentinel-assertions.js +67 -16
- package/src/stage-contract-engine.js +4 -1
- package/src/stage-contract-spec.js +103 -4
- package/src/stage-contract.js +55 -7
- package/src/stages/brainstorm.js +27 -14
- package/src/stages/knowledge.js +4 -3
- package/src/stages/plan-postcheck.js +98 -3
- package/src/stages/plan.js +22 -2
- package/src/stages/quick.js +1 -1
- package/src/stages/verify.js +2 -1
- package/src/task-review.js +1 -1
- package/src/task-tick.js +121 -0
- package/src/taskcard-frontmatter.js +31 -0
- package/src/ui-visual.js +43 -8
- package/src/verify-postcheck.js +518 -38
- package/src/verify-probes.js +216 -5
- package/src/watcher.js +1067 -1031
- package/src/worktree-apply.js +9 -1
- package/src/worktree.js +21 -6
- package/templates/agents-instruction.md +15 -15
- package/templates/prompts/taskcard-rules.md +1 -0
|
@@ -118,6 +118,10 @@ export function storeQualityScan({ specBase, cwd, changeName, testResult, lintRe
|
|
|
118
118
|
// RERUN 失败签名(r5l 方案 4):dedupKey × 测试面 × 环境探针;同样 additive——存量
|
|
119
119
|
// 记录无此键 → RERUN 签名闸判无签名放行一次(同 no-dedup-key 先例口径)
|
|
120
120
|
rerunSignature: computeRerunSignature({ cwd, specBase, dedupKey, env: process.env }),
|
|
121
|
+
// passed 幂等闸键(2026-09-30-quality-scan-passed-idempotent):dedupKey × 非文档脏集
|
|
122
|
+
// 内容键——内容敏感(dedupKey 文件集口径对同文件未提交修改是盲的,闸会吞真实代码
|
|
123
|
+
// 修改);additive——存量记录无此键 → 闸判 no-passed-key 保守重跑一次后携带
|
|
124
|
+
passedKey: computeQualityScanPassedKey({ cwd, specBase }),
|
|
121
125
|
usedSnapshot: Boolean(usedSnapshot),
|
|
122
126
|
ranAt: new Date().toISOString(),
|
|
123
127
|
testResult,
|
|
@@ -208,6 +212,42 @@ export function computeRerunSignature({ cwd, specBase, dedupKey = null, env = pr
|
|
|
208
212
|
.digest('hex')
|
|
209
213
|
}
|
|
210
214
|
|
|
215
|
+
/**
|
|
216
|
+
* 非文档脏文件内容键(2026-09-30-quality-scan-passed-idempotent):对 porcelain 非文档脏集
|
|
217
|
+
* 逐文件取内容哈希。存在理由:指纹/dedupKey 的脏集是**文件集**口径(对齐 --done 复用面——
|
|
218
|
+
* verify 窗口内合法产出=文档,代码交付经 apply/commit 进 HEAD,同文件未提交内容修改的盲区
|
|
219
|
+
* 历史无害);passed 幂等闸直接终止本步骤执行,盲区会吞真实代码修改(实证:fixture 同文件
|
|
220
|
+
* 翻转失败版,dedupKey 全等闸误命中)——闸键必须内容敏感。单文件读不到(rename 行/删除中/
|
|
221
|
+
* 权限)按 '<unreadable>' 占位(状态变化即键变化,恒占位也稳定);git 不可用 → null。
|
|
222
|
+
*/
|
|
223
|
+
export function computeQualityScanDirtyContentKey({ cwd }) {
|
|
224
|
+
// trim:false 保 porcelain 行首状态位(git() 缺省 trim 会吃掉首行前导空格,` M x` → `M x`,
|
|
225
|
+
// slice(3) 路径错位恒读不到文件——实证:fixture 单文件脏集 digest 恒 <unreadable>)
|
|
226
|
+
const porcelain = gitQuiet(cwd, ['status', '--porcelain'], { trim: false })
|
|
227
|
+
if (porcelain === null) return null
|
|
228
|
+
const paths = porcelainCodeLines(porcelain)
|
|
229
|
+
const parts = []
|
|
230
|
+
for (const p of paths) {
|
|
231
|
+
let digest = '<unreadable>'
|
|
232
|
+
try {
|
|
233
|
+
digest = createHash('sha256').update(readFileSync(join(cwd, p)).toString('latin1')).digest('hex')
|
|
234
|
+
} catch { /* 读不到按占位(内容敏感方向保守) */ }
|
|
235
|
+
parts.push(p + '\n' + digest)
|
|
236
|
+
}
|
|
237
|
+
return createHash('sha256').update(parts.join('\n==\n')).digest('hex')
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* passed 幂等闸键:dedupKey(配置×豁免面×HEAD×文件集)× 非文档脏集内容键 的合成。
|
|
242
|
+
* 任一分量不可得 → null(fail-closed:无键即无闸,保守真跑)。
|
|
243
|
+
*/
|
|
244
|
+
export function computeQualityScanPassedKey({ cwd, specBase }) {
|
|
245
|
+
const dedup = computeQualityScanDedupKey({ cwd, specBase })
|
|
246
|
+
const content = computeQualityScanDirtyContentKey({ cwd })
|
|
247
|
+
if (!dedup || !content) return null
|
|
248
|
+
return createHash('sha256').update(dedup + '\n##dirty-content##\n' + content).digest('hex')
|
|
249
|
+
}
|
|
250
|
+
|
|
211
251
|
/**
|
|
212
252
|
* 读回最近一条扫描记录(不论成败)——失败去重重放用。loadReusableQualityScan 拒绝
|
|
213
253
|
* failed 记录(复用是优化不是正确性依赖),去重恰恰要消费 failed 态;schema/source/
|
|
@@ -242,6 +282,31 @@ export function shouldReuseLastFailedScan({ lastRecord, currentDedupKey, planned
|
|
|
242
282
|
return { reuse: true, reason: null }
|
|
243
283
|
}
|
|
244
284
|
|
|
285
|
+
/**
|
|
286
|
+
* passed 幂等闸(2026-09-30-quality-scan-passed-idempotent,与 shouldReuseLastFailedScan 对称):
|
|
287
|
+
* 上次质量扫描 test passed 且 lint 非 failed、passedKey 全等(dedupKey × 非文档脏集内容键
|
|
288
|
+
* ——dedupKey 的文件集口径对同文件未提交内容修改是盲的,闸直接终止本步骤执行会吞真实代码
|
|
289
|
+
* 修改,故键必须内容敏感)、快照口径一致 → 免重跑。实证:multi-agent-platform 2026-09-30
|
|
290
|
+
* verify 收敛循环里 agent 修 verify-result.md 文档反复重入本步,码态恒定(零 commit)下质量
|
|
291
|
+
* 扫描 11 轮 ×~290s 全量真跑——失败有签名闸防假红重试循环,passed 反而无幂等闸。同键 + 同
|
|
292
|
+
* 快照口径 ⇒ 测试输入完全相同 ⇒ 重跑结果确定性相同,纯等待。
|
|
293
|
+
* lint failed 记录不入闸:真跑时 lint 失败会让本步骤 throw 不完成,幂等命中直接完成会吞
|
|
294
|
+
* 掉 lint 阻断发声——lint 债场景须重跑(修债/加豁免即 dedupKey 失配自动重测,闭环不堵)。
|
|
295
|
+
*/
|
|
296
|
+
export function shouldReuseLastPassedScan({ lastRecord, currentPassedKey, plannedSnapshot, forceRerun = false }) {
|
|
297
|
+
if (forceRerun) return { reuse: false, reason: 'force-rerun(逃生阀 SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN)' }
|
|
298
|
+
if (!lastRecord || !lastRecord.testResult || lastRecord.testResult.status !== 'passed') {
|
|
299
|
+
return { reuse: false, reason: 'no-passed-record' }
|
|
300
|
+
}
|
|
301
|
+
if (lastRecord.lintResult && lastRecord.lintResult.status === 'failed') {
|
|
302
|
+
return { reuse: false, reason: 'lint-failed(lint 阻断语义须重跑发声;修债/加豁免即失配自动重测)' }
|
|
303
|
+
}
|
|
304
|
+
if (!lastRecord.passedKey || !currentPassedKey) return { reuse: false, reason: 'no-passed-key(存量记录 / git 不可用——保守重跑一次后记录即携带)' }
|
|
305
|
+
if (lastRecord.passedKey !== currentPassedKey) return { reuse: false, reason: 'passed-key-mismatch(代码内容/文件集/配置/豁免面/HEAD 已变化)' }
|
|
306
|
+
if (Boolean(lastRecord.usedSnapshot) !== Boolean(plannedSnapshot)) return { reuse: false, reason: 'snapshot-scope-changed' }
|
|
307
|
+
return { reuse: true, reason: null }
|
|
308
|
+
}
|
|
309
|
+
|
|
245
310
|
/**
|
|
246
311
|
* 伪影分诊编排(ql-20260920-010 修复一 a):测试失败 → classify 分类 → 命中且本次是
|
|
247
312
|
* 快照口径 → 主树口径单点复跑对照。复跑过 = 伪影坐实:返回主树结果 + artifactTriage
|
|
@@ -589,6 +654,25 @@ export async function executeVerifyQualityScan({ cwd, specBase, changeName, plat
|
|
|
589
654
|
throw new Error(`noAI 质量扫描复用上次失败结果(失败签名未变,跳过重跑)——${(t.reason || '测试失败').split('\n')[0]}。修复后重跑 sillyspec run verify${changeName ? ` --change ${changeName}` : ''},或按上方出路处置。`)
|
|
590
655
|
}
|
|
591
656
|
}
|
|
657
|
+
// ── passed 幂等闸(2026-09-30-quality-scan-passed-idempotent,与失败签名闸对称)──
|
|
658
|
+
// 上次 passed 且 passedKey(dedupKey × 非文档脏集内容键——内容敏感,同文件未提交修改
|
|
659
|
+
// 即失配)/快照口径全等 → 免重跑直接完成本步骤(verify 收敛循环里 agent 修文档反复重入
|
|
660
|
+
// 本步,码态未变时快照构建+测试+lint 的 ~分钟级全量重跑是纯等待——实证 11 轮 ×~290s)。
|
|
661
|
+
// 复用只做披露,不重写记录(ranAt 不动,--done 读侧 loadReusableQualityScan 口径不变);
|
|
662
|
+
// 逃生阀与失败闸同款 RERUN=1/force。
|
|
663
|
+
if (rerunMode !== '1' && rerunMode !== 'force') {
|
|
664
|
+
const lastRecord = loadLastQualityScanRecord({ specBase, changeName })
|
|
665
|
+
const passedVerdict = shouldReuseLastPassedScan({
|
|
666
|
+
lastRecord,
|
|
667
|
+
currentPassedKey: computeQualityScanPassedKey({ cwd, specBase }),
|
|
668
|
+
plannedSnapshot: !process.env.SILLYSPEC_VERIFY_GATE_SNAPSHOT_OFF,
|
|
669
|
+
})
|
|
670
|
+
if (passedVerdict.reuse) {
|
|
671
|
+
console.log(`\n♻️ noAI 质量扫描幂等命中:上次实测 passed 且代码指纹/配置/豁免面/快照口径全等${lastRecord.ranAt ? `(实测于 ${lastRecord.ranAt})` : ''}——本轮免重跑,--done 对账继续复用该记录;确需重跑设 SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN=1。`)
|
|
672
|
+
console.log(`\n✅ noAI 质量扫描全绿(幂等复用)——本步骤自动完成;代码/配置/豁免面任一变化即 dedupKey 失配自动重测。`)
|
|
673
|
+
return
|
|
674
|
+
}
|
|
675
|
+
}
|
|
592
676
|
// ── 隔离快照定向跑(2026-09-11 驾驭第十二批,用户第三次撞上:noAI 质量扫描是 verify 的
|
|
593
677
|
// 第一实测执行点,第八批只接了 gates.js verify 块——本步仍跑 main 工作区,并行 WIP 在此弄红
|
|
594
678
|
// 无辜变更)。与 gates.js 同款:createVerifyGateSnapshot(HEAD + 本变更文件集,native worktree
|
package/src/scan-facts.js
CHANGED
|
@@ -18,6 +18,7 @@ import { join, relative } from 'node:path'
|
|
|
18
18
|
import { scanBackendEndpoints, scanFrontendApiCalls } from './endpoint-extractor.js'
|
|
19
19
|
import { estimateSourceSize } from './run/scan-profile.js'
|
|
20
20
|
import { safeGit } from './git-helper.js'
|
|
21
|
+
import { toWallClock } from './datetime.js'
|
|
21
22
|
|
|
22
23
|
/** 底稿各清单的上限(防大仓把底稿撑爆反而变成新的 token 黑洞) */
|
|
23
24
|
const MAX_ENDPOINTS = 60
|
|
@@ -98,7 +99,7 @@ export function collectScanFacts({ cwd, projectPath = null }) {
|
|
|
98
99
|
: manifest.manifests.includes('Cargo.toml') ? 'rust'
|
|
99
100
|
: 'generic'
|
|
100
101
|
return {
|
|
101
|
-
generatedAt: new Date()
|
|
102
|
+
generatedAt: toWallClock(new Date()),
|
|
102
103
|
projectPath,
|
|
103
104
|
type,
|
|
104
105
|
pkgName: manifest.pkgName,
|
package/src/scope-audit.js
CHANGED
|
@@ -267,6 +267,72 @@ function readFrozenPatch(changeDir) {
|
|
|
267
267
|
} catch { return null }
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
+
/**
|
|
271
|
+
* flow done 冻结记录读取(2026-10-07-scope-audit-thin-patch-replay):change-patch.json 是
|
|
272
|
+
* thin 轻量变更 flow done 时点冻结的归属面(files/baseline/head/totals/patchSha256)——thin
|
|
273
|
+
* 通道不落 execute --done 快照,本件即其对账级冻结记录。缺 json / files 非数组 → null
|
|
274
|
+
* (调用方回退实时开放区间,不出伪数据)。
|
|
275
|
+
*/
|
|
276
|
+
function readChangePatchMeta(changeDir) {
|
|
277
|
+
if (!changeDir) return null
|
|
278
|
+
try {
|
|
279
|
+
const meta = JSON.parse(readFileSync(join(changeDir, 'change-patch.json'), 'utf8'))
|
|
280
|
+
if (meta && typeof meta === 'object' && Array.isArray(meta.files)) return meta
|
|
281
|
+
} catch { /* 缺失/损坏 → null */ }
|
|
282
|
+
return null
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* 冻结 patch 正文读取(thin 回放数据源):变更目录 change.patch;缺失/空 → null。
|
|
287
|
+
* @returns {string|null}
|
|
288
|
+
*/
|
|
289
|
+
function readChangePatchText(changeDir) {
|
|
290
|
+
if (!changeDir) return null
|
|
291
|
+
try {
|
|
292
|
+
const text = readFileSync(join(changeDir, 'change.patch'), 'utf8')
|
|
293
|
+
return text && text.trim() ? text : null
|
|
294
|
+
} catch { return null }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* 冻结 patch 按段行数统计(thin 回放数据源):`diff --git a/<old> b/<new>` 段头切分
|
|
299
|
+
* (正则与 filterPatchForFiles/slicePatchForFile 同款,取 b/ 新路径),段内 +/− 前缀行计数
|
|
300
|
+
* (+++ / --- 文件头与 \ 续行标记不计);`new file mode` → new、`deleted file mode` →
|
|
301
|
+
* deleted、Binary/GIT binary patch → null 档——三档与 collectNumstatByPath 口径对齐。
|
|
302
|
+
* 自包含解析(不采 git numstat):冻结面含 done 时点工作树件(治理工件/untracked),且不
|
|
303
|
+
* 依赖 git 对象库存活。
|
|
304
|
+
* @returns {Map<string, {additions: number|null, deletions: number|null, kind: 'binary'|'new'|'modified'|'deleted'}>}
|
|
305
|
+
*/
|
|
306
|
+
function parseFrozenPatchStats(patchText) {
|
|
307
|
+
const stats = new Map()
|
|
308
|
+
if (!patchText) return stats
|
|
309
|
+
let cur = null
|
|
310
|
+
const flush = () => {
|
|
311
|
+
if (cur && cur.path) {
|
|
312
|
+
stats.set(cur.path, cur.binary
|
|
313
|
+
? { additions: null, deletions: null, kind: 'binary' }
|
|
314
|
+
: { additions: cur.additions, deletions: cur.deletions, kind: cur.kind })
|
|
315
|
+
}
|
|
316
|
+
cur = null
|
|
317
|
+
}
|
|
318
|
+
for (const line of String(patchText).split('\n')) {
|
|
319
|
+
if (line.startsWith('diff --git ')) {
|
|
320
|
+
flush()
|
|
321
|
+
const m = line.match(/^diff --git a\/(.+) b\/(.+)$/)
|
|
322
|
+
cur = m ? { path: toPosix(m[2]), additions: 0, deletions: 0, kind: 'modified', binary: false } : null
|
|
323
|
+
continue
|
|
324
|
+
}
|
|
325
|
+
if (!cur) continue
|
|
326
|
+
if (line.startsWith('new file mode')) cur.kind = 'new'
|
|
327
|
+
else if (line.startsWith('deleted file mode')) cur.kind = 'deleted'
|
|
328
|
+
else if (line.startsWith('Binary') || line.startsWith('GIT binary patch')) cur.binary = true
|
|
329
|
+
else if (line.startsWith('+') && !line.startsWith('+++')) cur.additions++
|
|
330
|
+
else if (line.startsWith('-') && !line.startsWith('---')) cur.deletions++
|
|
331
|
+
}
|
|
332
|
+
flush()
|
|
333
|
+
return stats
|
|
334
|
+
}
|
|
335
|
+
|
|
270
336
|
/**
|
|
271
337
|
* patch 段过滤(quick-90015473):全量 diff 按 `diff --git a/<old> b/<new>` 段头切分,
|
|
272
338
|
* 只保留 files 集合内的段——json rows(退栈归属集)与 patch 同口径,多会话仓不把并行
|
|
@@ -315,12 +381,19 @@ function verifyPatchIntegrity(recordedSha, patchText) {
|
|
|
315
381
|
* @param {{ baseRef: string }} opts
|
|
316
382
|
* @returns {string|null} null=采集失败(调用方 fail-soft 不落伪 patch)
|
|
317
383
|
*/
|
|
318
|
-
export function buildFrozenPatch(root, files, { baseRef } = {}) {
|
|
384
|
+
export function buildFrozenPatch(root, files, { baseRef, headRef } = {}) {
|
|
319
385
|
if (!root || !baseRef) return null
|
|
320
386
|
const wanted = new Set((Array.isArray(files) ? files : []).map(toPosix).filter(Boolean))
|
|
321
387
|
const parts = []
|
|
322
388
|
try {
|
|
323
|
-
|
|
389
|
+
// headRef(2026-09-28-split-guard-and-gate-report 评审 P2 清偿):缺省 `git diff <baseRef>` 比对
|
|
390
|
+
// 的是**工作树**——共享主仓里并行会话对同文件的未提交 hunk 会漏进冻结件(本变更实证
|
|
391
|
+
// extractTestAnchors hunk 夹带)。传 headRef 时改为提交区间 diff(baseRef..headRef),工作树
|
|
392
|
+
// 在途改动不再入冻;调用方对「治理工件目录的未提交槽位编辑」等确需工作树口径的场景不传
|
|
393
|
+
// headRef(变更目录归本变更独占,无他会话面)。
|
|
394
|
+
const r = safeGit(root, headRef
|
|
395
|
+
? ['diff', '--no-color', baseRef, headRef]
|
|
396
|
+
: ['diff', '--no-color', baseRef], { timeout: 60 * 1000 })
|
|
324
397
|
// diff 采集失败 fail-closed(2026-09-27-thin-module-scope-persist 评审 P1 清偿:此前失败静默
|
|
325
398
|
// 跳过 tracked 段,untracked 自拼 hunk 仍拼出非空 patch → patchStatus ok 的伪完整件——实证
|
|
326
399
|
// core.bare 被误写 true 时 `git diff <ref>` 报「must be run in a work tree」,change.patch
|
|
@@ -965,6 +1038,9 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
|
|
|
965
1038
|
// execute --done 落盘,complete.js `...snap` 展开零改动自动携带)原样回放;旧快照无该键
|
|
966
1039
|
// → undefined(JSON.stringify 自动省略——单仓/旧快照零新增字段,additive 契约)。
|
|
967
1040
|
const snapRepos = Array.isArray(snap.repos) ? snap.repos : undefined
|
|
1041
|
+
// 通道标注(2026-10-07-unify-close-trace):新快照带 closedBy(flow done / execute --done)
|
|
1042
|
+
// ——thin 快照回放不再误标 execute --done;旧快照无键缺省兼容。
|
|
1043
|
+
const closeChannel = typeof snap.closedBy === 'string' && snap.closedBy ? snap.closedBy : 'execute --done'
|
|
968
1044
|
// 旧快照跨仓注记(R-07):快照冻结于跨仓对账上线前(跨仓行恒 untouched 的 v1 ⊘ 形态、
|
|
969
1045
|
// 无 repos 键)——照旧回放不重算(「快照说什么是什么」契约不破),注记指明人工渠道
|
|
970
1046
|
// (该仓锚点锡点冻结在 execute-runs reviews,可人工 git diff 核实)。
|
|
@@ -1008,7 +1084,7 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
|
|
|
1008
1084
|
: { foreignDeclared: [] },
|
|
1009
1085
|
patchSha256: snap.patchSha256 || null,
|
|
1010
1086
|
patchStatus: snap.patchStatus || null,
|
|
1011
|
-
note: `${settleLabel}
|
|
1087
|
+
note: `${settleLabel}——${closeChannel} 时点冻结快照${snap.savedAt ? '(' + String(snap.savedAt).replace('T', ' ').slice(0, 19) + ' 落盘)' : ''}:文件集封闭在收尾时点,主仓后续新文件不进表;行数按锚 ${actual.baseAnchor.slice(0, 7)}→当前工作树 补采(同文件后续演进会计入)${legacyCrossNote}`,
|
|
1012
1088
|
}
|
|
1013
1089
|
}
|
|
1014
1090
|
return {
|
|
@@ -1026,7 +1102,64 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
|
|
|
1026
1102
|
: { foreignDeclared: [] },
|
|
1027
1103
|
patchSha256: snap.patchSha256 || null,
|
|
1028
1104
|
patchStatus: snap.patchStatus || null,
|
|
1029
|
-
note: `${settleLabel}
|
|
1105
|
+
note: `${settleLabel}——${closeChannel} 时点冻结快照${snap.savedAt ? '(' + String(snap.savedAt).replace('T', ' ').slice(0, 19) + ' 落盘)' : ''}:范围封闭在收尾时点,主仓后续改动不反映到本表;需要看当前工作区实时状态请跑 git status${legacyCrossNote}`,
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
// —— thin flow done 冻结记录回放(2026-10-07-scope-audit-thin-patch-replay):thin 轻量变更
|
|
1109
|
+
// flow done 只冻 change-patch.json + change.patch、不落 execute 快照——快照缺失时回读冻结件
|
|
1110
|
+
// 出真实三态(存量归档立即受益),不再落「实时开放区间 → 干净树空 → 清单全行计划未动
|
|
1111
|
+
// +0/−0」的失真链。文件集 = meta.files 过 filterDeliverableFiles(与 actual 侧同口径,
|
|
1112
|
+
// 治理工件不进表);行数自冻结 patch 按段统计(parseFrozenPatchStats,自包含);三态按
|
|
1113
|
+
// design 清单 pathMatches 判定(与主链路三态判定同款);跨仓条目维持 v1 ⊘ untouched 形态
|
|
1114
|
+
// (thin 冻结件只有主仓 patch,不假装对账过)。
|
|
1115
|
+
const doneMeta = readChangePatchMeta(changeDirInfo.dir)
|
|
1116
|
+
if (doneMeta && doneMeta.files.length > 0) {
|
|
1117
|
+
const frozenFiles = filterDeliverableFiles([...new Set(doneMeta.files.map(toPosix).filter(Boolean))]).sort()
|
|
1118
|
+
const patchOk = doneMeta.patchStatus === 'ok'
|
|
1119
|
+
const segStats = patchOk ? parseFrozenPatchStats(readChangePatchText(changeDirInfo.dir)) : new Map()
|
|
1120
|
+
const doneRows = []
|
|
1121
|
+
const doneMatched = new Set()
|
|
1122
|
+
for (const f of frozenFiles) {
|
|
1123
|
+
// 段缺失(冻结面零 diff 文件)/ patch 采集失败 → 行数 null 档(—),不出伪数据
|
|
1124
|
+
const st = segStats.get(f) || { additions: null, deletions: null, kind: 'modified' }
|
|
1125
|
+
const row = { path: f, additions: st.additions, deletions: st.deletions, kind: st.kind }
|
|
1126
|
+
if (!planDegraded) {
|
|
1127
|
+
const entry = plannedEntries.find(e => pathMatches(f, e.path))
|
|
1128
|
+
if (entry) {
|
|
1129
|
+
doneMatched.add(entry.path)
|
|
1130
|
+
row.planned = entry.operation || null
|
|
1131
|
+
row.verdict = 'planned'
|
|
1132
|
+
} else {
|
|
1133
|
+
row.planned = null
|
|
1134
|
+
row.verdict = 'unplanned'
|
|
1135
|
+
const facility = classifyToolScaffold(f)
|
|
1136
|
+
if (facility) row.facility = facility
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
doneRows.push(row)
|
|
1140
|
+
}
|
|
1141
|
+
// 计划未动补行:清单文件不在冻结面 → untouched 0/0(与主链路补行同款语义)
|
|
1142
|
+
for (const e of plannedEntries) {
|
|
1143
|
+
if (doneMatched.has(e.path)) continue
|
|
1144
|
+
doneRows.push(e.repo
|
|
1145
|
+
? { path: e.path, planned: e.operation || null, additions: 0, deletions: 0, kind: 'modified', verdict: 'untouched', crossRepo: e.repo }
|
|
1146
|
+
: { path: e.path, planned: e.operation || null, additions: 0, deletions: 0, kind: 'modified', verdict: 'untouched' })
|
|
1147
|
+
}
|
|
1148
|
+
const doneSettleLabel = changeDirInfo.archived ? '已归档' : 'execute 已收尾(分支已清理,待 verify/archive)'
|
|
1149
|
+
const doneSavedAt = doneMeta.savedAt ? ',' + String(doneMeta.savedAt).replace('T', ' ').slice(0, 19) + ' 落盘' : ''
|
|
1150
|
+
return {
|
|
1151
|
+
...base,
|
|
1152
|
+
ok: true,
|
|
1153
|
+
degradedReason: planDegraded,
|
|
1154
|
+
baseAnchor: typeof doneMeta.baseline === 'string' && /^[0-9a-f]{7,40}$/.test(doneMeta.baseline) ? doneMeta.baseline : null,
|
|
1155
|
+
totals: { files: doneRows.length, ...sumTotals(doneRows) },
|
|
1156
|
+
rows: doneRows,
|
|
1157
|
+
excluded: { foreignDeclared: [] },
|
|
1158
|
+
patchSha256: doneMeta.patchSha256 || null,
|
|
1159
|
+
patchStatus: doneMeta.patchStatus || null,
|
|
1160
|
+
// --file 冻结切片消费(getFileDiff 唯一读取方):指向 change.patch,sha256 校验 A-F01 同款
|
|
1161
|
+
...(patchOk ? { frozenPatchPath: join(changeDirInfo.dir, 'change.patch') } : {}),
|
|
1162
|
+
note: `${doneSettleLabel}——flow done 时点冻结 patch 记录(change-patch.json + change.patch${doneSavedAt}):文件集与行数封闭在 done 时点(thin 轻量变更无 execute 快照),主仓后续改动不反映到本表${patchOk ? '' : ';冻结 patch 当时采集失败——行数列不可得(—),文件集仍为 done 时点冻结面'};需要看当前工作区实时状态请跑 git status`,
|
|
1030
1163
|
}
|
|
1031
1164
|
}
|
|
1032
1165
|
// 快照缺失 → 实时开放区间兜底(下方主链路),note 追加漂移警告
|
|
@@ -1040,7 +1173,7 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
|
|
|
1040
1173
|
// 已收尾但快照缺失(上方分支置位):实时开放区间兜底,明示漂移语义——快照机制上线
|
|
1041
1174
|
// (2026-09-10)前收尾的旧变更冻结记录无法重建,如实告知替代渠道。
|
|
1042
1175
|
if (noSnapshotDrift) {
|
|
1043
|
-
notes.push('快照缺失(变更已收尾——已归档或 execute
|
|
1176
|
+
notes.push('快照缺失(变更已收尾——已归档或 execute 分支已清理;快照与 flow done 冻结 patch 记录(change-patch.json)均无,无法重建冻结记录——快照机制与冻结件留档上线前收尾的变更即此形态)——下表为实时开放区间(基点→当前工作树,含并行会话与后续演进,随主仓改动漂移),非本变更冻结范围;记录态参考 verify-result.md / git log')
|
|
1044
1177
|
}
|
|
1045
1178
|
|
|
1046
1179
|
// —— 行数采集根与基点 ——
|
|
@@ -52,7 +52,12 @@ function listReviewEvidence(changeDir, opts) {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
* 假勾选三态判定(L0
|
|
55
|
+
* 假勾选三态判定(L0 收口拒收支点)。
|
|
56
|
+
*
|
|
57
|
+
* 证据判据统一(2026-10-07-thin-tasks-v3,镜像豁免退役):tasks.md 是 agent 的工作分解
|
|
58
|
+
* 队列,每格勾选都是 agent 的完成主张——一律按 per-task 证据判(提交消息含完整 token
|
|
59
|
+
* task-NN(负向前瞻,task-01 不证 task-010)或对应 review.json 在场)。机器种子与
|
|
60
|
+
* agent 改写行同判,不再区分「镜像/覆写」。
|
|
56
61
|
*
|
|
57
62
|
* @param {object} args
|
|
58
63
|
* @param {string|null} args.changeDir 变更目录(推导 specBase 定位 execute-runs;null/探测
|
|
@@ -63,7 +68,7 @@ function listReviewEvidence(changeDir, opts) {
|
|
|
63
68
|
* @param {{listReviewsImpl?: Function}} [args.opts] 注入面(测试替身)
|
|
64
69
|
* @returns {{status:'complete'|'fake'|'none', claimTotal:number, checked:number, missing:string[]}}
|
|
65
70
|
* none=无完成主张(判集空或未全勾);complete=全勾且零 missing;fake=全勾且有零证据
|
|
66
|
-
* 任务(missing 列其 id
|
|
71
|
+
* 任务(missing 列其 id,收口侧拒收依据)
|
|
67
72
|
*/
|
|
68
73
|
export function detectFakeCheckCompletion({ changeDir, tasksMd, commits, opts = {} } = {}) {
|
|
69
74
|
const entries = [];
|
|
@@ -73,14 +78,15 @@ export function detectFakeCheckCompletion({ changeDir, tasksMd, commits, opts =
|
|
|
73
78
|
}
|
|
74
79
|
const claimTotal = entries.length;
|
|
75
80
|
const checked = entries.filter((e) => e.checked).length;
|
|
81
|
+
const messages = (Array.isArray(commits) ? commits : [])
|
|
82
|
+
.map((c) => (typeof c === 'string' ? c : String((c && (c.message ?? c.subject)) || '')));
|
|
76
83
|
if (claimTotal === 0 || checked < claimTotal) {
|
|
77
84
|
return { status: 'none', claimTotal, checked, missing: [] };
|
|
78
85
|
}
|
|
79
|
-
const messages = (Array.isArray(commits) ? commits : [])
|
|
80
|
-
.map((c) => (typeof c === 'string' ? c : String((c && (c.message ?? c.subject)) || '')));
|
|
81
86
|
const reviews = listReviewEvidence(changeDir, opts);
|
|
82
87
|
const missing = entries
|
|
83
88
|
.filter((e) => {
|
|
89
|
+
if (!e.checked) return false;
|
|
84
90
|
const re = taskTokenRe(e.id);
|
|
85
91
|
const byCommit = messages.some((msg) => re.test(msg));
|
|
86
92
|
const byReview = reviews.some((p) => p.includes(`/tasks/${e.id}/`));
|
|
@@ -91,24 +97,69 @@ export function detectFakeCheckCompletion({ changeDir, tasksMd, commits, opts =
|
|
|
91
97
|
}
|
|
92
98
|
|
|
93
99
|
/**
|
|
94
|
-
* 勾选节奏检测(2026-09-26-thin-check-cadence
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
100
|
+
* 勾选节奏检测(2026-09-26-thin-check-cadence;2026-10-07-thin-tasks-v3 采样去重):
|
|
101
|
+
* 事件流里 task-done 单拍跳 ≥2 格 = 一把全勾(未按工作单元逐个勾)。
|
|
102
|
+
* 事件两源同流:`task tick` 命令直写的精确事件(source:'task-tick',一次一格)与 watcher
|
|
103
|
+
* 3s 轮询的采样事件(快速连续 tick 被合并成一跳)。判最大跳以 CLI 精确事件为权威:
|
|
104
|
+
* ① 采样事件落点计数 M 与任一 CLI 事件落点相同 → 整跳被 CLI 精确序列覆盖,剔除;
|
|
105
|
+
* ② 其余采样事件起点按「小于其落点的最大 CLI 落点」抬高(CLI 已勾到的格不重复计入跳幅)。
|
|
106
|
+
* 纯函数:事件清单→最大跳格记录;无多格跳返回 null。detail 与事件生成格式成对
|
|
107
|
+
* (`checked N→M`);解析失配(格式漂移/坏行/非 task-done)按无证据静默——fail-open。
|
|
99
108
|
*/
|
|
100
109
|
export function detectBatchCheckCadence(events) {
|
|
101
|
-
|
|
110
|
+
const taskEvents = [];
|
|
102
111
|
for (const e of events || []) {
|
|
103
|
-
if (
|
|
112
|
+
if (e && e.kind === 'task-done') taskEvents.push(e);
|
|
113
|
+
}
|
|
114
|
+
const parse = (e) => {
|
|
104
115
|
const m = /^checked (\d+)→(\d+)$/.exec(String(e.detail || ''));
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (
|
|
116
|
+
return m ? { from: Number(m[1]), to: Number(m[2]) } : null;
|
|
117
|
+
};
|
|
118
|
+
const cliParsed = [];
|
|
119
|
+
for (const e of taskEvents) {
|
|
120
|
+
if (e.source !== 'task-tick') continue;
|
|
121
|
+
const r = parse(e);
|
|
122
|
+
if (r) cliParsed.push(r);
|
|
123
|
+
}
|
|
124
|
+
const cliTargets = new Set(cliParsed.map((r) => r.to));
|
|
125
|
+
const maxCliBelow = (to) => {
|
|
126
|
+
let best = 0;
|
|
127
|
+
for (const r of cliParsed) if (r.to < to && r.to > best) best = r.to;
|
|
128
|
+
return best;
|
|
129
|
+
};
|
|
130
|
+
let worst = null;
|
|
131
|
+
for (const e of taskEvents) {
|
|
132
|
+
const r = parse(e);
|
|
133
|
+
if (!r) continue;
|
|
134
|
+
const isCli = e.source === 'task-tick';
|
|
135
|
+
if (!isCli && cliTargets.has(r.to)) continue; // 采样合并跳被 CLI 精确序列覆盖
|
|
136
|
+
const from = isCli ? r.from : Math.max(r.from, maxCliBelow(r.to));
|
|
137
|
+
if (r.to - from < 2) continue;
|
|
138
|
+
if (!worst || r.to - from > worst.to - worst.from) worst = { from, to: r.to, detail: e.detail, ts: e.ts };
|
|
110
139
|
}
|
|
111
140
|
return worst;
|
|
112
141
|
}
|
|
113
142
|
|
|
143
|
+
/**
|
|
144
|
+
* 单拍勾选门决策(2026-09-29-batch-tick-gate,纯函数):检出单拍跳(batchTick 非 null)
|
|
145
|
+
* 时的收口动作裁决。镜像豁免分支退役(2026-10-07-thin-tasks-v3——任务面统一为 agent
|
|
146
|
+
* 工作分解,无镜像面)。四态(决策顺序即豁免优先序):
|
|
147
|
+
* 'silent' 无单拍跳;
|
|
148
|
+
* 'bypass' --allow-batch-tick 显式旁路——留痕放行(同意门先例);
|
|
149
|
+
* 'advisory' 机器代勾可解释整跳(autopilotTicked >= 跳幅——governance-autopilot 代勾是
|
|
150
|
+
* 单拍多格机械写,非 agent 纪律面;代勾数不可解释最大跳时落到 reject——防「留
|
|
151
|
+
* 一格给代勾补、自身大跳蹭豁免」的对抗绕过,二轮评审 P3-E);
|
|
152
|
+
* 'reject' 非旁路、代勾解释不了整跳——agent 一把勾,拒收。
|
|
153
|
+
* @param {{batchTick:object|null, nonMirrorCount?:number|null, allowBatchTick?:boolean, autopilotTicked?:number}} args
|
|
154
|
+
* nonMirrorCount 保留为哨兵面未知信号(null → fail-open advisory);镜像-only 语义已退役。
|
|
155
|
+
* @returns {{action:'silent'|'advisory'|'bypass'|'reject', reason:string}}
|
|
156
|
+
*/
|
|
157
|
+
export function resolveBatchTickAction({ batchTick, nonMirrorCount = null, allowBatchTick = false, autopilotTicked = 0 } = {}) {
|
|
158
|
+
if (!batchTick) return { action: 'silent', reason: 'no-batch-tick' };
|
|
159
|
+
if (allowBatchTick === true) return { action: 'bypass', reason: 'flag' };
|
|
160
|
+
if (nonMirrorCount === null) return { action: 'advisory', reason: 'sentinel-unknown' };
|
|
161
|
+
if (autopilotTicked >= batchTick.to - batchTick.from) return { action: 'advisory', reason: 'autopilot-ticked' };
|
|
162
|
+
return { action: 'reject', reason: 'agent-batch-tick' };
|
|
163
|
+
}
|
|
164
|
+
|
|
114
165
|
export default { detectFakeCheckCompletion, detectBatchCheckCadence };
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* (plan-postcheck 注入 LF-normalizing reader),引擎拿到什么判什么,不在引擎层二次引入差异。
|
|
11
11
|
* - regex 每次 new RegExp:manifest 存 {pattern, flags} 字符串,规避 lastIndex 状态坑
|
|
12
12
|
* (旧代码多处 /g 正则跨调用复用需手写 lastIndex=0,是潜在 bug)。
|
|
13
|
-
* - 内容规则(literal-any、literal-all、regex、contains-section、field-present、header-field、list-non-empty、
|
|
13
|
+
* - 内容规则(literal-any、literal-all、literal-none、regex、contains-section、field-present、header-field、list-non-empty、
|
|
14
14
|
* min-lines/no-placeholder-line)在 target 文件不存在时自动 skip(不报错)——存在性由独立的
|
|
15
15
|
* file-exists 规则保证,避免"文件缺失"被内容规则重复报。
|
|
16
16
|
* - custom kind 引擎 skip(不计 errors/warnings),由调用方 validator 保留判定算法,
|
|
@@ -75,6 +75,9 @@ function dispatchPure(rule, entry) {
|
|
|
75
75
|
switch (rule.kind) {
|
|
76
76
|
case 'literal-any': return (rule.data.literals || []).some(s => scoped.includes(s))
|
|
77
77
|
case 'literal-all': return (rule.data.literals || []).every(s => scoped.includes(s))
|
|
78
|
+
// literal-none(2026-09-29-brainstorm-closure-gates):「不得含字面量」判定——与 literal-any 互补,
|
|
79
|
+
// 服务 must-not-contain 契约(残留标记类:待确认/自审存疑等"在场即未闭合"语义)。
|
|
80
|
+
case 'literal-none': return !(rule.data.literals || []).some(s => scoped.includes(s))
|
|
78
81
|
case 'regex': return new RegExp(rule.data.pattern, rule.data.flags || '').test(scoped)
|
|
79
82
|
case 'contains-section': return hasAllSections(scoped, rule.data.sections || [])
|
|
80
83
|
case 'no-placeholder-line': return hasNoPlaceholderLine(scoped, rule.data.patterns)
|
|
@@ -25,11 +25,12 @@
|
|
|
25
25
|
* (小变更只产 design.md)。判定见 stage-contract-engine.js conditionHolds(fail-safe:ctx 字段缺失时 ne 成立)。
|
|
26
26
|
*
|
|
27
27
|
* 纯 kind(stage-contract-engine.dispatchPure 直接判定):
|
|
28
|
-
* file-exists / dir-exists / literal-any / literal-all / regex / contains-section /
|
|
28
|
+
* file-exists / dir-exists / literal-any / literal-all / literal-none / regex / contains-section /
|
|
29
29
|
* no-placeholder-line / field-present / header-field / list-non-empty / min-lines / dir-non-empty
|
|
30
|
-
* —— 语义:若 target 文件不存在,内容类规则(literal-any、literal-all、regex、contains-section、…、
|
|
31
|
-
* min-lines)自动 skip(不报错,存在性由独立的 file-exists 规则保证);file-exists/dir-exists
|
|
32
|
-
* 则是"不存在即 fail"。
|
|
30
|
+
* —— 语义:若 target 文件不存在,内容类规则(literal-any、literal-all、literal-none、regex、contains-section、…、
|
|
31
|
+
* list-non-empty、min-lines)自动 skip(不报错,存在性由独立的 file-exists 规则保证);file-exists/dir-exists
|
|
32
|
+
* 则是"不存在即 fail"。literal-none 是「不得含字面量」判定(2026-09-29-brainstorm-closure-gates),
|
|
33
|
+
* 服务 must-not-contain 契约(残留标记类:在场即未闭合)。
|
|
33
34
|
*
|
|
34
35
|
* custom kind(引擎 skip,validator 保留判定算法,但 import 本 manifest 的 data + failMessage):
|
|
35
36
|
* 见 CUSTOM_KINDS。这些是复杂条件校验(lifecycle 多层豁免 / 入口接线多步对账 / decisions 解析 /
|
|
@@ -44,6 +45,9 @@ const CUSTOM_KINDS = new Set([
|
|
|
44
45
|
'entry-point-wiring', // design.md 入口实例化 → task allowed_paths 覆盖对账(validatePlanOutputs)
|
|
45
46
|
'id-traceability', // FR/D 引用追踪(extractIds / warnMissingIds)
|
|
46
47
|
'decision-blocker', // decisions.md P0/P1 未决阻塞(parseDecisionRecords)
|
|
48
|
+
'decision-coverage', // requirements.md 对 decisions.md 当前版本 D 的覆盖缺口点名(2026-09-29-brainstorm-closure-gates,validateBrainstormOutputs)
|
|
49
|
+
'doubt-closure', // design.md「自审存疑:」应用形态标记行的闭合 token 判定(同上)
|
|
50
|
+
'risk-mitigation', // design.md 风险登记表 R-xx 行应对策略列空/占位判定(同上)
|
|
47
51
|
'cross-task-contract', // task 卡片 provides/expects_from 对账(plan-postcheck)
|
|
48
52
|
'design-file-coverage', // design.md 文件清单 vs allowed_paths 覆盖(plan-postcheck)
|
|
49
53
|
'task-id-continuity', // task-NN id 连续性
|
|
@@ -151,6 +155,101 @@ const BRAINSTORM_RULES = [
|
|
|
151
155
|
spec: 'tasks.md 至少有一个任务列表项(行首 `-` / `*` / `数字.`)',
|
|
152
156
|
failMessage: 'tasks.md 没有任务列表项',
|
|
153
157
|
},
|
|
158
|
+
// ── 闭环收口规则(2026-09-29-brainstorm-closure-gates)────────────────────────────
|
|
159
|
+
// 判据:开放可以,但每个开口必须能回答「谁、在哪个阶段、以什么证据关掉它」。六类无主开口全部
|
|
160
|
+
// 补机器收口,severity 全 warning 不阻断(存量零破坏——故障面/退役判据软警告棘轮先例,观测一个
|
|
161
|
+
// 周期误报率后再评估升 error)。纯 kind 走引擎,复杂判定 custom kind 留 validateBrainstormOutputs。
|
|
162
|
+
{
|
|
163
|
+
id: 'brainstorm.proposal.success-criteria',
|
|
164
|
+
stage: 'brainstorm', source: 'validateBrainstormOutputs', severity: 'warning', kind: 'literal-any',
|
|
165
|
+
target: { root: 'change', path: 'proposal.md', scope: 'full' },
|
|
166
|
+
data: { literals: ['成功标准', 'Success Criteria', 'success criteria'] },
|
|
167
|
+
condition: { ctxField: 'scale', ne: 'small' },
|
|
168
|
+
spec: 'proposal.md 含「成功标准 / Success Criteria」章节(字面命中;问题→方案→成功标准主环最后一环——轻量道 flow start --input 对成功标准 0 条 exit 2,完整路径同理)',
|
|
169
|
+
failMessage: 'proposal.md 缺少「成功标准」章节(需字面命中其一:成功标准 / Success Criteria)——可验证的成功标准是收口验收面,轻量道同款要求',
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: 'brainstorm.design.no-placeholder',
|
|
173
|
+
stage: 'brainstorm', source: 'validateBrainstormOutputs', severity: 'warning', kind: 'no-placeholder-line',
|
|
174
|
+
target: { root: 'change', path: 'design.md', scope: 'full' },
|
|
175
|
+
data: { patterns: ['待补充', 'TODO', 'TBD', '未分析', '根据项目情况', '根据实际情况', '按需填写', '待完善', '待设计', '待确认'] },
|
|
176
|
+
spec: 'design.md 无独立成行占位词(待补充/TODO/TBD/未分析/待完善/待设计/待确认等——占位符在场=开口无人认领)',
|
|
177
|
+
failMessage: 'design.md 存在独立成行占位词(待补充/TODO/TBD/未分析/待完善/待设计/待确认等)——占位符须替换为真实内容或删除该行',
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: 'brainstorm.proposal.no-placeholder',
|
|
181
|
+
stage: 'brainstorm', source: 'validateBrainstormOutputs', severity: 'warning', kind: 'no-placeholder-line',
|
|
182
|
+
target: { root: 'change', path: 'proposal.md', scope: 'full' },
|
|
183
|
+
data: { patterns: ['待补充', 'TODO', 'TBD', '未分析', '根据项目情况', '根据实际情况', '按需填写', '待完善', '待设计', '待确认'] },
|
|
184
|
+
condition: { ctxField: 'scale', ne: 'small' },
|
|
185
|
+
spec: 'proposal.md 无独立成行占位词(待补充/TODO/TBD/未分析/待完善/待设计/待确认等)',
|
|
186
|
+
failMessage: 'proposal.md 存在独立成行占位词(待补充/TODO/TBD/未分析/待完善/待设计/待确认等)——占位符须替换为真实内容或删除该行',
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: 'brainstorm.requirements.no-placeholder',
|
|
190
|
+
stage: 'brainstorm', source: 'validateBrainstormOutputs', severity: 'warning', kind: 'no-placeholder-line',
|
|
191
|
+
target: { root: 'change', path: 'requirements.md', scope: 'full' },
|
|
192
|
+
data: { patterns: ['待补充', 'TODO', 'TBD', '未分析', '根据项目情况', '根据实际情况', '按需填写', '待完善', '待设计', '待确认'] },
|
|
193
|
+
condition: { ctxField: 'scale', ne: 'small' },
|
|
194
|
+
spec: 'requirements.md 无独立成行占位词(待补充/TODO/TBD/未分析/待完善/待设计/待确认等)',
|
|
195
|
+
failMessage: 'requirements.md 存在独立成行占位词(待补充/TODO/TBD/未分析/待完善/待设计/待确认等)——占位符须替换为真实内容或删除该行',
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: 'brainstorm.tasks.no-placeholder',
|
|
199
|
+
stage: 'brainstorm', source: 'validateBrainstormOutputs', severity: 'warning', kind: 'no-placeholder-line',
|
|
200
|
+
target: { root: 'change', path: 'tasks.md', scope: 'full' },
|
|
201
|
+
data: { patterns: ['待补充', 'TODO', 'TBD', '未分析', '根据项目情况', '根据实际情况', '按需填写', '待完善', '待设计', '待确认'] },
|
|
202
|
+
condition: { ctxField: 'scale', ne: 'small' },
|
|
203
|
+
spec: 'tasks.md 无独立成行占位词(待补充/TODO/TBD/未分析/待完善/待设计/待确认等)',
|
|
204
|
+
failMessage: 'tasks.md 存在独立成行占位词(待补充/TODO/TBD/未分析/待完善/待设计/待确认等)——占位符须替换为真实内容或删除该行',
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
id: 'brainstorm.design.pending-confirm-residue',
|
|
208
|
+
stage: 'brainstorm', source: 'validateBrainstormOutputs', severity: 'warning', kind: 'literal-none',
|
|
209
|
+
target: { root: 'change', path: 'design.md', scope: 'full' },
|
|
210
|
+
data: { literals: ['待确认'] },
|
|
211
|
+
spec: 'design.md 无「待确认」残留——design-init 决策追踪表预填的「待确认」须逐行补覆盖点后改「已覆盖」(确无决策记录时删除预填行或写「无决策记录」)',
|
|
212
|
+
failMessage: 'design.md 残留「待确认」——决策追踪表(design-init 预填)须逐行补覆盖点并把「待确认」改为「已覆盖」;确无决策记录时删除预填行或写「无决策记录」',
|
|
213
|
+
},
|
|
214
|
+
// decision-coverage(custom):requirements.md 必须引用 decisions.md 全部当前版本 D-xxx@vN
|
|
215
|
+
// (裸号词边界匹配,D-001 即算引用 D-001@v1,大小写不敏感——与 warnMissingIds 同语义)。
|
|
216
|
+
// 「剩余风险」行含裸号亦算归属(显式登记为剩余风险=开口有主)。算法留 validateBrainstormOutputs
|
|
217
|
+
// (复用 extractCurrentDecisionIds),匹配口径/failMessage 从本 manifest 同源。
|
|
218
|
+
{
|
|
219
|
+
id: 'brainstorm.requirements.decision-coverage',
|
|
220
|
+
stage: 'brainstorm', source: 'validateBrainstormOutputs', severity: 'warning', kind: 'decision-coverage',
|
|
221
|
+
target: { root: 'change', path: 'requirements.md', scope: 'full' },
|
|
222
|
+
data: {},
|
|
223
|
+
spec: 'requirements.md 引用 decisions.md 的全部当前版本 D-xxx@vN(裸号 D-001 词边界匹配即算;未覆盖的在「决策覆盖矩阵」补行,或显式标注为剩余风险——被覆盖或显式登记为剩余风险都算归属,不出现才算开口)',
|
|
224
|
+
failMessage: 'requirements.md 未引用 decisions.md 中的 ${id} — 在「决策覆盖矩阵」补覆盖行,或在剩余风险中显式标注该决策未覆盖(每个 D 须有归属:被覆盖或显式登记为剩余风险)',
|
|
225
|
+
},
|
|
226
|
+
// doubt-closure(custom):「自审存疑:」应用形态标记行须含闭合 token。查带冒号的应用形态而非
|
|
227
|
+
// 裸词——design-init 自审 checklist 模板行含裸词,裸词判定必常驻误报。算法留 validator。
|
|
228
|
+
{
|
|
229
|
+
id: 'brainstorm.design.doubt-closure',
|
|
230
|
+
stage: 'brainstorm', source: 'validateBrainstormOutputs', severity: 'warning', kind: 'doubt-closure',
|
|
231
|
+
target: { root: 'change', path: 'design.md', scope: 'full' },
|
|
232
|
+
data: {
|
|
233
|
+
markerPattern: { pattern: '自审存疑\\s*[::]', flags: '' },
|
|
234
|
+
closureTokens: ['D-\\d+', 'R-\\d+', '已解决', '已闭合', '已确认', '不适用'],
|
|
235
|
+
},
|
|
236
|
+
spec: 'design.md 的「自审存疑:」标记行须含闭合 token(D-xxx/R-xx/已解决/已闭合/已确认)——已解决的改写为结论并引用 D/R,未解决的转 decisions.md D-xxx 或风险登记 R-xx;存疑标记不得原样带进 plan',
|
|
237
|
+
failMessage: 'design.md 存在未闭合的自审存疑:「${line}」——已解决的改写为结论并引用 D-xxx/R-xx,未解决的转 decisions.md 决策或风险登记 R-xx 后去标记;开放须有归属,存疑标记不得带进 plan',
|
|
238
|
+
},
|
|
239
|
+
// risk-mitigation(custom):风险登记表 R-xx 行应对策略列(行末单元格)非空且非占位。design-init
|
|
240
|
+
// 骨架预填「(待填应对策略)」——骨架行命中即提醒填语义;显式「接受」是合法闭合(接受=归属明确)。
|
|
241
|
+
{
|
|
242
|
+
id: 'brainstorm.design.risk-mitigation',
|
|
243
|
+
stage: 'brainstorm', source: 'validateBrainstormOutputs', severity: 'warning', kind: 'risk-mitigation',
|
|
244
|
+
target: { root: 'change', path: 'design.md', scope: 'full' },
|
|
245
|
+
data: {
|
|
246
|
+
rowPattern: { pattern: '^\\|\\s*(R-\\d+)', flags: '' },
|
|
247
|
+
placeholderCell: '待填|待补充|待定|TODO|TBD|仅占位|^\\s*[-—–]?\\s*$',
|
|
248
|
+
acceptToken: '接受',
|
|
249
|
+
},
|
|
250
|
+
spec: 'design.md 风险登记表每行 R-xx 的应对策略列(行末单元格)非空且非占位词(待填/待补充/待定/TODO/TBD)——每条风险须有应对;确实接受的风险显式写「接受:<理由>」(接受也是闭合),不得留空/占位',
|
|
251
|
+
failMessage: 'design.md 风险登记 ${id} 的应对策略列为空/占位——每条风险须有应对策略;确实接受的风险显式写「接受:<理由>」,不得留空或占位(风险无应对=开口无归属)',
|
|
252
|
+
},
|
|
154
253
|
// lifecycle(custom kind):design.md 命中生命周期关键词时必须含契约表或紧邻豁免。判定算法留 validator
|
|
155
254
|
// (trigger/exemption/table 三段短路),data + failMessage/exemptionPassedMessage 从本 manifest 同源。
|
|
156
255
|
{
|