dsh-data-domain-knowledge 0.1.0-alpha.1

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.
Files changed (231) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +360 -0
  3. package/cordis.patch.yml +410 -0
  4. package/eval/README.md +76 -0
  5. package/eval/evaluate-retrieval.mjs +121 -0
  6. package/eval/queries.example.jsonl +2 -0
  7. package/lib/client.js +111415 -0
  8. package/lib/types/config-schema.d.ts +35 -0
  9. package/lib/types/config-schema.d.ts.map +1 -0
  10. package/lib/types/config-schema.js +34 -0
  11. package/lib/types/config-schema.js.map +1 -0
  12. package/lib/types/convert/core-loader.d.ts +32 -0
  13. package/lib/types/convert/core-loader.d.ts.map +1 -0
  14. package/lib/types/convert/core-loader.js +60 -0
  15. package/lib/types/convert/core-loader.js.map +1 -0
  16. package/lib/types/convert/host.d.ts +88 -0
  17. package/lib/types/convert/host.d.ts.map +1 -0
  18. package/lib/types/convert/host.js +479 -0
  19. package/lib/types/convert/host.js.map +1 -0
  20. package/lib/types/convert/ingest.d.ts +125 -0
  21. package/lib/types/convert/ingest.d.ts.map +1 -0
  22. package/lib/types/convert/ingest.js +228 -0
  23. package/lib/types/convert/ingest.js.map +1 -0
  24. package/lib/types/convert/settings-schema.d.ts +18 -0
  25. package/lib/types/convert/settings-schema.d.ts.map +1 -0
  26. package/lib/types/convert/settings-schema.js +26 -0
  27. package/lib/types/convert/settings-schema.js.map +1 -0
  28. package/lib/types/convert/types.d.ts +46 -0
  29. package/lib/types/convert/types.d.ts.map +1 -0
  30. package/lib/types/convert/types.js +41 -0
  31. package/lib/types/convert/types.js.map +1 -0
  32. package/lib/types/diagnosis/cards.d.ts +71 -0
  33. package/lib/types/diagnosis/cards.d.ts.map +1 -0
  34. package/lib/types/diagnosis/cards.js +482 -0
  35. package/lib/types/diagnosis/cards.js.map +1 -0
  36. package/lib/types/diagnosis/problem-classes.d.ts +51 -0
  37. package/lib/types/diagnosis/problem-classes.d.ts.map +1 -0
  38. package/lib/types/diagnosis/problem-classes.js +66 -0
  39. package/lib/types/diagnosis/problem-classes.js.map +1 -0
  40. package/lib/types/diagnosis/schema.d.ts +65 -0
  41. package/lib/types/diagnosis/schema.d.ts.map +1 -0
  42. package/lib/types/diagnosis/schema.js +208 -0
  43. package/lib/types/diagnosis/schema.js.map +1 -0
  44. package/lib/types/diagnosis/seed-source.d.ts +20 -0
  45. package/lib/types/diagnosis/seed-source.d.ts.map +1 -0
  46. package/lib/types/diagnosis/seed-source.js +96 -0
  47. package/lib/types/diagnosis/seed-source.js.map +1 -0
  48. package/lib/types/diagnosis/seed.d.ts +50 -0
  49. package/lib/types/diagnosis/seed.d.ts.map +1 -0
  50. package/lib/types/diagnosis/seed.js +242 -0
  51. package/lib/types/diagnosis/seed.js.map +1 -0
  52. package/lib/types/diagnosis/store.d.ts +81 -0
  53. package/lib/types/diagnosis/store.d.ts.map +1 -0
  54. package/lib/types/diagnosis/store.js +409 -0
  55. package/lib/types/diagnosis/store.js.map +1 -0
  56. package/lib/types/diagnosis/types.d.ts +124 -0
  57. package/lib/types/diagnosis/types.d.ts.map +1 -0
  58. package/lib/types/diagnosis/types.js +13 -0
  59. package/lib/types/diagnosis/types.js.map +1 -0
  60. package/lib/types/directory-picker.d.ts +94 -0
  61. package/lib/types/directory-picker.d.ts.map +1 -0
  62. package/lib/types/directory-picker.js +110 -0
  63. package/lib/types/directory-picker.js.map +1 -0
  64. package/lib/types/extract-model.d.ts +76 -0
  65. package/lib/types/extract-model.d.ts.map +1 -0
  66. package/lib/types/extract-model.js +193 -0
  67. package/lib/types/extract-model.js.map +1 -0
  68. package/lib/types/extract.d.ts +121 -0
  69. package/lib/types/extract.d.ts.map +1 -0
  70. package/lib/types/extract.js +549 -0
  71. package/lib/types/extract.js.map +1 -0
  72. package/lib/types/fts.d.ts +96 -0
  73. package/lib/types/fts.d.ts.map +1 -0
  74. package/lib/types/fts.js +184 -0
  75. package/lib/types/fts.js.map +1 -0
  76. package/lib/types/index.d.ts +39 -0
  77. package/lib/types/index.d.ts.map +1 -0
  78. package/lib/types/index.js +1112 -0
  79. package/lib/types/index.js.map +1 -0
  80. package/lib/types/intent/engine.d.ts +108 -0
  81. package/lib/types/intent/engine.d.ts.map +1 -0
  82. package/lib/types/intent/engine.js +501 -0
  83. package/lib/types/intent/engine.js.map +1 -0
  84. package/lib/types/intent/host.d.ts +88 -0
  85. package/lib/types/intent/host.d.ts.map +1 -0
  86. package/lib/types/intent/host.js +389 -0
  87. package/lib/types/intent/host.js.map +1 -0
  88. package/lib/types/intent/preset-tools.d.ts +37 -0
  89. package/lib/types/intent/preset-tools.d.ts.map +1 -0
  90. package/lib/types/intent/preset-tools.js +45 -0
  91. package/lib/types/intent/preset-tools.js.map +1 -0
  92. package/lib/types/intent/settings-schema.d.ts +59 -0
  93. package/lib/types/intent/settings-schema.d.ts.map +1 -0
  94. package/lib/types/intent/settings-schema.js +57 -0
  95. package/lib/types/intent/settings-schema.js.map +1 -0
  96. package/lib/types/intent/store.d.ts +75 -0
  97. package/lib/types/intent/store.d.ts.map +1 -0
  98. package/lib/types/intent/store.js +139 -0
  99. package/lib/types/intent/store.js.map +1 -0
  100. package/lib/types/intent/tools.d.ts +63 -0
  101. package/lib/types/intent/tools.d.ts.map +1 -0
  102. package/lib/types/intent/tools.js +108 -0
  103. package/lib/types/intent/tools.js.map +1 -0
  104. package/lib/types/intent/types.d.ts +189 -0
  105. package/lib/types/intent/types.d.ts.map +1 -0
  106. package/lib/types/intent/types.js +62 -0
  107. package/lib/types/intent/types.js.map +1 -0
  108. package/lib/types/knowledge-bases.d.ts +266 -0
  109. package/lib/types/knowledge-bases.d.ts.map +1 -0
  110. package/lib/types/knowledge-bases.js +1107 -0
  111. package/lib/types/knowledge-bases.js.map +1 -0
  112. package/lib/types/layout.d.ts +262 -0
  113. package/lib/types/layout.d.ts.map +1 -0
  114. package/lib/types/layout.js +312 -0
  115. package/lib/types/layout.js.map +1 -0
  116. package/lib/types/migrate.d.ts +110 -0
  117. package/lib/types/migrate.d.ts.map +1 -0
  118. package/lib/types/migrate.js +280 -0
  119. package/lib/types/migrate.js.map +1 -0
  120. package/lib/types/recall-rank.d.ts +158 -0
  121. package/lib/types/recall-rank.d.ts.map +1 -0
  122. package/lib/types/recall-rank.js +261 -0
  123. package/lib/types/recall-rank.js.map +1 -0
  124. package/lib/types/runtime.d.ts +568 -0
  125. package/lib/types/runtime.d.ts.map +1 -0
  126. package/lib/types/runtime.js +3045 -0
  127. package/lib/types/runtime.js.map +1 -0
  128. package/lib/types/settings-schema.d.ts +117 -0
  129. package/lib/types/settings-schema.d.ts.map +1 -0
  130. package/lib/types/settings-schema.js +92 -0
  131. package/lib/types/settings-schema.js.map +1 -0
  132. package/lib/types/settings-volatile.d.ts +102 -0
  133. package/lib/types/settings-volatile.d.ts.map +1 -0
  134. package/lib/types/settings-volatile.js +114 -0
  135. package/lib/types/settings-volatile.js.map +1 -0
  136. package/lib/types/types.d.ts +799 -0
  137. package/lib/types/types.d.ts.map +1 -0
  138. package/lib/types/types.js +387 -0
  139. package/lib/types/types.js.map +1 -0
  140. package/lib/types/wiki-types.d.ts +68 -0
  141. package/lib/types/wiki-types.d.ts.map +1 -0
  142. package/lib/types/wiki-types.js +322 -0
  143. package/lib/types/wiki-types.js.map +1 -0
  144. package/package.json +101 -0
  145. package/preset/tita-data-agent/agent.cordis.yml +376 -0
  146. package/preset/tita-data-agent/preset.yml +3 -0
  147. package/python/requirements.txt +8 -0
  148. package/scripts/doctor.mjs +70 -0
  149. package/scripts/e2e-pipeline.mjs +355 -0
  150. package/scripts/install-agent-preset.mjs +92 -0
  151. package/scripts/setup-embedding.sh +34 -0
  152. package/scripts/sync-agent-preset.mjs +188 -0
  153. package/src/client/AgentPresetCard.tsx +247 -0
  154. package/src/client/DirectoryPicker.tsx +357 -0
  155. package/src/client/InfoHint.tsx +65 -0
  156. package/src/client/IntentLogicTab.tsx +185 -0
  157. package/src/client/IntentTab.tsx +715 -0
  158. package/src/client/KnowledgeBaseTab.tsx +1454 -0
  159. package/src/client/KnowledgeGraphView.tsx +306 -0
  160. package/src/client/KnowledgePanel.tsx +381 -0
  161. package/src/client/KnowledgeStepRail.tsx +60 -0
  162. package/src/client/NodeSettings.tsx +392 -0
  163. package/src/client/PreviewSections.tsx +116 -0
  164. package/src/client/RecallPanel.tsx +399 -0
  165. package/src/client/TypeDefinitionDrawer.tsx +215 -0
  166. package/src/client/WikiFlowCard.tsx +226 -0
  167. package/src/client/WikiTypeEditor.tsx +332 -0
  168. package/src/client/agent-preset.ts +121 -0
  169. package/src/client/arch/AgentArchitectureGraph.tsx +219 -0
  170. package/src/client/arch/NodeLogicPanel.tsx +133 -0
  171. package/src/client/arch/ProblemDetailPanel.tsx +88 -0
  172. package/src/client/arch/model.ts +691 -0
  173. package/src/client/architecture.css +191 -0
  174. package/src/client/architecture.tsx +530 -0
  175. package/src/client/attribution/AttributionGraph.tsx +104 -0
  176. package/src/client/attribution/AttributionTab.tsx +332 -0
  177. package/src/client/attribution/CaseView.tsx +102 -0
  178. package/src/client/attribution/GraphStage.tsx +87 -0
  179. package/src/client/attribution/RunView.tsx +184 -0
  180. package/src/client/attribution/StageChrome.tsx +403 -0
  181. package/src/client/attribution/ViewSwitch.tsx +107 -0
  182. package/src/client/attribution/graph-options.ts +381 -0
  183. package/src/client/attribution/model.ts +462 -0
  184. package/src/client/config-scope.ts +119 -0
  185. package/src/client/convert/ConvertPanel.tsx +594 -0
  186. package/src/client/convert/styles.css +138 -0
  187. package/src/client/convert/styles.ts +49 -0
  188. package/src/client/format.ts +39 -0
  189. package/src/client/graph/layout.ts +181 -0
  190. package/src/client/graph.css +113 -0
  191. package/src/client/index.ts +98 -0
  192. package/src/client/problem-domains.ts +114 -0
  193. package/src/client/styles.css +940 -0
  194. package/src/client/styles.ts +11 -0
  195. package/src/client/useFitScale.ts +68 -0
  196. package/src/config-schema.ts +45 -0
  197. package/src/convert/core-loader.ts +68 -0
  198. package/src/convert/host.ts +579 -0
  199. package/src/convert/ingest.ts +367 -0
  200. package/src/convert/settings-schema.ts +27 -0
  201. package/src/convert/types.ts +65 -0
  202. package/src/diagnosis/cards.ts +498 -0
  203. package/src/diagnosis/problem-classes.ts +117 -0
  204. package/src/diagnosis/schema.ts +260 -0
  205. package/src/diagnosis/seed-source.ts +107 -0
  206. package/src/diagnosis/seed.ts +291 -0
  207. package/src/diagnosis/store.ts +432 -0
  208. package/src/diagnosis/types.ts +152 -0
  209. package/src/directory-picker.ts +164 -0
  210. package/src/extract-model.ts +227 -0
  211. package/src/extract.ts +632 -0
  212. package/src/fts.ts +203 -0
  213. package/src/index.ts +1091 -0
  214. package/src/intent/README.md +63 -0
  215. package/src/intent/engine.ts +527 -0
  216. package/src/intent/host.ts +462 -0
  217. package/src/intent/preset-tools.ts +48 -0
  218. package/src/intent/settings-schema.ts +63 -0
  219. package/src/intent/store.ts +143 -0
  220. package/src/intent/tools.ts +139 -0
  221. package/src/intent/types.ts +245 -0
  222. package/src/knowledge-bases.ts +1160 -0
  223. package/src/layout.ts +350 -0
  224. package/src/migrate.ts +262 -0
  225. package/src/python/embedding_worker.py +471 -0
  226. package/src/recall-rank.ts +299 -0
  227. package/src/runtime.ts +3069 -0
  228. package/src/settings-schema.ts +94 -0
  229. package/src/settings-volatile.ts +122 -0
  230. package/src/types.ts +1140 -0
  231. package/src/wiki-types.ts +357 -0
