sillyspec 3.32.2 → 3.32.3
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-flow/SKILL.md +1 -0
- package/package.json +1 -1
- package/src/cross-repo-reconcile.js +33 -0
- package/src/flow-parity.js +19 -26
- package/src/flow.js +10 -8
- package/src/friction-tally.js +2 -1
- package/src/git-helper.js +23 -1
- package/src/index.js +23 -11
- package/src/module-impact.js +3 -1
- package/src/progress/stage-machine.js +8 -1
- package/src/run/code-face-key.js +84 -0
- package/src/run/complete.js +16 -7
- package/src/run/gate-snapshot.js +19 -0
- package/src/run/gates.js +109 -67
- package/src/run/green-cache.js +11 -19
- package/src/run/verify-quality-scan.js +88 -45
- package/src/scope-audit.js +50 -24
- package/src/spec-sync.js +98 -20
- package/src/stages/brainstorm.js +2 -1
- package/src/stages/cmd-existence.js +5 -1
- package/src/test-bindings.js +2 -0
- package/src/verify-postcheck.js +141 -18
- package/src/verify-probes.js +141 -0
- package/src/wt-commit.js +82 -3
package/src/verify-probes.js
CHANGED
|
@@ -2180,6 +2180,9 @@ export function runVerifyProbes({ cwd, changeName, specDir = null }) {
|
|
|
2180
2180
|
task: fmId || f.replace(/\.md$/, ''),
|
|
2181
2181
|
acceptance: parsed.acceptance,
|
|
2182
2182
|
fmError: parsed.status === 'invalid-yaml' ? parsed.error : null,
|
|
2183
|
+
// FR-06(2026-10-09-verify-reuse-friction):行 repo 归属写侧透传(跨仓卡 tests
|
|
2184
|
+
// 仓根相对,读侧按行根解析)
|
|
2185
|
+
repo: parseRepo(raw) || null,
|
|
2183
2186
|
raw,
|
|
2184
2187
|
})
|
|
2185
2188
|
}
|
|
@@ -2280,6 +2283,9 @@ export function runVerifyProbes({ cwd, changeName, specDir = null }) {
|
|
|
2280
2283
|
discovery: 'machine',
|
|
2281
2284
|
confirmed_by: null,
|
|
2282
2285
|
source_change: changeName,
|
|
2286
|
+
// FR-06(2026-10-09-verify-reuse-friction):跨仓卡的 tests 是仓根相对——
|
|
2287
|
+
// 行携 repo 归属供读侧按行根解析(缺省不落键=主仓,D-005@v1)。
|
|
2288
|
+
...(t.repo && t.repo !== 'main' ? { repo: t.repo } : {}),
|
|
2283
2289
|
})
|
|
2284
2290
|
})
|
|
2285
2291
|
}
|
|
@@ -3854,3 +3860,138 @@ export function refreshProbeSections(existingText, freshReportText) {
|
|
|
3854
3860
|
}
|
|
3855
3861
|
return { text: out.join('\n'), replaced, kept, appended, carriedRows }
|
|
3856
3862
|
}
|
|
3863
|
+
|
|
3864
|
+
// ── --force 保人工面(2026-10-09-verify-papercuts ②)──
|
|
3865
|
+
// postmortem(2026-10-09-verify-reuse-friction 收口实证):--force 全骨架重置虽有自动备份,
|
|
3866
|
+
// 但结论槽/移交项表/探针矩阵已填判定/决策矩阵已填格仍全部被 wipe——重填整套人工面是当日
|
|
3867
|
+
// 最大单点浪费(备份在 .runtime 里,找回-回填的来回远比携载贵)。本组合器把 --force 语义
|
|
3868
|
+
// 从「全量重置 + 备份找回」升级为「机器段全量重刷 + 人工面按段携载」:
|
|
3869
|
+
// - 纯人工段(结论/移交项/证据账/集成回执/任务完成度/设计一致性/独立复核/风险等级):
|
|
3870
|
+
// 旧段已填(无 <待填/<TODO 占位)→ 原样保留;未填 → 用新骨架(占位换新占位,无损失);
|
|
3871
|
+
// - 矩阵段(决策追踪矩阵/接口验证覆盖矩阵):表格行按首列键携载(mergeProbeSection 同
|
|
3872
|
+
// 款语义——已填 Evidence/判定行不丢,新增行从新骨架进场);
|
|
3873
|
+
// - 探针容器段(## 探针结果):逐探针号 merge(已填表格行携载,机器结论行刷新);
|
|
3874
|
+
// - 其余机器段:新骨架;旧文独有的自定义段:追加保留(不销毁用户内容)。
|
|
3875
|
+
// 安全方向与 refreshProbeSections 一致:宁可少刷新不误删;stale 由 gate 的
|
|
3876
|
+
// checkProbeConsistency 独立把关。备份照旧保留(兜底通道不撤)。
|
|
3877
|
+
|
|
3878
|
+
/** 顶层段切分(^## 标题行起,至下一 ^## 或 EOF;# 级更细的 #### 归属其容器段) */
|
|
3879
|
+
function splitTopSections(text) {
|
|
3880
|
+
const lines = String(text ?? '').split('\n')
|
|
3881
|
+
const sections = []
|
|
3882
|
+
let cur = null
|
|
3883
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3884
|
+
if (/^## /.test(lines[i])) {
|
|
3885
|
+
if (cur) cur.endLine = i
|
|
3886
|
+
cur = { headingLine: lines[i], startLine: i, endLine: lines.length }
|
|
3887
|
+
sections.push(cur)
|
|
3888
|
+
}
|
|
3889
|
+
}
|
|
3890
|
+
return { lines, sections }
|
|
3891
|
+
}
|
|
3892
|
+
|
|
3893
|
+
/** 段身份键:剥 [层:…] 尾注与(…)括注——同族段跨渲染措辞漂移仍可对上 */
|
|
3894
|
+
function topSectionKey(headingLine) {
|
|
3895
|
+
return headingLine
|
|
3896
|
+
.replace(/^##\s*/, '')
|
|
3897
|
+
.replace(/\s*\[.*\]\s*$/, '')
|
|
3898
|
+
.replace(/(.*$/, '')
|
|
3899
|
+
.trim()
|
|
3900
|
+
}
|
|
3901
|
+
|
|
3902
|
+
const HUMAN_SECTION_KEYS = ['结论', '移交项', '证据账', '集成验证回执', '任务完成度', '设计一致性', '独立复核', '变更风险等级', '技术债务', 'Runtime Evidence', '代码审查']
|
|
3903
|
+
const MATRIX_SECTION_KEYS = ['决策追踪矩阵', '接口验证覆盖矩阵']
|
|
3904
|
+
const PROBE_CONTAINER_KEY = '探针结果'
|
|
3905
|
+
|
|
3906
|
+
/** 探针容器段合并:新骨架容器为基,逐探针号对旧容器做已填行携载;旧独有探针号追加 */
|
|
3907
|
+
function mergeProbeContainer(oldContainerLines, freshContainerLines) {
|
|
3908
|
+
const oldParsed = splitProbeSectionRanges(oldContainerLines.join('\n'))
|
|
3909
|
+
const freshParsed = splitProbeSectionRanges(freshContainerLines.join('\n'))
|
|
3910
|
+
const out = [...freshParsed.lines]
|
|
3911
|
+
let carriedRows = 0
|
|
3912
|
+
const oldByNum = new Map(oldParsed.sections.map(s => [s.num, s]))
|
|
3913
|
+
for (let idx = freshParsed.sections.length - 1; idx >= 0; idx--) {
|
|
3914
|
+
const fs = freshParsed.sections[idx]
|
|
3915
|
+
const os = oldByNum.get(fs.num)
|
|
3916
|
+
if (!os) continue
|
|
3917
|
+
const merged = mergeProbeSection(
|
|
3918
|
+
oldParsed.lines.slice(os.startLine, os.endLine),
|
|
3919
|
+
freshParsed.lines.slice(fs.startLine, fs.endLine),
|
|
3920
|
+
)
|
|
3921
|
+
// 倒序替换(审查 P2 修正,2026-10-09-verify-papercuts):mergeProbeSection 对含 stale
|
|
3922
|
+
// 键的段会在段尾追加(行数 > freshSeg),正序 splice 会使后续探针段 startLine 错位——
|
|
3923
|
+
// 从后往前替换则前面行号不受影响,任一段行数膨胀都安全。
|
|
3924
|
+
carriedRows += merged.carried
|
|
3925
|
+
// 用 merged 文本替换 fresh 容器中该探针段(逐段行数已知,从后往前替换保行号稳定)
|
|
3926
|
+
const freshSeg = freshParsed.lines.slice(fs.startLine, fs.endLine)
|
|
3927
|
+
out.splice(fs.startLine, freshSeg.length, ...merged.text.split('\n'))
|
|
3928
|
+
}
|
|
3929
|
+
const freshNums = new Set(freshParsed.sections.map(s => s.num))
|
|
3930
|
+
for (const os of oldParsed.sections) {
|
|
3931
|
+
if (!freshNums.has(os.num)) {
|
|
3932
|
+
out.push('', ...oldParsed.lines.slice(os.startLine, os.endLine))
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
return { text: out.join('\n'), carriedRows }
|
|
3936
|
+
}
|
|
3937
|
+
|
|
3938
|
+
/**
|
|
3939
|
+
* --force 应用骨架:机器段全量重刷 + 人工面携载(纯函数,导出供回归直测)。
|
|
3940
|
+
* @param {string} existingText 现文(含已填人工面)
|
|
3941
|
+
* @param {string} skeletonText generateVerifyResultSkeleton 新骨架
|
|
3942
|
+
* @returns {{ text: string, carriedSections: string[], carriedRows: number }}
|
|
3943
|
+
*/
|
|
3944
|
+
export function applySkeletonPreservingHuman(existingText, skeletonText) {
|
|
3945
|
+
const old = splitTopSections(existingText)
|
|
3946
|
+
const fresh = splitTopSections(skeletonText)
|
|
3947
|
+
const oldByKey = new Map()
|
|
3948
|
+
for (const s of old.sections) {
|
|
3949
|
+
const k = topSectionKey(s.headingLine)
|
|
3950
|
+
if (!oldByKey.has(k)) oldByKey.set(k, s) // 同键段取首个(重复标题形态保首段)
|
|
3951
|
+
}
|
|
3952
|
+
const carriedSections = []
|
|
3953
|
+
let carriedRows = 0
|
|
3954
|
+
const out = []
|
|
3955
|
+
let cursor = 0
|
|
3956
|
+
for (const fs of fresh.sections) {
|
|
3957
|
+
// 骨架段前的行原样带过
|
|
3958
|
+
for (; cursor < fs.startLine; cursor++) out.push(fresh.lines[cursor])
|
|
3959
|
+
const k = topSectionKey(fs.headingLine)
|
|
3960
|
+
const os = oldByKey.get(k)
|
|
3961
|
+
const freshSeg = fresh.lines.slice(fs.startLine, fs.endLine)
|
|
3962
|
+
if (!os) {
|
|
3963
|
+
out.push(...freshSeg)
|
|
3964
|
+
} else if (HUMAN_SECTION_KEYS.includes(k)) {
|
|
3965
|
+
const oldSeg = old.lines.slice(os.startLine, os.endLine)
|
|
3966
|
+
if (/<待填|<!--TODO/.test(oldSeg.join('\n'))) {
|
|
3967
|
+
out.push(...freshSeg) // 未填 → 换新骨架占位
|
|
3968
|
+
} else {
|
|
3969
|
+
out.push(...oldSeg) // 已填 → 原样保留
|
|
3970
|
+
carriedSections.push(k)
|
|
3971
|
+
}
|
|
3972
|
+
} else if (MATRIX_SECTION_KEYS.includes(k)) {
|
|
3973
|
+
const merged = mergeProbeSection(old.lines.slice(os.startLine, os.endLine), freshSeg)
|
|
3974
|
+
carriedRows += merged.carried
|
|
3975
|
+
out.push(...merged.text.split('\n'))
|
|
3976
|
+
if (merged.carried > 0) carriedSections.push(k)
|
|
3977
|
+
} else if (k === PROBE_CONTAINER_KEY) {
|
|
3978
|
+
const merged = mergeProbeContainer(old.lines.slice(os.startLine, os.endLine), freshSeg)
|
|
3979
|
+
carriedRows += merged.carriedRows
|
|
3980
|
+
out.push(...merged.text.split('\n'))
|
|
3981
|
+
if (merged.carriedRows > 0) carriedSections.push(k)
|
|
3982
|
+
} else {
|
|
3983
|
+
out.push(...freshSeg) // 机器段(探针概览/接口声明等)→ 新骨架
|
|
3984
|
+
}
|
|
3985
|
+
cursor = fs.endLine
|
|
3986
|
+
oldByKey.delete(k)
|
|
3987
|
+
}
|
|
3988
|
+
for (; cursor < fresh.lines.length; cursor++) out.push(fresh.lines[cursor])
|
|
3989
|
+
// 旧文独有段(用户自定义节)追加保留
|
|
3990
|
+
if (oldByKey.size > 0) {
|
|
3991
|
+
out.push('')
|
|
3992
|
+
for (const [, s] of oldByKey) {
|
|
3993
|
+
out.push(...old.lines.slice(s.startLine, s.endLine))
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
return { text: out.join('\n'), carriedSections, carriedRows }
|
|
3997
|
+
}
|
package/src/wt-commit.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*
|
|
18
18
|
* 成功返回 HEAD hash(review.json 的 head 锚点直接可用);无变更 → ok=true + skipped。
|
|
19
19
|
*/
|
|
20
|
-
import { join, resolve } from 'node:path'
|
|
20
|
+
import { join, resolve, relative, sep, basename, dirname } from 'node:path'
|
|
21
21
|
import { existsSync, readFileSync } from 'node:fs'
|
|
22
22
|
import { withFileLock } from './quicklog.js'
|
|
23
23
|
import { safeGit } from './git-helper.js'
|
|
@@ -55,6 +55,48 @@ async function gitWithLockRetry(cwd, args, label, { retries = 6, waitMs = 1000 }
|
|
|
55
55
|
* @returns {Promise<{ok:boolean,skipped:boolean,head:string,shortHead:string,worktreePath:string,changeName:string,files:string[]}>}
|
|
56
56
|
* ok=false 时抛错(调用方打错误 + exit 1),skipped=无变更可提交(HEAD 不动,不算失败)。
|
|
57
57
|
*/
|
|
58
|
+
/**
|
|
59
|
+
* 从 cwd 推断变更名(wt-commit 免 --change 通道;FR-08 / D-006@v1)。
|
|
60
|
+
* 跨仓 worktree 名 <change>--<repoKey>——仅当后缀命中 repos 注册表(worktrees 上两级
|
|
61
|
+
* local.yaml)且剥出候选是已知变更目录才剥(不猜切分);全段命中主仓已知变更不剥(防
|
|
62
|
+
* 变更真名恰以 --repoKey 结尾);均不中 → 原样透传(现状行为,调用方自然报错引导
|
|
63
|
+
* 显式 --change)。导出供三态回归直测(index.js dispatch 内联形态不可测——独立审查指出的
|
|
64
|
+
* 验证面缺口)。
|
|
65
|
+
*/
|
|
66
|
+
export async function inferChangeFromWorktreeCwd(dir) {
|
|
67
|
+
// 深路径归一(FR-08):cwd 可能在 worktree 内多层深处——specBase 推导须从 worktree 根起算。
|
|
68
|
+
// 找 'worktrees' 目录段在原路径中的位置,根 = 该段后第一段截尾;specBaseGuess = 根上溯
|
|
69
|
+
// 三级(worktrees → .runtime → .sillyspec 的父,即主仓 specBase)。
|
|
70
|
+
const s = String(dir)
|
|
71
|
+
const norm = s.split('\\').join('/')
|
|
72
|
+
const wtIdx = norm.lastIndexOf('/worktrees/')
|
|
73
|
+
let seg = null
|
|
74
|
+
let specBaseGuess = null
|
|
75
|
+
if (wtIdx !== -1) {
|
|
76
|
+
const rest = norm.slice(wtIdx + '/worktrees/'.length)
|
|
77
|
+
const rootRel = rest.split('/')[0]
|
|
78
|
+
seg = rootRel || null
|
|
79
|
+
const rootNorm = norm.slice(0, wtIdx + '/worktrees/'.length + rootRel.length)
|
|
80
|
+
specBaseGuess = rootNorm.split('/').slice(0, -3).join('/')
|
|
81
|
+
}
|
|
82
|
+
if (seg && seg.includes('--')) {
|
|
83
|
+
try {
|
|
84
|
+
if (specBaseGuess) {
|
|
85
|
+
const knownWhole = existsSync(join(specBaseGuess, 'changes', seg))
|
|
86
|
+
const dash = seg.lastIndexOf('--')
|
|
87
|
+
const cand = seg.slice(0, dash)
|
|
88
|
+
const suffix = seg.slice(dash + 2)
|
|
89
|
+
if (!knownWhole && cand && /^[A-Za-z0-9_.-]+$/.test(suffix)) {
|
|
90
|
+
const { parseRepoRegistry } = await import('./stages/plan-postcheck.js')
|
|
91
|
+
const reg = parseRepoRegistry(readFileSync(join(specBaseGuess, 'local.yaml'), 'utf8'))
|
|
92
|
+
if (reg.has(suffix) && existsSync(join(specBaseGuess, 'changes', cand))) seg = cand
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} catch { /* 注册表不可读 → 原样透传(fail-safe) */ }
|
|
96
|
+
}
|
|
97
|
+
return seg
|
|
98
|
+
}
|
|
99
|
+
|
|
58
100
|
export async function runWtCommit({ changeName, message, pathspecs = [], pathspecFile = null, cwd = process.cwd() }) {
|
|
59
101
|
if (!changeName) throw new Error('缺少 --change <变更名>(worktree 名;在 worktree 内运行时可省略,CLI 从 cwd 推断)')
|
|
60
102
|
if (!message || !message.trim()) throw new Error('缺少 -m/--message <提交信息>(建议 "<task-NN 摘要>")')
|
|
@@ -77,10 +119,47 @@ export async function runWtCommit({ changeName, message, pathspecs = [], pathspe
|
|
|
77
119
|
// 隔离模式(worktree 目录存在)→ 提交目标 = worktree 本体;in-place-fallback(meta 声明无
|
|
78
120
|
// worktree 直改主仓)→ 主仓根(与 MultiRepoContext 主仓 entry 的 worktreePath 兜底口径一致)。
|
|
79
121
|
// 顺序先看目录再看 meta:worktree 目录存在即隔离态,meta 缺失/损坏不改变目标(fail-safe)。
|
|
122
|
+
// FR-08(2026-10-09-verify-reuse-friction / D-006@v1):cwd 已在本变更的 worktree 内 →
|
|
123
|
+
// 提交目标=该 worktree 本体。跨仓 worktree 名 <change>--<repoKey> 不在 getWorktreePath
|
|
124
|
+
// 的主仓命名域(原路径必落「worktree 不存在」错);主仓 worktree 场景与原值同目录零漂移。
|
|
125
|
+
// 归属校验(不猜):worktree 根段 === changeName,或 === changeName--<注册repoKey>(repos
|
|
126
|
+
// 注册表读自 worktreeBase 上两级的 local.yaml;注册表不可读 → 只认精确名,fail-safe)。
|
|
80
127
|
let target = null
|
|
81
|
-
|
|
128
|
+
{
|
|
129
|
+
// 基座发现(目录事实):优先 wm.worktreeBase(主仓);cwd 不在其下时向上找名为 worktrees
|
|
130
|
+
// 的目录作基座(跨仓 worktree 是跨仓仓的 git worktree,WorktreeManager 从其 cwd 算不出
|
|
131
|
+
// 主仓基座——跨仓 worktree 的父目录本身就是 worktrees 基座)。
|
|
132
|
+
let base = resolve(wm.worktreeBase)
|
|
133
|
+
{
|
|
134
|
+
let d = resolve(cwd)
|
|
135
|
+
for (;;) {
|
|
136
|
+
if (dirname(d) === base) break
|
|
137
|
+
const parent = dirname(d)
|
|
138
|
+
if (basename(parent) === 'worktrees') { base = parent; break }
|
|
139
|
+
if (parent === d) break
|
|
140
|
+
d = parent
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const cwdAbs = resolve(cwd)
|
|
144
|
+
if (cwdAbs.startsWith(base + sep)) {
|
|
145
|
+
const wtRoot = relative(base, cwdAbs).split(sep)[0]
|
|
146
|
+
let own = wtRoot === changeName
|
|
147
|
+
if (!own && wtRoot.startsWith(changeName + '--')) {
|
|
148
|
+
const suffix = wtRoot.slice(changeName.length + 2)
|
|
149
|
+
if (suffix && /^[A-Za-z0-9_.-]+$/.test(suffix)) {
|
|
150
|
+
try {
|
|
151
|
+
const { parseRepoRegistry } = await import('./stages/plan-postcheck.js')
|
|
152
|
+
const reg = parseRepoRegistry(readFileSync(join(base, '..', '..', 'local.yaml'), 'utf8'))
|
|
153
|
+
own = reg.has(suffix)
|
|
154
|
+
} catch { own = false }
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (own && existsSync(join(base, wtRoot))) target = join(base, wtRoot)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (!target && existsSync(worktreePath)) {
|
|
82
161
|
target = worktreePath
|
|
83
|
-
} else {
|
|
162
|
+
} else if (!target) {
|
|
84
163
|
const meta = wm.getMeta(changeName)
|
|
85
164
|
if (meta && meta.mode === 'in-place-fallback') {
|
|
86
165
|
target = resolve(wm.worktreeBase, '..', '..', '..') // <main>/.sillyspec/.runtime/worktrees → <main>
|