okengine 0.13.0 → 0.15.2

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 (277) hide show
  1. package/manifest.v1.schema.json +7 -1
  2. package/package.json +4 -5
  3. package/site/content/docs/ai/mcp.mdx +27 -2
  4. package/site/content/docs/elements/ai.mdx +59 -11
  5. package/site/content/docs/elements/channel.mdx +1 -0
  6. package/site/content/docs/elements/clock.mdx +30 -12
  7. package/site/content/docs/elements/flow.mdx +9 -3
  8. package/site/content/docs/elements/gate.mdx +25 -22
  9. package/site/content/docs/elements/signal.mdx +71 -25
  10. package/site/content/docs/elements/store.mdx +87 -28
  11. package/site/content/docs/elements/vault.mdx +1 -0
  12. package/site/content/docs/get-started/basic-usage.mdx +22 -13
  13. package/site/content/docs/get-started/index.mdx +5 -0
  14. package/site/content/docs/get-started/installation.mdx +2 -2
  15. package/site/content/docs/get-started/introduction.mdx +11 -3
  16. package/site/content/docs/get-started/meta.json +1 -1
  17. package/site/content/docs/get-started/project-structure.mdx +932 -0
  18. package/site/content/docs/reference/cli.md +13 -10
  19. package/site/content/docs/reference/client.mdx +25 -21
  20. package/site/content/docs/reference/fx.mdx +27 -22
  21. package/site/content/docs/reference/security.md +1 -1
  22. package/src/auth/bindings.ts +14 -5
  23. package/src/auth/cross-plane.ts +10 -3
  24. package/src/cli/ai-setup/ai-setup.test.ts +62 -0
  25. package/src/cli/ai-setup/apply.ts +32 -1
  26. package/src/cli/ai-setup/recommend.test.ts +25 -0
  27. package/src/cli/ai-setup/recommend.ts +8 -3
  28. package/src/cli/attach-host-console.test.ts +3 -2
  29. package/src/cli/attach-host-console.ts +3 -0
  30. package/src/cli/build.test.ts +4 -2
  31. package/src/cli/db-seed.ts +48 -15
  32. package/src/cli/db.test.ts +104 -0
  33. package/src/cli/db.ts +122 -10
  34. package/src/cli/dev-controls.test.ts +20 -0
  35. package/src/cli/dev-controls.ts +13 -0
  36. package/src/cli/dev.test.ts +15 -4
  37. package/src/cli/dev.ts +102 -29
  38. package/src/cli/ensure-drizzle-config.ts +2 -1
  39. package/src/cli/tui/DevLive.tsx +12 -2
  40. package/src/cli/tui/keys.test.ts +2 -1
  41. package/src/cli/tui/keys.ts +3 -2
  42. package/src/compiler/aot.test.ts +1 -1
  43. package/src/compiler/effects-infer.ts +58 -3
  44. package/src/compiler/extract.test.ts +458 -29
  45. package/src/compiler/extract.ts +387 -69
  46. package/src/compiler/flow-path.test.ts +96 -0
  47. package/src/compiler/flow-path.ts +173 -0
  48. package/src/compiler/generate-adopt.test.ts +112 -3
  49. package/src/compiler/generate-adopt.ts +280 -26
  50. package/src/compiler/response.ts +45 -1
  51. package/src/compiler/schema-from-ast.ts +609 -0
  52. package/src/console/server/ai.ts +5 -2
  53. package/src/console/server/bind.ts +2 -2
  54. package/src/console/server/console.test.ts +3 -0
  55. package/src/console/server/flows.ts +121 -40
  56. package/src/console/server/invoke-user-flow.test.ts +3 -1
  57. package/src/console/server/invoke-user-flow.ts +17 -2
  58. package/src/console/server/iso-at.test.ts +9 -0
  59. package/src/console/server/iso-at.ts +11 -0
  60. package/src/console/server/security-headers.ts +6 -3
  61. package/src/console/server/serve.ts +17 -2
  62. package/src/console/server/spa-proxy.test.ts +1 -0
  63. package/src/console/server/sql-catalog.ts +147 -7
  64. package/src/console/server/sql-rls.test.ts +16 -0
  65. package/src/console/server/state.ts +8 -0
  66. package/src/console/server/store.test.ts +7 -4
  67. package/src/console/server/store.ts +142 -58
  68. package/src/console/ui-next/dist/assets/agent-disclosure-C0X1fbWF.js +1 -0
  69. package/src/console/ui-next/dist/assets/cache-glyph-F1FI122b.js +1 -0
  70. package/src/console/ui-next/dist/assets/{call-pii-button-bqkxMrJH.js → call-pii-button-DEDCl_j3.js} +1 -1
  71. package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +1 -0
  72. package/src/console/ui-next/dist/assets/{confirm-sheet-DDCRmG62.js → confirm-sheet-3ptDzXbz.js} +1 -1
  73. package/src/console/ui-next/dist/assets/{duration-tone-oiRxPVsZ.js → duration-tone-sC3lGABz.js} +1 -1
  74. package/src/console/ui-next/dist/assets/{explorer-empty-HjTnVQoR.js → explorer-empty-BvYrygyO.js} +1 -1
  75. package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +1 -0
  76. package/src/console/ui-next/dist/assets/{highlighted-json-CvDPvveV.js → highlighted-json-CS_O8L-r.js} +1 -1
  77. package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +1 -0
  78. package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +66 -0
  79. package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +2 -0
  80. package/src/console/ui-next/dist/assets/{link-DF7SZ9Ek.js → link-COeyggt-.js} +1 -1
  81. package/src/console/ui-next/dist/assets/observability-page-HK9-BO8z.js +4 -0
  82. package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +1 -0
  83. package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +18 -0
  84. package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +1 -0
  85. package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +1 -0
  86. package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +1 -0
  87. package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +41 -0
  88. package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +2 -0
  89. package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +54 -0
  90. package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +1 -0
  91. package/src/console/ui-next/dist/assets/{use-vault-list-Cl79j_ku.js → use-vault-list-BLbZhzyF.js} +1 -1
  92. package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +2 -0
  93. package/src/console/ui-next/dist/index.html +6 -6
  94. package/src/console/ui-next/seed-invoke-host.ts +2 -0
  95. package/src/console/ui-next/src/client.ts +35 -7
  96. package/src/console/ui-next/src/components/motion/table/index.tsx +16 -0
  97. package/src/console/ui-next/src/components/motion/table/types.ts +4 -0
  98. package/src/console/ui-next/src/components/ui/kbd.tsx +2 -2
  99. package/src/console/ui-next/src/features/flows/graph/build-flow-graph.test.ts +18 -0
  100. package/src/console/ui-next/src/features/flows/graph/build-flow-graph.ts +14 -1
  101. package/src/console/ui-next/src/features/flows/graph/neighborhood.test.ts +17 -0
  102. package/src/console/ui-next/src/features/flows/graph/neighborhood.ts +16 -3
  103. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +3 -1
  104. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
  105. package/src/console/ui-next/src/features/flows/traces/http-method.ts +19 -0
  106. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +9 -3
  107. package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +29 -2
  108. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +10 -3
  109. package/src/console/ui-next/src/features/observability/lib/ask-count.test.ts +25 -0
  110. package/src/console/ui-next/src/features/observability/lib/ask-count.ts +4 -1
  111. package/src/console/ui-next/src/features/store/detail/browse-section.tsx +45 -1
  112. package/src/console/ui-next/src/features/store/detail/resource-panel.tsx +78 -16
  113. package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +155 -14
  114. package/src/console/ui-next/src/features/store/explorer/store-tree.tsx +33 -133
  115. package/src/console/ui-next/src/features/store/files/file-preview.tsx +4 -1
  116. package/src/console/ui-next/src/features/store/grid/rls-policy-sheet.tsx +326 -165
  117. package/src/console/ui-next/src/features/store/grid/sql-insert-sheet.tsx +6 -0
  118. package/src/console/ui-next/src/features/store/grid/store-data-grid.tsx +50 -3
  119. package/src/console/ui-next/src/features/store/lib/files-meta.test.ts +3 -0
  120. package/src/console/ui-next/src/features/store/lib/files-meta.ts +12 -0
  121. package/src/console/ui-next/src/features/store/lib/query-defaults.test.ts +25 -0
  122. package/src/console/ui-next/src/features/store/lib/query-defaults.ts +85 -3
  123. package/src/console/ui-next/src/features/store/lib/query-gate.ts +30 -4
  124. package/src/console/ui-next/src/features/store/lib/rls-policy.test.ts +139 -0
  125. package/src/console/ui-next/src/features/store/lib/rls-policy.ts +223 -21
  126. package/src/console/ui-next/src/features/store/lib/schema-graph.ts +4 -0
  127. package/src/console/ui-next/src/features/store/lib/sql-catalog.test.ts +24 -0
  128. package/src/console/ui-next/src/features/store/lib/sql-catalog.ts +19 -0
  129. package/src/console/ui-next/src/features/store/lib/store-tree.test.ts +6 -0
  130. package/src/console/ui-next/src/features/store/lib/store-tree.ts +13 -0
  131. package/src/console/ui-next/src/features/store/query/query-console.tsx +48 -14
  132. package/src/console/ui-next/src/features/store/query/query-gate-menu.tsx +19 -20
  133. package/src/console/ui-next/src/features/store/query/query-gate-parts.tsx +2 -1
  134. package/src/console/ui-next/src/features/store/query/query-results.tsx +110 -83
  135. package/src/console/ui-next/src/features/store/schema/schema-table-node.tsx +17 -0
  136. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +112 -72
  137. package/src/console/ui-next/src/features/units/detail/effects-summary.tsx +12 -4
  138. package/src/console/ui-next/src/features/units/lib/contract-input.test.ts +206 -1
  139. package/src/console/ui-next/src/features/units/lib/contract-input.ts +55 -0
  140. package/src/console/ui-next/src/features/vault/lib/types.ts +1 -1
  141. package/src/docker/compose-health.test.ts +27 -0
  142. package/src/docker/compose-health.ts +19 -3
  143. package/src/docker/docker.test.ts +4 -1
  144. package/src/docker/dockerfile.ts +1 -1
  145. package/src/docker/recipes/pgdog.ts +2 -0
  146. package/src/drivers/ai-anthropic.ts +5 -0
  147. package/src/drivers/ai-ollama.ts +49 -30
  148. package/src/drivers/ai-openai-compatible.ts +57 -46
  149. package/src/drivers/ai-providers.test.ts +3 -0
  150. package/src/drivers/bun-native-completeness.test.ts +7 -9
  151. package/src/drivers/clock-postgres.ts +13 -9
  152. package/src/drivers/instances-postgres.ts +4 -6
  153. package/src/drivers/journal-postgres.ts +13 -9
  154. package/src/drivers/pg-rls.test.ts +87 -0
  155. package/src/drivers/pg-rls.ts +233 -7
  156. package/src/drivers/pglite.ts +26 -3
  157. package/src/drivers/postgres.test.ts +23 -0
  158. package/src/drivers/postgres.ts +133 -9
  159. package/src/drivers/redis.ts +11 -4
  160. package/src/drivers/signal-redis.ts +24 -14
  161. package/src/drivers/signal-types.ts +2 -1
  162. package/src/drivers/types.ts +10 -0
  163. package/src/elements/ai/declare.ts +109 -0
  164. package/src/elements/ai/errors.test.ts +5 -1
  165. package/src/elements/ai/errors.ts +30 -2
  166. package/src/elements/ai/eval.ts +4 -6
  167. package/src/elements/ai/mcp-client.test.ts +206 -0
  168. package/src/elements/ai/mcp-client.ts +362 -0
  169. package/src/elements/ai/mcp-http.ts +159 -0
  170. package/src/elements/ai/mcp-mock.ts +134 -0
  171. package/src/elements/ai/mcp-protocol.ts +234 -0
  172. package/src/elements/ai/mcp-stdio.test.ts +50 -0
  173. package/src/elements/ai/mcp-stdio.ts +212 -0
  174. package/src/elements/ai/mcp-transport.ts +70 -0
  175. package/src/elements/ai/runtime.ts +159 -29
  176. package/src/elements/ai.test.ts +139 -0
  177. package/src/elements/ai.ts +16 -0
  178. package/src/elements/clock/declare.ts +26 -1
  179. package/src/elements/clock/health.test.ts +43 -0
  180. package/src/elements/clock/runtime.ts +55 -2
  181. package/src/elements/clock/schedule.ts +71 -142
  182. package/src/elements/clock.test.ts +9 -40
  183. package/src/elements/clock.ts +6 -1
  184. package/src/elements/gate/boot.ts +5 -5
  185. package/src/elements/gate/declare.ts +36 -4
  186. package/src/elements/gate/flatten.ts +1 -1
  187. package/src/elements/gate/runtime.ts +1 -1
  188. package/src/elements/index.ts +2 -0
  189. package/src/elements/signal/declare.ts +2 -1
  190. package/src/elements/signal/runtime.ts +16 -1
  191. package/src/elements/signal.ts +1 -0
  192. package/src/elements/store/cache.test.ts +2 -0
  193. package/src/elements/store/cache.ts +3 -3
  194. package/src/elements/store/declare.ts +9 -6
  195. package/src/elements/store/emit-drizzle.ts +44 -8
  196. package/src/elements/store/resource.test.ts +1 -1
  197. package/src/elements/store/rls-identity.test.ts +95 -0
  198. package/src/elements/store/rls-identity.ts +116 -0
  199. package/src/elements/store/runtime.ts +4 -0
  200. package/src/elements/store/schema-decl.test.ts +37 -0
  201. package/src/elements/store/schema-decl.ts +161 -0
  202. package/src/elements/store/seed.ts +1 -1
  203. package/src/elements/store/sql-condition.test.ts +12 -0
  204. package/src/elements/store/sql-condition.ts +26 -2
  205. package/src/elements/store/sql-rls-isolation.test.ts +165 -0
  206. package/src/elements/store/sql-rls-stamp.test.ts +117 -0
  207. package/src/elements/store/sql-session.ts +81 -3
  208. package/src/elements/store/upsert-app.test.ts +1 -1
  209. package/src/elements/store.ts +14 -0
  210. package/src/elements/vault/builtin-adapter.ts +17 -0
  211. package/src/full.ts +2 -0
  212. package/src/http.ts +20 -3
  213. package/src/index.ts +14 -1
  214. package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
  215. package/src/kernel/adopt-routes.ts +30 -2
  216. package/src/kernel/app.ts +227 -77
  217. package/src/kernel/auto-cache.test.ts +8 -8
  218. package/src/kernel/auto-registry.test.ts +54 -9
  219. package/src/kernel/boot-bind/ai.ts +24 -0
  220. package/src/kernel/boot-bind/clock.ts +2 -0
  221. package/src/kernel/boot.test.ts +4 -4
  222. package/src/kernel/boot.ts +5 -3
  223. package/src/kernel/effects-stamping.test.ts +3 -3
  224. package/src/kernel/element-registries.ts +15 -5
  225. package/src/kernel/errors.ts +24 -0
  226. package/src/kernel/flow-units.ts +64 -0
  227. package/src/kernel/flow.ts +29 -17
  228. package/src/kernel/fx-dead-letters.test.ts +77 -0
  229. package/src/kernel/fx.test.ts +22 -0
  230. package/src/kernel/fx.ts +135 -6
  231. package/src/kernel/horizontal-child.ts +4 -4
  232. package/src/kernel/http-path-pending.ts +20 -0
  233. package/src/kernel/http-resource.ts +4 -0
  234. package/src/kernel/http-stream.test.ts +174 -0
  235. package/src/kernel/index.ts +2 -0
  236. package/src/kernel/pipeline.test.ts +2 -2
  237. package/src/kernel/plugin-elements.test.ts +1 -1
  238. package/src/kernel/plugin-needs.test.ts +1 -1
  239. package/src/kernel/ready.test.ts +2 -2
  240. package/src/kernel/resource-mount.test.ts +14 -0
  241. package/src/kernel/run-duration.test.ts +2 -2
  242. package/src/kernel/stamp-http.test.ts +79 -0
  243. package/src/kernel/stamp-http.ts +75 -0
  244. package/src/kernel/triggers.ts +52 -27
  245. package/src/manifest/mcp-ref.ts +88 -0
  246. package/src/manifest/types.ts +41 -2
  247. package/src/manifest/validate.test.ts +20 -0
  248. package/src/mcp/docs-server.ts +1 -1
  249. package/src/mcp/server.ts +1 -1
  250. package/src/plugins/compression.test.ts +21 -0
  251. package/src/plugins/compression.ts +1 -0
  252. package/src/release/measure.ts +2 -2
  253. package/src/runtime/bun.ts +41 -4
  254. package/src/runtime/json-code-block.test.ts +2 -2
  255. package/src/runtime/serve.test.ts +4 -4
  256. package/src/term.test.ts +52 -4
  257. package/src/term.ts +125 -17
  258. package/src/test/reset-element-registries.ts +10 -3
  259. package/src/cli/ask-seed.test.ts +0 -96
  260. package/src/cli/ask-seed.ts +0 -82
  261. package/src/console/ui-next/dist/assets/cache-glyph-CLPBqZeb.js +0 -1
  262. package/src/console/ui-next/dist/assets/collapsible-DYb0xU8C.js +0 -1
  263. package/src/console/ui-next/dist/assets/flows-page-Dg8CTE29.js +0 -1
  264. package/src/console/ui-next/dist/assets/http-method-BJ92Z_ke.js +0 -1
  265. package/src/console/ui-next/dist/assets/index-Bp-R7jtM.js +0 -66
  266. package/src/console/ui-next/dist/assets/index-D4Ldtj79.css +0 -2
  267. package/src/console/ui-next/dist/assets/observability-page-DAnpEaq1.js +0 -4
  268. package/src/console/ui-next/dist/assets/query-gate-parts-1m8m1iNp.js +0 -2
  269. package/src/console/ui-next/dist/assets/react-B1ML8gxg.js +0 -1
  270. package/src/console/ui-next/dist/assets/replica-lag-C8_BRt2x.js +0 -18
  271. package/src/console/ui-next/dist/assets/sheet-form-D-ata7jy.js +0 -1
  272. package/src/console/ui-next/dist/assets/shortcut-keys-DO4IsVqv.js +0 -1
  273. package/src/console/ui-next/dist/assets/skeleton-CL_X0GCj.js +0 -1
  274. package/src/console/ui-next/dist/assets/store-page-v3LXdYpr.js +0 -45
  275. package/src/console/ui-next/dist/assets/trace-detail-sheet-D16lWQMt.js +0 -2
  276. package/src/console/ui-next/dist/assets/units-page-BtQ0bqMe.js +0 -1
  277. package/src/console/ui-next/dist/assets/vault-page-Ca-MvcmJ.js +0 -2
