okengine 0.1.4

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 (674) hide show
  1. package/AGENTS.md +91 -0
  2. package/README.md +327 -0
  3. package/docs/spec/complete-example.md +1188 -0
  4. package/docs/spec/console.md +739 -0
  5. package/docs/spec/four-applications.md +1188 -0
  6. package/docs/spec/unified-theory.md +489 -0
  7. package/package.json +152 -0
  8. package/spec/manifest.v1.schema.json +624 -0
  9. package/src/auth/api-keys.ts +168 -0
  10. package/src/auth/attenuation.ts +116 -0
  11. package/src/auth/auth.test.ts +273 -0
  12. package/src/auth/cross-plane.ts +98 -0
  13. package/src/auth/index.ts +112 -0
  14. package/src/auth/invites.ts +66 -0
  15. package/src/auth/invoke-as.ts +49 -0
  16. package/src/auth/operator.ts +178 -0
  17. package/src/auth/planes.ts +102 -0
  18. package/src/auth/plugin.ts +44 -0
  19. package/src/auth/roles.ts +84 -0
  20. package/src/auth/sessions.ts +418 -0
  21. package/src/auth/tables.ts +131 -0
  22. package/src/cli/args.ts +50 -0
  23. package/src/cli/branch.ts +75 -0
  24. package/src/cli/build.ts +86 -0
  25. package/src/cli/client-add.test.ts +140 -0
  26. package/src/cli/client-add.ts +164 -0
  27. package/src/cli/completion.test.ts +58 -0
  28. package/src/cli/completion.ts +296 -0
  29. package/src/cli/dev-app-runner.ts +77 -0
  30. package/src/cli/dev.test.ts +435 -0
  31. package/src/cli/dev.ts +594 -0
  32. package/src/cli/doc-drift.test.ts +146 -0
  33. package/src/cli/doc-drift.ts +397 -0
  34. package/src/cli/docker-cli.test.ts +221 -0
  35. package/src/cli/docker.ts +127 -0
  36. package/src/cli/doctor-diff-examples.ts +92 -0
  37. package/src/cli/doctor-diff.test.ts +91 -0
  38. package/src/cli/doctor-diff.ts +274 -0
  39. package/src/cli/doctor-pii.test.ts +98 -0
  40. package/src/cli/doctor-pii.ts +151 -0
  41. package/src/cli/doctor.test.ts +150 -0
  42. package/src/cli/doctor.ts +223 -0
  43. package/src/cli/eval.ts +122 -0
  44. package/src/cli/exit.ts +19 -0
  45. package/src/cli/gates-list.test.ts +47 -0
  46. package/src/cli/gates-list.ts +84 -0
  47. package/src/cli/images.ts +277 -0
  48. package/src/cli/index.ts +99 -0
  49. package/src/cli/json-out.test.ts +129 -0
  50. package/src/cli/load-config.ts +71 -0
  51. package/src/cli/mcp-from-console.ts +50 -0
  52. package/src/cli/privacy-erase.ts +73 -0
  53. package/src/cli/registry.ts +455 -0
  54. package/src/cli/safe-defaults.test.ts +92 -0
  55. package/src/cli/schema.ts +196 -0
  56. package/src/cli/stack.ts +82 -0
  57. package/src/cli/start.ts +106 -0
  58. package/src/cli/upgrade.ts +105 -0
  59. package/src/cli/vault-cmd.ts +140 -0
  60. package/src/client/budget-entry.ts +34 -0
  61. package/src/client/budget.test.ts +17 -0
  62. package/src/client/create.test.ts +159 -0
  63. package/src/client/create.ts +152 -0
  64. package/src/client/errors.ts +60 -0
  65. package/src/client/index.ts +35 -0
  66. package/src/client/notes-contract.test.ts +289 -0
  67. package/src/client/transport.test.ts +123 -0
  68. package/src/client/transport.ts +254 -0
  69. package/src/client/types.ts +282 -0
  70. package/src/compiler/aot.test.ts +248 -0
  71. package/src/compiler/aot.ts +182 -0
  72. package/src/compiler/differential.test.ts +241 -0
  73. package/src/compiler/dynamic.ts +54 -0
  74. package/src/compiler/effects-infer.ts +541 -0
  75. package/src/compiler/emit.ts +40 -0
  76. package/src/compiler/extract.test.ts +177 -0
  77. package/src/compiler/extract.ts +1368 -0
  78. package/src/compiler/fixtures/raw/raw-unannotated.ts +28 -0
  79. package/src/compiler/fixtures/skyport/oke.config.ts +24 -0
  80. package/src/compiler/fixtures/skyport/src/ai.ts +15 -0
  81. package/src/compiler/fixtures/skyport/src/app.ts +10 -0
  82. package/src/compiler/fixtures/skyport/src/channels.ts +6 -0
  83. package/src/compiler/fixtures/skyport/src/core.ts +3 -0
  84. package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +57 -0
  85. package/src/compiler/fixtures/skyport/src/flows/bookings/signals.ts +11 -0
  86. package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +14 -0
  87. package/src/compiler/fixtures/skyport/src/flows/support/index.ts +20 -0
  88. package/src/compiler/fixtures/skyport/src/gates.ts +14 -0
  89. package/src/compiler/fixtures/skyport/src/journeys.ts +9 -0
  90. package/src/compiler/fixtures/skyport/src/schema.ts +3 -0
  91. package/src/compiler/fixtures/skyport/src/vault.ts +6 -0
  92. package/src/compiler/fixtures/skyport.expected.json +241 -0
  93. package/src/compiler/fixtures/triggers/five-triggers.ts +69 -0
  94. package/src/compiler/http-parse.ts +214 -0
  95. package/src/compiler/index.ts +67 -0
  96. package/src/compiler/response.ts +89 -0
  97. package/src/compiler/sucrose.ts +238 -0
  98. package/src/config/index.ts +167 -0
  99. package/src/console/budget.test.ts +73 -0
  100. package/src/console/index.ts +30 -0
  101. package/src/console/server/access.test.ts +370 -0
  102. package/src/console/server/access.ts +777 -0
  103. package/src/console/server/ai.test.ts +172 -0
  104. package/src/console/server/ai.ts +632 -0
  105. package/src/console/server/app.ts +294 -0
  106. package/src/console/server/auth-rate.test.ts +81 -0
  107. package/src/console/server/auth-rate.ts +72 -0
  108. package/src/console/server/bind.ts +25 -0
  109. package/src/console/server/channels.test.ts +179 -0
  110. package/src/console/server/channels.ts +469 -0
  111. package/src/console/server/claim.test.ts +66 -0
  112. package/src/console/server/claim.ts +134 -0
  113. package/src/console/server/clock.test.ts +239 -0
  114. package/src/console/server/clock.ts +390 -0
  115. package/src/console/server/console.test.ts +338 -0
  116. package/src/console/server/diff.test.ts +299 -0
  117. package/src/console/server/diff.ts +352 -0
  118. package/src/console/server/dry-run.audit.test.ts +118 -0
  119. package/src/console/server/flows-invoke.test.ts +148 -0
  120. package/src/console/server/flows.ts +3633 -0
  121. package/src/console/server/gates.test.ts +360 -0
  122. package/src/console/server/gates.ts +869 -0
  123. package/src/console/server/index.ts +141 -0
  124. package/src/console/server/live.test.ts +30 -0
  125. package/src/console/server/live.ts +109 -0
  126. package/src/console/server/plugin.ts +50 -0
  127. package/src/console/server/plugins.test.ts +131 -0
  128. package/src/console/server/plugins.ts +429 -0
  129. package/src/console/server/runs-pii.ts +98 -0
  130. package/src/console/server/security-headers.ts +97 -0
  131. package/src/console/server/security.gate.test.ts +286 -0
  132. package/src/console/server/serve.ts +319 -0
  133. package/src/console/server/signals.test.ts +88 -0
  134. package/src/console/server/signals.ts +282 -0
  135. package/src/console/server/state.ts +982 -0
  136. package/src/console/server/store.test.ts +233 -0
  137. package/src/console/server/store.ts +861 -0
  138. package/src/console/server/structural.ts +95 -0
  139. package/src/console/server/vault.test.ts +251 -0
  140. package/src/console/server/vault.ts +430 -0
  141. package/src/console/ui/access/AccessA11yView.tsx +237 -0
  142. package/src/console/ui/access/a11y.test.tsx +122 -0
  143. package/src/console/ui/access/acknowledgement.test.ts +13 -0
  144. package/src/console/ui/access/acknowledgement.ts +35 -0
  145. package/src/console/ui/access/blast-radius.test.ts +26 -0
  146. package/src/console/ui/access/blast-radius.ts +40 -0
  147. package/src/console/ui/access/confirmation.test.ts +38 -0
  148. package/src/console/ui/access/confirmation.ts +36 -0
  149. package/src/console/ui/access/fixture.ts +203 -0
  150. package/src/console/ui/access/grantable.test.ts +17 -0
  151. package/src/console/ui/access/grantable.ts +36 -0
  152. package/src/console/ui/access/hygiene.test.ts +12 -0
  153. package/src/console/ui/access/hygiene.ts +39 -0
  154. package/src/console/ui/access/index.ts +54 -0
  155. package/src/console/ui/access/provenance.test.ts +12 -0
  156. package/src/console/ui/access/provenance.ts +29 -0
  157. package/src/console/ui/access/search.test.ts +17 -0
  158. package/src/console/ui/access/search.ts +72 -0
  159. package/src/console/ui/access/types.ts +111 -0
  160. package/src/console/ui/ai/AiA11yView.tsx +265 -0
  161. package/src/console/ui/ai/a11y.test.tsx +99 -0
  162. package/src/console/ui/ai/findings.ts +41 -0
  163. package/src/console/ui/ai/fixture.ts +182 -0
  164. package/src/console/ui/ai/format.ts +90 -0
  165. package/src/console/ui/ai/group.test.ts +37 -0
  166. package/src/console/ui/ai/group.ts +88 -0
  167. package/src/console/ui/ai/index.ts +66 -0
  168. package/src/console/ui/ai/promotion.test.ts +77 -0
  169. package/src/console/ui/ai/promotion.ts +150 -0
  170. package/src/console/ui/ai/search.test.ts +40 -0
  171. package/src/console/ui/ai/search.ts +89 -0
  172. package/src/console/ui/ai/types.ts +139 -0
  173. package/src/console/ui/architecture/ArchitectureA11yView.tsx +163 -0
  174. package/src/console/ui/architecture/a11y.test.tsx +97 -0
  175. package/src/console/ui/architecture/boundary.ts +42 -0
  176. package/src/console/ui/architecture/declared.ts +165 -0
  177. package/src/console/ui/architecture/fixture.ts +74 -0
  178. package/src/console/ui/architecture/index.ts +84 -0
  179. package/src/console/ui/architecture/layers.ts +64 -0
  180. package/src/console/ui/architecture/layout.ts +68 -0
  181. package/src/console/ui/architecture/pathologies.ts +260 -0
  182. package/src/console/ui/architecture/search.ts +145 -0
  183. package/src/console/ui/architecture/traffic.ts +102 -0
  184. package/src/console/ui/architecture/types.ts +144 -0
  185. package/src/console/ui/architecture/view.test.ts +165 -0
  186. package/src/console/ui/architecture/view.ts +516 -0
  187. package/src/console/ui/channels/ChannelsA11yView.tsx +180 -0
  188. package/src/console/ui/channels/a11y.test.tsx +96 -0
  189. package/src/console/ui/channels/confirmation.test.ts +37 -0
  190. package/src/console/ui/channels/confirmation.ts +27 -0
  191. package/src/console/ui/channels/fallback.test.ts +15 -0
  192. package/src/console/ui/channels/fallback.ts +20 -0
  193. package/src/console/ui/channels/findings.ts +38 -0
  194. package/src/console/ui/channels/fixture.ts +134 -0
  195. package/src/console/ui/channels/group.ts +25 -0
  196. package/src/console/ui/channels/index.ts +57 -0
  197. package/src/console/ui/channels/locale.test.ts +17 -0
  198. package/src/console/ui/channels/locale.ts +31 -0
  199. package/src/console/ui/channels/mask.test.ts +14 -0
  200. package/src/console/ui/channels/mask.ts +12 -0
  201. package/src/console/ui/channels/search.ts +58 -0
  202. package/src/console/ui/channels/taxonomy.test.ts +17 -0
  203. package/src/console/ui/channels/taxonomy.ts +49 -0
  204. package/src/console/ui/channels/types.ts +117 -0
  205. package/src/console/ui/clock/ClockA11yView.tsx +182 -0
  206. package/src/console/ui/clock/a11y.test.tsx +99 -0
  207. package/src/console/ui/clock/confirmation.test.ts +32 -0
  208. package/src/console/ui/clock/confirmation.ts +34 -0
  209. package/src/console/ui/clock/findings.ts +40 -0
  210. package/src/console/ui/clock/fixture.ts +111 -0
  211. package/src/console/ui/clock/health.test.ts +25 -0
  212. package/src/console/ui/clock/health.ts +61 -0
  213. package/src/console/ui/clock/index.ts +49 -0
  214. package/src/console/ui/clock/search.test.ts +39 -0
  215. package/src/console/ui/clock/search.ts +76 -0
  216. package/src/console/ui/clock/timeline.test.ts +44 -0
  217. package/src/console/ui/clock/timeline.ts +41 -0
  218. package/src/console/ui/clock/types.ts +78 -0
  219. package/src/console/ui/clock/waiting-on.test.ts +34 -0
  220. package/src/console/ui/clock/waiting-on.ts +83 -0
  221. package/src/console/ui/confirmation.unify.test.ts +92 -0
  222. package/src/console/ui/diff/DiffA11yView.tsx +85 -0
  223. package/src/console/ui/diff/a11y.test.tsx +99 -0
  224. package/src/console/ui/diff/fixture.ts +85 -0
  225. package/src/console/ui/diff/group.test.ts +58 -0
  226. package/src/console/ui/diff/group.ts +97 -0
  227. package/src/console/ui/diff/index.ts +38 -0
  228. package/src/console/ui/diff/search.test.ts +32 -0
  229. package/src/console/ui/diff/search.ts +75 -0
  230. package/src/console/ui/diff/summary.ts +74 -0
  231. package/src/console/ui/diff/types.ts +48 -0
  232. package/src/console/ui/dist/assets/index-B71Yl_SS.js +10 -0
  233. package/src/console/ui/dist/assets/panel-access-Dd37LU2c.js +64 -0
  234. package/src/console/ui/dist/assets/panel-ai-CC7LR6-J.js +1 -0
  235. package/src/console/ui/dist/assets/panel-architecture-B5b3iKCz.js +1 -0
  236. package/src/console/ui/dist/assets/panel-channels-CeNjTKXp.js +1 -0
  237. package/src/console/ui/dist/assets/panel-clock-DgFTLoHV.js +1 -0
  238. package/src/console/ui/dist/assets/panel-diff-DxehccqB.js +1 -0
  239. package/src/console/ui/dist/assets/panel-flows-BtrVn-Eg.js +45 -0
  240. package/src/console/ui/dist/assets/panel-gates-Z9MKRGdH.js +1 -0
  241. package/src/console/ui/dist/assets/panel-overview-Bd48d9km.js +1 -0
  242. package/src/console/ui/dist/assets/panel-plugins-DWd0TowH.js +1 -0
  243. package/src/console/ui/dist/assets/panel-runs-BwsWqKeB.js +1 -0
  244. package/src/console/ui/dist/assets/panel-signals-9najbZY2.js +1 -0
  245. package/src/console/ui/dist/assets/panel-store-OHkP2pDp.js +1 -0
  246. package/src/console/ui/dist/assets/panel-traces-tn2JoY8U.js +1 -0
  247. package/src/console/ui/dist/assets/panel-vault-BbfWdox0.js +1 -0
  248. package/src/console/ui/dist/assets/rolldown-runtime-CNC7AqOf.js +1 -0
  249. package/src/console/ui/dist/assets/style-Cnl7WLya.css +3 -0
  250. package/src/console/ui/dist/index.html +14 -0
  251. package/src/console/ui/flows/FlowsA11yView.tsx +155 -0
  252. package/src/console/ui/flows/a11y.test.tsx +105 -0
  253. package/src/console/ui/flows/buffer.test.ts +91 -0
  254. package/src/console/ui/flows/buffer.ts +118 -0
  255. package/src/console/ui/flows/confirmation.ts +131 -0
  256. package/src/console/ui/flows/contract.test.ts +89 -0
  257. package/src/console/ui/flows/contract.ts +353 -0
  258. package/src/console/ui/flows/fixture.ts +81 -0
  259. package/src/console/ui/flows/graph.test.ts +101 -0
  260. package/src/console/ui/flows/graph.ts +549 -0
  261. package/src/console/ui/flows/index.ts +87 -0
  262. package/src/console/ui/flows/save-as-test.test.ts +47 -0
  263. package/src/console/ui/flows/save-as-test.ts +99 -0
  264. package/src/console/ui/flows/search.ts +255 -0
  265. package/src/console/ui/flows/tiers.ts +140 -0
  266. package/src/console/ui/gates/GatesA11yView.tsx +192 -0
  267. package/src/console/ui/gates/a11y.test.tsx +116 -0
  268. package/src/console/ui/gates/audit.test.ts +40 -0
  269. package/src/console/ui/gates/audit.ts +63 -0
  270. package/src/console/ui/gates/denial.test.ts +36 -0
  271. package/src/console/ui/gates/denial.ts +51 -0
  272. package/src/console/ui/gates/findings.ts +19 -0
  273. package/src/console/ui/gates/fixture.ts +189 -0
  274. package/src/console/ui/gates/group.test.ts +30 -0
  275. package/src/console/ui/gates/group.ts +102 -0
  276. package/src/console/ui/gates/index.ts +47 -0
  277. package/src/console/ui/gates/search.test.ts +49 -0
  278. package/src/console/ui/gates/search.ts +115 -0
  279. package/src/console/ui/gates/types.ts +121 -0
  280. package/src/console/ui/overview/OverviewA11yView.tsx +152 -0
  281. package/src/console/ui/overview/a11y.test.tsx +113 -0
  282. package/src/console/ui/overview/busiest.ts +26 -0
  283. package/src/console/ui/overview/compose.ts +133 -0
  284. package/src/console/ui/overview/cost.test.ts +39 -0
  285. package/src/console/ui/overview/cost.ts +138 -0
  286. package/src/console/ui/overview/fixture.ts +161 -0
  287. package/src/console/ui/overview/golden.ts +57 -0
  288. package/src/console/ui/overview/index.ts +59 -0
  289. package/src/console/ui/overview/rank.test.ts +136 -0
  290. package/src/console/ui/overview/rank.ts +210 -0
  291. package/src/console/ui/overview/slo.test.ts +73 -0
  292. package/src/console/ui/overview/slo.ts +229 -0
  293. package/src/console/ui/overview/types.ts +121 -0
  294. package/src/console/ui/overview/verdict.ts +131 -0
  295. package/src/console/ui/plugins/PluginsA11yView.tsx +227 -0
  296. package/src/console/ui/plugins/a11y.test.tsx +97 -0
  297. package/src/console/ui/plugins/command.test.ts +22 -0
  298. package/src/console/ui/plugins/command.ts +27 -0
  299. package/src/console/ui/plugins/findings.ts +39 -0
  300. package/src/console/ui/plugins/fixture.ts +233 -0
  301. package/src/console/ui/plugins/group.test.ts +38 -0
  302. package/src/console/ui/plugins/group.ts +67 -0
  303. package/src/console/ui/plugins/index.ts +40 -0
  304. package/src/console/ui/plugins/search.ts +67 -0
  305. package/src/console/ui/plugins/types.ts +115 -0
  306. package/src/console/ui/runs/RunsA11yView.tsx +267 -0
  307. package/src/console/ui/runs/a11y.test.tsx +99 -0
  308. package/src/console/ui/runs/explain.test.ts +23 -0
  309. package/src/console/ui/runs/explain.ts +32 -0
  310. package/src/console/ui/runs/fixture.ts +189 -0
  311. package/src/console/ui/runs/group.test.ts +24 -0
  312. package/src/console/ui/runs/group.ts +85 -0
  313. package/src/console/ui/runs/histogram.test.ts +31 -0
  314. package/src/console/ui/runs/histogram.ts +85 -0
  315. package/src/console/ui/runs/index.ts +75 -0
  316. package/src/console/ui/runs/project.ts +122 -0
  317. package/src/console/ui/runs/query.test.ts +50 -0
  318. package/src/console/ui/runs/query.ts +283 -0
  319. package/src/console/ui/runs/search.test.ts +45 -0
  320. package/src/console/ui/runs/search.ts +141 -0
  321. package/src/console/ui/runs/trace-link.test.ts +30 -0
  322. package/src/console/ui/runs/trace-link.ts +99 -0
  323. package/src/console/ui/runs/types.ts +137 -0
  324. package/src/console/ui/shell/App.tsx +56 -0
  325. package/src/console/ui/shell/client.ts +1715 -0
  326. package/src/console/ui/shell/components/ui.tsx +110 -0
  327. package/src/console/ui/shell/index.html +12 -0
  328. package/src/console/ui/shell/layout/Shell.tsx +95 -0
  329. package/src/console/ui/shell/main.tsx +281 -0
  330. package/src/console/ui/shell/panels/Access.tsx +5 -0
  331. package/src/console/ui/shell/panels/Ai.tsx +5 -0
  332. package/src/console/ui/shell/panels/Architecture.tsx +5 -0
  333. package/src/console/ui/shell/panels/Channels.tsx +5 -0
  334. package/src/console/ui/shell/panels/Clock.tsx +5 -0
  335. package/src/console/ui/shell/panels/Diff.tsx +5 -0
  336. package/src/console/ui/shell/panels/Flows.tsx +5 -0
  337. package/src/console/ui/shell/panels/Gates.tsx +5 -0
  338. package/src/console/ui/shell/panels/Overview.tsx +6 -0
  339. package/src/console/ui/shell/panels/Plugins.tsx +5 -0
  340. package/src/console/ui/shell/panels/Runs.tsx +5 -0
  341. package/src/console/ui/shell/panels/Signals.tsx +5 -0
  342. package/src/console/ui/shell/panels/Store.tsx +5 -0
  343. package/src/console/ui/shell/panels/Traces.tsx +5 -0
  344. package/src/console/ui/shell/panels/Vault.tsx +5 -0
  345. package/src/console/ui/shell/panels/access/AccessPanel.tsx +813 -0
  346. package/src/console/ui/shell/panels/ai/AiPanel.tsx +563 -0
  347. package/src/console/ui/shell/panels/architecture/ArchitecturePanel.tsx +377 -0
  348. package/src/console/ui/shell/panels/channels/ChannelsPanel.tsx +454 -0
  349. package/src/console/ui/shell/panels/clock/ClockPanel.tsx +479 -0
  350. package/src/console/ui/shell/panels/diff/DiffPanel.tsx +275 -0
  351. package/src/console/ui/shell/panels/flows/ContractEditor.tsx +301 -0
  352. package/src/console/ui/shell/panels/flows/FlowDrawer.tsx +350 -0
  353. package/src/console/ui/shell/panels/flows/FlowsPanel.tsx +502 -0
  354. package/src/console/ui/shell/panels/gates/GatesPanel.tsx +482 -0
  355. package/src/console/ui/shell/panels/overview/OverviewPanel.tsx +428 -0
  356. package/src/console/ui/shell/panels/plugins/PluginsPanel.tsx +413 -0
  357. package/src/console/ui/shell/panels/runs/RunsPanel.tsx +649 -0
  358. package/src/console/ui/shell/panels/signals/SignalsPanel.tsx +840 -0
  359. package/src/console/ui/shell/panels/store/StorePanel.tsx +787 -0
  360. package/src/console/ui/shell/panels/traces/TracesPanel.tsx +539 -0
  361. package/src/console/ui/shell/panels/vault/VaultPanel.tsx +449 -0
  362. package/src/console/ui/shell/plugin/Sandbox.tsx +29 -0
  363. package/src/console/ui/shell/setup/Wizard.tsx +189 -0
  364. package/src/console/ui/shell/styles.css +75 -0
  365. package/src/console/ui/signals/SignalsA11yView.tsx +276 -0
  366. package/src/console/ui/signals/a11y.test.tsx +99 -0
  367. package/src/console/ui/signals/confirmation.test.ts +39 -0
  368. package/src/console/ui/signals/confirmation.ts +63 -0
  369. package/src/console/ui/signals/dry-run.test.ts +18 -0
  370. package/src/console/ui/signals/dry-run.ts +36 -0
  371. package/src/console/ui/signals/durable.test.ts +22 -0
  372. package/src/console/ui/signals/durable.ts +41 -0
  373. package/src/console/ui/signals/findings.ts +33 -0
  374. package/src/console/ui/signals/fixture.ts +242 -0
  375. package/src/console/ui/signals/group.test.ts +32 -0
  376. package/src/console/ui/signals/group.ts +49 -0
  377. package/src/console/ui/signals/index.ts +52 -0
  378. package/src/console/ui/signals/monitor.test.ts +30 -0
  379. package/src/console/ui/signals/monitor.ts +79 -0
  380. package/src/console/ui/signals/schema-form.test.ts +34 -0
  381. package/src/console/ui/signals/schema-form.ts +112 -0
  382. package/src/console/ui/signals/search.ts +102 -0
  383. package/src/console/ui/signals/types.ts +71 -0
  384. package/src/console/ui/store/StoreA11yView.tsx +199 -0
  385. package/src/console/ui/store/a11y.test.tsx +99 -0
  386. package/src/console/ui/store/cache-view.test.ts +13 -0
  387. package/src/console/ui/store/cache-view.ts +36 -0
  388. package/src/console/ui/store/confirmation.test.ts +38 -0
  389. package/src/console/ui/store/confirmation.ts +55 -0
  390. package/src/console/ui/store/dry-run.test.ts +15 -0
  391. package/src/console/ui/store/dry-run.ts +29 -0
  392. package/src/console/ui/store/fixture.ts +163 -0
  393. package/src/console/ui/store/group.test.ts +21 -0
  394. package/src/console/ui/store/group.ts +41 -0
  395. package/src/console/ui/store/index.ts +40 -0
  396. package/src/console/ui/store/search.ts +78 -0
  397. package/src/console/ui/store/types.ts +65 -0
  398. package/src/console/ui/store/will-not-fire.test.ts +19 -0
  399. package/src/console/ui/store/will-not-fire.ts +40 -0
  400. package/src/console/ui/traces/TracesA11yView.tsx +200 -0
  401. package/src/console/ui/traces/a11y.test.tsx +97 -0
  402. package/src/console/ui/traces/chain.test.ts +61 -0
  403. package/src/console/ui/traces/chain.ts +144 -0
  404. package/src/console/ui/traces/critical-path.test.ts +24 -0
  405. package/src/console/ui/traces/critical-path.ts +59 -0
  406. package/src/console/ui/traces/filter.test.ts +78 -0
  407. package/src/console/ui/traces/filter.ts +112 -0
  408. package/src/console/ui/traces/fixture.ts +126 -0
  409. package/src/console/ui/traces/fold.test.ts +67 -0
  410. package/src/console/ui/traces/fold.ts +170 -0
  411. package/src/console/ui/traces/index.ts +64 -0
  412. package/src/console/ui/traces/mini.ts +65 -0
  413. package/src/console/ui/traces/replay.test.ts +24 -0
  414. package/src/console/ui/traces/replay.ts +35 -0
  415. package/src/console/ui/traces/sampling.test.ts +26 -0
  416. package/src/console/ui/traces/sampling.ts +66 -0
  417. package/src/console/ui/traces/search.ts +148 -0
  418. package/src/console/ui/traces/tier.ts +72 -0
  419. package/src/console/ui/traces/types.ts +125 -0
  420. package/src/console/ui/tsconfig.json +20 -0
  421. package/src/console/ui/vault/VaultA11yView.tsx +177 -0
  422. package/src/console/ui/vault/a11y.test.tsx +97 -0
  423. package/src/console/ui/vault/blast-radius.test.ts +35 -0
  424. package/src/console/ui/vault/blast-radius.ts +55 -0
  425. package/src/console/ui/vault/confirmation.test.ts +41 -0
  426. package/src/console/ui/vault/confirmation.ts +39 -0
  427. package/src/console/ui/vault/dormant.test.ts +37 -0
  428. package/src/console/ui/vault/dormant.ts +28 -0
  429. package/src/console/ui/vault/export-safe.test.ts +30 -0
  430. package/src/console/ui/vault/export-safe.ts +65 -0
  431. package/src/console/ui/vault/fixture.ts +75 -0
  432. package/src/console/ui/vault/group.test.ts +25 -0
  433. package/src/console/ui/vault/group.ts +50 -0
  434. package/src/console/ui/vault/index.ts +45 -0
  435. package/src/console/ui/vault/search.ts +62 -0
  436. package/src/console/ui/vault/types.ts +57 -0
  437. package/src/console/ui/vite.config.ts +84 -0
  438. package/src/console/xss.gate.test.ts +118 -0
  439. package/src/docker/compose.ts +226 -0
  440. package/src/docker/credentials.ts +25 -0
  441. package/src/docker/derive.ts +83 -0
  442. package/src/docker/docker.test.ts +185 -0
  443. package/src/docker/dockerfile.integration.test.ts +170 -0
  444. package/src/docker/dockerfile.ts +39 -0
  445. package/src/docker/helpers.ts +103 -0
  446. package/src/docker/index.ts +52 -0
  447. package/src/docker/pin.ts +90 -0
  448. package/src/docker/recipes/index.ts +33 -0
  449. package/src/docker/recipes/postgres.ts +18 -0
  450. package/src/docker/recipes/redis.ts +18 -0
  451. package/src/docker/stack.integration.test.ts +140 -0
  452. package/src/docker/stack.ts +62 -0
  453. package/src/docker/types.ts +134 -0
  454. package/src/drivers/ai-anthropic.ts +128 -0
  455. package/src/drivers/ai-mock.ts +82 -0
  456. package/src/drivers/ai-openai-compatible.ts +139 -0
  457. package/src/drivers/ai-providers.test.ts +163 -0
  458. package/src/drivers/ai-types.ts +83 -0
  459. package/src/drivers/bun-native-completeness.test.ts +155 -0
  460. package/src/drivers/channel-console.ts +118 -0
  461. package/src/drivers/channel-fcm.ts +96 -0
  462. package/src/drivers/channel-resend.ts +27 -0
  463. package/src/drivers/channel-smtp.ts +33 -0
  464. package/src/drivers/channel-types.ts +160 -0
  465. package/src/drivers/channel-unifonic.ts +79 -0
  466. package/src/drivers/channel-wa-cloud.ts +84 -0
  467. package/src/drivers/channel-webpush.ts +317 -0
  468. package/src/drivers/conformance.test.ts +89 -0
  469. package/src/drivers/conformance.ts +162 -0
  470. package/src/drivers/fs.ts +73 -0
  471. package/src/drivers/index.ts +194 -0
  472. package/src/drivers/kv-lua.ts +222 -0
  473. package/src/drivers/memory.ts +472 -0
  474. package/src/drivers/pgvector.ts +130 -0
  475. package/src/drivers/postgres.ts +221 -0
  476. package/src/drivers/redis.ts +254 -0
  477. package/src/drivers/s3.ts +129 -0
  478. package/src/drivers/signal-engine.ts +690 -0
  479. package/src/drivers/signal-memory.ts +27 -0
  480. package/src/drivers/signal-nats.ts +131 -0
  481. package/src/drivers/signal-postgres.ts +1089 -0
  482. package/src/drivers/signal-redis.ts +275 -0
  483. package/src/drivers/signal-types.ts +328 -0
  484. package/src/drivers/sqlite.ts +53 -0
  485. package/src/drivers/types.ts +334 -0
  486. package/src/drivers/vault-dotenv-parse.ts +56 -0
  487. package/src/drivers/vault-env.ts +101 -0
  488. package/src/drivers/vault-infisical.ts +61 -0
  489. package/src/drivers/vault-managed.ts +42 -0
  490. package/src/drivers/vault-memory.ts +38 -0
  491. package/src/drivers/vault-openbao.ts +80 -0
  492. package/src/drivers/vault-sops.ts +252 -0
  493. package/src/drivers/vault-types.ts +84 -0
  494. package/src/elements/ai/declare.ts +206 -0
  495. package/src/elements/ai/eval.ts +113 -0
  496. package/src/elements/ai/pii.ts +108 -0
  497. package/src/elements/ai/runtime.ts +540 -0
  498. package/src/elements/ai/schema.ts +194 -0
  499. package/src/elements/ai.test.ts +393 -0
  500. package/src/elements/ai.ts +56 -0
  501. package/src/elements/channel/consent.ts +81 -0
  502. package/src/elements/channel/costs.test.ts +62 -0
  503. package/src/elements/channel/costs.ts +130 -0
  504. package/src/elements/channel/declare.ts +141 -0
  505. package/src/elements/channel/email-auth.test.ts +47 -0
  506. package/src/elements/channel/email-auth.ts +109 -0
  507. package/src/elements/channel/locale.ts +97 -0
  508. package/src/elements/channel/mask.test.ts +19 -0
  509. package/src/elements/channel/mask.ts +60 -0
  510. package/src/elements/channel/mime.ts +23 -0
  511. package/src/elements/channel/outcomes.test.ts +40 -0
  512. package/src/elements/channel/outcomes.ts +126 -0
  513. package/src/elements/channel/receipts.ts +121 -0
  514. package/src/elements/channel/runtime.ts +500 -0
  515. package/src/elements/channel/suppression.ts +136 -0
  516. package/src/elements/channel.test.ts +232 -0
  517. package/src/elements/channel.ts +109 -0
  518. package/src/elements/clock/actions.ts +170 -0
  519. package/src/elements/clock/declare.ts +58 -0
  520. package/src/elements/clock/dst.ts +197 -0
  521. package/src/elements/clock/durable.ts +140 -0
  522. package/src/elements/clock/duration.ts +29 -0
  523. package/src/elements/clock/health.test.ts +103 -0
  524. package/src/elements/clock/health.ts +112 -0
  525. package/src/elements/clock/leader.ts +127 -0
  526. package/src/elements/clock/reconcile.ts +241 -0
  527. package/src/elements/clock/runtime.ts +243 -0
  528. package/src/elements/clock/schedule.ts +262 -0
  529. package/src/elements/clock/time-travel.ts +53 -0
  530. package/src/elements/clock.test.ts +365 -0
  531. package/src/elements/clock.ts +80 -0
  532. package/src/elements/gate/declare.ts +134 -0
  533. package/src/elements/gate/permissions.ts +104 -0
  534. package/src/elements/gate/runtime.ts +177 -0
  535. package/src/elements/gate/strategies.ts +388 -0
  536. package/src/elements/gate.test.ts +145 -0
  537. package/src/elements/gate.ts +41 -0
  538. package/src/elements/index.ts +169 -0
  539. package/src/elements/signal/chaos-child.ts +47 -0
  540. package/src/elements/signal/declare.ts +77 -0
  541. package/src/elements/signal/dry-run-replay.test.ts +95 -0
  542. package/src/elements/signal/dry-run-write-isolation.test.ts +94 -0
  543. package/src/elements/signal/reconcile.test.ts +23 -0
  544. package/src/elements/signal/reconcile.ts +122 -0
  545. package/src/elements/signal/runtime.ts +94 -0
  546. package/src/elements/signal.test.ts +457 -0
  547. package/src/elements/signal.ts +26 -0
  548. package/src/elements/store/cache.ts +197 -0
  549. package/src/elements/store/classify.ts +125 -0
  550. package/src/elements/store/declare.ts +200 -0
  551. package/src/elements/store/files-policy.test.ts +31 -0
  552. package/src/elements/store/files-policy.ts +70 -0
  553. package/src/elements/store/replica.ts +68 -0
  554. package/src/elements/store/runtime.ts +393 -0
  555. package/src/elements/store/sql-condition.ts +205 -0
  556. package/src/elements/store/sql-session.ts +579 -0
  557. package/src/elements/store/table.ts +282 -0
  558. package/src/elements/store.test.ts +370 -0
  559. package/src/elements/store.ts +99 -0
  560. package/src/elements/vault/declare.ts +143 -0
  561. package/src/elements/vault/fingerprint.ts +30 -0
  562. package/src/elements/vault/redact.ts +52 -0
  563. package/src/elements/vault/runtime.ts +461 -0
  564. package/src/elements/vault.test.ts +296 -0
  565. package/src/elements/vault.ts +44 -0
  566. package/src/index.ts +183 -0
  567. package/src/kernel/adopt-routes.ts +194 -0
  568. package/src/kernel/app.ts +1068 -0
  569. package/src/kernel/auth-resolve.ts +93 -0
  570. package/src/kernel/boot.test.ts +173 -0
  571. package/src/kernel/boot.ts +439 -0
  572. package/src/kernel/budget-entry.ts +55 -0
  573. package/src/kernel/budget.test.ts +19 -0
  574. package/src/kernel/call.test.ts +83 -0
  575. package/src/kernel/capability.ts +113 -0
  576. package/src/kernel/dry-run.test.ts +64 -0
  577. package/src/kernel/dry-run.ts +171 -0
  578. package/src/kernel/effects.test.ts +89 -0
  579. package/src/kernel/effects.ts +140 -0
  580. package/src/kernel/errors.registry.test.ts +39 -0
  581. package/src/kernel/errors.ts +225 -0
  582. package/src/kernel/flow.test.ts +154 -0
  583. package/src/kernel/flow.ts +290 -0
  584. package/src/kernel/fx.test.ts +245 -0
  585. package/src/kernel/fx.ts +996 -0
  586. package/src/kernel/hook-timing.test.ts +72 -0
  587. package/src/kernel/hook-timing.ts +190 -0
  588. package/src/kernel/hooks.test.ts +181 -0
  589. package/src/kernel/hooks.ts +275 -0
  590. package/src/kernel/index.ts +278 -0
  591. package/src/kernel/journal.ts +405 -0
  592. package/src/kernel/journey.ts +75 -0
  593. package/src/kernel/on.ts +74 -0
  594. package/src/kernel/pipeline.test.ts +349 -0
  595. package/src/kernel/pipeline.ts +245 -0
  596. package/src/kernel/plug.ts +82 -0
  597. package/src/kernel/plugin/capabilities.test.ts +84 -0
  598. package/src/kernel/plugin/conflicts.test.ts +106 -0
  599. package/src/kernel/plugin/decorate.test.ts +111 -0
  600. package/src/kernel/plugin/scoping.test.ts +138 -0
  601. package/src/kernel/plugin.ts +485 -0
  602. package/src/kernel/rate-limit.ts +25 -0
  603. package/src/kernel/registry.ts +398 -0
  604. package/src/kernel/router.test.ts +114 -0
  605. package/src/kernel/router.ts +556 -0
  606. package/src/kernel/routing-budget.test.ts +19 -0
  607. package/src/kernel/run-telemetry.ts +70 -0
  608. package/src/kernel/triggers.ts +324 -0
  609. package/src/kernel/unit.ts +88 -0
  610. package/src/manifest/diff.test.ts +866 -0
  611. package/src/manifest/diff.ts +1670 -0
  612. package/src/manifest/fixtures/base.manifest.json +112 -0
  613. package/src/manifest/fixtures/skyport.excerpt.json +51 -0
  614. package/src/manifest/fixtures/skyport.manifest.json +153 -0
  615. package/src/manifest/index.ts +27 -0
  616. package/src/manifest/types.ts +358 -0
  617. package/src/manifest/undeclared.test.ts +82 -0
  618. package/src/manifest/undeclared.ts +65 -0
  619. package/src/manifest/validate.test.ts +105 -0
  620. package/src/manifest/validate.ts +174 -0
  621. package/src/mcp/authorization.ts +340 -0
  622. package/src/mcp/confirmation.ts +174 -0
  623. package/src/mcp/data.ts +90 -0
  624. package/src/mcp/index.ts +80 -0
  625. package/src/mcp/injection.gate.test.ts +176 -0
  626. package/src/mcp/mcp.test.ts +348 -0
  627. package/src/mcp/protocol.ts +172 -0
  628. package/src/mcp/server.ts +283 -0
  629. package/src/mcp/session.ts +139 -0
  630. package/src/mcp/tools.ts +391 -0
  631. package/src/plugins/catalogue.test.ts +48 -0
  632. package/src/plugins/catalogue.ts +113 -0
  633. package/src/plugins/node-import-scan.test.ts +28 -0
  634. package/src/plugins/node-import-scan.ts +98 -0
  635. package/src/plugins/supply-chain.test.ts +48 -0
  636. package/src/plugins/supply-chain.ts +460 -0
  637. package/src/release/index.ts +30 -0
  638. package/src/release/limits.ts +20 -0
  639. package/src/release/measure.ts +443 -0
  640. package/src/release/publish.ts +56 -0
  641. package/src/runs/bench.test.ts +93 -0
  642. package/src/runs/collect.ts +144 -0
  643. package/src/runs/drivers/clickhouse.ts +108 -0
  644. package/src/runs/drivers/files.ts +201 -0
  645. package/src/runs/drivers/memory.ts +94 -0
  646. package/src/runs/drivers/postgres.ts +110 -0
  647. package/src/runs/duckdb.ts +99 -0
  648. package/src/runs/index.ts +82 -0
  649. package/src/runs/outlier.ts +180 -0
  650. package/src/runs/parquet.ts +210 -0
  651. package/src/runs/privacy.ts +49 -0
  652. package/src/runs/runs.test.ts +324 -0
  653. package/src/runs/runtime.ts +229 -0
  654. package/src/runs/shred.ts +243 -0
  655. package/src/runs/telemetry.ts +10 -0
  656. package/src/runs/types.ts +179 -0
  657. package/src/runtime/bun.ts +140 -0
  658. package/src/runtime/cold-start.bench.ts +20 -0
  659. package/src/runtime/index.ts +46 -0
  660. package/src/runtime/primitives.ts +242 -0
  661. package/src/runtime/security.test.ts +170 -0
  662. package/src/runtime/security.ts +168 -0
  663. package/src/runtime/serve.test.ts +223 -0
  664. package/src/runtime/types.ts +217 -0
  665. package/src/runtime/web-standard.ts +77 -0
  666. package/src/test/create-test-app.test.ts +161 -0
  667. package/src/test/create-test-app.ts +533 -0
  668. package/src/test/index.ts +21 -0
  669. package/src/test/provisions.integration.test.ts +174 -0
  670. package/src/upgrade/codemods.test.ts +58 -0
  671. package/src/upgrade/codemods.ts +83 -0
  672. package/src/validation/index.ts +23 -0
  673. package/src/validation/standard-schema.test.ts +96 -0
  674. package/src/validation/standard-schema.ts +316 -0
