sillyspec 3.33.8 → 3.34.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/AGENTS.md +3 -2
- package/eval/knowledge/README.md +119 -0
- package/eval/knowledge/baseline.json +907 -0
- package/eval/knowledge/datasets/organic.jsonl +79 -0
- package/eval/knowledge/fixtures/index.mjs +132 -0
- package/eval/knowledge/lib/dataset.mjs +171 -0
- package/eval/knowledge/lib/engine.mjs +136 -0
- package/eval/knowledge/lib/faces.mjs +88 -0
- package/eval/knowledge/lib/metrics.mjs +56 -0
- package/eval/knowledge/lib/probes.mjs +98 -0
- package/eval/knowledge/lib/report.mjs +82 -0
- package/eval/knowledge/lib/repos.mjs +41 -0
- package/eval/knowledge/lib/snapshot.mjs +55 -0
- package/eval/knowledge/run.mjs +206 -0
- package/package.json +3 -2
- package/src/config-schema.js +22 -0
- package/src/decision-distill.js +38 -0
- package/src/flow-parity.js +10 -50
- package/src/flow-review.js +4 -0
- package/src/flow.js +37 -10
- package/src/index.js +7 -46
- package/src/knowledge-graph.js +243 -25
- package/src/knowledge-match.js +130 -21
- package/src/knowledge-stats.js +3 -0
- package/src/knowledge-vector.js +29 -4
- package/src/redomain-unmapped.js +163 -2
- package/src/run/complete.js +20 -1
- package/src/run/gate-snapshot.js +3 -1
- package/src/run/prompt.js +67 -37
- package/src/run/quick-audit.js +58 -10
- package/src/run/verify-quality-scan.js +52 -7
- package/src/stages/execute.js +64 -16
- package/src/stages/knowledge.js +57 -28
- package/src/verify-postcheck.js +330 -3
- package/templates/agents-instruction.md +1 -1
- package/src/knowledge-quicklog.js +0 -261
package/src/knowledge-match.js
CHANGED
|
@@ -62,6 +62,10 @@ function isDecisionRoute(entry) {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
const DECISION_HEADER_RE = /^##\s+(D-(?:[A-Za-z0-9_\-\u4e00-\u9fff]+-)?\d+@v\d+)\s*(.*)$/
|
|
65
|
+
|
|
66
|
+
/** 底稿模板标题特征词(与 4A provenance 分类同源单一口径):harvestSlot4Decision 产的
|
|
67
|
+
* process 级流程取舍框架词——不是具体方案否决,deathPath 不应赋予(2026-11-02-series-final-cleanup)。 */
|
|
68
|
+
const TEMPLATE_TITLE_RE = /设计期风险与放弃方案|风险与死路|design\s*槽\s*4\s*收割|底稿/
|
|
65
69
|
const DECISION_FIELD_RE = /^(状态|文件|理由|否决理由|复潮条件|supersedes)\s*[::]\s*(.*)$/
|
|
66
70
|
// 「锚点:」单独标签精确匹配读入(仿 docs-check.js parseDecisionEntries 先例)——绝不并入
|
|
67
71
|
// DECISION_FIELD_RE:其 else-if 链末尾的 reason 回填(else if (!cur.reason))会把锚点值
|
|
@@ -130,7 +134,11 @@ function parseDecisionFile(filePath, file) {
|
|
|
130
134
|
// 死路注记识别(2026-09-28-knowledge-inject-ranking):「理由」含「死路:」字面标记的教训型
|
|
131
135
|
// 条目——记录它的决策本体是 accepted/implemented(死路只是理由里的注记),按状态过滤会被
|
|
132
136
|
// 压制到组外,恰丢最该防复潮的(实测:D-001@v1 枚举开放世界排 148/188 位永不回显)。
|
|
133
|
-
|
|
137
|
+
// 底稿资格门(2026-11-02-series-final-cleanup):模板标题条目(设计期风险与放弃方案/风险
|
|
138
|
+
// 与死路/底稿——harvestSlot4Decision 产的 process 级流程取舍)不获 deathPath——它们不是真
|
|
139
|
+
// 否决,标 deathPath 会占防复潮席位且使停车场清零条件永不可满足(终审实证 4C/5/6 归档蒸馏
|
|
140
|
+
// 又产出 4 条底稿 D-485~488)
|
|
141
|
+
cur.deathPath = /死路[::]/.test(cur.reason || '') && !TEMPLATE_TITLE_RE.test(cur.title || '')
|
|
134
142
|
hits.push(cur)
|
|
135
143
|
}
|
|
136
144
|
cur = null
|
|
@@ -264,21 +272,31 @@ function escapeRegex(s) {
|
|
|
264
272
|
* - 过短关键词(<2 字符)视为噪音,不参与匹配
|
|
265
273
|
* - 非 ASCII(中文等)用子串匹配
|
|
266
274
|
* - ASCII 关键词用词边界匹配,避免子串误命中(如 "DB" 不命中 "dashboard")
|
|
275
|
+
* 词边界口径(2026-11-02-knowledge-rank-denoise FR-02):`_` 是词字符、`-` 是边界——
|
|
276
|
+
* node 不命中 node_modules,但 worktree 仍命中 kebab-case 变更名(2026-05-29-worktree-*)。
|
|
277
|
+
* 导出供评测适配层(eval/knowledge)复用同一判定——零行为变化。
|
|
267
278
|
*/
|
|
268
|
-
function keywordMatchesContext(keyword, contextLower) {
|
|
279
|
+
export function keywordMatchesContext(keyword, contextLower) {
|
|
269
280
|
const kw = keyword.toLowerCase().trim()
|
|
270
281
|
if (kw.length < 2) return false
|
|
271
282
|
if (/[^\x00-\x7f]/.test(kw)) return contextLower.includes(kw)
|
|
272
|
-
return new RegExp(`(^|[^a-z0-
|
|
283
|
+
return new RegExp(`(^|[^a-z0-9_])${escapeRegex(kw)}([^a-z0-9_]|$)`).test(contextLower)
|
|
273
284
|
}
|
|
274
285
|
|
|
275
286
|
/** 查询词片抽取:CJK 连续段 bigram + ≥4 字符 ASCII 词(小写;短词与标点数字不参与;
|
|
276
|
-
*
|
|
277
|
-
|
|
287
|
+
* 纯 ASCII 查询不产词片——中文知识库,英文查询维持既有零命中行为)。
|
|
288
|
+
* snake/kebab 复合词保留整词并额外产出 ≥4 字符拆分片段(FR-02:查询 worktree_apply 时
|
|
289
|
+
* worktree、apply 仍可命中正文只写 worktree 的条目)。导出供评测与单测(零行为变化)。 */
|
|
290
|
+
export function queryShingles(taskContext) {
|
|
278
291
|
const raw = String(taskContext || '')
|
|
279
292
|
const shingles = new Set()
|
|
280
293
|
if (/[\u4e00-\u9fff]/.test(raw)) {
|
|
281
|
-
for (const w of raw.toLowerCase().match(/[a-z][a-z0-
|
|
294
|
+
for (const w of raw.toLowerCase().match(/[a-z][a-z0-9_-]{3,}/g) || []) {
|
|
295
|
+
shingles.add(w)
|
|
296
|
+
if (/[_-]/.test(w)) {
|
|
297
|
+
for (const part of w.split(/[_-]/)) if (part.length >= 4) shingles.add(part)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
282
300
|
}
|
|
283
301
|
for (const seg of raw.match(/[\u4e00-\u9fff]+/g) || []) {
|
|
284
302
|
for (let i = 0; i < seg.length - 1; i++) shingles.add(seg.slice(i, i + 2))
|
|
@@ -286,10 +304,11 @@ function queryShingles(taskContext) {
|
|
|
286
304
|
return [...shingles]
|
|
287
305
|
}
|
|
288
306
|
|
|
289
|
-
/** ASCII 词片词边界命中(CJK 词片 includes 即可);转义防正则注入。
|
|
307
|
+
/** ASCII 词片词边界命中(CJK 词片 includes 即可);转义防正则注入。
|
|
308
|
+
* 边界口径与 keywordMatchesContext 统一:`_` 词字符、`-` 边界(FR-02)。 */
|
|
290
309
|
function asciiWordHit(sh, text) {
|
|
291
310
|
if (!/^[a-z]/.test(sh)) return text.includes(sh)
|
|
292
|
-
return new RegExp(`(^|[^a-z0-
|
|
311
|
+
return new RegExp(`(^|[^a-z0-9_])${sh.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^a-z0-9_]|$)`, 'i').test(text)
|
|
293
312
|
}
|
|
294
313
|
|
|
295
314
|
/** 停车场文件(未归类 unmapped 域——decisions 与 fr 两面):检索核心零返回,文件本身
|
|
@@ -302,6 +321,44 @@ export function isParkingFile(file) {
|
|
|
302
321
|
return PARKING_FILES.has(String(file || '').replace(/\\/g, '/'))
|
|
303
322
|
}
|
|
304
323
|
|
|
324
|
+
/** 已修复条目显式词(FR-03,2026-11-02-knowledge-rank-denoise):只认 INDEX display/anchor
|
|
325
|
+
* 的显式词,不读正文 heading(锚点→heading 归一脆弱);绿色标记不作唯一键(平台实测
|
|
326
|
+
* 6 条绿色非词条目中 4 条是现行事实/债务,误杀代价高于漏 2 条已解决项)。 */
|
|
327
|
+
const RESOLVED_WORDS = /已修复|已解决|已退役/
|
|
328
|
+
|
|
329
|
+
/** 锚点/标题容错归一(坑 knowledge-hits-anchor-drift T4):锚点三代规则并存(CLI slug /
|
|
330
|
+
* INDEX 手写 / 平台 slugify_anchor)对 emoji 前缀、点号、斜杠、首尾中划线的处理互不一致
|
|
331
|
+
* ——归一到「仅字母数字 + 小写」后比对,三代产物归一相等;截断(60 字符)由调用方前缀
|
|
332
|
+
* 容忍处理。单一源:knowledge 命令校验与图谱 section 匹配(2026-11-02-scope-recall)共用。 */
|
|
333
|
+
export function normalizeAnchorForCompare(s) {
|
|
334
|
+
return String(s || '').replace(/[^\p{L}\p{N}]+/gu, '').toLowerCase()
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** 条目是否「已知问题-已修复」类(默认注入/检索排除;--include-resolved 才返回)。
|
|
338
|
+
* 双源判定(2026-11-03 接线):① INDEX display/anchor 标题词(既有——覆盖未迁移到状态行格式的条目)
|
|
339
|
+
* ② 目标文件正文状态行(`- 状态:已修复/已解决/已退役`——4C 落盘后主判据)。任一命中即 resolved。 */
|
|
340
|
+
function isResolvedRoute(entry, knowledgeRoot) {
|
|
341
|
+
const file = String(entry.file || '').replace(/\\/g, '/')
|
|
342
|
+
const isKnownIssues = entry.category === 'Known Issues' || /(^|\/)known-issues\.md$/.test(file)
|
|
343
|
+
if (!isKnownIssues) return false
|
|
344
|
+
if (RESOLVED_WORDS.test(`${entry.display || ''} ${entry.anchor || ''}`)) return true
|
|
345
|
+
// 正文状态行判据:读目标文件,按 anchor 匹配 heading 后查段内状态行
|
|
346
|
+
if (knowledgeRoot && entry.anchor) {
|
|
347
|
+
try {
|
|
348
|
+
const content = readFileSync(join(knowledgeRoot, file), 'utf8')
|
|
349
|
+
const key = normalizeAnchorForCompare(entry.anchor)
|
|
350
|
+
const blocks = content.replace(/\r\n?/g, '\n').split(/^## /m).slice(1)
|
|
351
|
+
for (const b of blocks) {
|
|
352
|
+
const hn = normalizeAnchorForCompare(b.split('\n')[0])
|
|
353
|
+
if (hn === key || (hn.length >= 6 && key.length >= 6 && (hn.startsWith(key) || key.startsWith(hn)))) {
|
|
354
|
+
return /^\s*-\s*状态[::]\s*(已修复|已解决|已退役)/m.test(b)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
} catch { /* 文件不可读回退标题词 */ }
|
|
358
|
+
}
|
|
359
|
+
return false
|
|
360
|
+
}
|
|
361
|
+
|
|
305
362
|
/**
|
|
306
363
|
* 零命中查询侧词片回退(2026-09-29-decision-route-vocab):路由行零命中时,只测「查询自己的
|
|
307
364
|
* 词片」——逐 decisions 域文件数词片在条目文本(标题∪理由)的出现次数,落在「跨条目复现但
|
|
@@ -380,7 +437,7 @@ export function fallbackByQueryShingles(indexDir, taskContext) {
|
|
|
380
437
|
* 命中的 Decisions 路由行所指向 decisions/<域>.md 内的全部 D-xxx@vN 条目,rejected 优先排序;
|
|
381
438
|
* 无 decisions 库 / 路由行未命中 → decisionHits: [](其余字段行为与现状一致)。
|
|
382
439
|
*/
|
|
383
|
-
export function matchByRouting(indexDir, taskContext) {
|
|
440
|
+
export function matchByRouting(indexDir, taskContext, opts = {}) {
|
|
384
441
|
const indexPath = join(indexDir, 'INDEX.md')
|
|
385
442
|
|
|
386
443
|
// INDEX.md 不存在
|
|
@@ -413,19 +470,59 @@ export function matchByRouting(indexDir, taskContext) {
|
|
|
413
470
|
// 只命中停车场行 → 按零命中处理(hybrid 链继续走向量/图/词片层),json.parkingFiltered
|
|
414
471
|
// 披露被滤文件供 search 面提示人工直查(可见性不因去噪丢失)。
|
|
415
472
|
const parkingFiltered = [...new Set(matchedRaw.filter((e) => isParkingFile(e.file)).map((e) => e.file))]
|
|
416
|
-
const
|
|
473
|
+
const parkingExcluded = matchedRaw.filter((e) => !isParkingFile(e.file))
|
|
474
|
+
// 已修复过滤(FR-03,2026-11-02-knowledge-rank-denoise):known-issues 显式词条目默认排除,
|
|
475
|
+
// includeResolved 才返回;json.resolvedFiltered 披露(仿 parkingFiltered)。
|
|
476
|
+
const resolvedFiltered = opts.includeResolved ? [] : parkingExcluded.filter((e) => isResolvedRoute(e, indexDir))
|
|
477
|
+
const resolvedFiles = [...new Set(resolvedFiltered.map((e) => e.file))]
|
|
478
|
+
const resolvedNote = resolvedFiles.length > 0
|
|
479
|
+
? { resolvedFiltered: resolvedFiles, resolvedNote: '已修复条目已过滤——需查看加 --include-resolved' }
|
|
480
|
+
: {}
|
|
481
|
+
const matchedAll = parkingExcluded.filter((e) => !resolvedFiltered.includes(e))
|
|
482
|
+
|
|
483
|
+
// 路由行分离(FR-04,2026-11-02-knowledge-rank-denoise):fr/decisions 路由行不进面向人的
|
|
484
|
+
// entries(各有 decisionHits / readActiveFrDigest 专用通道),改走条件式 routeHits——
|
|
485
|
+
// 消费面据此出指针行,避免静默丢失 implemented 决策与 FR 的可见性;顶层键仅在非空时出现
|
|
486
|
+
// (无路由行命中的场景保持旧四键+decisionHits 结构不变)。
|
|
487
|
+
const isRouteRow = (e) => isDecisionRoute(e) || /^fr\//.test(String(e.file || '').replace(/\\/g, '/'))
|
|
488
|
+
const humanEntries = matchedAll.filter((e) => !isRouteRow(e))
|
|
489
|
+
const routeHits = matchedAll.filter(isRouteRow)
|
|
490
|
+
|
|
491
|
+
// DF 相关度排序(FR-05,2026-11-02-knowledge-rank-denoise):分母 = entries 可展示语料
|
|
492
|
+
// (排除 fr/decisions 路由行、停车场、已修复条目);命中数优先 → Σ(1/DF) 破平 → INDEX 行序兜底。
|
|
493
|
+
// 泛词(出现在大量路由行的流程词)权重被压缩,长尾具体词上浮;同分顺序确定(重复运行一致)。
|
|
494
|
+
const humanCorpus = allEntries.filter((e) => !isRouteRow(e) && !isParkingFile(e.file) && !isResolvedRoute(e, indexDir))
|
|
495
|
+
const dfMap = new Map()
|
|
496
|
+
for (const e of humanCorpus) {
|
|
497
|
+
const seenKw = new Set()
|
|
498
|
+
for (const kw of e.keywords) {
|
|
499
|
+
const k = String(kw).toLowerCase()
|
|
500
|
+
if (seenKw.has(k)) continue
|
|
501
|
+
seenKw.add(k)
|
|
502
|
+
dfMap.set(k, (dfMap.get(k) || 0) + 1)
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const scoredHuman = humanEntries.map((e, i) => {
|
|
506
|
+
const matchedKws = [...new Set(e.keywords.map((k) => String(k).toLowerCase()))]
|
|
507
|
+
.filter((k) => keywordMatchesContext(k, contextLower))
|
|
508
|
+
const hits = matchedKws.length
|
|
509
|
+
const rarity = matchedKws.reduce((s, k) => s + 1 / Math.max(1, dfMap.get(k) || 1), 0)
|
|
510
|
+
return { e, i, hits, rarity }
|
|
511
|
+
})
|
|
512
|
+
scoredHuman.sort((a, b) => (b.hits - a.hits) || (b.rarity - a.rarity) || (a.i - b.i))
|
|
513
|
+
const rankedHuman = scoredHuman.map((x) => x.e)
|
|
417
514
|
|
|
418
|
-
if (
|
|
515
|
+
if (rankedHuman.length === 0 && routeHits.length === 0) {
|
|
419
516
|
return {
|
|
420
517
|
matched: false,
|
|
421
518
|
entries: [],
|
|
422
519
|
report: 'Status: no matches',
|
|
423
|
-
json: { matched: false, entry_count: 0, entries: [], ...(parkingFiltered.length > 0 ? { parkingFiltered } : {}) },
|
|
520
|
+
json: { matched: false, entry_count: 0, entries: [], ...(parkingFiltered.length > 0 ? { parkingFiltered } : {}), ...resolvedNote },
|
|
424
521
|
decisionHits: []
|
|
425
522
|
}
|
|
426
523
|
}
|
|
427
524
|
|
|
428
|
-
const sources =
|
|
525
|
+
const sources = [...rankedHuman, ...routeHits].map(e => {
|
|
429
526
|
const base = e.anchor ? `${e.file}#${e.anchor}` : e.file
|
|
430
527
|
return ` - ${base}`
|
|
431
528
|
}).join('\n')
|
|
@@ -434,20 +531,25 @@ export function matchByRouting(indexDir, taskContext) {
|
|
|
434
531
|
'Knowledge Context',
|
|
435
532
|
'─────────────────',
|
|
436
533
|
`Status: matched`,
|
|
437
|
-
`Entries: ${
|
|
534
|
+
`Entries: ${rankedHuman.length}`,
|
|
535
|
+
...(routeHits.length > 0 ? [`Route hits: ${routeHits.length}`] : []),
|
|
438
536
|
'Sources:',
|
|
439
537
|
sources
|
|
440
538
|
].join('\n')
|
|
441
539
|
|
|
442
540
|
const json = {
|
|
443
541
|
matched: true,
|
|
444
|
-
entry_count:
|
|
445
|
-
entries:
|
|
542
|
+
entry_count: rankedHuman.length,
|
|
543
|
+
entries: rankedHuman.map(e => ({
|
|
446
544
|
file: e.file,
|
|
447
545
|
anchor: e.anchor,
|
|
448
546
|
keywords: e.keywords,
|
|
449
547
|
category: e.category
|
|
450
|
-
}))
|
|
548
|
+
})),
|
|
549
|
+
...(routeHits.length > 0 ? {
|
|
550
|
+
routeHits: routeHits.map(e => ({ file: e.file, anchor: e.anchor, keywords: e.keywords, category: e.category })),
|
|
551
|
+
} : {}),
|
|
552
|
+
...resolvedNote
|
|
451
553
|
}
|
|
452
554
|
|
|
453
555
|
// decisionHits(task-04):任务上下文命中的 Decisions 路由行 → 解析其指向的
|
|
@@ -457,7 +559,7 @@ export function matchByRouting(indexDir, taskContext) {
|
|
|
457
559
|
// D-001@v1 排 148/188 位)。重叠判据复用 fr-index frTitleOverlap(字符 bigram,
|
|
458
560
|
// 封闭面非语义判定)。
|
|
459
561
|
// 不引入新顶层 hits 字段,不改 matched/entries/report/json 四个既有键。
|
|
460
|
-
const decisionRoutes =
|
|
562
|
+
const decisionRoutes = routeHits.filter(isDecisionRoute)
|
|
461
563
|
const allDecisionHits = decisionRoutes.length > 0 ? parseDecisionEntries(indexDir, decisionRoutes) : []
|
|
462
564
|
// 评分文本 = id+标题;评分前剥除数字与标点(2026-09-28-knowledge-score-denoise:{DECISION_HITS}
|
|
463
565
|
// 场景 taskContext=ASCII 变更名,日期「2026-09-28」与 id「D-009@v1」共享 -0/09 数字 bigram 即
|
|
@@ -473,7 +575,14 @@ export function matchByRouting(indexDir, taskContext) {
|
|
|
473
575
|
...allDecisionHits.filter((h) => h.status !== 'rejected' && !h.deathPath).sort(byOverlapDesc),
|
|
474
576
|
].map((h) => ({ ...h, score: relScore(h) }))
|
|
475
577
|
|
|
476
|
-
return {
|
|
578
|
+
return {
|
|
579
|
+
matched: true,
|
|
580
|
+
entries: rankedHuman,
|
|
581
|
+
...(routeHits.length > 0 ? { routeHits } : {}),
|
|
582
|
+
report,
|
|
583
|
+
json,
|
|
584
|
+
decisionHits,
|
|
585
|
+
}
|
|
477
586
|
}
|
|
478
587
|
|
|
479
588
|
/**
|
|
@@ -481,8 +590,8 @@ export function matchByRouting(indexDir, taskContext) {
|
|
|
481
590
|
* 异步三层入口(平台向量优先于本地回退)见 knowledge-vector.js matchKnowledgeHybrid
|
|
482
591
|
* (2026-09-29-knowledge-vector-recall)——同步调用方与既有测试全部走本函数。
|
|
483
592
|
*/
|
|
484
|
-
export function matchKnowledge(indexDir, taskContext) {
|
|
485
|
-
const r = matchByRouting(indexDir, taskContext)
|
|
593
|
+
export function matchKnowledge(indexDir, taskContext, opts = {}) {
|
|
594
|
+
const r = matchByRouting(indexDir, taskContext, opts)
|
|
486
595
|
if (r.matched) return r
|
|
487
596
|
const fb = fallbackByQueryShingles(indexDir, taskContext)
|
|
488
597
|
return fb || r
|
package/src/knowledge-stats.js
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
* 职责:聚合 .runtime/knowledge-hits.jsonl 遥测事件流(task-01 KnowledgeHitsAPI 写入:
|
|
5
5
|
* task-04 注入点写 type:inject、task-02 classify 写 type:classify),输出近 N 天命中矩阵
|
|
6
6
|
* (文件 × 次数 × 最近命中时间)与从未命中清单(对照 INDEX.md 全集标注疑似死重)。
|
|
7
|
+
* 注(2026-11-02-knowledge-engine-tail FR-02):scope 展示面走独立字段 `scopeMatchedFiles`
|
|
8
|
+
* (top3+3 展示子集)——命中矩阵/neverHit 只按 `matchedFiles`(查询通道)统计,scope 通道
|
|
9
|
+
* 不参与 neverHit 判定(防同一决策域文件反复计命中原决定死重)。
|
|
7
10
|
* 纯只读:不写 hits/INDEX/知识文件任何输入;死重清单只报告,不自动删除/合并(非目标)。
|
|
8
11
|
*
|
|
9
12
|
* 计数口径:
|
package/src/knowledge-vector.js
CHANGED
|
@@ -236,6 +236,24 @@ export function buildResultFromPlatform(indexDir, pvResults) {
|
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
/**
|
|
240
|
+
* base(路由/向量结果)+ scopeFiles → 融合结果(2026-11-02-knowledge-engine-tail FR-01)。
|
|
241
|
+
* 路由命中与向量命中两条路径共用本实现(消除「同一 early-return 病只修一半」):
|
|
242
|
+
* scopeFiles 经 graphPathCandidates 规整;scope 构建/遍历失败返回 base 原对象(fail-soft)。
|
|
243
|
+
*/
|
|
244
|
+
async function fuseBaseWithScope(base, indexDir, scopeFiles) {
|
|
245
|
+
if (!base || !base.matched) return base
|
|
246
|
+
const raw = Array.isArray(scopeFiles) ? scopeFiles.filter(Boolean) : []
|
|
247
|
+
if (raw.length === 0) return base
|
|
248
|
+
try {
|
|
249
|
+
const { graphPathCandidates, getKnowledgeGraph, scopeRecall, fuseScopeRecall } = await import('./knowledge-graph.js')
|
|
250
|
+
const files = graphPathCandidates(raw)
|
|
251
|
+
if (files.length === 0) return base
|
|
252
|
+
const specRoot = String(indexDir).replace(/[\\/]knowledge[\\/]?$/, '')
|
|
253
|
+
return fuseScopeRecall(base, scopeRecall(getKnowledgeGraph(specRoot), files)) || base
|
|
254
|
+
} catch { return base }
|
|
255
|
+
}
|
|
256
|
+
|
|
239
257
|
/**
|
|
240
258
|
* 异步三层检索入口(四消费方使用:flow 注入段/complete 门/prompt {DECISION_HITS}/
|
|
241
259
|
* knowledge search CLI):路由 → 平台向量(已连接且开关开)→ scope 遍历 → 本地词片 → 空。
|
|
@@ -245,16 +263,23 @@ export function buildResultFromPlatform(indexDir, pvResults) {
|
|
|
245
263
|
* 缺省/空数组 → 遍历层整体跳过,行为与不携带该参数的旧行为逐字段一致(回归钉死)。
|
|
246
264
|
*/
|
|
247
265
|
export async function matchKnowledgeHybrid(indexDir, taskContext, opts = {}) {
|
|
248
|
-
const r = matchByRouting(indexDir, taskContext)
|
|
249
|
-
|
|
266
|
+
const r = matchByRouting(indexDir, taskContext, opts)
|
|
267
|
+
const scopeFiles = Array.isArray(opts.scopeFiles) ? opts.scopeFiles.filter(Boolean) : []
|
|
268
|
+
if (r.matched) {
|
|
269
|
+
// 融合(2026-11-02-knowledge-fusion):路由命中后仍跑 scope——热文件触达的 rejected/手工条目
|
|
270
|
+
// 原先被 early-return 永久遮住。只追加 scopeDecisionHits/scopeEntries(不重排路由结果);
|
|
271
|
+
// scope 构建失败原样返回路由结果;仍不跑平台向量(路由命中零网络)。
|
|
272
|
+
return fuseBaseWithScope(r, indexDir, scopeFiles)
|
|
273
|
+
}
|
|
250
274
|
const cwd = opts.cwd || dirname(dirname(String(indexDir)))
|
|
251
275
|
if (vectorEnabled(cwd)) {
|
|
252
276
|
const pv = await platformVectorRecall({ cwd, query: taskContext, limit: opts.limit || 10 })
|
|
253
277
|
const built = pv ? buildResultFromPlatform(indexDir, pv.results) : null
|
|
254
|
-
|
|
278
|
+
// 向量命中路径同样融合(FR-01 对称性):平台向量自动探测开启、词面弱、路由零命中时,
|
|
279
|
+
// scope 增量不应因向量 early-return 丢失。
|
|
280
|
+
if (built) return fuseBaseWithScope(built, indexDir, scopeFiles)
|
|
255
281
|
}
|
|
256
282
|
// scope 遍历层(第三层):只在 scope 非空且向量零命中后接管;fail-soft(图构建异常静默降级词片)
|
|
257
|
-
const scopeFiles = Array.isArray(opts.scopeFiles) ? opts.scopeFiles.filter(Boolean) : []
|
|
258
283
|
if (scopeFiles.length > 0) {
|
|
259
284
|
try {
|
|
260
285
|
const { buildKnowledgeGraph, scopeRecall, buildScopeRecallResult } = await import('./knowledge-graph.js')
|
package/src/redomain-unmapped.js
CHANGED
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
* 目标域同 id 同变更冲突跳过、幂等;INDEX 路由行随原语内 sync 维护(FR-02 泛词已退役)。
|
|
11
11
|
* 只动 knowledge 活库,不回写归档件(D-unmapped-152 死路:归档是冻结审计面)。
|
|
12
12
|
*/
|
|
13
|
-
import { existsSync, readFileSync } from 'fs'
|
|
13
|
+
import { existsSync, readFileSync, unlinkSync } from 'fs'
|
|
14
14
|
import { join } from 'path'
|
|
15
|
-
import { splitKnowledgeSections, discoverModuleIndex } from './decision-distill.js'
|
|
15
|
+
import { splitKnowledgeSections, discoverModuleIndex, joinKnowledgeFile, syncIndexRoutingLines, DECISIONS_ROUTE_STRIP_TOKENS, FR_ROUTE_STRIP_TOKENS } from './decision-distill.js'
|
|
16
|
+
import { writeAtomicSync } from './fs-atomic.js'
|
|
16
17
|
import { matchedModuleIds, frCoverageFiles, FR_SECTION_RE } from './fr-index.js'
|
|
17
18
|
import { anchorFilePaths } from './knowledge-match.js'
|
|
18
19
|
import { redomainDecisionEntries, sectionMeta } from './decision-redomain.js'
|
|
@@ -181,12 +182,46 @@ export async function cmdRedomainUnmapped(dir, args, opts = {}) {
|
|
|
181
182
|
? (args[args.indexOf('--face') + 1] === 'decisions' ? 'decisions' : args[args.indexOf('--face') + 1] === 'fr' ? 'fr' : 'all')
|
|
182
183
|
: 'all'
|
|
183
184
|
const apply = args.includes('--apply')
|
|
185
|
+
const mapIdx = args.indexOf('--map')
|
|
186
|
+
const mapFile = mapIdx >= 0 ? args[mapIdx + 1] : null
|
|
184
187
|
const emit = (ok, data, error) => console.log(JSON.stringify({ ok, ...data, ...(error ? { error } : {}) }, null, 2))
|
|
185
188
|
|
|
189
|
+
// --map 缺值显式拒绝(评审 P2:静默回落启发式通道 = 显式契约被切到投票且带写盘)
|
|
190
|
+
if (mapIdx >= 0 && (!mapFile || mapFile.startsWith('--'))) {
|
|
191
|
+
emit(false, {}, { code: 'map_value_required', usage: '--map <file.json>([{id, domain}],domain 可为 __prune__)' })
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
|
|
186
195
|
if (!existsSync(join(knowledgeRoot, 'INDEX.md'))) {
|
|
187
196
|
emit(false, {}, { code: 'knowledge_dir_missing', path: knowledgeRoot })
|
|
188
197
|
return
|
|
189
198
|
}
|
|
199
|
+
// --map 显式映射模式(2026-11-02-knowledge-redomain-map FR-01):旁路投票;apply fail-fast
|
|
200
|
+
if (mapFile) {
|
|
201
|
+
let map = null
|
|
202
|
+
try { map = JSON.parse(readFileSync(mapFile, 'utf8')) } catch (e) {
|
|
203
|
+
emit(false, {}, { code: 'map_file_invalid', file: mapFile, detail: e && e.message })
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
if (!Array.isArray(map) || map.some((e) => !e || typeof e.id !== 'string' || typeof e.domain !== 'string')) {
|
|
207
|
+
emit(false, {}, { code: 'map_entries_invalid', expected: '[{id, domain}]' })
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
const faces = face === 'all' ? ['decisions', 'fr'] : [face]
|
|
211
|
+
try {
|
|
212
|
+
if (apply) {
|
|
213
|
+
const results = faces.map((f) => applyFromMap({ knowledgeRoot, archiveRoot, face: f, map }))
|
|
214
|
+
emit(true, { mode: 'apply-map', face, results })
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
const plans = faces.map((f) => planFromMap({ knowledgeRoot, face: f, map }))
|
|
218
|
+
emit(true, { mode: 'dry-run-map', face, plans })
|
|
219
|
+
return
|
|
220
|
+
} catch (e) {
|
|
221
|
+
emit(false, {}, { code: 'map_apply_failed', detail: e && e.message ? e.message : String(e) })
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
}
|
|
190
225
|
if (apply) {
|
|
191
226
|
const faces = face === 'all' ? ['decisions', 'fr'] : [face]
|
|
192
227
|
const { results, plan } = applyUnmappedRedomain({ knowledgeRoot, archiveRoot, faces })
|
|
@@ -202,3 +237,129 @@ export async function cmdRedomainUnmapped(dir, args, opts = {}) {
|
|
|
202
237
|
const plan = planUnmappedRedomain({ knowledgeRoot, archiveRoot })
|
|
203
238
|
emit(true, { mode: 'dry-run', face, decisions: plan.decisions, fr: plan.fr })
|
|
204
239
|
}
|
|
240
|
+
|
|
241
|
+
// ═══ 2026-11-02-knowledge-redomain-map:--map 显式映射模式(FR-01)═══
|
|
242
|
+
// 显式契约 [{id, domain}](domain 可为 __prune__ 删段)——旁路启发式投票;apply fail-fast
|
|
243
|
+
// (显式契约不容半途容错);守恒/幂等断言以 apply 时刻文件为准;supersedes 留源作警告。
|
|
244
|
+
|
|
245
|
+
export const PRUNE_DOMAIN = '__prune__'
|
|
246
|
+
|
|
247
|
+
/** unmapped 文件段解析(face 决定契约)。 */
|
|
248
|
+
function parseUnmappedFace(knowledgeRoot, face) {
|
|
249
|
+
const fromPath = face === 'fr' ? join(knowledgeRoot, 'fr', 'unmapped.md') : join(knowledgeRoot, 'decisions', 'unmapped.md')
|
|
250
|
+
if (!existsSync(fromPath)) return null
|
|
251
|
+
const parsed = face === 'fr'
|
|
252
|
+
? splitKnowledgeSections(readFileSync(fromPath, 'utf8'), { sectionRegex: FR_SECTION_RE, buildId: (n) => n })
|
|
253
|
+
: splitKnowledgeSections(readFileSync(fromPath, 'utf8'))
|
|
254
|
+
const metas = parsed.sections.map((s) => (face === 'fr' ? frMeta(s) : sectionMeta(s))).filter(Boolean)
|
|
255
|
+
return { fromPath, parsed, metas, total: metas.length }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function frMeta(s) {
|
|
259
|
+
const h = FR_HEADER_RE.exec(s.lines[0] || '')
|
|
260
|
+
return h ? { id: h[1], title: (h[2] || '').trim() } : null
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** 显式映射 → 计划:迁移 groups + prunes + unknownIds(部分未知=报错;全部未知=已全部处理)。 */
|
|
264
|
+
export function planFromMap({ knowledgeRoot, face, map }) {
|
|
265
|
+
const src = parseUnmappedFace(knowledgeRoot, face)
|
|
266
|
+
if (!src) throw new Error(`源文件不存在(face=${face}: unmapped.md)`)
|
|
267
|
+
const byId = new Map(src.metas.map((m) => [m.id, m]))
|
|
268
|
+
const unknownIds = map.map((e) => e.id).filter((id) => !byId.has(id))
|
|
269
|
+
const groups = new Map()
|
|
270
|
+
const prunes = []
|
|
271
|
+
for (const e of map) {
|
|
272
|
+
if (!byId.has(e.id)) continue
|
|
273
|
+
if (e.domain === PRUNE_DOMAIN) { prunes.push(e.id); continue }
|
|
274
|
+
if (!groups.has(e.domain)) groups.set(e.domain, [])
|
|
275
|
+
groups.get(e.domain).push(e.id)
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
face,
|
|
279
|
+
total: src.total,
|
|
280
|
+
groups: [...groups.entries()].map(([domain, ids]) => ({ domain, ids })).sort((a, b) => a.domain.localeCompare(b.domain)),
|
|
281
|
+
prunes,
|
|
282
|
+
unknownIds,
|
|
283
|
+
alreadyApplied: unknownIds.length === map.length && map.length > 0,
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** __prune__ 原语:从源文件删段——精确计数断言、裁空删文件 + INDEX 同步(prune-only 也调)。 */
|
|
288
|
+
export function pruneUnmappedEntries({ knowledgeRoot, face, anchors }) {
|
|
289
|
+
const src = parseUnmappedFace(knowledgeRoot, face)
|
|
290
|
+
if (!src) throw new Error(`源文件不存在(face=${face}: unmapped.md)`)
|
|
291
|
+
const anchorSet = new Set(anchors)
|
|
292
|
+
const staying = []
|
|
293
|
+
const pruned = []
|
|
294
|
+
for (const s of src.parsed.sections) {
|
|
295
|
+
const meta = face === 'fr' ? frMeta(s) : sectionMeta(s)
|
|
296
|
+
if (!meta) { staying.push(s); continue }
|
|
297
|
+
if (anchorSet.has(meta.id)) pruned.push(meta.id)
|
|
298
|
+
else staying.push(s)
|
|
299
|
+
}
|
|
300
|
+
if (pruned.length !== anchors.length) {
|
|
301
|
+
const got = new Set(pruned)
|
|
302
|
+
throw new Error(`prune 精确计数断言失败:未命中 ${anchors.filter((a) => !got.has(a)).join('、')}`)
|
|
303
|
+
}
|
|
304
|
+
const sourceDeleted = staying.length === 0
|
|
305
|
+
if (sourceDeleted) unlinkSync(src.fromPath)
|
|
306
|
+
else writeAtomicSync(src.fromPath, joinKnowledgeFile(src.parsed.preamble, staying))
|
|
307
|
+
try {
|
|
308
|
+
// fr 面必须传 FR_ROUTE_STRIP_TOKENS(评审 P2:传 {} 落默认 Decisions 段——fr 裁空后 INDEX 残留幽灵路由行)
|
|
309
|
+
syncIndexRoutingLines(knowledgeRoot, face === 'fr' ? { stripTokens: FR_ROUTE_STRIP_TOKENS } : { stripTokens: DECISIONS_ROUTE_STRIP_TOKENS })
|
|
310
|
+
} catch { /* INDEX 缺席/同步失败不拦裁剪(幂等可重跑) */ }
|
|
311
|
+
return { pruned, sourceDeleted }
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** supersedes 留源警告:被处理段的 supersedes 引用(基号)与留守段 id 基号相同 → 警告。 */
|
|
315
|
+
function supersedesLeftBehind(knowledgeRoot, face, processedIds) {
|
|
316
|
+
const src = parseUnmappedFace(knowledgeRoot, face)
|
|
317
|
+
if (!src) return []
|
|
318
|
+
const baseOf = (s) => String(s || '').replace(/@v\d+$/, '')
|
|
319
|
+
const processedBases = new Set(processedIds.map(baseOf))
|
|
320
|
+
const stayingBases = new Set(src.metas.map((m) => m.id).filter((id) => !processedBases.has(baseOf(id))).map(baseOf))
|
|
321
|
+
const warnings = []
|
|
322
|
+
for (const s of src.parsed.sections) {
|
|
323
|
+
const meta = face === 'fr' ? frMeta(s) : sectionMeta(s)
|
|
324
|
+
if (!meta || !processedBases.has(baseOf(meta.id))) continue
|
|
325
|
+
const sup = (s.lines.join('\n').match(/^supersedes[::]\s*(\S+)/m) || [])[1]
|
|
326
|
+
if (sup && stayingBases.has(baseOf(sup))) warnings.push(`${meta.id} 的 supersedes 目标 ${sup} 留在源文件`)
|
|
327
|
+
}
|
|
328
|
+
return warnings
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* --map apply(fail-fast):迁移组 + 裁剪顺序执行,任何失败整体上抛(已写部分靠幂等复跑收敛)。
|
|
333
|
+
* 守恒断言(apply 后重算段头 = 原段头 − moved − pruned)与幂等(全部已处理 → alreadyApplied)内嵌。
|
|
334
|
+
*/
|
|
335
|
+
export function applyFromMap({ knowledgeRoot, archiveRoot, face, map }) {
|
|
336
|
+
const plan = planFromMap({ knowledgeRoot, face, map })
|
|
337
|
+
if (plan.alreadyApplied) return { face, alreadyApplied: true, moved: 0, pruned: 0, conservation: 'ok', warnings: [] }
|
|
338
|
+
if (plan.unknownIds.length > 0) throw new Error(`映射表含源文件不存在的 id:${plan.unknownIds.join('、')}`)
|
|
339
|
+
const processed = [...plan.groups.flatMap((g) => g.ids), ...plan.prunes]
|
|
340
|
+
const warnings = supersedesLeftBehind(knowledgeRoot, face, processed)
|
|
341
|
+
let moved = 0
|
|
342
|
+
const movedByDomain = {}
|
|
343
|
+
for (const g of plan.groups) {
|
|
344
|
+
const res = face === 'fr'
|
|
345
|
+
? redomainFrEntries({ knowledgeRoot, from: 'unmapped', to: g.domain, anchors: g.ids })
|
|
346
|
+
: redomainDecisionEntries({ knowledgeRoot, from: 'unmapped', to: g.domain, anchors: g.ids })
|
|
347
|
+
moved += res.moved.length
|
|
348
|
+
movedByDomain[g.domain] = res.moved.length
|
|
349
|
+
if (Array.isArray(res.skippedConflicts) && res.skippedConflicts.length > 0) {
|
|
350
|
+
warnings.push(`目标域 ${g.domain} 冲突跳过:${res.skippedConflicts.join('、')}`)
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
let pruned = 0
|
|
354
|
+
if (plan.prunes.length > 0) {
|
|
355
|
+
pruned = pruneUnmappedEntries({ knowledgeRoot, face, anchors: plan.prunes }).pruned.length
|
|
356
|
+
}
|
|
357
|
+
// 守恒断言:apply 后源段头 = 原 − moved − pruned(skippedConflicts 留守 → moved 不含)
|
|
358
|
+
const after = parseUnmappedFace(knowledgeRoot, face)
|
|
359
|
+
const afterTotal = after ? after.total : 0
|
|
360
|
+
const expected = plan.total - moved - pruned
|
|
361
|
+
if (afterTotal !== expected) {
|
|
362
|
+
throw new Error(`守恒断言失败:apply 后段头 ${afterTotal} ≠ 原段头 ${plan.total} − moved ${moved} − pruned ${pruned}`)
|
|
363
|
+
}
|
|
364
|
+
return { face, alreadyApplied: false, moved, movedByDomain, pruned, conservation: `ok(${plan.total} − ${moved} − ${pruned} = ${afterTotal})`, warnings }
|
|
365
|
+
}
|
package/src/run/complete.js
CHANGED
|
@@ -532,7 +532,10 @@ async function _completeStepInner(pm, progress, stageName, cwd, outputText, inpu
|
|
|
532
532
|
: () => false
|
|
533
533
|
const _echoHits = _hits.filter((h) => !_answered(h))
|
|
534
534
|
const _entries = km.matched ? (km.entries || []) : []
|
|
535
|
-
|
|
535
|
+
const _routeHits = km.matched ? (km.routeHits || []) : []
|
|
536
|
+
const _scopeHitsAll = km.matched ? (km.scopeDecisionHits || []) : []
|
|
537
|
+
const _scopeHits = _scopeHitsAll.filter((h) => h.deathPath || h.status === 'rejected').slice(0, 3)
|
|
538
|
+
if (_echoHits.length > 0 || _entries.length > 0 || _routeHits.length > 0 || _scopeHits.length > 0) {
|
|
536
539
|
const { deathPathNote, decisionChangeQualifier } = await import('../knowledge-match.js')
|
|
537
540
|
console.warn(`\n⚠️ [knowledge-gate] 方案/决策知识命中(设计时点防复潮提示,warn 不阻断):`)
|
|
538
541
|
for (const h of _echoHits.slice(0, 5)) {
|
|
@@ -546,7 +549,23 @@ async function _completeStepInner(pm, progress, stageName, cwd, outputText, inpu
|
|
|
546
549
|
const _base = e.anchor ? `${e.file}#${e.anchor}` : e.file
|
|
547
550
|
console.warn(` - 知识条目 ${_base}${e.display ? `(${e.display})` : ''}`)
|
|
548
551
|
}
|
|
552
|
+
for (const e of _routeHits.slice(0, 3)) {
|
|
553
|
+
const _base = e.anchor ? `${e.file}#${e.anchor}` : e.file
|
|
554
|
+
console.warn(` - 路由命中 ${_base}(按需 Read)`)
|
|
555
|
+
}
|
|
556
|
+
// 融合增量(2026-11-02-knowledge-fusion FR-03):scope 否决独立小节(≤3,已回应过滤同源)
|
|
557
|
+
for (const h of _scopeHits.filter((h) => !_answered(h))) {
|
|
558
|
+
const _via = h.via === 'two-hop' ? '两跳' : '直连'
|
|
559
|
+
console.warn(` - 触达文件关联否决 ${h.id} ${h.title}(${h.file}|${decisionChangeQualifier(h.change)}|${_via})${h.status === 'rejected' ? `否决理由:${h.reason || '(未记录)'}` : `⚰️死路:${deathPathNote(h.reason)}`}`)
|
|
560
|
+
}
|
|
549
561
|
console.warn(` 须在对应决策的 evidence 回应或说明不复潮(v1 提示级;local.yaml commands.knowledge-gate: false 可关本回显)`)
|
|
562
|
+
// 用户纠正转写自检(2026-11-02-constraint-mechanize FR-02):本变更 decisions.md 有
|
|
563
|
+
// 「用户原话保留」事故类条目时提示转写层——点火器转写纪律(用户纠正是生成时刻约束力
|
|
564
|
+
// 最强的力量,跨会话零存续,最强用法是当场转写进测试/门/接口/规约)
|
|
565
|
+
const _hasUserCorrection = _dm && /用户原话保留|落地层[::]/.test(_dm)
|
|
566
|
+
if (_hasUserCorrection) {
|
|
567
|
+
console.warn(` 🔥 用户纠正转写自检:本变更记录了用户纠正——原话转写进了哪一层(测试/门/接口/规约)?未转写则在对应决策补「落地层:」字段(D-002 先例)`)
|
|
568
|
+
}
|
|
550
569
|
}
|
|
551
570
|
}
|
|
552
571
|
}
|
package/src/run/gate-snapshot.js
CHANGED
|
@@ -999,7 +999,9 @@ export async function createVerifyGateSnapshot({ cwd, changeName, specBase, plat
|
|
|
999
999
|
}
|
|
1000
1000
|
} catch { /* 文档复制失败不连坐快照(test_strategy 降级 module 集仍可跑) */ }
|
|
1001
1001
|
|
|
1002
|
-
|
|
1002
|
+
// changeFiles 透出(2026-10-29-open-metrics-gate task-03):厚道 verify 质量扫描的 metrics
|
|
1003
|
+
// 增量门注入(SILLYSPEC_CHANGE_FILES)需要本快照自算的变更文件集——additive,既有消费方零感知。
|
|
1004
|
+
return { ...snap, changeFileCount: changeFiles.length, changeFiles, sourceRoot }
|
|
1003
1005
|
} catch (e) {
|
|
1004
1006
|
// 任一链路异常 → 回退主仓现行为。异常细节不再静默(坑 snapshot-fallback-reason-blind):
|
|
1005
1007
|
// failRef 带原因 + warn 落日志,排查不再靠 stdout 考古。
|