okengine 0.21.1 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (342) hide show
  1. package/AGENTS.md +11 -4
  2. package/LICENSE +202 -0
  3. package/NOTICE +2 -0
  4. package/README.md +15 -9
  5. package/manifest.v1.schema.json +54 -0
  6. package/package.json +53 -38
  7. package/site/content/docs/ai/mcp.mdx +13 -9
  8. package/site/content/docs/ai/skills.mdx +2 -2
  9. package/site/content/docs/client/calling.mdx +27 -19
  10. package/site/content/docs/client/index.mdx +1 -1
  11. package/site/content/docs/elements/ai/agents.mdx +61 -13
  12. package/site/content/docs/elements/ai/decide.mdx +536 -0
  13. package/site/content/docs/elements/ai/events.mdx +373 -0
  14. package/site/content/docs/elements/ai/index.mdx +9 -3
  15. package/site/content/docs/elements/ai/meta.json +1 -1
  16. package/site/content/docs/elements/ai/prompts.mdx +4 -2
  17. package/site/content/docs/elements/channel/email.mdx +1 -1
  18. package/site/content/docs/elements/channel/index.mdx +1 -1
  19. package/site/content/docs/elements/flow/http.mdx +4 -4
  20. package/site/content/docs/elements/flow/index.mdx +3 -3
  21. package/site/content/docs/elements/flow/meta.json +1 -1
  22. package/site/content/docs/elements/gate/auth.mdx +4 -1
  23. package/site/content/docs/elements/gate/tenancy.mdx +1 -1
  24. package/site/content/docs/elements/store/index.mdx +1 -1
  25. package/site/content/docs/elements/store/kv.mdx +5 -5
  26. package/site/content/docs/elements/vault/rotation.mdx +15 -0
  27. package/site/content/docs/plugins/otp.mdx +31 -31
  28. package/site/content/docs/recipes/dragonfly.mdx +5 -5
  29. package/site/content/docs/recipes/index.mdx +6 -6
  30. package/site/content/docs/recipes/mailpit.mdx +1 -1
  31. package/site/content/docs/recipes/meilisearch.mdx +1 -1
  32. package/site/content/docs/recipes/pgdog.mdx +1 -1
  33. package/site/content/docs/recipes/postgres.mdx +12 -8
  34. package/site/content/docs/recipes/redis.mdx +4 -4
  35. package/site/content/docs/reference/cli.mdx +2 -1
  36. package/site/content/docs/reference/configuration.mdx +11 -9
  37. package/site/content/docs/reference/errors.mdx +12 -2
  38. package/site/content/docs/reference/fx.mdx +35 -21
  39. package/site/content/docs/reference/idempotency.mdx +192 -0
  40. package/site/content/docs/reference/index.mdx +6 -1
  41. package/site/content/docs/reference/meta.json +2 -1
  42. package/site/content/docs/understand/meta.json +1 -1
  43. package/site/content/docs/{elements/flow → understand}/routing.mdx +11 -10
  44. package/site/content/docs/understand/the-architecture.mdx +14 -12
  45. package/site/content/docs/understand/try-it.mdx +3 -3
  46. package/src/auth/api-key-sql.test.ts +44 -0
  47. package/src/auth/api-key-sql.ts +1 -0
  48. package/src/auth/api-keys.ts +30 -12
  49. package/src/auth/config.ts +6 -2
  50. package/src/auth/gate-auth.test.ts +20 -0
  51. package/src/cli/agents-md.test.ts +25 -0
  52. package/src/cli/ask-vault-gaps.test.ts +38 -1
  53. package/src/cli/ask-vault-gaps.ts +35 -2
  54. package/src/cli/competitor-mention-removal.test.ts +3 -3
  55. package/src/cli/decide.ts +174 -0
  56. package/src/cli/decision-lock-watch.test.ts +45 -0
  57. package/src/cli/decision-lock-watch.ts +51 -0
  58. package/src/cli/dev-app-runner.ts +34 -8
  59. package/src/cli/dev-auth-secret.test.ts +49 -0
  60. package/src/cli/dev-auth-secret.ts +49 -0
  61. package/src/cli/dev-hot.test.ts +41 -0
  62. package/src/cli/dev-hot.ts +48 -0
  63. package/src/cli/dev.test.ts +1 -1
  64. package/src/cli/dev.ts +24 -6
  65. package/src/cli/docker-cli.test.ts +1 -1
  66. package/src/cli/eval.ts +142 -1
  67. package/src/cli/index.ts +14 -1
  68. package/src/cli/load-config.images.test.ts +12 -12
  69. package/src/cli/load-config.ts +2 -2
  70. package/src/cli/mcp-from-console.ts +48 -0
  71. package/src/cli/registry.ts +35 -1
  72. package/src/cli/vault-cmd.test.ts +23 -0
  73. package/src/cli/vault-cmd.ts +7 -0
  74. package/src/client/agent.ts +307 -0
  75. package/src/client/create.ts +18 -1
  76. package/src/client/transport.test.ts +245 -0
  77. package/src/client/transport.ts +133 -19
  78. package/src/client/types.ts +29 -3
  79. package/src/client-react/index.ts +7 -1
  80. package/src/client-react/use-agent-run.test.ts +271 -0
  81. package/src/client-react/use-agent-run.ts +193 -0
  82. package/src/compiler/ai-approval.test.ts +36 -0
  83. package/src/compiler/ai-repair.test.ts +22 -0
  84. package/src/compiler/decisions.extract.test.ts +164 -0
  85. package/src/compiler/effects-infer.ts +753 -17
  86. package/src/compiler/extract.test.ts +37 -0
  87. package/src/compiler/extract.ts +381 -24
  88. package/src/compiler/fixtures/skyport.expected.json +24 -0
  89. package/src/compiler/fx-follow.test.ts +164 -0
  90. package/src/compiler/fx-index.ts +399 -0
  91. package/src/compiler/response.ts +35 -2
  92. package/src/config/index.ts +21 -3
  93. package/src/console/server/ai-runs-flows.ts +269 -0
  94. package/src/console/server/ai-runs.test.ts +323 -0
  95. package/src/console/server/ai-runs.ts +562 -0
  96. package/src/console/server/ai.ts +12 -1
  97. package/src/console/server/app.ts +10 -1
  98. package/src/console/server/decisions-flows.ts +157 -0
  99. package/src/console/server/decisions.test.ts +66 -0
  100. package/src/console/server/decisions.ts +229 -0
  101. package/src/console/server/flows-invoke.test.ts +2 -2
  102. package/src/console/server/flows.ts +50 -2
  103. package/src/console/server/invoke-user-flow.ts +9 -0
  104. package/src/console/server/runs-ingest.ts +16 -0
  105. package/src/console/server/serve.ts +5 -0
  106. package/src/console/server/state.ts +25 -1
  107. package/src/console/server/store-stats.test.ts +47 -0
  108. package/src/console/server/store-stats.ts +92 -18
  109. package/src/console/ui-next/dist/assets/access-page-DDKkhT9v.js +4 -0
  110. package/src/console/ui-next/dist/assets/agent-disclosure-62Xts8Xi.js +1 -0
  111. package/src/console/ui-next/dist/assets/{cache-glyph-92uM5MO7.js → cache-glyph-DyeoKHKA.js} +1 -1
  112. package/src/console/ui-next/dist/assets/{call-pii-button-DtGVPtcs.js → call-pii-button-DvfpLXsU.js} +1 -1
  113. package/src/console/ui-next/dist/assets/{collapsible-DN7l6zmC.js → collapsible-BY03SeCg.js} +1 -1
  114. package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
  115. package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
  116. package/src/console/ui-next/dist/assets/decisions-page-CgkKlA56.js +1 -0
  117. package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
  118. package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
  119. package/src/console/ui-next/dist/assets/duration-tone-Y6HLjyJ5.js +9 -0
  120. package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
  121. package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
  122. package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
  123. package/src/console/ui-next/dist/assets/flows-page-DLPfNy-_.js +1 -0
  124. package/src/console/ui-next/dist/assets/{highlighted-json-BlAEVgNW.js → highlighted-json-D7nNzpgB.js} +1 -1
  125. package/src/console/ui-next/dist/assets/{http-method-BDf7OAHv.js → http-method-DdL19zzh.js} +1 -1
  126. package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
  127. package/src/console/ui-next/dist/assets/index-C0lc_s9d.js +58 -0
  128. package/src/console/ui-next/dist/assets/observability-page-CRlcyLCC.js +8 -0
  129. package/src/console/ui-next/dist/assets/{replica-lag-B3GLNVfF.js → replica-lag-Bb3FWUu9.js} +7 -7
  130. package/src/console/ui-next/dist/assets/request-meta-DYBMWhX8.js +1 -0
  131. package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
  132. package/src/console/ui-next/dist/assets/store-page-CcE-SXC8.js +41 -0
  133. package/src/console/ui-next/dist/assets/trace-detail-sheet-CKMSEQ4s.js +2 -0
  134. package/src/console/ui-next/dist/assets/tree-expand-toggle-QOfk0M0H.js +55 -0
  135. package/src/console/ui-next/dist/assets/units-page-DKC1CDDd.js +1 -0
  136. package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
  137. package/src/console/ui-next/dist/assets/vault-page-B54v4Yit.js +2 -0
  138. package/src/console/ui-next/dist/index.html +4 -3
  139. package/src/console/ui-next/seed-parked-approval.ts +126 -0
  140. package/src/console/ui-next/src/client.ts +224 -1
  141. package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
  142. package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
  143. package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
  144. package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
  145. package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
  146. package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
  147. package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
  148. package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
  149. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
  150. package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
  151. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
  152. package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
  153. package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
  154. package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
  155. package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
  156. package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
  157. package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
  158. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
  159. package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
  160. package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
  161. package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
  162. package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
  163. package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
  164. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
  165. package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
  166. package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
  167. package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
  168. package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
  169. package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
  170. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
  171. package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +15 -2
  172. package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
  173. package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
  174. package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
  175. package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
  176. package/src/console/ui-next/src/router.tsx +17 -0
  177. package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
  178. package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
  179. package/src/console/ui-next/vite.config.ts +9 -0
  180. package/src/docker/docker.test.ts +13 -13
  181. package/src/docker/images-config.test.ts +12 -12
  182. package/src/docker/recipes/dragonfly.ts +3 -2
  183. package/src/docker/recipes/redis.ts +1 -1
  184. package/src/docker/stack-id.test.ts +1 -1
  185. package/src/drivers/ai-anthropic.ts +212 -21
  186. package/src/drivers/ai-mock.ts +34 -1
  187. package/src/drivers/ai-openai-compatible.ts +67 -26
  188. package/src/drivers/ai-preconnect.ts +18 -0
  189. package/src/drivers/ai-stream.test.ts +62 -0
  190. package/src/drivers/ai-types.ts +20 -0
  191. package/src/drivers/journal-postgres.ts +455 -3
  192. package/src/drivers/meilisearch.ts +2 -0
  193. package/src/drivers/memory-ddl.test.ts +18 -0
  194. package/src/drivers/memory.ts +21 -1
  195. package/src/drivers/pg-rls.test.ts +31 -0
  196. package/src/drivers/pg-rls.ts +15 -0
  197. package/src/drivers/postgres.test.ts +9 -0
  198. package/src/drivers/postgres.ts +49 -11
  199. package/src/drivers/vault-builtin.test.ts +10 -0
  200. package/src/drivers/vault-builtin.ts +13 -1
  201. package/src/drivers/vault-remote-bag.ts +5 -1
  202. package/src/drivers/vault-types.ts +6 -1
  203. package/src/elements/ai/agent-event-slot.ts +27 -0
  204. package/src/elements/ai/approval-http.ts +207 -0
  205. package/src/elements/ai/approval.test.ts +741 -0
  206. package/src/elements/ai/approval.ts +228 -0
  207. package/src/elements/ai/decisions/bind.ts +163 -0
  208. package/src/elements/ai/decisions/certificate.test.ts +291 -0
  209. package/src/elements/ai/decisions/certificate.ts +377 -0
  210. package/src/elements/ai/decisions/certify.ts +314 -0
  211. package/src/elements/ai/decisions/decide.test.ts +953 -0
  212. package/src/elements/ai/decisions/e2e.test.ts +399 -0
  213. package/src/elements/ai/decisions/export.test.ts +120 -0
  214. package/src/elements/ai/decisions/export.ts +146 -0
  215. package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
  216. package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
  217. package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
  218. package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
  219. package/src/elements/ai/decisions/http.test.ts +154 -0
  220. package/src/elements/ai/decisions/http.ts +240 -0
  221. package/src/elements/ai/decisions/labels.test.ts +282 -0
  222. package/src/elements/ai/decisions/labels.ts +257 -0
  223. package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
  224. package/src/elements/ai/decisions/openrouter.ts +36 -0
  225. package/src/elements/ai/decisions/provider.ts +102 -0
  226. package/src/elements/ai/decisions/typesafe.ts +36 -0
  227. package/src/elements/ai/declare.ts +252 -1
  228. package/src/elements/ai/events.test.ts +379 -0
  229. package/src/elements/ai/events.ts +138 -0
  230. package/src/elements/ai/run-events.test.ts +461 -0
  231. package/src/elements/ai/run-events.ts +522 -0
  232. package/src/elements/ai/runtime.ts +1186 -105
  233. package/src/elements/ai/stream-turn.ts +214 -0
  234. package/src/elements/ai/stream.live.test.ts +76 -0
  235. package/src/elements/ai/subagent.test.ts +136 -0
  236. package/src/elements/ai.test.ts +497 -3
  237. package/src/elements/ai.ts +16 -1
  238. package/src/elements/clock/durable.ts +27 -1
  239. package/src/elements/store/emit-drizzle.ts +2 -2
  240. package/src/elements/store/live-default.test.ts +2 -0
  241. package/src/elements/store/live-http.test.ts +1 -0
  242. package/src/elements/store/resource-list-docs.test.ts +2 -2
  243. package/src/elements/store/resource.test.ts +3 -3
  244. package/src/elements/store/schema-decl.test.ts +1 -0
  245. package/src/elements/store/sql-condition.ts +24 -7
  246. package/src/elements/store/sql-session.test.ts +51 -0
  247. package/src/elements/store/sql-session.ts +111 -1
  248. package/src/elements/store/upsert-app.test.ts +1 -1
  249. package/src/elements/vault/audit.test.ts +137 -0
  250. package/src/elements/vault/audit.ts +106 -13
  251. package/src/elements/vault/boot-chain.ts +9 -1
  252. package/src/elements/vault/builtin-adapter.ts +33 -3
  253. package/src/i18n/catalogs/ar.ts +9 -0
  254. package/src/i18n/catalogs/en.ts +9 -0
  255. package/src/index.ts +2 -0
  256. package/src/kernel/abort-scope.ts +33 -2
  257. package/src/kernel/agent-event-store.ts +448 -0
  258. package/src/kernel/app.ts +294 -19
  259. package/src/kernel/auto-cache.test.ts +6 -6
  260. package/src/kernel/boot-bind/ai.ts +3 -0
  261. package/src/kernel/boot-bind/store.test.ts +26 -0
  262. package/src/kernel/boot-bind/store.ts +82 -6
  263. package/src/kernel/boot-bind/vault.ts +1 -0
  264. package/src/kernel/boot.ts +34 -5
  265. package/src/kernel/builtin-errors.ts +10 -0
  266. package/src/kernel/capability.ts +3 -0
  267. package/src/kernel/client-descriptor.ts +1 -0
  268. package/src/kernel/concurrency.test.ts +33 -0
  269. package/src/kernel/decision-label-store.ts +441 -0
  270. package/src/kernel/effects-stamping.test.ts +2 -2
  271. package/src/kernel/effects.test.ts +2 -1
  272. package/src/kernel/effects.ts +13 -1
  273. package/src/kernel/element-registries.ts +3 -0
  274. package/src/kernel/errors-compiler.ts +20 -0
  275. package/src/kernel/errors-text.ts +4 -0
  276. package/src/kernel/errors.ts +2 -0
  277. package/src/kernel/external-effects.test.ts +36 -0
  278. package/src/kernel/flow.test.ts +2 -3
  279. package/src/kernel/flow.ts +24 -0
  280. package/src/kernel/fx-decide.ts +761 -0
  281. package/src/kernel/fx-fetch.ts +5 -1
  282. package/src/kernel/fx.test.ts +12 -5
  283. package/src/kernel/fx.ts +316 -29
  284. package/src/kernel/http-frame.test.ts +94 -0
  285. package/src/kernel/http-frame.ts +203 -0
  286. package/src/kernel/idempotency-store.ts +578 -0
  287. package/src/kernel/idempotency.test.ts +556 -0
  288. package/src/kernel/idempotency.ts +304 -0
  289. package/src/kernel/index.ts +1 -0
  290. package/src/kernel/journal.ts +160 -0
  291. package/src/kernel/json-result.ts +10 -0
  292. package/src/kernel/pipeline.test.ts +45 -0
  293. package/src/kernel/sse-id.ts +32 -0
  294. package/src/manifest/diff.ts +1 -0
  295. package/src/manifest/types.ts +37 -2
  296. package/src/mcp/ai-tools.test.ts +202 -0
  297. package/src/mcp/authorization.ts +66 -0
  298. package/src/mcp/session.ts +3 -0
  299. package/src/mcp/tools.ts +168 -0
  300. package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
  301. package/src/plugins/otp.test.ts +65 -1
  302. package/src/plugins/otp.ts +59 -7
  303. package/src/release/absolute-regression.test.ts +63 -0
  304. package/src/release/http-graph.test.ts +27 -0
  305. package/src/release/http-graph.ts +95 -0
  306. package/src/release/index.ts +3 -0
  307. package/src/release/limits.ts +17 -2
  308. package/src/release/measure.ts +144 -28
  309. package/src/runs/collect.ts +4 -1
  310. package/src/runs/parquet.test.ts +27 -1
  311. package/src/runs/parquet.ts +19 -0
  312. package/src/runs/types.ts +31 -0
  313. package/src/runtime/json-code-block.test.ts +106 -47
  314. package/src/runtime/json-code-block.ts +1209 -46
  315. package/src/term.test.ts +13 -0
  316. package/src/term.ts +81 -49
  317. package/src/test/create-test-app.test.ts +3 -1
  318. package/src/test/create-test-app.ts +40 -0
  319. package/src/test/live-signals.test.ts +1 -0
  320. package/src/test/provisions.integration.test.ts +1 -0
  321. package/src/test/tenant-isolation.test.ts +1 -0
  322. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
  323. package/src/console/ui-next/dist/assets/access-page-Bgt9bq2r.js +0 -4
  324. package/src/console/ui-next/dist/assets/agent-disclosure-B43CXZZR.js +0 -1
  325. package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +0 -1
  326. package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +0 -1
  327. package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +0 -1
  328. package/src/console/ui-next/dist/assets/duration-tone-BmIR9FV8.js +0 -9
  329. package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +0 -1
  330. package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +0 -1
  331. package/src/console/ui-next/dist/assets/flows-page-BMHs-IzK.js +0 -1
  332. package/src/console/ui-next/dist/assets/index-BKpaes3n.js +0 -63
  333. package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
  334. package/src/console/ui-next/dist/assets/observability-page-WnVLI-0j.js +0 -4
  335. package/src/console/ui-next/dist/assets/request-meta-DMbnAe3f.js +0 -1
  336. package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +0 -1
  337. package/src/console/ui-next/dist/assets/store-page-KvFDingJ.js +0 -41
  338. package/src/console/ui-next/dist/assets/trace-detail-sheet-Htk8Cm9t.js +0 -2
  339. package/src/console/ui-next/dist/assets/tree-expand-toggle-CFMPWX4f.js +0 -55
  340. package/src/console/ui-next/dist/assets/units-page-C4NdNuxP.js +0 -1
  341. package/src/console/ui-next/dist/assets/use-vault-list-CT4-gajj.js +0 -1
  342. package/src/console/ui-next/dist/assets/vault-page-CBYl0LW_.js +0 -2
