dsh-data-domain-knowledge 0.1.0-alpha.3 → 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.
Files changed (148) hide show
  1. package/README.md +126 -22
  2. package/cordis.patch.yml +25 -16
  3. package/lib/client.js +2144 -1571
  4. package/lib/doc-page.js +40 -19
  5. package/lib/types/convert/host.d.ts.map +1 -1
  6. package/lib/types/convert/host.js +27 -2
  7. package/lib/types/convert/host.js.map +1 -1
  8. package/lib/types/convert/ingest.d.ts +7 -4
  9. package/lib/types/convert/ingest.d.ts.map +1 -1
  10. package/lib/types/convert/ingest.js +52 -14
  11. package/lib/types/convert/ingest.js.map +1 -1
  12. package/lib/types/convert/types.d.ts +2 -2
  13. package/lib/types/convert/types.d.ts.map +1 -1
  14. package/lib/types/convert/types.js +2 -2
  15. package/lib/types/convert/types.js.map +1 -1
  16. package/lib/types/diagnosis/cards.js +2 -2
  17. package/lib/types/diagnosis/run.d.ts +7 -1
  18. package/lib/types/diagnosis/run.d.ts.map +1 -1
  19. package/lib/types/diagnosis/run.js +8 -2
  20. package/lib/types/diagnosis/run.js.map +1 -1
  21. package/lib/types/diagnosis/seed-source.js +1 -1
  22. package/lib/types/diagnosis/seed.d.ts +1 -1
  23. package/lib/types/diagnosis/seed.js +1 -1
  24. package/lib/types/diagnosis/store.d.ts +13 -31
  25. package/lib/types/diagnosis/store.d.ts.map +1 -1
  26. package/lib/types/diagnosis/store.js +33 -71
  27. package/lib/types/diagnosis/store.js.map +1 -1
  28. package/lib/types/diagnosis/types.d.ts +1 -1
  29. package/lib/types/diagnosis/types.js +1 -1
  30. package/lib/types/extract-model.d.ts +67 -27
  31. package/lib/types/extract-model.d.ts.map +1 -1
  32. package/lib/types/extract-model.js +225 -97
  33. package/lib/types/extract-model.js.map +1 -1
  34. package/lib/types/extract.d.ts +16 -0
  35. package/lib/types/extract.d.ts.map +1 -1
  36. package/lib/types/extract.js +19 -2
  37. package/lib/types/extract.js.map +1 -1
  38. package/lib/types/fs-atomic.d.ts +3 -4
  39. package/lib/types/fs-atomic.d.ts.map +1 -1
  40. package/lib/types/fs-atomic.js +3 -4
  41. package/lib/types/fs-atomic.js.map +1 -1
  42. package/lib/types/index.d.ts.map +1 -1
  43. package/lib/types/index.js +302 -89
  44. package/lib/types/index.js.map +1 -1
  45. package/lib/types/intent/types.js +1 -1
  46. package/lib/types/intent/types.js.map +1 -1
  47. package/lib/types/knowledge-bases.d.ts +13 -10
  48. package/lib/types/knowledge-bases.d.ts.map +1 -1
  49. package/lib/types/knowledge-bases.js +18 -27
  50. package/lib/types/knowledge-bases.js.map +1 -1
  51. package/lib/types/layout.d.ts +3 -3
  52. package/lib/types/layout.d.ts.map +1 -1
  53. package/lib/types/layout.js +4 -4
  54. package/lib/types/layout.js.map +1 -1
  55. package/lib/types/migrate.d.ts +1 -1
  56. package/lib/types/migrate.js +2 -2
  57. package/lib/types/migrate.js.map +1 -1
  58. package/lib/types/model-text.d.ts +99 -0
  59. package/lib/types/model-text.d.ts.map +1 -0
  60. package/lib/types/model-text.js +103 -0
  61. package/lib/types/model-text.js.map +1 -0
  62. package/lib/types/present.d.ts +239 -0
  63. package/lib/types/present.d.ts.map +1 -0
  64. package/lib/types/present.js +255 -0
  65. package/lib/types/present.js.map +1 -0
  66. package/lib/types/recall-rank.d.ts +37 -0
  67. package/lib/types/recall-rank.d.ts.map +1 -1
  68. package/lib/types/recall-rank.js +41 -1
  69. package/lib/types/recall-rank.js.map +1 -1
  70. package/lib/types/runtime.d.ts +419 -78
  71. package/lib/types/runtime.d.ts.map +1 -1
  72. package/lib/types/runtime.js +2104 -334
  73. package/lib/types/runtime.js.map +1 -1
  74. package/lib/types/settings-schema.d.ts +2 -0
  75. package/lib/types/settings-schema.d.ts.map +1 -1
  76. package/lib/types/settings-schema.js +2 -0
  77. package/lib/types/settings-schema.js.map +1 -1
  78. package/lib/types/source-images.d.ts +118 -0
  79. package/lib/types/source-images.d.ts.map +1 -0
  80. package/lib/types/source-images.js +288 -0
  81. package/lib/types/source-images.js.map +1 -0
  82. package/lib/types/types.d.ts +316 -39
  83. package/lib/types/types.d.ts.map +1 -1
  84. package/lib/types/types.js +57 -11
  85. package/lib/types/types.js.map +1 -1
  86. package/lib/types/wiki-finalize.d.ts +140 -0
  87. package/lib/types/wiki-finalize.d.ts.map +1 -0
  88. package/lib/types/wiki-finalize.js +269 -0
  89. package/lib/types/wiki-finalize.js.map +1 -0
  90. package/lib/types/wiki-types.d.ts +2 -1
  91. package/lib/types/wiki-types.d.ts.map +1 -1
  92. package/lib/types/wiki-types.js +13 -7
  93. package/lib/types/wiki-types.js.map +1 -1
  94. package/package.json +3 -3
  95. package/preset/tita-data-agent/agent.cordis.yml +25 -16
  96. package/src/client/DrawerSection.tsx +18 -0
  97. package/src/client/KnowledgeBaseTab.tsx +294 -104
  98. package/src/client/KnowledgePanel.tsx +4 -3
  99. package/src/client/KnowledgeToolRow.tsx +331 -0
  100. package/src/client/PreviewSections.tsx +115 -34
  101. package/src/client/RecallPanel.tsx +418 -127
  102. package/src/client/TypeDefinitionDrawer.tsx +82 -16
  103. package/src/client/WikiCardReview.tsx +266 -0
  104. package/src/client/WikiFlowCard.tsx +57 -95
  105. package/src/client/WikiTypeEditor.tsx +6 -20
  106. package/src/client/arch/AgentArchitectureGraph.tsx +22 -135
  107. package/src/client/architecture.css +22 -27
  108. package/src/client/architecture.tsx +12 -12
  109. package/src/client/asset-images.ts +125 -0
  110. package/src/client/attribution/AttributionTab.tsx +4 -10
  111. package/src/client/attribution/StageChrome.tsx +8 -24
  112. package/src/client/convert/ConvertPanel.tsx +30 -3
  113. package/src/client/doc-page.tsx +6 -27
  114. package/src/client/index.ts +18 -0
  115. package/src/client/problem-domains.ts +1 -1
  116. package/src/client/styles.css +80 -11
  117. package/src/convert/host.ts +27 -2
  118. package/src/convert/ingest.ts +61 -18
  119. package/src/convert/types.ts +2 -2
  120. package/src/diagnosis/cards.ts +2 -2
  121. package/src/diagnosis/run.ts +8 -2
  122. package/src/diagnosis/seed-source.ts +1 -1
  123. package/src/diagnosis/seed.ts +1 -1
  124. package/src/diagnosis/store.ts +32 -79
  125. package/src/diagnosis/types.ts +2 -2
  126. package/src/extract-model.ts +254 -99
  127. package/src/extract.ts +35 -3
  128. package/src/fs-atomic.ts +3 -4
  129. package/src/index.ts +338 -88
  130. package/src/intent/types.ts +1 -1
  131. package/src/knowledge-bases.ts +31 -32
  132. package/src/layout.ts +4 -4
  133. package/src/migrate.ts +2 -2
  134. package/src/model-text.ts +126 -0
  135. package/src/present.ts +367 -0
  136. package/src/recall-rank.ts +57 -2
  137. package/src/runtime.ts +2282 -374
  138. package/src/settings-schema.ts +2 -0
  139. package/src/source-images.ts +326 -0
  140. package/src/types.ts +354 -47
  141. package/src/wiki-finalize.ts +310 -0
  142. package/src/wiki-types.ts +13 -7
  143. package/lib/types/proposals.d.ts +0 -166
  144. package/lib/types/proposals.d.ts.map +0 -1
  145. package/lib/types/proposals.js +0 -238
  146. package/lib/types/proposals.js.map +0 -1
  147. package/src/client/ProposalReview.tsx +0 -313
  148. package/src/proposals.ts +0 -337
