@dotbep/core 0.2.7 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/index.d.ts +7 -5
  2. package/dist/index.js +604 -596
  3. package/package.json +4 -1
  4. package/examples/01-participants.ts +0 -127
  5. package/examples/02-files.ts +0 -100
  6. package/examples/03-workflows.ts +0 -149
  7. package/examples/04-bim-uses.ts +0 -70
  8. package/examples/05-standards.ts +0 -60
  9. package/examples/06-schedule.ts +0 -124
  10. package/examples/07-loin.ts +0 -133
  11. package/examples/08-deliverables.ts +0 -126
  12. package/examples/09-notes.ts +0 -73
  13. package/examples/10-llm.ts +0 -109
  14. package/examples/11-resolved.ts +0 -133
  15. package/examples/12-history.ts +0 -166
  16. package/examples/13-engine.ts +0 -152
  17. package/examples/bep.d.ts +0 -38
  18. package/examples/example.bep +0 -0
  19. package/examples/run-all.ts +0 -38
  20. package/src/base/entity.ts +0 -148
  21. package/src/base/history.ts +0 -497
  22. package/src/base/index.ts +0 -5
  23. package/src/base/singleton.ts +0 -26
  24. package/src/entities/actions.ts +0 -25
  25. package/src/entities/adapters.ts +0 -16
  26. package/src/entities/annexes.ts +0 -17
  27. package/src/entities/assetTypes.ts +0 -30
  28. package/src/entities/automations.ts +0 -24
  29. package/src/entities/bimUses.ts +0 -50
  30. package/src/entities/deliverables.ts +0 -66
  31. package/src/entities/disciplines.ts +0 -21
  32. package/src/entities/effects.ts +0 -28
  33. package/src/entities/env.ts +0 -17
  34. package/src/entities/events.ts +0 -24
  35. package/src/entities/extensions.ts +0 -16
  36. package/src/entities/flags.ts +0 -17
  37. package/src/entities/guides.ts +0 -26
  38. package/src/entities/index.ts +0 -32
  39. package/src/entities/lbsNodes.ts +0 -193
  40. package/src/entities/lods.ts +0 -22
  41. package/src/entities/loin.ts +0 -127
  42. package/src/entities/lois.ts +0 -22
  43. package/src/entities/members.ts +0 -137
  44. package/src/entities/milestones.ts +0 -32
  45. package/src/entities/notes.ts +0 -27
  46. package/src/entities/objectives.ts +0 -17
  47. package/src/entities/phases.ts +0 -17
  48. package/src/entities/remoteData.ts +0 -17
  49. package/src/entities/resolvers.ts +0 -20
  50. package/src/entities/roles.ts +0 -29
  51. package/src/entities/softwares.ts +0 -26
  52. package/src/entities/standards.ts +0 -68
  53. package/src/entities/teams.ts +0 -42
  54. package/src/entities/workflows.ts +0 -256
  55. package/src/index.ts +0 -464
  56. package/src/runtime/Engine.ts +0 -352
  57. package/src/runtime/MemoryStorage.ts +0 -31
  58. package/src/runtime/Runtime.ts +0 -106
  59. package/src/runtime/index.ts +0 -4
  60. package/src/runtime/transitions.ts +0 -456
  61. package/src/runtime/types.ts +0 -279
  62. package/src/types/history.ts +0 -37
  63. package/src/types/index.ts +0 -24
  64. package/src/types/resolved.ts +0 -137
  65. package/src/types/schema.ts +0 -757
  66. package/src/utils/diff.ts +0 -109
  67. package/src/utils/index.ts +0 -9
  68. package/src/utils/integrity.ts +0 -108
  69. package/src/utils/lbs.ts +0 -116
  70. package/src/utils/mermaid.ts +0 -110
  71. package/src/utils/naming.ts +0 -62
  72. package/src/utils/nomenclature.ts +0 -107
  73. package/src/utils/normalize.ts +0 -35
  74. package/src/utils/raci.ts +0 -25
  75. package/src/utils/textFile.ts +0 -24
  76. package/tsconfig.json +0 -12
  77. package/vite.config.ts +0 -24
