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

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 (266) hide show
  1. package/README.md +89 -56
  2. package/cordis.patch.yml +36 -129
  3. package/eval/benchmark-extraction-format.mjs +20 -0
  4. package/lib/client.js +3262 -4142
  5. package/lib/doc-page.js +1 -1
  6. package/lib/pages.js +1147 -364
  7. package/lib/types/axis-registry.d.ts +8 -1
  8. package/lib/types/axis-registry.d.ts.map +1 -1
  9. package/lib/types/axis-registry.js +62 -7
  10. package/lib/types/axis-registry.js.map +1 -1
  11. package/lib/types/convert/host.d.ts.map +1 -1
  12. package/lib/types/convert/host.js +3 -0
  13. package/lib/types/convert/host.js.map +1 -1
  14. package/lib/types/convert/ingest.d.ts +9 -0
  15. package/lib/types/convert/ingest.d.ts.map +1 -1
  16. package/lib/types/convert/ingest.js +68 -9
  17. package/lib/types/convert/ingest.js.map +1 -1
  18. package/lib/types/diagnosis/cards.d.ts +7 -3
  19. package/lib/types/diagnosis/cards.d.ts.map +1 -1
  20. package/lib/types/diagnosis/cards.js +13 -8
  21. package/lib/types/diagnosis/cards.js.map +1 -1
  22. package/lib/types/diagnosis/run.d.ts +4 -13
  23. package/lib/types/diagnosis/run.d.ts.map +1 -1
  24. package/lib/types/diagnosis/run.js +26 -16
  25. package/lib/types/diagnosis/run.js.map +1 -1
  26. package/lib/types/diagnosis/schema.js +2 -2
  27. package/lib/types/diagnosis/schema.js.map +1 -1
  28. package/lib/types/diagnosis/seed-source.d.ts +1 -1
  29. package/lib/types/diagnosis/seed-source.d.ts.map +1 -1
  30. package/lib/types/diagnosis/seed-source.js +5 -4
  31. package/lib/types/diagnosis/seed-source.js.map +1 -1
  32. package/lib/types/diagnosis/seed.d.ts +4 -4
  33. package/lib/types/diagnosis/seed.d.ts.map +1 -1
  34. package/lib/types/diagnosis/seed.js +6 -8
  35. package/lib/types/diagnosis/seed.js.map +1 -1
  36. package/lib/types/diagnosis/store.d.ts +18 -17
  37. package/lib/types/diagnosis/store.d.ts.map +1 -1
  38. package/lib/types/diagnosis/store.js +94 -66
  39. package/lib/types/diagnosis/store.js.map +1 -1
  40. package/lib/types/diagnosis/types.d.ts +10 -16
  41. package/lib/types/diagnosis/types.d.ts.map +1 -1
  42. package/lib/types/diagnosis/types.js.map +1 -1
  43. package/lib/types/environment/dsh-python.d.ts +77 -0
  44. package/lib/types/environment/dsh-python.d.ts.map +1 -0
  45. package/lib/types/environment/dsh-python.js +139 -0
  46. package/lib/types/environment/dsh-python.js.map +1 -0
  47. package/lib/types/environment/install.d.ts +9 -1
  48. package/lib/types/environment/install.d.ts.map +1 -1
  49. package/lib/types/environment/install.js +149 -24
  50. package/lib/types/environment/install.js.map +1 -1
  51. package/lib/types/environment/model-catalog.d.ts +34 -2
  52. package/lib/types/environment/model-catalog.d.ts.map +1 -1
  53. package/lib/types/environment/model-catalog.js +40 -2
  54. package/lib/types/environment/model-catalog.js.map +1 -1
  55. package/lib/types/environment/service.d.ts +13 -3
  56. package/lib/types/environment/service.d.ts.map +1 -1
  57. package/lib/types/environment/service.js +127 -23
  58. package/lib/types/environment/service.js.map +1 -1
  59. package/lib/types/environment/types.d.ts +9 -0
  60. package/lib/types/environment/types.d.ts.map +1 -1
  61. package/lib/types/events.d.ts +2 -0
  62. package/lib/types/events.d.ts.map +1 -1
  63. package/lib/types/events.js +13 -0
  64. package/lib/types/events.js.map +1 -1
  65. package/lib/types/extract-contract.d.ts +1 -1
  66. package/lib/types/extract-contract.d.ts.map +1 -1
  67. package/lib/types/extract-contract.js +34 -31
  68. package/lib/types/extract-contract.js.map +1 -1
  69. package/lib/types/extract-model.d.ts +2 -0
  70. package/lib/types/extract-model.d.ts.map +1 -1
  71. package/lib/types/extract-model.js.map +1 -1
  72. package/lib/types/extract-table-contract.d.ts +3 -0
  73. package/lib/types/extract-table-contract.d.ts.map +1 -0
  74. package/lib/types/extract-table-contract.js +30 -0
  75. package/lib/types/extract-table-contract.js.map +1 -0
  76. package/lib/types/extract-table.d.ts +8 -0
  77. package/lib/types/extract-table.d.ts.map +1 -0
  78. package/lib/types/extract-table.js +151 -0
  79. package/lib/types/extract-table.js.map +1 -0
  80. package/lib/types/extraction-runs.d.ts +30 -6
  81. package/lib/types/extraction-runs.d.ts.map +1 -1
  82. package/lib/types/extraction-runs.js +69 -8
  83. package/lib/types/extraction-runs.js.map +1 -1
  84. package/lib/types/fs-atomic.d.ts +0 -7
  85. package/lib/types/fs-atomic.d.ts.map +1 -1
  86. package/lib/types/fs-atomic.js +17 -2
  87. package/lib/types/fs-atomic.js.map +1 -1
  88. package/lib/types/images.d.ts +1 -1
  89. package/lib/types/images.d.ts.map +1 -1
  90. package/lib/types/images.js +2 -2
  91. package/lib/types/images.js.map +1 -1
  92. package/lib/types/index.d.ts.map +1 -1
  93. package/lib/types/index.js +232 -50
  94. package/lib/types/index.js.map +1 -1
  95. package/lib/types/intent/scope.d.ts.map +1 -1
  96. package/lib/types/intent/scope.js +1 -0
  97. package/lib/types/intent/scope.js.map +1 -1
  98. package/lib/types/intent/tools.d.ts +19 -0
  99. package/lib/types/intent/tools.d.ts.map +1 -1
  100. package/lib/types/intent/tools.js +37 -1
  101. package/lib/types/intent/tools.js.map +1 -1
  102. package/lib/types/knowledge-bases.d.ts.map +1 -1
  103. package/lib/types/knowledge-bases.js +5 -16
  104. package/lib/types/knowledge-bases.js.map +1 -1
  105. package/lib/types/knowledge.d.ts +3 -0
  106. package/lib/types/knowledge.d.ts.map +1 -1
  107. package/lib/types/knowledge.js +21 -5
  108. package/lib/types/knowledge.js.map +1 -1
  109. package/lib/types/materialize-snapshot.d.ts +49 -0
  110. package/lib/types/materialize-snapshot.d.ts.map +1 -0
  111. package/lib/types/materialize-snapshot.js +2 -0
  112. package/lib/types/materialize-snapshot.js.map +1 -0
  113. package/lib/types/model-text.d.ts +1 -1
  114. package/lib/types/model-text.d.ts.map +1 -1
  115. package/lib/types/model-text.js +5 -4
  116. package/lib/types/model-text.js.map +1 -1
  117. package/lib/types/oplog.d.ts.map +1 -1
  118. package/lib/types/oplog.js +7 -0
  119. package/lib/types/oplog.js.map +1 -1
  120. package/lib/types/pipeline/engine.d.ts +60 -0
  121. package/lib/types/pipeline/engine.d.ts.map +1 -0
  122. package/lib/types/pipeline/engine.js +338 -0
  123. package/lib/types/pipeline/engine.js.map +1 -0
  124. package/lib/types/pipeline/store.d.ts +73 -0
  125. package/lib/types/pipeline/store.d.ts.map +1 -0
  126. package/lib/types/pipeline/store.js +143 -0
  127. package/lib/types/pipeline/store.js.map +1 -0
  128. package/lib/types/pipeline/types.d.ts +101 -0
  129. package/lib/types/pipeline/types.d.ts.map +1 -0
  130. package/lib/types/pipeline/types.js +5 -0
  131. package/lib/types/pipeline/types.js.map +1 -0
  132. package/lib/types/present.d.ts +2 -0
  133. package/lib/types/present.d.ts.map +1 -1
  134. package/lib/types/present.js +9 -4
  135. package/lib/types/present.js.map +1 -1
  136. package/lib/types/retention.d.ts +39 -0
  137. package/lib/types/retention.d.ts.map +1 -0
  138. package/lib/types/retention.js +42 -0
  139. package/lib/types/retention.js.map +1 -0
  140. package/lib/types/runtime.d.ts +115 -5
  141. package/lib/types/runtime.d.ts.map +1 -1
  142. package/lib/types/runtime.js +862 -132
  143. package/lib/types/runtime.js.map +1 -1
  144. package/lib/types/source-evidence.d.ts +9 -1
  145. package/lib/types/source-evidence.d.ts.map +1 -1
  146. package/lib/types/source-evidence.js +13 -5
  147. package/lib/types/source-evidence.js.map +1 -1
  148. package/lib/types/store/axis-references.js +3 -3
  149. package/lib/types/store/axis-references.js.map +1 -1
  150. package/lib/types/store/derived.d.ts +1 -1
  151. package/lib/types/store/derived.d.ts.map +1 -1
  152. package/lib/types/store/derived.js +41 -7
  153. package/lib/types/store/derived.js.map +1 -1
  154. package/lib/types/store/knowledge-db.d.ts +18 -0
  155. package/lib/types/store/knowledge-db.d.ts.map +1 -1
  156. package/lib/types/store/knowledge-db.js +111 -6
  157. package/lib/types/store/knowledge-db.js.map +1 -1
  158. package/lib/types/store/migrate.js +1 -1
  159. package/lib/types/store/migrate.js.map +1 -1
  160. package/lib/types/trajectory-types.d.ts +172 -0
  161. package/lib/types/trajectory-types.d.ts.map +1 -0
  162. package/lib/types/trajectory-types.js +36 -0
  163. package/lib/types/trajectory-types.js.map +1 -0
  164. package/lib/types/types.d.ts +3 -4
  165. package/lib/types/types.d.ts.map +1 -1
  166. package/lib/types/types.js.map +1 -1
  167. package/lib/types/wiki-finalize.d.ts +16 -0
  168. package/lib/types/wiki-finalize.d.ts.map +1 -1
  169. package/lib/types/wiki-finalize.js +26 -0
  170. package/lib/types/wiki-finalize.js.map +1 -1
  171. package/lib/types/wiki-materialize.d.ts.map +1 -1
  172. package/lib/types/wiki-materialize.js +3 -15
  173. package/lib/types/wiki-materialize.js.map +1 -1
  174. package/lib/types/wiki-trial-types.d.ts +3 -0
  175. package/lib/types/wiki-trial-types.d.ts.map +1 -1
  176. package/lib/types/wiki-trial.d.ts +2 -0
  177. package/lib/types/wiki-trial.d.ts.map +1 -1
  178. package/lib/types/wiki-trial.js +31 -7
  179. package/lib/types/wiki-trial.js.map +1 -1
  180. package/package.json +6 -3
  181. package/preset/tita-data-agent/agent.cordis.yml +36 -129
  182. package/src/axis-registry.ts +57 -9
  183. package/src/client/AttributionEventGuide.tsx +10 -9
  184. package/src/client/CompileDialogs.tsx +10 -2
  185. package/src/client/EnvironmentSetup.tsx +76 -16
  186. package/src/client/ExpansionGraph.tsx +13 -7
  187. package/src/client/ExtractionTasksPage.tsx +543 -156
  188. package/src/client/ImportFilesDialog.tsx +2 -13
  189. package/src/client/IntentTab.tsx +3 -1
  190. package/src/client/KnowledgeBaseTab.tsx +139 -122
  191. package/src/client/KnowledgeLayerSection.tsx +19 -2
  192. package/src/client/KnowledgePanel.tsx +17 -3
  193. package/src/client/ListPager.tsx +15 -5
  194. package/src/client/MaterializeTrajectoryView.tsx +124 -0
  195. package/src/client/NodeSettings.tsx +90 -25
  196. package/src/client/NormalizeTrajectoryView.tsx +136 -0
  197. package/src/client/PipelineNotice.tsx +26 -0
  198. package/src/client/RecallPanel.tsx +55 -25
  199. package/src/client/TabGuide.tsx +4 -4
  200. package/src/client/TrialReportView.tsx +61 -16
  201. package/src/client/TypeDefinitionDrawer.tsx +33 -9
  202. package/src/client/WikiCardPreview.tsx +43 -10
  203. package/src/client/WikiFlowPanel.tsx +4 -4
  204. package/src/client/architecture.tsx +1 -1
  205. package/src/client/attribution/AttributionTab.tsx +37 -36
  206. package/src/client/attribution/CaseView.tsx +3 -1
  207. package/src/client/attribution/EventView.tsx +14 -4
  208. package/src/client/attribution/RunView.tsx +2 -2
  209. package/src/client/attribution/ViewSwitch.tsx +20 -5
  210. package/src/client/attribution/graph-options.ts +38 -31
  211. package/src/client/attribution/model.ts +16 -9
  212. package/src/client/graph/palette.ts +165 -0
  213. package/src/client/pages.css +224 -28
  214. package/src/client/pages.tsx +33 -11
  215. package/src/client/styles.css +61 -6
  216. package/src/client/wiki-trial.ts +30 -1
  217. package/src/convert/host.ts +3 -0
  218. package/src/convert/ingest.ts +73 -9
  219. package/src/diagnosis/cards.ts +13 -8
  220. package/src/diagnosis/run.ts +27 -17
  221. package/src/diagnosis/schema.ts +2 -2
  222. package/src/diagnosis/seed-source.ts +5 -4
  223. package/src/diagnosis/seed.ts +6 -8
  224. package/src/diagnosis/store.ts +99 -69
  225. package/src/diagnosis/types.ts +10 -16
  226. package/src/environment/dsh-python.ts +156 -0
  227. package/src/environment/install.ts +134 -19
  228. package/src/environment/model-catalog.ts +45 -3
  229. package/src/environment/service.ts +111 -23
  230. package/src/environment/types.ts +6 -0
  231. package/src/events.ts +12 -0
  232. package/src/extract-contract.ts +6 -3
  233. package/src/extract-model.ts +2 -0
  234. package/src/extract-table-contract.ts +29 -0
  235. package/src/extract-table.ts +126 -0
  236. package/src/extraction-runs.ts +70 -11
  237. package/src/fs-atomic.ts +19 -2
  238. package/src/images.ts +2 -2
  239. package/src/index.ts +207 -72
  240. package/src/intent/scope.ts +1 -0
  241. package/src/intent/tools.ts +38 -1
  242. package/src/knowledge-bases.ts +4 -12
  243. package/src/knowledge.ts +24 -4
  244. package/src/materialize-snapshot.ts +46 -0
  245. package/src/model-text.ts +5 -3
  246. package/src/oplog.ts +7 -0
  247. package/src/pipeline/engine.ts +312 -0
  248. package/src/pipeline/store.ts +147 -0
  249. package/src/pipeline/types.ts +92 -0
  250. package/src/present.ts +10 -4
  251. package/src/retention.ts +76 -0
  252. package/src/runtime.ts +853 -131
  253. package/src/source-evidence.ts +12 -5
  254. package/src/store/axis-references.ts +3 -3
  255. package/src/store/derived.ts +40 -7
  256. package/src/store/knowledge-db.ts +111 -5
  257. package/src/store/migrate.ts +1 -1
  258. package/src/trajectory-types.ts +142 -0
  259. package/src/types.ts +3 -4
  260. package/src/wiki-finalize.ts +28 -0
  261. package/src/wiki-materialize.ts +3 -14
  262. package/src/wiki-trial-types.ts +3 -0
  263. package/src/wiki-trial.ts +26 -7
  264. package/src/client/convert/ConvertPanel.tsx +0 -583
  265. package/src/client/convert/styles.css +0 -138
  266. package/src/client/convert/styles.ts +0 -49
