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,561 @@
1
+ from .graph_projection import (
2
+ coalesce_personal_project_identities,
3
+ coalesce_personal_project_identity,
4
+ management_projection,
5
+ personal_project_projection,
6
+ )
7
+ from .graph_models import GraphEdge, GraphNode, GraphResult
8
+ from .models import (
9
+ ConfirmationBasis,
10
+ GraphEvidence,
11
+ )
12
+ from .knowledge_audit import read_knowledge_audits
13
+ from .knowledge_records import assignment_record, revision_record
14
+ from .personal_project_access import authorize_personal_project
15
+ from .provider_values import json_object, native_datetime as _native_datetime
16
+ from .project_knowledge import (
17
+ read_knowledge_conflicts,
18
+ read_personal_project_relations,
19
+ read_project_references,
20
+ )
21
+ from .search_projection import effective_confirmation_status
22
+
23
+
24
+ async def read_graph(
25
+ store,
26
+ actor: dict,
27
+ space_id: str,
28
+ limit: int | None = None,
29
+ personal_project_id: str | None = None,
30
+ ) -> GraphResult:
31
+ space = await store.authorize(actor, space_id, 'reader')
32
+ project = None
33
+ if personal_project_id:
34
+ project = await authorize_personal_project(store, actor, space, personal_project_id)
35
+ bounded_limit = min(limit or store.settings.graph_limit, store.settings.graph_limit)
36
+ node_records, _, _ = await store.runtime.driver.execute_query(
37
+ _node_query(bool(project)),
38
+ space_id=space_id,
39
+ group_id=space['group_id'],
40
+ project_id=personal_project_id,
41
+ limit=bounded_limit + 1,
42
+ routing_='r',
43
+ )
44
+ truncated = len(node_records) > bounded_limit
45
+ node_records = node_records[:bounded_limit]
46
+ node_ids = [record['id'] for record in node_records]
47
+ episode_ids = list({
48
+ episode_id
49
+ for record in node_records
50
+ for episode_id in (record.get('episodes') or [])
51
+ })
52
+ edge_records, _, _ = await store.runtime.driver.execute_query(
53
+ _edge_query(bool(project)),
54
+ space_id=space_id,
55
+ group_id=space['group_id'],
56
+ project_id=personal_project_id,
57
+ node_ids=node_ids,
58
+ episode_ids=episode_ids,
59
+ limit=bounded_limit,
60
+ routing_='r',
61
+ )
62
+ all_episode_ids = list({
63
+ *episode_ids,
64
+ *(episode_id for record in edge_records for episode_id in (record.get('episodes') or [])),
65
+ })
66
+ evidence = await _read_evidence(store, all_episode_ids)
67
+ item_ids = node_ids + [record['id'] for record in edge_records]
68
+ revisions = await _read_revisions(store, space_id, item_ids)
69
+ assignments = await _read_assignments(store, space_id, item_ids)
70
+ references = await read_project_references(store, space_id, item_ids)
71
+ conflicts = await read_knowledge_conflicts(store, space_id, item_ids)
72
+ audits = await read_knowledge_audits(store, space_id, item_ids)
73
+ knowledge_nodes = [
74
+ _graph_node(
75
+ record,
76
+ evidence,
77
+ revisions,
78
+ assignments,
79
+ references,
80
+ conflicts,
81
+ audits,
82
+ )
83
+ for record in node_records
84
+ ]
85
+ knowledge_edges = [
86
+ _graph_edge(
87
+ record,
88
+ evidence,
89
+ revisions,
90
+ assignments,
91
+ references,
92
+ conflicts,
93
+ audits,
94
+ )
95
+ for record in edge_records
96
+ ]
97
+
98
+ personal_relations = (
99
+ await read_personal_project_relations(store, space_id)
100
+ if space['kind'] == 'personal' else []
101
+ )
102
+ if project:
103
+ management_nodes, management_edges = personal_project_projection(
104
+ space, project, personal_relations
105
+ )
106
+ graph_nodes, graph_edges = coalesce_personal_project_identity(
107
+ project,
108
+ management_nodes,
109
+ management_edges,
110
+ knowledge_nodes,
111
+ knowledge_edges,
112
+ )
113
+ else:
114
+ personal_projects = await _personal_projects(store, space)
115
+ management_nodes, management_edges = management_projection(
116
+ space, personal_projects, personal_relations
117
+ )
118
+ graph_nodes, graph_edges = coalesce_personal_project_identities(
119
+ personal_projects,
120
+ management_nodes,
121
+ management_edges,
122
+ knowledge_nodes,
123
+ knowledge_edges,
124
+ )
125
+ return GraphResult(
126
+ space_id=space_id,
127
+ nodes=graph_nodes,
128
+ edges=graph_edges,
129
+ truncated=truncated,
130
+ )
131
+
132
+
133
+ def _node_query(project_scoped: bool) -> str:
134
+ if project_scoped:
135
+ match = '''
136
+ MATCH (node:Entity {group_id: $group_id})
137
+ OPTIONAL MATCH (episode:Episodic {group_id: $group_id})-[:MENTIONS]->(node)
138
+ WITH node, collect(DISTINCT episode) AS knowledge_episodes
139
+ OPTIONAL MATCH (assignment:FuliKnowledgeAssignment {
140
+ space_id: $space_id,
141
+ item_kind: 'entity',
142
+ item_id: node.uuid
143
+ })
144
+ OPTIONAL MATCH (:FuliSpace {id: $space_id})-[:HAS_KNOWLEDGE_REFERENCE]->
145
+ (reference:FuliKnowledgeProjectReference {
146
+ item_kind: 'entity', item_id: node.uuid
147
+ })
148
+ WITH node, knowledge_episodes, assignment, collect(reference) AS references
149
+ WHERE (
150
+ (
151
+ node.fuli_profile_aspect IS NOT NULL
152
+ AND node.fuli_preference_scope = 'project'
153
+ AND node.fuli_preference_project_id = $project_id
154
+ )
155
+ OR (
156
+ node.fuli_profile_aspect IS NULL
157
+ AND (
158
+ (assignment.project_id IS NOT NULL AND assignment.project_id = $project_id)
159
+ OR (assignment.project_id IS NULL AND any(
160
+ episode IN knowledge_episodes
161
+ WHERE episode.fuli_personal_project_id = $project_id
162
+ ))
163
+ OR any(reference IN references WHERE
164
+ reference.project_id = $project_id AND reference.status = 'active')
165
+ )
166
+ )
167
+ )
168
+ AND NOT EXISTS {
169
+ MATCH (:FuliSpace {id: $space_id})-[:HAS_KNOWLEDGE_CONFLICT]->
170
+ (conflict:FuliKnowledgeConflict)
171
+ WHERE conflict.target_project_id = $project_id
172
+ AND conflict.target_item_id = node.uuid
173
+ AND conflict.status = 'resolved'
174
+ AND conflict.resolution = 'use_source'
175
+ }
176
+ WITH node, knowledge_episodes AS episodes
177
+ '''
178
+ else:
179
+ match = '''
180
+ MATCH (node:Entity {group_id: $group_id})
181
+ OPTIONAL MATCH (episode:Episodic {group_id: $group_id})-[:MENTIONS]->(node)
182
+ WITH node, collect(DISTINCT episode) AS episodes
183
+ '''
184
+ return match + '''
185
+ RETURN node.uuid AS id, node.name AS name, node.group_id AS group_id,
186
+ coalesce(node.fuli_type, 'Entity') AS type,
187
+ coalesce(node.summary, '') AS summary,
188
+ coalesce(node.fuli_origin_quadrant, 'known_known') AS origin_quadrant,
189
+ coalesce(node.fuli_current_quadrant, 'known_known') AS current_quadrant,
190
+ coalesce(node.fuli_epistemic_status, 'confirmed') AS epistemic_status,
191
+ node.fuli_origin_quadrant IS NOT NULL AS epistemic_state_explicit,
192
+ coalesce(node.fuli_confirmation_status, 'pending')
193
+ AS confirmation_status,
194
+ node.fuli_confirmation_status IS NOT NULL
195
+ AND node.fuli_confirmation_basis_json IS NOT NULL
196
+ AS confirmation_state_explicit,
197
+ node.fuli_confirmation_basis_json AS confirmation_basis_json,
198
+ node.fuli_reasoning_summary AS reasoning_summary,
199
+ node.fuli_profile_aspect AS profile_aspect,
200
+ CASE WHEN node.fuli_profile_aspect IS NULL THEN NULL
201
+ ELSE coalesce(node.fuli_preference_scope, 'global') END
202
+ AS preference_scope,
203
+ node.fuli_preference_project_id AS preference_project_id,
204
+ coalesce(node.fuli_inheritance_mode, 'local_only')
205
+ AS inheritance_mode,
206
+ coalesce(node.fuli_inherited_project_ids, [])
207
+ AS inherited_project_ids,
208
+ coalesce(node.fuli_human_edited, false) AS human_edited,
209
+ coalesce(node.fuli_human_change_status, 'none')
210
+ AS human_change_status,
211
+ coalesce(node.fuli_human_change_version, 0)
212
+ AS human_change_version,
213
+ node.fuli_last_human_changed_at AS last_human_changed_at,
214
+ node.fuli_last_agent_viewed_at AS last_agent_viewed_at,
215
+ node.fuli_last_agent_reviewed_at AS last_agent_reviewed_at,
216
+ coalesce(node.fuli_utility_score, 0.0) AS utility_score,
217
+ coalesce(node.fuli_confidence_score, 0.5) AS confidence_score,
218
+ coalesce(node.fuli_qualified_use_count, 0)
219
+ AS qualified_use_count,
220
+ coalesce(node.fuli_distinct_task_count, 0)
221
+ AS distinct_task_count,
222
+ node.fuli_last_used_at AS last_used_at,
223
+ coalesce(node.fuli_usage_generation, 1) AS usage_generation,
224
+ coalesce(node.fuli_attributes_json, '{}') AS attributes_json,
225
+ [episode IN episodes WHERE episode IS NOT NULL | episode.uuid] AS episodes,
226
+ node.created_at AS created_at,
227
+ node.fuli_invalid_at AS invalid_at,
228
+ node.fuli_replaced_by_item_id AS replaced_by_item_id,
229
+ node.fuli_replaced_by_item_kind AS replaced_by_item_kind
230
+ ORDER BY created_at DESC LIMIT $limit
231
+ '''
232
+
233
+
234
+ def _edge_query(project_scoped: bool) -> str:
235
+ project_filter = '''
236
+ OPTIONAL MATCH (assignment:FuliKnowledgeAssignment {
237
+ space_id: $space_id,
238
+ item_kind: 'relationship',
239
+ item_id: edge.uuid
240
+ })
241
+ WITH source, edge, target, assignment
242
+ WHERE (
243
+ edge.fuli_profile_aspect IS NOT NULL
244
+ AND edge.fuli_preference_scope = 'project'
245
+ AND edge.fuli_preference_project_id = $project_id
246
+ ) OR (
247
+ edge.fuli_profile_aspect IS NULL
248
+ AND (
249
+ (assignment.project_id IS NOT NULL AND assignment.project_id = $project_id)
250
+ OR (assignment.project_id IS NULL AND any(
251
+ episode_id IN coalesce(edge.episodes, [])
252
+ WHERE episode_id IN $episode_ids
253
+ ))
254
+ )
255
+ )
256
+ ''' if project_scoped else ''
257
+ return '''
258
+ MATCH (source:Entity)-[edge:RELATES_TO {group_id: $group_id}]->(target:Entity)
259
+ WHERE source.uuid IN $node_ids AND target.uuid IN $node_ids
260
+ ''' + project_filter + '''
261
+ RETURN edge.uuid AS id, source.uuid AS source, target.uuid AS target,
262
+ edge.name AS type, edge.fact AS fact,
263
+ coalesce(edge.fuli_origin_quadrant, 'known_known') AS origin_quadrant,
264
+ coalesce(edge.fuli_current_quadrant, 'known_known') AS current_quadrant,
265
+ coalesce(edge.fuli_epistemic_status, 'confirmed') AS epistemic_status,
266
+ edge.fuli_origin_quadrant IS NOT NULL AS epistemic_state_explicit,
267
+ coalesce(edge.fuli_confirmation_status, 'pending')
268
+ AS confirmation_status,
269
+ edge.fuli_confirmation_status IS NOT NULL
270
+ AND edge.fuli_confirmation_basis_json IS NOT NULL
271
+ AS confirmation_state_explicit,
272
+ edge.fuli_confirmation_basis_json AS confirmation_basis_json,
273
+ edge.fuli_reasoning_summary AS reasoning_summary,
274
+ edge.fuli_profile_aspect AS profile_aspect,
275
+ CASE WHEN edge.fuli_profile_aspect IS NULL THEN NULL
276
+ ELSE coalesce(edge.fuli_preference_scope, 'global') END
277
+ AS preference_scope,
278
+ edge.fuli_preference_project_id AS preference_project_id,
279
+ coalesce(edge.fuli_inheritance_mode, 'local_only')
280
+ AS inheritance_mode,
281
+ coalesce(edge.fuli_inherited_project_ids, [])
282
+ AS inherited_project_ids,
283
+ coalesce(edge.fuli_human_edited, false) AS human_edited,
284
+ coalesce(edge.fuli_human_change_status, 'none')
285
+ AS human_change_status,
286
+ coalesce(edge.fuli_human_change_version, 0)
287
+ AS human_change_version,
288
+ edge.fuli_last_human_changed_at AS last_human_changed_at,
289
+ edge.fuli_last_agent_viewed_at AS last_agent_viewed_at,
290
+ edge.fuli_last_agent_reviewed_at AS last_agent_reviewed_at,
291
+ coalesce(edge.fuli_utility_score, 0.0) AS utility_score,
292
+ coalesce(edge.fuli_confidence_score, 0.5) AS confidence_score,
293
+ coalesce(edge.fuli_qualified_use_count, 0)
294
+ AS qualified_use_count,
295
+ coalesce(edge.fuli_distinct_task_count, 0)
296
+ AS distinct_task_count,
297
+ edge.fuli_last_used_at AS last_used_at,
298
+ coalesce(edge.fuli_usage_generation, 1) AS usage_generation,
299
+ edge.valid_at AS valid_at, edge.invalid_at AS invalid_at,
300
+ edge.fuli_replaced_by_item_id AS replaced_by_item_id,
301
+ edge.fuli_replaced_by_item_kind AS replaced_by_item_kind,
302
+ edge.created_at AS created_at,
303
+ edge.fuli_confidence AS confidence,
304
+ coalesce(edge.fuli_attributes_json, '{}') AS attributes_json,
305
+ coalesce(edge.episodes, []) AS episodes
306
+ ORDER BY edge.created_at DESC LIMIT $limit
307
+ '''
308
+
309
+
310
+ async def _personal_projects(store, space: dict) -> list[dict]:
311
+ if space['kind'] != 'personal':
312
+ return []
313
+ records, _, _ = await store.runtime.driver.execute_query(
314
+ '''
315
+ MATCH (:FuliSpace {id: $space_id, kind: 'personal'})-
316
+ [:CONTAINS_PROJECT]->(project:FuliPersonalProject)
317
+ RETURN project ORDER BY project.updated_at DESC
318
+ ''',
319
+ space_id=space['id'],
320
+ routing_='r',
321
+ )
322
+ return [dict(record['project']) for record in records]
323
+
324
+
325
+ async def _read_evidence(store, episode_ids: list[str]) -> dict[str, GraphEvidence]:
326
+ if not episode_ids:
327
+ return {}
328
+ records, _, _ = await store.runtime.driver.execute_query(
329
+ '''
330
+ MATCH (episode:Episodic) WHERE episode.uuid IN $episode_ids
331
+ RETURN episode.uuid AS id, episode.name AS name,
332
+ episode.source_description AS source_description,
333
+ coalesce(episode.fuli_source_kind, 'unknown') AS source_kind,
334
+ coalesce(episode.fuli_summary, '') AS summary,
335
+ episode.fuli_session_id AS session_id,
336
+ episode.fuli_source_application AS source_application,
337
+ episode.fuli_source_turn_id AS source_turn_id,
338
+ episode.fuli_source_excerpt AS source_excerpt,
339
+ episode.fuli_personal_project_id AS personal_project_id,
340
+ episode.valid_at AS reference_time,
341
+ episode.created_at AS created_at
342
+ ''',
343
+ episode_ids=episode_ids,
344
+ routing_='r',
345
+ )
346
+ return {
347
+ record['id']: GraphEvidence(
348
+ **{
349
+ **dict(record),
350
+ 'reference_time': _native_datetime(record.get('reference_time')),
351
+ 'created_at': _native_datetime(record.get('created_at')),
352
+ }
353
+ )
354
+ for record in records
355
+ }
356
+
357
+
358
+ async def _read_revisions(store, space_id: str, item_ids: list[str]):
359
+ if not item_ids:
360
+ return {}
361
+ records, _, _ = await store.runtime.driver.execute_query(
362
+ '''
363
+ MATCH (:FuliSpace {id: $space_id, kind: 'personal'})-
364
+ [:HAS_KNOWLEDGE_REVISION]->(revision:FuliKnowledgeRevision)
365
+ WHERE revision.item_id IN $item_ids
366
+ RETURN revision ORDER BY revision.created_at DESC
367
+ ''',
368
+ space_id=space_id,
369
+ item_ids=item_ids,
370
+ routing_='r',
371
+ )
372
+ result = {}
373
+ for record in records:
374
+ value = dict(record['revision'])
375
+ result.setdefault(value['item_id'], []).append(revision_record(value))
376
+ return result
377
+
378
+
379
+ async def _read_assignments(store, space_id: str, item_ids: list[str]):
380
+ if not item_ids:
381
+ return {}
382
+ records, _, _ = await store.runtime.driver.execute_query(
383
+ '''
384
+ MATCH (:FuliSpace {id: $space_id, kind: 'personal'})-
385
+ [:HAS_KNOWLEDGE_ASSIGNMENT]->(assignment:FuliKnowledgeAssignment)
386
+ WHERE assignment.item_id IN $item_ids
387
+ RETURN assignment
388
+ ''',
389
+ space_id=space_id,
390
+ item_ids=item_ids,
391
+ routing_='r',
392
+ )
393
+ result = {}
394
+ for record in records:
395
+ value = dict(record['assignment'])
396
+ result.setdefault(value['item_id'], []).append(assignment_record(value))
397
+ return result
398
+
399
+
400
+ def _graph_node(
401
+ record,
402
+ evidence,
403
+ revisions,
404
+ assignments,
405
+ references,
406
+ conflicts,
407
+ audits=None,
408
+ ) -> GraphNode:
409
+ episode_ids = record.get('episodes') or []
410
+ status, basis, confidence, confirmation_explicit = _confirmation_projection(
411
+ record
412
+ )
413
+ return GraphNode(
414
+ id=record['id'],
415
+ name=record['name'],
416
+ type=record['type'],
417
+ group_id=record['group_id'],
418
+ summary=record['summary'],
419
+ origin_quadrant=record.get('origin_quadrant') or 'known_known',
420
+ current_quadrant=record.get('current_quadrant') or 'known_known',
421
+ epistemic_status=record.get('epistemic_status') or 'confirmed',
422
+ epistemic_state_explicit=record.get('epistemic_state_explicit') is True,
423
+ confirmation_status=status,
424
+ confirmation_state_explicit=confirmation_explicit,
425
+ confirmation_basis=basis,
426
+ reasoning_summary=record.get('reasoning_summary'),
427
+ profile_aspect=record.get('profile_aspect'),
428
+ preference_scope=record.get('preference_scope'),
429
+ preference_project_id=record.get('preference_project_id'),
430
+ inheritance_mode=record.get('inheritance_mode') or 'local_only',
431
+ inherited_project_ids=record.get('inherited_project_ids') or [],
432
+ human_edited=record.get('human_edited') is True,
433
+ human_change_status=record.get('human_change_status') or 'none',
434
+ human_change_version=int(record.get('human_change_version') or 0),
435
+ last_human_changed_at=_native_datetime(
436
+ record.get('last_human_changed_at')
437
+ ),
438
+ last_agent_viewed_at=_native_datetime(
439
+ record.get('last_agent_viewed_at')
440
+ ),
441
+ last_agent_reviewed_at=_native_datetime(
442
+ record.get('last_agent_reviewed_at')
443
+ ),
444
+ utility_score=float(record.get('utility_score') or 0),
445
+ confidence_score=confidence,
446
+ qualified_use_count=int(record.get('qualified_use_count') or 0),
447
+ distinct_task_count=int(record.get('distinct_task_count') or 0),
448
+ last_used_at=_native_datetime(record.get('last_used_at')),
449
+ usage_generation=int(record.get('usage_generation') or 1),
450
+ attributes=json_object(record.get('attributes_json')),
451
+ evidence=[evidence[item] for item in episode_ids if item in evidence],
452
+ created_at=_native_datetime(record.get('created_at')),
453
+ invalid_at=_native_datetime(record.get('invalid_at')),
454
+ replaced_by_item_id=record.get('replaced_by_item_id'),
455
+ replaced_by_item_kind=record.get('replaced_by_item_kind'),
456
+ revisions=revisions.get(record['id'], []),
457
+ assignments=assignments.get(record['id'], []),
458
+ project_references=references.get(record['id'], []),
459
+ conflicts=conflicts.get(record['id'], []),
460
+ audit_events=(audits or {}).get(record['id'], []),
461
+ )
462
+
463
+
464
+ def _graph_edge(
465
+ record,
466
+ evidence,
467
+ revisions,
468
+ assignments,
469
+ references,
470
+ conflicts,
471
+ audits=None,
472
+ ) -> GraphEdge:
473
+ episode_ids = record.get('episodes') or []
474
+ status, basis, confidence, confirmation_explicit = _confirmation_projection(
475
+ record
476
+ )
477
+ return GraphEdge(
478
+ id=record['id'],
479
+ source=record['source'],
480
+ target=record['target'],
481
+ type=record['type'],
482
+ fact=record['fact'],
483
+ origin_quadrant=record.get('origin_quadrant') or 'known_known',
484
+ current_quadrant=record.get('current_quadrant') or 'known_known',
485
+ epistemic_status=record.get('epistemic_status') or 'confirmed',
486
+ epistemic_state_explicit=record.get('epistemic_state_explicit') is True,
487
+ confirmation_status=status,
488
+ confirmation_state_explicit=confirmation_explicit,
489
+ confirmation_basis=basis,
490
+ reasoning_summary=record.get('reasoning_summary'),
491
+ profile_aspect=record.get('profile_aspect'),
492
+ preference_scope=record.get('preference_scope'),
493
+ preference_project_id=record.get('preference_project_id'),
494
+ inheritance_mode=record.get('inheritance_mode') or 'local_only',
495
+ inherited_project_ids=record.get('inherited_project_ids') or [],
496
+ human_edited=record.get('human_edited') is True,
497
+ human_change_status=record.get('human_change_status') or 'none',
498
+ human_change_version=int(record.get('human_change_version') or 0),
499
+ last_human_changed_at=_native_datetime(
500
+ record.get('last_human_changed_at')
501
+ ),
502
+ last_agent_viewed_at=_native_datetime(
503
+ record.get('last_agent_viewed_at')
504
+ ),
505
+ last_agent_reviewed_at=_native_datetime(
506
+ record.get('last_agent_reviewed_at')
507
+ ),
508
+ utility_score=float(record.get('utility_score') or 0),
509
+ confidence_score=confidence,
510
+ qualified_use_count=int(record.get('qualified_use_count') or 0),
511
+ distinct_task_count=int(record.get('distinct_task_count') or 0),
512
+ last_used_at=_native_datetime(record.get('last_used_at')),
513
+ usage_generation=int(record.get('usage_generation') or 1),
514
+ valid_at=_native_datetime(record.get('valid_at')),
515
+ invalid_at=_native_datetime(record.get('invalid_at')),
516
+ replaced_by_item_id=record.get('replaced_by_item_id'),
517
+ replaced_by_item_kind=record.get('replaced_by_item_kind'),
518
+ created_at=_native_datetime(record.get('created_at')),
519
+ confidence=record.get('confidence'),
520
+ attributes=json_object(record.get('attributes_json')),
521
+ episodes=episode_ids,
522
+ evidence=[evidence[item] for item in episode_ids if item in evidence],
523
+ revisions=revisions.get(record['id'], []),
524
+ assignments=assignments.get(record['id'], []),
525
+ project_references=references.get(record['id'], []),
526
+ conflicts=conflicts.get(record['id'], []),
527
+ audit_events=(audits or {}).get(record['id'], []),
528
+ )
529
+
530
+
531
+ def _confirmation_basis(value) -> ConfirmationBasis | None:
532
+ data = json_object(value)
533
+ if not data:
534
+ return None
535
+ try:
536
+ return ConfirmationBasis.model_validate(data)
537
+ except ValueError:
538
+ return None
539
+
540
+
541
+ def _confirmation_projection(record) -> tuple[
542
+ str,
543
+ ConfirmationBasis | None,
544
+ float,
545
+ bool,
546
+ ]:
547
+ raw_status = record.get('confirmation_status') or 'pending'
548
+ status = effective_confirmation_status(record)
549
+ confidence = float(
550
+ record['confidence_score']
551
+ if record.get('confidence_score') is not None else 0.5
552
+ )
553
+ if status != raw_status:
554
+ confidence = min(confidence, 0.5)
555
+ basis = _confirmation_basis(record.get('confirmation_basis_json'))
556
+ explicit = (
557
+ record.get('confirmation_state_explicit') is True
558
+ and status == raw_status
559
+ and basis is not None
560
+ )
561
+ return status, basis, confidence, explicit