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
@@ -0,0 +1,126 @@
1
+ import { decode, encode } from '@toon-format/toon'
2
+
3
+ export const EXTRACTION_CONTRACT_VERSION = 'knowledge-table-v1'
4
+ type Row = Record<string, unknown>
5
+ const object = (value: unknown): Row => value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Row : {}
6
+ const list = (value: unknown): Row[] => Array.isArray(value) ? value.map(object) : []
7
+ const string = (value: unknown): string => typeof value === 'string' ? value : ''
8
+
9
+ /** Strict tabular transport. The decoded result retains the existing extraction JSON model. */
10
+ export function decodeExtractionTable(raw: string): Row {
11
+ const record = object(decode(raw, { strict: true }))
12
+ if (record.format !== EXTRACTION_CONTRACT_VERSION) throw new Error(`表格 format 必须为 ${EXTRACTION_CONTRACT_VERSION}`)
13
+ for (const key of ['knowledge', 'events', 'timeline', 'objects', 'actions', 'scenes', 'evidence', 'dropped']) {
14
+ if (!Array.isArray(record[key])) throw new Error(`表格缺少 ${key};没有内容时写 ${key}[0]:`)
15
+ if ((record[key] as unknown[]).some(row => row === null || typeof row !== 'object' || Array.isArray(row))) throw new Error(`${key} 必须由固定列的记录组成`)
16
+ }
17
+ const columns: Record<string, string[]> = {
18
+ knowledge: ['id', 'text', 'object', 'action', 'scene', 'section', 'order', 'evidence'],
19
+ events: ['id', 'external_id', 'title', 'summary', 'object', 'action', 'scene', 'occurred_at', 'symptom', 'root_cause', 'root_cause_status', 'outcome', 'missing', 'evidence'],
20
+ timeline: ['event_id', 'order', 'type', 'description', 'result', 'at', 'evidence'],
21
+ objects: ['mention', 'definition', 'evidence', 'existing', 'parent'],
22
+ actions: ['mention', 'definition', 'evidence', 'existing', 'parent'],
23
+ scenes: ['mention', 'definition', 'evidence', 'existing', 'parent'],
24
+ evidence: ['id', 'quote', 'heading_path'], dropped: ['quote', 'why'],
25
+ }
26
+ for (const [key, fields] of Object.entries(columns)) {
27
+ for (const row of list(record[key])) {
28
+ if (fields.some(field => !(field in row))) throw new Error(`${key} 缺少固定列`)
29
+ for (const field of fields) {
30
+ if (field === 'order') {
31
+ if (!Number.isInteger(row[field]) || Number(row[field]) < 1) throw new Error(`${key}.order 必须为正整数`)
32
+ } else if (row[field] !== null && typeof row[field] !== 'string') throw new Error(`${key}.${field} 必须为字符串或 null`)
33
+ }
34
+ }
35
+ }
36
+ const evidence = new Map<string, Row>()
37
+ for (const row of list(record.evidence)) {
38
+ const id = string(row.id)
39
+ if (!id || evidence.has(id) || !string(row.quote)) throw new Error('出处 ID 重复、为空或缺少 quote')
40
+ let headings: unknown
41
+ try { headings = JSON.parse(string(row.heading_path) || '[]') } catch { throw new Error(`出处 ${id} 的 heading_path 不是字符串数组`) }
42
+ if (!Array.isArray(headings) || headings.some(value => typeof value !== 'string')) throw new Error(`出处 ${id} 的 heading_path 不是字符串数组`)
43
+ evidence.set(id, { quote: row.quote, heading_path: headings })
44
+ }
45
+ const refs = (value: unknown): Row[] => {
46
+ const ids = string(value).trim().split(/\s+/).filter(Boolean)
47
+ return ids.map(id => {
48
+ const source = evidence.get(id)
49
+ if (!source) throw new Error(`引用了不存在的出处 ${id}`)
50
+ return source
51
+ })
52
+ }
53
+ const assertIds = (rows: Row[], label: string): Set<string> => {
54
+ const ids = new Set<string>()
55
+ for (const row of rows) {
56
+ const id = string(row.id)
57
+ if (!id || ids.has(id)) throw new Error(`${label} 的 id 重复或为空`)
58
+ ids.add(id)
59
+ }
60
+ return ids
61
+ }
62
+ const knowledge = list(record.knowledge)
63
+ const events = list(record.events)
64
+ assertIds(knowledge, 'Knowledge')
65
+ const eventIds = assertIds(events, 'Event')
66
+ if (knowledge.some(row => !string(row.text).trim() || !string(row.evidence).trim())) throw new Error('Knowledge 缺少正文或出处')
67
+ const orders = new Set<string>()
68
+ for (const row of list(record.timeline)) {
69
+ if (!eventIds.has(string(row.event_id))) throw new Error('timeline 引用了不存在的 Event')
70
+ const key = `${row.event_id}:${row.order}`
71
+ if (orders.has(key) || !string(row.description).trim() || !['observation', 'check', 'action', 'result', 'conclusion'].includes(string(row.type))) throw new Error('timeline 顺序重复、描述为空或类型不合法')
72
+ orders.add(key)
73
+ }
74
+ for (const row of events) if (!string(row.title).trim() || (row.root_cause_status !== null && !['confirmed', 'suspected'].includes(string(row.root_cause_status)))) throw new Error('Event 标题为空或根因状态不合法')
75
+ return {
76
+ subject: record.subject,
77
+ knowledge: knowledge.map(row => ({ ...row, sources: refs(row.evidence) })),
78
+ events: events.map(row => {
79
+ let missing: unknown
80
+ try { missing = JSON.parse(string(row.missing) || '[]') } catch { throw new Error(`Event ${row.id} 的 missing 不是字符串数组`) }
81
+ if (!Array.isArray(missing) || missing.some(value => typeof value !== 'string')) throw new Error(`Event ${row.id} 的 missing 不是字符串数组`)
82
+ return {
83
+ ...row,
84
+ coordinate: { object_mention: row.object, action_mention: row.action, scene_mention: row.scene },
85
+ root_cause: row.root_cause === null || row.root_cause === '' ? null : { text: row.root_cause, status: row.root_cause_status },
86
+ missing,
87
+ sources: refs(row.evidence),
88
+ timeline: list(record.timeline).filter(step => step.event_id === row.id).sort((a, b) => Number(a.order) - Number(b.order)).map(step => ({ ...step, sources: refs(step.evidence) })),
89
+ }
90
+ }),
91
+ ...Object.fromEntries(['objects', 'actions', 'scenes'].map(key => [key, list(record[key]).map(row => ({ ...row, parent_mention: row.parent, evidence: refs(row.evidence).map(source => source.quote).join('\n') }))])),
92
+ dropped: record.dropped,
93
+ }
94
+ }
95
+
96
+ /** Used by the format benchmark and fixtures; evidence is emitted once and referenced by ID. */
97
+ export function encodeExtractionTable(value: Row): string {
98
+ const evidence: Row[] = []
99
+ const byQuote = new Map<string, string>()
100
+ const references = (sources: unknown): string => list(sources).map(source => {
101
+ const key = JSON.stringify([source.quote, source.heading_path ?? []])
102
+ const existing = byQuote.get(key)
103
+ if (existing) return existing
104
+ const id = `q${evidence.length + 1}`
105
+ evidence.push({ id, quote: source.quote, heading_path: JSON.stringify(source.heading_path ?? []) })
106
+ byQuote.set(key, id)
107
+ return id
108
+ }).join(' ')
109
+ const timeline: Row[] = []
110
+ const table: Row = {
111
+ format: EXTRACTION_CONTRACT_VERSION,
112
+ subject: value.subject ?? '',
113
+ knowledge: list(value.knowledge).map(row => ({ id: row.id, text: row.text, object: row.object_mention ?? row.object ?? null, action: row.action_mention ?? row.action ?? null, scene: row.scene_mention ?? row.scene ?? null, section: row.section ?? '字段', order: row.order ?? 1, evidence: references(row.sources) })),
114
+ events: list(value.events).map(row => {
115
+ list(row.timeline).forEach((step, index) => timeline.push({ event_id: row.id, order: index + 1, type: step.type, description: step.description, result: step.result ?? null, at: step.at ?? null, evidence: references(step.sources) }))
116
+ const coordinate = object(row.coordinate)
117
+ const cause = object(row.root_cause)
118
+ return { id: row.id, external_id: row.external_id ?? null, title: row.title, summary: row.summary, object: coordinate.object_mention ?? null, action: coordinate.action_mention ?? null, scene: coordinate.scene_mention ?? null, occurred_at: row.occurred_at ?? null, symptom: row.symptom ?? null, root_cause: cause.text ?? null, root_cause_status: cause.status ?? null, outcome: row.outcome ?? null, missing: JSON.stringify(row.missing ?? []), evidence: references(row.sources) }
119
+ }),
120
+ timeline,
121
+ ...Object.fromEntries(['objects', 'actions', 'scenes'].map(key => [key, list(value[key]).map(row => ({ mention: row.mention, definition: row.definition ?? '', evidence: row.evidence ? references([{ quote: row.evidence, heading_path: [] }]) : '', existing: row.existing ?? null, parent: row.parent_mention ?? row.parent ?? null }))])),
122
+ evidence,
123
+ dropped: value.dropped ?? [],
124
+ }
125
+ return encode(table)
126
+ }
@@ -1,5 +1,14 @@
1
1
  /**
2
- * 抽取轨迹(`extraction_run`):把每一次「这份文档抽出了什么」记成一条**不可重建**的记录。
2
+ * 轨迹流水(`extraction_run`):把每一次「这一段处理了什么」记成一条**不可重建**的记录。
3
+ *
4
+ * ## 三段,一张表
5
+ *
6
+ * 2026-10-18 起抽取、归一、物化是三个独立阶段,所以轨迹也分三种(`stage` 列):
7
+ * 抽取(一份来源 × 一次)、归一(一份来源 × 一次)、物化(一个卡片分组 × 一次)。
8
+ * **共用一张表**是因为它们的读写形状完全一样:按 `(kb, stage, 时间)` 倒序翻页、
9
+ * 点开读一份 JSON 报告、不可重建。拆三张表只会让「这份文档跑过几次」变成三次查询。
10
+ *
11
+ * 抽取那一份的正文是整份 `TrialReport`;归一与物化的形状见 `trajectory-types.ts`。
3
12
  *
4
13
  * ## 它是什么,不是什么
5
14
  *
@@ -30,12 +39,14 @@
30
39
  import { mkdir } from 'node:fs/promises'
31
40
  import { dirname } from 'node:path'
32
41
  import { loadSqlite, type SqliteDatabase } from './fts.js'
42
+ import { applyRetention } from './retention.js'
43
+ import type { TrajectoryStage } from './trajectory-types.js'
33
44
 
34
45
  /** 一次抽取是谁发起的。 */
