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.
Files changed (147) hide show
  1. package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
  2. package/.claude/skills/sillyspec-execute/SKILL.md +222 -222
  3. package/.claude/skills/sillyspec-explore/SKILL.md +107 -107
  4. package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
  5. package/.github/workflows/ci.yml +58 -0
  6. package/.husky/pre-push +7 -6
  7. package/eval/eval.config.json +3 -3
  8. package/eval/lib/config.mjs +6 -3
  9. package/eval/lib/util.mjs +16 -3
  10. package/eval/lib/workdir.mjs +4 -1
  11. package/eval/results/eval.db +0 -0
  12. package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/prompt.txt +10 -0
  13. package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/stderr.txt +0 -0
  14. package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/stdout.txt +1 -0
  15. package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/prompt.txt +10 -0
  16. package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/stderr.txt +0 -0
  17. package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/stdout.txt +1 -0
  18. package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/prompt.txt +10 -0
  19. package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/stderr.txt +0 -0
  20. package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/stdout.txt +1 -0
  21. package/eval/results/logs/2026-10-15T13-00-05-625Z-004-phase-stats-armB/prompt.txt +10 -0
  22. package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/prompt.txt +10 -0
  23. package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/stderr.txt +0 -0
  24. package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/stdout.txt +1 -0
  25. package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/prompt.txt +10 -0
  26. package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/stderr.txt +0 -0
  27. package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/stdout.txt +1 -0
  28. package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/prompt.txt +10 -0
  29. package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/stderr.txt +0 -0
  30. package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/stdout.txt +1 -0
  31. package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/prompt.txt +8 -0
  32. package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/stderr.txt +0 -0
  33. package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/stdout.txt +1 -0
  34. package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/prompt.txt +8 -0
  35. package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/stderr.txt +0 -0
  36. package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/stdout.txt +1 -0
  37. package/eval/results/workdirs/001-slugify__armA/package.json +6 -0
  38. package/eval/results/workdirs/001-slugify__armA/src/slugify.mjs +26 -0
  39. package/eval/results/workdirs/001-slugify__armB/package.json +6 -0
  40. package/eval/results/workdirs/001-slugify__armB/src/slugify.mjs +26 -0
  41. package/eval/results/workdirs/002-each-day__armA/package.json +6 -0
  42. package/eval/results/workdirs/002-each-day__armA/src/dates.mjs +30 -0
  43. package/eval/results/workdirs/002-each-day__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
  44. package/eval/results/workdirs/002-each-day__armB/package.json +6 -0
  45. package/eval/results/workdirs/002-each-day__armB/src/dates.mjs +4 -0
  46. package/eval/results/workdirs/003-word-report__armA/bin/report.mjs +36 -0
  47. package/eval/results/workdirs/003-word-report__armA/data/notes.txt +5 -0
  48. package/eval/results/workdirs/003-word-report__armA/package.json +6 -0
  49. package/eval/results/workdirs/003-word-report__armB/bin/report.mjs +36 -0
  50. package/eval/results/workdirs/003-word-report__armB/data/notes.txt +5 -0
  51. package/eval/results/workdirs/003-word-report__armB/package.json +6 -0
  52. package/eval/results/workdirs/004-phase-stats__armA/data/events.log +16 -0
  53. package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/flow.md +44 -0
  54. package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-archive.md +27 -0
  55. package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-brainstorm.md +34 -0
  56. package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-execute.md +30 -0
  57. package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-plan.md +28 -0
  58. package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-verify.md +30 -0
  59. package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/status.md +28 -0
  60. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-archive/SKILL.md +87 -0
  61. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-auto/SKILL.md +54 -0
  62. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -0
  63. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-commit/SKILL.md +67 -0
  64. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-continue/SKILL.md +17 -0
  65. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-doctor/SKILL.md +76 -0
  66. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-execute/SKILL.md +222 -0
  67. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-explore/SKILL.md +107 -0
  68. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-export/SKILL.md +57 -0
  69. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-flow/SKILL.md +62 -0
  70. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-init/SKILL.md +177 -0
  71. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-knowledge/SKILL.md +40 -0
  72. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-plan/SKILL.md +156 -0
  73. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-propose/SKILL.md +16 -0
  74. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-quick/SKILL.md +116 -0
  75. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-resume/SKILL.md +21 -0
  76. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-scan/SKILL.md +79 -0
  77. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-state/SKILL.md +25 -0
  78. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-status/SKILL.md +54 -0
  79. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-verify/SKILL.md +87 -0
  80. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-workspace/SKILL.md +33 -0
  81. package/eval/results/workdirs/004-phase-stats__armB/AGENTS.md +45 -0
  82. package/eval/results/workdirs/004-phase-stats__armB/CLAUDE.md +3 -0
  83. package/eval/results/workdirs/004-phase-stats__armB/data/events.log +16 -0
  84. package/eval/results/workdirs/run-001-slugify__armA/package.json +6 -0
  85. package/eval/results/workdirs/run-001-slugify__armA/src/slugify.mjs +26 -0
  86. package/eval/results/workdirs/run-001-slugify__armB/package.json +6 -0
  87. package/eval/results/workdirs/run-001-slugify__armB/src/slugify.mjs +26 -0
  88. package/eval/results/workdirs/run-002-each-day__armA/package.json +6 -0
  89. package/eval/results/workdirs/run-002-each-day__armA/src/dates.mjs +30 -0
  90. package/eval/results/workdirs/run-002-each-day__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
  91. package/eval/results/workdirs/run-002-each-day__armB/package.json +6 -0
  92. package/eval/results/workdirs/run-002-each-day__armB/src/dates.mjs +4 -0
  93. package/eval/results/workdirs/run-003-word-report__armA/bin/report.mjs +36 -0
  94. package/eval/results/workdirs/run-003-word-report__armA/data/notes.txt +5 -0
  95. package/eval/results/workdirs/run-003-word-report__armA/package.json +6 -0
  96. package/eval/results/workdirs/run-003-word-report__armB/bin/report.mjs +36 -0
  97. package/eval/results/workdirs/run-003-word-report__armB/data/notes.txt +5 -0
  98. package/eval/results/workdirs/run-003-word-report__armB/package.json +6 -0
  99. package/eval/results/workdirs/run-004-phase-stats__armA/bin/phase-stats.mjs +37 -0
  100. package/eval/results/workdirs/run-004-phase-stats__armA/data/events.log +16 -0
  101. package/eval/results/workdirs/run-004-phase-stats__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
  102. package/eval/results/workdirs/run-004-phase-stats__armB/data/events.log +16 -0
  103. package/eval/results/workdirs/run-005-orders-export__armA/bin/export-orders.mjs +57 -0
  104. package/eval/results/workdirs/run-005-orders-export__armA/data/.gitattributes +1 -0
  105. package/eval/results/workdirs/run-005-orders-export__armA/data/orders.csv +9 -0
  106. package/eval/results/workdirs/run-005-orders-export__armA/out/summary.json +1 -0
  107. package/eval/results/workdirs/run-005-orders-export__armB/bin/export-orders.mjs +57 -0
  108. package/eval/results/workdirs/run-005-orders-export__armB/data/.gitattributes +1 -0
  109. package/eval/results/workdirs/run-005-orders-export__armB/data/orders.csv +9 -0
  110. package/eval/results/workdirs/run-005-orders-export__armB/out/summary.json +1 -0
  111. package/eval/tasks/004-phase-stats/files/data/events.log +16 -0
  112. package/eval/tasks/004-phase-stats/solution/bin/phase-stats.mjs +37 -0
  113. package/eval/tasks/004-phase-stats/task.json +6 -0
  114. package/eval/tasks/004-phase-stats/verify.mjs +38 -0
  115. package/eval/tasks/005-orders-export/files/data/.gitattributes +1 -0
  116. package/eval/tasks/005-orders-export/files/data/orders.csv +9 -0
  117. package/eval/tasks/005-orders-export/solution/bin/export-orders.mjs +57 -0
  118. package/eval/tasks/005-orders-export/task.json +6 -0
  119. package/eval/tasks/005-orders-export/verify.mjs +28 -0
  120. package/package.json +1 -1
  121. package/quicklog/tmp-fix-review.mjs +21 -0
  122. package/src/ceremony-tier.js +2 -1
  123. package/src/decision-distill.js +29 -3
  124. package/src/decision-redomain.js +148 -0
  125. package/src/design-facts.js +3 -3
  126. package/src/docs-debt.js +3 -3
  127. package/src/flow-parity.js +10 -2
  128. package/src/flow.js +1 -1
  129. package/src/forensics.js +236 -0
  130. package/src/fr-index.js +2 -1
  131. package/src/index.js +34 -1
  132. package/src/knowledge-graph.js +3 -4
  133. package/src/module-impact.js +2 -5
  134. package/src/modules.js +31 -0
  135. package/src/run/gates.js +7 -14
  136. package/src/run/lint-ownership.js +37 -0
  137. package/src/run/prompt.js +40 -25
  138. package/src/run/verify-quality-scan.js +10 -1
  139. package/src/spec-sync.js +81 -2
  140. package/src/stage-review.js +2 -1
  141. package/src/stages/brainstorm.js +71 -20
  142. package/src/stages/plan-postcheck.js +13 -2
  143. package/src/stages/plan.js +19 -0
  144. package/src/test-bindings.js +32 -3
  145. package/src/ui-visual.js +119 -4
  146. package/src/verify-postcheck.js +117 -17
  147. package/src/worktree-apply.js +26 -2
