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,984 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Indexer - orchestrates the full ingestion pipeline.
|
|
3
|
+
* Pipeline: file -> hash check -> AST parse -> chunk -> embed -> store (Qdrant + FalkorDB)
|
|
4
|
+
* Handles both initial full indexing and incremental re-indexing.
|
|
5
|
+
*
|
|
6
|
+
* Phase 2 additions:
|
|
7
|
+
* - Populate full graph edges: IMPORTS, EXPORTS, EXTENDS, IMPLEMENTS, CALLS
|
|
8
|
+
* - Create FalkorDB indexes on startup
|
|
9
|
+
* - Run git analysis (CO_MODIFIED, TESTS edges) after full index
|
|
10
|
+
* - Static CALLS edge discovery via identifier matching
|
|
11
|
+
*
|
|
12
|
+
* Note: routing-engine.ts is not merged from forge-graph-rag (it is replaced by the
|
|
13
|
+
* pipeline server's own knowledge/ modules). Ownership routing is inlined here instead.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readFile, stat, readdir } from 'fs/promises';
|
|
17
|
+
import { resolve, extname, join, dirname, isAbsolute } from 'path';
|
|
18
|
+
import { createHash } from 'crypto';
|
|
19
|
+
import type { GraphStore, VectorStore, FileContentCache } from '../storage/interfaces.js';
|
|
20
|
+
import type { RepoConfig, ParseResult, ParsedEntity, ParsedImport } from '../util/types.js';
|
|
21
|
+
import { parseFile, getLanguage } from './parser.js';
|
|
22
|
+
import { chunkFromEntities, chunkFixed } from './chunker.js';
|
|
23
|
+
import { chunkMarkdown } from './markdown-chunker.js';
|
|
24
|
+
import { extractKnowledge } from './markdown-knowledge.js';
|
|
25
|
+
import { embedBatch, embedText } from './embedder.js';
|
|
26
|
+
import { hashContent } from '../storage/file-cache.js';
|
|
27
|
+
import { logger } from '../util/logger.js';
|
|
28
|
+
import { runGitAnalysis, analyzeFileStats, extractCommitRecords } from './git-analyzer.js';
|
|
29
|
+
import { MARK_SYMBOL_OBSERVATIONS_STALE } from '../query/graph-queries.js';
|
|
30
|
+
|
|
31
|
+
// ============================================================
|
|
32
|
+
// Inline routing helpers (replaces knowledge/routing-engine.js import)
|
|
33
|
+
// ============================================================
|
|
34
|
+
|
|
35
|
+
interface IndexingRoute {
|
|
36
|
+
writeGraphNode: boolean;
|
|
37
|
+
writeEntityNodes: boolean;
|
|
38
|
+
writeVectorChunks: boolean;
|
|
39
|
+
writeFileCache: boolean;
|
|
40
|
+
parseAst: boolean;
|
|
41
|
+
watchForChanges: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isOwnedRepo(repoId: string, repos: RepoConfig[]): boolean {
|
|
45
|
+
const repo = repos.find(r => r.id === repoId);
|
|
46
|
+
return repo?.ownership === 'owned';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getIndexingRoute(repoId: string, repos: RepoConfig[]): IndexingRoute {
|
|
50
|
+
const owned = isOwnedRepo(repoId, repos);
|
|
51
|
+
return {
|
|
52
|
+
writeGraphNode: owned,
|
|
53
|
+
writeEntityNodes: owned,
|
|
54
|
+
writeVectorChunks: true, // Always write vectors
|
|
55
|
+
writeFileCache: owned, // Cache only owned repo files
|
|
56
|
+
parseAst: owned, // Full AST parse only for owned repos
|
|
57
|
+
watchForChanges: owned, // Watch only owned repos
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ============================================================
|
|
62
|
+
// Module state
|
|
63
|
+
// ============================================================
|
|
64
|
+
|
|
65
|
+
// Track which files are indexed and their content hashes
|
|
66
|
+
const indexedFiles = new Map<string, { contentHash: string; indexedAt: number }>();
|
|
67
|
+
|
|
68
|
+
// Track indexing stats per repo
|
|
69
|
+
const repoStats = new Map<string, {
|
|
70
|
+
filesIndexed: number;
|
|
71
|
+
filesTotal: number;
|
|
72
|
+
lastIndexedAt: number;
|
|
73
|
+
}>();
|
|
74
|
+
|
|
75
|
+
// Track all entities discovered per repo for CALLS edge analysis
|
|
76
|
+
// Map<repoId, Map<entityName, filePath>>
|
|
77
|
+
const repoEntityRegistry = new Map<string, Map<string, string>>();
|
|
78
|
+
|
|
79
|
+
const NON_CODE_EXTENSIONS = new Set([
|
|
80
|
+
'.json', '.yaml', '.yml', '.toml', '.md', '.mdx',
|
|
81
|
+
'.graphql', '.gql', '.sql', '.txt', '.csv',
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
const CODE_EXTENSIONS = new Set([
|
|
85
|
+
'.ts', '.tsx', '.js', '.jsx', '.mts', '.cts', '.mjs', '.cjs',
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
const SKIP_EXTENSIONS = new Set([
|
|
89
|
+
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.webp',
|
|
90
|
+
'.ttf', '.woff', '.woff2', '.eot',
|
|
91
|
+
'.pdf', '.docx', '.xlsx',
|
|
92
|
+
'.zip', '.tar', '.gz',
|
|
93
|
+
'.exe', '.dll', '.so', '.dylib',
|
|
94
|
+
'.lock',
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
export interface IndexerOptions {
|
|
98
|
+
repoConfig: RepoConfig;
|
|
99
|
+
graphStore: GraphStore;
|
|
100
|
+
vectorStore: VectorStore;
|
|
101
|
+
fileCache: FileContentCache;
|
|
102
|
+
maxFileSizeKb?: number;
|
|
103
|
+
/**
|
|
104
|
+
* Full list of repos from config. Required for routing decisions.
|
|
105
|
+
* If omitted, the indexer falls back to treating the repo as owned
|
|
106
|
+
* (safe backward-compatible default — single-repo setups are always owned).
|
|
107
|
+
*/
|
|
108
|
+
allRepos?: RepoConfig[];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface IndexResult {
|
|
112
|
+
repoId: string;
|
|
113
|
+
filesProcessed: number;
|
|
114
|
+
filesSkipped: number;
|
|
115
|
+
filesErrored: number;
|
|
116
|
+
chunksCreated: number;
|
|
117
|
+
knowledgeItemsExtracted: number;
|
|
118
|
+
errors: string[];
|
|
119
|
+
durationMs: number;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Patterns to skip during directory walk
|
|
123
|
+
const SKIP_DIRS = new Set([
|
|
124
|
+
'node_modules', '.git', 'dist', 'build', '.cache', '.next', 'coverage',
|
|
125
|
+
'__pycache__', '.pytest_cache', 'vendor', 'target',
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Walk a directory tree and collect all file paths.
|
|
130
|
+
* Respects SKIP_DIRS and file extension filters.
|
|
131
|
+
*/
|
|
132
|
+
async function walkDir(dirPath: string, results: string[] = []): Promise<string[]> {
|
|
133
|
+
let entries;
|
|
134
|
+
try {
|
|
135
|
+
entries = await readdir(dirPath, { withFileTypes: true });
|
|
136
|
+
} catch {
|
|
137
|
+
return results;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
const fullPath = join(dirPath, entry.name);
|
|
142
|
+
|
|
143
|
+
if (entry.isDirectory()) {
|
|
144
|
+
if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
|
|
145
|
+
await walkDir(fullPath, results);
|
|
146
|
+
}
|
|
147
|
+
} else if (entry.isFile()) {
|
|
148
|
+
const ext = extname(entry.name).toLowerCase();
|
|
149
|
+
if (!SKIP_EXTENSIONS.has(ext) && (CODE_EXTENSIONS.has(ext) || NON_CODE_EXTENSIONS.has(ext))) {
|
|
150
|
+
results.push(fullPath);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return results;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Perform a full index of a repository.
|
|
160
|
+
* Walks all files, computes content hashes, skips unchanged, indexes new/changed.
|
|
161
|
+
* Phase 2: also creates FalkorDB indexes and runs git analysis afterward.
|
|
162
|
+
*/
|
|
163
|
+
export async function fullIndex(opts: IndexerOptions): Promise<IndexResult> {
|
|
164
|
+
const { repoConfig, graphStore } = opts;
|
|
165
|
+
const { id: repoId, path: repoPath } = repoConfig;
|
|
166
|
+
const startTime = Date.now();
|
|
167
|
+
|
|
168
|
+
const result: IndexResult = {
|
|
169
|
+
repoId,
|
|
170
|
+
filesProcessed: 0,
|
|
171
|
+
filesSkipped: 0,
|
|
172
|
+
filesErrored: 0,
|
|
173
|
+
chunksCreated: 0,
|
|
174
|
+
knowledgeItemsExtracted: 0,
|
|
175
|
+
errors: [],
|
|
176
|
+
durationMs: 0,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
logger.info('Starting full index', { repoId, path: repoPath });
|
|
180
|
+
|
|
181
|
+
// Phase 2: Ensure FalkorDB indexes exist before indexing
|
|
182
|
+
try {
|
|
183
|
+
await graphStore.ensureIndexes();
|
|
184
|
+
} catch (err) {
|
|
185
|
+
logger.warn('Failed to ensure graph indexes', { error: String(err) });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Initialize entity registry for this repo
|
|
189
|
+
repoEntityRegistry.set(repoId, new Map());
|
|
190
|
+
|
|
191
|
+
// Walk the directory tree
|
|
192
|
+
let allFiles: string[];
|
|
193
|
+
try {
|
|
194
|
+
allFiles = await walkDir(resolve(repoPath));
|
|
195
|
+
} catch (err) {
|
|
196
|
+
const msg = `Failed to walk repo directory: ${String(err)}`;
|
|
197
|
+
result.errors.push(msg);
|
|
198
|
+
result.durationMs = Date.now() - startTime;
|
|
199
|
+
logger.error(msg, { repoId, path: repoPath });
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Update total file count in stats
|
|
204
|
+
repoStats.set(repoId, {
|
|
205
|
+
filesIndexed: repoStats.get(repoId)?.filesIndexed ?? 0,
|
|
206
|
+
filesTotal: allFiles.length,
|
|
207
|
+
lastIndexedAt: repoStats.get(repoId)?.lastIndexedAt ?? 0,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Process files in batches to avoid overwhelming memory
|
|
211
|
+
const BATCH_SIZE = 20;
|
|
212
|
+
for (let i = 0; i < allFiles.length; i += BATCH_SIZE) {
|
|
213
|
+
const batch = allFiles.slice(i, i + BATCH_SIZE);
|
|
214
|
+
|
|
215
|
+
await Promise.all(batch.map(async (filePath) => {
|
|
216
|
+
try {
|
|
217
|
+
const { indexed, chunksCreated, knowledgeItemsExtracted } = await indexFile(filePath, repoId, opts);
|
|
218
|
+
if (indexed) {
|
|
219
|
+
result.filesProcessed++;
|
|
220
|
+
result.chunksCreated += chunksCreated;
|
|
221
|
+
result.knowledgeItemsExtracted += knowledgeItemsExtracted;
|
|
222
|
+
} else {
|
|
223
|
+
result.filesSkipped++;
|
|
224
|
+
}
|
|
225
|
+
} catch (err) {
|
|
226
|
+
result.filesErrored++;
|
|
227
|
+
result.errors.push(`${filePath}: ${String(err)}`);
|
|
228
|
+
logger.error('Error indexing file', { filePath, error: String(err) });
|
|
229
|
+
}
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Phase 2: Build CALLS edges across the repo using the entity registry.
|
|
234
|
+
const allReposForFullIndex = opts.allRepos ?? [opts.repoConfig];
|
|
235
|
+
const fullIndexRoute = getIndexingRoute(repoId, allReposForFullIndex);
|
|
236
|
+
if (fullIndexRoute.writeEntityNodes) {
|
|
237
|
+
await buildCallsEdges(repoId, opts);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Phase 2: Run git analysis for CO_MODIFIED and TESTS edges
|
|
241
|
+
try {
|
|
242
|
+
await runGitAnalysis(
|
|
243
|
+
resolve(repoPath),
|
|
244
|
+
repoId,
|
|
245
|
+
allFiles,
|
|
246
|
+
opts.graphStore
|
|
247
|
+
);
|
|
248
|
+
} catch (err) {
|
|
249
|
+
const msg = `Git analysis failed: ${String(err)}`;
|
|
250
|
+
result.errors.push(msg);
|
|
251
|
+
logger.warn(msg, { repoId });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Phase 3: Git history enrichment — file stats + commit record embeddings
|
|
255
|
+
try {
|
|
256
|
+
const resolvedRepoPath = resolve(repoPath);
|
|
257
|
+
|
|
258
|
+
// 3a. Analyze per-file stats and write to FalkorDB File nodes
|
|
259
|
+
const fileStats = await analyzeFileStats(resolvedRepoPath);
|
|
260
|
+
|
|
261
|
+
if (fileStats.size > 0 && fullIndexRoute.writeGraphNode) {
|
|
262
|
+
let statsWritten = 0;
|
|
263
|
+
for (const [filePath, stats] of fileStats) {
|
|
264
|
+
try {
|
|
265
|
+
await opts.graphStore.upsertNode(
|
|
266
|
+
'File',
|
|
267
|
+
{ path: filePath, repo_id: repoId },
|
|
268
|
+
{
|
|
269
|
+
commit_count: stats.commitCount,
|
|
270
|
+
stability_score: stats.stabilityScore,
|
|
271
|
+
change_velocity: stats.changeVelocity,
|
|
272
|
+
last_commit_hash: stats.lastCommitHash,
|
|
273
|
+
last_commit_ts: stats.lastCommitTs,
|
|
274
|
+
}
|
|
275
|
+
);
|
|
276
|
+
statsWritten++;
|
|
277
|
+
} catch {
|
|
278
|
+
// Non-fatal: file node may not exist if it was filtered during indexing
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
logger.info('Git file stats written to graph', { repoId, statsWritten, totalFiles: fileStats.size });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 3b. Extract commit records and embed messages into git_commits collection
|
|
285
|
+
const commits = await extractCommitRecords(resolvedRepoPath);
|
|
286
|
+
|
|
287
|
+
if (commits.length > 0) {
|
|
288
|
+
// Check if vectorStore supports git commit operations (QdrantVectorStore does)
|
|
289
|
+
const store = opts.vectorStore as typeof opts.vectorStore & {
|
|
290
|
+
upsertGitCommits?: (commits: Array<{ id: string; vector: number[]; payload: Record<string, unknown> }>) => Promise<void>;
|
|
291
|
+
deleteGitCommitsByRepo?: (repoId: string) => Promise<void>;
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
if (typeof store.upsertGitCommits === 'function') {
|
|
295
|
+
// Delete existing git commits for this repo before re-indexing
|
|
296
|
+
if (typeof store.deleteGitCommitsByRepo === 'function') {
|
|
297
|
+
await store.deleteGitCommitsByRepo(repoId);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Embed commit messages in batches
|
|
301
|
+
const COMMIT_BATCH = 50;
|
|
302
|
+
let totalUpserted = 0;
|
|
303
|
+
|
|
304
|
+
for (let i = 0; i < commits.length; i += COMMIT_BATCH) {
|
|
305
|
+
const batch = commits.slice(i, i + COMMIT_BATCH);
|
|
306
|
+
const texts = batch.map(c => c.message);
|
|
307
|
+
const vectors = await embedBatch(texts);
|
|
308
|
+
const ZERO_VEC = new Array(384).fill(0) as number[];
|
|
309
|
+
|
|
310
|
+
const points = batch.map((commit, idx) => {
|
|
311
|
+
// Generate a deterministic ID from repo_id + commit hash
|
|
312
|
+
const idSource = `${repoId}:${commit.hash}`;
|
|
313
|
+
const id = createHash('sha256').update(idSource).digest('hex').slice(0, 32);
|
|
314
|
+
const uuid = `${id.slice(0, 8)}-${id.slice(8, 12)}-4${id.slice(13, 16)}-8${id.slice(17, 20)}-${id.slice(20, 32)}`;
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
id: uuid,
|
|
318
|
+
vector: vectors[idx] ?? ZERO_VEC,
|
|
319
|
+
payload: {
|
|
320
|
+
repo_id: repoId,
|
|
321
|
+
commit_hash: commit.hash,
|
|
322
|
+
message: commit.message,
|
|
323
|
+
author: commit.author,
|
|
324
|
+
timestamp: commit.timestamp,
|
|
325
|
+
file_paths: commit.files,
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
await store.upsertGitCommits(points);
|
|
331
|
+
totalUpserted += points.length;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
logger.info('Git commit messages embedded', { repoId, commitsEmbedded: totalUpserted });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
} catch (err) {
|
|
338
|
+
const msg = `Git enrichment (Phase 3) failed: ${String(err)}`;
|
|
339
|
+
result.errors.push(msg);
|
|
340
|
+
logger.warn(msg, { repoId });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Update stats
|
|
344
|
+
const now = Date.now();
|
|
345
|
+
repoStats.set(repoId, {
|
|
346
|
+
filesIndexed: result.filesProcessed,
|
|
347
|
+
filesTotal: allFiles.length,
|
|
348
|
+
lastIndexedAt: now,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
result.durationMs = now - startTime;
|
|
352
|
+
logger.info('Full index complete', {
|
|
353
|
+
repoId,
|
|
354
|
+
filesProcessed: result.filesProcessed,
|
|
355
|
+
filesSkipped: result.filesSkipped,
|
|
356
|
+
filesErrored: result.filesErrored,
|
|
357
|
+
chunksCreated: result.chunksCreated,
|
|
358
|
+
knowledgeItemsExtracted: result.knowledgeItemsExtracted,
|
|
359
|
+
errors: result.errors.length,
|
|
360
|
+
durationMs: result.durationMs,
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
return result;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Re-index a single file after a change event (incremental update).
|
|
368
|
+
* Deletes old data, re-indexes with fresh content.
|
|
369
|
+
*/
|
|
370
|
+
export async function incrementalIndex(
|
|
371
|
+
filePath: string,
|
|
372
|
+
repoId: string,
|
|
373
|
+
opts: IndexerOptions
|
|
374
|
+
): Promise<void> {
|
|
375
|
+
logger.debug('Incremental re-index', { filePath, repoId });
|
|
376
|
+
|
|
377
|
+
// Delete old vectors and graph nodes first
|
|
378
|
+
await opts.vectorStore.deleteFileChunks(filePath, repoId);
|
|
379
|
+
await opts.graphStore.deleteFile(filePath, repoId);
|
|
380
|
+
opts.fileCache.invalidate(repoId, filePath);
|
|
381
|
+
|
|
382
|
+
// Remove from in-memory tracking so indexFile doesn't skip it
|
|
383
|
+
indexedFiles.delete(`${repoId}:${filePath}`);
|
|
384
|
+
|
|
385
|
+
// Re-index
|
|
386
|
+
const { indexed, chunksCreated } = await indexFile(filePath, repoId, opts);
|
|
387
|
+
if (indexed) {
|
|
388
|
+
logger.debug('Incremental re-index complete', { filePath, repoId, chunksCreated });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Remove all data for a repo from vector store and graph.
|
|
394
|
+
*/
|
|
395
|
+
export async function removeRepoIndex(repoId: string, opts: IndexerOptions): Promise<void> {
|
|
396
|
+
logger.info('Removing repo index', { repoId });
|
|
397
|
+
await opts.vectorStore.deleteRepoChunks(repoId);
|
|
398
|
+
await opts.graphStore.deleteRepo(repoId);
|
|
399
|
+
opts.fileCache.invalidateRepo(repoId);
|
|
400
|
+
|
|
401
|
+
// Clear in-memory tracking for this repo
|
|
402
|
+
const prefix = `${repoId}:`;
|
|
403
|
+
for (const key of indexedFiles.keys()) {
|
|
404
|
+
if (key.startsWith(prefix)) {
|
|
405
|
+
indexedFiles.delete(key);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
repoStats.delete(repoId);
|
|
409
|
+
repoEntityRegistry.delete(repoId);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Index a single file (full pipeline).
|
|
414
|
+
* Returns true if indexed, false if skipped.
|
|
415
|
+
*
|
|
416
|
+
* Phase 2: also populates IMPORTS, EXPORTS, EXTENDS, IMPLEMENTS edges.
|
|
417
|
+
* Phase 5: added forceReindex flag (bypasses hash check) and debug logging for skips.
|
|
418
|
+
*/
|
|
419
|
+
export async function indexFile(
|
|
420
|
+
filePath: string,
|
|
421
|
+
repoId: string,
|
|
422
|
+
opts: IndexerOptions,
|
|
423
|
+
forceReindex = false
|
|
424
|
+
): Promise<{ indexed: boolean; chunksCreated: number; knowledgeItemsExtracted: number }> {
|
|
425
|
+
const { graphStore, vectorStore, fileCache, maxFileSizeKb = 500 } = opts;
|
|
426
|
+
|
|
427
|
+
const allRepos = opts.allRepos ?? [opts.repoConfig];
|
|
428
|
+
const route = getIndexingRoute(repoId, allRepos);
|
|
429
|
+
|
|
430
|
+
logger.debug('Indexing route determined', {
|
|
431
|
+
repoId,
|
|
432
|
+
filePath,
|
|
433
|
+
writeGraphNode: route.writeGraphNode,
|
|
434
|
+
writeEntityNodes: route.writeEntityNodes,
|
|
435
|
+
parseAst: route.parseAst,
|
|
436
|
+
writeFileCache: route.writeFileCache,
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
try {
|
|
440
|
+
// Check file size
|
|
441
|
+
const fileStat = await stat(filePath);
|
|
442
|
+
if (fileStat.size > maxFileSizeKb * 1024) {
|
|
443
|
+
logger.debug('Skipping large file', { filePath, sizeKb: Math.round(fileStat.size / 1024) });
|
|
444
|
+
return { indexed: false, chunksCreated: 0, knowledgeItemsExtracted: 0 };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Read file content
|
|
448
|
+
const content = await readFile(filePath, 'utf8');
|
|
449
|
+
const contentHash = hashContent(content);
|
|
450
|
+
|
|
451
|
+
// Check if already indexed with same hash (skip unless forceReindex is set)
|
|
452
|
+
const existing = indexedFiles.get(`${repoId}:${filePath}`);
|
|
453
|
+
if (existing && existing.contentHash === contentHash && !forceReindex) {
|
|
454
|
+
logger.debug('Skipping unchanged file', { filePath, repoId, contentHash });
|
|
455
|
+
return { indexed: false, chunksCreated: 0, knowledgeItemsExtracted: 0 };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (forceReindex && existing) {
|
|
459
|
+
logger.debug('Force re-indexing file (bypassing hash check)', { filePath, repoId });
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Remove old data for this file
|
|
463
|
+
if (existing) {
|
|
464
|
+
await vectorStore.deleteFileChunks(filePath, repoId);
|
|
465
|
+
|
|
466
|
+
// Phase 3: Mark observations stale for changed symbols before deleting graph nodes.
|
|
467
|
+
// Re-parse the old file's entities to find symbols that may have changed.
|
|
468
|
+
if (route.writeEntityNodes) {
|
|
469
|
+
try {
|
|
470
|
+
const oldParseResult = await parseFile(filePath, repoId);
|
|
471
|
+
if (oldParseResult.success) {
|
|
472
|
+
for (const entity of oldParseResult.entities) {
|
|
473
|
+
try {
|
|
474
|
+
await graphStore.query(
|
|
475
|
+
MARK_SYMBOL_OBSERVATIONS_STALE(entity.name, repoId)
|
|
476
|
+
);
|
|
477
|
+
} catch {
|
|
478
|
+
// Non-fatal: observation staleness marking is best-effort
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
} catch {
|
|
483
|
+
// Non-fatal: if we can't re-parse, skip staleness marking
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (route.writeGraphNode) {
|
|
488
|
+
await graphStore.deleteFile(filePath, repoId);
|
|
489
|
+
}
|
|
490
|
+
if (route.writeFileCache) {
|
|
491
|
+
fileCache.invalidate(repoId, filePath);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const ext = extname(filePath).toLowerCase();
|
|
496
|
+
const language = getLanguage(filePath) ?? ext.slice(1) ?? 'unknown';
|
|
497
|
+
const now = Date.now();
|
|
498
|
+
|
|
499
|
+
let chunks;
|
|
500
|
+
|
|
501
|
+
if (SKIP_EXTENSIONS.has(ext)) {
|
|
502
|
+
return { indexed: false, chunksCreated: 0, knowledgeItemsExtracted: 0 };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (CODE_EXTENSIONS.has(ext)) {
|
|
506
|
+
if (route.parseAst) {
|
|
507
|
+
// Full AST parse pipeline — owned repos get graph nodes + entity extraction
|
|
508
|
+
const parseResult = await parseFile(filePath, repoId);
|
|
509
|
+
|
|
510
|
+
// Store file node in graph only for owned repos
|
|
511
|
+
if (route.writeGraphNode) {
|
|
512
|
+
await upsertFileNode(graphStore, filePath, repoId, contentHash, language, now);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Phase 2: Store entity nodes and all graph edges (owned repos only)
|
|
516
|
+
if (route.writeEntityNodes && parseResult.success) {
|
|
517
|
+
await upsertEntitiesWithEdges(graphStore, parseResult, content, repoId, filePath);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Generate chunks from AST entities
|
|
521
|
+
if (parseResult.success && parseResult.entities.length > 0) {
|
|
522
|
+
chunks = chunkFromEntities(
|
|
523
|
+
parseResult.entities,
|
|
524
|
+
parseResult.imports,
|
|
525
|
+
content,
|
|
526
|
+
filePath,
|
|
527
|
+
repoId,
|
|
528
|
+
language,
|
|
529
|
+
now
|
|
530
|
+
);
|
|
531
|
+
} else {
|
|
532
|
+
// Fallback to fixed chunking if AST parse failed or no entities
|
|
533
|
+
chunks = chunkFixed(content, filePath, repoId, language, now);
|
|
534
|
+
}
|
|
535
|
+
} else {
|
|
536
|
+
// External repo: skip AST parse entirely, use fixed chunking only.
|
|
537
|
+
// This keeps vectors (semantic search) without polluting the graph.
|
|
538
|
+
chunks = chunkFixed(content, filePath, repoId, language, now);
|
|
539
|
+
logger.debug('External repo — skipping AST parse, using fixed chunking', { filePath, repoId });
|
|
540
|
+
}
|
|
541
|
+
} else if (NON_CODE_EXTENSIONS.has(ext)) {
|
|
542
|
+
// Smart markdown chunking for .md/.mdx, fixed chunking for other non-code
|
|
543
|
+
if (ext === '.md' || ext === '.mdx') {
|
|
544
|
+
chunks = chunkMarkdown(content, filePath, repoId, now);
|
|
545
|
+
} else {
|
|
546
|
+
chunks = chunkFixed(content, filePath, repoId, language, now);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Store a File node only for owned repos
|
|
550
|
+
if (route.writeGraphNode) {
|
|
551
|
+
await upsertFileNode(graphStore, filePath, repoId, contentHash, language, now);
|
|
552
|
+
}
|
|
553
|
+
} else {
|
|
554
|
+
return { indexed: false, chunksCreated: 0, knowledgeItemsExtracted: 0 };
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (chunks.length === 0) {
|
|
558
|
+
return { indexed: false, chunksCreated: 0, knowledgeItemsExtracted: 0 };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Embed all chunks in batches (always — vectors written for all repos)
|
|
562
|
+
const chunkTexts = chunks.map(c => c.content);
|
|
563
|
+
const vectors = await embedBatch(chunkTexts);
|
|
564
|
+
|
|
565
|
+
// Upsert to Qdrant (route.writeVectorChunks is always true per routing logic)
|
|
566
|
+
const ZERO_VECTOR = new Array(384).fill(0) as number[];
|
|
567
|
+
const qdrantPoints = chunks.map((chunk, i) => ({
|
|
568
|
+
id: chunk.id,
|
|
569
|
+
vector: vectors[i] ?? ZERO_VECTOR,
|
|
570
|
+
payload: {
|
|
571
|
+
repo_id: chunk.repoId,
|
|
572
|
+
file_path: chunk.filePath,
|
|
573
|
+
entity_name: chunk.entityName,
|
|
574
|
+
entity_type: chunk.entityType,
|
|
575
|
+
start_line: chunk.startLine,
|
|
576
|
+
end_line: chunk.endLine,
|
|
577
|
+
language: chunk.language,
|
|
578
|
+
content_hash: chunk.contentHash,
|
|
579
|
+
content_preview: chunk.contentPreview,
|
|
580
|
+
indexed_at: chunk.indexedAt,
|
|
581
|
+
} as const,
|
|
582
|
+
}));
|
|
583
|
+
|
|
584
|
+
await vectorStore.upsertCodeChunks(qdrantPoints);
|
|
585
|
+
|
|
586
|
+
// Extract and upsert knowledge items from markdown files
|
|
587
|
+
let knowledgeCount = 0;
|
|
588
|
+
if (ext === '.md' || ext === '.mdx') {
|
|
589
|
+
try {
|
|
590
|
+
// Resolve stack tags from repo config for knowledge items
|
|
591
|
+
const repoManifestStack = opts.repoConfig.languages ?? [];
|
|
592
|
+
const knowledgeItems = extractKnowledge(content, filePath, repoId, repoManifestStack);
|
|
593
|
+
|
|
594
|
+
if (knowledgeItems.length > 0) {
|
|
595
|
+
// Check if vectorStore has upsertKnowledge (QdrantVectorStore does)
|
|
596
|
+
const store = vectorStore as typeof vectorStore & {
|
|
597
|
+
upsertKnowledge?: (id: string, vector: number[], payload: Record<string, unknown>) => Promise<void>;
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
if (typeof store.upsertKnowledge === 'function') {
|
|
601
|
+
for (const item of knowledgeItems) {
|
|
602
|
+
try {
|
|
603
|
+
const text = `${item.title} ${item.content}`;
|
|
604
|
+
const [vector] = await embedBatch([text]);
|
|
605
|
+
const ZERO_VEC = new Array(384).fill(0) as number[];
|
|
606
|
+
await store.upsertKnowledge(item.id, vector ?? ZERO_VEC, {
|
|
607
|
+
id: item.id,
|
|
608
|
+
repo_id: repoId,
|
|
609
|
+
category: item.id.split('-')[0] ?? 'pattern',
|
|
610
|
+
title: item.title,
|
|
611
|
+
content: item.content,
|
|
612
|
+
stack_tags: item.stack_tags,
|
|
613
|
+
confidence: item.confidence,
|
|
614
|
+
source: item.source,
|
|
615
|
+
source_phase: item.source_phase,
|
|
616
|
+
source_agent: item.source_agent,
|
|
617
|
+
sharing: opts.repoConfig.ownership === 'owned' ? 'team' : 'private',
|
|
618
|
+
created_at: item.created_at,
|
|
619
|
+
updated_at: item.updated_at,
|
|
620
|
+
accessed_at: now,
|
|
621
|
+
access_count: 0,
|
|
622
|
+
});
|
|
623
|
+
knowledgeCount++;
|
|
624
|
+
} catch (kErr) {
|
|
625
|
+
logger.warn('Failed to upsert knowledge item from markdown', {
|
|
626
|
+
id: item.id,
|
|
627
|
+
filePath,
|
|
628
|
+
error: String(kErr),
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
if (knowledgeCount > 0) {
|
|
633
|
+
logger.debug('Knowledge items extracted from markdown', {
|
|
634
|
+
filePath, repoId, count: knowledgeCount,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
} catch (kErr) {
|
|
640
|
+
logger.warn('Knowledge extraction failed for markdown file', {
|
|
641
|
+
filePath, error: String(kErr),
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Cache file content only for owned repos (RAM budget control)
|
|
647
|
+
if (route.writeFileCache) {
|
|
648
|
+
fileCache.set(repoId, filePath, content, contentHash);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Update indexed files tracking
|
|
652
|
+
indexedFiles.set(`${repoId}:${filePath}`, { contentHash, indexedAt: now });
|
|
653
|
+
|
|
654
|
+
logger.debug('File indexed', {
|
|
655
|
+
filePath,
|
|
656
|
+
repoId,
|
|
657
|
+
chunks: chunks.length,
|
|
658
|
+
knowledgeItems: knowledgeCount,
|
|
659
|
+
language,
|
|
660
|
+
owned: route.writeGraphNode,
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
return { indexed: true, chunksCreated: chunks.length, knowledgeItemsExtracted: knowledgeCount };
|
|
664
|
+
} catch (err) {
|
|
665
|
+
logger.error('Failed to index file', { filePath, error: String(err) });
|
|
666
|
+
return { indexed: false, chunksCreated: 0, knowledgeItemsExtracted: 0 };
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Get repo index stats.
|
|
672
|
+
*/
|
|
673
|
+
export function getRepoStats(repoId: string) {
|
|
674
|
+
return repoStats.get(repoId) ?? {
|
|
675
|
+
filesIndexed: 0,
|
|
676
|
+
filesTotal: 0,
|
|
677
|
+
lastIndexedAt: 0,
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Get all indexed files for a repo.
|
|
683
|
+
*/
|
|
684
|
+
export function getIndexedFiles(repoId: string): string[] {
|
|
685
|
+
const prefix = `${repoId}:`;
|
|
686
|
+
return Array.from(indexedFiles.keys())
|
|
687
|
+
.filter(k => k.startsWith(prefix))
|
|
688
|
+
.map(k => k.slice(prefix.length));
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// ============================================================
|
|
692
|
+
// Phase 2: Graph population helpers
|
|
693
|
+
// ============================================================
|
|
694
|
+
|
|
695
|
+
async function upsertFileNode(
|
|
696
|
+
graphStore: GraphStore,
|
|
697
|
+
filePath: string,
|
|
698
|
+
repoId: string,
|
|
699
|
+
contentHash: string,
|
|
700
|
+
language: string,
|
|
701
|
+
now: number
|
|
702
|
+
): Promise<void> {
|
|
703
|
+
try {
|
|
704
|
+
await graphStore.upsertNode(
|
|
705
|
+
'File',
|
|
706
|
+
{ path: filePath, repo_id: repoId },
|
|
707
|
+
{
|
|
708
|
+
content_hash: contentHash,
|
|
709
|
+
language,
|
|
710
|
+
last_indexed: now,
|
|
711
|
+
size_bytes: 0,
|
|
712
|
+
}
|
|
713
|
+
);
|
|
714
|
+
} catch (err) {
|
|
715
|
+
logger.warn('Failed to upsert file node', { filePath, error: String(err) });
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Upsert all entities for a file and create the full set of edges:
|
|
721
|
+
* CONTAINS (File -> entity)
|
|
722
|
+
* EXPORTS (File -> entity, for exported symbols)
|
|
723
|
+
* IMPORTS (File -> File, from import statements)
|
|
724
|
+
* EXTENDS (Class -> Class, when extends clause is present)
|
|
725
|
+
* IMPLEMENTS (Class -> Interface, when implements clause is present)
|
|
726
|
+
*
|
|
727
|
+
* Also registers entity names in the repo entity registry for later CALLS analysis.
|
|
728
|
+
*/
|
|
729
|
+
async function upsertEntitiesWithEdges(
|
|
730
|
+
graphStore: GraphStore,
|
|
731
|
+
parseResult: ParseResult,
|
|
732
|
+
_content: string,
|
|
733
|
+
repoId: string,
|
|
734
|
+
filePath: string
|
|
735
|
+
): Promise<void> {
|
|
736
|
+
const entityRegistry = repoEntityRegistry.get(repoId) ?? new Map<string, string>();
|
|
737
|
+
repoEntityRegistry.set(repoId, entityRegistry);
|
|
738
|
+
|
|
739
|
+
// 1. Upsert entity nodes + CONTAINS + EXPORTS edges
|
|
740
|
+
for (const entity of parseResult.entities) {
|
|
741
|
+
await upsertEntityNode(graphStore, entity);
|
|
742
|
+
|
|
743
|
+
// Register in entity registry for CALLS edge discovery
|
|
744
|
+
entityRegistry.set(entity.name, filePath);
|
|
745
|
+
|
|
746
|
+
// Create CONTAINS edge
|
|
747
|
+
await safeUpsertEdge(
|
|
748
|
+
graphStore,
|
|
749
|
+
'File', { path: filePath, repo_id: repoId },
|
|
750
|
+
'CONTAINS', {},
|
|
751
|
+
entityLabel(entity.type), { name: entity.name, file_path: filePath, repo_id: repoId }
|
|
752
|
+
);
|
|
753
|
+
|
|
754
|
+
// Create EXPORTS edge for exported symbols
|
|
755
|
+
if (entity.isExported) {
|
|
756
|
+
await safeUpsertEdge(
|
|
757
|
+
graphStore,
|
|
758
|
+
'File', { path: filePath, repo_id: repoId },
|
|
759
|
+
'EXPORTS', { is_default: entity.isDefault ?? false },
|
|
760
|
+
entityLabel(entity.type), { name: entity.name, file_path: filePath, repo_id: repoId }
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Phase 2: EXTENDS and IMPLEMENTS edges (extracted from sourceText heuristics)
|
|
765
|
+
if (entity.type === 'class' && entity.sourceText) {
|
|
766
|
+
await createClassRelationshipEdges(graphStore, entity, repoId);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// 2. Create IMPORTS edges from import declarations
|
|
771
|
+
for (const imp of parseResult.imports) {
|
|
772
|
+
const resolvedPath = resolveImportPath(filePath, imp.fromPath, repoId);
|
|
773
|
+
if (resolvedPath) {
|
|
774
|
+
await safeUpsertEdge(
|
|
775
|
+
graphStore,
|
|
776
|
+
'File', { path: filePath, repo_id: repoId },
|
|
777
|
+
'IMPORTS', { specifiers: imp.specifiers },
|
|
778
|
+
'File', { path: resolvedPath, repo_id: repoId }
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Create EXTENDS and IMPLEMENTS edges for class declarations.
|
|
786
|
+
* Uses simple regex-based heuristics on the class source text since we
|
|
787
|
+
* don't have full type resolution. Handles:
|
|
788
|
+
* class Foo extends Bar { ... }
|
|
789
|
+
* class Foo implements IFoo, IBar { ... }
|
|
790
|
+
* class Foo extends Bar implements IFoo { ... }
|
|
791
|
+
*/
|
|
792
|
+
async function createClassRelationshipEdges(
|
|
793
|
+
graphStore: GraphStore,
|
|
794
|
+
classEntity: ParsedEntity,
|
|
795
|
+
repoId: string
|
|
796
|
+
): Promise<void> {
|
|
797
|
+
const src = classEntity.sourceText;
|
|
798
|
+
|
|
799
|
+
// Extract extends clause: class Foo extends BarName
|
|
800
|
+
const extendsMatch = src.match(/\bextends\s+([A-Za-z_$][A-Za-z0-9_$]*)/);
|
|
801
|
+
if (extendsMatch) {
|
|
802
|
+
const parentName = extendsMatch[1];
|
|
803
|
+
// Create EXTENDS edge — target Class node may or may not exist yet
|
|
804
|
+
await safeUpsertEdge(
|
|
805
|
+
graphStore,
|
|
806
|
+
'Class', { name: classEntity.name, file_path: classEntity.filePath, repo_id: repoId },
|
|
807
|
+
'EXTENDS', {},
|
|
808
|
+
'Class', { name: parentName, repo_id: repoId }
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Extract implements clause: implements IFoo, IBar<T>
|
|
813
|
+
const implementsMatch = src.match(/\bimplements\s+([A-Za-z_$][A-Za-z0-9_$<>,\s]*?)(?:\s*\{|extends|implements|$)/);
|
|
814
|
+
if (implementsMatch) {
|
|
815
|
+
// Split on comma, strip generic type params
|
|
816
|
+
const interfaces = implementsMatch[1]!
|
|
817
|
+
.split(',')
|
|
818
|
+
.map(s => s.replace(/<[^>]*>/g, '').trim())
|
|
819
|
+
.filter(s => /^[A-Za-z_$]/.test(s));
|
|
820
|
+
|
|
821
|
+
for (const ifaceName of interfaces) {
|
|
822
|
+
await safeUpsertEdge(
|
|
823
|
+
graphStore,
|
|
824
|
+
'Class', { name: classEntity.name, file_path: classEntity.filePath, repo_id: repoId },
|
|
825
|
+
'IMPLEMENTS', {},
|
|
826
|
+
'Interface', { name: ifaceName, repo_id: repoId }
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Upsert a single entity node in FalkorDB.
|
|
834
|
+
*/
|
|
835
|
+
async function upsertEntityNode(graphStore: GraphStore, entity: ParsedEntity): Promise<void> {
|
|
836
|
+
const label = entityLabel(entity.type);
|
|
837
|
+
if (!label) return;
|
|
838
|
+
|
|
839
|
+
try {
|
|
840
|
+
await graphStore.upsertNode(
|
|
841
|
+
label,
|
|
842
|
+
{ name: entity.name, file_path: entity.filePath, repo_id: entity.repoId },
|
|
843
|
+
{
|
|
844
|
+
start_line: entity.startLine,
|
|
845
|
+
end_line: entity.endLine,
|
|
846
|
+
is_exported: entity.isExported,
|
|
847
|
+
is_async: entity.isAsync ?? false,
|
|
848
|
+
params: entity.params ?? '',
|
|
849
|
+
is_const: entity.isConst ?? false,
|
|
850
|
+
}
|
|
851
|
+
);
|
|
852
|
+
} catch (err) {
|
|
853
|
+
logger.warn('Failed to upsert entity node', { name: entity.name, error: String(err) });
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Build CALLS edges across the entire repo using static identifier matching.
|
|
859
|
+
* Algorithm: for each function entity, scan its sourceText for identifiers that
|
|
860
|
+
* match other known function names in the same repo. If found, create CALLS edge.
|
|
861
|
+
*
|
|
862
|
+
* This is Phase 2 static analysis — no type resolution required.
|
|
863
|
+
*/
|
|
864
|
+
async function buildCallsEdges(repoId: string, opts: IndexerOptions): Promise<void> {
|
|
865
|
+
const entityRegistry = repoEntityRegistry.get(repoId);
|
|
866
|
+
if (!entityRegistry || entityRegistry.size === 0) return;
|
|
867
|
+
|
|
868
|
+
logger.debug('Building CALLS edges via static analysis', {
|
|
869
|
+
repoId,
|
|
870
|
+
knownEntities: entityRegistry.size,
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
// We need to re-read parse results to get sourceText.
|
|
874
|
+
// We iterate over all indexed files for this repo.
|
|
875
|
+
const allIndexedFiles = getIndexedFiles(repoId);
|
|
876
|
+
|
|
877
|
+
for (const filePath of allIndexedFiles) {
|
|
878
|
+
const ext = extname(filePath).toLowerCase();
|
|
879
|
+
if (!CODE_EXTENSIONS.has(ext)) continue;
|
|
880
|
+
|
|
881
|
+
try {
|
|
882
|
+
const parseResult = await parseFile(filePath, repoId);
|
|
883
|
+
if (!parseResult.success) continue;
|
|
884
|
+
|
|
885
|
+
for (const entity of parseResult.entities) {
|
|
886
|
+
if (entity.type !== 'function') continue;
|
|
887
|
+
if (!entity.sourceText) continue;
|
|
888
|
+
|
|
889
|
+
// Find all identifiers in the function body that match known entity names
|
|
890
|
+
// Skip the entity's own name to avoid self-references
|
|
891
|
+
for (const [calleeName, calleePath] of entityRegistry) {
|
|
892
|
+
if (calleeName === entity.name) continue;
|
|
893
|
+
if (!isIdentifierReferenced(entity.sourceText, calleeName)) continue;
|
|
894
|
+
|
|
895
|
+
// Create CALLS edge
|
|
896
|
+
await safeUpsertEdge(
|
|
897
|
+
opts.graphStore,
|
|
898
|
+
'Function', { name: entity.name, file_path: filePath, repo_id: repoId },
|
|
899
|
+
'CALLS', { call_count: 1 },
|
|
900
|
+
'Function', { name: calleeName, file_path: calleePath, repo_id: repoId }
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
} catch (err) {
|
|
905
|
+
logger.warn('CALLS edge analysis failed for file', { filePath, error: String(err) });
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
logger.debug('CALLS edge construction complete', { repoId });
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Check if an identifier name appears as a standalone word in source text.
|
|
914
|
+
* Uses word boundary matching to avoid matching substrings.
|
|
915
|
+
*/
|
|
916
|
+
function isIdentifierReferenced(sourceText: string, identifierName: string): boolean {
|
|
917
|
+
// Escape any regex special chars in the name
|
|
918
|
+
const escaped = identifierName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
919
|
+
const pattern = new RegExp(`\\b${escaped}\\b`);
|
|
920
|
+
return pattern.test(sourceText);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* Resolve an import path specifier to an absolute file path.
|
|
925
|
+
* Only resolves relative imports (./foo, ../bar). Skips node_modules imports.
|
|
926
|
+
*/
|
|
927
|
+
function resolveImportPath(
|
|
928
|
+
fromFile: string,
|
|
929
|
+
importPath: string,
|
|
930
|
+
_repoId: string
|
|
931
|
+
): string | null {
|
|
932
|
+
// Skip non-relative imports (node_modules, absolute)
|
|
933
|
+
if (!importPath.startsWith('.')) return null;
|
|
934
|
+
if (isAbsolute(importPath)) return null;
|
|
935
|
+
|
|
936
|
+
const fromDir = dirname(fromFile);
|
|
937
|
+
const resolved = join(fromDir, importPath);
|
|
938
|
+
|
|
939
|
+
// Return the first candidate without extension check (graph allows forward references)
|
|
940
|
+
// The .ts extension is most common in TS projects
|
|
941
|
+
const withExt = resolved.endsWith('.js') || resolved.endsWith('.ts') ||
|
|
942
|
+
resolved.endsWith('.tsx') || resolved.endsWith('.jsx');
|
|
943
|
+
|
|
944
|
+
if (withExt) return resolved;
|
|
945
|
+
return `${resolved}.ts`; // Default assumption for TS projects
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* Map entity type string to FalkorDB graph label.
|
|
950
|
+
*/
|
|
951
|
+
function entityLabel(type: string): string {
|
|
952
|
+
const labelMap: Record<string, string> = {
|
|
953
|
+
function: 'Function',
|
|
954
|
+
class: 'Class',
|
|
955
|
+
interface: 'Interface',
|
|
956
|
+
type_alias: 'TypeAlias',
|
|
957
|
+
variable: 'Variable',
|
|
958
|
+
module: 'Module',
|
|
959
|
+
};
|
|
960
|
+
return labelMap[type] ?? '';
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Wrapper around graphStore.upsertEdge that catches and logs errors.
|
|
965
|
+
*/
|
|
966
|
+
async function safeUpsertEdge(
|
|
967
|
+
graphStore: GraphStore,
|
|
968
|
+
fromLabel: string,
|
|
969
|
+
fromProps: Record<string, unknown>,
|
|
970
|
+
edgeType: string,
|
|
971
|
+
edgeProps: Record<string, unknown>,
|
|
972
|
+
toLabel: string,
|
|
973
|
+
toProps: Record<string, unknown>
|
|
974
|
+
): Promise<void> {
|
|
975
|
+
try {
|
|
976
|
+
await graphStore.upsertEdge(fromLabel, fromProps, edgeType, edgeProps, toLabel, toProps);
|
|
977
|
+
} catch (err) {
|
|
978
|
+
logger.warn(`Failed to upsert ${edgeType} edge`, {
|
|
979
|
+
from: JSON.stringify(fromProps),
|
|
980
|
+
to: JSON.stringify(toProps),
|
|
981
|
+
error: String(err),
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
}
|