@@ -0,0 +1,1368 @@
1
+ /**
2
+ * Manifest extraction — oxc parse → bindings → flows → effects → Manifest.
3
+ *
4
+ * Never uses the TypeScript compiler API (unified-theory §11). `tsc` remains
5
+ * for type-checking only; this module is the build-time close-the-loop path
6
+ * from source to `manifest.oke.json`.
7
+ */
8
+
9
+ import { parseSync } from "oxc-parser";
10
+
11
+ import type {
12
+ Ai,
13
+ AiAgent,
14
+ AiModel,
15
+ AiPrompt,
16
+ Channel,
17
+ Clock,
18
+ Effects,
19
+ Flow,
20
+ Gate,
21
+ Journey,
22
+ Manifest,
23
+ RateStrategy,
24
+ SecretContract,
25
+ Signal,
26
+ SignalDelivery,
27
+ Slo,
28
+ Store,
29
+ Trigger,
30
+ } from "../manifest/types.ts";
31
+ import {
32
+ identifierName,
33
+ inferEffects,
34
+ stringArg,
35
+ walk,
36
+ type AstNode,
37
+ type CallExpression,
38
+ type Identifier,
39
+ type InferBinding,
40
+ type Literal,
41
+ } from "./effects-infer.ts";
42
+
43
+ /** One source file for extraction. */
44
+ export interface SourceFile {
45
+ /** Path relative to the app root (used in `source` and for imports). */
46
+ readonly path: string;
47
+ /** File contents. */
48
+ readonly source: string;
49
+ }
50
+
51
+ /** Options for {@link extractManifest}. */
52
+ export interface ExtractManifestOptions {
53
+ /** Application root (for reading files when `files` is omitted). */
54
+ readonly rootDir?: string;
55
+ /** Explicit sources (tests / synthetic apps). */
56
+ readonly files?: readonly SourceFile[];
57
+ /** Glob under `rootDir` (default: all TypeScript files recursively). */
58
+ readonly glob?: string;
59
+ /** Override app name when `oke({ name })` is absent. */
60
+ readonly app?: string;
61
+ }
62
+
63
+ /** Internal project scope accumulated across files. */
64
+ interface ProjectScope {
65
+ app: string;
66
+ bindings: Map<string, InferBinding>;
67
+ signals: Record<string, Signal>;
68
+ stores: Record<string, Store>;
69
+ clocks: Record<string, Clock>;
70
+ gates: Record<string, Gate>;
71
+ /** Local binding name → gate manifest id (policy name or rate expression). */
72
+ gateIds: Map<string, string>;
73
+ vault: Record<string, SecretContract>;
74
+ channels: Record<string, Channel>;
75
+ ai: Ai;
76
+ journeys: Record<string, Journey>;
77
+ drivers: Record<string, string[]>;
78
+ tenancy?: Manifest["tenancy"];
79
+ i18n?: Manifest["i18n"];
80
+ topology?: Manifest["topology"];
81
+ images?: Record<string, string>;
82
+ flows: Record<string, Flow>;
83
+ /** Export name → flow id (for agent tools). */
84
+ flowExports: Map<string, string>;
85
+ }
86
+
87
+ /**
88
+ * Extract a Manifest from TypeScript sources via oxc.
89
+ *
90
+ * @param options - Root directory and/or explicit files
91
+ */
92
+ export async function extractManifest(
93
+ options: ExtractManifestOptions = {},
94
+ ): Promise<Manifest> {
95
+ const files = options.files
96
+ ? [...options.files]
97
+ : await readSources(options.rootDir ?? ".", options.glob ?? "**/*.{ts,tsx}");
98
+
99
+ const scope: ProjectScope = {
100
+ app: options.app ?? "app",
101
+ bindings: new Map(),
102
+ signals: {},
103
+ stores: {},
104
+ clocks: {},
105
+ gates: {},
106
+ gateIds: new Map(),
107
+ vault: {},
108
+ channels: {},
109
+ ai: {},
110
+ journeys: {},
111
+ drivers: {},
112
+ flows: {},
113
+ flowExports: new Map(),
114
+ };
115
+
116
+ const parsed = files.map((file) => {
117
+ const result = parseSync(file.path, file.source, {
118
+ sourceType: "module",
119
+ lang: file.path.endsWith("x") ? "tsx" : "ts",
120
+ });
121
+ return { file, program: result.program as unknown as AstNode };
122
+ });
123
+
124
+ // Pass 1 — declarations (elements, config, named bindings).
125
+ for (const { program } of parsed) {
126
+ collectDeclarations(program, scope);
127
+ }
128
+
129
+ // Pass 2 — flows / on() bindings.
130
+ for (const { file, program } of parsed) {
131
+ collectFlows(file, program, scope);
132
+ }
133
+
134
+ // Pass 3 — resolve deferred refs (agent tools, journey paths).
135
+ finalizeRefs(scope);
136
+
137
+ // Stable ordering for golden files.
138
+ const flows = sortRecord(scope.flows);
139
+ const signals = sortRecord(scope.signals);
140
+ const stores = sortRecord(scope.stores);
141
+ const clocks = sortRecord(scope.clocks);
142
+ const gates = sortRecord(scope.gates);
143
+ const vault = sortRecord(scope.vault);
144
+ const channels = sortRecord(scope.channels);
145
+ const journeys = sortRecord(scope.journeys);
146
+ const drivers = sortRecord(scope.drivers);
147
+
148
+ const manifest: Manifest = {
149
+ oke: "1.0",
150
+ app: scope.app,
151
+ };
152
+
153
+ if (Object.keys(flows).length > 0) manifest.flows = flows;
154
+ if (Object.keys(signals).length > 0) manifest.signals = signals;
155
+ if (Object.keys(stores).length > 0) manifest.stores = stores;
156
+ if (Object.keys(clocks).length > 0) manifest.clocks = clocks;
157
+ if (Object.keys(gates).length > 0) manifest.gates = gates;
158
+ if (Object.keys(vault).length > 0) manifest.vault = vault;
159
+ if (Object.keys(channels).length > 0) manifest.channels = channels;
160
+ if (scope.ai.models || scope.ai.prompts || scope.ai.agents) {
161
+ manifest.ai = {
162
+ ...(scope.ai.models ? { models: sortRecord(scope.ai.models) } : {}),
163
+ ...(scope.ai.prompts ? { prompts: sortRecord(scope.ai.prompts) } : {}),
164
+ ...(scope.ai.agents ? { agents: sortRecord(scope.ai.agents) } : {}),
165
+ };
166
+ }
167
+ if (Object.keys(journeys).length > 0) manifest.journeys = journeys;
168
+ if (Object.keys(drivers).length > 0) manifest.drivers = drivers;
169
+ if (scope.tenancy) manifest.tenancy = scope.tenancy;
170
+ if (scope.i18n) manifest.i18n = scope.i18n;
171
+ if (scope.topology) manifest.topology = scope.topology;
172
+ if (scope.images) manifest.images = scope.images;
173
+
174
+ return manifest;
175
+ }
176
+
177
+ /**
178
+ * Extract from an in-memory map of path → source (convenience for tests).
179
+ *
180
+ * @param sources - Path → source text
181
+ * @param app - Optional app name override
182
+ */
183
+ export async function extractFromSources(
184
+ sources: Readonly<Record<string, string>>,
185
+ app?: string,
186
+ ): Promise<Manifest> {
187
+ const files = Object.entries(sources).map(([path, source]) => ({
188
+ path,
189
+ source,
190
+ }));
191
+ return extractManifest({ files, app });
192
+ }
193
+
194
+ async function readSources(
195
+ rootDir: string,
196
+ pattern: string,
197
+ ): Promise<SourceFile[]> {
198
+ // Bun.Glob (global) — bare `import … from "bun"` is rejected by JSR.
199
+ const glob = new Bun.Glob(pattern);
200
+ const files: SourceFile[] = [];
201
+ for await (const path of glob.scan({
202
+ cwd: rootDir,
203
+ onlyFiles: true,
204
+ })) {
205
+ if (path.includes("node_modules/") || path.endsWith(".test.ts")) continue;
206
+ const abs = `${rootDir.replace(/\/$/, "")}/${path}`;
207
+ files.push({ path, source: await Bun.file(abs).text() });
208
+ }
209
+ files.sort((a, b) => a.path.localeCompare(b.path));
210
+ return files;
211
+ }
212
+
213
+ function collectDeclarations(program: AstNode, scope: ProjectScope): void {
214
+ walk(program, (node) => {
215
+ if (node.type === "CallExpression") {
216
+ visitDeclarationCall(node as CallExpression, program, scope);
217
+ }
218
+ if (node.type === "VariableDeclarator") {
219
+ visitDeclarator(node as AstNode, scope);
220
+ }
221
+ });
222
+
223
+ // oke({ name })
224
+ walk(program, (node) => {
225
+ if (node.type !== "CallExpression") return;
226
+ const call = node as CallExpression;
227
+ if (identifierName(call.callee) !== "oke") return;
228
+ const opts = objectArg(call.arguments[0]);
229
+ const name = stringProp(opts, "name");
230
+ if (name) scope.app = name;
231
+ });
232
+ }
233
+
234
+ function visitDeclarator(decl: AstNode, scope: ProjectScope): void {
235
+ const id = (decl as AstNode & { id?: AstNode }).id;
236
+ const init = (decl as AstNode & { init?: AstNode }).init;
237
+ if (!id || !init) return;
238
+ const name = identifierName(id);
239
+ if (!name) return;
240
+
241
+ // export const x = on(...) / flow(...) — flow export alias
242
+ if (init.type === "CallExpression") {
243
+ const call = init as CallExpression;
244
+ const callee = identifierName(call.callee);
245
+ if (callee === "on" || callee === "flow") {
246
+ // Resolved in pass 2 once the flow name is known; stash export hint.
247
+ scope.flowExports.set(name, name);
248
+ }
249
+ }
250
+
251
+ // Re-exports / aliases: const canBook = bookingCreateGate
252
+ if (init.type === "Identifier") {
253
+ const target = (init as Identifier).name;
254
+ const binding = scope.bindings.get(target);
255
+ if (binding) scope.bindings.set(name, binding);
256
+ const gateId = scope.gateIds.get(target);
257
+ if (gateId) scope.gateIds.set(name, gateId);
258
+ }
259
+ }
260
+
261
+ function finalizeRefs(scope: ProjectScope): void {
262
+ if (scope.ai.agents) {
263
+ for (const agent of Object.values(scope.ai.agents)) {
264
+ if (!agent.tools) continue;
265
+ agent.tools = agent.tools.map(
266
+ (id) => scope.flowExports.get(id) ?? scope.bindings.get(id)?.ref ?? id,
267
+ );
268
+ }
269
+ }
270
+ for (const journey of Object.values(scope.journeys)) {
271
+ if (!journey.flows) continue;
272
+ journey.flows = journey.flows.map(
273
+ (id) => scope.flowExports.get(id) ?? scope.bindings.get(id)?.ref ?? id,
274
+ );
275
+ }
276
+ }
277
+
278
+ function visitDeclarationCall(
279
+ call: CallExpression,
280
+ program: AstNode,
281
+ scope: ProjectScope,
282
+ ): void {
283
+ const callee = call.callee;
284
+
285
+ // store.sql("name") / store.kv(...) / …
286
+ if (callee.type === "MemberExpression") {
287
+ const member = callee as AstNode & {
288
+ object: AstNode;
289
+ property: AstNode;
290
+ };
291
+ const obj = identifierName(member.object);
292
+ const prop = identifierName(member.property);
293
+
294
+ if (obj === "store" && prop) {
295
+ const facets = ["sql", "kv", "files", "index"] as const;
296
+ if ((facets as readonly string[]).includes(prop)) {
297
+ const facet = prop as (typeof facets)[number];
298
+ const storeName = stringArg(call.arguments[0]) ?? "store";
299
+ const ref = `${facet}:${storeName}` as const;
300
+ const bindingName = enclosingConstName(call, program);
301
+ scope.stores[storeName] = scope.stores[storeName] ?? { facet };
302
+ if (bindingName) {
303
+ scope.bindings.set(bindingName, {
304
+ kind: "store",
305
+ ref,
306
+ facet,
307
+ });
308
+ }
309
+ }
310
+ }
311
+
312
+ if (obj === "gate" && prop === "policy") {
313
+ const policyName = stringArg(call.arguments[0]);
314
+ if (policyName) {
315
+ scope.gates[policyName] = { kind: "policy", roles: [policyName] };
316
+ const bindingName = enclosingConstName(call, program);
317
+ if (bindingName) {
318
+ scope.gateIds.set(bindingName, policyName);
319
+ scope.bindings.set(bindingName, {
320
+ kind: "unknown",
321
+ ref: policyName,
322
+ });
323
+ }
324
+ }
325
+ }
326
+
327
+ if (obj === "gate" && prop === "rate") {
328
+ const opts = objectArg(call.arguments[0]);
329
+ const strategy = (stringProp(opts, "strategy") ??
330
+ "sliding-window-counter") as RateStrategy;
331
+ const max = numberProp(opts, "max");
332
+ const per = stringProp(opts, "per");
333
+ const keyBy = stringProp(opts, "keyBy");
334
+ if (max !== undefined && per) {
335
+ const expr = `rate:${strategy}:${max}/${per}`;
336
+ const bindingName = enclosingConstName(call, program);
337
+ if (bindingName) {
338
+ scope.gates[bindingName] = {
339
+ kind: "rate",
340
+ strategy,
341
+ max,
342
+ per,
343
+ ...(keyBy ? { keyBy } : {}),
344
+ };
345
+ scope.gateIds.set(bindingName, expr);
346
+ } else {
347
+ scope.gates[expr] = {
348
+ kind: "rate",
349
+ strategy,
350
+ max,
351
+ per,
352
+ ...(keyBy ? { keyBy } : {}),
353
+ };
354
+ }
355
+ }
356
+ }
357
+
358
+ if (obj === "channel" && prop === "template") {
359
+ const templateName = stringArg(call.arguments[0]);
360
+ const opts = objectArg(call.arguments[1]);
361
+ if (templateName) {
362
+ const medium = stringProp(opts, "medium");
363
+ const locales = stringArrayProp(opts, "locales");
364
+ scope.channels[templateName] = {
365
+ ...(medium
366
+ ? { medium: medium as Channel["medium"] }
367
+ : { medium: "email" }),
368
+ ...(locales ? { locales } : {}),
369
+ };
370
+ const bindingName = enclosingConstName(call, program);
371
+ if (bindingName) {
372
+ scope.bindings.set(bindingName, {
373
+ kind: "template",
374
+ ref: templateName,
375
+ });
376
+ }
377
+ }
378
+ }
379
+
380
+ if (obj === "ai" && prop === "model") {
381
+ const modelName = stringArg(call.arguments[0]);
382
+ const opts = objectArg(call.arguments[1]);
383
+ if (modelName) {
384
+ const model: AiModel = {
385
+ ...(stringProp(opts, "provider")
386
+ ? { provider: stringProp(opts, "provider") }
387
+ : {}),
388
+ ...(stringProp(opts, "tier")
389
+ ? { tier: stringProp(opts, "tier") }
390
+ : {}),
391
+ };
392
+ scope.ai.models = scope.ai.models ?? {};
393
+ scope.ai.models[modelName] = model;
394
+ const bindingName = enclosingConstName(call, program);
395
+ if (bindingName) {
396
+ scope.bindings.set(bindingName, {
397
+ kind: "unknown",
398
+ ref: modelName,
399
+ });
400
+ }
401
+ }
402
+ }
403
+
404
+ if (obj === "ai" && prop === "agent") {
405
+ collectAgent(call, scope);
406
+ }
407
+
408
+ if (obj === "ai" && prop === "embed") {
409
+ const embedName = stringArg(call.arguments[0]);
410
+ if (embedName) {
411
+ const bindingName = enclosingConstName(call, program);
412
+ if (bindingName) {
413
+ scope.bindings.set(bindingName, {
414
+ kind: "embed",
415
+ ref: embedName,
416
+ facet: "index",
417
+ });
418
+ }
419
+ }
420
+ }
421
+
422
+ // model.prompt("ticket-triage", { version, evals, budget })
423
+ if (prop === "prompt") {
424
+ const promptName = stringArg(call.arguments[0]);
425
+ const opts = objectArg(call.arguments[1]);
426
+ if (promptName) {
427
+ const version = numberProp(opts, "version");
428
+ const evals = stringProp(opts, "evals");
429
+ const budgetObj = objectProp(opts, "budget");
430
+ const prompt: AiPrompt = {
431
+ ...(version !== undefined ? { version } : {}),
432
+ ...(evals ? { evals } : {}),
433
+ ...(budgetObj
434
+ ? {
435
+ budget: {
436
+ ...(numberProp(budgetObj, "maxCostPerCall") !== undefined
437
+ ? {
438
+ maxCostPerCall: numberProp(budgetObj, "maxCostPerCall"),
439
+ }
440
+ : {}),
441
+ },
442
+ }
443
+ : {}),
444
+ };
445
+ scope.ai.prompts = scope.ai.prompts ?? {};
446
+ scope.ai.prompts[promptName] = prompt;
447
+ const bindingName = enclosingConstName(call, program);
448
+ if (bindingName) {
449
+ scope.bindings.set(bindingName, {
450
+ kind: "prompt",
451
+ ref: promptName,
452
+ version,
453
+ });
454
+ }
455
+ }
456
+ }
457
+
458
+ if (obj === "vault" && (prop === "secret" || prop === "define")) {
459
+ const secretName = stringArg(call.arguments[0]);
460
+ const opts = objectArg(call.arguments[1]);
461
+ if (secretName) {
462
+ scope.vault[secretName] = {
463
+ ...(stringProp(opts, "description")
464
+ ? { description: stringProp(opts, "description") }
465
+ : {}),
466
+ ...(stringProp(opts, "rotate")
467
+ ? { rotate: stringProp(opts, "rotate") }
468
+ : {}),
469
+ };
470
+ const bindingName = enclosingConstName(call, program);
471
+ if (bindingName) {
472
+ scope.bindings.set(bindingName, {
473
+ kind: "secret",
474
+ ref: secretName,
475
+ });
476
+ }
477
+ }
478
+ }
479
+ }
480
+
481
+ // signal("name", { delivery, … })
482
+ if (identifierName(callee) === "signal") {
483
+ const signalName = stringArg(call.arguments[0]);
484
+ const opts = objectArg(call.arguments[1]);
485
+ if (signalName) {
486
+ const delivery = stringProp(opts, "delivery") as SignalDelivery | undefined;
487
+ if (delivery) {
488
+ const signal: Signal = {
489
+ delivery,
490
+ ...(numberProp(opts, "retries") !== undefined
491
+ ? { retries: numberProp(opts, "retries") }
492
+ : {}),
493
+ ...(boolProp(opts, "deadLetter") !== undefined
494
+ ? { deadLetter: boolProp(opts, "deadLetter") }
495
+ : {}),
496
+ };
497
+ scope.signals[signalName] = signal;
498
+ const bindingName = enclosingConstName(call, program);
499
+ if (bindingName) {
500
+ scope.bindings.set(bindingName, {
501
+ kind: "signal",
502
+ ref: signalName,
503
+ });
504
+ }
505
+ }
506
+ }
507
+ }
508
+
509
+ // clock.every("10m") / clock("expire-holds", { every, overridable })
510
+ if (identifierName(callee) === "clock") {
511
+ const name = stringArg(call.arguments[0]);
512
+ const opts = objectArg(call.arguments[1]);
513
+ if (name && opts) {
514
+ scope.clocks[name] = {
515
+ ...(stringProp(opts, "every")
516
+ ? { every: stringProp(opts, "every") }
517
+ : {}),
518
+ ...(stringProp(opts, "cron") ? { cron: stringProp(opts, "cron") } : {}),
519
+ ...(boolProp(opts, "overridable") !== undefined
520
+ ? { overridable: boolProp(opts, "overridable") }
521
+ : {}),
522
+ };
523
+ }
524
+ }
525
+
526
+ // journey("book-a-flight", { slo, composes, path })
527
+ if (identifierName(callee) === "journey") {
528
+ const name = stringArg(call.arguments[0]);
529
+ const opts = objectArg(call.arguments[1]);
530
+ if (name && opts) {
531
+ const sloObj = objectProp(opts, "slo");
532
+ const pathArr = arrayProp(opts, "path");
533
+ const flows = pathArr
534
+ ?.map((el) => {
535
+ const id = identifierName(el);
536
+ if (!id) return stringArg(el);
537
+ return scope.flowExports.get(id) ?? scope.bindings.get(id)?.ref ?? id;
538
+ })
539
+ .filter((x): x is string => typeof x === "string");
540
+ const journey: Journey = {
541
+ ...(sloObj
542
+ ? {
543
+ slo: {
544
+ ...(stringProp(sloObj, "availability")
545
+ ? { availability: stringProp(sloObj, "availability") }
546
+ : {}),
547
+ },
548
+ }
549
+ : {}),
550
+ ...(stringProp(opts, "composes")
551
+ ? { composes: stringProp(opts, "composes") }
552
+ : {}),
553
+ ...(flows && flows.length > 0 ? { flows } : {}),
554
+ };
555
+ scope.journeys[name] = journey;
556
+ }
557
+ }
558
+
559
+ // defineConfig({ drivers, tenancy, i18n, topology, images })
560
+ if (identifierName(callee) === "defineConfig") {
561
+ collectConfig(objectArg(call.arguments[0]), scope);
562
+ }
563
+
564
+ // vault("STRIPE_KEY", { … }) — bare call form
565
+ if (identifierName(callee) === "vault") {
566
+ const secretName = stringArg(call.arguments[0]);
567
+ const opts = objectArg(call.arguments[1]);
568
+ if (secretName) {
569
+ scope.vault[secretName] = {
570
+ ...(stringProp(opts, "description")
571
+ ? { description: stringProp(opts, "description") }
572
+ : {}),
573
+ ...(stringProp(opts, "rotate")
574
+ ? { rotate: stringProp(opts, "rotate") }
575
+ : {}),
576
+ };
577
+ const bindingName = enclosingConstName(call, program);
578
+ if (bindingName) {
579
+ scope.bindings.set(bindingName, { kind: "secret", ref: secretName });
580
+ }
581
+ }
582
+ }
583
+ }
584
+
585
+ function collectAgent(call: CallExpression, scope: ProjectScope): void {
586
+ const agentName = stringArg(call.arguments[0]);
587
+ const opts = objectArg(call.arguments[1]);
588
+ if (!agentName || !opts) return;
589
+ const toolsArr = arrayProp(opts, "tools");
590
+ const tools = toolsArr
591
+ ?.map((el) => identifierName(el) ?? stringArg(el))
592
+ .filter((x): x is string => typeof x === "string")
593
+ .map((id) => scope.flowExports.get(id) ?? scope.bindings.get(id)?.ref ?? id);
594
+ const agent: AiAgent = {
595
+ ...(tools && tools.length > 0 ? { tools } : {}),
596
+ ...(numberProp(opts, "maxSteps") !== undefined
597
+ ? { maxSteps: numberProp(opts, "maxSteps") }
598
+ : {}),
599
+ };
600
+ scope.ai.agents = scope.ai.agents ?? {};
601
+ scope.ai.agents[agentName] = agent;
602
+ }
603
+
604
+ function collectConfig(
605
+ opts: AstNode | undefined,
606
+ scope: ProjectScope,
607
+ ): void {
608
+ if (!opts) return;
609
+
610
+ const tenancy = objectProp(opts, "tenancy");
611
+ if (tenancy) {
612
+ const isolation = stringProp(tenancy, "isolation");
613
+ if (
614
+ isolation === "row" ||
615
+ isolation === "schema" ||
616
+ isolation === "database"
617
+ ) {
618
+ scope.tenancy = { isolation };
619
+ }
620
+ }
621
+
622
+ const i18n = objectProp(opts, "i18n");
623
+ if (i18n) {
624
+ scope.i18n = {
625
+ ...(stringArrayProp(i18n, "locales")
626
+ ? { locales: stringArrayProp(i18n, "locales") }
627
+ : {}),
628
+ ...(stringProp(i18n, "default")
629
+ ? { default: stringProp(i18n, "default") }
630
+ : {}),
631
+ };
632
+ const dir = objectProp(i18n, "dir");
633
+ if (dir && dir.type === "ObjectExpression") {
634
+ const dirMap: Record<string, "ltr" | "rtl"> = {};
635
+ for (const prop of objectProperties(dir)) {
636
+ const key = propKey(prop);
637
+ const val = stringArg(
638
+ (prop as AstNode & { value?: AstNode }).value,
639
+ );
640
+ if (key && (val === "ltr" || val === "rtl")) dirMap[key] = val;
641
+ }
642
+ if (Object.keys(dirMap).length > 0) {
643
+ scope.i18n = { ...scope.i18n, dir: dirMap };
644
+ }
645
+ }
646
+ }
647
+
648
+ const topology = stringProp(opts, "topology");
649
+ if (topology === "monolith" || topology === "services") {
650
+ scope.topology = topology;
651
+ }
652
+
653
+ const images = objectProp(opts, "images");
654
+ if (images) {
655
+ const map: Record<string, string> = {};
656
+ for (const prop of objectProperties(images)) {
657
+ const key = propKey(prop);
658
+ const val = stringArg((prop as AstNode & { value?: AstNode }).value);
659
+ if (key && val) map[key] = val;
660
+ }
661
+ if (Object.keys(map).length > 0) scope.images = map;
662
+ }
663
+
664
+ // drivers: prefer an explicit prod string[], else flatten nested role maps.
665
+ const drivers = objectProp(opts, "drivers");
666
+ if (drivers) {
667
+ const prodArr = arrayProp(drivers, "prod");
668
+ if (prodArr && prodArr.length > 0 && prodArr.every((el) => stringArg(el))) {
669
+ scope.drivers.prod = prodArr
670
+ .map((el) => stringArg(el)!)
671
+ .filter((x) => x.length > 0);
672
+ } else {
673
+ const prod = new Set<string>();
674
+ collectDriverProtocols(drivers, "prod", prod);
675
+ if (prod.size > 0) scope.drivers.prod = [...prod];
676
+ }
677
+ }
678
+ }
679
+
680
+ function collectDriverProtocols(
681
+ node: AstNode,
682
+ env: string,
683
+ into: Set<string>,
684
+ ): void {
685
+ if (node.type !== "ObjectExpression") return;
686
+ for (const prop of objectProperties(node)) {
687
+ const key = propKey(prop);
688
+ const value = (prop as AstNode & { value?: AstNode }).value;
689
+ if (!value) continue;
690
+
691
+ if (key === env) {
692
+ if (value.type === "Literal" && typeof (value as Literal).value === "string") {
693
+ into.add((value as Literal).value as string);
694
+ } else if (value.type === "ObjectExpression") {
695
+ const driver = stringProp(value, "driver");
696
+ if (driver) into.add(driver);
697
+ // Nested role objects may also use env keys.
698
+ collectDriverProtocols(value, env, into);
699
+ }
700
+ continue;
701
+ }
702
+
703
+ if (value.type === "ObjectExpression") {
704
+ // role: { dev, test, prod } or nested store: { sql: { prod } }
705
+ const envVal = objectProp(value, env);
706
+ if (envVal) {
707
+ if (
708
+ envVal.type === "Literal" &&
709
+ typeof (envVal as Literal).value === "string"
710
+ ) {
711
+ into.add((envVal as Literal).value as string);
712
+ } else if (envVal.type === "ObjectExpression") {
713
+ const driver = stringProp(envVal, "driver");
714
+ if (driver) into.add(driver);
715
+ }
716
+ } else {
717
+ collectDriverProtocols(value, env, into);
718
+ }
719
+ }
720
+ }
721
+ }
722
+
723
+ function collectFlows(
724
+ file: SourceFile,
725
+ program: AstNode,
726
+ scope: ProjectScope,
727
+ ): void {
728
+ walk(program, (node) => {
729
+ if (node.type !== "CallExpression") return;
730
+ const call = node as CallExpression;
731
+ const callee = identifierName(call.callee);
732
+
733
+ if (callee === "on") {
734
+ const triggerNode = call.arguments[0];
735
+ const flowNode = call.arguments[1];
736
+ if (!triggerNode || !flowNode) return;
737
+ const flowCall = unwrapFlowCall(flowNode);
738
+ if (!flowCall) return;
739
+ const exportName = enclosingConstName(call, program);
740
+ registerFlow({
741
+ flowCall,
742
+ triggerNode,
743
+ file,
744
+ scope,
745
+ exportName,
746
+ });
747
+ return;
748
+ }
749
+
750
+ if (callee === "flow") {
751
+ // Bare flow — skip when this call is the second arg of an on().
752
+ if (isOnFlowArgument(call, program)) return;
753
+ const exportName = enclosingConstName(call, program);
754
+ registerFlow({
755
+ flowCall: call,
756
+ triggerNode: undefined,
757
+ file,
758
+ scope,
759
+ exportName,
760
+ });
761
+ }
762
+ });
763
+ }
764
+
765
+ function isOnFlowArgument(flowCall: CallExpression, program: AstNode): boolean {
766
+ const targetStart = flowCall.start;
767
+ if (targetStart === undefined) return false;
768
+ let found = false;
769
+ walk(program, (node) => {
770
+ if (found) return;
771
+ if (node.type !== "CallExpression") return;
772
+ const call = node as CallExpression;
773
+ if (identifierName(call.callee) !== "on") return;
774
+ const arg = call.arguments[1];
775
+ if (arg && arg.type === "CallExpression" && arg.start === targetStart) {
776
+ found = true;
777
+ }
778
+ });
779
+ return found;
780
+ }
781
+
782
+ function unwrapFlowCall(node: AstNode): CallExpression | undefined {
783
+ if (node.type === "CallExpression" && identifierName((node as CallExpression).callee) === "flow") {
784
+ return node as CallExpression;
785
+ }
786
+ return undefined;
787
+ }
788
+
789
+ function registerFlow(args: {
790
+ flowCall: CallExpression;
791
+ triggerNode: AstNode | undefined;
792
+ file: SourceFile;
793
+ scope: ProjectScope;
794
+ exportName: string | undefined;
795
+ }): void {
796
+ const opts = objectArg(args.flowCall.arguments[0]);
797
+ if (!opts) return;
798
+
799
+ const name =
800
+ stringProp(opts, "name") ??
801
+ args.exportName ??
802
+ `flow_${Object.keys(args.scope.flows).length + 1}`;
803
+
804
+ if (args.exportName) {
805
+ args.scope.flowExports.set(args.exportName, name);
806
+ args.scope.bindings.set(args.exportName, { kind: "flow", ref: name });
807
+ }
808
+ args.scope.bindings.set(name, { kind: "flow", ref: name });
809
+
810
+ const hasExplicitEffects = objectProp(opts, "effects") !== undefined;
811
+ const doNode = objectProp(opts, "do");
812
+ const inferred = doNode
813
+ ? inferEffects({
814
+ doNode,
815
+ bindings: args.scope.bindings,
816
+ hasExplicitEffects,
817
+ })
818
+ : {
819
+ effects: {} as Effects,
820
+ steps: [] as string[],
821
+ usesRaw: false,
822
+ cacheIneligible: false,
823
+ nondeterministic: false,
824
+ readsUserId: false,
825
+ };
826
+
827
+ let effects: Effects | undefined;
828
+ if (hasExplicitEffects) {
829
+ effects = parseEffectsObject(objectProp(opts, "effects"));
830
+ } else {
831
+ effects = inferred.effects;
832
+ }
833
+
834
+ // Spec excerpt includes empty secrets[] when other keys present for create.
835
+ if (
836
+ effects &&
837
+ (effects.reads || effects.writes || effects.emits) &&
838
+ effects.secrets === undefined &&
839
+ !inferred.usesRaw
840
+ ) {
841
+ // Keep secrets omitted unless the excerpt-style empty list is needed —
842
+ // only add empty secrets when emits are present (bookings.create shape).
843
+ if (effects.emits && effects.emits.length > 0) {
844
+ effects = { ...effects, secrets: [] };
845
+ }
846
+ }
847
+
848
+ const trigger = args.triggerNode
849
+ ? parseTrigger(args.triggerNode, args.scope)
850
+ : undefined;
851
+
852
+ const gates = trigger?.gates;
853
+ const liveFromTrigger = trigger?.live;
854
+ const manifestTrigger = trigger
855
+ ? stripTriggerExtras(trigger.trigger)
856
+ : undefined;
857
+
858
+ const flow: Flow = {};
859
+
860
+ if (manifestTrigger && Object.keys(manifestTrigger).length > 0) {
861
+ flow.trigger = manifestTrigger;
862
+ }
863
+ if (gates && gates.length > 0) flow.gates = gates;
864
+
865
+ const inSchema = schemaProp(opts, "in");
866
+ if (inSchema !== undefined) flow.in = inSchema;
867
+ const outSchema = schemaProp(opts, "out");
868
+ if (outSchema !== undefined) flow.out = outSchema;
869
+
870
+ const errors = parseErrors(objectProp(opts, "errors"));
871
+ if (errors) flow.errors = errors;
872
+
873
+ if (effects && Object.keys(effects).length > 0) flow.effects = effects;
874
+
875
+ const line = lineAt(args.file.source, args.flowCall.start ?? 0);
876
+ flow.source = `${args.file.path}:${line}`;
877
+
878
+ if (boolProp(opts, "durable")) flow.durable = true;
879
+ if (boolProp(opts, "live") || liveFromTrigger) flow.live = true;
880
+
881
+ const slo = parseSlo(objectProp(opts, "slo"));
882
+ if (slo) flow.slo = slo;
883
+
884
+ const stepsFromOpts = stringArrayProp(opts, "steps");
885
+ const steps =
886
+ stepsFromOpts && stepsFromOpts.length > 0
887
+ ? stepsFromOpts
888
+ : inferred.steps;
889
+ if (steps.length > 0) flow.steps = steps;
890
+
891
+ if (boolProp(opts, "nondeterministic") || inferred.nondeterministic) {
892
+ flow.nondeterministic = true;
893
+ }
894
+
895
+ const cost = objectProp(opts, "cost");
896
+ if (cost) {
897
+ flow.cost = {
898
+ ...(numberProp(cost, "estimatePerCall") !== undefined
899
+ ? { estimatePerCall: numberProp(cost, "estimatePerCall") }
900
+ : {}),
901
+ ...(numberProp(cost, "budget") !== undefined
902
+ ? { budget: numberProp(cost, "budget") }
903
+ : {}),
904
+ };
905
+ } else if (inferred.nondeterministic) {
906
+ // Derive budget from the first ask prompt when present.
907
+ const ask = effects?.asks?.[0];
908
+ if (ask) {
909
+ const promptName = ask.split("@")[0]!;
910
+ const promptBudget = args.scope.ai.prompts?.[promptName]?.budget
911
+ ?.maxCostPerCall;
912
+ if (promptBudget !== undefined) {
913
+ flow.cost = {
914
+ estimatePerCall: Number((promptBudget * 0.55).toFixed(3)),
915
+ budget: promptBudget,
916
+ };
917
+ }
918
+ }
919
+ }
920
+
921
+ const pii = stringProp(opts, "pii");
922
+ if (pii === "masked" || pii === "allow" || pii === "denied") {
923
+ flow.pii = pii;
924
+ } else if (inferred.nondeterministic) {
925
+ flow.pii = "masked";
926
+ }
927
+ if (boolProp(opts, "allowPii") === true || pii === "allow") {
928
+ flow.allowPii = true;
929
+ flow.pii = "allow";
930
+ }
931
+
932
+ const plane = stringProp(opts, "plane");
933
+ if (plane === "user" || plane === "operator") flow.plane = plane;
934
+
935
+ if (inferred.cacheIneligible) {
936
+ flow.cache = false;
937
+ } else {
938
+ const cache = (opts &&
939
+ objectProperties(opts).find((p) => propKey(p) === "cache")) as
940
+ | AstNode
941
+ | undefined;
942
+ if (cache) {
943
+ const val = (cache as AstNode & { value?: AstNode }).value;
944
+ if (val?.type === "Literal") {
945
+ const v = (val as Literal).value;
946
+ if (typeof v === "boolean" || typeof v === "string") flow.cache = v;
947
+ }
948
+ }
949
+ }
950
+
951
+ const cacheKeys = stringProp(opts, "cacheKeys");
952
+ if (cacheKeys) {
953
+ flow.cacheKeys = cacheKeys;
954
+ } else if (flow.live && inferred.readsUserId && effects?.reads?.[0]) {
955
+ flow.cacheKeys = `computed:${effects.reads[0]}/userId`;
956
+ }
957
+
958
+ args.scope.flows[name] = flow;
959
+ }
960
+
961
+ interface ParsedTrigger {
962
+ trigger: Trigger;
963
+ gates?: string[];
964
+ live?: boolean;
965
+ }
966
+
967
+ function parseTrigger(node: AstNode, scope: ProjectScope): ParsedTrigger | undefined {
968
+ // every("10m")
969
+ if (node.type === "CallExpression") {
970
+ const call = node as CallExpression;
971
+ const name = identifierName(call.callee);
972
+ if (name === "every") {
973
+ const interval = stringArg(call.arguments[0]);
974
+ if (interval) return { trigger: { every: interval } };
975
+ }
976
+ if (name === "internal") {
977
+ return { trigger: {} };
978
+ }
979
+
980
+ // http.post("/x").gate(...).live()
981
+ const http = parseHttpTrigger(call, scope);
982
+ if (http) return http;
983
+
984
+ // table("orders").changed("status") / db.table(orders).changed(...)
985
+ const cdc = parseCdcTrigger(call);
986
+ if (cdc) return { trigger: { cdc } };
987
+
988
+ // on(signalHandle, …) — Identifier referring to a signal binding
989
+ }
990
+
991
+ if (node.type === "Identifier") {
992
+ const id = (node as Identifier).name;
993
+ if (id === "internal") return { trigger: {} };
994
+ const binding = scope.bindings.get(id);
995
+ if (binding?.kind === "signal") {
996
+ return { trigger: { signal: binding.ref } };
997
+ }
998
+ if (scope.signals[id]) return { trigger: { signal: id } };
999
+ return { trigger: { signal: id } };
1000
+ }
1001
+
1002
+ return undefined;
1003
+ }
1004
+
1005
+ function parseHttpTrigger(
1006
+ call: CallExpression,
1007
+ scope: ProjectScope,
1008
+ ): ParsedTrigger | undefined {
1009
+ // Walk the chain: http.METHOD(path).gate(...).live()
1010
+ let current: AstNode = call;
1011
+ let method: string | undefined;
1012
+ let path: string | undefined;
1013
+ let live = false;
1014
+ const gateNames: string[] = [];
1015
+
1016
+ // Flatten chain from leaf to root
1017
+ const chain: CallExpression[] = [];
1018
+ while (current.type === "CallExpression") {
1019
+ chain.unshift(current as CallExpression);
1020
+ const callee = (current as CallExpression).callee;
1021
+ if (callee.type === "MemberExpression") {
1022
+ const obj = (callee as AstNode & { object: AstNode }).object;
1023
+ current = obj;
1024
+ continue;
1025
+ }
1026
+ break;
1027
+ }
1028
+
1029
+ for (const c of chain) {
1030
+ const callee = c.callee;
1031
+ if (callee.type !== "MemberExpression") continue;
1032
+ const member = callee as AstNode & {
1033
+ object: AstNode;
1034
+ property: AstNode;
1035
+ };
1036
+ const prop = identifierName(member.property);
1037
+ const obj = member.object;
1038
+
1039
+ if (obj.type === "Identifier" && (obj as Identifier).name === "http" && prop) {
1040
+ method = prop.toUpperCase();
1041
+ path = stringArg(c.arguments[0]);
1042
+ continue;
1043
+ }
1044
+
1045
+ if (prop === "gate") {
1046
+ for (const arg of c.arguments) {
1047
+ const id = identifierName(arg);
1048
+ if (!id) continue;
1049
+ const resolved = scope.gateIds.get(id) ?? id;
1050
+ gateNames.push(resolved);
1051
+ }
1052
+ continue;
1053
+ }
1054
+
1055
+ if (prop === "live") {
1056
+ live = true;
1057
+ }
1058
+ }
1059
+
1060
+ if (!method || !path) return undefined;
1061
+
1062
+ const httpMethods = [
1063
+ "GET",
1064
+ "POST",
1065
+ "PUT",
1066
+ "PATCH",
1067
+ "DELETE",
1068
+ "HEAD",
1069
+ "OPTIONS",
1070
+ ] as const;
1071
+ if (!(httpMethods as readonly string[]).includes(method)) return undefined;
1072
+
1073
+ return {
1074
+ trigger: {
1075
+ http: {
1076
+ method: method as (typeof httpMethods)[number],
1077
+ path,
1078
+ },
1079
+ },
1080
+ gates: gateNames.length > 0 ? gateNames : undefined,
1081
+ live: live || undefined,
1082
+ };
1083
+ }
1084
+
1085
+ function parseCdcTrigger(
1086
+ call: CallExpression,
1087
+ ): Trigger["cdc"] | undefined {
1088
+ // *.changed("col") or *.changed()
1089
+ const callee = call.callee;
1090
+ if (callee.type !== "MemberExpression") return undefined;
1091
+ const member = callee as AstNode & { object: AstNode; property: AstNode };
1092
+ if (identifierName(member.property) !== "changed") return undefined;
1093
+
1094
+ const column = stringArg(call.arguments[0]);
1095
+ // object is table("orders") or db.table(orders) or table(orders)
1096
+ const obj = member.object;
1097
+ if (obj.type === "CallExpression") {
1098
+ const inner = obj as CallExpression;
1099
+ const innerCallee = inner.callee;
1100
+ if (identifierName(innerCallee) === "table") {
1101
+ const tableName =
1102
+ stringArg(inner.arguments[0]) ?? identifierName(inner.arguments[0]);
1103
+ if (!tableName) return undefined;
1104
+ return column
1105
+ ? { table: tableName, column }
1106
+ : { table: tableName };
1107
+ }
1108
+ if (
1109
+ innerCallee.type === "MemberExpression" &&
1110
+ identifierName(
1111
+ (innerCallee as AstNode & { property: AstNode }).property,
1112
+ ) === "table"
1113
+ ) {
1114
+ const tableName =
1115
+ stringArg(inner.arguments[0]) ?? identifierName(inner.arguments[0]);
1116
+ if (!tableName) return undefined;
1117
+ return column
1118
+ ? { table: tableName, column }
1119
+ : { table: tableName };
1120
+ }
1121
+ }
1122
+ return undefined;
1123
+ }
1124
+
1125
+ function stripTriggerExtras(trigger: Trigger): Trigger {
1126
+ return trigger;
1127
+ }
1128
+
1129
+ function parseEffectsObject(node: AstNode | undefined): Effects | undefined {
1130
+ if (!node) return undefined;
1131
+ const effects: Effects = {};
1132
+ const reads = stringArrayProp(node, "reads");
1133
+ const writes = stringArrayProp(node, "writes");
1134
+ const emits = stringArrayProp(node, "emits");
1135
+ const sends = stringArrayProp(node, "sends");
1136
+ const asks = stringArrayProp(node, "asks");
1137
+ const secrets = stringArrayProp(node, "secrets");
1138
+ const calls = stringArrayProp(node, "calls");
1139
+ if (reads) effects.reads = reads as Effects["reads"];
1140
+ if (writes) effects.writes = writes as Effects["writes"];
1141
+ if (emits) effects.emits = emits;
1142
+ if (sends) effects.sends = sends;
1143
+ if (asks) effects.asks = asks;
1144
+ if (secrets) effects.secrets = secrets;
1145
+ if (calls) effects.calls = calls;
1146
+ return effects;
1147
+ }
1148
+
1149
+ function parseErrors(
1150
+ node: AstNode | undefined,
1151
+ ): Flow["errors"] | undefined {
1152
+ if (!node) return undefined;
1153
+ if (node.type === "ObjectExpression") {
1154
+ const names: string[] = [];
1155
+ for (const prop of objectProperties(node)) {
1156
+ const key = propKey(prop);
1157
+ if (key) names.push(key);
1158
+ }
1159
+ return names.length > 0 ? names : undefined;
1160
+ }
1161
+ if (node.type === "ArrayExpression") {
1162
+ const els = (node as AstNode & { elements?: AstNode[] }).elements ?? [];
1163
+ const names = els
1164
+ .map((el) => stringArg(el) ?? identifierName(el))
1165
+ .filter((x): x is string => typeof x === "string");
1166
+ return names.length > 0 ? names : undefined;
1167
+ }
1168
+ return undefined;
1169
+ }
1170
+
1171
+ function parseSlo(node: AstNode | undefined): Slo | undefined {
1172
+ if (!node) return undefined;
1173
+ const availability = stringProp(node, "availability");
1174
+ const latencyObj = objectProp(node, "latency");
1175
+ const latency: Record<string, string> = {};
1176
+ if (latencyObj) {
1177
+ for (const prop of objectProperties(latencyObj)) {
1178
+ const key = propKey(prop);
1179
+ const val = stringArg((prop as AstNode & { value?: AstNode }).value);
1180
+ if (key && val) latency[key] = val;
1181
+ }
1182
+ }
1183
+ if (!availability && Object.keys(latency).length === 0) return undefined;
1184
+ return {
1185
+ ...(availability ? { availability } : {}),
1186
+ ...(Object.keys(latency).length > 0 ? { latency } : {}),
1187
+ };
1188
+ }
1189
+
1190
+ function schemaProp(
1191
+ obj: AstNode | undefined,
1192
+ key: string,
1193
+ ): string | undefined {
1194
+ const node = objectProp(obj, key);
1195
+ if (!node) return undefined;
1196
+ // Identifier / member schemas → opaque placeholder matching the spec excerpt.
1197
+ if (node.type === "Identifier" || node.type === "MemberExpression") {
1198
+ return "…";
1199
+ }
1200
+ if (node.type === "CallExpression") return "…";
1201
+ const lit = stringArg(node);
1202
+ if (lit) return lit;
1203
+ return "…";
1204
+ }
1205
+
1206
+ // ── AST helpers ────────────────────────────────────────────────────────────
1207
+
1208
+ function enclosingConstName(
1209
+ call: CallExpression,
1210
+ program: AstNode,
1211
+ ): string | undefined {
1212
+ const targetStart = call.start;
1213
+ if (targetStart === undefined) return undefined;
1214
+ let found: string | undefined;
1215
+ walk(program, (node) => {
1216
+ if (found) return;
1217
+ if (node.type !== "VariableDeclarator") return;
1218
+ const id = (node as AstNode & { id?: AstNode }).id;
1219
+ const init = (node as AstNode & { init?: AstNode }).init;
1220
+ if (!id || !init) return;
1221
+ const name = identifierName(id);
1222
+ if (!name) return;
1223
+ if (containsOffset(init, targetStart)) found = name;
1224
+ });
1225
+ return found;
1226
+ }
1227
+
1228
+ function containsOffset(root: AstNode, start: number): boolean {
1229
+ let found = false;
1230
+ walk(root, (node) => {
1231
+ if (node.start === start) found = true;
1232
+ });
1233
+ return found;
1234
+ }
1235
+
1236
+ function objectArg(node: AstNode | undefined): AstNode | undefined {
1237
+ if (!node) return undefined;
1238
+ if (node.type === "ObjectExpression") return node;
1239
+ return undefined;
1240
+ }
1241
+
1242
+ function objectProp(
1243
+ obj: AstNode | undefined,
1244
+ key: string,
1245
+ ): AstNode | undefined {
1246
+ if (!obj || obj.type !== "ObjectExpression") return undefined;
1247
+ for (const prop of objectProperties(obj)) {
1248
+ if (propKey(prop) === key) {
1249
+ return (prop as AstNode & { value?: AstNode }).value;
1250
+ }
1251
+ }
1252
+ return undefined;
1253
+ }
1254
+
1255
+ function objectProperties(obj: AstNode): AstNode[] {
1256
+ return (
1257
+ (obj as AstNode & { properties?: AstNode[] }).properties ?? []
1258
+ ).filter((p) => p.type === "Property" || p.type === "ObjectProperty");
1259
+ }
1260
+
1261
+ function propKey(prop: AstNode): string | undefined {
1262
+ const key = (prop as AstNode & { key?: AstNode }).key;
1263
+ if (!key) return undefined;
1264
+ if (key.type === "Identifier") return (key as Identifier).name;
1265
+ if (key.type === "Literal" && typeof (key as Literal).value === "string") {
1266
+ return (key as Literal).value as string;
1267
+ }
1268
+ return undefined;
1269
+ }
1270
+
1271
+ function stringProp(
1272
+ obj: AstNode | undefined,
1273
+ key: string,
1274
+ ): string | undefined {
1275
+ return stringArg(objectProp(obj, key));
1276
+ }
1277
+
1278
+ function numberProp(
1279
+ obj: AstNode | undefined,
1280
+ key: string,
1281
+ ): number | undefined {
1282
+ const node = objectProp(obj, key);
1283
+ if (!node || node.type !== "Literal") return undefined;
1284
+ const v = (node as Literal).value;
1285
+ return typeof v === "number" ? v : undefined;
1286
+ }
1287
+
1288
+ function boolProp(
1289
+ obj: AstNode | undefined,
1290
+ key: string,
1291
+ ): boolean | undefined {
1292
+ const node = objectProp(obj, key);
1293
+ if (!node || node.type !== "Literal") return undefined;
1294
+ const v = (node as Literal).value;
1295
+ return typeof v === "boolean" ? v : undefined;
1296
+ }
1297
+
1298
+ function stringArrayProp(
1299
+ obj: AstNode | undefined,
1300
+ key: string,
1301
+ ): string[] | undefined {
1302
+ const node = objectProp(obj, key);
1303
+ if (!node || node.type !== "ArrayExpression") return undefined;
1304
+ const els = (node as AstNode & { elements?: AstNode[] }).elements ?? [];
1305
+ const out = els
1306
+ .map((el) => stringArg(el))
1307
+ .filter((x): x is string => typeof x === "string");
1308
+ return out;
1309
+ }
1310
+
1311
+ function arrayProp(
1312
+ obj: AstNode | undefined,
1313
+ key: string,
1314
+ ): AstNode[] | undefined {
1315
+ const node = objectProp(obj, key);
1316
+ if (!node || node.type !== "ArrayExpression") return undefined;
1317
+ return ((node as AstNode & { elements?: AstNode[] }).elements ?? []).filter(
1318
+ (el): el is AstNode => el !== null && el !== undefined,
1319
+ );
1320
+ }
1321
+
1322
+ /**
1323
+ * 1-based line number for a byte offset.
1324
+ *
1325
+ * @param source - Source text
1326
+ * @param offset - Byte offset
1327
+ */
1328
+ export function lineAt(source: string, offset: number): number {
1329
+ let line = 1;
1330
+ const end = Math.min(offset, source.length);
1331
+ for (let i = 0; i < end; i++) {
1332
+ if (source.charCodeAt(i) === 10) line++;
1333
+ }
1334
+ return line;
1335
+ }
1336
+
1337
+ function sortRecord<T>(record: Record<string, T>): Record<string, T> {
1338
+ const out: Record<string, T> = {};
1339
+ for (const key of Object.keys(record).sort()) {
1340
+ out[key] = record[key]!;
1341
+ }
1342
+ return out;
1343
+ }
1344
+
1345
+ /**
1346
+ * Deep subset match — every key/value in `expected` appears in `actual`.
1347
+ *
1348
+ * @param actual - Extracted value
1349
+ * @param expected - Spec excerpt (or golden subset)
1350
+ */
1351
+ export function deepMatch(actual: unknown, expected: unknown): boolean {
1352
+ if (expected === null || typeof expected !== "object") {
1353
+ return Object.is(actual, expected);
1354
+ }
1355
+ if (actual === null || typeof actual !== "object") return false;
1356
+ if (Array.isArray(expected)) {
1357
+ if (!Array.isArray(actual)) return false;
1358
+ if (expected.length !== actual.length) return false;
1359
+ return expected.every((item, i) => deepMatch(actual[i], item));
1360
+ }
1361
+ const expObj = expected as Record<string, unknown>;
1362
+ const actObj = actual as Record<string, unknown>;
1363
+ for (const key of Object.keys(expObj)) {
1364
+ if (!(key in actObj)) return false;
1365
+ if (!deepMatch(actObj[key], expObj[key])) return false;
1366
+ }
1367
+ return true;
1368
+ }