business-as-code 2.1.3 → 2.3.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 (260) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +2 -0
  3. package/package.json +16 -13
  4. package/src/dollar.ts +5 -2
  5. package/src/entities/organization.ts +31 -18
  6. package/src/goals.ts +78 -12
  7. package/src/index.ts +48 -18
  8. package/src/kpis.ts +62 -8
  9. package/src/metrics.ts +92 -79
  10. package/src/okrs.ts +120 -20
  11. package/src/organization.ts +12 -15
  12. package/src/process.ts +11 -12
  13. package/src/product.ts +8 -9
  14. package/src/queries.ts +238 -75
  15. package/src/roles.ts +62 -61
  16. package/src/workflow.ts +22 -15
  17. package/test/business.test.ts +282 -0
  18. package/test/dollar.test.ts +270 -0
  19. package/test/entities.test.ts +628 -0
  20. package/test/financials.test.ts +539 -0
  21. package/test/goals.test.ts +451 -0
  22. package/{src → test}/index.test.ts +1 -1
  23. package/test/kpis.test.ts +440 -0
  24. package/test/metrics.test.ts +744 -0
  25. package/test/okrs.test.ts +741 -0
  26. package/test/organization.test.ts +548 -0
  27. package/test/process.test.ts +503 -0
  28. package/test/product.test.ts +430 -0
  29. package/test/queries.test.ts +556 -0
  30. package/test/roles.test.ts +546 -0
  31. package/test/service.test.ts +450 -0
  32. package/test/types.test.ts +1141 -0
  33. package/test/vision.test.ts +214 -0
  34. package/test/workflow.test.ts +501 -0
  35. package/vitest.config.ts +47 -0
  36. package/.turbo/turbo-build.log +0 -5
  37. package/LICENSE +0 -21
  38. package/dist/business.d.ts +0 -62
  39. package/dist/business.d.ts.map +0 -1
  40. package/dist/business.js +0 -109
  41. package/dist/business.js.map +0 -1
  42. package/dist/canvas/activities.d.ts +0 -19
  43. package/dist/canvas/activities.d.ts.map +0 -1
  44. package/dist/canvas/activities.js +0 -20
  45. package/dist/canvas/activities.js.map +0 -1
  46. package/dist/canvas/channels.d.ts +0 -20
  47. package/dist/canvas/channels.d.ts.map +0 -1
  48. package/dist/canvas/channels.js +0 -21
  49. package/dist/canvas/channels.js.map +0 -1
  50. package/dist/canvas/relationships.d.ts +0 -20
  51. package/dist/canvas/relationships.d.ts.map +0 -1
  52. package/dist/canvas/relationships.js +0 -21
  53. package/dist/canvas/relationships.js.map +0 -1
  54. package/dist/canvas/resources.d.ts +0 -20
  55. package/dist/canvas/resources.d.ts.map +0 -1
  56. package/dist/canvas/resources.js +0 -30
  57. package/dist/canvas/resources.js.map +0 -1
  58. package/dist/canvas/revenue.d.ts +0 -22
  59. package/dist/canvas/revenue.d.ts.map +0 -1
  60. package/dist/canvas/revenue.js +0 -30
  61. package/dist/canvas/revenue.js.map +0 -1
  62. package/dist/canvas/segments.d.ts +0 -20
  63. package/dist/canvas/segments.d.ts.map +0 -1
  64. package/dist/canvas/segments.js +0 -28
  65. package/dist/canvas/segments.js.map +0 -1
  66. package/dist/canvas/types.d.ts +0 -232
  67. package/dist/canvas/types.d.ts.map +0 -1
  68. package/dist/canvas/types.js +0 -8
  69. package/dist/canvas/types.js.map +0 -1
  70. package/dist/canvas/value.d.ts +0 -20
  71. package/dist/canvas/value.d.ts.map +0 -1
  72. package/dist/canvas/value.js +0 -21
  73. package/dist/canvas/value.js.map +0 -1
  74. package/dist/dollar.d.ts +0 -60
  75. package/dist/dollar.d.ts.map +0 -1
  76. package/dist/dollar.js +0 -107
  77. package/dist/dollar.js.map +0 -1
  78. package/dist/entities/assets.d.ts +0 -21
  79. package/dist/entities/assets.d.ts.map +0 -1
  80. package/dist/entities/assets.js +0 -323
  81. package/dist/entities/assets.js.map +0 -1
  82. package/dist/entities/business.d.ts +0 -36
  83. package/dist/entities/business.d.ts.map +0 -1
  84. package/dist/entities/business.js +0 -370
  85. package/dist/entities/business.js.map +0 -1
  86. package/dist/entities/communication.d.ts +0 -21
  87. package/dist/entities/communication.d.ts.map +0 -1
  88. package/dist/entities/communication.js +0 -255
  89. package/dist/entities/communication.js.map +0 -1
  90. package/dist/entities/customers.d.ts +0 -58
  91. package/dist/entities/customers.d.ts.map +0 -1
  92. package/dist/entities/customers.js +0 -989
  93. package/dist/entities/customers.js.map +0 -1
  94. package/dist/entities/financials.d.ts +0 -59
  95. package/dist/entities/financials.d.ts.map +0 -1
  96. package/dist/entities/financials.js +0 -932
  97. package/dist/entities/financials.js.map +0 -1
  98. package/dist/entities/goals.d.ts +0 -58
  99. package/dist/entities/goals.d.ts.map +0 -1
  100. package/dist/entities/goals.js +0 -800
  101. package/dist/entities/goals.js.map +0 -1
  102. package/dist/entities/index.d.ts +0 -299
  103. package/dist/entities/index.d.ts.map +0 -1
  104. package/dist/entities/index.js +0 -198
  105. package/dist/entities/index.js.map +0 -1
  106. package/dist/entities/legal.d.ts +0 -21
  107. package/dist/entities/legal.d.ts.map +0 -1
  108. package/dist/entities/legal.js +0 -301
  109. package/dist/entities/legal.js.map +0 -1
  110. package/dist/entities/market.d.ts +0 -21
  111. package/dist/entities/market.d.ts.map +0 -1
  112. package/dist/entities/market.js +0 -301
  113. package/dist/entities/market.js.map +0 -1
  114. package/dist/entities/marketing.d.ts +0 -67
  115. package/dist/entities/marketing.d.ts.map +0 -1
  116. package/dist/entities/marketing.js +0 -1157
  117. package/dist/entities/marketing.js.map +0 -1
  118. package/dist/entities/offerings.d.ts +0 -51
  119. package/dist/entities/offerings.d.ts.map +0 -1
  120. package/dist/entities/offerings.js +0 -727
  121. package/dist/entities/offerings.js.map +0 -1
  122. package/dist/entities/operations.d.ts +0 -58
  123. package/dist/entities/operations.d.ts.map +0 -1
  124. package/dist/entities/operations.js +0 -787
  125. package/dist/entities/operations.js.map +0 -1
  126. package/dist/entities/organization.d.ts +0 -57
  127. package/dist/entities/organization.d.ts.map +0 -1
  128. package/dist/entities/organization.js +0 -807
  129. package/dist/entities/organization.js.map +0 -1
  130. package/dist/entities/partnerships.d.ts +0 -21
  131. package/dist/entities/partnerships.d.ts.map +0 -1
  132. package/dist/entities/partnerships.js +0 -300
  133. package/dist/entities/partnerships.js.map +0 -1
  134. package/dist/entities/planning.d.ts +0 -0
  135. package/dist/entities/planning.d.ts.map +0 -1
  136. package/dist/entities/planning.js +0 -271
  137. package/dist/entities/planning.js.map +0 -1
  138. package/dist/entities/projects.d.ts +0 -25
  139. package/dist/entities/projects.d.ts.map +0 -1
  140. package/dist/entities/projects.js +0 -349
  141. package/dist/entities/projects.js.map +0 -1
  142. package/dist/entities/risk.d.ts +0 -21
  143. package/dist/entities/risk.d.ts.map +0 -1
  144. package/dist/entities/risk.js +0 -293
  145. package/dist/entities/risk.js.map +0 -1
  146. package/dist/entities/sales.d.ts +0 -72
  147. package/dist/entities/sales.d.ts.map +0 -1
  148. package/dist/entities/sales.js +0 -1248
  149. package/dist/entities/sales.js.map +0 -1
  150. package/dist/financials.d.ts +0 -130
  151. package/dist/financials.d.ts.map +0 -1
  152. package/dist/financials.js +0 -297
  153. package/dist/financials.js.map +0 -1
  154. package/dist/goals.d.ts +0 -87
  155. package/dist/goals.d.ts.map +0 -1
  156. package/dist/goals.js +0 -215
  157. package/dist/goals.js.map +0 -1
  158. package/dist/index.d.ts +0 -97
  159. package/dist/index.d.ts.map +0 -1
  160. package/dist/index.js +0 -132
  161. package/dist/index.js.map +0 -1
  162. package/dist/kpis.d.ts +0 -118
  163. package/dist/kpis.d.ts.map +0 -1
  164. package/dist/kpis.js +0 -232
  165. package/dist/kpis.js.map +0 -1
  166. package/dist/metrics.d.ts +0 -448
  167. package/dist/metrics.d.ts.map +0 -1
  168. package/dist/metrics.js +0 -325
  169. package/dist/metrics.js.map +0 -1
  170. package/dist/okrs.d.ts +0 -123
  171. package/dist/okrs.d.ts.map +0 -1
  172. package/dist/okrs.js +0 -269
  173. package/dist/okrs.js.map +0 -1
  174. package/dist/organization.d.ts +0 -585
  175. package/dist/organization.d.ts.map +0 -1
  176. package/dist/organization.js +0 -173
  177. package/dist/organization.js.map +0 -1
  178. package/dist/process.d.ts +0 -112
  179. package/dist/process.d.ts.map +0 -1
  180. package/dist/process.js +0 -241
  181. package/dist/process.js.map +0 -1
  182. package/dist/product.d.ts +0 -85
  183. package/dist/product.d.ts.map +0 -1
  184. package/dist/product.js +0 -145
  185. package/dist/product.js.map +0 -1
  186. package/dist/queries.d.ts +0 -304
  187. package/dist/queries.d.ts.map +0 -1
  188. package/dist/queries.js +0 -415
  189. package/dist/queries.js.map +0 -1
  190. package/dist/roles.d.ts +0 -340
  191. package/dist/roles.d.ts.map +0 -1
  192. package/dist/roles.js +0 -255
  193. package/dist/roles.js.map +0 -1
  194. package/dist/service.d.ts +0 -61
  195. package/dist/service.d.ts.map +0 -1
  196. package/dist/service.js +0 -140
  197. package/dist/service.js.map +0 -1
  198. package/dist/types.d.ts +0 -459
  199. package/dist/types.d.ts.map +0 -1
  200. package/dist/types.js +0 -5
  201. package/dist/types.js.map +0 -1
  202. package/dist/vision.d.ts +0 -38
  203. package/dist/vision.d.ts.map +0 -1
  204. package/dist/vision.js +0 -68
  205. package/dist/vision.js.map +0 -1
  206. package/dist/workflow.d.ts +0 -115
  207. package/dist/workflow.d.ts.map +0 -1
  208. package/dist/workflow.js +0 -247
  209. package/dist/workflow.js.map +0 -1
  210. package/src/business.js +0 -108
  211. package/src/canvas/activities.ts +0 -32
  212. package/src/canvas/canvas.ts +0 -482
  213. package/src/canvas/channels.ts +0 -34
  214. package/src/canvas/costs.ts +0 -43
  215. package/src/canvas/economics.ts +0 -99
  216. package/src/canvas/index.ts +0 -206
  217. package/src/canvas/partnerships.ts +0 -34
  218. package/src/canvas/projections.ts +0 -141
  219. package/src/canvas/relationships.ts +0 -34
  220. package/src/canvas/resources.ts +0 -43
  221. package/src/canvas/revenue.ts +0 -56
  222. package/src/canvas/segments.ts +0 -42
  223. package/src/canvas/types.ts +0 -363
  224. package/src/canvas/value.ts +0 -34
  225. package/src/dollar.js +0 -106
  226. package/src/entities/assets.js +0 -322
  227. package/src/entities/business.js +0 -369
  228. package/src/entities/communication.js +0 -254
  229. package/src/entities/customers.js +0 -988
  230. package/src/entities/financials.js +0 -931
  231. package/src/entities/goals.js +0 -799
  232. package/src/entities/index.js +0 -197
  233. package/src/entities/legal.js +0 -300
  234. package/src/entities/market.js +0 -300
  235. package/src/entities/marketing.js +0 -1156
  236. package/src/entities/offerings.js +0 -726
  237. package/src/entities/operations.js +0 -786
  238. package/src/entities/organization.js +0 -806
  239. package/src/entities/partnerships.js +0 -299
  240. package/src/entities/planning.js +0 -270
  241. package/src/entities/projects.js +0 -348
  242. package/src/entities/risk.js +0 -292
  243. package/src/entities/sales.js +0 -1247
  244. package/src/financials.js +0 -296
  245. package/src/goals.js +0 -214
  246. package/src/index.js +0 -131
  247. package/src/index.test.js +0 -274
  248. package/src/kpis.js +0 -231
  249. package/src/metrics.js +0 -324
  250. package/src/okrs.js +0 -268
  251. package/src/organization.js +0 -172
  252. package/src/process.js +0 -240
  253. package/src/product.js +0 -144
  254. package/src/queries.js +0 -414
  255. package/src/roles.js +0 -254
  256. package/src/service.js +0 -139
  257. package/src/types.js +0 -4
  258. package/src/vision.js +0 -67
  259. package/src/workflow.js +0 -246
  260. package/tests/canvas.test.ts +0 -842
