sillyspec 3.29.5 → 3.31.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-auto/SKILL.md +1 -1
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +2 -2
- package/.claude/skills/sillyspec-commit/SKILL.md +1 -1
- package/.claude/skills/sillyspec-flow/SKILL.md +57 -0
- package/.claude/skills/sillyspec-quick/SKILL.md +7 -105
- package/AGENTS.md +38 -20
- package/SKILL.md +9 -3
- package/assets/command-cards/flow.md +32 -0
- package/assets/command-cards/run-brainstorm.md +1 -1
- package/assets/command-cards/run-quick.md +8 -25
- package/package.json +2 -1
- package/packages/dashboard/server/executor.js +2 -1
- package/packages/dashboard/server/parser.js +7 -7
- package/r7-test-env/README-R7-TEST.md +93 -0
- package/r7-test-env/demo-project/lib/calc.js +6 -0
- package/r7-test-env/demo-project/package.json +1 -0
- package/r7-test-env/demo-project/test.js +10 -0
- package/r7-test-env/prompt-R7-SMOKE.md +41 -0
- package/r7-test-env/sillyspec-r7.cmd +3 -0
- package/round4/account.mjs +18 -5
- package/round4/b1-analyze-task08.mjs +40 -0
- package/round4/b1-b1-target-validation.md +32 -0
- package/round4/b3a-prompt-audit-result.md +66 -0
- package/round4/b3a-prompt-audit.mjs +89 -0
- package/round4/optimization-plan.md +165 -0
- package/round4/r4-final-report.html +347 -0
- package/round4/r5-scenario-matrix.md +46 -0
- package/src/change-list.js +9 -0
- package/src/classify-change.js +6 -0
- package/src/command-cards.js +2 -1
- package/src/commit-guard.js +2 -0
- package/src/config-schema.js +64 -12
- package/src/db.js +35 -4
- package/src/decision-distill.js +9 -3
- package/src/docs-check.js +1 -1
- package/src/doctor-diagnostics.js +44 -2
- package/src/flow-draft.js +748 -0
- package/src/flow-parity.js +236 -0
- package/src/flow-review.js +184 -0
- package/src/flow.js +1492 -0
- package/src/foreign-declared.js +17 -1
- package/src/fr-index.js +396 -22
- package/src/fs-atomic.js +5 -1
- package/src/git-helper.js +2 -2
- package/src/greenfield-bootstrap.js +98 -0
- package/src/handoff.js +120 -7
- package/src/hunk-attribution.js +191 -0
- package/src/index.js +570 -16
- package/src/knowledge-classify.js +47 -1
- package/src/knowledge-digest.js +162 -0
- package/src/knowledge-quicklog.js +246 -0
- package/src/machine-draft.js +128 -0
- package/src/machine-interface.js +107 -5
- package/src/mcp-server.js +1 -0
- package/src/module-resolve.js +3 -1
- package/src/preview-progress.js +160 -0
- package/src/progress/change-registry.js +11 -3
- package/src/progress/shared.js +3 -2
- package/src/progress.js +56 -13
- package/src/quicklog.js +44 -6
- package/src/redomain.js +147 -0
- package/src/review-material-pack.js +204 -1
- package/src/run/archive-distill.js +4 -2
- package/src/run/bg-sync.js +67 -0
- package/src/run/command.js +123 -10
- package/src/run/complete-handlers.js +327 -72
- package/src/run/complete.js +350 -71
- package/src/run/gate-snapshot-ledger.js +213 -0
- package/src/run/gate-snapshot.js +250 -34
- package/src/run/gates.js +370 -233
- package/src/run/green-cache.js +123 -0
- package/src/run/junction-rm.js +70 -0
- package/src/run/next.js +14 -0
- package/src/run/prompt.js +185 -55
- package/src/run/quick-audit.js +185 -12
- package/src/run/shared.js +120 -3
- package/src/run/stage.js +182 -216
- package/src/run/test-ledger.js +264 -0
- package/src/run/verify-quality-scan.js +84 -7
- package/src/scope-audit.js +6 -1
- package/src/sentinel-assertions.js +114 -0
- package/src/setup.js +3 -3
- package/src/spec-sync.js +41 -1
- package/src/stage-contract.js +26 -8
- package/src/stage-review-checklist.js +2 -0
- package/src/stage-review.js +7 -2
- package/src/stage-templates.js +157 -0
- package/src/stages/archive.js +2 -0
- package/src/stages/brainstorm.js +24 -106
- package/src/stages/execute.js +221 -84
- package/src/stages/knowledge.js +74 -1
- package/src/stages/plan-postcheck.js +352 -1
- package/src/stages/plan.js +23 -1
- package/src/stages/quick.js +10 -2
- package/src/stages/verify.js +4 -2
- package/src/sync.js +81 -23
- package/src/task-done.js +181 -0
- package/src/task-review.js +8 -55
- package/src/test-bindings.js +549 -0
- package/src/timeline.js +218 -0
- package/src/ui-visual.js +175 -0
- package/src/verify-draft.js +214 -0
- package/src/verify-postcheck.js +860 -158
- package/src/verify-probes.js +59 -3
- package/src/watcher.js +1031 -0
- package/src/worktree-apply.js +44 -4
- package/src/worktree-deps.js +22 -8
- package/src/worktree-policy.js +42 -0
- package/src/worktree.js +102 -15
- package/templates/agents-instruction.md +37 -18
package/src/foreign-declared.js
CHANGED
|
@@ -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
|
|
134
|
-
|
|
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
|
-
|
|
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
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|