dsh-data-domain-knowledge 0.1.0-alpha.13 → 0.1.0-alpha.14

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 (172) hide show
  1. package/README.md +84 -52
  2. package/cordis.patch.yml +21 -17
  3. package/eval/benchmark-extraction-format.mjs +20 -0
  4. package/lib/client.js +2846 -3872
  5. package/lib/doc-page.js +1 -1
  6. package/lib/pages.js +1299 -1660
  7. package/lib/types/axis-registry.d.ts +6 -0
  8. package/lib/types/axis-registry.d.ts.map +1 -1
  9. package/lib/types/axis-registry.js +45 -0
  10. package/lib/types/axis-registry.js.map +1 -1
  11. package/lib/types/diagnosis/cards.d.ts +7 -3
  12. package/lib/types/diagnosis/cards.d.ts.map +1 -1
  13. package/lib/types/diagnosis/cards.js +13 -8
  14. package/lib/types/diagnosis/cards.js.map +1 -1
  15. package/lib/types/diagnosis/run.d.ts +4 -13
  16. package/lib/types/diagnosis/run.d.ts.map +1 -1
  17. package/lib/types/diagnosis/run.js +26 -16
  18. package/lib/types/diagnosis/run.js.map +1 -1
  19. package/lib/types/diagnosis/schema.js +2 -2
  20. package/lib/types/diagnosis/schema.js.map +1 -1
  21. package/lib/types/diagnosis/seed-source.d.ts +1 -1
  22. package/lib/types/diagnosis/seed-source.d.ts.map +1 -1
  23. package/lib/types/diagnosis/seed-source.js +5 -4
  24. package/lib/types/diagnosis/seed-source.js.map +1 -1
  25. package/lib/types/diagnosis/seed.d.ts +4 -4
  26. package/lib/types/diagnosis/seed.d.ts.map +1 -1
  27. package/lib/types/diagnosis/seed.js +6 -8
  28. package/lib/types/diagnosis/seed.js.map +1 -1
  29. package/lib/types/diagnosis/store.d.ts +8 -16
  30. package/lib/types/diagnosis/store.d.ts.map +1 -1
  31. package/lib/types/diagnosis/store.js +41 -66
  32. package/lib/types/diagnosis/store.js.map +1 -1
  33. package/lib/types/diagnosis/types.d.ts +10 -16
  34. package/lib/types/diagnosis/types.d.ts.map +1 -1
  35. package/lib/types/diagnosis/types.js.map +1 -1
  36. package/lib/types/environment/service.d.ts +4 -3
  37. package/lib/types/environment/service.d.ts.map +1 -1
  38. package/lib/types/environment/service.js +91 -23
  39. package/lib/types/environment/service.js.map +1 -1
  40. package/lib/types/environment/types.d.ts +9 -0
  41. package/lib/types/environment/types.d.ts.map +1 -1
  42. package/lib/types/events.d.ts +2 -0
  43. package/lib/types/events.d.ts.map +1 -1
  44. package/lib/types/events.js +13 -0
  45. package/lib/types/events.js.map +1 -1
  46. package/lib/types/extract-contract.d.ts +1 -1
  47. package/lib/types/extract-contract.d.ts.map +1 -1
  48. package/lib/types/extract-contract.js +34 -31
  49. package/lib/types/extract-contract.js.map +1 -1
  50. package/lib/types/extract-model.d.ts +2 -0
  51. package/lib/types/extract-model.d.ts.map +1 -1
  52. package/lib/types/extract-model.js.map +1 -1
  53. package/lib/types/extract-table-contract.d.ts +3 -0
  54. package/lib/types/extract-table-contract.d.ts.map +1 -0
  55. package/lib/types/extract-table-contract.js +30 -0
  56. package/lib/types/extract-table-contract.js.map +1 -0
  57. package/lib/types/extract-table.d.ts +8 -0
  58. package/lib/types/extract-table.d.ts.map +1 -0
  59. package/lib/types/extract-table.js +151 -0
  60. package/lib/types/extract-table.js.map +1 -0
  61. package/lib/types/extraction-runs.d.ts +3 -0
  62. package/lib/types/extraction-runs.d.ts.map +1 -1
  63. package/lib/types/extraction-runs.js +6 -0
  64. package/lib/types/extraction-runs.js.map +1 -1
  65. package/lib/types/index.d.ts.map +1 -1
  66. package/lib/types/index.js +196 -44
  67. package/lib/types/index.js.map +1 -1
  68. package/lib/types/intent/scope.d.ts.map +1 -1
  69. package/lib/types/intent/scope.js +1 -0
  70. package/lib/types/intent/scope.js.map +1 -1
  71. package/lib/types/knowledge-bases.d.ts.map +1 -1
  72. package/lib/types/knowledge-bases.js +3 -2
  73. package/lib/types/knowledge-bases.js.map +1 -1
  74. package/lib/types/knowledge.d.ts +3 -0
  75. package/lib/types/knowledge.d.ts.map +1 -1
  76. package/lib/types/knowledge.js +12 -3
  77. package/lib/types/knowledge.js.map +1 -1
  78. package/lib/types/model-text.d.ts +1 -1
  79. package/lib/types/model-text.d.ts.map +1 -1
  80. package/lib/types/model-text.js +5 -4
  81. package/lib/types/model-text.js.map +1 -1
  82. package/lib/types/pipeline/engine.d.ts +57 -0
  83. package/lib/types/pipeline/engine.d.ts.map +1 -0
  84. package/lib/types/pipeline/engine.js +268 -0
  85. package/lib/types/pipeline/engine.js.map +1 -0
  86. package/lib/types/pipeline/store.d.ts +56 -0
  87. package/lib/types/pipeline/store.d.ts.map +1 -0
  88. package/lib/types/pipeline/store.js +115 -0
  89. package/lib/types/pipeline/store.js.map +1 -0
  90. package/lib/types/pipeline/types.d.ts +82 -0
  91. package/lib/types/pipeline/types.d.ts.map +1 -0
  92. package/lib/types/pipeline/types.js +5 -0
  93. package/lib/types/pipeline/types.js.map +1 -0
  94. package/lib/types/present.d.ts +2 -0
  95. package/lib/types/present.d.ts.map +1 -1
  96. package/lib/types/present.js +9 -4
  97. package/lib/types/present.js.map +1 -1
  98. package/lib/types/runtime.d.ts +20 -1
  99. package/lib/types/runtime.d.ts.map +1 -1
  100. package/lib/types/runtime.js +389 -112
  101. package/lib/types/runtime.js.map +1 -1
  102. package/lib/types/source-evidence.d.ts.map +1 -1
  103. package/lib/types/source-evidence.js +2 -0
  104. package/lib/types/source-evidence.js.map +1 -1
  105. package/lib/types/store/axis-references.js +3 -3
  106. package/lib/types/store/axis-references.js.map +1 -1
  107. package/lib/types/store/migrate.js +1 -1
  108. package/lib/types/store/migrate.js.map +1 -1
  109. package/lib/types/types.d.ts +3 -4
  110. package/lib/types/types.d.ts.map +1 -1
  111. package/lib/types/types.js.map +1 -1
  112. package/lib/types/wiki-materialize.d.ts.map +1 -1
  113. package/lib/types/wiki-materialize.js +3 -15
  114. package/lib/types/wiki-materialize.js.map +1 -1
  115. package/lib/types/wiki-trial-types.d.ts +3 -0
  116. package/lib/types/wiki-trial-types.d.ts.map +1 -1
  117. package/lib/types/wiki-trial.d.ts +2 -0
  118. package/lib/types/wiki-trial.d.ts.map +1 -1
  119. package/lib/types/wiki-trial.js +31 -7
  120. package/lib/types/wiki-trial.js.map +1 -1
  121. package/package.json +6 -3
  122. package/preset/tita-data-agent/agent.cordis.yml +21 -17
  123. package/src/axis-registry.ts +38 -0
  124. package/src/client/AttributionEventGuide.tsx +10 -9
  125. package/src/client/EnvironmentSetup.tsx +35 -15
  126. package/src/client/ExtractionTasksPage.tsx +129 -176
  127. package/src/client/KnowledgeBaseTab.tsx +31 -76
  128. package/src/client/NodeSettings.tsx +2 -3
  129. package/src/client/PipelineNotice.tsx +26 -0
  130. package/src/client/TabGuide.tsx +4 -4
  131. package/src/client/TypeDefinitionDrawer.tsx +23 -8
  132. package/src/client/WikiCardPreview.tsx +43 -10
  133. package/src/client/WikiFlowPanel.tsx +4 -4
  134. package/src/client/architecture.tsx +1 -1
  135. package/src/client/attribution/AttributionTab.tsx +6 -6
  136. package/src/client/attribution/CaseView.tsx +3 -1
  137. package/src/client/attribution/EventView.tsx +14 -4
  138. package/src/client/attribution/RunView.tsx +2 -2
  139. package/src/client/attribution/graph-options.ts +2 -2
  140. package/src/client/pages.tsx +2 -2
  141. package/src/diagnosis/cards.ts +13 -8
  142. package/src/diagnosis/run.ts +27 -17
  143. package/src/diagnosis/schema.ts +2 -2
  144. package/src/diagnosis/seed-source.ts +5 -4
  145. package/src/diagnosis/seed.ts +6 -8
  146. package/src/diagnosis/store.ts +47 -68
  147. package/src/diagnosis/types.ts +10 -16
  148. package/src/environment/service.ts +79 -23
  149. package/src/environment/types.ts +6 -0
  150. package/src/events.ts +12 -0
  151. package/src/extract-contract.ts +6 -3
  152. package/src/extract-model.ts +2 -0
  153. package/src/extract-table-contract.ts +29 -0
  154. package/src/extract-table.ts +126 -0
  155. package/src/extraction-runs.ts +6 -0
  156. package/src/index.ts +156 -45
  157. package/src/intent/scope.ts +1 -0
  158. package/src/knowledge-bases.ts +3 -2
  159. package/src/knowledge.ts +13 -3
  160. package/src/model-text.ts +5 -3
  161. package/src/pipeline/engine.ts +238 -0
  162. package/src/pipeline/store.ts +119 -0
  163. package/src/pipeline/types.ts +74 -0
  164. package/src/present.ts +10 -4
  165. package/src/runtime.ts +350 -110
  166. package/src/source-evidence.ts +1 -0
  167. package/src/store/axis-references.ts +3 -3
  168. package/src/store/migrate.ts +1 -1
  169. package/src/types.ts +3 -4
  170. package/src/wiki-materialize.ts +3 -14
  171. package/src/wiki-trial-types.ts +3 -0
  172. package/src/wiki-trial.ts +26 -7
