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.
Files changed (180) hide show
  1. package/README.md +147 -19
  2. package/cordis.patch.yml +25 -10
  3. package/lib/client.js +6400 -3326
  4. package/lib/doc-page.js +7761 -0
  5. package/lib/types/convert/host.d.ts.map +1 -1
  6. package/lib/types/convert/host.js +63 -11
  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.d.ts +8 -7
  17. package/lib/types/diagnosis/cards.d.ts.map +1 -1
  18. package/lib/types/diagnosis/cards.js +119 -49
  19. package/lib/types/diagnosis/cards.js.map +1 -1
  20. package/lib/types/diagnosis/run.d.ts +57 -0
  21. package/lib/types/diagnosis/run.d.ts.map +1 -0
  22. package/lib/types/diagnosis/run.js +245 -0
  23. package/lib/types/diagnosis/run.js.map +1 -0
  24. package/lib/types/diagnosis/schema.d.ts +25 -2
  25. package/lib/types/diagnosis/schema.d.ts.map +1 -1
  26. package/lib/types/diagnosis/schema.js +64 -25
  27. package/lib/types/diagnosis/schema.js.map +1 -1
  28. package/lib/types/diagnosis/seed-source.d.ts +17 -2
  29. package/lib/types/diagnosis/seed-source.d.ts.map +1 -1
  30. package/lib/types/diagnosis/seed-source.js +19 -2
  31. package/lib/types/diagnosis/seed-source.js.map +1 -1
  32. package/lib/types/diagnosis/seed.d.ts +32 -42
  33. package/lib/types/diagnosis/seed.d.ts.map +1 -1
  34. package/lib/types/diagnosis/seed.js +187 -51
  35. package/lib/types/diagnosis/seed.js.map +1 -1
  36. package/lib/types/diagnosis/store.d.ts +95 -25
  37. package/lib/types/diagnosis/store.d.ts.map +1 -1
  38. package/lib/types/diagnosis/store.js +297 -166
  39. package/lib/types/diagnosis/store.js.map +1 -1
  40. package/lib/types/diagnosis/types.d.ts +168 -16
  41. package/lib/types/diagnosis/types.d.ts.map +1 -1
  42. package/lib/types/diagnosis/types.js +1 -1
  43. package/lib/types/diagnosis/types.js.map +1 -1
  44. package/lib/types/document-page.d.ts +39 -0
  45. package/lib/types/document-page.d.ts.map +1 -0
  46. package/lib/types/document-page.js +43 -0
  47. package/lib/types/document-page.js.map +1 -0
  48. package/lib/types/extract-model.d.ts +67 -27
  49. package/lib/types/extract-model.d.ts.map +1 -1
  50. package/lib/types/extract-model.js +225 -97
  51. package/lib/types/extract-model.js.map +1 -1
  52. package/lib/types/extract.d.ts +16 -0
  53. package/lib/types/extract.d.ts.map +1 -1
  54. package/lib/types/extract.js +19 -2
  55. package/lib/types/extract.js.map +1 -1
  56. package/lib/types/fs-atomic.d.ts +11 -0
  57. package/lib/types/fs-atomic.d.ts.map +1 -0
  58. package/lib/types/fs-atomic.js +41 -0
  59. package/lib/types/fs-atomic.js.map +1 -0
  60. package/lib/types/index.d.ts.map +1 -1
  61. package/lib/types/index.js +466 -63
  62. package/lib/types/index.js.map +1 -1
  63. package/lib/types/intent/types.js +1 -1
  64. package/lib/types/intent/types.js.map +1 -1
  65. package/lib/types/knowledge-bases.d.ts +23 -12
  66. package/lib/types/knowledge-bases.d.ts.map +1 -1
  67. package/lib/types/knowledge-bases.js +37 -44
  68. package/lib/types/knowledge-bases.js.map +1 -1
  69. package/lib/types/layout.d.ts +3 -3
  70. package/lib/types/layout.d.ts.map +1 -1
  71. package/lib/types/layout.js +4 -4
  72. package/lib/types/layout.js.map +1 -1
  73. package/lib/types/migrate.d.ts +1 -1
  74. package/lib/types/migrate.js +2 -2
  75. package/lib/types/migrate.js.map +1 -1
  76. package/lib/types/model-text.d.ts +99 -0
  77. package/lib/types/model-text.d.ts.map +1 -0
  78. package/lib/types/model-text.js +103 -0
  79. package/lib/types/model-text.js.map +1 -0
  80. package/lib/types/present.d.ts +239 -0
  81. package/lib/types/present.d.ts.map +1 -0
  82. package/lib/types/present.js +255 -0
  83. package/lib/types/present.js.map +1 -0
  84. package/lib/types/recall-rank.d.ts +37 -0
  85. package/lib/types/recall-rank.d.ts.map +1 -1
  86. package/lib/types/recall-rank.js +41 -1
  87. package/lib/types/recall-rank.js.map +1 -1
  88. package/lib/types/runtime.d.ts +564 -31
  89. package/lib/types/runtime.d.ts.map +1 -1
  90. package/lib/types/runtime.js +2645 -292
  91. package/lib/types/runtime.js.map +1 -1
  92. package/lib/types/settings-schema.d.ts +4 -2
  93. package/lib/types/settings-schema.d.ts.map +1 -1
  94. package/lib/types/settings-schema.js +2 -0
  95. package/lib/types/settings-schema.js.map +1 -1
  96. package/lib/types/source-images.d.ts +118 -0
  97. package/lib/types/source-images.d.ts.map +1 -0
  98. package/lib/types/source-images.js +288 -0
  99. package/lib/types/source-images.js.map +1 -0
  100. package/lib/types/types.d.ts +356 -28
  101. package/lib/types/types.d.ts.map +1 -1
  102. package/lib/types/types.js +112 -18
  103. package/lib/types/types.js.map +1 -1
  104. package/lib/types/wiki-finalize.d.ts +140 -0
  105. package/lib/types/wiki-finalize.d.ts.map +1 -0
  106. package/lib/types/wiki-finalize.js +269 -0
  107. package/lib/types/wiki-finalize.js.map +1 -0
  108. package/lib/types/wiki-types.d.ts +10 -2
  109. package/lib/types/wiki-types.d.ts.map +1 -1
  110. package/lib/types/wiki-types.js +27 -30
  111. package/lib/types/wiki-types.js.map +1 -1
  112. package/package.json +4 -3
  113. package/preset/tita-data-agent/agent.cordis.yml +25 -10
  114. package/src/client/DirectoryPicker.tsx +12 -12
  115. package/src/client/DrawerSection.tsx +18 -0
  116. package/src/client/InfoHint.tsx +21 -8
  117. package/src/client/IntentTab.tsx +20 -26
  118. package/src/client/KnowledgeBaseTab.tsx +458 -158
  119. package/src/client/KnowledgePanel.tsx +44 -10
  120. package/src/client/KnowledgeStepRail.tsx +3 -0
  121. package/src/client/KnowledgeToolRow.tsx +331 -0
  122. package/src/client/NodeSettings.tsx +20 -22
  123. package/src/client/PreviewSections.tsx +178 -43
  124. package/src/client/RecallPanel.tsx +418 -127
  125. package/src/client/TypeDefinitionDrawer.tsx +154 -54
  126. package/src/client/WikiCardReview.tsx +266 -0
  127. package/src/client/WikiFlowCard.tsx +57 -95
  128. package/src/client/WikiTypeEditor.tsx +197 -95
  129. package/src/client/arch/AgentArchitectureGraph.tsx +30 -14
  130. package/src/client/arch/NodeLogicPanel.tsx +6 -3
  131. package/src/client/arch/model.ts +164 -26
  132. package/src/client/architecture.css +25 -10
  133. package/src/client/architecture.tsx +66 -41
  134. package/src/client/asset-images.ts +125 -0
  135. package/src/client/attribution/AttributionGraph.tsx +481 -34
  136. package/src/client/attribution/AttributionTab.tsx +501 -106
  137. package/src/client/attribution/CaseView.tsx +9 -14
  138. package/src/client/attribution/GraphStage.tsx +25 -18
  139. package/src/client/attribution/RunView.tsx +27 -9
  140. package/src/client/attribution/StageChrome.tsx +280 -140
  141. package/src/client/attribution/graph-options.ts +611 -47
  142. package/src/client/attribution/model.ts +559 -30
  143. package/src/client/convert/ConvertPanel.tsx +36 -4
  144. package/src/client/doc-page.css +134 -0
  145. package/src/client/doc-page.tsx +242 -0
  146. package/src/client/index.ts +18 -0
  147. package/src/client/panel-state.ts +105 -0
  148. package/src/client/problem-domains.ts +1 -1
  149. package/src/client/request.ts +36 -0
  150. package/src/client/styles.css +397 -54
  151. package/src/client/useFitScale.ts +15 -2
  152. package/src/client/wiki-type-groups.ts +86 -0
  153. package/src/convert/host.ts +68 -9
  154. package/src/convert/ingest.ts +61 -18
  155. package/src/convert/types.ts +2 -2
  156. package/src/diagnosis/cards.ts +120 -50
  157. package/src/diagnosis/run.ts +251 -0
  158. package/src/diagnosis/schema.ts +67 -26
  159. package/src/diagnosis/seed-source.ts +29 -4
  160. package/src/diagnosis/seed.ts +187 -85
  161. package/src/diagnosis/store.ts +310 -173
  162. package/src/diagnosis/types.ts +181 -16
  163. package/src/document-page.ts +51 -0
  164. package/src/extract-model.ts +254 -99
  165. package/src/extract.ts +35 -3
  166. package/src/fs-atomic.ts +41 -0
  167. package/src/index.ts +499 -62
  168. package/src/intent/types.ts +1 -1
  169. package/src/knowledge-bases.ts +50 -49
  170. package/src/layout.ts +4 -4
  171. package/src/migrate.ts +2 -2
  172. package/src/model-text.ts +126 -0
  173. package/src/present.ts +367 -0
  174. package/src/recall-rank.ts +57 -2
  175. package/src/runtime.ts +2802 -293
  176. package/src/settings-schema.ts +2 -0
  177. package/src/source-images.ts +326 -0
  178. package/src/types.ts +446 -45
  179. package/src/wiki-finalize.ts +310 -0
  180. package/src/wiki-types.ts +28 -32
