tasker-engine 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (605) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +22 -0
  3. data/README.md +443 -0
  4. data/Rakefile +10 -0
  5. data/app/controllers/tasker/analytics_controller.rb +179 -0
  6. data/app/controllers/tasker/application_controller.rb +45 -0
  7. data/app/controllers/tasker/graphql_controller.rb +193 -0
  8. data/app/controllers/tasker/handlers_controller.rb +217 -0
  9. data/app/controllers/tasker/health_controller.rb +229 -0
  10. data/app/controllers/tasker/metrics_controller.rb +111 -0
  11. data/app/controllers/tasker/page_sort.rb +97 -0
  12. data/app/controllers/tasker/task_diagrams_controller.rb +30 -0
  13. data/app/controllers/tasker/tasks_controller.rb +123 -0
  14. data/app/controllers/tasker/workflow_steps_controller.rb +69 -0
  15. data/app/graphql/examples/all_tasks.graphql +22 -0
  16. data/app/graphql/examples/pending_tasks.graphql +23 -0
  17. data/app/graphql/tasker/graph_ql_types/annotation_type.rb +14 -0
  18. data/app/graphql/tasker/graph_ql_types/base_argument.rb +9 -0
  19. data/app/graphql/tasker/graph_ql_types/base_connection.rb +11 -0
  20. data/app/graphql/tasker/graph_ql_types/base_edge.rb +10 -0
  21. data/app/graphql/tasker/graph_ql_types/base_enum.rb +9 -0
  22. data/app/graphql/tasker/graph_ql_types/base_field.rb +10 -0
  23. data/app/graphql/tasker/graph_ql_types/base_input_object.rb +10 -0
  24. data/app/graphql/tasker/graph_ql_types/base_interface.rb +14 -0
  25. data/app/graphql/tasker/graph_ql_types/base_object.rb +10 -0
  26. data/app/graphql/tasker/graph_ql_types/base_scalar.rb +9 -0
  27. data/app/graphql/tasker/graph_ql_types/base_union.rb +11 -0
  28. data/app/graphql/tasker/graph_ql_types/dependent_system_object_map_type.rb +18 -0
  29. data/app/graphql/tasker/graph_ql_types/dependent_system_type.rb +13 -0
  30. data/app/graphql/tasker/graph_ql_types/mutation_type.rb +16 -0
  31. data/app/graphql/tasker/graph_ql_types/named_step_type.rb +16 -0
  32. data/app/graphql/tasker/graph_ql_types/named_task_type.rb +14 -0
  33. data/app/graphql/tasker/graph_ql_types/named_tasks_named_step_type.rb +19 -0
  34. data/app/graphql/tasker/graph_ql_types/node_type.rb +12 -0
  35. data/app/graphql/tasker/graph_ql_types/query_type.rb +20 -0
  36. data/app/graphql/tasker/graph_ql_types/task_annotation_type.rb +17 -0
  37. data/app/graphql/tasker/graph_ql_types/task_interface.rb +17 -0
  38. data/app/graphql/tasker/graph_ql_types/task_type.rb +26 -0
  39. data/app/graphql/tasker/graph_ql_types/workflow_step_type.rb +154 -0
  40. data/app/graphql/tasker/graph_ql_types.rb +42 -0
  41. data/app/graphql/tasker/mutations/base_mutation.rb +13 -0
  42. data/app/graphql/tasker/mutations/cancel_step.rb +29 -0
  43. data/app/graphql/tasker/mutations/cancel_task.rb +29 -0
  44. data/app/graphql/tasker/mutations/create_task.rb +52 -0
  45. data/app/graphql/tasker/mutations/update_step.rb +36 -0
  46. data/app/graphql/tasker/mutations/update_task.rb +41 -0
  47. data/app/graphql/tasker/queries/all_annotation_types.rb +17 -0
  48. data/app/graphql/tasker/queries/all_tasks.rb +23 -0
  49. data/app/graphql/tasker/queries/base_query.rb +9 -0
  50. data/app/graphql/tasker/queries/helpers.rb +16 -0
  51. data/app/graphql/tasker/queries/one_step.rb +24 -0
  52. data/app/graphql/tasker/queries/one_task.rb +18 -0
  53. data/app/graphql/tasker/queries/tasks_by_annotation.rb +31 -0
  54. data/app/graphql/tasker/queries/tasks_by_status.rb +30 -0
  55. data/app/graphql/tasker/tasker_rails_schema.rb +52 -0
  56. data/app/jobs/tasker/application_job.rb +8 -0
  57. data/app/jobs/tasker/metrics_export_job.rb +252 -0
  58. data/app/jobs/tasker/task_runner_job.rb +224 -0
  59. data/app/models/tasker/annotation_type.rb +26 -0
  60. data/app/models/tasker/application_record.rb +70 -0
  61. data/app/models/tasker/dependent_system.rb +26 -0
  62. data/app/models/tasker/dependent_system_object_map.rb +64 -0
  63. data/app/models/tasker/diagram/edge.rb +106 -0
  64. data/app/models/tasker/diagram/flowchart.rb +137 -0
  65. data/app/models/tasker/diagram/node.rb +99 -0
  66. data/app/models/tasker/named_step.rb +41 -0
  67. data/app/models/tasker/named_task.rb +121 -0
  68. data/app/models/tasker/named_tasks_named_step.rb +82 -0
  69. data/app/models/tasker/step_dag_relationship.rb +65 -0
  70. data/app/models/tasker/step_readiness_status.rb +59 -0
  71. data/app/models/tasker/task.rb +424 -0
  72. data/app/models/tasker/task_annotation.rb +36 -0
  73. data/app/models/tasker/task_diagram.rb +332 -0
  74. data/app/models/tasker/task_execution_context.rb +29 -0
  75. data/app/models/tasker/task_namespace.rb +41 -0
  76. data/app/models/tasker/task_transition.rb +235 -0
  77. data/app/models/tasker/workflow_step.rb +461 -0
  78. data/app/models/tasker/workflow_step_edge.rb +94 -0
  79. data/app/models/tasker/workflow_step_transition.rb +434 -0
  80. data/app/serializers/tasker/annotation_type_serializer.rb +8 -0
  81. data/app/serializers/tasker/handler_serializer.rb +109 -0
  82. data/app/serializers/tasker/task_annotation_serializer.rb +32 -0
  83. data/app/serializers/tasker/task_serializer.rb +168 -0
  84. data/app/serializers/tasker/workflow_step_serializer.rb +27 -0
  85. data/app/services/tasker/analytics_service.rb +409 -0
  86. data/app/views/tasker/task/_diagram.html.erb +32 -0
  87. data/config/initializers/dry_struct.rb +11 -0
  88. data/config/initializers/statesman.rb +6 -0
  89. data/config/initializers/tasker_orchestration.rb +17 -0
  90. data/config/initializers/time_formats.rb +4 -0
  91. data/config/routes.rb +34 -0
  92. data/config/tasker/subscriptions/example_integrations.yml +67 -0
  93. data/config/tasker/system_events.yml +305 -0
  94. data/db/functions/calculate_dependency_levels_v01.sql +45 -0
  95. data/db/functions/get_analytics_metrics_v01.sql +137 -0
  96. data/db/functions/get_slowest_steps_v01.sql +82 -0
  97. data/db/functions/get_slowest_tasks_v01.sql +96 -0
  98. data/db/functions/get_step_readiness_status_batch_v01.sql +140 -0
  99. data/db/functions/get_step_readiness_status_v01.sql +139 -0
  100. data/db/functions/get_system_health_counts_v01.sql +108 -0
  101. data/db/functions/get_task_execution_context_v01.sql +108 -0
  102. data/db/functions/get_task_execution_contexts_batch_v01.sql +104 -0
  103. data/db/init/schema.sql +2277 -0
  104. data/db/migrate/20250701165431_initial_tasker_schema.rb +116 -0
  105. data/db/views/tasker_step_dag_relationships_v01.sql +69 -0
  106. data/docs/APPLICATION_GENERATOR.md +384 -0
  107. data/docs/AUTH.md +1780 -0
  108. data/docs/CIRCUIT_BREAKER.md +224 -0
  109. data/docs/DEVELOPER_GUIDE.md +2665 -0
  110. data/docs/EVENT_SYSTEM.md +637 -0
  111. data/docs/EXECUTION_CONFIGURATION.md +341 -0
  112. data/docs/FLOW_CHART.md +149 -0
  113. data/docs/HEALTH.md +542 -0
  114. data/docs/METRICS.md +731 -0
  115. data/docs/OPTIMIZATION_PLAN.md +1479 -0
  116. data/docs/OVERVIEW.md +552 -0
  117. data/docs/QUICK_START.md +270 -0
  118. data/docs/REGISTRY_SYSTEMS.md +373 -0
  119. data/docs/REST_API.md +632 -0
  120. data/docs/ROADMAP.md +221 -0
  121. data/docs/SQL_FUNCTIONS.md +1408 -0
  122. data/docs/TASK_DIAGRAM.md +252 -0
  123. data/docs/TASK_EXECUTION_CONTROL_FLOW.md +237 -0
  124. data/docs/TELEMETRY.md +795 -0
  125. data/docs/TROUBLESHOOTING.md +756 -0
  126. data/docs/TaskHandlerGenerator.html +255 -0
  127. data/docs/Tasker/Analysis/RuntimeGraphAnalyzer.html +907 -0
  128. data/docs/Tasker/Analysis/TemplateGraphAnalyzer.html +1236 -0
  129. data/docs/Tasker/Analysis.html +117 -0
  130. data/docs/Tasker/AnalyticsController.html +450 -0
  131. data/docs/Tasker/AnalyticsService/BottleneckAnalytics.html +816 -0
  132. data/docs/Tasker/AnalyticsService/PerformanceAnalytics.html +586 -0
  133. data/docs/Tasker/AnalyticsService.html +2221 -0
  134. data/docs/Tasker/AnnotationType.html +137 -0
  135. data/docs/Tasker/AnnotationTypeSerializer.html +124 -0
  136. data/docs/Tasker/ApplicationController.html +147 -0
  137. data/docs/Tasker/ApplicationJob.html +128 -0
  138. data/docs/Tasker/ApplicationRecord.html +378 -0
  139. data/docs/Tasker/Authentication/AuthenticationError.html +124 -0
  140. data/docs/Tasker/Authentication/ConfigurationError.html +124 -0
  141. data/docs/Tasker/Authentication/Coordinator.html +242 -0
  142. data/docs/Tasker/Authentication/Interface.html +560 -0
  143. data/docs/Tasker/Authentication/InterfaceError.html +124 -0
  144. data/docs/Tasker/Authentication/NoneAuthenticator.html +338 -0
  145. data/docs/Tasker/Authentication.html +119 -0
  146. data/docs/Tasker/Authorization/AuthorizationError.html +139 -0
  147. data/docs/Tasker/Authorization/BaseCoordinator.html +927 -0
  148. data/docs/Tasker/Authorization/ConfigurationError.html +153 -0
  149. data/docs/Tasker/Authorization/ResourceConstants/ACTIONS.html +428 -0
  150. data/docs/Tasker/Authorization/ResourceConstants/RESOURCES.html +365 -0
  151. data/docs/Tasker/Authorization/ResourceConstants.html +146 -0
  152. data/docs/Tasker/Authorization/ResourceRegistry.html +882 -0
  153. data/docs/Tasker/Authorization/UnauthorizedError.html +153 -0
  154. data/docs/Tasker/Authorization.html +582 -0
  155. data/docs/Tasker/CacheCapabilities.html +167 -0
  156. data/docs/Tasker/CacheStrategy.html +1297 -0
  157. data/docs/Tasker/Concerns/Authenticatable.html +116 -0
  158. data/docs/Tasker/Concerns/Authorizable/AdminStatusChecker.html +256 -0
  159. data/docs/Tasker/Concerns/Authorizable.html +816 -0
  160. data/docs/Tasker/Concerns/ControllerAuthorizable.html +157 -0
  161. data/docs/Tasker/Concerns/EventPublisher.html +4023 -0
  162. data/docs/Tasker/Concerns/IdempotentStateTransitions.html +806 -0
  163. data/docs/Tasker/Concerns/LifecycleEventHelpers.html +129 -0
  164. data/docs/Tasker/Concerns/OrchestrationPublisher.html +129 -0
  165. data/docs/Tasker/Concerns/StateMachineBase/ClassMethods.html +1075 -0
  166. data/docs/Tasker/Concerns/StateMachineBase/StateMachineBase/ClassMethods.html +191 -0
  167. data/docs/Tasker/Concerns/StateMachineBase/StateMachineBase.html +126 -0
  168. data/docs/Tasker/Concerns/StateMachineBase.html +153 -0
  169. data/docs/Tasker/Concerns/StructuredLogging.html +1413 -0
  170. data/docs/Tasker/Concerns.html +117 -0
  171. data/docs/Tasker/Configuration/AuthConfiguration.html +1023 -0
  172. data/docs/Tasker/Configuration/ConfigurationProxy.html +581 -0
  173. data/docs/Tasker/Configuration/DatabaseConfiguration.html +475 -0
  174. data/docs/Tasker/Configuration/EngineConfiguration.html +1265 -0
  175. data/docs/Tasker/Configuration/HealthConfiguration.html +791 -0
  176. data/docs/Tasker/Configuration/TelemetryConfiguration.html +1308 -0
  177. data/docs/Tasker/Configuration/TelemetryConfigurationProxy.html +388 -0
  178. data/docs/Tasker/Configuration.html +1669 -0
  179. data/docs/Tasker/ConfigurationError.html +143 -0
  180. data/docs/Tasker/ConfiguredTask.html +514 -0
  181. data/docs/Tasker/Constants/EventDefinitions.html +590 -0
  182. data/docs/Tasker/Constants/LifecycleEvents.html +137 -0
  183. data/docs/Tasker/Constants/ObservabilityEvents/Step.html +152 -0
  184. data/docs/Tasker/Constants/ObservabilityEvents/Task.html +142 -0
  185. data/docs/Tasker/Constants/ObservabilityEvents.html +126 -0
  186. data/docs/Tasker/Constants/RegistryEvents.html +285 -0
  187. data/docs/Tasker/Constants/StepEvents.html +177 -0
  188. data/docs/Tasker/Constants/TaskEvents.html +167 -0
  189. data/docs/Tasker/Constants/TaskExecution/ExecutionStatus.html +207 -0
  190. data/docs/Tasker/Constants/TaskExecution/HealthStatus.html +191 -0
  191. data/docs/Tasker/Constants/TaskExecution/RecommendedAction.html +207 -0
  192. data/docs/Tasker/Constants/TaskExecution.html +126 -0
  193. data/docs/Tasker/Constants/TaskFinalization/ErrorMessages.html +132 -0
  194. data/docs/Tasker/Constants/TaskFinalization/PendingReasons.html +207 -0
  195. data/docs/Tasker/Constants/TaskFinalization/ReenqueueReasons.html +239 -0
  196. data/docs/Tasker/Constants/TaskFinalization.html +126 -0
  197. data/docs/Tasker/Constants/TaskStatuses.html +223 -0
  198. data/docs/Tasker/Constants/TestEvents.html +163 -0
  199. data/docs/Tasker/Constants/WorkflowEvents.html +222 -0
  200. data/docs/Tasker/Constants/WorkflowStepStatuses.html +223 -0
  201. data/docs/Tasker/Constants.html +561 -0
  202. data/docs/Tasker/DependentSystem.html +137 -0
  203. data/docs/Tasker/DependentSystemObjectMap.html +250 -0
  204. data/docs/Tasker/DetectorRegistry.html +598 -0
  205. data/docs/Tasker/Diagram/Edge.html +1191 -0
  206. data/docs/Tasker/Diagram/Flowchart.html +1539 -0
  207. data/docs/Tasker/Diagram/Node.html +1165 -0
  208. data/docs/Tasker/Diagram.html +117 -0
  209. data/docs/Tasker/Engine.html +215 -0
  210. data/docs/Tasker/Error.html +139 -0
  211. data/docs/Tasker/Events/Bus.html +1226 -0
  212. data/docs/Tasker/Events/Catalog/CatalogPrinter.html +258 -0
  213. data/docs/Tasker/Events/Catalog/CustomEventRegistrar.html +276 -0
  214. data/docs/Tasker/Events/Catalog/ExamplePayloadGenerator.html +294 -0
  215. data/docs/Tasker/Events/Catalog.html +1291 -0
  216. data/docs/Tasker/Events/CustomRegistry.html +943 -0
  217. data/docs/Tasker/Events/DefinitionLoader.html +575 -0
  218. data/docs/Tasker/Events/EventPayloadBuilder/ErrorInfoExtractor.html +286 -0
  219. data/docs/Tasker/Events/EventPayloadBuilder/StepPayloadBuilder.html +312 -0
  220. data/docs/Tasker/Events/EventPayloadBuilder.html +664 -0
  221. data/docs/Tasker/Events/Publisher.html +365 -0
  222. data/docs/Tasker/Events/Subscribers/BaseSubscriber/ErrorCategorizer/ErrorTypeClassifier.html +1128 -0
  223. data/docs/Tasker/Events/Subscribers/BaseSubscriber/ErrorCategorizer.html +270 -0
  224. data/docs/Tasker/Events/Subscribers/BaseSubscriber/MetricTagsExtractor.html +266 -0
  225. data/docs/Tasker/Events/Subscribers/BaseSubscriber.html +2556 -0
  226. data/docs/Tasker/Events/Subscribers/MetricsSubscriber.html +723 -0
  227. data/docs/Tasker/Events/Subscribers/TelemetrySubscriber.html +2251 -0
  228. data/docs/Tasker/Events/Subscribers.html +117 -0
  229. data/docs/Tasker/Events/SubscriptionLoader.html +493 -0
  230. data/docs/Tasker/Events.html +294 -0
  231. data/docs/Tasker/EventsGenerator.html +459 -0
  232. data/docs/Tasker/Functions/FunctionBasedAnalyticsMetrics/AnalyticsMetrics.html +135 -0
  233. data/docs/Tasker/Functions/FunctionBasedAnalyticsMetrics.html +412 -0
  234. data/docs/Tasker/Functions/FunctionBasedDependencyLevels.html +598 -0
  235. data/docs/Tasker/Functions/FunctionBasedSlowestSteps/SlowestStep.html +135 -0
  236. data/docs/Tasker/Functions/FunctionBasedSlowestSteps.html +453 -0
  237. data/docs/Tasker/Functions/FunctionBasedSlowestTasks/SlowestTask.html +135 -0
  238. data/docs/Tasker/Functions/FunctionBasedSlowestTasks.html +453 -0
  239. data/docs/Tasker/Functions/FunctionBasedStepReadinessStatus.html +1457 -0
  240. data/docs/Tasker/Functions/FunctionBasedSystemHealthCounts/HealthMetrics.html +135 -0
  241. data/docs/Tasker/Functions/FunctionBasedSystemHealthCounts.html +370 -0
  242. data/docs/Tasker/Functions/FunctionBasedTaskExecutionContext.html +1250 -0
  243. data/docs/Tasker/Functions/FunctionWrapper.html +479 -0
  244. data/docs/Tasker/Functions.html +117 -0
  245. data/docs/Tasker/Generators/AuthenticatorGenerator/UsageInstructionsFormatter.html +244 -0
  246. data/docs/Tasker/Generators/AuthenticatorGenerator.html +373 -0
  247. data/docs/Tasker/Generators/AuthorizationCoordinatorGenerator.html +430 -0
  248. data/docs/Tasker/Generators/SubscriberGenerator.html +377 -0
  249. data/docs/Tasker/Generators/TaskHandlerGenerator.html +263 -0
  250. data/docs/Tasker/Generators.html +117 -0
  251. data/docs/Tasker/GraphQLTypes/AnnotationType.html +132 -0
  252. data/docs/Tasker/GraphQLTypes/BaseArgument.html +124 -0
  253. data/docs/Tasker/GraphQLTypes/BaseConnection.html +124 -0
  254. data/docs/Tasker/GraphQLTypes/BaseEdge.html +130 -0
  255. data/docs/Tasker/GraphQLTypes/BaseEnum.html +124 -0
  256. data/docs/Tasker/GraphQLTypes/BaseField.html +124 -0
  257. data/docs/Tasker/GraphQLTypes/BaseInputObject.html +124 -0
  258. data/docs/Tasker/GraphQLTypes/BaseInterface.html +116 -0
  259. data/docs/Tasker/GraphQLTypes/BaseObject.html +128 -0
  260. data/docs/Tasker/GraphQLTypes/BaseScalar.html +124 -0
  261. data/docs/Tasker/GraphQLTypes/BaseUnion.html +124 -0
  262. data/docs/Tasker/GraphQLTypes/DependentSystemObjectMapType.html +132 -0
  263. data/docs/Tasker/GraphQLTypes/DependentSystemType.html +132 -0
  264. data/docs/Tasker/GraphQLTypes/MutationType.html +132 -0
  265. data/docs/Tasker/GraphQLTypes/NamedStepType.html +132 -0
  266. data/docs/Tasker/GraphQLTypes/NamedTaskType.html +132 -0
  267. data/docs/Tasker/GraphQLTypes/NamedTasksNamedStepType.html +132 -0
  268. data/docs/Tasker/GraphQLTypes/NodeType.html +118 -0
  269. data/docs/Tasker/GraphQLTypes/QueryType.html +139 -0
  270. data/docs/Tasker/GraphQLTypes/TaskAnnotationType.html +132 -0
  271. data/docs/Tasker/GraphQLTypes/TaskInterface.html +111 -0
  272. data/docs/Tasker/GraphQLTypes/TaskType.html +201 -0
  273. data/docs/Tasker/GraphQLTypes/WorkflowStepType.html +694 -0
  274. data/docs/Tasker/GraphQLTypes.html +130 -0
  275. data/docs/Tasker/GraphqlController.html +251 -0
  276. data/docs/Tasker/HandlerFactory.html +1518 -0
  277. data/docs/Tasker/HandlerSerializer.html +682 -0
  278. data/docs/Tasker/HandlersController.html +574 -0
  279. data/docs/Tasker/HashIdentityStrategy.html +278 -0
  280. data/docs/Tasker/Health/ReadinessChecker.html +712 -0
  281. data/docs/Tasker/Health/StatusChecker.html +653 -0
  282. data/docs/Tasker/Health.html +117 -0
  283. data/docs/Tasker/HealthController.html +523 -0
  284. data/docs/Tasker/IdentityStrategy.html +276 -0
  285. data/docs/Tasker/InvalidTaskHandlerConfig.html +135 -0
  286. data/docs/Tasker/LifecycleEvents/Events/Step.html +162 -0
  287. data/docs/Tasker/LifecycleEvents/Events/Task.html +162 -0
  288. data/docs/Tasker/LifecycleEvents/Events.html +204 -0
  289. data/docs/Tasker/LifecycleEvents/Publisher.html +132 -0
  290. data/docs/Tasker/LifecycleEvents.html +799 -0
  291. data/docs/Tasker/Logging/CorrelationIdGenerator.html +688 -0
  292. data/docs/Tasker/Logging.html +115 -0
  293. data/docs/Tasker/MetricsController.html +293 -0
  294. data/docs/Tasker/MetricsExportJob.html +414 -0
  295. data/docs/Tasker/Mutations/BaseMutation.html +128 -0
  296. data/docs/Tasker/Mutations/CancelStep.html +219 -0
  297. data/docs/Tasker/Mutations/CancelTask.html +221 -0
  298. data/docs/Tasker/Mutations/CreateTask.html +243 -0
  299. data/docs/Tasker/Mutations/UpdateStep.html +243 -0
  300. data/docs/Tasker/Mutations/UpdateTask.html +243 -0
  301. data/docs/Tasker/Mutations.html +117 -0
  302. data/docs/Tasker/NamedStep.html +216 -0
  303. data/docs/Tasker/NamedTask.html +910 -0
  304. data/docs/Tasker/NamedTasksNamedStep.html +435 -0
  305. data/docs/Tasker/Orchestration/BackoffCalculator.html +404 -0
  306. data/docs/Tasker/Orchestration/ConnectionBuilder/ConfigValidator.html +258 -0
  307. data/docs/Tasker/Orchestration/ConnectionBuilder.html +435 -0
  308. data/docs/Tasker/Orchestration/ConnectionPoolIntelligence.html +513 -0
  309. data/docs/Tasker/Orchestration/Coordinator.html +641 -0
  310. data/docs/Tasker/Orchestration/FutureStateAnalyzer.html +1045 -0
  311. data/docs/Tasker/Orchestration/Orchestrator.html +679 -0
  312. data/docs/Tasker/Orchestration/PluginIntegration.html +1127 -0
  313. data/docs/Tasker/Orchestration/ResponseProcessor.html +504 -0
  314. data/docs/Tasker/Orchestration/RetryHeaderParser.html +304 -0
  315. data/docs/Tasker/Orchestration/StepExecutor.html +995 -0
  316. data/docs/Tasker/Orchestration/StepSequenceFactory.html +644 -0
  317. data/docs/Tasker/Orchestration/TaskFinalizer/BlockageChecker.html +264 -0
  318. data/docs/Tasker/Orchestration/TaskFinalizer/ContextManager.html +254 -0
  319. data/docs/Tasker/Orchestration/TaskFinalizer/DelayCalculator.html +556 -0
  320. data/docs/Tasker/Orchestration/TaskFinalizer/FinalizationDecisionMaker.html +348 -0
  321. data/docs/Tasker/Orchestration/TaskFinalizer/FinalizationProcessor.html +286 -0
  322. data/docs/Tasker/Orchestration/TaskFinalizer/ReasonDeterminer.html +432 -0
  323. data/docs/Tasker/Orchestration/TaskFinalizer/ReenqueueManager.html +296 -0
  324. data/docs/Tasker/Orchestration/TaskFinalizer/UnclearStateHandler.html +314 -0
  325. data/docs/Tasker/Orchestration/TaskFinalizer.html +1212 -0
  326. data/docs/Tasker/Orchestration/TaskInitializer.html +766 -0
  327. data/docs/Tasker/Orchestration/TaskReenqueuer.html +506 -0
  328. data/docs/Tasker/Orchestration/ViableStepDiscovery.html +442 -0
  329. data/docs/Tasker/Orchestration/WorkflowCoordinator.html +510 -0
  330. data/docs/Tasker/Orchestration.html +130 -0
  331. data/docs/Tasker/PageSort/PageSortParamsBuilder.html +296 -0
  332. data/docs/Tasker/PageSort.html +247 -0
  333. data/docs/Tasker/PermanentError.html +518 -0
  334. data/docs/Tasker/ProceduralError.html +147 -0
  335. data/docs/Tasker/Queries/AllAnnotationTypes.html +217 -0
  336. data/docs/Tasker/Queries/AllTasks.html +221 -0
  337. data/docs/Tasker/Queries/BaseQuery.html +128 -0
  338. data/docs/Tasker/Queries/Helpers.html +187 -0
  339. data/docs/Tasker/Queries/OneStep.html +225 -0
  340. data/docs/Tasker/Queries/OneTask.html +217 -0
  341. data/docs/Tasker/Queries/TasksByAnnotation.html +231 -0
  342. data/docs/Tasker/Queries/TasksByStatus.html +233 -0
  343. data/docs/Tasker/Queries.html +119 -0
  344. data/docs/Tasker/Railtie.html +124 -0
  345. data/docs/Tasker/Registry/BaseRegistry.html +1690 -0
  346. data/docs/Tasker/Registry/EventPublisher.html +667 -0
  347. data/docs/Tasker/Registry/InterfaceValidator.html +569 -0
  348. data/docs/Tasker/Registry/RegistrationError.html +132 -0
  349. data/docs/Tasker/Registry/RegistryError.html +139 -0
  350. data/docs/Tasker/Registry/StatisticsCollector.html +841 -0
  351. data/docs/Tasker/Registry/SubscriberRegistry.html +1504 -0
  352. data/docs/Tasker/Registry/ValidationError.html +132 -0
  353. data/docs/Tasker/Registry.html +119 -0
  354. data/docs/Tasker/RetryableError.html +515 -0
  355. data/docs/Tasker/StateMachine/Compatibility.html +282 -0
  356. data/docs/Tasker/StateMachine/InvalidStateTransition.html +135 -0
  357. data/docs/Tasker/StateMachine/StepStateMachine/StandardizedPayloadBuilder.html +260 -0
  358. data/docs/Tasker/StateMachine/StepStateMachine.html +2215 -0
  359. data/docs/Tasker/StateMachine/TaskStateMachine.html +734 -0
  360. data/docs/Tasker/StateMachine.html +602 -0
  361. data/docs/Tasker/StepDagRelationship.html +657 -0
  362. data/docs/Tasker/StepHandler/Api/Config.html +1091 -0
  363. data/docs/Tasker/StepHandler/Api.html +884 -0
  364. data/docs/Tasker/StepHandler/AutomaticEventPublishing.html +321 -0
  365. data/docs/Tasker/StepHandler/Base.html +970 -0
  366. data/docs/Tasker/StepHandler.html +119 -0
  367. data/docs/Tasker/StepReadinessStatus.html +836 -0
  368. data/docs/Tasker/Task.html +2575 -0
  369. data/docs/Tasker/TaskAnnotation.html +137 -0
  370. data/docs/Tasker/TaskAnnotationSerializer.html +124 -0
  371. data/docs/Tasker/TaskBuilder/StepNameValidator.html +264 -0
  372. data/docs/Tasker/TaskBuilder/StepTemplateDefiner.html +264 -0
  373. data/docs/Tasker/TaskBuilder.html +764 -0
  374. data/docs/Tasker/TaskDiagram/StepToStepEdgeBuilder.html +260 -0
  375. data/docs/Tasker/TaskDiagram/TaskToRootStepEdgeBuilder.html +290 -0
  376. data/docs/Tasker/TaskDiagram.html +548 -0
  377. data/docs/Tasker/TaskDiagramsController.html +240 -0
  378. data/docs/Tasker/TaskExecutionContext.html +469 -0
  379. data/docs/Tasker/TaskHandler/ClassMethods/StepTemplateDefiner/ClassBasedEventRegistrar.html +238 -0
  380. data/docs/Tasker/TaskHandler/ClassMethods/StepTemplateDefiner/YamlEventRegistrar.html +254 -0
  381. data/docs/Tasker/TaskHandler/ClassMethods/StepTemplateDefiner.html +988 -0
  382. data/docs/Tasker/TaskHandler/ClassMethods.html +357 -0
  383. data/docs/Tasker/TaskHandler/InstanceMethods.html +1396 -0
  384. data/docs/Tasker/TaskHandler/StepGroup.html +1748 -0
  385. data/docs/Tasker/TaskHandler.html +271 -0
  386. data/docs/Tasker/TaskNamespace.html +312 -0
  387. data/docs/Tasker/TaskRunnerJob.html +406 -0
  388. data/docs/Tasker/TaskSerializer.html +474 -0
  389. data/docs/Tasker/TaskTransition.html +1517 -0
  390. data/docs/Tasker/TaskWorkflowSummary.html +988 -0
  391. data/docs/Tasker/TaskerRailsSchema/InvalidObjectTypeError.html +132 -0
  392. data/docs/Tasker/TaskerRailsSchema/TypeResolutionError.html +139 -0
  393. data/docs/Tasker/TaskerRailsSchema/UnknownInterfaceError.html +132 -0
  394. data/docs/Tasker/TaskerRailsSchema.html +384 -0
  395. data/docs/Tasker/TasksController.html +595 -0
  396. data/docs/Tasker/Telemetry/EventMapping.html +1307 -0
  397. data/docs/Tasker/Telemetry/EventRouter.html +2178 -0
  398. data/docs/Tasker/Telemetry/Events/ExportEvents.html +246 -0
  399. data/docs/Tasker/Telemetry/Events.html +115 -0
  400. data/docs/Tasker/Telemetry/ExportCoordinator/DistributedLockTimeoutError.html +135 -0
  401. data/docs/Tasker/Telemetry/ExportCoordinator.html +2137 -0
  402. data/docs/Tasker/Telemetry/IntelligentCacheManager.html +1083 -0
  403. data/docs/Tasker/Telemetry/LogBackend.html +1088 -0
  404. data/docs/Tasker/Telemetry/MetricTypes/Counter.html +1054 -0
  405. data/docs/Tasker/Telemetry/MetricTypes/Gauge.html +1270 -0
  406. data/docs/Tasker/Telemetry/MetricTypes/Histogram.html +1492 -0
  407. data/docs/Tasker/Telemetry/MetricTypes.html +153 -0
  408. data/docs/Tasker/Telemetry/MetricsBackend.html +2510 -0
  409. data/docs/Tasker/Telemetry/MetricsExportService.html +578 -0
  410. data/docs/Tasker/Telemetry/PluginRegistry.html +1774 -0
  411. data/docs/Tasker/Telemetry/Plugins/BaseExporter.html +1835 -0
  412. data/docs/Tasker/Telemetry/Plugins/CsvExporter.html +768 -0
  413. data/docs/Tasker/Telemetry/Plugins/JsonExporter.html +747 -0
  414. data/docs/Tasker/Telemetry/Plugins.html +117 -0
  415. data/docs/Tasker/Telemetry/PrometheusExporter.html +481 -0
  416. data/docs/Tasker/Telemetry/TraceBackend.html +891 -0
  417. data/docs/Tasker/Telemetry.html +130 -0
  418. data/docs/Tasker/Types/AuthConfig.html +886 -0
  419. data/docs/Tasker/Types/BackoffConfig.html +1063 -0
  420. data/docs/Tasker/Types/BaseConfig.html +227 -0
  421. data/docs/Tasker/Types/CacheConfig.html +1731 -0
  422. data/docs/Tasker/Types/DatabaseConfig.html +388 -0
  423. data/docs/Tasker/Types/DependencyGraph.html +526 -0
  424. data/docs/Tasker/Types/DependencyGraphConfig.html +753 -0
  425. data/docs/Tasker/Types/EngineConfig.html +1181 -0
  426. data/docs/Tasker/Types/ExecutionConfig.html +1963 -0
  427. data/docs/Tasker/Types/GraphEdge.html +517 -0
  428. data/docs/Tasker/Types/GraphMetadata.html +781 -0
  429. data/docs/Tasker/Types/GraphNode.html +694 -0
  430. data/docs/Tasker/Types/HealthConfig.html +784 -0
  431. data/docs/Tasker/Types/StepSequence.html +353 -0
  432. data/docs/Tasker/Types/StepTemplate.html +1193 -0
  433. data/docs/Tasker/Types/TaskRequest.html +1179 -0
  434. data/docs/Tasker/Types/TelemetryConfig.html +2746 -0
  435. data/docs/Tasker/Types.html +154 -0
  436. data/docs/Tasker/WorkflowStep/StepFinder.html +282 -0
  437. data/docs/Tasker/WorkflowStep.html +2724 -0
  438. data/docs/Tasker/WorkflowStepEdge.html +304 -0
  439. data/docs/Tasker/WorkflowStepSerializer.html +305 -0
  440. data/docs/Tasker/WorkflowStepTransition/TransitionDescriptionFormatter.html +282 -0
  441. data/docs/Tasker/WorkflowStepTransition.html +2201 -0
  442. data/docs/Tasker/WorkflowStepsController.html +462 -0
  443. data/docs/Tasker.html +452 -0
  444. data/docs/VISION.md +584 -0
  445. data/docs/WHY.md +21 -0
  446. data/docs/_index.html +2375 -0
  447. data/docs/class_list.html +54 -0
  448. data/docs/css/common.css +1 -0
  449. data/docs/css/full_list.css +58 -0
  450. data/docs/css/style.css +503 -0
  451. data/docs/events/migration_plan_outcomes.md +80 -0
  452. data/docs/file.README.html +541 -0
  453. data/docs/file_list.html +59 -0
  454. data/docs/frames.html +22 -0
  455. data/docs/index.html +541 -0
  456. data/docs/js/app.js +344 -0
  457. data/docs/js/full_list.js +242 -0
  458. data/docs/js/jquery.js +4 -0
  459. data/docs/method_list.html +9182 -0
  460. data/docs/top-level-namespace.html +110 -0
  461. data/lib/generators/tasker/authenticator_generator.rb +301 -0
  462. data/lib/generators/tasker/authorization_coordinator_generator.rb +139 -0
  463. data/lib/generators/tasker/events_generator.rb +91 -0
  464. data/lib/generators/tasker/subscriber_generator.rb +107 -0
  465. data/lib/generators/tasker/task_handler_generator.rb +138 -0
  466. data/lib/generators/tasker/templates/api_token_authenticator.rb.erb +113 -0
  467. data/lib/generators/tasker/templates/api_token_authenticator_spec.rb.erb +144 -0
  468. data/lib/generators/tasker/templates/authorization_coordinator.rb.erb +95 -0
  469. data/lib/generators/tasker/templates/authorization_coordinator_spec.rb.erb +142 -0
  470. data/lib/generators/tasker/templates/custom_authenticator.rb.erb +108 -0
  471. data/lib/generators/tasker/templates/custom_authenticator_spec.rb.erb +162 -0
  472. data/lib/generators/tasker/templates/custom_events.yml.erb +62 -0
  473. data/lib/generators/tasker/templates/custom_subscriber.rb.erb +72 -0
  474. data/lib/generators/tasker/templates/devise_authenticator.rb.erb +101 -0
  475. data/lib/generators/tasker/templates/devise_authenticator_spec.rb.erb +126 -0
  476. data/lib/generators/tasker/templates/initialize.rb.erb +202 -0
  477. data/lib/generators/tasker/templates/jwt_authenticator.rb.erb +144 -0
  478. data/lib/generators/tasker/templates/jwt_authenticator_spec.rb.erb +298 -0
  479. data/lib/generators/tasker/templates/metrics_subscriber.rb.erb +258 -0
  480. data/lib/generators/tasker/templates/metrics_subscriber_spec.rb.erb +308 -0
  481. data/lib/generators/tasker/templates/omniauth_authenticator.rb.erb +135 -0
  482. data/lib/generators/tasker/templates/omniauth_authenticator_spec.rb.erb +196 -0
  483. data/lib/generators/tasker/templates/opentelemetry_initializer.rb +52 -0
  484. data/lib/generators/tasker/templates/subscriber.rb.erb +64 -0
  485. data/lib/generators/tasker/templates/subscriber_spec.rb.erb +80 -0
  486. data/lib/generators/tasker/templates/task_config.yaml.erb +117 -0
  487. data/lib/generators/tasker/templates/task_handler.rb.erb +59 -0
  488. data/lib/generators/tasker/templates/task_handler_spec.rb.erb +159 -0
  489. data/lib/tasker/analysis/runtime_graph_analyzer.rb +1168 -0
  490. data/lib/tasker/analysis/template_graph_analyzer.rb +328 -0
  491. data/lib/tasker/authentication/coordinator.rb +78 -0
  492. data/lib/tasker/authentication/errors.rb +9 -0
  493. data/lib/tasker/authentication/interface.rb +36 -0
  494. data/lib/tasker/authentication/none_authenticator.rb +26 -0
  495. data/lib/tasker/authorization/base_coordinator.rb +112 -0
  496. data/lib/tasker/authorization/errors.rb +26 -0
  497. data/lib/tasker/authorization/resource_constants.rb +74 -0
  498. data/lib/tasker/authorization/resource_registry.rb +143 -0
  499. data/lib/tasker/authorization.rb +75 -0
  500. data/lib/tasker/cache_capabilities.rb +131 -0
  501. data/lib/tasker/cache_strategy.rb +469 -0
  502. data/lib/tasker/concerns/authenticatable.rb +41 -0
  503. data/lib/tasker/concerns/authorizable.rb +204 -0
  504. data/lib/tasker/concerns/controller_authorizable.rb +124 -0
  505. data/lib/tasker/concerns/event_publisher.rb +716 -0
  506. data/lib/tasker/concerns/idempotent_state_transitions.rb +128 -0
  507. data/lib/tasker/concerns/state_machine_base.rb +218 -0
  508. data/lib/tasker/concerns/structured_logging.rb +387 -0
  509. data/lib/tasker/configuration.rb +325 -0
  510. data/lib/tasker/constants/event_definitions.rb +147 -0
  511. data/lib/tasker/constants/registry_events.rb +54 -0
  512. data/lib/tasker/constants.rb +417 -0
  513. data/lib/tasker/engine.rb +90 -0
  514. data/lib/tasker/errors.rb +90 -0
  515. data/lib/tasker/events/catalog.rb +432 -0
  516. data/lib/tasker/events/custom_registry.rb +175 -0
  517. data/lib/tasker/events/definition_loader.rb +199 -0
  518. data/lib/tasker/events/event_payload_builder.rb +461 -0
  519. data/lib/tasker/events/publisher.rb +149 -0
  520. data/lib/tasker/events/subscribers/base_subscriber.rb +601 -0
  521. data/lib/tasker/events/subscribers/metrics_subscriber.rb +120 -0
  522. data/lib/tasker/events/subscribers/telemetry_subscriber.rb +462 -0
  523. data/lib/tasker/events/subscription_loader.rb +161 -0
  524. data/lib/tasker/events.rb +37 -0
  525. data/lib/tasker/functions/function_based_analytics_metrics.rb +103 -0
  526. data/lib/tasker/functions/function_based_dependency_levels.rb +54 -0
  527. data/lib/tasker/functions/function_based_slowest_steps.rb +84 -0
  528. data/lib/tasker/functions/function_based_slowest_tasks.rb +84 -0
  529. data/lib/tasker/functions/function_based_step_readiness_status.rb +183 -0
  530. data/lib/tasker/functions/function_based_system_health_counts.rb +94 -0
  531. data/lib/tasker/functions/function_based_task_execution_context.rb +148 -0
  532. data/lib/tasker/functions/function_wrapper.rb +42 -0
  533. data/lib/tasker/functions.rb +12 -0
  534. data/lib/tasker/handler_factory.rb +322 -0
  535. data/lib/tasker/health/readiness_checker.rb +186 -0
  536. data/lib/tasker/health/status_checker.rb +203 -0
  537. data/lib/tasker/identity_strategy.rb +38 -0
  538. data/lib/tasker/logging/correlation_id_generator.rb +120 -0
  539. data/lib/tasker/orchestration/backoff_calculator.rb +184 -0
  540. data/lib/tasker/orchestration/connection_builder.rb +122 -0
  541. data/lib/tasker/orchestration/connection_pool_intelligence.rb +177 -0
  542. data/lib/tasker/orchestration/coordinator.rb +119 -0
  543. data/lib/tasker/orchestration/future_state_analyzer.rb +137 -0
  544. data/lib/tasker/orchestration/plugin_integration.rb +124 -0
  545. data/lib/tasker/orchestration/response_processor.rb +168 -0
  546. data/lib/tasker/orchestration/retry_header_parser.rb +78 -0
  547. data/lib/tasker/orchestration/step_executor.rb +941 -0
  548. data/lib/tasker/orchestration/step_sequence_factory.rb +67 -0
  549. data/lib/tasker/orchestration/task_finalizer.rb +564 -0
  550. data/lib/tasker/orchestration/task_initializer.rb +140 -0
  551. data/lib/tasker/orchestration/task_reenqueuer.rb +71 -0
  552. data/lib/tasker/orchestration/viable_step_discovery.rb +65 -0
  553. data/lib/tasker/orchestration/workflow_coordinator.rb +294 -0
  554. data/lib/tasker/orchestration.rb +45 -0
  555. data/lib/tasker/railtie.rb +9 -0
  556. data/lib/tasker/registry/base_registry.rb +177 -0
  557. data/lib/tasker/registry/event_publisher.rb +91 -0
  558. data/lib/tasker/registry/interface_validator.rb +140 -0
  559. data/lib/tasker/registry/statistics_collector.rb +381 -0
  560. data/lib/tasker/registry/subscriber_registry.rb +285 -0
  561. data/lib/tasker/registry.rb +22 -0
  562. data/lib/tasker/state_machine/step_state_machine.rb +508 -0
  563. data/lib/tasker/state_machine/task_state_machine.rb +192 -0
  564. data/lib/tasker/state_machine.rb +83 -0
  565. data/lib/tasker/step_handler/api.rb +410 -0
  566. data/lib/tasker/step_handler/base.rb +206 -0
  567. data/lib/tasker/task_builder.rb +432 -0
  568. data/lib/tasker/task_handler/class_methods.rb +324 -0
  569. data/lib/tasker/task_handler/instance_methods.rb +293 -0
  570. data/lib/tasker/task_handler/step_group.rb +182 -0
  571. data/lib/tasker/task_handler.rb +43 -0
  572. data/lib/tasker/telemetry/event_mapping.rb +126 -0
  573. data/lib/tasker/telemetry/event_router.rb +318 -0
  574. data/lib/tasker/telemetry/events/export_events.rb +38 -0
  575. data/lib/tasker/telemetry/export_coordinator.rb +497 -0
  576. data/lib/tasker/telemetry/intelligent_cache_manager.rb +508 -0
  577. data/lib/tasker/telemetry/log_backend.rb +224 -0
  578. data/lib/tasker/telemetry/metric_types.rb +368 -0
  579. data/lib/tasker/telemetry/metrics_backend.rb +1227 -0
  580. data/lib/tasker/telemetry/metrics_export_service.rb +392 -0
  581. data/lib/tasker/telemetry/plugin_registry.rb +333 -0
  582. data/lib/tasker/telemetry/plugins/base_exporter.rb +246 -0
  583. data/lib/tasker/telemetry/plugins/csv_exporter.rb +198 -0
  584. data/lib/tasker/telemetry/plugins/json_exporter.rb +141 -0
  585. data/lib/tasker/telemetry/prometheus_exporter.rb +249 -0
  586. data/lib/tasker/telemetry/trace_backend.rb +186 -0
  587. data/lib/tasker/telemetry.rb +59 -0
  588. data/lib/tasker/types/auth_config.rb +81 -0
  589. data/lib/tasker/types/backoff_config.rb +142 -0
  590. data/lib/tasker/types/cache_config.rb +257 -0
  591. data/lib/tasker/types/database_config.rb +39 -0
  592. data/lib/tasker/types/dependency_graph.rb +225 -0
  593. data/lib/tasker/types/dependency_graph_config.rb +149 -0
  594. data/lib/tasker/types/engine_config.rb +131 -0
  595. data/lib/tasker/types/execution_config.rb +289 -0
  596. data/lib/tasker/types/health_config.rb +84 -0
  597. data/lib/tasker/types/step_sequence.rb +24 -0
  598. data/lib/tasker/types/step_template.rb +63 -0
  599. data/lib/tasker/types/task_request.rb +60 -0
  600. data/lib/tasker/types/telemetry_config.rb +273 -0
  601. data/lib/tasker/types.rb +64 -0
  602. data/lib/tasker/version.rb +7 -0
  603. data/lib/tasker.rb +82 -0
  604. data/lib/tasks/tasker_tasks.rake +302 -0
  605. metadata +958 -0
