sillyspec 3.29.4 → 3.30.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 (148) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +54 -54
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +2 -2
  3. package/.claude/skills/sillyspec-commit/SKILL.md +1 -1
  4. package/.claude/skills/sillyspec-flow/SKILL.md +57 -0
  5. package/.claude/skills/sillyspec-quick/SKILL.md +18 -116
  6. package/AGENTS.md +38 -20
  7. package/SKILL.md +9 -3
  8. package/assets/command-cards/flow.md +32 -0
  9. package/assets/command-cards/run-archive.md +26 -0
  10. package/assets/command-cards/run-brainstorm.md +33 -0
  11. package/assets/command-cards/run-execute.md +29 -0
  12. package/assets/command-cards/run-plan.md +27 -0
  13. package/assets/command-cards/run-quick.md +17 -0
  14. package/assets/command-cards/run-verify.md +29 -0
  15. package/assets/command-cards/status.md +27 -0
  16. package/package.json +2 -1
  17. package/packages/dashboard/server/executor.js +2 -1
  18. package/packages/dashboard/server/parser.js +7 -7
  19. package/r7-test-env/README-R7-TEST.md +93 -0
  20. package/r7-test-env/demo-project/lib/calc.js +6 -0
  21. package/r7-test-env/demo-project/package.json +1 -0
  22. package/r7-test-env/demo-project/test.js +10 -0
  23. package/r7-test-env/prompt-R7-SMOKE.md +41 -0
  24. package/r7-test-env/sillyspec-r7.cmd +3 -0
  25. package/round4/account.mjs +98 -0
  26. package/round4/b1-analyze-task08.mjs +40 -0
  27. package/round4/b1-b1-target-validation.md +32 -0
  28. package/round4/b3a-prompt-audit-result.md +66 -0
  29. package/round4/b3a-prompt-audit.mjs +89 -0
  30. package/round4/brief-autocompact.md +34 -0
  31. package/round4/brief-session-replay.md +50 -0
  32. package/round4/deep-read-synthesis.md +38 -0
  33. package/round4/extract-full.mjs +38 -0
  34. package/round4/extract-timeline.mjs +43 -0
  35. package/round4/find-sessions.mjs +13 -0
  36. package/round4/full-r4-l.md +4707 -0
  37. package/round4/full-r4-o-l.md +4300 -0
  38. package/round4/full-r4-os.md +1681 -0
  39. package/round4/full-r4-sf.md +4255 -0
  40. package/round4/full-r4-sq.md +1734 -0
  41. package/round4/gate-audit-relay-receipt.md +30 -0
  42. package/round4/gate-value-audit.md +52 -0
  43. package/round4/optimization-plan.md +165 -0
  44. package/round4/prereg.md +67 -0
  45. package/round4/prompt-R4-L.md +63 -0
  46. package/round4/prompt-R4-O-L.md +59 -0
  47. package/round4/prompt-R4-O-S.md +56 -0
  48. package/round4/prompt-R4-S-F.md +60 -0
  49. package/round4/prompt-R4-S-Q.md +66 -0
  50. package/round4/r4-final-report.html +347 -0
  51. package/round4/r4-os-timeline.txt +297 -0
  52. package/round4/r4-sf-timeline.txt +738 -0
  53. package/round4/r4-sq-timeline.txt +322 -0
  54. package/round4/r4l-timeline.txt +793 -0
  55. package/round4/r4ol-timeline.txt +729 -0
  56. package/round4/r4sf-timeline.txt +852 -0
  57. package/round4/r5-scenario-matrix.md +46 -0
  58. package/round4/result-S.md +171 -0
  59. package/round4/result-final.md +46 -0
  60. package/round4/result-quality.md +63 -0
  61. package/src/change-list.js +9 -0
  62. package/src/classify-change.js +6 -0
  63. package/src/command-cards.js +74 -0
  64. package/src/commit-guard.js +2 -0
  65. package/src/config-schema.js +64 -12
  66. package/src/contract-matrix.js +26 -5
  67. package/src/db.js +35 -4
  68. package/src/decision-distill.js +9 -3
  69. package/src/diagnostic-codes.js +2 -1
  70. package/src/docs-check.js +1 -1
  71. package/src/doctor-diagnostics.js +44 -2
  72. package/src/flow-draft.js +748 -0
  73. package/src/flow-parity.js +236 -0
  74. package/src/flow-review.js +184 -0
  75. package/src/flow.js +1492 -0
  76. package/src/foreign-declared.js +17 -1
  77. package/src/fr-index.js +396 -22
  78. package/src/fs-atomic.js +5 -1
  79. package/src/git-helper.js +2 -2
  80. package/src/greenfield-bootstrap.js +98 -0
  81. package/src/handoff.js +120 -7
  82. package/src/hunk-attribution.js +191 -0
  83. package/src/index.js +602 -21
  84. package/src/init.js +25 -8
  85. package/src/knowledge-classify.js +47 -1
  86. package/src/knowledge-digest.js +162 -0
  87. package/src/knowledge-quicklog.js +246 -0
  88. package/src/machine-draft.js +128 -0
  89. package/src/machine-interface.js +194 -7
  90. package/src/mcp-server.js +1 -0
  91. package/src/module-impact.js +51 -22
  92. package/src/module-resolve.js +3 -1
  93. package/src/preview-progress.js +160 -0
  94. package/src/progress/change-registry.js +11 -3
  95. package/src/progress/shared.js +3 -2
  96. package/src/progress.js +56 -13
  97. package/src/quicklog.js +44 -6
  98. package/src/redomain.js +147 -0
  99. package/src/review-material-pack.js +204 -1
  100. package/src/run/archive-distill.js +4 -2
  101. package/src/run/bg-sync.js +67 -0
  102. package/src/run/command.js +127 -10
  103. package/src/run/complete-handlers.js +363 -73
  104. package/src/run/complete.js +356 -74
  105. package/src/run/gate-snapshot-ledger.js +213 -0
  106. package/src/run/gate-snapshot.js +260 -38
  107. package/src/run/gates.js +482 -268
  108. package/src/run/green-cache.js +123 -0
  109. package/src/run/junction-rm.js +70 -0
  110. package/src/run/next.js +14 -0
  111. package/src/run/prompt.js +185 -55
  112. package/src/run/quick-audit.js +213 -14
  113. package/src/run/shared.js +123 -4
  114. package/src/run/stage.js +182 -216
  115. package/src/run/test-ledger.js +264 -0
  116. package/src/run/verify-quality-scan.js +84 -7
  117. package/src/scope-audit.js +6 -1
  118. package/src/sentinel-assertions.js +114 -0
  119. package/src/setup.js +3 -3
  120. package/src/spec-sync.js +41 -1
  121. package/src/stage-contract.js +26 -8
  122. package/src/stage-review-checklist.js +2 -0
  123. package/src/stage-review.js +7 -2
  124. package/src/stage-templates.js +157 -0
  125. package/src/stages/archive.js +2 -0
  126. package/src/stages/brainstorm.js +24 -106
  127. package/src/stages/execute.js +222 -85
  128. package/src/stages/knowledge.js +74 -1
  129. package/src/stages/plan-postcheck.js +385 -20
  130. package/src/stages/plan.js +23 -1
  131. package/src/stages/quick.js +10 -2
  132. package/src/stages/verify.js +4 -2
  133. package/src/sync.js +81 -23
  134. package/src/task-done.js +181 -0
  135. package/src/task-review.js +8 -55
  136. package/src/taskcard-frontmatter.js +57 -0
  137. package/src/test-bindings.js +549 -0
  138. package/src/timeline.js +218 -0
  139. package/src/ui-visual.js +175 -0
  140. package/src/verify-draft.js +214 -0
  141. package/src/verify-postcheck.js +919 -159
  142. package/src/verify-probes.js +103 -25
  143. package/src/watcher.js +1031 -0
  144. package/src/worktree-apply.js +44 -4
  145. package/src/worktree-deps.js +22 -8
  146. package/src/worktree-policy.js +42 -0
  147. package/src/worktree.js +102 -15
  148. package/templates/agents-instruction.md +37 -18
