dsh-data-domain-knowledge 0.1.1-alpha.2 → 0.1.1-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/README.md +23 -16
  2. package/lib/client.js +596 -841
  3. package/lib/contract-refs.d.ts +0 -1
  4. package/lib/doc-page.js +7 -7
  5. package/lib/pages.js +928 -1145
  6. package/lib/types/agent-behavior-rules.d.ts.map +1 -1
  7. package/lib/types/agent-behavior-rules.js +1 -2
  8. package/lib/types/agent-behavior-rules.js.map +1 -1
  9. package/lib/types/axis-contract.js +1 -1
  10. package/lib/types/axis-contract.js.map +1 -1
  11. package/lib/types/card-organize.d.ts +2 -1
  12. package/lib/types/card-organize.d.ts.map +1 -1
  13. package/lib/types/card-organize.js +5 -3
  14. package/lib/types/card-organize.js.map +1 -1
  15. package/lib/types/config-schema.d.ts +1 -1
  16. package/lib/types/config-schema.js +1 -1
  17. package/lib/types/convert/host.d.ts +2 -3
  18. package/lib/types/convert/host.d.ts.map +1 -1
  19. package/lib/types/convert/host.js +5 -2
  20. package/lib/types/convert/host.js.map +1 -1
  21. package/lib/types/convert/ingest.d.ts +2 -0
  22. package/lib/types/convert/ingest.d.ts.map +1 -1
  23. package/lib/types/convert/ingest.js +1 -0
  24. package/lib/types/convert/ingest.js.map +1 -1
  25. package/lib/types/diagnosis/types.d.ts +1 -1
  26. package/lib/types/diagnosis/types.js +1 -1
  27. package/lib/types/images.js +1 -1
  28. package/lib/types/index.d.ts +0 -4
  29. package/lib/types/index.d.ts.map +1 -1
  30. package/lib/types/index.js +30 -41
  31. package/lib/types/index.js.map +1 -1
  32. package/lib/types/knowledge-contract.d.ts +6 -0
  33. package/lib/types/knowledge-contract.d.ts.map +1 -1
  34. package/lib/types/knowledge-contract.js +6 -2
  35. package/lib/types/knowledge-contract.js.map +1 -1
  36. package/lib/types/layout.d.ts +9 -26
  37. package/lib/types/layout.d.ts.map +1 -1
  38. package/lib/types/layout.js +10 -28
  39. package/lib/types/layout.js.map +1 -1
  40. package/lib/types/materialize-snapshot.d.ts +5 -0
  41. package/lib/types/materialize-snapshot.d.ts.map +1 -1
  42. package/lib/types/migrate.d.ts +3 -1
  43. package/lib/types/migrate.d.ts.map +1 -1
  44. package/lib/types/migrate.js +3 -1
  45. package/lib/types/migrate.js.map +1 -1
  46. package/lib/types/organize-contract.d.ts.map +1 -1
  47. package/lib/types/organize-contract.js +2 -1
  48. package/lib/types/organize-contract.js.map +1 -1
  49. package/lib/types/pipeline/engine.d.ts +1 -0
  50. package/lib/types/pipeline/engine.d.ts.map +1 -1
  51. package/lib/types/pipeline/engine.js +9 -3
  52. package/lib/types/pipeline/engine.js.map +1 -1
  53. package/lib/types/pipeline/types.d.ts +6 -0
  54. package/lib/types/pipeline/types.d.ts.map +1 -1
  55. package/lib/types/pipeline/types.js.map +1 -1
  56. package/lib/types/present.d.ts +1 -1
  57. package/lib/types/present.js +1 -1
  58. package/lib/types/preset-ids.d.ts +0 -1
  59. package/lib/types/preset-ids.d.ts.map +1 -1
  60. package/lib/types/preset-ids.js +0 -3
  61. package/lib/types/preset-ids.js.map +1 -1
  62. package/lib/types/query-analysis.d.ts +0 -5
  63. package/lib/types/query-analysis.d.ts.map +1 -1
  64. package/lib/types/query-analysis.js +0 -16
  65. package/lib/types/query-analysis.js.map +1 -1
  66. package/lib/types/recall-delivery.d.ts +36 -0
  67. package/lib/types/recall-delivery.d.ts.map +1 -0
  68. package/lib/types/recall-delivery.js +16 -0
  69. package/lib/types/recall-delivery.js.map +1 -0
  70. package/lib/types/runtime.d.ts +26 -12
  71. package/lib/types/runtime.d.ts.map +1 -1
  72. package/lib/types/runtime.js +231 -106
  73. package/lib/types/runtime.js.map +1 -1
  74. package/lib/types/settings-volatile.d.ts +0 -7
  75. package/lib/types/settings-volatile.d.ts.map +1 -1
  76. package/lib/types/settings-volatile.js +0 -7
  77. package/lib/types/settings-volatile.js.map +1 -1
  78. package/lib/types/source-identity.d.ts +3 -0
  79. package/lib/types/source-identity.d.ts.map +1 -0
  80. package/lib/types/source-identity.js +5 -0
  81. package/lib/types/source-identity.js.map +1 -0
  82. package/lib/types/store/axis-references.d.ts.map +1 -1
  83. package/lib/types/store/axis-references.js +5 -3
  84. package/lib/types/store/axis-references.js.map +1 -1
  85. package/lib/types/store/knowledge-db.d.ts +1 -1
  86. package/lib/types/store/knowledge-db.d.ts.map +1 -1
  87. package/lib/types/store/knowledge-db.js +16 -2
  88. package/lib/types/store/knowledge-db.js.map +1 -1
  89. package/lib/types/types.d.ts +5 -4
  90. package/lib/types/types.d.ts.map +1 -1
  91. package/lib/types/types.js +6 -3
  92. package/lib/types/types.js.map +1 -1
  93. package/lib/types/wiki-availability.d.ts +11 -0
  94. package/lib/types/wiki-availability.d.ts.map +1 -0
  95. package/lib/types/wiki-availability.js +39 -0
  96. package/lib/types/wiki-availability.js.map +1 -0
  97. package/lib/types/wiki-finalize.d.ts +4 -2
  98. package/lib/types/wiki-finalize.d.ts.map +1 -1
  99. package/lib/types/wiki-finalize.js +9 -27
  100. package/lib/types/wiki-finalize.js.map +1 -1
  101. package/lib/types/wiki-links.d.ts +35 -0
  102. package/lib/types/wiki-links.d.ts.map +1 -0
  103. package/lib/types/wiki-links.js +129 -0
  104. package/lib/types/wiki-links.js.map +1 -0
  105. package/package.json +16 -16
  106. package/scripts/doctor.mjs +0 -7
  107. package/scripts/setup-embedding.sh +0 -0
  108. package/src/agent-behavior-rules.ts +1 -2
  109. package/src/axis-contract.ts +1 -1
  110. package/src/card-organize.ts +5 -3
  111. package/src/client/AttributionEventGuide.tsx +3 -3
  112. package/src/client/AxisMatrix.tsx +86 -19
  113. package/src/client/DirectoryPicker.tsx +0 -2
  114. package/src/client/GuideArchitectureCanvas.tsx +5 -12
  115. package/src/client/ImportFilesDialog.tsx +1 -0
  116. package/src/client/InfoHint.tsx +1 -1
  117. package/src/client/KnowledgeBaseTab.tsx +113 -39
  118. package/src/client/KnowledgePanel.tsx +16 -9
  119. package/src/client/OperationLogTab.tsx +1 -1
  120. package/src/client/RecallPanel.tsx +5 -16
  121. package/src/client/TabGuide.tsx +3 -7
  122. package/src/client/WikiFlowPanel.tsx +10 -11
  123. package/src/client/arch/NodeLogicPanel.tsx +3 -3
  124. package/src/client/arch/model.ts +5 -113
  125. package/src/client/architecture.css +1 -1
  126. package/src/client/architecture.tsx +13 -27
  127. package/src/client/attribution/AttributionTab.tsx +141 -10
  128. package/src/client/pages.css +2 -2
  129. package/src/client/pages.tsx +4 -11
  130. package/src/client/styles.css +15 -96
  131. package/src/config-schema.ts +1 -1
  132. package/src/convert/host.ts +7 -5
  133. package/src/convert/ingest.ts +3 -0
  134. package/src/diagnosis/types.ts +1 -1
  135. package/src/images.ts +1 -1
  136. package/src/index.ts +27 -49
  137. package/src/knowledge-contract.ts +7 -2
  138. package/src/layout.ts +10 -32
  139. package/src/materialize-snapshot.ts +1 -0
  140. package/src/migrate.ts +3 -1
  141. package/src/organize-contract.ts +2 -1
  142. package/src/pipeline/engine.ts +9 -3
  143. package/src/pipeline/types.ts +2 -0
  144. package/src/present.ts +1 -1
  145. package/src/preset-ids.ts +0 -4
  146. package/src/query-analysis.ts +0 -20
  147. package/src/recall-delivery.ts +19 -0
  148. package/src/runtime.ts +220 -102
  149. package/src/settings-volatile.ts +0 -7
  150. package/src/source-identity.ts +4 -0
  151. package/src/store/axis-references.ts +5 -3
  152. package/src/store/knowledge-db.ts +14 -2
  153. package/src/types.ts +6 -4
  154. package/src/wiki-availability.ts +37 -0
  155. package/src/wiki-finalize.ts +9 -25
  156. package/src/wiki-links.ts +120 -0
  157. package/lib/types/intent/engine.d.ts +0 -122
  158. package/lib/types/intent/engine.d.ts.map +0 -1
  159. package/lib/types/intent/engine.js +0 -488
  160. package/lib/types/intent/engine.js.map +0 -1
  161. package/lib/types/intent/host.d.ts +0 -68
  162. package/lib/types/intent/host.d.ts.map +0 -1
  163. package/lib/types/intent/host.js +0 -249
  164. package/lib/types/intent/host.js.map +0 -1
  165. package/lib/types/intent/scope.d.ts +0 -25
  166. package/lib/types/intent/scope.d.ts.map +0 -1
  167. package/lib/types/intent/scope.js +0 -116
  168. package/lib/types/intent/scope.js.map +0 -1
  169. package/lib/types/intent/store.d.ts +0 -33
  170. package/lib/types/intent/store.d.ts.map +0 -1
  171. package/lib/types/intent/store.js +0 -239
  172. package/lib/types/intent/store.js.map +0 -1
  173. package/lib/types/intent/tools.d.ts +0 -62
  174. package/lib/types/intent/tools.d.ts.map +0 -1
  175. package/lib/types/intent/tools.js +0 -130
  176. package/lib/types/intent/tools.js.map +0 -1
  177. package/lib/types/intent/types.d.ts +0 -170
  178. package/lib/types/intent/types.d.ts.map +0 -1
  179. package/lib/types/intent/types.js +0 -56
  180. package/lib/types/intent/types.js.map +0 -1
  181. package/src/client/IntentLogic.tsx +0 -192
  182. package/src/client/IntentTab.tsx +0 -343
  183. package/src/intent/README.md +0 -42
  184. package/src/intent/engine.ts +0 -515
  185. package/src/intent/host.ts +0 -315
  186. package/src/intent/scope.ts +0 -125
  187. package/src/intent/store.ts +0 -218
  188. package/src/intent/tools.ts +0 -154
  189. package/src/intent/types.ts +0 -229
