gsd-pi 2.74.0-dev.2b524c3 → 2.74.0-dev.703eabc

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 (484) hide show
  1. package/dist/cli.js +85 -0
  2. package/dist/headless-query.js +4 -1
  3. package/dist/help-text.js +23 -0
  4. package/dist/resources/extensions/gsd/activity-log.js +16 -0
  5. package/dist/resources/extensions/gsd/auto/detect-stuck.js +11 -4
  6. package/dist/resources/extensions/gsd/auto/loop.js +147 -10
  7. package/dist/resources/extensions/gsd/auto/phases.js +158 -4
  8. package/dist/resources/extensions/gsd/auto/session.js +10 -0
  9. package/dist/resources/extensions/gsd/auto-dispatch.js +11 -1
  10. package/dist/resources/extensions/gsd/auto-model-selection.js +51 -5
  11. package/dist/resources/extensions/gsd/auto-post-unit.js +220 -17
  12. package/dist/resources/extensions/gsd/auto-prompts.js +12 -0
  13. package/dist/resources/extensions/gsd/auto-unit-closeout.js +18 -0
  14. package/dist/resources/extensions/gsd/auto-verification.js +100 -2
  15. package/dist/resources/extensions/gsd/auto.js +36 -4
  16. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +30 -8
  17. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +45 -4
  18. package/dist/resources/extensions/gsd/commands/catalog.js +26 -1
  19. package/dist/resources/extensions/gsd/commands/handlers/ops.js +25 -0
  20. package/dist/resources/extensions/gsd/commands/handlers/workflow.js +68 -9
  21. package/dist/resources/extensions/gsd/commands-add-tests.js +111 -0
  22. package/dist/resources/extensions/gsd/commands-backlog.js +140 -0
  23. package/dist/resources/extensions/gsd/commands-do.js +79 -0
  24. package/dist/resources/extensions/gsd/commands-extract-learnings.js +225 -0
  25. package/dist/resources/extensions/gsd/commands-maintenance.js +6 -6
  26. package/dist/resources/extensions/gsd/commands-pr-branch.js +180 -0
  27. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  28. package/dist/resources/extensions/gsd/commands-session-report.js +82 -0
  29. package/dist/resources/extensions/gsd/commands-ship.js +187 -0
  30. package/dist/resources/extensions/gsd/db-writer.js +3 -5
  31. package/dist/resources/extensions/gsd/docs/preferences-reference.md +14 -1
  32. package/dist/resources/extensions/gsd/ecosystem/gsd-extension-api.js +144 -0
  33. package/dist/resources/extensions/gsd/ecosystem/loader.js +145 -0
  34. package/dist/resources/extensions/gsd/git-service.js +49 -1
  35. package/dist/resources/extensions/gsd/graph-context.js +157 -0
  36. package/dist/resources/extensions/gsd/gsd-db.js +581 -2
  37. package/dist/resources/extensions/gsd/guided-flow.js +23 -0
  38. package/dist/resources/extensions/gsd/index.js +15 -2
  39. package/dist/resources/extensions/gsd/init-wizard.js +1 -0
  40. package/dist/resources/extensions/gsd/journal.js +27 -0
  41. package/dist/resources/extensions/gsd/md-importer.js +3 -4
  42. package/dist/resources/extensions/gsd/memory-store.js +19 -51
  43. package/dist/resources/extensions/gsd/metrics.js +19 -0
  44. package/dist/resources/extensions/gsd/milestone-validation-gates.js +13 -12
  45. package/dist/resources/extensions/gsd/native-git-bridge.js +7 -4
  46. package/dist/resources/extensions/gsd/parallel-orchestrator.js +33 -1
  47. package/dist/resources/extensions/gsd/preferences-models.js +20 -3
  48. package/dist/resources/extensions/gsd/preferences-types.js +1 -0
  49. package/dist/resources/extensions/gsd/preferences-validation.js +108 -2
  50. package/dist/resources/extensions/gsd/preferences.js +26 -0
  51. package/dist/resources/extensions/gsd/prompts/add-tests.md +35 -0
  52. package/dist/resources/extensions/gsd/slice-parallel-orchestrator.js +12 -2
  53. package/dist/resources/extensions/gsd/state.js +5 -1
  54. package/dist/resources/extensions/gsd/templates/PREFERENCES.md +18 -0
  55. package/dist/resources/extensions/gsd/tools/complete-slice.js +20 -0
  56. package/dist/resources/extensions/gsd/tools/validate-milestone.js +39 -4
  57. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +3 -14
  58. package/dist/resources/extensions/gsd/triage-resolution.js +2 -5
  59. package/dist/resources/extensions/gsd/unit-ownership.js +1 -1
  60. package/dist/resources/extensions/gsd/uok/audit-toggle.js +7 -0
  61. package/dist/resources/extensions/gsd/uok/audit.js +40 -0
  62. package/dist/resources/extensions/gsd/uok/contracts.js +1 -0
  63. package/dist/resources/extensions/gsd/uok/execution-graph.js +179 -0
  64. package/dist/resources/extensions/gsd/uok/flags.js +29 -0
  65. package/dist/resources/extensions/gsd/uok/gate-runner.js +109 -0
  66. package/dist/resources/extensions/gsd/uok/gitops.js +53 -0
  67. package/dist/resources/extensions/gsd/uok/kernel.js +80 -0
  68. package/dist/resources/extensions/gsd/uok/loop-adapter.js +133 -0
  69. package/dist/resources/extensions/gsd/uok/model-policy.js +66 -0
  70. package/dist/resources/extensions/gsd/uok/plan-v2.js +132 -0
  71. package/dist/resources/extensions/gsd/workflow-logger.js +22 -0
  72. package/dist/resources/extensions/gsd/workflow-manifest.js +8 -69
  73. package/dist/resources/extensions/gsd/workflow-migration.js +21 -22
  74. package/dist/resources/extensions/gsd/workflow-projections.js +4 -1
  75. package/dist/resources/extensions/gsd/workflow-reconcile.js +14 -11
  76. package/dist/resources/extensions/ttsr/ttsr-manager.js +3 -1
  77. package/dist/tsconfig.extensions.tsbuildinfo +1 -0
  78. package/dist/web/standalone/.next/BUILD_ID +1 -1
  79. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  80. package/dist/web/standalone/.next/build-manifest.json +2 -2
  81. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  82. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  83. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  84. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  85. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  86. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  87. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  88. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  89. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  90. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  91. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  92. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  93. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  94. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  95. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  96. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  97. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  98. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  99. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +1 -1
  103. package/dist/web/standalone/.next/server/app/index.html +1 -1
  104. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  105. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  106. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  107. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  108. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  109. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  110. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  111. package/dist/web/standalone/.next/server/chunks/6897.js +3 -3
  112. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  113. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  114. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  115. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  116. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  117. package/package.json +3 -2
  118. package/packages/daemon/package.json +2 -2
  119. package/packages/mcp-server/dist/index.d.ts +3 -0
  120. package/packages/mcp-server/dist/index.d.ts.map +1 -1
  121. package/packages/mcp-server/dist/index.js +3 -0
  122. package/packages/mcp-server/dist/index.js.map +1 -1
  123. package/packages/mcp-server/dist/readers/graph.d.ts +87 -0
  124. package/packages/mcp-server/dist/readers/graph.d.ts.map +1 -0
  125. package/packages/mcp-server/dist/readers/graph.js +655 -0
  126. package/packages/mcp-server/dist/readers/graph.js.map +1 -0
  127. package/packages/mcp-server/dist/readers/index.d.ts +2 -0
  128. package/packages/mcp-server/dist/readers/index.d.ts.map +1 -1
  129. package/packages/mcp-server/dist/readers/index.js +1 -0
  130. package/packages/mcp-server/dist/readers/index.js.map +1 -1
  131. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  132. package/packages/mcp-server/dist/server.js +65 -0
  133. package/packages/mcp-server/dist/server.js.map +1 -1
  134. package/packages/mcp-server/package.json +2 -2
  135. package/packages/mcp-server/src/index.ts +15 -0
  136. package/packages/mcp-server/src/readers/graph.test.ts +604 -0
  137. package/packages/mcp-server/src/readers/graph.ts +855 -0
  138. package/packages/mcp-server/src/readers/index.ts +12 -0
  139. package/packages/mcp-server/src/server.ts +83 -0
  140. package/packages/mcp-server/tsconfig.json +1 -0
  141. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -0
  142. package/packages/native/package.json +2 -2
  143. package/packages/native/tsconfig.tsbuildinfo +1 -0
  144. package/packages/pi-agent-core/package.json +1 -1
  145. package/packages/pi-agent-core/tsconfig.json +1 -0
  146. package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -0
  147. package/packages/pi-ai/dist/index.d.ts +1 -9
  148. package/packages/pi-ai/dist/index.d.ts.map +1 -1
  149. package/packages/pi-ai/dist/index.js +1 -9
  150. package/packages/pi-ai/dist/index.js.map +1 -1
  151. package/packages/pi-ai/dist/models/capability-patches.d.ts +19 -0
  152. package/packages/pi-ai/dist/models/capability-patches.d.ts.map +1 -0
  153. package/packages/pi-ai/dist/models/capability-patches.js +36 -0
  154. package/packages/pi-ai/dist/models/capability-patches.js.map +1 -0
  155. package/packages/pi-ai/dist/{models.custom.d.ts → models/custom.d.ts} +1 -1
  156. package/packages/pi-ai/dist/models/custom.d.ts.map +1 -0
  157. package/packages/pi-ai/dist/{models.custom.js → models/custom.js} +4 -4
  158. package/packages/pi-ai/dist/models/custom.js.map +1 -0
  159. package/packages/pi-ai/dist/models/generated/amazon-bedrock.d.ts +1482 -0
  160. package/packages/pi-ai/dist/models/generated/amazon-bedrock.d.ts.map +1 -0
  161. package/packages/pi-ai/dist/models/generated/amazon-bedrock.js +1484 -0
  162. package/packages/pi-ai/dist/models/generated/amazon-bedrock.js.map +1 -0
  163. package/packages/pi-ai/dist/models/generated/anthropic.d.ts +377 -0
  164. package/packages/pi-ai/dist/models/generated/anthropic.d.ts.map +1 -0
  165. package/packages/pi-ai/dist/models/generated/anthropic.js +379 -0
  166. package/packages/pi-ai/dist/models/generated/anthropic.js.map +1 -0
  167. package/packages/pi-ai/dist/models/generated/azure-openai-responses.d.ts +700 -0
  168. package/packages/pi-ai/dist/models/generated/azure-openai-responses.d.ts.map +1 -0
  169. package/packages/pi-ai/dist/models/generated/azure-openai-responses.js +702 -0
  170. package/packages/pi-ai/dist/models/generated/azure-openai-responses.js.map +1 -0
  171. package/packages/pi-ai/dist/models/generated/cerebras.d.ts +71 -0
  172. package/packages/pi-ai/dist/models/generated/cerebras.d.ts.map +1 -0
  173. package/packages/pi-ai/dist/models/generated/cerebras.js +73 -0
  174. package/packages/pi-ai/dist/models/generated/cerebras.js.map +1 -0
  175. package/packages/pi-ai/dist/models/generated/github-copilot.d.ts +590 -0
  176. package/packages/pi-ai/dist/models/generated/github-copilot.d.ts.map +1 -0
  177. package/packages/pi-ai/dist/models/generated/github-copilot.js +444 -0
  178. package/packages/pi-ai/dist/models/generated/github-copilot.js.map +1 -0
  179. package/packages/pi-ai/dist/models/generated/google-antigravity.d.ts +156 -0
  180. package/packages/pi-ai/dist/models/generated/google-antigravity.d.ts.map +1 -0
  181. package/packages/pi-ai/dist/models/generated/google-antigravity.js +158 -0
  182. package/packages/pi-ai/dist/models/generated/google-antigravity.js.map +1 -0
  183. package/packages/pi-ai/dist/models/generated/google-gemini-cli.d.ts +105 -0
  184. package/packages/pi-ai/dist/models/generated/google-gemini-cli.d.ts.map +1 -0
  185. package/packages/pi-ai/dist/models/generated/google-gemini-cli.js +107 -0
  186. package/packages/pi-ai/dist/models/generated/google-gemini-cli.js.map +1 -0
  187. package/packages/pi-ai/dist/models/generated/google-vertex.d.ts +207 -0
  188. package/packages/pi-ai/dist/models/generated/google-vertex.d.ts.map +1 -0
  189. package/packages/pi-ai/dist/models/generated/google-vertex.js +209 -0
  190. package/packages/pi-ai/dist/models/generated/google-vertex.js.map +1 -0
  191. package/packages/pi-ai/dist/models/generated/google.d.ts +462 -0
  192. package/packages/pi-ai/dist/models/generated/google.d.ts.map +1 -0
  193. package/packages/pi-ai/dist/models/generated/google.js +464 -0
  194. package/packages/pi-ai/dist/models/generated/google.js.map +1 -0
  195. package/packages/pi-ai/dist/models/generated/groq.d.ts +309 -0
  196. package/packages/pi-ai/dist/models/generated/groq.d.ts.map +1 -0
  197. package/packages/pi-ai/dist/models/generated/groq.js +311 -0
  198. package/packages/pi-ai/dist/models/generated/groq.js.map +1 -0
  199. package/packages/pi-ai/dist/models/generated/huggingface.d.ts +383 -0
  200. package/packages/pi-ai/dist/models/generated/huggingface.d.ts.map +1 -0
  201. package/packages/pi-ai/dist/models/generated/huggingface.js +347 -0
  202. package/packages/pi-ai/dist/models/generated/huggingface.js.map +1 -0
  203. package/packages/pi-ai/dist/{models.generated.d.ts → models/generated/index.d.ts} +1 -1
  204. package/packages/pi-ai/dist/{models.generated.d.ts.map → models/generated/index.d.ts.map} +1 -1
  205. package/packages/pi-ai/dist/models/generated/index.js +51 -0
  206. package/packages/pi-ai/dist/models/generated/index.js.map +1 -0
  207. package/packages/pi-ai/dist/models/generated/kimi-coding.d.ts +37 -0
  208. package/packages/pi-ai/dist/models/generated/kimi-coding.d.ts.map +1 -0
  209. package/packages/pi-ai/dist/models/generated/kimi-coding.js +39 -0
  210. package/packages/pi-ai/dist/models/generated/kimi-coding.js.map +1 -0
  211. package/packages/pi-ai/dist/models/generated/minimax-cn.d.ts +105 -0
  212. package/packages/pi-ai/dist/models/generated/minimax-cn.d.ts.map +1 -0
  213. package/packages/pi-ai/dist/models/generated/minimax-cn.js +107 -0
  214. package/packages/pi-ai/dist/models/generated/minimax-cn.js.map +1 -0
  215. package/packages/pi-ai/dist/models/generated/minimax.d.ts +105 -0
  216. package/packages/pi-ai/dist/models/generated/minimax.d.ts.map +1 -0
  217. package/packages/pi-ai/dist/models/generated/minimax.js +107 -0
  218. package/packages/pi-ai/dist/models/generated/minimax.js.map +1 -0
  219. package/packages/pi-ai/dist/models/generated/mistral.d.ts +445 -0
  220. package/packages/pi-ai/dist/models/generated/mistral.d.ts.map +1 -0
  221. package/packages/pi-ai/dist/models/generated/mistral.js +447 -0
  222. package/packages/pi-ai/dist/models/generated/mistral.js.map +1 -0
  223. package/packages/pi-ai/dist/models/generated/openai-codex.d.ts +139 -0
  224. package/packages/pi-ai/dist/models/generated/openai-codex.d.ts.map +1 -0
  225. package/packages/pi-ai/dist/models/generated/openai-codex.js +141 -0
  226. package/packages/pi-ai/dist/models/generated/openai-codex.js.map +1 -0
  227. package/packages/pi-ai/dist/models/generated/openai.d.ts +700 -0
  228. package/packages/pi-ai/dist/models/generated/openai.d.ts.map +1 -0
  229. package/packages/pi-ai/dist/models/generated/openai.js +702 -0
  230. package/packages/pi-ai/dist/models/generated/openai.js.map +1 -0
  231. package/packages/pi-ai/dist/models/generated/opencode-go.d.ts +122 -0
  232. package/packages/pi-ai/dist/models/generated/opencode-go.d.ts.map +1 -0
  233. package/packages/pi-ai/dist/models/generated/opencode-go.js +124 -0
  234. package/packages/pi-ai/dist/models/generated/opencode-go.js.map +1 -0
  235. package/packages/pi-ai/dist/models/generated/opencode.d.ts +530 -0
  236. package/packages/pi-ai/dist/models/generated/opencode.d.ts.map +1 -0
  237. package/packages/pi-ai/dist/models/generated/opencode.js +532 -0
  238. package/packages/pi-ai/dist/models/generated/opencode.js.map +1 -0
  239. package/packages/pi-ai/dist/models/generated/openrouter.d.ts +4270 -0
  240. package/packages/pi-ai/dist/models/generated/openrouter.d.ts.map +1 -0
  241. package/packages/pi-ai/dist/models/generated/openrouter.js +4272 -0
  242. package/packages/pi-ai/dist/models/generated/openrouter.js.map +1 -0
  243. package/packages/pi-ai/dist/models/generated/vercel-ai-gateway.d.ts +2604 -0
  244. package/packages/pi-ai/dist/models/generated/vercel-ai-gateway.d.ts.map +1 -0
  245. package/packages/pi-ai/dist/models/generated/vercel-ai-gateway.js +2606 -0
  246. package/packages/pi-ai/dist/models/generated/vercel-ai-gateway.js.map +1 -0
  247. package/packages/pi-ai/dist/models/generated/xai.d.ts +411 -0
  248. package/packages/pi-ai/dist/models/generated/xai.d.ts.map +1 -0
  249. package/packages/pi-ai/dist/models/generated/xai.js +413 -0
  250. package/packages/pi-ai/dist/models/generated/xai.js.map +1 -0
  251. package/packages/pi-ai/dist/models/generated/zai.d.ts +276 -0
  252. package/packages/pi-ai/dist/models/generated/zai.d.ts.map +1 -0
  253. package/packages/pi-ai/dist/models/generated/zai.js +239 -0
  254. package/packages/pi-ai/dist/models/generated/zai.js.map +1 -0
  255. package/packages/pi-ai/dist/models/index.d.ts +27 -0
  256. package/packages/pi-ai/dist/models/index.d.ts.map +1 -0
  257. package/packages/pi-ai/dist/models/index.js +80 -0
  258. package/packages/pi-ai/dist/models/index.js.map +1 -0
  259. package/packages/pi-ai/dist/models.d.ts +1 -36
  260. package/packages/pi-ai/dist/models.d.ts.map +1 -1
  261. package/packages/pi-ai/dist/models.generated.test.js +1 -2
  262. package/packages/pi-ai/dist/models.generated.test.js.map +1 -1
  263. package/packages/pi-ai/dist/models.js +3 -112
  264. package/packages/pi-ai/dist/models.js.map +1 -1
  265. package/packages/pi-ai/dist/models.test.js +6 -5
  266. package/packages/pi-ai/dist/models.test.js.map +1 -1
  267. package/packages/pi-ai/package.json +1 -1
  268. package/packages/pi-ai/scripts/generate-models.ts +74 -40
  269. package/packages/pi-ai/src/index.ts +1 -9
  270. package/packages/pi-ai/src/models/capability-patches.ts +40 -0
  271. package/packages/pi-ai/src/{models.custom.ts → models/custom.ts} +4 -4
  272. package/packages/pi-ai/src/models/generated/amazon-bedrock.ts +1486 -0
  273. package/packages/pi-ai/src/models/generated/anthropic.ts +381 -0
  274. package/packages/pi-ai/src/models/generated/azure-openai-responses.ts +704 -0
  275. package/packages/pi-ai/src/models/generated/cerebras.ts +75 -0
  276. package/packages/pi-ai/src/models/generated/github-copilot.ts +446 -0
  277. package/packages/pi-ai/src/models/generated/google-antigravity.ts +160 -0
  278. package/packages/pi-ai/src/models/generated/google-gemini-cli.ts +109 -0
  279. package/packages/pi-ai/src/models/generated/google-vertex.ts +211 -0
  280. package/packages/pi-ai/src/models/generated/google.ts +466 -0
  281. package/packages/pi-ai/src/models/generated/groq.ts +313 -0
  282. package/packages/pi-ai/src/models/generated/huggingface.ts +349 -0
  283. package/packages/pi-ai/src/models/generated/index.ts +52 -0
  284. package/packages/pi-ai/src/models/generated/kimi-coding.ts +41 -0
  285. package/packages/pi-ai/src/models/generated/minimax-cn.ts +109 -0
  286. package/packages/pi-ai/src/models/generated/minimax.ts +109 -0
  287. package/packages/pi-ai/src/models/generated/mistral.ts +449 -0
  288. package/packages/pi-ai/src/models/generated/openai-codex.ts +143 -0
  289. package/packages/pi-ai/src/models/generated/openai.ts +704 -0
  290. package/packages/pi-ai/src/models/generated/opencode-go.ts +126 -0
  291. package/packages/pi-ai/src/models/generated/opencode.ts +534 -0
  292. package/packages/pi-ai/src/models/generated/openrouter.ts +4274 -0
  293. package/packages/pi-ai/src/models/generated/vercel-ai-gateway.ts +2608 -0
  294. package/packages/pi-ai/src/models/generated/xai.ts +415 -0
  295. package/packages/pi-ai/src/models/generated/zai.ts +241 -0
  296. package/packages/pi-ai/src/models/index.ts +106 -0
  297. package/packages/pi-ai/src/models.generated.test.ts +1 -2
  298. package/packages/pi-ai/src/models.test.ts +6 -5
  299. package/packages/pi-ai/src/models.ts +3 -153
  300. package/packages/pi-ai/tsconfig.json +1 -0
  301. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -0
  302. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  303. package/packages/pi-coding-agent/dist/core/agent-session.js +8 -2
  304. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  305. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +472 -0
  306. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
  307. package/packages/pi-coding-agent/dist/core/model-registry-env-fallback.test.d.ts +2 -0
  308. package/packages/pi-coding-agent/dist/core/model-registry-env-fallback.test.d.ts.map +1 -0
  309. package/packages/pi-coding-agent/dist/core/model-registry-env-fallback.test.js +52 -0
  310. package/packages/pi-coding-agent/dist/core/model-registry-env-fallback.test.js.map +1 -0
  311. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  312. package/packages/pi-coding-agent/dist/core/model-registry.js +2 -2
  313. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  314. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js +11 -0
  315. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js.map +1 -1
  316. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts +1 -0
  317. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  318. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +23 -9
  319. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
  320. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts +11 -0
  321. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts.map +1 -0
  322. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js +47 -0
  323. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js.map +1 -0
  324. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  325. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +51 -8
  326. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  327. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  328. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js +22 -22
  329. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js.map +1 -1
  330. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  331. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +232 -18
  332. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  333. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-ordering.test.d.ts +2 -0
  334. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-ordering.test.d.ts.map +1 -0
  335. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-ordering.test.js +38 -0
  336. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-ordering.test.js.map +1 -0
  337. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +13 -0
  338. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  339. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +53 -6
  340. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  341. package/packages/pi-coding-agent/src/core/agent-session.ts +12 -6
  342. package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +612 -0
  343. package/packages/pi-coding-agent/src/core/model-registry-env-fallback.test.ts +59 -0
  344. package/packages/pi-coding-agent/src/core/model-registry.ts +2 -1
  345. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts +19 -0
  346. package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +25 -10
  347. package/packages/pi-coding-agent/src/modes/interactive/components/chat-frame.ts +67 -0
  348. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +66 -7
  349. package/packages/pi-coding-agent/src/modes/interactive/components/user-message.ts +23 -26
  350. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +298 -41
  351. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-ordering.test.ts +44 -0
  352. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +73 -6
  353. package/packages/pi-coding-agent/src/types/ambient-modules.d.ts +69 -0
  354. package/packages/pi-coding-agent/tsconfig.json +3 -2
  355. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -0
  356. package/packages/pi-tui/package.json +1 -1
  357. package/packages/pi-tui/tsconfig.json +1 -0
  358. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -0
  359. package/packages/rpc-client/package.json +1 -1
  360. package/packages/rpc-client/tsconfig.json +1 -0
  361. package/packages/rpc-client/tsconfig.tsbuildinfo +1 -0
  362. package/src/resources/extensions/gsd/activity-log.ts +21 -0
  363. package/src/resources/extensions/gsd/auto/detect-stuck.ts +12 -4
  364. package/src/resources/extensions/gsd/auto/loop-deps.ts +10 -0
  365. package/src/resources/extensions/gsd/auto/loop.ts +159 -10
  366. package/src/resources/extensions/gsd/auto/phases.ts +191 -4
  367. package/src/resources/extensions/gsd/auto/session.ts +10 -0
  368. package/src/resources/extensions/gsd/auto-dispatch.ts +16 -6
  369. package/src/resources/extensions/gsd/auto-model-selection.ts +66 -5
  370. package/src/resources/extensions/gsd/auto-post-unit.ts +238 -18
  371. package/src/resources/extensions/gsd/auto-prompts.ts +13 -0
  372. package/src/resources/extensions/gsd/auto-unit-closeout.ts +25 -1
  373. package/src/resources/extensions/gsd/auto-verification.ts +129 -2
  374. package/src/resources/extensions/gsd/auto.ts +41 -2
  375. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +38 -8
  376. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +56 -3
  377. package/src/resources/extensions/gsd/commands/catalog.ts +26 -1
  378. package/src/resources/extensions/gsd/commands/handlers/ops.ts +25 -0
  379. package/src/resources/extensions/gsd/commands/handlers/workflow.ts +74 -9
  380. package/src/resources/extensions/gsd/commands-add-tests.ts +137 -0
  381. package/src/resources/extensions/gsd/commands-backlog.ts +182 -0
  382. package/src/resources/extensions/gsd/commands-do.ts +109 -0
  383. package/src/resources/extensions/gsd/commands-extract-learnings.ts +304 -0
  384. package/src/resources/extensions/gsd/commands-maintenance.ts +6 -6
  385. package/src/resources/extensions/gsd/commands-pr-branch.ts +234 -0
  386. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  387. package/src/resources/extensions/gsd/commands-session-report.ts +101 -0
  388. package/src/resources/extensions/gsd/commands-ship.ts +219 -0
  389. package/src/resources/extensions/gsd/db-writer.ts +3 -5
  390. package/src/resources/extensions/gsd/docs/preferences-reference.md +14 -1
  391. package/src/resources/extensions/gsd/ecosystem/gsd-extension-api.ts +228 -0
  392. package/src/resources/extensions/gsd/ecosystem/loader.ts +201 -0
  393. package/src/resources/extensions/gsd/git-service.ts +68 -0
  394. package/src/resources/extensions/gsd/graph-context.ts +212 -0
  395. package/src/resources/extensions/gsd/gsd-db.ts +788 -3
  396. package/src/resources/extensions/gsd/guided-flow.ts +32 -0
  397. package/src/resources/extensions/gsd/index.ts +18 -2
  398. package/src/resources/extensions/gsd/init-wizard.ts +3 -2
  399. package/src/resources/extensions/gsd/journal.ts +30 -0
  400. package/src/resources/extensions/gsd/md-importer.ts +3 -5
  401. package/src/resources/extensions/gsd/memory-store.ts +31 -62
  402. package/src/resources/extensions/gsd/metrics.ts +26 -0
  403. package/src/resources/extensions/gsd/milestone-validation-gates.ts +13 -14
  404. package/src/resources/extensions/gsd/native-git-bridge.ts +11 -12
  405. package/src/resources/extensions/gsd/parallel-orchestrator.ts +40 -1
  406. package/src/resources/extensions/gsd/preferences-models.ts +20 -3
  407. package/src/resources/extensions/gsd/preferences-types.ts +32 -0
  408. package/src/resources/extensions/gsd/preferences-validation.ts +107 -2
  409. package/src/resources/extensions/gsd/preferences.ts +28 -0
  410. package/src/resources/extensions/gsd/prompts/add-tests.md +35 -0
  411. package/src/resources/extensions/gsd/session-lock.ts +14 -2
  412. package/src/resources/extensions/gsd/slice-parallel-orchestrator.ts +20 -1
  413. package/src/resources/extensions/gsd/state.ts +9 -2
  414. package/src/resources/extensions/gsd/templates/PREFERENCES.md +18 -0
  415. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +7 -3
  416. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +20 -0
  417. package/src/resources/extensions/gsd/tests/auto-project-root-env.test.ts +7 -3
  418. package/src/resources/extensions/gsd/tests/cold-resume-db-reopen.test.ts +6 -2
  419. package/src/resources/extensions/gsd/tests/commands-backlog.test.ts +158 -0
  420. package/src/resources/extensions/gsd/tests/commands-do.test.ts +127 -0
  421. package/src/resources/extensions/gsd/tests/commands-extract-learnings.test.ts +340 -0
  422. package/src/resources/extensions/gsd/tests/commands-pr-branch.test.ts +68 -0
  423. package/src/resources/extensions/gsd/tests/commands-session-report.test.ts +82 -0
  424. package/src/resources/extensions/gsd/tests/commands-ship.test.ts +71 -0
  425. package/src/resources/extensions/gsd/tests/commands-workflow-custom.test.ts +14 -0
  426. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +2 -2
  427. package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
  428. package/src/resources/extensions/gsd/tests/extension-bootstrap-isolation.test.ts +154 -0
  429. package/src/resources/extensions/gsd/tests/finalize-timeout-guard.test.ts +10 -7
  430. package/src/resources/extensions/gsd/tests/graph-context.test.ts +337 -0
  431. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +1 -1
  432. package/src/resources/extensions/gsd/tests/health-widget.test.ts +1 -1
  433. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +68 -1
  434. package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -2
  435. package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -3
  436. package/src/resources/extensions/gsd/tests/native-git-bridge-exec-fallback.test.ts +140 -0
  437. package/src/resources/extensions/gsd/tests/post-exec-retry-bypass.test.ts +79 -1
  438. package/src/resources/extensions/gsd/tests/post-unit-state-rebuild.test.ts +2 -1
  439. package/src/resources/extensions/gsd/tests/pre-execution-pause-wiring.test.ts +40 -1
  440. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +1 -1
  441. package/src/resources/extensions/gsd/tests/single-writer-invariant.test.ts +180 -0
  442. package/src/resources/extensions/gsd/tests/token-profile.test.ts +8 -5
  443. package/src/resources/extensions/gsd/tests/uok-audit-unified.test.ts +101 -0
  444. package/src/resources/extensions/gsd/tests/uok-contracts.test.ts +85 -0
  445. package/src/resources/extensions/gsd/tests/uok-execution-graph.test.ts +69 -0
  446. package/src/resources/extensions/gsd/tests/uok-flags.test.ts +39 -0
  447. package/src/resources/extensions/gsd/tests/uok-gate-runner.test.ts +70 -0
  448. package/src/resources/extensions/gsd/tests/uok-gitops-turn-action.test.ts +85 -0
  449. package/src/resources/extensions/gsd/tests/uok-gitops-wiring.test.ts +35 -0
  450. package/src/resources/extensions/gsd/tests/uok-model-policy.test.ts +89 -0
  451. package/src/resources/extensions/gsd/tests/uok-plan-v2-wiring.test.ts +167 -0
  452. package/src/resources/extensions/gsd/tests/uok-preferences.test.ts +42 -0
  453. package/src/resources/extensions/gsd/tests/validate-milestone-write-order.test.ts +39 -0
  454. package/src/resources/extensions/gsd/tests/workflow-logger-wiring.test.ts +223 -0
  455. package/src/resources/extensions/gsd/tools/complete-slice.ts +26 -0
  456. package/src/resources/extensions/gsd/tools/validate-milestone.ts +48 -3
  457. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +3 -11
  458. package/src/resources/extensions/gsd/triage-resolution.ts +2 -7
  459. package/src/resources/extensions/gsd/types.ts +14 -1
  460. package/src/resources/extensions/gsd/unit-ownership.ts +2 -2
  461. package/src/resources/extensions/gsd/uok/audit-toggle.ts +9 -0
  462. package/src/resources/extensions/gsd/uok/audit.ts +51 -0
  463. package/src/resources/extensions/gsd/uok/contracts.ts +135 -0
  464. package/src/resources/extensions/gsd/uok/execution-graph.ts +241 -0
  465. package/src/resources/extensions/gsd/uok/flags.ts +45 -0
  466. package/src/resources/extensions/gsd/uok/gate-runner.ts +146 -0
  467. package/src/resources/extensions/gsd/uok/gitops.ts +75 -0
  468. package/src/resources/extensions/gsd/uok/kernel.ts +105 -0
  469. package/src/resources/extensions/gsd/uok/loop-adapter.ts +162 -0
  470. package/src/resources/extensions/gsd/uok/model-policy.ts +112 -0
  471. package/src/resources/extensions/gsd/uok/plan-v2.ts +156 -0
  472. package/src/resources/extensions/gsd/workflow-logger.ts +27 -1
  473. package/src/resources/extensions/gsd/workflow-manifest.ts +9 -104
  474. package/src/resources/extensions/gsd/workflow-migration.ts +21 -29
  475. package/src/resources/extensions/gsd/workflow-projections.ts +8 -1
  476. package/src/resources/extensions/gsd/workflow-reconcile.ts +15 -15
  477. package/src/resources/extensions/ttsr/ttsr-manager.ts +10 -5
  478. package/packages/pi-ai/dist/models.custom.d.ts.map +0 -1
  479. package/packages/pi-ai/dist/models.custom.js.map +0 -1
  480. package/packages/pi-ai/dist/models.generated.js +0 -14343
  481. package/packages/pi-ai/dist/models.generated.js.map +0 -1
  482. package/packages/pi-ai/src/models.generated.ts +0 -14345
  483. /package/dist/web/standalone/.next/static/{YzIEI9sxJy4t5xgClF08g → 3U-oZ5FT59BM7sm2GInic}/_buildManifest.js +0 -0
  484. /package/dist/web/standalone/.next/static/{YzIEI9sxJy4t5xgClF08g → 3U-oZ5FT59BM7sm2GInic}/_ssgManifest.js +0 -0
