dsh-data-domain-knowledge 0.1.0-alpha.13 → 0.1.0-alpha.15

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 (266) hide show
  1. package/README.md +89 -56
  2. package/cordis.patch.yml +36 -129
  3. package/eval/benchmark-extraction-format.mjs +20 -0
  4. package/lib/client.js +3262 -4142
  5. package/lib/doc-page.js +1 -1
  6. package/lib/pages.js +1147 -364
  7. package/lib/types/axis-registry.d.ts +8 -1
  8. package/lib/types/axis-registry.d.ts.map +1 -1
  9. package/lib/types/axis-registry.js +62 -7
  10. package/lib/types/axis-registry.js.map +1 -1
  11. package/lib/types/convert/host.d.ts.map +1 -1
  12. package/lib/types/convert/host.js +3 -0
  13. package/lib/types/convert/host.js.map +1 -1
  14. package/lib/types/convert/ingest.d.ts +9 -0
  15. package/lib/types/convert/ingest.d.ts.map +1 -1
  16. package/lib/types/convert/ingest.js +68 -9
  17. package/lib/types/convert/ingest.js.map +1 -1
  18. package/lib/types/diagnosis/cards.d.ts +7 -3
  19. package/lib/types/diagnosis/cards.d.ts.map +1 -1
  20. package/lib/types/diagnosis/cards.js +13 -8
  21. package/lib/types/diagnosis/cards.js.map +1 -1
  22. package/lib/types/diagnosis/run.d.ts +4 -13
  23. package/lib/types/diagnosis/run.d.ts.map +1 -1
  24. package/lib/types/diagnosis/run.js +26 -16
  25. package/lib/types/diagnosis/run.js.map +1 -1
  26. package/lib/types/diagnosis/schema.js +2 -2
  27. package/lib/types/diagnosis/schema.js.map +1 -1
  28. package/lib/types/diagnosis/seed-source.d.ts +1 -1
  29. package/lib/types/diagnosis/seed-source.d.ts.map +1 -1
  30. package/lib/types/diagnosis/seed-source.js +5 -4
  31. package/lib/types/diagnosis/seed-source.js.map +1 -1
  32. package/lib/types/diagnosis/seed.d.ts +4 -4
  33. package/lib/types/diagnosis/seed.d.ts.map +1 -1
  34. package/lib/types/diagnosis/seed.js +6 -8
  35. package/lib/types/diagnosis/seed.js.map +1 -1
  36. package/lib/types/diagnosis/store.d.ts +18 -17
  37. package/lib/types/diagnosis/store.d.ts.map +1 -1
  38. package/lib/types/diagnosis/store.js +94 -66
  39. package/lib/types/diagnosis/store.js.map +1 -1
  40. package/lib/types/diagnosis/types.d.ts +10 -16
  41. package/lib/types/diagnosis/types.d.ts.map +1 -1
  42. package/lib/types/diagnosis/types.js.map +1 -1
  43. package/lib/types/environment/dsh-python.d.ts +77 -0
  44. package/lib/types/environment/dsh-python.d.ts.map +1 -0
  45. package/lib/types/environment/dsh-python.js +139 -0
  46. package/lib/types/environment/dsh-python.js.map +1 -0
  47. package/lib/types/environment/install.d.ts +9 -1
  48. package/lib/types/environment/install.d.ts.map +1 -1
  49. package/lib/types/environment/install.js +149 -24
  50. package/lib/types/environment/install.js.map +1 -1
  51. package/lib/types/environment/model-catalog.d.ts +34 -2
  52. package/lib/types/environment/model-catalog.d.ts.map +1 -1
  53. package/lib/types/environment/model-catalog.js +40 -2
  54. package/lib/types/environment/model-catalog.js.map +1 -1
  55. package/lib/types/environment/service.d.ts +13 -3
  56. package/lib/types/environment/service.d.ts.map +1 -1
  57. package/lib/types/environment/service.js +127 -23
  58. package/lib/types/environment/service.js.map +1 -1
  59. package/lib/types/environment/types.d.ts +9 -0
  60. package/lib/types/environment/types.d.ts.map +1 -1
  61. package/lib/types/events.d.ts +2 -0
  62. package/lib/types/events.d.ts.map +1 -1
  63. package/lib/types/events.js +13 -0
  64. package/lib/types/events.js.map +1 -1
  65. package/lib/types/extract-contract.d.ts +1 -1
  66. package/lib/types/extract-contract.d.ts.map +1 -1
  67. package/lib/types/extract-contract.js +34 -31
  68. package/lib/types/extract-contract.js.map +1 -1
  69. package/lib/types/extract-model.d.ts +2 -0
  70. package/lib/types/extract-model.d.ts.map +1 -1
  71. package/lib/types/extract-model.js.map +1 -1
  72. package/lib/types/extract-table-contract.d.ts +3 -0
  73. package/lib/types/extract-table-contract.d.ts.map +1 -0
  74. package/lib/types/extract-table-contract.js +30 -0
  75. package/lib/types/extract-table-contract.js.map +1 -0
  76. package/lib/types/extract-table.d.ts +8 -0
  77. package/lib/types/extract-table.d.ts.map +1 -0
  78. package/lib/types/extract-table.js +151 -0
  79. package/lib/types/extract-table.js.map +1 -0
  80. package/lib/types/extraction-runs.d.ts +30 -6
  81. package/lib/types/extraction-runs.d.ts.map +1 -1
  82. package/lib/types/extraction-runs.js +69 -8
  83. package/lib/types/extraction-runs.js.map +1 -1
  84. package/lib/types/fs-atomic.d.ts +0 -7
  85. package/lib/types/fs-atomic.d.ts.map +1 -1
  86. package/lib/types/fs-atomic.js +17 -2
  87. package/lib/types/fs-atomic.js.map +1 -1
  88. package/lib/types/images.d.ts +1 -1
  89. package/lib/types/images.d.ts.map +1 -1
  90. package/lib/types/images.js +2 -2
  91. package/lib/types/images.js.map +1 -1
  92. package/lib/types/index.d.ts.map +1 -1
  93. package/lib/types/index.js +232 -50
  94. package/lib/types/index.js.map +1 -1
  95. package/lib/types/intent/scope.d.ts.map +1 -1
  96. package/lib/types/intent/scope.js +1 -0
  97. package/lib/types/intent/scope.js.map +1 -1
  98. package/lib/types/intent/tools.d.ts +19 -0
  99. package/lib/types/intent/tools.d.ts.map +1 -1
  100. package/lib/types/intent/tools.js +37 -1
  101. package/lib/types/intent/tools.js.map +1 -1
  102. package/lib/types/knowledge-bases.d.ts.map +1 -1
  103. package/lib/types/knowledge-bases.js +5 -16
  104. package/lib/types/knowledge-bases.js.map +1 -1
  105. package/lib/types/knowledge.d.ts +3 -0
  106. package/lib/types/knowledge.d.ts.map +1 -1
  107. package/lib/types/knowledge.js +21 -5
  108. package/lib/types/knowledge.js.map +1 -1
  109. package/lib/types/materialize-snapshot.d.ts +49 -0
  110. package/lib/types/materialize-snapshot.d.ts.map +1 -0
  111. package/lib/types/materialize-snapshot.js +2 -0
  112. package/lib/types/materialize-snapshot.js.map +1 -0
  113. package/lib/types/model-text.d.ts +1 -1
  114. package/lib/types/model-text.d.ts.map +1 -1
  115. package/lib/types/model-text.js +5 -4
  116. package/lib/types/model-text.js.map +1 -1
  117. package/lib/types/oplog.d.ts.map +1 -1
  118. package/lib/types/oplog.js +7 -0
  119. package/lib/types/oplog.js.map +1 -1
  120. package/lib/types/pipeline/engine.d.ts +60 -0
  121. package/lib/types/pipeline/engine.d.ts.map +1 -0
  122. package/lib/types/pipeline/engine.js +338 -0
  123. package/lib/types/pipeline/engine.js.map +1 -0
  124. package/lib/types/pipeline/store.d.ts +73 -0
  125. package/lib/types/pipeline/store.d.ts.map +1 -0
  126. package/lib/types/pipeline/store.js +143 -0
  127. package/lib/types/pipeline/store.js.map +1 -0
  128. package/lib/types/pipeline/types.d.ts +101 -0
  129. package/lib/types/pipeline/types.d.ts.map +1 -0
  130. package/lib/types/pipeline/types.js +5 -0
  131. package/lib/types/pipeline/types.js.map +1 -0
  132. package/lib/types/present.d.ts +2 -0
  133. package/lib/types/present.d.ts.map +1 -1
  134. package/lib/types/present.js +9 -4
  135. package/lib/types/present.js.map +1 -1
  136. package/lib/types/retention.d.ts +39 -0
  137. package/lib/types/retention.d.ts.map +1 -0
  138. package/lib/types/retention.js +42 -0
  139. package/lib/types/retention.js.map +1 -0
  140. package/lib/types/runtime.d.ts +115 -5
  141. package/lib/types/runtime.d.ts.map +1 -1
  142. package/lib/types/runtime.js +862 -132
  143. package/lib/types/runtime.js.map +1 -1
  144. package/lib/types/source-evidence.d.ts +9 -1
  145. package/lib/types/source-evidence.d.ts.map +1 -1
  146. package/lib/types/source-evidence.js +13 -5
  147. package/lib/types/source-evidence.js.map +1 -1
  148. package/lib/types/store/axis-references.js +3 -3
  149. package/lib/types/store/axis-references.js.map +1 -1
  150. package/lib/types/store/derived.d.ts +1 -1
  151. package/lib/types/store/derived.d.ts.map +1 -1
  152. package/lib/types/store/derived.js +41 -7
  153. package/lib/types/store/derived.js.map +1 -1
  154. package/lib/types/store/knowledge-db.d.ts +18 -0
  155. package/lib/types/store/knowledge-db.d.ts.map +1 -1
  156. package/lib/types/store/knowledge-db.js +111 -6
  157. package/lib/types/store/knowledge-db.js.map +1 -1
  158. package/lib/types/store/migrate.js +1 -1
  159. package/lib/types/store/migrate.js.map +1 -1
  160. package/lib/types/trajectory-types.d.ts +172 -0
  161. package/lib/types/trajectory-types.d.ts.map +1 -0
  162. package/lib/types/trajectory-types.js +36 -0
  163. package/lib/types/trajectory-types.js.map +1 -0
  164. package/lib/types/types.d.ts +3 -4
  165. package/lib/types/types.d.ts.map +1 -1
  166. package/lib/types/types.js.map +1 -1
  167. package/lib/types/wiki-finalize.d.ts +16 -0
  168. package/lib/types/wiki-finalize.d.ts.map +1 -1
  169. package/lib/types/wiki-finalize.js +26 -0
  170. package/lib/types/wiki-finalize.js.map +1 -1
  171. package/lib/types/wiki-materialize.d.ts.map +1 -1
  172. package/lib/types/wiki-materialize.js +3 -15
  173. package/lib/types/wiki-materialize.js.map +1 -1
  174. package/lib/types/wiki-trial-types.d.ts +3 -0
  175. package/lib/types/wiki-trial-types.d.ts.map +1 -1
  176. package/lib/types/wiki-trial.d.ts +2 -0
  177. package/lib/types/wiki-trial.d.ts.map +1 -1
  178. package/lib/types/wiki-trial.js +31 -7
  179. package/lib/types/wiki-trial.js.map +1 -1
  180. package/package.json +6 -3
  181. package/preset/tita-data-agent/agent.cordis.yml +36 -129
  182. package/src/axis-registry.ts +57 -9
  183. package/src/client/AttributionEventGuide.tsx +10 -9
  184. package/src/client/CompileDialogs.tsx +10 -2
  185. package/src/client/EnvironmentSetup.tsx +76 -16
  186. package/src/client/ExpansionGraph.tsx +13 -7
  187. package/src/client/ExtractionTasksPage.tsx +543 -156
  188. package/src/client/ImportFilesDialog.tsx +2 -13
  189. package/src/client/IntentTab.tsx +3 -1
  190. package/src/client/KnowledgeBaseTab.tsx +139 -122
  191. package/src/client/KnowledgeLayerSection.tsx +19 -2
  192. package/src/client/KnowledgePanel.tsx +17 -3
  193. package/src/client/ListPager.tsx +15 -5
  194. package/src/client/MaterializeTrajectoryView.tsx +124 -0
  195. package/src/client/NodeSettings.tsx +90 -25
  196. package/src/client/NormalizeTrajectoryView.tsx +136 -0
  197. package/src/client/PipelineNotice.tsx +26 -0
  198. package/src/client/RecallPanel.tsx +55 -25
  199. package/src/client/TabGuide.tsx +4 -4
  200. package/src/client/TrialReportView.tsx +61 -16
  201. package/src/client/TypeDefinitionDrawer.tsx +33 -9
  202. package/src/client/WikiCardPreview.tsx +43 -10
  203. package/src/client/WikiFlowPanel.tsx +4 -4
  204. package/src/client/architecture.tsx +1 -1
  205. package/src/client/attribution/AttributionTab.tsx +37 -36
  206. package/src/client/attribution/CaseView.tsx +3 -1
  207. package/src/client/attribution/EventView.tsx +14 -4
  208. package/src/client/attribution/RunView.tsx +2 -2
  209. package/src/client/attribution/ViewSwitch.tsx +20 -5
  210. package/src/client/attribution/graph-options.ts +38 -31
  211. package/src/client/attribution/model.ts +16 -9
  212. package/src/client/graph/palette.ts +165 -0
  213. package/src/client/pages.css +224 -28
  214. package/src/client/pages.tsx +33 -11
  215. package/src/client/styles.css +61 -6
  216. package/src/client/wiki-trial.ts +30 -1
  217. package/src/convert/host.ts +3 -0
  218. package/src/convert/ingest.ts +73 -9
  219. package/src/diagnosis/cards.ts +13 -8
  220. package/src/diagnosis/run.ts +27 -17
  221. package/src/diagnosis/schema.ts +2 -2
  222. package/src/diagnosis/seed-source.ts +5 -4
  223. package/src/diagnosis/seed.ts +6 -8
  224. package/src/diagnosis/store.ts +99 -69
  225. package/src/diagnosis/types.ts +10 -16
  226. package/src/environment/dsh-python.ts +156 -0
  227. package/src/environment/install.ts +134 -19
  228. package/src/environment/model-catalog.ts +45 -3
  229. package/src/environment/service.ts +111 -23
  230. package/src/environment/types.ts +6 -0
  231. package/src/events.ts +12 -0
  232. package/src/extract-contract.ts +6 -3
  233. package/src/extract-model.ts +2 -0
  234. package/src/extract-table-contract.ts +29 -0
  235. package/src/extract-table.ts +126 -0
  236. package/src/extraction-runs.ts +70 -11
  237. package/src/fs-atomic.ts +19 -2
  238. package/src/images.ts +2 -2
  239. package/src/index.ts +207 -72
  240. package/src/intent/scope.ts +1 -0
  241. package/src/intent/tools.ts +38 -1
  242. package/src/knowledge-bases.ts +4 -12
  243. package/src/knowledge.ts +24 -4
  244. package/src/materialize-snapshot.ts +46 -0
  245. package/src/model-text.ts +5 -3
  246. package/src/oplog.ts +7 -0
  247. package/src/pipeline/engine.ts +312 -0
  248. package/src/pipeline/store.ts +147 -0
  249. package/src/pipeline/types.ts +92 -0
  250. package/src/present.ts +10 -4
  251. package/src/retention.ts +76 -0
  252. package/src/runtime.ts +853 -131
  253. package/src/source-evidence.ts +12 -5
  254. package/src/store/axis-references.ts +3 -3
  255. package/src/store/derived.ts +40 -7
  256. package/src/store/knowledge-db.ts +111 -5
  257. package/src/store/migrate.ts +1 -1
  258. package/src/trajectory-types.ts +142 -0
  259. package/src/types.ts +3 -4
  260. package/src/wiki-finalize.ts +28 -0
  261. package/src/wiki-materialize.ts +3 -14
  262. package/src/wiki-trial-types.ts +3 -0
  263. package/src/wiki-trial.ts +26 -7
  264. package/src/client/convert/ConvertPanel.tsx +0 -583
  265. package/src/client/convert/styles.css +0 -138
  266. package/src/client/convert/styles.ts +0 -49