@@ -13,11 +13,14 @@ import { MAX_LOG_ITEMS, OperationLog, operationId } from './oplog.js';
13
13
  import { ExtractionRunStore, extractionRunId } from './extraction-runs.js';
14
14
  import { AxisRegistry, normalizeAxisName } from './axis-registry.js';
15
15
  import { KNOWLEDGE_STATE_LABEL, KnowledgeStore } from './knowledge.js';
16
- import { compositeEventIdentity, eventEntityFromRecords } from './events.js';
16
+ import { compositeEventIdentity, eventEntityFromRecords, stripEventReferenceSection } from './events.js';
17
17
  import { buildAliasIndex, candidateEntityId, extractCandidates, normalizeCandidates, RELATES_TO } from './extract.js';
18
18
  import { skeletonBody } from './extract-model.js';
19
- import { applyTrialAxisResolution, normalizeKnowledge, runWikiTrial, trialSummary } from './wiki-trial.js';
20
- import { candidatesFromReport, coordinateIdsOf } from './wiki-materialize.js';
19
+ import { applyTrialAxisResolution, cardNameFor, normalizeKnowledge, runWikiTrial, trialSummary } from './wiki-trial.js';
20
+ import { EXTRACTION_CONTRACT_VERSION } from './extract-table.js';
21
+ import { KnowledgePipeline } from './pipeline/engine.js';
22
+ import { PipelineStore } from './pipeline/store.js';
23
+ import { candidatesFromReport, coordinateIdsOf, coordinateIsEmpty } from './wiki-materialize.js';
21
24
  import { buildOrganizePrompt, buildOrganizeSystem, checkOrganize, citationsFrom, layoutHash, layoutOf, parseOrganize, placementsOf, renderFootnoteDefinitions, renderOrganizeBody, } from './card-organize.js';
22
25
  import { FTS_FILE, loadSqlite, segment } from './fts.js';
23
26
  import { attachImagesToKnowledge, insertImage, planSourceImages } from './source-images.js';
@@ -684,38 +687,6 @@ function appendExtracted(existing, additions) {
684
687
  }
685
688
  return content;
686
689
  }
687
- /** 坐标 WikiCard 的事件摘要段由 Event 引用派生;全文与时间线留在独立 Event 实体。 */
688
- function withEventReferences(content, rawRefs, eventsById) {
689
- let refs = [];
690
- if (Array.isArray(rawRefs))
691
- refs = rawRefs.filter((item) => typeof item === 'string');
692
- else if (typeof rawRefs === 'string') {
693
- try {
694
- const parsed = JSON.parse(rawRefs);
695
- if (Array.isArray(parsed))
696
- refs = parsed.filter((item) => typeof item === 'string');
697
- }
698
- catch { /* 无效引用会作为空列表处理 */ }
699
- }
700
- const lines = content.split('\n');
701
- const start = lines.findIndex(line => line.trim() === '## 关联 Event');
702
- if (start >= 0) {
703
- let end = start + 1;
704
- while (end < lines.length && !/^#{1,2}\s+/.test(lines[end] ?? ''))
705
- end += 1;
706
- lines.splice(start, end - start);
707
- }
708
- const base = lines.join('\n').trimEnd();
709
- const entries = [...new Set(refs)].map(id => eventsById.get(id)).filter((event) => event !== undefined);
710
- if (entries.length === 0)
711
- return base;
712
- const bullets = entries.map(event => {
713
- const occurredAt = event.occurredAt ? ` · ${event.occurredAt}` : '';
714
- const summary = event.summary.trim() ? `:${event.summary.trim()}` : '';
715
- return `- **${event.title}**${occurredAt}${summary}(${event.id})`;
716
- });
717
- return `${base ? `${base}\n\n` : ''}## 关联 Event\n\n${bullets.join('\n')}`;
718
- }
719
690
  /** 把 `incoming` 按小节折进 `existing`;结构对不上时整段追加。 */
720
691
  function foldBody(existing, incoming) {
721
692
  if (!existing)
@@ -1008,9 +979,10 @@ function markdownImageRefs(content) {
1008
979
  * `tags` / `generated` / `sources`。它们是真 YAML 键,人能读、`grep` 得到、
1009
980
  * 外部工具能按公开规范读走。`generated.at` 就是**采集时间**(OKF 里没有「抓取
1010
981
  * 时间」这个概念,本文件何时被谁写出来就是它)。
1011
- * 2. **我们自己的运行键**:`source_id` / `external_id` / `created_at` / `updated_at` /
1012
- * `metadata` / `lineage`。OKF 明确允许自定义键,且要求消费者不得因为未知键拒收,
1013
- * 所以这两类可以同处一个 front matter。
982
+ * 2. **我们自己的运行键**:`created_at` / `updated_at` / `metadata` / `lineage`。
983
+ * 来源身份保存在 SQLite 的 `entity_id` / `external_id` 列,不在 front matter 重复一份。
984
+ * OKF 明确允许自定义键,且要求消费者不得因为未知键拒收,所以运行元数据可以同处
985
+ * 一个 front matter。
1014
986
  *
1015
987
  * `metadata` 只剩**类型专属的扩展键**(转换任务 id、内容哈希、资产清单、格式…):
1016
988
  * 那些东西 OKF 没有对应概念,但也不该混进上面那组公开字段里。
@@ -1043,8 +1015,6 @@ function sourceFrontMatter(record, provenance) {
1043
1015
  ...(origin.lastModified?.trim() ? { last_modified: origin.lastModified.trim() } : {}),
1044
1016
  }));
1045
1017
  }
1046
- frontMatter.source_id = record.sourceId;
1047
- frontMatter.external_id = record.externalId ?? '';
1048
1018
  frontMatter.created_at = record.createdAt;
1049
1019
  frontMatter.updated_at = record.updatedAt;
1050
1020
  frontMatter.metadata = record.metadata;
