sillyspec 3.33.3 → 3.33.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/sillyspec-config/SKILL.md +48 -0
- package/.claude/skills/sillyspec-flow/SKILL.md +4 -0
- package/AGENTS.md +1 -1
- package/package.json +1 -1
- package/src/config-known-failures.js +258 -0
- package/src/config-schema.js +28 -0
- package/src/config-validate.js +180 -0
- package/src/decision-distill.js +26 -5
- package/src/decision-redomain.js +5 -4
- package/src/docs-check.js +8 -4
- package/src/doctor-diagnostics.js +89 -6
- package/src/flow-parity.js +22 -2
- package/src/flow-preflight.js +67 -0
- package/src/flow.js +252 -55
- package/src/fr-index.js +7 -3
- package/src/git-helper.js +82 -0
- package/src/handoff.js +4 -0
- package/src/hooks/worktree-guard.js +12 -3
- package/src/hot-cache.js +60 -0
- package/src/index.js +111 -7
- package/src/knowledge-graph.js +82 -2
- package/src/knowledge-match.js +18 -2
- package/src/knowledge-quicklog.js +17 -2
- package/src/knowledge-vector.js +5 -3
- package/src/machine-interface.js +34 -13
- package/src/progress/change-registry.js +15 -0
- package/src/progress/shared.js +4 -0
- package/src/progress/stage-machine.js +15 -2
- package/src/progress.js +5 -0
- package/src/redomain-unmapped.js +204 -0
- package/src/redomain.js +5 -3
- package/src/review-dispatch.js +208 -47
- package/src/run/bg-sync.js +4 -0
- package/src/run/code-face-key.js +4 -1
- package/src/run/command.js +20 -5
- package/src/run/complete-handlers.js +38 -4
- package/src/run/complete.js +185 -14
- package/src/run/gates.js +30 -1
- package/src/run/prompt.js +9 -1
- package/src/run/quick-audit.js +4 -2
- package/src/run/shared.js +18 -1
- package/src/run/stage.js +22 -19
- package/src/run/verify-quality-scan.js +51 -0
- package/src/runtime-paths.js +3 -0
- package/src/sillyhub-mcp/client.js +11 -6
- package/src/spawn-settle.js +47 -0
- package/src/spec-sync.js +89 -3
- package/src/stages/knowledge.js +25 -3
- package/src/timing.js +55 -0
- package/src/verify-postcheck.js +153 -11
- package/src/watcher.js +4 -0
- package/src/worktree-apply.js +67 -4
- package/src/worktree.js +80 -11
- package/src/yaml-scalar-unquote.js +43 -0
- package/templates/agents-instruction.md +1 -1
package/src/verify-postcheck.js
CHANGED
|
@@ -28,6 +28,7 @@ import { createHash } from 'crypto'
|
|
|
28
28
|
import { IR_STRICT_SINCE } from './constants.js'
|
|
29
29
|
import { gitQuiet } from './git-helper.js'
|
|
30
30
|
import { resolveRuntimeRoot } from './run/shared.js'
|
|
31
|
+
import { unquoteYamlScalar } from './yaml-scalar-unquote.js'
|
|
31
32
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync, appendFileSync } from 'fs'
|
|
32
33
|
import { join, resolve, isAbsolute } from 'path'
|
|
33
34
|
import { readChangeTrace, testAnchorFile } from './test-bindings.js'
|
|
@@ -975,7 +976,10 @@ export function extractKnownFailures(yamlText) {
|
|
|
975
976
|
// 吃到最后一个 ](行尾注释里含 ] 的极端形态会被并入值,远优于静默清空)。
|
|
976
977
|
const inline = yaml.match(/^known_failures:\s*\[(.*)\]\s*(?:#.*)?$/m)
|
|
977
978
|
if (inline) {
|
|
978
|
-
|
|
979
|
+
// 条目规范化走 yaml-scalar-unquote 单点(2026-10-21-known-failures-unescape):双引号条目
|
|
980
|
+
// 反转义——config known-failures add 写侧按 YAML 规范转义 \\.,旧读侧只剥引号不反转义,
|
|
981
|
+
// 含反斜杠的锚定豁免模式带着字面双反斜杠进 buildExemptPats,正则语义漂移、豁免静默失配。
|
|
982
|
+
return inline[1].split(',').map(s => unquoteYamlScalar(s)).filter(Boolean)
|
|
979
983
|
}
|
|
980
984
|
// 坑 verify-known-failures-comment-line-truncation(2026-08-28 连续踩两次)→ 坑
|
|
981
985
|
// verify-known-failures-block-fragile-chain(2026-09-07 工具复盘升级修复):块式捕获曾要求
|
|
@@ -1002,10 +1006,12 @@ export function extractKnownFailures(yamlText) {
|
|
|
1002
1006
|
.map(s => {
|
|
1003
1007
|
const item = s.trim()
|
|
1004
1008
|
const full = item.match(/^(['"])(.*)\1$/)
|
|
1005
|
-
|
|
1009
|
+
// 引号值经 unquoteYamlScalar 反转义(2026-10-21-known-failures-unescape:与流式同口径,
|
|
1010
|
+
// 双引号 \\.→\. / ''→';裸值照旧只剥行尾注释)
|
|
1011
|
+
if (full) return unquoteYamlScalar(item)
|
|
1006
1012
|
const stripped = item.replace(/[ \t]+#.*$/, '').trim() // 行尾注释须 # 前有空白(YAML 语义),裸 # 不截
|
|
1007
1013
|
const q2 = stripped.match(/^(['"])(.*)\1$/)
|
|
1008
|
-
return q2 ?
|
|
1014
|
+
return q2 ? unquoteYamlScalar(stripped) : stripped
|
|
1009
1015
|
})
|
|
1010
1016
|
.filter(Boolean)
|
|
1011
1017
|
}
|
|
@@ -2156,6 +2162,7 @@ export function runVerifyTestCheck({ cwd, specBase, changeName = null, ctx = nul
|
|
|
2156
2162
|
// ① 本变更测试 ∪ ③ import 依赖(discoverModuleDependentTests);② FR 关联回归(fail-open)。
|
|
2157
2163
|
let depsAutoFiles = []
|
|
2158
2164
|
let frPre = null
|
|
2165
|
+
let frCapApplied = null
|
|
2159
2166
|
if (strategy === 'dynamic' && !gitUnavailable) {
|
|
2160
2167
|
try {
|
|
2161
2168
|
depsAutoFiles = discoverModuleDependentTests({ cwd, changedFiles: lastChangedFiles, coveredCommands: [] })
|
|
@@ -2163,6 +2170,17 @@ export function runVerifyTestCheck({ cwd, specBase, changeName = null, ctx = nul
|
|
|
2163
2170
|
try {
|
|
2164
2171
|
frPre = collectFrLinkedTests({ specBase, changeName, changedFiles: lastChangedFiles, projectRoot: cwd })
|
|
2165
2172
|
} catch { frPre = null }
|
|
2173
|
+
// 帽在 frPre 产生点一次成型(2026-10-23-fr-regress-default-off):决策空判/展示计数/执行
|
|
2174
|
+
// 组面全用帽后面——单一时点单一口径(v2 缺省 0=off:FR 层不进面即不触发 dynamic-subset
|
|
2175
|
+
// 空跑);capApplied 随行传执行层出披露行(total=真实绑定面,防二次施帽口径失真)。
|
|
2176
|
+
try {
|
|
2177
|
+
const capVal = readFrRegressCap(specBase)
|
|
2178
|
+
if (frPre && capVal !== null) {
|
|
2179
|
+
const capped = applyFrRegressCap(frPre, capVal)
|
|
2180
|
+
frPre = capped.fr
|
|
2181
|
+
frCapApplied = capped.capApplied
|
|
2182
|
+
}
|
|
2183
|
+
} catch { /* 键读取 fail-soft=缺省 off */ }
|
|
2166
2184
|
}
|
|
2167
2185
|
const scopeFiles = frPre ? [...new Set([...depsAutoFiles, ...frPre.files])] : [...new Set(depsAutoFiles)]
|
|
2168
2186
|
const action = decideVerifyTestAction({ strategy, scopeCount: scopeFiles.length, gitUnavailable })
|
|
@@ -2229,11 +2247,13 @@ export function runVerifyTestCheck({ cwd, specBase, changeName = null, ctx = nul
|
|
|
2229
2247
|
incremental = computeIncrementalFace({ ledger, filesSince, fullFace: lastChangedFiles })
|
|
2230
2248
|
}
|
|
2231
2249
|
}
|
|
2250
|
+
// frCount 展示口径随帽(2026-10-23-fr-regress-default-off):frPre 已在产生点施帽——
|
|
2251
|
+
// 展示计数/空判/执行同源(见上方帽一次成型注释)。
|
|
2232
2252
|
const frCount = frPre && frPre.files.length ? frPre.files.length : 0
|
|
2233
2253
|
if (incremental) {
|
|
2234
2254
|
subsetInput = incremental.files
|
|
2235
2255
|
console.log(`⚡ ${incremental.note}`)
|
|
2236
|
-
console.log(`ℹ️ 动态测试子集(增量重跑档):修复面三源重算(本变更测试 ∪ FR
|
|
2256
|
+
console.log(`ℹ️ 动态测试子集(增量重跑档):修复面三源重算(本变更测试 ∪ import 依赖;FR 关联回归层按 fr_regress_cap——缺省不跑归 pre-push/CI 门,2026-10-23-fr-regress-default-off),runner 自项目结构推断。恒全子集:local.yaml verify: test_rerun: full`)
|
|
2237
2257
|
mainResult = runModuleSubset({ cwd, specBase, changeName, hits: [], knownFailures, changedFiles: subsetInput, frPre: null })
|
|
2238
2258
|
if (mainResult && typeof mainResult === 'object') {
|
|
2239
2259
|
mainResult.mode = 'incremental-rerun'
|
|
@@ -2242,8 +2262,8 @@ export function runVerifyTestCheck({ cwd, specBase, changeName = null, ctx = nul
|
|
|
2242
2262
|
: `增量重跑通过:${incremental.note}`
|
|
2243
2263
|
}
|
|
2244
2264
|
} else {
|
|
2245
|
-
console.log(`ℹ️ 动态测试子集(缺省):本变更测试 ∪ FR 关联回归
|
|
2246
|
-
mainResult = runModuleSubset({ cwd, specBase, changeName, hits: [], knownFailures, changedFiles: lastChangedFiles, frPre, observability })
|
|
2265
|
+
console.log(`ℹ️ 动态测试子集(缺省):本变更测试 ∪ import 依赖${frCount ? ' ∪ FR 关联回归' : '(FR 关联回归层缺省不跑归 pre-push/CI 门——fr_regress_cap: N|all 可开启)'} = ${scopeFiles.length} 个(并集去重后的总数;分量 deps ${depsAutoFiles.length}${frCount ? ` + FR 绑定 ${frCount}` : ''},重叠只计一次);runner 自项目结构推断。显式 test_strategy: full 恢复全量`)
|
|
2266
|
+
mainResult = runModuleSubset({ cwd, specBase, changeName, hits: [], knownFailures, changedFiles: lastChangedFiles, frPre, frCapApplied, observability })
|
|
2247
2267
|
}
|
|
2248
2268
|
// ledger 记账(无论增/全):head=当前 HEAD(git 失败 null → 下轮自动回全子集);失败批文件
|
|
2249
2269
|
// 落账供下轮增量;passed 后 failedFiles 清空 → 下轮自然回全子集基线
|
|
@@ -2260,15 +2280,23 @@ export function runVerifyTestCheck({ cwd, specBase, changeName = null, ctx = nul
|
|
|
2260
2280
|
} catch { /* 记账失败不阻断(下轮退化全子集) */ }
|
|
2261
2281
|
} else {
|
|
2262
2282
|
// —— dynamic-empty:变更与测试面零关系——
|
|
2263
|
-
// 无自身测试、无 import
|
|
2264
|
-
//
|
|
2283
|
+
// 无自身测试、无 import 依赖测试(+FR 关联回归层:v2 缺省跳过或绑定面为空)→ 不硬跑
|
|
2284
|
+
// 全量(防超时/预存失败面,同旧 module 0 命中防误伤初衷);全量覆盖请显式 test_strategy: full。
|
|
2285
|
+
// FR 层被帽跳过(frCapApplied 在场 total>0)≠「无绑定」——reason 如实陈述并打披露行
|
|
2286
|
+
// (2026-10-23-fr-regress-default-off 评审 P1 修复:缺省跳过恒披露不静默)。
|
|
2287
|
+
if (frCapApplied && frCapApplied.total > 0) {
|
|
2288
|
+
console.log(`ℹ️ fr_regress_cap:FR 关联回归面 ${frCapApplied.kept}/${frCapApplied.total} 进实测面,其余 ${frCapApplied.total - frCapApplied.kept} 个归 pre-push/CI 门(可开启:local.yaml fr_regress_cap: <N|all>——N=前 N 个 / all=全跑旧行为;缺省不跑)`)
|
|
2289
|
+
}
|
|
2290
|
+
const frEmptyNote = frCapApplied && frCapApplied.total > 0
|
|
2291
|
+
? `FR 关联回归层被 fr_regress_cap 跳过(绑定 ${frCapApplied.total} 个未跑,归 pre-push/CI 门)`
|
|
2292
|
+
: '无 FR 关联绑定'
|
|
2265
2293
|
mainResult = {
|
|
2266
2294
|
status: 'skipped',
|
|
2267
2295
|
command: null,
|
|
2268
2296
|
exitCode: null,
|
|
2269
2297
|
durationMs: null,
|
|
2270
2298
|
outputTail: null,
|
|
2271
|
-
reason: `变更与测试面零关系(${lastChangedFiles.length} 个变更文件无自身测试、无 import
|
|
2299
|
+
reason: `变更与测试面零关系(${lastChangedFiles.length} 个变更文件无自身测试、无 import 依赖测试、${frEmptyNote})——动态子集空,不硬跑全量(防超时/预存失败面)。全量覆盖请显式 test_strategy: full。` + eaNote,
|
|
2272
2300
|
resultPath: null,
|
|
2273
2301
|
mode: 'dynamic-empty',
|
|
2274
2302
|
fallbackReason: null,
|
|
@@ -2809,6 +2837,64 @@ function inferFullTestCommand(projectRoot) {
|
|
|
2809
2837
|
return null
|
|
2810
2838
|
}
|
|
2811
2839
|
|
|
2840
|
+
/**
|
|
2841
|
+
* 运行器尾随 flag 保留(2026-10-21-deps-batch-runner-flags,dsh 0ff6f1c8 补充诊断取证)。
|
|
2842
|
+
* buildDepsBatches 的运行器提取正则「截到 pytest 一词为止」,模块命令(local.yaml modules
|
|
2843
|
+
* 命中)里用户显式配置的尾随 flag(-q --no-cov -n auto 等)被静默丢弃——大依赖批单核
|
|
2844
|
+
* 串行实测 1183s(同仓全量带 -n auto 仅 357s),超时=失败再叠整轮重跑,六轮累计空耗约 1h。
|
|
2845
|
+
*
|
|
2846
|
+
* 本函数从命令串中 runner 词之后、段边界(& | ; 或串尾)之前的 token 流里保留 flag:
|
|
2847
|
+
* - 裸 flag(-q/--no-cov)与含等号一体 flag(--cov-report=xml)整 token 保留;
|
|
2848
|
+
* - 已知吃值 flag(-n/--numprocesses/-k/-m/--maxfail/--timeout/--durations/--junitxml/-p/
|
|
2849
|
+
* --config/--reporter 等)连同其值保留;值 token 带未闭合引号时消费到引号闭合(-k "a and b");
|
|
2850
|
+
* - 位置参数(不以 - 开头且非吃值 flag 之值——原命令的文件/目录面)丢弃:批文件面由
|
|
2851
|
+
* buildDepsBatches 重定基供给,混入会跑面失控(目录参数把整个目录收进来,子集退化全量)。
|
|
2852
|
+
* 白名单外吃值 flag 的值按位置参数丢弃(如 --cov-report xml 丢 xml 留 --cov-report)——
|
|
2853
|
+
* 温和退化不炸批;漏网成本是性能/报告形态,不是假绿假红。
|
|
2854
|
+
* fail-open:runner 不在命令中/无 flag → ''(调用方拼接零变化)。
|
|
2855
|
+
* @returns {string} 保留 flag 串(含前导空格);无可保留返回 ''
|
|
2856
|
+
*/
|
|
2857
|
+
const RUNNER_VALUE_FLAGS = new Set([
|
|
2858
|
+
'-n', '--numprocesses', '-k', '-m', '--maxfail', '--timeout', '--durations',
|
|
2859
|
+
'--junitxml', '--junit-xml', '-p', '--rootdir', '--config', '--reporter', '--reporters',
|
|
2860
|
+
])
|
|
2861
|
+
export function extractRunnerTrailingFlags({ command, runner }) {
|
|
2862
|
+
if (!command || !runner) return ''
|
|
2863
|
+
const cmd = String(command)
|
|
2864
|
+
const runnerStr = String(runner)
|
|
2865
|
+
const runnerIdx = cmd.indexOf(runnerStr)
|
|
2866
|
+
if (runnerIdx === -1) return ''
|
|
2867
|
+
let after = cmd.slice(runnerIdx + runnerStr.length)
|
|
2868
|
+
const segEnd = after.search(/[&|;]/)
|
|
2869
|
+
if (segEnd !== -1) after = after.slice(0, segEnd)
|
|
2870
|
+
const tokens = after.trim().split(/\s+/).filter(Boolean)
|
|
2871
|
+
if (tokens.length === 0) return ''
|
|
2872
|
+
const kept = []
|
|
2873
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
2874
|
+
const t = tokens[i]
|
|
2875
|
+
if (!t.startsWith('-')) continue // 位置参数(原命令文件面)丢弃
|
|
2876
|
+
kept.push(t)
|
|
2877
|
+
const bare = t.replace(/^["']+|["']+$/g, '')
|
|
2878
|
+
if (bare.includes('=')) continue // 含等号一体值,无需吃下一 token
|
|
2879
|
+
if (!RUNNER_VALUE_FLAGS.has(bare) || i + 1 >= tokens.length) continue
|
|
2880
|
+
const value = tokens[i + 1]
|
|
2881
|
+
kept.push(value)
|
|
2882
|
+
i++
|
|
2883
|
+
// 值 token 引号未闭合(-k "fast and not slow")→ 消费到引号闭合(空格分隔的多 token 值)
|
|
2884
|
+
const openQ = (value.match(/["']/g) || []).length
|
|
2885
|
+
if (value.startsWith('"') || value.startsWith("'")) {
|
|
2886
|
+
let quotes = openQ
|
|
2887
|
+
while (quotes % 2 === 1 && i + 1 < tokens.length) {
|
|
2888
|
+
const rest = tokens[i + 1]
|
|
2889
|
+
kept.push(rest)
|
|
2890
|
+
quotes += (rest.match(/["']/g) || []).length
|
|
2891
|
+
i++
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
return kept.length ? ' ' + kept.join(' ') : ''
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2812
2898
|
/**
|
|
2813
2899
|
* deps(auto) 运行器推断 + 公平组卷(2026-09-24 R10 实证修复):
|
|
2814
2900
|
* - R10 缺陷①:deps 一律 `node --test` 执行——.py 依赖测试全 SyntaxError 伪败(只能靠豁免放行)。
|
|
@@ -2898,11 +2984,16 @@ function buildDepsBatches({ deps, changedFiles = [], hits = [], cwd = null, prio
|
|
|
2898
2984
|
// 整批 exit 2 假败)。每个 .py 文件按最近 pyproject/uv.lock 祖先归组(与 runner 推断
|
|
2899
2985
|
// 同款就近原则——monorepo 子项目各自独立环境);组内共用该包根的 runner 命令与重定基。
|
|
2900
2986
|
// 显式命中命令串(hits)优先且整批共用(用户显式配置语义优先,不拆组)。
|
|
2987
|
+
// 尾随 flag 保留(2026-10-21-deps-batch-runner-flags,dsh 0ff6f1c8 补充诊断取证):提取
|
|
2988
|
+
// 正则「截到 pytest 一词为止」曾把 local.yaml 模块命令显式配置的 -q --no-cov -n auto 静默
|
|
2989
|
+
// 丢弃——deps(auto-py) 批 124 文件(2081 例)单核串行 1183s,同仓全量 4771 例带 -n auto
|
|
2990
|
+
// 仅 357s(6-10 倍差),600s 超时截在 69% 判失败、六轮累计空耗约 1h。runner 提取后追加
|
|
2991
|
+
// extractRunnerTrailingFlags 保留 flag 串(位置参数仍丢弃——批文件面由重定基供给)。
|
|
2901
2992
|
let pyRunner = null
|
|
2902
2993
|
for (const h of hits || []) {
|
|
2903
2994
|
const cmd = String(h.test || '')
|
|
2904
2995
|
const m = /^((?:cd\s+[^&|;]+&&\s*)*[^&|;]*?pytest)/.exec(cmd) || /(?:^|&&|;|\|\|)\s*([^&|;]*pytest)/.exec(cmd)
|
|
2905
|
-
if (m) { pyRunner = m[1].trim(); break }
|
|
2996
|
+
if (m) { pyRunner = m[1].trim() + extractRunnerTrailingFlags({ command: cmd, runner: m[1] }); break }
|
|
2906
2997
|
}
|
|
2907
2998
|
// pyGroups: Map<dirKey, { files, command }>;dirKey null = 主仓根(无 cd 前缀)
|
|
2908
2999
|
const pyGroups = new Map()
|
|
@@ -3280,7 +3371,44 @@ export function applyTraceResidual({ mainResult, action, cwd, specBase, changeNa
|
|
|
3280
3371
|
* export 供直测(buildDepsBatches 同款先例——fixture 需真跑测试文件,不宜经
|
|
3281
3372
|
* runVerifyTestCheck 全链只为断言组卷接线)。
|
|
3282
3373
|
*/
|
|
3283
|
-
|
|
3374
|
+
/**
|
|
3375
|
+
* fr_regress_cap 读取 v2(2026-10-23-fr-regress-default-off,缺省翻转:用户裁决「默认就不跑,
|
|
3376
|
+
* 配置可以开启」):缺省(无键)/坏值/不可读 → 0(off——FR 关联回归层本地不跑,归 pre-push/CI
|
|
3377
|
+
* 门);off → 0;N(1-2000 正整数)→ 前 N 个;all → null(无帽=旧行为,显式恢复全跑)。
|
|
3378
|
+
* normalizeLineEndings CRLF 归一保留;轻量正则不引 yaml 依赖(extractTestCommand 同风格)。
|
|
3379
|
+
*/
|
|
3380
|
+
export function readFrRegressCap(specBase) {
|
|
3381
|
+
try {
|
|
3382
|
+
const raw = normalizeLineEndings(readFileSync(join(specBase, 'local.yaml'), 'utf8'))
|
|
3383
|
+
const m = raw.match(/^[ \t]*fr_regress_cap:[ \t]*([^\n#]+?)[ \t]*(?:#.*)?$/m)
|
|
3384
|
+
if (!m) return 0 // 缺省=off(v2 翻转)
|
|
3385
|
+
const v = m[1].trim().replace(/^["']|["']$/g, '')
|
|
3386
|
+
if (/^all$/i.test(v)) return null // 显式恢复旧行为(无帽整跑)
|
|
3387
|
+
if (/^off$/i.test(v)) return 0
|
|
3388
|
+
if (/^\d+$/.test(v)) {
|
|
3389
|
+
const n = Number(v)
|
|
3390
|
+
if (n >= 1 && n <= 2000) return n
|
|
3391
|
+
}
|
|
3392
|
+
return 0 // 坏值 fail-soft=新缺省 off
|
|
3393
|
+
} catch { return 0 } // 不可读 fail-soft=新缺省 off
|
|
3394
|
+
}
|
|
3395
|
+
|
|
3396
|
+
/**
|
|
3397
|
+
* FR 回归面帽纯函数(2026-10-23-fr-regress-cap):fr.files 超帽截前 N 个(现有稳定序);
|
|
3398
|
+
* cap=0(off)整层归零;null/undefined/帽内零截断 → 原对象零变化。返回 { fr, capApplied },
|
|
3399
|
+
* capApplied={kept,total}|null 供 loud 披露。下游(frLinked/frReport/优先面标记/批组卷)
|
|
3400
|
+
* 从帽后面对象自然推导,无第二套口径。
|
|
3401
|
+
*/
|
|
3402
|
+
export function applyFrRegressCap(fr, cap) {
|
|
3403
|
+
if (cap === null || cap === undefined || !fr || !Array.isArray(fr.files) || fr.files.length === 0) {
|
|
3404
|
+
return { fr, capApplied: null }
|
|
3405
|
+
}
|
|
3406
|
+
if (cap === 0) return { fr: { ...fr, files: [] }, capApplied: { kept: 0, total: fr.files.length } }
|
|
3407
|
+
if (fr.files.length <= cap) return { fr, capApplied: null }
|
|
3408
|
+
return { fr: { ...fr, files: fr.files.slice(0, cap) }, capApplied: { kept: cap, total: fr.files.length } }
|
|
3409
|
+
}
|
|
3410
|
+
|
|
3411
|
+
export function runModuleSubset({ cwd, specBase, changeName, hits, knownFailures = [], changedFiles = [], frPre = null, frCapApplied = null, observability = null }) {
|
|
3284
3412
|
const subsetStartedAt = Date.now()
|
|
3285
3413
|
const perModule = hits.map(h => runOneModule(h.name, h.test, cwd, knownFailures))
|
|
3286
3414
|
// 依赖测试伪模块(module-test-face-rot,2026-09-16 本会话实证):硬编码模块测试清单不含
|
|
@@ -3296,6 +3424,17 @@ export function runModuleSubset({ cwd, specBase, changeName, hits, knownFailures
|
|
|
3296
3424
|
let fr = frPre
|
|
3297
3425
|
if (!fr) {
|
|
3298
3426
|
try { fr = collectFrLinkedTests({ specBase, changeName, changedFiles: changedFiles || [], projectRoot: cwd }) } catch { fr = null }
|
|
3427
|
+
// 本地收集路径同样施帽(2026-10-23-fr-regress-default-off 单一口径:直调方/增量重跑未带
|
|
3428
|
+
// frPre——v2 缺省 0=off,FR 层本地不跑归 pre-push/CI 门;all=null 无帽旧行为)。frPre 由
|
|
3429
|
+
// 调用方在产生点施帽并随行带 frCapApplied(防二次施帽口径失真),本块不再重复施帽。
|
|
3430
|
+
try {
|
|
3431
|
+
const capVal = readFrRegressCap(specBase)
|
|
3432
|
+
if (fr && capVal !== null) {
|
|
3433
|
+
const capped = applyFrRegressCap(fr, capVal)
|
|
3434
|
+
fr = capped.fr
|
|
3435
|
+
frCapApplied = capped.capApplied
|
|
3436
|
+
}
|
|
3437
|
+
} catch { /* 键读取 fail-soft=缺省 off */ }
|
|
3299
3438
|
}
|
|
3300
3439
|
// 优先面全量口径(2026-10-06-fr-priority-overlap):frLinked 只含与 deps 无重叠的「新增」
|
|
3301
3440
|
// 绑定文件——重叠者此前拿不到优先权,仍按普通依赖受帽字母序弃置(resume-title 收口实测
|
|
@@ -3313,6 +3452,9 @@ export function runModuleSubset({ cwd, specBase, changeName, hits, knownFailures
|
|
|
3313
3452
|
console.warn(`⚠️ ${fr.unresolved.length} 个 FR 绑定测试路径无法自仓根解析(不入实测面):${fr.unresolved.slice(0, 3).join('、')}${fr.unresolved.length > 3 ? ' 等' : ''}——sillyspec tests repair-paths 可修`)
|
|
3314
3453
|
}
|
|
3315
3454
|
}
|
|
3455
|
+
if (frCapApplied) {
|
|
3456
|
+
console.log(`ℹ️ fr_regress_cap:FR 关联回归面 ${frCapApplied.kept}/${frCapApplied.total} 进实测面,其余 ${frCapApplied.total - frCapApplied.kept} 个归 pre-push/CI 门(可开启:local.yaml fr_regress_cap: <N|all>——N=前 N 个 / all=全跑旧行为;缺省不跑)`)
|
|
3457
|
+
}
|
|
3316
3458
|
const depsAll = [...new Set([...deps, ...frLinked])].sort()
|
|
3317
3459
|
let depsBatches = []
|
|
3318
3460
|
if (depsAll.length > 0) {
|
package/src/watcher.js
CHANGED
|
@@ -904,6 +904,10 @@ export async function spawnWatcher(cwd, changeName, opts = {}) {
|
|
|
904
904
|
env: childEnv,
|
|
905
905
|
});
|
|
906
906
|
child.unref?.();
|
|
907
|
+
// 孵化稳定化(FR-08 / dsh 实证 libuv 退出断言):等 spawn 事件落定(≤150ms 兜底)再放行,
|
|
908
|
+
// 压掉「父进程在子进程句柄建立中途退出」的崩溃竞态窗口。
|
|
909
|
+
const { awaitSpawnSettled } = await import('./spawn-settle.js');
|
|
910
|
+
await awaitSpawnSettled(child);
|
|
907
911
|
} finally {
|
|
908
912
|
if (logFd !== null) {
|
|
909
913
|
try { closeSync(logFd); } catch { /* 父侧 fd 副本关闭;子进程持有继承副本 */ }
|
package/src/worktree-apply.js
CHANGED
|
@@ -2260,8 +2260,45 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
2260
2260
|
return result;
|
|
2261
2261
|
} finally {
|
|
2262
2262
|
// stash 恢复(含 --merge 早退 / 各拦截 return / 异常路径——finally 全覆盖,绝不遗漏)
|
|
2263
|
-
if (stashInfo)
|
|
2263
|
+
if (stashInfo) {
|
|
2264
|
+
const restored = restoreMainStash(projectRoot, stashInfo);
|
|
2265
|
+
// 恢复步冲突标记检出(FR-02 / dsh z2 实证:apply 输出声称 clean,主仓 tsc 才暴露 stash 冲突
|
|
2266
|
+
// 标记)——机械扫描命中即 fail-closed:错误入 result.errors(apply exit 非零),文件清单与
|
|
2267
|
+
// 处置指引已在恢复处打印。finally 内改写 result 对仍在返回中的同一对象生效。
|
|
2268
|
+
if (restored.conflictFiles && restored.conflictFiles.length > 0) {
|
|
2269
|
+
result.conflictMarkerFiles = restored.conflictFiles;
|
|
2270
|
+
result.errors.push(
|
|
2271
|
+
`--stash-dirty 恢复后检出 git 冲突标记(${restored.conflictFiles.length} 个文件,stash 条目已保留未丢弃):\n ${restored.conflictFiles.join('\n ')}\n` +
|
|
2272
|
+
`逐文件解决冲突标记(<<<<<<< / ======= / >>>>>>>)后 git add,再手动 git stash drop;处置明细见上方恢复输出。`
|
|
2273
|
+
);
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
/** git 冲突标记行首三形态(FR-02 / dsh z2 实证:stash 恢复留 `<<<<<<< Updated upstream` 类标记静默落地,主仓 tsc 才暴露)。
|
|
2280
|
+
* 行首锚定 + `<{7} `/`>{7} ` 带标签(git 冲突标签恒随标记出现)、`={7}` 独占行——文档正文中的
|
|
2281
|
+
* 示例标记(缩进/行中)不触发;CRLF 先归一再匹配。 */
|
|
2282
|
+
const CONFLICT_MARKER_RE = /^<{7} .*$|^={7}$|^>{7} .*$/m;
|
|
2283
|
+
|
|
2284
|
+
/**
|
|
2285
|
+
* 机械扫描文件集的 git 冲突标记(FR-02)。
|
|
2286
|
+
* 只读;文件缺失/不可读跳过(stash 面里的已删文件不在扫描面)。限定调用方传入的文件集
|
|
2287
|
+
* (stash 面口径),不扫全仓——防把规范文档里描述冲突标记的示例文本误报。
|
|
2288
|
+
* @param {string} projectRoot
|
|
2289
|
+
* @param {string[]} files
|
|
2290
|
+
* @returns {string[]} 命中文件(相对 projectRoot 路径,原序)
|
|
2291
|
+
*/
|
|
2292
|
+
export function scanConflictMarkers(projectRoot, files) {
|
|
2293
|
+
const hits = [];
|
|
2294
|
+
for (const f of (Array.isArray(files) ? files : [])) {
|
|
2295
|
+
if (!f) continue;
|
|
2296
|
+
try {
|
|
2297
|
+
const text = readFileSync(join(projectRoot, f), 'utf8');
|
|
2298
|
+
if (CONFLICT_MARKER_RE.test(text.replace(/\r\n/g, '\n'))) hits.push(f);
|
|
2299
|
+
} catch { /* 已删/不可读——不在扫描面 */ }
|
|
2264
2300
|
}
|
|
2301
|
+
return hits;
|
|
2265
2302
|
}
|
|
2266
2303
|
|
|
2267
2304
|
/**
|
|
@@ -2273,16 +2310,28 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
2273
2310
|
* 形态,退出码不可尽信);
|
|
2274
2311
|
* - apply --index 冲突/异常 → stash 条目保留(git apply 失败不 drop),大字打印 SHA 兜底,
|
|
2275
2312
|
* 绝不自动丢弃、绝不盲 pop 栈顶(并行外部操作可能插队,先按 SHA 校验栈顶归属)。
|
|
2313
|
+
* - 恢复动作后机械扫描 stash 面冲突标记(FR-02 / dsh z2 实证:两级恢复的 --index 尝试可先落
|
|
2314
|
+
* 标记再失败、普通恢复可带着标记以 0 退出——git 退出码不可尽信):命中 → 不 drop、打印文件
|
|
2315
|
+
* 清单与处置指引、返回 conflictFiles(调用方 fail-closed 进 errors 使 apply exit 非零)。
|
|
2276
2316
|
* @param {string} projectRoot
|
|
2277
2317
|
* @param {{ sha: string, marker: string }} stashInfo
|
|
2318
|
+
* @returns {{ conflictFiles: string[] }}
|
|
2278
2319
|
*/
|
|
2279
|
-
function restoreMainStash(projectRoot, { sha, marker }) {
|
|
2320
|
+
export function restoreMainStash(projectRoot, { sha, marker }) {
|
|
2280
2321
|
// 栈顶归属校验按 SHA(git stash list 行不含 SHA,须 rev-parse 对比)——防并行外部操作插队后误恢复他者条目
|
|
2281
2322
|
const headSha = (gitQuiet(projectRoot, ['rev-parse', '--verify', 'stash@{0}']) || '').trim();
|
|
2282
2323
|
if (headSha !== sha) {
|
|
2283
2324
|
console.error(`⚠️ stash 恢复跳过:栈顶 stash@{0} 已非本次条目(本次 ${sha},栈顶 ${headSha || '空'})——可能被并行操作消费/弹出。若本次条目仍在栈中:git stash list 找 ${sha.slice(0, 8)} 后 git stash apply --index <该ref>`);
|
|
2284
|
-
return;
|
|
2325
|
+
return { conflictFiles: [] };
|
|
2285
2326
|
}
|
|
2327
|
+
// stash 面文件清单(冲突标记扫描口径):恢复动作前取(条目此刻必在栈顶)。
|
|
2328
|
+
// stash show 只列 tracked 面(--include-untracked 的第三 parent 不在内)——未跟踪文件
|
|
2329
|
+
// 恢复冲突走 already exists 报错路径(非标记形态),不在扫描面。
|
|
2330
|
+
let stashFace = [];
|
|
2331
|
+
try {
|
|
2332
|
+
stashFace = (gitQuiet(projectRoot, ['stash', 'show', '--name-only', 'stash@{0}']) || '')
|
|
2333
|
+
.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
2334
|
+
} catch { /* 面取失败 → 空面,恢复报错路径兜底 */ }
|
|
2286
2335
|
// 两级恢复:① apply --index(保暂存区状态)——但 apply 本身会把变更留在工作区未提交,stash 树是
|
|
2287
2336
|
// 全量快照,--index 的合并对「stash 树内但未被 stash 改动的脏文件」(正是刚 apply 落地的文件)
|
|
2288
2337
|
// 会报 local changes would be overwritten;② 退普通 apply(内容保真恢复,staged 扁平化为
|
|
@@ -2297,12 +2346,25 @@ function restoreMainStash(projectRoot, { sha, marker }) {
|
|
|
2297
2346
|
restored = 'plain';
|
|
2298
2347
|
console.warn(`⚠️ 暂存区状态未能完整还原(apply --index 与本次 apply 落地的未提交变更互斥),已退普通恢复——内容保真,staged 文件被扁平化为 unstaged,如需请重新 git add。原因:${String(eIndex.message || eIndex).split('\n').slice(-2)[0] || eIndex}`);
|
|
2299
2348
|
} catch (e) {
|
|
2349
|
+
// 恢复失败的现场也可能残留标记(--index 尝试先行落标记后抛错)——扫描补报,处置信息更完整
|
|
2350
|
+
const failConflicts = scanConflictMarkers(projectRoot, stashFace);
|
|
2300
2351
|
console.error(`⚠️ stash 恢复失败(apply 冲突或异常)——stash 条目已保留(未丢弃),SHA 兜底:${sha}`);
|
|
2301
2352
|
console.error(` 处理:git stash list 定位条目(marker 含 "${String(marker || '').slice(0, 50)}");解决工作区冲突后 git stash apply stash@{N} 重试或人工合并;确认无需后再 git stash drop。未跟踪文件若报 already exists,比对内容后手动处理。`);
|
|
2353
|
+
if (failConflicts.length > 0) {
|
|
2354
|
+
console.error(` 恢复失败但工作区已残留冲突标记(${failConflicts.length} 个文件):${failConflicts.join('、')}——逐文件解决标记后 git add,再 git stash apply stash@{0} 重试`);
|
|
2355
|
+
}
|
|
2302
2356
|
console.error(` git 输出: ${String(e.message || e).split('\n').slice(0, 3).join('\n ')}`);
|
|
2303
|
-
return;
|
|
2357
|
+
return { conflictFiles: failConflicts };
|
|
2304
2358
|
}
|
|
2305
2359
|
}
|
|
2360
|
+
// 恢复后机械扫描(FR-02):命中 → 不 drop、fail-closed(git 退出码 0 ≠ 无标记,dsh z2 实证)
|
|
2361
|
+
const conflictFiles = scanConflictMarkers(projectRoot, stashFace);
|
|
2362
|
+
if (conflictFiles.length > 0) {
|
|
2363
|
+
console.error(`🚫 stash 恢复后检出 git 冲突标记(${conflictFiles.length} 个文件)——stash 条目保留未丢弃(SHA 兜底:${sha}),本次 apply 记为失败:`);
|
|
2364
|
+
for (const f of conflictFiles) console.error(` - ${f}`);
|
|
2365
|
+
console.error(` 处置:逐文件解决冲突标记(<<<<<<< / ======= / >>>>>>> 段落二选一或手工合并)后 git add -- <文件>;或 git checkout -- <文件> 放弃恢复态后 git stash apply stash@{0} 重试;确认完成后手动 git stash drop stash@{0}。`);
|
|
2366
|
+
return { conflictFiles };
|
|
2367
|
+
}
|
|
2306
2368
|
try {
|
|
2307
2369
|
git(projectRoot, ['stash', 'drop', 'stash@{0}'], { timeout: 60000 });
|
|
2308
2370
|
// 防静默不落地(用户实证②):drop 后栈顶不得仍是本 SHA
|
|
@@ -2315,6 +2377,7 @@ function restoreMainStash(projectRoot, { sha, marker }) {
|
|
|
2315
2377
|
} catch (e) {
|
|
2316
2378
|
console.error(`⚠️ stash drop 失败(改动应已恢复)——手动 git stash drop stash@{0}(${sha}):${e.message}`);
|
|
2317
2379
|
}
|
|
2380
|
+
return { conflictFiles: [] };
|
|
2318
2381
|
}
|
|
2319
2382
|
|
|
2320
2383
|
/**
|
package/src/worktree.js
CHANGED
|
@@ -331,17 +331,42 @@ export function detectIsolation(cwd = process.cwd()) {
|
|
|
331
331
|
}
|
|
332
332
|
|
|
333
333
|
/**
|
|
334
|
-
* 检查 worktree 存储目录是否被 .gitignore
|
|
335
|
-
*
|
|
336
|
-
*
|
|
334
|
+
* 检查 worktree 存储目录是否被 .gitignore 忽略(2026-10-21-p0-p1-hardening FR-06 三态分流)。
|
|
335
|
+
*
|
|
336
|
+
* 三态结局(dsh z2 实证:沙箱下 spawnSync git 抛 EPERM 被旧实现当成「未忽略」误报,阻断
|
|
337
|
+
* worktree 创建 27 分钟并迫使用户提权——git 退出码与探测失败必须分流):
|
|
338
|
+
* - { ignored: true } git 判定已忽略(exit 0);
|
|
339
|
+
* - { ignored: false } git 判定确未忽略(check-ignore exit 1)——调用方照旧阻断;
|
|
340
|
+
* - { ignored: null, probeError } 探测失败(spawn 异常 EPERM/ENOENT/超时等,git 未给出
|
|
341
|
+
* 判定)——立即重试一次仍失败时按「无法证实未忽略」上报,调用方 fail-open 放行+警告。
|
|
342
|
+
* runner 可注入供测试(缺省本模块 git)。
|
|
343
|
+
* @param {string} cwd 项目根目录
|
|
344
|
+
* @param {Function} [runner] 注入式 git 执行器((cwd, args, opts) => string)
|
|
345
|
+
* @returns {{ ignored: boolean|null, path: string, probeError?: string, retried?: boolean }}
|
|
337
346
|
*/
|
|
338
|
-
function checkWorktreeDirIgnored(cwd = process.cwd()) {
|
|
347
|
+
export function checkWorktreeDirIgnored(cwd = process.cwd(), runner = git) {
|
|
339
348
|
const relPath = WORKTREES_REL;
|
|
349
|
+
const attempt = () => {
|
|
350
|
+
try {
|
|
351
|
+
runner(cwd, ['check-ignore', '-q', relPath]);
|
|
352
|
+
return { ignored: true };
|
|
353
|
+
} catch (e) {
|
|
354
|
+
// git 正常判定「未忽略」= exit 1(e.status 数字)——与 spawn 失败(e.status 缺失,
|
|
355
|
+
// e.code=EPERM/ENOENT/ETIMEDOUT 等)分流:前者是判定结果,后者是探测故障。
|
|
356
|
+
if (e && typeof e.status === 'number') return { ignored: false };
|
|
357
|
+
throw e;
|
|
358
|
+
}
|
|
359
|
+
};
|
|
340
360
|
try {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
361
|
+
return { ...attempt(), path: relPath };
|
|
362
|
+
} catch (first) {
|
|
363
|
+
try {
|
|
364
|
+
const r = attempt();
|
|
365
|
+
return { ...r, path: relPath, retried: true };
|
|
366
|
+
} catch (second) {
|
|
367
|
+
const probeError = String((second && (second.code || second.message)) || second).split('\n')[0];
|
|
368
|
+
return { ignored: null, path: relPath, probeError, retried: true };
|
|
369
|
+
}
|
|
345
370
|
}
|
|
346
371
|
}
|
|
347
372
|
|
|
@@ -669,6 +694,23 @@ export class WorktreeManager {
|
|
|
669
694
|
return parseJSON(readFileSync(metaPath, 'utf8'));
|
|
670
695
|
}
|
|
671
696
|
|
|
697
|
+
/**
|
|
698
|
+
* 确保 worktree 存在(幂等入口,2026-10-21-brainstorm-proto-worktree)
|
|
699
|
+
* 头脑风暴原型协议的复用语义收敛点:变更 key 已出后可先 create(或 native 复用),
|
|
700
|
+
* 后续 run execute 再走本入口——meta 已登记即复用,不重建不重新 overlay。
|
|
701
|
+
* 与 ensureCrossWorktrees 同款「命中即复用」可测先例。
|
|
702
|
+
* 返回完整 meta 透传(调用方如 ensureDepsFreshness 会整包 spread 写回,瘦身返回会丢字段)。
|
|
703
|
+
* @param {string} changeName - 变更名
|
|
704
|
+
* @param {{ base?: string, adoptBranch?: boolean }} opts - 同 create
|
|
705
|
+
* @returns {{ ...meta, existing: boolean }} 完整 meta + existing 标志(未登记走 create 时为 create 返回值)
|
|
706
|
+
* @throws {Error} create 失败时原样上抛(git 不可用、changeName 为空等)
|
|
707
|
+
*/
|
|
708
|
+
ensure(changeName, opts = {}) {
|
|
709
|
+
const existing = this.getMeta(changeName);
|
|
710
|
+
if (existing) return { ...existing, existing: true };
|
|
711
|
+
return { ...this.create(changeName, opts), existing: false };
|
|
712
|
+
}
|
|
713
|
+
|
|
672
714
|
/**
|
|
673
715
|
* 创建 worktree
|
|
674
716
|
* @param {string} changeName - 变更名
|
|
@@ -708,9 +750,10 @@ export class WorktreeManager {
|
|
|
708
750
|
});
|
|
709
751
|
}
|
|
710
752
|
|
|
711
|
-
// 1. 检查 worktree 目录是否被 gitignore
|
|
753
|
+
// 1. 检查 worktree 目录是否被 gitignore(三态:已忽略放行 / 确未忽略阻断 / 探测失败
|
|
754
|
+
// fail-open 放行+警告——FR-06 / dsh z2 实证 EPERM 误报「未忽略」阻断创建 27 分钟)
|
|
712
755
|
const ignoreStatus = checkWorktreeDirIgnored(this.cwd);
|
|
713
|
-
if (
|
|
756
|
+
if (ignoreStatus.ignored === false) {
|
|
714
757
|
throw new Error(
|
|
715
758
|
`worktree 存储目录 ${ignoreStatus.path} 未被 .gitignore 忽略,` +
|
|
716
759
|
`创建 worktree 可能导致内容被误提交。\n` +
|
|
@@ -718,6 +761,12 @@ export class WorktreeManager {
|
|
|
718
761
|
`或运行 sillyspec doctor 检查修复。`
|
|
719
762
|
);
|
|
720
763
|
}
|
|
764
|
+
if (ignoreStatus.ignored === null) {
|
|
765
|
+
console.warn(
|
|
766
|
+
`⚠️ worktrees gitignore 探测失败(${ignoreStatus.probeError}${ignoreStatus.retried ? ',已重试 1 次仍失败' : ''}——沙箱/杀软拦 git 子进程常见)——无法证实「未忽略」,按已忽略继续(不阻断创建)。` +
|
|
767
|
+
`人工复核:git check-ignore -q ${ignoreStatus.path}(exit 0=已忽略);若确实未忽略请补 .gitignore: ${ignoreStatus.path}/`
|
|
768
|
+
);
|
|
769
|
+
}
|
|
721
770
|
|
|
722
771
|
// 2. 检查 worktree 是否已存在
|
|
723
772
|
if (existsSync(worktreePath)) {
|
|
@@ -1306,8 +1355,28 @@ export class WorktreeManager {
|
|
|
1306
1355
|
// 探针二对象显式主仓根(_resolveMainRepoRoot)——native-worktree 复用路径下 cwd 是
|
|
1307
1356
|
// worktree、其 HEAD 即分支自身,按 cwd 探会「永不锚定」fail-open。
|
|
1308
1357
|
const mainRoot = this._resolveMainRepoRoot();
|
|
1358
|
+
// ── 锚集批量化(2026-10-21-protocol-tax task-07 / FR-07)──
|
|
1359
|
+
// 锚定判定 =「可达自 branch」∧「不可达自主仓 HEAD」,数学上等价于 hash ∈
|
|
1360
|
+
// rev-list(branch --not mainHEAD)(分支独有集,sillyspec/* 变更分支恒小)。旧实现逐
|
|
1361
|
+
// hash 双 merge-base 探针(Windows spawn 贵,doctor 实测 8.2s/21%);两 spawn 等价替换。
|
|
1362
|
+
// 等价性:is-ancestor(hash,branch) ⟺ hash ∈ rev-list(branch);探针二排除语义 ⟺ --not。
|
|
1363
|
+
// 任一 spawn 失败(null)→ anchorSet 置空回退旧逐对探针,fail-closed 语义原样保留。
|
|
1364
|
+
let anchorSet = null;
|
|
1365
|
+
try {
|
|
1366
|
+
const mainHead = gitQuiet(mainRoot, ['rev-parse', 'HEAD']);
|
|
1367
|
+
if (mainHead) {
|
|
1368
|
+
const unique = gitQuiet(this.cwd, ['rev-list', branch, '--not', mainHead]);
|
|
1369
|
+
if (unique !== null) anchorSet = new Set(unique ? unique.split('\n').filter(Boolean) : []);
|
|
1370
|
+
}
|
|
1371
|
+
} catch { /* 回退逐对探针 */ }
|
|
1309
1372
|
for (const [hash, paths] of candidates) {
|
|
1310
|
-
|
|
1373
|
+
if (anchorSet) {
|
|
1374
|
+
// 查锚集:在分支独有集内 → 锚定;否则不计入(含未知/畸形 hash——非真实对象无可
|
|
1375
|
+
// 悬空链,同旧探针一 null→continue 语义)
|
|
1376
|
+
if (anchorSet.has(hash)) refs.push(...paths);
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
// 探针一(语义不变,批量化失败回退路径):merge-base --is-ancestor 祖先或自身 → exit 0 输出空串(gitQuiet
|
|
1311
1380
|
// 返回 ''≠null);非祖先 → exit 1 → null;未知/畸形 hash → 报错非零 → null(不在分支
|
|
1312
1381
|
// 内,语义正确——非真实对象无可悬空链)
|
|
1313
1382
|
if (gitQuiet(this.cwd, ['merge-base', '--is-ancestor', hash, branch]) === null) continue;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* yaml-scalar-unquote —— local.yaml known_failures 条目的 YAML 标量规范化单点
|
|
3
|
+
* (2026-10-21-known-failures-unescape)。
|
|
4
|
+
*
|
|
5
|
+
* 背景(2026-10-21-verify-reuse-large-repo 收口实战):写侧(config-known-failures
|
|
6
|
+
* renderKey)按 YAML 规范把含反斜杠的锚定豁免模式转义成双引号形态,三个读侧
|
|
7
|
+
* (verify-postcheck extractKnownFailures / docs-check / config-known-failures
|
|
8
|
+
* parseKnownFailureKeys)各自拷贝的「剥引号」提取不反转义——模式带着字面双反斜杠
|
|
9
|
+
* 到达豁免匹配层,正则语义漂移为「字面反斜杠+任意字符」,豁免静默永不命中;CLI
|
|
10
|
+
* remove 同因必未命中。本模块把「条目级规范化」收敛为单点,三读侧统一 import。
|
|
11
|
+
*
|
|
12
|
+
* 零依赖叶子(三消费方刻意轻量化不引重链——js-yaml 整解析会动既有「断链容忍/
|
|
13
|
+
* 注释容忍」行为面,超出对称性修复);分段/注释容忍结构归各读侧既有实现。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 双引号标量内文反转义(YAML 双引号转义集;未知转义回落字面字符——键面无 \x 八进制等
|
|
18
|
+
* 罕见形态,宽松回落优于抛错)。传 inner(不含两侧引号)。
|
|
19
|
+
* @param {string} inner
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
22
|
+
export function unescapeYamlDoubleQuoted(inner) {
|
|
23
|
+
return String(inner ?? '').replace(/\\(.)/gs, (m, c) => {
|
|
24
|
+
if (c === 'n') return '\n'
|
|
25
|
+
if (c === 't') return '\t'
|
|
26
|
+
if (c === 'r') return '\r'
|
|
27
|
+
if (c === '0') return '\0'
|
|
28
|
+
return c // \\ → \、\" → "、\' → ';未知转义回落字面
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 条目标量规范化(token 可带/不带引号):双引号 → 反转义;单引号 → '' → '(YAML 单引号
|
|
34
|
+
* 唯一转义);裸值 → trim 原样。不剥行尾注释(归调用方既有结构)。
|
|
35
|
+
* @param {string} s
|
|
36
|
+
* @returns {string}
|
|
37
|
+
*/
|
|
38
|
+
export function unquoteYamlScalar(s) {
|
|
39
|
+
const t = String(s ?? '').trim()
|
|
40
|
+
if (t.length >= 2 && t.startsWith('"') && t.endsWith('"')) return unescapeYamlDoubleQuoted(t.slice(1, -1))
|
|
41
|
+
if (t.length >= 2 && t.startsWith("'") && t.endsWith("'")) return t.slice(1, -1).replace(/''/g, "'")
|
|
42
|
+
return t
|
|
43
|
+
}
|
|
@@ -32,7 +32,7 @@ sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句 10-30 字中文
|
|
|
32
32
|
1. **禁止绕过本文件规则和 SillySpec 流程**。所有变更走 sillyspec 流程,不裸改裸提交。
|
|
33
33
|
2. **改代码前必须先说明依据**——依据的文档路径(design.md / 模块文档 / file-lifecycle.md(如有))或现有代码依据,无依据不改。
|
|
34
34
|
3. **执行顺序**:文档 → 读代码 → 写测试 → 写实现 → 跑测试 → 验收 → 更新文档(轻量道即:FR 绑定+design 四节+用户批准先于动码,读代码探究不算动码)。测试按层适用:纯逻辑先写测试,行为契约断言用户可见行为,视觉样式与纯配置/文档不写测试(记「不适用:理由」,验收靠规范与人工)。
|
|
35
|
-
4. **实证核验再收口**:触及 `src`/`test` 的改动,CLI 会在收口时亲自实测(实测失败阻断收口;纯 doc/配置自动跳过)。测试面按变更动态推断(本变更测试 ∪ FR 关联回归 ∪ import 依赖,runner 自项目结构推断——无需配置);lint 走 `.sillyspec/local.yaml` 的 `commands.lint
|
|
35
|
+
4. **实证核验再收口**:触及 `src`/`test` 的改动,CLI 会在收口时亲自实测(实测失败阻断收口;纯 doc/配置自动跳过)。测试面按变更动态推断(本变更测试 ∪ FR 关联回归 ∪ import 依赖,runner 自项目结构推断——无需配置);lint 走 `.sillyspec/local.yaml` 的 `commands.lint`(未配置/标 `unavailable` → 收口跳过并留痕理由,归 pre-push/CI 门——pre-push 已跑同款 lint 的仓可标 unavailable 免重复)。以落盘文件与测试结果为准,不信口头"已完成"。自验口径=变更测试面(新增测试+受影响既有面)即收口证据(lint 已配置时一并实测;标 `unavailable` 的仓归 git 钩子/CI 门)——全量套件归 pre-push 门,勿每变更重复手跑(共享仓全量红先做归属判别,非本变更引入如实留痕不阻断);跑批输出落 `.sillyspec/.runtime/logs/` 日志回看尾部,勿整段吞掉。
|
|
36
36
|
5. **非测试逻辑本身有误时,禁止改测试来"通过"**——修逻辑,不修测试。
|
|
37
37
|
6. **git hook 拦截提交时禁止跳过**(如 `.husky/pre-push`),修复问题后再提交。
|
|
38
38
|
7. **代码必须兼容 Windows / Linux / macOS**(路径 / 换行 / 并发都要顾)。
|