gsd-pi 2.68.0 → 2.68.1-dev.abc8f2b

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 (258) hide show
  1. package/README.md +66 -70
  2. package/dist/resources/extensions/gsd/auto-model-selection.js +27 -1
  3. package/dist/resources/extensions/gsd/auto.js +8 -2
  4. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +7 -0
  5. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +1 -5
  6. package/dist/resources/extensions/gsd/guided-flow.js +25 -70
  7. package/dist/resources/extensions/gsd/model-router.js +85 -2
  8. package/dist/resources/extensions/gsd/prompts/discuss.md +2 -0
  9. package/dist/resources/extensions/gsd/templates/context.md +34 -2
  10. package/dist/web/standalone/.next/BUILD_ID +1 -1
  11. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  12. package/dist/web/standalone/.next/build-manifest.json +3 -3
  13. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  14. package/dist/web/standalone/.next/required-server-files.json +3 -3
  15. package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
  16. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  17. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  18. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  19. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  26. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  27. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  28. package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
  29. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  30. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  32. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  36. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  37. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  38. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  39. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  40. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  41. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  42. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  43. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  44. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  45. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  46. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  47. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  48. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  49. package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
  50. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  51. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  52. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  53. package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
  54. package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
  55. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  56. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  57. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  58. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  59. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  60. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  61. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  62. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  63. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  64. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  65. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  66. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  67. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  68. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  69. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  70. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  71. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  72. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  73. package/dist/web/standalone/.next/server/app/api/notifications/route.js +2 -2
  74. package/dist/web/standalone/.next/server/app/api/notifications/route_client-reference-manifest.js +1 -1
  75. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  76. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  77. package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
  78. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  79. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  80. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  81. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  82. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  83. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
  84. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  85. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  86. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  87. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  88. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  89. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  90. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  91. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  92. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  93. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  94. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  95. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  96. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  97. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  98. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  99. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  103. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +1 -1
  104. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  105. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
  106. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  107. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +1 -1
  108. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  109. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +2 -2
  110. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  111. package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
  112. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  113. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  114. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  115. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  116. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  117. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  118. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  119. package/dist/web/standalone/.next/server/app/index.html +1 -1
  120. package/dist/web/standalone/.next/server/app/index.rsc +4 -4
  121. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  122. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
  123. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  124. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +3 -3
  125. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  126. package/dist/web/standalone/.next/server/app/page.js +2 -2
  127. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  128. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  129. package/dist/web/standalone/.next/server/chunks/63.js +3 -3
  130. package/dist/web/standalone/.next/server/chunks/6897.js +1 -1
  131. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  132. package/dist/web/standalone/.next/server/middleware.js +2 -2
  133. package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
  134. package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
  135. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  136. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  137. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  138. package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-2f24283c162b6ab3.js → page-f2a7482d42a5614b.js} +1 -1
  139. package/dist/web/standalone/.next/static/chunks/app/{layout-9ecfd95f343793f0.js → layout-a16c7a7ecdf0c2cf.js} +1 -1
  140. package/dist/web/standalone/.next/static/chunks/app/page-f1e30ab6bb269149.js +1 -0
  141. package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +1 -0
  142. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +1 -0
  143. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  144. package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
  145. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  146. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
  147. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
  148. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
  149. package/dist/web/standalone/server.js +1 -1
  150. package/package.json +1 -1
  151. package/packages/pi-ai/dist/index.d.ts +3 -0
  152. package/packages/pi-ai/dist/index.d.ts.map +1 -1
  153. package/packages/pi-ai/dist/index.js +2 -0
  154. package/packages/pi-ai/dist/index.js.map +1 -1
  155. package/packages/pi-ai/dist/providers/amazon-bedrock.js +2 -2
  156. package/packages/pi-ai/dist/providers/amazon-bedrock.js.map +1 -1
  157. package/packages/pi-ai/dist/providers/anthropic-shared.js +2 -2
  158. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
  159. package/packages/pi-ai/dist/providers/google-shared.js +2 -2
  160. package/packages/pi-ai/dist/providers/google-shared.js.map +1 -1
  161. package/packages/pi-ai/dist/providers/mistral.js +2 -2
  162. package/packages/pi-ai/dist/providers/mistral.js.map +1 -1
  163. package/packages/pi-ai/dist/providers/openai-completions.js +2 -2
  164. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  165. package/packages/pi-ai/dist/providers/openai-responses-shared.js +2 -2
  166. package/packages/pi-ai/dist/providers/openai-responses-shared.js.map +1 -1
  167. package/packages/pi-ai/dist/providers/provider-capabilities.d.ts +59 -0
  168. package/packages/pi-ai/dist/providers/provider-capabilities.d.ts.map +1 -0
  169. package/packages/pi-ai/dist/providers/provider-capabilities.js +173 -0
  170. package/packages/pi-ai/dist/providers/provider-capabilities.js.map +1 -0
  171. package/packages/pi-ai/dist/providers/provider-capabilities.test.d.ts +2 -0
  172. package/packages/pi-ai/dist/providers/provider-capabilities.test.d.ts.map +1 -0
  173. package/packages/pi-ai/dist/providers/provider-capabilities.test.js +132 -0
  174. package/packages/pi-ai/dist/providers/provider-capabilities.test.js.map +1 -0
  175. package/packages/pi-ai/dist/providers/transform-messages-report.test.d.ts +2 -0
  176. package/packages/pi-ai/dist/providers/transform-messages-report.test.d.ts.map +1 -0
  177. package/packages/pi-ai/dist/providers/transform-messages-report.test.js +172 -0
  178. package/packages/pi-ai/dist/providers/transform-messages-report.test.js.map +1 -0
  179. package/packages/pi-ai/dist/providers/transform-messages.d.ts +34 -1
  180. package/packages/pi-ai/dist/providers/transform-messages.d.ts.map +1 -1
  181. package/packages/pi-ai/dist/providers/transform-messages.js +73 -2
  182. package/packages/pi-ai/dist/providers/transform-messages.js.map +1 -1
  183. package/packages/pi-ai/src/index.ts +3 -0
  184. package/packages/pi-ai/src/providers/amazon-bedrock.ts +2 -2
  185. package/packages/pi-ai/src/providers/anthropic-shared.ts +2 -2
  186. package/packages/pi-ai/src/providers/google-shared.ts +2 -2
  187. package/packages/pi-ai/src/providers/mistral.ts +2 -2
  188. package/packages/pi-ai/src/providers/openai-completions.ts +2 -2
  189. package/packages/pi-ai/src/providers/openai-responses-shared.ts +2 -2
  190. package/packages/pi-ai/src/providers/provider-capabilities.test.ts +174 -0
  191. package/packages/pi-ai/src/providers/provider-capabilities.ts +215 -0
  192. package/packages/pi-ai/src/providers/transform-messages-report.test.ts +189 -0
  193. package/packages/pi-ai/src/providers/transform-messages.ts +94 -1
  194. package/packages/pi-coding-agent/dist/core/extensions/index.d.ts +1 -1
  195. package/packages/pi-coding-agent/dist/core/extensions/index.d.ts.map +1 -1
  196. package/packages/pi-coding-agent/dist/core/extensions/index.js.map +1 -1
  197. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  198. package/packages/pi-coding-agent/dist/core/extensions/loader.js +10 -1
  199. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  200. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +2 -1
  201. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  202. package/packages/pi-coding-agent/dist/core/extensions/runner.js +15 -0
  203. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  204. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +41 -0
  205. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  206. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  207. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +1 -0
  208. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  209. package/packages/pi-coding-agent/dist/core/tools/index.js +1 -0
  210. package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  211. package/packages/pi-coding-agent/dist/core/tools/tool-compatibility-registry.d.ts +27 -0
  212. package/packages/pi-coding-agent/dist/core/tools/tool-compatibility-registry.d.ts.map +1 -0
  213. package/packages/pi-coding-agent/dist/core/tools/tool-compatibility-registry.js +69 -0
  214. package/packages/pi-coding-agent/dist/core/tools/tool-compatibility-registry.js.map +1 -0
  215. package/packages/pi-coding-agent/dist/index.d.ts +2 -2
  216. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  217. package/packages/pi-coding-agent/dist/index.js +3 -1
  218. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  219. package/packages/pi-coding-agent/package.json +1 -1
  220. package/packages/pi-coding-agent/src/core/extensions/index.ts +4 -0
  221. package/packages/pi-coding-agent/src/core/extensions/loader.ts +11 -1
  222. package/packages/pi-coding-agent/src/core/extensions/runner.ts +18 -0
  223. package/packages/pi-coding-agent/src/core/extensions/types.ts +45 -0
  224. package/packages/pi-coding-agent/src/core/tools/index.ts +7 -0
  225. package/packages/pi-coding-agent/src/core/tools/tool-compatibility-registry.ts +83 -0
  226. package/packages/pi-coding-agent/src/index.ts +9 -0
  227. package/pkg/package.json +1 -1
  228. package/src/resources/extensions/gsd/auto-model-selection.ts +36 -4
  229. package/src/resources/extensions/gsd/auto.ts +8 -2
  230. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +8 -0
  231. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +1 -5
  232. package/src/resources/extensions/gsd/guided-flow.ts +22 -84
  233. package/src/resources/extensions/gsd/model-router.ts +117 -10
  234. package/src/resources/extensions/gsd/preferences-types.ts +3 -1
  235. package/src/resources/extensions/gsd/prompts/discuss.md +2 -0
  236. package/src/resources/extensions/gsd/templates/context.md +34 -2
  237. package/src/resources/extensions/gsd/tests/capability-router.test.ts +31 -7
  238. package/src/resources/extensions/gsd/tests/discord-invite-links.test.ts +1 -1
  239. package/src/resources/extensions/gsd/tests/model-router.test.ts +2 -2
  240. package/src/resources/extensions/gsd/tests/resource-loader-import-path.test.ts +37 -0
  241. package/src/resources/extensions/gsd/tests/tool-compatibility.test.ts +199 -0
  242. package/src/resources/extensions/gsd/tests/write-gate.test.ts +13 -16
  243. package/dist/resources/extensions/gsd/prompt-validation.js +0 -67
  244. package/dist/resources/extensions/gsd/prompts/discuss-prepared.md +0 -424
  245. package/dist/resources/extensions/gsd/templates/context-enhanced.md +0 -138
  246. package/dist/web/standalone/.next/static/chunks/app/page-7115e62689b5fd84.js +0 -1
  247. package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +0 -1
  248. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +0 -1
  249. package/src/resources/extensions/gsd/prompt-validation.ts +0 -88
  250. package/src/resources/extensions/gsd/prompts/discuss-prepared.md +0 -424
  251. package/src/resources/extensions/gsd/templates/context-enhanced.md +0 -138
  252. package/src/resources/extensions/gsd/tests/adversarial-review-fixes.test.ts +0 -223
  253. package/src/resources/extensions/gsd/tests/integration/test-isolation.ts +0 -53
  254. package/src/resources/extensions/gsd/tests/integration-prepared-discussion.test.ts +0 -525
  255. package/src/resources/extensions/gsd/tests/preparation.test.ts +0 -1211
  256. package/src/resources/extensions/gsd/tests/prompt-builder.test.ts +0 -669
  257. /package/dist/web/standalone/.next/static/{ka3ShQTakcliYL-EXRRb6 → 3HMOXcBoys84RYd2F8a79}/_buildManifest.js +0 -0
  258. /package/dist/web/standalone/.next/static/{ka3ShQTakcliYL-EXRRb6 → 3HMOXcBoys84RYd2F8a79}/_ssgManifest.js +0 -0
