dsh-data-domain-knowledge 0.1.0-alpha.2 → 0.1.0-alpha.4
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.
- package/README.md +147 -19
- package/cordis.patch.yml +25 -10
- package/lib/client.js +6400 -3326
- package/lib/doc-page.js +7761 -0
- package/lib/types/convert/host.d.ts.map +1 -1
- package/lib/types/convert/host.js +63 -11
- package/lib/types/convert/host.js.map +1 -1
- package/lib/types/convert/ingest.d.ts +7 -4
- package/lib/types/convert/ingest.d.ts.map +1 -1
- package/lib/types/convert/ingest.js +52 -14
- package/lib/types/convert/ingest.js.map +1 -1
- package/lib/types/convert/types.d.ts +2 -2
- package/lib/types/convert/types.d.ts.map +1 -1
- package/lib/types/convert/types.js +2 -2
- package/lib/types/convert/types.js.map +1 -1
- package/lib/types/diagnosis/cards.d.ts +8 -7
- package/lib/types/diagnosis/cards.d.ts.map +1 -1
- package/lib/types/diagnosis/cards.js +119 -49
- package/lib/types/diagnosis/cards.js.map +1 -1
- package/lib/types/diagnosis/run.d.ts +57 -0
- package/lib/types/diagnosis/run.d.ts.map +1 -0
- package/lib/types/diagnosis/run.js +245 -0
- package/lib/types/diagnosis/run.js.map +1 -0
- package/lib/types/diagnosis/schema.d.ts +25 -2
- package/lib/types/diagnosis/schema.d.ts.map +1 -1
- package/lib/types/diagnosis/schema.js +64 -25
- package/lib/types/diagnosis/schema.js.map +1 -1
- package/lib/types/diagnosis/seed-source.d.ts +17 -2
- package/lib/types/diagnosis/seed-source.d.ts.map +1 -1
- package/lib/types/diagnosis/seed-source.js +19 -2
- package/lib/types/diagnosis/seed-source.js.map +1 -1
- package/lib/types/diagnosis/seed.d.ts +32 -42
- package/lib/types/diagnosis/seed.d.ts.map +1 -1
- package/lib/types/diagnosis/seed.js +187 -51
- package/lib/types/diagnosis/seed.js.map +1 -1
- package/lib/types/diagnosis/store.d.ts +95 -25
- package/lib/types/diagnosis/store.d.ts.map +1 -1
- package/lib/types/diagnosis/store.js +297 -166
- package/lib/types/diagnosis/store.js.map +1 -1
- package/lib/types/diagnosis/types.d.ts +168 -16
- package/lib/types/diagnosis/types.d.ts.map +1 -1
- package/lib/types/diagnosis/types.js +1 -1
- package/lib/types/diagnosis/types.js.map +1 -1
- package/lib/types/document-page.d.ts +39 -0
- package/lib/types/document-page.d.ts.map +1 -0
- package/lib/types/document-page.js +43 -0
- package/lib/types/document-page.js.map +1 -0
- package/lib/types/extract-model.d.ts +67 -27
- package/lib/types/extract-model.d.ts.map +1 -1
- package/lib/types/extract-model.js +225 -97
- package/lib/types/extract-model.js.map +1 -1
- package/lib/types/extract.d.ts +16 -0
- package/lib/types/extract.d.ts.map +1 -1
- package/lib/types/extract.js +19 -2
- package/lib/types/extract.js.map +1 -1
- package/lib/types/fs-atomic.d.ts +11 -0
- package/lib/types/fs-atomic.d.ts.map +1 -0
- package/lib/types/fs-atomic.js +41 -0
- package/lib/types/fs-atomic.js.map +1 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +466 -63
- package/lib/types/index.js.map +1 -1
- package/lib/types/intent/types.js +1 -1
- package/lib/types/intent/types.js.map +1 -1
- package/lib/types/knowledge-bases.d.ts +23 -12
- package/lib/types/knowledge-bases.d.ts.map +1 -1
- package/lib/types/knowledge-bases.js +37 -44
- package/lib/types/knowledge-bases.js.map +1 -1
- package/lib/types/layout.d.ts +3 -3
- package/lib/types/layout.d.ts.map +1 -1
- package/lib/types/layout.js +4 -4
- package/lib/types/layout.js.map +1 -1
- package/lib/types/migrate.d.ts +1 -1
- package/lib/types/migrate.js +2 -2
- package/lib/types/migrate.js.map +1 -1
- package/lib/types/model-text.d.ts +99 -0
- package/lib/types/model-text.d.ts.map +1 -0
- package/lib/types/model-text.js +103 -0
- package/lib/types/model-text.js.map +1 -0
- package/lib/types/present.d.ts +239 -0
- package/lib/types/present.d.ts.map +1 -0
- package/lib/types/present.js +255 -0
- package/lib/types/present.js.map +1 -0
- package/lib/types/recall-rank.d.ts +37 -0
- package/lib/types/recall-rank.d.ts.map +1 -1
- package/lib/types/recall-rank.js +41 -1
- package/lib/types/recall-rank.js.map +1 -1
- package/lib/types/runtime.d.ts +564 -31
- package/lib/types/runtime.d.ts.map +1 -1
- package/lib/types/runtime.js +2645 -292
- package/lib/types/runtime.js.map +1 -1
- package/lib/types/settings-schema.d.ts +4 -2
- package/lib/types/settings-schema.d.ts.map +1 -1
- package/lib/types/settings-schema.js +2 -0
- package/lib/types/settings-schema.js.map +1 -1
- package/lib/types/source-images.d.ts +118 -0
- package/lib/types/source-images.d.ts.map +1 -0
- package/lib/types/source-images.js +288 -0
- package/lib/types/source-images.js.map +1 -0
- package/lib/types/types.d.ts +356 -28
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/types.js +112 -18
- package/lib/types/types.js.map +1 -1
- package/lib/types/wiki-finalize.d.ts +140 -0
- package/lib/types/wiki-finalize.d.ts.map +1 -0
- package/lib/types/wiki-finalize.js +269 -0
- package/lib/types/wiki-finalize.js.map +1 -0
- package/lib/types/wiki-types.d.ts +10 -2
- package/lib/types/wiki-types.d.ts.map +1 -1
- package/lib/types/wiki-types.js +27 -30
- package/lib/types/wiki-types.js.map +1 -1
- package/package.json +4 -3
- package/preset/tita-data-agent/agent.cordis.yml +25 -10
- package/src/client/DirectoryPicker.tsx +12 -12
- package/src/client/DrawerSection.tsx +18 -0
- package/src/client/InfoHint.tsx +21 -8
- package/src/client/IntentTab.tsx +20 -26
- package/src/client/KnowledgeBaseTab.tsx +458 -158
- package/src/client/KnowledgePanel.tsx +44 -10
- package/src/client/KnowledgeStepRail.tsx +3 -0
- package/src/client/KnowledgeToolRow.tsx +331 -0
- package/src/client/NodeSettings.tsx +20 -22
- package/src/client/PreviewSections.tsx +178 -43
- package/src/client/RecallPanel.tsx +418 -127
- package/src/client/TypeDefinitionDrawer.tsx +154 -54
- package/src/client/WikiCardReview.tsx +266 -0
- package/src/client/WikiFlowCard.tsx +57 -95
- package/src/client/WikiTypeEditor.tsx +197 -95
- package/src/client/arch/AgentArchitectureGraph.tsx +30 -14
- package/src/client/arch/NodeLogicPanel.tsx +6 -3
- package/src/client/arch/model.ts +164 -26
- package/src/client/architecture.css +25 -10
- package/src/client/architecture.tsx +66 -41
- package/src/client/asset-images.ts +125 -0
- package/src/client/attribution/AttributionGraph.tsx +481 -34
- package/src/client/attribution/AttributionTab.tsx +501 -106
- package/src/client/attribution/CaseView.tsx +9 -14
- package/src/client/attribution/GraphStage.tsx +25 -18
- package/src/client/attribution/RunView.tsx +27 -9
- package/src/client/attribution/StageChrome.tsx +280 -140
- package/src/client/attribution/graph-options.ts +611 -47
- package/src/client/attribution/model.ts +559 -30
- package/src/client/convert/ConvertPanel.tsx +36 -4
- package/src/client/doc-page.css +134 -0
- package/src/client/doc-page.tsx +242 -0
- package/src/client/index.ts +18 -0
- package/src/client/panel-state.ts +105 -0
- package/src/client/problem-domains.ts +1 -1
- package/src/client/request.ts +36 -0
- package/src/client/styles.css +397 -54
- package/src/client/useFitScale.ts +15 -2
- package/src/client/wiki-type-groups.ts +86 -0
- package/src/convert/host.ts +68 -9
- package/src/convert/ingest.ts +61 -18
- package/src/convert/types.ts +2 -2
- package/src/diagnosis/cards.ts +120 -50
- package/src/diagnosis/run.ts +251 -0
- package/src/diagnosis/schema.ts +67 -26
- package/src/diagnosis/seed-source.ts +29 -4
- package/src/diagnosis/seed.ts +187 -85
- package/src/diagnosis/store.ts +310 -173
- package/src/diagnosis/types.ts +181 -16
- package/src/document-page.ts +51 -0
- package/src/extract-model.ts +254 -99
- package/src/extract.ts +35 -3
- package/src/fs-atomic.ts +41 -0
- package/src/index.ts +499 -62
- package/src/intent/types.ts +1 -1
- package/src/knowledge-bases.ts +50 -49
- package/src/layout.ts +4 -4
- package/src/migrate.ts +2 -2
- package/src/model-text.ts +126 -0
- package/src/present.ts +367 -0
- package/src/recall-rank.ts +57 -2
- package/src/runtime.ts +2802 -293
- package/src/settings-schema.ts +2 -0
- package/src/source-images.ts +326 -0
- package/src/types.ts +446 -45
- package/src/wiki-finalize.ts +310 -0
- package/src/wiki-types.ts +28 -32
package/src/diagnosis/cards.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { DIAGNOSIS_ENTITIES, type DiagnosisEntityType } from './schema.js'
|
|
2
|
-
import type { AttributionCase } from './seed.js'
|
|
1
|
+
import { DIAGNOSIS_ENTITIES, LEGACY_BLOCK_FIELDS, LEGACY_ENTITY_TYPES, type DiagnosisEntityType } from './schema.js'
|
|
3
2
|
import type {
|
|
3
|
+
AssetType,
|
|
4
|
+
AttributionCase,
|
|
4
5
|
AttributionEdge, AttributionEdgeKind, AttributionGraphData, AttributionNode, AttributionNodeKind,
|
|
5
|
-
|
|
6
|
+
FailureReason,
|
|
6
7
|
} from './types.js'
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -10,13 +11,12 @@ import type {
|
|
|
10
11
|
*
|
|
11
12
|
* ## 为什么这一层必须存在
|
|
12
13
|
*
|
|
13
|
-
* 在它之前,智能归因的图只有一条来源:`seed.ts` 里的 TypeScript
|
|
14
|
-
* `
|
|
15
|
-
*
|
|
16
|
-
* `diagnosis/hypothesis` 这个 entity_type 都不认 —— 于是它既不会被 wikiNote 化,
|
|
14
|
+
* 在它之前,智能归因的图只有一条来源:`seed.ts` 里的 TypeScript 字面量。那意味着
|
|
15
|
+
* **排查思路从来没有进过 wiki**:它不在 `01_source_raw` 里、不是 `02_wiki` 的卡片、
|
|
16
|
+
* 编译器连 `diagnosis/hypothesis` 这个 entity_type 都不认 —— 于是它既不会被 wikiNote 化,
|
|
17
17
|
* 也不会进向量索引,**召回永远召不到它**。而「遇到类似问题先召回」正是这个产品的主张。
|
|
18
18
|
*
|
|
19
|
-
* 这一层把图变成卡片的**投影**:卡片是手写的(人写、Agent
|
|
19
|
+
* 这一层把图变成卡片的**投影**:卡片是手写的(人写、Agent 提议后由人确认),
|
|
20
20
|
* 图是算出来的。§4「存图、显树」讲的是同一件事 —— 存的是知识,图是它的一种读法。
|
|
21
21
|
*
|
|
22
22
|
* ## 卡片三段式与「谁由卡片定义」的边界
|
|
@@ -31,10 +31,10 @@ import type {
|
|
|
31
31
|
*
|
|
32
32
|
* | 图上的东西 | 谁定义 | 为什么 |
|
|
33
33
|
* | --- | --- | --- |
|
|
34
|
-
* | symptom / hypothesis /
|
|
34
|
+
* | symptom / hypothesis / check_point / failure_reason / case 节点 | **卡片** | 这五类是人确认过的知识(§6.4) |
|
|
35
35
|
* | asset 节点 | 引用反推 | 它随数据目录同步,不手写;卡片里只留 id |
|
|
36
|
-
* | 现象→假设 /
|
|
37
|
-
* | evidence / cause 节点,produces / supports / refutes / confirmed_as 边 | **一次排查** |
|
|
36
|
+
* | 现象→假设 / 假设→检查点 / 假设→失败原因 / 检查点→资产 / 资产→假设 / 案例→现象 边 | **卡片** | 都是卡片字段的确定性投影 |
|
|
37
|
+
* | evidence / cause 节点,produces / supports / refutes / confirmed_as 边 | **一次排查** | 它们是一次执行的结果,不是知识;由 `01_source_raw/agent_diagnosis_case/` 那条 SourceRaw 读时解析出来 |
|
|
38
38
|
*
|
|
39
39
|
* 最后一行不是妥协,是分层:把「上次查到什么」写进卡片,等于让一份可复用的知识
|
|
40
40
|
* 绑死在一次具体的执行上。
|
|
@@ -80,14 +80,15 @@ const CARD_LAYOUT: Record<DiagnosisEntityType, { block: string[]; summaryFrom: s
|
|
|
80
80
|
summaryFrom: 'observable',
|
|
81
81
|
},
|
|
82
82
|
'diagnosis/hypothesis': {
|
|
83
|
-
block: ['discriminator', 'not_when', '
|
|
83
|
+
block: ['discriminator', 'not_when', 'failure_reason', 'verified_by', 'assets', 'reuse_count', 'stale'],
|
|
84
84
|
summaryFrom: 'summary',
|
|
85
85
|
},
|
|
86
|
-
'diagnosis/check': {
|
|
87
|
-
|
|
86
|
+
'diagnosis/check-point': {
|
|
87
|
+
// `assets` 是 v1.3 新增的**查询依据**:它承载「检查点 → 资产」这条 `queries` 边。
|
|
88
|
+
block: ['cost_minutes', 'automation', 'verdict_true', 'verdict_false', 'runbook', 'side_effect', 'assets', 'stale'],
|
|
88
89
|
summaryFrom: 'summary',
|
|
89
90
|
},
|
|
90
|
-
'diagnosis/failure-
|
|
91
|
+
'diagnosis/failure-reason': {
|
|
91
92
|
block: ['code', 'label', 'boundary'],
|
|
92
93
|
summaryFrom: 'summary',
|
|
93
94
|
},
|
|
@@ -107,7 +108,7 @@ export const CARD_NODE_KINDS: AttributionNodeKind[] = DIAGNOSIS_ENTITIES.map(spe
|
|
|
107
108
|
|
|
108
109
|
/** 卡片能定义的边:全部是卡片字段的确定性投影。 */
|
|
109
110
|
export const CARD_EDGE_KINDS: AttributionEdgeKind[] = [
|
|
110
|
-
'may_be_caused_by', 'verified_by', 'categorized_as', 'indexed_from', 'instance_of',
|
|
111
|
+
'may_be_caused_by', 'verified_by', 'categorized_as', 'queries', 'indexed_from', 'instance_of',
|
|
111
112
|
]
|
|
112
113
|
|
|
113
114
|
/** 一次排查写下的边。它们不属于任何卡片,见文件头的边界表。 */
|
|
@@ -150,8 +151,8 @@ export function serializeCard(card: DiagnosisCard): string {
|
|
|
150
151
|
export function parseCard(source: string): DiagnosisCard | undefined {
|
|
151
152
|
// 复用运行时的 front matter 解析(同一个 yaml 子集),不另写一套。
|
|
152
153
|
const parsed = parseFrontmatter(source)
|
|
153
|
-
const entityType = String(parsed.frontmatter.entity_type ?? '')
|
|
154
|
-
if (!
|
|
154
|
+
const entityType = normalizeEntityType(String(parsed.frontmatter.entity_type ?? ''))
|
|
155
|
+
if (!entityType) return undefined
|
|
155
156
|
const entityId = String(parsed.frontmatter.entity_id ?? '').trim()
|
|
156
157
|
if (!entityId) return undefined
|
|
157
158
|
|
|
@@ -160,7 +161,7 @@ export function parseCard(source: string): DiagnosisCard | undefined {
|
|
|
160
161
|
if (match) {
|
|
161
162
|
try {
|
|
162
163
|
const value = JSON.parse(match[1])
|
|
163
|
-
if (value && typeof value === 'object' && !Array.isArray(value)) block = value as Record<string, unknown>
|
|
164
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) block = normalizeBlock(value as Record<string, unknown>)
|
|
164
165
|
} catch { /* 块坏了不静默降级成空对象:留空之后由三层校验的「格式层」报缺字段。 */ }
|
|
165
166
|
}
|
|
166
167
|
const body = match ? parsed.content.replace(match[0], '') : parsed.content
|
|
@@ -180,6 +181,22 @@ export function parseCard(source: string): DiagnosisCard | undefined {
|
|
|
180
181
|
}
|
|
181
182
|
}
|
|
182
183
|
|
|
184
|
+
/** 改名之前的 `entity_type` 按别名认,而不是判为未知类型然后静默丢掉这张卡片。 */
|
|
185
|
+
export function normalizeEntityType(value: string): DiagnosisEntityType | undefined {
|
|
186
|
+
const mapped = LEGACY_ENTITY_TYPES[value] ?? value
|
|
187
|
+
return DIAGNOSIS_ENTITIES.some(spec => spec.entityType === mapped) ? mapped as DiagnosisEntityType : undefined
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** 结构化块里的旧字段名(`failure_mode`)搬到新名字上。 */
|
|
191
|
+
function normalizeBlock(block: Record<string, unknown>): Record<string, unknown> {
|
|
192
|
+
const next: Record<string, unknown> = { ...block }
|
|
193
|
+
for (const [from, to] of Object.entries(LEGACY_BLOCK_FIELDS)) {
|
|
194
|
+
if (next[to] === undefined && next[from] !== undefined) next[to] = next[from]
|
|
195
|
+
delete next[from]
|
|
196
|
+
}
|
|
197
|
+
return next
|
|
198
|
+
}
|
|
199
|
+
|
|
183
200
|
/**
|
|
184
201
|
* 只解析 front matter 与正文。
|
|
185
202
|
*
|
|
@@ -224,20 +241,21 @@ function splitSections(body: string): DiagnosisCardSection[] {
|
|
|
224
241
|
return sections.filter(section => section.body !== '')
|
|
225
242
|
}
|
|
226
243
|
|
|
227
|
-
function isEntityType(value: string): value is DiagnosisEntityType {
|
|
228
|
-
return DIAGNOSIS_ENTITIES.some(spec => spec.entityType === value)
|
|
229
|
-
}
|
|
230
|
-
|
|
231
244
|
/* ─────────────────────────────────────────────── 图 → 卡片 ── */
|
|
232
245
|
|
|
233
246
|
/**
|
|
234
247
|
* 类型层与案例 → 卡片。
|
|
235
248
|
*
|
|
236
|
-
* `asset` / `evidence` / `cause`
|
|
237
|
-
*
|
|
249
|
+
* `asset` / `evidence` / `cause` 节点**不产出卡片**:它们不是手确认过的知识(§6.4)。
|
|
250
|
+
* 被引用的资产会在反序列化时按 id 补一个最小节点回来。
|
|
238
251
|
*/
|
|
239
252
|
export function graphToCards(graph: AttributionGraphData, cases: AttributionCase[] = []): DiagnosisCard[] {
|
|
240
253
|
const cards: DiagnosisCard[] = []
|
|
254
|
+
/** 资产 id → 卡片声明过的种类。它写在**引用它的那张卡片**上,不写在资产节点自己身上。 */
|
|
255
|
+
const assetTypeById = new Map<string, AssetType>()
|
|
256
|
+
for (const node of graph.nodes) {
|
|
257
|
+
if (node.kind === 'asset' && node.assetType) assetTypeById.set(node.id, node.assetType)
|
|
258
|
+
}
|
|
241
259
|
|
|
242
260
|
for (const node of graph.nodes) {
|
|
243
261
|
// 案例节点只是图上的**桩**:它的正文与有序路径住在 `cases` 里(§6.4)。图上的桩不单独成卡,
|
|
@@ -264,18 +282,25 @@ export function graphToCards(graph: AttributionGraphData, cases: AttributionCase
|
|
|
264
282
|
} else if (node.kind === 'hypothesis') {
|
|
265
283
|
block.discriminator = node.discriminator ?? ''
|
|
266
284
|
block.not_when = node.notWhen ?? ''
|
|
267
|
-
block.
|
|
285
|
+
block.failure_reason = node.failureReason ?? null
|
|
268
286
|
block.verified_by = graph.edges.filter(edge => edge.kind === 'verified_by' && edge.source === node.id).map(edge => edge.target)
|
|
269
|
-
block.assets = graph.edges
|
|
287
|
+
block.assets = graph.edges
|
|
288
|
+
.filter(edge => edge.kind === 'indexed_from' && edge.target === node.id)
|
|
289
|
+
.map(edge => assetEntryOf(edge.source, assetTypeById))
|
|
270
290
|
block.reuse_count = node.reuseCount ?? 0
|
|
271
291
|
if (node.stale !== undefined) block.stale = node.stale
|
|
272
|
-
} else if (node.kind === '
|
|
292
|
+
} else if (node.kind === 'check_point') {
|
|
273
293
|
block.cost_minutes = node.costMinutes ?? null
|
|
274
294
|
block.automation = node.automation ?? null
|
|
275
295
|
block.verdict_true = node.verdictTrue ?? ''
|
|
276
296
|
block.verdict_false = node.verdictFalse ?? ''
|
|
297
|
+
// 检查点 → 资产:`queries` 边。它和假设上的 `assets`(`indexed_from`)是两条边,
|
|
298
|
+
// 两端与回答的问题都不同,所以各读各的,不共用一份。
|
|
299
|
+
block.assets = graph.edges
|
|
300
|
+
.filter(edge => edge.kind === 'queries' && edge.source === node.id)
|
|
301
|
+
.map(edge => assetEntryOf(edge.target, assetTypeById))
|
|
277
302
|
if (node.stale !== undefined) block.stale = node.stale
|
|
278
|
-
} else if (node.kind === '
|
|
303
|
+
} else if (node.kind === 'failure_reason') {
|
|
279
304
|
block.code = node.code ?? null
|
|
280
305
|
block.label = node.title
|
|
281
306
|
block.boundary = ''
|
|
@@ -328,9 +353,9 @@ export interface CompiledDiagnosis {
|
|
|
328
353
|
/**
|
|
329
354
|
* 卡片 → 图。
|
|
330
355
|
*
|
|
331
|
-
* 只产出「卡片定义得了」的那部分(类型层 + 引用层的资产 +
|
|
332
|
-
* `evidence` / `cause`
|
|
333
|
-
*
|
|
356
|
+
* 只产出「卡片定义得了」的那部分(类型层 + 引用层的资产 + 案例,以及六种卡片边);
|
|
357
|
+
* `evidence` / `cause` 与四条执行边由一次排查写入(`agent_diagnosis_case` 的 SourceRaw),
|
|
358
|
+
* 不在这里合成 —— 否则一次执行的结果会被伪装成可复用的知识。
|
|
334
359
|
*/
|
|
335
360
|
export function cardsToGraph(cards: DiagnosisCard[]): CompiledDiagnosis {
|
|
336
361
|
const nodes: AttributionNode[] = []
|
|
@@ -343,16 +368,25 @@ export function cardsToGraph(cards: DiagnosisCard[]): CompiledDiagnosis {
|
|
|
343
368
|
seen.add(card.entityId)
|
|
344
369
|
}
|
|
345
370
|
|
|
346
|
-
/**
|
|
371
|
+
/**
|
|
372
|
+
* 资产节点按引用反推:引用层随数据目录同步,卡片里只留 id。
|
|
373
|
+
*
|
|
374
|
+
* `assets[]` 的每一项可以是 `"dwd_ord_di"`,也可以是 `{ id, type }` —— 后者顺带声明
|
|
375
|
+
* 资产的种类,它只影响「查询依据」那一段怎么分组(§7.7 ④),不改变边的两端。
|
|
376
|
+
*/
|
|
347
377
|
const assetIds = new Set<string>()
|
|
348
|
-
const
|
|
378
|
+
const assetTypes = new Map<string, AssetType>()
|
|
379
|
+
const reasonIdByCode = new Map<FailureReason, string>()
|
|
349
380
|
|
|
350
381
|
for (const card of cards) {
|
|
351
|
-
if (card.entityType === 'diagnosis/failure-
|
|
352
|
-
|
|
382
|
+
if (card.entityType === 'diagnosis/failure-reason' && typeof card.block.code === 'string') {
|
|
383
|
+
reasonIdByCode.set(card.block.code as FailureReason, card.entityId)
|
|
353
384
|
}
|
|
354
|
-
if (card.entityType === 'diagnosis/hypothesis'
|
|
355
|
-
for (const
|
|
385
|
+
if (card.entityType === 'diagnosis/hypothesis' || card.entityType === 'diagnosis/check-point') {
|
|
386
|
+
for (const ref of assetRefsOf(card.block.assets)) {
|
|
387
|
+
assetIds.add(ref.id)
|
|
388
|
+
if (ref.type) assetTypes.set(ref.id, ref.type)
|
|
389
|
+
}
|
|
356
390
|
}
|
|
357
391
|
}
|
|
358
392
|
|
|
@@ -393,8 +427,8 @@ export function cardsToGraph(cards: DiagnosisCard[]): CompiledDiagnosis {
|
|
|
393
427
|
if (discriminator) node.discriminator = discriminator
|
|
394
428
|
const notWhen = stringOf(card.block.not_when)
|
|
395
429
|
if (notWhen) node.notWhen = notWhen
|
|
396
|
-
const
|
|
397
|
-
if (
|
|
430
|
+
const failureReason = stringOf(card.block.failure_reason)
|
|
431
|
+
if (failureReason) node.failureReason = failureReason as FailureReason
|
|
398
432
|
const reuse = numberOf(card.block.reuse_count)
|
|
399
433
|
if (reuse !== undefined) node.reuseCount = reuse
|
|
400
434
|
if (typeof card.block.stale === 'boolean') node.stale = card.block.stale
|
|
@@ -402,15 +436,15 @@ export function cardsToGraph(cards: DiagnosisCard[]): CompiledDiagnosis {
|
|
|
402
436
|
for (const [index, target] of listOf(card.block.verified_by).entries()) {
|
|
403
437
|
edges.push({ id: `e-${card.entityId.toLowerCase()}-${String(target).toLowerCase()}-${index}`, source: card.entityId, target: String(target), kind: 'verified_by' })
|
|
404
438
|
}
|
|
405
|
-
for (const [index, asset] of
|
|
406
|
-
edges.push({ id: `e-${
|
|
439
|
+
for (const [index, asset] of assetRefsOf(card.block.assets).entries()) {
|
|
440
|
+
edges.push({ id: `e-${asset.id.toLowerCase()}-${card.entityId.toLowerCase()}-${index}`, source: asset.id, target: card.entityId, kind: 'indexed_from' })
|
|
407
441
|
}
|
|
408
|
-
if (
|
|
409
|
-
const
|
|
410
|
-
if (
|
|
411
|
-
else problems.push(`${card.entityId} 的
|
|
442
|
+
if (failureReason) {
|
|
443
|
+
const reasonNode = reasonIdByCode.get(failureReason as FailureReason)
|
|
444
|
+
if (reasonNode) edges.push({ id: `e-${card.entityId.toLowerCase()}-${reasonNode.toLowerCase()}`, source: card.entityId, target: reasonNode, kind: 'categorized_as' })
|
|
445
|
+
else problems.push(`${card.entityId} 的 failure_reason=${failureReason} 找不到对应的失败原因卡片 —— categorized_as 边无从建立`)
|
|
412
446
|
}
|
|
413
|
-
} else if (card.entityType === 'diagnosis/check') {
|
|
447
|
+
} else if (card.entityType === 'diagnosis/check-point') {
|
|
414
448
|
const cost = numberOf(card.block.cost_minutes)
|
|
415
449
|
if (cost !== undefined) node.costMinutes = cost
|
|
416
450
|
const automation = stringOf(card.block.automation)
|
|
@@ -419,10 +453,18 @@ export function cardsToGraph(cards: DiagnosisCard[]): CompiledDiagnosis {
|
|
|
419
453
|
if (verdictTrue) node.verdictTrue = verdictTrue
|
|
420
454
|
const verdictFalse = stringOf(card.block.verdict_false)
|
|
421
455
|
if (verdictFalse) node.verdictFalse = verdictFalse
|
|
456
|
+
const assets = assetRefsOf(card.block.assets).map(ref => ref.id)
|
|
457
|
+
if (assets.length > 0) node.assets = assets
|
|
422
458
|
if (typeof card.block.stale === 'boolean') node.stale = card.block.stale
|
|
423
|
-
|
|
459
|
+
|
|
460
|
+
// 检查点 → 资产(`queries`):这条检查要去查哪些对象。与假设上的
|
|
461
|
+
// `indexed_from`(这条假设落在哪个对象上)是两条边,方向和语义都不同。
|
|
462
|
+
for (const [index, asset] of assets.entries()) {
|
|
463
|
+
edges.push({ id: `e-${card.entityId.toLowerCase()}-${asset.toLowerCase()}-q${index}`, source: card.entityId, target: asset, kind: 'queries' })
|
|
464
|
+
}
|
|
465
|
+
} else if (card.entityType === 'diagnosis/failure-reason') {
|
|
424
466
|
const code = stringOf(card.block.code)
|
|
425
|
-
if (code) node.code = code as
|
|
467
|
+
if (code) node.code = code as FailureReason
|
|
426
468
|
} else if (card.entityType === 'diagnosis/case') {
|
|
427
469
|
const symptom = stringOf(card.block.symptom)
|
|
428
470
|
if (!symptom) problems.push(`${card.entityId} 没有 symptom —— 案例必须从一个现象进入`)
|
|
@@ -447,7 +489,8 @@ export function cardsToGraph(cards: DiagnosisCard[]): CompiledDiagnosis {
|
|
|
447
489
|
|
|
448
490
|
for (const id of assetIds) {
|
|
449
491
|
if (nodes.some(node => node.id === id)) continue
|
|
450
|
-
|
|
492
|
+
const assetType = assetTypes.get(id)
|
|
493
|
+
nodes.push({ id, kind: 'asset', layer: 'reference', title: id, ...(assetType ? { assetType } : {}) })
|
|
451
494
|
}
|
|
452
495
|
|
|
453
496
|
// 引用完整性:卡片引用了不存在的卡片时,必须报出来(否则图上会出现悬空边,
|
|
@@ -461,6 +504,33 @@ export function cardsToGraph(cards: DiagnosisCard[]): CompiledDiagnosis {
|
|
|
461
504
|
return { graph: { nodes, edges }, cases, problems }
|
|
462
505
|
}
|
|
463
506
|
|
|
507
|
+
/**
|
|
508
|
+
* `assets[]` 的一项 → `{ id, type? }`。
|
|
509
|
+
*
|
|
510
|
+
* 两种写法都认:`"dwd_ord_di"`(只留 id,资产随数据目录同步)与
|
|
511
|
+
* `{ id: "dwd_ord_di", type: "table" }`(顺带声明种类,供「查询依据」那一段分组)。
|
|
512
|
+
* 认不出来的项直接丢掉 —— 造一个 id 是 `[object Object]` 的资产节点,比少一条边难解释。
|
|
513
|
+
*/
|
|
514
|
+
function assetRefsOf(value: unknown): Array<{ id: string; type?: AssetType }> {
|
|
515
|
+
return listOf(value).flatMap((entry): Array<{ id: string; type?: AssetType }> => {
|
|
516
|
+
if (typeof entry === 'string') return entry.trim() ? [{ id: entry.trim() }] : []
|
|
517
|
+
if (entry && typeof entry === 'object') {
|
|
518
|
+
const record = entry as { id?: unknown; type?: unknown }
|
|
519
|
+
const id = stringOf(record.id)
|
|
520
|
+
if (!id) return []
|
|
521
|
+
const type = stringOf(record.type) as AssetType | undefined
|
|
522
|
+
return [{ id, ...(type ? { type } : {}) }]
|
|
523
|
+
}
|
|
524
|
+
return []
|
|
525
|
+
})
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** 写回卡片时用同一条规则:声明过种类就写成对象,否则保持一行一个 id 的短写法。 */
|
|
529
|
+
function assetEntryOf(id: string, types: Map<string, AssetType>): string | { id: string; type: AssetType } {
|
|
530
|
+
const type = types.get(id)
|
|
531
|
+
return type ? { id, type } : id
|
|
532
|
+
}
|
|
533
|
+
|
|
464
534
|
function stringOf(value: unknown): string | undefined {
|
|
465
535
|
return typeof value === 'string' && value.trim() !== '' ? value : undefined
|
|
466
536
|
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AttributionEdge, AttributionGraphData, AttributionNode, CasePathStep, DiagnosisRunEvidence, DiagnosisRunRecord,
|
|
3
|
+
} from './types.js'
|
|
4
|
+
import type { DiagnosisCard } from './cards.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 一次排查的记录 —— `01_source_raw/agent_diagnosis_case/` 那条 SourceRaw 的编解码。
|
|
8
|
+
*
|
|
9
|
+
* ## 为什么它是一条 SourceRaw,而不是一个自造的 jsonl
|
|
10
|
+
*
|
|
11
|
+
* 排查产物要能被 `query_source_truth` 读到、被卡片引用、被 SourceRaw 已有的治理覆盖
|
|
12
|
+
* (分类、版本、检索),就得是一条 SourceRaw。自造一个 jsonl 格式,等于在知识库旁边
|
|
13
|
+
* 再养一套只读不进链路的数据 —— 这也是 0021 §0.3 撤销 `05_diagnosis/` 的直接原因。
|
|
14
|
+
*
|
|
15
|
+
* ## 它与案例卡片的区别
|
|
16
|
+
*
|
|
17
|
+
* | | 谁写 | 性质 |
|
|
18
|
+
* | --- | --- | --- |
|
|
19
|
+
* | 这条 SourceRaw(`agent_diagnosis_case`) | **Agent**,每次排查结束自动写 | 跑出来的**过程记录** |
|
|
20
|
+
* | 案例卡片(`diagnosis/case`) | Agent 提议 → **人确认** | 归纳出来的**结论** |
|
|
21
|
+
*
|
|
22
|
+
* 同一件事可以有两条记录,它们不是重复:一个回答「当时怎么走的」,一个回答「结论是什么」。
|
|
23
|
+
* 而案例卡片必须经人确认 —— 它落在类型层收敛的那一侧,见 0021 §6.1。
|
|
24
|
+
*
|
|
25
|
+
* ## 三段式
|
|
26
|
+
*
|
|
27
|
+
* 与卡片同构:正文小节给人读、` ```json diagnosis-run ` 块给机器读。序和枚举一进散文,
|
|
28
|
+
* 归并算法就再也读不到它了。
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** 机器可读块的围栏标记。与卡片的 `json diagnosis` 分开,两种块不会互相误读。 */
|
|
32
|
+
const RUN_FENCE = 'json diagnosis-run'
|
|
33
|
+
|
|
34
|
+
/** 证据节点的标题取自 `result`;太长就截断,完整值仍在节点上。 */
|
|
35
|
+
const EVIDENCE_TITLE_CHARS = 60
|
|
36
|
+
|
|
37
|
+
export interface DiagnosisRunFile {
|
|
38
|
+
record: DiagnosisRunRecord
|
|
39
|
+
/** 正文小节(人读的部分)。由记录派生,不手写第二份。 */
|
|
40
|
+
sections: Array<{ heading: string; body: string }>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 一次排查的记录 → SourceRaw 正文。
|
|
45
|
+
*
|
|
46
|
+
* 正文按「现象 / 走过的路径 / 结论」三段组织:那是人打开这条来源时想看的三件事,
|
|
47
|
+
* 而机器要读的一切都在下面的块里。
|
|
48
|
+
*/
|
|
49
|
+
export function renderRunSource(record: DiagnosisRunRecord): string {
|
|
50
|
+
const path = record.path.length > 0
|
|
51
|
+
? record.path.map(step => `- \`${step.label}\` ${step.note}${step.refuted ? '(否定)' : ''}`).join('\n')
|
|
52
|
+
: '(这次没有记录路径)'
|
|
53
|
+
const evidence = record.evidence.length > 0
|
|
54
|
+
? record.evidence.map(item => `- \`${item.id}\` 跑 \`${item.checkPointId}\`:${item.result} —— ${verdictWord(item.verdict)}${item.why ? `,因为${item.why}` : ''}`).join('\n')
|
|
55
|
+
: '(这次没有产出证据)'
|
|
56
|
+
const conclusion = record.rootCause
|
|
57
|
+
? [`**根因**:${record.rootCause.text}`, record.rootCause.fixAction ? `**修复**:${record.rootCause.fixAction}` : ''].filter(Boolean).join('\n\n')
|
|
58
|
+
: '(这次没有确认根因)'
|
|
59
|
+
const misleading = record.misleading && record.misleading.length > 0
|
|
60
|
+
? `\n\n## 误导段\n\n${record.misleading.map(item => `- ${item.label}:浪费 ${item.wastedMinutes} 分钟。${item.why}`).join('\n')}`
|
|
61
|
+
: ''
|
|
62
|
+
|
|
63
|
+
return [
|
|
64
|
+
'## 现象',
|
|
65
|
+
'',
|
|
66
|
+
`入口现象 \`${record.symptomId}\`,开始于 ${record.startedAt},总耗时 ${record.durationMinutes} 分钟。`,
|
|
67
|
+
'',
|
|
68
|
+
'## 走过的路径',
|
|
69
|
+
'',
|
|
70
|
+
path,
|
|
71
|
+
'',
|
|
72
|
+
'## 每个检查点的产出',
|
|
73
|
+
'',
|
|
74
|
+
evidence,
|
|
75
|
+
'',
|
|
76
|
+
'## 结论',
|
|
77
|
+
'',
|
|
78
|
+
conclusion,
|
|
79
|
+
misleading,
|
|
80
|
+
'',
|
|
81
|
+
'```' + RUN_FENCE,
|
|
82
|
+
JSON.stringify(record, null, 2),
|
|
83
|
+
'```',
|
|
84
|
+
'',
|
|
85
|
+
].join('\n')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function verdictWord(verdict: DiagnosisRunEvidence['verdict']): string {
|
|
89
|
+
if (verdict === 'supports') return '支持'
|
|
90
|
+
if (verdict === 'refutes') return '否定'
|
|
91
|
+
return '给不出结论'
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** SourceRaw 正文 → 记录。块坏了或缺 runId 时返回 undefined(调用方如实报问题,不猜)。 */
|
|
95
|
+
export function parseRunSource(content: string): DiagnosisRunRecord | undefined {
|
|
96
|
+
const match = content.match(new RegExp('```' + RUN_FENCE + '\\s*\\n([\\s\\S]*?)```'))
|
|
97
|
+
if (!match) return undefined
|
|
98
|
+
let value: unknown
|
|
99
|
+
try { value = JSON.parse(match[1]) } catch { return undefined }
|
|
100
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined
|
|
101
|
+
const record = value as Partial<DiagnosisRunRecord>
|
|
102
|
+
if (typeof record.runId !== 'string' || !record.runId.trim()) return undefined
|
|
103
|
+
if (typeof record.symptomId !== 'string' || !record.symptomId.trim()) return undefined
|
|
104
|
+
return {
|
|
105
|
+
runId: record.runId,
|
|
106
|
+
symptomId: record.symptomId,
|
|
107
|
+
startedAt: typeof record.startedAt === 'string' ? record.startedAt : '',
|
|
108
|
+
durationMinutes: typeof record.durationMinutes === 'number' ? record.durationMinutes : 0,
|
|
109
|
+
path: Array.isArray(record.path) ? record.path as CasePathStep[] : [],
|
|
110
|
+
evidence: Array.isArray(record.evidence) ? record.evidence.filter(isEvidence) : [],
|
|
111
|
+
...(record.rootCause && typeof record.rootCause === 'object' ? { rootCause: record.rootCause } : {}),
|
|
112
|
+
...(Array.isArray(record.misleading) ? { misleading: record.misleading } : {}),
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isEvidence(value: unknown): value is DiagnosisRunEvidence {
|
|
117
|
+
if (!value || typeof value !== 'object') return false
|
|
118
|
+
const item = value as Partial<DiagnosisRunEvidence>
|
|
119
|
+
return typeof item.id === 'string' && typeof item.checkPointId === 'string'
|
|
120
|
+
&& (item.verdict === 'supports' || item.verdict === 'refutes' || item.verdict === 'inconclusive')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 根因节点 id:记录里给了就用它,没给就按假设 id 生成一个稳定的。 */
|
|
124
|
+
export function causeIdOf(record: DiagnosisRunRecord): string | undefined {
|
|
125
|
+
if (!record.rootCause) return undefined
|
|
126
|
+
return record.rootCause.id?.trim() || `C-${record.rootCause.hypothesisId}`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 记录 → 实例层节点与四条执行边。
|
|
131
|
+
*
|
|
132
|
+
* **它只产出执行产物**:`evidence` / `cause` 节点与 `produces` / `supports` / `refutes` /
|
|
133
|
+
* `confirmed_as`。类型层(现象、假设、检查点、失败原因)永远来自卡片 —— 把两者混在一起,
|
|
134
|
+
* 一次执行的结果就会被伪装成可复用的知识(§6.7 的边界表)。
|
|
135
|
+
*/
|
|
136
|
+
export function runToGraph(record: DiagnosisRunRecord): { nodes: AttributionNode[]; edges: AttributionEdge[]; problems: string[] } {
|
|
137
|
+
const nodes: AttributionNode[] = []
|
|
138
|
+
const edges: AttributionEdge[] = []
|
|
139
|
+
const problems: string[] = []
|
|
140
|
+
const seen = new Set<string>()
|
|
141
|
+
|
|
142
|
+
for (const item of record.evidence) {
|
|
143
|
+
if (seen.has(item.id)) problems.push(`证据 ${item.id} 在这次排查里出现了两次 —— 同一份产出只能有一个 id`)
|
|
144
|
+
seen.add(item.id)
|
|
145
|
+
const result = item.result ?? ''
|
|
146
|
+
nodes.push({
|
|
147
|
+
id: item.id,
|
|
148
|
+
kind: 'evidence',
|
|
149
|
+
layer: 'instance',
|
|
150
|
+
title: result.length > EVIDENCE_TITLE_CHARS ? `${result.slice(0, EVIDENCE_TITLE_CHARS)}…` : result || item.id,
|
|
151
|
+
summary: item.why,
|
|
152
|
+
query: item.query,
|
|
153
|
+
result,
|
|
154
|
+
verdict: item.verdict,
|
|
155
|
+
why: item.why,
|
|
156
|
+
at: item.at,
|
|
157
|
+
by: item.by,
|
|
158
|
+
...(item.rawRef ? { rawRef: item.rawRef } : {}),
|
|
159
|
+
})
|
|
160
|
+
edges.push({ id: `run-${record.runId}-${item.checkPointId}-${item.id}`, source: item.checkPointId, target: item.id, kind: 'produces' })
|
|
161
|
+
// 给不出结论的证据**不挂支持 / 否定边**:它不是「弱支持」,它是没有结论。
|
|
162
|
+
// 把它挂成 supports 会让最贵的教训(便宜检查给不出结论)从模型里消失。
|
|
163
|
+
if (item.verdict !== 'inconclusive' && item.hypothesisId) {
|
|
164
|
+
edges.push({
|
|
165
|
+
id: `run-${record.runId}-${item.id}-${item.hypothesisId}`,
|
|
166
|
+
source: item.id,
|
|
167
|
+
target: item.hypothesisId,
|
|
168
|
+
kind: item.verdict === 'supports' ? 'supports' : 'refutes',
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const causeId = causeIdOf(record)
|
|
174
|
+
if (record.rootCause && causeId) {
|
|
175
|
+
nodes.push({
|
|
176
|
+
id: causeId,
|
|
177
|
+
kind: 'cause',
|
|
178
|
+
layer: 'instance',
|
|
179
|
+
title: record.rootCause.text,
|
|
180
|
+
...(record.rootCause.fixAction ? { summary: record.rootCause.fixAction } : {}),
|
|
181
|
+
...(record.rootCause.confirmedAt ? { at: record.rootCause.confirmedAt } : {}),
|
|
182
|
+
})
|
|
183
|
+
edges.push({ id: `run-${record.runId}-${record.rootCause.hypothesisId}-${causeId}`, source: record.rootCause.hypothesisId, target: causeId, kind: 'confirmed_as' })
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return { nodes, edges, problems }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 引用完整性:一次排查里的每个 id 都必须在图上真实存在。
|
|
191
|
+
*
|
|
192
|
+
* 缺了这条,一条写错 id 的诊断日志会在图上留下悬空的执行边 —— 而
|
|
193
|
+
* `validateDiagnosis` 只在读图之后才看得到,那时已经分不清是卡片错了还是日志错了。
|
|
194
|
+
*
|
|
195
|
+
* `knownIds` 要**同时包含卡片那部分与这次排查自己产出的那部分**:路径里的 `E1` / `C1`
|
|
196
|
+
* 正是这次排查自己写下的,只拿卡片图去比会把它们全判成悬空。
|
|
197
|
+
*/
|
|
198
|
+
export function runReferenceProblems(record: DiagnosisRunRecord, knownIds: ReadonlySet<string>): string[] {
|
|
199
|
+
const ids = knownIds
|
|
200
|
+
const problems: string[] = []
|
|
201
|
+
if (!ids.has(record.symptomId)) problems.push(`诊断日志 ${record.runId} 的入口现象 ${record.symptomId} 不存在`)
|
|
202
|
+
for (const item of record.evidence) {
|
|
203
|
+
if (!ids.has(item.checkPointId)) problems.push(`诊断日志 ${record.runId} 的证据 ${item.id} 指向不存在的检查点 ${item.checkPointId}`)
|
|
204
|
+
if (item.hypothesisId && !ids.has(item.hypothesisId)) problems.push(`诊断日志 ${record.runId} 的证据 ${item.id} 指向不存在的假设 ${item.hypothesisId}`)
|
|
205
|
+
}
|
|
206
|
+
if (record.rootCause && !ids.has(record.rootCause.hypothesisId)) {
|
|
207
|
+
problems.push(`诊断日志 ${record.runId} 的根因指向不存在的假设 ${record.rootCause.hypothesisId}`)
|
|
208
|
+
}
|
|
209
|
+
for (const step of record.path) {
|
|
210
|
+
if (!ids.has(step.nodeId)) problems.push(`诊断日志 ${record.runId} 的路径节点 ${step.nodeId} 不存在`)
|
|
211
|
+
}
|
|
212
|
+
return problems
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 把一次排查归纳成一张**案例卡片**(还只是提议,要人确认才生效)。
|
|
217
|
+
*
|
|
218
|
+
* 归纳这一步是「过程记录 → 可复用知识」的那一跳,也是唯一需要人看的一跳:
|
|
219
|
+
* 路径里哪几步其实是误导、根因一句话怎么写,都是判断题。
|
|
220
|
+
*/
|
|
221
|
+
/**
|
|
222
|
+
* 一次排查的案例卡片草案(0044 前叫 `caseProposalOf`)。
|
|
223
|
+
*
|
|
224
|
+
* 改名是因为它产出的东西**直接落卡**,不再是一条提议:名字里留着 `Proposal` 会让下一个
|
|
225
|
+
* 读的人继续在两个时代之间猜。函数体一行没动 —— 它本来就是「从一条排查记录编出一张案例卡」。
|
|
226
|
+
*/
|
|
227
|
+
export function caseCardOf(record: DiagnosisRunRecord, symptomTitle: string): DiagnosisCard {
|
|
228
|
+
const rootCause = record.rootCause?.text ?? '(这次没有确认根因)'
|
|
229
|
+
const firstMisleading = record.misleading?.[0]
|
|
230
|
+
return {
|
|
231
|
+
entityId: `CASE-${record.runId}`,
|
|
232
|
+
entityType: 'diagnosis/case',
|
|
233
|
+
title: symptomTitle,
|
|
234
|
+
status: 'unreviewed',
|
|
235
|
+
sourceRefs: [],
|
|
236
|
+
sections: [
|
|
237
|
+
{ heading: '现象', body: `入口现象 \`${record.symptomId}\`(${symptomTitle}),开始于 ${record.startedAt}。` },
|
|
238
|
+
{ heading: '影响面', body: '(待补:这次影响到了谁、范围多大)' },
|
|
239
|
+
{ heading: '排查过程', body: record.path.map(step => `- \`${step.label}\` ${step.note}${step.refuted ? '(否定)' : ''}`).join('\n') || '(没有记录路径)' },
|
|
240
|
+
{ heading: '根因', body: rootCause },
|
|
241
|
+
{ heading: '修复与防复发', body: record.rootCause?.fixAction ?? '(待补:修复动作与防复发措施)' },
|
|
242
|
+
],
|
|
243
|
+
block: {
|
|
244
|
+
symptom: record.symptomId,
|
|
245
|
+
path: record.path,
|
|
246
|
+
duration_minutes: record.durationMinutes,
|
|
247
|
+
...(firstMisleading ? { misleading: firstMisleading } : {}),
|
|
248
|
+
root_cause: rootCause,
|
|
249
|
+
},
|
|
250
|
+
}
|
|
251
|
+
}
|