fuli-context 0.2.0

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 (220) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +178 -0
  3. package/compose.graphiti.yml +86 -0
  4. package/dist/web/assets/ConnectionsPage-CY6H_mJJ.js +1 -0
  5. package/dist/web/assets/KnowledgeInspector-BSpz5Sfm.js +1 -0
  6. package/dist/web/assets/KnowledgeInspector-CHbB_0rC.css +1 -0
  7. package/dist/web/assets/KnowledgeOrganizerPage-BlEvhPrv.css +1 -0
  8. package/dist/web/assets/KnowledgeOrganizerPage-CvjGPlUc.js +1 -0
  9. package/dist/web/assets/KnowledgePage-Khuo3cNs.js +1 -0
  10. package/dist/web/assets/KnowledgeWorkspace-DVZusvem.js +4 -0
  11. package/dist/web/assets/OverviewPage-DC4C4Fhn.js +1 -0
  12. package/dist/web/assets/PersonalProfilePage-CSV9Mtgz.js +1 -0
  13. package/dist/web/assets/PersonalProfilePage-CVJ3TcRi.css +1 -0
  14. package/dist/web/assets/PersonalProjectsPage-A50EaseQ.js +1 -0
  15. package/dist/web/assets/PublicProjectsPage-BAdO_bae.js +1 -0
  16. package/dist/web/assets/ReviewPage-Dztk2jDG.js +1 -0
  17. package/dist/web/assets/console-tbvTUi6p.js +1 -0
  18. package/dist/web/assets/fuli-logo-DGuyqchQ.png +0 -0
  19. package/dist/web/assets/identity-B3s5CXhv.js +1 -0
  20. package/dist/web/assets/index-Devf7glZ.css +1 -0
  21. package/dist/web/assets/index-DwUDhQKc.js +6 -0
  22. package/dist/web/assets/model-DVT8sjy9.js +1 -0
  23. package/dist/web/assets/paths-B790iniO.js +1 -0
  24. package/dist/web/assets/runtime-dom.esm-bundler-DttsK21F.js +1 -0
  25. package/dist/web/assets/vue-router-BYTAoWT3.js +1 -0
  26. package/dist/web/index.html +22 -0
  27. package/graph-provider/Dockerfile +16 -0
  28. package/graph-provider/fuli_graph/__init__.py +5 -0
  29. package/graph-provider/fuli_graph/app.py +547 -0
  30. package/graph-provider/fuli_graph/auth.py +17 -0
  31. package/graph-provider/fuli_graph/collaboration_context.py +290 -0
  32. package/graph-provider/fuli_graph/config.py +34 -0
  33. package/graph-provider/fuli_graph/graph_models.py +123 -0
  34. package/graph-provider/fuli_graph/graph_projection.py +405 -0
  35. package/graph-provider/fuli_graph/graph_query.py +561 -0
  36. package/graph-provider/fuli_graph/knowledge_audit.py +765 -0
  37. package/graph-provider/fuli_graph/knowledge_batch_confirmation.py +246 -0
  38. package/graph-provider/fuli_graph/knowledge_management.py +816 -0
  39. package/graph-provider/fuli_graph/knowledge_records.py +65 -0
  40. package/graph-provider/fuli_graph/knowledge_search.py +723 -0
  41. package/graph-provider/fuli_graph/knowledge_usage_models.py +52 -0
  42. package/graph-provider/fuli_graph/model_validation.py +58 -0
  43. package/graph-provider/fuli_graph/models.py +1179 -0
  44. package/graph-provider/fuli_graph/personal_project_access.py +29 -0
  45. package/graph-provider/fuli_graph/preference_conflicts.py +420 -0
  46. package/graph-provider/fuli_graph/project_action_models.py +142 -0
  47. package/graph-provider/fuli_graph/project_knowledge.py +707 -0
  48. package/graph-provider/fuli_graph/project_management.py +251 -0
  49. package/graph-provider/fuli_graph/provider_values.py +40 -0
  50. package/graph-provider/fuli_graph/runtime.py +350 -0
  51. package/graph-provider/fuli_graph/search_projection.py +156 -0
  52. package/graph-provider/fuli_graph/store.py +24 -0
  53. package/graph-provider/fuli_graph/store_identity.py +237 -0
  54. package/graph-provider/fuli_graph/store_knowledge.py +447 -0
  55. package/graph-provider/fuli_graph/store_projects.py +242 -0
  56. package/graph-provider/fuli_graph/store_publication.py +370 -0
  57. package/graph-provider/fuli_graph/store_records.py +134 -0
  58. package/graph-provider/pyproject.toml +35 -0
  59. package/npm-shrinkwrap.json +6045 -0
  60. package/package.json +99 -0
  61. package/skills/capturing-session-knowledge/SKILL.md +226 -0
  62. package/skills/capturing-session-knowledge/agents/openai.yaml +13 -0
  63. package/skills/grilling-project/SKILL.md +129 -0
  64. package/skills/grilling-project/agents/openai.yaml +4 -0
  65. package/skills/grilling-project/references/project-profile.md +105 -0
  66. package/src/agent/candidate-read-service.js +53 -0
  67. package/src/agent/context-pack-service.js +83 -0
  68. package/src/agent/public-read-policy.js +39 -0
  69. package/src/agent/public-read-service.js +69 -0
  70. package/src/agent/read-projection.js +107 -0
  71. package/src/agent/search-service.js +61 -0
  72. package/src/agent/space-boundary.js +33 -0
  73. package/src/agent-tools/graph-definitions.js +479 -0
  74. package/src/agent-tools/graph-handlers.js +96 -0
  75. package/src/agent-tools/schema.js +37 -0
  76. package/src/agent-tools.js +16 -0
  77. package/src/app/application-error.js +13 -0
  78. package/src/app/bootstrap.js +8 -0
  79. package/src/app/create-application.js +143 -0
  80. package/src/app/state-service.js +38 -0
  81. package/src/candidates.js +78 -0
  82. package/src/classifier.js +63 -0
  83. package/src/cli/command-arguments.js +14 -0
  84. package/src/cli/command-registry.js +78 -0
  85. package/src/cli/commands/candidates.js +17 -0
  86. package/src/cli/commands/ingestion.js +32 -0
  87. package/src/cli/commands/reads.js +50 -0
  88. package/src/cli/commands/spaces.js +20 -0
  89. package/src/cli/invocation.js +47 -0
  90. package/src/cli/local-runtime-command.js +94 -0
  91. package/src/cli/local-runtime-options.js +64 -0
  92. package/src/cli/migrate-command.js +71 -0
  93. package/src/cli/migration-destination.js +117 -0
  94. package/src/cli/setup-agent-selection.js +195 -0
  95. package/src/cli/setup-command.js +85 -0
  96. package/src/cli/uninstall-command.js +67 -0
  97. package/src/cli.js +90 -0
  98. package/src/context-pack.js +206 -0
  99. package/src/defaults.js +1 -0
  100. package/src/extractor.js +77 -0
  101. package/src/fact-history.js +39 -0
  102. package/src/facts/fact-service.js +89 -0
  103. package/src/graphiti/agent-access-policy.js +88 -0
  104. package/src/graphiti/capture-policy.js +84 -0
  105. package/src/graphiti/federated-application.js +1292 -0
  106. package/src/graphiti/personal-context.js +29 -0
  107. package/src/graphiti/project-path-context.js +159 -0
  108. package/src/graphiti/provider-client.js +288 -0
  109. package/src/graphiti/runtime-config.js +85 -0
  110. package/src/graphiti/setup.js +43 -0
  111. package/src/graphiti/source-marker.js +142 -0
  112. package/src/http/api-router.js +133 -0
  113. package/src/http/bounded-json-array.js +19 -0
  114. package/src/http/error-mapping.js +36 -0
  115. package/src/http/graph-api-router.js +257 -0
  116. package/src/http/lens-route.js +50 -0
  117. package/src/http/request-policy.js +41 -0
  118. package/src/http/response.js +29 -0
  119. package/src/http/static-handler.js +55 -0
  120. package/src/ingestion.js +161 -0
  121. package/src/lens/lens-correction-service.js +81 -0
  122. package/src/lens/lens-fact-writer.js +85 -0
  123. package/src/lens/lens-input.js +62 -0
  124. package/src/lens/lens-query-projection.js +87 -0
  125. package/src/lens/lens-query-ranking.js +62 -0
  126. package/src/lens/lens-query.js +143 -0
  127. package/src/lens/lens-resource-projection.js +93 -0
  128. package/src/lens/lens-resource-selection.js +45 -0
  129. package/src/lens/lens-resource-service.js +103 -0
  130. package/src/lens/lens-search-query.js +122 -0
  131. package/src/lens/lens-service.js +183 -0
  132. package/src/local-runtime/lifecycle.js +254 -0
  133. package/src/mcp/create-mcp-server.js +85 -0
  134. package/src/mcp/instructions.js +10 -0
  135. package/src/mcp/project-action-preview-tokens.js +72 -0
  136. package/src/mcp/runtime.js +71 -0
  137. package/src/mcp/tool-annotations.js +63 -0
  138. package/src/mcp/tool-result.js +167 -0
  139. package/src/mcp/tool-schema.js +60 -0
  140. package/src/mcp-server.js +76 -0
  141. package/src/models.js +44 -0
  142. package/src/observer.js +83 -0
  143. package/src/package-metadata.js +6 -0
  144. package/src/project-rules.js +33 -0
  145. package/src/publication/canonical-json.js +39 -0
  146. package/src/publication/outbox-service.js +46 -0
  147. package/src/publication/publication-envelope-schema.js +75 -0
  148. package/src/publication/publication-envelope.js +37 -0
  149. package/src/publication/publication-policy.js +82 -0
  150. package/src/publication/publication-service.js +48 -0
  151. package/src/router.js +58 -0
  152. package/src/runtime-options.js +139 -0
  153. package/src/security/episode-visibility.js +14 -0
  154. package/src/security/fact-visibility.js +15 -0
  155. package/src/security/sensitive-content.js +18 -0
  156. package/src/security/source-metadata-policy.js +8 -0
  157. package/src/server/application-lifecycle.js +34 -0
  158. package/src/server/listen.js +24 -0
  159. package/src/server.js +135 -0
  160. package/src/setup/agent-installation-status.js +97 -0
  161. package/src/setup/agents.js +123 -0
  162. package/src/setup/codex-bootstrap.js +118 -0
  163. package/src/setup/codex-config.js +122 -0
  164. package/src/setup/config-backup.js +22 -0
  165. package/src/setup/container-runtime.js +388 -0
  166. package/src/setup/cursor-config.js +61 -0
  167. package/src/setup/graph-runtime.js +360 -0
  168. package/src/setup/node-runtime.js +24 -0
  169. package/src/setup/options.js +91 -0
  170. package/src/setup/paths.js +47 -0
  171. package/src/setup/session-skill.js +91 -0
  172. package/src/setup/setup.js +85 -0
  173. package/src/setup/uninstall.js +90 -0
  174. package/src/storage/episode-evidence-preview.js +100 -0
  175. package/src/storage/fact-search-order.js +18 -0
  176. package/src/storage/file/episode-evidence.js +37 -0
  177. package/src/storage/file/episode-index.js +59 -0
  178. package/src/storage/file/fact-search.js +60 -0
  179. package/src/storage/file-store.js +437 -0
  180. package/src/storage/import-json.js +70 -0
  181. package/src/storage/json-file.js +41 -0
  182. package/src/storage/legacy-record-validation.js +152 -0
  183. package/src/storage/legacy-snapshot-validation.js +32 -0
  184. package/src/storage/migrate.js +128 -0
  185. package/src/storage/migrations/001-initial.sql +83 -0
  186. package/src/storage/migrations/002-storage-integrity.sql +113 -0
  187. package/src/storage/record-validation.js +46 -0
  188. package/src/storage/snapshot-defaults.js +54 -0
  189. package/src/storage/sqlite/candidates-repository.js +84 -0
  190. package/src/storage/sqlite/episode-evidence-repository.js +154 -0
  191. package/src/storage/sqlite/episode-privacy-sql.js +33 -0
  192. package/src/storage/sqlite/episodes-repository.js +52 -0
  193. package/src/storage/sqlite/fact-search-repository.js +162 -0
  194. package/src/storage/sqlite/facts-repository.js +146 -0
  195. package/src/storage/sqlite/imports-repository.js +39 -0
  196. package/src/storage/sqlite/mapper.js +84 -0
  197. package/src/storage/sqlite/open-database.js +15 -0
  198. package/src/storage/sqlite/outbox-repository.js +103 -0
  199. package/src/storage/sqlite/snapshot-repository.js +69 -0
  200. package/src/storage/sqlite/spaces-repository.js +52 -0
  201. package/src/storage/sqlite/subscriptions-repository.js +54 -0
  202. package/src/storage/sqlite-store.js +159 -0
  203. package/src/storage/stable-json.js +24 -0
  204. package/src/storage/store-port.js +20 -0
  205. package/src/store.js +2 -0
  206. package/src/workspace-protocol/array-validation.js +19 -0
  207. package/src/workspace-protocol/base64url-contract.js +27 -0
  208. package/src/workspace-protocol/constants.js +37 -0
  209. package/src/workspace-protocol/error-contract.js +24 -0
  210. package/src/workspace-protocol/fact-contract.js +110 -0
  211. package/src/workspace-protocol/governance-contract.js +47 -0
  212. package/src/workspace-protocol/index.js +53 -0
  213. package/src/workspace-protocol/json-budget.js +19 -0
  214. package/src/workspace-protocol/manifest-contract.js +186 -0
  215. package/src/workspace-protocol/publication-contract.js +212 -0
  216. package/src/workspace-protocol/query-contract.js +149 -0
  217. package/src/workspace-protocol/scalar-contract.js +192 -0
  218. package/src/workspace-protocol/sync-contract.js +184 -0
  219. package/src/workspace-protocol/text-contract.js +39 -0
  220. package/src/workspace-protocol/validation.js +66 -0
