@xdelivered/emberflow 0.2.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.
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/bin/commands.test.ts +185 -0
- package/bin/commands.ts +306 -0
- package/bin/emberflow.mjs +38 -0
- package/bin/init.test.ts +165 -0
- package/bin/init.ts +297 -0
- package/bin/runtime.mjs +105 -0
- package/bin/runtime.test.ts +167 -0
- package/bin/skillTargets.test.ts +108 -0
- package/bin/skillTargets.ts +36 -0
- package/bin/skillTemplates.test.ts +37 -0
- package/bin/tsconfig.json +23 -0
- package/dist/bin/commands.d.ts +24 -0
- package/dist/bin/commands.js +252 -0
- package/dist/bin/init.d.ts +17 -0
- package/dist/bin/init.js +241 -0
- package/dist/bin/runtime.d.mts +39 -0
- package/dist/bin/runtime.mjs +96 -0
- package/dist/bin/skillTargets.d.ts +13 -0
- package/dist/bin/skillTargets.js +22 -0
- package/dist/server/agents/claudeAdapter.d.ts +8 -0
- package/dist/server/agents/claudeAdapter.js +190 -0
- package/dist/server/agents/claudeParse.d.ts +2 -0
- package/dist/server/agents/claudeParse.js +162 -0
- package/dist/server/agents/codexAdapter.d.ts +13 -0
- package/dist/server/agents/codexAdapter.js +174 -0
- package/dist/server/agents/codexParse.d.ts +2 -0
- package/dist/server/agents/codexParse.js +74 -0
- package/dist/server/agents/detect.d.ts +23 -0
- package/dist/server/agents/detect.js +35 -0
- package/dist/server/agents/gitScope.d.ts +24 -0
- package/dist/server/agents/gitScope.js +95 -0
- package/dist/server/agents/modelRejectionHint.d.ts +8 -0
- package/dist/server/agents/modelRejectionHint.js +12 -0
- package/dist/server/agents/prompt.d.ts +79 -0
- package/dist/server/agents/prompt.js +364 -0
- package/dist/server/agents/runManager.d.ts +90 -0
- package/dist/server/agents/runManager.js +259 -0
- package/dist/server/agents/types.d.ts +15 -0
- package/dist/server/agents/types.js +1 -0
- package/dist/server/apiStore.d.ts +60 -0
- package/dist/server/apiStore.js +249 -0
- package/dist/server/auth/enforce.d.ts +1 -0
- package/dist/server/auth/enforce.js +6 -0
- package/dist/server/auth/types.d.ts +1 -0
- package/dist/server/auth/types.js +1 -0
- package/dist/server/auth/verifiers.d.ts +1 -0
- package/dist/server/auth/verifiers.js +4 -0
- package/dist/server/authAttach.d.ts +19 -0
- package/dist/server/authAttach.js +79 -0
- package/dist/server/cli.d.ts +7 -0
- package/dist/server/cli.js +804 -0
- package/dist/server/client.d.ts +130 -0
- package/dist/server/client.js +247 -0
- package/dist/server/environments.d.ts +103 -0
- package/dist/server/environments.js +486 -0
- package/dist/server/flowStore.d.ts +37 -0
- package/dist/server/flowStore.js +117 -0
- package/dist/server/httpOperations.d.ts +32 -0
- package/dist/server/httpOperations.js +62 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1022 -0
- package/dist/server/infrastructure.d.ts +51 -0
- package/dist/server/infrastructure.js +116 -0
- package/dist/server/login.d.ts +21 -0
- package/dist/server/login.js +44 -0
- package/dist/server/mcp.d.ts +1 -0
- package/dist/server/mcp.js +182 -0
- package/dist/server/migrateFlows.d.ts +8 -0
- package/dist/server/migrateFlows.js +25 -0
- package/dist/server/mockHandler.d.ts +15 -0
- package/dist/server/mockHandler.js +35 -0
- package/dist/server/nodesPayload.d.ts +14 -0
- package/dist/server/nodesPayload.js +14 -0
- package/dist/server/normalizeFlow.d.ts +21 -0
- package/dist/server/normalizeFlow.js +48 -0
- package/dist/server/openBrowser.d.ts +8 -0
- package/dist/server/openBrowser.js +16 -0
- package/dist/server/operationResult.d.ts +12 -0
- package/dist/server/operationResult.js +26 -0
- package/dist/server/pathGuard.d.ts +1 -0
- package/dist/server/pathGuard.js +16 -0
- package/dist/server/pathSafety.d.ts +20 -0
- package/dist/server/pathSafety.js +35 -0
- package/dist/server/projectConfig.d.ts +47 -0
- package/dist/server/projectConfig.js +61 -0
- package/dist/server/projectMode.d.ts +26 -0
- package/dist/server/projectMode.js +44 -0
- package/dist/server/redact.d.ts +6 -0
- package/dist/server/redact.js +37 -0
- package/dist/server/runRegistry.d.ts +84 -0
- package/dist/server/runRegistry.js +200 -0
- package/dist/server/runtime/RuntimeAdapter.d.ts +22 -0
- package/dist/server/runtime/RuntimeAdapter.js +1 -0
- package/dist/server/runtime/expressAdapter.d.ts +19 -0
- package/dist/server/runtime/expressAdapter.js +56 -0
- package/dist/server/scenarioTest.d.ts +14 -0
- package/dist/server/scenarioTest.js +66 -0
- package/dist/server/subflowRunner.d.ts +32 -0
- package/dist/server/subflowRunner.js +72 -0
- package/dist/server/testRunner.d.ts +70 -0
- package/dist/server/testRunner.js +151 -0
- package/dist/src/config.d.ts +3 -0
- package/dist/src/config.js +2 -0
- package/dist/src/engine/authVerify.d.ts +33 -0
- package/dist/src/engine/authVerify.js +63 -0
- package/dist/src/engine/diagnostics.d.ts +62 -0
- package/dist/src/engine/diagnostics.js +148 -0
- package/dist/src/engine/executor.d.ts +151 -0
- package/dist/src/engine/executor.js +684 -0
- package/dist/src/engine/httpError.d.ts +7 -0
- package/dist/src/engine/httpError.js +14 -0
- package/dist/src/engine/index.d.ts +11 -0
- package/dist/src/engine/index.js +11 -0
- package/dist/src/engine/output.d.ts +11 -0
- package/dist/src/engine/output.js +20 -0
- package/dist/src/engine/publish.d.ts +30 -0
- package/dist/src/engine/publish.js +95 -0
- package/dist/src/engine/registry.d.ts +34 -0
- package/dist/src/engine/registry.js +67 -0
- package/dist/src/engine/scenarios.d.ts +12 -0
- package/dist/src/engine/scenarios.js +19 -0
- package/dist/src/engine/schemaCheck.d.ts +4 -0
- package/dist/src/engine/schemaCheck.js +34 -0
- package/dist/src/engine/trace.d.ts +12 -0
- package/dist/src/engine/trace.js +12 -0
- package/dist/src/engine/types.d.ts +308 -0
- package/dist/src/engine/types.js +1 -0
- package/dist/src/engine/validation.d.ts +26 -0
- package/dist/src/engine/validation.js +318 -0
- package/dist/src/flows/anomaly-flows.d.ts +23 -0
- package/dist/src/flows/anomaly-flows.js +151 -0
- package/dist/src/flows/city-sweep-flow.d.ts +10 -0
- package/dist/src/flows/city-sweep-flow.js +127 -0
- package/dist/src/flows/login-flow.d.ts +13 -0
- package/dist/src/flows/login-flow.js +145 -0
- package/dist/src/flows/pradar-flows.d.ts +5 -0
- package/dist/src/flows/pradar-flows.js +375 -0
- package/dist/src/flows/weather-flow.d.ts +12 -0
- package/dist/src/flows/weather-flow.js +150 -0
- package/dist/src/lib/pathParams.d.ts +3 -0
- package/dist/src/lib/pathParams.js +6 -0
- package/dist/src/nodes/anomaly.d.ts +2 -0
- package/dist/src/nodes/anomaly.js +556 -0
- package/dist/src/nodes/flow-control.d.ts +7 -0
- package/dist/src/nodes/flow-control.js +270 -0
- package/dist/src/nodes/index.d.ts +3 -0
- package/dist/src/nodes/index.js +20 -0
- package/dist/src/nodes/login.d.ts +7 -0
- package/dist/src/nodes/login.js +158 -0
- package/dist/src/nodes/loops.d.ts +11 -0
- package/dist/src/nodes/loops.js +60 -0
- package/dist/src/nodes/pradar/index.d.ts +7 -0
- package/dist/src/nodes/pradar/index.js +9 -0
- package/dist/src/nodes/pradar/logic.d.ts +189 -0
- package/dist/src/nodes/pradar/logic.js +370 -0
- package/dist/src/nodes/pradar/nodes.d.ts +42 -0
- package/dist/src/nodes/pradar/nodes.js +801 -0
- package/dist/src/nodes/requireAuth.d.ts +20 -0
- package/dist/src/nodes/requireAuth.js +48 -0
- package/dist/src/nodes/response.d.ts +8 -0
- package/dist/src/nodes/response.js +33 -0
- package/dist/src/nodes/weather.d.ts +33 -0
- package/dist/src/nodes/weather.js +260 -0
- package/package.json +92 -0
- package/server/agentRoute.test.ts +157 -0
- package/server/agents/__fixtures__/fake-claude-writer.mjs +23 -0
- package/server/agents/__fixtures__/fake-claude.mjs +55 -0
- package/server/agents/__fixtures__/fake-codex-writer.mjs +27 -0
- package/server/agents/__fixtures__/fake-codex.mjs +52 -0
- package/server/agents/__fixtures__/fake-version-empty.mjs +3 -0
- package/server/agents/__fixtures__/fake-version-fail.mjs +4 -0
- package/server/agents/__fixtures__/fake-version-garbage.mjs +4 -0
- package/server/agents/__fixtures__/fake-version-good.mjs +4 -0
- package/server/agents/__fixtures__/fake-version-hang.mjs +3 -0
- package/server/agents/claudeAdapter.test.ts +127 -0
- package/server/agents/claudeAdapter.ts +212 -0
- package/server/agents/claudeParse.ts +193 -0
- package/server/agents/codexAdapter.test.ts +125 -0
- package/server/agents/codexAdapter.ts +198 -0
- package/server/agents/codexParse.test.ts +58 -0
- package/server/agents/codexParse.ts +126 -0
- package/server/agents/detect.test.ts +65 -0
- package/server/agents/detect.ts +43 -0
- package/server/agents/gitScope.test.ts +129 -0
- package/server/agents/gitScope.ts +110 -0
- package/server/agents/modelRejectionHint.test.ts +30 -0
- package/server/agents/modelRejectionHint.ts +14 -0
- package/server/agents/prompt.test.ts +613 -0
- package/server/agents/prompt.ts +527 -0
- package/server/agents/runManager.test.ts +251 -0
- package/server/agents/runManager.ts +286 -0
- package/server/agents/types.ts +17 -0
- package/server/apiMount.test.ts +591 -0
- package/server/apiStore.test.ts +324 -0
- package/server/apiStore.ts +241 -0
- package/server/auth/enforce.test.ts +61 -0
- package/server/auth/enforce.ts +6 -0
- package/server/auth/types.ts +5 -0
- package/server/auth/verifiers.test.ts +37 -0
- package/server/auth/verifiers.ts +9 -0
- package/server/authAttach.test.ts +175 -0
- package/server/authAttach.ts +99 -0
- package/server/cli.ts +847 -0
- package/server/cliAuth.test.ts +229 -0
- package/server/cliCreate.test.ts +131 -0
- package/server/cliDoctor.test.ts +326 -0
- package/server/cliTest.test.ts +735 -0
- package/server/client.ts +314 -0
- package/server/demoProject.test.ts +29 -0
- package/server/diagnosticsRoute.test.ts +169 -0
- package/server/environments.test.ts +620 -0
- package/server/environments.ts +603 -0
- package/server/environmentsRoute.test.ts +385 -0
- package/server/errorWorkflow.test.ts +205 -0
- package/server/flowStore.test.ts +84 -0
- package/server/flowStore.ts +125 -0
- package/server/httpOperations.test.ts +377 -0
- package/server/httpOperations.ts +96 -0
- package/server/index.ts +1111 -0
- package/server/infrastructure.test.ts +154 -0
- package/server/infrastructure.ts +161 -0
- package/server/infrastructureRoute.test.ts +105 -0
- package/server/login.test.ts +153 -0
- package/server/login.ts +67 -0
- package/server/mcp.ts +278 -0
- package/server/migrateFlows.test.ts +55 -0
- package/server/migrateFlows.ts +23 -0
- package/server/mockHandler.test.ts +138 -0
- package/server/mockHandler.ts +52 -0
- package/server/mockRun.test.ts +367 -0
- package/server/mockServe.test.ts +467 -0
- package/server/nodesPayload.test.ts +29 -0
- package/server/nodesPayload.ts +26 -0
- package/server/normalizeFlow.test.ts +80 -0
- package/server/normalizeFlow.ts +59 -0
- package/server/openBrowser.test.ts +20 -0
- package/server/openBrowser.ts +22 -0
- package/server/operationResult.test.ts +67 -0
- package/server/operationResult.ts +27 -0
- package/server/pathGuard.test.ts +19 -0
- package/server/pathGuard.ts +12 -0
- package/server/pathSafety.test.ts +38 -0
- package/server/pathSafety.ts +33 -0
- package/server/projectConfig.test.ts +67 -0
- package/server/projectConfig.ts +101 -0
- package/server/projectMode.test.ts +106 -0
- package/server/projectMode.ts +59 -0
- package/server/redact.test.ts +88 -0
- package/server/redact.ts +35 -0
- package/server/redactEgress.test.ts +281 -0
- package/server/runRegistry.ts +270 -0
- package/server/runsAuth.test.ts +119 -0
- package/server/runtime/RuntimeAdapter.ts +24 -0
- package/server/runtime/expressAdapter.test.ts +38 -0
- package/server/runtime/expressAdapter.ts +70 -0
- package/server/scenarioTest.test.ts +148 -0
- package/server/scenarioTest.ts +88 -0
- package/server/setupStatus.test.ts +127 -0
- package/server/subflowRunner.ts +104 -0
- package/server/testRunner.ts +216 -0
- package/server/tsconfig.json +20 -0
- package/server/workflowTestRoute.test.ts +297 -0
- package/src/App.tsx +233 -0
- package/src/components/AgentConsole.tsx +461 -0
- package/src/components/BranchRulesEditor.tsx +156 -0
- package/src/components/CreateModal.tsx +275 -0
- package/src/components/Dock.tsx +487 -0
- package/src/components/EnvironmentDialog.tsx +569 -0
- package/src/components/EnvironmentPicker.tsx +357 -0
- package/src/components/EnvironmentsDialog.tsx +144 -0
- package/src/components/ExecutionPager.tsx +67 -0
- package/src/components/InfraPanel.test.tsx +103 -0
- package/src/components/InfraPanel.tsx +171 -0
- package/src/components/Inspector.tsx +1182 -0
- package/src/components/Json.tsx +50 -0
- package/src/components/JsonModal.tsx +52 -0
- package/src/components/LogMessage.tsx +61 -0
- package/src/components/NodeRunModal.tsx +213 -0
- package/src/components/RunLogPanel.tsx +397 -0
- package/src/components/RunbookView.tsx +1085 -0
- package/src/components/Scenarios.tsx +476 -0
- package/src/components/SettingsDialog.tsx +229 -0
- package/src/components/Sidebar.tsx +436 -0
- package/src/components/StatusBar.tsx +302 -0
- package/src/components/Toolbar.tsx +663 -0
- package/src/components/WelcomeDialog.test.tsx +107 -0
- package/src/components/WelcomeDialog.tsx +412 -0
- package/src/components/agentMarkdown.test.tsx +91 -0
- package/src/components/agentMarkdown.tsx +158 -0
- package/src/components/ui/badge.tsx +31 -0
- package/src/components/ui/button.tsx +46 -0
- package/src/components/ui/combobox.tsx +114 -0
- package/src/components/ui/command.tsx +82 -0
- package/src/components/ui/dialog.tsx +73 -0
- package/src/components/ui/input.tsx +20 -0
- package/src/components/ui/popover.tsx +32 -0
- package/src/components/ui/resizable.tsx +44 -0
- package/src/components/ui/tabs.tsx +36 -0
- package/src/config.ts +4 -0
- package/src/engine/authVerify.ts +81 -0
- package/src/engine/diagnostics.test.ts +437 -0
- package/src/engine/diagnostics.ts +211 -0
- package/src/engine/executor.test.ts +1339 -0
- package/src/engine/executor.ts +840 -0
- package/src/engine/httpError.test.ts +12 -0
- package/src/engine/httpError.ts +16 -0
- package/src/engine/index.ts +11 -0
- package/src/engine/output.ts +21 -0
- package/src/engine/publish.test.ts +176 -0
- package/src/engine/publish.ts +127 -0
- package/src/engine/registry.test.ts +64 -0
- package/src/engine/registry.ts +80 -0
- package/src/engine/scenarios.test.ts +55 -0
- package/src/engine/scenarios.ts +29 -0
- package/src/engine/schemaCheck.test.ts +21 -0
- package/src/engine/schemaCheck.ts +35 -0
- package/src/engine/trace.test.ts +19 -0
- package/src/engine/trace.ts +23 -0
- package/src/engine/types.test.ts +22 -0
- package/src/engine/types.ts +316 -0
- package/src/engine/validation.test.ts +314 -0
- package/src/engine/validation.ts +354 -0
- package/src/flows/anomaly-flows.test.ts +81 -0
- package/src/flows/anomaly-flows.ts +202 -0
- package/src/flows/city-sweep-flow.ts +131 -0
- package/src/flows/flows.test.ts +30 -0
- package/src/flows/login-flow.ts +150 -0
- package/src/flows/pradar-flows.test.ts +204 -0
- package/src/flows/pradar-flows.ts +419 -0
- package/src/flows/weather-flow.ts +154 -0
- package/src/globals.css +147 -0
- package/src/lib/pathParams.test.ts +24 -0
- package/src/lib/pathParams.ts +6 -0
- package/src/lib/registerLens.test.ts +79 -0
- package/src/lib/registerLens.ts +59 -0
- package/src/lib/runbookModel.test.ts +61 -0
- package/src/lib/runbookModel.ts +290 -0
- package/src/lib/runbookProjection.test.ts +417 -0
- package/src/lib/runbookProjection.ts +254 -0
- package/src/lib/useTailAnchor.ts +53 -0
- package/src/lib/utils.ts +7 -0
- package/src/main.tsx +13 -0
- package/src/nodes/anomaly.test.ts +98 -0
- package/src/nodes/anomaly.ts +637 -0
- package/src/nodes/effects-scan.test.ts +44 -0
- package/src/nodes/flow-control.test.ts +280 -0
- package/src/nodes/flow-control.ts +305 -0
- package/src/nodes/index.ts +22 -0
- package/src/nodes/login.test.ts +81 -0
- package/src/nodes/login.ts +186 -0
- package/src/nodes/loops.ts +69 -0
- package/src/nodes/pradar/index.ts +11 -0
- package/src/nodes/pradar/logic.test.ts +288 -0
- package/src/nodes/pradar/logic.ts +582 -0
- package/src/nodes/pradar/nodes.test.ts +146 -0
- package/src/nodes/pradar/nodes.ts +955 -0
- package/src/nodes/requireAuth.test.ts +75 -0
- package/src/nodes/requireAuth.ts +55 -0
- package/src/nodes/response.test.ts +33 -0
- package/src/nodes/response.ts +39 -0
- package/src/nodes/traceDetail.test.ts +39 -0
- package/src/nodes/weather.test.ts +65 -0
- package/src/nodes/weather.ts +306 -0
- package/src/store/agentClient.test.ts +103 -0
- package/src/store/agentClient.ts +131 -0
- package/src/store/apiTree.test.ts +17 -0
- package/src/store/apiTree.ts +73 -0
- package/src/store/builderStore.test.ts +1234 -0
- package/src/store/builderStore.ts +1879 -0
- package/src/store/infraClient.ts +63 -0
- package/src/store/nodeMeta.test.ts +24 -0
- package/src/store/nodeMeta.ts +21 -0
- package/src/store/persistence.test.ts +15 -0
- package/src/store/persistence.ts +54 -0
- package/src/store/serverRunner.ts +381 -0
- package/src/store/setupClient.ts +57 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
- package/studio-dist/assets/index-DNJwW-hM.css +1 -0
- package/studio-dist/assets/index-O26dKRjW.js +65 -0
- package/studio-dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/studio-dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/studio-dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/studio-dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/studio-dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/studio-dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/studio-dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/studio-dist/favicon.svg +1 -0
- package/studio-dist/icons.svg +24 -0
- package/studio-dist/index.html +14 -0
- package/templates/skills/emberflow-basics/SKILL.md +316 -0
- package/templates/skills/emberflow-model-process/SKILL.md +209 -0
- package/templates/skills/emberflow-new-workflow/SKILL.md +211 -0
- package/templates/skills/emberflow-review-workflow/SKILL.md +148 -0
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
import type { NodeExecutionContext, NodeRegistry } from '../engine';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Anomaly Detection API nodes.
|
|
5
|
+
*
|
|
6
|
+
* A demo integration for time-series anomaly detection backed by a pluggable
|
|
7
|
+
* local service. Detection nodes POST to that service (default
|
|
8
|
+
* http://localhost:8091, overridable per environment); in the browser they go
|
|
9
|
+
* through the Vite dev proxy at /anomaly to keep requests same-origin.
|
|
10
|
+
*
|
|
11
|
+
* The quota / API-key nodes are self-contained business-logic examples: they
|
|
12
|
+
* only compute and return values (no persistence, no outbound writes), so none
|
|
13
|
+
* are declared `effects: 'mutation'`. `DetectEntireSeries` /
|
|
14
|
+
* `DetectLastPoint` / `DetectAttributed` POST to the detection service, but
|
|
15
|
+
* only to run detection (a read), not to create or change a resource.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolves the anomaly-detector service base URL. Prefers the run's
|
|
20
|
+
* environment (`ctx.vars.ANOMALY_API_URL`, set per-environment by the
|
|
21
|
+
* runner); falls back to the pre-environments resolution (browser: Vite
|
|
22
|
+
* proxy at /anomaly; Node: ANOMALY_API_URL process env, else localhost).
|
|
23
|
+
*/
|
|
24
|
+
function resolveApiBase(ctx: NodeExecutionContext): string {
|
|
25
|
+
if (ctx.vars.ANOMALY_API_URL) return ctx.vars.ANOMALY_API_URL;
|
|
26
|
+
if (typeof window !== 'undefined') return '/anomaly';
|
|
27
|
+
const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env;
|
|
28
|
+
return env?.ANOMALY_API_URL ?? 'http://localhost:8091';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface SeriesPoint {
|
|
32
|
+
timestamp: string;
|
|
33
|
+
value: number;
|
|
34
|
+
dims?: Record<string, Record<string, number>>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function postJson(
|
|
38
|
+
ctx: NodeExecutionContext,
|
|
39
|
+
path: string,
|
|
40
|
+
apiKey: string,
|
|
41
|
+
body: unknown,
|
|
42
|
+
): Promise<Record<string, unknown>> {
|
|
43
|
+
const base = resolveApiBase(ctx);
|
|
44
|
+
const response = await fetch(`${base}${path}`, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: { 'content-type': 'application/json', 'x-api-key': apiKey },
|
|
47
|
+
body: JSON.stringify(body),
|
|
48
|
+
});
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
const text = await response.text();
|
|
51
|
+
throw new Error(`HTTP ${response.status} from ${path}: ${text.slice(0, 200)}`);
|
|
52
|
+
}
|
|
53
|
+
return (await response.json()) as Record<string, unknown>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function hourlyTimestamps(count: number): string[] {
|
|
57
|
+
const start = Date.UTC(2026, 0, 1);
|
|
58
|
+
return Array.from({ length: count }, (_, i) =>
|
|
59
|
+
new Date(start + i * 3_600_000).toISOString(),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Plan catalog: monthly quota + price by plan code. */
|
|
64
|
+
const PLANS: Record<string, { name: string; quota: number; priceUsd: number }> = {
|
|
65
|
+
anomaly_free: { name: 'Free', quota: 1_000, priceUsd: 0 },
|
|
66
|
+
anomaly_starter: { name: 'Starter', quota: 50_000, priceUsd: 29 },
|
|
67
|
+
anomaly_pro: { name: 'Pro', quota: 500_000, priceUsd: 99 },
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export function registerAnomalyNodes(registry: NodeRegistry): void {
|
|
71
|
+
// ── Series sources ────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
registry.register(
|
|
74
|
+
{
|
|
75
|
+
type: 'BuildSeries',
|
|
76
|
+
label: 'Build Series',
|
|
77
|
+
description: 'Generates an hourly metric series with a configurable injected spike.',
|
|
78
|
+
category: 'anomaly',
|
|
79
|
+
traceKind: 'compute',
|
|
80
|
+
// Parameters are declared as inputs too, so upstream nodes (e.g. an
|
|
81
|
+
// Input entry node) can drive them; config remains the fallback
|
|
82
|
+
// (resolveInput copies config values for declared input fields).
|
|
83
|
+
inputSchema: {
|
|
84
|
+
fields: [
|
|
85
|
+
{ name: 'points', type: 'number' },
|
|
86
|
+
{ name: 'baseValue', type: 'number' },
|
|
87
|
+
{ name: 'spikeIndex', type: 'number' },
|
|
88
|
+
{ name: 'spikeFactor', type: 'number' },
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
configSchema: {
|
|
92
|
+
fields: [
|
|
93
|
+
{ name: 'points', type: 'number' },
|
|
94
|
+
{ name: 'baseValue', type: 'number' },
|
|
95
|
+
{ name: 'spikeIndex', type: 'number' },
|
|
96
|
+
{ name: 'spikeFactor', type: 'number' },
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
outputSchema: {
|
|
100
|
+
fields: [
|
|
101
|
+
{ name: 'series', type: 'array' },
|
|
102
|
+
{ name: 'pointCount', type: 'number' },
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
async (ctx) => {
|
|
107
|
+
const points = Number(ctx.input.points ?? 24);
|
|
108
|
+
const base = Number(ctx.input.baseValue ?? 100);
|
|
109
|
+
const spikeIndex = ctx.input.spikeIndex === undefined ? -1 : Number(ctx.input.spikeIndex);
|
|
110
|
+
const spikeFactor = Number(ctx.input.spikeFactor ?? 3);
|
|
111
|
+
const timestamps = hourlyTimestamps(points);
|
|
112
|
+
const series: SeriesPoint[] = timestamps.map((timestamp, i) => {
|
|
113
|
+
// Deterministic gentle wave so the series is realistic but replayable.
|
|
114
|
+
let value = base + Math.round(Math.sin(i / 3) * base * 0.08);
|
|
115
|
+
if (i === (spikeIndex < 0 ? -999 : spikeIndex)) value = Math.round(value * spikeFactor);
|
|
116
|
+
return { timestamp, value };
|
|
117
|
+
});
|
|
118
|
+
ctx.log('info', `Built ${points}-point hourly series${spikeIndex >= 0 ? `, spike ×${spikeFactor} at index ${spikeIndex}` : ''}`);
|
|
119
|
+
return { series, pointCount: points };
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
registry.register(
|
|
124
|
+
{
|
|
125
|
+
type: 'BuildDimensionalSeries',
|
|
126
|
+
label: 'Build Dimensional Series',
|
|
127
|
+
description: 'Generates a series with per-point channel dimensions; the spike is driven by one slice (for RCA).',
|
|
128
|
+
category: 'anomaly',
|
|
129
|
+
traceKind: 'compute',
|
|
130
|
+
configSchema: {
|
|
131
|
+
fields: [
|
|
132
|
+
{ name: 'points', type: 'number', required: true },
|
|
133
|
+
{ name: 'baseValue', type: 'number', required: true },
|
|
134
|
+
{ name: 'spikeIndex', type: 'number', required: true },
|
|
135
|
+
{ name: 'culpritSlice', type: 'enum', enumValues: ['organic', 'paid'], required: true },
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
outputSchema: {
|
|
139
|
+
fields: [
|
|
140
|
+
{ name: 'series', type: 'array' },
|
|
141
|
+
{ name: 'culpritSlice', type: 'string' },
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
async (ctx) => {
|
|
146
|
+
const points = Number(ctx.config.points ?? 14);
|
|
147
|
+
const base = Number(ctx.config.baseValue ?? 100);
|
|
148
|
+
const spikeIndex = Number(ctx.config.spikeIndex ?? points - 2);
|
|
149
|
+
const culprit = String(ctx.config.culpritSlice ?? 'paid');
|
|
150
|
+
const timestamps = hourlyTimestamps(points);
|
|
151
|
+
const series: SeriesPoint[] = timestamps.map((timestamp, i) => {
|
|
152
|
+
const value = base;
|
|
153
|
+
let organic = Math.round(value * 0.6);
|
|
154
|
+
let paid = value - organic;
|
|
155
|
+
if (i === spikeIndex) {
|
|
156
|
+
// Triple the culprit slice only — Adtributor should name it.
|
|
157
|
+
if (culprit === 'paid') paid *= 3;
|
|
158
|
+
else organic *= 3;
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
timestamp,
|
|
162
|
+
value: organic + paid,
|
|
163
|
+
dims: { channel: { organic, paid } },
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
ctx.log('info', `Built ${points}-point dimensional series, ${culprit} spike at index ${spikeIndex}`);
|
|
167
|
+
return { series, culpritSlice: culprit };
|
|
168
|
+
},
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// ── Live detection calls (real service via /anomaly proxy) ────────
|
|
172
|
+
|
|
173
|
+
registry.register(
|
|
174
|
+
{
|
|
175
|
+
type: 'DetectEntireSeries',
|
|
176
|
+
label: 'Detect · Entire Series',
|
|
177
|
+
description: 'POST /timeseries/entire/detect — STL decomposition + residual-band flagging over the whole series.',
|
|
178
|
+
simpleDescription: 'Scans the whole series for unusual spikes or dips',
|
|
179
|
+
category: 'anomaly',
|
|
180
|
+
traceKind: 'http',
|
|
181
|
+
traceDetail: 'POST {ANOMALY_API_URL}/timeseries/entire/detect (browser: /anomaly proxy → localhost:8091)',
|
|
182
|
+
inputSchema: {
|
|
183
|
+
fields: [{ name: 'series', type: 'array', required: true }],
|
|
184
|
+
},
|
|
185
|
+
configSchema: {
|
|
186
|
+
fields: [
|
|
187
|
+
{ name: 'apiKey', type: 'string' },
|
|
188
|
+
{ name: 'sensitivity', type: 'number' },
|
|
189
|
+
],
|
|
190
|
+
},
|
|
191
|
+
outputSchema: {
|
|
192
|
+
fields: [
|
|
193
|
+
{ name: 'isAnomaly', type: 'array' },
|
|
194
|
+
{ name: 'expectedValues', type: 'array' },
|
|
195
|
+
{ name: 'upperMargins', type: 'array' },
|
|
196
|
+
{ name: 'lowerMargins', type: 'array' },
|
|
197
|
+
{ name: 'period', type: 'number' },
|
|
198
|
+
],
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
async (ctx) => {
|
|
202
|
+
const series = ctx.input.series as SeriesPoint[];
|
|
203
|
+
const apiKey = ctx.secrets.ANOMALY_API_KEY ?? String(ctx.config.apiKey ?? '');
|
|
204
|
+
if (!apiKey) throw new Error('No API key: set secret ANOMALY_API_KEY or config apiKey');
|
|
205
|
+
ctx.log('info', `POST ${resolveApiBase(ctx)}/timeseries/entire/detect (${series.length} points)`);
|
|
206
|
+
const data = await postJson(ctx, '/timeseries/entire/detect', apiKey, {
|
|
207
|
+
series,
|
|
208
|
+
sensitivity: Number(ctx.config.sensitivity ?? 80),
|
|
209
|
+
});
|
|
210
|
+
const flags = (data.isAnomaly as boolean[]) ?? [];
|
|
211
|
+
ctx.log('info', `Service flagged ${flags.filter(Boolean).length}/${flags.length} points, period=${String(data.period)}`);
|
|
212
|
+
return data;
|
|
213
|
+
},
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
registry.register(
|
|
217
|
+
{
|
|
218
|
+
type: 'DetectLastPoint',
|
|
219
|
+
label: 'Detect · Last Point',
|
|
220
|
+
description: 'POST /timeseries/last/detect — streaming-style verdict for the latest point only.',
|
|
221
|
+
simpleDescription: 'Checks whether the newest data point looks unusual',
|
|
222
|
+
category: 'anomaly',
|
|
223
|
+
traceKind: 'http',
|
|
224
|
+
traceDetail: 'POST {ANOMALY_API_URL}/timeseries/last/detect (browser: /anomaly proxy → localhost:8091)',
|
|
225
|
+
inputSchema: {
|
|
226
|
+
fields: [{ name: 'series', type: 'array', required: true }],
|
|
227
|
+
},
|
|
228
|
+
configSchema: {
|
|
229
|
+
fields: [
|
|
230
|
+
{ name: 'apiKey', type: 'string' },
|
|
231
|
+
{ name: 'sensitivity', type: 'number' },
|
|
232
|
+
],
|
|
233
|
+
},
|
|
234
|
+
outputSchema: {
|
|
235
|
+
fields: [
|
|
236
|
+
{ name: 'isAnomaly', type: 'boolean' },
|
|
237
|
+
{ name: 'isPositiveAnomaly', type: 'boolean' },
|
|
238
|
+
{ name: 'expectedValue', type: 'number' },
|
|
239
|
+
{ name: 'upperMargin', type: 'number' },
|
|
240
|
+
{ name: 'lowerMargin', type: 'number' },
|
|
241
|
+
{ name: 'suggestedWindow', type: 'number' },
|
|
242
|
+
],
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
async (ctx) => {
|
|
246
|
+
const series = ctx.input.series as SeriesPoint[];
|
|
247
|
+
const apiKey = ctx.secrets.ANOMALY_API_KEY ?? String(ctx.config.apiKey ?? '');
|
|
248
|
+
if (!apiKey) throw new Error('No API key: set secret ANOMALY_API_KEY or config apiKey');
|
|
249
|
+
ctx.log('info', `POST ${resolveApiBase(ctx)}/timeseries/last/detect (${series.length} points)`);
|
|
250
|
+
const data = await postJson(ctx, '/timeseries/last/detect', apiKey, {
|
|
251
|
+
series,
|
|
252
|
+
sensitivity: Number(ctx.config.sensitivity ?? 80),
|
|
253
|
+
});
|
|
254
|
+
ctx.log('info', `Last point anomaly=${String(data.isAnomaly)} (expected ${String(data.expectedValue)})`);
|
|
255
|
+
return data;
|
|
256
|
+
},
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
registry.register(
|
|
260
|
+
{
|
|
261
|
+
type: 'DetectAttributed',
|
|
262
|
+
label: 'Detect · Attributed (RCA)',
|
|
263
|
+
description: 'POST /detect/attributed — detection plus Adtributor root-cause analysis over dimensions.',
|
|
264
|
+
simpleDescription: 'Finds anomalies and which dimension caused them',
|
|
265
|
+
category: 'anomaly',
|
|
266
|
+
traceKind: 'http',
|
|
267
|
+
traceDetail: 'POST {ANOMALY_API_URL}/detect/attributed (browser: /anomaly proxy → localhost:8091)',
|
|
268
|
+
inputSchema: {
|
|
269
|
+
fields: [{ name: 'series', type: 'array', required: true }],
|
|
270
|
+
},
|
|
271
|
+
configSchema: {
|
|
272
|
+
fields: [
|
|
273
|
+
{ name: 'apiKey', type: 'string' },
|
|
274
|
+
{ name: 'sensitivity', type: 'number' },
|
|
275
|
+
{ name: 'topK', type: 'number' },
|
|
276
|
+
],
|
|
277
|
+
},
|
|
278
|
+
outputSchema: {
|
|
279
|
+
fields: [
|
|
280
|
+
{ name: 'rca', type: 'array' },
|
|
281
|
+
{ name: 'response', type: 'object' },
|
|
282
|
+
{ name: 'warnings', type: 'array' },
|
|
283
|
+
],
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
async (ctx) => {
|
|
287
|
+
const series = ctx.input.series as SeriesPoint[];
|
|
288
|
+
const apiKey = ctx.secrets.ANOMALY_API_KEY ?? String(ctx.config.apiKey ?? '');
|
|
289
|
+
if (!apiKey) throw new Error('No API key: set secret ANOMALY_API_KEY or config apiKey');
|
|
290
|
+
ctx.log('info', `POST ${resolveApiBase(ctx)}/detect/attributed (${series.length} points)`);
|
|
291
|
+
const data = await postJson(ctx, '/detect/attributed', apiKey, {
|
|
292
|
+
series,
|
|
293
|
+
sensitivity: Number(ctx.config.sensitivity ?? 80),
|
|
294
|
+
topK: Number(ctx.config.topK ?? 3),
|
|
295
|
+
});
|
|
296
|
+
const rca = (data.rca as unknown[]) ?? [];
|
|
297
|
+
ctx.log('info', `RCA returned ${rca.length} attributed anomal${rca.length === 1 ? 'y' : 'ies'}`);
|
|
298
|
+
return data;
|
|
299
|
+
},
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
// ── Post-processing ───────────────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
registry.register(
|
|
305
|
+
{
|
|
306
|
+
type: 'SummarizeAnomalies',
|
|
307
|
+
label: 'Summarize Anomalies',
|
|
308
|
+
description: 'Reduces per-point anomaly flags to indices, count, and a headline.',
|
|
309
|
+
simpleDescription: 'Summarizes how many anomalies were found',
|
|
310
|
+
category: 'anomaly',
|
|
311
|
+
traceKind: 'compute',
|
|
312
|
+
inputSchema: {
|
|
313
|
+
fields: [
|
|
314
|
+
{ name: 'isAnomaly', type: 'array', required: true },
|
|
315
|
+
{ name: 'series', type: 'array', required: true },
|
|
316
|
+
],
|
|
317
|
+
},
|
|
318
|
+
outputSchema: {
|
|
319
|
+
fields: [
|
|
320
|
+
{ name: 'anomalyCount', type: 'number' },
|
|
321
|
+
{ name: 'anomalyIndices', type: 'array' },
|
|
322
|
+
{ name: 'headline', type: 'string' },
|
|
323
|
+
],
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
async (ctx) => {
|
|
327
|
+
const flags = (ctx.input.isAnomaly as boolean[]) ?? [];
|
|
328
|
+
const series = (ctx.input.series as SeriesPoint[]) ?? [];
|
|
329
|
+
const anomalyIndices = flags.flatMap((f, i) => (f ? [i] : []));
|
|
330
|
+
const detail = anomalyIndices
|
|
331
|
+
.map((i) => `#${i} (${series[i]?.timestamp ?? '?'} → ${series[i]?.value ?? '?'})`)
|
|
332
|
+
.join(', ');
|
|
333
|
+
const headline =
|
|
334
|
+
anomalyIndices.length === 0
|
|
335
|
+
? `No anomalies across ${flags.length} points`
|
|
336
|
+
: `${anomalyIndices.length} anomal${anomalyIndices.length === 1 ? 'y' : 'ies'} in ${flags.length} points: ${detail}`;
|
|
337
|
+
ctx.log('info', headline);
|
|
338
|
+
return { anomalyCount: anomalyIndices.length, anomalyIndices, headline };
|
|
339
|
+
},
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
registry.register(
|
|
343
|
+
{
|
|
344
|
+
type: 'ExtractRootCause',
|
|
345
|
+
label: 'Extract Root Cause',
|
|
346
|
+
description: 'Pulls the top attributed dimension slice out of an RCA response.',
|
|
347
|
+
simpleDescription: 'Finds the main cause of the anomaly',
|
|
348
|
+
category: 'anomaly',
|
|
349
|
+
traceKind: 'compute',
|
|
350
|
+
inputSchema: {
|
|
351
|
+
fields: [{ name: 'rca', type: 'array', required: true }],
|
|
352
|
+
},
|
|
353
|
+
outputSchema: {
|
|
354
|
+
fields: [
|
|
355
|
+
{ name: 'anomalyCount', type: 'number' },
|
|
356
|
+
{ name: 'topDimension', type: 'string' },
|
|
357
|
+
{ name: 'topSlice', type: 'string' },
|
|
358
|
+
{ name: 'contributionScore', type: 'number' },
|
|
359
|
+
{ name: 'confidence', type: 'number' },
|
|
360
|
+
{ name: 'headline', type: 'string' },
|
|
361
|
+
],
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
async (ctx) => {
|
|
365
|
+
const rca = (ctx.input.rca ?? []) as Array<{
|
|
366
|
+
timestamp: string;
|
|
367
|
+
rootCause: {
|
|
368
|
+
confidence: number;
|
|
369
|
+
dimensions: Array<{
|
|
370
|
+
name: string;
|
|
371
|
+
topValues: Array<{ value: string; contributionScore: number }>;
|
|
372
|
+
}>;
|
|
373
|
+
};
|
|
374
|
+
}>;
|
|
375
|
+
if (rca.length === 0) {
|
|
376
|
+
ctx.log('warn', 'RCA list empty — nothing to attribute');
|
|
377
|
+
return { anomalyCount: 0, topDimension: '—', topSlice: '—', contributionScore: 0, confidence: 0, headline: 'No attributed anomalies' };
|
|
378
|
+
}
|
|
379
|
+
const first = rca[0];
|
|
380
|
+
const dimension = first.rootCause.dimensions[0];
|
|
381
|
+
const slice = dimension?.topValues[0];
|
|
382
|
+
const headline = `Anomaly at ${first.timestamp}: ${dimension?.name}=${slice?.value} (contribution ${(slice?.contributionScore ?? 0).toFixed(2)}, confidence ${first.rootCause.confidence.toFixed(2)})`;
|
|
383
|
+
ctx.log('info', headline);
|
|
384
|
+
return {
|
|
385
|
+
anomalyCount: rca.length,
|
|
386
|
+
topDimension: dimension?.name ?? '—',
|
|
387
|
+
topSlice: slice?.value ?? '—',
|
|
388
|
+
contributionScore: slice?.contributionScore ?? 0,
|
|
389
|
+
confidence: first.rootCause.confidence,
|
|
390
|
+
headline,
|
|
391
|
+
};
|
|
392
|
+
},
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
registry.register(
|
|
396
|
+
{
|
|
397
|
+
type: 'ClassifySeverity',
|
|
398
|
+
label: 'Classify Severity',
|
|
399
|
+
description: 'Routes on whether the series carried any anomalies (anomalous vs clean).',
|
|
400
|
+
category: 'anomaly',
|
|
401
|
+
traceKind: 'compute',
|
|
402
|
+
tags: ['branching'],
|
|
403
|
+
inputSchema: {
|
|
404
|
+
fields: [{ name: 'anomalyCount', type: 'number', required: true }],
|
|
405
|
+
},
|
|
406
|
+
outputSchema: {
|
|
407
|
+
fields: [
|
|
408
|
+
{ name: 'severity', type: 'enum', enumValues: ['anomalous', 'clean'] },
|
|
409
|
+
{ name: 'anomalyCount', type: 'number' },
|
|
410
|
+
],
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
async (ctx) => {
|
|
414
|
+
const anomalyCount = Number(ctx.input.anomalyCount ?? 0);
|
|
415
|
+
const severity = anomalyCount >= 1 ? 'anomalous' : 'clean';
|
|
416
|
+
ctx.log('info', `Severity ${severity} (${anomalyCount} anomal${anomalyCount === 1 ? 'y' : 'ies'})`);
|
|
417
|
+
return { severity, anomalyCount };
|
|
418
|
+
},
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
registry.register(
|
|
422
|
+
{
|
|
423
|
+
type: 'ComposeIncident',
|
|
424
|
+
label: 'Compose Incident',
|
|
425
|
+
description: 'Builds an incident record with a priority derived from the anomaly count.',
|
|
426
|
+
category: 'anomaly',
|
|
427
|
+
traceKind: 'compute',
|
|
428
|
+
inputSchema: {
|
|
429
|
+
fields: [
|
|
430
|
+
{ name: 'headline', type: 'string', required: true },
|
|
431
|
+
{ name: 'anomalyCount', type: 'number' },
|
|
432
|
+
],
|
|
433
|
+
},
|
|
434
|
+
outputSchema: {
|
|
435
|
+
fields: [
|
|
436
|
+
{ name: 'incident', type: 'string' },
|
|
437
|
+
{ name: 'priority', type: 'enum', enumValues: ['P1', 'P2'] },
|
|
438
|
+
],
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
async (ctx) => {
|
|
442
|
+
const headline = String(ctx.input.headline ?? 'Anomalies detected');
|
|
443
|
+
const anomalyCount = Number(ctx.input.anomalyCount ?? 0);
|
|
444
|
+
const priority = anomalyCount >= 3 ? 'P1' : 'P2';
|
|
445
|
+
const incident = `[${priority}] ${headline}`;
|
|
446
|
+
ctx.log('info', `Raised ${priority} incident: ${headline}`);
|
|
447
|
+
return { incident, priority };
|
|
448
|
+
},
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
// ── Quota & plans ─────────────────────────────────────────────────
|
|
452
|
+
|
|
453
|
+
registry.register(
|
|
454
|
+
{
|
|
455
|
+
type: 'PlanCatalog',
|
|
456
|
+
label: 'Plan Catalog',
|
|
457
|
+
description: 'Resolves a plan code to quota and price (unknown codes fail safe to Free).',
|
|
458
|
+
simpleDescription: 'Looks up a plan’s quota and price',
|
|
459
|
+
category: 'anomaly-ops',
|
|
460
|
+
traceKind: 'compute',
|
|
461
|
+
configSchema: {
|
|
462
|
+
fields: [
|
|
463
|
+
{
|
|
464
|
+
name: 'planCode',
|
|
465
|
+
type: 'enum',
|
|
466
|
+
enumValues: ['anomaly_free', 'anomaly_starter', 'anomaly_pro'],
|
|
467
|
+
required: true,
|
|
468
|
+
},
|
|
469
|
+
],
|
|
470
|
+
},
|
|
471
|
+
outputSchema: {
|
|
472
|
+
fields: [
|
|
473
|
+
{ name: 'planCode', type: 'string' },
|
|
474
|
+
{ name: 'name', type: 'string' },
|
|
475
|
+
{ name: 'quota', type: 'number' },
|
|
476
|
+
{ name: 'priceUsd', type: 'number' },
|
|
477
|
+
],
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
async (ctx) => {
|
|
481
|
+
const requested = String(ctx.config.planCode ?? 'anomaly_free');
|
|
482
|
+
const plan = PLANS[requested] ?? PLANS.anomaly_free;
|
|
483
|
+
const planCode = PLANS[requested] ? requested : 'anomaly_free';
|
|
484
|
+
ctx.log('info', `Plan ${planCode}: ${plan.quota.toLocaleString()} calls/mo, $${plan.priceUsd}`);
|
|
485
|
+
return { planCode, ...plan };
|
|
486
|
+
},
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
registry.register(
|
|
490
|
+
{
|
|
491
|
+
type: 'UsageState',
|
|
492
|
+
label: 'Usage State',
|
|
493
|
+
description: 'Computes percentUsed / warn80 / overQuota for the current period.',
|
|
494
|
+
simpleDescription: 'Checks how much of the plan’s quota has been used',
|
|
495
|
+
category: 'anomaly-ops',
|
|
496
|
+
traceKind: 'compute',
|
|
497
|
+
inputSchema: {
|
|
498
|
+
fields: [
|
|
499
|
+
{ name: 'calls', type: 'number', required: true },
|
|
500
|
+
{ name: 'quota', type: 'number', required: true },
|
|
501
|
+
],
|
|
502
|
+
},
|
|
503
|
+
outputSchema: {
|
|
504
|
+
fields: [
|
|
505
|
+
{ name: 'period', type: 'string' },
|
|
506
|
+
{ name: 'calls', type: 'number' },
|
|
507
|
+
{ name: 'percentUsed', type: 'number' },
|
|
508
|
+
{ name: 'warn80', type: 'boolean' },
|
|
509
|
+
{ name: 'overQuota', type: 'boolean' },
|
|
510
|
+
],
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
async (ctx) => {
|
|
514
|
+
const calls = Number(ctx.input.calls);
|
|
515
|
+
const quota = Number(ctx.input.quota);
|
|
516
|
+
const percentUsed = Math.round((calls / quota) * 1000) / 10;
|
|
517
|
+
const overQuota = calls >= quota;
|
|
518
|
+
const warn80 = calls >= 0.8 * quota;
|
|
519
|
+
const period = new Date().toISOString().slice(0, 7);
|
|
520
|
+
ctx.log('info', `${period}: ${calls}/${quota} calls (${percentUsed}%) warn80=${warn80} overQuota=${overQuota}`);
|
|
521
|
+
return { period, calls, percentUsed, warn80, overQuota };
|
|
522
|
+
},
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
registry.register(
|
|
526
|
+
{
|
|
527
|
+
type: 'ClassifyQuotaEmail',
|
|
528
|
+
label: 'Classify Quota Email',
|
|
529
|
+
description: 'Decides exceeded / warning / none for the 5-minute email sweep.',
|
|
530
|
+
simpleDescription: 'Decides whether to send a quota warning email',
|
|
531
|
+
category: 'anomaly-ops',
|
|
532
|
+
traceKind: 'compute',
|
|
533
|
+
inputSchema: {
|
|
534
|
+
fields: [
|
|
535
|
+
{ name: 'warn80', type: 'boolean', required: true },
|
|
536
|
+
{ name: 'overQuota', type: 'boolean', required: true },
|
|
537
|
+
],
|
|
538
|
+
},
|
|
539
|
+
configSchema: {
|
|
540
|
+
fields: [
|
|
541
|
+
{ name: 'alreadyWarned', type: 'boolean' },
|
|
542
|
+
{ name: 'alreadyNotifiedExceeded', type: 'boolean' },
|
|
543
|
+
],
|
|
544
|
+
},
|
|
545
|
+
outputSchema: {
|
|
546
|
+
fields: [
|
|
547
|
+
{ name: 'action', type: 'enum', enumValues: ['exceeded', 'warning', 'none'] },
|
|
548
|
+
{ name: 'reason', type: 'string' },
|
|
549
|
+
],
|
|
550
|
+
},
|
|
551
|
+
},
|
|
552
|
+
async (ctx) => {
|
|
553
|
+
const overQuota = Boolean(ctx.input.overQuota);
|
|
554
|
+
const warn80 = Boolean(ctx.input.warn80);
|
|
555
|
+
const warnedAt = Boolean(ctx.config.alreadyWarned);
|
|
556
|
+
const exceededNotifiedAt = Boolean(ctx.config.alreadyNotifiedExceeded);
|
|
557
|
+
let action: 'exceeded' | 'warning' | 'none' = 'none';
|
|
558
|
+
let reason = 'below warning threshold or already notified';
|
|
559
|
+
if (overQuota && !exceededNotifiedAt) {
|
|
560
|
+
action = 'exceeded';
|
|
561
|
+
reason = 'over quota and exceeded email not yet sent';
|
|
562
|
+
} else if (warn80 && !overQuota && !warnedAt) {
|
|
563
|
+
action = 'warning';
|
|
564
|
+
reason = 'past 80% of quota and warning email not yet sent';
|
|
565
|
+
}
|
|
566
|
+
ctx.log('info', `Email action: ${action} — ${reason}`);
|
|
567
|
+
return { action, reason };
|
|
568
|
+
},
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
// ── API keys ─────────────────────────────────────────────────────
|
|
572
|
+
|
|
573
|
+
registry.register(
|
|
574
|
+
{
|
|
575
|
+
type: 'GenerateApiKey',
|
|
576
|
+
label: 'Generate API Key',
|
|
577
|
+
description: 'Issues an ef_ak_ key, SHA-256 hashed at rest, 15-char display prefix.',
|
|
578
|
+
simpleDescription: 'Creates a new API key',
|
|
579
|
+
category: 'anomaly-ops',
|
|
580
|
+
traceKind: 'compute',
|
|
581
|
+
configSchema: {
|
|
582
|
+
fields: [{ name: 'label', type: 'string' }],
|
|
583
|
+
},
|
|
584
|
+
outputSchema: {
|
|
585
|
+
fields: [
|
|
586
|
+
{ name: 'rawKey', type: 'string' },
|
|
587
|
+
{ name: 'keyPrefix', type: 'string' },
|
|
588
|
+
{ name: 'hash', type: 'string' },
|
|
589
|
+
{ name: 'label', type: 'string' },
|
|
590
|
+
],
|
|
591
|
+
},
|
|
592
|
+
},
|
|
593
|
+
async (ctx) => {
|
|
594
|
+
const bytes = new Uint8Array(24);
|
|
595
|
+
crypto.getRandomValues(bytes);
|
|
596
|
+
const rawKey = `ef_ak_${Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('')}`;
|
|
597
|
+
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(rawKey));
|
|
598
|
+
const hash = Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, '0')).join('');
|
|
599
|
+
const keyPrefix = rawKey.slice(0, 15);
|
|
600
|
+
ctx.log('info', `Issued key ${keyPrefix}… (raw key shown once; only the hash is stored)`);
|
|
601
|
+
return { rawKey, keyPrefix, hash, label: String(ctx.config.label ?? 'default') };
|
|
602
|
+
},
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
registry.register(
|
|
606
|
+
{
|
|
607
|
+
type: 'EnforceKeyLimit',
|
|
608
|
+
label: 'Enforce Key Limit',
|
|
609
|
+
description: 'Rejects issuance when the user already has 5 active keys (set activeKeyCount to 5 to see the failure path).',
|
|
610
|
+
simpleDescription: 'Blocks new keys once the limit is reached',
|
|
611
|
+
category: 'anomaly-ops',
|
|
612
|
+
traceKind: 'compute',
|
|
613
|
+
inputSchema: {
|
|
614
|
+
fields: [{ name: 'keyPrefix', type: 'string', required: true }],
|
|
615
|
+
},
|
|
616
|
+
configSchema: {
|
|
617
|
+
fields: [{ name: 'activeKeyCount', type: 'number', required: true }],
|
|
618
|
+
},
|
|
619
|
+
outputSchema: {
|
|
620
|
+
fields: [
|
|
621
|
+
{ name: 'allowed', type: 'boolean' },
|
|
622
|
+
{ name: 'keyPrefix', type: 'string' },
|
|
623
|
+
{ name: 'remainingSlots', type: 'number' },
|
|
624
|
+
],
|
|
625
|
+
},
|
|
626
|
+
},
|
|
627
|
+
async (ctx) => {
|
|
628
|
+
const activeKeyCount = Number(ctx.config.activeKeyCount ?? 0);
|
|
629
|
+
if (activeKeyCount >= 5) {
|
|
630
|
+
throw new Error('KEY_LIMIT_REACHED: user already has 5 active keys');
|
|
631
|
+
}
|
|
632
|
+
const remainingSlots = 5 - activeKeyCount - 1;
|
|
633
|
+
ctx.log('info', `Key ${String(ctx.input.keyPrefix)}… accepted (${remainingSlots} slots left)`);
|
|
634
|
+
return { allowed: true, keyPrefix: ctx.input.keyPrefix, remainingSlots };
|
|
635
|
+
},
|
|
636
|
+
);
|
|
637
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createDefaultRegistry } from './index';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Validation-layer backstop (design doc "Safe-mode enforcement", layer 3): any
|
|
6
|
+
* node implementation whose source references the shared write primitives
|
|
7
|
+
* (goTransaction, raw INSERT/UPDATE/DELETE SQL, or the assertEffectsAllowed
|
|
8
|
+
* backstop) must declare `effects: 'mutation'` — this catches drift where a
|
|
9
|
+
* node grows a write path but the declaration is forgotten. Heuristic, not
|
|
10
|
+
* exhaustive: a node could write through some other, unmatched mechanism.
|
|
11
|
+
*/
|
|
12
|
+
const WRITE_PATTERN = /goTransaction|INSERT\s|UPDATE\s|DELETE\s|assertEffectsAllowed/i;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Nodes whose implementation source matches WRITE_PATTERN for a reason other
|
|
16
|
+
* than performing a write (e.g. a variable name that happens to contain
|
|
17
|
+
* "Update " once concatenated, or a write pattern appearing only inside a
|
|
18
|
+
* string built for a *different*, already-covered node). Empty today —
|
|
19
|
+
* every node currently caught by the pattern is a genuine write node with
|
|
20
|
+
* `effects: 'mutation'` declared. Add entries here only after inspecting the
|
|
21
|
+
* matched source and confirming it isn't a real write.
|
|
22
|
+
*/
|
|
23
|
+
const FALSE_POSITIVES = new Set<string>([]);
|
|
24
|
+
|
|
25
|
+
describe('effects-scan: node implementations that look like writes declare effects: mutation', () => {
|
|
26
|
+
const registry = createDefaultRegistry(0);
|
|
27
|
+
const definitions = registry.list();
|
|
28
|
+
|
|
29
|
+
it('found at least one node to scan (registry is populated)', () => {
|
|
30
|
+
expect(definitions.length).toBeGreaterThan(0);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
for (const definition of definitions) {
|
|
34
|
+
const { implementation } = registry.get(definition.type);
|
|
35
|
+
const source = implementation.toString();
|
|
36
|
+
if (!WRITE_PATTERN.test(source)) continue;
|
|
37
|
+
|
|
38
|
+
it(`${definition.type}: matches the write pattern, so it must declare effects: 'mutation'`, () => {
|
|
39
|
+
if (FALSE_POSITIVES.has(definition.type)) return;
|
|
40
|
+
expect(definition.effects).toBe('mutation');
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
});
|