@@ -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 normalized = rows.map(normalizeRow)
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
- export function buildUiGuidanceLines() {
52
- return [
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)) {
@@ -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) batches.push({ name: 'deps(auto-js)', short: 'js', command: `node --test --test-reporter=spec --test-reporter-destination=stderr --test-reporter=tap --test-reporter-destination=stdout ${jsNative.join(' ')}`, count: jsNative.length, prioCount: jsNative.filter(f => prio(f) === 0).length, dropped: jsTest.length - jsRun.length, prioDropped: jsQ.prioDropped, tap: true, files: jsNative })
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
- if (jsxRunner) {
2967
- const isVitest = /vitest/.test(jsxRunner)
2968
- // jsx 批的 cd 重定基(评审 MEDIUM 清偿):cdDir 只源 pyRunner——jsxRunner 自带 cd 前缀
2969
- // (cd frontend && pnpm exec vitest)时文件未重定基产双前缀错路径;独立提取 jsxDir。
2970
- const jsxDir = /(?:^|\s)cd\s+(\S+)\s*&&/.exec(jsxRunner)?.[1]
2971
- const rebaseJsx = (f) => (jsxDir && f.startsWith(jsxDir + '/')) ? f.slice(jsxDir.length + 1) : f
2972
- // R20(2026-09-27-thin-display-fix 实证,用户授权修复):Windows cmd 执行器对含
2973
- // 括号/方括号/空格的路径(src/app/(dashboard)/workspaces/[id]/...)不加引号会以
2974
- // 「此时不应有 <。」语法错误炸整批——文件参数含特殊字符时逐个 wrap 双引号。
2975
- const quoteWinPath = (f) => (/[()\[\]\s&^%,;!]/.test(f) ? `"${f}"` : f)
2976
- batches.push({ name: 'deps(auto-jsx)', short: 'jsx', command: `${jsxRunner} ${isVitest ? 'run ' : ''}${jsProjectRun.map(rebaseJsx).map(quoteWinPath).join(' ')}`, count: jsProjectRun.length, dropped: 0, files: jsProjectRun })
2977
- } else {
2978
- batches.push({ name: 'deps(auto-jsx-skip)', short: 'jsx-skip', command: null, count: jsProjectRun.length, dropped: 0, skip: true, files: jsProjectRun, reason: `JSX 测试文件(tsx/jsx,${jsProjectRun.length} 个)node 原生不可跑且项目结构无 vitest/jest 运行器可推断——转项目运行器执行并如实披露,不制造恒败段(R18-SF-full 残差段 11 连败的坑)` })
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((x.repo && repoRoots.get(x.repo)) || cwd, x.file)))
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
- const _root = (_rk && _repoRoots.get(_rk)) || cwd
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()
@@ -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 前状态(无半成品冲突标记)。\n` +
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
- return { conflicts, error };
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
  /**