@@ -1,7 +1,16 @@
1
- import { type CardSchemaIssue, type DocumentVersion, type Envelope, type KnowledgeCard, type KnowledgeDocument, type KnowledgeDocumentDetail, type KnowledgeStatus, type RecallMode, type RecallResult, type RebuildReport, type RuntimeKnowledgeConfig, type SchemaType, type SourceRaw, type SourceRawRead, type SourceType, type SourceTypeCatalog, type SourceTypeDef, type WikiCardVersion, type WikiSchemaMode, type WikiTypeCatalog, type WikiTypeDef } from './types.js';
1
+ import { type CardAnnotation, type CardReview, type CardSchemaIssue, type DocumentVersion, type Envelope, type KnowledgeAssetRef, type KnowledgeCard, type KnowledgeDocument, type KnowledgeDocumentDetail, type KnowledgeStatus, type RecallMode, type RecallResult, type RebuildReport, type RuntimeKnowledgeConfig, type SchemaType, type SourceProvenance, type SourceRaw, type SourceRawRead, type SourceType, type SourceTypeCatalog, type SourceTypeDef, type WikiCardVersion, type WikiTypeCatalog, type WikiTypeDef } from './types.js';
2
+ import { DiagnosisStore } from './diagnosis/store.js';
2
3
  import { type ModelCall } from './extract-model.js';
4
+ import { type RelatedEntry } from './wiki-finalize.js';
3
5
  import type { LineageContext, LineageRef } from 'dsh-data-contracts/refs';
4
- declare const LAYOUT: readonly ["01_source_truth", "02_wiki", "03_compiled", "05_diagnosis"];
6
+ declare const LAYOUT: readonly ["01_source_raw", "02_wiki", "03_compiled"];
7
+ /**
8
+ * 一次 `import_sources` 能带多少份来源(0045)。
9
+ *
10
+ * 这不是产品配额,是**一次调用能被按住的时长**:编译逐份来源串行抽实体,50 份已经要跑
11
+ * 很久。超了就分批,每批各自编译 —— 而不是让一次调用变成没人知道什么时候结束的东西。
12
+ */
13
+ export declare const MAX_IMPORT_SOURCES = 50;
5
14
  /**
6
15
  * Both helpers delegate to the shared envelope constructors: the five-key shape
7
16
  * and the "warnings ⇒ partial" rule live in `dsh-data-contracts`,
@@ -10,6 +19,70 @@ declare const LAYOUT: readonly ["01_source_truth", "02_wiki", "03_compiled", "05
10
19
  */
11
20
  export declare function success<T>(data: T, warnings?: string[]): Envelope<T>;
12
21
  export declare function failure<T>(data: T, code: string, message: string, retryable?: boolean, suggestion?: string): Envelope<T>;
