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
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { ZOMBIE_EDGE_DAYS } from '../../diagnosis/types.js'
|
|
2
2
|
import {
|
|
3
|
-
PROBLEM_CLASSES, PROBLEM_CLASS_LABEL, problemClassIds, problemSubclassOf,
|
|
3
|
+
PROBLEM_CLASSES, PROBLEM_CLASS_LABEL, problemClassIds, problemClassOf, problemSubclassOf,
|
|
4
4
|
} from '../../diagnosis/problem-classes.js'
|
|
5
5
|
import type { ProblemClassId } from '../../diagnosis/problem-classes.js'
|
|
6
6
|
import type {
|
|
7
|
+
AssetType,
|
|
7
8
|
AttributionEdge, AttributionEdgeKind, AttributionGraphData, AttributionNode, AttributionNodeKind,
|
|
8
|
-
AttributionStats,
|
|
9
|
+
AttributionStats, FailureReason, NodeLayer,
|
|
9
10
|
} from '../../diagnosis/types.js'
|
|
10
11
|
|
|
11
12
|
export { ZOMBIE_EDGE_DAYS, PROBLEM_CLASSES, PROBLEM_CLASS_LABEL, problemClassIds, problemSubclassOf }
|
|
12
13
|
export type {
|
|
14
|
+
AssetType,
|
|
13
15
|
AttributionEdge, AttributionGraphData, AttributionNode, AttributionNodeKind,
|
|
14
|
-
AttributionEdgeKind, AttributionStats,
|
|
16
|
+
AttributionEdgeKind, AttributionStats, FailureReason, NodeLayer,
|
|
17
|
+
AttributionCase, CaseSection, CasePathStep,
|
|
15
18
|
} from '../../diagnosis/types.js'
|
|
16
19
|
export type { ProblemClassId, ProblemSubclassSpec } from '../../diagnosis/problem-classes.js'
|
|
17
20
|
|
|
@@ -30,10 +33,10 @@ export const LAYER_TONES: Record<NodeLayer, 'blue' | 'violet' | 'amber'> = {
|
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
/**
|
|
33
|
-
*
|
|
36
|
+
* 失败原因 → 颜色。分类在图谱视图里是**着色**而不是分区,所以它只影响一眼扫过的
|
|
34
37
|
* 分组感,不影响节点位置 —— 位置由布局按边的方向决定。
|
|
35
38
|
*/
|
|
36
|
-
export const
|
|
39
|
+
export const FAILURE_REASON_STYLE: Record<FailureReason, { label: string; fill: string; stroke: string; text: string }> = {
|
|
37
40
|
'upstream-missing': { label: '上游未产出', fill: '#eef4ff', stroke: '#2563eb', text: '#1d4ed8' },
|
|
38
41
|
'spec-change': { label: '口径变更', fill: '#f3f0ff', stroke: '#6b54cf', text: '#5b45be' },
|
|
39
42
|
'data-processing': { label: '数据处理', fill: '#eafaf2', stroke: '#12875f', text: '#0f6b4d' },
|
|
@@ -78,23 +81,458 @@ export const NEUTRAL_STYLE = { fill: '#ffffff', stroke: '#d6dce6', text: '#74809
|
|
|
78
81
|
export const NODE_KIND_LABEL: Record<AttributionNodeKind, string> = {
|
|
79
82
|
symptom: '现象',
|
|
80
83
|
hypothesis: '假设',
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
check_point: '检查点',
|
|
85
|
+
failure_reason: '失败原因',
|
|
83
86
|
asset: '资产',
|
|
84
87
|
case: '案例',
|
|
85
88
|
evidence: '证据',
|
|
86
89
|
cause: '根因',
|
|
87
90
|
unknown: '未归类',
|
|
88
91
|
}
|
|
92
|
+
/* ────────────────────────────────────────────────────────────────────────────
|
|
93
|
+
* 动线视角:五段泳道(0021 §7.7 / §4.6)
|
|
94
|
+
*
|
|
95
|
+
* 这一节是**纯数据**:段名、每段装什么、段内怎么排、段位坐标。
|
|
96
|
+
* 「画在画布上」与「画成 HTML 泳道带」都读同一份结果,两处不可能对不上 ——
|
|
97
|
+
* 这正是 §7.7「空段不隐藏」「断链」能被断言而不是被眼力的原因。
|
|
98
|
+
* ──────────────────────────────────────────────────────────────────────────── */
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 五个段位。
|
|
102
|
+
*
|
|
103
|
+
* 段名固定成这五个词,**不写「第一步 / 第二步」**(§7.5):它们是数据的分段,
|
|
104
|
+
* 不是操作步骤;操作步骤在排查视图里,写成步骤会让「同一现象有 N 个假设」
|
|
105
|
+
* 读起来像「做错了要重来」。
|
|
106
|
+
*
|
|
107
|
+
* `note` 是**空段说明**:一段为空时画在段头右边的那句话。它必须与段的存在理由
|
|
108
|
+
* 对上 —— 曲线是「哪一段还没建」这张图最该显眼的信息(§7.7)。
|
|
109
|
+
*/
|
|
110
|
+
export type LaneId = 'symptom' | 'hypothesis' | 'check_point' | 'asset' | 'instance'
|
|
111
|
+
|
|
112
|
+
export interface LaneSpec {
|
|
113
|
+
id: LaneId
|
|
114
|
+
/** 段号 ①②③④⑤。它是**顺序**,不是步骤编号。 */
|
|
115
|
+
ordinal: string
|
|
116
|
+
label: string
|
|
117
|
+
/** 这一段按什么排序 —— 写在段头右侧的读法。 */
|
|
118
|
+
orderHint: string
|
|
119
|
+
/** 空段时显示的一句话。 */
|
|
120
|
+
note: string
|
|
121
|
+
kinds: AttributionNodeKind[]
|
|
122
|
+
/** 实例层:随每次排查线性增长,因此默认折叠(§7.7 末段)。 */
|
|
123
|
+
instance: boolean
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const LANES: LaneSpec[] = [
|
|
127
|
+
{
|
|
128
|
+
id: 'symptom', ordinal: '①', label: '问题现象', orderHint: '按问题大类 → 子类',
|
|
129
|
+
note: '还没有现象:先登记一条异常现象', kinds: ['symptom'], instance: false,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: 'hypothesis', ordinal: '②', label: '假设原因', orderHint: '按 prior ÷ 最低检查点成本',
|
|
133
|
+
note: '还没有假设:现象下面一条候选原因都没有', kinds: ['hypothesis'], instance: false,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: 'check_point', ordinal: '③', label: '检查点', orderHint: '按 cost_minutes 升序',
|
|
137
|
+
note: '还没有检查点:有假设但走不下去,就是断链', kinds: ['check_point'], instance: false,
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: 'asset', ordinal: '④', label: '查询依据', orderHint: '经 queries 边 · 按类型',
|
|
141
|
+
note: '还没有查询依据:检查点没写 assets', kinds: ['asset'], instance: false,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: 'instance', ordinal: '⑤', label: '实际查询与结果', orderHint: '按时间倒序 · 默认折叠',
|
|
145
|
+
note: '还没有实际查询:这些检查一次都没跑过',
|
|
146
|
+
kinds: ['evidence', 'cause', 'unknown'], instance: true,
|
|
147
|
+
},
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
const LANE_BY_KIND: Record<AttributionNodeKind, LaneId | undefined> = {
|
|
151
|
+
symptom: 'symptom',
|
|
152
|
+
hypothesis: 'hypothesis',
|
|
153
|
+
check_point: 'check_point',
|
|
154
|
+
asset: 'asset',
|
|
155
|
+
failure_reason: undefined,
|
|
156
|
+
case: undefined,
|
|
157
|
+
evidence: 'instance',
|
|
158
|
+
cause: 'instance',
|
|
159
|
+
unknown: 'instance',
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 节点属于哪一段。`failure_reason` / `case` 不属于任何一段(见 LANES 的注释)。 */
|
|
163
|
+
export function laneOf(node: AttributionNode): LaneId | undefined {
|
|
164
|
+
return LANE_BY_KIND[node.kind]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 动线布局里**没有段位**的节点种类。
|
|
169
|
+
*
|
|
170
|
+
* 从 `LANE_BY_KIND` 派生,不另写一份清单:漏一个的后果不是报错,而是画布上多一个
|
|
171
|
+
* **无处安放的孤儿节点** —— 它没有段位坐标,只能落在力导向的种子位置上,看起来像
|
|
172
|
+
* 「图里有个不该在这儿的东西」。这正是 `failure_reason` 之前的状态:5 个节点悬在
|
|
173
|
+
* 画布上,还带着 7 条 `categorized_as` 长边从②段横穿出去。
|
|
174
|
+
*
|
|
175
|
+
* 它们要说的信息**已经由假设节点的填充色承载**(§7.2「颜色 = 失败原因」),
|
|
176
|
+
* 所以动线视角不画它们不是丢信息,是去掉重复表达。关系视图照旧全画。
|
|
177
|
+
*/
|
|
178
|
+
export const OFF_LANE_KINDS: AttributionNodeKind[] = (Object.keys(LANE_BY_KIND) as AttributionNodeKind[])
|
|
179
|
+
.filter(kind => LANE_BY_KIND[kind] === undefined)
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 动线布局里**不画**的边。
|
|
183
|
+
*
|
|
184
|
+
* `indexed_from` 是「资产 → 假设」的**反向索引**,而动线的方向是
|
|
185
|
+
* 假设 → 检查点 → 查询依据。它指向反方向,会从④段一路横穿③段回到②段 ——
|
|
186
|
+
* 在这个布局里它只制造交叉,不承载动线。关系视图里保留(那里方向由力导向决定)。
|
|
187
|
+
*/
|
|
188
|
+
export const OFF_LANE_EDGE_KINDS: AttributionEdgeKind[] = ['indexed_from']
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 画布的作用域:按布局决定「哪些节点与边根本不进这张画布」。
|
|
192
|
+
*
|
|
193
|
+
* 把它做成一个函数而不是散在各处的 `if (layout === 'line')`,是为了让
|
|
194
|
+
* 「动线少画什么」只有一处答案 —— 否则它迟早会在三处各说各话,而表现是
|
|
195
|
+
* 「同一个节点在画布上有、在段头计数里没有」这类对不上的读数。
|
|
196
|
+
*
|
|
197
|
+
* 形参写 `'line' | 'force'` 而不是 `GraphLayout`:那个类型住在 `graph-options.ts`,
|
|
198
|
+
* 而它 import 这个文件 —— 反向 import 会成环。
|
|
199
|
+
*/
|
|
200
|
+
export function canvasScope(layout: 'line' | 'force'): { kinds: ReadonlySet<AttributionNodeKind>; edges: ReadonlySet<AttributionEdgeKind> } {
|
|
201
|
+
return layout === 'line'
|
|
202
|
+
? { kinds: new Set(OFF_LANE_KINDS), edges: new Set(OFF_LANE_EDGE_KINDS) }
|
|
203
|
+
: { kinds: new Set<AttributionNodeKind>(), edges: new Set<AttributionEdgeKind>() }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** 段 → 这一段的节点种类。四组切面用它表达「关掉检查点 = 第三段整段不画」。 */
|
|
207
|
+
export const LANE_KINDS: Record<LaneId, AttributionNodeKind[]> = Object.fromEntries(
|
|
208
|
+
LANES.map(lane => [lane.id, lane.kinds]),
|
|
209
|
+
) as Record<LaneId, AttributionNodeKind[]>
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 这一段的节点是否被画出。
|
|
213
|
+
*
|
|
214
|
+
* `kinds` 空集 = 不过滤(与其余切面同一条规则)。实例层还要过 `includeInstance`:
|
|
215
|
+
* ⑤ 段默认折叠,但被点亮的那条链例外(§7.7 末段),那条例外由 `chainIds` 承担。
|
|
216
|
+
*/
|
|
217
|
+
export function laneKindVisible(kinds: ReadonlySet<AttributionNodeKind>, lane: LaneSpec): boolean {
|
|
218
|
+
if (kinds.size === 0) return true
|
|
219
|
+
return lane.kinds.some(kind => kinds.has(kind))
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 资产类型的固定顺序(表 / 指标 / 任务 / 链路)。
|
|
224
|
+
*
|
|
225
|
+
* 顺序按「排查时最常先想到的对象」排:先看表,再看指标口径,再看产出它的任务,
|
|
226
|
+
* 最后才是链路级的东西。缺省的类型排在最后 —— **不抖动**比排得对更重要,
|
|
227
|
+
* 同一份数据刷新之后顺序变了,图上就没有可依赖的空间记忆。
|
|
228
|
+
*/
|
|
229
|
+
export const ASSET_TYPE_ORDER: AssetType[] = ['table', 'metric', 'job', 'linkage']
|
|
230
|
+
|
|
231
|
+
/** 资产类型的中文界面词。 */
|
|
232
|
+
export const ASSET_TYPE_LABEL: Record<AssetType, string> = {
|
|
233
|
+
table: '表',
|
|
234
|
+
metric: '指标',
|
|
235
|
+
job: '任务',
|
|
236
|
+
linkage: '链路',
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** 「≤ 5 分钟」这条成本预设的界线。 */
|
|
240
|
+
export const CHECK_COST_FAST_MINUTES = 5
|
|
241
|
+
|
|
242
|
+
/** 成本预设:三档。`undefined` 表示某一档不参与。 */
|
|
243
|
+
export interface CheckCostBand {
|
|
244
|
+
id: 'fast' | 'medium' | 'slow'
|
|
245
|
+
label: string
|
|
246
|
+
within: (minutes: number) => boolean
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export const CHECK_COST_BANDS: CheckCostBand[] = [
|
|
250
|
+
{ id: 'fast', label: `≤ ${CHECK_COST_FAST_MINUTES} 分钟`, within: m => m <= CHECK_COST_FAST_MINUTES },
|
|
251
|
+
{ id: 'medium', label: `${CHECK_COST_FAST_MINUTES}–15 分钟`, within: m => m > CHECK_COST_FAST_MINUTES && m <= 15 },
|
|
252
|
+
{ id: 'slow', label: '> 15 分钟', within: m => m > 15 },
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
/** 时间倒序用的时间戳:解析不了就当作 0(排在最后),绝不抛。 */
|
|
256
|
+
export function timestampOf(node: AttributionNode): number {
|
|
257
|
+
const raw = node.at
|
|
258
|
+
if (!raw) return 0
|
|
259
|
+
const parsed = Date.parse(raw)
|
|
260
|
+
return Number.isNaN(parsed) ? 0 : parsed
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* 一段的头两行读数:`段名 + 这一段的节点数`。
|
|
265
|
+
*
|
|
266
|
+
* `total` 是**过滤前**的节点数:段头必须回答「这一段本来有多少、现在画了几个」,
|
|
267
|
+
* 只报画出来的那个数,切面一开就成了「这张图本来就这么小」。
|
|
268
|
+
*/
|
|
269
|
+
export interface LaneSegment {
|
|
270
|
+
lane: LaneSpec
|
|
271
|
+
/** 过滤后、落在这一段里的节点,已按 `orderHint` 排好。 */
|
|
272
|
+
nodes: AttributionNode[]
|
|
273
|
+
/** 过滤前的节点数。 */
|
|
274
|
+
total: number
|
|
275
|
+
/** 这一段在数据里本来就一条都没有(不是被切面关掉的,也不是被收起的)。 */
|
|
276
|
+
empty: boolean
|
|
277
|
+
/**
|
|
278
|
+
* 这一段里**没被画出来**的节点(`total - nodes` 的那一份,保持段内顺序)。
|
|
279
|
+
*
|
|
280
|
+
* 两种用途,都是「有,但这一屏不画」:
|
|
281
|
+
* - ① 段:`focusSymptomId` 之外的现象 —— 它们变成段头上的切换入口;
|
|
282
|
+
* - ④⑤ 段:收起时的那几个 —— 段头写「已收起 · N 个」。
|
|
283
|
+
*
|
|
284
|
+
* 与 `total` 分开是因为 `total` 只是个数字,而入口需要**知道是哪几个**。
|
|
285
|
+
*/
|
|
286
|
+
hidden: AttributionNode[]
|
|
287
|
+
/**
|
|
288
|
+
* 这一段被**收起**:有数据,是「暂时不看」。
|
|
289
|
+
*
|
|
290
|
+
* 与 `empty` 是两件必须分开的事:收起是选择,空是缺失。把两者画成同一个样子,
|
|
291
|
+
* 段头就会在用户主动收起④段时写「还没有查询依据:检查点没写 assets」——
|
|
292
|
+
* 那是**在撒谎**,而且恰好抹掉了「哪一段还没建」这个最该显眼的读数(§7.7)。
|
|
293
|
+
*/
|
|
294
|
+
collapsed: boolean
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* 段内排序键(升序)。
|
|
299
|
+
*
|
|
300
|
+
* 每条都对应 §7.7 表里的那一列,而且**只用卡片上能核对的字段**:
|
|
301
|
+
* 现象用问题大类 → 子类;假设用优先分(§4.4)取负;检查点用成本;
|
|
302
|
+
* 资产用类型;实例层用时间取负。
|
|
303
|
+
*
|
|
304
|
+
* `priorities` 是优先分表(见 `lanePriorityScores`)。**必须由调用方传入而不是
|
|
305
|
+
* 在这里现算** —— 优先分的定义点在 `candidatesOf`,如果这里再写一遍公式,
|
|
306
|
+
* 「排序」和「排查视图的候选排序」就有了两份实现,迟早会漂移。
|
|
307
|
+
*/
|
|
308
|
+
export function laneSortKey(node: AttributionNode, priorities: ReadonlyMap<string, number> = new Map()): number {
|
|
309
|
+
switch (node.kind) {
|
|
310
|
+
case 'symptom': {
|
|
311
|
+
// 先按问题大类,再按子类(§7.7 ① 段)。子类的顺序取它在大类里的定义顺序 ——
|
|
312
|
+
// 那是「看到异常之后的判断顺序」,不是字母序。
|
|
313
|
+
const owner = problemClassOf(node.problemSubclass)
|
|
314
|
+
const major = owner ? PROBLEM_CLASS_ORDER.indexOf(owner) : PROBLEM_CLASS_ORDER.length
|
|
315
|
+
const siblings = owner ? (PROBLEM_CLASSES.find(item => item.id === owner)?.subclasses ?? []) : []
|
|
316
|
+
const sub = siblings.findIndex(item => item.id === node.problemSubclass)
|
|
317
|
+
return major * 100 + (sub < 0 ? 99 : sub)
|
|
318
|
+
}
|
|
319
|
+
case 'hypothesis':
|
|
320
|
+
// 优先分越大越靠前 → 取负。空区拿不到分(表里没有它),记 0 排在这一段末尾。
|
|
321
|
+
return -(priorities.get(node.id) ?? 0)
|
|
322
|
+
case 'check_point':
|
|
323
|
+
return node.costMinutes ?? Number.MAX_SAFE_INTEGER
|
|
324
|
+
case 'asset': {
|
|
325
|
+
// 资产类型**由卡片声明**(`assets[]` 写成 `{ id, type }`),不由代码从 id 猜:
|
|
326
|
+
// 猜前缀会在有人给表起名 `t_xxx` 的那天静默失效,而失效的表现是「这一段排序
|
|
327
|
+
// 莫名其妙地变了」。没声明就退回 id 序 —— 两种都稳定、可复现。
|
|
328
|
+
const index = node.assetType ? ASSET_TYPE_ORDER.indexOf(node.assetType) : -1
|
|
329
|
+
return index < 0 ? ASSET_TYPE_ORDER.length : index
|
|
330
|
+
}
|
|
331
|
+
default:
|
|
332
|
+
// 实例层:按时间倒序 → 取负。
|
|
333
|
+
return -timestampOf(node)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* 假设 id → 优先分(§4.4 的 `prior ÷ 最低检查点成本`)。
|
|
339
|
+
*
|
|
340
|
+
* **直接读 `candidatesOf`**:那是优先分唯一的实现,排查视图的候选排序也用它。
|
|
341
|
+
* 一条假设被多个现象指向时取其中**最高**的分 —— 它问的是「这条假设现在值不值得先查」,
|
|
342
|
+
* 而不是「哪个现象问的」。不在任何现象的候选集里的假设(今天没有这种数据,
|
|
343
|
+
* 但结构上允许)分记 0,排在这一段的末尾而不是消失。
|
|
344
|
+
*/
|
|
345
|
+
export function lanePriorityScores(data: AttributionGraphData): Map<string, number> {
|
|
346
|
+
const scores = new Map<string, number>()
|
|
347
|
+
for (const symptom of data.nodes) {
|
|
348
|
+
if (symptom.kind !== 'symptom') continue
|
|
349
|
+
for (const candidate of candidatesOf(data, symptom.id)) {
|
|
350
|
+
const best = scores.get(candidate.hypothesis.id)
|
|
351
|
+
if (best === undefined || candidate.score > best) scores.set(candidate.hypothesis.id, candidate.score)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return scores
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* 只留下「至少有一段要画它」的节点种类。
|
|
359
|
+
*
|
|
360
|
+
* 这是 §7.3「类型切面被三组吸收」的实现:按 `kind` 勾选与按动线段落收窄说的
|
|
361
|
+
* 是同一件事,同时存在只会让人犹豫点哪个 —— 所以 `kinds` 收窄的是**段**,
|
|
362
|
+
* 关闭一段 = 那一段的节点种类一起离开。
|
|
363
|
+
*/
|
|
364
|
+
export function narrowedToLanes(nodes: AttributionNode[], kinds: ReadonlySet<AttributionNodeKind>): AttributionNode[] {
|
|
365
|
+
if (kinds.size === 0) return nodes
|
|
366
|
+
const open = new Set<LaneId>()
|
|
367
|
+
for (const lane of LANES) if (laneKindVisible(kinds, lane)) open.add(lane.id)
|
|
368
|
+
// 不属于任何一段的节点(失败原因、案例)跟着**任意一段开着**就留下:
|
|
369
|
+
// 它们不是某一段的内容,而是挂在段上的标签与路径记录。
|
|
370
|
+
return nodes.filter(node => {
|
|
371
|
+
const lane = laneOf(node)
|
|
372
|
+
return lane === undefined ? open.size > 0 : open.has(lane)
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* 五段泳道的分段结果。
|
|
378
|
+
*
|
|
379
|
+
* 两条硬规则:
|
|
380
|
+
* 1. **空段不隐藏** —— 一条都没有的段照样返回,`empty: true`,段头写 note。
|
|
381
|
+
* 2. **实例层默认折叠** —— `includeInstance` 关掉时 ⑤ 段只留计数;`chainIds`
|
|
382
|
+
* 里被点亮的那条链上的实例节点例外(§7.7 末段)。
|
|
383
|
+
*/
|
|
384
|
+
export function laneSegments(data: AttributionGraphData, options: {
|
|
385
|
+
/** 是否展开实例层(⑤ 段)。 */
|
|
386
|
+
includeInstance?: boolean
|
|
387
|
+
/** 被点亮的那条链;其中的实例节点在折叠态下仍然画出来。 */
|
|
388
|
+
chainIds?: ReadonlySet<string>
|
|
389
|
+
/**
|
|
390
|
+
* 被用户收起的段。**有数据但不画**。
|
|
391
|
+
*
|
|
392
|
+
* 收起是**默认读法的一部分**,不是附加功能:面板只有 380px 宽,五段全铺时
|
|
393
|
+
* ④ 查询依据要占掉 5 个节点 + 11 条跨段边,而它是引用层的定位锚点(§0.1
|
|
394
|
+
* 「只作定位锚点」),不是动线上的决策对象。默认收起它,非空段从 4 段降到 3 段,
|
|
395
|
+
* 每段的节点能明显变大。
|
|
396
|
+
*
|
|
397
|
+
* 与 ⑤ 段的折叠是同一条规矩(§7.7 末段):**被点亮的那条链上的节点例外**,
|
|
398
|
+
* 照样画出来 —— 用户点开一条链,就是在问「这条路到底走到哪」。
|
|
399
|
+
*/
|
|
400
|
+
collapsedLanes?: ReadonlySet<LaneId>
|
|
401
|
+
/**
|
|
402
|
+
* ① 段**只画这一个现象**(动线视角的当前现象),其余现象进 `hidden` 变成切换入口。
|
|
403
|
+
*
|
|
404
|
+
* 为什么必须收窄:3 个现象共享 7 个假设(§4.5),于是 ①→② 之间有 14 条
|
|
405
|
+
* `may_be_caused_by`,而它们**互相交叉** —— 交叉的成因是「有三个起点」,不是
|
|
406
|
+
* 「哪条边不该画」。只留一个起点之后,那 7 条线从一个点扇出,彼此不再相交,
|
|
407
|
+
* 而画布上剩下的每一条边**都真实属于当前这个现象**(不是把别的现象的边藏起来
|
|
408
|
+
* 假装它不存在)。
|
|
409
|
+
*
|
|
410
|
+
* 代价说清楚:「三个现象共享同一批假设」这件事在动线视角里就看不到了,
|
|
411
|
+
* 它在关系视图里照旧。动线回答的是「这个现象的下一步」。
|
|
412
|
+
*/
|
|
413
|
+
focusSymptomId?: string
|
|
414
|
+
} = {}): LaneSegment[] {
|
|
415
|
+
const includeInstance = options.includeInstance ?? false
|
|
416
|
+
const chainIds = options.chainIds ?? new Set<string>()
|
|
417
|
+
const collapsedLanes = options.collapsedLanes ?? new Set<LaneId>()
|
|
418
|
+
const focusSymptomId = options.focusSymptomId
|
|
419
|
+
const priorities = lanePriorityScores(data)
|
|
420
|
+
|
|
421
|
+
return LANES.map(lane => {
|
|
422
|
+
const all = data.nodes.filter(node => laneOf(node) === lane.id)
|
|
423
|
+
/**
|
|
424
|
+
* **三态互斥**:空(没数据)/ 收起(有数据但不画)/ 展开。
|
|
425
|
+
*
|
|
426
|
+
* `all.length > 0 &&` 这一半不能省:⑤ 实例层默认就是「收起」的,而一个**空**的
|
|
427
|
+
* ⑤ 段也会因此被标成「收起」。段头的读数先看 `total === 0`,所以画出来是对的 ——
|
|
428
|
+
* 但模型层同时说「它是空的」和「它被收起了」,下一个读这两个字段的人就会踩坑。
|
|
429
|
+
*/
|
|
430
|
+
const collapsed = all.length > 0 && (collapsedLanes.has(lane.id) || (lane.instance && !includeInstance))
|
|
431
|
+
// ① 段收窄到当前现象。**只在真的能指出那一个时收窄** —— `focusSymptomId` 指向一个
|
|
432
|
+
// 不存在的 id 时若照收窄,① 段会整段空掉,而段头写的是「还没有现象」:那是在撒谎。
|
|
433
|
+
const focused = lane.id === 'symptom' && focusSymptomId !== undefined && all.some(node => node.id === focusSymptomId)
|
|
434
|
+
const visible = collapsed
|
|
435
|
+
? all.filter(node => chainIds.has(node.id))
|
|
436
|
+
: focused ? all.filter(node => node.id === focusSymptomId) : all
|
|
437
|
+
const ordered = visible.slice().sort((a, b) =>
|
|
438
|
+
laneSortKey(a, priorities) - laneSortKey(b, priorities) || a.id.localeCompare(b.id))
|
|
439
|
+
const drawn = new Set(ordered.map(node => node.id))
|
|
440
|
+
return {
|
|
441
|
+
lane, nodes: ordered, total: all.length, empty: all.length === 0, collapsed,
|
|
442
|
+
hidden: all.filter(node => !drawn.has(node.id)),
|
|
443
|
+
}
|
|
444
|
+
})
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* 段的分段结果 → 段位布局的入参。
|
|
449
|
+
*
|
|
450
|
+
* `laneLayoutOf` 只关心「哪一段、按什么顺序」,段名与计数是段头的事;两边用这一层
|
|
451
|
+
* 薄映射隔开,`graph-options.ts` 就不必 import 这个文件的段类型。
|
|
452
|
+
*/
|
|
453
|
+
export function laneBandsOf(segments: LaneSegment[]): Array<{ laneId: LaneId; nodes: AttributionNode[] }> {
|
|
454
|
+
return segments.map(segment => ({ laneId: segment.lane.id, nodes: segment.nodes }))
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* 一条被点亮的链:从选中的现象 / 假设出发,沿**有语义方向**的边往前走。
|
|
459
|
+
*
|
|
460
|
+
* 方向沿用 `PROBLEM_SCOPE_EDGES` 那份判断(产出性关系朝前、归属与索引朝后),
|
|
461
|
+
* 再加上实例层的四条执行边。之所以不能双向:双向会经由共享假设串回另外两个
|
|
462
|
+
* 现象,点一个现象就把整张图点亮 —— 那等于没有高亮。
|
|
463
|
+
*
|
|
464
|
+
* **从现象出发只走一跳。** 现象有 7 条候选假设(§4.5:三个现象共享同一批假设),
|
|
465
|
+
* 再往前就会经由每条假设铺开它们的检查点、资产与证据 —— 点 S1 会点亮 25/28 个节点。
|
|
466
|
+
* 那正是「点亮」要避免的结果。现象那一跳要回答的是「它有哪些候选原因」;
|
|
467
|
+
* 「这条假设往下怎么查」属于假设的链,点那个假设才展开(§7.4 第 9 条)。
|
|
468
|
+
*
|
|
469
|
+
* `refutes` 也算数:被否定的边同样是这条链走过的一段(§7.4 第 3 条)。
|
|
470
|
+
*/
|
|
471
|
+
export function chainOf(data: AttributionGraphData, startId: string): Set<string> {
|
|
472
|
+
const start = data.nodes.find(node => node.id === startId)
|
|
473
|
+
const chain = new Set<string>([startId])
|
|
474
|
+
let frontier = new Set<string>([startId])
|
|
475
|
+
// 链最长不会超过节点数;这个上界只是防止数据里出现环时死循环。
|
|
476
|
+
for (let step = 0; step < data.nodes.length; step += 1) {
|
|
477
|
+
const next = new Set<string>()
|
|
478
|
+
for (const edge of data.edges) {
|
|
479
|
+
const direction = CHAIN_EDGES[edge.kind]
|
|
480
|
+
if (direction === undefined) continue
|
|
481
|
+
// 现象出发只走一跳:第二跳起整张图都会被点亮(见函数头)。
|
|
482
|
+
if (start?.kind === 'symptom' && step >= 1) continue
|
|
483
|
+
const from = direction === 'forward' ? edge.source : edge.target
|
|
484
|
+
const to = direction === 'forward' ? edge.target : edge.source
|
|
485
|
+
if (frontier.has(from) && !chain.has(to)) next.add(to)
|
|
486
|
+
}
|
|
487
|
+
if (next.size === 0) break
|
|
488
|
+
for (const id of next) chain.add(id)
|
|
489
|
+
frontier = next
|
|
490
|
+
}
|
|
491
|
+
return chain
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** 点亮链允许走的边与方向。 */
|
|
495
|
+
const CHAIN_EDGES: Partial<Record<AttributionEdgeKind, 'forward' | 'backward'>> = {
|
|
496
|
+
may_be_caused_by: 'forward',
|
|
497
|
+
verified_by: 'forward',
|
|
498
|
+
categorized_as: 'forward',
|
|
499
|
+
queries: 'forward',
|
|
500
|
+
produces: 'forward',
|
|
501
|
+
confirmed_as: 'forward',
|
|
502
|
+
// 证据 → 假设:由已跑过的证据找回它支持 / 否定的假设。方向朝前即可 ——
|
|
503
|
+
// 起点若是假设,`supports` 的箭头指向它,落在 `backward` 那一侧才是它的入边。
|
|
504
|
+
supports: 'backward',
|
|
505
|
+
refutes: 'backward',
|
|
506
|
+
}
|
|
89
507
|
|
|
90
508
|
/**
|
|
91
|
-
*
|
|
509
|
+
* 会不会被点亮:**只有从现象或假设出发**才点亮一条链。
|
|
92
510
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* 不是界面偏好,所以放在模型层 —— 界面层两个组件(工具条、筛选条)都要用它。
|
|
511
|
+
* 从资产或检查点出发也「有链」,但那回答的是「谁指向我」,与 §7.4 第 9 条
|
|
512
|
+
* 「下一步查什么」不是同一个问题。所以起点做限制,画布上点其它节点只切聚焦。
|
|
96
513
|
*/
|
|
97
|
-
export
|
|
514
|
+
export function chainStartAllowed(node: AttributionNode | undefined): boolean {
|
|
515
|
+
return node?.kind === 'symptom' || node?.kind === 'hypothesis'
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* 会出现在**段切面**里的节点种类 = 会画在画布上的那几段。
|
|
521
|
+
*
|
|
522
|
+
* **不再是独立的一份名单。** v1.4 撤销了笼统的「类型切面」:节点可见性由它所属的
|
|
523
|
+
* 那一段承担(§7.3),所以这份名单由 `LANES` 派生 —— 加一段就自动多一类,
|
|
524
|
+
* 不会出现「段能画、切面里找不到」这种两处对不上的情况。
|
|
525
|
+
*
|
|
526
|
+
* **实例层(⑤ 段)不在这份名单里**:它默认就折叠着,列进切面只会让用户点出一个
|
|
527
|
+
* 空图(那份名单原来把这条写成了注释,现在由 `instance` 这一格承担)。
|
|
528
|
+
* 展开 ⑤ 段是右上的一个显式开关,不是切面。
|
|
529
|
+
*
|
|
530
|
+
* 顺序按动线:现象 → 假设 → 检查点 → 资产。失败原因与案例不在这份名单里,
|
|
531
|
+
* 它们不是某一段的内容(失败原因是假设的标签,案例是路径的记录)。
|
|
532
|
+
* 定义点排在 `LANES` 之后 —— 模块求值时它要读到 `LANES`。
|
|
533
|
+
*/
|
|
534
|
+
export const FILTERABLE_KINDS: AttributionNodeKind[] =
|
|
535
|
+
LANES.filter(lane => !lane.instance).flatMap(lane => lane.kinds)
|
|
98
536
|
|
|
99
537
|
/**
|
|
100
538
|
* 节点尺寸:假设节点按复用次数放大,其余按类型给固定宽高。
|
|
@@ -111,9 +549,9 @@ export function nodeBoxOf(node: AttributionNode): [number, number] {
|
|
|
111
549
|
return [size, size]
|
|
112
550
|
}
|
|
113
551
|
if (node.kind === 'symptom') return [150, 44]
|
|
114
|
-
if (node.kind === '
|
|
552
|
+
if (node.kind === 'check_point') return [156, 38]
|
|
115
553
|
if (node.kind === 'asset') return [132, 36]
|
|
116
|
-
if (node.kind === '
|
|
554
|
+
if (node.kind === 'failure_reason') return [104, 30]
|
|
117
555
|
if (node.kind === 'case') return [184, 38]
|
|
118
556
|
return [150, 36]
|
|
119
557
|
}
|
|
@@ -125,10 +563,10 @@ export function nodeShapeOf(node: AttributionNode): 'circle' | 'rect' {
|
|
|
125
563
|
|
|
126
564
|
/** 节点的取色:假设跟着失败模式走,其余走层色。 */
|
|
127
565
|
export function nodeStyleOf(node: AttributionNode): { fill: string; stroke: string; text: string } {
|
|
128
|
-
if (node.
|
|
566
|
+
if (node.failureReason) return FAILURE_REASON_STYLE[node.failureReason]
|
|
129
567
|
if (node.layer === 'reference') return { fill: '#eef4ff', stroke: '#9ec2ff', text: '#1d4ed8' }
|
|
130
568
|
if (node.kind === 'symptom') return { fill: '#fff7ea', stroke: '#b96d14', text: '#96580f' }
|
|
131
|
-
if (node.kind === '
|
|
569
|
+
if (node.kind === 'check_point') return { fill: '#f5f1ff', stroke: '#c9b8ff', text: '#5b45be' }
|
|
132
570
|
return NEUTRAL_STYLE
|
|
133
571
|
}
|
|
134
572
|
|
|
@@ -186,6 +624,24 @@ export function visibleEdges(data: AttributionGraphData, nodes: AttributionNode[
|
|
|
186
624
|
return data.edges.filter(edge => ids.has(edge.source) && ids.has(edge.target))
|
|
187
625
|
}
|
|
188
626
|
|
|
627
|
+
/* ────────────────────────────────────────────────────────────────────────────
|
|
628
|
+
* 动线视角:五段泳道(0021 §7.7 / §4.6)
|
|
629
|
+
*
|
|
630
|
+
* 这一节是**纯数据**:段名、每段装什么、段内怎么排、段位坐标。
|
|
631
|
+
* 「画在画布上」与「画成 HTML 泳道带」都读同一份结果,两处不可能对不上 ——
|
|
632
|
+
* 这正是 §7.7「空段不隐藏」「断链」能被断言而不是被眼力的原因。
|
|
633
|
+
* ──────────────────────────────────────────────────────────────────────────── */
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* 五个段位。
|
|
637
|
+
*
|
|
638
|
+
* 段名固定成这五个词,**不写「第一步 / 第二步」**(§7.5):它们是数据的分段,
|
|
639
|
+
* 不是操作步骤;操作步骤在排查视图里,写成步骤会让「同一现象有 N 个假设」
|
|
640
|
+
* 读起来像「做错了要重来」。
|
|
641
|
+
*
|
|
642
|
+
* `note` 是**空段说明**:一段为空时画在段头右边的那句话。它必须与段的存在理由
|
|
643
|
+
* 对上 —— 曲线是「哪一段还没建」这张图最该显眼的信息(§7.7)。
|
|
644
|
+
*/
|
|
189
645
|
/**
|
|
190
646
|
* 以某个节点为中心取 N 跳邻域。
|
|
191
647
|
*
|
|
@@ -234,6 +690,28 @@ export function degreeOf(data: AttributionGraphData, id: string): number {
|
|
|
234
690
|
}
|
|
235
691
|
|
|
236
692
|
/** 默认焦点:类型层里度数最高的假设。没有假设时退化为第一个节点。 */
|
|
693
|
+
/**
|
|
694
|
+
* 打开这一栏时,**默认点亮哪条链**(§7.4 第 9 条:「路径高亮是默认读法,不是附加功能」)。
|
|
695
|
+
*
|
|
696
|
+
* 取①段**排好序之后的第一个现象** —— 就是扫视时先看到的那个。不取「候选最多的」:
|
|
697
|
+
* 那会让默认焦点随着别处新增一个假设而跳走,而空间记忆靠的正是「上次那个在哪儿」。
|
|
698
|
+
*
|
|
699
|
+
* 只挑**真的能点亮一条链**的现象(至少有一条 `may_be_caused_by` 从它出去)。点亮一个
|
|
700
|
+
* 孤立现象会让整张图暗下去而它什么都没连上 —— 那看起来像界面坏了,而不是
|
|
701
|
+
* 「这个现象还没建」,而后者才是真相。
|
|
702
|
+
*/
|
|
703
|
+
export function defaultChainStart(data: AttributionGraphData): string | undefined {
|
|
704
|
+
const hasCandidate = new Set(
|
|
705
|
+
data.edges.filter(edge => edge.kind === 'may_be_caused_by').map(edge => edge.source),
|
|
706
|
+
)
|
|
707
|
+
const priorities = lanePriorityScores(data)
|
|
708
|
+
const symptoms = data.nodes
|
|
709
|
+
.filter(node => node.kind === 'symptom' && hasCandidate.has(node.id))
|
|
710
|
+
// 与①段的段内排序**同一把尺**:这样「默认点亮的那个」就是段里最左边那个。
|
|
711
|
+
.sort((a, b) => laneSortKey(a, priorities) - laneSortKey(b, priorities) || a.id.localeCompare(b.id))
|
|
712
|
+
return symptoms[0]?.id
|
|
713
|
+
}
|
|
714
|
+
|
|
237
715
|
export function defaultFocusId(data: AttributionGraphData): string | undefined {
|
|
238
716
|
const hypotheses = data.nodes.filter(node => node.kind === 'hypothesis')
|
|
239
717
|
if (hypotheses.length === 0) return data.nodes[0]?.id
|
|
@@ -256,9 +734,13 @@ export interface AttributionCandidate {
|
|
|
256
734
|
}
|
|
257
735
|
|
|
258
736
|
/**
|
|
259
|
-
*
|
|
737
|
+
* 根因占比的**示例分母**,取自 0021 §4.4 的示例表
|
|
738
|
+
* (12 / 6 / 4 / 3 / 1 / 5 / 2,合计 33 次已归类 + 8 次未归类 = 41)。
|
|
260
739
|
*
|
|
261
|
-
*
|
|
740
|
+
* ⚠️ **它不是这个知识库的真实样本量。** `prior` 属于卡片信封里的 `stats`(系统回填),
|
|
741
|
+
* 而回填还没实现 —— 于是界面上的「根因占比」今天是用这张示例表当分母算出来的。
|
|
742
|
+
* 界面必须如实说清这一点(`RunView` 的副标题就是这么写的):产品的第一条品牌承诺是
|
|
743
|
+
* **不编造数字**,把一个示例分母渲染成「样本量 41 次(近 30 天)」正是那条承诺要拦的事。
|
|
262
744
|
*/
|
|
263
745
|
export const SAMPLE_SIZE = 41
|
|
264
746
|
|
|
@@ -314,8 +796,15 @@ export function candidatesOf(data: AttributionGraphData, symptomId: string): Att
|
|
|
314
796
|
/** 筛选条件。空集合 = 不按该维度过滤。 */
|
|
315
797
|
export interface GraphFilter {
|
|
316
798
|
query: string
|
|
799
|
+
/**
|
|
800
|
+
* 节点种类切面。
|
|
801
|
+
*
|
|
802
|
+
* **它就是动线段落的开关**(§7.3):按 `kind` 勾选与按动线段落收窄说的是同一件事。
|
|
803
|
+
* 关闭「检查点」= 第三段整段不画,所以界面上一律用段名做标签(`LANES`),
|
|
804
|
+
* 不再并列一个笼统的「类型」切面。
|
|
805
|
+
*/
|
|
317
806
|
kinds: ReadonlySet<AttributionNodeKind>
|
|
318
|
-
|
|
807
|
+
failureReasons: ReadonlySet<FailureReason>
|
|
319
808
|
/**
|
|
320
809
|
* 问题大类。**它不是一条节点谓词,而是一个子图选择器。**
|
|
321
810
|
*
|
|
@@ -325,6 +814,16 @@ export interface GraphFilter {
|
|
|
325
814
|
* 其余条件再在这个范围里生效(见 `filterGraph`)。
|
|
326
815
|
*/
|
|
327
816
|
problemClasses: ReadonlySet<ProblemClassId>
|
|
817
|
+
/**
|
|
818
|
+
* 问题子类。**在大类选中的范围之内**再收窄形态(§4.5 的两级切面)。
|
|
819
|
+
*
|
|
820
|
+
* 它**只作用于现象节点**:子类是现象的字段,原因侧没有这一格。与 `kinds` 不同,
|
|
821
|
+
* 它不是范围选择器 —— 命中现象之后邻居照旧由 `includeNeighbors` 带出来,
|
|
822
|
+
* 否则按子类筛会把「这个问题能查什么」一起筛掉。
|
|
823
|
+
*/
|
|
824
|
+
problemSubclasses: ReadonlySet<string>
|
|
825
|
+
/** 只留空区:有假设、一条检查都没有。空区是读数,也是一个切面(§7.3 第 3 组)。 */
|
|
826
|
+
gapOnly: boolean
|
|
328
827
|
/** 命中节点之外,是否把它们的 1 跳邻居也留下作为上下文。 */
|
|
329
828
|
includeNeighbors: boolean
|
|
330
829
|
}
|
|
@@ -332,11 +831,23 @@ export interface GraphFilter {
|
|
|
332
831
|
export const EMPTY_FILTER: GraphFilter = {
|
|
333
832
|
query: '',
|
|
334
833
|
kinds: new Set(),
|
|
335
|
-
|
|
834
|
+
failureReasons: new Set(),
|
|
336
835
|
problemClasses: new Set(),
|
|
836
|
+
problemSubclasses: new Set(),
|
|
837
|
+
gapOnly: false,
|
|
337
838
|
includeNeighbors: true,
|
|
338
839
|
}
|
|
339
840
|
|
|
841
|
+
/**
|
|
842
|
+
* 空图:一个知识库还没有任何排查知识时的取值。
|
|
843
|
+
*
|
|
844
|
+
* 存在的理由是**初始状态不能是示例**。这一栏原来用 `SEED_GRAPH` 当初始值,于是
|
|
845
|
+
* 「服务端还没答话」和「这个库有 34 个节点的排查知识」在界面上长得一模一样 ——
|
|
846
|
+
* 用户没法分辨自己看到的到底是自己的库还是内置样例。现在初始值是空的,
|
|
847
|
+
* 真数据由 `/diagnosis/graph` 填进来,示例只走「载入内置示例」那枚按钮。
|
|
848
|
+
*/
|
|
849
|
+
export const EMPTY_GRAPH: AttributionGraphData = { nodes: [], edges: [] }
|
|
850
|
+
|
|
340
851
|
/**
|
|
341
852
|
* 按问题筛时要走几跳。
|
|
342
853
|
*
|
|
@@ -397,9 +908,9 @@ function matchesQuery(node: AttributionNode, query: string): boolean {
|
|
|
397
908
|
/**
|
|
398
909
|
* 图上的筛选。
|
|
399
910
|
*
|
|
400
|
-
*
|
|
401
|
-
* 0021 §4.3 已经定了分类是切面,所以这里也做成切面 ——
|
|
402
|
-
*
|
|
911
|
+
* 按「问题大类(范围)/ 问题子类 / 段(节点种类)/ 失败原因 / 空区 / 文本」过滤,
|
|
912
|
+
* **不新增一个「分类树」**:0021 §4.3 已经定了分类是切面,所以这里也做成切面 ——
|
|
913
|
+
* 它可以多选、可以为空、可以随时改回来看全图,而不像树的层级那样「选错就得往回退」。
|
|
403
914
|
*
|
|
404
915
|
* `includeNeighbors` 默认开:只留命中节点会让图散成孤点,边的语义也一起丢了。
|
|
405
916
|
* 留下来的是命中节点及其 1 跳邻居,边只在两端都留下时才保留。
|
|
@@ -408,9 +919,9 @@ export function filterGraph(data: AttributionGraphData, filter: GraphFilter): At
|
|
|
408
919
|
const query = filter.query.trim().toLocaleLowerCase()
|
|
409
920
|
|
|
410
921
|
// 第一步:问题大类先把**范围**收窄成「命中现象 2 跳以内」的子图。
|
|
411
|
-
// 顺序很重要 ——
|
|
922
|
+
// 顺序很重要 —— 先收范围再套文本/段/失败原因,语义才是「在这类问题里找」,
|
|
412
923
|
// 而不是「在整张图里找同时满足所有条件的节点」(后者永远是空集)。
|
|
413
|
-
|
|
924
|
+
let inScope = filter.problemClasses.size === 0
|
|
414
925
|
? data
|
|
415
926
|
: problemScopeOf(
|
|
416
927
|
data,
|
|
@@ -419,18 +930,36 @@ export function filterGraph(data: AttributionGraphData, filter: GraphFilter): At
|
|
|
419
930
|
.map(node => node.id),
|
|
420
931
|
)
|
|
421
932
|
|
|
422
|
-
|
|
933
|
+
// 空区是**假设的谓词**,但它要连同「谁指向它、它在哪张表上」一起看 —— 只留一个孤零零的
|
|
934
|
+
// 假设圆等于把这张图上唯一能动手的线索(它缺什么)删掉了。所以它先算命中集,
|
|
935
|
+
// 再照常走邻居扩张。
|
|
936
|
+
const gapOnlyHits = filter.gapOnly
|
|
937
|
+
? new Set(gapHypothesisIds(inScope))
|
|
938
|
+
: undefined
|
|
939
|
+
if (gapOnlyHits) {
|
|
940
|
+
const keep = filter.includeNeighbors ? neighborhoodIds(inScope, gapOnlyHits) : gapOnlyHits
|
|
941
|
+
inScope = {
|
|
942
|
+
nodes: inScope.nodes.filter(node => keep.has(node.id)),
|
|
943
|
+
edges: inScope.edges.filter(edge => keep.has(edge.source) && keep.has(edge.target)),
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const noFilter = query === '' && filter.kinds.size === 0 && filter.failureReasons.size === 0
|
|
948
|
+
&& filter.problemSubclasses.size === 0
|
|
423
949
|
if (noFilter) return inScope
|
|
424
950
|
|
|
425
951
|
const hits = new Set<string>()
|
|
426
952
|
for (const node of inScope.nodes) {
|
|
427
953
|
if (query !== '' && !matchesQuery(node, query)) continue
|
|
428
954
|
if (filter.kinds.size > 0 && !filter.kinds.has(node.kind)) continue
|
|
429
|
-
if (filter.
|
|
430
|
-
//
|
|
431
|
-
const
|
|
432
|
-
if (!
|
|
955
|
+
if (filter.failureReasons.size > 0) {
|
|
956
|
+
// 失败原因是假设的属性;失败原因节点本身也要能被选出来。
|
|
957
|
+
const isReasonNode = node.kind === 'failure_reason'
|
|
958
|
+
if (!isReasonNode && (node.failureReason === undefined || !filter.failureReasons.has(node.failureReason))) continue
|
|
433
959
|
}
|
|
960
|
+
// 子类是现象的字段:它只收窄现象,原因侧的节点不受它影响 —— 作用对象不同(§7.3)。
|
|
961
|
+
if (filter.problemSubclasses.size > 0 && node.kind === 'symptom'
|
|
962
|
+
&& (node.problemSubclass === undefined || !filter.problemSubclasses.has(node.problemSubclass))) continue
|
|
434
963
|
hits.add(node.id)
|
|
435
964
|
}
|
|
436
965
|
|