@dotbep/core 0.2.7 → 0.2.9
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.
- package/dist/index.d.ts +7 -40
- package/dist/index.js +1142 -1179
- package/package.json +4 -1
- package/examples/01-participants.ts +0 -127
- package/examples/02-files.ts +0 -100
- package/examples/03-workflows.ts +0 -149
- package/examples/04-bim-uses.ts +0 -70
- package/examples/05-standards.ts +0 -60
- package/examples/06-schedule.ts +0 -124
- package/examples/07-loin.ts +0 -133
- package/examples/08-deliverables.ts +0 -126
- package/examples/09-notes.ts +0 -73
- package/examples/10-llm.ts +0 -109
- package/examples/11-resolved.ts +0 -133
- package/examples/12-history.ts +0 -166
- package/examples/13-engine.ts +0 -152
- package/examples/bep.d.ts +0 -38
- package/examples/example.bep +0 -0
- package/examples/run-all.ts +0 -38
- package/src/base/entity.ts +0 -148
- package/src/base/history.ts +0 -497
- package/src/base/index.ts +0 -5
- package/src/base/singleton.ts +0 -26
- package/src/entities/actions.ts +0 -25
- package/src/entities/adapters.ts +0 -16
- package/src/entities/annexes.ts +0 -17
- package/src/entities/assetTypes.ts +0 -30
- package/src/entities/automations.ts +0 -24
- package/src/entities/bimUses.ts +0 -50
- package/src/entities/deliverables.ts +0 -66
- package/src/entities/disciplines.ts +0 -21
- package/src/entities/effects.ts +0 -28
- package/src/entities/env.ts +0 -17
- package/src/entities/events.ts +0 -24
- package/src/entities/extensions.ts +0 -16
- package/src/entities/flags.ts +0 -17
- package/src/entities/guides.ts +0 -26
- package/src/entities/index.ts +0 -32
- package/src/entities/lbsNodes.ts +0 -193
- package/src/entities/lods.ts +0 -22
- package/src/entities/loin.ts +0 -127
- package/src/entities/lois.ts +0 -22
- package/src/entities/members.ts +0 -137
- package/src/entities/milestones.ts +0 -32
- package/src/entities/notes.ts +0 -27
- package/src/entities/objectives.ts +0 -17
- package/src/entities/phases.ts +0 -17
- package/src/entities/remoteData.ts +0 -17
- package/src/entities/resolvers.ts +0 -20
- package/src/entities/roles.ts +0 -29
- package/src/entities/softwares.ts +0 -26
- package/src/entities/standards.ts +0 -68
- package/src/entities/teams.ts +0 -42
- package/src/entities/workflows.ts +0 -256
- package/src/index.ts +0 -464
- package/src/runtime/Engine.ts +0 -352
- package/src/runtime/MemoryStorage.ts +0 -31
- package/src/runtime/Runtime.ts +0 -106
- package/src/runtime/index.ts +0 -4
- package/src/runtime/transitions.ts +0 -456
- package/src/runtime/types.ts +0 -279
- package/src/types/history.ts +0 -37
- package/src/types/index.ts +0 -24
- package/src/types/resolved.ts +0 -137
- package/src/types/schema.ts +0 -757
- package/src/utils/diff.ts +0 -109
- package/src/utils/index.ts +0 -9
- package/src/utils/integrity.ts +0 -108
- package/src/utils/lbs.ts +0 -116
- package/src/utils/mermaid.ts +0 -110
- package/src/utils/naming.ts +0 -62
- package/src/utils/nomenclature.ts +0 -107
- package/src/utils/normalize.ts +0 -35
- package/src/utils/raci.ts +0 -25
- package/src/utils/textFile.ts +0 -24
- package/tsconfig.json +0 -12
- package/vite.config.ts +0 -24
|
@@ -1,456 +0,0 @@
|
|
|
1
|
-
// Pure workflow engine — no I/O, no side effects.
|
|
2
|
-
// Takes BEP schema + instance state, returns new state + effects to fire.
|
|
3
|
-
|
|
4
|
-
import type { BEP, FlowEdge, EdgeGuard } from '../types/schema.js'
|
|
5
|
-
import type {
|
|
6
|
-
IncomingEvent,
|
|
7
|
-
TransitionEvent,
|
|
8
|
-
WorkflowInstance,
|
|
9
|
-
InstanceStatus,
|
|
10
|
-
NodeConfig,
|
|
11
|
-
RoleRef,
|
|
12
|
-
TeamRef,
|
|
13
|
-
RaciLevel,
|
|
14
|
-
ProcessEventError,
|
|
15
|
-
TransitionStep,
|
|
16
|
-
PayloadFieldError,
|
|
17
|
-
} from './types.js'
|
|
18
|
-
|
|
19
|
-
// Safety limit to prevent infinite loops in malformed decision chains.
|
|
20
|
-
const MAX_DECISION_DEPTH = 10
|
|
21
|
-
|
|
22
|
-
// ─── Guard evaluation ─────────────────────────────────────────────────────────
|
|
23
|
-
|
|
24
|
-
/** Evaluates a guard condition against an event payload. Pure. */
|
|
25
|
-
export function evaluateGuard(guard: EdgeGuard, payload: Record<string, unknown>): boolean {
|
|
26
|
-
const val = payload[guard.field]
|
|
27
|
-
switch (guard.operator) {
|
|
28
|
-
case 'exists': return val !== undefined && val !== null
|
|
29
|
-
case 'eq': return val === guard.value
|
|
30
|
-
case 'neq': return val !== guard.value
|
|
31
|
-
case 'gt': return typeof val === 'number' && typeof guard.value === 'number' && val > guard.value
|
|
32
|
-
case 'lt': return typeof val === 'number' && typeof guard.value === 'number' && val < guard.value
|
|
33
|
-
case 'contains':
|
|
34
|
-
if (typeof val === 'string' && typeof guard.value === 'string') return val.includes(guard.value)
|
|
35
|
-
if (Array.isArray(val)) return (val as unknown[]).includes(guard.value)
|
|
36
|
-
return false
|
|
37
|
-
default: return false
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// ─── Authorization ────────────────────────────────────────────────────────────
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Returns true if the actor satisfies the R or A constraints of a process node.
|
|
45
|
-
* If no R or A is defined, the node is open to anyone.
|
|
46
|
-
*/
|
|
47
|
-
function isActorAuthorized(bep: BEP, nodeId: string, workflowId: string, actorEmail: string): boolean {
|
|
48
|
-
const workflow = bep.workflows.find(w => w.id === workflowId)
|
|
49
|
-
const node = workflow?.diagram.nodes[nodeId]
|
|
50
|
-
if (!node || node.type !== 'process') return true
|
|
51
|
-
|
|
52
|
-
const hasResponsible = !!(node.responsibleRoleIds?.length || node.responsibleTeamIds?.length || node.responsibleEmails?.length)
|
|
53
|
-
const hasAccountable = !!(node.accountableRoleIds?.length || node.accountableTeamIds?.length || node.accountableEmails?.length)
|
|
54
|
-
if (!hasResponsible && !hasAccountable) return true
|
|
55
|
-
|
|
56
|
-
const member = bep.members.find(m => m.email === actorEmail)
|
|
57
|
-
const actorRoleId = member?.roleId
|
|
58
|
-
const actorTeamIds = new Set(bep.teams.filter(t => (t.memberEmails ?? []).includes(actorEmail)).map(t => t.id))
|
|
59
|
-
|
|
60
|
-
const matches = (roleIds?: string[], teamIds?: string[], emails?: string[]): boolean => {
|
|
61
|
-
if (emails?.includes(actorEmail)) return true
|
|
62
|
-
const hasRoles = !!roleIds?.length
|
|
63
|
-
const hasTeams = !!teamIds?.length
|
|
64
|
-
if (hasTeams && hasRoles)
|
|
65
|
-
return !!actorRoleId && roleIds!.includes(actorRoleId) && teamIds!.some(tid => actorTeamIds.has(tid))
|
|
66
|
-
if (hasTeams) return teamIds!.some(tid => actorTeamIds.has(tid))
|
|
67
|
-
if (hasRoles) return !!actorRoleId && roleIds!.includes(actorRoleId)
|
|
68
|
-
return false
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return (
|
|
72
|
-
(hasResponsible && matches(node.responsibleRoleIds, node.responsibleTeamIds, node.responsibleEmails)) ||
|
|
73
|
-
(hasAccountable && matches(node.accountableRoleIds, node.accountableTeamIds, node.accountableEmails))
|
|
74
|
-
)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// ─── Payload validation ───────────────────────────────────────────────────────
|
|
78
|
-
|
|
79
|
-
const JS_TYPE: Record<string, string> = { string: 'string', number: 'number', boolean: 'boolean' }
|
|
80
|
-
|
|
81
|
-
function validatePayload(
|
|
82
|
-
bep: BEP,
|
|
83
|
-
eventId: string,
|
|
84
|
-
payload: Record<string, unknown> | undefined,
|
|
85
|
-
): PayloadFieldError[] {
|
|
86
|
-
const def = bep.events.find(e => e.id === eventId)
|
|
87
|
-
if (!def?.payload?.length) return []
|
|
88
|
-
|
|
89
|
-
const errors: PayloadFieldError[] = []
|
|
90
|
-
const incoming = payload ?? {}
|
|
91
|
-
|
|
92
|
-
for (const field of def.payload) {
|
|
93
|
-
const val = incoming[field.key]
|
|
94
|
-
if (val === undefined || val === null) {
|
|
95
|
-
if (field.required) errors.push({ field: field.key, reason: 'missing' })
|
|
96
|
-
} else {
|
|
97
|
-
const expected = JS_TYPE[field.type]
|
|
98
|
-
if (expected && typeof val !== expected) {
|
|
99
|
-
errors.push({ field: field.key, reason: 'wrong_type' })
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const declaredKeys = new Set(def.payload.map(f => f.key))
|
|
105
|
-
for (const key of Object.keys(incoming)) {
|
|
106
|
-
if (!declaredKeys.has(key)) errors.push({ field: key, reason: 'unknown_field' })
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return errors
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ─── Edge matching ────────────────────────────────────────────────────────────
|
|
113
|
-
|
|
114
|
-
function edgeMatchesEvent(edge: FlowEdge, event: IncomingEvent): boolean {
|
|
115
|
-
if (!('triggerEventId' in edge)) return false
|
|
116
|
-
if (edge.triggerEventId !== event.eventId) return false
|
|
117
|
-
return true
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// ─── Internal result type ─────────────────────────────────────────────────────
|
|
121
|
-
|
|
122
|
-
/** Internal result of processEvent — used by Engine to build EventResult. */
|
|
123
|
-
export interface ProcessEventResult {
|
|
124
|
-
ok: boolean
|
|
125
|
-
/** Updated instance. Present when ok = true. */
|
|
126
|
-
instance?: WorkflowInstance
|
|
127
|
-
/** Ordered list of transitions applied (may include decision auto-traversals). */
|
|
128
|
-
transitionsApplied?: TransitionStep[]
|
|
129
|
-
/** Effects to execute after the transition. Caller is responsible for firing them. */
|
|
130
|
-
effectsToFire?: { effectId: string; fromEdgeId: string }[]
|
|
131
|
-
/** Present when the final node is an automation node — Engine must execute the automation then emit the returned eventId. */
|
|
132
|
-
automationNodePending?: { nodeId: string; automationId: string }
|
|
133
|
-
error?: ProcessEventError
|
|
134
|
-
/** Present when error = 'INVALID_PAYLOAD'. */
|
|
135
|
-
payloadErrors?: PayloadFieldError[]
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ─── Create instance ──────────────────────────────────────────────────────────
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Creates a new WorkflowInstance and advances it past the start node.
|
|
142
|
-
*
|
|
143
|
-
* The start node is always a visual anchor — it has no RACI, no action, and no
|
|
144
|
-
* trigger event on its outgoing edge. The instance is immediately moved to the
|
|
145
|
-
* first node after start so that the first emit() acts on the real first step.
|
|
146
|
-
*
|
|
147
|
-
* Returns null if the workflow is not found or has no start node.
|
|
148
|
-
*/
|
|
149
|
-
export function createInstance(
|
|
150
|
-
bep: BEP,
|
|
151
|
-
workflowId: string,
|
|
152
|
-
trackedAsset: WorkflowInstance['trackedAsset'],
|
|
153
|
-
initiatedBy: string,
|
|
154
|
-
bepVersion: string,
|
|
155
|
-
): { instance: WorkflowInstance; startEffects: { effectId: string; fromEdgeId: string }[] } | null {
|
|
156
|
-
const workflow = bep.workflows.find(w => w.id === workflowId)
|
|
157
|
-
if (!workflow) return null
|
|
158
|
-
|
|
159
|
-
const startNodeId = Object.keys(workflow.diagram.nodes).find(
|
|
160
|
-
k => workflow.diagram.nodes[k]!.type === 'start',
|
|
161
|
-
)
|
|
162
|
-
if (!startNodeId) return null
|
|
163
|
-
|
|
164
|
-
// Advance past the start node — find its single outgoing edge (no trigger required).
|
|
165
|
-
const startEdgeEntry = Object.entries(workflow.diagram.edges).find(([, e]) => e.from === startNodeId)
|
|
166
|
-
const firstNodeId = startEdgeEntry?.[1].to ?? startNodeId
|
|
167
|
-
const startEffects = startEdgeEntry
|
|
168
|
-
? (startEdgeEntry[1].effectIds ?? []).map(effectId => ({ effectId, fromEdgeId: startEdgeEntry[0] }))
|
|
169
|
-
: []
|
|
170
|
-
|
|
171
|
-
const now = new Date().toISOString()
|
|
172
|
-
return {
|
|
173
|
-
instance: {
|
|
174
|
-
id: globalThis.crypto.randomUUID(),
|
|
175
|
-
workflowId,
|
|
176
|
-
bepVersion,
|
|
177
|
-
trackedAsset,
|
|
178
|
-
currentNodeId: firstNodeId,
|
|
179
|
-
status: 'active',
|
|
180
|
-
history: [],
|
|
181
|
-
createdAt: now,
|
|
182
|
-
updatedAt: now,
|
|
183
|
-
initiatedBy,
|
|
184
|
-
},
|
|
185
|
-
startEffects,
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// ─── Process event ────────────────────────────────────────────────────────────
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Processes an incoming event against a workflow instance.
|
|
193
|
-
*
|
|
194
|
-
* - Finds the matching outgoing edge from the current node.
|
|
195
|
-
* - Auto-traverses decision nodes using the same event payload.
|
|
196
|
-
* - Returns the updated instance and the effects to fire. Pure — does not mutate.
|
|
197
|
-
*/
|
|
198
|
-
export function processEvent(
|
|
199
|
-
bep: BEP,
|
|
200
|
-
instance: WorkflowInstance,
|
|
201
|
-
event: IncomingEvent,
|
|
202
|
-
options?: { skipRaci?: boolean },
|
|
203
|
-
): ProcessEventResult {
|
|
204
|
-
if (instance.status !== 'active') {
|
|
205
|
-
return { ok: false, error: 'INSTANCE_NOT_ACTIVE' }
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (!options?.skipRaci && !isActorAuthorized(bep, instance.currentNodeId, instance.workflowId, event.actor)) {
|
|
209
|
-
return { ok: false, error: 'UNAUTHORIZED' }
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const workflow = bep.workflows.find(w => w.id === instance.workflowId)
|
|
213
|
-
if (!workflow) return { ok: false, error: 'NO_MATCHING_EDGE' }
|
|
214
|
-
|
|
215
|
-
const { nodes, edges } = workflow.diagram
|
|
216
|
-
|
|
217
|
-
// Working state — assembled immutably into the final instance at the end.
|
|
218
|
-
let currentNodeId = instance.currentNodeId
|
|
219
|
-
const newHistory: TransitionEvent[] = []
|
|
220
|
-
const effectsToFire: { effectId: string; fromEdgeId: string }[] = []
|
|
221
|
-
const transitionsApplied: TransitionStep[] = []
|
|
222
|
-
|
|
223
|
-
// ── Step 1: match an edge from the current node ───────────────────────────
|
|
224
|
-
|
|
225
|
-
const candidates = Object.entries(edges).filter(
|
|
226
|
-
([, e]) => e.from === currentNodeId && edgeMatchesEvent(e, event),
|
|
227
|
-
)
|
|
228
|
-
|
|
229
|
-
if (candidates.length === 0) return { ok: false, error: 'NO_MATCHING_EDGE' }
|
|
230
|
-
if (candidates.length > 1) return { ok: false, error: 'AMBIGUOUS_TRANSITION' }
|
|
231
|
-
|
|
232
|
-
const [edgeId, edge] = candidates[0]!
|
|
233
|
-
|
|
234
|
-
if ('triggerEventId' in edge) {
|
|
235
|
-
const payloadErrors = validatePayload(bep, edge.triggerEventId, event.payload)
|
|
236
|
-
if (payloadErrors.length > 0) return { ok: false, error: 'INVALID_PAYLOAD', payloadErrors }
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
newHistory.push(buildTransitionEvent(edgeId, currentNodeId, edge.to, event))
|
|
240
|
-
effectsToFire.push(...(edge.effectIds ?? []).map(effectId => ({ effectId, fromEdgeId: edgeId })))
|
|
241
|
-
transitionsApplied.push({ edgeId, fromNodeId: currentNodeId, toNodeId: edge.to })
|
|
242
|
-
currentNodeId = edge.to
|
|
243
|
-
|
|
244
|
-
// ── Step 2: auto-traverse decision nodes ─────────────────────────────────
|
|
245
|
-
//
|
|
246
|
-
// Decision nodes are never stable resting points — the engine evaluates their
|
|
247
|
-
// outgoing edges immediately using the original event's payload.
|
|
248
|
-
// Guards on outgoing decision edges should be mutually exclusive.
|
|
249
|
-
|
|
250
|
-
let depth = 0
|
|
251
|
-
while (nodes[currentNodeId]?.type === 'decision') {
|
|
252
|
-
if (++depth > MAX_DECISION_DEPTH) return { ok: false, error: 'DECISION_LOOP' }
|
|
253
|
-
|
|
254
|
-
const outgoing = Object.entries(edges).filter(([, e]) => {
|
|
255
|
-
if (e.from !== currentNodeId) return false
|
|
256
|
-
if (!('guard' in e)) return false
|
|
257
|
-
return evaluateGuard(e.guard, event.payload ?? {})
|
|
258
|
-
})
|
|
259
|
-
|
|
260
|
-
// No branch matches — leave instance on the decision node (diagram error).
|
|
261
|
-
if (outgoing.length === 0) break
|
|
262
|
-
|
|
263
|
-
// Take first matching branch — guards are expected to be mutually exclusive.
|
|
264
|
-
const [decEdgeId, decEdge] = outgoing[0]!
|
|
265
|
-
|
|
266
|
-
newHistory.push(buildTransitionEvent(decEdgeId, currentNodeId, decEdge.to, event, true))
|
|
267
|
-
effectsToFire.push(...(decEdge.effectIds ?? []).map(effectId => ({ effectId, fromEdgeId: decEdgeId })))
|
|
268
|
-
transitionsApplied.push({ edgeId: decEdgeId, fromNodeId: currentNodeId, toNodeId: decEdge.to })
|
|
269
|
-
currentNodeId = decEdge.to
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// ── Step 3: compute final status ─────────────────────────────────────────
|
|
273
|
-
|
|
274
|
-
const finalNode = nodes[currentNodeId]
|
|
275
|
-
const newStatus: InstanceStatus = finalNode?.type === 'end' ? 'completed' : 'active'
|
|
276
|
-
|
|
277
|
-
const updatedInstance: WorkflowInstance = {
|
|
278
|
-
...instance,
|
|
279
|
-
currentNodeId,
|
|
280
|
-
status: newStatus,
|
|
281
|
-
history: [...instance.history, ...newHistory],
|
|
282
|
-
updatedAt: new Date().toISOString(),
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const automationNodePending = finalNode?.type === 'automation' && finalNode.automationId
|
|
286
|
-
? { nodeId: currentNodeId, automationId: finalNode.automationId }
|
|
287
|
-
: undefined
|
|
288
|
-
|
|
289
|
-
return { ok: true, instance: updatedInstance, transitionsApplied, effectsToFire, automationNodePending }
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// ─── Node config ──────────────────────────────────────────────────────────────
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Returns what a specific actor can do from the current node of an instance.
|
|
296
|
-
* Used by apps to render only the actions available to the logged-in user.
|
|
297
|
-
*/
|
|
298
|
-
export function getNodeConfig(
|
|
299
|
-
bep: BEP,
|
|
300
|
-
instance: WorkflowInstance,
|
|
301
|
-
actorEmail: string,
|
|
302
|
-
): NodeConfig {
|
|
303
|
-
const workflow = bep.workflows.find(w => w.id === instance.workflowId)!
|
|
304
|
-
const { nodes, edges } = workflow.diagram
|
|
305
|
-
const currentNode = nodes[instance.currentNodeId]!
|
|
306
|
-
|
|
307
|
-
// Resolve actor profile.
|
|
308
|
-
const member = bep.members.find(m => m.email === actorEmail)
|
|
309
|
-
const actorRoleId = member?.roleId
|
|
310
|
-
const actorTeamIds = new Set(
|
|
311
|
-
bep.teams.filter(t => (t.memberEmails ?? []).includes(actorEmail)).map(t => t.id)
|
|
312
|
-
)
|
|
313
|
-
|
|
314
|
-
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
315
|
-
|
|
316
|
-
const resolveRoles = (ids?: string[]): RoleRef[] =>
|
|
317
|
-
(ids ?? []).flatMap(id => {
|
|
318
|
-
const role = bep.roles.find(r => r.id === id)
|
|
319
|
-
return role ? [{ id: role.id, name: role.name }] : []
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
const resolveTeams = (ids?: string[]): TeamRef[] =>
|
|
323
|
-
(ids ?? []).flatMap(id => {
|
|
324
|
-
const team = bep.teams.find(t => t.id === id)
|
|
325
|
-
return team ? [{ id: team.id, name: team.name }] : []
|
|
326
|
-
})
|
|
327
|
-
|
|
328
|
-
const buildRaciLevel = (
|
|
329
|
-
roleIds?: string[], teamIds?: string[], emails?: string[],
|
|
330
|
-
): RaciLevel => ({
|
|
331
|
-
roles: resolveRoles(roleIds),
|
|
332
|
-
teams: resolveTeams(teamIds),
|
|
333
|
-
emails: emails ?? [],
|
|
334
|
-
})
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* Three-level authorization check for a single RACI letter (R or A).
|
|
338
|
-
* Only call this when the letter has at least one constraint defined.
|
|
339
|
-
* 1. Email match — explicit member, always authorized.
|
|
340
|
-
* 2. Team + Role — actor must be in a listed team AND have a listed role.
|
|
341
|
-
* 3. Team only — actor must be in a listed team.
|
|
342
|
-
* 4. Role only — actor must have a listed role.
|
|
343
|
-
*/
|
|
344
|
-
const matchesConstraints = (
|
|
345
|
-
roleIds?: string[], teamIds?: string[], emails?: string[],
|
|
346
|
-
): boolean => {
|
|
347
|
-
if (emails?.includes(actorEmail)) return true
|
|
348
|
-
const hasRoles = !!roleIds?.length
|
|
349
|
-
const hasTeams = !!teamIds?.length
|
|
350
|
-
if (hasTeams && hasRoles) {
|
|
351
|
-
return (
|
|
352
|
-
!!actorRoleId && roleIds!.includes(actorRoleId) &&
|
|
353
|
-
teamIds!.some(tid => actorTeamIds.has(tid))
|
|
354
|
-
)
|
|
355
|
-
}
|
|
356
|
-
if (hasTeams) return teamIds!.some(tid => actorTeamIds.has(tid))
|
|
357
|
-
if (hasRoles) return !!actorRoleId && roleIds!.includes(actorRoleId)
|
|
358
|
-
return false
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const raciNode = currentNode.type === 'process' ? currentNode : null
|
|
362
|
-
|
|
363
|
-
const hasResponsible = !!(
|
|
364
|
-
raciNode?.responsibleRoleIds?.length ||
|
|
365
|
-
raciNode?.responsibleTeamIds?.length ||
|
|
366
|
-
raciNode?.responsibleEmails?.length
|
|
367
|
-
)
|
|
368
|
-
const hasAccountable = !!(
|
|
369
|
-
raciNode?.accountableRoleIds?.length ||
|
|
370
|
-
raciNode?.accountableTeamIds?.length ||
|
|
371
|
-
raciNode?.accountableEmails?.length
|
|
372
|
-
)
|
|
373
|
-
|
|
374
|
-
// No R or A defined on the node → open to anyone.
|
|
375
|
-
// Otherwise actor must satisfy at least one of the defined constraints.
|
|
376
|
-
const actorIsAuthorized =
|
|
377
|
-
(!hasResponsible && !hasAccountable) ||
|
|
378
|
-
(hasResponsible && matchesConstraints(raciNode?.responsibleRoleIds, raciNode?.responsibleTeamIds, raciNode?.responsibleEmails)) ||
|
|
379
|
-
(hasAccountable && matchesConstraints(raciNode?.accountableRoleIds, raciNode?.accountableTeamIds, raciNode?.accountableEmails))
|
|
380
|
-
|
|
381
|
-
// Resolve required payload fields from the global FlowEvent catalog.
|
|
382
|
-
const resolvePayload = (eventId: string) =>
|
|
383
|
-
(bep.events.find(e => e.id === eventId)?.payload ?? []).map(p => ({
|
|
384
|
-
key: p.key,
|
|
385
|
-
type: p.type,
|
|
386
|
-
required: p.required,
|
|
387
|
-
}))
|
|
388
|
-
|
|
389
|
-
const availableTransitions: NodeConfig['availableTransitions'] = []
|
|
390
|
-
const blockedTransitions: NodeConfig['blockedTransitions'] = []
|
|
391
|
-
|
|
392
|
-
for (const [edgeId, edge] of Object.entries(edges)) {
|
|
393
|
-
if (edge.from !== instance.currentNodeId) continue
|
|
394
|
-
if (!('triggerEventId' in edge)) continue
|
|
395
|
-
|
|
396
|
-
const eventId = edge.triggerEventId
|
|
397
|
-
|
|
398
|
-
if (actorIsAuthorized) {
|
|
399
|
-
availableTransitions.push({
|
|
400
|
-
edgeId,
|
|
401
|
-
label: edge.label ?? eventId,
|
|
402
|
-
emits: eventId,
|
|
403
|
-
requiredPayload: resolvePayload(eventId),
|
|
404
|
-
})
|
|
405
|
-
} else {
|
|
406
|
-
blockedTransitions.push({
|
|
407
|
-
edgeId,
|
|
408
|
-
label: edge.label ?? eventId,
|
|
409
|
-
reason: 'UNAUTHORIZED',
|
|
410
|
-
required: buildRaciLevel(
|
|
411
|
-
[...(raciNode?.responsibleRoleIds ?? []), ...(raciNode?.accountableRoleIds ?? [])],
|
|
412
|
-
[...(raciNode?.responsibleTeamIds ?? []), ...(raciNode?.accountableTeamIds ?? [])],
|
|
413
|
-
[...(raciNode?.responsibleEmails ?? []), ...(raciNode?.accountableEmails ?? [])],
|
|
414
|
-
),
|
|
415
|
-
})
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
return {
|
|
420
|
-
currentNode: {
|
|
421
|
-
id: instance.currentNodeId,
|
|
422
|
-
type: currentNode.type,
|
|
423
|
-
label: instance.currentNodeId,
|
|
424
|
-
},
|
|
425
|
-
availableTransitions,
|
|
426
|
-
blockedTransitions,
|
|
427
|
-
raci: {
|
|
428
|
-
responsible: buildRaciLevel(raciNode?.responsibleRoleIds, raciNode?.responsibleTeamIds, raciNode?.responsibleEmails),
|
|
429
|
-
accountable: buildRaciLevel(raciNode?.accountableRoleIds, raciNode?.accountableTeamIds, raciNode?.accountableEmails),
|
|
430
|
-
consulted: buildRaciLevel(raciNode?.consultedRoleIds, raciNode?.consultedTeamIds, raciNode?.consultedEmails),
|
|
431
|
-
informed: buildRaciLevel(raciNode?.informedRoleIds, raciNode?.informedTeamIds, raciNode?.informedEmails),
|
|
432
|
-
},
|
|
433
|
-
isTerminal: currentNode.type === 'end',
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// ─── Internal helpers ─────────────────────────────────────────────────────────
|
|
438
|
-
|
|
439
|
-
function buildTransitionEvent(
|
|
440
|
-
edgeId: string,
|
|
441
|
-
fromNodeId: string,
|
|
442
|
-
toNodeId: string,
|
|
443
|
-
trigger: IncomingEvent,
|
|
444
|
-
auto?: boolean,
|
|
445
|
-
): TransitionEvent {
|
|
446
|
-
return {
|
|
447
|
-
id: globalThis.crypto.randomUUID(),
|
|
448
|
-
edgeId,
|
|
449
|
-
fromNodeId,
|
|
450
|
-
toNodeId,
|
|
451
|
-
trigger,
|
|
452
|
-
actor: trigger.actor,
|
|
453
|
-
timestamp: new Date().toISOString(),
|
|
454
|
-
...(auto ? { auto: true } : {}),
|
|
455
|
-
}
|
|
456
|
-
}
|