memor-code-cli 0.2.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 (418) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -0
  3. package/dist/IntentionalStructure/index.d.ts +13 -0
  4. package/dist/IntentionalStructure/index.d.ts.map +1 -0
  5. package/dist/IntentionalStructure/index.js +46 -0
  6. package/dist/IntentionalStructure/index.js.map +1 -0
  7. package/dist/IntentionalStructure/parseCompose.d.ts +13 -0
  8. package/dist/IntentionalStructure/parseCompose.d.ts.map +1 -0
  9. package/dist/IntentionalStructure/parseCompose.js +234 -0
  10. package/dist/IntentionalStructure/parseCompose.js.map +1 -0
  11. package/dist/IntentionalStructure/parseDeployConfigs.d.ts +7 -0
  12. package/dist/IntentionalStructure/parseDeployConfigs.d.ts.map +1 -0
  13. package/dist/IntentionalStructure/parseDeployConfigs.js +168 -0
  14. package/dist/IntentionalStructure/parseDeployConfigs.js.map +1 -0
  15. package/dist/IntentionalStructure/parseEnvWiring.d.ts +7 -0
  16. package/dist/IntentionalStructure/parseEnvWiring.d.ts.map +1 -0
  17. package/dist/IntentionalStructure/parseEnvWiring.js +159 -0
  18. package/dist/IntentionalStructure/parseEnvWiring.js.map +1 -0
  19. package/dist/IntentionalStructure/parseWorkspaces.d.ts +8 -0
  20. package/dist/IntentionalStructure/parseWorkspaces.d.ts.map +1 -0
  21. package/dist/IntentionalStructure/parseWorkspaces.js +179 -0
  22. package/dist/IntentionalStructure/parseWorkspaces.js.map +1 -0
  23. package/dist/IntentionalStructure/types.d.ts +34 -0
  24. package/dist/IntentionalStructure/types.d.ts.map +1 -0
  25. package/dist/IntentionalStructure/types.js +8 -0
  26. package/dist/IntentionalStructure/types.js.map +1 -0
  27. package/dist/RepoTypeDetection/applyRepoModeConsistency.d.ts +11 -0
  28. package/dist/RepoTypeDetection/applyRepoModeConsistency.d.ts.map +1 -0
  29. package/dist/RepoTypeDetection/applyRepoModeConsistency.js +53 -0
  30. package/dist/RepoTypeDetection/applyRepoModeConsistency.js.map +1 -0
  31. package/dist/RepoTypeDetection/detectAppArchetype.d.ts +12 -0
  32. package/dist/RepoTypeDetection/detectAppArchetype.d.ts.map +1 -0
  33. package/dist/RepoTypeDetection/detectAppArchetype.js +162 -0
  34. package/dist/RepoTypeDetection/detectAppArchetype.js.map +1 -0
  35. package/dist/RepoTypeDetection/detectPackageArchetype.d.ts +10 -0
  36. package/dist/RepoTypeDetection/detectPackageArchetype.d.ts.map +1 -0
  37. package/dist/RepoTypeDetection/detectPackageArchetype.js +149 -0
  38. package/dist/RepoTypeDetection/detectPackageArchetype.js.map +1 -0
  39. package/dist/RepoTypeDetection/detectRepoCenterSystems.d.ts +11 -0
  40. package/dist/RepoTypeDetection/detectRepoCenterSystems.d.ts.map +1 -0
  41. package/dist/RepoTypeDetection/detectRepoCenterSystems.js +123 -0
  42. package/dist/RepoTypeDetection/detectRepoCenterSystems.js.map +1 -0
  43. package/dist/RepoTypeDetection/detectRepoMode.d.ts +16 -0
  44. package/dist/RepoTypeDetection/detectRepoMode.d.ts.map +1 -0
  45. package/dist/RepoTypeDetection/detectRepoMode.js +338 -0
  46. package/dist/RepoTypeDetection/detectRepoMode.js.map +1 -0
  47. package/dist/RepoTypeDetection/detectRepoSignals.d.ts +13 -0
  48. package/dist/RepoTypeDetection/detectRepoSignals.d.ts.map +1 -0
  49. package/dist/RepoTypeDetection/detectRepoSignals.js +451 -0
  50. package/dist/RepoTypeDetection/detectRepoSignals.js.map +1 -0
  51. package/dist/RepoTypeDetection/inferSupportRole.d.ts +8 -0
  52. package/dist/RepoTypeDetection/inferSupportRole.d.ts.map +1 -0
  53. package/dist/RepoTypeDetection/inferSupportRole.js +177 -0
  54. package/dist/RepoTypeDetection/inferSupportRole.js.map +1 -0
  55. package/dist/RuntimeInference/detectEnvConsumers.d.ts +7 -0
  56. package/dist/RuntimeInference/detectEnvConsumers.d.ts.map +1 -0
  57. package/dist/RuntimeInference/detectEnvConsumers.js +108 -0
  58. package/dist/RuntimeInference/detectEnvConsumers.js.map +1 -0
  59. package/dist/RuntimeInference/detectHttpEdges.d.ts +4 -0
  60. package/dist/RuntimeInference/detectHttpEdges.d.ts.map +1 -0
  61. package/dist/RuntimeInference/detectHttpEdges.js +146 -0
  62. package/dist/RuntimeInference/detectHttpEdges.js.map +1 -0
  63. package/dist/RuntimeInference/detectOrmConsumers.d.ts +5 -0
  64. package/dist/RuntimeInference/detectOrmConsumers.d.ts.map +1 -0
  65. package/dist/RuntimeInference/detectOrmConsumers.js +117 -0
  66. package/dist/RuntimeInference/detectOrmConsumers.js.map +1 -0
  67. package/dist/RuntimeInference/detectProxyRewrites.d.ts +5 -0
  68. package/dist/RuntimeInference/detectProxyRewrites.d.ts.map +1 -0
  69. package/dist/RuntimeInference/detectProxyRewrites.js +137 -0
  70. package/dist/RuntimeInference/detectProxyRewrites.js.map +1 -0
  71. package/dist/RuntimeInference/detectTrpcEdges.d.ts +4 -0
  72. package/dist/RuntimeInference/detectTrpcEdges.d.ts.map +1 -0
  73. package/dist/RuntimeInference/detectTrpcEdges.js +137 -0
  74. package/dist/RuntimeInference/detectTrpcEdges.js.map +1 -0
  75. package/dist/RuntimeInference/index.d.ts +17 -0
  76. package/dist/RuntimeInference/index.d.ts.map +1 -0
  77. package/dist/RuntimeInference/index.js +32 -0
  78. package/dist/RuntimeInference/index.js.map +1 -0
  79. package/dist/RuntimeInference/types.d.ts +8 -0
  80. package/dist/RuntimeInference/types.d.ts.map +1 -0
  81. package/dist/RuntimeInference/types.js +3 -0
  82. package/dist/RuntimeInference/types.js.map +1 -0
  83. package/dist/SystemSynthesis/index.d.ts +27 -0
  84. package/dist/SystemSynthesis/index.d.ts.map +1 -0
  85. package/dist/SystemSynthesis/index.js +31 -0
  86. package/dist/SystemSynthesis/index.js.map +1 -0
  87. package/dist/SystemSynthesis/mergeLayerOutputs.d.ts +13 -0
  88. package/dist/SystemSynthesis/mergeLayerOutputs.d.ts.map +1 -0
  89. package/dist/SystemSynthesis/mergeLayerOutputs.js +220 -0
  90. package/dist/SystemSynthesis/mergeLayerOutputs.js.map +1 -0
  91. package/dist/SystemSynthesis/stampDeterministic.d.ts +11 -0
  92. package/dist/SystemSynthesis/stampDeterministic.d.ts.map +1 -0
  93. package/dist/SystemSynthesis/stampDeterministic.js +177 -0
  94. package/dist/SystemSynthesis/stampDeterministic.js.map +1 -0
  95. package/dist/SystemSynthesis/synthesize.d.ts +15 -0
  96. package/dist/SystemSynthesis/synthesize.d.ts.map +1 -0
  97. package/dist/SystemSynthesis/synthesize.js +258 -0
  98. package/dist/SystemSynthesis/synthesize.js.map +1 -0
  99. package/dist/SystemSynthesis/types.d.ts +110 -0
  100. package/dist/SystemSynthesis/types.d.ts.map +1 -0
  101. package/dist/SystemSynthesis/types.js +12 -0
  102. package/dist/SystemSynthesis/types.js.map +1 -0
  103. package/dist/amGeneration/buildFlowGraph.d.ts +39 -0
  104. package/dist/amGeneration/buildFlowGraph.d.ts.map +1 -0
  105. package/dist/amGeneration/buildFlowGraph.js +643 -0
  106. package/dist/amGeneration/buildFlowGraph.js.map +1 -0
  107. package/dist/amGeneration/enrichAMEdges.d.ts +16 -0
  108. package/dist/amGeneration/enrichAMEdges.d.ts.map +1 -0
  109. package/dist/amGeneration/enrichAMEdges.js +291 -0
  110. package/dist/amGeneration/enrichAMEdges.js.map +1 -0
  111. package/dist/amGeneration/generateAM.d.ts +23 -0
  112. package/dist/amGeneration/generateAM.d.ts.map +1 -0
  113. package/dist/amGeneration/generateAM.js +131 -0
  114. package/dist/amGeneration/generateAM.js.map +1 -0
  115. package/dist/amGeneration/generateAMForRepo.d.ts +6 -0
  116. package/dist/amGeneration/generateAMForRepo.d.ts.map +1 -0
  117. package/dist/amGeneration/generateAMForRepo.js +130 -0
  118. package/dist/amGeneration/generateAMForRepo.js.map +1 -0
  119. package/dist/amGeneration/generateFlows.d.ts +27 -0
  120. package/dist/amGeneration/generateFlows.d.ts.map +1 -0
  121. package/dist/amGeneration/generateFlows.js +320 -0
  122. package/dist/amGeneration/generateFlows.js.map +1 -0
  123. package/dist/amGeneration/promptBuilder.d.ts +24 -0
  124. package/dist/amGeneration/promptBuilder.d.ts.map +1 -0
  125. package/dist/amGeneration/promptBuilder.js +299 -0
  126. package/dist/amGeneration/promptBuilder.js.map +1 -0
  127. package/dist/amGeneration/selectStrategicFiles.d.ts +42 -0
  128. package/dist/amGeneration/selectStrategicFiles.d.ts.map +1 -0
  129. package/dist/amGeneration/selectStrategicFiles.js +672 -0
  130. package/dist/amGeneration/selectStrategicFiles.js.map +1 -0
  131. package/dist/amGeneration/types.d.ts +83 -0
  132. package/dist/amGeneration/types.d.ts.map +1 -0
  133. package/dist/amGeneration/types.js +9 -0
  134. package/dist/amGeneration/types.js.map +1 -0
  135. package/dist/amSections.d.ts +29 -0
  136. package/dist/amSections.d.ts.map +1 -0
  137. package/dist/amSections.js +424 -0
  138. package/dist/amSections.js.map +1 -0
  139. package/dist/analysis/analysisCache.d.ts +4 -0
  140. package/dist/analysis/analysisCache.d.ts.map +1 -0
  141. package/dist/analysis/analysisCache.js +92 -0
  142. package/dist/analysis/analysisCache.js.map +1 -0
  143. package/dist/analysis/buildBranchStory.d.ts +95 -0
  144. package/dist/analysis/buildBranchStory.d.ts.map +1 -0
  145. package/dist/analysis/buildBranchStory.js +1264 -0
  146. package/dist/analysis/buildBranchStory.js.map +1 -0
  147. package/dist/analysis/buildDetailedFileXRay.d.ts +49 -0
  148. package/dist/analysis/buildDetailedFileXRay.d.ts.map +1 -0
  149. package/dist/analysis/buildDetailedFileXRay.js +607 -0
  150. package/dist/analysis/buildDetailedFileXRay.js.map +1 -0
  151. package/dist/analysis/buildFileXRay.d.ts +35 -0
  152. package/dist/analysis/buildFileXRay.d.ts.map +1 -0
  153. package/dist/analysis/buildFileXRay.js +305 -0
  154. package/dist/analysis/buildFileXRay.js.map +1 -0
  155. package/dist/analysis/classifySilentKiller.d.ts +14 -0
  156. package/dist/analysis/classifySilentKiller.d.ts.map +1 -0
  157. package/dist/analysis/classifySilentKiller.js +235 -0
  158. package/dist/analysis/classifySilentKiller.js.map +1 -0
  159. package/dist/analysis/diffChunks.d.ts +21 -0
  160. package/dist/analysis/diffChunks.d.ts.map +1 -0
  161. package/dist/analysis/diffChunks.js +302 -0
  162. package/dist/analysis/diffChunks.js.map +1 -0
  163. package/dist/analysis/extractRouteMap.d.ts +49 -0
  164. package/dist/analysis/extractRouteMap.d.ts.map +1 -0
  165. package/dist/analysis/extractRouteMap.js +354 -0
  166. package/dist/analysis/extractRouteMap.js.map +1 -0
  167. package/dist/analysis/generateFileInsight.d.ts +19 -0
  168. package/dist/analysis/generateFileInsight.d.ts.map +1 -0
  169. package/dist/analysis/generateFileInsight.js +103 -0
  170. package/dist/analysis/generateFileInsight.js.map +1 -0
  171. package/dist/analysis/llmXRay.d.ts +39 -0
  172. package/dist/analysis/llmXRay.d.ts.map +1 -0
  173. package/dist/analysis/llmXRay.js +208 -0
  174. package/dist/analysis/llmXRay.js.map +1 -0
  175. package/dist/analysis/simulateFailure.d.ts +44 -0
  176. package/dist/analysis/simulateFailure.d.ts.map +1 -0
  177. package/dist/analysis/simulateFailure.js +407 -0
  178. package/dist/analysis/simulateFailure.js.map +1 -0
  179. package/dist/anthropic.d.ts +3 -0
  180. package/dist/anthropic.d.ts.map +1 -0
  181. package/dist/anthropic.js +16 -0
  182. package/dist/anthropic.js.map +1 -0
  183. package/dist/app/buildAppPage.d.ts +34 -0
  184. package/dist/app/buildAppPage.d.ts.map +1 -0
  185. package/dist/app/buildAppPage.js +3085 -0
  186. package/dist/app/buildAppPage.js.map +1 -0
  187. package/dist/app-bundle.js +122 -0
  188. package/dist/buildAppData.d.ts +3 -0
  189. package/dist/buildAppData.d.ts.map +1 -0
  190. package/dist/buildAppData.js +207 -0
  191. package/dist/buildAppData.js.map +1 -0
  192. package/dist/builders/analyzeRepo.d.ts +25 -0
  193. package/dist/builders/analyzeRepo.d.ts.map +1 -0
  194. package/dist/builders/analyzeRepo.js +873 -0
  195. package/dist/builders/analyzeRepo.js.map +1 -0
  196. package/dist/builders/buildSystemConnections.d.ts +3 -0
  197. package/dist/builders/buildSystemConnections.d.ts.map +1 -0
  198. package/dist/builders/buildSystemConnections.js +388 -0
  199. package/dist/builders/buildSystemConnections.js.map +1 -0
  200. package/dist/builders/buildTextSummary.d.ts +61 -0
  201. package/dist/builders/buildTextSummary.d.ts.map +1 -0
  202. package/dist/builders/buildTextSummary.js +178 -0
  203. package/dist/builders/buildTextSummary.js.map +1 -0
  204. package/dist/builders/deriveRecommendedStartPath.d.ts +11 -0
  205. package/dist/builders/deriveRecommendedStartPath.d.ts.map +1 -0
  206. package/dist/builders/deriveRecommendedStartPath.js +140 -0
  207. package/dist/builders/deriveRecommendedStartPath.js.map +1 -0
  208. package/dist/builders/deriveRuntimeRole.d.ts +4 -0
  209. package/dist/builders/deriveRuntimeRole.d.ts.map +1 -0
  210. package/dist/builders/deriveRuntimeRole.js +30 -0
  211. package/dist/builders/deriveRuntimeRole.js.map +1 -0
  212. package/dist/builders/detectRunCommands.d.ts +16 -0
  213. package/dist/builders/detectRunCommands.d.ts.map +1 -0
  214. package/dist/builders/detectRunCommands.js +285 -0
  215. package/dist/builders/detectRunCommands.js.map +1 -0
  216. package/dist/builders/generateSystemNarrative.d.ts +12 -0
  217. package/dist/builders/generateSystemNarrative.d.ts.map +1 -0
  218. package/dist/builders/generateSystemNarrative.js +474 -0
  219. package/dist/builders/generateSystemNarrative.js.map +1 -0
  220. package/dist/builders/gitLogParser.d.ts +15 -0
  221. package/dist/builders/gitLogParser.d.ts.map +1 -0
  222. package/dist/builders/gitLogParser.js +116 -0
  223. package/dist/builders/gitLogParser.js.map +1 -0
  224. package/dist/builders/readRepoContext.d.ts +30 -0
  225. package/dist/builders/readRepoContext.d.ts.map +1 -0
  226. package/dist/builders/readRepoContext.js +323 -0
  227. package/dist/builders/readRepoContext.js.map +1 -0
  228. package/dist/builders/readSystemReadme.d.ts +39 -0
  229. package/dist/builders/readSystemReadme.d.ts.map +1 -0
  230. package/dist/builders/readSystemReadme.js +133 -0
  231. package/dist/builders/readSystemReadme.js.map +1 -0
  232. package/dist/builders/systemRanking.d.ts +24 -0
  233. package/dist/builders/systemRanking.d.ts.map +1 -0
  234. package/dist/builders/systemRanking.js +153 -0
  235. package/dist/builders/systemRanking.js.map +1 -0
  236. package/dist/cli.d.ts +3 -0
  237. package/dist/cli.d.ts.map +1 -0
  238. package/dist/cli.js +170 -0
  239. package/dist/cli.js.map +1 -0
  240. package/dist/detectors/applyRunnableConfidenceGate.d.ts +8 -0
  241. package/dist/detectors/applyRunnableConfidenceGate.d.ts.map +1 -0
  242. package/dist/detectors/applyRunnableConfidenceGate.js +166 -0
  243. package/dist/detectors/applyRunnableConfidenceGate.js.map +1 -0
  244. package/dist/detectors/classifySystemType.d.ts +17 -0
  245. package/dist/detectors/classifySystemType.d.ts.map +1 -0
  246. package/dist/detectors/classifySystemType.js +460 -0
  247. package/dist/detectors/classifySystemType.js.map +1 -0
  248. package/dist/detectors/detectAppInternalUnits.d.ts +20 -0
  249. package/dist/detectors/detectAppInternalUnits.d.ts.map +1 -0
  250. package/dist/detectors/detectAppInternalUnits.js +454 -0
  251. package/dist/detectors/detectAppInternalUnits.js.map +1 -0
  252. package/dist/detectors/detectBlocks.d.ts +6 -0
  253. package/dist/detectors/detectBlocks.d.ts.map +1 -0
  254. package/dist/detectors/detectBlocks.js +354 -0
  255. package/dist/detectors/detectBlocks.js.map +1 -0
  256. package/dist/detectors/detectComposeServices.d.ts +23 -0
  257. package/dist/detectors/detectComposeServices.d.ts.map +1 -0
  258. package/dist/detectors/detectComposeServices.js +255 -0
  259. package/dist/detectors/detectComposeServices.js.map +1 -0
  260. package/dist/detectors/detectEntryPoints.d.ts +6 -0
  261. package/dist/detectors/detectEntryPoints.d.ts.map +1 -0
  262. package/dist/detectors/detectEntryPoints.js +376 -0
  263. package/dist/detectors/detectEntryPoints.js.map +1 -0
  264. package/dist/detectors/detectSubsystems.d.ts +6 -0
  265. package/dist/detectors/detectSubsystems.d.ts.map +1 -0
  266. package/dist/detectors/detectSubsystems.js +376 -0
  267. package/dist/detectors/detectSubsystems.js.map +1 -0
  268. package/dist/detectors/detectSystemCandidates.d.ts +3 -0
  269. package/dist/detectors/detectSystemCandidates.d.ts.map +1 -0
  270. package/dist/detectors/detectSystemCandidates.js +577 -0
  271. package/dist/detectors/detectSystemCandidates.js.map +1 -0
  272. package/dist/devWatcher.d.ts +14 -0
  273. package/dist/devWatcher.d.ts.map +1 -0
  274. package/dist/devWatcher.js +96 -0
  275. package/dist/devWatcher.js.map +1 -0
  276. package/dist/graph/buildCodebaseGraph.d.ts +3 -0
  277. package/dist/graph/buildCodebaseGraph.d.ts.map +1 -0
  278. package/dist/graph/buildCodebaseGraph.js +602 -0
  279. package/dist/graph/buildCodebaseGraph.js.map +1 -0
  280. package/dist/graph/buildLLMPayload.d.ts +89 -0
  281. package/dist/graph/buildLLMPayload.d.ts.map +1 -0
  282. package/dist/graph/buildLLMPayload.js +715 -0
  283. package/dist/graph/buildLLMPayload.js.map +1 -0
  284. package/dist/graph/callLLM.d.ts +16 -0
  285. package/dist/graph/callLLM.d.ts.map +1 -0
  286. package/dist/graph/callLLM.js +274 -0
  287. package/dist/graph/callLLM.js.map +1 -0
  288. package/dist/graph/types.d.ts +76 -0
  289. package/dist/graph/types.d.ts.map +1 -0
  290. package/dist/graph/types.js +5 -0
  291. package/dist/graph/types.js.map +1 -0
  292. package/dist/graph/walkExports.d.ts +2 -0
  293. package/dist/graph/walkExports.d.ts.map +1 -0
  294. package/dist/graph/walkExports.js +172 -0
  295. package/dist/graph/walkExports.js.map +1 -0
  296. package/dist/heuristics/file-patterns.json +325 -0
  297. package/dist/heuristics/known-packages.json +1022 -0
  298. package/dist/heuristics/loader.d.ts +121 -0
  299. package/dist/heuristics/loader.d.ts.map +1 -0
  300. package/dist/heuristics/loader.js +196 -0
  301. package/dist/heuristics/loader.js.map +1 -0
  302. package/dist/heuristics/repo-mode-signals.json +248 -0
  303. package/dist/index.d.ts +3 -0
  304. package/dist/index.d.ts.map +1 -0
  305. package/dist/index.js +93 -0
  306. package/dist/index.js.map +1 -0
  307. package/dist/mcp.d.ts +3 -0
  308. package/dist/mcp.d.ts.map +1 -0
  309. package/dist/mcp.js +356 -0
  310. package/dist/mcp.js.map +1 -0
  311. package/dist/scanner/buildImportGraph.d.ts +23 -0
  312. package/dist/scanner/buildImportGraph.d.ts.map +1 -0
  313. package/dist/scanner/buildImportGraph.js +186 -0
  314. package/dist/scanner/buildImportGraph.js.map +1 -0
  315. package/dist/scanner/detectDBOps.d.ts +21 -0
  316. package/dist/scanner/detectDBOps.d.ts.map +1 -0
  317. package/dist/scanner/detectDBOps.js +346 -0
  318. package/dist/scanner/detectDBOps.js.map +1 -0
  319. package/dist/scanner/detectOutbound.d.ts +6 -0
  320. package/dist/scanner/detectOutbound.d.ts.map +1 -0
  321. package/dist/scanner/detectOutbound.js +101 -0
  322. package/dist/scanner/detectOutbound.js.map +1 -0
  323. package/dist/scanner/detectRepoPurpose.d.ts +19 -0
  324. package/dist/scanner/detectRepoPurpose.d.ts.map +1 -0
  325. package/dist/scanner/detectRepoPurpose.js +335 -0
  326. package/dist/scanner/detectRepoPurpose.js.map +1 -0
  327. package/dist/scanner/detectRoutes.d.ts +22 -0
  328. package/dist/scanner/detectRoutes.d.ts.map +1 -0
  329. package/dist/scanner/detectRoutes.js +406 -0
  330. package/dist/scanner/detectRoutes.js.map +1 -0
  331. package/dist/scanner/filterNoise.d.ts +4 -0
  332. package/dist/scanner/filterNoise.d.ts.map +1 -0
  333. package/dist/scanner/filterNoise.js +95 -0
  334. package/dist/scanner/filterNoise.js.map +1 -0
  335. package/dist/scanner/loadTsAliases.d.ts +20 -0
  336. package/dist/scanner/loadTsAliases.d.ts.map +1 -0
  337. package/dist/scanner/loadTsAliases.js +135 -0
  338. package/dist/scanner/loadTsAliases.js.map +1 -0
  339. package/dist/scanner/routeHandlers.d.ts +15 -0
  340. package/dist/scanner/routeHandlers.d.ts.map +1 -0
  341. package/dist/scanner/routeHandlers.js +154 -0
  342. package/dist/scanner/routeHandlers.js.map +1 -0
  343. package/dist/scanner/scanRepo.d.ts +10 -0
  344. package/dist/scanner/scanRepo.d.ts.map +1 -0
  345. package/dist/scanner/scanRepo.js +165 -0
  346. package/dist/scanner/scanRepo.js.map +1 -0
  347. package/dist/scanner/walkImports.d.ts +14 -0
  348. package/dist/scanner/walkImports.d.ts.map +1 -0
  349. package/dist/scanner/walkImports.js +162 -0
  350. package/dist/scanner/walkImports.js.map +1 -0
  351. package/dist/server.d.ts +2 -0
  352. package/dist/server.d.ts.map +1 -0
  353. package/dist/server.js +956 -0
  354. package/dist/server.js.map +1 -0
  355. package/dist/session/diff.d.ts +4 -0
  356. package/dist/session/diff.d.ts.map +1 -0
  357. package/dist/session/diff.js +150 -0
  358. package/dist/session/diff.js.map +1 -0
  359. package/dist/session/snapshot.d.ts +5 -0
  360. package/dist/session/snapshot.d.ts.map +1 -0
  361. package/dist/session/snapshot.js +114 -0
  362. package/dist/session/snapshot.js.map +1 -0
  363. package/dist/session/store.d.ts +8 -0
  364. package/dist/session/store.d.ts.map +1 -0
  365. package/dist/session/store.js +101 -0
  366. package/dist/session/store.js.map +1 -0
  367. package/dist/session/types.d.ts +46 -0
  368. package/dist/session/types.d.ts.map +1 -0
  369. package/dist/session/types.js +4 -0
  370. package/dist/session/types.js.map +1 -0
  371. package/dist/types.d.ts +350 -0
  372. package/dist/types.d.ts.map +1 -0
  373. package/dist/types.js +4 -0
  374. package/dist/types.js.map +1 -0
  375. package/dist/utils/file.d.ts +5 -0
  376. package/dist/utils/file.d.ts.map +1 -0
  377. package/dist/utils/file.js +76 -0
  378. package/dist/utils/file.js.map +1 -0
  379. package/dist/utils/path.d.ts +7 -0
  380. package/dist/utils/path.d.ts.map +1 -0
  381. package/dist/utils/path.js +62 -0
  382. package/dist/utils/path.js.map +1 -0
  383. package/dist/utils/text.d.ts +5 -0
  384. package/dist/utils/text.d.ts.map +1 -0
  385. package/dist/utils/text.js +29 -0
  386. package/dist/utils/text.js.map +1 -0
  387. package/dist/viewBuilders/buildSystemFolders.d.ts +39 -0
  388. package/dist/viewBuilders/buildSystemFolders.d.ts.map +1 -0
  389. package/dist/viewBuilders/buildSystemFolders.js +198 -0
  390. package/dist/viewBuilders/buildSystemFolders.js.map +1 -0
  391. package/dist/watcher/repoWatcher.d.ts +17 -0
  392. package/dist/watcher/repoWatcher.d.ts.map +1 -0
  393. package/dist/watcher/repoWatcher.js +87 -0
  394. package/dist/watcher/repoWatcher.js.map +1 -0
  395. package/package.json +102 -0
  396. package/public/memor_logo.svg +18 -0
  397. package/public/memor_transparent_logo.svg +25 -0
  398. package/public/tIcons/bun.svg +1 -0
  399. package/public/tIcons/css.svg +1 -0
  400. package/public/tIcons/docker.svg +3 -0
  401. package/public/tIcons/expressjs.svg +1 -0
  402. package/public/tIcons/html5.svg +6 -0
  403. package/public/tIcons/javascript.svg +1 -0
  404. package/public/tIcons/jest.svg +4 -0
  405. package/public/tIcons/json.svg +1 -0
  406. package/public/tIcons/markdown-light.svg +1 -0
  407. package/public/tIcons/nestjs.svg +1 -0
  408. package/public/tIcons/nextjs_icon_dark.svg +1 -0
  409. package/public/tIcons/npm.svg +1 -0
  410. package/public/tIcons/pnpm.svg +1 -0
  411. package/public/tIcons/prisma.svg +1 -0
  412. package/public/tIcons/react_dark.svg +11 -0
  413. package/public/tIcons/supabase.svg +15 -0
  414. package/public/tIcons/tailwindcss.svg +1 -0
  415. package/public/tIcons/turborepo-icon-light.svg +1 -0
  416. package/public/tIcons/typescript.svg +1 -0
  417. package/public/tIcons/vite.svg +1 -0
  418. package/public/tIcons/yarn.svg +1 -0
