dsh-data-domain-knowledge 0.1.0-alpha.2 → 0.1.0-alpha.3
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 +31 -7
- package/cordis.patch.yml +11 -5
- package/lib/client.js +5557 -3056
- package/lib/doc-page.js +7740 -0
- package/lib/types/convert/host.d.ts.map +1 -1
- package/lib/types/convert/host.js +36 -9
- package/lib/types/convert/host.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 +51 -0
- package/lib/types/diagnosis/run.d.ts.map +1 -0
- package/lib/types/diagnosis/run.js +239 -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 +18 -1
- 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 +114 -26
- package/lib/types/diagnosis/store.d.ts.map +1 -1
- package/lib/types/diagnosis/store.js +336 -167
- 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/fs-atomic.d.ts +12 -0
- package/lib/types/fs-atomic.d.ts.map +1 -0
- package/lib/types/fs-atomic.js +42 -0
- package/lib/types/fs-atomic.js.map +1 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +214 -24
- package/lib/types/index.js.map +1 -1
- package/lib/types/knowledge-bases.d.ts +10 -2
- package/lib/types/knowledge-bases.d.ts.map +1 -1
- package/lib/types/knowledge-bases.js +20 -18
- package/lib/types/knowledge-bases.js.map +1 -1
- package/lib/types/proposals.d.ts +166 -0
- package/lib/types/proposals.d.ts.map +1 -0
- package/lib/types/proposals.js +238 -0
- package/lib/types/proposals.js.map +1 -0
- package/lib/types/runtime.d.ts +199 -7
- package/lib/types/runtime.d.ts.map +1 -1
- package/lib/types/runtime.js +728 -145
- package/lib/types/runtime.js.map +1 -1
- package/lib/types/settings-schema.d.ts +2 -2
- package/lib/types/types.d.ts +61 -10
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/types.js +56 -8
- package/lib/types/types.js.map +1 -1
- package/lib/types/wiki-types.d.ts +8 -1
- package/lib/types/wiki-types.d.ts.map +1 -1
- package/lib/types/wiki-types.js +16 -25
- package/lib/types/wiki-types.js.map +1 -1
- package/package.json +4 -3
- package/preset/tita-data-agent/agent.cordis.yml +11 -5
- package/src/client/DirectoryPicker.tsx +12 -12
- package/src/client/InfoHint.tsx +21 -8
- package/src/client/IntentTab.tsx +20 -26
- package/src/client/KnowledgeBaseTab.tsx +190 -80
- package/src/client/KnowledgePanel.tsx +42 -9
- package/src/client/KnowledgeStepRail.tsx +3 -0
- package/src/client/NodeSettings.tsx +20 -22
- package/src/client/PreviewSections.tsx +87 -33
- package/src/client/ProposalReview.tsx +313 -0
- package/src/client/TypeDefinitionDrawer.tsx +72 -38
- package/src/client/WikiTypeEditor.tsx +193 -77
- package/src/client/arch/AgentArchitectureGraph.tsx +143 -14
- package/src/client/arch/NodeLogicPanel.tsx +6 -3
- package/src/client/arch/model.ts +164 -26
- package/src/client/architecture.css +29 -9
- package/src/client/architecture.tsx +54 -29
- package/src/client/attribution/AttributionGraph.tsx +481 -34
- package/src/client/attribution/AttributionTab.tsx +507 -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 +296 -140
- package/src/client/attribution/graph-options.ts +611 -47
- package/src/client/attribution/model.ts +559 -30
- package/src/client/convert/ConvertPanel.tsx +6 -1
- package/src/client/doc-page.css +134 -0
- package/src/client/doc-page.tsx +263 -0
- package/src/client/panel-state.ts +105 -0
- package/src/client/request.ts +36 -0
- package/src/client/styles.css +317 -43
- package/src/client/useFitScale.ts +15 -2
- package/src/client/wiki-type-groups.ts +86 -0
- package/src/convert/host.ts +41 -7
- package/src/diagnosis/cards.ts +120 -50
- package/src/diagnosis/run.ts +245 -0
- package/src/diagnosis/schema.ts +67 -26
- package/src/diagnosis/seed-source.ts +28 -3
- package/src/diagnosis/seed.ts +187 -85
- package/src/diagnosis/store.ts +358 -174
- package/src/diagnosis/types.ts +181 -16
- package/src/document-page.ts +51 -0
- package/src/fs-atomic.ts +42 -0
- package/src/index.ts +209 -22
- package/src/knowledge-bases.ts +20 -18
- package/src/proposals.ts +337 -0
- package/src/runtime.ts +736 -135
- package/src/types.ts +113 -19
- package/src/wiki-types.ts +17 -27
package/src/convert/host.ts
CHANGED
|
@@ -156,7 +156,7 @@ function send(res: ServerResponse, status: number, body: unknown): void {
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
/** Compact task view for tools — the full ingest payload never enters context. */
|
|
159
|
-
function taskView(task: ConvertTask): Record<string, unknown> {
|
|
159
|
+
function taskView(kernel: ConvertService, task: ConvertTask): Record<string, unknown> {
|
|
160
160
|
const manifest = task.manifest
|
|
161
161
|
return {
|
|
162
162
|
taskId: task.id,
|
|
@@ -171,6 +171,8 @@ function taskView(task: ConvertTask): Record<string, unknown> {
|
|
|
171
171
|
error: task.error ?? null,
|
|
172
172
|
warnings: task.warnings.map(warning => warning.code),
|
|
173
173
|
attempts: task.attempts,
|
|
174
|
+
/** 与服务端 retry() 的守卫同源(`canRetryTask`):Agent 不该去点一枚注定失败的按钮。 */
|
|
175
|
+
canRetry: kernel.canRetry(task).ok,
|
|
174
176
|
deliveryDir: task.deliveryDir,
|
|
175
177
|
assetCount: manifest?.assets.length ?? 0,
|
|
176
178
|
wordCount: manifest?.wordCount ?? 0,
|
|
@@ -307,10 +309,24 @@ export function applyConvert(ctx: Context, deps: ConvertHostDeps): ConvertHost {
|
|
|
307
309
|
config = { ...DEFAULT_CONVERT_CONFIG, ...readConfig() }
|
|
308
310
|
void resetService()
|
|
309
311
|
|
|
312
|
+
/**
|
|
313
|
+
* 写操作要用户点头。
|
|
314
|
+
*
|
|
315
|
+
* `convert_task` 是**一个混合工具**:`list` / `get` / `markdown` / `manifest` 只读,
|
|
316
|
+
* `cancel` / `retry` 会改队列。所以它不能整只进 `WRITE_TOOLS`(那会让「看一眼队列」
|
|
317
|
+
* 也要审批),也不能整只留在外面 —— 那样一个自称只读的工具就能取消别人的任务。
|
|
318
|
+
* 判据落在 `action` 上。
|
|
319
|
+
*/
|
|
310
320
|
ctx.on('tools/pre-execute', (exec, next): Promise<PreToolDecision> => {
|
|
311
321
|
if ((WRITE_TOOLS as readonly string[]).includes(exec.name)) {
|
|
312
322
|
return Promise.resolve({ kind: 'ask', reason: '该操作会转换本地内容,并写入 Tita Knowledge Store,需要本轮用户审批。' })
|
|
313
323
|
}
|
|
324
|
+
if (exec.name === 'convert_task') {
|
|
325
|
+
const action = (exec.arguments as { action?: unknown } | undefined)?.action
|
|
326
|
+
if (action === 'cancel' || action === 'retry') {
|
|
327
|
+
return Promise.resolve({ kind: 'ask', reason: `convert_task 的 ${String(action)} 会改动转换队列,需要本轮用户审批。` })
|
|
328
|
+
}
|
|
329
|
+
}
|
|
314
330
|
return next()
|
|
315
331
|
})
|
|
316
332
|
|
|
@@ -402,7 +418,7 @@ export function applyConvert(ctx: Context, deps: ConvertHostDeps): ConvertHost {
|
|
|
402
418
|
const submitted = await kernel.submit(request)
|
|
403
419
|
const shouldWait = args.wait !== false
|
|
404
420
|
const finished = shouldWait ? await waitForTask(kernel, submitted.id, config.toolWaitMs) : submitted
|
|
405
|
-
const view = taskView(finished ?? submitted)
|
|
421
|
+
const view = taskView(kernel, finished ?? submitted)
|
|
406
422
|
return success({
|
|
407
423
|
...view,
|
|
408
424
|
waited: shouldWait,
|
|
@@ -413,7 +429,7 @@ export function applyConvert(ctx: Context, deps: ConvertHostDeps): ConvertHost {
|
|
|
413
429
|
|
|
414
430
|
ctx.tools.register(tool(
|
|
415
431
|
'convert_task',
|
|
416
|
-
'Inspect the conversion queue
|
|
432
|
+
'Inspect and steer the conversion queue. list / get / markdown / manifest are read-only; cancel and retry change the queue (both need user approval). retry only works on a failed or cancelled task whose staged input is still in tmp/ — a delivered task cannot be retried, because its input is cleaned up when it is delivered; submit the file again instead.',
|
|
417
433
|
{
|
|
418
434
|
action: { type: 'string', enum: ['list', 'get', 'cancel', 'retry', 'markdown', 'manifest'], required: true },
|
|
419
435
|
task_id: { type: 'string' },
|
|
@@ -425,12 +441,30 @@ export function applyConvert(ctx: Context, deps: ConvertHostDeps): ConvertHost {
|
|
|
425
441
|
const action = String(args.action)
|
|
426
442
|
if (action === 'list') {
|
|
427
443
|
const limit = Number.isFinite(args.limit) ? Math.max(1, Math.min(100, Number(args.limit))) : 20
|
|
428
|
-
return success({ tasks: kernel.list().slice(0, limit).map(taskView) })
|
|
444
|
+
return success({ tasks: kernel.list().slice(0, limit).map(task => taskView(kernel, task)) })
|
|
429
445
|
}
|
|
430
446
|
const taskId = typeof args.task_id === 'string' ? args.task_id.trim() : ''
|
|
431
447
|
if (!taskId) return failure({ taskId: null }, 'TASK_ID_REQUIRED', `${action} 需要 task_id。`)
|
|
432
|
-
|
|
433
|
-
|
|
448
|
+
/*
|
|
449
|
+
* `cancel` / `retry` **会改队列**,所以它们不把 `AdapterError` 抛出去让宿主兜,
|
|
450
|
+
* 而是把 code 与 suggestion 如实放进 envelope:Agent 读到的应该是「为什么不能」
|
|
451
|
+
* 与「那该怎么办」(例如:已交付的任务要**重新提交文件**,而不是重试 ——
|
|
452
|
+
* 它的输入在交付后就清理了)。
|
|
453
|
+
*/
|
|
454
|
+
if (action === 'cancel' || action === 'retry') {
|
|
455
|
+
try {
|
|
456
|
+
return success(taskView(kernel, action === 'cancel' ? kernel.cancel(taskId) : await kernel.retry(taskId)))
|
|
457
|
+
} catch (cause) {
|
|
458
|
+
const error = cause as { code?: unknown; message?: unknown; suggestion?: unknown }
|
|
459
|
+
return failure(
|
|
460
|
+
{ taskId },
|
|
461
|
+
typeof error.code === 'string' ? error.code : 'CONVERT_TASK_FAILED',
|
|
462
|
+
typeof error.message === 'string' ? error.message : String(cause),
|
|
463
|
+
false,
|
|
464
|
+
typeof error.suggestion === 'string' ? error.suggestion : undefined,
|
|
465
|
+
)
|
|
466
|
+
}
|
|
467
|
+
}
|
|
434
468
|
if (action === 'markdown') {
|
|
435
469
|
const file = await kernel.readDeliveryFile(taskId, 'index.md')
|
|
436
470
|
if (!file) return failure({ taskId }, 'ARTIFACT_NOT_FOUND', '该任务没有 index.md(未成功或已被清理)。')
|
|
@@ -443,7 +477,7 @@ export function applyConvert(ctx: Context, deps: ConvertHostDeps): ConvertHost {
|
|
|
443
477
|
}
|
|
444
478
|
const task = kernel.get(taskId)
|
|
445
479
|
if (!task) return failure({ taskId }, 'TASK_NOT_FOUND', `任务不存在:${taskId}`)
|
|
446
|
-
return success(taskView(task))
|
|
480
|
+
return success(taskView(kernel, task))
|
|
447
481
|
},
|
|
448
482
|
))
|
|
449
483
|
|
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_truth` 里、不是 `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_truth/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
|
}
|