fuli-context 0.5.0 → 0.7.2

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 (206) hide show
  1. package/README.en.md +2 -333
  2. package/README.md +370 -202
  3. package/README.zh-CN.md +448 -0
  4. package/compose.graphiti.yml +6 -6
  5. package/dist/web/assets/AboutPage-DXh7Fwxt.css +1 -0
  6. package/dist/web/assets/AboutPage-DfnZo7Lo.js +1 -0
  7. package/dist/web/assets/ConnectionsPage-BloaSKmt.js +1 -0
  8. package/dist/web/assets/ConnectionsPage-CBHAdZXT.css +1 -0
  9. package/dist/web/assets/KnowledgeInspector-Bjhdu2mf.css +1 -0
  10. package/dist/web/assets/KnowledgeInspector-QvNpv0_k.js +1 -0
  11. package/dist/web/assets/KnowledgeOrganizerPage-Cx1w1jb2.css +1 -0
  12. package/dist/web/assets/KnowledgeOrganizerPage-DKq1jVD5.js +1 -0
  13. package/dist/web/assets/KnowledgePage-BWL0jT01.js +1 -0
  14. package/dist/web/assets/KnowledgeWorkspace-Czq6aZ7b.js +4 -0
  15. package/dist/web/assets/OverviewPage-5sJoO1Gh.js +1 -0
  16. package/dist/web/assets/{PersonalProfilePage-S8ZcG2LU.css → PersonalProfilePage-CdmEzyvZ.css} +1 -1
  17. package/dist/web/assets/PersonalProfilePage-tVChM9ik.js +1 -0
  18. package/dist/web/assets/PersonalProjectsPage-BPFrup4V.js +1 -0
  19. package/dist/web/assets/PublicProjectsPage-BPJSeREN.js +1 -0
  20. package/dist/web/assets/ReviewPage-B7EN817i.js +1 -0
  21. package/dist/web/assets/SearchableSelect-ClLN0rIb.js +1 -0
  22. package/dist/web/assets/SettingsPage-C6ouU0sR.css +1 -0
  23. package/dist/web/assets/SettingsPage-Chd_B3eH.js +1 -0
  24. package/dist/web/assets/_plugin-vue_export-helper-BDNMzG2s.js +1 -0
  25. package/dist/web/assets/console-CbDsKrsO.js +1 -0
  26. package/dist/web/assets/fuli-logo-Bu9WBuQe.js +1 -0
  27. package/dist/web/assets/fuli-logo-ER1wQ8Kk.png +0 -0
  28. package/dist/web/assets/i18n-BQcns1PH.js +5 -0
  29. package/dist/web/assets/identity-B8K4VYg6.js +1 -0
  30. package/dist/web/assets/index-CB8Xmsc2.js +2 -0
  31. package/dist/web/assets/index-CZeuPh-U.css +1 -0
  32. package/dist/web/assets/model-eF52GgPF.js +1 -0
  33. package/dist/web/assets/paths-hwz-Ac1g.js +1 -0
  34. package/dist/web/assets/useMinimumLoadingDisplay-BpCyBeko.css +1 -0
  35. package/dist/web/assets/useMinimumLoadingDisplay-wCgGDmHg.js +1 -0
  36. package/dist/web/assets/vue-router-BsEp_XwI.js +1 -0
  37. package/dist/web/index.html +9 -6
  38. package/docs/external-knowledge-architecture.md +279 -0
  39. package/examples/external-knowledge/markdown-folder.mjs +235 -0
  40. package/graph-provider/fuli_graph/app.py +57 -1
  41. package/graph-provider/fuli_graph/graph_models.py +3 -0
  42. package/graph-provider/fuli_graph/graph_query.py +62 -16
  43. package/graph-provider/fuli_graph/knowledge_review.py +612 -0
  44. package/graph-provider/fuli_graph/knowledge_review_models.py +127 -0
  45. package/graph-provider/fuli_graph/knowledge_search.py +51 -4
  46. package/graph-provider/fuli_graph/models.py +19 -0
  47. package/graph-provider/fuli_graph/runtime.py +15 -0
  48. package/graph-provider/fuli_graph/store_knowledge.py +2 -1
  49. package/graph-provider/pyproject.toml +1 -1
  50. package/npm-shrinkwrap.json +5 -407
  51. package/package.json +7 -5
  52. package/skills/capturing-session-knowledge/SKILL.md +24 -4
  53. package/skills/flreview/SKILL.md +73 -0
  54. package/skills/flreview/agents/openai.yaml +6 -0
  55. package/src/agent-tools/graph-definitions.js +79 -4
  56. package/src/agent-tools/graph-handlers.js +12 -0
  57. package/src/cli/command-registry.js +3 -3
  58. package/src/cli/local-runtime-command.js +51 -28
  59. package/src/cli/local-runtime-options.js +14 -8
  60. package/src/cli/setup-agent-selection.js +12 -12
  61. package/src/cli/setup-command.js +41 -18
  62. package/src/cli/shell-argument.js +10 -0
  63. package/src/cli/uninstall-command.js +17 -15
  64. package/src/cli/update-command.js +38 -30
  65. package/src/external-knowledge/catalog.js +61 -0
  66. package/src/external-knowledge/conflict-policy.js +58 -0
  67. package/src/external-knowledge/connected-search.js +166 -0
  68. package/src/external-knowledge/connectors/custom.js +133 -0
  69. package/src/external-knowledge/connectors/feishu.js +461 -0
  70. package/src/external-knowledge/connectors/mcp.js +438 -0
  71. package/src/external-knowledge/connectors/notion.js +307 -0
  72. package/src/external-knowledge/connectors/retrieval-api.js +222 -0
  73. package/src/external-knowledge/document-mapping.js +151 -0
  74. package/src/external-knowledge/graph-projection.js +17 -0
  75. package/src/external-knowledge/index.js +8 -0
  76. package/src/external-knowledge/registry.js +91 -0
  77. package/src/external-knowledge/runtime.js +68 -0
  78. package/src/external-knowledge/safe-diagnostic.js +14 -0
  79. package/src/external-knowledge/service.js +811 -0
  80. package/src/graphiti/agent-knowledge-workflows.js +7 -2
  81. package/src/graphiti/federated-application.js +66 -41
  82. package/src/graphiti/knowledge-item-role.js +21 -0
  83. package/src/graphiti/knowledge-review.js +43 -0
  84. package/src/graphiti/project-profile-mapping.js +32 -0
  85. package/src/graphiti/provider-client.js +22 -1
  86. package/src/graphiti/source-marker.js +54 -3
  87. package/src/graphiti/task-knowledge-recall.js +250 -0
  88. package/src/http/api-router.js +23 -108
  89. package/src/http/external-knowledge-api-router.js +104 -0
  90. package/src/http/graph-api-router.js +12 -1
  91. package/src/http/request-policy.js +1 -1
  92. package/src/local-runtime/lifecycle.js +26 -15
  93. package/src/mcp/create-mcp-server.js +1 -0
  94. package/src/mcp/instructions.js +7 -7
  95. package/src/mcp/tool-annotations.js +6 -0
  96. package/src/mcp/tool-result.js +1 -0
  97. package/src/server/application-lifecycle.js +2 -11
  98. package/src/server/blocked-ports.js +11 -0
  99. package/src/server.js +36 -17
  100. package/src/setup/agent-installation-status.js +2 -1
  101. package/src/setup/agents.js +2 -1
  102. package/src/setup/claude-code-config.js +2 -1
  103. package/src/setup/codex-bootstrap.js +12 -7
  104. package/src/setup/container-runtime.js +25 -21
  105. package/src/setup/graph-runtime.js +148 -56
  106. package/src/setup/node-runtime.js +3 -3
  107. package/src/setup/options.js +2 -4
  108. package/src/setup/paths.js +18 -3
  109. package/src/setup/review-skill-adapters.js +49 -0
  110. package/src/setup/setup.js +26 -2
  111. package/src/setup/uninstall.js +4 -2
  112. package/src/system/resource-monitor.js +317 -0
  113. package/src/system/runtime-settings.js +131 -0
  114. package/src/system/system-service.js +119 -0
  115. package/dist/web/assets/ConnectionsPage-CCKWm-6p.js +0 -1
  116. package/dist/web/assets/KnowledgeInspector-BZkB2wJn.js +0 -1
  117. package/dist/web/assets/KnowledgeInspector-CrGYxEob.css +0 -1
  118. package/dist/web/assets/KnowledgeOrganizerPage-CZAF_OlN.css +0 -1
  119. package/dist/web/assets/KnowledgeOrganizerPage-DlvcLwlL.js +0 -1
  120. package/dist/web/assets/KnowledgePage-CNQXd15y.js +0 -1
  121. package/dist/web/assets/KnowledgeWorkspace-CXPE4GCD.js +0 -4
  122. package/dist/web/assets/OverviewPage-ul7g7HXh.js +0 -1
  123. package/dist/web/assets/PersonalProfilePage-CC40Pw2L.js +0 -1
  124. package/dist/web/assets/PersonalProjectsPage-CPIEcShD.js +0 -1
  125. package/dist/web/assets/PublicProjectsPage-C700sR7z.js +0 -1
  126. package/dist/web/assets/ReviewPage-7MDDUDgA.js +0 -1
  127. package/dist/web/assets/console-B_Ena2DS.js +0 -5
  128. package/dist/web/assets/fuli-logo-DGuyqchQ.png +0 -0
  129. package/dist/web/assets/identity-CjBwYSMX.js +0 -1
  130. package/dist/web/assets/index-CpLDx3of.css +0 -1
  131. package/dist/web/assets/index-DAzeFJSc.js +0 -2
  132. package/dist/web/assets/model-C-5RVIJ2.js +0 -1
  133. package/dist/web/assets/paths-BkULqmGI.js +0 -1
  134. package/dist/web/assets/vue-router-DnD3hUm7.js +0 -1
  135. package/src/agent/candidate-read-service.js +0 -53
  136. package/src/agent/context-pack-service.js +0 -83
  137. package/src/agent/public-read-policy.js +0 -39
  138. package/src/agent/public-read-service.js +0 -69
  139. package/src/agent/read-projection.js +0 -107
  140. package/src/agent/search-service.js +0 -61
  141. package/src/agent/space-boundary.js +0 -33
  142. package/src/app/bootstrap.js +0 -8
  143. package/src/app/create-application.js +0 -143
  144. package/src/app/state-service.js +0 -38
  145. package/src/candidates.js +0 -78
  146. package/src/classifier.js +0 -63
  147. package/src/cli/command-arguments.js +0 -14
  148. package/src/context-pack.js +0 -206
  149. package/src/extractor.js +0 -77
  150. package/src/fact-history.js +0 -39
  151. package/src/facts/fact-service.js +0 -89
  152. package/src/http/bounded-json-array.js +0 -19
  153. package/src/http/lens-route.js +0 -50
  154. package/src/ingestion.js +0 -161
  155. package/src/lens/lens-correction-service.js +0 -81
  156. package/src/lens/lens-fact-writer.js +0 -85
  157. package/src/lens/lens-input.js +0 -62
  158. package/src/lens/lens-query-projection.js +0 -87
  159. package/src/lens/lens-query-ranking.js +0 -62
  160. package/src/lens/lens-query.js +0 -143
  161. package/src/lens/lens-resource-projection.js +0 -93
  162. package/src/lens/lens-resource-selection.js +0 -45
  163. package/src/lens/lens-resource-service.js +0 -103
  164. package/src/lens/lens-search-query.js +0 -122
  165. package/src/lens/lens-service.js +0 -183
  166. package/src/models.js +0 -44
  167. package/src/observer.js +0 -83
  168. package/src/project-rules.js +0 -33
  169. package/src/publication/outbox-service.js +0 -46
  170. package/src/publication/publication-envelope-schema.js +0 -75
  171. package/src/publication/publication-envelope.js +0 -37
  172. package/src/publication/publication-policy.js +0 -82
  173. package/src/publication/publication-service.js +0 -48
  174. package/src/router.js +0 -58
  175. package/src/runtime-options.js +0 -138
  176. package/src/security/episode-visibility.js +0 -14
  177. package/src/security/fact-visibility.js +0 -15
  178. package/src/security/source-metadata-policy.js +0 -8
  179. package/src/storage/episode-evidence-preview.js +0 -100
  180. package/src/storage/fact-search-order.js +0 -18
  181. package/src/storage/file/episode-evidence.js +0 -37
  182. package/src/storage/file/episode-index.js +0 -59
  183. package/src/storage/file/fact-search.js +0 -60
  184. package/src/storage/file-store.js +0 -437
  185. package/src/storage/migrate.js +0 -128
  186. package/src/storage/migrations/001-initial.sql +0 -83
  187. package/src/storage/migrations/002-storage-integrity.sql +0 -113
  188. package/src/storage/record-validation.js +0 -46
  189. package/src/storage/snapshot-defaults.js +0 -54
  190. package/src/storage/sqlite/candidates-repository.js +0 -84
  191. package/src/storage/sqlite/episode-evidence-repository.js +0 -154
  192. package/src/storage/sqlite/episode-privacy-sql.js +0 -33
  193. package/src/storage/sqlite/episodes-repository.js +0 -52
  194. package/src/storage/sqlite/fact-search-repository.js +0 -162
  195. package/src/storage/sqlite/facts-repository.js +0 -146
  196. package/src/storage/sqlite/imports-repository.js +0 -39
  197. package/src/storage/sqlite/mapper.js +0 -84
  198. package/src/storage/sqlite/open-database.js +0 -15
  199. package/src/storage/sqlite/outbox-repository.js +0 -103
  200. package/src/storage/sqlite/snapshot-repository.js +0 -69
  201. package/src/storage/sqlite/spaces-repository.js +0 -52
  202. package/src/storage/sqlite/subscriptions-repository.js +0 -54
  203. package/src/storage/sqlite-store.js +0 -159
  204. package/src/storage/stable-json.js +0 -24
  205. package/src/storage/store-port.js +0 -20
  206. package/src/store.js +0 -2