22
+ /**
23
+ * 把这次新抽到的正文**追加**进已有卡片的正文。
24
+ *
25
+ * 这是「wiki 编译第二条链路」的落点:实体已经在 wiki 里 → 在原卡片上追加;不在 →
26
+ * 新建一张。追加而不是覆盖,是因为覆盖会把别的来源贡献的内容删掉;而**丢弃**
27
+ * (第一版的行为)更糟 —— 来源改写了,卡片却永远停在第一次抽取的样子。
28
+ *
29
+ * 已经在正文里出现过的那一段不再追加:同一份来源重建、或同一段被两个候选命中时,
30
+ * 卡片不该越编译越长。判重按整段文本,所以它是幂等的。
31
+ */
32
+ /** 批量导入里成功写入的一条来源(0045)。`index` 回指输入数组,调用方能对上号。 */
33
+ export interface ImportedSourceReport {
34
+ index: number;
35
+ sourceId: string;
36
+ title?: string;
37
+ path: string;
38
+ bytes: number;
39
+ }
40
+ /** 批量导入里失败的一条。**带着原因回来**,因为局部失败要求调用方能只重试那几条。 */
41
+ export interface ImportFailure {
42
+ index: number;
43
+ title?: string;
44
+ code: string;
45
+ message: string;
46
+ }
47
+ /**
48
+ * 批量导入顺带那次编译的结果。
49
+ *
50
+ * 只带**能被人读懂**的几个数:新建 / 并入 / 复用 / 重编 / 重抽 / 跳过 / 移除、索引版本、
51
+ * 向量那一段的状态。整份 `RebuildReport` 有三十多个字段(指纹、计数、诊断),
52
+ * 而这里要回答的是「刚才那一下让库里多了什么」。
53
+ */
54
+ export interface ImportCompileReport {
55
+ cardsCreated: number;
56
+ cardsUpdated: number;
57
+ cardsReused?: number;
58
+ notesReused?: number;
59
+ notesRecompiled?: number;
60
+ sourcesExtracted?: number;
61
+ sourcesSkipped?: number;
62
+ cardsRemoved?: number;
63
+ embedding?: string;
64
+ version?: number;
65
+ }
66
+ /** 一次 `import_sources` 的账本:写了哪些、哪些没写成、顺带编译出了什么。 */
67
+ export interface ImportSourcesResult {
68
+ written: ImportedSourceReport[];
69
+ failed: ImportFailure[];
70
+ /** 输入里一共有几份 —— 成功数 + 失败数 = 它,除非整批在写入前就被拒了。 */
71
+ attempted: number;
72
+ /** `compile: false` 时是 `null`:明确表示「这次没编译」,而不是「编译了但没数字」。 */
73
+ compile: ImportCompileReport | null;
74
+ }
75
+ /**
76
+ * 抽屉里的原文预览**只发**正文的前这么多字符。
77
+ *
78
+ * 依据是实跑反馈:一份几十万字的转换文档,抽屉只渲染前 4000 字符(更长的在
79
+ * 380px 宽的抽屉里没有阅读意义),而接口原来把**整份正文**发过去 —— 传输、JSON 解析、
80
+ * 塞进 React state 全按全文的量级付费,于是「打开弹层要等半天」。上面那段封顶之后,
81
+ * 弹层的耗时与文档大小**脱钩**;要读全文走独立整页(分页取,见 `documentContentPage`)。
82
+ *
83
+ * 4000 是用户明确要的阈值,所以它是一个具名常量而不是散在各处的字面量。
84
+ */
85
+ export declare const SOURCE_PREVIEW_CHARS = 4000;
13
86
  export declare function parseMarkdown(source: string): {
14
87
  frontmatter: Record<string, unknown>;
15
88
  content: string;
@@ -37,7 +110,6 @@ export type SourceTypeProvider = () => SourceTypeDef[];
37
110
  /** Wiki 卡片类型注册表的读取入口(0024):类型定义 + schema 生效方式。 */
38
111
  export interface WikiSchemaSnapshot {
39
112
  types: WikiTypeDef[];
40
- mode: WikiSchemaMode;
41
113
  }
42
114
  export type WikiSchemaProvider = () => WikiSchemaSnapshot;
43
115
  export interface KnowledgeRuntimeOptions {
@@ -72,6 +144,29 @@ export interface KnowledgeRuntimeOptions {
72
144
  * 不能让「没有模型就跑不动」成为新的前提。
73
145
  */
74
146
  modelCall?: ModelCall;
147
+ /**
148
+ * **按需解析**模型接缝(0044)。
149
+ *
150
+ * 为什么不能只用上面那个 `modelCall`:它是**构造时读一次**的值,而插件 apply 的
151
+ * 时刻早于 `agent-default-model` 那个 bundle 挂载 —— profile 的组合顺序是
152
+ * 「package.json 的 bundles → cordis.patch.yml」,而 `agent-default-model` 在 patch 层。
153
+ * 于是那一次读到的永远是 undefined,而它**再也不会变**,模型抽取一次都不跑。
154
+ *
155
+ * 实跑证据(2026-09-27):默认模型配的是 `opencode-go / deepseek-v4.1-flash`,而编译出来的
156
+ * 卡片 `metadata.extraction` 是 `pass0`(规则路径),正文是整篇文章 —— 也就是用户报的
157
+ * 「编译只是按目录截取」。静默降级了很久,因为没有任何一处说「模型抽取没生效」。
158
+ *
159
+ * 所以两者都给:`modelCall` 供测试注入固定值,`modelSeam` 供插件层延迟解析,
160
+ * **并且必须能说出拿不到的原因** —— `why` 会原样进 `rebuild` 的 warnings。
161
+ * 一个说不出理由的降级,等于没有降级信息。
162
+ */
163
+ modelSeam?: () => ModelSeam;
164
+ }
165
+ /** 一次模型接缝解析的结果。两件东西都要有交代:能不能用,以及不能用时为什么。 */
166
+ export interface ModelSeam {
167
+ call?: ModelCall;
168
+ /** 拿不到 `call` 的原因,人话。会被原样写进 `rebuild` 的 warnings。 */
169
+ why?: string;
75
170
  }
76
171
  export declare class KnowledgeRuntime {
77
172
  /** 当前生效的配置。volatile 改动会就地合并进来(见 `updateConfig`),所以它不是只读的。 */
@@ -103,8 +198,16 @@ export declare class KnowledgeRuntime {
103
198
  * 写入与删除后必须失效(见 `addSource` / `deleteDocument`)。
104
199
  */
105
200
  private sourceIndex;
201
+ /** `path → sourceId`:目标路径被谁占着(0051 的重名判定)。与 `sourceIndex` 同一次遍历建。 */
202
+ private sourcePathOwner;
106
203
  /** 段内布局整理是否已经跑过(每个 runtime 一次)。 */
107
204
  private segmentsOrganized;
205
+ /**
206
+ * 一次性迁移里**没有静默处理掉**的事(改名冲突、迁移后比对不一致)。
207
+ *
208
+ * 它必须能被界面读到:迁移失败而没人知道,等价于数据已经丢了。
209
+ */
210
+ private migrationProblems;
108
211
  private vectorIndex;
109
212
  private vectorError;
110
213
  private embeddingWorker;
@@ -134,9 +237,18 @@ export declare class KnowledgeRuntime {
134
237
  * `memoryPath`,守卫换成「不许跑出 memoryDir」。
135
238
  */
136
239
  private readonly memoryDir;
137
- /** #3b 的模型调用接缝;undefined = 退回纯规则抽取。 */
138
- private readonly modelCall;
240
+ /** #3b 的模型调用接缝;undefined = 退回纯规则抽取。测试注入的固定值。 */
241
+ private readonly staticModelCall;
242
+ /** 每次真正要用模型时问一遍的那个接缝 provider(0044)。为空则退回 `staticModelCall`。 */
243
+ private readonly modelSeamProvider?;
139
244
  constructor(rootDir: string, config?: Partial<RuntimeKnowledgeConfig>, options?: KnowledgeRuntimeOptions);
245
+ /**
246
+ * 现在能不能用模型抽实体。
247
+ *
248
+ * **每次现问**,不是在构造时定死:插件 apply 的时刻早于 `agent-default-model` 挂载,
249
+ * 那时问一次只会得到 undefined,而定死之后就再也不会变(见 `modelCallProvider`)。
250
+ */
251
+ private modelSeamNow;
140
252
  /**
141
253
  * 就地应用一份新的运行时配置(0.1.7 的 volatile 改动走这条路)。
142
254
  *
@@ -157,20 +269,124 @@ export declare class KnowledgeRuntime {
157
269
  /** 当前生效的卡片 schema:类型定义 + 生效方式。 */
158
270
  wikiSchema(): WikiSchemaSnapshot;
159
271
  /**
160
- * Agent 与设置面板共用的类型目录:注册表 + 生效方式 + 内置只读判定规则 +
161
- * 现有卡片的三层校验结果(`report` 模式下这是"待补"清单)。
272
+ * Agent 与设置面板共用的类型目录:注册表 + 内置只读判定规则 +
273
+ * 现有卡片的三层校验结果(`report` 模式下这是"待补"清单)+ **review 进度**。
274
+ *
275
+ * review 计数挂在这里而不是另开一路,是因为 review 就发生在这一屏(「知识库 → Wiki」)——
276
+ * 与 lint 待补清单是同一处,不新造第二套界面。
277
+ *
278
+ * 0044:`proposals`(待审提议条数)改成 `reviews`。队列退役之后「还有几条等人确认」
279
+ * 这个问题不再有意义 —— 卡片一编译就入库了,剩下的问题是「入库的这些里,我看过几条」。
162
280
  */
163
281
  wikiTypeCatalog(): Promise<Envelope<WikiTypeCatalog>>;
164
- /** 对已落库的卡片跑三层校验。不写盘,只报告。 */
282
+ /**
283
+ * 智能归因图谱的存储句柄。
284
+ *
285
+ * 它由 runtime 自己构造(而不是由 index.ts 注入),因为 `rebuild` 要产出
286
+ * `03_compiled/diagnosis_graph.json` —— 那次产出必须和 `knowledge_graph.json`
287
+ * 在同一次卡片遍历的语义下发生,否则两份产物会来自两个不同版本的卡片。
288
+ */
289
+ private diagnosis?;
290
+ diagnosisStore(): DiagnosisStore;
291
+ /**
292
+ * 把一批卡片**直接写进 `02_wiki`**(0044)。
293
+ *
294
+ * 0040 v1.5 的这道门原来是「提一条提议 → 人在面板确认 → 才落卡」,理由是类型层是收敛的
295
+ * 那一层,错一条所有现象的排序一起错。0044 把方向反过来:**入库在前、review 在后**。
296
+ * 判据是实践中的两个事实:
297
+ *
298
+ * 1. **确认门把知识挡在了门外。** 抽到的东西停在 `memory/card-proposals/` 里等人点,
299
+ * 而人不会一条条点。用户看到的现象就是「编译没生效」——文档导进去了,wiki 是空的。
300
+ * 2. **review 的价值不需要排队来实现。** 「这张卡片对不对」可以在它已经可召回之后回答;
301
+ * 而「未 review 就不许进来」等于把「暂时没人看的正确知识」和「错误知识」一起丢掉。
302
+ *
303
+ * 于是现在的形状是:入库 → 召回得到(评分稍低)→ 人 review → 评分抬高 / 备注异常点。
304
+ * 拒绝某条知识的正确做法是 `set_knowledge_status(deprecated|problem)`,它会留痕,
305
+ * 也能被撤回 —— 比「从来没见过它」更容易纠正。
306
+ *
307
+ * 与编译路径的差别只有一条:这里允许调用方指定 `entityId` 与正文,编译路径由候选推导。
308
+ * 两者共用 `writeCard`,所以校验、版本快照、备注保留的行为完全一致。
309
+ */
310
+ writeCards(input: {
311
+ sourceId: string;
312
+ cards: Array<{
313
+ action?: 'create' | 'update' | 'merge';
314
+ entityType: string;
315
+ entityId: string;
316
+ title: string;
317
+ summary?: string;
318
+ content: string;
319
+ sourceRefs?: string[];
320
+ metadata?: Record<string, unknown>;
321
+ }>;
322
+ lineage?: LineageContext;
323
+ }): Promise<Envelope<{
324
+ written: number;
325
+ ids: string[];
326
+ rejected: string[];
327
+ warnings: string[];
328
+ }>>;
329
+ /**
330
+ * 给一张卡片加一条**人工备注**,或撤掉一条(0044)。
331
+ *
332
+ * 这是「wiki 不可修改」的对偶:正文改不了,但**判断改得了**。「这份文档的数字和卡片
333
+ * 对不上」「这个口径在另一份文档里是反的」——这些都是关于卡片的判断,不是卡片的内容,
334
+ * 所以它们落在 frontmatter 的 `annotations` 里,编译永远不碰。
335
+ *
336
+ * `kind: 'anomaly'` 会把卡片的有效评分压到 `ANOMALY_SCORE`(见 `effectiveReviewScore`):
337
+ * 异常卡片照样召回得到,只是排在后面 —— 而不是被藏起来。藏起来的问题在于没人会再看到它,
338
+ * 于是也就没人会去修。
339
+ */
340
+ annotateCard(input: {
341
+ entityId: string;
342
+ entityType?: SchemaType;
343
+ /** 不传 `note` 时必须是「撤销一条」:给出 `removeId`。 */
344
+ kind?: 'note' | 'anomaly';
345
+ note?: string;
346
+ author?: string;
347
+ sourceRef?: string;
348
+ removeId?: string;
349
+ lineage?: LineageContext;
350
+ }): Promise<Envelope<{
351
+ entityId: string;
352
+ annotations: CardAnnotation[];
353
+ reviewScore: number;
354
+ }>>;
355
+ /**
356
+ * review 一张卡片(0044)。**它不是准入闸门** —— 卡片在 review 之前就已经入库、可召回了。
357
+ *
358
+ * `review` 把评分抬到 `REVIEWED_SCORE_FLOOR`,`unreview` 放回出厂分。两个动作都可逆:
359
+ * review 是一个可以被推翻的判断,不是一次性的凭证。
360
+ */
361
+ reviewCard(input: {
362
+ entityId: string;
363
+ entityType?: SchemaType;
364
+ action: 'review' | 'unreview';
365
+ reviewer?: string;
366
+ lineage?: LineageContext;
367
+ }): Promise<Envelope<{
368
+ entityId: string;
369
+ review: CardReview;
370
+ reviewScore: number;
371
+ status: KnowledgeStatus;
372
+ }>>;
373
+ /**
374
+ * 对已落库的卡片跑三层校验。不写盘,只报告。
375
+ *
376
+ * 0049 补齐了 `reference` 那一层:正文里的 `[[类型/名称]]` 指向一张不存在的卡片,
377
+ * 就是一条断链。此前这一层只有类型定义里的名字,没有任何产生者 —— 而「引用错说明
378
+ * 图断了」正是它要回答的问题(format 错说明没填完,语义错说明这条知识没用)。
379
+ */
165
380
  lintCards(): Promise<Array<CardSchemaIssue & {
166
381
  entityId: string;
167
382
  entityType: string;
168
383
  }>>;
169
384
  /**
170
- * `write_knowledge_card` 的类型闸门 + 三层校验。
385
+ * 类型闸门 + 三层校验。
171
386
  *
172
- * 返回 `{ type, issues }` 或一个失败信封。是否因 issues 拒绝写入由 `mode` 决定:
173
- * `strict` 直接拒绝,`report` 把问题写进卡片并回报——存量卡片因此不会被升级挡在门外。
387
+ * 返回 `{ type, issues }` 或一个失败信封(只可能因为**未知类型** —— 没有类型定义就连
388
+ * 「缺什么」都说不出来)。issues **不决定写不写**:0044 起卡片一律直接入库,问题写进
389
+ * `metadata.schema_issues` 由面板显示成「待补」。原来的 `strict` / `report` 开关一并撤掉了。
174
390
  */
175
391
  private cardSchemaGate;
176
392
  /** 当前生效的来源分类注册表(含计算字段 `sourceCount`)。 */
@@ -187,7 +403,43 @@ export declare class KnowledgeRuntime {
187
403
  */
188
404
  private sourceTypeGate;
189
405
  directory(name: typeof LAYOUT[number]): string;
406
+ /**
407
+ * 0051 之前的段目录路径。
408
+ *
409
+ * 它**不在 `LAYOUT` 里**:`LAYOUT` 是「应该建出来的目录」清单,而老段名只是要读、
410
+ * 要搬走的东西 —— 把它加进 `LAYOUT`,`ensureLayout` 每次启动都会把那个空目录建回来。
411
+ */
412
+ private legacySourceDir;
190
413
  ensureLayout(): Promise<void>;
414
+ /**
415
+ * 0051:`01_source_truth/` → `01_source_raw/`。
416
+ *
417
+ * **整棵树一次 `rename`**,不逐文件搬:`_assets/` 就在那个目录里,逐文件搬会把
418
+ * 图片落下的概率变成「每个文件各一次」。一次 rename 是原子的,要么全过来要么全没动。
419
+ *
420
+ * 两种中间态都要处理:
421
+ * - 目标不存在 → 直接 rename;
422
+ * - 两个目录都在(上一次搬到一半)→ 逐项合并:目标里没有的搬过去,两边都有的
423
+ * (`_assets` 与分类目录)递归合并,最后删掉空壳。
424
+ *
425
+ * 失败不回滚、不抛:`loadSources` 两个段名都扫,搬到一半的库照常可读,
426
+ * 下一次启动接着搬。
427
+ */
428
+ private migrateSourceTree;
429
+ /**
430
+ * 0051:文件名改成标题。
431
+ *
432
+ * 只动**名字不对**的那些(可重入),而且**只 rename,不改内容** —— 正文里那些
433
+ * `../../01_source_truth/_assets/…` 的老引用一个字节都不动,解析器认两个段名
434
+ * (见 `assetRefCandidates`)。改内容会造内容版本、让卡片重编重编码,代价远大于
435
+ * 一个字符串好不好看。
436
+ *
437
+ * 目标路径被别的来源占着时**不覆盖**:加内容哈希后缀并记一条,让人去改标题
438
+ * (与 `addSource` 的默认拒收同一条纪律,只是这里不能拒 —— 那是历史数据)。
439
+ */
440
+ private migrateSourceFileNames;
441
+ /** 这份文件自己声明了合法分类吗(`migrateSourceFileNames` 只搬声明过的)。 */
442
+ private hasDeclaredSourceType;
191
443
  /**
192
444
  * 0030:把平铺的 `<段>/*.md` 搬进 `<段>/<source_type>/*.md`。
193
445
  *
@@ -197,6 +449,23 @@ export declare class KnowledgeRuntime {
197
449
  * `loadSources` 两种布局都能读,搬不动不影响正确性)。
198
450
  */
199
451
  private migrateSourceTruthLayout;
452
+ /**
453
+ * 0048:把存量 SourceRaw 的 front matter 改成 OKF v0.2 的形状。
454
+ *
455
+ * 改三件事,都只搬**已经存在**的值,不猜、不造:
456
+ *
457
+ * 1. `source_type` → `type`(OKF 唯一必填键)。旧键删掉 —— 同一个事实留两份,
458
+ * 迟早会出现「改了这份、那份没改」。读取侧两个键都认(见 `sourceTypeOf`),
459
+ * 所以迁移没跑到、或别的库还是旧形状,都不影响可用性。
460
+ * 2. `metadata.source_url` → 顶层 `resource`(OKF 的「底层资产地址」)。
461
+ * 3. 补 `generated{by,at}` 与 `sources[]`:`by` 取自 `metadata.parser`(谁解析的),
462
+ * `at` 取自 `created_at`(这份文件什么时候写出来的)。**作者与发布时间补不出来**
463
+ * —— 0048 之前适配器把它们抽出来就扔了,这里如实留空,不编一个。
464
+ *
465
+ * 正文一个字节都不动,也不制造内容版本(版本只认正文变化)。整个迁移可重入:
466
+ * 已经有 `type` 的文件直接跳过。
467
+ */
468
+ private migrateSourceFrontMatter;
200
469
  /**
201
470
  * 0030:删掉已经被撤掉的 `00_schema` 段。
202
471
  *
@@ -204,7 +473,55 @@ export declare class KnowledgeRuntime {
204
473
  * 有人放了文件,那就不属于「没用的空文件夹」了 —— 那种情况下留着并让人自己判断,
205
474
  * 比替他们删掉安全。
206
475
  */
207
- private dropLegacySchemaSegment;
476
+ /**
477
+ * 清掉**只装了旧版本自己生成的东西**的退役段。
478
+ *
479
+ * `00_schema` 从来没有一行代码写过东西,所以「空」就是它的全部历史形态 —— 空就删。
480
+ *
481
+ * `05_diagnosis` 不一样:旧版本在里面铺过 `templates/*.md` 与一份 README,那两样是
482
+ * 可以重建的(模板由类型定义派生、README 是说明),删掉不丢知识。但同一个目录里
483
+ * 可能躺着**真正的历史快照**(`*.jsonl` / `cases.json`),那是只读的迁移输入,
484
+ * 一个字节都不能动。所以判据是**文件清单**而不是「空不空」:只认得旧版本自己写的
485
+ * 那些名字时,才删;出现任何别的东西就整个留着。
486
+ */
487
+ private dropRetiredSegments;
488
+ private dropSegmentIfEmpty;
489
+ /**
490
+ * 0040 §3.2 的一次性迁移:`01_source_raw/ai_chat_message/` → `agent_chat_message/`。
491
+ *
492
+ * 改名不能只改注册表:`01_source_raw/` 是**按分类分目录**的,存量来源的
493
+ * frontmatter 里写着 `source_type: ai_chat_message`。只改注册表会有两个后果 ——
494
+ * 旧目录里那些来源变成一个「注册表里没有的分类」(界面上显示成未知分类),
495
+ * 而旧分类因为还有来源在,永远删不掉。
496
+ *
497
+ * 三条纪律:
498
+ * 1. **逐份搬,不整体 rename 目录。** 目标里可能已经有同名文件(用户在新版本里
499
+ * 重新录过同一份来源),整体搬会覆盖它。
500
+ * 2. **同名时比对正文**:一致就删掉旧的那份(同一份东西,只是目录名变了);
501
+ * 不一致就**两边都留**,并把冲突报给界面 —— 静默覆盖等于丢数据。
502
+ * 3. **迁移无损才认**:写完读回来比对 frontmatter 与正文,不一致就放弃这一份。
503
+ */
504
+ private migrateLegacySourceTypeRename;
505
+ /**
506
+ * 0040 §4 的一次性迁移:两次改名(`check` → `check_point`、`failure-mode` →
507
+ * `failure_reason`)的存量卡片。
508
+ *
509
+ * 两件事一起做,因为它们是同一次改名的两个面:
510
+ * 1. **卡片类型**:`02_wiki/diagnosis/check/` → `check-point/`,
511
+ * frontmatter 的 `entity_type` 一起改;
512
+ * 2. **结构化块里的字段名**:`failure_mode` → `failure_reason`。这一条与卡片类型
513
+ * 无关 —— `diagnosis/hypothesis` 卡片的类型没变,但它块里那个字段变了。
514
+ * 只按目录过滤的话,假设卡片上的旧字段名会永远留在磁盘上,而读回来是「少一个
515
+ * 必填字段」,正文里却明明写着它。
516
+ *
517
+ * 读取端本来也认别名(`cards.parseCard`),但**只在内存里认是不够的**:`writeCard`
518
+ * 按 `entity_type` 决定写哪个目录,改一张旧卡片就会在 `check-point/` 下多写一份,
519
+ * 而旧目录里那份还在 —— 同一张卡片变成两张,读图时看谁先被遍历到。
520
+ *
521
+ * 用 `writeCard` 而不是自己拼 frontmatter:它是唯一知道卡片信封长什么样、并且会
522
+ * 顺手做版本快照与三层校验的地方。
523
+ */
524
+ private migrateLegacyDiagnosisCards;
208
525
  private loadVectorIndex;
209
526
  private vectorWorkerPath;
210
527
  private encodeWithOllama;
@@ -238,6 +555,48 @@ export declare class KnowledgeRuntime {
238
555
  /** 记忆段的真实位置(面板与诊断要显示它,而不是让人以为还在知识树里)。 */
239
556
  get memoryRoot(): string;
240
557
  stats(): Promise<Record<string, unknown>>;
558
+ /**
559
+ * 批量写入来源 + **一次**编译(0045)。
560
+ *
561
+ * ## 为什么值得单独一个方法,而不是让调用方循环 `add_source` + `rebuild_knowledge`
562
+ *
563
+ * 三件事只有放在一起才对:
564
+ *
565
+ * 1. **一次审批。** Agent 侧写入都要过 DSH 的审批闸门(`tools/pre-execute` 里那几个
566
+ * 写工具)。十份文档 = 十次弹窗,而人真正要同意的是「把这十份导进去」这一件事。
567
+ * 2. **编译只跑一次。** 每写一份就 `rebuild` 一次,第 10 份文档要付 10 次抽取的钱,
568
+ * 而前 9 次的结果当场就被下一次覆盖。
569
+ * 3. **失败要局部化。** 第 7 份文档格式不对,不该让前 6 份白写、后 3 份不写。
570
+ * 所以逐条记录成败,最后一次性汇报 —— 成功的已经在库里,失败的带着原因回来,
571
+ * 调用方可以只重试那几条。
572
+ *
573
+ * ## 为什么全部失败时返回 failure 而不是带 warning 的 success
574
+ *
575
+ * 「10 份全没进去」与「10 份都进去了,其中 1 份顺带报了个警告」是两件完全不同的事,
576
+ * 用同一种信封表达,模型会把前一种当成后一种继续往下走。所以一份都没写成时如实失败,
577
+ * 每条的原因仍然在 `sources[].message` 里。
578
+ */
579
+ importSources(input: {
580
+ sources: Array<{
581
+ sourceType: SourceType;
582
+ title?: string;
583
+ content: string;
584
+ externalId?: string;
585
+ metadata?: Record<string, unknown>;
586
+ allowDuplicateTitle?: boolean;
587
+ }>;
588
+ /** 默认编译(`only: 'wiki'`)。关掉它就只做写入,留给调用方自己决定何时编译。 */
589
+ compile?: boolean;
590
+ lineage?: LineageContext;
591
+ }): Promise<Envelope<ImportSourcesResult>>;
592
+ /**
593
+ * 写入一份 SourceRaw。
594
+ *
595
+ * `provenance` 是 OKF v0.2 那一组(`description` / `resource` / `generated` /
596
+ * `sources[]` / `tags`),它们**写成真正的顶层键**而不是塞进 `metadata` 那个 JSON
597
+ * 字符串里(0048):元数据要能被人读、被 `grep` 到、被外部工具按公开规范读走。
598
+ * `metadata` 只留类型专属的扩展键。
599
+ */
241
600
  addSource(input: {
242
601
  sourceType: SourceType;
243
602
  title?: string;
@@ -246,6 +605,12 @@ export declare class KnowledgeRuntime {
246
605
  metadata?: Record<string, unknown>;
247
606
  sourceId?: string;
248
607
  lineage?: LineageContext;
608
+ provenance?: SourceProvenance;
609
+ /**
610
+ * 允许与已有的来源同标题(0051)。默认 `false`:重名是错误,拒收并要求改标题。
611
+ * 传 `true` 时落成 `<标题>-<内容哈希前 6 位>.md`,并回一条 warning。
612
+ */
613
+ allowDuplicateTitle?: boolean;
249
614
  }): Promise<Envelope<{
250
615
  sourceId: string;
251
616
  path: string;
@@ -266,28 +631,68 @@ export declare class KnowledgeRuntime {
266
631
  * 不会引入路径穿越。
267
632
  */
268
633
  private sourcePath;
269
- /** 递归扫 SourceRaw 段,建 `docSlug → path` 索引;两种布局都覆盖。 */
634
+ /**
635
+ * `docSlug → path` 索引;两种布局都覆盖。
636
+ *
637
+ * 0051 起它**由 `loadSources` 建**(那一次遍历本来就要读每个文件的 front matter,
638
+ * 而文件名不再是身份,只能从 front matter 里拿)。这里只是把「还没建」补上。
639
+ */
270
640
  private sourceIndexMap;
641
+ /** 这个路径现在被哪一份来源占着(0051:重名判定的依据)。 */
642
+ private sourceIdAtPath;
271
643
  /** 已知文档 id 的落盘位置;不存在时 `undefined`(调用方各自决定怎么报错)。 */
272
644
  private sourcePathOf;
273
645
  /**
274
646
  * Assets a document really has on disk.
275
647
  *
276
- * Preferred source of truth is `metadata.assets` (knowledge-root-relative
277
- * paths written by `convert/ingest.ts`). Documents written by hand fall back
278
- * to parsing local markdown image links. A link is resolved **knowledge-root
279
- * first, then relative to the document's own directory**: the convert ingest
280
- * seam re-bases assets to `_assets/<bucket>/<name>` (root-relative), while
281
- * hand-written docs typically use a sibling path. Only existing files are
282
- * returned, so `assetCount` is never an over-count.
648
+ * Preferred source of truth is `metadata.assets` (paths written by
649
+ * `convert/ingest.ts`). Documents written by hand fall back to parsing local
650
+ * markdown image links. Both go through {@link assetRefCandidates}, so a
651
+ * document's asset list and a card's `imagePaths` answer the same question the
652
+ * same way. Only existing files are returned, so `assetCount` is never an
653
+ * over-count.
283
654
  */
284
655
  private assetsOf;
285
- /** Parse one on-disk SourceRaw file into the detail projection. */
656
+ /**
657
+ * 图片素材的浏览器地址。面板与整页都用它加载图,所以只能有一个拼法。
658
+ */
659
+ private assetUrlOf;
660
+ /**
661
+ * 一张 wiki 卡片的图片素材(0047)。
662
+ *
663
+ * 卡片详情原先只回 `imagePaths` —— **磁盘绝对路径**。客户端拿它加载不了图(浏览器
664
+ * 只认 URL),所以预览抽屉里卡片的图一直不显示;而 SourceRaw 那边早就有 `assets`
665
+ * (`rel` + `url` + `bytes`)。这里把同一种东西补齐,两个预览走同一条路。
666
+ *
667
+ * 只认知识库根目录以内的文件:卡片正文里可能留着别的绝对路径(人手工写的),
668
+ * 那种不该变成一个可下载的 URL。
669
+ */
670
+ assetsOfCard(card: KnowledgeCard): Promise<KnowledgeAssetRef[]>;
671
+ /**
672
+ * Parse one on-disk SourceRaw file into the detail projection.
673
+ *
674
+ * `previewChars` 给了就把正文截到那么长,并把**总长度**如实放进 `contentChars`
675
+ * (见 `SOURCE_PREVIEW_CHARS` 的理由)。不给就是不截断 —— 那是「我确实要全文」的
676
+ * 调用方,目前只有 `readDocumentBody()` 之外的内部路径会这么用。
677
+ */
286
678
  private readDocumentFile;
287
679
  /** 文档列表(新→旧),正文/资产明细留在详情接口里。 */
288
680
  listDocuments(): Promise<KnowledgeDocument[]>;
289
681
  /** 文档详情:正文、frontmatter、资产与 lineage。 */
290
- readDocument(docId: string): Promise<KnowledgeDocumentDetail | undefined>;
682
+ /**
683
+ * 一份文档的详情投影,**正文默认按 `SOURCE_PREVIEW_CHARS` 封顶**。
684
+ *
685
+ * 这是明细接口(抽屉里的预览)读的那一份,所以默认就是「够预览」而不是「全文」:
686
+ * 全文由 `readDocumentBody()` 提供服务,而它只服务独立整页的分页读取。
687
+ */
688
+ readDocument(docId: string, previewChars?: number): Promise<KnowledgeDocumentDetail | undefined>;
689
+ /**
690
+ * 一份文档的**完整正文**(不含 frontmatter),给独立整页分页取用。
691
+ *
692
+ * 每次调用都重读一遍盘:独立整页是「用户滚到哪儿取哪一页」,一次请求一段文本,
693
+ * 谁都不该拿到整份文档 —— 这正是抽屉那一版的问题。缓存留到真的量出来是瓶颈再加。
694
+ */
695
+ readDocumentBody(docId: string): Promise<string | undefined>;
291
696
  /**
292
697
  * 版本历史,新→旧。
293
698
  *
@@ -322,7 +727,25 @@ export declare class KnowledgeRuntime {
322
727
  owner?: string;
323
728
  metadata?: Record<string, unknown>;
324
729
  lineage?: LineageContext;
325
- provenance?: LineageRef[];
730
+ provenance?: LineageRef[]; /**
731
+ * 额外的校验问题(0049)。编译收尾段算出「正文里的 wiki 链接指向不存在的卡片」,
732
+ * 它是 `reference` 那一层的问题 —— 与 format / semantic 一起写进 `metadata.schema_issues`,
733
+ * 卡片抽屉上就会出现一枚「引用」徽标。收尾段不能自己拼 metadata:那样会把写入时
734
+ * 重算的 format / semantic 覆盖掉。
735
+ */
736
+ extraIssues?: CardSchemaIssue[]; /**
737
+ * 人工备注(0044)。**不传 = 保留磁盘上已有的那些**,不是清空 —— 编译路径不传它,
738
+ * 所以「重新编译一次不会抹掉备注」是结构上的保证,而不是靠每个调用方记得带上。
739
+ */
740
+ annotations?: CardAnnotation[]; /** review 结论与评分(0044)。同样:不传 = 保留已有的。 */
741
+ review?: CardReview; /**
742
+ * 谁在写这张卡片。
743
+ *
744
+ * `'manual'` 是**唯一会被正文不可变规则拦下**的来源:已有卡片的正文字节不同 →
745
+ * `WIKI_CARD_IMMUTABLE`。编译(`'compile'`)追加正文、review 与备注写信封字段,
746
+ * 两者都不受这条规则约束 —— 把它们也拦掉,卡片就再也不会长了。
747
+ */
748
+ origin?: 'compile' | 'review' | 'manual' | 'internal';
326
749
  }): Promise<Envelope<{
327
750
  entityId: string;
328
751
  path: string;
@@ -387,10 +810,22 @@ export declare class KnowledgeRuntime {
387
810
  * Fingerprint of everything a wikiNote is compiled *from*.
388
811
  *
389
812
  * Deliberately excludes the fields that are cheap to refresh on a reused note
390
- * (`provenance`, `imagePaths`, `metadata`) and includes the ones that change
391
- * what the note says (`title`, `status`, `content`, `sourceRefs`). A card whose
392
- * fingerprint is unchanged produces byte-identical notes, so reusing them is
393
- * not a shortcut — it is the same answer without the churn.
813
+ * (`provenance`, `imagePaths`, `metadata`, `annotations`) and includes the ones
814
+ * that change what the note says (`title`, `status`, `content`, `sourceRefs`,
815
+ * `review`). A card whose fingerprint is unchanged produces byte-identical
816
+ * notes, so reusing them is not a shortcut — it is the same answer without the
817
+ * churn.
818
+ *
819
+ * 0044 把**有效评分**算进来,因为 note 上确实带着它。少了这一条会踩一个很隐蔽的坑:
820
+ * review 不改 `title` / `content` / `sourceRefs`,于是增量编译判定「语料没变」直接
821
+ * 跳过,`wiki_notes.json` 停在旧评分上 —— 表现就是「review 了,但召回排序一动不动」。
822
+ *
823
+ * 代价可控:评分变了会重编这条 note,但**不会重新编码**。向量指纹(`vectorInputs`)
824
+ * 只哈希 `unitId + text + sourceRefs + images`,而 review 一个字都不改这些 ——
825
+ * 所以重编拿到的还是同一条文本、同一个向量。
826
+ *
827
+ * `annotations` 刻意不进指纹:备注只出现在召回结果附带的 wiki 卡片上,不进 note。
828
+ * 把它算进来会让「加一条备注」触发一次没必要的版本升级。
394
829
  */
395
830
  private cardHashOf;
396
831
  /** Previous `wiki_notes.json` (or the pre-0014 mirror), without touching `this.units`. */
@@ -409,13 +844,13 @@ export declare class KnowledgeRuntime {
409
844
  /** 一次 Pass 0 的真实结果:计数器 + 待装配的关系 + 来源指纹。 */
410
845
  private emptyExtractionOutcome;
411
846
  /**
412
- * Pass 0:对指纹变了的来源做候选抽取 + 归一,并把结果落成 wiki 卡片。
847
+ * Pass 0:对指纹变了的来源做候选抽取 + 归一,并把结果**直接落成 wiki 卡片**。
413
848
  *
414
849
  * 三条硬约束:
415
850
  * 1. **增量**:指纹没变的来源一次都不抽(这是 0025 增量承诺的一部分);
416
851
  * 2. **不重写没变的卡片**:合并只追加 `source_refs`,正文不动,所以不会凭空造
417
852
  * 版本快照(`snapshotCard` 只认正文变化);
418
- * 3. **fail-soft**:一个候选被闸门拒绝、甚至一份来源整个抛错,都只记一笔,
853
+ * 3. **fail-soft**:一个候选写不进去、甚至一份来源整个抛错,都只记一笔,
419
854
  * 绝不带走整次重建 —— 重建失败比少一张卡片严重得多。
420
855
  */
421
856
  private runExtractionStage;
@@ -423,8 +858,27 @@ export declare class KnowledgeRuntime {
423
858
  private readExtractedRelations;
424
859
  /** 落盘关系产物。失败只记一笔:它不该带走整次重建。 */
425
860
  private writeExtractedRelations;
426
- /** 一份来源的抽取结果:新建候选卡片、把证据来源并进已有卡片、收集关系候选。 */
427
- private extractFromSource;
861
+ /**
862
+ * 一份来源的编译结果:**按 wiki schema 抽实体**(新建 / 更新),并收集关系候选。
863
+ *
864
+ * ## 模型先抽实体,规则补关系
865
+ *
866
+ * 规则抽取把「小节标题」当实体身份,实测下来的后果是同一件东西横跨两份文档就
867
+ * 变成两张卡片(`新建 2 / 并入 0`),而且卡片正文是按 `##` 切下来的原文片段 ——
868
+ * 也就是「编译只是按目录截取」。所以实体这一层交给模型:它按类型的必填小节与字段
869
+ * 写正文,并回答「这个实体是不是已经在下表里了」。
870
+ *
871
+ * 但仍然**照跑一遍规则抽取**,理由有两个:关系候选只有规则会抽(模型这一层不产
872
+ * 关系),而且模型不可用时它就是那条降级路径。两者不是二选一。
873
+ *
874
+ * ## 缺信息也抽
875
+ *
876
+ * 0040 那一版在这里拒绝「没有正文」的候选、拒绝「类型未登记」的候选。两条都撤掉了:
877
+ * 编译的价值在于**记下来,后面有涉及到的文档再慢慢补**,而拒绝恰好把这个机会丢掉。
878
+ * 只剩一条硬要求 —— 正文不能是空的(一条什么都说不出的卡片没有东西可召回);
879
+ * 模型路径已经保证只要给了 summary 就有骨架,规则路径的候选天然带正文。
880
+ */
881
+ private compileSource;
428
882
  /**
429
883
  * 把关系候选装配成 wiki↔wiki 的边。
430
884
  *
@@ -433,6 +887,67 @@ export declare class KnowledgeRuntime {
433
887
  * `supported_by`(卡片 → 来源)完全不受影响,它是另一套边。
434
888
  */
435
889
  private applyExtractedRelations;
890
+ /**
891
+ * 收尾段的**正文层**:给卡片补相关块、把断链写成 `reference` 问题。
892
+ *
893
+ * 它必须在 stage 2(切 wikiNotes)**之前**跑:相关块是正文的一部分,晚于切分写进去,
894
+ * 这一轮编出来的召回单元就与卡片正文对不上,而下次重建又会因为正文变了重切一遍 ——
895
+ * 每次编译都白编一次。
896
+ *
897
+ * 三段都是确定性的,所以这个函数可以重复跑:没变化的卡片一次都不写盘(判据是
898
+ * 「正文 + 问题清单逐字相同」)。这也让「重编一次,卡片不该有任何写入」这条承诺
899
+ * 在收尾段照样成立。
900
+ */
901
+ private runFinalizeStage;
902
+ /** 收尾段的**产物层**:写索引页。它读的是正文层算完之后的卡片清单。 */
903
+ private writeWikiIndex;
904
+ /** 一条路的候选 → 账本行(0050)。名次按这一路自己的顺序,没过阈值的也在。 */
905
+ private ledgerRows;
906
+ /**
907
+ * **1 跳扩展**:从命中的卡片出发,按图上的边拉邻居。
908
+ *
909
+ * 两个来源,优先级相同(都记进 `via`,一条邻居可以有多条来路):
910
+ * 1. **图上的边**(`relates_to` 这类卡片之间的关系)—— `supported_by` 不算:
911
+ * 它连的是「卡片 → 来源」,而来源不是 wiki 卡片,进不了上下文;
912
+ * 2. **共同来源**:同一份 SourceRaw 编译出来的卡片也是邻居(这是最实用的一类,
913
+ * 新库里往往一条 `relates_to` 都没有)。
914
+ *
915
+ * 调用点必须在**按卡片去重之后**:先扩展再去重会把邻居与命中混在一起,
916
+ * 而上下文里必须分得清「这是召回来的」还是「这是延伸出来的」。
917
+ */
918
+ private expandWiki;
919
+ /** 一条上下文条目「值多少字符」:命中条目算它那些 note 的正文,扩展条目算标题 + 摘要(+ 正文)。 */
920
+ private contextCost;
921
+ /** 溢出目录:**系统临时目录**,不随知识库走(0050 的取舍,见 issue)。 */
922
+ private recallOverflowDir;
923
+ /**
924
+ * 上下文预算:装箱 + 溢出(0050)。
925
+ *
926
+ * 让位顺序(写死在这里,不是启发式):
927
+ * 1. **命中的 note 正文优先**:它是证据,装不下就该少给几张卡片,而不是把证据截断;
928
+ * 2. 扩展卡片先给**标题 + 摘要 + 来路**(便宜,且足以让模型知道「还有这么一张卡」);
929
+ * 3. 还有余量才给扩展卡片的正文。
930
+ *
931
+ * 溢出的内容落**系统临时目录**(`<os.tmpdir()>/dsh-knowledge-recall/<库>/<recallId>.md`),
932
+ * 不随库走:它是一次召回的临时产物,下一次召回就是另一份。代价是系统会清它 ——
933
+ * 所以指路行必须说清「本次会话内有效」,并给出「读不到就重新召回」这条出路。
934
+ */
935
+ private packContext;
936
+ /** 把**完整**的上下文写进溢出文件(含被预算挡在外面的那些)。 */
937
+ private writeRecallOverflow;
938
+ /** 清掉本库前缀下超过保留时长的溢出文件(自清理,不依赖系统清 tmp)。 */
939
+ private pruneRecallOverflow;
940
+ /**
941
+ * 一张卡片的出链与入链(面板抽屉里的「相关」分块)。
942
+ *
943
+ * 与写进正文的那一块**同一份计划**:两处都调 `planRelated`,所以界面上看到的关系
944
+ * 与文件里写的不会漂移。入链要单独算 —— 关系是双向记的,但同源那一类是「谁和我
945
+ * 同源」,只能扫一遍别人的出链。
946
+ */
947
+ relatedOfCard(card: KnowledgeCard): Promise<{
948
+ outgoing: RelatedEntry[];
949
+ incoming: RelatedEntry[];
950
+ }>;
436
951
  rebuild(only?: 'all' | 'schema' | 'wiki' | 'embeddings', lineage?: LineageContext, options?: {
437
952
  extract?: boolean;
438
953
  }): Promise<Envelope<RebuildReport>>;
@@ -476,6 +991,22 @@ export declare class KnowledgeRuntime {
476
991
  * 匹配逻辑与 0030 相同(词面匹配节点 title / id 与关系词,命中节点带出 1 跳邻居并衰减),
477
992
  * 但返回值只用于给**已经在候选池里**的条目加权 —— 它自己不能制造候选。
478
993
  */
994
+ /**
995
+ * 字段级的归因图(`03_compiled/diagnosis_graph.json`)。
996
+ *
997
+ * 它只用于**给已经在候选池里的条目加权**(和卡片级图一样,不能自己制造候选)。
998
+ * 用途是把「假设 → 检查点」「证据 → 假设」这两类卡片级图看不见的语义带进召回。
999
+ */
1000
+ private diagnosisGraph;
1001
+ /**
1002
+ * 归因图上按词面命中的节点 → 分数,含 1 跳衰减。
1003
+ *
1004
+ * 与卡片级那一段是**同一条算法、两份数据**:卡片级图里只有 `supported_by` /
1005
+ * `relates_to`,而字段级图里是十种语义边。分开算再按 `entityId` 合流,
1006
+ * 是因为两张图的节点含义不同(一个是卡片,一个是字段级节点),合成一张会让
1007
+ * 图谱 Tab 看到一张卡片散成七个点。
1008
+ */
1009
+ private diagnosisHits;
479
1010
  private graphBoostRanking;
480
1011
  /** SourceRaw 正文,按 sourceId 索引;证据片段从这里取。写入后失效。 */
481
1012
  private sourceCorpusCache;
@@ -491,6 +1022,8 @@ export declare class KnowledgeRuntime {
491
1022
  recall(query: string, limit?: number, sourceTypes?: string[], includeProblems?: boolean, mode?: RecallMode, lineage?: LineageContext, options?: {
492
1023
  contextTopN?: number;
493
1024
  rerank?: boolean;
1025
+ contextMaxChars?: number;
1026
+ detail?: boolean;
494
1027
  }): Promise<Envelope<RecallResult>>;
495
1028
  /**
496
1029
  * 向量路的原始余弦(0031 修了两个数)。