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.
Files changed (412) hide show
  1. package/.claude/hooks/worktree-create.sh +64 -0
  2. package/.claude/hooks/worktree-remove.sh +57 -0
  3. package/.claude/settings.local.json +29 -0
  4. package/.forge/knowledge/conventions.yaml +1 -0
  5. package/.forge/knowledge/decisions.yaml +1 -0
  6. package/.forge/knowledge/gotchas.yaml +1 -0
  7. package/.forge/knowledge/patterns.yaml +1 -0
  8. package/.forge/manifest.yaml +6 -0
  9. package/CLAUDE.md +144 -0
  10. package/bin/setup-forge.sh +132 -0
  11. package/dist/cli.d.ts +3 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +553 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/context/codebase.d.ts +57 -0
  16. package/dist/context/codebase.d.ts.map +1 -0
  17. package/dist/context/codebase.js +301 -0
  18. package/dist/context/codebase.js.map +1 -0
  19. package/dist/context/injector.d.ts +147 -0
  20. package/dist/context/injector.d.ts.map +1 -0
  21. package/dist/context/injector.js +533 -0
  22. package/dist/context/injector.js.map +1 -0
  23. package/dist/context/memory.d.ts +32 -0
  24. package/dist/context/memory.d.ts.map +1 -0
  25. package/dist/context/memory.js +140 -0
  26. package/dist/context/memory.js.map +1 -0
  27. package/dist/context/session-index.d.ts +54 -0
  28. package/dist/context/session-index.d.ts.map +1 -0
  29. package/dist/context/session-index.js +265 -0
  30. package/dist/context/session-index.js.map +1 -0
  31. package/dist/context/session.d.ts +42 -0
  32. package/dist/context/session.d.ts.map +1 -0
  33. package/dist/context/session.js +121 -0
  34. package/dist/context/session.js.map +1 -0
  35. package/dist/index.d.ts +3 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +37 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/ingestion/chunker.d.ts +19 -0
  40. package/dist/ingestion/chunker.d.ts.map +1 -0
  41. package/dist/ingestion/chunker.js +189 -0
  42. package/dist/ingestion/chunker.js.map +1 -0
  43. package/dist/ingestion/embedder.d.ts +45 -0
  44. package/dist/ingestion/embedder.d.ts.map +1 -0
  45. package/dist/ingestion/embedder.js +152 -0
  46. package/dist/ingestion/embedder.js.map +1 -0
  47. package/dist/ingestion/git-analyzer.d.ts +77 -0
  48. package/dist/ingestion/git-analyzer.d.ts.map +1 -0
  49. package/dist/ingestion/git-analyzer.js +437 -0
  50. package/dist/ingestion/git-analyzer.js.map +1 -0
  51. package/dist/ingestion/indexer.d.ts +79 -0
  52. package/dist/ingestion/indexer.d.ts.map +1 -0
  53. package/dist/ingestion/indexer.js +766 -0
  54. package/dist/ingestion/indexer.js.map +1 -0
  55. package/dist/ingestion/markdown-chunker.d.ts +19 -0
  56. package/dist/ingestion/markdown-chunker.d.ts.map +1 -0
  57. package/dist/ingestion/markdown-chunker.js +243 -0
  58. package/dist/ingestion/markdown-chunker.js.map +1 -0
  59. package/dist/ingestion/markdown-knowledge.d.ts +21 -0
  60. package/dist/ingestion/markdown-knowledge.d.ts.map +1 -0
  61. package/dist/ingestion/markdown-knowledge.js +129 -0
  62. package/dist/ingestion/markdown-knowledge.js.map +1 -0
  63. package/dist/ingestion/parser.d.ts +20 -0
  64. package/dist/ingestion/parser.d.ts.map +1 -0
  65. package/dist/ingestion/parser.js +429 -0
  66. package/dist/ingestion/parser.js.map +1 -0
  67. package/dist/ingestion/watcher.d.ts +28 -0
  68. package/dist/ingestion/watcher.d.ts.map +1 -0
  69. package/dist/ingestion/watcher.js +147 -0
  70. package/dist/ingestion/watcher.js.map +1 -0
  71. package/dist/knowledge/hydrator.d.ts +37 -0
  72. package/dist/knowledge/hydrator.d.ts.map +1 -0
  73. package/dist/knowledge/hydrator.js +220 -0
  74. package/dist/knowledge/hydrator.js.map +1 -0
  75. package/dist/knowledge/registry.d.ts +129 -0
  76. package/dist/knowledge/registry.d.ts.map +1 -0
  77. package/dist/knowledge/registry.js +361 -0
  78. package/dist/knowledge/registry.js.map +1 -0
  79. package/dist/knowledge/search.d.ts +114 -0
  80. package/dist/knowledge/search.d.ts.map +1 -0
  81. package/dist/knowledge/search.js +428 -0
  82. package/dist/knowledge/search.js.map +1 -0
  83. package/dist/knowledge/store.d.ts +76 -0
  84. package/dist/knowledge/store.d.ts.map +1 -0
  85. package/dist/knowledge/store.js +230 -0
  86. package/dist/knowledge/store.js.map +1 -0
  87. package/dist/learning/confidence.d.ts +30 -0
  88. package/dist/learning/confidence.d.ts.map +1 -0
  89. package/dist/learning/confidence.js +165 -0
  90. package/dist/learning/confidence.js.map +1 -0
  91. package/dist/learning/patterns.d.ts +52 -0
  92. package/dist/learning/patterns.d.ts.map +1 -0
  93. package/dist/learning/patterns.js +290 -0
  94. package/dist/learning/patterns.js.map +1 -0
  95. package/dist/learning/trajectory.d.ts +55 -0
  96. package/dist/learning/trajectory.d.ts.map +1 -0
  97. package/dist/learning/trajectory.js +200 -0
  98. package/dist/learning/trajectory.js.map +1 -0
  99. package/dist/memory/memory-compat.d.ts +100 -0
  100. package/dist/memory/memory-compat.d.ts.map +1 -0
  101. package/dist/memory/memory-compat.js +146 -0
  102. package/dist/memory/memory-compat.js.map +1 -0
  103. package/dist/memory/observation-store.d.ts +57 -0
  104. package/dist/memory/observation-store.d.ts.map +1 -0
  105. package/dist/memory/observation-store.js +154 -0
  106. package/dist/memory/observation-store.js.map +1 -0
  107. package/dist/memory/session-tracker.d.ts +81 -0
  108. package/dist/memory/session-tracker.d.ts.map +1 -0
  109. package/dist/memory/session-tracker.js +262 -0
  110. package/dist/memory/session-tracker.js.map +1 -0
  111. package/dist/pipeline/engine.d.ts +179 -0
  112. package/dist/pipeline/engine.d.ts.map +1 -0
  113. package/dist/pipeline/engine.js +691 -0
  114. package/dist/pipeline/engine.js.map +1 -0
  115. package/dist/pipeline/events.d.ts +54 -0
  116. package/dist/pipeline/events.d.ts.map +1 -0
  117. package/dist/pipeline/events.js +157 -0
  118. package/dist/pipeline/events.js.map +1 -0
  119. package/dist/pipeline/parallel.d.ts +83 -0
  120. package/dist/pipeline/parallel.d.ts.map +1 -0
  121. package/dist/pipeline/parallel.js +277 -0
  122. package/dist/pipeline/parallel.js.map +1 -0
  123. package/dist/pipeline/state-machine.d.ts +65 -0
  124. package/dist/pipeline/state-machine.d.ts.map +1 -0
  125. package/dist/pipeline/state-machine.js +176 -0
  126. package/dist/pipeline/state-machine.js.map +1 -0
  127. package/dist/query/graph-queries.d.ts +84 -0
  128. package/dist/query/graph-queries.d.ts.map +1 -0
  129. package/dist/query/graph-queries.js +216 -0
  130. package/dist/query/graph-queries.js.map +1 -0
  131. package/dist/query/hybrid-search.d.ts +34 -0
  132. package/dist/query/hybrid-search.d.ts.map +1 -0
  133. package/dist/query/hybrid-search.js +263 -0
  134. package/dist/query/hybrid-search.js.map +1 -0
  135. package/dist/query/intent-detector.d.ts +35 -0
  136. package/dist/query/intent-detector.d.ts.map +1 -0
  137. package/dist/query/intent-detector.js +115 -0
  138. package/dist/query/intent-detector.js.map +1 -0
  139. package/dist/query/ranking.d.ts +57 -0
  140. package/dist/query/ranking.d.ts.map +1 -0
  141. package/dist/query/ranking.js +109 -0
  142. package/dist/query/ranking.js.map +1 -0
  143. package/dist/server.d.ts +3 -0
  144. package/dist/server.d.ts.map +1 -0
  145. package/dist/server.js +291 -0
  146. package/dist/server.js.map +1 -0
  147. package/dist/storage/falkordb-store.d.ts +73 -0
  148. package/dist/storage/falkordb-store.d.ts.map +1 -0
  149. package/dist/storage/falkordb-store.js +346 -0
  150. package/dist/storage/falkordb-store.js.map +1 -0
  151. package/dist/storage/file-cache.d.ts +32 -0
  152. package/dist/storage/file-cache.d.ts.map +1 -0
  153. package/dist/storage/file-cache.js +115 -0
  154. package/dist/storage/file-cache.js.map +1 -0
  155. package/dist/storage/interfaces.d.ts +151 -0
  156. package/dist/storage/interfaces.d.ts.map +1 -0
  157. package/dist/storage/interfaces.js +7 -0
  158. package/dist/storage/interfaces.js.map +1 -0
  159. package/dist/storage/qdrant-store.d.ts +110 -0
  160. package/dist/storage/qdrant-store.d.ts.map +1 -0
  161. package/dist/storage/qdrant-store.js +467 -0
  162. package/dist/storage/qdrant-store.js.map +1 -0
  163. package/dist/storage/schema.d.ts +4 -0
  164. package/dist/storage/schema.d.ts.map +1 -0
  165. package/dist/storage/schema.js +136 -0
  166. package/dist/storage/schema.js.map +1 -0
  167. package/dist/storage/sqlite.d.ts +35 -0
  168. package/dist/storage/sqlite.d.ts.map +1 -0
  169. package/dist/storage/sqlite.js +132 -0
  170. package/dist/storage/sqlite.js.map +1 -0
  171. package/dist/tools/collaboration-tools.d.ts +111 -0
  172. package/dist/tools/collaboration-tools.d.ts.map +1 -0
  173. package/dist/tools/collaboration-tools.js +174 -0
  174. package/dist/tools/collaboration-tools.js.map +1 -0
  175. package/dist/tools/context-tools.d.ts +293 -0
  176. package/dist/tools/context-tools.d.ts.map +1 -0
  177. package/dist/tools/context-tools.js +437 -0
  178. package/dist/tools/context-tools.js.map +1 -0
  179. package/dist/tools/graph-tools.d.ts +129 -0
  180. package/dist/tools/graph-tools.d.ts.map +1 -0
  181. package/dist/tools/graph-tools.js +237 -0
  182. package/dist/tools/graph-tools.js.map +1 -0
  183. package/dist/tools/ingestion-tools.d.ts +96 -0
  184. package/dist/tools/ingestion-tools.d.ts.map +1 -0
  185. package/dist/tools/ingestion-tools.js +90 -0
  186. package/dist/tools/ingestion-tools.js.map +1 -0
  187. package/dist/tools/learning-tools.d.ts +168 -0
  188. package/dist/tools/learning-tools.d.ts.map +1 -0
  189. package/dist/tools/learning-tools.js +158 -0
  190. package/dist/tools/learning-tools.js.map +1 -0
  191. package/dist/tools/memory-tools.d.ts +183 -0
  192. package/dist/tools/memory-tools.d.ts.map +1 -0
  193. package/dist/tools/memory-tools.js +197 -0
  194. package/dist/tools/memory-tools.js.map +1 -0
  195. package/dist/tools/phase-tools.d.ts +954 -0
  196. package/dist/tools/phase-tools.d.ts.map +1 -0
  197. package/dist/tools/phase-tools.js +1215 -0
  198. package/dist/tools/phase-tools.js.map +1 -0
  199. package/dist/tools/pipeline-tools.d.ts +140 -0
  200. package/dist/tools/pipeline-tools.d.ts.map +1 -0
  201. package/dist/tools/pipeline-tools.js +162 -0
  202. package/dist/tools/pipeline-tools.js.map +1 -0
  203. package/dist/tools/registration-tools.d.ts +220 -0
  204. package/dist/tools/registration-tools.d.ts.map +1 -0
  205. package/dist/tools/registration-tools.js +391 -0
  206. package/dist/tools/registration-tools.js.map +1 -0
  207. package/dist/util/circuit-breaker.d.ts +75 -0
  208. package/dist/util/circuit-breaker.d.ts.map +1 -0
  209. package/dist/util/circuit-breaker.js +159 -0
  210. package/dist/util/circuit-breaker.js.map +1 -0
  211. package/dist/util/config.d.ts +23 -0
  212. package/dist/util/config.d.ts.map +1 -0
  213. package/dist/util/config.js +164 -0
  214. package/dist/util/config.js.map +1 -0
  215. package/dist/util/logger.d.ts +13 -0
  216. package/dist/util/logger.d.ts.map +1 -0
  217. package/dist/util/logger.js +45 -0
  218. package/dist/util/logger.js.map +1 -0
  219. package/dist/util/token-counter.d.ts +24 -0
  220. package/dist/util/token-counter.d.ts.map +1 -0
  221. package/dist/util/token-counter.js +48 -0
  222. package/dist/util/token-counter.js.map +1 -0
  223. package/dist/util/types.d.ts +525 -0
  224. package/dist/util/types.d.ts.map +1 -0
  225. package/dist/util/types.js +5 -0
  226. package/dist/util/types.js.map +1 -0
  227. package/docker-compose.yml +20 -0
  228. package/docs/plans/2026-02-27-swarm-coordination/architecture.md +203 -0
  229. package/docs/plans/2026-02-27-swarm-coordination/vision.md +57 -0
  230. package/docs/plans/completed/2026-02-26-forge-plugin-bundling/architecture.md +1 -0
  231. package/docs/plans/completed/2026-02-26-forge-plugin-bundling/vision.md +300 -0
  232. package/docs/plans/completed/2026-02-27-forge-swarm-learning/architecture.md +480 -0
  233. package/docs/plans/completed/2026-02-27-forge-swarm-learning/verification-checklist.md +462 -0
  234. package/docs/plans/completed/2026-02-27-git-history-atlassian/git-jira-plan.md +181 -0
  235. package/package.json +39 -0
  236. package/plugin/.claude-plugin/plugin.json +8 -0
  237. package/plugin/.mcp.json +15 -0
  238. package/plugin/README.md +134 -0
  239. package/plugin/agents/architect.md +367 -0
  240. package/plugin/agents/backend-specialist.md +263 -0
  241. package/plugin/agents/brainstormer.md +122 -0
  242. package/plugin/agents/data-specialist.md +266 -0
  243. package/plugin/agents/designer.md +408 -0
  244. package/plugin/agents/frontend-specialist.md +241 -0
  245. package/plugin/agents/inspector.md +406 -0
  246. package/plugin/agents/knowledge-keeper.md +443 -0
  247. package/plugin/agents/platform-engineer.md +326 -0
  248. package/plugin/agents/product-manager.md +268 -0
  249. package/plugin/agents/product-owner.md +438 -0
  250. package/plugin/agents/pulse-checker.md +73 -0
  251. package/plugin/agents/qa-strategist.md +500 -0
  252. package/plugin/agents/self-improver.md +310 -0
  253. package/plugin/agents/strategist.md +360 -0
  254. package/plugin/agents/supervisor.md +380 -0
  255. package/plugin/commands/brainstorm.md +25 -0
  256. package/plugin/commands/forge.md +88 -0
  257. package/plugin/docs/atlassian-integration.md +110 -0
  258. package/plugin/docs/workflow.md +126 -0
  259. package/plugin/skills/agent-development/.skillfish.json +10 -0
  260. package/plugin/skills/agent-development/SKILL.md +415 -0
  261. package/plugin/skills/agent-development/examples/agent-creation-prompt.md +238 -0
  262. package/plugin/skills/agent-development/examples/complete-agent-examples.md +427 -0
  263. package/plugin/skills/agent-development/references/agent-creation-system-prompt.md +207 -0
  264. package/plugin/skills/agent-development/references/system-prompt-design.md +411 -0
  265. package/plugin/skills/agent-development/references/triggering-examples.md +491 -0
  266. package/plugin/skills/agent-development/scripts/validate-agent.sh +217 -0
  267. package/plugin/skills/agent-handoff/SKILL.md +335 -0
  268. package/plugin/skills/anti-stub/SKILL.md +317 -0
  269. package/plugin/skills/brainstorm/SKILL.md +31 -0
  270. package/plugin/skills/debugging/SKILL.md +276 -0
  271. package/plugin/skills/fix/SKILL.md +62 -0
  272. package/plugin/skills/frontend-design/.skillfish.json +10 -0
  273. package/plugin/skills/frontend-design/SKILL.md +42 -0
  274. package/plugin/skills/gotchas/SKILL.md +61 -0
  275. package/plugin/skills/graph-orchestrator/SKILL.md +38 -0
  276. package/plugin/skills/history/SKILL.md +58 -0
  277. package/plugin/skills/impact/SKILL.md +59 -0
  278. package/plugin/skills/implementation-execution/SKILL.md +291 -0
  279. package/plugin/skills/index-repo/SKILL.md +55 -0
  280. package/plugin/skills/interviewing/SKILL.md +225 -0
  281. package/plugin/skills/knowledge-curation/SKILL.md +393 -0
  282. package/plugin/skills/learn/SKILL.md +69 -0
  283. package/plugin/skills/mcp-integration/.skillfish.json +10 -0
  284. package/plugin/skills/mcp-integration/SKILL.md +554 -0
  285. package/plugin/skills/mcp-integration/examples/http-server.json +20 -0
  286. package/plugin/skills/mcp-integration/examples/sse-server.json +19 -0
  287. package/plugin/skills/mcp-integration/examples/stdio-server.json +26 -0
  288. package/plugin/skills/mcp-integration/references/authentication.md +549 -0
  289. package/plugin/skills/mcp-integration/references/server-types.md +536 -0
  290. package/plugin/skills/mcp-integration/references/tool-usage.md +538 -0
  291. package/plugin/skills/nestjs/.skillfish.json +10 -0
  292. package/plugin/skills/nestjs/SKILL.md +669 -0
  293. package/plugin/skills/nestjs/drizzle-reference.md +1894 -0
  294. package/plugin/skills/nestjs/reference.md +1447 -0
  295. package/plugin/skills/nestjs/workflow-optimization.md +229 -0
  296. package/plugin/skills/parallel-dispatch/SKILL.md +308 -0
  297. package/plugin/skills/project-discovery/SKILL.md +304 -0
  298. package/plugin/skills/search/SKILL.md +56 -0
  299. package/plugin/skills/security-audit/SKILL.md +362 -0
  300. package/plugin/skills/skill-development/.skillfish.json +10 -0
  301. package/plugin/skills/skill-development/SKILL.md +637 -0
  302. package/plugin/skills/skill-development/references/skill-creator-original.md +209 -0
  303. package/plugin/skills/tdd/SKILL.md +273 -0
  304. package/plugin/skills/terminal-presentation/SKILL.md +395 -0
  305. package/plugin/skills/test-strategy/SKILL.md +365 -0
  306. package/plugin/skills/verification-protocol/SKILL.md +256 -0
  307. package/plugin/skills/visual-explainer/CHANGELOG.md +97 -0
  308. package/plugin/skills/visual-explainer/LICENSE +21 -0
  309. package/plugin/skills/visual-explainer/README.md +137 -0
  310. package/plugin/skills/visual-explainer/SKILL.md +352 -0
  311. package/plugin/skills/visual-explainer/banner.png +0 -0
  312. package/plugin/skills/visual-explainer/package.json +11 -0
  313. package/plugin/skills/visual-explainer/prompts/diff-review.md +68 -0
  314. package/plugin/skills/visual-explainer/prompts/fact-check.md +63 -0
  315. package/plugin/skills/visual-explainer/prompts/generate-slides.md +18 -0
  316. package/plugin/skills/visual-explainer/prompts/generate-web-diagram.md +10 -0
  317. package/plugin/skills/visual-explainer/prompts/plan-review.md +86 -0
  318. package/plugin/skills/visual-explainer/prompts/project-recap.md +61 -0
  319. package/plugin/skills/visual-explainer/references/css-patterns.md +1188 -0
  320. package/plugin/skills/visual-explainer/references/libraries.md +470 -0
  321. package/plugin/skills/visual-explainer/references/responsive-nav.md +212 -0
  322. package/plugin/skills/visual-explainer/references/slide-patterns.md +1403 -0
  323. package/plugin/skills/visual-explainer/templates/architecture.html +596 -0
  324. package/plugin/skills/visual-explainer/templates/data-table.html +540 -0
  325. package/plugin/skills/visual-explainer/templates/mermaid-flowchart.html +435 -0
  326. package/plugin/skills/visual-explainer/templates/slide-deck.html +913 -0
  327. package/src/cli.ts +655 -0
  328. package/src/context/.gitkeep +0 -0
  329. package/src/context/codebase.ts +393 -0
  330. package/src/context/injector.ts +797 -0
  331. package/src/context/memory.ts +187 -0
  332. package/src/context/session-index.ts +327 -0
  333. package/src/context/session.ts +152 -0
  334. package/src/index.ts +47 -0
  335. package/src/ingestion/.gitkeep +0 -0
  336. package/src/ingestion/chunker.ts +277 -0
  337. package/src/ingestion/embedder.ts +167 -0
  338. package/src/ingestion/git-analyzer.ts +545 -0
  339. package/src/ingestion/indexer.ts +984 -0
  340. package/src/ingestion/markdown-chunker.ts +337 -0
  341. package/src/ingestion/markdown-knowledge.ts +175 -0
  342. package/src/ingestion/parser.ts +475 -0
  343. package/src/ingestion/watcher.ts +182 -0
  344. package/src/knowledge/.gitkeep +0 -0
  345. package/src/knowledge/hydrator.ts +246 -0
  346. package/src/knowledge/registry.ts +463 -0
  347. package/src/knowledge/search.ts +565 -0
  348. package/src/knowledge/store.ts +262 -0
  349. package/src/learning/.gitkeep +0 -0
  350. package/src/learning/confidence.ts +193 -0
  351. package/src/learning/patterns.ts +360 -0
  352. package/src/learning/trajectory.ts +268 -0
  353. package/src/memory/.gitkeep +0 -0
  354. package/src/memory/memory-compat.ts +233 -0
  355. package/src/memory/observation-store.ts +224 -0
  356. package/src/memory/session-tracker.ts +332 -0
  357. package/src/pipeline/.gitkeep +0 -0
  358. package/src/pipeline/engine.ts +1139 -0
  359. package/src/pipeline/events.ts +253 -0
  360. package/src/pipeline/parallel.ts +394 -0
  361. package/src/pipeline/state-machine.ts +199 -0
  362. package/src/query/.gitkeep +0 -0
  363. package/src/query/graph-queries.ts +262 -0
  364. package/src/query/hybrid-search.ts +337 -0
  365. package/src/query/intent-detector.ts +131 -0
  366. package/src/query/ranking.ts +161 -0
  367. package/src/server.ts +352 -0
  368. package/src/storage/.gitkeep +0 -0
  369. package/src/storage/falkordb-store.ts +388 -0
  370. package/src/storage/file-cache.ts +141 -0
  371. package/src/storage/interfaces.ts +201 -0
  372. package/src/storage/qdrant-store.ts +557 -0
  373. package/src/storage/schema.ts +139 -0
  374. package/src/storage/sqlite.ts +168 -0
  375. package/src/tools/.gitkeep +0 -0
  376. package/src/tools/collaboration-tools.ts +208 -0
  377. package/src/tools/context-tools.ts +493 -0
  378. package/src/tools/graph-tools.ts +295 -0
  379. package/src/tools/ingestion-tools.ts +122 -0
  380. package/src/tools/learning-tools.ts +181 -0
  381. package/src/tools/memory-tools.ts +234 -0
  382. package/src/tools/phase-tools.ts +1452 -0
  383. package/src/tools/pipeline-tools.ts +188 -0
  384. package/src/tools/registration-tools.ts +450 -0
  385. package/src/util/.gitkeep +0 -0
  386. package/src/util/circuit-breaker.ts +193 -0
  387. package/src/util/config.ts +177 -0
  388. package/src/util/logger.ts +53 -0
  389. package/src/util/token-counter.ts +52 -0
  390. package/src/util/types.ts +710 -0
  391. package/tests/context/.gitkeep +0 -0
  392. package/tests/integration/.gitkeep +0 -0
  393. package/tests/knowledge/.gitkeep +0 -0
  394. package/tests/learning/.gitkeep +0 -0
  395. package/tests/pipeline/.gitkeep +0 -0
  396. package/tests/tools/.gitkeep +0 -0
  397. package/tsconfig.json +21 -0
  398. package/vitest.config.ts +10 -0
  399. package/vscode-extension/.vscodeignore +7 -0
  400. package/vscode-extension/README.md +43 -0
  401. package/vscode-extension/out/edge-collector.js +274 -0
  402. package/vscode-extension/out/edge-collector.js.map +1 -0
  403. package/vscode-extension/out/extension.js +264 -0
  404. package/vscode-extension/out/extension.js.map +1 -0
  405. package/vscode-extension/out/forge-client.js +318 -0
  406. package/vscode-extension/out/forge-client.js.map +1 -0
  407. package/vscode-extension/package-lock.json +59 -0
  408. package/vscode-extension/package.json +71 -0
  409. package/vscode-extension/src/edge-collector.ts +320 -0
  410. package/vscode-extension/src/extension.ts +269 -0
  411. package/vscode-extension/src/forge-client.ts +364 -0
  412. package/vscode-extension/tsconfig.json +19 -0