@@ -0,0 +1,310 @@
1
+ /**
2
+ * 编译的收尾段(issue 0049):**索引页、断链检查、相关卡片**。
3
+ *
4
+ * 三段都不调用模型,全是规则与图算法 —— 它们是「已经落盘的卡片」上的确定性计算,
5
+ * 所以可以重复跑,跑出来的东西逐字相同。这一点是它们能被信任的前提:一次重编不会
6
+ * 让索引页改一个字,也不会让卡片的相关列表换一批。
7
+ *
8
+ * ## 三段各自解决什么
9
+ *
10
+ * 1. **相关卡片**(`planRelated`)—— 卡片之间原来只有图上的边,正文里什么都没有,
11
+ * 人打开一张卡片看不到它跟谁有关系。现在把关系**写进正文末尾的一块**,并且
12
+ * 整块替换(不是追加):追加会让卡片每编译一次长一截。
13
+ * 2. **断链检查**(`deadLinksOf`)—— 正文里的 `[[类型/名称]]` 原来没有任何人校验,
14
+ * 指向一张不存在的卡片也照样写着。这是「三层校验」里 `reference` 那一层的实际内容
15
+ * (0044 之前它定义了但没有任何产生者)。
16
+ * 3. **索引页**(`buildWikiIndex`)—— 知识库原来没有「这里有什么」的落盘入口:
17
+ * 卡片列表只在界面上,不打开界面就没有目录。
18
+ *
19
+ * ## 为什么相关块带一个 HTML 注释标记
20
+ *
21
+ * 卡片正文是**只读**的(编译追加,人和 Agent 都不能改),而模型写卡片时完全可能
22
+ * 自己写一个 `## 相关` 小节。靠小节名去删,会把模型写的正文当成编译产物删掉。
23
+ * 所以块首带 `<!-- compiled:related -->`:只删带标记的那一块,别的一个字不碰。
24
+ *
25
+ * ## 为什么只认 wiki 链接语法
26
+ *
27
+ * 相关块里写的是 `[[类型/标题|标题]]`,不是 markdown 相对链接。两个理由:卡片没有
28
+ * 自己的页面地址,写 `../concept/x.md` 在面板里点开是 404;而 `[[…]]` 是这套知识库
29
+ * 自己的引用语法(来源文档里的 `[[链接]]` 会被 Pass 0 读成关系候选),断链检查认它,
30
+ * `grep` 也认它。
31
+ *
32
+ * @module
33
+ */
34
+
35
+ import { buildAliasIndex, candidateKey, normalizeCandidateName } from './extract.js'
36
+ import type { KnowledgeCard } from './types.js'
37
+
38
+ /** 卡片之间的关系来源:来源里写明的关系,还是「同一份来源编译出来的」。 */
39
+ export type RelatedReason = 'relation' | 'source'
40
+
41
+ export interface RelatedEntry {
42
+ /** 图上的节点键 `<entityType>:<entityId>`,与 `knowledge_graph.json` 同一套键。 */
43
+ key: string
44
+ entityType: string
45
+ entityId: string
46
+ title: string
47
+ reason: RelatedReason
48
+ }
49
+
50
+ /**
51
+ * 一张卡片最多列几条相关。
52
+ *
53
+ * 上限是**有意的**,不是省略号:一份来源编译出十张卡片时,两两相连是 90 条,
54
+ * 那样每张卡片的末尾都会挂一份比正文还长的清单,人就不看了。关系优先于同源。
55
+ */
56
+ export const MAX_RELATED = 8
57
+
58
+ /** 相关块的标记行。删块只认它,不认小节名。 */
59
+ export const RELATED_MARK = '<!-- compiled:related -->'
60
+
61
+ export const RELATED_HEADING = '## 相关'
62
+
63
+ /** 正文里的 wiki 链接:`[[类型/名称]]`、`[[类型/名称|显示文字]]`、`[[名称]]`。 */
64
+ const WIKI_LINK = /\[\[([^\]|]+)(?:\|[^\]]*)?\]\]/g
65
+
66
+ /** 卡片正文里出现的 wiki 链接目标(原样,未归一)。 */
67
+ export function wikiLinkTargets(content: string): string[] {
68
+ const targets: string[] = []
69
+ for (const match of content.matchAll(WIKI_LINK)) {
70
+ const target = (match[1] ?? '').trim()
71
+ if (target) targets.push(target)
72
+ }
73
+ return targets
74
+ }
75
+
76
+ /**
77
+ * 去掉正文末尾的相关块。**只删带标记的那一块**,没有标记时原样返回。
78
+ *
79
+ * 相关块永远在末尾(`applyRelatedSection` 只往末尾写),所以找到标记就是找到块首。
80
+ */
81
+ export function stripRelatedSection(content: string): string {
82
+ const at = content.indexOf(RELATED_MARK)
83
+ if (at < 0) return content
84
+ return content.slice(0, at).replace(/\s+$/, '')
85
+ }
86
+
87
+ /** `[[类型/标题|标题]]` 里的目标:标题里的 `|` 与 `]` 会破坏语法,去掉。 */
88
+ export function wikiLinkOf(entityType: string, title: string, fallbackId: string): string {
89
+ const clean = title.replace(/[|\]]/g, '').trim() || fallbackId
90
+ return `[[${entityType}/${clean}|${clean}]]`
91
+ }
92
+
93
+ function linkTargetOf(entry: RelatedEntry): string {
94
+ return wikiLinkOf(entry.entityType, entry.title, entry.entityId)
95
+ }
96
+
97
+ const REASON_LABEL: Record<RelatedReason, string> = {
98
+ relation: '来源里写明的关系',
99
+ source: '同一份来源',
100
+ }
101
+
102
+ export function renderRelatedSection(entries: RelatedEntry[]): string {
103
+ const lines = entries.map(entry => `- ${linkTargetOf(entry)} · ${REASON_LABEL[entry.reason]}`)
104
+ return `${RELATED_MARK}\n${RELATED_HEADING}\n${lines.join('\n')}`
105
+ }
106
+
107
+ /**
108
+ * 把相关块写到正文末尾。**幂等**:先删旧块再写新块,所以重复调用结果相同;
109
+ * 一条相关都没有时不留空块(留一个只有标题的小节比没有更难看)。
110
+ */
111
+ export function applyRelatedSection(content: string, entries: RelatedEntry[]): string {
112
+ const base = stripRelatedSection(content)
113
+ if (entries.length === 0) return base
114
+ return `${base}\n\n${renderRelatedSection(entries)}`
115
+ }
116
+
117
+ /** 卡片的节点键,与图、与关系候选同一套写法。 */
118
+ export function cardKey(card: { entityType: string; entityId: string }): string {
119
+ return `${card.entityType}:${card.entityId}`
120
+ }
121
+
122
+ /**
123
+ * 相关卡片计划:`卡片键 → 相关条目`(双向)。
124
+ *
125
+ * 两个来源,优先级不同:
126
+ * 1. **关系**(`relations`,即 Pass 0 从来源文档里抽出的 `relates_to`)—— 强,双向都记;
127
+ * 2. **同源**(两张卡片引用了同一份 SourceRaw)—— 弱,只补前面没覆盖到的。
128
+ *
129
+ * 弱的那一类是有用的:知识库刚建起来时,关系一条都没有,而「这几张卡来自同一篇文章」
130
+ * 恰好是人最需要的导航。它不该盖过写明的关系,所以排在后面、并且有上限。
131
+ */
132
+ export function planRelated(
133
+ cards: Array<Pick<KnowledgeCard, 'entityType' | 'entityId' | 'title' | 'sourceRefs'>>,
134
+ relations: Array<{ fromKey: string; toKey: string }>,
135
+ ): Map<string, RelatedEntry[]> {
136
+ const byKey = new Map(cards.map(card => [cardKey(card), card]))
137
+ const planned = new Map<string, RelatedEntry[]>()
138
+ const seen = new Map<string, Set<string>>()
139
+
140
+ const add = (fromKey: string, toKey: string, reason: RelatedReason): void => {
141
+ if (fromKey === toKey) return
142
+ // **两端都要真实存在**:来源文档里可以写一条指向还没建出来的实体的关系,
143
+ // 但那张卡片不存在,就不能凭空给它挂一份相关清单(它的键在卡片清单里没有对应物,
144
+ // 索引页也会据此写出一行点不开的目录)。
145
+ if (!byKey.has(fromKey)) return
146
+ const target = byKey.get(toKey)
147
+ if (!target) return
148
+ const keys = seen.get(fromKey) ?? new Set<string>()
149
+ if (keys.has(toKey)) return
150
+ keys.add(toKey)
151
+ seen.set(fromKey, keys)
152
+ const bucket = planned.get(fromKey) ?? []
153
+ bucket.push({ key: toKey, entityType: target.entityType, entityId: target.entityId, title: target.title, reason })
154
+ planned.set(fromKey, bucket)
155
+ }
156
+
157
+ // 关系:双向都记。来源文档里「A 提到 B」本来就意味着两边互相能找得到。
158
+ for (const relation of relations) {
159
+ add(relation.fromKey, relation.toKey, 'relation')
160
+ add(relation.toKey, relation.fromKey, 'relation')
161
+ }
162
+
163
+ // 同源:按来源分组,组内两两相连。标题排序,所以同源那几条的顺序不随文件系统变。
164
+ const bySource = new Map<string, Array<Pick<KnowledgeCard, 'entityType' | 'entityId' | 'title' | 'sourceRefs'>>>()
165
+ for (const card of cards) {
166
+ for (const sourceRef of card.sourceRefs) {
167
+ const bucket = bySource.get(sourceRef) ?? []
168
+ bucket.push(card)
169
+ bySource.set(sourceRef, bucket)
170
+ }
171
+ }
172
+ for (const bucket of [...bySource.values()].sort((left, right) => cardKey(left[0]!).localeCompare(cardKey(right[0]!)))) {
173
+ const ordered = [...bucket].sort((left, right) => left.title.localeCompare(right.title, 'zh'))
174
+ for (const card of ordered) {
175
+ for (const other of ordered) add(cardKey(card), cardKey(other), 'source')
176
+ }
177
+ }
178
+
179
+ // 上限:关系排在前面(它们先入桶),所以截断只会截掉同源那几条。
180
+ for (const [key, entries] of planned) planned.set(key, entries.slice(0, MAX_RELATED))
181
+ return planned
182
+ }
183
+
184
+ export interface DeadLink {
185
+ /** 出现这条断链的卡片键。 */
186
+ key: string
187
+ entityId: string
188
+ entityType: string
189
+ /** 原样的链接目标,报给用户看的就是它。 */
190
+ target: string
191
+ }
192
+
193
+ /**
194
+ * 断链:正文里 `[[…]]` 指向一张不存在的卡片。
195
+ *
196
+ * 判据与抽取的判重**同一套**(`buildAliasIndex`:类型 + entityId / 标题 / 别名),
197
+ * 所以「抽取时认得是同一张卡」与「链接能解析」不可能给出两个答案。自链接不算断链:
198
+ * 卡片提到自己不是错误,只是没有信息量。
199
+ *
200
+ * 裸名字(`[[GMV 口径]]`)不带类型,所以按名字在**所有类型**里找一遍 —— 找不到才算断。
201
+ *
202
+ * 类型 id 自己可以带 `/`(`diagnosis/check-point` 就是一个真实类型),所以
203
+ * `[[diagnosis/check-point/主键唯一性]]` 不能只按**第一个** `/` 切:那样切出来的类型是
204
+ * `diagnosis`,名字是 `check-point/主键唯一性`,一个都查不到 —— 卡片之间那几条相关链接
205
+ * 会被整批报成断链。这里从最长的类型前缀开始试(即从左往右逐个 `/` 切)。
206
+ */
207
+ export function deadLinksOf(cards: KnowledgeCard[], index?: Map<string, KnowledgeCard>): DeadLink[] {
208
+ const resolved = index ?? buildAliasIndex(cards)
209
+ const byName = new Map<string, KnowledgeCard>()
210
+ for (const card of cards) {
211
+ for (const name of [card.entityId, card.title, ...aliasesOf(card)]) {
212
+ const normalized = normalizeCandidateName(name)
213
+ if (normalized && !byName.has(normalized)) byName.set(normalized, card)
214
+ }
215
+ }
216
+ const resolveTarget = (target: string): KnowledgeCard | undefined => {
217
+ for (let at = target.indexOf('/'); at > 0; at = target.indexOf('/', at + 1)) {
218
+ const hit = resolved.get(candidateKey({ type: target.slice(0, at), name: target.slice(at + 1) }))
219
+ if (hit) return hit
220
+ }
221
+ return byName.get(normalizeCandidateName(target))
222
+ }
223
+ const dead: DeadLink[] = []
224
+ for (const card of cards) {
225
+ const self = cardKey(card)
226
+ for (const target of wikiLinkTargets(card.content)) {
227
+ // 解析得到就不算断链 —— 自链接(卡片提到自己)也走这一条。
228
+ if (resolveTarget(target) !== undefined) continue
229
+ dead.push({ key: self, entityId: card.entityId, entityType: card.entityType, target })
230
+ }
231
+ }
232
+ return dead
233
+ }
234
+
235
+ function aliasesOf(card: KnowledgeCard): string[] {
236
+ const raw = card.metadata.aliases
237
+ if (typeof raw === 'string') return [raw]
238
+ if (!Array.isArray(raw)) return []
239
+ return raw.filter((item): item is string => typeof item === 'string')
240
+ }
241
+
242
+ /** 三层校验里 `reference` 那一层的报错文案(写进卡片的 `schema_issues`)。 */
243
+ export function deadLinkIssue(target: string): { level: 'reference'; field: string; message: string } {
244
+ return { level: 'reference', field: 'links', message: `正文里的 [[${target}]] 指向一张不存在的卡片` }
245
+ }
246
+
247
+ export interface WikiIndexInput {
248
+ cards: KnowledgeCard[]
249
+ related: Map<string, RelatedEntry[]>
250
+ deadLinks: DeadLink[]
251
+ }
252
+
253
+ /**
254
+ * 索引页:**不打开界面也能知道这个库里有什么**。
255
+ *
256
+ * 它是派生文件(每次编译重算),所以落在 `03_compiled/`,不落在 `02_wiki/` ——
257
+ * 后者的每一份 `.md` 都会被当成一张卡片读进卡片清单,把索引页放进去,它会变成一张
258
+ * 标题叫「知识库索引」的卡片,而且每次编译都变一次。
259
+ *
260
+ * 内容按类型分组(类型是这套 schema 的一等公民),每张卡片一行:链接 + 状态 + 待补数。
261
+ * 断链单独一节列在末尾 —— 它是「要动手修」的东西,不该混在目录里。
262
+ *
263
+ * **刻意不写生成时间与版本号。** 写了它们,这份文件每次重建都会变一个字节,而增量
264
+ * 编译要靠「索引页变了没有」判断这一轮要不要真的落盘 —— 一个永远在变的产物会让
265
+ * 增量编译永远判定「有变化」,空操作那条路就再也走不到。新鲜度由文件本身的
266
+ * 修改时间与 `build-manifest.json` 回答,不需要在正文里再写一遍。
267
+ */
268
+ export function buildWikiIndex(input: WikiIndexInput): string {
269
+ const { cards, related, deadLinks } = input
270
+ const byType = new Map<string, KnowledgeCard[]>()
271
+ for (const card of cards) {
272
+ const bucket = byType.get(card.entityType) ?? []
273
+ bucket.push(card)
274
+ byType.set(card.entityType, bucket)
275
+ }
276
+ const relatedLinks = [...related.values()].reduce((sum, entries) => sum + entries.length, 0)
277
+ const missing = cards.reduce((sum, card) => sum + issuesOf(card).length, 0)
278
+
279
+ const lines: string[] = []
280
+ lines.push('# 知识库索引', '')
281
+ lines.push(`- 卡片 ${cards.length} 张 · 类型 ${byType.size} 个 · 相关链接 ${relatedLinks} 条 · 断链 ${deadLinks.length} 条`)
282
+ lines.push(`- 待补 ${missing} 项`)
283
+ lines.push('', '这个文件由编译生成,不是手写的。它列出库里现有的卡片;改卡片请改 `02_wiki/` 下的那一份。', '')
284
+
285
+ for (const type of [...byType.keys()].sort((left, right) => left.localeCompare(right))) {
286
+ const bucket = [...byType.get(type)!].sort((left, right) => left.title.localeCompare(right.title, 'zh'))
287
+ lines.push(`## ${type}(${bucket.length} 张)`, '')
288
+ for (const card of bucket) {
289
+ const issues = issuesOf(card).length
290
+ const facts = [card.status, issues > 0 ? `待补 ${issues}` : undefined].filter(Boolean).join(' · ')
291
+ lines.push(`- ${wikiLinkOf(card.entityType, card.title, card.entityId)} · ${facts}`)
292
+ if (card.summary?.trim()) lines.push(` ${card.summary.trim().replace(/\s+/g, ' ')}`)
293
+ }
294
+ lines.push('')
295
+ }
296
+
297
+ if (deadLinks.length > 0) {
298
+ lines.push(`## 断链(${deadLinks.length} 条)`, '')
299
+ for (const link of deadLinks) lines.push(`- ${link.entityType}/${link.entityId} 里的 [[${link.target}]] 找不到对应卡片`)
300
+ lines.push('')
301
+ }
302
+
303
+ return `${lines.join('\n').replace(/\n+$/, '')}\n`
304
+ }
305
+
306
+ /** 卡片上的待补项(`metadata.schema_issues`)。索引页只数个数,不搬全文。 */
307
+ function issuesOf(card: KnowledgeCard): unknown[] {
308
+ const raw = card.metadata.schema_issues
309
+ return Array.isArray(raw) ? raw : []
310
+ }
package/src/wiki-types.ts CHANGED
@@ -5,15 +5,19 @@ import type { CardSchemaIssue, WikiFieldSpec, WikiSectionSpec, WikiTypeDef } fro
5
5
  * Wiki 卡片类型的出厂定义与校验(issue 0024)。
