sillyspec 3.31.0 → 3.32.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 +10 -6
- package/.claude/skills/sillyspec-quick/SKILL.md +105 -7
- package/.zcodeignore +66 -0
- package/AGENTS.md +16 -16
- package/assets/command-cards/flow.md +9 -3
- package/assets/command-cards/run-quick.md +4 -1
- package/package.json +2 -2
- package/src/agent-session-log.js +27 -5
- package/src/config-schema.js +21 -0
- package/src/datetime.js +51 -0
- package/src/db.js +10 -3
- package/src/decision-distill.js +21 -12
- package/src/design-facts.js +46 -6
- package/src/flow-draft.js +468 -95
- package/src/flow-parity.js +62 -4
- package/src/flow-review.js +136 -6
- package/src/flow.js +729 -125
- package/src/foreign-declared.js +71 -3
- package/src/fr-index.js +148 -162
- package/src/git-helper.js +16 -3
- package/src/hooks/worktree-guard.js +16 -5
- package/src/hunk-attribution.js +28 -5
- package/src/index.js +186 -19
- package/src/knowledge-classify.js +3 -1
- package/src/knowledge-digest.js +55 -26
- package/src/knowledge-match.js +135 -7
- package/src/knowledge-stats.js +66 -4
- package/src/knowledge-vector.js +160 -0
- package/src/machine-interface.js +90 -2
- package/src/module-impact.js +22 -1
- package/src/modules.js +9 -0
- package/src/progress/change-registry.js +20 -0
- package/src/progress/stage-machine.js +4 -7
- package/src/progress.js +8 -1
- package/src/quicklog.js +20 -0
- package/src/redomain.js +23 -7
- package/src/route-hindsight.js +276 -0
- package/src/run/archive-distill.js +3 -1
- package/src/run/command.js +65 -12
- package/src/run/complete-handlers.js +219 -5
- package/src/run/complete.js +77 -11
- package/src/run/gate-snapshot.js +15 -0
- package/src/run/gates.js +60 -4
- package/src/run/prompt.js +159 -31
- package/src/run/quick-audit.js +66 -7
- package/src/run/shared.js +6 -11
- package/src/run/stage.js +5 -1
- package/src/run/verify-quality-scan.js +84 -0
- package/src/scan-facts.js +2 -1
- package/src/scope-audit.js +9 -2
- package/src/sentinel-assertions.js +67 -16
- package/src/stage-contract-engine.js +4 -1
- package/src/stage-contract-spec.js +103 -4
- package/src/stage-contract.js +55 -7
- package/src/stages/brainstorm.js +27 -14
- package/src/stages/knowledge.js +4 -3
- package/src/stages/plan-postcheck.js +98 -3
- package/src/stages/plan.js +22 -2
- package/src/stages/quick.js +1 -1
- package/src/stages/verify.js +2 -1
- package/src/task-review.js +1 -1
- package/src/task-tick.js +121 -0
- package/src/taskcard-frontmatter.js +31 -0
- package/src/ui-visual.js +43 -8
- package/src/verify-postcheck.js +509 -37
- package/src/verify-probes.js +216 -5
- package/src/watcher.js +1067 -1031
- package/src/worktree-apply.js +9 -1
- package/src/worktree.js +21 -6
- package/templates/agents-instruction.md +15 -15
- package/templates/prompts/taskcard-rules.md +1 -0
package/src/foreign-declared.js
CHANGED
|
@@ -202,6 +202,70 @@ function loadOwnDeclaredSet(specBase, runtimeRoot, currentChangeName) {
|
|
|
202
202
|
return set
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
/**
|
|
206
|
+
* 提交标题 → 变更名数组(2026-10-05-diff-commit-attribution):项目提交惯例 message 尾部
|
|
207
|
+
* 携带 "(2026-MM-DD-名)" 后缀(多变更并列各自提取;归档提交 chore(archive): <名> 的标题
|
|
208
|
+
* 首部同名形态不提取——归档提交触碰的是治理面移动,交付文件归属看交付提交)。
|
|
209
|
+
*/
|
|
210
|
+
export function parseChangeNamesFromSubject(subject) {
|
|
211
|
+
const out = []
|
|
212
|
+
const re = /\((\d{4}-\d{2}-\d{2}-[A-Za-z0-9._-]+)\)/g
|
|
213
|
+
let m
|
|
214
|
+
while ((m = re.exec(String(subject || '')))) out.push(m[1])
|
|
215
|
+
return out
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* git log 文本(--format=%x00%s --name-only,每提交 NUL+标题+文件行)→ 文件提交归属映射。
|
|
220
|
+
* 每文件聚合:owners=触碰过它的全部提交携带的变更名;unknown=被无变更名裸提交触碰过
|
|
221
|
+
* (归属不明,消费端必须保守保留——fail-closed 不因裸提交剔文件)。
|
|
222
|
+
*/
|
|
223
|
+
export function buildCommitAttribution(logText) {
|
|
224
|
+
const map = new Map()
|
|
225
|
+
for (const block of String(logText || '').split('\x00')) {
|
|
226
|
+
const lines = block.split('\n').map((l) => l.trim()).filter(Boolean)
|
|
227
|
+
if (lines.length === 0) continue
|
|
228
|
+
const names = parseChangeNamesFromSubject(lines[0])
|
|
229
|
+
for (const f of lines.slice(1)) {
|
|
230
|
+
const file = f.replace(/\\/g, '/')
|
|
231
|
+
let e = map.get(file)
|
|
232
|
+
if (!e) { e = { owners: new Set(), unknown: false }; map.set(file, e) }
|
|
233
|
+
if (names.length === 0) e.unknown = true
|
|
234
|
+
else for (const n of names) e.owners.add(n)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return map
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* 提交事实归属切分(2026-10-05-diff-commit-attribution):声明面(collectForeignDeclaredFiles
|
|
242
|
+
* ②扫描)对已归档他侧变更不可见——他会话在 baseline..HEAD 窗口内交付并归档后目录进
|
|
243
|
+
* archive/(「archive/ 下不算」),其文件漏归本变更 → patch 冻结面夹带 + FR 域路由触达
|
|
244
|
+
* 他侧域(双 P1 实证)。按提交 message 变更名归属是已发生的提交事实,不是声明抢文件——
|
|
245
|
+
* 不触碰否决决策 sentinel-evidence-freeze⑤(其禁的是陈旧声明的意图抢夺)。
|
|
246
|
+
* baseline 自取:opts.baselineCommit 优先,缺省读当前变更 flow-state.yaml(调用点零改动)。
|
|
247
|
+
* @returns {Map|null} null=不可得(非 git 仓/无 baseline/git 失败)——消费端退化现状。
|
|
248
|
+
*/
|
|
249
|
+
export function commitAttributionForChange(cwd, specBase, currentChangeName, baselineCommit) {
|
|
250
|
+
try {
|
|
251
|
+
let base = baselineCommit || null
|
|
252
|
+
if (!base && currentChangeName) {
|
|
253
|
+
const st = readFileSync(join(specBase, 'changes', currentChangeName, 'flow-state.yaml'), 'utf8')
|
|
254
|
+
const m = /^baseline_commit:\s*(\S+)/m.exec(st)
|
|
255
|
+
if (m) base = m[1]
|
|
256
|
+
}
|
|
257
|
+
if (!base) return null
|
|
258
|
+
const r = safeGit(cwd, ['log', '--name-only', '--format=%x00%s', `${base}..HEAD`], { trim: false })
|
|
259
|
+
if (r.value === null) return null
|
|
260
|
+
return buildCommitAttribution(r.value)
|
|
261
|
+
} catch { return null }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** 纯他侧文件判定:有提交归属且全部提交均属他侧(无本变更名、无裸提交触碰)。 */
|
|
265
|
+
export function isForeignByCommit(entry, currentChangeName) {
|
|
266
|
+
return !!entry && !entry.unknown && entry.owners.size > 0 && !entry.owners.has(currentChangeName)
|
|
267
|
+
}
|
|
268
|
+
|
|
205
269
|
/**
|
|
206
270
|
* 按他者声明集切分 diff 文件(collectForeignDeclaredFiles 的消费端快捷封装)。
|
|
207
271
|
*
|
|
@@ -214,7 +278,7 @@ function loadOwnDeclaredSet(specBase, runtimeRoot, currentChangeName) {
|
|
|
214
278
|
* @param {string} cwd 项目根
|
|
215
279
|
* @param {string|null} currentChangeName 本变更名
|
|
216
280
|
* @param {string[]|null} diffFiles 待切分文件列表(null 透传,保持调用方 null 语义)
|
|
217
|
-
* @param {{ specBase?: string, runtimeRoot?: string }} [opts] 平台模式路径(透传 collectForeignDeclaredFiles)
|
|
281
|
+
* @param {{ specBase?: string, runtimeRoot?: string, baselineCommit?: string }} [opts] 平台模式路径(透传 collectForeignDeclaredFiles)+ 显式 baseline(缺省自取 flow-state)
|
|
218
282
|
* @returns {{ own: string[]|null, foreign: Array<{file: string, owners: string[]}> }}
|
|
219
283
|
*/
|
|
220
284
|
export function splitOwnVsForeignDiffFiles(cwd, currentChangeName, diffFiles, opts = {}) {
|
|
@@ -222,7 +286,9 @@ export function splitOwnVsForeignDiffFiles(cwd, currentChangeName, diffFiles, op
|
|
|
222
286
|
const specBase = opts.specBase || join(cwd, '.sillyspec')
|
|
223
287
|
const runtimeRoot = opts.runtimeRoot || join(specBase, '.runtime')
|
|
224
288
|
const foreignMap = collectForeignDeclaredFiles(cwd, currentChangeName, { specBase, runtimeRoot })
|
|
225
|
-
|
|
289
|
+
// 提交事实归属补位(声明面归档盲区):优先级 ownDeclared > 声明面 foreign > 提交归属 foreign > own
|
|
290
|
+
const commitAttr = commitAttributionForChange(cwd, specBase, currentChangeName, opts.baselineCommit)
|
|
291
|
+
if (foreignMap.size === 0 && !commitAttr) return { own: diffFiles, foreign: [] }
|
|
226
292
|
const ownDeclared = loadOwnDeclaredSet(specBase, runtimeRoot, currentChangeName)
|
|
227
293
|
const own = []
|
|
228
294
|
const foreign = []
|
|
@@ -230,7 +296,9 @@ export function splitOwnVsForeignDiffFiles(cwd, currentChangeName, diffFiles, op
|
|
|
230
296
|
const n = String(f).replace(/\\/g, '/')
|
|
231
297
|
if (ownDeclared.size > 0 && ownDeclared.has(n)) { own.push(f); continue } // own 优先
|
|
232
298
|
const owners = foreignMap.get(n)
|
|
233
|
-
if (owners) foreign.push({ file: f, owners })
|
|
299
|
+
if (owners) { foreign.push({ file: f, owners }); continue }
|
|
300
|
+
const ce = commitAttr ? commitAttr.get(n) : null
|
|
301
|
+
if (isForeignByCommit(ce, currentChangeName)) foreign.push({ file: f, owners: [...ce.owners] })
|
|
234
302
|
else own.push(f)
|
|
235
303
|
}
|
|
236
304
|
return { own, foreign }
|
package/src/fr-index.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { readChangeTrace, upsertFrBindings, applySupersededToEntryLines, testAnchorFile, resolveTestFileRel } from './test-bindings.js'
|
|
21
|
-
import { suggestDomainFromFiles } from './knowledge-digest.js'
|
|
21
|
+
import { suggestDomainFromFiles, snapDomainToDictionary } from './knowledge-digest.js'
|
|
22
22
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'fs';
|
|
23
23
|
import { basename, dirname, join } from 'path';
|
|
24
24
|
import { writeAtomicSync } from './fs-atomic.js';
|
|
@@ -79,7 +79,7 @@ export function frCoverageFiles({ archiveRoot, changeName }) {
|
|
|
79
79
|
/** 条目「测试绑定:」子块的 tests 文件提取(多文件 | 分隔)。边界=下一个 ## 节头(FR 节)——
|
|
80
80
|
* 子块内顶格的 `<!-- test-bindings -->` 机器注释与 `- row:` 行不终止收集(评审 P1 修复:
|
|
81
81
|
* 原「任意顶格行 break」遇子块首行顶格注释即断,生产库 bindings 恒空;缩进 tests: 键名 +
|
|
82
|
-
* 节头边界已足够防条目正文误匹配)。readActiveFrDigest.bindings 与
|
|
82
|
+
* 节头边界已足够防条目正文误匹配)。readActiveFrDigest.bindings 与 activeFrCoverageHits 共用。 */
|
|
83
83
|
function readEntryBindings(lines) {
|
|
84
84
|
const idx = lines.findIndex((l) => l.startsWith('测试绑定:'));
|
|
85
85
|
if (idx === -1) return [];
|
|
@@ -229,18 +229,26 @@ export function parseChangeRequirements(changeDir) {
|
|
|
229
229
|
* unmapped 大池且 INDEX 路由键为通用词无法命中,模块卡覆盖不足时 FR 复利断路);无文件清单 → unmapped。
|
|
230
230
|
* filesOverride(2026-09-22-thin-fr-distill-sync):显式交付文件清单旁路 design.md 解析——轻量变更
|
|
231
231
|
* 变更无 design.md(轻量工件面三件套),flow done 以基线以来交付 diff 供清单,伪域路由同口径。 */
|
|
232
|
-
|
|
232
|
+
/** 路径 ↔ 模块匹配规则(精确或目录前缀)——resolveTouchedDomains 域路由与 flow start 在场
|
|
233
|
+
* 过滤(坑 module-map-list-leak)共用同一判定的单一来源。@returns {string[]} 命中模块 id */
|
|
234
|
+
export function matchedModuleIds(moduleIndex, filePath) {
|
|
235
|
+
if (!moduleIndex) return [];
|
|
236
|
+
const hits = [];
|
|
237
|
+
for (const [modId, mod] of Object.entries(moduleIndex)) {
|
|
238
|
+
const paths = (mod && Array.isArray(mod.paths)) ? mod.paths : [];
|
|
239
|
+
if (paths.some((pp) => filePath === pp || filePath.startsWith(pp.endsWith('/') ? pp : pp + '/'))) {
|
|
240
|
+
hits.push(modId);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return hits;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function resolveTouchedDomains(changeDir, moduleIndex, filesOverride = null, knowledgeRoot = null) {
|
|
233
247
|
const domains = new Set();
|
|
234
248
|
const designPath = join(changeDir, 'design.md');
|
|
235
249
|
const files = [];
|
|
236
250
|
const matchModules = (filePath) => {
|
|
237
|
-
|
|
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
|
-
}
|
|
251
|
+
for (const modId of matchedModuleIds(moduleIndex, filePath)) domains.add(modId);
|
|
244
252
|
};
|
|
245
253
|
if (Array.isArray(filesOverride)) {
|
|
246
254
|
for (const f of filesOverride) {
|
|
@@ -261,7 +269,9 @@ export function resolveTouchedDomains(changeDir, moduleIndex, filesOverride = nu
|
|
|
261
269
|
matchModules(filePath);
|
|
262
270
|
}
|
|
263
271
|
}
|
|
264
|
-
|
|
272
|
+
// 伪域铸造过词典守卫(坑 fr-domain-suggest-typo-and-no-split-migration 缺陷1:路径段投票产出
|
|
273
|
+
// 拼写漂移段时吸附既有域,杜绝 auto-rontend 类伪域凭空出生;knowledgeRoot 缺席=无词典不干预)。
|
|
274
|
+
if (domains.size === 0) domains.add(files.length > 0 ? pseudoDomainFromPaths(files, knowledgeRoot) : 'unmapped');
|
|
265
275
|
return [...domains];
|
|
266
276
|
}
|
|
267
277
|
|
|
@@ -273,7 +283,7 @@ const GENERIC_PATH_SEGMENTS = new Set(['src', 'lib', 'test', 'tests', 'spec', 'd
|
|
|
273
283
|
* 按出现次数投票取众数;全部泛化/根文件 → 'unmapped'。auto- 前缀保证与真实模块 id
|
|
274
284
|
* (模块卡派生)不冲突,且在 fr 文件头与 INDEX 路由键里自明身份。
|
|
275
285
|
*/
|
|
276
|
-
function pseudoDomainFromPaths(files) {
|
|
286
|
+
function pseudoDomainFromPaths(files, knowledgeRoot = null) {
|
|
277
287
|
const votes = new Map();
|
|
278
288
|
for (const f of files) {
|
|
279
289
|
const segs = String(f || '').replace(/\\/g, '/').split('/').filter(Boolean);
|
|
@@ -287,6 +297,10 @@ function pseudoDomainFromPaths(files) {
|
|
|
287
297
|
}
|
|
288
298
|
if (votes.size === 0) return 'unmapped';
|
|
289
299
|
const top = [...votes.entries()].sort((a, b) => b[1] - a[1])[0][0];
|
|
300
|
+
// 词典守卫(坑 fr-domain-suggest 缺陷1):top 与既有域编辑距离 ≤1 → 直接落既有域
|
|
301
|
+
//(fr/<既有域>.md 已在场,条目并入真域而非另铸 auto- 拼写漂移壳);远距离=绿地新模块照旧。
|
|
302
|
+
const snapped = snapDomainToDictionary(top, { knowledgeRoot });
|
|
303
|
+
if (snapped) return snapped;
|
|
290
304
|
return `auto-${top}`;
|
|
291
305
|
}
|
|
292
306
|
|
|
@@ -333,6 +347,9 @@ function renderFrLines(entry, headHash) {
|
|
|
333
347
|
// (design 接口定义示例中的「来源变更」即本字段,语义等价,取解析器认的字面)
|
|
334
348
|
lines.push(`变更:${entry.change}`);
|
|
335
349
|
lines.push(`状态:${entry.supersededBy ? 'superseded' : 'active'}`);
|
|
350
|
+
// 骨架标记(2026-10-03-fr-skeleton-gate):纯骨架条目(全部场景体 Then=flow-draft 占位句)
|
|
351
|
+
// 落「骨架:thin」——注入面据此排除(TierA 覆盖命中例外);查重/rot/绑定面不受影响。
|
|
352
|
+
if (entry.skeleton) lines.push('骨架:thin');
|
|
336
353
|
if (entry.supersededBy) lines.push(`superseded_by:${entry.supersededBy}`);
|
|
337
354
|
if (entry.supersededOf) lines.push(`取代链:${entry.supersededOf} ← 本条目(${entry.change} 承接)`);
|
|
338
355
|
lines.push(`摘要:${(entry.scenarios || []).slice(0, 5).join(';') || '(无场景名)'}`);
|
|
@@ -414,7 +431,7 @@ export function indexRequirements({ changeDir, knowledgeRoot, headHash = '', del
|
|
|
414
431
|
|
|
415
432
|
const warnings = [...parsed.malformed];
|
|
416
433
|
const moduleIndex = discoverModuleIndex(knowledgeRoot);
|
|
417
|
-
const domains = resolveTouchedDomains(changeDir, moduleIndex, deliverableFiles);
|
|
434
|
+
const domains = resolveTouchedDomains(changeDir, moduleIndex, deliverableFiles, knowledgeRoot);
|
|
418
435
|
const all = scanAllDomains(knowledgeRoot);
|
|
419
436
|
|
|
420
437
|
// 幂等闸门:任一域文件已有本变更「来源变更」条目 → 全量 no-op(同变更重跑零新增零漂移)
|
|
@@ -456,7 +473,7 @@ export function indexRequirements({ changeDir, knowledgeRoot, headHash = '', del
|
|
|
456
473
|
id,
|
|
457
474
|
title: fr.title,
|
|
458
475
|
change: changeName,
|
|
459
|
-
lines: renderFrLines({ id, title: fr.title, change: changeName, scenarios: fr.scenarios, decisions: fr.decisions, scenarioBodies: fr.scenarioBodies, local: fr.local }, headHash),
|
|
476
|
+
lines: renderFrLines({ id, title: fr.title, change: changeName, scenarios: fr.scenarios, decisions: fr.decisions, scenarioBodies: fr.scenarioBodies, local: fr.local, skeleton: isThinSkeletonBodies(fr.scenarioBodies) }, headHash),
|
|
460
477
|
});
|
|
461
478
|
dirtyDomains.add(primaryDomain);
|
|
462
479
|
written.push({ file: `${FR_DIR}/${primaryDomain}.md`, id, action: 'added' });
|
|
@@ -467,11 +484,9 @@ export function indexRequirements({ changeDir, knowledgeRoot, headHash = '', del
|
|
|
467
484
|
const target = st2.sections.find((s) => s.number === refId && !s.lines.some((l) => l.startsWith('superseded_by:')));
|
|
468
485
|
if (!target) continue;
|
|
469
486
|
// 就地补丁:状态翻 superseded + 插 superseded_by + 追取代链注记 + 刷新最近确认(摘要/标题保留)
|
|
470
|
-
// 待复核行随承接清除(2026-09-20-quick-asset-tail D-003:quick 触达留下的 needs_review 信号
|
|
471
|
-
// 在条目被正式承接取代时一并清——取代即复核完成,语义自然闭环)
|
|
472
487
|
target.lines = target.lines
|
|
473
488
|
.map((l) => (l.startsWith('状态:') ? `状态:superseded` : l.startsWith('最近确认:') ? `最近确认:${headHash || ''}` : l))
|
|
474
|
-
.filter((l) => !l.startsWith('superseded_by:') && !l.startsWith('取代链:') && !l.startsWith('退役理由:')
|
|
489
|
+
.filter((l) => !l.startsWith('superseded_by:') && !l.startsWith('取代链:') && !l.startsWith('退役理由:'));
|
|
475
490
|
const stateIdx = target.lines.findIndex((l) => l.startsWith('状态:'));
|
|
476
491
|
// 退役理由(追平刀②):承接令牌带(退役理由:…)时写进被取代条目——对标 OpenSpec REMOVED
|
|
477
492
|
// 的 Migration 语义;无理由则省略行(不伪造)。
|
|
@@ -522,8 +537,10 @@ export function indexRequirements({ changeDir, knowledgeRoot, headHash = '', del
|
|
|
522
537
|
.filter((s) => !s.lines.some((l) => l.startsWith('superseded_by:')))
|
|
523
538
|
.map((s) => s.number)
|
|
524
539
|
.filter((n) => n && !written.some((w) => w.id === n));
|
|
525
|
-
const
|
|
526
|
-
|
|
540
|
+
const unreferencedIds = activeIds.filter((id) => !refIds.has(id));
|
|
541
|
+
// ids(2026-10-03-fr-governance-telemetry):条目级信号帽 20——archive-distill 透传进事件,
|
|
542
|
+
// stats 裁决候选视图按 id 聚合(域级计数保留兼容)。
|
|
543
|
+
if (unreferencedIds.length > 0) unreferenced.push({ domain, count: unreferencedIds.length, ids: unreferencedIds.slice(0, 20) });
|
|
527
544
|
}
|
|
528
545
|
|
|
529
546
|
// 落盘(dirty 集=新条目域 ∪ 翻链命中域——R1 审查阻断②:漏翻链域会返回值谎报成功但盘上丢失)
|
|
@@ -576,7 +593,7 @@ export function indexRequirements({ changeDir, knowledgeRoot, headHash = '', del
|
|
|
576
593
|
if (pseudoHits.length > 0) {
|
|
577
594
|
// 落域机械改进(2026-09-27-knowledge-digest):伪域告警附交付路径推导的建议域——
|
|
578
595
|
// 归档时转达人工 1 秒确认(建议器与 digest 信号卡共用 suggestDomainFromFiles 单源)
|
|
579
|
-
const suggested = suggestDomainFromFiles(deliverableFiles || [])
|
|
596
|
+
const suggested = suggestDomainFromFiles(deliverableFiles || [], { knowledgeRoot })
|
|
580
597
|
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
598
|
if (suggested) console.warn(` 迁移/存量清单:sillyspec knowledge digest(伪域信号卡)`)
|
|
582
599
|
}
|
|
@@ -775,22 +792,118 @@ export function readActiveFrDigest(knowledgeRoot, domains) {
|
|
|
775
792
|
const decisionLine = s.lines.find((l) => l.startsWith('依据决策:'));
|
|
776
793
|
const decisions = decisionLine ? decisionLine.replace(/^依据决策:s*/, '').split('、').map((x) => x.trim()).filter(Boolean) : [];
|
|
777
794
|
const scenarios = scenarioLine ? scenarioLine.replace(/^摘要:\s*/, '').split(';').map((x) => x.trim()).filter(Boolean) : [];
|
|
778
|
-
//
|
|
779
|
-
//
|
|
780
|
-
const
|
|
781
|
-
const needsReview = reviewLine ? reviewLine.replace(/^待复核:\s*/, '').trim() : null;
|
|
795
|
+
// 骨架位(2026-10-03-fr-skeleton-gate):「骨架:thin」行在场 → skeleton=true——纯增量字段,
|
|
796
|
+
// 注入面消费(TierA 例外);查重/rot/绑定消费方不读此位照旧。
|
|
797
|
+
const skeleton = s.lines.some((l) => l.startsWith('骨架:thin'));
|
|
782
798
|
// bindings(fr-rot-precision):条目测试绑定的 test 文件——rot coverage 三源之一;纯新增
|
|
783
799
|
// 字段,既有消费方(prompt.js 注入渲染等)不受影响。
|
|
784
|
-
out.push({ domain, id: s.number, title: s.title || '', change: s.change || '', scenarios, decisions,
|
|
800
|
+
out.push({ domain, id: s.number, title: s.title || '', change: s.change || '', scenarios, decisions, bindings: readEntryBindings(s.lines), unconfirmed: readEntryUnconfirmed(s.lines), skeleton });
|
|
785
801
|
}
|
|
786
802
|
}
|
|
787
803
|
return out;
|
|
788
804
|
}
|
|
789
805
|
|
|
806
|
+
// ── 骨架信息量门(2026-10-03-fr-skeleton-gate)─────────────────────────────────────
|
|
807
|
+
// 薄道机器预填的空 GWT 骨架(When=标题回显、Then=占位句)永久入索引,挤占注入席位——
|
|
808
|
+
// 判据锚定 flow-draft 占位句字面量,标记(骨架:thin)+ 存量回填 + 注入面排除(TierA 例外)。
|
|
809
|
+
// 只装阀门不删数据:查重/rot/测试绑定/承接面零改动。
|
|
810
|
+
|
|
811
|
+
/** flow-draft draftGwtSkeleton 的 Then 兜底占位句(字面量锚点——判据与起草端同源防漂移)。 */
|
|
812
|
+
export const SKELETON_THEN_PLACEHOLDER = '行为符合本条标准描述';
|
|
813
|
+
|
|
814
|
+
/** 纯骨架判据:全部场景体的 Then 均为占位句 → true;任一实质 Then 或无场景体 → false
|
|
815
|
+
* (保守,宁漏勿误杀:误标只影响注入可见性,漏标只是继续占席)。 */
|
|
816
|
+
export function isThinSkeletonBodies(scenarioBodies) {
|
|
817
|
+
const bodies = (Array.isArray(scenarioBodies) ? scenarioBodies : []).filter((b) => b && (b.given || b.when || b.then))
|
|
818
|
+
if (bodies.length === 0) return false
|
|
819
|
+
return bodies.every((b) => String(b.then || '').trim() === SKELETON_THEN_PLACEHOLDER)
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/** 条目 场景正文 行的 Then 段提取(lastIndexOf——When 文本含「Then」字样不误切)。 */
|
|
823
|
+
function scenarioLineThen(line) {
|
|
824
|
+
const i = line.lastIndexOf('Then ')
|
|
825
|
+
return i < 0 ? null : line.slice(i + 5).trim()
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/** 存量回填(幂等):全域扫描,纯骨架(场景正文行的 Then 全为占位句)且未标 → 状态行后插
|
|
829
|
+
* 「骨架:thin」。已有标记/非骨架/无场景体零变更;二次执行零写盘。
|
|
830
|
+
* @returns {{ marked: Array<{domain,id}>, files: string[] }} */
|
|
831
|
+
export function markSkeletonThin(knowledgeRoot) {
|
|
832
|
+
const marked = []
|
|
833
|
+
const files = []
|
|
834
|
+
const all = scanAllDomains(knowledgeRoot)
|
|
835
|
+
for (const [domain, st] of all.entries()) {
|
|
836
|
+
if (!st.sections || st.sections.length === 0) continue
|
|
837
|
+
let dirty = false
|
|
838
|
+
for (const s of st.sections) {
|
|
839
|
+
if (s.lines.some((l) => l.startsWith('骨架:thin'))) continue
|
|
840
|
+
const scenarioLines = s.lines.filter((l) => l.startsWith('- 场景:'))
|
|
841
|
+
if (scenarioLines.length === 0) continue
|
|
842
|
+
if (!scenarioLines.every((l) => scenarioLineThen(l) === SKELETON_THEN_PLACEHOLDER)) continue
|
|
843
|
+
const stateIdx = s.lines.findIndex((l) => l.startsWith('状态:'))
|
|
844
|
+
if (stateIdx < 0) continue
|
|
845
|
+
s.lines.splice(stateIdx + 1, 0, '骨架:thin')
|
|
846
|
+
dirty = true
|
|
847
|
+
marked.push({ domain, id: s.number || s.id })
|
|
848
|
+
}
|
|
849
|
+
if (dirty) {
|
|
850
|
+
writeFileSync(join(frDirPath(knowledgeRoot), `${domain}.md`), joinKnowledgeFile(st.preamble, st.sections))
|
|
851
|
+
files.push(`${FR_DIR}/${domain}.md`)
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
return { marked, files }
|
|
855
|
+
}
|
|
856
|
+
|
|
790
857
|
// ── active FR 覆盖命中查询(2026-09-26-dynamic-test-inference)─────────────────────
|
|
791
|
-
// 查询核单源双消费:flow 侧 rotSuspectFlow
|
|
792
|
-
// collectFrLinkedTests
|
|
793
|
-
//
|
|
858
|
+
// 查询核单源双消费:flow 侧 rotSuspectFlow 出收口 advisory 与遥测(信号面);verify 侧
|
|
859
|
+
// collectFrLinkedTests 反用为「需求关联回归测试面」(测试门三源之二);2026-10-03 起
|
|
860
|
+
// rankFrDigestForInjection 作第三消费方(注入排序 TierA)。覆盖判定与 rot 完全同口径:
|
|
861
|
+
// 来源变更 patch 文件 ∪ 绑定 tests(剥用例锚)∩ 本次触碰文件 ≠ ∅。
|
|
862
|
+
|
|
863
|
+
/** 覆盖三分判定(2026-10-03-fr-inject-relevance-rank 自 activeFrCoverageHits 抽取):
|
|
864
|
+
* active FR × 触碰文件 → hits(覆盖相交)/ skip(覆盖可判无交集)/ unknown(覆盖源缺失)。
|
|
865
|
+
* 判据逐字不变——三消费方(rot/测试门/注入排序)共用同一实现,防口径漂移。
|
|
866
|
+
* covCache 供调用方跨查询复用归档读取。 */
|
|
867
|
+
function partitionActiveByCoverage({ archiveRoot, frs, changed, covCache = new Map() }) {
|
|
868
|
+
const hits = []
|
|
869
|
+
const unknownSources = new Set()
|
|
870
|
+
let skip = 0
|
|
871
|
+
let unknownFrCount = 0
|
|
872
|
+
for (const f of frs) {
|
|
873
|
+
if (!covCache.has(f.change)) covCache.set(f.change, frCoverageFiles({ archiveRoot, changeName: f.change }))
|
|
874
|
+
// bindings 可携带用例锚(2026-09-26-binding-anchor-fidelity)——覆盖判定按文件面取值走剥锚
|
|
875
|
+
const cov = new Set([...(covCache.get(f.change) || []), ...(Array.isArray(f.bindings) ? f.bindings.map((b) => testAnchorFile(b)) : [])])
|
|
876
|
+
if (cov.size === 0) { unknownSources.add(f.change || '(无来源变更)'); unknownFrCount++; continue }
|
|
877
|
+
const hit = [...cov].some((p) => changed.some((c) => c === p || c.startsWith(p.endsWith('/') ? p : p + '/')))
|
|
878
|
+
if (hit) hits.push({ domain: f.domain, id: f.id, title: f.title, change: f.change, bindings: f.bindings || [], coverage: cov })
|
|
879
|
+
else skip++
|
|
880
|
+
}
|
|
881
|
+
return { hits, skip, unknownFrCount, unknownSources: [...unknownSources] }
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* 注入排序(2026-10-03-fr-inject-relevance-rank):取代「文件序前 N」——域增长后注入面恒为
|
|
886
|
+
* 每域最老 N 条,新立规格与覆盖命中的老规格均不可见(平台 backend 域 480 条实证:9 月新立
|
|
887
|
+
* 455 条永不出现在注入里)。两档:TierA=覆盖命中(partitionActiveByCoverage 同口径)置前;
|
|
888
|
+
* TierB=其余按来源变更日期新→旧(`变更:` 字段 YYYY-MM-DD 前缀;无日期/异形来源居尾——存量
|
|
889
|
+
* 43 条非日期形态实证);同档 tie-break 全局 id 升序。纯排序不删条目,cap/指针行由消费方裁。
|
|
890
|
+
* @returns {{ ranked: Array, tierAIds: Set<string> }}
|
|
891
|
+
*/
|
|
892
|
+
export function rankFrDigestForInjection({ archiveRoot, frs, changed = [], covCache = new Map() }) {
|
|
893
|
+
const normalized = (Array.isArray(changed) ? changed : []).map((f) => String(f || '').replace(/\\/g, '/')).filter(Boolean)
|
|
894
|
+
const { hits } = partitionActiveByCoverage({ archiveRoot, frs, changed: normalized, covCache })
|
|
895
|
+
const tierAIds = new Set(hits.map((h) => h.id))
|
|
896
|
+
const dateOf = (f) => { const m = /^(\d{4}-\d{2}-\d{2})/.exec(String(f.change || '')); return m ? m[1] : '' }
|
|
897
|
+
const byRecency = (a, b) => {
|
|
898
|
+
const da = dateOf(a)
|
|
899
|
+
const db = dateOf(b)
|
|
900
|
+
if (da !== db) return da < db ? 1 : -1 // 日期新→旧;空串(无日期)居尾
|
|
901
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0
|
|
902
|
+
}
|
|
903
|
+
const tierA = frs.filter((f) => tierAIds.has(f.id)).sort(byRecency)
|
|
904
|
+
const tierB = frs.filter((f) => !tierAIds.has(f.id)).sort(byRecency)
|
|
905
|
+
return { ranked: [...tierA, ...tierB], tierAIds }
|
|
906
|
+
}
|
|
794
907
|
|
|
795
908
|
/**
|
|
796
909
|
* 触达域内 active FR 的强命中集。
|
|
@@ -803,25 +916,12 @@ export function activeFrCoverageHits({ specBase, change = null, changeDir = null
|
|
|
803
916
|
const moduleIndex = discoverModuleIndex(knowledgeRoot)
|
|
804
917
|
const changed = (Array.isArray(files) ? files : []).map((f) => String(f || '').replace(/\\/g, '/')).filter(Boolean)
|
|
805
918
|
// changeDir 仅为 design.md 兜底路由用(files 在场时不读);不可传 null——resolveTouchedDomains 无条件 join
|
|
806
|
-
const domains = resolveTouchedDomains(changeDir || join(specBase, 'changes', String(change || 'x')), moduleIndex, changed).filter((d) => d !== 'unmapped')
|
|
919
|
+
const domains = resolveTouchedDomains(changeDir || join(specBase, 'changes', String(change || 'x')), moduleIndex, changed, knowledgeRoot).filter((d) => d !== 'unmapped')
|
|
807
920
|
if (domains.length === 0) return { domains: [], hits: [], unknownSources: [] }
|
|
808
921
|
const frs = readActiveFrDigest(knowledgeRoot, domains)
|
|
809
922
|
if (frs.length === 0) return { domains, hits: [], unknownSources: [] }
|
|
810
|
-
const hits =
|
|
811
|
-
|
|
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 }
|
|
923
|
+
const { hits, skip, unknownFrCount, unknownSources } = partitionActiveByCoverage({ archiveRoot, frs, changed })
|
|
924
|
+
return { domains, hits, unknownSources, unknownFrCount, skip, total: frs.length }
|
|
825
925
|
}
|
|
826
926
|
|
|
827
927
|
/**
|
|
@@ -871,55 +971,10 @@ export function frTitleOverlap(a, b) {
|
|
|
871
971
|
return inter / (A.size + B.size - inter);
|
|
872
972
|
}
|
|
873
973
|
|
|
874
|
-
// ── needs_review
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
/**
|
|
880
|
-
* quick 触达域后给 active FR 条目打待复核标记(钩子#1 升级:遥测之外让地图本身诚实)。
|
|
881
|
-
* 幂等:条目已有含同 ref 的待复核行 → 跳过;superseded 条目不标(digest 注入面本就不含)。
|
|
882
|
-
* 只动文件不改语义——行是信号,承接翻链时被 indexRequirements 的 filter 清除。
|
|
883
|
-
* @param {string} knowledgeRoot
|
|
884
|
-
* @param {string[]} frIds - 全局 id 列表(钩子#1 的 readActiveFrDigest 命中集)
|
|
885
|
-
* @param {string} refNote - 标记来源(ql-id 或 change 名)
|
|
886
|
-
* @returns {{ marked: number, warnings: string[] }}
|
|
887
|
-
*/
|
|
888
|
-
export function markFrNeedsReview(knowledgeRoot, frIds, refNote) {
|
|
889
|
-
const warnings = [];
|
|
890
|
-
let marked = 0;
|
|
891
|
-
if (!Array.isArray(frIds) || frIds.length === 0 || !refNote) return { marked, warnings };
|
|
892
|
-
const all = scanAllDomains(knowledgeRoot);
|
|
893
|
-
const idSet = new Set(frIds);
|
|
894
|
-
const dirty = new Set();
|
|
895
|
-
for (const [domain, st] of all.entries()) {
|
|
896
|
-
for (const s of st.sections) {
|
|
897
|
-
if (!idSet.has(s.number)) continue;
|
|
898
|
-
if (s.lines.some((l) => l.startsWith('superseded_by:'))) continue; // 已取代不标
|
|
899
|
-
const line = `${FR_NEEDS_REVIEW_PREFIX}${refNote}`;
|
|
900
|
-
if (s.lines.some((l) => l === line)) continue; // 幂等:同 ref 已标
|
|
901
|
-
// 移除旧 ref 的待复核行(多轮标记仅保最新),追加新行到摘要行后
|
|
902
|
-
s.lines = s.lines.filter((l) => !l.startsWith(FR_NEEDS_REVIEW_PREFIX));
|
|
903
|
-
const sumIdx = s.lines.findIndex((l) => l.startsWith('摘要:'));
|
|
904
|
-
s.lines.splice(sumIdx === -1 ? s.lines.length : sumIdx + 1, 0, line);
|
|
905
|
-
dirty.add(domain);
|
|
906
|
-
marked++;
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
if (dirty.size > 0) {
|
|
910
|
-
mkdirSync(frDirPath(knowledgeRoot), { recursive: true });
|
|
911
|
-
for (const d of dirty) {
|
|
912
|
-
const st = all.get(d);
|
|
913
|
-
writeFileSync(join(frDirPath(knowledgeRoot), `${d}.md`), joinKnowledgeFile(st.preamble, st.sections));
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
for (const id of idSet) {
|
|
917
|
-
let found = false;
|
|
918
|
-
for (const st of all.values()) if (st.sections.some((s) => s.number === id)) { found = true; break; }
|
|
919
|
-
if (!found) warnings.push(`markFrNeedsReview:${id} 不在索引中(跳过)`);
|
|
920
|
-
}
|
|
921
|
-
return { marked, warnings };
|
|
922
|
-
}
|
|
974
|
+
// ── needs_review 持久标记层(2026-09-20-quick-asset-tail D-003)已拆除 ──────────────
|
|
975
|
+
// 2026-09-29-rot-retire-inject-cap:markFrNeedsReview/FR_NEEDS_REVIEW_PREFIX/cleanupStaleReviewMarks
|
|
976
|
+
// 移除——371 条标记零消费实证(归档全量检索无一次复核行动),信号饱和反噬注入排序。腐烂 suspect
|
|
977
|
+
// 的信号面保留在 flow.js rotSuspectFlow(收口 advisory + fr-rot-suspect 遥测),不再落盘。
|
|
923
978
|
|
|
924
979
|
/**
|
|
925
980
|
* 归档侧 FR 域路由文件面(greenfield-bootstrap:archive 侧 indexRequirements 此前不传
|
|
@@ -948,72 +1003,3 @@ export function archiveDeliverableFiles(changeDir) {
|
|
|
948
1003
|
return [...out].filter((p) => !p.startsWith('.sillyspec/'))
|
|
949
1004
|
}
|
|
950
1005
|
|
|
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/git-helper.js
CHANGED
|
@@ -79,6 +79,18 @@ function buildFullArgs(cwd, args) {
|
|
|
79
79
|
// ENOBUFS 只能 git merge 绕行)——上限只在真输出那么大时才占用
|
|
80
80
|
const GIT_MAX_BUFFER = 256 * 1024 * 1024
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* CLI 自建 git 子进程的统一 env:GIT_OPTIONAL_LOCKS=0(坑 git-optional-locks,
|
|
84
|
+
* 2026-10-06-module-map-list-leak 收口 add 静默失效根因收口)——status/diff 等读命令的
|
|
85
|
+
* 机会性 index 刷新会短暂持 index.lock(watcher 每 ~3s 轮询 status 即常驻锁源),并发
|
|
86
|
+
* git add 落进窗口即失败。该开关只禁机会性锁,add/commit 等真写的必需锁不受影响;
|
|
87
|
+
* 权威注入(调用方显式值不覆盖)——「CLI 子进程恒不机会性抢锁」是统一不变量,不逐调用点分叉。
|
|
88
|
+
* 展开合并不裸替换(丢 SystemRoot/USERPROFILE/TEMP 会毁 Windows 子进程,终批-③ 同坑)。
|
|
89
|
+
*/
|
|
90
|
+
function gitChildEnv(env) {
|
|
91
|
+
return { ...(env || process.env), GIT_OPTIONAL_LOCKS: '0' }
|
|
92
|
+
}
|
|
93
|
+
|
|
82
94
|
/**
|
|
83
95
|
* 安全执行 git:失败不抛,返回 { value, error } 结构。
|
|
84
96
|
* @param {string} cwd
|
|
@@ -99,7 +111,7 @@ export function safeGit(cwd, args, opts = {}) {
|
|
|
99
111
|
// 格式化(取首行)推迟到最终返回,避免重试分支重复格式化,并保留原始 code 供重试判定。
|
|
100
112
|
const attempt = (t) => {
|
|
101
113
|
try {
|
|
102
|
-
let value = execFileSync('git', fullArgs, { encoding: 'utf8', timeout: t, maxBuffer: GIT_MAX_BUFFER, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true })
|
|
114
|
+
let value = execFileSync('git', fullArgs, { encoding: 'utf8', timeout: t, maxBuffer: GIT_MAX_BUFFER, stdio: ['ignore', 'pipe', 'pipe'], env: gitChildEnv(), windowsHide: true })
|
|
103
115
|
if (trim) value = value.trim()
|
|
104
116
|
return { value, errorObj: null }
|
|
105
117
|
} catch (e) {
|
|
@@ -127,8 +139,9 @@ export function git(cwd, args, opts = {}) {
|
|
|
127
139
|
const { trim = true, timeout = 5000, encoding = 'utf8', env } = opts
|
|
128
140
|
const fullArgs = buildFullArgs(cwd, args)
|
|
129
141
|
// env(终批-③):可选注入(如 baseline checkpoint 的临时 GIT identity)——调用方必须
|
|
130
|
-
// 用 { ...process.env, GIT_xxx } 展开形式,裸替换会丢 SystemRoot/USERPROFILE/TEMP(Windows
|
|
131
|
-
|
|
142
|
+
// 用 { ...process.env, GIT_xxx } 展开形式,裸替换会丢 SystemRoot/USERPROFILE/TEMP(Windows);
|
|
143
|
+
// 最终经 gitChildEnv 展开合并并权威注入 GIT_OPTIONAL_LOCKS=0(调用方 GIT 键照常透传)
|
|
144
|
+
const value = execFileSync('git', fullArgs, { encoding, timeout, maxBuffer: GIT_MAX_BUFFER, stdio: ['ignore', 'pipe', 'pipe'], env: gitChildEnv(env), windowsHide: true })
|
|
132
145
|
if (Buffer.isBuffer(value)) return value
|
|
133
146
|
return trim ? value.trim() : value
|
|
134
147
|
}
|
|
@@ -60,7 +60,11 @@ const STAGE_HINTS = {
|
|
|
60
60
|
'没有检测到活跃的 SillySpec 流程。',
|
|
61
61
|
'你需要先启动一个任务流程才能修改源码(调用对应的 sillyspec skill):',
|
|
62
62
|
'',
|
|
63
|
-
' BUG
|
|
63
|
+
' 轻量变更/BUG修复(默认快道,skill sillyspec-flow)——--input 引号内换行合法,下例可照抄:',
|
|
64
|
+
' sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>',
|
|
65
|
+
'',
|
|
66
|
+
' 成功标准:',
|
|
67
|
+
' - <可验证标准>"',
|
|
64
68
|
' 逻辑变更(skill sillyspec-brainstorm):sillyspec run brainstorm → plan → execute → verify → archive',
|
|
65
69
|
' 全自动模式(skill sillyspec-auto):sillyspec run auto "任务描述"',
|
|
66
70
|
],
|
|
@@ -75,16 +79,23 @@ const STAGE_HINTS = {
|
|
|
75
79
|
],
|
|
76
80
|
'verify': [
|
|
77
81
|
'当前在 verify(验证)阶段,只做代码审查和测试验证,不修改源码。',
|
|
78
|
-
'如需修改,请先回到 execute
|
|
79
|
-
' sillyspec
|
|
82
|
+
'如需修改,请先回到 execute 阶段,或收口后开轻量变更(--input 引号内换行合法,可照抄):',
|
|
83
|
+
' sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>',
|
|
84
|
+
'',
|
|
85
|
+
' 成功标准:',
|
|
86
|
+
' - <可验证标准>"',
|
|
80
87
|
],
|
|
81
88
|
'archive': [
|
|
82
89
|
'当前在 archive(归档)阶段,不修改源码。',
|
|
83
|
-
'
|
|
90
|
+
'如需修改,请开启新变更(--input 引号内换行合法,可照抄):',
|
|
91
|
+
' sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>',
|
|
92
|
+
'',
|
|
93
|
+
' 成功标准:',
|
|
94
|
+
' - <可验证标准>"',
|
|
84
95
|
],
|
|
85
96
|
'explore': [
|
|
86
97
|
'当前在 explore(探索)阶段,只读不写。',
|
|
87
|
-
'确认方案后使用:sillyspec run brainstorm
|
|
98
|
+
'确认方案后使用:sillyspec run brainstorm,或小改动直接走轻量变更 sillyspec flow start',
|
|
88
99
|
],
|
|
89
100
|
}
|
|
90
101
|
|