@@ -0,0 +1,462 @@
1
+ import { ZOMBIE_EDGE_DAYS } from '../../diagnosis/types.js'
2
+ import {
3
+ PROBLEM_CLASSES, PROBLEM_CLASS_LABEL, problemClassIds, problemSubclassOf,
4
+ } from '../../diagnosis/problem-classes.js'
5
+ import type { ProblemClassId } from '../../diagnosis/problem-classes.js'
6
+ import type {
7
+ AttributionEdge, AttributionEdgeKind, AttributionGraphData, AttributionNode, AttributionNodeKind,
8
+ AttributionStats, FailureMode, NodeLayer,
9
+ } from '../../diagnosis/types.js'
10
+
11
+ export { ZOMBIE_EDGE_DAYS, PROBLEM_CLASSES, PROBLEM_CLASS_LABEL, problemClassIds, problemSubclassOf }
12
+ export type {
13
+ AttributionEdge, AttributionGraphData, AttributionNode, AttributionNodeKind,
14
+ AttributionEdgeKind, AttributionStats, FailureMode, NodeLayer,
15
+ } from '../../diagnosis/types.js'
16
+ export type { ProblemClassId, ProblemSubclassSpec } from '../../diagnosis/problem-classes.js'
17
+
18
+ /**
19
+ * 智能归因图谱的**展示层计算**:调色、尺寸映射、空区与僵尸边判定、筛选、排序。
20
+ *
21
+ * 领域模型(节点 / 边 / 图的形状)已经搬到 `src/diagnosis/types.ts` —— 宿主也要读写
22
+ * 同一套结构,视图只是它的消费者。这里留下的是「只有画布才关心」的部分。
23
+ */
24
+
25
+ /** 层 → 视觉基色(对应 styles.css 的 `--tita-*` 令牌)。 */
26
+ export const LAYER_TONES: Record<NodeLayer, 'blue' | 'violet' | 'amber'> = {
27
+ type: 'violet',
28
+ reference: 'blue',
29
+ instance: 'amber',
30
+ }
31
+
32
+ /**
33
+ * 失败模式 → 颜色。分类在图谱视图里是**着色**而不是分区,所以它只影响一眼扫过的
34
+ * 分组感,不影响节点位置 —— 位置由布局按边的方向决定。
35
+ */
36
+ export const FAILURE_MODE_STYLE: Record<FailureMode, { label: string; fill: string; stroke: string; text: string }> = {
37
+ 'upstream-missing': { label: '上游未产出', fill: '#eef4ff', stroke: '#2563eb', text: '#1d4ed8' },
38
+ 'spec-change': { label: '口径变更', fill: '#f3f0ff', stroke: '#6b54cf', text: '#5b45be' },
39
+ 'data-processing': { label: '数据处理', fill: '#eafaf2', stroke: '#12875f', text: '#0f6b4d' },
40
+ consumption: { label: '消费与展示', fill: '#fff7ea', stroke: '#b96d14', text: '#96580f' },
41
+ permission: { label: '权限与过滤', fill: '#fdecea', stroke: '#b42318', text: '#98200f' },
42
+ }
43
+
44
+ /**
45
+ * 界面里的固定顺序:与 §4.5 的判断顺序一致,不随数据出现顺序抖动。
46
+ *
47
+ * **问题大类刻意不给配色。** 它和失败模式是两根轴,而画布上的颜色通道只有一条 ——
48
+ * 失败模式已经占用了它(假设节点的填充色)。再给问题大类一套颜色,会出现两件事:
49
+ * 同一张图上「蓝」既可能指「偏差」又可能指「上游未产出」;而现象节点本来是琥珀色胶囊,
50
+ * 再染一层色就没人认得出它是现象了。
51
+ *
52
+ * 所以问题大类走**文字通道**:现象节点下方挂一个「缺失 · 全量为 0」的徽标,筛选与图例
53
+ * 用中性 chip,靠标签区分。这也是「怎么区分哪些是问题」的正解 —— 问题侧不是另一种颜色,
54
+ * 它是**另一个维度**,在图例里必须与原因侧分开成组。
55
+ */
56
+ export const PROBLEM_CLASS_ORDER = problemClassIds()
57
+
58
+ /** 现象节点上的分类徽标文案:「缺失 · 全量为 0」。没分类就返回 undefined。 */
59
+ export function problemBadgeOf(node: AttributionNode): string | undefined {
60
+ if (!node.problemClass) return undefined
61
+ const subclass = problemSubclassOf(node.problemSubclass)?.label
62
+ const major = PROBLEM_CLASS_LABEL[node.problemClass]
63
+ return subclass ? `${major} · ${subclass}` : major
64
+ }
65
+
66
+ /** 每个大类下各有几个现象 —— 筛选 chip 上直接标出来,避免点出一个空图。 */
67
+ export function symptomCountByProblemClass(nodes: AttributionNode[]): Record<ProblemClassId, number> {
68
+ const counts = Object.fromEntries(PROBLEM_CLASS_ORDER.map(id => [id, 0])) as Record<ProblemClassId, number>
69
+ for (const node of nodes) {
70
+ if (node.kind === 'symptom' && node.problemClass) counts[node.problemClass] += 1
71
+ }
72
+ return counts
73
+ }
74
+
75
+ /** 无失败模式的节点(现象、资产、检查)用中性色。 */
76
+ export const NEUTRAL_STYLE = { fill: '#ffffff', stroke: '#d6dce6', text: '#748091' }
77
+
78
+ export const NODE_KIND_LABEL: Record<AttributionNodeKind, string> = {
79
+ symptom: '现象',
80
+ hypothesis: '假设',
81
+ check: '检查',
82
+ 'failure-mode': '失败模式',
83
+ asset: '资产',
84
+ case: '案例',
85
+ evidence: '证据',
86
+ cause: '根因',
87
+ unknown: '未归类',
88
+ }
89
+
90
+ /**
91
+ * 会出现在「类型」筛选里的节点类型。
92
+ *
93
+ * 只列**会被筛**的类型:证据 / 根因 / 案例是实例层的附属,默认就折叠着,
94
+ * 把它们列进筛选器只会让用户点出一个空图。这份名单是模型事实(哪些类型值得筛),
95
+ * 不是界面偏好,所以放在模型层 —— 界面层两个组件(工具条、筛选条)都要用它。
96
+ */
97
+ export const FILTERABLE_KINDS: AttributionNodeKind[] = ['symptom', 'hypothesis', 'check', 'asset', 'failure-mode']
98
+
99
+ /**
100
+ * 节点尺寸:假设节点按复用次数放大,其余按类型给固定宽高。
101
+ *
102
+ * 用面积感而不是线性放大 —— 复用次数差 10 倍时,线性尺寸会让最大的节点把
103
+ * 版面撑爆。这里取 sqrt 并夹在 [46, 96]。
104
+ *
105
+ * 返回 `[宽, 高]`:假设是圆(宽高相等),其余是胶囊,宽度要容得下标题。
106
+ */
107
+ export function nodeBoxOf(node: AttributionNode): [number, number] {
108
+ if (node.kind === 'hypothesis') {
109
+ const reuse = Math.max(0, node.reuseCount ?? 0)
110
+ const size = Math.round(Math.min(96, Math.max(46, 24 + Math.sqrt(reuse) * 20)))
111
+ return [size, size]
112
+ }
113
+ if (node.kind === 'symptom') return [150, 44]
114
+ if (node.kind === 'check') return [156, 38]
115
+ if (node.kind === 'asset') return [132, 36]
116
+ if (node.kind === 'failure-mode') return [104, 30]
117
+ if (node.kind === 'case') return [184, 38]
118
+ return [150, 36]
119
+ }
120
+
121
+ /** 只有假设画成圆:圆没有方向感,正适合「一个机制候选」这种无内部结构的节点。 */
122
+ export function nodeShapeOf(node: AttributionNode): 'circle' | 'rect' {
123
+ return node.kind === 'hypothesis' ? 'circle' : 'rect'
124
+ }
125
+
126
+ /** 节点的取色:假设跟着失败模式走,其余走层色。 */
127
+ export function nodeStyleOf(node: AttributionNode): { fill: string; stroke: string; text: string } {
128
+ if (node.failureMode) return FAILURE_MODE_STYLE[node.failureMode]
129
+ if (node.layer === 'reference') return { fill: '#eef4ff', stroke: '#9ec2ff', text: '#1d4ed8' }
130
+ if (node.kind === 'symptom') return { fill: '#fff7ea', stroke: '#b96d14', text: '#96580f' }
131
+ if (node.kind === 'check') return { fill: '#f5f1ff', stroke: '#c9b8ff', text: '#5b45be' }
132
+ return NEUTRAL_STYLE
133
+ }
134
+
135
+ /**
136
+ * 空区判定:一个假设如果没有任何 `verified_by` 出去的检查,它就是空区。
137
+ *
138
+ * 这是图谱视图存在的首要理由 —— 排查视图看不到这个,它只在「我要完善这张图」
139
+ * 的时候才可见。
140
+ */
141
+ export function gapHypothesisIds(data: AttributionGraphData): string[] {
142
+ const hasCheck = new Set(
143
+ data.edges.filter(edge => edge.kind === 'verified_by').map(edge => edge.source),
144
+ )
145
+ return data.nodes
146
+ .filter(node => node.kind === 'hypothesis' && !hasCheck.has(node.id))
147
+ .map(node => node.id)
148
+ }
149
+
150
+ /** 僵尸边:90 天未走过的可能性边。它就该被剪掉,但剪之前先让人看见。 */
151
+ export function zombieEdgeIds(data: AttributionGraphData): string[] {
152
+ return data.edges.filter(isZombieEdge).map(edge => edge.id)
153
+ }
154
+
155
+ export function isZombieEdge(edge: AttributionEdge): boolean {
156
+ return edge.kind === 'may_be_caused_by' && (edge.staleDays ?? 0) > ZOMBIE_EDGE_DAYS
157
+ }
158
+
159
+ export function summarize(data: AttributionGraphData): AttributionStats {
160
+ const byLayer = (layer: NodeLayer): number => data.nodes.filter(node => node.layer === layer).length
161
+ const reuseTotal = data.nodes.reduce((sum, node) => sum + (node.reuseCount ?? 0), 0)
162
+ return {
163
+ typeNodes: byLayer('type'),
164
+ referenceNodes: byLayer('reference'),
165
+ instanceNodes: byLayer('instance'),
166
+ reuseTotal,
167
+ symptoms: data.nodes.filter(node => node.kind === 'symptom').length,
168
+ problemClasses: new Set(
169
+ data.nodes.filter(node => node.kind === 'symptom' && node.problemClass).map(node => node.problemClass),
170
+ ).size,
171
+ gaps: gapHypothesisIds(data).length,
172
+ zombies: zombieEdgeIds(data).length,
173
+ }
174
+ }
175
+
176
+ /** 默认只渲染类型层 + 引用层:实例层是每次排查线性增长的,展开它会淹掉版面。 */
177
+ export function visibleNodes(data: AttributionGraphData, includeInstance = false): AttributionNode[] {
178
+ const idSet = new Set(data.nodes.map(node => node.id))
179
+ const kept = includeInstance ? data.nodes : data.nodes.filter(node => node.layer !== 'instance')
180
+ // 只保留两端都在可见集里的边,否则 G6 会因为找不到 source/target 而整条图报错。
181
+ return kept.filter(node => idSet.has(node.id))
182
+ }
183
+
184
+ export function visibleEdges(data: AttributionGraphData, nodes: AttributionNode[]): AttributionEdge[] {
185
+ const ids = new Set(nodes.map(node => node.id))
186
+ return data.edges.filter(edge => ids.has(edge.source) && ids.has(edge.target))
187
+ }
188
+
189
+ /**
190
+ * 以某个节点为中心取 N 跳邻域。
191
+ *
192
+ * 存在的理由是**尺寸**:承载这个面板的右侧边栏实测只有 380px 宽(宿主
193
+ * `dsh-client-ui-sidebar-right` 的 `width:380px`)。整张图缩进 380px 之后节点
194
+ * 只剩十几像素,标签必然不可读。所以面板里默认走邻域视图 —— 五到八个节点可以按
195
+ * 接近 1:1 的缩放渲染,而「全图」只用来回答形状问题(哪块密、哪里空)。
196
+ *
197
+ * 双向遍历:入边和出边都算邻居。只走出边会把「谁指向我」丢掉,而资产反向索引
198
+ * 恰恰是入边。
199
+ */
200
+ export function neighborhoodOf(data: AttributionGraphData, centerId: string, hops = 1): AttributionGraphData {
201
+ return scopeOf(data, [centerId], hops)
202
+ }
203
+
204
+ /**
205
+ * 从一组种子节点出发、`hops` 跳内可达的**诱导子图**。
206
+ *
207
+ * 与 `neighborhoodOf` 是同一个算法(后者就是单种子的特例),抽出来是因为问题大类的
208
+ * 筛选需要「一组现象一起扩」——同一段逐跳 BFS 写两遍,迟早会在其中一处忘记
209
+ * 「新节点留给下一轮」这条规则。
210
+ */
211
+ export function scopeOf(data: AttributionGraphData, seedIds: Iterable<string>, hops: number): AttributionGraphData {
212
+ const keep = new Set<string>(seedIds)
213
+ let frontier = new Set<string>(keep)
214
+ for (let step = 0; step < hops; step += 1) {
215
+ const next = new Set<string>()
216
+ for (const edge of data.edges) {
217
+ if (frontier.has(edge.source) && !keep.has(edge.target)) next.add(edge.target)
218
+ if (frontier.has(edge.target) && !keep.has(edge.source)) next.add(edge.source)
219
+ }
220
+ for (const id of next) keep.add(id)
221
+ // 必须把这一轮的新节点**留给下一轮**,而不是在同一轮里继续扩散 ——
222
+ // 否则「经过 S1 再走到 H2」会被算作 1 跳,H1 的邻域会一步吃下整张图。
223
+ frontier = next
224
+ }
225
+ return {
226
+ nodes: data.nodes.filter(node => keep.has(node.id)),
227
+ edges: data.edges.filter(edge => keep.has(edge.source) && keep.has(edge.target)),
228
+ }
229
+ }
230
+
231
+ /** 度数:邻域视图里用来把「连接最多的那个」选成默认焦点。 */
232
+ export function degreeOf(data: AttributionGraphData, id: string): number {
233
+ return data.edges.reduce((sum, edge) => sum + (edge.source === id || edge.target === id ? 1 : 0), 0)
234
+ }
235
+
236
+ /** 默认焦点:类型层里度数最高的假设。没有假设时退化为第一个节点。 */
237
+ export function defaultFocusId(data: AttributionGraphData): string | undefined {
238
+ const hypotheses = data.nodes.filter(node => node.kind === 'hypothesis')
239
+ if (hypotheses.length === 0) return data.nodes[0]?.id
240
+ return hypotheses
241
+ .slice()
242
+ .sort((a, b) => degreeOf(data, b.id) - degreeOf(data, a.id) || (b.reuseCount ?? 0) - (a.reuseCount ?? 0))[0]!.id
243
+ }
244
+
245
+ /** 排查视图里的一个候选假设。 */
246
+ export interface AttributionCandidate {
247
+ hypothesis: AttributionNode
248
+ /** 同现象下的根因占比。分母由 `SAMPLE_SIZE` 说明,不写死在界面上。 */
249
+ prior: number
250
+ /** 该假设下成本最低检查的耗时;没有检查时为 undefined(= 走不下去)。 */
251
+ costMinutes?: number
252
+ /** 优先分 = 根因占比(百分点)÷ 检查成本。没有检查时记为 0。 */
253
+ score: number
254
+ checks: AttributionNode[]
255
+ gap: boolean
256
+ }
257
+
258
+ /**
259
+ * 近 30 天的同现象排查样本量,用作根因占比的分母。
260
+ *
261
+ * 与 0021 §4.4 的示例表一致(12 / 6 / 4 / 3 / 1 / 5 / 2,合计 33 次已归类 + 8 次未归类)。
262
+ */
263
+ export const SAMPLE_SIZE = 41
264
+
265
+ /**
266
+ * 从一个现象节点出发算候选假设集,并按优先分排序。
267
+ *
268
+ * 这是**排查视图和排序公式的唯一实现**:界面只负责把结果画出来,不再各自算一遍。
269
+ * `prior` 这里先用「全量复用次数 ÷ 样本量」充当 —— 等 M1 的写回链路落地后,
270
+ * 换成本次召回集内的占比并叠加 α 平滑(0021 §4.4),公式位置不变。
271
+ */
272
+ export function candidatesOf(data: AttributionGraphData, symptomId: string): AttributionCandidate[] {
273
+ const checksByHypothesis = new Map<string, AttributionNode[]>()
274
+ for (const edge of data.edges) {
275
+ if (edge.kind !== 'verified_by') continue
276
+ const list = checksByHypothesis.get(edge.source) ?? []
277
+ const check = data.nodes.find(node => node.id === edge.target)
278
+ if (check) list.push(check)
279
+ checksByHypothesis.set(edge.source, list)
280
+ }
281
+
282
+ const hypothesisIds = new Set(
283
+ data.edges.filter(edge => edge.kind === 'may_be_caused_by' && edge.source === symptomId).map(edge => edge.target),
284
+ )
285
+
286
+ return data.nodes
287
+ .filter(node => hypothesisIds.has(node.id))
288
+ .map(hypothesis => {
289
+ const checks = (checksByHypothesis.get(hypothesis.id) ?? [])
290
+ .slice()
291
+ .sort((a, b) => (a.costMinutes ?? 0) - (b.costMinutes ?? 0))
292
+ const costMinutes = checks[0]?.costMinutes
293
+ const prior = (hypothesis.reuseCount ?? 0) / SAMPLE_SIZE
294
+ return {
295
+ hypothesis,
296
+ prior,
297
+ ...(costMinutes === undefined ? {} : { costMinutes }),
298
+ // ×100 让分数与 0021 §4.4 的示例表同刻度(占比按百分点算),界面上
299
+ // 显示的就是「29 分」而不是「0.29 分」。
300
+ score: costMinutes === undefined || costMinutes === 0 ? 0 : (prior * 100) / costMinutes,
301
+ checks,
302
+ gap: checks.length === 0,
303
+ }
304
+ })
305
+ // 先比分数,并列再比占比(0021 §4.4 的 tie-break)。
306
+ // 必须带容差:6/41/3 与 4/41/2 在数学上相等,在浮点里差 1 ULP ——
307
+ // 不加容差,「占比高者胜出」这条规则会在这种恰好并列的情况下静默失效。
308
+ .sort((a, b) => {
309
+ const gap = b.score - a.score
310
+ return Math.abs(gap) > 1e-9 ? gap : b.prior - a.prior
311
+ })
312
+ }
313
+
314
+ /** 筛选条件。空集合 = 不按该维度过滤。 */
315
+ export interface GraphFilter {
316
+ query: string
317
+ kinds: ReadonlySet<AttributionNodeKind>
318
+ failureModes: ReadonlySet<FailureMode>
319
+ /**
320
+ * 问题大类。**它不是一条节点谓词,而是一个子图选择器。**
321
+ *
322
+ * 原因侧的节点(假设、检查、资产)身上根本没有问题大类,按「节点必须同时满足所有
323
+ * 激活条件」的通行做法去合取,选了任一问题大类之后整张图会被清空 —— 那是把
324
+ * 「分类」当成了「合法性检查」,而它其实是「范围」。所以它先按现象把范围收窄,
325
+ * 其余条件再在这个范围里生效(见 `filterGraph`)。
326
+ */
327
+ problemClasses: ReadonlySet<ProblemClassId>
328
+ /** 命中节点之外,是否把它们的 1 跳邻居也留下作为上下文。 */
329
+ includeNeighbors: boolean
330
+ }
331
+
332
+ export const EMPTY_FILTER: GraphFilter = {
333
+ query: '',
334
+ kinds: new Set(),
335
+ failureModes: new Set(),
336
+ problemClasses: new Set(),
337
+ includeNeighbors: true,
338
+ }
339
+
340
+ /**
341
+ * 按问题筛时要走几跳。
342
+ *
343
+ * **2 跳**,因为「现象 → 假设 → 检查」正好两跳:只走 1 跳会得到一个只有假设、没有检查的
344
+ * 半张图,而检查恰好是这张图唯一能动手的地方 —— 筛完却看不到该做什么,等于没筛。
345
+ */
346
+ export const PROBLEM_SCOPE_HOPS = 2
347
+
348
+ /**
349
+ * 按问题扩范围时**允许走哪几种边、朝哪个方向**。
350
+ *
351
+ * 这里不能用 `neighborhoodOf` 那种双向扩散。双向 2 跳从「日报 GMV 为 0」出发会经由共享的
352
+ * 假设走回**另外两个现象**,再各自铺开 —— 结果是「按问题筛」返回了 32/34 个节点,
353
+ * 等于什么都没筛。原因不在跳数,在方向:双向扩散回答的是「谁和我相连」,
354
+ * 而按问题筛要回答的是**「这个问题能顺着哪些假设查到什么」**。
355
+ *
356
+ * 所以这里逐边显式定义方向:产出性关系朝前走(现象→假设→检查 / 失败模式),
357
+ * 归属与索引关系朝后找(由假设找回它的资产,由现象找回属于它的案例)。
358
+ */
359
+ export const PROBLEM_SCOPE_EDGES: Partial<Record<AttributionEdgeKind, 'forward' | 'backward'>> = {
360
+ may_be_caused_by: 'forward',
361
+ verified_by: 'forward',
362
+ categorized_as: 'forward',
363
+ indexed_from: 'backward',
364
+ instance_of: 'backward',
365
+ }
366
+
367
+ /**
368
+ * 一组现象能「查得到」的子图:它的假设、那些假设的检查与失败模式、背后的资产、
369
+ * 以及属于这些现象的案例。**不包含相邻现象** —— 那是 `neighborhoodOf` 的语义。
370
+ */
371
+ export function problemScopeOf(data: AttributionGraphData, symptomIds: Iterable<string>): AttributionGraphData {
372
+ const keep = new Set<string>(symptomIds)
373
+ let frontier = new Set<string>(keep)
374
+ for (let step = 0; step < PROBLEM_SCOPE_HOPS; step += 1) {
375
+ const next = new Set<string>()
376
+ for (const edge of data.edges) {
377
+ const direction = PROBLEM_SCOPE_EDGES[edge.kind]
378
+ if (direction === undefined) continue
379
+ const from = direction === 'forward' ? edge.source : edge.target
380
+ const to = direction === 'forward' ? edge.target : edge.source
381
+ if (frontier.has(from) && !keep.has(to)) next.add(to)
382
+ }
383
+ for (const id of next) keep.add(id)
384
+ frontier = next
385
+ }
386
+ return {
387
+ nodes: data.nodes.filter(node => keep.has(node.id)),
388
+ edges: data.edges.filter(edge => keep.has(edge.source) && keep.has(edge.target)),
389
+ }
390
+ }
391
+
392
+ function matchesQuery(node: AttributionNode, query: string): boolean {
393
+ const text = `${node.id} ${node.title} ${node.summary ?? ''}`.toLocaleLowerCase()
394
+ return text.includes(query)
395
+ }
396
+
397
+ /**
398
+ * 图上的筛选。
399
+ *
400
+ * 只按「类型 / 失败模式 / 文本」三个维度过滤,**不新增一个「分类树」**:
401
+ * 0021 §4.3 已经定了分类是切面,所以这里也做成切面 —— 它可以多选、可以为空、
402
+ * 可以随时改回来看全图,而不像树的层级那样「选错就得往回退」。
403
+ *
404
+ * `includeNeighbors` 默认开:只留命中节点会让图散成孤点,边的语义也一起丢了。
405
+ * 留下来的是命中节点及其 1 跳邻居,边只在两端都留下时才保留。
406
+ */
407
+ export function filterGraph(data: AttributionGraphData, filter: GraphFilter): AttributionGraphData {
408
+ const query = filter.query.trim().toLocaleLowerCase()
409
+
410
+ // 第一步:问题大类先把**范围**收窄成「命中现象 2 跳以内」的子图。
411
+ // 顺序很重要 —— 先收范围再套文本/类型/失败模式,语义才是「在这类问题里找」,
412
+ // 而不是「在整张图里找同时满足所有条件的节点」(后者永远是空集)。
413
+ const inScope = filter.problemClasses.size === 0
414
+ ? data
415
+ : problemScopeOf(
416
+ data,
417
+ data.nodes
418
+ .filter(node => node.kind === 'symptom' && node.problemClass !== undefined && filter.problemClasses.has(node.problemClass))
419
+ .map(node => node.id),
420
+ )
421
+
422
+ const noFilter = query === '' && filter.kinds.size === 0 && filter.failureModes.size === 0
423
+ if (noFilter) return inScope
424
+
425
+ const hits = new Set<string>()
426
+ for (const node of inScope.nodes) {
427
+ if (query !== '' && !matchesQuery(node, query)) continue
428
+ if (filter.kinds.size > 0 && !filter.kinds.has(node.kind)) continue
429
+ if (filter.failureModes.size > 0) {
430
+ // 失败模式是假设的属性;失败模式节点本身也要能被选出来。
431
+ const isModeNode = node.kind === 'failure-mode'
432
+ if (!isModeNode && (node.failureMode === undefined || !filter.failureModes.has(node.failureMode))) continue
433
+ }
434
+ hits.add(node.id)
435
+ }
436
+
437
+ const keep = filter.includeNeighbors ? neighborhoodIds(inScope, hits) : hits
438
+ return {
439
+ nodes: inScope.nodes.filter(node => keep.has(node.id)),
440
+ edges: inScope.edges.filter(edge => keep.has(edge.source) && keep.has(edge.target)),
441
+ }
442
+ }
443
+
444
+ /** 命中集合 + 每命中节点各自的 1 跳邻居。 */
445
+ function neighborhoodIds(data: AttributionGraphData, seeds: ReadonlySet<string>): Set<string> {
446
+ const keep = new Set<string>(seeds)
447
+ for (const edge of data.edges) {
448
+ if (seeds.has(edge.source)) keep.add(edge.target)
449
+ if (seeds.has(edge.target)) keep.add(edge.source)
450
+ }
451
+ return keep
452
+ }
453
+
454
+ /** 筛选结果计数,用于把「筛掉了多少」显式告诉用户。 */
455
+ export function countByKind(nodes: AttributionNode[]): Array<{ kind: AttributionNodeKind; count: number }> {
456
+ const counts = new Map<AttributionNodeKind, number>()
457
+ for (const node of nodes) counts.set(node.kind, (counts.get(node.kind) ?? 0) + 1)
458
+ return [...counts.entries()]
459
+ .map(([kind, count]) => ({ kind, count }))
460
+ .sort((a, b) => b.count - a.count)
461
+ }
462
+
@@ -0,0 +1,119 @@
1
+ /**
2
+ * 0.1.7 设置适配层:把**一个 entry 的 config 表单**投影成若干
3
+ * `SettingsScopeLike` 作用域。
4
+ *
5
+ * ## 为什么需要这一层
6
+ *
7
+ * 0.1.7 删掉了 `ctx.settingsScope.bind({ namespace })` 这套客户端设置 API,换成
8
+ * `ctx.configForms.get(entryId)`:**设置不再是按命名空间分的独立文档,而是一个
9
+ * 插件 entry 自己的 config**。本插件把三块设置合并进同一个 entry(`knowledge` /
10
+ * `intent` / `convert` 三个组),但下层的编辑器(`NodeSettings`、`KnowledgeBaseTab`
11
+ * …)是按「一个平铺的设置组」写的,它们读 `snapshot.value.recallTopK`、
12
+ * 写 `scope.set('recallTopK', 20)`。
13
+ *
14
+ * 让每个编辑器都知道「我这一组嵌在 entry config 的哪个键下面」会把这个细节漏进
15
+ * 十几处 UI 代码。这一层只做一次投影:值是组内切片,写操作把组名补成**路径前缀**
16
+ * (`set('recallTopK', 20)` → `{ op: 'set', path: ['knowledge', 'recallTopK'] }`),
17
+ * 于是上面的组件一行都不用改。
18
+ *
19
+ * ## 为什么用 `mutate` 而不是 `set`
20
+ *
21
+ * `ConfigForm.set(field, …)` 的契约是「**命名空间 section 里的标量字段**」——只认
22
+ * 顶层的键。分组之后的字段都在第二层,所以一律走按路径寻址的 `mutate`。`group`
23
+ * 省略时(`agent-preset-registry` 那条 DSH 自己的 entry)路径就只有字段名本身,
24
+ * 与 `set` 等价。
25
+ *
26
+ * @module
27
+ */
28
+
29
+ import type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client'
30
+ import type { SettingsScopeLike, SettingsSnapshot } from './NodeSettings.tsx'
31
+
32
+ /** `mutate` 接受的那组路径操作。 */
33
+ type MutateOps = Parameters<ConfigForm<unknown>['mutate']>[0]
34
+
35
+ /** 读一层对象成员;不是容器就是没有。 */
36
+ function pick(container: unknown, key: string): unknown {
37
+ if (container === null || typeof container !== 'object') return undefined
38
+ return (container as Record<string, unknown>)[key]
39
+ }
40
+
41
+ /** 只保留有值的字段:`undefined` 与「字段不存在」在快照里不是一回事。 */
42
+ function defined(entries: Record<string, unknown>): Record<string, unknown> {
43
+ return Object.fromEntries(Object.entries(entries).filter(([, value]) => value !== undefined))
44
+ }
45
+
46
+ /**
47
+ * 「宿主没给表单」时的快照。
48
+ *
49
+ * 模块级常量,**不是每次现造** —— 理由与下面的缓存相同:新的对象引用会让
50
+ * `useSyncExternalStore` 永远认为状态变了。(`NodeSettings.tsx` 里那个
51
+ * `UNAVAILABLE_SNAPSHOT` 是同一个理由。)
52
+ */
53
+ const UNAVAILABLE: SettingsSnapshot = { status: 'unavailable', writable: false, mode: 'memory' }
54
+
55
+ /**
56
+ * 把一个 entry 的 config 表单投影成某一组的设置作用域。
57
+ *
58
+ * @param form - `ctx.configForms.get(entryId)`;undefined 表示宿主没提供(退化成
59
+ * `unavailable`,UI 显示「不可用」而不是崩)。
60
+ * @param group - entry config 里的组名。省略 = 整个 section。
61
+ */
62
+ export function groupScope(
63
+ form: ConfigForm<Record<string, unknown>> | undefined,
64
+ group?: string,
65
+ ): SettingsScopeLike {
66
+ /** 组内切片;无组时就是整个 section。 */
67
+ const slice = <T>(container: unknown): T | undefined =>
68
+ group === undefined ? (container as T | undefined) : (pick(container, group) as T | undefined)
69
+ /** 字段名 → entry config 里的完整路径。 */
70
+ const pathOf = (field: string): string[] => (group === undefined ? [field] : [group, field])
71
+
72
+ /**
73
+ * 派生快照的缓存,键是**底层快照的引用**。
74
+ *
75
+ * `getSnapshot()` **必须**在底层没变时返回同一个对象:读它的是
76
+ * `useSyncExternalStore`(`NodeSettings.tsx` 的 `useScope`),而它的契约是
77
+ * 「快照引用在变化之前保持稳定」。每次现造一个对象 = 每次调用都「检测到变化」=
78
+ * 无限重渲染 → **Minified React error #185(Maximum update depth exceeded)**。
79
+ *
80
+ * 这个故障的表现极具误导性:`SlotErrorBoundary` 会接住它,把整个 tab 体换成
81
+ * `<div data-slot-error="sidebar.right.pane.tab">` —— 面板整片空白,而控制台里
82
+ * **没有一条来自本插件的错误**(渲染器那行 `slot entry crashed in '…'` 在首次挂载
83
+ * 时就打完了)。第一版漏了这层缓存,代价就是一次「打开 TitaAgent 什么都看不到」。
84
+ */
85
+ let lastSource: unknown
86
+ let lastDerived: SettingsSnapshot | undefined
87
+
88
+ return {
89
+ getSnapshot(): SettingsSnapshot {
90
+ if (!form) return UNAVAILABLE
91
+ const snapshot = form.getSnapshot()
92
+ // `ConfigForm` 承诺底层快照在变化前引用稳定,所以引用相等就等价于「没变化」。
93
+ if (lastDerived !== undefined && snapshot === lastSource) return lastDerived
94
+
95
+ const value = slice<Record<string, unknown>>(snapshot.value)
96
+ const user = slice<unknown>(snapshot.user)
97
+ lastSource = snapshot
98
+ lastDerived = {
99
+ status: snapshot.status,
100
+ ...defined({ value, user, writable: snapshot.writable, mode: snapshot.mode, revision: snapshot.revision }),
101
+ } as SettingsSnapshot
102
+ return lastDerived
103
+ },
104
+ subscribe(listener: () => void): () => void {
105
+ return form ? form.subscribe(listener) : () => undefined
106
+ },
107
+ async set(field: string, value: unknown): Promise<void> {
108
+ if (!form) return
109
+ // `value` 在这一层是 `unknown`(组件按自己的控件类型传),而线上协议的
110
+ // `JsonValue` 是这一层的上游约束。两者在这里必须对上:这一层不校验,交给
111
+ // 宿主的 schema 与 `SettingsForms.mutate` 去拒绝非法值。
112
+ await form.mutate([{ op: 'set', path: pathOf(field), value } as unknown as MutateOps[number]])
113
+ },
114
+ async unset(field: string): Promise<void> {
115
+ if (!form) return
116
+ await form.mutate([{ op: 'unset', path: pathOf(field) }])
117
+ },
118
+ }
119
+ }