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,357 @@
|
|
|
1
|
+
import { DIAGNOSIS_ENTITIES } from './diagnosis/schema.js'
|
|
2
|
+
import type { CardSchemaIssue, WikiFieldSpec, WikiSectionSpec, WikiTypeDef } from './types.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Wiki 卡片类型的出厂定义与校验(issue 0024)。
|
|
6
|
+
*
|
|
7
|
+
* 这一层回答的是"**SourceRaw → Wiki** 能生成什么、每类长什么样"。它不是生成器:
|
|
8
|
+
* 生成仍由模型完成(`write_knowledge_card`),这里提供的是**判据 + 骨架 + 校验**。
|
|
9
|
+
*
|
|
10
|
+
* 三个设计判断:
|
|
11
|
+
*
|
|
12
|
+
* 1. **schema 就是提示词。** 类型定义、示例、必填小节都随 `write_knowledge_card` 的工具
|
|
13
|
+
* 描述下发(与 0023 同一机制),所以用户改完类型下一轮模型就看到新规则。
|
|
14
|
+
* 2. **模板决定这套 schema 能不能被用起来**(0021 的判断)。`template` 由 `sections` 派生,
|
|
15
|
+
* 不手写第二份,否则又是一处"校验通过但人照着抄还是错"。
|
|
16
|
+
* 3. **校验分三层**:格式 / 引用 / 语义。混成同一个"校验失败",人就学不会怎么写卡片。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** 出厂 wiki 类型:6 个通用类型 + diagnosis 的 5 个类型族。 */
|
|
20
|
+
const GENERIC_TYPES: Array<Omit<WikiTypeDef, 'template' | 'enabled' | 'builtin' | 'cardCount'>> = [
|
|
21
|
+
{
|
|
22
|
+
id: 'concept',
|
|
23
|
+
label: '概念',
|
|
24
|
+
description: '一个业务或技术概念的定义与边界,回答「这个词在我們这儿指什么」。',
|
|
25
|
+
layer: 'type',
|
|
26
|
+
sections: [
|
|
27
|
+
{ label: '定义', required: true },
|
|
28
|
+
{ label: '边界与反例', required: true },
|
|
29
|
+
{ label: '关联', required: false },
|
|
30
|
+
],
|
|
31
|
+
fields: [{ name: 'owner', required: false, note: '谁维护这个概念的定义' }],
|
|
32
|
+
sourceTypes: [],
|
|
33
|
+
examples: ['什么是活跃用户', '「有效订单」的口径是什么', '这个指标和那个指标什么关系'],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'metric',
|
|
37
|
+
label: '指标',
|
|
38
|
+
description: '一个可计算、可归属的量化口径,回答「这个数是怎么算出来的」。',
|
|
39
|
+
layer: 'type',
|
|
40
|
+
sections: [
|
|
41
|
+
{ label: '口径定义', required: true },
|
|
42
|
+
{ label: '计算逻辑', required: true },
|
|
43
|
+
{ label: '负责人', required: false },
|
|
44
|
+
],
|
|
45
|
+
fields: [
|
|
46
|
+
{ name: 'formula', required: true, note: '可执行的表达式或伪代码' },
|
|
47
|
+
{ name: 'owner', required: true, note: '谁对这个口径负责' },
|
|
48
|
+
],
|
|
49
|
+
sourceTypes: ['document', 'prd'],
|
|
50
|
+
examples: ['上个月的 GMV 怎么算', '转化率的口径是什么', '这个指标怎么拆'],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'table',
|
|
54
|
+
label: '数据表',
|
|
55
|
+
description: '一张有明确语义、粒度和归属的数据表。',
|
|
56
|
+
layer: 'type',
|
|
57
|
+
sections: [
|
|
58
|
+
{ label: '用途', required: true },
|
|
59
|
+
{ label: '字段与口径', required: true },
|
|
60
|
+
{ label: '更新与负责人', required: false },
|
|
61
|
+
],
|
|
62
|
+
fields: [
|
|
63
|
+
{ name: 'owner', required: true, note: '表责任人' },
|
|
64
|
+
{ name: 'grain', required: true, note: '一行代表什么(粒度)' },
|
|
65
|
+
],
|
|
66
|
+
sourceTypes: ['document'],
|
|
67
|
+
examples: ['订单表有哪些字段', '这张表的粒度是什么', '这张表多久更新一次'],
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: 'event',
|
|
71
|
+
label: '事件',
|
|
72
|
+
description: '一个会改变业务状态、需要被记录和响应的事实。',
|
|
73
|
+
layer: 'instance',
|
|
74
|
+
sections: [
|
|
75
|
+
{ label: '触发条件', required: true },
|
|
76
|
+
{ label: '影响', required: true },
|
|
77
|
+
{ label: '处理方式', required: true },
|
|
78
|
+
],
|
|
79
|
+
fields: [{ name: 'severity', required: true, note: '影响级别' }],
|
|
80
|
+
sourceTypes: ['chat_message', 'question'],
|
|
81
|
+
examples: ['上游延迟告警怎么处理', '这次大促爆单是怎么回事'],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: 'playbook',
|
|
85
|
+
label: '手册',
|
|
86
|
+
description: '一类问题的标准做法,回答「该怎么做」而不是「做过什么」。',
|
|
87
|
+
layer: 'type',
|
|
88
|
+
sections: [
|
|
89
|
+
{ label: '适用场景', required: true },
|
|
90
|
+
{ label: '步骤', required: true },
|
|
91
|
+
{ label: '验收与失败处理', required: false },
|
|
92
|
+
],
|
|
93
|
+
fields: [{ name: 'owner', required: true, note: '手册维护人' }],
|
|
94
|
+
sourceTypes: ['document', 'prd'],
|
|
95
|
+
examples: ['新人怎么接入一张表', '发布前要检查什么'],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: 'case',
|
|
99
|
+
label: '案例',
|
|
100
|
+
description: '一件真实发生过的事:背景、经过、结果与教训。',
|
|
101
|
+
layer: 'instance',
|
|
102
|
+
sections: [
|
|
103
|
+
{ label: '背景', required: true },
|
|
104
|
+
{ label: '经过', required: true },
|
|
105
|
+
{ label: '结果与教训', required: true },
|
|
106
|
+
],
|
|
107
|
+
fields: [{ name: 'occurred_at', required: true, note: '发生时间' }],
|
|
108
|
+
sourceTypes: ['chat_message', 'question'],
|
|
109
|
+
examples: ['上次数据延迟是怎么定位的', '那次口径改动的来龙去脉'],
|
|
110
|
+
},
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
/** diagnosis 五类的说明;字段与小节直接取自 0021 的 schema,不复制第二份。 */
|
|
114
|
+
const DIAGNOSIS_DESCRIPTIONS: Record<string, string> = {
|
|
115
|
+
'diagnosis/symptom': '一类可复用的现象:可观测口径 + 进入条件,是排查的入口。',
|
|
116
|
+
'diagnosis/hypothesis': '一条可判真假的假设,带反例边界,是排查的中间层。',
|
|
117
|
+
'diagnosis/check': '一步可执行的检查,回答「怎么验证一个假设」。',
|
|
118
|
+
'diagnosis/failure-mode': '失败模式切面:把假设按根因类别分组,用于排序与查重。',
|
|
119
|
+
'diagnosis/case': '一次真实排查的完整路径,是实例层的记录而不是可复用知识。',
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const DIAGNOSIS_EXAMPLES: Record<string, string[]> = {
|
|
123
|
+
'diagnosis/symptom': ['销售额突然掉了', '今天报表数对不上'],
|
|
124
|
+
'diagnosis/hypothesis': ['上游同步漏了', '口径被人改过'],
|
|
125
|
+
'diagnosis/check': ['查一下上游任务状态', '比对两天的分区行数'],
|
|
126
|
+
'diagnosis/failure-mode': ['上游缺失', '口径变更', '数据处理'],
|
|
127
|
+
'diagnosis/case': ['上次大促数据延迟的排查过程'],
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** 由小节派生模板卡。**没有第二份手写模板**:改了 sections,模板跟着变。 */
|
|
131
|
+
export function templateCardFor(sections: WikiSectionSpec[], fields: WikiFieldSpec[] = []): string {
|
|
132
|
+
const required = sections.filter(section => section.required).map(section => section.label)
|
|
133
|
+
const head = fields.filter(field => field.required).map(field => `> 必填字段:\`${field.name}\`(${field.note})`)
|
|
134
|
+
const body = sections.map(section => `## ${section.label}\n\n(${section.required ? '必填' : '可选'}:写什么)\n`).join('\n')
|
|
135
|
+
const summary = required.length > 0 ? `<!-- 必填小节:${required.join(' / ')} -->` : ''
|
|
136
|
+
return [summary, ...head, '', body].filter(Boolean).join('\n')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function make(def: Omit<WikiTypeDef, 'template' | 'enabled' | 'builtin' | 'cardCount'>): WikiTypeDef {
|
|
140
|
+
const sections: WikiSectionSpec[] = def.sections.map(section => ({ ...section }))
|
|
141
|
+
const fields: WikiFieldSpec[] = def.fields.map(field => ({ ...field }))
|
|
142
|
+
return { ...def, sections, fields, sourceTypes: [...def.sourceTypes], examples: [...def.examples], template: templateCardFor(sections, fields), enabled: true, builtin: true, cardCount: 0 }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 出厂注册表:6 个通用类型 + diagnosis 的 5 个类型族。
|
|
147
|
+
*
|
|
148
|
+
* diagnosis 的部分**从 `diagnosis/schema.ts` 取**——0021 明确要求那张清单只有一个定义处,
|
|
149
|
+
* 抄第二份的下场是"校验通过了但编译器不认"。
|
|
150
|
+
*/
|
|
151
|
+
export const DEFAULT_WIKI_TYPES: WikiTypeDef[] = [
|
|
152
|
+
...GENERIC_TYPES.map(make),
|
|
153
|
+
...DIAGNOSIS_ENTITIES.map(spec => make({
|
|
154
|
+
id: spec.entityType,
|
|
155
|
+
label: spec.label,
|
|
156
|
+
description: DIAGNOSIS_DESCRIPTIONS[spec.entityType] ?? spec.label,
|
|
157
|
+
layer: spec.layer === 'instance' ? 'instance' : 'type',
|
|
158
|
+
sections: spec.sections.map(label => ({ label, required: true })),
|
|
159
|
+
fields: spec.fields.map(field => ({ name: field.name, required: field.system !== true, note: field.note })),
|
|
160
|
+
sourceTypes: [],
|
|
161
|
+
examples: DIAGNOSIS_EXAMPLES[spec.entityType] ?? [],
|
|
162
|
+
})),
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
const BUILTIN_IDS: ReadonlySet<string> = new Set(DEFAULT_WIKI_TYPES.map(def => def.id))
|
|
166
|
+
|
|
167
|
+
export function isBuiltinWikiType(id: string): boolean {
|
|
168
|
+
return BUILTIN_IDS.has(id)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** 出厂注册表副本(全部启用,`cardCount` 待算)。 */
|
|
172
|
+
export function defaultWikiTypes(): WikiTypeDef[] {
|
|
173
|
+
return DEFAULT_WIKI_TYPES.map(def => ({
|
|
174
|
+
...def,
|
|
175
|
+
sections: def.sections.map(section => ({ ...section })),
|
|
176
|
+
fields: def.fields.map(field => ({ ...field })),
|
|
177
|
+
sourceTypes: [...def.sourceTypes],
|
|
178
|
+
examples: [...def.examples],
|
|
179
|
+
cardCount: 0,
|
|
180
|
+
}))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 内置只读的判定规则:模型该按什么顺序挑类型。
|
|
185
|
+
*
|
|
186
|
+
* 与 0013 的 `classifierPrompt` 同构——**不允许修改,但必须暴露**。
|
|
187
|
+
*/
|
|
188
|
+
export const WIKI_TYPE_RULES = [
|
|
189
|
+
'先按每个类型的「定义」与「例如」判断这张卡片属于哪一类;命中就直接用它,不要新起一个近义的类型。',
|
|
190
|
+
'所有类型都不命中时,才按语义新起一个类型 id(小写英文,可带 `命名空间/名字`),并在知识库设置里把它登记成正式类型。',
|
|
191
|
+
'一张卡片只讲一件事:概念讲定义、指标讲口径、手册讲做法、案例讲发生过的事。混在一起时要拆成多张卡片。',
|
|
192
|
+
'卡片必须引用真实存在的 SourceRaw:`source_refs` 写了不存在的来源会被直接拒绝。',
|
|
193
|
+
'必填小节与必填字段来自类型定义;缺了它们卡片仍然可写(report 模式),但会被标成待补。',
|
|
194
|
+
].join('\n')
|
|
195
|
+
|
|
196
|
+
/** 新增 / 编辑类型时提交的草稿。 */
|
|
197
|
+
export interface WikiTypeDraft {
|
|
198
|
+
id?: string
|
|
199
|
+
label?: string
|
|
200
|
+
description?: string
|
|
201
|
+
layer?: string
|
|
202
|
+
sections?: unknown
|
|
203
|
+
fields?: unknown
|
|
204
|
+
sourceTypes?: unknown
|
|
205
|
+
examples?: unknown
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export interface WikiTypeDraftProblem {
|
|
209
|
+
ok: false
|
|
210
|
+
code: string
|
|
211
|
+
message: string
|
|
212
|
+
suggestion?: string
|
|
213
|
+
field: 'id' | 'label' | 'description' | 'sections' | 'fields'
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** 解析「每行一条」的小节草稿:`名称` 或 `名称*`(`*` = 必填)。 */
|
|
217
|
+
export function parseSectionLines(value: unknown): WikiSectionSpec[] {
|
|
218
|
+
if (!Array.isArray(value)) return []
|
|
219
|
+
return value.flatMap((item): WikiSectionSpec[] => {
|
|
220
|
+
if (typeof item === 'object' && item !== null) {
|
|
221
|
+
const record = item as Record<string, unknown>
|
|
222
|
+
const label = typeof record.label === 'string' ? record.label.trim() : ''
|
|
223
|
+
return label ? [{ label, required: record.required !== false }] : []
|
|
224
|
+
}
|
|
225
|
+
const text = String(item).trim()
|
|
226
|
+
if (!text) return []
|
|
227
|
+
const required = !text.endsWith('?')
|
|
228
|
+
return [{ label: required ? text : text.slice(0, -1).trim(), required }]
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** 解析字段草稿:`name` / `name|说明` / `name|说明|可选`。 */
|
|
233
|
+
export function parseFieldLines(value: unknown): WikiFieldSpec[] {
|
|
234
|
+
if (!Array.isArray(value)) return []
|
|
235
|
+
return value.flatMap((item): WikiFieldSpec[] => {
|
|
236
|
+
// 已经是对象形态(整表 round-trip / 编辑已有类型)时按字段取,不要再 stringify 成 [object Object]。
|
|
237
|
+
if (typeof item === 'object' && item !== null) {
|
|
238
|
+
const record = item as Record<string, unknown>
|
|
239
|
+
const name = typeof record.name === 'string' ? record.name.trim() : ''
|
|
240
|
+
if (!name) return []
|
|
241
|
+
return [{ name, note: typeof record.note === 'string' ? record.note : '', required: record.required !== false }]
|
|
242
|
+
}
|
|
243
|
+
const text = String(item).trim()
|
|
244
|
+
if (!text) return []
|
|
245
|
+
const [rawName, rawNote = '', rawOptional = ''] = text.split('|').map(part => part.trim())
|
|
246
|
+
if (!rawName) return []
|
|
247
|
+
return [{ name: rawName, note: rawNote, required: rawOptional !== '可选' && rawOptional !== 'optional' }]
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* 校验一份类型草稿。
|
|
253
|
+
*
|
|
254
|
+
* 与 0013 的 source type 一样:光给个名字不让入库——入库的 `sections` / `fields` 会直接
|
|
255
|
+
* 变成"卡片该怎么写"的约束。
|
|
256
|
+
*/
|
|
257
|
+
export function validateWikiTypeDraft(draft: WikiTypeDraft, knownIds: ReadonlySet<string>): { ok: true } | WikiTypeDraftProblem {
|
|
258
|
+
const id = typeof draft.id === 'string' ? draft.id.trim() : ''
|
|
259
|
+
if (!/^[a-z][a-z0-9_/-]{1,60}$/.test(id)) {
|
|
260
|
+
return { ok: false, code: 'WIKI_TYPE_ID_INVALID', field: 'id', message: '类型 id 必须是小写字母开头的 2–61 位小写字母 / 数字 / 下划线 / 连字符 / 斜杠', suggestion: '例如 metric、diagnosis/symptom;id 创建后不可修改。' }
|
|
261
|
+
}
|
|
262
|
+
if (isBuiltinWikiType(id)) {
|
|
263
|
+
return { ok: false, code: 'WIKI_TYPE_BUILTIN_LOCKED', field: 'id', message: `内置类型 id 不可占用: ${id}`, suggestion: '内置类型可以改文案、停用,但不能删;新场景请另起 id。' }
|
|
264
|
+
}
|
|
265
|
+
void knownIds
|
|
266
|
+
return validateWikiTypeText(draft)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** 只校验文案与小节 / 字段,不碰 id(内置类型改内容走这里)。 */
|
|
270
|
+
export function validateWikiTypeText(draft: WikiTypeDraft): { ok: true } | WikiTypeDraftProblem {
|
|
271
|
+
const label = typeof draft.label === 'string' ? draft.label.trim() : ''
|
|
272
|
+
if (!label) return { ok: false, code: 'WIKI_TYPE_LABEL_REQUIRED', field: 'label', message: 'label 不能为空' }
|
|
273
|
+
const description = typeof draft.description === 'string' ? draft.description.trim() : ''
|
|
274
|
+
if (description.length < 8) {
|
|
275
|
+
return { ok: false, code: 'WIKI_TYPE_DESCRIPTION_REQUIRED', field: 'description', message: 'description 必填,且要写清这个类型「是什么」', suggestion: '一句话定义,至少 8 个字;它是模型判断卡片类型的主要依据。' }
|
|
276
|
+
}
|
|
277
|
+
const sections = parseSectionLines(draft.sections)
|
|
278
|
+
if (sections.length === 0) {
|
|
279
|
+
return { ok: false, code: 'WIKI_TYPE_SECTIONS_REQUIRED', field: 'sections', message: '至少写一个小节', suggestion: '小节是卡片的骨架,也是模板卡与校验的依据;一行一个,例如「口径定义」。' }
|
|
280
|
+
}
|
|
281
|
+
if (!sections.some(section => section.required)) {
|
|
282
|
+
return { ok: false, code: 'WIKI_TYPE_SECTION_REQUIRED_MISSING', field: 'sections', message: '至少要有一个必填小节', suggestion: '没有必填小节,这张卡片就没有可校验的骨架。' }
|
|
283
|
+
}
|
|
284
|
+
const fields = parseFieldLines(draft.fields)
|
|
285
|
+
const seenNames = new Set<string>()
|
|
286
|
+
for (const field of fields) {
|
|
287
|
+
if (seenNames.has(field.name)) return { ok: false, code: 'WIKI_TYPE_FIELD_DUPLICATE', field: 'fields', message: `字段名重复: ${field.name}` }
|
|
288
|
+
seenNames.add(field.name)
|
|
289
|
+
}
|
|
290
|
+
return { ok: true }
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const PLACEHOLDER = /(TODO|待补充|待填|xxx|XXX|写什么)/
|
|
294
|
+
|
|
295
|
+
/** 卡片正文里找小节标题:`## 小节名` 或 `### 小节名`(标题后可有补充文字)。 */
|
|
296
|
+
function hasSection(content: string, label: string): boolean {
|
|
297
|
+
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
298
|
+
return new RegExp(`^#{2,4}\\s*${escaped}`, 'm').test(content)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* 三层校验:format / reference / semantic。
|
|
303
|
+
*
|
|
304
|
+
* 只报问题,不决定是否写入——是否拦截由 `wikiSchemaMode` 决定(strict 拒绝 / report 标记)。
|
|
305
|
+
*/
|
|
306
|
+
export function validateCardAgainstType(
|
|
307
|
+
type: WikiTypeDef,
|
|
308
|
+
card: { title: string; content: string; sourceRefs: string[]; metadata: Record<string, unknown>; owner?: string; summary?: string },
|
|
309
|
+
): CardSchemaIssue[] {
|
|
310
|
+
const issues: CardSchemaIssue[] = []
|
|
311
|
+
|
|
312
|
+
// ---- format:必填小节与必填字段
|
|
313
|
+
for (const section of type.sections) {
|
|
314
|
+
if (!section.required) continue
|
|
315
|
+
if (!hasSection(card.content, section.label)) {
|
|
316
|
+
issues.push({ level: 'format', field: section.label, message: `缺少必填小节「${section.label}」` })
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
for (const field of type.fields) {
|
|
320
|
+
if (!field.required) continue
|
|
321
|
+
const topLevel = field.name === 'owner' ? card.owner : field.name === 'summary' ? card.summary : undefined
|
|
322
|
+
const inMetadata = card.metadata[field.name]
|
|
323
|
+
if (!topLevel?.toString().trim() && (inMetadata === undefined || inMetadata === null || String(inMetadata).trim() === '')) {
|
|
324
|
+
issues.push({ level: 'format', field: field.name, message: `缺少必填字段 ${field.name}(${field.note})` })
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ---- reference:来源分类是否符合该类型的期望(只提示,不阻断)
|
|
329
|
+
if (type.sourceTypes.length > 0) {
|
|
330
|
+
const mismatched = card.metadata.source_types
|
|
331
|
+
if (Array.isArray(mismatched) && mismatched.length > 0) {
|
|
332
|
+
const allowed = new Set(type.sourceTypes)
|
|
333
|
+
const outside = mismatched.map(String).filter(id => !allowed.has(id))
|
|
334
|
+
if (outside.length > 0) {
|
|
335
|
+
issues.push({ level: 'reference', field: 'source_types', message: `来源分类 ${outside.join(' / ')} 不在该类型的期望范围(${type.sourceTypes.join(' / ')})内` })
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ---- semantic:正文不能只有标题,也不能留着占位符
|
|
341
|
+
const body = card.content
|
|
342
|
+
.split('\n')
|
|
343
|
+
.filter(line => !/^\s*#{1,6}\s/.test(line))
|
|
344
|
+
.join('')
|
|
345
|
+
.replace(/\s+/g, '')
|
|
346
|
+
if (body.length < 20) {
|
|
347
|
+
issues.push({ level: 'semantic', message: `正文实质内容过少(去掉标题后 ${body.length} 字),像是只搭了骨架` })
|
|
348
|
+
}
|
|
349
|
+
if (PLACEHOLDER.test(card.content)) {
|
|
350
|
+
issues.push({ level: 'semantic', message: '正文里还留着占位符(TODO / 待补充 / 写什么)' })
|
|
351
|
+
}
|
|
352
|
+
if (type.layer === 'instance' && !card.metadata.occurred_at && type.id === 'case') {
|
|
353
|
+
issues.push({ level: 'semantic', field: 'occurred_at', message: '案例类卡片应写明发生时间' })
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return issues
|
|
357
|
+
}
|