@@ -0,0 +1,33 @@
1
+ import { isRestrictedEpisode } from '../../security/episode-visibility.js';
2
+
3
+ const FUNCTION_NAME = 'episode_evidence_is_restricted';
4
+
5
+ export function registerEpisodePrivacyFunction(db) {
6
+ db.function(
7
+ FUNCTION_NAME,
8
+ { deterministic: true },
9
+ (sourceKind, body, sourceUri, metadataJson) => Number(isRestrictedEpisode({
10
+ sourceKind,
11
+ body,
12
+ sourceUri,
13
+ metadata: parseMetadata(metadataJson)
14
+ }))
15
+ );
16
+ }
17
+
18
+ export function sqliteEpisodeRestrictedSql(alias) {
19
+ return `${FUNCTION_NAME}(
20
+ ${alias}.source_kind,
21
+ ${alias}.body,
22
+ ${alias}.source_uri,
23
+ ${alias}.metadata_json
24
+ ) = 1`;
25
+ }
26
+
27
+ function parseMetadata(value) {
28
+ try {
29
+ return JSON.parse(value);
30
+ } catch {
31
+ return { invalidMetadataRestricted: 'password=invalid-metadata' };
32
+ }
33
+ }
@@ -0,0 +1,52 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ import { nowIso } from '../../models.js';
4
+ import { mapEpisode } from './mapper.js';
5
+
6
+ export class EpisodesRepository {
7
+ constructor(db) {
8
+ this.insertStatement = db.prepare(`
9
+ INSERT INTO episodes (
10
+ id, space_id, source_kind, body, source_uri, metadata_json, created_at
11
+ ) VALUES (
12
+ @id, @spaceId, @sourceKind, @body, @sourceUri, @metadataJson, @createdAt
13
+ )
14
+ `);
15
+ this.getStatement = db.prepare('SELECT * FROM episodes WHERE id = ?');
16
+ this.listStatement = db.prepare('SELECT * FROM episodes ORDER BY rowid');
17
+ this.deleteStatement = db.prepare('DELETE FROM episodes');
18
+ }
19
+
20
+ add(spaceId, sourceKind, body, sourceUri = null, metadata = {}) {
21
+ const episode = {
22
+ id: randomUUID(),
23
+ spaceId,
24
+ sourceKind,
25
+ body,
26
+ sourceUri,
27
+ metadataJson: JSON.stringify(metadata),
28
+ createdAt: nowIso()
29
+ };
30
+ this.insertStatement.run(episode);
31
+ return this.get(episode.id);
32
+ }
33
+
34
+ get(id) {
35
+ return mapEpisode(this.getStatement.get(id));
36
+ }
37
+
38
+ list() {
39
+ return this.listStatement.all().map(mapEpisode);
40
+ }
41
+
42
+ insertSnapshot(episode) {
43
+ this.insertStatement.run({
44
+ ...episode,
45
+ metadataJson: JSON.stringify(episode.metadata)
46
+ });
47
+ }
48
+
49
+ deleteAll() {
50
+ this.deleteStatement.run();
51
+ }
52
+ }
@@ -0,0 +1,162 @@
1
+ import { isRestrictedFact } from '../../security/fact-visibility.js';
2
+ import { searchTextSortKey } from '../fact-search-order.js';
3
+ import { sqliteEpisodeRestrictedSql } from './episode-privacy-sql.js';
4
+ import { mapFact } from './mapper.js';
5
+
6
+ export class FactSearchRepository {
7
+ constructor(db) {
8
+ this.db = db;
9
+ this.statements = new Map();
10
+ this.omissionStatements = new Map();
11
+ registerSearchFunctions(db);
12
+ }
13
+
14
+ search(spaceIds, query, {
15
+ includeHistorical = false,
16
+ scope = null,
17
+ limit = null,
18
+ excludeRestricted = false,
19
+ safeSourceSpaceId = null,
20
+ maxObjectBytes = null
21
+ } = {}) {
22
+ return this.searchPage(spaceIds, query, {
23
+ includeHistorical,
24
+ scope,
25
+ limit,
26
+ excludeRestricted,
27
+ safeSourceSpaceId,
28
+ maxObjectBytes
29
+ }).facts;
30
+ }
31
+
32
+ searchPage(spaceIds, query, options = {}) {
33
+ const {
34
+ includeHistorical = false,
35
+ scope = null,
36
+ limit = null,
37
+ excludeRestricted = false,
38
+ safeSourceSpaceId = null,
39
+ maxObjectBytes = null
40
+ } = options;
41
+ if (spaceIds.length === 0) return { facts: [], objectLimitTruncated: false };
42
+ if (limit !== null && (!Number.isInteger(limit) || limit <= 0)) {
43
+ throw new TypeError('Fact search limit must be a positive integer');
44
+ }
45
+
46
+ const shape = {
47
+ spaceCount: spaceIds.length,
48
+ includeHistorical,
49
+ scoped: scope !== null,
50
+ limited: limit !== null,
51
+ excludeRestricted: excludeRestricted === true,
52
+ safeSourceRequired: safeSourceSpaceId !== null,
53
+ objectBytesBounded: maxObjectBytes !== null
54
+ };
55
+ const parameters = [...spaceIds];
56
+ if (scope !== null) parameters.push(scope);
57
+ if (safeSourceSpaceId !== null) parameters.push(safeSourceSpaceId);
58
+ if (maxObjectBytes !== null) parameters.push(maxObjectBytes);
59
+ parameters.push(query.toLowerCase());
60
+ const factParameters = limit === null ? parameters : [...parameters, limit];
61
+ const facts = this.#statement(shape).all(...factParameters).map(mapFact);
62
+ const objectLimitTruncated = maxObjectBytes === null
63
+ ? false
64
+ : this.#omissionStatement(shape).pluck().get(...parameters) === 1;
65
+ return { facts, objectLimitTruncated };
66
+ }
67
+
68
+ #statement(shape) {
69
+ const key = Object.values(shape).join(':');
70
+ if (!this.statements.has(key)) {
71
+ this.statements.set(key, this.db.prepare(buildSearchQuery(shape)));
72
+ }
73
+ return this.statements.get(key);
74
+ }
75
+
76
+ #omissionStatement(shape) {
77
+ const key = Object.values(shape).join(':');
78
+ if (!this.omissionStatements.has(key)) {
79
+ this.omissionStatements.set(key, this.db.prepare(buildOmissionQuery(shape)));
80
+ }
81
+ return this.omissionStatements.get(key);
82
+ }
83
+ }
84
+
85
+ function buildOmissionQuery(shape) {
86
+ return `SELECT EXISTS (
87
+ SELECT 1 FROM facts
88
+ WHERE ${buildWhereClause(shape, { oversized: true })}
89
+ LIMIT 1
90
+ )`;
91
+ }
92
+
93
+ function registerSearchFunctions(db) {
94
+ db.function('fact_search_text_sort_key', { deterministic: true }, searchTextSortKey);
95
+ db.function('fact_search_is_restricted', { deterministic: true },
96
+ (subject, predicate, object, sensitivity) => Number(isRestrictedFact({
97
+ subject, predicate, object, sensitivity
98
+ })));
99
+ }
100
+
101
+ function buildSearchQuery({
102
+ spaceCount,
103
+ includeHistorical,
104
+ scoped,
105
+ limited,
106
+ excludeRestricted,
107
+ safeSourceRequired,
108
+ objectBytesBounded
109
+ }) {
110
+ const shape = {
111
+ spaceCount,
112
+ includeHistorical,
113
+ scoped,
114
+ excludeRestricted,
115
+ safeSourceRequired,
116
+ objectBytesBounded
117
+ };
118
+ const orderClause = limited
119
+ ? 'ORDER BY valid_at DESC, fact_search_text_sort_key(id) ASC LIMIT ?'
120
+ : 'ORDER BY rowid';
121
+ return `
122
+ SELECT * FROM facts
123
+ WHERE ${buildWhereClause(shape)}
124
+ ${orderClause}
125
+ `;
126
+ }
127
+
128
+ function buildWhereClause({
129
+ spaceCount,
130
+ includeHistorical,
131
+ scoped,
132
+ excludeRestricted,
133
+ safeSourceRequired,
134
+ objectBytesBounded
135
+ }, { oversized = false } = {}) {
136
+ const placeholders = Array.from({ length: spaceCount }, () => '?').join(', ');
137
+ const currentClause = includeHistorical
138
+ ? ''
139
+ : "AND invalid_at IS NULL AND status NOT IN ('rejected', 'deprecated')";
140
+ const scopeClause = scoped ? 'AND scope = ?' : '';
141
+ const restrictedClause = excludeRestricted
142
+ ? `AND fact_search_is_restricted(
143
+ subject, predicate, object, sensitivity
144
+ ) = 0`
145
+ : '';
146
+ const sourceClause = safeSourceRequired ? `AND EXISTS (
147
+ SELECT 1 FROM episodes source
148
+ WHERE source.id = source_episode_id
149
+ AND source.space_id = ?
150
+ AND NOT ${sqliteEpisodeRestrictedSql('source')}
151
+ )` : '';
152
+ const objectBytesClause = objectBytesBounded
153
+ ? `AND length(CAST(object AS BLOB)) ${oversized ? '>' : '<='} ?`
154
+ : '';
155
+ return `space_id IN (${placeholders})
156
+ ${currentClause}
157
+ ${scopeClause}
158
+ ${restrictedClause}
159
+ ${sourceClause}
160
+ ${objectBytesClause}
161
+ AND instr(lower(subject || ' ' || predicate || ' ' || object), ?) > 0`;
162
+ }
@@ -0,0 +1,146 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ import { FactStatus, nowIso } from '../../models.js';
4
+ import { validateFactRecord } from '../record-validation.js';
5
+ import { FactSearchRepository } from './fact-search-repository.js';
6
+ import { mapFact } from './mapper.js';
7
+
8
+ export class FactsRepository {
9
+ constructor(db) {
10
+ this.factSearch = new FactSearchRepository(db);
11
+ this.insertStatement = db.prepare(`
12
+ INSERT INTO facts (
13
+ id, space_id, subject, predicate, object, source_episode_id, status,
14
+ confidence, sensitivity, scope, valid_at, invalid_at, replaced_by_fact_id
15
+ ) VALUES (
16
+ @id, @spaceId, @subject, @predicate, @object, @sourceEpisodeId, @status,
17
+ @confidence, @sensitivity, @scope, @validAt, @invalidAt, @replacedByFactId
18
+ )
19
+ `);
20
+ this.getStatement = db.prepare('SELECT * FROM facts WHERE id = ?');
21
+ this.updateStatement = db.prepare(`
22
+ UPDATE facts SET
23
+ space_id = @spaceId,
24
+ subject = @subject,
25
+ predicate = @predicate,
26
+ object = @object,
27
+ source_episode_id = @sourceEpisodeId,
28
+ status = @status,
29
+ confidence = @confidence,
30
+ sensitivity = @sensitivity,
31
+ scope = @scope,
32
+ valid_at = @validAt,
33
+ invalid_at = @invalidAt,
34
+ replaced_by_fact_id = @replacedByFactId
35
+ WHERE id = @id
36
+ `);
37
+ this.currentStatement = db.prepare(`
38
+ SELECT * FROM facts
39
+ WHERE space_id = ?
40
+ AND invalid_at IS NULL
41
+ AND status NOT IN ('rejected', 'deprecated')
42
+ ORDER BY rowid
43
+ `);
44
+ this.currentListStatement = db.prepare(`
45
+ SELECT * FROM facts
46
+ WHERE invalid_at IS NULL AND status NOT IN ('rejected', 'deprecated')
47
+ ORDER BY rowid
48
+ `);
49
+ this.fullListStatement = db.prepare('SELECT * FROM facts ORDER BY rowid');
50
+ this.timelineStatement = db.prepare(`
51
+ SELECT * FROM facts
52
+ WHERE space_id = ? AND subject = ?
53
+ ORDER BY valid_at, rowid
54
+ `);
55
+ this.replaceLinkStatement = db.prepare(`
56
+ UPDATE facts SET replaced_by_fact_id = ? WHERE id = ?
57
+ `);
58
+ this.deleteStatement = db.prepare('DELETE FROM facts');
59
+ }
60
+
61
+ add(fact) {
62
+ if (fact.id && this.get(fact.id)) {
63
+ throw new Error(`Duplicate fact id: ${fact.id}`);
64
+ }
65
+
66
+ const stored = {
67
+ id: fact.id || randomUUID(),
68
+ spaceId: fact.spaceId,
69
+ subject: fact.subject,
70
+ predicate: fact.predicate,
71
+ object: fact.object,
72
+ sourceEpisodeId: fact.sourceEpisodeId,
73
+ status: fact.status ?? FactStatus.CONFIRMED,
74
+ confidence: fact.confidence ?? 1,
75
+ sensitivity: fact.sensitivity ?? 'normal',
76
+ scope: fact.scope ?? 'personal',
77
+ validAt: fact.validAt ?? nowIso(),
78
+ invalidAt: fact.invalidAt ?? null,
79
+ replacedByFactId: fact.replacedByFactId ?? null
80
+ };
81
+ validateFactRecord(stored);
82
+ this.insertStatement.run(stored);
83
+ return this.get(stored.id);
84
+ }
85
+
86
+ get(id) {
87
+ return mapFact(this.getStatement.get(id));
88
+ }
89
+
90
+ update(id, updates) {
91
+ const existing = this.get(id);
92
+ if (!existing) throw new Error(`Fact not found: ${id}`);
93
+
94
+ const stored = { ...existing, ...updates, id };
95
+ validateFactRecord(stored);
96
+ this.updateStatement.run(stored);
97
+ return this.get(id);
98
+ }
99
+
100
+ invalidate(id, replacedByFactId = null) {
101
+ const fact = this.get(id);
102
+ if (!fact || fact.invalidAt) return;
103
+ this.updateStatement.run({
104
+ ...fact,
105
+ invalidAt: nowIso(),
106
+ replacedByFactId
107
+ });
108
+ }
109
+
110
+ current(spaceId) {
111
+ return this.currentStatement.all(spaceId).map(mapFact);
112
+ }
113
+
114
+ list({ includeHistorical = false } = {}) {
115
+ const statement = includeHistorical ? this.fullListStatement : this.currentListStatement;
116
+ return statement.all().map(mapFact);
117
+ }
118
+
119
+ timeline(spaceId, subject) {
120
+ return this.timelineStatement.all(spaceId, subject).map(mapFact);
121
+ }
122
+
123
+ search(spaceIds, query, options = {}) {
124
+ return this.factSearch.search(spaceIds, query, options);
125
+ }
126
+
127
+ searchPage(spaceIds, query, options = {}) {
128
+ return this.factSearch.searchPage(spaceIds, query, options);
129
+ }
130
+
131
+ insertSnapshot(fact, { deferReplacement = false } = {}) {
132
+ validateFactRecord(fact);
133
+ this.insertStatement.run({
134
+ ...fact,
135
+ replacedByFactId: deferReplacement ? null : fact.replacedByFactId
136
+ });
137
+ }
138
+
139
+ restoreReplacement(id, replacedByFactId) {
140
+ this.replaceLinkStatement.run(replacedByFactId, id);
141
+ }
142
+
143
+ deleteAll() {
144
+ this.deleteStatement.run();
145
+ }
146
+ }
@@ -0,0 +1,39 @@
1
+ import { mapImport } from './mapper.js';
2
+
3
+ export class ImportsRepository {
4
+ constructor(db) {
5
+ this.insertStatement = db.prepare(`
6
+ INSERT INTO imports (content_hash, source_path, imported_at)
7
+ VALUES (@contentHash, @sourcePath, @importedAt)
8
+ `);
9
+ this.hasStatement = db.prepare(`
10
+ SELECT 1 FROM imports WHERE content_hash = ?
11
+ `);
12
+ this.listStatement = db.prepare('SELECT * FROM imports ORDER BY rowid');
13
+ this.deleteStatement = db.prepare('DELETE FROM imports');
14
+ }
15
+
16
+ has(contentHash) {
17
+ return this.hasStatement.get(contentHash) !== undefined;
18
+ }
19
+
20
+ record(record) {
21
+ if (this.has(record.contentHash)) {
22
+ throw new Error(`Duplicate import content hash: ${record.contentHash}`);
23
+ }
24
+ this.insertStatement.run(record);
25
+ return { ...record };
26
+ }
27
+
28
+ listAll() {
29
+ return this.listStatement.all().map(mapImport);
30
+ }
31
+
32
+ insertSnapshot(record) {
33
+ this.insertStatement.run(record);
34
+ }
35
+
36
+ deleteAll() {
37
+ this.deleteStatement.run();
38
+ }
39
+ }
@@ -0,0 +1,84 @@
1
+ export function mapSpace(row) {
2
+ return row ? {
3
+ id: row.id,
4
+ name: row.name,
5
+ kind: row.kind,
6
+ description: row.description,
7
+ createdAt: row.created_at
8
+ } : null;
9
+ }
10
+
11
+ export function mapSubscription(row) {
12
+ return row ? {
13
+ personalSpaceId: row.personal_space_id,
14
+ spaceId: row.space_id,
15
+ mode: row.mode,
16
+ createdAt: row.created_at
17
+ } : null;
18
+ }
19
+
20
+ export function mapEpisode(row) {
21
+ return row ? {
22
+ id: row.id,
23
+ spaceId: row.space_id,
24
+ sourceKind: row.source_kind,
25
+ body: row.body,
26
+ sourceUri: row.source_uri,
27
+ metadata: JSON.parse(row.metadata_json),
28
+ createdAt: row.created_at
29
+ } : null;
30
+ }
31
+
32
+ export function mapFact(row) {
33
+ return row ? {
34
+ id: row.id,
35
+ spaceId: row.space_id,
36
+ subject: row.subject,
37
+ predicate: row.predicate,
38
+ object: row.object,
39
+ sourceEpisodeId: row.source_episode_id,
40
+ status: row.status,
41
+ confidence: row.confidence,
42
+ sensitivity: row.sensitivity,
43
+ scope: row.scope,
44
+ validAt: row.valid_at,
45
+ invalidAt: row.invalid_at,
46
+ replacedByFactId: row.replaced_by_fact_id
47
+ } : null;
48
+ }
49
+
50
+ export function mapCandidate(row) {
51
+ return row ? {
52
+ id: row.id,
53
+ personalSpaceId: row.personal_space_id,
54
+ targetSpaceId: row.target_space_id,
55
+ episodeId: row.episode_id,
56
+ reason: row.reason,
57
+ status: row.status,
58
+ createdAt: row.created_at,
59
+ decidedAt: row.decided_at
60
+ } : null;
61
+ }
62
+
63
+ export function mapOutbox(row) {
64
+ return row ? {
65
+ id: row.id,
66
+ kind: row.kind,
67
+ aggregateId: row.aggregate_id,
68
+ payload: JSON.parse(row.payload_json),
69
+ status: row.status,
70
+ attempts: row.attempts,
71
+ nextAttemptAt: row.next_attempt_at,
72
+ createdAt: row.created_at,
73
+ sentAt: row.sent_at,
74
+ lastError: row.last_error
75
+ } : null;
76
+ }
77
+
78
+ export function mapImport(row) {
79
+ return row ? {
80
+ contentHash: row.content_hash,
81
+ sourcePath: row.source_path,
82
+ importedAt: row.imported_at
83
+ } : null;
84
+ }
@@ -0,0 +1,15 @@
1
+ import { mkdirSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+
4
+ import Database from 'better-sqlite3';
5
+
6
+ export function openDatabase(filePath) {
7
+ if (isFileDatabase(filePath)) {
8
+ mkdirSync(dirname(resolve(filePath)), { recursive: true });
9
+ }
10
+ return new Database(filePath);
11
+ }
12
+
13
+ export function isFileDatabase(filePath) {
14
+ return typeof filePath === 'string' && filePath !== ':memory:' && filePath !== '';
15
+ }
@@ -0,0 +1,103 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ import { nowIso } from '../../models.js';
4
+ import { validateOutboxRecord } from '../record-validation.js';
5
+ import { mapOutbox } from './mapper.js';
6
+
7
+ export class OutboxRepository {
8
+ constructor(db) {
9
+ this.insertStatement = db.prepare(`
10
+ INSERT INTO outbox (
11
+ id, kind, aggregate_id, payload_json, status, attempts,
12
+ next_attempt_at, created_at, sent_at, last_error
13
+ ) VALUES (
14
+ @id, @kind, @aggregateId, @payloadJson, @status, @attempts,
15
+ @nextAttemptAt, @createdAt, @sentAt, @lastError
16
+ )
17
+ `);
18
+ this.getStatement = db.prepare('SELECT * FROM outbox WHERE id = ?');
19
+ this.listStatement = db.prepare('SELECT * FROM outbox ORDER BY rowid');
20
+ this.pendingStatement = db.prepare(`
21
+ SELECT * FROM outbox
22
+ WHERE status = 'pending'
23
+ AND (next_attempt_at IS NULL OR next_attempt_at <= ?)
24
+ ORDER BY created_at, rowid
25
+ `);
26
+ this.markSentStatement = db.prepare(`
27
+ UPDATE outbox SET
28
+ status = 'sent', next_attempt_at = NULL, sent_at = ?, last_error = NULL
29
+ WHERE id = ?
30
+ `);
31
+ this.markFailedStatement = db.prepare(`
32
+ UPDATE outbox SET
33
+ status = 'pending', attempts = attempts + 1,
34
+ next_attempt_at = ?, last_error = ?
35
+ WHERE id = ?
36
+ `);
37
+ this.deleteStatement = db.prepare('DELETE FROM outbox');
38
+ }
39
+
40
+ enqueue(entry) {
41
+ if (entry.id && this.get(entry.id)) {
42
+ throw new Error(`Duplicate outbox id: ${entry.id}`);
43
+ }
44
+
45
+ const stored = {
46
+ id: entry.id || randomUUID(),
47
+ kind: entry.kind,
48
+ aggregateId: entry.aggregateId,
49
+ payloadJson: JSON.stringify(entry.payload),
50
+ status: entry.status ?? 'pending',
51
+ attempts: entry.attempts ?? 0,
52
+ nextAttemptAt: entry.nextAttemptAt ?? null,
53
+ createdAt: entry.createdAt ?? nowIso(),
54
+ sentAt: entry.sentAt ?? null,
55
+ lastError: entry.lastError ?? null
56
+ };
57
+ validateOutboxRecord(stored);
58
+ this.insertStatement.run(stored);
59
+ return this.get(stored.id);
60
+ }
61
+
62
+ listPending(at = nowIso()) {
63
+ return this.pendingStatement.all(at).map(mapOutbox);
64
+ }
65
+
66
+ markSent(id, sentAt = nowIso()) {
67
+ this.require(id);
68
+ this.markSentStatement.run(sentAt, id);
69
+ return this.get(id);
70
+ }
71
+
72
+ markFailed(id, error, nextAttemptAt = null) {
73
+ this.require(id);
74
+ this.markFailedStatement.run(nextAttemptAt, error, id);
75
+ return this.get(id);
76
+ }
77
+
78
+ get(id) {
79
+ return mapOutbox(this.getStatement.get(id));
80
+ }
81
+
82
+ listAll() {
83
+ return this.listStatement.all().map(mapOutbox);
84
+ }
85
+
86
+ insertSnapshot(entry) {
87
+ validateOutboxRecord(entry);
88
+ this.insertStatement.run({
89
+ ...entry,
90
+ payloadJson: JSON.stringify(entry.payload)
91
+ });
92
+ }
93
+
94
+ deleteAll() {
95
+ this.deleteStatement.run();
96
+ }
97
+
98
+ require(id) {
99
+ const row = this.get(id);
100
+ if (!row) throw new Error(`Outbox row not found: ${id}`);
101
+ return row;
102
+ }
103
+ }
@@ -0,0 +1,69 @@
1
+ import { cloneSnapshot, normalizeSnapshot } from '../snapshot-defaults.js';
2
+
3
+ export class SnapshotRepository {
4
+ constructor(db, repositories) {
5
+ this.db = db;
6
+ this.repositories = repositories;
7
+ }
8
+
9
+ export() {
10
+ const repositories = this.repositories;
11
+ return cloneSnapshot({
12
+ spaces: repositories.spaces.list(),
13
+ subscriptions: repositories.subscriptions.list(),
14
+ episodes: repositories.episodes.list(),
15
+ facts: repositories.facts.list({ includeHistorical: true }),
16
+ candidates: repositories.candidates.list(),
17
+ outbox: repositories.outbox.listAll(),
18
+ imports: repositories.imports.listAll()
19
+ });
20
+ }
21
+
22
+ import(snapshot) {
23
+ const current = this.export();
24
+ const normalized = normalizeSnapshot(snapshot, {
25
+ outbox: current.outbox,
26
+ imports: current.imports
27
+ });
28
+ const replaceOutbox = Object.hasOwn(snapshot, 'outbox');
29
+ const replaceImports = Object.hasOwn(snapshot, 'imports');
30
+ const apply = () => this.replace(normalized, { replaceOutbox, replaceImports });
31
+
32
+ if (this.db.inTransaction) apply();
33
+ else this.db.transaction(apply)();
34
+ }
35
+
36
+ replace(snapshot, { replaceOutbox, replaceImports }) {
37
+ const repositories = this.repositories;
38
+ repositories.subscriptions.deleteAll();
39
+ repositories.candidates.deleteAll();
40
+ repositories.facts.deleteAll();
41
+ repositories.episodes.deleteAll();
42
+ if (replaceOutbox) repositories.outbox.deleteAll();
43
+ if (replaceImports) repositories.imports.deleteAll();
44
+ repositories.spaces.deleteAll();
45
+
46
+ for (const space of snapshot.spaces) repositories.spaces.insertSnapshot(space);
47
+ for (const subscription of snapshot.subscriptions) {
48
+ repositories.subscriptions.insertSnapshot(subscription);
49
+ }
50
+ for (const episode of snapshot.episodes) repositories.episodes.insertSnapshot(episode);
51
+ for (const fact of snapshot.facts) {
52
+ repositories.facts.insertSnapshot(fact, { deferReplacement: true });
53
+ }
54
+ for (const fact of snapshot.facts) {
55
+ if (fact.replacedByFactId) {
56
+ repositories.facts.restoreReplacement(fact.id, fact.replacedByFactId);
57
+ }
58
+ }
59
+ for (const candidate of snapshot.candidates) {
60
+ repositories.candidates.insertSnapshot(candidate);
61
+ }
62
+ if (replaceOutbox) {
63
+ for (const entry of snapshot.outbox) repositories.outbox.insertSnapshot(entry);
64
+ }
65
+ if (replaceImports) {
66
+ for (const record of snapshot.imports) repositories.imports.insertSnapshot(record);
67
+ }
68
+ }
69
+ }