space-data-module-sdk 0.8.3 → 0.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (354) hide show
  1. package/README.md +117 -20
  2. package/bin/space-data-module.js +263 -0
  3. package/docs/.nojekyll +1 -0
  4. package/docs/AGENTS.md +34 -0
  5. package/docs/architecture.svg +141 -0
  6. package/docs/browser-wasmedge-isomorphic.md +179 -0
  7. package/docs/flatsql-streaming-standard.md +341 -0
  8. package/docs/gpu-module-abi.md +193 -0
  9. package/docs/index.html +125 -0
  10. package/docs/isomorphic-pthreads.md +188 -0
  11. package/docs/isomorphic-sdn-runtime-plan.md +167 -0
  12. package/docs/language-runtime-matrix.md +82 -0
  13. package/docs/manifest-structure.svg +114 -0
  14. package/docs/module-bundle-runtime-plan.md +19 -0
  15. package/docs/module-lifecycle.svg +70 -0
  16. package/docs/module-publication-standard.md +540 -0
  17. package/docs/node-red-default-node-parity.md +88 -0
  18. package/docs/protocol-installation.md +196 -0
  19. package/docs/secrets-capability.md +152 -0
  20. package/docs/styles.css +288 -0
  21. package/docs/testing-harness.md +408 -0
  22. package/docs/tri-runtime-parity.md +132 -0
  23. package/docs/wallet-callback.html +2 -0
  24. package/package.json +26 -6
  25. package/schemas/HttpRequestAbi.fbs +41 -0
  26. package/schemas/HttpResponseAbi.fbs +36 -0
  27. package/schemas/PluginManifest.fbs +2 -1
  28. package/src/AGENTS.md +1 -1
  29. package/src/app/codec.js +548 -0
  30. package/src/app/index.js +6 -0
  31. package/src/browser.js +5 -0
  32. package/src/bundle/constants.js +1 -0
  33. package/src/bundle/index.js +15 -0
  34. package/src/bundle/sigdomain.js +179 -0
  35. package/src/bundle/signing.js +767 -0
  36. package/src/bundle/wasm.js +3 -2
  37. package/src/capabilities.js +3 -0
  38. package/src/compat/index.js +7 -0
  39. package/src/compat/sdnLegacy.js +449 -0
  40. package/src/compiler/compileModule.js +556 -55
  41. package/src/compiler/flatcSupport.js +41 -27
  42. package/src/compiler/index.d.ts +2 -0
  43. package/src/compiler/index.js +18 -0
  44. package/src/compiler/invokeGlue.js +1212 -183
  45. package/src/compiler/pthreadArtifactGuard.js +910 -0
  46. package/src/compiler/sdnShimGenerator.js +323 -0
  47. package/src/compiler/wasiThreadsToolchain.js +150 -0
  48. package/src/compliance/index.js +11 -1
  49. package/src/compliance/legacyWildcardPorts.js +228 -0
  50. package/src/compliance/pluginCompliance.js +1000 -96
  51. package/src/field-stream/index.d.ts +31 -0
  52. package/src/field-stream/index.js +119 -0
  53. package/src/flow/dependencyStreamBridge.js +234 -0
  54. package/src/flow/flatsqlLinkShim.js +304 -0
  55. package/src/flow/flowCodec.js +107 -0
  56. package/src/flow/flowCompiler.js +2900 -0
  57. package/src/flow/flowRuntimeHost.js +877 -0
  58. package/src/flow/index.d.ts +117 -0
  59. package/src/flow/index.js +31 -0
  60. package/src/flow/isomorphicFlowHost.js +384 -0
  61. package/src/flow/jsFlowRuntime.js +275 -0
  62. package/src/flow/normalize.js +294 -0
  63. package/src/flow/runtime-src/flow_runtime.cpp +1699 -0
  64. package/src/flow/vendor/sdn-flow/MethodRegistry.js +400 -0
  65. package/src/flow/vendor/sdn-flow/constants.js +39 -0
  66. package/src/flow/vendor/sdn-flow/index.js +29 -0
  67. package/src/flow/vendor/sdn-flow/normalize.js +506 -0
  68. package/src/flow/vendor/sdn-flow/wasmCompatibility.js +189 -0
  69. package/src/generated/http/cpp/HttpRequestAbi_generated.h +285 -0
  70. package/src/generated/http/cpp/HttpResponseAbi_generated.h +192 -0
  71. package/src/generated/http/sdn/http/http-header.js +53 -0
  72. package/src/generated/http/sdn/http/http-header.ts +67 -0
  73. package/src/generated/http/sdn/http/http-request.js +132 -0
  74. package/src/generated/http/sdn/http/http-request.ts +179 -0
  75. package/src/generated/http/sdn/http/http-response.js +144 -0
  76. package/src/generated/http/sdn/http/http-response.ts +168 -0
  77. package/src/generated/http/sdn/http.js +11 -0
  78. package/src/generated/http/sdn/http.ts +4 -0
  79. package/src/generated/orbpro/flow/backpressure-policy.d.ts +12 -0
  80. package/src/generated/orbpro/flow/backpressure-policy.d.ts.map +1 -0
  81. package/src/generated/orbpro/flow/backpressure-policy.js +17 -0
  82. package/src/generated/orbpro/flow/backpressure-policy.js.map +1 -0
  83. package/src/generated/orbpro/flow/backpressure-policy.ts +15 -0
  84. package/src/generated/orbpro/flow/flow-bundle-manifest.d.ts +142 -0
  85. package/src/generated/orbpro/flow/flow-bundle-manifest.d.ts.map +1 -0
  86. package/src/generated/orbpro/flow/flow-bundle-manifest.js +304 -0
  87. package/src/generated/orbpro/flow/flow-bundle-manifest.js.map +1 -0
  88. package/src/generated/orbpro/flow/flow-bundle-manifest.ts +399 -0
  89. package/src/generated/orbpro/flow/flow-edge.d.ts +113 -0
  90. package/src/generated/orbpro/flow/flow-edge.d.ts.map +1 -0
  91. package/src/generated/orbpro/flow/flow-edge.js +221 -0
  92. package/src/generated/orbpro/flow/flow-edge.js.map +1 -0
  93. package/src/generated/orbpro/flow/flow-edge.ts +292 -0
  94. package/src/generated/orbpro/flow/flow-node.d.ts +80 -0
  95. package/src/generated/orbpro/flow/flow-node.d.ts.map +1 -0
  96. package/src/generated/orbpro/flow/flow-node.js +158 -0
  97. package/src/generated/orbpro/flow/flow-node.js.map +1 -0
  98. package/src/generated/orbpro/flow/flow-node.ts +202 -0
  99. package/src/generated/orbpro/flow/flow-program.d.ts +164 -0
  100. package/src/generated/orbpro/flow/flow-program.d.ts.map +1 -0
  101. package/src/generated/orbpro/flow/flow-program.js +350 -0
  102. package/src/generated/orbpro/flow/flow-program.js.map +1 -0
  103. package/src/generated/orbpro/flow/flow-program.ts +477 -0
  104. package/src/generated/orbpro/flow/flow-trigger.d.ts +108 -0
  105. package/src/generated/orbpro/flow/flow-trigger.d.ts.map +1 -0
  106. package/src/generated/orbpro/flow/flow-trigger.js +207 -0
  107. package/src/generated/orbpro/flow/flow-trigger.js.map +1 -0
  108. package/src/generated/orbpro/flow/flow-trigger.ts +276 -0
  109. package/src/generated/orbpro/flow/node-kind.d.ts +13 -0
  110. package/src/generated/orbpro/flow/node-kind.d.ts.map +1 -0
  111. package/src/generated/orbpro/flow/node-kind.js +16 -0
  112. package/src/generated/orbpro/flow/node-kind.js.map +1 -0
  113. package/src/generated/orbpro/{invoke.ts → flow/node-kind.ts} +12 -2
  114. package/src/generated/orbpro/flow/plugin-version-binding.d.ts +52 -0
  115. package/src/generated/orbpro/flow/plugin-version-binding.d.ts.map +1 -0
  116. package/src/generated/orbpro/flow/plugin-version-binding.js +85 -0
  117. package/src/generated/orbpro/flow/plugin-version-binding.js.map +1 -0
  118. package/src/generated/orbpro/flow/plugin-version-binding.ts +121 -0
  119. package/src/generated/orbpro/flow/schema-hash-binding.d.ts +70 -0
  120. package/src/generated/orbpro/flow/schema-hash-binding.d.ts.map +1 -0
  121. package/src/generated/orbpro/flow/schema-hash-binding.js +143 -0
  122. package/src/generated/orbpro/flow/schema-hash-binding.js.map +1 -0
  123. package/src/generated/orbpro/flow/schema-hash-binding.ts +189 -0
  124. package/src/generated/orbpro/flow/trigger-binding.d.ts +71 -0
  125. package/src/generated/orbpro/flow/trigger-binding.d.ts.map +1 -0
  126. package/src/generated/orbpro/flow/trigger-binding.js +142 -0
  127. package/src/generated/orbpro/flow/trigger-binding.js.map +1 -0
  128. package/src/generated/orbpro/flow/trigger-binding.ts +181 -0
  129. package/src/generated/orbpro/flow/trigger-kind.d.ts +14 -0
  130. package/src/generated/orbpro/flow/trigger-kind.d.ts.map +1 -0
  131. package/src/generated/orbpro/flow/trigger-kind.js +17 -0
  132. package/src/generated/orbpro/flow/trigger-kind.js.map +1 -0
  133. package/src/generated/orbpro/flow/trigger-kind.ts +17 -0
  134. package/src/generated/orbpro/flow.d.ts +21 -0
  135. package/src/generated/orbpro/flow.d.ts.map +1 -0
  136. package/src/generated/orbpro/flow.js +23 -0
  137. package/src/generated/orbpro/flow.js.map +1 -0
  138. package/src/generated/orbpro/flow.ts +24 -0
  139. package/src/generated/orbpro/manifest/capability-kind.d.ts +2 -1
  140. package/src/generated/orbpro/manifest/capability-kind.js +1 -0
  141. package/src/generated/orbpro/manifest/capability-kind.ts +1 -0
  142. package/src/generated/orbpro/plugin/stream-invoke-request.d.ts +93 -0
  143. package/src/generated/orbpro/plugin/stream-invoke-request.js +160 -0
  144. package/src/generated/orbpro/plugin/stream-invoke-request.ts +211 -0
  145. package/src/generated/orbpro/plugin/stream-invoke-response.d.ts +99 -0
  146. package/src/generated/orbpro/plugin/stream-invoke-response.js +175 -0
  147. package/src/generated/orbpro/plugin/stream-invoke-response.ts +226 -0
  148. package/src/generated/orbpro/plugin.js +8 -0
  149. package/src/generated/orbpro/stream.d.ts +12 -0
  150. package/src/generated/orbpro/stream.js +14 -0
  151. package/src/generated/orbpro/stream.ts +15 -0
  152. package/src/generated/spacedatastandards/plg/EntryFunction.d.ts +52 -0
  153. package/src/generated/spacedatastandards/plg/EntryFunction.d.ts.map +1 -0
  154. package/src/generated/spacedatastandards/plg/EntryFunction.js +106 -0
  155. package/src/generated/spacedatastandards/plg/EntryFunction.ts +162 -0
  156. package/src/generated/spacedatastandards/plg/FlatBufferTypeRef.d.ts +90 -0
  157. package/src/generated/spacedatastandards/plg/FlatBufferTypeRef.d.ts.map +1 -0
  158. package/src/generated/spacedatastandards/plg/FlatBufferTypeRef.js +189 -0
  159. package/src/generated/spacedatastandards/plg/FlatBufferTypeRef.ts +270 -0
  160. package/src/generated/spacedatastandards/plg/PLG.d.ts +492 -0
  161. package/src/generated/spacedatastandards/plg/PLG.d.ts.map +1 -0
  162. package/src/generated/spacedatastandards/plg/PLG.js +1139 -0
  163. package/src/generated/spacedatastandards/plg/PLG.ts +1600 -0
  164. package/src/generated/spacedatastandards/plg/PLGAcceptedTypeSet.d.ts +63 -0
  165. package/src/generated/spacedatastandards/plg/PLGAcceptedTypeSet.d.ts.map +1 -0
  166. package/src/generated/spacedatastandards/plg/PLGAcceptedTypeSet.js +138 -0
  167. package/src/generated/spacedatastandards/plg/PLGAcceptedTypeSet.ts +189 -0
  168. package/src/generated/spacedatastandards/plg/PLGBuildArtifact.d.ts +41 -0
  169. package/src/generated/spacedatastandards/plg/PLGBuildArtifact.d.ts.map +1 -0
  170. package/src/generated/spacedatastandards/plg/PLGBuildArtifact.js +104 -0
  171. package/src/generated/spacedatastandards/plg/PLGBuildArtifact.ts +151 -0
  172. package/src/generated/spacedatastandards/plg/PLGFlowEdge.d.ts +68 -0
  173. package/src/generated/spacedatastandards/plg/PLGFlowEdge.d.ts.map +1 -0
  174. package/src/generated/spacedatastandards/plg/PLGFlowEdge.js +124 -0
  175. package/src/generated/spacedatastandards/plg/PLGFlowEdge.ts +184 -0
  176. package/src/generated/spacedatastandards/plg/PLGFlowEdgeContract.d.ts +58 -0
  177. package/src/generated/spacedatastandards/plg/PLGFlowEdgeContract.d.ts.map +1 -0
  178. package/src/generated/spacedatastandards/plg/PLGFlowEdgeContract.js +126 -0
  179. package/src/generated/spacedatastandards/plg/PLGFlowEdgeContract.ts +161 -0
  180. package/src/generated/spacedatastandards/plg/PLGFlowNode.d.ts +82 -0
  181. package/src/generated/spacedatastandards/plg/PLGFlowNode.d.ts.map +1 -0
  182. package/src/generated/spacedatastandards/plg/PLGFlowNode.js +164 -0
  183. package/src/generated/spacedatastandards/plg/PLGFlowNode.ts +244 -0
  184. package/src/generated/spacedatastandards/plg/PLGFlowTrigger.d.ts +55 -0
  185. package/src/generated/spacedatastandards/plg/PLGFlowTrigger.d.ts.map +1 -0
  186. package/src/generated/spacedatastandards/plg/PLGFlowTrigger.js +105 -0
  187. package/src/generated/spacedatastandards/plg/PLGFlowTrigger.ts +162 -0
  188. package/src/generated/spacedatastandards/plg/PLGFlowTriggerBinding.d.ts +42 -0
  189. package/src/generated/spacedatastandards/plg/PLGFlowTriggerBinding.d.ts.map +1 -0
  190. package/src/generated/spacedatastandards/plg/PLGFlowTriggerBinding.js +83 -0
  191. package/src/generated/spacedatastandards/plg/PLGFlowTriggerBinding.ts +127 -0
  192. package/src/generated/spacedatastandards/plg/PLGHostCapability.d.ts +36 -0
  193. package/src/generated/spacedatastandards/plg/PLGHostCapability.d.ts.map +1 -0
  194. package/src/generated/spacedatastandards/plg/PLGHostCapability.js +90 -0
  195. package/src/generated/spacedatastandards/plg/PLGHostCapability.ts +127 -0
  196. package/src/generated/spacedatastandards/plg/PLGMethodManifest.d.ts +53 -0
  197. package/src/generated/spacedatastandards/plg/PLGMethodManifest.d.ts.map +1 -0
  198. package/src/generated/spacedatastandards/plg/PLGMethodManifest.js +153 -0
  199. package/src/generated/spacedatastandards/plg/PLGMethodManifest.ts +210 -0
  200. package/src/generated/spacedatastandards/plg/PLGPortManifest.d.ts +70 -0
  201. package/src/generated/spacedatastandards/plg/PLGPortManifest.d.ts.map +1 -0
  202. package/src/generated/spacedatastandards/plg/PLGPortManifest.js +149 -0
  203. package/src/generated/spacedatastandards/plg/PLGPortManifest.ts +212 -0
  204. package/src/generated/spacedatastandards/plg/PLGProtocolSpec.d.ts +73 -0
  205. package/src/generated/spacedatastandards/plg/PLGProtocolSpec.d.ts.map +1 -0
  206. package/src/generated/spacedatastandards/plg/PLGProtocolSpec.js +199 -0
  207. package/src/generated/spacedatastandards/plg/PLGProtocolSpec.ts +292 -0
  208. package/src/generated/spacedatastandards/plg/PLGTimerSpec.d.ts +40 -0
  209. package/src/generated/spacedatastandards/plg/PLGTimerSpec.d.ts.map +1 -0
  210. package/src/generated/spacedatastandards/plg/PLGTimerSpec.js +103 -0
  211. package/src/generated/spacedatastandards/plg/PLGTimerSpec.ts +148 -0
  212. package/src/generated/spacedatastandards/plg/PluginCapability.d.ts +41 -0
  213. package/src/generated/spacedatastandards/plg/PluginCapability.d.ts.map +1 -0
  214. package/src/generated/spacedatastandards/plg/PluginCapability.js +82 -0
  215. package/src/generated/spacedatastandards/plg/{plugin-capability.ts → PluginCapability.ts} +55 -18
  216. package/src/generated/spacedatastandards/plg/PluginDependency.d.ts +42 -0
  217. package/src/generated/spacedatastandards/plg/PluginDependency.d.ts.map +1 -0
  218. package/src/generated/spacedatastandards/plg/PluginDependency.js +80 -0
  219. package/src/generated/spacedatastandards/plg/PluginDependency.ts +124 -0
  220. package/src/generated/spacedatastandards/plg/TAB.d.ts +85 -0
  221. package/src/generated/spacedatastandards/plg/TAB.d.ts.map +1 -0
  222. package/src/generated/spacedatastandards/plg/TAB.js +173 -0
  223. package/src/generated/spacedatastandards/plg/TAB.ts +228 -0
  224. package/src/generated/spacedatastandards/plg/bufferMutability.d.ts +18 -0
  225. package/src/generated/spacedatastandards/plg/bufferMutability.d.ts.map +1 -0
  226. package/src/generated/spacedatastandards/plg/bufferMutability.js +20 -0
  227. package/src/generated/spacedatastandards/plg/bufferMutability.ts +23 -0
  228. package/src/generated/spacedatastandards/plg/bufferOwnership.d.ts +18 -0
  229. package/src/generated/spacedatastandards/plg/bufferOwnership.d.ts.map +1 -0
  230. package/src/generated/spacedatastandards/plg/bufferOwnership.js +20 -0
  231. package/src/generated/spacedatastandards/plg/bufferOwnership.ts +23 -0
  232. package/src/generated/spacedatastandards/plg/drainBehavior.d.ts +20 -0
  233. package/src/generated/spacedatastandards/plg/drainBehavior.d.ts.map +1 -0
  234. package/src/generated/spacedatastandards/plg/drainBehavior.js +22 -0
  235. package/src/generated/spacedatastandards/plg/drainBehavior.ts +25 -0
  236. package/src/generated/spacedatastandards/plg/flowEdgeRoutePolicy.d.ts +10 -0
  237. package/src/generated/spacedatastandards/plg/flowEdgeRoutePolicy.d.ts.map +1 -0
  238. package/src/generated/spacedatastandards/plg/flowEdgeRoutePolicy.js +12 -0
  239. package/src/generated/spacedatastandards/plg/flowEdgeRoutePolicy.ts +13 -0
  240. package/src/generated/spacedatastandards/plg/hostCapabilityKind.d.ts +51 -0
  241. package/src/generated/spacedatastandards/plg/hostCapabilityKind.d.ts.map +1 -0
  242. package/src/generated/spacedatastandards/plg/hostCapabilityKind.js +53 -0
  243. package/src/generated/spacedatastandards/plg/hostCapabilityKind.ts +55 -0
  244. package/src/generated/spacedatastandards/plg/invokeSurfaceKind.d.ts +15 -0
  245. package/src/generated/spacedatastandards/plg/invokeSurfaceKind.d.ts.map +1 -0
  246. package/src/generated/spacedatastandards/plg/invokeSurfaceKind.js +17 -0
  247. package/src/generated/spacedatastandards/plg/invokeSurfaceKind.ts +19 -0
  248. package/src/generated/spacedatastandards/plg/main.d.ts +29 -0
  249. package/src/generated/spacedatastandards/plg/main.d.ts.map +1 -0
  250. package/src/generated/spacedatastandards/plg/main.js +30 -0
  251. package/src/generated/spacedatastandards/plg/main.ts +32 -0
  252. package/src/generated/spacedatastandards/plg/payloadWireFormat.d.ts +17 -0
  253. package/src/generated/spacedatastandards/plg/payloadWireFormat.d.ts.map +1 -0
  254. package/src/generated/spacedatastandards/plg/payloadWireFormat.js +19 -0
  255. package/src/generated/spacedatastandards/plg/payloadWireFormat.ts +21 -0
  256. package/src/generated/spacedatastandards/plg/pluginCategory.d.ts +82 -0
  257. package/src/generated/spacedatastandards/plg/pluginCategory.d.ts.map +1 -0
  258. package/src/generated/spacedatastandards/plg/pluginCategory.js +84 -0
  259. package/src/generated/spacedatastandards/plg/pluginCategory.ts +103 -0
  260. package/src/generated/spacedatastandards/plg/publicationState.d.ts +18 -0
  261. package/src/generated/spacedatastandards/plg/publicationState.d.ts.map +1 -0
  262. package/src/generated/spacedatastandards/plg/publicationState.js +20 -0
  263. package/src/generated/spacedatastandards/plg/purchaseTier.d.ts +18 -0
  264. package/src/generated/spacedatastandards/plg/purchaseTier.d.ts.map +1 -0
  265. package/src/generated/spacedatastandards/plg/purchaseTier.js +20 -0
  266. package/src/host/AGENTS.md +2 -2
  267. package/src/host/abi.js +53 -66
  268. package/src/host/browserHost.js +355 -8
  269. package/src/host/cpp/keyslotClient.hpp +231 -0
  270. package/src/host/cpp/secretsClient.hpp +583 -0
  271. package/src/host/hostcallWire.js +183 -0
  272. package/src/host/index.js +2 -0
  273. package/src/host/isomorphicLoader.js +44 -7
  274. package/src/host/nodeHost.js +227 -8
  275. package/src/host/sabHostcallChannel.js +206 -0
  276. package/src/host/timerDriver.js +203 -0
  277. package/src/host/wasiShim.js +41 -1
  278. package/src/host/wasiThreadBrowserWorker.mjs +48 -0
  279. package/src/host/wasiThreadHost.js +393 -0
  280. package/src/host/wasiThreadWorker.mjs +32 -0
  281. package/src/host/wasiThreadWorkerRuntime.js +93 -0
  282. package/src/http/index.js +339 -0
  283. package/src/index.d.ts +716 -10
  284. package/src/index.js +4 -0
  285. package/src/invoke/codec.js +1202 -128
  286. package/src/invoke/index.js +5 -1
  287. package/src/licensing/index.d.ts +2 -0
  288. package/src/licensing/index.js +2 -0
  289. package/src/licensing/records.js +287 -8
  290. package/src/manifest/codec.js +62 -14
  291. package/src/manifest/index.js +2 -0
  292. package/src/manifest/legacyToPlg.js +164 -25
  293. package/src/manifest/normalize.js +1 -0
  294. package/src/manifest/plgCodec.js +1303 -62
  295. package/src/manifest/typeRefs.js +162 -24
  296. package/src/runtime/compiledRuntimeAbi.js +47 -0
  297. package/src/runtime/compiledRuntimeAbi.json +33 -0
  298. package/src/runtime/index.d.ts +32 -0
  299. package/src/runtime/index.js +1 -0
  300. package/src/runtime-host/index.js +3 -0
  301. package/src/runtime-host/methodRegistry.js +55 -0
  302. package/src/standards/index.js +200 -56
  303. package/src/standards/sharedCatalog.js +16 -0
  304. package/src/testing/browserModuleHarness.js +604 -31
  305. package/src/testing/docker/wasmedge-parity.Dockerfile +39 -0
  306. package/src/testing/index.d.ts +151 -0
  307. package/src/testing/index.js +21 -0
  308. package/src/testing/native/wasmedge_emscripten_pthread_runner.c +25 -1
  309. package/src/testing/parityBrowserRunner.js +138 -0
  310. package/src/testing/parityHarness.js +616 -0
  311. package/src/testing/parityLanes.js +537 -0
  312. package/src/testing/processInvoke.js +29 -21
  313. package/src/testing/streamInvokeCodec.js +10 -175
  314. package/src/testing/wasmedgePin.json +6 -0
  315. package/src/testing/workerModuleHarness.js +242 -0
  316. package/src/testing/workerModuleHarnessWorker.js +176 -0
  317. package/src/transport/index.js +3 -0
  318. package/src/transport/pki.js +299 -11
  319. package/src/transport/records.js +16 -7
  320. package/src/utils/ecdsaDer.js +106 -0
  321. package/src/utils/wasmCrypto.js +0 -43
  322. package/templates/gpu-module/README.md +48 -0
  323. package/templates/gpu-module/include/space_data_gpu_abi.h +122 -0
  324. package/templates/gpu-module/manifest.json +84 -0
  325. package/schemas/PluginInvokeRequest.fbs +0 -18
  326. package/schemas/PluginInvokeResponse.fbs +0 -30
  327. package/schemas/spacedatastandards/PLG.fbs +0 -191
  328. package/src/generated/orbpro/invoke/plugin-invoke-request.d.ts +0 -51
  329. package/src/generated/orbpro/invoke/plugin-invoke-request.d.ts.map +0 -1
  330. package/src/generated/orbpro/invoke/plugin-invoke-request.js +0 -131
  331. package/src/generated/orbpro/invoke/plugin-invoke-request.js.map +0 -1
  332. package/src/generated/orbpro/invoke/plugin-invoke-request.ts +0 -173
  333. package/src/generated/orbpro/invoke/plugin-invoke-response.d.ts +0 -76
  334. package/src/generated/orbpro/invoke/plugin-invoke-response.d.ts.map +0 -1
  335. package/src/generated/orbpro/invoke/plugin-invoke-response.js +0 -184
  336. package/src/generated/orbpro/invoke/plugin-invoke-response.js.map +0 -1
  337. package/src/generated/orbpro/invoke/plugin-invoke-response.ts +0 -243
  338. package/src/generated/orbpro/invoke.d.ts +0 -3
  339. package/src/generated/orbpro/invoke.js +0 -5
  340. package/src/generated/spacedatastandards/plg/entry-function.js +0 -86
  341. package/src/generated/spacedatastandards/plg/entry-function.ts +0 -119
  342. package/src/generated/spacedatastandards/plg/index.js +0 -16
  343. package/src/generated/spacedatastandards/plg/index.ts +0 -11
  344. package/src/generated/spacedatastandards/plg/plg.js +0 -657
  345. package/src/generated/spacedatastandards/plg/plg.ts +0 -924
  346. package/src/generated/spacedatastandards/plg/plugin-capability.js +0 -66
  347. package/src/generated/spacedatastandards/plg/plugin-category.js +0 -15
  348. package/src/generated/spacedatastandards/plg/plugin-category.ts +0 -53
  349. package/src/generated/spacedatastandards/plg/plugin-dependency.js +0 -63
  350. package/src/generated/spacedatastandards/plg/plugin-dependency.ts +0 -86
  351. package/src/generated/spacedatastandards/plg/publication-state.js +0 -9
  352. package/src/generated/spacedatastandards/plg/purchase-tier.js +0 -9
  353. /package/src/generated/spacedatastandards/plg/{publication-state.ts → publicationState.ts} +0 -0
  354. /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
+ };