35
46
  export type ExtractionRunActor = 'panel' | 'agent'
36
47
 
37
48
  /**
38
- * 一条抽取轨迹的**列表投影**(不含报告本体)。
49
+ * 一条轨迹的**列表投影**(不含报告本体)。
39
50
  *
40
51
  * 列表要回答的是「这份文档跑过几次、什么时候跑的、模型通了没有」,所以这些字段进列;
41
52
  * 报告要等点开某一条时才读。
@@ -56,9 +67,11 @@ export interface ExtractionRunSummary {
56
67
  summary: string
57
68
  /** 报告本体在不在。列表接口不返回它。 */
58
69
  hasReport: boolean
70
+ /** 这条轨迹属于哪一段。旧记录一律是 `extract`(那一列有默认值)。 */
71
+ stage: TrajectoryStage
59
72
  }
60
73
 
61
- /** 一条完整的抽取轨迹:列表投影 + 报告本体。 */
74
+ /** 一条完整的轨迹:列表投影 + 报告本体。 */
62
75
  export interface ExtractionRunRecord extends ExtractionRunSummary {
63
76
  /** 整份报告(`TrialReport` 的形状)。读的时候按 unknown 处理,由前端类型收窄。 */
64
77
  report: unknown
@@ -66,6 +79,8 @@ export interface ExtractionRunRecord extends ExtractionRunSummary {
66
79
 
67
80
  export interface ExtractionRunQuery {
68
81
  docId?: string
82
+ /** 只看某一段。省略 = 三段一起(面板里那份「这份文档的轨迹」就是这么读的)。 */
83
+ stage?: TrajectoryStage
69
84
  limit?: number
70
85
  offset?: number
71
86
  }
@@ -86,19 +101,22 @@ CREATE TABLE IF NOT EXISTS extraction_runs (
86
101
  model_ok INTEGER NOT NULL DEFAULT 0,
87
102
  model_why TEXT,
88
103
  summary TEXT NOT NULL DEFAULT '',
89
- report TEXT NOT NULL DEFAULT '{}'
104
+ report TEXT NOT NULL DEFAULT '{}',
105
+ stage TEXT NOT NULL DEFAULT 'extract'
90
106
  );
91
107
  CREATE INDEX IF NOT EXISTS idx_extraction_runs_doc ON extraction_runs(kb, doc_id, created_at DESC);
92
108
  CREATE INDEX IF NOT EXISTS idx_extraction_runs_time ON extraction_runs(kb, created_at DESC);
93
109
  `
94
110
 
95
111
  /**
96
- * 抽取轨迹库。**一个知识库一份**(随 Runtime 创建),懒打开。
112
+ * 轨迹库。**一个知识库一份**(随 Runtime 创建),懒打开。
97
113
  *
98
114
  * API 与 `OperationLog` 同形(同步的 `DatabaseSync`):本地文件、单写者,同步最简单。
99
115
  */
100
116
  export class ExtractionRunStore {
101
117
  private db: SqliteDatabase | null = null
118
+ /** 距离上一次裁剪又落了几条(`append` 里用)。 */
119
+ private appendsSincePrune = 0
102
120
 
103
121
  constructor(private readonly file: string) {}
104
122
 
@@ -111,6 +129,16 @@ export class ExtractionRunStore {
111
129
  await mkdir(dirname(this.file), { recursive: true })
112
130
  this.db = new sqlite.DatabaseSync(this.file)
113
131
  this.db.exec(SCHEMA)
132
+ /**
133
+ * 旧库补 `stage` 列(2026-10-18 加)。
134
+ *
135
+ * `CREATE TABLE IF NOT EXISTS` 不会给**已存在**的表加列,所以升级必须在真库上跑一遍迁移:
136
+ * 否则读旧库时那条 SELECT 直接报错,历史轨迹一条也打不开。旧记录落到默认值
137
+ * `extract` —— 它们本来就只有抽取这一种。
138
+ */
139
+ const columns = this.db.prepare('PRAGMA table_info(extraction_runs)').all() as Array<{ name: string }>
140
+ if (!columns.some(column => column.name === 'stage')) this.db.exec("ALTER TABLE extraction_runs ADD COLUMN stage TEXT NOT NULL DEFAULT 'extract'")
141
+ this.db.exec('CREATE INDEX IF NOT EXISTS idx_extraction_runs_stage ON extraction_runs(kb, stage, created_at DESC)')
114
142
  return true
115
143
  } catch {
116
144
  this.db = null
@@ -122,15 +150,37 @@ export class ExtractionRunStore {
122
150
  return this.db !== null
123
151
  }
124
152
 
153
+ /** Shared audit connection; callers must not close it. */
154
+ connection(): SqliteDatabase {
155
+ if (!this.db) throw new Error('EXTRACTION_RUNS_UNAVAILABLE')
156
+ return this.db
157
+ }
158
+
125
159
  /** 落一条。报告整份序列化进 `report` 列。 */
126
160
  async append(entry: ExtractionRunRecord): Promise<void> {
127
161
  if (this.db === null) throw new Error('EXTRACTION_RUNS_UNAVAILABLE')
128
162
  this.db.prepare(
129
- 'INSERT INTO extraction_runs (id, kb, doc_id, title, actor, created_at, model_ok, model_why, summary, report) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
163
+ 'INSERT INTO extraction_runs (id, kb, doc_id, title, actor, created_at, model_ok, model_why, summary, report, stage) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
130
164
  ).run(
131
165
  entry.id, entry.kb, entry.docId, entry.title, entry.actor, entry.createdAt,
132
- entry.modelOk ? 1 : 0, entry.modelWhy ?? null, entry.summary, JSON.stringify(entry.report ?? {}),
166
+ entry.modelOk ? 1 : 0, entry.modelWhy ?? null, entry.summary, JSON.stringify(entry.report ?? {}), entry.stage,
133
167
  )
168
+ /**
169
+ * 这张表才是审计库里真正会长大的那张(`report` 是整份模型返回的 JSON,一条几十到几百 KB)。
170
+ * 上限同样只裁「水位以上」的新行 —— 升级那一刻已有的轨迹一条不删(见 `retention.ts`)。
171
+ * 每 25 次落一条才做一次,避免每次写入都去数一遍。
172
+ *
173
+ * **按阶段各留各的窗口**(2026-10-18):三段的条数差一个量级 —— 300 个分组的库跑一次
174
+ * 全链路,物化落 300 条,抽取只落 1 条。用一条全表 FIFO 上限的话,物化会把抽取挤出去,
175
+ * 而抽取那份是三者里最贵、最不可重建的(它是模型的原始返回)。所以每段各自计数。
176
+ */
177
+ this.appendsSincePrune += 1
178
+ if (this.appendsSincePrune >= 25) {
179
+ this.appendsSincePrune = 0
180
+ for (const stage of ['extract', 'normalize', 'materialize'] as const) {
181
+ try { applyRetention(this.db, `extraction_runs:${stage}`, { table: 'extraction_runs', key: 'rowid', keep: 2000, where: `stage = '${stage}'` }) } catch { /* 裁剪是尽力而为 */ }
182
+ }
183
+ }
134
184
  }
135
185
 
136
186
  /** 一条完整记录(含报告)。 */
@@ -146,12 +196,13 @@ export class ExtractionRunStore {
146
196
  const where: string[] = []
147
197
  const params: Record<string, unknown> = {}
148
198
  if (query.docId !== undefined) { where.push('doc_id = :docId'); params.docId = query.docId }
199
+ if (query.stage !== undefined) { where.push('stage = :stage'); params.stage = query.stage }
149
200
  const clause = where.length > 0 ? `WHERE ${where.join(' AND ')}` : ''
150
201
  const total = this.db.prepare(`SELECT COUNT(*) AS n FROM extraction_runs ${clause}`).get(params) as { n: number }
151
202
  const limit = Math.min(100, Math.max(1, query.limit ?? 20))
152
203
  const offset = Math.max(0, query.offset ?? 0)
153
204
  const rows = this.db.prepare(
154
- `SELECT id, kb, doc_id, title, actor, created_at, model_ok, model_why, summary, report <> '{}' AS has_report
205
+ `SELECT id, kb, doc_id, title, actor, created_at, model_ok, model_why, summary, stage, report <> '{}' AS has_report
155
206
  FROM extraction_runs ${clause} ORDER BY created_at DESC, id DESC LIMIT :limit OFFSET :offset`,
156
207
  ).all({ ...params, limit, offset }) as Array<Record<string, unknown>>
157
208
  return { runs: rows.map(rowToSummary), total: total.n }
@@ -197,6 +248,8 @@ function rowToSummary(row: Record<string, unknown>): ExtractionRunSummary {
197
248
  ...(row.model_why === null || row.model_why === undefined ? {} : { modelWhy: String(row.model_why) }),
198
249
  summary: String(row.summary ?? ''),
199
250
  hasReport: row.has_report === 1 || row.has_report === true,
251
+ // 旧库补列之前落下的行没有这一列(或为 null):它们只可能是抽取。
252
+ stage: (row.stage === 'normalize' || row.stage === 'materialize' ? row.stage : 'extract') as TrajectoryStage,
200
253
  }
201
254
  }