package/src/roles.ts CHANGED
@@ -9,6 +9,13 @@
9
9
  * @packageDocumentation
10
10
  */
11
11
 
12
+ import type { Role as OrgRole, RoleType as OrgRoleType, RoleWorkerType } from 'org.ai'
13
+ import type { Worker, WorkerRef } from 'digital-workers'
14
+
15
+ // Re-export for convenience
16
+ export type { Worker, WorkerRef } from 'digital-workers'
17
+ export type { OrgRole, OrgRoleType, RoleWorkerType }
18
+
12
19
  // =============================================================================
13
20
  // Business Role - Bridges Worker Role and Authorization
14
21
  // =============================================================================
@@ -43,7 +50,10 @@ export type BusinessRoleType =
43
50
  | string
44
51
 
45
52
  /**
46
- * Business Role - extends WorkerRole with authorization and task capabilities
53
+ * Business Role - extends org.ai Role with authorization and task capabilities
54
+ *
55
+ * Composes with org.ai Role to provide additional business-specific properties
56
+ * like authorization permissions, task capabilities, and compensation.
47
57
  *
48
58
  * @example
49
59
  * ```ts
@@ -54,6 +64,9 @@ export type BusinessRoleType =
54
64
  * department: 'Engineering',
55
65
  * description: 'Leads engineering team and makes technical decisions',
56
66
  *
67
+ * // From org.ai Role
68
+ * skills: ['TypeScript', 'Architecture', 'Team Leadership'],
69
+ *
57
70
  * // Business responsibilities
58
71
  * responsibilities: [
59
72
  * 'Lead engineering team',
@@ -78,28 +91,10 @@ export type BusinessRoleType =
78
91
  * }
79
92
  * ```
80
93
  */