6
6
  *
7
7
  * 这一层回答的是"**SourceRaw → Wiki** 能生成什么、每类长什么样"。它不是生成器:
8
- * 生成仍由模型完成(`write_knowledge_card`),这里提供的是**判据 + 骨架 + 校验**。
8
+ * 生成由模型完成(`extract-model.ts` 的编译器,或 Agent 的 `propose_wiki_cards`),
9
+ * 这里提供的是**判据 + 骨架 + 校验**。
9
10
  *
10
11
  * 三个设计判断:
11
12
  *
12
- * 1. **schema 就是提示词。** 类型定义、示例、必填小节都随 `write_knowledge_card` 的工具
13
- * 描述下发(与 0023 同一机制),所以用户改完类型下一轮模型就看到新规则。
13
+ * 1. **schema 就是提示词。** 类型定义、示例、必填小节、模板都会下发 —— 0044 起直接下发给
14
+ * 编译器的抽取提示词(`extract-model.ts` 的 `renderTypeSpec`),所以用户改完类型,
15
+ * 下一次编译写出来的卡片骨架就变了。改 schema 因此是一件真的会改变产物的事。
14
16
  * 2. **模板决定这套 schema 能不能被用起来**(0021 的判断)。`template` 由 `sections` 派生,
15
17
  * 不手写第二份,否则又是一处"校验通过但人照着抄还是错"。