202
255
 
@@ -210,7 +263,13 @@ function rowToRecord(row: Record<string, unknown>): ExtractionRunRecord {
210
263
  }
211
264
  }
212
265
 
213
- /** 一条轨迹的 id:时间戳(36 进制)+ 随机尾巴,排序即创建序。 */
214
- export function extractionRunId(): string {
215
- return `run_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`
266
+ /**
267
+ * 一条轨迹的 id:时间戳(36 进制)+ 随机尾巴,排序即创建序。
268
+ *
269
+ * `stage` 只影响前缀,不影响排序:id 自己写着它属于哪一段,日志里一眼分得清;
270
+ * 而**不可依赖**它做判别 —— 判别读 `stage` 列(前缀是给人看的,列才是真源)。
271
+ */
272
+ export function extractionRunId(stage?: TrajectoryStage): string {
273
+ const prefix = stage === undefined ? 'run' : stage
274
+ return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`
216
275
  }
package/src/fs-atomic.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from 'node:crypto'
1
2
  import { mkdir, rename, unlink, writeFile } from 'node:fs/promises'
2
3
  import { dirname } from 'node:path'
3
4
 
@@ -9,11 +10,27 @@ import { dirname } from 'node:path'
9
10
  * `diagnosis/` 反过来依赖 runtime 的实现细节 —— 分层就倒过来了。
10
11
  */
11
12
 
13
+ /** 进程内的单调计数,和随机尾巴一起保证同进程同毫秒的两次写不会用同一个临时文件。 */
14
+ let temporaryCounter = 0
15
+
16
+ /**
17
+ * 临时文件名。
18
+ *
19
+ * 曾经是 `${path}.${pid}.${Date.now().toString(36)}.tmp` —— 同一个进程、同一个目标、
20
+ * 同一毫秒就会撞名。撞上之后先完成的 `rename` 把文件吃掉,后完成的那次 `writeFile`
21
+ * 直接 ENOENT,表现为「整次 persist 抛错、内存改了但磁盘没落」。
22
+ * 计数 + 随机尾巴把这一条从「取决于时序」变成「不可能」。
23
+ */
24
+ function temporaryPath(path: string): string {
25
+ temporaryCounter = (temporaryCounter + 1) % Number.MAX_SAFE_INTEGER
26
+ return `${path}.${process.pid}.${temporaryCounter.toString(36)}.${randomUUID().slice(0, 8)}.tmp`
27
+ }
28
+
12
29
  export async function writeTextAtomic(path: string, content: string): Promise<void> {
13
30
  // 原子写需要一个与目标同目录的临时文件,所以目标目录必须先存在。0030 起
14
31
  // SourceRaw 现在平铺在 `<段>/` 下,确保段目录存在即可。
15
32
  await mkdir(dirname(path), { recursive: true })
16
- const temporary = `${path}.${process.pid}.${Date.now().toString(36)}.tmp`
33
+ const temporary = temporaryPath(path)
17
34
  try {
18
35
  await writeFile(temporary, content, 'utf8')
19
36
  await rename(temporary, path)
@@ -24,7 +41,7 @@ export async function writeTextAtomic(path: string, content: string): Promise<vo
24
41
 
25
42
  export async function writeBufferAtomic(path: string, content: Buffer): Promise<void> {
26
43
  await mkdir(dirname(path), { recursive: true })
27
- const temporary = `${path}.${process.pid}.${Date.now().toString(36)}.tmp`
44
+ const temporary = temporaryPath(path)
28
45
  try {
29
46
  await writeFile(temporary, content)
30
47
  await rename(temporary, path)
package/src/images.ts CHANGED
@@ -4,7 +4,7 @@ import { basename, extname, join } from 'node:path'
4
4
  import { ASSETS_DIR, PUBLISHED_IMAGE_MARKER } from './layout.js'
5
5
  import { writeBufferAtomic, writeTextAtomic } from './fs-atomic.js'
6
6
 
7
- /** Maximum decoded image size accepted by `images.publish`. */
7
+ /** Maximum decoded image size accepted by `image_publish`. */
8
8
  export const MAX_PUBLISHED_IMAGE_BYTES = 20 * 1024 * 1024
9
9
 
10
10
  const IMAGE_FORMATS = [
@@ -187,7 +187,7 @@ export async function publishKnowledgeImage(
187
187
  if (!existing?.equals(bytes)) await writeBufferAtomic(join(assetDir, storedFilename), bytes)
188
188
  // Standalone hosted images have no SourceRaw or card by design. Keep that intent beside the
189
189
  // asset so the orphan sweep does not mistake it for a disposable extraction artifact.
190
- await writeTextAtomic(join(assetDir, PUBLISHED_IMAGE_MARKER), 'images.publish\n')
190
+ await writeTextAtomic(join(assetDir, PUBLISHED_IMAGE_MARKER), 'image_publish\n')
191
191
  const relativePath = `${ASSETS_DIR}/${bucket}/${storedFilename}`
192
192
  const url = `/knowledge/assets/${encodeURIComponent(kb)}/${relativePath.split('/').map(encodeURIComponent).join('/')}`
193
193
  return {