space-data-module-sdk 0.8.4 → 0.8.6
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/README.md +117 -20
- package/bin/space-data-module.js +263 -0
- package/docs/.nojekyll +1 -0
- package/docs/AGENTS.md +34 -0
- package/docs/architecture.svg +141 -0
- package/docs/browser-wasmedge-isomorphic.md +179 -0
- package/docs/flatsql-streaming-standard.md +341 -0
- package/docs/gpu-module-abi.md +193 -0
- package/docs/index.html +125 -0
- package/docs/isomorphic-pthreads.md +188 -0
- package/docs/isomorphic-sdn-runtime-plan.md +167 -0
- package/docs/language-runtime-matrix.md +82 -0
- package/docs/manifest-structure.svg +114 -0
- package/docs/module-bundle-runtime-plan.md +19 -0
- package/docs/module-lifecycle.svg +70 -0
- package/docs/module-publication-standard.md +540 -0
- package/docs/node-red-default-node-parity.md +88 -0
- package/docs/protocol-installation.md +196 -0
- package/docs/secrets-capability.md +152 -0
- package/docs/styles.css +288 -0
- package/docs/testing-harness.md +408 -0
- package/docs/tri-runtime-parity.md +132 -0
- package/docs/wallet-callback.html +2 -0
- package/package.json +25 -6
- package/schemas/HttpRequestAbi.fbs +41 -0
- package/schemas/HttpResponseAbi.fbs +36 -0
- package/schemas/PluginManifest.fbs +2 -1
- package/src/AGENTS.md +1 -1
- package/src/app/codec.js +548 -0
- package/src/app/index.js +6 -0
- package/src/browser.js +5 -0
- package/src/bundle/constants.js +1 -0
- package/src/bundle/index.js +15 -0
- package/src/bundle/sigdomain.js +179 -0
- package/src/bundle/signing.js +767 -0
- package/src/bundle/wasm.js +3 -2
- package/src/capabilities.js +3 -0
- package/src/compat/index.js +7 -0
- package/src/compat/sdnLegacy.js +449 -0
- package/src/compiler/compileModule.js +556 -55
- package/src/compiler/flatcSupport.js +41 -27
- package/src/compiler/index.d.ts +2 -0
- package/src/compiler/index.js +18 -0
- package/src/compiler/invokeGlue.js +1212 -183
- package/src/compiler/pthreadArtifactGuard.js +910 -0
- package/src/compiler/sdnShimGenerator.js +323 -0
- package/src/compiler/wasiThreadsToolchain.js +150 -0
- package/src/compliance/index.js +11 -1
- package/src/compliance/legacyWildcardPorts.js +228 -0
- package/src/compliance/pluginCompliance.js +1000 -96
- package/src/field-stream/index.d.ts +31 -0
- package/src/field-stream/index.js +119 -0
- package/src/flow/dependencyStreamBridge.js +234 -0
- package/src/flow/flatsqlLinkShim.js +304 -0
- package/src/flow/flowCodec.js +107 -0
- package/src/flow/flowCompiler.js +2900 -0
- package/src/flow/flowRuntimeHost.js +877 -0
- package/src/flow/index.d.ts +117 -0
- package/src/flow/index.js +31 -0
- package/src/flow/isomorphicFlowHost.js +384 -0
- package/src/flow/jsFlowRuntime.js +275 -0
- package/src/flow/normalize.js +294 -0
- package/src/flow/runtime-src/flow_runtime.cpp +1699 -0
- package/src/flow/vendor/sdn-flow/MethodRegistry.js +400 -0
- package/src/flow/vendor/sdn-flow/constants.js +39 -0
- package/src/flow/vendor/sdn-flow/index.js +29 -0
- package/src/flow/vendor/sdn-flow/normalize.js +506 -0
- package/src/flow/vendor/sdn-flow/wasmCompatibility.js +189 -0
- package/src/generated/http/cpp/HttpRequestAbi_generated.h +285 -0
- package/src/generated/http/cpp/HttpResponseAbi_generated.h +192 -0
- package/src/generated/http/sdn/http/http-header.js +53 -0
- package/src/generated/http/sdn/http/http-header.ts +67 -0
- package/src/generated/http/sdn/http/http-request.js +132 -0
- package/src/generated/http/sdn/http/http-request.ts +179 -0
- package/src/generated/http/sdn/http/http-response.js +144 -0
- package/src/generated/http/sdn/http/http-response.ts +168 -0
- package/src/generated/http/sdn/http.js +11 -0
- package/src/generated/http/sdn/http.ts +4 -0
- package/src/generated/orbpro/flow/backpressure-policy.d.ts +12 -0
- package/src/generated/orbpro/flow/backpressure-policy.d.ts.map +1 -0
- package/src/generated/orbpro/flow/backpressure-policy.js +17 -0
- package/src/generated/orbpro/flow/backpressure-policy.js.map +1 -0
- package/src/generated/orbpro/flow/backpressure-policy.ts +15 -0
- package/src/generated/orbpro/flow/flow-bundle-manifest.d.ts +142 -0
- package/src/generated/orbpro/flow/flow-bundle-manifest.d.ts.map +1 -0
- package/src/generated/orbpro/flow/flow-bundle-manifest.js +304 -0
- package/src/generated/orbpro/flow/flow-bundle-manifest.js.map +1 -0
- package/src/generated/orbpro/flow/flow-bundle-manifest.ts +399 -0
- package/src/generated/orbpro/flow/flow-edge.d.ts +113 -0
- package/src/generated/orbpro/flow/flow-edge.d.ts.map +1 -0
- package/src/generated/orbpro/flow/flow-edge.js +221 -0
- package/src/generated/orbpro/flow/flow-edge.js.map +1 -0
- package/src/generated/orbpro/flow/flow-edge.ts +292 -0
- package/src/generated/orbpro/flow/flow-node.d.ts +80 -0
- package/src/generated/orbpro/flow/flow-node.d.ts.map +1 -0
- package/src/generated/orbpro/flow/flow-node.js +158 -0
- package/src/generated/orbpro/flow/flow-node.js.map +1 -0
- package/src/generated/orbpro/flow/flow-node.ts +202 -0
- package/src/generated/orbpro/flow/flow-program.d.ts +164 -0
- package/src/generated/orbpro/flow/flow-program.d.ts.map +1 -0
- package/src/generated/orbpro/flow/flow-program.js +350 -0
- package/src/generated/orbpro/flow/flow-program.js.map +1 -0
- package/src/generated/orbpro/flow/flow-program.ts +477 -0
- package/src/generated/orbpro/flow/flow-trigger.d.ts +108 -0
- package/src/generated/orbpro/flow/flow-trigger.d.ts.map +1 -0
- package/src/generated/orbpro/flow/flow-trigger.js +207 -0
- package/src/generated/orbpro/flow/flow-trigger.js.map +1 -0
- package/src/generated/orbpro/flow/flow-trigger.ts +276 -0
- package/src/generated/orbpro/flow/node-kind.d.ts +13 -0
- package/src/generated/orbpro/flow/node-kind.d.ts.map +1 -0
- package/src/generated/orbpro/flow/node-kind.js +16 -0
- package/src/generated/orbpro/flow/node-kind.js.map +1 -0
- package/src/generated/orbpro/{invoke.ts → flow/node-kind.ts} +12 -2
- package/src/generated/orbpro/flow/plugin-version-binding.d.ts +52 -0
- package/src/generated/orbpro/flow/plugin-version-binding.d.ts.map +1 -0
- package/src/generated/orbpro/flow/plugin-version-binding.js +85 -0
- package/src/generated/orbpro/flow/plugin-version-binding.js.map +1 -0
- package/src/generated/orbpro/flow/plugin-version-binding.ts +121 -0
- package/src/generated/orbpro/flow/schema-hash-binding.d.ts +70 -0
- package/src/generated/orbpro/flow/schema-hash-binding.d.ts.map +1 -0
- package/src/generated/orbpro/flow/schema-hash-binding.js +143 -0
- package/src/generated/orbpro/flow/schema-hash-binding.js.map +1 -0
- package/src/generated/orbpro/flow/schema-hash-binding.ts +189 -0
- package/src/generated/orbpro/flow/trigger-binding.d.ts +71 -0
- package/src/generated/orbpro/flow/trigger-binding.d.ts.map +1 -0
- package/src/generated/orbpro/flow/trigger-binding.js +142 -0
- package/src/generated/orbpro/flow/trigger-binding.js.map +1 -0
- package/src/generated/orbpro/flow/trigger-binding.ts +181 -0
- package/src/generated/orbpro/flow/trigger-kind.d.ts +14 -0
- package/src/generated/orbpro/flow/trigger-kind.d.ts.map +1 -0
- package/src/generated/orbpro/flow/trigger-kind.js +17 -0
- package/src/generated/orbpro/flow/trigger-kind.js.map +1 -0
- package/src/generated/orbpro/flow/trigger-kind.ts +17 -0
- package/src/generated/orbpro/flow.d.ts +21 -0
- package/src/generated/orbpro/flow.d.ts.map +1 -0
- package/src/generated/orbpro/flow.js +23 -0
- package/src/generated/orbpro/flow.js.map +1 -0
- package/src/generated/orbpro/flow.ts +24 -0
- package/src/generated/orbpro/manifest/capability-kind.d.ts +2 -1
- package/src/generated/orbpro/manifest/capability-kind.js +1 -0
- package/src/generated/orbpro/manifest/capability-kind.ts +1 -0
- package/src/generated/orbpro/plugin/stream-invoke-request.d.ts +93 -0
- package/src/generated/orbpro/plugin/stream-invoke-request.js +160 -0
- package/src/generated/orbpro/plugin/stream-invoke-request.ts +211 -0
- package/src/generated/orbpro/plugin/stream-invoke-response.d.ts +99 -0
- package/src/generated/orbpro/plugin/stream-invoke-response.js +175 -0
- package/src/generated/orbpro/plugin/stream-invoke-response.ts +226 -0
- package/src/generated/orbpro/plugin.js +8 -0
- package/src/generated/orbpro/stream.d.ts +12 -0
- package/src/generated/orbpro/stream.js +14 -0
- package/src/generated/orbpro/stream.ts +15 -0
- package/src/generated/spacedatastandards/plg/EntryFunction.d.ts +52 -0
- package/src/generated/spacedatastandards/plg/EntryFunction.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/EntryFunction.js +106 -0
- package/src/generated/spacedatastandards/plg/EntryFunction.ts +162 -0
- package/src/generated/spacedatastandards/plg/FlatBufferTypeRef.d.ts +90 -0
- package/src/generated/spacedatastandards/plg/FlatBufferTypeRef.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/FlatBufferTypeRef.js +189 -0
- package/src/generated/spacedatastandards/plg/FlatBufferTypeRef.ts +270 -0
- package/src/generated/spacedatastandards/plg/PLG.d.ts +492 -0
- package/src/generated/spacedatastandards/plg/PLG.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/PLG.js +1139 -0
- package/src/generated/spacedatastandards/plg/PLG.ts +1600 -0
- package/src/generated/spacedatastandards/plg/PLGAcceptedTypeSet.d.ts +63 -0
- package/src/generated/spacedatastandards/plg/PLGAcceptedTypeSet.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/PLGAcceptedTypeSet.js +138 -0
- package/src/generated/spacedatastandards/plg/PLGAcceptedTypeSet.ts +189 -0
- package/src/generated/spacedatastandards/plg/PLGBuildArtifact.d.ts +41 -0
- package/src/generated/spacedatastandards/plg/PLGBuildArtifact.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/PLGBuildArtifact.js +104 -0
- package/src/generated/spacedatastandards/plg/PLGBuildArtifact.ts +151 -0
- package/src/generated/spacedatastandards/plg/PLGFlowEdge.d.ts +68 -0
- package/src/generated/spacedatastandards/plg/PLGFlowEdge.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/PLGFlowEdge.js +124 -0
- package/src/generated/spacedatastandards/plg/PLGFlowEdge.ts +184 -0
- package/src/generated/spacedatastandards/plg/PLGFlowEdgeContract.d.ts +58 -0
- package/src/generated/spacedatastandards/plg/PLGFlowEdgeContract.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/PLGFlowEdgeContract.js +126 -0
- package/src/generated/spacedatastandards/plg/PLGFlowEdgeContract.ts +161 -0
- package/src/generated/spacedatastandards/plg/PLGFlowNode.d.ts +82 -0
- package/src/generated/spacedatastandards/plg/PLGFlowNode.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/PLGFlowNode.js +164 -0
- package/src/generated/spacedatastandards/plg/PLGFlowNode.ts +244 -0
- package/src/generated/spacedatastandards/plg/PLGFlowTrigger.d.ts +55 -0
- package/src/generated/spacedatastandards/plg/PLGFlowTrigger.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/PLGFlowTrigger.js +105 -0
- package/src/generated/spacedatastandards/plg/PLGFlowTrigger.ts +162 -0
- package/src/generated/spacedatastandards/plg/PLGFlowTriggerBinding.d.ts +42 -0
- package/src/generated/spacedatastandards/plg/PLGFlowTriggerBinding.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/PLGFlowTriggerBinding.js +83 -0
- package/src/generated/spacedatastandards/plg/PLGFlowTriggerBinding.ts +127 -0
- package/src/generated/spacedatastandards/plg/PLGHostCapability.d.ts +36 -0
- package/src/generated/spacedatastandards/plg/PLGHostCapability.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/PLGHostCapability.js +90 -0
- package/src/generated/spacedatastandards/plg/PLGHostCapability.ts +127 -0
- package/src/generated/spacedatastandards/plg/PLGMethodManifest.d.ts +53 -0
- package/src/generated/spacedatastandards/plg/PLGMethodManifest.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/PLGMethodManifest.js +153 -0
- package/src/generated/spacedatastandards/plg/PLGMethodManifest.ts +210 -0
- package/src/generated/spacedatastandards/plg/PLGPortManifest.d.ts +70 -0
- package/src/generated/spacedatastandards/plg/PLGPortManifest.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/PLGPortManifest.js +149 -0
- package/src/generated/spacedatastandards/plg/PLGPortManifest.ts +212 -0
- package/src/generated/spacedatastandards/plg/PLGProtocolSpec.d.ts +73 -0
- package/src/generated/spacedatastandards/plg/PLGProtocolSpec.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/PLGProtocolSpec.js +199 -0
- package/src/generated/spacedatastandards/plg/PLGProtocolSpec.ts +292 -0
- package/src/generated/spacedatastandards/plg/PLGTimerSpec.d.ts +40 -0
- package/src/generated/spacedatastandards/plg/PLGTimerSpec.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/PLGTimerSpec.js +103 -0
- package/src/generated/spacedatastandards/plg/PLGTimerSpec.ts +148 -0
- package/src/generated/spacedatastandards/plg/PluginCapability.d.ts +41 -0
- package/src/generated/spacedatastandards/plg/PluginCapability.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/PluginCapability.js +82 -0
- package/src/generated/spacedatastandards/plg/{plugin-capability.ts → PluginCapability.ts} +55 -18
- package/src/generated/spacedatastandards/plg/PluginDependency.d.ts +42 -0
- package/src/generated/spacedatastandards/plg/PluginDependency.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/PluginDependency.js +80 -0
- package/src/generated/spacedatastandards/plg/PluginDependency.ts +124 -0
- package/src/generated/spacedatastandards/plg/TAB.d.ts +85 -0
- package/src/generated/spacedatastandards/plg/TAB.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/TAB.js +173 -0
- package/src/generated/spacedatastandards/plg/TAB.ts +228 -0
- package/src/generated/spacedatastandards/plg/bufferMutability.d.ts +18 -0
- package/src/generated/spacedatastandards/plg/bufferMutability.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/bufferMutability.js +20 -0
- package/src/generated/spacedatastandards/plg/bufferMutability.ts +23 -0
- package/src/generated/spacedatastandards/plg/bufferOwnership.d.ts +18 -0
- package/src/generated/spacedatastandards/plg/bufferOwnership.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/bufferOwnership.js +20 -0
- package/src/generated/spacedatastandards/plg/bufferOwnership.ts +23 -0
- package/src/generated/spacedatastandards/plg/drainBehavior.d.ts +20 -0
- package/src/generated/spacedatastandards/plg/drainBehavior.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/drainBehavior.js +22 -0
- package/src/generated/spacedatastandards/plg/drainBehavior.ts +25 -0
- package/src/generated/spacedatastandards/plg/flowEdgeRoutePolicy.d.ts +10 -0
- package/src/generated/spacedatastandards/plg/flowEdgeRoutePolicy.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/flowEdgeRoutePolicy.js +12 -0
- package/src/generated/spacedatastandards/plg/flowEdgeRoutePolicy.ts +13 -0
- package/src/generated/spacedatastandards/plg/hostCapabilityKind.d.ts +51 -0
- package/src/generated/spacedatastandards/plg/hostCapabilityKind.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/hostCapabilityKind.js +53 -0
- package/src/generated/spacedatastandards/plg/hostCapabilityKind.ts +55 -0
- package/src/generated/spacedatastandards/plg/invokeSurfaceKind.d.ts +15 -0
- package/src/generated/spacedatastandards/plg/invokeSurfaceKind.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/invokeSurfaceKind.js +17 -0
- package/src/generated/spacedatastandards/plg/invokeSurfaceKind.ts +19 -0
- package/src/generated/spacedatastandards/plg/main.d.ts +29 -0
- package/src/generated/spacedatastandards/plg/main.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/main.js +30 -0
- package/src/generated/spacedatastandards/plg/main.ts +32 -0
- package/src/generated/spacedatastandards/plg/payloadWireFormat.d.ts +17 -0
- package/src/generated/spacedatastandards/plg/payloadWireFormat.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/payloadWireFormat.js +19 -0
- package/src/generated/spacedatastandards/plg/payloadWireFormat.ts +21 -0
- package/src/generated/spacedatastandards/plg/pluginCategory.d.ts +82 -0
- package/src/generated/spacedatastandards/plg/pluginCategory.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/pluginCategory.js +84 -0
- package/src/generated/spacedatastandards/plg/pluginCategory.ts +103 -0
- package/src/generated/spacedatastandards/plg/publicationState.d.ts +18 -0
- package/src/generated/spacedatastandards/plg/publicationState.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/publicationState.js +20 -0
- package/src/generated/spacedatastandards/plg/purchaseTier.d.ts +18 -0
- package/src/generated/spacedatastandards/plg/purchaseTier.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/purchaseTier.js +20 -0
- package/src/host/AGENTS.md +2 -2
- package/src/host/abi.js +53 -66
- package/src/host/browserHost.js +355 -8
- package/src/host/cpp/keyslotClient.hpp +231 -0
- package/src/host/cpp/secretsClient.hpp +583 -0
- package/src/host/hostcallWire.js +183 -0
- package/src/host/index.js +2 -0
- package/src/host/isomorphicLoader.js +44 -7
- package/src/host/nodeHost.js +227 -8
- package/src/host/sabHostcallChannel.js +206 -0
- package/src/host/timerDriver.js +203 -0
- package/src/host/wasiShim.js +41 -1
- package/src/host/wasiThreadBrowserWorker.mjs +48 -0
- package/src/host/wasiThreadHost.js +393 -0
- package/src/host/wasiThreadWorker.mjs +32 -0
- package/src/host/wasiThreadWorkerRuntime.js +93 -0
- package/src/http/index.js +339 -0
- package/src/index.d.ts +725 -11
- package/src/index.js +4 -0
- package/src/invoke/codec.js +1202 -128
- package/src/invoke/index.js +5 -1
- package/src/licensing/index.d.ts +2 -0
- package/src/licensing/index.js +2 -0
- package/src/licensing/records.js +287 -8
- package/src/manifest/codec.js +62 -14
- package/src/manifest/index.js +2 -0
- package/src/manifest/legacyToPlg.js +164 -25
- package/src/manifest/normalize.js +1 -0
- package/src/manifest/plgCodec.js +1303 -62
- package/src/manifest/typeRefs.js +162 -24
- package/src/runtime/compiledRuntimeAbi.js +47 -0
- package/src/runtime/compiledRuntimeAbi.json +33 -0
- package/src/runtime/index.d.ts +32 -0
- package/src/runtime/index.js +1 -0
- package/src/runtime-host/index.js +3 -0
- package/src/runtime-host/methodRegistry.js +55 -0
- package/src/standards/index.js +200 -56
- package/src/standards/sharedCatalog.js +16 -0
- package/src/testing/browserModuleHarness.js +604 -31
- package/src/testing/docker/wasmedge-parity.Dockerfile +39 -0
- package/src/testing/index.d.ts +151 -0
- package/src/testing/index.js +21 -0
- package/src/testing/native/wasmedge_emscripten_pthread_runner.c +25 -1
- package/src/testing/parityBrowserRunner.js +138 -0
- package/src/testing/parityHarness.js +616 -0
- package/src/testing/parityLanes.js +537 -0
- package/src/testing/processInvoke.js +29 -21
- package/src/testing/streamInvokeCodec.js +10 -175
- package/src/testing/wasmedgePin.json +6 -0
- package/src/testing/workerModuleHarness.js +242 -0
- package/src/testing/workerModuleHarnessWorker.js +176 -0
- package/src/transport/index.js +3 -0
- package/src/transport/pki.js +352 -14
- package/src/transport/records.js +16 -7
- package/src/utils/ecdsaDer.js +106 -0
- package/src/utils/wasmCrypto.js +0 -43
- package/templates/gpu-module/README.md +48 -0
- package/templates/gpu-module/include/space_data_gpu_abi.h +122 -0
- package/templates/gpu-module/manifest.json +84 -0
- package/schemas/PluginInvokeRequest.fbs +0 -18
- package/schemas/PluginInvokeResponse.fbs +0 -30
- package/schemas/spacedatastandards/PLG.fbs +0 -191
- package/src/generated/orbpro/invoke/plugin-invoke-request.d.ts +0 -51
- package/src/generated/orbpro/invoke/plugin-invoke-request.d.ts.map +0 -1
- package/src/generated/orbpro/invoke/plugin-invoke-request.js +0 -131
- package/src/generated/orbpro/invoke/plugin-invoke-request.js.map +0 -1
- package/src/generated/orbpro/invoke/plugin-invoke-request.ts +0 -173
- package/src/generated/orbpro/invoke/plugin-invoke-response.d.ts +0 -76
- package/src/generated/orbpro/invoke/plugin-invoke-response.d.ts.map +0 -1
- package/src/generated/orbpro/invoke/plugin-invoke-response.js +0 -184
- package/src/generated/orbpro/invoke/plugin-invoke-response.js.map +0 -1
- package/src/generated/orbpro/invoke/plugin-invoke-response.ts +0 -243
- package/src/generated/orbpro/invoke.d.ts +0 -3
- package/src/generated/orbpro/invoke.js +0 -5
- package/src/generated/spacedatastandards/plg/entry-function.js +0 -86
- package/src/generated/spacedatastandards/plg/entry-function.ts +0 -119
- package/src/generated/spacedatastandards/plg/index.js +0 -16
- package/src/generated/spacedatastandards/plg/index.ts +0 -11
- package/src/generated/spacedatastandards/plg/plg.js +0 -657
- package/src/generated/spacedatastandards/plg/plg.ts +0 -924
- package/src/generated/spacedatastandards/plg/plugin-capability.js +0 -66
- package/src/generated/spacedatastandards/plg/plugin-category.js +0 -15
- package/src/generated/spacedatastandards/plg/plugin-category.ts +0 -53
- package/src/generated/spacedatastandards/plg/plugin-dependency.js +0 -63
- package/src/generated/spacedatastandards/plg/plugin-dependency.ts +0 -86
- package/src/generated/spacedatastandards/plg/publication-state.js +0 -9
- package/src/generated/spacedatastandards/plg/purchase-tier.js +0 -9
- /package/src/generated/spacedatastandards/plg/{publication-state.ts → publicationState.ts} +0 -0
- /package/src/generated/spacedatastandards/plg/{purchase-tier.ts → purchaseTier.ts} +0 -0
|
@@ -0,0 +1,2900 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDN flow compiler (loop C.3c) — `space-data-module flow check|compile`.
|
|
3
|
+
*
|
|
4
|
+
* Compiles a *.flow.json FlowProgram document (the hosted-runtime fixture
|
|
5
|
+
* shape: programId/nodes/edges/triggers/triggerBindings/requiredPlugins)
|
|
6
|
+
* into a LINKED-DIRECT flow artifact:
|
|
7
|
+
*
|
|
8
|
+
* flow.json + dependency manifests
|
|
9
|
+
* -> validation (plugin/method existence, edge port type compatibility,
|
|
10
|
+
* trigger bindings, capability union)
|
|
11
|
+
* -> flow_generated.inc (descriptor tables + required-port readiness
|
|
12
|
+
* tables + prefixed guest-link entry dispatch)
|
|
13
|
+
* -> a thread-model-matched monolithic link of the SDK flow runtime template
|
|
14
|
+
* (src/flow/runtime-src/flow_runtime.cpp, the
|
|
15
|
+
* space_data_module_runtime_* ABI) with each dependency's PREFIXED
|
|
16
|
+
* guest-link wasm object (dist/guest-link/module-link.o, emitted by
|
|
17
|
+
* compileModuleFromSource) — ONE artifact, ONE linear memory
|
|
18
|
+
* -> a legal SDK module: plugin_get_manifest_flatbuffer/_size expose the
|
|
19
|
+
* flow's own $PLG manifest (capability union stamped), the
|
|
20
|
+
* flow_get_manifest_flatbuffer/_size pair exposes the encoded "FLOW"
|
|
21
|
+
* FlowProgram buffer, and the sds.manifest custom section carries the
|
|
22
|
+
* manifest bytes so `space-data-module check` passes.
|
|
23
|
+
*
|
|
24
|
+
* The emitted dist/ is loadable by BOTH flow hosts (sdn-server
|
|
25
|
+
* internal/flowrt and src/flow/flowRuntimeHost.js) and follows the
|
|
26
|
+
* FlowStore install triple (runtime.wasm + flow.json + artifact.json) plus
|
|
27
|
+
* the module layout (isomorphic/module.wasm + plugin-manifest.json).
|
|
28
|
+
*
|
|
29
|
+
* Single-thread guest sets use Emception (the same runWithEmceptionLock
|
|
30
|
+
* pipeline the module compiler uses). All-wasi-threads guest sets use the
|
|
31
|
+
* SDK's canonical wasm32-wasip1-threads toolchain and artifact guard. Mixing
|
|
32
|
+
* those object models is rejected before linking.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import os from "node:os";
|
|
36
|
+
import path from "node:path";
|
|
37
|
+
import { execFile } from "node:child_process";
|
|
38
|
+
import { fileURLToPath } from "node:url";
|
|
39
|
+
import { promisify } from "node:util";
|
|
40
|
+
import {
|
|
41
|
+
mkdir,
|
|
42
|
+
mkdtemp,
|
|
43
|
+
readFile,
|
|
44
|
+
readdir,
|
|
45
|
+
rm,
|
|
46
|
+
writeFile,
|
|
47
|
+
} from "node:fs/promises";
|
|
48
|
+
|
|
49
|
+
import { generateEmbeddedManifestSource } from "../embeddedManifest.js";
|
|
50
|
+
import { generateInvokeSupportHeader } from "../compiler/invokeGlue.js";
|
|
51
|
+
import { runWithEmceptionLock } from "../compiler/emceptionNode.js";
|
|
52
|
+
import {
|
|
53
|
+
assertPthreadArtifact,
|
|
54
|
+
assertPthreadFlagsPresent,
|
|
55
|
+
PTHREAD_FINAL_LINK_FLAGS,
|
|
56
|
+
} from "../compiler/pthreadArtifactGuard.js";
|
|
57
|
+
import { resolveWasiThreadsToolchain } from "../compiler/wasiThreadsToolchain.js";
|
|
58
|
+
import {
|
|
59
|
+
validateArtifactWithStandards,
|
|
60
|
+
validatePluginManifest,
|
|
61
|
+
} from "../compliance/index.js";
|
|
62
|
+
import {
|
|
63
|
+
alignedPayloadLayoutsCompatible,
|
|
64
|
+
encodePluginManifest,
|
|
65
|
+
getPayloadTypeWireFormat,
|
|
66
|
+
payloadSchemaIdentitiesEqual,
|
|
67
|
+
} from "../manifest/index.js";
|
|
68
|
+
import { appendWasmCustomSection } from "../bundle/wasm.js";
|
|
69
|
+
import { SDS_MANIFEST_SECTION_NAME } from "../bundle/constants.js";
|
|
70
|
+
import { verifyModuleArtifact } from "../bundle/signing.js";
|
|
71
|
+
import { DOMAIN_MODULE_PUBLICATION_V1 } from "../bundle/sigdomain.js";
|
|
72
|
+
import { sha256Bytes } from "../utils/crypto.js";
|
|
73
|
+
import { bytesToHex } from "../utils/encoding.js";
|
|
74
|
+
import { encodeFlowProgram } from "./flowCodec.js";
|
|
75
|
+
import { FLATSQL_LINK_SHIM_WASM } from "./flatsqlLinkShim.js";
|
|
76
|
+
import { normalizeManifestForSdnFlow } from "./normalize.js";
|
|
77
|
+
import {
|
|
78
|
+
BackpressurePolicy,
|
|
79
|
+
FlowEdgeT,
|
|
80
|
+
FlowNodeT,
|
|
81
|
+
FlowProgramT,
|
|
82
|
+
FlowTriggerT,
|
|
83
|
+
NodeKind,
|
|
84
|
+
TriggerBindingT,
|
|
85
|
+
TriggerKind,
|
|
86
|
+
} from "../generated/orbpro/flow.js";
|
|
87
|
+
import { DrainPolicy } from "../generated/orbpro/manifest.js";
|
|
88
|
+
|
|
89
|
+
const RUNTIME_TEMPLATE_PATH = fileURLToPath(
|
|
90
|
+
new URL("./runtime-src/flow_runtime.cpp", import.meta.url),
|
|
91
|
+
);
|
|
92
|
+
const execFileAsync = promisify(execFile);
|
|
93
|
+
|
|
94
|
+
const FLOW_THREAD_MODEL = Object.freeze({
|
|
95
|
+
SINGLE_THREAD: "single-thread",
|
|
96
|
+
WASI_THREADS: "wasi-threads",
|
|
97
|
+
});
|
|
98
|
+
const HISTORICAL_PTHREAD_THREAD_MODEL = "emscripten-pthreads";
|
|
99
|
+
// A guest built by the clang wasi toolchain that provably never spawns a
|
|
100
|
+
// thread. For LINKING purposes it is a wasi-threads object — same target, same
|
|
101
|
+
// object format, same ABI — so it unions with wasi-threads rather than
|
|
102
|
+
// conflicting with it. See normalizeGuestThreadModel.
|
|
103
|
+
const WASI_SEQUENTIAL_THREAD_MODEL = "wasi-sequential";
|
|
104
|
+
|
|
105
|
+
const RUNTIME_ABI_EXPORTS = [
|
|
106
|
+
"get_descriptor_abi_generation",
|
|
107
|
+
"get_node_descriptor_count",
|
|
108
|
+
"get_edge_descriptor_count",
|
|
109
|
+
"get_edge_descriptors",
|
|
110
|
+
"get_route_edge_descriptor_count",
|
|
111
|
+
"get_routing_state",
|
|
112
|
+
"get_current_invocation_generation",
|
|
113
|
+
"get_trigger_descriptor_count",
|
|
114
|
+
"get_dependency_descriptor_count",
|
|
115
|
+
"reset_state",
|
|
116
|
+
"get_ready_node_index",
|
|
117
|
+
"begin_node_invocation",
|
|
118
|
+
"get_current_invocation_descriptor",
|
|
119
|
+
"apply_node_invocation_result",
|
|
120
|
+
"complete_node_invocation",
|
|
121
|
+
"enqueue_trigger_frames",
|
|
122
|
+
"enqueue_trigger_frame",
|
|
123
|
+
"get_node_dispatch_descriptors",
|
|
124
|
+
"get_dependency_descriptors",
|
|
125
|
+
"get_node_states",
|
|
126
|
+
"get_ingress_states",
|
|
127
|
+
"dispatch_current_invocation_direct",
|
|
128
|
+
"drain_linked",
|
|
129
|
+
].map((name) => `space_data_module_runtime_${name}`);
|
|
130
|
+
|
|
131
|
+
const FLOW_ARTIFACT_EXPORTS = [
|
|
132
|
+
...RUNTIME_ABI_EXPORTS,
|
|
133
|
+
"malloc",
|
|
134
|
+
"free",
|
|
135
|
+
"plugin_get_manifest_flatbuffer",
|
|
136
|
+
"plugin_get_manifest_flatbuffer_size",
|
|
137
|
+
"flow_get_manifest_flatbuffer",
|
|
138
|
+
"flow_get_manifest_flatbuffer_size",
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
// Additional exports of LINKED-mode artifacts (flow.engineLinkage ==
|
|
142
|
+
// "flatsql"): the host wires the store's live engine db handle in and reads
|
|
143
|
+
// the engine body-reference table out (loop C.7 direct linkage).
|
|
144
|
+
const FLOW_LINKED_EXPORTS = [
|
|
145
|
+
"sdn_flatsql_link_init",
|
|
146
|
+
"sdn_flatsql_link_ref_table",
|
|
147
|
+
"sdn_flatsql_link_ref_slots",
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
// Capability stamped onto linked-mode flow manifests. Hosts treat it as the
|
|
151
|
+
// first-party gate: linking grants FULL store-memory access, so a host must
|
|
152
|
+
// refuse to mount a linked artifact unless it can (and is willing to) share
|
|
153
|
+
// its live engine instance. Untrusted modules never get this capability —
|
|
154
|
+
// they stay on the storage.flatsql_* hostcall bridge permanently.
|
|
155
|
+
export const ENGINE_LINK_CAPABILITY = "storage_engine_link";
|
|
156
|
+
|
|
157
|
+
export function flowEngineLinkage(flow) {
|
|
158
|
+
const linkage = String(flow?.engineLinkage ?? "").trim().toLowerCase();
|
|
159
|
+
if (linkage === "" || linkage === "none" || linkage === "bridge") return null;
|
|
160
|
+
throw new Error(
|
|
161
|
+
`flow.engineLinkage "${flow.engineLinkage}" is retired: FlatSQL must be an independently signed and instantiated isomorphic WASM node.`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const GUEST_LINK_OBJECT_FILENAME = "module-link.o";
|
|
166
|
+
const GUEST_LINK_METADATA_FILENAME = "metadata.json";
|
|
167
|
+
const GUEST_LINK_LINKED_DIRNAME = "guest-link-linked";
|
|
168
|
+
const SKIPPED_SCAN_DIRECTORIES = new Set([
|
|
169
|
+
"node_modules",
|
|
170
|
+
".git",
|
|
171
|
+
"dist",
|
|
172
|
+
"deps",
|
|
173
|
+
"third_party",
|
|
174
|
+
".emcache",
|
|
175
|
+
]);
|
|
176
|
+
|
|
177
|
+
function cIdent(value) {
|
|
178
|
+
return String(value ?? "").replace(/[^A-Za-z0-9_]/g, "_");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function cString(value) {
|
|
182
|
+
return JSON.stringify(String(value ?? ""));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function cNullableString(value) {
|
|
186
|
+
return value === undefined || value === null ? "nullptr" : cString(value);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function schemaHashBytes(value) {
|
|
190
|
+
if (value === undefined || value === null) return [];
|
|
191
|
+
if (typeof value === "string") {
|
|
192
|
+
const hex = value.startsWith("0x") ? value.slice(2) : value;
|
|
193
|
+
if (hex.length === 0) return [];
|
|
194
|
+
if (hex.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(hex)) {
|
|
195
|
+
throw new TypeError("Validated edge contract contains an invalid schemaHash.");
|
|
196
|
+
}
|
|
197
|
+
const bytes = [];
|
|
198
|
+
for (let index = 0; index < hex.length; index += 2) {
|
|
199
|
+
bytes.push(Number.parseInt(hex.slice(index, index + 2), 16));
|
|
200
|
+
}
|
|
201
|
+
return bytes;
|
|
202
|
+
}
|
|
203
|
+
if (Array.isArray(value) || value instanceof Uint8Array) {
|
|
204
|
+
return Array.from(value);
|
|
205
|
+
}
|
|
206
|
+
throw new TypeError("Validated edge contract contains an invalid schemaHash.");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function edgeLayoutScalar(value, label) {
|
|
210
|
+
if (value === undefined || value === null) {
|
|
211
|
+
return { present: false, value: 0 };
|
|
212
|
+
}
|
|
213
|
+
const numeric = Number(value);
|
|
214
|
+
if (!Number.isInteger(numeric) || numeric < 0 || numeric > 0xffffffff) {
|
|
215
|
+
throw new TypeError(`Validated edge contract contains an invalid ${label}.`);
|
|
216
|
+
}
|
|
217
|
+
return { present: true, value: numeric };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function pushIssue(issues, severity, code, message, location) {
|
|
221
|
+
issues.push({ severity, code, message, location });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function verifyIsomorphicNodeArtifacts({
|
|
225
|
+
flow,
|
|
226
|
+
check,
|
|
227
|
+
dependencies,
|
|
228
|
+
flowSourcePath,
|
|
229
|
+
}) {
|
|
230
|
+
const nodesById = new Map(
|
|
231
|
+
(flow?.nodes ?? []).map((node) => [node.nodeId, node]),
|
|
232
|
+
);
|
|
233
|
+
const verifiedByPluginId = new Map();
|
|
234
|
+
for (const checkedNode of check.nodes) {
|
|
235
|
+
if (checkedNode.dispatchModel !== "isomorphic") continue;
|
|
236
|
+
const node = nodesById.get(checkedNode.nodeId);
|
|
237
|
+
const dependency = dependencies.get(checkedNode.pluginId);
|
|
238
|
+
if (!node || !dependency) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
`Cannot verify isomorphic node "${checkedNode.nodeId}" without its resolved dependency.`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
const prior = verifiedByPluginId.get(checkedNode.pluginId);
|
|
244
|
+
if (prior) {
|
|
245
|
+
if (prior.sha256 !== node.artifact.sha256) {
|
|
246
|
+
throw new Error(
|
|
247
|
+
`Isomorphic plugin "${checkedNode.pluginId}" is bound to conflicting artifact hashes.`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const flowBase = flowSourcePath
|
|
254
|
+
? path.dirname(path.resolve(flowSourcePath))
|
|
255
|
+
: dependency.dir ?? process.cwd();
|
|
256
|
+
const artifactPath = path.isAbsolute(node.artifact.path)
|
|
257
|
+
? node.artifact.path
|
|
258
|
+
: path.resolve(flowBase, node.artifact.path);
|
|
259
|
+
const artifactBytes = dependency.artifactBytes
|
|
260
|
+
? new Uint8Array(dependency.artifactBytes)
|
|
261
|
+
: new Uint8Array(await readFile(artifactPath));
|
|
262
|
+
const exactSha256 = bytesToHex(await sha256Bytes(artifactBytes));
|
|
263
|
+
if (exactSha256 !== node.artifact.sha256) {
|
|
264
|
+
throw new Error(
|
|
265
|
+
`Isomorphic node "${checkedNode.nodeId}" artifact hash mismatch: expected ${node.artifact.sha256}, received ${exactSha256}.`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const publisherPath = path.isAbsolute(node.artifact.publisher)
|
|
270
|
+
? node.artifact.publisher
|
|
271
|
+
: path.resolve(flowBase, node.artifact.publisher);
|
|
272
|
+
const publisher = dependency.publisherRecord
|
|
273
|
+
? dependency.publisherRecord
|
|
274
|
+
: JSON.parse(await readFile(publisherPath, "utf8"));
|
|
275
|
+
if (publisher?.developmentOnly === true) {
|
|
276
|
+
throw new Error(
|
|
277
|
+
`Isomorphic node "${checkedNode.nodeId}" is signed only by a development publisher.`,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
if (!/^[0-9a-f]{64}$/.test(publisher?.publicKeyHex ?? "")) {
|
|
281
|
+
throw new Error(
|
|
282
|
+
`Isomorphic node "${checkedNode.nodeId}" publisher has no valid Ed25519 public key.`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
const verification = await verifyModuleArtifact(artifactBytes, {
|
|
286
|
+
trustedPublicKeys: [publisher.publicKeyHex],
|
|
287
|
+
requireSignature: true,
|
|
288
|
+
});
|
|
289
|
+
// WHAT THE FLOW LANE REQUIRES OF A SIGNATURE, and why it is two forms.
|
|
290
|
+
//
|
|
291
|
+
// Composition inlines somebody else's object code into a binary this
|
|
292
|
+
// process then ships. The signature has to bind, at minimum, everything
|
|
293
|
+
// that ends up compiled in. Two signatures do that, and they are NOT the
|
|
294
|
+
// same statement:
|
|
295
|
+
//
|
|
296
|
+
// - BUNDLE scope — binds the whole MBL: the canonical module hash, the
|
|
297
|
+
// manifest hash, and every non-signature member around it. The widest
|
|
298
|
+
// commitment available, and the only one that covers the surrounding
|
|
299
|
+
// bundle metadata.
|
|
300
|
+
//
|
|
301
|
+
// - MODULE scope UNDER THE MODULE-PUBLICATION DOMAIN — the node's own
|
|
302
|
+
// signing endpoint (sdn-server/internal/modulesign) issues
|
|
303
|
+
// `SDN-MODULE-PUBLICATION-V1 || 0x00 || sha256(portable)`. That covers
|
|
304
|
+
// the ENTIRE trailer-stripped payload — the code, the embedded
|
|
305
|
+
// manifest section, every sds.* section — which is strictly more of the
|
|
306
|
+
// compiled artifact than the canonical hash inside a bundle signature
|
|
307
|
+
// binds (the canonical hash strips sds.* before hashing). It does not
|
|
308
|
+
// bind the trailer metadata around it; the flow document's own
|
|
309
|
+
// artifact.sha256 check above already pinned the exact file bytes, so
|
|
310
|
+
// nothing the compiler consumes is left unbound.
|
|
311
|
+
//
|
|
312
|
+
// A BARE-DIGEST module signature (statementDomain absent, the pre-domain
|
|
313
|
+
// SDK form) is still refused here: no domain separation means a signature
|
|
314
|
+
// minted over some other protocol's digest can be replayed into this lane,
|
|
315
|
+
// and this lane compiles what it admits.
|
|
316
|
+
//
|
|
317
|
+
// Refusing module scope outright — which this did until 2026-07-30 — meant
|
|
318
|
+
// the flow compiler rejected EVERY artifact the node is able to sign, one
|
|
319
|
+
// line after verifying it, with a message that blamed the signature rather
|
|
320
|
+
// than the policy. See graph/tasks/sdk-flow-compiler-refuses-node-signed.md.
|
|
321
|
+
const domainSeparatedModuleScope =
|
|
322
|
+
verification.signatureScope === "module" &&
|
|
323
|
+
verification.statementDomain === DOMAIN_MODULE_PUBLICATION_V1;
|
|
324
|
+
if (
|
|
325
|
+
!verification.verified ||
|
|
326
|
+
(verification.signatureScope !== "bundle" && !domainSeparatedModuleScope)
|
|
327
|
+
) {
|
|
328
|
+
throw new Error(
|
|
329
|
+
`Isomorphic node "${checkedNode.nodeId}" artifact signature is not a verified ` +
|
|
330
|
+
`whole-bundle signature, nor a module signature under the ` +
|
|
331
|
+
`${DOMAIN_MODULE_PUBLICATION_V1} statement domain.`,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
verifiedByPluginId.set(checkedNode.pluginId, {
|
|
335
|
+
artifactBytes,
|
|
336
|
+
artifactPath,
|
|
337
|
+
publisher,
|
|
338
|
+
publisherPath,
|
|
339
|
+
sha256: exactSha256,
|
|
340
|
+
verification,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
return verifiedByPluginId;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
// Flow document + dependency loading
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
|
|
350
|
+
export async function loadFlowDocument(flowPath) {
|
|
351
|
+
const raw = await readFile(flowPath, "utf8");
|
|
352
|
+
const flow = JSON.parse(raw);
|
|
353
|
+
if (!flow || typeof flow !== "object" || Array.isArray(flow)) {
|
|
354
|
+
throw new Error(`Flow document ${flowPath} must be a JSON object.`);
|
|
355
|
+
}
|
|
356
|
+
return flow;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function loadDependencyFromDirectory(dir) {
|
|
360
|
+
const manifestPath = path.join(dir, "plugin-manifest.json");
|
|
361
|
+
let manifest;
|
|
362
|
+
try {
|
|
363
|
+
manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
364
|
+
} catch {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
if (!manifest?.pluginId) {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
const entry = {
|
|
371
|
+
pluginId: manifest.pluginId,
|
|
372
|
+
dir,
|
|
373
|
+
manifestPath,
|
|
374
|
+
manifest,
|
|
375
|
+
normalized: normalizeManifestForSdnFlow(manifest),
|
|
376
|
+
wasmPath: path.join(dir, "dist", "isomorphic", "module.wasm"),
|
|
377
|
+
guestLink: null,
|
|
378
|
+
};
|
|
379
|
+
const readGuestLink = async (dirname) => {
|
|
380
|
+
const guestLinkDir = path.join(dir, "dist", dirname);
|
|
381
|
+
const [objectBytes, metadata] = await Promise.all([
|
|
382
|
+
readFile(path.join(guestLinkDir, GUEST_LINK_OBJECT_FILENAME)),
|
|
383
|
+
readFile(path.join(guestLinkDir, GUEST_LINK_METADATA_FILENAME), "utf8").then(JSON.parse),
|
|
384
|
+
]);
|
|
385
|
+
return { objectBytes: new Uint8Array(objectBytes), metadata };
|
|
386
|
+
};
|
|
387
|
+
try {
|
|
388
|
+
entry.guestLink = await readGuestLink("guest-link");
|
|
389
|
+
} catch {
|
|
390
|
+
entry.guestLink = null;
|
|
391
|
+
}
|
|
392
|
+
// Optional engine-linked object variant (compiled -DSDN_FLATSQL_LINKED):
|
|
393
|
+
// used instead of guestLink when the flow compiles in linked mode.
|
|
394
|
+
try {
|
|
395
|
+
entry.guestLinkLinked = await readGuestLink(GUEST_LINK_LINKED_DIRNAME);
|
|
396
|
+
} catch {
|
|
397
|
+
entry.guestLinkLinked = null;
|
|
398
|
+
}
|
|
399
|
+
return entry;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function scanForDependencyDirs(root, found) {
|
|
403
|
+
let entries;
|
|
404
|
+
try {
|
|
405
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
406
|
+
} catch {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (entries.some((entry) => entry.isFile() && entry.name === "plugin-manifest.json")) {
|
|
410
|
+
found.push(root);
|
|
411
|
+
}
|
|
412
|
+
for (const entry of entries) {
|
|
413
|
+
if (!entry.isDirectory() || SKIPPED_SCAN_DIRECTORIES.has(entry.name)) {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
await scanForDependencyDirs(path.join(root, entry.name), found);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Resolve dependency module packages for a flow. `depsPath` is either a JSON
|
|
422
|
+
* file mapping pluginId -> package directory, or a directory root that is
|
|
423
|
+
* scanned recursively for `plugin-manifest.json` packages (dist/ trees are
|
|
424
|
+
* skipped; each package's dist/guest-link is picked up when present).
|
|
425
|
+
* Returns Map<pluginId, dependencyEntry>.
|
|
426
|
+
*/
|
|
427
|
+
export async function resolveFlowDependencies({ depsPath, cwd = process.cwd() } = {}) {
|
|
428
|
+
const dependencies = new Map();
|
|
429
|
+
if (!depsPath) {
|
|
430
|
+
return dependencies;
|
|
431
|
+
}
|
|
432
|
+
const resolved = path.resolve(cwd, depsPath);
|
|
433
|
+
const candidateDirs = [];
|
|
434
|
+
if (resolved.endsWith(".json")) {
|
|
435
|
+
const map = JSON.parse(await readFile(resolved, "utf8"));
|
|
436
|
+
for (const dir of Object.values(map)) {
|
|
437
|
+
candidateDirs.push(path.resolve(path.dirname(resolved), dir));
|
|
438
|
+
}
|
|
439
|
+
} else {
|
|
440
|
+
await scanForDependencyDirs(resolved, candidateDirs);
|
|
441
|
+
}
|
|
442
|
+
for (const dir of candidateDirs) {
|
|
443
|
+
const entry = await loadDependencyFromDirectory(dir);
|
|
444
|
+
if (entry && !dependencies.has(entry.pluginId)) {
|
|
445
|
+
dependencies.set(entry.pluginId, entry);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return dependencies;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ---------------------------------------------------------------------------
|
|
452
|
+
// Validation — flow check
|
|
453
|
+
// ---------------------------------------------------------------------------
|
|
454
|
+
|
|
455
|
+
function findMethod(normalizedManifest, methodId) {
|
|
456
|
+
return (normalizedManifest?.methods ?? []).find(
|
|
457
|
+
(method) => method.methodId === methodId,
|
|
458
|
+
) ?? null;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function portAcceptedTypes(port) {
|
|
462
|
+
const types = [];
|
|
463
|
+
for (const typeSet of port?.acceptedTypeSets ?? []) {
|
|
464
|
+
for (const type of typeSet?.allowedTypes ?? []) {
|
|
465
|
+
types.push(type);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return types;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function concreteTypesByWireFormat(types, wireFormat) {
|
|
472
|
+
return types.filter(
|
|
473
|
+
(typeRef) =>
|
|
474
|
+
typeRef?.acceptsAnyFlatbuffer !== true &&
|
|
475
|
+
getPayloadTypeWireFormat(typeRef) === wireFormat,
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function resolveEdgeTypeContract(fromTypes, toTypes) {
|
|
480
|
+
const fromCanonical = concreteTypesByWireFormat(fromTypes, "flatbuffer");
|
|
481
|
+
const toCanonical = concreteTypesByWireFormat(toTypes, "flatbuffer");
|
|
482
|
+
const fromAligned = concreteTypesByWireFormat(fromTypes, "aligned-binary");
|
|
483
|
+
const toAligned = concreteTypesByWireFormat(toTypes, "aligned-binary");
|
|
484
|
+
|
|
485
|
+
// WILDCARD PARTICIPATION — application-blind ports.
|
|
486
|
+
//
|
|
487
|
+
// A port declaring `acceptsAnyFlatbuffer` is generic ON PURPOSE.
|
|
488
|
+
// `hostcap/http-request`, `storage-ingest`, `clock`, `random`, `file` and the
|
|
489
|
+
// flow compiler's OWN synthesized egress ports (see wildcardPortDefinition,
|
|
490
|
+
// "for graph endpoints whose backing manifest port cannot be resolved")
|
|
491
|
+
// cannot name one concrete SDS identity, because they are generic by
|
|
492
|
+
// construction. 65 shipped manifests across every module family declare one.
|
|
493
|
+
//
|
|
494
|
+
// concreteTypesByWireFormat() deliberately filters wildcards out, so such a
|
|
495
|
+
// port has a NON-EMPTY `types` array but ZERO concrete entries. Without the
|
|
496
|
+
// cases below it falls past BOTH the `length === 0` host-boundary checks (its
|
|
497
|
+
// array is not empty) AND the concrete-vs-concrete loop (it has no concrete
|
|
498
|
+
// entries), landing on the null return that reads as the self-contradictory
|
|
499
|
+
// "* does not satisfy *" — a wildcard failing to satisfy a wildcard. That
|
|
500
|
+
// made the shipped, live-deployed celestrak ingest family (gp/satcat/spw +
|
|
501
|
+
// the publish-trigger variants) unbuildable: 36 errors on `flow check`.
|
|
502
|
+
//
|
|
503
|
+
// Resolved HERE, before the concrete paths, and ONLY when a side has no
|
|
504
|
+
// concrete alternative — a port declaring both a wildcard and a concrete
|
|
505
|
+
// identity still takes the strict path below, so nothing is widened for
|
|
506
|
+
// typed edges.
|
|
507
|
+
const isWildcardOnly = (types, canonical, aligned) =>
|
|
508
|
+
canonical.length === 0 &&
|
|
509
|
+
aligned.length === 0 &&
|
|
510
|
+
types.some((type) => type?.acceptsAnyFlatbuffer === true);
|
|
511
|
+
|
|
512
|
+
const fromWildcardOnly = isWildcardOnly(fromTypes, fromCanonical, fromAligned);
|
|
513
|
+
const toWildcardOnly = isWildcardOnly(toTypes, toCanonical, toAligned);
|
|
514
|
+
|
|
515
|
+
// A generic port meeting a genuinely UNTYPED host boundary (the flow egress
|
|
516
|
+
// sink declares no types at all, so it is neither concrete nor wildcard).
|
|
517
|
+
// Both sides are application-blind, so this is the same untyped byte edge as
|
|
518
|
+
// wildcard -> wildcard.
|
|
519
|
+
if (
|
|
520
|
+
(fromWildcardOnly && toTypes.length === 0) ||
|
|
521
|
+
(toWildcardOnly && fromTypes.length === 0)
|
|
522
|
+
) {
|
|
523
|
+
return {
|
|
524
|
+
schemaName: null,
|
|
525
|
+
fileIdentifier: null,
|
|
526
|
+
schemaVersion: null,
|
|
527
|
+
schemaHash: null,
|
|
528
|
+
rootTypeName: null,
|
|
529
|
+
compatibleWireFormats: ["flatbuffer"],
|
|
530
|
+
canonical: { producer: null, consumer: null },
|
|
531
|
+
aligned: null,
|
|
532
|
+
wildcard: true,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (fromWildcardOnly && toWildcardOnly) {
|
|
537
|
+
// Generic -> generic: a legitimate untyped byte edge (http-request ->
|
|
538
|
+
// flow egress). No canonical identity exists to record here, and inventing
|
|
539
|
+
// one would be a claim the runtime cannot honour.
|
|
540
|
+
return {
|
|
541
|
+
schemaName: null,
|
|
542
|
+
fileIdentifier: null,
|
|
543
|
+
schemaVersion: null,
|
|
544
|
+
schemaHash: null,
|
|
545
|
+
rootTypeName: null,
|
|
546
|
+
compatibleWireFormats: ["flatbuffer"],
|
|
547
|
+
canonical: { producer: null, consumer: null },
|
|
548
|
+
aligned: null,
|
|
549
|
+
wildcard: true,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
if (fromWildcardOnly && toCanonical.length === 1) {
|
|
553
|
+
// Generic producer into a typed consumer: the edge carries the CONSUMER's
|
|
554
|
+
// identity — exactly what the host-boundary case below does for an
|
|
555
|
+
// untyped side.
|
|
556
|
+
const canonical = toCanonical[0];
|
|
557
|
+
return {
|
|
558
|
+
schemaName: canonical.schemaName ?? null,
|
|
559
|
+
fileIdentifier: canonical.fileIdentifier ?? null,
|
|
560
|
+
schemaVersion: canonical.schemaVersion ?? null,
|
|
561
|
+
schemaHash: canonical.schemaHash ?? null,
|
|
562
|
+
rootTypeName: canonical.rootTypeName ?? null,
|
|
563
|
+
compatibleWireFormats: ["flatbuffer"],
|
|
564
|
+
canonical: { producer: null, consumer: canonical },
|
|
565
|
+
aligned: null,
|
|
566
|
+
wildcard: true,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
if (toWildcardOnly && fromCanonical.length === 1) {
|
|
570
|
+
// Typed producer into a generic consumer: the edge carries the PRODUCER's.
|
|
571
|
+
const canonical = fromCanonical[0];
|
|
572
|
+
return {
|
|
573
|
+
schemaName: canonical.schemaName ?? null,
|
|
574
|
+
fileIdentifier: canonical.fileIdentifier ?? null,
|
|
575
|
+
schemaVersion: canonical.schemaVersion ?? null,
|
|
576
|
+
schemaHash: canonical.schemaHash ?? null,
|
|
577
|
+
rootTypeName: canonical.rootTypeName ?? null,
|
|
578
|
+
compatibleWireFormats: ["flatbuffer"],
|
|
579
|
+
canonical: { producer: canonical, consumer: null },
|
|
580
|
+
aligned: null,
|
|
581
|
+
wildcard: true,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Host-model graph boundaries are not PLG ports. They remain
|
|
586
|
+
// application-blind byte ingress/egress and therefore support only the
|
|
587
|
+
// concrete canonical identity declared by the module-facing side.
|
|
588
|
+
if (fromTypes.length === 0 && toCanonical.length === 1) {
|
|
589
|
+
const canonical = toCanonical[0];
|
|
590
|
+
return {
|
|
591
|
+
schemaName: canonical.schemaName ?? null,
|
|
592
|
+
fileIdentifier: canonical.fileIdentifier ?? null,
|
|
593
|
+
schemaVersion: canonical.schemaVersion ?? null,
|
|
594
|
+
schemaHash: canonical.schemaHash ?? null,
|
|
595
|
+
rootTypeName: canonical.rootTypeName ?? null,
|
|
596
|
+
compatibleWireFormats: ["flatbuffer"],
|
|
597
|
+
canonical: { producer: null, consumer: canonical },
|
|
598
|
+
aligned: null,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
if (toTypes.length === 0 && fromCanonical.length === 1) {
|
|
602
|
+
const canonical = fromCanonical[0];
|
|
603
|
+
return {
|
|
604
|
+
schemaName: canonical.schemaName ?? null,
|
|
605
|
+
fileIdentifier: canonical.fileIdentifier ?? null,
|
|
606
|
+
schemaVersion: canonical.schemaVersion ?? null,
|
|
607
|
+
schemaHash: canonical.schemaHash ?? null,
|
|
608
|
+
rootTypeName: canonical.rootTypeName ?? null,
|
|
609
|
+
compatibleWireFormats: ["flatbuffer"],
|
|
610
|
+
canonical: { producer: canonical, consumer: null },
|
|
611
|
+
aligned: null,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
for (const producerCanonical of fromCanonical) {
|
|
616
|
+
const consumerCanonical = toCanonical.find((candidate) =>
|
|
617
|
+
payloadSchemaIdentitiesEqual(producerCanonical, candidate),
|
|
618
|
+
);
|
|
619
|
+
if (!consumerCanonical) continue;
|
|
620
|
+
|
|
621
|
+
const producerAligned = fromAligned.find((candidate) =>
|
|
622
|
+
payloadSchemaIdentitiesEqual(producerCanonical, candidate),
|
|
623
|
+
);
|
|
624
|
+
const consumerAligned = toAligned.find((candidate) =>
|
|
625
|
+
payloadSchemaIdentitiesEqual(consumerCanonical, candidate),
|
|
626
|
+
);
|
|
627
|
+
// THE ALIGNED PEER IS AN OPTIMISATION, NOT A PRECONDITION.
|
|
628
|
+
//
|
|
629
|
+
// This used to hard-error (`edge-missing-representation-pair`) whenever
|
|
630
|
+
// either port lacked an aligned-binary peer for the shared canonical
|
|
631
|
+
// identity — which made a plain canonical FlatBuffer edge ILLEGAL. It
|
|
632
|
+
// contradicted the compliance layer, which emits `no-aligned-peer` as a
|
|
633
|
+
// WARNING and says in as many words that having no peer "is correct for a
|
|
634
|
+
// variable-length record" and that the peer should be declared "only if the
|
|
635
|
+
// record has a FIXED aligned layout". A variable-length $CAQ query frame has
|
|
636
|
+
// no fixed layout and can never declare one, so under the old rule
|
|
637
|
+
// data-retrieval could not be compiled at all.
|
|
638
|
+
//
|
|
639
|
+
// The aligned representation only exists to offer the shared-arena zero-copy
|
|
640
|
+
// route. When BOTH ports declare it, the edge advertises it (and the layouts
|
|
641
|
+
// must then agree — that check is still fatal). When either side does not,
|
|
642
|
+
// the mutually available representation is the canonical FlatBuffer, which
|
|
643
|
+
// is exactly the "canonical fallback" the sibling diagnostics name.
|
|
644
|
+
const alignedPair = Boolean(producerAligned && consumerAligned);
|
|
645
|
+
if (
|
|
646
|
+
alignedPair &&
|
|
647
|
+
!alignedPayloadLayoutsCompatible(producerAligned, consumerAligned)
|
|
648
|
+
) {
|
|
649
|
+
return {
|
|
650
|
+
errorCode: "edge-aligned-layout-mismatch",
|
|
651
|
+
errorMessage:
|
|
652
|
+
`Aligned-binary peers for ${producerCanonical.schemaName ?? "the SDS type"} ` +
|
|
653
|
+
"declare incompatible byteLength, fixedStringLength, or requiredAlignment metadata.",
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return {
|
|
658
|
+
schemaName: producerCanonical.schemaName ?? null,
|
|
659
|
+
fileIdentifier: producerCanonical.fileIdentifier ?? null,
|
|
660
|
+
schemaVersion: producerCanonical.schemaVersion ?? null,
|
|
661
|
+
schemaHash: producerCanonical.schemaHash ?? null,
|
|
662
|
+
rootTypeName: producerCanonical.rootTypeName ?? null,
|
|
663
|
+
compatibleWireFormats: alignedPair
|
|
664
|
+
? ["flatbuffer", "aligned-binary"]
|
|
665
|
+
: ["flatbuffer"],
|
|
666
|
+
canonical: {
|
|
667
|
+
producer: producerCanonical,
|
|
668
|
+
consumer: consumerCanonical,
|
|
669
|
+
},
|
|
670
|
+
aligned: alignedPair
|
|
671
|
+
? { producer: producerAligned, consumer: consumerAligned }
|
|
672
|
+
: null,
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function describeTypes(types) {
|
|
679
|
+
if (types.length === 0) return "(untyped)";
|
|
680
|
+
return types
|
|
681
|
+
.map((type) =>
|
|
682
|
+
type.acceptsAnyFlatbuffer
|
|
683
|
+
? "*"
|
|
684
|
+
: `${type.schemaName ?? "?"}${type.fileIdentifier ? `/${type.fileIdentifier}` : ""}`,
|
|
685
|
+
)
|
|
686
|
+
.join("|");
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function isHostModelNode(node) {
|
|
690
|
+
const kind = String(node?.kind ?? "").toLowerCase();
|
|
691
|
+
return node?.dispatchModel === "host" || kind === "sink" || kind === "source";
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function requestedNodeDispatchModel(node) {
|
|
695
|
+
const requested = String(node?.dispatchModel ?? "").trim().toLowerCase();
|
|
696
|
+
return requested || null;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function validateIsomorphicArtifactLock(node, issues, location) {
|
|
700
|
+
const artifact = node?.artifact;
|
|
701
|
+
const valid =
|
|
702
|
+
artifact &&
|
|
703
|
+
typeof artifact === "object" &&
|
|
704
|
+
!Array.isArray(artifact) &&
|
|
705
|
+
typeof artifact.path === "string" &&
|
|
706
|
+
artifact.path.trim().length > 0 &&
|
|
707
|
+
typeof artifact.publisher === "string" &&
|
|
708
|
+
artifact.publisher.trim().length > 0 &&
|
|
709
|
+
typeof artifact.sha256 === "string" &&
|
|
710
|
+
/^[0-9a-f]{64}$/.test(artifact.sha256);
|
|
711
|
+
if (valid) return;
|
|
712
|
+
pushIssue(
|
|
713
|
+
issues,
|
|
714
|
+
"error",
|
|
715
|
+
"missing-isomorphic-artifact-lock",
|
|
716
|
+
`Isomorphic node "${node?.nodeId ?? "?"}" must bind artifact.path, artifact.publisher, and an exact lowercase SHA-256 digest.`,
|
|
717
|
+
`${location}.artifact`,
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function capabilityId(capability) {
|
|
722
|
+
if (typeof capability === "string") return capability;
|
|
723
|
+
return capability?.capability ?? null;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function normalizedCapabilityIds(capabilities) {
|
|
727
|
+
const ids = [];
|
|
728
|
+
const seen = new Set();
|
|
729
|
+
for (const capability of Array.isArray(capabilities) ? capabilities : []) {
|
|
730
|
+
const id = String(capabilityId(capability) ?? "").trim();
|
|
731
|
+
if (!id || seen.has(id)) continue;
|
|
732
|
+
seen.add(id);
|
|
733
|
+
ids.push(id);
|
|
734
|
+
}
|
|
735
|
+
return ids;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function selectedGuestLink(dependency, engineLinked) {
|
|
739
|
+
if (!dependency) return null;
|
|
740
|
+
return engineLinked
|
|
741
|
+
? (dependency.guestLinkLinked ?? dependency.guestLink ?? null)
|
|
742
|
+
: (dependency.guestLink ?? null);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function normalizeGuestThreadModel(value) {
|
|
746
|
+
if (value === undefined || value === null || String(value).trim() === "") {
|
|
747
|
+
return FLOW_THREAD_MODEL.SINGLE_THREAD;
|
|
748
|
+
}
|
|
749
|
+
const normalized = String(value).trim().toLowerCase();
|
|
750
|
+
if (normalized === FLOW_THREAD_MODEL.SINGLE_THREAD) {
|
|
751
|
+
return FLOW_THREAD_MODEL.SINGLE_THREAD;
|
|
752
|
+
}
|
|
753
|
+
if (
|
|
754
|
+
normalized === FLOW_THREAD_MODEL.WASI_THREADS ||
|
|
755
|
+
normalized === HISTORICAL_PTHREAD_THREAD_MODEL ||
|
|
756
|
+
// wasi-sequential collapses to wasi-threads HERE, at the linking layer,
|
|
757
|
+
// and only here. The distinction is a property of a guest (may it spawn?),
|
|
758
|
+
// not of the object format: both are clang wasm32-wasip1-threads objects
|
|
759
|
+
// and link together cleanly. Keeping them separate at this layer would
|
|
760
|
+
// make a flow of one sequential node fall through to the emception branch
|
|
761
|
+
// and be linked by emcc — exactly the toolchain drift this model exists to
|
|
762
|
+
// prevent. The per-guest guarantee is enforced where it belongs: on each
|
|
763
|
+
// MODULE artifact, by assertSequentialArtifact.
|
|
764
|
+
normalized === WASI_SEQUENTIAL_THREAD_MODEL
|
|
765
|
+
) {
|
|
766
|
+
return FLOW_THREAD_MODEL.WASI_THREADS;
|
|
767
|
+
}
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function resolveNodeCapabilitySlice(entry, guestLink) {
|
|
772
|
+
if (Array.isArray(entry.node?.capabilities)) {
|
|
773
|
+
return {
|
|
774
|
+
capabilities: normalizedCapabilityIds(entry.node.capabilities),
|
|
775
|
+
source: "node",
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
const guestCapabilities = guestLink?.metadata?.capabilities ?? guestLink?.capabilities;
|
|
779
|
+
if (Array.isArray(guestCapabilities)) {
|
|
780
|
+
return {
|
|
781
|
+
capabilities: normalizedCapabilityIds(guestCapabilities),
|
|
782
|
+
source: "guest-link",
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
return {
|
|
786
|
+
capabilities: normalizedCapabilityIds(entry.dependency?.manifest?.capabilities),
|
|
787
|
+
source: "manifest",
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// A module manifest's own DEPENDENCIES entries (PLG PluginDependency:
|
|
792
|
+
// PLUGIN_ID / MIN_VERSION / MAX_VERSION) — component dependencies (linked
|
|
793
|
+
// libraries such as the flatsql engine module), NOT graph nodes.
|
|
794
|
+
function normalizeManifestDependency(entry) {
|
|
795
|
+
if (typeof entry === "string") {
|
|
796
|
+
return entry.trim() ? { pluginId: entry.trim(), minVersion: null, maxVersion: null } : null;
|
|
797
|
+
}
|
|
798
|
+
const pluginId =
|
|
799
|
+
entry?.pluginId ?? entry?.PLUGIN_ID ?? entry?.plugin_id ?? null;
|
|
800
|
+
if (!pluginId || typeof pluginId !== "string") {
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
803
|
+
return {
|
|
804
|
+
pluginId,
|
|
805
|
+
minVersion:
|
|
806
|
+
entry?.minVersion ?? entry?.MIN_VERSION ?? entry?.min_version ?? entry?.version ?? null,
|
|
807
|
+
maxVersion: entry?.maxVersion ?? entry?.MAX_VERSION ?? entry?.max_version ?? null,
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function manifestDependencies(manifest) {
|
|
812
|
+
const raw = manifest?.dependencies ?? manifest?.DEPENDENCIES ?? [];
|
|
813
|
+
return (Array.isArray(raw) ? raw : [])
|
|
814
|
+
.map(normalizeManifestDependency)
|
|
815
|
+
.filter(Boolean);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Walk node modules' manifest DEPENDENCIES transitively and collect the
|
|
820
|
+
* flow's COMPONENT dependency set (deduped, version-bound). Resolvable
|
|
821
|
+
* components contribute their declared capabilities to the union; components
|
|
822
|
+
* that cannot be resolved from the dependency set are reported as warnings
|
|
823
|
+
* but still propagated into the emitted bundle's DEPENDENCIES.
|
|
824
|
+
*/
|
|
825
|
+
function collectComponentDependencies({ nodePluginIds, dependencies, issues, capabilities }) {
|
|
826
|
+
const components = new Map();
|
|
827
|
+
const visited = new Set(nodePluginIds);
|
|
828
|
+
const queue = [];
|
|
829
|
+
for (const pluginId of nodePluginIds) {
|
|
830
|
+
const dependency = dependencies.get(pluginId);
|
|
831
|
+
for (const declared of manifestDependencies(dependency?.manifest)) {
|
|
832
|
+
queue.push({ declared, declaredBy: pluginId });
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
while (queue.length > 0) {
|
|
836
|
+
const { declared, declaredBy } = queue.shift();
|
|
837
|
+
const existing = components.get(declared.pluginId);
|
|
838
|
+
if (existing) {
|
|
839
|
+
if (
|
|
840
|
+
declared.minVersion &&
|
|
841
|
+
existing.minVersion &&
|
|
842
|
+
declared.minVersion !== existing.minVersion
|
|
843
|
+
) {
|
|
844
|
+
pushIssue(
|
|
845
|
+
issues,
|
|
846
|
+
"warning",
|
|
847
|
+
"component-version-conflict",
|
|
848
|
+
`Component dependency "${declared.pluginId}" is declared with conflicting version bindings ` +
|
|
849
|
+
`("${existing.minVersion}" by ${existing.declaredBy.join(", ")} vs "${declared.minVersion}" by ${declaredBy}); ` +
|
|
850
|
+
`keeping "${existing.minVersion}".`,
|
|
851
|
+
"flow.dependencies",
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
if (!existing.declaredBy.includes(declaredBy)) {
|
|
855
|
+
existing.declaredBy.push(declaredBy);
|
|
856
|
+
}
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
const resolved = dependencies.get(declared.pluginId) ?? null;
|
|
860
|
+
const record = {
|
|
861
|
+
pluginId: declared.pluginId,
|
|
862
|
+
minVersion: declared.minVersion ?? resolved?.manifest?.version ?? null,
|
|
863
|
+
maxVersion: declared.maxVersion ?? null,
|
|
864
|
+
resolved: Boolean(resolved),
|
|
865
|
+
declaredBy: [declaredBy],
|
|
866
|
+
};
|
|
867
|
+
components.set(declared.pluginId, record);
|
|
868
|
+
if (resolved) {
|
|
869
|
+
for (const capability of resolved.manifest?.capabilities ?? []) {
|
|
870
|
+
const id = capabilityId(capability);
|
|
871
|
+
if (id) capabilities.add(id);
|
|
872
|
+
}
|
|
873
|
+
if (!visited.has(declared.pluginId)) {
|
|
874
|
+
visited.add(declared.pluginId);
|
|
875
|
+
for (const transitive of manifestDependencies(resolved.manifest)) {
|
|
876
|
+
queue.push({ declared: transitive, declaredBy: declared.pluginId });
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
} else {
|
|
880
|
+
pushIssue(
|
|
881
|
+
issues,
|
|
882
|
+
"warning",
|
|
883
|
+
"unresolved-component-dependency",
|
|
884
|
+
`Component dependency "${declared.pluginId}" (declared by "${declaredBy}") is not resolvable from the ` +
|
|
885
|
+
"dependency set; it is propagated into the bundle DEPENDENCIES but its capabilities cannot be verified.",
|
|
886
|
+
"flow.dependencies",
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
return [...components.values()].sort((a, b) => a.pluginId.localeCompare(b.pluginId));
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* findFlowCycles returns every elementary cycle reachable in the node graph
|
|
895
|
+
* as { nodeIds, edges } (iterative DFS with back-edge extraction; each cycle
|
|
896
|
+
* reported once). Self-loops are cycles of length one.
|
|
897
|
+
*/
|
|
898
|
+
export function findFlowCycles(nodes, edges) {
|
|
899
|
+
const WHITE = 0;
|
|
900
|
+
const GRAY = 1;
|
|
901
|
+
const BLACK = 2;
|
|
902
|
+
const color = new Map();
|
|
903
|
+
const out = new Map();
|
|
904
|
+
for (const node of nodes) {
|
|
905
|
+
if (!node?.nodeId) continue;
|
|
906
|
+
color.set(node.nodeId, WHITE);
|
|
907
|
+
out.set(node.nodeId, []);
|
|
908
|
+
}
|
|
909
|
+
for (const edge of edges) {
|
|
910
|
+
if (out.has(edge?.fromNodeId) && color.has(edge?.toNodeId)) {
|
|
911
|
+
out.get(edge.fromNodeId).push(edge);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const cycles = [];
|
|
916
|
+
const seen = new Set();
|
|
917
|
+
const stack = [];
|
|
918
|
+
|
|
919
|
+
const visit = (start) => {
|
|
920
|
+
const frames = [{ nodeId: start, edgeIdx: 0 }];
|
|
921
|
+
color.set(start, GRAY);
|
|
922
|
+
stack.push(start);
|
|
923
|
+
while (frames.length > 0) {
|
|
924
|
+
const frame = frames[frames.length - 1];
|
|
925
|
+
const nodeEdges = out.get(frame.nodeId) ?? [];
|
|
926
|
+
if (frame.edgeIdx >= nodeEdges.length) {
|
|
927
|
+
color.set(frame.nodeId, BLACK);
|
|
928
|
+
stack.pop();
|
|
929
|
+
frames.pop();
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
const edge = nodeEdges[frame.edgeIdx];
|
|
933
|
+
frame.edgeIdx += 1;
|
|
934
|
+
const next = edge.toNodeId;
|
|
935
|
+
const state = color.get(next);
|
|
936
|
+
if (state === GRAY) {
|
|
937
|
+
// Back edge: extract the cycle from the stack.
|
|
938
|
+
const at = stack.indexOf(next);
|
|
939
|
+
const nodeIds = stack.slice(at);
|
|
940
|
+
const key = [...nodeIds].sort().join("\0");
|
|
941
|
+
if (!seen.has(key)) {
|
|
942
|
+
seen.add(key);
|
|
943
|
+
const cycleEdges = [];
|
|
944
|
+
for (let i = 0; i < nodeIds.length; i += 1) {
|
|
945
|
+
const from = nodeIds[i];
|
|
946
|
+
const to = nodeIds[(i + 1) % nodeIds.length];
|
|
947
|
+
const found = (out.get(from) ?? []).find((candidate) => candidate.toNodeId === to);
|
|
948
|
+
if (found) cycleEdges.push(found);
|
|
949
|
+
}
|
|
950
|
+
cycles.push({ nodeIds, edges: cycleEdges });
|
|
951
|
+
}
|
|
952
|
+
} else if (state === WHITE) {
|
|
953
|
+
color.set(next, GRAY);
|
|
954
|
+
stack.push(next);
|
|
955
|
+
frames.push({ nodeId: next, edgeIdx: 0 });
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
for (const nodeId of color.keys()) {
|
|
961
|
+
if (color.get(nodeId) === WHITE) visit(nodeId);
|
|
962
|
+
}
|
|
963
|
+
return cycles;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// edgeIsBounded: a cycle edge is safe only with a finite queue bound and a
|
|
967
|
+
// policy that cannot grow the queue without limit.
|
|
968
|
+
function edgeIsBounded(edge) {
|
|
969
|
+
const policy = String(edge?.backpressurePolicy ?? "queue").toLowerCase();
|
|
970
|
+
const depth = Number(edge?.queueDepth ?? 1);
|
|
971
|
+
const boundedPolicy = policy === "queue" || policy === "drop-oldest" || policy === "drop-newest";
|
|
972
|
+
return boundedPolicy && Number.isFinite(depth) && depth > 0 && depth <= 65536;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// ---------------------------------------------------------------------------
|
|
976
|
+
// Flow-manifest `api` block validation (gateway loop G.1 extension, validated
|
|
977
|
+
// since G.2). The block is declarative OpenAPI metadata copied VERBATIM into
|
|
978
|
+
// the compiled bundle and parsed at mount time by the host's OpenAPI
|
|
979
|
+
// generator, so shape errors would otherwise surface only as a wrong or
|
|
980
|
+
// missing spec entry on a live node. Rules mirror the host-side reader
|
|
981
|
+
// (sdn-server internal/flowrt/apidoc.go + internal/api/docs.go).
|
|
982
|
+
// ---------------------------------------------------------------------------
|
|
983
|
+
|
|
984
|
+
const FLOW_API_METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
|
|
985
|
+
const FLOW_API_PARAM_LOCATIONS = new Set(["query", "path", "header", "cookie"]);
|
|
986
|
+
const FLOW_API_TEMPLATE_SEGMENT = /^\{[A-Za-z_][A-Za-z0-9_]*\}$/;
|
|
987
|
+
|
|
988
|
+
function flowAPIPathTemplateParams(path) {
|
|
989
|
+
const params = [];
|
|
990
|
+
for (const segment of String(path ?? "").split("/")) {
|
|
991
|
+
if (FLOW_API_TEMPLATE_SEGMENT.test(segment)) params.push(segment.slice(1, -1));
|
|
992
|
+
}
|
|
993
|
+
return params;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function flowAPIPathSegmentValid(segment) {
|
|
997
|
+
if (segment.includes("{") || segment.includes("}")) {
|
|
998
|
+
return FLOW_API_TEMPLATE_SEGMENT.test(segment);
|
|
999
|
+
}
|
|
1000
|
+
return true;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
export function validateFlowAPIDeclaration(flow, issues) {
|
|
1004
|
+
const api = flow?.api;
|
|
1005
|
+
if (api === undefined || api === null) return;
|
|
1006
|
+
if (typeof api !== "object" || Array.isArray(api)) {
|
|
1007
|
+
pushIssue(issues, "error", "api-invalid", "flow.api must be an object when present.", "flow.api");
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
if (api.basePath !== undefined) {
|
|
1011
|
+
if (typeof api.basePath !== "string" || !api.basePath.startsWith("/")) {
|
|
1012
|
+
pushIssue(
|
|
1013
|
+
issues,
|
|
1014
|
+
"error",
|
|
1015
|
+
"api-invalid-base-path",
|
|
1016
|
+
'flow.api.basePath must be a string starting with "/" (documentation hint; the node config mount path wins).',
|
|
1017
|
+
"flow.api.basePath",
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
for (const key of ["tag", "tagDescription"]) {
|
|
1022
|
+
if (api[key] !== undefined && typeof api[key] !== "string") {
|
|
1023
|
+
pushIssue(issues, "error", "api-invalid-tag", `flow.api.${key} must be a string when present.`, `flow.api.${key}`);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
const routes = api.routes;
|
|
1027
|
+
if (!Array.isArray(routes) || routes.length === 0) {
|
|
1028
|
+
pushIssue(
|
|
1029
|
+
issues,
|
|
1030
|
+
"error",
|
|
1031
|
+
"api-missing-routes",
|
|
1032
|
+
"flow.api.routes must be a non-empty array of route declarations.",
|
|
1033
|
+
"flow.api.routes",
|
|
1034
|
+
);
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
const seenRoutes = new Set();
|
|
1038
|
+
routes.forEach((route, index) => {
|
|
1039
|
+
const location = `flow.api.routes[${index}]`;
|
|
1040
|
+
if (typeof route !== "object" || route === null || Array.isArray(route)) {
|
|
1041
|
+
pushIssue(issues, "error", "api-invalid-route", "Route declaration must be an object.", location);
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
if (typeof route.path !== "string") {
|
|
1045
|
+
pushIssue(issues, "error", "api-invalid-route-path", 'Route "path" must be a string (may be "" for the mount root).', location);
|
|
1046
|
+
} else {
|
|
1047
|
+
if (/\s/.test(route.path)) {
|
|
1048
|
+
pushIssue(issues, "error", "api-invalid-route-path", `Route path "${route.path}" must not contain whitespace.`, location);
|
|
1049
|
+
}
|
|
1050
|
+
for (const segment of route.path.split("/")) {
|
|
1051
|
+
if (!flowAPIPathSegmentValid(segment)) {
|
|
1052
|
+
pushIssue(
|
|
1053
|
+
issues,
|
|
1054
|
+
"error",
|
|
1055
|
+
"api-invalid-route-path",
|
|
1056
|
+
`Route path "${route.path}" has a malformed template segment "${segment}" (expected "{paramName}").`,
|
|
1057
|
+
location,
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
let method = "GET";
|
|
1063
|
+
if (route.method !== undefined) {
|
|
1064
|
+
if (typeof route.method !== "string" || !FLOW_API_METHODS.has(route.method.trim().toUpperCase())) {
|
|
1065
|
+
pushIssue(
|
|
1066
|
+
issues,
|
|
1067
|
+
"error",
|
|
1068
|
+
"api-invalid-route-method",
|
|
1069
|
+
`Route method "${route.method}" is not a valid HTTP method (${[...FLOW_API_METHODS].join(", ")}).`,
|
|
1070
|
+
location,
|
|
1071
|
+
);
|
|
1072
|
+
} else {
|
|
1073
|
+
method = route.method.trim().toUpperCase();
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
if (typeof route.path === "string") {
|
|
1077
|
+
const key = `${method} ${route.path}`;
|
|
1078
|
+
if (seenRoutes.has(key)) {
|
|
1079
|
+
pushIssue(issues, "error", "api-duplicate-route", `Route "${key}" is declared more than once.`, location);
|
|
1080
|
+
}
|
|
1081
|
+
seenRoutes.add(key);
|
|
1082
|
+
}
|
|
1083
|
+
for (const key of ["operationId", "summary", "description"]) {
|
|
1084
|
+
if (route[key] !== undefined && typeof route[key] !== "string") {
|
|
1085
|
+
pushIssue(issues, "error", "api-invalid-route-field", `Route "${key}" must be a string when present.`, location);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
for (const key of ["anonymous", "deprecated"]) {
|
|
1089
|
+
if (route[key] !== undefined && typeof route[key] !== "boolean") {
|
|
1090
|
+
pushIssue(issues, "error", "api-invalid-route-field", `Route "${key}" must be a boolean when present.`, location);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
const declaredPathParams = new Set();
|
|
1094
|
+
if (route.params !== undefined) {
|
|
1095
|
+
if (!Array.isArray(route.params)) {
|
|
1096
|
+
pushIssue(issues, "error", "api-invalid-params", 'Route "params" must be an array of OpenAPI parameter objects.', location);
|
|
1097
|
+
} else {
|
|
1098
|
+
route.params.forEach((param, paramIndex) => {
|
|
1099
|
+
const paramLocation = `${location}.params[${paramIndex}]`;
|
|
1100
|
+
if (typeof param !== "object" || param === null || Array.isArray(param)) {
|
|
1101
|
+
pushIssue(issues, "error", "api-invalid-params", "Parameter must be an OpenAPI parameter object.", paramLocation);
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
if (typeof param.name !== "string" || param.name.length === 0) {
|
|
1105
|
+
pushIssue(issues, "error", "api-invalid-params", 'Parameter "name" must be a non-empty string.', paramLocation);
|
|
1106
|
+
}
|
|
1107
|
+
if (typeof param.in !== "string" || !FLOW_API_PARAM_LOCATIONS.has(param.in)) {
|
|
1108
|
+
pushIssue(
|
|
1109
|
+
issues,
|
|
1110
|
+
"error",
|
|
1111
|
+
"api-invalid-params",
|
|
1112
|
+
`Parameter "in" must be one of ${[...FLOW_API_PARAM_LOCATIONS].join(", ")}.`,
|
|
1113
|
+
paramLocation,
|
|
1114
|
+
);
|
|
1115
|
+
} else if (param.in === "path" && typeof param.name === "string") {
|
|
1116
|
+
declaredPathParams.add(param.name);
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
for (const templateParam of flowAPIPathTemplateParams(route.path)) {
|
|
1122
|
+
if (!declaredPathParams.has(templateParam)) {
|
|
1123
|
+
pushIssue(
|
|
1124
|
+
issues,
|
|
1125
|
+
"warning",
|
|
1126
|
+
"api-undeclared-path-param",
|
|
1127
|
+
`Route path template "{${templateParam}}" has no matching {"name":"${templateParam}","in":"path"} entry in params; ` +
|
|
1128
|
+
"the generated spec will carry an undocumented path parameter.",
|
|
1129
|
+
location,
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
if (route.requestBody !== undefined && (typeof route.requestBody !== "object" || route.requestBody === null || Array.isArray(route.requestBody))) {
|
|
1134
|
+
pushIssue(issues, "error", "api-invalid-request-body", 'Route "requestBody" must be an OpenAPI requestBody object.', location);
|
|
1135
|
+
}
|
|
1136
|
+
if (route.responses !== undefined) {
|
|
1137
|
+
if (typeof route.responses !== "object" || route.responses === null || Array.isArray(route.responses)) {
|
|
1138
|
+
pushIssue(issues, "error", "api-invalid-responses", 'Route "responses" must be an object keyed by status code.', location);
|
|
1139
|
+
} else {
|
|
1140
|
+
for (const [status, response] of Object.entries(route.responses)) {
|
|
1141
|
+
const responseLocation = `${location}.responses["${status}"]`;
|
|
1142
|
+
if (status !== "default" && !/^[1-5][0-9]{2}$/.test(status)) {
|
|
1143
|
+
pushIssue(
|
|
1144
|
+
issues,
|
|
1145
|
+
"error",
|
|
1146
|
+
"api-invalid-responses",
|
|
1147
|
+
`Response key "${status}" must be a 3-digit HTTP status code or "default".`,
|
|
1148
|
+
responseLocation,
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
if (typeof response !== "object" || response === null || Array.isArray(response)) {
|
|
1152
|
+
pushIssue(issues, "error", "api-invalid-responses", "Response value must be an object.", responseLocation);
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
if (response.recordStream !== undefined && typeof response.recordStream !== "boolean") {
|
|
1156
|
+
pushIssue(issues, "error", "api-invalid-responses", '"recordStream" must be a boolean when present.', responseLocation);
|
|
1157
|
+
}
|
|
1158
|
+
if (response.content !== undefined) {
|
|
1159
|
+
if (typeof response.content !== "object" || response.content === null || Array.isArray(response.content)) {
|
|
1160
|
+
pushIssue(issues, "error", "api-invalid-responses", '"content" must be an object keyed by media type.', responseLocation);
|
|
1161
|
+
} else {
|
|
1162
|
+
for (const [mediaType, mediaValue] of Object.entries(response.content)) {
|
|
1163
|
+
if (!mediaType.includes("/")) {
|
|
1164
|
+
pushIssue(
|
|
1165
|
+
issues,
|
|
1166
|
+
"error",
|
|
1167
|
+
"api-invalid-responses",
|
|
1168
|
+
`Content key "${mediaType}" is not a media type (expected e.g. "application/json").`,
|
|
1169
|
+
responseLocation,
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
if (typeof mediaValue !== "object" || mediaValue === null || Array.isArray(mediaValue)) {
|
|
1173
|
+
pushIssue(issues, "error", "api-invalid-responses", `Content["${mediaType}"] must be an object.`, responseLocation);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
if (response.headers !== undefined && (typeof response.headers !== "object" || response.headers === null || Array.isArray(response.headers))) {
|
|
1179
|
+
pushIssue(issues, "error", "api-invalid-responses", '"headers" must be an object of OpenAPI header objects.', responseLocation);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
/**
|
|
1188
|
+
* Validate a flow document against its dependency manifests. Returns
|
|
1189
|
+
* { ok, issues, capabilities, nodes } where `capabilities` is the computed
|
|
1190
|
+
* capability union of all linked nodes and `nodes` carries the resolved
|
|
1191
|
+
* per-node dispatch classification (linked-direct vs host).
|
|
1192
|
+
*/
|
|
1193
|
+
export function checkFlowProgram({ flow, dependencies = new Map() } = {}) {
|
|
1194
|
+
const issues = [];
|
|
1195
|
+
const nodes = Array.isArray(flow?.nodes) ? flow.nodes : [];
|
|
1196
|
+
const edges = Array.isArray(flow?.edges) ? flow.edges : [];
|
|
1197
|
+
const triggers = Array.isArray(flow?.triggers) ? flow.triggers : [];
|
|
1198
|
+
const triggerBindings = Array.isArray(flow?.triggerBindings) ? flow.triggerBindings : [];
|
|
1199
|
+
const requiredPlugins = Array.isArray(flow?.requiredPlugins) ? flow.requiredPlugins : [];
|
|
1200
|
+
|
|
1201
|
+
if (!flow?.programId || typeof flow.programId !== "string") {
|
|
1202
|
+
pushIssue(issues, "error", "missing-program-id", "flow.programId must be a non-empty string.", "flow.programId");
|
|
1203
|
+
}
|
|
1204
|
+
if (nodes.length === 0) {
|
|
1205
|
+
pushIssue(issues, "error", "missing-nodes", "flow.nodes must declare at least one node.", "flow.nodes");
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
const nodeIndex = new Map();
|
|
1209
|
+
const resolvedNodes = [];
|
|
1210
|
+
const validatedDependencyIds = new Set();
|
|
1211
|
+
nodes.forEach((node, index) => {
|
|
1212
|
+
const location = `flow.nodes[${index}]`;
|
|
1213
|
+
if (!node?.nodeId) {
|
|
1214
|
+
pushIssue(issues, "error", "missing-node-id", "Node is missing nodeId.", location);
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
if (nodeIndex.has(node.nodeId)) {
|
|
1218
|
+
pushIssue(issues, "error", "duplicate-node-id", `Node id "${node.nodeId}" is declared more than once.`, location);
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
nodeIndex.set(node.nodeId, index);
|
|
1222
|
+
|
|
1223
|
+
const dependency = dependencies.get(node.pluginId) ?? null;
|
|
1224
|
+
const requestedDispatchModel = requestedNodeDispatchModel(node);
|
|
1225
|
+
let method = null;
|
|
1226
|
+
let dispatchModel = "host";
|
|
1227
|
+
if (dependency) {
|
|
1228
|
+
if (
|
|
1229
|
+
requestedDispatchModel &&
|
|
1230
|
+
requestedDispatchModel !== "linked-direct" &&
|
|
1231
|
+
requestedDispatchModel !== "isomorphic"
|
|
1232
|
+
) {
|
|
1233
|
+
pushIssue(
|
|
1234
|
+
issues,
|
|
1235
|
+
"error",
|
|
1236
|
+
"invalid-dispatch-model",
|
|
1237
|
+
`Resolved node "${node.nodeId}" requests unsupported dispatch model "${node.dispatchModel}".`,
|
|
1238
|
+
`${location}.dispatchModel`,
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
dispatchModel =
|
|
1242
|
+
requestedDispatchModel === "isomorphic"
|
|
1243
|
+
? "isomorphic"
|
|
1244
|
+
: "linked-direct";
|
|
1245
|
+
if (dispatchModel === "isomorphic") {
|
|
1246
|
+
validateIsomorphicArtifactLock(node, issues, location);
|
|
1247
|
+
}
|
|
1248
|
+
if (!validatedDependencyIds.has(node.pluginId)) {
|
|
1249
|
+
validatedDependencyIds.add(node.pluginId);
|
|
1250
|
+
const report = validatePluginManifest(dependency.manifest, {
|
|
1251
|
+
sourceName: `dependency[${JSON.stringify(node.pluginId)}]`,
|
|
1252
|
+
});
|
|
1253
|
+
for (const issue of report.issues) {
|
|
1254
|
+
pushIssue(
|
|
1255
|
+
issues,
|
|
1256
|
+
issue.severity,
|
|
1257
|
+
issue.code,
|
|
1258
|
+
`Plugin "${node.pluginId}": ${issue.message}`,
|
|
1259
|
+
issue.location,
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
method = findMethod(dependency.normalized, node.methodId);
|
|
1264
|
+
if (!method) {
|
|
1265
|
+
pushIssue(
|
|
1266
|
+
issues,
|
|
1267
|
+
"error",
|
|
1268
|
+
"unknown-method",
|
|
1269
|
+
`Node "${node.nodeId}" references method "${node.methodId}" which does not exist on plugin "${node.pluginId}".`,
|
|
1270
|
+
location,
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
} else if (!isHostModelNode(node)) {
|
|
1274
|
+
pushIssue(
|
|
1275
|
+
issues,
|
|
1276
|
+
"error",
|
|
1277
|
+
"unknown-plugin",
|
|
1278
|
+
`Node "${node.nodeId}" references plugin "${node.pluginId}" which is not resolvable from the dependency set.`,
|
|
1279
|
+
location,
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
resolvedNodes.push({ node, index, dependency, method, dispatchModel });
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
const resolvedByNodeId = new Map(resolvedNodes.map((entry) => [entry.node.nodeId, entry]));
|
|
1286
|
+
const edgeTypeContracts = Array(edges.length).fill(null);
|
|
1287
|
+
|
|
1288
|
+
for (const entry of resolvedNodes) {
|
|
1289
|
+
if (entry.dispatchModel !== "host" || !isHostModelNode(entry.node)) continue;
|
|
1290
|
+
const incomingEdges = edges.filter(
|
|
1291
|
+
(edge) => edge?.toNodeId === entry.node.nodeId,
|
|
1292
|
+
);
|
|
1293
|
+
const outgoingEdges = edges.filter(
|
|
1294
|
+
(edge) => edge?.fromNodeId === entry.node.nodeId,
|
|
1295
|
+
);
|
|
1296
|
+
const location = `flow.nodes[${entry.index}]`;
|
|
1297
|
+
if (incomingEdges.length > 0 && outgoingEdges.length > 0) {
|
|
1298
|
+
pushIssue(
|
|
1299
|
+
issues,
|
|
1300
|
+
"error",
|
|
1301
|
+
"host-node-schema-bridge",
|
|
1302
|
+
`Unresolved host node "${entry.node.nodeId}" cannot sit between graph nodes or bridge typed schemas; host-model nodes are canonical ingress or egress boundaries only.`,
|
|
1303
|
+
location,
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
const kind = String(entry.node?.kind ?? "").toLowerCase();
|
|
1307
|
+
if (kind === "source" && incomingEdges.length > 0) {
|
|
1308
|
+
pushIssue(
|
|
1309
|
+
issues,
|
|
1310
|
+
"error",
|
|
1311
|
+
"host-source-has-input",
|
|
1312
|
+
`Host source boundary "${entry.node.nodeId}" cannot have incoming graph edges.`,
|
|
1313
|
+
location,
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
if (kind === "sink" && outgoingEdges.length > 0) {
|
|
1317
|
+
pushIssue(
|
|
1318
|
+
issues,
|
|
1319
|
+
"error",
|
|
1320
|
+
"host-sink-has-output",
|
|
1321
|
+
`Host sink boundary "${entry.node.nodeId}" cannot have outgoing graph edges.`,
|
|
1322
|
+
location,
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
edges.forEach((edge, index) => {
|
|
1328
|
+
const location = `flow.edges[${index}]`;
|
|
1329
|
+
const from = resolvedByNodeId.get(edge?.fromNodeId);
|
|
1330
|
+
const to = resolvedByNodeId.get(edge?.toNodeId);
|
|
1331
|
+
if (!from) {
|
|
1332
|
+
pushIssue(issues, "error", "unknown-edge-node", `Edge references unknown fromNodeId "${edge?.fromNodeId}".`, location);
|
|
1333
|
+
}
|
|
1334
|
+
if (!to) {
|
|
1335
|
+
pushIssue(issues, "error", "unknown-edge-node", `Edge references unknown toNodeId "${edge?.toNodeId}".`, location);
|
|
1336
|
+
}
|
|
1337
|
+
if (!from || !to) return;
|
|
1338
|
+
|
|
1339
|
+
let fromTypes = [];
|
|
1340
|
+
if (from.method) {
|
|
1341
|
+
const fromPort = (from.method.outputPorts ?? []).find((port) => port.portId === edge.fromPortId);
|
|
1342
|
+
if (!fromPort) {
|
|
1343
|
+
pushIssue(
|
|
1344
|
+
issues,
|
|
1345
|
+
"error",
|
|
1346
|
+
"unknown-output-port",
|
|
1347
|
+
`Edge fromPortId "${edge.fromPortId}" is not an output port of ${from.node.pluginId}:${from.node.methodId}.`,
|
|
1348
|
+
location,
|
|
1349
|
+
);
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
fromTypes = portAcceptedTypes(fromPort);
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
let toTypes = [];
|
|
1356
|
+
if (to.method) {
|
|
1357
|
+
const toPort = (to.method.inputPorts ?? []).find((port) => port.portId === edge.toPortId);
|
|
1358
|
+
if (!toPort) {
|
|
1359
|
+
pushIssue(
|
|
1360
|
+
issues,
|
|
1361
|
+
"error",
|
|
1362
|
+
"unknown-input-port",
|
|
1363
|
+
`Edge toPortId "${edge.toPortId}" is not an input port of ${to.node.pluginId}:${to.node.methodId}.`,
|
|
1364
|
+
location,
|
|
1365
|
+
);
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
toTypes = portAcceptedTypes(toPort);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
const edgeTypeContract = resolveEdgeTypeContract(fromTypes, toTypes);
|
|
1372
|
+
if (edgeTypeContract?.errorCode) {
|
|
1373
|
+
pushIssue(
|
|
1374
|
+
issues,
|
|
1375
|
+
"error",
|
|
1376
|
+
edgeTypeContract.errorCode,
|
|
1377
|
+
`Edge ${edge.fromNodeId}.${edge.fromPortId} -> ${edge.toNodeId}.${edge.toPortId}: ${edgeTypeContract.errorMessage}`,
|
|
1378
|
+
location,
|
|
1379
|
+
);
|
|
1380
|
+
} else if (!edgeTypeContract) {
|
|
1381
|
+
pushIssue(
|
|
1382
|
+
issues,
|
|
1383
|
+
"error",
|
|
1384
|
+
"edge-type-mismatch",
|
|
1385
|
+
`Edge ${edge.fromNodeId}.${edge.fromPortId} -> ${edge.toNodeId}.${edge.toPortId} is not type-compatible: ` +
|
|
1386
|
+
`${describeTypes(fromTypes)} does not satisfy ${describeTypes(toTypes)}.`,
|
|
1387
|
+
location,
|
|
1388
|
+
);
|
|
1389
|
+
pushIssue(
|
|
1390
|
+
issues,
|
|
1391
|
+
"error",
|
|
1392
|
+
"edge-missing-canonical-fallback",
|
|
1393
|
+
`Edge ${edge.fromNodeId}.${edge.fromPortId} -> ${edge.toNodeId}.${edge.toPortId} has no mutually available canonical SDS FlatBuffer representation.`,
|
|
1394
|
+
location,
|
|
1395
|
+
);
|
|
1396
|
+
} else {
|
|
1397
|
+
// OPAQUE (SDS $PLG 1.0.13 / v1.164.0). A contract MUST carry exactly one
|
|
1398
|
+
// of CANONICAL_TYPE or OPAQUE = true, and opacity is a DELIBERATE SIGNED
|
|
1399
|
+
// ASSERTION, never an absent field. The compiler resolves it from the
|
|
1400
|
+
// ports' declared types; an author may also assert it on the edge, and
|
|
1401
|
+
// then the two must agree — a marking that contradicts the resolved types
|
|
1402
|
+
// is a defect, not an override.
|
|
1403
|
+
const resolvedOpaque =
|
|
1404
|
+
edgeTypeContract.wildcard === true &&
|
|
1405
|
+
!(edgeTypeContract.canonical?.producer ?? edgeTypeContract.canonical?.consumer);
|
|
1406
|
+
if (edge?.opaque !== undefined) {
|
|
1407
|
+
if (typeof edge.opaque !== "boolean") {
|
|
1408
|
+
pushIssue(
|
|
1409
|
+
issues,
|
|
1410
|
+
"error",
|
|
1411
|
+
"invalid-edge-opacity",
|
|
1412
|
+
`Edge ${edge.fromNodeId}.${edge.fromPortId} -> ${edge.toNodeId}.${edge.toPortId}: opaque must be a boolean when present.`,
|
|
1413
|
+
location,
|
|
1414
|
+
);
|
|
1415
|
+
} else if (edge.opaque !== resolvedOpaque) {
|
|
1416
|
+
pushIssue(
|
|
1417
|
+
issues,
|
|
1418
|
+
"error",
|
|
1419
|
+
"edge-opacity-mismatch",
|
|
1420
|
+
`Edge ${edge.fromNodeId}.${edge.fromPortId} -> ${edge.toNodeId}.${edge.toPortId} is marked opaque=${edge.opaque}, ` +
|
|
1421
|
+
(resolvedOpaque
|
|
1422
|
+
? "but its ports declare no SDS identity, so the edge IS opaque."
|
|
1423
|
+
: `but its ports resolve the concrete identity ${edgeTypeContract.fileIdentifier ?? edgeTypeContract.schemaName}, ` +
|
|
1424
|
+
"which must be declared rather than signed away."),
|
|
1425
|
+
location,
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
edgeTypeContracts[index] = {
|
|
1430
|
+
edgeIndex: index,
|
|
1431
|
+
fromNodeId: edge.fromNodeId,
|
|
1432
|
+
fromPortId: edge.fromPortId,
|
|
1433
|
+
toNodeId: edge.toNodeId,
|
|
1434
|
+
toPortId: edge.toPortId,
|
|
1435
|
+
...edgeTypeContract,
|
|
1436
|
+
opaque: resolvedOpaque,
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
if (from.index > to.index) {
|
|
1441
|
+
pushIssue(
|
|
1442
|
+
issues,
|
|
1443
|
+
"warning",
|
|
1444
|
+
"non-topological-node-order",
|
|
1445
|
+
`Edge ${edge.fromNodeId} -> ${edge.toNodeId} flows backwards in node declaration order; ` +
|
|
1446
|
+
"the compiled scheduler scans nodes in declaration order, so declare producers before consumers.",
|
|
1447
|
+
location,
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
// Unending-loop detection: cycles in the node graph make the drain loop
|
|
1453
|
+
// spin forever on a single ingress frame. Cycles are a hard error unless
|
|
1454
|
+
// the flow explicitly opts in with `allowCycles: true` AND every edge on
|
|
1455
|
+
// every cycle is bounded (finite queueDepth with a queue/drop policy) —
|
|
1456
|
+
// sanctioned feedback must be unable to grow without bound. Applies to
|
|
1457
|
+
// `flow check`, `flow compile`, and any flow UI built on this validator.
|
|
1458
|
+
const cycles = findFlowCycles(nodes, edges);
|
|
1459
|
+
if (cycles.length > 0) {
|
|
1460
|
+
const allowCycles = flow?.allowCycles === true;
|
|
1461
|
+
for (const cycle of cycles) {
|
|
1462
|
+
const path = [...cycle.nodeIds, cycle.nodeIds[0]].join(" -> ");
|
|
1463
|
+
if (!allowCycles) {
|
|
1464
|
+
pushIssue(
|
|
1465
|
+
issues,
|
|
1466
|
+
"error",
|
|
1467
|
+
"flow-cycle",
|
|
1468
|
+
`Flow graph contains a cycle: ${path}. Cycles hang the scheduler; break the loop, ` +
|
|
1469
|
+
`or set flow.allowCycles = true AND give every edge in the cycle a bounded ` +
|
|
1470
|
+
`backpressure policy (finite queueDepth).`,
|
|
1471
|
+
"flow.edges",
|
|
1472
|
+
);
|
|
1473
|
+
continue;
|
|
1474
|
+
}
|
|
1475
|
+
const unbounded = cycle.edges.filter((edge) => !edgeIsBounded(edge));
|
|
1476
|
+
if (unbounded.length > 0) {
|
|
1477
|
+
const labels = unbounded.map((edge) => `${edge.fromNodeId} -> ${edge.toNodeId}`).join(", ");
|
|
1478
|
+
pushIssue(
|
|
1479
|
+
issues,
|
|
1480
|
+
"error",
|
|
1481
|
+
"unbounded-cycle",
|
|
1482
|
+
`Flow cycle ${path} is allowed by flow.allowCycles but has unbounded edges (${labels}); ` +
|
|
1483
|
+
`every cycle edge needs backpressurePolicy queue|drop-oldest|drop-newest with a finite queueDepth.`,
|
|
1484
|
+
"flow.edges",
|
|
1485
|
+
);
|
|
1486
|
+
} else {
|
|
1487
|
+
pushIssue(
|
|
1488
|
+
issues,
|
|
1489
|
+
"warning",
|
|
1490
|
+
"sanctioned-cycle",
|
|
1491
|
+
`Flow contains a sanctioned feedback cycle (${path}); all cycle edges are bounded.`,
|
|
1492
|
+
"flow.edges",
|
|
1493
|
+
);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
const triggerIds = new Set(triggers.map((trigger) => trigger?.triggerId).filter(Boolean));
|
|
1499
|
+
if (triggers.length === 0) {
|
|
1500
|
+
pushIssue(issues, "error", "missing-triggers", "flow.triggers must declare at least one trigger.", "flow.triggers");
|
|
1501
|
+
}
|
|
1502
|
+
triggerBindings.forEach((binding, index) => {
|
|
1503
|
+
const location = `flow.triggerBindings[${index}]`;
|
|
1504
|
+
if (!triggerIds.has(binding?.triggerId)) {
|
|
1505
|
+
pushIssue(issues, "error", "unknown-trigger", `Trigger binding references unknown triggerId "${binding?.triggerId}".`, location);
|
|
1506
|
+
}
|
|
1507
|
+
const target = resolvedByNodeId.get(binding?.targetNodeId);
|
|
1508
|
+
if (!target) {
|
|
1509
|
+
pushIssue(issues, "error", "unknown-binding-node", `Trigger binding references unknown targetNodeId "${binding?.targetNodeId}".`, location);
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
if (target.method) {
|
|
1513
|
+
const port = (target.method.inputPorts ?? []).find((candidate) => candidate.portId === binding.targetPortId);
|
|
1514
|
+
if (!port) {
|
|
1515
|
+
pushIssue(
|
|
1516
|
+
issues,
|
|
1517
|
+
"error",
|
|
1518
|
+
"unknown-binding-port",
|
|
1519
|
+
`Trigger binding targetPortId "${binding.targetPortId}" is not an input port of ${target.node.pluginId}:${target.node.methodId}.`,
|
|
1520
|
+
location,
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
});
|
|
1525
|
+
|
|
1526
|
+
for (const pluginId of requiredPlugins) {
|
|
1527
|
+
if (!dependencies.has(pluginId)) {
|
|
1528
|
+
pushIssue(
|
|
1529
|
+
issues,
|
|
1530
|
+
"error",
|
|
1531
|
+
"unknown-plugin",
|
|
1532
|
+
`flow.requiredPlugins lists "${pluginId}" which is not resolvable from the dependency set.`,
|
|
1533
|
+
"flow.requiredPlugins",
|
|
1534
|
+
);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
for (const entry of resolvedNodes) {
|
|
1538
|
+
if (entry.dependency && !requiredPlugins.includes(entry.node.pluginId)) {
|
|
1539
|
+
pushIssue(
|
|
1540
|
+
issues,
|
|
1541
|
+
"warning",
|
|
1542
|
+
"missing-required-plugin",
|
|
1543
|
+
`Node "${entry.node.nodeId}" uses plugin "${entry.node.pluginId}" which is not listed in flow.requiredPlugins.`,
|
|
1544
|
+
"flow.requiredPlugins",
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// Gateway `api` block (when present): shape-validate the declarative HTTP
|
|
1550
|
+
// surface so a bad block fails `flow check`/`flow compile` instead of
|
|
1551
|
+
// silently corrupting the host-generated OpenAPI spec.
|
|
1552
|
+
validateFlowAPIDeclaration(flow, issues);
|
|
1553
|
+
|
|
1554
|
+
// Engine linkage mode (loop C.7): linked flows stamp the engine-link
|
|
1555
|
+
// capability so hosts can gate mounting (first-party only) and know to
|
|
1556
|
+
// register the live engine instance + link shim at instantiation.
|
|
1557
|
+
let engineLinkage = null;
|
|
1558
|
+
try {
|
|
1559
|
+
engineLinkage = flowEngineLinkage(flow);
|
|
1560
|
+
} catch (error) {
|
|
1561
|
+
pushIssue(issues, "error", "invalid-engine-linkage", error.message, "flow.engineLinkage");
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
// Capability union — explicit per-node capability slices take precedence,
|
|
1565
|
+
// including an explicit empty array. Otherwise use the selected guest-link
|
|
1566
|
+
// object's metadata, with the root manifest as the conservative fallback.
|
|
1567
|
+
// A slice may narrow a module's declared permissions but may never escalate
|
|
1568
|
+
// beyond them.
|
|
1569
|
+
const capabilities = new Set();
|
|
1570
|
+
const threadModels = new Set();
|
|
1571
|
+
for (const entry of resolvedNodes) {
|
|
1572
|
+
if (!entry.dependency) continue;
|
|
1573
|
+
const guestLink = selectedGuestLink(entry.dependency, Boolean(engineLinkage));
|
|
1574
|
+
const rootCapabilities = new Set(
|
|
1575
|
+
normalizedCapabilityIds(entry.dependency.manifest?.capabilities),
|
|
1576
|
+
);
|
|
1577
|
+
const slice = resolveNodeCapabilitySlice(entry, guestLink);
|
|
1578
|
+
for (const capability of slice.capabilities) {
|
|
1579
|
+
if (!rootCapabilities.has(capability)) {
|
|
1580
|
+
pushIssue(
|
|
1581
|
+
issues,
|
|
1582
|
+
"error",
|
|
1583
|
+
"capability-escalation",
|
|
1584
|
+
`Node "${entry.node.nodeId}" requests capability "${capability}" from its ${slice.source} ` +
|
|
1585
|
+
`slice, but plugin "${entry.node.pluginId}" does not declare it in the root manifest.`,
|
|
1586
|
+
`flow.nodes[${entry.index}].capabilities`,
|
|
1587
|
+
);
|
|
1588
|
+
continue;
|
|
1589
|
+
}
|
|
1590
|
+
capabilities.add(capability);
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
if (entry.dispatchModel === "linked-direct") {
|
|
1594
|
+
const declaredThreadModel =
|
|
1595
|
+
guestLink?.metadata?.threadModel ?? guestLink?.threadModel;
|
|
1596
|
+
const threadModel = normalizeGuestThreadModel(declaredThreadModel);
|
|
1597
|
+
if (!threadModel) {
|
|
1598
|
+
pushIssue(
|
|
1599
|
+
issues,
|
|
1600
|
+
"error",
|
|
1601
|
+
"invalid-guest-thread-model",
|
|
1602
|
+
`Plugin "${entry.node.pluginId}" declares unsupported guest-link thread model ` +
|
|
1603
|
+
`"${declaredThreadModel}".`,
|
|
1604
|
+
`flow.nodes[${entry.index}]`,
|
|
1605
|
+
);
|
|
1606
|
+
} else {
|
|
1607
|
+
threadModels.add(threadModel);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
let threadModel =
|
|
1613
|
+
threadModels.size === 1
|
|
1614
|
+
? [...threadModels][0]
|
|
1615
|
+
: FLOW_THREAD_MODEL.SINGLE_THREAD;
|
|
1616
|
+
if (threadModels.size > 1) {
|
|
1617
|
+
threadModel = null;
|
|
1618
|
+
pushIssue(
|
|
1619
|
+
issues,
|
|
1620
|
+
"error",
|
|
1621
|
+
"mixed-guest-thread-models",
|
|
1622
|
+
"A flow cannot link single-thread (Emscripten) and wasi (clang) guest objects " +
|
|
1623
|
+
"into one artifact. Note that wasi-threads and wasi-sequential guests DO mix " +
|
|
1624
|
+
"freely — they are the same object format — so only a single-thread/Emscripten " +
|
|
1625
|
+
"node conflicts here.",
|
|
1626
|
+
"flow.nodes",
|
|
1627
|
+
);
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
if (engineLinkage) {
|
|
1631
|
+
capabilities.add(ENGINE_LINK_CAPABILITY);
|
|
1632
|
+
}
|
|
1633
|
+
const componentDependencies = collectComponentDependencies({
|
|
1634
|
+
nodePluginIds: [
|
|
1635
|
+
...new Set(resolvedNodes.filter((entry) => entry.dependency).map((entry) => entry.node.pluginId)),
|
|
1636
|
+
],
|
|
1637
|
+
dependencies,
|
|
1638
|
+
issues,
|
|
1639
|
+
capabilities,
|
|
1640
|
+
});
|
|
1641
|
+
|
|
1642
|
+
const errors = issues.filter((issue) => issue.severity === "error");
|
|
1643
|
+
return {
|
|
1644
|
+
ok: errors.length === 0,
|
|
1645
|
+
issues,
|
|
1646
|
+
errors,
|
|
1647
|
+
warnings: issues.filter((issue) => issue.severity === "warning"),
|
|
1648
|
+
capabilities: [...capabilities].sort(),
|
|
1649
|
+
engineLinkage,
|
|
1650
|
+
threadModel,
|
|
1651
|
+
componentDependencies,
|
|
1652
|
+
edgeTypeContracts,
|
|
1653
|
+
nodes: resolvedNodes.map((entry) => ({
|
|
1654
|
+
nodeId: entry.node.nodeId,
|
|
1655
|
+
pluginId: entry.node.pluginId,
|
|
1656
|
+
methodId: entry.node.methodId,
|
|
1657
|
+
dispatchModel: entry.dispatchModel,
|
|
1658
|
+
})),
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// ---------------------------------------------------------------------------
|
|
1663
|
+
// Flow module manifest — the flow bundle is itself a legal SDK module.
|
|
1664
|
+
// ---------------------------------------------------------------------------
|
|
1665
|
+
|
|
1666
|
+
function clonePortDefinition(port, portId) {
|
|
1667
|
+
return {
|
|
1668
|
+
...JSON.parse(JSON.stringify(port ?? {})),
|
|
1669
|
+
portId,
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// Fully-shaped wildcard port for graph endpoints whose backing manifest port
|
|
1674
|
+
// cannot be resolved (e.g. host-model egress sinks) — the compliance rules
|
|
1675
|
+
// require acceptedTypeSets/minStreams/maxStreams/required on every port.
|
|
1676
|
+
function wildcardPortDefinition(portId, { required }) {
|
|
1677
|
+
return {
|
|
1678
|
+
portId,
|
|
1679
|
+
displayName: portId,
|
|
1680
|
+
acceptedTypeSets: [
|
|
1681
|
+
{
|
|
1682
|
+
setId: `${cIdent(portId)}-any`,
|
|
1683
|
+
allowedTypes: [{ acceptsAnyFlatbuffer: true }],
|
|
1684
|
+
description: "Accepts any frame (flow-compiler wildcard).",
|
|
1685
|
+
},
|
|
1686
|
+
],
|
|
1687
|
+
minStreams: required ? 1 : 0,
|
|
1688
|
+
maxStreams: 1,
|
|
1689
|
+
required,
|
|
1690
|
+
description: "Flow-compiler synthesized port.",
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
function rawMethod(dependency, methodId) {
|
|
1695
|
+
return (dependency?.manifest?.methods ?? []).find(
|
|
1696
|
+
(method) => method?.methodId === methodId,
|
|
1697
|
+
) ?? null;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
/**
|
|
1701
|
+
* THE SIGNING COMPILER'S REFUSAL (SDS $PLG 1.0.13 / v1.164.0).
|
|
1702
|
+
*
|
|
1703
|
+
* `PLGFlowEdge.CONTRACT` and `PLGFlowEdgeContract.CANONICAL_TYPE` are both
|
|
1704
|
+
* OPTIONAL in the schema — marking them `required` would have broken every
|
|
1705
|
+
* pre-1.0.13 buffer — so presence is enforced HERE instead: this compiler
|
|
1706
|
+
* refuses to sign a contract-less edge, and a contract must carry EXACTLY ONE
|
|
1707
|
+
* of `CANONICAL_TYPE` or `OPAQUE = true`. Opacity is a deliberate signed
|
|
1708
|
+
* assertion, never an absent field, which is what keeps "this edge carries
|
|
1709
|
+
* bytes by design" distinguishable from "somebody forgot the type".
|
|
1710
|
+
*/
|
|
1711
|
+
function signedFlowEdgeContract(contract, fromDispatchModel, toDispatchModel) {
|
|
1712
|
+
if (!contract) {
|
|
1713
|
+
throw new Error(
|
|
1714
|
+
"Refusing to sign a flow edge with no type contract: PLGFlowEdge.CONTRACT is schema-optional " +
|
|
1715
|
+
"only for pre-1.0.13 compatibility, and this compiler will not emit an edge without one.",
|
|
1716
|
+
);
|
|
1717
|
+
}
|
|
1718
|
+
const canonicalType = contract.canonical?.producer ?? contract.canonical?.consumer;
|
|
1719
|
+
// Opacity is derived here as well as annotated upstream: the signer must not
|
|
1720
|
+
// depend on a caller's bookkeeping for a fact it can establish itself, or a
|
|
1721
|
+
// dropped annotation would silently become "somebody forgot the type".
|
|
1722
|
+
const opaque =
|
|
1723
|
+
contract.opaque === true || (contract.wildcard === true && !canonicalType);
|
|
1724
|
+
if (!canonicalType && !opaque) {
|
|
1725
|
+
throw new Error(
|
|
1726
|
+
"Validated flow edge is missing CANONICAL_TYPE and is not marked OPAQUE. " +
|
|
1727
|
+
"An edge that genuinely carries bytes with no SDS identity must assert OPAQUE = true; " +
|
|
1728
|
+
"otherwise declare the concrete SDS identity on its ports.",
|
|
1729
|
+
);
|
|
1730
|
+
}
|
|
1731
|
+
if (canonicalType && opaque) {
|
|
1732
|
+
throw new Error(
|
|
1733
|
+
"Validated flow edge declares BOTH a CANONICAL_TYPE and OPAQUE = true. " +
|
|
1734
|
+
"A contract must carry exactly one: a known identity is never signed away as opaque.",
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
const alignedType = contract.aligned?.producer ?? contract.aligned?.consumer ?? null;
|
|
1738
|
+
const sharedArena =
|
|
1739
|
+
fromDispatchModel === "linked-direct" &&
|
|
1740
|
+
toDispatchModel === "linked-direct";
|
|
1741
|
+
// An opaque edge is ineligible for the aligned route: there is no layout to
|
|
1742
|
+
// prove bounds, alignment, ownership or lifetime against.
|
|
1743
|
+
const alignedEligible =
|
|
1744
|
+
!opaque &&
|
|
1745
|
+
sharedArena &&
|
|
1746
|
+
contract.compatibleWireFormats?.includes("aligned-binary") === true &&
|
|
1747
|
+
Boolean(contract.aligned?.producer && contract.aligned?.consumer);
|
|
1748
|
+
return {
|
|
1749
|
+
canonicalType: canonicalType
|
|
1750
|
+
? { ...canonicalType, wireFormat: "flatbuffer" }
|
|
1751
|
+
: null,
|
|
1752
|
+
alignedType:
|
|
1753
|
+
!opaque && alignedType
|
|
1754
|
+
? { ...alignedType, wireFormat: "aligned-binary" }
|
|
1755
|
+
: null,
|
|
1756
|
+
canonicalFallbackAvailable: true,
|
|
1757
|
+
alignedEligible,
|
|
1758
|
+
opaque,
|
|
1759
|
+
routePolicy: alignedEligible
|
|
1760
|
+
? "aligned-shared-arena-or-canonical"
|
|
1761
|
+
: "canonical-only",
|
|
1762
|
+
};
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
export function buildFlowModuleManifest({ flow, check, dependencies }) {
|
|
1766
|
+
const nodes = Array.isArray(flow?.nodes) ? flow.nodes : [];
|
|
1767
|
+
const edges = Array.isArray(flow?.edges) ? flow.edges : [];
|
|
1768
|
+
const triggers = Array.isArray(flow?.triggers) ? flow.triggers : [];
|
|
1769
|
+
const triggerBindings = Array.isArray(flow?.triggerBindings) ? flow.triggerBindings : [];
|
|
1770
|
+
const nodeById = new Map(nodes.map((node) => [node.nodeId, node]));
|
|
1771
|
+
const checkedNodeById = new Map(
|
|
1772
|
+
check.nodes.map((node) => [node.nodeId, node]),
|
|
1773
|
+
);
|
|
1774
|
+
|
|
1775
|
+
const hostModelNodeIds = new Set(
|
|
1776
|
+
check.nodes.filter((node) => node.dispatchModel === "host").map((node) => node.nodeId),
|
|
1777
|
+
);
|
|
1778
|
+
|
|
1779
|
+
// One method per trigger: the flow's ingress surface. Input ports mirror the
|
|
1780
|
+
// trigger bindings' target ports; output ports mirror the frames delivered
|
|
1781
|
+
// to host-model (egress) nodes.
|
|
1782
|
+
const egressPorts = [];
|
|
1783
|
+
const seenEgressPorts = new Set();
|
|
1784
|
+
for (const edge of edges) {
|
|
1785
|
+
if (!hostModelNodeIds.has(edge.toNodeId)) continue;
|
|
1786
|
+
const fromNode = nodeById.get(edge.fromNodeId);
|
|
1787
|
+
const fromDependency = fromNode ? dependencies.get(fromNode.pluginId) : null;
|
|
1788
|
+
const fromMethod = fromDependency ? rawMethod(fromDependency, fromNode.methodId) : null;
|
|
1789
|
+
const fromPort = (fromMethod?.outputPorts ?? []).find(
|
|
1790
|
+
(port) => port?.portId === edge.fromPortId,
|
|
1791
|
+
);
|
|
1792
|
+
const key = `${edge.toNodeId}:${edge.toPortId}`;
|
|
1793
|
+
if (seenEgressPorts.has(key)) continue;
|
|
1794
|
+
seenEgressPorts.add(key);
|
|
1795
|
+
egressPorts.push(
|
|
1796
|
+
fromPort
|
|
1797
|
+
? clonePortDefinition(fromPort, edge.toPortId)
|
|
1798
|
+
: wildcardPortDefinition(edge.toPortId, { required: false }),
|
|
1799
|
+
);
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
const methods = triggers.map((trigger) => {
|
|
1803
|
+
const inputPorts = [];
|
|
1804
|
+
const seenInputPorts = new Set();
|
|
1805
|
+
for (const binding of triggerBindings) {
|
|
1806
|
+
if (binding.triggerId !== trigger.triggerId) continue;
|
|
1807
|
+
if (seenInputPorts.has(binding.targetPortId)) continue;
|
|
1808
|
+
seenInputPorts.add(binding.targetPortId);
|
|
1809
|
+
const targetNode = nodeById.get(binding.targetNodeId);
|
|
1810
|
+
const targetDependency = targetNode ? dependencies.get(targetNode.pluginId) : null;
|
|
1811
|
+
const targetMethod = targetDependency ? rawMethod(targetDependency, targetNode.methodId) : null;
|
|
1812
|
+
const targetPort = (targetMethod?.inputPorts ?? []).find(
|
|
1813
|
+
(port) => port?.portId === binding.targetPortId,
|
|
1814
|
+
);
|
|
1815
|
+
inputPorts.push(
|
|
1816
|
+
targetPort
|
|
1817
|
+
? clonePortDefinition(targetPort, binding.targetPortId)
|
|
1818
|
+
: wildcardPortDefinition(binding.targetPortId, { required: true }),
|
|
1819
|
+
);
|
|
1820
|
+
}
|
|
1821
|
+
return {
|
|
1822
|
+
methodId: cIdent(trigger.triggerId),
|
|
1823
|
+
displayName: `Flow trigger ${trigger.triggerId}`,
|
|
1824
|
+
description: `Compiled-flow ingress for trigger "${trigger.triggerId}" (${trigger.kind ?? "manual"}).`,
|
|
1825
|
+
inputPorts,
|
|
1826
|
+
outputPorts: egressPorts,
|
|
1827
|
+
maxBatch: 1,
|
|
1828
|
+
drainPolicy: "single-shot",
|
|
1829
|
+
};
|
|
1830
|
+
});
|
|
1831
|
+
|
|
1832
|
+
const externalInterfaces = [];
|
|
1833
|
+
const seenInterfaceIds = new Set();
|
|
1834
|
+
const schemasUsed = [];
|
|
1835
|
+
const seenSchemas = new Set();
|
|
1836
|
+
for (const node of nodes) {
|
|
1837
|
+
const dependency = dependencies.get(node.pluginId);
|
|
1838
|
+
for (const externalInterface of dependency?.manifest?.externalInterfaces ?? []) {
|
|
1839
|
+
if (!externalInterface?.interfaceId || seenInterfaceIds.has(externalInterface.interfaceId)) {
|
|
1840
|
+
continue;
|
|
1841
|
+
}
|
|
1842
|
+
seenInterfaceIds.add(externalInterface.interfaceId);
|
|
1843
|
+
externalInterfaces.push(JSON.parse(JSON.stringify(externalInterface)));
|
|
1844
|
+
}
|
|
1845
|
+
for (const schema of dependency?.manifest?.schemasUsed ?? []) {
|
|
1846
|
+
const key = `${schema?.schemaName ?? ""}:${schema?.fileIdentifier ?? ""}`;
|
|
1847
|
+
if (seenSchemas.has(key)) continue;
|
|
1848
|
+
seenSchemas.add(key);
|
|
1849
|
+
schemasUsed.push(JSON.parse(JSON.stringify(schema)));
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
// Bundle DEPENDENCIES (PLG PluginDependency entries): NODE dependencies
|
|
1854
|
+
// (the modules linked into the artifact as graph nodes, version-bound to
|
|
1855
|
+
// the exact resolved version) merged with the transitively collected
|
|
1856
|
+
// COMPONENT dependencies (modules declared in node manifests' own
|
|
1857
|
+
// DEPENDENCIES). The PLG PluginDependency table has no "kind" field, so
|
|
1858
|
+
// both kinds share the manifest vector; the node/component split is
|
|
1859
|
+
// preserved in artifact.json's dependency records.
|
|
1860
|
+
const seenDependencyIds = new Set();
|
|
1861
|
+
const bundleDependencies = [];
|
|
1862
|
+
for (const node of nodes) {
|
|
1863
|
+
const dependency = dependencies.get(node.pluginId);
|
|
1864
|
+
if (!dependency || seenDependencyIds.has(node.pluginId)) continue;
|
|
1865
|
+
seenDependencyIds.add(node.pluginId);
|
|
1866
|
+
bundleDependencies.push({
|
|
1867
|
+
pluginId: node.pluginId,
|
|
1868
|
+
minVersion: dependency.manifest?.version ?? null,
|
|
1869
|
+
maxVersion: dependency.manifest?.version ?? null,
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
for (const component of check.componentDependencies ?? []) {
|
|
1873
|
+
if (seenDependencyIds.has(component.pluginId)) continue;
|
|
1874
|
+
seenDependencyIds.add(component.pluginId);
|
|
1875
|
+
bundleDependencies.push({
|
|
1876
|
+
pluginId: component.pluginId,
|
|
1877
|
+
minVersion: component.minVersion,
|
|
1878
|
+
maxVersion: component.maxVersion,
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
return {
|
|
1883
|
+
pluginId: flow.programId,
|
|
1884
|
+
name: flow.name ?? flow.programId,
|
|
1885
|
+
version: flow.version ?? "0.0.0",
|
|
1886
|
+
pluginFamily: "flow",
|
|
1887
|
+
description:
|
|
1888
|
+
flow.description ??
|
|
1889
|
+
`Compiled SDN flow "${flow.programId}" (linked-direct monolithic artifact).`,
|
|
1890
|
+
// The flow's permission set is the union of its nodes' declared
|
|
1891
|
+
// capabilities plus resolved component-dependency capabilities, computed
|
|
1892
|
+
// and stamped by `flow check`.
|
|
1893
|
+
capabilities: check.capabilities,
|
|
1894
|
+
dependencies: bundleDependencies,
|
|
1895
|
+
externalInterfaces,
|
|
1896
|
+
methods,
|
|
1897
|
+
schemasUsed,
|
|
1898
|
+
runtimeTargets: ["browser", "wasmedge"],
|
|
1899
|
+
buildArtifacts: [
|
|
1900
|
+
{
|
|
1901
|
+
artifactId: `${cIdent(flow.programId)}-flow-runtime`,
|
|
1902
|
+
kind: "wasm",
|
|
1903
|
+
path: "dist/isomorphic/module.wasm",
|
|
1904
|
+
target: "browser,wasmedge",
|
|
1905
|
+
},
|
|
1906
|
+
],
|
|
1907
|
+
flowNodes: nodes.map((node) => {
|
|
1908
|
+
const checkedNode = checkedNodeById.get(node.nodeId);
|
|
1909
|
+
const config =
|
|
1910
|
+
node.config instanceof Uint8Array
|
|
1911
|
+
? new Uint8Array(node.config)
|
|
1912
|
+
: Array.isArray(node.config)
|
|
1913
|
+
? [...node.config]
|
|
1914
|
+
: undefined;
|
|
1915
|
+
return {
|
|
1916
|
+
nodeId: node.nodeId,
|
|
1917
|
+
pluginId: node.pluginId,
|
|
1918
|
+
methodId: node.methodId ?? null,
|
|
1919
|
+
kind: node.kind ?? null,
|
|
1920
|
+
dispatchModel:
|
|
1921
|
+
checkedNode?.dispatchModel === "host"
|
|
1922
|
+
? "host-capability"
|
|
1923
|
+
: checkedNode?.dispatchModel === "isomorphic"
|
|
1924
|
+
? "isomorphic"
|
|
1925
|
+
: null,
|
|
1926
|
+
...(config ? { config } : {}),
|
|
1927
|
+
uiX: Number(node.uiX ?? 0),
|
|
1928
|
+
uiY: Number(node.uiY ?? 0),
|
|
1929
|
+
};
|
|
1930
|
+
}),
|
|
1931
|
+
flowEdges: edges.map((edge, index) => ({
|
|
1932
|
+
edgeId: edge.edgeId ?? `edge-${index}`,
|
|
1933
|
+
fromNodeId: edge.fromNodeId,
|
|
1934
|
+
fromPortId: edge.fromPortId,
|
|
1935
|
+
toNodeId: edge.toNodeId,
|
|
1936
|
+
toPortId: edge.toPortId,
|
|
1937
|
+
contract: signedFlowEdgeContract(
|
|
1938
|
+
check.edgeTypeContracts?.[index],
|
|
1939
|
+
checkedNodeById.get(edge.fromNodeId)?.dispatchModel,
|
|
1940
|
+
checkedNodeById.get(edge.toNodeId)?.dispatchModel,
|
|
1941
|
+
),
|
|
1942
|
+
})),
|
|
1943
|
+
flowTriggers: triggers.map((trigger) => ({
|
|
1944
|
+
triggerId: trigger.triggerId,
|
|
1945
|
+
kind: trigger.kind ?? null,
|
|
1946
|
+
source: trigger.source ?? null,
|
|
1947
|
+
defaultIntervalMs: Number(trigger.defaultIntervalMs ?? 0),
|
|
1948
|
+
httpPath: trigger.httpPath ?? null,
|
|
1949
|
+
})),
|
|
1950
|
+
flowTriggerBindings: triggerBindings.map((binding) => ({
|
|
1951
|
+
triggerId: binding.triggerId,
|
|
1952
|
+
targetNodeId: binding.targetNodeId,
|
|
1953
|
+
targetPortId: binding.targetPortId,
|
|
1954
|
+
})),
|
|
1955
|
+
abiVersion: 1,
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
// ---------------------------------------------------------------------------
|
|
1960
|
+
// FlowProgram flatbuffer encoding (plain JSON document -> FlowProgramT)
|
|
1961
|
+
// ---------------------------------------------------------------------------
|
|
1962
|
+
|
|
1963
|
+
const nodeKindByName = {
|
|
1964
|
+
trigger: NodeKind.TRIGGER,
|
|
1965
|
+
transform: NodeKind.TRANSFORM,
|
|
1966
|
+
analyzer: NodeKind.ANALYZER,
|
|
1967
|
+
publisher: NodeKind.PUBLISHER,
|
|
1968
|
+
responder: NodeKind.RESPONDER,
|
|
1969
|
+
renderer: NodeKind.RENDERER,
|
|
1970
|
+
sink: NodeKind.SINK,
|
|
1971
|
+
};
|
|
1972
|
+
|
|
1973
|
+
const triggerKindByName = {
|
|
1974
|
+
manual: TriggerKind.MANUAL,
|
|
1975
|
+
timer: TriggerKind.TIMER,
|
|
1976
|
+
"pubsub-subscription": TriggerKind.PUBSUB_SUBSCRIPTION,
|
|
1977
|
+
"protocol-request": TriggerKind.PROTOCOL_REQUEST,
|
|
1978
|
+
"http-request": TriggerKind.HTTP_REQUEST,
|
|
1979
|
+
"orbpro-event": TriggerKind.ORBPRO_EVENT,
|
|
1980
|
+
"scene-event": TriggerKind.SCENE_EVENT,
|
|
1981
|
+
"system-event": TriggerKind.SYSTEM_EVENT,
|
|
1982
|
+
};
|
|
1983
|
+
|
|
1984
|
+
const drainPolicyByName = {
|
|
1985
|
+
"single-shot": DrainPolicy.SINGLE_SHOT,
|
|
1986
|
+
"drain-until-yield": DrainPolicy.DRAIN_UNTIL_YIELD,
|
|
1987
|
+
"drain-to-empty": DrainPolicy.DRAIN_TO_EMPTY,
|
|
1988
|
+
};
|
|
1989
|
+
|
|
1990
|
+
const backpressureByName = {
|
|
1991
|
+
queue: BackpressurePolicy.QUEUE,
|
|
1992
|
+
"drop-oldest": BackpressurePolicy.DROP_OLDEST,
|
|
1993
|
+
"drop-newest": BackpressurePolicy.DROP_NEWEST,
|
|
1994
|
+
};
|
|
1995
|
+
|
|
1996
|
+
function lookupEnum(map, value, fallback) {
|
|
1997
|
+
const key = String(value ?? "").trim().toLowerCase();
|
|
1998
|
+
return map[key] ?? fallback;
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
export function encodeFlowDocumentProgram(flow) {
|
|
2002
|
+
const program = Object.assign(new FlowProgramT(), {
|
|
2003
|
+
programId: flow.programId ?? "",
|
|
2004
|
+
// FlowProgram.name is a required field in the FLOW schema.
|
|
2005
|
+
name: flow.name ?? flow.programId ?? "",
|
|
2006
|
+
version: flow.version ?? null,
|
|
2007
|
+
description: flow.description ?? null,
|
|
2008
|
+
requiredPlugins: Array.isArray(flow.requiredPlugins) ? [...flow.requiredPlugins] : [],
|
|
2009
|
+
nodes: (flow.nodes ?? []).map((node) =>
|
|
2010
|
+
Object.assign(new FlowNodeT(), {
|
|
2011
|
+
nodeId: node.nodeId ?? "",
|
|
2012
|
+
pluginId: node.pluginId ?? "",
|
|
2013
|
+
methodId: node.methodId ?? "",
|
|
2014
|
+
kind: lookupEnum(nodeKindByName, node.kind, NodeKind.TRANSFORM),
|
|
2015
|
+
drainPolicy: lookupEnum(drainPolicyByName, node.drainPolicy, DrainPolicy.DRAIN_UNTIL_YIELD),
|
|
2016
|
+
timeSliceMicros: Number(node.timeSliceMicros ?? 0),
|
|
2017
|
+
}),
|
|
2018
|
+
),
|
|
2019
|
+
edges: (flow.edges ?? []).map((edge, index) =>
|
|
2020
|
+
Object.assign(new FlowEdgeT(), {
|
|
2021
|
+
edgeId: edge.edgeId ?? `edge-${index}`,
|
|
2022
|
+
fromNodeId: edge.fromNodeId ?? "",
|
|
2023
|
+
fromPortId: edge.fromPortId ?? "",
|
|
2024
|
+
toNodeId: edge.toNodeId ?? "",
|
|
2025
|
+
toPortId: edge.toPortId ?? "",
|
|
2026
|
+
acceptedTypes: [],
|
|
2027
|
+
backpressurePolicy: lookupEnum(backpressureByName, edge.backpressurePolicy, BackpressurePolicy.QUEUE),
|
|
2028
|
+
queueDepth: Number(edge.queueDepth ?? 1),
|
|
2029
|
+
}),
|
|
2030
|
+
),
|
|
2031
|
+
triggers: (flow.triggers ?? []).map((trigger) =>
|
|
2032
|
+
Object.assign(new FlowTriggerT(), {
|
|
2033
|
+
triggerId: trigger.triggerId ?? "",
|
|
2034
|
+
kind: lookupEnum(triggerKindByName, trigger.kind, TriggerKind.MANUAL),
|
|
2035
|
+
source: trigger.source ?? null,
|
|
2036
|
+
protocolId: trigger.protocolId ?? null,
|
|
2037
|
+
defaultIntervalMs: BigInt(trigger.defaultIntervalMs ?? 0),
|
|
2038
|
+
acceptedTypes: [],
|
|
2039
|
+
description: trigger.description ?? null,
|
|
2040
|
+
}),
|
|
2041
|
+
),
|
|
2042
|
+
triggerBindings: (flow.triggerBindings ?? []).map((binding) =>
|
|
2043
|
+
Object.assign(new TriggerBindingT(), {
|
|
2044
|
+
triggerId: binding.triggerId ?? "",
|
|
2045
|
+
targetNodeId: binding.targetNodeId ?? "",
|
|
2046
|
+
targetPortId: binding.targetPortId ?? "",
|
|
2047
|
+
backpressurePolicy: lookupEnum(backpressureByName, binding.backpressurePolicy, BackpressurePolicy.QUEUE),
|
|
2048
|
+
queueDepth: Number(binding.queueDepth ?? 1),
|
|
2049
|
+
}),
|
|
2050
|
+
),
|
|
2051
|
+
});
|
|
2052
|
+
return encodeFlowProgram(program);
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
// ---------------------------------------------------------------------------
|
|
2056
|
+
// flow_generated.inc codegen
|
|
2057
|
+
// ---------------------------------------------------------------------------
|
|
2058
|
+
|
|
2059
|
+
export function generateFlowTables({ flow, check, dependencies }) {
|
|
2060
|
+
const nodes = Array.isArray(flow?.nodes) ? flow.nodes : [];
|
|
2061
|
+
const edges = Array.isArray(flow?.edges) ? flow.edges : [];
|
|
2062
|
+
const triggers = Array.isArray(flow?.triggers) ? flow.triggers : [];
|
|
2063
|
+
const triggerBindings = Array.isArray(flow?.triggerBindings) ? flow.triggerBindings : [];
|
|
2064
|
+
const nodeIndex = new Map(nodes.map((node, index) => [node.nodeId, index]));
|
|
2065
|
+
const triggerIndex = new Map(triggers.map((trigger, index) => [trigger.triggerId, index]));
|
|
2066
|
+
const dispatchByNodeId = new Map(check.nodes.map((node) => [node.nodeId, node.dispatchModel]));
|
|
2067
|
+
|
|
2068
|
+
// Linked dependencies (deduped by pluginId, in first-use order). In
|
|
2069
|
+
// engine-linked mode a dependency's `guest-link-linked` object variant
|
|
2070
|
+
// (compiled -DSDN_FLATSQL_LINKED, calling the runtime template's direct
|
|
2071
|
+
// engine helpers instead of storage.flatsql_* hostcalls) is preferred when
|
|
2072
|
+
// the package ships one; pure compute nodes fall back to their standard
|
|
2073
|
+
// object (both variants share the deterministic symbol prefix).
|
|
2074
|
+
const engineLinked = Boolean(check.engineLinkage);
|
|
2075
|
+
const linkedDependencies = [];
|
|
2076
|
+
const linkedDependencyIndexByPluginId = new Map();
|
|
2077
|
+
for (const node of nodes) {
|
|
2078
|
+
if (dispatchByNodeId.get(node.nodeId) !== "linked-direct") continue;
|
|
2079
|
+
if (linkedDependencyIndexByPluginId.has(node.pluginId)) continue;
|
|
2080
|
+
const dependency = dependencies.get(node.pluginId);
|
|
2081
|
+
const chosenGuestLink = selectedGuestLink(dependency, engineLinked);
|
|
2082
|
+
if (!chosenGuestLink) {
|
|
2083
|
+
throw new Error(
|
|
2084
|
+
`Plugin "${node.pluginId}" has no guest-link object (dist/guest-link/${GUEST_LINK_OBJECT_FILENAME}); ` +
|
|
2085
|
+
"rebuild the module with a build that persists compileModuleFromSource's guestLink output.",
|
|
2086
|
+
);
|
|
2087
|
+
}
|
|
2088
|
+
linkedDependencyIndexByPluginId.set(node.pluginId, linkedDependencies.length);
|
|
2089
|
+
linkedDependencies.push({ ...dependency, guestLink: chosenGuestLink });
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
// Runtime dependency descriptors cover both monolithically linked guests
|
|
2093
|
+
// and exact hash-bound isomorphic children. Isomorphic children deliberately
|
|
2094
|
+
// carry no guest-link object: both hosts instantiate their signed artifact
|
|
2095
|
+
// bytes separately and service the graph-selected invocation descriptor.
|
|
2096
|
+
const runtimeDependencies = [];
|
|
2097
|
+
const runtimeDependencyIndexByPluginId = new Map();
|
|
2098
|
+
for (const node of nodes) {
|
|
2099
|
+
const dispatchModel = dispatchByNodeId.get(node.nodeId) ?? "host";
|
|
2100
|
+
if (dispatchModel !== "linked-direct" && dispatchModel !== "isomorphic") {
|
|
2101
|
+
continue;
|
|
2102
|
+
}
|
|
2103
|
+
if (runtimeDependencyIndexByPluginId.has(node.pluginId)) continue;
|
|
2104
|
+
const dependency = dependencies.get(node.pluginId);
|
|
2105
|
+
const runtimeDependency =
|
|
2106
|
+
dispatchModel === "linked-direct"
|
|
2107
|
+
? linkedDependencies[linkedDependencyIndexByPluginId.get(node.pluginId)]
|
|
2108
|
+
: { ...dependency, guestLink: null, artifactLock: { ...node.artifact } };
|
|
2109
|
+
runtimeDependencyIndexByPluginId.set(node.pluginId, runtimeDependencies.length);
|
|
2110
|
+
runtimeDependencies.push(runtimeDependency);
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
const nodeEntries = nodes.map((node) => {
|
|
2114
|
+
const dispatchModel = dispatchByNodeId.get(node.nodeId) ?? "host";
|
|
2115
|
+
if (dispatchModel === "isomorphic") {
|
|
2116
|
+
return {
|
|
2117
|
+
node,
|
|
2118
|
+
dispatchModel,
|
|
2119
|
+
entrySymbol: null,
|
|
2120
|
+
dependency:
|
|
2121
|
+
runtimeDependencies[runtimeDependencyIndexByPluginId.get(node.pluginId)],
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
if (dispatchModel !== "linked-direct") {
|
|
2125
|
+
return { node, dispatchModel, entrySymbol: null, dependency: null };
|
|
2126
|
+
}
|
|
2127
|
+
const dependency = linkedDependencies[linkedDependencyIndexByPluginId.get(node.pluginId)];
|
|
2128
|
+
const entrySymbol = dependency.guestLink.metadata?.methodSymbols?.[node.methodId];
|
|
2129
|
+
if (!entrySymbol) {
|
|
2130
|
+
throw new Error(
|
|
2131
|
+
`Guest-link metadata for plugin "${node.pluginId}" carries no method symbol for "${node.methodId}".`,
|
|
2132
|
+
);
|
|
2133
|
+
}
|
|
2134
|
+
return { node, dispatchModel, entrySymbol, dependency };
|
|
2135
|
+
});
|
|
2136
|
+
|
|
2137
|
+
// Required input ports per node — the compiled scheduler's readiness rule.
|
|
2138
|
+
const requiredPorts = [];
|
|
2139
|
+
for (const entry of nodeEntries) {
|
|
2140
|
+
if (!entry.dependency) continue;
|
|
2141
|
+
const method = findMethod(entry.dependency.normalized, entry.node.methodId);
|
|
2142
|
+
for (const port of method?.inputPorts ?? []) {
|
|
2143
|
+
if (port.required !== false && Number(port.minStreams ?? 1) > 0) {
|
|
2144
|
+
requiredPorts.push({ nodeIndex: nodeIndex.get(entry.node.nodeId), portId: port.portId });
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
const reservedTriggerFromPorts = new Set();
|
|
2150
|
+
for (const dependency of dependencies.values()) {
|
|
2151
|
+
for (const method of dependency?.normalized?.methods ?? []) {
|
|
2152
|
+
for (const outputPort of method?.outputPorts ?? []) {
|
|
2153
|
+
if (outputPort?.portId) reservedTriggerFromPorts.add(outputPort.portId);
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
const triggerBindingDescriptors = triggerBindings.map((binding, index) => {
|
|
2159
|
+
const targetNodeIndex = nodeIndex.get(binding.targetNodeId);
|
|
2160
|
+
const targetEntry = nodeEntries[targetNodeIndex];
|
|
2161
|
+
const targetMethod = targetEntry?.dependency
|
|
2162
|
+
? findMethod(targetEntry.dependency.normalized, targetEntry.node.methodId)
|
|
2163
|
+
: null;
|
|
2164
|
+
const targetPort = (targetMethod?.inputPorts ?? []).find(
|
|
2165
|
+
(port) => port?.portId === binding.targetPortId,
|
|
2166
|
+
);
|
|
2167
|
+
const targetTypes = portAcceptedTypes(targetPort);
|
|
2168
|
+
const canonicalTypes = concreteTypesByWireFormat(targetTypes, "flatbuffer");
|
|
2169
|
+
const alignedTypes = concreteTypesByWireFormat(targetTypes, "aligned-binary");
|
|
2170
|
+
// WILDCARD TRIGGER TARGET — the same 4810b01 regression as the edge contract
|
|
2171
|
+
// above, one surface further in. `concreteTypesByWireFormat()` filters
|
|
2172
|
+
// wildcards out, so a trigger aimed at an application-blind port
|
|
2173
|
+
// (celestrak-request `gp.tick`, and every timer-driven hostcap node) yields
|
|
2174
|
+
// ZERO canonical and ZERO aligned types and threw here — `flow check` passed
|
|
2175
|
+
// while `flow compile` could not emit the family. An untyped trigger frame
|
|
2176
|
+
// carries no SDS identity and no aligned layout, exactly like the untyped
|
|
2177
|
+
// edges resolved in resolveEdgeTypeContract; it is emitted as such rather
|
|
2178
|
+
// than inventing one. A target with concrete types still takes the strict
|
|
2179
|
+
// paired path below.
|
|
2180
|
+
const wildcardTarget =
|
|
2181
|
+
canonicalTypes.length === 0 &&
|
|
2182
|
+
alignedTypes.length === 0 &&
|
|
2183
|
+
targetTypes.some((type) => type?.acceptsAnyFlatbuffer === true);
|
|
2184
|
+
// The aligned peer is OPTIONAL here for the same reason it is optional on
|
|
2185
|
+
// an edge (see resolveEdgeTypeContract): it offers the shared-arena
|
|
2186
|
+
// zero-copy route, it is not what makes the frame legal. Demanding one made
|
|
2187
|
+
// every HTTP-served flow uncompilable — the $HTQ HttpRequest envelope is a
|
|
2188
|
+
// variable-length record and declares no aligned peer, so `flow check`
|
|
2189
|
+
// passed and `flow compile` threw on node-status, node-activity,
|
|
2190
|
+
// public-query and data-retrieval alike. `alignedEligible` below already
|
|
2191
|
+
// carries "there is no aligned route on this binding" to the descriptor.
|
|
2192
|
+
if (!wildcardTarget && (canonicalTypes.length !== 1 || alignedTypes.length > 1)) {
|
|
2193
|
+
throw new Error(
|
|
2194
|
+
`Validated trigger binding ${index} (${binding.targetNodeId}.${binding.targetPortId}) ` +
|
|
2195
|
+
"must resolve to exactly one canonical SDS type, with at most one aligned-binary peer.",
|
|
2196
|
+
);
|
|
2197
|
+
}
|
|
2198
|
+
const canonical = wildcardTarget ? null : canonicalTypes[0];
|
|
2199
|
+
const aligned = wildcardTarget ? null : (alignedTypes[0] ?? null);
|
|
2200
|
+
const byteLength = edgeLayoutScalar(aligned?.byteLength, "trigger aligned byteLength");
|
|
2201
|
+
const fixedStringLength = edgeLayoutScalar(
|
|
2202
|
+
aligned?.fixedStringLength,
|
|
2203
|
+
"trigger aligned fixedStringLength",
|
|
2204
|
+
);
|
|
2205
|
+
const requiredAlignment = edgeLayoutScalar(
|
|
2206
|
+
aligned?.requiredAlignment,
|
|
2207
|
+
"trigger aligned requiredAlignment",
|
|
2208
|
+
);
|
|
2209
|
+
const sentinelBase = `@trigger:${binding.triggerId}:${index}`;
|
|
2210
|
+
let sentinelFromPort = sentinelBase;
|
|
2211
|
+
let collisionIndex = 0;
|
|
2212
|
+
while (reservedTriggerFromPorts.has(sentinelFromPort)) {
|
|
2213
|
+
collisionIndex += 1;
|
|
2214
|
+
sentinelFromPort = `${sentinelBase}:${collisionIndex}`;
|
|
2215
|
+
}
|
|
2216
|
+
reservedTriggerFromPorts.add(sentinelFromPort);
|
|
2217
|
+
return {
|
|
2218
|
+
binding,
|
|
2219
|
+
canonical,
|
|
2220
|
+
sentinelFromPort,
|
|
2221
|
+
// A wildcard trigger target resolves no SDS identity and no aligned
|
|
2222
|
+
// layout — the same fact an opaque edge asserts, recorded the same way.
|
|
2223
|
+
opaque: wildcardTarget,
|
|
2224
|
+
alignedEligible: aligned !== null,
|
|
2225
|
+
alignedLayoutFields:
|
|
2226
|
+
(byteLength.present ? 1 : 0) |
|
|
2227
|
+
(fixedStringLength.present ? 2 : 0) |
|
|
2228
|
+
(requiredAlignment.present ? 4 : 0),
|
|
2229
|
+
byteLength: byteLength.value,
|
|
2230
|
+
fixedStringLength: fixedStringLength.value,
|
|
2231
|
+
requiredAlignment: requiredAlignment.value,
|
|
2232
|
+
schemaHash: schemaHashBytes(canonical?.schemaHash),
|
|
2233
|
+
};
|
|
2234
|
+
});
|
|
2235
|
+
|
|
2236
|
+
const edgeDescriptors = edges.map((edge, index) => {
|
|
2237
|
+
const contract = check.edgeTypeContracts?.[index];
|
|
2238
|
+
if (!contract) {
|
|
2239
|
+
throw new Error(
|
|
2240
|
+
`Validated flow is missing the compiled type contract for edge ${index} ` +
|
|
2241
|
+
`(${edge.fromNodeId}.${edge.fromPortId} -> ${edge.toNodeId}.${edge.toPortId}).`,
|
|
2242
|
+
);
|
|
2243
|
+
}
|
|
2244
|
+
const canonicalFallbackAvailable =
|
|
2245
|
+
contract.compatibleWireFormats?.includes("flatbuffer") === true &&
|
|
2246
|
+
Boolean(contract.canonical?.producer || contract.canonical?.consumer);
|
|
2247
|
+
const sharedArenaEligible =
|
|
2248
|
+
dispatchByNodeId.get(edge.fromNodeId) === "linked-direct" &&
|
|
2249
|
+
dispatchByNodeId.get(edge.toNodeId) === "linked-direct";
|
|
2250
|
+
const alignedEligible =
|
|
2251
|
+
sharedArenaEligible &&
|
|
2252
|
+
contract.compatibleWireFormats?.includes("aligned-binary") === true &&
|
|
2253
|
+
Boolean(contract.aligned?.producer && contract.aligned?.consumer);
|
|
2254
|
+
const alignedLayout = alignedEligible ? contract.aligned.producer : null;
|
|
2255
|
+
const byteLength = edgeLayoutScalar(
|
|
2256
|
+
alignedLayout?.byteLength,
|
|
2257
|
+
"aligned byteLength",
|
|
2258
|
+
);
|
|
2259
|
+
const fixedStringLength = edgeLayoutScalar(
|
|
2260
|
+
alignedLayout?.fixedStringLength,
|
|
2261
|
+
"aligned fixedStringLength",
|
|
2262
|
+
);
|
|
2263
|
+
const requiredAlignment = edgeLayoutScalar(
|
|
2264
|
+
alignedLayout?.requiredAlignment,
|
|
2265
|
+
"aligned requiredAlignment",
|
|
2266
|
+
);
|
|
2267
|
+
return {
|
|
2268
|
+
edge,
|
|
2269
|
+
contract,
|
|
2270
|
+
// OPAQUE (SDS $PLG 1.0.13 / v1.164.0) reaches the compiled routing table.
|
|
2271
|
+
// Without it an opaque edge has neither a canonical type nor an aligned
|
|
2272
|
+
// layout, so BOTH typed routes in flow_runtime.cpp reject every frame and
|
|
2273
|
+
// the flow is inert after its first node. Opacity is the third, explicit
|
|
2274
|
+
// route: bytes, no identity, no layout.
|
|
2275
|
+
opaque: contract.opaque === true,
|
|
2276
|
+
canonicalFallbackAvailable,
|
|
2277
|
+
alignedEligible,
|
|
2278
|
+
alignedLayoutFields:
|
|
2279
|
+
(byteLength.present ? 1 : 0) |
|
|
2280
|
+
(fixedStringLength.present ? 2 : 0) |
|
|
2281
|
+
(requiredAlignment.present ? 4 : 0),
|
|
2282
|
+
byteLength: byteLength.value,
|
|
2283
|
+
fixedStringLength: fixedStringLength.value,
|
|
2284
|
+
requiredAlignment: requiredAlignment.value,
|
|
2285
|
+
schemaHash: schemaHashBytes(contract.schemaHash),
|
|
2286
|
+
};
|
|
2287
|
+
});
|
|
2288
|
+
|
|
2289
|
+
const lines = [];
|
|
2290
|
+
lines.push("// Generated from the flow document by space-data-module flow compile — do not edit.");
|
|
2291
|
+
lines.push(`#define FLOW_NODE_COUNT ${nodes.length}u`);
|
|
2292
|
+
lines.push(`#define FLOW_ROUTE_EDGE_COUNT ${edges.length}u`);
|
|
2293
|
+
lines.push(`#define FLOW_EDGE_COUNT ${edges.length + triggerBindingDescriptors.length}u`);
|
|
2294
|
+
lines.push(`#define FLOW_TRIGGER_COUNT ${triggers.length}u`);
|
|
2295
|
+
lines.push(`#define FLOW_DEP_COUNT ${runtimeDependencies.length}u`);
|
|
2296
|
+
lines.push(`#define FLOW_TRIGGER_BINDING_COUNT ${triggerBindings.length}u`);
|
|
2297
|
+
lines.push(`#define FLOW_REQUIRED_PORT_COUNT ${requiredPorts.length}u`);
|
|
2298
|
+
lines.push("");
|
|
2299
|
+
|
|
2300
|
+
// Prefixed guest-link entry declarations.
|
|
2301
|
+
const declaredEntries = new Set();
|
|
2302
|
+
for (const entry of nodeEntries) {
|
|
2303
|
+
if (entry.entrySymbol && !declaredEntries.has(entry.entrySymbol)) {
|
|
2304
|
+
declaredEntries.add(entry.entrySymbol);
|
|
2305
|
+
lines.push(`extern "C" int ${entry.entrySymbol}(void);`);
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
lines.push("");
|
|
2309
|
+
|
|
2310
|
+
const strConsts = [];
|
|
2311
|
+
const strName = (tag, value) => {
|
|
2312
|
+
const name = `kStr_${tag}`;
|
|
2313
|
+
strConsts.push(`static const char ${name}[] = ${cString(value)};`);
|
|
2314
|
+
return name;
|
|
2315
|
+
};
|
|
2316
|
+
const nodeStrs = nodeEntries.map((entry, index) => ({
|
|
2317
|
+
id: strName(`node${index}_id`, entry.node.nodeId),
|
|
2318
|
+
dep: entry.dependency ? strName(`node${index}_dep`, entry.dependency.pluginId) : null,
|
|
2319
|
+
plugin: strName(`node${index}_plugin`, entry.node.pluginId),
|
|
2320
|
+
method: strName(`node${index}_method`, entry.node.methodId),
|
|
2321
|
+
model: strName(`node${index}_model`, entry.dispatchModel),
|
|
2322
|
+
entry: entry.entrySymbol ? strName(`node${index}_entry`, entry.entrySymbol) : null,
|
|
2323
|
+
}));
|
|
2324
|
+
const depStrs = runtimeDependencies.map((dependency, index) => ({
|
|
2325
|
+
id: strName(`dep${index}_id`, dependency.pluginId),
|
|
2326
|
+
plugin: strName(`dep${index}_plugin`, dependency.pluginId),
|
|
2327
|
+
version: strName(`dep${index}_version`, dependency.manifest?.version ?? ""),
|
|
2328
|
+
sha256: strName(`dep${index}_sha256`, dependency.artifactLock?.sha256 ?? ""),
|
|
2329
|
+
entry: dependency.guestLink
|
|
2330
|
+
? strName(
|
|
2331
|
+
`dep${index}_entry`,
|
|
2332
|
+
Object.values(dependency.guestLink.metadata?.methodSymbols ?? {})[0] ?? "",
|
|
2333
|
+
)
|
|
2334
|
+
: null,
|
|
2335
|
+
}));
|
|
2336
|
+
const sym = {
|
|
2337
|
+
malloc: strName("sym_malloc", "malloc"),
|
|
2338
|
+
free: strName("sym_free", "free"),
|
|
2339
|
+
};
|
|
2340
|
+
lines.push(...strConsts, "");
|
|
2341
|
+
|
|
2342
|
+
for (const [index, descriptor] of edgeDescriptors.entries()) {
|
|
2343
|
+
if (descriptor.schemaHash.length === 0) continue;
|
|
2344
|
+
lines.push(
|
|
2345
|
+
`static const uint8_t kEdge${index}SchemaHash[] = { ${descriptor.schemaHash
|
|
2346
|
+
.map((byte) => `0x${byte.toString(16).padStart(2, "0")}`)
|
|
2347
|
+
.join(", ")} };`,
|
|
2348
|
+
);
|
|
2349
|
+
}
|
|
2350
|
+
for (const [index, descriptor] of triggerBindingDescriptors.entries()) {
|
|
2351
|
+
if (descriptor.schemaHash.length === 0) continue;
|
|
2352
|
+
lines.push(
|
|
2353
|
+
`static const uint8_t kTriggerBinding${index}SchemaHash[] = { ${descriptor.schemaHash
|
|
2354
|
+
.map((byte) => `0x${byte.toString(16).padStart(2, "0")}`)
|
|
2355
|
+
.join(", ")} };`,
|
|
2356
|
+
);
|
|
2357
|
+
}
|
|
2358
|
+
if (
|
|
2359
|
+
edgeDescriptors.some((descriptor) => descriptor.schemaHash.length > 0) ||
|
|
2360
|
+
triggerBindingDescriptors.some((descriptor) => descriptor.schemaHash.length > 0)
|
|
2361
|
+
) {
|
|
2362
|
+
lines.push("");
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
lines.push(
|
|
2366
|
+
`FlowEdge g_edges[FLOW_EDGE_COUNT${edges.length + triggerBindingDescriptors.length === 0 ? " + 1" : ""}] = {`,
|
|
2367
|
+
);
|
|
2368
|
+
for (const [index, descriptor] of edgeDescriptors.entries()) {
|
|
2369
|
+
const { edge, contract } = descriptor;
|
|
2370
|
+
lines.push(
|
|
2371
|
+
` { ${nodeIndex.get(edge.fromNodeId)}u, ${cString(edge.fromPortId)}, ` +
|
|
2372
|
+
`${nodeIndex.get(edge.toNodeId)}u, ${cString(edge.toPortId)}, ` +
|
|
2373
|
+
`${cNullableString(contract.schemaName)}, ${cNullableString(contract.fileIdentifier)}, ` +
|
|
2374
|
+
`${cNullableString(contract.schemaVersion)}, ` +
|
|
2375
|
+
`${descriptor.schemaHash.length > 0 ? `kEdge${index}SchemaHash` : "nullptr"}, ` +
|
|
2376
|
+
`${descriptor.schemaHash.length}u, ${cNullableString(contract.rootTypeName)}, ` +
|
|
2377
|
+
`${descriptor.canonicalFallbackAvailable ? 1 : 0}u, ` +
|
|
2378
|
+
`${descriptor.alignedEligible ? 1 : 0}u, ${descriptor.alignedLayoutFields}u, ` +
|
|
2379
|
+
`${descriptor.byteLength}u, ${descriptor.fixedStringLength}u, ` +
|
|
2380
|
+
`${descriptor.requiredAlignment}u, ${descriptor.opaque ? 1 : 0}u },`,
|
|
2381
|
+
);
|
|
2382
|
+
}
|
|
2383
|
+
for (const [index, descriptor] of triggerBindingDescriptors.entries()) {
|
|
2384
|
+
const { binding, canonical } = descriptor;
|
|
2385
|
+
lines.push(
|
|
2386
|
+
` { ${nodeIndex.get(binding.targetNodeId)}u, ` +
|
|
2387
|
+
`${cString(descriptor.sentinelFromPort)}, ` +
|
|
2388
|
+
`${nodeIndex.get(binding.targetNodeId)}u, ${cString(binding.targetPortId)}, ` +
|
|
2389
|
+
`${cNullableString(canonical?.schemaName)}, ${cNullableString(canonical?.fileIdentifier)}, ` +
|
|
2390
|
+
`${cNullableString(canonical?.schemaVersion)}, ` +
|
|
2391
|
+
`${descriptor.schemaHash.length > 0 ? `kTriggerBinding${index}SchemaHash` : "nullptr"}, ` +
|
|
2392
|
+
`${descriptor.schemaHash.length}u, ${cNullableString(canonical?.rootTypeName)}, ` +
|
|
2393
|
+
`1u, ${descriptor.alignedEligible ? 1 : 0}u, ${descriptor.alignedLayoutFields}u, ${descriptor.byteLength}u, ` +
|
|
2394
|
+
`${descriptor.fixedStringLength}u, ${descriptor.requiredAlignment}u, ` +
|
|
2395
|
+
`${descriptor.opaque ? 1 : 0}u },`,
|
|
2396
|
+
);
|
|
2397
|
+
}
|
|
2398
|
+
lines.push("};");
|
|
2399
|
+
lines.push(
|
|
2400
|
+
`FlowTriggerBinding g_trigger_bindings[FLOW_TRIGGER_BINDING_COUNT${triggerBindings.length === 0 ? " + 1" : ""}] = {`,
|
|
2401
|
+
);
|
|
2402
|
+
for (const [index, descriptor] of triggerBindingDescriptors.entries()) {
|
|
2403
|
+
const { binding } = descriptor;
|
|
2404
|
+
lines.push(
|
|
2405
|
+
` { ${triggerIndex.get(binding.triggerId)}u, ${nodeIndex.get(binding.targetNodeId)}u, ` +
|
|
2406
|
+
`${cString(binding.targetPortId)}, ${edges.length + index}u },`,
|
|
2407
|
+
);
|
|
2408
|
+
}
|
|
2409
|
+
lines.push("};");
|
|
2410
|
+
lines.push(
|
|
2411
|
+
`FlowRequiredPort g_required_ports[FLOW_REQUIRED_PORT_COUNT${requiredPorts.length === 0 ? " + 1" : ""}] = {`,
|
|
2412
|
+
);
|
|
2413
|
+
for (const requirement of requiredPorts) {
|
|
2414
|
+
lines.push(` { ${requirement.nodeIndex}u, ${cString(requirement.portId)} },`);
|
|
2415
|
+
}
|
|
2416
|
+
lines.push("};");
|
|
2417
|
+
lines.push("");
|
|
2418
|
+
lines.push("FlowNodeDispatchDescriptorC g_dispatch_descriptors[FLOW_NODE_COUNT];");
|
|
2419
|
+
lines.push(
|
|
2420
|
+
`SignedArtifactDependencyDescriptorC g_dependency_descriptors[FLOW_DEP_COUNT${linkedDependencies.length === 0 ? " + 1" : ""}];`,
|
|
2421
|
+
);
|
|
2422
|
+
lines.push("");
|
|
2423
|
+
lines.push("static void flow_init_descriptors() {");
|
|
2424
|
+
nodeEntries.forEach((entry, index) => {
|
|
2425
|
+
const strs = nodeStrs[index];
|
|
2426
|
+
const dependencyIdx = entry.dependency
|
|
2427
|
+
? `${runtimeDependencyIndexByPluginId.get(entry.dependency.pluginId)}u`
|
|
2428
|
+
: "0xFFFFFFFFu";
|
|
2429
|
+
lines.push(" {");
|
|
2430
|
+
lines.push(` FlowNodeDispatchDescriptorC &d = g_dispatch_descriptors[${index}];`);
|
|
2431
|
+
lines.push(` d.node_id_ptr = reinterpret_cast<uint32_t>(${strs.id});`);
|
|
2432
|
+
lines.push(` d.node_index = ${index}u;`);
|
|
2433
|
+
lines.push(` d.dependency_id_ptr = ${strs.dep ? `reinterpret_cast<uint32_t>(${strs.dep})` : "0"};`);
|
|
2434
|
+
lines.push(` d.dependency_index = ${dependencyIdx};`);
|
|
2435
|
+
lines.push(` d.plugin_id_ptr = reinterpret_cast<uint32_t>(${strs.plugin});`);
|
|
2436
|
+
lines.push(` d.method_id_ptr = reinterpret_cast<uint32_t>(${strs.method});`);
|
|
2437
|
+
lines.push(` d.dispatch_model_ptr = reinterpret_cast<uint32_t>(${strs.model});`);
|
|
2438
|
+
lines.push(` d.entrypoint_ptr = ${strs.entry ? `reinterpret_cast<uint32_t>(${strs.entry})` : "0"};`);
|
|
2439
|
+
lines.push(` d.malloc_symbol_ptr = reinterpret_cast<uint32_t>(${sym.malloc});`);
|
|
2440
|
+
lines.push(` d.free_symbol_ptr = reinterpret_cast<uint32_t>(${sym.free});`);
|
|
2441
|
+
lines.push(` d.stream_invoke_symbol_ptr = ${strs.entry ? `reinterpret_cast<uint32_t>(${strs.entry})` : "0"};`);
|
|
2442
|
+
lines.push(" }");
|
|
2443
|
+
});
|
|
2444
|
+
runtimeDependencies.forEach((dependency, index) => {
|
|
2445
|
+
const strs = depStrs[index];
|
|
2446
|
+
lines.push(" {");
|
|
2447
|
+
lines.push(` SignedArtifactDependencyDescriptorC &dd = g_dependency_descriptors[${index}];`);
|
|
2448
|
+
lines.push(` dd.dependency_id_ptr = reinterpret_cast<uint32_t>(${strs.id});`);
|
|
2449
|
+
lines.push(` dd.plugin_id_ptr = reinterpret_cast<uint32_t>(${strs.plugin});`);
|
|
2450
|
+
lines.push(` dd.version_ptr = reinterpret_cast<uint32_t>(${strs.version});`);
|
|
2451
|
+
lines.push(` dd.sha256_ptr = reinterpret_cast<uint32_t>(${strs.sha256});`);
|
|
2452
|
+
lines.push(` dd.entrypoint_ptr = ${strs.entry ? `reinterpret_cast<uint32_t>(${strs.entry})` : "0"};`);
|
|
2453
|
+
lines.push(` dd.malloc_symbol_ptr = reinterpret_cast<uint32_t>(${sym.malloc});`);
|
|
2454
|
+
lines.push(` dd.free_symbol_ptr = reinterpret_cast<uint32_t>(${sym.free});`);
|
|
2455
|
+
lines.push(` dd.stream_invoke_symbol_ptr = ${strs.entry ? `reinterpret_cast<uint32_t>(${strs.entry})` : "0"};`);
|
|
2456
|
+
lines.push(" }");
|
|
2457
|
+
});
|
|
2458
|
+
lines.push("}");
|
|
2459
|
+
lines.push(
|
|
2460
|
+
"static struct FlowDescriptorInit { FlowDescriptorInit() { flow_init_descriptors(); } } g_flow_descriptor_init;",
|
|
2461
|
+
);
|
|
2462
|
+
lines.push("");
|
|
2463
|
+
|
|
2464
|
+
const linked = nodeEntries
|
|
2465
|
+
.map((entry, index) => ({ entry, index }))
|
|
2466
|
+
.filter(({ entry }) => entry.dispatchModel === "linked-direct");
|
|
2467
|
+
lines.push("static inline bool flow_node_is_linked(uint32_t node) {");
|
|
2468
|
+
lines.push(
|
|
2469
|
+
` switch (node) { ${linked.map(({ index }) => `case ${index}u:`).join(" ")} return true; default: return false; }`,
|
|
2470
|
+
);
|
|
2471
|
+
lines.push("}");
|
|
2472
|
+
lines.push("static inline int32_t flow_call_entry(uint32_t node) {");
|
|
2473
|
+
lines.push(" switch (node) {");
|
|
2474
|
+
for (const { entry, index } of linked) {
|
|
2475
|
+
lines.push(` case ${index}u: return ${entry.entrySymbol}();`);
|
|
2476
|
+
}
|
|
2477
|
+
lines.push(" default: return -1;");
|
|
2478
|
+
lines.push(" }");
|
|
2479
|
+
lines.push("}");
|
|
2480
|
+
return {
|
|
2481
|
+
source: lines.join("\n") + "\n",
|
|
2482
|
+
linkedDependencies,
|
|
2483
|
+
runtimeDependencies,
|
|
2484
|
+
};
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
// ---------------------------------------------------------------------------
|
|
2488
|
+
// Compilation — flow compile (thread-model-matched monolithic link)
|
|
2489
|
+
// ---------------------------------------------------------------------------
|
|
2490
|
+
|
|
2491
|
+
async function linkFlowArtifactWithEmception({ runtimeSource, invokeHeader, generatedInc, manifestSource, programSource, dependencyObjects, engineLinked = false }) {
|
|
2492
|
+
return runWithEmceptionLock(async (emception) => {
|
|
2493
|
+
const workDir = "/working/space-data-module-flow-compile";
|
|
2494
|
+
const write = (name, content) => emception.writeFile(`${workDir}/${name}`, content);
|
|
2495
|
+
emception.FS.mkdirTree(workDir);
|
|
2496
|
+
try {
|
|
2497
|
+
write("space_data_module_invoke.h", invokeHeader);
|
|
2498
|
+
write("flow_runtime.cpp", runtimeSource);
|
|
2499
|
+
write("flow_generated.inc", generatedInc);
|
|
2500
|
+
write("flow-manifest-exports.cpp", manifestSource);
|
|
2501
|
+
write("flow-program-exports.cpp", programSource);
|
|
2502
|
+
const objectPaths = [];
|
|
2503
|
+
dependencyObjects.forEach((objectBytes, index) => {
|
|
2504
|
+
const objectPath = `${workDir}/dep${index}.o`;
|
|
2505
|
+
emception.writeFile(objectPath, objectBytes);
|
|
2506
|
+
objectPaths.push(objectPath);
|
|
2507
|
+
});
|
|
2508
|
+
|
|
2509
|
+
// -mbulk-memory: frame moves in the flow runtime (edge queues, arena
|
|
2510
|
+
// copies) lower to native memory.copy/memory.fill instead of byte
|
|
2511
|
+
// loops — decisive for interpreted hosts (loop C.5 wirespeed gate).
|
|
2512
|
+
// -DSDN_FLATSQL_LINKED compiles the direct engine-linkage block into
|
|
2513
|
+
// the runtime template (loop C.7): engine-facing calls become wasm
|
|
2514
|
+
// imports from modules "flatsql"/"flatsql_link" instead of hostcalls.
|
|
2515
|
+
const linkedDefine = engineLinked ? " -DSDN_FLATSQL_LINKED=1" : "";
|
|
2516
|
+
const compileCommands = [
|
|
2517
|
+
`em++ -c ${workDir}/flow_runtime.cpp -I${workDir} -std=c++17 -O3 -mbulk-memory -DNDEBUG${linkedDefine} -o ${workDir}/flow_runtime.o`,
|
|
2518
|
+
`em++ -c ${workDir}/flow-manifest-exports.cpp -std=c++17 -O3 -mbulk-memory -o ${workDir}/flow-manifest-exports.o`,
|
|
2519
|
+
`em++ -c ${workDir}/flow-program-exports.cpp -std=c++17 -O3 -mbulk-memory -o ${workDir}/flow-program-exports.o`,
|
|
2520
|
+
];
|
|
2521
|
+
const exportSymbols = engineLinked
|
|
2522
|
+
? [...FLOW_ARTIFACT_EXPORTS, ...FLOW_LINKED_EXPORTS]
|
|
2523
|
+
: FLOW_ARTIFACT_EXPORTS;
|
|
2524
|
+
const exportArgs = exportSymbols.map((symbol) => `-Wl,--export=${symbol}`).join(" ");
|
|
2525
|
+
const linkCommand =
|
|
2526
|
+
`em++ ${workDir}/flow_runtime.o ${workDir}/flow-manifest-exports.o ${workDir}/flow-program-exports.o ` +
|
|
2527
|
+
`${objectPaths.join(" ")} -O3 -s STANDALONE_WASM=1 -s ALLOW_MEMORY_GROWTH=1 ` +
|
|
2528
|
+
`-s ERROR_ON_UNDEFINED_SYMBOLS=0 -Wl,--allow-undefined --no-entry ${exportArgs} ` +
|
|
2529
|
+
`-o ${workDir}/flow.wasm`;
|
|
2530
|
+
|
|
2531
|
+
for (const command of [...compileCommands, linkCommand]) {
|
|
2532
|
+
const result = emception.run(command);
|
|
2533
|
+
if (result.returncode !== 0) {
|
|
2534
|
+
throw new Error(
|
|
2535
|
+
`Flow compilation failed (emception): ${command}\n${result.stderr || result.stdout}`,
|
|
2536
|
+
);
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
return new Uint8Array(emception.readFile(`${workDir}/flow.wasm`));
|
|
2540
|
+
} finally {
|
|
2541
|
+
try {
|
|
2542
|
+
const entries = emception.FS.readdir(workDir).filter((entry) => entry !== "." && entry !== "..");
|
|
2543
|
+
for (const entry of entries) {
|
|
2544
|
+
emception.FS.unlink(`${workDir}/${entry}`);
|
|
2545
|
+
}
|
|
2546
|
+
emception.FS.rmdir(workDir);
|
|
2547
|
+
} catch {
|
|
2548
|
+
// Best-effort cleanup only.
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
});
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
async function runWasiThreadsCompiler(command, args, cwd) {
|
|
2555
|
+
try {
|
|
2556
|
+
await execFileAsync(command, args, {
|
|
2557
|
+
cwd,
|
|
2558
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
2559
|
+
});
|
|
2560
|
+
} catch (error) {
|
|
2561
|
+
const stderr = typeof error?.stderr === "string" ? error.stderr.trim() : "";
|
|
2562
|
+
const stdout = typeof error?.stdout === "string" ? error.stdout.trim() : "";
|
|
2563
|
+
throw new Error(
|
|
2564
|
+
`Flow compilation failed (wasi-threads): ${command} ${args.join(" ")}` +
|
|
2565
|
+
`${stderr || stdout ? `\n${stderr || stdout}` : ""}`,
|
|
2566
|
+
{ cause: error },
|
|
2567
|
+
);
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
async function linkFlowArtifactWithWasiThreads({
|
|
2572
|
+
runtimeSource,
|
|
2573
|
+
invokeHeader,
|
|
2574
|
+
generatedInc,
|
|
2575
|
+
manifestSource,
|
|
2576
|
+
programSource,
|
|
2577
|
+
dependencyObjects,
|
|
2578
|
+
engineLinked = false,
|
|
2579
|
+
}) {
|
|
2580
|
+
const toolchain = resolveWasiThreadsToolchain();
|
|
2581
|
+
const workDir = await mkdtemp(
|
|
2582
|
+
path.join(os.tmpdir(), "space-data-module-flow-wasi-threads-"),
|
|
2583
|
+
);
|
|
2584
|
+
try {
|
|
2585
|
+
const runtimePath = path.join(workDir, "flow_runtime.cpp");
|
|
2586
|
+
const manifestPath = path.join(workDir, "flow-manifest-exports.cpp");
|
|
2587
|
+
const programPath = path.join(workDir, "flow-program-exports.cpp");
|
|
2588
|
+
const runtimeObjectPath = path.join(workDir, "flow_runtime.o");
|
|
2589
|
+
const manifestObjectPath = path.join(workDir, "flow-manifest-exports.o");
|
|
2590
|
+
const programObjectPath = path.join(workDir, "flow-program-exports.o");
|
|
2591
|
+
const outputPath = path.join(workDir, "flow.wasm");
|
|
2592
|
+
|
|
2593
|
+
await Promise.all([
|
|
2594
|
+
writeFile(path.join(workDir, "space_data_module_invoke.h"), invokeHeader),
|
|
2595
|
+
writeFile(path.join(workDir, "flow_generated.inc"), generatedInc),
|
|
2596
|
+
writeFile(runtimePath, runtimeSource),
|
|
2597
|
+
writeFile(manifestPath, manifestSource),
|
|
2598
|
+
writeFile(programPath, programSource),
|
|
2599
|
+
]);
|
|
2600
|
+
const dependencyObjectPaths = [];
|
|
2601
|
+
for (let index = 0; index < dependencyObjects.length; index += 1) {
|
|
2602
|
+
const objectPath = path.join(workDir, `dep${index}.o`);
|
|
2603
|
+
await writeFile(objectPath, dependencyObjects[index]);
|
|
2604
|
+
dependencyObjectPaths.push(objectPath);
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
const commonCompileArgs = [
|
|
2608
|
+
...toolchain.toolchainArgs,
|
|
2609
|
+
"-c",
|
|
2610
|
+
"-std=c++17",
|
|
2611
|
+
`-I${workDir}`,
|
|
2612
|
+
"-O3",
|
|
2613
|
+
"-matomics",
|
|
2614
|
+
"-mbulk-memory",
|
|
2615
|
+
"-pthread",
|
|
2616
|
+
"-fno-exceptions",
|
|
2617
|
+
"-DNDEBUG",
|
|
2618
|
+
];
|
|
2619
|
+
const compileUnits = [
|
|
2620
|
+
[runtimePath, runtimeObjectPath],
|
|
2621
|
+
[manifestPath, manifestObjectPath],
|
|
2622
|
+
[programPath, programObjectPath],
|
|
2623
|
+
];
|
|
2624
|
+
for (const [sourcePath, objectPath] of compileUnits) {
|
|
2625
|
+
const args = [
|
|
2626
|
+
...commonCompileArgs,
|
|
2627
|
+
...(sourcePath === runtimePath && engineLinked
|
|
2628
|
+
? ["-DSDN_FLATSQL_LINKED=1"]
|
|
2629
|
+
: []),
|
|
2630
|
+
sourcePath,
|
|
2631
|
+
"-o",
|
|
2632
|
+
objectPath,
|
|
2633
|
+
];
|
|
2634
|
+
await runWasiThreadsCompiler(toolchain.clangxx, args, workDir);
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
const exportSymbols = engineLinked
|
|
2638
|
+
? [...FLOW_ARTIFACT_EXPORTS, ...FLOW_LINKED_EXPORTS]
|
|
2639
|
+
: FLOW_ARTIFACT_EXPORTS;
|
|
2640
|
+
const linkArgs = [
|
|
2641
|
+
...toolchain.toolchainArgs,
|
|
2642
|
+
runtimeObjectPath,
|
|
2643
|
+
manifestObjectPath,
|
|
2644
|
+
programObjectPath,
|
|
2645
|
+
...dependencyObjectPaths,
|
|
2646
|
+
"-O3",
|
|
2647
|
+
"-mexec-model=reactor",
|
|
2648
|
+
...PTHREAD_FINAL_LINK_FLAGS,
|
|
2649
|
+
"-Wl,--export-memory",
|
|
2650
|
+
"-Wl,--allow-undefined",
|
|
2651
|
+
...exportSymbols.map((symbol) => `-Wl,--export=${symbol}`),
|
|
2652
|
+
"-o",
|
|
2653
|
+
outputPath,
|
|
2654
|
+
];
|
|
2655
|
+
assertPthreadFlagsPresent(linkArgs, {
|
|
2656
|
+
threadModel: HISTORICAL_PTHREAD_THREAD_MODEL,
|
|
2657
|
+
});
|
|
2658
|
+
await runWasiThreadsCompiler(toolchain.clangxx, linkArgs, workDir);
|
|
2659
|
+
return new Uint8Array(await readFile(outputPath));
|
|
2660
|
+
} finally {
|
|
2661
|
+
await rm(workDir, { recursive: true, force: true });
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
/**
|
|
2666
|
+
* Compile a validated flow document into the linked-direct flow bundle.
|
|
2667
|
+
* Returns { check, manifest, artifact, outputs, report }.
|
|
2668
|
+
*/
|
|
2669
|
+
export async function compileFlowProgram({
|
|
2670
|
+
flow,
|
|
2671
|
+
dependencies,
|
|
2672
|
+
outDir,
|
|
2673
|
+
flowSourcePath = null,
|
|
2674
|
+
catalog,
|
|
2675
|
+
standardsRoot,
|
|
2676
|
+
}) {
|
|
2677
|
+
const check = checkFlowProgram({ flow, dependencies });
|
|
2678
|
+
if (!check.ok) {
|
|
2679
|
+
const error = new Error(
|
|
2680
|
+
`Flow validation failed:\n${check.errors.map((issue) => ` ${issue.code}: ${issue.message}`).join("\n")}`,
|
|
2681
|
+
);
|
|
2682
|
+
error.check = check;
|
|
2683
|
+
throw error;
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
await verifyIsomorphicNodeArtifacts({
|
|
2687
|
+
flow,
|
|
2688
|
+
check,
|
|
2689
|
+
dependencies,
|
|
2690
|
+
flowSourcePath,
|
|
2691
|
+
});
|
|
2692
|
+
|
|
2693
|
+
const standardsOptions = { catalog, standardsRoot };
|
|
2694
|
+
const dependencyPluginIds = [
|
|
2695
|
+
...new Set(
|
|
2696
|
+
check.nodes
|
|
2697
|
+
.filter(
|
|
2698
|
+
(node) =>
|
|
2699
|
+
node.dispatchModel === "linked-direct" ||
|
|
2700
|
+
node.dispatchModel === "isomorphic",
|
|
2701
|
+
)
|
|
2702
|
+
.map((node) => node.pluginId),
|
|
2703
|
+
),
|
|
2704
|
+
];
|
|
2705
|
+
for (const pluginId of dependencyPluginIds) {
|
|
2706
|
+
const dependency = dependencies.get(pluginId);
|
|
2707
|
+
const report = await validateArtifactWithStandards({
|
|
2708
|
+
manifest: dependency.manifest,
|
|
2709
|
+
sourceName: `dependency[${JSON.stringify(pluginId)}]`,
|
|
2710
|
+
...standardsOptions,
|
|
2711
|
+
});
|
|
2712
|
+
if (!report.ok) {
|
|
2713
|
+
const error = new Error(
|
|
2714
|
+
`Dependency manifest standards validation failed for plugin "${pluginId}".`,
|
|
2715
|
+
);
|
|
2716
|
+
error.report = report;
|
|
2717
|
+
error.dependencyPluginId = pluginId;
|
|
2718
|
+
throw error;
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
const {
|
|
2723
|
+
source: generatedInc,
|
|
2724
|
+
linkedDependencies,
|
|
2725
|
+
runtimeDependencies,
|
|
2726
|
+
} = generateFlowTables({ flow, check, dependencies });
|
|
2727
|
+
const manifest = buildFlowModuleManifest({ flow, check, dependencies });
|
|
2728
|
+
const sourceName = outDir
|
|
2729
|
+
? path.join(outDir, "isomorphic", "module.wasm")
|
|
2730
|
+
: `${flow.programId} (flow bundle)`;
|
|
2731
|
+
const manifestReport = await validateArtifactWithStandards({
|
|
2732
|
+
manifest,
|
|
2733
|
+
sourceName,
|
|
2734
|
+
...standardsOptions,
|
|
2735
|
+
});
|
|
2736
|
+
if (!manifestReport.ok) {
|
|
2737
|
+
const error = new Error("Flow manifest compliance validation failed.");
|
|
2738
|
+
error.report = manifestReport;
|
|
2739
|
+
throw error;
|
|
2740
|
+
}
|
|
2741
|
+
const flowPlgBytes = encodePluginManifest(manifest);
|
|
2742
|
+
const programBytes = encodeFlowDocumentProgram(flow);
|
|
2743
|
+
|
|
2744
|
+
const runtimeSource = await readFile(RUNTIME_TEMPLATE_PATH, "utf8");
|
|
2745
|
+
const manifestSource = generateEmbeddedManifestSource({
|
|
2746
|
+
manifest: flowPlgBytes,
|
|
2747
|
+
});
|
|
2748
|
+
const programSource = generateEmbeddedManifestSource({
|
|
2749
|
+
manifest: programBytes,
|
|
2750
|
+
bytesSymbol: "flow_get_manifest_flatbuffer",
|
|
2751
|
+
sizeSymbol: "flow_get_manifest_flatbuffer_size",
|
|
2752
|
+
bufferSymbol: "g_flow_program",
|
|
2753
|
+
});
|
|
2754
|
+
|
|
2755
|
+
const linkFlowArtifact =
|
|
2756
|
+
check.threadModel === FLOW_THREAD_MODEL.WASI_THREADS
|
|
2757
|
+
? linkFlowArtifactWithWasiThreads
|
|
2758
|
+
: linkFlowArtifactWithEmception;
|
|
2759
|
+
let wasmBytes = await linkFlowArtifact({
|
|
2760
|
+
runtimeSource,
|
|
2761
|
+
invokeHeader: generateInvokeSupportHeader(),
|
|
2762
|
+
generatedInc,
|
|
2763
|
+
manifestSource,
|
|
2764
|
+
programSource,
|
|
2765
|
+
dependencyObjects: linkedDependencies.map((dependency) => dependency.guestLink.objectBytes),
|
|
2766
|
+
engineLinked: Boolean(check.engineLinkage),
|
|
2767
|
+
});
|
|
2768
|
+
wasmBytes = appendWasmCustomSection(
|
|
2769
|
+
wasmBytes,
|
|
2770
|
+
SDS_MANIFEST_SECTION_NAME,
|
|
2771
|
+
flowPlgBytes,
|
|
2772
|
+
);
|
|
2773
|
+
if (check.threadModel === FLOW_THREAD_MODEL.WASI_THREADS) {
|
|
2774
|
+
assertPthreadArtifact(wasmBytes, { source: sourceName });
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
const report = await validateArtifactWithStandards({
|
|
2778
|
+
manifest,
|
|
2779
|
+
wasmBytes,
|
|
2780
|
+
sourceName,
|
|
2781
|
+
...standardsOptions,
|
|
2782
|
+
});
|
|
2783
|
+
if (!report.ok) {
|
|
2784
|
+
const error = new Error("Flow artifact compliance validation failed.");
|
|
2785
|
+
error.report = report;
|
|
2786
|
+
throw error;
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2789
|
+
const dependencyRecords = [];
|
|
2790
|
+
for (const dependency of runtimeDependencies) {
|
|
2791
|
+
let sha256 = dependency.artifactLock?.sha256 ?? null;
|
|
2792
|
+
if (!sha256) {
|
|
2793
|
+
try {
|
|
2794
|
+
sha256 = bytesToHex(
|
|
2795
|
+
await sha256Bytes(new Uint8Array(await readFile(dependency.wasmPath))),
|
|
2796
|
+
);
|
|
2797
|
+
} catch {
|
|
2798
|
+
sha256 = null;
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
dependencyRecords.push({
|
|
2802
|
+
kind: "node",
|
|
2803
|
+
dependencyId: dependency.pluginId,
|
|
2804
|
+
pluginId: dependency.pluginId,
|
|
2805
|
+
version: dependency.manifest?.version ?? null,
|
|
2806
|
+
sha256,
|
|
2807
|
+
dispatchModel: dependency.artifactLock ? "isomorphic" : "linked-direct",
|
|
2808
|
+
artifactPath: dependency.artifactLock?.path ?? null,
|
|
2809
|
+
publisher: dependency.artifactLock?.publisher ?? null,
|
|
2810
|
+
symbolPrefix: dependency.guestLink?.metadata?.symbolPrefix ?? null,
|
|
2811
|
+
methodSymbols: dependency.guestLink?.metadata?.methodSymbols ?? {},
|
|
2812
|
+
});
|
|
2813
|
+
}
|
|
2814
|
+
for (const component of check.componentDependencies ?? []) {
|
|
2815
|
+
dependencyRecords.push({
|
|
2816
|
+
kind: "component",
|
|
2817
|
+
dependencyId: component.pluginId,
|
|
2818
|
+
pluginId: component.pluginId,
|
|
2819
|
+
minVersion: component.minVersion,
|
|
2820
|
+
maxVersion: component.maxVersion,
|
|
2821
|
+
resolved: component.resolved,
|
|
2822
|
+
declaredBy: component.declaredBy,
|
|
2823
|
+
});
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2826
|
+
const artifact = {
|
|
2827
|
+
programId: flow.programId,
|
|
2828
|
+
name: flow.name ?? null,
|
|
2829
|
+
version: flow.version ?? null,
|
|
2830
|
+
compiler:
|
|
2831
|
+
check.threadModel === FLOW_THREAD_MODEL.WASI_THREADS
|
|
2832
|
+
? "space-data-module flow compile (isomorphic, wasi-threads)"
|
|
2833
|
+
: "space-data-module flow compile (isomorphic, emception)",
|
|
2834
|
+
threadModel: check.threadModel,
|
|
2835
|
+
nodes: (flow.nodes ?? []).length,
|
|
2836
|
+
edges: (flow.edges ?? []).length,
|
|
2837
|
+
triggers: (flow.triggers ?? []).length,
|
|
2838
|
+
capabilities: check.capabilities,
|
|
2839
|
+
// "flatsql-direct": engine-facing query calls are direct wasm imports
|
|
2840
|
+
// from the live store engine instance (modules "flatsql"/"flatsql_link");
|
|
2841
|
+
// "bridge": queries travel the storage.flatsql_* capability hostcalls.
|
|
2842
|
+
engineLinkage: check.engineLinkage ? "flatsql-direct" : "bridge",
|
|
2843
|
+
dispatch: check.nodes,
|
|
2844
|
+
dependencies: dependencyRecords,
|
|
2845
|
+
};
|
|
2846
|
+
|
|
2847
|
+
const outputs = {};
|
|
2848
|
+
if (outDir) {
|
|
2849
|
+
const moduleWasmPath = path.join(outDir, "isomorphic", "module.wasm");
|
|
2850
|
+
await mkdir(path.dirname(moduleWasmPath), { recursive: true });
|
|
2851
|
+
await writeFile(moduleWasmPath, wasmBytes);
|
|
2852
|
+
const runtimeWasmPath = path.join(outDir, "runtime.wasm");
|
|
2853
|
+
await writeFile(runtimeWasmPath, wasmBytes);
|
|
2854
|
+
const manifestPath = path.join(outDir, "plugin-manifest.json");
|
|
2855
|
+
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
2856
|
+
const flowPlgPath = path.join(outDir, "flow.plg");
|
|
2857
|
+
await writeFile(flowPlgPath, flowPlgBytes);
|
|
2858
|
+
const artifactPath = path.join(outDir, "artifact.json");
|
|
2859
|
+
await writeFile(artifactPath, `${JSON.stringify(artifact, null, 2)}\n`);
|
|
2860
|
+
const flowJsonPath = path.join(outDir, "flow.json");
|
|
2861
|
+
const flowJson = flowSourcePath
|
|
2862
|
+
? await readFile(flowSourcePath, "utf8")
|
|
2863
|
+
: `${JSON.stringify(flow, null, 2)}\n`;
|
|
2864
|
+
await writeFile(flowJsonPath, flowJson);
|
|
2865
|
+
outputs.moduleWasmPath = moduleWasmPath;
|
|
2866
|
+
outputs.runtimeWasmPath = runtimeWasmPath;
|
|
2867
|
+
outputs.manifestPath = manifestPath;
|
|
2868
|
+
outputs.flowPlgPath = flowPlgPath;
|
|
2869
|
+
outputs.artifactPath = artifactPath;
|
|
2870
|
+
outputs.flowJsonPath = flowJsonPath;
|
|
2871
|
+
if (check.engineLinkage) {
|
|
2872
|
+
// Ship the deterministic flatsql_link shim next to the artifact so any
|
|
2873
|
+
// host can instantiate the memory-crossing component without depending
|
|
2874
|
+
// on the SDK at runtime (sdn-server embeds the identical bytes).
|
|
2875
|
+
const shimPath = path.join(outDir, "flatsql-link-shim.wasm");
|
|
2876
|
+
await writeFile(shimPath, FLATSQL_LINK_SHIM_WASM);
|
|
2877
|
+
outputs.linkShimPath = shimPath;
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
return {
|
|
2882
|
+
check,
|
|
2883
|
+
manifest,
|
|
2884
|
+
flowPlgBytes,
|
|
2885
|
+
artifact,
|
|
2886
|
+
wasmBytes,
|
|
2887
|
+
outputs,
|
|
2888
|
+
report,
|
|
2889
|
+
};
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
// Internals exposed for regression tests only (see test/flow-wildcard-edge-contract.test.js).
|
|
2893
|
+
export const __testables = {
|
|
2894
|
+
resolveEdgeTypeContract,
|
|
2895
|
+
signedFlowEdgeContract,
|
|
2896
|
+
// Exposed so the admissible-signature contract can be tested against the
|
|
2897
|
+
// SHARED statement-domain vectors directly, instead of inferring it from a
|
|
2898
|
+
// full compile that fails for a dozen later reasons.
|
|
2899
|
+
verifyIsomorphicNodeArtifacts,
|
|
2900
|
+
};
|