@@ -577,6 +577,14 @@ body {
577
577
  显示三条完整路径。400px 下两列会挤成一列,路径会被截成读不出。 */
578
578
  .tita-settingsDrawer { width: min(560px, 100vw); }
579
579
  .tita-settingsError { border: 1px solid var(--ddk-danger-line); border-radius: 9px; background: var(--ddk-danger-tint); color: var(--tita-danger); padding: 9px 11px; font-size: 12px; }
580
+ /* 一个数字字段的草稿不合法:**不是错误框,是一句旁注**。它说的是「这一笔现在还写不了」,
581
+ 而人正停在输入框里改它 —— 套一层带边框的红块会把一个中间打字状态说成一次故障。 */
582
+ .tita-settingInvalid { color: var(--tita-danger); font-size: 11px; line-height: 1.5; }
583
+ /* 「读不到,重试」那一行。它长在**字段自己身上**(就在路径输入框下面),因为失败的是
584
+ 这一次读取,而不是整个设置分区 —— 报在分区头上会让人以为所有设置都读不到。 */
585
+ .tita-settingsRetry { display: flex; align-items: center; gap: 6px; margin: 6px 0 0; color: var(--tita-danger); font-size: 11.5px; line-height: 1.5; }
586
+ .tita-settingsRetry > svg { flex: 0 0 auto; }
587
+ .tita-settingsRetry > span { min-width: 0; flex: 1 1 auto; }
580
588
  /*
581
589
  * 分组**不是一个卡片**。
582
590
  *
@@ -635,10 +643,13 @@ body {
635
643
  * 见 `InfoHint`),所以这里只需要管「一行多长」这一件事。
636
644
  */
637
645
  .tita-infoHintPanel.tita-infoHintPanel { display: block; max-width: 320px; padding: 8px 12px; line-height: 1.65; text-align: left; white-space: normal; }
638
- /* 小节标题旁的说明入口:图标常驻、说明悬停才出现(见 `InfoHint`)。 */
639
- .tita-infoHint { display: inline-flex; width: 16px; height: 16px; align-items: center; justify-content: center; padding: 0; border: 0; border-radius: 50%; background: transparent; color: var(--tita-subtle); cursor: help; }
646
+ /* 小节标题旁的说明入口:图标常驻、说明悬停才出现(见 `InfoHint`)。
647
+ 三个颜色原来指着 `--tita-subtle` / `--tita-blue` / `--tita-accent*` —— 这四个名字
648
+ 在 `packages/ui` 与本插件里**都没有定义**。未定义的 `var()` 让整条声明在计算值阶段
649
+ 失效:入口的常驻色回退成继承色、焦点圈回退成 `currentColor`。现在指向真实 token。 */
650
+ .tita-infoHint { display: inline-flex; width: 16px; height: 16px; align-items: center; justify-content: center; padding: 0; border: 0; border-radius: 50%; background: transparent; color: var(--tita-ink-faint); cursor: help; }
640
651
  .tita-infoHint:hover { background: var(--tita-surface-muted); color: var(--tita-ink-muted); }
641
- .tita-infoHint:focus-visible { outline: 2px solid var(--tita-blue); outline-offset: 1px; }
652
+ .tita-infoHint:focus-visible { outline: 2px solid var(--tita-focus-ring); outline-offset: 1px; }
642
653
  /* 可点击的那一枚(传了 `href`):它从「说明」变成「入口」,所以光标从 help 换成 pointer ——
643
654
  同一个图标有两种含义时,光标是零成本的那条线索。 */
644
655
  .tita-infoHintAction { cursor: pointer; }
@@ -838,7 +849,10 @@ body {
838
849
  /* 琥珀:**是账,不是故障** —— 真源与来源对不上,而它是可以清掉的(判据确定、可重建)。 */
839
850
  .tita-kbNoticeWarn { border-color: var(--tita-tone-amber-line); background: var(--tita-tone-amber-tint); color: var(--tita-tone-amber); }
840
851
  .tita-kbNoticeWarn > span b { color: var(--tita-tone-amber); }
841
- .tita-kbNoticeClose { display: inline-flex; flex: 0 0 auto; align-items: center; justify-content: center; border: 0; border-radius: 6px; background: transparent; color: inherit; padding: 3px; cursor: pointer; opacity: .7; }
852
+ /* 提示条右端的关闭动作。面板里的关闭按钮现在是共享的 `Button variant="ghost" size="icon"`,
853
+ 所以这里只剩**这一条提示自己的**语气:比正文淡一点,悬停才回到满值。
854
+ `border`/`background`/`padding` 的复位撤掉 —— 那是按钮自己带的了。 */
855
+ .tita-kbNoticeClose { flex: 0 0 auto; color: inherit; opacity: .7; }
842
856
  .tita-kbNoticeClose:hover { opacity: 1; }
843
857
 
844
858
  .tita-kbRoot { overflow: hidden; max-width: 100%; color: var(--tita-ink-faint); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
@@ -1497,6 +1511,40 @@ button.tita-trialMatrixGroup:focus-visible { outline: 2px solid var(--tita-focus
1497
1511
  .tita-trialCheckHit .tita-trialCheckHits { background: var(--tita-tone-amber-tint); color: var(--tita-tone-amber); }
1498
1512
  .tita-trialCheckWhat { color: var(--tita-ink); font-size: 10.5px; line-height: 1.6; }
1499
1513
  .tita-trialCheckNote { grid-column: 3; color: var(--tita-ink-muted); font-size: 10px; line-height: 1.6; }
1514
+ /* 区块导航(2026-10-18)。报告可以长到几百 KB,打开它的人第一件事是「跳到要看的那一段」。
1515
+ **不吸顶**:上面那条常驻标题条是 `sticky; z-index: 2`,而它的高度随标题换行变化
1516
+ (窄屏上 meta 会折成两行)。导航条吸在 `top: 0` 会正好钻到它下面 —— 看起来是「一滚就没了」。
1517
+ 让开它需要一个写不死的高度,而写死的高度在窄屏上会错位。所以它就在正文顶部,
1518
+ 加上每个区块的 `scroll-margin`,点一下照样落到该看的位置。 */
1519
+ .tita-trialNav {
1520
+ display: flex;
1521
+ flex-wrap: wrap;
1522
+ gap: 2px 14px;
1523
+ border-bottom: 1px solid var(--tita-line);
1524
+ padding: 0 0 7px;
1525
+ }
1526
+ .tita-trialNav a {
1527
+ color: var(--tita-ink-muted);
1528
+ font-size: 11.5px;
1529
+ font-weight: 650;
1530
+ text-decoration: none;
1531
+ }
1532
+ .tita-trialNav a:hover { color: var(--tita-primary); text-decoration: underline; text-underline-offset: 3px; }
1533
+ .tita-trialNav a:focus-visible { outline: 2px solid var(--tita-focus-ring); outline-offset: 2px; border-radius: 4px; }
1534
+ /* 一段说明(不是警告)。警告用 amber,这条是「报告的范围边界」,用中性底色。 */
1535
+ .tita-trialNote {
1536
+ margin: 0 0 10px;
1537
+ border-radius: 8px;
1538
+ background: var(--tita-surface-muted);
1539
+ padding: 8px 10px;
1540
+ color: var(--tita-ink-muted);
1541
+ font-size: 11px;
1542
+ line-height: 1.6;
1543
+ }
1544
+ .tita-trialNote b { color: var(--tita-ink); }
1545
+ /* 锚点落点留一口气:`scroll-margin` 让标题不贴住吸顶的导航条。 */
1546
+ .tita-trialSection, .tita-trialOverview { scroll-margin-block-start: 44px; }
1547
+
1500
1548
  .tita-trialReport { display: grid; gap: 22px; }
1501
1549
  .tita-trialOverview { display: grid; gap: 9px; border-bottom: 1px solid var(--tita-line-strong); padding-bottom: 13px; }
1502
1550
  .tita-trialSection { display: grid; min-width: 0; gap: 10px; border-top: 1px solid var(--tita-line); padding-top: 14px; }
@@ -1514,6 +1562,7 @@ button.tita-trialMatrixGroup:focus-visible { outline: 2px solid var(--tita-focus
1514
1562
  .tita-trialOverviewTitle { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; margin: 0; }
1515
1563
  .tita-trialOverviewTitle strong { color: var(--tita-ink); font-size: 11.5px; font-weight: 700; overflow-wrap: anywhere; }
1516
1564
  .tita-trialOverviewTitle span { color: var(--tita-ink-muted); font-size: 10px; font-variant-numeric: tabular-nums; }
1565
+ .tita-trialOverviewEmpty { margin: 2px 0 0; color: var(--tita-ink-muted); font-size: 11.5px; line-height: 1.6; }
1517
1566
  .tita-trialOverviewSubject { margin: 0; color: var(--tita-ink-muted); font-size: 11px; line-height: 1.6; }
1518
1567
  .tita-trialOutputTabs { display: grid; min-width: 0; gap: 10px; }
1519
1568
  .tita-trialOutputTabs > [data-slot='tabs-list'] { max-width: 100%; overflow-x: auto; }
@@ -1616,6 +1665,10 @@ button.tita-trialMatrixGroup:focus-visible { outline: 2px solid var(--tita-focus
1616
1665
  .tita-trialRunFlags { display: flex; flex: 1 1 auto; flex-wrap: wrap; align-items: center; gap: 4px; }
1617
1666
  .tita-trialRunFlag { border-radius: 999px; background: var(--tita-surface-muted); color: var(--tita-ink); padding: 1px 6px; font-size: 9.5px; font-weight: 700; }
1618
1667
  .tita-trialRunFlag[data-ok="false"] { background: var(--tita-tone-amber-tint); color: var(--tita-tone-amber); }
1668
+ /* 阶段徽标(2026-10-18)。同一条列表里现在混着抽取与归一,两个都是「这一份文档的一次运行」,
1669
+ 所以它们必须是同一枚小徽标、只换颜色 —— 换成两种形状会读成两类东西。 */
1670
+ .tita-trialRunFlag[data-stage="normalize"] { background: var(--tita-primary-soft); color: var(--tita-primary); }
1671
+ .tita-trialRunFlag[data-stage="materialize"] { background: var(--tita-tone-violet-tint); color: var(--tita-tone-violet); }
1619
1672
  /* 行尾的外链图标:推到这一行最右,它是「点这里去浏览器」的记号。 */
1620
1673
  .tita-trialRunOpen { display: inline-flex; margin-left: auto; color: var(--tita-ink-faint); }
1621
1674
  .tita-trialRunRow:hover .tita-trialRunOpen { color: var(--tita-ink); }
@@ -1746,8 +1799,10 @@ button.tita-trialMatrixGroup:focus-visible { outline: 2px solid var(--tita-focus
1746
1799
  * 与 `.tita-knowledgeConfirm` 同一条理由。做成无边框小字而不是按钮:这一行里三百来个
1747
1800
  * 像素都归正文,动作是**注脚**,不该摆一个跟正文抢眼的东西。
1748
1801
  */
1749
- .tita-knowledgeOpen { justify-self: end; margin-top: 2px; border: 0; background: none; padding: 0; color: var(--tita-accent); font: inherit; font-size: 10px; cursor: pointer; text-decoration: underline; text-underline-offset: 2px; }
1750
- .tita-knowledgeOpen:hover { color: var(--tita-accent-strong, var(--tita-accent)); }
1802
+ .tita-knowledgeOpen { justify-self: end; margin-top: 2px; border: 0; background: none; padding: 0; color: var(--tita-primary); font: inherit; font-size: 10px; cursor: pointer; text-decoration: underline; text-underline-offset: 2px; }
1803
+ /* 悬停原来指着一对**没有定义**的变量,而 `var()` 失效时整条 `color` 声明作废 ——
1804
+ 这就是「悬停完全没有反馈」的原因。现在两档都是真 token:主色 → 它的深色档。 */
1805
+ .tita-knowledgeOpen:hover { color: var(--tita-primary-hover); }
1751
1806
  .tita-knowledgeSources { display: grid; margin: 2px 0 0; padding: 0; list-style: none; gap: 4px; }
1752
1807
  .tita-knowledgeNote { margin-top: 8px; border-top: 1px dashed var(--tita-line); padding-top: 8px; }
1753
1808
  /*
@@ -1,3 +1,4 @@
1
+ import type { MaterializeTrajectoryReport, NormalizeTrajectoryReport, TrajectoryStage } from '../trajectory-types.js'
1
2
  import { requestJson } from './request.ts'
2
3
 
3
4
  /**
@@ -43,6 +44,11 @@ import type { TrialReport } from '../wiki-trial-types.ts'
43
44
  */
44
45
  export type TrialReportView = TrialReport & {
45
46
  summary?: string
47
+ /**
48
+ * `'extract-only'` = 这条记录是**流水线的抽取阶段**落的,报告里 ②③④ 段是试跑顺带算的
49
+ * 机械预览,不是这个库里真的做过的事。没有这个键 = 旧的全链路编译,四段都真跑过。
50
+ */
51
+ scope?: 'extract-only'
46
52
  /** 这一次的轨迹落库结果。`saved: false` 时 `why` 要显示出来 —— 不能假装存下来了。 */
47
53
  run?: { saved: boolean; id?: string; why?: string }
48
54
  /**
@@ -72,11 +78,31 @@ export interface ExtractionRunSummary {
72
78
  modelWhy?: string
73
79
  summary: string
74
80
  hasReport: boolean
81
+ /** 这条轨迹属于哪一段(2026-10-18 起三段共用一张表)。 */
82
+ stage: TrajectoryStage
75
83
  }
76
84
 
85
+ /**
86
+ * 三份轨迹的报告本体,按 `stage` 分派。
87
+ *
88
+ * 它们**不共用一个形状**:抽取是整份 `TrialReport`(七步、矩阵、四条核验),
89
+ * 归一是一张「名字 → 坐标」的账,物化是「这一组 → 这一张卡」的账。
90
+ * 硬塞进一个形状只会让三份都变得半空。
91
+ */
92
+ export type TrajectoryReportView = TrialReportView | NormalizeTrajectoryReport | MaterializeTrajectoryReport
93
+
94
+ /**
95
+ * 这条记录属于哪一段。
96
+ *
97
+ * 「缺字段 = 抽取」是**与库同一条规则**(补 `stage` 列时默认值就是 `extract`):
98
+ * 升级后服务端还没起来的那一小段窗口里,接口给的是旧形状;这时把它读成「没有轨迹」
99
+ * 会让列表整片空白,而它其实就在那儿。一处收口,别处不用各写一遍 `?? 'extract'`。
100
+ */
101
+ export const trajectoryStageOf = (run: { stage?: TrajectoryStage }): TrajectoryStage => run.stage ?? 'extract'
102
+
77
103
  /** 一条完整轨迹:列表投影 + 报告本体。 */
78
104
  export interface ExtractionRunRecord extends ExtractionRunSummary {
79
- report: TrialReportView
105
+ report: TrajectoryReportView
80
106
  }
81
107
 
82
108
  export interface ExtractionRunPage {
@@ -123,10 +149,13 @@ export async function requestTrial(input: {
123
149
  export async function requestExtractionRuns(input: {
124
150
  kb: string
125
151
  docId?: string
152
+ /** 只看某一段。省略 = 三段一起。 */
153
+ stage?: TrajectoryStage
126
154
  limit?: number
127
155
  }): Promise<ExtractionRunPage> {
128
156
  const params = new URLSearchParams({ kb: input.kb })
129
157
  if (input.docId) params.set('doc_id', input.docId)
158
+ if (input.stage) params.set('stage', input.stage)
130
159
  if (input.limit) params.set('limit', String(input.limit))
131
160
  const envelope = await requestJson<ExtractionRunPage>(`/knowledge/extraction-runs?${params.toString()}`)
132
161
  return envelope.data ?? { runs: [], total: 0, available: false }
@@ -506,6 +506,7 @@ export function applyConvert(ctx: Context, deps: ConvertHostDeps): ConvertHost {
506
506
  convert_task_id: { type: 'string', required: true },
507
507
  kb: { type: 'string', description: '目标知识库 id;省略则写入默认知识库' },
508
508
  title: { type: 'string' },
509
+ source_id: { type: 'string', description: '写进这一条已有的 SourceRaw(同一实体的新版本)。省略则按 external_id / 内容算一个新来源 —— 给一份「已经录进库的文档重新转换」时应当传它,否则会多出一条来源。' },
509
510
  plan_only: { type: 'boolean' },
510
511
  session_key: { type: 'string' },
511
512
  task_id: { type: 'string' },
@@ -531,6 +532,7 @@ export function applyConvert(ctx: Context, deps: ConvertHostDeps): ConvertHost {
531
532
  runtime: target.runtime,
532
533
  knowledgeRoot: target.knowledgeRoot,
533
534
  ...(typeof args.title === 'string' && args.title.trim() ? { title: args.title.trim() } : {}),
535
+ ...(typeof args.source_id === 'string' && args.source_id.trim() ? { sourceId: args.source_id.trim() } : {}),
534
536
  planOnly: args.plan_only === true,
535
537
  ...(lineageOf(args, exec) ? { lineage: lineageOf(args, exec)! } : {}),
536
538
  })
@@ -585,6 +587,7 @@ export function applyConvert(ctx: Context, deps: ConvertHostDeps): ConvertHost {
585
587
  runtime: target.runtime,
586
588
  knowledgeRoot: target.knowledgeRoot,
587
589
  ...(typeof body.title === 'string' && body.title.trim() ? { title: body.title.trim() } : {}),
590
+ ...(typeof body.source_id === 'string' && body.source_id.trim() ? { sourceId: body.source_id.trim() } : {}),
588
591
  planOnly: body.plan_only === true,
589
592
  ...(body.lineage && typeof body.lineage === 'object' ? { lineage: body.lineage as Record<string, string> } : {}),
590
593
  })
@@ -19,7 +19,9 @@
19
19
  */
20
20
 
21
21
  import { ASSETS_DIR, SOURCE_RAW_DIR } from '../layout.js'
22
- import { cp, mkdir, rm, stat } from 'node:fs/promises'
22
+ import { createHash } from 'node:crypto'
23
+ import { constants } from 'node:fs'
24
+ import { copyFile, mkdir, readFile, rm, stat } from 'node:fs/promises'
23
25
  import { basename, dirname, join, resolve, sep } from 'node:path'
24
26
  import type { ConvertService, IngestPayload } from 'dsh-convert-core'
25
27
  import type { LineageContext } from 'dsh-data-contracts/refs'
@@ -66,6 +68,15 @@ export interface IngestOptions {
66
68
  /** Absolute knowledge root the runtime currently writes to. */
67
69
  knowledgeRoot: string
68
70
  title?: string
71
+ /**
72
+ * 写进**哪一条** SourceRaw(0070)。
73
+ *
74
+ * 省略时 `addSource` 按 `external_id ?? content` 算一个新 id —— 于是「把一份已经录进库
75
+ * 的文档重新转换一次(比如这次补上了它的图)」会**多出一条来源**,而旧那条仍挂着断引用、
76
+ * 它的卡片与知识也还指着旧 id。给出 `sourceId` 时走的是同一实体的新版本:
77
+ * 历史保留、卡片与知识的出处不变、正文与图片引用一起更新。
78
+ */
79
+ sourceId?: string
69
80
  metadata?: Record<string, unknown>
70
81
  lineage?: Record<string, unknown>
71
82
  /** Dry run: build the plan without copying assets or writing the source. */
@@ -178,6 +189,40 @@ function assetImageRef(hash: string, rel: string): string {
178
189
  * @param created 这一趟**新写进磁盘**的路径。被 `addSource` 拒收时调用方要按它回收 ——
179
190
  * 底稿和图片一样,先落盘后写库,不回收就会留下谁也指不到的文件。
180
191
  */
192
+ /**
193
+ * 抢占式复制:返回**这一次是否真的由我创建了目标**。
194
+ *
195
+ * 用 `copyFile` + `COPYFILE_EXCL`,不用 `cp({errorOnExist:true})` —— 后者抛的是
196
+ * `ERR_FS_CP_EEXIST`(实测 Node 24),而这里判的是 `EEXIST`,于是「已存在」会被当成
197
+ * 复制失败:资产不进 `assetMapping`,正文里的引用就永久断掉。而 `copyFile` 的
198
+ * `COPYFILE_EXCL` 抛的正是 `EEXIST`。
199
+ */
200
+ async function copyExclusive(from: string, to: string): Promise<boolean> {
201
+ try {
202
+ await copyFile(from, to, constants.COPYFILE_EXCL)
203
+ return true
204
+ } catch (cause) {
205
+ if ((cause as NodeJS.ErrnoException).code === 'EEXIST') return false
206
+ throw cause
207
+ }
208
+ }
209
+
210
+ /**
211
+ * 目标已经存在时,确认它就是**同一份内容**。
212
+ *
213
+ * 桶名是内容哈希的前 12 位,所以正常情况下「已存在」= 同一份文件。但前缀只有 12 位,
214
+ * 而同名也可能来自别的来源:直接采信会把别人的字节记成自己的(大小与哈希都对不上却
215
+ * 静默通过)。核对不过就保留已有那一份,并如实说一句。
216
+ */
217
+ async function contentMatches(path: string, expected: { bytes: number; hash: string; algorithm: 'sha1' | 'sha256' }): Promise<boolean> {
218
+ try {
219
+ const info = await stat(path)
220
+ if (info.size !== expected.bytes) return false
221
+ const actual = createHash(expected.algorithm).update(await readFile(path)).digest('hex')
222
+ return actual === expected.hash
223
+ } catch { return false }
224
+ }
225
+
181
226
  async function placeOriginal(
182
227
  knowledgeRoot: string,
183
228
  original: { rel: string; path: string; bytes: number; sha256: string },
@@ -190,10 +235,20 @@ async function placeOriginal(
190
235
  if (target === undefined) return undefined
191
236
  try {
192
237
  await mkdir(dirname(target), { recursive: true })
193
- const existed = await stat(target).then(() => true, () => false)
194
- await cp(original.path, target, { force: true })
238
+ /**
239
+ * 「是不是我建的」由文件系统回答,不能靠 `stat` 之后自己判断(0070)。
240
+ *
241
+ * 同一份原件并发导入两次时,两次 `stat` 都会说「不存在」,两次都会把自己记成创建者;
242
+ * 其中一次被 `addSource` 拒收时,`removeCreated` 就会删掉另一次正在引用的底稿。
243
+ * `COPYFILE_EXCL` 让「创建」变成原子的:只有一个赢家,输的那个知道不是自己建的。
244
+ */
245
+ const createdHere = await copyExclusive(original.path, target)
246
+ if (!createdHere && !await contentMatches(target, { bytes: original.bytes, hash: original.sha256, algorithm: 'sha256' })) {
247
+ // 同桶同名但不是同一份内容:不覆盖、也不把它记成自己的底稿。
248
+ return undefined
249
+ }
195
250
  const info = await stat(target)
196
- if (!existed) created.push(target)
251
+ if (createdHere) created.push(target)
197
252
  return {
198
253
  rel: `${SOURCE_RAW_DIR}/${bucket}/${basename(original.rel)}`,
199
254
  target,
@@ -208,8 +263,9 @@ async function placeOriginal(
208
263
  /**
209
264
  * 把 store 拒绝的那一趟**新落盘的文件**收回去(图片与底稿一起)。
210
265
  *
211
- * 判据是「复制前不存在」:桶名是内容哈希,同一份内容重复导入会落在同一个桶里 ——
212
- * 删它等于删别人的图、别人的底稿。
266
+ * 判据是「**这一次真的由我创建**」:桶名是内容哈希,同一份内容重复导入会落在同一个桶里 ——
267
+ * 删别人的图、别人的底稿是不可逆的。所以 `created` 只收 `COPYFILE_EXCL` 抢到的那些,
268
+ * 而不是「复制前 `stat` 说没有」的那些(后者在并发下两边都会成立)。
213
269
  */
214
270
  async function removeCreated(paths: string[]): Promise<void> {
215
271
  for (const path of paths) {
@@ -307,11 +363,18 @@ export async function ingestDelivery(options: IngestOptions): Promise<Envelope<I
307
363
  continue
308
364
  }
309
365
  try {
310
- const existed = await stat(target).then(() => true, () => false)
311
- await cp(asset.path, target, { force: true })
366
+ /** 同 `placeOriginal`:创建权由 `COPYFILE_EXCL` 决定,不由 `stat` 猜。 */
367
+ const createdHere = await copyExclusive(asset.path, target)
368
+ /**
369
+ * 已存在时核对内容。**不能核对失败就跳过这张图** —— 那会让正文里的引用断掉
370
+ * (这正是本函数要避免的)。同一份内容照旧登记;不同内容才保留已有那份并说一句。
371
+ */
372
+ if (!createdHere && !await contentMatches(target, { bytes: asset.bytes, hash: asset.sha1, algorithm: 'sha1' })) {
373
+ warnings.push(`资产 ${asset.rel} 已存在且内容与本次不同,保留已有那一份`)
374
+ }
312
375
  const info = await stat(target)
313
376
  placed.push({ rel: asset.rel, target, bytes: info.size })
314
- if (!existed) created.push(target)
377
+ if (createdHere) created.push(target)
315
378
  assetMapping.set(asset.rel, asset.rel)
316
379
  } catch (cause) {
317
380
  warnings.push(`资产复制失败(${asset.rel}):${cause instanceof Error ? cause.message : String(cause)}`)
@@ -391,6 +454,7 @@ export async function ingestDelivery(options: IngestOptions): Promise<Envelope<I
391
454
  externalId,
392
455
  metadata,
393
456
  provenance,
457
+ ...(options.sourceId?.trim() ? { sourceId: options.sourceId.trim() } : {}),
394
458
  ...(options.lineage ? { lineage: options.lineage as LineageContext } : {}),
395
459
  })
396
460
 
@@ -7,7 +7,7 @@ import type {
7
7
  } from './types.js'
8
8
 
9
9
  /**
10
- * 诊断卡片的**编解码**:markdown 卡片 ⇄ 图上的节点与边(0021 §6.3)。
10
+ * 诊断域 WikiCard 的**编解码**:卡片 ⇄ 图上的可复用节点与关系(0021 §6.3)。
11
11
  *
12
12
  * ## 为什么这一层必须存在
13
13
  *
@@ -16,8 +16,8 @@ import type {
16
16
  * 编译器连 `diagnosis/hypothesis` 这个 entity_type 都不认 —— 于是它既不会被 wikiNote 化,
17
17
  * 也不会进向量索引,**召回永远召不到它**。而「遇到类似问题先召回」正是这个产品的主张。
18
18
  *
19
- * 这一层把图变成卡片的**投影**:卡片是手写的(人写、Agent 提议后由人确认),
20
- * 图是算出来的。§4「存图、显树」讲的是同一件事 —— 存的是知识,图是它的一种读法。
19
+ * 这一层把预先整理好的可复用诊断 Knowledge 投影为 WikiCard。新排查记录走 SourceRaw,
20
+ * Case 由读取层派生;`diagnosis/case` 只保留旧格式读取与迁移兼容。
21
21
  *
22
22
  * ## 卡片三段式与「谁由卡片定义」的边界
23
23
  *
@@ -31,9 +31,10 @@ import type {
31
31
  *
32
32
  * | 图上的东西 | 谁定义 | 为什么 |
33
33
  * | --- | --- | --- |
34
- * | symptom / hypothesis / check_point / failure_reason / case 节点 | **卡片** | 这五类是人确认过的知识(§6.4) |
34
+ * | symptom / hypothesis / check_point / failure_reason 节点 | **WikiCard** | 这些是可复用知识(§6.4) |
35
35
  * | asset 节点 | 引用反推 | 它随数据目录同步,不手写;卡片里只留 id |
36
- * | 现象→假设 / 假设→检查点 / 假设→失败原因 / 检查点→资产 / 资产→假设 / 案例→现象 边 | **卡片** | 都是卡片字段的确定性投影 |
36
+ * | 现象→假设 / 假设→检查点 / 假设→失败原因 / 检查点→资产 / 资产→假设 边 | **WikiCard** | 都是卡片字段的确定性投影 |
37
+ * | 历史 case 节点与案例→现象边 | **旧 WikiCard** | 仅为旧库迁移与读取兼容;新 Case 由排查 SourceRaw 派生 |
37
38
  * | evidence / cause 节点,produces / supports / refutes / confirmed_as 边 | **一次排查** | 它们是一次执行的结果,不是知识;由 `01_source_raw/agent_diagnosis_case/` 那条 SourceRaw 读时解析出来 |
38
39
  *
39
40
  * 最后一行不是妥协,是分层:把「上次查到什么」写进卡片,等于让一份可复用的知识
@@ -286,9 +287,13 @@ function splitSections(body: string): DiagnosisCardSection[] {
286
287
  /* ─────────────────────────────────────────────── 图 → 卡片 ── */
287
288
 
288
289
  /**
289
- * 类型层与案例 → 卡片。
290
+ * 类型层与历史 Case 卡 → 卡片的兼容编解码器。
290
291
  *
291
- * `asset` / `evidence` / `cause` 节点**不产出卡片**:它们不是手确认过的知识(§6.4)。
292
+ * 新样例会用它写入预先整理的可复用诊断 Knowledge;旧 Case 卡仅用于读取和兼容迁移。
293
+ * 新排查不会把 Case 写成 WikiCard。
294
+ *
295
+ * `case`、`asset` / `evidence` / `cause` 节点**不产出新卡片**:前者是排查记录视图,
296
+ * 后三者不是可复用 Knowledge(§6.4)。
292
297
  * 被引用的资产会在反序列化时按 id 补一个最小节点回来。
293
298
  */
294
299
  export function graphToCards(graph: AttributionGraphData, cases: AttributionCase[] = []): DiagnosisCard[] {
@@ -395,7 +400,7 @@ export interface CompiledDiagnosis {
395
400
  /**
396
401
  * 卡片 → 图。
397
402
  *
398
- * 只产出「卡片定义得了」的那部分(类型层 + 引用层的资产 + 案例,以及六种卡片边);
403
+ * 只产出当前 WikiCard 定义得了的部分(Knowledge 类型 + 引用层资产及其关系);历史 Case 卡仅用于读取兼容;
399
404
  * `evidence` / `cause` 与四条执行边由一次排查写入(`agent_diagnosis_case` 的 SourceRaw),
400
405
  * 不在这里合成 —— 否则一次执行的结果会被伪装成可复用的知识。
401
406
  */
@@ -1,5 +1,5 @@
1
1
  import type {
2
- AttributionEdge, AttributionGraphData, AttributionNode, CasePathStep, DiagnosisRunEvidence, DiagnosisRunRecord,
2
+ AttributionCase, AttributionEdge, AttributionGraphData, AttributionNode, CasePathStep, DiagnosisRunEvidence, DiagnosisRunRecord,
3
3
  } from './types.js'
4
4
  import type { DiagnosisCard } from './cards.js'
5
5
 
@@ -12,15 +12,15 @@ import type { DiagnosisCard } from './cards.js'
12
12
  * (分类、版本、检索),就得是一条 SourceRaw。自造一个 jsonl 格式,等于在知识库旁边
13
13
  * 再养一套只读不进链路的数据 —— 这也是 0021 §0.3 撤销 `05_diagnosis/` 的直接原因。
14
14
  *
15
- * ## 它与案例卡片的区别
15
+ * ## 它与排查视图的关系
16
16
  *
17
17
  * | | 谁写 | 性质 |
18
18
  * | --- | --- | --- |
19
19
  * | 这条 SourceRaw(`agent_diagnosis_case`) | **Agent**,每次排查结束自动写 | 跑出来的**过程记录** |
20
- * | 案例卡片(`diagnosis/case`) | Agent 提议 → **人确认** | 归纳出来的**结论** |
20
+ * | 排查视图(Case) | 从这条记录读取 | 只读展示,不再生成第二张 WikiCard |
21
21
  *
22
- * 同一件事可以有两条记录,它们不是重复:一个回答「当时怎么走的」,一个回答「结论是什么」。
23
- * 而案例卡片必须经人确认 —— 它落在类型层收敛的那一侧,见 0021 §6.1。
22
+ * Event 与 Knowledge 在归一层并列;WikiCard 由 Knowledge 组织生成,并可指向 Event。
23
+ * Case 仅是一次排查记录的展示视图,不是新的持久化实体。
24
24
  *
25
25
  * ## 三段式
26
26
  *
@@ -212,18 +212,28 @@ export function runReferenceProblems(record: DiagnosisRunRecord, knownIds: Reado
212
212
  return problems
213
213
  }
214
214
 
215
- /**
216
- * 把一次排查归纳成一张**案例卡片**(还只是提议,要人确认才生效)。
217
- *
218
- * 归纳这一步是「过程记录 → 可复用知识」的那一跳,也是唯一需要人看的一跳:
219
- * 路径里哪几步其实是误导、根因一句话怎么写,都是判断题。
220
- */
221
- /**
222
- * 一次排查的案例卡片草案(0044 前叫 `caseProposalOf`)。
223
- *
224
- * 改名是因为它产出的东西**直接落卡**,不再是一条提议:名字里留着 `Proposal` 会让下一个
225
- * 读的人继续在两个时代之间猜。函数体一行没动 —— 它本来就是「从一条排查记录编出一张案例卡」。
226
- */
215
+ /** Read-only case presentation projected from a diagnosis source record. */
216
+ export function caseViewOfRun(record: DiagnosisRunRecord, symptomTitle: string): AttributionCase {
217
+ const rootCause = record.rootCause?.text ?? '(这次没有确认根因)'
218
+ const firstMisleading = record.misleading?.[0]
219
+ return {
220
+ id: `CASE-${record.runId}`,
221
+ title: symptomTitle,
222
+ symptomId: record.symptomId,
223
+ sections: [
224
+ { heading: '现象', body: `入口现象 \`${record.symptomId}\`(${symptomTitle}),开始于 ${record.startedAt}。` },
225
+ { heading: '排查过程', body: record.path.map(step => `- \`${step.label}\` ${step.note}${step.refuted ? '(否定)' : ''}`).join('\n') || '(没有记录路径)' },
226
+ { heading: '检查结果', body: record.evidence.map(item => `- \`${item.checkPointId}\`:${item.result ?? ''}(${item.verdict})${item.why ? `,${item.why}` : ''}`).join('\n') || '(没有记录检查结果)' },
227
+ { heading: '根因与处理', body: [rootCause, record.rootCause?.fixAction].filter(Boolean).join('\n\n') },
228
+ ],
229
+ path: record.path,
230
+ durationMinutes: record.durationMinutes,
231
+ ...(firstMisleading ? { misleading: firstMisleading } : {}),
232
+ rootCause,
233
+ rootCauseStatus: record.rootCause ? 'confirmed' : 'unconfirmed',
234
+ }
235
+ }
236
+ /** 兼容旧版案例卡片格式的转换器;新排查只写 SourceRaw,不调用此函数。 */
227
237
  export function caseCardOf(record: DiagnosisRunRecord, symptomTitle: string): DiagnosisCard {
228
238
  const rootCause = record.rootCause?.text ?? '(这次没有确认根因)'
229
239
  const firstMisleading = record.misleading?.[0]
@@ -247,8 +247,8 @@ export function validateDiagnosis(graph: AttributionGraphData): DiagnosisIssue[]
247
247
  if (!node.verdict) issues.push(fieldIssue(node, 'verdict', '证据必须给出结论:supports / refutes / inconclusive'))
248
248
  if (!node.why?.trim()) issues.push(fieldIssue(node, 'why', '证据必须说明「为什么这个结果算数或不算数」—— 采信与剔除都得能说理由'))
249
249
  }
250
- // `case` 节点只是图上的桩:案例的正文与路径住在案例卡片里,由
251
- // DiagnosisSnapshot.cases 校验,不在节点上重复要求。
250
+ // `case` 节点只是兼容旧图的桩;当前排查视图由 SourceRaw 记录派生,
251
+ // 新排查不会再写 diagnosis/case WikiCard。
252
252
  }
253
253
 
254
254
  // ── 第二层:引用完整性 ────────────────────────────────────────────────────
@@ -6,11 +6,12 @@
6
6
  * 所以 seed 也走完整条动线:
7
7
  *
8
8
  * ```
9
- * 这些文本(SourceRaw) → 五类卡片 → 编译 → 图
9
+ * 原文(SourceRaw) → 抽取 Knowledge / Event → 归一
10
+ * 排查运行记录(SourceRaw) → 只读 Case 视图
10
11
  * ```
11
12
  *
12
- * 手册与复盘是**人写的散文**(就是下面这些字面量),卡片是从它们编译出来的结构化提取。
13
- * 两者内容会有重叠 —— 那是正常的:卡片是散文的投影,不是它的替代品。
13
+ * 手册与复盘是**人写的散文**(就是下面这些字面量)。载入示例只保存来源与可复用诊断卡;
14
+ * Case 从独立的排查运行记录派生,不生成第二张诊断卡。
14
15
  */
15
16
  import { SEED_CASES, SEED_RUNS } from './seed.js'
16
17
  import { renderRunSource } from './run.js'
@@ -83,7 +84,7 @@ export const SEED_HANDBOOK = `# 日报指标异常排查手册(内部版)
83
84
  同一批假设被反复验证,而永远不会被剪掉 —— 图上会长出一堆僵尸边。
84
85
  `
85
86
 
86
- /** 三篇复盘:案例卡片的出处。正文就是案例的散文段(现象 / 影响面 / 排查过程 / 根因 / 修复)。 */
87
+ /** 三篇复盘:供后续抽取 Knowledge / Event 的来源文档。 */
87
88
  export function seedCaseSources(): SeedSource[] {
88
89
  return SEED_CASES.map(item => ({
89
90
  id: item.id,
@@ -19,7 +19,7 @@ import type { AttributionCase, AttributionGraphData, DiagnosisRunRecord } from '
19
19
  * | `question` | 人 | 手册与复盘 —— 卡片 `source_refs` 指向这里 |
20
20
  * | `agent_diagnosis_case` | Agent | 一次排查的过程记录(`SEED_RUNS`) |
21
21
  *
22
- * `SEED_GRAPH` 里**只有卡片定义得了的东西**:类型层、引用层资产、案例桩,以及六种卡片边。
22
+ * `SEED_GRAPH` 里有可复用诊断 Knowledge 与引用层资产;历史案例桩只用于旧格式兼容夹具。
23
23
  * `evidence` / `cause` 与四条执行边由 `SEED_RUNS` 读时解析出来 —— 把「上次查到什么」
24
24
  * 写进卡片,等于让一份可复用的知识绑死在一次执行上。
25
25
  */
@@ -122,10 +122,8 @@ export const SEED_GRAPH: AttributionGraphData = {
122
122
  { id: 'metric_daily_gmv', kind: 'asset', layer: 'reference', title: 'metric_daily_gmv', assetType: 'metric' },
123
123
  { id: 'bi_daily_dashboard', kind: 'asset', layer: 'reference', title: 'bi_daily_dashboard', assetType: 'linkage' },
124
124
 
125
- // ── 实例层:案例节点是**图上的桩**(正文与有序路径住在案例卡片里) ──────
126
- // 标题必须与 `SEED_CASES[].title` 是同一个字符串,否则同一份知识会有两份标题
127
- // (而且还真的不一致过:「只剩」vs「只有」)。画布要在标签前显示案例 id,
128
- // 那是渲染层的事(见 `nodeStyleFor` 的 labelText)。
125
+ // ── 兼容层:旧格式里的案例节点桩。新样例的排查记录由 SEED_RUNS 派生为只读视图。 ──
126
+ // 保留稳定 ID,供旧卡片编解码与历史图迁移夹具使用。
129
127
  { id: 'CASE-001', kind: 'case', layer: 'instance', title: '日报 GMV 为 0' },
130
128
  { id: 'CASE-002', kind: 'case', layer: 'instance', title: '订单数只有昨日 12%' },
131
129
  { id: 'CASE-003', kind: 'case', layer: 'instance', title: 'DAU 突增 3.1 倍' },
@@ -197,7 +195,7 @@ export const SEED_GRAPH: AttributionGraphData = {
197
195
  ],
198
196
  }
199
197
 
200
- /** 三个 seed 案例卡片,与上面图谱里的节点 id 对齐(0021 §11)。 */
198
+ /** 旧版示例 Case 投影夹具;仅供历史卡片编解码与来源文案使用,当前载入流程不落盘。 */
201
199
  export const SEED_CASES: AttributionCase[] = [
202
200
  {
203
201
  id: 'CASE-001',
@@ -280,8 +278,8 @@ export const SEED_CASES: AttributionCase[] = [
280
278
  * 三次排查的**过程记录**(`agent_diagnosis_case`)。
281
279
  *
282
280
  * 它们与 `SEED_CASES` 一一对应,但**不是同一份东西**:这里是 Agent 跑出来的原始记录
283
- * (每条检查点真跑了什么、看到什么、这个结果算不算数),`SEED_CASES` 是之后归纳出来的
284
- * 案例卡片(要经人确认才生效)。一个回答「当时怎么走的」,一个回答「结论是什么」。
281
+ * (每条检查点真跑了什么、看到什么、这个结果算不算数);当前 Case 视图从运行记录派生,
282
+ * `SEED_CASES` 仅保留作旧卡片格式夹具。
285
283
  *
286
284
  * `E7` 是 CASE-001 里那次**贵的否定**:T7.2 直连明细对账推翻了 H7。它不在案例的
287
285
  * 主路径上(主路径早就确认了 H1),但它是这次排查最贵的一条知识 —— 40 分钟的误导段