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/store.ts
CHANGED
|
@@ -1,45 +1,47 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFile, readdir } from 'node:fs/promises'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
-
import { SEED_CASES, SEED_GRAPH
|
|
3
|
+
import { SEED_CASES, SEED_GRAPH } from './seed.js'
|
|
4
4
|
import { seedSources, type SeedSource } from './seed-source.js'
|
|
5
|
-
import {
|
|
5
|
+
import { validateDiagnosis, type DiagnosisIssue } from './schema.js'
|
|
6
6
|
import {
|
|
7
|
-
cardsToGraph,
|
|
7
|
+
cardsToGraph, CARD_EDGE_KINDS, graphToCards, parseCard, serializeCard,
|
|
8
8
|
RUN_EDGE_KINDS, type DiagnosisCard,
|
|
9
9
|
} from './cards.js'
|
|
10
|
-
import
|
|
10
|
+
import { caseCardOf, parseRunSource, renderRunSource, runReferenceProblems, runToGraph } from './run.js'
|
|
11
|
+
import type {
|
|
12
|
+
AttributionCase, AttributionEdge, AttributionGraphData, AttributionNode, AttributionNodeKind, DiagnosisRunRecord,
|
|
13
|
+
} from './types.js'
|
|
11
14
|
import type { KnowledgeRuntime } from '../runtime.js'
|
|
12
15
|
import type { SchemaType } from '../types.js'
|
|
13
16
|
|
|
14
17
|
/**
|
|
15
|
-
* `05_diagnosis/`
|
|
18
|
+
* 排查知识的读取与写入(v1.1 起:**没有 `05_diagnosis/` 了**)。
|
|
16
19
|
*
|
|
17
|
-
* ##
|
|
18
|
-
*
|
|
19
|
-
* 改版前这里同时扮演两个角色:既是 seed 写出来的 jsonl 数据集,又被当成图的唯一来源。
|
|
20
|
-
* 后果是排查思路**从来没进过 wiki** —— 不在 `01_source_truth`、不是 `02_wiki` 的卡片、
|
|
21
|
-
* 编译器不认它的 entity_type,于是不会被 wikiNote 化、不会进向量索引,**永远召不回来**。
|
|
22
|
-
* 而 `store.ts` 顶上那句「全部内容都能从 wiki 卡片确定性重建」描述的其实是目标状态,
|
|
23
|
-
* 代码走的是相反的路径。
|
|
24
|
-
*
|
|
25
|
-
* ## 现在的分工
|
|
20
|
+
* ## 三段,各一个答案
|
|
26
21
|
*
|
|
27
22
|
* | 目录 | 内容 | 谁是真相 |
|
|
28
23
|
* | --- | --- | --- |
|
|
29
|
-
* | `
|
|
30
|
-
* | `
|
|
31
|
-
* | `
|
|
24
|
+
* | `01_source_raw/question/` | 手册与复盘原文 | 人写的原始材料 |
|
|
25
|
+
* | `01_source_raw/agent_diagnosis_case/` | 一次排查的过程记录(一条 SourceRaw) | Agent 跑出来的原始记录 |
|
|
26
|
+
* | `02_wiki/diagnosis/` | 五类卡片(三段式) | **知识侧的唯一真相**,由一次排查 / 载入示例 / 迁移直接写入(0044 起不经人确认) |
|
|
27
|
+
*
|
|
28
|
+
* `05_diagnosis/` 在 v1.1 撤销了(0021 §0.3)。它同时装着两类东西 —— 「一次排查的产物」
|
|
29
|
+
* (源头)与「五类模板卡」(配置)—— 一个编号段混装两种性质,`rebuild --only` 的语义
|
|
30
|
+
* 立刻含糊。而且它自造的 jsonl 过不了 `source_refs` 那道门:卡片要求引用**真实存在的
|
|
31
|
+
* SourceRaw**,jsonl 不是。
|
|
32
|
+
*
|
|
33
|
+
* 旧的 `05_diagnosis/*.jsonl` 因此降为**只读的迁移输入与历史快照**:还在,还能读,
|
|
34
|
+
* 但不再被写、也不再是任何东西的真相。
|
|
35
|
+
*
|
|
36
|
+
* ## 读取顺序是显式的(`snapshot().source` 会把它报给界面)
|
|
32
37
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
38
|
+
* 1. 有卡片 → 卡片 + 一次排查的 SourceRaw;
|
|
39
|
+
* 2. 没有卡片但有旧 jsonl → 按旧格式读出来(并尝试**无损**前向迁移);
|
|
40
|
+
* 3. 两者都没有 → **什么都不写**:空态 + 「载入内置示例」按钮。
|
|
35
41
|
*/
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const RUN_NODE_SHARDS: Record<string, AttributionNodeKind> = {
|
|
40
|
-
'evidence.jsonl': 'evidence',
|
|
41
|
-
'causes.jsonl': 'cause',
|
|
42
|
-
}
|
|
42
|
+
const LEGACY_DIR = '05_diagnosis'
|
|
43
|
+
/** 一次排查的产物:`01_source_raw/<这个分类>/`。 */
|
|
44
|
+
const RUN_SOURCE_TYPE = 'agent_diagnosis_case'
|
|
43
45
|
|
|
44
46
|
/**
|
|
45
47
|
* 旧版的类型层分片。**只读,不再写**:v0.7 之前它们就是真相,现在它们的角色是
|
|
@@ -48,48 +50,52 @@ const RUN_NODE_SHARDS: Record<string, AttributionNodeKind> = {
|
|
|
48
50
|
const LEGACY_SHARDS: Record<string, AttributionNodeKind> = {
|
|
49
51
|
'symptoms.jsonl': 'symptom',
|
|
50
52
|
'hypotheses.jsonl': 'hypothesis',
|
|
51
|
-
'checks.jsonl': '
|
|
52
|
-
'failure-modes.jsonl': '
|
|
53
|
+
'checks.jsonl': 'check_point',
|
|
54
|
+
'failure-modes.jsonl': 'failure_reason',
|
|
53
55
|
'assets.jsonl': 'asset',
|
|
54
56
|
'cases.jsonl': 'case',
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
\`02_wiki/diagnosis/\`,出处是 \`01_source_truth/\` 里的手册与复盘。图由卡片编译而来 ——
|
|
63
|
-
所以改知识要改卡片(或改它的来源文档再重新编译),不要改这里。
|
|
64
|
-
|
|
65
|
-
\`*.jsonl\` 里若出现 \`symptoms\` / \`hypotheses\` / \`checks\` / \`failure-modes\` / \`assets\`
|
|
66
|
-
等分片,那是 v0.7 之前的历史快照:版本升级时被读出来迁移成了卡片,之后不再更新。
|
|
67
|
-
`
|
|
59
|
+
/** 一次排查写下的节点(旧格式)。同样只读。 */
|
|
60
|
+
const LEGACY_RUN_SHARDS: Record<string, AttributionNodeKind> = {
|
|
61
|
+
'evidence.jsonl': 'evidence',
|
|
62
|
+
'causes.jsonl': 'cause',
|
|
63
|
+
}
|
|
68
64
|
|
|
69
65
|
/** 读取顺序的显式来源,界面据此说明「现在看到的是哪一份」。 */
|
|
70
|
-
export type DiagnosisSource = 'cards' | 'jsonl' | '
|
|
66
|
+
export type DiagnosisSource = 'cards' | 'jsonl' | 'empty'
|
|
71
67
|
|
|
72
68
|
export interface DiagnosisSnapshot {
|
|
73
69
|
graph: AttributionGraphData
|
|
74
70
|
cases: AttributionCase[]
|
|
75
71
|
issues: DiagnosisIssue[]
|
|
76
|
-
/**
|
|
72
|
+
/** 数据是不是本次调用从内置示例写进去的(只有 `loadSample()` 会把它变成 true)。 */
|
|
77
73
|
seeded: boolean
|
|
78
74
|
/** 旧知识根是否在本次被前向迁移成卡片。 */
|
|
79
75
|
migrated: boolean
|
|
80
76
|
/** 读的是哪一份真相。 */
|
|
81
77
|
source: DiagnosisSource
|
|
82
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* 这个库还什么都没有 —— 界面据此显示空态(说什么会有、怎么开始),
|
|
80
|
+
* 而不是画一张空图。
|
|
81
|
+
*/
|
|
82
|
+
canLoadSample: boolean
|
|
83
|
+
/** 写卡片 / 迁移 / 读诊断日志过程中发现的问题。必须能显示出来,不能只进日志。 */
|
|
83
84
|
problems: string[]
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
/**
|
|
87
|
-
|
|
87
|
+
/**
|
|
88
|
+
* 只取运行时里真正要用的那几件事:读根目录、落 SourceRaw、写卡片、列来源。
|
|
89
|
+
*
|
|
90
|
+
* `loadSources` 是 v1.1 新增的:一次排查的产物现在是一条 `agent_diagnosis_case`
|
|
91
|
+
* 的 SourceRaw,归因图读的是它,而不是自造的 jsonl。
|
|
92
|
+
*/
|
|
93
|
+
type DiagnosisHost = Pick<KnowledgeRuntime, 'rootDir' | 'addSource' | 'writeCard' | 'loadSources'>
|
|
88
94
|
|
|
89
95
|
export class DiagnosisStore {
|
|
90
96
|
constructor(private readonly host: DiagnosisHost) {}
|
|
91
97
|
|
|
92
|
-
private get
|
|
98
|
+
private get legacyDir(): string { return join(this.host.rootDir, LEGACY_DIR) }
|
|
93
99
|
private get cardsDir(): string { return join(this.host.rootDir, '02_wiki', 'diagnosis') }
|
|
94
100
|
|
|
95
101
|
/**
|
|
@@ -99,85 +105,151 @@ export class DiagnosisStore {
|
|
|
99
105
|
* 2. 没有卡片但有旧 jsonl → **前向迁移**:把旧图反推成卡片写出去,并**校验迁移无损**
|
|
100
106
|
* (重新编译一遍与旧图比对,不一致就不认这次迁移,继续按 jsonl 读 —— 宁可留在旧形态,
|
|
101
107
|
* 也不要静默丢知识);
|
|
102
|
-
* 3. 两者都没有 →
|
|
108
|
+
* 3. 两者都没有 → **什么都不写**。这个库还没开始做排查。
|
|
109
|
+
*
|
|
110
|
+
* 第 3 条原来是自动写入内置示例。实跑反馈把它改掉了:那份示例长得像真的业务文档,
|
|
111
|
+
* 而它落在用户自己的 `01_source_raw/` 与 `02_wiki/` 里 —— 新装一台机器,一打开
|
|
112
|
+
* 「智能归因」就凭空多出几份来源和几十张卡片。示例还在(`loadSample()`),
|
|
113
|
+
* 但改成**用户点一下才载入**。
|
|
114
|
+
*
|
|
115
|
+
* **v1.1 起连模板文件也不铺了**:模板卡由类型定义派生(0021 §6.6),落成磁盘文件会
|
|
116
|
+
* 造出「设置里改了、磁盘上那份没改」的第二真源。
|
|
103
117
|
*/
|
|
104
118
|
async ensure(): Promise<{ seeded: boolean; migrated: boolean; problems: string[] }> {
|
|
105
|
-
await mkdir(this.dir, { recursive: true })
|
|
106
|
-
const templates = join(this.dir, 'templates')
|
|
107
|
-
await mkdir(templates, { recursive: true })
|
|
108
|
-
await Promise.all(DIAGNOSIS_ENTITIES.map(spec =>
|
|
109
|
-
this.writeIfAbsent(join(templates, `${spec.entityType.replace(/\//g, '-')}.md`), templateCard(spec))))
|
|
110
|
-
// 目录角色的说明也要落盘:下一个人只打开 05_diagnosis 时,得知道知识已经搬走了。
|
|
111
|
-
await this.writeIfAbsent(join(this.dir, 'README.md'), README)
|
|
112
|
-
// 注意:**不要**在这里为一次排查的产物建空占位文件。它们走 `writeIfAbsent`,
|
|
113
|
-
// 而「占位」会在首次 seed 之前就把文件创建出来,于是 seed 的真实内容被判为「已存在」
|
|
114
|
-
// 而写不进去 —— 结果是图上少了 evidence / cause 五个节点,且没有任何报错。
|
|
115
|
-
|
|
116
119
|
if ((await this.readCards()).length > 0) return { seeded: false, migrated: false, problems: [] }
|
|
117
120
|
if ((await this.legacyShardFiles()).length > 0) return await this.migrate()
|
|
118
|
-
return
|
|
121
|
+
return { seeded: false, migrated: false, problems: [] }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 载入**内置示例**:一份排查手册 + 三篇复盘 + 三次排查日志 → SourceRaw → 卡片 → 图。
|
|
126
|
+
*
|
|
127
|
+
* 只有用户明确点了「载入内置示例」才走这里(`ensure()` 不再自动调用)。幂等:
|
|
128
|
+
* `addSource` 按 `externalId`、`writeCard` 按 entityId,点两次不会写出第二份。
|
|
129
|
+
*
|
|
130
|
+
* **它走的是「提议 → 确认」那条门,不是绕过它**(0021 §6.1)。区别只在确认者是谁:
|
|
131
|
+
* 用户点「载入内置示例」这个动作本身就是**对整批示例的确认**,所以这里给每条提议记下
|
|
132
|
+
* `confirmedBy = user:load-sample`,然后照常走 `writeCard`。这样做的两个好处:
|
|
133
|
+
* 待办区里能看到这批卡片是怎么来的;而「未确认不落 `02_wiki/`」这条规则没有被开一个
|
|
134
|
+
* 后门 —— 例外是**确认者的身份**,不是门的缺失。
|
|
135
|
+
*/
|
|
136
|
+
async loadSample(): Promise<{ seeded: boolean; migrated: boolean; problems: string[] }> {
|
|
137
|
+
const { problems: sourceProblems, refs } = await this.writeSources(seedSources())
|
|
138
|
+
const cards = withSourceRefs(graphToCards(SEED_GRAPH, SEED_CASES), refs, SEED_CASES)
|
|
139
|
+
const problems = [...sourceProblems, ...await this.writeCards(cards, 'user:load-sample')]
|
|
140
|
+
return { seeded: true, migrated: false, problems }
|
|
119
141
|
}
|
|
120
142
|
|
|
121
143
|
async snapshot(): Promise<DiagnosisSnapshot> {
|
|
122
144
|
const { seeded, migrated, problems } = await this.ensure()
|
|
123
145
|
const cards = await this.readCards()
|
|
124
|
-
|
|
146
|
+
/**
|
|
147
|
+
* 「什么都没有」= 没有卡片、也没有旧 jsonl。它是**可以载入内置示例**的唯一条件:
|
|
148
|
+
* 已经有知识(不管哪一代形态)时不该再往里塞示例 —— 那会变成第二份互相矛盾的知识。
|
|
149
|
+
*/
|
|
150
|
+
const canLoadSample = cards.length === 0 && (await this.legacyShardFiles()).length === 0
|
|
125
151
|
|
|
126
152
|
if (cards.length > 0) {
|
|
127
|
-
const
|
|
128
|
-
const graph: AttributionGraphData = {
|
|
129
|
-
nodes: [...compiled.graph.nodes, ...run.nodes],
|
|
130
|
-
edges: [...compiled.graph.edges, ...run.edges],
|
|
131
|
-
}
|
|
153
|
+
const built = await this.buildGraph(cards)
|
|
132
154
|
return {
|
|
133
|
-
graph,
|
|
134
|
-
cases:
|
|
135
|
-
issues: validateDiagnosis(graph),
|
|
155
|
+
graph: built.graph,
|
|
156
|
+
cases: built.cases,
|
|
157
|
+
issues: validateDiagnosis(built.graph),
|
|
136
158
|
seeded,
|
|
137
159
|
migrated,
|
|
138
160
|
source: 'cards',
|
|
139
|
-
|
|
161
|
+
canLoadSample: false,
|
|
162
|
+
problems: [...problems, ...built.problems],
|
|
140
163
|
}
|
|
141
164
|
}
|
|
142
165
|
|
|
143
|
-
// 还没有卡片:旧 jsonl
|
|
144
|
-
const graph = await this.
|
|
145
|
-
const cases = await this.
|
|
166
|
+
// 还没有卡片:旧 jsonl 就是当前可读的来源;两份都没有就是一个空库。
|
|
167
|
+
const graph = await this.readLegacyGraph()
|
|
168
|
+
const cases = await this.readLegacyCases()
|
|
146
169
|
return {
|
|
147
170
|
graph,
|
|
148
171
|
cases,
|
|
149
172
|
issues: validateDiagnosis(graph),
|
|
150
173
|
seeded,
|
|
151
174
|
migrated,
|
|
152
|
-
source:
|
|
175
|
+
source: canLoadSample ? 'empty' : 'jsonl',
|
|
176
|
+
canLoadSample,
|
|
153
177
|
problems,
|
|
154
178
|
}
|
|
155
179
|
}
|
|
156
180
|
|
|
157
181
|
/**
|
|
158
|
-
*
|
|
182
|
+
* 图 = **卡片那张图** + **一次排查的产物**。
|
|
159
183
|
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
184
|
+
* 两条来源严格分工:类型层与引用层(以及六种卡片边)全部来自卡片;`evidence` / `cause`
|
|
185
|
+
* 与四条执行边全部来自 `agent_diagnosis_case` 的 SourceRaw。合成一张图是**读**的时候
|
|
186
|
+
* 做的,不是写的时候 —— 写的时候合成,等于让一次执行的结果变成可复用的知识。
|
|
162
187
|
*/
|
|
188
|
+
private async buildGraph(cards: DiagnosisCard[]): Promise<{ graph: AttributionGraphData; cases: AttributionCase[]; problems: string[] }> {
|
|
189
|
+
const compiled = cardsToGraph(cards)
|
|
190
|
+
const problems = [...compiled.problems]
|
|
191
|
+
const nodes: AttributionNode[] = [...compiled.graph.nodes]
|
|
192
|
+
const edges: AttributionEdge[] = [...compiled.graph.edges]
|
|
193
|
+
|
|
194
|
+
const runs = await this.readRuns()
|
|
195
|
+
problems.push(...runs.problems)
|
|
196
|
+
for (const record of runs.records) {
|
|
197
|
+
const run = runToGraph(record)
|
|
198
|
+
// 引用完整性要**连这次排查自己产出的 id 一起算**:路径里的 `E1` / `C1` 正是它写下的。
|
|
199
|
+
const known = new Set([...compiled.graph.nodes.map(node => node.id), ...run.nodes.map(node => node.id)])
|
|
200
|
+
problems.push(...run.problems, ...runReferenceProblems(record, known))
|
|
201
|
+
nodes.push(...run.nodes)
|
|
202
|
+
edges.push(...run.edges)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* **旧 jsonl 里的执行产物继续可读。**
|
|
207
|
+
*
|
|
208
|
+
* 它们是只读的历史快照(v0.7 之前一次排查的产物就写在那里),而卡片表达不了它们
|
|
209
|
+
* —— `evidence` / `cause` 与四条执行边不属于任何卡片。所以迁移只搬「卡片能表达的那
|
|
210
|
+
* 部分」,这一部分留在原处继续供读。少了这一段,一次旧知识根迁移之后图会静默地
|
|
211
|
+
* 少掉全部证据与根因,而迁移还报成功。
|
|
212
|
+
*/
|
|
213
|
+
const legacyRun = await this.readLegacyRunArtifacts()
|
|
214
|
+
nodes.push(...legacyRun.nodes)
|
|
215
|
+
edges.push(...legacyRun.edges)
|
|
216
|
+
|
|
217
|
+
return { graph: dedupe({ nodes, edges }, problems), cases: compiled.cases, problems }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 旧 `05_diagnosis/` 里的执行产物:`evidence.jsonl` / `causes.jsonl` / `edges.jsonl`。
|
|
222
|
+
*
|
|
223
|
+
* 只取**执行边**:卡片边由卡片投影出来,这里再读一遍就会出现重复边。同时按 id 去重
|
|
224
|
+
* —— 已经由 `agent_diagnosis_case` 解析出来的节点不重复加(新旧两代可能描述同一次排查)。
|
|
225
|
+
*/
|
|
226
|
+
private async readLegacyRunArtifacts(): Promise<{ nodes: AttributionNode[]; edges: AttributionEdge[] }> {
|
|
227
|
+
const nodes: AttributionNode[] = []
|
|
228
|
+
for (const file of Object.keys(LEGACY_RUN_SHARDS)) {
|
|
229
|
+
for (const row of await this.readLines(join(this.legacyDir, file))) {
|
|
230
|
+
try { nodes.push(JSON.parse(row) as AttributionNode) } catch { /* 跳过损坏行 */ }
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const edges: AttributionEdge[] = []
|
|
234
|
+
for (const row of await this.readLines(join(this.legacyDir, 'edges.jsonl'))) {
|
|
235
|
+
try {
|
|
236
|
+
const edge = JSON.parse(row) as AttributionEdge
|
|
237
|
+
if (RUN_EDGE_KINDS.includes(edge.kind)) edges.push(edge)
|
|
238
|
+
} catch { /* 同上 */ }
|
|
239
|
+
}
|
|
240
|
+
return { nodes, edges }
|
|
241
|
+
}
|
|
242
|
+
|
|
163
243
|
async readGraph(): Promise<AttributionGraphData> {
|
|
164
244
|
const cards = await this.readCards()
|
|
165
|
-
if (cards.length > 0)
|
|
166
|
-
const compiled = cardsToGraph(cards)
|
|
167
|
-
const run = await this.readRunArtifacts()
|
|
168
|
-
return { nodes: [...compiled.graph.nodes, ...run.nodes], edges: [...compiled.graph.edges, ...run.edges] }
|
|
169
|
-
}
|
|
245
|
+
if (cards.length > 0) return (await this.buildGraph(cards)).graph
|
|
170
246
|
return await this.readLegacyGraph()
|
|
171
247
|
}
|
|
172
248
|
|
|
173
249
|
async readCases(): Promise<AttributionCase[]> {
|
|
174
250
|
const cards = await this.readCards()
|
|
175
251
|
if (cards.length > 0) return cardsToGraph(cards).cases
|
|
176
|
-
|
|
177
|
-
const raw = await readFile(join(this.dir, 'cases.json'), 'utf8')
|
|
178
|
-
const parsed = JSON.parse(raw) as AttributionCase[]
|
|
179
|
-
return Array.isArray(parsed) ? parsed : []
|
|
180
|
-
} catch { return [] }
|
|
252
|
+
return await this.readLegacyCases()
|
|
181
253
|
}
|
|
182
254
|
|
|
183
255
|
/* ────────────────────────────────────────────── 卡片:读写 ── */
|
|
@@ -192,42 +264,66 @@ export class DiagnosisStore {
|
|
|
192
264
|
return cards
|
|
193
265
|
}
|
|
194
266
|
|
|
195
|
-
|
|
196
|
-
|
|
267
|
+
/* ────────────────────────────────────────────── 一次排查的记录 ── */
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* 读全部 `agent_diagnosis_case` 来源,解析出排查记录。
|
|
271
|
+
*
|
|
272
|
+
* 块坏了、`runId` 缺了、`symptomId` 缺了就**跳过并如实报问题** —— 不猜。
|
|
273
|
+
* 一条坏日志不该让整张图读不出来,但它也不能被静默吞掉。
|
|
274
|
+
*/
|
|
275
|
+
async readRuns(): Promise<{ records: DiagnosisRunRecord[]; problems: string[] }> {
|
|
276
|
+
const records: DiagnosisRunRecord[] = []
|
|
197
277
|
const problems: string[] = []
|
|
198
|
-
for (const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
content: renderBody(card),
|
|
204
|
-
sourceRefs: card.sourceRefs,
|
|
205
|
-
status: card.status as never,
|
|
206
|
-
...(card.summary ? { summary: card.summary } : {}),
|
|
207
|
-
...(card.owner ? { owner: card.owner } : {}),
|
|
208
|
-
})
|
|
209
|
-
if (written.ok === false) problems.push(`卡片 ${card.entityId} 写入失败:${written.error.code} ${written.error.message}`)
|
|
278
|
+
for (const source of await this.host.loadSources()) {
|
|
279
|
+
if (source.sourceType !== RUN_SOURCE_TYPE) continue
|
|
280
|
+
const record = parseRunSource(source.content)
|
|
281
|
+
if (record) records.push(record)
|
|
282
|
+
else problems.push(`诊断日志 ${source.sourceId} 里没有可解析的 \`json diagnosis-run\` 块,已跳过`)
|
|
210
283
|
}
|
|
211
|
-
return problems
|
|
284
|
+
return { records, problems }
|
|
212
285
|
}
|
|
213
286
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
287
|
+
/**
|
|
288
|
+
* 一次排查结束:写一条 `agent_diagnosis_case` 的 SourceRaw,并**直接写入**一张案例卡片。
|
|
289
|
+
*
|
|
290
|
+
* 0044:案例卡片原来只提议、等人确认。与编译那条链路一样改成直接入库 ——
|
|
291
|
+
* 一次排查的价值就在于「当时是怎么走的」,让它停在待办队列里等于把经验丢掉。
|
|
292
|
+
* 卡片会被标成 `unreviewed`,人在「知识库 → Wiki」里 review 它。
|
|
293
|
+
*
|
|
294
|
+
* 幂等:`externalId` = runId,同一次排查重跑不会写出第二条来源;卡片按 entityId 写入。
|
|
295
|
+
*/
|
|
296
|
+
async recordRun(record: DiagnosisRunRecord): Promise<{ sourceId?: string; entityId?: string; problems: string[] }> {
|
|
297
|
+
const problems: string[] = []
|
|
298
|
+
const added = await this.host.addSource({
|
|
299
|
+
sourceType: RUN_SOURCE_TYPE,
|
|
300
|
+
title: `AI 诊断日志 · ${record.symptomId} · ${record.runId}`,
|
|
301
|
+
content: renderRunSource(record),
|
|
302
|
+
externalId: record.runId,
|
|
303
|
+
sourceId: `${RUN_SOURCE_TYPE}:${record.runId}`,
|
|
304
|
+
})
|
|
305
|
+
if (added.ok === false) return { problems: [`诊断日志 ${record.runId} 写入失败:${added.error.code} ${added.error.message}`] }
|
|
306
|
+
const sourceId = added.data.sourceId
|
|
307
|
+
|
|
308
|
+
const symptomTitle = (await this.readCards()).find(card => card.entityId === record.symptomId)?.title ?? record.symptomId
|
|
309
|
+
const draft = caseCardOf(record, symptomTitle)
|
|
310
|
+
const written = await this.host.writeCard({
|
|
311
|
+
entityType: draft.entityType as SchemaType,
|
|
312
|
+
entityId: draft.entityId,
|
|
313
|
+
title: draft.title,
|
|
314
|
+
content: renderCardBody(draft),
|
|
315
|
+
sourceRefs: [sourceId],
|
|
316
|
+
status: 'unreviewed',
|
|
317
|
+
})
|
|
318
|
+
if (written.ok === false) problems.push(`案例卡片 ${draft.entityId} 写入失败:${written.error.code} ${written.error.message}`)
|
|
319
|
+
return { sourceId, ...(written.ok === false ? {} : { entityId: draft.entityId }), problems }
|
|
224
320
|
}
|
|
225
321
|
|
|
226
322
|
/* ────────────────────────────────────────────── 旧知识根迁移 ── */
|
|
227
323
|
|
|
228
324
|
private async migrate(): Promise<{ seeded: boolean; migrated: boolean; problems: string[] }> {
|
|
229
325
|
const legacy = await this.readLegacyGraph()
|
|
230
|
-
const cases = await this.
|
|
326
|
+
const cases = await this.readLegacyCases()
|
|
231
327
|
|
|
232
328
|
// **先在内存里验证,通过才写盘。**
|
|
233
329
|
//
|
|
@@ -235,19 +331,23 @@ export class DiagnosisStore {
|
|
|
235
331
|
// 就宁可留在旧形态,也不要静默丢知识。而「先写再验、失败回滚」会留一个更糟的中间态:
|
|
236
332
|
// 卡片已经落盘,下一次读就成了卡片来源 —— 于是分歧只在第一次读时被报出来,
|
|
237
333
|
// 之后再也看不见。所以顺序是:编译 → 比对 → 才写。
|
|
238
|
-
const run = await this.readRunArtifacts()
|
|
239
334
|
const draft = graphToCards(legacy, cases)
|
|
240
335
|
const compiled = cardsToGraph(draft)
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
336
|
+
/**
|
|
337
|
+
* 比对的是**卡片能表达的那部分**。
|
|
338
|
+
*
|
|
339
|
+
* `evidence` / `cause` 与四条执行边不在卡片里(§6.7 的边界表),它们留在
|
|
340
|
+
* `05_diagnosis/` 里继续供读(见 `readLegacyRunArtifacts`)。把它们算进比对,
|
|
341
|
+
* 结果是**任何带排查产物的旧知识根都迁不动** —— 而失败方式是静默的:图少了一半,
|
|
342
|
+
* 迁移还报成功。
|
|
343
|
+
*/
|
|
344
|
+
const divergence = compareGraphs(cardExpressible(legacy), compiled.graph)
|
|
245
345
|
if (divergence) {
|
|
246
346
|
return { seeded: false, migrated: false, problems: [`迁移未生效(仍按 05_diagnosis/*.jsonl 读):${divergence}`] }
|
|
247
347
|
}
|
|
248
348
|
|
|
249
349
|
const { problems: sourceProblems, refs } = await this.writeSources(seedSources())
|
|
250
|
-
const problems = [...sourceProblems, ...await this.writeCards(withSourceRefs(draft, refs,
|
|
350
|
+
const problems = [...sourceProblems, ...await this.writeCards(withSourceRefs(draft, refs, cases), 'migration:05_diagnosis')]
|
|
251
351
|
return { seeded: false, migrated: problems.length === 0, problems }
|
|
252
352
|
}
|
|
253
353
|
|
|
@@ -275,58 +375,56 @@ export class DiagnosisStore {
|
|
|
275
375
|
return { problems, refs }
|
|
276
376
|
}
|
|
277
377
|
|
|
278
|
-
/* ──────────────────────────────────────────────
|
|
378
|
+
/* ────────────────────────────────────────────── 写卡片 ── */
|
|
279
379
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
380
|
+
/**
|
|
381
|
+
* 写一批卡片。返回写失败的原因(类型不认、出处不存在等),由调用方决定怎么显示。
|
|
382
|
+
*
|
|
383
|
+
* 0044:**直接写**,不再先提一条提议再确认再落卡。`confirmedBy` 现在只是一个
|
|
384
|
+
* 归因用的标签(载入示例时是 `user:load-sample`,迁移时是迁移逻辑),不再是准入凭证 ——
|
|
385
|
+
* 凭证这道门已经不存在了。留它是因为它进 `writeCard` 的 lineage 留痕,
|
|
386
|
+
* 而「这张卡片是哪一次动作产生的」仍然值得回答。
|
|
387
|
+
*/
|
|
388
|
+
private async writeCards(cards: DiagnosisCard[], confirmedBy: string): Promise<string[]> {
|
|
389
|
+
const problems: string[] = []
|
|
390
|
+
for (const card of cards) {
|
|
391
|
+
const written = await this.host.writeCard({
|
|
392
|
+
entityType: card.entityType as SchemaType,
|
|
393
|
+
entityId: card.entityId,
|
|
394
|
+
title: card.title,
|
|
395
|
+
content: renderCardBody(card),
|
|
396
|
+
sourceRefs: card.sourceRefs,
|
|
397
|
+
status: card.status as never,
|
|
398
|
+
...(card.summary ? { summary: card.summary } : {}),
|
|
399
|
+
...(card.owner ? { owner: card.owner } : {}),
|
|
400
|
+
})
|
|
401
|
+
if (written.ok === false) problems.push(`卡片 ${card.entityId} 写入失败:${written.error.code} ${written.error.message}`)
|
|
294
402
|
}
|
|
295
|
-
|
|
403
|
+
if (problems.length > 0) problems.push(`(这一批卡片由 ${confirmedBy} 写入)`)
|
|
404
|
+
return problems
|
|
296
405
|
}
|
|
297
406
|
|
|
298
|
-
|
|
299
|
-
for (const [file, kind] of Object.entries(RUN_NODE_SHARDS)) {
|
|
300
|
-
const rows = graph.nodes.filter(node => node.kind === kind)
|
|
301
|
-
await this.writeIfAbsent(join(this.dir, file), rows.map(row => JSON.stringify(row)).join('\n'))
|
|
302
|
-
}
|
|
303
|
-
await this.writeIfAbsent(
|
|
304
|
-
join(this.dir, 'edges.jsonl'),
|
|
305
|
-
graph.edges.filter(edge => RUN_EDGE_KINDS.includes(edge.kind)).map(edge => JSON.stringify(edge)).join('\n'),
|
|
306
|
-
)
|
|
307
|
-
}
|
|
407
|
+
/* ────────────────────────────────────────────── 旧读法(只读) ── */
|
|
308
408
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
await this.writeIfAbsent(join(this.dir, 'cases.json'), JSON.stringify(SEED_CASES, null, 2))
|
|
409
|
+
private async readLegacyCases(): Promise<AttributionCase[]> {
|
|
410
|
+
try {
|
|
411
|
+
const raw = await readFile(join(this.legacyDir, 'cases.json'), 'utf8')
|
|
412
|
+
const parsed = JSON.parse(raw) as AttributionCase[]
|
|
413
|
+
return Array.isArray(parsed) ? parsed : []
|
|
414
|
+
} catch { return [] }
|
|
316
415
|
}
|
|
317
416
|
|
|
318
|
-
/* ────────────────────────────────────────────── 旧读法 ── */
|
|
319
|
-
|
|
320
417
|
private async readLegacyGraph(): Promise<AttributionGraphData> {
|
|
321
418
|
const nodes: AttributionNode[] = []
|
|
322
|
-
for (const
|
|
323
|
-
for (const row of await this.readLines(join(this.
|
|
419
|
+
for (const file of Object.keys({ ...LEGACY_SHARDS, ...LEGACY_RUN_SHARDS })) {
|
|
420
|
+
for (const row of await this.readLines(join(this.legacyDir, file))) {
|
|
324
421
|
try { nodes.push(JSON.parse(row) as AttributionNode) } catch { /* 跳过损坏行 */ }
|
|
325
422
|
}
|
|
326
|
-
void kind
|
|
327
423
|
}
|
|
328
424
|
const edges: AttributionEdge[] = []
|
|
329
|
-
for (const row of await this.readLines(join(this.
|
|
425
|
+
for (const row of await this.readLines(join(this.legacyDir, 'edges.jsonl'))) {
|
|
426
|
+
// **全部边都读**,不筛:旧文件是**迁移输入**,六种卡片边正是要从这里反推成
|
|
427
|
+
// 卡片字段的。只在读「当前图」时才筛执行边(`readLegacyRunArtifacts`)。
|
|
330
428
|
try { edges.push(JSON.parse(row) as AttributionEdge) } catch { /* 同上 */ }
|
|
331
429
|
}
|
|
332
430
|
return { nodes, edges }
|
|
@@ -335,14 +433,13 @@ export class DiagnosisStore {
|
|
|
335
433
|
/**
|
|
336
434
|
* 是否存在**值得迁移的旧知识根**。
|
|
337
435
|
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
* 迁移分支,然后在空图上白跑一趟。
|
|
436
|
+
* 只看旧版的**类型层**分片,而且要求它真的有内容:空文件不算「旧知识根」,
|
|
437
|
+
* 否则首次启动会走进迁移分支,然后在空图上白跑一趟。
|
|
341
438
|
*/
|
|
342
439
|
private async legacyShardFiles(): Promise<string[]> {
|
|
343
440
|
const found: string[] = []
|
|
344
441
|
for (const file of Object.keys(LEGACY_SHARDS)) {
|
|
345
|
-
if ((await this.readLines(join(this.
|
|
442
|
+
if ((await this.readLines(join(this.legacyDir, file))).length > 0) found.push(file)
|
|
346
443
|
}
|
|
347
444
|
return found
|
|
348
445
|
}
|
|
@@ -352,17 +449,10 @@ export class DiagnosisStore {
|
|
|
352
449
|
return (await readFile(path, 'utf8')).split('\n').map(line => line.trim()).filter(Boolean)
|
|
353
450
|
} catch { return [] }
|
|
354
451
|
}
|
|
355
|
-
|
|
356
|
-
private async writeIfAbsent(path: string, content: string): Promise<void> {
|
|
357
|
-
try {
|
|
358
|
-
await readFile(path)
|
|
359
|
-
return
|
|
360
|
-
} catch { await writeFile(path, content, 'utf8') }
|
|
361
|
-
}
|
|
362
452
|
}
|
|
363
453
|
|
|
364
454
|
/** 卡片正文:小节 + 结构化块。复用编解码里的同一套序列化,避免两处格式漂移。 */
|
|
365
|
-
function
|
|
455
|
+
export function renderCardBody(card: DiagnosisCard): string {
|
|
366
456
|
const text = serializeCard(card)
|
|
367
457
|
return text.slice(text.indexOf('\n---\n', 3) + 5).trim()
|
|
368
458
|
}
|
|
@@ -376,7 +466,6 @@ function renderBody(card: DiagnosisCard): string {
|
|
|
376
466
|
function withSourceRefs(
|
|
377
467
|
cards: DiagnosisCard[],
|
|
378
468
|
refs: Map<string, string>,
|
|
379
|
-
graph: AttributionGraphData,
|
|
380
469
|
cases: AttributionCase[],
|
|
381
470
|
): DiagnosisCard[] {
|
|
382
471
|
const handbook = refs.get('handbook')
|
|
@@ -388,7 +477,6 @@ function withSourceRefs(
|
|
|
388
477
|
caseOf.set(item.symptomId, ref)
|
|
389
478
|
for (const step of item.path) if (!caseOf.has(step.nodeId)) caseOf.set(step.nodeId, ref)
|
|
390
479
|
}
|
|
391
|
-
void graph
|
|
392
480
|
return cards.map(card => {
|
|
393
481
|
const own = card.entityType === 'diagnosis/case' ? refs.get(card.entityId) : undefined
|
|
394
482
|
const ref = own ?? caseOf.get(card.entityId) ?? handbook
|
|
@@ -396,6 +484,57 @@ function withSourceRefs(
|
|
|
396
484
|
})
|
|
397
485
|
}
|
|
398
486
|
|
|
487
|
+
/**
|
|
488
|
+
* 按 id 去重。
|
|
489
|
+
*
|
|
490
|
+
* 新旧两代可能描述**同一次排查**:一条 `agent_diagnosis_case` 的 SourceRaw(v1.1 起)
|
|
491
|
+
* 与 `05_diagnosis/evidence.jsonl`(历史快照)里会有同一个 `E1`。不去重的后果是图上
|
|
492
|
+
* 出现两个 `E1` —— 而画布按 id 建节点,第二个会静默覆盖第一个的位置与样式,
|
|
493
|
+
* 读起来像「这次排查跑了两遍」。
|
|
494
|
+
*
|
|
495
|
+
* 同 id 但 kind 不同是真冲突(同一份知识被两处定义成两样东西),报出来而不是静默取第一个。
|
|
496
|
+
*/
|
|
497
|
+
function dedupe(graph: AttributionGraphData, problems: string[]): AttributionGraphData {
|
|
498
|
+
const nodes: AttributionNode[] = []
|
|
499
|
+
const byId = new Map<string, AttributionNode>()
|
|
500
|
+
for (const node of graph.nodes) {
|
|
501
|
+
const previous = byId.get(node.id)
|
|
502
|
+
if (previous) {
|
|
503
|
+
if (previous.kind !== node.kind) problems.push(`节点 ${node.id} 被定义成了两种类型(${previous.kind} / ${node.kind})—— 只保留第一个`)
|
|
504
|
+
continue
|
|
505
|
+
}
|
|
506
|
+
byId.set(node.id, node)
|
|
507
|
+
nodes.push(node)
|
|
508
|
+
}
|
|
509
|
+
const edges: AttributionEdge[] = []
|
|
510
|
+
const seen = new Set<string>()
|
|
511
|
+
for (const edge of graph.edges) {
|
|
512
|
+
const key = `${edge.kind}:${edge.source}->${edge.target}`
|
|
513
|
+
if (seen.has(key)) continue
|
|
514
|
+
seen.add(key)
|
|
515
|
+
edges.push(edge)
|
|
516
|
+
}
|
|
517
|
+
return { nodes, edges }
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** 只能由一次排查产出的节点类型。它们不属于任何卡片(§6.7 的边界表)。 */
|
|
521
|
+
const RUN_ONLY_KINDS: AttributionNodeKind[] = ['evidence', 'cause', 'unknown']
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* 旧图里**卡片能表达的那部分**:类型层、引用层的资产、案例桩,以及六种卡片边。
|
|
525
|
+
*
|
|
526
|
+
* 执行产物(`evidence` / `cause` / `unknown` 与四条执行边)不在其中 —— 它们的归宿是
|
|
527
|
+
* `agent_diagnosis_case` 的 SourceRaw,而不是卡片。
|
|
528
|
+
*/
|
|
529
|
+
function cardExpressible(graph: AttributionGraphData): AttributionGraphData {
|
|
530
|
+
const nodes = graph.nodes.filter(node => !RUN_ONLY_KINDS.includes(node.kind))
|
|
531
|
+
const ids = new Set(nodes.map(node => node.id))
|
|
532
|
+
return {
|
|
533
|
+
nodes,
|
|
534
|
+
edges: graph.edges.filter(edge => CARD_EDGE_KINDS.includes(edge.kind) && ids.has(edge.source) && ids.has(edge.target)),
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
399
538
|
/**
|
|
400
539
|
* 两张图是否一致。返回 `undefined` 表示一致,否则返回第一条差异的人话描述。
|
|
401
540
|
*
|
|
@@ -428,5 +567,3 @@ async function walkMarkdown(dir: string): Promise<string[]> {
|
|
|
428
567
|
}
|
|
429
568
|
return found
|
|
430
569
|
}
|
|
431
|
-
|
|
432
|
-
export { cardPathOf }
|