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.
@@ -0,0 +1,79 @@
1
+ {"id":"s-tel-01","repo":"sillyspec","face":"decisions","query":"2026-09-28-kgd 方案探索:用枚举词表扫描开放世界形态的需求并自动分类","expected":["decisions/cli-entry.md#D-unmapped-150@v1"],"note":"防复潮:查询应召回已否决的「枚举开放世界」决策(epoch 迁移后落 cli-entry 域)"}
2
+ {"id":"s-tel-02","repo":"sillyspec","face":"decisions","query":"route-hindsight 模块(指标计算/阈值/落库/读取)+单测 brainstorm Step4/5 指引模板加机制词检索固定动作","expected":["decisions/core-engine.md"],"note":"防复潮注入挂 brainstorm Step2 的决策在 core-engine 域"}
3
+ {"id":"s-tel-03","repo":"sillyspec","face":"decisions","query":"flow.js 前门盘问渲染+hindsight 提示注入+收口指标接线+单测 complete.js 方案步 --done 门检索回显+config-schema 逃生阀","expected":["decisions/cli-entry.md"],"note":"flow 前门/命令面决策在 cli-entry 域"}
4
+ {"id":"s-tel-04","repo":"sillyspec","face":"fr","query":"标题/摘要提取对段头行免疫:标签行跳过取次行+复合词白名单+模块索引摘要同源+模板首行改概括句","expected":["fr/cli-entry.md"],"subset":"approx","note":"changes.title 三段链路等 FR 在 cli-entry 域"}
5
+ {"id":"s-tel-05","repo":"sillyspec","face":"decisions","query":"用枚举词表扫描开放世界形态的需求并自动分类","expected":["decisions/cli-entry.md#D-unmapped-150@v1"],"note":"同一意图的短查询变体"}
6
+ {"id":"s-man-01","repo":"sillyspec","face":"entries","query":"worktree 清理 node_modules junction 删除","expected":["sillyspec-gotchas.md"],"scope_files":["src/worktree.js"],"subset":"frozen","note":"junction 解链坑在 gotchas 手工条目"}
7
+ {"id":"s-man-02","repo":"sillyspec","face":"decisions","query":"worktree 清理 node_modules junction 删除","expected":["decisions/worktree.md#D-worktree-001@v1"],"scope_files":["src/worktree.js"],"subset":"frozen","note":"两次事故实录的决策条目(hybrid 面已命中 raw_rank=1)"}
8
+ {"id":"s-man-03","repo":"sillyspec","face":"entries","query":"worktree doctor --fix 标记 installed 但 node_modules junction 实际未建","expected":["sillyspec-gotchas.md#worktree-doctor---fix-标记-installed-但-node_modules-junction-实际未建"],"scope_files":["src/worktree.js"],"subset":"frozen","note":"精确锚点命中验证"}
9
+ {"id":"s-man-04","repo":"sillyspec","face":"entries","query":"daemon mcp-server 子进程不继承 claude.exe 环境","expected":["patterns.md#mcp-server-子进程不继承-claudeexe-完整环境env-必须放-mcpserversenv"],"scope_files":["src/mcp-server.js"],"subset":"frozen"}
10
+ {"id":"s-man-05","repo":"sillyspec","face":"entries","query":"execute worktree 里没有 node_modules 子代理 cwd 要显式 worktree 路径","expected":["sillyspec-gotchas.md#execute-worktree-无-node_modules--子代理-cwd-需显式-worktree-路径"],"scope_files":["src/stages/execute.js"],"subset":"frozen"}
11
+ {"id":"s-man-06","repo":"sillyspec","face":"entries","query":"reopen --from step n 后 --done 把后续未执行步骤回填 completed","expected":["sillyspec-gotchas.md#reopen---from-step-n-后---done-会把后续未执行步骤回填-completed"]}
12
+ {"id":"s-man-07","repo":"sillyspec","face":"entries","query":"sqlite pragma foreign_keys 默认关闭 cascade 失效 孤儿变更清理","expected":["sillyspec-gotchas.md#sqlite-pragma-foreign_keys-默认关闭致-cascade-失效清理孤儿变更"]}
13
+ {"id":"s-man-08","repo":"sillyspec","face":"entries","query":"plan execute 子代理 cwd 设到变更目录 嵌套 sillyspec 副作用","expected":["sillyspec-gotchas.md#planexecute-子代理可能把-cwd-设到变更目录产生嵌套-sillyspec-副作用"]}
14
+ {"id":"s-man-09","repo":"sillyspec","face":"fr","query":"sillyspec 版本发布 package.json 同步 R5 锚","expected":["fr/release.md"],"subset":"approx"}
15
+ {"id":"s-man-10","repo":"sillyspec","face":"fr","query":"docs gate 基线棘轮 339 替换 379","expected":["fr/docs.md"],"subset":"approx"}
16
+ {"id":"p-tel-01","repo":"platform","face":"entries","query":"工具输出捕获链路按 Windows 码页探测解码,修复 TOOL_RESULT 中文乱码落库问题","expected":["known-issues.md"],"note":"码页乱码条目在 known-issues"}
17
+ {"id":"p-tel-02","repo":"platform","face":"entries","query":"修复 daemon 码页探测解码器 GBK 流式回退死代码(StringDecoder.write 不抛错)并顺修两处注释漂移","expected":["known-issues.md"]}
18
+ {"id":"p-tel-03","repo":"platform","face":"entries","query":"buildSpawnEnv 注入 PYTHONIOENCODING/PYTHONUTF8 缺省让 claude Bash 子进程源端吐 UTF-8","expected":["known-issues.md"]}
19
+ {"id":"p-tel-04","repo":"platform","face":"entries","query":"修复 codex driver JSON-RPC id 空间碰撞:nextRpcId 与 nextJsonRpcId 统一单计数器分配","expected":["patterns.md"]}
20
+ {"id":"p-tel-05","repo":"platform","face":"entries","query":"后端事件表 ORM + alembic 建表迁移 + conftest 建表","expected":["known-issues.md"],"note":"alembic 并行变更撞 revision 多 head 坑"}
21
+ {"id":"p-tel-06","repo":"platform","face":"entries","query":"后端 schema + service(append_events 去重上限 / list_events 正序增量)","expected":["conventions.md"]}
22
+ {"id":"p-tel-07","repo":"platform","face":"entries","query":"蒸馏任务条点击跳转会话页——distill-task-bar 增查看会话入口,经 /sessions?session=<agent_session_id> 深链打开对应蒸馏会话","expected":["known-issues.md"],"note":"AgentMission.session_id 列不可信判别口径坑"}
23
+ {"id":"p-tel-08","repo":"platform","face":"decisions","query":"claude 会话 one_m 勾选未作用于主模型——ANTHROPIC_MODEL 与 interactive create model 参数按角色映射 one_m 补 [1m] 后缀","expected":["decisions/interactive.md"]}
24
+ {"id":"p-tel-09","repo":"platform","face":"entries","query":"会话页空闲降频:消息队列兜底轮询 5s→30s(SSE queue_changed 事件驱动即时刷新为主链,轮询纯兜底)","expected":["conventions.md#前端-sse-消费统一-fetch-ssetoken-走-authorization-header禁用-eventsource"]}
25
+ {"id":"p-tel-10","repo":"platform","face":"fr","query":"手机端会话页样式优化:降噪(用量条/配置条/占位文案)+ 会话内容放大(气泡字号宽度),仅 mobile 变体生效","expected":["fr/app-mobile-pages.md"],"subset":"approx"}
26
+ {"id":"p-tel-11","repo":"platform","face":"fr","query":"手机端会话页功能入口收纳(用户约束:不砍功能):用量条收进头部更多菜单会话用量区,配置条 mobile 增设设置展开钮","expected":["fr/app-mobile-pages.md"],"subset":"approx"}
27
+ {"id":"p-tel-12","repo":"platform","face":"fr","query":"手机端会话页二轮样式收敛:正文 16 回调 14px,底部配置区一行化,打断钮 mobile 图标化","expected":["fr/app-mobile-pages.md"],"subset":"approx"}
28
+ {"id":"p-tel-13","repo":"platform","face":"entries","query":"沉淀弹层三修:快速修复多选失效(只能选一个)弹层样式溢出超出视口 新建 agent 配置补供应商/模型选择","expected":["conventions.md#tailwind-md-是视口断点非容器断点侧栏内嵌组件禁用响应式前缀"]}
29
+ {"id":"p-tel-14","repo":"platform","face":"fr","query":"修复 frontend-ci 连续失败:page.test.tsx user_input 断言未随 ql-20260918-003 行为变更同步","expected":["fr/api-types.md"],"subset":"approx"}
30
+ {"id":"p-tel-15","repo":"platform","face":"fr","query":"registry 注册两 format + host-fs-handler 透传新字段与 totalUsage 平台 messages schema 可选新字段 + openapi/gen:types","expected":["fr/host-fs-handler.md"],"subset":"approx"}
31
+ {"id":"p-dec-01","repo":"platform","face":"decisions","query":"backend 落选 方案 B 扩展 CLI 向量端点至全语料 放开 JWT","expected":["decisions/backend.md#D-backend-009@v1"]}
32
+ {"id":"p-dec-02","repo":"platform","face":"decisions","query":"backend 落选 方案 C 基于 DB 表混合检索 ScanDocument 新增 knowledge 入库","expected":["decisions/backend.md#D-backend-010@v1"]}
33
+ {"id":"p-dec-03","repo":"platform","face":"decisions","query":"backend 落选 本地文件缓存 向量序列化 runtime 缓存文件","expected":["decisions/backend.md#D-backend-020@v1"]}
34
+ {"id":"p-dec-04","repo":"platform","face":"decisions","query":"backend 落选 仅做启动预热 不改存储","expected":["decisions/backend.md#D-backend-021@v1"]}
35
+ {"id":"p-dec-05","repo":"platform","face":"decisions","query":"backend 落选 方案 B 后台常驻同步协程","expected":["decisions/backend.md#D-backend-023@v1"]}
36
+ {"id":"p-dec-06","repo":"platform","face":"decisions","query":"backend 落选 平台行携带记录名 方案 B","expected":["decisions/backend.md#D-backend-033@v1"]}
37
+ {"id":"p-dec-07","repo":"platform","face":"decisions","query":"backend 落选 新增 daemon 列记录 RPC 方案 C","expected":["decisions/backend.md#D-sillyhub-daemon-001@v1"]}
38
+ {"id":"p-dec-08","repo":"platform","face":"decisions","query":"backend 落选 方案 C 首查触发 周期刷新混合","expected":["decisions/backend.md#D-backend-024@v1"]}
39
+ {"id":"p-scope-01","repo":"platform","face":"scope","query":"frontend SSE fetch-sse Authorization header","expected":["conventions.md#前端-sse-消费统一-fetch-ssetoken-走-authorization-header禁用-eventsource"],"scope_files":["frontend/src/lib/fetch-sse.ts"],"subset":"added"}
40
+ {"id":"p-scope-02","repo":"platform","face":"scope","query":"backend auth_deps monkeypatch has_permission","expected":["testing-gotchas.md#后端monkeypatch-has_permission-必须双-patchauth_deps-导入时绑定--rbac-模块属性"],"scope_files":["backend/app/core/auth_deps.py"],"subset":"added"}
41
+ {"id":"p-scope-03","repo":"platform","face":"scope","query":"backend audit hooks lifespan 审计日志","expected":["known-issues.md#-audit_hooks-只在测试-lifespan-注册生产审计要业务代码显式写-auditlog"],"scope_files":["backend/app/main.py"],"subset":"added"}
42
+ {"id":"s-scope-01","repo":"sillyspec","face":"scope","query":"cli flag 白名单死路报错","expected":["known-issues.md#-cli-flag-白名单死路报错指引指向未登记-flag双实例"],"scope_files":["test/flag-contract.test.mjs"],"subset":"added"}
43
+ {"id":"s-scope-02","repo":"sillyspec","face":"scope","query":"知识匹配引擎 枚举开放世界 否决","expected":["decisions/core-engine.md#D-unmapped-197@v1"],"scope_files":["src/knowledge-match.js"],"subset":"added"}
44
+ {"id":"s-scope-03","repo":"sillyspec","face":"scope","query":"flow 前门 决策记录","expected":["decisions/cli-entry.md#D-unmapped-433@v1"],"scope_files":["src/flow.js"],"subset":"added"}
45
+ {"id":"s-scope-04","repo":"sillyspec","face":"scope","query":"execute 阶段 风险死路","expected":["decisions/stages.md#D-unmapped-153@v1"],"scope_files":["src/stages/execute.js"],"subset":"added"}
46
+ {"id":"s-scope-05","repo":"sillyspec","face":"scope","query":"CLI 入口 决策","expected":["decisions/cli-entry.md#D-unmapped-161@v1"],"scope_files":["src/index.js"],"subset":"added"}
47
+ {"id":"p-scope-04","repo":"platform","face":"scope","query":"frontend SSE agent-stream 流式消费","expected":["conventions.md#前端-sse-消费统一-fetch-ssetoken-走-authorization-header禁用-eventsource"],"scope_files":["frontend/src/lib/agent-stream.ts"],"subset":"added"}
48
+ {"id":"p-scope-05","repo":"platform","face":"scope","query":"login_enabled get_current_user 检查","expected":["known-issues.md#-2026-06-17--login_enabled-必须在-get_current_user-检查不能只在-login-入口commit-d62ec975"],"scope_files":["backend/app/core/auth_deps.py"],"subset":"added"}
49
+ {"id":"p-scope-06","repo":"platform","face":"scope","query":"tailwind md 视口断点 侧栏","expected":["conventions.md#tailwind-md-是视口断点非容器断点侧栏内嵌组件禁用响应式前缀"],"scope_files":["frontend/src/components/changes/detail/change-sessions-card.tsx"],"subset":"added"}
50
+ {"id":"s-fusion-01","repo":"sillyspec","face":"entries","query":"worktree 清理流程说明","expected":["known-issues.md#-cli-flag-白名单死路报错指引指向未登记-flag双实例"],"scope_files":["test/flag-contract.test.mjs"],"subset":"fusion","note":"路由命中 worktree 相关但手工条目仅 scope doc-refs 可达(融合增量)"}
51
+ {"id":"s-fusion-02","repo":"sillyspec","face":"decisions","query":"cli flag 白名单 死路 报错","expected":["decisions/core-engine.md#D-unmapped-197@v1"],"scope_files":["src/knowledge-match.js"],"subset":"fusion","note":"路由命中 known-issues 但 rejected 决策仅 scope 两跳可达(融合增量)"}
52
+ // organic 评测集 — 遥测 query 抽样 + 人工 anchor 级标签(冻结:提交后不随遥测漂移)
53
+ // 字段:id / repo(sillyspec|platform) / face(entries|decisions|search|scope) / query / expected[]
54
+ // expected 含 "#" = 精确 file#anchor;不含 "#" = 文件级(匹配该文件任意命中)
55
+ // scope_files(可选)= scope 面图遍历的机器键;无此字段的 case 不进 scope 面
56
+ // 标注口径:查询的真实技术意图应对口的知识条目;当前未命中即为红基线,供后续变更对比
57
+
58
+ {"id":"s-fr-prod-01","repo":"sillyspec","face":"fr","query":"worktree 工作树 清理 隔离","scope_files":["src/worktree.js"],"expected":["fr/worktree.md"],"subset":"prod","note":"生产同源:src/worktree.js → worktree 域"}
59
+ {"id":"p-fr-prod-01","repo":"platform","face":"fr","query":"SSE 流式消费 token authorization","scope_files":["frontend/src/lib/fetch-sse.ts"],"expected":["fr/frontend.md"],"subset":"prod","note":"生产同源:fetch-sse.ts → frontend 域"}
60
+ {"id":"auto-sillyspec-01","repo":"sillyspec","face":"decisions","query":"cli-entry 收口机器门拦 decisions.md 缺席","expected":["decisions/cli-entry.md#D-unmapped-433@v1"]}
61
+ {"id":"auto-sillyspec-02","repo":"sillyspec","face":"decisions","query":"core-engine 否决入库侧路由词表派生 同窗词片按序截断=抽签","expected":["decisions/core-engine.md#D-unmapped-197@v1"]}
62
+ {"id":"auto-sillyspec-03","repo":"sillyspec","face":"decisions","query":"core-engine 错位目录归一否决「marker 值改为完整目录名」","expected":["decisions/core-engine.md#D-unmapped-375@v1"]}
63
+ {"id":"auto-sillyspec-04","repo":"sillyspec","face":"decisions","query":"core-engine archive 复合真相源方案否决","expected":["decisions/core-engine.md#D-unmapped-377@v1"]}
64
+ {"id":"auto-sillyspec-05","repo":"sillyspec","face":"decisions","query":"docs 收割器机器自动拆 rejected 条目","expected":["decisions/docs.md#D-unmapped-299@v1"]}
65
+ {"id":"auto-sillyspec-06","repo":"sillyspec","face":"decisions","query":"docs distill 侧从 confirmed","expected":["decisions/docs.md#D-unmapped-300@v1"]}
66
+ {"id":"auto-sillyspec-07","repo":"sillyspec","face":"decisions","query":"docs 只修厚道不动轻量道","expected":["decisions/docs.md#D-unmapped-301@v1"]}
67
+ {"id":"auto-sillyspec-08","repo":"sillyspec","face":"decisions","query":"docs 存量 40+ 种散值","expected":["decisions/docs.md#D-unmapped-436@v1"]}
68
+ {"id":"auto-platform-01","repo":"platform","face":"decisions","query":"app-admin-pages 落选 并入设置页 Tab(方案","expected":["decisions/app-admin-pages.md#D-unmapped-004@v1"]}
69
+ {"id":"auto-platform-02","repo":"platform","face":"decisions","query":"app-admin-pages 落选 工作区首页挂卡片(方案 C)","expected":["decisions/app-admin-pages.md#D-unmapped-005@v1"]}
70
+ {"id":"auto-platform-03","repo":"platform","face":"decisions","query":"backend 落选 方案 B(扩展","expected":["decisions/backend.md#D-backend-009@v1"]}
71
+ {"id":"auto-platform-04","repo":"platform","face":"decisions","query":"backend 落选 方案 C(基于","expected":["decisions/backend.md#D-backend-010@v1"]}
72
+ {"id":"auto-platform-05","repo":"platform","face":"decisions","query":"backend 落选 本地文件缓存(向量序列化到 .runtime","expected":["decisions/backend.md#D-backend-020@v1"]}
73
+ {"id":"auto-platform-06","repo":"platform","face":"decisions","query":"backend 落选 仅做启动预热(不改存储)","expected":["decisions/backend.md#D-backend-021@v1"]}
74
+ {"id":"auto-platform-07","repo":"platform","face":"decisions","query":"backend 落选 方案 B(后台常驻同步协程)","expected":["decisions/backend.md#D-backend-023@v1"]}
75
+ {"id":"auto-platform-08","repo":"platform","face":"decisions","query":"backend 落选 方案 C(首查触发","expected":["decisions/backend.md#D-backend-024@v1"]}
76
+ {"id":"auto-platform-09","repo":"platform","face":"decisions","query":"backend 落选 平台行携带记录名(方案 B)","expected":["decisions/backend.md#D-backend-033@v1"]}
77
+ {"id":"auto-platform-10","repo":"platform","face":"decisions","query":"backend 落选 新增 daemon「列记录」RPC(方案","expected":["decisions/backend.md#D-sillyhub-daemon-001@v1"]}
78
+ {"id":"auto-platform-11","repo":"platform","face":"decisions","query":"backend 落选 方案 C(进程内卸载","expected":["decisions/backend.md#D-unmapped-057@v1"]}
79
+ {"id":"auto-platform-12","repo":"platform","face":"decisions","query":"backend 方案 B(只加大宽限窗 不加族系列)","expected":["decisions/backend.md#D-unmapped-065@v1"]}
@@ -0,0 +1,132 @@
1
+ /**
2
+ * eval/knowledge/fixtures/index.mjs — 六类缺陷探针 fixture(合成最小知识库 + 正反例断言)
3
+ *
4
+ * 每个 fixture 的 files 相对 .sillyspec/knowledge/ 物化到 mkdtemp 仓根后跑真实引擎。
5
+ * 断言语义(evaluateCase):contains 必须全部命中、excludes 必须全部不命中、empty 指定空性、
6
+ * score_contract 要求 search 视图 score === matchedTags.length、health 逐键对账 catalog。
7
+ * 探针的 pass 语义是「修复后的期望」:缺陷存在时 pass=false(executor 检出缺陷),修复后转 true。
8
+ */
9
+ export const PROBE_FIXTURES = [
10
+ {
11
+ defect: 'score-contract',
12
+ files: {
13
+ 'INDEX.md': '# Knowledge Index\n\n## Handbook\n- alpha|beta|gamma|delta → [Alpha 手册](handbook.md#alpha)\n',
14
+ 'handbook.md': '# Handbook\n\n## Alpha\n\nalpha 的说明文字。\n',
15
+ },
16
+ cases: [
17
+ {
18
+ name: 'search score 必须等于实际命中标签数',
19
+ kind: 'search',
20
+ query: 'alpha 用法',
21
+ expect: { score_contract: true, contains: ['handbook.md'] },
22
+ },
23
+ ],
24
+ },
25
+ {
26
+ defect: 'word-boundary',
27
+ files: {
28
+ 'INDEX.md': [
29
+ '# Knowledge Index', '',
30
+ '## Handbook',
31
+ '- node → [Node 环境](env.md#node)',
32
+ '- worktree → [Worktree](wt.md#wt)', '',
33
+ ].join('\n'),
34
+ 'env.md': '# 环境\n\n## node\n\nnode 版本要求说明。\n',
35
+ 'wt.md': '# Worktree\n\n## wt\n\nworktree 隔离说明。\n',
36
+ },
37
+ cases: [
38
+ { name: 'node 不得命中 node_modules(_ 是词字符)', kind: 'entries', query: '修复 node_modules junction', expect: { excludes: ['env.md'] } },
39
+ { name: 'node 独立词仍命中', kind: 'entries', query: 'node 版本升级', expect: { contains: ['env.md'] } },
40
+ { name: 'worktree 不得命中 worktree_apply(_ 是词字符)', kind: 'entries', query: 'worktree_apply 冲突', expect: { excludes: ['wt.md'] } },
41
+ ],
42
+ },
43
+ {
44
+ defect: 'route-rows',
45
+ files: {
46
+ 'INDEX.md': [
47
+ '# Knowledge Index', '',
48
+ '## Manual',
49
+ '- alpha|beta → [Alpha 手册](handbook.md#alpha)', '',
50
+ '## Decisions',
51
+ '- alpha|beta → [决策域](decisions/demo.md)', '',
52
+ '## FR',
53
+ '- alpha|beta → [FR 域](fr/demo.md)', '',
54
+ ].join('\n'),
55
+ 'handbook.md': '# Handbook\n\n## Alpha\n\nalpha 手册内容。\n',
56
+ 'decisions/demo.md': '## D-001@v1 alpha 决策\n状态:implemented\n变更:fix-demo\n',
57
+ 'fr/demo.md': '## FR-demo-001 alpha 需求\n变更:fix-demo\n状态:active\n',
58
+ },
59
+ cases: [
60
+ {
61
+ name: 'entries 不得含 fr/decisions 路由行',
62
+ kind: 'entries',
63
+ query: 'alpha 功能',
64
+ expect: { contains: ['handbook.md#alpha'], excludes: ['decisions/demo.md', 'fr/demo.md'] },
65
+ },
66
+ { name: 'decisionHits 通道不受影响', kind: 'decisions', query: 'alpha 功能', expect: { contains: ['decisions/demo.md#D-001@v1'] } },
67
+ ],
68
+ },
69
+ {
70
+ defect: 'scope-manual',
71
+ files: {
72
+ 'INDEX.md': [
73
+ '# Knowledge Index', '',
74
+ '## Decisions',
75
+ '- foo → [决策域](decisions/demo.md)', '',
76
+ '## Gotchas',
77
+ '- foo|junction → [Foo 坑](gotchas.md#foo-junction)',
78
+ '- other → [Other 区](gotchas.md#other-section)', '',
79
+ ].join('\n'),
80
+ 'gotchas.md': [
81
+ '# Gotchas', '',
82
+ '## foo junction 清理', '',
83
+ '修 src/foo.js 时先解链 node_modules junction。', '',
84
+ '## other section', '',
85
+ '另一个模块维护 src/bar.js 的说明。', '',
86
+ ].join('\n'),
87
+ 'decisions/demo.md': '## D-001@v1 foo 决策\n状态:implemented\n锚点:src/foo.js:10\n变更:fix-demo\n',
88
+ },
89
+ cases: [
90
+ { name: '决策锚点强边可达(基础链路)', kind: 'scope', scope_files: ['src/foo.js'], expect: { contains: ['decisions/demo.md#D-001@v1'] } },
91
+ { name: '手工条目正文路径可达(手工知识不短路)', kind: 'scope', scope_files: ['src/foo.js'], expect: { contains: ['gotchas.md#foo-junction'] } },
92
+ { name: 'entries-only 正例(无决策锚点仍返回)', kind: 'scope', scope_files: ['src/bar.js'], expect: { contains: ['gotchas.md#other-section'] } },
93
+ { name: 'section 隔离负例(异路径不串节)', kind: 'scope', scope_files: ['src/bar.js'], expect: { excludes: ['gotchas.md#foo-junction'] } },
94
+ ],
95
+ },
96
+ {
97
+ defect: 'fixed-entries',
98
+ files: {
99
+ 'INDEX.md': [
100
+ '# Knowledge Index', '',
101
+ '## Known Issues',
102
+ '- alpha → [alpha 已修复](known-issues.md#alpha-已修复)',
103
+ '- beta → [beta 坑](known-issues.md#beta-坑)', '',
104
+ ].join('\n'),
105
+ 'known-issues.md': [
106
+ '# Known Issues', '',
107
+ '## 🟢 alpha 问题已修复(commit abc)', '',
108
+ '历史坑,已修复。', '',
109
+ '## 🔴 beta 问题', '',
110
+ '当前仍然存在的坑。', '',
111
+ ].join('\n'),
112
+ },
113
+ cases: [
114
+ { name: '已修复条目不进默认 entries', kind: 'entries', query: 'alpha 问题', expect: { excludes: ['known-issues.md#alpha-已修复'] } },
115
+ { name: '未修复条目正常命中', kind: 'entries', query: 'beta 问题', expect: { contains: ['known-issues.md#beta-坑'] } },
116
+ ],
117
+ },
118
+ {
119
+ defect: 'linkage',
120
+ files: {
121
+ 'INDEX.md': '# Knowledge Index\n\n## Decisions\n- demo|linkage → [决策域](decisions/demo.md)\n',
122
+ 'decisions/demo.md': [
123
+ '## D-001@v1 有锚点', '状态:implemented', '锚点:src/foo.js:10', '',
124
+ '## D-002@v1 有文件字段', '状态:implemented', '文件:src/bar.js', '',
125
+ '## D-003@v1 无关联', '状态:implemented', '理由:纯决策记录', '',
126
+ ].join('\n'),
127
+ },
128
+ cases: [
129
+ { name: '语料健康报告关联率', kind: 'catalog', expect: { health: { total: 3, anchored: 1, files: 1, linked: 2 } } },
130
+ ],
131
+ },
132
+ ]
@@ -0,0 +1,171 @@
1
+ /**
2
+ * eval/knowledge/lib/dataset.mjs — 三源数据集加载/校验/现场构建
3
+ *
4
+ * organic:提交冻结的 JSONL(遥测 query 抽样 + 人工 anchor 级标签),加载即校验。
5
+ * self:现场从归档 flow-state.input 构建,期望集 = 该变更蒸馏出的决策(按 变更+id/原编号 映射),
6
+ * 仅用于「蒸馏→检索链路」smoke,禁止进质量指标(自产自测偏差,design 已论证)。
7
+ * probes:fixtures 目录枚举(每个子目录一个缺陷探针,runner 负责执行)。
8
+ */
9
+ import { existsSync, readFileSync, readdirSync } from 'node:fs'
10
+ import { join } from 'node:path'
11
+ import { parseDecisionEntries, normalizeAnchorForCompare } from '../../../src/knowledge-match.js'
12
+ import { readFlowState } from '../../../src/flow-state.js'
13
+
14
+ const FACES = new Set(['entries', 'decisions', 'search', 'scope', 'fr'])
15
+
16
+ /** organic case 字段校验;返回 null = 合法,否则错误原因。
17
+ * repo 是数据集标签(对应 --repo 的 label 或路径 basename),非项目枚举——任意仓可用。 */
18
+ export function validateOrganicCase(c) {
19
+ if (!c || typeof c !== 'object' || Array.isArray(c)) return 'not-object'
20
+ if (!c.id || typeof c.id !== 'string') return 'id'
21
+ if (!c.repo || typeof c.repo !== 'string') return 'repo'
22
+ if (!FACES.has(c.face)) return 'face'
23
+ if (!c.query || typeof c.query !== 'string' || !c.query.trim()) return 'query'
24
+ if (!Array.isArray(c.expected) || c.expected.length === 0) return 'expected-empty'
25
+ if (c.expected.some((e) => typeof e !== 'string' || !e.trim())) return 'expected-item'
26
+ if (c.scope_files !== undefined) {
27
+ if (!Array.isArray(c.scope_files) || c.scope_files.length === 0) return 'scope_files'
28
+ if (c.scope_files.some((f) => typeof f !== 'string' || !f.trim())) return 'scope_files-item'
29
+ }
30
+ return null
31
+ }
32
+
33
+ /**
34
+ * 加载 organic JSONL:空行/`//` 注释跳过;坏 JSON、字段缺失、expected 空 → skipped(禁止静默当零命中)。
35
+ * @returns {{cases: Array, skipped: Array<{line:number, reason:string, id?:string}>, missing: boolean}}
36
+ */
37
+ export function loadOrganic(filePath) {
38
+ const cases = []
39
+ const skipped = []
40
+ if (!existsSync(filePath)) return { cases, skipped, missing: true }
41
+ const lines = readFileSync(filePath, 'utf8').split('\n')
42
+ lines.forEach((raw, i) => {
43
+ const line = raw.trim()
44
+ if (!line || line.startsWith('//')) return
45
+ let c
46
+ try { c = JSON.parse(line) } catch { skipped.push({ line: i + 1, reason: 'bad-json' }); return }
47
+ const err = validateOrganicCase(c)
48
+ if (err) { skipped.push({ line: i + 1, reason: err, id: c && c.id }); return }
49
+ cases.push({ source: 'organic', ...c })
50
+ })
51
+ return { cases, skipped, missing: false }
52
+ }
53
+
54
+ /**
55
+ * 现场构建 self 集:归档 flow-state.input → 该变更蒸馏决策的映射(变更+id 或 变更+原编号)。
56
+ * 映射不到任何决策的归档 → skipped(无期望集可测,不造 case)。
57
+ * @returns {{cases: Array, skipped: Array<{archive:string, reason:string}>}}
58
+ */
59
+ export function buildSelfCases(repoRoot, repo) {
60
+ const archive = join(repoRoot, '.sillyspec', 'changes', 'archive')
61
+ const knowledgeDir = join(repoRoot, '.sillyspec', 'knowledge')
62
+ const cases = []
63
+ const skipped = []
64
+ if (!existsSync(archive)) return { cases, skipped }
65
+ const decisions = parseDecisionEntries(knowledgeDir)
66
+ for (const name of readdirSync(archive).sort()) {
67
+ const dir = join(archive, name)
68
+ const st = readFlowState(dir)
69
+ if (!st || !st.input || typeof st.input !== 'string' || !st.input.trim()) {
70
+ skipped.push({ archive: name, reason: 'no-input' })
71
+ continue
72
+ }
73
+ const dmd = join(dir, 'decisions.md')
74
+ if (!existsSync(dmd)) { skipped.push({ archive: name, reason: 'no-decisions-md' }); continue }
75
+ const localIds = [...readFileSync(dmd, 'utf8').matchAll(/^##\s+(D-(?:[A-Za-z0-9_-]+-)?\d+@v\d+)/gm)].map((m) => m[1])
76
+ const own = []
77
+ for (const id of localIds) {
78
+ const hit = decisions.find((h) => h.change === name && (h.id === id || h.originId === id))
79
+ if (hit) own.push(`${hit.file}#${hit.id}`)
80
+ }
81
+ if (own.length === 0) { skipped.push({ archive: name, reason: 'no-mapped-decision' }); continue }
82
+ cases.push({
83
+ source: 'self', id: `self-${name}`, repo, face: 'self',
84
+ query: st.input.trim(), expected: [...new Set(own)], own: [...new Set(own)],
85
+ })
86
+ }
87
+ return { cases, skipped }
88
+ }
89
+
90
+ /**
91
+ * 遥测 inject 事件 → organic 候选池(去重 change+query,单变更限流)。
92
+ * @returns {Array<{change:string, query:string}>}
93
+ */
94
+ export function extractTelemetryPool(telemetryPath, { limitPerChange = 3 } = {}) {
95
+ if (!existsSync(telemetryPath)) return []
96
+ const seen = new Set()
97
+ const byChange = new Map()
98
+ for (const line of readFileSync(telemetryPath, 'utf8').split('\n')) {
99
+ const t = line.trim()
100
+ if (!t) continue
101
+ let r
102
+ try { r = JSON.parse(t) } catch { continue }
103
+ if (!r || r.type !== 'inject' || typeof r.query !== 'string' || typeof r.change !== 'string') continue
104
+ const q = r.query.replace(/\r\n/g, '\n').trim()
105
+ if (!q) continue
106
+ const key = `${r.change}\u0000${q}`
107
+ if (seen.has(key)) continue
108
+ seen.add(key)
109
+ const arr = byChange.get(r.change) || []
110
+ if (arr.length < limitPerChange) arr.push(q)
111
+ byChange.set(r.change, arr)
112
+ }
113
+ const out = []
114
+ for (const [change, queries] of byChange) for (const q of queries) out.push({ change, query: q })
115
+ // 稳定排序(不依赖遥测事件到达顺序——乱序/迟到无关,变量见 design 盲维①)
116
+ out.sort((a, b) => a.change.localeCompare(b.change) || a.query.localeCompare(b.query))
117
+ return out
118
+ }
119
+
120
+ /** 枚举探针 fixture 目录(每子目录一个;无目录 = 空数组)。 */
121
+ export function listProbes(fixturesDir) {
122
+ try {
123
+ return readdirSync(fixturesDir, { withFileTypes: true })
124
+ .filter((d) => d.isDirectory())
125
+ .map((d) => d.name)
126
+ .sort()
127
+ } catch {
128
+ return []
129
+ }
130
+ }
131
+
132
+ /**
133
+ * 标签有效性自检(2026-11-02 复核 P1-①):expected 的 file 必须存在于目标仓 knowledge 下、
134
+ * 含 # 时 anchor 必须能以归一化形态在目标文件正文中找到(不再回退 INDEX——防止「anchor 只在
135
+ * INDEX、heading 已漂移」的旧契约通过自检);scope_files 必须是目标仓真实存在的文件。
136
+ * reject 结构性不可命中标签(如 epoch 迁移后 D-id 换了域文件)。
137
+ * @param {Array} cases organic cases
138
+ * @param {Map<string,string>} repoRootByTag 仓标签 → 仓根
139
+ * @returns {Array<{id:string, expected?:string, reason:string}>}
140
+ */
141
+ export function validateLabels(cases, repoRootByTag) {
142
+ const issues = []
143
+ for (const c of cases) {
144
+ const root = repoRootByTag.get(c.repo)
145
+ if (!root) continue // 未参与本次运行的仓标签不算标签错误
146
+ const kdir = join(root, '.sillyspec', 'knowledge')
147
+ for (const sf of c.scope_files || []) {
148
+ const p = join(root, String(sf).replace(/\\/g, '/'))
149
+ if (!existsSync(p)) issues.push({ id: c.id, expected: sf, reason: 'scope-file-missing' })
150
+ }
151
+ for (const exp of c.expected) {
152
+ const [file, anchor] = String(exp).split('#')
153
+ const fp = join(kdir, file)
154
+ if (!existsSync(fp)) {
155
+ issues.push({ id: c.id, expected: exp, reason: 'file-missing' })
156
+ continue
157
+ }
158
+ if (!anchor) continue
159
+ let text = ''
160
+ try { text = readFileSync(fp, 'utf8') } catch {
161
+ issues.push({ id: c.id, expected: exp, reason: 'file-unreadable' })
162
+ continue
163
+ }
164
+ const normAnchor = normalizeAnchorForCompare(anchor)
165
+ if (!normAnchor || !normalizeAnchorForCompare(text).includes(normAnchor)) {
166
+ issues.push({ id: c.id, expected: exp, reason: 'anchor-heading-missing', file })
167
+ }
168
+ }
169
+ }
170
+ return issues
171
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * eval/knowledge/lib/engine.mjs — 只读适配生产检索引擎(评测面)
3
+ *
4
+ * 统一命中键:entries/search/scope-entry = `file#anchor`(anchor 缺省为 file);
5
+ * decisions/scope-decision = `file#id`;fr = `fr/<域>.md#<FR-id>`。
6
+ * 期望侧不含 "#" 时按文件级前缀匹配(runner 展开)。
7
+ *
8
+ * 三个检索通道与生产同源:
9
+ * - entries/decisions 走 matchKnowledgeHybrid(路由→向量→词片回退,生产 flow/prompt/execute 同入口);
10
+ * - search 走 matchKnowledgeHybrid + buildSearchMatches(cmdSearch 生产构造单一源);
11
+ * - fr 走 readActiveFrDigest(FR 索引 digest 生产通道)+ 标题重叠排序。
12
+ * 零网络零遥测:向量层由调用方 env 关闭;只调纯读函数。图构建缓存写 <specRoot>/.runtime/cache
13
+ * (可再生派生,fixture 场景落 mkdtemp,真实仓仅缓存,不触测试/遥测面)。
14
+ */
15
+ import { join } from 'node:path'
16
+ import { existsSync, readdirSync, readFileSync } from 'node:fs'
17
+ import { tmpdir } from 'node:os'
18
+ import { parseDecisionEntries, keywordMatchesContext } from '../../../src/knowledge-match.js'
19
+ import { buildKnowledgeGraph, scopeRecall } from '../../../src/knowledge-graph.js'
20
+ import { matchKnowledgeHybrid } from '../../../src/knowledge-vector.js'
21
+ import { buildSearchMatches } from '../../../src/stages/knowledge.js'
22
+ import { readActiveFrDigest, frTitleOverlap, resolveTouchedDomains, rankFrDigestForInjection } from '../../../src/fr-index.js'
23
+ import { parseModuleMapSimple } from '../../../src/modules.js'
24
+
25
+ const graphCache = new Map()
26
+
27
+ function specRootOf(repoRoot) { return join(repoRoot, '.sillyspec') }
28
+ function knowledgeDirOf(repoRoot) { return join(specRootOf(repoRoot), 'knowledge') }
29
+
30
+ function entryKey(e) { return e.anchor ? `${e.file}#${e.anchor}` : e.file }
31
+
32
+ /** entries 面:生产 hybrid 入口的 entries(只手工条目;路由行已由变更 1 移入 routeHits)。
33
+ * opts.scopeFiles(仅 fusion 子集传):融合增量 scopeEntries 追加在路由 entries 之后。 */
34
+ export async function runEntries(repoRoot, query, opts = {}) {
35
+ const r = await matchKnowledgeHybrid(knowledgeDirOf(repoRoot), query, { cwd: repoRoot, scopeFiles: opts.scopeFiles || [] })
36
+ const hits = [...(r.entries || []).map(entryKey), ...(r.scopeEntries || []).map(entryKey)]
37
+ return { matched: r.matched, hits, raw: r.entries || [], routeHits: r.routeHits || [], scopeEntries: r.scopeEntries || [] }
38
+ }
39
+
40
+ /** decisions 面:生产 hybrid 入口的 decisionHits(rejected/死路优先序)。
41
+ * opts.scopeFiles(仅 fusion 子集传):融合增量 scopeDecisionHits 追加在路由 decisionHits 之后。 */
42
+ export async function runDecisions(repoRoot, query, opts = {}) {
43
+ const r = await matchKnowledgeHybrid(knowledgeDirOf(repoRoot), query, { cwd: repoRoot, scopeFiles: opts.scopeFiles || [] })
44
+ const hits = [
45
+ ...(r.decisionHits || []).map((h) => `${h.file}#${h.id}`),
46
+ ...(r.scopeDecisionHits || []).map((h) => `${h.file}#${h.id}`),
47
+ ]
48
+ return { hits, raw: r.decisionHits || [], scopeDecisionHits: r.scopeDecisionHits || [] }
49
+ }
50
+
51
+ /** 合并 docs/<项目>/modules/_module-map.yaml 模块索引(生产触达域解析输入)。 */
52
+ function moduleIndexFor(specRoot) {
53
+ const out = {}
54
+ try {
55
+ for (const p of readdirSync(join(specRoot, 'docs'), { withFileTypes: true })) {
56
+ if (!p.isDirectory()) continue
57
+ const f = join(specRoot, 'docs', p.name, 'modules', '_module-map.yaml')
58
+ if (!existsSync(f)) continue
59
+ Object.assign(out, parseModuleMapSimple(readFileSync(f, 'utf8')))
60
+ }
61
+ } catch { /* 无 docs = 空索引 */ }
62
+ return out
63
+ }
64
+
65
+ /** fr 面:带 scope_files 的 case 走生产同源通道(resolveTouchedDomains 触达域 → 域内
66
+ * readActiveFrDigest → rankFrDigestForInjection TierA/TierB);纯文本查询无机器键,走近似面
67
+ * (全域 digest + 标题重叠,channel=approx)并单列、不进对比门槛。 */
68
+ export function runFr(repoRoot, query, opts = {}) {
69
+ const specRoot = specRootOf(repoRoot)
70
+ const kdir = knowledgeDirOf(repoRoot)
71
+ const scopeFiles = Array.isArray(opts.scopeFiles) ? opts.scopeFiles.filter(Boolean) : []
72
+ if (scopeFiles.length > 0) {
73
+ try {
74
+ const domains = [...resolveTouchedDomains(tmpdir(), moduleIndexFor(specRoot), scopeFiles, kdir)]
75
+ const frs = readActiveFrDigest(kdir, domains)
76
+ const ranked = rankFrDigestForInjection({ archiveRoot: join(specRoot, 'changes', 'archive'), frs, changed: scopeFiles })
77
+ const ordered = ranked.ranked || []
78
+ return { hits: ordered.map((f) => `fr/${f.domain}.md#${f.id}`), raw: ordered, channel: 'prod' }
79
+ } catch { /* 生产路由失败 → 近似面兜底(如实标 channel) */ }
80
+ }
81
+ let domains = []
82
+ try {
83
+ domains = readdirSync(join(kdir, 'fr')).filter((f) => f.endsWith('.md')).map((f) => f.replace(/\.md$/, ''))
84
+ } catch { /* 无 fr 目录 = 空面 */ }
85
+ const entries = readActiveFrDigest(kdir, domains)
86
+ const q = String(query || '')
87
+ const scored = entries.map((e) => ({ e, s: frTitleOverlap(q, `${e.id} ${e.title || ''}`) }))
88
+ scored.sort((a, b) => (b.s - a.s) || a.e.id.localeCompare(b.e.id))
89
+ return { hits: scored.map((x) => `fr/${x.e.domain}.md#${x.e.id}`), raw: scored.map((x) => x.e), channel: 'approx' }
90
+ }
91
+
92
+ /**
93
+ * search 面(cmdSearch 等价):真实 hybrid 检索 + 生产构造 buildSearchMatches。
94
+ * limit 缺省 Infinity = 全量(raw 口径);rendered 由 faces 层按消费面 limit 截断。
95
+ * 返回行带 matchedTags(与 score 契约对照用)。
96
+ */
97
+ export async function searchView(repoRoot, query, limit = Infinity) {
98
+ const knowledgeDir = knowledgeDirOf(repoRoot)
99
+ const result = await matchKnowledgeHybrid(knowledgeDir, query, { cwd: repoRoot })
100
+ if (!result || !result.matched) return []
101
+ const ctx = String(query || '').toLowerCase()
102
+ return buildSearchMatches(result, knowledgeDir, limit, query).map((row, i) => ({
103
+ ...row,
104
+ file: result.entries[i] ? result.entries[i].file : row.path.replace(/^knowledge[\\/]/, ''),
105
+ matchedTags: (row.tags || []).filter((k) => keywordMatchesContext(k, ctx)),
106
+ }))
107
+ }
108
+
109
+ /** scope 面:图遍历召回(引擎内建 cap 保持——entry 3 + decision 20,与生产 scopeRecall 一致)。 */
110
+ export function runScope(repoRoot, scopeFiles) {
111
+ const root = specRootOf(repoRoot)
112
+ if (!graphCache.has(root)) graphCache.set(root, buildKnowledgeGraph(root))
113
+ const recall = scopeRecall(graphCache.get(root), scopeFiles || [])
114
+ if (!recall) return { entryHits: [], decisionHits: [], merged: [] }
115
+ const entryHits = recall.entries.map(entryKey)
116
+ const decisionHits = recall.decisionHits.map((h) => `${h.file}#${h.id}`)
117
+ return { entryHits, decisionHits, merged: [...entryHits, ...decisionHits] }
118
+ }
119
+
120
+ /** 语料健康:决策条目总数 / 有锚点 / 有文件字段 / 任一关联(linkage 探针与 baseline 用)。 */
121
+ export function catalogHealth(repoRoot) {
122
+ const kdir = knowledgeDirOf(repoRoot)
123
+ if (!existsSync(join(kdir, 'INDEX.md'))) return { total: 0, anchored: 0, files: 0, linked: 0 }
124
+ const hits = parseDecisionEntries(kdir)
125
+ let anchored = 0
126
+ let files = 0
127
+ let linked = 0
128
+ for (const h of hits) {
129
+ const hasAnchor = Boolean(h.anchor && h.anchor.trim() && h.anchor.trim() !== '未记录')
130
+ const hasFiles = Array.isArray(h.files) && h.files.length > 0
131
+ if (hasAnchor) anchored++
132
+ if (hasFiles) files++
133
+ if (hasAnchor || hasFiles) linked++
134
+ }
135
+ return { total: hits.length, anchored, files, linked }
136
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * eval/knowledge/lib/faces.mjs — 分面执行(raw/rendered 双口径 + 截断归因)
3
+ *
4
+ * raw = 引擎全量命中序(不预先截断);rendered = 各消费面实际切片(flow 📚 行 top-3、
5
+ * rejected top-5、search limit 10、scope entry 3 + decision 20)。raw 命中而 rendered 不中
6
+ * → truncated;每 case 的 raw_rank/rendered_rank 进 details(FR-03 的 rank 记录面)。
7
+ */
8
+ import { runEntries, runDecisions, runFr, searchView, runScope } from './engine.mjs'
9
+ import { hitMatches } from './probes.mjs'
10
+ import { summarize } from './metrics.mjs'
11
+
12
+ export const FACES = ['entries', 'decisions', 'search', 'scope', 'fr']
13
+
14
+ /** 期望谓词排名(file 级前缀语义;1-based,未命中 = Infinity)。 */
15
+ export function rankWithMatches(hits, expected) {
16
+ let best = Infinity
17
+ hits.forEach((h, i) => {
18
+ if (expected.some((e) => hitMatches(h, e))) best = Math.min(best, i + 1)
19
+ })
20
+ return best
21
+ }
22
+
23
+ /**
24
+ * scope 面按 subset 分组指标(frozen/added——FR-05 子集单列,禁止用新增 case 掩盖冻结子集)。
25
+ * reachable = rank 非 null 比例:融合子集(2026-11-02-knowledge-fusion)的价值是「从不可达变可达」,
26
+ * 在「不重排路由」契约下期望可能落在路由尾部(top3=0 但 reachable=1)——两者都要如实可见。
27
+ */
28
+ export function subsetSummaries(faceResult, cases) {
29
+ const groups = {}
30
+ for (const c of cases) {
31
+ const d = faceResult.details.find((x) => x.id === c.id)
32
+ if (!d) continue
33
+ const key = c.subset || 'default'
34
+ ;(groups[key] = groups[key] || []).push(d.raw_rank === null ? Infinity : d.raw_rank)
35
+ }
36
+ const r4 = (x) => Math.round(x * 10000) / 10000
37
+ return Object.fromEntries(Object.entries(groups).map(([k, ranks]) => [
38
+ k,
39
+ { ...summarize(ranks), reachable: ranks.length > 0 ? r4(ranks.filter((r) => Number.isFinite(r)).length / ranks.length) : 0 },
40
+ ]))
41
+ }
42
+
43
+ /** 单仓单面:raw/rendered 双口径 + 截断清单 + 每 case 名次明细。 */
44
+ export async function runFace(repoPath, face, cases) {
45
+ const ranksRaw = []
46
+ const ranksRendered = []
47
+ const truncated = []
48
+ const details = []
49
+ for (const c of cases) {
50
+ let rawHits = []
51
+ let renderedHits = []
52
+ if (face === 'entries') {
53
+ const r = await runEntries(repoPath, c.query, { scopeFiles: c.subset === 'fusion' ? (c.scope_files || []) : [] })
54
+ rawHits = r.hits
55
+ renderedHits = r.hits.slice(0, 3)
56
+ } else if (face === 'decisions') {
57
+ const r = await runDecisions(repoPath, c.query, { scopeFiles: c.subset === 'fusion' ? (c.scope_files || []) : [] })
58
+ rawHits = r.hits
59
+ renderedHits = r.hits.slice(0, 5)
60
+ } else if (face === 'fr') {
61
+ const r = runFr(repoPath, c.query, { scopeFiles: c.scope_files || [] })
62
+ rawHits = r.hits
63
+ renderedHits = r.hits.slice(0, 8)
64
+ } else if (face === 'search') {
65
+ rawHits = (await searchView(repoPath, c.query, Infinity)).map((r) => r.file)
66
+ renderedHits = rawHits.slice(0, 10)
67
+ } else if (face === 'scope') {
68
+ const r = runScope(repoPath, c.scope_files || [])
69
+ rawHits = r.merged
70
+ renderedHits = [...r.entryHits.slice(0, 3), ...r.decisionHits.slice(0, 20)]
71
+ }
72
+ // search 面单位是 file:expected 的 anchor 后缀投影掉
73
+ const expected = face === 'search' ? c.expected.map((e) => String(e).split('#')[0]) : c.expected
74
+ const rr = rankWithMatches(rawHits, expected)
75
+ const rn = rankWithMatches(renderedHits, expected)
76
+ ranksRaw.push(rr)
77
+ ranksRendered.push(rn)
78
+ const isTruncated = Number.isFinite(rr) && !Number.isFinite(rn)
79
+ if (isTruncated) truncated.push(c.id)
80
+ details.push({
81
+ id: c.id,
82
+ raw_rank: Number.isFinite(rr) ? rr : null,
83
+ rendered_rank: Number.isFinite(rn) ? rn : null,
84
+ truncated: isTruncated,
85
+ })
86
+ }
87
+ return { raw: summarize(ranksRaw), rendered: summarize(ranksRendered), truncated, details }
88
+ }