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

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 (172) hide show
  1. package/README.md +84 -52
  2. package/cordis.patch.yml +21 -17
  3. package/eval/benchmark-extraction-format.mjs +20 -0
  4. package/lib/client.js +2846 -3872
  5. package/lib/doc-page.js +1 -1
  6. package/lib/pages.js +1299 -1660
  7. package/lib/types/axis-registry.d.ts +6 -0
  8. package/lib/types/axis-registry.d.ts.map +1 -1
  9. package/lib/types/axis-registry.js +45 -0
  10. package/lib/types/axis-registry.js.map +1 -1
  11. package/lib/types/diagnosis/cards.d.ts +7 -3
  12. package/lib/types/diagnosis/cards.d.ts.map +1 -1
  13. package/lib/types/diagnosis/cards.js +13 -8
  14. package/lib/types/diagnosis/cards.js.map +1 -1
  15. package/lib/types/diagnosis/run.d.ts +4 -13
  16. package/lib/types/diagnosis/run.d.ts.map +1 -1
  17. package/lib/types/diagnosis/run.js +26 -16
  18. package/lib/types/diagnosis/run.js.map +1 -1
  19. package/lib/types/diagnosis/schema.js +2 -2
  20. package/lib/types/diagnosis/schema.js.map +1 -1
  21. package/lib/types/diagnosis/seed-source.d.ts +1 -1
  22. package/lib/types/diagnosis/seed-source.d.ts.map +1 -1
  23. package/lib/types/diagnosis/seed-source.js +5 -4
  24. package/lib/types/diagnosis/seed-source.js.map +1 -1
  25. package/lib/types/diagnosis/seed.d.ts +4 -4
  26. package/lib/types/diagnosis/seed.d.ts.map +1 -1
  27. package/lib/types/diagnosis/seed.js +6 -8
  28. package/lib/types/diagnosis/seed.js.map +1 -1
  29. package/lib/types/diagnosis/store.d.ts +8 -16
  30. package/lib/types/diagnosis/store.d.ts.map +1 -1
  31. package/lib/types/diagnosis/store.js +41 -66
  32. package/lib/types/diagnosis/store.js.map +1 -1
  33. package/lib/types/diagnosis/types.d.ts +10 -16
  34. package/lib/types/diagnosis/types.d.ts.map +1 -1
  35. package/lib/types/diagnosis/types.js.map +1 -1
  36. package/lib/types/environment/service.d.ts +4 -3
  37. package/lib/types/environment/service.d.ts.map +1 -1
  38. package/lib/types/environment/service.js +91 -23
  39. package/lib/types/environment/service.js.map +1 -1
  40. package/lib/types/environment/types.d.ts +9 -0
  41. package/lib/types/environment/types.d.ts.map +1 -1
  42. package/lib/types/events.d.ts +2 -0
  43. package/lib/types/events.d.ts.map +1 -1
  44. package/lib/types/events.js +13 -0
  45. package/lib/types/events.js.map +1 -1
  46. package/lib/types/extract-contract.d.ts +1 -1
  47. package/lib/types/extract-contract.d.ts.map +1 -1
  48. package/lib/types/extract-contract.js +34 -31
  49. package/lib/types/extract-contract.js.map +1 -1
  50. package/lib/types/extract-model.d.ts +2 -0
  51. package/lib/types/extract-model.d.ts.map +1 -1
  52. package/lib/types/extract-model.js.map +1 -1
  53. package/lib/types/extract-table-contract.d.ts +3 -0
  54. package/lib/types/extract-table-contract.d.ts.map +1 -0
  55. package/lib/types/extract-table-contract.js +30 -0
  56. package/lib/types/extract-table-contract.js.map +1 -0
  57. package/lib/types/extract-table.d.ts +8 -0
  58. package/lib/types/extract-table.d.ts.map +1 -0
  59. package/lib/types/extract-table.js +151 -0
  60. package/lib/types/extract-table.js.map +1 -0
  61. package/lib/types/extraction-runs.d.ts +3 -0
  62. package/lib/types/extraction-runs.d.ts.map +1 -1
  63. package/lib/types/extraction-runs.js +6 -0
  64. package/lib/types/extraction-runs.js.map +1 -1
  65. package/lib/types/index.d.ts.map +1 -1
  66. package/lib/types/index.js +196 -44
  67. package/lib/types/index.js.map +1 -1
  68. package/lib/types/intent/scope.d.ts.map +1 -1
  69. package/lib/types/intent/scope.js +1 -0
  70. package/lib/types/intent/scope.js.map +1 -1
  71. package/lib/types/knowledge-bases.d.ts.map +1 -1
  72. package/lib/types/knowledge-bases.js +3 -2
  73. package/lib/types/knowledge-bases.js.map +1 -1
  74. package/lib/types/knowledge.d.ts +3 -0
  75. package/lib/types/knowledge.d.ts.map +1 -1
  76. package/lib/types/knowledge.js +12 -3
  77. package/lib/types/knowledge.js.map +1 -1
  78. package/lib/types/model-text.d.ts +1 -1
  79. package/lib/types/model-text.d.ts.map +1 -1
  80. package/lib/types/model-text.js +5 -4
  81. package/lib/types/model-text.js.map +1 -1
  82. package/lib/types/pipeline/engine.d.ts +57 -0
  83. package/lib/types/pipeline/engine.d.ts.map +1 -0
  84. package/lib/types/pipeline/engine.js +268 -0
  85. package/lib/types/pipeline/engine.js.map +1 -0
  86. package/lib/types/pipeline/store.d.ts +56 -0
  87. package/lib/types/pipeline/store.d.ts.map +1 -0
  88. package/lib/types/pipeline/store.js +115 -0
  89. package/lib/types/pipeline/store.js.map +1 -0
  90. package/lib/types/pipeline/types.d.ts +82 -0
  91. package/lib/types/pipeline/types.d.ts.map +1 -0
  92. package/lib/types/pipeline/types.js +5 -0
  93. package/lib/types/pipeline/types.js.map +1 -0
  94. package/lib/types/present.d.ts +2 -0
  95. package/lib/types/present.d.ts.map +1 -1
  96. package/lib/types/present.js +9 -4
  97. package/lib/types/present.js.map +1 -1
  98. package/lib/types/runtime.d.ts +20 -1
  99. package/lib/types/runtime.d.ts.map +1 -1
  100. package/lib/types/runtime.js +389 -112
  101. package/lib/types/runtime.js.map +1 -1
  102. package/lib/types/source-evidence.d.ts.map +1 -1
  103. package/lib/types/source-evidence.js +2 -0
  104. package/lib/types/source-evidence.js.map +1 -1
  105. package/lib/types/store/axis-references.js +3 -3
  106. package/lib/types/store/axis-references.js.map +1 -1
  107. package/lib/types/store/migrate.js +1 -1
  108. package/lib/types/store/migrate.js.map +1 -1
  109. package/lib/types/types.d.ts +3 -4
  110. package/lib/types/types.d.ts.map +1 -1
  111. package/lib/types/types.js.map +1 -1
  112. package/lib/types/wiki-materialize.d.ts.map +1 -1
  113. package/lib/types/wiki-materialize.js +3 -15
  114. package/lib/types/wiki-materialize.js.map +1 -1
  115. package/lib/types/wiki-trial-types.d.ts +3 -0
  116. package/lib/types/wiki-trial-types.d.ts.map +1 -1
  117. package/lib/types/wiki-trial.d.ts +2 -0
  118. package/lib/types/wiki-trial.d.ts.map +1 -1
  119. package/lib/types/wiki-trial.js +31 -7
  120. package/lib/types/wiki-trial.js.map +1 -1
  121. package/package.json +6 -3
  122. package/preset/tita-data-agent/agent.cordis.yml +21 -17
  123. package/src/axis-registry.ts +38 -0
  124. package/src/client/AttributionEventGuide.tsx +10 -9
  125. package/src/client/EnvironmentSetup.tsx +35 -15
  126. package/src/client/ExtractionTasksPage.tsx +129 -176
  127. package/src/client/KnowledgeBaseTab.tsx +31 -76
  128. package/src/client/NodeSettings.tsx +2 -3
  129. package/src/client/PipelineNotice.tsx +26 -0
  130. package/src/client/TabGuide.tsx +4 -4
  131. package/src/client/TypeDefinitionDrawer.tsx +23 -8
  132. package/src/client/WikiCardPreview.tsx +43 -10
  133. package/src/client/WikiFlowPanel.tsx +4 -4
  134. package/src/client/architecture.tsx +1 -1
  135. package/src/client/attribution/AttributionTab.tsx +6 -6
  136. package/src/client/attribution/CaseView.tsx +3 -1
  137. package/src/client/attribution/EventView.tsx +14 -4
  138. package/src/client/attribution/RunView.tsx +2 -2
  139. package/src/client/attribution/graph-options.ts +2 -2
  140. package/src/client/pages.tsx +2 -2
  141. package/src/diagnosis/cards.ts +13 -8
  142. package/src/diagnosis/run.ts +27 -17
  143. package/src/diagnosis/schema.ts +2 -2
  144. package/src/diagnosis/seed-source.ts +5 -4
  145. package/src/diagnosis/seed.ts +6 -8
  146. package/src/diagnosis/store.ts +47 -68
  147. package/src/diagnosis/types.ts +10 -16
  148. package/src/environment/service.ts +79 -23
  149. package/src/environment/types.ts +6 -0
  150. package/src/events.ts +12 -0
  151. package/src/extract-contract.ts +6 -3
  152. package/src/extract-model.ts +2 -0
  153. package/src/extract-table-contract.ts +29 -0
  154. package/src/extract-table.ts +126 -0
  155. package/src/extraction-runs.ts +6 -0
  156. package/src/index.ts +156 -45
  157. package/src/intent/scope.ts +1 -0
  158. package/src/knowledge-bases.ts +3 -2
  159. package/src/knowledge.ts +13 -3
  160. package/src/model-text.ts +5 -3
  161. package/src/pipeline/engine.ts +238 -0
  162. package/src/pipeline/store.ts +119 -0
  163. package/src/pipeline/types.ts +74 -0
  164. package/src/present.ts +10 -4
  165. package/src/runtime.ts +350 -110
  166. package/src/source-evidence.ts +1 -0
  167. package/src/store/axis-references.ts +3 -3
  168. package/src/store/migrate.ts +1 -1
  169. package/src/types.ts +3 -4
  170. package/src/wiki-materialize.ts +3 -14
  171. package/src/wiki-trial-types.ts +3 -0
  172. package/src/wiki-trial.ts +26 -7
