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,52 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ import { nowIso } from '../../models.js';
4
+ import { mapSpace } from './mapper.js';
5
+
6
+ export class SpacesRepository {
7
+ constructor(db) {
8
+ this.insertStatement = db.prepare(`
9
+ INSERT INTO spaces (id, name, kind, description, created_at)
10
+ VALUES (@id, @name, @kind, @description, @createdAt)
11
+ `);
12
+ this.listStatement = db.prepare('SELECT * FROM spaces ORDER BY rowid');
13
+ this.findByNameStatement = db.prepare('SELECT * FROM spaces WHERE name = ?');
14
+ this.getStatement = db.prepare('SELECT * FROM spaces WHERE id = ?');
15
+ this.deleteStatement = db.prepare('DELETE FROM spaces');
16
+ }
17
+
18
+ create(name, kind, description = null) {
19
+ const existing = this.findByName(name);
20
+ if (existing) return existing;
21
+
22
+ const space = {
23
+ id: randomUUID(),
24
+ name,
25
+ kind,
26
+ description,
27
+ createdAt: nowIso()
28
+ };
29
+ this.insertStatement.run(space);
30
+ return this.get(space.id);
31
+ }
32
+
33
+ list() {
34
+ return this.listStatement.all().map(mapSpace);
35
+ }
36
+
37
+ findByName(name) {
38
+ return mapSpace(this.findByNameStatement.get(name));
39
+ }
40
+
41
+ get(id) {
42
+ return mapSpace(this.getStatement.get(id));
43
+ }
44
+
45
+ insertSnapshot(space) {
46
+ this.insertStatement.run(space);
47
+ }
48
+
49
+ deleteAll() {
50
+ this.deleteStatement.run();
51
+ }
52
+ }
@@ -0,0 +1,54 @@
1
+ import { nowIso } from '../../models.js';
2
+ import { mapSubscription } from './mapper.js';
3
+
4
+ export class SubscriptionsRepository {
5
+ constructor(db) {
6
+ this.insertStatement = db.prepare(`
7
+ INSERT INTO subscriptions (personal_space_id, space_id, mode, created_at)
8
+ VALUES (@personalSpaceId, @spaceId, @mode, @createdAt)
9
+ `);
10
+ this.getStatement = db.prepare(`
11
+ SELECT * FROM subscriptions
12
+ WHERE personal_space_id = ? AND space_id = ?
13
+ `);
14
+ this.listStatement = db.prepare('SELECT * FROM subscriptions ORDER BY rowid');
15
+ this.forPersonalSpaceStatement = db.prepare(`
16
+ SELECT * FROM subscriptions WHERE personal_space_id = ? ORDER BY rowid
17
+ `);
18
+ this.deleteStatement = db.prepare('DELETE FROM subscriptions');
19
+ }
20
+
21
+ subscribe(personalSpaceId, spaceId, mode = 'latest') {
22
+ const existing = this.get(personalSpaceId, spaceId);
23
+ if (existing) return existing;
24
+
25
+ const subscription = {
26
+ personalSpaceId,
27
+ spaceId,
28
+ mode,
29
+ createdAt: nowIso()
30
+ };
31
+ this.insertStatement.run(subscription);
32
+ return this.get(personalSpaceId, spaceId);
33
+ }
34
+
35
+ list() {
36
+ return this.listStatement.all().map(mapSubscription);
37
+ }
38
+
39
+ forPersonalSpace(personalSpaceId) {
40
+ return this.forPersonalSpaceStatement.all(personalSpaceId).map(mapSubscription);
41
+ }
42
+
43
+ insertSnapshot(subscription) {
44
+ this.insertStatement.run(subscription);
45
+ }
46
+
47
+ deleteAll() {
48
+ this.deleteStatement.run();
49
+ }
50
+
51
+ get(personalSpaceId, spaceId) {
52
+ return mapSubscription(this.getStatement.get(personalSpaceId, spaceId));
53
+ }
54
+ }
@@ -0,0 +1,159 @@
1
+ import { configureDatabase, runMigrations } from './migrate.js';
2
+ import { CandidatesRepository } from './sqlite/candidates-repository.js';
3
+ import { EpisodeEvidenceRepository } from './sqlite/episode-evidence-repository.js';
4
+ import { registerEpisodePrivacyFunction } from './sqlite/episode-privacy-sql.js';
5
+ import { EpisodesRepository } from './sqlite/episodes-repository.js';
6
+ import { FactsRepository } from './sqlite/facts-repository.js';
7
+ import { ImportsRepository } from './sqlite/imports-repository.js';
8
+ import { OutboxRepository } from './sqlite/outbox-repository.js';
9
+ import { openDatabase } from './sqlite/open-database.js';
10
+ import { SnapshotRepository } from './sqlite/snapshot-repository.js';
11
+ import { SpacesRepository } from './sqlite/spaces-repository.js';
12
+ import { SubscriptionsRepository } from './sqlite/subscriptions-repository.js';
13
+
14
+ export class SqliteStore {
15
+ #db;
16
+ #repositories;
17
+ #schemaVersionsStatement;
18
+ #transactionActive = false;
19
+
20
+ constructor(filePath = '.fuli/context.db') {
21
+ this.#db = openDatabase(filePath);
22
+ try {
23
+ configureDatabase(this.#db, filePath);
24
+ runMigrations(this.#db);
25
+ this.#repositories = createRepositories(this.#db);
26
+ this.#schemaVersionsStatement = this.#db.prepare(`
27
+ SELECT version FROM schema_migrations ORDER BY version
28
+ `);
29
+ } catch (error) {
30
+ this.#db.close();
31
+ throw error;
32
+ }
33
+ }
34
+
35
+ transaction(fn, { mode = 'default' } = {}) {
36
+ if (this.#transactionActive) {
37
+ throw new TypeError('Nested SqliteStore transactions are not supported');
38
+ }
39
+ if (fn.constructor?.name === 'AsyncFunction') {
40
+ throw new TypeError('Transaction callback must be synchronous');
41
+ }
42
+
43
+ this.#transactionActive = true;
44
+ try {
45
+ const transaction = this.#db.transaction(() => {
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
+ return result;
52
+ });
53
+ const run = mode === 'immediate' ? transaction.immediate : transaction;
54
+ if (typeof run !== 'function') throw new TypeError(`Unsupported transaction mode: ${mode}`);
55
+ return run();
56
+ } finally {
57
+ this.#transactionActive = false;
58
+ }
59
+ }
60
+
61
+ createSpace(name, kind, description = null) {
62
+ return this.#repositories.spaces.create(name, kind, description);
63
+ }
64
+
65
+ listSpaces() { return this.#repositories.spaces.list(); }
66
+ findSpaceByName(name) { return this.#repositories.spaces.findByName(name); }
67
+ getSpace(id) { return this.#repositories.spaces.get(id); }
68
+
69
+ subscribe(personalSpaceId, spaceId, mode = 'latest') {
70
+ return this.#repositories.subscriptions.subscribe(personalSpaceId, spaceId, mode);
71
+ }
72
+
73
+ listSubscriptions() { return this.#repositories.subscriptions.list(); }
74
+ subscriptionsFor(personalSpaceId) {
75
+ return this.#repositories.subscriptions.forPersonalSpace(personalSpaceId);
76
+ }
77
+
78
+ addEpisode(spaceId, sourceKind, body, sourceUri = null, metadata = {}) {
79
+ return this.#repositories.episodes.add(spaceId, sourceKind, body, sourceUri, metadata);
80
+ }
81
+
82
+ getEpisode(id) { return this.#repositories.episodes.get(id); }
83
+ listEpisodes() { return this.#repositories.episodes.list(); }
84
+ episodeEvidencePreview(spaceId, episodeId, options = {}) {
85
+ return this.#repositories.episodeEvidence.source(spaceId, episodeId, options);
86
+ }
87
+ correctionEpisodeEvidencePreviews(spaceId, factIds, options = {}) {
88
+ return this.#repositories.episodeEvidence.corrections(spaceId, factIds, options);
89
+ }
90
+ addFact(fact) { return this.#repositories.facts.add(fact); }
91
+ getFact(id) { return this.#repositories.facts.get(id); }
92
+ updateFact(id, updates) { return this.#repositories.facts.update(id, updates); }
93
+ invalidateFact(id, replacedByFactId = null) {
94
+ return this.#repositories.facts.invalidate(id, replacedByFactId);
95
+ }
96
+
97
+ currentFacts(spaceId) { return this.#repositories.facts.current(spaceId); }
98
+ listFacts(options = {}) { return this.#repositories.facts.list(options); }
99
+ timeline(spaceId, subject) { return this.#repositories.facts.timeline(spaceId, subject); }
100
+ searchFacts(spaceIds, query, options = {}) {
101
+ return this.#repositories.facts.search(spaceIds, query, options);
102
+ }
103
+ searchFactsPage(spaceIds, query, options = {}) {
104
+ return this.#repositories.facts.searchPage(spaceIds, query, options);
105
+ }
106
+
107
+ addCandidate(candidate) { return this.#repositories.candidates.add(candidate); }
108
+ getCandidate(id) { return this.#repositories.candidates.get(id); }
109
+ listCandidates() { return this.#repositories.candidates.list(); }
110
+ pendingCandidates(personalSpaceId) {
111
+ return this.#repositories.candidates.pending(personalSpaceId);
112
+ }
113
+
114
+ updateCandidateStatus(id, status) {
115
+ return this.#repositories.candidates.updateStatus(id, status);
116
+ }
117
+
118
+ enqueueOutbox(entry) { return this.#repositories.outbox.enqueue(entry); }
119
+ listPendingOutbox(at) { return this.#repositories.outbox.listPending(at); }
120
+ markOutboxSent(id, sentAt) { return this.#repositories.outbox.markSent(id, sentAt); }
121
+ markOutboxFailed(id, error, nextAttemptAt = null) {
122
+ return this.#repositories.outbox.markFailed(id, error, nextAttemptAt);
123
+ }
124
+
125
+ hasImport(contentHash) { return this.#repositories.imports.has(contentHash); }
126
+ recordImport(record) { return this.#repositories.imports.record(record); }
127
+ exportSnapshot() { return this.#repositories.snapshot.export(); }
128
+ importSnapshot(snapshot) { return this.#repositories.snapshot.import(snapshot); }
129
+
130
+ schemaVersions() {
131
+ return this.#schemaVersionsStatement.pluck().all();
132
+ }
133
+
134
+ close() {
135
+ if (this.#db.open) this.#db.close();
136
+ }
137
+ }
138
+
139
+ function createRepositories(db) {
140
+ registerEpisodePrivacyFunction(db);
141
+ const repositories = {
142
+ spaces: new SpacesRepository(db),
143
+ subscriptions: new SubscriptionsRepository(db),
144
+ episodes: new EpisodesRepository(db),
145
+ episodeEvidence: new EpisodeEvidenceRepository(db),
146
+ facts: new FactsRepository(db),
147
+ candidates: new CandidatesRepository(db),
148
+ outbox: new OutboxRepository(db),
149
+ imports: new ImportsRepository(db)
150
+ };
151
+ repositories.snapshot = new SnapshotRepository(db, repositories);
152
+ return repositories;
153
+ }
154
+
155
+ function isThenable(value) {
156
+ return value !== null &&
157
+ (typeof value === 'object' || typeof value === 'function') &&
158
+ typeof value.then === 'function';
159
+ }
@@ -0,0 +1,24 @@
1
+ export function stableJson(value) {
2
+ return JSON.stringify(sortJsonValue(value));
3
+ }
4
+
5
+ function sortJsonValue(value) {
6
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
7
+ if (typeof value === 'number') {
8
+ if (Number.isFinite(value)) return value;
9
+ throw new TypeError('Snapshot must contain only finite JSON numbers');
10
+ }
11
+ if (Array.isArray(value)) return value.map(sortJsonValue);
12
+ if (isPlainObject(value)) {
13
+ return Object.fromEntries(
14
+ Object.keys(value).sort().map((key) => [key, sortJsonValue(value[key])])
15
+ );
16
+ }
17
+ throw new TypeError('Snapshot must contain only JSON values');
18
+ }
19
+
20
+ function isPlainObject(value) {
21
+ if (!value || typeof value !== 'object') return false;
22
+ const prototype = Object.getPrototypeOf(value);
23
+ return prototype === Object.prototype || prototype === null;
24
+ }
@@ -0,0 +1,20 @@
1
+ export const STORE_METHODS = Object.freeze([
2
+ 'transaction',
3
+ 'createSpace', 'listSpaces', 'findSpaceByName', 'getSpace',
4
+ 'subscribe', 'listSubscriptions', 'subscriptionsFor',
5
+ 'addEpisode', 'getEpisode', 'listEpisodes',
6
+ 'episodeEvidencePreview', 'correctionEpisodeEvidencePreviews',
7
+ 'addFact', 'getFact', 'updateFact', 'invalidateFact',
8
+ 'currentFacts', 'listFacts', 'timeline', 'searchFacts', 'searchFactsPage',
9
+ 'addCandidate', 'getCandidate', 'listCandidates',
10
+ 'pendingCandidates', 'updateCandidateStatus',
11
+ 'enqueueOutbox', 'listPendingOutbox', 'markOutboxSent', 'markOutboxFailed',
12
+ 'hasImport', 'recordImport',
13
+ 'exportSnapshot', 'importSnapshot', 'close'
14
+ ]);
15
+
16
+ export function assertStorePort(store) {
17
+ const missing = STORE_METHODS.filter((name) => typeof store?.[name] !== 'function');
18
+ if (missing.length) throw new TypeError(`Store Port missing: ${missing.join(', ')}`);
19
+ return store;
20
+ }
package/src/store.js ADDED
@@ -0,0 +1,2 @@
1
+ export { FileStore } from './storage/file-store.js';
2
+ export { SqliteStore } from './storage/sqlite-store.js';
@@ -0,0 +1,19 @@
1
+ export function rejectDuplicateValues(
2
+ values,
3
+ context,
4
+ pathForIndex = (index) => [index],
5
+ ) {
6
+ const seen = new Set();
7
+
8
+ values.forEach((value, index) => {
9
+ if (seen.has(value)) {
10
+ context.addIssue({
11
+ code: 'custom',
12
+ path: pathForIndex(index),
13
+ message: 'Duplicate values are not allowed',
14
+ });
15
+ }
16
+
17
+ seen.add(value);
18
+ });
19
+ }
@@ -0,0 +1,27 @@
1
+ import { Buffer } from 'node:buffer';
2
+
3
+ import { z } from 'zod';
4
+
5
+ export function createCanonicalBase64UrlSchema(byteLength) {
6
+ if (!Number.isSafeInteger(byteLength) || byteLength < 0) {
7
+ throw new TypeError('byteLength must be a non-negative safe integer');
8
+ }
9
+
10
+ const encodedLength = Math.ceil(byteLength * 4 / 3);
11
+
12
+ return z
13
+ .string()
14
+ .length(encodedLength)
15
+ .refine((value) => {
16
+ if (!/^[A-Za-z0-9_-]*$/u.test(value)) {
17
+ return false;
18
+ }
19
+
20
+ const decoded = Buffer.from(value, 'base64url');
21
+
22
+ return (
23
+ decoded.byteLength === byteLength
24
+ && decoded.toString('base64url') === value
25
+ );
26
+ });
27
+ }
@@ -0,0 +1,37 @@
1
+ export const WORKSPACE_PROTOCOL_VERSION = '1';
2
+
3
+ export const ProviderCapability = Object.freeze({
4
+ QUERY: 'query',
5
+ SYNC: 'sync',
6
+ PUBLISH: 'publish',
7
+ REVIEW: 'review',
8
+ });
9
+
10
+ export const WorkspaceVisibility = Object.freeze({
11
+ PUBLIC: 'public',
12
+ UNLISTED: 'unlisted',
13
+ PRIVATE: 'private',
14
+ });
15
+
16
+ export const WorkspaceRole = Object.freeze({
17
+ MEMBER: 'member',
18
+ MAINTAINER: 'maintainer',
19
+ });
20
+
21
+ export const ProposalStatus = Object.freeze({
22
+ EFFECTIVE: 'effective',
23
+ PENDING_REVIEW: 'pending_review',
24
+ REJECTED: 'rejected',
25
+ });
26
+
27
+ export const FactState = Object.freeze({
28
+ ACTIVE: 'active',
29
+ SUPERSEDED: 'superseded',
30
+ RETRACTED: 'retracted',
31
+ DISPUTED: 'disputed',
32
+ });
33
+
34
+ export const FactCertainty = Object.freeze({
35
+ CONFIRMED: 'confirmed',
36
+ KNOWN_UNKNOWN: 'known_unknown',
37
+ });
@@ -0,0 +1,24 @@
1
+ import { z } from 'zod';
2
+
3
+ import { idSchema } from './scalar-contract.js';
4
+ import { createPreservedSemanticTextSchema } from './text-contract.js';
5
+
6
+ export const ProtocolErrorCode = Object.freeze({
7
+ AUTHENTICATION_REQUIRED: 'AUTHENTICATION_REQUIRED',
8
+ FORBIDDEN: 'FORBIDDEN',
9
+ NOT_FOUND: 'NOT_FOUND',
10
+ VALIDATION_FAILED: 'VALIDATION_FAILED',
11
+ IDEMPOTENCY_CONFLICT: 'IDEMPOTENCY_CONFLICT',
12
+ REVISION_CONFLICT: 'REVISION_CONFLICT',
13
+ CURSOR_EXPIRED: 'CURSOR_EXPIRED',
14
+ RATE_LIMITED: 'RATE_LIMITED',
15
+ PROVIDER_UNAVAILABLE: 'PROVIDER_UNAVAILABLE',
16
+ PROTOCOL_INCOMPATIBLE: 'PROTOCOL_INCOMPATIBLE',
17
+ });
18
+
19
+ export const protocolErrorResponseSchema = z.strictObject({
20
+ code: z.enum(Object.values(ProtocolErrorCode)),
21
+ message: createPreservedSemanticTextSchema(500),
22
+ traceId: idSchema,
23
+ retryAfterSeconds: z.number().int().min(1).max(86_400).optional(),
24
+ });
@@ -0,0 +1,110 @@
1
+ import { z } from 'zod';
2
+
3
+ import { rejectDuplicateValues } from './array-validation.js';
4
+ import { FactCertainty, FactState } from './constants.js';
5
+ import {
6
+ idSchema,
7
+ revisionSchema,
8
+ sha256Schema,
9
+ sourceUriSchema,
10
+ timestampSchema,
11
+ } from './scalar-contract.js';
12
+ import {
13
+ createPreservedContentTextSchema,
14
+ createPreservedSemanticTextSchema,
15
+ } from './text-contract.js';
16
+
17
+ export const sourceReferenceSchema = z.strictObject({
18
+ episodeId: idSchema,
19
+ kind: z.string().regex(/^[A-Za-z][A-Za-z0-9._-]{0,63}$/u),
20
+ uri: sourceUriSchema.nullable(),
21
+ capturedAt: timestampSchema,
22
+ contentHash: sha256Schema,
23
+ });
24
+
25
+ export const factProjectionSchema = z
26
+ .strictObject({
27
+ id: idSchema,
28
+ subject: createPreservedSemanticTextSchema(500),
29
+ predicate: createPreservedSemanticTextSchema(160),
30
+ object: createPreservedContentTextSchema(20_000),
31
+ state: z.enum(Object.values(FactState)),
32
+ certainty: z.enum(Object.values(FactCertainty)),
33
+ validFrom: timestampSchema,
34
+ validTo: timestampSchema.nullable(),
35
+ recordedAt: timestampSchema,
36
+ revision: revisionSchema,
37
+ sources: z.array(sourceReferenceSchema).min(1).max(20),
38
+ replaces: z.array(idSchema).max(50),
39
+ replacedBy: z.array(idSchema).max(50),
40
+ })
41
+ .superRefine((fact, context) => {
42
+ rejectDuplicateValues(
43
+ fact.sources.map(({ episodeId }) => episodeId),
44
+ context,
45
+ (index) => ['sources', index, 'episodeId'],
46
+ );
47
+ rejectDuplicateValues(
48
+ fact.replaces,
49
+ context,
50
+ (index) => ['replaces', index],
51
+ );
52
+ rejectDuplicateValues(
53
+ fact.replacedBy,
54
+ context,
55
+ (index) => ['replacedBy', index],
56
+ );
57
+
58
+ fact.replaces.forEach((id, index) => {
59
+ if (id === fact.id) {
60
+ context.addIssue({
61
+ code: 'custom',
62
+ path: ['replaces', index],
63
+ message: 'A fact cannot replace itself',
64
+ });
65
+ }
66
+ });
67
+
68
+ const replacedIds = new Set(fact.replaces);
69
+
70
+ fact.replacedBy.forEach((id, index) => {
71
+ if (id === fact.id) {
72
+ context.addIssue({
73
+ code: 'custom',
74
+ path: ['replacedBy', index],
75
+ message: 'A fact cannot be replaced by itself',
76
+ });
77
+ }
78
+
79
+ if (replacedIds.has(id)) {
80
+ context.addIssue({
81
+ code: 'custom',
82
+ path: ['replacedBy', index],
83
+ message: 'Replacement relationships cannot overlap',
84
+ });
85
+ }
86
+ });
87
+
88
+ if (
89
+ fact.validTo !== null
90
+ && Date.parse(fact.validTo) < Date.parse(fact.validFrom)
91
+ ) {
92
+ context.addIssue({
93
+ code: 'custom',
94
+ path: ['validTo'],
95
+ message: 'validTo cannot be earlier than validFrom',
96
+ });
97
+ }
98
+
99
+ const recordedAtEpoch = Date.parse(fact.recordedAt);
100
+
101
+ fact.sources.forEach(({ capturedAt }, index) => {
102
+ if (Date.parse(capturedAt) > recordedAtEpoch) {
103
+ context.addIssue({
104
+ code: 'custom',
105
+ path: ['sources', index, 'capturedAt'],
106
+ message: 'Source capture time cannot follow fact recording time',
107
+ });
108
+ }
109
+ });
110
+ });
@@ -0,0 +1,47 @@
1
+ import { z } from 'zod';
2
+
3
+ import { idSchema, timestampSchema } from './scalar-contract.js';
4
+ import { createPreservedSemanticTextSchema } from './text-contract.js';
5
+ import { parseProtocolValue } from './validation.js';
6
+
7
+ const semanticGovernanceTextSchema = createPreservedSemanticTextSchema(500);
8
+
9
+ export const joinRequestSchema = z.strictObject({
10
+ requestId: idSchema,
11
+ workspaceId: idSchema,
12
+ requesterId: idSchema,
13
+ message: semanticGovernanceTextSchema.nullable(),
14
+ createdAt: timestampSchema,
15
+ status: z.enum(['pending', 'approved', 'rejected']),
16
+ });
17
+
18
+ export const joinDecisionSchema = z.strictObject({
19
+ decision: z.enum(['approve', 'reject']),
20
+ reason: semanticGovernanceTextSchema,
21
+ });
22
+
23
+ export const proposalDecisionSchema = z.strictObject({
24
+ decision: z.enum([
25
+ 'accept_new',
26
+ 'keep_current',
27
+ 'keep_both',
28
+ 'request_source',
29
+ ]),
30
+ reason: semanticGovernanceTextSchema,
31
+ });
32
+
33
+ export function parseJoinRequest(value) {
34
+ return parseProtocolValue(joinRequestSchema, value, 'Join request');
35
+ }
36
+
37
+ export function parseJoinDecision(value) {
38
+ return parseProtocolValue(joinDecisionSchema, value, 'Join decision');
39
+ }
40
+
41
+ export function parseProposalDecision(value) {
42
+ return parseProtocolValue(
43
+ proposalDecisionSchema,
44
+ value,
45
+ 'Proposal decision',
46
+ );
47
+ }
@@ -0,0 +1,53 @@
1
+ export {
2
+ FactCertainty,
3
+ FactState,
4
+ ProposalStatus,
5
+ ProviderCapability,
6
+ WORKSPACE_PROTOCOL_VERSION,
7
+ WorkspaceRole,
8
+ WorkspaceVisibility,
9
+ } from './constants.js';
10
+ export {
11
+ parseProtocolValue,
12
+ ProtocolValidationError,
13
+ } from './validation.js';
14
+ export {
15
+ ProtocolErrorCode,
16
+ protocolErrorResponseSchema,
17
+ } from './error-contract.js';
18
+ export {
19
+ parseProviderManifest,
20
+ parseWorkspaceDescriptor,
21
+ providerManifestSchema,
22
+ workspaceDescriptorSchema,
23
+ } from './manifest-contract.js';
24
+ export {
25
+ factProjectionSchema,
26
+ sourceReferenceSchema,
27
+ } from './fact-contract.js';
28
+ export {
29
+ contextPackSchema,
30
+ parseContextPack,
31
+ parseWorkspaceQuery,
32
+ workspaceQuerySchema,
33
+ } from './query-contract.js';
34
+ export {
35
+ parsePublicationProposal,
36
+ parsePublicationResult,
37
+ publicationProposalSchema,
38
+ publicationResultSchema,
39
+ publicationSigningBytes,
40
+ } from './publication-contract.js';
41
+ export {
42
+ parseSyncPage,
43
+ syncPageSchema,
44
+ workspaceEventSchema,
45
+ } from './sync-contract.js';
46
+ export {
47
+ joinDecisionSchema,
48
+ joinRequestSchema,
49
+ parseJoinDecision,
50
+ parseJoinRequest,
51
+ parseProposalDecision,
52
+ proposalDecisionSchema,
53
+ } from './governance-contract.js';
@@ -0,0 +1,19 @@
1
+ import { Buffer } from 'node:buffer';
2
+
3
+ export function utf8JsonByteLength(value) {
4
+ return Buffer.byteLength(JSON.stringify(value), 'utf8');
5
+ }
6
+
7
+ export function refineUtf8JsonByteBudget(
8
+ value,
9
+ context,
10
+ maxBytes,
11
+ label,
12
+ ) {
13
+ if (utf8JsonByteLength(value) > maxBytes) {
14
+ context.addIssue({
15
+ code: 'custom',
16
+ message: `${label} exceeds the aggregate UTF-8 JSON byte budget`,
17
+ });
18
+ }
19
+ }