ultravisor 1.0.3 → 1.0.8

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 (248) hide show
  1. package/.claude/launch.json +11 -0
  2. package/.claude/ultravisor-dev-config.json +3 -0
  3. package/card-audit.json +1545 -0
  4. package/docs/_sidebar.md +17 -0
  5. package/docs/css/docuserve.css +73 -0
  6. package/docs/features/beacon-authentication.md +209 -0
  7. package/docs/features/beacon-providers.md +1299 -0
  8. package/docs/features/beacons.md +530 -0
  9. package/docs/features/capabilities.md +193 -0
  10. package/docs/features/llm-model-setup.md +505 -0
  11. package/docs/features/llm.md +262 -0
  12. package/docs/features/tasks-content-system.md +110 -0
  13. package/docs/features/tasks-data-transform.md +233 -0
  14. package/docs/features/tasks-extension.md +36 -0
  15. package/docs/features/tasks-file-system.md +203 -0
  16. package/docs/features/tasks-flow-control.md +125 -0
  17. package/docs/features/tasks-http-client.md +119 -0
  18. package/docs/features/tasks-llm.md +118 -0
  19. package/docs/features/tasks-meadow-api.md +163 -0
  20. package/docs/features/tasks-user-interaction.md +49 -0
  21. package/docs/features/tasks.md +14 -0
  22. package/docs/index.html +1 -1
  23. package/docs/retold-catalog.json +1 -1
  24. package/docs/retold-keyword-index.json +17374 -5
  25. package/operation-library/api-data-pipeline.json +143 -0
  26. package/operation-library/beacon-remote-command.json +72 -0
  27. package/operation-library/conditional-file-backup.json +143 -0
  28. package/operation-library/config-processor.json +136 -0
  29. package/operation-library/csv-data-analysis.json +228 -0
  30. package/operation-library/csv-to-json.json +160 -0
  31. package/operation-library/directory-inventory.json +128 -0
  32. package/operation-library/expression-calculator.json +114 -0
  33. package/operation-library/file-search-replace.json +175 -0
  34. package/operation-library/git-status-report.json +368 -0
  35. package/operation-library/health-check-ping.json +329 -0
  36. package/operation-library/json-config-merge.json +313 -0
  37. package/operation-library/llm-embedding-tool-use.json +314 -0
  38. package/operation-library/llm-multi-turn-analysis.json +322 -0
  39. package/operation-library/llm-summarize.json +264 -0
  40. package/operation-library/log-line-counter.json +185 -0
  41. package/operation-library/meadow-crud-lifecycle.json +264 -0
  42. package/operation-library/npm-project-validator.json +193 -0
  43. package/operation-library/rest-api-orchestrator.json +218 -0
  44. package/operation-library/shell-system-info.json +137 -0
  45. package/operation-library/simple-echo.json +95 -0
  46. package/operation-library/template-processor.json +299 -0
  47. package/operation-library/text-sanitizer.json +176 -0
  48. package/package.json +12 -8
  49. package/source/Ultravisor.cjs +30 -3
  50. package/source/beacon/Ultravisor-Beacon-CLI.cjs +143 -0
  51. package/source/beacon/Ultravisor-Beacon-CapabilityProvider.cjs +129 -0
  52. package/source/beacon/Ultravisor-Beacon-Client.cjs +568 -0
  53. package/source/beacon/Ultravisor-Beacon-Executor.cjs +500 -0
  54. package/source/beacon/Ultravisor-Beacon-ProviderRegistry.cjs +330 -0
  55. package/source/beacon/providers/Ultravisor-Beacon-Provider-FileSystem.cjs +331 -0
  56. package/source/beacon/providers/Ultravisor-Beacon-Provider-LLM.cjs +966 -0
  57. package/source/beacon/providers/Ultravisor-Beacon-Provider-Shell.cjs +95 -0
  58. package/source/cli/Ultravisor-CLIProgram.cjs +44 -22
  59. package/source/cli/commands/Ultravisor-Command-SingleOperation.cjs +29 -18
  60. package/source/cli/commands/Ultravisor-Command-SingleTask.cjs +62 -19
  61. package/source/cli/commands/Ultravisor-Command-UpdateTask.cjs +27 -15
  62. package/source/config/Ultravisor-Default-Command-Configuration.cjs +12 -3
  63. package/source/services/Ultravisor-Beacon-Coordinator.cjs +966 -0
  64. package/source/services/Ultravisor-ExecutionEngine.cjs +1093 -0
  65. package/source/services/Ultravisor-ExecutionManifest.cjs +629 -0
  66. package/source/services/Ultravisor-Hypervisor-State.cjs +270 -97
  67. package/source/services/Ultravisor-Hypervisor.cjs +185 -75
  68. package/source/services/Ultravisor-Schedule-Persistence-Base.cjs +45 -0
  69. package/source/services/Ultravisor-StateManager.cjs +253 -0
  70. package/source/services/Ultravisor-TaskTypeRegistry.cjs +213 -0
  71. package/source/services/persistence/Ultravisor-Schedule-Persistence-JSONFile.cjs +140 -0
  72. package/source/services/tasks/Ultravisor-BuiltIn-TaskConfigs.cjs +19 -0
  73. package/source/services/tasks/Ultravisor-TaskHelper-BeaconDispatch.cjs +107 -0
  74. package/source/services/tasks/Ultravisor-TaskType-Base.cjs +125 -0
  75. package/source/services/tasks/content-system/Ultravisor-TaskConfigs-ContentSystem.cjs +90 -0
  76. package/source/services/tasks/content-system/definitions/content-create-folder.json +24 -0
  77. package/source/services/tasks/content-system/definitions/content-list-files.json +27 -0
  78. package/source/services/tasks/content-system/definitions/content-read-file.json +26 -0
  79. package/source/services/tasks/content-system/definitions/content-save-file.json +26 -0
  80. package/source/services/tasks/data-transform/Ultravisor-TaskConfigs-DataTransform.cjs +577 -0
  81. package/source/services/tasks/data-transform/Ultravisor-TaskType-ReplaceString.cjs +80 -0
  82. package/source/services/tasks/data-transform/Ultravisor-TaskType-SetValues.cjs +74 -0
  83. package/source/services/tasks/data-transform/Ultravisor-TaskType-StringAppender.cjs +90 -0
  84. package/source/services/tasks/data-transform/definitions/comprehension-intersect.json +24 -0
  85. package/source/services/tasks/data-transform/definitions/csv-transform.json +24 -0
  86. package/source/services/tasks/data-transform/definitions/expression-solver.json +20 -0
  87. package/source/services/tasks/data-transform/definitions/histogram.json +23 -0
  88. package/source/services/tasks/data-transform/definitions/parse-csv.json +27 -0
  89. package/source/services/tasks/data-transform/definitions/replace-string.json +27 -0
  90. package/source/services/tasks/data-transform/definitions/set-values.json +17 -0
  91. package/source/services/tasks/data-transform/definitions/string-appender.json +22 -0
  92. package/source/services/tasks/data-transform/definitions/template-string.json +20 -0
  93. package/source/services/tasks/extension/Ultravisor-TaskConfigs-Extension.cjs +87 -0
  94. package/source/services/tasks/extension/definitions/beacon-dispatch.json +32 -0
  95. package/source/services/tasks/file-system/Ultravisor-TaskConfigs-FileSystem.cjs +509 -0
  96. package/source/services/tasks/file-system/Ultravisor-TaskType-ReadFile.cjs +96 -0
  97. package/source/services/tasks/file-system/Ultravisor-TaskType-ReadFileBuffered.cjs +123 -0
  98. package/source/services/tasks/file-system/Ultravisor-TaskType-WriteFile.cjs +106 -0
  99. package/source/services/tasks/file-system/definitions/copy-file.json +27 -0
  100. package/source/services/tasks/file-system/definitions/list-files.json +27 -0
  101. package/source/services/tasks/file-system/definitions/read-file-buffered.json +31 -0
  102. package/source/services/tasks/file-system/definitions/read-file.json +26 -0
  103. package/source/services/tasks/file-system/definitions/read-json.json +23 -0
  104. package/source/services/tasks/file-system/definitions/write-file.json +29 -0
  105. package/source/services/tasks/file-system/definitions/write-json.json +30 -0
  106. package/source/services/tasks/flow-control/Ultravisor-TaskConfigs-FlowControl.cjs +353 -0
  107. package/source/services/tasks/flow-control/Ultravisor-TaskType-IfConditional.cjs +125 -0
  108. package/source/services/tasks/flow-control/Ultravisor-TaskType-LaunchOperation.cjs +186 -0
  109. package/source/services/tasks/flow-control/Ultravisor-TaskType-SplitExecute.cjs +164 -0
  110. package/source/services/tasks/flow-control/definitions/if-conditional.json +25 -0
  111. package/source/services/tasks/flow-control/definitions/launch-operation.json +27 -0
  112. package/source/services/tasks/flow-control/definitions/split-execute.json +32 -0
  113. package/source/services/tasks/http-client/Ultravisor-TaskConfigs-HttpClient.cjs +281 -0
  114. package/source/services/tasks/http-client/definitions/get-json.json +26 -0
  115. package/source/services/tasks/http-client/definitions/get-text.json +26 -0
  116. package/source/services/tasks/http-client/definitions/rest-request.json +32 -0
  117. package/source/services/tasks/http-client/definitions/send-json.json +28 -0
  118. package/source/services/tasks/llm/Ultravisor-TaskConfigs-LLM.cjs +605 -0
  119. package/source/services/tasks/llm/definitions/llm-chat-completion.json +46 -0
  120. package/source/services/tasks/llm/definitions/llm-embedding.json +31 -0
  121. package/source/services/tasks/llm/definitions/llm-tool-use.json +42 -0
  122. package/source/services/tasks/meadow-api/Ultravisor-TaskConfigs-MeadowApi.cjs +341 -0
  123. package/source/services/tasks/meadow-api/definitions/meadow-count.json +26 -0
  124. package/source/services/tasks/meadow-api/definitions/meadow-create.json +26 -0
  125. package/source/services/tasks/meadow-api/definitions/meadow-delete.json +23 -0
  126. package/source/services/tasks/meadow-api/definitions/meadow-read.json +26 -0
  127. package/source/services/tasks/meadow-api/definitions/meadow-reads.json +29 -0
  128. package/source/services/tasks/meadow-api/definitions/meadow-update.json +26 -0
  129. package/source/services/tasks/shell/Ultravisor-TaskConfigs-Shell.cjs +63 -0
  130. package/source/services/tasks/shell/definitions/command.json +29 -0
  131. package/source/services/tasks/user-interaction/Ultravisor-TaskConfigs-UserInteraction.cjs +64 -0
  132. package/source/services/tasks/user-interaction/Ultravisor-TaskType-ErrorMessage.cjs +55 -0
  133. package/source/services/tasks/user-interaction/Ultravisor-TaskType-ValueInput.cjs +47 -0
  134. package/source/services/tasks/user-interaction/definitions/error-message.json +18 -0
  135. package/source/services/tasks/user-interaction/definitions/value-input.json +23 -0
  136. package/source/web_server/Ultravisor-API-Server.cjs +849 -124
  137. package/test/Ultravisor_browser_tests.js +2226 -0
  138. package/test/Ultravisor_operation_library_tests.js +894 -0
  139. package/test/Ultravisor_tests.js +4946 -5044
  140. package/test/workflows/test-content-concatenate-all.json +339 -0
  141. package/test/workflows/test-content-full-pipeline.json +159 -0
  142. package/test/workflows/test-content-list-files.json +76 -0
  143. package/test/workflows/test-content-save-and-read.json +106 -0
  144. package/webinterface/css/ultravisor-themes.css +668 -0
  145. package/webinterface/css/ultravisor.css +37 -14
  146. package/webinterface/docs/card-help/beacon-dispatch.md +30 -0
  147. package/webinterface/docs/card-help/command.md +27 -0
  148. package/webinterface/docs/card-help/comprehension-intersect.md +24 -0
  149. package/webinterface/docs/card-help/content-create-folder.md +22 -0
  150. package/webinterface/docs/card-help/content-list-files.md +25 -0
  151. package/webinterface/docs/card-help/content-read-file.md +24 -0
  152. package/webinterface/docs/card-help/content-save-file.md +24 -0
  153. package/webinterface/docs/card-help/copy-file.md +25 -0
  154. package/webinterface/docs/card-help/csv-transform.md +24 -0
  155. package/webinterface/docs/card-help/end.md +11 -0
  156. package/webinterface/docs/card-help/error-message.md +16 -0
  157. package/webinterface/docs/card-help/expression-solver.md +21 -0
  158. package/webinterface/docs/card-help/get-json.md +24 -0
  159. package/webinterface/docs/card-help/get-text.md +24 -0
  160. package/webinterface/docs/card-help/histogram.md +23 -0
  161. package/webinterface/docs/card-help/if-conditional.md +23 -0
  162. package/webinterface/docs/card-help/launch-operation.md +25 -0
  163. package/webinterface/docs/card-help/list-files.md +25 -0
  164. package/webinterface/docs/card-help/llm-chat-completion.md +43 -0
  165. package/webinterface/docs/card-help/llm-embedding.md +24 -0
  166. package/webinterface/docs/card-help/llm-tool-use.md +39 -0
  167. package/webinterface/docs/card-help/meadow-count.md +24 -0
  168. package/webinterface/docs/card-help/meadow-create.md +24 -0
  169. package/webinterface/docs/card-help/meadow-delete.md +19 -0
  170. package/webinterface/docs/card-help/meadow-read.md +24 -0
  171. package/webinterface/docs/card-help/meadow-reads.md +27 -0
  172. package/webinterface/docs/card-help/meadow-update.md +24 -0
  173. package/webinterface/docs/card-help/parse-csv.md +27 -0
  174. package/webinterface/docs/card-help/read-file-buffered.md +29 -0
  175. package/webinterface/docs/card-help/read-file.md +24 -0
  176. package/webinterface/docs/card-help/read-json.md +21 -0
  177. package/webinterface/docs/card-help/replace-string.md +25 -0
  178. package/webinterface/docs/card-help/rest-request.md +30 -0
  179. package/webinterface/docs/card-help/send-json.md +26 -0
  180. package/webinterface/docs/card-help/set-values.md +16 -0
  181. package/webinterface/docs/card-help/split-execute.md +35 -0
  182. package/webinterface/docs/card-help/start.md +11 -0
  183. package/webinterface/docs/card-help/string-appender.md +22 -0
  184. package/webinterface/docs/card-help/template-string.md +21 -0
  185. package/webinterface/docs/card-help/value-input.md +24 -0
  186. package/webinterface/docs/card-help/write-file.md +27 -0
  187. package/webinterface/docs/card-help/write-json.md +28 -0
  188. package/webinterface/html/index.html +5 -0
  189. package/webinterface/package.json +13 -4
  190. package/webinterface/source/Pict-Application-Ultravisor.js +564 -70
  191. package/webinterface/source/Ultravisor-TimingUtils.js +99 -0
  192. package/webinterface/source/card-help-content.js +46 -0
  193. package/webinterface/source/cards/Ultravisor-BuiltIn-CardConfigs.js +174 -0
  194. package/webinterface/source/cards/Ultravisor-CardConfigGenerator.js +431 -0
  195. package/webinterface/source/data/ExampleFlow-CSVPipeline.js +231 -0
  196. package/webinterface/source/data/ExampleFlow-FileProcessor.js +315 -0
  197. package/webinterface/source/data/ExampleFlow-MeadowPipeline.js +328 -0
  198. package/webinterface/source/panels/Ultravisor-CardSettingsPanel.js +902 -0
  199. package/webinterface/source/providers/PictRouter-Ultravisor-Configuration.json +20 -8
  200. package/webinterface/source/views/PictView-Ultravisor-BeaconList.js +817 -0
  201. package/webinterface/source/views/PictView-Ultravisor-BottomBar.js +5 -5
  202. package/webinterface/source/views/PictView-Ultravisor-Dashboard.js +25 -25
  203. package/webinterface/source/views/PictView-Ultravisor-Documentation.js +856 -0
  204. package/webinterface/source/views/PictView-Ultravisor-FlowEditor.js +777 -0
  205. package/webinterface/source/views/PictView-Ultravisor-ManifestList.js +321 -58
  206. package/webinterface/source/views/PictView-Ultravisor-OperationEdit.js +36 -91
  207. package/webinterface/source/views/PictView-Ultravisor-OperationList.js +388 -16
  208. package/webinterface/source/views/PictView-Ultravisor-PendingInput.js +314 -0
  209. package/webinterface/source/views/PictView-Ultravisor-Schedule.js +521 -65
  210. package/webinterface/source/views/PictView-Ultravisor-TimingView.js +333 -71
  211. package/webinterface/source/views/PictView-Ultravisor-TopBar.js +257 -21
  212. package/webinterface/theme-sampler.html +645 -0
  213. package/debug/Harness.js +0 -6
  214. package/source/services/Ultravisor-Operation-Manifest.cjs +0 -160
  215. package/source/services/Ultravisor-Operation.cjs +0 -200
  216. package/source/services/Ultravisor-Task.cjs +0 -349
  217. package/source/services/events/Ultravisor-Hypervisor-Event-Solver.cjs +0 -11
  218. package/source/services/tasks/Ultravisor-Task-Base.cjs +0 -264
  219. package/source/services/tasks/Ultravisor-Task-CollectValues.cjs +0 -188
  220. package/source/services/tasks/Ultravisor-Task-Command.cjs +0 -65
  221. package/source/services/tasks/Ultravisor-Task-CommandEach.cjs +0 -190
  222. package/source/services/tasks/Ultravisor-Task-Conditional.cjs +0 -104
  223. package/source/services/tasks/Ultravisor-Task-DateWindow.cjs +0 -72
  224. package/source/services/tasks/Ultravisor-Task-GeneratePagedOperation.cjs +0 -336
  225. package/source/services/tasks/Ultravisor-Task-LaunchOperation.cjs +0 -143
  226. package/source/services/tasks/Ultravisor-Task-LaunchTask.cjs +0 -146
  227. package/source/services/tasks/Ultravisor-Task-LineMatch.cjs +0 -158
  228. package/source/services/tasks/Ultravisor-Task-Request.cjs +0 -56
  229. package/source/services/tasks/Ultravisor-Task-Solver.cjs +0 -89
  230. package/source/services/tasks/Ultravisor-Task-TemplateString.cjs +0 -93
  231. package/source/services/tasks/rest/Ultravisor-Task-GetBinary.cjs +0 -127
  232. package/source/services/tasks/rest/Ultravisor-Task-GetJSON.cjs +0 -119
  233. package/source/services/tasks/rest/Ultravisor-Task-GetText.cjs +0 -109
  234. package/source/services/tasks/rest/Ultravisor-Task-GetXML.cjs +0 -112
  235. package/source/services/tasks/rest/Ultravisor-Task-RestRequest.cjs +0 -499
  236. package/source/services/tasks/rest/Ultravisor-Task-SendJSON.cjs +0 -150
  237. package/source/services/tasks/stagingfiles/Ultravisor-Task-CopyFile.cjs +0 -110
  238. package/source/services/tasks/stagingfiles/Ultravisor-Task-ListFiles.cjs +0 -89
  239. package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadBinary.cjs +0 -87
  240. package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadJSON.cjs +0 -67
  241. package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadText.cjs +0 -66
  242. package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadXML.cjs +0 -69
  243. package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteBinary.cjs +0 -95
  244. package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteJSON.cjs +0 -96
  245. package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteText.cjs +0 -99
  246. package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteXML.cjs +0 -102
  247. package/webinterface/source/views/PictView-Ultravisor-TaskEdit.js +0 -220
  248. package/webinterface/source/views/PictView-Ultravisor-TaskList.js +0 -248
