dal-engine-core-js-lib-dev 0.0.4 → 0.0.5

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.esm.js CHANGED
@@ -1,23 +1,49 @@
1
- /**
2
- * Base class for all objects in design.
3
- */
1
+ class DALEngineError extends Error {
2
+ constructor (message) {
3
+ super(message);
4
+ }
5
+ }
6
+
7
+ class GraphWithNameExistsError extends DALEngineError {
8
+ constructor (name) {
9
+ let msg = `Graph with name "${name}" already exists.`;
10
+ super(msg);
11
+ }
12
+ }
13
+
14
+ class UnknownGraph extends DALEngineError {
15
+ constructor (name) {
16
+ let msg = `Graph with name "${name}" does not exist.`;
17
+ super(msg);
18
+ }
19
+ }
20
+
4
21
  class Base {
5
22
  /**
6
- * Initialize the base class.
23
+ * Initialize the base class of all members of the design.
24
+ * The dal_engine_uid is a unique identifier for each instance.
25
+ * When the design is serialized, the dal_engine_uid is used
26
+ * to identify when the design is being loaded from file.
7
27
  * @param {String} name
8
28
  */
9
29
  constructor () {
10
- this.uid = crypto.randomUUID();
30
+ this.dal_engine_uid = crypto.randomUUID();
11
31
  }
12
32
 
33
+ /**
34
+ * Returns the type of object as specifed in TYPES.js.
35
+ * (e.g. participant, behavior, invariant, graph node, etc.)
36
+ * @returns {Number} The type of node.
37
+ */
13
38
  getType () {
14
39
  return this.type;
15
40
  }
16
41
  }
17
42
 