16
18
  * 3. **校验分三层**:格式 / 引用 / 语义。混成同一个"校验失败",人就学不会怎么写卡片。
19
+ * 0044 起校验**不再拦截写盘**(缺信息也入库),它只把问题写进 `metadata.schema_issues`,
20
+ * 由面板显示成「待补」。
17
21
  */
18
22
 
19
23
  /** 出厂 wiki 类型:6 个通用类型 + diagnosis 的 5 个类型族。 */
@@ -191,9 +195,10 @@ export const WIKI_TYPE_RULES = [
191
195
  '所有类型都不命中时,才按语义新起一个类型 id(小写英文,可带 `命名空间/名字`),并在知识库设置里把它登记成正式类型。',
192
196
  '一张卡片只讲一件事:概念讲定义、指标讲口径、手册讲做法、案例讲发生过的事。混在一起时要拆成多张卡片。',
193
197
  '卡片必须引用真实存在的 SourceRaw:`source_refs` 写了不存在的来源会被直接拒绝。',
194
- '必填小节与必填字段来自类型定义;缺了它们卡片仍然可写(report 模式),但会被标成待补。',
195
- '**写卡片之前先提提议**:用 `propose_wiki_cards` 把草案挂到触发它的那条 SourceRaw 上,由人在「知识库 → Wiki」的待办区确认之后才落卡。未确认的草案不落 `02_wiki/`、不进编译、不参与召回、不在图上出现。',
196
- '`write_knowledge_card` 只接受**已确认且尚未落卡**的提议(带 `proposal_id`)—— 它没有「未确认也能写」这条路径。',
198
+ '必填小节与必填字段来自类型定义;**缺了它们卡片照样入库**,会被标成待补,后面有涉及到的文档再慢慢补。',
199
+ '**入库在前、review 在后**(0044):卡片一编译就直接写进 `02_wiki` 并立刻可召回,不需要任何人先确认。',
200
+ '**正文只读**:卡片正文由编译产出(新文档会追加进来),人和 Agent 都不能手工改它。要指出问题就加备注(`annotate_wiki_card`,异常点用 `kind: anomaly`)。',
201
+ 'review(`review_wiki_card`)在入库之后做,它抬高卡片的召回评分;两个动作都可逆,review 不是准入凭证。',
197
202
  ].join('\n')
