dsh-data-domain-knowledge 0.1.0-alpha.2 → 0.1.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -7
- package/cordis.patch.yml +11 -5
- package/lib/client.js +5557 -3056
- package/lib/doc-page.js +7740 -0
- package/lib/types/convert/host.d.ts.map +1 -1
- package/lib/types/convert/host.js +36 -9
- package/lib/types/convert/host.js.map +1 -1
- package/lib/types/diagnosis/cards.d.ts +8 -7
- package/lib/types/diagnosis/cards.d.ts.map +1 -1
- package/lib/types/diagnosis/cards.js +119 -49
- package/lib/types/diagnosis/cards.js.map +1 -1
- package/lib/types/diagnosis/run.d.ts +51 -0
- package/lib/types/diagnosis/run.d.ts.map +1 -0
- package/lib/types/diagnosis/run.js +239 -0
- package/lib/types/diagnosis/run.js.map +1 -0
- package/lib/types/diagnosis/schema.d.ts +25 -2
- package/lib/types/diagnosis/schema.d.ts.map +1 -1
- package/lib/types/diagnosis/schema.js +64 -25
- package/lib/types/diagnosis/schema.js.map +1 -1
- package/lib/types/diagnosis/seed-source.d.ts +17 -2
- package/lib/types/diagnosis/seed-source.d.ts.map +1 -1
- package/lib/types/diagnosis/seed-source.js +18 -1
- package/lib/types/diagnosis/seed-source.js.map +1 -1
- package/lib/types/diagnosis/seed.d.ts +32 -42
- package/lib/types/diagnosis/seed.d.ts.map +1 -1
- package/lib/types/diagnosis/seed.js +187 -51
- package/lib/types/diagnosis/seed.js.map +1 -1
- package/lib/types/diagnosis/store.d.ts +114 -26
- package/lib/types/diagnosis/store.d.ts.map +1 -1
- package/lib/types/diagnosis/store.js +336 -167
- package/lib/types/diagnosis/store.js.map +1 -1
- package/lib/types/diagnosis/types.d.ts +168 -16
- package/lib/types/diagnosis/types.d.ts.map +1 -1
- package/lib/types/diagnosis/types.js +1 -1
- package/lib/types/diagnosis/types.js.map +1 -1
- package/lib/types/document-page.d.ts +39 -0
- package/lib/types/document-page.d.ts.map +1 -0
- package/lib/types/document-page.js +43 -0
- package/lib/types/document-page.js.map +1 -0
- package/lib/types/fs-atomic.d.ts +12 -0
- package/lib/types/fs-atomic.d.ts.map +1 -0
- package/lib/types/fs-atomic.js +42 -0
- package/lib/types/fs-atomic.js.map +1 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +214 -24
- package/lib/types/index.js.map +1 -1
- package/lib/types/knowledge-bases.d.ts +10 -2
- package/lib/types/knowledge-bases.d.ts.map +1 -1
- package/lib/types/knowledge-bases.js +20 -18
- package/lib/types/knowledge-bases.js.map +1 -1
- package/lib/types/proposals.d.ts +166 -0
- package/lib/types/proposals.d.ts.map +1 -0
- package/lib/types/proposals.js +238 -0
- package/lib/types/proposals.js.map +1 -0
- package/lib/types/runtime.d.ts +199 -7
- package/lib/types/runtime.d.ts.map +1 -1
- package/lib/types/runtime.js +728 -145
- package/lib/types/runtime.js.map +1 -1
- package/lib/types/settings-schema.d.ts +2 -2
- package/lib/types/types.d.ts +61 -10
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/types.js +56 -8
- package/lib/types/types.js.map +1 -1
- package/lib/types/wiki-types.d.ts +8 -1
- package/lib/types/wiki-types.d.ts.map +1 -1
- package/lib/types/wiki-types.js +16 -25
- package/lib/types/wiki-types.js.map +1 -1
- package/package.json +4 -3
- package/preset/tita-data-agent/agent.cordis.yml +11 -5
- package/src/client/DirectoryPicker.tsx +12 -12
- package/src/client/InfoHint.tsx +21 -8
- package/src/client/IntentTab.tsx +20 -26
- package/src/client/KnowledgeBaseTab.tsx +190 -80
- package/src/client/KnowledgePanel.tsx +42 -9
- package/src/client/KnowledgeStepRail.tsx +3 -0
- package/src/client/NodeSettings.tsx +20 -22
- package/src/client/PreviewSections.tsx +87 -33
- package/src/client/ProposalReview.tsx +313 -0
- package/src/client/TypeDefinitionDrawer.tsx +72 -38
- package/src/client/WikiTypeEditor.tsx +193 -77
- package/src/client/arch/AgentArchitectureGraph.tsx +143 -14
- package/src/client/arch/NodeLogicPanel.tsx +6 -3
- package/src/client/arch/model.ts +164 -26
- package/src/client/architecture.css +29 -9
- package/src/client/architecture.tsx +54 -29
- package/src/client/attribution/AttributionGraph.tsx +481 -34
- package/src/client/attribution/AttributionTab.tsx +507 -106
- package/src/client/attribution/CaseView.tsx +9 -14
- package/src/client/attribution/GraphStage.tsx +25 -18
- package/src/client/attribution/RunView.tsx +27 -9
- package/src/client/attribution/StageChrome.tsx +296 -140
- package/src/client/attribution/graph-options.ts +611 -47
- package/src/client/attribution/model.ts +559 -30
- package/src/client/convert/ConvertPanel.tsx +6 -1
- package/src/client/doc-page.css +134 -0
- package/src/client/doc-page.tsx +263 -0
- package/src/client/panel-state.ts +105 -0
- package/src/client/request.ts +36 -0
- package/src/client/styles.css +317 -43
- package/src/client/useFitScale.ts +15 -2
- package/src/client/wiki-type-groups.ts +86 -0
- package/src/convert/host.ts +41 -7
- package/src/diagnosis/cards.ts +120 -50
- package/src/diagnosis/run.ts +245 -0
- package/src/diagnosis/schema.ts +67 -26
- package/src/diagnosis/seed-source.ts +28 -3
- package/src/diagnosis/seed.ts +187 -85
- package/src/diagnosis/store.ts +358 -174
- package/src/diagnosis/types.ts +181 -16
- package/src/document-page.ts +51 -0
- package/src/fs-atomic.ts +42 -0
- package/src/index.ts +209 -22
- package/src/knowledge-bases.ts +20 -18
- package/src/proposals.ts +337 -0
- package/src/runtime.ts +736 -135
- package/src/types.ts +113 -19
- package/src/wiki-types.ts +17 -27
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AttributionEdge, AttributionGraphData, AttributionNode, CasePathStep, DiagnosisRunEvidence, DiagnosisRunRecord,
|
|
3
|
+
} from './types.js'
|
|
4
|
+
import type { DiagnosisCard } from './cards.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 一次排查的记录 —— `01_source_truth/agent_diagnosis_case/` 那条 SourceRaw 的编解码。
|
|
8
|
+
*
|
|
9
|
+
* ## 为什么它是一条 SourceRaw,而不是一个自造的 jsonl
|
|
10
|
+
*
|
|
11
|
+
* 排查产物要能被 `query_source_truth` 读到、被卡片引用、被 SourceRaw 已有的治理覆盖
|
|
12
|
+
* (分类、版本、检索),就得是一条 SourceRaw。自造一个 jsonl 格式,等于在知识库旁边
|
|
13
|
+
* 再养一套只读不进链路的数据 —— 这也是 0021 §0.3 撤销 `05_diagnosis/` 的直接原因。
|
|
14
|
+
*
|
|
15
|
+
* ## 它与案例卡片的区别
|
|
16
|
+
*
|
|
17
|
+
* | | 谁写 | 性质 |
|
|
18
|
+
* | --- | --- | --- |
|
|
19
|
+
* | 这条 SourceRaw(`agent_diagnosis_case`) | **Agent**,每次排查结束自动写 | 跑出来的**过程记录** |
|
|
20
|
+
* | 案例卡片(`diagnosis/case`) | Agent 提议 → **人确认** | 归纳出来的**结论** |
|
|
21
|
+
*
|
|
22
|
+
* 同一件事可以有两条记录,它们不是重复:一个回答「当时怎么走的」,一个回答「结论是什么」。
|
|
23
|
+
* 而案例卡片必须经人确认 —— 它落在类型层收敛的那一侧,见 0021 §6.1。
|
|
24
|
+
*
|
|
25
|
+
* ## 三段式
|
|
26
|
+
*
|
|
27
|
+
* 与卡片同构:正文小节给人读、` ```json diagnosis-run ` 块给机器读。序和枚举一进散文,
|
|
28
|
+
* 归并算法就再也读不到它了。
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** 机器可读块的围栏标记。与卡片的 `json diagnosis` 分开,两种块不会互相误读。 */
|
|
32
|
+
const RUN_FENCE = 'json diagnosis-run'
|
|
33
|
+
|
|
34
|
+
/** 证据节点的标题取自 `result`;太长就截断,完整值仍在节点上。 */
|
|
35
|
+
const EVIDENCE_TITLE_CHARS = 60
|
|
36
|
+
|
|
37
|
+
export interface DiagnosisRunFile {
|
|
38
|
+
record: DiagnosisRunRecord
|
|
39
|
+
/** 正文小节(人读的部分)。由记录派生,不手写第二份。 */
|
|
40
|
+
sections: Array<{ heading: string; body: string }>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 一次排查的记录 → SourceRaw 正文。
|
|
45
|
+
*
|
|
46
|
+
* 正文按「现象 / 走过的路径 / 结论」三段组织:那是人打开这条来源时想看的三件事,
|
|
47
|
+
* 而机器要读的一切都在下面的块里。
|
|
48
|
+
*/
|
|
49
|
+
export function renderRunSource(record: DiagnosisRunRecord): string {
|
|
50
|
+
const path = record.path.length > 0
|
|
51
|
+
? record.path.map(step => `- \`${step.label}\` ${step.note}${step.refuted ? '(否定)' : ''}`).join('\n')
|
|
52
|
+
: '(这次没有记录路径)'
|
|
53
|
+
const evidence = record.evidence.length > 0
|
|
54
|
+
? record.evidence.map(item => `- \`${item.id}\` 跑 \`${item.checkPointId}\`:${item.result} —— ${verdictWord(item.verdict)}${item.why ? `,因为${item.why}` : ''}`).join('\n')
|
|
55
|
+
: '(这次没有产出证据)'
|
|
56
|
+
const conclusion = record.rootCause
|
|
57
|
+
? [`**根因**:${record.rootCause.text}`, record.rootCause.fixAction ? `**修复**:${record.rootCause.fixAction}` : ''].filter(Boolean).join('\n\n')
|
|
58
|
+
: '(这次没有确认根因)'
|
|
59
|
+
const misleading = record.misleading && record.misleading.length > 0
|
|
60
|
+
? `\n\n## 误导段\n\n${record.misleading.map(item => `- ${item.label}:浪费 ${item.wastedMinutes} 分钟。${item.why}`).join('\n')}`
|
|
61
|
+
: ''
|
|
62
|
+
|
|
63
|
+
return [
|
|
64
|
+
'## 现象',
|
|
65
|
+
'',
|
|
66
|
+
`入口现象 \`${record.symptomId}\`,开始于 ${record.startedAt},总耗时 ${record.durationMinutes} 分钟。`,
|
|
67
|
+
'',
|
|
68
|
+
'## 走过的路径',
|
|
69
|
+
'',
|
|
70
|
+
path,
|
|
71
|
+
'',
|
|
72
|
+
'## 每个检查点的产出',
|
|
73
|
+
'',
|
|
74
|
+
evidence,
|
|
75
|
+
'',
|
|
76
|
+
'## 结论',
|
|
77
|
+
'',
|
|
78
|
+
conclusion,
|
|
79
|
+
misleading,
|
|
80
|
+
'',
|
|
81
|
+
'```' + RUN_FENCE,
|
|
82
|
+
JSON.stringify(record, null, 2),
|
|
83
|
+
'```',
|
|
84
|
+
'',
|
|
85
|
+
].join('\n')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function verdictWord(verdict: DiagnosisRunEvidence['verdict']): string {
|
|
89
|
+
if (verdict === 'supports') return '支持'
|
|
90
|
+
if (verdict === 'refutes') return '否定'
|
|
91
|
+
return '给不出结论'
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** SourceRaw 正文 → 记录。块坏了或缺 runId 时返回 undefined(调用方如实报问题,不猜)。 */
|
|
95
|
+
export function parseRunSource(content: string): DiagnosisRunRecord | undefined {
|
|
96
|
+
const match = content.match(new RegExp('```' + RUN_FENCE + '\\s*\\n([\\s\\S]*?)```'))
|
|
97
|
+
if (!match) return undefined
|
|
98
|
+
let value: unknown
|
|
99
|
+
try { value = JSON.parse(match[1]) } catch { return undefined }
|
|
100
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined
|
|
101
|
+
const record = value as Partial<DiagnosisRunRecord>
|
|
102
|
+
if (typeof record.runId !== 'string' || !record.runId.trim()) return undefined
|
|
103
|
+
if (typeof record.symptomId !== 'string' || !record.symptomId.trim()) return undefined
|
|
104
|
+
return {
|
|
105
|
+
runId: record.runId,
|
|
106
|
+
symptomId: record.symptomId,
|
|
107
|
+
startedAt: typeof record.startedAt === 'string' ? record.startedAt : '',
|
|
108
|
+
durationMinutes: typeof record.durationMinutes === 'number' ? record.durationMinutes : 0,
|
|
109
|
+
path: Array.isArray(record.path) ? record.path as CasePathStep[] : [],
|
|
110
|
+
evidence: Array.isArray(record.evidence) ? record.evidence.filter(isEvidence) : [],
|
|
111
|
+
...(record.rootCause && typeof record.rootCause === 'object' ? { rootCause: record.rootCause } : {}),
|
|
112
|
+
...(Array.isArray(record.misleading) ? { misleading: record.misleading } : {}),
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isEvidence(value: unknown): value is DiagnosisRunEvidence {
|
|
117
|
+
if (!value || typeof value !== 'object') return false
|
|
118
|
+
const item = value as Partial<DiagnosisRunEvidence>
|
|
119
|
+
return typeof item.id === 'string' && typeof item.checkPointId === 'string'
|
|
120
|
+
&& (item.verdict === 'supports' || item.verdict === 'refutes' || item.verdict === 'inconclusive')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 根因节点 id:记录里给了就用它,没给就按假设 id 生成一个稳定的。 */
|
|
124
|
+
export function causeIdOf(record: DiagnosisRunRecord): string | undefined {
|
|
125
|
+
if (!record.rootCause) return undefined
|
|
126
|
+
return record.rootCause.id?.trim() || `C-${record.rootCause.hypothesisId}`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 记录 → 实例层节点与四条执行边。
|
|
131
|
+
*
|
|
132
|
+
* **它只产出执行产物**:`evidence` / `cause` 节点与 `produces` / `supports` / `refutes` /
|
|
133
|
+
* `confirmed_as`。类型层(现象、假设、检查点、失败原因)永远来自卡片 —— 把两者混在一起,
|
|
134
|
+
* 一次执行的结果就会被伪装成可复用的知识(§6.7 的边界表)。
|
|
135
|
+
*/
|
|
136
|
+
export function runToGraph(record: DiagnosisRunRecord): { nodes: AttributionNode[]; edges: AttributionEdge[]; problems: string[] } {
|
|
137
|
+
const nodes: AttributionNode[] = []
|
|
138
|
+
const edges: AttributionEdge[] = []
|
|
139
|
+
const problems: string[] = []
|
|
140
|
+
const seen = new Set<string>()
|
|
141
|
+
|
|
142
|
+
for (const item of record.evidence) {
|
|
143
|
+
if (seen.has(item.id)) problems.push(`证据 ${item.id} 在这次排查里出现了两次 —— 同一份产出只能有一个 id`)
|
|
144
|
+
seen.add(item.id)
|
|
145
|
+
const result = item.result ?? ''
|
|
146
|
+
nodes.push({
|
|
147
|
+
id: item.id,
|
|
148
|
+
kind: 'evidence',
|
|
149
|
+
layer: 'instance',
|
|
150
|
+
title: result.length > EVIDENCE_TITLE_CHARS ? `${result.slice(0, EVIDENCE_TITLE_CHARS)}…` : result || item.id,
|
|
151
|
+
summary: item.why,
|
|
152
|
+
query: item.query,
|
|
153
|
+
result,
|
|
154
|
+
verdict: item.verdict,
|
|
155
|
+
why: item.why,
|
|
156
|
+
at: item.at,
|
|
157
|
+
by: item.by,
|
|
158
|
+
...(item.rawRef ? { rawRef: item.rawRef } : {}),
|
|
159
|
+
})
|
|
160
|
+
edges.push({ id: `run-${record.runId}-${item.checkPointId}-${item.id}`, source: item.checkPointId, target: item.id, kind: 'produces' })
|
|
161
|
+
// 给不出结论的证据**不挂支持 / 否定边**:它不是「弱支持」,它是没有结论。
|
|
162
|
+
// 把它挂成 supports 会让最贵的教训(便宜检查给不出结论)从模型里消失。
|
|
163
|
+
if (item.verdict !== 'inconclusive' && item.hypothesisId) {
|
|
164
|
+
edges.push({
|
|
165
|
+
id: `run-${record.runId}-${item.id}-${item.hypothesisId}`,
|
|
166
|
+
source: item.id,
|
|
167
|
+
target: item.hypothesisId,
|
|
168
|
+
kind: item.verdict === 'supports' ? 'supports' : 'refutes',
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const causeId = causeIdOf(record)
|
|
174
|
+
if (record.rootCause && causeId) {
|
|
175
|
+
nodes.push({
|
|
176
|
+
id: causeId,
|
|
177
|
+
kind: 'cause',
|
|
178
|
+
layer: 'instance',
|
|
179
|
+
title: record.rootCause.text,
|
|
180
|
+
...(record.rootCause.fixAction ? { summary: record.rootCause.fixAction } : {}),
|
|
181
|
+
...(record.rootCause.confirmedAt ? { at: record.rootCause.confirmedAt } : {}),
|
|
182
|
+
})
|
|
183
|
+
edges.push({ id: `run-${record.runId}-${record.rootCause.hypothesisId}-${causeId}`, source: record.rootCause.hypothesisId, target: causeId, kind: 'confirmed_as' })
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return { nodes, edges, problems }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 引用完整性:一次排查里的每个 id 都必须在图上真实存在。
|
|
191
|
+
*
|
|
192
|
+
* 缺了这条,一条写错 id 的诊断日志会在图上留下悬空的执行边 —— 而
|
|
193
|
+
* `validateDiagnosis` 只在读图之后才看得到,那时已经分不清是卡片错了还是日志错了。
|
|
194
|
+
*
|
|
195
|
+
* `knownIds` 要**同时包含卡片那部分与这次排查自己产出的那部分**:路径里的 `E1` / `C1`
|
|
196
|
+
* 正是这次排查自己写下的,只拿卡片图去比会把它们全判成悬空。
|
|
197
|
+
*/
|
|
198
|
+
export function runReferenceProblems(record: DiagnosisRunRecord, knownIds: ReadonlySet<string>): string[] {
|
|
199
|
+
const ids = knownIds
|
|
200
|
+
const problems: string[] = []
|
|
201
|
+
if (!ids.has(record.symptomId)) problems.push(`诊断日志 ${record.runId} 的入口现象 ${record.symptomId} 不存在`)
|
|
202
|
+
for (const item of record.evidence) {
|
|
203
|
+
if (!ids.has(item.checkPointId)) problems.push(`诊断日志 ${record.runId} 的证据 ${item.id} 指向不存在的检查点 ${item.checkPointId}`)
|
|
204
|
+
if (item.hypothesisId && !ids.has(item.hypothesisId)) problems.push(`诊断日志 ${record.runId} 的证据 ${item.id} 指向不存在的假设 ${item.hypothesisId}`)
|
|
205
|
+
}
|
|
206
|
+
if (record.rootCause && !ids.has(record.rootCause.hypothesisId)) {
|
|
207
|
+
problems.push(`诊断日志 ${record.runId} 的根因指向不存在的假设 ${record.rootCause.hypothesisId}`)
|
|
208
|
+
}
|
|
209
|
+
for (const step of record.path) {
|
|
210
|
+
if (!ids.has(step.nodeId)) problems.push(`诊断日志 ${record.runId} 的路径节点 ${step.nodeId} 不存在`)
|
|
211
|
+
}
|
|
212
|
+
return problems
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 把一次排查归纳成一张**案例卡片**(还只是提议,要人确认才生效)。
|
|
217
|
+
*
|
|
218
|
+
* 归纳这一步是「过程记录 → 可复用知识」的那一跳,也是唯一需要人看的一跳:
|
|
219
|
+
* 路径里哪几步其实是误导、根因一句话怎么写,都是判断题。
|
|
220
|
+
*/
|
|
221
|
+
export function caseProposalOf(record: DiagnosisRunRecord, symptomTitle: string): DiagnosisCard {
|
|
222
|
+
const rootCause = record.rootCause?.text ?? '(这次没有确认根因)'
|
|
223
|
+
const firstMisleading = record.misleading?.[0]
|
|
224
|
+
return {
|
|
225
|
+
entityId: `CASE-${record.runId}`,
|
|
226
|
+
entityType: 'diagnosis/case',
|
|
227
|
+
title: symptomTitle,
|
|
228
|
+
status: 'unreviewed',
|
|
229
|
+
sourceRefs: [],
|
|
230
|
+
sections: [
|
|
231
|
+
{ heading: '现象', body: `入口现象 \`${record.symptomId}\`(${symptomTitle}),开始于 ${record.startedAt}。` },
|
|
232
|
+
{ heading: '影响面', body: '(待补:这次影响到了谁、范围多大)' },
|
|
233
|
+
{ heading: '排查过程', body: record.path.map(step => `- \`${step.label}\` ${step.note}${step.refuted ? '(否定)' : ''}`).join('\n') || '(没有记录路径)' },
|
|
234
|
+
{ heading: '根因', body: rootCause },
|
|
235
|
+
{ heading: '修复与防复发', body: record.rootCause?.fixAction ?? '(待补:修复动作与防复发措施)' },
|
|
236
|
+
],
|
|
237
|
+
block: {
|
|
238
|
+
symptom: record.symptomId,
|
|
239
|
+
path: record.path,
|
|
240
|
+
duration_minutes: record.durationMinutes,
|
|
241
|
+
...(firstMisleading ? { misleading: firstMisleading } : {}),
|
|
242
|
+
root_cause: rootCause,
|
|
243
|
+
},
|
|
244
|
+
}
|
|
245
|
+
}
|
package/src/diagnosis/schema.ts
CHANGED
|
@@ -16,13 +16,16 @@ import { PROBLEM_CLASS_LABEL, problemClassIds, problemSubclassOf } from './probl
|
|
|
16
16
|
* 序和枚举一旦写进散文,归并算法就再也读不到它们了。
|
|
17
17
|
* 3. **校验必须分三层报错**:格式 / 引用完整性 / 语义。混成同一个「校验失败」,
|
|
18
18
|
* 人就学不会怎么写卡片。
|
|
19
|
+
*
|
|
20
|
+
* 两处更名(0021 v1.1 / v1.2):`check` → **`check_point`**(检查点)、
|
|
21
|
+
* `failure-mode` → **`failure_reason`**(失败原因)。kind 用下划线,卡片类型用连字符。
|
|
19
22
|
*/
|
|
20
23
|
|
|
21
24
|
export type DiagnosisEntityType =
|
|
22
25
|
| 'diagnosis/symptom'
|
|
23
26
|
| 'diagnosis/hypothesis'
|
|
24
|
-
| 'diagnosis/check'
|
|
25
|
-
| 'diagnosis/failure-
|
|
27
|
+
| 'diagnosis/check-point'
|
|
28
|
+
| 'diagnosis/failure-reason'
|
|
26
29
|
| 'diagnosis/case'
|
|
27
30
|
|
|
28
31
|
export interface DiagnosisFieldSpec {
|
|
@@ -53,11 +56,29 @@ export interface DiagnosisEntitySpec {
|
|
|
53
56
|
export const DIAGNOSIS_ENTITY_TYPES: DiagnosisEntityType[] = [
|
|
54
57
|
'diagnosis/symptom',
|
|
55
58
|
'diagnosis/hypothesis',
|
|
56
|
-
'diagnosis/check',
|
|
57
|
-
'diagnosis/failure-
|
|
59
|
+
'diagnosis/check-point',
|
|
60
|
+
'diagnosis/failure-reason',
|
|
58
61
|
'diagnosis/case',
|
|
59
62
|
]
|
|
60
63
|
|
|
64
|
+
/**
|
|
65
|
+
* 改名之前的两个 `entity_type`,以及它们对应的新名字。
|
|
66
|
+
*
|
|
67
|
+
* 留着这张表是为了**存量卡片不静默消失**:磁盘上还叫 `diagnosis/check` 的卡片,
|
|
68
|
+
* 如果读的时候直接判为「未知类型」,后果是这张卡片既不进 wikiNotes、也不上图,
|
|
69
|
+
* 而且没有任何报错。所以读取端按别名认,写入端做一次性迁移(见
|
|
70
|
+
* `runtime.migrateLegacyDiagnosisCards`)。
|
|
71
|
+
*/
|
|
72
|
+
export const LEGACY_ENTITY_TYPES: Record<string, DiagnosisEntityType> = {
|
|
73
|
+
'diagnosis/check': 'diagnosis/check-point',
|
|
74
|
+
'diagnosis/failure-mode': 'diagnosis/failure-reason',
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 卡片里那个旧字段名 → 新字段名。`failure_mode` 改叫 `failure_reason`。 */
|
|
78
|
+
export const LEGACY_BLOCK_FIELDS: Record<string, string> = {
|
|
79
|
+
failure_mode: 'failure_reason',
|
|
80
|
+
}
|
|
81
|
+
|
|
61
82
|
/** 不是 wiki 卡片、但仍属于同一张图的节点类型。校验不按卡片规则要求它们。 */
|
|
62
83
|
export const NON_CARD_KINDS: AttributionNodeKind[] = ['asset', 'evidence', 'cause', 'unknown']
|
|
63
84
|
|
|
@@ -76,11 +97,11 @@ export const DIAGNOSIS_ENTITIES: DiagnosisEntitySpec[] = [
|
|
|
76
97
|
entityType: 'diagnosis/symptom',
|
|
77
98
|
kind: 'symptom',
|
|
78
99
|
layer: 'type',
|
|
79
|
-
label: '
|
|
100
|
+
label: '现象',
|
|
80
101
|
fields: [
|
|
81
102
|
{ name: 'observable', note: '可观测口径:指标 + 维度 + 时间窗' },
|
|
82
103
|
{ name: 'entry_when', note: '什么条件下适用' },
|
|
83
|
-
{ name: 'problem_class', note: '
|
|
104
|
+
{ name: 'problem_class', note: '问题大类:必填,取值只能来自 §4.5 的四个大类' },
|
|
84
105
|
{ name: 'problem_subclass', note: '问题子类:必须属于所选大类;它同时决定 §5.1 的 shape / time_pattern' },
|
|
85
106
|
],
|
|
86
107
|
sections: ['观测口径', '典型信号', '不适用的情况', '关联假设'],
|
|
@@ -93,34 +114,35 @@ export const DIAGNOSIS_ENTITIES: DiagnosisEntitySpec[] = [
|
|
|
93
114
|
fields: [
|
|
94
115
|
{ name: 'discriminator', note: '**一句可判真假的话** —— 它同时是查重的比对对象' },
|
|
95
116
|
{ name: 'not_when', note: '**反例边界**:什么情况下这个假设不成立。refutes 边在文档层的表达' },
|
|
96
|
-
{ name: '
|
|
97
|
-
{ name: 'verified_by', note: '
|
|
98
|
-
{ name: 'assets', note: '
|
|
117
|
+
{ name: 'failure_reason', note: '切面:upstream-missing | spec-change | data-processing | consumption | permission' },
|
|
118
|
+
{ name: 'verified_by', note: '引用的检查点 id 列表' },
|
|
119
|
+
{ name: 'assets', note: '这条假设**落在**哪些对象上(反向索引入口)。可写成 `"dwd_ord_di"`,或 `{ id, type }` 顺带声明种类' },
|
|
99
120
|
{ name: 'stats', system: true, note: 'prior / last_hit_at / refutes_count / case_count' },
|
|
100
121
|
],
|
|
101
122
|
sections: ['判据', '反例边界', '典型信号', '关联检查', '案例索引'],
|
|
102
123
|
},
|
|
103
124
|
{
|
|
104
|
-
entityType: 'diagnosis/check',
|
|
105
|
-
kind: '
|
|
125
|
+
entityType: 'diagnosis/check-point',
|
|
126
|
+
kind: 'check_point',
|
|
106
127
|
layer: 'type',
|
|
107
|
-
label: '
|
|
128
|
+
label: '检查点',
|
|
108
129
|
fields: [
|
|
109
130
|
{ name: 'cost_minutes', note: '耗时(分钟),优先分的分母' },
|
|
110
131
|
{ name: 'automation', note: 'auto | manual' },
|
|
111
132
|
{ name: 'owner', note: '执行人或角色' },
|
|
112
133
|
{ name: 'verdict_true', note: '**看到什么算真**' },
|
|
113
134
|
{ name: 'verdict_false', note: '**看到什么算假** —— 只写「算真」就无法否定假设' },
|
|
114
|
-
{ name: 'runbook', note: '怎么跑:SQL / 工具 /
|
|
135
|
+
{ name: 'runbook', note: '怎么跑:SQL / 工具 / 找谁。**可复用的查询模板**写这里' },
|
|
115
136
|
{ name: 'side_effect', note: '是否有副作用(只读 / 会写)' },
|
|
137
|
+
{ name: 'assets', note: '**查询依据**:这条检查要去查哪些对象(表 / 指标 / 任务 / 链路)。可写成 `"dwd_ord_di"`,或 `{ id, type }` 顺带声明种类' },
|
|
116
138
|
],
|
|
117
139
|
sections: ['怎么跑', '判定阈值', '有结论与无结论的区分'],
|
|
118
140
|
},
|
|
119
141
|
{
|
|
120
|
-
entityType: 'diagnosis/failure-
|
|
121
|
-
kind: '
|
|
142
|
+
entityType: 'diagnosis/failure-reason',
|
|
143
|
+
kind: 'failure_reason',
|
|
122
144
|
layer: 'type',
|
|
123
|
-
label: '
|
|
145
|
+
label: '失败原因',
|
|
124
146
|
fields: [
|
|
125
147
|
{ name: 'code', note: '稳定编码' },
|
|
126
148
|
{ name: 'label', note: '界面词' },
|
|
@@ -135,7 +157,7 @@ export const DIAGNOSIS_ENTITIES: DiagnosisEntitySpec[] = [
|
|
|
135
157
|
label: '案例',
|
|
136
158
|
fields: [
|
|
137
159
|
{ name: 'symptom', note: '入口现象 id' },
|
|
138
|
-
{ name: 'path', note: '**有序**节点序列:现象 → 假设 →
|
|
160
|
+
{ name: 'path', note: '**有序**节点序列:现象 → 假设 → 检查点 → 证据 → 根因' },
|
|
139
161
|
{ name: 'duration_minutes', note: '总耗时' },
|
|
140
162
|
{ name: 'misleading_edges', note: '误导边:节点 + 浪费的分钟数 + 为什么' },
|
|
141
163
|
{ name: 'root_cause', note: '根因一句话' },
|
|
@@ -146,7 +168,7 @@ export const DIAGNOSIS_ENTITIES: DiagnosisEntitySpec[] = [
|
|
|
146
168
|
},
|
|
147
169
|
]
|
|
148
170
|
|
|
149
|
-
/**
|
|
171
|
+
/** 语义校验用的判据:检查点的标题必须**可执行**。 */
|
|
150
172
|
const VAGUE_CHECK_TITLES = /^(检查|看看|排查|确认)?\s*(一下)?\s*(数据质量|是不是|有没有问题|上游|下游)/
|
|
151
173
|
|
|
152
174
|
export type DiagnosisIssueLevel = 'format' | 'reference' | 'semantic'
|
|
@@ -171,6 +193,9 @@ export function specOf(kind: AttributionNodeKind): DiagnosisEntitySpec | undefin
|
|
|
171
193
|
*
|
|
172
194
|
* 分开报错不是洁癖:**格式错说明卡片没填完,引用错说明图断了,语义错说明这条知识
|
|
173
195
|
* 本身没用**。三者对应的动作完全不同(补字段 / 修边 / 重写),混在一起就只能靠猜。
|
|
196
|
+
*
|
|
197
|
+
* 它同时是**提议阶段与确认阶段的前置**(0021 §6.5):两道都跑,是因为这两次修改
|
|
198
|
+
* 来自不同的人。
|
|
174
199
|
*/
|
|
175
200
|
export function validateDiagnosis(graph: AttributionGraphData): DiagnosisIssue[] {
|
|
176
201
|
const issues: DiagnosisIssue[] = []
|
|
@@ -185,7 +210,7 @@ export function validateDiagnosis(graph: AttributionGraphData): DiagnosisIssue[]
|
|
|
185
210
|
if (node.kind === 'hypothesis') {
|
|
186
211
|
if (!node.discriminator?.trim()) issues.push(fieldIssue(node, 'discriminator', '假设必须有一句可判真假的判据'))
|
|
187
212
|
if (!node.notWhen?.trim()) issues.push(fieldIssue(node, 'notWhen', '假设必须写明反例边界 —— 否则查重会把相似情形并成一条'))
|
|
188
|
-
if (!node.
|
|
213
|
+
if (!node.failureReason) issues.push(fieldIssue(node, 'failureReason', '假设必须归属一个失败原因切面'))
|
|
189
214
|
}
|
|
190
215
|
if (node.kind === 'symptom') {
|
|
191
216
|
// 问题侧的分类是**必填**:留空的后果不是「少个字段」,而是这个现象在按问题筛选时
|
|
@@ -207,12 +232,22 @@ export function validateDiagnosis(graph: AttributionGraphData): DiagnosisIssue[]
|
|
|
207
232
|
// 分类字段只属于现象。假设上挂问题大类,会把「问题」和「原因」两根轴搅在一起。
|
|
208
233
|
issues.push(fieldIssue(node, 'problemClass', '问题分类只能写在现象(symptom)上,不能挂在原因侧'))
|
|
209
234
|
}
|
|
210
|
-
if (node.kind === '
|
|
235
|
+
if (node.kind === 'check_point') {
|
|
211
236
|
for (const [field, value] of [['costMinutes', node.costMinutes], ['automation', node.automation], ['owner', node.owner], ['verdictTrue', node.verdictTrue], ['verdictFalse', node.verdictFalse]] as const) {
|
|
212
|
-
if (value === undefined || value === '') issues.push(fieldIssue(node, field,
|
|
237
|
+
if (value === undefined || value === '') issues.push(fieldIssue(node, field, `检查点缺少 ${field} —— 缺了它这一步就无法被自动化或复核`))
|
|
213
238
|
}
|
|
239
|
+
// `assets[]` 是 v1.3 的必填:缺了它,这条检查**不知道去哪查**,
|
|
240
|
+
// 也就没法生成查询、判断影响面。
|
|
241
|
+
if (!node.assets || node.assets.length === 0) {
|
|
242
|
+
issues.push(fieldIssue(node, 'assets', '检查点必须写明查询依据(assets[])—— 缺了它这条检查不知道去哪查'))
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (node.kind === 'evidence') {
|
|
246
|
+
if (!node.query?.trim()) issues.push(fieldIssue(node, 'query', '证据必须记下这次实际跑的查询'))
|
|
247
|
+
if (!node.verdict) issues.push(fieldIssue(node, 'verdict', '证据必须给出结论:supports / refutes / inconclusive'))
|
|
248
|
+
if (!node.why?.trim()) issues.push(fieldIssue(node, 'why', '证据必须说明「为什么这个结果算数或不算数」—— 采信与剔除都得能说理由'))
|
|
214
249
|
}
|
|
215
|
-
// `case`
|
|
250
|
+
// `case` 节点只是图上的桩:案例的正文与路径住在案例卡片里,由
|
|
216
251
|
// DiagnosisSnapshot.cases 校验,不在节点上重复要求。
|
|
217
252
|
}
|
|
218
253
|
|
|
@@ -225,18 +260,18 @@ export function validateDiagnosis(graph: AttributionGraphData): DiagnosisIssue[]
|
|
|
225
260
|
if (node.kind !== 'hypothesis') continue
|
|
226
261
|
const hasCheck = graph.edges.some(edge => edge.kind === 'verified_by' && edge.source === node.id)
|
|
227
262
|
// 空区不是错,是**待办**:它是一条信息(这张图哪里还没建完),所以只提示不报错。
|
|
228
|
-
if (!hasCheck) issues.push({ level: 'semantic', entityId: node.id, message: '
|
|
263
|
+
if (!hasCheck) issues.push({ level: 'semantic', entityId: node.id, message: '空区:有假设但没有任何检查点,拿不出成本,因此进不了排序' })
|
|
229
264
|
}
|
|
230
265
|
|
|
231
266
|
// ── 第三层:语义 ──────────────────────────────────────────────────────────
|
|
232
267
|
for (const node of graph.nodes) {
|
|
233
|
-
if (node.kind !== '
|
|
268
|
+
if (node.kind !== 'check_point') continue
|
|
234
269
|
if (VAGUE_CHECK_TITLES.test(node.title.trim())) {
|
|
235
270
|
issues.push({
|
|
236
271
|
level: 'semantic',
|
|
237
272
|
entityId: node.id,
|
|
238
273
|
field: 'title',
|
|
239
|
-
message:
|
|
274
|
+
message: `检查点标题不可执行:「${node.title}」。必须能回答「谁、用什么、多久、看到什么算真、看到什么算假」`,
|
|
240
275
|
})
|
|
241
276
|
}
|
|
242
277
|
}
|
|
@@ -244,7 +279,13 @@ export function validateDiagnosis(graph: AttributionGraphData): DiagnosisIssue[]
|
|
|
244
279
|
return issues
|
|
245
280
|
}
|
|
246
281
|
|
|
247
|
-
/**
|
|
282
|
+
/**
|
|
283
|
+
* 空的模板卡:新人写卡片时看的是它,不是 schema。
|
|
284
|
+
*
|
|
285
|
+
* 模板卡**由类型定义派生、不再落成磁盘文件**(0021 §6.6):模板是类型定义的一种
|
|
286
|
+
* 呈现,定义搬进设置,模板跟着走 —— 否则会出现「设置里改了、磁盘上那份没改」的
|
|
287
|
+
* 第二真源。
|
|
288
|
+
*/
|
|
248
289
|
export function templateCard(spec: DiagnosisEntitySpec): string {
|
|
249
290
|
const front = [
|
|
250
291
|
`entity_type: ${spec.entityType}`,
|
|
@@ -12,14 +12,22 @@
|
|
|
12
12
|
* 手册与复盘是**人写的散文**(就是下面这些字面量),卡片是从它们编译出来的结构化提取。
|
|
13
13
|
* 两者内容会有重叠 —— 那是正常的:卡片是散文的投影,不是它的替代品。
|
|
14
14
|
*/
|
|
15
|
-
import { SEED_CASES } from './seed.js'
|
|
15
|
+
import { SEED_CASES, SEED_RUNS } from './seed.js'
|
|
16
|
+
import { renderRunSource } from './run.js'
|
|
16
17
|
|
|
17
18
|
export interface SeedSource {
|
|
18
19
|
/** 稳定 id,用作 `external_id`,迁移与重跑都靠它幂等。 */
|
|
19
20
|
id: string
|
|
20
21
|
title: string
|
|
21
|
-
/**
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* 来源分类。**两种**:
|
|
24
|
+
* - `question`:人写的手册与复盘(卡片 `source_refs` 指向这里);
|
|
25
|
+
* - `agent_diagnosis_case`:Agent 跑出来的三次排查记录(v1.1 新增的来源分类)。
|
|
26
|
+
*
|
|
27
|
+
* 边界裁决是「谁写的、是不是已经归纳过」:同一件事可以有两条来源,
|
|
28
|
+
* 一个回答「结论是什么」,一个回答「当时怎么走的」。
|
|
29
|
+
*/
|
|
30
|
+
sourceType: 'question' | 'agent_diagnosis_case'
|
|
23
31
|
content: string
|
|
24
32
|
}
|
|
25
33
|
|
|
@@ -99,9 +107,26 @@ export function seedCaseSources(): SeedSource[] {
|
|
|
99
107
|
}))
|
|
100
108
|
}
|
|
101
109
|
|
|
110
|
+
/**
|
|
111
|
+
* 三次排查的**过程记录**。
|
|
112
|
+
*
|
|
113
|
+
* 它们不由散文手写,而是从 `SEED_RUNS` **渲染**出来(`renderRunSource`)——
|
|
114
|
+
* 这正是运行时那条路径:一次排查结束 → 一条 `agent_diagnosis_case` 的 SourceRaw。
|
|
115
|
+
* 示例走同一条路,所以它同时也是这条路的回归夹具。
|
|
116
|
+
*/
|
|
117
|
+
export function seedRunSources(): SeedSource[] {
|
|
118
|
+
return SEED_RUNS.map(record => ({
|
|
119
|
+
id: record.runId,
|
|
120
|
+
title: `AI 诊断日志 · ${record.symptomId} · ${record.runId}`,
|
|
121
|
+
sourceType: 'agent_diagnosis_case' as const,
|
|
122
|
+
content: renderRunSource(record),
|
|
123
|
+
}))
|
|
124
|
+
}
|
|
125
|
+
|
|
102
126
|
export function seedSources(): SeedSource[] {
|
|
103
127
|
return [
|
|
104
128
|
{ id: 'handbook', title: '日报指标异常排查手册(内部版)', sourceType: 'question', content: SEED_HANDBOOK },
|
|
105
129
|
...seedCaseSources(),
|
|
130
|
+
...seedRunSources(),
|
|
106
131
|
]
|
|
107
132
|
}
|