@@ -1,352 +0,0 @@
1
- import type { BEP } from '../types/schema.js'
2
- import { createInstance as _createInstance, processEvent, getNodeConfig as _getNodeConfig } from './transitions.js'
3
- import { MemoryStorage } from './MemoryStorage.js'
4
- import type { Runtime } from './Runtime.js'
5
- import type {
6
- WorkflowInstance,
7
- IncomingEvent,
8
- InstanceFilter,
9
- NodeConfig,
10
- EffectOutcome,
11
- EventResult,
12
- InstanceStore,
13
- TransitionListener,
14
- LifecycleListener,
15
- EffectFailedListener,
16
- } from './types.js'
17
-
18
- export interface EngineInitConfig {
19
- /** The runtime that accompanies the BEP — declares effects, automations, etc. */
20
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
- runtime: Runtime<any>
22
- /** Storage backend for workflow instances. Defaults to in-memory. */
23
- storage?: InstanceStore
24
- /** Event-processing options. */
25
- events?: {
26
- /** Skip RACI authorization checks on emit(). Intended for local testing only. */
27
- skipRaci?: boolean
28
- }
29
- }
30
-
31
- /**
32
- * Serializes a caught error to a plain string, safe across VM realm boundaries.
33
- * In Node.js vm.createContext(), thrown Error objects have a different prototype
34
- * chain than the host's Error — instanceof checks fail. Access .message and .name
35
- * as plain properties instead.
36
- */
37
- function serializeError(err: unknown): string {
38
- if (err == null) return 'Unknown error'
39
- if (typeof err === 'string') return err
40
- const e = err as Record<string, unknown>
41
- const name = typeof e['name'] === 'string' ? e['name'] : 'Error'
42
- const msg = typeof e['message'] === 'string' ? e['message'] : undefined
43
- if (msg !== undefined) return msg ? `${name}: ${msg}` : name
44
- try { return String(err) } catch { return 'Unknown error' }
45
- }
46
-
47
- export class Engine {
48
- private readonly getBep: () => BEP
49
- private readonly getHistoricalBep?: (version: string) => Promise<BEP>
50
-
51
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
- private runtime!: Runtime<any>
53
- private storage!: InstanceStore
54
- private skipRaci = false
55
-
56
- private readonly transitionListeners: TransitionListener[] = []
57
- private readonly createdListeners: LifecycleListener[] = []
58
- private readonly completedListeners: LifecycleListener[] = []
59
- private readonly effectFailedListeners: EffectFailedListener[] = []
60
-
61
- /**
62
- * Called internally by Bep — injects the BEP data getter and history resolver.
63
- * Use bep.engine.init() to configure the runtime and storage before operating.
64
- */
65
- constructor(getBep: () => BEP, getHistoricalBep?: (version: string) => Promise<BEP>) {
66
- this.getBep = getBep
67
- this.getHistoricalBep = getHistoricalBep
68
- }
69
-
70
- /**
71
- * Configures the engine with a runtime and storage backend.
72
- * Must be called before any operations (createInstance, emit, etc.).
73
- * Returns `this` for chaining.
74
- */
75
- init(config: EngineInitConfig): this {
76
- this.runtime = config.runtime
77
- this.storage = config.storage ?? new MemoryStorage()
78
- this.skipRaci = config.events?.skipRaci ?? false
79
- return this
80
- }
81
-
82
- // ─── Lifecycle listeners ─────────────────────────────────────────────────────
83
-
84
- /** Fires after every successful emit() — all listeners run concurrently. */
85
- onTransition(listener: TransitionListener): this {
86
- this.transitionListeners.push(listener)
87
- return this
88
- }
89
-
90
- /** Fires after createInstance() persists the new instance. */
91
- onInstanceCreated(listener: LifecycleListener): this {
92
- this.createdListeners.push(listener)
93
- return this
94
- }
95
-
96
- /** Fires when instance.status becomes 'completed'. */
97
- onInstanceCompleted(listener: LifecycleListener): this {
98
- this.completedListeners.push(listener)
99
- return this
100
- }
101
-
102
- /** Fires when an effect handler throws or returns status 'failed'. */
103
- onEffectFailed(listener: EffectFailedListener): this {
104
- this.effectFailedListeners.push(listener)
105
- return this
106
- }
107
-
108
- // ─── Operations ─────────────────────────────────────────────────────────────
109
-
110
- /**
111
- * Creates a new workflow instance positioned at the first node after start and persists it.
112
- * Records the current BEP version on the instance for historical resolution.
113
- * Returns null if the workflowId does not exist or has no start node.
114
- */
115
- async createInstance(
116
- workflowId: string,
117
- trackedAsset: WorkflowInstance['trackedAsset'],
118
- initiatedBy: string,
119
- ): Promise<WorkflowInstance | null> {
120
- this._assertInit()
121
- const bep = this.getBep()
122
- const bepVersion = 'unversioned'
123
- const result = _createInstance(bep, workflowId, trackedAsset, initiatedBy, bepVersion)
124
- if (!result) return null
125
- const { instance, startEffects } = result
126
- for (const ef of startEffects) {
127
- await this._executeEffect(instance, ef)
128
- }
129
- await this.storage.saveInstance(instance)
130
- await this._fire(this.createdListeners, instance)
131
- return instance
132
- }
133
-
134
- /**
135
- * Emits an event against a workflow instance.
136
- *
137
- * 1. Loads the instance from storage.
138
- * 2. Resolves the BEP version the instance was created against.
139
- * 3. Processes the event (pure transition logic — transitions + decision auto-traversal).
140
- * 4. Persists the updated instance.
141
- * 5. Executes effect handlers declared in the runtime.
142
- * 6. Fires lifecycle listeners concurrently.
143
- * 7. Returns the result with the updated instance and effect outcomes.
144
- */
145
- async emit(instanceId: string, event: IncomingEvent): Promise<EventResult> {
146
- this._assertInit()
147
- const instance = await this.storage.getInstance(instanceId)
148
- if (!instance) return { ok: false, error: 'NO_MATCHING_EDGE' }
149
-
150
- const bep = await this._resolveBep(instance.bepVersion)
151
- let result = processEvent(bep, instance, event, { skipRaci: this.skipRaci })
152
- if (!result.ok) return { ok: false, error: result.error, payloadErrors: result.payloadErrors }
153
-
154
- const allTransitions = [...(result.transitionsApplied ?? [])]
155
- const allEffects: EffectOutcome[] = []
156
-
157
- let current = result.instance!
158
-
159
- for (const ef of result.effectsToFire ?? []) {
160
- allEffects.push(await this._executeEffect(current, ef))
161
- }
162
-
163
- // Auto-execute automation nodes — loop in case an automation leads to another automation
164
- const MAX_SERVICE_DEPTH = 10
165
- let serviceDepth = 0
166
- while (result.automationNodePending && serviceDepth++ < MAX_SERVICE_DEPTH) {
167
- const { automationId } = result.automationNodePending
168
- const { eventId, ...automationPayload } = await this._executeAutomationNode(current, automationId)
169
-
170
- result = processEvent(bep, current, {
171
- eventId,
172
- actor: '_system',
173
- softwareId: '_system',
174
- payload: automationPayload,
175
- })
176
-
177
- if (!result.ok) break
178
-
179
- current = result.instance!
180
- allTransitions.push(...(result.transitionsApplied ?? []))
181
- for (const ef of result.effectsToFire ?? []) {
182
- allEffects.push(await this._executeEffect(current, ef))
183
- }
184
- }
185
-
186
- await this.storage.saveInstance(current)
187
-
188
- await this._fire(this.transitionListeners, current, allTransitions, allEffects)
189
- if (current.status === 'completed') {
190
- await this._fire(this.completedListeners, current)
191
- }
192
-
193
- return {
194
- ok: true,
195
- instance: current,
196
- transitionsApplied: allTransitions,
197
- effects: allEffects,
198
- }
199
- }
200
-
201
- // ─── Read ────────────────────────────────────────────────────────────────────
202
-
203
- async getInstance(instanceId: string): Promise<WorkflowInstance | null> {
204
- this._assertInit()
205
- return this.storage.getInstance(instanceId)
206
- }
207
-
208
- /**
209
- * Returns instances matching the filter.
210
- * `pendingActionFor` (Member.email) is resolved at the Engine level using
211
- * the BEP RACI data — the storage layer does not need to understand it.
212
- */
213
- async getInstances(filter?: InstanceFilter): Promise<WorkflowInstance[]> {
214
- this._assertInit()
215
- const { pendingActionFor, ...storageFilter } = filter ?? {}
216
- const instances = await this.storage.listInstances(storageFilter)
217
- if (!pendingActionFor) return instances
218
-
219
- const bep = this.getBep()
220
- const member = bep.members.find(m => m.email === pendingActionFor)
221
- if (!member) return []
222
-
223
- return instances.filter(instance => {
224
- const workflow = bep.workflows.find(w => w.id === instance.workflowId)
225
- if (!workflow) return false
226
- const node = workflow.diagram.nodes[instance.currentNodeId]
227
- if (!node) return false
228
- const raciNode = node.type === 'process' ? node : null
229
- const requiredRoleIds = [
230
- ...(raciNode?.responsibleRoleIds ?? []),
231
- ...(raciNode?.accountableRoleIds ?? []),
232
- ]
233
- return requiredRoleIds.length === 0 || requiredRoleIds.includes(member.roleId)
234
- })
235
- }
236
-
237
- /**
238
- * Returns what a specific actor can do from the current node of an instance.
239
- * Returns null if the instance does not exist.
240
- */
241
- async getNodeConfig(instanceId: string, actorEmail: string): Promise<NodeConfig | null> {
242
- this._assertInit()
243
- const instance = await this.storage.getInstance(instanceId)
244
- if (!instance) return null
245
- const bep = await this._resolveBep(instance.bepVersion)
246
- return _getNodeConfig(bep, instance, actorEmail)
247
- }
248
-
249
- async deleteInstance(instanceId: string): Promise<void> {
250
- this._assertInit()
251
- await this.storage.deleteInstance(instanceId)
252
- }
253
-
254
- /**
255
- * Runs the resolver declared for a remote data source and returns the raw payload.
256
- * Throws if the remoteDataId does not exist in the BEP or has no resolver assigned.
257
- */
258
- async getRemoteData(remoteDataId: string): Promise<unknown> {
259
- this._assertInit()
260
- const bep = this.getBep()
261
- const remote = bep.remoteData.find(r => r.id === remoteDataId)
262
- if (!remote) throw new Error(`Remote data "${remoteDataId}" not found in BEP`)
263
- if (!remote.resolverId) throw new Error(`Remote data "${remoteDataId}" has no resolver assigned`)
264
- return this.runtime._runResolver(remote.resolverId, remote.url)
265
- }
266
-
267
- /**
268
- * Runs an adapter to transform data into a lens-compatible format.
269
- * Throws if the adapterId has no registered handler.
270
- */
271
- useAdapter(adapterId: string, data: unknown): unknown {
272
- this._assertInit()
273
- return this.runtime._runAdapter(adapterId, data)
274
- }
275
-
276
- // ─── Internal ────────────────────────────────────────────────────────────────
277
-
278
- private _assertInit(): void {
279
- if (!this.runtime || !this.storage) {
280
- throw new Error('Engine not initialized — call bep.engine.init({ runtime, storage }) first.')
281
- }
282
- }
283
-
284
- private async _resolveBep(bepVersion: string): Promise<BEP> {
285
- if (this.getHistoricalBep && bepVersion !== 'unversioned') {
286
- return this.getHistoricalBep(bepVersion)
287
- }
288
- return this.getBep()
289
- }
290
-
291
- private async _fire<A extends unknown[]>(
292
- listeners: ((...args: A) => Promise<void>)[],
293
- ...args: A
294
- ): Promise<void> {
295
- await Promise.allSettled(listeners.map(fn => fn(...args)))
296
- }
297
-
298
- private _resolveFromHistory(key: string, history: WorkflowInstance['history']): unknown {
299
- for (let i = history.length - 1; i >= 0; i--) {
300
- const payload = history[i]!.trigger.payload ?? {}
301
- if (key in payload) return payload[key]
302
- }
303
- return undefined
304
- }
305
-
306
- private async _executeAutomationNode(
307
- instance: WorkflowInstance,
308
- automationId: string,
309
- ): Promise<{ eventId: string } & Record<string, unknown>> {
310
- const bep = this.getBep()
311
- const automationDef = bep.automations.find(s => s.id === automationId)
312
- const fields = automationDef?.payload ?? []
313
- const payload = Object.fromEntries(fields.map(f => [f.key, this._resolveFromHistory(f.key, instance.history)]))
314
-
315
- const handler = this.runtime.automations[automationId]
316
- if (!handler) throw new Error(`No handler declared for automation "${automationId}"`)
317
- return handler(instance, payload)
318
- }
319
-
320
- private async _executeEffect(
321
- instance: WorkflowInstance,
322
- ef: { effectId: string; fromEdgeId: string },
323
- ): Promise<EffectOutcome> {
324
- const bep = this.getBep()
325
- const effectDef = bep.effects.find(e => e.id === ef.effectId)
326
- const fields = effectDef?.payload ?? []
327
-
328
- const missing = fields
329
- .filter(f => f.required && this._resolveFromHistory(f.key, instance.history) === undefined)
330
- .map(f => f.key)
331
-
332
- if (missing.length > 0) {
333
- return { effectId: ef.effectId, fromEdgeId: ef.fromEdgeId, status: 'skipped', missingFields: missing }
334
- }
335
-
336
- const handler = this.runtime.effects[ef.effectId]
337
- if (!handler) {
338
- return { effectId: ef.effectId, fromEdgeId: ef.fromEdgeId, status: 'skipped' }
339
- }
340
-
341
- const payload = Object.fromEntries(fields.map(f => [f.key, this._resolveFromHistory(f.key, instance.history)]))
342
-
343
- try {
344
- await handler(instance, payload)
345
- return { effectId: ef.effectId, fromEdgeId: ef.fromEdgeId, status: 'executed' }
346
- } catch (error) {
347
- const outcome: EffectOutcome = { effectId: ef.effectId, fromEdgeId: ef.fromEdgeId, status: 'failed', error: serializeError(error) }
348
- await this._fire(this.effectFailedListeners, instance, outcome)
349
- return outcome
350
- }
351
- }
352
- }
@@ -1,31 +0,0 @@
1
- import type { InstanceStore, WorkflowInstance, InstanceFilter } from './types.js'
2
-
3
- /**
4
- * In-memory InstanceStore implementation.
5
- * Default storage for local development and testing.
6
- * State is lost when the process exits.
7
- */
8
- export class MemoryStorage implements InstanceStore {
9
- private readonly instances = new Map<string, WorkflowInstance>()
10
-
11
- async listInstances(filter?: InstanceFilter): Promise<WorkflowInstance[]> {
12
- let results = [...this.instances.values()]
13
- if (filter?.workflowId) results = results.filter(i => i.workflowId === filter.workflowId)
14
- if (filter?.status) results = results.filter(i => i.status === filter.status)
15
- if (filter?.trackedAssetTypeId) results = results.filter(i => i.trackedAsset.assetTypeId === filter.trackedAssetTypeId)
16
- if (filter?.trackedAssetId) results = results.filter(i => i.trackedAsset.id === filter.trackedAssetId)
17
- return results
18
- }
19
-
20
- async getInstance(instanceId: string): Promise<WorkflowInstance | null> {
21
- return this.instances.get(instanceId) ?? null
22
- }
23
-
24
- async saveInstance(instance: WorkflowInstance): Promise<void> {
25
- this.instances.set(instance.id, instance)
26
- }
27
-
28
- async deleteInstance(instanceId: string): Promise<void> {
29
- this.instances.delete(instanceId)
30
- }
31
- }
@@ -1,106 +0,0 @@
1
- import type { WorkflowInstance, EffectHandler, AutomationHandler, ResolverHandler, AdapterHandler } from './types.js'
2
-
3
- export interface BepTypes {
4
- effects: Record<string, (...args: any[]) => void>
5
- automations: Record<string, (...args: any[]) => { eventId: string } & Record<string, unknown>>
6
- resolvers: Record<string, (url: string, ...args: any[]) => unknown>
7
- adapters: Record<string, (data: unknown) => unknown>
8
- }
9
-
10
- /**
11
- * Base class for the runtime that accompanies a BEP.
12
- * Extend this class and register handlers in the constructor.
13
- * Pass the generated BepTypes as the generic parameter for full type safety.
14
- *
15
- * @example
16
- * import type { BepTypes } from './bep.js'
17
- * import * as BEP from '@dotbep/core'
18
- *
19
- * class MyRuntime extends BEP.Runtime<BepTypes> {
20
- * constructor(options: BEP.RuntimeOptions) {
21
- * super(options)
22
- * this.effect('send-email', async (instance, payload) => {
23
- * await sendEmail(this.env.SENDGRID_KEY, payload.to)
24
- * })
25
- * this.automation('check-approval', async (instance) => {
26
- * return { eventId: 'approved' }
27
- * })
28
- * this.resolver('fetch-data', async (url) => {
29
- * return fetch(url).then(r => r.json())
30
- * })
31
- * this.adapter('to-chart', (data) => data)
32
- * }
33
- * }
34
- *
35
- * bep.engine.init({ runtime: new MyRuntime({ env: process.env }) })
36
- */
37
- export interface RuntimeOptions {
38
- env?: Record<string, string>
39
- }
40
-
41
- export class Runtime<T extends {
42
- effects: Record<string, any>
43
- automations: Record<string, any>
44
- resolvers: Record<string, any>
45
- adapters: Record<string, any>
46
- } = BepTypes> {
47
- protected readonly env: Record<string, string>
48
-
49
- readonly effects: Record<string, EffectHandler> = {}
50
- readonly automations: Record<string, AutomationHandler> = {}
51
- readonly resolvers: Record<string, ResolverHandler> = {}
52
- readonly adapters: Record<string, AdapterHandler> = {}
53
-
54
- constructor({ env = {} }: RuntimeOptions = {}) {
55
- this.env = env
56
- }
57
-
58
- protected effect<K extends keyof T['effects'] & string>(
59
- key: K,
60
- handler: (instance: WorkflowInstance, ...args: Parameters<T['effects'][K]>) => Promise<void>,
61
- ): this {
62
- this.effects[key] = handler as unknown as EffectHandler
63
- return this
64
- }
65
-
66
- protected automation<K extends keyof T['automations'] & string>(
67
- key: K,
68
- handler: (instance: WorkflowInstance, ...args: Parameters<T['automations'][K]>) => Promise<ReturnType<T['automations'][K]>>,
69
- ): this {
70
- this.automations[key] = handler as unknown as AutomationHandler
71
- return this
72
- }
73
-
74
- protected resolver<K extends keyof T['resolvers'] & string>(
75
- key: K,
76
- handler: (...args: Parameters<T['resolvers'][K]>) => Promise<ReturnType<T['resolvers'][K]>>,
77
- ): this {
78
- this.resolvers[key] = handler as unknown as ResolverHandler
79
- return this
80
- }
81
-
82
- protected adapter<K extends keyof T['adapters'] & string>(
83
- key: K,
84
- handler: (...args: Parameters<T['adapters'][K]>) => ReturnType<T['adapters'][K]>,
85
- ): this {
86
- this.adapters[key] = handler as unknown as AdapterHandler
87
- return this
88
- }
89
-
90
- /** @internal Called by Engine.getRemoteData — keeps env encapsulated inside the Runtime. */
91
- _runResolver(id: string, url: string): Promise<unknown> {
92
- const handler = this.resolvers[id]
93
- if (!handler) throw new Error(`No handler declared for resolver "${id}"`)
94
- return handler(url, this.env)
95
- }
96
-
97
- /** @internal Called by Engine.useAdapter — keeps handler lookup inside the Runtime. */
98
- _runAdapter(id: string, data: unknown): unknown {
99
- const handler = this.adapters[id]
100
- if (!handler) throw new Error(`No handler declared for adapter "${id}"`)
101
- return handler(data)
102
- }
103
- }
104
-
105
- // Untyped aliases used internally by Engine (which works with the base contract)
106
- export type { EffectHandler, AutomationHandler, ResolverHandler, AdapterHandler }
@@ -1,4 +0,0 @@
1
- export * from './types.js'
2
- export * from './Runtime.js'
3
- export * from './Engine.js'
4
- export * from './MemoryStorage.js'