@@ -0,0 +1,1093 @@
1
+ const libPictService = require('pict-serviceproviderbase');
2
+
3
+ /**
4
+ * Event-driven graph executor for Ultravisor operations.
5
+ *
6
+ * Replaces the old sequential task runner. Processes an operation's directed
7
+ * graph by following event connections between task nodes. State connections
8
+ * are resolved just-in-time when a task is triggered.
9
+ */
10
+ class UltravisorExecutionEngine extends libPictService
11
+ {
12
+ constructor(pPict, pOptions, pServiceHash)
13
+ {
14
+ super(pPict, pOptions, pServiceHash);
15
+
16
+ this.serviceType = 'UltravisorExecutionEngine';
17
+ }
18
+
19
+ /**
20
+ * Append a timestamped entry to the execution context log and the fable logger.
21
+ *
22
+ * @param {object} pContext - The execution context.
23
+ * @param {string} pMessage - The log message.
24
+ * @param {string} [pLevel] - Log level: 'info' (default), 'warn', 'error', 'trace'.
25
+ */
26
+ _log(pContext, pMessage, pLevel)
27
+ {
28
+ let tmpLevel = pLevel || 'info';
29
+ pContext.Log.push(`[${new Date().toISOString()}] ${pMessage}`);
30
+ this.log[tmpLevel](`ExecutionEngine [${pContext.OperationHash || '?'}]: ${pMessage}`);
31
+ }
32
+
33
+ /**
34
+ * Execute an operation by its definition.
35
+ *
36
+ * @param {object} pOperationDefinition - The operation definition with Graph.
37
+ * @param {object} [pInitialState] - Optional initial state overrides:
38
+ * GlobalState {object} - seed values for global state
39
+ * OperationState {object} - seed values for operation state
40
+ * RunMode {string} - 'production' | 'standard' | 'debug'
41
+ * @param {function} fCallback - function(pError, pExecutionContext)
42
+ */
43
+ executeOperation(pOperationDefinition, pInitialState, fCallback)
44
+ {
45
+ if (typeof(pInitialState) === 'function')
46
+ {
47
+ fCallback = pInitialState;
48
+ pInitialState = {};
49
+ }
50
+
51
+ if (!pOperationDefinition || !pOperationDefinition.Graph)
52
+ {
53
+ return fCallback(new Error('ExecutionEngine: operation definition must have a Graph.'));
54
+ }
55
+
56
+ let tmpInitialState = pInitialState || {};
57
+
58
+ // Get services
59
+ let tmpManifestService = this.fable.servicesMap['UltravisorExecutionManifest']
60
+ ? Object.values(this.fable.servicesMap['UltravisorExecutionManifest'])[0]
61
+ : null;
62
+
63
+ if (!tmpManifestService)
64
+ {
65
+ return fCallback(new Error('ExecutionEngine: UltravisorExecutionManifest service not found.'));
66
+ }
67
+
68
+ // Create execution context with staging folder
69
+ let tmpContext = tmpManifestService.createExecutionContext(
70
+ pOperationDefinition, tmpInitialState.RunMode);
71
+
72
+ // Seed initial state
73
+ if (tmpInitialState.GlobalState && typeof(tmpInitialState.GlobalState) === 'object')
74
+ {
75
+ Object.assign(tmpContext.GlobalState, tmpInitialState.GlobalState);
76
+ }
77
+ if (tmpInitialState.OperationState && typeof(tmpInitialState.OperationState) === 'object')
78
+ {
79
+ Object.assign(tmpContext.OperationState, tmpInitialState.OperationState);
80
+ }
81
+ if (pOperationDefinition.InitialGlobalState && typeof(pOperationDefinition.InitialGlobalState) === 'object')
82
+ {
83
+ // Operation-level defaults (overridden by runtime initial state)
84
+ let tmpKeys = Object.keys(pOperationDefinition.InitialGlobalState);
85
+ for (let i = 0; i < tmpKeys.length; i++)
86
+ {
87
+ if (!tmpContext.GlobalState.hasOwnProperty(tmpKeys[i]))
88
+ {
89
+ tmpContext.GlobalState[tmpKeys[i]] = pOperationDefinition.InitialGlobalState[tmpKeys[i]];
90
+ }
91
+ }
92
+ }
93
+ if (pOperationDefinition.InitialOperationState && typeof(pOperationDefinition.InitialOperationState) === 'object')
94
+ {
95
+ let tmpKeys = Object.keys(pOperationDefinition.InitialOperationState);
96
+ for (let i = 0; i < tmpKeys.length; i++)
97
+ {
98
+ if (!tmpContext.OperationState.hasOwnProperty(tmpKeys[i]))
99
+ {
100
+ tmpContext.OperationState[tmpKeys[i]] = pOperationDefinition.InitialOperationState[tmpKeys[i]];
101
+ }
102
+ }
103
+ }
104
+
105
+ // Store the graph for lookup
106
+ tmpContext._Graph = pOperationDefinition.Graph;
107
+ tmpContext._NodeMap = this._buildNodeMap(pOperationDefinition.Graph);
108
+ tmpContext._PortLabelMap = this._buildPortLabelMap(pOperationDefinition.Graph);
109
+ tmpContext._ConnectionMap = this._buildConnectionMap(
110
+ pOperationDefinition.Graph, tmpContext._NodeMap, tmpContext._PortLabelMap);
111
+
112
+ // Mark as running
113
+ tmpContext.Status = 'Running';
114
+ tmpContext.StartTime = new Date().toISOString();
115
+ this._log(tmpContext, `Operation [${pOperationDefinition.Hash}] started.`);
116
+
117
+ // Find Start node and enqueue its output events
118
+ let tmpStartNode = this._findStartNode(tmpContext);
119
+
120
+ if (!tmpStartNode)
121
+ {
122
+ tmpContext.Status = 'Error';
123
+ this._log(tmpContext, 'No Start node found in the graph.');
124
+ tmpManifestService.finalizeExecution(tmpContext);
125
+ return fCallback(new Error('No Start node found in the graph.'), tmpContext);
126
+ }
127
+
128
+ // Fire all outgoing event connections from the Start node.
129
+ // Start nodes have a single output, so we enqueue all downstream targets
130
+ // without filtering on port name (avoids label vs hash naming mismatches).
131
+ this._enqueueAllDownstreamEvents(tmpStartNode.Hash, tmpContext);
132
+
133
+ // Process the event queue
134
+ this._processEventQueue(tmpContext,
135
+ (pError) =>
136
+ {
137
+ if (pError)
138
+ {
139
+ this._log(tmpContext, `Execution error: ${pError.message}`, 'error');
140
+ tmpContext.Errors.push({
141
+ NodeHash: null,
142
+ Message: pError.message,
143
+ Timestamp: new Date().toISOString()
144
+ });
145
+ }
146
+
147
+ tmpManifestService.finalizeExecution(tmpContext);
148
+ return fCallback(null, tmpContext);
149
+ });
150
+ }
151
+
152
+ /**
153
+ * Resume a paused operation after a value-input task receives input.
154
+ *
155
+ * @param {string} pRunHash - The execution run hash.
156
+ * @param {string} pNodeHash - The waiting task node hash.
157
+ * @param {*} pValue - The provided value.
158
+ * @param {function} fCallback - function(pError, pExecutionContext)
159
+ */
160
+ resumeOperation(pRunHash, pNodeHash, pValue, fCallback)
161
+ {
162
+ let tmpManifestService = this.fable.servicesMap['UltravisorExecutionManifest']
163
+ ? Object.values(this.fable.servicesMap['UltravisorExecutionManifest'])[0]
164
+ : null;
165
+
166
+ if (!tmpManifestService)
167
+ {
168
+ return fCallback(new Error('ExecutionEngine: UltravisorExecutionManifest service not found.'));
169
+ }
170
+
171
+ let tmpContext = tmpManifestService.getRun(pRunHash);
172
+
173
+ if (!tmpContext)
174
+ {
175
+ return fCallback(new Error(`ExecutionEngine: run [${pRunHash}] not found.`));
176
+ }
177
+
178
+ if (tmpContext.Status !== 'WaitingForInput')
179
+ {
180
+ return fCallback(new Error(`ExecutionEngine: run [${pRunHash}] is not waiting for input (status: ${tmpContext.Status}).`));
181
+ }
182
+
183
+ let tmpWaitingInfo = tmpContext.WaitingTasks[pNodeHash];
184
+
185
+ if (!tmpWaitingInfo)
186
+ {
187
+ return fCallback(new Error(`ExecutionEngine: node [${pNodeHash}] is not waiting for input.`));
188
+ }
189
+
190
+ // Get the StateManager to write the value
191
+ let tmpStateManager = this._getStateManager();
192
+
193
+ // Write the value to the specified output address
194
+ if (tmpWaitingInfo.OutputAddress)
195
+ {
196
+ tmpStateManager.setAddress(tmpWaitingInfo.OutputAddress, pValue, tmpContext, pNodeHash);
197
+ }
198
+
199
+ // Store in task outputs
200
+ if (!tmpContext.TaskOutputs[pNodeHash])
201
+ {
202
+ tmpContext.TaskOutputs[pNodeHash] = {};
203
+ }
204
+
205
+ // If pValue is a structured object, merge all keys into TaskOutputs (beacon results)
206
+ // If pValue is a scalar, store as InputValue (backward compat for value-input)
207
+ if (typeof pValue === 'object' && pValue !== null && !Array.isArray(pValue))
208
+ {
209
+ Object.assign(tmpContext.TaskOutputs[pNodeHash], pValue);
210
+ }
211
+ else
212
+ {
213
+ tmpContext.TaskOutputs[pNodeHash].InputValue = pValue;
214
+ }
215
+
216
+ // Update the task's ElapsedMs to include the time spent waiting for input
217
+ let tmpTaskManifest = tmpContext.TaskManifests[pNodeHash];
218
+ if (tmpTaskManifest && tmpTaskManifest.Executions && tmpTaskManifest.Executions.length > 0)
219
+ {
220
+ let tmpExecution = tmpTaskManifest.Executions[tmpTaskManifest.Executions.length - 1];
221
+ let tmpNow = Date.now();
222
+ // Recompute elapsed from original start to now (includes the wait period)
223
+ if (tmpExecution.StartTimeMs)
224
+ {
225
+ tmpExecution.StopTimeMs = tmpNow;
226
+ tmpExecution.StopTime = new Date(tmpNow).toISOString();
227
+ tmpExecution.ElapsedMs = tmpNow - tmpExecution.StartTimeMs;
228
+ }
229
+ }
230
+
231
+ // Remove from waiting list
232
+ delete tmpContext.WaitingTasks[pNodeHash];
233
+
234
+ // Fire the completion event (custom resume event for beacon-dispatch, default for value-input)
235
+ let tmpResumeEvent = tmpWaitingInfo.ResumeEventName || 'ValueInputComplete';
236
+ tmpContext.Status = 'Running';
237
+ this._log(tmpContext, `Input received for node [${pNodeHash}], resuming execution (event: ${tmpResumeEvent}).`);
238
+
239
+ this._enqueueDownstreamEvents(pNodeHash, tmpResumeEvent, tmpContext);
240
+
241
+ // Process the event queue
242
+ this._processEventQueue(tmpContext,
243
+ (pError) =>
244
+ {
245
+ if (pError)
246
+ {
247
+ this._log(tmpContext, `Execution error after resume: ${pError.message}`, 'error');
248
+ }
249
+ tmpManifestService.finalizeExecution(tmpContext);
250
+ return fCallback(null, tmpContext);
251
+ });
252
+ }
253
+
254
+ // ====================================================================
255
+ // Internal: Event Queue Processing
256
+ // ====================================================================
257
+
258
+ /**
259
+ * Process events from the queue until it is empty.
260
+ *
261
+ * @param {object} pContext - The execution context.
262
+ * @param {function} fCallback - Called when queue is empty or operation is paused.
263
+ */
264
+ _processEventQueue(pContext, fCallback)
265
+ {
266
+ if (pContext.PendingEvents.length === 0)
267
+ {
268
+ // Check if we're waiting for input
269
+ if (Object.keys(pContext.WaitingTasks).length > 0)
270
+ {
271
+ pContext.Status = 'WaitingForInput';
272
+ this._log(pContext, 'Operation paused: waiting for user input.');
273
+ }
274
+ return fCallback(null);
275
+ }
276
+
277
+ // Dequeue the next event
278
+ let tmpEvent = pContext.PendingEvents.shift();
279
+
280
+ let tmpDequeueManifest = this._getManifestService();
281
+ if (tmpDequeueManifest)
282
+ {
283
+ tmpDequeueManifest.recordEvent(pContext, tmpEvent.TargetNodeHash, 'EventDequeued',
284
+ `Dequeued [${tmpEvent.EventName}] for [${tmpEvent.TargetNodeHash}]`, 1);
285
+ }
286
+
287
+ this._executeTaskForEvent(tmpEvent.TargetNodeHash, tmpEvent.EventName, pContext,
288
+ (pError) =>
289
+ {
290
+ if (pError)
291
+ {
292
+ this._log(pContext, `Error processing event [${tmpEvent.EventName}] on node [${tmpEvent.TargetNodeHash}]: ${pError.message}`, 'error');
293
+ // Continue processing other events despite errors
294
+ }
295
+
296
+ // Recurse to process next event
297
+ this._processEventQueue(pContext, fCallback);
298
+ });
299
+ }
300
+
301
+ /**
302
+ * Execute a task node in response to an incoming event.
303
+ *
304
+ * @param {string} pNodeHash - The target node hash.
305
+ * @param {string} pEventName - The event that triggered this execution.
306
+ * @param {object} pContext - The execution context.
307
+ * @param {function} fCallback - Called when task execution is complete.
308
+ */
309
+ _executeTaskForEvent(pNodeHash, pEventName, pContext, fCallback)
310
+ {
311
+ let tmpNode = pContext._NodeMap[pNodeHash];
312
+
313
+ if (!tmpNode)
314
+ {
315
+ this._log(pContext, `Node [${pNodeHash}] not found in graph.`, 'error');
316
+ return fCallback(null);
317
+ }
318
+
319
+ // Handle built-in End node
320
+ if (tmpNode.Type === 'end')
321
+ {
322
+ this._log(pContext, `Reached End node [${pNodeHash}].`);
323
+ return fCallback(null);
324
+ }
325
+
326
+ // Handle built-in Start node (shouldn't be a target, but handle gracefully)
327
+ if (tmpNode.Type === 'start')
328
+ {
329
+ return fCallback(null);
330
+ }
331
+
332
+ // Find the task type
333
+ let tmpRegistry = this._getTaskTypeRegistry();
334
+
335
+ if (!tmpRegistry)
336
+ {
337
+ return fCallback(new Error('TaskTypeRegistry service not found.'));
338
+ }
339
+
340
+ let tmpDefinitionHash = tmpNode.DefinitionHash || tmpNode.Type;
341
+ let tmpDefinition = tmpRegistry.getDefinition(tmpDefinitionHash);
342
+
343
+ if (!tmpDefinition)
344
+ {
345
+ this._log(pContext, `Unknown task type [${tmpDefinitionHash}] for node [${pNodeHash}].`, 'error');
346
+ return fCallback(null);
347
+ }
348
+
349
+ // Resolve incoming state connections
350
+ let tmpResolvedSettings = this._resolveStateConnections(pNodeHash, tmpNode, pContext);
351
+
352
+ // Get the manifest service for recording
353
+ let tmpManifestService = this._getManifestService();
354
+ if (tmpManifestService)
355
+ {
356
+ tmpManifestService.recordTaskStart(pContext, pNodeHash, pEventName, {
357
+ DefinitionHash: tmpDefinitionHash,
358
+ TaskTypeName: tmpDefinition.Name || '',
359
+ Category: tmpDefinition.Category || '',
360
+ Capability: tmpDefinition.Capability || '',
361
+ Action: tmpDefinition.Action || '',
362
+ Tier: tmpDefinition.Tier || ''
363
+ });
364
+ tmpManifestService.recordEvent(pContext, pNodeHash, 'TaskStart',
365
+ `Executing [${pNodeHash}] (${tmpDefinition.Name || tmpDefinitionHash}) triggered by [${pEventName}]`, 0);
366
+ }
367
+
368
+ this._log(pContext, `Executing node [${pNodeHash}] (${tmpDefinition.Name}) triggered by [${pEventName}]`);
369
+
370
+ // Create task instance and execute
371
+ let tmpTaskInstance = tmpRegistry.instantiateTaskType(tmpDefinitionHash);
372
+
373
+ if (!tmpTaskInstance)
374
+ {
375
+ this._log(pContext, `Failed to instantiate task type [${tmpDefinitionHash}].`, 'error');
376
+ return fCallback(null);
377
+ }
378
+
379
+ // Build the per-task execution context
380
+ let tmpTaskContext = {
381
+ GlobalState: pContext.GlobalState,
382
+ OperationState: pContext.OperationState,
383
+ TaskOutputs: pContext.TaskOutputs,
384
+ StagingPath: pContext.StagingPath,
385
+ OperationHash: pContext.OperationHash,
386
+ NodeHash: pNodeHash,
387
+ RunHash: pContext.Hash,
388
+ RunMode: pContext.RunMode,
389
+ StateManager: this._getStateManager(),
390
+ TriggeringEventName: pEventName
391
+ };
392
+
393
+ // Build the fFireIntermediateEvent function for re-entrant tasks
394
+ let fFireIntermediateEvent = (pIntermediateEventName, pIntermediateOutputs, fResumeCallback) =>
395
+ {
396
+ // Record the intermediate event
397
+ if (tmpManifestService)
398
+ {
399
+ tmpManifestService.recordEvent(pContext, pNodeHash, 'IntermediateEvent',
400
+ `Intermediate event [${pIntermediateEventName}] from [${pNodeHash}]`, 1);
401
+ }
402
+
403
+ // Store the intermediate outputs
404
+ if (!pContext.TaskOutputs[pNodeHash])
405
+ {
406
+ pContext.TaskOutputs[pNodeHash] = {};
407
+ }
408
+ Object.assign(pContext.TaskOutputs[pNodeHash], pIntermediateOutputs);
409
+
410
+ // Find downstream nodes for this intermediate event
411
+ let tmpDownstreamEvents = this._getDownstreamEvents(pNodeHash, pIntermediateEventName, pContext);
412
+
413
+ if (tmpDownstreamEvents.length === 0)
414
+ {
415
+ // No downstream connections for this event
416
+ return fResumeCallback();
417
+ }
418
+
419
+ // Process the downstream sub-graph synchronously
420
+ let tmpSubIndex = 0;
421
+
422
+ let fProcessNextDownstream = () =>
423
+ {
424
+ if (tmpSubIndex >= tmpDownstreamEvents.length)
425
+ {
426
+ return fResumeCallback();
427
+ }
428
+
429
+ let tmpDownstreamEvent = tmpDownstreamEvents[tmpSubIndex];
430
+ tmpSubIndex++;
431
+
432
+ this._executeTaskForEvent(tmpDownstreamEvent.TargetNodeHash, tmpDownstreamEvent.EventName, pContext,
433
+ (pError) =>
434
+ {
435
+ if (pError)
436
+ {
437
+ this._log(pContext, `Error in sub-graph: ${pError.message}`, 'error');
438
+ }
439
+
440
+ // Also process any events that were enqueued during sub-graph execution
441
+ this._drainEventsForSubgraph(pContext, () =>
442
+ {
443
+ fProcessNextDownstream();
444
+ });
445
+ });
446
+ };
447
+
448
+ fProcessNextDownstream();
449
+ };
450
+
451
+ // Execute the task
452
+ tmpTaskInstance.execute(tmpResolvedSettings, tmpTaskContext, (pError, pResult) =>
453
+ {
454
+ if (pError)
455
+ {
456
+ this._log(pContext, `Task [${pNodeHash}] error: ${pError.message}`, 'error');
457
+ if (tmpManifestService)
458
+ {
459
+ tmpManifestService.recordTaskError(pContext, pNodeHash, pError);
460
+ tmpManifestService.recordEvent(pContext, pNodeHash, 'TaskError',
461
+ `Error in [${pNodeHash}]: ${pError.message}`, 0);
462
+ }
463
+
464
+ // Fire error event if the task has one
465
+ this._enqueueDownstreamEvents(pNodeHash, 'Error', pContext);
466
+ return fCallback(null);
467
+ }
468
+
469
+ if (!pResult)
470
+ {
471
+ this._log(pContext, `Task [${pNodeHash}] returned no result.`, 'warn');
472
+ return fCallback(null);
473
+ }
474
+
475
+ // Check for WaitingForInput (value-input or beacon-dispatch task)
476
+ if (pResult.WaitingForInput)
477
+ {
478
+ pContext.WaitingTasks[pNodeHash] = {
479
+ PromptMessage: pResult.PromptMessage || '',
480
+ OutputAddress: pResult.OutputAddress || '',
481
+ ResumeEventName: pResult.ResumeEventName || '',
482
+ Timestamp: new Date().toISOString()
483
+ };
484
+ this._log(pContext, `Task [${pNodeHash}] is waiting for input (resume event: ${pResult.ResumeEventName || 'ValueInputComplete'}).`);
485
+ if (tmpManifestService)
486
+ {
487
+ tmpManifestService.recordTaskComplete(pContext, pNodeHash, pResult);
488
+ }
489
+ return fCallback(null);
490
+ }
491
+
492
+ // Store outputs in TaskOutputs
493
+ if (pResult.Outputs && typeof(pResult.Outputs) === 'object')
494
+ {
495
+ if (!pContext.TaskOutputs[pNodeHash])
496
+ {
497
+ pContext.TaskOutputs[pNodeHash] = {};
498
+ }
499
+ Object.assign(pContext.TaskOutputs[pNodeHash], pResult.Outputs);
500
+ }
501
+
502
+ // Store any state writes from the result
503
+ if (pResult.StateWrites && typeof(pResult.StateWrites) === 'object')
504
+ {
505
+ let tmpStateManager = this._getStateManager();
506
+ let tmpWriteKeys = Object.keys(pResult.StateWrites);
507
+ for (let i = 0; i < tmpWriteKeys.length; i++)
508
+ {
509
+ tmpStateManager.setAddress(tmpWriteKeys[i], pResult.StateWrites[tmpWriteKeys[i]],
510
+ pContext, pNodeHash);
511
+ }
512
+ }
513
+
514
+ // Determine if this result is an error event
515
+ let tmpIsErrorResult = false;
516
+ if (pResult.EventToFire && tmpDefinition && Array.isArray(tmpDefinition.EventOutputs))
517
+ {
518
+ for (let e = 0; e < tmpDefinition.EventOutputs.length; e++)
519
+ {
520
+ if (tmpDefinition.EventOutputs[e].Name === pResult.EventToFire
521
+ && tmpDefinition.EventOutputs[e].IsError)
522
+ {
523
+ tmpIsErrorResult = true;
524
+ break;
525
+ }
526
+ }
527
+ }
528
+
529
+ // Log task messages
530
+ if (Array.isArray(pResult.Log))
531
+ {
532
+ let tmpLogLevel = tmpIsErrorResult ? 'error' : 'trace';
533
+ for (let i = 0; i < pResult.Log.length; i++)
534
+ {
535
+ this._log(pContext, ` [${pNodeHash}] ${pResult.Log[i]}`, tmpLogLevel);
536
+ }
537
+ }
538
+
539
+ // Record completion
540
+ if (tmpManifestService)
541
+ {
542
+ tmpManifestService.recordTaskComplete(pContext, pNodeHash, pResult);
543
+ tmpManifestService.recordEvent(pContext, pNodeHash, 'TaskComplete',
544
+ `Completed [${pNodeHash}] -> ${pResult.EventToFire || 'no event'}`, 0);
545
+ }
546
+
547
+ // Fire the output event (enqueue downstream tasks)
548
+ if (pResult.EventToFire)
549
+ {
550
+ let tmpQueueLenBefore = pContext.PendingEvents.length;
551
+ this._enqueueDownstreamEvents(pNodeHash, pResult.EventToFire, pContext);
552
+ let tmpHandled = pContext.PendingEvents.length > tmpQueueLenBefore;
553
+
554
+ // Record an unhandled error on the context when no downstream
555
+ // error handler is connected.
556
+ if (tmpIsErrorResult && !tmpHandled)
557
+ {
558
+ let tmpErrorMessage = (Array.isArray(pResult.Log) && pResult.Log.length > 0)
559
+ ? pResult.Log.join('; ')
560
+ : `Task [${pNodeHash}] fired error event.`;
561
+ pContext.Errors.push({
562
+ NodeHash: pNodeHash,
563
+ Message: tmpErrorMessage,
564
+ Timestamp: new Date().toISOString()
565
+ });
566
+ }
567
+ }
568
+ else if (tmpIsErrorResult)
569
+ {
570
+ // Error result with no EventToFire — still record the error
571
+ let tmpErrorMessage = (Array.isArray(pResult.Log) && pResult.Log.length > 0)
572
+ ? pResult.Log.join('; ')
573
+ : `Task [${pNodeHash}] fired error event.`;
574
+ pContext.Errors.push({
575
+ NodeHash: pNodeHash,
576
+ Message: tmpErrorMessage,
577
+ Timestamp: new Date().toISOString()
578
+ });
579
+ }
580
+
581
+ return fCallback(null);
582
+ },
583
+ fFireIntermediateEvent);
584
+ }
585
+
586
+ // ====================================================================
587
+ // Internal: State Connection Resolution
588
+ // ====================================================================
589
+
590
+ /**
591
+ * Resolve all incoming state connections for a node, producing merged settings.
592
+ *
593
+ * @param {string} pNodeHash - The target node hash.
594
+ * @param {object} pNode - The node definition from the graph.
595
+ * @param {object} pContext - The execution context.
596
+ * @returns {object} The resolved settings object.
597
+ */
598
+ _resolveStateConnections(pNodeHash, pNode, pContext)
599
+ {
600
+ // Start with a copy of the node's static settings
601
+ let tmpSettings = {};
602
+
603
+ if (pNode.Settings && typeof(pNode.Settings) === 'object')
604
+ {
605
+ tmpSettings = JSON.parse(JSON.stringify(pNode.Settings));
606
+ }
607
+
608
+ // Find all incoming State connections targeting this node
609
+ let tmpStateConnections = pContext._ConnectionMap.stateTargets[pNodeHash] || [];
610
+ let tmpStateManager = this._getStateManager();
611
+ let tmpPortLabelMap = pContext._PortLabelMap;
612
+
613
+ for (let i = 0; i < tmpStateConnections.length; i++)
614
+ {
615
+ let tmpConn = tmpStateConnections[i];
616
+
617
+ // Get the source port name
618
+ let tmpSourcePortName = this._extractPortName(tmpConn.SourcePortHash, tmpPortLabelMap);
619
+ let tmpTargetPortName = this._extractPortName(tmpConn.TargetPortHash, tmpPortLabelMap);
620
+
621
+ // Read the source value from the source node's outputs
622
+ let tmpSourceNodeOutputs = pContext.TaskOutputs[tmpConn.SourceNodeHash] || {};
623
+ let tmpSourceValue = tmpSourceNodeOutputs[tmpSourcePortName];
624
+
625
+ // Apply template if defined
626
+ if (tmpConn.Data && tmpConn.Data.Template && typeof(tmpConn.Data.Template) === 'string')
627
+ {
628
+ let tmpTemplateContext = tmpStateManager.buildTemplateContext(pContext, tmpSourceValue);
629
+ tmpSourceValue = this._resolveTemplate(tmpConn.Data.Template, tmpTemplateContext);
630
+ }
631
+
632
+ // Write the resolved value into settings
633
+ if (tmpTargetPortName && tmpSourceValue !== undefined)
634
+ {
635
+ tmpSettings[tmpTargetPortName] = tmpSourceValue;
636
+ }
637
+ }
638
+
639
+ // Resolve any template expressions in settings values
640
+ // (e.g. "{~D:Record.Operation.InputFilePath~}" -> actual value from state)
641
+ let tmpTemplateContext = tmpStateManager.buildTemplateContext(pContext);
642
+
643
+ let tmpSettingsKeys = Object.keys(tmpSettings);
644
+ for (let i = 0; i < tmpSettingsKeys.length; i++)
645
+ {
646
+ let tmpKey = tmpSettingsKeys[i];
647
+ let tmpVal = tmpSettings[tmpKey];
648
+
649
+ if (typeof(tmpVal) === 'string' && tmpVal.indexOf('{~') >= 0)
650
+ {
651
+ tmpSettings[tmpKey] = this._resolveTemplate(tmpVal, tmpTemplateContext);
652
+ }
653
+ }
654
+
655
+ return tmpSettings;
656
+ }
657
+
658
+ // ====================================================================
659
+ // Internal: Graph Traversal Helpers
660
+ // ====================================================================
661
+
662
+ /**
663
+ * Build a lookup map of nodes keyed by Hash.
664
+ */
665
+ _buildNodeMap(pGraph)
666
+ {
667
+ let tmpMap = {};
668
+ let tmpNodes = pGraph.Nodes || [];
669
+
670
+ for (let i = 0; i < tmpNodes.length; i++)
671
+ {
672
+ let tmpNode = tmpNodes[i];
673
+
674
+ // Normalize flow editor format: "Data" -> "Settings"
675
+ if (!tmpNode.Settings && tmpNode.Data && typeof(tmpNode.Data) === 'object')
676
+ {
677
+ tmpNode.Settings = tmpNode.Data;
678
+ }
679
+
680
+ tmpMap[tmpNode.Hash] = tmpNode;
681
+ }
682
+
683
+ return tmpMap;
684
+ }
685
+
686
+ /**
687
+ * Build connection lookup maps for fast traversal.
688
+ * Creates two indices:
689
+ * eventSources[sourceNodeHash] -> array of connections with ConnectionType='Event'
690
+ * stateTargets[targetNodeHash] -> array of connections with ConnectionType='State'
691
+ *
692
+ * When ConnectionType is not explicitly set (flow editor format), the type is
693
+ * inferred from port hash convention (-eo-/-ei- vs -so-/-si-), node types
694
+ * (start/end are always event), or task type definitions.
695
+ */
696
+ _buildConnectionMap(pGraph, pNodeMap, pPortLabelMap)
697
+ {
698
+ let tmpMap = {
699
+ eventSources: {},
700
+ stateTargets: {}
701
+ };
702
+
703
+ let tmpConnections = pGraph.Connections || [];
704
+
705
+ for (let i = 0; i < tmpConnections.length; i++)
706
+ {
707
+ let tmpConn = tmpConnections[i];
708
+
709
+ // Determine connection type (explicit or inferred)
710
+ let tmpType = tmpConn.ConnectionType
711
+ || this._inferConnectionType(tmpConn, pNodeMap, pPortLabelMap);
712
+
713
+ if (tmpType === 'Event')
714
+ {
715
+ if (!tmpMap.eventSources[tmpConn.SourceNodeHash])
716
+ {
717
+ tmpMap.eventSources[tmpConn.SourceNodeHash] = [];
718
+ }
719
+ tmpMap.eventSources[tmpConn.SourceNodeHash].push(tmpConn);
720
+ }
721
+ else if (tmpType === 'State')
722
+ {
723
+ if (!tmpMap.stateTargets[tmpConn.TargetNodeHash])
724
+ {
725
+ tmpMap.stateTargets[tmpConn.TargetNodeHash] = [];
726
+ }
727
+ tmpMap.stateTargets[tmpConn.TargetNodeHash].push(tmpConn);
728
+ }
729
+ }
730
+
731
+ return tmpMap;
732
+ }
733
+
734
+ /**
735
+ * Find the Start node in the graph.
736
+ */
737
+ _findStartNode(pContext)
738
+ {
739
+ let tmpNodes = pContext._Graph.Nodes || [];
740
+
741
+ for (let i = 0; i < tmpNodes.length; i++)
742
+ {
743
+ if (tmpNodes[i].Type === 'start')
744
+ {
745
+ return tmpNodes[i];
746
+ }
747
+ }
748
+
749
+ return null;
750
+ }
751
+
752
+ /**
753
+ * Enqueue downstream event connections from a source node's output event port.
754
+ *
755
+ * @param {string} pSourceNodeHash - The node firing the event.
756
+ * @param {string} pEventName - The event name (matches the source port name).
757
+ * @param {object} pContext - The execution context.
758
+ */
759
+ /**
760
+ * Enqueue ALL downstream event connections from a source node, ignoring port name.
761
+ * Used for Start nodes which have a single output port.
762
+ */
763
+ _enqueueAllDownstreamEvents(pSourceNodeHash, pContext)
764
+ {
765
+ let tmpConnections = pContext._ConnectionMap.eventSources[pSourceNodeHash] || [];
766
+ let tmpPortLabelMap = pContext._PortLabelMap;
767
+
768
+ for (let i = 0; i < tmpConnections.length; i++)
769
+ {
770
+ let tmpConn = tmpConnections[i];
771
+ let tmpTargetPortName = this._extractPortName(tmpConn.TargetPortHash, tmpPortLabelMap);
772
+ pContext.PendingEvents.push({
773
+ TargetNodeHash: tmpConn.TargetNodeHash,
774
+ EventName: tmpTargetPortName
775
+ });
776
+ }
777
+ }
778
+
779
+ _enqueueDownstreamEvents(pSourceNodeHash, pEventName, pContext)
780
+ {
781
+ let tmpConnections = pContext._ConnectionMap.eventSources[pSourceNodeHash] || [];
782
+ let tmpPortLabelMap = pContext._PortLabelMap;
783
+
784
+ for (let i = 0; i < tmpConnections.length; i++)
785
+ {
786
+ let tmpConn = tmpConnections[i];
787
+ let tmpSourcePortName = this._extractPortName(tmpConn.SourcePortHash, tmpPortLabelMap);
788
+
789
+ if (tmpSourcePortName === pEventName)
790
+ {
791
+ let tmpTargetPortName = this._extractPortName(tmpConn.TargetPortHash, tmpPortLabelMap);
792
+ pContext.PendingEvents.push({
793
+ TargetNodeHash: tmpConn.TargetNodeHash,
794
+ EventName: tmpTargetPortName
795
+ });
796
+ }
797
+ }
798
+ }
799
+
800
+ /**
801
+ * Get downstream event targets for an intermediate event (for sub-graph execution).
802
+ * Returns the targets directly instead of enqueuing them.
803
+ */
804
+ _getDownstreamEvents(pSourceNodeHash, pEventName, pContext)
805
+ {
806
+ let tmpTargets = [];
807
+ let tmpConnections = pContext._ConnectionMap.eventSources[pSourceNodeHash] || [];
808
+ let tmpPortLabelMap = pContext._PortLabelMap;
809
+
810
+ for (let i = 0; i < tmpConnections.length; i++)
811
+ {
812
+ let tmpConn = tmpConnections[i];
813
+ let tmpSourcePortName = this._extractPortName(tmpConn.SourcePortHash, tmpPortLabelMap);
814
+
815
+ if (tmpSourcePortName === pEventName)
816
+ {
817
+ let tmpTargetPortName = this._extractPortName(tmpConn.TargetPortHash, tmpPortLabelMap);
818
+ tmpTargets.push({
819
+ TargetNodeHash: tmpConn.TargetNodeHash,
820
+ EventName: tmpTargetPortName
821
+ });
822
+ }
823
+ }
824
+
825
+ return tmpTargets;
826
+ }
827
+
828
+ /**
829
+ * Drain any events that were enqueued during sub-graph execution.
830
+ * This ensures intermediate event processing completes before
831
+ * the parent task continues.
832
+ */
833
+ _drainEventsForSubgraph(pContext, fCallback)
834
+ {
835
+ if (pContext.PendingEvents.length === 0)
836
+ {
837
+ return fCallback();
838
+ }
839
+
840
+ let tmpEvent = pContext.PendingEvents.shift();
841
+
842
+ this._executeTaskForEvent(tmpEvent.TargetNodeHash, tmpEvent.EventName, pContext,
843
+ (pError) =>
844
+ {
845
+ if (pError)
846
+ {
847
+ this._log(pContext, `Error in sub-graph drain: ${pError.message}`, 'error');
848
+ }
849
+
850
+ this._drainEventsForSubgraph(pContext, fCallback);
851
+ });
852
+ }
853
+
854
+ /**
855
+ * Extract the port name from a port hash.
856
+ *
857
+ * Supports two formats:
858
+ * 1. Programmatic: {NodeHash}-{portTypePrefix}-{Name}
859
+ * e.g. 'TSK-READFILE-001-eo-ReadComplete' -> 'ReadComplete'
860
+ * 2. Flow editor: arbitrary hashes with port Labels stored in the graph
861
+ * e.g. 'fp-read-done' -> looks up Label 'ReadComplete' from PortLabelMap
862
+ *
863
+ * @param {string} pPortHash - The port hash string.
864
+ * @param {object} [pPortLabelMap] - Optional mapping of port hash -> label.
865
+ */
866
+ _extractPortName(pPortHash, pPortLabelMap)
867
+ {
868
+ if (!pPortHash || typeof(pPortHash) !== 'string')
869
+ {
870
+ return '';
871
+ }
872
+
873
+ // First try the standard -eo-/-ei-/-so-/-si- convention
874
+ let tmpPrefixes = ['-ei-', '-eo-', '-si-', '-so-'];
875
+
876
+ for (let i = 0; i < tmpPrefixes.length; i++)
877
+ {
878
+ let tmpIndex = pPortHash.lastIndexOf(tmpPrefixes[i]);
879
+ if (tmpIndex > -1)
880
+ {
881
+ return pPortHash.substring(tmpIndex + tmpPrefixes[i].length);
882
+ }
883
+ }
884
+
885
+ // Then try the port label map (flow editor format)
886
+ if (pPortLabelMap && pPortLabelMap[pPortHash])
887
+ {
888
+ return pPortLabelMap[pPortHash];
889
+ }
890
+
891
+ // Fallback: return the hash as-is
892
+ return pPortHash;
893
+ }
894
+
895
+ // ====================================================================
896
+ // Internal: Template Resolution
897
+ // ====================================================================
898
+
899
+ /**
900
+ * Resolve a template string against a template context object.
901
+ *
902
+ * Uses Pict's parseTemplate for full template support. The template context
903
+ * from StateManager.buildTemplateContext() is passed as the record parameter,
904
+ * which Pict places at `Record` on the root data object.
905
+ *
906
+ * Template addresses use the `Record.` prefix to reach the context:
907
+ * {~D:Record.Value~} -> the source value from the state connection
908
+ * {~D:Record.Global.X~} -> GlobalState.X
909
+ * {~D:Record.Operation.X~} -> OperationState.X
910
+ * {~D:Record.TaskOutput.NodeHash.X~} -> TaskOutputs[NodeHash].X
911
+ * {~D:Record.Staging.Path~} -> StagingPath
912
+ *
913
+ * @param {string} pTemplate - The template string.
914
+ * @param {object} pContext - The template context (from StateManager.buildTemplateContext).
915
+ * @returns {string} The resolved string.
916
+ */
917
+ _resolveTemplate(pTemplate, pContext)
918
+ {
919
+ if (typeof(this.fable.parseTemplate) === 'function')
920
+ {
921
+ return this.fable.parseTemplate(pTemplate, pContext);
922
+ }
923
+
924
+ this.log.warn('ExecutionEngine._resolveTemplate: parseTemplate not available on fable instance. Template expressions will not be resolved.');
925
+ return pTemplate;
926
+ }
927
+
928
+ // ====================================================================
929
+ // Internal: Port and Connection Helpers
930
+ // ====================================================================
931
+
932
+ /**
933
+ * Build a lookup map from port hash -> port Label for all nodes in the graph.
934
+ * Used to resolve port names when hashes don't follow the -eo-/-ei- convention.
935
+ */
936
+ _buildPortLabelMap(pGraph)
937
+ {
938
+ let tmpMap = {};
939
+ let tmpNodes = pGraph.Nodes || [];
940
+
941
+ for (let i = 0; i < tmpNodes.length; i++)
942
+ {
943
+ let tmpPorts = tmpNodes[i].Ports || [];
944
+
945
+ for (let j = 0; j < tmpPorts.length; j++)
946
+ {
947
+ tmpMap[tmpPorts[j].Hash] = tmpPorts[j].Label || tmpPorts[j].Hash;
948
+ }
949
+ }
950
+
951
+ return tmpMap;
952
+ }
953
+
954
+ /**
955
+ * Infer the ConnectionType when not explicitly set on a connection.
956
+ *
957
+ * Uses these heuristics in order:
958
+ * 1. Port hash convention: -eo-/-ei- -> Event, -so-/-si- -> State
959
+ * 2. Start/End node connections are always Event
960
+ * 3. Task type definitions: check if port labels match EventOutputs or StateOutputs
961
+ * 4. Default: Event
962
+ */
963
+ _inferConnectionType(pConn, pNodeMap, pPortLabelMap)
964
+ {
965
+ let tmpSourcePortHash = pConn.SourcePortHash || '';
966
+ let tmpTargetPortHash = pConn.TargetPortHash || '';
967
+
968
+ // Check port hash convention
969
+ if (tmpSourcePortHash.includes('-eo-') || tmpTargetPortHash.includes('-ei-'))
970
+ {
971
+ return 'Event';
972
+ }
973
+ if (tmpSourcePortHash.includes('-so-') || tmpTargetPortHash.includes('-si-'))
974
+ {
975
+ return 'State';
976
+ }
977
+
978
+ // Start and End nodes only have event ports
979
+ let tmpSourceNode = pNodeMap ? pNodeMap[pConn.SourceNodeHash] : null;
980
+ let tmpTargetNode = pNodeMap ? pNodeMap[pConn.TargetNodeHash] : null;
981
+
982
+ if (tmpSourceNode && (tmpSourceNode.Type === 'start' || tmpSourceNode.Type === 'end'))
983
+ {
984
+ return 'Event';
985
+ }
986
+ if (tmpTargetNode && (tmpTargetNode.Type === 'start' || tmpTargetNode.Type === 'end'))
987
+ {
988
+ return 'Event';
989
+ }
990
+
991
+ // Look up port labels and check against task type definitions
992
+ let tmpRegistry = this._getTaskTypeRegistry();
993
+
994
+ if (tmpRegistry && tmpSourceNode)
995
+ {
996
+ let tmpSourceLabel = pPortLabelMap ? (pPortLabelMap[tmpSourcePortHash] || '') : '';
997
+ let tmpDefHash = tmpSourceNode.DefinitionHash || tmpSourceNode.Type;
998
+ let tmpDef = tmpRegistry.getDefinition(tmpDefHash);
999
+
1000
+ if (tmpDef)
1001
+ {
1002
+ if (tmpDef.EventOutputs)
1003
+ {
1004
+ for (let i = 0; i < tmpDef.EventOutputs.length; i++)
1005
+ {
1006
+ if (tmpDef.EventOutputs[i].Name === tmpSourceLabel)
1007
+ {
1008
+ return 'Event';
1009
+ }
1010
+ }
1011
+ }
1012
+ if (tmpDef.StateOutputs)
1013
+ {
1014
+ for (let i = 0; i < tmpDef.StateOutputs.length; i++)
1015
+ {
1016
+ if (tmpDef.StateOutputs[i].Name === tmpSourceLabel)
1017
+ {
1018
+ return 'State';
1019
+ }
1020
+ }
1021
+ }
1022
+ }
1023
+ }
1024
+
1025
+ // Also check target node to classify
1026
+ if (tmpRegistry && tmpTargetNode)
1027
+ {
1028
+ let tmpTargetLabel = pPortLabelMap ? (pPortLabelMap[tmpTargetPortHash] || '') : '';
1029
+ let tmpDefHash = tmpTargetNode.DefinitionHash || tmpTargetNode.Type;
1030
+ let tmpDef = tmpRegistry.getDefinition(tmpDefHash);
1031
+
1032
+ if (tmpDef)
1033
+ {
1034
+ if (tmpDef.EventInputs)
1035
+ {
1036
+ for (let i = 0; i < tmpDef.EventInputs.length; i++)
1037
+ {
1038
+ if (tmpDef.EventInputs[i].Name === tmpTargetLabel)
1039
+ {
1040
+ return 'Event';
1041
+ }
1042
+ }
1043
+ }
1044
+ if (tmpDef.SettingsInputs)
1045
+ {
1046
+ for (let i = 0; i < tmpDef.SettingsInputs.length; i++)
1047
+ {
1048
+ if (tmpDef.SettingsInputs[i].Name === tmpTargetLabel)
1049
+ {
1050
+ return 'State';
1051
+ }
1052
+ }
1053
+ }
1054
+ }
1055
+ }
1056
+
1057
+ // Default to Event
1058
+ return 'Event';
1059
+ }
1060
+
1061
+ // ====================================================================
1062
+ // Internal: Service Access
1063
+ // ====================================================================
1064
+
1065
+ _getStateManager()
1066
+ {
1067
+ if (this.fable.servicesMap['UltravisorStateManager'])
1068
+ {
1069
+ return Object.values(this.fable.servicesMap['UltravisorStateManager'])[0];
1070
+ }
1071
+ return null;
1072
+ }
1073
+
1074
+ _getTaskTypeRegistry()
1075
+ {
1076
+ if (this.fable.servicesMap['UltravisorTaskTypeRegistry'])
1077
+ {
1078
+ return Object.values(this.fable.servicesMap['UltravisorTaskTypeRegistry'])[0];
1079
+ }
1080
+ return null;
1081
+ }
1082
+
1083
+ _getManifestService()
1084
+ {
1085
+ if (this.fable.servicesMap['UltravisorExecutionManifest'])
1086
+ {
1087
+ return Object.values(this.fable.servicesMap['UltravisorExecutionManifest'])[0];
1088
+ }
1089
+ return null;
1090
+ }
1091
+ }
1092
+
1093
+ module.exports = UltravisorExecutionEngine;