@@ -10,7 +10,7 @@
10
10
  * 独立零环模块:被 verify-postcheck / verify-probes / contract-matrix 共用(verify-
11
11
  * postcheck import contract-matrix,parity 侧不能反向 import verify-postcheck)。
12
12
  */
13
- import { existsSync, readFileSync, readdirSync } from 'fs'
13
+ import { existsSync, readFileSync, readdirSync, statSync } from 'fs'
14
14
  import { join } from 'path'
15
15
  import { parseFileChangeListDetailed } from './change-list.js'
16
16
  import { safeGit, unquoteGitPath } from './git-helper.js'
@@ -131,6 +131,10 @@ export function collectForeignDeclaredFiles(cwd, currentChangeName, opts = {}) {
131
131
  } catch { /* 损坏/缺失跳过 */ }
132
132
  }
133
133
  // ② 其他变更的 design §6 清单(目录在即视为在途变更;archive/ 下不算)
134
+ // 时效判据(2026-09-25-platform-feedback-batch2 B):变更目录 mtime 超 7 天未动 → 声明视为
135
+ // 陈旧忽略(平台狗粮实证:9-21 未归档旧变更 9-25 还在抢文件——活性被 dirty 反向喂活但
136
+ // 变更本身已死。mtime 是最便宜的活性信号:任何文件写入/勾选/槽位填写都会 touch 目录)。
137
+ const STALE_CHANGE_MS = 7 * 24 * 60 * 60 * 1000
134
138
  const changesDir = join(specBase, 'changes')
135
139
  let changeDirs = []
