sillyspec 3.32.9 → 3.32.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +2 -2
- package/.claude/skills/sillyspec-execute/SKILL.md +2 -2
- package/.claude/skills/sillyspec-flow/SKILL.md +1 -1
- package/.claude/skills/sillyspec-plan/SKILL.md +2 -2
- package/AGENTS.md +1 -1
- package/assets/command-cards/flow.md +1 -1
- package/package.json +1 -1
- package/src/commit-suggest.js +45 -4
- package/src/config-schema.js +14 -0
- package/src/decision-distill.js +77 -21
- package/src/decision-renumber.js +157 -0
- package/src/docs-check.js +30 -15
- package/src/docs-debt.js +3 -2
- package/src/doctor-diagnostics.js +3 -1
- package/src/flow-parity.js +18 -2
- package/src/flow.js +75 -12
- package/src/fr-index.js +15 -1
- package/src/hooks/worktree-guard.js +3 -3
- package/src/index.js +7 -5
- package/src/knowledge-digest.js +23 -6
- package/src/knowledge-graph.js +92 -14
- package/src/knowledge-match.js +34 -9
- package/src/knowledge-stats.js +12 -13
- package/src/knowledge-vector.js +98 -7
- package/src/machine-interface.js +26 -0
- package/src/quicklog.js +6 -1
- package/src/run/archive-distill.js +3 -0
- package/src/run/command.js +3 -3
- package/src/run/complete-handlers.js +8 -3
- package/src/run/complete.js +113 -44
- package/src/run/gates.js +93 -89
- package/src/run/prompt.js +3 -3
- package/src/run/stage.js +1 -1
- package/src/sentinel-assertions.js +43 -1
- package/src/stage-contract.js +26 -3
- package/src/stage-review.js +44 -11
- package/src/stage-templates.js +2 -0
- package/src/stages/brainstorm.js +5 -1
- package/src/stages/knowledge.js +56 -5
- package/src/sync.js +28 -2
- package/src/task-tick.js +12 -0
- package/src/timeline.js +60 -19
- package/src/verify-postcheck.js +49 -1
- package/src/verify-probes.js +61 -18
- package/templates/agents-instruction.md +1 -1
package/src/docs-debt.js
CHANGED
|
@@ -15,6 +15,7 @@ import { join, relative } from 'node:path'
|
|
|
15
15
|
import { safeGit } from './git-helper.js'
|
|
16
16
|
import { parseModuleMapSimple } from './modules.js'
|
|
17
17
|
import { runDocsCheck, readDocsCheckConfig, parseDecisionEntries, anchorFilePath } from './docs-check.js'
|
|
18
|
+
import { decisionChangeQualifier } from './knowledge-match.js'
|
|
18
19
|
|
|
19
20
|
/** safeGit 超时(大仓 git log 慢时降级该模块,不挂起 Wave 渲染) */
|
|
20
21
|
const GIT_TIMEOUT_MS = 5000
|
|
@@ -323,7 +324,7 @@ export function computeDecisionTouches(changedFiles, knowledgeRoot) {
|
|
|
323
324
|
for (const c of changed) {
|
|
324
325
|
// 前缀匹配粒度 = 文件或其子路径(R-03:锚点是文件级非目录级,防同前缀不同文件误报)
|
|
325
326
|
if (c === anchorPath || c.startsWith(anchorPath + '/')) {
|
|
326
|
-
touches.push({ id: e.id, title: e.title, anchorFile: anchorPath, touchedFile: c, file })
|
|
327
|
+
touches.push({ id: e.id, title: e.title, anchorFile: anchorPath, touchedFile: c, file, change: e.change || '' })
|
|
327
328
|
}
|
|
328
329
|
}
|
|
329
330
|
}
|
|
@@ -344,7 +345,7 @@ export function renderDecisionTouchFacts(touches) {
|
|
|
344
345
|
const shown = touches.slice(0, DECISION_TOUCH_RENDER_LIMIT)
|
|
345
346
|
const lines = [`[decision-touch] 本次变更触碰 ${touches.length} 条决策锚点(改动可能使该决策需复核):`]
|
|
346
347
|
for (const t of shown) {
|
|
347
|
-
lines.push(` - ${t.id}(${t.title || '无标题'},锚点 ${t.anchorFile})← 触碰文件 ${t.touchedFile}(${t.file})`)
|
|
348
|
+
lines.push(` - ${t.id}(${t.title || '无标题'},锚点 ${t.anchorFile})← 触碰文件 ${t.touchedFile}(${t.file}|${decisionChangeQualifier(t.change)})`)
|
|
348
349
|
}
|
|
349
350
|
if (touches.length > DECISION_TOUCH_RENDER_LIMIT) {
|
|
350
351
|
lines.push(` …另有 ${touches.length - DECISION_TOUCH_RENDER_LIMIT} 条`)
|
|
@@ -946,7 +946,9 @@ function detectDecisionDrift(cwd) {
|
|
|
946
946
|
let count = 0
|
|
947
947
|
for (const f of readdirSync(decDir)) {
|
|
948
948
|
if (!f.endsWith('.md')) continue
|
|
949
|
-
|
|
949
|
+
// 双形态计数(2026-10-14-decision-global-id-epoch):局部 D-011@v1 与全局 D-<域>-042@v1;
|
|
950
|
+
// 旧正则 @\d+ 漏 @v 前缀恒零——随本单修正
|
|
951
|
+
count += (readFileSync(join(decDir, f), 'utf8').match(/^## D-(?:[A-Za-z0-9_\-\u4e00-\u9fff]+-)?\d+@v\d+/gm) || []).length
|
|
950
952
|
}
|
|
951
953
|
return { ...base, findings: [`decisions 库 ${count} 条(behind 精确复核在 verify evidence-auto 与 docs-check 决策规则族)`], pass: true, severity: CHECK_SEVERITY.PASSED }
|
|
952
954
|
} catch (e) {
|
package/src/flow-parity.js
CHANGED
|
@@ -31,6 +31,7 @@ import { readV2SectionAnswer } from './flow-review.js'
|
|
|
31
31
|
import { DESIGN_QUESTIONS } from './flow-draft.js'
|
|
32
32
|
import { pathMatches } from './change-list.js'
|
|
33
33
|
import { classifyToolScaffold, filterDeliverableFiles } from './worktree-apply.js'
|
|
34
|
+
import { deriveChangeTitle } from './quicklog.js'
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* 模块文档对账(advisory)+ 结构化落盘面(2026-09-27-thin-module-scope-persist)。
|
|
@@ -133,6 +134,19 @@ export function evaluateModuleGate({ moduleScope, ignore } = {}) {
|
|
|
133
134
|
return { block: maps.length > 0 && dirs.length > 0, dirs, maps }
|
|
134
135
|
}
|
|
135
136
|
|
|
137
|
+
/**
|
|
138
|
+
* ⑥-0 motiveFromInput:模块变更索引摘要提取(2026-10-15-title-label-skip)。与变更标题
|
|
139
|
+
* 同源走 deriveChangeTitle——此前 flow.js 裸取 --input 首行,「动机与背景:」段头行整行
|
|
140
|
+
* 入摘要(stages.changelog 实证)。段头行跳过/标签剥离随 deriveChangeTitle 单源;提取为
|
|
141
|
+
* 空回退变更名(可辨识不达意优于空串;appendChangeIndexEntries 侧另有 '轻量变更' 二级兜底)。
|
|
142
|
+
* @returns {string} 摘要(≤50 字)
|
|
143
|
+
*/
|
|
144
|
+
export function motiveFromInput(input, change = '') {
|
|
145
|
+
let derived = ''
|
|
146
|
+
try { derived = deriveChangeTitle(input) } catch { derived = '' }
|
|
147
|
+
return derived || String(change || '')
|
|
148
|
+
}
|
|
149
|
+
|
|
136
150
|
/**
|
|
137
151
|
* ⑥ appendChangeIndexEntries:命中模块变更索引机械回写(二档)。零语义判断——日期/变更名/
|
|
138
152
|
* 动机首行全是已知量,与 decision-distill 降 noAI 同性质。目标判定遵守 split-changelog 契约
|
|
@@ -309,7 +323,9 @@ export function harvestSlot4Decision({ changeDir, change }) {
|
|
|
309
323
|
'---',
|
|
310
324
|
`# 决策记录(Decisions)— ${change}`,
|
|
311
325
|
'',
|
|
312
|
-
|
|
326
|
+
// 标题自解释(2026-10-15-harvest-title-clarify):读者面是 decisions 文档与 knowledge,
|
|
327
|
+
// 机制出处(槽4 收割)由 frontmatter author: flow-machine-draft 承载,标题只说内容与性质。
|
|
328
|
+
`## D-001@v1: 设计期风险与放弃方案(底稿)`,
|
|
313
329
|
`- 类型:process`,
|
|
314
330
|
`- 状态:confirmed`,
|
|
315
331
|
// 类型/状态字段(2026-09-26-slot4-distill-fix):蒸馏链入选要 status∈{confirmed|accepted|rejected}
|
|
@@ -326,7 +342,7 @@ export function harvestSlot4Decision({ changeDir, change }) {
|
|
|
326
342
|
return { harvested: true }
|
|
327
343
|
}
|
|
328
344
|
|
|
329
|
-
export default { reconcileModuleDocs, renderVerifyReceipt, harvestSlot4Decision, collectFreezeFiles, backfillGateSummary }
|
|
345
|
+
export default { reconcileModuleDocs, renderVerifyReceipt, harvestSlot4Decision, collectFreezeFiles, backfillGateSummary, motiveFromInput }
|
|
330
346
|
|
|
331
347
|
/**
|
|
332
348
|
* patch 冻结面的提交面过滤(2026-09-27-tool-debt-cleanup 抽出为纯函数供单测锁定):
|
package/src/flow.js
CHANGED
|
@@ -224,6 +224,23 @@ export async function extractRoutingInputPaths(cwd, specBase, input) {
|
|
|
224
224
|
})
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
/**
|
|
228
|
+
* 轻量道知识注入查询串瘦身(2026-10-15-thin-query-slim):--input 的「成功标准:」段是验收
|
|
229
|
+
* 判据语料(文件路径/测试名/断言词——对知识匹配是纯噪音,且路径词会过触路由关键词把命中面
|
|
230
|
+
* 撑爆:本仓实证整段 input 匹配出 76 条目、58 个 fr 域文件全命中)。查询只取动机段——
|
|
231
|
+
* 「成功标准:」标记前正文;标记缺席=无标准段,全文照旧(兼容旧形态);标记前为空白则退全 input。
|
|
232
|
+
* @param {string} change 变更名(查询首行,既有口径)
|
|
233
|
+
* @param {string} input --input 全文
|
|
234
|
+
* @returns {string} 瘦身后的查询串
|
|
235
|
+
*/
|
|
236
|
+
export function thinKnowledgeQuery(change, input) {
|
|
237
|
+
const raw = String(input || '')
|
|
238
|
+
const markerIdx = raw.search(/成功标准[::]/)
|
|
239
|
+
const motivation = markerIdx >= 0 ? raw.slice(0, markerIdx) : raw
|
|
240
|
+
const body = motivation.trim() || raw.trim()
|
|
241
|
+
return `${change || ''}\n${body}`
|
|
242
|
+
}
|
|
243
|
+
|
|
227
244
|
/**
|
|
228
245
|
* 轻量道知识注入段(2026-09-25-thin-fr-inject-parity):默认快道读取面对齐——
|
|
229
246
|
* {FR_INDEX_DIGEST}/{DECISION_HITS}/execute 知识命中报告此前只在 run 族装配,轻量道 fresh
|
|
@@ -295,10 +312,11 @@ export async function flowKnowledgeDigest({ specBase, change, changeDir, input,
|
|
|
295
312
|
}
|
|
296
313
|
}
|
|
297
314
|
const { matchKnowledgeHybrid } = await import('./knowledge-vector.js')
|
|
298
|
-
const { deathPathNote } = await import('./knowledge-match.js')
|
|
315
|
+
const { deathPathNote, decisionChangeQualifier } = await import('./knowledge-match.js')
|
|
299
316
|
// scope 遍历召回(2026-10-08-knowledge-graph,FR-04):touched=机器算的图查询键
|
|
300
317
|
// (filesOverride ∪ design 交付表——上方既有变量),词面三层零命中时沿强边保底防复潮
|
|
301
|
-
const
|
|
318
|
+
const kmQuery = thinKnowledgeQuery(change, input)
|
|
319
|
+
const km = await matchKnowledgeHybrid(knowledgeRoot, kmQuery, { cwd: dirname(specBase), scopeFiles: touched })
|
|
302
320
|
// 防复潮面 = 死路注记 ∪ 有主题重叠的 rejected(2026-09-28-knowledge-inject-ranking 起 rejected∪死路;
|
|
303
321
|
// 2026-09-28-knowledge-gate-denoise 收紧:score 零的 rejected 与查询零主题重叠——空标题条目靠状态
|
|
304
322
|
// 蹭进回显是行为实测三例的噪音源,不再注入)
|
|
@@ -306,20 +324,51 @@ export async function flowKnowledgeDigest({ specBase, change, changeDir, input,
|
|
|
306
324
|
summary.rejectedDecisions = rejected.length
|
|
307
325
|
summary.knowledgeEntries = km.matched ? (km.entries || []).length : 0
|
|
308
326
|
if (rejected.length > 0) {
|
|
309
|
-
lines.push(` ⚠️ 否决决策/死路注记(历史已否决或已记死路,防复潮——除非复潮条件满足勿重提,复潮须在本变更 decisions.md
|
|
327
|
+
lines.push(` ⚠️ 否决决策/死路注记(历史已否决或已记死路,防复潮——除非复潮条件满足勿重提,复潮须在本变更 decisions.md 记新版本;跨变更引用带变更名 变更名#D-xxx@vN):`)
|
|
310
328
|
for (const h of rejected.slice(0, 5)) {
|
|
311
329
|
const note = h.status === 'rejected' ? (h.reason || '(未记录)') : `死路——${deathPathNote(h.reason)}`
|
|
312
|
-
lines.push(` - ${h.id} ${h.title}(${h.file})${h.status === 'rejected' ? '否决理由' : '⚰️'}:${note}`)
|
|
330
|
+
lines.push(` - ${h.id} ${h.title}(${h.file}|${decisionChangeQualifier(h.change)})${h.status === 'rejected' ? '否决理由' : '⚰️'}:${note}`)
|
|
313
331
|
}
|
|
314
332
|
}
|
|
315
333
|
if (km.matched && (km.entries || []).length > 0) {
|
|
316
334
|
const src = km.entries.slice(0, 3).map((e) => (e.anchor ? `${e.file}#${e.anchor}` : e.file)).join(';')
|
|
317
335
|
lines.push(` 📚 知识命中(INDEX 关键词,按需 Read):${src}`)
|
|
318
336
|
}
|
|
337
|
+
// 遥测导线(2026-10-15-thin-inject-telemetry):轻量道注入此前不落 hits.jsonl——thin 常态化后
|
|
338
|
+
// inject 事件流断流(本仓 10-09 后归零实证),knowledge stats 从未命中判定被盲区污染。
|
|
339
|
+
// 分级字段(A2-lite):injectedFiles=正文实际注入(thin 面恒空——只出按需 Read 行)、
|
|
340
|
+
// mentionedFiles=显式展示给 agent 的锚点(📚 行 top-3)。既有 matchedFiles 口径不变
|
|
341
|
+
// (knowledge-stats 消费面零回归)。仅命中面非空才落盘;fail-soft(R-04)不阻断注入。
|
|
342
|
+
const thinMatchedFiles = km.matched ? (km.entries || []).map((e) => (e.anchor ? `${e.file}#${e.anchor}` : e.file)) : []
|
|
343
|
+
if (thinMatchedFiles.length > 0 || rejected.length > 0) {
|
|
344
|
+
try {
|
|
345
|
+
const { appendKnowledgeHit } = await import('./knowledge-hits.js')
|
|
346
|
+
appendKnowledgeHit(join(specBase, '.runtime'), {
|
|
347
|
+
type: 'inject',
|
|
348
|
+
change,
|
|
349
|
+
query: kmQuery,
|
|
350
|
+
matchedFiles: thinMatchedFiles,
|
|
351
|
+
injectedFiles: [],
|
|
352
|
+
mentionedFiles: thinMatchedFiles.slice(0, 3),
|
|
353
|
+
rejectedCount: rejected.length,
|
|
354
|
+
source: 'thin-start',
|
|
355
|
+
})
|
|
356
|
+
} catch { /* 遥测 fail-soft(R-04):hits 落盘失败不影响注入本体 */ }
|
|
357
|
+
}
|
|
319
358
|
if (domains.length === 0 && rejected.length === 0 && !km.matched) {
|
|
320
359
|
lines.length = 0
|
|
321
360
|
lines.push(`🧠 知识注入(轻量道读取面):语料未命中知识库(无触达域依据、无否决命中、INDEX 无对齐条目)——可自行查 knowledge/INDEX.md。`)
|
|
322
361
|
}
|
|
362
|
+
// 向量开关发现性提示(2026-10-15-vector-switch-discoverability):零命中=正需要语义召回
|
|
363
|
+
// 而未获得的时刻——平台已连接但开关未开(缺省关)时给一行可执行提示;有命中/未连接/
|
|
364
|
+
// 已开启零新增(vectorSwitchHint 单源,同款提示也挂 knowledge search hint 键)。
|
|
365
|
+
if (!km.matched && rejected.length === 0) {
|
|
366
|
+
try {
|
|
367
|
+
const { vectorSwitchHint } = await import('./knowledge-vector.js')
|
|
368
|
+
const hint = vectorSwitchHint(dirname(specBase))
|
|
369
|
+
if (hint) lines.push(` ${hint}`)
|
|
370
|
+
} catch { /* 提示 fail-open */ }
|
|
371
|
+
}
|
|
323
372
|
// 模块卡欠账提示前移(2026-10-12-thin-module-gate-and-feed 三档):触碰命中模块 behind>0
|
|
324
373
|
// 才出事实行——动手前看到「你要动的卡已落后」比收口时警告有效;computeDocsDebt(厚道
|
|
325
374
|
// execute {DOCS_DEBT})同源,空债/无图零输出,异常走外层 catch fail-open 不阻断 start。
|
|
@@ -1425,11 +1474,22 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1425
1474
|
// 采样去重(2026-10-07-thin-tasks-v3):task tick 直写的精确事件(source:'task-tick')为
|
|
1426
1475
|
// 权威口径,watcher 3s 采样合并跳被去重——CLI 快速连续 tick 不再误伤;观测旁路缺席
|
|
1427
1476
|
// (无流/读失败)→ 跳过(fail-open,watcher 非真相源);--allow-batch-tick 显式旁路留痕。
|
|
1477
|
+
// 侧车突发回退(2026-10-14-tick-burst-sidecar-gate):CLI 收口端快连点对事件流不可见(单格
|
|
1478
|
+
// 精确跳),空结果时回退 task-ticks.jsonl 毫秒时戳判突发——CLI 与直接 Edit 两条路径同判。
|
|
1428
1479
|
try {
|
|
1429
1480
|
const { readWatcherEvents } = await import('./watcher.js')
|
|
1430
|
-
const { detectBatchCheckCadence, resolveBatchTickAction } = await import('./sentinel-assertions.js')
|
|
1481
|
+
const { detectBatchCheckCadence, detectTickBurst, resolveBatchTickAction } = await import('./sentinel-assertions.js')
|
|
1431
1482
|
const stream = readWatcherEvents({ runtimeRoot, change })
|
|
1432
|
-
|
|
1483
|
+
let batch = stream.exists ? detectBatchCheckCadence(stream.events) : null
|
|
1484
|
+
if (!batch) {
|
|
1485
|
+
// 侧车突发回退(2026-10-14-tick-burst-sidecar-gate):事件流空结果时读 change 目录
|
|
1486
|
+
// task-ticks.jsonl(CLI tick 毫秒时戳事实源,随归档进 git、不受观测盲窗影响)判快连点;
|
|
1487
|
+
// 事件流结果优先不双重报告;侧车缺失/坏文件按无证据静默(fail-open)。
|
|
1488
|
+
try {
|
|
1489
|
+
const { parseTickLog } = await import('./timeline.js')
|
|
1490
|
+
batch = detectTickBurst(parseTickLog(readFileSync(join(changeDir, 'task-ticks.jsonl'), 'utf8')))
|
|
1491
|
+
} catch { /* 侧车缺失/不可读 → 无侧车证据(fail-open) */ }
|
|
1492
|
+
}
|
|
1433
1493
|
if (batch) {
|
|
1434
1494
|
// 代勾计数取「本拍实时代勾 ∨ 持久化代勾」较大者(二轮 P2-B:重入漂移防护);决策纯函数
|
|
1435
1495
|
// 单源(resolveBatchTickAction)四态与豁免优先序见其 JSDoc——代勾须解释整跳才降级
|
|
@@ -1454,7 +1514,8 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1454
1514
|
console.warn(`⚠️ 勾选节奏:单拍多格勾选(${batch.detail},${at})——${why},降级提醒不拒`)
|
|
1455
1515
|
}
|
|
1456
1516
|
} else if (v.action === 'reject') {
|
|
1457
|
-
|
|
1517
|
+
const _via = batch.source === 'tick-burst' ? '侧车时戳观测到' : '事件流观测到'
|
|
1518
|
+
console.error(`🚫 单拍勾选拒收:${_via}一拍勾选 ${batch.detail}(${at})——勾选纪律要求逐格(做一件→勾一格→下一个),勾选任务 ${_sentinelNonMirrorTasks} 格`)
|
|
1458
1519
|
console.error(' 出口:①节奏违例已既成——认知后重跑带 --allow-batch-tick 显式留痕过门(平台时间线可见旁路);②疑观测误判→sillyspec doctor 核对事件流')
|
|
1459
1520
|
appendTelemetry({ sentinel: 'batch-tick', jump: batch.detail })
|
|
1460
1521
|
reportMidFail('ledger')
|
|
@@ -2272,8 +2333,10 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
2272
2333
|
// 回读块保证在场(patch 本轮算出或 change-patch.json 回读)。
|
|
2273
2334
|
try {
|
|
2274
2335
|
if (moduleScope && Array.isArray(moduleScope.modules) && moduleScope.modules.length > 0) {
|
|
2275
|
-
const { appendChangeIndexEntries } = await import('./flow-parity.js')
|
|
2276
|
-
|
|
2336
|
+
const { appendChangeIndexEntries, motiveFromInput } = await import('./flow-parity.js')
|
|
2337
|
+
// 摘要与变更标题同源(2026-10-15-title-label-skip):段头行(动机与背景:等)跳过取次行,
|
|
2338
|
+
// 空提取回退变更名——此前裸取首行致「动机与背景:」整行入摘要(stages.changelog 实证)。
|
|
2339
|
+
const motive = motiveFromInput(st.input, change)
|
|
2277
2340
|
const wr = appendChangeIndexEntries({ specBase, modules: moduleScope.modules, change, summary: motive, date: new Date().toISOString().slice(0, 10) })
|
|
2278
2341
|
changelogAppended = wr.appended
|
|
2279
2342
|
if (wr.appended.length > 0) console.log(`📖 模块变更索引回写:${wr.appended.length} 个目标(${wr.appended.join('、')})——sidecar 优先/卡内节次之,随归档窄化入暂存`)
|
|
@@ -2600,7 +2663,7 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
|
|
|
2600
2663
|
console.error(`❌ ${e.message}`)
|
|
2601
2664
|
// 可照抄实例(2026-10-05-input-teach-copyable):引号内换行合法,行级 trim 容忍缩进
|
|
2602
2665
|
console.error(' 重试(--input 引号内换行合法,可照抄):')
|
|
2603
|
-
console.error(' sillyspec flow start --change <YYYY-MM-DD-简短描述> --input "
|
|
2666
|
+
console.error(' sillyspec flow start --change <YYYY-MM-DD-简短描述> --input "<一句中文概括(首行即变更标题);随后动机与背景')
|
|
2604
2667
|
console.error('')
|
|
2605
2668
|
console.error(' 成功标准:')
|
|
2606
2669
|
console.error(' - <可验证标准>"')
|
|
@@ -2829,9 +2892,9 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
|
|
|
2829
2892
|
console.log(`✅ flow amend-draft 留痕重锚:${r.reanchored.join('、')}——ledger amendment 审计在案;editRatio=${maxRatio}(段级最大;基准=首版原文,AGENT 槽不计入)`)
|
|
2830
2893
|
return r
|
|
2831
2894
|
}
|
|
2832
|
-
console.error('用法: sillyspec flow start --change <名> --input "
|
|
2895
|
+
console.error('用法: sillyspec flow start --change <名> --input "<一句中文概括(首行即变更标题)+动机与背景详述+独立一行『成功标准:』+每行一条『- 可验证标准』>" [--thick|--with-tasks] [--no-prestage(hindsight 标记在场时声明需求已含决策,留痕放行)] | sillyspec flow done --change <名> | sillyspec flow status --change <名> [--json] | sillyspec flow diff --change <名> [--json](只读文件统计)')
|
|
2833
2896
|
console.error(' --input 可照抄形态(引号内换行合法):')
|
|
2834
|
-
console.error(' sillyspec flow start --change <名> --input "
|
|
2897
|
+
console.error(' sillyspec flow start --change <名> --input "<一句中文概括(首行即变更标题);随后动机与背景')
|
|
2835
2898
|
console.error('')
|
|
2836
2899
|
console.error(' 成功标准:')
|
|
2837
2900
|
console.error(' - <可验证标准>"')
|
package/src/fr-index.js
CHANGED
|
@@ -21,6 +21,7 @@ import { readChangeTrace, upsertFrBindings, applySupersededToEntryLines, testAnc
|
|
|
21
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
|
+
import { execFileSync } from 'node:child_process';
|
|
24
25
|
import { writeAtomicSync } from './fs-atomic.js';
|
|
25
26
|
import {
|
|
26
27
|
splitKnowledgeSections,
|
|
@@ -73,6 +74,19 @@ export function frCoverageFiles({ archiveRoot, changeName }) {
|
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
} catch { /* 源2 fail-soft */ }
|
|
77
|
+
try {
|
|
78
|
+
// 源3(2026-10-15-rot-coverage-git-source):thin 归档常无交付表无 patch 文件清单——覆盖面
|
|
79
|
+
// 从 git 历史恢复:提交主题含变更名的提交的 name-only 文件集(--grep 子串匹配,变更名含
|
|
80
|
+
// 日期前缀足够特异;剔除 .sillyspec/ 自身工件面)。只读确定性;调用侧 covCache 每运行缓存,
|
|
81
|
+
// 非 git 仓/无记录 fail-soft=源缺(六顽固化 unknown 来源实测全部可恢复 1~23 文件)。
|
|
82
|
+
const gitOut = execFileSync('git', ['log', '--all', '--name-only', '--pretty=format:COMMIT', '--grep', String(changeName || '')],
|
|
83
|
+
{ cwd: archiveRoot, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
84
|
+
for (const f of String(gitOut).split(/\r?\n/)) {
|
|
85
|
+
const v = f.trim().replace(/\\/g, '/');
|
|
86
|
+
if (!v || v.startsWith('COMMIT') || v.startsWith('.sillyspec/')) continue;
|
|
87
|
+
out.add(v);
|
|
88
|
+
}
|
|
89
|
+
} catch { /* 源3 fail-soft(非 git 仓/无记录) */ }
|
|
76
90
|
return [...out].filter((p) => !p.startsWith('.sillyspec/'));
|
|
77
91
|
}
|
|
78
92
|
|
|
@@ -873,7 +887,7 @@ export function markSkeletonThin(knowledgeRoot) {
|
|
|
873
887
|
* active FR × 触碰文件 → hits(覆盖相交)/ skip(覆盖可判无交集)/ unknown(覆盖源缺失)。
|
|
874
888
|
* 判据逐字不变——三消费方(rot/测试门/注入排序)共用同一实现,防口径漂移。
|
|
875
889
|
* covCache 供调用方跨查询复用归档读取。 */
|
|
876
|
-
function partitionActiveByCoverage({ archiveRoot, frs, changed, covCache = new Map() }) {
|
|
890
|
+
export function partitionActiveByCoverage({ archiveRoot, frs, changed, covCache = new Map() }) {
|
|
877
891
|
const hits = []
|
|
878
892
|
const unknownSources = new Set()
|
|
879
893
|
let skip = 0
|
|
@@ -61,7 +61,7 @@ const STAGE_HINTS = {
|
|
|
61
61
|
'你需要先启动一个任务流程才能修改源码(调用对应的 sillyspec skill):',
|
|
62
62
|
'',
|
|
63
63
|
' 轻量变更/BUG修复(默认快道,skill sillyspec-flow)——--input 引号内换行合法,下例可照抄:',
|
|
64
|
-
' sillyspec flow start --change <YYYY-MM-DD-名> --input "
|
|
64
|
+
' sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(首行即变更标题);随后动机与背景',
|
|
65
65
|
'',
|
|
66
66
|
' 成功标准:',
|
|
67
67
|
' - <可验证标准>"',
|
|
@@ -80,7 +80,7 @@ const STAGE_HINTS = {
|
|
|
80
80
|
'verify': [
|
|
81
81
|
'当前在 verify(验证)阶段,只做代码审查和测试验证,不修改源码。',
|
|
82
82
|
'如需修改,请先回到 execute 阶段,或收口后开轻量变更(--input 引号内换行合法,可照抄):',
|
|
83
|
-
' sillyspec flow start --change <YYYY-MM-DD-名> --input "
|
|
83
|
+
' sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(首行即变更标题);随后动机与背景',
|
|
84
84
|
'',
|
|
85
85
|
' 成功标准:',
|
|
86
86
|
' - <可验证标准>"',
|
|
@@ -88,7 +88,7 @@ const STAGE_HINTS = {
|
|
|
88
88
|
'archive': [
|
|
89
89
|
'当前在 archive(归档)阶段,不修改源码。',
|
|
90
90
|
'如需修改,请开启新变更(--input 引号内换行合法,可照抄):',
|
|
91
|
-
' sillyspec flow start --change <YYYY-MM-DD-名> --input "
|
|
91
|
+
' sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(首行即变更标题);随后动机与背景',
|
|
92
92
|
'',
|
|
93
93
|
' 成功标准:',
|
|
94
94
|
' - <可验证标准>"',
|
package/src/index.js
CHANGED
|
@@ -171,7 +171,7 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
171
171
|
示例:
|
|
172
172
|
sillyspec init
|
|
173
173
|
sillyspec run brainstorm --change 2026-07-03-add-login
|
|
174
|
-
sillyspec flow start --change 2026-07-03-my-fix --input "
|
|
174
|
+
sillyspec flow start --change 2026-07-03-my-fix --input "<一句中文概括(首行即变更标题)+动机与背景详述+独立一行『成功标准:』+每行一条『- 可验证标准』>" # 轻量变更(默认快道,2 次调用;变更名须 YYYY-MM-DD-<简短描述> 日期前缀)
|
|
175
175
|
sillyspec run verify --done --output "验证通过,测试全绿"
|
|
176
176
|
sillyspec run archive --done --confirm --output "归档完成"
|
|
177
177
|
sillyspec run plan --reopen --from-step 2 # 修订 plan,从第 2 步重做
|
|
@@ -226,7 +226,7 @@ function refuseRetiredQuickFreshStart(stageArgs) {
|
|
|
226
226
|
console.error('❌ quick 通道已退役,不再接受新会话(v3.30.0 起)。')
|
|
227
227
|
// 可照抄实例(2026-10-05-input-teach-copyable):引号内换行合法,行级 trim 容忍缩进
|
|
228
228
|
console.error(' 新工作请走轻量变更(--input 引号内换行合法,下例可照抄):')
|
|
229
|
-
console.error(' sillyspec flow start --change <YYYY-MM-DD-名> --input "
|
|
229
|
+
console.error(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(首行即变更标题);随后动机与背景')
|
|
230
230
|
console.error('')
|
|
231
231
|
console.error(' 成功标准:')
|
|
232
232
|
console.error(' - <可验证标准>"')
|
|
@@ -5499,7 +5499,7 @@ SillySpec pull — 拉取服务器 spec 快照到本地(X2 / FR-07)
|
|
|
5499
5499
|
console.error(` 事件流路径:${join(tRuntimeRoot, `watcher-events-${tChange}.jsonl`)};活跃变更跑任意 flow/run 命令会拉起 watcher`);
|
|
5500
5500
|
process.exit(2);
|
|
5501
5501
|
}
|
|
5502
|
-
const { changeDir, tasksMd } = loadChangeTasks(tSpecBase, tChange);
|
|
5502
|
+
const { changeDir, tasksMd, tickLog } = loadChangeTasks(tSpecBase, tChange);
|
|
5503
5503
|
const tasks = tasksMd ? parseTaskLines(tasksMd) : null;
|
|
5504
5504
|
let tier = null;
|
|
5505
5505
|
if (changeDir) {
|
|
@@ -5509,6 +5509,8 @@ SillySpec pull — 拉取服务器 spec 快照到本地(X2 / FR-07)
|
|
|
5509
5509
|
} catch { /* 缺 flow-state(头脑风暴预段等)按未知 */ }
|
|
5510
5510
|
}
|
|
5511
5511
|
const { gitQuiet } = await import('./git-helper.js');
|
|
5512
|
+
// 锚收敛(2026-10-14-timeline-ticktime-anchor-scope):change 参进锚解析——消息不含本变更
|
|
5513
|
+
// 名的提交不参与任务行锚定(跨变更 token 撞车防线);事件时间轴不受影响。
|
|
5512
5514
|
const anchors = resolveCommitAnchors(tRes.events, (hash) => {
|
|
5513
5515
|
const out = gitQuiet(dir, ['log', '-1', '--format=%h%x1f%aI%x1f%B', hash]);
|
|
5514
5516
|
if (typeof out !== 'string' || !out.trim()) return null;
|
|
@@ -5516,8 +5518,8 @@ SillySpec pull — 拉取服务器 spec 快照到本地(X2 / FR-07)
|
|
|
5516
5518
|
const rest = out.slice(sep + 1);
|
|
5517
5519
|
const sep2 = rest.indexOf('\x1f');
|
|
5518
5520
|
return { hash: out.slice(0, sep), dateISO: rest.slice(0, sep2), message: rest.slice(sep2 + 1) };
|
|
5519
|
-
});
|
|
5520
|
-
console.log(renderTimeline({ change: tChange, events: tRes.events, tasks, anchors, birthTs: readBirthTs(changeDir), tier }));
|
|
5521
|
+
}, tChange);
|
|
5522
|
+
console.log(renderTimeline({ change: tChange, events: tRes.events, tasks, anchors, birthTs: readBirthTs(changeDir), tier, tickLog }));
|
|
5521
5523
|
if (tSourceNote) console.log(tSourceNote);
|
|
5522
5524
|
break;
|
|
5523
5525
|
}
|
package/src/knowledge-digest.js
CHANGED
|
@@ -152,6 +152,14 @@ export function collectKnowledgeDigest({ specBase, projectRoot }) {
|
|
|
152
152
|
mk('inbox', 'warn', `知识收件箱积压(${totals.inbox} 条 > 20)`, totals.inbox,
|
|
153
153
|
inboxTitles.slice(0, 5).join(';') + (inboxTitles.length > 5 ? ' 等' : ''),
|
|
154
154
|
'sillyspec knowledge inbox 逐条 classify 清账')
|
|
155
|
+
} else if (totals.inbox > 0) {
|
|
156
|
+
// 低量收件箱可见性(2026-10-15-classify-surface):>20 才报警导致 1~20 条常态化静默——
|
|
157
|
+
// classify 遥测零消费实证(工具在、入口隐身)。info 级提示给可执行命令(classify 零使用
|
|
158
|
+
// 的接入点);不进 healthy 判定否决——info 信号 healthy 语义见下(healthy=signals 全空,
|
|
159
|
+
// info 在场仍算 healthy=false?否——info 级降噪:healthy 只看 warn 级)。
|
|
160
|
+
mk('inbox', 'info', `知识收件箱 ${totals.inbox} 条待归类`, totals.inbox,
|
|
161
|
+
inboxTitles.slice(0, 5).join(';') + (inboxTitles.length > 5 ? ' 等' : ''),
|
|
162
|
+
'sillyspec knowledge inbox 看清单 → knowledge classify --ql <id> --file <known-issues.md|patterns.md|conventions.md> [--dry-run] 逐条归类')
|
|
155
163
|
}
|
|
156
164
|
if (totals.pseudo > 0) {
|
|
157
165
|
mk('pseudo-domain', 'warn', `伪域在库(auto-*/unmapped 共 ${totals.pseudo} 条)`, totals.pseudo,
|
|
@@ -166,7 +174,9 @@ export function collectKnowledgeDigest({ specBase, projectRoot }) {
|
|
|
166
174
|
|
|
167
175
|
return {
|
|
168
176
|
generated_at: new Date().toISOString(),
|
|
169
|
-
healthy
|
|
177
|
+
// healthy 只看 warn 级(2026-10-15-classify-surface):info 是提示不是超阈——
|
|
178
|
+
// 低量收件箱提示不该把治理卡打成人人自危的告警面;平台 governance 消费同语义。
|
|
179
|
+
healthy: !signals.some((s) => s.level === 'warn'),
|
|
170
180
|
signals,
|
|
171
181
|
totals,
|
|
172
182
|
}
|
|
@@ -176,15 +186,22 @@ export function collectKnowledgeDigest({ specBase, projectRoot }) {
|
|
|
176
186
|
export function renderKnowledgeDigestText(d) {
|
|
177
187
|
const lines = []
|
|
178
188
|
lines.push(`📬 知识资产治理摘要(${d.generated_at.slice(0, 16).replace('T', ' ')})`)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
lines.push(`⚠️ ${
|
|
183
|
-
for (const s of
|
|
189
|
+
const warns = d.signals.filter((s) => s.level === 'warn')
|
|
190
|
+
const infos = d.signals.filter((s) => s.level === 'info')
|
|
191
|
+
if (warns.length > 0) {
|
|
192
|
+
lines.push(`⚠️ ${warns.length} 类信号超阈:`)
|
|
193
|
+
for (const s of warns) {
|
|
184
194
|
lines.push(` [${s.kind}] ${s.title}`)
|
|
185
195
|
if (s.detail) lines.push(` 明细:${s.detail}`)
|
|
186
196
|
if (s.suggestion) lines.push(` 处置:${s.suggestion}`)
|
|
187
197
|
}
|
|
198
|
+
} else {
|
|
199
|
+
lines.push('✅ 超阈信号零(inbox/伪域/坏绑定)——健康态')
|
|
200
|
+
}
|
|
201
|
+
for (const s of infos) {
|
|
202
|
+
lines.push(`ℹ️ [${s.kind}] ${s.title}`)
|
|
203
|
+
if (s.detail) lines.push(` 明细:${s.detail}`)
|
|
204
|
+
if (s.suggestion) lines.push(` 处置:${s.suggestion}`)
|
|
188
205
|
}
|
|
189
206
|
lines.push(`底数:收件箱 ${d.totals.inbox}|伪域条目 ${d.totals.pseudo}|坏绑定 ${d.totals.unresolvedBindings}`)
|
|
190
207
|
return lines.join('\n')
|
package/src/knowledge-graph.js
CHANGED
|
@@ -103,6 +103,7 @@ export function buildKnowledgeGraph(specRoot) {
|
|
|
103
103
|
|
|
104
104
|
// ── ② 决策库(parseDecisionEntries:去重键 file+id+change 跨变更同号不折叠) ──
|
|
105
105
|
const decisionHits = existsSync(join(knowledgeDir, 'INDEX.md')) ? parseDecisionEntries(knowledgeDir) : []
|
|
106
|
+
const supersedesRefs = []
|
|
106
107
|
for (const h of decisionHits) {
|
|
107
108
|
const id = `decision:${h.file}#${h.id}${h.change ? `@${h.change}` : ''}`
|
|
108
109
|
addNode(id, 'decision', `${h.id} ${h.title || ''}`.trim(), {
|
|
@@ -123,17 +124,45 @@ export function buildKnowledgeGraph(specRoot) {
|
|
|
123
124
|
else addNode(h.change, 'change', h.change)
|
|
124
125
|
addEdge(id, h.change, 'from-change')
|
|
125
126
|
}
|
|
126
|
-
// supersedes:同域 D-id 解析(「supersedes:无(…)」等非 id
|
|
127
|
-
|
|
127
|
+
// supersedes:同域 D-id 解析(「supersedes:无(…)」等非 id 值被正则白名单滤掉)。目标必须同变更
|
|
128
|
+
// (「变更:」行一致,含双方均无该行的 legacy 同源)——D 号是变更内局部序号,跨变更同号互不相干;
|
|
129
|
+
// v2 蒸馏落地时同变更 v1 段已被就地替换,此时无目标即不建边(2026-10-14-supersedes-mislink-and-change-display:
|
|
130
|
+
// 旧「再退同文件首个」回退几乎必然连到他变更同号条目,实测 33 条误连,回退已除)。epoch 形态段
|
|
131
|
+
// (2026-10-14-decision-global-id-epoch)id 为全局号——局部号引用经「原编号」字段匹配(originId)。
|
|
132
|
+
const supIds = String(h.supersedesText || '').split(/[,,、\s]+/).filter((s) => /^D-(?:[A-Za-z0-9_\-\u4e00-\u9fff]+-)?\d+@v\d+$/.test(s))
|
|
128
133
|
for (const sid of supIds) {
|
|
129
|
-
const target = decisionHits.find((x) => x.id === sid && x.file === h.file && (x.change || '') === (h.change || ''))
|
|
130
|
-
|| decisionHits.find((x) => x.id === sid && x.file === h.file)
|
|
134
|
+
const target = decisionHits.find((x) => (x.id === sid || x.originId === sid) && x.file === h.file && (x.change || '') === (h.change || ''))
|
|
131
135
|
if (target) {
|
|
132
|
-
|
|
133
|
-
|
|
136
|
+
supersedesRefs.push([id, `decision:${target.file}#${target.id}${target.change ? `@${target.change}` : ''}`])
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
// 幽灵版本节点(2026-10-15-supersedes-ghost-nodes):同号版本取代是蒸馏就地替换(旧段被删,
|
|
140
|
+
// 跨文件同谓词同删),自身旧版本引用的目标从不在场——真实库实证 39 条引用 0 条可解析、
|
|
141
|
+
// 703 决策节点全图仅 1 条 supersedes 边(边体系闲置根因)。合成口径**只限自身版本链**:
|
|
142
|
+
// sid 基号 === 本条目 id 基号(legacy 同号自链)或 === originId 基号(epoch 原编号自链,
|
|
143
|
+
// 迁移后真实库 36/36 全为此形态);跨号/跨变更缺失引用不合成(2026-10-14-supersedes-mislink
|
|
144
|
+
// 的跨变更误连防护不因幽灵形态复活:为他号凭空铸造节点=误连换皮)。ghost 只带 supersedes
|
|
145
|
+
// 入边(非孤儿)、不参与 anchors/dangling 面(supersedes 不在悬空检测边型集),impact 闭包
|
|
146
|
+
// 经传递边自然延伸。
|
|
147
|
+
const ghostBase = (s) => (/^D-((?:[A-Za-z0-9_\-\u4e00-\u9fff]+-)?\d+)@v\d+$/.exec(String(s || '')) || [])[1]
|
|
148
|
+
const selfBase = ghostBase(h.id)
|
|
149
|
+
const originBase = h.originId ? ghostBase(h.originId) : null
|
|
150
|
+
const sidBase = ghostBase(sid)
|
|
151
|
+
if ((selfBase && sidBase === selfBase) || (originBase && sidBase === originBase)) {
|
|
152
|
+
const gid = `decision:${h.file}#${sid}${h.change ? `@${h.change}` : ''}`
|
|
153
|
+
if (!nodes.has(gid)) {
|
|
154
|
+
addNode(gid, 'decision', `${sid}(已被 ${h.id} 取代)`, {
|
|
155
|
+
ghost: true, status: 'superseded', supersededBy: h.id,
|
|
156
|
+
domain: String(h.file || '').replace(/^decisions\//, '').replace(/\.md$/, ''),
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
supersedesRefs.push([id, gid])
|
|
134
160
|
}
|
|
135
161
|
}
|
|
136
162
|
}
|
|
163
|
+
// 目标节点可能晚于引用者定义(同文件行序)——addEdge 要求两端节点在场,引用者先行遍历会静默丢边;
|
|
164
|
+
// 边统一在节点面建齐后补挂(2026-10-14-supersedes-mislink-and-change-display 测试实证)
|
|
165
|
+
for (const [s, t] of supersedesRefs) addEdge(s, t, 'supersedes')
|
|
137
166
|
|
|
138
167
|
// ── ③ FR 索引(fr/<域>.md:变更/状态/superseded_by/取代链/测试绑定子块) ──
|
|
139
168
|
const frDir = join(knowledgeDir, 'fr')
|
|
@@ -542,6 +571,27 @@ export function graphChangelogDanglings(graph, { existsFn = existsSync } = {}) {
|
|
|
542
571
|
return out
|
|
543
572
|
}
|
|
544
573
|
|
|
574
|
+
/** 登记仓前缀(graph 惰性缓存):<specRoot>/projects/*.yaml 的 name: 字段——本 spec 树管理
|
|
575
|
+
* 多仓时(register-repo 机制),跨仓交付路径(backend/**、frontend/**…)按前缀归到登记仓名下,
|
|
576
|
+
* 不再落 _unmapped 大簇(2026-10-15-graph-registry-domain-split:真图 2183 未挂模块文件节点
|
|
577
|
+
* 实测 1220 为跨仓前缀、421 为本仓 test 面)。无 projects 目录/解析失败 = 空集(fail-soft)。 */
|
|
578
|
+
function registryProjectPrefixes(graph) {
|
|
579
|
+
if (graph._projPrefixes) return graph._projPrefixes
|
|
580
|
+
const out = []
|
|
581
|
+
try {
|
|
582
|
+
const dir = join(String(graph.root || ''), 'projects')
|
|
583
|
+
if (existsSync(dir)) {
|
|
584
|
+
for (const f of readdirSync(dir)) {
|
|
585
|
+
if (!f.endsWith('.yaml')) continue
|
|
586
|
+
const m = /^name:\s*([\w-]+)\s*$/m.exec(readFileSync(join(dir, f), 'utf8'))
|
|
587
|
+
if (m) out.push(m[1])
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
} catch { /* projects 缺席/不可读 = 无登记前缀 */ }
|
|
591
|
+
graph._projPrefixes = out
|
|
592
|
+
return out
|
|
593
|
+
}
|
|
594
|
+
|
|
545
595
|
/** 节点 → 簇域(lite 总览聚类键 type×domain;缺省占位防散簇)。 */
|
|
546
596
|
function graphNodeDomain(graph, n) {
|
|
547
597
|
switch (n.type) {
|
|
@@ -550,7 +600,20 @@ function graphNodeDomain(graph, n) {
|
|
|
550
600
|
case 'file': {
|
|
551
601
|
// module-files 精确挂接反查(多挂取最长模块 id——窄域优先)
|
|
552
602
|
const mods = graph.edges.filter((e) => e.type === 'module-files' && e.t === n.id).map((e) => e.s.replace(/^module:/, ''))
|
|
553
|
-
return mods.sort((a, b) => b.length - a.length)[0]
|
|
603
|
+
if (mods.length > 0) return mods.sort((a, b) => b.length - a.length)[0]
|
|
604
|
+
// 登记仓前缀归属(2026-10-15-graph-registry-domain-split):跨仓交付路径按 projects 登记
|
|
605
|
+
// 名归簇(最长前缀优先、大小写不敏感——登记名 SillyHub vs 路径 sillyhub/ 实证);本仓
|
|
606
|
+
// 测试面归 _tests——残余 _unmapped 只剩真无归属面(module-map 覆盖缺口,可行动)
|
|
607
|
+
const id = String(n.id)
|
|
608
|
+
const idLower = id.toLowerCase()
|
|
609
|
+
let best = ''
|
|
610
|
+
for (const p of registryProjectPrefixes(graph)) {
|
|
611
|
+
const pl = p.toLowerCase()
|
|
612
|
+
if (idLower.startsWith(`${pl}/`) && pl.length > best.length) best = p
|
|
613
|
+
}
|
|
614
|
+
if (best) return best
|
|
615
|
+
if (/^(test|tests)\//.test(id)) return '_tests'
|
|
616
|
+
return '_unmapped'
|
|
554
617
|
}
|
|
555
618
|
case 'module':
|
|
556
619
|
return n.id.replace(/^module:/, '')
|
|
@@ -573,12 +636,18 @@ export function graphSummary(graph, { existsFn = existsSync, clustersLimit = 0 }
|
|
|
573
636
|
const orphans = graphOrphans(graph).length
|
|
574
637
|
const dangling = graphDangling(graph, { existsFn })
|
|
575
638
|
const danglingRefs = dangling.length
|
|
639
|
+
// 跨仓口径分流(2026-10-15-graph-dangling-crossrepo-split):知识库集中管理仓、代码分仓存放
|
|
640
|
+
// 是本体系常态拓扑——跨仓强边锚点(顶级目录本仓不存在)永不可在本仓解析却全数计入 dangling_refs,
|
|
641
|
+
// 健康数字长期失真(本仓实证 2537 中 2472 为跨仓注记)。主计数与守恒口径不动(消费方零回归),
|
|
642
|
+
// 增加分流字段:breakdown.cross_repo=跨仓注记数、dangling_refs_local=本仓可行动悬空数。
|
|
643
|
+
const danglingCrossRepo = dangling.filter((d) => d.crossRepo).length
|
|
576
644
|
// 分强度计数(附加字段——dangling_refs 是两类之和,消费方对账 doctor 时按 breakdown 拆)
|
|
577
645
|
const danglingBreakdown = dangling.reduce((a, d) => {
|
|
578
646
|
if (EDGE_STRENGTH[d.edge.type] === 'strong') a.strong_anchors++
|
|
579
647
|
else if (EDGE_STRENGTH[d.edge.type] === 'medium') a.medium_doc_refs++
|
|
580
648
|
return a
|
|
581
649
|
}, { strong_anchors: 0, medium_doc_refs: 0 })
|
|
650
|
+
danglingBreakdown.cross_repo = danglingCrossRepo
|
|
582
651
|
const moduleDocGaps = graphModuleDocGaps(graph).length
|
|
583
652
|
const changelogDanglings = graphChangelogDanglings(graph, { existsFn }).length
|
|
584
653
|
// clusters:type×domain 聚簇,representatives 度数 top-5(同度按 id 字典序稳序)
|
|
@@ -612,7 +681,10 @@ export function graphSummary(graph, { existsFn = existsSync, clustersLimit = 0 }
|
|
|
612
681
|
module_doc_gaps: moduleDocGaps,
|
|
613
682
|
changelog_danglings: changelogDanglings,
|
|
614
683
|
dangling_refs: danglingRefs,
|
|
615
|
-
// dangling_refs
|
|
684
|
+
// 本仓可行动悬空(附加字段,跨仓口径分流):dangling_refs − 跨仓注记——治理动作只看这个数
|
|
685
|
+
dangling_refs_local: danglingRefs - danglingCrossRepo,
|
|
686
|
+
// dangling_refs 构成(附加字段,向后兼容):doctor graph-dangling-anchor(强)+ graph-doc-dangling-ref(中)之和;
|
|
687
|
+
// cross_repo=跨仓注记数(顶级目录本仓不存在——目标活在独立仓,单列不对账)
|
|
616
688
|
dangling_refs_breakdown: danglingBreakdown,
|
|
617
689
|
clusters,
|
|
618
690
|
}
|
|
@@ -791,7 +863,7 @@ export function impactFromDecisionsMd(graph, text) {
|
|
|
791
863
|
// 输出沿知识命令族全 JSON 约定({ok,...data});人类可读摘要行内嵌 summary 字段。
|
|
792
864
|
// ═══════════════════════════════════════════════════════════════
|
|
793
865
|
|
|
794
|
-
const GRAPH_USAGE = '用法:sillyspec knowledge graph <summary|nodes|dump|neighbors|path|impact|orphans|dangling> [锚点...] [--search <模糊>] [--limit N] [--edges <边型>] [--depth N];dump 需 --layout'
|
|
866
|
+
const GRAPH_USAGE = '用法:sillyspec knowledge graph <summary|nodes|dump|neighbors|path|impact|orphans|dangling> [锚点...] [--search <模糊>] [--limit N] [--edges <边型>] [--depth N] [--local];dump 需 --layout;dangling --local 只列本仓悬空(跨仓注记单列不对账)'
|
|
795
867
|
|
|
796
868
|
export async function cmdKnowledgeGraph(dir, args, opts = {}) {
|
|
797
869
|
const out = (ok, data, error) => console.log(JSON.stringify({ ok, ...(data || {}) , ...(error ? { error } : {}) }, null, 2))
|
|
@@ -822,7 +894,7 @@ export async function cmdKnowledgeGraph(dir, args, opts = {}) {
|
|
|
822
894
|
stats: s,
|
|
823
895
|
summary: [
|
|
824
896
|
`节点 ${s.nodes} · 边 ${s.edges}`,
|
|
825
|
-
`孤儿 ${s.orphans} · 模块文档缺口 ${s.module_doc_gaps} · changelog 悬空 ${s.changelog_danglings} · 悬空引用 ${s.dangling_refs}
|
|
897
|
+
`孤儿 ${s.orphans} · 模块文档缺口 ${s.module_doc_gaps} · changelog 悬空 ${s.changelog_danglings} · 悬空引用 ${s.dangling_refs}(本仓 ${s.dangling_refs_local} · 跨仓注记 ${s.dangling_refs_breakdown.cross_repo}——目标仓不在本仓,单列不对账)`,
|
|
826
898
|
`簇 ${s.clusters.length} 个(最大 ${s.clusters[0]?.key ?? '—'} × ${s.clusters[0]?.count ?? 0})`,
|
|
827
899
|
],
|
|
828
900
|
})
|
|
@@ -907,12 +979,18 @@ export async function cmdKnowledgeGraph(dir, args, opts = {}) {
|
|
|
907
979
|
orphans: list.map((f) => ({ id: f.node.id, type: f.node.type, kind: f.kind })),
|
|
908
980
|
})
|
|
909
981
|
}
|
|
910
|
-
// dangling
|
|
911
|
-
const
|
|
982
|
+
// dangling(--local 过滤跨仓注记:治理动作只看本仓可解析面——跨仓目标活在独立仓)
|
|
983
|
+
const localOnly = args.includes('--local')
|
|
984
|
+
const allDangling = graphDangling(graph)
|
|
985
|
+
const crossRepoCount = allDangling.filter((d) => d.crossRepo).length
|
|
986
|
+
const list = localOnly ? allDangling.filter((d) => !d.crossRepo) : allDangling
|
|
912
987
|
return out(true, {
|
|
913
|
-
query: { sub },
|
|
988
|
+
query: { sub, local: localOnly || undefined },
|
|
914
989
|
count: list.length,
|
|
915
|
-
summary: list.map((d) => `⚠️ ${d.edge.type}(${d.strength}) ${d.edge.s} → ${d.missing}`),
|
|
990
|
+
summary: list.map((d) => `⚠️ ${d.edge.type}(${d.strength}) ${d.edge.s} → ${d.missing}${d.crossRepo ? ' 〔跨仓注记〕' : ''}`),
|
|
916
991
|
dangling: list.map((d) => ({ edge: d.edge, missing: d.missing, strength: d.strength })),
|
|
992
|
+
// 口径分流(附加字段):本仓可行动数 + 跨仓注记数单列不对账;--local 时 count 即本仓数
|
|
993
|
+
dangling_refs_local: allDangling.length - crossRepoCount,
|
|
994
|
+
cross_repo: crossRepoCount,
|
|
917
995
|
})
|
|
918
996
|
}
|