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,29 @@
1
+ // Keep local API request buffering at or below a fixed 64 KiB budget.
2
+ export const MAX_JSON_BODY_BYTES = 64 * 1024;
3
+
4
+ export class JsonBodyTooLargeError extends Error {
5
+ constructor() {
6
+ super(`JSON request body exceeds ${MAX_JSON_BODY_BYTES} bytes`);
7
+ this.name = 'JsonBodyTooLargeError';
8
+ }
9
+ }
10
+
11
+ export async function readJson(request) {
12
+ const chunks = [];
13
+ let receivedBytes = 0;
14
+ for await (const chunk of request) {
15
+ receivedBytes += chunk.length;
16
+ if (receivedBytes > MAX_JSON_BODY_BYTES) {
17
+ chunks.length = 0;
18
+ throw new JsonBodyTooLargeError();
19
+ }
20
+ chunks.push(chunk);
21
+ }
22
+ const raw = Buffer.concat(chunks).toString('utf8');
23
+ return raw ? JSON.parse(raw) : {};
24
+ }
25
+
26
+ export function sendJson(response, status, body) {
27
+ response.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
28
+ response.end(JSON.stringify(body));
29
+ }
@@ -0,0 +1,55 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { extname, isAbsolute, join, normalize, relative, resolve, sep } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const PROJECT_ROOT = resolve(fileURLToPath(new URL('../..', import.meta.url)));
6
+ export const WEB_ROOT = join(PROJECT_ROOT, 'dist', 'web');
7
+ const MIME_TYPES = {
8
+ '.html': 'text/html; charset=utf-8',
9
+ '.css': 'text/css; charset=utf-8',
10
+ '.js': 'text/javascript; charset=utf-8',
11
+ '.map': 'application/json; charset=utf-8',
12
+ '.json': 'application/json; charset=utf-8',
13
+ '.png': 'image/png',
14
+ '.svg': 'image/svg+xml',
15
+ '.webp': 'image/webp',
16
+ '.woff2': 'font/woff2'
17
+ };
18
+
19
+ export function serveStatic(pathname, response) {
20
+ if (pathname === '/favicon.ico') {
21
+ response.writeHead(204);
22
+ response.end();
23
+ return;
24
+ }
25
+
26
+ const relativePath = pathname === '/' ? 'index.html' : pathname.slice(1);
27
+ const requestedFile = normalize(join(WEB_ROOT, relativePath));
28
+ const pathFromRoot = relative(WEB_ROOT, requestedFile);
29
+ if (pathFromRoot === '..' || pathFromRoot.startsWith(`..${sep}`) ||
30
+ isAbsolute(pathFromRoot)) {
31
+ response.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
32
+ response.end('Not found');
33
+ return;
34
+ }
35
+
36
+ const filePath = existsSync(requestedFile)
37
+ ? requestedFile
38
+ : shouldServeApplication(pathname)
39
+ ? join(WEB_ROOT, 'index.html')
40
+ : null;
41
+ if (!filePath || !existsSync(filePath)) {
42
+ response.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
43
+ response.end('Not found');
44
+ return;
45
+ }
46
+
47
+ response.writeHead(200, {
48
+ 'content-type': MIME_TYPES[extname(filePath)] ?? 'application/octet-stream'
49
+ });
50
+ response.end(readFileSync(filePath));
51
+ }
52
+
53
+ function shouldServeApplication(pathname) {
54
+ return extname(pathname) === '' && !pathname.startsWith('/api/');
55
+ }
@@ -0,0 +1,161 @@
1
+ import { classifyEpisode } from './classifier.js';
2
+ import { extractFactSpecs } from './extractor.js';
3
+ import { FactService } from './facts/fact-service.js';
4
+ import { LensService } from './lens/lens-service.js';
5
+ import { ApplicationError, ApplicationErrorCode } from './app/application-error.js';
6
+ import { CandidateStatus, FactScope, PublishRoute, SpaceKind } from './models.js';
7
+ import { detectSensitiveContent } from './security/sensitive-content.js';
8
+ import { assertSafeSourceMetadata } from './security/source-metadata-policy.js';
9
+ import { PublicationService } from './publication/publication-service.js';
10
+
11
+ export class IngestionService {
12
+ constructor(store, {
13
+ facts = new FactService(store),
14
+ lens = new LensService(store),
15
+ publication = new PublicationService(store)
16
+ } = {}) {
17
+ this.store = store;
18
+ this.facts = facts;
19
+ this.lens = lens;
20
+ this.publication = publication;
21
+ }
22
+
23
+ remember(input) {
24
+ this.#assertSafeInput(input);
25
+ return this.store.transaction(
26
+ () => this.rememberInCurrentTransaction(input),
27
+ { mode: 'immediate' }
28
+ );
29
+ }
30
+
31
+ rememberInCurrentTransaction({
32
+ personalSpaceId,
33
+ sourceKind,
34
+ body,
35
+ targetSpaceId = null,
36
+ sourceUri = null
37
+ }) {
38
+ this.#assertSafeInput({ body, sourceKind, sourceUri });
39
+ const route = classifyEpisode({ sourceKind, body });
40
+
41
+ if (route === PublishRoute.PERSONAL) {
42
+ const result = this.lens.rememberUserFactsFromSourceInCurrentTransaction({
43
+ personalSpaceId,
44
+ subject: this.#spaceName(personalSpaceId),
45
+ specs: extractFactSpecs(this.#spaceName(personalSpaceId), body),
46
+ sourceText: body,
47
+ sourceKind,
48
+ sourceUri
49
+ });
50
+ return { route, ...result };
51
+ }
52
+
53
+ const personalEpisode = this.store.addEpisode(personalSpaceId, sourceKind, body, sourceUri);
54
+
55
+ if (route === PublishRoute.PUBLIC && targetSpaceId) {
56
+ const targetEpisode = this.store.addEpisode(targetSpaceId, sourceKind, body, sourceUri);
57
+ const facts = this.facts.writeSpecs({
58
+ spaceId: targetSpaceId,
59
+ subject: this.#spaceName(targetSpaceId),
60
+ episodeId: targetEpisode.id,
61
+ specs: extractFactSpecs(this.#spaceName(targetSpaceId), body),
62
+ scope: FactScope.PUBLIC
63
+ });
64
+ const publication = facts.length === 0
65
+ ? null
66
+ : this.publication.prepareInCurrentTransaction({
67
+ spaceId: targetSpaceId,
68
+ episode: targetEpisode,
69
+ facts
70
+ });
71
+ return { route, episode: targetEpisode, publication };
72
+ }
73
+
74
+ const candidate = this.store.addCandidate({
75
+ personalSpaceId,
76
+ targetSpaceId,
77
+ episodeId: personalEpisode.id,
78
+ reason: 'not safe enough to publish automatically',
79
+ status: CandidateStatus.PENDING
80
+ });
81
+ return { route: PublishRoute.CANDIDATE, episode: personalEpisode, candidate };
82
+ }
83
+
84
+ publishConfirmed(input) {
85
+ this.#assertSafeInput(input);
86
+ return this.store.transaction(
87
+ () => this.publishConfirmedInCurrentTransaction(input),
88
+ { mode: 'immediate' }
89
+ );
90
+ }
91
+
92
+ publishConfirmedInCurrentTransaction({ targetSpaceId, sourceKind, body, sourceUri = null }) {
93
+ this.#assertSafeInput({ body, sourceKind, sourceUri });
94
+ const targetEpisode = this.store.addEpisode(targetSpaceId, sourceKind, body, sourceUri);
95
+ const result = this.confirmEpisodeInCurrentTransaction({
96
+ spaceId: targetSpaceId,
97
+ episode: targetEpisode
98
+ });
99
+ return result;
100
+ }
101
+
102
+ confirmEpisode(input) {
103
+ this.#assertSafeEpisode(input.episode);
104
+ return this.store.transaction(
105
+ () => this.confirmEpisodeInCurrentTransaction(input),
106
+ { mode: 'immediate' }
107
+ );
108
+ }
109
+
110
+ confirmEpisodeInCurrentTransaction({ spaceId, episode }) {
111
+ this.#assertSafeEpisode(episode);
112
+ const subject = this.#spaceName(spaceId);
113
+ const specs = extractFactSpecs(subject, episode.body);
114
+ if (this.store.getSpace(spaceId)?.kind === SpaceKind.PERSONAL) {
115
+ return this.lens.writeUserFactsForEpisodeInCurrentTransaction({
116
+ personalSpaceId: spaceId,
117
+ subject,
118
+ specs,
119
+ episode
120
+ });
121
+ }
122
+ const facts = this.facts.writeSpecs({
123
+ spaceId,
124
+ subject,
125
+ episodeId: episode.id,
126
+ specs,
127
+ scope: FactScope.PUBLIC
128
+ });
129
+ const publication = facts.length === 0
130
+ ? null
131
+ : this.publication.prepareInCurrentTransaction({ spaceId, episode, facts });
132
+ return { episode, publication };
133
+ }
134
+
135
+ #spaceName(spaceId) {
136
+ return this.store.getSpace(spaceId)?.name ?? spaceId;
137
+ }
138
+
139
+ #assertSafeInput({ body, sourceKind, sourceUri }) {
140
+ this.#assertSafeBody(body);
141
+ assertSafeSourceMetadata(
142
+ sourceKind,
143
+ sourceUri,
144
+ 'Sensitive source metadata is not allowed in ingestion'
145
+ );
146
+ }
147
+
148
+ #assertSafeEpisode(episode) {
149
+ this.#assertSafeInput(episode);
150
+ }
151
+
152
+ #assertSafeBody(body) {
153
+ if (detectSensitiveContent(body).restricted) {
154
+ throw new ApplicationError(
155
+ ApplicationErrorCode.VALIDATION,
156
+ 'Sensitive content is not allowed in ingestion'
157
+ );
158
+ }
159
+ }
160
+
161
+ }
@@ -0,0 +1,81 @@
1
+ import { nowIso, FactScope, FactStatus, isCurrentFact } from '../models.js';
2
+ import { ApplicationError, ApplicationErrorCode } from '../app/application-error.js';
3
+ import { assertStorePort } from '../storage/store-port.js';
4
+ import { writePersonalFact } from './lens-fact-writer.js';
5
+ import { assertSafeLensTexts, requirePersonalSpace } from './lens-input.js';
6
+
7
+ const ACTIONS = new Set(['replace', 'reject', 'deprecate']);
8
+ const CORRECTABLE_STATUSES = new Set([
9
+ FactStatus.CONFIRMED,
10
+ FactStatus.OBSERVED,
11
+ FactStatus.SUGGESTED
12
+ ]);
13
+ const IMMEDIATE = { mode: 'immediate' };
14
+
15
+ export class LensCorrectionService {
16
+ constructor(store) {
17
+ assertStorePort(store);
18
+ this.store = store;
19
+ }
20
+
21
+ correctUserFact({ personalSpaceId, factId, action, value, sourceText, sourceKind = 'correction' }) {
22
+ if (!ACTIONS.has(action)) {
23
+ throw new ApplicationError(ApplicationErrorCode.VALIDATION, `Unknown correction action: ${action}`);
24
+ }
25
+
26
+ requirePersonalSpace(this.store, personalSpaceId);
27
+ assertSafeLensTexts([sourceText]);
28
+ if (action === 'replace') assertSafeLensTexts([value]);
29
+
30
+ const original = this.#requireCurrentFact(personalSpaceId, factId);
31
+ if (action === 'replace') assertSafeLensTexts([original.subject, original.predicate]);
32
+
33
+ return this.store.transaction(() => {
34
+ requirePersonalSpace(this.store, personalSpaceId);
35
+ const current = this.#requireCurrentFact(personalSpaceId, factId);
36
+ assertSafeLensTexts([sourceText]);
37
+ if (action === 'replace') assertSafeLensTexts([current.subject, current.predicate, value]);
38
+
39
+ const episode = this.store.addEpisode(
40
+ personalSpaceId,
41
+ sourceKind,
42
+ sourceText,
43
+ null,
44
+ { kind: 'lens_correction', factId: current.id, action }
45
+ );
46
+
47
+ if (action === 'replace') {
48
+ const fact = writePersonalFact(this.store, {
49
+ spaceId: personalSpaceId,
50
+ subject: current.subject,
51
+ predicate: current.predicate,
52
+ object: value,
53
+ sourceEpisodeId: episode.id,
54
+ status: FactStatus.CONFIRMED,
55
+ sensitivity: current.sensitivity,
56
+ confidence: current.confidence,
57
+ scope: FactScope.PERSONAL
58
+ });
59
+ this.store.invalidateFact(current.id, fact.id);
60
+ return { episode, fact, replacedFact: this.store.getFact(current.id) };
61
+ }
62
+
63
+ const fact = this.store.updateFact(current.id, {
64
+ status: action === 'reject' ? FactStatus.REJECTED : FactStatus.DEPRECATED,
65
+ invalidAt: nowIso()
66
+ });
67
+ return { episode, fact };
68
+ }, IMMEDIATE);
69
+ }
70
+
71
+ #requireCurrentFact(personalSpaceId, factId) {
72
+ const fact = this.store.getFact(factId);
73
+ if (!fact || fact.spaceId !== personalSpaceId || fact.scope !== FactScope.PERSONAL) {
74
+ throw new ApplicationError(ApplicationErrorCode.NOT_FOUND, `Lens fact not found: ${factId}`);
75
+ }
76
+ if (!isCurrentFact(fact) || !CORRECTABLE_STATUSES.has(fact.status)) {
77
+ throw new ApplicationError(ApplicationErrorCode.VALIDATION, `Lens fact is not current: ${factId}`);
78
+ }
79
+ return fact;
80
+ }
81
+ }
@@ -0,0 +1,85 @@
1
+ import { FactScope, FactStatus } from '../models.js';
2
+
3
+ const STATUS_RANK = Object.freeze({
4
+ [FactStatus.SUGGESTED]: 1,
5
+ [FactStatus.OBSERVED]: 2,
6
+ [FactStatus.CONFIRMED]: 3
7
+ });
8
+
9
+ export function writePersonalFact(store, input) {
10
+ const equivalent = equivalentCurrentFacts(store, input);
11
+ const fact = store.addFact({
12
+ spaceId: input.spaceId,
13
+ subject: input.subject,
14
+ predicate: input.predicate,
15
+ object: input.object,
16
+ sourceEpisodeId: input.sourceEpisodeId,
17
+ status: input.status,
18
+ confidence: input.confidence,
19
+ sensitivity: input.sensitivity,
20
+ scope: FactScope.PERSONAL
21
+ });
22
+ const candidates = [...equivalent, fact];
23
+ const winner = candidates.reduce(strongerOrLater);
24
+ for (const candidate of candidates) {
25
+ if (candidate.id !== winner.id) store.invalidateFact(candidate.id, winner.id);
26
+ }
27
+ return store.getFact(fact.id);
28
+ }
29
+
30
+ export function writePersonalFactSpecs(store, input) {
31
+ const facts = [];
32
+ for (const spec of input.specs) {
33
+ if (spec.kind === 'replacement') {
34
+ facts.push(...writeReplacement(store, input, spec));
35
+ continue;
36
+ }
37
+ facts.push(writePersonalFact(store, {
38
+ ...input,
39
+ subject: spec.subject ?? input.subject,
40
+ predicate: spec.predicate,
41
+ object: spec.object,
42
+ status: FactStatus.CONFIRMED
43
+ }));
44
+ }
45
+ return facts;
46
+ }
47
+
48
+ function writeReplacement(store, input, spec) {
49
+ const subject = spec.subject ?? input.subject;
50
+ const oldFacts = store.currentFacts(input.spaceId).filter(
51
+ (fact) => fact.subject === subject && fact.object === spec.oldValue
52
+ );
53
+ const predicates = oldFacts.length
54
+ ? [...new Set(oldFacts.map((fact) => fact.predicate))]
55
+ : ['has_replacement'];
56
+ return predicates.map((predicate) => {
57
+ const fact = writePersonalFact(store, {
58
+ ...input,
59
+ subject,
60
+ predicate,
61
+ object: spec.newValue,
62
+ status: FactStatus.CONFIRMED
63
+ });
64
+ for (const previous of oldFacts.filter((item) => item.predicate === predicate)) {
65
+ store.invalidateFact(previous.id, fact.id);
66
+ }
67
+ return fact;
68
+ });
69
+ }
70
+
71
+ function equivalentCurrentFacts(store, input) {
72
+ return store.currentFacts(input.spaceId).filter((fact) =>
73
+ fact.subject === input.subject &&
74
+ fact.predicate === input.predicate &&
75
+ fact.object === input.object
76
+ );
77
+ }
78
+
79
+ function strongerOrLater(current, candidate) {
80
+ return rank(candidate.status) >= rank(current.status) ? candidate : current;
81
+ }
82
+
83
+ function rank(status) {
84
+ return STATUS_RANK[status] ?? 0;
85
+ }
@@ -0,0 +1,62 @@
1
+ import { ApplicationError, ApplicationErrorCode } from '../app/application-error.js';
2
+ import { Sensitivity, SpaceKind } from '../models.js';
3
+ import { detectSensitiveContent } from '../security/sensitive-content.js';
4
+
5
+ export function validateLensWriteInput(
6
+ store,
7
+ { personalSpaceId, texts, sensitivity, confidence }
8
+ ) {
9
+ requirePersonalSpace(store, personalSpaceId);
10
+ assertSafeLensTexts(texts);
11
+ if (!Object.values(Sensitivity).includes(sensitivity)) {
12
+ throw new ApplicationError(
13
+ ApplicationErrorCode.VALIDATION,
14
+ `Invalid sensitivity: ${sensitivity}`
15
+ );
16
+ }
17
+ if (
18
+ typeof confidence !== 'number' ||
19
+ !Number.isFinite(confidence) ||
20
+ confidence < 0 ||
21
+ confidence > 1
22
+ ) {
23
+ throw new ApplicationError(
24
+ ApplicationErrorCode.VALIDATION,
25
+ 'Confidence must be between 0 and 1'
26
+ );
27
+ }
28
+ }
29
+
30
+ export function requirePersonalSpace(store, spaceId) {
31
+ const space = store.getSpace(spaceId);
32
+ if (!space) {
33
+ throw new ApplicationError(
34
+ ApplicationErrorCode.NOT_FOUND,
35
+ `Personal space not found: ${spaceId}`
36
+ );
37
+ }
38
+ if (space.kind !== SpaceKind.PERSONAL) {
39
+ throw new ApplicationError(
40
+ ApplicationErrorCode.VALIDATION,
41
+ `Space is not personal: ${spaceId}`
42
+ );
43
+ }
44
+ return space;
45
+ }
46
+
47
+ export function assertSafeLensTexts(texts) {
48
+ for (const text of texts) {
49
+ if (typeof text !== 'string' || text.length === 0) {
50
+ throw new ApplicationError(
51
+ ApplicationErrorCode.VALIDATION,
52
+ 'Lens text must be a nonempty string'
53
+ );
54
+ }
55
+ if (detectSensitiveContent(text).restricted) {
56
+ throw new ApplicationError(
57
+ ApplicationErrorCode.VALIDATION,
58
+ 'Sensitive content is not allowed in Personal Lens'
59
+ );
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,87 @@
1
+ import { FactScope, FactStatus, isCurrentFact } from '../models.js';
2
+ import { isRestrictedEpisode } from '../security/episode-visibility.js';
3
+ import { isRestrictedFact } from '../security/fact-visibility.js';
4
+
5
+ export function isInjectableFact(
6
+ fact,
7
+ { includeObserved, includeSuggested, includeRestricted }
8
+ ) {
9
+ if (fact.invalidAt) return false;
10
+ if (fact.status === FactStatus.CONFIRMED) return isVisibleFact(fact, includeRestricted);
11
+ if (fact.status === FactStatus.OBSERVED && includeObserved) {
12
+ return isVisibleFact(fact, includeRestricted);
13
+ }
14
+ if (fact.status === FactStatus.SUGGESTED && includeSuggested) {
15
+ return isVisibleFact(fact, includeRestricted);
16
+ }
17
+ return false;
18
+ }
19
+
20
+ export function isSearchableFact(fact, { includeHistorical, includeRestricted }) {
21
+ return (includeHistorical || isCurrentFact(fact)) && isVisibleFact(fact, includeRestricted);
22
+ }
23
+
24
+ export function buildCorrectionIndex(groups, personalSpaceId, includeRestricted) {
25
+ const index = new Map();
26
+ for (const group of groups) {
27
+ if (typeof group.factId !== 'string') continue;
28
+ const episodes = group.episodes.filter((episode) =>
29
+ episode.spaceId === personalSpaceId &&
30
+ episode.metadata?.kind === 'lens_correction' &&
31
+ episode.metadata?.factId === group.factId &&
32
+ (includeRestricted || !isRestrictedEpisode(episode))
33
+ );
34
+ index.set(group.factId, {
35
+ episodes,
36
+ truncated: group.truncated === true || episodes.length < group.episodes.length
37
+ });
38
+ }
39
+ return index;
40
+ }
41
+
42
+ export function projectSearchFact(
43
+ store,
44
+ fact,
45
+ evidenceEpisode,
46
+ personalSpaceId,
47
+ correctionIndex,
48
+ includeRestricted
49
+ ) {
50
+ const sourceEpisode = safePersonalEpisode(
51
+ evidenceEpisode,
52
+ personalSpaceId,
53
+ includeRestricted
54
+ );
55
+ const replacement = fact.replacedByFactId ? store.getFact(fact.replacedByFactId) : null;
56
+ const replacementFact = replacement?.spaceId === personalSpaceId &&
57
+ replacement.scope === FactScope.PERSONAL &&
58
+ isVisibleFact(replacement, includeRestricted)
59
+ ? replacement
60
+ : null;
61
+ return {
62
+ fact: projectSearchFactDto(fact, sourceEpisode, replacementFact),
63
+ current: isCurrentFact(fact),
64
+ sourceEpisode,
65
+ replacementFact,
66
+ correctionEpisodes: correctionIndex.get(fact.id)?.episodes ?? [],
67
+ correctionEpisodesTruncated: correctionIndex.get(fact.id)?.truncated ?? false
68
+ };
69
+ }
70
+
71
+ function projectSearchFactDto(fact, sourceEpisode, replacementFact) {
72
+ return {
73
+ ...fact,
74
+ sourceEpisodeId: sourceEpisode?.id ?? null,
75
+ replacedByFactId: replacementFact?.id ?? null
76
+ };
77
+ }
78
+
79
+ function isVisibleFact(fact, includeRestricted) {
80
+ return includeRestricted === true || !isRestrictedFact(fact);
81
+ }
82
+
83
+ export function safePersonalEpisode(episode, personalSpaceId, includeRestricted) {
84
+ if (!episode || episode.spaceId !== personalSpaceId) return null;
85
+ if (!includeRestricted && isRestrictedEpisode(episode)) return null;
86
+ return episode;
87
+ }
@@ -0,0 +1,62 @@
1
+ import { FactStatus } from '../models.js';
2
+
3
+ export { compareSearchFacts } from '../storage/fact-search-order.js';
4
+
5
+ export const BOUNDARY_PREDICATES = Object.freeze([
6
+ 'has_boundary',
7
+ 'has_access_boundary',
8
+ 'does_not_want',
9
+ 'avoids',
10
+ 'forbids'
11
+ ]);
12
+ export const BOUNDARY_PREFIXES = Object.freeze([
13
+ 'boundary_',
14
+ 'does_not_',
15
+ 'avoids_',
16
+ 'forbids_',
17
+ 'must_not_'
18
+ ]);
19
+
20
+ const STATUS_RANK = Object.freeze({
21
+ [FactStatus.SUGGESTED]: 1,
22
+ [FactStatus.OBSERVED]: 2,
23
+ [FactStatus.CONFIRMED]: 3
24
+ });
25
+
26
+ export function taskTokens(task) {
27
+ return new Set(normalizedTokens(task));
28
+ }
29
+
30
+ export function compareLensFacts(left, right, terms) {
31
+ return compareNumber(isBoundary(right), isBoundary(left)) ||
32
+ compareNumber(termMatches(right, terms), termMatches(left, terms)) ||
33
+ compareNumber(STATUS_RANK[right.status] ?? 0, STATUS_RANK[left.status] ?? 0) ||
34
+ compareText(right.validAt, left.validAt) ||
35
+ compareText(left.id, right.id);
36
+ }
37
+
38
+ function termMatches(fact, terms) {
39
+ const factTokens = new Set(normalizedTokens(`${fact.subject} ${fact.predicate} ${fact.object}`));
40
+ let matches = 0;
41
+ for (const term of terms) if (factTokens.has(term)) matches += 1;
42
+ return matches;
43
+ }
44
+
45
+ function normalizedTokens(text) {
46
+ return String(text).normalize('NFKC').toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
47
+ }
48
+
49
+ function isBoundary(fact) {
50
+ return BOUNDARY_PREDICATES.includes(fact.predicate) ||
51
+ BOUNDARY_PREFIXES.some((prefix) => fact.predicate.startsWith(prefix));
52
+ }
53
+
54
+ function compareNumber(left, right) {
55
+ return left - right;
56
+ }
57
+
58
+ function compareText(left, right) {
59
+ const first = String(left ?? '');
60
+ const second = String(right ?? '');
61
+ return first === second ? 0 : (first < second ? -1 : 1);
62
+ }