@@ -0,0 +1,399 @@
1
+ /**
2
+ * Decision autonomy from certify through boot, review, and promote.
3
+ * The app restarts between steps. Labels and the drift flag stay on disk.
4
+ */
5
+
6
+ import { afterEach, describe, expect, test } from "bun:test";
7
+ import { mkdtemp } from "node:fs/promises";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { ai, resetAiDecls } from "../../ai.ts";
11
+ import { runOkeCertify } from "../../../cli/eval.ts";
12
+ import { promoteDecision } from "../../../cli/decide.ts";
13
+ import { gate, resetGates } from "../../gate/declare.ts";
14
+ import {
15
+ decisionDriftSuspended,
16
+ getDecisionLock,
17
+ loadDecisionLockfile,
18
+ resetDecisionCertificates,
19
+ } from "./certificate.ts";
20
+ import {
21
+ closeDecisionLabelStore,
22
+ flushDecisionLabels,
23
+ loadDecisionLabels,
24
+ persistDecisionLabel,
25
+ } from "./labels.ts";
26
+ import { oke } from "../../../kernel/app.ts";
27
+ import { flow } from "../../../kernel/flow.ts";
28
+ import { signal } from "../../signal/declare.ts";
29
+ import { resetBindings } from "../../../kernel/on.ts";
30
+ import { http } from "../../../kernel/triggers.ts";
31
+ import { extractFromSources } from "../../../compiler/extract.ts";
32
+ import type { Manifest } from "../../../manifest/types.ts";
33
+ import { resetDecisionProvider, setDecisionProvider } from "../../../kernel/fx-decide.ts";
34
+ import { DECISION_DRIFT_SIGNAL, decisionOperatorGate } from "./bind.ts";
35
+ import { consoleOperatorGate } from "../../../console/server/console-gates.ts";
36
+ import { decisionConsoleBindings } from "../../../console/server/decisions-flows.ts";
37
+ import type { ConsoleState } from "../../../console/server/state.ts";
38
+ import {
39
+ createPostgresJournalFake,
40
+ createPostgresJournalStore,
41
+ } from "../../../drivers/journal-postgres.ts";
42
+
43
+ afterEach(() => {
44
+ closeDecisionLabelStore();
45
+ resetBindings();
46
+ resetAiDecls();
47
+ resetGates();
48
+ resetDecisionCertificates();
49
+ resetDecisionProvider();
50
+ });
51
+
52
+ function providerBody() {
53
+ return {
54
+ model: "typesafe/jev-1.13.0",
55
+ provider: "openrouter",
56
+ answers: {
57
+ team: {
58
+ type: "choice" as const,
59
+ choice: "technical",
60
+ probabilities: { billing: 0.05, technical: 0.93, none_of_these: 0.02 },
61
+ },
62
+ },
63
+ usage: {},
64
+ };
65
+ }
66
+
67
+ describe("fx.decide end to end", () => {
68
+ test("boot without a project root fails when decisions are declared", async () => {
69
+ const previousRoot = process.env["OKE_ROOT_DIR"];
70
+ delete process.env["OKE_ROOT_DIR"];
71
+ const manifest = await extractFromSources({
72
+ "src/flows/run.ts": `
73
+ ai.decision("triage", {
74
+ onUncertain: "abstain",
75
+ ask: { team: ai.choice("which", { a: "A" }) },
76
+ });
77
+ `,
78
+ });
79
+ const app = oke({
80
+ name: "decide-no-root",
81
+ env: "test",
82
+ startScheduler: false,
83
+ registry: "ignore",
84
+ manifest,
85
+ });
86
+ try {
87
+ await expect(app.boot({ env: "test" })).rejects.toThrow(/rootDir/);
88
+ } finally {
89
+ if (previousRoot === undefined) delete process.env["OKE_ROOT_DIR"];
90
+ else process.env["OKE_ROOT_DIR"] = previousRoot;
91
+ }
92
+ });
93
+
94
+ test("certify, review, drift, and promote use the real routes", async () => {
95
+ const root = await mkdtemp(join(tmpdir(), "oke-decide-e2e-"));
96
+ const question = ai.choice("which team", { billing: "Billing", technical: "Technical" });
97
+ const ship = ai.decision("ship", {
98
+ onUncertain: "abstain",
99
+ model: "typesafe/jev-1.13.0",
100
+ autonomy: { maxError: 0.05, audit: 0 },
101
+ evals: join(root, "ship.jsonl"),
102
+ ask: { team: question },
103
+ });
104
+ const ops = gate.policy("ops", (ctx) => ctx.operator.id === "reviewer");
105
+ const triage = ai.decision("triage", {
106
+ review: "ops",
107
+ model: "typesafe/jev-1.13.0",
108
+ autonomy: { maxError: 0.05, audit: 0 },
109
+ ask: { team: question },
110
+ });
111
+ const route = ai.decision("route", {
112
+ review: "ops",
113
+ model: "typesafe/jev-1.13.0",
114
+ autonomy: { maxError: 0.05, audit: 0 },
115
+ ask: { team: question },
116
+ });
117
+ const seed = Array.from({ length: 200 }, (_, i) =>
118
+ JSON.stringify({
119
+ input: { ticket: `T-${i}`, note: `Customer ${i} cannot sign in after the deploy` },
120
+ expect: { team: "technical" },
121
+ }),
122
+ ).join("\n");
123
+ let certifyCalls = 0;
124
+ await Bun.write(join(root, "ship.jsonl"), seed);
125
+ const evals = JSON.stringify(join(root, "ship.jsonl"));
126
+ const manifest = (await extractFromSources({
127
+ "src/flows/support/route.ts": `
128
+ const ops = gate.policy("ops", () => true);
129
+ ai.decision("ship", {
130
+ onUncertain: "abstain",
131
+ model: "typesafe/jev-1.13.0",
132
+ autonomy: { maxError: 0.05, audit: 0 },
133
+ evals: ${evals},
134
+ ask: { team: ai.choice("which team", { billing: "Billing", technical: "Technical" }) },
135
+ });
136
+ ai.decision("triage", {
137
+ review: ops,
138
+ model: "typesafe/jev-1.13.0",
139
+ autonomy: { maxError: 0.05, audit: 0 },
140
+ ask: { team: ai.choice("which team", { billing: "Billing", technical: "Technical" }) },
141
+ });
142
+ ai.decision("route", {
143
+ review: ops,
144
+ model: "typesafe/jev-1.13.0",
145
+ autonomy: { maxError: 0.05, audit: 0 },
146
+ ask: { team: ai.choice("which team", { billing: "Billing", technical: "Technical" }) },
147
+ });
148
+ `,
149
+ })) as Manifest;
150
+ expect(manifest.ai?.decisions?.ship?.evals).toBe(join(root, "ship.jsonl"));
151
+ expect(manifest.ai?.decisions?.ship?.autonomy).toEqual({ maxError: 0.05, audit: 0 });
152
+ const code = await runOkeCertify({
153
+ root,
154
+ manifest,
155
+ evaluate: async () => {
156
+ const technical = 0.62 + (certifyCalls++ % 30) / 100;
157
+ return {
158
+ ...providerBody(),
159
+ answers: {
160
+ team: {
161
+ type: "choice" as const,
162
+ choice: "technical",
163
+ probabilities: {
164
+ billing: Number((1 - technical - 0.02).toFixed(4)),
165
+ technical: Number(technical.toFixed(4)),
166
+ none_of_these: 0.02,
167
+ },
168
+ },
169
+ },
170
+ };
171
+ },
172
+ });
173
+ expect(code).toBe(0);
174
+
175
+ setDecisionProvider(async () => providerBody());
176
+ const sql = createPostgresJournalFake();
177
+ const store = await createPostgresJournalStore({ sql });
178
+ const consoleState = { journalStore: store, manifest } as unknown as ConsoleState;
179
+ const resolve = decisionConsoleBindings(consoleState).find(
180
+ (binding) => binding.flow.name === "console.decisions.resolve",
181
+ );
182
+ if (!resolve) throw new Error("expected the console resolve route");
183
+ const work = flow("work", {
184
+ durable: true,
185
+ effects: { decides: ["ship", "triage", "route"] },
186
+ do: async (input: { which: "ship" | "triage" | "route" }, fx) =>
187
+ fx.decide(input.which === "ship" ? ship : input.which === "route" ? route : triage, {
188
+ ticket: "1",
189
+ }),
190
+ });
191
+
192
+ const locked = oke({
193
+ name: "decide-e2e",
194
+ env: "test",
195
+ startScheduler: false,
196
+ registry: "ignore",
197
+ manifest,
198
+ rootDir: root,
199
+ signals: [
200
+ signal.once(DECISION_DRIFT_SIGNAL, { optional: true, retries: 0, deadLetter: false }),
201
+ ],
202
+ fx: { operator: { id: "reviewer" } },
203
+ elements: { journal: { store, instanceId: "e2e", leaseMs: 30_000, driverId: "memory" } },
204
+ gate: {
205
+ unguardedHttp: "allow",
206
+ policies: [consoleOperatorGate, decisionOperatorGate, ops],
207
+ },
208
+ bindings: [resolve, { trigger: http.post("/work"), flow: work }],
209
+ });
210
+ await locked.boot({ env: "test" });
211
+ const auto = (await locked.call(work, { which: "ship" })) as {
212
+ team: string;
213
+ $: { team: { how: string } };
214
+ };
215
+ expect(auto.team).toBe("technical");
216
+ expect(auto.$.team.how).toBe("auto");
217
+ expect(getDecisionLock()?.decisions.ship?.model).toBe("typesafe/jev-1.13.0");
218
+
219
+ await locked.call(work, { which: "triage" });
220
+ const run = (await store.list()).find((item) =>
221
+ item.entries.some((entry) => entry.kind === "step" && entry.name.startsWith("ai-decision:")),
222
+ );
223
+ const step = run?.entries.find(
224
+ (entry) => entry.kind === "step" && entry.name.startsWith("ai-decision:"),
225
+ );
226
+ if (!run || !step || step.kind !== "step") throw new Error("expected a parked review");
227
+ const review = await locked.fetch(
228
+ new Request("http://localhost/console/decisions/resolve", {
229
+ method: "POST",
230
+ headers: { "content-type": "application/json" },
231
+ body: JSON.stringify({
232
+ id: step.name.slice("ai-decision:".length),
233
+ values: { team: "billing" },
234
+ }),
235
+ }),
236
+ );
237
+ expect(review.status).toBe(200);
238
+ await flushDecisionLabels();
239
+ await locked.resumeDurable(Date.now() + 1);
240
+ const finished = await store.get(run.id);
241
+ const output = finished?.output as { team: string; $: { team: { how: string } } } | undefined;
242
+ expect(output?.team).toBe("billing");
243
+ expect(output?.$.team.how).toBe("reviewed");
244
+
245
+ const now = Date.now();
246
+ for (let i = 0; i < 40; i++) {
247
+ persistDecisionLabel({
248
+ decision: "ship",
249
+ question: "team",
250
+ value: "billing",
251
+ propensity: 0.1,
252
+ reviewer: "audit",
253
+ model: "typesafe/jev-1.13.0",
254
+ loss: 1,
255
+ score: 0.9,
256
+ at: now,
257
+ });
258
+ }
259
+ await flushDecisionLabels();
260
+ const drift = locked.flow("oke.decisions.drift");
261
+ if (!drift) throw new Error("expected the drift monitor");
262
+ await locked.call(drift, {});
263
+ expect(decisionDriftSuspended("ship")).toBe(true);
264
+ expect(decisionDriftSuspended("triage")).toBe(false);
265
+
266
+ const aggregate = locked.flow("oke.decisions.aggregate");
267
+ if (!aggregate) throw new Error("expected the candidate job");
268
+ await locked.call(aggregate, {});
269
+ const thin = await locked.fetch(
270
+ new Request("http://localhost/_oke/decisions/triage/candidate"),
271
+ );
272
+ expect(thin.status).toBe(200);
273
+ const thinBody = (await thin.json()) as {
274
+ data?: { questions?: Record<string, unknown> };
275
+ questions?: Record<string, unknown>;
276
+ };
277
+ const thinQuestions = thinBody.data?.questions ?? thinBody.questions ?? {};
278
+ expect(thinQuestions.team).toBeUndefined();
279
+ await locked.call(work, { which: "triage" });
280
+ const still = (await store.list()).filter((item) =>
281
+ item.entries.some(
282
+ (entry) =>
283
+ entry.kind === "step" &&
284
+ entry.name.startsWith("ai-decision:") &&
285
+ (entry.value as { status?: string }).status === "pending",
286
+ ),
287
+ );
288
+ expect(still.length).toBeGreaterThan(0);
289
+
290
+ for (let i = 0; i < 160; i++) {
291
+ await locked.call(work, { which: "route" });
292
+ const parked = (await store.list()).find((item) =>
293
+ item.entries.some(
294
+ (entry) =>
295
+ entry.kind === "step" &&
296
+ entry.name.startsWith("ai-decision:") &&
297
+ (entry.value as { status?: string }).status === "pending",
298
+ ),
299
+ );
300
+ const parkedStep = parked?.entries.find(
301
+ (entry) =>
302
+ entry.kind === "step" &&
303
+ entry.name.startsWith("ai-decision:") &&
304
+ (entry.value as { status?: string }).status === "pending",
305
+ );
306
+ if (!parked || !parkedStep || parkedStep.kind !== "step") {
307
+ throw new Error("expected a parked route review");
308
+ }
309
+ const agreed = await locked.fetch(
310
+ new Request("http://localhost/console/decisions/resolve", {
311
+ method: "POST",
312
+ headers: { "content-type": "application/json" },
313
+ body: JSON.stringify({
314
+ id: parkedStep.name.slice("ai-decision:".length),
315
+ values: { team: "technical" },
316
+ }),
317
+ }),
318
+ );
319
+ expect(agreed.status).toBe(200);
320
+ await flushDecisionLabels();
321
+ await locked.resumeDurable(Date.now() + 1);
322
+ }
323
+
324
+ await locked.call(aggregate, {});
325
+ const candidate = await locked.fetch(
326
+ new Request("http://localhost/_oke/decisions/route/candidate"),
327
+ );
328
+ expect(candidate.status).toBe(200);
329
+ const candidateBody = (await candidate.json()) as {
330
+ data?: { questions?: { team?: Record<string, unknown> } };
331
+ questions?: { team?: Record<string, unknown> };
332
+ };
333
+ const routeQuestions = candidateBody.data?.questions ?? candidateBody.questions;
334
+ expect(routeQuestions?.team?.[""]).toBeDefined();
335
+ const promoted = await promoteDecision({
336
+ name: "route",
337
+ origin: "http://localhost",
338
+ lockPath: join(root, "oke-decisions.lock.json"),
339
+ fetcher: async (url) => {
340
+ const res = await locked.fetch(new Request(url));
341
+ const body = (await res.json()) as { data?: unknown };
342
+ return new Response(JSON.stringify(body.data ?? body), { status: res.status });
343
+ },
344
+ });
345
+ expect(promoted.decisions.ship).toBeDefined();
346
+ expect(promoted.decisions.route?.questions.team?.[""]).toBeDefined();
347
+ expect(decisionDriftSuspended("route")).toBe(false);
348
+ expect(decisionDriftSuspended("ship")).toBe(true);
349
+ await loadDecisionLockfile(root);
350
+ const after = (await locked.call(work, { which: "route" })) as {
351
+ team: string;
352
+ $: { team: { how: string } };
353
+ };
354
+ expect(after.team).toBe("technical");
355
+ expect(after.$.team.how).toBe("auto");
356
+ await locked.bootResult?.close();
357
+
358
+ await flushDecisionLabels();
359
+ closeDecisionLabelStore();
360
+ resetDecisionCertificates();
361
+ const restartedStore = await createPostgresJournalStore({ sql });
362
+ const restarted = oke({
363
+ name: "decide-e2e-2",
364
+ env: "test",
365
+ startScheduler: false,
366
+ registry: "ignore",
367
+ manifest,
368
+ rootDir: root,
369
+ fx: { operator: { id: "reviewer" } },
370
+ signals: [
371
+ signal.once(DECISION_DRIFT_SIGNAL, { optional: true, retries: 0, deadLetter: false }),
372
+ ],
373
+ gate: {
374
+ unguardedHttp: "allow",
375
+ policies: [consoleOperatorGate, decisionOperatorGate, ops],
376
+ },
377
+ elements: {
378
+ journal: {
379
+ store: restartedStore,
380
+ instanceId: "e2e-2",
381
+ leaseMs: 30_000,
382
+ driverId: "memory",
383
+ },
384
+ },
385
+ bindings: [{ trigger: http.post("/work"), flow: work }],
386
+ });
387
+ await restarted.boot({ env: "test" });
388
+ expect((await loadDecisionLabels("route")).length).toBeGreaterThan(0);
389
+ expect(getDecisionLock()?.decisions.route?.questions.team?.[""]).toBeDefined();
390
+ expect(decisionDriftSuspended("ship")).toBe(true);
391
+ expect(decisionDriftSuspended("route")).toBe(false);
392
+ const restartedAuto = (await restarted.call(work, { which: "route" })) as {
393
+ team: string;
394
+ $: { team: { how: string } };
395
+ };
396
+ expect(restartedAuto.$.team.how).toBe("auto");
397
+ await restarted.bootResult?.close();
398
+ }, 60_000);
399
+ });
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Label export shape and tenant rejection.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { mkdtemp } from "node:fs/promises";
7
+ import { tmpdir } from "node:os";
8
+ import { join } from "node:path";
9
+ import { z } from "zod";
10
+ import { Redacted } from "../../../kernel/redacted.ts";
11
+ import { exportDecisionLabelsFile } from "../../../cli/decide.ts";
12
+ import { DECISION_EXPORT_WARNING, decisionExportFields, exportDecisionLabels } from "./export.ts";
13
+
14
+ const schema = z.object({
15
+ ticket: z.string(),
16
+ token: z.string().describe("secret"),
17
+ note: z.string().describe("redacted"),
18
+ });
19
+
20
+ describe("decision label export", () => {
21
+ test("keeps declared fields, masks secret markers, and groups expect", () => {
22
+ const fields = decisionExportFields(schema);
23
+ expect(fields.fields).toEqual(["ticket", "token", "note"]);
24
+ expect(fields.secretFields).toEqual(["token", "note"]);
25
+ const exported = exportDecisionLabels({
26
+ callerTenant: "acme",
27
+ fields,
28
+ labels: [
29
+ {
30
+ decision: "triage",
31
+ question: "team",
32
+ value: "billing",
33
+ propensity: 1,
34
+ reviewer: "ops",
35
+ tenant: "acme",
36
+ locale: "en",
37
+ at: 10,
38
+ input: { ticket: "14", token: "sekret", note: Redacted.of("hidden"), extra: "nope" },
39
+ },
40
+ {
41
+ decision: "triage",
42
+ question: "rank",
43
+ value: "high",
44
+ propensity: 1,
45
+ reviewer: "ops",
46
+ tenant: "acme",
47
+ locale: "en",
48
+ at: 10,
49
+ input: { ticket: "14", token: "sekret" },
50
+ },
51
+ {
52
+ decision: "triage",
53
+ question: "team",
54
+ value: "technical",
55
+ propensity: 1,
56
+ reviewer: "ops",
57
+ tenant: "globex",
58
+ at: 11,
59
+ input: { ticket: "other" },
60
+ },
61
+ ],
62
+ });
63
+ expect(exported.ok).toBe(true);
64
+ if (!exported.ok) return;
65
+ const lines = exported.lines
66
+ .trim()
67
+ .split("\n")
68
+ .map(
69
+ (line) =>
70
+ JSON.parse(line) as {
71
+ input: Record<string, unknown>;
72
+ expect: Record<string, unknown>;
73
+ locale?: string;
74
+ },
75
+ );
76
+ expect(lines).toEqual([
77
+ {
78
+ input: { ticket: "14", token: "[redacted]", note: "[redacted]" },
79
+ expect: { team: "billing", rank: "high" },
80
+ locale: "en",
81
+ },
82
+ ]);
83
+ });
84
+
85
+ test("a different tenant is rejected", () => {
86
+ const exported = exportDecisionLabels({
87
+ callerTenant: "acme",
88
+ requestedTenant: "globex",
89
+ fields: { fields: ["ticket"], secretFields: [] },
90
+ labels: [],
91
+ });
92
+ expect(exported).toEqual({ ok: false, status: 404 });
93
+ });
94
+
95
+ test("the command prints that the file contains production data", async () => {
96
+ const root = await mkdtemp(join(tmpdir(), "oke-export-"));
97
+ const out = join(root, "labels.jsonl");
98
+ const printed: string[] = [];
99
+ await exportDecisionLabelsFile({
100
+ name: "triage",
101
+ origin: "http://127.0.0.1:6530",
102
+ out,
103
+ write: (text) => {
104
+ printed.push(text);
105
+ },
106
+ fetcher: async () =>
107
+ new Response(
108
+ JSON.stringify({
109
+ lines: `${JSON.stringify({ input: { ticket: "14" }, expect: { team: "billing" } })}\n`,
110
+ }),
111
+ {
112
+ status: 200,
113
+ headers: { "content-type": "application/json" },
114
+ },
115
+ ),
116
+ });
117
+ expect(printed).toEqual([`${DECISION_EXPORT_WARNING}\n`]);
118
+ expect(await Bun.file(out).text()).toContain('"ticket":"14"');
119
+ });
120
+ });
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Export reviewed labels as certify seed JSONL.
3
+ * Only the decision's declared `in` fields are written.
4
+ */
5
+
6
+ import { maskRedactedDeep, REDACTED_PLACEHOLDER } from "../../../kernel/redacted.ts";
7
+ import type { DecisionLabel } from "./certificate.ts";
8
+
9
+ /** Fields the export may copy out of a stored input. */
10
+ export interface DecisionExportFields {
11
+ readonly fields: readonly string[];
12
+ readonly secretFields: readonly string[];
13
+ }
14
+
15
+ /** One exported seed line. */
16
+ export interface DecisionExportLine {
17
+ readonly input: Readonly<Record<string, unknown>>;
18
+ readonly expect: Readonly<Record<string, unknown>>;
19
+ readonly locale?: string;
20
+ }
21
+
22
+ /**
23
+ * Read declared `in` keys. A field marked secret or redacted is masked.
24
+ *
25
+ * @param schema - Zod object from `ai.decision({ in })`, when one was declared
26
+ */
27
+ export function decisionExportFields(schema: unknown): DecisionExportFields {
28
+ const shape = zodShape(schema);
29
+ const fields = Object.keys(shape);
30
+ const secretFields = fields.filter((name) => secretMarked(shape[name]));
31
+ return { fields, secretFields };
32
+ }
33
+
34
+ /**
35
+ * Group labels that share a write time into seed rows.
36
+ * A requested tenant that is not the caller's is rejected.
37
+ *
38
+ * @param options - Labels, the caller's tenant, and the declared fields
39
+ */
40
+ export function exportDecisionLabels(options: {
41
+ readonly labels: readonly DecisionLabel[];
42
+ readonly callerTenant: string | null;
43
+ readonly requestedTenant?: string | null;
44
+ readonly fields: DecisionExportFields;
45
+ }): { ok: true; lines: string } | { ok: false; status: 404 } {
46
+ if (
47
+ options.requestedTenant != null &&
48
+ options.requestedTenant !== (options.callerTenant ?? null)
49
+ ) {
50
+ return { ok: false, status: 404 };
51
+ }
52
+ const tenant = options.callerTenant;
53
+ const kept = options.labels.filter((label) => (label.tenant ?? null) === tenant);
54
+ const groups = new Map<string, DecisionLabel[]>();
55
+ for (const label of kept) {
56
+ const key = label.reviewId ?? looseGroupKey(label, options.fields);
57
+ const group = groups.get(key);
58
+ if (group) group.push(label);
59
+ else groups.set(key, [label]);
60
+ }
61
+ const lines: string[] = [];
62
+ for (const group of groups.values()) {
63
+ const first = group[0];
64
+ if (!first) continue;
65
+ const row: DecisionExportLine = {
66
+ input: maskDecisionInput(first.input, options.fields),
67
+ expect: Object.fromEntries(group.map((label) => [label.question, label.value])),
68
+ ...(first.locale !== undefined ? { locale: first.locale } : {}),
69
+ };
70
+ lines.push(JSON.stringify(row));
71
+ }
72
+ return { ok: true, lines: lines.length > 0 ? `${lines.join("\n")}\n` : "" };
73
+ }
74
+
75
+ /**
76
+ * Labels with no review id group by decision, the non-secret input, reviewer, and time.
77
+ *
78
+ * @param label - Stored label
79
+ * @param fields - Declared `in` fields
80
+ */
81
+ function looseGroupKey(label: DecisionLabel, fields: DecisionExportFields): string {
82
+ const source =
83
+ label.input && typeof label.input === "object" && !Array.isArray(label.input)
84
+ ? (label.input as Record<string, unknown>)
85
+ : {};
86
+ const secret = new Set(fields.secretFields);
87
+ const picked: Record<string, unknown> = {};
88
+ for (const name of fields.fields) {
89
+ if (secret.has(name)) continue;
90
+ if (Object.prototype.hasOwnProperty.call(source, name)) picked[name] = source[name];
91
+ }
92
+ return `${label.decision}:${JSON.stringify(picked)}:${label.reviewer}:${label.at ?? 0}`;
93
+ }
94
+
95
+ /** Printed by `oke decide labels --export`. The file itself is only JSONL. */
96
+ export const DECISION_EXPORT_WARNING = "This file contains production data.";
97
+
98
+ /** Keep declared fields and replace secret or redacted values. */
99
+ export function maskDecisionInput(
100
+ input: unknown,
101
+ fields: DecisionExportFields,
102
+ ): Record<string, unknown> {
103
+ const source =
104
+ input && typeof input === "object" && !Array.isArray(input)
105
+ ? (input as Record<string, unknown>)
106
+ : {};
107
+ const secret = new Set(fields.secretFields);
108
+ const picked: Record<string, unknown> = {};
109
+ for (const name of fields.fields) {
110
+ if (!Object.prototype.hasOwnProperty.call(source, name)) continue;
111
+ picked[name] = secret.has(name) ? REDACTED_PLACEHOLDER : source[name];
112
+ }
113
+ return maskRedactedDeep(picked);
114
+ }
115
+
116
+ function zodShape(schema: unknown): Record<string, unknown> {
117
+ if (!schema || typeof schema !== "object") return {};
118
+ const shape = (schema as { shape?: unknown }).shape;
119
+ if (shape && typeof shape === "object" && !Array.isArray(shape)) {
120
+ return shape as Record<string, unknown>;
121
+ }
122
+ return {};
123
+ }
124
+
125
+ function secretMarked(field: unknown): boolean {
126
+ let current = field;
127
+ for (let depth = 0; depth < 8 && current && typeof current === "object"; depth++) {
128
+ if (marker(current)) return true;
129
+ const unwrap = (current as { unwrap?: () => unknown }).unwrap;
130
+ if (typeof unwrap !== "function") return false;
131
+ const next = unwrap.call(current);
132
+ if (next === current) return false;
133
+ current = next;
134
+ }
135
+ return false;
136
+ }
137
+
138
+ function marker(field: object): boolean {
139
+ const record = field as { description?: string; meta?: () => unknown };
140
+ if (record.description === "secret" || record.description === "redacted") return true;
141
+ if (typeof record.meta !== "function") return false;
142
+ const meta = record.meta();
143
+ if (!meta || typeof meta !== "object") return false;
144
+ const flags = meta as Record<string, unknown>;
145
+ return flags.secret === true || flags.redacted === true;
146
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "model": "typesafe/jev-1.13",
3
+ "state": { "ticket": "Checkout is blank after Pay." },
4
+ "questions": {
5
+ "team": {
6
+ "type": "choice",
7
+ "instructions": "Which team should own this ticket?",
8
+ "criteria": {
9
+ "billing": "Payments and refunds",
10
+ "technical": "Bugs and outages",
11
+ "none_of_these": "None of the listed options fit"
12
+ }
13
+ },
14
+ "severity": {
15
+ "type": "score",
16
+ "instructions": "How severe is this?",
17
+ "criteria": ["low", "medium", "high"]
18
+ },
19
+ "urgent": {
20
+ "type": "noul",
21
+ "instructions": "Does this need a person today?"
22
+ }
23
+ }
24
+ }