dsh-data-domain-knowledge 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +360 -0
- package/cordis.patch.yml +410 -0
- package/eval/README.md +76 -0
- package/eval/evaluate-retrieval.mjs +121 -0
- package/eval/queries.example.jsonl +2 -0
- package/lib/client.js +111415 -0
- package/lib/types/config-schema.d.ts +35 -0
- package/lib/types/config-schema.d.ts.map +1 -0
- package/lib/types/config-schema.js +34 -0
- package/lib/types/config-schema.js.map +1 -0
- package/lib/types/convert/core-loader.d.ts +32 -0
- package/lib/types/convert/core-loader.d.ts.map +1 -0
- package/lib/types/convert/core-loader.js +60 -0
- package/lib/types/convert/core-loader.js.map +1 -0
- package/lib/types/convert/host.d.ts +88 -0
- package/lib/types/convert/host.d.ts.map +1 -0
- package/lib/types/convert/host.js +479 -0
- package/lib/types/convert/host.js.map +1 -0
- package/lib/types/convert/ingest.d.ts +125 -0
- package/lib/types/convert/ingest.d.ts.map +1 -0
- package/lib/types/convert/ingest.js +228 -0
- package/lib/types/convert/ingest.js.map +1 -0
- package/lib/types/convert/settings-schema.d.ts +18 -0
- package/lib/types/convert/settings-schema.d.ts.map +1 -0
- package/lib/types/convert/settings-schema.js +26 -0
- package/lib/types/convert/settings-schema.js.map +1 -0
- package/lib/types/convert/types.d.ts +46 -0
- package/lib/types/convert/types.d.ts.map +1 -0
- package/lib/types/convert/types.js +41 -0
- package/lib/types/convert/types.js.map +1 -0
- package/lib/types/diagnosis/cards.d.ts +71 -0
- package/lib/types/diagnosis/cards.d.ts.map +1 -0
- package/lib/types/diagnosis/cards.js +482 -0
- package/lib/types/diagnosis/cards.js.map +1 -0
- package/lib/types/diagnosis/problem-classes.d.ts +51 -0
- package/lib/types/diagnosis/problem-classes.d.ts.map +1 -0
- package/lib/types/diagnosis/problem-classes.js +66 -0
- package/lib/types/diagnosis/problem-classes.js.map +1 -0
- package/lib/types/diagnosis/schema.d.ts +65 -0
- package/lib/types/diagnosis/schema.d.ts.map +1 -0
- package/lib/types/diagnosis/schema.js +208 -0
- package/lib/types/diagnosis/schema.js.map +1 -0
- package/lib/types/diagnosis/seed-source.d.ts +20 -0
- package/lib/types/diagnosis/seed-source.d.ts.map +1 -0
- package/lib/types/diagnosis/seed-source.js +96 -0
- package/lib/types/diagnosis/seed-source.js.map +1 -0
- package/lib/types/diagnosis/seed.d.ts +50 -0
- package/lib/types/diagnosis/seed.d.ts.map +1 -0
- package/lib/types/diagnosis/seed.js +242 -0
- package/lib/types/diagnosis/seed.js.map +1 -0
- package/lib/types/diagnosis/store.d.ts +81 -0
- package/lib/types/diagnosis/store.d.ts.map +1 -0
- package/lib/types/diagnosis/store.js +409 -0
- package/lib/types/diagnosis/store.js.map +1 -0
- package/lib/types/diagnosis/types.d.ts +124 -0
- package/lib/types/diagnosis/types.d.ts.map +1 -0
- package/lib/types/diagnosis/types.js +13 -0
- package/lib/types/diagnosis/types.js.map +1 -0
- package/lib/types/directory-picker.d.ts +94 -0
- package/lib/types/directory-picker.d.ts.map +1 -0
- package/lib/types/directory-picker.js +110 -0
- package/lib/types/directory-picker.js.map +1 -0
- package/lib/types/extract-model.d.ts +76 -0
- package/lib/types/extract-model.d.ts.map +1 -0
- package/lib/types/extract-model.js +193 -0
- package/lib/types/extract-model.js.map +1 -0
- package/lib/types/extract.d.ts +121 -0
- package/lib/types/extract.d.ts.map +1 -0
- package/lib/types/extract.js +549 -0
- package/lib/types/extract.js.map +1 -0
- package/lib/types/fts.d.ts +96 -0
- package/lib/types/fts.d.ts.map +1 -0
- package/lib/types/fts.js +184 -0
- package/lib/types/fts.js.map +1 -0
- package/lib/types/index.d.ts +39 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +1112 -0
- package/lib/types/index.js.map +1 -0
- package/lib/types/intent/engine.d.ts +108 -0
- package/lib/types/intent/engine.d.ts.map +1 -0
- package/lib/types/intent/engine.js +501 -0
- package/lib/types/intent/engine.js.map +1 -0
- package/lib/types/intent/host.d.ts +88 -0
- package/lib/types/intent/host.d.ts.map +1 -0
- package/lib/types/intent/host.js +389 -0
- package/lib/types/intent/host.js.map +1 -0
- package/lib/types/intent/preset-tools.d.ts +37 -0
- package/lib/types/intent/preset-tools.d.ts.map +1 -0
- package/lib/types/intent/preset-tools.js +45 -0
- package/lib/types/intent/preset-tools.js.map +1 -0
- package/lib/types/intent/settings-schema.d.ts +59 -0
- package/lib/types/intent/settings-schema.d.ts.map +1 -0
- package/lib/types/intent/settings-schema.js +57 -0
- package/lib/types/intent/settings-schema.js.map +1 -0
- package/lib/types/intent/store.d.ts +75 -0
- package/lib/types/intent/store.d.ts.map +1 -0
- package/lib/types/intent/store.js +139 -0
- package/lib/types/intent/store.js.map +1 -0
- package/lib/types/intent/tools.d.ts +63 -0
- package/lib/types/intent/tools.d.ts.map +1 -0
- package/lib/types/intent/tools.js +108 -0
- package/lib/types/intent/tools.js.map +1 -0
- package/lib/types/intent/types.d.ts +189 -0
- package/lib/types/intent/types.d.ts.map +1 -0
- package/lib/types/intent/types.js +62 -0
- package/lib/types/intent/types.js.map +1 -0
- package/lib/types/knowledge-bases.d.ts +266 -0
- package/lib/types/knowledge-bases.d.ts.map +1 -0
- package/lib/types/knowledge-bases.js +1107 -0
- package/lib/types/knowledge-bases.js.map +1 -0
- package/lib/types/layout.d.ts +262 -0
- package/lib/types/layout.d.ts.map +1 -0
- package/lib/types/layout.js +312 -0
- package/lib/types/layout.js.map +1 -0
- package/lib/types/migrate.d.ts +110 -0
- package/lib/types/migrate.d.ts.map +1 -0
- package/lib/types/migrate.js +280 -0
- package/lib/types/migrate.js.map +1 -0
- package/lib/types/recall-rank.d.ts +158 -0
- package/lib/types/recall-rank.d.ts.map +1 -0
- package/lib/types/recall-rank.js +261 -0
- package/lib/types/recall-rank.js.map +1 -0
- package/lib/types/runtime.d.ts +568 -0
- package/lib/types/runtime.d.ts.map +1 -0
- package/lib/types/runtime.js +3045 -0
- package/lib/types/runtime.js.map +1 -0
- package/lib/types/settings-schema.d.ts +117 -0
- package/lib/types/settings-schema.d.ts.map +1 -0
- package/lib/types/settings-schema.js +92 -0
- package/lib/types/settings-schema.js.map +1 -0
- package/lib/types/settings-volatile.d.ts +102 -0
- package/lib/types/settings-volatile.d.ts.map +1 -0
- package/lib/types/settings-volatile.js +114 -0
- package/lib/types/settings-volatile.js.map +1 -0
- package/lib/types/types.d.ts +799 -0
- package/lib/types/types.d.ts.map +1 -0
- package/lib/types/types.js +387 -0
- package/lib/types/types.js.map +1 -0
- package/lib/types/wiki-types.d.ts +68 -0
- package/lib/types/wiki-types.d.ts.map +1 -0
- package/lib/types/wiki-types.js +322 -0
- package/lib/types/wiki-types.js.map +1 -0
- package/package.json +101 -0
- package/preset/tita-data-agent/agent.cordis.yml +376 -0
- package/preset/tita-data-agent/preset.yml +3 -0
- package/python/requirements.txt +8 -0
- package/scripts/doctor.mjs +70 -0
- package/scripts/e2e-pipeline.mjs +355 -0
- package/scripts/install-agent-preset.mjs +92 -0
- package/scripts/setup-embedding.sh +34 -0
- package/scripts/sync-agent-preset.mjs +188 -0
- package/src/client/AgentPresetCard.tsx +247 -0
- package/src/client/DirectoryPicker.tsx +357 -0
- package/src/client/InfoHint.tsx +65 -0
- package/src/client/IntentLogicTab.tsx +185 -0
- package/src/client/IntentTab.tsx +715 -0
- package/src/client/KnowledgeBaseTab.tsx +1454 -0
- package/src/client/KnowledgeGraphView.tsx +306 -0
- package/src/client/KnowledgePanel.tsx +381 -0
- package/src/client/KnowledgeStepRail.tsx +60 -0
- package/src/client/NodeSettings.tsx +392 -0
- package/src/client/PreviewSections.tsx +116 -0
- package/src/client/RecallPanel.tsx +399 -0
- package/src/client/TypeDefinitionDrawer.tsx +215 -0
- package/src/client/WikiFlowCard.tsx +226 -0
- package/src/client/WikiTypeEditor.tsx +332 -0
- package/src/client/agent-preset.ts +121 -0
- package/src/client/arch/AgentArchitectureGraph.tsx +219 -0
- package/src/client/arch/NodeLogicPanel.tsx +133 -0
- package/src/client/arch/ProblemDetailPanel.tsx +88 -0
- package/src/client/arch/model.ts +691 -0
- package/src/client/architecture.css +191 -0
- package/src/client/architecture.tsx +530 -0
- package/src/client/attribution/AttributionGraph.tsx +104 -0
- package/src/client/attribution/AttributionTab.tsx +332 -0
- package/src/client/attribution/CaseView.tsx +102 -0
- package/src/client/attribution/GraphStage.tsx +87 -0
- package/src/client/attribution/RunView.tsx +184 -0
- package/src/client/attribution/StageChrome.tsx +403 -0
- package/src/client/attribution/ViewSwitch.tsx +107 -0
- package/src/client/attribution/graph-options.ts +381 -0
- package/src/client/attribution/model.ts +462 -0
- package/src/client/config-scope.ts +119 -0
- package/src/client/convert/ConvertPanel.tsx +594 -0
- package/src/client/convert/styles.css +138 -0
- package/src/client/convert/styles.ts +49 -0
- package/src/client/format.ts +39 -0
- package/src/client/graph/layout.ts +181 -0
- package/src/client/graph.css +113 -0
- package/src/client/index.ts +98 -0
- package/src/client/problem-domains.ts +114 -0
- package/src/client/styles.css +940 -0
- package/src/client/styles.ts +11 -0
- package/src/client/useFitScale.ts +68 -0
- package/src/config-schema.ts +45 -0
- package/src/convert/core-loader.ts +68 -0
- package/src/convert/host.ts +579 -0
- package/src/convert/ingest.ts +367 -0
- package/src/convert/settings-schema.ts +27 -0
- package/src/convert/types.ts +65 -0
- package/src/diagnosis/cards.ts +498 -0
- package/src/diagnosis/problem-classes.ts +117 -0
- package/src/diagnosis/schema.ts +260 -0
- package/src/diagnosis/seed-source.ts +107 -0
- package/src/diagnosis/seed.ts +291 -0
- package/src/diagnosis/store.ts +432 -0
- package/src/diagnosis/types.ts +152 -0
- package/src/directory-picker.ts +164 -0
- package/src/extract-model.ts +227 -0
- package/src/extract.ts +632 -0
- package/src/fts.ts +203 -0
- package/src/index.ts +1091 -0
- package/src/intent/README.md +63 -0
- package/src/intent/engine.ts +527 -0
- package/src/intent/host.ts +462 -0
- package/src/intent/preset-tools.ts +48 -0
- package/src/intent/settings-schema.ts +63 -0
- package/src/intent/store.ts +143 -0
- package/src/intent/tools.ts +139 -0
- package/src/intent/types.ts +245 -0
- package/src/knowledge-bases.ts +1160 -0
- package/src/layout.ts +350 -0
- package/src/migrate.ts +262 -0
- package/src/python/embedding_worker.py +471 -0
- package/src/recall-rank.ts +299 -0
- package/src/runtime.ts +3069 -0
- package/src/settings-schema.ts +94 -0
- package/src/settings-volatile.ts +122 -0
- package/src/types.ts +1140 -0
- package/src/wiki-types.ts +357 -0
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
import { DIAGNOSIS_ENTITIES, type DiagnosisEntityType } from './schema.js'
|
|
2
|
+
import type { AttributionCase } from './seed.js'
|
|
3
|
+
import type {
|
|
4
|
+
AttributionEdge, AttributionEdgeKind, AttributionGraphData, AttributionNode, AttributionNodeKind,
|
|
5
|
+
FailureMode,
|
|
6
|
+
} from './types.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 诊断卡片的**编解码**:markdown 卡片 ⇄ 图上的节点与边(0021 §6.3)。
|
|
10
|
+
*
|
|
11
|
+
* ## 为什么这一层必须存在
|
|
12
|
+
*
|
|
13
|
+
* 在它之前,智能归因的图只有一条来源:`seed.ts` 里的 TypeScript 字面量,被
|
|
14
|
+
* `DiagnosisStore.ensure()` 直接写成 `05_diagnosis/*.jsonl`。那意味着**排查思路从来没有
|
|
15
|
+
* 进过 wiki**:它不在 `01_source_truth` 里、不是 `02_wiki` 的卡片、编译器连
|
|
16
|
+
* `diagnosis/hypothesis` 这个 entity_type 都不认 —— 于是它既不会被 wikiNote 化,
|
|
17
|
+
* 也不会进向量索引,**召回永远召不到它**。而「遇到类似问题先召回」正是这个产品的主张。
|
|
18
|
+
*
|
|
19
|
+
* 这一层把图变成卡片的**投影**:卡片是手写的(人写、Agent 写、从 SourceRaw 编译而来),
|
|
20
|
+
* 图是算出来的。§4「存图、显树」讲的是同一件事 —— 存的是知识,图是它的一种读法。
|
|
21
|
+
*
|
|
22
|
+
* ## 卡片三段式与「谁由卡片定义」的边界
|
|
23
|
+
*
|
|
24
|
+
* ```
|
|
25
|
+
* front matter ── 信封:id / entity_type / title / summary / status / owner / source_refs
|
|
26
|
+
* 正文小节 ────── 人读、给向量召回(可 diff、可评审)
|
|
27
|
+
* 结构化块 ────── 机器读:枚举、引用、**顺序**、逐边属性
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* 边界要说清楚,否则「为什么这张图里有东西不在卡片里」会变成一个反复出现的疑问:
|
|
31
|
+
*
|
|
32
|
+
* | 图上的东西 | 谁定义 | 为什么 |
|
|
33
|
+
* | --- | --- | --- |
|
|
34
|
+
* | symptom / hypothesis / check / failure-mode / case 节点 | **卡片** | 这五类是人写的知识(§6.4) |
|
|
35
|
+
* | asset 节点 | 引用反推 | 它随数据目录同步,不手写;卡片里只留 id |
|
|
36
|
+
* | 现象→假设 / 假设→检查 / 假设→失败模式 / 资产→假设 / 案例→现象 边 | **卡片** | 都是卡片字段的确定性投影 |
|
|
37
|
+
* | evidence / cause 节点,produces / supports / refutes / confirmed_as 边 | **一次排查** | 它们是一次执行的结果,不是知识;由排查自动写入(§6.4) |
|
|
38
|
+
*
|
|
39
|
+
* 最后一行不是妥协,是分层:把「上次查到什么」写进卡片,等于让一份可复用的知识
|
|
40
|
+
* 绑死在一次具体的执行上。
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/** 卡片正文里那个机器可读块的围栏标记。用带名字的围栏,方便人和编辑器识别。 */
|
|
44
|
+
const BLOCK_FENCE = 'json diagnosis'
|
|
45
|
+
|
|
46
|
+
export interface DiagnosisCardSection {
|
|
47
|
+
heading: string
|
|
48
|
+
body: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface DiagnosisCard {
|
|
52
|
+
entityId: string
|
|
53
|
+
entityType: DiagnosisEntityType
|
|
54
|
+
title: string
|
|
55
|
+
/** 一句话说明。落到节点上是 `summary`(现象上是 `observable`)。 */
|
|
56
|
+
summary?: string
|
|
57
|
+
status: string
|
|
58
|
+
owner?: string
|
|
59
|
+
sourceRefs: string[]
|
|
60
|
+
updatedAt?: string
|
|
61
|
+
/** 正文小节,顺序按 spec。 */
|
|
62
|
+
sections: DiagnosisCardSection[]
|
|
63
|
+
/** 结构化块。字段随 entity_type 而定,见 `CARD_LAYOUT`。 */
|
|
64
|
+
block: Record<string, unknown>
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 卡片不声明状态时的默认值:它已经被人写过一遍,只是还没走评审流程。 */
|
|
68
|
+
export const DEFAULT_CARD_STATUS = 'reviewed'
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 每种卡片:结构化块里放哪些字段,以及哪个字段是「节点的一句话说明」。
|
|
72
|
+
*
|
|
73
|
+
* 这张表是**唯一的映射声明**。校验、序列化、反序列化都读它 —— 三处各写一份字段清单,
|
|
74
|
+
* 迟早会出现「校验要求 A、写盘漏了 B、读回来又不认识 C」。
|
|
75
|
+
*/
|
|
76
|
+
const CARD_LAYOUT: Record<DiagnosisEntityType, { block: string[]; summaryFrom: string }> = {
|
|
77
|
+
'diagnosis/symptom': {
|
|
78
|
+
// 现象的「一句话说明」就是它的可观测口径 —— §6.4 把 observable 定成必填正因如此。
|
|
79
|
+
block: ['observable', 'entry_when', 'problem_class', 'problem_subclass', 'candidates'],
|
|
80
|
+
summaryFrom: 'observable',
|
|
81
|
+
},
|
|
82
|
+
'diagnosis/hypothesis': {
|
|
83
|
+
block: ['discriminator', 'not_when', 'failure_mode', 'verified_by', 'assets', 'reuse_count', 'stale'],
|
|
84
|
+
summaryFrom: 'summary',
|
|
85
|
+
},
|
|
86
|
+
'diagnosis/check': {
|
|
87
|
+
block: ['cost_minutes', 'automation', 'verdict_true', 'verdict_false', 'stale'],
|
|
88
|
+
summaryFrom: 'summary',
|
|
89
|
+
},
|
|
90
|
+
'diagnosis/failure-mode': {
|
|
91
|
+
block: ['code', 'label', 'boundary'],
|
|
92
|
+
summaryFrom: 'summary',
|
|
93
|
+
},
|
|
94
|
+
'diagnosis/case': {
|
|
95
|
+
block: ['symptom', 'path', 'duration_minutes', 'misleading', 'root_cause', 'impact', 'prevention'],
|
|
96
|
+
summaryFrom: 'summary',
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 反向查表:节点 kind → 卡片 entity_type。`asset` 与执行产物没有卡片。 */
|
|
101
|
+
export function entityTypeOf(kind: AttributionNodeKind): DiagnosisEntityType | undefined {
|
|
102
|
+
return DIAGNOSIS_ENTITIES.find(spec => spec.kind === kind)?.entityType
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 卡片能定义的节点类型。其余(asset / evidence / cause / unknown)不走卡片。 */
|
|
106
|
+
export const CARD_NODE_KINDS: AttributionNodeKind[] = DIAGNOSIS_ENTITIES.map(spec => spec.kind)
|
|
107
|
+
|
|
108
|
+
/** 卡片能定义的边:全部是卡片字段的确定性投影。 */
|
|
109
|
+
export const CARD_EDGE_KINDS: AttributionEdgeKind[] = [
|
|
110
|
+
'may_be_caused_by', 'verified_by', 'categorized_as', 'indexed_from', 'instance_of',
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
/** 一次排查写下的边。它们不属于任何卡片,见文件头的边界表。 */
|
|
114
|
+
export const RUN_EDGE_KINDS: AttributionEdgeKind[] = ['produces', 'supports', 'refutes', 'confirmed_as']
|
|
115
|
+
|
|
116
|
+
/* ─────────────────────────────────────────────── 序列化:卡片 → markdown ── */
|
|
117
|
+
|
|
118
|
+
function frontMatterValue(value: unknown): string {
|
|
119
|
+
return typeof value === 'string' ? value : JSON.stringify(value)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 卡片 → markdown 文本。
|
|
124
|
+
*
|
|
125
|
+
* 结构化块放在**正文里**而不是 front matter 的 JSON 里:改一个字段就从「一个几百字符的
|
|
126
|
+
* 长行整体重写」变成「一行改动」,review 时才看得出改了哪条判据、哪一步顺序。
|
|
127
|
+
* 这正是 §6.3 把三段分开的理由。
|
|
128
|
+
*/
|
|
129
|
+
export function serializeCard(card: DiagnosisCard): string {
|
|
130
|
+
const front: Array<[string, unknown]> = [
|
|
131
|
+
['entity_id', card.entityId],
|
|
132
|
+
['entity_type', card.entityType],
|
|
133
|
+
['title', card.title],
|
|
134
|
+
...(card.summary ? [['summary', card.summary] as [string, string]] : []),
|
|
135
|
+
['status', card.status],
|
|
136
|
+
...(card.owner ? [['owner', card.owner] as [string, string]] : []),
|
|
137
|
+
['source_refs', card.sourceRefs],
|
|
138
|
+
...(card.updatedAt ? [['updated_at', card.updatedAt] as [string, string]] : []),
|
|
139
|
+
]
|
|
140
|
+
const body = card.sections
|
|
141
|
+
.filter(section => section.body.trim() !== '')
|
|
142
|
+
.map(section => `## ${section.heading}\n\n${section.body.trim()}`)
|
|
143
|
+
.join('\n\n')
|
|
144
|
+
const block = JSON.stringify(card.block, null, 2)
|
|
145
|
+
return `---\n${front.map(([key, value]) => `${key}: ${frontMatterValue(value)}`).join('\n')}\n---\n\n${body}\n\n\`\`\`${BLOCK_FENCE}\n${block}\n\`\`\`\n`
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* ─────────────────────────────────────────────── 反序列化:markdown → 卡片 ── */
|
|
149
|
+
|
|
150
|
+
export function parseCard(source: string): DiagnosisCard | undefined {
|
|
151
|
+
// 复用运行时的 front matter 解析(同一个 yaml 子集),不另写一套。
|
|
152
|
+
const parsed = parseFrontmatter(source)
|
|
153
|
+
const entityType = String(parsed.frontmatter.entity_type ?? '')
|
|
154
|
+
if (!isEntityType(entityType)) return undefined
|
|
155
|
+
const entityId = String(parsed.frontmatter.entity_id ?? '').trim()
|
|
156
|
+
if (!entityId) return undefined
|
|
157
|
+
|
|
158
|
+
const match = parsed.content.match(new RegExp('```' + BLOCK_FENCE + '\\s*\\n([\\s\\S]*?)```'))
|
|
159
|
+
let block: Record<string, unknown> = {}
|
|
160
|
+
if (match) {
|
|
161
|
+
try {
|
|
162
|
+
const value = JSON.parse(match[1])
|
|
163
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) block = value as Record<string, unknown>
|
|
164
|
+
} catch { /* 块坏了不静默降级成空对象:留空之后由三层校验的「格式层」报缺字段。 */ }
|
|
165
|
+
}
|
|
166
|
+
const body = match ? parsed.content.replace(match[0], '') : parsed.content
|
|
167
|
+
const refs = parsed.frontmatter.source_refs
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
entityId,
|
|
171
|
+
entityType,
|
|
172
|
+
title: String(parsed.frontmatter.title ?? entityId),
|
|
173
|
+
...(parsed.frontmatter.summary ? { summary: String(parsed.frontmatter.summary) } : {}),
|
|
174
|
+
status: String(parsed.frontmatter.status ?? DEFAULT_CARD_STATUS),
|
|
175
|
+
...(parsed.frontmatter.owner ? { owner: String(parsed.frontmatter.owner) } : {}),
|
|
176
|
+
sourceRefs: Array.isArray(refs) ? refs.map(String) : [],
|
|
177
|
+
...(parsed.frontmatter.updated_at ? { updatedAt: String(parsed.frontmatter.updated_at) } : {}),
|
|
178
|
+
sections: splitSections(body),
|
|
179
|
+
block,
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 只解析 front matter 与正文。
|
|
185
|
+
*
|
|
186
|
+
* 与 `runtime.ts` 里的 `parseMarkdown` 是同一条规则(同一个 yaml 子集),这里保留一份
|
|
187
|
+
* 只读副本是为了让 `diagnosis/` 不反向依赖宿主的运行时 —— 领域模块被 runtime 依赖,
|
|
188
|
+
* 不能形成环。
|
|
189
|
+
*/
|
|
190
|
+
function parseFrontmatter(source: string): { frontmatter: Record<string, unknown>; content: string } {
|
|
191
|
+
if (!source.startsWith('---')) return { frontmatter: {}, content: source.trim() }
|
|
192
|
+
const end = source.indexOf('\n---', 3)
|
|
193
|
+
if (end < 0) return { frontmatter: {}, content: source.trim() }
|
|
194
|
+
const frontmatter: Record<string, unknown> = {}
|
|
195
|
+
for (const line of source.slice(3, end).replace(/^\n/, '').split(/\r?\n/)) {
|
|
196
|
+
const separator = line.indexOf(':')
|
|
197
|
+
if (separator <= 0) continue
|
|
198
|
+
const key = line.slice(0, separator).trim()
|
|
199
|
+
const text = line.slice(separator + 1).trim()
|
|
200
|
+
if (text === 'true') frontmatter[key] = true
|
|
201
|
+
else if (text === 'false') frontmatter[key] = false
|
|
202
|
+
else if (text === 'null') frontmatter[key] = null
|
|
203
|
+
else if (/^-?\d+(\.\d+)?$/.test(text)) frontmatter[key] = Number(text)
|
|
204
|
+
else if ((text.startsWith('[') && text.endsWith(']')) || (text.startsWith('{') && text.endsWith('}'))) {
|
|
205
|
+
try { frontmatter[key] = JSON.parse(text) } catch { frontmatter[key] = text.replace(/^['"]|['"]$/g, '') }
|
|
206
|
+
} else frontmatter[key] = text.replace(/^['"]|['"]$/g, '')
|
|
207
|
+
}
|
|
208
|
+
return { frontmatter, content: source.slice(end + 4).trim() }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function splitSections(body: string): DiagnosisCardSection[] {
|
|
212
|
+
const sections: DiagnosisCardSection[] = []
|
|
213
|
+
let heading: string | undefined
|
|
214
|
+
let lines: string[] = []
|
|
215
|
+
const flush = (): void => {
|
|
216
|
+
if (heading !== undefined) sections.push({ heading, body: lines.join('\n').trim() })
|
|
217
|
+
lines = []
|
|
218
|
+
}
|
|
219
|
+
for (const line of body.split(/\r?\n/)) {
|
|
220
|
+
const match = /^##\s+(.+)$/.exec(line)
|
|
221
|
+
if (match) { flush(); heading = match[1].trim() } else if (heading !== undefined) lines.push(line)
|
|
222
|
+
}
|
|
223
|
+
flush()
|
|
224
|
+
return sections.filter(section => section.body !== '')
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function isEntityType(value: string): value is DiagnosisEntityType {
|
|
228
|
+
return DIAGNOSIS_ENTITIES.some(spec => spec.entityType === value)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* ─────────────────────────────────────────────── 图 → 卡片 ── */
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 类型层与案例 → 卡片。
|
|
235
|
+
*
|
|
236
|
+
* `asset` / `evidence` / `cause` 节点**不产出卡片**:它们不是手写的知识(§6.4)。
|
|
237
|
+
* 被假设引用的资产会在反序列化时按 id 补一个最小节点回来。
|
|
238
|
+
*/
|
|
239
|
+
export function graphToCards(graph: AttributionGraphData, cases: AttributionCase[] = []): DiagnosisCard[] {
|
|
240
|
+
const cards: DiagnosisCard[] = []
|
|
241
|
+
|
|
242
|
+
for (const node of graph.nodes) {
|
|
243
|
+
// 案例节点只是图上的**桩**:它的正文与有序路径住在 `cases` 里(§6.4)。图上的桩不单独成卡,
|
|
244
|
+
// 否则同一个 CASE-001 会有两张卡 —— 一张带路径、一张空壳,读图时看谁先被遍历到。
|
|
245
|
+
if (node.kind === 'case') continue
|
|
246
|
+
const entityType = entityTypeOf(node.kind)
|
|
247
|
+
if (!entityType) continue
|
|
248
|
+
const layout = CARD_LAYOUT[entityType]
|
|
249
|
+
const block: Record<string, unknown> = {}
|
|
250
|
+
|
|
251
|
+
if (node.kind === 'symptom') {
|
|
252
|
+
block.observable = node.summary ?? ''
|
|
253
|
+
block.entry_when = ''
|
|
254
|
+
block.problem_class = node.problemClass ?? null
|
|
255
|
+
block.problem_subclass = node.problemSubclass ?? null
|
|
256
|
+
// 现象 → 假设:**逐边属性必须落在边上**(stale_days 属于「这个现象 × 这个假设」这一对),
|
|
257
|
+
// 所以它跟着现象卡片走,而不是跟着假设卡片走。
|
|
258
|
+
block.candidates = graph.edges
|
|
259
|
+
.filter(edge => edge.kind === 'may_be_caused_by' && edge.source === node.id)
|
|
260
|
+
.map(edge => ({
|
|
261
|
+
id: edge.target,
|
|
262
|
+
...(edge.staleDays === undefined ? {} : { stale_days: edge.staleDays }),
|
|
263
|
+
}))
|
|
264
|
+
} else if (node.kind === 'hypothesis') {
|
|
265
|
+
block.discriminator = node.discriminator ?? ''
|
|
266
|
+
block.not_when = node.notWhen ?? ''
|
|
267
|
+
block.failure_mode = node.failureMode ?? null
|
|
268
|
+
block.verified_by = graph.edges.filter(edge => edge.kind === 'verified_by' && edge.source === node.id).map(edge => edge.target)
|
|
269
|
+
block.assets = graph.edges.filter(edge => edge.kind === 'indexed_from' && edge.target === node.id).map(edge => edge.source)
|
|
270
|
+
block.reuse_count = node.reuseCount ?? 0
|
|
271
|
+
if (node.stale !== undefined) block.stale = node.stale
|
|
272
|
+
} else if (node.kind === 'check') {
|
|
273
|
+
block.cost_minutes = node.costMinutes ?? null
|
|
274
|
+
block.automation = node.automation ?? null
|
|
275
|
+
block.verdict_true = node.verdictTrue ?? ''
|
|
276
|
+
block.verdict_false = node.verdictFalse ?? ''
|
|
277
|
+
if (node.stale !== undefined) block.stale = node.stale
|
|
278
|
+
} else if (node.kind === 'failure-mode') {
|
|
279
|
+
block.code = node.code ?? null
|
|
280
|
+
block.label = node.title
|
|
281
|
+
block.boundary = ''
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
cards.push({
|
|
285
|
+
entityId: node.id,
|
|
286
|
+
entityType,
|
|
287
|
+
title: node.title,
|
|
288
|
+
...(layout.summaryFrom === 'summary' && node.summary ? { summary: node.summary } : {}),
|
|
289
|
+
status: DEFAULT_CARD_STATUS,
|
|
290
|
+
...(node.owner ? { owner: node.owner } : {}),
|
|
291
|
+
sourceRefs: [],
|
|
292
|
+
sections: [],
|
|
293
|
+
block,
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
for (const item of cases) {
|
|
298
|
+
cards.push({
|
|
299
|
+
entityId: item.id,
|
|
300
|
+
entityType: 'diagnosis/case',
|
|
301
|
+
title: item.title,
|
|
302
|
+
status: DEFAULT_CARD_STATUS,
|
|
303
|
+
sourceRefs: [],
|
|
304
|
+
sections: item.sections.map(section => ({ heading: section.heading, body: section.body })),
|
|
305
|
+
block: {
|
|
306
|
+
// 案例必须从一个现象进入 —— 这条信息在图上就是 `instance_of` 边,在卡片上是这一个字段。
|
|
307
|
+
symptom: item.symptomId,
|
|
308
|
+
path: item.path,
|
|
309
|
+
duration_minutes: item.durationMinutes,
|
|
310
|
+
...(item.misleading ? { misleading: item.misleading } : {}),
|
|
311
|
+
root_cause: item.rootCause,
|
|
312
|
+
},
|
|
313
|
+
})
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return cards
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/* ─────────────────────────────────────────────── 卡片 → 图 ── */
|
|
320
|
+
|
|
321
|
+
export interface CompiledDiagnosis {
|
|
322
|
+
graph: AttributionGraphData
|
|
323
|
+
cases: AttributionCase[]
|
|
324
|
+
/** 卡片之间的关系(引用完整性等);节点字段校验仍走 `validateDiagnosis`。 */
|
|
325
|
+
problems: string[]
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* 卡片 → 图。
|
|
330
|
+
*
|
|
331
|
+
* 只产出「卡片定义得了」的那部分(类型层 + 引用层的资产 + 案例,以及五种卡片边);
|
|
332
|
+
* `evidence` / `cause` 与四条执行边由一次排查写入,不在这里合成 —— 否则一次执行的结果
|
|
333
|
+
* 会被伪装成可复用的知识。
|
|
334
|
+
*/
|
|
335
|
+
export function cardsToGraph(cards: DiagnosisCard[]): CompiledDiagnosis {
|
|
336
|
+
const nodes: AttributionNode[] = []
|
|
337
|
+
const edges: AttributionEdge[] = []
|
|
338
|
+
const cases: AttributionCase[] = []
|
|
339
|
+
const problems: string[] = []
|
|
340
|
+
const seen = new Set<string>()
|
|
341
|
+
for (const card of cards) {
|
|
342
|
+
if (seen.has(card.entityId)) problems.push(`卡片 ${card.entityId} 被定义了多次 —— 同一份知识只能有一张卡片`)
|
|
343
|
+
seen.add(card.entityId)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** 资产节点按引用反推:引用层随数据目录同步,卡片里只留 id。 */
|
|
347
|
+
const assetIds = new Set<string>()
|
|
348
|
+
const modeIdByCode = new Map<FailureMode, string>()
|
|
349
|
+
|
|
350
|
+
for (const card of cards) {
|
|
351
|
+
if (card.entityType === 'diagnosis/failure-mode' && typeof card.block.code === 'string') {
|
|
352
|
+
modeIdByCode.set(card.block.code as FailureMode, card.entityId)
|
|
353
|
+
}
|
|
354
|
+
if (card.entityType === 'diagnosis/hypothesis' && Array.isArray(card.block.assets)) {
|
|
355
|
+
for (const id of card.block.assets) assetIds.add(String(id))
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
for (const card of cards) {
|
|
360
|
+
const spec = DIAGNOSIS_ENTITIES.find(candidate => candidate.entityType === card.entityType)
|
|
361
|
+
if (!spec) continue
|
|
362
|
+
const layout = CARD_LAYOUT[card.entityType]
|
|
363
|
+
const summary = layout.summaryFrom === 'observable' ? stringOf(card.block.observable) : card.summary
|
|
364
|
+
const node: AttributionNode = {
|
|
365
|
+
id: card.entityId,
|
|
366
|
+
kind: spec.kind,
|
|
367
|
+
layer: spec.layer,
|
|
368
|
+
title: card.title,
|
|
369
|
+
...(summary ? { summary } : {}),
|
|
370
|
+
...(card.owner ? { owner: card.owner } : {}),
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (card.entityType === 'diagnosis/symptom') {
|
|
374
|
+
const problemClass = stringOf(card.block.problem_class)
|
|
375
|
+
if (problemClass) node.problemClass = problemClass as AttributionNode['problemClass']
|
|
376
|
+
const subclass = stringOf(card.block.problem_subclass)
|
|
377
|
+
if (subclass) node.problemSubclass = subclass
|
|
378
|
+
const candidates = Array.isArray(card.block.candidates) ? card.block.candidates : []
|
|
379
|
+
candidates.forEach((entry, index) => {
|
|
380
|
+
const target = stringOf((entry as { id?: unknown })?.id)
|
|
381
|
+
if (!target) return
|
|
382
|
+
const staleDays = numberOf((entry as { stale_days?: unknown })?.stale_days)
|
|
383
|
+
edges.push({
|
|
384
|
+
id: `e-${card.entityId.toLowerCase()}-${target.toLowerCase()}-${index}`,
|
|
385
|
+
source: card.entityId,
|
|
386
|
+
target,
|
|
387
|
+
kind: 'may_be_caused_by',
|
|
388
|
+
...(staleDays === undefined ? {} : { staleDays }),
|
|
389
|
+
})
|
|
390
|
+
})
|
|
391
|
+
} else if (card.entityType === 'diagnosis/hypothesis') {
|
|
392
|
+
const discriminator = stringOf(card.block.discriminator)
|
|
393
|
+
if (discriminator) node.discriminator = discriminator
|
|
394
|
+
const notWhen = stringOf(card.block.not_when)
|
|
395
|
+
if (notWhen) node.notWhen = notWhen
|
|
396
|
+
const failureMode = stringOf(card.block.failure_mode)
|
|
397
|
+
if (failureMode) node.failureMode = failureMode as FailureMode
|
|
398
|
+
const reuse = numberOf(card.block.reuse_count)
|
|
399
|
+
if (reuse !== undefined) node.reuseCount = reuse
|
|
400
|
+
if (typeof card.block.stale === 'boolean') node.stale = card.block.stale
|
|
401
|
+
|
|
402
|
+
for (const [index, target] of listOf(card.block.verified_by).entries()) {
|
|
403
|
+
edges.push({ id: `e-${card.entityId.toLowerCase()}-${String(target).toLowerCase()}-${index}`, source: card.entityId, target: String(target), kind: 'verified_by' })
|
|
404
|
+
}
|
|
405
|
+
for (const [index, asset] of listOf(card.block.assets).entries()) {
|
|
406
|
+
edges.push({ id: `e-${String(asset).toLowerCase()}-${card.entityId.toLowerCase()}-${index}`, source: String(asset), target: card.entityId, kind: 'indexed_from' })
|
|
407
|
+
}
|
|
408
|
+
if (failureMode) {
|
|
409
|
+
const modeNode = modeIdByCode.get(failureMode as FailureMode)
|
|
410
|
+
if (modeNode) edges.push({ id: `e-${card.entityId.toLowerCase()}-${modeNode.toLowerCase()}`, source: card.entityId, target: modeNode, kind: 'categorized_as' })
|
|
411
|
+
else problems.push(`${card.entityId} 的 failure_mode=${failureMode} 找不到对应的失败模式卡片 —— categorized_as 边无从建立`)
|
|
412
|
+
}
|
|
413
|
+
} else if (card.entityType === 'diagnosis/check') {
|
|
414
|
+
const cost = numberOf(card.block.cost_minutes)
|
|
415
|
+
if (cost !== undefined) node.costMinutes = cost
|
|
416
|
+
const automation = stringOf(card.block.automation)
|
|
417
|
+
if (automation === 'auto' || automation === 'manual') node.automation = automation
|
|
418
|
+
const verdictTrue = stringOf(card.block.verdict_true)
|
|
419
|
+
if (verdictTrue) node.verdictTrue = verdictTrue
|
|
420
|
+
const verdictFalse = stringOf(card.block.verdict_false)
|
|
421
|
+
if (verdictFalse) node.verdictFalse = verdictFalse
|
|
422
|
+
if (typeof card.block.stale === 'boolean') node.stale = card.block.stale
|
|
423
|
+
} else if (card.entityType === 'diagnosis/failure-mode') {
|
|
424
|
+
const code = stringOf(card.block.code)
|
|
425
|
+
if (code) node.code = code as FailureMode
|
|
426
|
+
} else if (card.entityType === 'diagnosis/case') {
|
|
427
|
+
const symptom = stringOf(card.block.symptom)
|
|
428
|
+
if (!symptom) problems.push(`${card.entityId} 没有 symptom —— 案例必须从一个现象进入`)
|
|
429
|
+
else {
|
|
430
|
+
edges.push({ id: `e-${card.entityId.toLowerCase()}-${symptom.toLowerCase()}`, source: card.entityId, target: symptom, kind: 'instance_of' })
|
|
431
|
+
const path = Array.isArray(card.block.path) ? card.block.path as AttributionCase['path'] : []
|
|
432
|
+
cases.push({
|
|
433
|
+
id: card.entityId,
|
|
434
|
+
title: card.title,
|
|
435
|
+
symptomId: symptom,
|
|
436
|
+
sections: card.sections.map(section => ({ heading: section.heading, body: section.body })),
|
|
437
|
+
path,
|
|
438
|
+
durationMinutes: numberOf(card.block.duration_minutes) ?? 0,
|
|
439
|
+
...(card.block.misleading ? { misleading: card.block.misleading as AttributionCase['misleading'] } : {}),
|
|
440
|
+
rootCause: stringOf(card.block.root_cause) ?? '',
|
|
441
|
+
})
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
nodes.push(node)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
for (const id of assetIds) {
|
|
449
|
+
if (nodes.some(node => node.id === id)) continue
|
|
450
|
+
nodes.push({ id, kind: 'asset', layer: 'reference', title: id })
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// 引用完整性:卡片引用了不存在的卡片时,必须报出来(否则图上会出现悬空边,
|
|
454
|
+
// 而 `validateDiagnosis` 只在读图之后才看得到)。
|
|
455
|
+
const ids = new Set(nodes.map(node => node.id))
|
|
456
|
+
for (const edge of edges) {
|
|
457
|
+
if (!ids.has(edge.source)) problems.push(`边 ${edge.id} 的起点 ${edge.source} 没有对应卡片`)
|
|
458
|
+
if (!ids.has(edge.target)) problems.push(`边 ${edge.id} 的终点 ${edge.target} 没有对应卡片`)
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return { graph: { nodes, edges }, cases, problems }
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function stringOf(value: unknown): string | undefined {
|
|
465
|
+
return typeof value === 'string' && value.trim() !== '' ? value : undefined
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function numberOf(value: unknown): number | undefined {
|
|
469
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function listOf(value: unknown): unknown[] {
|
|
473
|
+
return Array.isArray(value) ? value : []
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* 路径片段规范化。**必须与 `KnowledgeRuntime` 里那份逐字相同** —— 差一个字符,
|
|
478
|
+
* 「写进去的卡片」和「编译器去读的路径」就永远对不上,而且失败方式是静默的(读不到 = 图上少几条边)。
|
|
479
|
+
*
|
|
480
|
+
* 之所以在这里重复三行而不是把它从 runtime 里 import 出来:`diagnosis/` 是领域子模块,
|
|
481
|
+
* 反向依赖宿主运行时会把分层倒过来。取而代之的是一条测试,拿同一批 id 比对两边的输出 ——
|
|
482
|
+
* 规则漂移时测试会失败,而不是靠人记得同时改两处。
|
|
483
|
+
*/
|
|
484
|
+
function slugSegment(value: string): string {
|
|
485
|
+
const normalized = value.trim().replace(/[^\p{L}\p{N}._-]+/gu, '-').replace(/^-+|-+$/g, '')
|
|
486
|
+
return normalized.slice(0, 120) || 'item'
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* 卡片在磁盘上的相对路径:`02_wiki/<entity_type>/<slug(entity_id)>.md`。
|
|
491
|
+
*
|
|
492
|
+
* `entity_type` 里的斜杠是**目录层级**(五类排查卡片因此落在 `02_wiki/diagnosis/` 下,
|
|
493
|
+
* 与其它 wiki 子树分开),不是要压成横杠 —— 我一开始压了,结果写进去的卡片和编译器去读的
|
|
494
|
+
* 路径差一层目录,而失败是静默的:读不到,就只是图上少几条边。
|
|
495
|
+
*/
|
|
496
|
+
export function cardPathOf(card: DiagnosisCard): string {
|
|
497
|
+
return `02_wiki/${card.entityType}/${slugSegment(card.entityId)}.md`
|
|
498
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 问题侧的分类表(0021 §4.5)。
|
|
3
|
+
*
|
|
4
|
+
* **为什么是一张表,而不是让每个现象随手写一个字符串。** 分类的价值来自「同一批大类
|
|
5
|
+
* 被反复用」;一旦允许自由书写,两周后就会出现「上游未产出」「上游没产出」「上游未输出」
|
|
6
|
+
* 三个大类各挂一个现象 —— 那等于没分类,而且图上看不出来。所以大类只能从这张表里取。
|
|
7
|
+
*
|
|
8
|
+
* **两级就够了。** 第三层「详细问题」在图上是 symptom 节点本身(带它的资产、时间窗、
|
|
9
|
+
* 维度),不再是目录里的一层。理由见 §4.5:把实例塞进类型层,正是 §4.1 明令要分开的事。
|
|
10
|
+
*
|
|
11
|
+
* **子类同时是召回特征的来源。** §5.1 的 `shape` / `time_pattern` 由前端从告警载荷里
|
|
12
|
+
* 提取,这里把两者的对应关系**写死在数据里**:`deviation-drop-sharp` 就是「偏低 × 突降」。
|
|
13
|
+
* 这样「分类」与「召回权重」不可能各说各话 —— 它们读的是同一份表,而不是两份逐渐漂移的清单。
|
|
14
|
+
*/
|
|
15
|
+
export type ProblemClassId = 'missing' | 'deviation' | 'inconsistency' | 'granularity'
|
|
16
|
+
|
|
17
|
+
/** 差异形态(§5.1 召回权重里的 .30 项)。 */
|
|
18
|
+
export type SymptomShape = 'zero' | 'low' | 'high' | 'mismatch' | 'error' | 'granularity'
|
|
19
|
+
|
|
20
|
+
/** 时间模式(§5.1 召回权重里的 .20 项)。缺席表示这条子类不携带时间语义。 */
|
|
21
|
+
export type SymptomTimePattern = 'drop-sharp' | 'drop-slow' | 'rise-sharp' | 'rise-slow' | 'periodic' | 'single-day'
|
|
22
|
+
|
|
23
|
+
export interface ProblemSubclassSpec {
|
|
24
|
+
/** 全局唯一,形如 `missing-zero`。落盘的是它,不是中文标签。 */
|
|
25
|
+
id: string
|
|
26
|
+
label: string
|
|
27
|
+
shape: SymptomShape
|
|
28
|
+
timePattern?: SymptomTimePattern
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ProblemClassSpec {
|
|
32
|
+
id: ProblemClassId
|
|
33
|
+
label: string
|
|
34
|
+
/** 「第一眼看到什么」—— 大类是按值班同学的判断顺序切的,这句话就是那个判断。 */
|
|
35
|
+
cue: string
|
|
36
|
+
subclasses: ProblemSubclassSpec[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 四个大类,按「看到异常之后的第一眼判断」排序:
|
|
41
|
+
* 该有的有没有 → 有的话幅度对不对 → 对不对得上 → 粒度对不对。
|
|
42
|
+
*/
|
|
43
|
+
export const PROBLEM_CLASSES: ProblemClassSpec[] = [
|
|
44
|
+
{
|
|
45
|
+
id: 'missing',
|
|
46
|
+
label: '缺失',
|
|
47
|
+
cue: '该有的没有',
|
|
48
|
+
subclasses: [
|
|
49
|
+
{ id: 'missing-zero', label: '全量为 0', shape: 'zero' },
|
|
50
|
+
{ id: 'missing-no-partition', label: '分区 / 表未产出', shape: 'zero' },
|
|
51
|
+
{ id: 'missing-job-failed', label: '任务报错未产出', shape: 'error' },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'deviation',
|
|
56
|
+
label: '偏差',
|
|
57
|
+
cue: '有,但幅度不对',
|
|
58
|
+
subclasses: [
|
|
59
|
+
{ id: 'deviation-drop-sharp', label: '突降', shape: 'low', timePattern: 'drop-sharp' },
|
|
60
|
+
{ id: 'deviation-drop-slow', label: '缓降', shape: 'low', timePattern: 'drop-slow' },
|
|
61
|
+
{ id: 'deviation-rise-sharp', label: '突增', shape: 'high', timePattern: 'rise-sharp' },
|
|
62
|
+
{ id: 'deviation-rise-slow', label: '缓升', shape: 'high', timePattern: 'rise-slow' },
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'inconsistency',
|
|
67
|
+
label: '不一致',
|
|
68
|
+
cue: '有,但对不上',
|
|
69
|
+
subclasses: [
|
|
70
|
+
{ id: 'inconsistency-vs-source', label: '与源系统对不上', shape: 'mismatch' },
|
|
71
|
+
{ id: 'inconsistency-cross-report', label: '跨报表勾稽不平', shape: 'mismatch' },
|
|
72
|
+
{ id: 'inconsistency-spec-change', label: '口径变更后不一致', shape: 'mismatch', timePattern: 'periodic' },
|
|
73
|
+
{ id: 'inconsistency-tz-boundary', label: '跨天 / 时区边界', shape: 'mismatch', timePattern: 'single-day' },
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'granularity',
|
|
78
|
+
label: '粒度',
|
|
79
|
+
cue: '有,但切分不对',
|
|
80
|
+
subclasses: [
|
|
81
|
+
{ id: 'granularity-dim-missing', label: '维度值缺失', shape: 'granularity' },
|
|
82
|
+
{ id: 'granularity-dim-empty', label: '部分维度为空', shape: 'granularity' },
|
|
83
|
+
{ id: 'granularity-dup-count', label: '重复计数', shape: 'granularity', timePattern: 'rise-sharp' },
|
|
84
|
+
{ id: 'granularity-mapping-shift', label: '映射错位', shape: 'granularity' },
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
export const PROBLEM_CLASS_LABEL: Record<ProblemClassId, string> = Object.fromEntries(
|
|
90
|
+
PROBLEM_CLASSES.map(item => [item.id, item.label]),
|
|
91
|
+
) as Record<ProblemClassId, string>
|
|
92
|
+
|
|
93
|
+
/** 子类表项 + 它属于哪个大类 —— 有了它,界面和校验都不用再自己反查归属。 */
|
|
94
|
+
export interface IndexedProblemSubclass extends ProblemSubclassSpec { classId: ProblemClassId }
|
|
95
|
+
|
|
96
|
+
/** 子类 id → 表项。查一次就够,落盘的 jsonl 里只写 id。 */
|
|
97
|
+
export const PROBLEM_SUBCLASS_INDEX: Record<string, IndexedProblemSubclass> =
|
|
98
|
+
Object.fromEntries(PROBLEM_CLASSES.flatMap(item =>
|
|
99
|
+
item.subclasses.map(subclass => [subclass.id, { ...subclass, classId: item.id }]),
|
|
100
|
+
))
|
|
101
|
+
|
|
102
|
+
export function problemClassOf(subclassId: string | undefined): ProblemClassId | undefined {
|
|
103
|
+
return subclassId === undefined ? undefined : PROBLEM_SUBCLASS_INDEX[subclassId]?.classId
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function problemSubclassOf(subclassId: string | undefined): IndexedProblemSubclass | undefined {
|
|
107
|
+
return subclassId === undefined ? undefined : PROBLEM_SUBCLASS_INDEX[subclassId]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 子类是否真的属于该大类 —— 校验和界面联动都用它,不再各写一遍包含判断。 */
|
|
111
|
+
export function subclassBelongsTo(subclassId: string | undefined, classId: ProblemClassId): boolean {
|
|
112
|
+
return problemClassOf(subclassId) === classId
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function problemClassIds(): ProblemClassId[] {
|
|
116
|
+
return PROBLEM_CLASSES.map(item => item.id)
|
|
117
|
+
}
|