@@ -0,0 +1,1168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../functions/function_based_dependency_levels'
4
+ require_relative '../telemetry/intelligent_cache_manager'
5
+
6
+ module Tasker
7
+ module Analysis
8
+ # Runtime Graph Analyzer for Workflow Dependencies
9
+ #
10
+ # Provides comprehensive analysis of workflow step dependencies, execution flow,
11
+ # and performance bottlenecks using database-backed graph analysis. Leverages
12
+ # existing SQL functions for optimal performance and consistency.
13
+ #
14
+ # @example Basic usage
15
+ # analyzer = RuntimeGraphAnalyzer.new(task: my_task)
16
+ # analysis = analyzer.analyze
17
+ # puts analysis[:critical_paths][:longest_path_length]
18
+ #
19
+ # @example Analyzing bottlenecks
20
+ # bottlenecks = analyzer.analyze[:bottlenecks]
21
+ # critical_bottlenecks = bottlenecks[:bottlenecks].select { |b| b[:severity] == 'Critical' }
22
+ #
23
+ # @since 2.2.1
24
+ class RuntimeGraphAnalyzer
25
+ # @return [Tasker::Task] The task being analyzed
26
+ attr_reader :task
27
+
28
+ # @return [Integer] The task ID for database queries
29
+ attr_reader :task_id
30
+
31
+ # Initialize the analyzer for a specific task
32
+ #
33
+ # @param task [Tasker::Task] The task to analyze
34
+ # @raise [ArgumentError] if task is nil or invalid
35
+ def initialize(task:)
36
+ @task = task
37
+ @task_id = task.task_id
38
+ @cache = {}
39
+ @intelligent_cache = Tasker::Telemetry::IntelligentCacheManager.new
40
+ end
41
+
42
+ # Perform comprehensive workflow analysis
43
+ #
44
+ # Returns a complete analysis of the workflow including dependency graphs,
45
+ # critical paths, parallelism opportunities, error chains, and bottlenecks.
46
+ # Results are cached using IntelligentCacheManager with adaptive TTL.
47
+ #
48
+ # @return [Hash] Complete analysis with the following keys:
49
+ # - :dependency_graph [Hash] Graph structure with nodes, edges, and adjacency lists
50
+ # - :critical_paths [Hash] Critical path analysis with longest paths and bottlenecks
51
+ # - :parallelism_opportunities [Hash] Analysis of parallel execution opportunities
52
+ # - :error_chains [Hash] Error propagation analysis and recovery strategies
53
+ # - :bottlenecks [Hash] Bottleneck identification with impact scoring
54
+ #
55
+ # @example
56
+ # analysis = analyzer.analyze
57
+ # puts "Longest path: #{analysis[:critical_paths][:longest_path_length]} steps"
58
+ # puts "Critical bottlenecks: #{analysis[:bottlenecks][:critical_bottlenecks]}"
59
+ def analyze
60
+ # Use intelligent caching for expensive workflow analysis
61
+ cache_key = "tasker:analysis:runtime_graph:#{task_id}:#{task_analysis_cache_version}"
62
+
63
+ @intelligent_cache.intelligent_fetch(cache_key, base_ttl: 90.seconds) do
64
+ {
65
+ dependency_graph: build_dependency_graph,
66
+ critical_paths: analyze_critical_paths,
67
+ parallelism_opportunities: analyze_parallelism,
68
+ error_chains: analyze_error_chains,
69
+ bottlenecks: identify_bottlenecks,
70
+ generated_at: Time.current,
71
+ task_id: task_id
72
+ }
73
+ end
74
+ end
75
+
76
+ # Clear all cached analysis results
77
+ #
78
+ # Forces fresh analysis on next call to {#analyze}. Useful when task state
79
+ # has changed and you need updated analysis.
80
+ #
81
+ # @return [void]
82
+ def clear_cache!
83
+ @cache.clear
84
+
85
+ # Clear intelligent cache for this task
86
+ cache_key = "tasker:analysis:runtime_graph:#{task_id}:#{task_analysis_cache_version}"
87
+ @intelligent_cache.clear_performance_data(cache_key)
88
+ Rails.cache.delete(cache_key)
89
+ end
90
+
91
+ # Build complete dependency graph structure
92
+ #
93
+ # Constructs a comprehensive graph representation of workflow step dependencies
94
+ # using database edges and SQL-based topological sorting for optimal performance.
95
+ #
96
+ # @return [Hash] Graph structure containing:
97
+ # - :nodes [Array<Hash>] Graph nodes with id, name, and dependency level
98
+ # - :edges [Array<Hash>] Graph edges with from/to IDs and step names
99
+ # - :adjacency_list [Hash] Forward adjacency list for graph traversal
100
+ # - :reverse_adjacency_list [Hash] Reverse adjacency list for dependency analysis
101
+ # - :dependency_levels [Hash] Step ID to dependency level mapping
102
+ #
103
+ # @example
104
+ # graph = analyzer.build_dependency_graph
105
+ # root_steps = graph[:nodes].select { |n| n[:level] == 0 }
106
+ # puts "Root steps: #{root_steps.map { |s| s[:name] }}"
107
+ def build_dependency_graph
108
+ steps = load_workflow_steps
109
+ step_map = steps.index_by(&:workflow_step_id)
110
+ edges = load_workflow_edges
111
+ adjacency_lists = build_adjacency_lists(steps, edges)
112
+ dependency_levels = calculate_dependency_levels_sql
113
+
114
+ {
115
+ nodes: build_graph_nodes(steps, dependency_levels),
116
+ edges: build_graph_edges(edges, step_map),
117
+ adjacency_list: adjacency_lists[:forward],
118
+ reverse_adjacency_list: adjacency_lists[:reverse],
119
+ dependency_levels: dependency_levels
120
+ }
121
+ end
122
+
123
+ private
124
+
125
+ # Get configuration for dependency graph calculations
126
+ #
127
+ # @return [Tasker::Types::DependencyGraphConfig] Configuration for calculations
128
+ # @api private
129
+ def dependency_graph_config
130
+ @dependency_graph_config ||= Tasker.configuration.dependency_graph
131
+ end
132
+
133
+ # Generate cache version based on task state for intelligent cache invalidation
134
+ #
135
+ # @return [String] Cache version string that changes when task state changes
136
+ # @api private
137
+ def task_analysis_cache_version
138
+ # Include task updated_at and step count to invalidate when task changes
139
+ step_count = task.workflow_steps.count
140
+ task_updated = task.updated_at.to_i
141
+ "v1:#{task_updated}:#{step_count}"
142
+ end
143
+
144
+ # Load workflow steps with named step associations
145
+ #
146
+ # @return [ActiveRecord::Relation] Workflow steps with eager-loaded named steps
147
+ # @api private
148
+ def load_workflow_steps
149
+ task.workflow_steps.includes(:named_step)
150
+ end
151
+
152
+ # Load workflow edges for dependency analysis
153
+ #
154
+ # @return [ActiveRecord::Relation] Workflow step edges for this task
155
+ # @api private
156
+ def load_workflow_edges
157
+ WorkflowStepEdge.joins(:from_step, :to_step)
158
+ .where(from_step: { task_id: task_id })
159
+ .select(:from_step_id, :to_step_id)
160
+ end
161
+
162
+ # Build forward and reverse adjacency lists for graph traversal
163
+ #
164
+ # Creates both forward (parent → children) and reverse (child → parents)
165
+ # adjacency lists for efficient graph analysis in both directions.
166
+ #
167
+ # @param steps [Array] Workflow steps to initialize
168
+ # @param edges [Array] Workflow step edges to populate
169
+ # @return [Hash] Hash with :forward and :reverse adjacency lists
170
+ # @api private
171
+ def build_adjacency_lists(steps, edges)
172
+ adjacency_list = {}
173
+ reverse_adjacency_list = {}
174
+
175
+ # Initialize empty lists for all steps
176
+ steps.each do |step|
177
+ adjacency_list[step.workflow_step_id] = []
178
+ reverse_adjacency_list[step.workflow_step_id] = []
179
+ end
180
+
181
+ # Populate adjacency lists from edges
182
+ edges.each do |edge|
183
+ adjacency_list[edge.from_step_id] << edge.to_step_id
184
+ reverse_adjacency_list[edge.to_step_id] << edge.from_step_id
185
+ end
186
+
187
+ { forward: adjacency_list, reverse: reverse_adjacency_list }
188
+ end
189
+
190
+ # Build graph nodes with dependency level information
191
+ #
192
+ # @param steps [Array] Workflow steps to convert to nodes
193
+ # @param dependency_levels [Hash] Step ID to dependency level mapping
194
+ # @return [Array<Hash>] Graph nodes with id, name, and level
195
+ # @api private
196
+ def build_graph_nodes(steps, dependency_levels)
197
+ steps.map do |step|
198
+ {
199
+ id: step.workflow_step_id,
200
+ name: step.named_step.name,
201
+ level: dependency_levels[step.workflow_step_id] || 0
202
+ }
203
+ end
204
+ end
205
+
206
+ # Build graph edges with human-readable step names
207
+ #
208
+ # @param edges [Array] Raw workflow step edges
209
+ # @param step_map [Hash] Step ID to step object mapping
210
+ # @return [Array<Hash>] Graph edges with from/to IDs and names
211
+ # @api private
212
+ def build_graph_edges(edges, step_map)
213
+ edges.map do |edge|
214
+ {
215
+ from: edge.from_step_id,
216
+ to: edge.to_step_id,
217
+ from_name: step_map[edge.from_step_id]&.named_step&.name,
218
+ to_name: step_map[edge.to_step_id]&.named_step&.name
219
+ }
220
+ end
221
+ end
222
+
223
+ # Calculate dependency levels using optimized SQL function
224
+ #
225
+ # @return [Hash] Step ID to dependency level mapping
226
+ # @api private
227
+ def calculate_dependency_levels_sql
228
+ Tasker::Functions::FunctionBasedDependencyLevels.levels_hash_for_task(task_id)
229
+ end
230
+
231
+ # Analyze critical paths through the dependency graph
232
+ def analyze_critical_paths
233
+ graph = build_dependency_graph
234
+ root_nodes = find_root_nodes(graph)
235
+ leaf_nodes = find_leaf_nodes(graph)
236
+ all_paths = find_all_root_to_leaf_paths(root_nodes, leaf_nodes, graph[:adjacency_list])
237
+ critical_paths = analyze_all_paths_for_criticality(all_paths, graph)
238
+
239
+ {
240
+ total_paths: all_paths.length,
241
+ longest_path_length: critical_paths.first&.dig(:length) || 0,
242
+ paths: critical_paths.first(5), # Top 5 critical paths
243
+ root_nodes: root_nodes,
244
+ leaf_nodes: leaf_nodes
245
+ }
246
+ end
247
+
248
+ # Find root nodes (nodes with no incoming edges)
249
+ def find_root_nodes(graph)
250
+ graph[:adjacency_list].select do |node, _children|
251
+ graph[:reverse_adjacency_list][node].empty?
252
+ end.keys
253
+ end
254
+
255
+ # Find leaf nodes (nodes with no outgoing edges)
256
+ def find_leaf_nodes(graph)
257
+ graph[:adjacency_list].select do |_node, children|
258
+ children.empty?
259
+ end.keys
260
+ end
261
+
262
+ # Find all paths from root nodes to leaf nodes
263
+ def find_all_root_to_leaf_paths(root_nodes, leaf_nodes, adjacency_list)
264
+ all_paths = []
265
+ root_nodes.each do |root|
266
+ leaf_nodes.each do |leaf|
267
+ paths = find_all_paths(root, leaf, adjacency_list)
268
+ all_paths.concat(paths)
269
+ end
270
+ end
271
+ all_paths
272
+ end
273
+
274
+ # Analyze all paths for criticality and sort by importance
275
+ def analyze_all_paths_for_criticality(all_paths, graph)
276
+ all_paths.map do |path|
277
+ analyze_path_criticality(path, graph)
278
+ end.sort_by { |analysis| -analysis[:criticality_score] }
279
+ end
280
+
281
+ # Find all paths between two nodes using DFS
282
+ def find_all_paths(start_node, end_node, adjacency_list, current_path = [], visited = Set.new)
283
+ return [] if visited.include?(start_node)
284
+
285
+ current_path += [start_node]
286
+ visited += [start_node]
287
+
288
+ return [current_path] if start_node == end_node
289
+
290
+ paths = []
291
+ adjacency_list[start_node].each do |neighbor|
292
+ neighbor_paths = find_all_paths(neighbor, end_node, adjacency_list, current_path, visited)
293
+ paths.concat(neighbor_paths)
294
+ end
295
+
296
+ paths
297
+ end
298
+
299
+ # Analyze a specific path for criticality factors
300
+ def analyze_path_criticality(path, _graph)
301
+ # Get step readiness data for analysis
302
+ step_readiness_data = get_step_readiness_data
303
+ task_context = get_task_execution_context
304
+ path_steps = path.filter_map { |step_id| step_readiness_data[step_id] }
305
+
306
+ # Use efficient counting methods for path-specific metrics
307
+ path_metrics = calculate_path_metrics(path_steps)
308
+
309
+ # Calculate estimated duration and bottlenecks
310
+ estimated_duration = calculate_path_duration(path_steps)
311
+ bottlenecks = identify_path_bottlenecks(path_steps)
312
+
313
+ # Calculate overall criticality score using task context for efficiency
314
+ criticality_score = calculate_path_criticality_score(
315
+ path.length, path_metrics, estimated_duration, task_context
316
+ )
317
+
318
+ {
319
+ path: path,
320
+ length: path.length,
321
+ step_names: path_steps.map(&:name),
322
+ **path_metrics,
323
+ estimated_duration: estimated_duration,
324
+ bottlenecks: bottlenecks,
325
+ criticality_score: criticality_score,
326
+ completion_percentage: (path_metrics[:completed_steps].to_f / path.length * 100).round(1)
327
+ }
328
+ end
329
+
330
+ # Calculate metrics for a specific path efficiently
331
+ def calculate_path_metrics(path_steps)
332
+ # Use single pass through path_steps for all counts
333
+ metrics = {
334
+ completed_steps: 0,
335
+ blocked_steps: 0,
336
+ error_steps: 0,
337
+ retry_steps: 0,
338
+ ready_steps: 0
339
+ }
340
+
341
+ path_steps.each do |step|
342
+ metrics[:completed_steps] += 1 if step.current_state.in?(%w[complete resolved_manually])
343
+ metrics[:blocked_steps] += 1 unless step.dependencies_satisfied
344
+ metrics[:error_steps] += 1 if step.current_state == 'error'
345
+ metrics[:retry_steps] += 1 if step.attempts.positive? && step.current_state == 'error'
346
+ metrics[:ready_steps] += 1 if step.ready_for_execution
347
+ end
348
+
349
+ metrics
350
+ end
351
+
352
+ # Calculate criticality score using task context for efficiency
353
+ # Calculate path criticality score using configurable multipliers
354
+ #
355
+ # Uses configurable impact multipliers for flexible path scoring.
356
+ #
357
+ # @param path_length [Integer] Length of the path
358
+ # @param path_metrics [Hash] Metrics about the path steps
359
+ # @param duration [Integer] Estimated path duration
360
+ # @param task_context [Object] Task execution context
361
+ # @return [Float] Path criticality score using configurable weights
362
+ def calculate_path_criticality_score(path_length, path_metrics, duration, task_context)
363
+ config = dependency_graph_config
364
+
365
+ # Base score from path length relative to total task size
366
+ score = path_length * config.impact_multipliers[:path_length_weight]
367
+
368
+ # Factor in task-wide context for better scoring
369
+ task_completion_factor = task_context.completion_percentage / 100.0
370
+ task_health_factor = task_context.health_status == 'healthy' ? 1.0 : 1.5
371
+
372
+ # Reduce score for completed work
373
+ score -= path_metrics[:completed_steps] * config.impact_multipliers[:completed_penalty]
374
+
375
+ # Increase score for problems (weighted by task health)
376
+ score += path_metrics[:blocked_steps] * config.impact_multipliers[:blocked_penalty] * task_health_factor
377
+ score += path_metrics[:error_steps] * config.impact_multipliers[:error_penalty] * task_health_factor
378
+ score += path_metrics[:retry_steps] * config.impact_multipliers[:retry_penalty]
379
+
380
+ # Factor in estimated duration and task completion
381
+ score += (duration / 60.0) * 2 # 2 points per minute
382
+ score *= (1.0 - (task_completion_factor * 0.3)) # Reduce score for mostly complete tasks
383
+
384
+ [score, 0].max # Ensure non-negative score
385
+ end
386
+
387
+ # Get step readiness data with memoization
388
+ #
389
+ # Retrieves and caches step readiness status for all steps in the task.
390
+ # Uses SQL-based functions for optimal performance and consistency.
391
+ #
392
+ # @return [Hash] Step ID to step readiness status object mapping
393
+ # @api private
394
+ def get_step_readiness_data
395
+ @get_step_readiness_data ||= begin
396
+ data = Tasker::Functions::FunctionBasedStepReadinessStatus.for_task(task_id)
397
+ data.index_by(&:workflow_step_id)
398
+ end
399
+ end
400
+
401
+ # Get task execution context with memoization
402
+ #
403
+ # Retrieves and caches task-wide execution metrics including completion
404
+ # percentages, step counts, and health status.
405
+ #
406
+ # @return [Object] Task execution context with metrics and status
407
+ # @api private
408
+ def get_task_execution_context
409
+ @get_task_execution_context ||= Tasker::Functions::FunctionBasedTaskExecutionContext.find(task_id)
410
+ end
411
+
412
+ # Calculate estimated duration for a path using configurable estimates
413
+ #
414
+ # Uses configurable duration constants for flexible time estimation.
415
+ #
416
+ # @param path_steps [Array] Steps in the path
417
+ # @return [Integer] Estimated duration in seconds using configurable estimates
418
+ def calculate_path_duration(path_steps)
419
+ config = dependency_graph_config
420
+
421
+ # Base estimation: configurable seconds per step
422
+ base_duration = path_steps.length * config.duration_estimates[:base_step_seconds]
423
+
424
+ # Calculate penalties in a single pass
425
+ error_penalty = 0
426
+ retry_penalty = 0
427
+
428
+ path_steps.each do |step|
429
+ error_penalty += config.duration_estimates[:error_penalty_seconds] if step.current_state == 'error'
430
+ retry_penalty += step.attempts * config.duration_estimates[:retry_penalty_seconds]
431
+ end
432
+
433
+ base_duration + error_penalty + retry_penalty
434
+ end
435
+
436
+ # Identify bottlenecks within a specific path
437
+ def identify_path_bottlenecks(path_steps)
438
+ bottlenecks = []
439
+
440
+ path_steps.each do |step|
441
+ # A step is a bottleneck if it's blocking and has high retry count or long backoff
442
+ next unless !step.dependencies_satisfied || step.current_state == 'error'
443
+
444
+ severity = calculate_bottleneck_severity(step)
445
+ bottlenecks << {
446
+ step_id: step.workflow_step_id,
447
+ step_name: step.name,
448
+ reason: determine_bottleneck_reason(step),
449
+ severity: severity,
450
+ impact: "Blocks #{path_steps.length - path_steps.index(step) - 1} downstream steps"
451
+ }
452
+ end
453
+
454
+ bottlenecks.sort_by { |b| -b[:severity] }
455
+ end
456
+
457
+ # Calculate bottleneck severity score
458
+ def calculate_bottleneck_severity(step)
459
+ severity = 0
460
+ severity += 10 if step.current_state == 'error'
461
+ severity += step.attempts * 5 # More attempts = higher severity
462
+ severity += 20 if step.attempts >= (step.retry_limit || 3) # Exhausted retries
463
+ severity += 5 unless step.dependencies_satisfied # Dependency issues
464
+ severity
465
+ end
466
+
467
+ # Determine the reason for a bottleneck using step readiness status
468
+ #
469
+ # Leverages the existing step readiness infrastructure to provide consistent
470
+ # and accurate bottleneck reason identification. Maps technical blocking
471
+ # reasons to user-friendly descriptions.
472
+ #
473
+ # @param step [Object] Step readiness status object
474
+ # @return [String] User-friendly description of why the step is a bottleneck
475
+ #
476
+ # @example
477
+ # reason = determine_bottleneck_reason(step)
478
+ # puts "Step blocked: #{reason}" # "Dependencies not satisfied"
479
+ #
480
+ # @api private
481
+ def determine_bottleneck_reason(step)
482
+ # Use the existing blocking_reason from step readiness status
483
+ blocking_reason = step.blocking_reason
484
+
485
+ # Map the technical blocking reasons to user-friendly descriptions
486
+ case blocking_reason
487
+ when 'dependencies_not_satisfied'
488
+ 'Dependencies not satisfied'
489
+ when 'retry_not_eligible'
490
+ if step.attempts >= (step.retry_limit || 3)
491
+ 'Exhausted retries'
492
+ else
493
+ 'In backoff period'
494
+ end
495
+ when 'invalid_state'
496
+ case step.current_state
497
+ when 'error'
498
+ 'In error state'
499
+ else
500
+ "Invalid state: #{step.current_state}"
501
+ end
502
+ when 'unknown'
503
+ 'Unknown blocking condition'
504
+ when nil
505
+ # Step is ready for execution, shouldn't be a bottleneck
506
+ 'Not blocked (ready for execution)'
507
+ else
508
+ "Unrecognized blocking reason: #{blocking_reason}"
509
+ end
510
+ end
511
+
512
+ # Analyze parallelism opportunities in the workflow
513
+ def analyze_parallelism
514
+ graph = build_dependency_graph
515
+ dependency_levels = graph[:dependency_levels]
516
+ step_readiness_data = get_step_readiness_data
517
+ task_context = get_task_execution_context
518
+
519
+ # Group steps by dependency level
520
+ levels_groups = dependency_levels.group_by { |_step_id, level| level }
521
+
522
+ parallelism_analysis = levels_groups.map do |level, step_level_pairs|
523
+ step_ids = step_level_pairs.map(&:first)
524
+ step_data = step_ids.filter_map { |id| step_readiness_data[id] }
525
+
526
+ analyze_level_parallelism(level, step_data)
527
+ end.sort_by { |analysis| analysis[:level] }
528
+
529
+ {
530
+ # Use task context for overall counts (more efficient)
531
+ total_steps: task_context.total_steps,
532
+ total_levels: levels_groups.keys.max + 1,
533
+ max_parallel_steps: parallelism_analysis.pluck(:total_steps).max || 0,
534
+ current_parallel_opportunities: task_context.ready_steps,
535
+ blocked_parallel_opportunities: parallelism_analysis.sum { |a| a[:blocked_steps] },
536
+ overall_completion: task_context.completion_percentage,
537
+ levels: parallelism_analysis,
538
+ parallelism_efficiency: calculate_parallelism_efficiency(parallelism_analysis)
539
+ }
540
+ end
541
+
542
+ # Analyze parallelism opportunities at a specific dependency level
543
+ def analyze_level_parallelism(level, step_data)
544
+ # For level-specific analysis, we still need to count within this level
545
+ # But we can optimize by using the step data directly
546
+ ready_steps = step_data.count(&:ready_for_execution)
547
+ blocked_steps = step_data.count { |s| !s.dependencies_satisfied }
548
+ error_steps = step_data.count { |s| s.current_state == 'error' }
549
+ completed_steps = step_data.count { |s| s.current_state.in?(%w[complete resolved_manually]) }
550
+
551
+ {
552
+ level: level,
553
+ total_steps: step_data.length,
554
+ ready_steps: ready_steps,
555
+ blocked_steps: blocked_steps,
556
+ error_steps: error_steps,
557
+ completed_steps: completed_steps,
558
+ step_names: step_data.map(&:name),
559
+ parallelism_potential: if ready_steps > 1
560
+ 'High'
561
+ else
562
+ ready_steps == 1 ? 'Medium' : 'Low'
563
+ end,
564
+ bottleneck_risk: blocked_steps > (step_data.length / 2) ? 'High' : 'Low'
565
+ }
566
+ end
567
+
568
+ # Calculate overall parallelism efficiency
569
+ def calculate_parallelism_efficiency(parallelism_analysis)
570
+ total_steps = parallelism_analysis.sum { |a| a[:total_steps] }
571
+ return 0 if total_steps.zero?
572
+
573
+ # Efficiency is based on how well steps are distributed across levels
574
+ # and how many can run in parallel
575
+ parallel_opportunities = parallelism_analysis.count { |a| a[:ready_steps] > 1 }
576
+ total_levels = parallelism_analysis.length
577
+
578
+ return 0 if total_levels.zero?
579
+
580
+ (parallel_opportunities.to_f / total_levels * 100).round(1)
581
+ end
582
+
583
+ # Analyze error propagation chains in the workflow
584
+ def analyze_error_chains
585
+ graph = build_dependency_graph
586
+ step_readiness_data = get_step_readiness_data
587
+ task_context = get_task_execution_context
588
+
589
+ # Use task context for efficient error step count
590
+ return empty_error_analysis if task_context.failed_steps.zero?
591
+
592
+ # Find all steps currently in error state
593
+ error_steps = step_readiness_data.values.select { |s| s.current_state == 'error' }
594
+
595
+ error_chains = error_steps.map do |error_step|
596
+ analyze_error_impact_chain(error_step, graph, step_readiness_data)
597
+ end.sort_by { |chain| -chain[:total_impact] }
598
+
599
+ {
600
+ # Use task context for overall metrics (more efficient)
601
+ total_error_steps: task_context.failed_steps,
602
+ total_steps: task_context.total_steps,
603
+ error_percentage: (task_context.failed_steps.to_f / task_context.total_steps * 100).round(1),
604
+ total_blocked_by_errors: error_chains.sum { |c| c[:blocked_downstream_steps] },
605
+ most_critical_error: error_chains.first,
606
+ error_chains: error_chains,
607
+ recovery_priority: determine_recovery_priority(error_chains),
608
+ task_health: task_context.health_status
609
+ }
610
+ end
611
+
612
+ # Return empty error analysis when no errors exist
613
+ def empty_error_analysis
614
+ {
615
+ total_error_steps: 0,
616
+ total_steps: get_task_execution_context.total_steps,
617
+ error_percentage: 0.0,
618
+ total_blocked_by_errors: 0,
619
+ most_critical_error: nil,
620
+ error_chains: [],
621
+ recovery_priority: [],
622
+ task_health: get_task_execution_context.health_status
623
+ }
624
+ end
625
+
626
+ # Analyze the impact chain of a specific error step
627
+ def analyze_error_impact_chain(error_step, graph, step_readiness_data)
628
+ # Find all downstream steps affected by this error
629
+ downstream_steps = find_downstream_steps(error_step.workflow_step_id, graph[:adjacency_list])
630
+ blocked_steps = downstream_steps.select do |step_id|
631
+ step_data = step_readiness_data[step_id]
632
+ step_data && !step_data.dependencies_satisfied
633
+ end
634
+
635
+ # Calculate impact metrics
636
+ total_downstream = downstream_steps.length
637
+ blocked_downstream = blocked_steps.length
638
+
639
+ # Analyze retry situation
640
+ retry_analysis = analyze_error_retry_situation(error_step)
641
+
642
+ {
643
+ error_step_id: error_step.workflow_step_id,
644
+ error_step_name: error_step.name,
645
+ error_attempts: error_step.attempts,
646
+ retry_limit: error_step.retry_limit || 3,
647
+ retry_analysis: retry_analysis,
648
+ downstream_steps: downstream_steps,
649
+ blocked_downstream_steps: blocked_downstream,
650
+ total_downstream_steps: total_downstream,
651
+ total_impact: calculate_error_impact_score(error_step, blocked_downstream, total_downstream),
652
+ recovery_strategies: suggest_recovery_strategies(error_step, retry_analysis)
653
+ }
654
+ end
655
+
656
+ # Find all downstream steps from a given step
657
+ def find_downstream_steps(step_id, adjacency_list, visited = Set.new)
658
+ return [] if visited.include?(step_id)
659
+
660
+ visited.add(step_id)
661
+ downstream = []
662
+
663
+ adjacency_list[step_id].each do |child_id|
664
+ downstream << child_id
665
+ downstream.concat(find_downstream_steps(child_id, adjacency_list, visited))
666
+ end
667
+
668
+ downstream.uniq
669
+ end
670
+
671
+ # Analyze the retry situation for an error step
672
+ def analyze_error_retry_situation(error_step)
673
+ retry_limit = error_step.retry_limit || 3
674
+ attempts = error_step.attempts
675
+
676
+ {
677
+ exhausted: attempts >= retry_limit,
678
+ remaining_attempts: [retry_limit - attempts, 0].max,
679
+ in_backoff: error_step.next_retry_at && error_step.next_retry_at > Time.current,
680
+ next_retry_at: error_step.next_retry_at,
681
+ retryable: error_step.retry_eligible
682
+ }
683
+ end
684
+
685
+ # Calculate impact score for an error using configurable weights
686
+ #
687
+ # Uses configurable impact multipliers for flexible error impact assessment.
688
+ #
689
+ # @param error_step [Object] Error step object
690
+ # @param blocked_downstream [Integer] Number of blocked downstream steps
691
+ # @param total_downstream [Integer] Total downstream steps
692
+ # @return [Integer] Error impact score using configurable weights
693
+ def calculate_error_impact_score(error_step, blocked_downstream, total_downstream)
694
+ config = dependency_graph_config
695
+ score = 0
696
+
697
+ # Base impact from blocked downstream steps
698
+ score += blocked_downstream * config.impact_multipliers[:blocked_weight]
699
+
700
+ # Additional impact for total downstream reach
701
+ score += total_downstream * config.impact_multipliers[:downstream_weight]
702
+
703
+ # Penalty for exhausted retries (permanent blockage)
704
+ score += config.penalty_constants[:exhausted_retry] if error_step.attempts >= (error_step.retry_limit || 3)
705
+
706
+ # Penalty for high retry count (instability)
707
+ score += error_step.attempts * config.penalty_constants[:retry_instability]
708
+
709
+ score
710
+ end
711
+
712
+ # Suggest recovery strategies for an error step
713
+ def suggest_recovery_strategies(_error_step, retry_analysis)
714
+ if retry_analysis[:exhausted]
715
+ exhausted_retry_strategies
716
+ elsif retry_analysis[:in_backoff]
717
+ backoff_strategies(retry_analysis)
718
+ elsif retry_analysis[:retryable]
719
+ retryable_strategies(retry_analysis)
720
+ else
721
+ non_retryable_strategies
722
+ end
723
+ end
724
+
725
+ # Recovery strategies for steps that have exhausted retries
726
+ #
727
+ # @return [Array<Hash>] Strategy recommendations with priority and description
728
+ # @api private
729
+ def exhausted_retry_strategies
730
+ [
731
+ {
732
+ strategy: 'Manual Resolution',
733
+ priority: 'High',
734
+ description: 'Step has exhausted retries and requires manual intervention'
735
+ },
736
+ {
737
+ strategy: 'Increase Retry Limit',
738
+ priority: 'Medium',
739
+ description: 'Consider increasing retry limit if error is transient'
740
+ }
741
+ ]
742
+ end
743
+
744
+ # Recovery strategies for steps in backoff period
745
+ #
746
+ # @param retry_analysis [Hash] Retry analysis data including next_retry_at
747
+ # @return [Array<Hash>] Strategy recommendations with timing information
748
+ # @api private
749
+ def backoff_strategies(retry_analysis)
750
+ [
751
+ {
752
+ strategy: 'Wait for Backoff',
753
+ priority: 'Low',
754
+ description: "Step will retry automatically at #{retry_analysis[:next_retry_at]}"
755
+ }
756
+ ]
757
+ end
758
+
759
+ # Recovery strategies for retryable steps
760
+ #
761
+ # @param retry_analysis [Hash] Retry analysis data including remaining_attempts
762
+ # @return [Array<Hash>] Strategy recommendations with attempt information
763
+ # @api private
764
+ def retryable_strategies(retry_analysis)
765
+ [
766
+ {
767
+ strategy: 'Monitor Retries',
768
+ priority: 'Medium',
769
+ description: "Step has #{retry_analysis[:remaining_attempts]} retry attempts remaining"
770
+ }
771
+ ]
772
+ end
773
+
774
+ # Recovery strategies for non-retryable steps
775
+ #
776
+ # @return [Array<Hash>] Strategy recommendations for investigation
777
+ # @api private
778
+ def non_retryable_strategies
779
+ [
780
+ {
781
+ strategy: 'Investigate Root Cause',
782
+ priority: 'High',
783
+ description: 'Step is not retryable, requires investigation'
784
+ }
785
+ ]
786
+ end
787
+
788
+ # Determine recovery priority across all error chains
789
+ def determine_recovery_priority(error_chains)
790
+ return [] if error_chains.empty?
791
+
792
+ # Sort by impact score and return top priorities
793
+ error_chains.first(3).map do |chain|
794
+ {
795
+ step_name: chain[:error_step_name],
796
+ priority_level: determine_priority_level(chain[:total_impact]),
797
+ reason: "Blocking #{chain[:blocked_downstream_steps]} downstream steps",
798
+ recommended_action: chain[:recovery_strategies].first&.dig(:strategy) || 'Investigate'
799
+ }
800
+ end
801
+ end
802
+
803
+ # Determine priority level based on impact score
804
+ #
805
+ # Uses configurable thresholds for consistent severity classification.
806
+ #
807
+ # @param impact_score [Integer] The calculated impact score
808
+ # @return [String] Priority level: 'Critical', 'High', 'Medium', or 'Low'
809
+ def determine_priority_level(impact_score)
810
+ config = dependency_graph_config
811
+
812
+ return 'Critical' if impact_score >= config.severity_thresholds[:critical]
813
+ return 'High' if impact_score >= config.severity_thresholds[:high]
814
+ return 'Medium' if impact_score >= config.severity_thresholds[:medium]
815
+
816
+ 'Low'
817
+ end
818
+
819
+ # Identify bottlenecks with impact scoring
820
+ def identify_bottlenecks
821
+ graph = build_dependency_graph
822
+ step_readiness_data = get_step_readiness_data
823
+ task_context = get_task_execution_context
824
+
825
+ # Early return if no blocking issues (use task context for efficiency)
826
+ return empty_bottleneck_analysis if task_context.failed_steps.zero? &&
827
+ task_context.pending_steps.zero? &&
828
+ task_context.ready_steps == task_context.total_steps
829
+
830
+ # Find potential bottleneck steps more efficiently
831
+ bottleneck_candidates = find_bottleneck_candidates(step_readiness_data, task_context)
832
+
833
+ # Analyze each bottleneck candidate
834
+ bottleneck_analysis = bottleneck_candidates.map do |step|
835
+ analyze_bottleneck_impact(step, graph, step_readiness_data)
836
+ end.sort_by { |analysis| -analysis[:impact_score] }
837
+
838
+ {
839
+ total_bottlenecks: bottleneck_analysis.length,
840
+ critical_bottlenecks: bottleneck_analysis.count { |b| b[:severity] == 'Critical' },
841
+ total_blocked_steps: bottleneck_analysis.sum { |b| b[:blocked_downstream_count] },
842
+ bottlenecks: bottleneck_analysis.first(10), # Top 10 bottlenecks
843
+ task_health: task_context.health_status,
844
+ task_completion: task_context.completion_percentage,
845
+ failed_steps_ratio: (task_context.failed_steps.to_f / task_context.total_steps * 100).round(1),
846
+ overall_impact: calculate_overall_bottleneck_impact(bottleneck_analysis, task_context)
847
+ }
848
+ end
849
+
850
+ # Find bottleneck candidates efficiently using task context
851
+ def find_bottleneck_candidates(step_readiness_data, task_context)
852
+ # If we have many failed steps, focus on those first
853
+ if task_context.failed_steps.positive?
854
+ candidates = step_readiness_data.values.select { |step| step.current_state == 'error' }
855
+ return candidates unless candidates.empty?
856
+ end
857
+
858
+ # Otherwise, look for other blocking conditions
859
+ step_readiness_data.values.select do |step|
860
+ !step.dependencies_satisfied ||
861
+ step.attempts >= 2 ||
862
+ !step.retry_eligible
863
+ end
864
+ end
865
+
866
+ # Return empty bottleneck analysis when no bottlenecks exist
867
+ def empty_bottleneck_analysis
868
+ task_context = get_task_execution_context
869
+ {
870
+ total_bottlenecks: 0,
871
+ critical_bottlenecks: 0,
872
+ total_blocked_steps: 0,
873
+ bottlenecks: [],
874
+ task_health: task_context.health_status,
875
+ overall_impact: 'None'
876
+ }
877
+ end
878
+
879
+ # Analyze the comprehensive impact of a specific bottleneck step
880
+ #
881
+ # Performs detailed analysis of how a bottleneck step affects the overall
882
+ # workflow, including downstream impact, severity assessment, and resolution
883
+ # recommendations.
884
+ #
885
+ # @param step [Object] Step readiness status object
886
+ # @param graph [Hash] Dependency graph structure
887
+ # @param step_readiness_data [Hash] Step ID to readiness status mapping
888
+ # @return [Hash] Complete bottleneck analysis including:
889
+ # - Step metadata (ID, name, state, attempts, etc.)
890
+ # - Downstream impact counts
891
+ # - Impact score and severity level
892
+ # - Bottleneck type classification
893
+ # - Resolution strategy and time estimate
894
+ #
895
+ # @api private
896
+ def analyze_bottleneck_impact(step, graph, step_readiness_data)
897
+ downstream_impact = calculate_downstream_impact(step, graph, step_readiness_data)
898
+ impact_score = calculate_bottleneck_impact_score(step, downstream_impact[:downstream_steps_count],
899
+ downstream_impact[:blocked_downstream_count])
900
+
901
+ {
902
+ **extract_step_metadata(step),
903
+ **downstream_impact,
904
+ impact_score: impact_score,
905
+ severity: determine_bottleneck_severity_level(impact_score),
906
+ bottleneck_type: determine_bottleneck_type(step),
907
+ resolution_strategy: suggest_bottleneck_resolution(step),
908
+ estimated_resolution_time: estimate_resolution_time(step)
909
+ }
910
+ end
911
+
912
+ # Calculate the downstream impact of a bottleneck step
913
+ #
914
+ # Determines how many steps are affected by this bottleneck, both in terms
915
+ # of total downstream steps and those that are actually blocked.
916
+ #
917
+ # @param step [Object] Step readiness status object
918
+ # @param graph [Hash] Dependency graph structure
919
+ # @param step_readiness_data [Hash] Step ID to readiness status mapping
920
+ # @return [Hash] Impact metrics with :downstream_steps_count and :blocked_downstream_count
921
+ #
922
+ # @api private
923
+ def calculate_downstream_impact(step, graph, step_readiness_data)
924
+ downstream_steps = find_downstream_steps(step.workflow_step_id, graph[:adjacency_list])
925
+ blocked_downstream = count_blocked_downstream_steps(downstream_steps, step_readiness_data)
926
+
927
+ {
928
+ downstream_steps_count: downstream_steps.length,
929
+ blocked_downstream_count: blocked_downstream
930
+ }
931
+ end
932
+
933
+ # Extract essential metadata from a step for analysis
934
+ #
935
+ # @param step [Object] Step readiness status object
936
+ # @return [Hash] Step metadata including ID, name, state, attempts, and dependencies
937
+ #
938
+ # @api private
939
+ def extract_step_metadata(step)
940
+ {
941
+ step_id: step.workflow_step_id,
942
+ step_name: step.name,
943
+ current_state: step.current_state,
944
+ attempts: step.attempts,
945
+ retry_limit: step.retry_limit || 3,
946
+ dependencies_satisfied: step.dependencies_satisfied
947
+ }
948
+ end
949
+
950
+ # Count downstream steps that are actually blocked by dependencies
951
+ #
952
+ # @param downstream_steps [Array<Integer>] List of downstream step IDs
953
+ # @param step_readiness_data [Hash] Step ID to readiness status mapping
954
+ # @return [Integer] Number of downstream steps that are blocked
955
+ #
956
+ # @api private
957
+ def count_blocked_downstream_steps(downstream_steps, step_readiness_data)
958
+ downstream_steps.count do |step_id|
959
+ downstream_step = step_readiness_data[step_id]
960
+ downstream_step && !downstream_step.dependencies_satisfied
961
+ end
962
+ end
963
+
964
+ # Calculate comprehensive impact score for a bottleneck
965
+ #
966
+ # Combines base impact, state severity multipliers, and penalty scores to
967
+ # provide a comprehensive bottleneck impact assessment.
968
+ #
969
+ # @param step [Object] Step readiness status object
970
+ # @param downstream_count [Integer] Total number of downstream steps
971
+ # @param blocked_count [Integer] Number of blocked downstream steps
972
+ # @return [Integer] Rounded impact score for severity classification
973
+ #
974
+ # @api private
975
+ def calculate_bottleneck_impact_score(step, downstream_count, blocked_count)
976
+ base_score = calculate_base_impact_score(downstream_count, blocked_count)
977
+ state_multiplier = calculate_state_severity_multiplier(step)
978
+ penalty_score = calculate_bottleneck_penalties(step)
979
+
980
+ ((base_score * state_multiplier) + penalty_score).round
981
+ end
982
+
983
+ # Calculate base impact score from downstream effects
984
+ #
985
+ # Provides the foundation score based on how many steps are affected.
986
+ # Blocked steps are weighted more heavily than total downstream steps.
987
+ # Uses configurable multipliers for flexible impact weighting.
988
+ #
989
+ # @param downstream_count [Integer] Total number of downstream steps
990
+ # @param blocked_count [Integer] Number of blocked downstream steps
991
+ # @return [Integer] Base impact score using configurable weights
992
+ #
993
+ # @api private
994
+ def calculate_base_impact_score(downstream_count, blocked_count)
995
+ config = dependency_graph_config
996
+ (downstream_count * config.impact_multipliers[:downstream_weight]) +
997
+ (blocked_count * config.impact_multipliers[:blocked_weight])
998
+ end
999
+
1000
+ # Calculate severity multiplier based on step state
1001
+ #
1002
+ # Applies multipliers based on the current state of the step, with error
1003
+ # states receiving higher multipliers, especially for exhausted retries.
1004
+ # Uses configurable multipliers for flexible severity weighting.
1005
+ #
1006
+ # @param step [Object] Step readiness status object
1007
+ # @return [Float] Severity multiplier using configurable weights
1008
+ #
1009
+ # @api private
1010
+ def calculate_state_severity_multiplier(step)
1011
+ config = dependency_graph_config
1012
+
1013
+ case step.current_state
1014
+ when 'error'
1015
+ multiplier = config.severity_multipliers[:error_state] # Errors are serious
1016
+ # Exhausted retries are critical
1017
+ multiplier += config.severity_multipliers[:exhausted_retry_bonus] if step.attempts >= (step.retry_limit || 3)
1018
+ multiplier
1019
+ when 'pending'
1020
+ step.dependencies_satisfied ? 1.0 : config.severity_multipliers[:dependency_issue] # Dependency issues
1021
+ else
1022
+ 1.0
1023
+ end
1024
+ end
1025
+
1026
+ # Calculate additional penalty scores for problematic conditions
1027
+ #
1028
+ # Adds penalty points for retry instability, non-retryable issues,
1029
+ # and exhausted retry attempts. Uses configurable constants for
1030
+ # flexible penalty weighting.
1031
+ #
1032
+ # @param step [Object] Step readiness status object
1033
+ # @return [Integer] Total penalty score using configurable penalties
1034
+ #
1035
+ # @api private
1036
+ def calculate_bottleneck_penalties(step)
1037
+ config = dependency_graph_config
1038
+ penalty = 0
1039
+ penalty += step.attempts * config.penalty_constants[:retry_instability] # Retry instability
1040
+ penalty += config.penalty_constants[:non_retryable] unless step.retry_eligible # Non-retryable issues
1041
+ # Exhausted retries
1042
+ penalty += config.penalty_constants[:exhausted_retry] if step.attempts >= (step.retry_limit || 3)
1043
+ penalty
1044
+ end
1045
+
1046
+ # Determine severity level based on impact score
1047
+ #
1048
+ # Uses configurable thresholds for flexible severity classification.
1049
+ #
1050
+ # @param impact_score [Integer] The calculated impact score
1051
+ # @return [String] Severity level: 'Critical', 'High', 'Medium', or 'Low'
1052
+ def determine_bottleneck_severity_level(impact_score)
1053
+ config = dependency_graph_config
1054
+
1055
+ return 'Critical' if impact_score >= config.severity_thresholds[:critical]
1056
+ return 'High' if impact_score >= config.severity_thresholds[:high]
1057
+ return 'Medium' if impact_score >= config.severity_thresholds[:medium]
1058
+
1059
+ 'Low'
1060
+ end
1061
+
1062
+ # Determine the type of bottleneck using step readiness status
1063
+ #
1064
+ # Classifies the bottleneck type by leveraging step readiness status methods
1065
+ # for consistent and accurate categorization.
1066
+ #
1067
+ # @param step [Object] Step readiness status object
1068
+ # @return [String] Bottleneck type classification:
1069
+ # - 'Permanent Failure' for exhausted retries
1070
+ # - 'Active Error' for steps in error state
1071
+ # - 'Dependency Block' for dependency issues
1072
+ # - 'Retry Exhaustion' for high retry counts
1073
+ # - 'Non-Retryable' for non-retryable steps
1074
+ # - 'Unknown' for unclassified cases
1075
+ #
1076
+ # @api private
1077
+ def determine_bottleneck_type(step)
1078
+ # Use the existing retry_status and dependency_status from step readiness
1079
+ retry_status = step.retry_status
1080
+ dependency_status = step.dependency_status
1081
+
1082
+ # Check for permanent failures first
1083
+ return 'Permanent Failure' if retry_status == 'max_retries_reached'
1084
+
1085
+ # Check current state issues
1086
+ return 'Active Error' if step.current_state == 'error'
1087
+
1088
+ # Check dependency issues
1089
+ return 'Dependency Block' if dependency_status != 'all_satisfied' && dependency_status != 'no_dependencies'
1090
+
1091
+ # Check retry issues
1092
+ return 'Retry Exhaustion' if step.attempts >= 2
1093
+ return 'Non-Retryable' if retry_status != 'retry_eligible'
1094
+
1095
+ 'Unknown'
1096
+ end
1097
+
1098
+ # Suggest resolution strategy using step readiness status
1099
+ #
1100
+ # Provides actionable resolution strategies based on step readiness status
1101
+ # methods for more accurate and helpful suggestions.
1102
+ #
1103
+ # @param step [Object] Step readiness status object
1104
+ # @return [String] Recommended resolution strategy
1105
+ #
1106
+ # @api private
1107
+ def suggest_bottleneck_resolution(step)
1108
+ # Use step readiness status methods for more accurate suggestions
1109
+ retry_status = step.retry_status
1110
+ step.dependency_status
1111
+ blocking_reason = step.blocking_reason
1112
+
1113
+ return 'Manual intervention required' if retry_status == 'max_retries_reached'
1114
+ return 'Investigate error cause' if step.current_state == 'error'
1115
+ return 'Check upstream dependencies' if blocking_reason == 'dependencies_not_satisfied'
1116
+ return 'Wait for backoff period' if blocking_reason == 'retry_not_eligible' && step.next_retry_at
1117
+ return 'Monitor retry attempts' if step.attempts >= 1
1118
+
1119
+ 'Review step configuration'
1120
+ end
1121
+
1122
+ # Estimate time to resolve bottleneck using precise step readiness data
1123
+ #
1124
+ # Provides accurate time estimates by leveraging step readiness status
1125
+ # time calculations, including exact minute-based estimates when available.
1126
+ #
1127
+ # @param step [Object] Step readiness status object
1128
+ # @return [String] Time estimate for resolution:
1129
+ # - 'Immediate action needed' for exhausted retries
1130
+ # - Specific minute estimates when time_until_ready is available
1131
+ # - General estimates for other conditions
1132
+ #
1133
+ # @api private
1134
+ def estimate_resolution_time(step)
1135
+ retry_status = step.retry_status
1136
+ blocking_reason = step.blocking_reason
1137
+ time_until_ready = step.time_until_ready
1138
+
1139
+ return 'Immediate action needed' if retry_status == 'max_retries_reached'
1140
+
1141
+ # If we have a specific time until ready, use it
1142
+ if time_until_ready&.positive?
1143
+ minutes = (time_until_ready / 60.0).ceil
1144
+ return "#{minutes} minute#{'s' unless minutes == 1}"
1145
+ end
1146
+
1147
+ return 'Within next retry cycle' if step.current_state == 'error' && retry_status == 'retry_eligible'
1148
+ return 'Depends on upstream steps' if blocking_reason == 'dependencies_not_satisfied'
1149
+
1150
+ 'Unknown'
1151
+ end
1152
+
1153
+ # Calculate overall bottleneck impact on the task
1154
+ def calculate_overall_bottleneck_impact(bottleneck_analysis, task_context)
1155
+ return 'None' if bottleneck_analysis.empty?
1156
+
1157
+ critical_count = bottleneck_analysis.count { |b| b[:severity] == 'Critical' }
1158
+ high_count = bottleneck_analysis.count { |b| b[:severity] == 'High' }
1159
+
1160
+ return 'Severe' if critical_count >= 3 || task_context.failed_steps > (task_context.total_steps / 2)
1161
+ return 'High' if critical_count >= 1 || high_count >= 3
1162
+ return 'Medium' if high_count >= 1 || bottleneck_analysis.length >= 5
1163
+
1164
+ 'Low'
1165
+ end
1166
+ end
1167
+ end
1168
+ end