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,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FalkorDB implementation of GraphStore.
|
|
3
|
+
* FalkorDB speaks Redis protocol + GRAPH commands.
|
|
4
|
+
* Connects to localhost:6380 (remapped from container port 6379 per ADR-7).
|
|
5
|
+
*
|
|
6
|
+
* FalkorDB GRAPH.QUERY --compact response format:
|
|
7
|
+
* [
|
|
8
|
+
* ["header", [...column_names]],
|
|
9
|
+
* ["data", [[row], [row], ...]],
|
|
10
|
+
* ["stats", ["Query internal execution time: X ms"]]
|
|
11
|
+
* ]
|
|
12
|
+
*
|
|
13
|
+
* Without --compact:
|
|
14
|
+
* [header_array, data_array, stats_array]
|
|
15
|
+
* where header_array = [column_names]
|
|
16
|
+
* and data_array = [[cell, cell, ...], ...]
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { createClient, type RedisClientType } from 'redis';
|
|
20
|
+
import type { GraphStore } from './interfaces.js';
|
|
21
|
+
import type { GraphQueryResult } from '../util/types.js';
|
|
22
|
+
import { logger } from '../util/logger.js';
|
|
23
|
+
import { CircuitBreaker } from '../util/circuit-breaker.js';
|
|
24
|
+
|
|
25
|
+
const DEFAULT_QUERY_TIMEOUT_MS = 5000;
|
|
26
|
+
const RECONNECT_DELAY_MS = 1000;
|
|
27
|
+
const MAX_RECONNECT_ATTEMPTS = 3;
|
|
28
|
+
|
|
29
|
+
export class FalkorDBGraphStore implements GraphStore {
|
|
30
|
+
private client: RedisClientType | null = null;
|
|
31
|
+
private readonly url: string;
|
|
32
|
+
private readonly graphName: string;
|
|
33
|
+
private readonly queryTimeoutMs: number;
|
|
34
|
+
private connected = false;
|
|
35
|
+
private reconnectAttempts = 0;
|
|
36
|
+
private readonly breaker: CircuitBreaker;
|
|
37
|
+
|
|
38
|
+
constructor(
|
|
39
|
+
url: string = 'redis://localhost:6380',
|
|
40
|
+
graphName: string = 'forge_knowledge',
|
|
41
|
+
queryTimeoutMs: number = DEFAULT_QUERY_TIMEOUT_MS
|
|
42
|
+
) {
|
|
43
|
+
this.url = url;
|
|
44
|
+
this.graphName = graphName;
|
|
45
|
+
this.queryTimeoutMs = queryTimeoutMs;
|
|
46
|
+
this.breaker = new CircuitBreaker({
|
|
47
|
+
name: 'falkordb',
|
|
48
|
+
failureThreshold: 5,
|
|
49
|
+
resetTimeoutMs: 30_000,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async connect(): Promise<void> {
|
|
54
|
+
if (this.connected) return;
|
|
55
|
+
|
|
56
|
+
this.client = createClient({
|
|
57
|
+
url: this.url,
|
|
58
|
+
socket: {
|
|
59
|
+
connectTimeout: this.queryTimeoutMs,
|
|
60
|
+
reconnectStrategy: (retries) => {
|
|
61
|
+
if (retries >= MAX_RECONNECT_ATTEMPTS) {
|
|
62
|
+
logger.error('FalkorDB max reconnect attempts reached');
|
|
63
|
+
return new Error('Max reconnect attempts reached');
|
|
64
|
+
}
|
|
65
|
+
const delay = RECONNECT_DELAY_MS * Math.pow(2, retries);
|
|
66
|
+
logger.warn('FalkorDB reconnecting', { attempt: retries + 1, delayMs: delay });
|
|
67
|
+
return delay;
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
}) as RedisClientType;
|
|
71
|
+
|
|
72
|
+
this.client.on('error', (err: Error) => {
|
|
73
|
+
logger.error('FalkorDB connection error', { error: err.message });
|
|
74
|
+
this.connected = false;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
this.client.on('ready', () => {
|
|
78
|
+
this.connected = true;
|
|
79
|
+
this.reconnectAttempts = 0;
|
|
80
|
+
logger.info('FalkorDB ready', { url: this.url, graph: this.graphName });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
this.client.on('reconnecting', () => {
|
|
84
|
+
this.reconnectAttempts++;
|
|
85
|
+
logger.warn('FalkorDB reconnecting', { attempt: this.reconnectAttempts });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await this.client.connect();
|
|
89
|
+
this.connected = true;
|
|
90
|
+
logger.info('FalkorDB connected', { url: this.url, graph: this.graphName });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async disconnect(): Promise<void> {
|
|
94
|
+
if (this.client && this.connected) {
|
|
95
|
+
await this.client.disconnect();
|
|
96
|
+
this.client = null;
|
|
97
|
+
this.connected = false;
|
|
98
|
+
logger.info('FalkorDB disconnected');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async isHealthy(): Promise<boolean> {
|
|
103
|
+
// Circuit breaker short-circuits when OPEN — no ping needed
|
|
104
|
+
if (!this.breaker.isHealthy()) return false;
|
|
105
|
+
if (!this.client || !this.connected) return false;
|
|
106
|
+
try {
|
|
107
|
+
const result = await this.withTimeout(
|
|
108
|
+
this.client.ping(),
|
|
109
|
+
this.queryTimeoutMs
|
|
110
|
+
);
|
|
111
|
+
return result === 'PONG';
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Execute a raw Cypher query and return typed results.
|
|
119
|
+
* Parses FalkorDB's non-compact response format.
|
|
120
|
+
*/
|
|
121
|
+
async query(cypher: string, params?: Record<string, unknown>): Promise<GraphQueryResult> {
|
|
122
|
+
try {
|
|
123
|
+
const raw = await this.graphQuery(cypher, params);
|
|
124
|
+
return this.parseGraphQueryResult(raw);
|
|
125
|
+
} catch (err) {
|
|
126
|
+
logger.error('Graph query error', { error: String(err), cypher: cypher.slice(0, 100) });
|
|
127
|
+
return { nodes: [], edges: [], raw: [] };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async upsertNode(
|
|
132
|
+
label: string,
|
|
133
|
+
matchProps: Record<string, unknown>,
|
|
134
|
+
setProps: Record<string, unknown>
|
|
135
|
+
): Promise<void> {
|
|
136
|
+
const matchClause = this.propsToMatch(matchProps);
|
|
137
|
+
const setClause = this.propsToSet(setProps, 'n');
|
|
138
|
+
const cypher = `MERGE (n:${label} {${matchClause}}) SET ${setClause}`;
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
await this.graphQuery(cypher);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
logger.warn('upsertNode failed', { label, error: String(err) });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async upsertEdge(
|
|
148
|
+
fromLabel: string,
|
|
149
|
+
fromProps: Record<string, unknown>,
|
|
150
|
+
edgeType: string,
|
|
151
|
+
edgeProps: Record<string, unknown>,
|
|
152
|
+
toLabel: string,
|
|
153
|
+
toProps: Record<string, unknown>
|
|
154
|
+
): Promise<void> {
|
|
155
|
+
const fromMatch = this.propsToMatch(fromProps);
|
|
156
|
+
const toMatch = this.propsToMatch(toProps);
|
|
157
|
+
const edgeSet = Object.keys(edgeProps).length > 0
|
|
158
|
+
? ` {${this.propsToMatch(edgeProps)}}`
|
|
159
|
+
: '';
|
|
160
|
+
|
|
161
|
+
const cypher = `
|
|
162
|
+
MATCH (a:${fromLabel} {${fromMatch}})
|
|
163
|
+
MATCH (b:${toLabel} {${toMatch}})
|
|
164
|
+
MERGE (a)-[r:${edgeType}${edgeSet}]->(b)
|
|
165
|
+
`;
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
await this.graphQuery(cypher);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
logger.warn('upsertEdge failed', { edgeType, error: String(err) });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async deleteFile(filePath: string, repoId: string): Promise<void> {
|
|
175
|
+
// Delete child nodes (symbols contained in the file)
|
|
176
|
+
const deleteChildren = `
|
|
177
|
+
MATCH (f:File {path: "${this.escape(filePath)}", repo_id: "${this.escape(repoId)}"})-[:CONTAINS]->(s)
|
|
178
|
+
DETACH DELETE s
|
|
179
|
+
`;
|
|
180
|
+
// Delete the file node itself
|
|
181
|
+
const deleteFile = `
|
|
182
|
+
MATCH (f:File {path: "${this.escape(filePath)}", repo_id: "${this.escape(repoId)}"})
|
|
183
|
+
DETACH DELETE f
|
|
184
|
+
`;
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
await this.graphQuery(deleteChildren);
|
|
188
|
+
await this.graphQuery(deleteFile);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
logger.warn('deleteFile failed', { filePath, error: String(err) });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async deleteRepo(repoId: string): Promise<void> {
|
|
195
|
+
const cypher = `
|
|
196
|
+
MATCH (n {repo_id: "${this.escape(repoId)}"})
|
|
197
|
+
DETACH DELETE n
|
|
198
|
+
`;
|
|
199
|
+
try {
|
|
200
|
+
await this.graphQuery(cypher);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
logger.warn('deleteRepo failed', { repoId, error: String(err) });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async getCounts(): Promise<{ totalNodes: number; totalEdges: number; byLabel: Record<string, number> }> {
|
|
207
|
+
try {
|
|
208
|
+
// FalkorDB GRAPH.QUERY returns [headers, data, stats]
|
|
209
|
+
// MATCH (n) RETURN count(n) returns [[count_value]]
|
|
210
|
+
const nodeResult = await this.graphQuery('MATCH (n) RETURN count(n) AS cnt');
|
|
211
|
+
const edgeResult = await this.graphQuery('MATCH ()-[r]->() RETURN count(r) AS cnt');
|
|
212
|
+
|
|
213
|
+
const totalNodes = this.extractScalarNumber(nodeResult);
|
|
214
|
+
const totalEdges = this.extractScalarNumber(edgeResult);
|
|
215
|
+
|
|
216
|
+
// Get per-label counts for known node labels
|
|
217
|
+
const labels = ['File', 'Function', 'Class', 'Interface', 'Module', 'TypeAlias', 'Variable', 'Observation'];
|
|
218
|
+
const byLabel: Record<string, number> = {};
|
|
219
|
+
|
|
220
|
+
await Promise.all(labels.map(async (label) => {
|
|
221
|
+
try {
|
|
222
|
+
const result = await this.graphQuery(`MATCH (n:${label}) RETURN count(n) AS cnt`);
|
|
223
|
+
const count = this.extractScalarNumber(result);
|
|
224
|
+
if (count > 0) byLabel[label] = count;
|
|
225
|
+
} catch {
|
|
226
|
+
// Label might not exist in graph yet - skip
|
|
227
|
+
}
|
|
228
|
+
}));
|
|
229
|
+
|
|
230
|
+
return { totalNodes, totalEdges, byLabel };
|
|
231
|
+
} catch (err) {
|
|
232
|
+
logger.warn('getCounts failed', { error: String(err) });
|
|
233
|
+
return { totalNodes: 0, totalEdges: 0, byLabel: {} };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async ensureIndexes(): Promise<void> {
|
|
238
|
+
const indexes = [
|
|
239
|
+
'CREATE INDEX ON :File(path, repo_id)',
|
|
240
|
+
'CREATE INDEX ON :Function(name, repo_id)',
|
|
241
|
+
'CREATE INDEX ON :Class(name, repo_id)',
|
|
242
|
+
'CREATE INDEX ON :Interface(name, repo_id)',
|
|
243
|
+
'CREATE INDEX ON :Module(name, repo_id)',
|
|
244
|
+
'CREATE INDEX ON :Observation(id)',
|
|
245
|
+
'CREATE INDEX ON :Observation(session_id)',
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
for (const idx of indexes) {
|
|
249
|
+
try {
|
|
250
|
+
await this.graphQuery(idx);
|
|
251
|
+
} catch {
|
|
252
|
+
// Index may already exist - FalkorDB throws on duplicate index creation.
|
|
253
|
+
// This is expected and safe to ignore.
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
logger.info('FalkorDB indexes ensured');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ============================================================
|
|
260
|
+
// Private helpers
|
|
261
|
+
// ============================================================
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Execute a GRAPH.QUERY command with timeout, wrapped in the circuit breaker.
|
|
265
|
+
* Returns the raw FalkorDB response array.
|
|
266
|
+
*/
|
|
267
|
+
private async graphQuery(cypher: string, _params?: Record<string, unknown>): Promise<unknown[]> {
|
|
268
|
+
if (!this.client) throw new Error('FalkorDB not connected. Call connect() first.');
|
|
269
|
+
|
|
270
|
+
return this.breaker.execute(async () => {
|
|
271
|
+
const queryPromise = (this.client as RedisClientType).sendCommand([
|
|
272
|
+
'GRAPH.QUERY',
|
|
273
|
+
this.graphName,
|
|
274
|
+
cypher,
|
|
275
|
+
]) as Promise<unknown[]>;
|
|
276
|
+
|
|
277
|
+
return this.withTimeout(queryPromise, this.queryTimeoutMs);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Wrap a promise with a timeout that rejects after timeoutMs.
|
|
283
|
+
*/
|
|
284
|
+
private withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
|
|
285
|
+
return new Promise<T>((resolve, reject) => {
|
|
286
|
+
const timer = setTimeout(() => {
|
|
287
|
+
reject(new Error(`FalkorDB query timed out after ${timeoutMs}ms`));
|
|
288
|
+
}, timeoutMs);
|
|
289
|
+
|
|
290
|
+
promise.then(
|
|
291
|
+
(value) => { clearTimeout(timer); resolve(value); },
|
|
292
|
+
(err) => { clearTimeout(timer); reject(err); }
|
|
293
|
+
);
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Parse FalkorDB GRAPH.QUERY response into GraphQueryResult.
|
|
299
|
+
* FalkorDB non-compact format: [header_row, data_rows, stats_row]
|
|
300
|
+
* header_row: ["column1", "column2", ...]
|
|
301
|
+
* data_rows: [[val1, val2, ...], [val1, val2, ...], ...]
|
|
302
|
+
*/
|
|
303
|
+
private parseGraphQueryResult(raw: unknown[]): GraphQueryResult {
|
|
304
|
+
if (!Array.isArray(raw) || raw.length < 2) {
|
|
305
|
+
return { nodes: [], edges: [], raw };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const headers = raw[0] as string[];
|
|
309
|
+
const dataRows = raw[1] as unknown[][];
|
|
310
|
+
|
|
311
|
+
if (!Array.isArray(headers) || !Array.isArray(dataRows)) {
|
|
312
|
+
return { nodes: [], edges: [], raw };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const nodes = [];
|
|
316
|
+
const edges = [];
|
|
317
|
+
|
|
318
|
+
for (const row of dataRows) {
|
|
319
|
+
if (!Array.isArray(row)) continue;
|
|
320
|
+
for (let i = 0; i < headers.length; i++) {
|
|
321
|
+
const cell = row[i];
|
|
322
|
+
if (cell && typeof cell === 'object' && !Array.isArray(cell)) {
|
|
323
|
+
const obj = cell as Record<string, unknown>;
|
|
324
|
+
// FalkorDB node: { id, labels, properties }
|
|
325
|
+
if (Array.isArray(obj.labels)) {
|
|
326
|
+
nodes.push({
|
|
327
|
+
id: String(obj.id ?? ''),
|
|
328
|
+
label: (obj.labels as string[])[0] ?? '',
|
|
329
|
+
properties: (obj.properties as Record<string, unknown>) ?? {},
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
// FalkorDB edge: { id, type, src_node, dest_node, properties }
|
|
333
|
+
if (typeof obj.type === 'string' && obj.src_node !== undefined) {
|
|
334
|
+
edges.push({
|
|
335
|
+
type: obj.type,
|
|
336
|
+
from: String(obj.src_node),
|
|
337
|
+
to: String(obj.dest_node ?? ''),
|
|
338
|
+
properties: (obj.properties as Record<string, unknown>) ?? {},
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return { nodes, edges, raw };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Extract a single scalar number from a GRAPH.QUERY count result.
|
|
350
|
+
* Response: [["cnt"], [[42]], [...stats]]
|
|
351
|
+
*/
|
|
352
|
+
private extractScalarNumber(raw: unknown[]): number {
|
|
353
|
+
if (!Array.isArray(raw) || raw.length < 2) return 0;
|
|
354
|
+
const dataRows = raw[1] as unknown[][];
|
|
355
|
+
if (!Array.isArray(dataRows) || dataRows.length === 0) return 0;
|
|
356
|
+
const firstRow = dataRows[0] as unknown[];
|
|
357
|
+
if (!Array.isArray(firstRow) || firstRow.length === 0) return 0;
|
|
358
|
+
const val = firstRow[0];
|
|
359
|
+
if (typeof val === 'number') return val;
|
|
360
|
+
if (typeof val === 'string') return parseInt(val, 10) || 0;
|
|
361
|
+
return 0;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private propsToMatch(props: Record<string, unknown>): string {
|
|
365
|
+
return Object.entries(props)
|
|
366
|
+
.map(([k, v]) => `${k}: ${this.valueToLiteral(v)}`)
|
|
367
|
+
.join(', ');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private propsToSet(props: Record<string, unknown>, alias: string): string {
|
|
371
|
+
return Object.entries(props)
|
|
372
|
+
.map(([k, v]) => `${alias}.${k} = ${this.valueToLiteral(v)}`)
|
|
373
|
+
.join(', ');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private valueToLiteral(v: unknown): string {
|
|
377
|
+
if (typeof v === 'string') return `"${this.escape(v)}"`;
|
|
378
|
+
if (typeof v === 'number') return String(v);
|
|
379
|
+
if (typeof v === 'boolean') return v ? 'true' : 'false';
|
|
380
|
+
if (v === null || v === undefined) return 'null';
|
|
381
|
+
if (Array.isArray(v)) return `[${v.map(x => this.valueToLiteral(x)).join(', ')}]`;
|
|
382
|
+
return `"${this.escape(JSON.stringify(v))}"`;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private escape(s: string): string {
|
|
386
|
+
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
|
387
|
+
}
|
|
388
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory LRU file content cache.
|
|
3
|
+
* Provides the cross-agent shared context (ADR-8, section 4.6).
|
|
4
|
+
* Size-aware eviction with content-hash keying for invalidation.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { LRUCache } from 'lru-cache';
|
|
8
|
+
import { createHash } from 'crypto';
|
|
9
|
+
import type { FileContentCache } from './interfaces.js';
|
|
10
|
+
import type { CacheEntry, CacheStats } from '../util/types.js';
|
|
11
|
+
import { logger } from '../util/logger.js';
|
|
12
|
+
|
|
13
|
+
const DEFAULT_MAX_SIZE_BYTES = 100 * 1024 * 1024; // 100 MB
|
|
14
|
+
const DEFAULT_MAX_ENTRIES = 2000;
|
|
15
|
+
const DEFAULT_TTL_MS = 30 * 60 * 1000; // 30 min
|
|
16
|
+
|
|
17
|
+
export interface FileCacheOptions {
|
|
18
|
+
maxSizeBytes?: number;
|
|
19
|
+
maxEntries?: number;
|
|
20
|
+
ttlMs?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class LRUFileContentCache implements FileContentCache {
|
|
24
|
+
private cache: LRUCache<string, CacheEntry>;
|
|
25
|
+
private hits = 0;
|
|
26
|
+
private misses = 0;
|
|
27
|
+
private evictionCount = 0;
|
|
28
|
+
private startedAt = Date.now();
|
|
29
|
+
|
|
30
|
+
constructor(options: FileCacheOptions = {}) {
|
|
31
|
+
const maxSizeBytes = options.maxSizeBytes ?? DEFAULT_MAX_SIZE_BYTES;
|
|
32
|
+
const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
33
|
+
|
|
34
|
+
this.cache = new LRUCache<string, CacheEntry>({
|
|
35
|
+
maxSize: maxSizeBytes,
|
|
36
|
+
// LRU cache requires sizeCalculation to return a positive integer.
|
|
37
|
+
// Use minimum of 1 so zero-byte content (empty string) is still accepted.
|
|
38
|
+
sizeCalculation: (entry) => Math.max(1, entry.sizeBytes),
|
|
39
|
+
ttl: ttlMs,
|
|
40
|
+
max: options.maxEntries ?? DEFAULT_MAX_ENTRIES,
|
|
41
|
+
// Use Date.now() for TTL tracking. This makes the cache compatible with
|
|
42
|
+
// vi.useFakeTimers() in tests (which replaces Date.now but not performance.now).
|
|
43
|
+
// lru-cache v11 defaults to performance.now which isn't affected by fake timers.
|
|
44
|
+
perf: { now: () => Date.now() },
|
|
45
|
+
dispose: () => {
|
|
46
|
+
this.evictionCount++;
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private cacheKey(repoId: string, filePath: string): string {
|
|
52
|
+
return `${repoId}:${filePath}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get(repoId: string, filePath: string): CacheEntry | null {
|
|
56
|
+
const key = this.cacheKey(repoId, filePath);
|
|
57
|
+
const entry = this.cache.get(key);
|
|
58
|
+
|
|
59
|
+
if (entry) {
|
|
60
|
+
this.hits++;
|
|
61
|
+
// Update access count in-place
|
|
62
|
+
entry.accessCount++;
|
|
63
|
+
return entry;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.misses++;
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
set(repoId: string, filePath: string, content: string, contentHash: string): void {
|
|
71
|
+
const key = this.cacheKey(repoId, filePath);
|
|
72
|
+
const sizeBytes = Buffer.byteLength(content, 'utf8');
|
|
73
|
+
|
|
74
|
+
const entry: CacheEntry = {
|
|
75
|
+
content,
|
|
76
|
+
contentHash,
|
|
77
|
+
cachedAt: Date.now(),
|
|
78
|
+
accessCount: 0,
|
|
79
|
+
sizeBytes, // Report the actual byte size (may be 0 for empty string)
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
this.cache.set(key, entry);
|
|
83
|
+
logger.debug('File cached', { filePath, sizeBytes, repoId });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
invalidate(repoId: string, filePath: string): void {
|
|
87
|
+
const key = this.cacheKey(repoId, filePath);
|
|
88
|
+
this.cache.delete(key);
|
|
89
|
+
logger.debug('Cache invalidated', { filePath, repoId });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
invalidateRepo(repoId: string): void {
|
|
93
|
+
const prefix = `${repoId}:`;
|
|
94
|
+
let count = 0;
|
|
95
|
+
for (const key of this.cache.keys()) {
|
|
96
|
+
if (key.startsWith(prefix)) {
|
|
97
|
+
this.cache.delete(key);
|
|
98
|
+
count++;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
logger.info('Repo cache invalidated', { repoId, count });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
clear(): void {
|
|
105
|
+
this.cache.clear();
|
|
106
|
+
this.hits = 0;
|
|
107
|
+
this.misses = 0;
|
|
108
|
+
logger.info('File content cache cleared');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
getStats(): CacheStats {
|
|
112
|
+
const total = this.hits + this.misses;
|
|
113
|
+
const hitRate = total > 0 ? this.hits / total : 0;
|
|
114
|
+
|
|
115
|
+
// Calculate current memory usage
|
|
116
|
+
let totalBytes = 0;
|
|
117
|
+
let oldestAge = 0;
|
|
118
|
+
const now = Date.now();
|
|
119
|
+
|
|
120
|
+
for (const entry of this.cache.values()) {
|
|
121
|
+
totalBytes += entry.sizeBytes;
|
|
122
|
+
const age = (now - entry.cachedAt) / 1000;
|
|
123
|
+
if (age > oldestAge) oldestAge = age;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
entries: this.cache.size,
|
|
128
|
+
memoryUsageMb: totalBytes / (1024 * 1024),
|
|
129
|
+
hitRate,
|
|
130
|
+
evictionCount: this.evictionCount,
|
|
131
|
+
oldestEntryAgeSeconds: oldestAge,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Utility: compute SHA-256 hash of content string.
|
|
138
|
+
*/
|
|
139
|
+
export function hashContent(content: string): string {
|
|
140
|
+
return createHash('sha256').update(content, 'utf8').digest('hex');
|
|
141
|
+
}
|