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,337 @@
1
+ /**
2
+ * Hybrid search engine combining vector similarity + graph neighbor boost.
3
+ * Final scoring: 0.6 * vector_score + 0.3 * graph_centrality + 0.1 * recency_boost
4
+ *
5
+ * Phase 2: Full graph neighbor expansion.
6
+ * For each vector hit, traverse graph neighbors (IMPORTS, CALLS, TESTS, CO_MODIFIED)
7
+ * up to pivot_depth hops, collect neighbor file paths, and boost their scores.
8
+ * Graph centrality = number of times a file appears as a neighbor / max neighbor appearances.
9
+ */
10
+
11
+ import type { VectorStore, GraphStore } from '../storage/interfaces.js';
12
+ import type { CodeChunkPayload, QueryIntent } from '../util/types.js';
13
+ import { embedText } from '../ingestion/embedder.js';
14
+ import { computeScore, deduplicateByFile, buildMatchReason, type ScoredResult } from './ranking.js';
15
+ import { QUERY_FILE_NEIGHBORS, QUERY_FILE_HOTSPOTS } from './graph-queries.js';
16
+ import { logger } from '../util/logger.js';
17
+
18
+ export interface HybridSearchOptions {
19
+ query: string;
20
+ repoId?: string;
21
+ limit?: number;
22
+ includeTests?: boolean;
23
+ pivotDepth?: number;
24
+ vectorWeight?: number;
25
+ graphWeight?: number;
26
+ /** Query intent — affects whether hotspot scoring is applied */
27
+ intent?: QueryIntent;
28
+ }
29
+
30
+ export interface HybridSearchResult {
31
+ results: ScoredResult[];
32
+ cacheHits: number;
33
+ executionTimeMs: number;
34
+ }
35
+
36
+ /**
37
+ * Perform hybrid search over indexed code chunks.
38
+ * Returns deduplicated, ranked results by file.
39
+ */
40
+ export async function hybridSearch(
41
+ vectorStore: VectorStore,
42
+ graphStore: GraphStore,
43
+ options: HybridSearchOptions
44
+ ): Promise<HybridSearchResult> {
45
+ const startTime = Date.now();
46
+ const {
47
+ query,
48
+ repoId,
49
+ limit = 20,
50
+ includeTests = false,
51
+ pivotDepth = 2,
52
+ vectorWeight = 0.55,
53
+ graphWeight = 0.20,
54
+ intent,
55
+ } = options;
56
+
57
+ try {
58
+ // Step 1: Embed query
59
+ const queryVector = await embedText(query);
60
+
61
+ // Step 2: Vector search
62
+ const filter: Record<string, unknown> = {};
63
+ if (repoId) filter['repo_id'] = repoId;
64
+
65
+ const vectorResults = await vectorStore.searchCodeChunks(queryVector, {
66
+ limit: limit * 2, // Get extra to allow deduplication
67
+ filter: Object.keys(filter).length > 0 ? filter : undefined,
68
+ scoreThreshold: 0.1,
69
+ });
70
+
71
+ // Step 3: Filter test files if needed
72
+ const filteredResults = vectorResults.filter(r => {
73
+ const payload = r.payload as CodeChunkPayload;
74
+ if (!includeTests) {
75
+ const fp = payload.file_path ?? '';
76
+ if (fp.includes('.spec.') || fp.includes('.test.') || fp.includes('__tests__')) {
77
+ return false;
78
+ }
79
+ }
80
+ return true;
81
+ });
82
+
83
+ // Step 4: Graph neighbor expansion
84
+ // For each unique file in vector results, traverse graph neighbors and
85
+ // collect how often each neighboring file appears (centrality count).
86
+ const neighborCounts = new Map<string, number>(); // filePath -> appearance count
87
+ const vectorFileSet = new Set(
88
+ filteredResults.map(r => (r.payload as CodeChunkPayload).file_path)
89
+ );
90
+
91
+ const graphHealthy = await graphStore.isHealthy();
92
+ if (!graphHealthy && repoId) {
93
+ logger.warn('Graph store is unhealthy — skipping graph expansion (vector-only mode)', {
94
+ query: query.slice(0, 50),
95
+ repoId,
96
+ });
97
+ }
98
+ if (graphHealthy && repoId) {
99
+ const uniqueFiles = Array.from(vectorFileSet);
100
+
101
+ // Query graph neighbors for each vector hit file
102
+ await Promise.all(uniqueFiles.map(async (filePath) => {
103
+ try {
104
+ const result = await graphStore.query(
105
+ QUERY_FILE_NEIGHBORS(filePath, repoId, pivotDepth)
106
+ );
107
+
108
+ // Parse neighbor paths from raw result
109
+ const rows = result.raw ?? [];
110
+ for (const row of rows) {
111
+ const neighborPath = extractNeighborPath(row);
112
+ if (neighborPath && neighborPath !== filePath) {
113
+ neighborCounts.set(neighborPath, (neighborCounts.get(neighborPath) ?? 0) + 1);
114
+ }
115
+ }
116
+ } catch {
117
+ // Graph query failure is non-fatal — degrade to vector-only
118
+ }
119
+ }));
120
+ }
121
+
122
+ // Step 5: Compute normalized graph centrality scores
123
+ const maxNeighborCount = neighborCounts.size > 0
124
+ ? Math.max(...neighborCounts.values())
125
+ : 1;
126
+
127
+ // Step 5b: Fetch file hotspot scores from FalkorDB File node commit_count
128
+ const hotspotMap = new Map<string, number>(); // filePath -> normalized hotspot score
129
+ const useHotspot = intent === 'modify' || intent === 'debug' || intent === 'refactor';
130
+
131
+ if (useHotspot && graphHealthy && repoId) {
132
+ try {
133
+ const hotspotResult = await graphStore.query(
134
+ QUERY_FILE_HOTSPOTS(repoId, 100)
135
+ );
136
+ const rows = hotspotResult.raw ?? [];
137
+ let maxCommitCount = 1;
138
+
139
+ // First pass: find max commit count
140
+ for (const row of rows) {
141
+ const commitCount = extractHotspotCommitCount(row);
142
+ if (commitCount > maxCommitCount) maxCommitCount = commitCount;
143
+ }
144
+
145
+ // Second pass: normalize
146
+ for (const row of rows) {
147
+ const path = extractHotspotPath(row);
148
+ const commitCount = extractHotspotCommitCount(row);
149
+ if (path && commitCount > 0) {
150
+ hotspotMap.set(path, commitCount / maxCommitCount);
151
+ }
152
+ }
153
+ } catch {
154
+ // Hotspot query failure is non-fatal
155
+ logger.debug('Hotspot query failed — skipping hotspot scoring');
156
+ }
157
+ }
158
+
159
+ // Step 6: Convert to ScoredResult with combined scores
160
+ const scored: ScoredResult[] = filteredResults.map(r => {
161
+ const payload = r.payload as CodeChunkPayload;
162
+ const filePath = payload.file_path;
163
+
164
+ // Graph score: normalized neighbor appearance count
165
+ // Files that appear as neighbors of many vector hits get boosted
166
+ const rawGraphScore = neighborCounts.get(filePath) ?? 0;
167
+ const graphCentrality = maxNeighborCount > 0 ? rawGraphScore / maxNeighborCount : 0;
168
+
169
+ // Hotspot score: normalized commit count (0 for read intent)
170
+ const hotspotScore = useHotspot ? (hotspotMap.get(filePath) ?? 0) : 0;
171
+
172
+ const { finalScore, recencyScore } = computeScore(
173
+ r.score,
174
+ graphCentrality,
175
+ payload.indexed_at ?? Date.now(),
176
+ vectorWeight,
177
+ graphWeight,
178
+ undefined, // recencyWeight — use default
179
+ hotspotScore,
180
+ undefined, // hotspotWeight — use default
181
+ 0, // observationScore — Phase 4
182
+ undefined // observationWeight — use default
183
+ );
184
+
185
+ return {
186
+ filePath,
187
+ repoId: payload.repo_id,
188
+ vectorScore: r.score,
189
+ graphScore: graphCentrality,
190
+ recencyScore,
191
+ hotspotScore,
192
+ observationScore: 0,
193
+ finalScore,
194
+ entityName: payload.entity_name,
195
+ entityType: payload.entity_type,
196
+ startLine: payload.start_line,
197
+ endLine: payload.end_line,
198
+ matchReason: buildMatchReason(r.score, graphCentrality, payload.entity_name, payload.entity_type, hotspotScore),
199
+ indexedAt: payload.indexed_at,
200
+ } satisfies ScoredResult;
201
+ });
202
+
203
+ // Step 7: Add neighbor files that scored above threshold but weren't in vector results
204
+ // These are files boosted purely by graph centrality (indirect relevance)
205
+ for (const [neighborPath, count] of neighborCounts) {
206
+ if (vectorFileSet.has(neighborPath)) continue; // Already in vector results
207
+
208
+ // Skip test files if not requested
209
+ if (!includeTests) {
210
+ if (
211
+ neighborPath.includes('.spec.') ||
212
+ neighborPath.includes('.test.') ||
213
+ neighborPath.includes('__tests__')
214
+ ) continue;
215
+ }
216
+
217
+ const normalizedCount = count / maxNeighborCount;
218
+ if (normalizedCount < 0.3) continue; // Only include strongly-connected neighbors
219
+
220
+ const neighborHotspot = useHotspot ? (hotspotMap.get(neighborPath) ?? 0) : 0;
221
+
222
+ const { finalScore, recencyScore } = computeScore(
223
+ 0, // No direct vector score
224
+ normalizedCount,
225
+ Date.now(), // Unknown indexed_at for neighbor-only results
226
+ vectorWeight,
227
+ graphWeight,
228
+ undefined,
229
+ neighborHotspot,
230
+ undefined,
231
+ 0,
232
+ undefined
233
+ );
234
+
235
+ scored.push({
236
+ filePath: neighborPath,
237
+ repoId: repoId ?? '',
238
+ vectorScore: 0,
239
+ graphScore: normalizedCount,
240
+ recencyScore,
241
+ hotspotScore: neighborHotspot,
242
+ observationScore: 0,
243
+ finalScore,
244
+ entityName: null,
245
+ entityType: 'file_level',
246
+ startLine: 0,
247
+ endLine: 0,
248
+ matchReason: buildMatchReason(0, normalizedCount, null, 'file_level', neighborHotspot),
249
+ indexedAt: undefined,
250
+ });
251
+ }
252
+
253
+ // Step 8: Deduplicate by file, keep highest score per file
254
+ const deduplicated = deduplicateByFile(scored);
255
+
256
+ logger.debug('Hybrid search completed', {
257
+ query: query.slice(0, 50),
258
+ vectorHits: vectorResults.length,
259
+ filteredHits: filteredResults.length,
260
+ neighborFiles: neighborCounts.size,
261
+ uniqueFiles: deduplicated.length,
262
+ durationMs: Date.now() - startTime,
263
+ });
264
+
265
+ return {
266
+ results: deduplicated.slice(0, limit),
267
+ cacheHits: 0, // Tracked by FileContentCache in tool handler
268
+ executionTimeMs: Date.now() - startTime,
269
+ };
270
+ } catch (err) {
271
+ logger.error('Hybrid search failed', { query: query.slice(0, 50), error: String(err) });
272
+ return {
273
+ results: [],
274
+ cacheHits: 0,
275
+ executionTimeMs: Date.now() - startTime,
276
+ };
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Extract neighbor file path from a raw FalkorDB query result row.
282
+ * QUERY_FILE_NEIGHBORS returns: RETURN neighbor.path as path, ...
283
+ * FalkorDB non-compact format: [header, [rows], stats]
284
+ * Each row cell for an alias like `path` is a scalar string.
285
+ */
286
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
287
+ function extractNeighborPath(row: any): string | null {
288
+ if (!row) return null;
289
+
290
+ // Row is an array of cell values (one per column in SELECT)
291
+ // Column order: [path, rel_type, depth]
292
+ if (Array.isArray(row)) {
293
+ const first = row[0];
294
+ if (typeof first === 'string') return first;
295
+ }
296
+
297
+ // Object row format (some parsers return keyed objects)
298
+ if (typeof row === 'object' && row !== null) {
299
+ if (typeof row.path === 'string') return row.path;
300
+ }
301
+
302
+ return null;
303
+ }
304
+
305
+ /**
306
+ * Extract file path from a QUERY_FILE_HOTSPOTS result row.
307
+ * Column order: [path, commit_count, stability_score, change_velocity]
308
+ */
309
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
310
+ function extractHotspotPath(row: any): string | null {
311
+ if (!row) return null;
312
+ if (Array.isArray(row)) {
313
+ const first = row[0];
314
+ if (typeof first === 'string') return first;
315
+ }
316
+ if (typeof row === 'object' && row !== null) {
317
+ if (typeof row.path === 'string') return row.path;
318
+ }
319
+ return null;
320
+ }
321
+
322
+ /**
323
+ * Extract commit_count from a QUERY_FILE_HOTSPOTS result row.
324
+ * Column order: [path, commit_count, stability_score, change_velocity]
325
+ */
326
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
327
+ function extractHotspotCommitCount(row: any): number {
328
+ if (!row) return 0;
329
+ if (Array.isArray(row)) {
330
+ const second = row[1];
331
+ if (typeof second === 'number') return second;
332
+ }
333
+ if (typeof row === 'object' && row !== null) {
334
+ if (typeof row.commit_count === 'number') return row.commit_count;
335
+ }
336
+ return 0;
337
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Query intent classifier using keyword matching (Phase 1).
3
+ * Classifies queries as: read | modify | debug | refactor
4
+ * Phase 4 will enhance with ML-based classification.
5
+ */
6
+
7
+ import type { QueryIntent } from '../util/types.js';
8
+
9
+ const INTENT_KEYWORDS: Record<QueryIntent, string[]> = {
10
+ debug: [
11
+ 'error', 'bug', 'fix', 'broken', 'failing', 'crash', 'exception',
12
+ 'undefined', 'null', 'TypeError', 'undefined is not', 'cannot read',
13
+ 'stack trace', 'stacktrace', 'traceback', 'debug', 'diagnose', 'why is',
14
+ 'not working', 'breaks', 'issue', 'problem', 'wrong', 'incorrect',
15
+ ],
16
+ refactor: [
17
+ 'refactor', 'restructure', 'reorganize', 'extract', 'move', 'rename',
18
+ 'split', 'merge', 'clean up', 'cleanup', 'simplify', 'optimize',
19
+ 'performance', 'improve', 'better', 'smell', 'technical debt', 'dead code',
20
+ 'duplicate', 'dry', 'abstract',
21
+ ],
22
+ modify: [
23
+ 'add', 'implement', 'create', 'build', 'write', 'update', 'change',
24
+ 'modify', 'edit', 'extend', 'enhance', 'feature', 'new', 'support',
25
+ 'integrate', 'connect', 'wire', 'hook up', 'plug in', 'configure',
26
+ ],
27
+ read: [
28
+ 'how', 'what', 'where', 'explain', 'understand', 'show', 'list',
29
+ 'find', 'search', 'look', 'describe', 'overview', 'structure', 'schema',
30
+ 'architecture', 'flow', 'logic', 'context', 'about', 'definition',
31
+ ],
32
+ };
33
+
34
+ /**
35
+ * Detect the intent of a query string.
36
+ * Returns the intent category with highest keyword match count.
37
+ * Default: 'read' (safest, most common).
38
+ */
39
+ export function detectIntent(query: string): QueryIntent {
40
+ const lowerQuery = query.toLowerCase();
41
+ const scores: Record<QueryIntent, number> = { debug: 0, refactor: 0, modify: 0, read: 0 };
42
+
43
+ for (const [intent, keywords] of Object.entries(INTENT_KEYWORDS) as [QueryIntent, string[]][]) {
44
+ for (const keyword of keywords) {
45
+ if (lowerQuery.includes(keyword)) {
46
+ scores[intent]++;
47
+ }
48
+ }
49
+ }
50
+
51
+ // Find intent with highest score
52
+ let maxScore = 0;
53
+ let detectedIntent: QueryIntent = 'read';
54
+
55
+ // Priority: debug > refactor > modify > read (more specific wins on tie)
56
+ for (const intent of ['debug', 'refactor', 'modify', 'read'] as QueryIntent[]) {
57
+ if (scores[intent] > maxScore) {
58
+ maxScore = scores[intent];
59
+ detectedIntent = intent;
60
+ }
61
+ }
62
+
63
+ return detectedIntent;
64
+ }
65
+
66
+ /**
67
+ * Get retrieval strategy adjustments based on intent.
68
+ * These affect ranking weights and result composition.
69
+ *
70
+ * Phase 4: 5-signal weights (vector, graph, recency, hotspot, observation).
71
+ *
72
+ * | Intent | Vector | Graph | Recency | Hotspot | Observation |
73
+ * |----------|--------|-------|---------|---------|-------------|
74
+ * | read | 0.55 | 0.20 | 0.05 | 0.10 | 0.10 |
75
+ * | modify | 0.50 | 0.20 | 0.08 | 0.12 | 0.10 |
76
+ * | debug | 0.40 | 0.25 | 0.05 | 0.10 | 0.20 |
77
+ * | refactor | 0.35 | 0.30 | 0.05 | 0.15 | 0.15 |
78
+ */
79
+ export function getIntentStrategy(intent: QueryIntent): {
80
+ vectorWeight: number;
81
+ graphWeight: number;
82
+ recencyWeight: number;
83
+ hotspotWeight: number;
84
+ observationWeight: number;
85
+ includeTests: boolean;
86
+ expandDepth: number;
87
+ } {
88
+ switch (intent) {
89
+ case 'debug':
90
+ return {
91
+ vectorWeight: 0.40,
92
+ graphWeight: 0.25,
93
+ recencyWeight: 0.05,
94
+ hotspotWeight: 0.10,
95
+ observationWeight: 0.20, // Observations are critical for debugging
96
+ includeTests: true, // Tests often show error scenarios
97
+ expandDepth: 3,
98
+ };
99
+ case 'refactor':
100
+ return {
101
+ vectorWeight: 0.35,
102
+ graphWeight: 0.30, // Blast radius is critical for refactoring
103
+ recencyWeight: 0.05,
104
+ hotspotWeight: 0.15, // Hotspots indicate high-churn areas
105
+ observationWeight: 0.15,
106
+ includeTests: true,
107
+ expandDepth: 3,
108
+ };
109
+ case 'modify':
110
+ return {
111
+ vectorWeight: 0.50,
112
+ graphWeight: 0.20,
113
+ recencyWeight: 0.08,
114
+ hotspotWeight: 0.12,
115
+ observationWeight: 0.10,
116
+ includeTests: false,
117
+ expandDepth: 2,
118
+ };
119
+ case 'read':
120
+ default:
121
+ return {
122
+ vectorWeight: 0.55,
123
+ graphWeight: 0.20,
124
+ recencyWeight: 0.05,
125
+ hotspotWeight: 0.10,
126
+ observationWeight: 0.10,
127
+ includeTests: false,
128
+ expandDepth: 2,
129
+ };
130
+ }
131
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Result ranking and scoring for hybrid search.
3
+ * Phase 1: Vector score only (graph_centrality added in Phase 2).
4
+ * Phase 3: 4-signal scoring with hotspot.
5
+ * Phase 4: 5-signal scoring with observation relevance.
6
+ *
7
+ * Default weights: 0.55 vector + 0.20 graph + 0.05 recency + 0.10 hotspot + 0.10 observation
8
+ * All new weights are backward-compatible (default to 0).
9
+ */
10
+
11
+ const VECTOR_WEIGHT = 0.55;
12
+ const GRAPH_WEIGHT = 0.20;
13
+ const RECENCY_WEIGHT = 0.05;
14
+ const HOTSPOT_WEIGHT = 0.10;
15
+ const OBSERVATION_WEIGHT = 0.10;
16
+
17
+ // Recency boost: files indexed within the last hour get a small boost
18
+ const RECENCY_WINDOW_MS = 60 * 60 * 1000; // 1 hour
19
+
20
+ export interface ScoredResult {
21
+ filePath: string;
22
+ repoId: string;
23
+ vectorScore: number;
24
+ graphScore: number;
25
+ recencyScore: number;
26
+ hotspotScore: number;
27
+ observationScore: number;
28
+ finalScore: number;
29
+ entityName: string | null;
30
+ entityType: string;
31
+ startLine: number;
32
+ endLine: number;
33
+ matchReason: string;
34
+ indexedAt?: number;
35
+ }
36
+
37
+ /**
38
+ * Compute final ranking score combining up to 5 signals:
39
+ * vector, graph, recency, hotspot, observation.
40
+ *
41
+ * Backward-compatible: hotspotScore and observationScore default to 0,
42
+ * with their weights redistributed to maintain consistent scoring
43
+ * when callers haven't been updated to supply them.
44
+ */
45
+ export function computeScore(
46
+ vectorScore: number,
47
+ graphScore: number,
48
+ indexedAt: number,
49
+ vectorWeight = VECTOR_WEIGHT,
50
+ graphWeight = GRAPH_WEIGHT,
51
+ recencyWeight = RECENCY_WEIGHT,
52
+ hotspotScore: number = 0,
53
+ hotspotWeight: number = HOTSPOT_WEIGHT,
54
+ observationScore: number = 0,
55
+ observationWeight: number = OBSERVATION_WEIGHT
56
+ ): { finalScore: number; recencyScore: number } {
57
+ const now = Date.now();
58
+ const age = now - indexedAt;
59
+ const recencyScore = age < RECENCY_WINDOW_MS ? 1.0 - (age / RECENCY_WINDOW_MS) : 0;
60
+
61
+ const finalScore =
62
+ vectorWeight * vectorScore +
63
+ graphWeight * graphScore +
64
+ recencyWeight * recencyScore +
65
+ hotspotWeight * hotspotScore +
66
+ observationWeight * observationScore;
67
+
68
+ return { finalScore, recencyScore };
69
+ }
70
+
71
+ /**
72
+ * Deduplicate results by file path, keeping the highest-scored entry per file.
73
+ */
74
+ export function deduplicateByFile(results: ScoredResult[]): ScoredResult[] {
75
+ const byFile = new Map<string, ScoredResult>();
76
+
77
+ for (const result of results) {
78
+ const existing = byFile.get(result.filePath);
79
+ if (!existing || result.finalScore > existing.finalScore) {
80
+ byFile.set(result.filePath, result);
81
+ }
82
+ }
83
+
84
+ return Array.from(byFile.values()).sort((a, b) => b.finalScore - a.finalScore);
85
+ }
86
+
87
+ /**
88
+ * Split results into pivot files (full content) and skeletons (summaries) using actual token sizes.
89
+ * If fileSizes is provided (map of filePath -> estimated tokens), uses real sizes.
90
+ * Otherwise falls back to avgFileTokens estimate.
91
+ */
92
+ export function splitPivotsAndSkeletons(
93
+ results: ScoredResult[],
94
+ tokenBudget: number,
95
+ fileSizes?: Map<string, number>,
96
+ avgFileTokens: number = 500
97
+ ): { pivots: ScoredResult[]; skeletons: ScoredResult[] } {
98
+ const pivots: ScoredResult[] = [];
99
+ const skeletons: ScoredResult[] = [];
100
+ let usedTokens = 0;
101
+
102
+ for (const result of results) {
103
+ const fileTokens = fileSizes?.get(result.filePath) ?? avgFileTokens;
104
+ if (usedTokens + fileTokens <= tokenBudget) {
105
+ pivots.push(result);
106
+ usedTokens += fileTokens;
107
+ } else {
108
+ skeletons.push(result);
109
+ }
110
+ }
111
+
112
+ return { pivots, skeletons };
113
+ }
114
+
115
+ /**
116
+ * Generate a human-readable match reason string.
117
+ * Phase 3: includes hotspot info.
118
+ * Phase 4: includes observation info.
119
+ */
120
+ export function buildMatchReason(
121
+ vectorScore: number,
122
+ graphScore: number,
123
+ entityName: string | null,
124
+ entityType: string,
125
+ hotspotScore: number = 0,
126
+ observationScore: number = 0
127
+ ): string {
128
+ const parts: string[] = [];
129
+
130
+ if (vectorScore > 0.8) {
131
+ parts.push('high semantic similarity');
132
+ } else if (vectorScore > 0.5) {
133
+ parts.push('semantic match');
134
+ } else {
135
+ parts.push('partial semantic match');
136
+ }
137
+
138
+ if (graphScore > 0.5) {
139
+ parts.push('high graph centrality');
140
+ } else if (graphScore > 0) {
141
+ parts.push('graph neighbor');
142
+ }
143
+
144
+ if (hotspotScore > 0.7) {
145
+ parts.push('change hotspot');
146
+ } else if (hotspotScore > 0.3) {
147
+ parts.push('frequently changed');
148
+ }
149
+
150
+ if (observationScore > 0.5) {
151
+ parts.push('has relevant observations');
152
+ } else if (observationScore > 0) {
153
+ parts.push('has observations');
154
+ }
155
+
156
+ if (entityName) {
157
+ parts.push(`entity: ${entityName} (${entityType})`);
158
+ }
159
+
160
+ return parts.join(', ');
161
+ }