@servicenow/sdk-build-plugins 4.6.1 → 4.7.1

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 (279) hide show
  1. package/dist/acl-plugin.js +0 -3
  2. package/dist/acl-plugin.js.map +1 -1
  3. package/dist/applicability-plugin.js +0 -2
  4. package/dist/applicability-plugin.js.map +1 -1
  5. package/dist/application-menu-plugin.js +0 -2
  6. package/dist/application-menu-plugin.js.map +1 -1
  7. package/dist/arrow-function-plugin.js +0 -1
  8. package/dist/arrow-function-plugin.js.map +1 -1
  9. package/dist/atf/test-plugin.js +0 -2
  10. package/dist/atf/test-plugin.js.map +1 -1
  11. package/dist/basic-syntax-plugin.js +0 -1
  12. package/dist/basic-syntax-plugin.js.map +1 -1
  13. package/dist/business-rule-plugin.js +0 -1
  14. package/dist/business-rule-plugin.js.map +1 -1
  15. package/dist/call-expression-plugin.js +0 -1
  16. package/dist/call-expression-plugin.js.map +1 -1
  17. package/dist/claims-plugin.js +0 -1
  18. package/dist/claims-plugin.js.map +1 -1
  19. package/dist/client-script-plugin.js +0 -1
  20. package/dist/client-script-plugin.js.map +1 -1
  21. package/dist/column-plugin.js +24 -7
  22. package/dist/column-plugin.js.map +1 -1
  23. package/dist/cross-scope-privilege-plugin.js +0 -1
  24. package/dist/cross-scope-privilege-plugin.js.map +1 -1
  25. package/dist/dashboard/dashboard-plugin.js +0 -2
  26. package/dist/dashboard/dashboard-plugin.js.map +1 -1
  27. package/dist/data-plugin.js +0 -1
  28. package/dist/data-plugin.js.map +1 -1
  29. package/dist/data-policy-plugin.d.ts +2 -0
  30. package/dist/data-policy-plugin.js +276 -0
  31. package/dist/data-policy-plugin.js.map +1 -0
  32. package/dist/email-notification-plugin.js +2 -3
  33. package/dist/email-notification-plugin.js.map +1 -1
  34. package/dist/flow/flow-logic/flow-logic-constants.d.ts +2 -0
  35. package/dist/flow/flow-logic/flow-logic-constants.js +6 -1
  36. package/dist/flow/flow-logic/flow-logic-constants.js.map +1 -1
  37. package/dist/flow/flow-logic/flow-logic-diagnostics.js +192 -56
  38. package/dist/flow/flow-logic/flow-logic-diagnostics.js.map +1 -1
  39. package/dist/flow/flow-logic/flow-logic-plugin-helpers.d.ts +2 -1
  40. package/dist/flow/flow-logic/flow-logic-plugin-helpers.js +44 -5
  41. package/dist/flow/flow-logic/flow-logic-plugin-helpers.js.map +1 -1
  42. package/dist/flow/flow-logic/flow-logic-plugin.js +279 -29
  43. package/dist/flow/flow-logic/flow-logic-plugin.js.map +1 -1
  44. package/dist/flow/flow-logic/flow-logic-shapes.d.ts +15 -0
  45. package/dist/flow/flow-logic/flow-logic-shapes.js +25 -1
  46. package/dist/flow/flow-logic/flow-logic-shapes.js.map +1 -1
  47. package/dist/flow/plugins/approval-rules-plugin.js +0 -1
  48. package/dist/flow/plugins/approval-rules-plugin.js.map +1 -1
  49. package/dist/flow/plugins/flow-action-definition-plugin.js +804 -205
  50. package/dist/flow/plugins/flow-action-definition-plugin.js.map +1 -1
  51. package/dist/flow/plugins/flow-data-pill-plugin.js +3 -5
  52. package/dist/flow/plugins/flow-data-pill-plugin.js.map +1 -1
  53. package/dist/flow/plugins/flow-definition-plugin.js +84 -17
  54. package/dist/flow/plugins/flow-definition-plugin.js.map +1 -1
  55. package/dist/flow/plugins/flow-diagnostics-plugin.js +65 -3
  56. package/dist/flow/plugins/flow-diagnostics-plugin.js.map +1 -1
  57. package/dist/flow/plugins/flow-instance-plugin.js +13 -5
  58. package/dist/flow/plugins/flow-instance-plugin.js.map +1 -1
  59. package/dist/flow/plugins/flow-trigger-instance-plugin.js +0 -1
  60. package/dist/flow/plugins/flow-trigger-instance-plugin.js.map +1 -1
  61. package/dist/flow/plugins/inline-script-plugin.js +0 -1
  62. package/dist/flow/plugins/inline-script-plugin.js.map +1 -1
  63. package/dist/flow/plugins/step-definition-plugin.js +0 -2
  64. package/dist/flow/plugins/step-definition-plugin.js.map +1 -1
  65. package/dist/flow/plugins/step-instance-plugin.js +216 -77
  66. package/dist/flow/plugins/step-instance-plugin.js.map +1 -1
  67. package/dist/flow/plugins/trigger-plugin.js +0 -2
  68. package/dist/flow/plugins/trigger-plugin.js.map +1 -1
  69. package/dist/flow/plugins/wfa-datapill-plugin.js +0 -1
  70. package/dist/flow/plugins/wfa-datapill-plugin.js.map +1 -1
  71. package/dist/flow/utils/datapill-transformer.js +9 -5
  72. package/dist/flow/utils/datapill-transformer.js.map +1 -1
  73. package/dist/flow/utils/flow-constants.d.ts +12 -0
  74. package/dist/flow/utils/flow-constants.js +17 -3
  75. package/dist/flow/utils/flow-constants.js.map +1 -1
  76. package/dist/flow/utils/flow-io-to-record.d.ts +1 -1
  77. package/dist/flow/utils/flow-io-to-record.js +21 -13
  78. package/dist/flow/utils/flow-io-to-record.js.map +1 -1
  79. package/dist/flow/utils/flow-pill-utils.d.ts +26 -0
  80. package/dist/flow/utils/flow-pill-utils.js +50 -0
  81. package/dist/flow/utils/flow-pill-utils.js.map +1 -0
  82. package/dist/flow/utils/flow-stage-processor.d.ts +138 -0
  83. package/dist/flow/utils/flow-stage-processor.js +665 -0
  84. package/dist/flow/utils/flow-stage-processor.js.map +1 -0
  85. package/dist/flow/utils/pill-string-parser.js +28 -43
  86. package/dist/flow/utils/pill-string-parser.js.map +1 -1
  87. package/dist/flow/utils/utils.d.ts +11 -6
  88. package/dist/flow/utils/utils.js +37 -28
  89. package/dist/flow/utils/utils.js.map +1 -1
  90. package/dist/form-plugin.js +4 -14
  91. package/dist/form-plugin.js.map +1 -1
  92. package/dist/html-import-plugin.js +0 -1
  93. package/dist/html-import-plugin.js.map +1 -1
  94. package/dist/import-sets-plugin.js +0 -2
  95. package/dist/import-sets-plugin.js.map +1 -1
  96. package/dist/inbound-email-action-plugin.js +0 -1
  97. package/dist/inbound-email-action-plugin.js.map +1 -1
  98. package/dist/index.d.ts +2 -1
  99. package/dist/index.js +5 -1
  100. package/dist/index.js.map +1 -1
  101. package/dist/instance-scan-plugin.js +0 -7
  102. package/dist/instance-scan-plugin.js.map +1 -1
  103. package/dist/json-plugin.js +0 -1
  104. package/dist/json-plugin.js.map +1 -1
  105. package/dist/list-plugin.js +4 -1
  106. package/dist/list-plugin.js.map +1 -1
  107. package/dist/now-attach-plugin.js +0 -1
  108. package/dist/now-attach-plugin.js.map +1 -1
  109. package/dist/now-config-plugin.js +0 -1
  110. package/dist/now-config-plugin.js.map +1 -1
  111. package/dist/now-id-plugin.js +0 -1
  112. package/dist/now-id-plugin.js.map +1 -1
  113. package/dist/now-include-plugin.js +0 -1
  114. package/dist/now-include-plugin.js.map +1 -1
  115. package/dist/now-ref-plugin.js +0 -1
  116. package/dist/now-ref-plugin.js.map +1 -1
  117. package/dist/now-unresolved-plugin.js +0 -1
  118. package/dist/now-unresolved-plugin.js.map +1 -1
  119. package/dist/package-json-plugin.js +3 -2
  120. package/dist/package-json-plugin.js.map +1 -1
  121. package/dist/property-plugin.js +0 -2
  122. package/dist/property-plugin.js.map +1 -1
  123. package/dist/record-plugin.d.ts +2 -0
  124. package/dist/record-plugin.js +2 -2
  125. package/dist/record-plugin.js.map +1 -1
  126. package/dist/repack/lint/Rules.d.ts +1 -2
  127. package/dist/rest-api-plugin.js +6 -5
  128. package/dist/rest-api-plugin.js.map +1 -1
  129. package/dist/role-plugin.js +0 -1
  130. package/dist/role-plugin.js.map +1 -1
  131. package/dist/schedule-script/scheduled-script-plugin.js +5 -4
  132. package/dist/schedule-script/scheduled-script-plugin.js.map +1 -1
  133. package/dist/script-action-plugin.js +0 -2
  134. package/dist/script-action-plugin.js.map +1 -1
  135. package/dist/script-include-plugin.js +0 -4
  136. package/dist/script-include-plugin.js.map +1 -1
  137. package/dist/server-module-plugin/index.js +2 -3
  138. package/dist/server-module-plugin/index.js.map +1 -1
  139. package/dist/service-catalog/catalog-clientscript-plugin.js +0 -2
  140. package/dist/service-catalog/catalog-clientscript-plugin.js.map +1 -1
  141. package/dist/service-catalog/catalog-item-plugin.js +0 -2
  142. package/dist/service-catalog/catalog-item-plugin.js.map +1 -1
  143. package/dist/service-catalog/catalog-ui-policy-plugin.js +0 -2
  144. package/dist/service-catalog/catalog-ui-policy-plugin.js.map +1 -1
  145. package/dist/service-catalog/sc-record-producer-plugin.js +0 -2
  146. package/dist/service-catalog/sc-record-producer-plugin.js.map +1 -1
  147. package/dist/service-catalog/variable-set-plugin.js +0 -2
  148. package/dist/service-catalog/variable-set-plugin.js.map +1 -1
  149. package/dist/service-portal/angular-provider-plugin.js +0 -2
  150. package/dist/service-portal/angular-provider-plugin.js.map +1 -1
  151. package/dist/service-portal/dependency-plugin.js +3 -5
  152. package/dist/service-portal/dependency-plugin.js.map +1 -1
  153. package/dist/service-portal/header-footer-plugin.js +3 -5
  154. package/dist/service-portal/header-footer-plugin.js.map +1 -1
  155. package/dist/service-portal/menu-plugin.js +0 -1
  156. package/dist/service-portal/menu-plugin.js.map +1 -1
  157. package/dist/service-portal/page-plugin.js +0 -1
  158. package/dist/service-portal/page-plugin.js.map +1 -1
  159. package/dist/service-portal/page-route-map-plugin.js +0 -1
  160. package/dist/service-portal/page-route-map-plugin.js.map +1 -1
  161. package/dist/service-portal/portal-plugin.js +0 -2
  162. package/dist/service-portal/portal-plugin.js.map +1 -1
  163. package/dist/service-portal/theme-plugin.js +0 -2
  164. package/dist/service-portal/theme-plugin.js.map +1 -1
  165. package/dist/service-portal/widget-plugin.js +3 -5
  166. package/dist/service-portal/widget-plugin.js.map +1 -1
  167. package/dist/sla-plugin.js +0 -2
  168. package/dist/sla-plugin.js.map +1 -1
  169. package/dist/static-content-plugin.js +32 -3
  170. package/dist/static-content-plugin.js.map +1 -1
  171. package/dist/table-plugin.js +102 -11
  172. package/dist/table-plugin.js.map +1 -1
  173. package/dist/ui-action-plugin.js +26 -17
  174. package/dist/ui-action-plugin.js.map +1 -1
  175. package/dist/ui-page-plugin.js +159 -17
  176. package/dist/ui-page-plugin.js.map +1 -1
  177. package/dist/ui-policy-plugin.js +0 -1
  178. package/dist/ui-policy-plugin.js.map +1 -1
  179. package/dist/user-preference-plugin.js +0 -2
  180. package/dist/user-preference-plugin.js.map +1 -1
  181. package/dist/utils.d.ts +1 -9
  182. package/dist/utils.js +0 -14
  183. package/dist/utils.js.map +1 -1
  184. package/dist/ux-list-menu-config-plugin.js +0 -2
  185. package/dist/ux-list-menu-config-plugin.js.map +1 -1
  186. package/dist/view-plugin.js +0 -1
  187. package/dist/view-plugin.js.map +1 -1
  188. package/dist/workspace-plugin.js +0 -2
  189. package/dist/workspace-plugin.js.map +1 -1
  190. package/package.json +6 -6
  191. package/src/acl-plugin.ts +1 -4
  192. package/src/applicability-plugin.ts +0 -2
  193. package/src/application-menu-plugin.ts +0 -2
  194. package/src/arrow-function-plugin.ts +0 -1
  195. package/src/atf/test-plugin.ts +0 -2
  196. package/src/basic-syntax-plugin.ts +0 -1
  197. package/src/business-rule-plugin.ts +1 -2
  198. package/src/call-expression-plugin.ts +0 -1
  199. package/src/claims-plugin.ts +0 -1
  200. package/src/client-script-plugin.ts +1 -2
  201. package/src/column-plugin.ts +29 -9
  202. package/src/cross-scope-privilege-plugin.ts +1 -2
  203. package/src/dashboard/dashboard-plugin.ts +0 -2
  204. package/src/data-plugin.ts +0 -1
  205. package/src/data-policy-plugin.ts +333 -0
  206. package/src/email-notification-plugin.ts +8 -4
  207. package/src/flow/flow-logic/flow-logic-constants.ts +6 -0
  208. package/src/flow/flow-logic/flow-logic-diagnostics.ts +236 -58
  209. package/src/flow/flow-logic/flow-logic-plugin-helpers.ts +59 -6
  210. package/src/flow/flow-logic/flow-logic-plugin.ts +368 -38
  211. package/src/flow/flow-logic/flow-logic-shapes.ts +25 -0
  212. package/src/flow/plugins/approval-rules-plugin.ts +0 -1
  213. package/src/flow/plugins/flow-action-definition-plugin.ts +940 -208
  214. package/src/flow/plugins/flow-data-pill-plugin.ts +3 -5
  215. package/src/flow/plugins/flow-definition-plugin.ts +159 -26
  216. package/src/flow/plugins/flow-diagnostics-plugin.ts +89 -3
  217. package/src/flow/plugins/flow-instance-plugin.ts +26 -12
  218. package/src/flow/plugins/flow-trigger-instance-plugin.ts +0 -1
  219. package/src/flow/plugins/inline-script-plugin.ts +0 -1
  220. package/src/flow/plugins/step-definition-plugin.ts +0 -2
  221. package/src/flow/plugins/step-instance-plugin.ts +259 -65
  222. package/src/flow/plugins/trigger-plugin.ts +0 -2
  223. package/src/flow/plugins/wfa-datapill-plugin.ts +0 -1
  224. package/src/flow/utils/datapill-transformer.ts +13 -5
  225. package/src/flow/utils/flow-constants.ts +19 -1
  226. package/src/flow/utils/flow-io-to-record.ts +29 -19
  227. package/src/flow/utils/flow-pill-utils.ts +48 -0
  228. package/src/flow/utils/flow-stage-processor.ts +831 -0
  229. package/src/flow/utils/pill-string-parser.ts +29 -47
  230. package/src/flow/utils/utils.ts +39 -35
  231. package/src/form-plugin.ts +5 -15
  232. package/src/html-import-plugin.ts +0 -1
  233. package/src/import-sets-plugin.ts +0 -2
  234. package/src/inbound-email-action-plugin.ts +1 -2
  235. package/src/index.ts +7 -1
  236. package/src/instance-scan-plugin.ts +0 -7
  237. package/src/json-plugin.ts +0 -1
  238. package/src/list-plugin.ts +6 -2
  239. package/src/now-attach-plugin.ts +0 -1
  240. package/src/now-config-plugin.ts +0 -1
  241. package/src/now-id-plugin.ts +0 -1
  242. package/src/now-include-plugin.ts +0 -1
  243. package/src/now-ref-plugin.ts +0 -1
  244. package/src/now-unresolved-plugin.ts +0 -1
  245. package/src/package-json-plugin.ts +8 -3
  246. package/src/property-plugin.ts +0 -2
  247. package/src/record-plugin.ts +3 -3
  248. package/src/repack/lint/Rules.ts +1 -1
  249. package/src/rest-api-plugin.ts +7 -6
  250. package/src/role-plugin.ts +1 -2
  251. package/src/schedule-script/scheduled-script-plugin.ts +11 -5
  252. package/src/script-action-plugin.ts +0 -2
  253. package/src/script-include-plugin.ts +0 -4
  254. package/src/server-module-plugin/index.ts +2 -3
  255. package/src/service-catalog/catalog-clientscript-plugin.ts +0 -2
  256. package/src/service-catalog/catalog-item-plugin.ts +0 -2
  257. package/src/service-catalog/catalog-ui-policy-plugin.ts +0 -2
  258. package/src/service-catalog/sc-record-producer-plugin.ts +0 -2
  259. package/src/service-catalog/variable-set-plugin.ts +0 -2
  260. package/src/service-portal/angular-provider-plugin.ts +0 -2
  261. package/src/service-portal/dependency-plugin.ts +0 -2
  262. package/src/service-portal/header-footer-plugin.ts +0 -2
  263. package/src/service-portal/menu-plugin.ts +1 -2
  264. package/src/service-portal/page-plugin.ts +1 -2
  265. package/src/service-portal/page-route-map-plugin.ts +1 -2
  266. package/src/service-portal/portal-plugin.ts +0 -2
  267. package/src/service-portal/theme-plugin.ts +0 -2
  268. package/src/service-portal/widget-plugin.ts +0 -2
  269. package/src/sla-plugin.ts +0 -2
  270. package/src/static-content-plugin.ts +37 -4
  271. package/src/table-plugin.ts +118 -16
  272. package/src/ui-action-plugin.ts +30 -17
  273. package/src/ui-page-plugin.ts +188 -20
  274. package/src/ui-policy-plugin.ts +1 -2
  275. package/src/user-preference-plugin.ts +0 -2
  276. package/src/utils.ts +0 -15
  277. package/src/ux-list-menu-config-plugin.ts +0 -2
  278. package/src/view-plugin.ts +0 -1
  279. package/src/workspace-plugin.ts +0 -2
