sillyspec 3.32.8 → 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 +5 -4
- package/assets/command-cards/flow.md +9 -4
- package/package.json +1 -1
- package/src/change-list.js +13 -0
- package/src/commit-guard.js +70 -2
- package/src/commit-suggest.js +45 -4
- package/src/config-schema.js +14 -0
- package/src/cross-repo-reconcile.js +54 -1
- package/src/decision-distill.js +77 -21
- package/src/decision-renumber.js +157 -0
- package/src/deleted-ledger.js +75 -0
- package/src/design-facts.js +5 -3
- package/src/dispatch/backends/local-agent.js +1 -1
- package/src/dispatch/backends/sillyhub-mcp.js +1 -1
- package/src/docs-check.js +30 -15
- package/src/docs-debt.js +3 -2
- package/src/doctor-diagnostics.js +3 -1
- package/src/flow-draft.js +24 -2
- package/src/flow-parity.js +32 -2
- package/src/flow.js +427 -69
- package/src/fr-index.js +15 -1
- package/src/hooks/worktree-guard.js +3 -3
- package/src/index.js +22 -10
- 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/progress/change-registry.js +7 -0
- package/src/progress.js +67 -0
- package/src/quicklog.js +6 -1
- package/src/redomain.js +27 -0
- package/src/route-hindsight.js +34 -2
- package/src/run/archive-distill.js +3 -0
- package/src/run/command.js +40 -44
- package/src/run/complete-handlers.js +8 -3
- package/src/run/complete.js +129 -63
- package/src/run/gates.js +94 -90
- package/src/run/prompt.js +13 -3
- package/src/run/shared.js +19 -0
- package/src/run/stage.js +1 -1
- package/src/scope-audit.js +2 -2
- 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 +24 -12
- package/src/stages/execute.js +1 -1
- package/src/stages/explore.js +3 -5
- package/src/stages/knowledge.js +56 -5
- package/src/sync.js +98 -2
- package/src/task-tick.js +12 -0
- package/src/timeline.js +60 -19
- package/src/verify-postcheck.js +83 -6
- package/src/verify-probes.js +61 -18
- package/templates/agents-instruction.md +5 -4
- package/templates/prompts/testcase-design.md +5 -0
- package/src/stages/brainstorm-auto.js +0 -286
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(' - <可验证标准>"')
|
|
@@ -3382,17 +3382,19 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
|
|
|
3382
3382
|
// --from <域> --to <域> [--anchor <FR-id> 可重复];干跑缺省列条目,--write 落盘。
|
|
3383
3383
|
// 身份铁律:迁域不换号(绑定/supersede 链靠 ID 寻址);平台伪域卡「一键迁移」的 CLI 端。
|
|
3384
3384
|
if (has('--redomain')) {
|
|
3385
|
-
const
|
|
3386
|
-
|
|
3385
|
+
const { planRedomain, redomainFrEntries, normalizeDomainArg } = await import('./redomain.js')
|
|
3386
|
+
// 入参归一化(2026-10-14-redomain-domain-arg-normalize):--from/--to 容忍
|
|
3387
|
+
// flow done 提示语原样形态(fr/auto-backend、auto-backend.md),回显/报错统一裸域名。
|
|
3388
|
+
const fromD = normalizeDomainArg(flag('--from'))
|
|
3389
|
+
const toD = normalizeDomainArg(flag('--to'))
|
|
3387
3390
|
// --by-change(坑 fr-domain-suggest-typo-and-no-split-migration 缺陷2):unmapped 等
|
|
3388
3391
|
// 混合池按「变更:<名>」字段分批迁移——整域迁移会错置大部分条目,来源变更才是分批键。
|
|
3389
3392
|
const byChangeD = flag('--by-change')
|
|
3390
|
-
if (!fromD || !toD) { fail('用法: sillyspec tests --redomain --from <源域> --to <目标域> [--anchor <FR-id>] [--by-change <变更名>] [--write]'); break }
|
|
3393
|
+
if (!fromD || !toD) { fail('用法: sillyspec tests --redomain --from <源域> --to <目标域> [--anchor <FR-id>] [--by-change <变更名>] [--write]\n 域名可带 fr/ 前缀与 .md 后缀(fr/auto-backend ≍ auto-backend.md ≍ auto-backend)'); break }
|
|
3391
3394
|
// 多 --anchor 收集
|
|
3392
3395
|
const anchorList = []
|
|
3393
3396
|
for (let i = 0; i < filteredArgs.length - 1; i++) if (filteredArgs[i] === '--anchor') anchorList.push(filteredArgs[i + 1])
|
|
3394
3397
|
try {
|
|
3395
|
-
const { planRedomain, redomainFrEntries } = await import('./redomain.js')
|
|
3396
3398
|
if (!has('--write')) {
|
|
3397
3399
|
// 预览与落盘同口径透传 byChange(坑 redomain-plan-preview-by-change-ignored,
|
|
3398
3400
|
// 2026-10-02 实证:76338b4d 收编在途工作时此行漏透传——预览列整域、落盘只迁
|
|
@@ -4397,7 +4399,15 @@ SillySpec platform — SillyHub 平台同步
|
|
|
4397
4399
|
const syncChangeName = syncChangeIdx >= 0 && args[syncChangeIdx + 1] && !String(args[syncChangeIdx + 1]).startsWith("--") ? args[syncChangeIdx + 1] : null;
|
|
4398
4400
|
if (syncChangeName) {
|
|
4399
4401
|
// manual:用户显式同步,change_deleted 回执噪音闸旁路(显式动作的回执必须可见)
|
|
4400
|
-
|
|
4402
|
+
// 失败必须 exit 1(2026-10-13-sync-ledger-tombstone,事故 2026-10-13-new-change-e5a27813):
|
|
4403
|
+
// 此前忽略返回值恒 exit 0——变更不存在/未连接平台都「假成功」,daemon RPC
|
|
4404
|
+
// (sillyspec.sync_change)据退出码回执,假成功掩盖行缺失。成功路径(含
|
|
4405
|
+
// change_deleted 预期回执、台账墓碑补推)errors 为空,exit 0 语义不变。
|
|
4406
|
+
const syncResult = await syncModule.sync(syncChangeName, dir, { manual: true });
|
|
4407
|
+
if (syncResult && Array.isArray(syncResult.errors) && syncResult.errors.length > 0) {
|
|
4408
|
+
console.error(`❌ platform sync 失败(${syncChangeName}): ${syncResult.errors.join(';')}`);
|
|
4409
|
+
process.exit(1);
|
|
4410
|
+
}
|
|
4401
4411
|
} else {
|
|
4402
4412
|
// 全树 spec 文件同步(2026-10-11-sync-conflict-tri-fix FR-03):不带 --change
|
|
4403
4413
|
// 此前报「需要指定变更名称」——归档/历史变更无活跃会话标签时无从下手。现走
|
|
@@ -5489,7 +5499,7 @@ SillySpec pull — 拉取服务器 spec 快照到本地(X2 / FR-07)
|
|
|
5489
5499
|
console.error(` 事件流路径:${join(tRuntimeRoot, `watcher-events-${tChange}.jsonl`)};活跃变更跑任意 flow/run 命令会拉起 watcher`);
|
|
5490
5500
|
process.exit(2);
|
|
5491
5501
|
}
|
|
5492
|
-
const { changeDir, tasksMd } = loadChangeTasks(tSpecBase, tChange);
|
|
5502
|
+
const { changeDir, tasksMd, tickLog } = loadChangeTasks(tSpecBase, tChange);
|
|
5493
5503
|
const tasks = tasksMd ? parseTaskLines(tasksMd) : null;
|
|
5494
5504
|
let tier = null;
|
|
5495
5505
|
if (changeDir) {
|
|
@@ -5499,6 +5509,8 @@ SillySpec pull — 拉取服务器 spec 快照到本地(X2 / FR-07)
|
|
|
5499
5509
|
} catch { /* 缺 flow-state(头脑风暴预段等)按未知 */ }
|
|
5500
5510
|
}
|
|
5501
5511
|
const { gitQuiet } = await import('./git-helper.js');
|
|
5512
|
+
// 锚收敛(2026-10-14-timeline-ticktime-anchor-scope):change 参进锚解析——消息不含本变更
|
|
5513
|
+
// 名的提交不参与任务行锚定(跨变更 token 撞车防线);事件时间轴不受影响。
|
|
5502
5514
|
const anchors = resolveCommitAnchors(tRes.events, (hash) => {
|
|
5503
5515
|
const out = gitQuiet(dir, ['log', '-1', '--format=%h%x1f%aI%x1f%B', hash]);
|
|
5504
5516
|
if (typeof out !== 'string' || !out.trim()) return null;
|
|
@@ -5506,8 +5518,8 @@ SillySpec pull — 拉取服务器 spec 快照到本地(X2 / FR-07)
|
|
|
5506
5518
|
const rest = out.slice(sep + 1);
|
|
5507
5519
|
const sep2 = rest.indexOf('\x1f');
|
|
5508
5520
|
return { hash: out.slice(0, sep), dateISO: rest.slice(0, sep2), message: rest.slice(sep2 + 1) };
|
|
5509
|
-
});
|
|
5510
|
-
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 }));
|
|
5511
5523
|
if (tSourceNote) console.log(tSourceNote);
|
|
5512
5524
|
break;
|
|
5513
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
|
}
|
package/src/knowledge-match.js
CHANGED
|
@@ -61,7 +61,7 @@ function isDecisionRoute(entry) {
|
|
|
61
61
|
return /^decisions\//.test(String(entry.file || '').replace(/\\/g, '/'))
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
const DECISION_HEADER_RE = /^##\s+(D-\d+@v\d+)\s*(.*)$/
|
|
64
|
+
const DECISION_HEADER_RE = /^##\s+(D-(?:[A-Za-z0-9_\-\u4e00-\u9fff]+-)?\d+@v\d+)\s*(.*)$/
|
|
65
65
|
const DECISION_FIELD_RE = /^(状态|文件|理由|否决理由|复潮条件|supersedes)\s*[::]\s*(.*)$/
|
|
66
66
|
// 「锚点:」单独标签精确匹配读入(仿 docs-check.js parseDecisionEntries 先例)——绝不并入
|
|
67
67
|
// DECISION_FIELD_RE:其 else-if 链末尾的 reason 回填(else if (!cur.reason))会把锚点值
|
|
@@ -69,6 +69,12 @@ const DECISION_FIELD_RE = /^(状态|文件|理由|否决理由|复潮条件|supe
|
|
|
69
69
|
const DECISION_ANCHOR_RE = /^锚点\s*[::]\s*(.*)$/
|
|
70
70
|
// 变更:单独标签(变更名限定——D-xxx 是变更内局部序号,跨变更同号互不相干,去重键依赖它)
|
|
71
71
|
const DECISION_CHANGE_RE = /^变更\s*[::]\s*(.*)$/
|
|
72
|
+
// 来源:兜底标签(2026-10-14-decision-id-p2-hardening):2026-08-24 首修形态残留(后被「变更:」
|
|
73
|
+
// 取代,现存 20 行)——变更行无条件覆盖在前,来源仅补缺(任意行序下变更优先)
|
|
74
|
+
const DECISION_ORIGIN_RE = /^来源\s*[::]\s*(.*)$/
|
|
75
|
+
// 原编号:溯源标签(2026-10-14-decision-global-id-epoch):epoch 形态条目的局部号——graph
|
|
76
|
+
// supersedes 经它把「supersedes:D-011@v1」解析到同变更全局号段
|
|
77
|
+
const DECISION_ORIGINID_RE = /^原编号\s*[::]\s*(.*)$/
|
|
72
78
|
|
|
73
79
|
/** 「文件:」值切分 + 逐项反斜杠归一 POSIX(与 decision-distill 的 entry.files 契约对齐:
|
|
74
80
|
* split /[,,、\s]+/ + replace(/\\/g,'/'))。 */
|
|
@@ -133,7 +139,7 @@ function parseDecisionFile(filePath, file) {
|
|
|
133
139
|
const h = line.match(DECISION_HEADER_RE)
|
|
134
140
|
if (h) {
|
|
135
141
|
flush()
|
|
136
|
-
cur = { file, id: h[1], title: h[2].trim(), status: '', reason: '', revisitWhen: '', files: [], anchor: '', change: '', supersedesText: '' }
|
|
142
|
+
cur = { file, id: h[1], title: h[2].trim(), status: '', reason: '', revisitWhen: '', files: [], anchor: '', change: '', supersedesText: '', originId: '' }
|
|
137
143
|
continue
|
|
138
144
|
}
|
|
139
145
|
if (!cur) continue
|
|
@@ -144,6 +150,10 @@ function parseDecisionFile(filePath, file) {
|
|
|
144
150
|
// 域文件跨变更同号条目常见(D-xxx 是变更内局部序号),去重键须含变更名否则先到条目遮蔽后来者)
|
|
145
151
|
const c = line.match(DECISION_CHANGE_RE)
|
|
146
152
|
if (c) { cur.change = c[1].trim(); continue }
|
|
153
|
+
const o = line.match(DECISION_ORIGIN_RE)
|
|
154
|
+
if (o) { if (!cur.change) cur.change = o[1].trim(); continue }
|
|
155
|
+
const oi = line.match(DECISION_ORIGINID_RE)
|
|
156
|
+
if (oi) { cur.originId = oi[1].trim(); continue }
|
|
147
157
|
const f = line.match(DECISION_FIELD_RE)
|
|
148
158
|
if (!f) continue
|
|
149
159
|
const value = f[2].trim()
|
|
@@ -170,20 +180,25 @@ function parseDecisionFile(filePath, file) {
|
|
|
170
180
|
export function parseDecisionEntries(indexDir, indexEntries = null) {
|
|
171
181
|
const routes = (indexEntries || parseKnowledgeIndex(indexDir)).filter(isDecisionRoute)
|
|
172
182
|
const hits = []
|
|
183
|
+
// 路由行按文件去重单解析(2026-10-14-decision-id-p2-hardening):多条路由行指向同一域文件只解析
|
|
184
|
+
// 一次——此前靠条目级折叠兜完全重复,legacy 不折叠后从源头消除重复
|
|
185
|
+
const parsed = new Set()
|
|
173
186
|
for (const route of routes) {
|
|
187
|
+
const fileKey = String(route.file || '').replace(/\\/g, '/')
|
|
188
|
+
if (parsed.has(fileKey)) continue
|
|
189
|
+
parsed.add(fileKey)
|
|
174
190
|
const filePath = join(indexDir, route.file)
|
|
175
191
|
if (!existsSync(filePath)) continue // 路由行失效(域文件已删)→ 跳过不阻断
|
|
176
192
|
hits.push(...parseDecisionFile(filePath, route.file))
|
|
177
193
|
}
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
// 注入同样被遮蔽)。无「变更:」行的 legacy 条目 change='' 与同号 legacy 条目互折叠
|
|
183
|
-
// (维持旧行为,不放大历史条目数)。
|
|
194
|
+
// 带 change 条目按 file+id+change 去重(坑 decision-cross-change-id-shadow,2026-09-11 dogfood 实证:
|
|
195
|
+
// 域文件跨变更同号条目常见,键须含变更名否则先到遮蔽——runtime.md 七个 D-001@v1 分属七变更)。
|
|
196
|
+
// 无「变更:/来源:」行的 legacy 条目不再互折叠(2026-10-14-decision-id-p2-hardening:旧互折叠致
|
|
197
|
+
// 同号 legacy 互相遮蔽——实测 2 条 implemented 条目永久隐身;路由单解析后完全重复已从源头消除)。
|
|
184
198
|
const seen = new Set()
|
|
185
199
|
return hits.filter(h => {
|
|
186
|
-
|
|
200
|
+
if (!h.change) return true
|
|
201
|
+
const key = `${h.file}#${h.id}#${h.change}`
|
|
187
202
|
if (seen.has(key)) return false
|
|
188
203
|
seen.add(key)
|
|
189
204
|
return true
|
|
@@ -221,6 +236,16 @@ export function matchDecisionsByFiles(indexDir, files) {
|
|
|
221
236
|
return result
|
|
222
237
|
}
|
|
223
238
|
|
|
239
|
+
/** 决策展示行变更名限定词(2026-10-14-supersedes-mislink-and-change-display):D-xxx 是变更内
|
|
240
|
+
* 局部序号,跨变更同号条目常见(知识库 D-001@v1 ×296、其中 192 条同题)——四处展示面
|
|
241
|
+
* ({DECISION_HITS}/轻量道防复潮/knowledge-gate/decision-touch)经本函数统一渲染限定词消歧;
|
|
242
|
+
* change 缺失(legacy 无「变更:」行)固定降级「变更未记录」——不编造、不省略。
|
|
243
|
+
* 单一源防四处文案漂移(与 deathPathNote 同址同规约)。 */
|
|
244
|
+
export function decisionChangeQualifier(change) {
|
|
245
|
+
const c = String(change || '').trim()
|
|
246
|
+
return c ? `变更 ${c}` : '变更未记录'
|
|
247
|
+
}
|
|
248
|
+
|
|
224
249
|
/** 死路注记短句提取(消费方渲染用):理由里「死路:…」到首个句读为止;无标记时截前 60 字。
|
|
225
250
|
* 单一源供 flow 知识注入段与 complete.js knowledge-gate 回显共用(2026-09-28-knowledge-inject-ranking)。 */
|
|
226
251
|
export function deathPathNote(reason) {
|
package/src/knowledge-stats.js
CHANGED
|
@@ -99,6 +99,10 @@ export function buildHitMatrix(knowledgeDir, runtimeDir, { sinceDays = 30 } = {}
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
// ── FR 索引实验聚合(2026-09-18-fr-index-l1 后续钩子·L3 裁决仪表盘)──
|
|
102
|
+
// L3 已裁决冻结(2026-10-15-fr-chain-l3-adjudication D-001@v1):证伪三条件全中(承接引用率
|
|
103
|
+
// 1/452≈0% / fr-supersede 窗口 2 次 / fr-duplicate-warning 24 次零候选),条款明文合法结局——
|
|
104
|
+
// 活规格树不建,fr 索引降级保留检索/注入面;本区指标转历史观察,候选视图退役为冻结注记。
|
|
105
|
+
const FR_L3_ADJUDICATED_NOTE = '已裁决(2026-10-15 D-001@v1):L3 冻结不建,fr 索引保留检索/注入面——本区指标转历史观察(复潮条件见裁决条目)'
|
|
102
106
|
|
|
103
107
|
/**
|
|
104
108
|
* 聚合 L1 四类 fr-* 遥测事件 + knowledge/fr/ 索引面统计——L3 裁决(design 实验证伪条款)
|
|
@@ -314,7 +318,7 @@ export async function cmdKnowledgeStats(dir, args, opts = {}) {
|
|
|
314
318
|
if (frIndex.present) {
|
|
315
319
|
const e = frIndex.events
|
|
316
320
|
const i = frIndex.index
|
|
317
|
-
lines.push(`🔬 FR 索引实验(窗口内事件 / 索引面全量——L3 裁决指标,证伪条款见 2026-09-18-fr-index-l1 design
|
|
321
|
+
lines.push(`🔬 FR 索引实验(窗口内事件 / 索引面全量——L3 裁决指标,证伪条款见 2026-09-18-fr-index-l1 design)|${FR_L3_ADJUDICATED_NOTE}`)
|
|
318
322
|
lines.push(` 注入 fr-inject ${e.frInject} 次(${e.frInjectChanges} 变更) | 取代链 fr-supersede ${e.frSupersede} 次(${e.frSupersedeChanges} 变更) | 重复拦截 fr-duplicate-warning ${e.frDuplicateWarning} 次(${e.frDuplicateWarningCandidates || e.frDuplicateCandidates || 0} 候选)`)
|
|
319
323
|
lines.push(` 腐烂 suspect fr-rot-suspect ${e.frRotSuspect || 0} 次(${e.frRotSuspectChanges || 0} 变更${e.frRotSuspectByDomain && e.frRotSuspectByDomain.length > 0 ? `,按域 ${e.frRotSuspectByDomain.map(d => `${d.domain}×${d.count}`).join(',')}` : ''})`)
|
|
320
324
|
if (e.frUnreferenced.length > 0) {
|
|
@@ -325,10 +329,8 @@ export async function cmdKnowledgeStats(dir, args, opts = {}) {
|
|
|
325
329
|
lines.push(` 承接引用率:${frIndex.supersedeRate === null ? '—(索引空)' : `${(frIndex.supersedeRate * 100).toFixed(0)}%(${e.frSupersedeChanges}/${i.sourceChanges},分子窗口内/分母全量——裁决用大窗口)`}`)
|
|
326
330
|
const cand = frIndex.adjudicationCandidates || []
|
|
327
331
|
if (cand.length > 0) {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
lines.push(` - ${c.id} suspect×${c.suspect} unref×${c.unreferenced}${c.change ? `(来源 ${c.change})` : ''}`)
|
|
331
|
-
}
|
|
332
|
+
// L3 冻结后候选视图退役(数据面 adjudicationCandidates 保留 JSON 可查)——显示冻结注记
|
|
333
|
+
lines.push(` 裁决候选视图已退役|${FR_L3_ADJUDICATED_NOTE}`)
|
|
332
334
|
}
|
|
333
335
|
} else {
|
|
334
336
|
lines.push('(FR 索引面不存在——knowledge/fr/ 目录缺失或无条目;归档完整流程变更后此处可查)')
|
|
@@ -375,12 +377,12 @@ export async function cmdKnowledgeStats(dir, args, opts = {}) {
|
|
|
375
377
|
|
|
376
378
|
lines.push(`遥测计数(窗口内):注入 ${result.totalInjects} 次 | 归类 ${result.totalClassifies} 次${lastEventAt ? ` | 数据截至 ${lastEventAt.slice(0, 10)}` : ''}`)
|
|
377
379
|
|
|
378
|
-
// FR 索引实验(L3
|
|
380
|
+
// FR 索引实验(L3 裁决仪表盘——证伪条款出数面;L3 已裁决冻结 2026-10-15,指标转历史观察)
|
|
379
381
|
if (frIndex.present) {
|
|
380
382
|
const e = frIndex.events
|
|
381
383
|
const i = frIndex.index
|
|
382
384
|
lines.push('')
|
|
383
|
-
lines.push(`🔬 FR 索引实验(窗口内事件 / 索引面全量——L3 裁决指标,证伪条款见 2026-09-18-fr-index-l1 design
|
|
385
|
+
lines.push(`🔬 FR 索引实验(窗口内事件 / 索引面全量——L3 裁决指标,证伪条款见 2026-09-18-fr-index-l1 design)|${FR_L3_ADJUDICATED_NOTE}`)
|
|
384
386
|
lines.push(` 注入 fr-inject ${e.frInject} 次(${e.frInjectChanges} 变更) | 取代链 fr-supersede ${e.frSupersede} 次(${e.frSupersedeChanges} 变更) | 重复拦截 fr-duplicate-warning ${e.frDuplicateWarning} 次(${e.frDuplicateCandidates} 候选)`)
|
|
385
387
|
if (e.frUnreferenced.length > 0) {
|
|
386
388
|
const top = e.frUnreferenced.slice(0, 5).map((u) => `${u.domain}×${u.count}`).join(',')
|
|
@@ -388,14 +390,11 @@ export async function cmdKnowledgeStats(dir, args, opts = {}) {
|
|
|
388
390
|
}
|
|
389
391
|
lines.push(` 索引面:${i.entries} 条(active ${i.active} / superseded ${i.superseded})| 来源变更 ${i.sourceChanges} 个 | 域 ${i.domains.join(', ') || '—'}`)
|
|
390
392
|
lines.push(` 承接引用率:${frIndex.supersedeRate === null ? '—(索引空)' : `${(frIndex.supersedeRate * 100).toFixed(0)}%(${e.frSupersedeChanges}/${i.sourceChanges},分子窗口内/分母全量——裁决用大窗口)`}`)
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
+
// 裁决候选视图退役(L3 冻结,2026-10-15-fr-chain-l3-adjudication D-001@v1)——数据面
|
|
394
|
+
// adjudicationCandidates 保留 JSON 可查,显示面只留冻结注记。
|
|
393
395
|
const cand = frIndex.adjudicationCandidates || []
|
|
394
396
|
if (cand.length > 0) {
|
|
395
|
-
lines.push(
|
|
396
|
-
for (const c of cand.slice(0, 10)) {
|
|
397
|
-
lines.push(` - ${c.id} suspect×${c.suspect} unref×${c.unreferenced}${c.change ? `(来源 ${c.change})` : ''}`)
|
|
398
|
-
}
|
|
397
|
+
lines.push(` 裁决候选视图已退役|${FR_L3_ADJUDICATED_NOTE}`)
|
|
399
398
|
}
|
|
400
399
|
}
|
|
401
400
|
|