@@ -7,7 +7,7 @@ import {
7
7
  cardFromRecord, cardsToGraph, CARD_EDGE_KINDS, graphToCards, parseCard, serializeCard,
8
8
  RUN_EDGE_KINDS, type DiagnosisCard,
9
9
  } from './cards.js'
10
- import { caseCardOf, parseRunSource, renderRunSource, runReferenceProblems, runToGraph } from './run.js'
10
+ import { caseViewOfRun, parseRunSource, renderRunSource, runReferenceProblems, runToGraph } from './run.js'
11
11
  import type {
12
12
  AttributionCase, AttributionEdge, AttributionEvent, AttributionGraphData, AttributionNode, AttributionNodeKind, DiagnosisRunRecord,
13
13
  } from './types.js'
@@ -22,7 +22,7 @@ import type { SchemaType } from '../types.js'
22
22
  * | 目录 | 内容 | 谁是真相 |
23
23
  * | --- | --- | --- |
24
24
  * | `01_source_raw/` | 手册、复盘与排查原文 | SourceRaw 的唯一真相 |
25
- * | `02_wiki/diagnosis/` | 五类卡片(三段式) | **知识侧的唯一真相**,由一次排查 / 载入示例 / 迁移直接写入(0044 起不经人确认) |
25
+ * | `02_wiki/diagnosis/` | 历史诊断卡片 | 仅为旧数据兼容读取;新排查不再写 diagnosis/case 卡片 |
26
26
  *
27
27
  * `05_diagnosis/` 在 v1.1 撤销了(0021 §0.3)。它同时装着两类东西 —— 「一次排查的产物」
28
28
  * (源头)与「五类模板卡」(配置)—— 一个编号段混装两种性质,`rebuild --only` 的语义
@@ -58,8 +58,8 @@ const LEGACY_RUN_SHARDS: Record<string, AttributionNodeKind> = {
58
58
  'causes.jsonl': 'cause',
59
59
  }
60
60
 
61
- /** 读取顺序的显式来源,界面据此说明「现在看到的是哪一份」。 */
62
- export type DiagnosisSource = 'cards' | 'jsonl' | 'empty'
61
+ /** 读取来源,界面据此说明当前展示的是卡片、记录、旧快照还是空库。 */
62
+ export type DiagnosisSource = 'cards' | 'records' | 'jsonl' | 'empty'
63
63
 
