@jlongo78/agent-spaces 0.7.5 → 0.7.7

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 (634) hide show
  1. package/.next/standalone/.claude/settings.local.json +55 -0
  2. package/.next/standalone/.claude/spaces-env.json +1 -0
  3. package/.next/standalone/.next/BUILD_ID +1 -1
  4. package/.next/standalone/.next/app-path-routes-manifest.json +2 -1
  5. package/.next/standalone/.next/build-manifest.json +5 -5
  6. package/.next/standalone/.next/prerender-manifest.json +27 -3
  7. package/.next/standalone/.next/required-server-files.json +19 -19
  8. package/.next/standalone/.next/routes-manifest.json +6 -0
  9. package/.next/standalone/.next/server/app/(desktop)/admin/analytics/page/build-manifest.json +3 -3
  10. package/.next/standalone/.next/server/app/(desktop)/admin/analytics/page_client-reference-manifest.js +1 -1
  11. package/.next/standalone/.next/server/app/(desktop)/admin/users/page/build-manifest.json +3 -3
  12. package/.next/standalone/.next/server/app/(desktop)/admin/users/page_client-reference-manifest.js +1 -1
  13. package/.next/standalone/.next/server/app/(desktop)/analytics/page/build-manifest.json +3 -3
  14. package/.next/standalone/.next/server/app/(desktop)/analytics/page_client-reference-manifest.js +1 -1
  15. package/.next/standalone/.next/server/app/(desktop)/cortex/page/build-manifest.json +3 -3
  16. package/.next/standalone/.next/server/app/(desktop)/cortex/page/react-loadable-manifest.json +3 -3
  17. package/.next/standalone/.next/server/app/(desktop)/cortex/page.js.nft.json +1 -1
  18. package/.next/standalone/.next/server/app/(desktop)/cortex/page_client-reference-manifest.js +1 -1
  19. package/.next/standalone/.next/server/app/(desktop)/network/page/build-manifest.json +3 -3
  20. package/.next/standalone/.next/server/app/(desktop)/network/page_client-reference-manifest.js +1 -1
  21. package/.next/standalone/.next/server/app/(desktop)/page/build-manifest.json +3 -3
  22. package/.next/standalone/.next/server/app/(desktop)/page_client-reference-manifest.js +1 -1
  23. package/.next/standalone/.next/server/app/(desktop)/projects/page/build-manifest.json +3 -3
  24. package/.next/standalone/.next/server/app/(desktop)/projects/page_client-reference-manifest.js +1 -1
  25. package/.next/standalone/.next/server/app/(desktop)/sessions/[id]/page/build-manifest.json +3 -3
  26. package/.next/standalone/.next/server/app/(desktop)/sessions/[id]/page_client-reference-manifest.js +1 -1
  27. package/.next/standalone/.next/server/app/(desktop)/sessions/page/build-manifest.json +3 -3
  28. package/.next/standalone/.next/server/app/(desktop)/sessions/page_client-reference-manifest.js +1 -1
  29. package/.next/standalone/.next/server/app/(desktop)/settings/page/build-manifest.json +3 -3
  30. package/.next/standalone/.next/server/app/(desktop)/settings/page_client-reference-manifest.js +1 -1
  31. package/.next/standalone/.next/server/app/(desktop)/terminal/page/build-manifest.json +3 -3
  32. package/.next/standalone/.next/server/app/(desktop)/terminal/page.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/(desktop)/terminal/page_client-reference-manifest.js +1 -1
  34. package/.next/standalone/.next/server/app/(desktop)/terminal/pane/[id]/page/build-manifest.json +3 -3
  35. package/.next/standalone/.next/server/app/(desktop)/terminal/pane/[id]/page.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/(desktop)/terminal/pane/[id]/page_client-reference-manifest.js +1 -1
  37. package/.next/standalone/.next/server/app/(desktop)/terminal/remote/[nodeId]/[workspaceId]/page/build-manifest.json +3 -3
  38. package/.next/standalone/.next/server/app/(desktop)/terminal/remote/[nodeId]/[workspaceId]/page.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/(desktop)/terminal/remote/[nodeId]/[workspaceId]/page_client-reference-manifest.js +1 -1
  40. package/.next/standalone/.next/server/app/(desktop)/workspaces/page/build-manifest.json +3 -3
  41. package/.next/standalone/.next/server/app/(desktop)/workspaces/page_client-reference-manifest.js +1 -1
  42. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +3 -3
  43. package/.next/standalone/.next/server/app/_global-error.html +2 -2
  44. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  45. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  46. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  47. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  48. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  49. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  50. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +3 -3
  51. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  52. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  53. package/.next/standalone/.next/server/app/_not-found.rsc +2 -2
  54. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  55. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  56. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  57. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  58. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  59. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  60. package/.next/standalone/.next/server/app/admin/analytics.html +1 -1
  61. package/.next/standalone/.next/server/app/admin/analytics.rsc +7 -6
  62. package/.next/standalone/.next/server/app/admin/analytics.segments/!KGRlc2t0b3Ap/admin/analytics/__PAGE__.segment.rsc +2 -2
  63. package/.next/standalone/.next/server/app/admin/analytics.segments/!KGRlc2t0b3Ap/admin/analytics.segment.rsc +1 -1
  64. package/.next/standalone/.next/server/app/admin/analytics.segments/!KGRlc2t0b3Ap/admin.segment.rsc +1 -1
  65. package/.next/standalone/.next/server/app/admin/analytics.segments/!KGRlc2t0b3Ap.segment.rsc +1 -1
  66. package/.next/standalone/.next/server/app/admin/analytics.segments/_full.segment.rsc +7 -6
  67. package/.next/standalone/.next/server/app/admin/analytics.segments/_head.segment.rsc +1 -1
  68. package/.next/standalone/.next/server/app/admin/analytics.segments/_index.segment.rsc +2 -2
  69. package/.next/standalone/.next/server/app/admin/analytics.segments/_tree.segment.rsc +2 -2
  70. package/.next/standalone/.next/server/app/admin/users.html +1 -1
  71. package/.next/standalone/.next/server/app/admin/users.rsc +2 -2
  72. package/.next/standalone/.next/server/app/admin/users.segments/!KGRlc2t0b3Ap/admin/users/__PAGE__.segment.rsc +1 -1
  73. package/.next/standalone/.next/server/app/admin/users.segments/!KGRlc2t0b3Ap/admin/users.segment.rsc +1 -1
  74. package/.next/standalone/.next/server/app/admin/users.segments/!KGRlc2t0b3Ap/admin.segment.rsc +1 -1
  75. package/.next/standalone/.next/server/app/admin/users.segments/!KGRlc2t0b3Ap.segment.rsc +1 -1
  76. package/.next/standalone/.next/server/app/admin/users.segments/_full.segment.rsc +2 -2
  77. package/.next/standalone/.next/server/app/admin/users.segments/_head.segment.rsc +1 -1
  78. package/.next/standalone/.next/server/app/admin/users.segments/_index.segment.rsc +2 -2
  79. package/.next/standalone/.next/server/app/admin/users.segments/_tree.segment.rsc +2 -2
  80. package/.next/standalone/.next/server/app/analytics.html +1 -1
  81. package/.next/standalone/.next/server/app/analytics.rsc +3 -3
  82. package/.next/standalone/.next/server/app/analytics.segments/!KGRlc2t0b3Ap/analytics/__PAGE__.segment.rsc +2 -2
  83. package/.next/standalone/.next/server/app/analytics.segments/!KGRlc2t0b3Ap/analytics.segment.rsc +1 -1
  84. package/.next/standalone/.next/server/app/analytics.segments/!KGRlc2t0b3Ap.segment.rsc +1 -1
  85. package/.next/standalone/.next/server/app/analytics.segments/_full.segment.rsc +3 -3
  86. package/.next/standalone/.next/server/app/analytics.segments/_head.segment.rsc +1 -1
  87. package/.next/standalone/.next/server/app/analytics.segments/_index.segment.rsc +2 -2
  88. package/.next/standalone/.next/server/app/analytics.segments/_tree.segment.rsc +2 -2
  89. package/.next/standalone/.next/server/app/api/analytics/overview/route.js.nft.json +1 -1
  90. package/.next/standalone/.next/server/app/api/bulk/route.js.nft.json +1 -1
  91. package/.next/standalone/.next/server/app/api/config/route.js.nft.json +1 -1
  92. package/.next/standalone/.next/server/app/api/cortex/context/route.js.nft.json +1 -1
  93. package/.next/standalone/.next/server/app/api/cortex/curation/assess/route.js.nft.json +1 -1
  94. package/.next/standalone/.next/server/app/api/cortex/curation/publish/route.js.nft.json +1 -1
  95. package/.next/standalone/.next/server/app/api/cortex/curation/refine/route.js.nft.json +1 -1
  96. package/.next/standalone/.next/server/app/api/cortex/curation/review/route.js.nft.json +1 -1
  97. package/.next/standalone/.next/server/app/api/cortex/curation/seed/route.js.nft.json +1 -1
  98. package/.next/standalone/.next/server/app/api/cortex/export/route.js.nft.json +1 -1
  99. package/.next/standalone/.next/server/app/api/cortex/federation/pending/route.js.nft.json +1 -1
  100. package/.next/standalone/.next/server/app/api/cortex/federation/resolve/route.js.nft.json +1 -1
  101. package/.next/standalone/.next/server/app/api/cortex/federation/search/route.js.nft.json +1 -1
  102. package/.next/standalone/.next/server/app/api/cortex/federation/teach/route.js.nft.json +1 -1
  103. package/.next/standalone/.next/server/app/api/cortex/graph/edges/route.js.nft.json +1 -1
  104. package/.next/standalone/.next/server/app/api/cortex/graph/entities/[id]/route.js.nft.json +1 -1
  105. package/.next/standalone/.next/server/app/api/cortex/graph/entities/route.js.nft.json +1 -1
  106. package/.next/standalone/.next/server/app/api/cortex/graph/populate/route.js.nft.json +1 -1
  107. package/.next/standalone/.next/server/app/api/cortex/import/route.js.nft.json +1 -1
  108. package/.next/standalone/.next/server/app/api/cortex/import/status/route.js.nft.json +1 -1
  109. package/.next/standalone/.next/server/app/api/cortex/ingest/bootstrap/route.js.nft.json +1 -1
  110. package/.next/standalone/.next/server/app/api/cortex/ingest/status/route.js.nft.json +1 -1
  111. package/.next/standalone/.next/server/app/api/cortex/knowledge/[id]/route.js.nft.json +1 -1
  112. package/.next/standalone/.next/server/app/api/cortex/knowledge/route.js.nft.json +1 -1
  113. package/.next/standalone/.next/server/app/api/cortex/lobes/[id]/route.js.nft.json +1 -1
  114. package/.next/standalone/.next/server/app/api/cortex/lobes/route.js.nft.json +1 -1
  115. package/.next/standalone/.next/server/app/api/cortex/lobes/share/route.js.nft.json +1 -1
  116. package/.next/standalone/.next/server/app/api/cortex/marketplace/browse/route.js.nft.json +1 -1
  117. package/.next/standalone/.next/server/app/api/cortex/marketplace/preview/route.js.nft.json +1 -1
  118. package/.next/standalone/.next/server/app/api/cortex/mcp/call/route.js.nft.json +1 -1
  119. package/.next/standalone/.next/server/app/api/cortex/mcp/tools/route.js.nft.json +1 -1
  120. package/.next/standalone/.next/server/app/api/cortex/search/route.js.nft.json +1 -1
  121. package/.next/standalone/.next/server/app/api/cortex/settings/route.js.nft.json +1 -1
  122. package/.next/standalone/.next/server/app/api/cortex/status/route.js.nft.json +1 -1
  123. package/.next/standalone/.next/server/app/api/cortex/timeline/route.js.nft.json +1 -1
  124. package/.next/standalone/.next/server/app/api/cortex/usage/route.js.nft.json +1 -1
  125. package/.next/standalone/.next/server/app/api/cortex/workspace/[id]/context/route.js.nft.json +1 -1
  126. package/.next/standalone/.next/server/app/api/events/route.js.nft.json +1 -1
  127. package/.next/standalone/.next/server/app/api/folders/route.js.nft.json +1 -1
  128. package/.next/standalone/.next/server/app/api/network/handshake/route.js.nft.json +1 -1
  129. package/.next/standalone/.next/server/app/api/network/projects/route.js.nft.json +1 -1
  130. package/.next/standalone/.next/server/app/api/network/search/route.js.nft.json +1 -1
  131. package/.next/standalone/.next/server/app/api/network/sessions/[id]/messages/route.js.nft.json +1 -1
  132. package/.next/standalone/.next/server/app/api/network/sessions/[id]/route.js.nft.json +1 -1
  133. package/.next/standalone/.next/server/app/api/network/sessions/route.js.nft.json +1 -1
  134. package/.next/standalone/.next/server/app/api/network/workspaces/[id]/route.js.nft.json +1 -1
  135. package/.next/standalone/.next/server/app/api/network/workspaces/route.js.nft.json +1 -1
  136. package/.next/standalone/.next/server/app/api/panes/[id]/route.js.nft.json +1 -1
  137. package/.next/standalone/.next/server/app/api/panes/route.js.nft.json +1 -1
  138. package/.next/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
  139. package/.next/standalone/.next/server/app/api/search/route.js.nft.json +1 -1
  140. package/.next/standalone/.next/server/app/api/sessions/[id]/chat/route.js.nft.json +1 -1
  141. package/.next/standalone/.next/server/app/api/sessions/[id]/messages/route.js.nft.json +1 -1
  142. package/.next/standalone/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  143. package/.next/standalone/.next/server/app/api/sessions/route.js.nft.json +1 -1
  144. package/.next/standalone/.next/server/app/api/sync/route.js.nft.json +1 -1
  145. package/.next/standalone/.next/server/app/api/tags/route.js.nft.json +1 -1
  146. package/.next/standalone/.next/server/app/api/tier/route.js.nft.json +1 -1
  147. package/.next/standalone/.next/server/app/api/workspaces/[id]/context/[key]/route.js.nft.json +1 -1
  148. package/.next/standalone/.next/server/app/api/workspaces/[id]/context/route.js.nft.json +1 -1
  149. package/.next/standalone/.next/server/app/api/workspaces/[id]/messages/[msgId]/route.js.nft.json +1 -1
  150. package/.next/standalone/.next/server/app/api/workspaces/[id]/messages/route.js.nft.json +1 -1
  151. package/.next/standalone/.next/server/app/api/workspaces/[id]/route.js.nft.json +1 -1
  152. package/.next/standalone/.next/server/app/api/workspaces/[id]/sessions/route.js.nft.json +1 -1
  153. package/.next/standalone/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  154. package/.next/standalone/.next/server/app/cortex.html +1 -1
  155. package/.next/standalone/.next/server/app/cortex.rsc +3 -3
  156. package/.next/standalone/.next/server/app/cortex.segments/!KGRlc2t0b3Ap/cortex/__PAGE__.segment.rsc +2 -2
  157. package/.next/standalone/.next/server/app/cortex.segments/!KGRlc2t0b3Ap/cortex.segment.rsc +1 -1
  158. package/.next/standalone/.next/server/app/cortex.segments/!KGRlc2t0b3Ap.segment.rsc +1 -1
  159. package/.next/standalone/.next/server/app/cortex.segments/_full.segment.rsc +3 -3
  160. package/.next/standalone/.next/server/app/cortex.segments/_head.segment.rsc +1 -1
  161. package/.next/standalone/.next/server/app/cortex.segments/_index.segment.rsc +2 -2
  162. package/.next/standalone/.next/server/app/cortex.segments/_tree.segment.rsc +2 -2
  163. package/.next/standalone/.next/server/app/login/page/build-manifest.json +3 -3
  164. package/.next/standalone/.next/server/app/login/page_client-reference-manifest.js +1 -1
  165. package/.next/standalone/.next/server/app/login.html +1 -1
  166. package/.next/standalone/.next/server/app/login.rsc +2 -2
  167. package/.next/standalone/.next/server/app/login.segments/_full.segment.rsc +2 -2
  168. package/.next/standalone/.next/server/app/login.segments/_head.segment.rsc +1 -1
  169. package/.next/standalone/.next/server/app/login.segments/_index.segment.rsc +2 -2
  170. package/.next/standalone/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  171. package/.next/standalone/.next/server/app/login.segments/login/__PAGE__.segment.rsc +1 -1
  172. package/.next/standalone/.next/server/app/login.segments/login.segment.rsc +1 -1
  173. package/.next/standalone/.next/server/app/m/page/build-manifest.json +3 -3
  174. package/.next/standalone/.next/server/app/m/page_client-reference-manifest.js +1 -1
  175. package/.next/standalone/.next/server/app/m/projects/page/build-manifest.json +3 -3
  176. package/.next/standalone/.next/server/app/m/projects/page_client-reference-manifest.js +1 -1
  177. package/.next/standalone/.next/server/app/m/projects.html +1 -1
  178. package/.next/standalone/.next/server/app/m/projects.rsc +2 -2
  179. package/.next/standalone/.next/server/app/m/projects.segments/_full.segment.rsc +2 -2
  180. package/.next/standalone/.next/server/app/m/projects.segments/_head.segment.rsc +1 -1
  181. package/.next/standalone/.next/server/app/m/projects.segments/_index.segment.rsc +2 -2
  182. package/.next/standalone/.next/server/app/m/projects.segments/_tree.segment.rsc +2 -2
  183. package/.next/standalone/.next/server/app/m/projects.segments/m/projects/__PAGE__.segment.rsc +1 -1
  184. package/.next/standalone/.next/server/app/m/projects.segments/m/projects.segment.rsc +1 -1
  185. package/.next/standalone/.next/server/app/m/projects.segments/m.segment.rsc +1 -1
  186. package/.next/standalone/.next/server/app/m/sessions/[id]/page/build-manifest.json +3 -3
  187. package/.next/standalone/.next/server/app/m/sessions/[id]/page_client-reference-manifest.js +1 -1
  188. package/.next/standalone/.next/server/app/m/sessions/page/build-manifest.json +3 -3
  189. package/.next/standalone/.next/server/app/m/sessions/page_client-reference-manifest.js +1 -1
  190. package/.next/standalone/.next/server/app/m/sessions.html +1 -1
  191. package/.next/standalone/.next/server/app/m/sessions.rsc +2 -2
  192. package/.next/standalone/.next/server/app/m/sessions.segments/_full.segment.rsc +2 -2
  193. package/.next/standalone/.next/server/app/m/sessions.segments/_head.segment.rsc +1 -1
  194. package/.next/standalone/.next/server/app/m/sessions.segments/_index.segment.rsc +2 -2
  195. package/.next/standalone/.next/server/app/m/sessions.segments/_tree.segment.rsc +2 -2
  196. package/.next/standalone/.next/server/app/m/sessions.segments/m/sessions/__PAGE__.segment.rsc +1 -1
  197. package/.next/standalone/.next/server/app/m/sessions.segments/m/sessions.segment.rsc +1 -1
  198. package/.next/standalone/.next/server/app/m/sessions.segments/m.segment.rsc +1 -1
  199. package/.next/standalone/.next/server/app/m/settings/page/build-manifest.json +3 -3
  200. package/.next/standalone/.next/server/app/m/settings/page_client-reference-manifest.js +1 -1
  201. package/.next/standalone/.next/server/app/m/settings.html +1 -1
  202. package/.next/standalone/.next/server/app/m/settings.rsc +2 -2
  203. package/.next/standalone/.next/server/app/m/settings.segments/_full.segment.rsc +2 -2
  204. package/.next/standalone/.next/server/app/m/settings.segments/_head.segment.rsc +1 -1
  205. package/.next/standalone/.next/server/app/m/settings.segments/_index.segment.rsc +2 -2
  206. package/.next/standalone/.next/server/app/m/settings.segments/_tree.segment.rsc +2 -2
  207. package/.next/standalone/.next/server/app/m/settings.segments/m/settings/__PAGE__.segment.rsc +1 -1
  208. package/.next/standalone/.next/server/app/m/settings.segments/m/settings.segment.rsc +1 -1
  209. package/.next/standalone/.next/server/app/m/settings.segments/m.segment.rsc +1 -1
  210. package/.next/standalone/.next/server/app/m/terminal/page/build-manifest.json +3 -3
  211. package/.next/standalone/.next/server/app/m/terminal/page_client-reference-manifest.js +1 -1
  212. package/.next/standalone/.next/server/app/m/terminal.html +1 -1
  213. package/.next/standalone/.next/server/app/m/terminal.rsc +3 -3
  214. package/.next/standalone/.next/server/app/m/terminal.segments/_full.segment.rsc +3 -3
  215. package/.next/standalone/.next/server/app/m/terminal.segments/_head.segment.rsc +1 -1
  216. package/.next/standalone/.next/server/app/m/terminal.segments/_index.segment.rsc +2 -2
  217. package/.next/standalone/.next/server/app/m/terminal.segments/_tree.segment.rsc +2 -2
  218. package/.next/standalone/.next/server/app/m/terminal.segments/m/terminal/__PAGE__.segment.rsc +2 -2
  219. package/.next/standalone/.next/server/app/m/terminal.segments/m/terminal.segment.rsc +1 -1
  220. package/.next/standalone/.next/server/app/m/terminal.segments/m.segment.rsc +1 -1
  221. package/.next/standalone/.next/server/app/m.html +1 -1
  222. package/.next/standalone/.next/server/app/m.rsc +2 -2
  223. package/.next/standalone/.next/server/app/m.segments/_full.segment.rsc +2 -2
  224. package/.next/standalone/.next/server/app/m.segments/_head.segment.rsc +1 -1
  225. package/.next/standalone/.next/server/app/m.segments/_index.segment.rsc +2 -2
  226. package/.next/standalone/.next/server/app/m.segments/_tree.segment.rsc +2 -2
  227. package/.next/standalone/.next/server/app/m.segments/m/__PAGE__.segment.rsc +1 -1
  228. package/.next/standalone/.next/server/app/m.segments/m.segment.rsc +1 -1
  229. package/.next/standalone/.next/server/app/network.html +1 -1
  230. package/.next/standalone/.next/server/app/network.rsc +2 -2
  231. package/.next/standalone/.next/server/app/network.segments/!KGRlc2t0b3Ap/network/__PAGE__.segment.rsc +1 -1
  232. package/.next/standalone/.next/server/app/network.segments/!KGRlc2t0b3Ap/network.segment.rsc +1 -1
  233. package/.next/standalone/.next/server/app/network.segments/!KGRlc2t0b3Ap.segment.rsc +1 -1
  234. package/.next/standalone/.next/server/app/network.segments/_full.segment.rsc +2 -2
  235. package/.next/standalone/.next/server/app/network.segments/_head.segment.rsc +1 -1
  236. package/.next/standalone/.next/server/app/network.segments/_index.segment.rsc +2 -2
  237. package/.next/standalone/.next/server/app/network.segments/_tree.segment.rsc +2 -2
  238. package/.next/standalone/.next/server/app/projects.html +1 -1
  239. package/.next/standalone/.next/server/app/projects.rsc +2 -2
  240. package/.next/standalone/.next/server/app/projects.segments/!KGRlc2t0b3Ap/projects/__PAGE__.segment.rsc +1 -1
  241. package/.next/standalone/.next/server/app/projects.segments/!KGRlc2t0b3Ap/projects.segment.rsc +1 -1
  242. package/.next/standalone/.next/server/app/projects.segments/!KGRlc2t0b3Ap.segment.rsc +1 -1
  243. package/.next/standalone/.next/server/app/projects.segments/_full.segment.rsc +2 -2
  244. package/.next/standalone/.next/server/app/projects.segments/_head.segment.rsc +1 -1
  245. package/.next/standalone/.next/server/app/projects.segments/_index.segment.rsc +2 -2
  246. package/.next/standalone/.next/server/app/projects.segments/_tree.segment.rsc +2 -2
  247. package/.next/standalone/.next/server/app/sessions.html +1 -1
  248. package/.next/standalone/.next/server/app/sessions.rsc +2 -2
  249. package/.next/standalone/.next/server/app/sessions.segments/!KGRlc2t0b3Ap/sessions/__PAGE__.segment.rsc +1 -1
  250. package/.next/standalone/.next/server/app/sessions.segments/!KGRlc2t0b3Ap/sessions.segment.rsc +1 -1
  251. package/.next/standalone/.next/server/app/sessions.segments/!KGRlc2t0b3Ap.segment.rsc +1 -1
  252. package/.next/standalone/.next/server/app/sessions.segments/_full.segment.rsc +2 -2
  253. package/.next/standalone/.next/server/app/sessions.segments/_head.segment.rsc +1 -1
  254. package/.next/standalone/.next/server/app/sessions.segments/_index.segment.rsc +2 -2
  255. package/.next/standalone/.next/server/app/sessions.segments/_tree.segment.rsc +2 -2
  256. package/.next/standalone/.next/server/app/settings.html +1 -1
  257. package/.next/standalone/.next/server/app/settings.rsc +2 -2
  258. package/.next/standalone/.next/server/app/settings.segments/!KGRlc2t0b3Ap/settings/__PAGE__.segment.rsc +1 -1
  259. package/.next/standalone/.next/server/app/settings.segments/!KGRlc2t0b3Ap/settings.segment.rsc +1 -1
  260. package/.next/standalone/.next/server/app/settings.segments/!KGRlc2t0b3Ap.segment.rsc +1 -1
  261. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +2 -2
  262. package/.next/standalone/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  263. package/.next/standalone/.next/server/app/settings.segments/_index.segment.rsc +2 -2
  264. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  265. package/.next/standalone/.next/server/app/terminal.html +1 -1
  266. package/.next/standalone/.next/server/app/terminal.rsc +3 -3
  267. package/.next/standalone/.next/server/app/terminal.segments/!KGRlc2t0b3Ap/terminal/__PAGE__.segment.rsc +2 -2
  268. package/.next/standalone/.next/server/app/terminal.segments/!KGRlc2t0b3Ap/terminal.segment.rsc +1 -1
  269. package/.next/standalone/.next/server/app/terminal.segments/!KGRlc2t0b3Ap.segment.rsc +1 -1
  270. package/.next/standalone/.next/server/app/terminal.segments/_full.segment.rsc +3 -3
  271. package/.next/standalone/.next/server/app/terminal.segments/_head.segment.rsc +1 -1
  272. package/.next/standalone/.next/server/app/terminal.segments/_index.segment.rsc +2 -2
  273. package/.next/standalone/.next/server/app/terminal.segments/_tree.segment.rsc +2 -2
  274. package/.next/standalone/.next/server/app/vr/page/app-paths-manifest.json +3 -0
  275. package/.next/standalone/.next/server/app/vr/page/build-manifest.json +18 -0
  276. package/.next/standalone/.next/server/app/vr/page/next-font-manifest.json +11 -0
  277. package/.next/standalone/.next/server/app/vr/page/react-loadable-manifest.json +11 -0
  278. package/.next/standalone/.next/server/app/vr/page/server-reference-manifest.json +4 -0
  279. package/.next/standalone/.next/server/app/vr/page.js +17 -0
  280. package/.next/standalone/.next/server/app/vr/page.js.map +5 -0
  281. package/.next/standalone/.next/server/app/vr/page.js.nft.json +1 -0
  282. package/.next/standalone/.next/server/app/vr/page_client-reference-manifest.js +2 -0
  283. package/.next/standalone/.next/server/app/vr.html +1 -0
  284. package/.next/standalone/.next/server/app/vr.meta +15 -0
  285. package/.next/standalone/.next/server/app/vr.rsc +21 -0
  286. package/.next/standalone/.next/server/app/vr.segments/_full.segment.rsc +21 -0
  287. package/.next/standalone/.next/server/app/vr.segments/_head.segment.rsc +6 -0
  288. package/.next/standalone/.next/server/app/vr.segments/_index.segment.rsc +6 -0
  289. package/.next/standalone/.next/server/app/vr.segments/_tree.segment.rsc +4 -0
  290. package/.next/standalone/.next/server/app/vr.segments/vr/__PAGE__.segment.rsc +9 -0
  291. package/.next/standalone/.next/server/app/vr.segments/vr.segment.rsc +4 -0
  292. package/.next/standalone/.next/server/app/workspaces.html +1 -1
  293. package/.next/standalone/.next/server/app/workspaces.rsc +2 -2
  294. package/.next/standalone/.next/server/app/workspaces.segments/!KGRlc2t0b3Ap/workspaces/__PAGE__.segment.rsc +1 -1
  295. package/.next/standalone/.next/server/app/workspaces.segments/!KGRlc2t0b3Ap/workspaces.segment.rsc +1 -1
  296. package/.next/standalone/.next/server/app/workspaces.segments/!KGRlc2t0b3Ap.segment.rsc +1 -1
  297. package/.next/standalone/.next/server/app/workspaces.segments/_full.segment.rsc +2 -2
  298. package/.next/standalone/.next/server/app/workspaces.segments/_head.segment.rsc +1 -1
  299. package/.next/standalone/.next/server/app/workspaces.segments/_index.segment.rsc +2 -2
  300. package/.next/standalone/.next/server/app/workspaces.segments/_tree.segment.rsc +2 -2
  301. package/.next/standalone/.next/server/app-paths-manifest.json +2 -1
  302. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0041efe4._.js +2 -2
  303. package/.next/standalone/.next/server/chunks/[root-of-the-server]__00bf0ace._.js +2 -2
  304. package/.next/standalone/.next/server/chunks/[root-of-the-server]__08a68343._.js +1 -1
  305. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0add852f._.js +1 -1
  306. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0c113ed0._.js +1 -1
  307. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e1a27e0._.js +1 -1
  308. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e71d908._.js +3 -3
  309. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e9142f3._.js +2 -2
  310. package/.next/standalone/.next/server/chunks/[root-of-the-server]__10e47926._.js +2 -2
  311. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1194f2c1._.js +1 -1
  312. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1665dc78._.js +2 -2
  313. package/.next/standalone/.next/server/chunks/[root-of-the-server]__175cbabf._.js +2 -2
  314. package/.next/standalone/.next/server/chunks/[root-of-the-server]__19c2d094._.js +1 -1
  315. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1adae357._.js +2 -2
  316. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1d359752._.js +2 -2
  317. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1e8fabeb._.js +3 -3
  318. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1f8deca0._.js +8 -8
  319. package/.next/standalone/.next/server/chunks/[root-of-the-server]__253fdda1._.js +2 -2
  320. package/.next/standalone/.next/server/chunks/[root-of-the-server]__28e6434f._.js +2 -2
  321. package/.next/standalone/.next/server/chunks/[root-of-the-server]__2a386564._.js +3 -3
  322. package/.next/standalone/.next/server/chunks/[root-of-the-server]__2acbd703._.js +1 -1
  323. package/.next/standalone/.next/server/chunks/[root-of-the-server]__2acefabb._.js +1 -1
  324. package/.next/standalone/.next/server/chunks/[root-of-the-server]__2c20fb38._.js +2 -2
  325. package/.next/standalone/.next/server/chunks/[root-of-the-server]__309132cd._.js +1 -1
  326. package/.next/standalone/.next/server/chunks/[root-of-the-server]__33fec964._.js +3 -3
  327. package/.next/standalone/.next/server/chunks/[root-of-the-server]__3786d8ae._.js +2 -2
  328. package/.next/standalone/.next/server/chunks/[root-of-the-server]__3ae92407._.js +2 -2
  329. package/.next/standalone/.next/server/chunks/[root-of-the-server]__3beda9fe._.js +2 -2
  330. package/.next/standalone/.next/server/chunks/[root-of-the-server]__3e3f25a1._.js +1 -1
  331. package/.next/standalone/.next/server/chunks/[root-of-the-server]__4619e9bd._.js +1 -1
  332. package/.next/standalone/.next/server/chunks/[root-of-the-server]__4a051043._.js +2 -2
  333. package/.next/standalone/.next/server/chunks/[root-of-the-server]__50208a5f._.js +1 -1
  334. package/.next/standalone/.next/server/chunks/[root-of-the-server]__508002e4._.js +2 -2
  335. package/.next/standalone/.next/server/chunks/[root-of-the-server]__5086c373._.js +2 -2
  336. package/.next/standalone/.next/server/chunks/[root-of-the-server]__5913e097._.js +2 -2
  337. package/.next/standalone/.next/server/chunks/[root-of-the-server]__5b5f68d2._.js +2 -2
  338. package/.next/standalone/.next/server/chunks/[root-of-the-server]__5c1f2459._.js +2 -2
  339. package/.next/standalone/.next/server/chunks/[root-of-the-server]__5ec8c977._.js +2 -2
  340. package/.next/standalone/.next/server/chunks/[root-of-the-server]__5f8c694a._.js +1 -1
  341. package/.next/standalone/.next/server/chunks/[root-of-the-server]__63cebc6c._.js +2 -2
  342. package/.next/standalone/.next/server/chunks/[root-of-the-server]__64d30d4d._.js +2 -2
  343. package/.next/standalone/.next/server/chunks/[root-of-the-server]__6c54fc2e._.js +2 -2
  344. package/.next/standalone/.next/server/chunks/[root-of-the-server]__6dc1fb7e._.js +2 -2
  345. package/.next/standalone/.next/server/chunks/[root-of-the-server]__6e568102._.js +2 -2
  346. package/.next/standalone/.next/server/chunks/[root-of-the-server]__6faa04c0._.js +2 -2
  347. package/.next/standalone/.next/server/chunks/[root-of-the-server]__727d05f1._.js +1 -1
  348. package/.next/standalone/.next/server/chunks/[root-of-the-server]__74a34dc3._.js +2 -2
  349. package/.next/standalone/.next/server/chunks/[root-of-the-server]__75d12b32._.js +1 -1
  350. package/.next/standalone/.next/server/chunks/[root-of-the-server]__7e7250a4._.js +2 -2
  351. package/.next/standalone/.next/server/chunks/[root-of-the-server]__8309e0a4._.js +2 -2
  352. package/.next/standalone/.next/server/chunks/[root-of-the-server]__86cc0e2b._.js +6 -6
  353. package/.next/standalone/.next/server/chunks/[root-of-the-server]__8915603e._.js +1 -1
  354. package/.next/standalone/.next/server/chunks/[root-of-the-server]__89c2565a._.js +2 -2
  355. package/.next/standalone/.next/server/chunks/[root-of-the-server]__8d178ad9._.js +2 -2
  356. package/.next/standalone/.next/server/chunks/[root-of-the-server]__93ee06f3._.js +3 -3
  357. package/.next/standalone/.next/server/chunks/[root-of-the-server]__9e4c154a._.js +2 -2
  358. package/.next/standalone/.next/server/chunks/[root-of-the-server]__a1fbc199._.js +1 -1
  359. package/.next/standalone/.next/server/chunks/[root-of-the-server]__a9d2e1d3._.js +2 -2
  360. package/.next/standalone/.next/server/chunks/[root-of-the-server]__ae53d343._.js +2 -2
  361. package/.next/standalone/.next/server/chunks/[root-of-the-server]__b3a04cef._.js +2 -2
  362. package/.next/standalone/.next/server/chunks/[root-of-the-server]__b4270b77._.js +1 -1
  363. package/.next/standalone/.next/server/chunks/[root-of-the-server]__b6b6ce60._.js +1 -1
  364. package/.next/standalone/.next/server/chunks/[root-of-the-server]__b9545dd9._.js +1 -1
  365. package/.next/standalone/.next/server/chunks/[root-of-the-server]__c200e21a._.js +1 -1
  366. package/.next/standalone/.next/server/chunks/[root-of-the-server]__c3c74ca4._.js +1 -1
  367. package/.next/standalone/.next/server/chunks/[root-of-the-server]__c88b63f7._.js +2 -2
  368. package/.next/standalone/.next/server/chunks/[root-of-the-server]__cba5f007._.js +1 -1
  369. package/.next/standalone/.next/server/chunks/[root-of-the-server]__cbf4ceb0._.js +2 -2
  370. package/.next/standalone/.next/server/chunks/[root-of-the-server]__cefdba2f._.js +2 -2
  371. package/.next/standalone/.next/server/chunks/[root-of-the-server]__cf9e82bb._.js +2 -2
  372. package/.next/standalone/.next/server/chunks/[root-of-the-server]__d15515e3._.js +1 -1
  373. package/.next/standalone/.next/server/chunks/[root-of-the-server]__d2897392._.js +2 -2
  374. package/.next/standalone/.next/server/chunks/[root-of-the-server]__d3b2d856._.js +2 -2
  375. package/.next/standalone/.next/server/chunks/[root-of-the-server]__d73273ca._.js +2 -2
  376. package/.next/standalone/.next/server/chunks/[root-of-the-server]__d8417eb6._.js +2 -2
  377. package/.next/standalone/.next/server/chunks/[root-of-the-server]__db4726bc._.js +1 -1
  378. package/.next/standalone/.next/server/chunks/[root-of-the-server]__dc2a55de._.js +2 -2
  379. package/.next/standalone/.next/server/chunks/[root-of-the-server]__dc6e2e5f._.js +1 -1
  380. package/.next/standalone/.next/server/chunks/[root-of-the-server]__e0d4690b._.js +3 -3
  381. package/.next/standalone/.next/server/chunks/[root-of-the-server]__e3ecfd17._.js +3 -3
  382. package/.next/standalone/.next/server/chunks/[root-of-the-server]__e678dd53._.js +1 -1
  383. package/.next/standalone/.next/server/chunks/[root-of-the-server]__e9223f55._.js +2 -2
  384. package/.next/standalone/.next/server/chunks/[root-of-the-server]__ea630076._.js +3 -3
  385. package/.next/standalone/.next/server/chunks/[root-of-the-server]__eb8acb65._.js +1 -1
  386. package/.next/standalone/.next/server/chunks/[root-of-the-server]__f26ca49d._.js +1 -1
  387. package/.next/standalone/.next/server/chunks/[root-of-the-server]__f33e1101._.js +2 -2
  388. package/.next/standalone/.next/server/chunks/[root-of-the-server]__f3a4c668._.js +1 -1
  389. package/.next/standalone/.next/server/chunks/[root-of-the-server]__f515f865._.js +2 -2
  390. package/.next/standalone/.next/server/chunks/[root-of-the-server]__fceb5d60._.js +2 -2
  391. package/.next/standalone/.next/server/chunks/[root-of-the-server]__fed41403._.js +2 -2
  392. package/.next/standalone/.next/server/chunks/[root-of-the-server]__ff2e98c2._.js +2 -2
  393. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_339169c8.js +1 -1
  394. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_97dac613.js +1 -1
  395. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0d8d81ca._.js +1 -1
  396. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1425c64f._.js +1 -1
  397. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1d2ce8f1._.js +1 -1
  398. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__31137509._.js +1 -1
  399. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__3633a587._.js +1 -1
  400. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__3c79441b._.js +1 -1
  401. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__4ca0f26b._.js +1 -1
  402. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__5b90d3ad._.js +3 -0
  403. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__62a0b363._.js +1 -1
  404. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__66aca5d4._.js +1 -1
  405. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__68205a46._.js +1 -1
  406. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__69fd2efa._.js +1 -1
  407. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__85dcf0f7._.js +1 -1
  408. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__8c53a5da._.js +1 -1
  409. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__aecb1873._.js +1 -1
  410. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__b02cd143._.js +1 -1
  411. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__b9bcde11._.js +3 -0
  412. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__cac90169._.js +1 -1
  413. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__d25de2f0._.js +1 -1
  414. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__e2f86be8._.js +1 -1
  415. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__ee626b5b._.js +1 -1
  416. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__f39a9e98._.js +1 -1
  417. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__f3c566cd._.js +1 -1
  418. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__f76aa221._.js +1 -1
  419. package/.next/standalone/.next/server/chunks/ssr/_149d7fd4._.js +1 -1
  420. package/.next/standalone/.next/server/chunks/ssr/_2230ad2d._.js +1 -1
  421. package/.next/standalone/.next/server/chunks/ssr/_2e0dd6a7._.js +1 -1
  422. package/.next/standalone/.next/server/chunks/ssr/_3cd2355c._.js +1 -1
  423. package/.next/standalone/.next/server/chunks/ssr/_3d206597._.js +4 -0
  424. package/.next/standalone/.next/server/chunks/ssr/_47cc9af0._.js +1 -1
  425. package/.next/standalone/.next/server/chunks/ssr/_5cf334fd._.js +3 -0
  426. package/.next/standalone/.next/server/chunks/ssr/_7082788b._.js +1 -1
  427. package/.next/standalone/.next/server/chunks/ssr/_7154d8ae._.js +1 -1
  428. package/.next/standalone/.next/server/chunks/ssr/_75bb1b9a._.js +1 -1
  429. package/.next/standalone/.next/server/chunks/ssr/{_aeeff784._.js → _81abf587._.js} +2 -2
  430. package/.next/standalone/.next/server/chunks/ssr/_8acf81e2._.js +1 -1
  431. package/.next/standalone/.next/server/chunks/ssr/_8c36feb8._.js +1 -1
  432. package/.next/standalone/.next/server/chunks/ssr/_91e9bb86._.js +1 -1
  433. package/.next/standalone/.next/server/chunks/ssr/_ac4c1838._.js +1 -1
  434. package/.next/standalone/.next/server/chunks/ssr/_ad8515fc._.js +1 -1
  435. package/.next/standalone/.next/server/chunks/ssr/_b1f49e81._.js +1 -1
  436. package/.next/standalone/.next/server/chunks/ssr/_c0fe7614._.js +1 -1
  437. package/.next/standalone/.next/server/chunks/ssr/_d4825f5a._.js +1 -1
  438. package/.next/standalone/.next/server/chunks/ssr/_da10a9f4._.js +1 -1
  439. package/.next/standalone/.next/server/chunks/ssr/_db0abd0a._.js +3 -0
  440. package/.next/standalone/.next/server/chunks/ssr/_db2fec84._.js +1 -1
  441. package/.next/standalone/.next/server/chunks/ssr/_dee5d4a1._.js +1 -1
  442. package/.next/standalone/.next/server/chunks/ssr/_ef482c0c._.js +1 -1
  443. package/.next/standalone/.next/server/chunks/ssr/_efe43d2f._.js +1 -1
  444. package/.next/standalone/.next/server/chunks/ssr/_f4a4e116._.js +1 -1
  445. package/.next/standalone/.next/server/chunks/ssr/_f4d525d2._.js +1 -1
  446. package/.next/standalone/.next/server/chunks/ssr/_f4e57187._.js +3 -0
  447. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_vr_page_actions_3fb70d92.js +3 -0
  448. package/.next/standalone/.next/server/chunks/ssr/node_modules_32f9d62f._.js +1 -1
  449. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_02f39477.js +1 -1
  450. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_eedfc1fd._.js +1 -1
  451. package/.next/standalone/.next/server/chunks/ssr/src_40fa36ce._.js +7 -0
  452. package/.next/standalone/.next/server/chunks/ssr/src_app_(desktop)_cortex_page_tsx_0f33d8b3._.js +3 -0
  453. package/.next/standalone/.next/server/edge/chunks/[root-of-the-server]__32a0045c._.js +1 -1
  454. package/.next/standalone/.next/server/edge/chunks/_d73df637._.js +1 -1
  455. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  456. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  457. package/.next/standalone/.next/server/next-font-manifest.js +1 -1
  458. package/.next/standalone/.next/server/next-font-manifest.json +4 -0
  459. package/.next/standalone/.next/server/pages/404.html +1 -1
  460. package/.next/standalone/.next/server/pages/500.html +2 -2
  461. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  462. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  463. package/.next/standalone/.next/static/chunks/045c83caa4d15373.js +1 -0
  464. package/.next/standalone/.next/static/chunks/07ea09e6024a523b.js +1 -0
  465. package/.next/standalone/.next/static/chunks/{aae9e0fa485bd835.js → 158b52b84e647ac1.js} +2 -2
  466. package/.next/standalone/.next/static/chunks/232d8aae4fefab70.js +1 -0
  467. package/.next/standalone/.next/static/chunks/2ad22562bb37ecad.js +1011 -0
  468. package/.next/standalone/.next/static/chunks/{a4e5c700421eaa46.js → 412140a02893327a.js} +1 -1
  469. package/.next/standalone/.next/static/chunks/481cc11ae80b08b1.js +1 -0
  470. package/.next/standalone/.next/static/chunks/5325351ef49cb65f.js +1 -0
  471. package/.next/standalone/.next/static/chunks/559735e598ca3cbb.js +1 -0
  472. package/.next/standalone/.next/static/chunks/59c63d5af5cf3daf.js +1 -0
  473. package/.next/standalone/.next/static/chunks/5d5d7b0095dd52ae.js +1 -0
  474. package/.next/standalone/.next/static/chunks/69606d281c39f9b2.js +1 -0
  475. package/.next/standalone/.next/static/chunks/6ae575967d091df4.js +1 -0
  476. package/.next/standalone/.next/static/chunks/7f8455bb855a6c84.js +1 -0
  477. package/.next/standalone/.next/static/chunks/84fe8d44deeeb74f.js +757 -0
  478. package/.next/standalone/.next/static/chunks/898f380eba90427a.js +1 -0
  479. package/.next/standalone/.next/static/chunks/95339e55722bb4ca.js +5 -0
  480. package/.next/standalone/.next/static/chunks/9cd594813c539df9.js +1 -0
  481. package/.next/standalone/.next/static/chunks/{7424664c6ffa94bd.js → 9cfa0291d55d8d2a.js} +1 -1
  482. package/.next/standalone/.next/static/chunks/ad1423eed05d129b.js +1 -0
  483. package/.next/standalone/.next/static/chunks/ae7b146884c67d2a.js +1 -0
  484. package/.next/standalone/.next/static/chunks/b84072d72aa86417.js +1 -0
  485. package/.next/standalone/.next/static/chunks/c1a95aebf6725f64.css +3 -0
  486. package/.next/standalone/.next/static/chunks/c515eb77d9410aa0.js +5 -0
  487. package/.next/standalone/.next/static/chunks/{9899cf4c2bdbe61d.js → d9ae203a7f123546.js} +2 -2
  488. package/.next/standalone/.next/static/chunks/e116953dc83d4eec.js +1 -0
  489. package/.next/standalone/.next/static/chunks/fdc09bd135846960.js +1 -0
  490. package/.next/standalone/.next/static/chunks/ff0196911449e745.js +1 -0
  491. package/.next/standalone/.next/static/chunks/{turbopack-4c21186b79fb4c10.js → turbopack-e1a0994ed4af988c.js} +1 -1
  492. package/.next/standalone/.spaces/cortex-context.md +70 -0
  493. package/.next/standalone/bin/cortex-hook.sh +62 -62
  494. package/.next/standalone/bin/cortex-mcp.js +60 -60
  495. package/.next/standalone/docs/superpowers/plans/2026-03-13-cortex-wiring.md +1387 -1387
  496. package/.next/standalone/docs/superpowers/plans/2026-03-14-cortex-v2-entity-graph.md +1923 -1923
  497. package/.next/standalone/docs/superpowers/plans/2026-03-14-cortex-v2-knowledge-evolution.md +1113 -1113
  498. package/.next/standalone/docs/superpowers/plans/2026-03-15-cortex-v2-boundary-engine.md +853 -853
  499. package/.next/standalone/docs/superpowers/plans/2026-03-15-cortex-v2-context-engine.md +1274 -1274
  500. package/.next/standalone/docs/superpowers/plans/2026-03-15-cortex-v2-signal-ingestion.md +933 -933
  501. package/.next/standalone/docs/superpowers/plans/2026-03-16-cortex-lobes.md +1080 -1080
  502. package/.next/standalone/docs/superpowers/plans/2026-03-16-cortex-v2-gravity-system.md +768 -768
  503. package/.next/standalone/docs/superpowers/plans/2026-03-16-cortex-v2-ui.md +1108 -1108
  504. package/.next/standalone/docs/superpowers/plans/2026-03-18-cortex-ui-integration.md +1846 -1846
  505. package/.next/standalone/docs/superpowers/specs/2026-03-13-cortex-wiring-design.md +268 -268
  506. package/.next/standalone/docs/superpowers/specs/2026-03-14-cortex-v2-design.md +623 -623
  507. package/.next/standalone/docs/superpowers/specs/2026-03-16-cortex-lobes-design.md +263 -263
  508. package/.next/standalone/docs/superpowers/specs/2026-03-16-cortex-v2-ui-design.md +240 -240
  509. package/.next/standalone/docs/superpowers/specs/2026-03-18-cortex-ui-integration-design.md +341 -341
  510. package/.next/standalone/node_modules/@img/sharp-win32-x64/lib/sharp-win32-x64.node +0 -0
  511. package/.next/standalone/node_modules/@img/{sharp-linux-x64 → sharp-win32-x64}/package.json +39 -46
  512. package/.next/standalone/package.json +104 -102
  513. package/.next/standalone/server.js +1 -1
  514. package/.next/standalone/src/app/(desktop)/cortex/page.tsx +78 -78
  515. package/.next/standalone/src/app/api/cortex/context/route.ts +78 -78
  516. package/.next/standalone/src/app/api/cortex/curation/assess/route.ts +27 -27
  517. package/.next/standalone/src/app/api/cortex/curation/publish/route.ts +23 -23
  518. package/.next/standalone/src/app/api/cortex/curation/refine/route.ts +23 -23
  519. package/.next/standalone/src/app/api/cortex/curation/review/route.ts +29 -29
  520. package/.next/standalone/src/app/api/cortex/curation/seed/route.ts +23 -23
  521. package/.next/standalone/src/app/api/cortex/export/route.ts +40 -40
  522. package/.next/standalone/src/app/api/cortex/federation/pending/route.ts +20 -20
  523. package/.next/standalone/src/app/api/cortex/federation/resolve/route.ts +43 -43
  524. package/.next/standalone/src/app/api/cortex/federation/search/route.ts +35 -35
  525. package/.next/standalone/src/app/api/cortex/federation/teach/route.ts +76 -76
  526. package/.next/standalone/src/app/api/cortex/graph/edges/route.ts +112 -112
  527. package/.next/standalone/src/app/api/cortex/graph/entities/[id]/route.ts +73 -73
  528. package/.next/standalone/src/app/api/cortex/graph/entities/route.ts +75 -75
  529. package/.next/standalone/src/app/api/cortex/graph/populate/route.ts +203 -203
  530. package/.next/standalone/src/app/api/cortex/import/route.ts +75 -75
  531. package/.next/standalone/src/app/api/cortex/import/status/route.ts +15 -15
  532. package/.next/standalone/src/app/api/cortex/ingest/bootstrap/route.ts +29 -29
  533. package/.next/standalone/src/app/api/cortex/ingest/status/route.ts +15 -15
  534. package/.next/standalone/src/app/api/cortex/knowledge/[id]/route.ts +91 -91
  535. package/.next/standalone/src/app/api/cortex/knowledge/route.ts +93 -93
  536. package/.next/standalone/src/app/api/cortex/lobes/[id]/route.ts +67 -67
  537. package/.next/standalone/src/app/api/cortex/lobes/route.ts +22 -22
  538. package/.next/standalone/src/app/api/cortex/lobes/share/route.ts +80 -80
  539. package/.next/standalone/src/app/api/cortex/marketplace/browse/route.ts +43 -43
  540. package/.next/standalone/src/app/api/cortex/marketplace/preview/route.ts +46 -46
  541. package/.next/standalone/src/app/api/cortex/mcp/call/route.ts +11 -11
  542. package/.next/standalone/src/app/api/cortex/mcp/tools/route.ts +6 -6
  543. package/.next/standalone/src/app/api/cortex/search/route.ts +43 -43
  544. package/.next/standalone/src/app/api/cortex/settings/route.ts +33 -33
  545. package/.next/standalone/src/app/api/cortex/status/route.ts +169 -169
  546. package/.next/standalone/src/app/api/cortex/timeline/route.ts +42 -42
  547. package/.next/standalone/src/app/api/cortex/usage/route.ts +31 -31
  548. package/.next/standalone/src/app/api/cortex/workspace/[id]/context/route.ts +41 -41
  549. package/.next/standalone/src/components/cortex/constants.ts +29 -29
  550. package/.next/standalone/src/components/cortex/cortex-dashboard.tsx +304 -304
  551. package/.next/standalone/src/components/cortex/cortex-indicator.tsx +44 -44
  552. package/.next/standalone/src/components/cortex/cortex-panel.tsx +140 -140
  553. package/.next/standalone/src/components/cortex/cortex-settings.tsx +221 -221
  554. package/.next/standalone/src/components/cortex/curation-tab.tsx +810 -810
  555. package/.next/standalone/src/components/cortex/entity-detail.tsx +101 -101
  556. package/.next/standalone/src/components/cortex/entity-graph.tsx +382 -382
  557. package/.next/standalone/src/components/cortex/import-dialog.tsx +212 -212
  558. package/.next/standalone/src/components/cortex/injection-badge.tsx +72 -72
  559. package/.next/standalone/src/components/cortex/knowledge-card.tsx +109 -109
  560. package/.next/standalone/src/components/cortex/knowledge-tab.tsx +158 -158
  561. package/.next/standalone/src/components/cortex/lobe-settings.tsx +215 -215
  562. package/.next/standalone/src/components/cortex/marketplace-card.tsx +126 -126
  563. package/.next/standalone/src/components/cortex/marketplace-tab.tsx +113 -113
  564. package/.next/standalone/src/lib/cortex/config.ts +40 -40
  565. package/.next/standalone/src/lib/cortex/debug.ts +10 -10
  566. package/.next/standalone/src/lib/cortex/distillation/usage-store.ts +18 -18
  567. package/.next/standalone/src/lib/cortex/graph/resolver.ts +10 -10
  568. package/.next/standalone/src/lib/cortex/graph/types.ts +22 -22
  569. package/.next/standalone/src/lib/cortex/index.ts +56 -56
  570. package/.next/standalone/src/lib/cortex/ingestion/bootstrap.ts +14 -14
  571. package/.next/standalone/src/lib/cortex/knowledge/compat.ts +14 -14
  572. package/.next/standalone/src/lib/cortex/knowledge/contradiction.ts +10 -10
  573. package/.next/standalone/src/lib/cortex/knowledge/types.ts +67 -67
  574. package/.next/standalone/src/lib/cortex/lobes/config.ts +16 -16
  575. package/.next/standalone/src/lib/cortex/lobes/resolver.ts +8 -8
  576. package/.next/standalone/src/lib/cortex/lobes/shares.ts +14 -14
  577. package/.next/standalone/src/lib/cortex/mcp/server.ts +8 -8
  578. package/.next/standalone/src/lib/cortex/portability/exporter.ts +6 -6
  579. package/.next/standalone/src/lib/cortex/portability/importer.ts +10 -10
  580. package/.next/standalone/src/lib/cortex/retrieval/context-engine.ts +10 -10
  581. package/.next/standalone/src/lib/cortex/types.ts +39 -39
  582. package/.next/standalone/tsconfig.json +34 -34
  583. package/LICENSE +661 -661
  584. package/README.md +131 -131
  585. package/bin/cortex-hook.sh +62 -62
  586. package/bin/cortex-mcp.js +60 -60
  587. package/bin/fix-standalone-externals.js +79 -79
  588. package/bin/lib/auto-setup.js +110 -110
  589. package/bin/mdns-service.js +171 -171
  590. package/bin/postinstall.js +35 -35
  591. package/bin/setup-admin.js +195 -195
  592. package/bin/spaces-dev.js +208 -208
  593. package/bin/spaces-install.js +599 -599
  594. package/bin/spaces-reset-totp.js +50 -50
  595. package/bin/spaces-service.js +1020 -1020
  596. package/bin/spaces-setup.js +253 -253
  597. package/bin/spaces.js +776 -776
  598. package/bin/ssh-auth-keys.sh +68 -68
  599. package/bin/terminal-server.js +1683 -1649
  600. package/package.json +104 -102
  601. package/.next/standalone/.next/server/chunks/ssr/_078dd64d._.js +0 -3
  602. package/.next/standalone/.next/server/chunks/ssr/_701606d5._.js +0 -3
  603. package/.next/standalone/.next/server/chunks/ssr/_72b1de37._.js +0 -3
  604. package/.next/standalone/.next/server/chunks/ssr/_950142a4._.js +0 -3
  605. package/.next/standalone/.next/server/chunks/ssr/src_components_terminal_terminal-pane_tsx_803c5e2c._.js +0 -7
  606. package/.next/standalone/.next/static/chunks/18f168665aef1aab.js +0 -1
  607. package/.next/standalone/.next/static/chunks/25b7a243a404a1a7.js +0 -1
  608. package/.next/standalone/.next/static/chunks/4a50d2a3e9bc9b41.js +0 -1
  609. package/.next/standalone/.next/static/chunks/6c78a1dfa7ec2959.css +0 -3
  610. package/.next/standalone/.next/static/chunks/7e0091ab6c5ee8bd.js +0 -1
  611. package/.next/standalone/.next/static/chunks/869f562dc32e55f4.js +0 -1
  612. package/.next/standalone/.next/static/chunks/8b3f4572fec83caa.js +0 -5
  613. package/.next/standalone/.next/static/chunks/8d5419afc4b9116b.js +0 -1
  614. package/.next/standalone/.next/static/chunks/9b2c5451f0b67975.js +0 -1
  615. package/.next/standalone/.next/static/chunks/ac339e970df82fa5.js +0 -5
  616. package/.next/standalone/.next/static/chunks/e7772d64463868eb.js +0 -1
  617. package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/README.md +0 -46
  618. package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/lib/glib-2.0/include/glibconfig.h +0 -221
  619. package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/lib/index.js +0 -1
  620. package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/lib/libvips-cpp.so.8.17.3 +0 -0
  621. package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/package.json +0 -42
  622. package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/README.md +0 -46
  623. package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/glib-2.0/include/glibconfig.h +0 -221
  624. package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/index.js +0 -1
  625. package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/libvips-cpp.so.8.17.3 +0 -0
  626. package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json +0 -42
  627. package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/versions.json +0 -30
  628. package/.next/standalone/node_modules/@img/sharp-linux-x64/lib/sharp-linux-x64.node +0 -0
  629. package/.next/standalone/node_modules/@img/sharp-linuxmusl-x64/lib/sharp-linuxmusl-x64.node +0 -0
  630. package/.next/standalone/node_modules/@img/sharp-linuxmusl-x64/package.json +0 -46
  631. /package/.next/standalone/.next/static/{77VYbwIoyxFNr5xevTrCu → BEY-sql3lQLouidpurSQf}/_buildManifest.js +0 -0
  632. /package/.next/standalone/.next/static/{77VYbwIoyxFNr5xevTrCu → BEY-sql3lQLouidpurSQf}/_clientMiddlewareManifest.json +0 -0
  633. /package/.next/standalone/.next/static/{77VYbwIoyxFNr5xevTrCu → BEY-sql3lQLouidpurSQf}/_ssgManifest.js +0 -0
  634. /package/.next/standalone/node_modules/@img/{sharp-libvips-linux-x64 → sharp-win32-x64}/versions.json +0 -0
