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,143 @@
1
+ import { ApplicationError, ApplicationErrorCode } from '../app/application-error.js';
2
+ import { FactScope } from '../models.js';
3
+ import { requirePersonalSpace } from './lens-input.js';
4
+ import {
5
+ BOUNDARY_PREDICATES,
6
+ BOUNDARY_PREFIXES,
7
+ compareLensFacts,
8
+ taskTokens
9
+ } from './lens-query-ranking.js';
10
+ import {
11
+ isInjectableFact,
12
+ projectSearchFact,
13
+ safePersonalEpisode
14
+ } from './lens-query-projection.js';
15
+ import { LensSearchQuery } from './lens-search-query.js';
16
+
17
+ export { BOUNDARY_PREDICATES, BOUNDARY_PREFIXES };
18
+ export {
19
+ DEFAULT_SEARCH_USER_CONTEXT_LIMIT,
20
+ MAX_SEARCH_USER_CONTEXT_LIMIT
21
+ } from './lens-search-query.js';
22
+
23
+ export class LensQuery extends LensSearchQuery {
24
+ getUserLens(input) {
25
+ return this.#getUserLens(input, true);
26
+ }
27
+
28
+ #getUserLens({
29
+ personalSpaceId,
30
+ task,
31
+ budget,
32
+ includeObserved = true,
33
+ includeSuggested = false,
34
+ includeRestricted = false
35
+ }, redactRelations) {
36
+ requirePersonalSpace(this.store, personalSpaceId);
37
+ validateTask(task);
38
+ validateBudget(budget);
39
+
40
+ const terms = taskTokens(task);
41
+ const allowRestricted = includeRestricted === true;
42
+ const facts = this.store.currentFacts(personalSpaceId)
43
+ .filter((fact) => fact.spaceId === personalSpaceId && fact.scope === FactScope.PERSONAL)
44
+ .filter((fact) => isInjectableFact(fact, {
45
+ includeObserved,
46
+ includeSuggested,
47
+ includeRestricted: allowRestricted
48
+ }))
49
+ .sort((left, right) => compareLensFacts(left, right, terms));
50
+ const selected = [];
51
+ const lines = [];
52
+ let bytes = 0;
53
+
54
+ for (const fact of facts) {
55
+ const line = formatFactLine(fact);
56
+ const nextBytes = bytes + (lines.length ? 1 : 0) + Buffer.byteLength(line, 'utf8');
57
+ if (nextBytes > budget) continue;
58
+ selected.push(fact);
59
+ lines.push(line);
60
+ bytes = nextBytes;
61
+ }
62
+
63
+ return {
64
+ personalSpaceId,
65
+ task,
66
+ text: lines.join('\n'),
67
+ facts: redactRelations
68
+ ? selected.map((fact) => {
69
+ const source = this.store.episodeEvidencePreview(
70
+ personalSpaceId,
71
+ fact.sourceEpisodeId,
72
+ { includeRestricted: allowRestricted, maxBodyBytes: 0 }
73
+ );
74
+ return projectSearchFact(
75
+ this.store,
76
+ fact,
77
+ source,
78
+ personalSpaceId,
79
+ new Map(),
80
+ allowRestricted
81
+ ).fact;
82
+ })
83
+ : selected,
84
+ estimatedTokens: bytes,
85
+ truncated: selected.length < facts.length
86
+ };
87
+ }
88
+
89
+ getUserLensView(input) {
90
+ return this.store.transaction(() => {
91
+ const lens = this.#getUserLens(input, false);
92
+ const entries = [];
93
+ for (const fact of lens.facts) {
94
+ const source = safePersonalEpisode(
95
+ this.store.episodeEvidencePreview(
96
+ lens.personalSpaceId,
97
+ fact.sourceEpisodeId,
98
+ { includeRestricted: input.includeRestricted === true, maxBodyBytes: 0 }
99
+ ),
100
+ lens.personalSpaceId,
101
+ input.includeRestricted === true
102
+ );
103
+ if (source) entries.push(projectLensViewEntry(fact, source));
104
+ }
105
+ return {
106
+ entries,
107
+ truncated: lens.truncated || entries.length < lens.facts.length
108
+ };
109
+ });
110
+ }
111
+
112
+ }
113
+
114
+ function validateTask(task) {
115
+ if (typeof task !== 'string') {
116
+ throw new ApplicationError(ApplicationErrorCode.VALIDATION, 'Task must be a string');
117
+ }
118
+ }
119
+
120
+ function validateBudget(budget) {
121
+ if (!Number.isInteger(budget) || budget <= 0) {
122
+ throw new ApplicationError(ApplicationErrorCode.VALIDATION, 'Budget must be a positive integer');
123
+ }
124
+ }
125
+
126
+ function formatFactLine(fact) {
127
+ return `${fact.subject} ${fact.predicate} ${fact.object} (${fact.status})`;
128
+ }
129
+
130
+ function projectLensViewEntry(fact, source) {
131
+ return {
132
+ subject: fact.subject,
133
+ predicate: fact.predicate,
134
+ object: fact.object,
135
+ status: fact.status,
136
+ validAt: fact.validAt,
137
+ source: {
138
+ kind: source.sourceKind,
139
+ uri: source.sourceUri,
140
+ createdAt: source.createdAt
141
+ }
142
+ };
143
+ }
@@ -0,0 +1,93 @@
1
+ import { detectSensitiveContent } from '../security/sensitive-content.js';
2
+
3
+ export function projectCurrentFact(fact) {
4
+ return compactFact(fact);
5
+ }
6
+
7
+ export function isSafeResourceFact(fact) {
8
+ return compactFact(fact) !== null;
9
+ }
10
+
11
+ export function projectHistoryItem(item) {
12
+ const fact = compactFact(item.fact);
13
+ if (!fact) return null;
14
+ return {
15
+ current: item.current,
16
+ fact,
17
+ source: compactEpisode(item.sourceEpisode),
18
+ replacementFact: compactFact(item.replacementFact),
19
+ correctionEpisodes: item.correctionEpisodes.map(compactCorrection).filter(Boolean)
20
+ };
21
+ }
22
+
23
+ export function projectSubscription(subscription, space) {
24
+ if (![
25
+ space?.id,
26
+ space?.name,
27
+ space?.kind,
28
+ subscription?.mode,
29
+ subscription?.createdAt
30
+ ].every(isSafeResourceText)) return null;
31
+ const compactSpace = { id: space.id, name: space.name, kind: space.kind };
32
+ if (isSafeResourceText(space.description)) {
33
+ compactSpace.description = space.description;
34
+ }
35
+ return {
36
+ space: compactSpace,
37
+ mode: subscription.mode,
38
+ createdAt: subscription.createdAt
39
+ };
40
+ }
41
+
42
+ function compactFact(fact) {
43
+ if (!fact || ![
44
+ fact.id,
45
+ fact.subject,
46
+ fact.predicate,
47
+ fact.object,
48
+ fact.status,
49
+ fact.validAt
50
+ ].every(isSafeResourceText)) return null;
51
+ if (!Number.isFinite(fact.confidence) || fact.confidence < 0 || fact.confidence > 1) {
52
+ return null;
53
+ }
54
+ return {
55
+ id: fact.id,
56
+ subject: fact.subject,
57
+ predicate: fact.predicate,
58
+ object: fact.object,
59
+ status: fact.status,
60
+ confidence: fact.confidence,
61
+ validAt: fact.validAt
62
+ };
63
+ }
64
+
65
+ function compactEpisode(episode) {
66
+ if (!episode) return null;
67
+ if (![
68
+ episode.id,
69
+ episode.sourceKind,
70
+ episode.createdAt
71
+ ].every(isSafeResourceText)) return null;
72
+ if (!isSafeResourceText(episode.sourceUri, { nullable: true })) return null;
73
+ return {
74
+ id: episode.id,
75
+ kind: episode.sourceKind,
76
+ uri: episode.sourceUri,
77
+ createdAt: episode.createdAt
78
+ };
79
+ }
80
+
81
+ function compactCorrection(episode) {
82
+ const compact = compactEpisode(episode);
83
+ if (!compact || !isSafeResourceText(episode.metadata?.action)) return null;
84
+ return {
85
+ ...compact,
86
+ action: episode.metadata.action
87
+ };
88
+ }
89
+
90
+ export function isSafeResourceText(value, { nullable = false } = {}) {
91
+ if (value === null || value === undefined) return nullable;
92
+ return typeof value === 'string' && !detectSensitiveContent(value).restricted;
93
+ }
@@ -0,0 +1,45 @@
1
+ const EMPTY_ARRAY_BYTES = 2;
2
+ const HISTORY_ENVELOPE_RESERVE_BYTES = 1024;
3
+
4
+ export function selectCurrentFacts(facts, budgetBytes) {
5
+ const selected = [];
6
+ const lines = [];
7
+ let bytes = 0;
8
+ for (const fact of facts) {
9
+ const line = `${fact.subject} ${fact.predicate} ${fact.object} (${fact.status})`;
10
+ const nextBytes = bytes + (lines.length ? 1 : 0) + Buffer.byteLength(line, 'utf8');
11
+ if (nextBytes > budgetBytes) continue;
12
+ selected.push(fact);
13
+ lines.push(line);
14
+ bytes = nextBytes;
15
+ }
16
+ return {
17
+ facts: selected,
18
+ text: lines.join('\n'),
19
+ truncated: selected.length < facts.length
20
+ };
21
+ }
22
+
23
+ export function selectHistoryItems(items, { limit, budgetBytes }) {
24
+ const selected = [];
25
+ const itemsBudget = budgetBytes - HISTORY_ENVELOPE_RESERVE_BYTES;
26
+ let itemsBytes = EMPTY_ARRAY_BYTES;
27
+ let truncated = false;
28
+
29
+ for (const item of items) {
30
+ if (selected.length >= limit) {
31
+ truncated = true;
32
+ break;
33
+ }
34
+ const itemBytes = Buffer.byteLength(JSON.stringify(item), 'utf8');
35
+ const nextItemsBytes = itemsBytes + (selected.length ? 1 : 0) + itemBytes;
36
+ if (nextItemsBytes > itemsBudget) {
37
+ truncated = true;
38
+ continue;
39
+ }
40
+ selected.push(item);
41
+ itemsBytes = nextItemsBytes;
42
+ }
43
+
44
+ return { items: selected, itemsBytes, truncated };
45
+ }
@@ -0,0 +1,103 @@
1
+ import { SpaceKind } from '../models.js';
2
+ import {
3
+ isSafeResourceFact,
4
+ isSafeResourceText,
5
+ projectCurrentFact,
6
+ projectHistoryItem,
7
+ projectSubscription
8
+ } from './lens-resource-projection.js';
9
+ import { selectCurrentFacts, selectHistoryItems } from './lens-resource-selection.js';
10
+
11
+ export const CURRENT_LENS_BUDGET_BYTES = 16 * 1024;
12
+ export const HISTORY_BUDGET_BYTES = 64 * 1024;
13
+ export const HISTORY_LIMIT = 100;
14
+
15
+ export class LensResourceService {
16
+ constructor({ store, lens, activePersonalSpace }) {
17
+ this.store = store;
18
+ this.lens = lens;
19
+ this.activePersonalSpace = activePersonalSpace;
20
+ }
21
+
22
+ current() {
23
+ const personal = this.activePersonalSpace();
24
+ const result = this.lens.getUserLens({
25
+ personalSpaceId: personal.id,
26
+ task: '',
27
+ budget: CURRENT_LENS_BUDGET_BYTES,
28
+ includeObserved: true,
29
+ includeSuggested: false,
30
+ includeRestricted: false
31
+ });
32
+ const projected = result.facts.map(projectCurrentFact).filter(Boolean);
33
+ const selected = selectCurrentFacts(projected, CURRENT_LENS_BUDGET_BYTES);
34
+ return safePayload({
35
+ personalSpaceId: personal.id,
36
+ facts: selected.facts,
37
+ text: selected.text,
38
+ truncated: result.truncated ||
39
+ projected.length < result.facts.length ||
40
+ selected.truncated,
41
+ budget: {
42
+ encoding: 'utf-8',
43
+ limitBytes: CURRENT_LENS_BUDGET_BYTES,
44
+ usedBytes: Buffer.byteLength(selected.text, 'utf8')
45
+ }
46
+ });
47
+ }
48
+
49
+ history() {
50
+ const personal = this.activePersonalSpace();
51
+ const result = this.lens.searchUserContext({
52
+ personalSpaceId: personal.id,
53
+ query: '',
54
+ includeHistorical: true,
55
+ includeRestricted: false,
56
+ maxFactObjectBytes: HISTORY_BUDGET_BYTES,
57
+ maxEvidenceBodyBytes: 0,
58
+ scanLimit: HISTORY_LIMIT * 2,
59
+ factFilter: isSafeResourceFact,
60
+ limit: HISTORY_LIMIT
61
+ });
62
+ const safeItems = result.facts.map(projectHistoryItem).filter(Boolean);
63
+ const selected = selectHistoryItems(safeItems, {
64
+ limit: HISTORY_LIMIT,
65
+ budgetBytes: HISTORY_BUDGET_BYTES
66
+ });
67
+ return safePayload({
68
+ personalSpaceId: personal.id,
69
+ limit: HISTORY_LIMIT,
70
+ count: selected.items.length,
71
+ truncated: result.truncated ||
72
+ safeItems.length < result.facts.length ||
73
+ selected.truncated,
74
+ budget: {
75
+ encoding: 'utf-8',
76
+ limitBytes: HISTORY_BUDGET_BYTES,
77
+ itemsBytes: selected.itemsBytes
78
+ },
79
+ items: selected.items
80
+ }, HISTORY_BUDGET_BYTES);
81
+ }
82
+
83
+ subscribed() {
84
+ const personal = this.activePersonalSpace();
85
+ const subscriptions = this.store.subscriptionsFor(personal.id)
86
+ .map((subscription) => ({ subscription, space: this.store.getSpace(subscription.spaceId) }))
87
+ .filter(({ space }) => space?.kind === SpaceKind.PUBLIC)
88
+ .map(({ subscription, space }) => projectSubscription(subscription, space))
89
+ .filter(Boolean);
90
+ return safePayload({ personalSpaceId: personal.id, subscriptions });
91
+ }
92
+ }
93
+
94
+ function safePayload(payload, limitBytes = null) {
95
+ const serialized = JSON.stringify(payload);
96
+ if (!isSafeResourceText(serialized)) {
97
+ throw new Error('Unsafe resource projection');
98
+ }
99
+ if (limitBytes && Buffer.byteLength(serialized, 'utf8') > limitBytes) {
100
+ throw new Error('Resource projection exceeds its byte budget');
101
+ }
102
+ return payload;
103
+ }
@@ -0,0 +1,122 @@
1
+ import { ApplicationError, ApplicationErrorCode } from '../app/application-error.js';
2
+ import { FactScope } from '../models.js';
3
+ import { MAX_EPISODE_EVIDENCE_BODY_BYTES } from '../storage/episode-evidence-preview.js';
4
+ import { assertStorePort } from '../storage/store-port.js';
5
+ import { requirePersonalSpace } from './lens-input.js';
6
+ import {
7
+ buildCorrectionIndex,
8
+ isSearchableFact,
9
+ projectSearchFact
10
+ } from './lens-query-projection.js';
11
+
12
+ export const DEFAULT_SEARCH_USER_CONTEXT_LIMIT = 20;
13
+ export const MAX_SEARCH_USER_CONTEXT_LIMIT = 100;
14
+ const MAX_SEARCH_USER_CONTEXT_SCAN_LIMIT = 200;
15
+
16
+ export class LensSearchQuery {
17
+ constructor(store) {
18
+ assertStorePort(store);
19
+ this.store = store;
20
+ }
21
+
22
+ searchUserContext({
23
+ personalSpaceId,
24
+ query,
25
+ includeHistorical = false,
26
+ includeRestricted = false,
27
+ requireSafeSource = false,
28
+ maxFactObjectBytes = null,
29
+ maxEvidenceBodyBytes = MAX_EPISODE_EVIDENCE_BODY_BYTES,
30
+ limit = DEFAULT_SEARCH_USER_CONTEXT_LIMIT,
31
+ scanLimit = limit,
32
+ factFilter = null
33
+ }) {
34
+ requirePersonalSpace(this.store, personalSpaceId);
35
+ validateQuery(query);
36
+ validateSearchLimit(limit);
37
+ validateScanOptions(scanLimit, limit, factFilter);
38
+
39
+ const allowRestricted = includeRestricted === true;
40
+ const page = this.store.searchFactsPage([personalSpaceId], query, {
41
+ includeHistorical,
42
+ scope: FactScope.PERSONAL,
43
+ excludeRestricted: !allowRestricted,
44
+ safeSourceSpaceId: requireSafeSource === true ? personalSpaceId : null,
45
+ maxObjectBytes: maxFactObjectBytes,
46
+ limit: scanLimit + 1
47
+ });
48
+ const candidates = page.facts;
49
+ const scanned = candidates.slice(0, scanLimit);
50
+ const facts = scanned
51
+ .filter((fact) => fact.spaceId === personalSpaceId && fact.scope === FactScope.PERSONAL)
52
+ .filter((fact) => isSearchableFact(fact, {
53
+ includeHistorical,
54
+ includeRestricted: allowRestricted
55
+ }))
56
+ .filter((fact) => factFilter?.(fact) ?? true);
57
+ const selected = facts.slice(0, limit);
58
+ const correctionIndex = buildCorrectionIndex(
59
+ this.store.correctionEpisodeEvidencePreviews(
60
+ personalSpaceId,
61
+ selected.map((fact) => fact.id),
62
+ { includeRestricted: allowRestricted, maxBodyBytes: maxEvidenceBodyBytes }
63
+ ),
64
+ personalSpaceId,
65
+ allowRestricted
66
+ );
67
+ return {
68
+ personalSpaceId,
69
+ query,
70
+ truncated: page.objectLimitTruncated ||
71
+ candidates.length > scanLimit ||
72
+ facts.length > limit ||
73
+ facts.length < scanned.length,
74
+ facts: selected.map((fact) => {
75
+ const source = this.store.episodeEvidencePreview(
76
+ personalSpaceId,
77
+ fact.sourceEpisodeId,
78
+ { includeRestricted: allowRestricted, maxBodyBytes: maxEvidenceBodyBytes }
79
+ );
80
+ return projectSearchFact(
81
+ this.store,
82
+ fact,
83
+ source,
84
+ personalSpaceId,
85
+ correctionIndex,
86
+ allowRestricted
87
+ );
88
+ })
89
+ };
90
+ }
91
+ }
92
+
93
+ function validateScanOptions(scanLimit, limit, factFilter) {
94
+ if (
95
+ !Number.isInteger(scanLimit) ||
96
+ scanLimit < limit ||
97
+ scanLimit > MAX_SEARCH_USER_CONTEXT_SCAN_LIMIT
98
+ ) {
99
+ throw new ApplicationError(
100
+ ApplicationErrorCode.VALIDATION,
101
+ `Scan limit must be an integer between the result limit and ${MAX_SEARCH_USER_CONTEXT_SCAN_LIMIT}`
102
+ );
103
+ }
104
+ if (factFilter !== null && typeof factFilter !== 'function') {
105
+ throw new ApplicationError(ApplicationErrorCode.VALIDATION, 'Fact filter must be a function');
106
+ }
107
+ }
108
+
109
+ function validateQuery(query) {
110
+ if (typeof query !== 'string') {
111
+ throw new ApplicationError(ApplicationErrorCode.VALIDATION, 'Query must be a string');
112
+ }
113
+ }
114
+
115
+ function validateSearchLimit(limit) {
116
+ if (!Number.isInteger(limit) || limit < 1 || limit > MAX_SEARCH_USER_CONTEXT_LIMIT) {
117
+ throw new ApplicationError(
118
+ ApplicationErrorCode.VALIDATION,
119
+ `Limit must be an integer between 1 and ${MAX_SEARCH_USER_CONTEXT_LIMIT}`
120
+ );
121
+ }
122
+ }
@@ -0,0 +1,183 @@
1
+ import { ApplicationError, ApplicationErrorCode } from '../app/application-error.js';
2
+ import { FactScope, FactStatus, Sensitivity } from '../models.js';
3
+ import { assertSafeSourceMetadata } from '../security/source-metadata-policy.js';
4
+ import { assertStorePort } from '../storage/store-port.js';
5
+ import { writePersonalFact, writePersonalFactSpecs } from './lens-fact-writer.js';
6
+ import { assertSafeLensTexts, requirePersonalSpace, validateLensWriteInput } from './lens-input.js';
7
+ import { LensCorrectionService } from './lens-correction-service.js';
8
+ const OBSERVATION_INFERENCES = new Set(['direct', 'inferred']);
9
+ const IMMEDIATE = { mode: 'immediate' };
10
+ const SOURCE_METADATA_ERROR = 'Sensitive source metadata is not allowed in Personal Lens';
11
+
12
+ export class LensService {
13
+ #corrections;
14
+
15
+ constructor(store) {
16
+ assertStorePort(store);
17
+ this.store = store;
18
+ this.#corrections = new LensCorrectionService(store);
19
+ }
20
+ rememberUserFact({
21
+ personalSpaceId,
22
+ subject = 'user',
23
+ predicate,
24
+ value,
25
+ sourceText,
26
+ sourceKind = 'conversation',
27
+ sensitivity = Sensitivity.NORMAL,
28
+ confidence = 1
29
+ }) {
30
+ assertSafeSourceMetadata(sourceKind, null, SOURCE_METADATA_ERROR);
31
+ this.#validate(personalSpaceId, [subject, predicate, value, sourceText], sensitivity, confidence);
32
+ return this.store.transaction(() => {
33
+ const episode = this.store.addEpisode(personalSpaceId, sourceKind, sourceText);
34
+ const fact = this.#writeFact({
35
+ personalSpaceId, subject, predicate, value, episode,
36
+ status: FactStatus.CONFIRMED, sensitivity, confidence
37
+ });
38
+ return { episode, fact };
39
+ }, IMMEDIATE);
40
+ }
41
+ rememberUserFactsFromSourceInCurrentTransaction({
42
+ personalSpaceId,
43
+ subject = 'user',
44
+ specs,
45
+ sourceText,
46
+ sourceKind,
47
+ sourceUri = null,
48
+ sensitivity = Sensitivity.NORMAL,
49
+ confidence = 1
50
+ }) {
51
+ assertSafeSourceMetadata(sourceKind, sourceUri, SOURCE_METADATA_ERROR);
52
+ const specTexts = specs.flatMap(specTextsForValidation);
53
+ this.#validate(
54
+ personalSpaceId,
55
+ [subject, sourceText, ...specTexts],
56
+ sensitivity,
57
+ confidence
58
+ );
59
+ const episode = this.store.addEpisode(personalSpaceId, sourceKind, sourceText, sourceUri);
60
+ const facts = writePersonalFactSpecs(this.store, {
61
+ spaceId: personalSpaceId,
62
+ subject,
63
+ specs,
64
+ sourceEpisodeId: episode.id,
65
+ sensitivity,
66
+ confidence
67
+ });
68
+ return { episode, facts };
69
+ }
70
+ writeUserFactsForEpisodeInCurrentTransaction({
71
+ personalSpaceId,
72
+ subject = 'user',
73
+ specs,
74
+ episode,
75
+ sensitivity = Sensitivity.NORMAL,
76
+ confidence = 1
77
+ }) {
78
+ assertSafeSourceMetadata(episode?.sourceKind, episode?.sourceUri, SOURCE_METADATA_ERROR);
79
+ if (episode.spaceId !== personalSpaceId) {
80
+ throw new ApplicationError(ApplicationErrorCode.VALIDATION, 'Lens source episode belongs to another space');
81
+ }
82
+ this.#validate(
83
+ personalSpaceId,
84
+ [subject, episode.body, ...specs.flatMap(specTextsForValidation)],
85
+ sensitivity,
86
+ confidence
87
+ );
88
+ const facts = writePersonalFactSpecs(this.store, {
89
+ spaceId: personalSpaceId,
90
+ subject,
91
+ specs,
92
+ sourceEpisodeId: episode.id,
93
+ sensitivity,
94
+ confidence
95
+ });
96
+ return { episode, facts };
97
+ }
98
+ submitUserObservation({
99
+ personalSpaceId,
100
+ subject = 'user',
101
+ predicate,
102
+ value,
103
+ evidenceText,
104
+ inference,
105
+ sourceKind = 'observation',
106
+ sensitivity = Sensitivity.NORMAL,
107
+ confidence = inference === 'inferred' ? 0.5 : 1
108
+ }) {
109
+ assertSafeSourceMetadata(sourceKind, null, SOURCE_METADATA_ERROR);
110
+ this.#validate(personalSpaceId, [subject, predicate, value, evidenceText], sensitivity, confidence);
111
+ if (!OBSERVATION_INFERENCES.has(inference)) {
112
+ throw new ApplicationError(
113
+ ApplicationErrorCode.VALIDATION,
114
+ 'Observation inference must be direct or inferred'
115
+ );
116
+ }
117
+ return this.store.transaction(() => {
118
+ const episode = this.store.addEpisode(personalSpaceId, sourceKind, evidenceText);
119
+ const fact = this.#writeFact({
120
+ personalSpaceId, subject, predicate, value, episode,
121
+ status: inference === 'inferred' ? FactStatus.SUGGESTED : FactStatus.OBSERVED,
122
+ sensitivity, confidence
123
+ });
124
+ return { episode, fact };
125
+ }, IMMEDIATE);
126
+ }
127
+ confirmObservation({ personalSpaceId, factId, sourceText, sourceKind = 'confirmation' }) {
128
+ assertSafeSourceMetadata(sourceKind, null, SOURCE_METADATA_ERROR);
129
+ requirePersonalSpace(this.store, personalSpaceId);
130
+ assertSafeLensTexts([sourceText]);
131
+ return this.store.transaction(() => {
132
+ const original = this.#findConfirmableFact(personalSpaceId, factId);
133
+ assertSafeLensTexts([original.subject, original.predicate, original.object]);
134
+ const episode = this.store.addEpisode(personalSpaceId, sourceKind, sourceText);
135
+ const fact = this.#writeFact({
136
+ personalSpaceId,
137
+ subject: original.subject,
138
+ predicate: original.predicate,
139
+ value: original.object,
140
+ episode,
141
+ status: FactStatus.CONFIRMED,
142
+ sensitivity: original.sensitivity,
143
+ confidence: original.confidence
144
+ });
145
+ return { episode, fact, replacedFact: original };
146
+ }, IMMEDIATE);
147
+ }
148
+ correctUserFact(input) {
149
+ assertSafeSourceMetadata(input.sourceKind, null, SOURCE_METADATA_ERROR);
150
+ return this.#corrections.correctUserFact(input);
151
+ }
152
+
153
+ #writeFact(input) {
154
+ return writePersonalFact(this.store, {
155
+ spaceId: input.personalSpaceId,
156
+ subject: input.subject,
157
+ predicate: input.predicate,
158
+ object: input.value,
159
+ sourceEpisodeId: input.episode.id,
160
+ status: input.status,
161
+ sensitivity: input.sensitivity,
162
+ confidence: input.confidence
163
+ });
164
+ }
165
+ #validate(personalSpaceId, texts, sensitivity, confidence) {
166
+ validateLensWriteInput(this.store, { personalSpaceId, texts, sensitivity, confidence });
167
+ }
168
+
169
+ #findConfirmableFact(personalSpaceId, factId) {
170
+ const fact = this.store.getFact(factId);
171
+ if (!fact || fact.spaceId !== personalSpaceId || fact.scope !== FactScope.PERSONAL) {
172
+ throw new ApplicationError(ApplicationErrorCode.NOT_FOUND, `Observation not found: ${factId}`);
173
+ }
174
+ if (fact.invalidAt || ![FactStatus.OBSERVED, FactStatus.SUGGESTED].includes(fact.status)) {
175
+ throw new ApplicationError(ApplicationErrorCode.VALIDATION, `Observation cannot be confirmed: ${factId}`);
176
+ }
177
+ return fact;
178
+ }
179
+ }
180
+ function specTextsForValidation(spec) {
181
+ return [spec.subject, spec.predicate, spec.object, spec.oldValue, spec.newValue]
182
+ .filter(value => value !== undefined);
183
+ }