sillyspec 3.33.8 → 3.33.9
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/flow-parity.js +10 -50
- package/src/flow-review.js +4 -0
- package/src/flow.js +37 -10
- package/src/index.js +4 -45
- 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/run/prompt.js
CHANGED
|
@@ -96,9 +96,9 @@ import { REVIEW_SCHEMA_VERSION, isValidExecuteRunId } from '../task-review.js'
|
|
|
96
96
|
// 静态 import 不引入环(stages/* 反向引用本文件会撞 index.js 顶层 stageRegistry TDZ——见
|
|
97
97
|
// buildKnowledgeInjection docstring)。
|
|
98
98
|
import { matchKnowledge } from '../knowledge-match.js'
|
|
99
|
-
import { matchQuicklogContext } from '../knowledge-quicklog.js'
|
|
100
99
|
import { appendKnowledgeHit } from '../knowledge-hits.js'
|
|
101
|
-
import { readActiveFrDigest } from '../fr-index.js'
|
|
100
|
+
import { readActiveFrDigest, deliverableFilesFromDesignText } from '../fr-index.js'
|
|
101
|
+
import { matchKnowledgeWithScope, graphPathCandidates, scopeTelemetryRefs } from '../knowledge-graph.js'
|
|
102
102
|
// 前置失败清单 validator 面(2026-09-18-preflight-slimming task-01):design-facts /
|
|
103
103
|
// stage-contract-engine 均为 src/ 叶子方向(不反向 import 本模块),静态 import 不引入环。
|
|
104
104
|
import { validateDesignFileList } from '../design-facts.js'
|
|
@@ -524,33 +524,49 @@ function pickTopKnowledgeEntries(entries, maxFiles) {
|
|
|
524
524
|
return picked
|
|
525
525
|
}
|
|
526
526
|
|
|
527
|
-
/** 命中正文段渲染(段头 + Status/Sources 命中报告语义 + top-3 文件截断正文)。
|
|
527
|
+
/** 命中正文段渲染(段头 + Status/Sources 命中报告语义 + top-3 文件截断正文)。
|
|
528
|
+
* 路由行分离(FR-04,2026-11-02-knowledge-rank-denoise):entries 仅手工条目注入正文;
|
|
529
|
+
* routeHits(fr/decisions 路由行)以指针行呈现,保 implemented 决策可见性;空 entries 且
|
|
530
|
+
* 无 routeHits 时返回空串(不出段头空段)。 */
|
|
528
531
|
function renderKnowledgeInjectSection(knowledgeResult, { knowledgeDir, maxFiles, maxLines }) {
|
|
529
532
|
const ref = e => (e.anchor ? `${e.file}#${e.anchor}` : e.file)
|
|
533
|
+
const humanEntries = knowledgeResult.entries || []
|
|
534
|
+
const routeHits = knowledgeResult.routeHits || []
|
|
535
|
+
const scopeEntriesPreview = knowledgeResult.scopeEntries || []
|
|
536
|
+
if (humanEntries.length === 0 && routeHits.length === 0 && scopeEntriesPreview.length === 0) return ''
|
|
530
537
|
const lines = []
|
|
531
538
|
lines.push(`📚 命中知识(CLI 按任务描述机械匹配,top-${maxFiles})`)
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
539
|
+
if (humanEntries.length > 0) {
|
|
540
|
+
lines.push(`Status: matched | Entries: ${humanEntries.length} | Sources:`)
|
|
541
|
+
for (const e of humanEntries) lines.push(` - ${ref(e)}`)
|
|
542
|
+
lines.push(`(命中清单如上;正文按相关度注入前 ${maxFiles} 个不同文件,单文件首 ${maxLines} 行截断——未注入条目需要时按 Sources 路径自行读取)`)
|
|
543
|
+
for (const e of pickTopKnowledgeEntries(humanEntries, maxFiles)) {
|
|
544
|
+
lines.push('')
|
|
545
|
+
lines.push(`── ${ref(e)}${e.display ? `(${e.display})` : ''} ──`)
|
|
546
|
+
let body = ''
|
|
547
|
+
try {
|
|
548
|
+
// CRLF/CR 归一(Windows 知识文件 + 行数截断按 \n 切分)
|
|
549
|
+
body = readFileSync(join(knowledgeDir, e.file), 'utf8').replace(/\r\n?/g, '\n')
|
|
550
|
+
} catch (err) {
|
|
551
|
+
lines.push(`(文件不可读:${err && err.message ? err.message : err})`)
|
|
552
|
+
continue
|
|
553
|
+
}
|
|
554
|
+
const bodyLines = body.split('\n')
|
|
555
|
+
if (bodyLines.length > maxLines) {
|
|
556
|
+
lines.push(bodyLines.slice(0, maxLines).join('\n'))
|
|
557
|
+
lines.push('…(截断)')
|
|
558
|
+
} else {
|
|
559
|
+
lines.push(body.replace(/\n+$/, ''))
|
|
560
|
+
}
|
|
552
561
|
}
|
|
553
562
|
}
|
|
563
|
+
if (routeHits.length > 0) {
|
|
564
|
+
lines.push(`🧭 决策/FR 路由命中(专用通道,按需 Read):${routeHits.slice(0, 3).map(ref).join(';')}`)
|
|
565
|
+
}
|
|
566
|
+
const scopeEntries = knowledgeResult.scopeEntries || []
|
|
567
|
+
if (scopeEntries.length > 0) {
|
|
568
|
+
lines.push(`📚 触达文件关联手工条目(scope,按需 Read):${scopeEntries.slice(0, 3).map(ref).join(';')}`)
|
|
569
|
+
}
|
|
554
570
|
return lines.join('\n')
|
|
555
571
|
}
|
|
556
572
|
|
|
@@ -567,30 +583,28 @@ function renderKnowledgeInjectSection(knowledgeResult, { knowledgeDir, maxFiles,
|
|
|
567
583
|
* report/json:matchKnowledge 原样透传(execute 调用方沿用旧 report.json 落盘与未命中替换口径)。
|
|
568
584
|
* 遥测 append 单独 fail-soft(hits 写失败不影响注入本体);匹配/渲染异常向上抛由调用方降级。
|
|
569
585
|
*/
|
|
570
|
-
export function buildKnowledgeInjection({ knowledgeDir, runtimeDir, change, query, maxFiles = KNOWLEDGE_INJECT_MAX_FILES, maxLines = KNOWLEDGE_INJECT_MAX_LINES } = {}) {
|
|
571
|
-
|
|
572
|
-
//
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
const quick = knowledgeDir
|
|
576
|
-
? matchQuicklogContext(dirname(knowledgeDir), String(query || ''))
|
|
577
|
-
: { hits: [], report: '' }
|
|
578
|
-
if (!knowledgeResult.matched && quick.hits.length === 0) {
|
|
586
|
+
export function buildKnowledgeInjection({ knowledgeDir, runtimeDir, change, query, scopeFiles = [], maxFiles = KNOWLEDGE_INJECT_MAX_FILES, maxLines = KNOWLEDGE_INJECT_MAX_LINES } = {}) {
|
|
587
|
+
// 融合(2026-11-02-knowledge-fusion):同步入口 = matchKnowledge(路由/词片)+ scope 融合;
|
|
588
|
+
// scopeFiles 为机器键(design 交付清单/quick allowedFiles),键规整在 graphPathCandidates 内做
|
|
589
|
+
const knowledgeResult = matchKnowledgeWithScope(knowledgeDir, String(query || ''), { scopeFiles })
|
|
590
|
+
if (!knowledgeResult.matched) {
|
|
579
591
|
return { matched: false, section: '', report: knowledgeResult.report, json: knowledgeResult.json }
|
|
580
592
|
}
|
|
581
593
|
const kSection = knowledgeResult.matched ? renderKnowledgeInjectSection(knowledgeResult, { knowledgeDir, maxFiles, maxLines }) : ''
|
|
582
|
-
const
|
|
583
|
-
const
|
|
594
|
+
const section = kSection
|
|
595
|
+
const routeFiles = (knowledgeResult.routeHits || []).map(e => (e.anchor ? `${e.file}#${e.anchor}` : e.file))
|
|
596
|
+
const scopeMatchedFiles = scopeTelemetryRefs(knowledgeResult, { includeRejected: false })
|
|
584
597
|
try {
|
|
585
598
|
appendKnowledgeHit(runtimeDir, {
|
|
586
599
|
type: 'inject',
|
|
587
600
|
change: change || '',
|
|
588
601
|
query: String(query || ''),
|
|
589
|
-
matchedFiles: knowledgeResult.entries.map(e => (e.anchor ? `${e.file}#${e.anchor}` : e.file)),
|
|
590
|
-
|
|
602
|
+
matchedFiles: [...knowledgeResult.entries.map(e => (e.anchor ? `${e.file}#${e.anchor}` : e.file)), ...routeFiles],
|
|
603
|
+
// scope 展示面独立字段(2026-11-02-knowledge-engine-tail FR-02):不并入 matchedFiles
|
|
604
|
+
...(scopeMatchedFiles.length > 0 ? { scopeMatchedFiles } : {}),
|
|
591
605
|
})
|
|
592
606
|
} catch { /* 遥测 fail-soft(R-04):hits 落盘失败不阻断注入本体 */ }
|
|
593
|
-
return { matched: true, section, report: knowledgeResult.report, json: knowledgeResult.json
|
|
607
|
+
return { matched: true, section, report: knowledgeResult.report, json: knowledgeResult.json}
|
|
594
608
|
}
|
|
595
609
|
|
|
596
610
|
// parseModuleMapSimple 复用 modules.js 的 canonical 实现(合并历史 copy-paste 副本,2026-08-07;
|
|
@@ -1492,11 +1506,20 @@ async function outputStepImpl(stageName, stepIndex, steps, cwd, changeName, dbPr
|
|
|
1492
1506
|
} catch {}
|
|
1493
1507
|
}
|
|
1494
1508
|
const runtimeDir = join(effectiveSpecBase, '.runtime')
|
|
1509
|
+
// scope 键(FR-04):execute step 用 design 交付清单(机器键,图内精确路径由 graphPathCandidates 规整)
|
|
1510
|
+
let scopeFiles = []
|
|
1511
|
+
if (changeName) {
|
|
1512
|
+
try {
|
|
1513
|
+
const designText = readFileSync(join(effectiveSpecBase, 'changes', changeName, 'design.md'), 'utf8')
|
|
1514
|
+
scopeFiles = graphPathCandidates(deliverableFilesFromDesignText(designText))
|
|
1515
|
+
} catch { /* 无 design.md = 无键(不编造) */ }
|
|
1516
|
+
}
|
|
1495
1517
|
const knowledgeInjection = buildKnowledgeInjection({
|
|
1496
1518
|
knowledgeDir,
|
|
1497
1519
|
runtimeDir,
|
|
1498
1520
|
change: changeName || '',
|
|
1499
1521
|
query: taskContext,
|
|
1522
|
+
scopeFiles,
|
|
1500
1523
|
})
|
|
1501
1524
|
substitute(
|
|
1502
1525
|
/\{KNOWLEDGE_HIT_REPORT\}/g,
|
|
@@ -2012,11 +2035,18 @@ async function outputStepImpl(stageName, stepIndex, steps, cwd, changeName, dbPr
|
|
|
2012
2035
|
const kiSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
2013
2036
|
const kiQuery = String(readQuickGuardField(changeName, kiSpecBase, 'taskDescription') || '')
|
|
2014
2037
|
if (kiQuery.trim() !== '') {
|
|
2038
|
+
// scope 键(FR-04):quick 仅 guard.allowedFiles(--files 声明)存在时传,缺失不编造
|
|
2039
|
+
let kiScope = []
|
|
2040
|
+
try {
|
|
2041
|
+
const declared = readQuickGuardField(changeName, kiSpecBase, 'allowedFiles')
|
|
2042
|
+
if (declared) kiScope = graphPathCandidates(String(declared).split(/[,,\s]+/))
|
|
2043
|
+
} catch { /* 无声明 = 无键 */ }
|
|
2015
2044
|
const ki = buildKnowledgeInjection({
|
|
2016
2045
|
knowledgeDir: join(kiSpecBase, 'knowledge'),
|
|
2017
2046
|
runtimeDir: join(kiSpecBase, '.runtime'),
|
|
2018
2047
|
change: changeName || '',
|
|
2019
2048
|
query: kiQuery,
|
|
2049
|
+
scopeFiles: kiScope,
|
|
2020
2050
|
})
|
|
2021
2051
|
if (ki.section) {
|
|
2022
2052
|
promptText = promptText + '\n\n' + ki.section
|
package/src/run/quick-audit.js
CHANGED
|
@@ -401,7 +401,7 @@ export function buildTestSurfaceAdvisory(files) {
|
|
|
401
401
|
if (CODE_PATH_SEGMENT_RE.test(f) || CODE_EXTENSION_RE.test(f)) srcCount++
|
|
402
402
|
}
|
|
403
403
|
if (srcCount < MIN_SRC_FILES_FOR_TEST_ADVISORY || testCount > 0) return null
|
|
404
|
-
return `⚠️ 测试面厚度 advisory:交付面 ${srcCount} 个 src 类文件但测试面为空(0 个测试文件改动,P2 账本测试面无增量可对账),确认无需测试增量?(advisory 不阻断——轻量变更/burst 实证测试量仅为旧流程 40
|
|
404
|
+
return `⚠️ 测试面厚度 advisory:交付面 ${srcCount} 个 src 类文件但测试面为空(0 个测试文件改动,P2 账本测试面无增量可对账),确认无需测试增量?(advisory 不阻断——轻量变更/burst 实证测试量仅为旧流程 40%)另空实现判据:若把实现改成全部返回空测试仍全绿,则测试是摆设——要补就补断言可观察行为的真测试(2026-10-29-open-metrics-gate task-04)`
|
|
405
405
|
}
|
|
406
406
|
|
|
407
407
|
/**
|
|
@@ -477,7 +477,7 @@ export function mergeGateFiles(audited, declared) {
|
|
|
477
477
|
|
|
478
478
|
export async function runQuickTestLintGate({ cwd, specBase, changedFiles = [], declaredFiles = [], changeName = null, skipSentinel = false }) {
|
|
479
479
|
if (process.env.SILLYSPEC_QUICK_TEST_GATE === 'skip') {
|
|
480
|
-
return { action: 'skip', failed: [], reason: 'SILLYSPEC_QUICK_TEST_GATE=skip 显式跳过(审计留痕)', test: null, lint: null }
|
|
480
|
+
return { action: 'skip', failed: [], reason: 'SILLYSPEC_QUICK_TEST_GATE=skip 显式跳过(审计留痕)', test: null, lint: null, metrics: null, metricsRoutes: null }
|
|
481
481
|
}
|
|
482
482
|
const audited = Array.isArray(changedFiles) ? changedFiles : []
|
|
483
483
|
// 门禁文件集 = 审计 ∪ 声明边界(并集,2026-09-23 ql-018 修复):旧口径二选一(审计非空时
|
|
@@ -499,10 +499,10 @@ export async function runQuickTestLintGate({ cwd, specBase, changedFiles = [], d
|
|
|
499
499
|
(f) => typeof f === 'string' && (CODE_PATH_SEGMENT_RE.test(f) || CODE_EXTENSION_RE.test(f)),
|
|
500
500
|
)
|
|
501
501
|
if (files.length === 0) {
|
|
502
|
-
return { action: 'skip', failed: [], reason: `无变更文件清单(${fileSource}口径均空——brownfield 无 guard 或空审计),跳过`, test: null, lint: null }
|
|
502
|
+
return { action: 'skip', failed: [], reason: `无变更文件清单(${fileSource}口径均空——brownfield 无 guard 或空审计),跳过`, test: null, lint: null, metrics: null, metricsRoutes: null }
|
|
503
503
|
}
|
|
504
504
|
if (codeFiles.length === 0) {
|
|
505
|
-
return { action: 'skip', failed: [], reason: `纯 doc/配置改动(${files.length} 个文件均未触及 src/test,规则 8 语义跳过 test+lint)`, test: null, lint: null }
|
|
505
|
+
return { action: 'skip', failed: [], reason: `纯 doc/配置改动(${files.length} 个文件均未触及 src/test,规则 8 语义跳过 test+lint)`, test: null, lint: null, metrics: null, metricsRoutes: null }
|
|
506
506
|
}
|
|
507
507
|
|
|
508
508
|
// ── 测试最低面 advisory(资产三小件②):放在两条早退之后——纯 doc/env skip/空清单
|
|
@@ -517,7 +517,7 @@ export async function runQuickTestLintGate({ cwd, specBase, changedFiles = [], d
|
|
|
517
517
|
// (Grill X-001 附注)。specBase 同理用主仓的(快照副本 local.yaml 不参与开关判定)。
|
|
518
518
|
const semanticGuard = buildSemanticGuardHits({ cwd, specBase, files, changeName })
|
|
519
519
|
|
|
520
|
-
const { runVerifyTestCheck, runVerifyLintCheck } = await import('../verify-postcheck.js')
|
|
520
|
+
const { runVerifyTestCheck, runVerifyLintCheck, runVerifyMetricsCheck, runVerifyMetricsRoutesCheck } = await import('../verify-postcheck.js')
|
|
521
521
|
|
|
522
522
|
// ── 隔离快照执行(2026-09-10 驾驭小结第六批②,用户实证「lint 实测对账在主仓跑被并行
|
|
523
523
|
// 会话脏文件拦门」):HEAD 干净快照 + 本会话文件 overlay 后在快照内跑 test+lint——并行
|
|
@@ -565,6 +565,8 @@ export async function runQuickTestLintGate({ cwd, specBase, changedFiles = [], d
|
|
|
565
565
|
const failed = []
|
|
566
566
|
let test = null
|
|
567
567
|
let lint = null
|
|
568
|
+
let metrics = null
|
|
569
|
+
let metricsRoutes = null
|
|
568
570
|
try {
|
|
569
571
|
// ── P2 三键账本(D-001@v1,batch3 task-01):同码同环境免重跑(fail-closed——无记录/
|
|
570
572
|
// 键不等/分量不可得=真跑,行为不变)。quick 会话名 per-change 天然隔离账本文件。──
|
|
@@ -621,6 +623,30 @@ export async function runQuickTestLintGate({ cwd, specBase, changedFiles = [], d
|
|
|
621
623
|
console.warn('⚠️ 快照 lint 超时(node_modules junction I/O 慢)→ 主仓复跑 lint(并行噪声可能混入——失败先做归属鉴定)')
|
|
622
624
|
lint = runVerifyLintCheck({ cwd, specBase })
|
|
623
625
|
}
|
|
626
|
+
// ── 收口指标门(2026-10-29-open-metrics-gate task-03,ub-mode「确定性质量门」采纳):
|
|
627
|
+
// commands.metrics 配置才执行,lint 同款隔离快照口径 + 超时回退主仓;非零退出计入
|
|
628
|
+
// failed 阻断收口(通过标准由命令自身定义——design 风险节声明的边界)。files 透传注入
|
|
629
|
+
// SILLYSPEC_CHANGE_FILES(审计∪声明口径,大仓增量门只扫变更面,存量债不拦新变更)。──
|
|
630
|
+
metrics = runVerifyMetricsCheck({ cwd: gateCwd, specBase: gateSpecBase, timeoutMs: snapshot ? 5 * 60 * 1000 : undefined, changeFiles: files })
|
|
631
|
+
if (snapshot && metrics.status === 'failed' && /超时/.test(String(metrics.reason || ''))) {
|
|
632
|
+
console.warn('⚠️ 快照 metrics 超时(junction I/O 慢)→ 主仓复跑 metrics(并行噪声可能混入——失败先做归属鉴定)')
|
|
633
|
+
metrics = runVerifyMetricsCheck({ cwd, specBase, changeFiles: files })
|
|
634
|
+
}
|
|
635
|
+
if (metrics.status === 'failed') {
|
|
636
|
+
failed.push('metrics')
|
|
637
|
+
}
|
|
638
|
+
// ── 路由指标门(2026-10-29-metrics-routes task-02——多语言/monorepo 路径集合路由):
|
|
639
|
+
// 单命令门之后执行;failed 归并(同计 'metrics' 一次——两形态是同一道门的两种配置
|
|
640
|
+
// 形态,重复计会让 failed 数组出现同名项)。files 同源注入路由(gate 命中子集)。──
|
|
641
|
+
metricsRoutes = runVerifyMetricsRoutesCheck({ cwd: gateCwd, specBase: gateSpecBase, timeoutMs: snapshot ? 5 * 60 * 1000 : undefined, changeFiles: files })
|
|
642
|
+
const routeTimeoutHit = (metricsRoutes.gates || []).some((g) => g && g.status === 'failed' && /超时/.test(String(g.reason || '')))
|
|
643
|
+
if (snapshot && metricsRoutes.status === 'failed' && routeTimeoutHit) {
|
|
644
|
+
console.warn('⚠️ 快照 metrics 路由 gate 超时(junction I/O 慢)→ 主仓复跑路由门(并行噪声可能混入——失败先做归属鉴定)')
|
|
645
|
+
metricsRoutes = runVerifyMetricsRoutesCheck({ cwd, specBase, changeFiles: files })
|
|
646
|
+
}
|
|
647
|
+
if (metricsRoutes.status === 'failed' && !failed.includes('metrics')) {
|
|
648
|
+
failed.push('metrics')
|
|
649
|
+
}
|
|
624
650
|
// 哨兵断言(2026-09-25-sentinel-wiring,与 flow done 同判):tasks.md 全勾但零完成
|
|
625
651
|
// 证据(区间提交消息标题与正文均无 task-NN token 且无对应 review.json)→ 计入 failed 拒收。
|
|
626
652
|
// 证据面=整条提交消息(2026-09-25-thin-done-gate-calibration 坑2,同 flow 侧 %B%x1e 口径)。
|
|
@@ -700,14 +726,32 @@ export async function runQuickTestLintGate({ cwd, specBase, changedFiles = [], d
|
|
|
700
726
|
lint.resultPath = persistLintResult({ specBase, changeName: typeof changeName === 'string' ? changeName : null, testResultPath: test && test.resultPath, lint }) || lint.resultPath || null
|
|
701
727
|
}
|
|
702
728
|
} catch { /* 持久化 best-effort,不影响门禁语义 */ }
|
|
729
|
+
// metrics 结果持久化(2026-10-29-open-metrics-gate task-03):同 lint 语义——并入 test 的
|
|
730
|
+
// test-result.json(metrics 节)或独立落盘(kind: 'metrics'),证据面供收口评审与排障直读。
|
|
731
|
+
try {
|
|
732
|
+
if (metrics && (metrics.status === 'passed' || metrics.status === 'failed')) {
|
|
733
|
+
const { persistLintResult } = await import('../verify-postcheck.js')
|
|
734
|
+
metrics.resultPath = persistLintResult({ specBase, changeName: typeof changeName === 'string' ? changeName : null, testResultPath: test && test.resultPath, lint: metrics, section: 'metrics' }) || metrics.resultPath || null
|
|
735
|
+
}
|
|
736
|
+
} catch { /* 持久化 best-effort,不影响门禁语义 */ }
|
|
737
|
+
// 路由门证据(2026-10-29-metrics-routes task-02):section 'routes'——并入 test-result.json
|
|
738
|
+
// 的 routes 节或独立件 kind: 'routes'(带 gates 数组),与单命令 metrics 节并存互不覆盖。
|
|
739
|
+
try {
|
|
740
|
+
if (metricsRoutes && (metricsRoutes.status === 'passed' || metricsRoutes.status === 'failed')) {
|
|
741
|
+
const { persistLintResult } = await import('../verify-postcheck.js')
|
|
742
|
+
metricsRoutes.resultPath = persistLintResult({ specBase, changeName: typeof changeName === 'string' ? changeName : null, testResultPath: test && test.resultPath, lint: metricsRoutes, section: 'routes' }) || metricsRoutes.resultPath || null
|
|
743
|
+
}
|
|
744
|
+
} catch { /* 持久化 best-effort,不影响门禁语义 */ }
|
|
703
745
|
return {
|
|
704
746
|
action: failed.length > 0 ? 'fail' : 'pass',
|
|
705
747
|
failed,
|
|
706
748
|
reason: failed.length > 0
|
|
707
749
|
? `实测失败:${failed.join(' + ')}(命令与输出尾部见上;修复后重跑 --done 不丢进度)`
|
|
708
|
-
: `触及 src/test 共 ${codeFiles.length} 个文件,test+lint 实测通过(或未配置命令自动跳过)${snapshot ? '(隔离快照口径:并行会话脏文件不计入)' : ''}`,
|
|
750
|
+
: `触及 src/test 共 ${codeFiles.length} 个文件,test+lint 实测通过(或未配置命令自动跳过)${metrics && metrics.status === 'passed' ? ';metrics 指标门实测通过' : ''}${metricsRoutes && metricsRoutes.status === 'passed' ? ';metrics 路由门全过' : ''}${snapshot ? '(隔离快照口径:并行会话脏文件不计入)' : ''}`,
|
|
709
751
|
test,
|
|
710
752
|
lint,
|
|
753
|
+
metrics,
|
|
754
|
+
metricsRoutes,
|
|
711
755
|
// 语义护栏 WARNING(task-06):只增可选字段,action/failed 判定与调用方读取面均不受影响
|
|
712
756
|
semanticGuard,
|
|
713
757
|
}
|
|
@@ -721,7 +765,7 @@ export async function runQuickTestLintGate({ cwd, specBase, changedFiles = [], d
|
|
|
721
765
|
// 结果路径重映射(评审 P1 清偿):快照内写下的 resultPath 指向快照临时目录——清理后
|
|
722
766
|
// existsSync 恒 false,flow done 的 FAIL 三件套会静默失效。回拷后把 test/lint 的
|
|
723
767
|
// resultPath 改写为主仓路径(对象即返回值,finally 在 return 落地前变异生效)。
|
|
724
|
-
for (const r of [test, lint]) {
|
|
768
|
+
for (const r of [test, lint, metrics, metricsRoutes]) {
|
|
725
769
|
if (r && typeof r.resultPath === 'string' && r.resultPath.startsWith(gateSpecBase)) {
|
|
726
770
|
r.resultPath = specBase + r.resultPath.slice(gateSpecBase.length)
|
|
727
771
|
}
|
|
@@ -764,16 +808,20 @@ export function printQuickTestLintGate(gate) {
|
|
|
764
808
|
console.log(`\n🧪 quick test+lint 门禁 — SKIP:${gate.reason}`)
|
|
765
809
|
return
|
|
766
810
|
}
|
|
767
|
-
for (const [label, r] of [['test', gate.test], ['lint', gate.lint]]) {
|
|
811
|
+
for (const [label, r] of [['test', gate.test], ['lint', gate.lint], ['metrics', gate.metrics], ['metrics-routes', gate.metricsRoutes]]) {
|
|
768
812
|
if (!r) continue
|
|
769
813
|
const icon = r.status === 'failed' ? '❌' : r.status === 'passed' ? '✅' : '⏭️'
|
|
770
814
|
const dur = typeof r.durationMs === 'number' ? ` (${(r.durationMs / 1000).toFixed(1)}s)` : ''
|
|
771
|
-
|
|
815
|
+
const gatesNote = Array.isArray(r.gates) && r.gates.length > 0 ? ` [gates: ${r.gates.map((g) => `${g.name}=${g.status}`).join(', ')}]` : ''
|
|
816
|
+
console.log(`${icon} ${label}: ${r.status}${r.command ? ` ← ${r.command}${dur}` : ''}${gatesNote}${r.status === 'skipped' && r.reason ? ` — ${r.reason}` : ''}`)
|
|
772
817
|
}
|
|
773
818
|
if (gate.action === 'fail') {
|
|
774
819
|
console.error(`\n🚫 quick test+lint 门禁 — BLOCKED:${gate.reason}`)
|
|
775
820
|
for (const key of gate.failed) {
|
|
776
|
-
|
|
821
|
+
// 'metrics' 归并键:单命令门与路由门共用——取有输出尾部者(单命令绿而路由红时尾部挂
|
|
822
|
+
// metricsRoutes;两者均红时先单命令,路由尾部在其 resultPath 证据件可读)
|
|
823
|
+
let r = gate[key]
|
|
824
|
+
if (key === 'metrics' && !(r && r.outputTail) && gate.metricsRoutes && gate.metricsRoutes.outputTail) r = gate.metricsRoutes
|
|
777
825
|
if (r && r.outputTail) console.error(` ── ${key} 输出尾部 ──\n${r.outputTail}`)
|
|
778
826
|
}
|
|
779
827
|
console.error(` quick 已停止:修复后重跑 --done(进度不丢);确认要跳过实测请设 SILLYSPEC_QUICK_TEST_GATE=skip(审计留痕)。`)
|
|
@@ -105,7 +105,7 @@ export function computeQualityScanFingerprint({ cwd, specBase }) {
|
|
|
105
105
|
* 保持 1——存量记录无 smoke 段照常读回(smokeResult 兜底 null);未配置 smoke 时段形态为
|
|
106
106
|
* configured=false(记录仍写,供 --done PASS 封顶判 not-configured)。
|
|
107
107
|
*/
|
|
108
|
-
export function storeQualityScan({ specBase, cwd, changeName, testResult, lintResult, smokeResult, usedSnapshot = false, missReason = null, scopeDecision = null }) {
|
|
108
|
+
export function storeQualityScan({ specBase, cwd, changeName, testResult, lintResult, smokeResult, metricsResult = null, metricsRoutesResult = null, usedSnapshot = false, missReason = null, scopeDecision = null }) {
|
|
109
109
|
const path = qualityScanRecordPath(specBase, changeName)
|
|
110
110
|
if (!path) return null
|
|
111
111
|
const fingerprint = computeQualityScanFingerprint({ cwd, specBase })
|
|
@@ -137,6 +137,10 @@ export function storeQualityScan({ specBase, cwd, changeName, testResult, lintRe
|
|
|
137
137
|
testResult,
|
|
138
138
|
lintResult: lintResult || null,
|
|
139
139
|
smokeResult: smokeResult || null,
|
|
140
|
+
// metrics 指标门结果(2026-10-29-open-metrics-gate,additive——存量记录无键读侧兼容)
|
|
141
|
+
metricsResult: metricsResult || null,
|
|
142
|
+
// 路由门结果(2026-10-29-metrics-routes,additive 同上)
|
|
143
|
+
metricsRoutesResult: metricsRoutesResult || null,
|
|
140
144
|
}, null, 2) + '\n')
|
|
141
145
|
return { path, fingerprint }
|
|
142
146
|
} catch (e) {
|
|
@@ -653,7 +657,7 @@ function warnIfMainRepoDirtyForGate(cwd) {
|
|
|
653
657
|
* `run verify` 复入本步重新实测(指纹随代码修复失配,--done 不会复用旧失败结果)。
|
|
654
658
|
*/
|
|
655
659
|
export async function executeVerifyQualityScan({ cwd, specBase, changeName, platformOpts }) {
|
|
656
|
-
const { runVerifyTestCheck, printVerifyTestCheck, runVerifyLintCheck, printVerifyLintCheck, shouldBlockVerifyLint, classifyTestFailureArtifact } = await import('../verify-postcheck.js')
|
|
660
|
+
const { runVerifyTestCheck, printVerifyTestCheck, runVerifyLintCheck, printVerifyLintCheck, shouldBlockVerifyLint, classifyTestFailureArtifact, runVerifyMetricsCheck, runVerifyMetricsRoutesCheck } = await import('../verify-postcheck.js')
|
|
657
661
|
// ── RERUN 失败签名闸(r5l-forensic-verdict.md 方案 4 / 评审护栏#6 五态)──
|
|
658
662
|
// R5-L 实证:RERUN=1 两次强制重扫各烧 ~8M 重发 + 10m/6.8m 等待,而失败签名未变——重跑
|
|
659
663
|
// 结果注定相同。故 RERUN=1 生效前先对账失败签名(dedupKey 已含代码指纹×豁免面,再叠测试
|
|
@@ -807,6 +811,8 @@ export async function executeVerifyQualityScan({ cwd, specBase, changeName, plat
|
|
|
807
811
|
console.log(`\n⏳ noAI 质量扫描:CLI 亲自实测 commands.test(同步,长套件 2~10 分钟无输出属正常)——结果按代码指纹落盘,最终 --done 对账直接复用免重跑。`)
|
|
808
812
|
let testCheck
|
|
809
813
|
let lintCheck
|
|
814
|
+
let metricsCheck = null
|
|
815
|
+
let metricsRoutesCheck = null
|
|
810
816
|
let coverageCheck = null
|
|
811
817
|
let smokeResult = null
|
|
812
818
|
try {
|
|
@@ -827,6 +833,40 @@ export async function executeVerifyQualityScan({ cwd, specBase, changeName, plat
|
|
|
827
833
|
console.warn('⚠️ 快照 lint 超时(node_modules junction I/O 慢)→ 主仓复跑 lint(并行噪声可能混入——失败先做归属鉴定)')
|
|
828
834
|
lintCheck = runVerifyLintCheck({ cwd, specBase })
|
|
829
835
|
}
|
|
836
|
+
// ── 收口指标门(2026-10-29-open-metrics-gate task-03):commands.metrics 配置才执行,
|
|
837
|
+
// 与 quick 道同款语义(隔离快照 + 超时回退主仓 + 非零阻断);snap.changeFiles 是厚道
|
|
838
|
+
// 快照自算的变更文件集(gate-snapshot 透出),透传注入 SILLYSPEC_CHANGE_FILES 增量门。──
|
|
839
|
+
metricsCheck = runVerifyMetricsCheck({
|
|
840
|
+
cwd: gateCwd, specBase: gateSpecBase,
|
|
841
|
+
timeoutMs: snap ? 5 * 60 * 1000 : undefined,
|
|
842
|
+
changeFiles: snap && Array.isArray(snap.changeFiles) ? snap.changeFiles : null,
|
|
843
|
+
})
|
|
844
|
+
if (snap && metricsCheck.status === 'failed' && /超时/.test(String(metricsCheck.reason || ''))) {
|
|
845
|
+
console.warn('⚠️ 快照 metrics 超时(junction I/O 慢)→ 主仓复跑 metrics(并行噪声可能混入——失败先做归属鉴定)')
|
|
846
|
+
metricsCheck = runVerifyMetricsCheck({ cwd, specBase, changeFiles: snap && Array.isArray(snap.changeFiles) ? snap.changeFiles : null })
|
|
847
|
+
}
|
|
848
|
+
if (metricsCheck.status !== 'skipped') {
|
|
849
|
+
const mIcon = metricsCheck.status === 'failed' ? '❌' : '✅'
|
|
850
|
+
console.log(`${mIcon} metrics 指标门实测 ${metricsCheck.status}${metricsCheck.command ? ` ← ${metricsCheck.command}` : ''}${metricsCheck.status === 'failed' && metricsCheck.reason ? ` — ${String(metricsCheck.reason).split('\n')[0]}` : ''}`)
|
|
851
|
+
}
|
|
852
|
+
// ── 路由指标门(2026-10-29-metrics-routes task-02):与 quick 道同款——变更文件面 ∩
|
|
853
|
+
// gate.paths 段全等前缀路由,仅命中 gate 执行;failed 与单命令门归并阻断。──
|
|
854
|
+
metricsRoutesCheck = runVerifyMetricsRoutesCheck({
|
|
855
|
+
cwd: gateCwd, specBase: gateSpecBase,
|
|
856
|
+
timeoutMs: snap ? 5 * 60 * 1000 : undefined,
|
|
857
|
+
changeFiles: snap && Array.isArray(snap.changeFiles) ? snap.changeFiles : null,
|
|
858
|
+
})
|
|
859
|
+
const routeTimeoutHit = (metricsRoutesCheck.gates || []).some((g) => g && g.status === 'failed' && /超时/.test(String(g.reason || '')))
|
|
860
|
+
if (snap && metricsRoutesCheck.status === 'failed' && routeTimeoutHit) {
|
|
861
|
+
console.warn('⚠️ 快照 metrics 路由 gate 超时(junction I/O 慢)→ 主仓复跑路由门(并行噪声可能混入)')
|
|
862
|
+
metricsRoutesCheck = runVerifyMetricsRoutesCheck({ cwd, specBase, changeFiles: snap && Array.isArray(snap.changeFiles) ? snap.changeFiles : null })
|
|
863
|
+
}
|
|
864
|
+
if (metricsRoutesCheck.status !== 'skipped') {
|
|
865
|
+
for (const g of metricsRoutesCheck.gates || []) {
|
|
866
|
+
const icon = g.status === 'failed' ? '❌' : g.status === 'passed' ? '✅' : '⏭️'
|
|
867
|
+
console.log(`${icon} metrics gate「${g.name}」 ${g.status} ← ${g.command}${g.status === 'failed' && g.reason ? ` — ${String(g.reason).split('\n')[0]}` : ''}`)
|
|
868
|
+
}
|
|
869
|
+
}
|
|
830
870
|
if (lintCheck.status !== 'skipped') {
|
|
831
871
|
console.log(`\n⏳ noAI 质量扫描:CLI 亲自实测 commands.lint…`)
|
|
832
872
|
}
|
|
@@ -852,9 +892,14 @@ export async function executeVerifyQualityScan({ cwd, specBase, changeName, plat
|
|
|
852
892
|
} finally {
|
|
853
893
|
if (snap) { try { snap.cleanup() } catch {} }
|
|
854
894
|
}
|
|
855
|
-
storeQualityScan({ specBase, cwd, changeName, testResult: testCheck, lintResult: lintCheck, smokeResult, coverageCheck, usedSnapshot: Boolean(snap), missReason: _missReasonParts.join(';') || null, scopeDecision: { planned: !process.env.SILLYSPEC_VERIFY_GATE_SNAPSHOT_OFF, actual: Boolean(snap) } })
|
|
895
|
+
storeQualityScan({ specBase, cwd, changeName, testResult: testCheck, lintResult: lintCheck, smokeResult, coverageCheck, metricsResult: metricsCheck, metricsRoutesResult: metricsRoutesCheck, usedSnapshot: Boolean(snap), missReason: _missReasonParts.join(';') || null, scopeDecision: { planned: !process.env.SILLYSPEC_VERIFY_GATE_SNAPSHOT_OFF, actual: Boolean(snap) } })
|
|
856
896
|
const testFailed = testCheck.status === 'failed'
|
|
857
897
|
const lintBlocked = shouldBlockVerifyLint(lintCheck)
|
|
898
|
+
// metrics 指标门阻断(2026-10-29-open-metrics-gate task-03):非零/超时即 failed——通过标准
|
|
899
|
+
// 由命令自身定义(无 lint 式归属鉴定,design 风险节声明边界);skipped(未配置/环境缺件)不拦。
|
|
900
|
+
const metricsFailed = Boolean(metricsCheck && metricsCheck.status === 'failed')
|
|
901
|
+
// 路由门阻断(2026-10-29-metrics-routes task-02):任一命中 gate 失败即拦——与单命令门同一道门
|
|
902
|
+
const metricsRoutesFailed = Boolean(metricsRoutesCheck && metricsRoutesCheck.status === 'failed')
|
|
858
903
|
// 2026-10-15-lint-triage-path-unify:lint 归属鉴定降档接线(与 gates --done 路径同源共享
|
|
859
904
|
// helper)——零交集存量债降档 advisory 不 throw;owned/unattributable/鉴定异常维持硬拦
|
|
860
905
|
// (保守)。仅在「lint 单独失败」时降档(testFailed 优先 throw——测试失败不作 lint 降档
|
|
@@ -867,17 +912,17 @@ export async function executeVerifyQualityScan({ cwd, specBase, changeName, plat
|
|
|
867
912
|
// 摩擦计数(friction-signal-hint task-03):记录条件与 throw 条件**刻意解耦**——advisory 档
|
|
868
913
|
// lint 失败(SILLYSPEC_VERIFY_LINT_GATE=advisory 时不 throw)同样是摩擦信号,计数不应随
|
|
869
914
|
// 逃生阀漂移。recordFrictionEvent 自带静默降级,不影响本步推进/throw 语义。
|
|
870
|
-
if (testFailed || lintCheck.status === 'failed') {
|
|
871
|
-
await recordFrictionEvent({ cwd, changeName, platformOpts, type: 'verify_run_failed', detail: testFailed ? 'test' : 'lint' })
|
|
915
|
+
if (testFailed || lintCheck.status === 'failed' || metricsFailed || metricsRoutesFailed) {
|
|
916
|
+
await recordFrictionEvent({ cwd, changeName, platformOpts, type: 'verify_run_failed', detail: testFailed ? 'test' : (metricsFailed || metricsRoutesFailed) ? 'metrics' : 'lint' })
|
|
872
917
|
}
|
|
873
|
-
if (testFailed || (lintBlocked && !lintAdvisoryDowngraded)) {
|
|
918
|
+
if (testFailed || (lintBlocked && !lintAdvisoryDowngraded) || metricsFailed || metricsRoutesFailed) {
|
|
874
919
|
if (snap) {
|
|
875
920
|
try { const { printSnapshotFailureHint } = await import('./gate-snapshot.js')
|
|
876
921
|
printSnapshotFailureHint(snap) } catch { /* 提示失败不影响阻断 */ }
|
|
877
922
|
}
|
|
878
923
|
const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
|
|
879
924
|
await renderVerifyTestAttribution({ cwd, changeName, specBase, runtimeRoot })
|
|
880
|
-
const which = testFailed ? '测试' : 'lint'
|
|
925
|
+
const which = testFailed ? '测试' : (metricsFailed || metricsRoutesFailed) ? '指标门(metrics)' : 'lint'
|
|
881
926
|
throw new Error(`noAI 质量扫描实测${which}失败——本步骤未完成(进度不推进)。输出与归因提示见上方;修复后重跑 sillyspec run verify${changeName ? ` --change ${changeName}` : ''} 复入本步重新实测。`)
|
|
882
927
|
}
|
|
883
928
|
console.log(`\n✅ noAI 质量扫描全绿——本步骤自动完成;--done 对账将复用本次实测(代码变更即失配重测)。机械事实持续全绿时,verify-probes --init 会预填 PASS 结论草稿(草稿待确认,复核后可改写)。`)
|
package/src/stages/execute.js
CHANGED
|
@@ -22,13 +22,45 @@ import { collectExecuteChangedFiles, computeDecisionTouches, renderDecisionTouch
|
|
|
22
22
|
// const 的 TDZ 求值(ESM 环实证 2026-09-14)。此处为同格式本地孪生,漂移由
|
|
23
23
|
// test/knowledge-inject.test.mjs 格式等价断言锁定——改 prompt.js 版必同步改此处。
|
|
24
24
|
import { matchKnowledge } from '../knowledge-match.js'
|
|
25
|
-
import {
|
|
25
|
+
import { matchKnowledgeWithScope, graphPathCandidates, scopeTelemetryRefs } from '../knowledge-graph.js'
|
|
26
26
|
import { appendKnowledgeHit } from '../knowledge-hits.js'
|
|
27
27
|
|
|
28
28
|
// 机械知识注入限额(与 run/prompt.js KNOWLEDGE_INJECT_MAX_FILES/MAX_LINES 同值孪生,R-02 膨胀控制)
|
|
29
29
|
const KNOWLEDGE_INJECT_MAX_FILES = 3
|
|
30
30
|
const KNOWLEDGE_INJECT_MAX_LINES = 40
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* task 卡机器键解析(2026-11-02-knowledge-fusion 评审 P1):target_files / allowed_paths
|
|
34
|
+
* 三形态——内联列表 `[a, b]`、inline `a, b`、块列表(字段行后 `- item` 行);剥 NEW: 前缀
|
|
35
|
+
* 与反引号/引号。旧实现用单行正则 + \s* 跨行,块列表只捕获首项且带 `- ` 前缀。
|
|
36
|
+
*/
|
|
37
|
+
export function parseTaskCardPaths(text) {
|
|
38
|
+
const out = []
|
|
39
|
+
const lines = String(text || '').replace(/\r\n?/g, '\n').split('\n')
|
|
40
|
+
for (let i = 0; i < lines.length; i++) {
|
|
41
|
+
const m = /^(?:target_files|allowed_paths)\s*[::]\s*(.*)$/.exec(lines[i])
|
|
42
|
+
if (!m) continue
|
|
43
|
+
const inline = m[1].trim()
|
|
44
|
+
if (inline) {
|
|
45
|
+
const cleaned = inline.replace(/^\[|\]$/g, '')
|
|
46
|
+
for (const part of cleaned.split(/[,,\s]+/)) if (part) out.push(part)
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
// 块列表:后续 `- item`(缩进可选),遇到非列表行停止
|
|
50
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
51
|
+
const lm = /^\s*-\s+(.+)$/.exec(lines[j])
|
|
52
|
+
if (!lm) break
|
|
53
|
+
out.push(lm[1].trim())
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return out.map((p) => String(p)
|
|
57
|
+
.replace(/^NEW:\s*/i, '')
|
|
58
|
+
.replace(/^`+|`+$/g, '')
|
|
59
|
+
.replace(/^['"]|['"]$/g, '')
|
|
60
|
+
.trim())
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
}
|
|
63
|
+
|
|
32
64
|
/**
|
|
33
65
|
* Wave 粒度机械知识注入段(task-04,FR-04 / D-002@v1 总体方案 D):用本 Wave 各 task 标题拼接
|
|
34
66
|
* 串跑 matchKnowledge,命中文件正文以纯文本段注入 buildWavePrompt 输出(**禁止 {{include:}} 形式**
|
|
@@ -46,21 +78,32 @@ function buildWaveKnowledgeSection(changeDir, wave) {
|
|
|
46
78
|
const query = (Array.isArray(wave && wave.tasks) ? wave.tasks : [])
|
|
47
79
|
.map(t => String((t && t.name) || '')).filter(Boolean).join(' ')
|
|
48
80
|
if (!query.trim()) return ''
|
|
49
|
-
|
|
50
|
-
//
|
|
51
|
-
const
|
|
52
|
-
|
|
81
|
+
// scope 键(2026-11-02-knowledge-fusion FR-04):Wave 用 task 卡 target_files/allowed_paths
|
|
82
|
+
// (机器键)→ graphPathCandidates 规整为图内精确文件路径;无卡/无声明不编造
|
|
83
|
+
const scopeFiles = graphPathCandidates((wave.tasks || []).flatMap((t) => {
|
|
84
|
+
const file = t && (t.file || (t.index ? path.join('tasks', `task-${String(t.index).padStart(2, '0')}.md`) : ''))
|
|
85
|
+
if (!file) return []
|
|
86
|
+
try {
|
|
87
|
+
return parseTaskCardPaths(readFileSync(path.join(changeDir, file), 'utf8'))
|
|
88
|
+
} catch { return [] }
|
|
89
|
+
}))
|
|
90
|
+
const result = matchKnowledgeWithScope(knowledgeDir, query, { scopeFiles })
|
|
91
|
+
const humanEntries = result.entries || []
|
|
92
|
+
const routeHits = result.routeHits || []
|
|
93
|
+
const scopeEntries = result.scopeEntries || []
|
|
94
|
+
if (humanEntries.length === 0 && routeHits.length === 0 && scopeEntries.length === 0) return ''
|
|
53
95
|
const lines = []
|
|
54
|
-
if (
|
|
96
|
+
if (humanEntries.length > 0 || routeHits.length > 0 || scopeEntries.length > 0) {
|
|
55
97
|
// ── 以下与 run/prompt.js renderKnowledgeInjectSection 同格式孪生(含段头文案/截断标记)──
|
|
56
98
|
const ref = e => (e.anchor ? `${e.file}#${e.anchor}` : e.file)
|
|
57
99
|
lines.push(`### 📚 命中知识(CLI 按 Wave 任务名机械匹配,top-${KNOWLEDGE_INJECT_MAX_FILES}——勿自行重跑 INDEX 匹配)`)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
lines.push(
|
|
100
|
+
if (humanEntries.length > 0) {
|
|
101
|
+
lines.push(`Status: matched | Entries: ${humanEntries.length} | Sources:`)
|
|
102
|
+
for (const e of humanEntries) lines.push(` - ${ref(e)}`)
|
|
103
|
+
lines.push(`(命中清单如上;正文按相关度注入前 ${KNOWLEDGE_INJECT_MAX_FILES} 个不同文件,单文件首 ${KNOWLEDGE_INJECT_MAX_LINES} 行截断——未注入条目需要时按 Sources 路径自行读取)`)
|
|
61
104
|
const seen = new Set()
|
|
62
105
|
const picked = []
|
|
63
|
-
for (const e of
|
|
106
|
+
for (const e of humanEntries) {
|
|
64
107
|
if (seen.has(e.file)) continue
|
|
65
108
|
seen.add(e.file)
|
|
66
109
|
picked.push(e)
|
|
@@ -85,18 +128,23 @@ function buildWaveKnowledgeSection(changeDir, wave) {
|
|
|
85
128
|
}
|
|
86
129
|
}
|
|
87
130
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
131
|
+
if (routeHits.length > 0) {
|
|
132
|
+
lines.push(`🧭 决策/FR 路由命中(专用通道,按需 Read):${routeHits.slice(0, 3).map(ref).join(';')}`)
|
|
133
|
+
}
|
|
134
|
+
if (scopeEntries.length > 0) {
|
|
135
|
+
lines.push(`📚 触达文件关联手工条目(scope,按需 Read):${scopeEntries.slice(0, 3).map(ref).join(';')}`)
|
|
136
|
+
}
|
|
92
137
|
}
|
|
93
138
|
try {
|
|
94
139
|
appendKnowledgeHit(path.join(specBase, '.runtime'), {
|
|
95
140
|
type: 'inject',
|
|
96
141
|
change: path.basename(changeDir),
|
|
97
142
|
query,
|
|
98
|
-
matchedFiles: result.matched ?
|
|
99
|
-
|
|
143
|
+
matchedFiles: result.matched ? [...humanEntries, ...routeHits].map(e => (e.anchor ? `${e.file}#${e.anchor}` : e.file)) : [],
|
|
144
|
+
...(result.matched ? (() => {
|
|
145
|
+
const scopeMatchedFiles = scopeTelemetryRefs(result, { includeRejected: false })
|
|
146
|
+
return scopeMatchedFiles.length > 0 ? { scopeMatchedFiles } : {}
|
|
147
|
+
})() : {}),
|
|
100
148
|
})
|
|
101
149
|
} catch { /* 遥测 fail-soft(R-04) */ }
|
|
102
150
|
return lines.join('\n') + '\n'
|