@jlongo78/agent-spaces 0.7.5 → 0.7.6
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 +2 -2
- package/.next/standalone/.next/server/app/m/terminal.segments/_full.segment.rsc +2 -2
- 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 +1 -1
- 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/_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/_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/232d8aae4fefab70.js +1 -0
- package/.next/standalone/.next/static/chunks/2ad22562bb37ecad.js +1011 -0
- package/.next/standalone/.next/static/chunks/396eac60f496f178.js +1 -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/6ae575967d091df4.js +1 -0
- package/.next/standalone/.next/static/chunks/7a7c0d9d875332a3.js +1 -0
- package/.next/standalone/.next/static/chunks/7f8455bb855a6c84.js +1 -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/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/e23f20b51a75a5bb.js +757 -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 +103 -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 +103 -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 → ncDe4k4gvD0788HAnq_3G}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{77VYbwIoyxFNr5xevTrCu → ncDe4k4gvD0788HAnq_3G}/_clientMiddlewareManifest.json +0 -0
- /package/.next/standalone/.next/static/{77VYbwIoyxFNr5xevTrCu → ncDe4k4gvD0788HAnq_3G}/_ssgManifest.js +0 -0
- /package/.next/standalone/node_modules/@img/{sharp-libvips-linux-x64 → sharp-win32-x64}/versions.json +0 -0
|
@@ -1,1274 +1,1274 @@
|
|
|
1
|
-
# Cortex v2 — Pillar 3: Context Assembly Engine
|
|
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:** Replace the flat layer-iteration search with a 6-stage context assembly pipeline that detects intent, resolves entities, computes graph-aware weights, searches multiple scopes in parallel, fuses results with evidence scoring, and surfaces conflicts — all within 150ms.
|
|
6
|
-
|
|
7
|
-
**Architecture:** A new `ContextEngine` class wraps the existing `CortexStore` (for low-level vector search) and `EntityGraph` (for graph distance/proximity). It does NOT replace `CortexSearch` — instead it provides the higher-level retrieval interface that the RAG hook calls. The existing `CortexSearch` remains for backward-compatible simple searches. The RAG hook (`cortex-hook.js`) switches from calling the search API to calling a new context-assembly API endpoint.
|
|
8
|
-
|
|
9
|
-
**Tech Stack:** TypeScript, vitest, LanceDB, SQLite (entity graph)
|
|
10
|
-
|
|
11
|
-
**Spec:** `docs/superpowers/specs/2026-03-14-cortex-v2-design.md` — Pillar 3
|
|
12
|
-
|
|
13
|
-
**Depends on:** Pillar 1 (Entity Graph) + Pillar 2 (Knowledge Unit Evolution) — both completed
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
## File Structure
|
|
18
|
-
|
|
19
|
-
```
|
|
20
|
-
New files:
|
|
21
|
-
├── src/lib/cortex/retrieval/intent.ts — Intent detection (regex + keyword)
|
|
22
|
-
├── src/lib/cortex/retrieval/weight.ts — Weight computation (graph × intent × freshness × authority)
|
|
23
|
-
├── src/lib/cortex/retrieval/conflict.ts — Conflict detection in results
|
|
24
|
-
├── src/lib/cortex/retrieval/formatter.ts — Context formatting for RAG injection
|
|
25
|
-
├── src/lib/cortex/retrieval/context-engine.ts — Main 6-stage ContextEngine class
|
|
26
|
-
├── src/app/api/cortex/context/route.ts — API endpoint for context assembly
|
|
27
|
-
|
|
28
|
-
Modified files:
|
|
29
|
-
├── bin/cortex-hook.js — Switch to context-assembly endpoint
|
|
30
|
-
|
|
31
|
-
Test files:
|
|
32
|
-
├── tests/lib/cortex/retrieval/intent.test.ts
|
|
33
|
-
├── tests/lib/cortex/retrieval/weight.test.ts
|
|
34
|
-
├── tests/lib/cortex/retrieval/conflict.test.ts
|
|
35
|
-
├── tests/lib/cortex/retrieval/formatter.test.ts
|
|
36
|
-
├── tests/lib/cortex/retrieval/context-engine.test.ts
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
---
|
|
40
|
-
|
|
41
|
-
## Chunk 1: Intent Detection and Weight Computation
|
|
42
|
-
|
|
43
|
-
### Task 1: Intent detection
|
|
44
|
-
|
|
45
|
-
**Files:**
|
|
46
|
-
- Create: `src/lib/cortex/retrieval/intent.ts`
|
|
47
|
-
- Create: `tests/lib/cortex/retrieval/intent.test.ts`
|
|
48
|
-
|
|
49
|
-
- [ ] **Step 1: Write failing tests**
|
|
50
|
-
|
|
51
|
-
```typescript
|
|
52
|
-
// tests/lib/cortex/retrieval/intent.test.ts
|
|
53
|
-
import { describe, it, expect } from 'vitest';
|
|
54
|
-
import { detectIntent, INTENTS } from '@/lib/cortex/retrieval/intent';
|
|
55
|
-
import type { IntentResult } from '@/lib/cortex/retrieval/intent';
|
|
56
|
-
|
|
57
|
-
describe('detectIntent', () => {
|
|
58
|
-
it('detects debugging intent', () => {
|
|
59
|
-
const result = detectIntent('why does the auth service throw a timeout error?');
|
|
60
|
-
expect(result.intent).toBe('debugging');
|
|
61
|
-
expect(result.confidence).toBeGreaterThan(0.5);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('detects architecture intent', () => {
|
|
65
|
-
const result = detectIntent('what architecture pattern should we use for the new service?');
|
|
66
|
-
expect(result.intent).toBe('architecture');
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('detects how-to intent', () => {
|
|
70
|
-
const result = detectIntent('how do I deploy this service to production?');
|
|
71
|
-
expect(result.intent).toBe('how-to');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('detects security intent', () => {
|
|
75
|
-
const result = detectIntent('is there a vulnerability in our authentication flow?');
|
|
76
|
-
expect(result.intent).toBe('security');
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('defaults to general for ambiguous queries', () => {
|
|
80
|
-
const result = detectIntent('tell me about the project');
|
|
81
|
-
expect(result.intent).toBe('general');
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('returns bias config for the detected intent', () => {
|
|
85
|
-
const result = detectIntent('fix this bug in the login page');
|
|
86
|
-
expect(result.biases).toBeDefined();
|
|
87
|
-
expect(result.biases.scope_boost).toBeDefined();
|
|
88
|
-
expect(result.biases.type_boost).toBeDefined();
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('exports all intent definitions', () => {
|
|
92
|
-
expect(Object.keys(INTENTS)).toContain('debugging');
|
|
93
|
-
expect(Object.keys(INTENTS)).toContain('architecture');
|
|
94
|
-
expect(Object.keys(INTENTS)).toContain('general');
|
|
95
|
-
expect(Object.keys(INTENTS).length).toBe(8);
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
- [ ] **Step 2: Run tests to verify they fail**
|
|
101
|
-
|
|
102
|
-
Run: `npx vitest run tests/lib/cortex/retrieval/intent.test.ts`
|
|
103
|
-
|
|
104
|
-
- [ ] **Step 3: Implement intent detection**
|
|
105
|
-
|
|
106
|
-
```typescript
|
|
107
|
-
// src/lib/cortex/retrieval/intent.ts
|
|
108
|
-
|
|
109
|
-
export interface IntentBiases {
|
|
110
|
-
scope_boost: Record<string, number>; // scope level → multiplier
|
|
111
|
-
type_boost: Record<string, number>; // knowledge type → multiplier
|
|
112
|
-
recency_boost: number; // extra recency multiplier
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export interface IntentResult {
|
|
116
|
-
intent: string;
|
|
117
|
-
confidence: number;
|
|
118
|
-
biases: IntentBiases;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
interface IntentDef {
|
|
122
|
-
patterns: RegExp[];
|
|
123
|
-
keywords: string[];
|
|
124
|
-
biases: IntentBiases;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export const INTENTS: Record<string, IntentDef> = {
|
|
128
|
-
debugging: {
|
|
129
|
-
patterns: [
|
|
130
|
-
/\b(error|bug|fix|crash|fail|broken|throw|exception|timeout|issue)\b/i,
|
|
131
|
-
/\bwhy\s+(does|is|did|do)\b/i,
|
|
132
|
-
/\bnot\s+work/i,
|
|
133
|
-
],
|
|
134
|
-
keywords: ['error', 'bug', 'fix', 'debug', 'crash', 'fail', 'broken', 'throw', 'exception', 'timeout', 'issue', 'stack trace'],
|
|
135
|
-
biases: {
|
|
136
|
-
scope_boost: { personal: 1.2, team: 1.0, department: 0.9, organization: 0.8 },
|
|
137
|
-
type_boost: { error_fix: 1.3, pattern: 1.0, decision: 0.8, conversation: 0.7 },
|
|
138
|
-
recency_boost: 1.1,
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
architecture: {
|
|
142
|
-
patterns: [
|
|
143
|
-
/\b(architect|design|pattern|structure|approach)\b/i,
|
|
144
|
-
/\bshould\s+we\s+(use|adopt|switch|migrate)\b/i,
|
|
145
|
-
],
|
|
146
|
-
keywords: ['architecture', 'design', 'pattern', 'structure', 'approach', 'decision', 'migration', 'refactor'],
|
|
147
|
-
biases: {
|
|
148
|
-
scope_boost: { personal: 0.9, team: 1.1, department: 1.2, organization: 1.0 },
|
|
149
|
-
type_boost: { decision: 1.5, pattern: 1.2, error_fix: 0.7, conversation: 0.5 },
|
|
150
|
-
recency_boost: 1.0,
|
|
151
|
-
},
|
|
152
|
-
},
|
|
153
|
-
onboarding: {
|
|
154
|
-
patterns: [
|
|
155
|
-
/\b(how\s+does|explain|what\s+is|overview|getting\s+started)\b/i,
|
|
156
|
-
/\bnew\s+to\b/i,
|
|
157
|
-
],
|
|
158
|
-
keywords: ['explain', 'overview', 'introduction', 'getting started', 'onboarding', 'how does'],
|
|
159
|
-
biases: {
|
|
160
|
-
scope_boost: { personal: 0.7, team: 1.0, department: 1.1, organization: 1.2 },
|
|
161
|
-
type_boost: { pattern: 1.3, decision: 1.2, summary: 1.2, conversation: 0.5 },
|
|
162
|
-
recency_boost: 0.9,
|
|
163
|
-
},
|
|
164
|
-
},
|
|
165
|
-
policy: {
|
|
166
|
-
patterns: [
|
|
167
|
-
/\b(policy|compliance|regulation|standard|rule|requirement)\b/i,
|
|
168
|
-
/\ballowed\s+to\b/i,
|
|
169
|
-
],
|
|
170
|
-
keywords: ['policy', 'compliance', 'standard', 'regulation', 'rule', 'requirement', 'allowed'],
|
|
171
|
-
biases: {
|
|
172
|
-
scope_boost: { personal: 0.6, team: 0.8, department: 1.0, organization: 1.3 },
|
|
173
|
-
type_boost: { decision: 1.5, preference: 1.2, pattern: 0.8, conversation: 0.3 },
|
|
174
|
-
recency_boost: 1.0,
|
|
175
|
-
},
|
|
176
|
-
},
|
|
177
|
-
'how-to': {
|
|
178
|
-
patterns: [
|
|
179
|
-
/\bhow\s+(do|can|to|should)\s+I?\b/i,
|
|
180
|
-
/\bsteps?\s+(to|for)\b/i,
|
|
181
|
-
/\bwhat('s| is)\s+the\s+(command|way|process)\b/i,
|
|
182
|
-
],
|
|
183
|
-
keywords: ['how to', 'steps', 'command', 'run', 'deploy', 'install', 'configure', 'setup'],
|
|
184
|
-
biases: {
|
|
185
|
-
scope_boost: { personal: 1.2, team: 1.0, department: 0.8, organization: 0.7 },
|
|
186
|
-
type_boost: { command: 1.3, pattern: 1.2, error_fix: 1.0, conversation: 0.6 },
|
|
187
|
-
recency_boost: 1.05,
|
|
188
|
-
},
|
|
189
|
-
},
|
|
190
|
-
review: {
|
|
191
|
-
patterns: [
|
|
192
|
-
/\b(review|feedback|improve|quality|best\s+practice)\b/i,
|
|
193
|
-
/\bis\s+this\s+(good|correct|right)\b/i,
|
|
194
|
-
],
|
|
195
|
-
keywords: ['review', 'feedback', 'quality', 'improve', 'best practice', 'convention'],
|
|
196
|
-
biases: {
|
|
197
|
-
scope_boost: { personal: 0.9, team: 1.2, department: 1.0, organization: 0.8 },
|
|
198
|
-
type_boost: { preference: 1.3, pattern: 1.2, code_pattern: 1.2, decision: 1.0 },
|
|
199
|
-
recency_boost: 1.0,
|
|
200
|
-
},
|
|
201
|
-
},
|
|
202
|
-
security: {
|
|
203
|
-
patterns: [
|
|
204
|
-
/\b(security|vulnerab|exploit|attack|auth|cve|injection|xss)\b/i,
|
|
205
|
-
/\bsecure\b/i,
|
|
206
|
-
],
|
|
207
|
-
keywords: ['security', 'vulnerability', 'exploit', 'attack', 'authentication', 'authorization', 'cve', 'injection', 'xss', 'csrf'],
|
|
208
|
-
biases: {
|
|
209
|
-
scope_boost: { personal: 0.8, team: 1.0, department: 1.2, organization: 1.0 },
|
|
210
|
-
type_boost: { error_fix: 1.3, decision: 1.2, pattern: 1.0, conversation: 0.5 },
|
|
211
|
-
recency_boost: 1.1,
|
|
212
|
-
},
|
|
213
|
-
},
|
|
214
|
-
general: {
|
|
215
|
-
patterns: [],
|
|
216
|
-
keywords: [],
|
|
217
|
-
biases: {
|
|
218
|
-
scope_boost: { personal: 1.0, team: 1.0, department: 1.0, organization: 1.0 },
|
|
219
|
-
type_boost: {},
|
|
220
|
-
recency_boost: 1.0,
|
|
221
|
-
},
|
|
222
|
-
},
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Detect the intent of a query using regex patterns and keyword scoring.
|
|
227
|
-
* No LLM call — fast and deterministic.
|
|
228
|
-
*/
|
|
229
|
-
export function detectIntent(query: string): IntentResult {
|
|
230
|
-
const lower = query.toLowerCase();
|
|
231
|
-
let bestIntent = 'general';
|
|
232
|
-
let bestScore = 0;
|
|
233
|
-
|
|
234
|
-
for (const [name, def] of Object.entries(INTENTS)) {
|
|
235
|
-
if (name === 'general') continue;
|
|
236
|
-
|
|
237
|
-
let score = 0;
|
|
238
|
-
|
|
239
|
-
// Regex pattern matches (high weight)
|
|
240
|
-
for (const pattern of def.patterns) {
|
|
241
|
-
if (pattern.test(query)) score += 2;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Keyword matches (lower weight)
|
|
245
|
-
for (const keyword of def.keywords) {
|
|
246
|
-
if (lower.includes(keyword)) score += 1;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
if (score > bestScore) {
|
|
250
|
-
bestScore = score;
|
|
251
|
-
bestIntent = name;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return {
|
|
256
|
-
intent: bestIntent,
|
|
257
|
-
confidence: bestScore > 0 ? Math.min(1.0, bestScore / 6) : 0.5,
|
|
258
|
-
biases: INTENTS[bestIntent].biases,
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
- [ ] **Step 4: Run tests to verify they pass**
|
|
264
|
-
|
|
265
|
-
Run: `npx vitest run tests/lib/cortex/retrieval/intent.test.ts`
|
|
266
|
-
Expected: PASS (7 tests)
|
|
267
|
-
|
|
268
|
-
- [ ] **Step 5: Commit**
|
|
269
|
-
|
|
270
|
-
```bash
|
|
271
|
-
git add src/lib/cortex/retrieval/intent.ts tests/lib/cortex/retrieval/intent.test.ts
|
|
272
|
-
git commit -m "feat(cortex): add intent detection for context assembly"
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
---
|
|
276
|
-
|
|
277
|
-
### Task 2: Weight computation
|
|
278
|
-
|
|
279
|
-
**Files:**
|
|
280
|
-
- Create: `src/lib/cortex/retrieval/weight.ts`
|
|
281
|
-
- Create: `tests/lib/cortex/retrieval/weight.test.ts`
|
|
282
|
-
|
|
283
|
-
- [ ] **Step 1: Write failing tests**
|
|
284
|
-
|
|
285
|
-
```typescript
|
|
286
|
-
// tests/lib/cortex/retrieval/weight.test.ts
|
|
287
|
-
import { describe, it, expect } from 'vitest';
|
|
288
|
-
import { computeScopeWeight } from '@/lib/cortex/retrieval/weight';
|
|
289
|
-
import type { IntentBiases } from '@/lib/cortex/retrieval/intent';
|
|
290
|
-
|
|
291
|
-
describe('computeScopeWeight', () => {
|
|
292
|
-
const neutralBiases: IntentBiases = {
|
|
293
|
-
scope_boost: { personal: 1.0, team: 1.0, department: 1.0, organization: 1.0 },
|
|
294
|
-
type_boost: {},
|
|
295
|
-
recency_boost: 1.0,
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
it('returns 1.0 for self (distance 0)', () => {
|
|
299
|
-
const weight = computeScopeWeight({
|
|
300
|
-
graphProximity: 1.0, // 1/(1+0)
|
|
301
|
-
scopeLevel: 'personal',
|
|
302
|
-
intentBiases: neutralBiases,
|
|
303
|
-
authorityFactor: 1.0,
|
|
304
|
-
});
|
|
305
|
-
expect(weight).toBeCloseTo(1.0);
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
it('decreases with graph distance', () => {
|
|
309
|
-
const close = computeScopeWeight({
|
|
310
|
-
graphProximity: 0.5, // distance 1
|
|
311
|
-
scopeLevel: 'team',
|
|
312
|
-
intentBiases: neutralBiases,
|
|
313
|
-
authorityFactor: 1.0,
|
|
314
|
-
});
|
|
315
|
-
const far = computeScopeWeight({
|
|
316
|
-
graphProximity: 0.25, // distance 3
|
|
317
|
-
scopeLevel: 'organization',
|
|
318
|
-
intentBiases: neutralBiases,
|
|
319
|
-
authorityFactor: 1.0,
|
|
320
|
-
});
|
|
321
|
-
expect(close).toBeGreaterThan(far);
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
it('is boosted by intent biases', () => {
|
|
325
|
-
const debugBiases: IntentBiases = {
|
|
326
|
-
scope_boost: { personal: 1.2, team: 0.8 },
|
|
327
|
-
type_boost: {},
|
|
328
|
-
recency_boost: 1.0,
|
|
329
|
-
};
|
|
330
|
-
const personal = computeScopeWeight({
|
|
331
|
-
graphProximity: 0.5,
|
|
332
|
-
scopeLevel: 'personal',
|
|
333
|
-
intentBiases: debugBiases,
|
|
334
|
-
authorityFactor: 1.0,
|
|
335
|
-
});
|
|
336
|
-
const team = computeScopeWeight({
|
|
337
|
-
graphProximity: 0.5,
|
|
338
|
-
scopeLevel: 'team',
|
|
339
|
-
intentBiases: debugBiases,
|
|
340
|
-
authorityFactor: 1.0,
|
|
341
|
-
});
|
|
342
|
-
expect(personal).toBeGreaterThan(team);
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
it('is boosted by authority factor', () => {
|
|
346
|
-
const low = computeScopeWeight({
|
|
347
|
-
graphProximity: 0.5,
|
|
348
|
-
scopeLevel: 'team',
|
|
349
|
-
intentBiases: neutralBiases,
|
|
350
|
-
authorityFactor: 1.0,
|
|
351
|
-
});
|
|
352
|
-
const high = computeScopeWeight({
|
|
353
|
-
graphProximity: 0.5,
|
|
354
|
-
scopeLevel: 'team',
|
|
355
|
-
intentBiases: neutralBiases,
|
|
356
|
-
authorityFactor: 1.2,
|
|
357
|
-
});
|
|
358
|
-
expect(high).toBeGreaterThan(low);
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
it('never returns negative', () => {
|
|
362
|
-
const weight = computeScopeWeight({
|
|
363
|
-
graphProximity: 0,
|
|
364
|
-
scopeLevel: 'organization',
|
|
365
|
-
intentBiases: neutralBiases,
|
|
366
|
-
authorityFactor: 1.0,
|
|
367
|
-
});
|
|
368
|
-
expect(weight).toBeGreaterThanOrEqual(0);
|
|
369
|
-
});
|
|
370
|
-
});
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
- [ ] **Step 2: Run tests to verify they fail**
|
|
374
|
-
|
|
375
|
-
Run: `npx vitest run tests/lib/cortex/retrieval/weight.test.ts`
|
|
376
|
-
|
|
377
|
-
- [ ] **Step 3: Implement weight computation**
|
|
378
|
-
|
|
379
|
-
```typescript
|
|
380
|
-
// src/lib/cortex/retrieval/weight.ts
|
|
381
|
-
import type { IntentBiases } from './intent';
|
|
382
|
-
import type { ScopeLevel } from '../knowledge/types';
|
|
383
|
-
|
|
384
|
-
export interface ScopeWeightInput {
|
|
385
|
-
graphProximity: number; // 0-1, from EntityGraph.proximity()
|
|
386
|
-
scopeLevel: ScopeLevel | string;
|
|
387
|
-
intentBiases: IntentBiases;
|
|
388
|
-
authorityFactor: number; // 1.0 default, higher for experts/docs
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* Compute the retrieval weight for a knowledge scope.
|
|
393
|
-
*
|
|
394
|
-
* weight = graphProximity × intentBias × authorityFactor
|
|
395
|
-
*
|
|
396
|
-
* Per spec: weight(scope) = graph_proximity × intent_bias × freshness_bonus × authority
|
|
397
|
-
* Freshness is applied per-result in the fusion stage, not per-scope.
|
|
398
|
-
*/
|
|
399
|
-
export function computeScopeWeight(input: ScopeWeightInput): number {
|
|
400
|
-
const { graphProximity, scopeLevel, intentBiases, authorityFactor } = input;
|
|
401
|
-
|
|
402
|
-
const intentBias = intentBiases.scope_boost[scopeLevel] ?? 1.0;
|
|
403
|
-
|
|
404
|
-
return Math.max(0, graphProximity * intentBias * authorityFactor);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
/**
|
|
408
|
-
* Compute per-result type boost from intent biases.
|
|
409
|
-
*/
|
|
410
|
-
export function computeTypeBoost(knowledgeType: string, intentBiases: IntentBiases): number {
|
|
411
|
-
return intentBiases.type_boost[knowledgeType] ?? 1.0;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Authority factor for a source based on role and expertise.
|
|
416
|
-
*
|
|
417
|
-
* role_boost: 0.0 member, 0.1 lead, 0.15 senior/principal, 0.2 director+
|
|
418
|
-
* expertise_weight: EXPERT_IN edge weight (0-1)
|
|
419
|
-
* Documents get 1.2 base authority.
|
|
420
|
-
*/
|
|
421
|
-
export function computeAuthority(params: {
|
|
422
|
-
role?: string;
|
|
423
|
-
expertiseWeight?: number;
|
|
424
|
-
isDocument?: boolean;
|
|
425
|
-
}): number {
|
|
426
|
-
const { role, expertiseWeight = 0, isDocument = false } = params;
|
|
427
|
-
|
|
428
|
-
if (isDocument) return 1.2;
|
|
429
|
-
|
|
430
|
-
const roleBoosts: Record<string, number> = {
|
|
431
|
-
member: 0.0,
|
|
432
|
-
lead: 0.1,
|
|
433
|
-
senior: 0.15,
|
|
434
|
-
principal: 0.15,
|
|
435
|
-
director: 0.2,
|
|
436
|
-
vp: 0.2,
|
|
437
|
-
cto: 0.2,
|
|
438
|
-
};
|
|
439
|
-
|
|
440
|
-
const roleBoost = roleBoosts[role?.toLowerCase() ?? 'member'] ?? 0.0;
|
|
441
|
-
return Math.max(1.0, roleBoost + expertiseWeight);
|
|
442
|
-
}
|
|
443
|
-
```
|
|
444
|
-
|
|
445
|
-
- [ ] **Step 4: Run tests to verify they pass**
|
|
446
|
-
|
|
447
|
-
Run: `npx vitest run tests/lib/cortex/retrieval/weight.test.ts`
|
|
448
|
-
Expected: PASS (5 tests)
|
|
449
|
-
|
|
450
|
-
- [ ] **Step 5: Commit**
|
|
451
|
-
|
|
452
|
-
```bash
|
|
453
|
-
git add src/lib/cortex/retrieval/weight.ts tests/lib/cortex/retrieval/weight.test.ts
|
|
454
|
-
git commit -m "feat(cortex): add scope weight computation for context assembly"
|
|
455
|
-
```
|
|
456
|
-
|
|
457
|
-
---
|
|
458
|
-
|
|
459
|
-
## Chunk 2: Conflict Detection and Context Formatting
|
|
460
|
-
|
|
461
|
-
### Task 3: Conflict detection
|
|
462
|
-
|
|
463
|
-
**Files:**
|
|
464
|
-
- Create: `src/lib/cortex/retrieval/conflict.ts`
|
|
465
|
-
- Create: `tests/lib/cortex/retrieval/conflict.test.ts`
|
|
466
|
-
|
|
467
|
-
- [ ] **Step 1: Write failing tests**
|
|
468
|
-
|
|
469
|
-
```typescript
|
|
470
|
-
// tests/lib/cortex/retrieval/conflict.test.ts
|
|
471
|
-
import { describe, it, expect } from 'vitest';
|
|
472
|
-
import { detectConflicts } from '@/lib/cortex/retrieval/conflict';
|
|
473
|
-
import type { ScoredKnowledge } from '@/lib/cortex/knowledge/types';
|
|
474
|
-
|
|
475
|
-
function makeResult(overrides: Partial<ScoredKnowledge> = {}): ScoredKnowledge {
|
|
476
|
-
return {
|
|
477
|
-
id: 'r1', vector: [], text: 'test', type: 'decision', layer: 'personal',
|
|
478
|
-
workspace_id: null, session_id: null, agent_type: 'claude',
|
|
479
|
-
project_path: null, file_refs: [], confidence: 0.8,
|
|
480
|
-
created: new Date().toISOString(), source_timestamp: new Date().toISOString(),
|
|
481
|
-
stale_score: 0, access_count: 0, last_accessed: null, metadata: {},
|
|
482
|
-
relevance_score: 0.9, similarity: 0.9,
|
|
483
|
-
contradiction_refs: [], ...overrides,
|
|
484
|
-
};
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
describe('detectConflicts', () => {
|
|
488
|
-
it('returns no conflicts when no contradiction_refs', () => {
|
|
489
|
-
const results = [makeResult({ id: 'a' }), makeResult({ id: 'b' })];
|
|
490
|
-
const conflicts = detectConflicts(results);
|
|
491
|
-
expect(conflicts).toHaveLength(0);
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
it('detects conflict between two results', () => {
|
|
495
|
-
const results = [
|
|
496
|
-
makeResult({ id: 'a', text: 'use pool size 50', contradiction_refs: ['b'] }),
|
|
497
|
-
makeResult({ id: 'b', text: 'scale horizontally', contradiction_refs: ['a'] }),
|
|
498
|
-
];
|
|
499
|
-
const conflicts = detectConflicts(results);
|
|
500
|
-
expect(conflicts).toHaveLength(1);
|
|
501
|
-
expect(conflicts[0].unitA.id).toBe('a');
|
|
502
|
-
expect(conflicts[0].unitB.id).toBe('b');
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
it('ignores contradiction_refs pointing to results not in the set', () => {
|
|
506
|
-
const results = [
|
|
507
|
-
makeResult({ id: 'a', contradiction_refs: ['z'] }), // z is not in results
|
|
508
|
-
];
|
|
509
|
-
const conflicts = detectConflicts(results);
|
|
510
|
-
expect(conflicts).toHaveLength(0);
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
it('deduplicates symmetric conflicts', () => {
|
|
514
|
-
// A contradicts B and B contradicts A should produce one conflict, not two
|
|
515
|
-
const results = [
|
|
516
|
-
makeResult({ id: 'a', contradiction_refs: ['b'] }),
|
|
517
|
-
makeResult({ id: 'b', contradiction_refs: ['a'] }),
|
|
518
|
-
];
|
|
519
|
-
const conflicts = detectConflicts(results);
|
|
520
|
-
expect(conflicts).toHaveLength(1);
|
|
521
|
-
});
|
|
522
|
-
});
|
|
523
|
-
```
|
|
524
|
-
|
|
525
|
-
- [ ] **Step 2: Implement conflict detection**
|
|
526
|
-
|
|
527
|
-
```typescript
|
|
528
|
-
// src/lib/cortex/retrieval/conflict.ts
|
|
529
|
-
import type { ScoredKnowledge } from '../knowledge/types';
|
|
530
|
-
|
|
531
|
-
export interface ConflictPair {
|
|
532
|
-
unitA: ScoredKnowledge;
|
|
533
|
-
unitB: ScoredKnowledge;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
/**
|
|
537
|
-
* Detect conflicts among search results by checking contradiction_refs.
|
|
538
|
-
* Returns deduplicated conflict pairs (A↔B counted once, not twice).
|
|
539
|
-
*/
|
|
540
|
-
export function detectConflicts(results: ScoredKnowledge[]): ConflictPair[] {
|
|
541
|
-
const resultMap = new Map(results.map(r => [r.id, r]));
|
|
542
|
-
const seen = new Set<string>();
|
|
543
|
-
const conflicts: ConflictPair[] = [];
|
|
544
|
-
|
|
545
|
-
for (const result of results) {
|
|
546
|
-
const refs = result.contradiction_refs ?? [];
|
|
547
|
-
for (const refId of refs) {
|
|
548
|
-
const other = resultMap.get(refId);
|
|
549
|
-
if (!other) continue;
|
|
550
|
-
|
|
551
|
-
const key = [result.id, refId].sort().join('|');
|
|
552
|
-
if (seen.has(key)) continue;
|
|
553
|
-
seen.add(key);
|
|
554
|
-
|
|
555
|
-
conflicts.push({ unitA: result, unitB: other });
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
return conflicts;
|
|
560
|
-
}
|
|
561
|
-
```
|
|
562
|
-
|
|
563
|
-
- [ ] **Step 3: Run tests, commit**
|
|
564
|
-
|
|
565
|
-
Run: `npx vitest run tests/lib/cortex/retrieval/conflict.test.ts`
|
|
566
|
-
|
|
567
|
-
```bash
|
|
568
|
-
git add src/lib/cortex/retrieval/conflict.ts tests/lib/cortex/retrieval/conflict.test.ts
|
|
569
|
-
git commit -m "feat(cortex): add conflict detection for search results"
|
|
570
|
-
```
|
|
571
|
-
|
|
572
|
-
---
|
|
573
|
-
|
|
574
|
-
### Task 4: Context formatter
|
|
575
|
-
|
|
576
|
-
**Files:**
|
|
577
|
-
- Create: `src/lib/cortex/retrieval/formatter.ts`
|
|
578
|
-
- Create: `tests/lib/cortex/retrieval/formatter.test.ts`
|
|
579
|
-
|
|
580
|
-
- [ ] **Step 1: Write failing tests**
|
|
581
|
-
|
|
582
|
-
```typescript
|
|
583
|
-
// tests/lib/cortex/retrieval/formatter.test.ts
|
|
584
|
-
import { describe, it, expect } from 'vitest';
|
|
585
|
-
import { formatContext } from '@/lib/cortex/retrieval/formatter';
|
|
586
|
-
import type { ScoredKnowledge } from '@/lib/cortex/knowledge/types';
|
|
587
|
-
import type { ConflictPair } from '@/lib/cortex/retrieval/conflict';
|
|
588
|
-
|
|
589
|
-
function makeResult(overrides: Partial<ScoredKnowledge> = {}): ScoredKnowledge {
|
|
590
|
-
return {
|
|
591
|
-
id: 'r1', vector: [], text: 'test knowledge', type: 'decision', layer: 'personal',
|
|
592
|
-
workspace_id: null, session_id: null, agent_type: 'claude',
|
|
593
|
-
project_path: null, file_refs: [], confidence: 0.8,
|
|
594
|
-
created: '2026-03-15T00:00:00.000Z', source_timestamp: '2026-03-15T00:00:00.000Z',
|
|
595
|
-
stale_score: 0, access_count: 5, last_accessed: null, metadata: {},
|
|
596
|
-
relevance_score: 0.9, similarity: 0.9,
|
|
597
|
-
...overrides,
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
describe('formatContext', () => {
|
|
602
|
-
it('wraps results in cortex-context tags', () => {
|
|
603
|
-
const output = formatContext([makeResult()], []);
|
|
604
|
-
expect(output).toContain('<cortex-context>');
|
|
605
|
-
expect(output).toContain('</cortex-context>');
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
it('includes type labels and dates', () => {
|
|
609
|
-
const output = formatContext([makeResult({ type: 'error_fix', source_timestamp: '2026-03-10T00:00:00.000Z' })], []);
|
|
610
|
-
expect(output).toContain('[Error Fix]');
|
|
611
|
-
expect(output).toContain('2026-03-10');
|
|
612
|
-
});
|
|
613
|
-
|
|
614
|
-
it('includes source attribution when origin is present', () => {
|
|
615
|
-
const output = formatContext([makeResult({
|
|
616
|
-
origin: { source_type: 'conversation', source_ref: 'sess-1', creator_entity_id: 'person-alice' },
|
|
617
|
-
})], []);
|
|
618
|
-
expect(output).toContain('person-alice');
|
|
619
|
-
});
|
|
620
|
-
|
|
621
|
-
it('includes conflict callout when conflicts exist', () => {
|
|
622
|
-
const a = makeResult({ id: 'a', text: 'use pool size 50' });
|
|
623
|
-
const b = makeResult({ id: 'b', text: 'scale horizontally' });
|
|
624
|
-
const conflicts: ConflictPair[] = [{ unitA: a, unitB: b }];
|
|
625
|
-
const output = formatContext([a, b], conflicts);
|
|
626
|
-
expect(output).toContain('Conflicting');
|
|
627
|
-
});
|
|
628
|
-
|
|
629
|
-
it('respects max token budget', () => {
|
|
630
|
-
const bigResults = Array.from({ length: 20 }, (_, i) =>
|
|
631
|
-
makeResult({ id: `r${i}`, text: 'x'.repeat(500) })
|
|
632
|
-
);
|
|
633
|
-
const output = formatContext(bigResults, [], { maxTokens: 500 });
|
|
634
|
-
// Should not include all 20 results (would be ~2500 tokens)
|
|
635
|
-
expect(output.length).toBeLessThan(3000);
|
|
636
|
-
});
|
|
637
|
-
|
|
638
|
-
it('returns empty string when no results', () => {
|
|
639
|
-
expect(formatContext([], [])).toBe('');
|
|
640
|
-
});
|
|
641
|
-
});
|
|
642
|
-
```
|
|
643
|
-
|
|
644
|
-
- [ ] **Step 2: Implement context formatter**
|
|
645
|
-
|
|
646
|
-
```typescript
|
|
647
|
-
// src/lib/cortex/retrieval/formatter.ts
|
|
648
|
-
import type { ScoredKnowledge } from '../knowledge/types';
|
|
649
|
-
import type { ConflictPair } from './conflict';
|
|
650
|
-
|
|
651
|
-
const TYPE_LABELS: Record<string, string> = {
|
|
652
|
-
decision: 'Decision', pattern: 'Pattern', preference: 'Preference',
|
|
653
|
-
error_fix: 'Error Fix', context: 'Context', code_pattern: 'Code',
|
|
654
|
-
command: 'Command', conversation: 'Conversation', summary: 'Summary',
|
|
655
|
-
};
|
|
656
|
-
|
|
657
|
-
export interface FormatOptions {
|
|
658
|
-
maxTokens?: number;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
/**
|
|
662
|
-
* Format search results + conflicts as annotated <cortex-context> for RAG injection.
|
|
663
|
-
*/
|
|
664
|
-
export function formatContext(
|
|
665
|
-
results: ScoredKnowledge[],
|
|
666
|
-
conflicts: ConflictPair[],
|
|
667
|
-
options: FormatOptions = {},
|
|
668
|
-
): string {
|
|
669
|
-
if (results.length === 0) return '';
|
|
670
|
-
|
|
671
|
-
const maxTokens = options.maxTokens ?? 1500;
|
|
672
|
-
const entries: string[] = [];
|
|
673
|
-
let tokens = 20; // overhead for tags
|
|
674
|
-
|
|
675
|
-
// Format each result with attribution
|
|
676
|
-
for (const unit of results) {
|
|
677
|
-
const label = TYPE_LABELS[unit.type] || unit.type;
|
|
678
|
-
const date = (unit.source_timestamp || '').slice(0, 10);
|
|
679
|
-
const creator = unit.origin?.creator_entity_id ?? '';
|
|
680
|
-
const sourceInfo = creator ? ` (${creator})` : '';
|
|
681
|
-
|
|
682
|
-
let entry = `[${label}] ${date}${sourceInfo}:\n ${unit.text}`;
|
|
683
|
-
|
|
684
|
-
const entryTokens = Math.ceil(entry.length / 4);
|
|
685
|
-
if (tokens + entryTokens > maxTokens) break;
|
|
686
|
-
|
|
687
|
-
entries.push(entry);
|
|
688
|
-
tokens += entryTokens;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
if (entries.length === 0) return '';
|
|
692
|
-
|
|
693
|
-
// Build conflict section
|
|
694
|
-
let conflictSection = '';
|
|
695
|
-
if (conflicts.length > 0) {
|
|
696
|
-
const conflictLines = conflicts.map(c =>
|
|
697
|
-
` - "${c.unitA.text.slice(0, 80)}..." vs "${c.unitB.text.slice(0, 80)}..."`
|
|
698
|
-
);
|
|
699
|
-
conflictSection = `\nConflicting perspectives (${conflicts.length}):\n${conflictLines.join('\n')}\n`;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
const sourceCount = entries.length;
|
|
703
|
-
const header = `Relevant knowledge (${sourceCount} source${sourceCount > 1 ? 's' : ''}${conflicts.length > 0 ? `, ${conflicts.length} conflict${conflicts.length > 1 ? 's' : ''}` : ''}):`;
|
|
704
|
-
|
|
705
|
-
return [
|
|
706
|
-
'<cortex-context>',
|
|
707
|
-
header,
|
|
708
|
-
'',
|
|
709
|
-
...entries,
|
|
710
|
-
conflictSection,
|
|
711
|
-
'</cortex-context>',
|
|
712
|
-
].join('\n');
|
|
713
|
-
}
|
|
714
|
-
```
|
|
715
|
-
|
|
716
|
-
- [ ] **Step 3: Run tests, commit**
|
|
717
|
-
|
|
718
|
-
Run: `npx vitest run tests/lib/cortex/retrieval/formatter.test.ts`
|
|
719
|
-
|
|
720
|
-
```bash
|
|
721
|
-
git add src/lib/cortex/retrieval/formatter.ts tests/lib/cortex/retrieval/formatter.test.ts
|
|
722
|
-
git commit -m "feat(cortex): add context formatter for RAG injection"
|
|
723
|
-
```
|
|
724
|
-
|
|
725
|
-
---
|
|
726
|
-
|
|
727
|
-
## Chunk 3: Context Assembly Engine
|
|
728
|
-
|
|
729
|
-
### Task 5: ContextEngine — the 6-stage pipeline
|
|
730
|
-
|
|
731
|
-
**Files:**
|
|
732
|
-
- Create: `src/lib/cortex/retrieval/context-engine.ts`
|
|
733
|
-
- Create: `tests/lib/cortex/retrieval/context-engine.test.ts`
|
|
734
|
-
|
|
735
|
-
- [ ] **Step 1: Write failing tests**
|
|
736
|
-
|
|
737
|
-
```typescript
|
|
738
|
-
// tests/lib/cortex/retrieval/context-engine.test.ts
|
|
739
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
740
|
-
import { ContextEngine } from '@/lib/cortex/retrieval/context-engine';
|
|
741
|
-
|
|
742
|
-
// Create mocks for dependencies
|
|
743
|
-
const mockStore = {
|
|
744
|
-
search: vi.fn().mockResolvedValue([]),
|
|
745
|
-
};
|
|
746
|
-
|
|
747
|
-
const mockGraph = {
|
|
748
|
-
proximity: vi.fn().mockReturnValue(0.5),
|
|
749
|
-
neighborhood: vi.fn().mockReturnValue([]),
|
|
750
|
-
getEntity: vi.fn().mockReturnValue(null),
|
|
751
|
-
};
|
|
752
|
-
|
|
753
|
-
const mockResolver = {
|
|
754
|
-
extractEntities: vi.fn().mockReturnValue([]),
|
|
755
|
-
};
|
|
756
|
-
|
|
757
|
-
const mockEmbedding = {
|
|
758
|
-
embed: vi.fn().mockResolvedValue([[0.1, 0.2, 0.3]]),
|
|
759
|
-
dimensions: 3,
|
|
760
|
-
name: 'mock',
|
|
761
|
-
init: vi.fn(),
|
|
762
|
-
};
|
|
763
|
-
|
|
764
|
-
describe('ContextEngine', () => {
|
|
765
|
-
let engine: ContextEngine;
|
|
766
|
-
|
|
767
|
-
beforeEach(() => {
|
|
768
|
-
vi.clearAllMocks();
|
|
769
|
-
engine = new ContextEngine({
|
|
770
|
-
store: mockStore as any,
|
|
771
|
-
graph: mockGraph as any,
|
|
772
|
-
resolver: mockResolver as any,
|
|
773
|
-
embedding: mockEmbedding as any,
|
|
774
|
-
requesterId: 'person-alice',
|
|
775
|
-
});
|
|
776
|
-
});
|
|
777
|
-
|
|
778
|
-
it('returns empty context for empty results', async () => {
|
|
779
|
-
const result = await engine.assemble('some query');
|
|
780
|
-
expect(result.results).toHaveLength(0);
|
|
781
|
-
expect(result.context).toBe('');
|
|
782
|
-
});
|
|
783
|
-
|
|
784
|
-
it('calls embedding.embed with the query', async () => {
|
|
785
|
-
await engine.assemble('test query');
|
|
786
|
-
expect(mockEmbedding.embed).toHaveBeenCalledWith(['test query']);
|
|
787
|
-
});
|
|
788
|
-
|
|
789
|
-
it('detects intent from the query', async () => {
|
|
790
|
-
const result = await engine.assemble('why does auth throw an error?');
|
|
791
|
-
expect(result.intent.intent).toBe('debugging');
|
|
792
|
-
});
|
|
793
|
-
|
|
794
|
-
it('extracts entities from the query', async () => {
|
|
795
|
-
mockResolver.extractEntities.mockReturnValue([
|
|
796
|
-
{ entity: { id: 'system-auth', type: 'system', name: 'Auth' }, confidence: 0.9, method: 'alias' },
|
|
797
|
-
]);
|
|
798
|
-
const result = await engine.assemble('fix the auth service');
|
|
799
|
-
expect(mockResolver.extractEntities).toHaveBeenCalledWith('fix the auth service');
|
|
800
|
-
expect(result.entities).toHaveLength(1);
|
|
801
|
-
});
|
|
802
|
-
|
|
803
|
-
it('searches store with embedded query vector', async () => {
|
|
804
|
-
mockStore.search.mockResolvedValue([{
|
|
805
|
-
id: 'k1', text: 'test knowledge', type: 'decision', layer: 'personal',
|
|
806
|
-
confidence: 0.8, stale_score: 0, created: new Date().toISOString(),
|
|
807
|
-
source_timestamp: new Date().toISOString(), evidence_score: 0.7,
|
|
808
|
-
contradiction_refs: [], _distance: 0.2,
|
|
809
|
-
workspace_id: null, session_id: null, agent_type: 'claude',
|
|
810
|
-
project_path: null, file_refs: [], access_count: 0, last_accessed: null,
|
|
811
|
-
metadata: {},
|
|
812
|
-
}]);
|
|
813
|
-
|
|
814
|
-
const result = await engine.assemble('test query');
|
|
815
|
-
expect(result.results.length).toBeGreaterThanOrEqual(1);
|
|
816
|
-
expect(result.context).toContain('<cortex-context>');
|
|
817
|
-
});
|
|
818
|
-
|
|
819
|
-
it('completes within performance budget', async () => {
|
|
820
|
-
const start = Date.now();
|
|
821
|
-
await engine.assemble('test query');
|
|
822
|
-
const elapsed = Date.now() - start;
|
|
823
|
-
expect(elapsed).toBeLessThan(500); // generous for CI, target is 150ms
|
|
824
|
-
});
|
|
825
|
-
});
|
|
826
|
-
```
|
|
827
|
-
|
|
828
|
-
- [ ] **Step 2: Implement ContextEngine**
|
|
829
|
-
|
|
830
|
-
```typescript
|
|
831
|
-
// src/lib/cortex/retrieval/context-engine.ts
|
|
832
|
-
import type { CortexStore } from '../store';
|
|
833
|
-
import type { EntityGraph } from '../graph/entity-graph';
|
|
834
|
-
import type { EntityResolver, ResolvedEntity } from '../graph/resolver';
|
|
835
|
-
import type { EmbeddingProvider } from '../embeddings';
|
|
836
|
-
import type { ScoredKnowledge } from '../knowledge/types';
|
|
837
|
-
import { detectIntent } from './intent';
|
|
838
|
-
import type { IntentResult } from './intent';
|
|
839
|
-
import { computeScopeWeight, computeTypeBoost } from './weight';
|
|
840
|
-
import { detectConflicts } from './conflict';
|
|
841
|
-
import type { ConflictPair } from './conflict';
|
|
842
|
-
import { formatContext } from './formatter';
|
|
843
|
-
import { computeRelevanceScore } from './scoring';
|
|
844
|
-
|
|
845
|
-
export interface ContextEngineDeps {
|
|
846
|
-
store: CortexStore;
|
|
847
|
-
graph: EntityGraph;
|
|
848
|
-
resolver: EntityResolver;
|
|
849
|
-
embedding: EmbeddingProvider;
|
|
850
|
-
requesterId: string; // entity ID of the person making the query
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
export interface AssemblyResult {
|
|
854
|
-
results: ScoredKnowledge[];
|
|
855
|
-
conflicts: ConflictPair[];
|
|
856
|
-
context: string; // formatted <cortex-context> string
|
|
857
|
-
intent: IntentResult;
|
|
858
|
-
entities: ResolvedEntity[];
|
|
859
|
-
timing: {
|
|
860
|
-
intentMs: number;
|
|
861
|
-
entityMs: number;
|
|
862
|
-
searchMs: number;
|
|
863
|
-
totalMs: number;
|
|
864
|
-
};
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
interface SearchSource {
|
|
868
|
-
layerKey: string;
|
|
869
|
-
weight: number;
|
|
870
|
-
limit: number;
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
const DEFAULT_LAYERS = ['personal', 'workspace', 'team'] as const;
|
|
874
|
-
const SEARCH_TIMEOUT_MS = 100;
|
|
875
|
-
|
|
876
|
-
export class ContextEngine {
|
|
877
|
-
constructor(private deps: ContextEngineDeps) {}
|
|
878
|
-
|
|
879
|
-
async assemble(query: string, options: { limit?: number; workspaceId?: number | null; maxTokens?: number } = {}): Promise<AssemblyResult> {
|
|
880
|
-
const totalStart = Date.now();
|
|
881
|
-
const { limit = 5, workspaceId = null, maxTokens = 1500 } = options;
|
|
882
|
-
|
|
883
|
-
// Stage 1: Intent Detection
|
|
884
|
-
const intentStart = Date.now();
|
|
885
|
-
const intent = detectIntent(query);
|
|
886
|
-
const intentMs = Date.now() - intentStart;
|
|
887
|
-
|
|
888
|
-
// Stage 2: Entity Resolution
|
|
889
|
-
const entityStart = Date.now();
|
|
890
|
-
const entities = this.deps.resolver.extractEntities(query);
|
|
891
|
-
const entityMs = Date.now() - entityStart;
|
|
892
|
-
|
|
893
|
-
// Embed the query
|
|
894
|
-
const [queryVector] = await this.deps.embedding.embed([query]);
|
|
895
|
-
|
|
896
|
-
// Stage 3: Weight Computation
|
|
897
|
-
const sources = this.computeSourceWeights(intent, workspaceId);
|
|
898
|
-
|
|
899
|
-
// Stage 4: Parallel Multi-Source Search
|
|
900
|
-
const searchStart = Date.now();
|
|
901
|
-
const allResults = await this.parallelSearch(queryVector, sources, limit);
|
|
902
|
-
const searchMs = Date.now() - searchStart;
|
|
903
|
-
|
|
904
|
-
// Stage 5: Fusion + Re-Ranking
|
|
905
|
-
const fused = this.fuseAndRank(allResults, intent, limit);
|
|
906
|
-
|
|
907
|
-
// Stage 6: Conflict Detection + Formatting
|
|
908
|
-
const conflicts = detectConflicts(fused);
|
|
909
|
-
const context = formatContext(fused, conflicts, { maxTokens });
|
|
910
|
-
|
|
911
|
-
return {
|
|
912
|
-
results: fused,
|
|
913
|
-
conflicts,
|
|
914
|
-
context,
|
|
915
|
-
intent,
|
|
916
|
-
entities,
|
|
917
|
-
timing: {
|
|
918
|
-
intentMs,
|
|
919
|
-
entityMs,
|
|
920
|
-
searchMs,
|
|
921
|
-
totalMs: Date.now() - totalStart,
|
|
922
|
-
},
|
|
923
|
-
};
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
private computeSourceWeights(intent: IntentResult, workspaceId: number | null): SearchSource[] {
|
|
927
|
-
const sources: SearchSource[] = [];
|
|
928
|
-
|
|
929
|
-
for (const layer of DEFAULT_LAYERS) {
|
|
930
|
-
const layerKey = layer === 'workspace' && workspaceId
|
|
931
|
-
? `workspace/${workspaceId}` : layer;
|
|
932
|
-
|
|
933
|
-
// Map v1 layer to scope level for intent bias lookup
|
|
934
|
-
const scopeLevel = layer === 'personal' ? 'personal'
|
|
935
|
-
: layer === 'workspace' ? 'team' : 'organization';
|
|
936
|
-
|
|
937
|
-
// Use graph proximity if available, else fall back to fixed weights
|
|
938
|
-
let graphProximity: number;
|
|
939
|
-
try {
|
|
940
|
-
// For personal, distance is 0; for workspace, 1; for team, 2
|
|
941
|
-
const layerEntity = layer === 'personal' ? this.deps.requesterId
|
|
942
|
-
: layer === 'workspace' ? 'team-default' : 'organization-default';
|
|
943
|
-
graphProximity = this.deps.graph.proximity(this.deps.requesterId, layerEntity);
|
|
944
|
-
} catch {
|
|
945
|
-
// Graph not populated, use fallback
|
|
946
|
-
graphProximity = layer === 'personal' ? 1.0 : layer === 'workspace' ? 0.5 : 0.33;
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
// If graph returns 0 (unreachable/not found), use fallback
|
|
950
|
-
if (graphProximity === 0) {
|
|
951
|
-
graphProximity = layer === 'personal' ? 1.0 : layer === 'workspace' ? 0.5 : 0.33;
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
const weight = computeScopeWeight({
|
|
955
|
-
graphProximity,
|
|
956
|
-
scopeLevel,
|
|
957
|
-
intentBiases: intent.biases,
|
|
958
|
-
authorityFactor: 1.0,
|
|
959
|
-
});
|
|
960
|
-
|
|
961
|
-
sources.push({
|
|
962
|
-
layerKey,
|
|
963
|
-
weight,
|
|
964
|
-
limit: Math.max(3, Math.round(weight * 10)), // proportional slots
|
|
965
|
-
});
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
return sources.sort((a, b) => b.weight - a.weight);
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
private async parallelSearch(
|
|
972
|
-
queryVector: number[],
|
|
973
|
-
sources: SearchSource[],
|
|
974
|
-
limit: number,
|
|
975
|
-
): Promise<Array<ScoredKnowledge & { sourceWeight: number }>> {
|
|
976
|
-
const searchPromises = sources.map(async (source) => {
|
|
977
|
-
try {
|
|
978
|
-
const results = await Promise.race([
|
|
979
|
-
this.deps.store.search(source.layerKey, queryVector, source.limit),
|
|
980
|
-
new Promise<never>((_, reject) =>
|
|
981
|
-
setTimeout(() => reject(new Error('timeout')), SEARCH_TIMEOUT_MS)
|
|
982
|
-
),
|
|
983
|
-
]);
|
|
984
|
-
|
|
985
|
-
return results.map(unit => {
|
|
986
|
-
const similarity = 1 - ((unit as any)._distance ?? 0);
|
|
987
|
-
return {
|
|
988
|
-
...unit,
|
|
989
|
-
similarity,
|
|
990
|
-
relevance_score: 0, // computed in fusion
|
|
991
|
-
sourceWeight: source.weight,
|
|
992
|
-
} as ScoredKnowledge & { sourceWeight: number };
|
|
993
|
-
});
|
|
994
|
-
} catch {
|
|
995
|
-
return []; // source failed or timed out
|
|
996
|
-
}
|
|
997
|
-
});
|
|
998
|
-
|
|
999
|
-
const settled = await Promise.allSettled(searchPromises);
|
|
1000
|
-
const allResults: Array<ScoredKnowledge & { sourceWeight: number }> = [];
|
|
1001
|
-
|
|
1002
|
-
for (const result of settled) {
|
|
1003
|
-
if (result.status === 'fulfilled') {
|
|
1004
|
-
allResults.push(...result.value);
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
return allResults;
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
private fuseAndRank(
|
|
1012
|
-
results: Array<ScoredKnowledge & { sourceWeight: number }>,
|
|
1013
|
-
intent: IntentResult,
|
|
1014
|
-
limit: number,
|
|
1015
|
-
): ScoredKnowledge[] {
|
|
1016
|
-
// Score each result
|
|
1017
|
-
for (const result of results) {
|
|
1018
|
-
const typeBoost = computeTypeBoost(result.type, intent.biases);
|
|
1019
|
-
const recencyBoost = intent.biases.recency_boost;
|
|
1020
|
-
|
|
1021
|
-
result.relevance_score = computeRelevanceScore({
|
|
1022
|
-
similarity: result.similarity,
|
|
1023
|
-
confidence: result.confidence,
|
|
1024
|
-
stale_score: result.stale_score,
|
|
1025
|
-
created: result.created,
|
|
1026
|
-
evidence_score: result.evidence_score,
|
|
1027
|
-
}) * result.sourceWeight * typeBoost * recencyBoost;
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
// Deduplicate: cosine > 0.9 between results (approximate via text similarity)
|
|
1031
|
-
const deduped = this.deduplicateResults(results);
|
|
1032
|
-
|
|
1033
|
-
// Sort and take top K
|
|
1034
|
-
deduped.sort((a, b) => b.relevance_score - a.relevance_score);
|
|
1035
|
-
return deduped.slice(0, limit);
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
private deduplicateResults(results: ScoredKnowledge[]): ScoredKnowledge[] {
|
|
1039
|
-
const kept: ScoredKnowledge[] = [];
|
|
1040
|
-
const seenTexts = new Set<string>();
|
|
1041
|
-
|
|
1042
|
-
// Sort by score first so we keep the better-scored version
|
|
1043
|
-
results.sort((a, b) => b.relevance_score - a.relevance_score);
|
|
1044
|
-
|
|
1045
|
-
for (const result of results) {
|
|
1046
|
-
// Simple text-based dedup: normalize and check prefix overlap
|
|
1047
|
-
const normalized = result.text.slice(0, 200).toLowerCase().trim();
|
|
1048
|
-
if (seenTexts.has(normalized)) continue;
|
|
1049
|
-
|
|
1050
|
-
// Check against existing kept items for high text overlap
|
|
1051
|
-
let isDupe = false;
|
|
1052
|
-
for (const existing of kept) {
|
|
1053
|
-
if (result.id === existing.id) { isDupe = true; break; }
|
|
1054
|
-
// If texts share >80% of content, consider duplicate
|
|
1055
|
-
const existNorm = existing.text.slice(0, 200).toLowerCase().trim();
|
|
1056
|
-
if (normalized === existNorm) { isDupe = true; break; }
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
if (!isDupe) {
|
|
1060
|
-
seenTexts.add(normalized);
|
|
1061
|
-
kept.push(result);
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
return kept;
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
```
|
|
1069
|
-
|
|
1070
|
-
- [ ] **Step 3: Run tests to verify they pass**
|
|
1071
|
-
|
|
1072
|
-
Run: `npx vitest run tests/lib/cortex/retrieval/context-engine.test.ts`
|
|
1073
|
-
Expected: PASS (6 tests)
|
|
1074
|
-
|
|
1075
|
-
- [ ] **Step 4: Commit**
|
|
1076
|
-
|
|
1077
|
-
```bash
|
|
1078
|
-
git add src/lib/cortex/retrieval/context-engine.ts tests/lib/cortex/retrieval/context-engine.test.ts
|
|
1079
|
-
git commit -m "feat(cortex): add ContextEngine with 6-stage retrieval pipeline"
|
|
1080
|
-
```
|
|
1081
|
-
|
|
1082
|
-
---
|
|
1083
|
-
|
|
1084
|
-
## Chunk 4: API Endpoint and Hook Integration
|
|
1085
|
-
|
|
1086
|
-
### Task 6: Context assembly API endpoint
|
|
1087
|
-
|
|
1088
|
-
**Files:**
|
|
1089
|
-
- Create: `src/app/api/cortex/context/route.ts`
|
|
1090
|
-
|
|
1091
|
-
- [ ] **Step 1: Create the endpoint**
|
|
1092
|
-
|
|
1093
|
-
```typescript
|
|
1094
|
-
// src/app/api/cortex/context/route.ts
|
|
1095
|
-
import { NextResponse } from 'next/server';
|
|
1096
|
-
import type { NextRequest } from 'next/server';
|
|
1097
|
-
import { getAuthUser, withUser } from '@/lib/auth';
|
|
1098
|
-
import { getCortex, isCortexAvailable } from '@/lib/cortex';
|
|
1099
|
-
import { ContextEngine } from '@/lib/cortex/retrieval/context-engine';
|
|
1100
|
-
import { EntityResolver } from '@/lib/cortex/graph/resolver';
|
|
1101
|
-
import { slugify } from '@/lib/cortex/graph/types';
|
|
1102
|
-
|
|
1103
|
-
export async function GET(request: NextRequest) {
|
|
1104
|
-
const user = getAuthUser(request);
|
|
1105
|
-
return withUser(user, async () => {
|
|
1106
|
-
if (!isCortexAvailable()) {
|
|
1107
|
-
return NextResponse.json({ error: 'Cortex unavailable' }, { status: 403 });
|
|
1108
|
-
}
|
|
1109
|
-
const cortex = await getCortex();
|
|
1110
|
-
if (!cortex) return NextResponse.json({ results: [], context: '' });
|
|
1111
|
-
|
|
1112
|
-
const url = new URL(request.url);
|
|
1113
|
-
const query = url.searchParams.get('q') || '';
|
|
1114
|
-
const limit = parseInt(url.searchParams.get('limit') || '5', 10);
|
|
1115
|
-
const workspaceId = url.searchParams.get('workspace_id');
|
|
1116
|
-
const maxTokens = parseInt(url.searchParams.get('max_tokens') || '1500', 10);
|
|
1117
|
-
|
|
1118
|
-
if (!query || query.length < 3) {
|
|
1119
|
-
return NextResponse.json({ results: [], context: '' });
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
const resolver = new EntityResolver(cortex.graph);
|
|
1123
|
-
const requesterId = `person-${slugify(user)}`;
|
|
1124
|
-
|
|
1125
|
-
const engine = new ContextEngine({
|
|
1126
|
-
store: cortex.store,
|
|
1127
|
-
graph: cortex.graph,
|
|
1128
|
-
resolver,
|
|
1129
|
-
embedding: cortex.embedding,
|
|
1130
|
-
requesterId,
|
|
1131
|
-
});
|
|
1132
|
-
|
|
1133
|
-
const result = await engine.assemble(query, {
|
|
1134
|
-
limit,
|
|
1135
|
-
workspaceId: workspaceId ? parseInt(workspaceId, 10) : null,
|
|
1136
|
-
maxTokens,
|
|
1137
|
-
});
|
|
1138
|
-
|
|
1139
|
-
return NextResponse.json({
|
|
1140
|
-
results: result.results.map(r => ({ ...r, vector: undefined })), // strip vectors
|
|
1141
|
-
context: result.context,
|
|
1142
|
-
intent: result.intent,
|
|
1143
|
-
conflicts: result.conflicts.length,
|
|
1144
|
-
timing: result.timing,
|
|
1145
|
-
});
|
|
1146
|
-
});
|
|
1147
|
-
}
|
|
1148
|
-
```
|
|
1149
|
-
|
|
1150
|
-
- [ ] **Step 2: Commit**
|
|
1151
|
-
|
|
1152
|
-
```bash
|
|
1153
|
-
git add src/app/api/cortex/context/route.ts
|
|
1154
|
-
git commit -m "feat(cortex): add context assembly API endpoint"
|
|
1155
|
-
```
|
|
1156
|
-
|
|
1157
|
-
---
|
|
1158
|
-
|
|
1159
|
-
### Task 7: Update RAG hook to use context assembly endpoint
|
|
1160
|
-
|
|
1161
|
-
**Files:**
|
|
1162
|
-
- Modify: `bin/cortex-hook.js`
|
|
1163
|
-
|
|
1164
|
-
- [ ] **Step 1: Read current cortex-hook.js**
|
|
1165
|
-
|
|
1166
|
-
Read the file to understand the current flow: query → /api/cortex/search → format results → output.
|
|
1167
|
-
|
|
1168
|
-
- [ ] **Step 2: Update to call context assembly endpoint**
|
|
1169
|
-
|
|
1170
|
-
Change the URL from `/api/cortex/search/?q=...` to `/api/cortex/context/?q=...`. The new endpoint returns `{ context, results, intent, conflicts, timing }` — the `context` field is already pre-formatted as `<cortex-context>`, so the hook can output it directly instead of doing its own formatting.
|
|
1171
|
-
|
|
1172
|
-
Key changes:
|
|
1173
|
-
1. URL: `/api/cortex/search/` → `/api/cortex/context/`
|
|
1174
|
-
2. Response handling: use `parsed.context` directly instead of formatting results manually
|
|
1175
|
-
3. Keep the fallback: if the new endpoint returns empty or fails, fall back to old behavior
|
|
1176
|
-
|
|
1177
|
-
```javascript
|
|
1178
|
-
// Replace the response handling section:
|
|
1179
|
-
const parsed = JSON.parse(body);
|
|
1180
|
-
|
|
1181
|
-
// New: context is pre-formatted by the Context Assembly Engine
|
|
1182
|
-
if (parsed.context) {
|
|
1183
|
-
const output = JSON.stringify({
|
|
1184
|
-
hookSpecificOutput: {
|
|
1185
|
-
hookEventName: 'UserPromptSubmit',
|
|
1186
|
-
additionalContext: parsed.context,
|
|
1187
|
-
},
|
|
1188
|
-
});
|
|
1189
|
-
process.stdout.write(output);
|
|
1190
|
-
process.exit(0);
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
// Fallback: old-style results formatting (if context endpoint not available)
|
|
1194
|
-
const results = parsed.results;
|
|
1195
|
-
if (!results || results.length === 0) process.exit(0);
|
|
1196
|
-
// ... existing formatting code stays as fallback ...
|
|
1197
|
-
```
|
|
1198
|
-
|
|
1199
|
-
- [ ] **Step 3: Commit**
|
|
1200
|
-
|
|
1201
|
-
```bash
|
|
1202
|
-
git add bin/cortex-hook.js
|
|
1203
|
-
git commit -m "feat(cortex): switch RAG hook to context assembly endpoint"
|
|
1204
|
-
```
|
|
1205
|
-
|
|
1206
|
-
---
|
|
1207
|
-
|
|
1208
|
-
### Task 8: Integrate ContextEngine into CortexInstance
|
|
1209
|
-
|
|
1210
|
-
**Files:**
|
|
1211
|
-
- Modify: `src/lib/cortex/index.ts`
|
|
1212
|
-
|
|
1213
|
-
- [ ] **Step 1: Read current index.ts**
|
|
1214
|
-
|
|
1215
|
-
- [ ] **Step 2: Add ContextEngine to CortexInstance**
|
|
1216
|
-
|
|
1217
|
-
1. Import: `import { ContextEngine } from './retrieval/context-engine';` and `import { EntityResolver } from './graph/resolver';`
|
|
1218
|
-
2. Add `contextEngine?: ContextEngine` to `CortexInstance` interface (optional since it depends on graph)
|
|
1219
|
-
3. In `getCortex()`, after graph initialization, create the ContextEngine:
|
|
1220
|
-
|
|
1221
|
-
```typescript
|
|
1222
|
-
const resolver = new EntityResolver(graph);
|
|
1223
|
-
const contextEngine = new ContextEngine({
|
|
1224
|
-
store,
|
|
1225
|
-
graph,
|
|
1226
|
-
resolver,
|
|
1227
|
-
embedding,
|
|
1228
|
-
requesterId: 'person-default-user', // default; overridden per-request in API
|
|
1229
|
-
});
|
|
1230
|
-
```
|
|
1231
|
-
|
|
1232
|
-
4. Add to instance object: `contextEngine,`
|
|
1233
|
-
|
|
1234
|
-
- [ ] **Step 3: Run full cortex test suite**
|
|
1235
|
-
|
|
1236
|
-
Run: `npx vitest run tests/lib/cortex/`
|
|
1237
|
-
|
|
1238
|
-
- [ ] **Step 4: Commit**
|
|
1239
|
-
|
|
1240
|
-
```bash
|
|
1241
|
-
git add src/lib/cortex/index.ts
|
|
1242
|
-
git commit -m "feat(cortex): add ContextEngine to CortexInstance"
|
|
1243
|
-
```
|
|
1244
|
-
|
|
1245
|
-
---
|
|
1246
|
-
|
|
1247
|
-
## Summary
|
|
1248
|
-
|
|
1249
|
-
| Task | Component | Tests | Status |
|
|
1250
|
-
|------|-----------|-------|--------|
|
|
1251
|
-
| 1 | Intent detection | 7 | |
|
|
1252
|
-
| 2 | Weight computation | 5 | |
|
|
1253
|
-
| 3 | Conflict detection | 4 | |
|
|
1254
|
-
| 4 | Context formatter | 6 | |
|
|
1255
|
-
| 5 | ContextEngine (6-stage pipeline) | 6 | |
|
|
1256
|
-
| 6 | Context assembly API endpoint | — | |
|
|
1257
|
-
| 7 | RAG hook integration | — | |
|
|
1258
|
-
| 8 | CortexInstance integration | regression | |
|
|
1259
|
-
|
|
1260
|
-
**Total: 8 tasks, ~28 new tests, 4 chunks**
|
|
1261
|
-
|
|
1262
|
-
**Performance budget:** The ContextEngine targets <150ms total latency:
|
|
1263
|
-
- Intent detection: <5ms (regex, no LLM)
|
|
1264
|
-
- Entity resolution: <5ms (alias lookup)
|
|
1265
|
-
- Weight computation: <10ms (graph proximity, cached)
|
|
1266
|
-
- Parallel search: <100ms (concurrent vector search with 100ms timeout)
|
|
1267
|
-
- Fusion + formatting: <10ms
|
|
1268
|
-
|
|
1269
|
-
**Key design decisions:**
|
|
1270
|
-
- ContextEngine sits ABOVE CortexSearch — doesn't replace it, wraps it
|
|
1271
|
-
- Graph proximity drives weights — but gracefully degrades to fixed weights when graph is empty
|
|
1272
|
-
- Deduplication uses text prefix comparison (not cosine between result vectors — that would require N² vector ops)
|
|
1273
|
-
- RAG hook uses pre-formatted `context` from the engine — no more client-side formatting
|
|
1274
|
-
- Old `/api/cortex/search` endpoint still works — new `/api/cortex/context` endpoint adds the intelligence layer
|
|
1
|
+
# Cortex v2 — Pillar 3: Context Assembly Engine
|
|
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:** Replace the flat layer-iteration search with a 6-stage context assembly pipeline that detects intent, resolves entities, computes graph-aware weights, searches multiple scopes in parallel, fuses results with evidence scoring, and surfaces conflicts — all within 150ms.
|
|
6
|
+
|
|
7
|
+
**Architecture:** A new `ContextEngine` class wraps the existing `CortexStore` (for low-level vector search) and `EntityGraph` (for graph distance/proximity). It does NOT replace `CortexSearch` — instead it provides the higher-level retrieval interface that the RAG hook calls. The existing `CortexSearch` remains for backward-compatible simple searches. The RAG hook (`cortex-hook.js`) switches from calling the search API to calling a new context-assembly API endpoint.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript, vitest, LanceDB, SQLite (entity graph)
|
|
10
|
+
|
|
11
|
+
**Spec:** `docs/superpowers/specs/2026-03-14-cortex-v2-design.md` — Pillar 3
|
|
12
|
+
|
|
13
|
+
**Depends on:** Pillar 1 (Entity Graph) + Pillar 2 (Knowledge Unit Evolution) — both completed
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## File Structure
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
New files:
|
|
21
|
+
├── src/lib/cortex/retrieval/intent.ts — Intent detection (regex + keyword)
|
|
22
|
+
├── src/lib/cortex/retrieval/weight.ts — Weight computation (graph × intent × freshness × authority)
|
|
23
|
+
├── src/lib/cortex/retrieval/conflict.ts — Conflict detection in results
|
|
24
|
+
├── src/lib/cortex/retrieval/formatter.ts — Context formatting for RAG injection
|
|
25
|
+
├── src/lib/cortex/retrieval/context-engine.ts — Main 6-stage ContextEngine class
|
|
26
|
+
├── src/app/api/cortex/context/route.ts — API endpoint for context assembly
|
|
27
|
+
|
|
28
|
+
Modified files:
|
|
29
|
+
├── bin/cortex-hook.js — Switch to context-assembly endpoint
|
|
30
|
+
|
|
31
|
+
Test files:
|
|
32
|
+
├── tests/lib/cortex/retrieval/intent.test.ts
|
|
33
|
+
├── tests/lib/cortex/retrieval/weight.test.ts
|
|
34
|
+
├── tests/lib/cortex/retrieval/conflict.test.ts
|
|
35
|
+
├── tests/lib/cortex/retrieval/formatter.test.ts
|
|
36
|
+
├── tests/lib/cortex/retrieval/context-engine.test.ts
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Chunk 1: Intent Detection and Weight Computation
|
|
42
|
+
|
|
43
|
+
### Task 1: Intent detection
|
|
44
|
+
|
|
45
|
+
**Files:**
|
|
46
|
+
- Create: `src/lib/cortex/retrieval/intent.ts`
|
|
47
|
+
- Create: `tests/lib/cortex/retrieval/intent.test.ts`
|
|
48
|
+
|
|
49
|
+
- [ ] **Step 1: Write failing tests**
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// tests/lib/cortex/retrieval/intent.test.ts
|
|
53
|
+
import { describe, it, expect } from 'vitest';
|
|
54
|
+
import { detectIntent, INTENTS } from '@/lib/cortex/retrieval/intent';
|
|
55
|
+
import type { IntentResult } from '@/lib/cortex/retrieval/intent';
|
|
56
|
+
|
|
57
|
+
describe('detectIntent', () => {
|
|
58
|
+
it('detects debugging intent', () => {
|
|
59
|
+
const result = detectIntent('why does the auth service throw a timeout error?');
|
|
60
|
+
expect(result.intent).toBe('debugging');
|
|
61
|
+
expect(result.confidence).toBeGreaterThan(0.5);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('detects architecture intent', () => {
|
|
65
|
+
const result = detectIntent('what architecture pattern should we use for the new service?');
|
|
66
|
+
expect(result.intent).toBe('architecture');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('detects how-to intent', () => {
|
|
70
|
+
const result = detectIntent('how do I deploy this service to production?');
|
|
71
|
+
expect(result.intent).toBe('how-to');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('detects security intent', () => {
|
|
75
|
+
const result = detectIntent('is there a vulnerability in our authentication flow?');
|
|
76
|
+
expect(result.intent).toBe('security');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('defaults to general for ambiguous queries', () => {
|
|
80
|
+
const result = detectIntent('tell me about the project');
|
|
81
|
+
expect(result.intent).toBe('general');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('returns bias config for the detected intent', () => {
|
|
85
|
+
const result = detectIntent('fix this bug in the login page');
|
|
86
|
+
expect(result.biases).toBeDefined();
|
|
87
|
+
expect(result.biases.scope_boost).toBeDefined();
|
|
88
|
+
expect(result.biases.type_boost).toBeDefined();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('exports all intent definitions', () => {
|
|
92
|
+
expect(Object.keys(INTENTS)).toContain('debugging');
|
|
93
|
+
expect(Object.keys(INTENTS)).toContain('architecture');
|
|
94
|
+
expect(Object.keys(INTENTS)).toContain('general');
|
|
95
|
+
expect(Object.keys(INTENTS).length).toBe(8);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
- [ ] **Step 2: Run tests to verify they fail**
|
|
101
|
+
|
|
102
|
+
Run: `npx vitest run tests/lib/cortex/retrieval/intent.test.ts`
|
|
103
|
+
|
|
104
|
+
- [ ] **Step 3: Implement intent detection**
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
// src/lib/cortex/retrieval/intent.ts
|
|
108
|
+
|
|
109
|
+
export interface IntentBiases {
|
|
110
|
+
scope_boost: Record<string, number>; // scope level → multiplier
|
|
111
|
+
type_boost: Record<string, number>; // knowledge type → multiplier
|
|
112
|
+
recency_boost: number; // extra recency multiplier
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface IntentResult {
|
|
116
|
+
intent: string;
|
|
117
|
+
confidence: number;
|
|
118
|
+
biases: IntentBiases;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface IntentDef {
|
|
122
|
+
patterns: RegExp[];
|
|
123
|
+
keywords: string[];
|
|
124
|
+
biases: IntentBiases;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const INTENTS: Record<string, IntentDef> = {
|
|
128
|
+
debugging: {
|
|
129
|
+
patterns: [
|
|
130
|
+
/\b(error|bug|fix|crash|fail|broken|throw|exception|timeout|issue)\b/i,
|
|
131
|
+
/\bwhy\s+(does|is|did|do)\b/i,
|
|
132
|
+
/\bnot\s+work/i,
|
|
133
|
+
],
|
|
134
|
+
keywords: ['error', 'bug', 'fix', 'debug', 'crash', 'fail', 'broken', 'throw', 'exception', 'timeout', 'issue', 'stack trace'],
|
|
135
|
+
biases: {
|
|
136
|
+
scope_boost: { personal: 1.2, team: 1.0, department: 0.9, organization: 0.8 },
|
|
137
|
+
type_boost: { error_fix: 1.3, pattern: 1.0, decision: 0.8, conversation: 0.7 },
|
|
138
|
+
recency_boost: 1.1,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
architecture: {
|
|
142
|
+
patterns: [
|
|
143
|
+
/\b(architect|design|pattern|structure|approach)\b/i,
|
|
144
|
+
/\bshould\s+we\s+(use|adopt|switch|migrate)\b/i,
|
|
145
|
+
],
|
|
146
|
+
keywords: ['architecture', 'design', 'pattern', 'structure', 'approach', 'decision', 'migration', 'refactor'],
|
|
147
|
+
biases: {
|
|
148
|
+
scope_boost: { personal: 0.9, team: 1.1, department: 1.2, organization: 1.0 },
|
|
149
|
+
type_boost: { decision: 1.5, pattern: 1.2, error_fix: 0.7, conversation: 0.5 },
|
|
150
|
+
recency_boost: 1.0,
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
onboarding: {
|
|
154
|
+
patterns: [
|
|
155
|
+
/\b(how\s+does|explain|what\s+is|overview|getting\s+started)\b/i,
|
|
156
|
+
/\bnew\s+to\b/i,
|
|
157
|
+
],
|
|
158
|
+
keywords: ['explain', 'overview', 'introduction', 'getting started', 'onboarding', 'how does'],
|
|
159
|
+
biases: {
|
|
160
|
+
scope_boost: { personal: 0.7, team: 1.0, department: 1.1, organization: 1.2 },
|
|
161
|
+
type_boost: { pattern: 1.3, decision: 1.2, summary: 1.2, conversation: 0.5 },
|
|
162
|
+
recency_boost: 0.9,
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
policy: {
|
|
166
|
+
patterns: [
|
|
167
|
+
/\b(policy|compliance|regulation|standard|rule|requirement)\b/i,
|
|
168
|
+
/\ballowed\s+to\b/i,
|
|
169
|
+
],
|
|
170
|
+
keywords: ['policy', 'compliance', 'standard', 'regulation', 'rule', 'requirement', 'allowed'],
|
|
171
|
+
biases: {
|
|
172
|
+
scope_boost: { personal: 0.6, team: 0.8, department: 1.0, organization: 1.3 },
|
|
173
|
+
type_boost: { decision: 1.5, preference: 1.2, pattern: 0.8, conversation: 0.3 },
|
|
174
|
+
recency_boost: 1.0,
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
'how-to': {
|
|
178
|
+
patterns: [
|
|
179
|
+
/\bhow\s+(do|can|to|should)\s+I?\b/i,
|
|
180
|
+
/\bsteps?\s+(to|for)\b/i,
|
|
181
|
+
/\bwhat('s| is)\s+the\s+(command|way|process)\b/i,
|
|
182
|
+
],
|
|
183
|
+
keywords: ['how to', 'steps', 'command', 'run', 'deploy', 'install', 'configure', 'setup'],
|
|
184
|
+
biases: {
|
|
185
|
+
scope_boost: { personal: 1.2, team: 1.0, department: 0.8, organization: 0.7 },
|
|
186
|
+
type_boost: { command: 1.3, pattern: 1.2, error_fix: 1.0, conversation: 0.6 },
|
|
187
|
+
recency_boost: 1.05,
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
review: {
|
|
191
|
+
patterns: [
|
|
192
|
+
/\b(review|feedback|improve|quality|best\s+practice)\b/i,
|
|
193
|
+
/\bis\s+this\s+(good|correct|right)\b/i,
|
|
194
|
+
],
|
|
195
|
+
keywords: ['review', 'feedback', 'quality', 'improve', 'best practice', 'convention'],
|
|
196
|
+
biases: {
|
|
197
|
+
scope_boost: { personal: 0.9, team: 1.2, department: 1.0, organization: 0.8 },
|
|
198
|
+
type_boost: { preference: 1.3, pattern: 1.2, code_pattern: 1.2, decision: 1.0 },
|
|
199
|
+
recency_boost: 1.0,
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
security: {
|
|
203
|
+
patterns: [
|
|
204
|
+
/\b(security|vulnerab|exploit|attack|auth|cve|injection|xss)\b/i,
|
|
205
|
+
/\bsecure\b/i,
|
|
206
|
+
],
|
|
207
|
+
keywords: ['security', 'vulnerability', 'exploit', 'attack', 'authentication', 'authorization', 'cve', 'injection', 'xss', 'csrf'],
|
|
208
|
+
biases: {
|
|
209
|
+
scope_boost: { personal: 0.8, team: 1.0, department: 1.2, organization: 1.0 },
|
|
210
|
+
type_boost: { error_fix: 1.3, decision: 1.2, pattern: 1.0, conversation: 0.5 },
|
|
211
|
+
recency_boost: 1.1,
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
general: {
|
|
215
|
+
patterns: [],
|
|
216
|
+
keywords: [],
|
|
217
|
+
biases: {
|
|
218
|
+
scope_boost: { personal: 1.0, team: 1.0, department: 1.0, organization: 1.0 },
|
|
219
|
+
type_boost: {},
|
|
220
|
+
recency_boost: 1.0,
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Detect the intent of a query using regex patterns and keyword scoring.
|
|
227
|
+
* No LLM call — fast and deterministic.
|
|
228
|
+
*/
|
|
229
|
+
export function detectIntent(query: string): IntentResult {
|
|
230
|
+
const lower = query.toLowerCase();
|
|
231
|
+
let bestIntent = 'general';
|
|
232
|
+
let bestScore = 0;
|
|
233
|
+
|
|
234
|
+
for (const [name, def] of Object.entries(INTENTS)) {
|
|
235
|
+
if (name === 'general') continue;
|
|
236
|
+
|
|
237
|
+
let score = 0;
|
|
238
|
+
|
|
239
|
+
// Regex pattern matches (high weight)
|
|
240
|
+
for (const pattern of def.patterns) {
|
|
241
|
+
if (pattern.test(query)) score += 2;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Keyword matches (lower weight)
|
|
245
|
+
for (const keyword of def.keywords) {
|
|
246
|
+
if (lower.includes(keyword)) score += 1;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (score > bestScore) {
|
|
250
|
+
bestScore = score;
|
|
251
|
+
bestIntent = name;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
intent: bestIntent,
|
|
257
|
+
confidence: bestScore > 0 ? Math.min(1.0, bestScore / 6) : 0.5,
|
|
258
|
+
biases: INTENTS[bestIntent].biases,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
- [ ] **Step 4: Run tests to verify they pass**
|
|
264
|
+
|
|
265
|
+
Run: `npx vitest run tests/lib/cortex/retrieval/intent.test.ts`
|
|
266
|
+
Expected: PASS (7 tests)
|
|
267
|
+
|
|
268
|
+
- [ ] **Step 5: Commit**
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
git add src/lib/cortex/retrieval/intent.ts tests/lib/cortex/retrieval/intent.test.ts
|
|
272
|
+
git commit -m "feat(cortex): add intent detection for context assembly"
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
### Task 2: Weight computation
|
|
278
|
+
|
|
279
|
+
**Files:**
|
|
280
|
+
- Create: `src/lib/cortex/retrieval/weight.ts`
|
|
281
|
+
- Create: `tests/lib/cortex/retrieval/weight.test.ts`
|
|
282
|
+
|
|
283
|
+
- [ ] **Step 1: Write failing tests**
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
// tests/lib/cortex/retrieval/weight.test.ts
|
|
287
|
+
import { describe, it, expect } from 'vitest';
|
|
288
|
+
import { computeScopeWeight } from '@/lib/cortex/retrieval/weight';
|
|
289
|
+
import type { IntentBiases } from '@/lib/cortex/retrieval/intent';
|
|
290
|
+
|
|
291
|
+
describe('computeScopeWeight', () => {
|
|
292
|
+
const neutralBiases: IntentBiases = {
|
|
293
|
+
scope_boost: { personal: 1.0, team: 1.0, department: 1.0, organization: 1.0 },
|
|
294
|
+
type_boost: {},
|
|
295
|
+
recency_boost: 1.0,
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
it('returns 1.0 for self (distance 0)', () => {
|
|
299
|
+
const weight = computeScopeWeight({
|
|
300
|
+
graphProximity: 1.0, // 1/(1+0)
|
|
301
|
+
scopeLevel: 'personal',
|
|
302
|
+
intentBiases: neutralBiases,
|
|
303
|
+
authorityFactor: 1.0,
|
|
304
|
+
});
|
|
305
|
+
expect(weight).toBeCloseTo(1.0);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('decreases with graph distance', () => {
|
|
309
|
+
const close = computeScopeWeight({
|
|
310
|
+
graphProximity: 0.5, // distance 1
|
|
311
|
+
scopeLevel: 'team',
|
|
312
|
+
intentBiases: neutralBiases,
|
|
313
|
+
authorityFactor: 1.0,
|
|
314
|
+
});
|
|
315
|
+
const far = computeScopeWeight({
|
|
316
|
+
graphProximity: 0.25, // distance 3
|
|
317
|
+
scopeLevel: 'organization',
|
|
318
|
+
intentBiases: neutralBiases,
|
|
319
|
+
authorityFactor: 1.0,
|
|
320
|
+
});
|
|
321
|
+
expect(close).toBeGreaterThan(far);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('is boosted by intent biases', () => {
|
|
325
|
+
const debugBiases: IntentBiases = {
|
|
326
|
+
scope_boost: { personal: 1.2, team: 0.8 },
|
|
327
|
+
type_boost: {},
|
|
328
|
+
recency_boost: 1.0,
|
|
329
|
+
};
|
|
330
|
+
const personal = computeScopeWeight({
|
|
331
|
+
graphProximity: 0.5,
|
|
332
|
+
scopeLevel: 'personal',
|
|
333
|
+
intentBiases: debugBiases,
|
|
334
|
+
authorityFactor: 1.0,
|
|
335
|
+
});
|
|
336
|
+
const team = computeScopeWeight({
|
|
337
|
+
graphProximity: 0.5,
|
|
338
|
+
scopeLevel: 'team',
|
|
339
|
+
intentBiases: debugBiases,
|
|
340
|
+
authorityFactor: 1.0,
|
|
341
|
+
});
|
|
342
|
+
expect(personal).toBeGreaterThan(team);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('is boosted by authority factor', () => {
|
|
346
|
+
const low = computeScopeWeight({
|
|
347
|
+
graphProximity: 0.5,
|
|
348
|
+
scopeLevel: 'team',
|
|
349
|
+
intentBiases: neutralBiases,
|
|
350
|
+
authorityFactor: 1.0,
|
|
351
|
+
});
|
|
352
|
+
const high = computeScopeWeight({
|
|
353
|
+
graphProximity: 0.5,
|
|
354
|
+
scopeLevel: 'team',
|
|
355
|
+
intentBiases: neutralBiases,
|
|
356
|
+
authorityFactor: 1.2,
|
|
357
|
+
});
|
|
358
|
+
expect(high).toBeGreaterThan(low);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('never returns negative', () => {
|
|
362
|
+
const weight = computeScopeWeight({
|
|
363
|
+
graphProximity: 0,
|
|
364
|
+
scopeLevel: 'organization',
|
|
365
|
+
intentBiases: neutralBiases,
|
|
366
|
+
authorityFactor: 1.0,
|
|
367
|
+
});
|
|
368
|
+
expect(weight).toBeGreaterThanOrEqual(0);
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
- [ ] **Step 2: Run tests to verify they fail**
|
|
374
|
+
|
|
375
|
+
Run: `npx vitest run tests/lib/cortex/retrieval/weight.test.ts`
|
|
376
|
+
|
|
377
|
+
- [ ] **Step 3: Implement weight computation**
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
// src/lib/cortex/retrieval/weight.ts
|
|
381
|
+
import type { IntentBiases } from './intent';
|
|
382
|
+
import type { ScopeLevel } from '../knowledge/types';
|
|
383
|
+
|
|
384
|
+
export interface ScopeWeightInput {
|
|
385
|
+
graphProximity: number; // 0-1, from EntityGraph.proximity()
|
|
386
|
+
scopeLevel: ScopeLevel | string;
|
|
387
|
+
intentBiases: IntentBiases;
|
|
388
|
+
authorityFactor: number; // 1.0 default, higher for experts/docs
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Compute the retrieval weight for a knowledge scope.
|
|
393
|
+
*
|
|
394
|
+
* weight = graphProximity × intentBias × authorityFactor
|
|
395
|
+
*
|
|
396
|
+
* Per spec: weight(scope) = graph_proximity × intent_bias × freshness_bonus × authority
|
|
397
|
+
* Freshness is applied per-result in the fusion stage, not per-scope.
|
|
398
|
+
*/
|
|
399
|
+
export function computeScopeWeight(input: ScopeWeightInput): number {
|
|
400
|
+
const { graphProximity, scopeLevel, intentBiases, authorityFactor } = input;
|
|
401
|
+
|
|
402
|
+
const intentBias = intentBiases.scope_boost[scopeLevel] ?? 1.0;
|
|
403
|
+
|
|
404
|
+
return Math.max(0, graphProximity * intentBias * authorityFactor);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Compute per-result type boost from intent biases.
|
|
409
|
+
*/
|
|
410
|
+
export function computeTypeBoost(knowledgeType: string, intentBiases: IntentBiases): number {
|
|
411
|
+
return intentBiases.type_boost[knowledgeType] ?? 1.0;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Authority factor for a source based on role and expertise.
|
|
416
|
+
*
|
|
417
|
+
* role_boost: 0.0 member, 0.1 lead, 0.15 senior/principal, 0.2 director+
|
|
418
|
+
* expertise_weight: EXPERT_IN edge weight (0-1)
|
|
419
|
+
* Documents get 1.2 base authority.
|
|
420
|
+
*/
|
|
421
|
+
export function computeAuthority(params: {
|
|
422
|
+
role?: string;
|
|
423
|
+
expertiseWeight?: number;
|
|
424
|
+
isDocument?: boolean;
|
|
425
|
+
}): number {
|
|
426
|
+
const { role, expertiseWeight = 0, isDocument = false } = params;
|
|
427
|
+
|
|
428
|
+
if (isDocument) return 1.2;
|
|
429
|
+
|
|
430
|
+
const roleBoosts: Record<string, number> = {
|
|
431
|
+
member: 0.0,
|
|
432
|
+
lead: 0.1,
|
|
433
|
+
senior: 0.15,
|
|
434
|
+
principal: 0.15,
|
|
435
|
+
director: 0.2,
|
|
436
|
+
vp: 0.2,
|
|
437
|
+
cto: 0.2,
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const roleBoost = roleBoosts[role?.toLowerCase() ?? 'member'] ?? 0.0;
|
|
441
|
+
return Math.max(1.0, roleBoost + expertiseWeight);
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
- [ ] **Step 4: Run tests to verify they pass**
|
|
446
|
+
|
|
447
|
+
Run: `npx vitest run tests/lib/cortex/retrieval/weight.test.ts`
|
|
448
|
+
Expected: PASS (5 tests)
|
|
449
|
+
|
|
450
|
+
- [ ] **Step 5: Commit**
|
|
451
|
+
|
|
452
|
+
```bash
|
|
453
|
+
git add src/lib/cortex/retrieval/weight.ts tests/lib/cortex/retrieval/weight.test.ts
|
|
454
|
+
git commit -m "feat(cortex): add scope weight computation for context assembly"
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
## Chunk 2: Conflict Detection and Context Formatting
|
|
460
|
+
|
|
461
|
+
### Task 3: Conflict detection
|
|
462
|
+
|
|
463
|
+
**Files:**
|
|
464
|
+
- Create: `src/lib/cortex/retrieval/conflict.ts`
|
|
465
|
+
- Create: `tests/lib/cortex/retrieval/conflict.test.ts`
|
|
466
|
+
|
|
467
|
+
- [ ] **Step 1: Write failing tests**
|
|
468
|
+
|
|
469
|
+
```typescript
|
|
470
|
+
// tests/lib/cortex/retrieval/conflict.test.ts
|
|
471
|
+
import { describe, it, expect } from 'vitest';
|
|
472
|
+
import { detectConflicts } from '@/lib/cortex/retrieval/conflict';
|
|
473
|
+
import type { ScoredKnowledge } from '@/lib/cortex/knowledge/types';
|
|
474
|
+
|
|
475
|
+
function makeResult(overrides: Partial<ScoredKnowledge> = {}): ScoredKnowledge {
|
|
476
|
+
return {
|
|
477
|
+
id: 'r1', vector: [], text: 'test', type: 'decision', layer: 'personal',
|
|
478
|
+
workspace_id: null, session_id: null, agent_type: 'claude',
|
|
479
|
+
project_path: null, file_refs: [], confidence: 0.8,
|
|
480
|
+
created: new Date().toISOString(), source_timestamp: new Date().toISOString(),
|
|
481
|
+
stale_score: 0, access_count: 0, last_accessed: null, metadata: {},
|
|
482
|
+
relevance_score: 0.9, similarity: 0.9,
|
|
483
|
+
contradiction_refs: [], ...overrides,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
describe('detectConflicts', () => {
|
|
488
|
+
it('returns no conflicts when no contradiction_refs', () => {
|
|
489
|
+
const results = [makeResult({ id: 'a' }), makeResult({ id: 'b' })];
|
|
490
|
+
const conflicts = detectConflicts(results);
|
|
491
|
+
expect(conflicts).toHaveLength(0);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('detects conflict between two results', () => {
|
|
495
|
+
const results = [
|
|
496
|
+
makeResult({ id: 'a', text: 'use pool size 50', contradiction_refs: ['b'] }),
|
|
497
|
+
makeResult({ id: 'b', text: 'scale horizontally', contradiction_refs: ['a'] }),
|
|
498
|
+
];
|
|
499
|
+
const conflicts = detectConflicts(results);
|
|
500
|
+
expect(conflicts).toHaveLength(1);
|
|
501
|
+
expect(conflicts[0].unitA.id).toBe('a');
|
|
502
|
+
expect(conflicts[0].unitB.id).toBe('b');
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('ignores contradiction_refs pointing to results not in the set', () => {
|
|
506
|
+
const results = [
|
|
507
|
+
makeResult({ id: 'a', contradiction_refs: ['z'] }), // z is not in results
|
|
508
|
+
];
|
|
509
|
+
const conflicts = detectConflicts(results);
|
|
510
|
+
expect(conflicts).toHaveLength(0);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it('deduplicates symmetric conflicts', () => {
|
|
514
|
+
// A contradicts B and B contradicts A should produce one conflict, not two
|
|
515
|
+
const results = [
|
|
516
|
+
makeResult({ id: 'a', contradiction_refs: ['b'] }),
|
|
517
|
+
makeResult({ id: 'b', contradiction_refs: ['a'] }),
|
|
518
|
+
];
|
|
519
|
+
const conflicts = detectConflicts(results);
|
|
520
|
+
expect(conflicts).toHaveLength(1);
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
- [ ] **Step 2: Implement conflict detection**
|
|
526
|
+
|
|
527
|
+
```typescript
|
|
528
|
+
// src/lib/cortex/retrieval/conflict.ts
|
|
529
|
+
import type { ScoredKnowledge } from '../knowledge/types';
|
|
530
|
+
|
|
531
|
+
export interface ConflictPair {
|
|
532
|
+
unitA: ScoredKnowledge;
|
|
533
|
+
unitB: ScoredKnowledge;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Detect conflicts among search results by checking contradiction_refs.
|
|
538
|
+
* Returns deduplicated conflict pairs (A↔B counted once, not twice).
|
|
539
|
+
*/
|
|
540
|
+
export function detectConflicts(results: ScoredKnowledge[]): ConflictPair[] {
|
|
541
|
+
const resultMap = new Map(results.map(r => [r.id, r]));
|
|
542
|
+
const seen = new Set<string>();
|
|
543
|
+
const conflicts: ConflictPair[] = [];
|
|
544
|
+
|
|
545
|
+
for (const result of results) {
|
|
546
|
+
const refs = result.contradiction_refs ?? [];
|
|
547
|
+
for (const refId of refs) {
|
|
548
|
+
const other = resultMap.get(refId);
|
|
549
|
+
if (!other) continue;
|
|
550
|
+
|
|
551
|
+
const key = [result.id, refId].sort().join('|');
|
|
552
|
+
if (seen.has(key)) continue;
|
|
553
|
+
seen.add(key);
|
|
554
|
+
|
|
555
|
+
conflicts.push({ unitA: result, unitB: other });
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return conflicts;
|
|
560
|
+
}
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
- [ ] **Step 3: Run tests, commit**
|
|
564
|
+
|
|
565
|
+
Run: `npx vitest run tests/lib/cortex/retrieval/conflict.test.ts`
|
|
566
|
+
|
|
567
|
+
```bash
|
|
568
|
+
git add src/lib/cortex/retrieval/conflict.ts tests/lib/cortex/retrieval/conflict.test.ts
|
|
569
|
+
git commit -m "feat(cortex): add conflict detection for search results"
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
---
|
|
573
|
+
|
|
574
|
+
### Task 4: Context formatter
|
|
575
|
+
|
|
576
|
+
**Files:**
|
|
577
|
+
- Create: `src/lib/cortex/retrieval/formatter.ts`
|
|
578
|
+
- Create: `tests/lib/cortex/retrieval/formatter.test.ts`
|
|
579
|
+
|
|
580
|
+
- [ ] **Step 1: Write failing tests**
|
|
581
|
+
|
|
582
|
+
```typescript
|
|
583
|
+
// tests/lib/cortex/retrieval/formatter.test.ts
|
|
584
|
+
import { describe, it, expect } from 'vitest';
|
|
585
|
+
import { formatContext } from '@/lib/cortex/retrieval/formatter';
|
|
586
|
+
import type { ScoredKnowledge } from '@/lib/cortex/knowledge/types';
|
|
587
|
+
import type { ConflictPair } from '@/lib/cortex/retrieval/conflict';
|
|
588
|
+
|
|
589
|
+
function makeResult(overrides: Partial<ScoredKnowledge> = {}): ScoredKnowledge {
|
|
590
|
+
return {
|
|
591
|
+
id: 'r1', vector: [], text: 'test knowledge', type: 'decision', layer: 'personal',
|
|
592
|
+
workspace_id: null, session_id: null, agent_type: 'claude',
|
|
593
|
+
project_path: null, file_refs: [], confidence: 0.8,
|
|
594
|
+
created: '2026-03-15T00:00:00.000Z', source_timestamp: '2026-03-15T00:00:00.000Z',
|
|
595
|
+
stale_score: 0, access_count: 5, last_accessed: null, metadata: {},
|
|
596
|
+
relevance_score: 0.9, similarity: 0.9,
|
|
597
|
+
...overrides,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
describe('formatContext', () => {
|
|
602
|
+
it('wraps results in cortex-context tags', () => {
|
|
603
|
+
const output = formatContext([makeResult()], []);
|
|
604
|
+
expect(output).toContain('<cortex-context>');
|
|
605
|
+
expect(output).toContain('</cortex-context>');
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it('includes type labels and dates', () => {
|
|
609
|
+
const output = formatContext([makeResult({ type: 'error_fix', source_timestamp: '2026-03-10T00:00:00.000Z' })], []);
|
|
610
|
+
expect(output).toContain('[Error Fix]');
|
|
611
|
+
expect(output).toContain('2026-03-10');
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
it('includes source attribution when origin is present', () => {
|
|
615
|
+
const output = formatContext([makeResult({
|
|
616
|
+
origin: { source_type: 'conversation', source_ref: 'sess-1', creator_entity_id: 'person-alice' },
|
|
617
|
+
})], []);
|
|
618
|
+
expect(output).toContain('person-alice');
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it('includes conflict callout when conflicts exist', () => {
|
|
622
|
+
const a = makeResult({ id: 'a', text: 'use pool size 50' });
|
|
623
|
+
const b = makeResult({ id: 'b', text: 'scale horizontally' });
|
|
624
|
+
const conflicts: ConflictPair[] = [{ unitA: a, unitB: b }];
|
|
625
|
+
const output = formatContext([a, b], conflicts);
|
|
626
|
+
expect(output).toContain('Conflicting');
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it('respects max token budget', () => {
|
|
630
|
+
const bigResults = Array.from({ length: 20 }, (_, i) =>
|
|
631
|
+
makeResult({ id: `r${i}`, text: 'x'.repeat(500) })
|
|
632
|
+
);
|
|
633
|
+
const output = formatContext(bigResults, [], { maxTokens: 500 });
|
|
634
|
+
// Should not include all 20 results (would be ~2500 tokens)
|
|
635
|
+
expect(output.length).toBeLessThan(3000);
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it('returns empty string when no results', () => {
|
|
639
|
+
expect(formatContext([], [])).toBe('');
|
|
640
|
+
});
|
|
641
|
+
});
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
- [ ] **Step 2: Implement context formatter**
|
|
645
|
+
|
|
646
|
+
```typescript
|
|
647
|
+
// src/lib/cortex/retrieval/formatter.ts
|
|
648
|
+
import type { ScoredKnowledge } from '../knowledge/types';
|
|
649
|
+
import type { ConflictPair } from './conflict';
|
|
650
|
+
|
|
651
|
+
const TYPE_LABELS: Record<string, string> = {
|
|
652
|
+
decision: 'Decision', pattern: 'Pattern', preference: 'Preference',
|
|
653
|
+
error_fix: 'Error Fix', context: 'Context', code_pattern: 'Code',
|
|
654
|
+
command: 'Command', conversation: 'Conversation', summary: 'Summary',
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
export interface FormatOptions {
|
|
658
|
+
maxTokens?: number;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Format search results + conflicts as annotated <cortex-context> for RAG injection.
|
|
663
|
+
*/
|
|
664
|
+
export function formatContext(
|
|
665
|
+
results: ScoredKnowledge[],
|
|
666
|
+
conflicts: ConflictPair[],
|
|
667
|
+
options: FormatOptions = {},
|
|
668
|
+
): string {
|
|
669
|
+
if (results.length === 0) return '';
|
|
670
|
+
|
|
671
|
+
const maxTokens = options.maxTokens ?? 1500;
|
|
672
|
+
const entries: string[] = [];
|
|
673
|
+
let tokens = 20; // overhead for tags
|
|
674
|
+
|
|
675
|
+
// Format each result with attribution
|
|
676
|
+
for (const unit of results) {
|
|
677
|
+
const label = TYPE_LABELS[unit.type] || unit.type;
|
|
678
|
+
const date = (unit.source_timestamp || '').slice(0, 10);
|
|
679
|
+
const creator = unit.origin?.creator_entity_id ?? '';
|
|
680
|
+
const sourceInfo = creator ? ` (${creator})` : '';
|
|
681
|
+
|
|
682
|
+
let entry = `[${label}] ${date}${sourceInfo}:\n ${unit.text}`;
|
|
683
|
+
|
|
684
|
+
const entryTokens = Math.ceil(entry.length / 4);
|
|
685
|
+
if (tokens + entryTokens > maxTokens) break;
|
|
686
|
+
|
|
687
|
+
entries.push(entry);
|
|
688
|
+
tokens += entryTokens;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (entries.length === 0) return '';
|
|
692
|
+
|
|
693
|
+
// Build conflict section
|
|
694
|
+
let conflictSection = '';
|
|
695
|
+
if (conflicts.length > 0) {
|
|
696
|
+
const conflictLines = conflicts.map(c =>
|
|
697
|
+
` - "${c.unitA.text.slice(0, 80)}..." vs "${c.unitB.text.slice(0, 80)}..."`
|
|
698
|
+
);
|
|
699
|
+
conflictSection = `\nConflicting perspectives (${conflicts.length}):\n${conflictLines.join('\n')}\n`;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const sourceCount = entries.length;
|
|
703
|
+
const header = `Relevant knowledge (${sourceCount} source${sourceCount > 1 ? 's' : ''}${conflicts.length > 0 ? `, ${conflicts.length} conflict${conflicts.length > 1 ? 's' : ''}` : ''}):`;
|
|
704
|
+
|
|
705
|
+
return [
|
|
706
|
+
'<cortex-context>',
|
|
707
|
+
header,
|
|
708
|
+
'',
|
|
709
|
+
...entries,
|
|
710
|
+
conflictSection,
|
|
711
|
+
'</cortex-context>',
|
|
712
|
+
].join('\n');
|
|
713
|
+
}
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
- [ ] **Step 3: Run tests, commit**
|
|
717
|
+
|
|
718
|
+
Run: `npx vitest run tests/lib/cortex/retrieval/formatter.test.ts`
|
|
719
|
+
|
|
720
|
+
```bash
|
|
721
|
+
git add src/lib/cortex/retrieval/formatter.ts tests/lib/cortex/retrieval/formatter.test.ts
|
|
722
|
+
git commit -m "feat(cortex): add context formatter for RAG injection"
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
---
|
|
726
|
+
|
|
727
|
+
## Chunk 3: Context Assembly Engine
|
|
728
|
+
|
|
729
|
+
### Task 5: ContextEngine — the 6-stage pipeline
|
|
730
|
+
|
|
731
|
+
**Files:**
|
|
732
|
+
- Create: `src/lib/cortex/retrieval/context-engine.ts`
|
|
733
|
+
- Create: `tests/lib/cortex/retrieval/context-engine.test.ts`
|
|
734
|
+
|
|
735
|
+
- [ ] **Step 1: Write failing tests**
|
|
736
|
+
|
|
737
|
+
```typescript
|
|
738
|
+
// tests/lib/cortex/retrieval/context-engine.test.ts
|
|
739
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
740
|
+
import { ContextEngine } from '@/lib/cortex/retrieval/context-engine';
|
|
741
|
+
|
|
742
|
+
// Create mocks for dependencies
|
|
743
|
+
const mockStore = {
|
|
744
|
+
search: vi.fn().mockResolvedValue([]),
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
const mockGraph = {
|
|
748
|
+
proximity: vi.fn().mockReturnValue(0.5),
|
|
749
|
+
neighborhood: vi.fn().mockReturnValue([]),
|
|
750
|
+
getEntity: vi.fn().mockReturnValue(null),
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
const mockResolver = {
|
|
754
|
+
extractEntities: vi.fn().mockReturnValue([]),
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
const mockEmbedding = {
|
|
758
|
+
embed: vi.fn().mockResolvedValue([[0.1, 0.2, 0.3]]),
|
|
759
|
+
dimensions: 3,
|
|
760
|
+
name: 'mock',
|
|
761
|
+
init: vi.fn(),
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
describe('ContextEngine', () => {
|
|
765
|
+
let engine: ContextEngine;
|
|
766
|
+
|
|
767
|
+
beforeEach(() => {
|
|
768
|
+
vi.clearAllMocks();
|
|
769
|
+
engine = new ContextEngine({
|
|
770
|
+
store: mockStore as any,
|
|
771
|
+
graph: mockGraph as any,
|
|
772
|
+
resolver: mockResolver as any,
|
|
773
|
+
embedding: mockEmbedding as any,
|
|
774
|
+
requesterId: 'person-alice',
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
it('returns empty context for empty results', async () => {
|
|
779
|
+
const result = await engine.assemble('some query');
|
|
780
|
+
expect(result.results).toHaveLength(0);
|
|
781
|
+
expect(result.context).toBe('');
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
it('calls embedding.embed with the query', async () => {
|
|
785
|
+
await engine.assemble('test query');
|
|
786
|
+
expect(mockEmbedding.embed).toHaveBeenCalledWith(['test query']);
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
it('detects intent from the query', async () => {
|
|
790
|
+
const result = await engine.assemble('why does auth throw an error?');
|
|
791
|
+
expect(result.intent.intent).toBe('debugging');
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
it('extracts entities from the query', async () => {
|
|
795
|
+
mockResolver.extractEntities.mockReturnValue([
|
|
796
|
+
{ entity: { id: 'system-auth', type: 'system', name: 'Auth' }, confidence: 0.9, method: 'alias' },
|
|
797
|
+
]);
|
|
798
|
+
const result = await engine.assemble('fix the auth service');
|
|
799
|
+
expect(mockResolver.extractEntities).toHaveBeenCalledWith('fix the auth service');
|
|
800
|
+
expect(result.entities).toHaveLength(1);
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
it('searches store with embedded query vector', async () => {
|
|
804
|
+
mockStore.search.mockResolvedValue([{
|
|
805
|
+
id: 'k1', text: 'test knowledge', type: 'decision', layer: 'personal',
|
|
806
|
+
confidence: 0.8, stale_score: 0, created: new Date().toISOString(),
|
|
807
|
+
source_timestamp: new Date().toISOString(), evidence_score: 0.7,
|
|
808
|
+
contradiction_refs: [], _distance: 0.2,
|
|
809
|
+
workspace_id: null, session_id: null, agent_type: 'claude',
|
|
810
|
+
project_path: null, file_refs: [], access_count: 0, last_accessed: null,
|
|
811
|
+
metadata: {},
|
|
812
|
+
}]);
|
|
813
|
+
|
|
814
|
+
const result = await engine.assemble('test query');
|
|
815
|
+
expect(result.results.length).toBeGreaterThanOrEqual(1);
|
|
816
|
+
expect(result.context).toContain('<cortex-context>');
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
it('completes within performance budget', async () => {
|
|
820
|
+
const start = Date.now();
|
|
821
|
+
await engine.assemble('test query');
|
|
822
|
+
const elapsed = Date.now() - start;
|
|
823
|
+
expect(elapsed).toBeLessThan(500); // generous for CI, target is 150ms
|
|
824
|
+
});
|
|
825
|
+
});
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
- [ ] **Step 2: Implement ContextEngine**
|
|
829
|
+
|
|
830
|
+
```typescript
|
|
831
|
+
// src/lib/cortex/retrieval/context-engine.ts
|
|
832
|
+
import type { CortexStore } from '../store';
|
|
833
|
+
import type { EntityGraph } from '../graph/entity-graph';
|
|
834
|
+
import type { EntityResolver, ResolvedEntity } from '../graph/resolver';
|
|
835
|
+
import type { EmbeddingProvider } from '../embeddings';
|
|
836
|
+
import type { ScoredKnowledge } from '../knowledge/types';
|
|
837
|
+
import { detectIntent } from './intent';
|
|
838
|
+
import type { IntentResult } from './intent';
|
|
839
|
+
import { computeScopeWeight, computeTypeBoost } from './weight';
|
|
840
|
+
import { detectConflicts } from './conflict';
|
|
841
|
+
import type { ConflictPair } from './conflict';
|
|
842
|
+
import { formatContext } from './formatter';
|
|
843
|
+
import { computeRelevanceScore } from './scoring';
|
|
844
|
+
|
|
845
|
+
export interface ContextEngineDeps {
|
|
846
|
+
store: CortexStore;
|
|
847
|
+
graph: EntityGraph;
|
|
848
|
+
resolver: EntityResolver;
|
|
849
|
+
embedding: EmbeddingProvider;
|
|
850
|
+
requesterId: string; // entity ID of the person making the query
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
export interface AssemblyResult {
|
|
854
|
+
results: ScoredKnowledge[];
|
|
855
|
+
conflicts: ConflictPair[];
|
|
856
|
+
context: string; // formatted <cortex-context> string
|
|
857
|
+
intent: IntentResult;
|
|
858
|
+
entities: ResolvedEntity[];
|
|
859
|
+
timing: {
|
|
860
|
+
intentMs: number;
|
|
861
|
+
entityMs: number;
|
|
862
|
+
searchMs: number;
|
|
863
|
+
totalMs: number;
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
interface SearchSource {
|
|
868
|
+
layerKey: string;
|
|
869
|
+
weight: number;
|
|
870
|
+
limit: number;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
const DEFAULT_LAYERS = ['personal', 'workspace', 'team'] as const;
|
|
874
|
+
const SEARCH_TIMEOUT_MS = 100;
|
|
875
|
+
|
|
876
|
+
export class ContextEngine {
|
|
877
|
+
constructor(private deps: ContextEngineDeps) {}
|
|
878
|
+
|
|
879
|
+
async assemble(query: string, options: { limit?: number; workspaceId?: number | null; maxTokens?: number } = {}): Promise<AssemblyResult> {
|
|
880
|
+
const totalStart = Date.now();
|
|
881
|
+
const { limit = 5, workspaceId = null, maxTokens = 1500 } = options;
|
|
882
|
+
|
|
883
|
+
// Stage 1: Intent Detection
|
|
884
|
+
const intentStart = Date.now();
|
|
885
|
+
const intent = detectIntent(query);
|
|
886
|
+
const intentMs = Date.now() - intentStart;
|
|
887
|
+
|
|
888
|
+
// Stage 2: Entity Resolution
|
|
889
|
+
const entityStart = Date.now();
|
|
890
|
+
const entities = this.deps.resolver.extractEntities(query);
|
|
891
|
+
const entityMs = Date.now() - entityStart;
|
|
892
|
+
|
|
893
|
+
// Embed the query
|
|
894
|
+
const [queryVector] = await this.deps.embedding.embed([query]);
|
|
895
|
+
|
|
896
|
+
// Stage 3: Weight Computation
|
|
897
|
+
const sources = this.computeSourceWeights(intent, workspaceId);
|
|
898
|
+
|
|
899
|
+
// Stage 4: Parallel Multi-Source Search
|
|
900
|
+
const searchStart = Date.now();
|
|
901
|
+
const allResults = await this.parallelSearch(queryVector, sources, limit);
|
|
902
|
+
const searchMs = Date.now() - searchStart;
|
|
903
|
+
|
|
904
|
+
// Stage 5: Fusion + Re-Ranking
|
|
905
|
+
const fused = this.fuseAndRank(allResults, intent, limit);
|
|
906
|
+
|
|
907
|
+
// Stage 6: Conflict Detection + Formatting
|
|
908
|
+
const conflicts = detectConflicts(fused);
|
|
909
|
+
const context = formatContext(fused, conflicts, { maxTokens });
|
|
910
|
+
|
|
911
|
+
return {
|
|
912
|
+
results: fused,
|
|
913
|
+
conflicts,
|
|
914
|
+
context,
|
|
915
|
+
intent,
|
|
916
|
+
entities,
|
|
917
|
+
timing: {
|
|
918
|
+
intentMs,
|
|
919
|
+
entityMs,
|
|
920
|
+
searchMs,
|
|
921
|
+
totalMs: Date.now() - totalStart,
|
|
922
|
+
},
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
private computeSourceWeights(intent: IntentResult, workspaceId: number | null): SearchSource[] {
|
|
927
|
+
const sources: SearchSource[] = [];
|
|
928
|
+
|
|
929
|
+
for (const layer of DEFAULT_LAYERS) {
|
|
930
|
+
const layerKey = layer === 'workspace' && workspaceId
|
|
931
|
+
? `workspace/${workspaceId}` : layer;
|
|
932
|
+
|
|
933
|
+
// Map v1 layer to scope level for intent bias lookup
|
|
934
|
+
const scopeLevel = layer === 'personal' ? 'personal'
|
|
935
|
+
: layer === 'workspace' ? 'team' : 'organization';
|
|
936
|
+
|
|
937
|
+
// Use graph proximity if available, else fall back to fixed weights
|
|
938
|
+
let graphProximity: number;
|
|
939
|
+
try {
|
|
940
|
+
// For personal, distance is 0; for workspace, 1; for team, 2
|
|
941
|
+
const layerEntity = layer === 'personal' ? this.deps.requesterId
|
|
942
|
+
: layer === 'workspace' ? 'team-default' : 'organization-default';
|
|
943
|
+
graphProximity = this.deps.graph.proximity(this.deps.requesterId, layerEntity);
|
|
944
|
+
} catch {
|
|
945
|
+
// Graph not populated, use fallback
|
|
946
|
+
graphProximity = layer === 'personal' ? 1.0 : layer === 'workspace' ? 0.5 : 0.33;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// If graph returns 0 (unreachable/not found), use fallback
|
|
950
|
+
if (graphProximity === 0) {
|
|
951
|
+
graphProximity = layer === 'personal' ? 1.0 : layer === 'workspace' ? 0.5 : 0.33;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
const weight = computeScopeWeight({
|
|
955
|
+
graphProximity,
|
|
956
|
+
scopeLevel,
|
|
957
|
+
intentBiases: intent.biases,
|
|
958
|
+
authorityFactor: 1.0,
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
sources.push({
|
|
962
|
+
layerKey,
|
|
963
|
+
weight,
|
|
964
|
+
limit: Math.max(3, Math.round(weight * 10)), // proportional slots
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
return sources.sort((a, b) => b.weight - a.weight);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
private async parallelSearch(
|
|
972
|
+
queryVector: number[],
|
|
973
|
+
sources: SearchSource[],
|
|
974
|
+
limit: number,
|
|
975
|
+
): Promise<Array<ScoredKnowledge & { sourceWeight: number }>> {
|
|
976
|
+
const searchPromises = sources.map(async (source) => {
|
|
977
|
+
try {
|
|
978
|
+
const results = await Promise.race([
|
|
979
|
+
this.deps.store.search(source.layerKey, queryVector, source.limit),
|
|
980
|
+
new Promise<never>((_, reject) =>
|
|
981
|
+
setTimeout(() => reject(new Error('timeout')), SEARCH_TIMEOUT_MS)
|
|
982
|
+
),
|
|
983
|
+
]);
|
|
984
|
+
|
|
985
|
+
return results.map(unit => {
|
|
986
|
+
const similarity = 1 - ((unit as any)._distance ?? 0);
|
|
987
|
+
return {
|
|
988
|
+
...unit,
|
|
989
|
+
similarity,
|
|
990
|
+
relevance_score: 0, // computed in fusion
|
|
991
|
+
sourceWeight: source.weight,
|
|
992
|
+
} as ScoredKnowledge & { sourceWeight: number };
|
|
993
|
+
});
|
|
994
|
+
} catch {
|
|
995
|
+
return []; // source failed or timed out
|
|
996
|
+
}
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
const settled = await Promise.allSettled(searchPromises);
|
|
1000
|
+
const allResults: Array<ScoredKnowledge & { sourceWeight: number }> = [];
|
|
1001
|
+
|
|
1002
|
+
for (const result of settled) {
|
|
1003
|
+
if (result.status === 'fulfilled') {
|
|
1004
|
+
allResults.push(...result.value);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
return allResults;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
private fuseAndRank(
|
|
1012
|
+
results: Array<ScoredKnowledge & { sourceWeight: number }>,
|
|
1013
|
+
intent: IntentResult,
|
|
1014
|
+
limit: number,
|
|
1015
|
+
): ScoredKnowledge[] {
|
|
1016
|
+
// Score each result
|
|
1017
|
+
for (const result of results) {
|
|
1018
|
+
const typeBoost = computeTypeBoost(result.type, intent.biases);
|
|
1019
|
+
const recencyBoost = intent.biases.recency_boost;
|
|
1020
|
+
|
|
1021
|
+
result.relevance_score = computeRelevanceScore({
|
|
1022
|
+
similarity: result.similarity,
|
|
1023
|
+
confidence: result.confidence,
|
|
1024
|
+
stale_score: result.stale_score,
|
|
1025
|
+
created: result.created,
|
|
1026
|
+
evidence_score: result.evidence_score,
|
|
1027
|
+
}) * result.sourceWeight * typeBoost * recencyBoost;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Deduplicate: cosine > 0.9 between results (approximate via text similarity)
|
|
1031
|
+
const deduped = this.deduplicateResults(results);
|
|
1032
|
+
|
|
1033
|
+
// Sort and take top K
|
|
1034
|
+
deduped.sort((a, b) => b.relevance_score - a.relevance_score);
|
|
1035
|
+
return deduped.slice(0, limit);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
private deduplicateResults(results: ScoredKnowledge[]): ScoredKnowledge[] {
|
|
1039
|
+
const kept: ScoredKnowledge[] = [];
|
|
1040
|
+
const seenTexts = new Set<string>();
|
|
1041
|
+
|
|
1042
|
+
// Sort by score first so we keep the better-scored version
|
|
1043
|
+
results.sort((a, b) => b.relevance_score - a.relevance_score);
|
|
1044
|
+
|
|
1045
|
+
for (const result of results) {
|
|
1046
|
+
// Simple text-based dedup: normalize and check prefix overlap
|
|
1047
|
+
const normalized = result.text.slice(0, 200).toLowerCase().trim();
|
|
1048
|
+
if (seenTexts.has(normalized)) continue;
|
|
1049
|
+
|
|
1050
|
+
// Check against existing kept items for high text overlap
|
|
1051
|
+
let isDupe = false;
|
|
1052
|
+
for (const existing of kept) {
|
|
1053
|
+
if (result.id === existing.id) { isDupe = true; break; }
|
|
1054
|
+
// If texts share >80% of content, consider duplicate
|
|
1055
|
+
const existNorm = existing.text.slice(0, 200).toLowerCase().trim();
|
|
1056
|
+
if (normalized === existNorm) { isDupe = true; break; }
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
if (!isDupe) {
|
|
1060
|
+
seenTexts.add(normalized);
|
|
1061
|
+
kept.push(result);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
return kept;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
```
|
|
1069
|
+
|
|
1070
|
+
- [ ] **Step 3: Run tests to verify they pass**
|
|
1071
|
+
|
|
1072
|
+
Run: `npx vitest run tests/lib/cortex/retrieval/context-engine.test.ts`
|
|
1073
|
+
Expected: PASS (6 tests)
|
|
1074
|
+
|
|
1075
|
+
- [ ] **Step 4: Commit**
|
|
1076
|
+
|
|
1077
|
+
```bash
|
|
1078
|
+
git add src/lib/cortex/retrieval/context-engine.ts tests/lib/cortex/retrieval/context-engine.test.ts
|
|
1079
|
+
git commit -m "feat(cortex): add ContextEngine with 6-stage retrieval pipeline"
|
|
1080
|
+
```
|
|
1081
|
+
|
|
1082
|
+
---
|
|
1083
|
+
|
|
1084
|
+
## Chunk 4: API Endpoint and Hook Integration
|
|
1085
|
+
|
|
1086
|
+
### Task 6: Context assembly API endpoint
|
|
1087
|
+
|
|
1088
|
+
**Files:**
|
|
1089
|
+
- Create: `src/app/api/cortex/context/route.ts`
|
|
1090
|
+
|
|
1091
|
+
- [ ] **Step 1: Create the endpoint**
|
|
1092
|
+
|
|
1093
|
+
```typescript
|
|
1094
|
+
// src/app/api/cortex/context/route.ts
|
|
1095
|
+
import { NextResponse } from 'next/server';
|
|
1096
|
+
import type { NextRequest } from 'next/server';
|
|
1097
|
+
import { getAuthUser, withUser } from '@/lib/auth';
|
|
1098
|
+
import { getCortex, isCortexAvailable } from '@/lib/cortex';
|
|
1099
|
+
import { ContextEngine } from '@/lib/cortex/retrieval/context-engine';
|
|
1100
|
+
import { EntityResolver } from '@/lib/cortex/graph/resolver';
|
|
1101
|
+
import { slugify } from '@/lib/cortex/graph/types';
|
|
1102
|
+
|
|
1103
|
+
export async function GET(request: NextRequest) {
|
|
1104
|
+
const user = getAuthUser(request);
|
|
1105
|
+
return withUser(user, async () => {
|
|
1106
|
+
if (!isCortexAvailable()) {
|
|
1107
|
+
return NextResponse.json({ error: 'Cortex unavailable' }, { status: 403 });
|
|
1108
|
+
}
|
|
1109
|
+
const cortex = await getCortex();
|
|
1110
|
+
if (!cortex) return NextResponse.json({ results: [], context: '' });
|
|
1111
|
+
|
|
1112
|
+
const url = new URL(request.url);
|
|
1113
|
+
const query = url.searchParams.get('q') || '';
|
|
1114
|
+
const limit = parseInt(url.searchParams.get('limit') || '5', 10);
|
|
1115
|
+
const workspaceId = url.searchParams.get('workspace_id');
|
|
1116
|
+
const maxTokens = parseInt(url.searchParams.get('max_tokens') || '1500', 10);
|
|
1117
|
+
|
|
1118
|
+
if (!query || query.length < 3) {
|
|
1119
|
+
return NextResponse.json({ results: [], context: '' });
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
const resolver = new EntityResolver(cortex.graph);
|
|
1123
|
+
const requesterId = `person-${slugify(user)}`;
|
|
1124
|
+
|
|
1125
|
+
const engine = new ContextEngine({
|
|
1126
|
+
store: cortex.store,
|
|
1127
|
+
graph: cortex.graph,
|
|
1128
|
+
resolver,
|
|
1129
|
+
embedding: cortex.embedding,
|
|
1130
|
+
requesterId,
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
const result = await engine.assemble(query, {
|
|
1134
|
+
limit,
|
|
1135
|
+
workspaceId: workspaceId ? parseInt(workspaceId, 10) : null,
|
|
1136
|
+
maxTokens,
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
return NextResponse.json({
|
|
1140
|
+
results: result.results.map(r => ({ ...r, vector: undefined })), // strip vectors
|
|
1141
|
+
context: result.context,
|
|
1142
|
+
intent: result.intent,
|
|
1143
|
+
conflicts: result.conflicts.length,
|
|
1144
|
+
timing: result.timing,
|
|
1145
|
+
});
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
```
|
|
1149
|
+
|
|
1150
|
+
- [ ] **Step 2: Commit**
|
|
1151
|
+
|
|
1152
|
+
```bash
|
|
1153
|
+
git add src/app/api/cortex/context/route.ts
|
|
1154
|
+
git commit -m "feat(cortex): add context assembly API endpoint"
|
|
1155
|
+
```
|
|
1156
|
+
|
|
1157
|
+
---
|
|
1158
|
+
|
|
1159
|
+
### Task 7: Update RAG hook to use context assembly endpoint
|
|
1160
|
+
|
|
1161
|
+
**Files:**
|
|
1162
|
+
- Modify: `bin/cortex-hook.js`
|
|
1163
|
+
|
|
1164
|
+
- [ ] **Step 1: Read current cortex-hook.js**
|
|
1165
|
+
|
|
1166
|
+
Read the file to understand the current flow: query → /api/cortex/search → format results → output.
|
|
1167
|
+
|
|
1168
|
+
- [ ] **Step 2: Update to call context assembly endpoint**
|
|
1169
|
+
|
|
1170
|
+
Change the URL from `/api/cortex/search/?q=...` to `/api/cortex/context/?q=...`. The new endpoint returns `{ context, results, intent, conflicts, timing }` — the `context` field is already pre-formatted as `<cortex-context>`, so the hook can output it directly instead of doing its own formatting.
|
|
1171
|
+
|
|
1172
|
+
Key changes:
|
|
1173
|
+
1. URL: `/api/cortex/search/` → `/api/cortex/context/`
|
|
1174
|
+
2. Response handling: use `parsed.context` directly instead of formatting results manually
|
|
1175
|
+
3. Keep the fallback: if the new endpoint returns empty or fails, fall back to old behavior
|
|
1176
|
+
|
|
1177
|
+
```javascript
|
|
1178
|
+
// Replace the response handling section:
|
|
1179
|
+
const parsed = JSON.parse(body);
|
|
1180
|
+
|
|
1181
|
+
// New: context is pre-formatted by the Context Assembly Engine
|
|
1182
|
+
if (parsed.context) {
|
|
1183
|
+
const output = JSON.stringify({
|
|
1184
|
+
hookSpecificOutput: {
|
|
1185
|
+
hookEventName: 'UserPromptSubmit',
|
|
1186
|
+
additionalContext: parsed.context,
|
|
1187
|
+
},
|
|
1188
|
+
});
|
|
1189
|
+
process.stdout.write(output);
|
|
1190
|
+
process.exit(0);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// Fallback: old-style results formatting (if context endpoint not available)
|
|
1194
|
+
const results = parsed.results;
|
|
1195
|
+
if (!results || results.length === 0) process.exit(0);
|
|
1196
|
+
// ... existing formatting code stays as fallback ...
|
|
1197
|
+
```
|
|
1198
|
+
|
|
1199
|
+
- [ ] **Step 3: Commit**
|
|
1200
|
+
|
|
1201
|
+
```bash
|
|
1202
|
+
git add bin/cortex-hook.js
|
|
1203
|
+
git commit -m "feat(cortex): switch RAG hook to context assembly endpoint"
|
|
1204
|
+
```
|
|
1205
|
+
|
|
1206
|
+
---
|
|
1207
|
+
|
|
1208
|
+
### Task 8: Integrate ContextEngine into CortexInstance
|
|
1209
|
+
|
|
1210
|
+
**Files:**
|
|
1211
|
+
- Modify: `src/lib/cortex/index.ts`
|
|
1212
|
+
|
|
1213
|
+
- [ ] **Step 1: Read current index.ts**
|
|
1214
|
+
|
|
1215
|
+
- [ ] **Step 2: Add ContextEngine to CortexInstance**
|
|
1216
|
+
|
|
1217
|
+
1. Import: `import { ContextEngine } from './retrieval/context-engine';` and `import { EntityResolver } from './graph/resolver';`
|
|
1218
|
+
2. Add `contextEngine?: ContextEngine` to `CortexInstance` interface (optional since it depends on graph)
|
|
1219
|
+
3. In `getCortex()`, after graph initialization, create the ContextEngine:
|
|
1220
|
+
|
|
1221
|
+
```typescript
|
|
1222
|
+
const resolver = new EntityResolver(graph);
|
|
1223
|
+
const contextEngine = new ContextEngine({
|
|
1224
|
+
store,
|
|
1225
|
+
graph,
|
|
1226
|
+
resolver,
|
|
1227
|
+
embedding,
|
|
1228
|
+
requesterId: 'person-default-user', // default; overridden per-request in API
|
|
1229
|
+
});
|
|
1230
|
+
```
|
|
1231
|
+
|
|
1232
|
+
4. Add to instance object: `contextEngine,`
|
|
1233
|
+
|
|
1234
|
+
- [ ] **Step 3: Run full cortex test suite**
|
|
1235
|
+
|
|
1236
|
+
Run: `npx vitest run tests/lib/cortex/`
|
|
1237
|
+
|
|
1238
|
+
- [ ] **Step 4: Commit**
|
|
1239
|
+
|
|
1240
|
+
```bash
|
|
1241
|
+
git add src/lib/cortex/index.ts
|
|
1242
|
+
git commit -m "feat(cortex): add ContextEngine to CortexInstance"
|
|
1243
|
+
```
|
|
1244
|
+
|
|
1245
|
+
---
|
|
1246
|
+
|
|
1247
|
+
## Summary
|
|
1248
|
+
|
|
1249
|
+
| Task | Component | Tests | Status |
|
|
1250
|
+
|------|-----------|-------|--------|
|
|
1251
|
+
| 1 | Intent detection | 7 | |
|
|
1252
|
+
| 2 | Weight computation | 5 | |
|
|
1253
|
+
| 3 | Conflict detection | 4 | |
|
|
1254
|
+
| 4 | Context formatter | 6 | |
|
|
1255
|
+
| 5 | ContextEngine (6-stage pipeline) | 6 | |
|
|
1256
|
+
| 6 | Context assembly API endpoint | — | |
|
|
1257
|
+
| 7 | RAG hook integration | — | |
|
|
1258
|
+
| 8 | CortexInstance integration | regression | |
|
|
1259
|
+
|
|
1260
|
+
**Total: 8 tasks, ~28 new tests, 4 chunks**
|
|
1261
|
+
|
|
1262
|
+
**Performance budget:** The ContextEngine targets <150ms total latency:
|
|
1263
|
+
- Intent detection: <5ms (regex, no LLM)
|
|
1264
|
+
- Entity resolution: <5ms (alias lookup)
|
|
1265
|
+
- Weight computation: <10ms (graph proximity, cached)
|
|
1266
|
+
- Parallel search: <100ms (concurrent vector search with 100ms timeout)
|
|
1267
|
+
- Fusion + formatting: <10ms
|
|
1268
|
+
|
|
1269
|
+
**Key design decisions:**
|
|
1270
|
+
- ContextEngine sits ABOVE CortexSearch — doesn't replace it, wraps it
|
|
1271
|
+
- Graph proximity drives weights — but gracefully degrades to fixed weights when graph is empty
|
|
1272
|
+
- Deduplication uses text prefix comparison (not cosine between result vectors — that would require N² vector ops)
|
|
1273
|
+
- RAG hook uses pre-formatted `context` from the engine — no more client-side formatting
|
|
1274
|
+
- Old `/api/cortex/search` endpoint still works — new `/api/cortex/context` endpoint adds the intelligence layer
|