sillyspec 3.28.8 → 3.28.10
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/full.json +1 -0
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -224
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-quick/SKILL.md +118 -117
- package/AGENTS.md +3 -2
- package/README.md +35 -11
- package/SKILL.md +24 -10
- package/SillySpec-/350/203/275/345/212/233/344/272/256/347/202/271/345/205/250/346/231/257-2026-09-14.pptx +0 -0
- package/package.json +1 -1
- package/src/change-list.js +73 -20
- package/src/change-risk-profile.js +20 -0
- package/src/config-schema.js +62 -0
- package/src/contract-matrix.js +84 -5
- package/src/cross-repo-reconcile.js +134 -0
- package/src/db.js +11 -3
- package/src/decision-distill.js +15 -3
- package/src/design-facts.js +89 -20
- package/src/docs-gate.js +105 -6
- package/src/doctor-diagnostics.js +224 -2
- package/src/friction-ledger.js +171 -0
- package/src/hooks/worktree-guard.js +13 -1
- package/src/index.js +206 -18
- package/src/knowledge-classify.js +382 -0
- package/src/knowledge-hits.js +74 -0
- package/src/knowledge-stats.js +180 -0
- package/src/probe7-anchor-check.js +69 -0
- package/src/progress/change-registry.js +183 -8
- package/src/progress/shared.js +3 -2
- package/src/progress.js +100 -8
- package/src/quick-gate-profile.js +251 -0
- package/src/quicklog.js +164 -18
- package/src/review-dispatch.js +2 -1
- package/src/run/command.js +131 -13
- package/src/run/complete-handlers.js +511 -21
- package/src/run/complete.js +123 -23
- package/src/run/concurrent-detect.js +98 -0
- package/src/run/gate-snapshot.js +110 -2
- package/src/run/gates.js +141 -16
- package/src/run/prompt.js +221 -11
- package/src/run/quick-audit.js +111 -13
- package/src/run/shared.js +147 -11
- package/src/run/stage.js +6 -0
- package/src/scan-diff.js +61 -30
- package/src/scan-postcheck.js +74 -0
- package/src/scan-refresh.js +477 -0
- package/src/scan-staleness.js +25 -13
- package/src/scope-audit.js +456 -56
- package/src/stage-contract.js +185 -2
- package/src/stage-review.js +23 -7
- package/src/stages/brainstorm.js +6 -0
- package/src/stages/cmd-existence.js +9 -3
- package/src/stages/execute.js +85 -3
- package/src/stages/knowledge.js +76 -6
- package/src/stages/plan-postcheck.js +32 -5
- package/src/stages/quick.js +1 -1
- package/src/stages/verify.js +1 -0
- package/src/sync-noise.js +170 -6
- package/src/sync.js +141 -9
- package/src/task-review.js +366 -46
- package/src/verify-postcheck.js +212 -55
- package/src/verify-probes.js +727 -16
- package/src/worktree-apply.js +685 -48
- package/src/worktree-deps.js +124 -3
- package/src/worktree.js +391 -15
- package/templates/agents-instruction.md +2 -2
- package/templates/prompts/testcase-design.md +2 -1
- package/templates/prompts/verify-probes.md +4 -1
- package/.patch16-tmp.mjs +0 -169
- package/sa-rename-probe/src/new.js +0 -4
package/src/verify-postcheck.js
CHANGED
|
@@ -28,9 +28,10 @@ import { IR_STRICT_SINCE } from './constants.js'
|
|
|
28
28
|
import { gitQuiet } from './git-helper.js'
|
|
29
29
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs'
|
|
30
30
|
import { join } from 'path'
|
|
31
|
-
import { verifyApiParity } from './contract-matrix.js'
|
|
31
|
+
import { verifyApiParity, _readWorktreeMeta } from './contract-matrix.js'
|
|
32
|
+
import { reconcileCrossRepoDeclarations } from './cross-repo-reconcile.js'
|
|
32
33
|
import { parseFileChangeListDetailed, pathMatches } from './change-list.js'
|
|
33
|
-
import { filterDeliverableFiles } from './worktree-apply.js'
|
|
34
|
+
import { filterDeliverableFiles, classifyToolScaffold } from './worktree-apply.js'
|
|
34
35
|
// target_files 声明侧解析(Wave 1 已落地):依赖链已核实无环——plan-postcheck 不反向依赖本模块,
|
|
35
36
|
// 且本模块已经 worktree-apply.js:21 间接依赖 plan-postcheck,此处改直连不引入新环(task-04)
|
|
36
37
|
import { parseTargetFiles, parseRepo } from './stages/plan-postcheck.js'
|
|
@@ -605,6 +606,46 @@ function parseFlowValue(flowText, key) {
|
|
|
605
606
|
return v.length > 0 ? v : null
|
|
606
607
|
}
|
|
607
608
|
|
|
609
|
+
/**
|
|
610
|
+
* module 子集测试面的依赖测试自动发现(坑 module-test-face-rot,2026-09-16 本会话 5325f55
|
|
611
|
+
* 实证:local.yaml modules 的 test 命令是硬编码文件清单,src/verify-probes.js 命中 cli-core
|
|
612
|
+
* 但其断言测试 verify-probes-facts.test.mjs 不在清单 → quick --done 的 module 收窄实测漏掉
|
|
613
|
+
* 全量断言,回归漏到合并态才暴露)。发现口径:变更 src 文件的**直接 import 测试**(test/
|
|
614
|
+
* 下 *.test.* 内含 `../src/<changed>` 引用——静态/动态 import 通吃,子串匹配只可能多跑不漏)
|
|
615
|
+
* ∪ 变更的 test 文件本身;已被命中模块命令串覆盖的(命令串含该文件名)排除防重复。
|
|
616
|
+
* @param {{cwd: string, changedFiles: string[], coveredCommands?: string[]}} args
|
|
617
|
+
* @returns {string[]} 相对 cwd 的测试文件路径(posix 形态,已排序;无可发现 → [])
|
|
618
|
+
*/
|
|
619
|
+
export function discoverModuleDependentTests({ cwd, changedFiles, coveredCommands = [] }) {
|
|
620
|
+
if (!Array.isArray(changedFiles) || changedFiles.length === 0) return []
|
|
621
|
+
const norm = (p) => String(p).replace(/\\/g, '/')
|
|
622
|
+
const changedSrc = [...new Set(changedFiles.map(norm))]
|
|
623
|
+
.filter(p => p.startsWith('src/') && /\.(js|mjs|cjs|ts)$/.test(p))
|
|
624
|
+
const changedTests = [...new Set(changedFiles.map(norm))]
|
|
625
|
+
.filter(p => /^test\//.test(p) && /\.test\.[cm]?js$/.test(p))
|
|
626
|
+
if (changedSrc.length === 0 && changedTests.length === 0) return []
|
|
627
|
+
const covered = coveredCommands.filter(Boolean).join(' ')
|
|
628
|
+
const found = new Set()
|
|
629
|
+
const testDir = join(cwd, 'test')
|
|
630
|
+
let entries = []
|
|
631
|
+
try { entries = readdirSync(testDir) } catch { entries = [] }
|
|
632
|
+
for (const f of entries) {
|
|
633
|
+
if (!/\.test\.[cm]?js$/.test(f)) continue
|
|
634
|
+
if (covered.includes(f)) continue
|
|
635
|
+
let content = ''
|
|
636
|
+
try { content = readFileSync(join(testDir, f), 'utf8') } catch { continue }
|
|
637
|
+
for (const src of changedSrc) {
|
|
638
|
+
if (content.includes(`../${src}`)) { found.add(`test/${f}`); break }
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
for (const t of changedTests) {
|
|
642
|
+
const fname = t.split('/').pop()
|
|
643
|
+
if (covered.includes(fname)) continue
|
|
644
|
+
if (existsSync(join(cwd, t))) found.add(t)
|
|
645
|
+
}
|
|
646
|
+
return [...found].sort()
|
|
647
|
+
}
|
|
648
|
+
|
|
608
649
|
/**
|
|
609
650
|
* 根据变更文件列表 + modules 映射,算出被命中的模块(去重保序)。
|
|
610
651
|
* 文件路径以 module.path 为前缀(含子目录)即视为命中。
|
|
@@ -1049,20 +1090,20 @@ export function resolveVerifyChangedFiles(cwd, changeName, ctx = null, opts = {}
|
|
|
1049
1090
|
|
|
1050
1091
|
// 并入 worktree 未提交改动(坑 module-subset-zero-hit-uncommitted,2026-08-21 实证:
|
|
1051
1092
|
// 子代理默认不 commit,真实改动全在 worktree working-tree——只看 base..HEAD commit diff
|
|
1052
|
-
// 时 module 映射 0 命中直接跳过(frontend/** 变更未命中 frontend
|
|
1053
|
-
//
|
|
1093
|
+
// 时 module 映射 0 命中直接跳过(frontend/** 变更未命中 frontend 模块)。口径与
|
|
1094
|
+
// collectWorktreeChangedFiles(task-review.js,D-004@v1 单一真相 helper)同源,但本段是
|
|
1095
|
+
// form A 专属变体(滤网保 .sillyspec/docs/** 交付物)——已知残留 R-09,待下次触碰统一。
|
|
1054
1096
|
// (排除 .sillyspec/ 运行时产物)。opt-in(默认关):d drafts 有自己的并入点,避免双并。
|
|
1055
1097
|
if (includeWorkingTree && mainFiles !== null) {
|
|
1056
1098
|
try {
|
|
1057
|
-
//
|
|
1058
|
-
//
|
|
1059
|
-
//
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
: cwd
|
|
1099
|
+
// 双根读取(坑 platform-dual-root-form-misjudge,2026-09-15 复盘实证):P1 修复
|
|
1100
|
+
// (2026-09-07)只吃 opts.specBase 单路径——平台模式(specRoot 与 source_root 分离)下
|
|
1101
|
+
// meta 若落项目侧 .sillyspec/.runtime(resolveRuntimeRoot 的 localSpecBase 兜底形)仍读
|
|
1102
|
+
// 不到 → 形态 A 并入失效 → ②类假红。_readWorktreeMeta 双候选(specBase 优先 +
|
|
1103
|
+
// cwd/.sillyspec 兜底,contract-matrix 既有样板)两形全覆盖。
|
|
1104
|
+
const found = changeName ? _readWorktreeMeta(opts.specBase || null, cwd, changeName) : null
|
|
1105
|
+
if (found) {
|
|
1106
|
+
const wtGitDir = found.gitDir
|
|
1066
1107
|
const wtStatus = gitQuiet(wtGitDir, ['status', '--porcelain', '--untracked-files=all'], { timeout: 30000, trim: false })
|
|
1067
1108
|
// P1 修复(2026-09-07):过滤器改 filterDeliverableFiles 口径(worktree-apply.js 同款)——
|
|
1068
1109
|
// 此前 `.sillyspec/` 一刀切把 docs 交付物也滤掉,声明了模块文档的 task 在形态 A 下落②类假红
|
|
@@ -1152,17 +1193,37 @@ export function resolveVerifyChangedFiles(cwd, changeName, ctx = null, opts = {}
|
|
|
1152
1193
|
function resolveMainChangedFiles(cwd, changeName, specBase = null) {
|
|
1153
1194
|
const sb = specBase || join(cwd, '.sillyspec')
|
|
1154
1195
|
if (changeName) {
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1196
|
+
// 双根读取(坑 platform-dual-root-form-misjudge):specBase(平台模式=specRoot)单路径在
|
|
1197
|
+
// meta 落项目侧 .sillyspec 时静默 miss → 锚点 diff 整链失效落主仓 fallback。双候选
|
|
1198
|
+
// (specBase 优先 + cwd/.sillyspec 兜底)两形全覆盖;sb 仍作他者声明过滤根(原语义不动)。
|
|
1199
|
+
const found = _readWorktreeMeta(specBase || null, cwd, changeName)
|
|
1200
|
+
if (found) {
|
|
1201
|
+
const meta = found.meta
|
|
1159
1202
|
// 优先 baselineCommit/actualBaseHash(baseline checkpoint 之后),回退 baseHash
|
|
1160
1203
|
const diffBase = meta?.baselineCommit || meta?.actualBaseHash || meta?.baseHash
|
|
1161
1204
|
if (diffBase) {
|
|
1162
|
-
const gitDir =
|
|
1163
|
-
? meta.worktreePath
|
|
1164
|
-
: cwd
|
|
1205
|
+
const gitDir = found.gitDir
|
|
1165
1206
|
const files = runGitDiffNameOnly(gitDir, `${diffBase}..HEAD`)
|
|
1207
|
+
// 坑 verify-evidence-account-diff-misses-committed-changes(2026-09-13 实证):
|
|
1208
|
+
// wt-commit 流程把本变更提交落在**主仓 HEAD**,worktree 分支不前移——仅查 worktree
|
|
1209
|
+
// 侧 diffBase..HEAD 时已提交文件全部 miss(7 task diffHit=false 连环假红,verify 两连
|
|
1210
|
+
// 阻断只能走人工豁免)。并集**主仓同区间** diff(对齐 wt-commit 产出形态,坑文档建议的
|
|
1211
|
+
// 「change 基线 commit..HEAD」口径);主仓区间含并行会话提交 → 过他者声明过滤
|
|
1212
|
+
//(splitOwnVsForeignDiffFiles,与下方 fallback 同款)防 diffHit 假阳。主仓查询失败
|
|
1213
|
+
// 不损原 worktree 结果。
|
|
1214
|
+
if (files !== null && gitDir !== cwd) {
|
|
1215
|
+
try {
|
|
1216
|
+
const mainFiles = runGitDiffNameOnly(cwd, `${diffBase}..HEAD`)
|
|
1217
|
+
if (Array.isArray(mainFiles)) {
|
|
1218
|
+
const merged = [...new Set([...files, ...mainFiles])]
|
|
1219
|
+
const { own, foreign } = splitOwnVsForeignDiffFiles(cwd, changeName, merged, { specBase: sb })
|
|
1220
|
+
if (foreign.length > 0) {
|
|
1221
|
+
console.warn(`⚠️ verify 对账(主仓区间并集)已排除 ${foreign.length} 个并行会话声明的文件:${foreign.slice(0, 5).map(x => `${x.file}←${x.owners[0]}`).join(', ')}${foreign.length > 5 ? ' 等' : ''}`)
|
|
1222
|
+
}
|
|
1223
|
+
return own
|
|
1224
|
+
}
|
|
1225
|
+
} catch { /* 主仓区间查询失败 → 保持 worktree 结果 */ }
|
|
1226
|
+
}
|
|
1166
1227
|
if (files !== null) return files
|
|
1167
1228
|
// worktree diff 异常 → 落主仓兜底(保持与原 gitChangedFiles 相同的 null 语义)
|
|
1168
1229
|
}
|
|
@@ -1306,7 +1367,7 @@ export function runVerifyTestCheck({ cwd, specBase, changeName = null, ctx = nul
|
|
|
1306
1367
|
},
|
|
1307
1368
|
})
|
|
1308
1369
|
} else if (action === 'module-subset') {
|
|
1309
|
-
mainResult = runModuleSubset({ cwd, specBase, changeName, hits, knownFailures })
|
|
1370
|
+
mainResult = runModuleSubset({ cwd, specBase, changeName, hits, knownFailures, changedFiles: lastChangedFiles })
|
|
1310
1371
|
} else if (action === 'module-zero-hit-skip') {
|
|
1311
1372
|
// module 模式 0 命中:不静默回退注定超时/含预存失败的全量(坑 verify-worktree-... 修复方向 3)。
|
|
1312
1373
|
// 据 verify-result.md 自报告判定;想跑全量请显式设 test_strategy: full。
|
|
@@ -1637,12 +1698,21 @@ function runFullCommand({ yamlText, localYamlPath, cwd, specBase, changeName, fa
|
|
|
1637
1698
|
* 串行跑命中的模块子集,聚合结果。
|
|
1638
1699
|
* 返回 shape 与 runFullCommand 一致(status/command/exitCode/durationMs/outputTail/reason/resultPath)。
|
|
1639
1700
|
*/
|
|
1640
|
-
function runModuleSubset({ cwd, specBase, changeName, hits, knownFailures = [] }) {
|
|
1701
|
+
function runModuleSubset({ cwd, specBase, changeName, hits, knownFailures = [], changedFiles = [] }) {
|
|
1641
1702
|
const subsetStartedAt = Date.now()
|
|
1642
1703
|
const perModule = hits.map(h => runOneModule(h.name, h.test, cwd, knownFailures))
|
|
1704
|
+
// 依赖测试伪模块(module-test-face-rot,2026-09-16 本会话实证):硬编码模块测试清单不含
|
|
1705
|
+
// 变更 src 文件的直接断言测试 → module 收窄漏全量断言。附加执行「import 变更 src 的测试 ∪
|
|
1706
|
+
// 变更的 test 文件」中未被命中模块命令覆盖的部分;无额外文件零行为(不新增失败面)。
|
|
1707
|
+
const deps = discoverModuleDependentTests({ cwd, changedFiles, coveredCommands: hits.map(h => h.test) })
|
|
1708
|
+
if (deps.length > 0) {
|
|
1709
|
+
perModule.push(runOneModule('deps(auto)', `node --test ${deps.slice(0, 30).join(' ')}`, cwd, knownFailures))
|
|
1710
|
+
console.log(`ℹ️ module 子集已附加依赖测试伪模块 deps(auto):${deps.length} 个(import 变更 src/变更的 test 本体,未被模块命令串覆盖——治硬编码清单腐烂)`)
|
|
1711
|
+
}
|
|
1643
1712
|
const status = aggregateStatus(perModule)
|
|
1644
1713
|
|
|
1645
|
-
|
|
1714
|
+
let command = `module[${hits.map(h => h.name).join(',')}]`
|
|
1715
|
+
if (deps.length > 0) command += `+deps(${Math.min(deps.length, 30)})`
|
|
1646
1716
|
const exitCode = status === 'passed' ? 0 : 1
|
|
1647
1717
|
const durationMs = Date.now() - subsetStartedAt
|
|
1648
1718
|
|
|
@@ -2074,6 +2144,8 @@ export function runVerifyRequiredEvidenceCheck({ cwd, specBase, changeName = nul
|
|
|
2074
2144
|
* 天然不覆盖该类路径,硬要求交集必假红)。verifyStartAt 缺省走 R-05 fallback(design.md
|
|
2075
2145
|
* created_at 之后宽容 + warning)。status 语义扩 'blocked'(missing 无豁免 / satisfied 核验
|
|
2076
2146
|
* 不过)——gates 接线(task-03)据此阻断 verify 完成。
|
|
2147
|
+
* 逐文件存在性/mtime 双根取数(坑⑤ / D-005@v1):候选根 [cwd, worktree 根]——apply 前
|
|
2148
|
+
* 新文件只在 worktree,单查主仓必误报「文件不存在」;meta 缺失/in-place 退单根(零回归)。
|
|
2077
2149
|
*/
|
|
2078
2150
|
function runRequiredEvidenceCheckV2({ items, slots, cwd, specBase, changeName, verifyStartAt }) {
|
|
2079
2151
|
const warnings = []
|
|
@@ -2101,6 +2173,26 @@ function runRequiredEvidenceCheckV2({ items, slots, cwd, specBase, changeName, v
|
|
|
2101
2173
|
changedSet = new Set((changed || []).map(p => String(p).replace(/\\/g, '/')))
|
|
2102
2174
|
} catch { changedSet = null }
|
|
2103
2175
|
|
|
2176
|
+
// ── 候选根解析(坑⑤ / D-005@v1,消费侧双根):apply 前新文件只在 worktree,逐文件核验
|
|
2177
|
+
// 单查主仓 join(cwd, vf) 必误报「文件不存在」错误阻断 verify。worktree 根口径与
|
|
2178
|
+
// resolveVerifyChangedFiles(~1061 metaPath 段)同源:meta 在场 && 非 in-place-fallback
|
|
2179
|
+
// && worktreePath 目录存在 → roots = [cwd, worktreePath];任一不满足退 [cwd] 单根
|
|
2180
|
+
// (零回归)。解析异常 fail-open 退单根 + warning(不新增阻断路径)。静默双根——对齐
|
|
2181
|
+
// buildAcceptanceHints(verify-probes.js:223)双根先例,核验明细 root 字段已可辨。──
|
|
2182
|
+
let roots = [cwd]
|
|
2183
|
+
try {
|
|
2184
|
+
const metaPath = join(specBase || join(cwd, '.sillyspec'), '.runtime', 'worktrees', changeName, 'meta.json')
|
|
2185
|
+
if (existsSync(metaPath)) {
|
|
2186
|
+
const meta = JSON.parse(readFileSync(metaPath, 'utf8'))
|
|
2187
|
+
if (meta.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath)) {
|
|
2188
|
+
roots = [cwd, meta.worktreePath]
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
} catch (e) {
|
|
2192
|
+
roots = [cwd]
|
|
2193
|
+
warnings.push(`worktree meta 解析失败(${e.message}),evidence 双根核验退主仓单根`)
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2104
2196
|
const detailed = []
|
|
2105
2197
|
let blockedCount = 0
|
|
2106
2198
|
for (const item of items) {
|
|
@@ -2123,11 +2215,17 @@ function runRequiredEvidenceCheckV2({ items, slots, cwd, specBase, changeName, v
|
|
|
2123
2215
|
}
|
|
2124
2216
|
for (const vf of slot.verifiedFiles) {
|
|
2125
2217
|
const pathClass = classifyVerifiedFile(vf)
|
|
2126
|
-
|
|
2127
|
-
|
|
2218
|
+
// 双根取数(坑⑤ / D-005@v1,形态沿 buildAcceptanceHints 双根先例):任一根存在即
|
|
2219
|
+
// filesExist=true;双根都在取 worktree 根(R-05:新改动所在,主仓旧 mtime 不误判
|
|
2220
|
+
// 过期)——倒序找命中根(roots=[cwd] 时即单根现状;roots=[cwd, wt] 时先试 wt 再回退)。
|
|
2221
|
+
let hitRoot = null
|
|
2222
|
+
for (let i = roots.length - 1; i >= 0; i--) {
|
|
2223
|
+
if (existsSync(join(roots[i], vf))) { hitRoot = roots[i]; break }
|
|
2224
|
+
}
|
|
2225
|
+
const filesExist = hitRoot !== null
|
|
2128
2226
|
let mtimeOk = null
|
|
2129
2227
|
if (filesExist && startAt) {
|
|
2130
|
-
try { mtimeOk = statSync(
|
|
2228
|
+
try { mtimeOk = statSync(join(hitRoot, vf)).mtimeMs >= startAt - 60_000 } catch { mtimeOk = null }
|
|
2131
2229
|
}
|
|
2132
2230
|
let diffHit = null
|
|
2133
2231
|
if (pathClass === 'code') {
|
|
@@ -2137,7 +2235,7 @@ function runRequiredEvidenceCheckV2({ items, slots, cwd, specBase, changeName, v
|
|
|
2137
2235
|
}
|
|
2138
2236
|
const ok = filesExist && mtimeOk !== false && diffHit !== false
|
|
2139
2237
|
if (!ok) allOk = false
|
|
2140
|
-
perFile.push({ path: vf, pathClass, filesExist, mtimeOk, diffHit,
|
|
2238
|
+
perFile.push({ path: vf, pathClass, filesExist, mtimeOk, diffHit, root: hitRoot,
|
|
2141
2239
|
reason: !filesExist ? '文件不存在'
|
|
2142
2240
|
: (mtimeOk === false ? `mtime 早于 verifyStartAt(${new Date(startAt).toISOString()})`
|
|
2143
2241
|
: (diffHit === false ? '不在本变更 git diff 内' : null)) })
|
|
@@ -2225,7 +2323,7 @@ function normalizeReviewChangedFile(p) {
|
|
|
2225
2323
|
* @returns {{ cardCount: number, declarations: Array<{task:string, path:string, isNew:boolean, raw:string, invalid:string|null}>, crossRepoCards: string[], noDeclarationCount: number }}
|
|
2226
2324
|
*/
|
|
2227
2325
|
function collectDeclaredTargetFiles(specBase, changeName) {
|
|
2228
|
-
const out = { cardCount: 0, declarations: [], crossRepoCards: [], noDeclarationCount: 0 }
|
|
2326
|
+
const out = { cardCount: 0, declarations: [], crossRepoCards: [], crossRepoDeclarations: {}, noDeclarationCount: 0 }
|
|
2229
2327
|
const tasksDir = join(specBase, 'changes', changeName, 'tasks')
|
|
2230
2328
|
if (!existsSync(tasksDir)) return out
|
|
2231
2329
|
// task-NN.md 过滤 + 排序:与 plan-postcheck / task-review 的枚举口径一致,输出确定性
|
|
@@ -2237,7 +2335,18 @@ function collectDeclaredTargetFiles(specBase, changeName) {
|
|
|
2237
2335
|
const fm = content.match(/^---\n([\s\S]*?)\n---/)?.[1] || ''
|
|
2238
2336
|
const task = (fm.match(/^id:\s*(.+)/m)?.[1] || '').trim() || file.replace(/\.md$/, '')
|
|
2239
2337
|
const repo = parseRepo(content)
|
|
2240
|
-
if (repo) {
|
|
2338
|
+
if (repo) {
|
|
2339
|
+
out.crossRepoCards.push(`${task}(repo: ${repo})`)
|
|
2340
|
+
// 跨仓声明留存(坑 cross-repo-reconcile-blindness,2026-09-15 复盘兑现 D-004 分期):
|
|
2341
|
+
// 主仓对账仍剔除跨仓卡(主仓 actual 永远对不上跨仓声明),但声明不丢弃——
|
|
2342
|
+
// reconcileCrossRepoDeclarations 到各自仓对账(advisory)
|
|
2343
|
+
const crossEntries = parseTargetFiles(content).entries || []
|
|
2344
|
+
if (crossEntries.length > 0) {
|
|
2345
|
+
;(out.crossRepoDeclarations[repo] = out.crossRepoDeclarations[repo] || [])
|
|
2346
|
+
.push(...crossEntries.map(e => ({ task, path: e.path, isNew: e.isNew })))
|
|
2347
|
+
}
|
|
2348
|
+
continue
|
|
2349
|
+
}
|
|
2241
2350
|
const { entries, missing } = parseTargetFiles(content)
|
|
2242
2351
|
if (missing) { out.noDeclarationCount++; continue }
|
|
2243
2352
|
for (const e of entries) {
|
|
@@ -2304,14 +2413,22 @@ function parsePorcelainFilePaths(raw) {
|
|
|
2304
2413
|
* resolveMainChangedFiles 同口径,三锚全缺 null);形态 B 取 merge-base hash(分支不存在/
|
|
2305
2414
|
* merge-base 不可得 null);降级返回恒 null。
|
|
2306
2415
|
*
|
|
2307
|
-
* @returns={{ ok: boolean, form: 'worktree'|'post-apply', files: string[], sources: string[], foreignExcluded: number, degradedReason: string|null, baseAnchor: string|null }}
|
|
2416
|
+
* @returns={{ ok: boolean, form: 'worktree'|'post-apply', files: string[], sources: string[], foreignExcluded: number, foreignExcludedFiles: string[], degradedReason: string|null, baseAnchor: string|null }}
|
|
2308
2417
|
*/
|
|
2309
2418
|
export function resolveReconcileActualFiles({ cwd, specBase, runtimeRoot, changeName }) {
|
|
2310
|
-
|
|
2311
|
-
|
|
2419
|
+
// 形态判定双根化(坑 platform-dual-root-form-misjudge,2026-09-15 复盘实证:平台模式
|
|
2420
|
+
// specBase=specRoot 时项目侧 .sillyspec 的 meta 读不到 → 误判 post-apply → 按主仓 git
|
|
2421
|
+
// 三源对账 → ②类 21 条全量假红阻断 verify 收尾)。_readWorktreeMeta 双候选(specBase
|
|
2422
|
+
// 优先 + cwd/.sillyspec 兜底,contract-matrix 既有样板)为形态判定的唯一权威入口。
|
|
2423
|
+
const wtMeta = _readWorktreeMeta(specBase || null, cwd, changeName)
|
|
2424
|
+
const form = wtMeta ? 'worktree' : 'post-apply'
|
|
2312
2425
|
const sources = []
|
|
2313
2426
|
const union = new Set()
|
|
2314
2427
|
let foreignExcluded = 0
|
|
2428
|
+
// 退栈文件名单(scope-audit-cross-repo-blindness 改进点 3):原只回计数,scope-audit 侧
|
|
2429
|
+
// 无法区分「真未动」与「已写盘但被退栈」——补名单(仅文件名,owners 不重复携带),
|
|
2430
|
+
// 消费方按「planned ∩ excluded ∩ 盘面存在」标「疑似他者已实现」。
|
|
2431
|
+
const foreignExcludedFiles = []
|
|
2315
2432
|
let baseAnchor = null
|
|
2316
2433
|
|
|
2317
2434
|
if (form === 'worktree') {
|
|
@@ -2319,15 +2436,12 @@ export function resolveReconcileActualFiles({ cwd, specBase, runtimeRoot, change
|
|
|
2319
2436
|
// working-tree 并入、fail-open 均为该函数既有语义,不另造口径)——
|
|
2320
2437
|
const files = resolveVerifyChangedFiles(cwd, changeName, null, { includeWorkingTree: true, specBase })
|
|
2321
2438
|
if (files === null) {
|
|
2322
|
-
return { ok: false, form, files: [], sources, foreignExcluded,
|
|
2439
|
+
return { ok: false, form, files: [], sources, foreignExcluded, foreignExcludedFiles,
|
|
2323
2440
|
degradedReason: 'worktree 锚点 diff 与主仓 fallback 均失败(git 不可用 / 非仓库)', baseAnchor: null }
|
|
2324
2441
|
}
|
|
2325
|
-
// baseAnchor
|
|
2326
|
-
// resolveMainChangedFiles
|
|
2327
|
-
|
|
2328
|
-
const meta = JSON.parse(readFileSync(metaPath, 'utf8'))
|
|
2329
|
-
baseAnchor = meta?.baselineCommit || meta?.actualBaseHash || meta?.baseHash || null
|
|
2330
|
-
} catch {}
|
|
2442
|
+
// baseAnchor:读形态判定命中的同一份 worktree meta.json 锚点(baselineCommit>
|
|
2443
|
+
// actualBaseHash>baseHash,resolveMainChangedFiles 同口径;三锚全缺 → null)
|
|
2444
|
+
baseAnchor = wtMeta?.meta?.baselineCommit || wtMeta?.meta?.actualBaseHash || wtMeta?.meta?.baseHash || null
|
|
2331
2445
|
sources.push('worktree:diff-base..HEAD', 'worktree:status-porcelain(uncommitted)')
|
|
2332
2446
|
for (const f of files) union.add(normalizeReconcilePath(f))
|
|
2333
2447
|
} else {
|
|
@@ -2366,7 +2480,10 @@ export function resolveReconcileActualFiles({ cwd, specBase, runtimeRoot, change
|
|
|
2366
2480
|
let files = parsePorcelainFilePaths(statusRaw)
|
|
2367
2481
|
if (files.length > 0) {
|
|
2368
2482
|
const { own, foreign } = splitOwnVsForeignDiffFiles(cwd, changeName, files, { specBase })
|
|
2369
|
-
if (foreign.length > 0)
|
|
2483
|
+
if (foreign.length > 0) {
|
|
2484
|
+
foreignExcluded = foreign.length
|
|
2485
|
+
for (const fo of foreign) foreignExcludedFiles.push(normalizeReconcilePath(fo.file))
|
|
2486
|
+
}
|
|
2370
2487
|
files = own
|
|
2371
2488
|
}
|
|
2372
2489
|
for (const f of files) union.add(normalizeReconcilePath(f))
|
|
@@ -2383,13 +2500,13 @@ export function resolveReconcileActualFiles({ cwd, specBase, runtimeRoot, change
|
|
|
2383
2500
|
} catch { /* 兜底源失败 → 忽略 */ }
|
|
2384
2501
|
}
|
|
2385
2502
|
if (!diffOk && !statusOk) {
|
|
2386
|
-
return { ok: false, form, files: [], sources, foreignExcluded,
|
|
2503
|
+
return { ok: false, form, files: [], sources, foreignExcluded, foreignExcludedFiles,
|
|
2387
2504
|
degradedReason: 'merge-base 锚定 diff 未得且主仓 status 失败(git 不可用 / 非仓库 / 无锚点分支)', baseAnchor: null }
|
|
2388
2505
|
}
|
|
2389
2506
|
}
|
|
2390
2507
|
|
|
2391
2508
|
const files = [...new Set(filterDeliverableFiles([...union]).filter(Boolean))].sort()
|
|
2392
|
-
return { ok: true, form, files, sources, foreignExcluded, degradedReason: null, baseAnchor }
|
|
2509
|
+
return { ok: true, form, files, sources, foreignExcluded, foreignExcludedFiles, degradedReason: null, baseAnchor }
|
|
2393
2510
|
}
|
|
2394
2511
|
|
|
2395
2512
|
/**
|
|
@@ -2399,13 +2516,15 @@ export function resolveReconcileActualFiles({ cwd, specBase, runtimeRoot, change
|
|
|
2399
2516
|
* - 归属过滤:run 目录带 change 戳且不等值 → 他变更的 run 跳过(task-NN 跨变更同名,混扫必
|
|
2400
2517
|
* 错归因;无戳旧 run 保留——向后兼容,task-review.js resolveLatestExecuteRunIdWithTasks 同款)。
|
|
2401
2518
|
* - runId 形如 exec-YYYY-MM-DD-HHMMSS:字典序倒序 = 新 run 优先(review 以最新执行为准)。
|
|
2402
|
-
* - changedFiles 是 agent 手写(注记/相对路径常态),归一后 exact match
|
|
2403
|
-
*
|
|
2519
|
+
* - changedFiles 是 agent 手写(注记/相对路径常态),归一后 exact match、全命中收集(多归属,
|
|
2520
|
+
* 坑④ worktree-tick-guard-commit-only:原「首个命中即止」吞掉同文件多 task 的归属)——
|
|
2521
|
+
* 归因只是③类报告的附注,错比漏代价低;同一 taskId 去重,顺序 = 扫描序(run 倒序新优先 +
|
|
2522
|
+
* task 目录字典序)。
|
|
2404
2523
|
*
|
|
2405
2524
|
* @param {string} runtimeRoot .sillyspec/.runtime
|
|
2406
2525
|
* @param {string} changeName
|
|
2407
2526
|
* @param {string[]} paths 待归因的③类路径(git 口径的干净路径)
|
|
2408
|
-
* @returns {Map<string, string>} normalized(path) → taskId
|
|
2527
|
+
* @returns {Map<string, string[]>} normalized(path) → taskId[](全命中收集,保序去重)
|
|
2409
2528
|
*/
|
|
2410
2529
|
function attributeSuspectTasks(runtimeRoot, changeName, paths) {
|
|
2411
2530
|
const map = new Map()
|
|
@@ -2432,7 +2551,10 @@ function attributeSuspectTasks(runtimeRoot, changeName, paths) {
|
|
|
2432
2551
|
const taskId = (typeof review.task === 'string' && review.task.trim()) || t
|
|
2433
2552
|
for (const cf of Array.isArray(review.changedFiles) ? review.changedFiles : []) {
|
|
2434
2553
|
const n = normalizeReviewChangedFile(cf)
|
|
2435
|
-
if (n
|
|
2554
|
+
if (!n || !wantSet.has(n)) continue
|
|
2555
|
+
// 多归属:同文件被多 task 声明时全收集(D-004@v1,坑④)——旧单归属在此吞掉后续 task
|
|
2556
|
+
const list = map.get(n)
|
|
2557
|
+
if (list) { if (!list.includes(taskId)) list.push(taskId) } else map.set(n, [taskId])
|
|
2436
2558
|
}
|
|
2437
2559
|
}
|
|
2438
2560
|
}
|
|
@@ -2457,7 +2579,7 @@ function attributeSuspectTasks(runtimeRoot, changeName, paths) {
|
|
|
2457
2579
|
* status: 'ok'|'missing_declared'|'undeclared'|'skipped'|'degraded',
|
|
2458
2580
|
* matched: string[], // ①交集(唯一路径排序)
|
|
2459
2581
|
* missing: Array<{task: string, path: string, isNew?: boolean}>, // ②声明没做(isNew 标 NEW: 声明,剥前缀 path)
|
|
2460
|
-
* undeclared: Array<{path: string, suspectTask?: string}>, // ③做了没声明(suspectTask=review.json
|
|
2582
|
+
* undeclared: Array<{path: string, suspectTask?: string}>, // ③做了没声明(suspectTask=review.json 尽力归因,多归属 join('、') 后的 string)
|
|
2461
2583
|
* skipReason: string|null, // skipped/degraded 的原因(其余状态为 null)
|
|
2462
2584
|
* notes: string[], // 跨仓卡剔除 / 并行 WIP 剔除等提示(additive)
|
|
2463
2585
|
* form: 'worktree'|'post-apply'|null, // actual 形态(skipped 无 actual 时为 null;additive)
|
|
@@ -2479,8 +2601,24 @@ export function reconcileTargetFiles({ cwd, specBase = null, changeName = null,
|
|
|
2479
2601
|
|
|
2480
2602
|
// —— 声明侧 ——
|
|
2481
2603
|
const decl = collectDeclaredTargetFiles(sb, changeName)
|
|
2482
|
-
|
|
2483
|
-
|
|
2604
|
+
// —— 跨仓 per-repo 对账(坑 cross-repo-reconcile-blindness,2026-09-15 复盘兑现 D-004 分期:
|
|
2605
|
+
// 此前 22 个跨仓文件在归档表全标「计划未动」、跨仓全靠人工到对应仓解释)——advisory 不阻断:
|
|
2606
|
+
// 锚点是各仓 HEAD~1..HEAD 最近提交窗口(多 task 同仓只反映最近一笔,锚点脆弱期会产②类假
|
|
2607
|
+
// 信号),只报告不翻转主仓状态。结果经 crossRepo 字段(gates 渲染明细)与 notes(摘要随
|
|
2608
|
+
// 各状态出口流动)双通道出口。
|
|
2609
|
+
let crossRepo = []
|
|
2610
|
+
if (decl.crossRepoCards.length > 0) {
|
|
2611
|
+
try {
|
|
2612
|
+
crossRepo = reconcileCrossRepoDeclarations({ specBase: sb, cwd, declarationsByRepo: decl.crossRepoDeclarations })
|
|
2613
|
+
} catch { crossRepo = [] /* 跨仓对账异常不拖垮主仓链(advisory 定位) */ }
|
|
2614
|
+
for (const label of decl.crossRepoCards) {
|
|
2615
|
+
notes.push(`跨仓 task 卡 ${label} 不进主仓对账(主仓 actual 对不上跨仓声明,D-004)——其声明已转 per-repo 对账(见下方跨仓对账段)`)
|
|
2616
|
+
}
|
|
2617
|
+
for (const r of crossRepo) {
|
|
2618
|
+
notes.push(r.degradedReason
|
|
2619
|
+
? `跨仓 ${r.repo}:${r.degradedReason}`
|
|
2620
|
+
: `跨仓 ${r.repo} 对账:声明 ${r.declaredCount} / 实测 ${r.actualCount} / 对上 ${r.matched.length} / ②缺 ${r.missing.length} / ③多 ${r.undeclared.length}${r.scaffoldCount > 0 ? `(另有 ${r.scaffoldCount} 脚手架已聚合)` : ''}——advisory(锚点=该仓最近提交窗口)`)
|
|
2621
|
+
}
|
|
2484
2622
|
}
|
|
2485
2623
|
if (decl.cardCount === 0) {
|
|
2486
2624
|
return { status: 'skipped', matched, missing, undeclared,
|
|
@@ -2494,7 +2632,7 @@ export function reconcileTargetFiles({ cwd, specBase = null, changeName = null,
|
|
|
2494
2632
|
&& decl.noDeclarationCount === decl.cardCount - decl.crossRepoCards.length) {
|
|
2495
2633
|
return { status: 'skipped', matched, missing, undeclared,
|
|
2496
2634
|
skipReason: `严格模式变更(created_at ≥ IR_STRICT_SINCE)主仓 task 卡全部未声明 target_files(${decl.noDeclarationCount}/${decl.cardCount - decl.crossRepoCards.length} 张主仓卡${decl.crossRepoCards.length > 0 ? `,另 ${decl.crossRepoCards.length} 张跨仓卡已剔除` : ''})`,
|
|
2497
|
-
notes, form: null, sources: [],
|
|
2635
|
+
notes, form: null, sources: [], crossRepo,
|
|
2498
2636
|
strictViolation: {
|
|
2499
2637
|
code: 'target_files_all_missing_strict',
|
|
2500
2638
|
message: `严格模式要求每个主仓 task 卡声明 target_files(计划改动的文件清单,taskcard 骨架已预置字段)——逐卡 Edit 填入:已存在文件写仓根相对精确路径,新建文件加 NEW: 前缀;填毕重跑 --done(进度不丢)`,
|
|
@@ -2506,7 +2644,7 @@ export function reconcileTargetFiles({ cwd, specBase = null, changeName = null,
|
|
|
2506
2644
|
: `所有 task 卡均未声明 target_files(${decl.noDeclarationCount}/${decl.cardCount} 张无声明` +
|
|
2507
2645
|
`${decl.crossRepoCards.length > 0 ? `,另 ${decl.crossRepoCards.length} 张跨仓卡已剔除` : ''})` +
|
|
2508
2646
|
`——存量卡可忽略;新卡请补:每条为仓根相对精确文件路径,不存在的文件加 NEW: 前缀`
|
|
2509
|
-
return { status: 'skipped', matched, missing, undeclared, skipReason: reason, notes, form: null, sources: [] }
|
|
2647
|
+
return { status: 'skipped', matched, missing, undeclared, skipReason: reason, notes, form: null, sources: [], crossRepo }
|
|
2510
2648
|
}
|
|
2511
2649
|
|
|
2512
2650
|
// —— actual 侧(三源并集,两形态)——
|
|
@@ -2518,7 +2656,7 @@ export function reconcileTargetFiles({ cwd, specBase = null, changeName = null,
|
|
|
2518
2656
|
// 降级(fail-soft 不误红):git 全部不可用 → actual 不可得,不产生 missing/undeclared
|
|
2519
2657
|
return { status: 'degraded', matched, missing, undeclared,
|
|
2520
2658
|
skipReason: `git 全部不可用,actual 不可得,对账降级跳过——${actual.degradedReason}`,
|
|
2521
|
-
notes, form: actual.form, sources: actual.sources }
|
|
2659
|
+
notes, form: actual.form, sources: actual.sources, crossRepo }
|
|
2522
2660
|
}
|
|
2523
2661
|
|
|
2524
2662
|
// —— 三类差集 ——
|
|
@@ -2547,17 +2685,36 @@ export function reconcileTargetFiles({ cwd, specBase = null, changeName = null,
|
|
|
2547
2685
|
missing.push(d.isNew ? { task: d.task, path: d.path, isNew: true } : { task: d.task, path: d.path })
|
|
2548
2686
|
}
|
|
2549
2687
|
}
|
|
2550
|
-
// ③做了没声明(scope creep,WARNING):actual − 声明集;suspectTask 尽力归因仅报告(D-002
|
|
2688
|
+
// ③做了没声明(scope creep,WARNING):actual − 声明集;suspectTask 尽力归因仅报告(D-002)。
|
|
2689
|
+
// 归因多归属(attributeSuspectTasks 返 string[],坑④):组装边界 join('、') 成 string——
|
|
2690
|
+
// gates.js / archive-delta.js 等下游渲染零改动(suspectTask 对外形态保持 string)。
|
|
2551
2691
|
const suspect = attributeSuspectTasks(rt, changeName, actual.files.filter(p => !declaredKeySet.has(pathKey(p))))
|
|
2692
|
+
// 脚手架软桶(坑 reconcile-undeclared-scaffold-flood,2026-09-15 复盘实证:worktree baseline
|
|
2693
|
+
// 拷入的 .claude/skills、attachments 等设施文件 58 条逐条进③类、每轮 verify 重复刷屏):
|
|
2694
|
+
// classifyToolScaffold(worktree-apply / scope-audit 同款口径)命中的工具/平台设施文件聚合
|
|
2695
|
+
// 为一行 note——环境设施面非交付物,逐条列③类只会淹没真 scope creep。全为脚手架时③类
|
|
2696
|
+
// 判 ok(脚手架不构成 scope creep 语义)。
|
|
2697
|
+
const scaffoldBuckets = { platform: 0, tool: 0 }
|
|
2698
|
+
const scaffoldSamples = []
|
|
2552
2699
|
for (const path of actual.files) {
|
|
2553
2700
|
if (declaredKeySet.has(pathKey(path))) continue
|
|
2701
|
+
const bucket = classifyToolScaffold(path)
|
|
2702
|
+
if (bucket) {
|
|
2703
|
+
scaffoldBuckets[bucket] = (scaffoldBuckets[bucket] || 0) + 1
|
|
2704
|
+
if (scaffoldSamples.length < 5) scaffoldSamples.push(path)
|
|
2705
|
+
continue
|
|
2706
|
+
}
|
|
2554
2707
|
const s = suspect.get(normalizeReviewChangedFile(path))
|
|
2555
|
-
undeclared.push(s ? { path, suspectTask: s } : { path })
|
|
2708
|
+
undeclared.push(Array.isArray(s) && s.length > 0 ? { path, suspectTask: s.join('、') } : { path })
|
|
2709
|
+
}
|
|
2710
|
+
const scaffoldTotal = scaffoldBuckets.platform + scaffoldBuckets.tool
|
|
2711
|
+
if (scaffoldTotal > 0) {
|
|
2712
|
+
notes.push(`③类另有 ${scaffoldTotal} 个工具/平台脚手架文件(platform ${scaffoldBuckets.platform} / tool ${scaffoldBuckets.tool},如 ${scaffoldSamples.join('、')}${scaffoldTotal > scaffoldSamples.length ? ' 等' : ''})已聚合不逐条列——非交付物(scope-audit 设施桶同口径)`)
|
|
2556
2713
|
}
|
|
2557
2714
|
|
|
2558
2715
|
// 状态聚合:②在场即 ERROR 态优先(gates 阻断语义靠它兑现);仅③ → WARNING 态
|
|
2559
2716
|
const status = missing.length > 0 ? 'missing_declared' : (undeclared.length > 0 ? 'undeclared' : 'ok')
|
|
2560
|
-
return { status, matched, missing, undeclared, skipReason: null, notes, form: actual.form, sources: actual.sources }
|
|
2717
|
+
return { status, matched, missing, undeclared, undeclaredScaffold: scaffoldTotal, skipReason: null, notes, form: actual.form, sources: actual.sources, crossRepo }
|
|
2561
2718
|
}
|
|
2562
2719
|
|
|
2563
2720
|
// ─────────────────────────────────────────────────────────────────────────────
|