198
203
 
199
204
  /** 新增 / 编辑类型时提交的草稿。 */
@@ -303,7 +308,8 @@ function hasSection(content: string, label: string): boolean {
303
308
  /**
304
309
  * 三层校验:format / reference / semantic。
305
310
  *
306
- * 只报问题,不决定是否写入——是否拦截由 `wikiSchemaMode` 决定(strict 拒绝 / report 标记)。
311
+ * 只报问题,**不决定是否写入**:0044 起卡片一律直接入库,问题写进 `metadata.schema_issues`,
312
+ * 由面板显示成「待补」。
307
313
  */
308
314
  export function validateCardAgainstType(
309
315
  type: WikiTypeDef,
@@ -1,166 +0,0 @@
1
- import type { CardSchemaIssue, WikiTypeDef } from './types.js';
2
- /**
3
- * 卡片**提议 → 确认**的两段门(0021 §6.1 / §6.2、0040 §7)。
4
- *
5
- * ## 它解决什么
6
- *
7
- * 类型层是**收敛**的那一层 —— 它错一条,所有现象的排序一起错;可它又必须**能不断补充**,
8
- * 否则新根因永远进不来。两个要求只有一个解:**让补充低摩擦(Agent 每次排查都提议),
9
- * 让生效高门槛(人确认)**。
10
- *
11
- * ## 为什么不能靠 `status: draft`
12
- *
13
- * 今天 `status` 只是标签:`draft` 卡照样进 wikiNotes、进图、被召回(只有 `problem`
14
- * 被召回路径排除)。要让「未确认不生效」真正成立,就得让提议**根本不落卡片**。
15
- * 所以这一层的关键词是「不落 `02_wiki/`」—— 未确认的提议不参与编译、不参与召回、
16
- * 不在图上出现,它连一张卡片都还不是。
17
- *
18
- * ## 提议存在哪(**这一条与 PRD 的措辞有一处偏离,理由写在下面**)
19
- *
20
- * PRD 说「提议挂在触发它的那条 SourceRaw 上,停在来源侧」。这里按**逻辑上挂在它上面**
21
- * 实现:每条提议都带 `sourceRef`,按它索引;但**物理上落在记忆段**
22
- * (`<agentRoot>/memory/<库 id>/card-proposals/`)。
23
- *
24
- * 三条理由:
25
- * 1. **它不属于「源头」。** `01_source_truth/` 的性质是「删掉就丢数据」。提议删掉只丢
26
- * 待办 —— 重跑一次提议就能回来。把可重建的东西放进源头段,会让「这段能不能删」
27
- * 这个问题失去唯一答案(0021 §9 的三段判据)。
28
- * 2. **它更不属于知识侧。** 落 `02_wiki/` 正是这道门要拦的事。
29
- * 3. **记忆段就是为这种东西存在的。** 它的定义是「关于这个 Agent 的动作留痕,不是关于
30
- * 知识的断言」—— 一条待审的提议正好是 Agent 的动作留痕。
31
- *
32
- * 逻辑上「挂在 SourceRaw 上」这条没有丢:`sourceRef` 是必填字段,review 界面按它分组,
33
- * 来源被删掉时提议会如实报「来源已不存在」而不是静默生效。
34
- *
35
- * ## 判重与拒绝留痕
36
- *
37
- * 提议 id 由「来源 + 动作 + 类型 + 实体 id」算出来,**稳定**:同一件事重跑一次提议
38
- * 不会产生第二条,只会原地更新。内容指纹决定**拒绝之后要不要再提**:
39
- * 指纹没变就不再提(不打扰人),指纹变了才重新提(知识确实变了)。
40
- */
41
- export type ProposalAction = 'create' | 'update' | 'merge';
42
- export type ProposalTrigger = 'extraction' | 'agent' | 'diagnosis-run';
43
- export type ProposalStatus = 'pending' | 'confirmed' | 'rejected';
44
- export interface CardProposal {
45
- /** 稳定 id:`p-<hash(sourceRef|action|entityType|entityId)>`。 */
46
- id: string;
47
- /** 触发这条提议的那条 SourceRaw。 */
48
- sourceRef: string;
49
- trigger: ProposalTrigger;
50
- action: ProposalAction;
51
- entityType: string;
52
- entityId: string;
53
- title: string;
54
- summary?: string;
55
- /** 卡片正文(小节 + 结构化块),与 `writeCard` 的 `content` 同形。 */
56
- content: string;
57
- sourceRefs: string[];
58
- metadata?: Record<string, unknown>;
59
- status: ProposalStatus;
60
- /** 内容指纹:拒绝之后同一条不再重复提。 */
61
- fingerprint: string;
62
- createdAt: string;
63
- updatedAt: string;
64
- /** **提议阶段**跑出来的三层校验问题。确认阶段会再跑一遍。 */
65
- issues: CardSchemaIssue[];
66
- /** `update` / `merge` 的目标卡片当时的正文指纹 —— 提议之后卡片又被人改过时能发现。 */
67
- baseHash?: string;
68
- /** 已经确认、但还没落卡(等人或等 Agent 调 `write_knowledge_card`)。 */
69
- confirmedAt?: string;
70
- confirmedBy?: string;
71
- /** 落卡时间。有它就说明这条提议已经变成卡片了。 */
72
- writtenAt?: string;
73
- reviewedAt?: string;
74
- reviewedBy?: string;
75
- /** 拒绝原因。**必须留档** —— 「为什么这条不算」本身就是知识。 */
76
- reason?: string;
77
- }
78
- /** 提一条提议时给的东西。`id` / `status` / 时间戳由 store 生成。 */
79
- export interface ProposalInput {
80
- sourceRef: string;
81
- trigger: ProposalTrigger;
82
- action: ProposalAction;
83
- entityType: string;
84
- entityId: string;
85
- title: string;
86
- summary?: string;
87
- content: string;
88
- sourceRefs?: string[];
89
- metadata?: Record<string, unknown>;
90
- baseHash?: string;
91
- }
92
- export interface ProposalSchema {
93
- types: WikiTypeDef[];
94
- }
95
- export declare function triggerLabel(trigger: ProposalTrigger): string;
96
- /**
97
- * 提议的身份:**同一件事重跑不会产生第二条**。
98
- *
99
- * 它**刻意不含 `sourceRef`**。同一张卡片常常由多份来源共同支撑(同一份口径在两篇文档里
100
- * 都写了、一次排查与它的复盘各指一遍)—— 把来源算进身份,同一条提议就会按来源裂成
101
- * 好几条,人 review 时要在队列里连着点头三次,而且每次点的都是同一件事。
102
- *
103
- * 来源没有被丢掉:它是**多值**的。`sourceRef` 记「最先触发它的那一条」,
104
- * `sourceRefs` 累积全部触发它的来源(见 `upsert` 的合并规则)。
105
- */
106
- export declare function proposalIdOf(input: Pick<ProposalInput, 'action' | 'entityType' | 'entityId'>): string;
107
- /**
108
- * 内容指纹。
109
- *
110
- * 它只包含**会改变卡片内容的东西**(标题、正文、出处):状态流转、时间戳不参与 ——
111
- * 否则一次无意义的写入就会让「拒绝过的那条」重新冒出来。
112
- */
113
- export declare function fingerprintOf(input: Pick<ProposalInput, 'entityType' | 'entityId' | 'title' | 'content' | 'sourceRefs'>): string;
114
- /** 卡片正文的指纹,用来发现「提议之后卡片又被人改过」。 */
115
- export declare function contentHashOf(content: string): string;
116
- export interface ProposalStoreHost {
117
- /** 记忆段根(`<agentRoot>/memory/<库 id>`)。 */
118
- memoryRoot: string;
119
- /** 当前生效的卡片 schema。提议阶段与确认阶段各跑一遍校验。 */
120
- schema: () => ProposalSchema;
121
- }
122
- export declare class CardProposalStore {
123
- private readonly host;
124
- constructor(host: ProposalStoreHost);
125
- private get dir();
126
- /** 全部提议,新的在前。读不动就返回空 —— 提议是可重建的待办,不是数据。 */
127
- list(): Promise<CardProposal[]>;
128
- get(id: string): Promise<CardProposal | undefined>;
129
- pending(): Promise<CardProposal[]>;
130
- /**
131
- * 写入一批提议。返回真正**新增或更新**的那些(被指纹判重挡掉的不会出现在结果里)。
132
- *
133
- * 三条判重规则,按已有提议的状态分:
134
- * - `pending`:原地更新(来源可能又长了一节,正文跟着变),保留 `createdAt`;
135
- * - `confirmed` / `written`:指纹相同就跳过(人已经说过是了,不必再问一遍);
136
- * - `rejected`:指纹相同就跳过(不重复打扰人),**指纹变了才重新提**(知识确实变了)。
137
- */
138
- upsert(inputs: ProposalInput[]): Promise<CardProposal[]>;
139
- /** 提议阶段的三层校验。缺字段 / 图断了 / 知识没用,各报各的。 */
140
- private issuesOf;
141
- /**
142
- * 人点了「通过」。
143
- *
144
- * **确认与落卡是两件事**,所以这里只标状态、不写卡片:
145
- * - 面板上的主操作是「确认并写入」,它先调这里再调 `writeCard`;
146
- * - 副操作是「仅确认」,落卡交给 Agent 的 `write_knowledge_card`(它要求这条提议
147
- * 已确认且尚未落卡)。两条路都过同一道门。
148
- */
149
- confirm(id: string, by: string, patch?: {
150
- content?: string;
151
- title?: string;
152
- summary?: string;
153
- }): Promise<CardProposal | undefined>;
154
- /** 落卡成功之后回填。没有这一步,「已确认但没落卡」和「已经落卡了」分不开。 */
155
- markWritten(id: string): Promise<CardProposal | undefined>;
156
- /**
157
- * 人点了「拒绝」。原因留档 —— 它同时是「下次不要再提同一条」的判据。
158
- *
159
- * 只有 `pending` 能被拒绝:一条已经确认(甚至已经落卡)的提议再被拒,会让
160
- * 「卡片在库里、提议却是拒绝态」这种自相矛盾的状态出现在待办区里。
161
- */
162
- reject(id: string, reason: string, by: string): Promise<CardProposal | undefined>;
163
- /** 删掉一条提议(清理用)。已落卡的提议删掉不影响卡片本身。 */
164
- remove(id: string): Promise<boolean>;
165
- }
166
- //# sourceMappingURL=proposals.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"proposals.d.ts","sourceRoot":"","sources":["../../src/proposals.ts"],"names":[],"mappings":"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"}