@@ -1551,6 +1521,9 @@ export class KnowledgeRuntime {
1551
1521
  /** 抽取轨迹(`<memoryDir>/extraction-runs.sqlite`)。同一个懒打开的理由。 */
1552
1522
  extractionRunsStore = null;
1553
1523
  extractionRunsReady = null;
1524
+ closed = false;
1525
+ pipelineEngine;
1526
+ pipelineOpening;
1554
1527
  /** 轴注册表(`<memoryDir>/knowledge.sqlite`)。与抽取轨迹同一个懒打开的理由。 */
1555
1528
  axesStore = null;
1556
1529
  axesReady = null;
@@ -2292,6 +2265,10 @@ export class KnowledgeRuntime {
2292
2265
  return { vectors, device };
2293
2266
  }
2294
2267
  close() {
2268
+ if (this.closed)
2269
+ return;
2270
+ this.closed = true;
2271
+ this.pipelineEngine?.close();
2295
2272
  for (const task of this.compileTasks.values()) {
2296
2273
  if (['succeeded', 'failed', 'cancelled'].includes(task.progress.state))
2297
2274
  continue;
@@ -2299,6 +2276,13 @@ export class KnowledgeRuntime {
2299
2276
  task.pauseRequested = false;
2300
2277
  task.resume?.();
2301
2278
  }
2279
+ if (this.compilePending > 0) {
2280
+ void this.compileTail.finally(() => this.closeStores());
2281
+ return;
2282
+ }
2283
+ this.closeStores();
2284
+ }
2285
+ closeStores() {
2302
2286
  this.embeddingWorker?.close();
2303
2287
  this.embeddingWorker = null;
2304
2288
  this.rerankWorker?.close();
@@ -2680,39 +2664,26 @@ export class KnowledgeRuntime {
2680
2664
  if (written.length === 0) {
2681
2665
  return failure({ ...empty, failed, attempted: items.length }, 'IMPORT_ALL_FAILED', `${items.length} 份来源全部写入失败,没有任何东西进入知识库`, true, failed[0] ? `第一份的原因:${failed[0].message}` : undefined);
2682
2666
  }
2683
- /**
2684
- * 编译。**只要有一份写成就编**:成功的那几份已经是库里的知识,
2685
- * 「因为同批里有一份失败就不编」等于把已经进来的知识又挡在门外一次。
2686
- */
2687
2667
  let compile = null;
2688
- if (input.compile !== false) {
2689
- let rebuilt;
2668
+ if (input.compile === true) {
2690
2669
  try {
2691
- rebuilt = await this.rebuild('wiki', input.lineage);
2692
- }
2693
- catch (cause) {
2694
- warnings.push(`来源已写入,编译未完成:${cause instanceof Error ? cause.message : String(cause)}`);
2695
- }
2696
- if (rebuilt === undefined)
2697
- return success({ written, failed, attempted: items.length, compile }, warnings);
2698
- if (rebuilt.ok === false) {
2699
- warnings.push(`来源已写入,但编译失败:${rebuilt.error?.message ?? '未知原因'}`);
2700
- }
2701
- else {
2702
- const data = rebuilt.data;
2670
+ const before = new Map((await this.loadCards()).map(card => [card.entityId, this.cardHashOf(card)]));
2671
+ const pipeline = await this.pipeline();
2672
+ const task = await pipeline.start('all', { sourceIds: written.map(source => source.sourceId) }).completion;
2673
+ const after = await this.loadCards();
2703
2674
  compile = {
2704
- cardsCreated: data.cardsCreated,
2705
- cardsUpdated: data.cardsUpdated,
2706
- cardsReused: data.cardsReused,
2707
- notesReused: data.notesReused,
2708
- notesRecompiled: data.notesRecompiled,
2709
- sourcesExtracted: data.sourcesExtracted,
2710
- sourcesSkipped: data.sourcesSkipped,
2711
- cardsRemoved: data.cardsRemoved,
2712
- embedding: data.embedding,
2713
- version: data.version,
2675
+ taskId: task.id, state: task.state,
2676
+ cardsCreated: after.filter(card => !before.has(card.entityId)).length,
2677
+ cardsUpdated: after.filter(card => before.has(card.entityId) && before.get(card.entityId) !== this.cardHashOf(card)).length,
2678
+ sourcesExtracted: Math.max(0, (task.results?.extract?.done ?? 0) - (task.results?.extract?.reused ?? 0) - (task.results?.extract?.failed ?? 0)),
2679
+ sourcesSkipped: task.results?.extract?.reused ?? 0,
2714
2680
  };
2715
- warnings.push(...(rebuilt.warnings ?? []));
2681
+ warnings.push(...task.warnings);
2682
+ if (task.state !== 'succeeded')
2683
+ warnings.push(`来源已保存;全链路未完成:${task.summary}。可在知识处理页面继续。`);
2684
+ }
2685
+ catch (cause) {
2686
+ warnings.push(`来源已保存;知识处理未完成:${cause instanceof Error ? cause.message : String(cause)}`);
2716
2687
  }
2717
2688
  }
2718
2689
  return success({ written, failed, attempted: items.length, compile }, warnings);
@@ -3894,10 +3865,11 @@ export class KnowledgeRuntime {
3894
3865
  const store = await this.knowledgeStore();
3895
3866
  if (store === null)
3896
3867
  return { ok: false, reason: '知识库不可用,这次没有组织卡片(正文退回确定性布局)' };
3897
- const listed = await store.list({
3868
+ const listed = input.knowledge ? { knowledge: input.knowledge, total: input.knowledge.length } : await store.list({
3898
3869
  objectId: input.coordinate.objectId,
3899
3870
  actionId: input.coordinate.actionId,
3900
3871
  sceneId: input.coordinate.sceneId,
3872
+ excludeWithdrawn: true,
3901
3873
  limit: MAX_ORGANIZE_KNOWLEDGE,
3902
3874
  });
3903
3875
  if (listed.knowledge.length === 0) {
@@ -4075,7 +4047,8 @@ export class KnowledgeRuntime {
4075
4047
  if (call === undefined)
4076
4048
  return failure({ card, warnings: [] }, 'MODEL_UNAVAILABLE', '当前没有可用的模型,无法重新组织这张卡片', true, '检查模型配置后再试。');
4077
4049
  const profile = this.organizeProfile();
4078
- const related = await this.relatedForCard(card);
4050
+ const organizationCard = { ...card, content: stripEventReferenceSection(card.content) };
4051
+ const related = await this.relatedForCard(organizationCard);
4079
4052
  const images = await this.imagesForCard(card);
4080
4053
  const organized = await this.organizeCard({
4081
4054
  cardTitle: card.title,
@@ -4086,7 +4059,7 @@ export class KnowledgeRuntime {
4086
4059
  ...(profile === undefined ? {} : { requiredSections: profile.sections.filter(section => section.required).map(section => section.label) }),
4087
4060
  ...(related.related === undefined ? {} : { related: related.related }),
4088
4061
  ...(images.images.length === 0 ? {} : { images: images.images }),
4089
- existing: card,
4062
+ existing: organizationCard,
4090
4063
  call,
4091
4064
  });
4092
4065
  if (organized.kind !== 'applied') {
@@ -4097,7 +4070,7 @@ export class KnowledgeRuntime {
4097
4070
  entityType: card.entityType,
4098
4071
  entityId: card.entityId,
4099
4072
  title: organized.title,
4100
- content: organized.content,
4073
+ content: stripEventReferenceSection(organized.content),
4101
4074
  sourceRefs: card.sourceRefs,
4102
4075
  summary: organized.summary,
4103
4076
  status: card.status,
@@ -4183,6 +4156,7 @@ export class KnowledgeRuntime {
4183
4156
  }
4184
4157
  async organizeCard(input) {
4185
4158
  const context = await this.organizeContext({
4159
+ knowledge: input.knowledge,
4186
4160
  cardTitle: input.cardTitle,
4187
4161
  coordinate: input.coordinate,
4188
4162
  profileId: input.profileId,
@@ -4511,7 +4485,7 @@ export class KnowledgeRuntime {
4511
4485
  return cards.flatMap(card => this.compileCardNotes(card));
4512
4486
  }
4513
4487
  compileCardNotes(card) {
4514
- if (card.status === 'problem')
4488
+ if (card.status === 'problem' || card.status === 'deprecated')
4515
4489
  return [];
4516
4490
  const notes = [];
4517
4491
  /**
@@ -4529,7 +4503,7 @@ export class KnowledgeRuntime {
4529
4503
  * `metadata.related_cards`;这里继续剥离旧版本正文里的标记,保证存量卡片迁移期间
4530
4504
  * 也不会把导航送进召回。
4531
4505
  */
4532
- const recallBody = stripRelatedSection(card.content);
4506
+ const recallBody = stripEventReferenceSection(stripRelatedSection(card.content));
4533
4507
  for (const [index, chunk] of splitWikiNotes({ ...card, content: recallBody }).entries()) {
4534
4508
  const baseId = `${card.entityType}:${card.entityId}`;
4535
4509
  const unitId = index === 0 ? baseId : `${baseId}#${index}`;
@@ -4595,7 +4569,7 @@ export class KnowledgeRuntime {
4595
4569
  const provider = this.config.rerankProvider;
4596
4570
  if (provider === 'none')
4597
4571
  return { hits, result: { status: 'skipped', provider, threshold } };
4598
- if (hits.length < 2)
4572
+ if (hits.length === 0)
4599
4573
  return { hits, result: { status: 'skipped', provider, model: this.rerankModelPath, scored: hits.length, threshold } };
4600
4574
  if (this.rerankUnavailable)
4601
4575
  return { hits, result: { status: 'unavailable', provider, model: this.rerankModelPath, error: this.rerankUnavailable, threshold }, warning: `重排不可用,已按融合分数排序:${this.rerankUnavailable}` };
@@ -4843,7 +4817,6 @@ export class KnowledgeRuntime {
4843
4817
  */
4844
4818
  const profile = this.organizeProfile();
4845
4819
  const profileId = profile?.id ?? 'concept';
4846
- const eventsById = new Map((await this.loadEvents()).map(event => [event.id, event]));
4847
4820
  for (const item of pending.values()) {
4848
4821
  if (runId)
4849
4822
  await this.checkpointCompileTask(runId);
@@ -4892,7 +4865,7 @@ export class KnowledgeRuntime {
4892
4865
  && layoutOf(existing.metadata)?.origin === 'model';
4893
4866
  const finalTitle = preserveModelLayout ? existing.title : item.title;
4894
4867
  const contentBeforeEvents = preserveModelLayout ? existing.content : content;
4895
- const finalContent = withEventReferences(contentBeforeEvents, item.metadata.event_refs, eventsById);
4868
+ const finalContent = stripEventReferenceSection(contentBeforeEvents);
4896
4869
  const finalSummary = preserveModelLayout ? existing.summary : item.summary;
4897
4870
  let finalPlacements = preserveModelLayout || item.placements === undefined
4898
4871
  ? undefined
@@ -4986,7 +4959,7 @@ export class KnowledgeRuntime {
4986
4959
  for (const warning of written.warnings ?? [])
4987
4960
  outcome.warnings.push(`卡片 ${card.entityId}:${warning}`);
4988
4961
  }
4989
- // WikiCard 是 Knowledge 与 Event 的坐标聚合层;Event 本体读自独立 event 表。
4962
+ // WikiCard 只由 Knowledge 物化;Event 作为独立事实按坐标挂引用。
4990
4963
  const eventSync = await this.syncEventReferences();
4991
4964
  outcome.cardsUpdated += eventSync.updated;
4992
4965
  outcome.warnings.push(...eventSync.warnings);
@@ -5035,8 +5008,9 @@ export class KnowledgeRuntime {
5035
5008
  * 用的输入与收尾阶段那个相关块**完全相同**(同一份 `relations` + 同一个坐标投影),
5036
5009
  * 所以「正文里能提到谁」与「末尾相关块列了谁」不会是两个答案。
5037
5010
  */
5038
- const relatedPlan = planRelated(input.cards, input.relations, await this.projectedRelated(input.cards));
5039
- const withCoordinate = input.cards
5011
+ const cardsWithoutLegacyEventSections = input.cards.map(card => ({ ...card, content: stripEventReferenceSection(card.content) }));
5012
+ const relatedPlan = planRelated(cardsWithoutLegacyEventSections, input.relations, await this.projectedRelated(input.cards));
5013
+ const withCoordinate = cardsWithoutLegacyEventSections
5040
5014
  .filter(card => card.coordinate !== undefined)
5041
5015
  .sort((left, right) => left.entityId.localeCompare(right.entityId));
5042
5016
  if (withCoordinate.length === 0)
@@ -5674,7 +5648,7 @@ export class KnowledgeRuntime {
5674
5648
  const projected = await this.projectedRelated(cards);
5675
5649
  const related = planRelated(cards, relations, projected);
5676
5650
  /** 统计清理旧导航块之后的正文断链;相关卡片本身不再伪装成正文链接。 */
5677
- const nextCards = cards.map(card => ({ ...card, content: stripRelatedSection(card.content) }));
5651
+ const nextCards = cards.map(card => ({ ...card, content: stripEventReferenceSection(stripRelatedSection(card.content)) }));
5678
5652
  const deadLinks = deadLinksOf(nextCards);
5679
5653
  let rewritten = 0;
5680
5654
  for (const [index, card] of cards.entries()) {
@@ -6390,6 +6364,7 @@ export class KnowledgeRuntime {
6390
6364
  * 不能当成「没命中」。
6391
6365
  */
6392
6366
  async resolveTrialAxisSet(input) {
6367
+ const resolve = (value) => this.resolveAxisMention({ ...value, registry: input.registry });
6393
6368
  const groups = [
6394
6369
  { kind: 'object', key: 'objects' },
6395
6370
  { kind: 'action', key: 'actions' },
@@ -6400,7 +6375,7 @@ export class KnowledgeRuntime {
6400
6375
  const axisKey = (kind, mention) => `${kind}\u0000${normalizeAxisName(mention)}`;
6401
6376
  const supplementalEntries = { object: [], action: [], scene: [] };
6402
6377
  try {
6403
- const registry = await this.axisRegistry();
6378
+ const registry = input.registry ?? await this.axisRegistry();
6404
6379
  if (registry !== null) {
6405
6380
  // 初次抽取的候选清单可能被截断。再按本批轴 mention 做一次本地召回,补进未下发的已有坐标。
6406
6381
  const query = groups.flatMap(group => input.axes[group.key].flatMap(axis => [
@@ -6455,7 +6430,7 @@ export class KnowledgeRuntime {
6455
6430
  const key = axisKey(group.kind, axis.name);
6456
6431
  let lookup;
6457
6432
  try {
6458
- lookup = await this.resolveAxisMention({
6433
+ lookup = await resolve({
6459
6434
  kind: group.kind,
6460
6435
  mention: axis.name,
6461
6436
  parentMention: axis.parentMention,
@@ -6519,7 +6494,7 @@ export class KnowledgeRuntime {
6519
6494
  continue;
6520
6495
  }
6521
6496
  // 没有可供模型比较的现有候选时,沿用注册表的新建候选路径。
6522
- const created = await this.resolveAxisMention({
6497
+ const created = await resolve({
6523
6498
  kind: group.kind,
6524
6499
  mention: axis.name,
6525
6500
  parentMention: axis.parentMention,
@@ -6629,7 +6604,7 @@ export class KnowledgeRuntime {
6629
6604
  const key = axisKey(item.kind, item.axis.name);
6630
6605
  const judgment = decisions.get(key);
6631
6606
  if (judgment?.decision === 'new') {
6632
- const resolution = await this.resolveAxisMention({
6607
+ const resolution = await resolve({
6633
6608
  kind: item.kind,
6634
6609
  mention: item.axis.name,
6635
6610
  parentMention: item.axis.parentMention,
@@ -6643,7 +6618,7 @@ export class KnowledgeRuntime {
6643
6618
  if (judgment?.decision === 'match') {
6644
6619
  const candidate = item.candidates.find(value => value.id === judgment.candidateId);
6645
6620
  if (candidate) {
6646
- const resolution = await this.resolveAxisMention({
6621
+ const resolution = await resolve({
6647
6622
  kind: item.kind,
6648
6623
  mention: item.axis.name,
6649
6624
  parentMention: item.axis.parentMention,
@@ -6672,7 +6647,7 @@ export class KnowledgeRuntime {
6672
6647
  };
6673
6648
  }
6674
6649
  async resolveAxisMention(input) {
6675
- const registry = await this.axisRegistry();
6650
+ const registry = input.registry ?? await this.axisRegistry();
6676
6651
  if (registry === null)
6677
6652
  return null;
6678
6653
  const runId = input.runId ?? '';
@@ -6688,7 +6663,7 @@ export class KnowledgeRuntime {
6688
6663
  if (parent.entity)
6689
6664
  parentId = parent.entity.id;
6690
6665
  }
6691
- const preferId = await this.axisIdByName(input.kind, input.preferMention);
6666
+ const preferId = await this.axisIdByName(input.kind, input.preferMention, registry);
6692
6667
  return registry.resolve({
6693
6668
  kind: input.kind, mention: input.mention,
6694
6669
  ...(parentId ? { parentId } : {}), ...(runId ? { runId } : {}),
@@ -6704,11 +6679,11 @@ export class KnowledgeRuntime {
6704
6679
  * 「是已有的某个东西」而那个名字在库里指两个实体时,把它当成指认就等于替人做了
6705
6680
  * 「哪一个」的决定 —— 而注册表的规矩是「不唯一就不猜」。
6706
6681
  */
6707
- async axisIdByName(kind, mention) {
6682
+ async axisIdByName(kind, mention, supplied) {
6708
6683
  const name = (mention ?? '').trim();
6709
6684
  if (!name)
6710
6685
  return undefined;
6711
- const registry = await this.axisRegistry();
6686
+ const registry = supplied ?? await this.axisRegistry();
6712
6687
  if (registry === null)
6713
6688
  return undefined;
6714
6689
  try {
@@ -7461,14 +7436,12 @@ export class KnowledgeRuntime {
7461
7436
  refs.push(event.id);
7462
7437
  eventsByCoordinate.set(key, refs);
7463
7438
  }
7464
- const eventIndex = new Map(events.map(event => [event.id, event]));
7465
7439
  const warnings = [];
7466
7440
  let updated = 0;
7467
7441
  for (const card of await this.loadCards()) {
7468
- const refs = card.coordinate === undefined
7442
+ const refs = card.status === 'deprecated' || card.coordinate === undefined || card.entityType === 'diagnosis/case'
7469
7443
  ? []
7470
7444
  : (eventsByCoordinate.get(coordinateKeyOf(card.coordinate)) ?? []).sort();
7471
- db.setCardEvents(card.entityId, refs);
7472
7445
  const oldRaw = card.metadata.event_refs;
7473
7446
  let oldRefs = [];
7474
7447
  let oldRefsValid = false;
@@ -7486,13 +7459,17 @@ export class KnowledgeRuntime {
7486
7459
  oldRefs = oldRaw.filter((item) => typeof item === 'string').sort();
7487
7460
  oldRefsValid = true;
7488
7461
  }
7489
- const content = withEventReferences(card.content, refs, eventIndex);
7462
+ const content = stripEventReferenceSection(card.content);
7490
7463
  const contentChanged = content !== card.content;
7491
7464
  const refsChanged = !oldRefsValid || JSON.stringify(refs) !== JSON.stringify(oldRefs);
7492
- if (!refsChanged && !contentChanged)
7465
+ if (!refsChanged && !contentChanged) {
7466
+ db.setCardEvents(card.entityId, refs);
7493
7467
  continue;
7494
- if (!oldRefsValid && refs.length === 0 && oldRaw === undefined && !contentChanged)
7468
+ }
7469
+ if (!oldRefsValid && refs.length === 0 && oldRaw === undefined && !contentChanged) {
7470
+ db.setCardEvents(card.entityId, refs);
7495
7471
  continue;
7472
+ }
7496
7473
  const written = await this.writeCard({
7497
7474
  ...card,
7498
7475
  sourceRefs: card.sourceRefs,
@@ -7501,9 +7478,11 @@ export class KnowledgeRuntime {
7501
7478
  origin: 'internal',
7502
7479
  });
7503
7480
  if (written.ok === false)
7504
- warnings.push(`坐标 WikiCard ${card.title} 的 Event 聚合视图未更新:${written.error.message}`);
7505
- else
7481
+ warnings.push(`WikiCard ${card.title} 的 Event 引用未更新:${written.error.message}`);
7482
+ else {
7483
+ db.setCardEvents(card.entityId, refs);
7506
7484
  updated += 1;
7485
+ }
7507
7486
  }
7508
7487
  return { updated, warnings };
7509
7488
  }
@@ -7524,10 +7503,6 @@ export class KnowledgeRuntime {
7524
7503
  const ids = {};
7525
7504
  for (const event of db.events()) {
7526
7505
  const remaining = event.records.filter(record => record.sourceId !== input.sourceId);
7527
- if (remaining.length === 0) {
7528
- db.removeEvent(event.id);
7529
- continue;
7530
- }
7531
7506
  const state = {
7532
7507
  id: event.id,
7533
7508
  identityKey: event.identityKey,
@@ -7629,8 +7604,12 @@ export class KnowledgeRuntime {
7629
7604
  saved += 1;
7630
7605
  }
7631
7606
  for (const state of states.values()) {
7632
- if (!state.dirty || state.records.length === 0)
7607
+ if (!state.dirty)
7608
+ continue;
7609
+ if (state.records.length === 0) {
7610
+ db.removeEvent(state.id);
7633
7611
  continue;
7612
+ }
7634
7613
  const records = state.records;
7635
7614
  const entity = eventEntityFromRecords({
7636
7615
  id: state.id,
@@ -7650,6 +7629,298 @@ export class KnowledgeRuntime {
7650
7629
  }
7651
7630
  return { saved, merged, skipped, ids, warnings };
7652
7631
  }
7632
+ /** The stage runner shares this base's existing writer queue and audit database. */
7633
+ async pipeline() {
7634
+ if (this.closed)
7635
+ throw new Error('知识库已关闭');
7636
+ this.pipelineOpening ??= (async () => {
7637
+ const db = await this.storeHandle();
7638
+ const audit = await this.extractionRuns();
7639
+ if (this.closed)
7640
+ throw new Error('知识库已关闭');
7641
+ if (!db || !audit || !await this.knowledgeStore())
7642
+ throw new Error('知识库或任务历史不可用,无法启动处理任务');
7643
+ const store = new PipelineStore(db.connection(), audit.connection());
7644
+ const engine = new KnowledgePipeline(store, {
7645
+ capture: () => ({ model: this.modelSeamNow(), note: this.extractionNote(), profile: structuredClone(this.organizeProfile()), organize: this.config.cardOrganize, format: 'table' }),
7646
+ serialize: work => {
7647
+ this.compilePending += 1;
7648
+ const run = this.compileTail.then(work, work);
7649
+ this.compileTail = run.then(() => undefined, () => undefined).finally(() => { this.compilePending -= 1; });
7650
+ return run;
7651
+ },
7652
+ sources: () => this.pipelineSources(),
7653
+ fingerprint: (source, context) => contentHashOf(JSON.stringify({ source: source.sourceHash, title: source.title, contract: EXTRACTION_CONTRACT_VERSION, note: context.note, format: context.format, model: { provider: context.model.provider, model: context.model.model, reasoningEffort: context.model.reasoningEffort } })),
7654
+ extract: async (source, context, onDelta) => {
7655
+ if (!context.model.call)
7656
+ throw new Error(`抽取模型不可用:${context.model.why ?? '请在编译设置中选择模型'}`);
7657
+ const axes = await this.axisPromptBlock(source.content);
7658
+ return runWikiTrial({ title: source.title, text: source.content, docId: source.sourceId, axes, note: context.note, call: context.model.call, modelRequest: { provider: context.model.provider, model: context.model.model, reasoningEffort: context.model.reasoningEffort }, outputFormat: context.format, sessionId: `knowledge:${this.baseId || 'default'}`, onDelta });
7659
+ },
7660
+ normalize: (source, checkpoint, context, force, output) => this.normalizePipelineSource(store, source, checkpoint, context, force, output),
7661
+ groups: context => this.materializationGroups(store, context),
7662
+ materialize: (group, context, output) => this.materializePipelineGroup(store, group, context, output),
7663
+ build: async (taskId, checkpoint) => {
7664
+ let result;
7665
+ try {
7666
+ result = await this.compileOnce(taskId, 'all', undefined, { extract: false, checkpoint });
7667
+ }
7668
+ catch (cause) {
7669
+ await this.settleProgressFailure(taskId, cause);
7670
+ throw cause;
7671
+ }
7672
+ this.settleProgressReport(taskId, result);
7673
+ if (!result.ok)
7674
+ throw new Error(result.error.message);
7675
+ return `检索索引已发布:v${result.data.version}`;
7676
+ },
7677
+ record: async (checkpoint) => {
7678
+ const audit = await this.recordExtractionRun({ id: checkpoint.id, docId: checkpoint.sourceId, title: checkpoint.report.title, summary: trialSummary(checkpoint.report), report: checkpoint.report });
7679
+ if (!audit.saved)
7680
+ throw new Error(`抽取报告保存失败:${audit.why}`);
7681
+ },
7682
+ });
7683
+ this.pipelineEngine = engine;
7684
+ if (this.closed)
7685
+ engine.close();
7686
+ return engine;
7687
+ })();
7688
+ return this.pipelineOpening;
7689
+ }
7690
+ async pipelineSources() {
7691
+ const db = await this.storeHandle();
7692
+ if (!db)
7693
+ throw new Error('来源存储不可用');
7694
+ return db.sources().map(row => ({ ...row.record, version: row.version, sourceHash: contentHashOf(row.record.content) })).sort((a, b) => a.sourceId.localeCompare(b.sourceId));
7695
+ }
7696
+ async normalizationFingerprint(checkpoint, context) {
7697
+ const registry = await this.axisRegistry();
7698
+ const report = checkpoint.normalized?.report ?? checkpoint.report;
7699
+ const query = [...report.objects, ...report.actions, ...report.scenes].flatMap(axis => [axis.name, axis.existing ?? '', axis.definition ?? '', axis.parentMention ?? '']).join('\n');
7700
+ const candidates = await registry?.promptEntries(24, query);
7701
+ const mentions = [...checkpoint.report.objects, ...checkpoint.report.actions, ...checkpoint.report.scenes].flatMap(axis => [axis.name, axis.existing ?? '', axis.parentMention ?? '']).map(normalizeAxisName).filter(Boolean);
7702
+ const resolvedIds = new Set([...report.objects, ...report.actions, ...report.scenes].map(axis => axis.id).filter(Boolean));
7703
+ // Ignore unrelated filler rows and registry recency; retain candidates that can affect these mentions.
7704
+ const relevant = Object.fromEntries(Object.entries(candidates?.entries ?? {}).map(([kind, entries]) => [kind, entries.filter(entry => resolvedIds.has(entry.id) || mentions.some(mention => [entry.name, ...entry.aliases, entry.description].map(normalizeAxisName).some(value => value.includes(mention) || (value.length > 1 && mention.includes(value))))).sort((a, b) => a.id.localeCompare(b.id))]));
7705
+ return contentHashOf(JSON.stringify({ extraction: checkpoint.id, model: { provider: context.model.provider, model: context.model.model, reasoningEffort: context.model.reasoningEffort }, candidates: relevant }));
7706
+ }
7707
+ async normalizePipelineSource(store, source, checkpoint, context, force, output) {
7708
+ const fingerprint = await this.normalizationFingerprint(checkpoint, context);
7709
+ if (!force && checkpoint.normalized?.fingerprint === fingerprint && store.head(source.sourceId).normalized?.id === checkpoint.id)
7710
+ return { reused: true, warnings: [] };
7711
+ if (!checkpoint.report.model.response)
7712
+ throw new Error('抽取记录没有原始返回,无法归一;请重新抽取该文档');
7713
+ const registry = await this.axisRegistry();
7714
+ if (!registry)
7715
+ throw new Error('实体注册表不可用');
7716
+ const draft = await registry.draft();
7717
+ try {
7718
+ const axesBlock = await this.axisPromptBlock(source.content);
7719
+ const call = context.model.call ? request => context.model.call({ ...request, onDelta: output }) : undefined;
7720
+ const report = await runWikiTrial({
7721
+ title: source.title, text: source.content, docId: source.sourceId, axes: axesBlock,
7722
+ // Reparse the captured response; this callback never requests another extraction.
7723
+ call: async () => checkpoint.report.model.response, modelRequest: checkpoint.report.model.request,
7724
+ resolveAxisSet: axes => this.resolveTrialAxisSet({ registry: draft.registry, axes, axesBlock, runId: checkpoint.id, call, modelRequest: context.model }),
7725
+ });
7726
+ if (!report.model.parsed)
7727
+ throw new Error(report.model.why ?? '抽取结果无法归一');
7728
+ const db = await this.storeHandle();
7729
+ const current = db?.source(source.sourceId);
7730
+ if (this.closed)
7731
+ throw new Error('知识库已关闭,本次归一未提交');
7732
+ if (!db || !current || current.version !== source.version || contentHashOf(current.record.content) !== source.sourceHash)
7733
+ throw new Error('来源在归一期间已变化,请重新抽取最新版本');
7734
+ const sql = db.connection();
7735
+ sql.exec('SAVEPOINT pipeline_normalize');
7736
+ try {
7737
+ draft.commit();
7738
+ // Preserve evidence history while replacing this source's active contribution.
7739
+ sql.prepare('UPDATE knowledge_sources SET withdrawn=1 WHERE document_id=?').run(source.sourceId);
7740
+ const knowledge = await this.ingestTrialKnowledge({ runId: checkpoint.id, knowledge: report.knowledge, objects: report.objects, actions: report.actions, scenes: report.scenes, sourceVersions: { [source.sourceId]: source.version } });
7741
+ const events = await this.ingestTrialEvents({ sourceId: source.sourceId, runId: checkpoint.id, events: report.events, objects: report.objects, actions: report.actions, scenes: report.scenes });
7742
+ if (knowledge.skipped || events.skipped)
7743
+ throw new Error(`出处核验或写入失败:${knowledge.skipped} 条 Knowledge、${events.skipped} 个 Event;本次归一没有提交`);
7744
+ const unresolvedKnowledgeIds = report.knowledge.filter(item => [['object', report.objects], ['action', report.actions], ['scene', report.scenes]].some(([kind, axes]) => {
7745
+ const name = item[kind];
7746
+ const axis = name ? axes.find(axis => axis.name === name) : undefined;
7747
+ return name !== null && (!axis || axis.outcome === 'ambiguous' || axis.outcome === 'no-registry');
7748
+ })).map(item => knowledge.ids[item.id]).filter(Boolean);
7749
+ const normalized = { report, knowledgeIds: knowledge.ids, eventIds: events.ids, unresolvedKnowledgeIds, fingerprint: '' };
7750
+ // Store the post-resolution candidate snapshot so learning its own entities does not invalidate this run.
7751
+ normalized.fingerprint = await this.normalizationFingerprint({ ...checkpoint, normalized }, context);
7752
+ store.saveNormalization(checkpoint, normalized);
7753
+ sql.exec('RELEASE pipeline_normalize');
7754
+ // Updating Event references is a cheap relation refresh. It does not reorganize or index card content.
7755
+ const eventReferences = await this.syncEventReferences();
7756
+ return { reused: false, warnings: [...events.warnings, ...eventReferences.warnings, ...(unresolvedKnowledgeIds.length ? [`${unresolvedKnowledgeIds.length} 条 Knowledge 的坐标仍有歧义,保留事实与出处,暂不物化。`] : [])] };
7757
+ }
7758
+ catch (cause) {
7759
+ sql.exec('ROLLBACK TO pipeline_normalize; RELEASE pipeline_normalize');
7760
+ throw cause;
7761
+ }
7762
+ }
7763
+ finally {
7764
+ draft.close();
7765
+ }
7766
+ }
7767
+ async activePipelineKnowledge() {
7768
+ const knowledge = await this.knowledgeStore();
7769
+ const db = await this.storeHandle();
7770
+ if (!knowledge || !db)
7771
+ return [];
7772
+ const rows = [];
7773
+ for (let offset = 0;; offset += 500) {
7774
+ const page = await knowledge.list({ excludeWithdrawn: true, limit: 500, offset });
7775
+ rows.push(...page.knowledge.filter(row => row.status !== 'deprecated').map(row => assessKnowledgeEvidence(row, db)).map(row => ({ ...row, sources: row.sources.filter(source => !source.withdrawn && source.evidenceStatus !== 'missing') })).filter(row => row.sources.length > 0));
7776
+ if (offset + page.knowledge.length >= page.total)
7777
+ break;
7778
+ }
7779
+ return rows;
7780
+ }
7781
+ async materializationGroups(store, context) {
7782
+ const unresolved = store.unresolvedKnowledge();
7783
+ const groups = new Map();
7784
+ let blocked = 0;
7785
+ const groupFor = (coordinate) => {
7786
+ const key = coordinateKeyOf(coordinate);
7787
+ if (!groups.has(key))
7788
+ groups.set(key, { coordinate, knowledge: [] });
7789
+ return groups.get(key);
7790
+ };
7791
+ for (const row of await this.activePipelineKnowledge()) {
7792
+ const coordinate = { objectId: row.objectId, actionId: row.actionId, sceneId: row.sceneId };
7793
+ if (coordinateIsEmpty(coordinate)) {
7794
+ blocked += 1;
7795
+ continue;
7796
+ }
7797
+ groupFor(coordinate).knowledge.push(row);
7798
+ }
7799
+ const db = await this.storeHandle();
7800
+ const eventsByCoordinate = new Map();
7801
+ for (const event of db?.events() ?? []) {
7802
+ const coordinate = event.coordinateIds;
7803
+ if (!coordinate.objectId || !coordinate.actionId || !coordinate.sceneId)
7804
+ continue;
7805
+ const key = coordinateKeyOf(coordinate);
7806
+ const bucket = eventsByCoordinate.get(key) ?? [];
7807
+ bucket.push(event);
7808
+ eventsByCoordinate.set(key, bucket);
7809
+ }
7810
+ const cards = await this.loadCards();
7811
+ const byCoordinate = new Map(cards.filter(card => card.coordinate).map(card => [coordinateKeyOf(card.coordinate), card]));
7812
+ const result = [];
7813
+ for (const [key, value] of groups) {
7814
+ const names = await this.coordinateNames(value.coordinate);
7815
+ const existing = byCoordinate.get(key);
7816
+ // Event 只作为卡片引用;它的增删和正文变化不得触发 Knowledge 重新组织。
7817
+ const fingerprint = contentHashOf(JSON.stringify({ knowledge: value.knowledge.map(row => ({ id: row.id, text: row.text, status: row.status, sources: row.sources.filter(source => !source.withdrawn) })).sort((a, b) => a.id.localeCompare(b.id)), profile: context.profile, organize: context.organize, model: context.organize ? { provider: context.model.provider, model: context.model.model, reasoningEffort: context.model.reasoningEffort } : undefined }));
7818
+ const receipt = store.materialization(key);
7819
+ const current = existing && receipt?.fingerprint === fingerprint && receipt.cardId === existing.entityId;
7820
+ const contributingSources = [...new Set(value.knowledge.flatMap(row => row.sources.map(source => source.documentId)))].filter(Boolean).sort();
7821
+ const needsNormalization = value.knowledge.some(row => unresolved.ids.has(row.id)) || contributingSources.some(id => {
7822
+ const source = db?.source(id);
7823
+ const head = store.head(id);
7824
+ return head.normalized ? head.normalized.sourceVersion !== source?.version || head.extracted?.id !== head.normalized.id : value.knowledge.some(row => row.sources.some(evidence => evidence.documentId === id && evidence.toVersion !== undefined && evidence.toVersion !== source?.version));
7825
+ });
7826
+ if (needsNormalization)
7827
+ blocked += value.knowledge.length;
7828
+ result.push({ key, title: existing?.title ?? cardNameFor(names.object || null, names.action || null, names.scene || null), coordinate: value.coordinate, knowledgeCount: value.knowledge.length, eventCount: eventsByCoordinate.get(key)?.length ?? 0, sourceIds: contributingSources, ...(existing ? { cardId: existing.entityId } : {}), state: needsNormalization ? 'blocked' : current ? 'current' : existing ? 'changed' : 'new', ...(needsNormalization ? { reason: '分组中的来源已变化、新抽取待归一或坐标有歧义,请先完成归一' } : {}), fingerprint });
7829
+ }
7830
+ for (const card of cards) {
7831
+ if (!card.coordinate || groups.has(coordinateKeyOf(card.coordinate)) || !store.materialization(coordinateKeyOf(card.coordinate)) || card.status === 'deprecated')
7832
+ continue;
7833
+ result.push({ key: coordinateKeyOf(card.coordinate), title: card.title, coordinate: card.coordinate, knowledgeCount: 0, eventCount: 0, sourceIds: card.sourceRefs, cardId: card.entityId, state: card.sourceRefs.some(id => unresolved.sources.has(id)) ? 'blocked' : 'withdrawn', ...(card.sourceRefs.some(id => unresolved.sources.has(id)) ? { reason: '来源中的坐标仍有歧义,请先完成归一' } : {}), fingerprint: 'withdrawn' });
7834
+ }
7835
+ return { groups: result.sort((a, b) => a.title.localeCompare(b.title)), blocked };
7836
+ }
7837
+ async materializePipelineGroup(store, group, context, output) {
7838
+ const db = await this.storeHandle();
7839
+ if (!db)
7840
+ throw new Error('知识库不可用');
7841
+ const fresh = (await this.materializationGroups(store, context)).groups.find(item => item.key === group.key);
7842
+ if (!fresh || fresh.fingerprint !== group.fingerprint)
7843
+ throw new Error('卡片分组在处理期间已变化,请刷新并重试');
7844
+ if (fresh.state === 'blocked')
7845
+ throw new Error(fresh.reason);
7846
+ const storedExisting = group.cardId ? await this.readCard(group.cardId) : undefined;
7847
+ const existing = storedExisting
7848
+ ? { ...storedExisting, content: stripEventReferenceSection(storedExisting.content) }
7849
+ : undefined;
7850
+ if (group.state === 'withdrawn') {
7851
+ if (!existing)
7852
+ return;
7853
+ const written = await this.writeCard({ ...existing, status: 'deprecated', origin: 'compile' });
7854
+ if (!written.ok)
7855
+ throw new Error(written.error.message);
7856
+ store.materialized(group.key, group.fingerprint, existing.entityId);
7857
+ return;
7858
+ }
7859
+ const allKnowledge = await this.activePipelineKnowledge();
7860
+ const knowledge = allKnowledge.filter(row => coordinateKeyOf({ objectId: row.objectId, actionId: row.actionId, sceneId: row.sceneId }) === group.key);
7861
+ const events = db.events().filter(event => coordinateKeyOf(event.coordinateIds) === group.key);
7862
+ const profile = context.profile;
7863
+ const entityType = (profile?.id ?? 'concept');
7864
+ let title = group.title;
7865
+ let content = `## 知识\n\n${knowledge.map(row => `- ${row.text}`).join('\n')}`;
7866
+ let summary = knowledge[0]?.text ?? '';
7867
+ let placements = knowledge.map((row, index) => ({ knowledgeId: row.id, section: '知识', ordinal: index + 1, origin: 'compiler' }));
7868
+ let metadata = { ...(existing?.metadata ?? {}), pipeline_materialization: true };
7869
+ const entityId = existing?.entityId ?? `coord-${hash(group.key)}`;
7870
+ const imagePlan = planSourceImages({
7871
+ cards: (await this.materializationGroups(store, context)).groups.filter(item => item.state !== 'withdrawn').map(item => ({ key: item.key, content: allKnowledge.filter(row => coordinateKeyOf({ objectId: row.objectId, actionId: row.actionId, sceneId: row.sceneId }) === item.key).map(row => row.text).join('\n\n'), sourceRefs: item.sourceIds })),
7872
+ sources: (await this.loadSources()).map(source => ({ sourceId: source.sourceId, content: source.content })),
7873
+ });
7874
+ const images = imagePlan.byCard.get(group.key) ?? [];
7875
+ const related = existing ? await this.relatedForCard(existing) : {};
7876
+ metadata.citations = citationsFrom({ placements, knowledge: new Map(knowledge.map(row => [row.id, { source: row.sources[0]?.documentId, heading: row.sources[0]?.headingPath.at(-1) }])) });
7877
+ if (context.organize && knowledge.length > 0) {
7878
+ if (!context.model.call)
7879
+ throw new Error(`卡片组织模型不可用:${context.model.why ?? '请选择模型'}`);
7880
+ const result = await this.organizeCard({ knowledge, images, ...related, cardTitle: title, coordinate: group.coordinate, profileId: entityType, force: true, suggestedSections: profile?.sections.map(section => section.label), requiredSections: profile?.sections.filter(section => section.required).map(section => section.label), existing, call: request => context.model.call({ ...request, onDelta: output }) });
7881
+ if (result.kind !== 'applied')
7882
+ throw new Error(result.kind === 'reused' ? '组织结果已变化,请刷新后重试' : result.warnings.join(';'));
7883
+ title = result.title;
7884
+ content = result.content;
7885
+ summary = result.summary;
7886
+ placements = result.placements;
7887
+ metadata = { ...metadata, layout: result.layout, citations: result.citations };
7888
+ }
7889
+ for (const image of images)
7890
+ content = insertImage(content, image);
7891
+ const eventIds = events.map(event => event.id).sort();
7892
+ content = stripEventReferenceSection(content);
7893
+ metadata.event_refs = JSON.stringify(eventIds);
7894
+ // Freeze the selected group's facts, not an arbitrary subset of its rows.
7895
+ const latest = (await this.materializationGroups(store, context)).groups.find(item => item.key === group.key);
7896
+ if (!latest || latest.fingerprint !== group.fingerprint || latest.state === 'blocked')
7897
+ throw new Error('卡片组织期间事实发生变化,结果未提交,请重试');
7898
+ if (this.closed)
7899
+ throw new Error('知识库已关闭,本次物化未提交');
7900
+ const written = await this.writeCard({ entityType, entityId, title, content, summary, sourceRefs: group.sourceIds, status: existing?.status === 'deprecated' ? 'unreviewed' : existing?.status ?? 'unreviewed', coordinate: group.coordinate, placements, metadata, origin: 'compile' });
7901
+ if (!written.ok)
7902
+ throw new Error(written.error.message);
7903
+ db.setCardEvents(entityId, eventIds);
7904
+ store.materialized(group.key, group.fingerprint, entityId);
7905
+ }
7906
+ async pipelineStatus() {
7907
+ const pipeline = await this.pipeline();
7908
+ const sources = await this.pipelineSources();
7909
+ const context = { model: this.modelSeamNow(), note: this.extractionNote(), profile: this.organizeProfile(), organize: this.config.cardOrganize, format: 'table' };
7910
+ const rows = await Promise.all(sources.map(async (source) => {
7911
+ const head = pipeline.store.head(source.sourceId);
7912
+ const extracted = head.extracted?.sourceVersion === source.version && head.extracted.sourceHash === source.sourceHash;
7913
+ const normalized = extracted && head.normalized?.id === head.extracted?.id && head.normalized?.normalized?.fingerprint === await this.normalizationFingerprint(head.normalized, context);
7914
+ return { id: source.sourceId, title: source.title, version: source.version, extractionId: head.extracted?.id, normalizedId: head.normalized?.id, state: normalized ? 'normalized' : extracted ? 'extracted' : head.extracted ? 'outdated' : 'pending' };
7915
+ }));
7916
+ const { groups, blocked } = await this.materializationGroups(pipeline.store, context);
7917
+ const db = await this.derivedHandle();
7918
+ const manifest = db?.artifact('build-manifest.json');
7919
+ const hashes = manifest ? JSON.parse(manifest).cardHashes ?? {} : {};
7920
+ const cards = await this.loadCards();
7921
+ const indexPending = cards.some(card => hashes[`${card.entityType}:${card.entityId}`] !== this.cardHashOf(card)) || Object.keys(hashes).length !== cards.length;
7922
+ return { sources: rows, groups, blocked, pendingNew: groups.filter(group => group.state === 'new').length, pendingUpdated: groups.filter(group => group.state === 'changed').length, pendingWithdrawn: groups.filter(group => group.state === 'withdrawn').length, indexPending };
7923
+ }
7653
7924
  /** 抽取轨迹列表(新→旧,不含报告本体)。 */
7654
7925
  async listExtractionRuns(query = {}) {
7655
7926
  const store = await this.extractionRuns();
@@ -7853,10 +8124,12 @@ export class KnowledgeRuntime {
7853
8124
  * 它」—— 那两件事分别在 `enqueueCompile()` 与 `rebuild()` / `startCompile()` 里。
7854
8125
  */
7855
8126
  async compileOnce(runId, only = 'all', lineage, options = {}) {
7856
- await this.environmentGuard?.();
8127
+ if (only === 'all' || only === 'embeddings')
8128
+ await this.environmentGuard?.('embedding');
7857
8129
  this.publication = { artifacts: new Map() };
7858
8130
  const configAtStart = JSON.stringify({ config: this.config, fingerprint: this.embeddingFingerprint() });
7859
8131
  try {
8132
+ await options.checkpoint?.();
7860
8133
  await this.loadCompiledState();
7861
8134
  const result = await this.prepareCompile(runId, only, lineage, options);
7862
8135
  await this.checkpointCompileTask(runId);
@@ -7889,6 +8162,7 @@ export class KnowledgeRuntime {
7889
8162
  await this.checkpointCompileTask(runId);
7890
8163
  if (JSON.stringify({ config: this.config, fingerprint: this.embeddingFingerprint() }) !== configAtStart)
7891
8164
  throw new Error('COMPILE_CONFIG_CHANGED: 编译期间设置已变化,请重新编译');
8165
+ await options.checkpoint?.();
7892
8166
  db.publish(this.publication);
7893
8167
  this.vectorIndex = null;
7894
8168
  }
@@ -8042,7 +8316,7 @@ export class KnowledgeRuntime {
8042
8316
  tally.knowledge > 0
8043
8317
  ? `抽出知识 ${tally.knowledge} 条`
8044
8318
  : '未用模型:正文按 ## 小节切',
8045
- tally.skipped > 0 ? `跳过未变 ${tally.skipped} 份` : '指纹全变,没有可复用的',
8319
+ tally.skipped > 0 ? `复用未变来源 ${tally.skipped} 份` : options.forceExtract ? '本次为手动强制重抽' : '正在检查新增或变化的来源',
8046
8320
  ];
8047
8321
  if (done < total) {
8048
8322
  setStep('knowledge', 'running', knowledgeFacts);
@@ -9043,7 +9317,7 @@ export class KnowledgeRuntime {
9043
9317
  * 一次召回十来张卡,而库里可能有上千张。
9044
9318
  */
9045
9319
  try {
9046
- placements = snapshot?.placements.length ? new Map(snapshot.placements) : store.placementsOf([...new Set(hits.map(hit => hit.entityId))]);
9320
+ placements = snapshot ? new Map(snapshot.placements) : store.placementsOf([...new Set(hits.map(hit => hit.entityId))]);
9047
9321
  }
9048
9322
  catch {
9049
9323
  return;
@@ -9065,7 +9339,7 @@ export class KnowledgeRuntime {
9065
9339
  const wanted = new Set(perHit.flat().map(row => row.knowledgeId));
9066
9340
  if (wanted.size === 0)
9067
9341
  return;
9068
- const rows = snapshot && snapshot.knowledge.length > 0
9342
+ const rows = snapshot
9069
9343
  ? new Map(snapshot.knowledge.filter(row => wanted.has(row.id) && knowledge !== null)
9070
9344
  .flatMap(row => {
9071
9345
  // Deleted facts never reappear through a historical snapshot.
@@ -9159,14 +9433,17 @@ export class KnowledgeRuntime {
9159
9433
  if (!Number.isInteger(requestedTopN) || requestedTopN < 1 || (options.contextTopN !== undefined && requestedTopN > recallTopK))
9160
9434
  return failure(empty(), 'CONTEXT_TOP_N_INVALID', `上下文 top_n 必须是 1 到 ${recallTopK} 的整数`, false);
9161
9435
  try {
9162
- await this.environmentGuard?.();
9436
+ if (recallMode !== 'fts')
9437
+ await this.environmentGuard?.('embedding');
9438
+ if (options.rerank !== false && this.config.rerankProvider !== 'none')
9439
+ await this.environmentGuard?.('rerank');
9163
9440
  }
9164
9441
  catch (cause) {
9165
9442
  return failure(empty(), 'ENVIRONMENT_NOT_READY', cause instanceof Error ? cause.message : String(cause), true, '在 Tita 设置中完成环境准备并重新自检。');
9166
9443
  }
9167
9444
  const derived = await this.derivedHandle();
9168
9445
  const liveCards = await this.loadCards();
9169
- const liveKeys = new Set(liveCards.map(card => cardKey(card)));
9446
+ const liveKeys = new Set(liveCards.filter(card => card.status !== 'deprecated').map(card => cardKey(card)));
9170
9447
  const snapshotHash = derived?.artifactHash(COMPILE_SNAPSHOT);
9171
9448
  const cached = snapshotHash && this.recallSnapshotCache?.hash === snapshotHash ? this.recallSnapshotCache : undefined;
9172
9449
  const raw = cached ? undefined : derived?.artifact(COMPILE_SNAPSHOT);
@@ -9327,7 +9604,7 @@ export class KnowledgeRuntime {
9327
9604
  ].join('\n');
9328
9605
  const reranked = await this.rerankNotes(rerankQuery, hits, this.config.rerankThreshold);
9329
9606
  rerankScored = reranked.scored ?? [];
9330
- hits = reranked.hits.slice(0, recallTopK);
9607
+ hits = reranked.hits;
9331
9608
  rerank = reranked.result;
9332
9609
  if (reranked.warning)
9333
9610
  warnings.push(reranked.warning);
@@ -9337,7 +9614,7 @@ export class KnowledgeRuntime {
9337
9614
  }
9338
9615
  // 重排换的是相关性顺序,评分加权仍然要压在最上面 —— 否则「review 过的排前面一点」
9339
9616
  // 只在关掉重排时成立,而重排默认是开着的。
9340
- hits = [...hits].sort((left, right) => rankKey(right) - rankKey(left) || left.unitId.localeCompare(right.unitId));
9617
+ hits = [...hits].sort((left, right) => rankKey(right) - rankKey(left) || left.unitId.localeCompare(right.unitId)).slice(0, recallTopK);
9341
9618
  // --- R4 · 证据片段 -----------------------------------------------------
9342
9619
  const truth = await this.storeHandle();
9343
9620
  cards = cards.filter(card => truth?.card(card.entityId) !== undefined && card.sourceRefs.every(ref => truth?.source(ref) !== undefined));