81
- export interface BusinessRole {
82
- /** Unique role identifier */
83
- id: string
84
-
85
- /** Role display name */
86
- name: string
87
-
88
- /** Role type classification */
94
+ export interface BusinessRole extends Omit<OrgRole, 'permissions'> {
95
+ /** Role type classification (overrides OrgRole.type with business-specific types) */
89
96
  type: BusinessRoleType
90
97
 
91
- /** Department or team */
92
- department?: string
93
-
94
- /** Human-readable description */
95
- description?: string
96
-
97
- /** Key responsibilities */
98
- responsibilities?: string[]
99
-
100
- /** Required skills */
101
- skills?: string[]
102
-
103
98
  /**
104
99
  * Authorization permissions by resource type
105
100
  *
@@ -107,6 +102,8 @@ export interface BusinessRole {
107
102
  * - 'read', 'edit', 'delete', 'manage' (standard)
108
103
  * - 'act:*' or 'act:verb' (domain-specific verbs)
109
104
  *
105
+ * Note: This differs from OrgRole.permissions which is string[]
106
+ *
110
107
  * @example
111
108
  * ```ts
112
109
  * permissions: {
@@ -118,32 +115,8 @@ export interface BusinessRole {
118
115
  */
119
116
  permissions?: Record<string, string[]>
120
117
 
121
- /** Task types this role can handle */
122
- canHandle?: string[]
123
-
124
- /** Task types this role can delegate to others */
125
- canDelegate?: string[]
126
-
127
- /** Request types this role can approve */
128
- canApprove?: string[]
129
-
130
- /** Escalation path - role to escalate to */
131
- escalateTo?: string
132
-
133
- /** Reports to - manager role */
134
- reportsTo?: string
135
-
136
- /** Worker type preference: 'ai' | 'human' | 'hybrid' */
137
- workerType?: 'ai' | 'human' | 'hybrid'
138
-
139
- /** Level in hierarchy (1 = entry, higher = more senior) */
140
- level?: number
141
-
142
118
  /** Compensation band */
143
119
  compensationBand?: string
144
-
145
- /** Additional metadata */
146
- metadata?: Record<string, unknown>
147
120
  }
148
121
 
149
122
  // =============================================================================
@@ -263,6 +236,8 @@ export interface TaskAssignment {
263
236
 
264
237
  /**
265
238
  * Reference to an assignee (worker, team, or role)
239
+ *
240
+ * Compatible with digital-workers WorkerRef for worker assignments.
266
241
  */
267
242
  export interface AssigneeRef {
268
243
  /** Type of assignee */
@@ -273,6 +248,34 @@ export interface AssigneeRef {
273
248
  name?: string
274
249
  }
275
250
 
251
+ /**
252
+ * Convert a digital-workers WorkerRef to an AssigneeRef
253
+ */
254
+ export function workerRefToAssignee(workerRef: WorkerRef): AssigneeRef {
255
+ const result: AssigneeRef = {
256
+ type: 'worker',
257
+ id: workerRef.id,
258
+ }
259
+ if (workerRef.name !== undefined) {
260
+ result.name = workerRef.name
261
+ }
262
+ return result
263
+ }
264
+
265
+ /**
266
+ * Convert an AssigneeRef to a digital-workers WorkerRef (if type is 'worker')
267
+ */
268
+ export function assigneeToWorkerRef(assignee: AssigneeRef): WorkerRef | null {
269
+ if (assignee.type !== 'worker') return null
270
+ const result: WorkerRef = {
271
+ id: assignee.id,
272
+ }
273
+ if (assignee.name !== undefined) {
274
+ result.name = assignee.name
275
+ }
276
+ return result
277
+ }
278
+
276
279
  // =============================================================================
277
280
  // Role-Based Task Routing
278
281
  // =============================================================================
@@ -549,8 +552,8 @@ export function createBusinessRole(
549
552
 
550
553
  return {
551
554
  id,
552
- name: standard.name || template,
553
- type: standard.type || template,
555
+ name: standard['name'] || template,
556
+ type: standard['type'] || template,
554
557
  ...standard,
555
558
  ...overrides,
556
559
  } as BusinessRole
@@ -559,11 +562,7 @@ export function createBusinessRole(
559
562
  /**
560
563
  * Check if a role has permission for an action on a resource type
561
564
  */
562
- export function hasPermission(
563
- role: BusinessRole,
564
- resourceType: string,
565
- action: string
566
- ): boolean {
565
+ export function hasPermission(role: BusinessRole, resourceType: string, action: string): boolean {
567
566
  if (!role.permissions) return false
568
567
 
569
568
  // Check wildcard permissions
@@ -595,24 +594,24 @@ export function hasPermission(
595
594
  * Check if a role can handle a task type
596
595
  */
597
596
  export function canHandleTask(role: BusinessRole, taskType: string): boolean {
598
- if (!role.canHandle) return false
599
- return role.canHandle.includes(taskType) || role.canHandle.includes('*')
597
+ if (!role['canHandle']) return false
598
+ return role['canHandle'].includes(taskType) || role['canHandle'].includes('*')
600
599
  }
601
600
 
602
601
  /**
603
602
  * Check if a role can approve a request type
604
603
  */
605
604
  export function canApproveRequest(role: BusinessRole, requestType: string): boolean {
606
- if (!role.canApprove) return false
607
- return role.canApprove.includes(requestType) || role.canApprove.includes('*')
605
+ if (!role['canApprove']) return false
606
+ return role['canApprove'].includes(requestType) || role['canApprove'].includes('*')
608
607
  }
609
608
 
610
609
  /**
611
610
  * Check if a role can delegate a task type
612
611
  */
613
612
  export function canDelegateTask(role: BusinessRole, taskType: string): boolean {
614
- if (!role.canDelegate) return false
615
- return role.canDelegate.includes(taskType) || role.canDelegate.includes('*')
613
+ if (!role['canDelegate']) return false
614
+ return role['canDelegate'].includes(taskType) || role['canDelegate'].includes('*')
616
615
  }
617
616
 
618
617
  /**
@@ -623,7 +622,7 @@ export function findRoleForTask(
623
622
  rules: TaskRoutingRule[],
624
623
  context?: { amount?: number; skills?: string[] }
625
624
  ): TaskRoutingRule | undefined {
626
- const matchingRules = rules.filter(rule => rule.taskType === taskType)
625
+ const matchingRules = rules.filter((rule) => rule.taskType === taskType)
627
626
 
628
627
  if (matchingRules.length === 0) return undefined
629
628
 
@@ -633,7 +632,7 @@ export function findRoleForTask(
633
632
  if (rule.escalateAbove && context.amount > rule.escalateAbove) {
634
633
  // Find the escalated rule
635
634
  const escalatedRule = rules.find(
636
- r => r.taskType === taskType && r.requiredRole === rule.escalateTo
635
+ (r) => r.taskType === taskType && r.requiredRole === rule.escalateTo
637
636
  )
638
637
  if (escalatedRule) return escalatedRule
639
638
  }
@@ -651,7 +650,9 @@ export function createTaskAssignment(
651
650
  taskId: string,
652
651
  taskType: string,
653
652
  assignee: AssigneeRef,
654
- options?: Partial<Omit<TaskAssignment, 'id' | 'taskId' | 'taskType' | 'assignee' | 'status' | 'assignedAt'>>
653
+ options?: Partial<
654
+ Omit<TaskAssignment, 'id' | 'taskId' | 'taskType' | 'assignee' | 'status' | 'assignedAt'>
655
+ >
655
656
  ): TaskAssignment {
656
657
  return {
657
658
  id: `assign_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
package/src/workflow.ts CHANGED
@@ -88,20 +88,23 @@ export function getActionsByType(
88
88
  workflow: WorkflowDefinition,
89
89
  type: WorkflowAction['type']
90
90
  ): WorkflowAction[] {
91
- return workflow.actions?.filter(action => action.type === type) || []
91
+ return workflow.actions?.filter((action) => action.type === type) || []
92
92
  }
93
93
 
94
94
  /**
95
95
  * Get conditional actions
96
96
  */
97
97
  export function getConditionalActions(workflow: WorkflowDefinition): WorkflowAction[] {
98
- return workflow.actions?.filter(action => action.condition) || []
98
+ return workflow.actions?.filter((action) => action.condition) || []
99
99
  }
100
100
 
101
101
  /**
102
102
  * Add action to workflow
103
103
  */
104
- export function addAction(workflow: WorkflowDefinition, action: WorkflowAction): WorkflowDefinition {
104
+ export function addAction(
105
+ workflow: WorkflowDefinition,
106
+ action: WorkflowAction
107
+ ): WorkflowDefinition {
105
108
  return {
106
109
  ...workflow,
107
110
  actions: [...(workflow.actions || []), action],
@@ -112,10 +115,10 @@ export function addAction(workflow: WorkflowDefinition, action: WorkflowAction):
112
115
  * Remove action from workflow
113
116
  */
114
117
  export function removeAction(workflow: WorkflowDefinition, order: number): WorkflowDefinition {
115
- return {
116
- ...workflow,
117
- actions: workflow.actions?.filter(a => a.order !== order),
118
- }
118
+ const actions = workflow.actions?.filter((a) => a.order !== order)
119
+ const result: WorkflowDefinition = { ...workflow }
120
+ if (actions !== undefined) result.actions = actions
121
+ return result
119
122
  }
120
123
 
121
124
  /**
@@ -126,14 +129,13 @@ export function updateAction(
126
129
  order: number,
127
130
  updates: Partial<WorkflowAction>
128
131
  ): WorkflowDefinition {
129
- const actions = workflow.actions?.map(action =>
132
+ const actions = workflow.actions?.map((action) =>
130
133
  action.order === order ? { ...action, ...updates } : action
131
134
  )
132
135
 
133
- return {
134
- ...workflow,
135
- actions,
136
- }
136
+ const result: WorkflowDefinition = { ...workflow }
137
+ if (actions !== undefined) result.actions = actions
138
+ return result
137
139
  }
138
140
 
139
141
  /**
@@ -161,7 +163,9 @@ export function isWebhookTrigger(trigger: WorkflowTrigger): boolean {
161
163
  * Parse wait duration to milliseconds
162
164
  */
163
165
  export function parseWaitDuration(duration: string): number {
164
- const match = duration.match(/(\d+)\s*(ms|millisecond|milliseconds|s|second|seconds|m|minute|minutes|h|hour|hours|d|day|days)/)
166
+ const match = duration.match(
167
+ /(\d+)\s*(ms|millisecond|milliseconds|s|second|seconds|m|minute|minutes|h|hour|hours|d|day|days)/
168
+ )
165
169
  if (!match) return 0
166
170
 
167
171
  const value = parseInt(match[1] || '0', 10)
@@ -234,7 +238,10 @@ function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
234
238
  /**
235
239
  * Validate workflow definition
236
240
  */
237
- export function validateWorkflow(workflow: WorkflowDefinition): { valid: boolean; errors: string[] } {
241
+ export function validateWorkflow(workflow: WorkflowDefinition): {
242
+ valid: boolean
243
+ errors: string[]
244
+ } {
238
245
  const errors: string[] = []
239
246
 
240
247
  if (!workflow.name) {
@@ -267,7 +274,7 @@ export function validateWorkflow(workflow: WorkflowDefinition): { valid: boolean
267
274
  orders.add(action.order)
268
275
 
269
276
  // Validate action-specific requirements
270
- if (action.type === 'wait' && !action.params?.duration) {
277
+ if (action.type === 'wait' && !action.params?.['duration']) {
271
278
  errors.push(`Wait action at order ${action.order} must specify duration`)
272
279
  }
273
280
  }
@@ -0,0 +1,282 @@
1
+ /**
2
+ * Tests for business.ts - Business entity definition and management
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest'
6
+ import {
7
+ Business,
8
+ getTotalBudget,
9
+ getTotalTeamSize,
10
+ getDepartment,
11
+ getTeam,
12
+ validateBusiness,
13
+ } from '../src/business.js'
14
+ import type { BusinessDefinition } from '../src/types.js'
15
+
16
+ describe('Business', () => {
17
+ describe('Business()', () => {
18
+ it('should create a business entity with required fields', () => {
19
+ const business = Business({
20
+ name: 'Acme Corp',
21
+ })
22
+
23
+ expect(business.name).toBe('Acme Corp')
24
+ expect(business.values).toEqual([])
25
+ expect(business.teamSize).toBe(0)
26
+ expect(business.metadata).toEqual({})
27
+ expect(business.foundedAt).toBeInstanceOf(Date)
28
+ })
29
+
30
+ it('should create a business entity with all fields', () => {
31
+ const foundedDate = new Date('2020-01-01')
32
+ const business = Business({
33
+ name: 'Test Corp',
34
+ description: 'A test company',
35
+ industry: 'Technology',
36
+ mission: 'To innovate',
37
+ values: ['Innovation', 'Integrity'],
38
+ targetMarket: 'Enterprise',
39
+ foundedAt: foundedDate,
40
+ teamSize: 50,
41
+ structure: {
42
+ departments: [
43
+ {
44
+ name: 'Engineering',
45
+ head: 'Jane Smith',
46
+ members: ['Alice', 'Bob'],
47
+ budget: 1000000,
48
+ },
49
+ ],
50
+ },
51
+ metadata: { customField: 'value' },
52
+ })
53
+
54
+ expect(business.name).toBe('Test Corp')
55
+ expect(business.description).toBe('A test company')
56
+ expect(business.industry).toBe('Technology')
57
+ expect(business.mission).toBe('To innovate')
58
+ expect(business.values).toEqual(['Innovation', 'Integrity'])
59
+ expect(business.targetMarket).toBe('Enterprise')
60
+ expect(business.foundedAt).toBe(foundedDate)
61
+ expect(business.teamSize).toBe(50)
62
+ expect(business.structure?.departments).toHaveLength(1)
63
+ expect(business.metadata).toEqual({ customField: 'value' })
64
+ })
65
+
66
+ it('should throw error if name is empty', () => {
67
+ expect(() => Business({ name: '' })).toThrow('Business name is required')
68
+ })
69
+
70
+ it('should preserve provided values and not override with defaults', () => {
71
+ const business = Business({
72
+ name: 'Test',
73
+ values: ['Custom'],
74
+ teamSize: 100,
75
+ })
76
+
77
+ expect(business.values).toEqual(['Custom'])
78
+ expect(business.teamSize).toBe(100)
79
+ })
80
+ })
81
+
82
+ describe('getTotalBudget()', () => {
83
+ it('should return 0 if no departments', () => {
84
+ const business = Business({ name: 'Test' })
85
+ expect(getTotalBudget(business)).toBe(0)
86
+ })
87
+
88
+ it('should return 0 if structure has no departments', () => {
89
+ const business = Business({
90
+ name: 'Test',
91
+ structure: {},
92
+ })
93
+ expect(getTotalBudget(business)).toBe(0)
94
+ })
95
+
96
+ it('should calculate total budget across departments', () => {
97
+ const business = Business({
98
+ name: 'Test',
99
+ structure: {
100
+ departments: [
101
+ { name: 'Engineering', budget: 1000000 },
102
+ { name: 'Sales', budget: 500000 },
103
+ { name: 'HR' }, // No budget specified
104
+ ],
105
+ },
106
+ })
107
+
108
+ expect(getTotalBudget(business)).toBe(1500000)
109
+ })
110
+
111
+ it('should handle departments with zero budget', () => {
112
+ const business = Business({
113
+ name: 'Test',
114
+ structure: {
115
+ departments: [
116
+ { name: 'Engineering', budget: 1000000 },
117
+ { name: 'Sales', budget: 0 },
118
+ ],
119
+ },
120
+ })
121
+
122
+ expect(getTotalBudget(business)).toBe(1000000)
123
+ })
124
+ })
125
+
126
+ describe('getTotalTeamSize()', () => {
127
+ it('should return teamSize if no departments', () => {
128
+ const business = Business({ name: 'Test', teamSize: 50 })
129
+ expect(getTotalTeamSize(business)).toBe(50)
130
+ })
131
+
132
+ it('should return 0 if no teamSize and no departments', () => {
133
+ const business = Business({ name: 'Test' })
134
+ expect(getTotalTeamSize(business)).toBe(0)
135
+ })
136
+
137
+ it('should calculate total team size from department members', () => {
138
+ const business = Business({
139
+ name: 'Test',
140
+ structure: {
141
+ departments: [
142
+ { name: 'Engineering', members: ['Alice', 'Bob', 'Charlie'] },
143
+ { name: 'Sales', members: ['David', 'Eve'] },
144
+ { name: 'HR' }, // No members
145
+ ],
146
+ },
147
+ })
148
+
149
+ expect(getTotalTeamSize(business)).toBe(5)
150
+ })
151
+ })
152
+
153
+ describe('getDepartment()', () => {
154
+ const business = Business({
155
+ name: 'Test',
156
+ structure: {
157
+ departments: [
158
+ { name: 'Engineering', head: 'Jane' },
159
+ { name: 'Sales', head: 'John' },
160
+ ],
161
+ },
162
+ })
163
+
164
+ it('should find department by name', () => {
165
+ const dept = getDepartment(business, 'Engineering')
166
+ expect(dept?.name).toBe('Engineering')
167
+ expect(dept?.head).toBe('Jane')
168
+ })
169
+
170
+ it('should return undefined for non-existent department', () => {
171
+ const dept = getDepartment(business, 'Marketing')
172
+ expect(dept).toBeUndefined()
173
+ })
174
+
175
+ it('should return undefined if no structure', () => {
176
+ const simpleBusiness = Business({ name: 'Simple' })
177
+ const dept = getDepartment(simpleBusiness, 'Engineering')
178
+ expect(dept).toBeUndefined()
179
+ })
180
+ })
181
+
182
+ describe('getTeam()', () => {
183
+ const business = Business({
184
+ name: 'Test',
185
+ structure: {
186
+ teams: [
187
+ { name: 'Alpha', lead: 'Alice' },
188
+ { name: 'Beta', lead: 'Bob' },
189
+ ],
190
+ },
191
+ })
192
+
193
+ it('should find team by name', () => {
194
+ const team = getTeam(business, 'Alpha')
195
+ expect(team?.name).toBe('Alpha')
196
+ expect(team?.lead).toBe('Alice')
197
+ })
198
+
199
+ it('should return undefined for non-existent team', () => {
200
+ const team = getTeam(business, 'Gamma')
201
+ expect(team).toBeUndefined()
202
+ })
203
+
204
+ it('should return undefined if no structure', () => {
205
+ const simpleBusiness = Business({ name: 'Simple' })
206
+ const team = getTeam(simpleBusiness, 'Alpha')
207
+ expect(team).toBeUndefined()
208
+ })
209
+ })
210
+
211
+ describe('validateBusiness()', () => {
212
+ it('should validate valid business', () => {
213
+ const business = Business({
214
+ name: 'Valid Corp',
215
+ teamSize: 50,
216
+ })
217
+
218
+ const result = validateBusiness(business)
219
+ expect(result.valid).toBe(true)
220
+ expect(result.errors).toHaveLength(0)
221
+ })
222
+
223
+ it('should fail validation if name is missing', () => {
224
+ const business: BusinessDefinition = { name: '' }
225
+ const result = validateBusiness(business)
226
+
227
+ expect(result.valid).toBe(false)
228
+ expect(result.errors).toContain('Business name is required')
229
+ })
230
+
231
+ it('should fail validation if teamSize is negative', () => {
232
+ const business: BusinessDefinition = { name: 'Test', teamSize: -5 }
233
+ const result = validateBusiness(business)
234
+
235
+ expect(result.valid).toBe(false)
236
+ expect(result.errors).toContain('Team size cannot be negative')
237
+ })
238
+
239
+ it('should fail validation if department has no name', () => {
240
+ const business: BusinessDefinition = {
241
+ name: 'Test',
242
+ structure: {
243
+ departments: [{ name: '', budget: 1000 }],
244
+ },
245
+ }
246
+ const result = validateBusiness(business)
247
+
248
+ expect(result.valid).toBe(false)
249
+ expect(result.errors).toContain('Department name is required')
250
+ })
251
+
252
+ it('should fail validation if department budget is negative', () => {
253
+ const business: BusinessDefinition = {
254
+ name: 'Test',
255
+ structure: {
256
+ departments: [{ name: 'Engineering', budget: -1000 }],
257
+ },
258
+ }
259
+ const result = validateBusiness(business)
260
+
261
+ expect(result.valid).toBe(false)
262
+ expect(result.errors).toContain('Department Engineering budget cannot be negative')
263
+ })
264
+
265
+ it('should return multiple errors for multiple issues', () => {
266
+ const business: BusinessDefinition = {
267
+ name: '',
268
+ teamSize: -5,
269
+ structure: {
270
+ departments: [
271
+ { name: '', budget: 1000 },
272
+ { name: 'Sales', budget: -500 },
273
+ ],
274
+ },
275
+ }
276
+ const result = validateBusiness(business)
277
+
278
+ expect(result.valid).toBe(false)
279
+ expect(result.errors.length).toBeGreaterThan(1)
280
+ })
281
+ })
282
+ })