64
64
  export interface DiagnosisSnapshot {
65
65
  graph: AttributionGraphData
@@ -119,20 +119,21 @@ export class DiagnosisStore {
119
119
  }
120
120
 
121
121
  /**
122
- * 载入**内置示例**:一份排查手册 + 三篇复盘 + 三次排查日志 → SourceRaw → 卡片 → 图。
122
+ * 载入**内置示例**:一份排查手册 + 三篇复盘 + 三次排查日志 → SourceRaw、可复用诊断卡与只读排查视图。
123
123
  *
124
124
  * 只有用户明确点了「载入内置示例」才走这里(`ensure()` 不再自动调用)。幂等:
125
125
  * `addSource` 按 `externalId`、`writeCard` 按 entityId,点两次不会写出第二份。
126
126
  *
127
- * **它走的是「提议 → 确认」那条门,不是绕过它**(0021 §6.1)。区别只在确认者是谁:
128
- * 用户点「载入内置示例」这个动作本身就是**对整批示例的确认**,所以这里给每条提议记下
129
- * `confirmedBy = user:load-sample`,然后照常走 `writeCard`。这样做的两个好处:
130
- * 待办区里能看到这批卡片是怎么来的;而「未确认不落 `02_wiki/`」这条规则没有被开一个
131
- * 后门 —— 例外是**确认者的身份**,不是门的缺失。
127
+ * 示例里的可复用诊断 Knowledge 是预先整理好的演示数据,直接写入当前知识库。
128
+ * 三条排查记录作为 SourceRaw 写入,并由读取层派生只读 Case 视图;不创建 `diagnosis/case` WikiCard。
132
129
  */
133
130
  async loadSample(): Promise<{ seeded: boolean; migrated: boolean; problems: string[] }> {
134
131
  const { problems: sourceProblems, refs } = await this.writeSources(seedSources())
135
- const cards = withSourceRefs(graphToCards(SEED_GRAPH, SEED_CASES), refs, SEED_CASES)
132
+ const cards = withSourceRefs(
133
+ graphToCards(SEED_GRAPH),
134
+ refs,
135
+ SEED_CASES,
136
+ )
136
137
  const problems = [...sourceProblems, ...await this.writeCards(cards, 'user:load-sample')]
137
138
  return { seeded: true, migrated: false, problems }
138
139
  }
@@ -146,40 +147,26 @@ export class DiagnosisStore {
146
147
  const { seeded, migrated, problems } = await this.ensure()
147
148
  const cards = await this.readCards()
148
149
  const events = await this.readEvents()
150
+ const baseGraph = cards.length > 0 ? { nodes: [], edges: [] } : await this.readLegacyGraph()
151
+ const baseCases = cards.length > 0 ? [] : await this.readLegacyCases()
152
+ const built = await this.buildGraph(cards, baseGraph, baseCases)
153
+ const runRecords = (await this.readRuns()).records
149
154
  /**
150
- * 「什么都没有」= 没有卡片、也没有旧 jsonl。它是**可以载入内置示例**的唯一条件:
155
+ * 「什么都没有」= 没有卡片、Event、排查日志和旧 jsonl。它是**可以载入内置示例**的唯一条件:
151
156
  * 已经有知识(不管哪一代形态)时不该再往里塞示例 —— 那会变成第二份互相矛盾的知识。
152
157
  */
153
- const canLoadSample = cards.length === 0 && events.length === 0 && (await this.legacyShardFiles()).length === 0
154
-
155
- if (cards.length > 0) {
156
- const built = await this.buildGraph(cards)
157
- return {
158
- graph: built.graph,
159
- cases: built.cases,
160
- events,
161
- issues: validateDiagnosis(built.graph),
162
- seeded,
163
- migrated,
164
- source: 'cards',
165
- canLoadSample: false,
166
- problems: [...problems, ...built.problems],
167
- }
168
- }
169
-
170
- // 还没有卡片:旧 jsonl 就是当前可读的来源;两份都没有就是一个空库。
171
- const graph = await this.readLegacyGraph()
172
- const cases = await this.readLegacyCases()
158
+ const canLoadSample = cards.length === 0 && events.length === 0 && runRecords.length === 0
159
+ && baseGraph.nodes.length === 0 && baseCases.length === 0 && (await this.legacyShardFiles()).length === 0
173
160
  return {
174
- graph,
175
- cases,
161
+ graph: built.graph,
162
+ cases: built.cases,
176
163
  events,
177
- issues: validateDiagnosis(graph),
164
+ issues: validateDiagnosis(built.graph),
178
165
  seeded,
179
166
  migrated,
180
- source: canLoadSample ? 'empty' : events.length > 0 ? 'cards' : 'jsonl',
167
+ source: canLoadSample ? 'empty' : cards.length > 0 ? 'cards' : events.length > 0 || runRecords.length > 0 ? 'records' : 'jsonl',
181
168
  canLoadSample,
182
- problems,
169
+ problems: [...problems, ...built.problems],
183
170
  }
184
171
  }
185
172
 
@@ -190,21 +177,27 @@ export class DiagnosisStore {
190
177
  * 与四条执行边全部来自 `agent_diagnosis_case` 的 SourceRaw。合成一张图是**读**的时候
191
178
  * 做的,不是写的时候 —— 写的时候合成,等于让一次执行的结果变成可复用的知识。
192
179
  */
193
- private async buildGraph(cards: DiagnosisCard[]): Promise<{ graph: AttributionGraphData; cases: AttributionCase[]; problems: string[] }> {
180
+ private async buildGraph(cards: DiagnosisCard[], baseGraph: AttributionGraphData = { nodes: [], edges: [] }, baseCases: AttributionCase[] = []): Promise<{ graph: AttributionGraphData; cases: AttributionCase[]; problems: string[] }> {
194
181
  const compiled = cardsToGraph(cards)
195
182
  const problems = [...compiled.problems]
196
- const nodes: AttributionNode[] = [...compiled.graph.nodes]
197
- const edges: AttributionEdge[] = [...compiled.graph.edges]
183
+ const nodes: AttributionNode[] = [...baseGraph.nodes, ...compiled.graph.nodes]
184
+ const edges: AttributionEdge[] = [...baseGraph.edges, ...compiled.graph.edges]
185
+ const knownCardNodes = [...baseGraph.nodes, ...compiled.graph.nodes]
186
+ const cases = new Map<string, AttributionCase>()
187
+ for (const item of [...baseCases, ...compiled.cases]) cases.set(item.id, item)
198
188
 
199
189
  const runs = await this.readRuns()
200
190
  problems.push(...runs.problems)
201
191
  for (const record of runs.records) {
202
192
  const run = runToGraph(record)
203
193
  // 引用完整性要**连这次排查自己产出的 id 一起算**:路径里的 `E1` / `C1` 正是它写下的。
204
- const known = new Set([...compiled.graph.nodes.map(node => node.id), ...run.nodes.map(node => node.id)])
194
+ const known = new Set([...knownCardNodes.map(node => node.id), ...run.nodes.map(node => node.id)])
205
195
  problems.push(...run.problems, ...runReferenceProblems(record, known))
206
196
  nodes.push(...run.nodes)
207
197
  edges.push(...run.edges)
198
+ const symptomTitle = knownCardNodes.find(node => node.id === record.symptomId)?.title ?? record.symptomId
199
+ const view = caseViewOfRun(record, symptomTitle)
200
+ cases.set(view.id, view)
208
201
  }
209
202
 
210
203
  /**
@@ -219,7 +212,7 @@ export class DiagnosisStore {
219
212
  nodes.push(...legacyRun.nodes)
220
213
  edges.push(...legacyRun.edges)
221
214
 
222
- return { graph: dedupe({ nodes, edges }, problems), cases: compiled.cases, problems }
215
+ return { graph: dedupe({ nodes, edges }, problems), cases: [...cases.values()], problems }
223
216
  }
224
217
 
225
218
  /**
@@ -247,14 +240,15 @@ export class DiagnosisStore {
247
240
 
248
241
  async readGraph(): Promise<AttributionGraphData> {
249
242
  const cards = await this.readCards()
250
- if (cards.length > 0) return (await this.buildGraph(cards)).graph
251
- return await this.readLegacyGraph()
243
+ const baseGraph = cards.length > 0 ? { nodes: [], edges: [] } : await this.readLegacyGraph()
244
+ return (await this.buildGraph(cards, baseGraph)).graph
252
245
  }
253
246
 
254
247
  async readCases(): Promise<AttributionCase[]> {
255
248
  const cards = await this.readCards()
256
- if (cards.length > 0) return cardsToGraph(cards).cases
257
- return await this.readLegacyCases()
249
+ const baseGraph = cards.length > 0 ? { nodes: [], edges: [] } : await this.readLegacyGraph()
250
+ const baseCases = cards.length > 0 ? [] : await this.readLegacyCases()
251
+ return (await this.buildGraph(cards, baseGraph, baseCases)).cases
258
252
  }
259
253
 
260
254
  /* ────────────────────────────────────────────── 卡片:读写 ── */
@@ -326,15 +320,11 @@ export class DiagnosisStore {
326
320
  }
327
321
 
328
322
  /**
329
- * 一次排查结束:写一条 SourceRaw,并**直接写入**一张案例卡片。
330
- *
331
- * 0044:案例卡片原来只提议、等人确认。与编译那条链路一样改成直接入库 ——
332
- * 一次排查的价值就在于「当时是怎么走的」,让它停在待办队列里等于把经验丢掉。
333
- * 卡片会被标成 `unreviewed`,人在「知识库 → Wiki」里 review 它。
334
- *
335
- * 幂等:`externalId` = runId,同一次排查重跑不会写出第二条来源;卡片按 entityId 写入。
323
+ * 一次排查结束只写一条 SourceRaw。案例视图从这条记录派生;Knowledge / Event 的抽取与归一
324
+ * 仍由知识处理阶段执行,避免把同一份排查过程再写成另一张 WikiCard。
325
+ * 幂等:`externalId` = runId,同一次排查重跑不会写出第二条来源。
336
326
  */
337
- async recordRun(record: DiagnosisRunRecord): Promise<{ sourceId?: string; entityId?: string; problems: string[] }> {
327
+ async recordRun(record: DiagnosisRunRecord): Promise<{ sourceId?: string; problems: string[] }> {
338
328
  const problems: string[] = []
339
329
  const added = await this.host.addSource({
340
330
  title: `AI 诊断日志 · ${record.symptomId} · ${record.runId}`,
@@ -345,18 +335,7 @@ export class DiagnosisStore {
345
335
  if (added.ok === false) return { problems: [`诊断日志 ${record.runId} 写入失败:${added.error.code} ${added.error.message}`] }
346
336
  const sourceId = added.data.sourceId
347
337
 
348
- const symptomTitle = (await this.readCards()).find(card => card.entityId === record.symptomId)?.title ?? record.symptomId
349
- const draft = caseCardOf(record, symptomTitle)
350
- const written = await this.host.writeCard({
351
- entityType: draft.entityType as SchemaType,
352
- entityId: draft.entityId,
353
- title: draft.title,
354
- content: renderCardBody(draft),
355
- sourceRefs: [sourceId],
356
- status: 'unreviewed',
357
- })
358
- if (written.ok === false) problems.push(`案例卡片 ${draft.entityId} 写入失败:${written.error.code} ${written.error.message}`)
359
- return { sourceId, ...(written.ok === false ? {} : { entityId: draft.entityId }), problems }
338
+ return { sourceId, problems }
360
339
  }
361
340
 
362
341
  /* ────────────────────────────────────────────── 旧知识根迁移 ── */
@@ -499,8 +478,8 @@ export function renderCardBody(card: DiagnosisCard): string {
499
478
  /**
500
479
  * 给卡片挂出处。
501
480
  *
502
- * 规则是**数据驱动的**,不是凭印象分的:案例卡片与它自己的复盘原文一一对应;
503
- * 其它卡片取「第一篇走过它的复盘」,没有走过它的就挂那本手册。
481
+ * 规则是**数据驱动的**,不是凭印象分的:可复用诊断卡优先引用第一篇走过该节点的复盘;
482
+ * 没有经过复盘的节点引用手册。历史 Case 卡仍由旧数据读取,不在新示例里生成。
504
483
  */
505
484
  function withSourceRefs(
506
485
  cards: DiagnosisCard[],
@@ -15,7 +15,8 @@ import type { ProblemClassId } from './problem-classes.js'
15
15
  export type NodeLayer = 'type' | 'reference' | 'instance'
16
16
 
17
17
  /**
18
- * 九类归因节点。与 wiki 卡片的 `entity_type` 一一对应(`asset` 与三类执行产物除外)。
18
+ * 九类归因图节点。当前 Knowledge 节点可对应 WikiCard;`case` 是排查记录视图,
19
+ * `asset` 与执行产物也不对应 WikiCard。历史 Case WikiCard 仅为兼容数据。
19
20
  *
20
21
  * **两个名字用的是下划线**(`check_point` / `failure_reason`),卡片类型用连字符
21
22
  * (`diagnosis/check-point` / `diagnosis/failure-reason`):kind 是代码里的标识符,
@@ -187,18 +188,9 @@ export interface AttributionGraphData {
187
188
  }
188
189
 
189
190
  /**
190
- * 案例 = **一篇 wiki 定义 + 一条有序排查路径**。
191
- *
192
- * 它住在领域层而不是示例里(原来是 `seed.ts`):案例是编译产物的一种
193
- * (`cardsToGraph()` 从卡片把它算出来,`/diagnosis/graph` 把它发给界面),
194
- * 而示例只是**一份**案例的取值来源。视图要渲染「服务端给的案例」,
195
- * 那它 import 的形状就不该从示例模块里拿。
196
- *
197
- * 两段各有明确消费者,不能合并:
198
- * - `sections` 是给人读、给向量召回的散文(可 diff、可评审、可追溯);
199
- * - `path` 是给图和剪枝用的结构(有序、可判真假、带被否定的分支)。
200
- *
201
- * 序和枚举一旦写进散文,归并算法就再也读不到它们了 —— 所以路径单独成段。
191
+ * Case 是一次排查记录的只读展示投影,不是持久化实体。
192
+ * `sections` 供人阅读,`path` 保留检查顺序与被否定的分支;来源是 SourceRaw 运行记录。
193
+ * 历史 `diagnosis/case` WikiCard 仍可读取,但新的运行与示例载入不再生成它。
202
194
  */
203
195
  export interface CaseSection {
204
196
  heading: string
@@ -223,9 +215,11 @@ export interface AttributionCase {
223
215
  /** 误导段:最贵的那条知识。没有它,下次还会有人在同一个假设上烧掉四十分钟。 */
224
216
  misleading?: { nodeId: string; label: string; wastedMinutes: number; why: string }
225
217
  rootCause: string
218
+ /** 排查记录里的根因确认状态;旧案例卡片缺省时按已有内容展示。 */
219
+ rootCauseStatus?: 'confirmed' | 'unconfirmed'
226
220
  }
227
221
 
228
- /** 从通用 `event` Wiki 卡读取的一次具体发生,供智能归因的事件视图展示。 */
222
+ /** 从独立 Event 实体读取的一次具体发生,供智能归因的事件视图展示。 */
229
223
  export interface AttributionEvent {
230
224
  id: string
231
225
  title: string
@@ -323,8 +317,8 @@ export interface DiagnosisRunCause {
323
317
  /**
324
318
  * 一次排查的完整记录。落到 SourceRaw 正文里的 ```json diagnosis-run 块。
325
319
  *
326
- * 它与案例卡片的区别是**谁写的、有没有被归纳过**:这里是 Agent 跑完一圈留下的
327
- * 原始记录,案例卡片是**之后**归纳出来的知识(而且必须经人确认才生效)。
320
+ * 它是 Agent 跑完一圈留下的原始记录。Case 视图从这条记录派生;可复用 Knowledge 与 Event
321
+ * 由独立抽取和归一阶段生成,不在这条记录写成第二张卡片。
328
322
  */
329
323
  export interface DiagnosisRunRecord {
330
324
  /** 本次排查的 runId。同时用作 SourceRaw 的 `externalId`,重跑不产生第二条记录。 */
@@ -1,28 +1,58 @@
1
1
  import { createHash, randomUUID } from 'node:crypto'
2
- import { readdirSync, statSync } from 'node:fs'
2
+ import { readFileSync, readdirSync, statSync } from 'node:fs'
3
3
  import { mkdir, writeFile } from 'node:fs/promises'
4
- import { dirname, isAbsolute, join } from 'node:path'
4
+ import { basename, dirname, isAbsolute, join, resolve } from 'node:path'
5
5
  import { embeddingFingerprint } from '../compiler/embedding-identity.js'
6
6
  import type { RuntimeKnowledgeConfig } from '../types.js'
7
7
  import { installRequiredEnvironment, requirementsFile } from './install.js'
8
- import { modelOptionsView, parseModelSelection, type ModelSelection } from './model-catalog.js'
8
+ import { EMBEDDING_MODELS, modelOptionsView, parseModelSelection, RERANK_MODELS, type ModelSelection } from './model-catalog.js'
9
9
  import { ENVIRONMENT_PROBE_VERSION } from './manifest.js'
10
10
  import { runProcess } from './process.js'
11
11
  import { readModelSelection, writeModelSelection } from './selection.js'
12
- import type { EnvironmentItem, EnvironmentItemId, EnvironmentReport, EnvironmentView } from './types.js'
12
+ import type { EnvironmentCurrentModel, EnvironmentItem, EnvironmentItemId, EnvironmentReport, EnvironmentView } from './types.js'
13
13
 
14
14
  export interface EnvironmentOptions {
15
15
  resourceRoot: string
16
16
  snapshot: () => { config: RuntimeKnowledgeConfig; paths: { python: string; embeddingModel: string; rerankModel: string } }
17
17
  probe: (kind: 'embedding' | 'rerank') => Promise<{ ok: boolean; message: string }>
18
18
  reload: () => void
19
+ activateSelection?: () => Promise<() => Promise<void>>
19
20
  run?: typeof runProcess
20
21
  install?: typeof installRequiredEnvironment
21
22
  }
22
23
 
23
24
  const labels = { embedding: '向量模型', rerank: '重排模型' }
25
+ const CHECK_FILE = 'environment-last-check.json'
26
+ type ModelCatalogEntry = { id: string; label: string; directory: string; repo: string }
27
+ function currentModel(root: string, path: string, catalog: readonly ModelCatalogEntry[], disabled: boolean, provider?: string): EnvironmentCurrentModel {
28
+ if (disabled) return { state: 'disabled', label: '未启用' }
29
+ if (provider) return { state: 'active', label: `${provider} · ${basename(path) || path || '未指定模型'}` }
30
+ const option = catalog.find(model => isAbsolute(path)
31
+ ? resolve(path) === resolve(join(root, 'models', model.directory))
32
+ : path === model.repo)
33
+ if (option) return { state: 'active', id: option.id, label: option.label }
34
+ const name = isAbsolute(path) ? basename(path) : basename(path.replaceAll('\\', '/'))
35
+ return { state: 'active', label: `自定义模型 · ${name || '未指定模型'}` }
36
+ }
24
37
  function blankReport(): EnvironmentReport {
25
- return { state: 'unchecked', required: true, items: (['embedding', 'rerank'] as const).map(id => ({ id, label: labels[id], state: 'unchecked', message: '首次使用前必须完成模型推理自检' })) }
38
+ return { state: 'unchecked', required: true, items: (['embedding', 'rerank'] as const).map(id => ({ id, label: labels[id], state: 'unchecked', message: '尚未完成模型推理自检' })) }
39
+ }
40
+ function readCachedReport(resourceRoot: string, fingerprint: string): EnvironmentReport | undefined {
41
+ try {
42
+ const cached = JSON.parse(readFileSync(join(resourceRoot, CHECK_FILE), 'utf8')) as Record<string, unknown>
43
+ if (cached.required !== true || cached.fingerprint !== fingerprint || typeof cached.checkedAt !== 'string'
44
+ || !Number.isFinite(Date.parse(cached.checkedAt)) || !Array.isArray(cached.items)) return undefined
45
+ const items: EnvironmentReport['items'] = []
46
+ for (const id of ['embedding', 'rerank'] as const) {
47
+ const item = cached.items.find((value): value is Record<string, unknown> => Boolean(value && typeof value === 'object' && (value as Record<string, unknown>).id === id))
48
+ if (!item || item.label !== labels[id] || typeof item.message !== 'string'
49
+ || !['unchecked', 'ready', 'missing', 'failed'].includes(String(item.state))) return undefined
50
+ items.push({ id, label: labels[id], state: item.state as EnvironmentReport['items'][number]['state'], message: item.message,
51
+ ...(typeof item.path === 'string' ? { path: item.path } : {}) })
52
+ }
53
+ return { state: items.every(item => item.state === 'ready') ? 'ready' : 'blocked', required: true,
54
+ checkedAt: cached.checkedAt, fingerprint, items }
55
+ } catch { return undefined }
26
56
  }
27
57
  function stamp(path: string): unknown { try { const value = statSync(path); return [value.size, value.mtimeMs] } catch { return null } }
28
58
  function sitePackagesPath(pythonPath: string): string {
@@ -52,14 +82,16 @@ export function modelFilesProblem(path: string): string | undefined {
52
82
 
53
83
  /** Preparation and readiness are shared by all bases in one deployment. */
54
84
  export class KnowledgeEnvironment {
55
- private report = blankReport()
85
+ private report: EnvironmentReport
56
86
  private job?: EnvironmentView['job']
57
87
  private checkPending?: Promise<EnvironmentReport>
58
88
  private work?: Promise<void>
59
89
  private controller?: AbortController
60
90
  private checkController?: AbortController
61
91
  private disposed = false
62
- constructor(private readonly options: EnvironmentOptions) {}
92
+ constructor(private readonly options: EnvironmentOptions) {
93
+ this.report = readCachedReport(options.resourceRoot, this.fingerprint()) ?? blankReport()
94
+ }
63
95
 
64
96
  private fingerprint(): string {
65
97
  const { config, paths } = this.options.snapshot()
@@ -73,24 +105,34 @@ export class KnowledgeEnvironment {
73
105
  }
74
106
 
75
107
  view(): EnvironmentView {
76
- if (!this.checkPending && this.report.state === 'ready' && this.report.fingerprint !== this.fingerprint()) this.report = blankReport()
108
+ if (!this.checkPending && this.report.fingerprint && this.report.fingerprint !== this.fingerprint()) {
109
+ if (this.report.state === 'ready') this.report.state = 'unchecked'
110
+ this.report.items = this.report.items.map(item => item.state === 'ready' ? { ...item, state: 'unchecked', message: '自检后配置或资源发生变化,请重新自检' } : item)
111
+ }
112
+ const { config, paths } = this.options.snapshot()
77
113
  const selection = readModelSelection(this.options.resourceRoot)
78
- return structuredClone({ report: this.report, selection, models: modelOptionsView(), ...(this.job ? { job: this.job } : {}) })
114
+ const current = {
115
+ embedding: currentModel(this.options.resourceRoot, paths.embeddingModel, EMBEDDING_MODELS, config.embeddingProvider === 'none', config.embeddingProvider === 'ollama' ? 'Ollama' : undefined),
116
+ rerank: currentModel(this.options.resourceRoot, paths.rerankModel, RERANK_MODELS, config.rerankProvider === 'none'),
117
+ }
118
+ return structuredClone({ report: this.report, selection, current, models: modelOptionsView(), ...(this.job ? { job: this.job } : {}) })
79
119
  }
80
120
  invalidate(): void { if (!this.checkPending) this.report = blankReport() }
81
121
 
82
- async requireReady(): Promise<void> {
122
+ async requireReady(kind?: EnvironmentItemId): Promise<void> {
83
123
  if (this.disposed) throw new Error('环境服务已关闭')
84
124
  if (this.job?.state === 'running') throw new Error('环境正在准备或自检,请等待完成后再编译和召回')
85
125
  const view = this.view()
86
- const recent = view.report.checkedAt && Date.now() - Date.parse(view.report.checkedAt) < 5 * 60_000
87
- const report = view.report.state === 'ready' && recent ? view.report : await this.check(true)
88
- if (report.state !== 'ready') throw new Error(`环境未就绪:${report.items.filter(item => item.state !== 'ready').map(item => `${item.label}:${item.message}`).join(';')}。请在 Tita 设置中完成环境准备`)
126
+ // A passing result stays valid while the model, provider, runtime, and device fingerprint stays the same.
127
+ // The operation itself still reports inference failures, and the user can always request a fresh probe.
128
+ const alreadyReady = kind ? view.report.items.find(item => item.id === kind)?.state === 'ready' : view.report.state === 'ready'
129
+ const report = alreadyReady ? view.report : await this.check(true, kind)
130
+ if (kind ? report.items.find(item => item.id === kind)?.state !== 'ready' : report.state !== 'ready') throw new Error(`环境未就绪:${report.items.filter(item => (!kind || item.id === kind) && item.state !== 'ready').map(item => `${item.label}:${item.message}`).join(';')}。请在 Tita 设置中完成环境准备`)
89
131
  }
90
132
 
91
- check(deep = true): Promise<EnvironmentReport> {
92
- if (this.checkPending) return this.checkPending
93
- const pending = this.performCheck(deep)
133
+ check(deep = true, kind?: EnvironmentItemId): Promise<EnvironmentReport> {
134
+ if (this.checkPending) return this.checkPending.then(report => !kind || report.items.find(item => item.id === kind)?.state === 'ready' ? report : this.check(deep, kind))
135
+ const pending = this.performCheck(deep, kind)
94
136
  this.checkPending = pending
95
137
  void pending.finally(() => { if (this.checkPending === pending) this.checkPending = undefined }).catch(() => undefined)
96
138
  return pending
@@ -100,23 +142,28 @@ export class KnowledgeEnvironment {
100
142
  this.report.items = this.report.items.map(item => item.id === id ? { ...item, ...patch } : item)
101
143
  }
102
144
 
103
- private async performCheck(deep: boolean): Promise<EnvironmentReport> {
145
+ private async performCheck(deep: boolean, kind?: EnvironmentItemId): Promise<EnvironmentReport> {
104
146
  const checkController = new AbortController()
105
147
  this.checkController = checkController
106
148
  const signal = this.controller ? AbortSignal.any([this.controller.signal, checkController.signal]) : checkController.signal
107
149
  const fingerprint = this.fingerprint()
108
150
  const { config, paths } = this.options.snapshot()
109
- this.report = blankReport()
151
+ if (this.report.fingerprint !== fingerprint) this.report = blankReport()
110
152
  this.report.state = 'checking'
111
- for (const id of ['embedding', 'rerank'] as const) {
153
+ for (const id of kind ? [kind] : ['embedding', 'rerank'] as const) {
112
154
  if (signal.aborted || this.disposed) break
113
155
  const path = id === 'embedding' ? paths.embeddingModel : paths.rerankModel
156
+ const cached = this.report.fingerprint === fingerprint ? this.report.items.find(item => item.id === id) : undefined
114
157
  this.item(id, { path })
115
158
  const disabled = id === 'embedding' ? config.embeddingProvider === 'none' : config.rerankProvider === 'none'
116
159
  const problem = disabled ? '此能力是必需项,请启用模型'
117
160
  : id === 'embedding' && config.embeddingProvider === 'ollama' ? undefined : modelFilesProblem(path)
118
161
  if (problem) { this.item(id, { state: 'missing', message: problem }); continue }
119
- if (!deep) { this.item(id, { state: 'unchecked', message: '资源已找到,尚未完成实际推理自检' }); continue }
162
+ if (!deep) {
163
+ if (cached && cached.state !== 'unchecked' && cached.state !== 'checking') this.item(id, { state: cached.state, message: cached.message })
164
+ else this.item(id, { state: 'unchecked', message: '资源已找到,尚未完成实际推理自检' })
165
+ continue
166
+ }
120
167
  this.item(id, { state: 'checking', message: '加载模型并运行推理自检,首次加载可能较慢' })
121
168
  try {
122
169
  const result = await this.options.probe(id)
@@ -131,9 +178,15 @@ export class KnowledgeEnvironment {
131
178
  ? { ...item, state: 'unchecked', message: '自检已停止,请重新运行' }
132
179
  : item)
133
180
  this.report.state = this.report.items.every(item => item.state === 'ready') ? 'ready' : 'blocked'
134
- this.report.checkedAt = new Date().toISOString()
181
+ if (deep) this.report.checkedAt = new Date().toISOString()
135
182
  this.report.fingerprint = fingerprint
136
183
  if (this.checkController === checkController) this.checkController = undefined
184
+ if (deep && !signal.aborted && !this.disposed) {
185
+ try {
186
+ await mkdir(this.options.resourceRoot, { recursive: true })
187
+ await writeFile(join(this.options.resourceRoot, CHECK_FILE), JSON.stringify(this.report, null, 2))
188
+ } catch { /* The current process can use the result; a later restart may need to probe again. */ }
189
+ }
137
190
  return structuredClone(this.report)
138
191
  }
139
192
 
@@ -151,6 +204,7 @@ export class KnowledgeEnvironment {
151
204
  const job = this.job
152
205
  const log = (line: string): void => { job.logs = [...job.logs, line.slice(-1500)].slice(-12) }
153
206
  const work = async (): Promise<void> => {
207
+ let rollbackSettings: (() => Promise<void>) | undefined
154
208
  try {
155
209
  // A first-use guard can already be checking. Do not install into that probe.
156
210
  if (this.checkPending) await this.checkPending
@@ -164,16 +218,18 @@ export class KnowledgeEnvironment {
164
218
  const id = label.includes('向量') ? 'embedding' : label.includes('重排') ? 'rerank' : undefined
165
219
  if (id) this.item(id, { state: 'checking', message: label })
166
220
  }, log, this.options.run ?? runProcess, selection)
221
+ rollbackSettings = await this.options.activateSelection?.()
167
222
  this.options.reload()
168
223
  } else this.options.reload()
169
224
  job.stage = '运行实际推理自检'
170
225
  const report = await this.check(true)
171
226
  if (controller.signal.aborted) throw new Error('ENVIRONMENT_CANCELLED')
172
227
  if (report.state !== 'ready') throw new Error(report.items.filter(item => item.state !== 'ready').map(item => `${item.label}:${item.message}`).join(';'))
173
- await mkdir(this.options.resourceRoot, { recursive: true })
174
- await writeFile(join(this.options.resourceRoot, 'environment-last-check.json'), JSON.stringify(report, null, 2))
175
228
  job.state = 'succeeded'; job.stage = '环境已就绪'
176
229
  } catch (cause) {
230
+ if (rollbackSettings) {
231
+ try { await rollbackSettings() } catch (rollbackCause) { log(`恢复模型设置失败:${rollbackCause instanceof Error ? rollbackCause.message : String(rollbackCause)}`) }
232
+ }
177
233
  if (action === 'install') {
178
234
  const activeSelection = readModelSelection(this.options.resourceRoot)
179
235
  if (activeSelection.embedding !== previousSelection.embedding || activeSelection.rerank !== previousSelection.rerank) {
@@ -17,6 +17,11 @@ export interface EnvironmentReport {
17
17
  fingerprint?: string
18
18
  items: EnvironmentItem[]
19
19
  }
20
+ export interface EnvironmentCurrentModel {
21
+ id?: string
22
+ label: string
23
+ state: 'active' | 'disabled'
24
+ }
20
25
  export interface EnvironmentJob {
21
26
  id: string
22
27
  state: 'running' | 'succeeded' | 'failed' | 'cancelled'
@@ -30,5 +35,6 @@ export interface EnvironmentView {
30
35
  report: EnvironmentReport
31
36
  job?: EnvironmentJob
32
37
  selection: ModelSelection
38
+ current: { embedding: EnvironmentCurrentModel; rerank: EnvironmentCurrentModel }
33
39
  models: { embedding: ModelOptionView[]; rerank: ModelOptionView[] }
34
40
  }
package/src/events.ts CHANGED
@@ -46,6 +46,18 @@ export interface EventEntity {
46
46
  updatedAt: string
47
47
  }
48
48
 
49
+ /** Remove the legacy generated Event list from a WikiCard body. References live in card_event. */
50
+ export function stripEventReferenceSection(content: string): string {
51
+ const lines = content.split('\n')
52
+ for (let start = lines.length - 1; start >= 0; start -= 1) {
53
+ if (!/^##\s+关联\s*Events?\s*$|^##\s+关联事件\s*$/.test((lines[start] ?? '').trim())) continue
54
+ let end = start + 1
55
+ while (end < lines.length && !/^#{1,2}\s+/.test(lines[end] ?? '')) end += 1
56
+ lines.splice(start, end - start)
57
+ }
58
+ return lines.join('\n').trim()
59
+ }
60
+
49
61
  export function compositeEventIdentity(occurredAt: string | null, coordinate: CardCoordinate, symptom: string | null): string {
50
62
  const payload = [occurredAt, coordinate.objectId, coordinate.actionId, coordinate.sceneId, normalizeKnowledge(symptom ?? '')]
51
63
  return 'composite:' + createHash('sha256').update(JSON.stringify(payload)).digest('hex').slice(0, 24)
@@ -13,6 +13,7 @@
13
13
  */
14
14
 
15
15
  import { AXIS_CONTRACT_LINES } from './axis-contract.js'
16
+ import { TABLE_OUTPUT_CONTRACT } from './extract-table-contract.js'
16
17
 
17
18
  /**
18
19
  * 把骨架允许的小节拼成提示词里那一条。
@@ -37,14 +38,15 @@ function sectionRule(sections: string[] | undefined): string[] {
37
38
  * 编译链路用 `buildTrialSystem(profileSections)`;试跑面板用默认实例(不下发小节清单),
38
39
  * 因为那一层没有「本库骨架」这个概念。
39
40
  */
40
- export function buildTrialSystem(sections?: string[]): string {
41
- return trialSystemBody(sectionRule(sections))
41
+ export function buildTrialSystem(sections?: string[], format: 'table' | 'json' = 'table'): string {
42
+ return trialSystemBody(sectionRule(sections), format)
42
43
  }
43
44
 
44
- const trialSystemBody = (positionRule: string[]): string => [
45
+ const trialSystemBody = (positionRule: string[], format: 'table' | 'json'): string => [
45
46
  '你在把一份业务文档拆成知识库要用的内容。知识以 Knowledge 为单位:',
46
47
  '一条可独立检索、验证或执行的最小知识单元。',
47
48
  '',
49
+ ...(format === 'table' ? TABLE_OUTPUT_CONTRACT : [
48
50
  '只回一个 JSON 对象,不要散文,不要代码围栏。',
49
51
  '',
50
52
  '输出形状:',
@@ -73,6 +75,7 @@ const trialSystemBody = (positionRule: string[]): string => [
73
75
  ' "dropped": [ { "quote": "原文片段", "why": "为什么丢掉" } ]',
74
76
  '}',
75
77
  '',
78
+ ]),
76
79
  '# 知识:一条只讲一个单元',
77
80
  '- text 是一条能脱离上下文独立阅读的最小知识单元。事实要能判断真假,操作步骤要能被执行。',
78
81
  '- 一段原文里有几个独立单元就写几条。不要压成一条,也不要拆碎。',
@@ -65,6 +65,8 @@ export type ModelCall = (request: {
65
65
  sessionId?: string
66
66
  /** 可选观察本次请求的 provider 用量;普通调用方可以忽略。 */
67
67
  onUsage?: (usage: TrialTokenUsage) => void
68
+ /** Public response text only; reasoning chunks are not exposed. */
69
+ onDelta?: (text: string) => void
68
70
  }) => Promise<string>
69
71
 
70
72
  /** 归并条目:与规则抽取的 `NormalizedCandidates['merges']` 保持同一形状。 */
@@ -0,0 +1,29 @@
1
+ /** Kept free of server dependencies so the architecture page shows the real contract. */
2
+ export const TABLE_OUTPUT_CONTRACT = [
3
+ '只返回 TOON 固定列表格,不要 Markdown 围栏或说明。format 必须是 knowledge-table-v1。',
4
+ '每个数组头的 [N] 是本次实际行数;每行必须与 {列名} 的列数和顺序一致。空表写 名称[0]:。',
5
+ '逗号分列。字符串包含逗号、冒号、换行或首尾空格时用双引号包裹;内部双引号写 \\",换行写 \\n,反斜杠写 \\\\。',
6
+ '缺失值写 null。字符串列表 heading_path 和 missing 写为 JSON 数组字符串。',
7
+ 'evidence 表每个引文只写一次。其它表的 evidence 列写出处 ID,多个 ID 用空格分隔。所有 ID 必须存在。',
8
+ '以下是列定义和示例;示例内容必须换成当前文档的数据:',
9
+ 'format: knowledge-table-v1',
10
+ 'subject: 业务主体',
11
+ 'knowledge[1]{id,text,object,action,scene,section,order,evidence}:',
12
+ ' k1,一条最小知识单元,对象名,动作名,场景名,字段,1,q1',
13
+ 'events[1]{id,external_id,title,summary,object,action,scene,occurred_at,symptom,root_cause,root_cause_status,outcome,missing,evidence}:',
14
+ ' e1,null,一次具体发生,原文明确的概述,对象名,动作名,场景名,null,null,null,null,null,"[]",q1',
15
+ 'timeline[1]{event_id,order,type,description,result,at,evidence}:',
16
+ ' e1,1,observation,原文记录的现象,null,null,q1',
17
+ 'objects[1]{mention,definition,evidence,existing,parent}:',
18
+ ' 对象名,对象的边界和含义,q1,null,null',
19
+ 'actions[1]{mention,definition,evidence,existing,parent}:',
20
+ ' 动作名,动作的含义,q1,null,null',
21
+ 'scenes[1]{mention,definition,evidence,existing,parent}:',
22
+ ' 场景名,场景的含义,q1,null,null',
23
+ 'evidence[1]{id,quote,heading_path}:',
24
+ ' q1,原文逐字引文,"[]"',
25
+ 'dropped[0]:',
26
+ 'timeline.type 只能是 observation/check/action/result/conclusion;root_cause_status 只能是 confirmed/suspected/null。',
27
+ 'dropped 非空时列为 {quote,why}。对象 parent 只填原文有证据的父对象;existing 填已有实体名。',
28
+ '',
29
+ ]