@@ -0,0 +1,1264 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.getFileDiffRaw = getFileDiffRaw;
37
+ exports.getFileAtRef = getFileAtRef;
38
+ exports.resolveMergeBase = resolveMergeBase;
39
+ exports.buildBranchStory = buildBranchStory;
40
+ exports.buildBranchStoryForSource = buildBranchStoryForSource;
41
+ const child_process_1 = require("child_process");
42
+ const path = __importStar(require("path"));
43
+ const fs = __importStar(require("fs"));
44
+ const os = __importStar(require("os"));
45
+ const crypto = __importStar(require("crypto"));
46
+ const anthropic_1 = require("../anthropic");
47
+ const buildFileXRay_1 = require("./buildFileXRay");
48
+ const loadTsAliases_1 = require("../scanner/loadTsAliases");
49
+ const detectRoutes_1 = require("../scanner/detectRoutes");
50
+ const routeHandlers_1 = require("../scanner/routeHandlers");
51
+ const detectOutbound_1 = require("../scanner/detectOutbound");
52
+ const classifySilentKiller_1 = require("./classifySilentKiller");
53
+ const simulateFailure_1 = require("./simulateFailure");
54
+ const diffChunks_1 = require("./diffChunks");
55
+ const CODE_EXTS = [".ts", ".tsx", ".js", ".jsx"];
56
+ function resolveImport(source, importerAbs, repoRoot, aliases) {
57
+ let relative = source;
58
+ // Non-relative: try alias map first
59
+ if (!source.startsWith(".")) {
60
+ const fromDir = path.dirname(importerAbs);
61
+ const aliased = (0, loadTsAliases_1.applyAlias)(source, fromDir, aliases);
62
+ if (!aliased)
63
+ return null;
64
+ relative = aliased;
65
+ }
66
+ const base = path.resolve(path.dirname(importerAbs), relative);
67
+ const candidates = [
68
+ base,
69
+ ...CODE_EXTS.map(e => base + e),
70
+ ...CODE_EXTS.map(e => path.join(base, "index" + e)),
71
+ ];
72
+ for (const c of candidates) {
73
+ if (fs.existsSync(c))
74
+ return path.relative(repoRoot, c).replace(/\\/g, "/");
75
+ }
76
+ return null;
77
+ }
78
+ // Strip em/en dashes from LLM prose (they read as an "AI tell") and tidy the seams.
79
+ // Leaves real hyphens in compound words (human-readable, test-framework-helpers) intact.
80
+ function deDash(s) {
81
+ return s
82
+ .replace(/\s*[—–]\s*/g, ", ") // em/en dash → comma
83
+ .replace(/ +,/g, ",") // no space before comma
84
+ .replace(/,(\s*,)+/g, ",") // collapse repeated commas
85
+ .replace(/,\s*\./g, ".") // ", ." → "."
86
+ .replace(/ {2,}/g, " ") // collapse double spaces
87
+ .trim();
88
+ }
89
+ // ── Entry-point tracing ─────────────────────────────────────────────────────────
90
+ // "When does this changed code run?" Reuses detectRoutes + the import graph: a route's
91
+ // registration file hands off to a changed handler (it imports it); that handler — and
92
+ // everything it pulls in among the changed files — inherits the route as its trigger.
93
+ async function traceEntryPoints(repoRoot, changedFiles, edges, aliases) {
94
+ const result = new Map();
95
+ let routes = [];
96
+ try {
97
+ routes = await (0, detectRoutes_1.detectRoutes)(repoRoot);
98
+ }
99
+ catch {
100
+ return result;
101
+ }
102
+ if (routes.length === 0)
103
+ return result;
104
+ const addEp = (file, ep) => {
105
+ if (!result.has(file))
106
+ result.set(file, []);
107
+ const arr = result.get(file);
108
+ if (!arr.some(e => e.method === ep.method && e.path === ep.path))
109
+ arr.push(ep);
110
+ };
111
+ // Parse each route-registration file once → precise per-route handler specs + mounts.
112
+ const routeFiles = [...new Set(routes.map(r => r.file))];
113
+ const parsedByFile = new Map();
114
+ for (const rf of routeFiles) {
115
+ try {
116
+ parsedByFile.set(rf, (0, routeHandlers_1.parseRouteFile)(fs.readFileSync(path.join(repoRoot, rf), "utf8")));
117
+ }
118
+ catch { /* skip */ }
119
+ }
120
+ // Mount map: sub-router file → { prefix it's mounted at, file it's mounted in }. Lets us
121
+ // chain prefixes (app.use('/v1', router) → router.use('/executions', executionsRouter)).
122
+ const mountMap = new Map();
123
+ for (const [rf, parsed] of parsedByFile) {
124
+ const abs = path.join(repoRoot, rf);
125
+ for (const mnt of parsed.mounts) {
126
+ const sub = resolveImport(mnt.routerSpec, abs, repoRoot, aliases);
127
+ if (sub && !mountMap.has(sub))
128
+ mountMap.set(sub, { prefix: mnt.prefix, mountedIn: rf });
129
+ }
130
+ }
131
+ const composePrefix = (file) => {
132
+ const parts = [];
133
+ let cur = file;
134
+ const seen = new Set();
135
+ while (cur && mountMap.has(cur) && !seen.has(cur)) {
136
+ seen.add(cur);
137
+ const mnt = mountMap.get(cur);
138
+ if (mnt.prefix && mnt.prefix !== "/")
139
+ parts.unshift(mnt.prefix);
140
+ cur = mnt.mountedIn;
141
+ }
142
+ return parts.join("");
143
+ };
144
+ // Seed: map each SPECIFIC route to the SPECIFIC changed file its handler resolves to.
145
+ for (const [rf, parsed] of parsedByFile) {
146
+ const abs = path.join(repoRoot, rf);
147
+ const prefix = composePrefix(rf);
148
+ for (const route of parsed.routes) {
149
+ const fullPath = (prefix + (route.path === "/" ? "" : route.path)) || "/";
150
+ for (const spec of route.handlerSpecs) {
151
+ const handlerFile = resolveImport(spec, abs, repoRoot, aliases);
152
+ if (handlerFile && changedFiles.has(handlerFile)) {
153
+ addEp(handlerFile, { method: route.method, path: fullPath, via: rf });
154
+ }
155
+ }
156
+ }
157
+ }
158
+ if (result.size === 0)
159
+ return result;
160
+ // Propagate down the changed-file import graph: edge {from→to} means `from` imports `to`,
161
+ // so `to` runs within `from`'s trigger and inherits its entry points.
162
+ const adj = new Map();
163
+ for (const e of edges) {
164
+ if (!adj.has(e.from))
165
+ adj.set(e.from, []);
166
+ adj.get(e.from).push(e.to);
167
+ }
168
+ let changed = true, guard = 0;
169
+ while (changed && guard++ < 100) {
170
+ changed = false;
171
+ for (const [from, tos] of adj) {
172
+ const fromEps = result.get(from);
173
+ if (!fromEps)
174
+ continue;
175
+ for (const to of tos) {
176
+ if (!changedFiles.has(to))
177
+ continue;
178
+ for (const ep of fromEps) {
179
+ const before = result.get(to)?.length ?? 0;
180
+ addEp(to, ep);
181
+ if ((result.get(to)?.length ?? 0) !== before)
182
+ changed = true;
183
+ }
184
+ }
185
+ }
186
+ }
187
+ return result;
188
+ }
189
+ function getCommitContext(repoRoot, base) {
190
+ try {
191
+ return (0, child_process_1.execSync)(`git -C "${repoRoot}" log ${base}...HEAD --oneline`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
192
+ }
193
+ catch {
194
+ return "";
195
+ }
196
+ }
197
+ // When the file was last changed: working-tree edit time (mtime), else the last commit that
198
+ // touched it on the source ref. Returns an ISO string, or undefined if unknown.
199
+ function getLastChanged(repoRoot, file, srcRef, isWorking) {
200
+ if (isWorking) {
201
+ try {
202
+ return new Date(fs.statSync(path.join(repoRoot, file)).mtime).toISOString();
203
+ }
204
+ catch { /* not on disk */ }
205
+ }
206
+ try {
207
+ const out = (0, child_process_1.execSync)(`git -C "${repoRoot}" log -1 --format=%aI ${srcRef || "HEAD"} -- "${file}"`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
208
+ if (out)
209
+ return out;
210
+ }
211
+ catch { /* no history */ }
212
+ return undefined;
213
+ }
214
+ function getLineStats(repoRoot, file, base, srcRef = "") {
215
+ try {
216
+ const out = (0, child_process_1.execSync)(`git -C "${repoRoot}" diff ${base} ${srcRef} --numstat -- "${file}"`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
217
+ if (out) {
218
+ const p = out.split("\t");
219
+ return { added: parseInt(p[0]) || 0, removed: parseInt(p[1]) || 0 };
220
+ }
221
+ }
222
+ catch { /* fall through to untracked */ }
223
+ // Untracked/new file: count its lines as additions via --no-index (exits 1 → read stdout).
224
+ try {
225
+ (0, child_process_1.execSync)(`git -C "${repoRoot}" diff --no-index --numstat -- /dev/null "${file}"`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
226
+ }
227
+ catch (e) {
228
+ const out = String(e?.stdout ?? "").trim();
229
+ if (out) {
230
+ const p = out.split("\t");
231
+ return { added: parseInt(p[0]) || 0, removed: parseInt(p[1]) || 0 };
232
+ }
233
+ }
234
+ return { added: 0, removed: 0 };
235
+ }
236
+ function getFileDiffRaw(repoRoot, file, base, srcRef = "") {
237
+ try {
238
+ const out = (0, child_process_1.execSync)(`git -C "${repoRoot}" diff ${base} ${srcRef} -- "${file}"`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], maxBuffer: 500_000 }).trim();
239
+ if (out)
240
+ return out;
241
+ }
242
+ catch { /* fall through to untracked */ }
243
+ // Untracked/new file: render full content as an additions-only diff.
244
+ try {
245
+ (0, child_process_1.execSync)(`git -C "${repoRoot}" diff --no-index --no-color -- /dev/null "${file}"`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], maxBuffer: 500_000 });
246
+ }
247
+ catch (e) {
248
+ return String(e?.stdout ?? "").trim();
249
+ }
250
+ return "";
251
+ }
252
+ // Read a file's content at a specific git ref (for before/after parsing). null if absent.
253
+ function getFileAtRef(repoRoot, ref, file) {
254
+ try {
255
+ return (0, child_process_1.execSync)(`git -C "${repoRoot}" show ${ref}:"${file}"`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], maxBuffer: 2_000_000 });
256
+ }
257
+ catch {
258
+ return null;
259
+ }
260
+ }
261
+ function findBlastRadius(repoRoot, changedFiles, aliases) {
262
+ // Collect all TS/JS files outside node_modules
263
+ let allFiles = [];
264
+ try {
265
+ const out = (0, child_process_1.execSync)(`find "${repoRoot}" -type f \\( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \\) ! -path "*/node_modules/*" ! -path "*/.memor/*" ! -path "*/dist/*"`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], maxBuffer: 5_000_000 }).trim();
266
+ allFiles = out.split("\n").filter(Boolean);
267
+ }
268
+ catch {
269
+ return [];
270
+ }
271
+ // Build set of changed file canonical paths (no extension, for fuzzy match)
272
+ const changedSet = new Set();
273
+ for (const f of changedFiles.keys()) {
274
+ changedSet.add(path.join(repoRoot, f));
275
+ }
276
+ const affected = new Map();
277
+ for (const absPath of allFiles) {
278
+ const rel = path.relative(repoRoot, absPath).replace(/\\/g, "/");
279
+ if (changedFiles.has(rel))
280
+ continue; // skip files already in the PR
281
+ if (rel.includes("node_modules") || rel.includes(".memor") || rel.includes("/dist/"))
282
+ continue;
283
+ try {
284
+ const xray = (0, buildFileXRay_1.buildFileXRay)(absPath, repoRoot);
285
+ for (const imp of xray.imports) {
286
+ if (imp.isType)
287
+ continue;
288
+ const resolved = resolveImport(imp.source, absPath, repoRoot, aliases);
289
+ if (!resolved)
290
+ continue;
291
+ const absResolved = path.join(repoRoot, resolved);
292
+ if (changedSet.has(absResolved)) {
293
+ if (!affected.has(rel))
294
+ affected.set(rel, []);
295
+ if (!affected.get(rel).includes(resolved))
296
+ affected.get(rel).push(resolved);
297
+ }
298
+ }
299
+ }
300
+ catch { /* malformed file — skip */ }
301
+ }
302
+ return [...affected.entries()].map(([file, importedFiles]) => ({
303
+ file,
304
+ importedFiles,
305
+ category: classifyBlastFile(file),
306
+ }));
307
+ }
308
+ function classifyBlastFile(file) {
309
+ const lower = file.toLowerCase();
310
+ // HTTP handler: route/controller files or files that export HTTP-named functions
311
+ if (/\/(routes?|controllers?|handlers?|endpoints?)\//.test(lower) ||
312
+ /\.(route|controller|handler)\.(ts|js|tsx|jsx)$/.test(lower))
313
+ return "handler";
314
+ // Service/persistence: data access layer
315
+ if (/\/(services?|persistence|repositories?|repos?|dao|store)\//.test(lower) ||
316
+ /\.(service|persistence|repository|repo)\.(ts|js|tsx|jsx)$/.test(lower))
317
+ return "service";
318
+ return "transitive";
319
+ }
320
+ async function generatePRSummary(nodes, apiKey) {
321
+ const client = (0, anthropic_1.createAnthropicClient)(apiKey);
322
+ const summaries = nodes
323
+ .filter(n => n.status !== "context" && n.summary)
324
+ .map(n => `• ${n.file}: ${n.summary}`)
325
+ .join("\n");
326
+ if (!summaries)
327
+ return "";
328
+ try {
329
+ const msg = await client.messages.create({
330
+ model: "claude-haiku-4-5-20251001",
331
+ max_tokens: 50,
332
+ messages: [{ role: "user", content: `PR file changes:\n${summaries}\n\nIn ONE sentence (max 12 words), what does this PR add or fix?\nRespond with ONLY the sentence.` }],
333
+ });
334
+ return msg.content[0].type === "text" ? msg.content[0].text.trim() : "";
335
+ }
336
+ catch {
337
+ return "";
338
+ }
339
+ }
340
+ async function generateFileSummaries(repoRoot, base, changedFiles, apiKey, nodes, blastRadius, am, entryPoints, outboundByFile, srcRef = "") {
341
+ const client = (0, anthropic_1.createAnthropicClient)(apiKey);
342
+ const commitContext = getCommitContext(repoRoot, base);
343
+ const results = new Map();
344
+ // Persistent cache: hash(file + diff) → { role, summary }. Same diff → same brief,
345
+ // regenerated only when the diff changes (deterministic-by-memoization).
346
+ const cachePath = path.join(repoRoot, ".memor", "summary-cache.json");
347
+ let briefCache = {};
348
+ try {
349
+ const raw = JSON.parse(fs.readFileSync(cachePath, "utf8"));
350
+ // Only keep entries in the new {role,summary} shape; ignore legacy string entries.
351
+ for (const [k, v] of Object.entries(raw))
352
+ if (v && typeof v === "object" && "summary" in v)
353
+ briefCache[k] = v;
354
+ }
355
+ catch { /* no cache yet */ }
356
+ let cacheDirty = false;
357
+ // Bump when the brief prompt below changes, so stale cached briefs are regenerated.
358
+ const BRIEF_PROMPT_VERSION = "v12-hunks";
359
+ // Entry points + outbound destinations are part of the prompt context → belong in the cache key.
360
+ const epSig = (file) => (entryPoints.get(file) ?? []).map(e => `${e.method} ${e.path}`).join(",");
361
+ const obSig = (file) => (outboundByFile.get(file) ?? []).map(o => `${o.method} ${o.path}`).join(",");
362
+ const cacheKey = (file, diff) => crypto.createHash("sha1").update(`${BRIEF_PROMPT_VERSION}\n${epSig(file)}\n${obSig(file)}\n${file}\n${diff}`).digest("hex");
363
+ // Grounded context: which system it's in + who depends on it.
364
+ const buildContext = (file) => {
365
+ const lines = [];
366
+ const node = nodes.find(n => n.file === file);
367
+ const sys = node?.systemId ? (am?.systems ?? []).find((s) => s.id === node.systemId) : undefined;
368
+ if (sys)
369
+ lines.push(`System: ${sys.name ?? sys.id}${sys.description ? ` — ${String(sys.description).slice(0, 160)}` : ""}`);
370
+ const surface = node ? (node.exports?.length ? node.exports : node.functions) ?? [] : [];
371
+ if (surface.length > 0)
372
+ lines.push(`Exports/functions: ${surface.slice(0, 10).join(", ")}`);
373
+ const eps = entryPoints.get(file);
374
+ if (eps && eps.length)
375
+ lines.push(`Reached from (entry point): ${eps.slice(0, 3).map(e => `${e.method} ${e.path}`).join(", ")} — this code runs when those endpoint(s) are called. Use this to say WHEN the change takes effect.`);
376
+ const obs = outboundByFile.get(file);
377
+ if (obs && obs.length)
378
+ lines.push(`Sends to (outbound HTTP call in this file): ${obs.slice(0, 4).map(o => `${o.method} ${o.path}`.trim()).join(", ")} — this file makes those calls. Use this to say WHERE the data is sent.`);
379
+ return lines.join("\n");
380
+ };
381
+ await Promise.all([...changedFiles.entries()]
382
+ .filter(([, status]) => status !== "deleted")
383
+ .map(async ([file, status]) => {
384
+ const diff = getFileDiffRaw(repoRoot, file, base, srcRef);
385
+ if (!diff && status === "modified")
386
+ return;
387
+ const key = cacheKey(file, diff ?? `__new__:${status}`);
388
+ if (briefCache[key]) {
389
+ const cached = briefCache[key];
390
+ if (!cached.changePoints?.length && cached.summary && diff) {
391
+ cached.changePoints = (0, diffChunks_1.resolveChangePoints)(cached.summary.split("\n").filter(Boolean), diff);
392
+ }
393
+ results.set(file, cached);
394
+ return;
395
+ }
396
+ const diffLines = diff ? diff.split("\n") : [];
397
+ const linesAdded = diffLines.filter(l => l.startsWith("+") && !l.startsWith("+++")).length;
398
+ const linesRemoved = diffLines.filter(l => l.startsWith("-") && !l.startsWith("---")).length;
399
+ const fileContext = buildContext(file);
400
+ const diffHunks = diff ? (0, diffChunks_1.parseDiffHunks)(diff) : [];
401
+ const hunksBlock = diff ? (0, diffChunks_1.formatHunksForPrompt)(diffHunks) : "(new file — no diff available)";
402
+ const prompt = `You are helping a developer understand a code change they did not write.
403
+
404
+ Changed file: ${file} (${status}) — ${linesAdded > 0 || linesRemoved > 0 ? `+${linesAdded}/-${linesRemoved} lines` : "new file"}
405
+ ${fileContext ? `\n<context>\n${fileContext}\n</context>` : ""}
406
+ <diff_hunks>
407
+ ${hunksBlock}
408
+ </diff_hunks>
409
+
410
+ Output EXACTLY this format and nothing else:
411
+ ROLE: <what this file does in the codebase — one terse line, derived from its exports/functions, NOT from the change>
412
+ CHANGE:
413
+ - <one concrete point about what the change does> [hunk:N]
414
+ - <another point, if any> [hunk:N]
415
+
416
+ Rules for the CHANGE bullets:
417
+ - ONE bullet for a simple/small change; up to 4 for a complex one. Do not pad — fewer, denser bullets beat many thin ones.
418
+ - Each bullet MUST end with [hunk:N] where N is the Hunk number from <diff_hunks> that best supports that bullet. Use a DIFFERENT hunk when bullets describe different parts of the change. If only Hunk 0 exists, use [hunk:0].
419
+ - Each bullet is a concrete fact in plain language: what it adds / reads / writes / calls, the trigger (when it runs), or for replaced code the before → after (the "-" lines ARE the old behaviour).
420
+ - For a brand-new file there is no "before" — describe only what it adds, never invent a prior state.
421
+ - If the diff has comments that state intent, USE them. Avoid bare jargon like "synchronization"/"persistence" unless you say what it concretely means.
422
+ - If the change is config/deps/test-only with no product-behaviour effect, ONE bullet saying exactly that.
423
+
424
+ Ground every claim in <diff_hunks> and <context>. Never invent. Plain words, no markdown headers.`;
425
+ try {
426
+ const msg = await client.messages.create({
427
+ model: "claude-haiku-4-5-20251001",
428
+ max_tokens: 400,
429
+ temperature: 0,
430
+ system: "You explain an unfamiliar code change to a developer in plain language. ROLE = what the file is; CHANGE = what the change does and why it matters (its effect/behaviour), grounded only in the numbered diff hunks and context. Each CHANGE bullet MUST end with [hunk:N] citing the hunk it comes from. When code is replaced or removed, explain the before → after delta — what the previous behaviour was and how it changed — not just the new lines; but never invent a 'before' for a brand-new file. Avoid jargon like 'synchronization'/'persistence' unless you say what it concretely means. Be honest: if a change is scaffolding (config, deps, CI, tests) with no product-behaviour effect, say so. Never use em-dashes or en-dashes (— or –); use commas, periods, or parentheses. Output only the ROLE: and CHANGE: lines. No markdown, no backticks.",
431
+ messages: [{ role: "user", content: prompt }],
432
+ });
433
+ const text = msg.content[0].type === "text" ? msg.content[0].text.trim() : "";
434
+ if (text) {
435
+ const lines = text.split("\n").map(l => l.trim());
436
+ const roleLine = (lines.find(l => /^role:/i.test(l)) ?? "").replace(/^role:\s*/i, "").trim();
437
+ // CHANGE is now a block of bullets after the "CHANGE:" marker.
438
+ const changeIdx = lines.findIndex(l => /^change:/i.test(l));
439
+ const bullets = [];
440
+ if (changeIdx >= 0) {
441
+ const inline = lines[changeIdx].replace(/^change:\s*/i, "").replace(/^[-•*]\s*/, "").trim();
442
+ if (inline)
443
+ bullets.push(inline);
444
+ for (let i = changeIdx + 1; i < lines.length; i++) {
445
+ const l = lines[i];
446
+ if (!l || /^role:/i.test(l)) {
447
+ if (/^role:/i.test(l))
448
+ break;
449
+ else
450
+ continue;
451
+ }
452
+ bullets.push(l.replace(/^[-•*]\s*/, "").trim());
453
+ }
454
+ }
455
+ const cleaned = bullets.map(b => b.trim()).filter(Boolean);
456
+ const changePoints = (0, diffChunks_1.resolveChangePoints)(cleaned, diff ?? "");
457
+ const summaryText = changePoints.map(p => p.text).join("\n");
458
+ const brief = {
459
+ role: roleLine,
460
+ summary: summaryText || text,
461
+ changePoints: changePoints.length ? changePoints : undefined,
462
+ };
463
+ results.set(file, brief);
464
+ briefCache[key] = brief;
465
+ cacheDirty = true;
466
+ }
467
+ }
468
+ catch { /* non-fatal */ }
469
+ }));
470
+ // Persist briefs so they stay stable across runs.
471
+ if (cacheDirty) {
472
+ try {
473
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
474
+ fs.writeFileSync(cachePath, JSON.stringify(briefCache, null, 2));
475
+ }
476
+ catch { /* best-effort */ }
477
+ }
478
+ return results;
479
+ }
480
+ // ── Thread clustering — group the changeset into distinct threads of work ──────
481
+ // Deterministic inputs (briefs, import adjacency, systems, churn) → one LLM pass
482
+ // that names, classifies, narrates and ranks the threads. Cached per changeset.
483
+ async function generateChangeThreads(repoRoot, nodes, edges, apiKey, entryPoints, repoOutbound) {
484
+ const changed = nodes.filter(n => n.status !== "context");
485
+ if (changed.length === 0)
486
+ return [];
487
+ if (changed.length === 1) {
488
+ const f = changed[0];
489
+ return [{ id: "t0", title: f.file.split("/").pop() ?? f.file, type: "other", summary: f.summary ?? f.role ?? "", files: [f.file] }];
490
+ }
491
+ // Import adjacency among changed files only
492
+ const changedSet = new Set(changed.map(n => n.file));
493
+ const importsWithin = new Map();
494
+ for (const e of edges) {
495
+ if (changedSet.has(e.from) && changedSet.has(e.to)) {
496
+ if (!importsWithin.has(e.from))
497
+ importsWithin.set(e.from, []);
498
+ importsWithin.get(e.from).push(e.to.split("/").pop() ?? e.to);
499
+ }
500
+ }
501
+ // Compact, grounded representation of the changeset
502
+ const fileLines = changed.map(n => {
503
+ const churn = (n.linesAdded ?? 0) + (n.linesRemoved ?? 0);
504
+ const imp = importsWithin.get(n.file);
505
+ const eps = entryPoints.get(n.file);
506
+ return [
507
+ `${n.file} [${n.status}${churn ? `, ±${churn}` : ""}${n.systemId ? `, sys:${n.systemId}` : ""}]`,
508
+ n.role ? ` role: ${n.role}` : "",
509
+ n.summary ? ` change: ${(n.summary).split("\n").join("; ")}` : "",
510
+ imp && imp.length ? ` imports-in-change: ${imp.join(", ")}` : "",
511
+ eps && eps.length ? ` entry-point: ${eps.slice(0, 3).map(e => `${e.method} ${e.path}`).join(", ")}` : "",
512
+ ].filter(Boolean).join("\n");
513
+ }).join("\n\n");
514
+ // Cache by the shape of the changeset (paths + statuses + summaries). Bump the
515
+ // version token when the prompt below changes, to invalidate stale changesets.
516
+ const THREADS_PROMPT_VERSION = "v7-nodash";
517
+ const cachePath = path.join(repoRoot, ".memor", "threads-cache.json");
518
+ const sig = crypto.createHash("sha1").update(THREADS_PROMPT_VERSION + "|" + changed.map(n => `${n.file}:${n.status}:${n.summary ?? ""}`).sort().join("|")).digest("hex");
519
+ try {
520
+ const cached = JSON.parse(fs.readFileSync(cachePath, "utf8"));
521
+ if (cached.sig === sig && Array.isArray(cached.threads))
522
+ return cached.threads;
523
+ }
524
+ catch { /* no cache */ }
525
+ const prompt = `Document the changes in a WORKING DIRECTORY — uncommitted, mid-flight. A working directory often holds SEVERAL UNRELATED CHANGESETS at once (it is NOT one clean PR). A "changeset" = a group of files whose CHANGES serve one intent.
526
+
527
+ The goal is COMPREHENSION, not bookkeeping. Do two things:
528
+
529
+ 1) GROUP the files into changesets by the INTENT OF THEIR CHANGES — what THIS diff is building or doing. CRITICAL: group by what each file's CHANGE does, NOT by what the file permanently is.
530
+ - Files whose changes COLLABORATE toward one goal belong together even if they don't import each other (e.g. a new backend endpoint + the frontend change that calls it, even with no static import between them).
531
+ - BUT a file whose ACTUAL CHANGE is trivial or unrelated — a stray comment, a formatting tweak, a config bump — must NOT be pulled into a feature changeset just because the file normally supports that feature. Place it by what its CHANGE does (its own changeset, refactor, housekeeping, or scratch), even if the file is part of the feature's machinery.
532
+
533
+ 2) For each changeset, write a "summary" as an ARRAY of short BULLETS. Do NOT concatenate raw file changes. DECLARATIVE, IMPERSONAL voice — describe the change itself, like API documentation. NEVER write "The developer", "You", "They", "is building", or narrate a person. NEVER use the word "thread" — say "change" or "changeset".
534
+ - First bullet = the INTENT as a BEHAVIOURAL statement: what the system can now DO that it could not before, in plain language (e.g. "When an execution is posted, the server now updates the test catalog if the test's path or name changed — so renamed tests stay current without a manual edit"). State the EFFECT, not the mechanism; avoid bare jargon like "synchronization"/"persistence".
535
+ - Each following bullet = ONE file's role in the change, as "<file> <verb> <what>" (e.g. "server.ts exposes /api/commit-log", "App.tsx renders the change view"). Only include a file's bullet if its change is substantive — do NOT invent a role for a trivial change.
536
+ 2–4 bullets total. Terse, concrete, naming the moving parts.
537
+
538
+ Changeset types: feature (new capability) · fix (bug fix) · refactor (restructure, no behaviour change) · housekeeping (config, .gitignore, deps, generated/cache files, formatting, AND real committed unit/integration test suites — supporting work with no product-behaviour change) · scratch (only genuinely throwaway/demo/experimental files that look disposable — NEVER real committed tests).
539
+
540
+ ${repoOutbound.length ? `OUTBOUND — this repo sends data over HTTP to: ${repoOutbound.map(o => `${o.method} ${o.path}`.trim()).join(", ")}. If the changed code's output clearly flows to one of these, ANCHOR the intent with WHERE it goes (e.g. "registers test executions to the server at POST /v1/executions/batch"). Never claim a destination you cannot connect to the change.\n\n` : ""}Changed files (path, status, system, role = what the file does, change = what changed, imports-in-change = which other changed files it statically imports, entry-point = the HTTP route/trigger this code is reached from — use it to state WHEN the change runs, e.g. "when an execution is posted"):
541
+ ${fileLines}
542
+
543
+ Rules:
544
+ - EVERY file in exactly one changeset.
545
+ - Group by the CHANGE, not the file's permanent role. A trivial change to a feature's file is its OWN small changeset, not part of the feature.
546
+ - The TITLE names the INTENT (e.g. "Email verification on signup", "Rate limiting for the public API"), not the changes, and NEVER contains the word "thread".
547
+ - Rank changesets by importance: the main body of real source work FIRST, housekeeping/scratch LAST.
548
+ - HONESTY: for housekeeping/config/CI/deps changesets, the FIRST bullet must plainly state there is NO product behaviour change (e.g. "Test/build scaffolding only — no change to product behaviour"). Never imply behaviour where there is none. If a change's intent is genuinely unclear from the diff, say what it touches rather than inventing a purpose.
549
+
550
+ Output JSON only, no prose:
551
+ {"changesets":[{"title":"<the intent, short>","type":"feature|fix|refactor|housekeeping|scratch","summary":["<intent bullet>","<file role bullet>","<file role bullet>"],"files":["<path>", ...]}]}`;
552
+ try {
553
+ const client = (0, anthropic_1.createAnthropicClient)(apiKey);
554
+ const msg = await client.messages.create({
555
+ model: "claude-haiku-4-5-20251001",
556
+ max_tokens: 1400,
557
+ temperature: 0,
558
+ system: "You document a working directory's changes for an engineer. Group files into CHANGESETS by the intent of their CHANGES, not by what each file permanently is — a trivial change to a feature's file is its own small changeset, not part of the feature. Synthesize each changeset as terse declarative bullets. Never list raw per-file changes, never narrate a person ('the developer', 'you'), and never use the word 'thread' (say 'change'/'changeset'). Never use em-dashes or en-dashes (— or –); use commas, periods, or parentheses. Output ONLY valid JSON matching the requested schema, no markdown, no commentary.",
559
+ messages: [{ role: "user", content: prompt }],
560
+ });
561
+ const raw = msg.content[0].type === "text" ? msg.content[0].text.trim() : "{}";
562
+ const clean = raw.replace(/^```json?\s*/i, "").replace(/```\s*$/, "").trim();
563
+ const parsed = JSON.parse(clean);
564
+ const rawSets = parsed.changesets ?? parsed.threads ?? [];
565
+ const threads = rawSets.map((t, i) => ({
566
+ id: `t${i}`,
567
+ title: t.title || "Untitled",
568
+ type: (["feature", "fix", "refactor", "housekeeping", "scratch", "other"].includes(t.type ?? "") ? t.type : "other"),
569
+ // summary is an array of bullets → store as \n-joined lines (frontend splits on \n)
570
+ summary: Array.isArray(t.summary)
571
+ ? t.summary.map(s => String(s).trim().replace(/^[-•*]\s*/, "")).filter(Boolean).join("\n")
572
+ : String(t.summary ?? ""),
573
+ files: (t.files ?? []).filter(f => changedSet.has(f)),
574
+ }));
575
+ // Safety net: every changed file must appear somewhere.
576
+ const placed = new Set(threads.flatMap(t => t.files));
577
+ const orphans = changed.map(n => n.file).filter(f => !placed.has(f));
578
+ if (orphans.length > 0)
579
+ threads.push({ id: `t${threads.length}`, title: "Other changes", type: "other", summary: "", files: orphans });
580
+ try {
581
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
582
+ fs.writeFileSync(cachePath, JSON.stringify({ sig, threads }, null, 2));
583
+ }
584
+ catch { /* best-effort */ }
585
+ return threads.filter(t => t.files.length > 0);
586
+ }
587
+ catch {
588
+ // Fallback: one thread per file, ungrouped.
589
+ return changed.map((n, i) => ({ id: `t${i}`, title: n.file.split("/").pop() ?? n.file, type: "other", summary: n.summary ?? "", files: [n.file] }));
590
+ }
591
+ }
592
+ function parseNameStatus(raw) {
593
+ const result = new Map();
594
+ for (const line of raw.trim().split("\n")) {
595
+ if (!line.trim())
596
+ continue;
597
+ const parts = line.split("\t");
598
+ const s = parts[0][0];
599
+ const file = parts[parts.length - 1];
600
+ result.set(file, s === "A" ? "added" : s === "D" ? "deleted" : "modified");
601
+ }
602
+ return result;
603
+ }
604
+ function gitDiff(repoRoot, ref, srcRef = "") {
605
+ try {
606
+ const out = (0, child_process_1.execSync)(`git -C "${repoRoot}" diff ${ref} ${srcRef} --name-status`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
607
+ const m = parseNameStatus(out);
608
+ return m.size > 0 ? m : null;
609
+ }
610
+ catch {
611
+ return null;
612
+ }
613
+ }
614
+ // Resolve a base ref to the MERGE-BASE with HEAD — the point the branch diverged.
615
+ // Diffing against this (not the base's tip) shows only the branch's OWN work, the
616
+ // way a PR diff does, instead of treating the base's later commits as "reversions"
617
+ // when the branch is behind. Returns null if it can't be resolved.
618
+ // Resolve a source branch name to a real ref. Prefers the remote-tracking ref
619
+ // (origin/<name>) over the local branch — the local branch may be stale or point to
620
+ // a different commit than what was pushed (e.g. after a force-push or rebase).
621
+ // This matches what the forge (GitHub/GitLab) shows for the same branch.
622
+ // HEAD / explicit SHAs / already-prefixed refs are left as-is.
623
+ function resolveRef(repoRoot, name) {
624
+ if (/^(HEAD|origin\/)/.test(name) || /[~^]/.test(name) || /^[0-9a-f]{7,40}$/.test(name))
625
+ return name;
626
+ for (const cand of [`origin/${name}`, name]) {
627
+ try {
628
+ (0, child_process_1.execSync)(`git -C "${repoRoot}" rev-parse --verify ${cand}`, { stdio: "pipe" });
629
+ return cand;
630
+ }
631
+ catch { }
632
+ }
633
+ return name;
634
+ }
635
+ // For the comparison TARGET, PREFER the remote ref (origin/<name>) over a possibly-stale
636
+ // local branch — so "vs main" means the real integration state (matches the forge's PR view,
637
+ // immune to a stale local main). HEAD / HEAD~N / SHAs / explicit origin refs are left as-is.
638
+ function resolveTargetRef(repoRoot, name) {
639
+ if (/^(HEAD|origin\/)/.test(name) || /[~^]/.test(name) || /^[0-9a-f]{7,40}$/.test(name))
640
+ return name;
641
+ for (const cand of [`origin/${name}`, name]) {
642
+ try {
643
+ (0, child_process_1.execSync)(`git -C "${repoRoot}" rev-parse --verify ${cand}`, { stdio: "pipe" });
644
+ return cand;
645
+ }
646
+ catch { }
647
+ }
648
+ return name;
649
+ }
650
+ // Merge-base of two arbitrary refs (for Source=branch vs Target=branch, no HEAD assumption).
651
+ function mergeBaseOf(repoRoot, a, b) {
652
+ try {
653
+ const mb = (0, child_process_1.execSync)(`git -C "${repoRoot}" merge-base ${a} ${b}`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
654
+ return mb || null;
655
+ }
656
+ catch {
657
+ return null;
658
+ }
659
+ }
660
+ function resolveMergeBase(repoRoot, ref) {
661
+ try {
662
+ const mb = (0, child_process_1.execSync)(`git -C "${repoRoot}" merge-base ${ref} HEAD`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
663
+ return mb || null;
664
+ }
665
+ catch {
666
+ return null;
667
+ }
668
+ }
669
+ // How far the SOURCE (tip) is ahead of / behind the target — surfaces staleness AND
670
+ // tells you a branch is "even with main" (0 ahead) so an empty diff reads as informative,
671
+ // not broken. `tip` is HEAD for the working tree, or the source branch otherwise.
672
+ function getBranchState(repoRoot, ref, tip = "HEAD") {
673
+ try {
674
+ const out = (0, child_process_1.execSync)(`git -C "${repoRoot}" rev-list --left-right --count ${ref}...${tip}`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
675
+ const [behind, ahead] = out.split(/\s+/).map(n => parseInt(n, 10)); // left=target-only (behind), right=tip-only (ahead)
676
+ if (Number.isNaN(ahead) || Number.isNaN(behind))
677
+ return null;
678
+ return { ahead, behind };
679
+ }
680
+ catch {
681
+ return null;
682
+ }
683
+ }
684
+ // Untracked files (respecting .gitignore) — so new, not-yet-added files still appear.
685
+ function getUntrackedFiles(repoRoot) {
686
+ try {
687
+ const out = (0, child_process_1.execSync)(`git -C "${repoRoot}" ls-files --others --exclude-standard`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
688
+ return out ? out.split("\n").map(s => s.trim()).filter(Boolean) : [];
689
+ }
690
+ catch {
691
+ return [];
692
+ }
693
+ }
694
+ function mapFileToSystem(relFile, am) {
695
+ if (!am?.systems)
696
+ return undefined;
697
+ // Strategy 1: exact key_file match
698
+ for (const sys of am.systems) {
699
+ for (const kf of (sys.key_files ?? [])) {
700
+ if (relFile === kf)
701
+ return sys.id;
702
+ }
703
+ }
704
+ // Strategy 2: file lives inside a deep (depth≥2) key_file directory
705
+ for (const sys of am.systems) {
706
+ for (const kf of (sys.key_files ?? [])) {
707
+ const kfDir = kf.replace(/\/[^/]+$/, "");
708
+ const depth = kfDir.split("/").filter(Boolean).length;
709
+ if (depth >= 2 && relFile.startsWith(kfDir + "/"))
710
+ return sys.id;
711
+ }
712
+ }
713
+ // Strategy 3: path-segment token overlap with system id
714
+ const skip = new Set(["app", "api", "src", "pages", "route", "index", "lib", "utils", "helpers"]);
715
+ const fileSegs = relFile.replace(/\.(ts|tsx|js|jsx)$/, "").split("/")
716
+ .filter(s => s && !skip.has(s) && !s.startsWith("[") && !s.startsWith("("));
717
+ let bestSys;
718
+ let bestScore = 0;
719
+ for (const sys of am.systems) {
720
+ const sysTokens = sys.id.split("-").filter((t) => t.length > 2);
721
+ let score = 0;
722
+ for (const t of sysTokens) {
723
+ if (fileSegs.some(s => s.toLowerCase().includes(t) || t.includes(s.toLowerCase())))
724
+ score++;
725
+ }
726
+ if (score > 0 && score >= sysTokens.length - 1 && score > bestScore) {
727
+ bestScore = score;
728
+ bestSys = sys.id;
729
+ }
730
+ }
731
+ if (bestSys)
732
+ return bestSys;
733
+ // Strategy 4: nearest ancestor directory that contains any key_file of a system.
734
+ // Build a map: directory → system id (deepest key_file dir wins for specificity).
735
+ const dirToSys = new Map();
736
+ for (const sys of am.systems) {
737
+ for (const kf of (sys.key_files ?? [])) {
738
+ const dir = kf.includes("/") ? kf.replace(/\/[^/]+$/, "") : "";
739
+ if (!dir)
740
+ continue;
741
+ const depth = dir.split("/").filter(Boolean).length;
742
+ const existing = dirToSys.get(dir);
743
+ if (!existing || depth > existing.depth) {
744
+ dirToSys.set(dir, { sysId: sys.id, depth });
745
+ }
746
+ }
747
+ }
748
+ // Walk up from the file's directory toward root, first match wins
749
+ let dir = relFile.includes("/") ? relFile.replace(/\/[^/]+$/, "") : "";
750
+ while (dir) {
751
+ const match = dirToSys.get(dir);
752
+ if (match && match.depth >= 2)
753
+ return match.sysId;
754
+ const parent = dir.includes("/") ? dir.replace(/\/[^/]+$/, "") : "";
755
+ if (parent === dir)
756
+ break;
757
+ dir = parent;
758
+ }
759
+ return undefined;
760
+ }
761
+ // Extract bare import sources from raw file text (no AST needed — regex is fine for this)
762
+ function extractImportSources(src) {
763
+ const re = /(?:import|export)\s+(?:[\s\S]*?\s+from\s+)?['"]([^'"]+)['"]/g;
764
+ const out = [];
765
+ let m;
766
+ while ((m = re.exec(src)) !== null)
767
+ out.push(m[1]);
768
+ return out;
769
+ }
770
+ function getBaseFileContent(repoRoot, file, base) {
771
+ try {
772
+ return (0, child_process_1.execSync)(`git -C "${repoRoot}" show ${base}:${file}`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], maxBuffer: 500_000 });
773
+ }
774
+ catch {
775
+ return "";
776
+ }
777
+ }
778
+ // Resolve a bare import source to a repo-relative path without the filesystem
779
+ // (the base content may not be checked out, so we can't use fs.existsSync)
780
+ function resolveImportFuzzy(source, importerFile, aliases) {
781
+ if (source.startsWith(".")) {
782
+ const dir = path.dirname(importerFile);
783
+ const joined = path.join(dir, source).replace(/\\/g, "/");
784
+ // strip extension if present, normalise
785
+ return joined.replace(/\.(ts|tsx|js|jsx)$/, "");
786
+ }
787
+ // Try alias map
788
+ const fromDir = path.dirname(path.join("/", importerFile)); // fake abs for applyAlias
789
+ const aliased = (0, loadTsAliases_1.applyAlias)(source, fromDir, aliases);
790
+ if (aliased)
791
+ return aliased.replace(/\.(ts|tsx|js|jsx)$/, "");
792
+ return null;
793
+ }
794
+ function computeSystemEdgeDelta(repoRoot, base, changedFiles, fileData, aliases, am) {
795
+ if (!am?.systems)
796
+ return [];
797
+ const deltas = [];
798
+ const seen = new Set();
799
+ for (const [file, status] of changedFiles) {
800
+ // Added files create the most important new cross-system edges (a new handler that
801
+ // first wires the handler layer to persistence/DTOs). Their base content is empty, so
802
+ // every import reads as "added". Deleted files symmetrically yield "removed" edges.
803
+ // Only non-code files (no resolvable imports) are no-ops, handled naturally below.
804
+ const fromSys = mapFileToSystem(file, am);
805
+ if (!fromSys)
806
+ continue;
807
+ // Use fuzzy resolution for BOTH sides to avoid false positives from
808
+ // filesystem-vs-regex resolution asymmetry (resolveImport vs resolveImportFuzzy)
809
+ const mapSources = (sources) => {
810
+ const sys = new Set();
811
+ for (const src of sources) {
812
+ const resolved = resolveImportFuzzy(src, file, aliases);
813
+ if (!resolved)
814
+ continue;
815
+ const id = mapFileToSystem(resolved + ".ts", am)
816
+ ?? mapFileToSystem(resolved + ".tsx", am)
817
+ ?? mapFileToSystem(resolved, am);
818
+ if (id)
819
+ sys.add(id);
820
+ }
821
+ return sys;
822
+ };
823
+ const currentContent = (() => { try {
824
+ return fs.readFileSync(path.join(repoRoot, file), "utf8");
825
+ }
826
+ catch {
827
+ return "";
828
+ } })();
829
+ const currentSystems = mapSources(extractImportSources(currentContent));
830
+ const baseContent = getBaseFileContent(repoRoot, file, base);
831
+ const baseSystems = mapSources(extractImportSources(baseContent));
832
+ // New edges: in current but not in base
833
+ for (const toSys of currentSystems) {
834
+ if (toSys === fromSys)
835
+ continue;
836
+ if (!baseSystems.has(toSys)) {
837
+ const key = `added:${fromSys}→${toSys}`;
838
+ if (!seen.has(key)) {
839
+ seen.add(key);
840
+ deltas.push({ from: fromSys, to: toSys, kind: "added" });
841
+ }
842
+ }
843
+ }
844
+ // Removed edges: in base but not in current
845
+ for (const toSys of baseSystems) {
846
+ if (toSys === fromSys)
847
+ continue;
848
+ if (!currentSystems.has(toSys)) {
849
+ const key = `removed:${fromSys}→${toSys}`;
850
+ if (!seen.has(key)) {
851
+ seen.add(key);
852
+ deltas.push({ from: fromSys, to: toSys, kind: "removed" });
853
+ }
854
+ }
855
+ }
856
+ }
857
+ return deltas;
858
+ }
859
+ async function buildBranchStory(repoRoot, base = "main", apiKey, am, source = "working") {
860
+ const displayBase = base;
861
+ base = resolveTargetRef(repoRoot, base); // prefer origin/<branch> over a stale local target
862
+ const resolvedTarget = base; // capture before merge-base overwrites `base`
863
+ // SOURCE = what we inspect. "working" = current working tree + HEAD (full fidelity, fs reads
864
+ // are valid). A branch ref = that branch's committed work (Tier 1: diff + change summaries only;
865
+ // fs-dependent features — XRay/blast/entry-points — are gated because the branch isn't checked out).
866
+ const isWorking = !source || source === "working";
867
+ // Resolve the source to a concrete ref (local branch, else origin/<branch> for a peer's PR).
868
+ const srcResolved = isWorking ? "HEAD" : resolveRef(repoRoot, source);
869
+ const srcRef = isWorking ? "" : srcResolved;
870
+ const fidelity = isWorking ? "full" : "diff-only";
871
+ // Diff against the MERGE-BASE (PR-style) so we see only Source's own work, not the base's
872
+ // later commits. For working tree that's merge-base(target, HEAD); for a branch, merge-base(target, branch).
873
+ const mb = isWorking ? resolveMergeBase(repoRoot, base) : mergeBaseOf(repoRoot, base, srcResolved);
874
+ if (mb)
875
+ base = mb;
876
+ // For working tree: git diff <merge-base> includes uncommitted changes. For a branch: diff the branch tip.
877
+ let changedFiles = gitDiff(repoRoot, base, srcRef);
878
+ const scope = "session";
879
+ if (!changedFiles || changedFiles.size === 0) {
880
+ const mbMaster = isWorking ? resolveMergeBase(repoRoot, "master") : mergeBaseOf(repoRoot, "master", srcResolved);
881
+ changedFiles = gitDiff(repoRoot, mbMaster ?? "master", srcRef);
882
+ }
883
+ // Untracked files only exist for the working tree (a branch ref has no working state).
884
+ if (isWorking) {
885
+ const untracked = getUntrackedFiles(repoRoot).filter(f => !f.includes(".memor/") && !f.startsWith(".memor"));
886
+ if (untracked.length > 0) {
887
+ changedFiles = changedFiles ?? new Map();
888
+ for (const f of untracked)
889
+ if (!changedFiles.has(f))
890
+ changedFiles.set(f, "added");
891
+ }
892
+ }
893
+ const branchState = getBranchState(repoRoot, resolvedTarget, srcResolved) ?? undefined;
894
+ if (!changedFiles || changedFiles.size === 0) {
895
+ return { base: displayBase, scope, nodes: [], edges: [], summary: { added: 0, modified: 0, deleted: 0 }, blastRadius: [], systemEdgeDelta: [], branchState, baseSha: mb, source, fidelity };
896
+ }
897
+ // Load TS path aliases once for the repo
898
+ const aliases = await (0, loadTsAliases_1.loadTsAliases)(repoRoot);
899
+ const fileData = new Map();
900
+ for (const [file, status] of changedFiles) {
901
+ const abs = path.join(repoRoot, file);
902
+ const data = { status, exports: [], functions: [], lineCount: 0, resolvedImports: [] };
903
+ if (isWorking && status !== "deleted" && fs.existsSync(abs)) {
904
+ try {
905
+ const xray = (0, buildFileXRay_1.buildFileXRay)(abs, repoRoot);
906
+ data.exports = xray.exports.filter(e => !e.isType).map(e => e.name);
907
+ data.functions = xray.functions.map(f => f.name);
908
+ data.lineCount = xray.lineCount;
909
+ for (const imp of xray.imports) {
910
+ if (imp.isType)
911
+ continue;
912
+ const resolved = resolveImport(imp.source, abs, repoRoot, aliases);
913
+ if (resolved)
914
+ data.resolvedImports.push({ path: resolved, names: imp.names ?? [], defaultName: imp.defaultName });
915
+ }
916
+ }
917
+ catch { }
918
+ }
919
+ fileData.set(file, data);
920
+ }
921
+ // ── Pass 2: Collect context files ────────────────────────────────────────────
922
+ // Only include context nodes imported by ADDED files (keeps graph focused on what's new)
923
+ const contextFiles = new Set();
924
+ for (const [, data] of fileData) {
925
+ if (data.status !== "added")
926
+ continue; // only fan out from new code
927
+ for (const imp of data.resolvedImports) {
928
+ if (!changedFiles.has(imp.path)) {
929
+ const abs = path.join(repoRoot, imp.path);
930
+ if (fs.existsSync(abs))
931
+ contextFiles.add(imp.path);
932
+ }
933
+ }
934
+ }
935
+ // ── Build nodes ──────────────────────────────────────────────────────────────
936
+ const nodes = [];
937
+ for (const [file, data] of fileData) {
938
+ const ext = path.extname(file);
939
+ nodes.push({
940
+ id: file, file,
941
+ name: path.basename(file, ext),
942
+ ext, status: data.status,
943
+ exports: data.exports,
944
+ functions: data.functions,
945
+ lineCount: data.lineCount,
946
+ systemId: mapFileToSystem(file, am),
947
+ });
948
+ }
949
+ for (const file of contextFiles) {
950
+ const abs = path.join(repoRoot, file);
951
+ const ext = path.extname(file);
952
+ let exports = [], functions = [], lineCount = 0;
953
+ try {
954
+ const xray = (0, buildFileXRay_1.buildFileXRay)(abs, repoRoot);
955
+ exports = xray.exports.filter(e => !e.isType).map(e => e.name);
956
+ functions = xray.functions.map(f => f.name);
957
+ lineCount = xray.lineCount;
958
+ }
959
+ catch { }
960
+ nodes.push({
961
+ id: file, file, name: path.basename(file, ext),
962
+ ext, status: "context",
963
+ exports, functions, lineCount,
964
+ systemId: mapFileToSystem(file, am),
965
+ });
966
+ }
967
+ // ── Build edges ───────────────────────────────────────────────────────────────
968
+ const allNodeIds = new Set(nodes.map(n => n.id));
969
+ const edgeSeen = new Set();
970
+ const edges = [];
971
+ for (const [file, data] of fileData) {
972
+ for (const imp of data.resolvedImports) {
973
+ if (!allNodeIds.has(imp.path))
974
+ continue;
975
+ const key = `${file}→${imp.path}`;
976
+ if (edgeSeen.has(key))
977
+ continue;
978
+ edgeSeen.add(key);
979
+ const allNames = [...(imp.names ?? []).filter(n => n.length > 0)];
980
+ if (imp.defaultName)
981
+ allNames.unshift(imp.defaultName);
982
+ const label = allNames.length === 0 ? undefined
983
+ : allNames.length <= 3 ? allNames.join(", ")
984
+ : `${allNames.slice(0, 2).join(", ")} +${allNames.length - 2}`;
985
+ edges.push({ from: file, to: imp.path, label });
986
+ }
987
+ }
988
+ // ── Line stats for changed files ────────────────────────────────────────────
989
+ for (const node of nodes) {
990
+ if (node.status !== "context" && node.status !== "deleted") {
991
+ const stats = getLineStats(repoRoot, node.file, base, srcRef);
992
+ node.linesAdded = stats.added;
993
+ node.linesRemoved = stats.removed;
994
+ node.lastChanged = getLastChanged(repoRoot, node.file, srcRef, isWorking);
995
+ }
996
+ }
997
+ // ── Blast radius + system edges: whole-tree fs analysis — valid only for the checked-out
998
+ // working tree. For a non-current branch (diff-only fidelity) these are gated. ───────
999
+ const blastRadius = isWorking ? findBlastRadius(repoRoot, changedFiles, aliases) : [];
1000
+ const systemEdgeDelta = isWorking ? computeSystemEdgeDelta(repoRoot, base, changedFiles, fileData, aliases, am) : [];
1001
+ // ── Trace entry points (when does each change run?) — grounds the narration ──
1002
+ const rawEntryPoints = (isWorking && apiKey)
1003
+ ? await traceEntryPoints(repoRoot, new Set(changedFiles.keys()), edges, aliases)
1004
+ : new Map();
1005
+ if (process.env.MEMOR_DEBUG_EP)
1006
+ console.error("[ENTRY POINTS]", JSON.stringify([...rawEntryPoints].map(([f, e]) => [f.split("/").pop(), e.map(x => `${x.method} ${x.path}`)]), null, 0));
1007
+ // The import-proxy tracer over-assigns routes (it can't tell which route calls which handler,
1008
+ // nor which mount prefix applies). Gate it OUT of the narration until call-site precision lands —
1009
+ // we never feed a wrong trigger to the model. Flip EP_PRECISE once the precise resolver is in.
1010
+ const EP_PRECISE = true; // precise Babel call-site resolver verified on test-coverage-ui
1011
+ const entryPoints = EP_PRECISE ? rawEntryPoints : new Map();
1012
+ // ── Trace outbound destinations (where does the changed code SEND data?) ──────
1013
+ // The mirror of entry points: for a producer repo, "where it sends" is the orienting fact.
1014
+ const outboundByFile = new Map();
1015
+ const repoOutbound = [];
1016
+ if (isWorking && apiKey) {
1017
+ for (const f of changedFiles.keys()) {
1018
+ if (!/\.(ts|tsx|js|jsx|mjs|cjs)$/.test(f))
1019
+ continue;
1020
+ let src = "";
1021
+ try {
1022
+ src = fs.readFileSync(path.join(repoRoot, f), "utf8");
1023
+ }
1024
+ catch {
1025
+ continue;
1026
+ }
1027
+ const calls = (0, detectOutbound_1.detectOutboundInSource)(src);
1028
+ if (calls.length) {
1029
+ outboundByFile.set(f, calls);
1030
+ for (const c of calls)
1031
+ if (!repoOutbound.some(o => o.method === c.method && o.path === c.path))
1032
+ repoOutbound.push(c);
1033
+ }
1034
+ }
1035
+ }
1036
+ // ── Attach grounding facts to each node (deterministic — shown before the narration) ──
1037
+ // These are static-graph derived, so they're instant and never fabricated: the unfakeable
1038
+ // layer the right pane leads with, narration layers on top.
1039
+ for (const node of nodes) {
1040
+ const eps = entryPoints.get(node.file);
1041
+ if (eps && eps.length)
1042
+ node.entryPoints = eps;
1043
+ const obs = outboundByFile.get(node.file);
1044
+ if (obs && obs.length)
1045
+ node.outbound = obs;
1046
+ // Blast = the inverse of blastRadius: every file that imports THIS changed file.
1047
+ const importers = blastRadius.filter(b => b.importedFiles.includes(node.file));
1048
+ if (importers.length) {
1049
+ node.blast = {
1050
+ total: importers.length,
1051
+ handlers: importers.filter(b => b.category === "handler").length,
1052
+ services: importers.filter(b => b.category === "service").length,
1053
+ transitive: importers.filter(b => b.category === "transitive").length,
1054
+ files: importers.map(b => b.file),
1055
+ };
1056
+ }
1057
+ // Silent Killer: runtime/infra risk the code graph can't see (deterministic, diff-aware).
1058
+ // Works for any source (just needs the diff), so it runs in PR-review mode too.
1059
+ if (node.status !== "context" && (0, classifySilentKiller_1.isRiskCandidate)(node.file)) {
1060
+ let risk = null;
1061
+ if (node.file.split("/").pop() === "package.json") {
1062
+ // package.json gets precise block/type-aware parsing (prod vs dev vs @types).
1063
+ const readJson = (txt) => { try {
1064
+ return txt ? JSON.parse(txt) : null;
1065
+ }
1066
+ catch {
1067
+ return null;
1068
+ } };
1069
+ const before = readJson(getFileAtRef(repoRoot, base, node.file));
1070
+ const afterTxt = isWorking
1071
+ ? (() => { try {
1072
+ return fs.readFileSync(path.join(repoRoot, node.file), "utf8");
1073
+ }
1074
+ catch {
1075
+ return null;
1076
+ } })()
1077
+ : getFileAtRef(repoRoot, srcRef || source || "HEAD", node.file);
1078
+ const after = readJson(afterTxt);
1079
+ risk = (before || after) ? (0, classifySilentKiller_1.analyzePackageJson)(before ?? {}, after ?? {}) : (0, classifySilentKiller_1.classifySilentKiller)(node.file, getFileDiffRaw(repoRoot, node.file, base, srcRef));
1080
+ }
1081
+ else {
1082
+ risk = (0, classifySilentKiller_1.classifySilentKiller)(node.file, getFileDiffRaw(repoRoot, node.file, base, srcRef));
1083
+ }
1084
+ if (risk)
1085
+ node.risk = risk;
1086
+ }
1087
+ }
1088
+ // ── Simulate a Failure: evidence-chain pre-mortem (deterministic, no LLM) ──────
1089
+ // Risk as a CHAIN across files + architecture — catches what file-level flags miss
1090
+ // (the @types bump that crashes a runtime schema generator on the prod startup path).
1091
+ let failureReport;
1092
+ try {
1093
+ const changedForSim = nodes.filter(n => n.status !== "context").map(n => ({ file: n.file, status: n.status }));
1094
+ const pkgNode = changedForSim.find(c => c.file.split("/").pop() === "package.json");
1095
+ const rd = (t) => { try {
1096
+ return t ? JSON.parse(t) : null;
1097
+ }
1098
+ catch {
1099
+ return null;
1100
+ } };
1101
+ let pkgBefore = null, pkgAfter = null;
1102
+ if (pkgNode) {
1103
+ pkgBefore = rd(getFileAtRef(repoRoot, base, pkgNode.file));
1104
+ pkgAfter = isWorking
1105
+ ? rd((() => { try {
1106
+ return fs.readFileSync(path.join(repoRoot, pkgNode.file), "utf8");
1107
+ }
1108
+ catch {
1109
+ return null;
1110
+ } })())
1111
+ : rd(getFileAtRef(repoRoot, srcRef || source || "HEAD", pkgNode.file));
1112
+ }
1113
+ failureReport = (0, simulateFailure_1.runSimulateFailure)({
1114
+ repoRoot, changed: changedForSim, pkgBefore, pkgAfter,
1115
+ getDiff: (f) => getFileDiffRaw(repoRoot, f, base, srcRef),
1116
+ readSource: (f) => isWorking
1117
+ ? (() => { try {
1118
+ return fs.readFileSync(path.join(repoRoot, f), "utf8");
1119
+ }
1120
+ catch {
1121
+ return null;
1122
+ } })()
1123
+ : getFileAtRef(repoRoot, srcRef || source || "HEAD", f),
1124
+ });
1125
+ // Escalate the triggering file's badge to the finding severity so the file card reflects the chain.
1126
+ for (const f of failureReport.findings) {
1127
+ const triggerFile = f.evidence[0]?.file;
1128
+ const node = triggerFile ? nodes.find(n => n.file === triggerFile) : undefined;
1129
+ if (node && (!node.risk || (node.risk.tier !== "critical" && f.severity === "critical"))) {
1130
+ node.risk = { tier: f.severity, silentKiller: true, dimension: "runtime",
1131
+ reason: `${f.title}. ${f.ciBlindSpot ?? ""}`.trim(), signals: [f.patternId] };
1132
+ }
1133
+ }
1134
+ }
1135
+ catch {
1136
+ failureReport = undefined;
1137
+ }
1138
+ // ── LLM narration (narrate-only): tighten the deterministic pre-mortem prose. ──
1139
+ // The model may restate evidence facts; it must NEVER invent a file, cause, or severity.
1140
+ // Guardrail: reject any rewrite that mentions a file not already in the evidence chain.
1141
+ if (apiKey && failureReport && failureReport.findings.length) {
1142
+ try {
1143
+ const client = (0, anthropic_1.createAnthropicClient)(apiKey);
1144
+ const payload = failureReport.findings.map((f, idx) => ({ idx, title: f.title, severity: f.severity, failureMode: f.failureMode, narrative: f.narrative, evidence: f.evidence.map(e => ({ file: e.file, line: e.line, detail: e.detail })), questions: f.questions }));
1145
+ const msg = await client.messages.create({
1146
+ model: "claude-haiku-4-5-20251001",
1147
+ max_tokens: 900,
1148
+ system: "You rewrite deterministic pre-mortem findings into crisp prose for an engineer about to merge. You may ONLY restate facts already present in each finding's narrative and evidence. NEVER introduce a file, cause, or severity that is not already there. No em-dashes or en-dashes. Each narrative <= 55 words, concrete and direct. Output ONLY valid JSON.",
1149
+ messages: [{ role: "user", content: `Findings:\n${JSON.stringify(payload)}\n\nFor each finding, rewrite "narrative" and optionally tighten "questions" (keep <= 2). Return ONLY JSON: {"findings":[{"idx":0,"narrative":"...","questions":["..."]}]}` }],
1150
+ });
1151
+ const text = msg.content.filter(b => b.type === "text").map(b => b.text).join("");
1152
+ const parsed = JSON.parse(text.slice(text.indexOf("{"), text.lastIndexOf("}") + 1));
1153
+ const allFiles = new Set(failureReport.findings.flatMap(f => f.evidence.map(e => e.file.split("/").pop())));
1154
+ for (const r of parsed.findings ?? []) {
1155
+ const f = failureReport.findings[r.idx];
1156
+ if (!f || typeof r.narrative !== "string")
1157
+ continue;
1158
+ const badFile = (r.narrative.match(/[\w./-]+\.(ts|tsx|js|cjs|json|yml|yaml)/g) || []).some((m) => !allFiles.has(m.split("/").pop()));
1159
+ if (badFile)
1160
+ continue; // hallucinated a file → keep deterministic narrative
1161
+ f.narrative = deDash(r.narrative.trim());
1162
+ if (Array.isArray(r.questions) && r.questions.length)
1163
+ f.questions = r.questions.slice(0, 2).map((q) => deDash(String(q)));
1164
+ }
1165
+ }
1166
+ catch { /* keep deterministic narrative */ }
1167
+ }
1168
+ // ── Generate LLM summaries for changed files ─────────────────────────────────
1169
+ let prSummary;
1170
+ if (apiKey) {
1171
+ const summaries = await generateFileSummaries(repoRoot, base, changedFiles, apiKey, nodes, blastRadius, am, entryPoints, outboundByFile, srcRef);
1172
+ for (const node of nodes) {
1173
+ const s = summaries.get(node.file);
1174
+ if (s) {
1175
+ node.summary = s.summary;
1176
+ node.role = s.role;
1177
+ if (s.changePoints?.length)
1178
+ node.changePoints = s.changePoints;
1179
+ }
1180
+ }
1181
+ prSummary = await generatePRSummary(nodes, apiKey);
1182
+ }
1183
+ // ── Cluster the changeset into threads of work ──────────────────────────────
1184
+ // PAUSED: changeset grouping is intentionally off (files-first UX). Flip to re-enable.
1185
+ // Skipping it also drops one LLM call per analysis → faster.
1186
+ const ENABLE_THREADS = false;
1187
+ let threads;
1188
+ if (ENABLE_THREADS && apiKey) {
1189
+ threads = await generateChangeThreads(repoRoot, nodes, edges, apiKey, entryPoints, repoOutbound);
1190
+ }
1191
+ // ── Strip em/en dashes from ALL narration (the "AI tell") — the deterministic guarantee ──
1192
+ for (const n of nodes) {
1193
+ if (n.summary)
1194
+ n.summary = n.summary.split("\n").map(deDash).filter(Boolean).join("\n");
1195
+ if (n.role)
1196
+ n.role = deDash(n.role);
1197
+ if (n.changePoints) {
1198
+ n.changePoints = n.changePoints.map(p => ({ ...p, text: deDash(p.text) }));
1199
+ }
1200
+ }
1201
+ if (threads)
1202
+ for (const t of threads) {
1203
+ t.title = deDash(t.title);
1204
+ if (t.summary)
1205
+ t.summary = t.summary.split("\n").map(deDash).filter(Boolean).join("\n");
1206
+ }
1207
+ if (prSummary)
1208
+ prSummary = deDash(prSummary);
1209
+ const vals = [...changedFiles.values()];
1210
+ return {
1211
+ base: displayBase, scope, nodes, edges,
1212
+ summary: {
1213
+ added: vals.filter(s => s === "added").length,
1214
+ modified: vals.filter(s => s === "modified").length,
1215
+ deleted: vals.filter(s => s === "deleted").length,
1216
+ },
1217
+ blastRadius,
1218
+ systemEdgeDelta,
1219
+ prSummary,
1220
+ threads,
1221
+ branchState,
1222
+ baseSha: mb,
1223
+ source,
1224
+ fidelity,
1225
+ failureReport,
1226
+ };
1227
+ }
1228
+ /**
1229
+ * Public entry point. For the working tree, runs the normal full analysis. For a Source that is a
1230
+ * BRANCH (peer PR review), checks the branch out into an ISOLATED git worktree (a temp dir that
1231
+ * never touches the user's working tree), runs the full analysis there — so blast radius and the
1232
+ * deep import-graph analysis work without checkout friction — then tears the worktree down.
1233
+ * Falls back to diff-only on the main repo if the worktree can't be created.
1234
+ */
1235
+ async function buildBranchStoryForSource(repoRoot, base = "main", apiKey, am, source = "working") {
1236
+ const isWorking = !source || source === "working";
1237
+ if (isWorking)
1238
+ return buildBranchStory(repoRoot, base, apiKey, am, source);
1239
+ const ref = resolveRef(repoRoot, source); // local branch, else origin/<branch>
1240
+ const wt = path.join(os.tmpdir(), `memor-wt-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
1241
+ try {
1242
+ // --detach checks out the ref's commit without claiming the branch (works for origin/<x> too).
1243
+ (0, child_process_1.execSync)(`git -C "${repoRoot}" worktree add --detach "${wt}" ${ref}`, { stdio: ["pipe", "pipe", "pipe"], timeout: 90000 });
1244
+ // In the worktree the branch IS the working tree → run the normal full-fidelity path.
1245
+ const story = await buildBranchStory(wt, base, apiKey, am, "working");
1246
+ story.source = source; // reflect the branch under review (worktree HEAD is detached)
1247
+ return story;
1248
+ }
1249
+ catch {
1250
+ // Worktree unavailable (missing ref, git too old, disk) → honest diff-only on the main repo.
1251
+ return buildBranchStory(repoRoot, base, apiKey, am, source);
1252
+ }
1253
+ finally {
1254
+ try {
1255
+ (0, child_process_1.execSync)(`git -C "${repoRoot}" worktree remove --force "${wt}"`, { stdio: ["pipe", "pipe", "pipe"] });
1256
+ }
1257
+ catch { }
1258
+ try {
1259
+ (0, child_process_1.execSync)(`git -C "${repoRoot}" worktree prune`, { stdio: ["pipe", "pipe", "pipe"] });
1260
+ }
1261
+ catch { }
1262
+ }
1263
+ }
1264
+ //# sourceMappingURL=buildBranchStory.js.map