sillyspec 3.32.10 → 3.33.0
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-brainstorm/SKILL.md +114 -114
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -222
- package/.claude/skills/sillyspec-explore/SKILL.md +107 -107
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.github/workflows/ci.yml +58 -0
- package/.husky/pre-push +7 -6
- package/eval/eval.config.json +3 -3
- package/eval/lib/config.mjs +6 -3
- package/eval/lib/util.mjs +16 -3
- package/eval/lib/workdir.mjs +4 -1
- package/eval/results/eval.db +0 -0
- package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T13-00-05-625Z-004-phase-stats-armB/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/prompt.txt +8 -0
- package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/prompt.txt +8 -0
- package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/stdout.txt +1 -0
- package/eval/results/workdirs/001-slugify__armA/package.json +6 -0
- package/eval/results/workdirs/001-slugify__armA/src/slugify.mjs +26 -0
- package/eval/results/workdirs/001-slugify__armB/package.json +6 -0
- package/eval/results/workdirs/001-slugify__armB/src/slugify.mjs +26 -0
- package/eval/results/workdirs/002-each-day__armA/package.json +6 -0
- package/eval/results/workdirs/002-each-day__armA/src/dates.mjs +30 -0
- package/eval/results/workdirs/002-each-day__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
- package/eval/results/workdirs/002-each-day__armB/package.json +6 -0
- package/eval/results/workdirs/002-each-day__armB/src/dates.mjs +4 -0
- package/eval/results/workdirs/003-word-report__armA/bin/report.mjs +36 -0
- package/eval/results/workdirs/003-word-report__armA/data/notes.txt +5 -0
- package/eval/results/workdirs/003-word-report__armA/package.json +6 -0
- package/eval/results/workdirs/003-word-report__armB/bin/report.mjs +36 -0
- package/eval/results/workdirs/003-word-report__armB/data/notes.txt +5 -0
- package/eval/results/workdirs/003-word-report__armB/package.json +6 -0
- package/eval/results/workdirs/004-phase-stats__armA/data/events.log +16 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/flow.md +44 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-archive.md +27 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-brainstorm.md +34 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-execute.md +30 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-plan.md +28 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-verify.md +30 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/status.md +28 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-archive/SKILL.md +87 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-auto/SKILL.md +54 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-commit/SKILL.md +67 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-continue/SKILL.md +17 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-doctor/SKILL.md +76 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-execute/SKILL.md +222 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-explore/SKILL.md +107 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-export/SKILL.md +57 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-flow/SKILL.md +62 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-init/SKILL.md +177 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-knowledge/SKILL.md +40 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-plan/SKILL.md +156 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-propose/SKILL.md +16 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-quick/SKILL.md +116 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-resume/SKILL.md +21 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-scan/SKILL.md +79 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-state/SKILL.md +25 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-status/SKILL.md +54 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-verify/SKILL.md +87 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-workspace/SKILL.md +33 -0
- package/eval/results/workdirs/004-phase-stats__armB/AGENTS.md +45 -0
- package/eval/results/workdirs/004-phase-stats__armB/CLAUDE.md +3 -0
- package/eval/results/workdirs/004-phase-stats__armB/data/events.log +16 -0
- package/eval/results/workdirs/run-001-slugify__armA/package.json +6 -0
- package/eval/results/workdirs/run-001-slugify__armA/src/slugify.mjs +26 -0
- package/eval/results/workdirs/run-001-slugify__armB/package.json +6 -0
- package/eval/results/workdirs/run-001-slugify__armB/src/slugify.mjs +26 -0
- package/eval/results/workdirs/run-002-each-day__armA/package.json +6 -0
- package/eval/results/workdirs/run-002-each-day__armA/src/dates.mjs +30 -0
- package/eval/results/workdirs/run-002-each-day__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
- package/eval/results/workdirs/run-002-each-day__armB/package.json +6 -0
- package/eval/results/workdirs/run-002-each-day__armB/src/dates.mjs +4 -0
- package/eval/results/workdirs/run-003-word-report__armA/bin/report.mjs +36 -0
- package/eval/results/workdirs/run-003-word-report__armA/data/notes.txt +5 -0
- package/eval/results/workdirs/run-003-word-report__armA/package.json +6 -0
- package/eval/results/workdirs/run-003-word-report__armB/bin/report.mjs +36 -0
- package/eval/results/workdirs/run-003-word-report__armB/data/notes.txt +5 -0
- package/eval/results/workdirs/run-003-word-report__armB/package.json +6 -0
- package/eval/results/workdirs/run-004-phase-stats__armA/bin/phase-stats.mjs +37 -0
- package/eval/results/workdirs/run-004-phase-stats__armA/data/events.log +16 -0
- package/eval/results/workdirs/run-004-phase-stats__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
- package/eval/results/workdirs/run-004-phase-stats__armB/data/events.log +16 -0
- package/eval/results/workdirs/run-005-orders-export__armA/bin/export-orders.mjs +57 -0
- package/eval/results/workdirs/run-005-orders-export__armA/data/.gitattributes +1 -0
- package/eval/results/workdirs/run-005-orders-export__armA/data/orders.csv +9 -0
- package/eval/results/workdirs/run-005-orders-export__armA/out/summary.json +1 -0
- package/eval/results/workdirs/run-005-orders-export__armB/bin/export-orders.mjs +57 -0
- package/eval/results/workdirs/run-005-orders-export__armB/data/.gitattributes +1 -0
- package/eval/results/workdirs/run-005-orders-export__armB/data/orders.csv +9 -0
- package/eval/results/workdirs/run-005-orders-export__armB/out/summary.json +1 -0
- package/eval/tasks/004-phase-stats/files/data/events.log +16 -0
- package/eval/tasks/004-phase-stats/solution/bin/phase-stats.mjs +37 -0
- package/eval/tasks/004-phase-stats/task.json +6 -0
- package/eval/tasks/004-phase-stats/verify.mjs +38 -0
- package/eval/tasks/005-orders-export/files/data/.gitattributes +1 -0
- package/eval/tasks/005-orders-export/files/data/orders.csv +9 -0
- package/eval/tasks/005-orders-export/solution/bin/export-orders.mjs +57 -0
- package/eval/tasks/005-orders-export/task.json +6 -0
- package/eval/tasks/005-orders-export/verify.mjs +28 -0
- package/package.json +1 -1
- package/quicklog/tmp-fix-review.mjs +21 -0
- package/src/ceremony-tier.js +2 -1
- package/src/decision-distill.js +29 -3
- package/src/decision-redomain.js +148 -0
- package/src/design-facts.js +3 -3
- package/src/docs-debt.js +3 -3
- package/src/flow-parity.js +10 -2
- package/src/flow.js +1 -1
- package/src/forensics.js +236 -0
- package/src/fr-index.js +2 -1
- package/src/index.js +34 -1
- package/src/knowledge-graph.js +3 -4
- package/src/module-impact.js +2 -5
- package/src/modules.js +31 -0
- package/src/run/gates.js +7 -14
- package/src/run/lint-ownership.js +37 -0
- package/src/run/prompt.js +40 -25
- package/src/run/verify-quality-scan.js +10 -1
- package/src/spec-sync.js +81 -2
- package/src/stage-review.js +2 -1
- package/src/stages/brainstorm.js +71 -20
- package/src/stages/plan-postcheck.js +13 -2
- package/src/stages/plan.js +19 -0
- package/src/test-bindings.js +32 -3
- package/src/ui-visual.js +119 -4
- package/src/verify-postcheck.js +117 -17
- package/src/worktree-apply.js +26 -2
package/src/test-bindings.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { createHash } from 'crypto'
|
|
20
20
|
import { existsSync, readFileSync, readdirSync } from 'fs'
|
|
21
|
-
import { join } from 'path'
|
|
21
|
+
import { dirname, join } from 'path'
|
|
22
22
|
import { writeAtomicSync } from './fs-atomic.js'
|
|
23
23
|
import { gitQuiet } from './git-helper.js'
|
|
24
24
|
import { quicklogSidecarPath } from './quicklog.js'
|
|
@@ -193,9 +193,38 @@ export function readChangeTrace(changeDir) {
|
|
|
193
193
|
} catch { return [] }
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
/** 写 trace(幂等:序列化字节不变跳写;原子:writeAtomicSync)
|
|
196
|
+
/** 写 trace(幂等:序列化字节不变跳写;原子:writeAtomicSync)
|
|
197
|
+
*
|
|
198
|
+
* 幽灵绑定过滤(2026-10-15-zcode-engine-appserver 归档期实证,跨仓事故驱动修复):
|
|
199
|
+
* machine 行的 tests 条目若在任一候选根(变更目录上溯项目根 / process.cwd())都不存在——
|
|
200
|
+
* 多为卡片文本误提的不存在路径(如 verify 命令笔误被 deps 推断/绑定扫描捕获)——丢弃该
|
|
201
|
+
* 条目并 console.warn 留痕;agent 行不动(修理工/用户绑定如实保留);行内条目全被滤光则
|
|
202
|
+
* 保留原行不滤(交由 apply/verify 的悬空闸门裁决,不在写侧静默删行)。
|
|
203
|
+
*/
|
|
197
204
|
export function writeChangeTrace(changeDir, changeName, rows) {
|
|
198
|
-
const
|
|
205
|
+
const projectRoots = (() => {
|
|
206
|
+
const cands = []
|
|
207
|
+
let d = changeDir
|
|
208
|
+
for (let i = 0; i < 3; i++) { d = dirname(d); cands.push(d.replace(/\\/g, '/')) }
|
|
209
|
+
cands.push(process.cwd().replace(/\\/g, '/'))
|
|
210
|
+
return [...new Set(cands)]
|
|
211
|
+
})()
|
|
212
|
+
const _existsSomewhere = (f) => projectRoots.some(root => existsSync(join(root, f)))
|
|
213
|
+
let _ghostDropped = []
|
|
214
|
+
const _filtered = rows.map(r => {
|
|
215
|
+
if (r.discovery === 'agent') return r
|
|
216
|
+
const files = (r.tests || []).map(t => testAnchorFile(t))
|
|
217
|
+
const ghosts = files.filter(f => !_existsSomewhere(f))
|
|
218
|
+
if (ghosts.length === 0) return r
|
|
219
|
+
const ghostKeys = new Set(ghosts)
|
|
220
|
+
_ghostDropped.push(...ghosts.map(g => `${r.row_id}: ${g}`))
|
|
221
|
+
const remaining = (r.tests || []).filter(t => !ghostKeys.has(testAnchorFile(t)))
|
|
222
|
+
return remaining.length > 0 ? { ...r, tests: remaining } : r
|
|
223
|
+
})
|
|
224
|
+
if (_ghostDropped.length > 0) {
|
|
225
|
+
console.warn('⚠️ [test-bindings] 幽灵绑定条目过滤:' + _ghostDropped.length + ' 个不存在的 tests 路径已从 machine 行丢弃(agent 行不动):\n ' + _ghostDropped.slice(0, 5).join('\n '))
|
|
226
|
+
}
|
|
227
|
+
const normalized = _filtered.map(normalizeRow)
|
|
199
228
|
const payload = JSON.stringify({
|
|
200
229
|
schemaVersion: 1, change: changeName, rows: normalized,
|
|
201
230
|
}, null, 2) + '\n'
|
package/src/ui-visual.js
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { existsSync, readFileSync } from 'node:fs'
|
|
19
19
|
import { join } from 'node:path'
|
|
20
|
+
import jsYaml from 'js-yaml'
|
|
21
|
+
import { parseRepoRegistry } from './stages/plan-postcheck.js'
|
|
20
22
|
|
|
21
23
|
export const UI_VISUAL_PROBE_HEADING = '#### 探针 12:UI 视觉证据(分级门)'
|
|
22
24
|
export const UI_EVIDENCE_FILENAME = 'visual-evidence.md'
|
|
@@ -48,8 +50,114 @@ export function detectUiTouchInPaths(paths) {
|
|
|
48
50
|
|
|
49
51
|
/* ── flow start 须知(仓中立;证据约定与探针同源)── */
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
/**
|
|
54
|
+
* local.yaml ui_prototype 段解析(2026-10-15-ui-proto-form-config,FR-01)。
|
|
55
|
+
* 每仓原型形态契约:form: real-code | single-html;豁免(single-html)须带 ruling{by,at,reason}
|
|
56
|
+
* 全三字段(用户裁决留痕——缺任一=登记不完整,消费方须警告);repos.<子仓键> 同构 override。
|
|
57
|
+
* fail-soft:段缺席/文件不可读/YAML 坏/值非法 → registered:false 零抛出(与 readUiVisualGate
|
|
58
|
+
* 同根同哲学)。诚实边界:工具只消费登记与注入指引,不校验原型质量。
|
|
59
|
+
*/
|
|
60
|
+
export function readUiPrototypeForm(specBase) {
|
|
61
|
+
const empty = { registered: false, form: null, ruling: null, rulingComplete: false, repos: {} }
|
|
62
|
+
if (!specBase) return empty
|
|
63
|
+
let raw
|
|
64
|
+
try {
|
|
65
|
+
raw = jsYaml.load(readFileSync(join(specBase, 'local.yaml'), 'utf8'))
|
|
66
|
+
} catch { return empty }
|
|
67
|
+
const section = raw && typeof raw === 'object' ? raw.ui_prototype : null
|
|
68
|
+
if (!section || typeof section !== 'object') return empty
|
|
69
|
+
const parseEntry = (e) => {
|
|
70
|
+
if (!e || typeof e !== 'object') return null
|
|
71
|
+
const form = e.form === 'real-code' || e.form === 'single-html' ? e.form : null
|
|
72
|
+
if (!form) return null
|
|
73
|
+
const r = e.ruling && typeof e.ruling === 'object' ? e.ruling : null
|
|
74
|
+
const rulingComplete = !!(r && r.by && r.at && r.reason)
|
|
75
|
+
return { form, ruling: r, rulingComplete }
|
|
76
|
+
}
|
|
77
|
+
const top = parseEntry(section)
|
|
78
|
+
const repos = {}
|
|
79
|
+
if (section.repos && typeof section.repos === 'object') {
|
|
80
|
+
for (const [key, val] of Object.entries(section.repos)) {
|
|
81
|
+
const entry = parseEntry(val)
|
|
82
|
+
if (entry) repos[key] = entry
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (!top) return { registered: false, form: null, ruling: null, rulingComplete: false, repos }
|
|
86
|
+
return { registered: true, ...top, repos }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 多仓 override 解析(FR-02):触达路径与 local.yaml repos 注册匹配——子仓若在
|
|
91
|
+
* ui_prototype.repos 有 override 则该形态优先;未命中走仓级兜底。返回形态条目数组(去重):
|
|
92
|
+
* 每项 { scope: '仓级'|'<子仓键>', form, ruling, rulingComplete, registered }。
|
|
93
|
+
* 匹配三通道(评审 P2 清偿:注入语料常为相对路径,纯绝对前缀会让 override 静默失效):
|
|
94
|
+
* ① 绝对前缀:归一(反斜杠→斜杠/去尾斜杠/大小写不敏感)后 startsWith 注册路径;
|
|
95
|
+
* ② 相对键前缀:路径以 `<repoKey>/` 开头(语料常见 `spdemo/src/...` 形态);
|
|
96
|
+
* ③ 相对基名前缀:路径以注册路径 basename + `/` 开头(键与目录名不同名时兜底)。
|
|
97
|
+
*/
|
|
98
|
+
export function resolveUiPrototypeFor(specBase, paths = []) {
|
|
99
|
+
const proto = readUiPrototypeForm(specBase)
|
|
100
|
+
const out = []
|
|
101
|
+
const seen = new Set()
|
|
102
|
+
const push = (entry) => {
|
|
103
|
+
const key = `${entry.form}|${entry.rulingComplete}`
|
|
104
|
+
if (seen.has(key)) return
|
|
105
|
+
seen.add(key)
|
|
106
|
+
out.push(entry)
|
|
107
|
+
}
|
|
108
|
+
if (paths.length > 0) {
|
|
109
|
+
let repoPaths = {}
|
|
110
|
+
try {
|
|
111
|
+
// 复用既有注册解析(plan-postcheck _parseRepoEntries 同语义出口:字符串条目=路径、
|
|
112
|
+
// 对象条目取 path 子键)——不走 js-yaml 裸读,防对象形态静默漏配。
|
|
113
|
+
repoPaths = Object.fromEntries(parseRepoRegistry(readFileSync(join(specBase, 'local.yaml'), 'utf8')))
|
|
114
|
+
} catch { /* 解析失败=零子仓命中,仓级兜底 */ }
|
|
115
|
+
const norm = (p) => String(p).replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase()
|
|
116
|
+
for (const [key, registeredPath] of Object.entries(repoPaths)) {
|
|
117
|
+
if (typeof registeredPath !== 'string' || !registeredPath) continue
|
|
118
|
+
const reg = norm(registeredPath)
|
|
119
|
+
const regBase = reg.split('/').pop()
|
|
120
|
+
const hit = paths.some((p) => {
|
|
121
|
+
const np = norm(p)
|
|
122
|
+
return np.startsWith(reg + '/') || np === reg
|
|
123
|
+
|| np.startsWith(key.toLowerCase() + '/')
|
|
124
|
+
|| np.startsWith(regBase + '/')
|
|
125
|
+
})
|
|
126
|
+
const override = proto.repos[key]
|
|
127
|
+
if (hit && override) push({ scope: key, registered: true, ...override })
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
push(proto.registered
|
|
131
|
+
? { scope: '仓级', registered: true, form: proto.form, ruling: proto.ruling, rulingComplete: proto.rulingComplete }
|
|
132
|
+
: { scope: '仓级', registered: false, form: null, ruling: null, rulingComplete: false })
|
|
133
|
+
return out
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 须知第 5 节:按形态条目组合「本仓原型形态」行(FR-03 四态;生态命令词禁入——neutrality 契约)。 */
|
|
137
|
+
function uiPrototypeFormLines(entries) {
|
|
138
|
+
const lines = [`📐 本仓原型形态(local.yaml ui_prototype 登记;工具只查声明在场不验质量):`]
|
|
139
|
+
let anyContent = false
|
|
140
|
+
for (const e of entries) {
|
|
141
|
+
const scope = e.scope === '仓级' ? '' : `【子仓 ${e.scope}】`
|
|
142
|
+
if (!e.registered) {
|
|
143
|
+
anyContent = true
|
|
144
|
+
lines.push(` ⚠️ ${scope}未登记原型形态——本次按阶梯执行:有真实前端工具链→定稿原型必须真码;查无真码管线→先单开一个轻量变更搭最小入口(管线建设与业务变更分离),或登记豁免(form: single-html + ruling 用户裁决留痕)。收口前把 local.yaml ui_prototype 登记补上(强提醒,不阻断)。`)
|
|
145
|
+
} else if (e.form === 'single-html' && !e.rulingComplete) {
|
|
146
|
+
anyContent = true
|
|
147
|
+
lines.push(` ⚠️ ${scope}豁免登记不完整(single-html 缺 ruling{by,at,reason} 某字段)——这是静默漂移入口:补齐用户裁决留痕(by/at/reason)后再依赖豁免;本次仍按阶梯执行。`)
|
|
148
|
+
} else if (e.form === 'single-html') {
|
|
149
|
+
anyContent = true
|
|
150
|
+
lines.push(` 📋 ${scope}豁免生效(single-html;裁决 by=${e.ruling.by} at=${e.ruling.at}):定稿原型=自包含 HTML + 真值 token(来源须声明:token 源文件/组件参照),反向翻译税为已裁决接受项。`)
|
|
151
|
+
} else {
|
|
152
|
+
anyContent = true
|
|
153
|
+
lines.push(` 🔒 ${scope}真码契约(real-code):定稿原型必须与实现同方言真码;查无管线先单开轻量变更搭最小入口(bootstrap 与业务变更分离)。`)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return anyContent ? lines : []
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function buildUiGuidanceLines(specBase = null, paths = []) {
|
|
160
|
+
const lines = [
|
|
53
161
|
`🎨 UI 变更执行须知(本变更触及前端页面或 UI——advisory 引导非门禁):`,
|
|
54
162
|
` 1. 定稿原型必须是可复跑的真码产物(与实现同方言、能被本项目工具链构建)——手绘单文件`,
|
|
55
163
|
` 仅限一次性粗选对比,不作为交付原型(2026-09-28 frontend-apple-style 实证:手绘定稿后每条意见都要反向翻译回真实代码);`,
|
|
@@ -62,6 +170,13 @@ export function buildUiGuidanceLines() {
|
|
|
62
170
|
` 必须停下找用户签字,裁决记录写进 ${UI_EVIDENCE_FILENAME}(含「用户裁决」字样段)——`,
|
|
63
171
|
` 静默降级会在收口被「UI 视觉证据」探针硬拦(error 门)。`,
|
|
64
172
|
]
|
|
173
|
+
if (specBase) {
|
|
174
|
+
try {
|
|
175
|
+
const entries = resolveUiPrototypeFor(specBase, paths)
|
|
176
|
+
lines.push('', ...uiPrototypeFormLines(entries))
|
|
177
|
+
} catch { /* 登记解析失败零打扰(fail-soft 同 readUiVisualGate) */ }
|
|
178
|
+
}
|
|
179
|
+
return lines
|
|
65
180
|
}
|
|
66
181
|
|
|
67
182
|
/* ── 分级探针 ── */
|
|
@@ -173,8 +288,8 @@ export function uiEvidenceExecuteAdvisory({ changeDir, specBase = null }) {
|
|
|
173
288
|
}
|
|
174
289
|
}
|
|
175
290
|
|
|
176
|
-
/** design 文件变更清单行的路径粗提(反引号/裸路径行——探针侧仅作 UI
|
|
177
|
-
function extractQuotedPaths(designText) {
|
|
291
|
+
/** design 文件变更清单行的路径粗提(反引号/裸路径行——探针侧仅作 UI 扩展名兜底命中,不追求全解;导出供 prompt.js 组子仓 override 匹配路径用)。 */
|
|
292
|
+
export function extractQuotedPaths(designText) {
|
|
178
293
|
if (!designText) return []
|
|
179
294
|
const paths = []
|
|
180
295
|
for (const m of designText.matchAll(/`([^`]+)`/g)) {
|
package/src/verify-postcheck.js
CHANGED
|
@@ -2821,6 +2821,31 @@ function inferFullTestCommand(projectRoot) {
|
|
|
2821
2821
|
* 相关回归)。组卷改配额制:每组优先面整跑,普通 import 依赖只填剩余席位(可为 0)——帽
|
|
2822
2822
|
* 只界普通依赖尾。批尺寸上界护栏由 TEST_TIMEOUT_MS 兜底(超时=失败 fail-closed 不变)。
|
|
2823
2823
|
*/
|
|
2824
|
+
/**
|
|
2825
|
+
* 批命令长度分块(2026-10-15 平台侧实证,跨仓事故驱动最小修复):Windows cmd 执行器
|
|
2826
|
+
* 命令行上限 ~8191 字符——大依赖面(广被引用的前端文件变更 → 依赖测试 100+ 个)拼单条
|
|
2827
|
+
* vitest/node --test 命令必炸「命令行太长」exit 1,且失败批进增量账本自我循环恒败
|
|
2828
|
+
* (实测:137 文件批反复重跑复败,verify 门禁无法收口)。按「命令前缀+文件参数拼接
|
|
2829
|
+
* 总长」分块:单块保持原名(零回归),多块命名 <base>:N(N 从 1 起);每块独立计账。
|
|
2830
|
+
*/
|
|
2831
|
+
function chunkByCommandLength(prefix, files, maxLen = 6000) {
|
|
2832
|
+
const chunks = []
|
|
2833
|
+
let cur = []
|
|
2834
|
+
let curLen = prefix.length
|
|
2835
|
+
for (const f of files) {
|
|
2836
|
+
const add = f.length + 1
|
|
2837
|
+
if (cur.length > 0 && curLen + add > maxLen) {
|
|
2838
|
+
chunks.push(cur)
|
|
2839
|
+
cur = []
|
|
2840
|
+
curLen = prefix.length
|
|
2841
|
+
}
|
|
2842
|
+
cur.push(f)
|
|
2843
|
+
curLen += add
|
|
2844
|
+
}
|
|
2845
|
+
if (cur.length > 0) chunks.push(cur)
|
|
2846
|
+
return chunks
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2824
2849
|
function buildDepsBatches({ deps, changedFiles = [], hits = [], cwd = null, priorityFiles = [] }) {
|
|
2825
2850
|
const norm = (p2) => String(p2).replace(/\\/g, '/')
|
|
2826
2851
|
const changedSet = new Set((changedFiles || []).map(norm))
|
|
@@ -2930,10 +2955,11 @@ function buildDepsBatches({ deps, changedFiles = [], hits = [], cwd = null, prio
|
|
|
2930
2955
|
const jsProjectRun = jsProject.filter(f => !E2E_DIR_RE.test(_norm(f)))
|
|
2931
2956
|
const e2eDroppedFiles = jsProject.filter(f => E2E_DIR_RE.test(_norm(f)))
|
|
2932
2957
|
let jsxRunner = null
|
|
2958
|
+
let jsxRunnerFromHits = false
|
|
2933
2959
|
for (const h of hits || []) {
|
|
2934
2960
|
const cmd = String(h.test || '')
|
|
2935
2961
|
const m = /^((?:cd\s+[^&|;]+&&\s*)*[^&|;]*?(?:vitest|jest))(?=\s|$)/.exec(cmd) || /(?:^|&&|;|\|\|)\s*([^&|;]*(?:vitest|jest)(?:\s|$))/.exec(cmd)
|
|
2936
|
-
if (m) { jsxRunner = m[1].trim(); break }
|
|
2962
|
+
if (m) { jsxRunner = m[1].trim(); jsxRunnerFromHits = true; break }
|
|
2937
2963
|
}
|
|
2938
2964
|
if (!jsxRunner && cwd && jsProjectRun.length > 0) {
|
|
2939
2965
|
const inferred = inferJsxRunner(cwd, jsProjectRun[0])
|
|
@@ -2958,24 +2984,59 @@ function buildDepsBatches({ deps, changedFiles = [], hits = [], cwd = null, prio
|
|
|
2958
2984
|
gi++
|
|
2959
2985
|
}
|
|
2960
2986
|
}
|
|
2961
|
-
if (jsNative.length > 0)
|
|
2987
|
+
if (jsNative.length > 0) {
|
|
2988
|
+
const jsPrefix = 'node --test --test-reporter=spec --test-reporter-destination=stderr --test-reporter=tap --test-reporter-destination=stdout '
|
|
2989
|
+
const jsChunks = chunkByCommandLength(jsPrefix, jsNative)
|
|
2990
|
+
jsChunks.forEach((chunk, i) => {
|
|
2991
|
+
const isMulti = jsChunks.length > 1
|
|
2992
|
+
batches.push({ name: isMulti ? `deps(auto-js:${i + 1}/${jsChunks.length})` : 'deps(auto-js)', short: 'js', command: jsPrefix + chunk.join(' '), count: chunk.length, prioCount: chunk.filter(f => prio(f) === 0).length, dropped: i === 0 ? (jsTest.length - jsRun.length) : 0, prioDropped: i === 0 ? jsQ.prioDropped : 0, tap: true, files: chunk })
|
|
2993
|
+
})
|
|
2994
|
+
}
|
|
2962
2995
|
if (jsMeta.length > 0) batches.push({ name: 'deps(auto-js-meta-skip)', short: 'js-meta-skip', command: null, count: jsMeta.length, dropped: 0, skip: true, files: jsMeta, reason: '套件编排器(run-tests.mjs=npm test 组卷入口)非单测文件——内嵌执行=递归全量套件(嵌套 runner 污染假败);全量门由收口实测/CI 的 npm test 承担' })
|
|
2963
2996
|
|
|
2964
2997
|
if (nonTestFiles.length > 0) batches.push({ name: 'deps(auto-nontest-skip)', short: 'nontest-skip', command: null, count: nonTestFiles.length, dropped: 0, skip: true, files: nonTestFiles, reason: `非测试形态文件 ${nonTestFiles.length} 个不进单测执行批(node --test 语义「文件即测试」/ vitest No test suite found / pytest 0 collected exit 5——直跑必假败;坑 verify-dynamic-subset-node-test-ts-source):${nonTestFiles.slice(0, 3).join('、')}${nonTestFiles.length > 3 ? ' 等' : ''}——FR 绑定面脏数据可 sillyspec tests --unbind --tests <路径> 清行` })
|
|
2965
2998
|
if (jsProjectRun.length > 0) {
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2999
|
+
// R20(2026-09-27-thin-display-fix 实证,用户授权修复):Windows cmd 执行器对含
|
|
3000
|
+
// 括号/方括号/空格的路径(src/app/(dashboard)/workspaces/[id]/...)不加引号会以
|
|
3001
|
+
// 「此时不应有 <。」语法错误炸整批——文件参数含特殊字符时逐个 wrap 双引号。
|
|
3002
|
+
const quoteWinPath = (f) => (/[()\[\]\s&^%,;!]/.test(f) ? `"${f}"` : f)
|
|
3003
|
+
// jsx 批按包根分组(2026-10-15-zcode-engine-appserver verify 实证,跨仓事故驱动修复):
|
|
3004
|
+
// 混合包根(frontend/ 的 tsx 测试 + sillyhub-daemon/ 的 ts 测试同批)共享单一 runner
|
|
3005
|
+
// 前缀时,非本根路径在 runner 的 cwd 下「No test files found」exit 1 恒败(分块修复
|
|
3006
|
+
// 后暴露:第二块全 daemon 路径仍跑 cd frontend)。与 pyGroups 同款:每文件按最近
|
|
3007
|
+
// package.json 推断自身 runner/根;显式命中命令(hits)保持整批单组旧语义。
|
|
3008
|
+
const jsxGroups = new Map() // key: 组命令前缀
|
|
3009
|
+
const jsxUngrouped = []
|
|
3010
|
+
for (const f of jsProjectRun) {
|
|
3011
|
+
// 命中命令(hits)→ 整批单组旧语义;全局推断(jsProjectRun[0])仅兜底,逐文件按自身包根推断优先。
|
|
3012
|
+
let command = null
|
|
3013
|
+
let dirKey = null
|
|
3014
|
+
if (!jsxRunnerFromHits && cwd) {
|
|
3015
|
+
const inferred = inferJsxRunner(cwd, f)
|
|
3016
|
+
if (inferred) { command = inferred.command; dirKey = inferred.dir || null }
|
|
3017
|
+
}
|
|
3018
|
+
if (!command) command = jsxRunner
|
|
3019
|
+
if (!command) { jsxUngrouped.push(f); continue }
|
|
3020
|
+
if (!jsxGroups.has(command)) jsxGroups.set(command, { files: [], dirKey })
|
|
3021
|
+
jsxGroups.get(command).files.push(f)
|
|
3022
|
+
}
|
|
3023
|
+
const multiGroup = jsxGroups.size > 1
|
|
3024
|
+
for (const [command, grp] of jsxGroups) {
|
|
3025
|
+
const isVitest = /vitest/.test(command)
|
|
3026
|
+
const jsxPrefix = `${command} ${isVitest ? 'run ' : ''}`
|
|
3027
|
+
const grpDir = grp.dirKey || /(?:^|\s)cd\s+(\S+)\s*&&/.exec(command)?.[1] || null
|
|
3028
|
+
const rebaseJsx = (f) => (grpDir && f.startsWith(grpDir + '/')) ? f.slice(grpDir.length + 1) : f
|
|
3029
|
+
const jsxArgs = grp.files.map(rebaseJsx).map(quoteWinPath)
|
|
3030
|
+
const jsxChunks = chunkByCommandLength(jsxPrefix, jsxArgs)
|
|
3031
|
+
const baseName = multiGroup ? `deps(auto-jsx:${grp.dirKey || 'root'})` : 'deps(auto-jsx)'
|
|
3032
|
+
jsxChunks.forEach((chunk, i) => {
|
|
3033
|
+
const isMulti = jsxChunks.length > 1
|
|
3034
|
+
batches.push({ name: isMulti ? `${baseName}:${i + 1}/${jsxChunks.length}` : baseName, short: 'jsx', command: jsxPrefix + chunk.join(' '), count: chunk.length, dropped: 0, files: chunk })
|
|
3035
|
+
})
|
|
3036
|
+
}
|
|
3037
|
+
if (jsxUngrouped.length > 0) {
|
|
3038
|
+
// 无运行器可推断的文件(含 !jsxRunner && !cwd 全量形态):skip 批如实披露(原整批 skip 语义按文件收窄)。
|
|
3039
|
+
batches.push({ name: multiGroup || jsxGroups.size > 0 ? 'deps(auto-jsx-skip:unresolved)' : 'deps(auto-jsx-skip)', short: 'jsx-skip', command: null, count: jsxUngrouped.length, dropped: 0, skip: true, files: jsxUngrouped, reason: `JSX 测试文件(tsx/jsx,${jsxUngrouped.length} 个)node 原生不可跑且各自最近 package.json 无 vitest/jest 运行器可推断——转项目运行器执行并如实披露,不制造恒败段(R18-SF-full 残差段 11 连败的坑)` })
|
|
2979
3040
|
}
|
|
2980
3041
|
}
|
|
2981
3042
|
if (e2eDroppedFiles.length > 0) {
|
|
@@ -3042,6 +3103,26 @@ function traceRepoRoots({ specBase, cwd }) {
|
|
|
3042
3103
|
}
|
|
3043
3104
|
|
|
3044
3105
|
/** 残差解析:active·非 superseded·锚命中的 trace 行 → { anchors, rows, files, dangling, perFile } */
|
|
3106
|
+
/**
|
|
3107
|
+
* 主仓侧候选解析根(worktree 感知;2026-10-15-zcode-engine-appserver verify 实证,跨仓
|
|
3108
|
+
* 事故驱动最小修复):变更在隔离 worktree 内实现时,新增测试文件在主仓根不可见——
|
|
3109
|
+
* 悬空判定对主仓文件按「主仓根 ∪ 变更 worktree 根」多根解析(任一存在即非悬空);
|
|
3110
|
+
* 残差执行根同款(文件在 worktree 存在则从 worktree 根跑)。meta 缺失/不可读 → 仅主仓。
|
|
3111
|
+
*/
|
|
3112
|
+
function traceMainRoots({ specBase, changeName, cwd }) {
|
|
3113
|
+
const roots = [cwd]
|
|
3114
|
+
try {
|
|
3115
|
+
const metaPath = join(specBase, '.runtime', 'worktrees', changeName, 'meta.json')
|
|
3116
|
+
if (existsSync(metaPath)) {
|
|
3117
|
+
const meta = JSON.parse(readFileSync(metaPath, 'utf8'))
|
|
3118
|
+
if (meta && typeof meta.worktreePath === 'string' && meta.worktreePath && existsSync(meta.worktreePath) && meta.worktreePath !== cwd) {
|
|
3119
|
+
roots.push(meta.worktreePath)
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
} catch { /* meta 不可读 → 仅主仓(零回归) */ }
|
|
3123
|
+
return roots
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3045
3126
|
export function resolveTraceResidual({ specBase, changeName, cwd }) {
|
|
3046
3127
|
const anchors = resolveVerifyAnchorSet({ specBase, changeName })
|
|
3047
3128
|
if (anchors.length === 0) return { anchors, rows: [], files: [], dangling: [], perFile: [] }
|
|
@@ -3050,6 +3131,7 @@ export function resolveTraceResidual({ specBase, changeName, cwd }) {
|
|
|
3050
3131
|
// tests 条目可携带用例锚(2026-09-26-binding-anchor-fidelity)——文件面/悬空判定走剥锚。
|
|
3051
3132
|
// FR-06(2026-10-09):文件按行 repo 归属配对(perFile),悬空判定/残差执行按行根解析。
|
|
3052
3133
|
const repoRoots = traceRepoRoots({ specBase, cwd })
|
|
3134
|
+
const mainRoots = traceMainRoots({ specBase, changeName, cwd })
|
|
3053
3135
|
const seen = new Set()
|
|
3054
3136
|
const perFile = []
|
|
3055
3137
|
for (const r of rows) {
|
|
@@ -3061,8 +3143,13 @@ export function resolveTraceResidual({ specBase, changeName, cwd }) {
|
|
|
3061
3143
|
}
|
|
3062
3144
|
}
|
|
3063
3145
|
const files = [...new Set(perFile.map(x => x.file))].sort()
|
|
3146
|
+
// 主仓文件多根解析:解析到 worktree 根则记录 resolvedRoot(残差执行同款消费)。
|
|
3147
|
+
for (const x of perFile) {
|
|
3148
|
+
if (x.repo) { x.resolvedRoot = repoRoots.get(x.repo) || cwd; continue }
|
|
3149
|
+
x.resolvedRoot = mainRoots.find(root => existsSync(resolve(root, x.file))) || cwd
|
|
3150
|
+
}
|
|
3064
3151
|
const dangling = perFile
|
|
3065
|
-
.filter(x => !existsSync(resolve(
|
|
3152
|
+
.filter(x => !existsSync(resolve(x.resolvedRoot, x.file)))
|
|
3066
3153
|
.map(x => x.repo ? `${x.repo}:${x.file}` : x.file)
|
|
3067
3154
|
return { anchors, rows, files, dangling, perFile }
|
|
3068
3155
|
}
|
|
@@ -3141,7 +3228,8 @@ export function applyTraceResidual({ mainResult, action, cwd, specBase, changeNa
|
|
|
3141
3228
|
for (const x of (tr.perFile || [])) {
|
|
3142
3229
|
if (provable.has(String(x.file).replace(/\\/g, '/'))) continue
|
|
3143
3230
|
const _rk = x.repo || null
|
|
3144
|
-
|
|
3231
|
+
// worktree 感知(同悬空判定口径):主仓行按 resolvedRoot(主仓或变更 worktree 根)取执行根。
|
|
3232
|
+
const _root = (_rk && _repoRoots.get(_rk)) || x.resolvedRoot || cwd
|
|
3145
3233
|
const _gk = _rk + '' + _root
|
|
3146
3234
|
if (!_gmap.has(_gk)) _gmap.set(_gk, { repo: _rk, root: _root, files: [] })
|
|
3147
3235
|
_gmap.get(_gk).files.push(x.file)
|
|
@@ -3284,8 +3372,20 @@ export function runModuleSubset({ cwd, specBase, changeName, hits, knownFailures
|
|
|
3284
3372
|
// not ok 以测试文件为套件名(not ok N - test/b.test.mjs),用判账行集匹配批内文件名把
|
|
3285
3373
|
// 「批失败」收窄到「文件失败」;归因不出(非 TAP/行集缺文件名)保守全记批文件。
|
|
3286
3374
|
failedFiles: (() => {
|
|
3375
|
+
// 基建失败甄别(坑 verify-incremental-rerun-infra-batch-growth,2026-10-15 两变更实证:
|
|
3376
|
+
// Windows argv 超限/runner 启动失败类批失败无任何测试级失败行,重跑不可能转绿——计入
|
|
3377
|
+
// 前轮失败批只会让增量面随轮膨胀(141→159 自锁)。判据:输出无 not ok/FAILED/ERROR/
|
|
3378
|
+
// AssertionError 任何测试级痕迹,且 reason 命中基建关键词(命令行太长/E2BIG/spawn/
|
|
3379
|
+
// Cannot find module/No test files found)——此类批不进失败批账本,loud 留痕供人工。
|
|
3380
|
+
const INFRA_REASON_RE = /命令行太长|E2BIG|spawn|Cannot find module|No test files found|ENAMETOOLONG/i
|
|
3381
|
+
const TEST_FAILURE_RE = /not ok \d|FAILED|ERROR|AssertionError|assert/i
|
|
3287
3382
|
const out = new Set()
|
|
3288
3383
|
for (const r of perModule.filter(x => x.status === 'failed')) {
|
|
3384
|
+
const text = String(r.outputFull || r.outputTail || '')
|
|
3385
|
+
if (!TEST_FAILURE_RE.test(text) && INFRA_REASON_RE.test(String(r.reason || '') + text)) {
|
|
3386
|
+
console.warn(`⚠️ 批 ${r.name} 判定基建失败(${String(r.reason || 'runner 层失败').slice(0, 80)})——不进增量失败批账本(重跑不可能转绿),修复基建后整轮重测`)
|
|
3387
|
+
continue
|
|
3388
|
+
}
|
|
3289
3389
|
for (const f of attributeFailedFiles(r)) out.add(f)
|
|
3290
3390
|
}
|
|
3291
3391
|
return [...out].sort()
|
package/src/worktree-apply.js
CHANGED
|
@@ -2110,10 +2110,13 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
2110
2110
|
if (rollback.error) result.warnings = (result.warnings || []).concat([`回滚警告: ${rollback.error}`]);
|
|
2111
2111
|
return result;
|
|
2112
2112
|
}
|
|
2113
|
+
const _leftoverNote = Array.isArray(rollback.leftovers) && rollback.leftovers.length > 0
|
|
2114
|
+
? `\n⚠️ 回滚完整性自证:${rollback.leftovers.length} 个文件二次还原后仍有残留(${rollback.leftovers.join(', ')})——工作区并非完全回滚前状态,后续操作请先核对 git status。`
|
|
2115
|
+
: '';
|
|
2113
2116
|
result.errors.push(
|
|
2114
2117
|
`apply --3way 冲突:以下文件与主干「已提交」推进重叠,无法自动合并:\n` +
|
|
2115
2118
|
` ${rollback.conflicts.length > 0 ? rollback.conflicts.join('\n ') : '(未能获取冲突文件列表)'}\n` +
|
|
2116
|
-
`已回滚工作区到 apply
|
|
2119
|
+
`已回滚工作区到 apply 前状态(无半成品冲突标记)。${_leftoverNote}\n` +
|
|
2117
2120
|
`可选:用 --merge 自动三方合并兜底(git merge sillyspec/${changeName},会引入合并提交):\n` +
|
|
2118
2121
|
` sillyspec worktree apply ${changeName} --merge\n` +
|
|
2119
2122
|
`或手动解决后重试。`
|
|
@@ -2395,7 +2398,28 @@ function rollbackApply(projectRoot, trackedFiles, newFiles, rawStderr = '') {
|
|
|
2395
2398
|
}
|
|
2396
2399
|
// 兜底:若 index 处于 unmerged 状态,重置 index(不影响工作区已还原的文件)
|
|
2397
2400
|
try { gitQuiet(projectRoot, ['reset', '--quiet']); } catch {}
|
|
2398
|
-
|
|
2401
|
+
// 回滚完整性自证(2026-10-15-zcode-engine-appserver apply 实证:回滚声明后仍有 3 文件残留
|
|
2402
|
+
// dirty——「已回滚」措辞与工作区实态不符,残段会以「未 commit 变更」形态阻断后续 apply)。
|
|
2403
|
+
// patch 涉及文件逐个复核 status;残留则二次还原(checkout HEAD / 删新建),仍残留如实降级措辞。
|
|
2404
|
+
let leftover = [];
|
|
2405
|
+
try {
|
|
2406
|
+
const st = gitQuiet(projectRoot, ['status', '--porcelain', '--', ...patchFiles]) || '';
|
|
2407
|
+
leftover = st.split('\n').map(l => l.trim()).filter(Boolean).map(l => l.slice(3).replace(/^"|"$/g, '').replace(/\\/g, '/'));
|
|
2408
|
+
} catch { /* status 失败 → 不追加验证(保持旧行为) */ }
|
|
2409
|
+
if (leftover.length > 0) {
|
|
2410
|
+
for (const f of leftover) {
|
|
2411
|
+
try {
|
|
2412
|
+
if (trackedFiles.includes(f)) gitQuiet(projectRoot, ['checkout', 'HEAD', '--', f]);
|
|
2413
|
+
else { const p2 = join(projectRoot, f); if (existsSync(p2)) unlinkSync(p2); }
|
|
2414
|
+
} catch (e2) { error = (error ? error + '; ' : '') + `二次还原 ${f}: ${e2.message}`; }
|
|
2415
|
+
}
|
|
2416
|
+
// 复核
|
|
2417
|
+
try {
|
|
2418
|
+
const st2 = gitQuiet(projectRoot, ['status', '--porcelain', '--', ...patchFiles]) || '';
|
|
2419
|
+
leftover = st2.split('\n').map(l => l.trim()).filter(Boolean).map(l => l.slice(3).replace(/^"|"$/g, '').replace(/\\/g, '/'));
|
|
2420
|
+
} catch { /* 保持 leftovers */ }
|
|
2421
|
+
}
|
|
2422
|
+
return { conflicts, error, leftovers: leftover };
|
|
2399
2423
|
}
|
|
2400
2424
|
|
|
2401
2425
|
/**
|