forge-server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/hooks/worktree-create.sh +64 -0
- package/.claude/hooks/worktree-remove.sh +57 -0
- package/.claude/settings.local.json +29 -0
- package/.forge/knowledge/conventions.yaml +1 -0
- package/.forge/knowledge/decisions.yaml +1 -0
- package/.forge/knowledge/gotchas.yaml +1 -0
- package/.forge/knowledge/patterns.yaml +1 -0
- package/.forge/manifest.yaml +6 -0
- package/CLAUDE.md +144 -0
- package/bin/setup-forge.sh +132 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +553 -0
- package/dist/cli.js.map +1 -0
- package/dist/context/codebase.d.ts +57 -0
- package/dist/context/codebase.d.ts.map +1 -0
- package/dist/context/codebase.js +301 -0
- package/dist/context/codebase.js.map +1 -0
- package/dist/context/injector.d.ts +147 -0
- package/dist/context/injector.d.ts.map +1 -0
- package/dist/context/injector.js +533 -0
- package/dist/context/injector.js.map +1 -0
- package/dist/context/memory.d.ts +32 -0
- package/dist/context/memory.d.ts.map +1 -0
- package/dist/context/memory.js +140 -0
- package/dist/context/memory.js.map +1 -0
- package/dist/context/session-index.d.ts +54 -0
- package/dist/context/session-index.d.ts.map +1 -0
- package/dist/context/session-index.js +265 -0
- package/dist/context/session-index.js.map +1 -0
- package/dist/context/session.d.ts +42 -0
- package/dist/context/session.d.ts.map +1 -0
- package/dist/context/session.js +121 -0
- package/dist/context/session.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/ingestion/chunker.d.ts +19 -0
- package/dist/ingestion/chunker.d.ts.map +1 -0
- package/dist/ingestion/chunker.js +189 -0
- package/dist/ingestion/chunker.js.map +1 -0
- package/dist/ingestion/embedder.d.ts +45 -0
- package/dist/ingestion/embedder.d.ts.map +1 -0
- package/dist/ingestion/embedder.js +152 -0
- package/dist/ingestion/embedder.js.map +1 -0
- package/dist/ingestion/git-analyzer.d.ts +77 -0
- package/dist/ingestion/git-analyzer.d.ts.map +1 -0
- package/dist/ingestion/git-analyzer.js +437 -0
- package/dist/ingestion/git-analyzer.js.map +1 -0
- package/dist/ingestion/indexer.d.ts +79 -0
- package/dist/ingestion/indexer.d.ts.map +1 -0
- package/dist/ingestion/indexer.js +766 -0
- package/dist/ingestion/indexer.js.map +1 -0
- package/dist/ingestion/markdown-chunker.d.ts +19 -0
- package/dist/ingestion/markdown-chunker.d.ts.map +1 -0
- package/dist/ingestion/markdown-chunker.js +243 -0
- package/dist/ingestion/markdown-chunker.js.map +1 -0
- package/dist/ingestion/markdown-knowledge.d.ts +21 -0
- package/dist/ingestion/markdown-knowledge.d.ts.map +1 -0
- package/dist/ingestion/markdown-knowledge.js +129 -0
- package/dist/ingestion/markdown-knowledge.js.map +1 -0
- package/dist/ingestion/parser.d.ts +20 -0
- package/dist/ingestion/parser.d.ts.map +1 -0
- package/dist/ingestion/parser.js +429 -0
- package/dist/ingestion/parser.js.map +1 -0
- package/dist/ingestion/watcher.d.ts +28 -0
- package/dist/ingestion/watcher.d.ts.map +1 -0
- package/dist/ingestion/watcher.js +147 -0
- package/dist/ingestion/watcher.js.map +1 -0
- package/dist/knowledge/hydrator.d.ts +37 -0
- package/dist/knowledge/hydrator.d.ts.map +1 -0
- package/dist/knowledge/hydrator.js +220 -0
- package/dist/knowledge/hydrator.js.map +1 -0
- package/dist/knowledge/registry.d.ts +129 -0
- package/dist/knowledge/registry.d.ts.map +1 -0
- package/dist/knowledge/registry.js +361 -0
- package/dist/knowledge/registry.js.map +1 -0
- package/dist/knowledge/search.d.ts +114 -0
- package/dist/knowledge/search.d.ts.map +1 -0
- package/dist/knowledge/search.js +428 -0
- package/dist/knowledge/search.js.map +1 -0
- package/dist/knowledge/store.d.ts +76 -0
- package/dist/knowledge/store.d.ts.map +1 -0
- package/dist/knowledge/store.js +230 -0
- package/dist/knowledge/store.js.map +1 -0
- package/dist/learning/confidence.d.ts +30 -0
- package/dist/learning/confidence.d.ts.map +1 -0
- package/dist/learning/confidence.js +165 -0
- package/dist/learning/confidence.js.map +1 -0
- package/dist/learning/patterns.d.ts +52 -0
- package/dist/learning/patterns.d.ts.map +1 -0
- package/dist/learning/patterns.js +290 -0
- package/dist/learning/patterns.js.map +1 -0
- package/dist/learning/trajectory.d.ts +55 -0
- package/dist/learning/trajectory.d.ts.map +1 -0
- package/dist/learning/trajectory.js +200 -0
- package/dist/learning/trajectory.js.map +1 -0
- package/dist/memory/memory-compat.d.ts +100 -0
- package/dist/memory/memory-compat.d.ts.map +1 -0
- package/dist/memory/memory-compat.js +146 -0
- package/dist/memory/memory-compat.js.map +1 -0
- package/dist/memory/observation-store.d.ts +57 -0
- package/dist/memory/observation-store.d.ts.map +1 -0
- package/dist/memory/observation-store.js +154 -0
- package/dist/memory/observation-store.js.map +1 -0
- package/dist/memory/session-tracker.d.ts +81 -0
- package/dist/memory/session-tracker.d.ts.map +1 -0
- package/dist/memory/session-tracker.js +262 -0
- package/dist/memory/session-tracker.js.map +1 -0
- package/dist/pipeline/engine.d.ts +179 -0
- package/dist/pipeline/engine.d.ts.map +1 -0
- package/dist/pipeline/engine.js +691 -0
- package/dist/pipeline/engine.js.map +1 -0
- package/dist/pipeline/events.d.ts +54 -0
- package/dist/pipeline/events.d.ts.map +1 -0
- package/dist/pipeline/events.js +157 -0
- package/dist/pipeline/events.js.map +1 -0
- package/dist/pipeline/parallel.d.ts +83 -0
- package/dist/pipeline/parallel.d.ts.map +1 -0
- package/dist/pipeline/parallel.js +277 -0
- package/dist/pipeline/parallel.js.map +1 -0
- package/dist/pipeline/state-machine.d.ts +65 -0
- package/dist/pipeline/state-machine.d.ts.map +1 -0
- package/dist/pipeline/state-machine.js +176 -0
- package/dist/pipeline/state-machine.js.map +1 -0
- package/dist/query/graph-queries.d.ts +84 -0
- package/dist/query/graph-queries.d.ts.map +1 -0
- package/dist/query/graph-queries.js +216 -0
- package/dist/query/graph-queries.js.map +1 -0
- package/dist/query/hybrid-search.d.ts +34 -0
- package/dist/query/hybrid-search.d.ts.map +1 -0
- package/dist/query/hybrid-search.js +263 -0
- package/dist/query/hybrid-search.js.map +1 -0
- package/dist/query/intent-detector.d.ts +35 -0
- package/dist/query/intent-detector.d.ts.map +1 -0
- package/dist/query/intent-detector.js +115 -0
- package/dist/query/intent-detector.js.map +1 -0
- package/dist/query/ranking.d.ts +57 -0
- package/dist/query/ranking.d.ts.map +1 -0
- package/dist/query/ranking.js +109 -0
- package/dist/query/ranking.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +291 -0
- package/dist/server.js.map +1 -0
- package/dist/storage/falkordb-store.d.ts +73 -0
- package/dist/storage/falkordb-store.d.ts.map +1 -0
- package/dist/storage/falkordb-store.js +346 -0
- package/dist/storage/falkordb-store.js.map +1 -0
- package/dist/storage/file-cache.d.ts +32 -0
- package/dist/storage/file-cache.d.ts.map +1 -0
- package/dist/storage/file-cache.js +115 -0
- package/dist/storage/file-cache.js.map +1 -0
- package/dist/storage/interfaces.d.ts +151 -0
- package/dist/storage/interfaces.d.ts.map +1 -0
- package/dist/storage/interfaces.js +7 -0
- package/dist/storage/interfaces.js.map +1 -0
- package/dist/storage/qdrant-store.d.ts +110 -0
- package/dist/storage/qdrant-store.d.ts.map +1 -0
- package/dist/storage/qdrant-store.js +467 -0
- package/dist/storage/qdrant-store.js.map +1 -0
- package/dist/storage/schema.d.ts +4 -0
- package/dist/storage/schema.d.ts.map +1 -0
- package/dist/storage/schema.js +136 -0
- package/dist/storage/schema.js.map +1 -0
- package/dist/storage/sqlite.d.ts +35 -0
- package/dist/storage/sqlite.d.ts.map +1 -0
- package/dist/storage/sqlite.js +132 -0
- package/dist/storage/sqlite.js.map +1 -0
- package/dist/tools/collaboration-tools.d.ts +111 -0
- package/dist/tools/collaboration-tools.d.ts.map +1 -0
- package/dist/tools/collaboration-tools.js +174 -0
- package/dist/tools/collaboration-tools.js.map +1 -0
- package/dist/tools/context-tools.d.ts +293 -0
- package/dist/tools/context-tools.d.ts.map +1 -0
- package/dist/tools/context-tools.js +437 -0
- package/dist/tools/context-tools.js.map +1 -0
- package/dist/tools/graph-tools.d.ts +129 -0
- package/dist/tools/graph-tools.d.ts.map +1 -0
- package/dist/tools/graph-tools.js +237 -0
- package/dist/tools/graph-tools.js.map +1 -0
- package/dist/tools/ingestion-tools.d.ts +96 -0
- package/dist/tools/ingestion-tools.d.ts.map +1 -0
- package/dist/tools/ingestion-tools.js +90 -0
- package/dist/tools/ingestion-tools.js.map +1 -0
- package/dist/tools/learning-tools.d.ts +168 -0
- package/dist/tools/learning-tools.d.ts.map +1 -0
- package/dist/tools/learning-tools.js +158 -0
- package/dist/tools/learning-tools.js.map +1 -0
- package/dist/tools/memory-tools.d.ts +183 -0
- package/dist/tools/memory-tools.d.ts.map +1 -0
- package/dist/tools/memory-tools.js +197 -0
- package/dist/tools/memory-tools.js.map +1 -0
- package/dist/tools/phase-tools.d.ts +954 -0
- package/dist/tools/phase-tools.d.ts.map +1 -0
- package/dist/tools/phase-tools.js +1215 -0
- package/dist/tools/phase-tools.js.map +1 -0
- package/dist/tools/pipeline-tools.d.ts +140 -0
- package/dist/tools/pipeline-tools.d.ts.map +1 -0
- package/dist/tools/pipeline-tools.js +162 -0
- package/dist/tools/pipeline-tools.js.map +1 -0
- package/dist/tools/registration-tools.d.ts +220 -0
- package/dist/tools/registration-tools.d.ts.map +1 -0
- package/dist/tools/registration-tools.js +391 -0
- package/dist/tools/registration-tools.js.map +1 -0
- package/dist/util/circuit-breaker.d.ts +75 -0
- package/dist/util/circuit-breaker.d.ts.map +1 -0
- package/dist/util/circuit-breaker.js +159 -0
- package/dist/util/circuit-breaker.js.map +1 -0
- package/dist/util/config.d.ts +23 -0
- package/dist/util/config.d.ts.map +1 -0
- package/dist/util/config.js +164 -0
- package/dist/util/config.js.map +1 -0
- package/dist/util/logger.d.ts +13 -0
- package/dist/util/logger.d.ts.map +1 -0
- package/dist/util/logger.js +45 -0
- package/dist/util/logger.js.map +1 -0
- package/dist/util/token-counter.d.ts +24 -0
- package/dist/util/token-counter.d.ts.map +1 -0
- package/dist/util/token-counter.js +48 -0
- package/dist/util/token-counter.js.map +1 -0
- package/dist/util/types.d.ts +525 -0
- package/dist/util/types.d.ts.map +1 -0
- package/dist/util/types.js +5 -0
- package/dist/util/types.js.map +1 -0
- package/docker-compose.yml +20 -0
- package/docs/plans/2026-02-27-swarm-coordination/architecture.md +203 -0
- package/docs/plans/2026-02-27-swarm-coordination/vision.md +57 -0
- package/docs/plans/completed/2026-02-26-forge-plugin-bundling/architecture.md +1 -0
- package/docs/plans/completed/2026-02-26-forge-plugin-bundling/vision.md +300 -0
- package/docs/plans/completed/2026-02-27-forge-swarm-learning/architecture.md +480 -0
- package/docs/plans/completed/2026-02-27-forge-swarm-learning/verification-checklist.md +462 -0
- package/docs/plans/completed/2026-02-27-git-history-atlassian/git-jira-plan.md +181 -0
- package/package.json +39 -0
- package/plugin/.claude-plugin/plugin.json +8 -0
- package/plugin/.mcp.json +15 -0
- package/plugin/README.md +134 -0
- package/plugin/agents/architect.md +367 -0
- package/plugin/agents/backend-specialist.md +263 -0
- package/plugin/agents/brainstormer.md +122 -0
- package/plugin/agents/data-specialist.md +266 -0
- package/plugin/agents/designer.md +408 -0
- package/plugin/agents/frontend-specialist.md +241 -0
- package/plugin/agents/inspector.md +406 -0
- package/plugin/agents/knowledge-keeper.md +443 -0
- package/plugin/agents/platform-engineer.md +326 -0
- package/plugin/agents/product-manager.md +268 -0
- package/plugin/agents/product-owner.md +438 -0
- package/plugin/agents/pulse-checker.md +73 -0
- package/plugin/agents/qa-strategist.md +500 -0
- package/plugin/agents/self-improver.md +310 -0
- package/plugin/agents/strategist.md +360 -0
- package/plugin/agents/supervisor.md +380 -0
- package/plugin/commands/brainstorm.md +25 -0
- package/plugin/commands/forge.md +88 -0
- package/plugin/docs/atlassian-integration.md +110 -0
- package/plugin/docs/workflow.md +126 -0
- package/plugin/skills/agent-development/.skillfish.json +10 -0
- package/plugin/skills/agent-development/SKILL.md +415 -0
- package/plugin/skills/agent-development/examples/agent-creation-prompt.md +238 -0
- package/plugin/skills/agent-development/examples/complete-agent-examples.md +427 -0
- package/plugin/skills/agent-development/references/agent-creation-system-prompt.md +207 -0
- package/plugin/skills/agent-development/references/system-prompt-design.md +411 -0
- package/plugin/skills/agent-development/references/triggering-examples.md +491 -0
- package/plugin/skills/agent-development/scripts/validate-agent.sh +217 -0
- package/plugin/skills/agent-handoff/SKILL.md +335 -0
- package/plugin/skills/anti-stub/SKILL.md +317 -0
- package/plugin/skills/brainstorm/SKILL.md +31 -0
- package/plugin/skills/debugging/SKILL.md +276 -0
- package/plugin/skills/fix/SKILL.md +62 -0
- package/plugin/skills/frontend-design/.skillfish.json +10 -0
- package/plugin/skills/frontend-design/SKILL.md +42 -0
- package/plugin/skills/gotchas/SKILL.md +61 -0
- package/plugin/skills/graph-orchestrator/SKILL.md +38 -0
- package/plugin/skills/history/SKILL.md +58 -0
- package/plugin/skills/impact/SKILL.md +59 -0
- package/plugin/skills/implementation-execution/SKILL.md +291 -0
- package/plugin/skills/index-repo/SKILL.md +55 -0
- package/plugin/skills/interviewing/SKILL.md +225 -0
- package/plugin/skills/knowledge-curation/SKILL.md +393 -0
- package/plugin/skills/learn/SKILL.md +69 -0
- package/plugin/skills/mcp-integration/.skillfish.json +10 -0
- package/plugin/skills/mcp-integration/SKILL.md +554 -0
- package/plugin/skills/mcp-integration/examples/http-server.json +20 -0
- package/plugin/skills/mcp-integration/examples/sse-server.json +19 -0
- package/plugin/skills/mcp-integration/examples/stdio-server.json +26 -0
- package/plugin/skills/mcp-integration/references/authentication.md +549 -0
- package/plugin/skills/mcp-integration/references/server-types.md +536 -0
- package/plugin/skills/mcp-integration/references/tool-usage.md +538 -0
- package/plugin/skills/nestjs/.skillfish.json +10 -0
- package/plugin/skills/nestjs/SKILL.md +669 -0
- package/plugin/skills/nestjs/drizzle-reference.md +1894 -0
- package/plugin/skills/nestjs/reference.md +1447 -0
- package/plugin/skills/nestjs/workflow-optimization.md +229 -0
- package/plugin/skills/parallel-dispatch/SKILL.md +308 -0
- package/plugin/skills/project-discovery/SKILL.md +304 -0
- package/plugin/skills/search/SKILL.md +56 -0
- package/plugin/skills/security-audit/SKILL.md +362 -0
- package/plugin/skills/skill-development/.skillfish.json +10 -0
- package/plugin/skills/skill-development/SKILL.md +637 -0
- package/plugin/skills/skill-development/references/skill-creator-original.md +209 -0
- package/plugin/skills/tdd/SKILL.md +273 -0
- package/plugin/skills/terminal-presentation/SKILL.md +395 -0
- package/plugin/skills/test-strategy/SKILL.md +365 -0
- package/plugin/skills/verification-protocol/SKILL.md +256 -0
- package/plugin/skills/visual-explainer/CHANGELOG.md +97 -0
- package/plugin/skills/visual-explainer/LICENSE +21 -0
- package/plugin/skills/visual-explainer/README.md +137 -0
- package/plugin/skills/visual-explainer/SKILL.md +352 -0
- package/plugin/skills/visual-explainer/banner.png +0 -0
- package/plugin/skills/visual-explainer/package.json +11 -0
- package/plugin/skills/visual-explainer/prompts/diff-review.md +68 -0
- package/plugin/skills/visual-explainer/prompts/fact-check.md +63 -0
- package/plugin/skills/visual-explainer/prompts/generate-slides.md +18 -0
- package/plugin/skills/visual-explainer/prompts/generate-web-diagram.md +10 -0
- package/plugin/skills/visual-explainer/prompts/plan-review.md +86 -0
- package/plugin/skills/visual-explainer/prompts/project-recap.md +61 -0
- package/plugin/skills/visual-explainer/references/css-patterns.md +1188 -0
- package/plugin/skills/visual-explainer/references/libraries.md +470 -0
- package/plugin/skills/visual-explainer/references/responsive-nav.md +212 -0
- package/plugin/skills/visual-explainer/references/slide-patterns.md +1403 -0
- package/plugin/skills/visual-explainer/templates/architecture.html +596 -0
- package/plugin/skills/visual-explainer/templates/data-table.html +540 -0
- package/plugin/skills/visual-explainer/templates/mermaid-flowchart.html +435 -0
- package/plugin/skills/visual-explainer/templates/slide-deck.html +913 -0
- package/src/cli.ts +655 -0
- package/src/context/.gitkeep +0 -0
- package/src/context/codebase.ts +393 -0
- package/src/context/injector.ts +797 -0
- package/src/context/memory.ts +187 -0
- package/src/context/session-index.ts +327 -0
- package/src/context/session.ts +152 -0
- package/src/index.ts +47 -0
- package/src/ingestion/.gitkeep +0 -0
- package/src/ingestion/chunker.ts +277 -0
- package/src/ingestion/embedder.ts +167 -0
- package/src/ingestion/git-analyzer.ts +545 -0
- package/src/ingestion/indexer.ts +984 -0
- package/src/ingestion/markdown-chunker.ts +337 -0
- package/src/ingestion/markdown-knowledge.ts +175 -0
- package/src/ingestion/parser.ts +475 -0
- package/src/ingestion/watcher.ts +182 -0
- package/src/knowledge/.gitkeep +0 -0
- package/src/knowledge/hydrator.ts +246 -0
- package/src/knowledge/registry.ts +463 -0
- package/src/knowledge/search.ts +565 -0
- package/src/knowledge/store.ts +262 -0
- package/src/learning/.gitkeep +0 -0
- package/src/learning/confidence.ts +193 -0
- package/src/learning/patterns.ts +360 -0
- package/src/learning/trajectory.ts +268 -0
- package/src/memory/.gitkeep +0 -0
- package/src/memory/memory-compat.ts +233 -0
- package/src/memory/observation-store.ts +224 -0
- package/src/memory/session-tracker.ts +332 -0
- package/src/pipeline/.gitkeep +0 -0
- package/src/pipeline/engine.ts +1139 -0
- package/src/pipeline/events.ts +253 -0
- package/src/pipeline/parallel.ts +394 -0
- package/src/pipeline/state-machine.ts +199 -0
- package/src/query/.gitkeep +0 -0
- package/src/query/graph-queries.ts +262 -0
- package/src/query/hybrid-search.ts +337 -0
- package/src/query/intent-detector.ts +131 -0
- package/src/query/ranking.ts +161 -0
- package/src/server.ts +352 -0
- package/src/storage/.gitkeep +0 -0
- package/src/storage/falkordb-store.ts +388 -0
- package/src/storage/file-cache.ts +141 -0
- package/src/storage/interfaces.ts +201 -0
- package/src/storage/qdrant-store.ts +557 -0
- package/src/storage/schema.ts +139 -0
- package/src/storage/sqlite.ts +168 -0
- package/src/tools/.gitkeep +0 -0
- package/src/tools/collaboration-tools.ts +208 -0
- package/src/tools/context-tools.ts +493 -0
- package/src/tools/graph-tools.ts +295 -0
- package/src/tools/ingestion-tools.ts +122 -0
- package/src/tools/learning-tools.ts +181 -0
- package/src/tools/memory-tools.ts +234 -0
- package/src/tools/phase-tools.ts +1452 -0
- package/src/tools/pipeline-tools.ts +188 -0
- package/src/tools/registration-tools.ts +450 -0
- package/src/util/.gitkeep +0 -0
- package/src/util/circuit-breaker.ts +193 -0
- package/src/util/config.ts +177 -0
- package/src/util/logger.ts +53 -0
- package/src/util/token-counter.ts +52 -0
- package/src/util/types.ts +710 -0
- package/tests/context/.gitkeep +0 -0
- package/tests/integration/.gitkeep +0 -0
- package/tests/knowledge/.gitkeep +0 -0
- package/tests/learning/.gitkeep +0 -0
- package/tests/pipeline/.gitkeep +0 -0
- package/tests/tools/.gitkeep +0 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +10 -0
- package/vscode-extension/.vscodeignore +7 -0
- package/vscode-extension/README.md +43 -0
- package/vscode-extension/out/edge-collector.js +274 -0
- package/vscode-extension/out/edge-collector.js.map +1 -0
- package/vscode-extension/out/extension.js +264 -0
- package/vscode-extension/out/extension.js.map +1 -0
- package/vscode-extension/out/forge-client.js +318 -0
- package/vscode-extension/out/forge-client.js.map +1 -0
- package/vscode-extension/package-lock.json +59 -0
- package/vscode-extension/package.json +71 -0
- package/vscode-extension/src/edge-collector.ts +320 -0
- package/vscode-extension/src/extension.ts +269 -0
- package/vscode-extension/src/forge-client.ts +364 -0
- package/vscode-extension/tsconfig.json +19 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hybrid search engine combining vector similarity + graph neighbor boost.
|
|
3
|
+
* Final scoring: 0.6 * vector_score + 0.3 * graph_centrality + 0.1 * recency_boost
|
|
4
|
+
*
|
|
5
|
+
* Phase 2: Full graph neighbor expansion.
|
|
6
|
+
* For each vector hit, traverse graph neighbors (IMPORTS, CALLS, TESTS, CO_MODIFIED)
|
|
7
|
+
* up to pivot_depth hops, collect neighbor file paths, and boost their scores.
|
|
8
|
+
* Graph centrality = number of times a file appears as a neighbor / max neighbor appearances.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { VectorStore, GraphStore } from '../storage/interfaces.js';
|
|
12
|
+
import type { CodeChunkPayload, QueryIntent } from '../util/types.js';
|
|
13
|
+
import { embedText } from '../ingestion/embedder.js';
|
|
14
|
+
import { computeScore, deduplicateByFile, buildMatchReason, type ScoredResult } from './ranking.js';
|
|
15
|
+
import { QUERY_FILE_NEIGHBORS, QUERY_FILE_HOTSPOTS } from './graph-queries.js';
|
|
16
|
+
import { logger } from '../util/logger.js';
|
|
17
|
+
|
|
18
|
+
export interface HybridSearchOptions {
|
|
19
|
+
query: string;
|
|
20
|
+
repoId?: string;
|
|
21
|
+
limit?: number;
|
|
22
|
+
includeTests?: boolean;
|
|
23
|
+
pivotDepth?: number;
|
|
24
|
+
vectorWeight?: number;
|
|
25
|
+
graphWeight?: number;
|
|
26
|
+
/** Query intent — affects whether hotspot scoring is applied */
|
|
27
|
+
intent?: QueryIntent;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface HybridSearchResult {
|
|
31
|
+
results: ScoredResult[];
|
|
32
|
+
cacheHits: number;
|
|
33
|
+
executionTimeMs: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Perform hybrid search over indexed code chunks.
|
|
38
|
+
* Returns deduplicated, ranked results by file.
|
|
39
|
+
*/
|
|
40
|
+
export async function hybridSearch(
|
|
41
|
+
vectorStore: VectorStore,
|
|
42
|
+
graphStore: GraphStore,
|
|
43
|
+
options: HybridSearchOptions
|
|
44
|
+
): Promise<HybridSearchResult> {
|
|
45
|
+
const startTime = Date.now();
|
|
46
|
+
const {
|
|
47
|
+
query,
|
|
48
|
+
repoId,
|
|
49
|
+
limit = 20,
|
|
50
|
+
includeTests = false,
|
|
51
|
+
pivotDepth = 2,
|
|
52
|
+
vectorWeight = 0.55,
|
|
53
|
+
graphWeight = 0.20,
|
|
54
|
+
intent,
|
|
55
|
+
} = options;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
// Step 1: Embed query
|
|
59
|
+
const queryVector = await embedText(query);
|
|
60
|
+
|
|
61
|
+
// Step 2: Vector search
|
|
62
|
+
const filter: Record<string, unknown> = {};
|
|
63
|
+
if (repoId) filter['repo_id'] = repoId;
|
|
64
|
+
|
|
65
|
+
const vectorResults = await vectorStore.searchCodeChunks(queryVector, {
|
|
66
|
+
limit: limit * 2, // Get extra to allow deduplication
|
|
67
|
+
filter: Object.keys(filter).length > 0 ? filter : undefined,
|
|
68
|
+
scoreThreshold: 0.1,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Step 3: Filter test files if needed
|
|
72
|
+
const filteredResults = vectorResults.filter(r => {
|
|
73
|
+
const payload = r.payload as CodeChunkPayload;
|
|
74
|
+
if (!includeTests) {
|
|
75
|
+
const fp = payload.file_path ?? '';
|
|
76
|
+
if (fp.includes('.spec.') || fp.includes('.test.') || fp.includes('__tests__')) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Step 4: Graph neighbor expansion
|
|
84
|
+
// For each unique file in vector results, traverse graph neighbors and
|
|
85
|
+
// collect how often each neighboring file appears (centrality count).
|
|
86
|
+
const neighborCounts = new Map<string, number>(); // filePath -> appearance count
|
|
87
|
+
const vectorFileSet = new Set(
|
|
88
|
+
filteredResults.map(r => (r.payload as CodeChunkPayload).file_path)
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const graphHealthy = await graphStore.isHealthy();
|
|
92
|
+
if (!graphHealthy && repoId) {
|
|
93
|
+
logger.warn('Graph store is unhealthy — skipping graph expansion (vector-only mode)', {
|
|
94
|
+
query: query.slice(0, 50),
|
|
95
|
+
repoId,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (graphHealthy && repoId) {
|
|
99
|
+
const uniqueFiles = Array.from(vectorFileSet);
|
|
100
|
+
|
|
101
|
+
// Query graph neighbors for each vector hit file
|
|
102
|
+
await Promise.all(uniqueFiles.map(async (filePath) => {
|
|
103
|
+
try {
|
|
104
|
+
const result = await graphStore.query(
|
|
105
|
+
QUERY_FILE_NEIGHBORS(filePath, repoId, pivotDepth)
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Parse neighbor paths from raw result
|
|
109
|
+
const rows = result.raw ?? [];
|
|
110
|
+
for (const row of rows) {
|
|
111
|
+
const neighborPath = extractNeighborPath(row);
|
|
112
|
+
if (neighborPath && neighborPath !== filePath) {
|
|
113
|
+
neighborCounts.set(neighborPath, (neighborCounts.get(neighborPath) ?? 0) + 1);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
// Graph query failure is non-fatal — degrade to vector-only
|
|
118
|
+
}
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Step 5: Compute normalized graph centrality scores
|
|
123
|
+
const maxNeighborCount = neighborCounts.size > 0
|
|
124
|
+
? Math.max(...neighborCounts.values())
|
|
125
|
+
: 1;
|
|
126
|
+
|
|
127
|
+
// Step 5b: Fetch file hotspot scores from FalkorDB File node commit_count
|
|
128
|
+
const hotspotMap = new Map<string, number>(); // filePath -> normalized hotspot score
|
|
129
|
+
const useHotspot = intent === 'modify' || intent === 'debug' || intent === 'refactor';
|
|
130
|
+
|
|
131
|
+
if (useHotspot && graphHealthy && repoId) {
|
|
132
|
+
try {
|
|
133
|
+
const hotspotResult = await graphStore.query(
|
|
134
|
+
QUERY_FILE_HOTSPOTS(repoId, 100)
|
|
135
|
+
);
|
|
136
|
+
const rows = hotspotResult.raw ?? [];
|
|
137
|
+
let maxCommitCount = 1;
|
|
138
|
+
|
|
139
|
+
// First pass: find max commit count
|
|
140
|
+
for (const row of rows) {
|
|
141
|
+
const commitCount = extractHotspotCommitCount(row);
|
|
142
|
+
if (commitCount > maxCommitCount) maxCommitCount = commitCount;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Second pass: normalize
|
|
146
|
+
for (const row of rows) {
|
|
147
|
+
const path = extractHotspotPath(row);
|
|
148
|
+
const commitCount = extractHotspotCommitCount(row);
|
|
149
|
+
if (path && commitCount > 0) {
|
|
150
|
+
hotspotMap.set(path, commitCount / maxCommitCount);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
// Hotspot query failure is non-fatal
|
|
155
|
+
logger.debug('Hotspot query failed — skipping hotspot scoring');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Step 6: Convert to ScoredResult with combined scores
|
|
160
|
+
const scored: ScoredResult[] = filteredResults.map(r => {
|
|
161
|
+
const payload = r.payload as CodeChunkPayload;
|
|
162
|
+
const filePath = payload.file_path;
|
|
163
|
+
|
|
164
|
+
// Graph score: normalized neighbor appearance count
|
|
165
|
+
// Files that appear as neighbors of many vector hits get boosted
|
|
166
|
+
const rawGraphScore = neighborCounts.get(filePath) ?? 0;
|
|
167
|
+
const graphCentrality = maxNeighborCount > 0 ? rawGraphScore / maxNeighborCount : 0;
|
|
168
|
+
|
|
169
|
+
// Hotspot score: normalized commit count (0 for read intent)
|
|
170
|
+
const hotspotScore = useHotspot ? (hotspotMap.get(filePath) ?? 0) : 0;
|
|
171
|
+
|
|
172
|
+
const { finalScore, recencyScore } = computeScore(
|
|
173
|
+
r.score,
|
|
174
|
+
graphCentrality,
|
|
175
|
+
payload.indexed_at ?? Date.now(),
|
|
176
|
+
vectorWeight,
|
|
177
|
+
graphWeight,
|
|
178
|
+
undefined, // recencyWeight — use default
|
|
179
|
+
hotspotScore,
|
|
180
|
+
undefined, // hotspotWeight — use default
|
|
181
|
+
0, // observationScore — Phase 4
|
|
182
|
+
undefined // observationWeight — use default
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
filePath,
|
|
187
|
+
repoId: payload.repo_id,
|
|
188
|
+
vectorScore: r.score,
|
|
189
|
+
graphScore: graphCentrality,
|
|
190
|
+
recencyScore,
|
|
191
|
+
hotspotScore,
|
|
192
|
+
observationScore: 0,
|
|
193
|
+
finalScore,
|
|
194
|
+
entityName: payload.entity_name,
|
|
195
|
+
entityType: payload.entity_type,
|
|
196
|
+
startLine: payload.start_line,
|
|
197
|
+
endLine: payload.end_line,
|
|
198
|
+
matchReason: buildMatchReason(r.score, graphCentrality, payload.entity_name, payload.entity_type, hotspotScore),
|
|
199
|
+
indexedAt: payload.indexed_at,
|
|
200
|
+
} satisfies ScoredResult;
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Step 7: Add neighbor files that scored above threshold but weren't in vector results
|
|
204
|
+
// These are files boosted purely by graph centrality (indirect relevance)
|
|
205
|
+
for (const [neighborPath, count] of neighborCounts) {
|
|
206
|
+
if (vectorFileSet.has(neighborPath)) continue; // Already in vector results
|
|
207
|
+
|
|
208
|
+
// Skip test files if not requested
|
|
209
|
+
if (!includeTests) {
|
|
210
|
+
if (
|
|
211
|
+
neighborPath.includes('.spec.') ||
|
|
212
|
+
neighborPath.includes('.test.') ||
|
|
213
|
+
neighborPath.includes('__tests__')
|
|
214
|
+
) continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const normalizedCount = count / maxNeighborCount;
|
|
218
|
+
if (normalizedCount < 0.3) continue; // Only include strongly-connected neighbors
|
|
219
|
+
|
|
220
|
+
const neighborHotspot = useHotspot ? (hotspotMap.get(neighborPath) ?? 0) : 0;
|
|
221
|
+
|
|
222
|
+
const { finalScore, recencyScore } = computeScore(
|
|
223
|
+
0, // No direct vector score
|
|
224
|
+
normalizedCount,
|
|
225
|
+
Date.now(), // Unknown indexed_at for neighbor-only results
|
|
226
|
+
vectorWeight,
|
|
227
|
+
graphWeight,
|
|
228
|
+
undefined,
|
|
229
|
+
neighborHotspot,
|
|
230
|
+
undefined,
|
|
231
|
+
0,
|
|
232
|
+
undefined
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
scored.push({
|
|
236
|
+
filePath: neighborPath,
|
|
237
|
+
repoId: repoId ?? '',
|
|
238
|
+
vectorScore: 0,
|
|
239
|
+
graphScore: normalizedCount,
|
|
240
|
+
recencyScore,
|
|
241
|
+
hotspotScore: neighborHotspot,
|
|
242
|
+
observationScore: 0,
|
|
243
|
+
finalScore,
|
|
244
|
+
entityName: null,
|
|
245
|
+
entityType: 'file_level',
|
|
246
|
+
startLine: 0,
|
|
247
|
+
endLine: 0,
|
|
248
|
+
matchReason: buildMatchReason(0, normalizedCount, null, 'file_level', neighborHotspot),
|
|
249
|
+
indexedAt: undefined,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Step 8: Deduplicate by file, keep highest score per file
|
|
254
|
+
const deduplicated = deduplicateByFile(scored);
|
|
255
|
+
|
|
256
|
+
logger.debug('Hybrid search completed', {
|
|
257
|
+
query: query.slice(0, 50),
|
|
258
|
+
vectorHits: vectorResults.length,
|
|
259
|
+
filteredHits: filteredResults.length,
|
|
260
|
+
neighborFiles: neighborCounts.size,
|
|
261
|
+
uniqueFiles: deduplicated.length,
|
|
262
|
+
durationMs: Date.now() - startTime,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
results: deduplicated.slice(0, limit),
|
|
267
|
+
cacheHits: 0, // Tracked by FileContentCache in tool handler
|
|
268
|
+
executionTimeMs: Date.now() - startTime,
|
|
269
|
+
};
|
|
270
|
+
} catch (err) {
|
|
271
|
+
logger.error('Hybrid search failed', { query: query.slice(0, 50), error: String(err) });
|
|
272
|
+
return {
|
|
273
|
+
results: [],
|
|
274
|
+
cacheHits: 0,
|
|
275
|
+
executionTimeMs: Date.now() - startTime,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Extract neighbor file path from a raw FalkorDB query result row.
|
|
282
|
+
* QUERY_FILE_NEIGHBORS returns: RETURN neighbor.path as path, ...
|
|
283
|
+
* FalkorDB non-compact format: [header, [rows], stats]
|
|
284
|
+
* Each row cell for an alias like `path` is a scalar string.
|
|
285
|
+
*/
|
|
286
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
287
|
+
function extractNeighborPath(row: any): string | null {
|
|
288
|
+
if (!row) return null;
|
|
289
|
+
|
|
290
|
+
// Row is an array of cell values (one per column in SELECT)
|
|
291
|
+
// Column order: [path, rel_type, depth]
|
|
292
|
+
if (Array.isArray(row)) {
|
|
293
|
+
const first = row[0];
|
|
294
|
+
if (typeof first === 'string') return first;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Object row format (some parsers return keyed objects)
|
|
298
|
+
if (typeof row === 'object' && row !== null) {
|
|
299
|
+
if (typeof row.path === 'string') return row.path;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Extract file path from a QUERY_FILE_HOTSPOTS result row.
|
|
307
|
+
* Column order: [path, commit_count, stability_score, change_velocity]
|
|
308
|
+
*/
|
|
309
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
310
|
+
function extractHotspotPath(row: any): string | null {
|
|
311
|
+
if (!row) return null;
|
|
312
|
+
if (Array.isArray(row)) {
|
|
313
|
+
const first = row[0];
|
|
314
|
+
if (typeof first === 'string') return first;
|
|
315
|
+
}
|
|
316
|
+
if (typeof row === 'object' && row !== null) {
|
|
317
|
+
if (typeof row.path === 'string') return row.path;
|
|
318
|
+
}
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Extract commit_count from a QUERY_FILE_HOTSPOTS result row.
|
|
324
|
+
* Column order: [path, commit_count, stability_score, change_velocity]
|
|
325
|
+
*/
|
|
326
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
327
|
+
function extractHotspotCommitCount(row: any): number {
|
|
328
|
+
if (!row) return 0;
|
|
329
|
+
if (Array.isArray(row)) {
|
|
330
|
+
const second = row[1];
|
|
331
|
+
if (typeof second === 'number') return second;
|
|
332
|
+
}
|
|
333
|
+
if (typeof row === 'object' && row !== null) {
|
|
334
|
+
if (typeof row.commit_count === 'number') return row.commit_count;
|
|
335
|
+
}
|
|
336
|
+
return 0;
|
|
337
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query intent classifier using keyword matching (Phase 1).
|
|
3
|
+
* Classifies queries as: read | modify | debug | refactor
|
|
4
|
+
* Phase 4 will enhance with ML-based classification.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { QueryIntent } from '../util/types.js';
|
|
8
|
+
|
|
9
|
+
const INTENT_KEYWORDS: Record<QueryIntent, string[]> = {
|
|
10
|
+
debug: [
|
|
11
|
+
'error', 'bug', 'fix', 'broken', 'failing', 'crash', 'exception',
|
|
12
|
+
'undefined', 'null', 'TypeError', 'undefined is not', 'cannot read',
|
|
13
|
+
'stack trace', 'stacktrace', 'traceback', 'debug', 'diagnose', 'why is',
|
|
14
|
+
'not working', 'breaks', 'issue', 'problem', 'wrong', 'incorrect',
|
|
15
|
+
],
|
|
16
|
+
refactor: [
|
|
17
|
+
'refactor', 'restructure', 'reorganize', 'extract', 'move', 'rename',
|
|
18
|
+
'split', 'merge', 'clean up', 'cleanup', 'simplify', 'optimize',
|
|
19
|
+
'performance', 'improve', 'better', 'smell', 'technical debt', 'dead code',
|
|
20
|
+
'duplicate', 'dry', 'abstract',
|
|
21
|
+
],
|
|
22
|
+
modify: [
|
|
23
|
+
'add', 'implement', 'create', 'build', 'write', 'update', 'change',
|
|
24
|
+
'modify', 'edit', 'extend', 'enhance', 'feature', 'new', 'support',
|
|
25
|
+
'integrate', 'connect', 'wire', 'hook up', 'plug in', 'configure',
|
|
26
|
+
],
|
|
27
|
+
read: [
|
|
28
|
+
'how', 'what', 'where', 'explain', 'understand', 'show', 'list',
|
|
29
|
+
'find', 'search', 'look', 'describe', 'overview', 'structure', 'schema',
|
|
30
|
+
'architecture', 'flow', 'logic', 'context', 'about', 'definition',
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Detect the intent of a query string.
|
|
36
|
+
* Returns the intent category with highest keyword match count.
|
|
37
|
+
* Default: 'read' (safest, most common).
|
|
38
|
+
*/
|
|
39
|
+
export function detectIntent(query: string): QueryIntent {
|
|
40
|
+
const lowerQuery = query.toLowerCase();
|
|
41
|
+
const scores: Record<QueryIntent, number> = { debug: 0, refactor: 0, modify: 0, read: 0 };
|
|
42
|
+
|
|
43
|
+
for (const [intent, keywords] of Object.entries(INTENT_KEYWORDS) as [QueryIntent, string[]][]) {
|
|
44
|
+
for (const keyword of keywords) {
|
|
45
|
+
if (lowerQuery.includes(keyword)) {
|
|
46
|
+
scores[intent]++;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Find intent with highest score
|
|
52
|
+
let maxScore = 0;
|
|
53
|
+
let detectedIntent: QueryIntent = 'read';
|
|
54
|
+
|
|
55
|
+
// Priority: debug > refactor > modify > read (more specific wins on tie)
|
|
56
|
+
for (const intent of ['debug', 'refactor', 'modify', 'read'] as QueryIntent[]) {
|
|
57
|
+
if (scores[intent] > maxScore) {
|
|
58
|
+
maxScore = scores[intent];
|
|
59
|
+
detectedIntent = intent;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return detectedIntent;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get retrieval strategy adjustments based on intent.
|
|
68
|
+
* These affect ranking weights and result composition.
|
|
69
|
+
*
|
|
70
|
+
* Phase 4: 5-signal weights (vector, graph, recency, hotspot, observation).
|
|
71
|
+
*
|
|
72
|
+
* | Intent | Vector | Graph | Recency | Hotspot | Observation |
|
|
73
|
+
* |----------|--------|-------|---------|---------|-------------|
|
|
74
|
+
* | read | 0.55 | 0.20 | 0.05 | 0.10 | 0.10 |
|
|
75
|
+
* | modify | 0.50 | 0.20 | 0.08 | 0.12 | 0.10 |
|
|
76
|
+
* | debug | 0.40 | 0.25 | 0.05 | 0.10 | 0.20 |
|
|
77
|
+
* | refactor | 0.35 | 0.30 | 0.05 | 0.15 | 0.15 |
|
|
78
|
+
*/
|
|
79
|
+
export function getIntentStrategy(intent: QueryIntent): {
|
|
80
|
+
vectorWeight: number;
|
|
81
|
+
graphWeight: number;
|
|
82
|
+
recencyWeight: number;
|
|
83
|
+
hotspotWeight: number;
|
|
84
|
+
observationWeight: number;
|
|
85
|
+
includeTests: boolean;
|
|
86
|
+
expandDepth: number;
|
|
87
|
+
} {
|
|
88
|
+
switch (intent) {
|
|
89
|
+
case 'debug':
|
|
90
|
+
return {
|
|
91
|
+
vectorWeight: 0.40,
|
|
92
|
+
graphWeight: 0.25,
|
|
93
|
+
recencyWeight: 0.05,
|
|
94
|
+
hotspotWeight: 0.10,
|
|
95
|
+
observationWeight: 0.20, // Observations are critical for debugging
|
|
96
|
+
includeTests: true, // Tests often show error scenarios
|
|
97
|
+
expandDepth: 3,
|
|
98
|
+
};
|
|
99
|
+
case 'refactor':
|
|
100
|
+
return {
|
|
101
|
+
vectorWeight: 0.35,
|
|
102
|
+
graphWeight: 0.30, // Blast radius is critical for refactoring
|
|
103
|
+
recencyWeight: 0.05,
|
|
104
|
+
hotspotWeight: 0.15, // Hotspots indicate high-churn areas
|
|
105
|
+
observationWeight: 0.15,
|
|
106
|
+
includeTests: true,
|
|
107
|
+
expandDepth: 3,
|
|
108
|
+
};
|
|
109
|
+
case 'modify':
|
|
110
|
+
return {
|
|
111
|
+
vectorWeight: 0.50,
|
|
112
|
+
graphWeight: 0.20,
|
|
113
|
+
recencyWeight: 0.08,
|
|
114
|
+
hotspotWeight: 0.12,
|
|
115
|
+
observationWeight: 0.10,
|
|
116
|
+
includeTests: false,
|
|
117
|
+
expandDepth: 2,
|
|
118
|
+
};
|
|
119
|
+
case 'read':
|
|
120
|
+
default:
|
|
121
|
+
return {
|
|
122
|
+
vectorWeight: 0.55,
|
|
123
|
+
graphWeight: 0.20,
|
|
124
|
+
recencyWeight: 0.05,
|
|
125
|
+
hotspotWeight: 0.10,
|
|
126
|
+
observationWeight: 0.10,
|
|
127
|
+
includeTests: false,
|
|
128
|
+
expandDepth: 2,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result ranking and scoring for hybrid search.
|
|
3
|
+
* Phase 1: Vector score only (graph_centrality added in Phase 2).
|
|
4
|
+
* Phase 3: 4-signal scoring with hotspot.
|
|
5
|
+
* Phase 4: 5-signal scoring with observation relevance.
|
|
6
|
+
*
|
|
7
|
+
* Default weights: 0.55 vector + 0.20 graph + 0.05 recency + 0.10 hotspot + 0.10 observation
|
|
8
|
+
* All new weights are backward-compatible (default to 0).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const VECTOR_WEIGHT = 0.55;
|
|
12
|
+
const GRAPH_WEIGHT = 0.20;
|
|
13
|
+
const RECENCY_WEIGHT = 0.05;
|
|
14
|
+
const HOTSPOT_WEIGHT = 0.10;
|
|
15
|
+
const OBSERVATION_WEIGHT = 0.10;
|
|
16
|
+
|
|
17
|
+
// Recency boost: files indexed within the last hour get a small boost
|
|
18
|
+
const RECENCY_WINDOW_MS = 60 * 60 * 1000; // 1 hour
|
|
19
|
+
|
|
20
|
+
export interface ScoredResult {
|
|
21
|
+
filePath: string;
|
|
22
|
+
repoId: string;
|
|
23
|
+
vectorScore: number;
|
|
24
|
+
graphScore: number;
|
|
25
|
+
recencyScore: number;
|
|
26
|
+
hotspotScore: number;
|
|
27
|
+
observationScore: number;
|
|
28
|
+
finalScore: number;
|
|
29
|
+
entityName: string | null;
|
|
30
|
+
entityType: string;
|
|
31
|
+
startLine: number;
|
|
32
|
+
endLine: number;
|
|
33
|
+
matchReason: string;
|
|
34
|
+
indexedAt?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Compute final ranking score combining up to 5 signals:
|
|
39
|
+
* vector, graph, recency, hotspot, observation.
|
|
40
|
+
*
|
|
41
|
+
* Backward-compatible: hotspotScore and observationScore default to 0,
|
|
42
|
+
* with their weights redistributed to maintain consistent scoring
|
|
43
|
+
* when callers haven't been updated to supply them.
|
|
44
|
+
*/
|
|
45
|
+
export function computeScore(
|
|
46
|
+
vectorScore: number,
|
|
47
|
+
graphScore: number,
|
|
48
|
+
indexedAt: number,
|
|
49
|
+
vectorWeight = VECTOR_WEIGHT,
|
|
50
|
+
graphWeight = GRAPH_WEIGHT,
|
|
51
|
+
recencyWeight = RECENCY_WEIGHT,
|
|
52
|
+
hotspotScore: number = 0,
|
|
53
|
+
hotspotWeight: number = HOTSPOT_WEIGHT,
|
|
54
|
+
observationScore: number = 0,
|
|
55
|
+
observationWeight: number = OBSERVATION_WEIGHT
|
|
56
|
+
): { finalScore: number; recencyScore: number } {
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
const age = now - indexedAt;
|
|
59
|
+
const recencyScore = age < RECENCY_WINDOW_MS ? 1.0 - (age / RECENCY_WINDOW_MS) : 0;
|
|
60
|
+
|
|
61
|
+
const finalScore =
|
|
62
|
+
vectorWeight * vectorScore +
|
|
63
|
+
graphWeight * graphScore +
|
|
64
|
+
recencyWeight * recencyScore +
|
|
65
|
+
hotspotWeight * hotspotScore +
|
|
66
|
+
observationWeight * observationScore;
|
|
67
|
+
|
|
68
|
+
return { finalScore, recencyScore };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Deduplicate results by file path, keeping the highest-scored entry per file.
|
|
73
|
+
*/
|
|
74
|
+
export function deduplicateByFile(results: ScoredResult[]): ScoredResult[] {
|
|
75
|
+
const byFile = new Map<string, ScoredResult>();
|
|
76
|
+
|
|
77
|
+
for (const result of results) {
|
|
78
|
+
const existing = byFile.get(result.filePath);
|
|
79
|
+
if (!existing || result.finalScore > existing.finalScore) {
|
|
80
|
+
byFile.set(result.filePath, result);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return Array.from(byFile.values()).sort((a, b) => b.finalScore - a.finalScore);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Split results into pivot files (full content) and skeletons (summaries) using actual token sizes.
|
|
89
|
+
* If fileSizes is provided (map of filePath -> estimated tokens), uses real sizes.
|
|
90
|
+
* Otherwise falls back to avgFileTokens estimate.
|
|
91
|
+
*/
|
|
92
|
+
export function splitPivotsAndSkeletons(
|
|
93
|
+
results: ScoredResult[],
|
|
94
|
+
tokenBudget: number,
|
|
95
|
+
fileSizes?: Map<string, number>,
|
|
96
|
+
avgFileTokens: number = 500
|
|
97
|
+
): { pivots: ScoredResult[]; skeletons: ScoredResult[] } {
|
|
98
|
+
const pivots: ScoredResult[] = [];
|
|
99
|
+
const skeletons: ScoredResult[] = [];
|
|
100
|
+
let usedTokens = 0;
|
|
101
|
+
|
|
102
|
+
for (const result of results) {
|
|
103
|
+
const fileTokens = fileSizes?.get(result.filePath) ?? avgFileTokens;
|
|
104
|
+
if (usedTokens + fileTokens <= tokenBudget) {
|
|
105
|
+
pivots.push(result);
|
|
106
|
+
usedTokens += fileTokens;
|
|
107
|
+
} else {
|
|
108
|
+
skeletons.push(result);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { pivots, skeletons };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Generate a human-readable match reason string.
|
|
117
|
+
* Phase 3: includes hotspot info.
|
|
118
|
+
* Phase 4: includes observation info.
|
|
119
|
+
*/
|
|
120
|
+
export function buildMatchReason(
|
|
121
|
+
vectorScore: number,
|
|
122
|
+
graphScore: number,
|
|
123
|
+
entityName: string | null,
|
|
124
|
+
entityType: string,
|
|
125
|
+
hotspotScore: number = 0,
|
|
126
|
+
observationScore: number = 0
|
|
127
|
+
): string {
|
|
128
|
+
const parts: string[] = [];
|
|
129
|
+
|
|
130
|
+
if (vectorScore > 0.8) {
|
|
131
|
+
parts.push('high semantic similarity');
|
|
132
|
+
} else if (vectorScore > 0.5) {
|
|
133
|
+
parts.push('semantic match');
|
|
134
|
+
} else {
|
|
135
|
+
parts.push('partial semantic match');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (graphScore > 0.5) {
|
|
139
|
+
parts.push('high graph centrality');
|
|
140
|
+
} else if (graphScore > 0) {
|
|
141
|
+
parts.push('graph neighbor');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (hotspotScore > 0.7) {
|
|
145
|
+
parts.push('change hotspot');
|
|
146
|
+
} else if (hotspotScore > 0.3) {
|
|
147
|
+
parts.push('frequently changed');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (observationScore > 0.5) {
|
|
151
|
+
parts.push('has relevant observations');
|
|
152
|
+
} else if (observationScore > 0) {
|
|
153
|
+
parts.push('has observations');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (entityName) {
|
|
157
|
+
parts.push(`entity: ${entityName} (${entityType})`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return parts.join(', ');
|
|
161
|
+
}
|