@@ -0,0 +1,855 @@
1
+ // GSD MCP Server — knowledge graph reader
2
+ // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
3
+
4
+ /**
5
+ * Knowledge Graph for GSD projects.
6
+ *
7
+ * Parses .gsd/ artifacts (STATE.md, milestone ROADMAPs, slice PLANs,
8
+ * KNOWLEDGE.md) into a graph of nodes and edges. Parse errors in any
9
+ * single artifact are caught and never propagate — the artifact is skipped
10
+ * and the rest of the graph is returned.
11
+ *
12
+ * writeGraph() is atomic: writes to graph.tmp.json then renames to graph.json.
13
+ */
14
+
15
+ import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync } from 'node:fs';
16
+ import { join, resolve } from 'node:path';
17
+ import { resolveGsdRoot, findMilestoneIds, resolveMilestoneDir, findSliceIds, resolveSliceDir } from './paths.js';
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Types
21
+ // ---------------------------------------------------------------------------
22
+
23
+ export type NodeType =
24
+ | 'milestone'
25
+ | 'slice'
26
+ | 'task'
27
+ | 'rule'
28
+ | 'pattern'
29
+ | 'lesson'
30
+ | 'concept'
31
+ | 'decision';
32
+
33
+ export type EdgeType =
34
+ | 'contains'
35
+ | 'depends_on'
36
+ | 'relates_to'
37
+ | 'implements';
38
+
39
+ export type ConfidenceTier = 'EXTRACTED' | 'INFERRED' | 'AMBIGUOUS';
40
+
41
+ export interface GraphNode {
42
+ id: string;
43
+ label: string;
44
+ type: NodeType;
45
+ description?: string;
46
+ confidence: ConfidenceTier;
47
+ sourceFile?: string;
48
+ }
49
+
50
+ export interface GraphEdge {
51
+ from: string;
52
+ to: string;
53
+ type: EdgeType;
54
+ confidence: ConfidenceTier;
55
+ }
56
+
57
+ export interface KnowledgeGraph {
58
+ nodes: GraphNode[];
59
+ edges: GraphEdge[];
60
+ builtAt: string;
61
+ }
62
+
63
+ export interface GraphStatusResult {
64
+ exists: boolean;
65
+ lastBuild?: string;
66
+ nodeCount?: number;
67
+ edgeCount?: number;
68
+ stale?: boolean;
69
+ ageHours?: number;
70
+ }
71
+
72
+ export interface GraphQueryResult {
73
+ nodes: GraphNode[];
74
+ edges: GraphEdge[];
75
+ term: string;
76
+ budget: number;
77
+ }
78
+
79
+ export interface GraphDiffResult {
80
+ nodes: {
81
+ added: string[];
82
+ removed: string[];
83
+ changed: string[];
84
+ };
85
+ edges: {
86
+ added: string[];
87
+ removed: string[];
88
+ };
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Graph file paths
93
+ // ---------------------------------------------------------------------------
94
+
95
+ function graphsDir(gsdRoot: string): string {
96
+ return join(gsdRoot, 'graphs');
97
+ }
98
+
99
+ function graphJsonPath(gsdRoot: string): string {
100
+ return join(graphsDir(gsdRoot), 'graph.json');
101
+ }
102
+
103
+ function graphTmpPath(gsdRoot: string): string {
104
+ return join(graphsDir(gsdRoot), 'graph.tmp.json');
105
+ }
106
+
107
+ function snapshotPath(gsdRoot: string): string {
108
+ return join(graphsDir(gsdRoot), '.last-build-snapshot.json');
109
+ }
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Parsers — each returns nodes/edges and never throws
113
+ // ---------------------------------------------------------------------------
114
+
115
+ /**
116
+ * Parse STATE.md for active milestone and phase concepts.
117
+ */
118
+ function parseStateFile(gsdRoot: string, nodes: GraphNode[], _edges: GraphEdge[]): void {
119
+ const statePath = join(gsdRoot, 'STATE.md');
120
+ if (!existsSync(statePath)) return;
121
+
122
+ let content: string;
123
+ try {
124
+ content = readFileSync(statePath, 'utf-8');
125
+ } catch {
126
+ return;
127
+ }
128
+
129
+ // Extract active milestone
130
+ const activeMilestoneMatch = content.match(/\*\*Active Milestone:\*\*\s+([A-Z]\d+):\s+(.+)/i);
131
+ if (activeMilestoneMatch) {
132
+ const [, milestoneId, title] = activeMilestoneMatch;
133
+ const id = `milestone:${milestoneId}`;
134
+ if (!nodes.some((n) => n.id === id)) {
135
+ nodes.push({
136
+ id,
137
+ label: `${milestoneId}: ${title.trim()}`,
138
+ type: 'milestone',
139
+ description: `Active milestone: ${milestoneId}`,
140
+ confidence: 'EXTRACTED',
141
+ sourceFile: 'STATE.md',
142
+ });
143
+ }
144
+ }
145
+
146
+ // Extract phase as concept
147
+ const phaseMatch = content.match(/\*\*Phase:\*\*\s+(\S+)/i);
148
+ if (phaseMatch) {
149
+ const phase = phaseMatch[1].trim();
150
+ nodes.push({
151
+ id: `concept:phase:${phase}`,
152
+ label: `Phase: ${phase}`,
153
+ type: 'concept',
154
+ confidence: 'EXTRACTED',
155
+ sourceFile: 'STATE.md',
156
+ });
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Parse KNOWLEDGE.md for rules, patterns, and lessons.
162
+ */
163
+ function parseKnowledgeFile(gsdRoot: string, nodes: GraphNode[], _edges: GraphEdge[]): void {
164
+ const knowledgePath = join(gsdRoot, 'KNOWLEDGE.md');
165
+ if (!existsSync(knowledgePath)) return;
166
+
167
+ let content: string;
168
+ try {
169
+ content = readFileSync(knowledgePath, 'utf-8');
170
+ } catch {
171
+ return;
172
+ }
173
+
174
+ // Parse Rules table
175
+ const rulesMatch = content.match(/## Rules\s*\n([\s\S]*?)(?=\n## |$)/i);
176
+ if (rulesMatch) {
177
+ for (const line of rulesMatch[1].split('\n')) {
178
+ if (!line.includes('|')) continue;
179
+ const cells = line.split('|').map((c) => c.trim()).filter(Boolean);
180
+ if (cells.length < 3) continue;
181
+ if (cells[0].startsWith('#') || cells[0].startsWith('-')) continue;
182
+ const id = cells[0];
183
+ if (!/^K\d+$/i.test(id)) continue;
184
+ nodes.push({
185
+ id: `rule:${id}`,
186
+ label: id,
187
+ type: 'rule',
188
+ description: cells[2] ?? '',
189
+ confidence: 'EXTRACTED',
190
+ sourceFile: 'KNOWLEDGE.md',
191
+ });
192
+ }
193
+ }
194
+
195
+ // Parse Patterns table
196
+ const patternsMatch = content.match(/## Patterns\s*\n([\s\S]*?)(?=\n## |$)/i);
197
+ if (patternsMatch) {
198
+ for (const line of patternsMatch[1].split('\n')) {
199
+ if (!line.includes('|')) continue;
200
+ const cells = line.split('|').map((c) => c.trim()).filter(Boolean);
201
+ if (cells.length < 2) continue;
202
+ if (cells[0].startsWith('#') || cells[0].startsWith('-')) continue;
203
+ const id = cells[0];
204
+ if (!/^P\d+$/i.test(id)) continue;
205
+ nodes.push({
206
+ id: `pattern:${id}`,
207
+ label: id,
208
+ type: 'pattern',
209
+ description: cells[1] ?? '',
210
+ confidence: 'EXTRACTED',
211
+ sourceFile: 'KNOWLEDGE.md',
212
+ });
213
+ }
214
+ }
215
+
216
+ // Parse Lessons Learned table
217
+ const lessonsMatch = content.match(/## Lessons Learned\s*\n([\s\S]*?)(?=\n## |$)/i);
218
+ if (lessonsMatch) {
219
+ for (const line of lessonsMatch[1].split('\n')) {
220
+ if (!line.includes('|')) continue;
221
+ const cells = line.split('|').map((c) => c.trim()).filter(Boolean);
222
+ if (cells.length < 2) continue;
223
+ if (cells[0].startsWith('#') || cells[0].startsWith('-')) continue;
224
+ const id = cells[0];
225
+ if (!/^L\d+$/i.test(id)) continue;
226
+ nodes.push({
227
+ id: `lesson:${id}`,
228
+ label: id,
229
+ type: 'lesson',
230
+ description: cells[1] ?? '',
231
+ confidence: 'EXTRACTED',
232
+ sourceFile: 'KNOWLEDGE.md',
233
+ });
234
+ }
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Parse milestone ROADMAP.md files for milestones and slices.
240
+ */
241
+ function parseMilestoneFiles(
242
+ gsdRoot: string,
243
+ nodes: GraphNode[],
244
+ edges: GraphEdge[],
245
+ ): void {
246
+ const milestoneIds = findMilestoneIds(gsdRoot);
247
+
248
+ for (const milestoneId of milestoneIds) {
249
+ try {
250
+ parseSingleMilestone(gsdRoot, milestoneId, nodes, edges);
251
+ } catch {
252
+ // Skip this milestone on any error
253
+ }
254
+ }
255
+ }
256
+
257
+ function parseSingleMilestone(
258
+ gsdRoot: string,
259
+ milestoneId: string,
260
+ nodes: GraphNode[],
261
+ edges: GraphEdge[],
262
+ ): void {
263
+ const mDir = resolveMilestoneDir(gsdRoot, milestoneId);
264
+ if (!mDir) return;
265
+
266
+ const milestoneNodeId = `milestone:${milestoneId}`;
267
+
268
+ // Try to read the roadmap file
269
+ const roadmapPath = join(mDir, `${milestoneId}-ROADMAP.md`);
270
+ let roadmapContent: string | null = null;
271
+ if (existsSync(roadmapPath)) {
272
+ try {
273
+ roadmapContent = readFileSync(roadmapPath, 'utf-8');
274
+ } catch {
275
+ // Skip
276
+ }
277
+ }
278
+
279
+ // Extract milestone title from roadmap
280
+ let milestoneTitle = milestoneId;
281
+ if (roadmapContent) {
282
+ const titleMatch = roadmapContent.match(/^#\s+[A-Z]\d+:\s+(.+)/m);
283
+ if (titleMatch) milestoneTitle = `${milestoneId}: ${titleMatch[1].trim()}`;
284
+ }
285
+
286
+ // Ensure milestone node exists
287
+ if (!nodes.some((n) => n.id === milestoneNodeId)) {
288
+ nodes.push({
289
+ id: milestoneNodeId,
290
+ label: milestoneTitle,
291
+ type: 'milestone',
292
+ confidence: 'EXTRACTED',
293
+ sourceFile: roadmapContent ? `milestones/${milestoneId}/${milestoneId}-ROADMAP.md` : undefined,
294
+ });
295
+ }
296
+
297
+ // Parse slices from roadmap table or filesystem
298
+ const sliceIds = findSliceIds(gsdRoot, milestoneId);
299
+ for (const sliceId of sliceIds) {
300
+ try {
301
+ parseSingleSlice(gsdRoot, milestoneId, sliceId, milestoneNodeId, nodes, edges);
302
+ } catch {
303
+ // Skip this slice on any error
304
+ }
305
+ }
306
+ }
307
+
308
+ function parseSingleSlice(
309
+ gsdRoot: string,
310
+ milestoneId: string,
311
+ sliceId: string,
312
+ milestoneNodeId: string,
313
+ nodes: GraphNode[],
314
+ edges: GraphEdge[],
315
+ ): void {
316
+ const sDir = resolveSliceDir(gsdRoot, milestoneId, sliceId);
317
+ if (!sDir) return;
318
+
319
+ const sliceNodeId = `slice:${milestoneId}:${sliceId}`;
320
+
321
+ // Try to read the slice plan
322
+ const planPath = join(sDir, `${sliceId}-PLAN.md`);
323
+ let sliceTitle = `${milestoneId}/${sliceId}`;
324
+ let planContent: string | null = null;
325
+
326
+ if (existsSync(planPath)) {
327
+ try {
328
+ planContent = readFileSync(planPath, 'utf-8');
329
+ const titleMatch = planContent.match(/^#\s+[A-Z]\d+:\s+(.+)/m);
330
+ if (titleMatch) sliceTitle = `${sliceId}: ${titleMatch[1].trim()}`;
331
+ } catch {
332
+ // Use default title
333
+ }
334
+ }
335
+
336
+ nodes.push({
337
+ id: sliceNodeId,
338
+ label: sliceTitle,
339
+ type: 'slice',
340
+ confidence: 'EXTRACTED',
341
+ sourceFile: planContent ? `milestones/${milestoneId}/slices/${sliceId}/${sliceId}-PLAN.md` : undefined,
342
+ });
343
+
344
+ // Edge: milestone contains slice
345
+ edges.push({
346
+ from: milestoneNodeId,
347
+ to: sliceNodeId,
348
+ type: 'contains',
349
+ confidence: 'EXTRACTED',
350
+ });
351
+
352
+ // Parse tasks from the slice plan
353
+ if (planContent) {
354
+ parseTasksFromPlan(planContent, milestoneId, sliceId, sliceNodeId, nodes, edges);
355
+ }
356
+ }
357
+
358
+ function parseTasksFromPlan(
359
+ content: string,
360
+ milestoneId: string,
361
+ sliceId: string,
362
+ sliceNodeId: string,
363
+ nodes: GraphNode[],
364
+ edges: GraphEdge[],
365
+ ): void {
366
+ // Match lines like: - [ ] **T01: Title** — description
367
+ const taskPattern = /[-*]\s+\[[ x]\]\s+\*\*(T\d+):\s*([^*]+)\*\*/g;
368
+ let match: RegExpExecArray | null;
369
+
370
+ while ((match = taskPattern.exec(content)) !== null) {
371
+ const [, taskId, taskTitle] = match;
372
+ const taskNodeId = `task:${milestoneId}:${sliceId}:${taskId}`;
373
+
374
+ nodes.push({
375
+ id: taskNodeId,
376
+ label: `${taskId}: ${taskTitle.trim()}`,
377
+ type: 'task',
378
+ confidence: 'EXTRACTED',
379
+ });
380
+
381
+ edges.push({
382
+ from: sliceNodeId,
383
+ to: taskNodeId,
384
+ type: 'contains',
385
+ confidence: 'EXTRACTED',
386
+ });
387
+ }
388
+ }
389
+
390
+ // ---------------------------------------------------------------------------
391
+ // LEARNINGS.md parser
392
+ // ---------------------------------------------------------------------------
393
+
394
+ /**
395
+ * Parse all *-LEARNINGS.md files found in milestone directories.
396
+ * Extracts Decisions, Lessons, Patterns, and Surprises as typed graph nodes.
397
+ * Surprises are mapped to the 'lesson' NodeType (no distinct type exists).
398
+ * Parse errors per file are caught — the file is skipped, never rethrows.
399
+ */
400
+ function parseLearningsFiles(gsdRoot: string, nodes: GraphNode[], edges: GraphEdge[]): void {
401
+ const milestoneIds = findMilestoneIds(gsdRoot);
402
+
403
+ for (const milestoneId of milestoneIds) {
404
+ try {
405
+ parseSingleLearningsFile(gsdRoot, milestoneId, nodes, edges);
406
+ } catch {
407
+ // Skip this milestone's LEARNINGS.md on any error
408
+ }
409
+ }
410
+ }
411
+
412
+ function parseSingleLearningsFile(
413
+ gsdRoot: string,
414
+ milestoneId: string,
415
+ nodes: GraphNode[],
416
+ edges: GraphEdge[],
417
+ ): void {
418
+ const mDir = resolveMilestoneDir(gsdRoot, milestoneId);
419
+ if (!mDir) return;
420
+
421
+ const learningsPath = join(mDir, `${milestoneId}-LEARNINGS.md`);
422
+ if (!existsSync(learningsPath)) return;
423
+
424
+ let content: string;
425
+ try {
426
+ content = readFileSync(learningsPath, 'utf-8');
427
+ } catch {
428
+ return;
429
+ }
430
+
431
+ // Strip YAML frontmatter if present
432
+ const withoutFrontmatter = content.replace(/^---[\s\S]*?---\n?/, '');
433
+
434
+ const milestoneNodeId = `milestone:${milestoneId}`;
435
+ const sourceFile = `milestones/${milestoneId}/${milestoneId}-LEARNINGS.md`;
436
+
437
+ // Parse each section: [sectionName, nodeType, idPrefix]
438
+ const sections: Array<[string, NodeType, string]> = [
439
+ ['Decisions', 'decision', 'decision'],
440
+ ['Lessons', 'lesson', 'lesson'],
441
+ ['Patterns', 'pattern', 'pattern'],
442
+ ['Surprises', 'lesson', 'surprise'],
443
+ ];
444
+
445
+ for (const [sectionName, nodeType, idPrefix] of sections) {
446
+ const sectionMatch = withoutFrontmatter.match(
447
+ new RegExp(`##\\s+${sectionName}\\s*\\n([\\s\\S]*?)(?=\\n##\\s|$)`, 'i'),
448
+ );
449
+ if (!sectionMatch) continue;
450
+
451
+ const sectionContent = sectionMatch[1];
452
+ parseLearningsSection(
453
+ sectionContent,
454
+ milestoneId,
455
+ idPrefix,
456
+ nodeType,
457
+ milestoneNodeId,
458
+ sourceFile,
459
+ nodes,
460
+ edges,
461
+ );
462
+ }
463
+ }
464
+
465
+ function parseLearningsSection(
466
+ sectionContent: string,
467
+ milestoneId: string,
468
+ idPrefix: string,
469
+ nodeType: NodeType,
470
+ milestoneNodeId: string,
471
+ sourceFile: string,
472
+ nodes: GraphNode[],
473
+ edges: GraphEdge[],
474
+ ): void {
475
+ // Each item is a bullet line starting with "- " followed by optional
476
+ // indented "Source: ..." line.
477
+ // We collect bullet items and their associated source attribution.
478
+ const lines = sectionContent.split('\n');
479
+ let itemIndex = 0;
480
+ let currentText: string | null = null;
481
+ let currentSource: string | null = null;
482
+
483
+ const flushItem = (): void => {
484
+ if (!currentText) return;
485
+ itemIndex += 1;
486
+ const nodeId = `${idPrefix}:${milestoneId}:${itemIndex}`;
487
+ const description = currentSource ? `${currentSource}` : undefined;
488
+
489
+ nodes.push({
490
+ id: nodeId,
491
+ label: currentText,
492
+ type: nodeType,
493
+ description,
494
+ confidence: 'EXTRACTED',
495
+ sourceFile,
496
+ });
497
+
498
+ // Edge: milestone relates_to this learning node
499
+ edges.push({
500
+ from: milestoneNodeId,
501
+ to: nodeId,
502
+ type: 'relates_to',
503
+ confidence: 'EXTRACTED',
504
+ });
505
+
506
+ currentText = null;
507
+ currentSource = null;
508
+ };
509
+
510
+ for (const line of lines) {
511
+ const bulletMatch = line.match(/^[-*]\s+(.+)/);
512
+ if (bulletMatch) {
513
+ flushItem();
514
+ currentText = bulletMatch[1].trim();
515
+ continue;
516
+ }
517
+
518
+ // Indented source attribution: " Source: ..."
519
+ const sourceMatch = line.match(/^\s+Source:\s+(.+)/i);
520
+ if (sourceMatch && currentText !== null) {
521
+ currentSource = `Source: ${sourceMatch[1].trim()}`;
522
+ continue;
523
+ }
524
+
525
+ // Continuation of current item text (indented non-source line)
526
+ const continuationMatch = line.match(/^\s{2,}(.+)/);
527
+ if (continuationMatch && currentText !== null && currentSource === null) {
528
+ currentText += ' ' + continuationMatch[1].trim();
529
+ }
530
+ }
531
+
532
+ flushItem();
533
+ }
534
+
535
+ // ---------------------------------------------------------------------------
536
+ // buildGraph
537
+ // ---------------------------------------------------------------------------
538
+
539
+ /**
540
+ * Build a KnowledgeGraph by parsing all .gsd/ artifacts.
541
+ *
542
+ * Parse errors in any single artifact are caught — the artifact is skipped
543
+ * and never causes buildGraph() to throw.
544
+ */
545
+ export async function buildGraph(projectDir: string): Promise<KnowledgeGraph> {
546
+ const gsdRoot = resolveGsdRoot(resolve(projectDir));
547
+
548
+ const nodes: GraphNode[] = [];
549
+ const edges: GraphEdge[] = [];
550
+
551
+ // Each parser is wrapped so a crash in one never stops others
552
+ const parsers: Array<(g: string, n: GraphNode[], e: GraphEdge[]) => void> = [
553
+ parseStateFile,
554
+ parseKnowledgeFile,
555
+ parseMilestoneFiles,
556
+ parseLearningsFiles,
557
+ ];
558
+
559
+ for (const parser of parsers) {
560
+ try {
561
+ parser(gsdRoot, nodes, edges);
562
+ } catch {
563
+ // Parsing error — skip this artifact, mark as ambiguous
564
+ nodes.push({
565
+ id: `error:${parser.name}:${Date.now()}`,
566
+ label: `Parse error in ${parser.name}`,
567
+ type: 'concept',
568
+ confidence: 'AMBIGUOUS',
569
+ });
570
+ }
571
+ }
572
+
573
+ // Deduplicate nodes by id (keep first occurrence)
574
+ const seen = new Set<string>();
575
+ const dedupedNodes = nodes.filter((n) => {
576
+ if (seen.has(n.id)) return false;
577
+ seen.add(n.id);
578
+ return true;
579
+ });
580
+
581
+ return {
582
+ nodes: dedupedNodes,
583
+ edges,
584
+ builtAt: new Date().toISOString(),
585
+ };
586
+ }
587
+
588
+ // ---------------------------------------------------------------------------
589
+ // writeGraph — atomic write via tmp + rename
590
+ // ---------------------------------------------------------------------------
591
+
592
+ /**
593
+ * Write the graph to .gsd/graphs/graph.json atomically.
594
+ *
595
+ * Writes to graph.tmp.json first, then renames to graph.json.
596
+ * Creates the graphs/ directory if it does not exist.
597
+ */
598
+ export async function writeGraph(gsdRoot: string, graph: KnowledgeGraph): Promise<void> {
599
+ const dir = graphsDir(gsdRoot);
600
+ mkdirSync(dir, { recursive: true });
601
+
602
+ const tmp = graphTmpPath(gsdRoot);
603
+ const final = graphJsonPath(gsdRoot);
604
+
605
+ writeFileSync(tmp, JSON.stringify(graph, null, 2), 'utf-8');
606
+ renameSync(tmp, final);
607
+ }
608
+
609
+ // ---------------------------------------------------------------------------
610
+ // writeSnapshot
611
+ // ---------------------------------------------------------------------------
612
+
613
+ /**
614
+ * Copy the current graph.json to .last-build-snapshot.json.
615
+ * Adds a snapshotAt timestamp to the copy.
616
+ */
617
+ export async function writeSnapshot(gsdRoot: string): Promise<void> {
618
+ const src = graphJsonPath(gsdRoot);
619
+ if (!existsSync(src)) return;
620
+
621
+ const dir = graphsDir(gsdRoot);
622
+ mkdirSync(dir, { recursive: true });
623
+
624
+ const raw = readFileSync(src, 'utf-8');
625
+ let graph: KnowledgeGraph;
626
+ try {
627
+ graph = JSON.parse(raw) as KnowledgeGraph;
628
+ } catch {
629
+ return;
630
+ }
631
+ const snapshot = { ...graph, snapshotAt: new Date().toISOString() };
632
+
633
+ writeFileSync(snapshotPath(gsdRoot), JSON.stringify(snapshot, null, 2), 'utf-8');
634
+ }
635
+
636
+ // ---------------------------------------------------------------------------
637
+ // graphStatus
638
+ // ---------------------------------------------------------------------------
639
+
640
+ /**
641
+ * Return status of the graph: whether it exists, its age, and whether it is stale.
642
+ * Stale means builtAt is older than 24 hours.
643
+ */
644
+ export async function graphStatus(projectDir: string): Promise<GraphStatusResult> {
645
+ const gsdRoot = resolveGsdRoot(resolve(projectDir));
646
+ const graphPath = graphJsonPath(gsdRoot);
647
+
648
+ if (!existsSync(graphPath)) {
649
+ return { exists: false };
650
+ }
651
+
652
+ try {
653
+ const raw = readFileSync(graphPath, 'utf-8');
654
+ const graph = JSON.parse(raw) as KnowledgeGraph;
655
+
656
+ const builtAt = graph.builtAt;
657
+ const ageMs = Date.now() - new Date(builtAt).getTime();
658
+ const ageHours = ageMs / (1000 * 60 * 60);
659
+ const stale = ageHours > 24;
660
+
661
+ return {
662
+ exists: true,
663
+ lastBuild: builtAt,
664
+ nodeCount: graph.nodes.length,
665
+ edgeCount: graph.edges.length,
666
+ stale,
667
+ ageHours,
668
+ };
669
+ } catch {
670
+ return { exists: false };
671
+ }
672
+ }
673
+
674
+ // ---------------------------------------------------------------------------
675
+ // applyBudget — trim edges to stay within token budget
676
+ // ---------------------------------------------------------------------------
677
+
678
+ /**
679
+ * Given a set of seed node IDs and the full graph, apply BFS to collect
680
+ * reachable nodes and edges. Trims AMBIGUOUS edges first, then INFERRED,
681
+ * stopping when the estimated token count drops within budget.
682
+ *
683
+ * Budget is a rough token estimate: 1 node ≈ 20 tokens, 1 edge ≈ 10 tokens.
684
+ */
685
+ function applyBudget(
686
+ graph: KnowledgeGraph,
687
+ seedIds: Set<string>,
688
+ budget: number,
689
+ ): { nodes: GraphNode[]; edges: GraphEdge[] } {
690
+ // BFS to collect reachable nodes (start from seeds)
691
+ const reachable = new Set<string>(seedIds);
692
+ const queue = [...seedIds];
693
+
694
+ while (queue.length > 0) {
695
+ const current = queue.shift()!;
696
+ for (const edge of graph.edges) {
697
+ if (edge.from === current && !reachable.has(edge.to)) {
698
+ reachable.add(edge.to);
699
+ queue.push(edge.to);
700
+ }
701
+ }
702
+ }
703
+
704
+ let resultNodes = graph.nodes.filter((n) => reachable.has(n.id));
705
+ let resultEdges = graph.edges.filter(
706
+ (e) => reachable.has(e.from) && reachable.has(e.to),
707
+ );
708
+
709
+ // Estimate tokens and trim if over budget
710
+ // Trim AMBIGUOUS edges first, then INFERRED
711
+ const estimate = (): number =>
712
+ resultNodes.length * 20 + resultEdges.length * 10;
713
+
714
+ if (estimate() > budget) {
715
+ resultEdges = resultEdges.filter((e) => e.confidence !== 'AMBIGUOUS');
716
+ }
717
+ if (estimate() > budget) {
718
+ resultEdges = resultEdges.filter((e) => e.confidence !== 'INFERRED');
719
+ }
720
+ if (estimate() > budget) {
721
+ // Hard trim — keep only seed nodes and their EXTRACTED edges
722
+ const seedNodes = resultNodes.filter((n) => seedIds.has(n.id));
723
+ const seedEdges = resultEdges.filter(
724
+ (e) => seedIds.has(e.from) && e.confidence === 'EXTRACTED',
725
+ );
726
+ return { nodes: seedNodes, edges: seedEdges };
727
+ }
728
+
729
+ return { nodes: resultNodes, edges: resultEdges };
730
+ }
731
+
732
+ // ---------------------------------------------------------------------------
733
+ // graphQuery
734
+ // ---------------------------------------------------------------------------
735
+
736
+ /**
737
+ * Query the graph for nodes matching a term (case-insensitive on label + description).
738
+ * BFS from seed nodes, applying budget trimming.
739
+ *
740
+ * Reads from the pre-built graph.json. Falls back to an empty result if no
741
+ * graph exists.
742
+ */
743
+ export async function graphQuery(
744
+ projectDir: string,
745
+ term: string,
746
+ budget = 4000,
747
+ ): Promise<GraphQueryResult> {
748
+ const gsdRoot = resolveGsdRoot(resolve(projectDir));
749
+ const graphPath = graphJsonPath(gsdRoot);
750
+
751
+ if (!existsSync(graphPath)) {
752
+ return { nodes: [], edges: [], term, budget };
753
+ }
754
+
755
+ let graph: KnowledgeGraph;
756
+ try {
757
+ const raw = readFileSync(graphPath, 'utf-8');
758
+ graph = JSON.parse(raw) as KnowledgeGraph;
759
+ } catch {
760
+ return { nodes: [], edges: [], term, budget };
761
+ }
762
+
763
+ if (!term || term.trim() === '') {
764
+ // Empty term — return empty result
765
+ return { nodes: [], edges: [], term, budget };
766
+ }
767
+
768
+ const lower = term.toLowerCase();
769
+
770
+ // Find seed nodes that match the term
771
+ const seedIds = new Set<string>(
772
+ graph.nodes
773
+ .filter((n) => {
774
+ const labelMatch = n.label.toLowerCase().includes(lower);
775
+ const descMatch = n.description?.toLowerCase().includes(lower) ?? false;
776
+ return labelMatch || descMatch;
777
+ })
778
+ .map((n) => n.id),
779
+ );
780
+
781
+ if (seedIds.size === 0) {
782
+ return { nodes: [], edges: [], term, budget };
783
+ }
784
+
785
+ const result = applyBudget(graph, seedIds, budget);
786
+ return { ...result, term, budget };
787
+ }
788
+
789
+ // ---------------------------------------------------------------------------
790
+ // graphDiff
791
+ // ---------------------------------------------------------------------------
792
+
793
+ /**
794
+ * Compare the current graph.json with .last-build-snapshot.json.
795
+ * Returns added/removed/changed nodes and added/removed edges.
796
+ *
797
+ * If no snapshot exists, returns empty diff arrays.
798
+ */
799
+ export async function graphDiff(projectDir: string): Promise<GraphDiffResult> {
800
+ const gsdRoot = resolveGsdRoot(resolve(projectDir));
801
+ const empty: GraphDiffResult = {
802
+ nodes: { added: [], removed: [], changed: [] },
803
+ edges: { added: [], removed: [] },
804
+ };
805
+
806
+ const graphPath = graphJsonPath(gsdRoot);
807
+ const snap = snapshotPath(gsdRoot);
808
+
809
+ if (!existsSync(graphPath)) return empty;
810
+ if (!existsSync(snap)) return empty;
811
+
812
+ let current: KnowledgeGraph;
813
+ let snapshot: KnowledgeGraph;
814
+
815
+ try {
816
+ current = JSON.parse(readFileSync(graphPath, 'utf-8')) as KnowledgeGraph;
817
+ } catch {
818
+ return empty;
819
+ }
820
+
821
+ try {
822
+ snapshot = JSON.parse(readFileSync(snap, 'utf-8')) as KnowledgeGraph;
823
+ } catch {
824
+ return empty;
825
+ }
826
+
827
+ const currentNodeIds = new Set(current.nodes.map((n) => n.id));
828
+ const snapshotNodeIds = new Set(snapshot.nodes.map((n) => n.id));
829
+
830
+ const added = current.nodes.filter((n) => !snapshotNodeIds.has(n.id)).map((n) => n.id);
831
+ const removed = snapshot.nodes.filter((n) => !currentNodeIds.has(n.id)).map((n) => n.id);
832
+
833
+ // Changed: same id but different label or description
834
+ const snapshotNodeMap = new Map(snapshot.nodes.map((n) => [n.id, n]));
835
+ const changed = current.nodes
836
+ .filter((n) => {
837
+ const snap = snapshotNodeMap.get(n.id);
838
+ if (!snap) return false;
839
+ return n.label !== snap.label || n.description !== snap.description;
840
+ })
841
+ .map((n) => n.id);
842
+
843
+ // Edges — compare by string key "from->to:type"
844
+ const edgeKey = (e: GraphEdge): string => `${e.from}->${e.to}:${e.type}`;
845
+ const currentEdgeKeys = new Set(current.edges.map(edgeKey));
846
+ const snapshotEdgeKeys = new Set(snapshot.edges.map(edgeKey));
847
+
848
+ const edgesAdded = current.edges.filter((e) => !snapshotEdgeKeys.has(edgeKey(e))).map(edgeKey);
849
+ const edgesRemoved = snapshot.edges.filter((e) => !currentEdgeKeys.has(edgeKey(e))).map(edgeKey);
850
+
851
+ return {
852
+ nodes: { added, removed, changed },
853
+ edges: { added: edgesAdded, removed: edgesRemoved },
854
+ };
855
+ }