18
- class DALEngineError extends Error {
19
- constructor (message) {
20
- super(message);
43
+ class BehaviorAlreadyExistsError extends DALEngineError {
44
+ constructor (name) {
45
+ let msg = `Node with Behavior name "${name}" already exists in the graph.`;
46
+ super(msg);
21
47
  }
22
48
  }
23
49
 
@@ -31,6 +57,18 @@ class InvalidTransitionError extends DALEngineError {
31
57
  }
32
58
  }
33
59
 
60
+ class MissingAttributes extends DALEngineError {
61
+ constructor (type, attribute) {
62
+ let msg;
63
+ if (Array.isArray(attribute) && attribute.length > 1) {
64
+ msg = `"${type}" must be initialized with the attributes "${attribute}".`;
65
+ } else {
66
+ msg = `"${type}" must be initialized with the attribute "${attribute}".`;
67
+ }
68
+ super(msg);
69
+ }
70
+ }
71
+
34
72
  class UnknownBehaviorError extends DALEngineError {
35
73
  constructor (behaviorName) {
36
74
  super(`The behavior named "${behaviorName}" was not found in graph.`);
@@ -40,38 +78,69 @@ class UnknownBehaviorError extends DALEngineError {
40
78
  }
41
79
  }
42
80
 
43
- class MissingAttributes extends DALEngineError {
44
- constructor (type, attribute) {
45
- let msg;
46
- if (Array.isArray(attribute) && attribute.length > 1) {
47
- msg = `"${type}" must be initialized with the attributes "${attribute}".`;
48
- } else {
49
- msg = `"${type}" must be initialized with the attribute "${attribute}".`;
50
- }
81
+ /**
82
+ * Checks if the provided object was loaded from a file. This is determind by
83
+ * checking if the argument is an object and has a "dal_engine_uid" attribute,
84
+ * which is added to all objects when they are created and is written to file
85
+ * when the object is serialized.
86
+ *
87
+ * @param {*} obj The object to check.
88
+ * @returns {Boolean} True if the object was loaded from file, false otherwise.
89
+ */
90
+ const isLoadedFromFile = (obj) => {
91
+ return typeof obj === "object" && obj !== null
92
+ && !Array.isArray(obj) && Object.hasOwn(obj, "dal_engine_uid");
93
+ };
94
+
95
+ class ParticipantAlreadyExistsError extends DALEngineError {
96
+ constructor (name) {
97
+ let msg = `Participant with name "${name}" already exists in the behavior.`;
51
98
  super(msg);
52
99
  }
53
100
  }
54
101
 
102
+ class UnknownParticipantError extends DALEngineError {
103
+ constructor (participantName) {
104
+ super(`The participant named "${participantName}" was not found in the behavior.`);
105
+ }
106
+ }
107
+
55
108
  let ENGINE_TYPES$1 = {
56
109
  BEHAVIOR: 1,
57
110
  INVARIANT: 2,
58
111
  PARTICIPANT: 3,
59
- PRIMITIVE: 4,
60
- BEHAVIORAL_CONTROL_GRAPH: 5,
61
- GRAPH_NODE: 6,
112
+ BEHAVIORAL_CONTROL_GRAPH: 4,
113
+ GRAPH_NODE: 5,
62
114
  };
63
115
  ENGINE_TYPES$1 = Object.freeze(ENGINE_TYPES$1);
64
116
 
65
117
  var ENGINE_TYPES = ENGINE_TYPES$1;
66
118
 
67
- /**
68
- * Class representing a Invariant in the design.
69
- */
70
119
  class Invariant extends Base {
71
120
  /**
72
- * Initialize the Invariant.
73
- * @param {String} name
74
- * @param args
121
+ * Class representing a Invariant in the design. Invariants are rules that
122
+ * define a valid world state for participants in behaviors. The invariants
123
+ * are used to check if the design has entered a semantically invalid state
124
+ * during execution.
125
+ *
126
+ * The expected attributes in args are:
127
+ * - name: Name of the invariant.
128
+ * - rule: The rule that defines the how to enforce the invariant. This
129
+ * will be formally defined in a collection of invariant rules that can
130
+ * be chosen from when creating an invariant.
131
+ *
132
+ * Currently, the only supported invariant rule is the string min length
133
+ * rule, which can be specified as follows:
134
+ * {
135
+ * "name": "MinLengthConstraint",
136
+ * "rule": {
137
+ * "type": "minLength",
138
+ * "keys": ["value", "name"],
139
+ * "value": 1,
140
+ * },
141
+ * }
142
+ *
143
+ * @param {Object} args The arguments to initialize the invariant with.
75
144
  */
76
145
  constructor (args) {
77
146
  super();
@@ -79,17 +148,13 @@ class Invariant extends Base {
79
148
  this.invariantViolated = false;
80
149
  this.invariantType = null;
81
150
  this.traceId = null;
82
- if (typeof args === "object" && Object.hasOwn(args, "uid")) {
83
- this._loadInvariantFromJSON(args);
84
- } else {
85
- this._loadArgs(args);
86
- }
151
+ (isLoadedFromFile(args) ? this._loadFromFile(args) : this._loadArgs(args));
87
152
  }
88
153
 
89
154
  /**
90
- * Loads the provided arguments.
155
+ * Loads the invariant from the provided arguments.
91
156
  * @throws {MissingAttributes} Thrown when required attr is not present.
92
- * @param {Object} args
157
+ * @param {Object} args The arguments to initialize the invariant with.
93
158
  */
94
159
  _loadArgs (args) {
95
160
  const expectedAttributes = ["name", "rule"];
@@ -106,10 +171,10 @@ class Invariant extends Base {
106
171
  }
107
172
 
108
173
  /**
109
- * Loads the participant from a JSON object.
110
- * @param {Object} invariantJSON
174
+ * Loads the invariant from file.
175
+ * @param {Object} invariantJSON The JSON object to load the invariant from.
111
176
  */
112
- _loadInvariantFromJSON (invariantJSON) {
177
+ _loadFromFile (invariantJSON) {
113
178
  for (const [key, value] of Object.entries(invariantJSON)) {
114
179
  this[key] = value;
115
180
  } // Reset these because they are set by the execution
@@ -118,9 +183,10 @@ class Invariant extends Base {
118
183
  }
119
184
 
120
185
  /**
121
- * Evaluate the invariant.
122
- * @param {*} value
123
- * @returns {Boolean}
186
+ * Evaluate the invariant by applying the invariant rule to the provided
187
+ * value. Returns a flag indicating whether the invariant was violated.
188
+ * @param {*} value The value to evaluate the invariant on.
189
+ * @returns {Boolean} Returns flag indicating if invariant was violated.
124
190
  */
125
191
  evaluate (value) {
126
192
  this.invariantViolated = false;
@@ -131,8 +197,8 @@ class Invariant extends Base {
131
197
  }
132
198
 
133
199
  /**
134
- * Enforce the string min length invariant
135
- * @param value
200
+ * Enforce the string min length invariant.
201
+ * @param {*} value The value to enforce the invariant on.
136
202
  */
137
203
  enforceMinLength (value) {
138
204
  if ("keys" in this.rule) {
@@ -168,7 +234,7 @@ class Invariant extends Base {
168
234
  * automated testing.
169
235
  *
170
236
  * Substrate invariants correspond to a trace that represents
171
- * an environment that reveals a limitation of the substrate,
237
+ * an environment which reveals a limitation of the substrate,
172
238
  * thus motivating the invariant. It is also the environment
173
239
  * in which an implementation can prove that it respects this
174
240
  * invariant.
@@ -185,14 +251,17 @@ class Invariant extends Base {
185
251
  }
186
252
  }
187
253
 
188
- /**
189
- * Class representing a participant in the design.
190
- */
191
254
  class Participant extends Base {
192
255
  /**
193
- * Initialize the semantic participant.
194
- * @param {String} name
195
- * @param args
256
+ * Class representing a participant in the design. The participants are
257
+ * entites in deisgn that participate in the behavior. They are mapped onto
258
+ * the implementation and their value is loaded from the execution. The
259
+ * participants define a valid world state for the behavior to happen in
260
+ * through invariants. When the value of the participant violates an
261
+ * invariant, the design has entered a semantically invalid state and is
262
+ * the root cause of downstream failure(s).
263
+ *
264
+ * @param {Object} args The arguments to initialize the participant.
196
265
  */
197
266
  constructor (args) {
198
267
  super();
@@ -200,11 +269,7 @@ class Participant extends Base {
200
269
  this.invariants = [];
201
270
  this.abstractionId = null;
202
271
  this.invariantViolated = false;
203
- if (typeof args === "object" && Object.hasOwn(args, "uid")) {
204
- this._loadParticipantFromJSON(args);
205
- } else {
206
- this._loadArgs(args);
207
- }
272
+ (isLoadedFromFile(args) ? this._loadFromFile(args) : this._loadArgs(args));
208
273
  }
209
274
 
210
275
  /**
@@ -228,9 +293,9 @@ class Participant extends Base {
228
293
 
229
294
  /**
230
295
  * Loads the participant from a JSON object.
231
- * @param {Object} participantJSON
296
+ * @param {Object} participantJSON The JSON object read from file.
232
297
  */
233
- _loadParticipantFromJSON (participantJSON) {
298
+ _loadFromFile (participantJSON) {
234
299
  for (const [key, value] of Object.entries(participantJSON)) {
235
300
  if (key === "invariants") {
236
301
  value.forEach(node => this.invariants.push(new Invariant(node)));
@@ -241,8 +306,8 @@ class Participant extends Base {
241
306
 
242
307
  /**
243
308
  * Adds an invariant to the participant.
244
- * @param {Invariant} invariant
245
- * @returns
309
+ * @param {Invariant} invariant The invariant to add.
310
+ * @returns {Invariant} The invariant that was added.
246
311
  */
247
312
  addInvariant (invariant) {
248
313
  this.invariants.push(invariant);
@@ -250,16 +315,18 @@ class Participant extends Base {
250
315
  }
251
316
 
252
317
  /**
253
- * Sets the value of the participant.
254
- * @param {*} value
318
+ * Sets the value of this participant.
319
+ * @param {*} value The value to set for the participant.
255
320
  */
256
321
  setValue (value) {
257
322
  this.value = value;
258
323
  }
259
324
 
260
325
  /**
261
- * Enforces the particiants invariants.
262
- * @returns {Boolean}
326
+ * Enforces the participant's invariants. Raises a flag indicating if an
327
+ * invariant was violated and counts the number of invariant violations.
328
+ *
329
+ * @returns {Boolean} Returns flag indicating if any invariant was violated.
263
330
  */
264
331
  enforceInvariants () {
265
332
  this.invariantViolated = false;
@@ -279,19 +346,23 @@ class Participant extends Base {
279
346
  * This abstraction id will be used to assign a value to the
280
347
  * participant from the execution using the logged abstraction id.
281
348
  *
282
- * @param {String} abstractionId
349
+ * @param {String} abstractionId The abstraction ID to map to the
350
+ * participant.
283
351
  */
284
352
  mapAbstraction (abstractionId) {
353
+ /**
354
+ * TODO: Much like the behavior, I am settling on a clean way to map
355
+ * the participant onto the implementation without introducing new
356
+ * unncessary layers.
357
+ */
285
358
  this.abstractionId = abstractionId;
286
359
  }
287
360
  }
288
361
 
289
- /**
290
- * Class representing a Behavior in the design.
291
- */
292
362
  class Behavior extends Base {
293
363
  /**
294
364
  * Initialize the Behavior.
365
+ *
295
366
  * @param {String} name
296
367
  * @param args
297
368
  */
@@ -301,15 +372,12 @@ class Behavior extends Base {
301
372
  this.participants = [];
302
373
  this.abstractionIds = [];
303
374
  this.invalidWorldState = false;
304
- if (typeof args === "object" && Object.hasOwn(args, "uid")) {
305
- this._loadBehaviorFromJSON(args);
306
- } else {
307
- this._loadArgs(args);
308
- }
375
+ (isLoadedFromFile(args) ? this._loadFromFile(args) : this._loadArgs(args));
309
376
  }
310
377
 
311
378
  /**
312
379
  * Loads the provided arguments.
380
+ *
313
381
  * @throws {MissingAttributes} Thrown when required attr is not present.
314
382
  * @param {Object} args
315
383
  */
@@ -328,10 +396,12 @@ class Behavior extends Base {
328
396
  }
329
397
 
330
398
  /**
331
- * Loads the behavior from a JSON object.
332
- * @param {Object} behaviorJSON
399
+ * Loads the behavior from a JSON object that was read from file.
400
+ *
401
+ * @param {Object} behaviorJSON The JSON object representing the behavior
402
+ * read from file.
333
403
  */
334
- _loadBehaviorFromJSON (behaviorJSON) {
404
+ _loadFromFile (behaviorJSON) {
335
405
  for (const [key, value] of Object.entries(behaviorJSON)) {
336
406
  if (key === "participants") {
337
407
  value.forEach(node => this.participants.push(new Participant(node)));
@@ -342,45 +412,65 @@ class Behavior extends Base {
342
412
 
343
413
  /**
344
414
  * Adds a participant to the behavior.
345
- * @param {Participant} participant
346
- * @returns
415
+ *
416
+ * @param {Participant} participant The participant to add.
417
+ * @returns {Participant} The added participant.
418
+ * @throws {ParticipantAlreadyExistsError} Thrown when a participant with
419
+ * the same name already exists in the behavior.
347
420
  */
348
421
  addParticipant (participant) {
422
+ if (this.participants.some(p => p.name === participant.name)) {
423
+ throw new ParticipantAlreadyExistsError(participant.name);
424
+ }
349
425
  this.participants.push(participant);
350
426
  return participant;
351
427
  }
352
428
 
353
429
  /**
354
- * Set the participant value.
355
- * @param {String} participantName
356
- * @param {*} value
430
+ * Sets the value of a participant and checks for invariant violations.
431
+ * If any invariant is violated, the world state for this behavior is
432
+ * marked as invalid.
433
+ *
434
+ * @param {String} name Name of the participant whose value is being set.
435
+ * @param {*} value Value to set for the participant.
436
+ * @throws {UnknownParticipantError} Thrown when a participant with the
437
+ * provided name does not exist in the behavior.
357
438
  */
358
- setParticipantValue (participantName, value) {
359
- const participant = this.participants.find(obj => obj.name === participantName);
439
+ setParticipantValue (name, value) {
440
+ const participant = this.participants.find(obj => obj.name === name);
441
+ if (!participant) {
442
+ throw new UnknownParticipantError(name);
443
+ }
360
444
  participant.value = value;
361
- const violation = participant.enforceInvariants();
362
- if (violation) {
445
+ if (participant.enforceInvariants()) {
363
446
  this.invalidWorldState = true;
364
447
  }
365
448
  }
366
449
 
367
450
  /**
368
- * Maps the abstraction id from execution to the behavior.
369
- * @param {String} abstractionId
451
+ * Maps the abstraction id from implementation to the behavior.
452
+ *
453
+ * @param {String} abstractionId ID of mapped abstraction.
370
454
  */
371
455
  addMapping (abstractionId) {
456
+ /**
457
+ * TODO: After some more thinking, it seems to me that the
458
+ * uniqe identifier should be a file name and line number.
459
+ * It doesn't make sense to create a new abstraction id,
460
+ * however, I will resolve this soon and remove this TODO.
461
+ */
372
462
  this.abstractionIds.push(abstractionId);
373
463
  }
374
464
  }
375
465
 
376
- /**
377
- * Class representing a behavioral control graph node.
378
- */
379
466
  class GraphNode extends Base {
380
467
  /**
381
- * Initialize the node.
382
- * @param {String} name
383
- * @param args
468
+ * Class representing a behavioral control graph node. Each node has a
469
+ * behavior and a list of behaviors it can transition to. The node also
470
+ * has flags to indicate if the behavior is atomic or if the node is a
471
+ * design fork.
472
+ *
473
+ * @param {Object} args The args to initialize the graph node.
384
474
  */
385
475
  constructor (args) {
386
476
  super();
@@ -388,28 +478,41 @@ class GraphNode extends Base {
388
478
  this._behavior = null;
389
479
  this._goToBehaviorIds = [];
390
480
  this._isAtomic = false;
391
- if (typeof args === "object" && args !== null) {
392
- if (Object.hasOwn(args, "uid")) {
393
- this._loadNodeFromJSON(args);
394
- } else {
395
- this._behavior = args.behavior;
396
- this._goToBehaviorIds = args.goToBehaviorsIds;
397
- }
481
+ this._isDesignFork = false;
482
+ (isLoadedFromFile(args) ? this._loadFromFile(args) : this._loadArgs(args));
483
+ }
484
+
485
+ /**
486
+ * Loads the provided arguments.
487
+ * @param {Object} args Arguments to initialize the graph node with.
488
+ * @throws {MissingAttributes} Thrown when required attr is not present.
489
+ */
490
+ _loadArgs (args) {
491
+ const expectedAttributes = ["behavior", "goToBehaviorIds", "isAtomic", "isDesignFork"];
492
+ if (typeof args !== "object" || args === null || Array.isArray(args)) {
493
+ // Not an object, so all attributes are missing.
494
+ throw new MissingAttributes("GraphNode", expectedAttributes);
398
495
  }
496
+ expectedAttributes.forEach((attr) => {
497
+ if (!(attr in args)) {
498
+ throw new MissingAttributes("GraphNode", attr);
499
+ }
500
+ this["_" + attr] = args[attr];
501
+ });
399
502
  }
400
503
 
401
504
  /**
402
- * Loads the nodes from a JSON object.
403
- * @param {Object} nodesJSON
505
+ * Loads the node from file.
506
+ * @param {Object} nodeJSON The JSON object read from file.
404
507
  */
405
- _loadNodeFromJSON (nodesJSON) {
406
- for (const [key, value] of Object.entries(nodesJSON)) {
508
+ _loadFromFile (nodeJSON) {
509
+ for (const [key, value] of Object.entries(nodeJSON)) {
407
510
  if (key === "behavior") {
408
511
  this._behavior = new Behavior(value);
409
- } else if (key === "goToBehaviorsIds") {
512
+ } else if (key === "goToBehaviorIds") {
410
513
  value.forEach(behaviorId => this._goToBehaviorIds.push(behaviorId));
411
514
  } else {
412
- this[key] = nodesJSON[key];
515
+ this[key] = nodeJSON[key];
413
516
  }
414
517
  } }
415
518
 
@@ -423,15 +526,14 @@ class GraphNode extends Base {
423
526
 
424
527
  /**
425
528
  * Returns the list of behavior names that this node transitions to.
426
- * @returns {Array}
529
+ * @returns {Array} List of behavior names that this node transitions to.
427
530
  */
428
531
  getGoToBehaviors () {
429
532
  return this._goToBehaviorIds;
430
533
  }
431
534
 
432
535
  /**
433
- * Adds a behavior name to the list of behaviors that this
434
- * node transitions to.
536
+ * Adds a behavior to the transitions from this node.
435
537
  * @param {String} behaviorId ID of behavior.
436
538
  */
437
539
  addGoToBehavior (behaviorId) {
@@ -439,8 +541,7 @@ class GraphNode extends Base {
439
541
  }
440
542
 
441
543
  /**
442
- * Adds a behavior name to the list of behaviors that this
443
- * node transitions to.
544
+ * Adds list of behaviors to the transitions from this node.
444
545
  * @param {Array} behaviorIds IDs of behaviors.
445
546
  */
446
547
  addGoToBehaviors (behaviorIds) {
@@ -462,7 +563,7 @@ class GraphNode extends Base {
462
563
  * Raises a flag to indicate if the behavior is atomic or not.
463
564
  * @param {Boolean} isAtomic Flag indicates if the behavior is atomic.
464
565
  */
465
- setAtomic (isAtomic) {
566
+ setIsAtomic (isAtomic) {
466
567
  this._isAtomic = isAtomic;
467
568
  }
468
569
 
@@ -474,6 +575,22 @@ class GraphNode extends Base {
474
575
  return this._isAtomic;
475
576
  }
476
577
 
578
+ /**
579
+ * Raises a flag to indicate if this node is a fork in the design.
580
+ * @param {Boolean} forks Flag indicates if the node is a fork.
581
+ */
582
+ setIsDesignFork (forks) {
583
+ this._isDesignFork = forks;
584
+ }
585
+
586
+ /**
587
+ * Returns whether the node is a fork in the design or not.
588
+ * @returns {Boolean}
589
+ */
590
+ isDesignFork () {
591
+ return this._isDesignFork;
592
+ }
593
+
477
594
  /**
478
595
  * Checks if the provided behavior name is a valid
479
596
  * transition from this node.
@@ -491,33 +608,60 @@ class GraphNode extends Base {
491
608
  }
492
609
  }
493
610
 
494
- /**
495
- * Class representing the behavioral control graph.
496
- */
497
611
  class BehavioralControlGraph extends Base {
498
612
  /**
499
- * Initialize the behavioral control graph.
500
- * @param {String} name
501
- * @param args
613
+ * Class representing the behavioral control graph. The behavioral control
614
+ * graph is a directed graph where nodes represent behaviors and edges
615
+ * represent valid transitions between behaviors.
616
+ *
617
+ * The graph can be used to execute the design by starting at the atomic
618
+ * node and transitioning to observed behaviors. As the design is executed,
619
+ * the values of the participants are set from the observed values and the
620
+ * invariants are checked at each transition to recognize if the design has
621
+ * entered a semantically invalid state.
622
+ *
623
+ * Currently, it only has to be initialized with a name and the remaining
624
+ * attributes can be added using the provided methods. If the graph is being
625
+ * loaded from a file, then the presence of a UID in the args will select
626
+ * the relevant method to load the graph from a JSON object.
627
+ *
628
+ * @param {Object} args The args to initialize the behavioral control graph.
502
629
  */
503
630
  constructor (args) {
504
631
  super();
505
632
  this.type = ENGINE_TYPES.BEHAVIORAL_CONTROL_GRAPH;
506
633
  this.nodes = [];
507
- if (typeof args === "object" && args !== null) {
508
- if (Object.hasOwn(args, "uid")) {
509
- this._loadGraphFromJSON(args);
510
- } else {
511
- this.name = args.name;
512
- }
634
+ (isLoadedFromFile(args) ? this._loadFromFile(args) : this._loadArgs(args));
635
+ }
636
+
637
+ /**
638
+ * Loads the provided arguments.
639
+ * @throws {MissingAttributes} Thrown when required attr is not present.
640
+ * @param {Object} args
641
+ */
642
+ _loadArgs (args) {
643
+ /**
644
+ * TODO: Move the attributes to private and use getters and setters
645
+ * for them. Repeat for all the other classes.
646
+ */
647
+ const expectedAttributes = ["name"];
648
+ if (typeof args !== "object" || args === null || Array.isArray(args)) {
649
+ // Not an object, so all attributes are missing.
650
+ throw new MissingAttributes("BehavioralControlGraph", expectedAttributes);
513
651
  }
652
+ expectedAttributes.forEach((attr) => {
653
+ if (!(attr in args)) {
654
+ throw new MissingAttributes("BehavioralControlGraph", attr);
655
+ }
656
+ this[attr] = args[attr];
657
+ });
514
658
  }
515
659
 
516
660
  /**
517
661
  * Loads the graph from a JSON object..
518
662
  * @param {Object} graphJson
519
663
  */
520
- _loadGraphFromJSON (graphJson) {
664
+ _loadFromFile (graphJson) {
521
665
  for (const [key, value] of Object.entries(graphJson)) {
522
666
  if (key === "nodes") {
523
667
  value.forEach(node => this.nodes.push(new GraphNode(node)));
@@ -527,17 +671,27 @@ class BehavioralControlGraph extends Base {
527
671
  } }
528
672
 
529
673
  /**
530
- * Adds a node to the graph.
531
- * @param {Behavior} behaviorId
532
- * @param {Array} goToBehaviorsIds
533
- * @param {Boolean} isAtomic
534
- * @returns
674
+ * Adds a node to the graph with the provided arguments.
675
+ *
676
+ * @param {Behavior} behaviorId ID of the behavior represented by the node.
677
+ * @param {Array} goToBehaviorIds IDs of the behaviors that are valid
678
+ * transitions from this node.
679
+ * @param {Boolean} isAtomic Flag indicating if the behavior represented by
680
+ * the node is atomic.
681
+ * @param {Boolean} isDesignFork Flag indicating if the node is a
682
+ * design fork.
683
+ * @throws {BehaviorAlreadyExistsError} Raised when a node with the provided
684
+ * @returns {GraphNode} The created graph node.
535
685
  */
536
- _addNode (behaviorId, goToBehaviorsIds, isAtomic) {
686
+ addNode (behaviorId, goToBehaviorIds, isAtomic, isDesignFork) {
687
+ if (this.nodes.some((node) => node.getBehavior().name === behaviorId)) {
688
+ throw new BehaviorAlreadyExistsError(behaviorId);
689
+ }
537
690
  const node = new GraphNode({
538
691
  behavior: new Behavior({name: behaviorId}),
539
- goToBehaviorsIds: goToBehaviorsIds?goToBehaviorsIds:[],
692
+ goToBehaviorIds: goToBehaviorIds?goToBehaviorIds:[],
540
693
  isAtomic: isAtomic?isAtomic:false,
694
+ isDesignFork: isDesignFork?isDesignFork:false,
541
695
  });
542
696
  this.nodes.push(node);
543
697
  return node;
@@ -549,29 +703,25 @@ class BehavioralControlGraph extends Base {
549
703
  * @param {String} behaviorName
550
704
  * @throws {UnknownBehaviorError} Raised when the provided behavior
551
705
  * does not exist in the graph.
552
- * @returns
706
+ * @returns {GraphNode} The found graph node.
553
707
  */
554
- _findNode (behaviorName) {
555
- for (let i = 0; i < this.nodes.length; i++) {
556
- const behavior = this.nodes[i].getBehavior();
557
- if (behavior.name === behaviorName) {
558
- return this.nodes[i];
559
- }
708
+ findNode (behaviorName) {
709
+ const node = this.nodes.find(
710
+ (node) => node.getBehavior().name === behaviorName
711
+ );
712
+ if (!node) {
713
+ throw new UnknownBehaviorError(behaviorName);
560
714
  }
561
- throw new UnknownBehaviorError(behaviorName);
715
+ return node;
562
716
  }
563
717
 
564
718
  /**
565
719
  * Sets the active node given the behavior name.
566
- * The execution provides the next observed
567
- * behavior and the active node indicates if
568
- * if it is a valid transition.
569
- *
570
720
  *
571
721
  * @param {String} behaviorName
572
722
  */
573
- _setCurrentBehavior (behaviorName) {
574
- const node = this._findNode(behaviorName);
723
+ setCurrentBehavior (behaviorName) {
724
+ const node = this.findNode(behaviorName);
575
725
  /**
576
726
  * TODO: Ensure it is atomic because the execution
577
727
  * will only set a behavior when its the first one.
@@ -583,13 +733,15 @@ class BehavioralControlGraph extends Base {
583
733
  /**
584
734
  * Check if the observed behavior is a valid transition
585
735
  * given the current node.
586
- * @param {String} nextBehaviorName
736
+ *
737
+ * @param {String} nextBehaviorName Name of the next behavior to
738
+ * transition to.
587
739
  * @throws {InvalidTransitionError} Raised when the provided
588
740
  * behavior is not a valid transition.
589
741
  */
590
- _goToBehavior (nextBehaviorName) {
742
+ goToBehavior (nextBehaviorName) {
591
743
  if (this.currentNode.isValidTransition(nextBehaviorName)) {
592
- this.currentNode = this._findNode(nextBehaviorName);
744
+ this.currentNode = this.findNode(nextBehaviorName);
593
745
  } else {
594
746
  throw new InvalidTransitionError(this.currentNode.getBehavior().name, nextBehaviorName);
595
747
  }
@@ -611,31 +763,174 @@ class BehavioralControlGraph extends Base {
611
763
  }
612
764
 
613
765
  /**
614
- * An object representing an engine written in Design
615
- * abstraction language. It exposes functions
616
- * configure the engine through the DAL specification.
766
+ * Class representing a collection of atomic graphs.
767
+ *
768
+ * These graphs together represent the design. Within each graph, every node
769
+ * must be part of the same tree. There cannot be multiple disconnected trees,
770
+ * if there are, then it is a separate graph in this collection. Each graph
771
+ * is identified by a unique name and is selected as the active graph when the
772
+ * atomic behavior is observed. The atomic behavior is not transitioned to from
773
+ * any other behavior, it is the root of the tree.
774
+ */
775
+ class Graphs {
776
+ constructor () {
777
+ this._graphs = {};
778
+ this.addGraph("default graph");
779
+ }
780
+
781
+ /**
782
+ * Load the graphs from file.
783
+ *
784
+ * @param {String} jsonText JSON text representing the collection of graphs.
785
+ */
786
+ loadFromJson (jsonText) {
787
+ const parsed = JSON.parse(jsonText);
788
+ Object.keys(parsed._graphs).forEach(graphId => {
789
+ this._graphs[graphId] = new BehavioralControlGraph(parsed._graphs[graphId]);
790
+ });
791
+ this._activeGraph = this._graphs[Object.keys(this._graphs)[0]];
792
+ }
793
+
794
+ /**
795
+ * Given a graph ID, creates the graph and adds it to the collection.
796
+ *
797
+ * @param {String} graphId ID of the graph.
798
+ * @returns {BehavioralControlGraph} The graph that was added.
799
+ * @throws {GraphWithNameExistsError} Raised when a graph with the provided
800
+ * name already exists in the collection of graphs.
801
+ */
802
+ addGraph (graphId) {
803
+ if (graphId in this._graphs) {
804
+ throw new GraphWithNameExistsError(graphId);
805
+ }
806
+ this._graphs[graphId] = new BehavioralControlGraph({name: graphId});
807
+ this._activeGraph = this._graphs[graphId];
808
+ return this._activeGraph;
809
+ }
810
+
811
+ /**
812
+ * Returns the graph with the given graphID from the collection.
813
+ *
814
+ * @param {String} graphId ID of the graph to return.
815
+ * @returns {BehavioralControlGraph} The graph with the given graphId.
816
+ * @throws {UnknownGraph} Raised when a graph with the provided graphId does
817
+ * not exist in the collection of graphs.
818
+ */
819
+ getGraph (graphId) {
820
+ if (graphId in this._graphs) {
821
+ return this._graphs[graphId];
822
+ } else {
823
+ throw new UnknownGraph(graphId);
824
+ }
825
+ }
826
+
827
+ /**
828
+ * Removes a graph with the given id from the collection of graphs. After
829
+ * the graph is removed, it selects the first graph in the collection of
830
+ * graphs. If there are no graphs left in the collection, it creates a new
831
+ * graph with the name "default graph" and selects it as the active graph.
832
+ *
833
+ * @param {String} graphId ID of the graph to remove.
834
+ * @throws {UnknownGraph} Raised when the provided graphId does not exist
835
+ * in the collection of graphs.
836
+ */
837
+ removeGraph (graphId) {
838
+ if (graphId in this._graphs) {
839
+ delete this._graphs[graphId];
840
+ } else {
841
+ throw new UnknownGraph(graphId);
842
+ }
843
+ // TODO: Is there a better policy for which graph to select
844
+ // after the current graph is removed?
845
+ if (Object.keys(this._graphs).length === 0) {
846
+ this.addGraph("default graph");
847
+ } else {
848
+ this._activeGraph = this._graphs[Object.keys(this._graphs)[0]];
849
+ }
850
+ }
851
+
852
+ /**
853
+ * Finds the graph with the given graphId and sets it as the active graph.
854
+ *
855
+ * @param {String} graphId Id of the graph to set as active.
856
+ * @returns {BehavioralControlGraph} The currently active graph.
857
+ * @throws {UnknownGraph} Raised when the provided graphId does not exist
858
+ * in the collection of graphs.
859
+ */
860
+ setActiveGraph (graphId) {
861
+ if (graphId in this._graphs) {
862
+ this._activeGraph = this._graphs[graphId];
863
+ } else {
864
+ throw new UnknownGraph(graphId);
865
+ }
866
+ return this._activeGraph;
867
+ }
868
+
869
+ /**
870
+ * Returns the active graph.
871
+ *
872
+ * @returns {BehavioralControlGraph} The currently active graph.
873
+ */
874
+ getActiveGraph () {
875
+ return this._activeGraph;
876
+ }
877
+
878
+ /**
879
+ * Returns a collection of all the graphs in the design.
880
+ *
881
+ * @returns {Object} The collection of all graphs in the design.
882
+ */
883
+ getGraphs () {
884
+ return this._graphs;
885
+ }
886
+
887
+ /**
888
+ * Returns a list of all the graph names in the design.
889
+ *
890
+ * @returns {Array} A list of all graph names in the design.
891
+ */
892
+ getGraphNames () {
893
+ return Object.keys(this._graphs);
894
+ }
895
+ }
896
+
897
+ /**
898
+ * This engine can be used to define and execute designs defined in a
899
+ * Design Abstraction Language (DAL).
900
+ *
901
+ * This class is the main interface for users to interact with the
902
+ * engine. It exposes functions to configure the engine and to execute
903
+ * the design. It also exposes functions to serialize and deserialize
904
+ * the engine to and from JSON text.
905
+ *
906
+ * A design can consist of multiple atomic behavioral control graphs and
907
+ * this class allows users to create, select, and delete graphs. It also
908
+ * allows users to add nodes to the graph and to transition between
909
+ * behaviors in the graph. It also allows users to create participants,
910
+ * behaviors, and invariants and assign them to nodes in the graph.
617
911
  *
618
- * The design specified in this engine is mapped onto
619
- * the implementation using abstraction ids. The
620
- * implementation is then instrumented and the resulting
621
- * execution trace is fed back into the engine and is
622
- * automatically debugged by transforming the execution
623
- * into the behavior of the design and enforcing the
624
- * invariants.
912
+ * The selected graph is determined by the atomic behavior that is observed.
913
+ * The design is executed by transitioning between behaviors in the graph.
914
+ * The values of the participants are set from the observed values and the
915
+ * invariants are checked at each transition to recognize if the design has
916
+ * entered a semantically invalid state.
625
917
  */
626
918
  class DALEngine {
627
919
  constructor (args) {
628
- this.graph = new BehavioralControlGraph();
629
- this.atomicGraphs = [];
630
- this.loadArgs(args);
920
+ this.graphs = new Graphs();
921
+ this.graph = this.graphs.getActiveGraph();
922
+ this._loadArgs(args);
923
+ console.log("Initialized DAL Engine with graphs: ", this.graphs);
631
924
  }
632
925
 
633
926
  /**
634
- * Loads the provided arguments.
635
- * @throws {MissingAttributes} Thrown when required attr is not present.
636
- * @param {Object} args
927
+ * Sets the provided arguments to the engine.
928
+ *
929
+ * @throws {MissingAttributes} Thrown when required attributes are
930
+ * not present.
931
+ * @param {Object} args Arguments to load.
637
932
  */
638
- loadArgs (args) {
933
+ _loadArgs (args) {
639
934
  const expectedAttributes = ["name"];
640
935
  if (typeof args !== "object" || args === null || Array.isArray(args)) {
641
936
  // Not an object, so all attributes are missing.
@@ -650,98 +945,187 @@ class DALEngine {
650
945
  }
651
946
 
652
947
  /**
653
- * Exports the behavioral control graph to JSON text.
654
- * @returns {String}
948
+ * Serializes the behavioral control graphs and returns the JSON text.
949
+ *
950
+ * @returns {String} Returns JSON string representing the control graphs.
655
951
  */
656
952
  serialize () {
657
- return JSON.stringify(this.graph);
953
+ return JSON.stringify(this.graphs);
658
954
  }
659
955
 
660
956
  /**
661
- * Import the behavioral control graph from JSON text.
662
- * @param {String} jsonText
957
+ * Loads the behavioral control graphs from JSON text and sets
958
+ * the active graph to the first graph in the collection of graphs.
959
+ *
960
+ * @param {String} serializedText JSON text representing the control
961
+ * graphs.
962
+ * @throws {SyntaxError|TypeError} Thrown when the JSON text is invalid.
663
963
  */
664
- deserialize (jsonText) {
665
- this.graph = new BehavioralControlGraph(JSON.parse(jsonText));
964
+ deserialize (serializedText) {
965
+ // TODO: Improve validation to throw specific error.
966
+ this.graphs = new Graphs();
967
+ this.graphs.loadFromJson(serializedText);
968
+ this.graph = this.graphs.getActiveGraph();
666
969
  }
667
970
 
668
971
  /**
669
- * Creates a participant.
670
- * @param {Object} args
671
- * @returns {Participant}
972
+ * Creates a graph with the given name and sets it as the active graph.
973
+ *
974
+ * @param {String} name Name of the graph to create.
975
+ * @throws {GraphWithNameExistsError} Thrown when a graph with the
976
+ * provided name already exists.
977
+ */
978
+ createGraph (name) {
979
+ this.graph = this.graphs.addGraph(name);
980
+ }
981
+
982
+ /**
983
+ * Sets the active graph to the graph with the given graphId.
984
+ *
985
+ * @param {String} graphId ID of the graph to set as active
986
+ * @throws {UnknownGraph} Thrown when the provided graphId does
987
+ * not exist in the collection of graphs.
988
+ */
989
+ selectGraph (graphId) {
990
+ this.graph = this.graphs.setActiveGraph(graphId);
991
+ }
992
+
993
+ /**
994
+ * Removes the graph with the given graphId
995
+ *
996
+ * @param {String} graphId Id of graph to remove.
997
+ * @throws {UnknownGraph} Thrown when the provided graphId does not
998
+ * exist in the collection of graphs.
999
+ */
1000
+ removeGraph (graphId) {
1001
+ this.graphs.removeGraph(graphId);
1002
+ this.graph = this.graphs.getActiveGraph();
1003
+ }
1004
+
1005
+ /**
1006
+ * Returns the names of all the graphs in the design.
1007
+ *
1008
+ * @returns {Array} Returns an array of graph names.
1009
+ */
1010
+ getSelectableGraphs () {
1011
+ return this.graphs.getGraphNames();
1012
+ }
1013
+
1014
+ /**
1015
+ * Creates a participant with the provided args and returns it.
1016
+ *
1017
+ * @param {Object} args Arguments to create the participant with.
1018
+ * @returns {Participant} Returns the created participant.
1019
+ * @throws {MissingAttributes} Thrown when required attributes are not
1020
+ * present in the args. See Participant class for required attributes.
672
1021
  */
673
1022
  createParticipant (args) {
674
1023
  return new Participant(args);
675
1024
  }
676
1025
 
677
1026
  /**
678
- * Creates a behavior.
679
- * @param {Object} args
680
- * @returns {Behavior}
1027
+ * Creates a behavior with the provided args and returns it.
1028
+ *
1029
+ * @param {Object} args Arguments to create the behavior with.
1030
+ * @returns {Behavior} Returns the created behavior.
1031
+ * @throws {MissingAttributes} Thrown when required attributes are not
1032
+ * present in the args. See Behavior class for required attributes.
681
1033
  */
682
1034
  createBehavior (args) {
683
1035
  return new Behavior(args);
684
1036
  }
685
1037
 
686
1038
  /**
687
- * Creates an invariant.
688
- * @param {Object} args
689
- * @returns {Invariant}
1039
+ * Creates an invariant with the provided args and returns it.
1040
+ *
1041
+ * @param {Object} args Arguments to create the invariant with.
1042
+ * @returns {Invariant} Returns the created invariant.
1043
+ * @throws {MissingAttributes} Raised when required attributes are not
1044
+ * present in the args. See Invariant class for required attributes.
690
1045
  */
691
1046
  createInvariant (args) {
692
1047
  return new Invariant(args);
693
1048
  }
694
1049
 
695
-
696
1050
  /**
697
1051
  * Returns the node in the graph with the given behavior name.
698
- * @param {String} behaviorId
699
- * @returns {GraphNode}
1052
+ *
1053
+ * @param {String} behaviorId ID of the behavior.
1054
+ * @returns {GraphNode} Returns the node in the graph with the
1055
+ * given behavior name.
1056
+ * @throws {UnknownBehaviorError} Thrown when the provided behaviorId is not
1057
+ * a valid behavior in the graph.
700
1058
  */
701
1059
  getNode (behaviorId) {
702
- return this.graph._findNode(behaviorId);
1060
+ return this.graph.findNode(behaviorId);
703
1061
  }
704
1062
 
705
1063
  /**
706
1064
  * Adds a node to the graph with the given behaviorId and goToBehaviors.
707
- * @param {String} behaviorId
708
- * @param {Array} goToBehaviorsIds
709
- * @param {Boolean} isAtomic
710
- * @returns {GraphNode}
1065
+ *
1066
+ * @param {String} behaviorId ID of the behavior for the node.
1067
+ * @param {Array} goToBehaviorIds IDs of the behaviors that this node
1068
+ * transitions to.
1069
+ * @param {Boolean} isAtomic Flag to indicate if this node contains an
1070
+ * atomic behavior.
1071
+ * @param {Boolean} isDesignFork Flag to indicate if this node
1072
+ * is a fork in the design.
1073
+ * @returns {GraphNode} Returns the created graph node.
1074
+ * @throws {BehaviorAlreadyExistsError} Raised when a node with the provided
1075
+ * behaviorId already exists in the graph.
711
1076
  */
712
- addNode (behaviorId, goToBehaviorsIds, isAtomic) {
713
- return this.graph._addNode(behaviorId, goToBehaviorsIds, isAtomic);
1077
+ addNode (behaviorId, goToBehaviorIds, isAtomic, isDesignFork) {
1078
+ return this.graph.addNode(
1079
+ behaviorId,
1080
+ goToBehaviorIds,
1081
+ isAtomic,
1082
+ isDesignFork
1083
+ );
714
1084
  }
715
1085
 
716
1086
  /**
717
1087
  * Deletes a node from the graph with the given behaviorId and
718
1088
  * removes it from the goToBehavior list of all other nodes.
719
- * @param {String} behaviorId
1089
+ *
1090
+ * @param {String} behaviorId Behavior ID of the node to delete.
1091
+ * @returns {GraphNode} Returns the deleted graph node.
1092
+ * @throws {UnknownBehaviorError} Thrown when the provided behaviorId is not
1093
+ * a valid behavior in the graph.
720
1094
  */
721
1095
  removeNode (behaviorId) {
722
- const node = this.graph._findNode(behaviorId);
1096
+ const node = this.graph.findNode(behaviorId);
723
1097
  const nodeIndex = this.graph.nodes.indexOf(node);
724
- this.graph.nodes.splice(nodeIndex, 1);
1098
+ const removedNode = this.graph.nodes.splice(nodeIndex, 1);
725
1099
  for (const node of this.graph.nodes) {
726
1100
  node.removeGoToBehavior(behaviorId);
727
1101
  }
1102
+ return removedNode[0];
728
1103
  }
729
1104
 
730
1105
  /**
731
- * Sets the current behavior in the graph.
732
- * @param {String} behaviorId
1106
+ * Sets the current behavior in the graph. Since the behavior is not
1107
+ * being transitioned from another, it must be an atomic behavior.
1108
+ *
1109
+ * @param {String} behaviorId ID of the behavior to set as current.
1110
+ * @throws {UnknownBehaviorError} Thrown when the provided behavior is not
1111
+ * a valid behavior in the graph.
733
1112
  */
734
1113
  setCurrentBehavior (behaviorId) {
735
- this.graph._setCurrentBehavior(behaviorId);
1114
+ this.graph.setCurrentBehavior(behaviorId);
736
1115
  }
737
1116
 
738
1117
  /**
739
- * Transitions the graph to the given behavior if it
740
- * is a valid transition from the current behavior.
1118
+ * For the current node in the graph, transitions to the node with the given
1119
+ * behaviorId if it is a valid transition.
1120
+ *
741
1121
  * @param {String} nextBehaviorId ID of the next behavior.
1122
+ * @throws {UnknownBehaviorError} Thrown when the provided behavior is not
1123
+ * a valid behavior in the graph.
1124
+ * @throws {InvalidTransitionError} Thrown when the provided behavior is not
1125
+ * a valid transition from the current node.
742
1126
  */
743
1127
  goToBehavior (nextBehaviorId) {
744
- this.graph._goToBehavior(nextBehaviorId);
1128
+ this.graph.goToBehavior(nextBehaviorId);
745
1129
  }
746
1130
  }
747
1131