@@ -0,0 +1,831 @@
1
+ import {
2
+ CallExpressionShape,
3
+ DATA_HELPER_NAMES,
4
+ type Diagnostics,
5
+ DurationShape,
6
+ type Factory,
7
+ ObjectShape,
8
+ PropertyAccessShape,
9
+ Record,
10
+ Shape,
11
+ type Source,
12
+ VariableStatementShape,
13
+ } from '@servicenow/sdk-build-core'
14
+ import { ArrowFunctionShape } from '../../arrow-function-plugin'
15
+ import { NowIdShape } from '../../now-id-plugin'
16
+ import {
17
+ FLOW_STAGE_CALLEE,
18
+ FLOW_STAGE_TABLE,
19
+ STAGE_API_NAME,
20
+ STAGE_DEFAULT_DURATION,
21
+ SUBFLOW_INSTANCE_API_NAME,
22
+ } from './flow-constants'
23
+ import { FLOW_LOGIC } from '../flow-logic/flow-logic-constants'
24
+ import { sysIdToUuid } from './utils'
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Duration conversion (Fluent → XML)
28
+ // ---------------------------------------------------------------------------
29
+
30
+ /**
31
+ * Converts a `Duration({...})` CallExpressionShape from the Fluent API into the
32
+ * `YYYY-MM-DD HH:MM:SS` string stored in `sys_hub_flow_stage.duration`.
33
+ *
34
+ * `Duration()` is an identity function — ObjectShape.get() resolves it with
35
+ * resolve=true by default, but CallExpressionShape.resolve() returns itself
36
+ * (can't evaluate the body). We therefore read with resolve=false to get the
37
+ * raw CallExpressionShape, then delegate to DurationShape for formatting.
38
+ * Falls back to zero duration when absent or not a Duration() call.
39
+ */
40
+ export function stageDurationToString(stageConfig: ObjectShape): string {
41
+ const raw = stageConfig.get('duration', false)
42
+ const ce = raw.if(CallExpressionShape) ? raw.as(CallExpressionShape) : undefined
43
+ if (ce && ce.getCallee() === DATA_HELPER_NAMES.DURATION) {
44
+ return new DurationShape({
45
+ source: raw.getSource(),
46
+ value: ce.getArgument(0).asObject(),
47
+ })
48
+ .toString()
49
+ .getValue()
50
+ }
51
+ return STAGE_DEFAULT_DURATION
52
+ }
53
+
54
+ // camelCase API key → snake_case DB key for FlowStageStates fields
55
+ const STATE_KEY_MAP: { [k: string]: string } = {
56
+ pending: 'pending',
57
+ inProgress: 'in_progress',
58
+ complete: 'complete',
59
+ error: 'error',
60
+ skipped: 'skipped',
61
+ }
62
+
63
+ const DEFAULT_STATES: { [k: string]: string } = {
64
+ in_progress: 'In progress',
65
+ complete: 'Completed',
66
+ error: 'Error',
67
+ pending: 'Pending - has not started',
68
+ skipped: 'Skipped',
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Phase 1 — Header stage pre-creation
73
+ // ---------------------------------------------------------------------------
74
+
75
+ /**
76
+ * Reads the `stages: {}` config object from the Flow/Subflow first argument
77
+ * and creates `sys_hub_flow_stage` records for each declared stage.
78
+ *
79
+ * Records are created WITHOUT `component_indexes` — those are computed by
80
+ * `finalizeStageRecords` after the body loop has determined which actions
81
+ * each stage covers.
82
+ *
83
+ * `stage_id` is derived from the record's own coalesce-based sys_id (which the
84
+ * factory makes stable from `flow + value`), so it is deterministic across
85
+ * builds without requiring a separate hash.
86
+ *
87
+ * @returns Map keyed by the stages object property name (e.g. `'triage'`).
88
+ */
89
+ export async function processHeaderStages(
90
+ flowConfiguration: ObjectShape,
91
+ flowDefinitionRecord: Record,
92
+ factory: Factory
93
+ ): Promise<Map<string, Record>> {
94
+ const headerStageRecords = new Map<string, Record>()
95
+ if (!flowConfiguration.has('stages')) {
96
+ return headerStageRecords
97
+ }
98
+
99
+ const stagesObj = flowConfiguration.get('stages').asObject()
100
+
101
+ for (const [key, stageVal] of stagesObj.entries()) {
102
+ const stageCE = stageVal.if(CallExpressionShape) ? stageVal.as(CallExpressionShape) : undefined
103
+ if (!stageCE) {
104
+ continue
105
+ }
106
+
107
+ const stageConfig = stageCE.getArgument(0).asObject()
108
+ const stageValue = stageConfig.get('value').ifString()?.getValue() ?? ''
109
+
110
+ const statesArg = stageConfig.get('states').ifObject()?.asObject()
111
+ let statesJson: { [k: string]: string }
112
+ if (statesArg) {
113
+ statesJson = {}
114
+ for (const [apiKey, dbKey] of Object.entries(STATE_KEY_MAP)) {
115
+ const val = statesArg.get(apiKey).ifString()?.getValue()
116
+ if (val) {
117
+ statesJson[dbKey] = val
118
+ }
119
+ }
120
+ } else {
121
+ statesJson = { ...DEFAULT_STATES }
122
+ }
123
+
124
+ const stageRecord = await factory.createRecord({
125
+ source: stageCE,
126
+ table: FLOW_STAGE_TABLE,
127
+ properties: {
128
+ flow: flowDefinitionRecord.getId().getValue(),
129
+ label: stageConfig.get('label').ifString()?.getValue() ?? '',
130
+ value: stageValue,
131
+ duration: stageDurationToString(stageConfig),
132
+ always_show: stageConfig.get('alwaysShow').ifBoolean()?.getValue() ?? false,
133
+ states: JSON.stringify(statesJson),
134
+ type: 'standard',
135
+ ancestor_array_position: 0,
136
+ ancestor_component_id: '',
137
+ ancestor_stage_id: '',
138
+ ancestral_if_else_logic: '',
139
+ },
140
+ })
141
+
142
+ // stage_id reuses the record's coalesce-based sys_id — stable across builds
143
+ // because sys_hub_flow_stage coalesces on ['flow', 'value'].
144
+ headerStageRecords.set(
145
+ key,
146
+ stageRecord.merge({ stage_id: sysIdToUuid(String(stageRecord.getId().getValue())) })
147
+ )
148
+ }
149
+
150
+ return headerStageRecords
151
+ }
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // Phase 2 — Subflow stage creation
155
+ // ---------------------------------------------------------------------------
156
+
157
+ /**
158
+ * Scans `relatedRecords` for subflow instances with `show_stages=true` and
159
+ * creates a separate `sys_hub_flow_stage` record with `type:'subflow'` for each.
160
+ *
161
+ * These differ from developer-declared stages:
162
+ * - `type` = `'subflow'` (not `'standard'`)
163
+ * - `value` = the subflow instance's `ui_id` (UUID)
164
+ * - `label` = the subflow definition's name (resolved via `subflowDefNames` map)
165
+ * - `component_indexes`= empty — the platform links via `value` (the UUID)
166
+ *
167
+ * @param pendingStagesLength Number of developer-declared stages — subflow stages
168
+ * are ordered after them.
169
+ * @param subflowDefNames Map of subflow definition sys_id → name, built by the
170
+ * caller from the original shapes.
171
+ */
172
+ export async function createSubflowStageRecords(
173
+ relatedRecords: Record[],
174
+ pendingStagesLength: number,
175
+ flowDefinitionRecord: Record,
176
+ factory: Factory,
177
+ subflowDefNames: Map<string, string>
178
+ ): Promise<Record[]> {
179
+ const subflowStageRecords: Record[] = []
180
+ let subflowStageOrder = pendingStagesLength
181
+
182
+ for (const rec of relatedRecords) {
183
+ if (rec.getTable() !== 'sys_hub_sub_flow_instance_v2') {
184
+ continue
185
+ }
186
+ const showStages = rec.get('show_stages')?.getValue()
187
+ if (showStages !== true && showStages !== 'true') {
188
+ continue
189
+ }
190
+ const uiId = String(rec.get('ui_id')?.getValue() ?? '')
191
+ if (!uiId) {
192
+ continue
193
+ }
194
+ const defSysId = String(rec.get('subflow')?.getValue() ?? '')
195
+ const label = subflowDefNames.get(defSysId) ?? ''
196
+
197
+ const stageRecord = await factory.createRecord({
198
+ source: flowDefinitionRecord,
199
+ table: FLOW_STAGE_TABLE,
200
+ properties: {
201
+ flow: flowDefinitionRecord.getId().getValue(),
202
+ type: 'subflow',
203
+ value: uiId,
204
+ label,
205
+ always_show: false,
206
+ states: '{}',
207
+ component_indexes: '',
208
+ order: subflowStageOrder,
209
+ ancestor_array_position: 0,
210
+ ancestor_component_id: '',
211
+ ancestor_stage_id: '',
212
+ ancestral_if_else_logic: '',
213
+ duration: STAGE_DEFAULT_DURATION,
214
+ },
215
+ })
216
+
217
+ // stage_id reuses the record's coalesce-based sys_id — stable because
218
+ // sys_hub_flow_stage coalesces on ['flow', 'value'] and value = uiId.
219
+ subflowStageRecords.push(stageRecord.merge({ stage_id: sysIdToUuid(String(stageRecord.getId().getValue())) }))
220
+ subflowStageOrder++
221
+ }
222
+
223
+ return subflowStageRecords
224
+ }
225
+
226
+ // ---------------------------------------------------------------------------
227
+ // Body scanning — collect stage markers & subflow definition names
228
+ // ---------------------------------------------------------------------------
229
+
230
+ /**
231
+ * Extracts the stage key from a `wfa.stage(params.stages.<key>)` call.
232
+ * Returns `undefined` when the argument is not a PropertyAccessShape.
233
+ */
234
+ export function extractStageKey(callExpr: CallExpressionShape): string | undefined {
235
+ const arg = callExpr.getArgument(0, false)
236
+ if (!arg.if(PropertyAccessShape)) {
237
+ return undefined
238
+ }
239
+ return arg.as(PropertyAccessShape).getLastElement().getName()
240
+ }
241
+
242
+ /**
243
+ * Extracts the NowIdKey (`$id` value) from the config object of an instance call.
244
+ * Checks argument positions 0 and 1 (actions use position 1, flow logic uses 0).
245
+ */
246
+ function extractNowIdKey(callExpr: CallExpressionShape): string | undefined {
247
+ for (const idx of [0, 1]) {
248
+ try {
249
+ const a = callExpr.getArgument(idx)
250
+ if (a?.if(ObjectShape)) {
251
+ const $id = a.as(ObjectShape).get('$id')
252
+ if ($id?.if(NowIdShape)) {
253
+ return String($id.as(NowIdShape).getValue())
254
+ }
255
+ }
256
+ } catch {
257
+ /* skip */
258
+ }
259
+ }
260
+ return undefined
261
+ }
262
+
263
+ /** Unwrap a statement to the underlying CallExpressionShape if applicable. */
264
+ export function unwrapCall(stmt: Shape): CallExpressionShape | undefined {
265
+ const inner = stmt instanceof VariableStatementShape ? (stmt as VariableStatementShape).getInitializer() : stmt
266
+ return inner instanceof CallExpressionShape ? inner : undefined
267
+ }
268
+
269
+ /**
270
+ * Collects subflow definition name from a `wfa.subflow(defRecord, ...)` call
271
+ * and stores it in the provided map.
272
+ */
273
+ export function collectSubflowDefName(callExpr: CallExpressionShape, subflowDefNames: Map<string, string>): void {
274
+ const defArg = callExpr.getArgument(0)
275
+ if (defArg?.isRecord()) {
276
+ const defRecord = defArg.as(Record)
277
+ const defSysId = String(defRecord.getId().getValue())
278
+ if (!subflowDefNames.has(defSysId)) {
279
+ subflowDefNames.set(defSysId, String(defRecord.get('name')?.getValue() ?? ''))
280
+ }
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Resolves nested stage indexes inline during the main instance loop.
286
+ *
287
+ * Called when a flow logic instance (if/elseIf/else) is encountered.
288
+ * Builds a local NowIdKey → order map from the flat records (whose orders
289
+ * are known: `baseOrder + index`), then recursively scans the body for
290
+ * `wfa.stage()` calls and pushes resolved entries directly into `pendingStages`.
291
+ *
292
+ * Also collects subflow definition names from nested `wfa.subflow()` calls.
293
+ */
294
+ export function resolveNestedStagesInline(
295
+ callExpr: CallExpressionShape,
296
+ flatRecords: Record[],
297
+ baseOrder: number,
298
+ headerStageRecords: Map<string, Record>,
299
+ pendingStages: Array<{ startActionIndex: number; key: string }>,
300
+ subflowDefNames: Map<string, string>,
301
+ diagnostics: Diagnostics
302
+ ): void {
303
+ const bodyArg = callExpr.getArgument(1, false)
304
+ if (!bodyArg.if(ArrowFunctionShape)) {
305
+ return
306
+ }
307
+
308
+ // Build NowIdKey → order map for these flat records
309
+ const orderByNowIdKey = new Map<string, number>()
310
+ flatRecords.forEach((rec, idx) => {
311
+ const nk = String(rec.getId().getNowIdKey() ?? '')
312
+ if (nk) {
313
+ orderByNowIdKey.set(nk, baseOrder + idx)
314
+ }
315
+ })
316
+
317
+ resolveStagesInBody(
318
+ bodyArg.as(ArrowFunctionShape),
319
+ headerStageRecords,
320
+ orderByNowIdKey,
321
+ pendingStages,
322
+ subflowDefNames,
323
+ diagnostics
324
+ )
325
+ }
326
+
327
+ /**
328
+ * Resolves nested stages inside a `tryCatch(config, { try, catch })` call.
329
+ * Scans both the `try` and `catch` arrow function bodies for `wfa.stage()` calls.
330
+ */
331
+ export function resolveNestedStagesForTryCatch(
332
+ callExpr: CallExpressionShape,
333
+ flatRecords: Record[],
334
+ baseOrder: number,
335
+ headerStageRecords: Map<string, Record>,
336
+ pendingStages: Array<{ startActionIndex: number; key: string }>,
337
+ subflowDefNames: Map<string, string>,
338
+ diagnostics: Diagnostics
339
+ ): void {
340
+ const handlersArg = callExpr.getArgument(1, false)
341
+ if (!handlersArg.if(ObjectShape)) {
342
+ return
343
+ }
344
+
345
+ // Build NowIdKey → order map for these flat records
346
+ const orderByNowIdKey = new Map<string, number>()
347
+ flatRecords.forEach((rec, idx) => {
348
+ const nk = String(rec.getId().getNowIdKey() ?? '')
349
+ if (nk) {
350
+ orderByNowIdKey.set(nk, baseOrder + idx)
351
+ }
352
+ })
353
+
354
+ const handlers = handlersArg.as(ObjectShape)
355
+ const tryHandler = handlers.get('try', false)
356
+ const catchHandler = handlers.get('catch', false)
357
+
358
+ if (tryHandler.if(ArrowFunctionShape)) {
359
+ resolveStagesInBody(
360
+ tryHandler.as(ArrowFunctionShape),
361
+ headerStageRecords,
362
+ orderByNowIdKey,
363
+ pendingStages,
364
+ subflowDefNames,
365
+ diagnostics
366
+ )
367
+ }
368
+ if (catchHandler.if(ArrowFunctionShape)) {
369
+ resolveStagesInBody(
370
+ catchHandler.as(ArrowFunctionShape),
371
+ headerStageRecords,
372
+ orderByNowIdKey,
373
+ pendingStages,
374
+ subflowDefNames,
375
+ diagnostics
376
+ )
377
+ }
378
+ }
379
+
380
+ /**
381
+ * Recursively scans an arrow-function body for:
382
+ * - `wfa.stage()` calls: resolves the order of the *next* non-stage instance
383
+ * from `orderByNowIdKey` and pushes directly to `pendingStages`.
384
+ * - `wfa.subflow()` calls: collects the definition name for showSubflowStage labels.
385
+ */
386
+ function resolveStagesInBody(
387
+ body: ArrowFunctionShape,
388
+ headerStageRecords: Map<string, Record>,
389
+ orderByNowIdKey: Map<string, number>,
390
+ pendingStages: Array<{ startActionIndex: number; key: string }>,
391
+ subflowDefNames: Map<string, string>,
392
+ diagnostics: Diagnostics
393
+ ): void {
394
+ const stmts = body.getStatements()
395
+ for (let i = 0; i < stmts.length; i++) {
396
+ const inner = unwrapCall(stmts[i]!)
397
+ if (!inner) {
398
+ continue
399
+ }
400
+ const callee = inner.getCallee()
401
+
402
+ // Recurse into flow logic bodies (if / elseIf / else)
403
+ if (callee === FLOW_LOGIC.IF || callee === FLOW_LOGIC.ELSEIF || callee === FLOW_LOGIC.ELSE) {
404
+ const bodyArg = inner.getArgument(1, false)
405
+ if (bodyArg.if(ArrowFunctionShape)) {
406
+ resolveStagesInBody(
407
+ bodyArg.as(ArrowFunctionShape),
408
+ headerStageRecords,
409
+ orderByNowIdKey,
410
+ pendingStages,
411
+ subflowDefNames,
412
+ diagnostics
413
+ )
414
+ }
415
+ continue
416
+ }
417
+
418
+ // Recurse into tryCatch try/catch handler bodies
419
+ if (callee === FLOW_LOGIC.TRY_CATCH) {
420
+ const handlersArg = inner.getArgument(1, false)
421
+ if (handlersArg.if(ObjectShape)) {
422
+ const handlers = handlersArg.as(ObjectShape)
423
+ const tryHandler = handlers.get('try', false)
424
+ const catchHandler = handlers.get('catch', false)
425
+ if (tryHandler.if(ArrowFunctionShape)) {
426
+ resolveStagesInBody(
427
+ tryHandler.as(ArrowFunctionShape),
428
+ headerStageRecords,
429
+ orderByNowIdKey,
430
+ pendingStages,
431
+ subflowDefNames,
432
+ diagnostics
433
+ )
434
+ }
435
+ if (catchHandler.if(ArrowFunctionShape)) {
436
+ resolveStagesInBody(
437
+ catchHandler.as(ArrowFunctionShape),
438
+ headerStageRecords,
439
+ orderByNowIdKey,
440
+ pendingStages,
441
+ subflowDefNames,
442
+ diagnostics
443
+ )
444
+ }
445
+ }
446
+ continue
447
+ }
448
+
449
+ // Collect subflow definition names from nested bodies
450
+ if (callee === SUBFLOW_INSTANCE_API_NAME) {
451
+ collectSubflowDefName(inner, subflowDefNames)
452
+ }
453
+
454
+ if (callee !== STAGE_API_NAME) {
455
+ continue
456
+ }
457
+
458
+ // Extract the stage key and resolve the order of the next non-stage instance
459
+ const key = extractStageKey(inner)
460
+ if (!key) {
461
+ diagnostics.error(inner, 'wfa.stage() argument must be a stage reference like params.stages.<key>.')
462
+ continue
463
+ }
464
+ if (!headerStageRecords.has(key)) {
465
+ diagnostics.error(
466
+ inner,
467
+ `wfa.stage() references stage '${key}' which is not declared in the stages config.`
468
+ )
469
+ continue
470
+ }
471
+
472
+ for (let j = i + 1; j < stmts.length; j++) {
473
+ const next = unwrapCall(stmts[j]!)
474
+ if (!next || next.getCallee() === STAGE_API_NAME) {
475
+ continue
476
+ }
477
+ const nowIdKey = extractNowIdKey(next)
478
+ if (nowIdKey) {
479
+ const recOrder = orderByNowIdKey.get(nowIdKey)
480
+ if (recOrder != null) {
481
+ pendingStages.push({ startActionIndex: recOrder - 1, key })
482
+ }
483
+ }
484
+ break
485
+ }
486
+ }
487
+ }
488
+
489
+ // ---------------------------------------------------------------------------
490
+ // Header stage finalisation
491
+ // ---------------------------------------------------------------------------
492
+
493
+ /**
494
+ * Computes `component_indexes` for each developer-declared stage and merges it
495
+ * onto the pre-created header record.
496
+ *
497
+ * Matching is by **key name**: the `key` stored in each `pendingStage` entry is
498
+ * looked up in `headerStageRecords`. This is correct even when the order of
499
+ * `wfa.stage()` calls in the body differs from the declaration order in the
500
+ * stages config header.
501
+ *
502
+ * A stage may appear multiple times in `pendingStages` (e.g. inside both an
503
+ * `if` and an `elseIf` body). All indexes for the same key are collected and
504
+ * joined with `,` into a single `component_indexes` value on one record.
505
+ *
506
+ * @returns Array of finalised stage records ready to be added to `relatedRecords`.
507
+ */
508
+ export function finalizeStageRecords(
509
+ pendingStages: Array<{ startActionIndex: number; key: string }>,
510
+ headerStageRecords: Map<string, Record>
511
+ ): Record[] {
512
+ // Group indexes by stage key, preserving encounter order for `order`.
513
+ const indexesByKey = new Map<string, number[]>()
514
+ const keyOrder: string[] = []
515
+
516
+ for (const entry of pendingStages) {
517
+ if (!entry) {
518
+ continue
519
+ }
520
+ const existing = indexesByKey.get(entry.key)
521
+ if (existing) {
522
+ existing.push(entry.startActionIndex)
523
+ } else {
524
+ indexesByKey.set(entry.key, [entry.startActionIndex])
525
+ keyOrder.push(entry.key)
526
+ }
527
+ }
528
+
529
+ const finalised: Record[] = []
530
+ for (let i = 0; i < keyOrder.length; i++) {
531
+ const key = keyOrder[i]!
532
+ const indexes = indexesByKey.get(key)!
533
+ const headerRecord = headerStageRecords.get(key)
534
+ if (headerRecord) {
535
+ finalised.push(headerRecord.merge({ component_indexes: indexes.join(','), order: i }))
536
+ }
537
+ }
538
+
539
+ // Emit declared-but-never-activated stages with empty component_indexes.
540
+ // These are stages declared in the header config that have no wfa.stage()
541
+ // call anywhere in the flow body (e.g. a stage only activated inside a
542
+ // subflow). The platform still needs the sys_hub_flow_stage record so the
543
+ // stage tracker can display it.
544
+ const activatedKeys = new Set(keyOrder)
545
+ let nextOrder = keyOrder.length
546
+ for (const [key, record] of headerStageRecords) {
547
+ if (!activatedKeys.has(key)) {
548
+ finalised.push(record.merge({ component_indexes: '', order: nextOrder++ }))
549
+ }
550
+ }
551
+
552
+ return finalised
553
+ }
554
+
555
+ // ---------------------------------------------------------------------------
556
+ // toShape helpers — reconstruct stages from DB records back to Fluent shapes
557
+ // ---------------------------------------------------------------------------
558
+
559
+ // snake_case DB key → camelCase API key (reverse of STATE_KEY_MAP)
560
+ const REVERSE_STATE_KEY_MAP: { [k: string]: string } = {
561
+ pending: 'pending',
562
+ in_progress: 'inProgress',
563
+ complete: 'complete',
564
+ error: 'error',
565
+ skipped: 'skipped',
566
+ }
567
+
568
+ /**
569
+ * Converts a stage `value` string (which may contain spaces, dots, underscores
570
+ * etc.) into a valid camelCase JavaScript identifier suitable for use as an
571
+ * object property key and in `params.stages.<key>` property-access expressions.
572
+ *
573
+ * Examples:
574
+ * "Request Cancelled" → "requestCancelled"
575
+ * "Dept. Head Approval" → "deptHeadApproval"
576
+ * "CIO Approval" → "cioApproval"
577
+ * "manager_approval" → "managerApproval"
578
+ * "triage" → "triage"
579
+ */
580
+ export function toSafeIdentifier(value: string): string {
581
+ const parts = value.split(/[^a-zA-Z0-9]+/).filter(Boolean)
582
+ if (parts.length === 0) {
583
+ return value
584
+ }
585
+ const result = parts
586
+ .map((part, index) => {
587
+ const lower = part.toLowerCase()
588
+ if (index === 0) {
589
+ return lower
590
+ }
591
+ return lower.charAt(0).toUpperCase() + lower.slice(1)
592
+ })
593
+ .join('')
594
+ // Prefix with underscore if starts with a digit
595
+ return /^\d/.test(result) ? `_${result}` : result
596
+ }
597
+
598
+ /**
599
+ * Builds `FlowStage()` CallExpressionShapes from `sys_hub_flow_stage` records
600
+ * for the header `stages: {}` config property.
601
+ *
602
+ * Only standard stages are included (type='subflow' stages are skipped).
603
+ *
604
+ * @returns Plain object keyed by stage `value` — e.g. `{ triage: FlowStage({...}) }`.
605
+ */
606
+ export function buildStageShapes(stageRecords: Record[]): globalThis.Record<string, CallExpressionShape> {
607
+ const shapes: globalThis.Record<string, CallExpressionShape> = {}
608
+
609
+ const sorted = [...stageRecords].sort(
610
+ (a, b) => Number(a.get('order')?.getValue() ?? 0) - Number(b.get('order')?.getValue() ?? 0)
611
+ )
612
+
613
+ for (const rec of sorted) {
614
+ if (rec.get('type')?.getValue() !== 'standard') {
615
+ continue
616
+ }
617
+ const value = rec.get('value')?.ifString()?.getValue() ?? ''
618
+ if (!value) {
619
+ continue
620
+ }
621
+
622
+ // Build config properties
623
+ const props: globalThis.Record<string, unknown> = {
624
+ label: rec.get('label')?.ifString()?.getValue() ?? '',
625
+ value,
626
+ }
627
+
628
+ // Duration — only emit when not the default (zero duration)
629
+ const durationStr = rec.get('duration')?.ifString()?.getValue() ?? STAGE_DEFAULT_DURATION
630
+ if (durationStr && durationStr !== STAGE_DEFAULT_DURATION) {
631
+ try {
632
+ props['duration'] = DurationShape.from(rec, Shape.from(rec, durationStr).asString())
633
+ } catch {
634
+ /* keep default */
635
+ }
636
+ }
637
+
638
+ // alwaysShow
639
+ const alwaysShow = rec.get('always_show')?.getValue()
640
+ if (alwaysShow === true || alwaysShow === 'true') {
641
+ props['alwaysShow'] = true
642
+ }
643
+
644
+ // states — only emit when they differ from platform defaults
645
+ const statesStr = rec.get('states')?.ifString()?.getValue()
646
+ if (statesStr) {
647
+ try {
648
+ const parsed: { [k: string]: string } = JSON.parse(statesStr)
649
+ const isDefault =
650
+ Object.keys(parsed).length === Object.keys(DEFAULT_STATES).length &&
651
+ Object.entries(parsed).every(([k, v]) => DEFAULT_STATES[k] === v)
652
+ if (!isDefault) {
653
+ const stateProps: globalThis.Record<string, string> = {}
654
+ for (const [dbKey, label] of Object.entries(parsed)) {
655
+ const apiKey = REVERSE_STATE_KEY_MAP[dbKey]
656
+ if (apiKey && label) {
657
+ stateProps[apiKey] = label
658
+ }
659
+ }
660
+ if (Object.keys(stateProps).length > 0) {
661
+ props['states'] = Shape.from(rec, stateProps).asObject()
662
+ }
663
+ }
664
+ } catch {
665
+ /* skip malformed JSON */
666
+ }
667
+ }
668
+
669
+ let key = toSafeIdentifier(value)
670
+ if (key in shapes) {
671
+ let suffix = 2
672
+ while (`${key}_${suffix}` in shapes) {
673
+ suffix++
674
+ }
675
+ key = `${key}_${suffix}`
676
+ }
677
+ shapes[key] = new CallExpressionShape({
678
+ source: rec,
679
+ callee: FLOW_STAGE_CALLEE,
680
+ args: [new ObjectShape({ source: rec, properties: props })],
681
+ })
682
+ }
683
+
684
+ return shapes
685
+ }
686
+
687
+ /**
688
+ * Parses `component_indexes` from stage records and builds a map from
689
+ * action record order to the stage key(s) that should be inserted before it.
690
+ *
691
+ * component_index N means the stage belongs before the record with order = N + 1.
692
+ */
693
+ export function buildStageInsertionMap(stageRecords: Record[]): Map<number, string[]> {
694
+ const map = new Map<number, string[]>()
695
+ for (const rec of stageRecords) {
696
+ if (rec.get('type')?.getValue() !== 'standard') {
697
+ continue
698
+ }
699
+ const value = rec.get('value')?.ifString()?.getValue() ?? ''
700
+ if (!value) {
701
+ continue
702
+ }
703
+ const indexes = rec.get('component_indexes')?.ifString()?.getValue() ?? ''
704
+ if (!indexes) {
705
+ continue
706
+ }
707
+ for (const part of indexes.split(',')) {
708
+ const actionOrder = parseInt(part.trim(), 10) + 1
709
+ if (isNaN(actionOrder)) {
710
+ continue
711
+ }
712
+ const existing = map.get(actionOrder) ?? []
713
+ existing.push(toSafeIdentifier(value))
714
+ map.set(actionOrder, existing)
715
+ }
716
+ }
717
+ return map
718
+ }
719
+
720
+ /**
721
+ * Creates a `wfa.stage(params.stages.<key>)` CallExpressionShape.
722
+ */
723
+ export function createStageCallShape(stageKey: string, paramName: string, source: Source): CallExpressionShape {
724
+ return new CallExpressionShape({
725
+ source,
726
+ callee: STAGE_API_NAME,
727
+ args: [
728
+ new PropertyAccessShape({
729
+ source,
730
+ elements: [paramName, 'stages', stageKey],
731
+ }),
732
+ ],
733
+ })
734
+ }
735
+
736
+ /**
737
+ * Scans `stmts` for records whose order matches an entry in `stageInsertionMap`
738
+ * and splices `wfa.stage()` calls before them. Matched entries are removed from
739
+ * the map so they aren't inserted twice.
740
+ */
741
+ function insertStagesIntoStatements(
742
+ stmts: Shape[],
743
+ stageInsertionMap: Map<number, string[]>,
744
+ paramName: string,
745
+ source: Source,
746
+ orderFn: (rec: Record) => number
747
+ ): void {
748
+ const insertions: Array<{ before: number; keys: string[] }> = []
749
+ for (let i = 0; i < stmts.length; i++) {
750
+ const stmtSource = stmts[i]!.getSource()
751
+ if (!(stmtSource instanceof Record)) {
752
+ continue
753
+ }
754
+ const recOrder = orderFn(stmtSource)
755
+ const keys = stageInsertionMap.get(recOrder)
756
+ if (keys && keys.length > 0) {
757
+ insertions.push({ before: i, keys })
758
+ stageInsertionMap.delete(recOrder)
759
+ }
760
+ }
761
+ for (let j = insertions.length - 1; j >= 0; j--) {
762
+ const { before, keys } = insertions[j]!
763
+ const stageCalls = keys.map((k) => createStageCallShape(k, paramName, source))
764
+ stmts.splice(before, 0, ...stageCalls)
765
+ }
766
+ }
767
+
768
+ /**
769
+ * Walks through an array of body shapes and mutates flow logic bodies
770
+ * (if/elseIf/else/tryCatch) to insert `wfa.stage()` calls at positions identified
771
+ * by `stageInsertionMap`.
772
+ *
773
+ * Matched entries are removed from the map so they aren't inserted twice.
774
+ * Call this AFTER top-level stages have been handled.
775
+ */
776
+ export function insertNestedStageShapes(
777
+ shapes: Shape[],
778
+ stageInsertionMap: Map<number, string[]>,
779
+ paramName: string,
780
+ orderFn: (rec: Record) => number
781
+ ): void {
782
+ for (const shape of shapes) {
783
+ const callExpr =
784
+ shape instanceof VariableStatementShape ? (shape as VariableStatementShape).getInitializer() : shape
785
+ if (!(callExpr instanceof CallExpressionShape)) {
786
+ continue
787
+ }
788
+ const callee = callExpr.getCallee()
789
+
790
+ // Recurse into tryCatch try/catch handler bodies
791
+ if (callee === FLOW_LOGIC.TRY_CATCH) {
792
+ const handlersArg = callExpr.getArgument(1, false)
793
+ if (handlersArg.if(ObjectShape)) {
794
+ const handlers = handlersArg.as(ObjectShape)
795
+ for (const handlerKey of ['try', 'catch'] as const) {
796
+ const handler = handlers.get(handlerKey, false)
797
+ if (handler.if(ArrowFunctionShape)) {
798
+ const handlerBody = handler.as(ArrowFunctionShape)
799
+ const handlerStmts = handlerBody.getStatements()
800
+ insertStagesIntoStatements(
801
+ handlerStmts,
802
+ stageInsertionMap,
803
+ paramName,
804
+ handlerBody.getSource(),
805
+ orderFn
806
+ )
807
+ insertNestedStageShapes(handlerStmts, stageInsertionMap, paramName, orderFn)
808
+ }
809
+ }
810
+ }
811
+ continue
812
+ }
813
+
814
+ // Only recurse into conditional bodies
815
+ if (callee !== FLOW_LOGIC.IF && callee !== FLOW_LOGIC.ELSEIF && callee !== FLOW_LOGIC.ELSE) {
816
+ continue
817
+ }
818
+
819
+ const bodyArg = callExpr.getArgument(1, false)
820
+ if (!bodyArg.if(ArrowFunctionShape)) {
821
+ continue
822
+ }
823
+ const body = bodyArg.as(ArrowFunctionShape)
824
+ const stmts = body.getStatements() // mutable reference
825
+
826
+ insertStagesIntoStatements(stmts, stageInsertionMap, paramName, body.getSource(), orderFn)
827
+
828
+ // Recurse into children for deeper nesting
829
+ insertNestedStageShapes(stmts, stageInsertionMap, paramName, orderFn)
830
+ }
831
+ }