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