@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.
- package/.next/standalone/.claude/settings.local.json +55 -0
- package/.next/standalone/.claude/spaces-env.json +1 -0
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +2 -1
- package/.next/standalone/.next/build-manifest.json +5 -5
- package/.next/standalone/.next/prerender-manifest.json +27 -3
- package/.next/standalone/.next/required-server-files.json +19 -19
- package/.next/standalone/.next/routes-manifest.json +6 -0
- package/.next/standalone/.next/server/app/(desktop)/admin/analytics/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/(desktop)/admin/analytics/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/admin/users/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/(desktop)/admin/users/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/analytics/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/(desktop)/analytics/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/cortex/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/(desktop)/cortex/page/react-loadable-manifest.json +3 -3
- package/.next/standalone/.next/server/app/(desktop)/cortex/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/(desktop)/cortex/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/network/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/(desktop)/network/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/(desktop)/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/projects/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/(desktop)/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/sessions/[id]/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/(desktop)/sessions/[id]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/sessions/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/(desktop)/sessions/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/settings/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/(desktop)/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/terminal/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/(desktop)/terminal/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/(desktop)/terminal/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/terminal/pane/[id]/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/(desktop)/terminal/pane/[id]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/(desktop)/terminal/pane/[id]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/terminal/remote/[nodeId]/[workspaceId]/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/(desktop)/terminal/remote/[nodeId]/[workspaceId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/(desktop)/terminal/remote/[nodeId]/[workspaceId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/workspaces/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/(desktop)/workspaces/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/_global-error.html +2 -2
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/admin/analytics.html +1 -1
- package/.next/standalone/.next/server/app/admin/analytics.rsc +7 -6
- package/.next/standalone/.next/server/app/admin/analytics.segments/!KGRlc2t0b3Ap/admin/analytics/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/admin/analytics.segments/!KGRlc2t0b3Ap/admin/analytics.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/admin/analytics.segments/!KGRlc2t0b3Ap/admin.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/admin/analytics.segments/!KGRlc2t0b3Ap.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/admin/analytics.segments/_full.segment.rsc +7 -6
- package/.next/standalone/.next/server/app/admin/analytics.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/admin/analytics.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/admin/analytics.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/admin/users.html +1 -1
- package/.next/standalone/.next/server/app/admin/users.rsc +2 -2
- package/.next/standalone/.next/server/app/admin/users.segments/!KGRlc2t0b3Ap/admin/users/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/admin/users.segments/!KGRlc2t0b3Ap/admin/users.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/admin/users.segments/!KGRlc2t0b3Ap/admin.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/admin/users.segments/!KGRlc2t0b3Ap.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/admin/users.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/admin/users.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/admin/users.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/admin/users.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/analytics.html +1 -1
- package/.next/standalone/.next/server/app/analytics.rsc +3 -3
- package/.next/standalone/.next/server/app/analytics.segments/!KGRlc2t0b3Ap/analytics/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/analytics.segments/!KGRlc2t0b3Ap/analytics.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/analytics.segments/!KGRlc2t0b3Ap.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/analytics.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/analytics.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/analytics.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/analytics.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/analytics/overview/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bulk/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/config/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/context/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/curation/assess/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/curation/publish/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/curation/refine/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/curation/review/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/curation/seed/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/export/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/federation/pending/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/federation/resolve/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/federation/search/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/federation/teach/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/graph/edges/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/graph/entities/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/graph/entities/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/graph/populate/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/import/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/ingest/bootstrap/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/ingest/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/knowledge/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/knowledge/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/lobes/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/lobes/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/lobes/share/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/marketplace/browse/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/marketplace/preview/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/mcp/call/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/mcp/tools/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/search/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/settings/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/timeline/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/usage/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/workspace/[id]/context/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/events/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/folders/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/handshake/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/projects/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/search/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/sessions/[id]/messages/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/sessions/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/sessions/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/workspaces/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/workspaces/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/panes/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/panes/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/search/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/sessions/[id]/chat/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/sessions/[id]/messages/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/sync/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/tags/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/tier/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/workspaces/[id]/context/[key]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/workspaces/[id]/context/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/workspaces/[id]/messages/[msgId]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/workspaces/[id]/messages/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/workspaces/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/workspaces/[id]/sessions/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/workspaces/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/cortex.html +1 -1
- package/.next/standalone/.next/server/app/cortex.rsc +3 -3
- package/.next/standalone/.next/server/app/cortex.segments/!KGRlc2t0b3Ap/cortex/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/cortex.segments/!KGRlc2t0b3Ap/cortex.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/cortex.segments/!KGRlc2t0b3Ap.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/cortex.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/cortex.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/cortex.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/cortex.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/login/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/login.html +1 -1
- package/.next/standalone/.next/server/app/login.rsc +2 -2
- package/.next/standalone/.next/server/app/login.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/login.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/login.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/login.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/login.segments/login/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/login.segments/login.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/m/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/m/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/m/projects/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/m/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/m/projects.html +1 -1
- package/.next/standalone/.next/server/app/m/projects.rsc +2 -2
- package/.next/standalone/.next/server/app/m/projects.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/projects.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/m/projects.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/projects.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/projects.segments/m/projects/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/m/projects.segments/m/projects.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/m/projects.segments/m.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/m/sessions/[id]/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/m/sessions/[id]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/m/sessions/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/m/sessions/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/m/sessions.html +1 -1
- package/.next/standalone/.next/server/app/m/sessions.rsc +2 -2
- package/.next/standalone/.next/server/app/m/sessions.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/sessions.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/m/sessions.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/sessions.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/sessions.segments/m/sessions/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/m/sessions.segments/m/sessions.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/m/sessions.segments/m.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/m/settings/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/m/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/m/settings.html +1 -1
- package/.next/standalone/.next/server/app/m/settings.rsc +2 -2
- package/.next/standalone/.next/server/app/m/settings.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/settings.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/m/settings.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/settings.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/settings.segments/m/settings/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/m/settings.segments/m/settings.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/m/settings.segments/m.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/m/terminal/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/m/terminal/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/m/terminal.html +1 -1
- package/.next/standalone/.next/server/app/m/terminal.rsc +3 -3
- package/.next/standalone/.next/server/app/m/terminal.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/m/terminal.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/m/terminal.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/terminal.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/terminal.segments/m/terminal/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/terminal.segments/m/terminal.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/m/terminal.segments/m.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/m.html +1 -1
- package/.next/standalone/.next/server/app/m.rsc +2 -2
- package/.next/standalone/.next/server/app/m.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/m.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m.segments/m/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/m.segments/m.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/network.html +1 -1
- package/.next/standalone/.next/server/app/network.rsc +2 -2
- package/.next/standalone/.next/server/app/network.segments/!KGRlc2t0b3Ap/network/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/network.segments/!KGRlc2t0b3Ap/network.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/network.segments/!KGRlc2t0b3Ap.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/network.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/network.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/network.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/network.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/projects.html +1 -1
- package/.next/standalone/.next/server/app/projects.rsc +2 -2
- package/.next/standalone/.next/server/app/projects.segments/!KGRlc2t0b3Ap/projects/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/projects.segments/!KGRlc2t0b3Ap/projects.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/projects.segments/!KGRlc2t0b3Ap.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/projects.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/projects.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/projects.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/projects.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/sessions.html +1 -1
- package/.next/standalone/.next/server/app/sessions.rsc +2 -2
- package/.next/standalone/.next/server/app/sessions.segments/!KGRlc2t0b3Ap/sessions/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/sessions.segments/!KGRlc2t0b3Ap/sessions.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/sessions.segments/!KGRlc2t0b3Ap.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/sessions.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/sessions.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/sessions.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/sessions.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/settings.html +1 -1
- package/.next/standalone/.next/server/app/settings.rsc +2 -2
- package/.next/standalone/.next/server/app/settings.segments/!KGRlc2t0b3Ap/settings/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/settings.segments/!KGRlc2t0b3Ap/settings.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/settings.segments/!KGRlc2t0b3Ap.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/settings.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/settings.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/terminal.html +1 -1
- package/.next/standalone/.next/server/app/terminal.rsc +3 -3
- package/.next/standalone/.next/server/app/terminal.segments/!KGRlc2t0b3Ap/terminal/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/terminal.segments/!KGRlc2t0b3Ap/terminal.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/terminal.segments/!KGRlc2t0b3Ap.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/terminal.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/terminal.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/terminal.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/terminal.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/vr/page/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/vr/page/build-manifest.json +18 -0
- package/.next/standalone/.next/server/app/vr/page/next-font-manifest.json +11 -0
- package/.next/standalone/.next/server/app/vr/page/react-loadable-manifest.json +11 -0
- package/.next/standalone/.next/server/app/vr/page/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/vr/page.js +17 -0
- package/.next/standalone/.next/server/app/vr/page.js.map +5 -0
- package/.next/standalone/.next/server/app/vr/page.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/vr/page_client-reference-manifest.js +2 -0
- package/.next/standalone/.next/server/app/vr.html +1 -0
- package/.next/standalone/.next/server/app/vr.meta +15 -0
- package/.next/standalone/.next/server/app/vr.rsc +21 -0
- package/.next/standalone/.next/server/app/vr.segments/_full.segment.rsc +21 -0
- package/.next/standalone/.next/server/app/vr.segments/_head.segment.rsc +6 -0
- package/.next/standalone/.next/server/app/vr.segments/_index.segment.rsc +6 -0
- package/.next/standalone/.next/server/app/vr.segments/_tree.segment.rsc +4 -0
- package/.next/standalone/.next/server/app/vr.segments/vr/__PAGE__.segment.rsc +9 -0
- package/.next/standalone/.next/server/app/vr.segments/vr.segment.rsc +4 -0
- package/.next/standalone/.next/server/app/workspaces.html +1 -1
- package/.next/standalone/.next/server/app/workspaces.rsc +2 -2
- package/.next/standalone/.next/server/app/workspaces.segments/!KGRlc2t0b3Ap/workspaces/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/workspaces.segments/!KGRlc2t0b3Ap/workspaces.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/workspaces.segments/!KGRlc2t0b3Ap.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/workspaces.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/workspaces.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/workspaces.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/workspaces.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app-paths-manifest.json +2 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0041efe4._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__00bf0ace._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08a68343._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0add852f._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0c113ed0._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e1a27e0._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e71d908._.js +3 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e9142f3._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__10e47926._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1194f2c1._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1665dc78._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__175cbabf._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__19c2d094._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1adae357._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1d359752._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1e8fabeb._.js +3 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1f8deca0._.js +8 -8
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__253fdda1._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__28e6434f._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__2a386564._.js +3 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__2acbd703._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__2acefabb._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__2c20fb38._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__309132cd._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__33fec964._.js +3 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__3786d8ae._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__3ae92407._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__3beda9fe._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__3e3f25a1._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__4619e9bd._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__4a051043._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__50208a5f._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__508002e4._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__5086c373._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__5913e097._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__5b5f68d2._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__5c1f2459._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__5ec8c977._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__5f8c694a._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__63cebc6c._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__64d30d4d._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__6c54fc2e._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__6dc1fb7e._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__6e568102._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__6faa04c0._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__727d05f1._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__74a34dc3._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__75d12b32._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__7e7250a4._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__8309e0a4._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__86cc0e2b._.js +6 -6
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__8915603e._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__89c2565a._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__8d178ad9._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__93ee06f3._.js +3 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__9e4c154a._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__a1fbc199._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__a9d2e1d3._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__ae53d343._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__b3a04cef._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__b4270b77._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__b6b6ce60._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__b9545dd9._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__c200e21a._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__c3c74ca4._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__c88b63f7._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__cba5f007._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__cbf4ceb0._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__cefdba2f._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__cf9e82bb._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__d15515e3._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__d2897392._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__d3b2d856._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__d73273ca._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__d8417eb6._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__db4726bc._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__dc2a55de._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__dc6e2e5f._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__e0d4690b._.js +3 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__e3ecfd17._.js +3 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__e678dd53._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__e9223f55._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__ea630076._.js +3 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__eb8acb65._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__f26ca49d._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__f33e1101._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__f3a4c668._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__f515f865._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__fceb5d60._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__fed41403._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__ff2e98c2._.js +2 -2
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_339169c8.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_97dac613.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0d8d81ca._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1425c64f._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1d2ce8f1._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__31137509._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__3633a587._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__3c79441b._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__4ca0f26b._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__5b90d3ad._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__62a0b363._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__66aca5d4._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__68205a46._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__69fd2efa._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__85dcf0f7._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__8c53a5da._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__aecb1873._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__b02cd143._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__b9bcde11._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__cac90169._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__d25de2f0._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__e2f86be8._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__ee626b5b._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__f39a9e98._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__f3c566cd._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__f76aa221._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_149d7fd4._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_2230ad2d._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_2e0dd6a7._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_3cd2355c._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_3d206597._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/_47cc9af0._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_5cf334fd._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_7082788b._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_7154d8ae._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_75bb1b9a._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_aeeff784._.js → _81abf587._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_8acf81e2._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_8c36feb8._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_91e9bb86._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_ac4c1838._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_ad8515fc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_b1f49e81._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_c0fe7614._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_d4825f5a._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_da10a9f4._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_db0abd0a._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_db2fec84._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_dee5d4a1._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_ef482c0c._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_efe43d2f._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_f4a4e116._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_f4d525d2._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_f4e57187._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_vr_page_actions_3fb70d92.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_32f9d62f._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_02f39477.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_eedfc1fd._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_40fa36ce._.js +7 -0
- package/.next/standalone/.next/server/chunks/ssr/src_app_(desktop)_cortex_page_tsx_0f33d8b3._.js +3 -0
- package/.next/standalone/.next/server/edge/chunks/[root-of-the-server]__32a0045c._.js +1 -1
- package/.next/standalone/.next/server/edge/chunks/_d73df637._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/middleware-manifest.json +5 -5
- package/.next/standalone/.next/server/next-font-manifest.js +1 -1
- package/.next/standalone/.next/server/next-font-manifest.json +4 -0
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +2 -2
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/045c83caa4d15373.js +1 -0
- package/.next/standalone/.next/static/chunks/07ea09e6024a523b.js +1 -0
- package/.next/standalone/.next/static/chunks/{aae9e0fa485bd835.js → 158b52b84e647ac1.js} +2 -2
- package/.next/standalone/.next/static/chunks/232d8aae4fefab70.js +1 -0
- package/.next/standalone/.next/static/chunks/2ad22562bb37ecad.js +1011 -0
- package/.next/standalone/.next/static/chunks/{a4e5c700421eaa46.js → 412140a02893327a.js} +1 -1
- package/.next/standalone/.next/static/chunks/481cc11ae80b08b1.js +1 -0
- package/.next/standalone/.next/static/chunks/5325351ef49cb65f.js +1 -0
- package/.next/standalone/.next/static/chunks/559735e598ca3cbb.js +1 -0
- package/.next/standalone/.next/static/chunks/59c63d5af5cf3daf.js +1 -0
- package/.next/standalone/.next/static/chunks/5d5d7b0095dd52ae.js +1 -0
- package/.next/standalone/.next/static/chunks/69606d281c39f9b2.js +1 -0
- package/.next/standalone/.next/static/chunks/6ae575967d091df4.js +1 -0
- package/.next/standalone/.next/static/chunks/7f8455bb855a6c84.js +1 -0
- package/.next/standalone/.next/static/chunks/84fe8d44deeeb74f.js +757 -0
- package/.next/standalone/.next/static/chunks/898f380eba90427a.js +1 -0
- package/.next/standalone/.next/static/chunks/95339e55722bb4ca.js +5 -0
- package/.next/standalone/.next/static/chunks/9cd594813c539df9.js +1 -0
- package/.next/standalone/.next/static/chunks/{7424664c6ffa94bd.js → 9cfa0291d55d8d2a.js} +1 -1
- package/.next/standalone/.next/static/chunks/ad1423eed05d129b.js +1 -0
- package/.next/standalone/.next/static/chunks/ae7b146884c67d2a.js +1 -0
- package/.next/standalone/.next/static/chunks/b84072d72aa86417.js +1 -0
- package/.next/standalone/.next/static/chunks/c1a95aebf6725f64.css +3 -0
- package/.next/standalone/.next/static/chunks/c515eb77d9410aa0.js +5 -0
- package/.next/standalone/.next/static/chunks/{9899cf4c2bdbe61d.js → d9ae203a7f123546.js} +2 -2
- package/.next/standalone/.next/static/chunks/e116953dc83d4eec.js +1 -0
- package/.next/standalone/.next/static/chunks/fdc09bd135846960.js +1 -0
- package/.next/standalone/.next/static/chunks/ff0196911449e745.js +1 -0
- package/.next/standalone/.next/static/chunks/{turbopack-4c21186b79fb4c10.js → turbopack-e1a0994ed4af988c.js} +1 -1
- package/.next/standalone/.spaces/cortex-context.md +70 -0
- package/.next/standalone/bin/cortex-hook.sh +62 -62
- package/.next/standalone/bin/cortex-mcp.js +60 -60
- package/.next/standalone/docs/superpowers/plans/2026-03-13-cortex-wiring.md +1387 -1387
- package/.next/standalone/docs/superpowers/plans/2026-03-14-cortex-v2-entity-graph.md +1923 -1923
- package/.next/standalone/docs/superpowers/plans/2026-03-14-cortex-v2-knowledge-evolution.md +1113 -1113
- package/.next/standalone/docs/superpowers/plans/2026-03-15-cortex-v2-boundary-engine.md +853 -853
- package/.next/standalone/docs/superpowers/plans/2026-03-15-cortex-v2-context-engine.md +1274 -1274
- package/.next/standalone/docs/superpowers/plans/2026-03-15-cortex-v2-signal-ingestion.md +933 -933
- package/.next/standalone/docs/superpowers/plans/2026-03-16-cortex-lobes.md +1080 -1080
- package/.next/standalone/docs/superpowers/plans/2026-03-16-cortex-v2-gravity-system.md +768 -768
- package/.next/standalone/docs/superpowers/plans/2026-03-16-cortex-v2-ui.md +1108 -1108
- package/.next/standalone/docs/superpowers/plans/2026-03-18-cortex-ui-integration.md +1846 -1846
- package/.next/standalone/docs/superpowers/specs/2026-03-13-cortex-wiring-design.md +268 -268
- package/.next/standalone/docs/superpowers/specs/2026-03-14-cortex-v2-design.md +623 -623
- package/.next/standalone/docs/superpowers/specs/2026-03-16-cortex-lobes-design.md +263 -263
- package/.next/standalone/docs/superpowers/specs/2026-03-16-cortex-v2-ui-design.md +240 -240
- package/.next/standalone/docs/superpowers/specs/2026-03-18-cortex-ui-integration-design.md +341 -341
- package/.next/standalone/node_modules/@img/sharp-win32-x64/lib/sharp-win32-x64.node +0 -0
- package/.next/standalone/node_modules/@img/{sharp-linux-x64 → sharp-win32-x64}/package.json +39 -46
- package/.next/standalone/package.json +104 -102
- package/.next/standalone/server.js +1 -1
- package/.next/standalone/src/app/(desktop)/cortex/page.tsx +78 -78
- package/.next/standalone/src/app/api/cortex/context/route.ts +78 -78
- package/.next/standalone/src/app/api/cortex/curation/assess/route.ts +27 -27
- package/.next/standalone/src/app/api/cortex/curation/publish/route.ts +23 -23
- package/.next/standalone/src/app/api/cortex/curation/refine/route.ts +23 -23
- package/.next/standalone/src/app/api/cortex/curation/review/route.ts +29 -29
- package/.next/standalone/src/app/api/cortex/curation/seed/route.ts +23 -23
- package/.next/standalone/src/app/api/cortex/export/route.ts +40 -40
- package/.next/standalone/src/app/api/cortex/federation/pending/route.ts +20 -20
- package/.next/standalone/src/app/api/cortex/federation/resolve/route.ts +43 -43
- package/.next/standalone/src/app/api/cortex/federation/search/route.ts +35 -35
- package/.next/standalone/src/app/api/cortex/federation/teach/route.ts +76 -76
- package/.next/standalone/src/app/api/cortex/graph/edges/route.ts +112 -112
- package/.next/standalone/src/app/api/cortex/graph/entities/[id]/route.ts +73 -73
- package/.next/standalone/src/app/api/cortex/graph/entities/route.ts +75 -75
- package/.next/standalone/src/app/api/cortex/graph/populate/route.ts +203 -203
- package/.next/standalone/src/app/api/cortex/import/route.ts +75 -75
- package/.next/standalone/src/app/api/cortex/import/status/route.ts +15 -15
- package/.next/standalone/src/app/api/cortex/ingest/bootstrap/route.ts +29 -29
- package/.next/standalone/src/app/api/cortex/ingest/status/route.ts +15 -15
- package/.next/standalone/src/app/api/cortex/knowledge/[id]/route.ts +91 -91
- package/.next/standalone/src/app/api/cortex/knowledge/route.ts +93 -93
- package/.next/standalone/src/app/api/cortex/lobes/[id]/route.ts +67 -67
- package/.next/standalone/src/app/api/cortex/lobes/route.ts +22 -22
- package/.next/standalone/src/app/api/cortex/lobes/share/route.ts +80 -80
- package/.next/standalone/src/app/api/cortex/marketplace/browse/route.ts +43 -43
- package/.next/standalone/src/app/api/cortex/marketplace/preview/route.ts +46 -46
- package/.next/standalone/src/app/api/cortex/mcp/call/route.ts +11 -11
- package/.next/standalone/src/app/api/cortex/mcp/tools/route.ts +6 -6
- package/.next/standalone/src/app/api/cortex/search/route.ts +43 -43
- package/.next/standalone/src/app/api/cortex/settings/route.ts +33 -33
- package/.next/standalone/src/app/api/cortex/status/route.ts +169 -169
- package/.next/standalone/src/app/api/cortex/timeline/route.ts +42 -42
- package/.next/standalone/src/app/api/cortex/usage/route.ts +31 -31
- package/.next/standalone/src/app/api/cortex/workspace/[id]/context/route.ts +41 -41
- package/.next/standalone/src/components/cortex/constants.ts +29 -29
- package/.next/standalone/src/components/cortex/cortex-dashboard.tsx +304 -304
- package/.next/standalone/src/components/cortex/cortex-indicator.tsx +44 -44
- package/.next/standalone/src/components/cortex/cortex-panel.tsx +140 -140
- package/.next/standalone/src/components/cortex/cortex-settings.tsx +221 -221
- package/.next/standalone/src/components/cortex/curation-tab.tsx +810 -810
- package/.next/standalone/src/components/cortex/entity-detail.tsx +101 -101
- package/.next/standalone/src/components/cortex/entity-graph.tsx +382 -382
- package/.next/standalone/src/components/cortex/import-dialog.tsx +212 -212
- package/.next/standalone/src/components/cortex/injection-badge.tsx +72 -72
- package/.next/standalone/src/components/cortex/knowledge-card.tsx +109 -109
- package/.next/standalone/src/components/cortex/knowledge-tab.tsx +158 -158
- package/.next/standalone/src/components/cortex/lobe-settings.tsx +215 -215
- package/.next/standalone/src/components/cortex/marketplace-card.tsx +126 -126
- package/.next/standalone/src/components/cortex/marketplace-tab.tsx +113 -113
- package/.next/standalone/src/lib/cortex/config.ts +40 -40
- package/.next/standalone/src/lib/cortex/debug.ts +10 -10
- package/.next/standalone/src/lib/cortex/distillation/usage-store.ts +18 -18
- package/.next/standalone/src/lib/cortex/graph/resolver.ts +10 -10
- package/.next/standalone/src/lib/cortex/graph/types.ts +22 -22
- package/.next/standalone/src/lib/cortex/index.ts +56 -56
- package/.next/standalone/src/lib/cortex/ingestion/bootstrap.ts +14 -14
- package/.next/standalone/src/lib/cortex/knowledge/compat.ts +14 -14
- package/.next/standalone/src/lib/cortex/knowledge/contradiction.ts +10 -10
- package/.next/standalone/src/lib/cortex/knowledge/types.ts +67 -67
- package/.next/standalone/src/lib/cortex/lobes/config.ts +16 -16
- package/.next/standalone/src/lib/cortex/lobes/resolver.ts +8 -8
- package/.next/standalone/src/lib/cortex/lobes/shares.ts +14 -14
- package/.next/standalone/src/lib/cortex/mcp/server.ts +8 -8
- package/.next/standalone/src/lib/cortex/portability/exporter.ts +6 -6
- package/.next/standalone/src/lib/cortex/portability/importer.ts +10 -10
- package/.next/standalone/src/lib/cortex/retrieval/context-engine.ts +10 -10
- package/.next/standalone/src/lib/cortex/types.ts +39 -39
- package/.next/standalone/tsconfig.json +34 -34
- package/LICENSE +661 -661
- package/README.md +131 -131
- package/bin/cortex-hook.sh +62 -62
- package/bin/cortex-mcp.js +60 -60
- package/bin/fix-standalone-externals.js +79 -79
- package/bin/lib/auto-setup.js +110 -110
- package/bin/mdns-service.js +171 -171
- package/bin/postinstall.js +35 -35
- package/bin/setup-admin.js +195 -195
- package/bin/spaces-dev.js +208 -208
- package/bin/spaces-install.js +599 -599
- package/bin/spaces-reset-totp.js +50 -50
- package/bin/spaces-service.js +1020 -1020
- package/bin/spaces-setup.js +253 -253
- package/bin/spaces.js +776 -776
- package/bin/ssh-auth-keys.sh +68 -68
- package/bin/terminal-server.js +1683 -1649
- package/package.json +104 -102
- package/.next/standalone/.next/server/chunks/ssr/_078dd64d._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_701606d5._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_72b1de37._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_950142a4._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/src_components_terminal_terminal-pane_tsx_803c5e2c._.js +0 -7
- package/.next/standalone/.next/static/chunks/18f168665aef1aab.js +0 -1
- package/.next/standalone/.next/static/chunks/25b7a243a404a1a7.js +0 -1
- package/.next/standalone/.next/static/chunks/4a50d2a3e9bc9b41.js +0 -1
- package/.next/standalone/.next/static/chunks/6c78a1dfa7ec2959.css +0 -3
- package/.next/standalone/.next/static/chunks/7e0091ab6c5ee8bd.js +0 -1
- package/.next/standalone/.next/static/chunks/869f562dc32e55f4.js +0 -1
- package/.next/standalone/.next/static/chunks/8b3f4572fec83caa.js +0 -5
- package/.next/standalone/.next/static/chunks/8d5419afc4b9116b.js +0 -1
- package/.next/standalone/.next/static/chunks/9b2c5451f0b67975.js +0 -1
- package/.next/standalone/.next/static/chunks/ac339e970df82fa5.js +0 -5
- package/.next/standalone/.next/static/chunks/e7772d64463868eb.js +0 -1
- package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/README.md +0 -46
- package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/lib/glib-2.0/include/glibconfig.h +0 -221
- package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/lib/index.js +0 -1
- package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/lib/libvips-cpp.so.8.17.3 +0 -0
- package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/package.json +0 -42
- package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/README.md +0 -46
- package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/glib-2.0/include/glibconfig.h +0 -221
- package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/index.js +0 -1
- package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/libvips-cpp.so.8.17.3 +0 -0
- package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json +0 -42
- package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/versions.json +0 -30
- package/.next/standalone/node_modules/@img/sharp-linux-x64/lib/sharp-linux-x64.node +0 -0
- package/.next/standalone/node_modules/@img/sharp-linuxmusl-x64/lib/sharp-linuxmusl-x64.node +0 -0
- package/.next/standalone/node_modules/@img/sharp-linuxmusl-x64/package.json +0 -46
- /package/.next/standalone/.next/static/{77VYbwIoyxFNr5xevTrCu → BEY-sql3lQLouidpurSQf}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{77VYbwIoyxFNr5xevTrCu → BEY-sql3lQLouidpurSQf}/_clientMiddlewareManifest.json +0 -0
- /package/.next/standalone/.next/static/{77VYbwIoyxFNr5xevTrCu → BEY-sql3lQLouidpurSQf}/_ssgManifest.js +0 -0
- /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`.
|