@@ -0,0 +1,766 @@
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
+ import { readFile, stat, readdir } from 'fs/promises';
16
+ import { resolve, extname, join, dirname, isAbsolute } from 'path';
17
+ import { createHash } from 'crypto';
18
+ import { parseFile, getLanguage } from './parser.js';
19
+ import { chunkFromEntities, chunkFixed } from './chunker.js';
20
+ import { chunkMarkdown } from './markdown-chunker.js';
21
+ import { extractKnowledge } from './markdown-knowledge.js';
22
+ import { embedBatch } from './embedder.js';
23
+ import { hashContent } from '../storage/file-cache.js';
24
+ import { logger } from '../util/logger.js';
25
+ import { runGitAnalysis, analyzeFileStats, extractCommitRecords } from './git-analyzer.js';
26
+ import { MARK_SYMBOL_OBSERVATIONS_STALE } from '../query/graph-queries.js';
27
+ function isOwnedRepo(repoId, repos) {
28
+ const repo = repos.find(r => r.id === repoId);
29
+ return repo?.ownership === 'owned';
30
+ }
31
+ function getIndexingRoute(repoId, repos) {
32
+ const owned = isOwnedRepo(repoId, repos);
33
+ return {
34
+ writeGraphNode: owned,
35
+ writeEntityNodes: owned,
36
+ writeVectorChunks: true, // Always write vectors
37
+ writeFileCache: owned, // Cache only owned repo files
38
+ parseAst: owned, // Full AST parse only for owned repos
39
+ watchForChanges: owned, // Watch only owned repos
40
+ };
41
+ }
42
+ // ============================================================
43
+ // Module state
44
+ // ============================================================
45
+ // Track which files are indexed and their content hashes
46
+ const indexedFiles = new Map();
47
+ // Track indexing stats per repo
48
+ const repoStats = new Map();
49
+ // Track all entities discovered per repo for CALLS edge analysis
50
+ // Map<repoId, Map<entityName, filePath>>
51
+ const repoEntityRegistry = new Map();
52
+ const NON_CODE_EXTENSIONS = new Set([
53
+ '.json', '.yaml', '.yml', '.toml', '.md', '.mdx',
54
+ '.graphql', '.gql', '.sql', '.txt', '.csv',
55
+ ]);
56
+ const CODE_EXTENSIONS = new Set([
57
+ '.ts', '.tsx', '.js', '.jsx', '.mts', '.cts', '.mjs', '.cjs',
58
+ ]);
59
+ const SKIP_EXTENSIONS = new Set([
60
+ '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.webp',
61
+ '.ttf', '.woff', '.woff2', '.eot',
62
+ '.pdf', '.docx', '.xlsx',
63
+ '.zip', '.tar', '.gz',
64
+ '.exe', '.dll', '.so', '.dylib',
65
+ '.lock',
66
+ ]);
67
+ // Patterns to skip during directory walk
68
+ const SKIP_DIRS = new Set([
69
+ 'node_modules', '.git', 'dist', 'build', '.cache', '.next', 'coverage',
70
+ '__pycache__', '.pytest_cache', 'vendor', 'target',
71
+ ]);
72
+ /**
73
+ * Walk a directory tree and collect all file paths.
74
+ * Respects SKIP_DIRS and file extension filters.
75
+ */
76
+ async function walkDir(dirPath, results = []) {
77
+ let entries;
78
+ try {
79
+ entries = await readdir(dirPath, { withFileTypes: true });
80
+ }
81
+ catch {
82
+ return results;
83
+ }
84
+ for (const entry of entries) {
85
+ const fullPath = join(dirPath, entry.name);
86
+ if (entry.isDirectory()) {
87
+ if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
88
+ await walkDir(fullPath, results);
89
+ }
90
+ }
91
+ else if (entry.isFile()) {
92
+ const ext = extname(entry.name).toLowerCase();
93
+ if (!SKIP_EXTENSIONS.has(ext) && (CODE_EXTENSIONS.has(ext) || NON_CODE_EXTENSIONS.has(ext))) {
94
+ results.push(fullPath);
95
+ }
96
+ }
97
+ }
98
+ return results;
99
+ }
100
+ /**
101
+ * Perform a full index of a repository.
102
+ * Walks all files, computes content hashes, skips unchanged, indexes new/changed.
103
+ * Phase 2: also creates FalkorDB indexes and runs git analysis afterward.
104
+ */
105
+ export async function fullIndex(opts) {
106
+ const { repoConfig, graphStore } = opts;
107
+ const { id: repoId, path: repoPath } = repoConfig;
108
+ const startTime = Date.now();
109
+ const result = {
110
+ repoId,
111
+ filesProcessed: 0,
112
+ filesSkipped: 0,
113
+ filesErrored: 0,
114
+ chunksCreated: 0,
115
+ knowledgeItemsExtracted: 0,
116
+ errors: [],
117
+ durationMs: 0,
118
+ };
119
+ logger.info('Starting full index', { repoId, path: repoPath });
120
+ // Phase 2: Ensure FalkorDB indexes exist before indexing
121
+ try {
122
+ await graphStore.ensureIndexes();
123
+ }
124
+ catch (err) {
125
+ logger.warn('Failed to ensure graph indexes', { error: String(err) });
126
+ }
127
+ // Initialize entity registry for this repo
128
+ repoEntityRegistry.set(repoId, new Map());
129
+ // Walk the directory tree
130
+ let allFiles;
131
+ try {
132
+ allFiles = await walkDir(resolve(repoPath));
133
+ }
134
+ catch (err) {
135
+ const msg = `Failed to walk repo directory: ${String(err)}`;
136
+ result.errors.push(msg);
137
+ result.durationMs = Date.now() - startTime;
138
+ logger.error(msg, { repoId, path: repoPath });
139
+ return result;
140
+ }
141
+ // Update total file count in stats
142
+ repoStats.set(repoId, {
143
+ filesIndexed: repoStats.get(repoId)?.filesIndexed ?? 0,
144
+ filesTotal: allFiles.length,
145
+ lastIndexedAt: repoStats.get(repoId)?.lastIndexedAt ?? 0,
146
+ });
147
+ // Process files in batches to avoid overwhelming memory
148
+ const BATCH_SIZE = 20;
149
+ for (let i = 0; i < allFiles.length; i += BATCH_SIZE) {
150
+ const batch = allFiles.slice(i, i + BATCH_SIZE);
151
+ await Promise.all(batch.map(async (filePath) => {
152
+ try {
153
+ const { indexed, chunksCreated, knowledgeItemsExtracted } = await indexFile(filePath, repoId, opts);
154
+ if (indexed) {
155
+ result.filesProcessed++;
156
+ result.chunksCreated += chunksCreated;
157
+ result.knowledgeItemsExtracted += knowledgeItemsExtracted;
158
+ }
159
+ else {
160
+ result.filesSkipped++;
161
+ }
162
+ }
163
+ catch (err) {
164
+ result.filesErrored++;
165
+ result.errors.push(`${filePath}: ${String(err)}`);
166
+ logger.error('Error indexing file', { filePath, error: String(err) });
167
+ }
168
+ }));
169
+ }
170
+ // Phase 2: Build CALLS edges across the repo using the entity registry.
171
+ const allReposForFullIndex = opts.allRepos ?? [opts.repoConfig];
172
+ const fullIndexRoute = getIndexingRoute(repoId, allReposForFullIndex);
173
+ if (fullIndexRoute.writeEntityNodes) {
174
+ await buildCallsEdges(repoId, opts);
175
+ }
176
+ // Phase 2: Run git analysis for CO_MODIFIED and TESTS edges
177
+ try {
178
+ await runGitAnalysis(resolve(repoPath), repoId, allFiles, opts.graphStore);
179
+ }
180
+ catch (err) {
181
+ const msg = `Git analysis failed: ${String(err)}`;
182
+ result.errors.push(msg);
183
+ logger.warn(msg, { repoId });
184
+ }
185
+ // Phase 3: Git history enrichment — file stats + commit record embeddings
186
+ try {
187
+ const resolvedRepoPath = resolve(repoPath);
188
+ // 3a. Analyze per-file stats and write to FalkorDB File nodes
189
+ const fileStats = await analyzeFileStats(resolvedRepoPath);
190
+ if (fileStats.size > 0 && fullIndexRoute.writeGraphNode) {
191
+ let statsWritten = 0;
192
+ for (const [filePath, stats] of fileStats) {
193
+ try {
194
+ await opts.graphStore.upsertNode('File', { path: filePath, repo_id: repoId }, {
195
+ commit_count: stats.commitCount,
196
+ stability_score: stats.stabilityScore,
197
+ change_velocity: stats.changeVelocity,
198
+ last_commit_hash: stats.lastCommitHash,
199
+ last_commit_ts: stats.lastCommitTs,
200
+ });
201
+ statsWritten++;
202
+ }
203
+ catch {
204
+ // Non-fatal: file node may not exist if it was filtered during indexing
205
+ }
206
+ }
207
+ logger.info('Git file stats written to graph', { repoId, statsWritten, totalFiles: fileStats.size });
208
+ }
209
+ // 3b. Extract commit records and embed messages into git_commits collection
210
+ const commits = await extractCommitRecords(resolvedRepoPath);
211
+ if (commits.length > 0) {
212
+ // Check if vectorStore supports git commit operations (QdrantVectorStore does)
213
+ const store = opts.vectorStore;
214
+ if (typeof store.upsertGitCommits === 'function') {
215
+ // Delete existing git commits for this repo before re-indexing
216
+ if (typeof store.deleteGitCommitsByRepo === 'function') {
217
+ await store.deleteGitCommitsByRepo(repoId);
218
+ }
219
+ // Embed commit messages in batches
220
+ const COMMIT_BATCH = 50;
221
+ let totalUpserted = 0;
222
+ for (let i = 0; i < commits.length; i += COMMIT_BATCH) {
223
+ const batch = commits.slice(i, i + COMMIT_BATCH);
224
+ const texts = batch.map(c => c.message);
225
+ const vectors = await embedBatch(texts);
226
+ const ZERO_VEC = new Array(384).fill(0);
227
+ const points = batch.map((commit, idx) => {
228
+ // Generate a deterministic ID from repo_id + commit hash
229
+ const idSource = `${repoId}:${commit.hash}`;
230
+ const id = createHash('sha256').update(idSource).digest('hex').slice(0, 32);
231
+ const uuid = `${id.slice(0, 8)}-${id.slice(8, 12)}-4${id.slice(13, 16)}-8${id.slice(17, 20)}-${id.slice(20, 32)}`;
232
+ return {
233
+ id: uuid,
234
+ vector: vectors[idx] ?? ZERO_VEC,
235
+ payload: {
236
+ repo_id: repoId,
237
+ commit_hash: commit.hash,
238
+ message: commit.message,
239
+ author: commit.author,
240
+ timestamp: commit.timestamp,
241
+ file_paths: commit.files,
242
+ },
243
+ };
244
+ });
245
+ await store.upsertGitCommits(points);
246
+ totalUpserted += points.length;
247
+ }
248
+ logger.info('Git commit messages embedded', { repoId, commitsEmbedded: totalUpserted });
249
+ }
250
+ }
251
+ }
252
+ catch (err) {
253
+ const msg = `Git enrichment (Phase 3) failed: ${String(err)}`;
254
+ result.errors.push(msg);
255
+ logger.warn(msg, { repoId });
256
+ }
257
+ // Update stats
258
+ const now = Date.now();
259
+ repoStats.set(repoId, {
260
+ filesIndexed: result.filesProcessed,
261
+ filesTotal: allFiles.length,
262
+ lastIndexedAt: now,
263
+ });
264
+ result.durationMs = now - startTime;
265
+ logger.info('Full index complete', {
266
+ repoId,
267
+ filesProcessed: result.filesProcessed,
268
+ filesSkipped: result.filesSkipped,
269
+ filesErrored: result.filesErrored,
270
+ chunksCreated: result.chunksCreated,
271
+ knowledgeItemsExtracted: result.knowledgeItemsExtracted,
272
+ errors: result.errors.length,
273
+ durationMs: result.durationMs,
274
+ });
275
+ return result;
276
+ }
277
+ /**
278
+ * Re-index a single file after a change event (incremental update).
279
+ * Deletes old data, re-indexes with fresh content.
280
+ */
281
+ export async function incrementalIndex(filePath, repoId, opts) {
282
+ logger.debug('Incremental re-index', { filePath, repoId });
283
+ // Delete old vectors and graph nodes first
284
+ await opts.vectorStore.deleteFileChunks(filePath, repoId);
285
+ await opts.graphStore.deleteFile(filePath, repoId);
286
+ opts.fileCache.invalidate(repoId, filePath);
287
+ // Remove from in-memory tracking so indexFile doesn't skip it
288
+ indexedFiles.delete(`${repoId}:${filePath}`);
289
+ // Re-index
290
+ const { indexed, chunksCreated } = await indexFile(filePath, repoId, opts);
291
+ if (indexed) {
292
+ logger.debug('Incremental re-index complete', { filePath, repoId, chunksCreated });
293
+ }
294
+ }
295
+ /**
296
+ * Remove all data for a repo from vector store and graph.
297
+ */
298
+ export async function removeRepoIndex(repoId, opts) {
299
+ logger.info('Removing repo index', { repoId });
300
+ await opts.vectorStore.deleteRepoChunks(repoId);
301
+ await opts.graphStore.deleteRepo(repoId);
302
+ opts.fileCache.invalidateRepo(repoId);
303
+ // Clear in-memory tracking for this repo
304
+ const prefix = `${repoId}:`;
305
+ for (const key of indexedFiles.keys()) {
306
+ if (key.startsWith(prefix)) {
307
+ indexedFiles.delete(key);
308
+ }
309
+ }
310
+ repoStats.delete(repoId);
311
+ repoEntityRegistry.delete(repoId);
312
+ }
313
+ /**
314
+ * Index a single file (full pipeline).
315
+ * Returns true if indexed, false if skipped.
316
+ *
317
+ * Phase 2: also populates IMPORTS, EXPORTS, EXTENDS, IMPLEMENTS edges.
318
+ * Phase 5: added forceReindex flag (bypasses hash check) and debug logging for skips.
319
+ */
320
+ export async function indexFile(filePath, repoId, opts, forceReindex = false) {
321
+ const { graphStore, vectorStore, fileCache, maxFileSizeKb = 500 } = opts;
322
+ const allRepos = opts.allRepos ?? [opts.repoConfig];
323
+ const route = getIndexingRoute(repoId, allRepos);
324
+ logger.debug('Indexing route determined', {
325
+ repoId,
326
+ filePath,
327
+ writeGraphNode: route.writeGraphNode,
328
+ writeEntityNodes: route.writeEntityNodes,
329
+ parseAst: route.parseAst,
330
+ writeFileCache: route.writeFileCache,
331
+ });
332
+ try {
333
+ // Check file size
334
+ const fileStat = await stat(filePath);
335
+ if (fileStat.size > maxFileSizeKb * 1024) {
336
+ logger.debug('Skipping large file', { filePath, sizeKb: Math.round(fileStat.size / 1024) });
337
+ return { indexed: false, chunksCreated: 0, knowledgeItemsExtracted: 0 };
338
+ }
339
+ // Read file content
340
+ const content = await readFile(filePath, 'utf8');
341
+ const contentHash = hashContent(content);
342
+ // Check if already indexed with same hash (skip unless forceReindex is set)
343
+ const existing = indexedFiles.get(`${repoId}:${filePath}`);
344
+ if (existing && existing.contentHash === contentHash && !forceReindex) {
345
+ logger.debug('Skipping unchanged file', { filePath, repoId, contentHash });
346
+ return { indexed: false, chunksCreated: 0, knowledgeItemsExtracted: 0 };
347
+ }
348
+ if (forceReindex && existing) {
349
+ logger.debug('Force re-indexing file (bypassing hash check)', { filePath, repoId });
350
+ }
351
+ // Remove old data for this file
352
+ if (existing) {
353
+ await vectorStore.deleteFileChunks(filePath, repoId);
354
+ // Phase 3: Mark observations stale for changed symbols before deleting graph nodes.
355
+ // Re-parse the old file's entities to find symbols that may have changed.
356
+ if (route.writeEntityNodes) {
357
+ try {
358
+ const oldParseResult = await parseFile(filePath, repoId);
359
+ if (oldParseResult.success) {
360
+ for (const entity of oldParseResult.entities) {
361
+ try {
362
+ await graphStore.query(MARK_SYMBOL_OBSERVATIONS_STALE(entity.name, repoId));
363
+ }
364
+ catch {
365
+ // Non-fatal: observation staleness marking is best-effort
366
+ }
367
+ }
368
+ }
369
+ }
370
+ catch {
371
+ // Non-fatal: if we can't re-parse, skip staleness marking
372
+ }
373
+ }
374
+ if (route.writeGraphNode) {
375
+ await graphStore.deleteFile(filePath, repoId);
376
+ }
377
+ if (route.writeFileCache) {
378
+ fileCache.invalidate(repoId, filePath);
379
+ }
380
+ }
381
+ const ext = extname(filePath).toLowerCase();
382
+ const language = getLanguage(filePath) ?? ext.slice(1) ?? 'unknown';
383
+ const now = Date.now();
384
+ let chunks;
385
+ if (SKIP_EXTENSIONS.has(ext)) {
386
+ return { indexed: false, chunksCreated: 0, knowledgeItemsExtracted: 0 };
387
+ }
388
+ if (CODE_EXTENSIONS.has(ext)) {
389
+ if (route.parseAst) {
390
+ // Full AST parse pipeline — owned repos get graph nodes + entity extraction
391
+ const parseResult = await parseFile(filePath, repoId);
392
+ // Store file node in graph only for owned repos
393
+ if (route.writeGraphNode) {
394
+ await upsertFileNode(graphStore, filePath, repoId, contentHash, language, now);
395
+ }
396
+ // Phase 2: Store entity nodes and all graph edges (owned repos only)
397
+ if (route.writeEntityNodes && parseResult.success) {
398
+ await upsertEntitiesWithEdges(graphStore, parseResult, content, repoId, filePath);
399
+ }
400
+ // Generate chunks from AST entities
401
+ if (parseResult.success && parseResult.entities.length > 0) {
402
+ chunks = chunkFromEntities(parseResult.entities, parseResult.imports, content, filePath, repoId, language, now);
403
+ }
404
+ else {
405
+ // Fallback to fixed chunking if AST parse failed or no entities
406
+ chunks = chunkFixed(content, filePath, repoId, language, now);
407
+ }
408
+ }
409
+ else {
410
+ // External repo: skip AST parse entirely, use fixed chunking only.
411
+ // This keeps vectors (semantic search) without polluting the graph.
412
+ chunks = chunkFixed(content, filePath, repoId, language, now);
413
+ logger.debug('External repo — skipping AST parse, using fixed chunking', { filePath, repoId });
414
+ }
415
+ }
416
+ else if (NON_CODE_EXTENSIONS.has(ext)) {
417
+ // Smart markdown chunking for .md/.mdx, fixed chunking for other non-code
418
+ if (ext === '.md' || ext === '.mdx') {
419
+ chunks = chunkMarkdown(content, filePath, repoId, now);
420
+ }
421
+ else {
422
+ chunks = chunkFixed(content, filePath, repoId, language, now);
423
+ }
424
+ // Store a File node only for owned repos
425
+ if (route.writeGraphNode) {
426
+ await upsertFileNode(graphStore, filePath, repoId, contentHash, language, now);
427
+ }
428
+ }
429
+ else {
430
+ return { indexed: false, chunksCreated: 0, knowledgeItemsExtracted: 0 };
431
+ }
432
+ if (chunks.length === 0) {
433
+ return { indexed: false, chunksCreated: 0, knowledgeItemsExtracted: 0 };
434
+ }
435
+ // Embed all chunks in batches (always — vectors written for all repos)
436
+ const chunkTexts = chunks.map(c => c.content);
437
+ const vectors = await embedBatch(chunkTexts);
438
+ // Upsert to Qdrant (route.writeVectorChunks is always true per routing logic)
439
+ const ZERO_VECTOR = new Array(384).fill(0);
440
+ const qdrantPoints = chunks.map((chunk, i) => ({
441
+ id: chunk.id,
442
+ vector: vectors[i] ?? ZERO_VECTOR,
443
+ payload: {
444
+ repo_id: chunk.repoId,
445
+ file_path: chunk.filePath,
446
+ entity_name: chunk.entityName,
447
+ entity_type: chunk.entityType,
448
+ start_line: chunk.startLine,
449
+ end_line: chunk.endLine,
450
+ language: chunk.language,
451
+ content_hash: chunk.contentHash,
452
+ content_preview: chunk.contentPreview,
453
+ indexed_at: chunk.indexedAt,
454
+ },
455
+ }));
456
+ await vectorStore.upsertCodeChunks(qdrantPoints);
457
+ // Extract and upsert knowledge items from markdown files
458
+ let knowledgeCount = 0;
459
+ if (ext === '.md' || ext === '.mdx') {
460
+ try {
461
+ // Resolve stack tags from repo config for knowledge items
462
+ const repoManifestStack = opts.repoConfig.languages ?? [];
463
+ const knowledgeItems = extractKnowledge(content, filePath, repoId, repoManifestStack);
464
+ if (knowledgeItems.length > 0) {
465
+ // Check if vectorStore has upsertKnowledge (QdrantVectorStore does)
466
+ const store = vectorStore;
467
+ if (typeof store.upsertKnowledge === 'function') {
468
+ for (const item of knowledgeItems) {
469
+ try {
470
+ const text = `${item.title} ${item.content}`;
471
+ const [vector] = await embedBatch([text]);
472
+ const ZERO_VEC = new Array(384).fill(0);
473
+ await store.upsertKnowledge(item.id, vector ?? ZERO_VEC, {
474
+ id: item.id,
475
+ repo_id: repoId,
476
+ category: item.id.split('-')[0] ?? 'pattern',
477
+ title: item.title,
478
+ content: item.content,
479
+ stack_tags: item.stack_tags,
480
+ confidence: item.confidence,
481
+ source: item.source,
482
+ source_phase: item.source_phase,
483
+ source_agent: item.source_agent,
484
+ sharing: opts.repoConfig.ownership === 'owned' ? 'team' : 'private',
485
+ created_at: item.created_at,
486
+ updated_at: item.updated_at,
487
+ accessed_at: now,
488
+ access_count: 0,
489
+ });
490
+ knowledgeCount++;
491
+ }
492
+ catch (kErr) {
493
+ logger.warn('Failed to upsert knowledge item from markdown', {
494
+ id: item.id,
495
+ filePath,
496
+ error: String(kErr),
497
+ });
498
+ }
499
+ }
500
+ if (knowledgeCount > 0) {
501
+ logger.debug('Knowledge items extracted from markdown', {
502
+ filePath, repoId, count: knowledgeCount,
503
+ });
504
+ }
505
+ }
506
+ }
507
+ }
508
+ catch (kErr) {
509
+ logger.warn('Knowledge extraction failed for markdown file', {
510
+ filePath, error: String(kErr),
511
+ });
512
+ }
513
+ }
514
+ // Cache file content only for owned repos (RAM budget control)
515
+ if (route.writeFileCache) {
516
+ fileCache.set(repoId, filePath, content, contentHash);
517
+ }
518
+ // Update indexed files tracking
519
+ indexedFiles.set(`${repoId}:${filePath}`, { contentHash, indexedAt: now });
520
+ logger.debug('File indexed', {
521
+ filePath,
522
+ repoId,
523
+ chunks: chunks.length,
524
+ knowledgeItems: knowledgeCount,
525
+ language,
526
+ owned: route.writeGraphNode,
527
+ });
528
+ return { indexed: true, chunksCreated: chunks.length, knowledgeItemsExtracted: knowledgeCount };
529
+ }
530
+ catch (err) {
531
+ logger.error('Failed to index file', { filePath, error: String(err) });
532
+ return { indexed: false, chunksCreated: 0, knowledgeItemsExtracted: 0 };
533
+ }
534
+ }
535
+ /**
536
+ * Get repo index stats.
537
+ */
538
+ export function getRepoStats(repoId) {
539
+ return repoStats.get(repoId) ?? {
540
+ filesIndexed: 0,
541
+ filesTotal: 0,
542
+ lastIndexedAt: 0,
543
+ };
544
+ }
545
+ /**
546
+ * Get all indexed files for a repo.
547
+ */
548
+ export function getIndexedFiles(repoId) {
549
+ const prefix = `${repoId}:`;
550
+ return Array.from(indexedFiles.keys())
551
+ .filter(k => k.startsWith(prefix))
552
+ .map(k => k.slice(prefix.length));
553
+ }
554
+ // ============================================================
555
+ // Phase 2: Graph population helpers
556
+ // ============================================================
557
+ async function upsertFileNode(graphStore, filePath, repoId, contentHash, language, now) {
558
+ try {
559
+ await graphStore.upsertNode('File', { path: filePath, repo_id: repoId }, {
560
+ content_hash: contentHash,
561
+ language,
562
+ last_indexed: now,
563
+ size_bytes: 0,
564
+ });
565
+ }
566
+ catch (err) {
567
+ logger.warn('Failed to upsert file node', { filePath, error: String(err) });
568
+ }
569
+ }
570
+ /**
571
+ * Upsert all entities for a file and create the full set of edges:
572
+ * CONTAINS (File -> entity)
573
+ * EXPORTS (File -> entity, for exported symbols)
574
+ * IMPORTS (File -> File, from import statements)
575
+ * EXTENDS (Class -> Class, when extends clause is present)
576
+ * IMPLEMENTS (Class -> Interface, when implements clause is present)
577
+ *
578
+ * Also registers entity names in the repo entity registry for later CALLS analysis.
579
+ */
580
+ async function upsertEntitiesWithEdges(graphStore, parseResult, _content, repoId, filePath) {
581
+ const entityRegistry = repoEntityRegistry.get(repoId) ?? new Map();
582
+ repoEntityRegistry.set(repoId, entityRegistry);
583
+ // 1. Upsert entity nodes + CONTAINS + EXPORTS edges
584
+ for (const entity of parseResult.entities) {
585
+ await upsertEntityNode(graphStore, entity);
586
+ // Register in entity registry for CALLS edge discovery
587
+ entityRegistry.set(entity.name, filePath);
588
+ // Create CONTAINS edge
589
+ await safeUpsertEdge(graphStore, 'File', { path: filePath, repo_id: repoId }, 'CONTAINS', {}, entityLabel(entity.type), { name: entity.name, file_path: filePath, repo_id: repoId });
590
+ // Create EXPORTS edge for exported symbols
591
+ if (entity.isExported) {
592
+ await safeUpsertEdge(graphStore, 'File', { path: filePath, repo_id: repoId }, 'EXPORTS', { is_default: entity.isDefault ?? false }, entityLabel(entity.type), { name: entity.name, file_path: filePath, repo_id: repoId });
593
+ }
594
+ // Phase 2: EXTENDS and IMPLEMENTS edges (extracted from sourceText heuristics)
595
+ if (entity.type === 'class' && entity.sourceText) {
596
+ await createClassRelationshipEdges(graphStore, entity, repoId);
597
+ }
598
+ }
599
+ // 2. Create IMPORTS edges from import declarations
600
+ for (const imp of parseResult.imports) {
601
+ const resolvedPath = resolveImportPath(filePath, imp.fromPath, repoId);
602
+ if (resolvedPath) {
603
+ await safeUpsertEdge(graphStore, 'File', { path: filePath, repo_id: repoId }, 'IMPORTS', { specifiers: imp.specifiers }, 'File', { path: resolvedPath, repo_id: repoId });
604
+ }
605
+ }
606
+ }
607
+ /**
608
+ * Create EXTENDS and IMPLEMENTS edges for class declarations.
609
+ * Uses simple regex-based heuristics on the class source text since we
610
+ * don't have full type resolution. Handles:
611
+ * class Foo extends Bar { ... }
612
+ * class Foo implements IFoo, IBar { ... }
613
+ * class Foo extends Bar implements IFoo { ... }
614
+ */
615
+ async function createClassRelationshipEdges(graphStore, classEntity, repoId) {
616
+ const src = classEntity.sourceText;
617
+ // Extract extends clause: class Foo extends BarName
618
+ const extendsMatch = src.match(/\bextends\s+([A-Za-z_$][A-Za-z0-9_$]*)/);
619
+ if (extendsMatch) {
620
+ const parentName = extendsMatch[1];
621
+ // Create EXTENDS edge — target Class node may or may not exist yet
622
+ await safeUpsertEdge(graphStore, 'Class', { name: classEntity.name, file_path: classEntity.filePath, repo_id: repoId }, 'EXTENDS', {}, 'Class', { name: parentName, repo_id: repoId });
623
+ }
624
+ // Extract implements clause: implements IFoo, IBar<T>
625
+ const implementsMatch = src.match(/\bimplements\s+([A-Za-z_$][A-Za-z0-9_$<>,\s]*?)(?:\s*\{|extends|implements|$)/);
626
+ if (implementsMatch) {
627
+ // Split on comma, strip generic type params
628
+ const interfaces = implementsMatch[1]
629
+ .split(',')
630
+ .map(s => s.replace(/<[^>]*>/g, '').trim())
631
+ .filter(s => /^[A-Za-z_$]/.test(s));
632
+ for (const ifaceName of interfaces) {
633
+ await safeUpsertEdge(graphStore, 'Class', { name: classEntity.name, file_path: classEntity.filePath, repo_id: repoId }, 'IMPLEMENTS', {}, 'Interface', { name: ifaceName, repo_id: repoId });
634
+ }
635
+ }
636
+ }
637
+ /**
638
+ * Upsert a single entity node in FalkorDB.
639
+ */
640
+ async function upsertEntityNode(graphStore, entity) {
641
+ const label = entityLabel(entity.type);
642
+ if (!label)
643
+ return;
644
+ try {
645
+ await graphStore.upsertNode(label, { name: entity.name, file_path: entity.filePath, repo_id: entity.repoId }, {
646
+ start_line: entity.startLine,
647
+ end_line: entity.endLine,
648
+ is_exported: entity.isExported,
649
+ is_async: entity.isAsync ?? false,
650
+ params: entity.params ?? '',
651
+ is_const: entity.isConst ?? false,
652
+ });
653
+ }
654
+ catch (err) {
655
+ logger.warn('Failed to upsert entity node', { name: entity.name, error: String(err) });
656
+ }
657
+ }
658
+ /**
659
+ * Build CALLS edges across the entire repo using static identifier matching.
660
+ * Algorithm: for each function entity, scan its sourceText for identifiers that
661
+ * match other known function names in the same repo. If found, create CALLS edge.
662
+ *
663
+ * This is Phase 2 static analysis — no type resolution required.
664
+ */
665
+ async function buildCallsEdges(repoId, opts) {
666
+ const entityRegistry = repoEntityRegistry.get(repoId);
667
+ if (!entityRegistry || entityRegistry.size === 0)
668
+ return;
669
+ logger.debug('Building CALLS edges via static analysis', {
670
+ repoId,
671
+ knownEntities: entityRegistry.size,
672
+ });
673
+ // We need to re-read parse results to get sourceText.
674
+ // We iterate over all indexed files for this repo.
675
+ const allIndexedFiles = getIndexedFiles(repoId);
676
+ for (const filePath of allIndexedFiles) {
677
+ const ext = extname(filePath).toLowerCase();
678
+ if (!CODE_EXTENSIONS.has(ext))
679
+ continue;
680
+ try {
681
+ const parseResult = await parseFile(filePath, repoId);
682
+ if (!parseResult.success)
683
+ continue;
684
+ for (const entity of parseResult.entities) {
685
+ if (entity.type !== 'function')
686
+ continue;
687
+ if (!entity.sourceText)
688
+ continue;
689
+ // Find all identifiers in the function body that match known entity names
690
+ // Skip the entity's own name to avoid self-references
691
+ for (const [calleeName, calleePath] of entityRegistry) {
692
+ if (calleeName === entity.name)
693
+ continue;
694
+ if (!isIdentifierReferenced(entity.sourceText, calleeName))
695
+ continue;
696
+ // Create CALLS edge
697
+ await safeUpsertEdge(opts.graphStore, 'Function', { name: entity.name, file_path: filePath, repo_id: repoId }, 'CALLS', { call_count: 1 }, 'Function', { name: calleeName, file_path: calleePath, repo_id: repoId });
698
+ }
699
+ }
700
+ }
701
+ catch (err) {
702
+ logger.warn('CALLS edge analysis failed for file', { filePath, error: String(err) });
703
+ }
704
+ }
705
+ logger.debug('CALLS edge construction complete', { repoId });
706
+ }
707
+ /**
708
+ * Check if an identifier name appears as a standalone word in source text.
709
+ * Uses word boundary matching to avoid matching substrings.
710
+ */
711
+ function isIdentifierReferenced(sourceText, identifierName) {
712
+ // Escape any regex special chars in the name
713
+ const escaped = identifierName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
714
+ const pattern = new RegExp(`\\b${escaped}\\b`);
715
+ return pattern.test(sourceText);
716
+ }
717
+ /**
718
+ * Resolve an import path specifier to an absolute file path.
719
+ * Only resolves relative imports (./foo, ../bar). Skips node_modules imports.
720
+ */
721
+ function resolveImportPath(fromFile, importPath, _repoId) {
722
+ // Skip non-relative imports (node_modules, absolute)
723
+ if (!importPath.startsWith('.'))
724
+ return null;
725
+ if (isAbsolute(importPath))
726
+ return null;
727
+ const fromDir = dirname(fromFile);
728
+ const resolved = join(fromDir, importPath);
729
+ // Return the first candidate without extension check (graph allows forward references)
730
+ // The .ts extension is most common in TS projects
731
+ const withExt = resolved.endsWith('.js') || resolved.endsWith('.ts') ||
732
+ resolved.endsWith('.tsx') || resolved.endsWith('.jsx');
733
+ if (withExt)
734
+ return resolved;
735
+ return `${resolved}.ts`; // Default assumption for TS projects
736
+ }
737
+ /**
738
+ * Map entity type string to FalkorDB graph label.
739
+ */
740
+ function entityLabel(type) {
741
+ const labelMap = {
742
+ function: 'Function',
743
+ class: 'Class',
744
+ interface: 'Interface',
745
+ type_alias: 'TypeAlias',
746
+ variable: 'Variable',
747
+ module: 'Module',
748
+ };
749
+ return labelMap[type] ?? '';
750
+ }
751
+ /**
752
+ * Wrapper around graphStore.upsertEdge that catches and logs errors.
753
+ */
754
+ async function safeUpsertEdge(graphStore, fromLabel, fromProps, edgeType, edgeProps, toLabel, toProps) {
755
+ try {
756
+ await graphStore.upsertEdge(fromLabel, fromProps, edgeType, edgeProps, toLabel, toProps);
757
+ }
758
+ catch (err) {
759
+ logger.warn(`Failed to upsert ${edgeType} edge`, {
760
+ from: JSON.stringify(fromProps),
761
+ to: JSON.stringify(toProps),
762
+ error: String(err),
763
+ });
764
+ }
765
+ }
766
+ //# sourceMappingURL=indexer.js.map