okengine 0.22.0 → 0.23.1
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.
- package/AGENTS.md +7 -0
- package/manifest.v1.schema.json +44 -0
- package/package.json +32 -19
- package/site/content/docs/ai/mcp.mdx +14 -10
- package/site/content/docs/ai/skills.mdx +1 -1
- package/site/content/docs/elements/ai/agents.mdx +61 -13
- package/site/content/docs/elements/ai/decide.mdx +536 -0
- package/site/content/docs/elements/ai/events.mdx +373 -0
- package/site/content/docs/elements/ai/index.mdx +9 -3
- package/site/content/docs/elements/ai/meta.json +1 -1
- package/site/content/docs/elements/ai/prompts.mdx +4 -2
- package/site/content/docs/elements/channel/index.mdx +4 -4
- package/site/content/docs/elements/channel/receipts.mdx +7 -5
- package/site/content/docs/elements/channel/sms.mdx +2 -1
- package/site/content/docs/elements/flow/index.mdx +22 -18
- package/site/content/docs/elements/gate/auth.mdx +4 -1
- package/site/content/docs/elements/signal/index.mdx +15 -14
- package/site/content/docs/elements/store/index.mdx +1 -1
- package/site/content/docs/elements/store/kv.mdx +5 -5
- package/site/content/docs/plugins/otp.mdx +3 -3
- package/site/content/docs/recipes/dragonfly.mdx +5 -5
- package/site/content/docs/recipes/index.mdx +6 -6
- package/site/content/docs/recipes/postgres.mdx +12 -8
- package/site/content/docs/recipes/redis.mdx +4 -4
- package/site/content/docs/reference/cli.mdx +2 -1
- package/site/content/docs/reference/configuration.mdx +9 -7
- package/site/content/docs/reference/fx.mdx +19 -10
- package/src/auth/api-key-sql.test.ts +44 -0
- package/src/auth/api-key-sql.ts +1 -0
- package/src/auth/api-keys.ts +30 -12
- package/src/auth/config.ts +6 -2
- package/src/auth/gate-auth.test.ts +20 -0
- package/src/auth/tenants.ts +17 -0
- package/src/cli/agents-md.test.ts +25 -0
- package/src/cli/ask-vault-gaps.test.ts +38 -1
- package/src/cli/ask-vault-gaps.ts +35 -2
- package/src/cli/decide.ts +174 -0
- package/src/cli/decision-lock-watch.test.ts +45 -0
- package/src/cli/decision-lock-watch.ts +51 -0
- package/src/cli/dev-app-runner.ts +34 -8
- package/src/cli/dev-auth-secret.test.ts +49 -0
- package/src/cli/dev-auth-secret.ts +49 -0
- package/src/cli/dev-hot.test.ts +41 -0
- package/src/cli/dev-hot.ts +48 -0
- package/src/cli/dev.test.ts +1 -1
- package/src/cli/dev.ts +24 -6
- package/src/cli/doctor-diff.test.ts +29 -1
- package/src/cli/doctor-diff.ts +35 -0
- package/src/cli/eval.ts +142 -1
- package/src/cli/index.ts +5 -0
- package/src/cli/load-config.images.test.ts +2 -2
- package/src/cli/load-config.ts +1 -1
- package/src/cli/manifest-pr-diff.ts +16 -0
- package/src/cli/mcp-from-console.ts +48 -0
- package/src/cli/registry.ts +35 -1
- package/src/client/agent.ts +307 -0
- package/src/client/transport.test.ts +20 -0
- package/src/client/transport.ts +2 -1
- package/src/client/types.ts +1 -1
- package/src/client-react/index.ts +7 -1
- package/src/client-react/use-agent-run.test.ts +271 -0
- package/src/client-react/use-agent-run.ts +193 -0
- package/src/compiler/ai-approval.test.ts +36 -0
- package/src/compiler/ai-repair.test.ts +22 -0
- package/src/compiler/decisions.extract.test.ts +164 -0
- package/src/compiler/effects-fetch.test.ts +77 -0
- package/src/compiler/effects-infer.ts +114 -17
- package/src/compiler/effects-join.test.ts +48 -0
- package/src/compiler/extract.test.ts +28 -0
- package/src/compiler/extract.ts +279 -13
- package/src/compiler/fx-index.ts +58 -4
- package/src/compiler/response.ts +35 -2
- package/src/config/index.ts +21 -3
- package/src/console/index.ts +1 -1
- package/src/console/server/ai-runs-flows.ts +269 -0
- package/src/console/server/ai-runs.test.ts +323 -0
- package/src/console/server/ai-runs.ts +562 -0
- package/src/console/server/ai.ts +12 -1
- package/src/console/server/app.ts +10 -1
- package/src/console/server/decisions-flows.ts +157 -0
- package/src/console/server/decisions.test.ts +66 -0
- package/src/console/server/decisions.ts +229 -0
- package/src/console/server/flows.ts +50 -2
- package/src/console/server/invoke-user-flow.ts +9 -0
- package/src/console/server/runs-ingest.ts +16 -0
- package/src/console/server/serve.ts +5 -0
- package/src/console/server/state.ts +25 -1
- package/src/console/server/store-stats.test.ts +47 -0
- package/src/console/server/store-stats.ts +92 -18
- package/src/console/ui-next/dist/assets/access-page-C_N4qvai.js +4 -0
- package/src/console/ui-next/dist/assets/{agent-disclosure-Bkohc_8X.js → agent-disclosure-Cq1TxniW.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-rL0lHxat.js → cache-glyph-BaI4uxZz.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-Deo4PP18.js → call-pii-button-DNyK7IIY.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-CoJ6amHf.js → collapsible-CRhBJMZR.js} +1 -1
- package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
- package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
- package/src/console/ui-next/dist/assets/decisions-page-D9twj8Xy.js +1 -0
- package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
- package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
- package/src/console/ui-next/dist/assets/duration-tone-CsHWlVPU.js +9 -0
- package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-ZaTJAhR9.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CPYBzDh0.js → highlighted-json-DQ-EMAfz.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-DTPevKmM.js → http-method-Cn_Fl79s.js} +1 -1
- package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
- package/src/console/ui-next/dist/assets/index-CmRFeWdT.js +58 -0
- package/src/console/ui-next/dist/assets/observability-page-Cn6R_dXz.js +8 -0
- package/src/console/ui-next/dist/assets/{replica-lag-BM27lXld.js → replica-lag-Bx6EZhSe.js} +7 -7
- package/src/console/ui-next/dist/assets/request-meta-DmdiUrfh.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-8P6vGXnn.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-BfGvpFSv.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CWdUHbc0.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-CZOFZzay.js +1 -0
- package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
- package/src/console/ui-next/dist/assets/vault-page-ki_ishmX.js +2 -0
- package/src/console/ui-next/dist/index.html +4 -3
- package/src/console/ui-next/seed-parked-approval.ts +126 -0
- package/src/console/ui-next/src/client.ts +224 -1
- package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
- package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
- package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
- package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
- package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
- package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
- package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
- package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
- package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
- package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
- package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
- package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
- package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
- package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
- package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
- package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
- package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
- package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
- package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
- package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +6 -2
- package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
- package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
- package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
- package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
- package/src/console/ui-next/src/router.tsx +17 -0
- package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
- package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
- package/src/console/ui-next/vite.config.ts +9 -0
- package/src/docker/recipes/dragonfly.ts +3 -2
- package/src/docker/recipes/redis.ts +1 -1
- package/src/drivers/ai-anthropic.ts +212 -21
- package/src/drivers/ai-mock.ts +34 -1
- package/src/drivers/ai-openai-compatible.ts +67 -26
- package/src/drivers/ai-preconnect.ts +18 -0
- package/src/drivers/ai-stream.test.ts +62 -0
- package/src/drivers/ai-types.ts +20 -0
- package/src/drivers/journal-postgres.test.ts +22 -1
- package/src/drivers/journal-postgres.ts +511 -13
- package/src/drivers/memory-ddl.test.ts +18 -0
- package/src/drivers/memory.ts +21 -1
- package/src/drivers/pg-rls.test.ts +31 -0
- package/src/drivers/pg-rls.ts +15 -0
- package/src/drivers/postgres.test.ts +9 -0
- package/src/drivers/postgres.ts +49 -11
- package/src/drivers/signal-compete.test.ts +234 -0
- package/src/drivers/signal-postgres.ts +39 -19
- package/src/drivers/signal-redis.ts +167 -11
- package/src/drivers/signal-types.ts +12 -0
- package/src/elements/ai/agent-event-slot.ts +27 -0
- package/src/elements/ai/approval-http.ts +207 -0
- package/src/elements/ai/approval.test.ts +741 -0
- package/src/elements/ai/approval.ts +228 -0
- package/src/elements/ai/decisions/bind.ts +163 -0
- package/src/elements/ai/decisions/certificate.test.ts +291 -0
- package/src/elements/ai/decisions/certificate.ts +377 -0
- package/src/elements/ai/decisions/certify.ts +314 -0
- package/src/elements/ai/decisions/decide.test.ts +953 -0
- package/src/elements/ai/decisions/e2e.test.ts +399 -0
- package/src/elements/ai/decisions/export.test.ts +120 -0
- package/src/elements/ai/decisions/export.ts +146 -0
- package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
- package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
- package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
- package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
- package/src/elements/ai/decisions/http.test.ts +154 -0
- package/src/elements/ai/decisions/http.ts +240 -0
- package/src/elements/ai/decisions/labels.test.ts +282 -0
- package/src/elements/ai/decisions/labels.ts +257 -0
- package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
- package/src/elements/ai/decisions/openrouter.ts +36 -0
- package/src/elements/ai/decisions/provider.ts +102 -0
- package/src/elements/ai/decisions/typesafe.ts +36 -0
- package/src/elements/ai/declare.ts +252 -1
- package/src/elements/ai/events.test.ts +379 -0
- package/src/elements/ai/events.ts +138 -0
- package/src/elements/ai/mcp-protocol.ts +5 -6
- package/src/elements/ai/run-events.test.ts +461 -0
- package/src/elements/ai/run-events.ts +522 -0
- package/src/elements/ai/runtime.ts +1187 -105
- package/src/elements/ai/stream-turn.ts +214 -0
- package/src/elements/ai/stream.live.test.ts +76 -0
- package/src/elements/ai/subagent.test.ts +136 -0
- package/src/elements/ai.test.ts +497 -3
- package/src/elements/ai.ts +16 -1
- package/src/elements/channel/runtime.ts +11 -0
- package/src/elements/channel/sql-ledger.ts +251 -0
- package/src/elements/clock/durable.ts +27 -1
- package/src/elements/signal/runtime.ts +9 -0
- package/src/elements/signal.test.ts +1 -0
- package/src/elements/store/cache.test.ts +120 -15
- package/src/elements/store/cache.ts +200 -39
- package/src/elements/store/prepare-row.test.ts +6 -2
- package/src/elements/store/sql-session.test.ts +53 -0
- package/src/elements/store/sql-session.ts +206 -10
- package/src/elements/store.ts +1 -0
- package/src/i18n/catalogs/ar.ts +5 -0
- package/src/i18n/catalogs/en.ts +5 -0
- package/src/index.ts +2 -0
- package/src/kernel/agent-event-store.ts +448 -0
- package/src/kernel/app.ts +135 -17
- package/src/kernel/auto-cache.test.ts +228 -0
- package/src/kernel/boot-bind/ai.ts +3 -0
- package/src/kernel/boot-bind/channel.ts +63 -2
- package/src/kernel/boot-bind/honor-config.test.ts +40 -10
- package/src/kernel/boot-bind/signal.ts +61 -25
- package/src/kernel/boot-bind/store.test.ts +26 -0
- package/src/kernel/boot-bind/store.ts +82 -6
- package/src/kernel/boot.ts +31 -6
- package/src/kernel/builtin-errors.ts +2 -0
- package/src/kernel/capability.ts +3 -0
- package/src/kernel/decision-label-store.ts +441 -0
- package/src/kernel/effects.test.ts +2 -1
- package/src/kernel/effects.ts +13 -1
- package/src/kernel/element-registries.ts +3 -0
- package/src/kernel/errors-compiler.ts +11 -0
- package/src/kernel/errors-text.ts +20 -0
- package/src/kernel/errors.ts +10 -0
- package/src/kernel/flow.test.ts +2 -3
- package/src/kernel/flow.ts +33 -3
- package/src/kernel/fx-call-types.test.ts +32 -0
- package/src/kernel/fx-decide.ts +765 -0
- package/src/kernel/fx-sql-handle.ts +46 -7
- package/src/kernel/fx.test.ts +13 -8
- package/src/kernel/fx.ts +339 -45
- package/src/kernel/http-frame.test.ts +94 -0
- package/src/kernel/http-frame.ts +203 -0
- package/src/kernel/idempotency.test.ts +2 -1
- package/src/kernel/journal.test.ts +114 -0
- package/src/kernel/journal.ts +471 -51
- package/src/kernel/json-result.ts +10 -0
- package/src/kernel/pipeline-tenant.ts +5 -1
- package/src/kernel/pipeline.test.ts +45 -0
- package/src/kernel/pipeline.ts +5 -0
- package/src/kernel/signal-tx.ts +28 -0
- package/src/kernel/sse-id.ts +32 -0
- package/src/kernel/store-transaction.test.ts +95 -0
- package/src/manifest/diff.ts +1 -0
- package/src/manifest/types.ts +29 -2
- package/src/mcp/ai-tools.test.ts +202 -0
- package/src/mcp/authorization.ts +66 -0
- package/src/mcp/docs-server.ts +19 -2
- package/src/mcp/mcp.test.ts +83 -0
- package/src/mcp/protocol.ts +8 -2
- package/src/mcp/server.ts +23 -2
- package/src/mcp/session.ts +3 -0
- package/src/mcp/tools.ts +168 -0
- package/src/mcp/versions.ts +60 -0
- package/src/plugins/index.ts +1 -0
- package/src/plugins/mena.ts +56 -0
- package/src/release/http-graph.test.ts +2 -0
- package/src/release/http-graph.ts +5 -0
- package/src/release/measure.ts +39 -14
- package/src/runs/collect.ts +4 -1
- package/src/runs/parquet.test.ts +27 -1
- package/src/runs/parquet.ts +19 -0
- package/src/runs/types.ts +31 -0
- package/src/runtime/json-code-block.test.ts +106 -47
- package/src/runtime/json-code-block.ts +1209 -46
- package/src/term.test.ts +13 -0
- package/src/term.ts +81 -49
- package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
- package/src/console/ui-next/dist/assets/access-page-Ze6ckjdd.js +0 -4
- package/src/console/ui-next/dist/assets/copy-inline-button-CWG5_ZOh.js +0 -1
- package/src/console/ui-next/dist/assets/detail-header-b_ZzIsmU.js +0 -1
- package/src/console/ui-next/dist/assets/dropdown-menu-nwXEO1Ac.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-C8sSCqYT.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-D2E7BtKK.js +0 -1
- package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
- package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +0 -58
- package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +0 -4
- package/src/console/ui-next/dist/assets/request-meta-BjFT7DNl.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-Cmitq3da.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CbDIB-7x.js +0 -55
- package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +0 -1
- package/src/console/ui-next/dist/assets/use-vault-list-Cs45Czkt.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-BOMr0go9.js +0 -2
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision extraction: exclusive modes, and review cannot sit on HTTP.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { extractFromSources } from "./extract.ts";
|
|
7
|
+
|
|
8
|
+
const header = `const triage = ai.decision("triage", {\n`;
|
|
9
|
+
|
|
10
|
+
describe("ai.decision extract", () => {
|
|
11
|
+
test("review and abstain together fail to compile", async () => {
|
|
12
|
+
await expect(
|
|
13
|
+
extractFromSources({
|
|
14
|
+
"src/flows/run.ts": `${header} review: "ops",\n onUncertain: "abstain",\n ask: { team: ai.choice("which", { a: "A" }) },\n});\n`,
|
|
15
|
+
}),
|
|
16
|
+
).rejects.toThrow(/exactly one of review/);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("an HTTP trigger that reviews fails to compile", async () => {
|
|
20
|
+
await expect(
|
|
21
|
+
extractFromSources({
|
|
22
|
+
"src/flows/run.ts": `
|
|
23
|
+
const triage = ai.decision("triage", {
|
|
24
|
+
review: "ops",
|
|
25
|
+
ask: { team: ai.choice("which", { a: "A" }) },
|
|
26
|
+
});
|
|
27
|
+
on(http.post("/t"), flow("run", {
|
|
28
|
+
durable: true,
|
|
29
|
+
do: async (input, fx) => fx.decide(triage, input),
|
|
30
|
+
}));
|
|
31
|
+
`,
|
|
32
|
+
}),
|
|
33
|
+
).rejects.toThrow(/fx\.emit/);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("choice, score, and duplicate names fail at compile time", async () => {
|
|
37
|
+
const many = Array.from({ length: 255 }, (_, i) => `o${i}: "x"`).join(", ");
|
|
38
|
+
await expect(
|
|
39
|
+
extractFromSources({
|
|
40
|
+
"src/flows/run.ts": `${header} onUncertain: "abstain",\n ask: { team: ai.choice("which", { ${many} }) },\n});\n`,
|
|
41
|
+
}),
|
|
42
|
+
).rejects.toThrow(/max 254/);
|
|
43
|
+
await expect(
|
|
44
|
+
extractFromSources({
|
|
45
|
+
"src/flows/run.ts": `${header} onUncertain: "abstain",\n ask: { team: ai.choice("which", { none_of_these: null }) },\n});\n`,
|
|
46
|
+
}),
|
|
47
|
+
).rejects.toThrow(/none_of_these/);
|
|
48
|
+
await expect(
|
|
49
|
+
extractFromSources({
|
|
50
|
+
"src/flows/run.ts": `${header} onUncertain: "abstain",\n ask: { rank: ai.score("rank", ["only"]) },\n});\n`,
|
|
51
|
+
}),
|
|
52
|
+
).rejects.toThrow(/2–10 levels/);
|
|
53
|
+
await expect(
|
|
54
|
+
extractFromSources({
|
|
55
|
+
"src/flows/a.ts": `${header} onUncertain: "abstain",\n ask: { team: ai.choice("which", { a: "A", b: "B" }) },\n});\n`,
|
|
56
|
+
"src/flows/b.ts": `${header} onUncertain: "abstain",\n ask: { team: ai.choice("which", { a: "A", b: "B" }) },\n});\n`,
|
|
57
|
+
}),
|
|
58
|
+
).rejects.toThrow(/duplicate decision name/);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("review records the gate name", async () => {
|
|
62
|
+
const manifest = await extractFromSources({
|
|
63
|
+
"src/flows/run.ts": `
|
|
64
|
+
const ops = gate.policy("ops", () => true);
|
|
65
|
+
const triage = ai.decision("triage", {
|
|
66
|
+
review: ops,
|
|
67
|
+
ask: { team: ai.choice("which", { a: "A", b: "B" }) },
|
|
68
|
+
});
|
|
69
|
+
on(signal.once("job"), flow("run", {
|
|
70
|
+
durable: true,
|
|
71
|
+
do: async (input, fx) => fx.decide(triage, input),
|
|
72
|
+
}));
|
|
73
|
+
`,
|
|
74
|
+
});
|
|
75
|
+
expect(manifest.ai?.decisions?.triage?.review).toBe("ops");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("review resolves a variable to the gate's declared name", async () => {
|
|
79
|
+
const manifest = await extractFromSources({
|
|
80
|
+
"src/flows/run.ts": `
|
|
81
|
+
const someVar = gate.policy("ops", () => true);
|
|
82
|
+
const triage = ai.decision("triage", {
|
|
83
|
+
review: someVar,
|
|
84
|
+
ask: { team: ai.choice("which", { a: "A", b: "B" }) },
|
|
85
|
+
});
|
|
86
|
+
on(signal.once("job"), flow("run", {
|
|
87
|
+
durable: true,
|
|
88
|
+
do: async (input, fx) => fx.decide(triage, input),
|
|
89
|
+
}));
|
|
90
|
+
`,
|
|
91
|
+
});
|
|
92
|
+
expect(manifest.ai?.decisions?.triage?.review).toBe("ops");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("autonomy and evals survive extraction", async () => {
|
|
96
|
+
const manifest = await extractFromSources({
|
|
97
|
+
"src/flows/run.ts": `
|
|
98
|
+
const triage = ai.decision("triage", {
|
|
99
|
+
onUncertain: "abstain",
|
|
100
|
+
evals: "evals/triage.jsonl",
|
|
101
|
+
autonomy: { maxError: 0.05, audit: 0.1, risk: 0.1 },
|
|
102
|
+
ask: { team: ai.choice("which", { a: "A", b: "B" }) },
|
|
103
|
+
});
|
|
104
|
+
`,
|
|
105
|
+
});
|
|
106
|
+
expect(manifest.ai?.decisions?.triage?.evals).toBe("evals/triage.jsonl");
|
|
107
|
+
expect(manifest.ai?.decisions?.triage?.autonomy).toEqual({
|
|
108
|
+
maxError: 0.05,
|
|
109
|
+
audit: 0.1,
|
|
110
|
+
risk: 0.1,
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("choice options passed through a variable fail to compile", async () => {
|
|
115
|
+
await expect(
|
|
116
|
+
extractFromSources({
|
|
117
|
+
"src/flows/run.ts": `
|
|
118
|
+
const options = { a: "A", b: "B" };
|
|
119
|
+
ai.decision("triage", {
|
|
120
|
+
onUncertain: "abstain",
|
|
121
|
+
ask: { team: ai.choice("which", options) },
|
|
122
|
+
});
|
|
123
|
+
`,
|
|
124
|
+
}),
|
|
125
|
+
).rejects.toThrow(/object literal/);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("score levels and a whole question resolve a same-file const", async () => {
|
|
129
|
+
const manifest = await extractFromSources({
|
|
130
|
+
"src/flows/run.ts": `
|
|
131
|
+
const levels = ["low", "high"];
|
|
132
|
+
const team = ai.choice("which", { a: "A", b: "B" });
|
|
133
|
+
ai.decision("triage", {
|
|
134
|
+
onUncertain: "abstain",
|
|
135
|
+
ask: { team, rank: ai.score("rank", levels) },
|
|
136
|
+
});
|
|
137
|
+
`,
|
|
138
|
+
});
|
|
139
|
+
expect(manifest.ai?.decisions?.triage?.questions).toEqual(["team", "rank"]);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("an unresolved score or question fails to compile", async () => {
|
|
143
|
+
await expect(
|
|
144
|
+
extractFromSources({
|
|
145
|
+
"src/flows/run.ts": `
|
|
146
|
+
ai.decision("triage", {
|
|
147
|
+
onUncertain: "abstain",
|
|
148
|
+
ask: { rank: ai.score("rank", missing) },
|
|
149
|
+
});
|
|
150
|
+
`,
|
|
151
|
+
}),
|
|
152
|
+
).rejects.toThrow(/same-file const/);
|
|
153
|
+
await expect(
|
|
154
|
+
extractFromSources({
|
|
155
|
+
"src/flows/run.ts": `
|
|
156
|
+
ai.decision("triage", {
|
|
157
|
+
onUncertain: "abstain",
|
|
158
|
+
ask: { team: missingQuestion },
|
|
159
|
+
});
|
|
160
|
+
`,
|
|
161
|
+
}),
|
|
162
|
+
).rejects.toThrow(/not a same-file const/);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { extractFromSources } from "./extract.ts";
|
|
6
7
|
import { inferEffects, type AstNode, type InferBinding } from "./effects-infer.ts";
|
|
7
8
|
|
|
8
9
|
function parseDo(src: string): AstNode {
|
|
@@ -39,5 +40,81 @@ describe("inferEffects — fx.fetch → effects.fetches", () => {
|
|
|
39
40
|
expect(inferred.effects.fetches).toEqual(["api.stripe.com"]);
|
|
40
41
|
expect(inferred.effects.calls).toBeUndefined();
|
|
41
42
|
expect(inferred.effects.sends).toBeUndefined();
|
|
43
|
+
expect(inferred.bareIrreversible).toEqual(["fetch"]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("fx.fetch inside fx.step is not a bare irreversible call", () => {
|
|
47
|
+
const stepBody = {
|
|
48
|
+
type: "ArrowFunctionExpression",
|
|
49
|
+
params: [],
|
|
50
|
+
body: {
|
|
51
|
+
type: "BlockStatement",
|
|
52
|
+
body: [
|
|
53
|
+
{
|
|
54
|
+
type: "ExpressionStatement",
|
|
55
|
+
expression: {
|
|
56
|
+
type: "CallExpression",
|
|
57
|
+
callee: {
|
|
58
|
+
type: "MemberExpression",
|
|
59
|
+
object: { type: "Identifier", name: "fx" },
|
|
60
|
+
property: { type: "Identifier", name: "fetch" },
|
|
61
|
+
computed: false,
|
|
62
|
+
},
|
|
63
|
+
arguments: [{ type: "Literal", value: "https://example.com/data" }],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
const inferred = inferEffects({
|
|
70
|
+
doNode: {
|
|
71
|
+
type: "ArrowFunctionExpression",
|
|
72
|
+
params: [
|
|
73
|
+
{ type: "Identifier", name: "_input" },
|
|
74
|
+
{ type: "Identifier", name: "fx" },
|
|
75
|
+
],
|
|
76
|
+
body: {
|
|
77
|
+
type: "BlockStatement",
|
|
78
|
+
body: [
|
|
79
|
+
{
|
|
80
|
+
type: "ExpressionStatement",
|
|
81
|
+
expression: {
|
|
82
|
+
type: "CallExpression",
|
|
83
|
+
callee: {
|
|
84
|
+
type: "MemberExpression",
|
|
85
|
+
object: { type: "Identifier", name: "fx" },
|
|
86
|
+
property: { type: "Identifier", name: "step" },
|
|
87
|
+
computed: false,
|
|
88
|
+
},
|
|
89
|
+
arguments: [{ type: "Literal", value: "load" }, stepBody],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
} as AstNode,
|
|
95
|
+
bindings: new Map<string, InferBinding>(),
|
|
96
|
+
hasExplicitEffects: false,
|
|
97
|
+
});
|
|
98
|
+
expect(inferred.bareIrreversible).toEqual([]);
|
|
99
|
+
expect(inferred.steps).toEqual(["load"]);
|
|
100
|
+
expect(inferred.effects.fetches).toEqual(["example.com"]);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("extract — durable fx.fetch outside fx.step", () => {
|
|
105
|
+
test("OKE1901 when a durable flow calls fx.fetch directly", async () => {
|
|
106
|
+
await expect(
|
|
107
|
+
extractFromSources({
|
|
108
|
+
"src/flows/load.ts": `
|
|
109
|
+
import { flow, on, http } from "okengine";
|
|
110
|
+
export const load = on(http.get("/load"), flow("load", {
|
|
111
|
+
durable: true,
|
|
112
|
+
do: async (_input, fx) => {
|
|
113
|
+
return fx.fetch("https://example.com/data");
|
|
114
|
+
},
|
|
115
|
+
}));
|
|
116
|
+
`,
|
|
117
|
+
}),
|
|
118
|
+
).rejects.toThrow(/OKE1901/);
|
|
42
119
|
});
|
|
43
120
|
});
|
|
@@ -57,10 +57,12 @@ export interface InferBinding {
|
|
|
57
57
|
| "signal"
|
|
58
58
|
| "clock"
|
|
59
59
|
| "prompt"
|
|
60
|
+
| "agent"
|
|
60
61
|
| "secret"
|
|
61
62
|
| "template"
|
|
62
63
|
| "flow"
|
|
63
64
|
| "embed"
|
|
65
|
+
| "decision"
|
|
64
66
|
| "table"
|
|
65
67
|
| "mcp-server"
|
|
66
68
|
| "mcp-tool"
|
|
@@ -71,6 +73,8 @@ export interface InferBinding {
|
|
|
71
73
|
readonly facet?: "sql" | "kv" | "files" | "index";
|
|
72
74
|
/** Prompt version when kind is `prompt`. */
|
|
73
75
|
readonly version?: number;
|
|
76
|
+
/** Nested agent tools when kind is `agent`. */
|
|
77
|
+
readonly agentCalls?: readonly string[];
|
|
74
78
|
}
|
|
75
79
|
|
|
76
80
|
/** What a callee that is passed `fx` resolves to. */
|
|
@@ -106,6 +110,11 @@ export interface InferEffectsOptions {
|
|
|
106
110
|
* never pass `fx` into another function.
|
|
107
111
|
*/
|
|
108
112
|
readonly resolveCallee?: (callee: AstNode, file: string) => FxCalleeResolution;
|
|
113
|
+
/**
|
|
114
|
+
* Flow name of an imported handle in `file`. Absent when inference does
|
|
115
|
+
* not follow imports (unit tests).
|
|
116
|
+
*/
|
|
117
|
+
readonly resolveFlowRef?: (localName: string, file: string) => string | undefined;
|
|
109
118
|
}
|
|
110
119
|
|
|
111
120
|
/** Result of effect inference for one flow body. */
|
|
@@ -125,6 +134,11 @@ export interface InferredEffects {
|
|
|
125
134
|
readonly nondeterministic: boolean;
|
|
126
135
|
/** True when the body references `fx.auth.userId`. */
|
|
127
136
|
readonly readsUserId: boolean;
|
|
137
|
+
/**
|
|
138
|
+
* `fx.fetch` / `fx.send` calls that are not inside an `fx.step` callback.
|
|
139
|
+
* A `durable: true` flow must not call these directly.
|
|
140
|
+
*/
|
|
141
|
+
readonly bareIrreversible: readonly ("fetch" | "send")[];
|
|
128
142
|
}
|
|
129
143
|
|
|
130
144
|
const READ_METHODS = new Set([
|
|
@@ -141,6 +155,10 @@ const READ_METHODS = new Set([
|
|
|
141
155
|
"page",
|
|
142
156
|
"search",
|
|
143
157
|
"raw",
|
|
158
|
+
"innerJoin",
|
|
159
|
+
"leftJoin",
|
|
160
|
+
"rightJoin",
|
|
161
|
+
"fullJoin",
|
|
144
162
|
]);
|
|
145
163
|
|
|
146
164
|
const WRITE_METHODS = new Set([
|
|
@@ -171,6 +189,10 @@ const TABLE_ARG_METHODS = new Set([
|
|
|
171
189
|
"count",
|
|
172
190
|
"page",
|
|
173
191
|
"search",
|
|
192
|
+
"innerJoin",
|
|
193
|
+
"leftJoin",
|
|
194
|
+
"rightJoin",
|
|
195
|
+
"fullJoin",
|
|
174
196
|
]);
|
|
175
197
|
|
|
176
198
|
/**
|
|
@@ -190,6 +212,7 @@ export function inferEffects(options: InferEffectsOptions): InferredEffects {
|
|
|
190
212
|
const secrets = new Set<SecretRef>();
|
|
191
213
|
const calls = new Set<FlowRef>();
|
|
192
214
|
const fetches = new Set<FetchHostRef>();
|
|
215
|
+
const decides = new Set<string>();
|
|
193
216
|
const steps: string[] = [];
|
|
194
217
|
let usesRaw = false;
|
|
195
218
|
|
|
@@ -199,7 +222,8 @@ export function inferEffects(options: InferEffectsOptions): InferredEffects {
|
|
|
199
222
|
name: "liveQuery" | "applySearchEmbedCdc";
|
|
200
223
|
call: CallExpression;
|
|
201
224
|
}> = [];
|
|
202
|
-
const
|
|
225
|
+
const bareIrreversible = new Set<"fetch" | "send">();
|
|
226
|
+
const chains = collectFxChains(options, opaque, userIdRoots, intrinsics, bareIrreversible);
|
|
203
227
|
|
|
204
228
|
for (const { call, chain } of chains) {
|
|
205
229
|
if (chain.rootMethod === "raw") {
|
|
@@ -237,6 +261,17 @@ export function inferEffects(options: InferEffectsOptions): InferredEffects {
|
|
|
237
261
|
continue;
|
|
238
262
|
}
|
|
239
263
|
|
|
264
|
+
if (chain.rootMethod === "run" && call === chain.rootCall) {
|
|
265
|
+
const binding = resolveBinding(call.arguments[0], options.bindings);
|
|
266
|
+
const name =
|
|
267
|
+
stringArg(call.arguments[0]) ?? (binding?.kind === "agent" ? binding.ref : undefined);
|
|
268
|
+
if (name) asks.add(name);
|
|
269
|
+
if (binding?.kind === "agent") {
|
|
270
|
+
for (const child of binding.agentCalls ?? []) calls.add(child);
|
|
271
|
+
}
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
240
275
|
if (chain.rootMethod === "ask" && call === chain.rootCall) {
|
|
241
276
|
const ref = resolvePrompt(call.arguments[0], options.bindings);
|
|
242
277
|
if (ref) asks.add(ref);
|
|
@@ -268,6 +303,14 @@ export function inferEffects(options: InferEffectsOptions): InferredEffects {
|
|
|
268
303
|
continue;
|
|
269
304
|
}
|
|
270
305
|
|
|
306
|
+
if (chain.rootMethod === "decide" && call === chain.rootCall) {
|
|
307
|
+
const binding = resolveBinding(call.arguments[0], options.bindings);
|
|
308
|
+
const name =
|
|
309
|
+
stringArg(call.arguments[0]) ?? (binding?.kind === "decision" ? binding.ref : undefined);
|
|
310
|
+
if (name) decides.add(name);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
271
314
|
if (chain.rootMethod === "fetch" && call === chain.rootCall) {
|
|
272
315
|
// fx.fetch(url) — host becomes effects.fetches entry.
|
|
273
316
|
const lit = stringArg(call.arguments[0]);
|
|
@@ -289,7 +332,13 @@ export function inferEffects(options: InferEffectsOptions): InferredEffects {
|
|
|
289
332
|
}
|
|
290
333
|
|
|
291
334
|
if (chain.rootMethod === "call" && call === chain.rootCall) {
|
|
292
|
-
const
|
|
335
|
+
const arg = unwrapValueNode(call.arguments[0]);
|
|
336
|
+
const local = identifierName(arg);
|
|
337
|
+
const imported =
|
|
338
|
+
local && options.file && options.resolveFlowRef
|
|
339
|
+
? options.resolveFlowRef(local, options.file)
|
|
340
|
+
: undefined;
|
|
341
|
+
const ref = imported ?? resolveCallTarget(call.arguments[0], options.bindings);
|
|
293
342
|
if (ref) calls.add(ref);
|
|
294
343
|
continue;
|
|
295
344
|
}
|
|
@@ -335,18 +384,28 @@ export function inferEffects(options: InferEffectsOptions): InferredEffects {
|
|
|
335
384
|
// Skip incomplete chains (`select` before `.from`, `insert` before table) —
|
|
336
385
|
// the sibling call that carries the table arg records the real resource.
|
|
337
386
|
const leaf = resolved.methods[resolved.methods.length - 1]!;
|
|
338
|
-
const
|
|
387
|
+
const tables = tablesFromStoreChain(call, options.bindings, chain);
|
|
388
|
+
const hasTable = tables.length > 0;
|
|
339
389
|
if (!hasTable && (leaf === "select" || leaf === "insert" || leaf === "update")) {
|
|
340
390
|
continue;
|
|
341
391
|
}
|
|
342
392
|
// Intermediate chain links that only forward (where/values/returning)
|
|
343
393
|
// still carry read/write from earlier methods — record once we have a table
|
|
344
394
|
// or a terminal key-based op.
|
|
345
|
-
if (
|
|
395
|
+
if (
|
|
396
|
+
!hasTable &&
|
|
397
|
+
(leaf === "where" || leaf === "values" || leaf === "returning" || leaf === "groupBy")
|
|
398
|
+
) {
|
|
346
399
|
continue;
|
|
347
400
|
}
|
|
348
|
-
|
|
349
|
-
|
|
401
|
+
const resources =
|
|
402
|
+
tables.length > 0
|
|
403
|
+
? tables.map((table) => sqlTableRef(table) as ResourceRef)
|
|
404
|
+
: [resolved.resource];
|
|
405
|
+
for (const resource of resources) {
|
|
406
|
+
if (op === "read" || op === "both") reads.add(resource);
|
|
407
|
+
if (op === "write" || op === "both") writes.add(resource);
|
|
408
|
+
}
|
|
350
409
|
}
|
|
351
410
|
}
|
|
352
411
|
|
|
@@ -374,14 +433,16 @@ export function inferEffects(options: InferEffectsOptions): InferredEffects {
|
|
|
374
433
|
if (secrets.size > 0) effects.secrets = sortUnique([...secrets]);
|
|
375
434
|
if (calls.size > 0) effects.calls = sortUnique([...calls]);
|
|
376
435
|
if (fetches.size > 0) effects.fetches = sortUnique([...fetches]);
|
|
436
|
+
if (decides.size > 0) effects.decides = sortUnique([...decides]);
|
|
377
437
|
|
|
378
438
|
return {
|
|
379
439
|
effects,
|
|
380
440
|
steps,
|
|
381
441
|
usesRaw,
|
|
382
442
|
cacheIneligible: usesRaw && !options.hasExplicitEffects,
|
|
383
|
-
nondeterministic: asks.size > 0 || embeds.size > 0,
|
|
443
|
+
nondeterministic: asks.size > 0 || embeds.size > 0 || decides.size > 0,
|
|
384
444
|
readsUserId: userIdRoots.some((node) => containsAuthUserId(node)),
|
|
445
|
+
bareIrreversible: [...bareIrreversible],
|
|
385
446
|
};
|
|
386
447
|
}
|
|
387
448
|
|
|
@@ -395,6 +456,7 @@ const EFFECT_KEYS = [
|
|
|
395
456
|
"secrets",
|
|
396
457
|
"calls",
|
|
397
458
|
"fetches",
|
|
459
|
+
"decides",
|
|
398
460
|
] as const satisfies readonly (keyof Effects)[];
|
|
399
461
|
|
|
400
462
|
/**
|
|
@@ -584,21 +646,37 @@ function tableFromStoreChain(
|
|
|
584
646
|
bindings: ReadonlyMap<string, InferBinding>,
|
|
585
647
|
chain?: FxChain,
|
|
586
648
|
): string | undefined {
|
|
649
|
+
return tablesFromStoreChain(call, bindings, chain)[0];
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Every table argument on a store chain, including joins.
|
|
654
|
+
*
|
|
655
|
+
* @param call - Any call in the chain
|
|
656
|
+
* @param bindings - Scope bindings
|
|
657
|
+
* @param chain - Precomputed chain when already known
|
|
658
|
+
*/
|
|
659
|
+
function tablesFromStoreChain(
|
|
660
|
+
call: CallExpression,
|
|
661
|
+
bindings: ReadonlyMap<string, InferBinding>,
|
|
662
|
+
chain?: FxChain,
|
|
663
|
+
): string[] {
|
|
587
664
|
const found = tableFromStoreWalk(call, bindings, chain);
|
|
588
|
-
if (found) return found;
|
|
665
|
+
if (found.length > 0) return found;
|
|
589
666
|
if (chain?.aliasLeaf && chain.aliasLeaf !== call) {
|
|
590
667
|
return tableFromStoreWalk(chain.aliasLeaf, bindings);
|
|
591
668
|
}
|
|
592
|
-
return
|
|
669
|
+
return [];
|
|
593
670
|
}
|
|
594
671
|
|
|
595
672
|
function tableFromStoreWalk(
|
|
596
673
|
call: CallExpression,
|
|
597
674
|
bindings: ReadonlyMap<string, InferBinding>,
|
|
598
675
|
chain?: FxChain,
|
|
599
|
-
): string
|
|
676
|
+
): string[] {
|
|
600
677
|
let current: AstNode | undefined = call;
|
|
601
678
|
const seen = new Set<AstNode>();
|
|
679
|
+
const names: string[] = [];
|
|
602
680
|
while (current && current.type === "CallExpression" && !seen.has(current)) {
|
|
603
681
|
seen.add(current);
|
|
604
682
|
const c = current as CallExpression;
|
|
@@ -618,7 +696,8 @@ function tableFromStoreWalk(
|
|
|
618
696
|
const id = identifierName(c.arguments[0]);
|
|
619
697
|
if (id) {
|
|
620
698
|
const binding = bindings.get(id);
|
|
621
|
-
|
|
699
|
+
const tableName = binding?.kind === "table" ? binding.ref : id;
|
|
700
|
+
if (!names.includes(tableName)) names.push(tableName);
|
|
622
701
|
}
|
|
623
702
|
}
|
|
624
703
|
}
|
|
@@ -632,7 +711,7 @@ function tableFromStoreWalk(
|
|
|
632
711
|
}
|
|
633
712
|
break;
|
|
634
713
|
}
|
|
635
|
-
return
|
|
714
|
+
return names;
|
|
636
715
|
}
|
|
637
716
|
|
|
638
717
|
function methodNameOfCall(call: CallExpression): string | undefined {
|
|
@@ -882,7 +961,9 @@ function collectFxChains(
|
|
|
882
961
|
opaque: string[],
|
|
883
962
|
userIdRoots: AstNode[],
|
|
884
963
|
intrinsics: Array<{ name: "liveQuery" | "applySearchEmbedCdc"; call: CallExpression }>,
|
|
964
|
+
bareIrreversible: Set<"fetch" | "send">,
|
|
885
965
|
): ChainHit[] {
|
|
966
|
+
const stepBodies = new Set<AstNode>();
|
|
886
967
|
const hits: ChainHit[] = [];
|
|
887
968
|
const seen = new Set<AstNode>();
|
|
888
969
|
|
|
@@ -892,7 +973,7 @@ function collectFxChains(
|
|
|
892
973
|
aliases: ReadonlyMap<string, ChainAlias>,
|
|
893
974
|
locals: ReadonlyMap<string, AstNode>,
|
|
894
975
|
): void => {
|
|
895
|
-
analyzeFunction(fn, "fx", aliases, locals, file);
|
|
976
|
+
analyzeFunction(fn, "fx", aliases, locals, file, stepBodies.has(fn));
|
|
896
977
|
};
|
|
897
978
|
|
|
898
979
|
const analyzeFunction = (
|
|
@@ -901,13 +982,14 @@ function collectFxChains(
|
|
|
901
982
|
outerAliases: ReadonlyMap<string, ChainAlias>,
|
|
902
983
|
outerLocals: ReadonlyMap<string, AstNode>,
|
|
903
984
|
file: string,
|
|
985
|
+
insideStep: boolean,
|
|
904
986
|
): void => {
|
|
905
987
|
if (seen.has(fn)) return;
|
|
906
988
|
seen.add(fn);
|
|
907
989
|
userIdRoots.push(fn);
|
|
908
990
|
const body = (fn as AstNode & { body?: AstNode }).body;
|
|
909
991
|
if (!body) return;
|
|
910
|
-
analyzeBody(body, door, outerAliases, outerLocals, file);
|
|
992
|
+
analyzeBody(body, door, outerAliases, outerLocals, file, insideStep);
|
|
911
993
|
};
|
|
912
994
|
|
|
913
995
|
const analyzeBody = (
|
|
@@ -916,6 +998,7 @@ function collectFxChains(
|
|
|
916
998
|
outerAliases: ReadonlyMap<string, ChainAlias>,
|
|
917
999
|
outerLocals: ReadonlyMap<string, AstNode>,
|
|
918
1000
|
file: string,
|
|
1001
|
+
insideStep: boolean,
|
|
919
1002
|
): void => {
|
|
920
1003
|
const aliases = new Map(outerAliases);
|
|
921
1004
|
const locals = new Map(outerLocals);
|
|
@@ -925,6 +1008,20 @@ function collectFxChains(
|
|
|
925
1008
|
});
|
|
926
1009
|
for (const call of callsSkippingNested(body)) {
|
|
927
1010
|
const chain = chainFromCall(call, aliases);
|
|
1011
|
+
if (chain?.rootMethod === "step") {
|
|
1012
|
+
for (const arg of call.arguments) {
|
|
1013
|
+
const fn = unwrapValue(arg);
|
|
1014
|
+
if (fn && isFunctionNode(fn)) stepBodies.add(fn);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
if (
|
|
1018
|
+
!insideStep &&
|
|
1019
|
+
chain &&
|
|
1020
|
+
call === chain.rootCall &&
|
|
1021
|
+
(chain.rootMethod === "fetch" || chain.rootMethod === "send")
|
|
1022
|
+
) {
|
|
1023
|
+
bareIrreversible.add(chain.rootMethod);
|
|
1024
|
+
}
|
|
928
1025
|
if (chain) {
|
|
929
1026
|
hits.push({ call, chain });
|
|
930
1027
|
continue;
|
|
@@ -935,7 +1032,7 @@ function collectFxChains(
|
|
|
935
1032
|
if (seen.has(nested)) continue;
|
|
936
1033
|
const shadows = functionParams(nested).some((param) => paramIdentifierName(param) === door);
|
|
937
1034
|
if (shadows) continue;
|
|
938
|
-
analyzeFunction(nested, door, aliases, locals, file);
|
|
1035
|
+
analyzeFunction(nested, door, aliases, locals, file, insideStep || stepBodies.has(nested));
|
|
939
1036
|
}
|
|
940
1037
|
};
|
|
941
1038
|
|
|
@@ -946,9 +1043,9 @@ function collectFxChains(
|
|
|
946
1043
|
if (second && paramIdentifierName(second) !== "fx") {
|
|
947
1044
|
opaque.push("the do callback's second parameter is not named fx");
|
|
948
1045
|
}
|
|
949
|
-
analyzeFunction(options.doNode, "fx", new Map(), new Map(), startFile);
|
|
1046
|
+
analyzeFunction(options.doNode, "fx", new Map(), new Map(), startFile, false);
|
|
950
1047
|
} else {
|
|
951
|
-
analyzeBody(options.doNode, "fx", new Map(), new Map(), startFile);
|
|
1048
|
+
analyzeBody(options.doNode, "fx", new Map(), new Map(), startFile, false);
|
|
952
1049
|
}
|
|
953
1050
|
|
|
954
1051
|
return hits;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Joins and groupBy list every source table under the flow's reads.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { extractFromSources } from "./extract.ts";
|
|
7
|
+
|
|
8
|
+
describe("extractManifest — joins", () => {
|
|
9
|
+
test("innerJoin and groupBy record every table", async () => {
|
|
10
|
+
const manifest = await extractFromSources({
|
|
11
|
+
"src/app.ts": `
|
|
12
|
+
import { oke } from "okengine";
|
|
13
|
+
export const app = oke({ name: "joins" });
|
|
14
|
+
`,
|
|
15
|
+
"src/flows/orders.ts": `
|
|
16
|
+
import { on, flow, http, store, field } from "okengine";
|
|
17
|
+
import { eq, count } from "drizzle-orm";
|
|
18
|
+
|
|
19
|
+
export const db = store.sql("db");
|
|
20
|
+
export const orders = store.schema.table("orders", {
|
|
21
|
+
id: field.text().primaryKey(),
|
|
22
|
+
customerId: field.text(),
|
|
23
|
+
});
|
|
24
|
+
export const customers = store.schema.table("customers", {
|
|
25
|
+
id: field.text().primaryKey(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const totals = on(
|
|
29
|
+
http.get("/totals"),
|
|
30
|
+
flow("orders.totals", {
|
|
31
|
+
do: async (_input, fx) => {
|
|
32
|
+
return fx
|
|
33
|
+
.store(db)
|
|
34
|
+
.select({ n: count() })
|
|
35
|
+
.from(orders)
|
|
36
|
+
.innerJoin(customers, eq(orders.customerId, customers.id))
|
|
37
|
+
.groupBy(customers.id);
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
`,
|
|
42
|
+
});
|
|
43
|
+
expect(manifest.flows?.["orders.totals"]?.effects?.reads?.slice().sort()).toEqual([
|
|
44
|
+
"sql:customers",
|
|
45
|
+
"sql:orders",
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -1577,6 +1577,34 @@ export const ping = flow({ do: () => ({ ok: true }) });
|
|
|
1577
1577
|
expect(manifest.flows?.["notes.ping"]).toBeDefined();
|
|
1578
1578
|
});
|
|
1579
1579
|
|
|
1580
|
+
test("fx.call of an imported flow handle records that flow's name", async () => {
|
|
1581
|
+
const tasks = `
|
|
1582
|
+
import { on, flow, http } from "okengine";
|
|
1583
|
+
export const create = on(
|
|
1584
|
+
http.post("/tasks"),
|
|
1585
|
+
flow("tasks.create", { do: async () => ({ id: "1" }) }),
|
|
1586
|
+
);
|
|
1587
|
+
`;
|
|
1588
|
+
const forms = `
|
|
1589
|
+
import { on, flow, http } from "okengine";
|
|
1590
|
+
import { create as createTask } from "../tasks/index.ts";
|
|
1591
|
+
export const submit = on(
|
|
1592
|
+
http.post("/forms/:id/submit"),
|
|
1593
|
+
flow("forms.submit", {
|
|
1594
|
+
do: async (_input, fx) => {
|
|
1595
|
+
await fx.call(createTask, { title: "SSO" });
|
|
1596
|
+
return { ok: true };
|
|
1597
|
+
},
|
|
1598
|
+
}),
|
|
1599
|
+
);
|
|
1600
|
+
`;
|
|
1601
|
+
const manifest = await extractFromSources({
|
|
1602
|
+
"src/flows/tasks/index.ts": tasks,
|
|
1603
|
+
"src/flows/forms/index.ts": forms,
|
|
1604
|
+
});
|
|
1605
|
+
expect(manifest.flows?.["forms.submit"]?.effects?.calls).toEqual(["tasks.create"]);
|
|
1606
|
+
});
|
|
1607
|
+
|
|
1580
1608
|
test("effects are identical for the same do body in a tree file vs a barrel", async () => {
|
|
1581
1609
|
const doBody = `
|
|
1582
1610
|
const rows = await fx.store(db).select().from(notes);
|