136
140
  try { changeDirs = readdirSync(changesDir, { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name) } catch {}
@@ -138,6 +142,18 @@ export function collectForeignDeclaredFiles(cwd, currentChangeName, opts = {}) {
138
142
  if (cn === currentChangeName || cn === 'archive') continue
139
143
  const designPath = join(changesDir, cn, 'design.md')
140
144
  if (!existsSync(designPath)) continue
145
+ // 时效:变更目录内最新文件 mtime 超 7 天 → 陈旧(目录 mtime 在 POSIX 上只反映子项增删
146
+ // 不反映文件内容修改——用目录内所有文件的最大 mtime 做活性信号,跨平台一致)
147
+ try {
148
+ let newestMtime = 0
149
+ const changePath = join(changesDir, cn)
150
+ for (const fe of readdirSync(changePath)) {
151
+ try { const m = statSync(join(changePath, fe)).mtimeMs; if (m > newestMtime) newestMtime = m } catch { /* 个别文件 stat 失败跳过 */ }
152
+ }
153
+ if (newestMtime > 0 && Date.now() - newestMtime > STALE_CHANGE_MS) {
154
+ continue // 陈旧变更——声明已死,不抢活人的文件
155
+ }
156
+ } catch { /* stat 失败保守保留 */ }
141
157
  try {
142
158
  for (const e of parseFileChangeListDetailed(designPath)) add(e.path, cn)
143
159
  } catch { /* 清单解析失败跳过该变更 */ }
package/src/fr-index.js CHANGED
@@ -17,8 +17,11 @@
17
17
  * syncIndexRoutingLines/discoverModuleIndex),不复制实现。
18
18
  */
19
19
 
20
+ import { readChangeTrace, upsertFrBindings, applySupersededToEntryLines, testAnchorFile, resolveTestFileRel } from './test-bindings.js'
21
+ import { suggestDomainFromFiles } from './knowledge-digest.js'
20
22
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'fs';
21
- import { join } from 'path';
23
+ import { basename, dirname, join } from 'path';
24
+ import { writeAtomicSync } from './fs-atomic.js';
22
25
  import {
23
26
  splitKnowledgeSections,
24
27
  joinKnowledgeFile,
@@ -29,11 +32,102 @@ import {
29
32
  /** epoch:此前归档不检查/不索引(存量 93 份不回填——不伪造历史,D-007)。 */
30
33
  export const FR_INDEX_EPOCH = '2026-09-18';
31
34
 
35
+ /** design.md 交付清单表行解析(「| 新增|修改|删除 | 路径 |」行,含 NEW: 前缀形态)——剥反引号:
36
+ * 实测 29.4% 厚道交付条目带壳(fr-rot-precision 评审 P1-2),不剥与 git 路径永不相等。
37
+ * 从 resolveTouchedDomains 内部正则抽出公共(frCoverageFiles 与域路由共用同一解析)。 */
38
+ export function deliverableFilesFromDesignText(text) {
39
+ const out = [];
40
+ for (const line of String(text || '').replace(/\r\n/g, '\n').split('\n')) {
41
+ const m = line.match(/^\|\s*(?:新增|修改|删除)\s*\|\s*(?:NEW:)?([^\s|]+)\s*\|/);
42
+ if (m) out.push(m[1].trim().replace(/^`+|`+$/g, ''));
43
+ }
44
+ return out;
45
+ }
46
+
47
+ /**
48
+ * FR 覆盖文件集(fr-rot-precision:rot 三分判据/存量清理的文件面底座)——归档变更三源并集:
49
+ * ①design.md 交付表(厚道主源,剥反引号)②change-patch.json 的 files(thin 主源——thin 归档
50
+ * 27/30 在场,早期 3 份缺补位靠绑定)③(绑定由消费方并入——readActiveFrDigest.bindings)。
51
+ * 统一剔 .sillyspec/ 前缀(内部产物≠交付面);POSIX 归一;fail-soft(缺件=空贡献)。
52
+ * 匹配口径(消费方约定):changed 恒为文件级路径,判定用单向「相等 || changed.startsWith(cov 补/ 结尾)」。
53
+ */
54
+ export function frCoverageFiles({ archiveRoot, changeName }) {
55
+ const out = new Set();
56
+ const dir = join(archiveRoot, String(changeName || ''));
57
+ try {
58
+ const dp = join(dir, 'design.md');
59
+ if (existsSync(dp)) {
60
+ for (const f of deliverableFilesFromDesignText(readFileSync(dp, 'utf8'))) {
61
+ const v = f.replace(/\\/g, '/').replace(/\/+$/, '');
62
+ if (v) out.add(v);
63
+ }
64
+ }
65
+ } catch { /* 源1 fail-soft */ }
66
+ try {
67
+ const pp = join(dir, 'change-patch.json');
68
+ if (existsSync(pp)) {
69
+ const j = JSON.parse(readFileSync(pp, 'utf8'));
70
+ for (const f of (Array.isArray(j && j.files) ? j.files : [])) {
71
+ const v = String(f || '').replace(/\\/g, '/').replace(/\/+$/, '');
72
+ if (v) out.add(v);
73
+ }
74
+ }
75
+ } catch { /* 源2 fail-soft */ }
76
+ return [...out].filter((p) => !p.startsWith('.sillyspec/'));
77
+ }
78
+
79
+ /** 条目「测试绑定:」子块的 tests 文件提取(多文件 | 分隔)。边界=下一个 ## 节头(FR 节)——
80
+ * 子块内顶格的 `<!-- test-bindings -->` 机器注释与 `- row:` 行不终止收集(评审 P1 修复:
81
+ * 原「任意顶格行 break」遇子块首行顶格注释即断,生产库 bindings 恒空;缩进 tests: 键名 +
82
+ * 节头边界已足够防条目正文误匹配)。readActiveFrDigest.bindings 与 cleanupStaleReviewMarks 共用。 */
83
+ function readEntryBindings(lines) {
84
+ const idx = lines.findIndex((l) => l.startsWith('测试绑定:'));
85
+ if (idx === -1) return [];
86
+ const out = [];
87
+ for (const l of lines.slice(idx + 1)) {
88
+ if (/^##\s/.test(l)) break; // 下一节头=条目边界
89
+ const m = l.match(/^\s+tests:\s*(.+)$/);
90
+ if (m) {
91
+ for (const t of m[1].split('|')) {
92
+ const v = t.trim().replace(/^['"]|['"]$/g, '');
93
+ if (v) out.push(v.replace(/\\/g, '/'));
94
+ }
95
+ }
96
+ }
97
+ return out;
98
+ }
99
+
100
+ /** 条目未确认绑定行计数(2026-09-27-confirm-on-use):绑定子块内 confirmed_by≠agent 的
101
+ * `- row:` 块数——机器提升行恒 candidate/null(状态机有机制无消费者的病灶面),注入面据此
102
+ * 打 ⚪N未确认 标记 + 抽查确认提示。行块边界=下一个 `- row:` 或节头。 */
103
+ export function readEntryUnconfirmed(lines) {
104
+ const idx = lines.findIndex((l) => l.startsWith('测试绑定:'));
105
+ if (idx === -1) return 0;
106
+ let unconfirmed = 0
107
+ let inRow = false
108
+ let rowConfirmed = false
109
+ for (const l of lines.slice(idx + 1)) {
110
+ if (/^##\s/.test(l)) break
111
+ if (/^-\s+row:/.test(l)) {
112
+ if (inRow && !rowConfirmed) unconfirmed++
113
+ inRow = true; rowConfirmed = false; continue
114
+ }
115
+ if (inRow && /^\s+confirmed_by:\s*agent\s*$/.test(l)) rowConfirmed = true
116
+ }
117
+ if (inRow && !rowConfirmed) unconfirmed++
118
+ return unconfirmed
119
+ }
120
+
121
+ /** 标题 bigram 重叠判「重复嫌疑/承接漏写」的阈值(fr-index 判据函数与两处消费方——brainstorm
122
+ * 软门(stage-contract.js)与轻量道 dup 软门(flow.js)——共用同一常量,防两处各写一份字面量
123
+ * 漂移(fr-rot-precision 评审 P3 清偿:改阈值只改这里)。 */
124
+ export const FR_TITLE_OVERLAP_THRESHOLD = 0.6;
125
+
32
126
  const FR_DIR = 'fr';
33
127
  /** FR 节头:## FR-<域>-NNN 标题(域=[a-z0-9-] **含连字符**——真实模块 id 多为 cli-entry/core-engine 等连字符形态,
34
128
  * R1 审查阻断①实证:漏 - 会让连字符域的发号/幂等/解析/承接全断。第二组为非捕获可选占位,
35
129
  * 三组结构与底座 splitKnowledgeSections 的 decisions 正则对齐(m[3]=标题,勿改组序)。 */
36
- const FR_SECTION_RE = /^## (FR-[a-z0-9-]+-\d+)(?:@v(\d+))?\s*(.*)$/;
130
+ export const FR_SECTION_RE = /^## (FR-[a-z0-9-]+-\d+)(?:@v(\d+))?\s*(.*)$/;
37
131
  const FR_GLOBAL_ID_RE = /^FR-[a-z0-9-]+-\d+$/;
38
132
 
39
133
  /**
@@ -130,33 +224,72 @@ export function parseChangeRequirements(changeDir) {
130
224
  return { missing: false, frs, malformed };
131
225
  }
132
226
 
133
- /** 域解析(导出供注入/软门消费):变更自身 design.md 文件变更清单表行(剥 NEW: 前缀)× moduleIndex paths 前缀匹配;无匹配 → unmapped。 */
134
- export function resolveTouchedDomains(changeDir, moduleIndex) {
227
+ /** 域解析(导出供注入/软门消费):变更自身 design.md 文件变更清单表行(剥 NEW: 前缀)× moduleIndex paths 前缀匹配;
228
+ * 无匹配 → 伪域回退(auto-<路径段>,按文件路径段投票——2026-09-21 R5R 实证主仓 706 条 FR 落
229
+ * unmapped 大池且 INDEX 路由键为通用词无法命中,模块卡覆盖不足时 FR 复利断路);无文件清单 → unmapped。
230
+ * filesOverride(2026-09-22-thin-fr-distill-sync):显式交付文件清单旁路 design.md 解析——轻量变更
231
+ * 变更无 design.md(轻量工件面三件套),flow done 以基线以来交付 diff 供清单,伪域路由同口径。 */
232
+ export function resolveTouchedDomains(changeDir, moduleIndex, filesOverride = null) {
135
233
  const domains = new Set();
136
234
  const designPath = join(changeDir, 'design.md');
137
- if (moduleIndex && existsSync(designPath)) {
235
+ const files = [];
236
+ const matchModules = (filePath) => {
237
+ if (!moduleIndex) return;
238
+ for (const [modId, mod] of Object.entries(moduleIndex)) {
239
+ const paths = (mod && Array.isArray(mod.paths)) ? mod.paths : [];
240
+ if (paths.some((pp) => filePath === pp || filePath.startsWith(pp.endsWith('/') ? pp : pp + '/'))) {
241
+ domains.add(modId);
242
+ }
243
+ }
244
+ };
245
+ if (Array.isArray(filesOverride)) {
246
+ for (const f of filesOverride) {
247
+ const filePath = String(f || '').trim();
248
+ if (!filePath) continue;
249
+ files.push(filePath);
250
+ matchModules(filePath);
251
+ }
252
+ } else if (existsSync(designPath)) {
138
253
  let design;
139
254
  try {
140
255
  design = readFileSync(designPath, 'utf8');
141
256
  } catch {
142
257
  design = '';
143
258
  }
144
- for (const line of design.replace(/\r\n/g, '\n').split('\n')) {
145
- const m = line.match(/^\|\s*(?:新增|修改|删除)\s*\|\s*(?:NEW:)?([^\s|]+)\s*\|/);
146
- if (!m) continue;
147
- const filePath = m[1].trim();
148
- for (const [modId, mod] of Object.entries(moduleIndex)) {
149
- const paths = (mod && Array.isArray(mod.paths)) ? mod.paths : [];
150
- if (paths.some((pp) => filePath === pp || filePath.startsWith(pp.endsWith('/') ? pp : pp + '/'))) {
151
- domains.add(modId);
152
- }
153
- }
259
+ for (const filePath of deliverableFilesFromDesignText(design)) {
260
+ files.push(filePath);
261
+ matchModules(filePath);
154
262
  }
155
263
  }
156
- if (domains.size === 0) domains.add('unmapped');
264
+ if (domains.size === 0) domains.add(files.length > 0 ? pseudoDomainFromPaths(files) : 'unmapped');
157
265
  return [...domains];
158
266
  }
159
267
 
268
+ // 泛化首段(src/lib/test/...)不承载域语义——目录段里跳过它们取首个具体段。
269
+ const GENERIC_PATH_SEGMENTS = new Set(['src', 'lib', 'test', 'tests', 'spec', 'docs', 'doc', 'app', 'packages', 'scripts', 'config', 'public', 'internal']);
270
+
271
+ /**
272
+ * 文件清单 → 伪域名(auto-<段>):逐文件取目录段(末段是文件名不投票)里首个非泛化段,
273
+ * 按出现次数投票取众数;全部泛化/根文件 → 'unmapped'。auto- 前缀保证与真实模块 id
274
+ * (模块卡派生)不冲突,且在 fr 文件头与 INDEX 路由键里自明身份。
275
+ */
276
+ function pseudoDomainFromPaths(files) {
277
+ const votes = new Map();
278
+ for (const f of files) {
279
+ const segs = String(f || '').replace(/\\/g, '/').split('/').filter(Boolean);
280
+ if (segs.length < 2) continue; // 根文件无目录段
281
+ for (const seg of segs.slice(0, -1)) {
282
+ const s = seg.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '');
283
+ if (!s || GENERIC_PATH_SEGMENTS.has(s)) continue;
284
+ votes.set(s, (votes.get(s) || 0) + 1);
285
+ break; // 首个非泛化目录段即该文件的票,一文件一票
286
+ }
287
+ }
288
+ if (votes.size === 0) return 'unmapped';
289
+ const top = [...votes.entries()].sort((a, b) => b[1] - a[1])[0][0];
290
+ return `auto-${top}`;
291
+ }
292
+
160
293
  /** 域文件路径与域索引读写(复用参数化底座;FR 节无版本段,id 恒等)。 */
161
294
  function frDirPath(knowledgeRoot) {
162
295
  return join(knowledgeRoot, FR_DIR);
@@ -176,7 +309,9 @@ function loadDomainSections(knowledgeRoot, domain) {
176
309
  '',
177
310
  '> fr-index 从归档变更 requirements.md 幂等提炼(「最近确认」= 归档时 HEAD)。条目字段行为机械解析契约,勿手改。',
178
311
  '> superseded 条目保留供取代链回溯;brainstorm 注入默认只给 active。',
179
- `> 模块卡:modules/${domain}.md(域=模块 id 同构;行为条目↔模块契约互跳)`,
312
+ String(domain).startsWith('auto-')
313
+ ? '> 伪域(auto- 前缀):由文件路径段投票派生,无模块卡——为该域补模块卡后,新变更将自动落回真域'
314
+ : `> 模块卡:modules/${domain}.md(域=模块 id 同构;行为条目↔模块契约互跳)`,
180
315
  '',
181
316
  ],
182
317
  sections: [],
@@ -252,10 +387,22 @@ function nextIdForDomain(domain, allSections) {
252
387
 
253
388
  /**
254
389
  * 归档索引主入口(幂等):解析 → 域解析 → 发号 → 承接翻链 → 写域文件 → INDEX 路由。
255
- * @param {{ changeDir: string, knowledgeRoot: string, headHash?: string, cwd?: string }} args
390
+ * @param {{ changeDir: string, knowledgeRoot: string, headHash?: string, cwd?: string, deliverableFiles?: string[] }} args
391
+ * deliverableFiles:显式交付文件清单(轻量变更无 design.md 时由调用方供基线 diff,域路由同口径)
256
392
  * @returns {{ skipped?: string, written: Array<{file,id,action}>, superseded: Array<{from,to,change}>, unreferenced: Array<{domain,count}>, warnings: string[] }}
257
393
  */
258
- export function indexRequirements({ changeDir, knowledgeRoot, headHash = '' }) {
394
+ /** projectRoot 启发式(绑定路径仓根归一用):specBase 目录名是 .sillyspec 且其父像仓根
395
+ * (.git/package.json/pyproject.toml 在场)→ 父目录;否则 null(不猜——归一跳过保原值)。 */
396
+ function deriveProjectRootFromSpec(specBase) {
397
+ try {
398
+ const parent = dirname(specBase)
399
+ if (basename(specBase) === '.sillyspec'
400
+ && (existsSync(join(parent, '.git')) || existsSync(join(parent, 'package.json')) || existsSync(join(parent, 'pyproject.toml')))) return parent
401
+ } catch { /* 启发式失败 → null */ }
402
+ return null
403
+ }
404
+
405
+ export function indexRequirements({ changeDir, knowledgeRoot, headHash = '', deliverableFiles = null, projectRoot = null }) {
259
406
  const changeName = changeDir.split(/[\\/]/).pop();
260
407
  const parsed = parseChangeRequirements(changeDir);
261
408
  if (parsed.missing) {
@@ -267,7 +414,7 @@ export function indexRequirements({ changeDir, knowledgeRoot, headHash = '' }) {
267
414
 
268
415
  const warnings = [...parsed.malformed];
269
416
  const moduleIndex = discoverModuleIndex(knowledgeRoot);
270
- const domains = resolveTouchedDomains(changeDir, moduleIndex);
417
+ const domains = resolveTouchedDomains(changeDir, moduleIndex, deliverableFiles);
271
418
  const all = scanAllDomains(knowledgeRoot);
272
419
 
273
420
  // 幂等闸门:任一域文件已有本变更「来源变更」条目 → 全量 no-op(同变更重跑零新增零漂移)
@@ -333,6 +480,11 @@ export function indexRequirements({ changeDir, knowledgeRoot, headHash = '' }) {
333
480
  `superseded_by:${id}`,
334
481
  `取代链:${refId} ← ${id}(${changeName} 承接)`,
335
482
  ...(reason ? [`退役理由:${reason}`] : []));
483
+ // 绑定行同步退役(2026-09-24-fr-test-bindings task-06,fr-test-binding §3.2 四硬约束④):
484
+ // 被承接条目的绑定行 status→superseded(内存态翻转——文件稍后统一落盘,禁死锚)
485
+ try {
486
+ target.lines = applySupersededToEntryLines(target.lines);
487
+ } catch { /* 绑定翻转 fail-open:翻链语义不受影响 */ }
336
488
  // scenario-loss 检测(对标 OpenSpec 同名检查):被取代条目的场景名在新 FR 场景集
337
489
  //(scenarios 名字数组 ∪ scenarioBodies 名)无对应 → warning 提示核对(advisory 不阻断
338
490
  //——场景名合法漂移存在,人裁)。旧条目无场景信息(摘要「(无场景名)」/空)不比对。
@@ -380,14 +532,71 @@ export function indexRequirements({ changeDir, knowledgeRoot, headHash = '' }) {
380
532
  const stx = all.get(d);
381
533
  writeFileSync(join(frDirPath(knowledgeRoot), `${d}.md`), joinKnowledgeFile(stx.preamble, stx.sections));
382
534
  }
535
+ // ── 归档提升(2026-09-24-fr-test-bindings task-06,fr-test-binding §3.2):本变更 test-trace
536
+ // 的 FR 局部锚行随发号映射铸全局后 upsert 进活库条目机器子块(source_change+row_id 键、
537
+ // 内容全等 no-op、机器不删 agent 行);orphan/ql 行不在此面(前者留档审计,后者在
538
+ // quicklog 机器面)。fail-open:提升异常只告警不炸归档(幂等可重跑)。──
539
+ const promotedBindings = [];
540
+ try {
541
+ const traceRows = readChangeTrace(changeDir);
542
+ if (traceRows.length > 0) {
543
+ // 局部→全局映射:本函数发号段 written[{id}] 与 parsed.frs 逐条对应(同循环序)
544
+ const localToGlobal = new Map();
545
+ parsed.frs.forEach((fr, i) => { if (written[i] && written[i].id) localToGlobal.set(fr.local, written[i].id); });
546
+ for (const [local, globalId] of localToGlobal) {
547
+ const rows = traceRows.filter((r) => r.anchor === local);
548
+ if (rows.length === 0) continue;
549
+ // projectRoot 优先调用方(真仓根),缺省启发式自 specBase 推(.sillyspec 父目录像仓根才取)
550
+ const res = upsertFrBindings({ knowledgeRoot, frId: globalId, rows, projectRoot: projectRoot || deriveProjectRootFromSpec(dirname(changeDir)) });
551
+ if (res.ok) promotedBindings.push({ fr: globalId, rows: rows.length, changed: res.changed });
552
+ }
553
+ if (promotedBindings.length > 0) {
554
+ console.log(`🔗 测试绑定归档提升:${promotedBindings.map((p) => `${p.fr}(${p.rows}行)`).join('、')} → knowledge/fr/ 机器子块`);
555
+ }
556
+ }
557
+ } catch (e) {
558
+ console.warn(`⚠️ 测试绑定归档提升失败(fail-open,可重跑归档幂等重试):${e && e.message ? e.message : e}`);
559
+ }
383
560
  if (written.length > 0 || superseded.length > 0) {
384
561
  syncIndexRoutingLines(knowledgeRoot, {
385
562
  section: 'FR 需求索引',
386
563
  subdir: FR_DIR,
387
- makeLine: (d) => `- ${d}|FR|需求|承接 → [${FR_DIR}/${d}.md](${FR_DIR}/${d}.md)`,
564
+ // 伪域路由行追加裸段关键词(auto-backend 行同时吃 'backend' 命中——域名字面作为
565
+ // 唯一路由键对 auto- 域过窄,2026-09-21 unmapped 大池路由失效的教训)
566
+ makeLine: (d) => d.startsWith('auto-')
567
+ ? `- ${d}|${d.slice(5)}|FR|需求|承接 → [${FR_DIR}/${d}.md](${FR_DIR}/${d}.md)`
568
+ : `- ${d}|FR|需求|承接 → [${FR_DIR}/${d}.md](${FR_DIR}/${d}.md)`,
388
569
  });
389
570
  }
390
571
 
572
+ // ── 绿地知识面提醒(greenfield-bootstrap):本变更条目落伪域/unmapped 时提示升级路径;
573
+ // unmapped 域总条目超阈(50)告警配治理指引(本仓 720 条实证堆积病——告警不阻断)。──
574
+ try {
575
+ const pseudoHits = written.filter((w) => /^(auto-|unmapped$)/.test(String(w.file || '').replace(/^fr\//, '')))
576
+ if (pseudoHits.length > 0) {
577
+ // 落域机械改进(2026-09-27-knowledge-digest):伪域告警附交付路径推导的建议域——
578
+ // 归档时转达人工 1 秒确认(建议器与 digest 信号卡共用 suggestDomainFromFiles 单源)
579
+ const suggested = suggestDomainFromFiles(deliverableFiles || [])
580
+ console.warn(`⚠️ [FR 域路由降级] 本变更 ${pseudoHits.length} 条 FR 落伪域/unmapped(${[...new Set(pseudoHits.map((w) => w.file))].join('、')})${suggested ? `——按交付路径建议域:${suggested}(确认后可迁移;` : '('}补模块卡(docs/<项目>/modules/_module-map.yaml 登记该目录)后,新变更将自动落回真域;绿地仓可跑 sillyspec run scan 校准)`)
581
+ if (suggested) console.warn(` 迁移/存量清单:sillyspec knowledge digest(伪域信号卡)`)
582
+ }
583
+ const unm = all.get('unmapped')
584
+ if (unm && unm.sections.length > 50) {
585
+ // 基线消音(2026-09-26 修复轮):本仓 unmapped 720 条是 MP 仓历史变更的跨仓知识基线
586
+ // (103 个来源变更全在归档,非新增堆积)——警告只对新增超基线时触发,不重复刷历史池。
587
+ // 基线锚定 local.yaml 的 fr_unmapped_baseline(缺省 0=无基线,警告行为不变)。
588
+ let baseline = 0
589
+ try {
590
+ const raw = readFileSync(join(knowledgeRoot, '..', 'local.yaml'), 'utf8')
591
+ const m = raw.match(/^fr_unmapped_baseline:\s*(\d+)\s*$/m)
592
+ if (m) baseline = parseInt(m[1], 10)
593
+ } catch { /* 无 local.yaml = 无基线 */ }
594
+ if (unm.sections.length > Math.max(50, baseline + 10)) {
595
+ console.warn(`⚠️ [unmapped 大池] ${unm.sections.length} 条 FR 堆积在 unmapped 域(基线 ${baseline} + 增量 >10)——若为跨仓历史基线可在 local.yaml 设 fr_unmapped_baseline: ${unm.sections.length} 消音;新变更落 unmapped 时建议补模块卡自动分流`)
596
+ }
597
+ }
598
+ } catch { /* 提醒 fail-open */ }
599
+
391
600
  return { written, superseded, unreferenced, warnings };
392
601
  }
393
602
 
@@ -570,12 +779,80 @@ export function readActiveFrDigest(knowledgeRoot, domains) {
570
779
  // 注入面带 ⚠️ 提示后续变更核对——是信号非失效,承接翻链时清除。
571
780
  const reviewLine = s.lines.find((l) => l.startsWith('待复核:'));
572
781
  const needsReview = reviewLine ? reviewLine.replace(/^待复核:\s*/, '').trim() : null;
573
- out.push({ domain, id: s.number, title: s.title || '', change: s.change || '', scenarios, decisions, needsReview });
782
+ // bindings(fr-rot-precision):条目测试绑定的 test 文件——rot coverage 三源之一;纯新增
783
+ // 字段,既有消费方(prompt.js 注入渲染等)不受影响。
784
+ out.push({ domain, id: s.number, title: s.title || '', change: s.change || '', scenarios, decisions, needsReview, bindings: readEntryBindings(s.lines), unconfirmed: readEntryUnconfirmed(s.lines) });
574
785
  }
575
786
  }
576
787
  return out;
577
788
  }
578
789
 
790
+ // ── active FR 覆盖命中查询(2026-09-26-dynamic-test-inference)─────────────────────
791
+ // 查询核单源双消费:flow 侧 rotSuspectFlow 打待复核标记(信号面);verify 侧
792
+ // collectFrLinkedTests 反用为「需求关联回归测试面」(测试门三源之二)。覆盖判定与
793
+ // rot 完全同口径:来源变更 patch 文件 ∪ 绑定 tests(剥用例锚)∩ 本次触碰文件 ≠ ∅。
794
+
795
+ /**
796
+ * 触达域内 active FR 的强命中集。
797
+ * @returns {{ domains: string[], hits: Array<{domain,id,title,change,bindings:string[],coverage:Set}>, unknownSources: string[] }}
798
+ * hits=覆盖面与触碰文件相交者;unknownSources=覆盖源缺失(无 patch 无绑定)的来源变更名。
799
+ */
800
+ export function activeFrCoverageHits({ specBase, change = null, changeDir = null, files = [] }) {
801
+ const knowledgeRoot = join(specBase, 'knowledge')
802
+ const archiveRoot = join(specBase, 'changes', 'archive')
803
+ const moduleIndex = discoverModuleIndex(knowledgeRoot)
804
+ const changed = (Array.isArray(files) ? files : []).map((f) => String(f || '').replace(/\\/g, '/')).filter(Boolean)
805
+ // changeDir 仅为 design.md 兜底路由用(files 在场时不读);不可传 null——resolveTouchedDomains 无条件 join
806
+ const domains = resolveTouchedDomains(changeDir || join(specBase, 'changes', String(change || 'x')), moduleIndex, changed).filter((d) => d !== 'unmapped')
807
+ if (domains.length === 0) return { domains: [], hits: [], unknownSources: [] }
808
+ const frs = readActiveFrDigest(knowledgeRoot, domains)
809
+ if (frs.length === 0) return { domains, hits: [], unknownSources: [] }
810
+ const hits = []
811
+ const unknownSources = new Set()
812
+ const covCache = new Map()
813
+ let skip = 0
814
+ let unknownFrCount = 0
815
+ for (const f of frs) {
816
+ if (!covCache.has(f.change)) covCache.set(f.change, frCoverageFiles({ archiveRoot, changeName: f.change }))
817
+ // bindings 可携带用例锚(2026-09-26-binding-anchor-fidelity)——覆盖判定按文件面取值走剥锚
818
+ const cov = new Set([...(covCache.get(f.change) || []), ...(Array.isArray(f.bindings) ? f.bindings.map((b) => testAnchorFile(b)) : [])])
819
+ if (cov.size === 0) { unknownSources.add(f.change || '(无来源变更)'); unknownFrCount++; continue }
820
+ const hit = [...cov].some((p) => changed.some((c) => c === p || c.startsWith(p.endsWith('/') ? p : p + '/')))
821
+ if (hit) hits.push({ domain: f.domain, id: f.id, title: f.title, change: f.change, bindings: f.bindings || [], coverage: cov })
822
+ else skip++
823
+ }
824
+ return { domains, hits, unknownSources: [...unknownSources], unknownFrCount, skip, total: frs.length }
825
+ }
826
+
827
+ /**
828
+ * 需求关联回归测试面(测试门三源之二):强命中 FR 的绑定 tests 解析为仓根相对文件集。
829
+ * 路径解析三段(resolveTestFileRel):根相对直取 → 子项目根前缀补全 → 裸文件名受限 glob;
830
+ * 解析失败路径不进测试面(unresolved 披露,宁缺勿错跑)。
831
+ * @returns {{ files: string[], frHits: Array<{domain,id,title,files:string[]}>, unresolved: string[], domains: string[] }}
832
+ */
833
+ export function collectFrLinkedTests({ specBase, changeName = null, changedFiles = [], projectRoot }) {
834
+ const q = activeFrCoverageHits({
835
+ specBase,
836
+ change: changeName,
837
+ changeDir: changeName ? join(specBase, 'changes', changeName) : null,
838
+ files: changedFiles,
839
+ })
840
+ const filesSet = new Set()
841
+ const frHits = []
842
+ const unresolved = []
843
+ for (const h of q.hits) {
844
+ if (!Array.isArray(h.bindings) || h.bindings.length === 0) continue
845
+ const resolvedFiles = new Set()
846
+ for (const b of h.bindings) {
847
+ const filePart = testAnchorFile(b)
848
+ const rel = resolveTestFileRel(filePart, { projectRoot })
849
+ if (rel) { resolvedFiles.add(rel); filesSet.add(rel) } else unresolved.push(`${h.id}:${b}`)
850
+ }
851
+ if (resolvedFiles.size > 0) frHits.push({ domain: h.domain, id: h.id, title: h.title, files: [...resolvedFiles].sort() })
852
+ }
853
+ return { files: [...filesSet].sort(), frHits, unresolved, domains: q.domains }
854
+ }
855
+
579
856
  /**
580
857
  * 标题 bigram 重叠率(中英混排零依赖):字符 bigram 集合 |A∩B| / |A∪B|。
581
858
  */
@@ -643,3 +920,100 @@ export function markFrNeedsReview(knowledgeRoot, frIds, refNote) {
643
920
  }
644
921
  return { marked, warnings };
645
922
  }
923
+
924
+ /**
925
+ * 归档侧 FR 域路由文件面(greenfield-bootstrap:archive 侧 indexRequirements 此前不传
926
+ * deliverableFiles——域路由退化为 design 清单单源,design 全泛化段/根文件时落 unmapped,
927
+ * R17 臂3 十条 FR 实证):design 交付表 ∪ apply-manifest.json 的 files[].path(apply 链
928
+ * 落盘的真实交付清单)。与轻量道 attributedChangedFiles 口径对齐(文件面供域路由投票)。
929
+ */
930
+ export function archiveDeliverableFiles(changeDir) {
931
+ const out = new Set()
932
+ try {
933
+ const dp = join(changeDir, 'design.md')
934
+ if (existsSync(dp)) {
935
+ for (const f of deliverableFilesFromDesignText(readFileSync(dp, 'utf8'))) out.add(f.replace(/\\/g, '/'))
936
+ }
937
+ } catch { /* fail-soft */ }
938
+ try {
939
+ const mp = join(changeDir, 'apply-manifest.json')
940
+ if (existsSync(mp)) {
941
+ const j = JSON.parse(readFileSync(mp, 'utf8'))
942
+ for (const f of Array.isArray(j && j.files) ? j.files : []) {
943
+ const p = String(f && f.path ? f.path : f || '').replace(/\\/g, '/').replace(/\/+$/, '')
944
+ if (p) out.add(p)
945
+ }
946
+ }
947
+ } catch { /* fail-soft */ }
948
+ return [...out].filter((p) => !p.startsWith('.sillyspec/'))
949
+ }
950
+
951
+ /**
952
+ * 存量待复核标记治理(fr-rot-precision 评审修正稿):按「新判据下 ref 变更收口时该条会不会被
953
+ * 打标」重算——keep iff coverageFiles(FR 来源变更) ∪ bindings 与 coverageFiles(ref 变更) 有文件面
954
+ * 交集。ref 无归档或任一侧 coverage 空 → 删(自然覆盖 quick 侧 ref——quick 永不归档无文件面;
955
+ * 与运行时 unknown 不打标口径一致:宁漏勿滥,漏标只损失注入排序优先级)。
956
+ * 并发安全:写前重读比对快照,盘上已被并行会话改写的文件跳过(幂等可重跑消化);原子写。
957
+ * superseded 条目不碰(无 active 语义);**在途变更的标记跳过**(rot 打标在归档前、ref 必无
958
+ * 冻结件——误判面实证后修复:收口落地后重跑清理再判)。
959
+ * @returns {{ removed: number, kept: number, inFlight: number, skipped: string[], files: string[], byRef: Record<string, number> }}
960
+ */
961
+ export function cleanupStaleReviewMarks({ specBase, archiveRoot }) {
962
+ const knowledgeRoot = join(specBase, 'knowledge');
963
+ const frDir = frDirPath(knowledgeRoot);
964
+ const snapshot = new Map();
965
+ try {
966
+ for (const f of readdirSync(frDir)) {
967
+ if (f.endsWith('.md')) snapshot.set(f, readFileSync(join(frDir, f), 'utf8'));
968
+ }
969
+ } catch { /* 目录不可读=空治理面 */ }
970
+ const all = scanAllDomains(knowledgeRoot);
971
+ const covCache = new Map();
972
+ const coverageOf = (changeName) => {
973
+ if (!changeName) return [];
974
+ if (!covCache.has(changeName)) covCache.set(changeName, frCoverageFiles({ archiveRoot, changeName }));
975
+ return covCache.get(changeName);
976
+ };
977
+ let removed = 0, kept = 0, inFlight = 0;
978
+ const byRef = {};
979
+ const dirty = new Set();
980
+ const covHit = (frChange, bindings, refCover) => {
981
+ // bindings 可携带用例锚(2026-09-26-binding-anchor-fidelity)——覆盖判定按文件面取值走剥锚
982
+ const frCov = new Set([...coverageOf(frChange), ...(Array.isArray(bindings) ? bindings.map((b) => testAnchorFile(b)) : [])]);
983
+ if (frCov.size === 0 || refCover.length === 0) return false;
984
+ return [...frCov].some((p) => refCover.some((c) => c === p || c.startsWith(p.endsWith('/') ? p : p + '/') || p.startsWith(c.endsWith('/') ? c : c + '/')));
985
+ };
986
+ for (const [domain, st] of all.entries()) {
987
+ for (const s of st.sections) {
988
+ const reviewLines = s.lines.filter((l) => l.startsWith(FR_NEEDS_REVIEW_PREFIX));
989
+ if (reviewLines.length === 0) continue;
990
+ if (s.lines.some((l) => l.startsWith('superseded_by:'))) continue; // 已取代:翻链自会清,不在此治理
991
+ const ref = reviewLines[0].slice(FR_NEEDS_REVIEW_PREFIX.length).trim();
992
+ // 在途变更跳过(第二轮清理实证):rot 打标发生在 flow done ledger(归档前),ref 指向的
993
+ // 变更此刻必无归档冻结件——coverage(ref) 恒空会误判全删。跳过,收口落地后重跑再判。
994
+ if (ref && existsSync(join(specBase, 'changes', ref))) { inFlight++; continue; }
995
+ const keep = covHit(s.change, readEntryBindings(s.lines), coverageOf(ref));
996
+ if (keep) { kept++; continue; }
997
+ s.lines = s.lines.filter((l) => !l.startsWith(FR_NEEDS_REVIEW_PREFIX));
998
+ removed++;
999
+ byRef[ref] = (byRef[ref] || 0) + 1;
1000
+ dirty.add(domain);
1001
+ }
1002
+ }
1003
+ const files = [];
1004
+ const skipped = [];
1005
+ if (dirty.size > 0) {
1006
+ mkdirSync(frDir, { recursive: true });
1007
+ for (const d of dirty) {
1008
+ const fname = `${d}.md`;
1009
+ try {
1010
+ const before = snapshot.get(fname);
1011
+ const now = existsSync(join(frDir, fname)) ? readFileSync(join(frDir, fname), 'utf8') : null;
1012
+ if (before == null || before !== now) { skipped.push(fname); continue; } // 并行写已发生——跳过,重跑消化
1013
+ } catch { skipped.push(fname); continue; }
1014
+ writeAtomicSync(join(frDir, fname), joinKnowledgeFile(all.get(d).preamble, all.get(d).sections));
1015
+ files.push(fname);
1016
+ }
1017
+ }
1018
+ return { removed, kept, inFlight, skipped, files, byRef };
1019
+ }
package/src/fs-atomic.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * 注意:DB 持久化由 node:sqlite 引擎承担(提交即落盘 sillyspec.db + WAL 侧车),
8
8
  * 不经此处的 writeAtomicSync;sillyspec.db 不走原子写改名。
9
9
  */
10
- import { renameSync, unlinkSync, openSync, writeSync, fsyncSync, closeSync, statSync } from 'fs';
10
+ import { renameSync, unlinkSync, openSync, writeSync, fsyncSync, closeSync, statSync, mkdirSync } from 'fs';
11
11
  import { randomBytes } from 'node:crypto';
12
12
  import { dirname, basename, join } from 'path';
13
13
 
@@ -64,6 +64,10 @@ export function renameSyncRetry(from, to, retries = 5) {
64
64
  */
65
65
  export function writeAtomicSync(filePath, content) {
66
66
  const dir = dirname(filePath);
67
+ // 父目录保位(坑 thin-flow-adopt-edge-cases 坑2,2026-09-25 实证):adopt 收编补件等
68
+ // 「目录刚建、.runtime 尚不存在」的写点直接 ENOENT——原子写的契约是「写出完整文件」,
69
+ // 缺父目录按 mkdir -p 语义创建(已存在零成本);权限类失败仍如实抛错。
70
+ try { mkdirSync(dir, { recursive: true }); } catch { /* 创建失败留给 openSync 抛真实原因 */ }
67
71
  // pid + 随机段双因子:Windows PID 重用激进,两进程可能撞同 pid,tmp 名碰撞 rename 互相覆盖。
68
72
  // crypto 随机源(Math.random 可预测,tmp 名被预猜中有 symlink 预放置理论面)
69
73
  const rnd = randomBytes(6).toString('hex');
package/src/git-helper.js CHANGED
@@ -99,7 +99,7 @@ export function safeGit(cwd, args, opts = {}) {
99
99
  // 格式化(取首行)推迟到最终返回,避免重试分支重复格式化,并保留原始 code 供重试判定。
100
100
  const attempt = (t) => {
101
101
  try {
102
- let value = execFileSync('git', fullArgs, { encoding: 'utf8', timeout: t, maxBuffer: GIT_MAX_BUFFER, stdio: ['ignore', 'pipe', 'pipe'] })
102
+ let value = execFileSync('git', fullArgs, { encoding: 'utf8', timeout: t, maxBuffer: GIT_MAX_BUFFER, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true })
103
103
  if (trim) value = value.trim()
104
104
  return { value, errorObj: null }
105
105
  } catch (e) {
@@ -128,7 +128,7 @@ export function git(cwd, args, opts = {}) {
128
128
  const fullArgs = buildFullArgs(cwd, args)
129
129
  // env(终批-③):可选注入(如 baseline checkpoint 的临时 GIT identity)——调用方必须
130
130
  // 用 { ...process.env, GIT_xxx } 展开形式,裸替换会丢 SystemRoot/USERPROFILE/TEMP(Windows)
131
- const value = execFileSync('git', fullArgs, { encoding, timeout, maxBuffer: GIT_MAX_BUFFER, stdio: ['ignore', 'pipe', 'pipe'], env })
131
+ const value = execFileSync('git', fullArgs, { encoding, timeout, maxBuffer: GIT_MAX_BUFFER, stdio: ['ignore', 'pipe', 'pipe'], env, windowsHide: true })
132
132
  if (Buffer.isBuffer(value)) return value
133
133
  return trim ? value.trim() : value
134
134
  }