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,90 @@
1
+ import { existsSync } from 'node:fs';
2
+
3
+ import { stopLocalRuntime } from '../local-runtime/lifecycle.js';
4
+ import { backupAgentConfig } from './config-backup.js';
5
+ import { disconnectAgent, discoverAgents } from './agents.js';
6
+ import { resolveSetupPaths } from './paths.js';
7
+ import { removeBundledSkill } from './session-skill.js';
8
+
9
+ export function planLocalUninstall(options, {
10
+ env = process.env,
11
+ resolvePaths = resolveSetupPaths,
12
+ discover = discoverAgents,
13
+ fileExists = existsSync
14
+ } = {}) {
15
+ const paths = resolvePaths({ dataDir: options.dataDir, env });
16
+ const agents = discover({ env }).filter((agent) => (
17
+ agent.available ||
18
+ (agent.configPath && fileExists(agent.configPath)) ||
19
+ (agent.globalInstructionsPath && fileExists(agent.globalInstructionsPath)) ||
20
+ (agent.globalInstructionsOverridePath &&
21
+ fileExists(agent.globalInstructionsOverridePath)) ||
22
+ (agent.skillPath && fileExists(agent.skillPath)) ||
23
+ (agent.projectSkillPath && fileExists(agent.projectSkillPath))
24
+ ));
25
+ return { paths, agents };
26
+ }
27
+
28
+ export async function applyLocalUninstall(plan, dependencies = {}) {
29
+ const stopRuntime = dependencies.stopRuntime ?? stopLocalRuntime;
30
+ const backupConfig = dependencies.backupConfig ?? backupAgentConfig;
31
+ const disconnect = dependencies.disconnect ?? disconnectAgent;
32
+ const removeSkill = dependencies.removeSkill ?? removeBundledSkill;
33
+ let runtime;
34
+
35
+ try {
36
+ runtime = await stopRuntime({
37
+ paths: plan.paths,
38
+ port: dependencies.port,
39
+ env: dependencies.env ?? process.env
40
+ });
41
+ } catch (error) {
42
+ runtime = {
43
+ status: 'failed',
44
+ message: error instanceof Error ? error.message : 'Could not stop the local runtime'
45
+ };
46
+ }
47
+
48
+ const agents = plan.agents.map((agent) => {
49
+ const errors = [];
50
+ let backupPath = null;
51
+ let connection;
52
+ const skills = [];
53
+ try {
54
+ backupPath = backupConfig(agent, { backupDir: plan.paths.backupDir });
55
+ connection = disconnect(agent);
56
+ } catch (error) {
57
+ errors.push(error instanceof Error ? error.message : 'Agent disconnect failed');
58
+ connection = { status: 'failed' };
59
+ }
60
+
61
+ for (const [skillPath, sourcePath] of [
62
+ [agent.skillPath, plan.paths.sessionSkillPath],
63
+ [agent.projectSkillPath, plan.paths.projectSkillPath]
64
+ ]) {
65
+ try {
66
+ skills.push(removeSkill({ ...agent, skillPath }, { sourcePath }));
67
+ } catch (error) {
68
+ errors.push(error instanceof Error ? error.message : 'Skill removal failed');
69
+ }
70
+ }
71
+
72
+ return {
73
+ id: agent.id,
74
+ label: agent.label,
75
+ status: errors.length ? 'partial' : connection.status,
76
+ backupPath,
77
+ skills,
78
+ ...(errors.length ? { errors } : {})
79
+ };
80
+ });
81
+
82
+ const partial = runtime.status !== 'stopped' ||
83
+ agents.some(({ status }) => status === 'partial');
84
+ return {
85
+ status: partial ? 'partial' : 'ready',
86
+ runtime,
87
+ agents,
88
+ data: { status: 'preserved', path: plan.paths.dataDir }
89
+ };
90
+ }
@@ -0,0 +1,100 @@
1
+ export const MAX_EPISODE_EVIDENCE_BODY_BYTES = 16 * 1024;
2
+ export const MAX_EPISODE_METADATA_BYTES = 16 * 1024;
3
+ export const MAX_EPISODE_METADATA_FIELD_BYTES = 2048;
4
+ export const DEFAULT_CORRECTION_EVIDENCE_PER_FACT = 5;
5
+ export const MAX_CORRECTION_EVIDENCE_PER_FACT = 20;
6
+
7
+ export function normalizeEvidencePreviewOptions(options = {}) {
8
+ const maxBodyBytes = options.maxBodyBytes ?? MAX_EPISODE_EVIDENCE_BODY_BYTES;
9
+ if (
10
+ !Number.isInteger(maxBodyBytes) ||
11
+ maxBodyBytes < 0 ||
12
+ maxBodyBytes > MAX_EPISODE_EVIDENCE_BODY_BYTES
13
+ ) {
14
+ throw new TypeError(
15
+ `Episode evidence body limit must be between 0 and ${MAX_EPISODE_EVIDENCE_BODY_BYTES}`
16
+ );
17
+ }
18
+ return {
19
+ maxBodyBytes,
20
+ includeRestricted: options.includeRestricted === true
21
+ };
22
+ }
23
+
24
+ export function normalizeCorrectionEvidencePreviewOptions(options = {}) {
25
+ const normalized = normalizeEvidencePreviewOptions(options);
26
+ const maxCorrectionsPerFact = options.maxCorrectionsPerFact ??
27
+ DEFAULT_CORRECTION_EVIDENCE_PER_FACT;
28
+ if (
29
+ !Number.isInteger(maxCorrectionsPerFact) ||
30
+ maxCorrectionsPerFact < 1 ||
31
+ maxCorrectionsPerFact > MAX_CORRECTION_EVIDENCE_PER_FACT
32
+ ) {
33
+ throw new TypeError(
34
+ `Correction evidence limit must be between 1 and ${MAX_CORRECTION_EVIDENCE_PER_FACT}`
35
+ );
36
+ }
37
+ return { ...normalized, maxCorrectionsPerFact };
38
+ }
39
+
40
+ export function previewEpisode(episode, options, { correction = false } = {}) {
41
+ if (!episode) return null;
42
+ const { value: body, truncated: bodyTruncated } = clipUtf8(
43
+ episode.body,
44
+ options.maxBodyBytes
45
+ );
46
+ const metadata = correction
47
+ ? previewCorrectionMetadata(episode.metadata)
48
+ : previewSourceMetadata(episode.metadata);
49
+ const truncatedFields = [];
50
+ if (bodyTruncated || episode.bodyTruncated) truncatedFields.push('body');
51
+ if (metadata.truncated || episode.metadataTruncated) truncatedFields.push('metadata');
52
+
53
+ return {
54
+ id: episode.id,
55
+ spaceId: episode.spaceId,
56
+ sourceKind: episode.sourceKind,
57
+ body,
58
+ sourceUri: episode.sourceUri,
59
+ metadata: metadata.value,
60
+ createdAt: episode.createdAt,
61
+ truncatedFields
62
+ };
63
+ }
64
+
65
+ export function clipUtf8(value, maxBytes) {
66
+ const text = typeof value === 'string' ? value : '';
67
+ const encoded = Buffer.from(text, 'utf8');
68
+ if (encoded.length <= maxBytes) return { value: text, truncated: false };
69
+ let end = maxBytes;
70
+ while (end > 0 && (encoded[end] & 0xc0) === 0x80) end -= 1;
71
+ return { value: encoded.subarray(0, end).toString('utf8'), truncated: true };
72
+ }
73
+
74
+ function previewSourceMetadata(metadata) {
75
+ if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
76
+ return { value: {}, truncated: metadata !== undefined };
77
+ }
78
+ const serialized = JSON.stringify(metadata);
79
+ if (Buffer.byteLength(serialized, 'utf8') > MAX_EPISODE_METADATA_BYTES) {
80
+ return { value: {}, truncated: true };
81
+ }
82
+ return { value: JSON.parse(serialized), truncated: false };
83
+ }
84
+
85
+ function previewCorrectionMetadata(metadata) {
86
+ const source = metadata && typeof metadata === 'object' && !Array.isArray(metadata)
87
+ ? metadata
88
+ : {};
89
+ const value = {};
90
+ let truncated = Object.keys(source).some(
91
+ (key) => !['kind', 'factId', 'action'].includes(key)
92
+ );
93
+ for (const key of ['kind', 'factId', 'action']) {
94
+ if (typeof source[key] !== 'string') continue;
95
+ const clipped = clipUtf8(source[key], MAX_EPISODE_METADATA_FIELD_BYTES);
96
+ value[key] = clipped.value;
97
+ truncated ||= clipped.truncated;
98
+ }
99
+ return { value, truncated };
100
+ }
@@ -0,0 +1,18 @@
1
+ export function compareSearchFacts(left, right) {
2
+ return compareText(right.validAt, left.validAt) || compareText(left.id, right.id);
3
+ }
4
+
5
+ export function searchTextSortKey(value) {
6
+ const text = String(value ?? '');
7
+ const key = Buffer.allocUnsafe(text.length * 2);
8
+ for (let index = 0; index < text.length; index += 1) {
9
+ key.writeUInt16BE(text.charCodeAt(index), index * 2);
10
+ }
11
+ return key;
12
+ }
13
+
14
+ function compareText(left, right) {
15
+ const first = String(left ?? '');
16
+ const second = String(right ?? '');
17
+ return first === second ? 0 : (first < second ? -1 : 1);
18
+ }
@@ -0,0 +1,37 @@
1
+ import { isRestrictedEpisode } from '../../security/episode-visibility.js';
2
+ import {
3
+ normalizeCorrectionEvidencePreviewOptions,
4
+ normalizeEvidencePreviewOptions,
5
+ previewEpisode
6
+ } from '../episode-evidence-preview.js';
7
+
8
+ export function fileEpisodeEvidencePreview(index, spaceId, episodeId, options = {}) {
9
+ const normalized = normalizeEvidencePreviewOptions(options);
10
+ const episode = index.get(episodeId);
11
+ if (!isVisibleEpisode(episode, spaceId, normalized.includeRestricted)) return null;
12
+ return previewEpisode(episode, normalized);
13
+ }
14
+
15
+ export function fileCorrectionEpisodeEvidencePreviews(
16
+ index,
17
+ spaceId,
18
+ factIds,
19
+ options = {}
20
+ ) {
21
+ const normalized = normalizeCorrectionEvidencePreviewOptions(options);
22
+ return index.correctionWindows(spaceId, factIds, {
23
+ includeRestricted: normalized.includeRestricted,
24
+ limit: normalized.maxCorrectionsPerFact
25
+ }).map((group) => ({
26
+ factId: group.factId,
27
+ episodes: group.episodes.map(
28
+ (episode) => previewEpisode(episode, normalized, { correction: true })
29
+ ),
30
+ truncated: group.truncated
31
+ }));
32
+ }
33
+
34
+ function isVisibleEpisode(episode, spaceId, includeRestricted) {
35
+ return episode?.spaceId === spaceId &&
36
+ (includeRestricted || !isRestrictedEpisode(episode));
37
+ }
@@ -0,0 +1,59 @@
1
+ import { isRestrictedEpisode } from '../../security/episode-visibility.js';
2
+
3
+ export class FileEpisodeIndex {
4
+ #byId = new Map();
5
+ #correctionsBySpace = new Map();
6
+ #safeCorrectionsBySpace = new Map();
7
+
8
+ constructor(episodes) {
9
+ this.rebuild(episodes);
10
+ }
11
+
12
+ rebuild(episodes) {
13
+ this.#byId = new Map();
14
+ this.#correctionsBySpace = new Map();
15
+ this.#safeCorrectionsBySpace = new Map();
16
+ for (const episode of episodes) this.add(episode);
17
+ }
18
+
19
+ add(episode) {
20
+ this.#byId.set(episode.id, episode);
21
+ const factId = episode.metadata?.kind === 'lens_correction'
22
+ ? episode.metadata.factId
23
+ : null;
24
+ if (typeof factId !== 'string') return;
25
+
26
+ addCorrection(this.#correctionsBySpace, episode, factId);
27
+ if (!isRestrictedEpisode(episode)) {
28
+ addCorrection(this.#safeCorrectionsBySpace, episode, factId);
29
+ }
30
+ }
31
+
32
+ get(id) {
33
+ return this.#byId.get(id) ?? null;
34
+ }
35
+
36
+ correctionWindows(spaceId, factIds, { includeRestricted, limit }) {
37
+ const index = includeRestricted
38
+ ? this.#correctionsBySpace
39
+ : this.#safeCorrectionsBySpace;
40
+ const byFact = index.get(spaceId) ?? new Map();
41
+ return [...new Set(factIds)].map((factId) => {
42
+ const episodes = byFact.get(factId) ?? [];
43
+ return {
44
+ factId,
45
+ episodes: episodes.slice(Math.max(0, episodes.length - limit)).reverse(),
46
+ truncated: episodes.length > limit
47
+ };
48
+ });
49
+ }
50
+ }
51
+
52
+ function addCorrection(index, episode, factId) {
53
+ const byFact = index.get(episode.spaceId) ?? new Map();
54
+ const episodes = byFact.get(factId) ?? [];
55
+ episodes.push(episode);
56
+ episodes.sort((left, right) => left.createdAt.localeCompare(right.createdAt));
57
+ byFact.set(factId, episodes);
58
+ index.set(episode.spaceId, byFact);
59
+ }
@@ -0,0 +1,60 @@
1
+ import { isCurrentFact } from '../../models.js';
2
+ import { isRestrictedEpisode } from '../../security/episode-visibility.js';
3
+ import { isRestrictedFact } from '../../security/fact-visibility.js';
4
+ import { compareSearchFacts } from '../fact-search-order.js';
5
+
6
+ export function searchFileFacts(facts, episodeIndex, spaceIds, query, options = {}) {
7
+ return searchFileFactsPage(facts, episodeIndex, spaceIds, query, options).facts;
8
+ }
9
+
10
+ export function searchFileFactsPage(facts, episodeIndex, spaceIds, query, options = {}) {
11
+ const search = {
12
+ includeHistorical: options.includeHistorical ?? false,
13
+ scope: options.scope ?? null,
14
+ limit: options.limit ?? null,
15
+ excludeRestricted: options.excludeRestricted === true,
16
+ safeSourceSpaceId: options.safeSourceSpaceId ?? null,
17
+ maxObjectBytes: options.maxObjectBytes ?? null,
18
+ query: query.toLowerCase()
19
+ };
20
+
21
+ if (search.limit !== null && (!Number.isInteger(search.limit) || search.limit <= 0)) {
22
+ throw new TypeError('Fact search limit must be a positive integer');
23
+ }
24
+
25
+ const selected = [];
26
+ let objectLimitTruncated = false;
27
+ for (const fact of facts) {
28
+ if (!matchesSearchWithoutObjectLimit(fact, episodeIndex, spaceIds, search)) continue;
29
+ if (
30
+ search.maxObjectBytes !== null &&
31
+ Buffer.byteLength(fact.object, 'utf8') > search.maxObjectBytes
32
+ ) {
33
+ objectLimitTruncated = true;
34
+ continue;
35
+ }
36
+ selected.push(fact);
37
+ if (search.limit === null) continue;
38
+ selected.sort(compareSearchFacts);
39
+ if (selected.length > search.limit) selected.pop();
40
+ }
41
+ return { facts: selected, objectLimitTruncated };
42
+ }
43
+
44
+ function matchesSearchWithoutObjectLimit(fact, episodeIndex, spaceIds, search) {
45
+ if (!spaceIds.includes(fact.spaceId)) return false;
46
+ if (search.scope !== null && fact.scope !== search.scope) return false;
47
+ if (!search.includeHistorical && !isCurrentFact(fact)) return false;
48
+ if (search.excludeRestricted && isRestrictedFact(fact)) return false;
49
+ if (search.safeSourceSpaceId !== null) {
50
+ const source = episodeIndex.get(fact.sourceEpisodeId);
51
+ if (
52
+ !source ||
53
+ source.spaceId !== search.safeSourceSpaceId ||
54
+ isRestrictedEpisode(source)
55
+ ) return false;
56
+ }
57
+ return `${fact.subject} ${fact.predicate} ${fact.object}`
58
+ .toLowerCase()
59
+ .includes(search.query);
60
+ }