@@ -1,1923 +1,1923 @@
1
- # Cortex v2 — Pillar 1: Entity Graph Foundation
2
-
3
- > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
4
-
5
- **Goal:** Add a lightweight SQLite-backed relationship graph to Cortex that models people, teams, departments, projects, systems, modules, and topics — the skeleton on which all future Cortex v2 pillars depend.
6
-
7
- **Architecture:** A new `src/lib/cortex/graph/` module containing an `EntityGraph` class backed by `better-sqlite3` (already in package.json). The graph stores entities (nodes) and weighted edges (relationships) in three tables. Entity resolution provides alias-based and fuzzy lookup. BFS traversal computes graph distance for weight calculations. Auto-population seeds the graph from existing Spaces users and workspaces.
8
-
9
- **Tech Stack:** TypeScript, better-sqlite3, vitest, Next.js API routes
10
-
11
- **Spec:** `docs/superpowers/specs/2026-03-14-cortex-v2-design.md` — Pillar 1
12
-
13
- ---
14
-
15
- ## File Structure
16
-
17
- ```
18
- src/lib/cortex/graph/
19
- ├── types.ts — EntityType, EdgeRelation, Entity, Edge, interfaces
20
- ├── schema.ts — SQLite table DDL, migrations, constants
21
- ├── entity-graph.ts — EntityGraph class: entity CRUD, edge CRUD, traversal
22
- ├── resolver.ts — Entity resolution: alias lookup, fuzzy match
23
- └── auto-populate.ts — Seed graph from Spaces users, workspaces (git-based seeding deferred to Pillar 5: Signal Ingestion)
24
-
25
- tests/lib/cortex/graph/
26
- ├── entity-graph.test.ts — Entity + edge CRUD tests
27
- ├── traversal.test.ts — BFS distance, N-hop neighborhood tests
28
- ├── resolver.test.ts — Alias + fuzzy resolution tests
29
- └── auto-populate.test.ts — Auto-population tests
30
-
31
- src/app/api/cortex/graph/
32
- ├── entities/route.ts — GET (list/search), POST (create)
33
- ├── entities/[id]/route.ts — GET, PATCH, DELETE single entity
34
- └── edges/route.ts — GET (list), POST (create/upsert), DELETE (by query params)
35
- ```
36
-
37
- ---
38
-
39
- ## Chunk 1: Types, Schema, and Entity CRUD
40
-
41
- ### Task 1: Define graph types
42
-
43
- **Files:**
44
- - Create: `src/lib/cortex/graph/types.ts`
45
-
46
- - [ ] **Step 1: Create the types file**
47
-
48
- ```typescript
49
- // src/lib/cortex/graph/types.ts
50
-
51
- export const ENTITY_TYPES = [
52
- 'person', 'team', 'department', 'organization',
53
- 'project', 'system', 'module', 'topic',
54
- ] as const;
55
- export type EntityType = typeof ENTITY_TYPES[number];
56
-
57
- export const EDGE_RELATIONS = [
58
- // Organizational
59
- 'member_of', 'belongs_to', 'part_of',
60
- // Technical
61
- 'works_on', 'expert_in', 'touches', 'owns', 'contains', 'depends_on', 'relates_to',
62
- // Knowledge
63
- 'created_by', 'about', 'scoped_to', 'derived_from',
64
- ] as const;
65
- export type EdgeRelation = typeof EDGE_RELATIONS[number];
66
-
67
- export interface Entity {
68
- id: string; // format: {type}-{slug}
69
- type: EntityType;
70
- name: string;
71
- metadata: Record<string, unknown>;
72
- created: string; // ISO timestamp
73
- updated: string; // ISO timestamp
74
- }
75
-
76
- export interface Edge {
77
- source_id: string;
78
- target_id: string;
79
- relation: EdgeRelation;
80
- weight: number; // 0-1
81
- metadata: Record<string, unknown>;
82
- created: string; // ISO timestamp
83
- }
84
-
85
- export interface EntityAlias {
86
- entity_id: string;
87
- alias: string;
88
- }
89
-
90
- export interface AccessGrant {
91
- knowledge_id: string;
92
- grantee_entity_id: string;
93
- granted_by: string;
94
- created: string;
95
- }
96
-
97
- export function entityId(type: EntityType, slug: string): string {
98
- return `${type}-${slug}`;
99
- }
100
-
101
- export function slugify(name: string): string {
102
- return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
103
- }
104
-
105
- export function isValidEntityType(s: string): s is EntityType {
106
- return ENTITY_TYPES.includes(s as EntityType);
107
- }
108
-
109
- export function isValidEdgeRelation(s: string): s is EdgeRelation {
110
- return EDGE_RELATIONS.includes(s as EdgeRelation);
111
- }
112
- ```
113
-
114
- - [ ] **Step 2: Commit**
115
-
116
- ```bash
117
- git add src/lib/cortex/graph/types.ts
118
- git commit -m "feat(cortex): add entity graph type definitions"
119
- ```
120
-
121
- ---
122
-
123
- ### Task 2: Create SQLite schema
124
-
125
- **Files:**
126
- - Create: `src/lib/cortex/graph/schema.ts`
127
- - Test: `tests/lib/cortex/graph/entity-graph.test.ts`
128
-
129
- - [ ] **Step 1: Write the failing test for schema initialization**
130
-
131
- ```typescript
132
- // tests/lib/cortex/graph/entity-graph.test.ts
133
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
134
- import fs from 'fs';
135
- import path from 'path';
136
- import os from 'os';
137
- import Database from 'better-sqlite3';
138
- import { initGraphSchema } from '@/lib/cortex/graph/schema';
139
-
140
- describe('Graph Schema', () => {
141
- let tmpDir: string;
142
- let dbPath: string;
143
-
144
- beforeEach(() => {
145
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cortex-graph-'));
146
- dbPath = path.join(tmpDir, 'graph.db');
147
- });
148
-
149
- afterEach(() => {
150
- fs.rmSync(tmpDir, { recursive: true, force: true });
151
- });
152
-
153
- it('creates all tables and indexes', () => {
154
- const db = new Database(dbPath);
155
- initGraphSchema(db);
156
-
157
- // Verify tables exist
158
- const tables = db.prepare(
159
- "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
160
- ).all() as { name: string }[];
161
- const tableNames = tables.map(t => t.name);
162
-
163
- expect(tableNames).toContain('entities');
164
- expect(tableNames).toContain('edges');
165
- expect(tableNames).toContain('entity_aliases');
166
- expect(tableNames).toContain('access_grants');
167
- expect(tableNames).toContain('gravity_state');
168
-
169
- // Verify indexes exist
170
- const indexes = db.prepare(
171
- "SELECT name FROM sqlite_master WHERE type='index' AND name NOT LIKE 'sqlite_%'"
172
- ).all() as { name: string }[];
173
- const indexNames = indexes.map(i => i.name);
174
-
175
- expect(indexNames).toContain('idx_entities_type');
176
- expect(indexNames).toContain('idx_edges_target');
177
- expect(indexNames).toContain('idx_aliases_alias');
178
- expect(indexNames).toContain('idx_grants_grantee');
179
-
180
- db.close();
181
- });
182
-
183
- it('is idempotent — calling twice does not error', () => {
184
- const db = new Database(dbPath);
185
- initGraphSchema(db);
186
- initGraphSchema(db); // second call should not throw
187
- db.close();
188
- });
189
- });
190
- ```
191
-
192
- - [ ] **Step 2: Run test to verify it fails**
193
-
194
- Run: `npx vitest run tests/lib/cortex/graph/entity-graph.test.ts`
195
- Expected: FAIL — cannot find module `@/lib/cortex/graph/schema`
196
-
197
- - [ ] **Step 3: Implement the schema module**
198
-
199
- ```typescript
200
- // src/lib/cortex/graph/schema.ts
201
- import type Database from 'better-sqlite3';
202
-
203
- export function initGraphSchema(db: Database.Database): void {
204
- db.exec(`
205
- CREATE TABLE IF NOT EXISTS entities (
206
- id TEXT PRIMARY KEY,
207
- type TEXT NOT NULL,
208
- name TEXT NOT NULL,
209
- metadata TEXT DEFAULT '{}',
210
- created TEXT NOT NULL,
211
- updated TEXT NOT NULL
212
- );
213
-
214
- CREATE TABLE IF NOT EXISTS edges (
215
- source_id TEXT NOT NULL,
216
- target_id TEXT NOT NULL,
217
- relation TEXT NOT NULL,
218
- weight REAL DEFAULT 1.0,
219
- metadata TEXT DEFAULT '{}',
220
- created TEXT NOT NULL,
221
- PRIMARY KEY (source_id, target_id, relation),
222
- FOREIGN KEY (source_id) REFERENCES entities(id) ON DELETE CASCADE,
223
- FOREIGN KEY (target_id) REFERENCES entities(id) ON DELETE CASCADE
224
- );
225
-
226
- CREATE TABLE IF NOT EXISTS entity_aliases (
227
- entity_id TEXT NOT NULL,
228
- alias TEXT NOT NULL,
229
- PRIMARY KEY (entity_id, alias),
230
- FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE
231
- );
232
-
233
- CREATE TABLE IF NOT EXISTS access_grants (
234
- knowledge_id TEXT NOT NULL,
235
- grantee_entity_id TEXT NOT NULL,
236
- granted_by TEXT NOT NULL,
237
- created TEXT NOT NULL,
238
- PRIMARY KEY (knowledge_id, grantee_entity_id)
239
- );
240
-
241
- CREATE TABLE IF NOT EXISTS gravity_state (
242
- key TEXT PRIMARY KEY,
243
- value TEXT NOT NULL,
244
- updated TEXT NOT NULL
245
- );
246
-
247
- CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(type);
248
- CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_id, relation);
249
- CREATE INDEX IF NOT EXISTS idx_aliases_alias ON entity_aliases(alias);
250
- CREATE INDEX IF NOT EXISTS idx_grants_grantee ON access_grants(grantee_entity_id);
251
- `);
252
-
253
- // Enable WAL mode for better concurrent read performance
254
- db.pragma('journal_mode = WAL');
255
- db.pragma('foreign_keys = ON');
256
- }
257
- ```
258
-
259
- - [ ] **Step 4: Run test to verify it passes**
260
-
261
- Run: `npx vitest run tests/lib/cortex/graph/entity-graph.test.ts`
262
- Expected: PASS (2 tests)
263
-
264
- - [ ] **Step 5: Commit**
265
-
266
- ```bash
267
- git add src/lib/cortex/graph/schema.ts tests/lib/cortex/graph/entity-graph.test.ts
268
- git commit -m "feat(cortex): add SQLite schema for entity graph"
269
- ```
270
-
271
- ---
272
-
273
- ### Task 3: EntityGraph class — entity CRUD
274
-
275
- **Files:**
276
- - Create: `src/lib/cortex/graph/entity-graph.ts`
277
- - Modify: `tests/lib/cortex/graph/entity-graph.test.ts`
278
-
279
- - [ ] **Step 1: Write failing tests for entity CRUD**
280
-
281
- Append to `tests/lib/cortex/graph/entity-graph.test.ts`:
282
-
283
- ```typescript
284
- import { EntityGraph } from '@/lib/cortex/graph/entity-graph';
285
- import type { Entity } from '@/lib/cortex/graph/types';
286
-
287
- describe('EntityGraph — Entity CRUD', () => {
288
- let tmpDir: string;
289
- let graph: EntityGraph;
290
-
291
- beforeEach(() => {
292
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cortex-graph-'));
293
- graph = new EntityGraph(path.join(tmpDir, 'graph.db'));
294
- });
295
-
296
- afterEach(() => {
297
- graph.close();
298
- fs.rmSync(tmpDir, { recursive: true, force: true });
299
- });
300
-
301
- it('creates and retrieves an entity', () => {
302
- const entity = graph.createEntity({
303
- type: 'person',
304
- name: 'Alice Smith',
305
- metadata: { email: 'alice@acme.com', role: 'lead' },
306
- });
307
-
308
- expect(entity.id).toBe('person-alice-smith');
309
- expect(entity.type).toBe('person');
310
- expect(entity.name).toBe('Alice Smith');
311
- expect(entity.metadata).toEqual({ email: 'alice@acme.com', role: 'lead' });
312
-
313
- const fetched = graph.getEntity('person-alice-smith');
314
- expect(fetched).not.toBeNull();
315
- expect(fetched!.name).toBe('Alice Smith');
316
- });
317
-
318
- it('creates entity with explicit id', () => {
319
- const entity = graph.createEntity({
320
- id: 'person-custom-id',
321
- type: 'person',
322
- name: 'Bob',
323
- });
324
- expect(entity.id).toBe('person-custom-id');
325
- });
326
-
327
- it('updates an entity', () => {
328
- graph.createEntity({ type: 'team', name: 'Platform' });
329
- const updated = graph.updateEntity('team-platform', {
330
- name: 'Platform Engineering',
331
- metadata: { purpose: 'core infra' },
332
- });
333
- expect(updated!.name).toBe('Platform Engineering');
334
- expect(updated!.metadata).toEqual({ purpose: 'core infra' });
335
- });
336
-
337
- it('deletes an entity', () => {
338
- graph.createEntity({ type: 'topic', name: 'Auth' });
339
- expect(graph.getEntity('topic-auth')).not.toBeNull();
340
- graph.deleteEntity('topic-auth');
341
- expect(graph.getEntity('topic-auth')).toBeNull();
342
- });
343
-
344
- it('lists entities by type', () => {
345
- graph.createEntity({ type: 'person', name: 'Alice' });
346
- graph.createEntity({ type: 'person', name: 'Bob' });
347
- graph.createEntity({ type: 'team', name: 'Platform' });
348
-
349
- const people = graph.listEntities({ type: 'person' });
350
- expect(people).toHaveLength(2);
351
-
352
- const all = graph.listEntities();
353
- expect(all).toHaveLength(3);
354
- });
355
-
356
- it('returns null for non-existent entity', () => {
357
- expect(graph.getEntity('person-nobody')).toBeNull();
358
- });
359
-
360
- it('throws on duplicate entity id', () => {
361
- graph.createEntity({ type: 'person', name: 'Alice' });
362
- expect(() => graph.createEntity({ type: 'person', name: 'Alice' }))
363
- .toThrow();
364
- });
365
- });
366
- ```
367
-
368
- - [ ] **Step 2: Run test to verify it fails**
369
-
370
- Run: `npx vitest run tests/lib/cortex/graph/entity-graph.test.ts`
371
- Expected: FAIL — cannot find module `@/lib/cortex/graph/entity-graph`
372
-
373
- - [ ] **Step 3: Implement EntityGraph — entity CRUD**
374
-
375
- ```typescript
376
- // src/lib/cortex/graph/entity-graph.ts
377
- import Database from 'better-sqlite3';
378
- import { initGraphSchema } from './schema';
379
- import { entityId, slugify } from './types';
380
- import type { Entity, EntityType, Edge, EdgeRelation, EntityAlias } from './types';
381
-
382
- interface CreateEntityInput {
383
- id?: string;
384
- type: EntityType;
385
- name: string;
386
- metadata?: Record<string, unknown>;
387
- }
388
-
389
- interface UpdateEntityInput {
390
- name?: string;
391
- metadata?: Record<string, unknown>;
392
- }
393
-
394
- interface ListEntitiesFilter {
395
- type?: EntityType;
396
- limit?: number;
397
- }
398
-
399
- export class EntityGraph {
400
- private db: Database.Database;
401
-
402
- constructor(dbPath: string) {
403
- this.db = new Database(dbPath);
404
- initGraphSchema(this.db);
405
- }
406
-
407
- // --- Entity CRUD ---
408
-
409
- createEntity(input: CreateEntityInput): Entity {
410
- const id = input.id ?? entityId(input.type, slugify(input.name));
411
- const now = new Date().toISOString();
412
- const metadata = JSON.stringify(input.metadata ?? {});
413
-
414
- this.db.prepare(`
415
- INSERT INTO entities (id, type, name, metadata, created, updated)
416
- VALUES (?, ?, ?, ?, ?, ?)
417
- `).run(id, input.type, input.name, metadata, now, now);
418
-
419
- return { id, type: input.type, name: input.name, metadata: input.metadata ?? {}, created: now, updated: now };
420
- }
421
-
422
- getEntity(id: string): Entity | null {
423
- const row = this.db.prepare('SELECT * FROM entities WHERE id = ?').get(id) as any;
424
- if (!row) return null;
425
- return this.rowToEntity(row);
426
- }
427
-
428
- updateEntity(id: string, updates: UpdateEntityInput): Entity | null {
429
- const existing = this.getEntity(id);
430
- if (!existing) return null;
431
-
432
- const now = new Date().toISOString();
433
- const name = updates.name ?? existing.name;
434
- const metadata = updates.metadata !== undefined
435
- ? JSON.stringify(updates.metadata)
436
- : JSON.stringify(existing.metadata);
437
-
438
- this.db.prepare(`
439
- UPDATE entities SET name = ?, metadata = ?, updated = ? WHERE id = ?
440
- `).run(name, metadata, now, id);
441
-
442
- return { ...existing, name, metadata: updates.metadata ?? existing.metadata, updated: now };
443
- }
444
-
445
- deleteEntity(id: string): void {
446
- this.db.prepare('DELETE FROM entities WHERE id = ?').run(id);
447
- }
448
-
449
- listEntities(filter: ListEntitiesFilter = {}): Entity[] {
450
- let sql = 'SELECT * FROM entities';
451
- const params: any[] = [];
452
-
453
- if (filter.type) {
454
- sql += ' WHERE type = ?';
455
- params.push(filter.type);
456
- }
457
-
458
- sql += ' ORDER BY name';
459
-
460
- if (filter.limit) {
461
- sql += ' LIMIT ?';
462
- params.push(filter.limit);
463
- }
464
-
465
- const rows = this.db.prepare(sql).all(...params) as any[];
466
- return rows.map(r => this.rowToEntity(r));
467
- }
468
-
469
- close(): void {
470
- this.db.close();
471
- }
472
-
473
- private rowToEntity(row: any): Entity {
474
- return {
475
- id: row.id,
476
- type: row.type as EntityType,
477
- name: row.name,
478
- metadata: JSON.parse(row.metadata || '{}'),
479
- created: row.created,
480
- updated: row.updated,
481
- };
482
- }
483
- }
484
- ```
485
-
486
- - [ ] **Step 4: Run test to verify it passes**
487
-
488
- Run: `npx vitest run tests/lib/cortex/graph/entity-graph.test.ts`
489
- Expected: PASS (all 9 tests)
490
-
491
- - [ ] **Step 5: Commit**
492
-
493
- ```bash
494
- git add src/lib/cortex/graph/entity-graph.ts tests/lib/cortex/graph/entity-graph.test.ts
495
- git commit -m "feat(cortex): add EntityGraph class with entity CRUD"
496
- ```
497
-
498
- ---
499
-
500
- ## Chunk 2: Edge CRUD and Graph Traversal
501
-
502
- ### Task 4: Edge CRUD methods
503
-
504
- **Files:**
505
- - Modify: `src/lib/cortex/graph/entity-graph.ts`
506
- - Modify: `tests/lib/cortex/graph/entity-graph.test.ts`
507
-
508
- - [ ] **Step 1: Write failing tests for edge CRUD**
509
-
510
- Append to `tests/lib/cortex/graph/entity-graph.test.ts`:
511
-
512
- ```typescript
513
- describe('EntityGraph — Edge CRUD', () => {
514
- let tmpDir: string;
515
- let graph: EntityGraph;
516
-
517
- beforeEach(() => {
518
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cortex-graph-'));
519
- graph = new EntityGraph(path.join(tmpDir, 'graph.db'));
520
- // Seed entities
521
- graph.createEntity({ type: 'person', name: 'Alice' });
522
- graph.createEntity({ type: 'person', name: 'Bob' });
523
- graph.createEntity({ type: 'team', name: 'Platform' });
524
- graph.createEntity({ type: 'system', name: 'Auth Service' });
525
- graph.createEntity({ type: 'topic', name: 'Authentication' });
526
- });
527
-
528
- afterEach(() => {
529
- graph.close();
530
- fs.rmSync(tmpDir, { recursive: true, force: true });
531
- });
532
-
533
- it('creates an edge between entities', () => {
534
- const edge = graph.createEdge({
535
- source_id: 'person-alice',
536
- target_id: 'team-platform',
537
- relation: 'member_of',
538
- weight: 1.0,
539
- metadata: { role: 'lead' },
540
- });
541
-
542
- expect(edge.source_id).toBe('person-alice');
543
- expect(edge.target_id).toBe('team-platform');
544
- expect(edge.relation).toBe('member_of');
545
- expect(edge.weight).toBe(1.0);
546
- });
547
-
548
- it('upserts edge — updates weight on duplicate', () => {
549
- graph.createEdge({
550
- source_id: 'person-alice',
551
- target_id: 'topic-authentication',
552
- relation: 'expert_in',
553
- weight: 0.3,
554
- });
555
- graph.createEdge({
556
- source_id: 'person-alice',
557
- target_id: 'topic-authentication',
558
- relation: 'expert_in',
559
- weight: 0.8,
560
- });
561
-
562
- const edges = graph.getEdgesFrom('person-alice', 'expert_in');
563
- expect(edges).toHaveLength(1);
564
- expect(edges[0].weight).toBe(0.8);
565
- });
566
-
567
- it('lists edges from an entity', () => {
568
- graph.createEdge({ source_id: 'person-alice', target_id: 'team-platform', relation: 'member_of' });
569
- graph.createEdge({ source_id: 'person-alice', target_id: 'topic-authentication', relation: 'expert_in' });
570
- graph.createEdge({ source_id: 'person-bob', target_id: 'team-platform', relation: 'member_of' });
571
-
572
- const aliceEdges = graph.getEdgesFrom('person-alice');
573
- expect(aliceEdges).toHaveLength(2);
574
-
575
- const platformMembers = graph.getEdgesTo('team-platform', 'member_of');
576
- expect(platformMembers).toHaveLength(2);
577
- });
578
-
579
- it('deletes an edge', () => {
580
- graph.createEdge({ source_id: 'person-alice', target_id: 'team-platform', relation: 'member_of' });
581
- expect(graph.getEdgesFrom('person-alice')).toHaveLength(1);
582
-
583
- graph.deleteEdge('person-alice', 'team-platform', 'member_of');
584
- expect(graph.getEdgesFrom('person-alice')).toHaveLength(0);
585
- });
586
-
587
- it('cascades entity delete to edges', () => {
588
- graph.createEdge({ source_id: 'person-alice', target_id: 'team-platform', relation: 'member_of' });
589
- graph.deleteEntity('person-alice');
590
- expect(graph.getEdgesTo('team-platform', 'member_of')).toHaveLength(0);
591
- });
592
-
593
- it('increments edge weight', () => {
594
- graph.createEdge({ source_id: 'person-alice', target_id: 'topic-authentication', relation: 'expert_in', weight: 0.5 });
595
- graph.incrementEdgeWeight('person-alice', 'topic-authentication', 'expert_in', 0.1);
596
- const edges = graph.getEdgesFrom('person-alice', 'expert_in');
597
- expect(edges[0].weight).toBeCloseTo(0.6);
598
- });
599
-
600
- it('caps edge weight at 1.0', () => {
601
- graph.createEdge({ source_id: 'person-alice', target_id: 'topic-authentication', relation: 'expert_in', weight: 0.95 });
602
- graph.incrementEdgeWeight('person-alice', 'topic-authentication', 'expert_in', 0.2);
603
- const edges = graph.getEdgesFrom('person-alice', 'expert_in');
604
- expect(edges[0].weight).toBe(1.0);
605
- });
606
- });
607
- ```
608
-
609
- - [ ] **Step 2: Run test to verify it fails**
610
-
611
- Run: `npx vitest run tests/lib/cortex/graph/entity-graph.test.ts`
612
- Expected: FAIL — `graph.createEdge is not a function`
613
-
614
- - [ ] **Step 3: Add edge CRUD methods to EntityGraph**
615
-
616
- Add these methods to the `EntityGraph` class in `src/lib/cortex/graph/entity-graph.ts`:
617
-
618
- ```typescript
619
- interface CreateEdgeInput {
620
- source_id: string;
621
- target_id: string;
622
- relation: EdgeRelation;
623
- weight?: number;
624
- metadata?: Record<string, unknown>;
625
- }
626
-
627
- // Inside class EntityGraph:
628
-
629
- createEdge(input: CreateEdgeInput): Edge {
630
- const now = new Date().toISOString();
631
- const weight = input.weight ?? 1.0;
632
- const metadata = JSON.stringify(input.metadata ?? {});
633
-
634
- this.db.prepare(`
635
- INSERT INTO edges (source_id, target_id, relation, weight, metadata, created)
636
- VALUES (?, ?, ?, ?, ?, ?)
637
- ON CONFLICT(source_id, target_id, relation)
638
- DO UPDATE SET weight = excluded.weight, metadata = excluded.metadata
639
- `).run(input.source_id, input.target_id, input.relation, weight, metadata, now);
640
-
641
- return {
642
- source_id: input.source_id,
643
- target_id: input.target_id,
644
- relation: input.relation as EdgeRelation,
645
- weight,
646
- metadata: input.metadata ?? {},
647
- created: now,
648
- };
649
- }
650
-
651
- getEdgesFrom(entityId: string, relation?: EdgeRelation): Edge[] {
652
- let sql = 'SELECT * FROM edges WHERE source_id = ?';
653
- const params: any[] = [entityId];
654
- if (relation) {
655
- sql += ' AND relation = ?';
656
- params.push(relation);
657
- }
658
- return (this.db.prepare(sql).all(...params) as any[]).map(r => this.rowToEdge(r));
659
- }
660
-
661
- getEdgesTo(entityId: string, relation?: EdgeRelation): Edge[] {
662
- let sql = 'SELECT * FROM edges WHERE target_id = ?';
663
- const params: any[] = [entityId];
664
- if (relation) {
665
- sql += ' AND relation = ?';
666
- params.push(relation);
667
- }
668
- return (this.db.prepare(sql).all(...params) as any[]).map(r => this.rowToEdge(r));
669
- }
670
-
671
- deleteEdge(sourceId: string, targetId: string, relation: EdgeRelation): void {
672
- this.db.prepare(
673
- 'DELETE FROM edges WHERE source_id = ? AND target_id = ? AND relation = ?'
674
- ).run(sourceId, targetId, relation);
675
- }
676
-
677
- incrementEdgeWeight(sourceId: string, targetId: string, relation: EdgeRelation, delta: number): void {
678
- this.db.prepare(`
679
- UPDATE edges SET weight = MIN(1.0, weight + ?) WHERE source_id = ? AND target_id = ? AND relation = ?
680
- `).run(delta, sourceId, targetId, relation);
681
- }
682
-
683
- private rowToEdge(row: any): Edge {
684
- return {
685
- source_id: row.source_id,
686
- target_id: row.target_id,
687
- relation: row.relation as EdgeRelation,
688
- weight: row.weight,
689
- metadata: JSON.parse(row.metadata || '{}'),
690
- created: row.created,
691
- };
692
- }
693
- ```
694
-
695
- Also add the `CreateEdgeInput` interface import and export the interface types at the top of the file.
696
-
697
- - [ ] **Step 4: Run test to verify it passes**
698
-
699
- Run: `npx vitest run tests/lib/cortex/graph/entity-graph.test.ts`
700
- Expected: PASS (all 16 tests)
701
-
702
- - [ ] **Step 5: Commit**
703
-
704
- ```bash
705
- git add src/lib/cortex/graph/entity-graph.ts tests/lib/cortex/graph/entity-graph.test.ts
706
- git commit -m "feat(cortex): add edge CRUD with upsert and weight increment"
707
- ```
708
-
709
- ---
710
-
711
- ### Task 5: Alias management
712
-
713
- **Files:**
714
- - Modify: `src/lib/cortex/graph/entity-graph.ts`
715
- - Modify: `tests/lib/cortex/graph/entity-graph.test.ts`
716
-
717
- - [ ] **Step 1: Write failing tests for aliases**
718
-
719
- Append to `tests/lib/cortex/graph/entity-graph.test.ts`:
720
-
721
- ```typescript
722
- describe('EntityGraph — Aliases', () => {
723
- let tmpDir: string;
724
- let graph: EntityGraph;
725
-
726
- beforeEach(() => {
727
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cortex-graph-'));
728
- graph = new EntityGraph(path.join(tmpDir, 'graph.db'));
729
- graph.createEntity({ type: 'system', name: 'Auth Service' });
730
- graph.createEntity({ type: 'topic', name: 'Authentication' });
731
- });
732
-
733
- afterEach(() => {
734
- graph.close();
735
- fs.rmSync(tmpDir, { recursive: true, force: true });
736
- });
737
-
738
- it('adds and retrieves aliases', () => {
739
- graph.addAlias('system-auth-service', 'auth');
740
- graph.addAlias('system-auth-service', 'auth-svc');
741
- graph.addAlias('system-auth-service', 'authentication service');
742
-
743
- const aliases = graph.getAliases('system-auth-service');
744
- expect(aliases).toHaveLength(3);
745
- expect(aliases).toContain('auth');
746
- });
747
-
748
- it('looks up entity by alias', () => {
749
- graph.addAlias('system-auth-service', 'auth');
750
- const entity = graph.findByAlias('auth');
751
- expect(entity).not.toBeNull();
752
- expect(entity!.id).toBe('system-auth-service');
753
- });
754
-
755
- it('returns null for unknown alias', () => {
756
- expect(graph.findByAlias('nonexistent')).toBeNull();
757
- });
758
-
759
- it('auto-creates aliases from entity name on create', () => {
760
- graph.createEntity({ type: 'system', name: 'API Gateway' });
761
- // Should auto-create aliases: "api gateway", "api-gateway"
762
- expect(graph.findByAlias('api gateway')).not.toBeNull();
763
- expect(graph.findByAlias('api-gateway')).not.toBeNull();
764
- });
765
-
766
- it('removes aliases when entity is deleted', () => {
767
- graph.addAlias('system-auth-service', 'auth');
768
- graph.deleteEntity('system-auth-service');
769
- expect(graph.findByAlias('auth')).toBeNull();
770
- });
771
- });
772
- ```
773
-
774
- - [ ] **Step 2: Run test to verify it fails**
775
-
776
- Run: `npx vitest run tests/lib/cortex/graph/entity-graph.test.ts`
777
- Expected: FAIL — `graph.addAlias is not a function`
778
-
779
- - [ ] **Step 3: Add alias methods to EntityGraph**
780
-
781
- Add to `src/lib/cortex/graph/entity-graph.ts`:
782
-
783
- ```typescript
784
- // Inside class EntityGraph:
785
-
786
- addAlias(entityId: string, alias: string): void {
787
- const normalized = alias.toLowerCase().trim();
788
- this.db.prepare(
789
- 'INSERT OR IGNORE INTO entity_aliases (entity_id, alias) VALUES (?, ?)'
790
- ).run(entityId, normalized);
791
- }
792
-
793
- getAliases(entityId: string): string[] {
794
- const rows = this.db.prepare(
795
- 'SELECT alias FROM entity_aliases WHERE entity_id = ?'
796
- ).all(entityId) as { alias: string }[];
797
- return rows.map(r => r.alias);
798
- }
799
-
800
- findByAlias(alias: string): Entity | null {
801
- const normalized = alias.toLowerCase().trim();
802
- const row = this.db.prepare(
803
- 'SELECT entity_id FROM entity_aliases WHERE alias = ? LIMIT 1'
804
- ).get(normalized) as { entity_id: string } | undefined;
805
- if (!row) return null;
806
- return this.getEntity(row.entity_id);
807
- }
808
- ```
809
-
810
- Also update `createEntity` to auto-add aliases:
811
-
812
- ```typescript
813
- // After the INSERT in createEntity, add:
814
- const nameLower = input.name.toLowerCase();
815
- const nameSlug = slugify(input.name);
816
- this.addAlias(id, nameLower);
817
- if (nameSlug !== nameLower) {
818
- this.addAlias(id, nameSlug);
819
- }
820
- ```
821
-
822
- - [ ] **Step 4: Run test to verify it passes**
823
-
824
- Run: `npx vitest run tests/lib/cortex/graph/entity-graph.test.ts`
825
- Expected: PASS (all 21 tests)
826
-
827
- - [ ] **Step 5: Commit**
828
-
829
- ```bash
830
- git add src/lib/cortex/graph/entity-graph.ts tests/lib/cortex/graph/entity-graph.test.ts
831
- git commit -m "feat(cortex): add alias management with auto-alias on entity creation"
832
- ```
833
-
834
- ---
835
-
836
- ### Task 6: Graph traversal — BFS distance and N-hop neighborhood
837
-
838
- **Files:**
839
- - Modify: `src/lib/cortex/graph/entity-graph.ts`
840
- - Create: `tests/lib/cortex/graph/traversal.test.ts`
841
-
842
- - [ ] **Step 1: Write failing tests for traversal**
843
-
844
- ```typescript
845
- // tests/lib/cortex/graph/traversal.test.ts
846
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
847
- import fs from 'fs';
848
- import path from 'path';
849
- import os from 'os';
850
- import { EntityGraph } from '@/lib/cortex/graph/entity-graph';
851
-
852
- describe('EntityGraph — Traversal', () => {
853
- let tmpDir: string;
854
- let graph: EntityGraph;
855
-
856
- beforeEach(() => {
857
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cortex-graph-'));
858
- graph = new EntityGraph(path.join(tmpDir, 'graph.db'));
859
-
860
- // Build a test graph:
861
- // Alice --member_of--> Platform --part_of--> Engineering --part_of--> Acme
862
- // Bob --member_of--> Platform
863
- // Alice --expert_in--> Auth (topic)
864
- // Platform --owns--> Auth Service (system)
865
- // Security --owns--> Auth Service
866
- graph.createEntity({ type: 'organization', name: 'Acme' });
867
- graph.createEntity({ type: 'department', name: 'Engineering' });
868
- graph.createEntity({ type: 'department', name: 'Security Dept' });
869
- graph.createEntity({ type: 'team', name: 'Platform' });
870
- graph.createEntity({ type: 'team', name: 'Security' });
871
- graph.createEntity({ type: 'person', name: 'Alice' });
872
- graph.createEntity({ type: 'person', name: 'Bob' });
873
- graph.createEntity({ type: 'topic', name: 'Auth' });
874
- graph.createEntity({ type: 'system', name: 'Auth Service' });
875
-
876
- graph.createEdge({ source_id: 'person-alice', target_id: 'team-platform', relation: 'member_of' });
877
- graph.createEdge({ source_id: 'person-bob', target_id: 'team-platform', relation: 'member_of' });
878
- graph.createEdge({ source_id: 'team-platform', target_id: 'department-engineering', relation: 'part_of' });
879
- graph.createEdge({ source_id: 'team-security', target_id: 'department-security-dept', relation: 'part_of' });
880
- graph.createEdge({ source_id: 'department-engineering', target_id: 'organization-acme', relation: 'part_of' });
881
- graph.createEdge({ source_id: 'department-security-dept', target_id: 'organization-acme', relation: 'part_of' });
882
- graph.createEdge({ source_id: 'person-alice', target_id: 'topic-auth', relation: 'expert_in' });
883
- graph.createEdge({ source_id: 'team-platform', target_id: 'system-auth-service', relation: 'owns' });
884
- graph.createEdge({ source_id: 'team-security', target_id: 'system-auth-service', relation: 'owns' });
885
- });
886
-
887
- afterEach(() => {
888
- graph.close();
889
- fs.rmSync(tmpDir, { recursive: true, force: true });
890
- });
891
-
892
- it('computes distance 0 to self', () => {
893
- expect(graph.distance('person-alice', 'person-alice')).toBe(0);
894
- });
895
-
896
- it('computes distance 1 for direct neighbors', () => {
897
- expect(graph.distance('person-alice', 'team-platform')).toBe(1);
898
- expect(graph.distance('person-alice', 'topic-auth')).toBe(1);
899
- });
900
-
901
- it('computes distance 2 for two-hop paths', () => {
902
- // Alice -> Platform -> Engineering
903
- expect(graph.distance('person-alice', 'department-engineering')).toBe(2);
904
- // Alice -> Platform -> Bob (via Platform)
905
- expect(graph.distance('person-alice', 'person-bob')).toBe(2);
906
- });
907
-
908
- it('computes distance 3 for three-hop paths', () => {
909
- // Alice -> Platform -> Engineering -> Acme
910
- expect(graph.distance('person-alice', 'organization-acme')).toBe(3);
911
- });
912
-
913
- it('traverses edges bidirectionally', () => {
914
- // Bob -> Platform (outgoing), Platform -> Alice (incoming to Platform)
915
- expect(graph.distance('person-bob', 'person-alice')).toBe(2);
916
- });
917
-
918
- it('returns Infinity for unreachable entities', () => {
919
- graph.createEntity({ type: 'topic', name: 'Isolated' });
920
- expect(graph.distance('person-alice', 'topic-isolated')).toBe(Infinity);
921
- });
922
-
923
- it('respects maxHops limit', () => {
924
- // Alice -> Platform -> Engineering -> Acme is 3 hops
925
- expect(graph.distance('person-alice', 'organization-acme', 2)).toBe(Infinity);
926
- expect(graph.distance('person-alice', 'organization-acme', 3)).toBe(3);
927
- });
928
-
929
- it('returns entities within N hops', () => {
930
- const nearby = graph.neighborhood('person-alice', 1);
931
- const ids = nearby.map(e => e.id);
932
- expect(ids).toContain('team-platform');
933
- expect(ids).toContain('topic-auth');
934
- expect(ids).not.toContain('department-engineering');
935
- expect(ids).not.toContain('person-alice'); // self excluded
936
- });
937
-
938
- it('returns entities within 2 hops', () => {
939
- const nearby = graph.neighborhood('person-alice', 2);
940
- const ids = nearby.map(e => e.id);
941
- expect(ids).toContain('team-platform');
942
- expect(ids).toContain('department-engineering');
943
- expect(ids).toContain('person-bob');
944
- expect(ids).toContain('system-auth-service');
945
- });
946
-
947
- it('computes graph proximity score', () => {
948
- // Create an isolated entity within this test's scope
949
- graph.createEntity({ type: 'topic', name: 'Orphaned' });
950
-
951
- // proximity = 1 / (1 + distance)
952
- expect(graph.proximity('person-alice', 'person-alice')).toBe(1.0); // distance 0
953
- expect(graph.proximity('person-alice', 'team-platform')).toBe(0.5); // distance 1
954
- expect(graph.proximity('person-alice', 'department-engineering')).toBeCloseTo(0.333); // distance 2
955
- expect(graph.proximity('person-alice', 'topic-orphaned')).toBe(0); // unreachable (no edges)
956
- });
957
- });
958
- ```
959
-
960
- - [ ] **Step 2: Run test to verify it fails**
961
-
962
- Run: `npx vitest run tests/lib/cortex/graph/traversal.test.ts`
963
- Expected: FAIL — `graph.distance is not a function`
964
-
965
- - [ ] **Step 3: Implement traversal methods**
966
-
967
- Add to `src/lib/cortex/graph/entity-graph.ts`:
968
-
969
- ```typescript
970
- // Inside class EntityGraph:
971
-
972
- /**
973
- * BFS shortest-path distance between two entities.
974
- * Edges are traversed bidirectionally (undirected graph for distance).
975
- * Returns Infinity if no path exists within maxHops.
976
- */
977
- distance(fromId: string, toId: string, maxHops: number = 4): number {
978
- if (fromId === toId) return 0;
979
-
980
- const visited = new Set<string>([fromId]);
981
- let frontier = [fromId];
982
- let depth = 0;
983
-
984
- while (frontier.length > 0 && depth < maxHops) {
985
- depth++;
986
- const nextFrontier: string[] = [];
987
-
988
- for (const nodeId of frontier) {
989
- const neighbors = this.getNeighborIds(nodeId);
990
- for (const neighbor of neighbors) {
991
- if (neighbor === toId) return depth;
992
- if (!visited.has(neighbor)) {
993
- visited.add(neighbor);
994
- nextFrontier.push(neighbor);
995
- }
996
- }
997
- }
998
-
999
- frontier = nextFrontier;
1000
- }
1001
-
1002
- return Infinity;
1003
- }
1004
-
1005
- /**
1006
- * All entities within N hops (excluding self).
1007
- */
1008
- neighborhood(entityId: string, maxHops: number): Entity[] {
1009
- const visited = new Set<string>([entityId]);
1010
- let frontier = [entityId];
1011
-
1012
- for (let depth = 0; depth < maxHops; depth++) {
1013
- const nextFrontier: string[] = [];
1014
- for (const nodeId of frontier) {
1015
- for (const neighbor of this.getNeighborIds(nodeId)) {
1016
- if (!visited.has(neighbor)) {
1017
- visited.add(neighbor);
1018
- nextFrontier.push(neighbor);
1019
- }
1020
- }
1021
- }
1022
- frontier = nextFrontier;
1023
- }
1024
-
1025
- visited.delete(entityId); // exclude self
1026
- return [...visited]
1027
- .map(id => this.getEntity(id))
1028
- .filter((e): e is Entity => e !== null);
1029
- }
1030
-
1031
- /**
1032
- * Graph proximity: 1 / (1 + distance). Returns 0 for unreachable.
1033
- */
1034
- proximity(fromId: string, toId: string, maxHops: number = 4): number {
1035
- const d = this.distance(fromId, toId, maxHops);
1036
- if (d === Infinity) return 0;
1037
- return 1 / (1 + d);
1038
- }
1039
-
1040
- /**
1041
- * Get all neighbor IDs (both directions — edges are treated as undirected for traversal).
1042
- * Single UNION query for efficiency during BFS.
1043
- */
1044
- private getNeighborIds(entityId: string): string[] {
1045
- const rows = this.db.prepare(`
1046
- SELECT target_id AS id FROM edges WHERE source_id = ?
1047
- UNION
1048
- SELECT source_id AS id FROM edges WHERE target_id = ?
1049
- `).all(entityId, entityId) as { id: string }[];
1050
-
1051
- return rows.map(r => r.id);
1052
- }
1053
- ```
1054
-
1055
- - [ ] **Step 4: Run test to verify it passes**
1056
-
1057
- Run: `npx vitest run tests/lib/cortex/graph/traversal.test.ts`
1058
- Expected: PASS (all 10 tests)
1059
-
1060
- - [ ] **Step 5: Commit**
1061
-
1062
- ```bash
1063
- git add src/lib/cortex/graph/entity-graph.ts tests/lib/cortex/graph/traversal.test.ts
1064
- git commit -m "feat(cortex): add BFS distance, neighborhood, and proximity to entity graph"
1065
- ```
1066
-
1067
- ---
1068
-
1069
- ## Chunk 3: Entity Resolution and Auto-Population
1070
-
1071
- ### Task 7: Entity resolver — alias + fuzzy lookup
1072
-
1073
- **Files:**
1074
- - Create: `src/lib/cortex/graph/resolver.ts`
1075
- - Create: `tests/lib/cortex/graph/resolver.test.ts`
1076
-
1077
- - [ ] **Step 1: Write failing tests for resolver**
1078
-
1079
- ```typescript
1080
- // tests/lib/cortex/graph/resolver.test.ts
1081
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
1082
- import fs from 'fs';
1083
- import path from 'path';
1084
- import os from 'os';
1085
- import { EntityGraph } from '@/lib/cortex/graph/entity-graph';
1086
- import { EntityResolver } from '@/lib/cortex/graph/resolver';
1087
-
1088
- describe('EntityResolver', () => {
1089
- let tmpDir: string;
1090
- let graph: EntityGraph;
1091
- let resolver: EntityResolver;
1092
-
1093
- beforeEach(() => {
1094
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cortex-graph-'));
1095
- graph = new EntityGraph(path.join(tmpDir, 'graph.db'));
1096
- resolver = new EntityResolver(graph);
1097
-
1098
- graph.createEntity({ type: 'system', name: 'Auth Service' });
1099
- graph.createEntity({ type: 'system', name: 'API Gateway' });
1100
- graph.createEntity({ type: 'topic', name: 'Authentication' });
1101
- graph.createEntity({ type: 'topic', name: 'Performance' });
1102
- graph.createEntity({ type: 'person', name: 'Alice Smith' });
1103
- graph.addAlias('system-auth-service', 'auth');
1104
- graph.addAlias('system-auth-service', 'auth-svc');
1105
- });
1106
-
1107
- afterEach(() => {
1108
- graph.close();
1109
- fs.rmSync(tmpDir, { recursive: true, force: true });
1110
- });
1111
-
1112
- it('resolves exact alias match', () => {
1113
- const result = resolver.resolve('auth');
1114
- expect(result).not.toBeNull();
1115
- expect(result!.entity.id).toBe('system-auth-service');
1116
- expect(result!.confidence).toBeGreaterThanOrEqual(0.95);
1117
- expect(result!.method).toBe('alias');
1118
- });
1119
-
1120
- it('resolves fuzzy alias match', () => {
1121
- const result = resolver.resolve('auth servce'); // typo
1122
- expect(result).not.toBeNull();
1123
- expect(result!.entity.id).toBe('system-auth-service');
1124
- expect(result!.method).toBe('fuzzy');
1125
- expect(result!.confidence).toBeLessThan(0.95); // lower than exact
1126
- });
1127
-
1128
- it('returns null for unresolvable text', () => {
1129
- expect(resolver.resolve('completely unknown xyz')).toBeNull();
1130
- });
1131
-
1132
- it('extracts multiple entities from text', () => {
1133
- const entities = resolver.extractEntities('fix the auth service performance issue');
1134
- const ids = entities.map(e => e.entity.id);
1135
- expect(ids).toContain('system-auth-service');
1136
- expect(ids).toContain('topic-performance');
1137
- });
1138
-
1139
- it('prefers exact alias over fuzzy match', () => {
1140
- const result = resolver.resolve('auth');
1141
- expect(result!.method).toBe('alias');
1142
- });
1143
- });
1144
- ```
1145
-
1146
- - [ ] **Step 2: Run test to verify it fails**
1147
-
1148
- Run: `npx vitest run tests/lib/cortex/graph/resolver.test.ts`
1149
- Expected: FAIL — cannot find module `@/lib/cortex/graph/resolver`
1150
-
1151
- - [ ] **Step 3: Implement the resolver**
1152
-
1153
- ```typescript
1154
- // src/lib/cortex/graph/resolver.ts
1155
- import type { EntityGraph } from './entity-graph';
1156
- import type { Entity } from './types';
1157
-
1158
- export interface ResolvedEntity {
1159
- entity: Entity;
1160
- confidence: number;
1161
- method: 'alias' | 'fuzzy' | 'name';
1162
- }
1163
-
1164
- export class EntityResolver {
1165
- constructor(private graph: EntityGraph) {}
1166
-
1167
- /**
1168
- * Resolve a text fragment to an entity.
1169
- * Tries: 1) exact alias 2) entity name 3) fuzzy match (Levenshtein ≤ 2)
1170
- */
1171
- resolve(text: string): ResolvedEntity | null {
1172
- const normalized = text.toLowerCase().trim();
1173
-
1174
- // 1. Exact alias match
1175
- const byAlias = this.graph.findByAlias(normalized);
1176
- if (byAlias) {
1177
- return { entity: byAlias, confidence: 0.95, method: 'alias' };
1178
- }
1179
-
1180
- // 2. Exact name match (case-insensitive via alias auto-creation)
1181
- // Already covered by alias lookup since createEntity auto-adds name as alias
1182
-
1183
- // 3. Fuzzy match — scan all aliases for Levenshtein ≤ 2
1184
- const allEntities = this.graph.listEntities();
1185
- let bestMatch: ResolvedEntity | null = null;
1186
- let bestDistance = 3; // max acceptable
1187
-
1188
- for (const entity of allEntities) {
1189
- const aliases = this.graph.getAliases(entity.id);
1190
- const candidates = [entity.name.toLowerCase(), ...aliases];
1191
-
1192
- for (const candidate of candidates) {
1193
- const dist = levenshtein(normalized, candidate);
1194
- if (dist < bestDistance) {
1195
- bestDistance = dist;
1196
- bestMatch = {
1197
- entity,
1198
- confidence: Math.max(0.5, 0.9 - dist * 0.15),
1199
- method: 'fuzzy',
1200
- };
1201
- }
1202
- }
1203
- }
1204
-
1205
- return bestMatch;
1206
- }
1207
-
1208
- /**
1209
- * Extract all entity references from a text string.
1210
- * Scans for known entity names and aliases within the text.
1211
- */
1212
- extractEntities(text: string): ResolvedEntity[] {
1213
- const normalized = text.toLowerCase();
1214
- const results: ResolvedEntity[] = [];
1215
- const seen = new Set<string>();
1216
-
1217
- const allEntities = this.graph.listEntities();
1218
-
1219
- for (const entity of allEntities) {
1220
- if (seen.has(entity.id)) continue;
1221
-
1222
- const aliases = [entity.name.toLowerCase(), ...this.graph.getAliases(entity.id)];
1223
-
1224
- for (const alias of aliases) {
1225
- if (alias.length < 3) continue; // skip very short aliases to avoid false matches
1226
- if (normalized.includes(alias)) {
1227
- results.push({ entity, confidence: 0.85, method: 'alias' });
1228
- seen.add(entity.id);
1229
- break;
1230
- }
1231
- }
1232
- }
1233
-
1234
- return results;
1235
- }
1236
- }
1237
-
1238
- /**
1239
- * Levenshtein distance between two strings.
1240
- */
1241
- function levenshtein(a: string, b: string): number {
1242
- if (a.length === 0) return b.length;
1243
- if (b.length === 0) return a.length;
1244
-
1245
- const matrix: number[][] = [];
1246
-
1247
- for (let i = 0; i <= a.length; i++) {
1248
- matrix[i] = [i];
1249
- }
1250
- for (let j = 0; j <= b.length; j++) {
1251
- matrix[0][j] = j;
1252
- }
1253
-
1254
- for (let i = 1; i <= a.length; i++) {
1255
- for (let j = 1; j <= b.length; j++) {
1256
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
1257
- matrix[i][j] = Math.min(
1258
- matrix[i - 1][j] + 1, // deletion
1259
- matrix[i][j - 1] + 1, // insertion
1260
- matrix[i - 1][j - 1] + cost // substitution
1261
- );
1262
- }
1263
- }
1264
-
1265
- return matrix[a.length][b.length];
1266
- }
1267
- ```
1268
-
1269
- - [ ] **Step 4: Run test to verify it passes**
1270
-
1271
- Run: `npx vitest run tests/lib/cortex/graph/resolver.test.ts`
1272
- Expected: PASS (all 5 tests)
1273
-
1274
- - [ ] **Step 5: Commit**
1275
-
1276
- ```bash
1277
- git add src/lib/cortex/graph/resolver.ts tests/lib/cortex/graph/resolver.test.ts
1278
- git commit -m "feat(cortex): add entity resolver with alias and fuzzy matching"
1279
- ```
1280
-
1281
- ---
1282
-
1283
- ### Task 8: Auto-population from Spaces data
1284
-
1285
- > **Scope note:** This task seeds the graph from declarative configuration (org, users, teams, projects). Git-based seeding (WORKS_ON, TOUCHES, EXPERT_IN edges from commit history and blame; Systems/Modules from directory structure; Topics from file paths) is deferred to **Pillar 5: Observable Signal Ingestion** where the Git History adapter will populate these automatically.
1286
-
1287
- **Files:**
1288
- - Create: `src/lib/cortex/graph/auto-populate.ts`
1289
- - Create: `tests/lib/cortex/graph/auto-populate.test.ts`
1290
-
1291
- - [ ] **Step 1: Write failing tests for auto-population**
1292
-
1293
- ```typescript
1294
- // tests/lib/cortex/graph/auto-populate.test.ts
1295
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
1296
- import fs from 'fs';
1297
- import path from 'path';
1298
- import os from 'os';
1299
- import { EntityGraph } from '@/lib/cortex/graph/entity-graph';
1300
- import { autoPopulate } from '@/lib/cortex/graph/auto-populate';
1301
-
1302
- // Mock the user/workspace data sources
1303
- vi.mock('@/lib/auth', () => ({
1304
- getCurrentUser: () => 'test-user',
1305
- getAuthUser: () => 'test-user',
1306
- withUser: (_user: string, fn: () => any) => fn(),
1307
- }));
1308
-
1309
- describe('autoPopulate', () => {
1310
- let tmpDir: string;
1311
- let graph: EntityGraph;
1312
-
1313
- beforeEach(() => {
1314
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cortex-graph-'));
1315
- graph = new EntityGraph(path.join(tmpDir, 'graph.db'));
1316
- });
1317
-
1318
- afterEach(() => {
1319
- graph.close();
1320
- fs.rmSync(tmpDir, { recursive: true, force: true });
1321
- });
1322
-
1323
- it('creates default organization entity', () => {
1324
- autoPopulate(graph, { orgName: 'Acme Corp' });
1325
-
1326
- const org = graph.getEntity('organization-acme-corp');
1327
- expect(org).not.toBeNull();
1328
- expect(org!.name).toBe('Acme Corp');
1329
- expect(org!.type).toBe('organization');
1330
- });
1331
-
1332
- it('creates person entities from user list', () => {
1333
- autoPopulate(graph, {
1334
- orgName: 'Acme',
1335
- users: [
1336
- { name: 'Alice Smith', email: 'alice@acme.com', role: 'lead' },
1337
- { name: 'Bob Jones', email: 'bob@acme.com', role: 'member' },
1338
- ],
1339
- });
1340
-
1341
- const alice = graph.getEntity('person-alice-smith');
1342
- expect(alice).not.toBeNull();
1343
- expect(alice!.metadata).toEqual({ email: 'alice@acme.com', role: 'lead' });
1344
-
1345
- const bob = graph.getEntity('person-bob-jones');
1346
- expect(bob).not.toBeNull();
1347
- });
1348
-
1349
- it('creates team entities and membership edges', () => {
1350
- autoPopulate(graph, {
1351
- orgName: 'Acme',
1352
- teams: [
1353
- { name: 'Platform', department: 'Engineering', members: ['Alice Smith'] },
1354
- ],
1355
- users: [{ name: 'Alice Smith', email: 'alice@acme.com' }],
1356
- });
1357
-
1358
- const team = graph.getEntity('team-platform');
1359
- expect(team).not.toBeNull();
1360
-
1361
- const dept = graph.getEntity('department-engineering');
1362
- expect(dept).not.toBeNull();
1363
-
1364
- // Check edges
1365
- const memberEdges = graph.getEdgesTo('team-platform', 'member_of');
1366
- expect(memberEdges).toHaveLength(1);
1367
- expect(memberEdges[0].source_id).toBe('person-alice-smith');
1368
-
1369
- const partOfEdges = graph.getEdgesFrom('team-platform', 'part_of');
1370
- expect(partOfEdges).toHaveLength(1);
1371
- expect(partOfEdges[0].target_id).toBe('department-engineering');
1372
- });
1373
-
1374
- it('is idempotent — running twice creates no duplicates', () => {
1375
- const config = { orgName: 'Acme', users: [{ name: 'Alice', email: 'a@a.com' }] };
1376
- autoPopulate(graph, config);
1377
- autoPopulate(graph, config); // second run
1378
-
1379
- const people = graph.listEntities({ type: 'person' });
1380
- expect(people).toHaveLength(1);
1381
- });
1382
-
1383
- it('creates project entities from workspace data', () => {
1384
- autoPopulate(graph, {
1385
- orgName: 'Acme',
1386
- projects: [
1387
- { name: 'Spaces', team: 'Platform', repoUrl: 'https://github.com/org/spaces' },
1388
- ],
1389
- teams: [{ name: 'Platform', department: 'Engineering' }],
1390
- });
1391
-
1392
- const project = graph.getEntity('project-spaces');
1393
- expect(project).not.toBeNull();
1394
-
1395
- const ownsEdges = graph.getEdgesTo('project-spaces', 'owns');
1396
- expect(ownsEdges).toHaveLength(1);
1397
- expect(ownsEdges[0].source_id).toBe('team-platform');
1398
- });
1399
- });
1400
- ```
1401
-
1402
- - [ ] **Step 2: Run test to verify it fails**
1403
-
1404
- Run: `npx vitest run tests/lib/cortex/graph/auto-populate.test.ts`
1405
- Expected: FAIL — cannot find module `@/lib/cortex/graph/auto-populate`
1406
-
1407
- - [ ] **Step 3: Implement auto-populate**
1408
-
1409
- ```typescript
1410
- // src/lib/cortex/graph/auto-populate.ts
1411
- import type { EntityGraph } from './entity-graph';
1412
- import { slugify, entityId } from './types';
1413
-
1414
- interface UserInput {
1415
- name: string;
1416
- email?: string;
1417
- role?: string;
1418
- }
1419
-
1420
- interface TeamInput {
1421
- name: string;
1422
- department?: string;
1423
- members?: string[]; // person names
1424
- }
1425
-
1426
- interface ProjectInput {
1427
- name: string;
1428
- team?: string; // team name
1429
- repoUrl?: string;
1430
- }
1431
-
1432
- export interface AutoPopulateConfig {
1433
- orgName: string;
1434
- users?: UserInput[];
1435
- teams?: TeamInput[];
1436
- projects?: ProjectInput[];
1437
- }
1438
-
1439
- export function autoPopulate(graph: EntityGraph, config: AutoPopulateConfig): void {
1440
- const orgId = entityId('organization', slugify(config.orgName));
1441
-
1442
- // 1. Organization (idempotent)
1443
- if (!graph.getEntity(orgId)) {
1444
- graph.createEntity({ type: 'organization', name: config.orgName });
1445
- }
1446
-
1447
- // Track created departments for dedup
1448
- const deptIds = new Set<string>();
1449
-
1450
- // 2. Teams + departments
1451
- if (config.teams) {
1452
- for (const team of config.teams) {
1453
- const teamId = entityId('team', slugify(team.name));
1454
-
1455
- if (!graph.getEntity(teamId)) {
1456
- graph.createEntity({ type: 'team', name: team.name });
1457
- }
1458
-
1459
- // Department
1460
- if (team.department) {
1461
- const deptId = entityId('department', slugify(team.department));
1462
- if (!deptIds.has(deptId) && !graph.getEntity(deptId)) {
1463
- graph.createEntity({ type: 'department', name: team.department });
1464
- graph.createEdge({ source_id: deptId, target_id: orgId, relation: 'part_of' });
1465
- }
1466
- deptIds.add(deptId);
1467
- graph.createEdge({ source_id: teamId, target_id: deptId, relation: 'part_of' });
1468
- }
1469
- }
1470
- }
1471
-
1472
- // 3. Users
1473
- if (config.users) {
1474
- for (const user of config.users) {
1475
- const personId = entityId('person', slugify(user.name));
1476
-
1477
- if (!graph.getEntity(personId)) {
1478
- graph.createEntity({
1479
- type: 'person',
1480
- name: user.name,
1481
- metadata: {
1482
- ...(user.email && { email: user.email }),
1483
- ...(user.role && { role: user.role }),
1484
- },
1485
- });
1486
- }
1487
-
1488
- // Link to teams
1489
- if (config.teams) {
1490
- for (const team of config.teams) {
1491
- if (team.members?.includes(user.name)) {
1492
- const teamId = entityId('team', slugify(team.name));
1493
- graph.createEdge({
1494
- source_id: personId,
1495
- target_id: teamId,
1496
- relation: 'member_of',
1497
- metadata: { role: user.role ?? 'member' },
1498
- });
1499
- }
1500
- }
1501
- }
1502
- }
1503
- }
1504
-
1505
- // 4. Projects
1506
- if (config.projects) {
1507
- for (const project of config.projects) {
1508
- const projectId = entityId('project', slugify(project.name));
1509
-
1510
- if (!graph.getEntity(projectId)) {
1511
- graph.createEntity({
1512
- type: 'project',
1513
- name: project.name,
1514
- metadata: {
1515
- ...(project.repoUrl && { repo_url: project.repoUrl }),
1516
- },
1517
- });
1518
- }
1519
-
1520
- // Link to team
1521
- if (project.team) {
1522
- const teamId = entityId('team', slugify(project.team));
1523
- graph.createEdge({ source_id: teamId, target_id: projectId, relation: 'owns' });
1524
- }
1525
- }
1526
- }
1527
- }
1528
- ```
1529
-
1530
- - [ ] **Step 4: Run test to verify it passes**
1531
-
1532
- Run: `npx vitest run tests/lib/cortex/graph/auto-populate.test.ts`
1533
- Expected: PASS (all 5 tests)
1534
-
1535
- - [ ] **Step 5: Commit**
1536
-
1537
- ```bash
1538
- git add src/lib/cortex/graph/auto-populate.ts tests/lib/cortex/graph/auto-populate.test.ts
1539
- git commit -m "feat(cortex): add auto-populate for seeding entity graph from org data"
1540
- ```
1541
-
1542
- ---
1543
-
1544
- ## Chunk 4: API Routes and CortexInstance Integration
1545
-
1546
- ### Task 9: Graph API — entity endpoints
1547
-
1548
- **Files:**
1549
- - Create: `src/app/api/cortex/graph/entities/route.ts`
1550
- - Create: `src/app/api/cortex/graph/entities/[id]/route.ts`
1551
-
1552
- - [ ] **Step 1: Create entity list/create endpoint**
1553
-
1554
- ```typescript
1555
- // src/app/api/cortex/graph/entities/route.ts
1556
- import { NextResponse } from 'next/server';
1557
- import type { NextRequest } from 'next/server';
1558
- import { getAuthUser, withUser } from '@/lib/auth';
1559
- import { getCortex, isCortexAvailable } from '@/lib/cortex';
1560
-
1561
- export async function GET(request: NextRequest) {
1562
- const user = getAuthUser(request);
1563
- return withUser(user, async () => {
1564
- if (!isCortexAvailable()) {
1565
- return NextResponse.json({ error: 'Cortex unavailable' }, { status: 403 });
1566
- }
1567
- const cortex = await getCortex();
1568
- if (!cortex?.graph) return NextResponse.json({ entities: [] });
1569
-
1570
- const url = new URL(request.url);
1571
- const type = url.searchParams.get('type') || undefined;
1572
- const limit = parseInt(url.searchParams.get('limit') || '100', 10);
1573
-
1574
- const entities = cortex.graph.listEntities({
1575
- type: type as any,
1576
- limit,
1577
- });
1578
-
1579
- return NextResponse.json({ entities });
1580
- });
1581
- }
1582
-
1583
- export async function POST(request: NextRequest) {
1584
- const user = getAuthUser(request);
1585
- return withUser(user, async () => {
1586
- if (!isCortexAvailable()) {
1587
- return NextResponse.json({ error: 'Cortex unavailable' }, { status: 403 });
1588
- }
1589
- const cortex = await getCortex();
1590
- if (!cortex?.graph) {
1591
- return NextResponse.json({ error: 'Graph not initialized' }, { status: 500 });
1592
- }
1593
-
1594
- const body = await request.json();
1595
- const { type, name, id, metadata } = body;
1596
-
1597
- if (!type || !name) {
1598
- return NextResponse.json({ error: 'type and name are required' }, { status: 400 });
1599
- }
1600
-
1601
- const { isValidEntityType } = await import('@/lib/cortex/graph/types');
1602
- if (!isValidEntityType(type)) {
1603
- return NextResponse.json({ error: `Invalid entity type: ${type}` }, { status: 400 });
1604
- }
1605
-
1606
- try {
1607
- const entity = cortex.graph.createEntity({ id, type, name, metadata });
1608
- return NextResponse.json({ entity }, { status: 201 });
1609
- } catch (err: any) {
1610
- return NextResponse.json({ error: err.message }, { status: 409 });
1611
- }
1612
- });
1613
- }
1614
- ```
1615
-
1616
- - [ ] **Step 2: Create single-entity endpoint**
1617
-
1618
- ```typescript
1619
- // src/app/api/cortex/graph/entities/[id]/route.ts
1620
- import { NextResponse } from 'next/server';
1621
- import type { NextRequest } from 'next/server';
1622
- import { getAuthUser, withUser } from '@/lib/auth';
1623
- import { getCortex, isCortexAvailable } from '@/lib/cortex';
1624
-
1625
- export async function GET(
1626
- request: NextRequest,
1627
- { params }: { params: Promise<{ id: string }> },
1628
- ) {
1629
- const { id } = await params;
1630
- const user = getAuthUser(request);
1631
- return withUser(user, async () => {
1632
- if (!isCortexAvailable()) {
1633
- return NextResponse.json({ error: 'Cortex unavailable' }, { status: 403 });
1634
- }
1635
- const cortex = await getCortex();
1636
- if (!cortex?.graph) return NextResponse.json({ error: 'Graph not initialized' }, { status: 500 });
1637
-
1638
- const entity = cortex.graph.getEntity(id);
1639
- if (!entity) return NextResponse.json({ error: 'Not found' }, { status: 404 });
1640
- return NextResponse.json({ entity });
1641
- });
1642
- }
1643
-
1644
- export async function PATCH(
1645
- request: NextRequest,
1646
- { params }: { params: Promise<{ id: string }> },
1647
- ) {
1648
- const { id } = await params;
1649
- const user = getAuthUser(request);
1650
- return withUser(user, async () => {
1651
- if (!isCortexAvailable()) {
1652
- return NextResponse.json({ error: 'Cortex unavailable' }, { status: 403 });
1653
- }
1654
- const cortex = await getCortex();
1655
- if (!cortex?.graph) return NextResponse.json({ error: 'Graph not initialized' }, { status: 500 });
1656
-
1657
- const body = await request.json();
1658
- const updated = cortex.graph.updateEntity(id, body);
1659
- if (!updated) return NextResponse.json({ error: 'Not found' }, { status: 404 });
1660
- return NextResponse.json({ entity: updated });
1661
- });
1662
- }
1663
-
1664
- export async function DELETE(
1665
- request: NextRequest,
1666
- { params }: { params: Promise<{ id: string }> },
1667
- ) {
1668
- const { id } = await params;
1669
- const user = getAuthUser(request);
1670
- return withUser(user, async () => {
1671
- if (!isCortexAvailable()) {
1672
- return NextResponse.json({ error: 'Cortex unavailable' }, { status: 403 });
1673
- }
1674
- const cortex = await getCortex();
1675
- if (!cortex?.graph) return NextResponse.json({ error: 'Graph not initialized' }, { status: 500 });
1676
-
1677
- cortex.graph.deleteEntity(id);
1678
- return NextResponse.json({ deleted: true });
1679
- });
1680
- }
1681
-
1682
- - [ ] **Step 3: Commit**
1683
-
1684
- ```bash
1685
- git add src/app/api/cortex/graph/
1686
- git commit -m "feat(cortex): add API routes for entity CRUD"
1687
- ```
1688
-
1689
- ---
1690
-
1691
- ### Task 10: Graph API — edge endpoints
1692
-
1693
- **Files:**
1694
- - Create: `src/app/api/cortex/graph/edges/route.ts`
1695
-
1696
- - [ ] **Step 1: Create edge list/create endpoint**
1697
-
1698
- ```typescript
1699
- // src/app/api/cortex/graph/edges/route.ts
1700
- import { NextResponse } from 'next/server';
1701
- import type { NextRequest } from 'next/server';
1702
- import { getAuthUser, withUser } from '@/lib/auth';
1703
- import { getCortex, isCortexAvailable } from '@/lib/cortex';
1704
-
1705
- export async function GET(request: NextRequest) {
1706
- const user = getAuthUser(request);
1707
- return withUser(user, async () => {
1708
- if (!isCortexAvailable()) {
1709
- return NextResponse.json({ error: 'Cortex unavailable' }, { status: 403 });
1710
- }
1711
- const cortex = await getCortex();
1712
- if (!cortex?.graph) return NextResponse.json({ edges: [] });
1713
-
1714
- const url = new URL(request.url);
1715
- const from = url.searchParams.get('from');
1716
- const to = url.searchParams.get('to');
1717
- const relation = url.searchParams.get('relation') || undefined;
1718
-
1719
- let edges;
1720
- if (from) {
1721
- edges = cortex.graph.getEdgesFrom(from, relation as any);
1722
- } else if (to) {
1723
- edges = cortex.graph.getEdgesTo(to, relation as any);
1724
- } else {
1725
- return NextResponse.json({ error: 'Provide from or to parameter' }, { status: 400 });
1726
- }
1727
-
1728
- return NextResponse.json({ edges });
1729
- });
1730
- }
1731
-
1732
- export async function POST(request: NextRequest) {
1733
- const user = getAuthUser(request);
1734
- return withUser(user, async () => {
1735
- if (!isCortexAvailable()) {
1736
- return NextResponse.json({ error: 'Cortex unavailable' }, { status: 403 });
1737
- }
1738
- const cortex = await getCortex();
1739
- if (!cortex?.graph) {
1740
- return NextResponse.json({ error: 'Graph not initialized' }, { status: 500 });
1741
- }
1742
-
1743
- const body = await request.json();
1744
- const { source_id, target_id, relation, weight, metadata } = body;
1745
-
1746
- if (!source_id || !target_id || !relation) {
1747
- return NextResponse.json(
1748
- { error: 'source_id, target_id, and relation are required' },
1749
- { status: 400 },
1750
- );
1751
- }
1752
-
1753
- const edge = cortex.graph.createEdge({ source_id, target_id, relation, weight, metadata });
1754
- return NextResponse.json({ edge }, { status: 201 });
1755
- });
1756
- }
1757
-
1758
- export async function DELETE(request: NextRequest) {
1759
- const user = getAuthUser(request);
1760
- return withUser(user, async () => {
1761
- if (!isCortexAvailable()) {
1762
- return NextResponse.json({ error: 'Cortex unavailable' }, { status: 403 });
1763
- }
1764
- const cortex = await getCortex();
1765
- if (!cortex?.graph) return NextResponse.json({ error: 'Graph not initialized' }, { status: 500 });
1766
-
1767
- const url = new URL(request.url);
1768
- const source_id = url.searchParams.get('source_id');
1769
- const target_id = url.searchParams.get('target_id');
1770
- const relation = url.searchParams.get('relation');
1771
-
1772
- if (!source_id || !target_id || !relation) {
1773
- return NextResponse.json(
1774
- { error: 'source_id, target_id, and relation query params are required' },
1775
- { status: 400 },
1776
- );
1777
- }
1778
-
1779
- cortex.graph.deleteEdge(source_id, target_id, relation as any);
1780
- return NextResponse.json({ deleted: true });
1781
- });
1782
- }
1783
- ```
1784
-
1785
- - [ ] **Step 2: Commit**
1786
-
1787
- ```bash
1788
- git add src/app/api/cortex/graph/edges/route.ts
1789
- git commit -m "feat(cortex): add API routes for edge CRUD"
1790
- ```
1791
-
1792
- ---
1793
-
1794
- ### Task 11: Integrate EntityGraph into CortexInstance
1795
-
1796
- **Files:**
1797
- - Modify: `src/lib/cortex/index.ts`
1798
-
1799
- - [ ] **Step 1: Read the current index.ts**
1800
-
1801
- Read `src/lib/cortex/index.ts` to understand the current CortexInstance pattern and initialization flow.
1802
-
1803
- - [ ] **Step 2: Add graph to CortexInstance**
1804
-
1805
- Add the `graph` property and initialization:
1806
-
1807
- 1. Import EntityGraph:
1808
- ```typescript
1809
- import { EntityGraph } from './graph/entity-graph';
1810
- ```
1811
-
1812
- 2. Add to CortexInstance interface:
1813
- ```typescript
1814
- export interface CortexInstance {
1815
- config: CortexConfig;
1816
- store: CortexStore;
1817
- search: CortexSearch;
1818
- pipeline: IngestionPipeline;
1819
- embedding: EmbeddingProvider;
1820
- graph: EntityGraph; // NEW
1821
- sync?: FederationSync;
1822
- distillQueue?: DistillationQueue;
1823
- distillScheduler?: DistillationScheduler;
1824
- }
1825
- ```
1826
-
1827
- 3. In `getCortex()`, after store initialization and before `_instance` assignment:
1828
- ```typescript
1829
- // Initialize entity graph (SQLite)
1830
- const graphPath = path.join(cortexDir, 'graph.db');
1831
- const graph = new EntityGraph(graphPath);
1832
- ```
1833
-
1834
- 4. Add `graph` to the instance object.
1835
-
1836
- 5. In `resetCortex()`, add cleanup:
1837
- ```typescript
1838
- if (_instance) {
1839
- _instance.graph.close();
1840
- // ... existing cleanup
1841
- }
1842
- ```
1843
-
1844
- - [ ] **Step 3: Run existing tests to verify no regressions**
1845
-
1846
- Run: `npx vitest run tests/lib/cortex/`
1847
- Expected: All existing tests pass. May have 2 pre-existing failures in config.test.ts and chunker.test.ts (known issues, unrelated).
1848
-
1849
- - [ ] **Step 4: Commit**
1850
-
1851
- ```bash
1852
- git add src/lib/cortex/index.ts
1853
- git commit -m "feat(cortex): integrate EntityGraph into CortexInstance lifecycle"
1854
- ```
1855
-
1856
- ---
1857
-
1858
- ### Task 12: Module index and barrel export
1859
-
1860
- **Files:**
1861
- - Create: `src/lib/cortex/graph/index.ts`
1862
-
1863
- - [ ] **Step 1: Create barrel export**
1864
-
1865
- ```typescript
1866
- // src/lib/cortex/graph/index.ts
1867
- export { EntityGraph } from './entity-graph';
1868
- export { EntityResolver } from './resolver';
1869
- export { autoPopulate } from './auto-populate';
1870
- export type { AutoPopulateConfig } from './auto-populate';
1871
- export { initGraphSchema } from './schema';
1872
- export {
1873
- entityId,
1874
- slugify,
1875
- isValidEntityType,
1876
- isValidEdgeRelation,
1877
- ENTITY_TYPES,
1878
- EDGE_RELATIONS,
1879
- } from './types';
1880
- export type {
1881
- Entity,
1882
- Edge,
1883
- EntityType,
1884
- EdgeRelation,
1885
- EntityAlias,
1886
- AccessGrant,
1887
- } from './types';
1888
- ```
1889
-
1890
- - [ ] **Step 2: Run full test suite**
1891
-
1892
- Run: `npx vitest run tests/lib/cortex/graph/`
1893
- Expected: PASS — all graph tests pass (30+ tests across 4 files)
1894
-
1895
- - [ ] **Step 3: Commit**
1896
-
1897
- ```bash
1898
- git add src/lib/cortex/graph/index.ts
1899
- git commit -m "feat(cortex): add graph module barrel export"
1900
- ```
1901
-
1902
- ---
1903
-
1904
- ## Summary
1905
-
1906
- | Task | Component | Tests | Status |
1907
- |------|-----------|-------|--------|
1908
- | 1 | Graph types | — | |
1909
- | 2 | SQLite schema | 2 | |
1910
- | 3 | Entity CRUD | 7 | |
1911
- | 4 | Edge CRUD | 7 | |
1912
- | 5 | Alias management | 5 | |
1913
- | 6 | BFS traversal | 10 | |
1914
- | 7 | Entity resolver | 5 | |
1915
- | 8 | Auto-populate | 5 | |
1916
- | 9 | Entity API routes | — | |
1917
- | 10 | Edge API routes | — | |
1918
- | 11 | CortexInstance integration | regression | |
1919
- | 12 | Barrel export | regression | |
1920
-
1921
- **Total: 12 tasks, 41 tests, 4 chunks**
1922
-
1923
- After this plan is complete, the entity graph foundation is in place for Pillar 2 (Knowledge Unit Evolution) to build on — linking knowledge units to graph entities and replacing the flat `layer` field with graph-aware `scope`.
1
+ # Cortex v2 — Pillar 1: Entity Graph Foundation
2
+
3
+ > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Add a lightweight SQLite-backed relationship graph to Cortex that models people, teams, departments, projects, systems, modules, and topics — the skeleton on which all future Cortex v2 pillars depend.
6
+
7
+ **Architecture:** A new `src/lib/cortex/graph/` module containing an `EntityGraph` class backed by `better-sqlite3` (already in package.json). The graph stores entities (nodes) and weighted edges (relationships) in three tables. Entity resolution provides alias-based and fuzzy lookup. BFS traversal computes graph distance for weight calculations. Auto-population seeds the graph from existing Spaces users and workspaces.
8
+
9
+ **Tech Stack:** TypeScript, better-sqlite3, vitest, Next.js API routes
10
+
11
+ **Spec:** `docs/superpowers/specs/2026-03-14-cortex-v2-design.md` — Pillar 1
12
+
13
+ ---
14
+
15
+ ## File Structure
16
+
17
+ ```
18
+ src/lib/cortex/graph/
19
+ ├── types.ts — EntityType, EdgeRelation, Entity, Edge, interfaces
20
+ ├── schema.ts — SQLite table DDL, migrations, constants
21
+ ├── entity-graph.ts — EntityGraph class: entity CRUD, edge CRUD, traversal
22
+ ├── resolver.ts — Entity resolution: alias lookup, fuzzy match
23
+ └── auto-populate.ts — Seed graph from Spaces users, workspaces (git-based seeding deferred to Pillar 5: Signal Ingestion)
24
+
25
+ tests/lib/cortex/graph/
26
+ ├── entity-graph.test.ts — Entity + edge CRUD tests
27
+ ├── traversal.test.ts — BFS distance, N-hop neighborhood tests
28
+ ├── resolver.test.ts — Alias + fuzzy resolution tests
29
+ └── auto-populate.test.ts — Auto-population tests
30
+
31
+ src/app/api/cortex/graph/
32
+ ├── entities/route.ts — GET (list/search), POST (create)
33
+ ├── entities/[id]/route.ts — GET, PATCH, DELETE single entity
34
+ └── edges/route.ts — GET (list), POST (create/upsert), DELETE (by query params)
35
+ ```
36
+
37
+ ---
38
+
39
+ ## Chunk 1: Types, Schema, and Entity CRUD
40
+
41
+ ### Task 1: Define graph types
42
+
43
+ **Files:**
44
+ - Create: `src/lib/cortex/graph/types.ts`
45
+
46
+ - [ ] **Step 1: Create the types file**
47
+
48
+ ```typescript
49
+ // src/lib/cortex/graph/types.ts
50
+
51
+ export const ENTITY_TYPES = [
52
+ 'person', 'team', 'department', 'organization',
53
+ 'project', 'system', 'module', 'topic',
54
+ ] as const;
55
+ export type EntityType = typeof ENTITY_TYPES[number];
56
+
57
+ export const EDGE_RELATIONS = [
58
+ // Organizational
59
+ 'member_of', 'belongs_to', 'part_of',
60
+ // Technical
61
+ 'works_on', 'expert_in', 'touches', 'owns', 'contains', 'depends_on', 'relates_to',
62
+ // Knowledge
63
+ 'created_by', 'about', 'scoped_to', 'derived_from',
64
+ ] as const;
65
+ export type EdgeRelation = typeof EDGE_RELATIONS[number];
66
+
67
+ export interface Entity {
68
+ id: string; // format: {type}-{slug}
69
+ type: EntityType;
70
+ name: string;
71
+ metadata: Record<string, unknown>;
72
+ created: string; // ISO timestamp
73
+ updated: string; // ISO timestamp
74
+ }
75
+
76
+ export interface Edge {
77
+ source_id: string;
78
+ target_id: string;
79
+ relation: EdgeRelation;
80
+ weight: number; // 0-1
81
+ metadata: Record<string, unknown>;
82
+ created: string; // ISO timestamp
83
+ }
84
+
85
+ export interface EntityAlias {
86
+ entity_id: string;
87
+ alias: string;
88
+ }
89
+
90
+ export interface AccessGrant {
91
+ knowledge_id: string;
92
+ grantee_entity_id: string;
93
+ granted_by: string;
94
+ created: string;
95
+ }
96
+
97
+ export function entityId(type: EntityType, slug: string): string {
98
+ return `${type}-${slug}`;
99
+ }
100
+
101
+ export function slugify(name: string): string {
102
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
103
+ }
104
+
105
+ export function isValidEntityType(s: string): s is EntityType {
106
+ return ENTITY_TYPES.includes(s as EntityType);
107
+ }
108
+
109
+ export function isValidEdgeRelation(s: string): s is EdgeRelation {
110
+ return EDGE_RELATIONS.includes(s as EdgeRelation);
111
+ }
112
+ ```
113
+
114
+ - [ ] **Step 2: Commit**
115
+
116
+ ```bash
117
+ git add src/lib/cortex/graph/types.ts
118
+ git commit -m "feat(cortex): add entity graph type definitions"
119
+ ```
120
+
121
+ ---
122
+
123
+ ### Task 2: Create SQLite schema
124
+
125
+ **Files:**
126
+ - Create: `src/lib/cortex/graph/schema.ts`
127
+ - Test: `tests/lib/cortex/graph/entity-graph.test.ts`
128
+
129
+ - [ ] **Step 1: Write the failing test for schema initialization**
130
+
131
+ ```typescript
132
+ // tests/lib/cortex/graph/entity-graph.test.ts
133
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
134
+ import fs from 'fs';
135
+ import path from 'path';
136
+ import os from 'os';
137
+ import Database from 'better-sqlite3';
138
+ import { initGraphSchema } from '@/lib/cortex/graph/schema';
139
+
140
+ describe('Graph Schema', () => {
141
+ let tmpDir: string;
142
+ let dbPath: string;
143
+
144
+ beforeEach(() => {
145
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cortex-graph-'));
146
+ dbPath = path.join(tmpDir, 'graph.db');
147
+ });
148
+
149
+ afterEach(() => {
150
+ fs.rmSync(tmpDir, { recursive: true, force: true });
151
+ });
152
+
153
+ it('creates all tables and indexes', () => {
154
+ const db = new Database(dbPath);
155
+ initGraphSchema(db);
156
+
157
+ // Verify tables exist
158
+ const tables = db.prepare(
159
+ "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
160
+ ).all() as { name: string }[];
161
+ const tableNames = tables.map(t => t.name);
162
+
163
+ expect(tableNames).toContain('entities');
164
+ expect(tableNames).toContain('edges');
165
+ expect(tableNames).toContain('entity_aliases');
166
+ expect(tableNames).toContain('access_grants');
167
+ expect(tableNames).toContain('gravity_state');
168
+
169
+ // Verify indexes exist
170
+ const indexes = db.prepare(
171
+ "SELECT name FROM sqlite_master WHERE type='index' AND name NOT LIKE 'sqlite_%'"
172
+ ).all() as { name: string }[];
173
+ const indexNames = indexes.map(i => i.name);
174
+
175
+ expect(indexNames).toContain('idx_entities_type');
176
+ expect(indexNames).toContain('idx_edges_target');
177
+ expect(indexNames).toContain('idx_aliases_alias');
178
+ expect(indexNames).toContain('idx_grants_grantee');
179
+
180
+ db.close();
181
+ });
182
+
183
+ it('is idempotent — calling twice does not error', () => {
184
+ const db = new Database(dbPath);
185
+ initGraphSchema(db);
186
+ initGraphSchema(db); // second call should not throw
187
+ db.close();
188
+ });
189
+ });
190
+ ```
191
+
192
+ - [ ] **Step 2: Run test to verify it fails**
193
+
194
+ Run: `npx vitest run tests/lib/cortex/graph/entity-graph.test.ts`
195
+ Expected: FAIL — cannot find module `@/lib/cortex/graph/schema`
196
+
197
+ - [ ] **Step 3: Implement the schema module**
198
+
199
+ ```typescript
200
+ // src/lib/cortex/graph/schema.ts
201
+ import type Database from 'better-sqlite3';
202
+
203
+ export function initGraphSchema(db: Database.Database): void {
204
+ db.exec(`
205
+ CREATE TABLE IF NOT EXISTS entities (
206
+ id TEXT PRIMARY KEY,
207
+ type TEXT NOT NULL,
208
+ name TEXT NOT NULL,
209
+ metadata TEXT DEFAULT '{}',
210
+ created TEXT NOT NULL,
211
+ updated TEXT NOT NULL
212
+ );
213
+
214
+ CREATE TABLE IF NOT EXISTS edges (
215
+ source_id TEXT NOT NULL,
216
+ target_id TEXT NOT NULL,
217
+ relation TEXT NOT NULL,
218
+ weight REAL DEFAULT 1.0,
219
+ metadata TEXT DEFAULT '{}',
220
+ created TEXT NOT NULL,
221
+ PRIMARY KEY (source_id, target_id, relation),
222
+ FOREIGN KEY (source_id) REFERENCES entities(id) ON DELETE CASCADE,
223
+ FOREIGN KEY (target_id) REFERENCES entities(id) ON DELETE CASCADE
224
+ );
225
+
226
+ CREATE TABLE IF NOT EXISTS entity_aliases (
227
+ entity_id TEXT NOT NULL,
228
+ alias TEXT NOT NULL,
229
+ PRIMARY KEY (entity_id, alias),
230
+ FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE
231
+ );
232
+
233
+ CREATE TABLE IF NOT EXISTS access_grants (
234
+ knowledge_id TEXT NOT NULL,
235
+ grantee_entity_id TEXT NOT NULL,
236
+ granted_by TEXT NOT NULL,
237
+ created TEXT NOT NULL,
238
+ PRIMARY KEY (knowledge_id, grantee_entity_id)
239
+ );
240
+
241
+ CREATE TABLE IF NOT EXISTS gravity_state (
242
+ key TEXT PRIMARY KEY,
243
+ value TEXT NOT NULL,
244
+ updated TEXT NOT NULL
245
+ );
246
+
247
+ CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(type);
248
+ CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_id, relation);
249
+ CREATE INDEX IF NOT EXISTS idx_aliases_alias ON entity_aliases(alias);
250
+ CREATE INDEX IF NOT EXISTS idx_grants_grantee ON access_grants(grantee_entity_id);
251
+ `);
252
+
253
+ // Enable WAL mode for better concurrent read performance
254
+ db.pragma('journal_mode = WAL');
255
+ db.pragma('foreign_keys = ON');
256
+ }
257
+ ```
258
+
259
+ - [ ] **Step 4: Run test to verify it passes**
260
+
261
+ Run: `npx vitest run tests/lib/cortex/graph/entity-graph.test.ts`
262
+ Expected: PASS (2 tests)
263
+
264
+ - [ ] **Step 5: Commit**
265
+
266
+ ```bash
267
+ git add src/lib/cortex/graph/schema.ts tests/lib/cortex/graph/entity-graph.test.ts
268
+ git commit -m "feat(cortex): add SQLite schema for entity graph"
269
+ ```
270
+
271
+ ---
272
+
273
+ ### Task 3: EntityGraph class — entity CRUD
274
+
275
+ **Files:**
276
+ - Create: `src/lib/cortex/graph/entity-graph.ts`
277
+ - Modify: `tests/lib/cortex/graph/entity-graph.test.ts`
278
+
279
+ - [ ] **Step 1: Write failing tests for entity CRUD**
280
+
281
+ Append to `tests/lib/cortex/graph/entity-graph.test.ts`:
282
+
283
+ ```typescript
284
+ import { EntityGraph } from '@/lib/cortex/graph/entity-graph';
285
+ import type { Entity } from '@/lib/cortex/graph/types';
286
+
287
+ describe('EntityGraph — Entity CRUD', () => {
288
+ let tmpDir: string;
289
+ let graph: EntityGraph;
290
+
291
+ beforeEach(() => {
292
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cortex-graph-'));
293
+ graph = new EntityGraph(path.join(tmpDir, 'graph.db'));
294
+ });
295
+
296
+ afterEach(() => {
297
+ graph.close();
298
+ fs.rmSync(tmpDir, { recursive: true, force: true });
299
+ });
300
+
301
+ it('creates and retrieves an entity', () => {
302
+ const entity = graph.createEntity({
303
+ type: 'person',
304
+ name: 'Alice Smith',
305
+ metadata: { email: 'alice@acme.com', role: 'lead' },
306
+ });
307
+
308
+ expect(entity.id).toBe('person-alice-smith');
309
+ expect(entity.type).toBe('person');
310
+ expect(entity.name).toBe('Alice Smith');
311
+ expect(entity.metadata).toEqual({ email: 'alice@acme.com', role: 'lead' });
312
+
313
+ const fetched = graph.getEntity('person-alice-smith');
314
+ expect(fetched).not.toBeNull();
315
+ expect(fetched!.name).toBe('Alice Smith');
316
+ });
317
+
318
+ it('creates entity with explicit id', () => {
319
+ const entity = graph.createEntity({
320
+ id: 'person-custom-id',
321
+ type: 'person',
322
+ name: 'Bob',
323
+ });
324
+ expect(entity.id).toBe('person-custom-id');
325
+ });
326
+
327
+ it('updates an entity', () => {
328
+ graph.createEntity({ type: 'team', name: 'Platform' });
329
+ const updated = graph.updateEntity('team-platform', {
330
+ name: 'Platform Engineering',
331
+ metadata: { purpose: 'core infra' },
332
+ });
333
+ expect(updated!.name).toBe('Platform Engineering');
334
+ expect(updated!.metadata).toEqual({ purpose: 'core infra' });
335
+ });
336
+
337
+ it('deletes an entity', () => {
338
+ graph.createEntity({ type: 'topic', name: 'Auth' });
339
+ expect(graph.getEntity('topic-auth')).not.toBeNull();
340
+ graph.deleteEntity('topic-auth');
341
+ expect(graph.getEntity('topic-auth')).toBeNull();
342
+ });
343
+
344
+ it('lists entities by type', () => {
345
+ graph.createEntity({ type: 'person', name: 'Alice' });
346
+ graph.createEntity({ type: 'person', name: 'Bob' });
347
+ graph.createEntity({ type: 'team', name: 'Platform' });
348
+
349
+ const people = graph.listEntities({ type: 'person' });
350
+ expect(people).toHaveLength(2);
351
+
352
+ const all = graph.listEntities();
353
+ expect(all).toHaveLength(3);
354
+ });
355
+
356
+ it('returns null for non-existent entity', () => {
357
+ expect(graph.getEntity('person-nobody')).toBeNull();
358
+ });
359
+
360
+ it('throws on duplicate entity id', () => {
361
+ graph.createEntity({ type: 'person', name: 'Alice' });
362
+ expect(() => graph.createEntity({ type: 'person', name: 'Alice' }))
363
+ .toThrow();
364
+ });
365
+ });
366
+ ```
367
+
368
+ - [ ] **Step 2: Run test to verify it fails**
369
+
370
+ Run: `npx vitest run tests/lib/cortex/graph/entity-graph.test.ts`
371
+ Expected: FAIL — cannot find module `@/lib/cortex/graph/entity-graph`
372
+
373
+ - [ ] **Step 3: Implement EntityGraph — entity CRUD**
374
+
375
+ ```typescript
376
+ // src/lib/cortex/graph/entity-graph.ts
377
+ import Database from 'better-sqlite3';
378
+ import { initGraphSchema } from './schema';
379
+ import { entityId, slugify } from './types';
380
+ import type { Entity, EntityType, Edge, EdgeRelation, EntityAlias } from './types';
381
+
382
+ interface CreateEntityInput {
383
+ id?: string;
384
+ type: EntityType;
385
+ name: string;
386
+ metadata?: Record<string, unknown>;
387
+ }
388
+
389
+ interface UpdateEntityInput {
390
+ name?: string;
391
+ metadata?: Record<string, unknown>;
392
+ }
393
+
394
+ interface ListEntitiesFilter {
395
+ type?: EntityType;
396
+ limit?: number;
397
+ }
398
+
399
+ export class EntityGraph {
400
+ private db: Database.Database;
401
+
402
+ constructor(dbPath: string) {
403
+ this.db = new Database(dbPath);
404
+ initGraphSchema(this.db);
405
+ }
406
+
407
+ // --- Entity CRUD ---
408
+
409
+ createEntity(input: CreateEntityInput): Entity {
410
+ const id = input.id ?? entityId(input.type, slugify(input.name));
411
+ const now = new Date().toISOString();
412
+ const metadata = JSON.stringify(input.metadata ?? {});
413
+
414
+ this.db.prepare(`
415
+ INSERT INTO entities (id, type, name, metadata, created, updated)
416
+ VALUES (?, ?, ?, ?, ?, ?)
417
+ `).run(id, input.type, input.name, metadata, now, now);
418
+
419
+ return { id, type: input.type, name: input.name, metadata: input.metadata ?? {}, created: now, updated: now };
420
+ }
421
+
422
+ getEntity(id: string): Entity | null {
423
+ const row = this.db.prepare('SELECT * FROM entities WHERE id = ?').get(id) as any;
424
+ if (!row) return null;
425
+ return this.rowToEntity(row);
426
+ }
427
+
428
+ updateEntity(id: string, updates: UpdateEntityInput): Entity | null {
429
+ const existing = this.getEntity(id);
430
+ if (!existing) return null;
431
+
432
+ const now = new Date().toISOString();
433
+ const name = updates.name ?? existing.name;
434
+ const metadata = updates.metadata !== undefined
435
+ ? JSON.stringify(updates.metadata)
436
+ : JSON.stringify(existing.metadata);
437
+
438
+ this.db.prepare(`
439
+ UPDATE entities SET name = ?, metadata = ?, updated = ? WHERE id = ?
440
+ `).run(name, metadata, now, id);
441
+
442
+ return { ...existing, name, metadata: updates.metadata ?? existing.metadata, updated: now };
443
+ }
444
+
445
+ deleteEntity(id: string): void {
446
+ this.db.prepare('DELETE FROM entities WHERE id = ?').run(id);
447
+ }
448
+
449
+ listEntities(filter: ListEntitiesFilter = {}): Entity[] {
450
+ let sql = 'SELECT * FROM entities';
451
+ const params: any[] = [];
452
+
453
+ if (filter.type) {
454
+ sql += ' WHERE type = ?';
455
+ params.push(filter.type);
456
+ }
457
+
458
+ sql += ' ORDER BY name';
459
+
460
+ if (filter.limit) {
461
+ sql += ' LIMIT ?';
462
+ params.push(filter.limit);
463
+ }
464
+
465
+ const rows = this.db.prepare(sql).all(...params) as any[];
466
+ return rows.map(r => this.rowToEntity(r));
467
+ }
468
+
469
+ close(): void {
470
+ this.db.close();
471
+ }
472
+
473
+ private rowToEntity(row: any): Entity {
474
+ return {
475
+ id: row.id,
476
+ type: row.type as EntityType,
477
+ name: row.name,
478
+ metadata: JSON.parse(row.metadata || '{}'),
479
+ created: row.created,
480
+ updated: row.updated,
481
+ };
482
+ }
483
+ }
484
+ ```
485
+
486
+ - [ ] **Step 4: Run test to verify it passes**
487
+
488
+ Run: `npx vitest run tests/lib/cortex/graph/entity-graph.test.ts`
489
+ Expected: PASS (all 9 tests)
490
+
491
+ - [ ] **Step 5: Commit**
492
+
493
+ ```bash
494
+ git add src/lib/cortex/graph/entity-graph.ts tests/lib/cortex/graph/entity-graph.test.ts
495
+ git commit -m "feat(cortex): add EntityGraph class with entity CRUD"
496
+ ```
497
+
498
+ ---
499
+
500
+ ## Chunk 2: Edge CRUD and Graph Traversal
501
+
502
+ ### Task 4: Edge CRUD methods
503
+
504
+ **Files:**
505
+ - Modify: `src/lib/cortex/graph/entity-graph.ts`
506
+ - Modify: `tests/lib/cortex/graph/entity-graph.test.ts`
507
+
508
+ - [ ] **Step 1: Write failing tests for edge CRUD**
509
+
510
+ Append to `tests/lib/cortex/graph/entity-graph.test.ts`:
511
+
512
+ ```typescript
513
+ describe('EntityGraph — Edge CRUD', () => {
514
+ let tmpDir: string;
515
+ let graph: EntityGraph;
516
+
517
+ beforeEach(() => {
518
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cortex-graph-'));
519
+ graph = new EntityGraph(path.join(tmpDir, 'graph.db'));
520
+ // Seed entities
521
+ graph.createEntity({ type: 'person', name: 'Alice' });
522
+ graph.createEntity({ type: 'person', name: 'Bob' });
523
+ graph.createEntity({ type: 'team', name: 'Platform' });
524
+ graph.createEntity({ type: 'system', name: 'Auth Service' });
525
+ graph.createEntity({ type: 'topic', name: 'Authentication' });
526
+ });
527
+
528
+ afterEach(() => {
529
+ graph.close();
530
+ fs.rmSync(tmpDir, { recursive: true, force: true });
531
+ });
532
+
533
+ it('creates an edge between entities', () => {
534
+ const edge = graph.createEdge({
535
+ source_id: 'person-alice',
536
+ target_id: 'team-platform',
537
+ relation: 'member_of',
538
+ weight: 1.0,
539
+ metadata: { role: 'lead' },
540
+ });
541
+
542
+ expect(edge.source_id).toBe('person-alice');
543
+ expect(edge.target_id).toBe('team-platform');
544
+ expect(edge.relation).toBe('member_of');
545
+ expect(edge.weight).toBe(1.0);
546
+ });
547
+
548
+ it('upserts edge — updates weight on duplicate', () => {
549
+ graph.createEdge({
550
+ source_id: 'person-alice',
551
+ target_id: 'topic-authentication',
552
+ relation: 'expert_in',
553
+ weight: 0.3,
554
+ });
555
+ graph.createEdge({
556
+ source_id: 'person-alice',
557
+ target_id: 'topic-authentication',
558
+ relation: 'expert_in',
559
+ weight: 0.8,
560
+ });
561
+
562
+ const edges = graph.getEdgesFrom('person-alice', 'expert_in');
563
+ expect(edges).toHaveLength(1);
564
+ expect(edges[0].weight).toBe(0.8);
565
+ });
566
+
567
+ it('lists edges from an entity', () => {
568
+ graph.createEdge({ source_id: 'person-alice', target_id: 'team-platform', relation: 'member_of' });
569
+ graph.createEdge({ source_id: 'person-alice', target_id: 'topic-authentication', relation: 'expert_in' });
570
+ graph.createEdge({ source_id: 'person-bob', target_id: 'team-platform', relation: 'member_of' });
571
+
572
+ const aliceEdges = graph.getEdgesFrom('person-alice');
573
+ expect(aliceEdges).toHaveLength(2);
574
+
575
+ const platformMembers = graph.getEdgesTo('team-platform', 'member_of');
576
+ expect(platformMembers).toHaveLength(2);
577
+ });
578
+
579
+ it('deletes an edge', () => {
580
+ graph.createEdge({ source_id: 'person-alice', target_id: 'team-platform', relation: 'member_of' });
581
+ expect(graph.getEdgesFrom('person-alice')).toHaveLength(1);
582
+
583
+ graph.deleteEdge('person-alice', 'team-platform', 'member_of');
584
+ expect(graph.getEdgesFrom('person-alice')).toHaveLength(0);
585
+ });
586
+
587
+ it('cascades entity delete to edges', () => {
588
+ graph.createEdge({ source_id: 'person-alice', target_id: 'team-platform', relation: 'member_of' });
589
+ graph.deleteEntity('person-alice');
590
+ expect(graph.getEdgesTo('team-platform', 'member_of')).toHaveLength(0);
591
+ });
592
+
593
+ it('increments edge weight', () => {
594
+ graph.createEdge({ source_id: 'person-alice', target_id: 'topic-authentication', relation: 'expert_in', weight: 0.5 });
595
+ graph.incrementEdgeWeight('person-alice', 'topic-authentication', 'expert_in', 0.1);
596
+ const edges = graph.getEdgesFrom('person-alice', 'expert_in');
597
+ expect(edges[0].weight).toBeCloseTo(0.6);
598
+ });
599
+
600
+ it('caps edge weight at 1.0', () => {
601
+ graph.createEdge({ source_id: 'person-alice', target_id: 'topic-authentication', relation: 'expert_in', weight: 0.95 });
602
+ graph.incrementEdgeWeight('person-alice', 'topic-authentication', 'expert_in', 0.2);
603
+ const edges = graph.getEdgesFrom('person-alice', 'expert_in');
604
+ expect(edges[0].weight).toBe(1.0);
605
+ });
606
+ });
607
+ ```
608
+
609
+ - [ ] **Step 2: Run test to verify it fails**
610
+
611
+ Run: `npx vitest run tests/lib/cortex/graph/entity-graph.test.ts`
612
+ Expected: FAIL — `graph.createEdge is not a function`
613
+
614
+ - [ ] **Step 3: Add edge CRUD methods to EntityGraph**
615
+
616
+ Add these methods to the `EntityGraph` class in `src/lib/cortex/graph/entity-graph.ts`:
617
+
618
+ ```typescript
619
+ interface CreateEdgeInput {
620
+ source_id: string;
621
+ target_id: string;
622
+ relation: EdgeRelation;
623
+ weight?: number;
624
+ metadata?: Record<string, unknown>;
625
+ }
626
+
627
+ // Inside class EntityGraph:
628
+
629
+ createEdge(input: CreateEdgeInput): Edge {
630
+ const now = new Date().toISOString();
631
+ const weight = input.weight ?? 1.0;
632
+ const metadata = JSON.stringify(input.metadata ?? {});
633
+
634
+ this.db.prepare(`
635
+ INSERT INTO edges (source_id, target_id, relation, weight, metadata, created)
636
+ VALUES (?, ?, ?, ?, ?, ?)
637
+ ON CONFLICT(source_id, target_id, relation)
638
+ DO UPDATE SET weight = excluded.weight, metadata = excluded.metadata
639
+ `).run(input.source_id, input.target_id, input.relation, weight, metadata, now);
640
+
641
+ return {
642
+ source_id: input.source_id,
643
+ target_id: input.target_id,
644
+ relation: input.relation as EdgeRelation,
645
+ weight,
646
+ metadata: input.metadata ?? {},
647
+ created: now,
648
+ };
649
+ }
650
+
651
+ getEdgesFrom(entityId: string, relation?: EdgeRelation): Edge[] {
652
+ let sql = 'SELECT * FROM edges WHERE source_id = ?';
653
+ const params: any[] = [entityId];
654
+ if (relation) {
655
+ sql += ' AND relation = ?';
656
+ params.push(relation);
657
+ }
658
+ return (this.db.prepare(sql).all(...params) as any[]).map(r => this.rowToEdge(r));
659
+ }
660
+
661
+ getEdgesTo(entityId: string, relation?: EdgeRelation): Edge[] {
662
+ let sql = 'SELECT * FROM edges WHERE target_id = ?';
663
+ const params: any[] = [entityId];
664
+ if (relation) {
665
+ sql += ' AND relation = ?';
666
+ params.push(relation);
667
+ }
668
+ return (this.db.prepare(sql).all(...params) as any[]).map(r => this.rowToEdge(r));
669
+ }
670
+
671
+ deleteEdge(sourceId: string, targetId: string, relation: EdgeRelation): void {
672
+ this.db.prepare(
673
+ 'DELETE FROM edges WHERE source_id = ? AND target_id = ? AND relation = ?'
674
+ ).run(sourceId, targetId, relation);
675
+ }
676
+
677
+ incrementEdgeWeight(sourceId: string, targetId: string, relation: EdgeRelation, delta: number): void {
678
+ this.db.prepare(`
679
+ UPDATE edges SET weight = MIN(1.0, weight + ?) WHERE source_id = ? AND target_id = ? AND relation = ?
680
+ `).run(delta, sourceId, targetId, relation);
681
+ }
682
+
683
+ private rowToEdge(row: any): Edge {
684
+ return {
685
+ source_id: row.source_id,
686
+ target_id: row.target_id,
687
+ relation: row.relation as EdgeRelation,
688
+ weight: row.weight,
689
+ metadata: JSON.parse(row.metadata || '{}'),
690
+ created: row.created,
691
+ };
692
+ }
693
+ ```
694
+
695
+ Also add the `CreateEdgeInput` interface import and export the interface types at the top of the file.
696
+
697
+ - [ ] **Step 4: Run test to verify it passes**
698
+
699
+ Run: `npx vitest run tests/lib/cortex/graph/entity-graph.test.ts`
700
+ Expected: PASS (all 16 tests)
701
+
702
+ - [ ] **Step 5: Commit**
703
+
704
+ ```bash
705
+ git add src/lib/cortex/graph/entity-graph.ts tests/lib/cortex/graph/entity-graph.test.ts
706
+ git commit -m "feat(cortex): add edge CRUD with upsert and weight increment"
707
+ ```
708
+
709
+ ---
710
+
711
+ ### Task 5: Alias management
712
+
713
+ **Files:**
714
+ - Modify: `src/lib/cortex/graph/entity-graph.ts`
715
+ - Modify: `tests/lib/cortex/graph/entity-graph.test.ts`
716
+
717
+ - [ ] **Step 1: Write failing tests for aliases**
718
+
719
+ Append to `tests/lib/cortex/graph/entity-graph.test.ts`:
720
+
721
+ ```typescript
722
+ describe('EntityGraph — Aliases', () => {
723
+ let tmpDir: string;
724
+ let graph: EntityGraph;
725
+
726
+ beforeEach(() => {
727
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cortex-graph-'));
728
+ graph = new EntityGraph(path.join(tmpDir, 'graph.db'));
729
+ graph.createEntity({ type: 'system', name: 'Auth Service' });
730
+ graph.createEntity({ type: 'topic', name: 'Authentication' });
731
+ });
732
+
733
+ afterEach(() => {
734
+ graph.close();
735
+ fs.rmSync(tmpDir, { recursive: true, force: true });
736
+ });
737
+
738
+ it('adds and retrieves aliases', () => {
739
+ graph.addAlias('system-auth-service', 'auth');
740
+ graph.addAlias('system-auth-service', 'auth-svc');
741
+ graph.addAlias('system-auth-service', 'authentication service');
742
+
743
+ const aliases = graph.getAliases('system-auth-service');
744
+ expect(aliases).toHaveLength(3);
745
+ expect(aliases).toContain('auth');
746
+ });
747
+
748
+ it('looks up entity by alias', () => {
749
+ graph.addAlias('system-auth-service', 'auth');
750
+ const entity = graph.findByAlias('auth');
751
+ expect(entity).not.toBeNull();
752
+ expect(entity!.id).toBe('system-auth-service');
753
+ });
754
+
755
+ it('returns null for unknown alias', () => {
756
+ expect(graph.findByAlias('nonexistent')).toBeNull();
757
+ });
758
+
759
+ it('auto-creates aliases from entity name on create', () => {
760
+ graph.createEntity({ type: 'system', name: 'API Gateway' });
761
+ // Should auto-create aliases: "api gateway", "api-gateway"
762
+ expect(graph.findByAlias('api gateway')).not.toBeNull();
763
+ expect(graph.findByAlias('api-gateway')).not.toBeNull();
764
+ });
765
+
766
+ it('removes aliases when entity is deleted', () => {
767
+ graph.addAlias('system-auth-service', 'auth');
768
+ graph.deleteEntity('system-auth-service');
769
+ expect(graph.findByAlias('auth')).toBeNull();
770
+ });
771
+ });
772
+ ```
773
+
774
+ - [ ] **Step 2: Run test to verify it fails**
775
+
776
+ Run: `npx vitest run tests/lib/cortex/graph/entity-graph.test.ts`
777
+ Expected: FAIL — `graph.addAlias is not a function`
778
+
779
+ - [ ] **Step 3: Add alias methods to EntityGraph**
780
+
781
+ Add to `src/lib/cortex/graph/entity-graph.ts`:
782
+
783
+ ```typescript
784
+ // Inside class EntityGraph:
785
+
786
+ addAlias(entityId: string, alias: string): void {
787
+ const normalized = alias.toLowerCase().trim();
788
+ this.db.prepare(
789
+ 'INSERT OR IGNORE INTO entity_aliases (entity_id, alias) VALUES (?, ?)'
790
+ ).run(entityId, normalized);
791
+ }
792
+
793
+ getAliases(entityId: string): string[] {
794
+ const rows = this.db.prepare(
795
+ 'SELECT alias FROM entity_aliases WHERE entity_id = ?'
796
+ ).all(entityId) as { alias: string }[];
797
+ return rows.map(r => r.alias);
798
+ }
799
+
800
+ findByAlias(alias: string): Entity | null {
801
+ const normalized = alias.toLowerCase().trim();
802
+ const row = this.db.prepare(
803
+ 'SELECT entity_id FROM entity_aliases WHERE alias = ? LIMIT 1'
804
+ ).get(normalized) as { entity_id: string } | undefined;
805
+ if (!row) return null;
806
+ return this.getEntity(row.entity_id);
807
+ }
808
+ ```
809
+
810
+ Also update `createEntity` to auto-add aliases:
811
+
812
+ ```typescript
813
+ // After the INSERT in createEntity, add:
814
+ const nameLower = input.name.toLowerCase();
815
+ const nameSlug = slugify(input.name);
816
+ this.addAlias(id, nameLower);
817
+ if (nameSlug !== nameLower) {
818
+ this.addAlias(id, nameSlug);
819
+ }
820
+ ```
821
+
822
+ - [ ] **Step 4: Run test to verify it passes**
823
+
824
+ Run: `npx vitest run tests/lib/cortex/graph/entity-graph.test.ts`
825
+ Expected: PASS (all 21 tests)
826
+
827
+ - [ ] **Step 5: Commit**
828
+
829
+ ```bash
830
+ git add src/lib/cortex/graph/entity-graph.ts tests/lib/cortex/graph/entity-graph.test.ts
831
+ git commit -m "feat(cortex): add alias management with auto-alias on entity creation"
832
+ ```
833
+
834
+ ---
835
+
836
+ ### Task 6: Graph traversal — BFS distance and N-hop neighborhood
837
+
838
+ **Files:**
839
+ - Modify: `src/lib/cortex/graph/entity-graph.ts`
840
+ - Create: `tests/lib/cortex/graph/traversal.test.ts`
841
+
842
+ - [ ] **Step 1: Write failing tests for traversal**
843
+
844
+ ```typescript
845
+ // tests/lib/cortex/graph/traversal.test.ts
846
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
847
+ import fs from 'fs';
848
+ import path from 'path';
849
+ import os from 'os';
850
+ import { EntityGraph } from '@/lib/cortex/graph/entity-graph';
851
+
852
+ describe('EntityGraph — Traversal', () => {
853
+ let tmpDir: string;
854
+ let graph: EntityGraph;
855
+
856
+ beforeEach(() => {
857
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cortex-graph-'));
858
+ graph = new EntityGraph(path.join(tmpDir, 'graph.db'));
859
+
860
+ // Build a test graph:
861
+ // Alice --member_of--> Platform --part_of--> Engineering --part_of--> Acme
862
+ // Bob --member_of--> Platform
863
+ // Alice --expert_in--> Auth (topic)
864
+ // Platform --owns--> Auth Service (system)
865
+ // Security --owns--> Auth Service
866
+ graph.createEntity({ type: 'organization', name: 'Acme' });
867
+ graph.createEntity({ type: 'department', name: 'Engineering' });
868
+ graph.createEntity({ type: 'department', name: 'Security Dept' });
869
+ graph.createEntity({ type: 'team', name: 'Platform' });
870
+ graph.createEntity({ type: 'team', name: 'Security' });
871
+ graph.createEntity({ type: 'person', name: 'Alice' });
872
+ graph.createEntity({ type: 'person', name: 'Bob' });
873
+ graph.createEntity({ type: 'topic', name: 'Auth' });
874
+ graph.createEntity({ type: 'system', name: 'Auth Service' });
875
+
876
+ graph.createEdge({ source_id: 'person-alice', target_id: 'team-platform', relation: 'member_of' });
877
+ graph.createEdge({ source_id: 'person-bob', target_id: 'team-platform', relation: 'member_of' });
878
+ graph.createEdge({ source_id: 'team-platform', target_id: 'department-engineering', relation: 'part_of' });
879
+ graph.createEdge({ source_id: 'team-security', target_id: 'department-security-dept', relation: 'part_of' });
880
+ graph.createEdge({ source_id: 'department-engineering', target_id: 'organization-acme', relation: 'part_of' });
881
+ graph.createEdge({ source_id: 'department-security-dept', target_id: 'organization-acme', relation: 'part_of' });
882
+ graph.createEdge({ source_id: 'person-alice', target_id: 'topic-auth', relation: 'expert_in' });
883
+ graph.createEdge({ source_id: 'team-platform', target_id: 'system-auth-service', relation: 'owns' });
884
+ graph.createEdge({ source_id: 'team-security', target_id: 'system-auth-service', relation: 'owns' });
885
+ });
886
+
887
+ afterEach(() => {
888
+ graph.close();
889
+ fs.rmSync(tmpDir, { recursive: true, force: true });
890
+ });
891
+
892
+ it('computes distance 0 to self', () => {
893
+ expect(graph.distance('person-alice', 'person-alice')).toBe(0);
894
+ });
895
+
896
+ it('computes distance 1 for direct neighbors', () => {
897
+ expect(graph.distance('person-alice', 'team-platform')).toBe(1);
898
+ expect(graph.distance('person-alice', 'topic-auth')).toBe(1);
899
+ });
900
+
901
+ it('computes distance 2 for two-hop paths', () => {
902
+ // Alice -> Platform -> Engineering
903
+ expect(graph.distance('person-alice', 'department-engineering')).toBe(2);
904
+ // Alice -> Platform -> Bob (via Platform)
905
+ expect(graph.distance('person-alice', 'person-bob')).toBe(2);
906
+ });
907
+
908
+ it('computes distance 3 for three-hop paths', () => {
909
+ // Alice -> Platform -> Engineering -> Acme
910
+ expect(graph.distance('person-alice', 'organization-acme')).toBe(3);
911
+ });
912
+
913
+ it('traverses edges bidirectionally', () => {
914
+ // Bob -> Platform (outgoing), Platform -> Alice (incoming to Platform)
915
+ expect(graph.distance('person-bob', 'person-alice')).toBe(2);
916
+ });
917
+
918
+ it('returns Infinity for unreachable entities', () => {
919
+ graph.createEntity({ type: 'topic', name: 'Isolated' });
920
+ expect(graph.distance('person-alice', 'topic-isolated')).toBe(Infinity);
921
+ });
922
+
923
+ it('respects maxHops limit', () => {
924
+ // Alice -> Platform -> Engineering -> Acme is 3 hops
925
+ expect(graph.distance('person-alice', 'organization-acme', 2)).toBe(Infinity);
926
+ expect(graph.distance('person-alice', 'organization-acme', 3)).toBe(3);
927
+ });
928
+
929
+ it('returns entities within N hops', () => {
930
+ const nearby = graph.neighborhood('person-alice', 1);
931
+ const ids = nearby.map(e => e.id);
932
+ expect(ids).toContain('team-platform');
933
+ expect(ids).toContain('topic-auth');
934
+ expect(ids).not.toContain('department-engineering');
935
+ expect(ids).not.toContain('person-alice'); // self excluded
936
+ });
937
+
938
+ it('returns entities within 2 hops', () => {
939
+ const nearby = graph.neighborhood('person-alice', 2);
940
+ const ids = nearby.map(e => e.id);
941
+ expect(ids).toContain('team-platform');
942
+ expect(ids).toContain('department-engineering');
943
+ expect(ids).toContain('person-bob');
944
+ expect(ids).toContain('system-auth-service');
945
+ });
946
+
947
+ it('computes graph proximity score', () => {
948
+ // Create an isolated entity within this test's scope
949
+ graph.createEntity({ type: 'topic', name: 'Orphaned' });
950
+
951
+ // proximity = 1 / (1 + distance)
952
+ expect(graph.proximity('person-alice', 'person-alice')).toBe(1.0); // distance 0
953
+ expect(graph.proximity('person-alice', 'team-platform')).toBe(0.5); // distance 1
954
+ expect(graph.proximity('person-alice', 'department-engineering')).toBeCloseTo(0.333); // distance 2
955
+ expect(graph.proximity('person-alice', 'topic-orphaned')).toBe(0); // unreachable (no edges)
956
+ });
957
+ });
958
+ ```
959
+
960
+ - [ ] **Step 2: Run test to verify it fails**
961
+
962
+ Run: `npx vitest run tests/lib/cortex/graph/traversal.test.ts`
963
+ Expected: FAIL — `graph.distance is not a function`
964
+
965
+ - [ ] **Step 3: Implement traversal methods**
966
+
967
+ Add to `src/lib/cortex/graph/entity-graph.ts`:
968
+
969
+ ```typescript
970
+ // Inside class EntityGraph:
971
+
972
+ /**
973
+ * BFS shortest-path distance between two entities.
974
+ * Edges are traversed bidirectionally (undirected graph for distance).
975
+ * Returns Infinity if no path exists within maxHops.
976
+ */
977
+ distance(fromId: string, toId: string, maxHops: number = 4): number {
978
+ if (fromId === toId) return 0;
979
+
980
+ const visited = new Set<string>([fromId]);
981
+ let frontier = [fromId];
982
+ let depth = 0;
983
+
984
+ while (frontier.length > 0 && depth < maxHops) {
985
+ depth++;
986
+ const nextFrontier: string[] = [];
987
+
988
+ for (const nodeId of frontier) {
989
+ const neighbors = this.getNeighborIds(nodeId);
990
+ for (const neighbor of neighbors) {
991
+ if (neighbor === toId) return depth;
992
+ if (!visited.has(neighbor)) {
993
+ visited.add(neighbor);
994
+ nextFrontier.push(neighbor);
995
+ }
996
+ }
997
+ }
998
+
999
+ frontier = nextFrontier;
1000
+ }
1001
+
1002
+ return Infinity;
1003
+ }
1004
+
1005
+ /**
1006
+ * All entities within N hops (excluding self).
1007
+ */
1008
+ neighborhood(entityId: string, maxHops: number): Entity[] {
1009
+ const visited = new Set<string>([entityId]);
1010
+ let frontier = [entityId];
1011
+
1012
+ for (let depth = 0; depth < maxHops; depth++) {
1013
+ const nextFrontier: string[] = [];
1014
+ for (const nodeId of frontier) {
1015
+ for (const neighbor of this.getNeighborIds(nodeId)) {
1016
+ if (!visited.has(neighbor)) {
1017
+ visited.add(neighbor);
1018
+ nextFrontier.push(neighbor);
1019
+ }
1020
+ }
1021
+ }
1022
+ frontier = nextFrontier;
1023
+ }
1024
+
1025
+ visited.delete(entityId); // exclude self
1026
+ return [...visited]
1027
+ .map(id => this.getEntity(id))
1028
+ .filter((e): e is Entity => e !== null);
1029
+ }
1030
+
1031
+ /**
1032
+ * Graph proximity: 1 / (1 + distance). Returns 0 for unreachable.
1033
+ */
1034
+ proximity(fromId: string, toId: string, maxHops: number = 4): number {
1035
+ const d = this.distance(fromId, toId, maxHops);
1036
+ if (d === Infinity) return 0;
1037
+ return 1 / (1 + d);
1038
+ }
1039
+
1040
+ /**
1041
+ * Get all neighbor IDs (both directions — edges are treated as undirected for traversal).
1042
+ * Single UNION query for efficiency during BFS.
1043
+ */
1044
+ private getNeighborIds(entityId: string): string[] {
1045
+ const rows = this.db.prepare(`
1046
+ SELECT target_id AS id FROM edges WHERE source_id = ?
1047
+ UNION
1048
+ SELECT source_id AS id FROM edges WHERE target_id = ?
1049
+ `).all(entityId, entityId) as { id: string }[];
1050
+
1051
+ return rows.map(r => r.id);
1052
+ }
1053
+ ```
1054
+
1055
+ - [ ] **Step 4: Run test to verify it passes**
1056
+
1057
+ Run: `npx vitest run tests/lib/cortex/graph/traversal.test.ts`
1058
+ Expected: PASS (all 10 tests)
1059
+
1060
+ - [ ] **Step 5: Commit**
1061
+
1062
+ ```bash
1063
+ git add src/lib/cortex/graph/entity-graph.ts tests/lib/cortex/graph/traversal.test.ts
1064
+ git commit -m "feat(cortex): add BFS distance, neighborhood, and proximity to entity graph"
1065
+ ```
1066
+
1067
+ ---
1068
+
1069
+ ## Chunk 3: Entity Resolution and Auto-Population
1070
+
1071
+ ### Task 7: Entity resolver — alias + fuzzy lookup
1072
+
1073
+ **Files:**
1074
+ - Create: `src/lib/cortex/graph/resolver.ts`
1075
+ - Create: `tests/lib/cortex/graph/resolver.test.ts`
1076
+
1077
+ - [ ] **Step 1: Write failing tests for resolver**
1078
+
1079
+ ```typescript
1080
+ // tests/lib/cortex/graph/resolver.test.ts
1081
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
1082
+ import fs from 'fs';
1083
+ import path from 'path';
1084
+ import os from 'os';
1085
+ import { EntityGraph } from '@/lib/cortex/graph/entity-graph';
1086
+ import { EntityResolver } from '@/lib/cortex/graph/resolver';
1087
+
1088
+ describe('EntityResolver', () => {
1089
+ let tmpDir: string;
1090
+ let graph: EntityGraph;
1091
+ let resolver: EntityResolver;
1092
+
1093
+ beforeEach(() => {
1094
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cortex-graph-'));
1095
+ graph = new EntityGraph(path.join(tmpDir, 'graph.db'));
1096
+ resolver = new EntityResolver(graph);
1097
+
1098
+ graph.createEntity({ type: 'system', name: 'Auth Service' });
1099
+ graph.createEntity({ type: 'system', name: 'API Gateway' });
1100
+ graph.createEntity({ type: 'topic', name: 'Authentication' });
1101
+ graph.createEntity({ type: 'topic', name: 'Performance' });
1102
+ graph.createEntity({ type: 'person', name: 'Alice Smith' });
1103
+ graph.addAlias('system-auth-service', 'auth');
1104
+ graph.addAlias('system-auth-service', 'auth-svc');
1105
+ });
1106
+
1107
+ afterEach(() => {
1108
+ graph.close();
1109
+ fs.rmSync(tmpDir, { recursive: true, force: true });
1110
+ });
1111
+
1112
+ it('resolves exact alias match', () => {
1113
+ const result = resolver.resolve('auth');
1114
+ expect(result).not.toBeNull();
1115
+ expect(result!.entity.id).toBe('system-auth-service');
1116
+ expect(result!.confidence).toBeGreaterThanOrEqual(0.95);
1117
+ expect(result!.method).toBe('alias');
1118
+ });
1119
+
1120
+ it('resolves fuzzy alias match', () => {
1121
+ const result = resolver.resolve('auth servce'); // typo
1122
+ expect(result).not.toBeNull();
1123
+ expect(result!.entity.id).toBe('system-auth-service');
1124
+ expect(result!.method).toBe('fuzzy');
1125
+ expect(result!.confidence).toBeLessThan(0.95); // lower than exact
1126
+ });
1127
+
1128
+ it('returns null for unresolvable text', () => {
1129
+ expect(resolver.resolve('completely unknown xyz')).toBeNull();
1130
+ });
1131
+
1132
+ it('extracts multiple entities from text', () => {
1133
+ const entities = resolver.extractEntities('fix the auth service performance issue');
1134
+ const ids = entities.map(e => e.entity.id);
1135
+ expect(ids).toContain('system-auth-service');
1136
+ expect(ids).toContain('topic-performance');
1137
+ });
1138
+
1139
+ it('prefers exact alias over fuzzy match', () => {
1140
+ const result = resolver.resolve('auth');
1141
+ expect(result!.method).toBe('alias');
1142
+ });
1143
+ });
1144
+ ```
1145
+
1146
+ - [ ] **Step 2: Run test to verify it fails**
1147
+
1148
+ Run: `npx vitest run tests/lib/cortex/graph/resolver.test.ts`
1149
+ Expected: FAIL — cannot find module `@/lib/cortex/graph/resolver`
1150
+
1151
+ - [ ] **Step 3: Implement the resolver**
1152
+
1153
+ ```typescript
1154
+ // src/lib/cortex/graph/resolver.ts
1155
+ import type { EntityGraph } from './entity-graph';
1156
+ import type { Entity } from './types';
1157
+
1158
+ export interface ResolvedEntity {
1159
+ entity: Entity;
1160
+ confidence: number;
1161
+ method: 'alias' | 'fuzzy' | 'name';
1162
+ }
1163
+
1164
+ export class EntityResolver {
1165
+ constructor(private graph: EntityGraph) {}
1166
+
1167
+ /**
1168
+ * Resolve a text fragment to an entity.
1169
+ * Tries: 1) exact alias 2) entity name 3) fuzzy match (Levenshtein ≤ 2)
1170
+ */
1171
+ resolve(text: string): ResolvedEntity | null {
1172
+ const normalized = text.toLowerCase().trim();
1173
+
1174
+ // 1. Exact alias match
1175
+ const byAlias = this.graph.findByAlias(normalized);
1176
+ if (byAlias) {
1177
+ return { entity: byAlias, confidence: 0.95, method: 'alias' };
1178
+ }
1179
+
1180
+ // 2. Exact name match (case-insensitive via alias auto-creation)
1181
+ // Already covered by alias lookup since createEntity auto-adds name as alias
1182
+
1183
+ // 3. Fuzzy match — scan all aliases for Levenshtein ≤ 2
1184
+ const allEntities = this.graph.listEntities();
1185
+ let bestMatch: ResolvedEntity | null = null;
1186
+ let bestDistance = 3; // max acceptable
1187
+
1188
+ for (const entity of allEntities) {
1189
+ const aliases = this.graph.getAliases(entity.id);
1190
+ const candidates = [entity.name.toLowerCase(), ...aliases];
1191
+
1192
+ for (const candidate of candidates) {
1193
+ const dist = levenshtein(normalized, candidate);
1194
+ if (dist < bestDistance) {
1195
+ bestDistance = dist;
1196
+ bestMatch = {
1197
+ entity,
1198
+ confidence: Math.max(0.5, 0.9 - dist * 0.15),
1199
+ method: 'fuzzy',
1200
+ };
1201
+ }
1202
+ }
1203
+ }
1204
+
1205
+ return bestMatch;
1206
+ }
1207
+
1208
+ /**
1209
+ * Extract all entity references from a text string.
1210
+ * Scans for known entity names and aliases within the text.
1211
+ */
1212
+ extractEntities(text: string): ResolvedEntity[] {
1213
+ const normalized = text.toLowerCase();
1214
+ const results: ResolvedEntity[] = [];
1215
+ const seen = new Set<string>();
1216
+
1217
+ const allEntities = this.graph.listEntities();
1218
+
1219
+ for (const entity of allEntities) {
1220
+ if (seen.has(entity.id)) continue;
1221
+
1222
+ const aliases = [entity.name.toLowerCase(), ...this.graph.getAliases(entity.id)];
1223
+
1224
+ for (const alias of aliases) {
1225
+ if (alias.length < 3) continue; // skip very short aliases to avoid false matches
1226
+ if (normalized.includes(alias)) {
1227
+ results.push({ entity, confidence: 0.85, method: 'alias' });
1228
+ seen.add(entity.id);
1229
+ break;
1230
+ }
1231
+ }
1232
+ }
1233
+
1234
+ return results;
1235
+ }
1236
+ }
1237
+
1238
+ /**
1239
+ * Levenshtein distance between two strings.
1240
+ */
1241
+ function levenshtein(a: string, b: string): number {
1242
+ if (a.length === 0) return b.length;
1243
+ if (b.length === 0) return a.length;
1244
+
1245
+ const matrix: number[][] = [];
1246
+
1247
+ for (let i = 0; i <= a.length; i++) {
1248
+ matrix[i] = [i];
1249
+ }
1250
+ for (let j = 0; j <= b.length; j++) {
1251
+ matrix[0][j] = j;
1252
+ }
1253
+
1254
+ for (let i = 1; i <= a.length; i++) {
1255
+ for (let j = 1; j <= b.length; j++) {
1256
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
1257
+ matrix[i][j] = Math.min(
1258
+ matrix[i - 1][j] + 1, // deletion
1259
+ matrix[i][j - 1] + 1, // insertion
1260
+ matrix[i - 1][j - 1] + cost // substitution
1261
+ );
1262
+ }
1263
+ }
1264
+
1265
+ return matrix[a.length][b.length];
1266
+ }
1267
+ ```
1268
+
1269
+ - [ ] **Step 4: Run test to verify it passes**
1270
+
1271
+ Run: `npx vitest run tests/lib/cortex/graph/resolver.test.ts`
1272
+ Expected: PASS (all 5 tests)
1273
+
1274
+ - [ ] **Step 5: Commit**
1275
+
1276
+ ```bash
1277
+ git add src/lib/cortex/graph/resolver.ts tests/lib/cortex/graph/resolver.test.ts
1278
+ git commit -m "feat(cortex): add entity resolver with alias and fuzzy matching"
1279
+ ```
1280
+
1281
+ ---
1282
+
1283
+ ### Task 8: Auto-population from Spaces data
1284
+
1285
+ > **Scope note:** This task seeds the graph from declarative configuration (org, users, teams, projects). Git-based seeding (WORKS_ON, TOUCHES, EXPERT_IN edges from commit history and blame; Systems/Modules from directory structure; Topics from file paths) is deferred to **Pillar 5: Observable Signal Ingestion** where the Git History adapter will populate these automatically.
1286
+
1287
+ **Files:**
1288
+ - Create: `src/lib/cortex/graph/auto-populate.ts`
1289
+ - Create: `tests/lib/cortex/graph/auto-populate.test.ts`
1290
+
1291
+ - [ ] **Step 1: Write failing tests for auto-population**
1292
+
1293
+ ```typescript
1294
+ // tests/lib/cortex/graph/auto-populate.test.ts
1295
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
1296
+ import fs from 'fs';
1297
+ import path from 'path';
1298
+ import os from 'os';
1299
+ import { EntityGraph } from '@/lib/cortex/graph/entity-graph';
1300
+ import { autoPopulate } from '@/lib/cortex/graph/auto-populate';
1301
+
1302
+ // Mock the user/workspace data sources
1303
+ vi.mock('@/lib/auth', () => ({
1304
+ getCurrentUser: () => 'test-user',
1305
+ getAuthUser: () => 'test-user',
1306
+ withUser: (_user: string, fn: () => any) => fn(),
1307
+ }));
1308
+
1309
+ describe('autoPopulate', () => {
1310
+ let tmpDir: string;
1311
+ let graph: EntityGraph;
1312
+
1313
+ beforeEach(() => {
1314
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cortex-graph-'));
1315
+ graph = new EntityGraph(path.join(tmpDir, 'graph.db'));
1316
+ });
1317
+
1318
+ afterEach(() => {
1319
+ graph.close();
1320
+ fs.rmSync(tmpDir, { recursive: true, force: true });
1321
+ });
1322
+
1323
+ it('creates default organization entity', () => {
1324
+ autoPopulate(graph, { orgName: 'Acme Corp' });
1325
+
1326
+ const org = graph.getEntity('organization-acme-corp');
1327
+ expect(org).not.toBeNull();
1328
+ expect(org!.name).toBe('Acme Corp');
1329
+ expect(org!.type).toBe('organization');
1330
+ });
1331
+
1332
+ it('creates person entities from user list', () => {
1333
+ autoPopulate(graph, {
1334
+ orgName: 'Acme',
1335
+ users: [
1336
+ { name: 'Alice Smith', email: 'alice@acme.com', role: 'lead' },
1337
+ { name: 'Bob Jones', email: 'bob@acme.com', role: 'member' },
1338
+ ],
1339
+ });
1340
+
1341
+ const alice = graph.getEntity('person-alice-smith');
1342
+ expect(alice).not.toBeNull();
1343
+ expect(alice!.metadata).toEqual({ email: 'alice@acme.com', role: 'lead' });
1344
+
1345
+ const bob = graph.getEntity('person-bob-jones');
1346
+ expect(bob).not.toBeNull();
1347
+ });
1348
+
1349
+ it('creates team entities and membership edges', () => {
1350
+ autoPopulate(graph, {
1351
+ orgName: 'Acme',
1352
+ teams: [
1353
+ { name: 'Platform', department: 'Engineering', members: ['Alice Smith'] },
1354
+ ],
1355
+ users: [{ name: 'Alice Smith', email: 'alice@acme.com' }],
1356
+ });
1357
+
1358
+ const team = graph.getEntity('team-platform');
1359
+ expect(team).not.toBeNull();
1360
+
1361
+ const dept = graph.getEntity('department-engineering');
1362
+ expect(dept).not.toBeNull();
1363
+
1364
+ // Check edges
1365
+ const memberEdges = graph.getEdgesTo('team-platform', 'member_of');
1366
+ expect(memberEdges).toHaveLength(1);
1367
+ expect(memberEdges[0].source_id).toBe('person-alice-smith');
1368
+
1369
+ const partOfEdges = graph.getEdgesFrom('team-platform', 'part_of');
1370
+ expect(partOfEdges).toHaveLength(1);
1371
+ expect(partOfEdges[0].target_id).toBe('department-engineering');
1372
+ });
1373
+
1374
+ it('is idempotent — running twice creates no duplicates', () => {
1375
+ const config = { orgName: 'Acme', users: [{ name: 'Alice', email: 'a@a.com' }] };
1376
+ autoPopulate(graph, config);
1377
+ autoPopulate(graph, config); // second run
1378
+
1379
+ const people = graph.listEntities({ type: 'person' });
1380
+ expect(people).toHaveLength(1);
1381
+ });
1382
+
1383
+ it('creates project entities from workspace data', () => {
1384
+ autoPopulate(graph, {
1385
+ orgName: 'Acme',
1386
+ projects: [
1387
+ { name: 'Spaces', team: 'Platform', repoUrl: 'https://github.com/org/spaces' },
1388
+ ],
1389
+ teams: [{ name: 'Platform', department: 'Engineering' }],
1390
+ });
1391
+
1392
+ const project = graph.getEntity('project-spaces');
1393
+ expect(project).not.toBeNull();
1394
+
1395
+ const ownsEdges = graph.getEdgesTo('project-spaces', 'owns');
1396
+ expect(ownsEdges).toHaveLength(1);
1397
+ expect(ownsEdges[0].source_id).toBe('team-platform');
1398
+ });
1399
+ });
1400
+ ```
1401
+
1402
+ - [ ] **Step 2: Run test to verify it fails**
1403
+
1404
+ Run: `npx vitest run tests/lib/cortex/graph/auto-populate.test.ts`
1405
+ Expected: FAIL — cannot find module `@/lib/cortex/graph/auto-populate`
1406
+
1407
+ - [ ] **Step 3: Implement auto-populate**
1408
+
1409
+ ```typescript
1410
+ // src/lib/cortex/graph/auto-populate.ts
1411
+ import type { EntityGraph } from './entity-graph';
1412
+ import { slugify, entityId } from './types';
1413
+
1414
+ interface UserInput {
1415
+ name: string;
1416
+ email?: string;
1417
+ role?: string;
1418
+ }
1419
+
1420
+ interface TeamInput {
1421
+ name: string;
1422
+ department?: string;
1423
+ members?: string[]; // person names
1424
+ }
1425
+
1426
+ interface ProjectInput {
1427
+ name: string;
1428
+ team?: string; // team name
1429
+ repoUrl?: string;
1430
+ }
1431
+
1432
+ export interface AutoPopulateConfig {
1433
+ orgName: string;
1434
+ users?: UserInput[];
1435
+ teams?: TeamInput[];
1436
+ projects?: ProjectInput[];
1437
+ }
1438
+
1439
+ export function autoPopulate(graph: EntityGraph, config: AutoPopulateConfig): void {
1440
+ const orgId = entityId('organization', slugify(config.orgName));
1441
+
1442
+ // 1. Organization (idempotent)
1443
+ if (!graph.getEntity(orgId)) {
1444
+ graph.createEntity({ type: 'organization', name: config.orgName });
1445
+ }
1446
+
1447
+ // Track created departments for dedup
1448
+ const deptIds = new Set<string>();
1449
+
1450
+ // 2. Teams + departments
1451
+ if (config.teams) {
1452
+ for (const team of config.teams) {
1453
+ const teamId = entityId('team', slugify(team.name));
1454
+
1455
+ if (!graph.getEntity(teamId)) {
1456
+ graph.createEntity({ type: 'team', name: team.name });
1457
+ }
1458
+
1459
+ // Department
1460
+ if (team.department) {
1461
+ const deptId = entityId('department', slugify(team.department));
1462
+ if (!deptIds.has(deptId) && !graph.getEntity(deptId)) {
1463
+ graph.createEntity({ type: 'department', name: team.department });
1464
+ graph.createEdge({ source_id: deptId, target_id: orgId, relation: 'part_of' });
1465
+ }
1466
+ deptIds.add(deptId);
1467
+ graph.createEdge({ source_id: teamId, target_id: deptId, relation: 'part_of' });
1468
+ }
1469
+ }
1470
+ }
1471
+
1472
+ // 3. Users
1473
+ if (config.users) {
1474
+ for (const user of config.users) {
1475
+ const personId = entityId('person', slugify(user.name));
1476
+
1477
+ if (!graph.getEntity(personId)) {
1478
+ graph.createEntity({
1479
+ type: 'person',
1480
+ name: user.name,
1481
+ metadata: {
1482
+ ...(user.email && { email: user.email }),
1483
+ ...(user.role && { role: user.role }),
1484
+ },
1485
+ });
1486
+ }
1487
+
1488
+ // Link to teams
1489
+ if (config.teams) {
1490
+ for (const team of config.teams) {
1491
+ if (team.members?.includes(user.name)) {
1492
+ const teamId = entityId('team', slugify(team.name));
1493
+ graph.createEdge({
1494
+ source_id: personId,
1495
+ target_id: teamId,
1496
+ relation: 'member_of',
1497
+ metadata: { role: user.role ?? 'member' },
1498
+ });
1499
+ }
1500
+ }
1501
+ }
1502
+ }
1503
+ }
1504
+
1505
+ // 4. Projects
1506
+ if (config.projects) {
1507
+ for (const project of config.projects) {
1508
+ const projectId = entityId('project', slugify(project.name));
1509
+
1510
+ if (!graph.getEntity(projectId)) {
1511
+ graph.createEntity({
1512
+ type: 'project',
1513
+ name: project.name,
1514
+ metadata: {
1515
+ ...(project.repoUrl && { repo_url: project.repoUrl }),
1516
+ },
1517
+ });
1518
+ }
1519
+
1520
+ // Link to team
1521
+ if (project.team) {
1522
+ const teamId = entityId('team', slugify(project.team));
1523
+ graph.createEdge({ source_id: teamId, target_id: projectId, relation: 'owns' });
1524
+ }
1525
+ }
1526
+ }
1527
+ }
1528
+ ```
1529
+
1530
+ - [ ] **Step 4: Run test to verify it passes**
1531
+
1532
+ Run: `npx vitest run tests/lib/cortex/graph/auto-populate.test.ts`
1533
+ Expected: PASS (all 5 tests)
1534
+
1535
+ - [ ] **Step 5: Commit**
1536
+
1537
+ ```bash
1538
+ git add src/lib/cortex/graph/auto-populate.ts tests/lib/cortex/graph/auto-populate.test.ts
1539
+ git commit -m "feat(cortex): add auto-populate for seeding entity graph from org data"
1540
+ ```
1541
+
1542
+ ---
1543
+
1544
+ ## Chunk 4: API Routes and CortexInstance Integration
1545
+
1546
+ ### Task 9: Graph API — entity endpoints
1547
+
1548
+ **Files:**
1549
+ - Create: `src/app/api/cortex/graph/entities/route.ts`
1550
+ - Create: `src/app/api/cortex/graph/entities/[id]/route.ts`
1551
+
1552
+ - [ ] **Step 1: Create entity list/create endpoint**
1553
+
1554
+ ```typescript
1555
+ // src/app/api/cortex/graph/entities/route.ts
1556
+ import { NextResponse } from 'next/server';
1557
+ import type { NextRequest } from 'next/server';
1558
+ import { getAuthUser, withUser } from '@/lib/auth';
1559
+ import { getCortex, isCortexAvailable } from '@/lib/cortex';
1560
+
1561
+ export async function GET(request: NextRequest) {
1562
+ const user = getAuthUser(request);
1563
+ return withUser(user, async () => {
1564
+ if (!isCortexAvailable()) {
1565
+ return NextResponse.json({ error: 'Cortex unavailable' }, { status: 403 });
1566
+ }
1567
+ const cortex = await getCortex();
1568
+ if (!cortex?.graph) return NextResponse.json({ entities: [] });
1569
+
1570
+ const url = new URL(request.url);
1571
+ const type = url.searchParams.get('type') || undefined;
1572
+ const limit = parseInt(url.searchParams.get('limit') || '100', 10);
1573
+
1574
+ const entities = cortex.graph.listEntities({
1575
+ type: type as any,
1576
+ limit,
1577
+ });
1578
+
1579
+ return NextResponse.json({ entities });
1580
+ });
1581
+ }
1582
+
1583
+ export async function POST(request: NextRequest) {
1584
+ const user = getAuthUser(request);
1585
+ return withUser(user, async () => {
1586
+ if (!isCortexAvailable()) {
1587
+ return NextResponse.json({ error: 'Cortex unavailable' }, { status: 403 });
1588
+ }
1589
+ const cortex = await getCortex();
1590
+ if (!cortex?.graph) {
1591
+ return NextResponse.json({ error: 'Graph not initialized' }, { status: 500 });
1592
+ }
1593
+
1594
+ const body = await request.json();
1595
+ const { type, name, id, metadata } = body;
1596
+
1597
+ if (!type || !name) {
1598
+ return NextResponse.json({ error: 'type and name are required' }, { status: 400 });
1599
+ }
1600
+
1601
+ const { isValidEntityType } = await import('@/lib/cortex/graph/types');
1602
+ if (!isValidEntityType(type)) {
1603
+ return NextResponse.json({ error: `Invalid entity type: ${type}` }, { status: 400 });
1604
+ }
1605
+
1606
+ try {
1607
+ const entity = cortex.graph.createEntity({ id, type, name, metadata });
1608
+ return NextResponse.json({ entity }, { status: 201 });
1609
+ } catch (err: any) {
1610
+ return NextResponse.json({ error: err.message }, { status: 409 });
1611
+ }
1612
+ });
1613
+ }
1614
+ ```
1615
+
1616
+ - [ ] **Step 2: Create single-entity endpoint**
1617
+
1618
+ ```typescript
1619
+ // src/app/api/cortex/graph/entities/[id]/route.ts
1620
+ import { NextResponse } from 'next/server';
1621
+ import type { NextRequest } from 'next/server';
1622
+ import { getAuthUser, withUser } from '@/lib/auth';
1623
+ import { getCortex, isCortexAvailable } from '@/lib/cortex';
1624
+
1625
+ export async function GET(
1626
+ request: NextRequest,
1627
+ { params }: { params: Promise<{ id: string }> },
1628
+ ) {
1629
+ const { id } = await params;
1630
+ const user = getAuthUser(request);
1631
+ return withUser(user, async () => {
1632
+ if (!isCortexAvailable()) {
1633
+ return NextResponse.json({ error: 'Cortex unavailable' }, { status: 403 });
1634
+ }
1635
+ const cortex = await getCortex();
1636
+ if (!cortex?.graph) return NextResponse.json({ error: 'Graph not initialized' }, { status: 500 });
1637
+
1638
+ const entity = cortex.graph.getEntity(id);
1639
+ if (!entity) return NextResponse.json({ error: 'Not found' }, { status: 404 });
1640
+ return NextResponse.json({ entity });
1641
+ });
1642
+ }
1643
+
1644
+ export async function PATCH(
1645
+ request: NextRequest,
1646
+ { params }: { params: Promise<{ id: string }> },
1647
+ ) {
1648
+ const { id } = await params;
1649
+ const user = getAuthUser(request);
1650
+ return withUser(user, async () => {
1651
+ if (!isCortexAvailable()) {
1652
+ return NextResponse.json({ error: 'Cortex unavailable' }, { status: 403 });
1653
+ }
1654
+ const cortex = await getCortex();
1655
+ if (!cortex?.graph) return NextResponse.json({ error: 'Graph not initialized' }, { status: 500 });
1656
+
1657
+ const body = await request.json();
1658
+ const updated = cortex.graph.updateEntity(id, body);
1659
+ if (!updated) return NextResponse.json({ error: 'Not found' }, { status: 404 });
1660
+ return NextResponse.json({ entity: updated });
1661
+ });
1662
+ }
1663
+
1664
+ export async function DELETE(
1665
+ request: NextRequest,
1666
+ { params }: { params: Promise<{ id: string }> },
1667
+ ) {
1668
+ const { id } = await params;
1669
+ const user = getAuthUser(request);
1670
+ return withUser(user, async () => {
1671
+ if (!isCortexAvailable()) {
1672
+ return NextResponse.json({ error: 'Cortex unavailable' }, { status: 403 });
1673
+ }
1674
+ const cortex = await getCortex();
1675
+ if (!cortex?.graph) return NextResponse.json({ error: 'Graph not initialized' }, { status: 500 });
1676
+
1677
+ cortex.graph.deleteEntity(id);
1678
+ return NextResponse.json({ deleted: true });
1679
+ });
1680
+ }
1681
+
1682
+ - [ ] **Step 3: Commit**
1683
+
1684
+ ```bash
1685
+ git add src/app/api/cortex/graph/
1686
+ git commit -m "feat(cortex): add API routes for entity CRUD"
1687
+ ```
1688
+
1689
+ ---
1690
+
1691
+ ### Task 10: Graph API — edge endpoints
1692
+
1693
+ **Files:**
1694
+ - Create: `src/app/api/cortex/graph/edges/route.ts`
1695
+
1696
+ - [ ] **Step 1: Create edge list/create endpoint**
1697
+
1698
+ ```typescript
1699
+ // src/app/api/cortex/graph/edges/route.ts
1700
+ import { NextResponse } from 'next/server';
1701
+ import type { NextRequest } from 'next/server';
1702
+ import { getAuthUser, withUser } from '@/lib/auth';
1703
+ import { getCortex, isCortexAvailable } from '@/lib/cortex';
1704
+
1705
+ export async function GET(request: NextRequest) {
1706
+ const user = getAuthUser(request);
1707
+ return withUser(user, async () => {
1708
+ if (!isCortexAvailable()) {
1709
+ return NextResponse.json({ error: 'Cortex unavailable' }, { status: 403 });
1710
+ }
1711
+ const cortex = await getCortex();
1712
+ if (!cortex?.graph) return NextResponse.json({ edges: [] });
1713
+
1714
+ const url = new URL(request.url);
1715
+ const from = url.searchParams.get('from');
1716
+ const to = url.searchParams.get('to');
1717
+ const relation = url.searchParams.get('relation') || undefined;
1718
+
1719
+ let edges;
1720
+ if (from) {
1721
+ edges = cortex.graph.getEdgesFrom(from, relation as any);
1722
+ } else if (to) {
1723
+ edges = cortex.graph.getEdgesTo(to, relation as any);
1724
+ } else {
1725
+ return NextResponse.json({ error: 'Provide from or to parameter' }, { status: 400 });
1726
+ }
1727
+
1728
+ return NextResponse.json({ edges });
1729
+ });
1730
+ }
1731
+
1732
+ export async function POST(request: NextRequest) {
1733
+ const user = getAuthUser(request);
1734
+ return withUser(user, async () => {
1735
+ if (!isCortexAvailable()) {
1736
+ return NextResponse.json({ error: 'Cortex unavailable' }, { status: 403 });
1737
+ }
1738
+ const cortex = await getCortex();
1739
+ if (!cortex?.graph) {
1740
+ return NextResponse.json({ error: 'Graph not initialized' }, { status: 500 });
1741
+ }
1742
+
1743
+ const body = await request.json();
1744
+ const { source_id, target_id, relation, weight, metadata } = body;
1745
+
1746
+ if (!source_id || !target_id || !relation) {
1747
+ return NextResponse.json(
1748
+ { error: 'source_id, target_id, and relation are required' },
1749
+ { status: 400 },
1750
+ );
1751
+ }
1752
+
1753
+ const edge = cortex.graph.createEdge({ source_id, target_id, relation, weight, metadata });
1754
+ return NextResponse.json({ edge }, { status: 201 });
1755
+ });
1756
+ }
1757
+
1758
+ export async function DELETE(request: NextRequest) {
1759
+ const user = getAuthUser(request);
1760
+ return withUser(user, async () => {
1761
+ if (!isCortexAvailable()) {
1762
+ return NextResponse.json({ error: 'Cortex unavailable' }, { status: 403 });
1763
+ }
1764
+ const cortex = await getCortex();
1765
+ if (!cortex?.graph) return NextResponse.json({ error: 'Graph not initialized' }, { status: 500 });
1766
+
1767
+ const url = new URL(request.url);
1768
+ const source_id = url.searchParams.get('source_id');
1769
+ const target_id = url.searchParams.get('target_id');
1770
+ const relation = url.searchParams.get('relation');
1771
+
1772
+ if (!source_id || !target_id || !relation) {
1773
+ return NextResponse.json(
1774
+ { error: 'source_id, target_id, and relation query params are required' },
1775
+ { status: 400 },
1776
+ );
1777
+ }
1778
+
1779
+ cortex.graph.deleteEdge(source_id, target_id, relation as any);
1780
+ return NextResponse.json({ deleted: true });
1781
+ });
1782
+ }
1783
+ ```
1784
+
1785
+ - [ ] **Step 2: Commit**
1786
+
1787
+ ```bash
1788
+ git add src/app/api/cortex/graph/edges/route.ts
1789
+ git commit -m "feat(cortex): add API routes for edge CRUD"
1790
+ ```
1791
+
1792
+ ---
1793
+
1794
+ ### Task 11: Integrate EntityGraph into CortexInstance
1795
+
1796
+ **Files:**
1797
+ - Modify: `src/lib/cortex/index.ts`
1798
+
1799
+ - [ ] **Step 1: Read the current index.ts**
1800
+
1801
+ Read `src/lib/cortex/index.ts` to understand the current CortexInstance pattern and initialization flow.
1802
+
1803
+ - [ ] **Step 2: Add graph to CortexInstance**
1804
+
1805
+ Add the `graph` property and initialization:
1806
+
1807
+ 1. Import EntityGraph:
1808
+ ```typescript
1809
+ import { EntityGraph } from './graph/entity-graph';
1810
+ ```
1811
+
1812
+ 2. Add to CortexInstance interface:
1813
+ ```typescript
1814
+ export interface CortexInstance {
1815
+ config: CortexConfig;
1816
+ store: CortexStore;
1817
+ search: CortexSearch;
1818
+ pipeline: IngestionPipeline;
1819
+ embedding: EmbeddingProvider;
1820
+ graph: EntityGraph; // NEW
1821
+ sync?: FederationSync;
1822
+ distillQueue?: DistillationQueue;
1823
+ distillScheduler?: DistillationScheduler;
1824
+ }
1825
+ ```
1826
+
1827
+ 3. In `getCortex()`, after store initialization and before `_instance` assignment:
1828
+ ```typescript
1829
+ // Initialize entity graph (SQLite)
1830
+ const graphPath = path.join(cortexDir, 'graph.db');
1831
+ const graph = new EntityGraph(graphPath);
1832
+ ```
1833
+
1834
+ 4. Add `graph` to the instance object.
1835
+
1836
+ 5. In `resetCortex()`, add cleanup:
1837
+ ```typescript
1838
+ if (_instance) {
1839
+ _instance.graph.close();
1840
+ // ... existing cleanup
1841
+ }
1842
+ ```
1843
+
1844
+ - [ ] **Step 3: Run existing tests to verify no regressions**
1845
+
1846
+ Run: `npx vitest run tests/lib/cortex/`
1847
+ Expected: All existing tests pass. May have 2 pre-existing failures in config.test.ts and chunker.test.ts (known issues, unrelated).
1848
+
1849
+ - [ ] **Step 4: Commit**
1850
+
1851
+ ```bash
1852
+ git add src/lib/cortex/index.ts
1853
+ git commit -m "feat(cortex): integrate EntityGraph into CortexInstance lifecycle"
1854
+ ```
1855
+
1856
+ ---
1857
+
1858
+ ### Task 12: Module index and barrel export
1859
+
1860
+ **Files:**
1861
+ - Create: `src/lib/cortex/graph/index.ts`
1862
+
1863
+ - [ ] **Step 1: Create barrel export**
1864
+
1865
+ ```typescript
1866
+ // src/lib/cortex/graph/index.ts
1867
+ export { EntityGraph } from './entity-graph';
1868
+ export { EntityResolver } from './resolver';
1869
+ export { autoPopulate } from './auto-populate';
1870
+ export type { AutoPopulateConfig } from './auto-populate';
1871
+ export { initGraphSchema } from './schema';
1872
+ export {
1873
+ entityId,
1874
+ slugify,
1875
+ isValidEntityType,
1876
+ isValidEdgeRelation,
1877
+ ENTITY_TYPES,
1878
+ EDGE_RELATIONS,
1879
+ } from './types';
1880
+ export type {
1881
+ Entity,
1882
+ Edge,
1883
+ EntityType,
1884
+ EdgeRelation,
1885
+ EntityAlias,
1886
+ AccessGrant,
1887
+ } from './types';
1888
+ ```
1889
+
1890
+ - [ ] **Step 2: Run full test suite**
1891
+
1892
+ Run: `npx vitest run tests/lib/cortex/graph/`
1893
+ Expected: PASS — all graph tests pass (30+ tests across 4 files)
1894
+
1895
+ - [ ] **Step 3: Commit**
1896
+
1897
+ ```bash
1898
+ git add src/lib/cortex/graph/index.ts
1899
+ git commit -m "feat(cortex): add graph module barrel export"
1900
+ ```
1901
+
1902
+ ---
1903
+
1904
+ ## Summary
1905
+
1906
+ | Task | Component | Tests | Status |
1907
+ |------|-----------|-------|--------|
1908
+ | 1 | Graph types | — | |
1909
+ | 2 | SQLite schema | 2 | |
1910
+ | 3 | Entity CRUD | 7 | |
1911
+ | 4 | Edge CRUD | 7 | |
1912
+ | 5 | Alias management | 5 | |
1913
+ | 6 | BFS traversal | 10 | |
1914
+ | 7 | Entity resolver | 5 | |
1915
+ | 8 | Auto-populate | 5 | |
1916
+ | 9 | Entity API routes | — | |
1917
+ | 10 | Edge API routes | — | |
1918
+ | 11 | CortexInstance integration | regression | |
1919
+ | 12 | Barrel export | regression | |
1920
+
1921
+ **Total: 12 tasks, 41 tests, 4 chunks**
1922
+
1923
+ After this plan is complete, the entity graph foundation is in place for Pillar 2 (Knowledge Unit Evolution) to build on — linking knowledge units to graph entities and replacing the flat `layer` field with graph-aware `scope`.