package/src/runtime.ts CHANGED
@@ -1,4 +1,8 @@
1
- import { wikiProjection, procedureText, type ProcedureContent } from './knowledge-contract.js'
1
+ import { compileWikiNavigation, scanWikiLinks, stripWikiNavigation, wikiIndexContent, wikiLinkCandidates, wikiReference, type WikiReference } from './wiki-links.js'
2
+ import { wikiProjection, wikiGroupKey, procedureText, type ProcedureContent, type WikiKind } from './knowledge-contract.js'
3
+ import { wikiUnavailableReason } from './wiki-availability.js'
4
+ import { newSourceId } from './source-identity.js'
5
+ import { agentContextItem } from './recall-delivery.js'
2
6
  import { buildDeriveSystem, DERIVE_CONTRACT_VERSION } from './derive-contract.js'
3
7
  import { createHash, randomUUID } from 'node:crypto'
4
8
  import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
@@ -40,7 +44,7 @@ import type { MaterializationGroup, PipelineStatus } from './pipeline/types.js'
40
44
  import type { MaterializationSnapshot } from './materialize-snapshot.js'
41
45
  import { candidatesFromReport, coordinateIdsOf, coordinateIsEmpty, type MaterializedCard } from './wiki-materialize.js'
42
46
  import {
43
- sectionRecords,
47
+ sectionRecords, ORGANIZE_VERSION,
44
48
  buildOrganizePrompt, buildOrganizeSystem, checkOrganize, citationsFrom, layoutHash, layoutOf,
45
49
  parseOrganize, placementsOf, renderFootnoteDefinitions, renderOrganizeBody,
46
50
  type CardCitationRecord, type CardLayoutRecord, type OrganizeInput, type OrganizePlan,
@@ -1181,7 +1185,7 @@ function parseLineage(value: unknown): LineageContext | undefined {
1181
1185
  if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined
1182
1186
  const record = value as Record<string, unknown>
1183
1187
  const lineage: LineageContext = {}
1184
- for (const key of ['sessionId', 'turnId', 'intentId', 'taskId', 'subtaskId', 'parentSubtaskId', 'runId', 'traceId', 'domain'] as const) {
1188
+ for (const key of ['sessionId', 'turnId', 'taskId', 'subtaskId', 'parentSubtaskId', 'runId', 'traceId', 'domain'] as const) {
1185
1189
  if (typeof record[key] === 'string' && record[key].trim()) lineage[key] = record[key].trim().slice(0, 160)
1186
1190
  }
1187
1191
  return Object.keys(lineage).length > 0 ? lineage : undefined
@@ -1311,7 +1315,7 @@ export function splitWikiNotes(card: Pick<KnowledgeCard, 'summary' | 'content'>)
1311
1315
  const sections: Array<{ heading: string; lines: string[] }> = []
1312
1316
  let current: { heading: string; lines: string[] } | undefined
1313
1317
  let fence: string | undefined
1314
- for (const line of card.content.trim().split('\n')) {
1318
+ for (const line of stripWikiNavigation(card.content).trim().split('\n')) {
1315
1319
  const code = line.match(/^\s*(?:(?:\d+[.)]|[-+*])\s+)?(`{3,}|~{3,})(.*)$/)
1316
1320
  if (code) {
1317
1321
  if (!fence) fence = code[1]!
@@ -3179,8 +3183,8 @@ export class KnowledgeRuntime {
3179
3183
  * 每条的原因仍然在 `sources[].message` 里。
3180
3184
  */
3181
3185
  async importSources(input: {
3182
- sources: Array<{ title?: string; content: string; externalId?: string; metadata?: Record<string, unknown>; allowDuplicateTitle?: boolean }>
3183
- /** 默认编译(`only: 'wiki'`)。关掉它就只做写入,留给调用方自己决定何时编译。 */
3186
+ sources: Array<{ sourceId?: string; title?: string; content: string; externalId?: string; metadata?: Record<string, unknown>; allowDuplicateTitle?: boolean }>
3187
+ /** 默认只保存 Source Raw;显式 true 才运行完整处理流程。 */
3184
3188
  compile?: boolean
3185
3189
  lineage?: LineageContext
3186
3190
  }): Promise<Envelope<ImportSourcesResult>> {
@@ -3201,6 +3205,7 @@ export class KnowledgeRuntime {
3201
3205
  for (const [index, item] of items.entries()) {
3202
3206
  const title = item?.title?.trim() || undefined
3203
3207
  const result = await this.addSource({
3208
+ ...(item?.sourceId ? { sourceId: item.sourceId } : {}),
3204
3209
  ...(title ? { title } : {}),
3205
3210
  content: item?.content,
3206
3211
  ...(item?.externalId ? { externalId: item.externalId } : {}),
@@ -3321,7 +3326,7 @@ export class KnowledgeRuntime {
3321
3326
  provenance?: SourceProvenance
3322
3327
  /**
3323
3328
  * 允许与已有的来源同标题(0051)。默认 `false`:重名是错误,拒收并要求改标题。
3324
- * 传 `true` 时落成 `<标题>-<内容哈希前 6 位>.md`,并回一条 warning。
3329
+ * 传 `true` 时按各自 Source Raw ID 并存,并回一条 warning。
3325
3330
  */
3326
3331
  allowDuplicateTitle?: boolean
3327
3332
  }): Promise<Envelope<{ sourceId: string; lineage?: LineageContext }>> {
@@ -3344,7 +3349,7 @@ export class KnowledgeRuntime {
3344
3349
  if (db === null) {
3345
3350
  return failure({ sourceId: '' }, 'STORE_UNAVAILABLE', this.storeProblem ?? '知识库不可用', true, '检查 Node 版本(正文进库之后需要 22.5+ 的 node:sqlite)与知识库文件的属主标记。')
3346
3351
  }
3347
- const sourceId = input.sourceId?.trim() || hash(input.externalId ?? input.content)
3352
+ const sourceId = input.sourceId?.trim() || (input.externalId?.startsWith('upload:') ? newSourceId() : hash(input.externalId ?? input.content))
3348
3353
  if (!SAFE_ID.test(sourceId)) return failure({ sourceId: '' }, 'SOURCE_ID_INVALID', 'source_id 含有非法字符', false)
3349
3354
  const title = input.title?.trim() || sourceId
3350
3355
  /**
@@ -4386,7 +4391,8 @@ export class KnowledgeRuntime {
4386
4391
 
4387
4392
  const listed = input.knowledge ? { knowledge: input.knowledge, total: input.knowledge.length } : await store.list({
4388
4393
  objectId: input.coordinate.objectId,
4389
- ...(input.existing?.metadata.wiki_kind ? { kind: input.existing.metadata.wiki_kind === 'way' ? 'procedure' as const : 'fact' as const, ...(input.coordinate.actionId ? { actionId: input.coordinate.actionId } : {}) } : { actionId: input.coordinate.actionId, sceneId: input.coordinate.sceneId }),
4394
+ actionId: input.coordinate.actionId, sceneId: input.coordinate.sceneId,
4395
+ ...(input.existing?.metadata.wiki_kind ? { kind: input.existing.metadata.wiki_kind === 'way' ? 'procedure' as const : 'fact' as const } : {}),
4390
4396
  excludeWithdrawn: true,
4391
4397
  limit: MAX_ORGANIZE_KNOWLEDGE,
4392
4398
  })
@@ -4438,7 +4444,7 @@ export class KnowledgeRuntime {
4438
4444
  hash,
4439
4445
  layout,
4440
4446
  reuse,
4441
- system: buildOrganizeSystem(input.coordinate.actionId ? 'way' : 'fact'),
4447
+ system: buildOrganizeSystem(input.existing?.metadata.wiki_kind === 'way' || listed.knowledge[0]?.kind === 'procedure' ? 'way' : 'fact'),
4442
4448
  prompt: buildOrganizePrompt(organizeInput),
4443
4449
  }
4444
4450
  }
@@ -4637,7 +4643,7 @@ export class KnowledgeRuntime {
4637
4643
  const plan = planRelated(cards, relations, await this.projectedRelated(cards))
4638
4644
  const entries = plan.get(cardKey(card))
4639
4645
  if (entries === undefined || entries.length === 0) return {}
4640
- return { related: entries.map(entry => ({ title: entry.title, entityType: entry.entityType, reason: entry.reason })) }
4646
+ return { related: entries.map(entry => ({ title: entry.title, entityType: entry.entityType, entityId: entry.entityId, reason: entry.reason })) }
4641
4647
  } catch {
4642
4648
  // 相关投影是**增强**:算不出来时不给清单,于是正文里一个 `[[…]]` 都不许出现 —— 而不是把卡片卡住。
4643
4649
  return {}
@@ -4868,7 +4874,9 @@ export class KnowledgeRuntime {
4868
4874
  const createdAt = typeof existing?.frontmatter.created_at === 'string' && existing.frontmatter.created_at ? existing.frontmatter.created_at : updatedAt
4869
4875
  const provenance = [...(input.provenance ?? []), ...(input.lineage ? [{ ...input.lineage, role: 'created' as const, refType: 'knowledge_card', refId: input.entityId }] : [])]
4870
4876
  // 旧版本曾把 schema_issues 写入 metadata。重写卡片时主动摘掉它,避免旧的待补结果继续出现。
4871
- const { schema_issues: _staleIssues, ...metadata } = input.metadata ?? {}
4877
+ const { schema_issues: _staleIssues, ...metadata } = input.metadata ?? previous?.metadata ?? {}
4878
+ if (status === 'deprecated' && (input.origin === 'review' || input.origin === 'manual' || (input.origin === undefined && previous?.status !== 'deprecated'))) metadata.wiki_deprecation_origin = 'manual'
4879
+ if (status !== 'deprecated') delete metadata.wiki_deprecation_origin
4872
4880
  void _staleIssues
4873
4881
  // 备注与 review **默认继承库里已有的**:不传不等于清空(见入参注释)。
4874
4882
  const annotations = input.annotations ?? previousAnnotations ?? []
@@ -4949,7 +4957,89 @@ export class KnowledgeRuntime {
4949
4957
  return success({ entityId: input.entityId, ...(input.lineage ? { lineage: input.lineage } : {}) })
4950
4958
  }
4951
4959
 
4952
- /** Resolve one card by id, optionally constrained to a type (ids are unique per type). */
4960
+ /** Shared by direct reads, index publication and recall; always reads live contributions. */
4961
+ private async wikiUnavailability(cards: readonly KnowledgeCard[], suppliedPlacements?: Map<string, CardKnowledgeRow[]>): Promise<Map<string, string>> {
4962
+ const truth = await this.storeHandle()
4963
+ const knowledge = await this.knowledgeStore()
4964
+ if (!truth || !knowledge) return new Map(cards.map(card => [cardKey(card), '知识真源不可用,无法核验 Wiki。']))
4965
+ const placements = suppliedPlacements ?? truth.placementsOf(cards.map(card => card.entityId))
4966
+ const ids = [...new Set(cards.flatMap(card => [...(placements.get(card.entityId) ?? []).map(row => row.knowledgeId), ...Object.keys(card.metadata.knowledge_versions as object ?? {})]))]
4967
+ const rows = new Map<string, Knowledge>()
4968
+ for (let offset = 0; offset < ids.length; offset += 500) {
4969
+ const page = await knowledge.list({ ids: ids.slice(offset, offset + 500), includeMerged: true, limit: 500 })
4970
+ for (const row of page.knowledge) rows.set(row.id, row)
4971
+ }
4972
+ const sources = new Map((truth.raw('SELECT entity_id,head_version FROM source').all() as Array<{ entity_id: string; head_version: number }>).map(row => [row.entity_id, row.head_version]))
4973
+ const cases = new Map<string, number | undefined>()
4974
+ const result = new Map<string, string>()
4975
+ for (const card of cards) {
4976
+ const reason = wikiUnavailableReason(card, placements.get(card.entityId) ?? [], { knowledge: rows, sources, caseVersion: id => {
4977
+ if (!cases.has(id)) cases.set(id, truth.event(id)?.version)
4978
+ return cases.get(id)
4979
+ } })
4980
+ if (reason) result.set(cardKey(card), reason)
4981
+ }
4982
+ return result
4983
+ }
4984
+
4985
+ /** Read a stable Wiki link or browse an Object's coordinates without flattening its files. */
4986
+ async readWiki(input: { target?: string; objectId?: string; object?: string; actionId?: string; sceneId?: string; wikiKind?: WikiKind; includeContent?: boolean; limit?: number; offset?: number } = {}): Promise<Envelope<Record<string, unknown>>> {
4987
+ const empty = { wikis: [], total: 0, offset: input.offset ?? 0, hasMore: false, outgoing: [], backlinks: [] }
4988
+ const limit = input.limit ?? 20, offset = input.offset ?? 0
4989
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100 || !Number.isInteger(offset) || offset < 0) return failure(empty, 'WIKI_PAGE_INVALID', 'limit 必须为 1–100 的整数,offset 必须为非负整数。', false)
4990
+ if (input.wikiKind !== undefined && input.wikiKind !== 'fact' && input.wikiKind !== 'way') return failure(empty, 'WIKI_KIND_INVALID', 'wiki_kind 只能为 fact 或 way。', false)
4991
+ if (!input.target?.trim() && !input.objectId?.trim() && !input.object?.trim()) return failure(empty, 'WIKI_SCOPE_REQUIRED', '请提供双链 target、object_id 或明确的 Object 名称。', false)
4992
+ const cards = await this.loadCards()
4993
+ let objectId = input.objectId
4994
+ if (input.object !== undefined) {
4995
+ const name = input.object.trim().toLocaleLowerCase()
4996
+ const objects = (await this.listAxisEntities('object') ?? []).filter(object => object.status !== 'deprecated' && object.status !== 'merged'
4997
+ && [object.id, object.name, ...object.aliases].some(value => value.toLocaleLowerCase() === name))
4998
+ if (objects.length !== 1) return failure({ ...empty, candidates: objects.map(object => ({ id: object.id, name: object.name })) }, objects.length ? 'OBJECT_AMBIGUOUS' : 'OBJECT_NOT_FOUND', 'Object 名称未唯一解析,请使用明确的 object_id。', false)
4999
+ if (objectId !== undefined && objectId !== objects[0]!.id) return failure(empty, 'OBJECT_SCOPE_CONFLICT', 'object 与 object_id 指向不同对象。', false)
5000
+ objectId = objects[0]!.id
5001
+ }
5002
+ const inScope = (card: KnowledgeCard): boolean => (objectId === undefined || card.coordinate?.objectId === objectId)
5003
+ && (input.actionId === undefined || card.coordinate?.actionId === input.actionId)
5004
+ && (input.sceneId === undefined || card.coordinate?.sceneId === input.sceneId)
5005
+ && (input.wikiKind === undefined || card.metadata.wiki_kind === input.wikiKind)
5006
+ let selected = cards.filter(inScope)
5007
+ if (input.target?.trim()) {
5008
+ selected = wikiLinkCandidates(selected, input.target)
5009
+ if (selected.length !== 1) return failure({ ...empty, candidates: selected.map(card => ({ ...wikiReference(card), coordinate: card.coordinate, wikiKind: card.metadata.wiki_kind })) }, selected.length ? 'WIKI_LINK_AMBIGUOUS' : 'WIKI_NOT_FOUND', '双链未唯一指向 Wiki,请从候选中使用稳定 target。', false)
5010
+ if (selected[0]!.status === 'deprecated') return failure(empty, 'WIKI_UNAVAILABLE', '目标 Wiki 已停用,请按 Object 坐标查找当前文件。', false)
5011
+ }
5012
+ selected = selected.filter(card => card.status !== 'deprecated').sort((a, b) => wikiReference(a).target.localeCompare(wikiReference(b).target))
5013
+ const page = selected.slice(offset, offset + limit)
5014
+ const includeContent = input.includeContent ?? Boolean(input.target)
5015
+ const truth = await this.storeHandle()
5016
+ const placements = truth?.placementsOf(page.map(card => card.entityId)) ?? new Map<string, CardKnowledgeRow[]>()
5017
+ const unavailableByKey = await this.wikiUnavailability(page, placements)
5018
+ const wikis: Array<Record<string, unknown>> = []
5019
+ for (const card of page) {
5020
+ const unavailable = unavailableByKey.get(cardKey(card))
5021
+ wikis.push({ ...wikiReference(card), coordinate: card.coordinate, wikiKind: card.metadata.wiki_kind, status: card.status, available: !unavailable,
5022
+ ...(unavailable ? { reason: unavailable } : {}), ...(card.coordinate ? { coordinateLabels: await this.coordinateNames(card.coordinate) } : {}),
5023
+ ...(includeContent && !unavailable ? { wiki: card } : {}),
5024
+ })
5025
+ }
5026
+ const first = input.target ? page[0] : undefined
5027
+ const live = cards.filter(card => card.status !== 'deprecated')
5028
+ const outgoing = first ? [...new Map(scanWikiLinks(first.content).flatMap(link => {
5029
+ const targets = wikiLinkCandidates(live, link.target)
5030
+ return targets.length === 1 && targets[0]!.entityId !== first.entityId ? [[targets[0]!.entityId, wikiReference(targets[0]!)] as const] : []
5031
+ })).values()] : []
5032
+ const backlinks = first ? live.filter(card => card.entityId !== first.entityId && scanWikiLinks(card.content).some(link => {
5033
+ const targets = wikiLinkCandidates(live, link.target)
5034
+ return targets.length === 1 && targets[0]!.entityId === first.entityId
5035
+ })).map(wikiReference) : []
5036
+ const data = { wikis, total: selected.length, offset, hasMore: offset + page.length < selected.length,
5037
+ nextOffset: offset + page.length < selected.length ? offset + page.length : null, outgoing, backlinks }
5038
+ await this.audit('wiki.read', { target: input.target, objectId, actionId: input.actionId, sceneId: input.sceneId, wikiKind: input.wikiKind, includeContent, returned: page.length })
5039
+ return success(data)
5040
+ }
5041
+
5042
+ /** Resolve one card by its stable ID, optionally constrained to a type. */
4953
5043
  async readCard(entityId: string, entityType?: SchemaType): Promise<KnowledgeCard | undefined> {
4954
5044
  const db = await this.storeHandle()
4955
5045
  if (db === null) return undefined
@@ -5040,7 +5130,7 @@ export class KnowledgeRuntime {
5040
5130
  if (card.status === 'problem' && status !== 'unreviewed') return failure({ entityId, status }, 'STATUS_TRANSITION_INVALID', 'problem 不能直接恢复为其他状态,请先改为 unreviewed', false)
5041
5131
  const eventLineage = withRun(lineage, 'knowledge-status')
5042
5132
  const provenance = [...(card.provenance ?? []), ...(eventLineage ? [{ ...eventLineage, role: 'executed' as const, refType: 'knowledge_status', refId: entityId }] : [])]
5043
- const written = await this.writeCard({ ...card, status, sourceRefs: card.sourceRefs, provenance, metadata: { ...card.metadata, ...(problemNote?.trim() ? { problem_note: problemNote.trim() } : {}) } })
5133
+ const written = await this.writeCard({ ...card, status, sourceRefs: card.sourceRefs, provenance, origin: 'review', metadata: { ...card.metadata, ...(problemNote?.trim() ? { problem_note: problemNote.trim() } : {}) } })
5044
5134
  if (written.ok === false) return failure({ entityId, status }, written.error.code, written.error.message, written.error.retryable, written.error.suggestion)
5045
5135
  return success({ entityId, status, ...(eventLineage ? { lineage: eventLineage } : {}) })
5046
5136
  }
@@ -5087,7 +5177,7 @@ export class KnowledgeRuntime {
5087
5177
  * `metadata.related_cards`;这里继续剥离旧版本正文里的标记,保证存量卡片迁移期间
5088
5178
  * 也不会把导航送进召回。
5089
5179
  */
5090
- const recallBody = stripEventReferenceSection(stripRelatedSection(card.content))
5180
+ const recallBody = wikiIndexContent(stripEventReferenceSection(stripRelatedSection(card.content)))
5091
5181
  for (const [index, chunk] of splitWikiNotes({ ...card, content: recallBody }).entries()) {
5092
5182
  const baseId = `${card.entityType}:${card.entityId}`
5093
5183
  const stableSection = layoutOf(card.metadata)?.sections?.find(section => section.title === chunk.heading)
@@ -5649,7 +5739,7 @@ export class KnowledgeRuntime {
5649
5739
  existing: card,
5650
5740
  ...(input.imagePlan.byCard.get(key) === undefined ? {} : { images: input.imagePlan.byCard.get(key) }),
5651
5741
  ...(relatedPlan.get(key) === undefined ? {} : {
5652
- related: (relatedPlan.get(key) ?? []).map(entry => ({ title: entry.title, entityType: entry.entityType, reason: entry.reason })),
5742
+ related: (relatedPlan.get(key) ?? []).map(entry => ({ title: entry.title, entityType: entry.entityType, entityId: entry.entityId, reason: entry.reason })),
5653
5743
  }),
5654
5744
  call: input.call,
5655
5745
  })
@@ -6211,12 +6301,11 @@ export class KnowledgeRuntime {
6211
6301
  // ------------------------------------------- Finalize · 索引 / 断链 / 相关(0049)
6212
6302
 
6213
6303
  /**
6214
- * 收尾段的**结构化数据层**:把相关卡片写进 `metadata.related_cards`,清理旧版本写进
6215
- * 正文的相关块,并把断链统计交给编译报告与索引页。
6304
+ * 编译收尾:生成类型化 Wiki 的稳定双链与导航,更新 `metadata.related_cards`,
6305
+ * 清理旧相关块,并把断链统计交给编译报告与索引页。
6216
6306
  *
6217
- * 相关卡片是派生导航数据,不是知识正文。它放在 metadata 里有三个好处:正文不会被
6218
- * 机器段落污染,召回不需要再剥离导航块,面板也能直接按结构展示。旧卡片上的
6219
- * `<!-- compiled:related -->` 只在这里做一次兼容清理。
6307
+ * 导航只放链接,不复制目标正文;面板使用结构化元数据,检索排除生成的导航块。
6308
+ * 旧卡片上的 `<!-- compiled:related -->` 在这里兼容清理。
6220
6309
  *
6221
6310
  * 三段都是确定性的,所以这个函数可以重复跑:正文和结构化相关数据都没变化时不写盘。
6222
6311
  */
@@ -6230,11 +6319,24 @@ export class KnowledgeRuntime {
6230
6319
  * 它是**算出来的**,不是谁声明的:这张卡里的知识引用了哪个轴实体,而那个轴自己
6231
6320
  * 有一张卡,就有了链接。模型抽出来的关系(`relations`)是第二层。
6232
6321
  */
6233
- const projected = await this.projectedRelated(cards)
6234
- const related = planRelated(cards, relations, projected)
6235
- /** 统计清理旧导航块之后的正文断链;相关卡片本身不再伪装成正文链接。 */
6236
- const nextCards = cards.map(card => ({ ...card, content: stripEventReferenceSection(stripRelatedSection(card.content)) }))
6237
- const deadLinks = deadLinksOf(nextCards)
6322
+ const axes = (await this.listAxisEntities() ?? []).filter(axis => axis.status !== 'deprecated' && axis.status !== 'merged')
6323
+ const objects = axes.filter(axis => axis.kind === 'object')
6324
+ const names = new Map(axes.map(axis => [axis.id, axis.name]))
6325
+ const labeled = cards.map(card => card.metadata.wiki_kind && card.coordinate ? { ...card, metadata: { ...card.metadata,
6326
+ wiki_coordinate_labels: { object: names.get(card.coordinate.objectId) ?? card.coordinate.objectId, action: names.get(card.coordinate.actionId) ?? card.coordinate.actionId, scene: names.get(card.coordinate.sceneId) ?? card.coordinate.sceneId },
6327
+ } } : card)
6328
+ const live = labeled.filter(card => card.status !== 'deprecated')
6329
+ const nextCards = labeled.map(card => {
6330
+ const base = { ...card, content: stripEventReferenceSection(stripRelatedSection(card.content)) }
6331
+ if (!card.metadata.wiki_kind || card.status === 'deprecated') return base
6332
+ const navigation = compileWikiNavigation(base, live, objects)
6333
+ return { ...base, content: navigation.content, metadata: { ...base.metadata,
6334
+ wiki_links: navigation.outgoing, wiki_inline_targets: navigation.inlineTargets, wiki_object_refs: navigation.objectIds,
6335
+ } }
6336
+ })
6337
+ const projected = await this.projectedRelated(nextCards)
6338
+ const related = planRelated(live, relations, projected)
6339
+ const deadLinks = deadLinksOf(nextCards.filter(card => card.status !== 'deprecated'))
6238
6340
  let rewritten = 0
6239
6341
  for (const [index, card] of cards.entries()) {
6240
6342
  const key = cardKey(card)
@@ -6242,7 +6344,7 @@ export class KnowledgeRuntime {
6242
6344
  const entries = related.get(key) ?? []
6243
6345
  const previousEntries = relatedEntriesOf(card.metadata)
6244
6346
  const contentChanged = nextCard.content !== card.content
6245
- const relatedChanged = JSON.stringify(previousEntries) !== JSON.stringify(entries)
6347
+ const relatedChanged = JSON.stringify(previousEntries) !== JSON.stringify(entries) || JSON.stringify(card.metadata.wiki_coordinate_labels) !== JSON.stringify(nextCard.metadata.wiki_coordinate_labels) || JSON.stringify(card.metadata.wiki_links) !== JSON.stringify(nextCard.metadata.wiki_links) || JSON.stringify(card.metadata.wiki_inline_targets) !== JSON.stringify(nextCard.metadata.wiki_inline_targets) || JSON.stringify(card.metadata.wiki_object_refs) !== JSON.stringify(nextCard.metadata.wiki_object_refs)
6246
6348
  if (!contentChanged && !relatedChanged) continue
6247
6349
  /**
6248
6350
  * 没有来源的卡片写不进去(`writeCard` 要求 `source_refs` 非空),而这是**唯一**
@@ -6263,7 +6365,7 @@ export class KnowledgeRuntime {
6263
6365
  status: card.status,
6264
6366
  ...(card.owner ? { owner: card.owner } : {}),
6265
6367
  ...(card.provenance ? { provenance: card.provenance } : {}),
6266
- metadata: { ...card.metadata, [RELATED_METADATA_KEY]: entries },
6368
+ metadata: { ...nextCard.metadata, [RELATED_METADATA_KEY]: entries },
6267
6369
  origin: 'compile',
6268
6370
  })
6269
6371
  if (written.ok === false) {
@@ -6387,13 +6489,9 @@ export class KnowledgeRuntime {
6387
6489
  }
6388
6490
  }
6389
6491
 
6390
- /** 一条上下文条目「值多少字符」:命中条目算它那些 note 的正文,扩展条目算标题 + 摘要(+ 正文)。 */
6391
- private contextCost(item: ContextItem, full: boolean): number {
6392
- const card = item.wiki
6393
- if (card === null) return 0
6394
- const shallow = card.title.length + (card.summary?.length ?? 0)
6395
- const knowledge = (item.knowledge ?? []).reduce((sum, row) => sum + row.text.length + row.state.length + 8, 0)
6396
- return knowledge + shallow + card.content.length
6492
+ /** 与 Agent 实际收到的上下文条目使用同一序列化形状,包含完整 Wiki、依据和引用。 */
6493
+ private contextCost(item: ContextItem): number {
6494
+ return JSON.stringify(agentContextItem(item)).length
6397
6495
  }
6398
6496
 
6399
6497
  /** 溢出目录:**系统临时目录**,不随知识库走(0050 的取舍,见 issue)。 */
@@ -6421,17 +6519,14 @@ export class KnowledgeRuntime {
6421
6519
  const { items, maxChars, recallId } = input
6422
6520
  const hitCards = items.filter(item => item.reason === 'hit').length
6423
6521
  const expandedCards = items.length - hitCards
6424
- const totalChars = items.reduce((sum, item) => sum + this.contextCost(item, true), 0)
6522
+ const totalChars = items.reduce((sum, item) => sum + this.contextCost(item), 0)
6425
6523
  if (!Number.isFinite(maxChars) || maxChars <= 0) {
6426
6524
  return { items, report: { maxChars: 0, usedChars: totalChars, omittedCards: 0, omittedChars: 0, hitCards, expandedCards } }
6427
6525
  }
6428
6526
  const keptItems: ContextItem[] = []
6429
6527
  let used = 0
6430
6528
  for (const item of items) {
6431
- const full = this.contextCost(item, true)
6432
- const shallow = this.contextCost(item, false)
6433
- // 命中条目按整条算(它的正文就是证据);扩展条目可以先按「浅」那一档进。
6434
- const cost = item.reason === 'hit' ? full : shallow
6529
+ const cost = this.contextCost(item)
6435
6530
  if (used + cost > maxChars) continue
6436
6531
  used += cost
6437
6532
  keptItems.push(item)
@@ -6442,7 +6537,7 @@ export class KnowledgeRuntime {
6442
6537
  maxChars,
6443
6538
  usedChars: used,
6444
6539
  omittedCards: omitted.length,
6445
- omittedChars: omitted.reduce((sum, item) => sum + this.contextCost(item, true), 0),
6540
+ omittedChars: omitted.reduce((sum, item) => sum + this.contextCost(item), 0),
6446
6541
  hitCards: keptItems.filter(item => item.reason === 'hit').length,
6447
6542
  expandedCards: keptItems.filter(item => item.reason === 'expansion').length,
6448
6543
  }
@@ -6554,13 +6649,17 @@ export class KnowledgeRuntime {
6554
6649
  * 而不是报错:这一层是**增强**,它坏了不该让编译失败。
6555
6650
  */
6556
6651
  private async projectedRelated(cards: KnowledgeCard[]): Promise<Array<{ fromKey: string; toKey: string }>> {
6652
+ const links = cards.flatMap(card => scanWikiLinks(card.content).flatMap(link => {
6653
+ const candidates = wikiLinkCandidates(cards.filter(target => target.status !== 'deprecated'), link.target)
6654
+ return candidates.length === 1 && candidates[0]!.entityId !== card.entityId ? [{ fromKey: cardKey(card), toKey: cardKey(candidates[0]!) }] : []
6655
+ }))
6557
6656
  const knowledge = await this.knowledgeStore()
6558
- if (knowledge === null) return []
6657
+ if (knowledge === null) return links
6559
6658
  const db = await this.storeHandle()
6560
- if (db === null) return []
6659
+ if (db === null) return links
6561
6660
  let placements: Map<string, CardKnowledgeRow[]>
6562
- try { placements = db.placements() } catch { return [] }
6563
- if (placements.size === 0) return []
6661
+ try { placements = db.placements() } catch { return links }
6662
+ if (placements.size === 0) return links
6564
6663
  /**
6565
6664
  * 单轴卡:坐标里恰好只有一个分量非空。
6566
6665
  *
@@ -6585,11 +6684,11 @@ export class KnowledgeRuntime {
6585
6684
  bucket.push(key)
6586
6685
  byAxis.set(axis, bucket)
6587
6686
  }
6588
- if (byAxis.size === 0) return []
6687
+ if (byAxis.size === 0) return links
6589
6688
  const ids = [...new Set([...placements.values()].flat().map(row => row.knowledgeId))]
6590
- if (ids.length === 0) return []
6689
+ if (ids.length === 0) return links
6591
6690
  let axes: Map<string, { objectId: string; actionId: string; sceneId: string }>
6592
- try { axes = await knowledge.axesOf(ids) } catch { return [] }
6691
+ try { axes = await knowledge.axesOf(ids) } catch { return links }
6593
6692
  const pairs = new Map<string, { fromKey: string; toKey: string }>()
6594
6693
  for (const [cardId, rows] of placements) {
6595
6694
  const fromKeys = keyById.get(cardId) ?? []
@@ -6609,7 +6708,7 @@ export class KnowledgeRuntime {
6609
6708
  }
6610
6709
  }
6611
6710
  }
6612
- return [...pairs.values()]
6711
+ return [...pairs.values(), ...links]
6613
6712
  }
6614
6713
 
6615
6714
  /**
@@ -8127,7 +8226,7 @@ export class KnowledgeRuntime {
8127
8226
  : card.metadata.wiki_kind
8128
8227
  ? events.filter(event => {
8129
8228
  const c = event.coordinateIds
8130
- return c.objectId === card.coordinate!.objectId && (card.metadata.wiki_kind === 'fact' || c.actionId === card.coordinate!.actionId)
8229
+ return coordinateKeyOf(c) === coordinateKeyOf(card.coordinate!)
8131
8230
  }).map(event => event.id).sort()
8132
8231
  : (eventsByCoordinate.get(coordinateKeyOf(card.coordinate)) ?? []).sort()
8133
8232
  const oldRaw = card.metadata.event_refs
@@ -8372,9 +8471,15 @@ export class KnowledgeRuntime {
8372
8471
  derive: (sourceIds, context, force, output, checkpoint) => this.deriveProcedures(sourceIds, context, force, output, checkpoint),
8373
8472
  groups: async context => {
8374
8473
  const snapshot = await this.materializationSnapshot(store, context)
8375
- return { groups: snapshot.groups, blocked: snapshot.blocked, blockedAmbiguous: snapshot.blockedAmbiguous, blockedAwaitingNormalize: snapshot.blockedAwaitingNormalize, snapshot }
8474
+ return { groups: snapshot.groups, blocked: snapshot.blocked, blockedAmbiguous: snapshot.blockedAmbiguous, blockedAwaitingNormalize: snapshot.blockedAwaitingNormalize, blockedItems: snapshot.blockedItems, snapshot }
8376
8475
  },
8377
8476
  materialize: (group, context, output, snapshot) => this.materializePipelineGroup(store, group, context, output, snapshot as MaterializationSnapshot),
8477
+ link: async () => {
8478
+ const warnings: string[] = []
8479
+ const finalized = await this.runFinalizeStage(await this.loadCards(), Object.values(await this.readExtractedRelations()).flat(), warnings)
8480
+ if (warnings.length) throw new Error(warnings.join(';'))
8481
+ return `已更新 Wiki 双链与导航:${finalized.rewritten} 份`
8482
+ },
8378
8483
  build: async (taskId, checkpoint) => {
8379
8484
  let result: Envelope<RebuildReport>
8380
8485
  try { result = await this.compileOnce(taskId, 'all', undefined, { extract: false, checkpoint }) }
@@ -8568,7 +8673,7 @@ export class KnowledgeRuntime {
8568
8673
  const cards = await this.loadCards()
8569
8674
  const relations = Object.values(await this.readExtractedRelations()).flat()
8570
8675
  for (const [key, entries] of planRelated(cards, relations, await this.projectedRelated(cards))) {
8571
- if (entries.length > 0) plan.set(key, entries.map(entry => ({ title: entry.title, entityType: entry.entityType, reason: entry.reason })))
8676
+ if (entries.length > 0) plan.set(key, entries.map(entry => ({ title: entry.title, entityType: entry.entityType, entityId: entry.entityId, reason: entry.reason })))
8572
8677
  }
8573
8678
  } catch { /* 增强项:算不出来就不给清单 */ }
8574
8679
  return plan
@@ -8591,17 +8696,18 @@ export class KnowledgeRuntime {
8591
8696
  const evidence = [...item.sources, ...item.timeline.flatMap(step => step.sources)]
8592
8697
  const sourceVersions = new Map(evidence.map(e => [e.documentId, db.source(e.documentId)?.version]))
8593
8698
  const fingerprint = contentHashOf(JSON.stringify({ contract: DERIVE_CONTRACT_VERSION, item: { ...item, updatedAt: undefined }, sourceVersions: [...sourceVersions], model: { provider: context.model.provider, model: context.model.model, reasoningEffort: context.model.reasoningEffort } }))
8594
- const receipt = db.raw('SELECT fingerprint, knowledge_ids FROM pipeline_derivations WHERE case_id=?').get(item.id) as { fingerprint: string; knowledge_ids: string } | undefined
8595
- if (!force && receipt?.fingerprint === fingerprint) { done++; reused++; continue }
8699
+ const receipt = db.raw('SELECT fingerprint, response, knowledge_ids FROM pipeline_derivations WHERE case_id=?').get(item.id) as { fingerprint: string; response: string; knowledge_ids: string } | undefined
8700
+ // Replay validated cached output through ingest too: normalization may have withdrawn its support.
8701
+ const cachedResponse = !force && receipt?.fingerprint === fingerprint ? receipt.response : undefined
8596
8702
  const auditId = randomUUID()
8597
8703
  const request = { system: buildDeriveSystem(), prompt: JSON.stringify({ case: { ...item, version: caseVersion }, sourceEvidence: evidence, sourceVersions: [...sourceVersions] }) }
8598
- let response = '{"procedures":[],"skipped_cases":[]}'
8704
+ let response = cachedResponse ?? '{"procedures":[],"skipped_cases":[]}'
8599
8705
  db.raw('INSERT INTO pipeline_derivation_runs VALUES (?,?,?,?,?,?,?,?,?)').run(auditId, item.id, caseVersion, fingerprint, JSON.stringify(request), '', 'running', null, new Date().toISOString())
8600
8706
  try {
8601
- if (item.timeline.some(step => step.type === 'check' || step.type === 'action')) {
8707
+ if (cachedResponse === undefined && item.timeline.some(step => step.type === 'check' || step.type === 'action')) {
8602
8708
  if (!context.model.call) throw new Error(`提炼模型不可用:${context.model.why ?? '请选择模型'}`)
8603
8709
  response = await context.model.call({ ...request, sessionId: `knowledge:${this.baseId || 'default'}:derive`, onDelta: output })
8604
- } else output(`Case ${item.id} 没有已执行过程,不反推 Procedure。\n`)
8710
+ } else if (cachedResponse === undefined) output(`Case ${item.id} 没有已执行过程,不反推 Procedure。\n`)
8605
8711
  const parsed = JSON.parse(response.trim().replace(/^```(?:json)?\s*/, '').replace(/\s*```$/, '')) as { procedures?: Array<Record<string, unknown>> }
8606
8712
  if (!Array.isArray(parsed.procedures)) throw new Error('Case 提炼返回缺少 procedures 数组')
8607
8713
  if (this.closed || db.event(item.id)?.version !== caseVersion || [...sourceVersions].some(([id, version]) => db.source(id)?.version !== version)) throw new Error('Case 或来源在提炼期间变化,请重试')
@@ -8636,6 +8742,7 @@ export class KnowledgeRuntime {
8636
8742
  db.raw("UPDATE pipeline_derivation_runs SET response=?,state='succeeded' WHERE id=?").run(response, auditId)
8637
8743
  output(`Case ${item.id}:保存 ${ids.length} 条候选 Procedure。\n`)
8638
8744
  done++
8745
+ if (cachedResponse !== undefined) reused++
8639
8746
  } catch (cause) {
8640
8747
  db.raw("UPDATE pipeline_derivation_runs SET response=?,state='failed',error=? WHERE id=?").run(response, cause instanceof Error ? cause.message : String(cause), auditId)
8641
8748
  throw cause
@@ -8651,7 +8758,7 @@ export class KnowledgeRuntime {
8651
8758
  * 两处必须用同一个表达式,否则「什么算变化」就有两套说法。
8652
8759
  */
8653
8760
  private pipelineGroupFingerprint(knowledge: Knowledge[], context: PipelineContext): string {
8654
- return contentHashOf(JSON.stringify({ knowledge: knowledge.map(row => ({ id: row.id, kind: row.kind ?? 'fact', data: row.data, 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 }))
8761
+ return contentHashOf(JSON.stringify({ contract: 'coordinate-wiki-v2', organizeVersion: context.organize ? ORGANIZE_VERSION : undefined, knowledge: knowledge.map(row => ({ id: row.id, kind: row.kind ?? 'fact', data: row.data, 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 }))
8655
8762
  }
8656
8763
 
8657
8764
  /**
@@ -8688,12 +8795,12 @@ export class KnowledgeRuntime {
8688
8795
  ].filter(Boolean).map(text => `- ${text}[^${row.id}]`).join('\n')
8689
8796
  }
8690
8797
 
8691
- private async freshGroupFingerprint(coordinate: CardCoordinate, context: PipelineContext, db: KnowledgeDb): Promise<string> {
8798
+ private async freshGroupFingerprint(coordinate: CardCoordinate, wikiKind: WikiKind, context: PipelineContext, db: KnowledgeDb): Promise<string> {
8692
8799
  const store = await this.knowledgeStore()
8693
8800
  if (!store) return ''
8694
8801
  const raw: Knowledge[] = []
8695
8802
  for (let offset = 0;; offset += 500) {
8696
- const page = await store.list({ objectId: coordinate.objectId, kind: coordinate.actionId ? 'procedure' : 'fact', ...(coordinate.actionId ? { actionId: coordinate.actionId } : {}), excludeWithdrawn: true, limit: 500, offset })
8803
+ const page = await store.list({ objectId: coordinate.objectId, kind: wikiKind === 'way' ? 'procedure' : 'fact', actionId: coordinate.actionId, sceneId: coordinate.sceneId, excludeWithdrawn: true, limit: 500, offset })
8697
8804
  raw.push(...page.knowledge)
8698
8805
  if (offset + page.knowledge.length >= page.total) break
8699
8806
  }
@@ -8705,7 +8812,7 @@ export class KnowledgeRuntime {
8705
8812
  if (source) bodies.set(id, { version: source.version, content: source.record.content })
8706
8813
  }
8707
8814
  const rows = raw
8708
- .filter(row => row.status !== 'deprecated' && (row.kind !== 'procedure' || (row.data?.procedure?.completeness === 'complete' && row.data.procedure.method.length > 0 && row.status !== 'disputed' && row.data.procedure.caseRefs.every(ref => db.event(ref.id)?.version === ref.version) && !row.sources.some(source => source.toVersion !== undefined && db.source(source.documentId)?.version !== source.toVersion))))
8815
+ .filter(row => row.status !== 'deprecated' && (row.kind !== 'procedure' || (row.data?.procedure?.completeness === 'complete' && row.data.procedure.method.length > 0 && row.status !== 'disputed' && row.data.procedure.caseRefs.every(ref => db.event(ref.id)?.version === ref.version) && !row.sources.some(source => !source.withdrawn && source.toVersion !== undefined && db.source(source.documentId)?.version !== source.toVersion))))
8709
8816
  .map(row => assessKnowledgeEvidence(row, db, bodies))
8710
8817
  .map(row => ({ ...row, sources: row.sources.filter(source => !source.withdrawn && source.evidenceStatus !== 'missing') }))
8711
8818
  .filter(row => row.sources.length > 0)
@@ -8726,12 +8833,13 @@ export class KnowledgeRuntime {
8726
8833
  */
8727
8834
  const plans = options.plans !== false
8728
8835
  const unresolved = store.unresolvedKnowledge()
8729
- const grouped = new Map<string, { coordinate: CardCoordinate; knowledge: Knowledge[] }>()
8836
+ const grouped = new Map<string, { coordinate: CardCoordinate; wikiKind: WikiKind; knowledge: Knowledge[] }>()
8730
8837
  let blockedAmbiguous = 0
8731
8838
  let blockedAwaitingNormalize = 0
8732
- const groupFor = (coordinate: CardCoordinate) => {
8733
- const key = coordinateKeyOf(coordinate)
8734
- if (!grouped.has(key)) grouped.set(key, { coordinate, knowledge: [] })
8839
+ const blockedItems: MaterializationSnapshot['blockedItems'] = []
8840
+ const groupFor = (coordinate: CardCoordinate, wikiKind: WikiKind) => {
8841
+ const key = wikiGroupKey(wikiKind, coordinate)
8842
+ if (!grouped.has(key)) grouped.set(key, { coordinate, wikiKind, knowledge: [] })
8735
8843
  return grouped.get(key)!
8736
8844
  }
8737
8845
  const caseStore = await this.storeHandle()
@@ -8739,24 +8847,29 @@ export class KnowledgeRuntime {
8739
8847
  if (row.kind === 'case') continue
8740
8848
  const coordinate = wikiProjection(row.kind ?? 'fact', row)
8741
8849
  const method = row.data?.procedure
8742
- if (!coordinate || (row.kind === 'procedure' && (!method || method.completeness !== 'complete' || !method.method.length || row.status === 'disputed' || method.caseRefs.some(ref => caseStore?.event(ref.id)?.version !== ref.version) || row.sources.some(source => source.toVersion !== undefined && caseStore?.source(source.documentId)?.version !== source.toVersion)))) { blockedAmbiguous += 1; continue }
8743
- groupFor(coordinate).knowledge.push(row)
8850
+ if (!coordinate || (row.kind === 'procedure' && (!method || method.completeness !== 'complete' || !method.method.length || row.status === 'disputed' || method.caseRefs.some(ref => caseStore?.event(ref.id)?.version !== ref.version) || row.sources.some(source => source.toVersion !== undefined && caseStore?.source(source.documentId)?.version !== source.toVersion)))) {
8851
+ blockedAmbiguous += 1
8852
+ blockedItems.push({ knowledgeId: row.id, sourceIds: [...new Set(row.sources.map(source => source.documentId).filter(Boolean))], ...(coordinate ? { groupKey: wikiGroupKey(row.kind === 'procedure' ? 'way' : 'fact', coordinate) } : {}) })
8853
+ continue
8854
+ }
8855
+ groupFor(coordinate, row.kind === 'procedure' ? 'way' : 'fact').knowledge.push(row)
8744
8856
  }
8745
8857
  const db = await this.storeHandle()
8746
8858
  const events = new Map<string, EventEntity[]>()
8747
8859
  for (const event of db?.events() ?? []) {
8748
8860
  const coordinate = event.coordinateIds
8749
8861
  if (!coordinate.objectId) continue
8750
- for (const projection of [wikiProjection('fact', coordinate), wikiProjection('procedure', coordinate)]) {
8751
- if (!projection) continue
8752
- const key = coordinateKeyOf(projection)
8862
+ for (const kind of ['fact', 'way'] as const) {
8863
+ const key = wikiGroupKey(kind, coordinate)
8753
8864
  const bucket = events.get(key) ?? []
8754
8865
  bucket.push(event)
8755
8866
  events.set(key, bucket)
8756
8867
  }
8757
8868
  }
8758
8869
  const cards = await this.loadCards()
8759
- const byCoordinate = new Map(cards.filter(card => card.coordinate).map(card => [coordinateKeyOf(card.coordinate!), card]))
8870
+ const groupKeyOf = (card: KnowledgeCard): string => card.coordinate && (card.metadata.wiki_kind === 'fact' || card.metadata.wiki_kind === 'way')
8871
+ ? wikiGroupKey(card.metadata.wiki_kind, card.coordinate) : card.coordinate ? coordinateKeyOf(card.coordinate) : ''
8872
+ const byCoordinate = new Map(cards.filter(card => card.coordinate).map(card => [groupKeyOf(card), card]))
8760
8873
  const groups: MaterializationGroup[] = []
8761
8874
  const knowledge = new Map<string, Knowledge[]>()
8762
8875
  for (const [key, value] of grouped) {
@@ -8770,11 +8883,13 @@ export class KnowledgeRuntime {
8770
8883
  const needsNormalization = this.needsNormalizationForGroup(store, value.knowledge, unresolved, db)
8771
8884
  if (needsNormalization) blockedAwaitingNormalize += value.knowledge.length
8772
8885
  knowledge.set(key, value.knowledge)
8773
- groups.push({ key, wikiKind: value.coordinate.actionId ? 'way' : 'fact', title: existing?.title ?? `${names.object}${names.action ? ` · ${names.action}` : ''} · ${value.coordinate.actionId ? 'Wiki Way' : 'Wiki Fact'}`, coordinate: value.coordinate, knowledgeCount: value.knowledge.length, eventCount: events.get(key)?.length ?? 0, sourceIds: contributingSources, ...(existing ? { cardId: existing.entityId } : {}), state: needsNormalization ? 'blocked' : current ? 'current' : existing ? 'changed' : 'new', ...(needsNormalization ? { reason: '分组中的来源已变化、新抽取待归一或坐标有歧义,请先完成归一' } : {}), fingerprint })
8886
+ groups.push({ key, wikiKind: value.wikiKind, title: existing?.title ?? `${names.object}${names.action ? ` · ${names.action}` : ''}${names.scene ? ` · ${names.scene}` : ''} · ${value.wikiKind === 'way' ? 'Wiki Way' : 'Wiki Fact'}`, coordinate: value.coordinate, knowledgeCount: value.knowledge.length, eventCount: events.get(key)?.length ?? 0, sourceIds: contributingSources, ...(existing ? { cardId: existing.entityId } : {}), state: needsNormalization ? 'blocked' : current ? 'current' : existing ? 'changed' : 'new', ...(needsNormalization ? { reason: '分组中的来源已变化、新抽取待归一或坐标有歧义,请先完成归一' } : {}), fingerprint })
8774
8887
  }
8775
8888
  for (const card of cards) {
8776
- if (!card.coordinate || grouped.has(coordinateKeyOf(card.coordinate)) || !store.materialization(coordinateKeyOf(card.coordinate)) || card.status === 'deprecated') continue
8777
- groups.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' })
8889
+ const key = groupKeyOf(card)
8890
+ const legacyReceipt = card.coordinate ? store.materialization(coordinateKeyOf(card.coordinate)) : undefined
8891
+ if (!card.coordinate || grouped.has(key) || (!store.materialization(key) && !legacyReceipt && card.metadata.pipeline_materialization !== true) || card.status === 'deprecated') continue
8892
+ groups.push({ key, wikiKind: card.metadata.wiki_kind === 'way' ? 'way' : 'fact', 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' })
8778
8893
  }
8779
8894
  const sources = plans ? (await this.loadSources()).map(source => ({ sourceId: source.sourceId, content: source.content })) : []
8780
8895
  const imagePlan = plans
@@ -8805,7 +8920,7 @@ export class KnowledgeRuntime {
8805
8920
  return {
8806
8921
  groups: groups.sort((a, b) => a.title.localeCompare(b.title)),
8807
8922
  blocked: blockedAmbiguous + blockedAwaitingNormalize,
8808
- blockedAmbiguous, blockedAwaitingNormalize,
8923
+ blockedAmbiguous, blockedAwaitingNormalize, blockedItems,
8809
8924
  knowledge, events, sources, images: imagePlan.byCard, related: relatedByKey,
8810
8925
  }
8811
8926
  }
@@ -8836,7 +8951,7 @@ export class KnowledgeRuntime {
8836
8951
  const statusBeforeDeprecate = (await this.readCard(existing.entityId))?.status
8837
8952
  if (statusBeforeDeprecate === undefined) throw new Error('卡片在处理期间已被删除,本次物化未提交,请刷新后重试')
8838
8953
  if (statusBeforeDeprecate !== existing.status) throw new Error(`卡片在处理期间被人工改动(状态 ${existing.status} → ${statusBeforeDeprecate}),本次物化未提交,请重试`)
8839
- const written = await this.writeCard({ ...existing, status: 'deprecated', origin: 'compile' })
8954
+ const written = await this.writeCard({ ...existing, metadata: { ...existing.metadata, wiki_deprecation_origin: existing.status === 'deprecated' && existing.metadata.wiki_deprecation_origin !== 'withdrawal' ? 'manual' : 'withdrawal' }, status: 'deprecated', origin: 'compile' })
8840
8955
  if (!written.ok) throw new Error(written.error.message)
8841
8956
  store.materialized(group.key, group.fingerprint, existing.entityId)
8842
8957
  // 停用也是一次物化:卡片从可召回里退出,这一步发生过,就该留一条轨迹。
@@ -8855,7 +8970,7 @@ export class KnowledgeRuntime {
8855
8970
  let content = knowledge.map(row => `## ${row.data?.procedure?.question ?? row.text.split('\n')[0]!.slice(0, 60)}\n\n${this.organizeKnowledgeText(row)}[^${row.id}]${boundaries.get(row.id) ? `\n\n### 适用条件与边界\n\n${boundaries.get(row.id)}` : ''}`).join('\n\n')
8856
8971
  let summary = knowledge[0]?.text ?? ''
8857
8972
  let placements: CardKnowledgeRow[] = knowledge.map((row, index) => ({ knowledgeId: row.id, section: row.data?.procedure?.question ?? row.text.split('\n')[0]!.slice(0, 60), ordinal: index + 1, origin: 'compiler' }))
8858
- let metadata: Record<string, unknown> = { ...(existing?.metadata ?? {}), pipeline_materialization: true, wiki_kind: group.wikiKind ?? (group.coordinate.actionId ? 'way' : 'fact') }
8973
+ let metadata: Record<string, unknown> = { ...(existing?.metadata ?? {}), pipeline_materialization: true, wiki_kind: group.wikiKind ?? 'fact', wiki_coordinate_version: 2, knowledge_versions: Object.fromEntries(knowledge.map(row => [row.id, row.version])) }
8859
8974
  const entityId = existing?.entityId ?? `coord-${hash(group.key)}`
8860
8975
  const images = snapshot.images.get(group.key) ?? []
8861
8976
  const related = existing && snapshot.related.has(group.key) ? { related: snapshot.related.get(group.key)! } : {}
@@ -8880,7 +8995,7 @@ export class KnowledgeRuntime {
8880
8995
  * 「只有知识行变了」;这条按坐标重算整组指纹,能把那种变化也挡住。
8881
8996
  */
8882
8997
  if (this.needsNormalizationForGroup(store, knowledge, store.unresolvedKnowledge(), db)) throw new Error('卡片组织期间事实发生变化,结果未提交,请重试')
8883
- if (await this.freshGroupFingerprint(group.coordinate, context, db) !== group.fingerprint) throw new Error('卡片分组在处理期间事实发生了变化,结果未提交,请重试')
8998
+ if (await this.freshGroupFingerprint(group.coordinate, group.wikiKind ?? 'fact', context, db) !== group.fingerprint) throw new Error('卡片分组在处理期间事实发生了变化,结果未提交,请重试')
8884
8999
  if (this.closed) throw new Error('知识库已关闭,本次物化未提交')
8885
9000
  /**
8886
9001
  * 状态必须**在写之前**重读,并且和读到的旧值比对。
@@ -8896,7 +9011,8 @@ export class KnowledgeRuntime {
8896
9011
  if (statusAtWrite !== existing?.status) {
8897
9012
  throw new Error(`卡片在处理期间被人工改动(状态 ${existing?.status ?? '未知'} → ${statusAtWrite ?? '未知'}),本次物化未提交,请重试`)
8898
9013
  }
8899
- const written = await this.writeCard({ entityType, entityId, title, content, summary, sourceRefs: group.sourceIds, status: statusAtWrite === 'deprecated' ? 'unreviewed' : statusAtWrite ?? 'unreviewed', coordinate: group.coordinate, placements, metadata, origin: 'compile' })
9014
+ const nextStatus = statusAtWrite === 'deprecated' && existing?.metadata.wiki_deprecation_origin === 'withdrawal' ? 'unreviewed' : statusAtWrite ?? 'unreviewed'
9015
+ const written = await this.writeCard({ entityType, entityId, title, content, summary, sourceRefs: group.sourceIds, status: nextStatus, coordinate: group.coordinate, placements, metadata, origin: 'compile' })
8900
9016
  if (!written.ok) throw new Error(written.error.message)
8901
9017
  db.setCardEvents(entityId, eventIds)
8902
9018
  store.materialized(group.key, group.fingerprint, entityId)
@@ -8915,7 +9031,7 @@ export class KnowledgeRuntime {
8915
9031
  card: {
8916
9032
  id: entityId,
8917
9033
  title,
8918
- status: statusAtWrite === 'deprecated' ? 'unreviewed' : statusAtWrite ?? 'unreviewed',
9034
+ status: nextStatus,
8919
9035
  characters: content.length,
8920
9036
  events: eventIds.length,
8921
9037
  images: images.length,
@@ -8939,7 +9055,9 @@ export class KnowledgeRuntime {
8939
9055
  const db = await this.derivedHandle()
8940
9056
  const manifest = db?.artifact('build-manifest.json')
8941
9057
  const hashes = manifest ? (JSON.parse(manifest) as { cardHashes?: Record<string, string> }).cardHashes ?? {} : {}
8942
- const cards = await this.loadCards()
9058
+ const allCards = await this.loadCards()
9059
+ const unavailable = await this.wikiUnavailability(allCards)
9060
+ const cards = allCards.filter(card => !card.metadata.wiki_kind || !unavailable.has(cardKey(card)))
8943
9061
  const indexPending = cards.some(card => hashes[`${card.entityType}:${card.entityId}`] !== this.cardHashOf(card)) || Object.keys(hashes).length !== cards.length
8944
9062
  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 }
8945
9063
  }
@@ -9165,6 +9283,8 @@ export class KnowledgeRuntime {
9165
9283
  }
9166
9284
  this.publication.artifacts.set(COMPILE_SNAPSHOT, JSON.stringify(snapshot))
9167
9285
  await this.checkpointCompileTask(runId)
9286
+ const changedWiki = await this.wikiUnavailability(snapshot.cards, new Map(snapshot.placements))
9287
+ if (snapshot.cards.some(card => card.metadata.wiki_kind && changedWiki.has(cardKey(card)))) throw new Error('WIKI_CHANGED_DURING_BUILD: Wiki 的支撑知识在构建期间变化,请重新组织并构建索引')
9168
9288
  if (JSON.stringify({ config: this.config, fingerprint: this.embeddingFingerprint() }) !== configAtStart) throw new Error('COMPILE_CONFIG_CHANGED: 编译期间设置已变化,请重新编译')
9169
9289
  await options.checkpoint?.()
9170
9290
  db.publish(this.publication)
@@ -9173,6 +9293,8 @@ export class KnowledgeRuntime {
9173
9293
  return result
9174
9294
  } finally {
9175
9295
  this.publication = undefined
9296
+ // Compilation filters unavailable projections; the browsing cache still represents all saved Wiki.
9297
+ this.cards = await this.loadCards()
9176
9298
  }
9177
9299
  }
9178
9300
 
@@ -9436,6 +9558,9 @@ export class KnowledgeRuntime {
9436
9558
  await this.checkpointCompileTask(runId)
9437
9559
  // 相关 metadata 或旧正文清理刚落盘:stage 2 必须看到最新卡片。
9438
9560
  if (finalize.rewritten > 0) this.cards = await this.loadCards()
9561
+ const unavailableWiki = await this.wikiUnavailability(this.cards)
9562
+ for (const card of this.cards) if (card.metadata.wiki_kind && unavailableWiki.has(cardKey(card))) warnings.push(`Wiki「${card.title}」未进入索引:${unavailableWiki.get(cardKey(card))}`)
9563
+ this.cards = this.cards.filter(card => !card.metadata.wiki_kind || !unavailableWiki.has(cardKey(card)))
9439
9564
  /**
9440
9565
  * 索引页也是这次编译的产物,所以它变了也算「语料变了」。
9441
9566
  *
@@ -9848,12 +9973,11 @@ export class KnowledgeRuntime {
9848
9973
  }
9849
9974
 
9850
9975
  /**
9851
- * Coordinate 路:先解析 Query 中出现的 Object / Action / Scene,再把命中的卡片拆成
9976
+ * Coordinate 路:解析 Query 中出现的 Object / Action / Scene,再把命中的卡片拆成
9852
9977
  * Section 候选。它和 FTS / Vector 使用同一个 `unitId` 粒度,后面才能统一 RRF 与重排。
9853
9978
  *
9854
9979
  * 这里不让模型自由生成实体名:注册表负责 canonical / alias 命中,只有真实存在的
9855
- * 坐标 id 才能进入候选。意图目前使用轻量规则分类,结果会随 queryAnalysis 一起返回,
9856
- * 后续接入模型消歧时不需要改变召回合同。
9980
+ * 坐标 id 才能进入候选。未解析的原文继续交给关键词、向量路线与统一重排。
9857
9981
  */
9858
9982
  private async coordinateRoute(
9859
9983
  query: string,
@@ -10443,18 +10567,8 @@ export class KnowledgeRuntime {
10443
10567
  for (const key of liveKeys) {
10444
10568
  if (snapshotCardByKey.get(key)?.sourceRefs.some(ref => !liveSourceIds.has(ref))) liveKeys.delete(key)
10445
10569
  }
10446
- // A method becomes unavailable immediately when its supporting Case or structured content changes.
10447
- // Do not wait for a later rebuild to stop serving the old instructions.
10448
- const unsafeProcedures = new Set(snapshot.knowledge.filter(row => {
10449
- if (row.kind !== 'procedure') return false
10450
- const current = truthAtRead?.raw('SELECT status,head_version FROM knowledge WHERE id=?').get(row.id) as { status: string; head_version: number } | undefined
10451
- return !current || current.status === 'deprecated' || current.status === 'disputed' || current.head_version !== row.version
10452
- || !row.data?.procedure || row.data.procedure.completeness !== 'complete'
10453
- || row.data.procedure.caseRefs.some(ref => truthAtRead?.event(ref.id)?.version !== ref.version)
10454
- || row.sources.some(source => source.toVersion !== undefined && truthAtRead?.source(source.documentId)?.version !== source.toVersion)
10455
- }).map(row => row.id))
10456
- const unsafeCardIds = new Set(snapshot.placements.filter(([, placements]) => placements.some(p => unsafeProcedures.has(p.knowledgeId))).map(([id]) => id))
10457
- for (const key of liveKeys) if (unsafeCardIds.has(snapshotCardByKey.get(key)?.entityId ?? '')) liveKeys.delete(key)
10570
+ const unavailableWiki = await this.wikiUnavailability(snapshot.cards, new Map(snapshot.placements))
10571
+ for (const key of unavailableWiki.keys()) liveKeys.delete(key)
10458
10572
  let publishedGraph = visibleGraph(snapshot.graph, truthAtRead ? liveGraphKeys(truthAtRead) : new Set())
10459
10573
  const currentByKey = new Map(liveCards.map(card => [cardKey(card), card]))
10460
10574
  let cards: KnowledgeCard[] = snapshot.cards.filter(card => liveKeys.has(cardKey(card))).map(card => {
@@ -10575,9 +10689,6 @@ export class KnowledgeRuntime {
10575
10689
  } else {
10576
10690
  const rerankQuery = [
10577
10691
  query,
10578
- ...(queryAnalysis.intent.id !== 'unknown' && queryAnalysis.intent.confidence >= 0.7
10579
- ? [`意图:${queryAnalysis.intent.id}`]
10580
- : []),
10581
10692
  ...(['object', 'action', 'scene'] as const).flatMap(kind => {
10582
10693
  const values = queryAnalysis.axes[kind].map(item => item.name).slice(0, 3)
10583
10694
  return values.length > 0 ? [`${kind}:${values.join('、')}`] : []
@@ -10720,8 +10831,7 @@ export class KnowledgeRuntime {
10720
10831
  /**
10721
10832
  * 上下文预算(0050):装箱 + 溢出。
10722
10833
  *
10723
- * 让位顺序写死在 `packContext` 里:先保证命中 note 的正文完整(它是证据),
10724
- * 再给扩展卡片的标题 + 摘要 + 关系,最后才给扩展卡片的正文。
10834
+ * 让位顺序写死在 `packContext` 里:优先交付命中的完整 Wiki,再交付扩展 Wiki。
10725
10835
  */
10726
10836
  /**
10727
10837
  * 每次调用可以覆盖预算(0050):指路行里就写着「读不到就调大预算重新召回」,
@@ -10729,6 +10839,14 @@ export class KnowledgeRuntime {
10729
10839
  */
10730
10840
  const maxChars = options.contextMaxChars ?? this.config.contextMaxChars
10731
10841
  const packed = await this.packContext({ items, maxChars, recallId })
10842
+ const deliveredKeys = new Set(packed.items.flatMap(item => item.wiki ? [cardKey(item.wiki)] : []))
10843
+ // Diagnostic HTTP requests may inspect unbudgeted candidate text. Normal delivery cannot leak it.
10844
+ const deliveredHits = options.detail ? hits : hits.map(hit => {
10845
+ if (deliveredKeys.has(wikiKey(hit))) return hit
10846
+ const { evidence: _evidence, knowledge: _knowledge, ...reference } = hit
10847
+ return { ...reference, content: '' }
10848
+ })
10849
+ const deliveredWiki = options.detail ? wiki : wiki.filter(card => deliveredKeys.has(cardKey(card)))
10732
10850
  /** 哪些扩展卡片真的进了上下文(被预算挡住的也如实报出来)。 */
10733
10851
  expansion.included = packed.items
10734
10852
  .filter(item => item.reason === 'expansion' && item.wiki !== null)
@@ -10789,7 +10907,7 @@ export class KnowledgeRuntime {
10789
10907
  }
10790
10908
 
10791
10909
  const data: RecallResult = {
10792
- hits, context: packed.items, wiki, graph, backend, routes: ranRoutes, recallMode, rerank, recallTopK, candidatePool, fusedCount, contextTopN,
10910
+ hits: deliveredHits, context: packed.items, wiki: deliveredWiki, graph, backend, routes: ranRoutes, recallMode, rerank, recallTopK, candidatePool, fusedCount, contextTopN,
10793
10911
  queryAnalysis,
10794
10912
  insufficientEvidence, diagnostics, recallId, timings,
10795
10913
  expansion, budget: packed.report,