@@ -11,6 +11,7 @@ import { parseSync } from "oxc-parser";
11
11
  import type {
12
12
  Ai,
13
13
  AiAgent,
14
+ AiMcpServer,
14
15
  AiModel,
15
16
  AiPrompt,
16
17
  Channel,
@@ -21,6 +22,7 @@ import type {
21
22
  Flow,
22
23
  Gate,
23
24
  Journey,
25
+ JsonSchema,
24
26
  Manifest,
25
27
  RateStrategy,
26
28
  ResourceRef,
@@ -29,12 +31,14 @@ import type {
29
31
  SignalDelivery,
30
32
  Slo,
31
33
  Store,
34
+ TablePolicy,
32
35
  Trigger,
33
36
  } from "../manifest/types.ts";
34
37
  import { sqlTableRef } from "../manifest/sql-resource.ts";
35
38
  import {
36
39
  identifierName,
37
40
  inferEffects,
41
+ resolveCallTarget,
38
42
  stringArg,
39
43
  walk,
40
44
  type AstNode,
@@ -43,6 +47,12 @@ import {
43
47
  type InferBinding,
44
48
  type Literal,
45
49
  } from "./effects-infer.ts";
50
+ import { nameFromFlowFile, pathFromFlowFile } from "./flow-path.ts";
51
+ import {
52
+ defaultListInSchema,
53
+ jsonSchemaFromAst,
54
+ type SchemaExpandContext,
55
+ } from "./schema-from-ast.ts";
46
56
 
47
57
  /** One source file for extraction. */
48
58
  export interface SourceFile {
@@ -65,16 +75,24 @@ export interface ExtractManifestOptions {
65
75
  }
66
76
 
67
77
  /** Internal project scope accumulated across files. */
78
+ type DeclaredSchemaTable = {
79
+ readonly name: string;
80
+ readonly columns: Record<string, DeclaredColumn>;
81
+ readonly rls?: boolean;
82
+ readonly policies?: Record<string, TablePolicy>;
83
+ };
84
+
68
85
  interface ProjectScope {
69
86
  app: string;
70
87
  bindings: Map<string, InferBinding>;
71
88
  signals: Record<string, Signal>;
72
89
  stores: Record<string, Store>;
73
90
  /** Binding or table name → declared columns from `store.schema.table`. */
74
- schemaTables: Map<
75
- string,
76
- { readonly name: string; readonly columns: Record<string, DeclaredColumn> }
77
- >;
91
+ schemaTables: Map<string, DeclaredSchemaTable>;
92
+ /** `const Name = <init>` per file — used to expand flow `in` / `out`. */
93
+ constInitsByFile: Map<string, Map<string, AstNode>>;
94
+ /** Unique project-wide const names (ambiguous names omitted). */
95
+ uniqueConstInits: Map<string, { node: AstNode; file: string }>;
78
96
  clocks: Record<string, Clock>;
79
97
  gates: Record<string, Gate>;
80
98
  /** Local binding name → gate manifest id (policy name or rate expression). */
@@ -117,6 +135,8 @@ export async function extractManifest(options: ExtractManifestOptions = {}): Pro
117
135
  signals: {},
118
136
  stores: {},
119
137
  schemaTables: new Map(),
138
+ constInitsByFile: new Map(),
139
+ uniqueConstInits: new Map(),
120
140
  clocks: {},
121
141
  gates: {},
122
142
  gateIds: new Map(),
@@ -142,8 +162,8 @@ export async function extractManifest(options: ExtractManifestOptions = {}): Pro
142
162
  });
143
163
 
144
164
  // Pass 1 — declarations (elements, config, named bindings).
145
- for (const { program } of parsed) {
146
- collectDeclarations(program, scope);
165
+ for (const { file, program } of parsed) {
166
+ collectDeclarations(file.path, program, scope);
147
167
  }
148
168
 
149
169
  // Pass 2 — flows / on() bindings.
@@ -177,11 +197,12 @@ export async function extractManifest(options: ExtractManifestOptions = {}): Pro
177
197
  if (Object.keys(gates).length > 0) manifest.gates = gates;
178
198
  if (Object.keys(vault).length > 0) manifest.vault = vault;
179
199
  if (Object.keys(channels).length > 0) manifest.channels = channels;
180
- if (scope.ai.models || scope.ai.prompts || scope.ai.agents) {
200
+ if (scope.ai.models || scope.ai.prompts || scope.ai.agents || scope.ai.mcpServers) {
181
201
  manifest.ai = {
182
202
  ...(scope.ai.models ? { models: sortRecord(scope.ai.models) } : {}),
183
203
  ...(scope.ai.prompts ? { prompts: sortRecord(scope.ai.prompts) } : {}),
184
204
  ...(scope.ai.agents ? { agents: sortRecord(scope.ai.agents) } : {}),
205
+ ...(scope.ai.mcpServers ? { mcpServers: sortRecord(scope.ai.mcpServers) } : {}),
185
206
  };
186
207
  }
187
208
  if (Object.keys(journeys).length > 0) manifest.journeys = journeys;
@@ -227,13 +248,13 @@ async function readSources(rootDir: string, pattern: string): Promise<SourceFile
227
248
  return files;
228
249
  }
229
250
 
230
- function collectDeclarations(program: AstNode, scope: ProjectScope): void {
251
+ function collectDeclarations(filePath: string, program: AstNode, scope: ProjectScope): void {
231
252
  walk(program, (node) => {
232
253
  if (node.type === "CallExpression") {
233
254
  visitDeclarationCall(node as CallExpression, program, scope);
234
255
  }
235
256
  if (node.type === "VariableDeclarator") {
236
- visitDeclarator(node as AstNode, scope);
257
+ visitDeclarator(node as AstNode, filePath, scope);
237
258
  }
238
259
  });
239
260
 
@@ -248,12 +269,13 @@ function collectDeclarations(program: AstNode, scope: ProjectScope): void {
248
269
  });
249
270
  }
250
271
 
251
- function visitDeclarator(decl: AstNode, scope: ProjectScope): void {
272
+ function visitDeclarator(decl: AstNode, filePath: string, scope: ProjectScope): void {
252
273
  const id = (decl as AstNode & { id?: AstNode }).id;
253
274
  const init = (decl as AstNode & { init?: AstNode }).init;
254
275
  if (!id || !init) return;
255
276
  const name = identifierName(id);
256
277
  if (!name) return;
278
+ recordConstInit(scope, filePath, name, unwrapTsExpr(init));
257
279
 
258
280
  // export const x = on(...) / flow(...) — flow export alias
259
281
  if (init.type === "CallExpression") {
@@ -301,6 +323,13 @@ function finalizeRefs(scope: ProjectScope): void {
301
323
  );
302
324
  }
303
325
  }
326
+ if (scope.ai.mcpServers) {
327
+ for (const server of Object.values(scope.ai.mcpServers)) {
328
+ if (!server.auth) continue;
329
+ const binding = scope.bindings.get(server.auth);
330
+ if (binding?.kind === "secret") server.auth = binding.ref;
331
+ }
332
+ }
304
333
  for (const journey of Object.values(scope.journeys)) {
305
334
  if (!journey.flows) continue;
306
335
  journey.flows = journey.flows.map(
@@ -316,7 +345,7 @@ function finalizeRefs(scope: ProjectScope): void {
316
345
  if (!declared) continue;
317
346
  const existing = table.columns ?? {};
318
347
  if (Object.keys(existing).length === 0) {
319
- store.tables[name] = { ...table, columns: declared.columns };
348
+ store.tables[name] = { ...table, ...tableFromDeclared(declared) };
320
349
  }
321
350
  }
322
351
  }
@@ -328,7 +357,7 @@ function finalizeRefs(scope: ProjectScope): void {
328
357
  if (!store.tables) continue;
329
358
  for (const name of Object.keys(store.tables)) attachedNames.add(name);
330
359
  }
331
- const orphanByName = new Map<string, { name: string; columns: Record<string, DeclaredColumn> }>();
360
+ const orphanByName = new Map<string, DeclaredSchemaTable>();
332
361
  for (const t of scope.schemaTables.values()) {
333
362
  if (!attachedNames.has(t.name)) orphanByName.set(t.name, t);
334
363
  }
@@ -341,7 +370,7 @@ function finalizeRefs(scope: ProjectScope): void {
341
370
  target.tables = target.tables ?? {};
342
371
  for (const t of orphanByName.values()) {
343
372
  if (target.tables[t.name]) continue;
344
- target.tables[t.name] = { columns: t.columns };
373
+ target.tables[t.name] = tableFromDeclared(t);
345
374
  }
346
375
  }
347
376
  }
@@ -351,7 +380,8 @@ function collectSchemaTable(call: CallExpression, program: AstNode, scope: Proje
351
380
  if (!tableName) return;
352
381
  const colsNode = objectArg(call.arguments[1]);
353
382
  const columns = colsNode ? parseDeclaredColumns(colsNode) : {};
354
- const entry = { name: tableName, columns };
383
+ const extras = parseSchemaTableExtras(call.arguments[2]);
384
+ const entry = { name: tableName, columns, ...extras };
355
385
  scope.schemaTables.set(tableName, entry);
356
386
  const bindingName = enclosingConstName(call, program);
357
387
  if (bindingName) {
@@ -386,7 +416,7 @@ function attachSchemaOption(
386
416
  const id = (value as Identifier).name;
387
417
  const declared = scope.schemaTables.get(id) ?? scope.schemaTables.get(key);
388
418
  if (declared) {
389
- store.tables[declared.name] = { columns: declared.columns };
419
+ store.tables[declared.name] = tableFromDeclared(declared);
390
420
  } else {
391
421
  store.tables[key] = store.tables[key] ?? {};
392
422
  }
@@ -396,7 +426,7 @@ function attachSchemaOption(
396
426
  if (value.type === "CallExpression") {
397
427
  const inline = parseInlineSchemaTable(value as CallExpression);
398
428
  if (inline) {
399
- store.tables[inline.name] = { columns: inline.columns };
429
+ store.tables[inline.name] = tableFromDeclared(inline);
400
430
  scope.schemaTables.set(inline.name, inline);
401
431
  }
402
432
  }
@@ -410,9 +440,7 @@ function attachSchemaOption(
410
440
  }
411
441
  }
412
442
 
413
- function parseInlineSchemaTable(
414
- call: CallExpression,
415
- ): { name: string; columns: Record<string, DeclaredColumn> } | undefined {
443
+ function parseInlineSchemaTable(call: CallExpression): DeclaredSchemaTable | undefined {
416
444
  const callee = call.callee;
417
445
  if (callee.type !== "MemberExpression") return undefined;
418
446
  const member = callee as AstNode & { object: AstNode; property: AstNode };
@@ -428,6 +456,121 @@ function parseInlineSchemaTable(
428
456
  return {
429
457
  name: tableName,
430
458
  columns: colsNode ? parseDeclaredColumns(colsNode) : {},
459
+ ...parseSchemaTableExtras(call.arguments[2]),
460
+ };
461
+ }
462
+
463
+ function tableFromDeclared(t: DeclaredSchemaTable): {
464
+ columns: Record<string, DeclaredColumn>;
465
+ rls?: boolean;
466
+ policies?: Record<string, TablePolicy>;
467
+ } {
468
+ return {
469
+ columns: t.columns,
470
+ ...(t.rls ? { rls: true } : {}),
471
+ ...(t.policies ? { policies: t.policies } : {}),
472
+ };
473
+ }
474
+
475
+ function parseSchemaTableExtras(
476
+ node: AstNode | undefined,
477
+ ): Pick<DeclaredSchemaTable, "rls" | "policies"> {
478
+ if (!node || node.type !== "ArrayExpression") return {};
479
+ const els = (node as AstNode & { elements?: AstNode[] }).elements ?? [];
480
+ const policies: Record<string, TablePolicy> = {};
481
+ let rls = false;
482
+ for (const el of els) {
483
+ if (!el || el.type !== "CallExpression") continue;
484
+ const extra = parseSchemaTableExtra(el as CallExpression);
485
+ if (!extra) continue;
486
+ if (extra.kind === "rls") {
487
+ rls = true;
488
+ continue;
489
+ }
490
+ policies[extra.name] = extra.policy;
491
+ rls = true;
492
+ }
493
+ return {
494
+ ...(rls ? { rls: true } : {}),
495
+ ...(Object.keys(policies).length > 0 ? { policies } : {}),
496
+ };
497
+ }
498
+
499
+ function parseSchemaTableExtra(
500
+ call: CallExpression,
501
+ ): { kind: "rls" } | { kind: "policy"; name: string; policy: TablePolicy } | undefined {
502
+ const callee = call.callee;
503
+ if (callee.type !== "MemberExpression") return undefined;
504
+ const member = callee as AstNode & { object: AstNode; property: AstNode };
505
+ const method = identifierName(member.property);
506
+ if (method === "rls" && isStoreSchemaCallee(member.object)) {
507
+ return { kind: "rls" };
508
+ }
509
+ if (method === "policy" && isStoreSchemaCallee(member.object)) {
510
+ const name = stringArg(call.arguments[0]);
511
+ if (!name) return undefined;
512
+ return { kind: "policy", name, policy: parseTablePolicyOptions(objectArg(call.arguments[1])) };
513
+ }
514
+ if (
515
+ (method === "gate" || method === "owner" || method === "scope") &&
516
+ isStoreSchemaPolicyCallee(member.object)
517
+ ) {
518
+ const key = stringArg(call.arguments[0]);
519
+ if (!key) return undefined;
520
+ const opts = objectArg(call.arguments[1]);
521
+ const command = (stringProp(opts, "for") ??
522
+ (method === "owner"
523
+ ? "all"
524
+ : method === "scope"
525
+ ? "insert"
526
+ : "select")) as TablePolicy["for"];
527
+ const slug = key.replace(/[^A-Za-z0-9]+/g, "_").replace(/^_|_$/g, "");
528
+ const name = `${method}_${slug}_${command}`;
529
+ const lit = `'${key.replaceAll("'", "''")}'`;
530
+ const expr =
531
+ method === "gate"
532
+ ? `oke.gate() = ${lit}`
533
+ : method === "owner"
534
+ ? `${key} = oke.user()`
535
+ : `oke.has_scope(${lit})`;
536
+ const policy: TablePolicy = {
537
+ for: command,
538
+ ...(command === "insert"
539
+ ? { withCheck: expr }
540
+ : command === "update" || command === "all"
541
+ ? { using: expr, withCheck: expr }
542
+ : { using: expr }),
543
+ };
544
+ return { kind: "policy", name, policy };
545
+ }
546
+ return undefined;
547
+ }
548
+
549
+ function isStoreSchemaCallee(node: AstNode): boolean {
550
+ if (node.type !== "MemberExpression") return false;
551
+ const member = node as AstNode & { object: AstNode; property: AstNode };
552
+ return identifierName(member.object) === "store" && identifierName(member.property) === "schema";
553
+ }
554
+
555
+ function isStoreSchemaPolicyCallee(node: AstNode): boolean {
556
+ if (node.type !== "MemberExpression") return false;
557
+ const member = node as AstNode & { object: AstNode; property: AstNode };
558
+ return identifierName(member.property) === "policy" && isStoreSchemaCallee(member.object);
559
+ }
560
+
561
+ function parseTablePolicyOptions(opts: AstNode | undefined): TablePolicy {
562
+ const as = stringProp(opts, "as") as TablePolicy["as"];
563
+ const forCmd = stringProp(opts, "for") as TablePolicy["for"];
564
+ const using = stringProp(opts, "using");
565
+ const withCheck = stringProp(opts, "withCheck");
566
+ const toNode = objectProp(opts, "to");
567
+ const to = stringArg(toNode) ?? stringArrayProp(opts, "to");
568
+ return {
569
+ ...(as ? { as } : {}),
570
+ ...(to ? { to } : {}),
571
+ ...(forCmd ? { for: forCmd } : {}),
572
+ ...(using ? { using } : {}),
573
+ ...(withCheck ? { withCheck } : {}),
431
574
  };
432
575
  }
433
576
 
@@ -472,6 +615,10 @@ function parseFieldChain(node: AstNode, key: string): DeclaredColumn | undefined
472
615
  if (method === "describe") {
473
616
  description = stringArg(call.arguments[0]) ?? description;
474
617
  }
618
+ if (method === "defaultFn") {
619
+ hasDefault = true;
620
+ defaultValue = defaultValue ?? null;
621
+ }
475
622
  if (method === "default") {
476
623
  const lit = call.arguments[0];
477
624
  if (lit && lit.type === "Literal") {
@@ -816,6 +963,7 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
816
963
  const model: AiModel = {
817
964
  ...(stringProp(opts, "provider") ? { provider: stringProp(opts, "provider") } : {}),
818
965
  ...(stringProp(opts, "tier") ? { tier: stringProp(opts, "tier") } : {}),
966
+ ...(stringProp(opts, "driverId") ? { driverId: stringProp(opts, "driverId") } : {}),
819
967
  };
820
968
  scope.ai.models = scope.ai.models ?? {};
821
969
  scope.ai.models[modelName] = model;
@@ -833,6 +981,24 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
833
981
  collectAgent(call, scope);
834
982
  }
835
983
 
984
+ if (obj === "ai" && prop === "mcpServer") {
985
+ collectMcpServer(call, program, scope);
986
+ }
987
+
988
+ if (prop === "tool" && obj) {
989
+ const toolName = stringArg(call.arguments[0]);
990
+ const server = scope.bindings.get(obj);
991
+ if (server?.kind === "mcp-server" && toolName) {
992
+ const bindingName = enclosingConstName(call, program);
993
+ if (bindingName) {
994
+ scope.bindings.set(bindingName, {
995
+ kind: "mcp-tool",
996
+ ref: `mcp:${server.ref}/${toolName}`,
997
+ });
998
+ }
999
+ }
1000
+ }
1001
+
836
1002
  if (obj === "ai" && prop === "embed") {
837
1003
  const embedName = stringArg(call.arguments[0]);
838
1004
  if (embedName) {
@@ -1026,7 +1192,11 @@ function collectAgent(call: CallExpression, scope: ProjectScope): void {
1026
1192
  if (!agentName || !opts) return;
1027
1193
  const toolsArr = arrayProp(opts, "tools");
1028
1194
  const tools = toolsArr
1029
- ?.map((el) => identifierName(el) ?? stringArg(el))
1195
+ ?.map((el) => {
1196
+ const resolved = resolveCallTarget(el, scope.bindings);
1197
+ if (resolved) return resolved;
1198
+ return identifierName(el) ?? stringArg(el);
1199
+ })
1030
1200
  .filter((x): x is string => typeof x === "string")
1031
1201
  .map((id) => scope.flowExports.get(id) ?? scope.bindings.get(id)?.ref ?? id);
1032
1202
  const agent: AiAgent = {
@@ -1039,6 +1209,40 @@ function collectAgent(call: CallExpression, scope: ProjectScope): void {
1039
1209
  scope.ai.agents[agentName] = agent;
1040
1210
  }
1041
1211
 
1212
+ function collectMcpServer(call: CallExpression, program: AstNode, scope: ProjectScope): void {
1213
+ const serverName = stringArg(call.arguments[0]);
1214
+ const opts = objectArg(call.arguments[1]);
1215
+ if (!serverName || !opts) return;
1216
+ const tools = stringArrayProp(opts, "tools") ?? [];
1217
+ const url = stringProp(opts, "url");
1218
+ const command = stringProp(opts, "command");
1219
+ const args = stringArrayProp(opts, "args");
1220
+ const authObj = objectProp(opts, "auth");
1221
+ const bearerNode = authObj ? objectProp(authObj, "bearer") : undefined;
1222
+ const bearerLit = stringArg(bearerNode);
1223
+ const bearerId = identifierName(bearerNode);
1224
+ const auth =
1225
+ bearerLit ??
1226
+ (bearerId
1227
+ ? scope.bindings.get(bearerId)?.kind === "secret"
1228
+ ? scope.bindings.get(bearerId)?.ref
1229
+ : bearerId
1230
+ : undefined);
1231
+ const server: AiMcpServer = {
1232
+ tools,
1233
+ ...(url ? { url } : {}),
1234
+ ...(command ? { command } : {}),
1235
+ ...(args && args.length > 0 ? { args } : {}),
1236
+ ...(auth ? { auth } : {}),
1237
+ };
1238
+ scope.ai.mcpServers = scope.ai.mcpServers ?? {};
1239
+ scope.ai.mcpServers[serverName] = server;
1240
+ const bindingName = enclosingConstName(call, program);
1241
+ if (bindingName) {
1242
+ scope.bindings.set(bindingName, { kind: "mcp-server", ref: serverName });
1243
+ }
1244
+ }
1245
+
1042
1246
  function collectConfig(opts: AstNode | undefined, scope: ProjectScope): void {
1043
1247
  if (!opts) return;
1044
1248
 
@@ -1233,7 +1437,7 @@ function registerFlow(args: {
1233
1437
  scope: ProjectScope;
1234
1438
  exportName: string | undefined;
1235
1439
  }): void {
1236
- const opts = objectArg(args.flowCall.arguments[1]);
1440
+ const opts = flowOptionsArg(args.flowCall);
1237
1441
  if (!opts) return;
1238
1442
 
1239
1443
  // `flow(\`${unit}.list\`)` inside a helper — name is only known at the
@@ -1242,6 +1446,7 @@ function registerFlow(args: {
1242
1446
 
1243
1447
  const name =
1244
1448
  stringArg(args.flowCall.arguments[0]) ??
1449
+ nameFromFlowFile(args.file.path, args.exportName) ??
1245
1450
  args.exportName ??
1246
1451
  `flow_${Object.keys(args.scope.flows).length + 1}`;
1247
1452
 
@@ -1289,7 +1494,9 @@ function registerFlow(args: {
1289
1494
  }
1290
1495
  }
1291
1496
 
1292
- const trigger = args.triggerNode ? parseTrigger(args.triggerNode, args.scope) : undefined;
1497
+ const trigger = args.triggerNode
1498
+ ? parseTrigger(args.triggerNode, args.scope, args.file.path)
1499
+ : undefined;
1293
1500
 
1294
1501
  const gates = trigger?.gates;
1295
1502
  const liveFromTrigger = trigger?.live;
@@ -1302,9 +1509,9 @@ function registerFlow(args: {
1302
1509
  }
1303
1510
  if (gates && gates.length > 0) flow.gates = gates;
1304
1511
 
1305
- const inSchema = schemaProp(opts, "in");
1512
+ const inSchema = schemaProp(opts, "in", args.scope, args.file.path);
1306
1513
  if (inSchema !== undefined) flow.in = inSchema;
1307
- const outSchema = schemaProp(opts, "out");
1514
+ const outSchema = schemaProp(opts, "out", args.scope, args.file.path);
1308
1515
  if (outSchema !== undefined) flow.out = outSchema;
1309
1516
 
1310
1517
  const errors = parseErrors(objectProp(opts, "errors"));
@@ -1393,8 +1600,8 @@ function registerFlow(args: {
1393
1600
  }
1394
1601
 
1395
1602
  /**
1396
- * `http.resource(path, bag).gate(...).live()` — find the resource call and
1397
- * collect the same chain {@link parseHttpTrigger} reads on a single verb.
1603
+ * `http.resource(path, bag).gate(...).public().live()` — find the resource call
1604
+ * and collect the same chain {@link parseHttpTrigger} reads on a single verb.
1398
1605
  *
1399
1606
  * @param node - First argument to `on(…)`
1400
1607
  * @param scope - Project scope (gate name expansion)
@@ -1425,19 +1632,8 @@ function unwrapResourceMount(
1425
1632
  }
1426
1633
  continue;
1427
1634
  }
1635
+ if (prop === "public") gateNames.push("public");
1428
1636
  if (prop === "live") live = true;
1429
- continue;
1430
- }
1431
- if (item.type === "MemberExpression") {
1432
- const member = item as AstNode & { object: AstNode; property: AstNode };
1433
- if (identifierName(member.property) !== "public") continue;
1434
- const obj = member.object;
1435
- if (
1436
- obj.type === "MemberExpression" &&
1437
- identifierName((obj as AstNode & { property: AstNode }).property) === "gate"
1438
- ) {
1439
- gateNames.push("public");
1440
- }
1441
1637
  }
1442
1638
  }
1443
1639
  if (!httpCall) return undefined;
@@ -1558,32 +1754,86 @@ function registerNamedTableCrud(call: CallExpression, file: SourceFile, scope: P
1558
1754
  const idPath = `${path}/:id`;
1559
1755
  const line = lineAt(file.source, call.start ?? 0);
1560
1756
  const live = boolProp(opts, "liveList") === true;
1757
+ const skipCreate = boolProp(opts, "skipCreate") === true;
1758
+ const ctx = schemaExpandContext(scope, file.path);
1759
+ const createIn = jsonSchemaFromAst(objectProp(opts, "createIn"), ctx);
1760
+ const itemOut = jsonSchemaFromAst(objectProp(opts, "out"), ctx);
1761
+ const updateIn = jsonSchemaFromAst(objectProp(opts, "updateIn"), ctx) ?? createIn;
1762
+ const idSchema: JsonSchema = {
1763
+ type: "object",
1764
+ properties: { id: { type: "string" } },
1765
+ required: ["id"],
1766
+ };
1767
+ const okSchema: JsonSchema = {
1768
+ type: "object",
1769
+ properties: { ok: { const: true, type: "boolean" } },
1770
+ required: ["ok"],
1771
+ };
1561
1772
 
1562
- const verbs = [
1773
+ const verbs: readonly {
1774
+ readonly op: string;
1775
+ readonly method: string;
1776
+ readonly p: string;
1777
+ readonly eff: Effects | undefined;
1778
+ readonly live?: boolean;
1779
+ readonly in?: JsonSchema;
1780
+ readonly out?: JsonSchema;
1781
+ }[] = [
1563
1782
  {
1564
1783
  op: "list",
1565
1784
  method: "GET",
1566
1785
  p: path,
1567
1786
  eff: storeRef ? { reads: [storeRef] } : undefined,
1568
1787
  live,
1788
+ in: defaultListInSchema(),
1789
+ ...(itemOut ? { out: { type: "array", items: itemOut } } : {}),
1790
+ },
1791
+ ...(skipCreate
1792
+ ? []
1793
+ : [
1794
+ {
1795
+ op: "create",
1796
+ method: "POST",
1797
+ p: path,
1798
+ eff: storeRef ? { writes: [storeRef] } : undefined,
1799
+ ...(createIn ? { in: createIn } : {}),
1800
+ out: idSchema,
1801
+ },
1802
+ ]),
1803
+ {
1804
+ op: "get",
1805
+ method: "GET",
1806
+ p: idPath,
1807
+ eff: storeRef ? { reads: [storeRef] } : undefined,
1808
+ in: idSchema,
1809
+ ...(itemOut ? { out: itemOut } : {}),
1810
+ },
1811
+ {
1812
+ op: "update",
1813
+ method: "PATCH",
1814
+ p: idPath,
1815
+ eff: both,
1816
+ in: updateIn ? mergeIdIntoSchema(updateIn) : idSchema,
1817
+ out: idSchema,
1569
1818
  },
1570
- { op: "create", method: "POST", p: path, eff: storeRef ? { writes: [storeRef] } : undefined },
1571
- { op: "get", method: "GET", p: idPath, eff: storeRef ? { reads: [storeRef] } : undefined },
1572
- { op: "update", method: "PATCH", p: idPath, eff: both },
1573
1819
  {
1574
1820
  op: "delete",
1575
1821
  method: "DELETE",
1576
1822
  p: idPath,
1577
1823
  eff: storeRef ? { reads: [storeRef], writes: [storeRef] } : undefined,
1824
+ in: idSchema,
1825
+ out: okSchema,
1578
1826
  },
1579
- ] as const;
1827
+ ];
1580
1828
 
1581
1829
  for (const v of verbs) {
1582
1830
  const name = `${unit}.${v.op}`;
1583
1831
  const flow: Flow = {
1584
1832
  trigger: { http: { method: v.method as never, path: v.p } },
1585
1833
  ...(v.eff ? { effects: v.eff as Effects } : {}),
1586
- ...("live" in v && v.live ? { live: true } : {}),
1834
+ ...(v.live ? { live: true } : {}),
1835
+ ...(v.in !== undefined ? { in: v.in } : {}),
1836
+ ...(v.out !== undefined ? { out: v.out } : {}),
1587
1837
  source: `${file.path}:${line}`,
1588
1838
  };
1589
1839
  scope.flows[name] = flow;
@@ -1597,7 +1847,11 @@ interface ParsedTrigger {
1597
1847
  live?: boolean;
1598
1848
  }
1599
1849
 
1600
- function parseTrigger(node: AstNode, scope: ProjectScope): ParsedTrigger | undefined {
1850
+ function parseTrigger(
1851
+ node: AstNode,
1852
+ scope: ProjectScope,
1853
+ filePath?: string,
1854
+ ): ParsedTrigger | undefined {
1601
1855
  // every("10m")
1602
1856
  if (node.type === "CallExpression") {
1603
1857
  const call = node as CallExpression;
@@ -1610,8 +1864,8 @@ function parseTrigger(node: AstNode, scope: ProjectScope): ParsedTrigger | undef
1610
1864
  return { trigger: {} };
1611
1865
  }
1612
1866
 
1613
- // http.post("/x").gate(...).live() / http.get("/x").gate.public
1614
- const http = parseHttpTrigger(call, scope);
1867
+ // http.post("/x").gate(...).live() / http.get("/x").public()
1868
+ const http = parseHttpTrigger(call, scope, filePath);
1615
1869
  if (http) return http;
1616
1870
 
1617
1871
  // table("orders").changed("status") / db.table(orders).changed(...)
@@ -1622,7 +1876,7 @@ function parseTrigger(node: AstNode, scope: ProjectScope): ParsedTrigger | undef
1622
1876
  }
1623
1877
 
1624
1878
  if (node.type === "MemberExpression") {
1625
- const http = parseHttpTrigger(node, scope);
1879
+ const http = parseHttpTrigger(node, scope, filePath);
1626
1880
  if (http) return http;
1627
1881
  }
1628
1882
 
@@ -1640,8 +1894,12 @@ function parseTrigger(node: AstNode, scope: ProjectScope): ParsedTrigger | undef
1640
1894
  return undefined;
1641
1895
  }
1642
1896
 
1643
- function parseHttpTrigger(leaf: AstNode, scope: ProjectScope): ParsedTrigger | undefined {
1644
- // Walk: http.METHOD(path).gate(...).live() / .gate.public
1897
+ function parseHttpTrigger(
1898
+ leaf: AstNode,
1899
+ scope: ProjectScope,
1900
+ filePath?: string,
1901
+ ): ParsedTrigger | undefined {
1902
+ // Walk: http.METHOD(path).gate(...).live() / .public()
1645
1903
  const chain = flattenMemberCallChain(leaf);
1646
1904
  let method: string | undefined;
1647
1905
  let path: string | undefined;
@@ -1670,24 +1928,16 @@ function parseHttpTrigger(leaf: AstNode, scope: ProjectScope): ParsedTrigger | u
1670
1928
  continue;
1671
1929
  }
1672
1930
 
1931
+ if (prop === "public") gateNames.push("public");
1673
1932
  if (prop === "live") live = true;
1674
- continue;
1675
- }
1676
-
1677
- if (node.type === "MemberExpression") {
1678
- const member = node as AstNode & { object: AstNode; property: AstNode };
1679
- if (identifierName(member.property) !== "public") continue;
1680
- const obj = member.object;
1681
- if (
1682
- obj.type === "MemberExpression" &&
1683
- identifierName((obj as AstNode & { property: AstNode }).property) === "gate"
1684
- ) {
1685
- gateNames.push("public");
1686
- }
1687
1933
  }
1688
1934
  }
1689
1935
 
1690
- if (!method || !path) return undefined;
1936
+ if (!method) return undefined;
1937
+ if (!path) {
1938
+ path = filePath ? pathFromFlowFile(filePath) : undefined;
1939
+ }
1940
+ if (!path) return undefined;
1691
1941
 
1692
1942
  const httpMethods = [
1693
1943
  "GET",
@@ -1806,14 +2056,69 @@ function parseSlo(node: AstNode | undefined): Slo | undefined {
1806
2056
  };
1807
2057
  }
1808
2058
 
1809
- function schemaProp(obj: AstNode | undefined, key: string): string | undefined {
2059
+ function recordConstInit(scope: ProjectScope, filePath: string, name: string, node: AstNode): void {
2060
+ const byFile = scope.constInitsByFile.get(filePath) ?? new Map<string, AstNode>();
2061
+ byFile.set(name, node);
2062
+ scope.constInitsByFile.set(filePath, byFile);
2063
+ const existing = scope.uniqueConstInits.get(name);
2064
+ if (existing === undefined) {
2065
+ scope.uniqueConstInits.set(name, { node, file: filePath });
2066
+ return;
2067
+ }
2068
+ if (existing.file !== filePath) {
2069
+ scope.uniqueConstInits.delete(name);
2070
+ }
2071
+ }
2072
+
2073
+ function schemaExpandContext(scope: ProjectScope, filePath: string): SchemaExpandContext {
2074
+ return {
2075
+ filePath,
2076
+ resolveConst: (name, fromFile) => {
2077
+ const local = scope.constInitsByFile.get(fromFile)?.get(name);
2078
+ if (local) return { node: local, file: fromFile };
2079
+ return scope.uniqueConstInits.get(name);
2080
+ },
2081
+ bindings: scope.bindings,
2082
+ tableColumns: (name) => scope.schemaTables.get(name)?.columns,
2083
+ };
2084
+ }
2085
+
2086
+ function mergeIdIntoSchema(schema: JsonSchema): JsonSchema {
2087
+ if (typeof schema === "string") {
2088
+ return {
2089
+ type: "object",
2090
+ properties: { id: { type: "string" } },
2091
+ required: ["id"],
2092
+ };
2093
+ }
2094
+ const properties = {
2095
+ ...((schema.properties as Record<string, unknown> | undefined) ?? {}),
2096
+ id: { type: "string" },
2097
+ };
2098
+ const required = [
2099
+ ...new Set([...(Array.isArray(schema.required) ? (schema.required as string[]) : []), "id"]),
2100
+ ];
2101
+ return { ...schema, type: "object", properties, required };
2102
+ }
2103
+
2104
+ function schemaProp(
2105
+ obj: AstNode | undefined,
2106
+ key: string,
2107
+ scope: ProjectScope,
2108
+ filePath: string,
2109
+ ): JsonSchema | undefined {
1810
2110
  const node = objectProp(obj, key);
1811
2111
  if (!node) return undefined;
1812
- // Identifier / member schemas → opaque placeholder matching the spec excerpt.
1813
- if (node.type === "Identifier" || node.type === "MemberExpression") {
2112
+ const expanded = jsonSchemaFromAst(node, schemaExpandContext(scope, filePath));
2113
+ if (expanded) return expanded;
2114
+ // Identifier / member / call that we could not expand — spec excerpt placeholder.
2115
+ if (
2116
+ node.type === "Identifier" ||
2117
+ node.type === "MemberExpression" ||
2118
+ node.type === "CallExpression"
2119
+ ) {
1814
2120
  return "…";
1815
2121
  }
1816
- if (node.type === "CallExpression") return "…";
1817
2122
  const lit = stringArg(node);
1818
2123
  if (lit) return lit;
1819
2124
  return "…";
@@ -1821,7 +2126,7 @@ function schemaProp(obj: AstNode | undefined, key: string): string | undefined {
1821
2126
 
1822
2127
  // ── AST helpers ────────────────────────────────────────────────────────────
1823
2128
 
1824
- /** Leaf-to-root walk of `http.get("/x").gate.public.live()`. */
2129
+ /** Leaf-to-root walk of `http.get("/x").public().live()`. */
1825
2130
  function flattenMemberCallChain(leaf: AstNode): AstNode[] {
1826
2131
  const chain: AstNode[] = [];
1827
2132
  let current: AstNode = leaf;
@@ -1941,6 +2246,19 @@ function objectArg(node: AstNode | undefined): AstNode | undefined {
1941
2246
  return undefined;
1942
2247
  }
1943
2248
 
2249
+ /**
2250
+ * `flow("name", { do })` options live in arg1; `flow({ do })` in arg0.
2251
+ *
2252
+ * @param flowCall - `flow(...)` AST node
2253
+ */
2254
+ function flowOptionsArg(flowCall: CallExpression): AstNode | undefined {
2255
+ const named = objectArg(flowCall.arguments[1]);
2256
+ if (named) return named;
2257
+ if (stringArg(flowCall.arguments[0]) !== undefined) return undefined;
2258
+ if (isInterpolatedTemplate(flowCall.arguments[0])) return undefined;
2259
+ return objectArg(flowCall.arguments[0]);
2260
+ }
2261
+
1944
2262
  function objectProp(obj: AstNode | undefined, key: string): AstNode | undefined {
1945
2263
  if (!obj || obj.type !== "ObjectExpression") return undefined;
1946
2264
  for (const prop of objectProperties(obj)) {