@@ -1,1211 +0,0 @@
1
- /**
2
- * Unit tests for GSD Preparation — codebase analysis and brief generation.
3
- *
4
- * Exercises the pure preparation functions:
5
- * - analyzeCodebase() with various project layouts
6
- * - formatCodebaseBrief() output format and truncation
7
- * - Pattern extraction from sampled files
8
- */
9
-
10
- import test from "node:test";
11
- import assert from "node:assert/strict";
12
- import { mkdirSync, writeFileSync, rmSync, existsSync } from "node:fs";
13
- import { join } from "node:path";
14
- import { tmpdir } from "node:os";
15
- import {
16
- analyzeCodebase,
17
- formatCodebaseBrief,
18
- aggregatePriorContext,
19
- formatPriorContextBrief,
20
- researchEcosystem,
21
- formatEcosystemBrief,
22
- runPreparation,
23
- type CodebaseBrief,
24
- type PriorContextBrief,
25
- type EcosystemBrief,
26
- type EcosystemFinding,
27
- type PreparationUIContext,
28
- type PreparationPreferences,
29
- type PreparationResult,
30
- } from "../preparation.ts";
31
- import { PROJECT_FILES } from "../detection.ts";
32
-
33
- // ─── Test Helpers ───────────────────────────────────────────────────────────────
34
-
35
- function makeTempDir(prefix: string): string {
36
- const dir = join(
37
- tmpdir(),
38
- `gsd-preparation-test-${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
39
- );
40
- mkdirSync(dir, { recursive: true });
41
- return dir;
42
- }
43
-
44
- function cleanup(dir: string): void {
45
- try {
46
- rmSync(dir, { recursive: true, force: true });
47
- } catch {
48
- // best-effort
49
- }
50
- }
51
-
52
- // ─── analyzeCodebase ────────────────────────────────────────────────────────────
53
-
54
- test("analyzeCodebase: empty directory returns valid brief structure", async (t) => {
55
- const dir = makeTempDir("empty");
56
- t.after(() => cleanup(dir));
57
-
58
- const brief = await analyzeCodebase(dir);
59
-
60
- assert.ok(brief, "should return a brief");
61
- assert.ok(brief.techStack, "should have techStack");
62
- assert.ok(brief.moduleStructure, "should have moduleStructure");
63
- assert.ok(brief.patterns, "should have patterns");
64
- assert.ok(Array.isArray(brief.sampledFiles), "should have sampledFiles array");
65
- assert.equal(brief.sampledFiles.length, 0, "empty dir should have no sampled files");
66
- });
67
-
68
- test("analyzeCodebase: detects package.json in PROJECT_FILES", async (t) => {
69
- const dir = makeTempDir("pkg-json");
70
- t.after(() => cleanup(dir));
71
-
72
- writeFileSync(
73
- join(dir, "package.json"),
74
- JSON.stringify({ name: "test-project", scripts: { test: "jest" } }),
75
- "utf-8",
76
- );
77
-
78
- const brief = await analyzeCodebase(dir);
79
-
80
- assert.ok(
81
- brief.techStack.detectedFiles.includes("package.json"),
82
- "should detect package.json",
83
- );
84
- assert.equal(brief.techStack.primaryLanguage, "javascript/typescript");
85
- });
86
-
87
- test("analyzeCodebase: detects module structure from src/ directory", async (t) => {
88
- const dir = makeTempDir("module-struct");
89
- t.after(() => cleanup(dir));
90
-
91
- // Create src directory with subdirs
92
- mkdirSync(join(dir, "src", "components"), { recursive: true });
93
- mkdirSync(join(dir, "src", "utils"), { recursive: true });
94
- mkdirSync(join(dir, "src", "hooks"), { recursive: true });
95
- mkdirSync(join(dir, "test"), { recursive: true });
96
-
97
- const brief = await analyzeCodebase(dir);
98
-
99
- assert.ok(
100
- brief.moduleStructure.topLevelDirs.includes("src"),
101
- "should detect src as top-level dir",
102
- );
103
- assert.ok(
104
- brief.moduleStructure.topLevelDirs.includes("test"),
105
- "should detect test as top-level dir",
106
- );
107
- assert.ok(
108
- brief.moduleStructure.srcSubdirs.includes("components"),
109
- "should detect components subdir",
110
- );
111
- assert.ok(
112
- brief.moduleStructure.srcSubdirs.includes("utils"),
113
- "should detect utils subdir",
114
- );
115
- assert.ok(
116
- brief.moduleStructure.srcSubdirs.includes("hooks"),
117
- "should detect hooks subdir",
118
- );
119
- });
120
-
121
- test("analyzeCodebase: samples TypeScript files from src/", async (t) => {
122
- const dir = makeTempDir("sample-ts");
123
- t.after(() => cleanup(dir));
124
-
125
- // Create src directory with TypeScript files
126
- mkdirSync(join(dir, "src"), { recursive: true });
127
- writeFileSync(
128
- join(dir, "src", "index.ts"),
129
- `export async function main() { await fetch('/api'); }`,
130
- "utf-8",
131
- );
132
- writeFileSync(
133
- join(dir, "src", "utils.ts"),
134
- `export function helper() { try { return 1; } catch (e) { throw e; } }`,
135
- "utf-8",
136
- );
137
-
138
- const brief = await analyzeCodebase(dir);
139
-
140
- assert.ok(brief.sampledFiles.length > 0, "should sample at least one file");
141
- assert.ok(
142
- brief.sampledFiles.some((f) => f.startsWith("src/")),
143
- "should prefer src/ files",
144
- );
145
- });
146
-
147
- test("analyzeCodebase: excludes test files from sampling", async (t) => {
148
- const dir = makeTempDir("exclude-tests");
149
- t.after(() => cleanup(dir));
150
-
151
- mkdirSync(join(dir, "src"), { recursive: true });
152
- writeFileSync(join(dir, "src", "index.ts"), `export const x = 1;`, "utf-8");
153
- writeFileSync(
154
- join(dir, "src", "index.test.ts"),
155
- `import test from 'node:test'; test('x', () => {});`,
156
- "utf-8",
157
- );
158
- writeFileSync(
159
- join(dir, "src", "utils.spec.ts"),
160
- `describe('utils', () => { it('works', () => {}); });`,
161
- "utf-8",
162
- );
163
-
164
- const brief = await analyzeCodebase(dir);
165
-
166
- // Should only have index.ts, not test/spec files
167
- for (const file of brief.sampledFiles) {
168
- assert.ok(!file.endsWith(".test.ts"), `should not sample ${file}`);
169
- assert.ok(!file.endsWith(".spec.ts"), `should not sample ${file}`);
170
- }
171
- });
172
-
173
- test("analyzeCodebase: excludes node_modules from sampling", async (t) => {
174
- const dir = makeTempDir("exclude-nm");
175
- t.after(() => cleanup(dir));
176
-
177
- mkdirSync(join(dir, "src"), { recursive: true });
178
- mkdirSync(join(dir, "node_modules", "some-pkg"), { recursive: true });
179
- writeFileSync(join(dir, "src", "index.ts"), `export const x = 1;`, "utf-8");
180
- writeFileSync(
181
- join(dir, "node_modules", "some-pkg", "index.js"),
182
- `module.exports = {};`,
183
- "utf-8",
184
- );
185
-
186
- const brief = await analyzeCodebase(dir);
187
-
188
- for (const file of brief.sampledFiles) {
189
- assert.ok(!file.includes("node_modules"), `should not sample ${file}`);
190
- }
191
- });
192
-
193
- test("analyzeCodebase: extracts async/await pattern", async (t) => {
194
- const dir = makeTempDir("async-await");
195
- t.after(() => cleanup(dir));
196
-
197
- mkdirSync(join(dir, "src"), { recursive: true });
198
- writeFileSync(
199
- join(dir, "src", "api.ts"),
200
- `
201
- export async function fetchData() {
202
- const res = await fetch('/api');
203
- const data = await res.json();
204
- return data;
205
- }
206
-
207
- export async function saveData(data: any) {
208
- await fetch('/api', { method: 'POST', body: JSON.stringify(data) });
209
- }
210
- `,
211
- "utf-8",
212
- );
213
-
214
- const brief = await analyzeCodebase(dir);
215
-
216
- assert.equal(
217
- brief.patterns.asyncStyle,
218
- "async/await",
219
- "should detect async/await as primary style",
220
- );
221
- });
222
-
223
- test("analyzeCodebase: extracts try/catch error handling", async (t) => {
224
- const dir = makeTempDir("try-catch");
225
- t.after(() => cleanup(dir));
226
-
227
- mkdirSync(join(dir, "src"), { recursive: true });
228
- writeFileSync(
229
- join(dir, "src", "handler.ts"),
230
- `
231
- export function handleError() {
232
- try {
233
- doSomething();
234
- } catch (error) {
235
- console.error(error);
236
- }
237
- }
238
-
239
- export function anotherHandler() {
240
- try {
241
- doOther();
242
- } catch (e) {
243
- throw new Error('wrapped');
244
- }
245
- }
246
- `,
247
- "utf-8",
248
- );
249
-
250
- const brief = await analyzeCodebase(dir);
251
-
252
- assert.equal(
253
- brief.patterns.errorHandling,
254
- "try/catch",
255
- "should detect try/catch as primary error handling",
256
- );
257
- });
258
-
259
- test("analyzeCodebase: extracts camelCase naming convention", async (t) => {
260
- const dir = makeTempDir("camel-case");
261
- t.after(() => cleanup(dir));
262
-
263
- mkdirSync(join(dir, "src"), { recursive: true });
264
- writeFileSync(
265
- join(dir, "src", "utils.ts"),
266
- `
267
- export function getUserById(userId: string) {
268
- return fetchUser(userId);
269
- }
270
-
271
- export function calculateTotalPrice(itemPrices: number[]) {
272
- return itemPrices.reduce((a, b) => a + b, 0);
273
- }
274
-
275
- export function formatDisplayName(firstName: string, lastName: string) {
276
- return \`\${firstName} \${lastName}\`;
277
- }
278
- `,
279
- "utf-8",
280
- );
281
-
282
- const brief = await analyzeCodebase(dir);
283
-
284
- // camelCase should be detected (getUserById, userId, fetchUser, etc.)
285
- assert.ok(
286
- brief.patterns.namingConvention === "camelCase" || brief.patterns.namingConvention === "mixed",
287
- `should detect camelCase or mixed, got ${brief.patterns.namingConvention}`,
288
- );
289
- });
290
-
291
- test("analyzeCodebase: gracefully handles empty directories", async (t) => {
292
- const dir = makeTempDir("empty-src");
293
- t.after(() => cleanup(dir));
294
-
295
- // Create empty src directory
296
- mkdirSync(join(dir, "src"), { recursive: true });
297
-
298
- const brief = await analyzeCodebase(dir);
299
-
300
- // Should not throw, should return valid structure
301
- assert.ok(brief.patterns, "should have patterns");
302
- assert.equal(brief.patterns.asyncStyle, "unknown", "should return unknown for empty");
303
- assert.equal(brief.patterns.errorHandling, "unknown", "should return unknown for empty");
304
- assert.equal(brief.patterns.namingConvention, "unknown", "should return unknown for empty");
305
- });
306
-
307
- test("analyzeCodebase: returns unknown for unrecognized language patterns (Ruby)", async (t) => {
308
- // Ruby is detected by LANGUAGE_MAP but not in LANGUAGE_PATTERNS registry
309
- // This tests the graceful fallback behavior: naming convention still works,
310
- // but language-specific patterns (async/error) should return "unknown"
311
- const dir = makeTempDir("ruby-project");
312
- t.after(() => cleanup(dir));
313
-
314
- // Create a Ruby project with Gemfile (detected as "ruby" in LANGUAGE_MAP)
315
- writeFileSync(join(dir, "Gemfile"), `source "https://rubygems.org"\ngem "rails"`, "utf-8");
316
-
317
- // Add a Ruby file with patterns that would match JS/TS regexes incorrectly
318
- mkdirSync(join(dir, "lib"), { recursive: true });
319
- writeFileSync(
320
- join(dir, "lib", "service.rb"),
321
- `
322
- class UserService
323
- def fetch_user(user_id)
324
- user = User.find(user_id)
325
- user
326
- rescue ActiveRecord::RecordNotFound => e
327
- Rails.logger.error("User not found: #{e.message}")
328
- nil
329
- end
330
-
331
- def async_task(&block)
332
- # Ruby doesn't have async/await but has yield and blocks
333
- Thread.new { yield }
334
- end
335
- end
336
- `,
337
- "utf-8",
338
- );
339
-
340
- const brief = await analyzeCodebase(dir);
341
-
342
- // Language should be detected as Ruby
343
- assert.equal(brief.techStack.primaryLanguage, "ruby", "should detect ruby from Gemfile");
344
-
345
- // Language-specific patterns should return "unknown" (not JS/TS patterns)
346
- assert.equal(
347
- brief.patterns.asyncStyle,
348
- "unknown",
349
- "should return unknown for async style in unrecognized language",
350
- );
351
- assert.equal(
352
- brief.patterns.errorHandling,
353
- "unknown",
354
- "should return unknown for error handling in unrecognized language",
355
- );
356
-
357
- // But naming convention detection should still work (it's universal)
358
- // The Ruby code uses snake_case (fetch_user, user_id) and camelCase (UserService)
359
- assert.ok(
360
- brief.patterns.namingConvention !== "unknown",
361
- "naming convention should still be detected for unrecognized languages",
362
- );
363
-
364
- // Evidence should explain why patterns aren't available
365
- assert.ok(
366
- brief.patterns.evidence.asyncStyle.some((e) => e.includes("not in pattern registry")),
367
- "evidence should explain async style is not available",
368
- );
369
- assert.ok(
370
- brief.patterns.evidence.errorHandling.some((e) => e.includes("not in pattern registry")),
371
- "evidence should explain error handling is not available",
372
- );
373
- });
374
-
375
- // ─── formatCodebaseBrief ────────────────────────────────────────────────────────
376
-
377
- test("formatCodebaseBrief: produces markdown output", async (t) => {
378
- const brief: CodebaseBrief = {
379
- techStack: {
380
- primaryLanguage: "javascript/typescript",
381
- detectedFiles: ["package.json", "tsconfig.json"],
382
- packageManager: "npm",
383
- isMonorepo: false,
384
- hasTests: true,
385
- hasCI: true,
386
- },
387
- moduleStructure: {
388
- topLevelDirs: ["src", "test"],
389
- srcSubdirs: ["components", "utils"],
390
- totalFilesSampled: 5,
391
- },
392
- patterns: {
393
- asyncStyle: "async/await",
394
- errorHandling: "try/catch",
395
- namingConvention: "camelCase",
396
- evidence: {
397
- asyncStyle: ["src/api.ts: async/await (5 occurrences)"],
398
- errorHandling: ["src/handler.ts: try/catch (3 occurrences)"],
399
- namingConvention: ["camelCase: 50 occurrences"],
400
- },
401
- fileCounts: {
402
- asyncAwait: 3,
403
- promises: 0,
404
- callbacks: 0,
405
- tryCatch: 2,
406
- errorCallbacks: 0,
407
- resultTypes: 0,
408
- },
409
- },
410
- sampledFiles: ["src/index.ts", "src/utils.ts"],
411
- };
412
-
413
- const formatted = formatCodebaseBrief(brief);
414
-
415
- assert.ok(formatted.includes("## Tech Stack"), "should have Tech Stack section");
416
- assert.ok(formatted.includes("## Module Structure"), "should have Module Structure section");
417
- assert.ok(formatted.includes("## Code Patterns"), "should have Code Patterns section");
418
- assert.ok(formatted.includes("javascript/typescript"), "should include language");
419
- assert.ok(formatted.includes("npm"), "should include package manager");
420
- assert.ok(formatted.includes("async/await"), "should include async style");
421
- assert.ok(formatted.includes("try/catch"), "should include error handling");
422
- assert.ok(formatted.includes("camelCase"), "should include naming convention");
423
- assert.ok(formatted.includes("3 async/await files"), "should include file counts for async style");
424
- assert.ok(formatted.includes("2 try/catch files"), "should include file counts for error handling");
425
- });
426
-
427
- test("formatCodebaseBrief: caps output at 3000 chars", async (t) => {
428
- // Create a brief with many files to exceed the limit
429
- const manyFiles = Array.from({ length: 100 }, (_, i) => `file-${i}.ts`);
430
-
431
- const brief: CodebaseBrief = {
432
- techStack: {
433
- primaryLanguage: "javascript/typescript",
434
- detectedFiles: manyFiles,
435
- packageManager: "npm",
436
- isMonorepo: false,
437
- hasTests: true,
438
- hasCI: true,
439
- },
440
- moduleStructure: {
441
- topLevelDirs: Array.from({ length: 50 }, (_, i) => `dir-${i}`),
442
- srcSubdirs: Array.from({ length: 50 }, (_, i) => `subdir-${i}`),
443
- totalFilesSampled: 100,
444
- },
445
- patterns: {
446
- asyncStyle: "async/await",
447
- errorHandling: "try/catch",
448
- namingConvention: "camelCase",
449
- evidence: {
450
- asyncStyle: manyFiles.map((f) => `${f}: async/await (10 occurrences)`),
451
- errorHandling: manyFiles.map((f) => `${f}: try/catch (5 occurrences)`),
452
- namingConvention: ["camelCase: 500 occurrences"],
453
- },
454
- fileCounts: {
455
- asyncAwait: 50,
456
- promises: 10,
457
- callbacks: 5,
458
- tryCatch: 30,
459
- errorCallbacks: 5,
460
- resultTypes: 0,
461
- },
462
- },
463
- sampledFiles: manyFiles,
464
- };
465
-
466
- const formatted = formatCodebaseBrief(brief);
467
-
468
- assert.ok(
469
- formatted.length <= 3000,
470
- `should cap at 3000 chars, got ${formatted.length}`,
471
- );
472
- if (formatted.length === 3000) {
473
- assert.ok(formatted.endsWith("..."), "should end with ellipsis when truncated");
474
- }
475
- });
476
-
477
- test("formatCodebaseBrief: handles minimal brief", async (t) => {
478
- const brief: CodebaseBrief = {
479
- techStack: {
480
- primaryLanguage: undefined,
481
- detectedFiles: [],
482
- packageManager: undefined,
483
- isMonorepo: false,
484
- hasTests: false,
485
- hasCI: false,
486
- },
487
- moduleStructure: {
488
- topLevelDirs: [],
489
- srcSubdirs: [],
490
- totalFilesSampled: 0,
491
- },
492
- patterns: {
493
- asyncStyle: "unknown",
494
- errorHandling: "unknown",
495
- namingConvention: "unknown",
496
- evidence: {
497
- asyncStyle: [],
498
- errorHandling: [],
499
- namingConvention: [],
500
- },
501
- fileCounts: {
502
- asyncAwait: 0,
503
- promises: 0,
504
- callbacks: 0,
505
- tryCatch: 0,
506
- errorCallbacks: 0,
507
- resultTypes: 0,
508
- },
509
- },
510
- sampledFiles: [],
511
- };
512
-
513
- const formatted = formatCodebaseBrief(brief);
514
-
515
- assert.ok(formatted.includes("## Tech Stack"), "should still have sections");
516
- assert.ok(formatted.includes("**Monorepo:** No"), "should show monorepo status");
517
- assert.ok(formatted.includes("unknown"), "should show unknown patterns");
518
- });
519
-
520
- // ─── Integration: Brief includes PROJECT_FILES markers ──────────────────────────
521
-
522
- test("analyzeCodebase: brief includes detected files from PROJECT_FILES", async (t) => {
523
- const dir = makeTempDir("project-files");
524
- t.after(() => cleanup(dir));
525
-
526
- // Create several PROJECT_FILES markers
527
- writeFileSync(join(dir, "package.json"), '{"name": "test"}', "utf-8");
528
- writeFileSync(join(dir, "tsconfig.json"), '{}', "utf-8");
529
- mkdirSync(join(dir, ".github", "workflows"), { recursive: true });
530
- writeFileSync(
531
- join(dir, ".github", "workflows", "ci.yml"),
532
- "name: CI",
533
- "utf-8",
534
- );
535
-
536
- const brief = await analyzeCodebase(dir);
537
-
538
- assert.ok(
539
- brief.techStack.detectedFiles.includes("package.json"),
540
- "should detect package.json",
541
- );
542
- assert.ok(
543
- brief.techStack.hasCI,
544
- "should detect CI from .github/workflows",
545
- );
546
- });
547
-
548
- test("analyzeCodebase: brief includes sampled file patterns", async (t) => {
549
- const dir = makeTempDir("sampled-patterns");
550
- t.after(() => cleanup(dir));
551
-
552
- mkdirSync(join(dir, "src"), { recursive: true });
553
-
554
- // Write files with distinct patterns
555
- writeFileSync(
556
- join(dir, "src", "async-heavy.ts"),
557
- `
558
- async function one() { await fetch('/a'); }
559
- async function two() { await fetch('/b'); }
560
- async function three() { await fetch('/c'); }
561
- `,
562
- "utf-8",
563
- );
564
-
565
- const brief = await analyzeCodebase(dir);
566
-
567
- assert.ok(brief.sampledFiles.length > 0, "should have sampled files");
568
- assert.ok(
569
- brief.patterns.evidence.asyncStyle.length > 0,
570
- "should have async style evidence",
571
- );
572
- });
573
-
574
- // ─── aggregatePriorContext ──────────────────────────────────────────────────────
575
-
576
- test("aggregatePriorContext: handles missing files gracefully", async (t) => {
577
- const dir = makeTempDir("no-gsd");
578
- t.after(() => cleanup(dir));
579
-
580
- // Create .gsd directory but no files
581
- mkdirSync(join(dir, ".gsd"), { recursive: true });
582
-
583
- const brief = await aggregatePriorContext(dir);
584
-
585
- assert.equal(brief.decisions.totalCount, 0, "should have no decisions");
586
- assert.equal(brief.requirements.totalCount, 0, "should have no requirements");
587
- assert.equal(brief.knowledge, "No prior knowledge recorded.", "should indicate no knowledge");
588
- assert.equal(brief.summaries, "No prior milestone summaries.", "should indicate no summaries");
589
- });
590
-
591
- test("aggregatePriorContext: handles completely empty directory", async (t) => {
592
- const dir = makeTempDir("empty-project");
593
- t.after(() => cleanup(dir));
594
-
595
- const brief = await aggregatePriorContext(dir);
596
-
597
- assert.equal(brief.decisions.totalCount, 0);
598
- assert.equal(brief.requirements.totalCount, 0);
599
- assert.equal(brief.knowledge, "No prior knowledge recorded.");
600
- assert.equal(brief.summaries, "No prior milestone summaries.");
601
- });
602
-
603
- test("aggregatePriorContext: parses DECISIONS.md and groups by scope", async (t) => {
604
- const dir = makeTempDir("decisions");
605
- t.after(() => cleanup(dir));
606
-
607
- mkdirSync(join(dir, ".gsd"), { recursive: true });
608
- writeFileSync(
609
- join(dir, ".gsd", "DECISIONS.md"),
610
- `# Decisions Register
611
-
612
- | # | When | Scope | Decision | Choice | Rationale | Revisable? | Made By |
613
- |---|------|-------|----------|--------|-----------|------------|---------|
614
- | D001 | M001/S01 | pattern | Async style | async/await | Modern standard | Yes | agent |
615
- | D002 | M001/S02 | architecture | Data layer | SQLite | Simple, embedded | No | human |
616
- | D003 | M001/S03 | pattern | Error handling | try/catch | Consistency | Yes | agent |
617
- `,
618
- "utf-8",
619
- );
620
-
621
- const brief = await aggregatePriorContext(dir);
622
-
623
- assert.equal(brief.decisions.totalCount, 3, "should parse all decisions");
624
- assert.equal(brief.decisions.byScope.get("pattern")?.length, 2, "should group pattern scope");
625
- assert.equal(brief.decisions.byScope.get("architecture")?.length, 1, "should group architecture scope");
626
-
627
- const patternDecisions = brief.decisions.byScope.get("pattern")!;
628
- assert.equal(patternDecisions[0].id, "D001");
629
- assert.equal(patternDecisions[0].decision, "Async style");
630
- assert.equal(patternDecisions[0].choice, "async/await");
631
- });
632
-
633
- test("aggregatePriorContext: parses REQUIREMENTS.md and groups by status", async (t) => {
634
- const dir = makeTempDir("requirements");
635
- t.after(() => cleanup(dir));
636
-
637
- mkdirSync(join(dir, ".gsd"), { recursive: true });
638
- writeFileSync(
639
- join(dir, ".gsd", "REQUIREMENTS.md"),
640
- `# Requirements
641
-
642
- ## Active
643
-
644
- ### R001 — First requirement
645
- - Status: active
646
- - Description: Something active
647
-
648
- ### R002 — Second requirement
649
- - Status: active
650
- - Description: Also active
651
-
652
- ## Validated
653
-
654
- ### R003 — Validated requirement
655
- - Status: validated
656
- - Description: This was validated
657
-
658
- ## Deferred
659
-
660
- ### R004 — Deferred requirement
661
- - Status: deferred
662
- - Description: Postponed for later
663
- `,
664
- "utf-8",
665
- );
666
-
667
- const brief = await aggregatePriorContext(dir);
668
-
669
- assert.equal(brief.requirements.totalCount, 4, "should parse all requirements");
670
- assert.equal(brief.requirements.active.length, 2, "should have 2 active");
671
- assert.equal(brief.requirements.validated.length, 1, "should have 1 validated");
672
- assert.equal(brief.requirements.deferred.length, 1, "should have 1 deferred");
673
-
674
- assert.equal(brief.requirements.active[0].id, "R001");
675
- assert.equal(brief.requirements.active[0].description, "First requirement");
676
- });
677
-
678
- test("aggregatePriorContext: loads KNOWLEDGE.md content", async (t) => {
679
- const dir = makeTempDir("knowledge");
680
- t.after(() => cleanup(dir));
681
-
682
- mkdirSync(join(dir, ".gsd"), { recursive: true });
683
- writeFileSync(
684
- join(dir, ".gsd", "KNOWLEDGE.md"),
685
- `# Knowledge Base
686
-
687
- ## Rules
688
-
689
- | # | Scope | Rule | Why | Added |
690
- |---|-------|------|-----|-------|
691
- | K001 | global | Always use TypeScript | Type safety | manual |
692
-
693
- ## Patterns
694
-
695
- **Pattern X:** Do this for better Y.
696
- `,
697
- "utf-8",
698
- );
699
-
700
- const brief = await aggregatePriorContext(dir);
701
-
702
- assert.ok(brief.knowledge.includes("Rules"), "should include knowledge content");
703
- assert.ok(brief.knowledge.includes("TypeScript"), "should include rule text");
704
- });
705
-
706
- test("aggregatePriorContext: truncates oversized content without cutting mid-section", async (t) => {
707
- const dir = makeTempDir("large-knowledge");
708
- t.after(() => cleanup(dir));
709
-
710
- mkdirSync(join(dir, ".gsd"), { recursive: true });
711
-
712
- // Create large knowledge file
713
- const largeContent = `# Knowledge Base
714
-
715
- ## Section One
716
-
717
- ${"Lorem ipsum dolor sit amet. ".repeat(100)}
718
-
719
- ## Section Two
720
-
721
- ${"More content here. ".repeat(100)}
722
-
723
- ## Section Three
724
-
725
- ${"Even more content. ".repeat(100)}
726
- `;
727
-
728
- writeFileSync(join(dir, ".gsd", "KNOWLEDGE.md"), largeContent, "utf-8");
729
-
730
- const brief = await aggregatePriorContext(dir);
731
-
732
- assert.ok(brief.knowledge.length <= 2000, "should truncate to 2K chars");
733
- assert.ok(brief.knowledge.includes("[truncated]"), "should indicate truncation");
734
- // Should try to preserve section boundaries
735
- assert.ok(
736
- brief.knowledge.includes("## Section"),
737
- "should keep section headings intact",
738
- );
739
- });
740
-
741
- test("aggregatePriorContext: loads milestone summaries", async (t) => {
742
- const dir = makeTempDir("milestones");
743
- t.after(() => cleanup(dir));
744
-
745
- mkdirSync(join(dir, ".gsd", "milestones", "M001"), { recursive: true });
746
- mkdirSync(join(dir, ".gsd", "milestones", "M002"), { recursive: true });
747
-
748
- writeFileSync(
749
- join(dir, ".gsd", "milestones", "M001", "MILESTONE-SUMMARY.md"),
750
- `# M001 — First Milestone
751
-
752
- **Implemented core functionality and established patterns.**
753
-
754
- ## What Happened
755
- Did stuff.
756
- `,
757
- "utf-8",
758
- );
759
-
760
- writeFileSync(
761
- join(dir, ".gsd", "milestones", "M002", "MILESTONE-SUMMARY.md"),
762
- `# M002 — Second Milestone
763
-
764
- **Extended the system with new features.**
765
-
766
- ## What Happened
767
- Did more stuff.
768
- `,
769
- "utf-8",
770
- );
771
-
772
- const brief = await aggregatePriorContext(dir);
773
-
774
- assert.ok(brief.summaries.includes("M001"), "should include M001 summary");
775
- assert.ok(brief.summaries.includes("M002"), "should include M002 summary");
776
- assert.ok(
777
- brief.summaries.includes("core functionality"),
778
- "should extract one-liner from M001",
779
- );
780
- assert.ok(
781
- brief.summaries.includes("new features"),
782
- "should extract one-liner from M002",
783
- );
784
- });
785
-
786
- // ─── formatPriorContextBrief ────────────────────────────────────────────────────
787
-
788
- test("formatPriorContextBrief: produces markdown with all sections", async (t) => {
789
- const brief: PriorContextBrief = {
790
- decisions: {
791
- byScope: new Map([
792
- [
793
- "pattern",
794
- [
795
- { id: "D001", scope: "pattern", decision: "Async", choice: "await", rationale: "Modern" },
796
- ],
797
- ],
798
- [
799
- "architecture",
800
- [
801
- { id: "D002", scope: "architecture", decision: "DB", choice: "SQLite", rationale: "Simple" },
802
- ],
803
- ],
804
- ]),
805
- totalCount: 2,
806
- },
807
- requirements: {
808
- active: [{ id: "R001", description: "Core feature", status: "active" }],
809
- validated: [],
810
- deferred: [],
811
- totalCount: 1,
812
- },
813
- knowledge: "Some knowledge here.",
814
- summaries: "### M001\nDid things.",
815
- };
816
-
817
- const formatted = formatPriorContextBrief(brief);
818
-
819
- assert.ok(formatted.includes("## Prior Decisions"), "should have decisions section");
820
- assert.ok(formatted.includes("## Prior Requirements"), "should have requirements section");
821
- assert.ok(formatted.includes("## Prior Knowledge"), "should have knowledge section");
822
- assert.ok(formatted.includes("## Prior Milestone Summaries"), "should have summaries section");
823
- assert.ok(formatted.includes("D001"), "should include decision ID");
824
- assert.ok(formatted.includes("R001"), "should include requirement ID");
825
- assert.ok(formatted.includes("pattern"), "should include scope heading");
826
- });
827
-
828
- test("formatPriorContextBrief: handles empty brief", async (t) => {
829
- const brief: PriorContextBrief = {
830
- decisions: {
831
- byScope: new Map(),
832
- totalCount: 0,
833
- },
834
- requirements: {
835
- active: [],
836
- validated: [],
837
- deferred: [],
838
- totalCount: 0,
839
- },
840
- knowledge: "No prior knowledge recorded.",
841
- summaries: "No prior milestone summaries.",
842
- };
843
-
844
- const formatted = formatPriorContextBrief(brief);
845
-
846
- assert.ok(formatted.includes("No prior decisions recorded"), "should indicate no decisions");
847
- assert.ok(formatted.includes("No prior requirements recorded"), "should indicate no requirements");
848
- assert.ok(formatted.includes("No prior knowledge recorded"), "should indicate no knowledge");
849
- assert.ok(formatted.includes("No prior milestone summaries"), "should indicate no summaries");
850
- });
851
-
852
- test("formatPriorContextBrief: caps total output at 6K chars", async (t) => {
853
- // Create a brief with lots of content
854
- const manyDecisions: Array<{
855
- id: string;
856
- scope: string;
857
- decision: string;
858
- choice: string;
859
- rationale: string;
860
- }> = [];
861
- for (let i = 0; i < 100; i++) {
862
- manyDecisions.push({
863
- id: `D${String(i).padStart(3, "0")}`,
864
- scope: "pattern",
865
- decision: `Decision number ${i} with some extra text for length`,
866
- choice: `Choice ${i} with more text to make it longer`,
867
- rationale: `Rationale ${i}`,
868
- });
869
- }
870
-
871
- const manyRequirements: Array<{
872
- id: string;
873
- description: string;
874
- status: "active";
875
- }> = [];
876
- for (let i = 0; i < 100; i++) {
877
- manyRequirements.push({
878
- id: `R${String(i).padStart(3, "0")}`,
879
- description: `Requirement ${i} with a long description that takes up space`,
880
- status: "active",
881
- });
882
- }
883
-
884
- const brief: PriorContextBrief = {
885
- decisions: {
886
- byScope: new Map([["pattern", manyDecisions]]),
887
- totalCount: 100,
888
- },
889
- requirements: {
890
- active: manyRequirements,
891
- validated: [],
892
- deferred: [],
893
- totalCount: 100,
894
- },
895
- knowledge: "A ".repeat(1000),
896
- summaries: "B ".repeat(1000),
897
- };
898
-
899
- const formatted = formatPriorContextBrief(brief);
900
-
901
- assert.ok(formatted.length <= 6000, `should cap at 6000 chars, got ${formatted.length}`);
902
- });
903
-
904
- // ─── researchEcosystem ──────────────────────────────────────────────────────────
905
- // Note: Ecosystem research now always returns available: false from the preparation
906
- // phase. Research happens during the discussion using web search tools.
907
-
908
- test("researchEcosystem: always returns available: false (research happens during discussion)", async (t) => {
909
- const dir = makeTempDir("ecosystem-disabled");
910
- t.after(() => cleanup(dir));
911
-
912
- const brief = await researchEcosystem(["Next.js", "TypeScript"], dir);
913
-
914
- assert.equal(brief.available, false, "should indicate research not available from preparation");
915
- assert.ok(brief.skippedReason, "should have skipped reason");
916
- assert.ok(
917
- brief.skippedReason!.includes("during the discussion"),
918
- "should explain research happens during discussion",
919
- );
920
- assert.deepEqual(brief.queries, [], "should have empty queries");
921
- assert.deepEqual(brief.findings, [], "should have empty findings");
922
- });
923
-
924
- test("researchEcosystem: returns consistent result regardless of tech stack", async (t) => {
925
- const dir = makeTempDir("ecosystem-consistent");
926
- t.after(() => cleanup(dir));
927
-
928
- // With tech stack
929
- const briefWithTech = await researchEcosystem(["React", "Next.js"], dir);
930
- // Without tech stack
931
- const briefEmpty = await researchEcosystem([], dir);
932
-
933
- // Both should return the same unavailable result
934
- assert.equal(briefWithTech.available, false);
935
- assert.equal(briefEmpty.available, false);
936
- assert.deepEqual(briefWithTech.queries, []);
937
- assert.deepEqual(briefEmpty.queries, []);
938
- });
939
-
940
- // ─── formatEcosystemBrief ─��─────────────────────────────────────────────────────
941
- // Note: formatEcosystemBrief now returns a simple fixed message since ecosystem
942
- // research always returns unavailable from the preparation phase.
943
-
944
- test("formatEcosystemBrief: returns simplified message for discussion-phase research", async (t) => {
945
- const brief: EcosystemBrief = {
946
- available: false,
947
- queries: [],
948
- findings: [],
949
- skippedReason: "Ecosystem research is performed during the discussion using web search tools, not during preparation.",
950
- };
951
-
952
- const formatted = formatEcosystemBrief(brief);
953
-
954
- assert.ok(formatted.includes("## Ecosystem Research"), "should have section header");
955
- assert.ok(formatted.includes("during the discussion"), "should mention discussion phase");
956
- assert.ok(formatted.includes("web search tools"), "should mention web search tools");
957
- });
958
-
959
- test("formatEcosystemBrief: returns consistent output regardless of brief content", async (t) => {
960
- // Even if a brief has findings (which shouldn't happen from preparation),
961
- // the function returns the simplified message
962
- const briefWithFindings: EcosystemBrief = {
963
- available: true,
964
- queries: ["test query"],
965
- findings: [{ query: "test", title: "Test", snippet: "test", url: "https://example.com" }],
966
- provider: "tavily",
967
- };
968
-
969
- const briefEmpty: EcosystemBrief = {
970
- available: false,
971
- queries: [],
972
- findings: [],
973
- skippedReason: "Test reason",
974
- };
975
-
976
- const formatted1 = formatEcosystemBrief(briefWithFindings);
977
- const formatted2 = formatEcosystemBrief(briefEmpty);
978
-
979
- // Both should return the same simplified message
980
- assert.equal(formatted1, formatted2, "should return consistent output");
981
- assert.ok(formatted1.includes("## Ecosystem Research"), "should have section header");
982
- });
983
-
984
-
985
- // ─── runPreparation (Orchestrator) ──────────────────────────────────────────────
986
-
987
- /**
988
- * Mock UI context that captures notifications for testing.
989
- */
990
- function createMockUI(): PreparationUIContext & { notifications: Array<{ message: string; type?: string }> } {
991
- const notifications: Array<{ message: string; type?: string }> = [];
992
- return {
993
- notifications,
994
- notify(message: string, type?: "info" | "warning" | "error" | "success") {
995
- notifications.push({ message, type });
996
- },
997
- };
998
- }
999
-
1000
- test("runPreparation: returns complete result with all briefs populated", async (t) => {
1001
- const dir = makeTempDir("runprep-full");
1002
- t.after(() => cleanup(dir));
1003
-
1004
- // Set up a minimal project
1005
- mkdirSync(join(dir, "src"), { recursive: true });
1006
- mkdirSync(join(dir, ".gsd"), { recursive: true });
1007
- writeFileSync(join(dir, "package.json"), '{"name": "test-project"}', "utf-8");
1008
- writeFileSync(join(dir, "src", "index.ts"), 'export const x = 1;', "utf-8");
1009
-
1010
- const ui = createMockUI();
1011
- const prefs: PreparationPreferences = {
1012
- discuss_preparation: true,
1013
- discuss_web_research: false, // Skip web research to avoid API key requirement
1014
- discuss_depth: "standard",
1015
- };
1016
-
1017
- const result = await runPreparation(dir, ui, prefs);
1018
-
1019
- // Check result structure
1020
- assert.equal(result.enabled, true, "should be enabled");
1021
- assert.ok(result.codebase, "should have codebase");
1022
- assert.ok(result.priorContext, "should have priorContext");
1023
- assert.ok(result.ecosystem, "should have ecosystem");
1024
- assert.ok(typeof result.codebaseBrief === "string", "should have codebaseBrief");
1025
- assert.ok(typeof result.priorContextBrief === "string", "should have priorContextBrief");
1026
- assert.ok(typeof result.ecosystemBrief === "string", "should have ecosystemBrief");
1027
- assert.ok(result.durationMs > 0, "should have positive duration");
1028
- assert.equal(result.ecosystemResearchPerformed, false, "should not have performed ecosystem research");
1029
-
1030
- // Check TUI progress notifications
1031
- assert.ok(ui.notifications.length > 0, "should have notifications");
1032
- assert.ok(
1033
- ui.notifications.some((n) => n.message.includes("Analyzing codebase")),
1034
- "should show codebase analysis start",
1035
- );
1036
- assert.ok(
1037
- ui.notifications.some((n) => n.message.includes("✓ Analyzed codebase")),
1038
- "should show codebase analysis complete",
1039
- );
1040
- assert.ok(
1041
- ui.notifications.some((n) => n.message.includes("Reviewing prior context")),
1042
- "should show prior context start",
1043
- );
1044
- assert.ok(
1045
- ui.notifications.some((n) => n.message.includes("✓ Reviewed prior context")),
1046
- "should show prior context complete",
1047
- );
1048
- });
1049
-
1050
- test("runPreparation: returns early when discuss_preparation is false", async (t) => {
1051
- const dir = makeTempDir("runprep-disabled");
1052
- t.after(() => cleanup(dir));
1053
-
1054
- const ui = createMockUI();
1055
- const prefs: PreparationPreferences = {
1056
- discuss_preparation: false,
1057
- };
1058
-
1059
- const result = await runPreparation(dir, ui, prefs);
1060
-
1061
- assert.equal(result.enabled, false, "should indicate preparation disabled");
1062
- assert.equal(result.codebaseBrief, "", "should have empty codebase brief");
1063
- assert.equal(result.priorContextBrief, "", "should have empty prior context brief");
1064
- assert.equal(result.ecosystemBrief, "", "should have empty ecosystem brief");
1065
- assert.equal(ui.notifications.length, 0, "should not show any notifications");
1066
- assert.ok(result.durationMs >= 0, "should have non-negative duration");
1067
- });
1068
-
1069
- test("runPreparation: ecosystem research always returns unavailable (happens during discussion)", async (t) => {
1070
- const dir = makeTempDir("runprep-no-ecosystem");
1071
- t.after(() => cleanup(dir));
1072
-
1073
- mkdirSync(join(dir, ".gsd"), { recursive: true });
1074
- writeFileSync(join(dir, "package.json"), '{"name": "test"}', "utf-8");
1075
-
1076
- const ui = createMockUI();
1077
- const prefs: PreparationPreferences = {
1078
- discuss_preparation: true,
1079
- discuss_web_research: true, // Even with this enabled, ecosystem research returns unavailable
1080
- };
1081
-
1082
- const result = await runPreparation(dir, ui, prefs);
1083
-
1084
- assert.equal(result.enabled, true);
1085
- assert.equal(result.ecosystemResearchPerformed, false, "should not perform ecosystem research from preparation");
1086
- assert.equal(result.ecosystem.available, false);
1087
- assert.ok(
1088
- result.ecosystem.skippedReason?.includes("during the discussion"),
1089
- "should indicate research happens during discussion",
1090
- );
1091
-
1092
- // Should NOT have ecosystem research notifications (no longer part of preparation)
1093
- assert.ok(
1094
- !ui.notifications.some((n) => n.message.includes("Researching ecosystem")),
1095
- "should not show ecosystem research notification",
1096
- );
1097
- });
1098
-
1099
- test("runPreparation: works without UI context (silent mode)", async (t) => {
1100
- const dir = makeTempDir("runprep-silent");
1101
- t.after(() => cleanup(dir));
1102
-
1103
- mkdirSync(join(dir, ".gsd"), { recursive: true });
1104
- writeFileSync(join(dir, "package.json"), '{"name": "test"}', "utf-8");
1105
-
1106
- const prefs: PreparationPreferences = {
1107
- discuss_preparation: true,
1108
- discuss_web_research: false,
1109
- };
1110
-
1111
- // Pass null for UI to test silent mode
1112
- const result = await runPreparation(dir, null, prefs);
1113
-
1114
- assert.equal(result.enabled, true, "should work without UI");
1115
- assert.ok(result.codebase, "should have codebase");
1116
- assert.ok(result.priorContext, "should have priorContext");
1117
- assert.ok(result.durationMs > 0, "should have duration");
1118
- });
1119
-
1120
- test("runPreparation: completes within 60s requirement (R112)", async (t) => {
1121
- const dir = makeTempDir("runprep-timing");
1122
- t.after(() => cleanup(dir));
1123
-
1124
- // Create a project with some content to analyze
1125
- mkdirSync(join(dir, "src"), { recursive: true });
1126
- mkdirSync(join(dir, ".gsd"), { recursive: true });
1127
- writeFileSync(join(dir, "package.json"), '{"name": "test"}', "utf-8");
1128
- writeFileSync(join(dir, "tsconfig.json"), '{}', "utf-8");
1129
-
1130
- for (let i = 0; i < 10; i++) {
1131
- writeFileSync(
1132
- join(dir, "src", `file${i}.ts`),
1133
- `export async function fn${i}() { await Promise.resolve(); }\n`.repeat(50),
1134
- "utf-8",
1135
- );
1136
- }
1137
-
1138
- const prefs: PreparationPreferences = {
1139
- discuss_preparation: true,
1140
- discuss_web_research: false,
1141
- discuss_depth: "standard",
1142
- };
1143
-
1144
- const startTime = performance.now();
1145
- const result = await runPreparation(dir, null, prefs);
1146
- const elapsed = performance.now() - startTime;
1147
-
1148
- assert.ok(result.durationMs < 60000, `should complete within 60s, took ${result.durationMs}ms`);
1149
- assert.ok(elapsed < 60000, `elapsed time should be under 60s, was ${elapsed}ms`);
1150
- });
1151
-
1152
- test("runPreparation: does not throw on any input", async (t) => {
1153
- const dir = makeTempDir("runprep-robust");
1154
- t.after(() => cleanup(dir));
1155
-
1156
- // Test with completely empty directory
1157
- const prefs: PreparationPreferences = {};
1158
-
1159
- let result: PreparationResult | undefined;
1160
- let error: unknown;
1161
-
1162
- try {
1163
- result = await runPreparation(dir, null, prefs);
1164
- } catch (e) {
1165
- error = e;
1166
- }
1167
-
1168
- assert.equal(error, undefined, "should not throw");
1169
- assert.ok(result, "should return result");
1170
- assert.equal(result!.enabled, true, "should be enabled by default");
1171
- });
1172
-
1173
- test("runPreparation: detects framework from config files in codebase brief", async (t) => {
1174
- const dir = makeTempDir("runprep-framework");
1175
- t.after(() => cleanup(dir));
1176
-
1177
- mkdirSync(join(dir, ".gsd"), { recursive: true });
1178
- writeFileSync(join(dir, "package.json"), '{"name": "test"}', "utf-8");
1179
- writeFileSync(join(dir, "next.config.mjs"), 'export default {};', "utf-8");
1180
-
1181
- const prefs: PreparationPreferences = {
1182
- discuss_preparation: true,
1183
- };
1184
-
1185
- const result = await runPreparation(dir, null, prefs);
1186
-
1187
- // Should detect Next.js config file in codebase analysis
1188
- assert.ok(
1189
- result.codebase.techStack.detectedFiles.includes("next.config.mjs"),
1190
- "should detect next.config.mjs in codebase brief",
1191
- );
1192
- // Ecosystem queries are always empty from preparation (research happens during discussion)
1193
- assert.deepEqual(result.ecosystem.queries, [], "ecosystem queries should be empty from preparation");
1194
- });
1195
-
1196
- test("runPreparation: default preferences enable preparation and web research", async (t) => {
1197
- const dir = makeTempDir("runprep-defaults");
1198
- t.after(() => cleanup(dir));
1199
-
1200
- mkdirSync(join(dir, ".gsd"), { recursive: true });
1201
-
1202
- const ui = createMockUI();
1203
- const prefs: PreparationPreferences = {}; // All defaults
1204
-
1205
- const result = await runPreparation(dir, ui, prefs);
1206
-
1207
- // With defaults, preparation should be enabled
1208
- assert.equal(result.enabled, true, "should be enabled by default");
1209
- // Notifications should be shown
1210
- assert.ok(ui.notifications.length > 0, "should show notifications");
1211
- });