@@ -2,17 +2,24 @@ import type { Knowledge, KnowledgeSource } from './knowledge.js'
2
2
  import type { KnowledgeDb } from './store/knowledge-db.js'
3
3
 
4
4
  /** Checking an evidence anchor does not establish whether a claim is true. */
5
- export function assessKnowledgeEvidence(knowledge: Knowledge, db: KnowledgeDb): Knowledge {
5
+ /**
6
+ * `bodies` 是可选的**来源正文预读**(documentId → 当前版本与正文)。批量路径(物化快照)
7
+ * 一次读完全部来源再传进来,于是每条知识不再各自 join 一次 blob、各做一次整份文档的
8
+ * flatten 与子串匹配 —— 那是 O(知识条数 × 文档大小)。不传时退回按需读取(单条判定)。
9
+ */
10
+ export function assessKnowledgeEvidence(knowledge: Knowledge, db: KnowledgeDb, bodies?: ReadonlyMap<string, { version: number; content: string }>): Knowledge {
6
11
  return {
7
12
  ...knowledge,
8
- sources: knowledge.sources.map(source => assessSource(source, db)),
13
+ sources: knowledge.sources.map(source => assessSource(source, db, bodies)),
9
14
  }
10
15
  }
11
16
 
12
- function assessSource(source: KnowledgeSource, db: KnowledgeDb): KnowledgeSource {
17
+ function assessSource(source: KnowledgeSource, db: KnowledgeDb, bodies?: ReadonlyMap<string, { version: number; content: string }>): KnowledgeSource {
18
+ if (source.withdrawn) return { ...source, evidenceStatus: 'historical', quoteInCurrentVersion: false }
13
19
  if (!source.documentId) return { ...source, evidenceStatus: 'human' }
14
- const head = db.source(source.documentId)
20
+ const head = bodies?.get(source.documentId) ?? db.source(source.documentId)
15
21
  if (!head) return { ...source, evidenceStatus: 'missing', quoteInCurrentVersion: false }
22
+ const content = 'record' in head ? head.record.content : head.content
16
23
  const flatten = (text: string): string => text.replace(/\s+/g, '')
17
24
  const quote = flatten(source.quote)
18
25
  return {
@@ -20,7 +27,7 @@ function assessSource(source: KnowledgeSource, db: KnowledgeDb): KnowledgeSource
20
27
  currentVersion: head.version,
21
28
  evidenceStatus: source.toVersion === undefined ? 'unversioned'
22
29
  : source.toVersion === head.version ? 'current' : 'historical',
23
- quoteInCurrentVersion: quote !== '' && flatten(head.record.content).includes(quote),
30
+ quoteInCurrentVersion: quote !== '' && flatten(content).includes(quote),
24
31
  }
25
32
  }
26
33
 
@@ -1,6 +1,6 @@
1
1
  import { createHash } from 'node:crypto'
2
2
  import type { AxisKind } from '../axis-registry.js'
3
- import { compositeEventIdentity, eventEntityFromRecords, type EventEntity } from '../events.js'
3
+ import { compositeEventIdentity, eventEntityFromRecords, stripEventReferenceSection, type EventEntity } from '../events.js'
4
4
  import { cardCoordinateKey, type CardCoordinate } from '../types.js'
5
5
  import type { KnowledgeDb, CardKnowledgeRow } from './knowledge-db.js'
6
6
 
@@ -28,7 +28,7 @@ export function redirectAxisReferences(db: KnowledgeDb, kind: AxisKind, fromId:
28
28
  const eventRefs = [...new Set([...db.cardEventIds(base.card.entityId), ...db.cardEventIds(row.card.entityId)])]
29
29
  const card = {
30
30
  ...base.card, coordinate: next,
31
- content: merged ? `${base.card.content}\n\n${row.card.content}` : row.card.content,
31
+ content: stripEventReferenceSection(merged ? `${base.card.content}\n\n${row.card.content}` : row.card.content),
32
32
  sourceRefs: [...new Set([...base.card.sourceRefs, ...row.card.sourceRefs])],
33
33
  annotations: [...new Map([...(base.card.annotations ?? []), ...(row.card.annotations ?? [])].map(annotation => [annotation.id, annotation])).values()],
34
34
  metadata: { ...base.card.metadata, layout: undefined, citations: undefined,
@@ -84,7 +84,7 @@ export function redirectAxisReferences(db: KnowledgeDb, kind: AxisKind, fromId:
84
84
  }
85
85
  if (redirects.size) {
86
86
  for (const row of db.cards()) {
87
- let content = row.card.content
87
+ let content = stripEventReferenceSection(row.card.content)
88
88
  for (const [from, into] of redirects) content = content.replaceAll(from, into)
89
89
  if (content === row.card.content && !affectedCards.has(row.card.entityId)) continue
90
90
  const metadata = { ...row.card.metadata, event_refs: JSON.stringify(db.cardEventIds(row.card.entityId)) }
@@ -158,12 +158,29 @@ export class DerivedDb {
158
158
  try {
159
159
  const owner = readPragma(db, 'application_id')
160
160
  const version = readPragma(db, 'user_version')
161
- if (userTableCount(db) === 0 && owner === 0) {
161
+ /**
162
+ * 属主标记为 0 = 这份文件从来没有被我们的初始化**写完过**。两种来源都要按「无主」处理:
163
+ *
164
+ * 1. 全新/空的文件(一张表都没有);
165
+ * 2. `schemaSql()` 已经建好表、`PRAGMA application_id` 还没写就被杀掉的那一次
166
+ * (崩溃、kill、磁盘满)。第二种原来会落到下面的「属主不对」分支,返回
167
+ * `NOT_A_DERIVED_DB` —— 而 `derivedHandle()` 会把这个结论缓存住,于是编译与召回
168
+ * **永久**失败,尽管这个文件按定义是可重建的。
169
+ *
170
+ * 但不能因此误伤别人的库:只有「文件里所有表都是我们自己建的」才认领重建,
171
+ * 出现陌生表就仍然拒绝打开(认错库比打不开糟)。
172
+ */
173
+ const tables = userTables(db)
174
+ const foreign = tables.filter(name => !['artifact', 'vector_index', 'fts'].includes(name))
175
+ if (owner === 0 && foreign.length === 0) {
176
+ db.exec('DROP TABLE IF EXISTS fts')
177
+ db.exec('DROP TABLE IF EXISTS artifact')
178
+ db.exec('DROP TABLE IF EXISTS vector_index')
162
179
  db.exec('PRAGMA journal_mode = WAL')
163
180
  db.exec(schemaSql())
164
181
  db.exec(`PRAGMA application_id = ${DERIVED_DB_APPLICATION_ID}`)
165
182
  db.exec(`PRAGMA user_version = ${DERIVED_DB_SCHEMA}`)
166
- return { ok: true, db: new DerivedDb(db, file), rebuilt: false }
183
+ return { ok: true, db: new DerivedDb(db, file), rebuilt: tables.length > 0 }
167
184
  }
168
185
  if (owner !== DERIVED_DB_APPLICATION_ID) {
169
186
  db.close()
@@ -302,11 +319,19 @@ export class DerivedDb {
302
319
  * 查询语法错误或索引损坏时**返回空数组而不抛**:关键词路是三条召回路线之一,
303
320
  * 它挂了不该把整次召回带走 —— 调用方在诊断里如实说明这一路没结果。
304
321
  */
305
- searchFts(query: string, limit: number): FtsHit[] {
322
+ searchFts(query: string, limit: number, eligible?: ReadonlySet<string>): FtsHit[] {
306
323
  const expression = matchExpression(query)
307
324
  if (expression === null || !this.hasFts()) return []
308
325
  try {
309
- const rows = this.raw('SELECT unit_id, bm25(fts) AS rank FROM fts WHERE fts MATCH ? ORDER BY rank LIMIT ?').all(expression, limit) as Array<{ unit_id?: unknown; rank?: unknown }>
326
+ /*
327
+ * `eligible` 必须进 SQL。先 LIMIT 再在 JS 里过滤,会让已删除/停用卡片的死
328
+ * unitId 占满候选窗口(索引只在编译时重建,删除不会立刻反映到 fts 表)。
329
+ * 用 `json_each` 传一个 JSON 数组而不是拼参数列表:一个绑定参数,不受
330
+ * SQLITE_MAX_VARIABLE_NUMBER 限制。
331
+ */
332
+ const rows = (eligible === undefined
333
+ ? this.raw('SELECT unit_id, bm25(fts) AS rank FROM fts WHERE fts MATCH ? ORDER BY rank LIMIT ?').all(expression, limit)
334
+ : this.raw('SELECT unit_id, bm25(fts) AS rank FROM fts WHERE fts MATCH ? AND unit_id IN (SELECT value FROM json_each(?)) ORDER BY rank LIMIT ?').all(expression, JSON.stringify([...eligible]), limit)) as Array<{ unit_id?: unknown; rank?: unknown }>
310
335
  const parsed = rows
311
336
  .map(row => ({ unitId: String(row.unit_id ?? ''), raw: Number(row.rank ?? 0) }))
312
337
  .filter(row => row.unitId !== '' && Number.isFinite(row.raw))
@@ -316,6 +341,14 @@ export class DerivedDb {
316
341
  const scale = best > 0 ? best : 1
317
342
  return parsed.map(row => ({ ...row, score: Math.max(0, Math.min(1, -row.raw / scale)) }))
318
343
  } catch {
344
+ /**
345
+ * `json_each` 不可用(SQLite 没编 JSON)或索引损坏时,**不能直接返回空**。
346
+ *
347
+ * 空数组会被上游读成「这一路没命中」,而实际是「这一路坏了」—— 正是这次要消灭的
348
+ * 那种静默降级。退回不带资格过滤的两参数查询:结果仍然正确(调用方还会过滤一遍),
349
+ * 只是候选窗口可能被已删除的条目占掉。真的连那条也失败,才认「没有结果」。
350
+ */
351
+ if (eligible !== undefined) return this.searchFts(query, limit)
319
352
  return []
320
353
  }
321
354
  }
@@ -335,7 +368,7 @@ function readPragma(db: SqliteDatabase, name: string): number {
335
368
  return typeof first === 'number' ? first : Number(first ?? 0)
336
369
  }
337
370
 
338
- function userTableCount(db: SqliteDatabase): number {
339
- const rows = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'").all() as Array<{ name?: unknown }>
340
- return rows.length
371
+ /** 文件里现有的表名。用来判断「这份文件是不是我们建的」。 */
372
+ function userTables(db: SqliteDatabase): string[] {
373
+ return (db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'").all() as Array<{ name?: unknown }>).map(row => String(row.name ?? ''))
341
374
  }
@@ -31,6 +31,7 @@ import { createHash } from 'node:crypto'
31
31
  import { mkdir } from 'node:fs/promises'
32
32
  import { dirname } from 'node:path'
33
33
  import type { SqliteDatabase, SqliteStatement } from '../fts.js'
34
+ import { applyRetention } from '../retention.js'
34
35
  import { KNOWLEDGE_DB_FILE } from '../layout.js'
35
36
  import { cardCoordinateKey, legacyCardStorageKey, type CardCoordinate, type KnowledgeCard, type SourceRaw } from '../types.js'
36
37
  import type { EventEntity } from '../events.js'
@@ -445,15 +446,25 @@ function rebuildCardTableForSchemaMigration(db: SqliteDatabase): void {
445
446
  head_version, created_at, updated_at, object_id, action_id, scene_id)
446
447
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
447
448
  `)
449
+ /** 已经用掉的落位键。同坐标多行时后面那些要退回身份键(见下面的说明)。 */
450
+ const used = new Set<string>()
448
451
  for (const row of rows) {
449
452
  const entityId = String(row.entity_id ?? '')
450
453
  const objectId = String(row.object_id ?? '')
451
454
  const actionId = String(row.action_id ?? '')
452
455
  const sceneId = String(row.scene_id ?? '')
453
456
  const coordinate = coordinateOfRow({ object_id: objectId, action_id: actionId, scene_id: sceneId })
454
- const key = coordinate === undefined
457
+ let key = coordinate === undefined
455
458
  ? legacyCardStorageKey(entityId)
456
459
  : cardCoordinateKey(coordinate)
460
+ /**
461
+ * 同一坐标在旧表里可能有多行(那时还没有坐标唯一约束)。`coordinate_key` 是主键,
462
+ * 一旦冲突整次迁移就回滚 —— 而迁移在打开库时跑,于是**每次启动都打不开这个库**,
463
+ * 也没有修复路径。落在后面的那一行退回按 `entityId` 生成的身份键:两张卡都留住,
464
+ * 坐标冲突交给既有的审计路径去报。`entity_id` 在旧表里是唯一的,所以这个键不会重。
465
+ */
466
+ if (used.has(key)) key = legacyCardStorageKey(entityId)
467
+ used.add(key)
457
468
  insert.run(
458
469
  key,
459
470
  entityId,
@@ -615,6 +626,8 @@ interface ColumnInfo {
615
626
  name: string
616
627
  notNull: boolean
617
628
  hasDefault: boolean
629
+ /** 主键里的序号(1 起);0 = 不是主键的一部分。判「是不是同一行」要用它。 */
630
+ pk: number
618
631
  }
619
632
 
620
633
  /**
@@ -851,6 +864,8 @@ export class KnowledgeTx {
851
864
  */
852
865
  export class KnowledgeDb {
853
866
  private transactionSequence = 0
867
+ /** 距离上一次保留裁剪又提交了几次写事务。 */
868
+ private writesSincePrune = 0
854
869
  private constructor(private readonly db: SqliteDatabase, readonly file: string) {}
855
870
 
856
871
  /**
@@ -935,6 +950,8 @@ export class KnowledgeDb {
935
950
  }
936
951
 
937
952
  close(): void {
953
+ // 关库前收一次尾:否则「最后一次裁剪之后新增的行」会一直留到下次打开。
954
+ this.pruneRetention()
938
955
  try { this.db.close() } catch { /* already closed */ }
939
956
  }
940
957
 
@@ -971,6 +988,17 @@ export class KnowledgeDb {
971
988
  const result = fn(tx)
972
989
  this.raw('UPDATE tree SET manifest = ? WHERE op_id = ?').run(tx.manifestJson(), entry.opId)
973
990
  this.db.exec(`RELEASE ${savepoint}`)
991
+ /**
992
+ * 裁剪**每 50 次写入**做一次,不是每次。
993
+ *
994
+ * 每次都做的话,一次卡片写入要额外跑一遍 `ROW_NUMBER()` 按实体分区的窗口(水位以上
995
+ * 收敛到约 20 版/实体,5000 个实体就是十万行排序)外加一次 blob 引用集比对 ——
996
+ * 那是把维护成本摊到每一次交互上。每 50 次一次,界面上感觉不到,库也照样有界。
997
+ */
998
+ if (++this.writesSincePrune >= 50) {
999
+ this.writesSincePrune = 0
1000
+ this.pruneRetention()
1001
+ }
974
1002
  return result
975
1003
  } catch (cause) {
976
1004
  try { this.db.exec(`ROLLBACK TO ${savepoint}; RELEASE ${savepoint}`) } catch { /* rollback of a dead tx */ }
@@ -978,6 +1006,47 @@ export class KnowledgeDb {
978
1006
  }
979
1007
  }
980
1008
 
1009
+ /**
1010
+ * 保留上限(0070)。
1011
+ *
1012
+ * 文档里写了保留策略,代码里一条 `DELETE` 都没有。这里补上**新数据**的上限,
1013
+ * 且只裁「水位以上」的行 —— 升级那一刻已有的历史一行不动(见 `retention.ts`)。
1014
+ *
1015
+ * - `revision`:机器版本(`auto`)每个实体留最近 20 版;**命名版本永不裁**
1016
+ * (`named` 是人对内容做的决定,保留策略里它是永久的)。
1017
+ * - `pipeline_extractions`:抽取检查点同时是复用缓存,每份来源留最近 5 份;
1018
+ * `pipeline_sources` 指着的那两份(当前抽取 / 当前归一)绝不删 ——
1019
+ * 删了会让那份来源显示成「还没抽取」。
1020
+ * - `blob`:水位以上、已经没有任何 revision 引用的才删。水位以下的 blob 一律留着,
1021
+ * 因为老版本可能还在引用它们。
1022
+ * - `tree`:操作台账,与处理日志同一量级。
1023
+ */
1024
+ private pruneRetention(): void {
1025
+ /*
1026
+ * 每一张都要先问在不在:`pipeline_extractions` 与 `pipeline_sources` 是**管线模块**
1027
+ * 懒建的(`PipelineStore` 的构造函数),而 knowledge.db 可能被别的模块先打开、
1028
+ * 也可能根本没跑过管线。少了这一步,这里会以「no such table」把**每一次写入**都打挂。
1029
+ *
1030
+ * 整个方法包在 try 里还有一层理由:裁剪是**尽力而为**的维护动作,它绝不能成为
1031
+ * 一次已提交写入的失败原因。真出错时宁可不裁(库继续变大),也不让用户写不进去。
1032
+ */
1033
+ try {
1034
+ if (tableExists(this.db, 'revision')) {
1035
+ applyRetention(this.db, 'revision', { table: 'revision', key: 'rowid', keep: 20, where: "kind = 'auto'", groupBy: "entity_kind || '/' || entity_id" })
1036
+ }
1037
+ if (tableExists(this.db, 'pipeline_extractions') && tableExists(this.db, 'pipeline_sources')) {
1038
+ applyRetention(this.db, 'pipeline_extractions', {
1039
+ table: 'pipeline_extractions', key: 'rowid', keep: 5, groupBy: 'source_id',
1040
+ where: "id NOT IN (SELECT extracted_id FROM pipeline_sources WHERE extracted_id IS NOT NULL) AND id NOT IN (SELECT normalized_id FROM pipeline_sources WHERE normalized_id IS NOT NULL)",
1041
+ })
1042
+ }
1043
+ if (tableExists(this.db, 'blob') && tableExists(this.db, 'revision')) {
1044
+ applyRetention(this.db, 'blob', { table: 'blob', key: 'rowid', keep: 0, where: 'hash NOT IN (SELECT blob_hash FROM revision)' })
1045
+ }
1046
+ if (tableExists(this.db, 'tree')) applyRetention(this.db, 'tree', { table: 'tree', key: 'rowid', keep: 2000 })
1047
+ } catch { /* 裁剪是尽力而为:不让它成为写入失败的原因 */ }
1048
+ }
1049
+
981
1050
  /** 记一次**没有内容变更**的操作(迁移收尾、审核结论这一类)。 */
982
1051
  recordOperation(entry: TreeEntry, manifest: Record<string, number> = {}): void {
983
1052
  this.raw('INSERT OR REPLACE INTO tree(op_id, at, actor, summary, manifest) VALUES (?, ?, ?, ?, ?)')
@@ -1284,12 +1353,13 @@ export class KnowledgeDb {
1284
1353
  * 名字拼成语法错误。
1285
1354
  */
1286
1355
  private columnsOf(schema: 'main' | 'legacy', table: string): ColumnInfo[] {
1287
- const rows = this.raw('SELECT name, "notnull" AS not_null, dflt_value AS dflt FROM pragma_table_info(?, ?)')
1356
+ const rows = this.raw('SELECT name, "notnull" AS not_null, dflt_value AS dflt, pk FROM pragma_table_info(?, ?)')
1288
1357
  .all(table, schema) as Array<Record<string, unknown>>
1289
1358
  return rows.map(row => ({
1290
1359
  name: String(row.name ?? ''),
1291
1360
  notNull: Number(row.not_null ?? 0) === 1,
1292
1361
  hasDefault: row.dflt !== null && row.dflt !== undefined,
1362
+ pk: Number(row.pk ?? 0),
1293
1363
  }))
1294
1364
  }
1295
1365
 
@@ -1377,11 +1447,47 @@ export class KnowledgeDb {
1377
1447
  if (shared.length === 0) { problems.push(`${name} 与目标表没有同名的列,行搬不动`); continue }
1378
1448
  const columns = shared.map(quoteIdentifier).join(', ')
1379
1449
  const expected = Number((this.db.prepare(`SELECT count(*) AS c FROM legacy.${quoteIdentifier(name)}`).get() as { c?: unknown } | undefined)?.c ?? 0)
1380
- this.db.exec(`INSERT OR IGNORE INTO main.${quoteIdentifier(name)} (${columns}) SELECT ${columns} FROM legacy.${quoteIdentifier(name)}`)
1450
+ /**
1451
+ * 「有没有丢行」要按**三个账**算(0070):`already`(插入**之前**主键就已存在)、
1452
+ * `inserted`(本次真的插进去的)、以及两者之外的 `unaccounted`
1453
+ * —— 被唯一索引 / 非空 / 检查约束挡掉的那些才是真的丢。
1454
+ *
1455
+ * **顺序很要紧**:`already` 必须在 `INSERT` 之前数。插完再数会把本次插入的行
1456
+ * 也算进去,重复计数恰好把丢行掩盖成 0(试过,就是这样漏掉一行的)。
1457
+ *
1458
+ * 原来那条 `copied < expected`(拿目标**总行数**比旧文件行数)则**永远不成立** ——
1459
+ * 每一次主键冲突都对应一行早就存在 —— 于是丢行之后调用方还会把旧库删掉。
1460
+ */
1461
+ const keyColumns = this.columnsOf('main', name).filter(column => column.pk > 0).map(column => column.name)
1462
+ const comparable = shared.filter(column => !keyColumns.includes(column))
1463
+ const already = keyColumns.length === 0 ? 0 : Number((this.db.prepare(
1464
+ `SELECT COUNT(*) AS c FROM legacy.${quoteIdentifier(name)} l WHERE EXISTS (
1465
+ SELECT 1 FROM main.${quoteIdentifier(name)} m WHERE ${keyColumns.map(column => `m.${quoteIdentifier(column)} IS l.${quoteIdentifier(column)}`).join(' AND ')}
1466
+ )`,
1467
+ ).get() as { c?: unknown } | undefined)?.c ?? 0)
1468
+ const run = this.db.prepare(`INSERT OR IGNORE INTO main.${quoteIdentifier(name)} (${columns}) SELECT ${columns} FROM legacy.${quoteIdentifier(name)}`).run() as { changes?: unknown } | undefined
1469
+ const inserted = Number(run?.changes ?? 0)
1381
1470
  const copied = Number((this.db.prepare(`SELECT count(*) AS c FROM main.${quoteIdentifier(name)}`).get() as { c?: unknown } | undefined)?.c ?? 0)
1382
1471
  tables[name] = copied
1383
- // 目标里原本可能已经有行(重跑、或者两个来源都写过),所以判据是「不少于」。
1384
- if (copied < expected) problems.push(`${name} 只拷进 ${copied} 行,原有 ${expected} 行`)
1472
+ if (keyColumns.length === 0) {
1473
+ if (inserted < expected) problems.push(`${name} 只拷进 ${inserted} 行(旧文件 ${expected} 行):这张表没有主键,无法逐行核对,旧文件不删`)
1474
+ } else {
1475
+ const unaccounted = expected - already - inserted
1476
+ if (unaccounted > 0) problems.push(`${name} 有 ${unaccounted} 行没能进来(主键之外还有唯一索引 / 非空 / 检查约束挡住的行):旧文件不删`)
1477
+ /**
1478
+ * 纯主键表(例如 `entity_asset(entity_kind, entity_id, ref)`)没有可比的非键列:
1479
+ * 主键相同就是同一行。重跑时报「无法比对内容」是误报,会让旧文件永久留在磁盘上。
1480
+ */
1481
+ if (comparable.length > 0) {
1482
+ const onEq = keyColumns.map(column => `m.${quoteIdentifier(column)} = l.${quoteIdentifier(column)}`).join(' AND ')
1483
+ // `IS NOT` 而不是 `<>`:NULL 与任何值都不相等,用 `<>` 会把「一边是 NULL」漏判。
1484
+ const differs = Number((this.db.prepare(
1485
+ `SELECT COUNT(*) AS c FROM legacy.${quoteIdentifier(name)} l JOIN main.${quoteIdentifier(name)} m ON ${onEq}
1486
+ WHERE ${comparable.map(column => `m.${quoteIdentifier(column)} IS NOT l.${quoteIdentifier(column)}`).join(' OR ')}`,
1487
+ ).get() as { c?: unknown } | undefined)?.c ?? 0)
1488
+ if (differs > 0) problems.push(`${name} 有 ${differs} 行与目标库主键相同但内容不同:旧文件不删`)
1489
+ }
1490
+ }
1385
1491
  } catch (cause) {
1386
1492
  problems.push(`${name} 的行没拷成:${cause instanceof Error ? cause.message : String(cause)}`)
1387
1493
  }
@@ -525,7 +525,7 @@ export async function exportKnowledgeTree(input: {
525
525
  for (const item of db.sources()) {
526
526
  if (only !== undefined && item.record.sourceId !== only) continue
527
527
  const path = join(targetRoot, LEGACY_SOURCE, sourceName(item.record.title, item.record.sourceId))
528
- if (await write(path, serialize(item.frontmatter, item.record.content))) sources += 1
528
+ if (await write(path, serialize({ ...item.frontmatter, source_id: item.record.sourceId }, item.record.content))) sources += 1
529
529
  }
530
530
  for (const item of db.cards()) {
531
531
  const path = join(targetRoot, LEGACY_WIKI, item.card.entityType, cardName(item.card.entityId))
@@ -0,0 +1,142 @@
1
+ /**
2
+ * 三段轨迹的**传输形状**(抽取 / 归一 / 物化)。
3
+ *
4
+ * 抽取那段本来就有报告(`TrialReport`,见 `wiki-trial-types.ts`),形状没动。这一份只补
5
+ * 新分开的两段 —— 它们此前**没有轨迹**:只有任务里一段流式输出和一张结果表,跑完就查不到
6
+ * 「这条知识是怎么从模型给的名字变成库里的坐标的」「这张卡是按什么排出来的」。
7
+ *
8
+ * ## 为什么这些类型住在浏览器安全的一层
9
+ *
10
+ * 服务端按这个形状写,客户端按同一个形状渲染。写两份就会漂。它们不 import 任何
11
+ * `node:*`,所以可以直接被两边引用 —— 这与 `pipeline/types.ts` 是同一条规矩。
12
+ * (`client/wiki-trial.ts` 里那份手写镜像之所以必须手写,是因为它需要引用的服务端模块
13
+ * import 了 `node:fs`;这里没有那个问题,所以不重复。)
14
+ *
15
+ * ## 一份轨迹对应「一次工作」,不是一次任务
16
+ *
17
+ * - 抽取:一份来源 × 一次运行。
18
+ * - 归一:一份来源 × 一次运行(复用的那一次不落轨迹 —— 那一次什么都没做)。
19
+ * - 物化:一个卡片分组 × 一次运行。
20
+ *
21
+ * 粒度定在这里,是因为这三件事的失败与复用手忙脚乱的程度不同:一份文档抽取失败不影响
22
+ * 另一份;一张卡物化失败也不该让同批其它卡的轨迹看起来像没跑。
23
+ */
24
+
25
+ /** 轨迹属于哪一段。与 `PipelineStage` 的前三段同形(`all` 不是轨迹)。 */
26
+ export type TrajectoryStage = 'extract' | 'normalize' | 'materialize'
27
+
28
+ /** 三个名字只在这里写一次:页标题、列表徽标、日志都用它。 */
29
+ export const TRAJECTORY_STAGE_LABEL: Record<TrajectoryStage, string> = {
30
+ extract: '抽取轨迹',
31
+ normalize: '归一轨迹',
32
+ materialize: '物化轨迹',
33
+ }
34
+
35
+ /** 挤在行内时用的短名(列表徽标、筛选器)。 */
36
+ export const TRAJECTORY_STAGE_SHORT: Record<TrajectoryStage, string> = {
37
+ extract: '抽取',
38
+ normalize: '归一',
39
+ materialize: '物化',
40
+ }
41
+
42
+ /** 每条轨迹都要有的那段公共头。 */
43
+ export interface TrajectoryHead {
44
+ at: string
45
+ /** 模型接缝当时通不通。抽取以外的两段也可能根本没有模型调用。 */
46
+ model: { ok: boolean; why?: string; provider?: string; model?: string; reasoningEffort?: string }
47
+ warnings: string[]
48
+ }
49
+
50
+ /** 坐标三个分量在轨迹里只作为**展示文本**出现(真源是轴注册表的 id)。 */
51
+ export type TrajectoryAxisKind = 'object' | 'action' | 'scene'
52
+
53
+ /**
54
+ * 归一的坐标解析结论。
55
+ *
56
+ * `outcome` 的取值与 `TrialAxis.outcome` 同源:命中已有实体 / 新建候选 / 不唯一没猜 /
57
+ * 只做了文档内归一(`no-registry`)。
58
+ */
59
+ export interface NormalizeAxisRow {
60
+ kind: TrajectoryAxisKind
61
+ /** 文档里的写法。 */
62
+ name: string
63
+ outcome: 'matched' | 'new' | 'ambiguous' | 'no-registry' | string
64
+ /** 落到的实体 id(新建的是候选 id)。歧义时没有。 */
65
+ id?: string
66
+ /** 判成这个结果的理由 / 途径,模型或规则给的。 */
67
+ how?: string
68
+ aliases: string[]
69
+ parentMention?: string
70
+ }
71
+
72
+ /** 一条写进库的 Knowledge 在轨迹里的投影。 */
73
+ export interface NormalizeFactRow {
74
+ text: string
75
+ /** 「对象 · 动作 · 场景」的展示文本。 */
76
+ coordinate: string
77
+ /** 几条出处。 */
78
+ sources: number
79
+ /** 坐标仍有歧义:事实与出处都留着,但暂不进任何卡片分组。 */
80
+ unresolved: boolean
81
+ }
82
+
83
+ /** 归一轨迹:一份来源的 mention 名字 → 库内坐标,以及随之写入的事实。 */
84
+ export interface NormalizeTrajectoryReport extends TrajectoryHead {
85
+ stage: 'normalize'
86
+ source: { id: string; title: string; version: number }
87
+ /** 依据哪一条抽取记录做的归一。 */
88
+ extractionId: string
89
+ /** 归一之后存下的指纹(下一份文档归一能不能复用,看它)。 */
90
+ fingerprint: string
91
+ /** 模型这次给了多少个坐标提及。 */
92
+ mentions: number
93
+ /**
94
+ * 解析结论的分布。`mechanical` = 这一轮没接注册表,只做了文档内归一
95
+ * (三个分量各自计数,所以它是「多少条提及只做了机械归一」而不是一个布尔)。
96
+ */
97
+ counts: { matched: number; created: number; ambiguous: number; mechanical: number }
98
+ axes: NormalizeAxisRow[]
99
+ knowledge: { saved: number; merged: number; skipped: number; failures: number; unresolved: number }
100
+ events: { saved: number; merged: number; skipped: number }
101
+ rows: NormalizeFactRow[]
102
+ /** 已有坐标有没有下发进提示词,以及发了多少(0068)。 */
103
+ axesContext?: { injected: number; totals: number; truncated: number; selection?: string }
104
+ }
105
+
106
+ /** 物化轨迹里的一节。 */
107
+ export interface MaterializeSectionRow {
108
+ label: string
109
+ count: number
110
+ }
111
+
112
+ /** 物化轨迹:一个卡片分组 → 一张卡,以及它是怎么排出来的。 */
113
+ export interface MaterializeTrajectoryReport extends TrajectoryHead {
114
+ stage: 'materialize'
115
+ group: { key: string; title: string; coordinate: string; state: string; fingerprint: string }
116
+ action: 'created' | 'updated' | 'withdrawn'
117
+ card: {
118
+ id: string
119
+ title: string
120
+ status: string
121
+ /** 正文规模,字符数。 */
122
+ characters: number
123
+ /** 进卡的知识条数 / 事件数 / 图数。 */
124
+ knowledge: number
125
+ events: number
126
+ images: number
127
+ sections: MaterializeSectionRow[]
128
+ }
129
+ /**
130
+ * 卡片组织用了没有。
131
+ *
132
+ * `used: false` = 这一库没开卡片组织(`cardOrganize` 关着),正文是按坐标机械拼的;
133
+ * 关着不是失败,但两件事的正文长得不一样,报告必须分得清。
134
+ */
135
+ organize: { used: boolean; origin?: 'model' | 'fallback'; layoutHash?: string }
136
+ /** 进卡的每一条知识(带出处数),让人对着原文核。 */
137
+ rows: Array<{ id: string; text: string; sources: number }>
138
+ /** 这一组的来源文档。 */
139
+ sources: string[]
140
+ /** 正文里可以指向的其它卡片,以及为什么(`coordinate` / `relation` / `source`)。 */
141
+ related: Array<{ title: string; reason: string }>
142
+ }
package/src/types.ts CHANGED
@@ -7,10 +7,8 @@ export type KnowledgeStatus = CardStatus
7
7
  /**
8
8
  * 知识卡片的 entity_type 白名单。
9
9
  *
10
- * **智能归因的五类也在这里**,这不是放宽而是纠错:排查思路本来就应该是一张 wiki 卡片
11
- * (0021 §6.4 定了五类实体与三段式卡片),但白名单里没有它们的时候,`writeCard` 会直接
12
- * 返回 `SCHEMA_TYPE_INVALID`,`readCards` 也会跳过 —— 于是排查思路**永远进不了 wikiNotes,
13
- * 也就永远召不回来**。列表本身在 `diagnosis/schema.ts` 里定义一次,这里只做联合。
10
+ * 诊断卡片类型留在联合里供历史数据兼容。当前排查记录落 SourceRaw,Case 是只读投影;
11
+ * 新运行不会创建 `diagnosis/case` WikiCard。列表本身在 `diagnosis/schema.ts` 里定义一次。
14
12
  */
15
13
  export type SchemaType =
16
14
  | 'concept' | 'metric' | 'table' | 'event' | 'playbook' | 'case'
@@ -28,6 +26,7 @@ export type RerankProvider = 'bert-python' | 'none'
28
26
  */
29
27
  export type CompileModelMode = 'default' | 'fixed'
30
28
  export interface CompileModelSelection {
29
+ reasoningEffort?: string
31
30
  mode: CompileModelMode
32
31
  provider?: string
33
32
  model?: string
@@ -167,6 +167,34 @@ export function cardKey(card: { entityType: string; entityId: string }): string
167
167
  * 弱的那一类是有用的:知识库刚建起来时,关系一条都没有,而「这几张卡来自同一篇文章」
168
168
  * 恰好是人最需要的导航。
169
169
  */
170
+ /**
171
+ * 把「按**卡片身份**」的相关推荐计划,映射到「按**分组键**」(0070)。
172
+ *
173
+ * 两个键空间不同:`planRelated` 的键是 `type:entityId`,而物化阶段的分组键是坐标
174
+ * (`objectId\u0001actionId\u0001sceneId`)。不映射会出现一个**静默的**退化:
175
+ * `organizeCard` 拿不到「可引用的邻居」,而模型一旦照样写出 `[[名字]]`,校验会判
176
+ * `ORGANIZE_UNKNOWN_REFERENCE`,整组物化失败、卡片永远写不出来。
177
+ *
178
+ * 只映射**已经有卡片**的分组:新建的卡片在相关计划里本来就不存在(与旧的
179
+ * `relatedForCard(existing)` 行为一致)。键用卡片自己的类型,也与旧路径一致。
180
+ */
181
+ /** `T` 是相关推荐条目的形状:调用方按自己的投影传进来,这里只管键的映射。 */
182
+ export function relatedByGroupKey<T>(
183
+ groups: ReadonlyArray<{ key: string; cardId?: string }>,
184
+ cardKeyOf: (groupKey: string) => string | undefined,
185
+ plan: ReadonlyMap<string, T[]>,
186
+ ): Map<string, T[]> {
187
+ const mapped = new Map<string, T[]>()
188
+ for (const group of groups) {
189
+ if (group.cardId === undefined) continue
190
+ const key = cardKeyOf(group.key)
191
+ if (key === undefined) continue
192
+ const entries = plan.get(key)
193
+ if (entries !== undefined) mapped.set(group.key, entries)
194
+ }
195
+ return mapped
196
+ }
197
+
170
198
  export function planRelated(
171
199
  cards: Array<Pick<KnowledgeCard, 'entityType' | 'entityId' | 'title' | 'sourceRefs'>>,
172
200
  relations: Array<{ fromKey: string; toKey: string }>,
@@ -153,21 +153,10 @@ export function candidatesFromReport(input: {
153
153
  ? input.eventIdsByCoordinate?.[coordinateKey]
154
154
  ?? card.events.map(id => input.eventIds?.[id]).filter((id): id is string => Boolean(id))
155
155
  : []
156
- if (items.length === 0 && eventIds.length === 0) continue
157
- const localEvents = completeCoordinate
158
- ? report.events.filter(event => card.events.includes(event.id))
159
- : []
160
- const eventSection = localEvents.length > 0
161
- ? `## 关联事件\n\n${localEvents.map(event => `- ${event.title}${event.summary ? `:${event.summary}` : ''}`).join('\n')}`
162
- : ''
156
+ if (items.length === 0) continue
163
157
  const knowledgeBody = renderFallbackBody(card.placements, byId)
164
- const body = [knowledgeBody, eventSection].filter(Boolean).join('\n\n')
165
- const eventEvidence = localEvents.flatMap(event => [
166
- ...event.sources,
167
- ...event.timeline.flatMap(step => step.sources),
168
- ]).find(source => source.found)
158
+ const body = knowledgeBody
169
159
  const evidence = evidenceOf(items, sourceId)
170
- if (evidence.quote === undefined && eventEvidence) evidence.quote = eventEvidence.quote
171
160
 
172
161
  cards.push({
173
162
  candidate: {
@@ -178,7 +167,7 @@ export function candidatesFromReport(input: {
178
167
  aliases: [...new Set([legacyCardNameFor(card.object, card.action, card.scene)])]
179
168
  .filter(alias => alias !== card.name),
180
169
  body,
181
- ...(items[0] ? { summary: items[0].text } : localEvents[0] ? { summary: localEvents[0].summary || localEvents[0].title } : {}),
170
+ ...(items[0] ? { summary: items[0].text } : {}),
182
171
  /**
183
172
  * 证据取这条卡里**第一条核对得上的引文**。
184
173
  *
@@ -338,6 +338,9 @@ export interface TrialReport {
338
338
  */
339
339
  model: {
340
340
  ok: boolean
341
+ /** Structural decoding succeeded, including a valid empty extraction. */
342
+ parsed?: boolean
343
+ outputFormat?: 'json' | 'table'
341
344
  why?: string
342
345
  /** 解析失败时的短摘录,保留旧界面的错误提示。 */
343
346
  raw?: string