@@ -1,437 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
-
3
- import { CandidateStatus, FactStatus, isCurrentFact, nowIso } from '../models.js';
4
- import { FileEpisodeIndex } from './file/episode-index.js';
5
- import {
6
- fileCorrectionEpisodeEvidencePreviews,
7
- fileEpisodeEvidencePreview
8
- } from './file/episode-evidence.js';
9
- import { searchFileFacts, searchFileFactsPage } from './file/fact-search.js';
10
- import { readJsonFile, writeJsonFileAtomic } from './json-file.js';
11
- import {
12
- validateCandidateRecord,
13
- validateFactRecord,
14
- validateOutboxRecord
15
- } from './record-validation.js';
16
- import { normalizeSnapshot } from './snapshot-defaults.js';
17
-
18
- export class FileStore {
19
- #committedData;
20
- #episodeIndex;
21
-
22
- constructor(filePath, { jsonFileIo } = {}) {
23
- this.filePath = filePath;
24
- this.memoryOnly = filePath === ':memory:';
25
- this.jsonFileIo = jsonFileIo;
26
- this.transactionActive = false;
27
- this.transactionDirty = false;
28
- this.data = this.memoryOnly ? normalizeSnapshot() : this.#load();
29
- this.#committedData = cloneData(this.data);
30
- this.#episodeIndex = new FileEpisodeIndex(this.data.episodes);
31
- }
32
-
33
- transaction(fn, _options = {}) {
34
- if (this.transactionActive) {
35
- throw new TypeError('Nested FileStore transactions are not supported');
36
- }
37
-
38
- if (fn.constructor?.name === 'AsyncFunction') {
39
- throw new TypeError('Transaction callback must be synchronous');
40
- }
41
-
42
- const snapshot = cloneData(this.data);
43
- this.transactionActive = true;
44
- this.transactionDirty = false;
45
- try {
46
- const result = fn(this);
47
- if (isThenable(result)) {
48
- if (typeof result.catch === 'function') result.catch(() => {});
49
- throw new TypeError('Transaction callback must be synchronous');
50
- }
51
-
52
- const dirty = this.transactionDirty;
53
- this.transactionActive = false;
54
- this.transactionDirty = false;
55
- if (dirty) this.#commit();
56
- return result;
57
- } catch (error) {
58
- this.data = snapshot;
59
- this.#episodeIndex.rebuild(this.data.episodes);
60
- this.transactionActive = false;
61
- this.transactionDirty = false;
62
- throw error;
63
- }
64
- }
65
-
66
- createSpace(name, kind, description = null) {
67
- const existing = this.findSpaceByName(name);
68
- if (existing) return existing;
69
-
70
- const space = {
71
- id: randomUUID(),
72
- name,
73
- kind,
74
- description,
75
- createdAt: nowIso()
76
- };
77
- this.data.spaces.push(space);
78
- this.#save();
79
- return cloneData(space);
80
- }
81
-
82
- listSpaces() {
83
- return cloneData(this.data.spaces);
84
- }
85
-
86
- findSpaceByName(name) {
87
- return cloneData(this.data.spaces.find((space) => space.name === name) ?? null);
88
- }
89
-
90
- getSpace(id) {
91
- return cloneData(this.data.spaces.find((space) => space.id === id) ?? null);
92
- }
93
-
94
- subscribe(personalSpaceId, spaceId, mode = 'latest') {
95
- const existing = this.data.subscriptions.find(
96
- (subscription) =>
97
- subscription.personalSpaceId === personalSpaceId && subscription.spaceId === spaceId
98
- );
99
- if (existing) return cloneData(existing);
100
-
101
- const subscription = { personalSpaceId, spaceId, mode, createdAt: nowIso() };
102
- this.data.subscriptions.push(subscription);
103
- this.#save();
104
- return cloneData(subscription);
105
- }
106
-
107
- listSubscriptions() {
108
- return cloneData(this.data.subscriptions);
109
- }
110
-
111
- subscriptionsFor(personalSpaceId) {
112
- return cloneData(this.data.subscriptions.filter(
113
- (subscription) => subscription.personalSpaceId === personalSpaceId
114
- ));
115
- }
116
-
117
- addEpisode(spaceId, sourceKind, body, sourceUri = null, metadata = {}) {
118
- const episode = {
119
- id: randomUUID(),
120
- spaceId,
121
- sourceKind,
122
- body,
123
- sourceUri,
124
- metadata: cloneData(metadata),
125
- createdAt: nowIso()
126
- };
127
- this.data.episodes.push(episode);
128
- this.#episodeIndex.add(episode);
129
- this.#save();
130
- return cloneData(episode);
131
- }
132
-
133
- getEpisode(id) {
134
- return cloneData(this.#episodeIndex.get(id));
135
- }
136
-
137
- listEpisodes() {
138
- return cloneData(this.data.episodes);
139
- }
140
-
141
- episodeEvidencePreview(spaceId, episodeId, options = {}) {
142
- return cloneData(fileEpisodeEvidencePreview(
143
- this.#episodeIndex,
144
- spaceId,
145
- episodeId,
146
- options
147
- ));
148
- }
149
-
150
- correctionEpisodeEvidencePreviews(spaceId, factIds, options = {}) {
151
- return cloneData(fileCorrectionEpisodeEvidencePreviews(
152
- this.#episodeIndex,
153
- spaceId,
154
- factIds,
155
- options
156
- ));
157
- }
158
-
159
- addFact(fact) {
160
- if (fact.id && this.data.facts.some((item) => item.id === fact.id)) {
161
- throw new Error(`Duplicate fact id: ${fact.id}`);
162
- }
163
-
164
- const stored = {
165
- id: fact.id || randomUUID(),
166
- spaceId: fact.spaceId,
167
- subject: fact.subject,
168
- predicate: fact.predicate,
169
- object: fact.object,
170
- sourceEpisodeId: fact.sourceEpisodeId,
171
- status: fact.status ?? FactStatus.CONFIRMED,
172
- confidence: fact.confidence ?? 1,
173
- sensitivity: fact.sensitivity ?? 'normal',
174
- scope: fact.scope ?? 'personal',
175
- validAt: fact.validAt ?? nowIso(),
176
- invalidAt: fact.invalidAt ?? null,
177
- replacedByFactId: fact.replacedByFactId ?? null
178
- };
179
- validateFactRecord(stored);
180
- this.data.facts.push(stored);
181
- this.#save();
182
- return cloneData(stored);
183
- }
184
-
185
- getFact(id) {
186
- return cloneData(this.data.facts.find((fact) => fact.id === id) ?? null);
187
- }
188
-
189
- updateFact(id, updates) {
190
- const fact = this.data.facts.find((item) => item.id === id);
191
- if (!fact) {
192
- throw new Error(`Fact not found: ${id}`);
193
- }
194
-
195
- const updated = { ...fact, ...updates, id };
196
- validateFactRecord(updated);
197
- Object.assign(fact, updated);
198
- this.#save();
199
- return cloneData(fact);
200
- }
201
-
202
- invalidateFact(factId, replacedByFactId = null) {
203
- const fact = this.data.facts.find((item) => item.id === factId);
204
- if (!fact || fact.invalidAt) return;
205
-
206
- fact.invalidAt = nowIso();
207
- fact.replacedByFactId = replacedByFactId;
208
- this.#save();
209
- }
210
-
211
- currentFacts(spaceId) {
212
- return cloneData(
213
- this.data.facts.filter((fact) => fact.spaceId === spaceId && isCurrentFact(fact))
214
- );
215
- }
216
-
217
- listFacts(options = {}) {
218
- const includeHistorical = options.includeHistorical ?? false;
219
- return cloneData(
220
- this.data.facts.filter((fact) => includeHistorical || isCurrentFact(fact))
221
- );
222
- }
223
-
224
- timeline(spaceId, subject) {
225
- return cloneData(this.data.facts
226
- .filter((fact) => fact.spaceId === spaceId && fact.subject === subject)
227
- .sort((left, right) => left.validAt.localeCompare(right.validAt)));
228
- }
229
-
230
- searchFacts(spaceIds, query, options = {}) {
231
- return cloneData(searchFileFacts(
232
- this.data.facts,
233
- this.#episodeIndex,
234
- spaceIds,
235
- query,
236
- options
237
- ));
238
- }
239
-
240
- searchFactsPage(spaceIds, query, options = {}) {
241
- return cloneData(searchFileFactsPage(
242
- this.data.facts,
243
- this.#episodeIndex,
244
- spaceIds,
245
- query,
246
- options
247
- ));
248
- }
249
-
250
- addCandidate(candidate) {
251
- if (candidate.id && this.data.candidates.some((item) => item.id === candidate.id)) {
252
- throw new Error(`Duplicate candidate id: ${candidate.id}`);
253
- }
254
-
255
- const status = candidate.status ?? CandidateStatus.PENDING;
256
- const createdAt = candidate.createdAt ?? nowIso();
257
- const stored = {
258
- id: candidate.id || randomUUID(),
259
- personalSpaceId: candidate.personalSpaceId,
260
- targetSpaceId: candidate.targetSpaceId ?? null,
261
- episodeId: candidate.episodeId,
262
- reason: candidate.reason,
263
- status,
264
- createdAt,
265
- decidedAt: candidate.decidedAt ?? (
266
- status === CandidateStatus.PENDING ? null : createdAt
267
- )
268
- };
269
- validateCandidateRecord(stored);
270
- this.data.candidates.push(stored);
271
- this.#save();
272
- return cloneData(stored);
273
- }
274
-
275
- pendingCandidates(personalSpaceId) {
276
- return cloneData(this.data.candidates.filter(
277
- (candidate) =>
278
- candidate.personalSpaceId === personalSpaceId &&
279
- candidate.status === CandidateStatus.PENDING
280
- ));
281
- }
282
-
283
- getCandidate(id) {
284
- return cloneData(this.data.candidates.find((candidate) => candidate.id === id) ?? null);
285
- }
286
-
287
- listCandidates() {
288
- return cloneData(this.data.candidates);
289
- }
290
-
291
- updateCandidateStatus(id, status) {
292
- const candidate = this.data.candidates.find((item) => item.id === id);
293
- if (!candidate) {
294
- throw new Error(`Candidate not found: ${id}`);
295
- }
296
-
297
- const updated = {
298
- ...candidate,
299
- status,
300
- decidedAt: status === CandidateStatus.PENDING ? null : nowIso()
301
- };
302
- validateCandidateRecord(updated);
303
- Object.assign(candidate, updated);
304
- this.#save();
305
- return cloneData(candidate);
306
- }
307
-
308
- enqueueOutbox(entry) {
309
- if (entry.id && this.data.outbox.some((item) => item.id === entry.id)) {
310
- throw new Error(`Duplicate outbox id: ${entry.id}`);
311
- }
312
-
313
- const row = {
314
- id: entry.id || randomUUID(),
315
- kind: entry.kind,
316
- aggregateId: entry.aggregateId,
317
- payload: cloneData(entry.payload),
318
- status: entry.status ?? 'pending',
319
- attempts: entry.attempts ?? 0,
320
- nextAttemptAt: entry.nextAttemptAt ?? null,
321
- createdAt: entry.createdAt ?? nowIso(),
322
- sentAt: entry.sentAt ?? null,
323
- lastError: entry.lastError ?? null
324
- };
325
- validateOutboxRecord(row);
326
- this.data.outbox.push(row);
327
- this.#save();
328
- return cloneData(row);
329
- }
330
-
331
- listPendingOutbox(at = nowIso()) {
332
- return cloneData(this.data.outbox
333
- .filter(
334
- (row) =>
335
- row.status === 'pending' &&
336
- (row.nextAttemptAt === null || row.nextAttemptAt <= at)
337
- )
338
- .sort((left, right) => left.createdAt.localeCompare(right.createdAt)));
339
- }
340
-
341
- markOutboxSent(id, sentAt = nowIso()) {
342
- const row = this.data.outbox.find((item) => item.id === id);
343
- if (!row) {
344
- throw new Error(`Outbox row not found: ${id}`);
345
- }
346
-
347
- row.status = 'sent';
348
- row.nextAttemptAt = null;
349
- row.sentAt = sentAt;
350
- row.lastError = null;
351
- this.#save();
352
- return cloneData(row);
353
- }
354
-
355
- markOutboxFailed(id, error, nextAttemptAt = null) {
356
- const row = this.data.outbox.find((item) => item.id === id);
357
- if (!row) {
358
- throw new Error(`Outbox row not found: ${id}`);
359
- }
360
-
361
- row.status = 'pending';
362
- row.attempts += 1;
363
- row.nextAttemptAt = nextAttemptAt;
364
- row.lastError = error;
365
- this.#save();
366
- return cloneData(row);
367
- }
368
-
369
- hasImport(contentHash) {
370
- return this.data.imports.some((record) => record.contentHash === contentHash);
371
- }
372
-
373
- recordImport(record) {
374
- if (this.hasImport(record.contentHash)) {
375
- throw new Error(`Duplicate import content hash: ${record.contentHash}`);
376
- }
377
-
378
- const stored = cloneData(record);
379
- this.data.imports.push(stored);
380
- this.#save();
381
- return cloneData(stored);
382
- }
383
-
384
- exportSnapshot() {
385
- return cloneData(this.data);
386
- }
387
-
388
- importSnapshot(snapshot) {
389
- this.data = normalizeSnapshot(snapshot, {
390
- outbox: this.data.outbox,
391
- imports: this.data.imports
392
- });
393
- this.#episodeIndex.rebuild(this.data.episodes);
394
- this.#save();
395
- }
396
-
397
- close() {}
398
-
399
- #load() {
400
- return normalizeSnapshot(readJsonFile(this.filePath, {}));
401
- }
402
-
403
- #save() {
404
- if (this.transactionActive) {
405
- this.transactionDirty = true;
406
- return;
407
- }
408
-
409
- try {
410
- this.#commit();
411
- } catch (error) {
412
- this.data = cloneData(this.#committedData);
413
- this.#episodeIndex.rebuild(this.data.episodes);
414
- throw error;
415
- }
416
- }
417
-
418
- #commit() {
419
- this.#persist();
420
- this.#committedData = cloneData(this.data);
421
- }
422
-
423
- #persist() {
424
- if (this.memoryOnly) return;
425
- writeJsonFileAtomic(this.filePath, this.data, this.jsonFileIo);
426
- }
427
- }
428
-
429
- function cloneData(data) {
430
- return JSON.parse(JSON.stringify(data));
431
- }
432
-
433
- function isThenable(value) {
434
- return value !== null &&
435
- (typeof value === 'object' || typeof value === 'function') &&
436
- typeof value.then === 'function';
437
- }
@@ -1,128 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- import { readFileSync, readdirSync } from 'node:fs';
3
- import { join } from 'node:path';
4
-
5
- import { nowIso } from '../models.js';
6
- import { isFileDatabase } from './sqlite/open-database.js';
7
-
8
- const DEFAULT_MIGRATIONS_DIRECTORY = new URL('./migrations/', import.meta.url);
9
- const CHECKSUM_UPGRADE_MIGRATION = '002-storage-integrity.sql';
10
-
11
- export function configureDatabase(db, filePath) {
12
- db.pragma('foreign_keys = ON');
13
- db.pragma('busy_timeout = 5000');
14
- if (isFileDatabase(filePath)) db.pragma('journal_mode = WAL');
15
- }
16
-
17
- export function runMigrations(
18
- db,
19
- { migrationsDirectory = DEFAULT_MIGRATIONS_DIRECTORY } = {}
20
- ) {
21
- db.pragma('foreign_keys = ON');
22
- const migrations = discoverMigrations(migrationsDirectory);
23
- for (const migration of migrations) {
24
- const migrate = db.transaction(() => {
25
- const state = appliedMigrations(db);
26
- verifyAppliedMigrations(migrations, state.records, {
27
- allowMissingChecksums: hasPendingChecksumUpgrade(migrations, state)
28
- });
29
- if (state.records.has(migration.version)) return;
30
- db.exec(migration.sql);
31
- recordMigration(db, migration);
32
- });
33
- migrate.immediate();
34
- }
35
- verifyAppliedMigrations(migrations, appliedMigrations(db).records);
36
- }
37
-
38
- function discoverMigrations(directory) {
39
- const migrations = readdirSync(directory)
40
- .filter((name) => name.endsWith('.sql'))
41
- .map((name) => {
42
- const match = /^(\d+)-.+\.sql$/.exec(name);
43
- if (!match) throw new Error(`Invalid migration filename: ${name}`);
44
- const sql = readFileSync(migrationPath(directory, name), 'utf8');
45
- return {
46
- version: Number.parseInt(match[1], 10),
47
- name,
48
- sql,
49
- checksum: createHash('sha256').update(sql).digest('hex')
50
- };
51
- })
52
- .sort((left, right) => left.version - right.version || left.name.localeCompare(right.name));
53
-
54
- for (let index = 1; index < migrations.length; index += 1) {
55
- if (migrations[index - 1].version === migrations[index].version) {
56
- throw new Error(`Duplicate migration version: ${migrations[index].version}`);
57
- }
58
- }
59
- return migrations;
60
- }
61
-
62
- function appliedMigrations(db) {
63
- const columns = db.prepare('PRAGMA table_info(schema_migrations)').all();
64
- if (columns.length === 0) return { records: new Map(), supportsChecksums: false };
65
-
66
- const supportsChecksums = columns.some((column) => column.name === 'checksum');
67
- const rows = supportsChecksums
68
- ? db.prepare(`
69
- SELECT version, checksum FROM schema_migrations ORDER BY version
70
- `).all()
71
- : db.prepare(`
72
- SELECT version, NULL AS checksum FROM schema_migrations ORDER BY version
73
- `).all();
74
- return {
75
- records: new Map(rows.map((row) => [row.version, row.checksum])),
76
- supportsChecksums
77
- };
78
- }
79
-
80
- function verifyAppliedMigrations(
81
- migrations,
82
- applied,
83
- { allowMissingChecksums = false } = {}
84
- ) {
85
- const available = new Map(migrations.map((migration) => [migration.version, migration]));
86
- for (const [version, checksum] of applied) {
87
- const migration = available.get(version);
88
- if (!migration) {
89
- throw new Error(
90
- `Applied migration version ${version} is missing from the migration directory`
91
- );
92
- }
93
- if (checksum === null && allowMissingChecksums) continue;
94
- if (checksum === null) {
95
- throw new Error(`Applied migration version ${version} is missing a checksum`);
96
- }
97
- if (migration.checksum !== checksum) {
98
- throw new Error(`Migration checksum mismatch for version ${version}`);
99
- }
100
- }
101
- }
102
-
103
- function hasPendingChecksumUpgrade(migrations, state) {
104
- if (state.supportsChecksums) return false;
105
- const migration = migrations.find(
106
- (candidate) => candidate.name === CHECKSUM_UPGRADE_MIGRATION
107
- );
108
- return migration !== undefined && !state.records.has(migration.version);
109
- }
110
-
111
- function recordMigration(db, migration) {
112
- const supportsChecksums = db.prepare('PRAGMA table_info(schema_migrations)').all()
113
- .some((column) => column.name === 'checksum');
114
- if (supportsChecksums) {
115
- db.prepare(`
116
- INSERT INTO schema_migrations (version, checksum, applied_at)
117
- VALUES (?, ?, ?)
118
- `).run(migration.version, migration.checksum, nowIso());
119
- return;
120
- }
121
- db.prepare(`
122
- INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)
123
- `).run(migration.version, nowIso());
124
- }
125
-
126
- function migrationPath(directory, name) {
127
- return directory instanceof URL ? new URL(name, directory) : join(directory, name);
128
- }
@@ -1,83 +0,0 @@
1
- PRAGMA foreign_keys = ON;
2
-
3
- CREATE TABLE IF NOT EXISTS schema_migrations (
4
- version INTEGER PRIMARY KEY,
5
- applied_at TEXT NOT NULL
6
- );
7
-
8
- CREATE TABLE spaces (
9
- id TEXT PRIMARY KEY,
10
- name TEXT NOT NULL UNIQUE,
11
- kind TEXT NOT NULL CHECK (kind IN ('personal', 'public')),
12
- description TEXT,
13
- created_at TEXT NOT NULL
14
- );
15
-
16
- CREATE TABLE subscriptions (
17
- personal_space_id TEXT NOT NULL REFERENCES spaces(id),
18
- space_id TEXT NOT NULL REFERENCES spaces(id),
19
- mode TEXT NOT NULL DEFAULT 'latest',
20
- created_at TEXT NOT NULL,
21
- PRIMARY KEY (personal_space_id, space_id)
22
- );
23
-
24
- CREATE TABLE episodes (
25
- id TEXT PRIMARY KEY,
26
- space_id TEXT NOT NULL REFERENCES spaces(id),
27
- source_kind TEXT NOT NULL,
28
- body TEXT NOT NULL,
29
- source_uri TEXT,
30
- metadata_json TEXT NOT NULL DEFAULT '{}',
31
- created_at TEXT NOT NULL
32
- );
33
-
34
- CREATE TABLE facts (
35
- id TEXT PRIMARY KEY,
36
- space_id TEXT NOT NULL REFERENCES spaces(id),
37
- subject TEXT NOT NULL,
38
- predicate TEXT NOT NULL,
39
- object TEXT NOT NULL,
40
- source_episode_id TEXT NOT NULL REFERENCES episodes(id),
41
- status TEXT NOT NULL,
42
- confidence REAL NOT NULL DEFAULT 1.0,
43
- sensitivity TEXT NOT NULL DEFAULT 'normal',
44
- scope TEXT NOT NULL DEFAULT 'personal',
45
- valid_at TEXT NOT NULL,
46
- invalid_at TEXT,
47
- replaced_by_fact_id TEXT REFERENCES facts(id)
48
- );
49
-
50
- CREATE INDEX facts_current_idx ON facts(space_id, predicate, invalid_at);
51
- CREATE INDEX facts_source_idx ON facts(source_episode_id);
52
-
53
- CREATE TABLE candidates (
54
- id TEXT PRIMARY KEY,
55
- personal_space_id TEXT NOT NULL REFERENCES spaces(id),
56
- target_space_id TEXT REFERENCES spaces(id),
57
- episode_id TEXT NOT NULL REFERENCES episodes(id),
58
- reason TEXT NOT NULL,
59
- status TEXT NOT NULL,
60
- created_at TEXT NOT NULL,
61
- decided_at TEXT
62
- );
63
-
64
- CREATE TABLE outbox (
65
- id TEXT PRIMARY KEY,
66
- kind TEXT NOT NULL,
67
- aggregate_id TEXT NOT NULL,
68
- payload_json TEXT NOT NULL,
69
- status TEXT NOT NULL DEFAULT 'pending',
70
- attempts INTEGER NOT NULL DEFAULT 0,
71
- next_attempt_at TEXT,
72
- created_at TEXT NOT NULL,
73
- sent_at TEXT,
74
- last_error TEXT
75
- );
76
-
77
- CREATE INDEX outbox_pending_idx ON outbox(status, next_attempt_at, created_at);
78
-
79
- CREATE TABLE imports (
80
- content_hash TEXT PRIMARY KEY,
81
- source_path TEXT NOT NULL,
82
- imported_at TEXT NOT NULL
83
- );