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,691 @@
1
+ /**
2
+ * Agent 架构图的**几何与图形映射**(纯函数,可测)。
3
+ *
4
+ * 为什么不是「交给布局算法」:这是一张**架构图**,不是关系图。节点位置是设计结论 ——
5
+ * 三条链路各占一行、五列网格对齐、跨链路的流转与反馈各有固定走线。把坐标交给
6
+ * d3-force / dagre,同一份数据每次刷新会长成另一个形状,「重排在 Runtime 下方」
7
+ * 这种空间记忆就失效了。因此这里显式算出坐标,G6 只负责画和交互。
8
+ *
9
+ * 0027 的三条结论,都写在这份几何里:
10
+ *
11
+ * 1. **三条带是同一张图的三个区域,不是三块独立的板。** 旧版每条带是一块 14px 圆角、
12
+ * 带浅色填充、彼此隔开 40px 的「卡片」,于是图上读出的是三个并列的方框;现在带与带
13
+ * 之间只隔一条发丝线,带名从左侧留白搬进各自区域的左上角。图因此是**一张**连续画布。
14
+ * 2. **左侧留白改作跨链路走线通道。** 旧版那 196px 装的是链路名牌子,占着地方却只重复
15
+ * 了图例;现在链路名回到带内,留白让给 A↔B↔C 的连线 —— 那正是这张图最该说清、
16
+ * 旧版最没说清的一件事。
17
+ * 3. **跨链路关系只用一个强调色,而且永远带文字。** 带内流向是灰的,跨链路是蓝的;
18
+ * 颜色回答「这条线是不是跨链路」,文字回答「它跨的是什么」。四个跨链路关系
19
+ * (A→C 交互回采、C→B 沉淀、C→A 反馈、B→A 注入)正好构成 A → C → B → A 的闭环,
20
+ * 而旧版缺的正是 A→C 那一段 —— 闭环因此看不出来。
21
+ */
22
+
23
+ import {
24
+ NODE_LANES, NODE_META, NODE_STATUS_LABEL, NODE_STATUS_SHORT, NODE_STATUS_TONE, memoryCountOf, nodeMetrics, sourceCountOf,
25
+ type KnowledgeStats, type LaneId, type LaneMeta, type NodeId, type NodeMetric, type NodeStatus,
26
+ } from '../architecture.tsx'
27
+ import { PROBLEM_BASE_NOTE, PROBLEM_DOMAINS, type ProblemDomain } from '../problem-domains.ts'
28
+ // 画布几何是从这份数据推出来的,所以这里把它转出去:测试与画布读的必须是同一份,
29
+ // 否则「三条牌子」这句话在两个地方各有一个真相。
30
+ export { PROBLEM_BASE_NOTE, PROBLEM_DOMAINS, problemDomainOf, type ProblemDomain } from '../problem-domains.ts'
31
+
32
+ // ----------------------------------------------------------------- 画布常量
33
+
34
+ /**
35
+ * 左侧留白:**跨链路走线的通道**,不是边距。带名已经搬进带内,所以这块宽度只由
36
+ * 「一条总线 + 转角」决定。带内的所有内容(含带名)都从这个 x 开始,通道因此从头到尾
37
+ * 没有东西挡着 —— 这是让 C→A 那条长反馈能贴着板边走干净的前提。
38
+ */
39
+ export const GUTTER = 116
40
+ export const NODE_W = 180
41
+ /** 卡片高度要装下四层:标题行、两行说明、统计行。 */
42
+ export const NODE_H = 96
43
+ /** 堆叠节点(Table/Notebook、情节/操作记忆)用略矮的规格。 */
44
+ export const NODE_H_STACKED = 80
45
+ export const COL_GAP = 32
46
+ /** 堆叠两个节点时,各自中心相对带中心线的偏移。80 + 2×47 = 174 = 内容区高度,
47
+ * 且 80 ≤ 2×47 —— 这是两张卡不重叠的条件,不是审美偏好。 */
48
+ export const STACK_OFFSET = 47
49
+ /** 带名条的高度:一枚字母徽标 + 链路名。 */
50
+ export const LANE_HEAD_H = 30
51
+ /**
52
+ * 带名条**只占自己内容的宽度**,不铺满整条带。
53
+ *
54
+ * 铺满的话它就是一块横贯全图的障碍物,垂直的跨链路走线只能绕到它的左边或右边 ——
55
+ * 实测「每日更新 → 编译」本来是一条直线,被逼成「上、左、上、右」四段绕行,
56
+ * 而且正好从带名文字上穿过去。缩成一块牌子之后,垂直走线从牌子右边自由通过。
57
+ */
58
+ export const LANE_HEAD_W = 224
59
+ /**
60
+ * 带与带之间的留白。它同时是横向跨链路走线的通道,所以不能太窄;
61
+ * 也不放任何分隔线 —— 分隔线要么是障碍物(挡住走线),要么只是一条装饰。
62
+ */
63
+ export const BAND_GAP = 24
64
+ export const LANE_PAD = 12
65
+ /** 带的内容区高度:按最高的那种排布(两个堆叠节点)定,单节点在其中居中。 */
66
+ export const CONTENT_H = NODE_H_STACKED + 2 * STACK_OFFSET
67
+ /** 带高 = 带名条 + 上下内边距 + 内容区。三条带等高,网格才对得齐。 */
68
+ export const BAND_H = LANE_HEAD_H + LANE_PAD + CONTENT_H + LANE_PAD
69
+ /** 两条带的上沿间距 = 带高 + 通道。 */
70
+ export const BAND_PITCH = BAND_H + BAND_GAP
71
+ /** 画布顶部留白:图例与操作浮层浮在这条空白上,同时它也是 B→A 注入的走线通道。 */
72
+ export const TOP = 100
73
+ export const BOARD_RIGHT_PAD = 28
74
+ export const COLUMNS = 5
75
+
76
+ export const CANVAS_WIDTH = GUTTER + COLUMNS * NODE_W + (COLUMNS - 1) * COL_GAP + BOARD_RIGHT_PAD
77
+ /** 底部留白同样是走线通道:跨链路反馈会从 C 带下面绕回来。 */
78
+ export const BOTTOM = 40
79
+
80
+ // ------------------------------------------------- 第四条带 · 解决的问题域
81
+ //
82
+ // 0027 把问题域从「画布外面的一张卡」搬进画布本身。理由是这一页的定位:它是用户
83
+ // 对整个 Agent 的认知入口,而「它解决什么问题」和「它由哪些节点组成」是同一个问题的
84
+ // 两面 —— 分成两块表面之后,用户要在两块之间来回看才能把机制和问题对上。
85
+ //
86
+ // 因此画布是**四条带**:A / B / C 三条链路,加上最下面一条问题域带。它不是第四条链路,
87
+ // 所以不参与带间等距的推导(`laneTop` 只算链路),但它和链路带共用同一套网格与边距。
88
+
89
+ /** 链路带与问题域带之间的走线通道。与 BAND_GAP 同宽:两条带之间的关系也是「隔一条通道」。 */
90
+ export const PROBLEM_GAP = BAND_GAP
91
+ /** 问题域带的带名条:与链路带名条同规格,读起来才是同一种东西。 */
92
+ export const PROBLEM_HEAD_H = LANE_HEAD_H
93
+ /**
94
+ * 问题域带名牌比链路名牌宽得多 —— 这是**刻意**的:它下面没有垂直走线要过,
95
+ * 而且它要一次说清两件事:这一带是什么,以及全图现在实现到哪一步。
96
+ */
97
+ export const PROBLEM_HEAD_W = 640
98
+ export const PROBLEM_PLATE_H = 182
99
+ export const PROBLEM_PLATE_GAP = 24
100
+ /** 带内边距:与链路带的 LANE_PAD 同值,四条带的内容因此对齐在同一条垂直线上。 */
101
+ export const PROBLEM_PAD = LANE_PAD
102
+ export const PROBLEM_BAND_H = PROBLEM_HEAD_H + PROBLEM_PAD + PROBLEM_PLATE_H + PROBLEM_PAD
103
+ /** 问题域带的上沿:紧接最后一条链路带,隔一条走线通道。 */
104
+ export const PROBLEM_TOP = laneTop(NODE_LANES.length - 1) + BAND_H + PROBLEM_GAP
105
+ export const CANVAS_HEIGHT = PROBLEM_TOP + PROBLEM_BAND_H + BOTTOM
106
+
107
+ /** 三条问题域牌子的宽度:把内容区三等分(除不尽的余数留在最右边,不摊进牌子)。 */
108
+ export const PROBLEM_PLATE_W = Math.floor((CANVAS_WIDTH - GUTTER - BOARD_RIGHT_PAD - (PROBLEM_DOMAINS.length - 1) * PROBLEM_PLATE_GAP) / PROBLEM_DOMAINS.length)
109
+
110
+ /** 第 `index` 条问题域牌子的格子。 */
111
+ export function problemPlateBox(index: number): ArchBox {
112
+ return {
113
+ x: GUTTER + index * (PROBLEM_PLATE_W + PROBLEM_PLATE_GAP) + PROBLEM_PLATE_W / 2,
114
+ y: PROBLEM_TOP + PROBLEM_HEAD_H + PROBLEM_PAD + PROBLEM_PLATE_H / 2,
115
+ width: PROBLEM_PLATE_W,
116
+ height: PROBLEM_PLATE_H,
117
+ }
118
+ }
119
+
120
+ /** 第 `col` 列节点的中心 x(0 起)。 */
121
+ export function columnX(col: number): number {
122
+ return GUTTER + col * (NODE_W + COL_GAP) + NODE_W / 2
123
+ }
124
+
125
+ /** 第 `index` 条带的上沿。带与带之间不加间隙:一条发丝线就是全部的分隔。 */
126
+ export function laneTop(index: number): number {
127
+ return TOP + index * BAND_PITCH
128
+ }
129
+ /** 带内容区的纵向中心:节点(单行或堆叠)都以它为基准。 */
130
+ export function laneCenter(index: number): number {
131
+ return laneTop(index) + LANE_HEAD_H + LANE_PAD + CONTENT_H / 2
132
+ }
133
+
134
+ export interface ArchBox { x: number; y: number; width: number; height: number }
135
+
136
+ /**
137
+ * 每个节点的格子。`lane` 是带序号(0/1/2),`col` 是列号(0..4)。
138
+ *
139
+ * 知识链路从左往右(写入侧 → 读取侧),自进化链路**从右往左**:交互从回答那一侧
140
+ * 出来,经过记忆与质量门,最后回到最左边的「资金化」—— 反向流不是排版花招,
141
+ * 它就是闭环的方向,也是 A→C 那条连线能画得短的原因。
142
+ */
143
+ interface Slot { lane: number; col: number; stack?: 'upper' | 'lower' }
144
+
145
+ export const ARCH_SLOTS: Record<NodeId, Slot> = {
146
+ conversation: { lane: 0, col: 0 },
147
+ intent: { lane: 0, col: 1 },
148
+ runtime: { lane: 0, col: 2 },
149
+ answer: { lane: 0, col: 3 },
150
+ 'data-sheet': { lane: 0, col: 4, stack: 'upper' },
151
+ 'data-notebook': { lane: 0, col: 4, stack: 'lower' },
152
+
153
+ 'source-raw': { lane: 1, col: 0 },
154
+ compile: { lane: 1, col: 1 },
155
+ build: { lane: 1, col: 2 },
156
+ recall: { lane: 1, col: 3 },
157
+ context: { lane: 1, col: 4 },
158
+
159
+ 'funded-value': { lane: 2, col: 0 },
160
+ 'daily-update': { lane: 2, col: 1 },
161
+ 'memory-quality': { lane: 2, col: 2 },
162
+ 'episodic-memory': { lane: 2, col: 3, stack: 'upper' },
163
+ 'procedural-memory': { lane: 2, col: 3, stack: 'lower' },
164
+ interaction: { lane: 2, col: 4 },
165
+ }
166
+
167
+ /**
168
+ * 左侧走线挡板:**一块不可见的矩形,只给路由器一个约束**。
169
+ *
170
+ * 跨链路反馈要贴着板的最左边走,而标签是以线为中心摆的 —— 走线在 x≈20 时,
171
+ * 「沉淀进知识库」的左半边直接被画布边界裁掉(实测只剩「沉淀进知识…」)。
172
+ * 用可见的 padding 解决不了:路由器照样贴边。所以这里放一块不画任何东西的挡板,
173
+ * 把走线推离边界,标签才有地方站。
174
+ */
175
+ export const ROUTE_WALL_W = 56
176
+
177
+ /** 图上的装饰节点 id 前缀:它们不可选中,点击处理要忽略掉。 */
178
+ export const DECORATION_PREFIX = 'deco:'
179
+ export function isDecoration(id: string): boolean {
180
+ return id.startsWith(DECORATION_PREFIX)
181
+ }
182
+
183
+ export function boxOf(id: NodeId): ArchBox {
184
+ const slot = ARCH_SLOTS[id]
185
+ const height = slot.stack ? NODE_H_STACKED : NODE_H
186
+ const center = laneCenter(slot.lane)
187
+ const y = slot.stack
188
+ ? center + (slot.stack === 'upper' ? -STACK_OFFSET : STACK_OFFSET)
189
+ : center
190
+ return { x: columnX(slot.col), y, width: NODE_W, height }
191
+ }
192
+
193
+ // ----------------------------------------------------------------- 连边
194
+
195
+ /**
196
+ * 边的语义。**两类**,不是五类:
197
+ * - `flow` / `branch`:带内流向。灰色,因为它回答的是「这一带里谁接谁」,看图就知道。
198
+ * - `inject` / `collect` / `feedback`:跨链路关系。蓝色,因为它回答的是「三条链路怎么
199
+ * 咬合」—— 这正是这张图存在的理由,所以它必须比带内流向更响,而且每条都带文字标签。
200
+ * `feedback` 用虚线:它是回程,不是主方向。
201
+ */
202
+ export type EdgeKind = 'flow' | 'branch' | 'inject' | 'collect' | 'feedback'
203
+
204
+ export interface ArchEdge {
205
+ id: string
206
+ source: NodeId
207
+ target: NodeId
208
+ kind: EdgeKind
209
+ label?: string
210
+ }
211
+
212
+ /**
213
+ * 整张图的边。**每一类语义各只出现一次**,不再有第二处定义。
214
+ *
215
+ * 带内边按链路的阅读顺序写;跨链路边写在最后,因为它们是这张图的结论,
216
+ * 画在最上层才不会被节点卡片压住。
217
+ */
218
+ export const ARCH_EDGES: ArchEdge[] = [
219
+ // A · 用户操作链路
220
+ { id: 'a1', source: 'conversation', target: 'intent', kind: 'flow' },
221
+ { id: 'a2', source: 'intent', target: 'runtime', kind: 'flow' },
222
+ { id: 'a3', source: 'runtime', target: 'answer', kind: 'flow' },
223
+ { id: 'a4', source: 'answer', target: 'data-sheet', kind: 'branch' },
224
+ { id: 'a5', source: 'answer', target: 'data-notebook', kind: 'branch' },
225
+
226
+ // B · 知识编译与召回链路(写入侧 → 读取侧)
227
+ { id: 'b1', source: 'source-raw', target: 'compile', kind: 'flow' },
228
+ { id: 'b2', source: 'compile', target: 'build', kind: 'flow' },
229
+ { id: 'b3', source: 'build', target: 'recall', kind: 'flow' },
230
+ { id: 'b4', source: 'recall', target: 'context', kind: 'flow' },
231
+
232
+ // C · 自进化(从右往左:交互 → 记忆 → 质量门 → 每日更新 → 资金化)
233
+ { id: 'c1', source: 'interaction', target: 'episodic-memory', kind: 'branch' },
234
+ { id: 'c2', source: 'interaction', target: 'procedural-memory', kind: 'branch' },
235
+ { id: 'c3', source: 'episodic-memory', target: 'memory-quality', kind: 'flow' },
236
+ { id: 'c4', source: 'procedural-memory', target: 'memory-quality', kind: 'flow' },
237
+ { id: 'c5', source: 'memory-quality', target: 'daily-update', kind: 'flow' },
238
+ { id: 'c6', source: 'daily-update', target: 'funded-value', kind: 'flow' },
239
+
240
+ // 跨链路:A → C → B → A,四段合起来才是闭环。
241
+ // 旧版缺 x2,于是图上只有「知识注入进来」和「记忆反馈回去」,看不出用户侧
242
+ // 的交互是怎么进到自进化里去的 —— 闭环断在起点。
243
+ { id: 'x1', source: 'answer', target: 'interaction', kind: 'collect', label: '交互回采' },
244
+ { id: 'x2', source: 'daily-update', target: 'compile', kind: 'feedback', label: '沉淀进知识库' },
245
+ { id: 'x3', source: 'memory-quality', target: 'intent', kind: 'feedback', label: '反馈到意图理解' },
246
+ { id: 'x4', source: 'context', target: 'runtime', kind: 'inject', label: 'Context Pack 注入' },
247
+ ]
248
+
249
+ /**
250
+ * 边的外观。灰 / 蓝两档,没有第三档色相。
251
+ *
252
+ * **这一组是画布侧的字面量,不是漏改。** 它们进的是 G6 的 canvas 绘制路径,而
253
+ * `@antv/g-lite` 的颜色解析器只认 CSS 色值语法:`var(--tita-ink)` 会被 `color()`
254
+ * 判为无效,退回 `rgba(0,0,0,0)` —— 也就是线会**消失**,不是变色。所以画布上的颜色
255
+ * 只能保留字面量;卡片(HTML 节点)那一侧照常走共享令牌。
256
+ *
257
+ * 灰线 `#7d8b9e` 在白底 3.5:1 —— 线不是文字,3:1 是它的门槛,旧版的 `#9db2cc`
258
+ * 只有 2.2:1,在投影和缩放之后基本消失。蓝线 `#175cd3` 是 6.0:1。
259
+ */
260
+ export const EDGE_STYLE: Record<EdgeKind, { stroke: string; lineWidth: number; lineDash?: number[] }> = {
261
+ flow: { stroke: '#7d8b9e', lineWidth: 1.4 },
262
+ branch: { stroke: '#7d8b9e', lineWidth: 1.4 },
263
+ inject: { stroke: '#175cd3', lineWidth: 1.6 },
264
+ collect: { stroke: '#175cd3', lineWidth: 1.6 },
265
+ feedback: { stroke: '#175cd3', lineWidth: 1.6, lineDash: [5, 4] },
266
+ }
267
+
268
+ /** 跨链路边走正交路由并自动避障:它们是长线,不能斜穿卡片。 */
269
+ const CROSS_LANE: EdgeKind[] = ['inject', 'collect', 'feedback']
270
+
271
+ /** 图例条目。**从 `EDGE_STYLE` 派生**,所以图例与画布不可能各说一套。 */
272
+ export interface EdgeLegendItem { label: string; stroke: string; lineWidth: number; lineDash?: number[] }
273
+
274
+ export const EDGE_LEGEND: EdgeLegendItem[] = [
275
+ { label: '链路内流向', ...EDGE_STYLE.flow },
276
+ { label: '跨链路流转', ...EDGE_STYLE.inject },
277
+ { label: '跨链路反馈', ...EDGE_STYLE.feedback },
278
+ ]
279
+
280
+ // ----------------------------------------------------------------- 配色
281
+
282
+ export interface LanePalette { accent: string; soft: string }
283
+
284
+ /**
285
+ * 链路身份色。**只在带名的字母徽标上出现**,节点卡片本身是中性白 ——
286
+ * 「这条属于哪条链路」由带的位置和徽标回答,不由每张卡片再染一次色。
287
+ *
288
+ * 三个色都是暗色阶,因为徽标是 11px 的字母,它得能当正文色用。它们只进徽标的
289
+ * 行内样式(DOM),所以直接用共享 tone 词表;带内的知识链路原来是青绿 `#0e7490`,
290
+ * 共享词表里没有青色,取语义最近的一档绿。
291
+ */
292
+ export const LANE_PALETTE: Record<LaneId, LanePalette> = {
293
+ interaction: { accent: 'var(--tita-primary)', soft: 'var(--tita-primary-soft)' },
294
+ knowledge: { accent: 'var(--tita-tone-green)', soft: 'var(--tita-tone-green-tint)' },
295
+ evolution: { accent: 'var(--tita-tone-violet)', soft: 'var(--tita-tone-violet-tint)' },
296
+ }
297
+
298
+ const FONT = 'var(--font-tita)'
299
+ /* 卡片与牌子是 G6 的 HTML 节点:innerHTML 进的是真实 DOM,所以这几个值走共享令牌,
300
+ * 深色主题下卡片会跟着面板一起翻。画布**画**出来的东西见文件里的 CANVAS_* 常量。 */
301
+ const INK = 'var(--tita-ink)'
302
+ const INK_MUTED = 'var(--tita-ink-muted)'
303
+ const INK_FAINT = 'var(--tita-ink-faint)'
304
+ const RULE = 'var(--tita-line)'
305
+ const SUNKEN = 'var(--tita-surface-muted)'
306
+ const SURFACE = 'var(--tita-surface)'
307
+ /* 画布侧:边标签的底色与描边。canvas 里 `var()` 会被解析成全透明(见 EDGE_STYLE 注释),
308
+ * 所以这里保留字面量,值就是浅色主题下的 line / surface。 */
309
+ const CANVAS_LABEL_BG = '#ffffff'
310
+ const CANVAS_LABEL_RULE = '#e4e7ec'
311
+
312
+ function escapeHtml(value: string): string {
313
+ return value.replace(/[&<>"']/g, char => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[char] as string)
314
+ }
315
+
316
+ /**
317
+ * 一张节点卡片的 DOM 字符串。
318
+ *
319
+ * 用 G6 的 HTML 节点(而不是 canvas 文本)是因为这张卡片要写**四层信息**:
320
+ * 标题、一句话说明、数据统计、实现状态。canvas 文本只有一种字号与字重,
321
+ * 四层挤成一样大就等于没有层次。
322
+ *
323
+ * 关键样式全部内联:卡片是画布里的 DOM,不保证和插件样式表同时到位,
324
+ * 靠类名会得到一张没有边框的白块。类名只用于选中态这类可选增强。
325
+ *
326
+ * 视觉上是「白卡 + 1px 发丝边 + 6px 圆角 + 几乎看不见的投影」。状态不是一块色底,
327
+ * 而是一枚小圆点加状态文字 —— 四张卡并排时,色底会比标题还响。
328
+ */
329
+ export function cardHtml(id: NodeId, metrics: NodeMetric[], selected = false): string {
330
+ const meta = NODE_META[id]
331
+ const lane = LANE_PALETTE[meta.lane]
332
+ const status = NODE_STATUS_TONE[meta.status]
333
+ const compact = Boolean(ARCH_SLOTS[id].stack)
334
+ /**
335
+ * 卡片上最多两个计数,而且是**一行纯文字**,不是会自动换行的 chip 组。
336
+ *
337
+ * 0027 之前这里是三枚 chip、允许换行;「每日更新知识库」正好卡在换行边界上,
338
+ * 第二行被卡片裁掉一半 —— 裁掉的数字比没有数字更糟。两个计数 + 不换行,
339
+ * 一行放得下所有节点的组合(最宽的一组实测 141px,卡片内宽 158px),
340
+ * 完整的计数列表在右侧面板里,一个都没少。
341
+ */
342
+ const chips = metrics.slice(0, 2).map(metric =>
343
+ `<span style="display:inline-flex;align-items:baseline;gap:3px;white-space:nowrap;font-size:10px;line-height:16px;color:${INK_FAINT}">${escapeHtml(metric.label)}<b style="color:${INK};font-weight:600;font-variant-numeric:tabular-nums">${escapeHtml(metric.value)}</b></span>`).join('')
344
+ // 悬停给出完整名字与状态依据:卡片上的名字可能被省略号截短,但信息不能丢。
345
+ return `<div class="tita-archCard${selected ? ' tita-archCardOn' : ''}" title="${escapeHtml(`${meta.title} —— ${meta.statusNote}`)}" style="box-sizing:border-box;width:100%;height:100%;display:flex;flex-direction:column;gap:5px;padding:10px 11px;border-radius:6px;background:${SURFACE};border:1px solid ${selected ? lane.accent : RULE};box-shadow:${selected ? `0 0 0 3px ${lane.soft}` : '0 1px 2px rgba(16,24,40,.04)'};font-family:${FONT};overflow:hidden">
346
+ <div style="display:flex;align-items:center;gap:6px">
347
+ <span style="width:6px;height:6px;border-radius:999px;background:${lane.accent};flex:0 0 auto"></span>
348
+ <strong style="font-size:12.5px;font-weight:600;color:${INK};line-height:1.25;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${escapeHtml(meta.shortTitle ?? meta.title)}</strong>
349
+ <span style="margin-left:auto;flex:0 0 auto;display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:600;color:${status.text}"><i style="width:5px;height:5px;border-radius:999px;background:${status.dot}"></i>${escapeHtml(NODE_STATUS_SHORT[meta.status])}</span>
350
+ </div>
351
+ <div style="font-size:11px;line-height:1.4;color:${INK_MUTED};display:-webkit-box;-webkit-line-clamp:${compact ? 1 : 2};-webkit-box-orient:vertical;overflow:hidden">${escapeHtml(meta.subtitle)}</div>
352
+ ${chips ? `<div style="margin-top:auto;display:flex;align-items:baseline;gap:10px;overflow:hidden">${chips}</div>` : ''}
353
+ </div>`
354
+ }
355
+
356
+ /** 某条链路当前有多少节点落在每一档状态上。带名条右侧读它。 */
357
+ export function laneStatusCounts(lane: LaneId): Record<NodeStatus, number> {
358
+ const counts = { live: 0, partial: 0, planned: 0, host: 0 } as Record<NodeStatus, number>
359
+ for (const id of nodesOfLane(NODE_LANES.findIndex(item => item.id === lane))) counts[NODE_META[id].status] += 1
360
+ return counts
361
+ }
362
+
363
+ /**
364
+ * 问题域牌子的 id 前缀。它们**不是节点**(不可被当成架构节点选中),但和带名牌不同,
365
+ * 它们**可点**:点开是在画布内浮层里展开这个域的完整论述。所以它们既不能进
366
+ * `isDecoration`,也不能进 `NODE_META`,只能有自己的前缀。
367
+ */
368
+ export const PROBLEM_PREFIX = 'domain:'
369
+ export function isProblemPlate(id: string): boolean {
370
+ return id.startsWith(PROBLEM_PREFIX)
371
+ }
372
+ export function problemIdOf(id: string): string {
373
+ return id.slice(PROBLEM_PREFIX.length)
374
+ }
375
+
376
+ /** 问题域身份色。只在牌子的图标与序号上出现,牌子本身是中性白;同样只进 DOM。 */
377
+ const DOMAIN_TONE: Record<string, { accent: string; soft: string }> = {
378
+ violet: { accent: 'var(--tita-tone-violet)', soft: 'var(--tita-tone-violet-tint)' },
379
+ green: { accent: 'var(--tita-tone-green)', soft: 'var(--tita-tone-green-tint)' },
380
+ blue: { accent: 'var(--tita-primary)', soft: 'var(--tita-primary-soft)' },
381
+ amber: { accent: 'var(--tita-tone-amber)', soft: 'var(--tita-tone-amber-tint)' },
382
+ neutral: { accent: 'var(--tita-ink-muted)', soft: 'var(--tita-surface-muted)' },
383
+ }
384
+
385
+ /**
386
+ * 一条问题域牌子的 DOM。
387
+ *
388
+ * 它画在画布上,所以只能放**一眼能读完**的东西:这一带是什么、它现在走到哪一步、
389
+ * 以及衡量它的口径。业界对照那一段是整段引述,放上来会把牌子撑成一块文字墙,
390
+ * 而且它在「适应」档缩到 0.7 之后本来也读不了 —— 它属于点开后的浮层。
391
+ *
392
+ * 「点开看完整对照」这行提示不是装饰:牌子上没有任何看起来像按钮的东西,
393
+ * 不说清楚就没人知道它能点。
394
+ */
395
+ export function problemPlateHtml(domain: ProblemDomain, stats: KnowledgeStats, version: number, selected = false): string {
396
+ const palette = DOMAIN_TONE[domain.tone] ?? DOMAIN_TONE.neutral!
397
+ const progress = domain.progress?.(stats, version) ?? []
398
+ const statusColor = domain.status.tone === 'success' ? 'var(--tita-success)' : domain.status.tone === 'warning' ? 'var(--tita-warning)' : INK_FAINT
399
+ const statusDot = domain.status.tone === 'success' ? 'var(--tita-success)' : domain.status.tone === 'warning' ? 'var(--tita-warning)' : INK_FAINT
400
+ const mechanisms = domain.actions.map(action => action.node
401
+ ? escapeHtml(action.label)
402
+ : `<span style="color:${INK_FAINT}">${escapeHtml(action.label)}<span style="opacity:.7">(未落地)</span></span>`).join('<span style="color:var(--tita-line-strong)"> · </span>')
403
+ const numbers = progress.map(item =>
404
+ `<span style="display:inline-flex;align-items:baseline;gap:3px;white-space:nowrap;font-size:10px;color:${INK_FAINT}">${escapeHtml(item.label)}<b style="color:${INK};font-weight:600;font-variant-numeric:tabular-nums">${escapeHtml(item.value)}</b></span>`).join('')
405
+ return `<div class="tita-archPlate${selected ? ' tita-archPlateOn' : ''}" title="${escapeHtml(`${domain.title} —— 点击展开完整论述`)}" style="box-sizing:border-box;width:100%;height:100%;display:flex;flex-direction:column;gap:7px;padding:11px 12px;border-radius:6px;background:${SURFACE};border:1px solid ${selected ? palette.accent : RULE};box-shadow:${selected ? `0 0 0 3px ${palette.soft}` : '0 1px 2px rgba(16,24,40,.04)'};font-family:${FONT};overflow:hidden">
406
+ <div style="display:flex;align-items:center;gap:7px">
407
+ <span style="display:inline-flex;width:22px;height:22px;flex:0 0 auto;align-items:center;justify-content:center;border-radius:5px;background:${palette.soft};color:${palette.accent};font-size:10px;font-weight:700;font-variant-numeric:tabular-nums">${escapeHtml(domain.index)}</span>
408
+ <strong style="min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:12.5px;font-weight:650;color:${INK}">${escapeHtml(domain.title)}</strong>
409
+ <span style="margin-left:auto;flex:0 0 auto;display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:600;color:${statusColor}"><i style="width:5px;height:5px;border-radius:999px;background:${statusDot}"></i>${escapeHtml(domain.status.label)}</span>
410
+ </div>
411
+ <div style="font-size:11px;line-height:1.45;color:${INK_MUTED};display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden">${escapeHtml(domain.question)}</div>
412
+ <div style="display:flex;align-items:baseline;gap:6px;min-width:0"><span style="flex:0 0 auto;font-size:9.5px;font-weight:700;letter-spacing:.06em;color:${INK_FAINT}">机制</span><span style="min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:10.5px;color:${INK_MUTED}">${mechanisms}</span></div>
413
+ ${numbers ? `<div style="margin-top:auto;display:flex;align-items:baseline;gap:10px;overflow:hidden">${numbers}</div>` : '<div style="margin-top:auto"></div>'}
414
+ <div style="display:flex;align-items:baseline;gap:6px;border-top:1px solid ${RULE};padding-top:6px"><span style="flex:0 0 auto;font-size:9.5px;font-weight:700;letter-spacing:.06em;color:${INK_FAINT}">衡量</span><span style="min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:10.5px;color:${INK_MUTED}">${escapeHtml(domain.measure)}</span><span style="margin-left:auto;flex:0 0 auto;font-size:10px;color:${palette.accent}">展开 ›</span></div>
415
+ </div>`
416
+ }
417
+
418
+ /**
419
+ * 问题域带的带名条。
420
+ *
421
+ * 它比链路名牌宽,因为要一次说清两件事:这一带是什么,以及全图实现到哪一步。
422
+ * 逐带的状态计数(A / B / C 各自几实现几待做)也挂在这里 —— 0027 之前它写在右侧
423
+ * 面板的空态里,而空态面板已经撤掉了(画布上不再有「未选中」这一态)。
424
+ *
425
+ * 这里**不写**「三个问题域都建立在口径可信的取数之上」那句话:它是一整句论述,
426
+ * 挤进 30px 高的牌子只会被截成「…口径…」,读起来像坏了。那句话在浮层的「底座」一段。
427
+ */
428
+ export function problemHeadHtml(): string {
429
+ const laneLine = NODE_LANES.map(lane => {
430
+ const counts = laneStatusCounts(lane.id)
431
+ const parts = (['live', 'partial', 'planned', 'host'] as NodeStatus[])
432
+ .filter(status => counts[status] > 0)
433
+ .map(status => `${NODE_STATUS_SHORT[status]} ${counts[status]}`)
434
+ .join(' · ')
435
+ return `${lane.index} ${parts}`
436
+ }).join(' ')
437
+ return `<div style="box-sizing:border-box;width:100%;height:100%;display:flex;align-items:center;gap:10px;padding:0 11px;background:${SUNKEN};border:1px solid ${RULE};border-radius:6px;font-family:${FONT}">
438
+ <strong style="flex:0 0 auto;font-size:12px;font-weight:650;color:${INK}">Agent 解决的问题域</strong>
439
+ <span style="flex:0 0 auto;font-size:10.5px;color:${INK_FAINT}">按重要性排序 · 点开看业界对照与衡量方式</span>
440
+ <span style="margin-left:auto;flex:0 0 auto;font-size:10px;color:${INK_MUTED};font-variant-numeric:tabular-nums">${escapeHtml(laneLine)}</span>
441
+ </div>`
442
+ }
443
+
444
+ /**
445
+ * 一条带的带名牌。**这是链路名唯一出现的地方** —— 它挂在带自己的左上角,
446
+ * 而不是左侧留白里:留白要留给跨链路走线,而且「带名贴着自己那条带」本来就更好读。
447
+ *
448
+ * 牌子上只有字母徽标和链路名,因为再多的东西就要把牌子撑宽,而牌子越宽,
449
+ * 垂直走线能走的余地越小。流程说明与状态统计因此搬到**问题域带的带名条**上 ——
450
+ * 那条带下面没有垂直走线,是这块版式里唯一放得下它们的地方。
451
+ */
452
+ export function laneHeadHtml(lane: LaneMeta): string {
453
+ const palette = LANE_PALETTE[lane.id]
454
+ return `<div style="box-sizing:border-box;width:100%;height:100%;display:flex;align-items:center;gap:9px;padding:0 11px;background:${SUNKEN};border:1px solid ${RULE};border-radius:6px;font-family:${FONT}">
455
+ <span style="display:inline-flex;width:18px;height:18px;flex:0 0 auto;align-items:center;justify-content:center;border-radius:4px;background:${palette.soft};color:${palette.accent};font-size:11px;font-weight:700;line-height:1">${escapeHtml(lane.index)}</span>
456
+ <strong style="min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:12px;font-weight:650;color:${INK}">${escapeHtml(lane.title)}</strong>
457
+ </div>`
458
+ }
459
+
460
+ // ----------------------------------------------------------------- G6 数据
461
+
462
+ /**
463
+ * G6 的图数据形状。
464
+ *
465
+ * `[key: string]: unknown` 这一行不是偷懒:G6 的 `NodeData` / `EdgeData` 自带索引
466
+ * 签名,没有它这里的结构体无法赋给它(TS 会报「缺少索引签名」)。我仍然显式声明
467
+ * `id` / `source` / `target`,因为那才是我们真正依赖的字段。
468
+ */
469
+ export interface ArchNodeDatum {
470
+ id: string
471
+ type: string
472
+ style: Record<string, unknown>
473
+ data?: Record<string, unknown>
474
+ [key: string]: unknown
475
+ }
476
+
477
+ export interface ArchEdgeDatum {
478
+ id: string
479
+ source: string
480
+ target: string
481
+ /** G6 v5 读的是 **data 上的** `type`;写进 `style` 等于没写,会退回图级默认。 */
482
+ type: string
483
+ style: Record<string, unknown>
484
+ data?: Record<string, unknown>
485
+ [key: string]: unknown
486
+ }
487
+
488
+ export interface ArchGraph { nodes: ArchNodeDatum[]; edges: ArchEdgeDatum[] }
489
+
490
+ /** 三条带名牌 + 问题域带名牌 + 一块不可见的走线挡板。它们先入数组,画在卡片之下。 */
491
+ function decorations(): ArchNodeDatum[] {
492
+ const wall: ArchNodeDatum = {
493
+ id: `${DECORATION_PREFIX}route-wall`,
494
+ type: 'rect',
495
+ style: {
496
+ x: ROUTE_WALL_W / 2, y: CANVAS_HEIGHT / 2,
497
+ size: [ROUTE_WALL_W, CANVAS_HEIGHT],
498
+ fill: 'transparent', stroke: 'transparent', lineWidth: 0,
499
+ pointerEvents: 'none',
500
+ },
501
+ }
502
+ return [wall, ...NODE_LANES.map((lane, index) => ({
503
+ id: `${DECORATION_PREFIX}lane-${lane.id}`,
504
+ type: 'html',
505
+ style: {
506
+ x: GUTTER + LANE_HEAD_W / 2, y: laneTop(index) + LANE_HEAD_H / 2,
507
+ size: [LANE_HEAD_W, LANE_HEAD_H],
508
+ // HTML 节点的容器以左上角为原点,靠 dx/dy 把它对到格子的中心。
509
+ dx: -LANE_HEAD_W / 2, dy: -LANE_HEAD_H / 2,
510
+ innerHTML: laneHeadHtml(lane),
511
+ // 点带名牌等于点空白:它是装饰,不是节点。
512
+ pointerEvents: 'none',
513
+ },
514
+ })), {
515
+ id: `${DECORATION_PREFIX}problems-head`,
516
+ type: 'html',
517
+ style: {
518
+ x: GUTTER + PROBLEM_HEAD_W / 2, y: PROBLEM_TOP + PROBLEM_HEAD_H / 2,
519
+ size: [PROBLEM_HEAD_W, PROBLEM_HEAD_H],
520
+ dx: -PROBLEM_HEAD_W / 2, dy: -PROBLEM_HEAD_H / 2,
521
+ innerHTML: problemHeadHtml(),
522
+ pointerEvents: 'none',
523
+ },
524
+ }]
525
+ }
526
+
527
+ /** 带上按阅读顺序排好的节点(自进化带是反向的,由 ARCH_SLOTS 的列号决定)。 */
528
+ export function nodesOfLane(lane: number): NodeId[] {
529
+ const rank = (slot: Slot): number => slot.stack === 'upper' ? -1 : slot.stack === 'lower' ? 1 : 0
530
+ return (Object.keys(ARCH_SLOTS) as NodeId[])
531
+ .filter(id => ARCH_SLOTS[id].lane === lane)
532
+ .sort((left, right) => ARCH_SLOTS[left].col - ARCH_SLOTS[right].col || rank(ARCH_SLOTS[left]) - rank(ARCH_SLOTS[right]))
533
+ }
534
+
535
+ /**
536
+ * 把「节点 + 统计 + 状态」编译成 G6 的图数据。
537
+ *
538
+ * 统计只来自 `nodeMetrics(stats, version)`:图上卡片与右侧面板读的是同一个数组,
539
+ * 所以不会出现「图上 12 张卡片、面板 11 张」这种两边各算一次才有的错。
540
+ */
541
+ export function toGraphData(stats: KnowledgeStats, version: number, selected: NodeId | null = null, selectedDomain: string | null = null): ArchGraph {
542
+ const nodes: ArchNodeDatum[] = decorations()
543
+ for (const id of Object.keys(ARCH_SLOTS) as NodeId[]) {
544
+ const box = boxOf(id)
545
+ nodes.push({
546
+ id,
547
+ type: 'html',
548
+ data: { nodeId: id },
549
+ style: {
550
+ x: box.x, y: box.y, size: [box.width, box.height],
551
+ dx: -box.width / 2, dy: -box.height / 2,
552
+ innerHTML: cardHtml(id, nodeMetrics(id, stats, version), selected === id),
553
+ pointerEvents: 'auto',
554
+ },
555
+ })
556
+ }
557
+ // 问题域牌子:**可点**,所以它们是真正的图元素,不是装饰。它们与节点共用一套
558
+ // 「选中就换边框」的反馈,读起来才是同一张图上的两种东西。
559
+ PROBLEM_DOMAINS.forEach((domain, index) => {
560
+ const box = problemPlateBox(index)
561
+ nodes.push({
562
+ id: `${PROBLEM_PREFIX}${domain.id}`,
563
+ type: 'html',
564
+ data: { domainId: domain.id },
565
+ style: {
566
+ x: box.x, y: box.y, size: [box.width, box.height],
567
+ dx: -box.width / 2, dy: -box.height / 2,
568
+ innerHTML: problemPlateHtml(domain, stats, version, selectedDomain === domain.id),
569
+ pointerEvents: 'auto',
570
+ },
571
+ })
572
+ })
573
+ const edges: ArchEdgeDatum[] = ARCH_EDGES.map(edge => {
574
+ const palette = EDGE_STYLE[edge.kind]
575
+ const style: Record<string, unknown> = {
576
+ stroke: palette.stroke,
577
+ lineWidth: palette.lineWidth,
578
+ ...(palette.lineDash ? { lineDash: palette.lineDash } : {}),
579
+ endArrow: true, endArrowType: 'triangle', endArrowSize: 8,
580
+ }
581
+ if (CROSS_LANE.includes(edge.kind)) {
582
+ // 正交 + 自动避障:这几条是长线,斜着连会横穿中间的卡片。
583
+ style.radius = 10
584
+ style.router = { type: 'shortest-path', enableObstacleAvoidance: true, offset: 14, gridSize: 8 }
585
+ } else if (edge.kind === 'branch') {
586
+ style.radius = 8
587
+ style.router = { type: 'orth', padding: 14 }
588
+ }
589
+ if (edge.label) {
590
+ Object.assign(style, {
591
+ labelText: edge.label, labelFontSize: 10, labelFill: palette.stroke, labelFontFamily: FONT,
592
+ // 默认会按路径方向旋转标签,竖直那一段于是把中文排成了一列一个字。
593
+ labelAutoRotate: false,
594
+ labelBackground: true, labelBackgroundFill: CANVAS_LABEL_BG, labelBackgroundRadius: 4,
595
+ // 画布侧的字面量:见文件顶部 CANVAS_LABEL_* 的说明。
596
+ labelBackgroundStroke: CANVAS_LABEL_RULE, labelBackgroundLineWidth: 1,
597
+ labelPadding: [2, 6, 2, 6],
598
+ })
599
+ }
600
+ return {
601
+ id: edge.id,
602
+ source: edge.source,
603
+ target: edge.target,
604
+ type: 'polyline',
605
+ data: { kind: edge.kind },
606
+ style,
607
+ }
608
+ })
609
+ return { nodes, edges }
610
+ }
611
+
612
+ /** 全图每条链路当前有多少节点「已实现 / 部分实现 / 待实现」——概览页脚用。 */
613
+ export function statusCounts(): Record<string, number> {
614
+ const counts: Record<string, number> = {}
615
+ for (const meta of Object.values(NODE_META)) counts[meta.status] = (counts[meta.status] ?? 0) + 1
616
+ return counts
617
+ }
618
+
619
+ export function formatBuildTime(value: string | undefined): string {
620
+ if (!value) return '尚未编译'
621
+ const date = new Date(value)
622
+ if (Number.isNaN(date.getTime())) return '已编译'
623
+ return date.toLocaleString('zh-CN', { month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' })
624
+ }
625
+
626
+ /**
627
+ * 节点面板的「状态记录」。
628
+ *
629
+ * 只写**有出处**的字段:`/knowledge/stats` 里的构建清单与磁盘计数。拿不到的东西
630
+ * (比如「情节记忆 248 条」)就不写 —— 那一栏曾经是编出来的占位数字,0025 把它
631
+ * 换成了真实的增量编译计数,因为这张图现在是契约,不是示意图。
632
+ */
633
+ export function recordRows(id: NodeId, stats: KnowledgeStats, version: number): Array<[string, string]> {
634
+ const build = stats.lastBuild ?? null
635
+ const rows: Array<[string, string]> = []
636
+ const indexRow: [string, string] = ['索引版本', version > 0 ? `v${version}` : '未构建']
637
+ switch (id) {
638
+ case 'source-raw':
639
+ rows.push(['SourceRaw 文档', String(sourceCountOf(stats))])
640
+ rows.push(['卡片版本快照', String(stats.cardVersions ?? 0)])
641
+ rows.push(['最近编译', formatBuildTime(stats.builtAt)])
642
+ break
643
+ case 'compile':
644
+ rows.push(['上一轮编译', build ? (build.skipped ? '跳过(语料无变化)' : `复用 ${build.cardsReused ?? 0} / 重编 ${build.cardsRecompiled ?? 0}`) : '尚未编译'])
645
+ rows.push(['新增 / 删除卡片', `${build?.cardsAdded ?? 0} / ${build?.cardsRemoved ?? 0}`])
646
+ rows.push(['wikiNotes 复用 / 重编', `${build?.notesReused ?? 0} / ${build?.notesRecompiled ?? 0}`])
647
+ rows.push(['已纳入指纹的卡片', String(stats.trackedCards ?? 0)])
648
+ break
649
+ case 'build':
650
+ rows.push(['向量条目', String(stats.vectorCount ?? 0)])
651
+ rows.push(['量化方式', stats.vectorQuantization ?? 'binary+hamming+int8'])
652
+ rows.push(['最近构建', formatBuildTime(stats.builtAt)])
653
+ rows.push(['上一轮向量复用', build ? String(build.notesReused ?? 0) : '—'])
654
+ rows.push(indexRow)
655
+ break
656
+ case 'recall':
657
+ rows.push(['默认路线', stats.recallMode ?? 'hybrid'])
658
+ rows.push(['召回 topK', String(stats.recallTopK ?? 10)])
659
+ rows.push(['检索后端', stats.searchBackend === 'hybrid' ? 'Hybrid(关键词 + 向量 + 图谱)' : 'FTS fallback'])
660
+ rows.push(indexRow)
661
+ break
662
+ case 'context':
663
+ rows.push(['重排 Provider', stats.rerankProvider === 'none' ? '关闭(按融合分数)' : stats.rerankProvider ?? 'bert-python'])
664
+ rows.push(['重排模型', stats.rerankModel ?? 'BAAI/bge-reranker-base'])
665
+ rows.push(['上下文 topN', String(stats.contextTopN ?? 5)])
666
+ rows.push(['可注入 Runtime', version > 0 ? '是' : '等待编译'])
667
+ break
668
+ case 'runtime':
669
+ rows.push(indexRow)
670
+ rows.push(['检索后端', stats.searchBackend === 'hybrid' ? 'Hybrid' : 'FTS fallback'])
671
+ rows.push(['Embedding', stats.embeddingStatus === 'ready' ? '已就绪' : stats.embeddingProvider === 'none' ? '关闭' : '不可用(FTS fallback)'])
672
+ rows.push(['模型 / 设备', `${stats.embeddingModel ?? 'WeMM'} · ${stats.embeddingDevice ?? 'auto'}`])
673
+ break
674
+ case 'interaction':
675
+ rows.push(['意图审计流', '每轮一条 intent 记录(只标注不拦截)'])
676
+ rows.push(['memory/ 落盘', `${memoryCountOf(stats)} 个文件`])
677
+ break
678
+ case 'episodic-memory':
679
+ case 'procedural-memory':
680
+ case 'memory-quality':
681
+ case 'daily-update':
682
+ rows.push(['memory/ 落盘', `${memoryCountOf(stats)} 个文件`])
683
+ rows.push(['卡片版本快照', String(stats.cardVersions ?? 0)])
684
+ rows.push(['最近编译', formatBuildTime(stats.builtAt)])
685
+ break
686
+ default:
687
+ rows.push(indexRow)
688
+ break
689
+ }
690
+ return rows
691
+ }