@istar-ts/core 0.10.0 → 0.11.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.
package/dist/index.js CHANGED
@@ -267,7 +267,9 @@ function extendMetamodel(base, extension) {
267
267
  } } : {},
268
268
  ...parent ? { behavesLike: parent.kind } : {},
269
269
  pistarType: type,
270
- extension: extension.name
270
+ extension: extension.name,
271
+ ...declaration.shape ? { shape: declaration.shape } : {},
272
+ ...declaration.textBox ? { textBox: declaration.textBox } : {}
271
273
  };
272
274
  elements.set(kind, definition);
273
275
  elementsByType.set(type, kind);
@@ -300,7 +302,8 @@ function extendMetamodel(base, extension) {
300
302
  ...declaration.rules ? { rules: declaration.rules } : {},
301
303
  ...declaration.check ? { check: declaration.check } : {},
302
304
  pistarType: type,
303
- extension: extension.name
305
+ extension: extension.name,
306
+ ...declaration.line ? { line: declaration.line } : {}
304
307
  };
305
308
  links.set(kind, definition);
306
309
  linksByType.set(type, kind);
@@ -391,10 +394,27 @@ function withMetamodel(model, metamodel) {
391
394
  else metamodels.set(model, metamodel);
392
395
  return model;
393
396
  }
397
+ /**
398
+ * For models whose file declares part of their metamodel (see `withFileMetamodel`): the
399
+ * metamodel the host supplied, without the file's kinds.
400
+ */
401
+ const hostMetamodels = /* @__PURE__ */ new WeakMap();
402
+ /** @internal Records the host part of a model's metamodel (used by serialization). */
403
+ function setHostMetamodel(model, metamodel) {
404
+ if (metamodel) hostMetamodels.set(model, metamodel);
405
+ else hostMetamodels.delete(model);
406
+ }
407
+ /** @internal The host part of a model's metamodel, if the file declares the rest. */
408
+ function hostMetamodelOf(model) {
409
+ return hostMetamodels.get(model);
410
+ }
394
411
  /** Gives `to` the metamodel of `from` (used for every model an operation derives). */
395
412
  function inheritMetamodel(from, to) {
413
+ if (from === to) return to;
396
414
  const metamodel = metamodels.get(from);
397
- if (metamodel && from !== to) metamodels.set(to, metamodel);
415
+ if (metamodel) metamodels.set(to, metamodel);
416
+ const host = hostMetamodels.get(from);
417
+ if (host) hostMetamodels.set(to, host);
398
418
  return to;
399
419
  }
400
420
  function createEmptyModel(diagram = DEFAULT_DIAGRAM, options = {}) {
@@ -466,7 +486,7 @@ function dependencyLinksOf(model, dependumId) {
466
486
  //#endregion
467
487
  //#region src/constraints.ts
468
488
  const OK = { ok: true };
469
- function fail(code, reason) {
489
+ function fail$1(code, reason) {
470
490
  return {
471
491
  ok: false,
472
492
  code,
@@ -487,10 +507,10 @@ function canLink(model, source, target, kind, options = {}) {
487
507
  const metamodel = options.metamodel ?? metamodelOf(model);
488
508
  const s = resolve(any, source);
489
509
  const t = resolve(any, target);
490
- if (!s) return fail("unknown-element", `unknown source element "${refId(source)}"`);
491
- if (!t) return fail("unknown-element", `unknown target element "${refId(target)}"`);
510
+ if (!s) return fail$1("unknown-element", `unknown source element "${refId(source)}"`);
511
+ if (!t) return fail$1("unknown-element", `unknown target element "${refId(target)}"`);
492
512
  const definition = metamodel.links.get(kind);
493
- if (!definition) return fail("unknown-link-kind", `unknown link kind "${String(kind)}"`);
513
+ if (!definition) return fail$1("unknown-link-kind", `unknown link kind "${String(kind)}"`);
494
514
  return checkWith(new Graph(any, metamodel), definition, s, t);
495
515
  }
496
516
  /** Runs the rule a link kind resolves to, then every extra predicate along its chain. */
@@ -502,7 +522,7 @@ function checkWith(graph, definition, source, target) {
502
522
  let result;
503
523
  if (root.rules) result = declarativeCheck(graph, definition, root.rules, root.kind, source, target);
504
524
  else if (builtIn) result = builtIn(graph, source, target);
505
- else result = fail("unknown-link-kind", `link kind "${definition.kind}" has no rules`);
525
+ else result = fail$1("unknown-link-kind", `link kind "${definition.kind}" has no rules`);
506
526
  if (!result.ok) return result;
507
527
  const context = graph.context(definition.kind, source, target);
508
528
  for (const step of chain.toReversed()) {
@@ -586,18 +606,18 @@ const is = (graph, element, ...shortNames) => {
586
606
  };
587
607
  const isDependum = (element) => element.isDependum === true;
588
608
  const isALink = (graph, source, target) => {
589
- if (!is(graph, source, "Actor", "Role")) return fail("invalid-source", "the source of Is-A links must be a Role or a general Actor (iStar 2.0 Guide, Page 6).");
590
- if (source.kind !== target.kind) return fail("actor-types-differ", "the source and target of Is-A links must be of the same type - Actor and Actor, or Role and Role (iStar 2.0 Guide, Page 6).");
591
- if (!is(graph, target, "Actor", "Role")) return fail("invalid-target", "the target of Is-A links must be a Role or a general Actor (iStar 2.0 Guide, Page 6).");
592
- if (source.id === target.id) return fail("self-link", "you cannot make Is-A links from an actor onto itself.");
593
- if (graph.isThereLinkBetween(source, target)) return fail("duplicate-link", "there can only be one Actor link between the same two actors (iStar 2.0 Guide, Page 14).");
609
+ if (!is(graph, source, "Actor", "Role")) return fail$1("invalid-source", "the source of Is-A links must be a Role or a general Actor (iStar 2.0 Guide, Page 6).");
610
+ if (source.kind !== target.kind) return fail$1("actor-types-differ", "the source and target of Is-A links must be of the same type - Actor and Actor, or Role and Role (iStar 2.0 Guide, Page 6).");
611
+ if (!is(graph, target, "Actor", "Role")) return fail$1("invalid-target", "the target of Is-A links must be a Role or a general Actor (iStar 2.0 Guide, Page 6).");
612
+ if (source.id === target.id) return fail$1("self-link", "you cannot make Is-A links from an actor onto itself.");
613
+ if (graph.isThereLinkBetween(source, target)) return fail$1("duplicate-link", "there can only be one Actor link between the same two actors (iStar 2.0 Guide, Page 14).");
594
614
  return OK;
595
615
  };
596
616
  const participatesInLink = (graph, source, target) => {
597
- if (!graph.isActor(source)) return fail("invalid-source", "the source of a Participates-In link must be some kind of actor (iStar 2.0 Guide, Page 6)");
598
- if (!graph.isActor(target)) return fail("invalid-target", "the target of a Participates-In link must be some kind of actor (iStar 2.0 Guide, Page 6)");
599
- if (source.id === target.id) return fail("self-link", "you cannot make a Participates-In link from an actor onto itself.");
600
- if (graph.isThereLinkBetween(source, target)) return fail("duplicate-link", "there can only be one Actor link between the same two actors (iStar 2.0 Guide, Page 14).");
617
+ if (!graph.isActor(source)) return fail$1("invalid-source", "the source of a Participates-In link must be some kind of actor (iStar 2.0 Guide, Page 6)");
618
+ if (!graph.isActor(target)) return fail$1("invalid-target", "the target of a Participates-In link must be some kind of actor (iStar 2.0 Guide, Page 6)");
619
+ if (source.id === target.id) return fail$1("self-link", "you cannot make a Participates-In link from an actor onto itself.");
620
+ if (graph.isThereLinkBetween(source, target)) return fail$1("duplicate-link", "there can only be one Actor link between the same two actors (iStar 2.0 Guide, Page 14).");
601
621
  return OK;
602
622
  };
603
623
  /** The actor an element belongs to: itself for actors, its parent for inner elements. */
@@ -605,56 +625,56 @@ function owningActorId(graph, element) {
605
625
  return graph.isActor(element) ? element.id : element.parent;
606
626
  }
607
627
  const dependencyLink = (graph, source, target) => {
608
- if (source.id === target.id) return fail("self-link", "a Dependency link cannot link an element onto itself");
609
- if (isDependum(source)) return fail("dependum-endpoint", "a Dependency link cannot start from a dependum");
610
- if (isDependum(target)) return fail("dependum-endpoint", "a Dependency link cannot end in a dependum");
611
- if (owningActorId(graph, source) === owningActorId(graph, target)) return fail("same-actor", "a Dependency link must involve two different actors (iStar 2.0 Guide, Page 14)");
612
- if (graph.isTargetOfType(source, "istar.OrRefinementLink") || graph.isTargetOfType(source, "istar.AndRefinementLink")) return fail("refined-depender", "a refined element cannot be the Depender Element in a Dependency link (iStar 2.0 Guide, Page 14). Instead, you can try to add the Dependency originating from a child.");
613
- if (graph.isTargetOfType(source, "istar.ContributionLink")) return fail("contributed-depender", "a contributed element cannot be the Depender Element in a Dependency link (iStar 2.0 Guide, Page 14). Instead, you can try to add the Dependency originating from a child.");
628
+ if (source.id === target.id) return fail$1("self-link", "a Dependency link cannot link an element onto itself");
629
+ if (isDependum(source)) return fail$1("dependum-endpoint", "a Dependency link cannot start from a dependum");
630
+ if (isDependum(target)) return fail$1("dependum-endpoint", "a Dependency link cannot end in a dependum");
631
+ if (owningActorId(graph, source) === owningActorId(graph, target)) return fail$1("same-actor", "a Dependency link must involve two different actors (iStar 2.0 Guide, Page 14)");
632
+ if (graph.isTargetOfType(source, "istar.OrRefinementLink") || graph.isTargetOfType(source, "istar.AndRefinementLink")) return fail$1("refined-depender", "a refined element cannot be the Depender Element in a Dependency link (iStar 2.0 Guide, Page 14). Instead, you can try to add the Dependency originating from a child.");
633
+ if (graph.isTargetOfType(source, "istar.ContributionLink")) return fail$1("contributed-depender", "a contributed element cannot be the Depender Element in a Dependency link (iStar 2.0 Guide, Page 14). Instead, you can try to add the Dependency originating from a child.");
614
634
  return OK;
615
635
  };
616
636
  function refinementLink(label) {
617
637
  const other = label === "AND" ? "istar.OrRefinementLink" : "istar.AndRefinementLink";
618
638
  return (graph, source, target) => {
619
- if (!is(graph, source, "Task", "Goal")) return fail("invalid-source", `the source of an ${label}-refinement link must be a Goal or a Task (iStar 2.0 Guide, Table 1)`);
620
- if (!is(graph, target, "Task", "Goal")) return fail("invalid-target", `the target of an ${label}-refinement link must be a Goal or a Task (iStar 2.0 Guide, Table 1)`);
621
- if (source.id === target.id) return fail("self-link", `you cannot make an ${label}-refinement link from an element onto itself`);
622
- if (isDependum(source) || isDependum(target)) return fail("dependum-endpoint", `you cannot make an ${label}-refinement link with a dependum (iStar 2.0 Guide, Page 14)`);
623
- if (source.parent !== target.parent) return fail("different-actors", `the source and target of an ${label}-refinement link must pertain to the same actor (iStar 2.0 Guide, Page 14)`);
624
- if (graph.isThereLinkBetween(source, target)) return fail("duplicate-link", "there can only be one refinement link between the same two elements");
625
- if (graph.isSourceOfType(target, "istar.DependencyLink")) return fail("depender-target", "you cannot refine a Depender Element; that is, an element that is the source of a Dependency (iStar 2.0 Guide, Page 14). Instead, you can try to move the dependency to the sub-element.");
626
- if (graph.isTargetOfType(target, other)) return fail("mixed-refinement", label === "AND" ? "you cannot mix AND-refinements with OR-refinements targeting the same element (iStar 2.0 Guide, Page 10)." : "you cannot mix OR-refinements with AND-refinements targeting the same element (iStar 2.0 Guide, Page 10).");
639
+ if (!is(graph, source, "Task", "Goal")) return fail$1("invalid-source", `the source of an ${label}-refinement link must be a Goal or a Task (iStar 2.0 Guide, Table 1)`);
640
+ if (!is(graph, target, "Task", "Goal")) return fail$1("invalid-target", `the target of an ${label}-refinement link must be a Goal or a Task (iStar 2.0 Guide, Table 1)`);
641
+ if (source.id === target.id) return fail$1("self-link", `you cannot make an ${label}-refinement link from an element onto itself`);
642
+ if (isDependum(source) || isDependum(target)) return fail$1("dependum-endpoint", `you cannot make an ${label}-refinement link with a dependum (iStar 2.0 Guide, Page 14)`);
643
+ if (source.parent !== target.parent) return fail$1("different-actors", `the source and target of an ${label}-refinement link must pertain to the same actor (iStar 2.0 Guide, Page 14)`);
644
+ if (graph.isThereLinkBetween(source, target)) return fail$1("duplicate-link", "there can only be one refinement link between the same two elements");
645
+ if (graph.isSourceOfType(target, "istar.DependencyLink")) return fail$1("depender-target", "you cannot refine a Depender Element; that is, an element that is the source of a Dependency (iStar 2.0 Guide, Page 14). Instead, you can try to move the dependency to the sub-element.");
646
+ if (graph.isTargetOfType(target, other)) return fail$1("mixed-refinement", label === "AND" ? "you cannot mix AND-refinements with OR-refinements targeting the same element (iStar 2.0 Guide, Page 10)." : "you cannot mix OR-refinements with AND-refinements targeting the same element (iStar 2.0 Guide, Page 10).");
627
647
  return OK;
628
648
  };
629
649
  }
630
650
  const neededByLink = (graph, source, target) => {
631
- if (!is(graph, source, "Resource")) return fail("invalid-source", "the source of a Needed-By link must be a Resource (iStar 2.0 Guide, Table 1)");
632
- if (!is(graph, target, "Task")) return fail("invalid-target", "the target of a Needed-By link must be a Task (iStar 2.0 Guide, Table 1)");
633
- if (source.id === target.id) return fail("self-link", "you cannot make a Needed-By link from an element onto itself");
634
- if (isDependum(source) || isDependum(target)) return fail("dependum-endpoint", "you cannot make a Needed-By link with a dependum (iStar 2.0 Guide, Page 14)");
635
- if (source.parent !== target.parent) return fail("different-actors", "the source and target of a Needed-By link must pertain to the same actor (iStar 2.0 Guide, Page 14)");
636
- if (graph.isThereLinkBetween(source, target)) return fail("duplicate-link", "there can only be one Needed-By link between the same two elements");
651
+ if (!is(graph, source, "Resource")) return fail$1("invalid-source", "the source of a Needed-By link must be a Resource (iStar 2.0 Guide, Table 1)");
652
+ if (!is(graph, target, "Task")) return fail$1("invalid-target", "the target of a Needed-By link must be a Task (iStar 2.0 Guide, Table 1)");
653
+ if (source.id === target.id) return fail$1("self-link", "you cannot make a Needed-By link from an element onto itself");
654
+ if (isDependum(source) || isDependum(target)) return fail$1("dependum-endpoint", "you cannot make a Needed-By link with a dependum (iStar 2.0 Guide, Page 14)");
655
+ if (source.parent !== target.parent) return fail$1("different-actors", "the source and target of a Needed-By link must pertain to the same actor (iStar 2.0 Guide, Page 14)");
656
+ if (graph.isThereLinkBetween(source, target)) return fail$1("duplicate-link", "there can only be one Needed-By link between the same two elements");
637
657
  return OK;
638
658
  };
639
659
  const contributionLink = (graph, source, target) => {
640
- if (!is(graph, source, "Goal", "Quality", "Task", "Resource")) return fail("invalid-source", "the source of a Contribution link must be a Goal, a Quality, a Task or a Resource (iStar 2.0 Guide, Table 1)");
641
- if (!is(graph, target, "Quality")) return fail("invalid-target", "the target of a Contribution link must be a Quality (iStar 2.0 Guide, Table 1)");
642
- if (source.id === target.id) return fail("self-link", "you cannot make a Contribution link from an element onto itself (iStar 2.0 Guide, Page 15)");
643
- if (isDependum(source) || isDependum(target)) return fail("dependum-endpoint", "you cannot make a Contribution link with a dependum (iStar 2.0 Guide, Page 14)");
644
- if (source.parent !== target.parent) return fail("different-actors", "the source and target of a Contribution link must pertain to the same actor (iStar 2.0 Guide, Page 14)");
645
- if (graph.isThereLinkBetween(source, target, "istar.ContributionLink")) return fail("duplicate-link", "there can only be one Contribution link between the same two elements");
646
- if (graph.isThereLinkBetween(source, target, "istar.QualificationLink")) return fail("contribution-and-qualification", "you cannot have Contribution and Qualification links between the same two elements (iStar 2.0 Guide, Page 15)");
647
- if (graph.isSourceOfType(target, "istar.DependencyLink")) return fail("depender-target", "you cannot contribute to a Depender Element; that is, an element that is the source of a Dependency (iStar 2.0 Guide, Page 14). Instead, you can try to move the dependency to the sub-quality.");
660
+ if (!is(graph, source, "Goal", "Quality", "Task", "Resource")) return fail$1("invalid-source", "the source of a Contribution link must be a Goal, a Quality, a Task or a Resource (iStar 2.0 Guide, Table 1)");
661
+ if (!is(graph, target, "Quality")) return fail$1("invalid-target", "the target of a Contribution link must be a Quality (iStar 2.0 Guide, Table 1)");
662
+ if (source.id === target.id) return fail$1("self-link", "you cannot make a Contribution link from an element onto itself (iStar 2.0 Guide, Page 15)");
663
+ if (isDependum(source) || isDependum(target)) return fail$1("dependum-endpoint", "you cannot make a Contribution link with a dependum (iStar 2.0 Guide, Page 14)");
664
+ if (source.parent !== target.parent) return fail$1("different-actors", "the source and target of a Contribution link must pertain to the same actor (iStar 2.0 Guide, Page 14)");
665
+ if (graph.isThereLinkBetween(source, target, "istar.ContributionLink")) return fail$1("duplicate-link", "there can only be one Contribution link between the same two elements");
666
+ if (graph.isThereLinkBetween(source, target, "istar.QualificationLink")) return fail$1("contribution-and-qualification", "you cannot have Contribution and Qualification links between the same two elements (iStar 2.0 Guide, Page 15)");
667
+ if (graph.isSourceOfType(target, "istar.DependencyLink")) return fail$1("depender-target", "you cannot contribute to a Depender Element; that is, an element that is the source of a Dependency (iStar 2.0 Guide, Page 14). Instead, you can try to move the dependency to the sub-quality.");
648
668
  return OK;
649
669
  };
650
670
  const qualificationLink = (graph, source, target) => {
651
- if (!is(graph, source, "Quality")) return fail("invalid-source", "the source of a Qualification link must be a Quality (iStar 2.0 Guide, Table 1)");
652
- if (!is(graph, target, "Goal", "Task", "Resource")) return fail("invalid-target", "the target of a Qualification link must be a Goal, a Task or a Resource (iStar 2.0 Guide, Table 1)");
653
- if (source.id === target.id) return fail("self-link", "you cannot make a Qualification link from an element onto itself");
654
- if (isDependum(source) || isDependum(target)) return fail("dependum-endpoint", "you cannot make a Qualification link with a dependum (iStar 2.0 Guide, Page 14)");
655
- if (source.parent !== target.parent) return fail("different-actors", "the source and target of a Qualification link must pertain to the same actor (iStar 2.0 Guide, Page 14)");
656
- if (graph.isThereLinkBetween(source, target, "istar.QualificationLink")) return fail("duplicate-link", "there can only be one Qualification link between the same two elements");
657
- if (graph.isThereLinkBetween(source, target, "istar.ContributionLink")) return fail("contribution-and-qualification", "you cannot have Qualification and Contribution links between the same two elements (iStar 2.0 Guide, Page 15)");
671
+ if (!is(graph, source, "Quality")) return fail$1("invalid-source", "the source of a Qualification link must be a Quality (iStar 2.0 Guide, Table 1)");
672
+ if (!is(graph, target, "Goal", "Task", "Resource")) return fail$1("invalid-target", "the target of a Qualification link must be a Goal, a Task or a Resource (iStar 2.0 Guide, Table 1)");
673
+ if (source.id === target.id) return fail$1("self-link", "you cannot make a Qualification link from an element onto itself");
674
+ if (isDependum(source) || isDependum(target)) return fail$1("dependum-endpoint", "you cannot make a Qualification link with a dependum (iStar 2.0 Guide, Page 14)");
675
+ if (source.parent !== target.parent) return fail$1("different-actors", "the source and target of a Qualification link must pertain to the same actor (iStar 2.0 Guide, Page 14)");
676
+ if (graph.isThereLinkBetween(source, target, "istar.QualificationLink")) return fail$1("duplicate-link", "there can only be one Qualification link between the same two elements");
677
+ if (graph.isThereLinkBetween(source, target, "istar.ContributionLink")) return fail$1("contribution-and-qualification", "you cannot have Qualification and Contribution links between the same two elements (iStar 2.0 Guide, Page 15)");
658
678
  return OK;
659
679
  };
660
680
  /** The words used for a list of kind selectors in messages: "a Goal or a Task". */
@@ -682,15 +702,15 @@ function matchesSelector(graph, element, selector) {
682
702
  function declarativeCheck(graph, definition, rules, rulesKind, source, target) {
683
703
  const name = definition.label;
684
704
  const article = /^[aeiou]/i.test(name) ? "an" : "a";
685
- if (!rules.sources.some((selector) => matchesSelector(graph, source, selector))) return fail("invalid-source", `the source of ${article} ${name} link must be ${describe(graph, rules.sources)}`);
686
- if (!rules.targets.some((selector) => matchesSelector(graph, target, selector))) return fail("invalid-target", `the target of ${article} ${name} link must be ${describe(graph, rules.targets)}`);
687
- if (!rules.allowSelf && source.id === target.id) return fail("self-link", `you cannot make ${article} ${name} link from an element onto itself`);
705
+ if (!rules.sources.some((selector) => matchesSelector(graph, source, selector))) return fail$1("invalid-source", `the source of ${article} ${name} link must be ${describe(graph, rules.sources)}`);
706
+ if (!rules.targets.some((selector) => matchesSelector(graph, target, selector))) return fail$1("invalid-target", `the target of ${article} ${name} link must be ${describe(graph, rules.targets)}`);
707
+ if (!rules.allowSelf && source.id === target.id) return fail$1("self-link", `you cannot make ${article} ${name} link from an element onto itself`);
688
708
  if (definition.category === "node") {
689
- if (!rules.allowDependum && (isDependum(source) || isDependum(target))) return fail("dependum-endpoint", `you cannot make ${article} ${name} link with a dependum`);
690
- if (rules.sameActor !== false && source.parent !== target.parent) return fail("different-actors", `the source and target of ${article} ${name} link must pertain to the same actor (iStar 2.0 Guide, Page 14)`);
709
+ if (!rules.allowDependum && (isDependum(source) || isDependum(target))) return fail$1("dependum-endpoint", `you cannot make ${article} ${name} link with a dependum`);
710
+ if (rules.sameActor !== false && source.parent !== target.parent) return fail$1("different-actors", `the source and target of ${article} ${name} link must pertain to the same actor (iStar 2.0 Guide, Page 14)`);
691
711
  }
692
712
  const unique = rules.unique ?? "kind";
693
- if (unique === "any" && graph.isThereLinkBetween(source, target) || unique === "kind" && graph.isThereLinkOfKindBetween(source, target, definition.kind) || unique === "kind" && rulesKind !== definition.kind && graph.isThereLinkOfKindBetween(source, target, rulesKind)) return fail("duplicate-link", `there can only be one ${name} link between the same two elements`);
713
+ if (unique === "any" && graph.isThereLinkBetween(source, target) || unique === "kind" && graph.isThereLinkOfKindBetween(source, target, definition.kind) || unique === "kind" && rulesKind !== definition.kind && graph.isThereLinkOfKindBetween(source, target, rulesKind)) return fail$1("duplicate-link", `there can only be one ${name} link between the same two elements`);
694
714
  return OK;
695
715
  }
696
716
  const CHECKS = {
@@ -749,6 +769,126 @@ function validateModel(model, options) {
749
769
  return issues;
750
770
  }
751
771
  //#endregion
772
+ //#region src/file-metamodel.ts
773
+ /** The top-level key of the block in piStar JSON. */
774
+ const FILE_METAMODEL_KEY = "metamodel";
775
+ function isObject(value) {
776
+ return typeof value === "object" && value !== null && !Array.isArray(value);
777
+ }
778
+ function fail(path, message) {
779
+ throw new MetamodelError(`${path} ${message}`);
780
+ }
781
+ function optionalString(entry, key, path) {
782
+ if (entry[key] !== void 0 && typeof entry[key] !== "string") fail(`${path}.${key}`, "must be a string");
783
+ }
784
+ function optionalBoolean(entry, key, path) {
785
+ if (entry[key] !== void 0 && typeof entry[key] !== "boolean") fail(`${path}.${key}`, "must be true or false");
786
+ }
787
+ function checkSize(value, path) {
788
+ if (value === void 0) return;
789
+ if (!isObject(value)) fail(path, "must be an object { width, height }");
790
+ for (const key of ["width", "height"]) {
791
+ const n = value[key];
792
+ if (typeof n !== "number" || !Number.isFinite(n) || n <= 0) fail(`${path}.${key}`, "must be a positive number");
793
+ }
794
+ }
795
+ function checkShape(value, path) {
796
+ if (value === void 0) return;
797
+ if (!isObject(value)) fail(path, "must be an object { path, viewBox? }");
798
+ if (typeof value.path !== "string" || value.path.trim() === "") fail(`${path}.path`, "must be SVG path data");
799
+ optionalString(value, "viewBox", path);
800
+ }
801
+ function checkTextBox(value, path) {
802
+ if (value === void 0) return;
803
+ if (!isObject(value)) fail(path, "must be an object { top, right, bottom, left }");
804
+ for (const key of [
805
+ "top",
806
+ "right",
807
+ "bottom",
808
+ "left"
809
+ ]) {
810
+ const n = value[key];
811
+ if (typeof n !== "number" || n < 0 || n >= 1) fail(`${path}.${key}`, "must be a fraction from 0 (inclusive) to 1 (exclusive)");
812
+ }
813
+ }
814
+ function checkLine(value, path) {
815
+ if (value === void 0) return;
816
+ if (!isObject(value)) fail(path, "must be an object { dash?, marker?, markerFilled? }");
817
+ optionalString(value, "dash", path);
818
+ if (value.marker !== void 0 && value.marker !== false && typeof value.marker !== "string") fail(`${path}.marker`, "must be SVG path data or false");
819
+ optionalBoolean(value, "markerFilled", path);
820
+ }
821
+ function checkStringList(value, path) {
822
+ if (!Array.isArray(value) || value.length === 0 || value.some((v) => typeof v !== "string")) fail(path, "must be a non-empty list of kind names or categories");
823
+ }
824
+ function checkRules(value, path) {
825
+ if (!isObject(value)) fail(path, "must be an object { sources, targets, … }");
826
+ checkStringList(value.sources, `${path}.sources`);
827
+ checkStringList(value.targets, `${path}.targets`);
828
+ for (const key of [
829
+ "sameActor",
830
+ "allowDependum",
831
+ "allowSelf"
832
+ ]) optionalBoolean(value, key, path);
833
+ if (value.unique !== void 0 && ![
834
+ "kind",
835
+ "any",
836
+ false
837
+ ].includes(value.unique)) fail(`${path}.unique`, "must be \"kind\", \"any\" or false");
838
+ }
839
+ /**
840
+ * Checks that `value` is a well-formed `"metamodel"` block and returns it typed. Throws
841
+ * `MetamodelError` naming the offending path (`metamodel.elements[1].kind must be a string`).
842
+ * Whether its kinds fit a given metamodel (namespacing, collisions, `behavesLike` targets) is
843
+ * checked when it is applied, by `extendMetamodel`.
844
+ */
845
+ function validateFileMetamodel(value, path = FILE_METAMODEL_KEY) {
846
+ if (!isObject(value)) fail(path, "must be an object { name, elements?, links? }");
847
+ if (typeof value.name !== "string" || value.name === "") fail(`${path}.name`, "must be a non-empty string");
848
+ for (const [listKey, kind] of [["elements", "element"], ["links", "link"]]) {
849
+ const list = value[listKey];
850
+ if (list === void 0) continue;
851
+ if (!Array.isArray(list)) fail(`${path}.${listKey}`, "must be a list");
852
+ list.forEach((entry, i) => {
853
+ const at = `${path}.${listKey}[${i}]`;
854
+ if (!isObject(entry)) fail(at, `must be an object describing a ${kind} kind`);
855
+ if (typeof entry.kind !== "string" || entry.kind === "") fail(`${at}.kind`, "must be a string");
856
+ optionalString(entry, "label", at);
857
+ optionalString(entry, "behavesLike", at);
858
+ optionalString(entry, "pistarType", at);
859
+ if (kind === "element") {
860
+ if (entry.category !== void 0 && entry.category !== "node" && entry.category !== "actor") fail(`${at}.category`, "must be \"node\" or \"actor\"");
861
+ checkSize(entry.size, `${at}.size`);
862
+ if (entry.info !== void 0) {
863
+ if (!isObject(entry.info)) fail(`${at}.info`, "must be an object");
864
+ for (const key of [
865
+ "canBeInnerElement",
866
+ "canBeDependum",
867
+ "canBeOnPaper"
868
+ ]) optionalBoolean(entry.info, key, `${at}.info`);
869
+ }
870
+ checkShape(entry.shape, `${at}.shape`);
871
+ checkTextBox(entry.textBox, `${at}.textBox`);
872
+ } else {
873
+ if (entry.category !== void 0 && ![
874
+ "node",
875
+ "actor",
876
+ "dependency"
877
+ ].includes(entry.category)) fail(`${at}.category`, "must be \"node\", \"actor\" or \"dependency\"");
878
+ if (entry.rules !== void 0) checkRules(entry.rules, `${at}.rules`);
879
+ if ("check" in entry) fail(`${at}.check`, "cannot be stored in a file (it is code)");
880
+ if (entry.info !== void 0 && !isObject(entry.info)) fail(`${at}.info`, "must be an object");
881
+ checkLine(entry.line, `${at}.line`);
882
+ }
883
+ });
884
+ }
885
+ return value;
886
+ }
887
+ /** The block as an extension `extendMetamodel` applies (it ignores the extra keys). */
888
+ function fileMetamodelExtension(block) {
889
+ return block;
890
+ }
891
+ //#endregion
752
892
  //#region src/serialization.ts
753
893
  var PistarParseError = class extends Error {
754
894
  name = "PistarParseError";
@@ -826,7 +966,7 @@ function asArray(value, where) {
826
966
  return value;
827
967
  }
828
968
  function parsePistar(input, options = {}) {
829
- const metamodel = options.metamodel ?? ISTAR_2_0;
969
+ let metamodel = options.metamodel ?? ISTAR_2_0;
830
970
  const categoryOf = (kind) => metamodel.elements.get(kind)?.category;
831
971
  let json;
832
972
  if (typeof input === "string") try {
@@ -836,6 +976,12 @@ function parsePistar(input, options = {}) {
836
976
  }
837
977
  else json = structuredClone(input);
838
978
  if (!isRecord(json)) throw new PistarParseError("a piStar model must be a JSON object");
979
+ let host;
980
+ if (options.fileMetamodel && json["metamodel"] !== void 0) {
981
+ const block = validateFileMetamodel(json[FILE_METAMODEL_KEY]);
982
+ host = metamodel;
983
+ metamodel = extendMetamodel(metamodel, fileMetamodelExtension(block));
984
+ }
839
985
  const display = isRecord(json.display) ? json.display : {};
840
986
  const usedDisplay = /* @__PURE__ */ new Set();
841
987
  const elements = /* @__PURE__ */ new Map();
@@ -936,6 +1082,7 @@ function parsePistar(input, options = {}) {
936
1082
  keys: Object.keys(json),
937
1083
  display: Object.keys(display)
938
1084
  });
1085
+ setHostMetamodel(model, host);
939
1086
  return withMetamodel(model, metamodel);
940
1087
  }
941
1088
  function optional(key, value) {
@@ -1062,6 +1209,44 @@ function toPistarObject(input, options = {}) {
1062
1209
  function toPistar(model, options) {
1063
1210
  return JSON.stringify(toPistarObject(model, options), null, 2);
1064
1211
  }
1212
+ /**
1213
+ * The constructs the model's file declares in its `"metamodel"` block, when they were applied
1214
+ * (read with `fileMetamodel: true`, or set with `withFileMetamodel`); `undefined` otherwise.
1215
+ * Kinds the host supplied are never part of it.
1216
+ */
1217
+ function fileMetamodelOf(model) {
1218
+ if (!hostMetamodelOf(model)) return void 0;
1219
+ return model.extra?.[FILE_METAMODEL_KEY];
1220
+ }
1221
+ /**
1222
+ * A copy of `model` whose file declares `block` (`null` removes the block): its metamodel is the
1223
+ * host's extended by `block`, and `toPistar` writes the block. This is how a tool's "Add new"
1224
+ * construct joins a model. Throws `MetamodelError` if the block is malformed, collides with the
1225
+ * host's kinds, or leaves out a kind the model still uses.
1226
+ */
1227
+ function withFileMetamodel(model, block) {
1228
+ const any = model;
1229
+ const host = hostMetamodelOf(any) ?? metamodelOf(any);
1230
+ let next = host;
1231
+ if (block) {
1232
+ validateFileMetamodel(block);
1233
+ next = extendMetamodel(host, fileMetamodelExtension(block));
1234
+ }
1235
+ for (const element of any.elements.values()) if (!next.elements.has(element.kind)) throw new MetamodelError(`element "${element.id}" uses kind "${element.kind}", which the new metamodel lacks`);
1236
+ for (const link of any.links.values()) if (!next.links.has(link.kind)) throw new MetamodelError(`link "${link.id}" uses kind "${link.kind}", which the new metamodel lacks`);
1237
+ const { [FILE_METAMODEL_KEY]: _previous, ...others } = any.extra ?? {};
1238
+ const extra = block ? {
1239
+ ...any.extra,
1240
+ [FILE_METAMODEL_KEY]: structuredClone(block)
1241
+ } : others;
1242
+ const { extra: _extra, ...rest } = any;
1243
+ const result = inheritSourceLayout(any, {
1244
+ ...rest,
1245
+ ...Object.keys(extra).length > 0 ? { extra } : {}
1246
+ });
1247
+ setHostMetamodel(result, block ? host : void 0);
1248
+ return withMetamodel(result, next);
1249
+ }
1065
1250
  //#endregion
1066
1251
  //#region src/operations.ts
1067
1252
  var ModelOperationError = class extends Error {
@@ -1841,6 +2026,6 @@ function validateModelProperties(model, schemas) {
1841
2026
  return issues;
1842
2027
  }
1843
2028
  //#endregion
1844
- export { ACTOR_KINDS, ACTOR_LINK_KINDS, CONTRIBUTION_LABELS, DEFAULT_DIAGRAM, DEFAULT_ELEMENT_SIZE, DEPENDENCY_LINK_KINDS, ELEMENT_KINDS, ISTAR_2_0, ISTAR_PREFIX, ISTAR_VERSION, LINK_KINDS, LINK_KIND_INFO, METAMODEL_VERSION, MetamodelError, ModelOperationError, NODE_KINDS, NODE_KIND_INFO, NODE_LINK_KINDS, PISTAR_TOOL, PistarParseError, PistarWriteError, addDependency, addElement, boolean, canLink, childrenOf, connect, createEmptyModel, createModelStore, defaultIdGenerator, defineMetamodelExtension, defineProperties, dependencyEnds, dependencyLinksOf, disconnect, effectiveElementKind, effectiveLinkKind, elementBehavesLike, elementKindDefinition, enumOf, extendMetamodel, getSourceLayout, inheritMetamodel, inheritSourceLayout, isActor, isActorIn, isActorKind, isActorKindIn, isActorLinkKind, isContributionLabel, isDependencyLinkKind, isElementKind, isLinkKind, isNode, isNodeIn, isNodeKind, isNodeKindIn, isNodeLinkKind, linkKindDefinition, linksOf, metamodelOf, moveElement, nestElement, number, parsePistar, prop, removeElement, removeElements, shortKindName, string, toPistar, toPistarObject, updateDiagram, updateElement, updateLink, validateModel, validateModelProperties, withMetamodel };
2029
+ export { ACTOR_KINDS, ACTOR_LINK_KINDS, CONTRIBUTION_LABELS, DEFAULT_DIAGRAM, DEFAULT_ELEMENT_SIZE, DEPENDENCY_LINK_KINDS, ELEMENT_KINDS, FILE_METAMODEL_KEY, ISTAR_2_0, ISTAR_PREFIX, ISTAR_VERSION, LINK_KINDS, LINK_KIND_INFO, METAMODEL_VERSION, MetamodelError, ModelOperationError, NODE_KINDS, NODE_KIND_INFO, NODE_LINK_KINDS, PISTAR_TOOL, PistarParseError, PistarWriteError, addDependency, addElement, boolean, canLink, childrenOf, connect, createEmptyModel, createModelStore, defaultIdGenerator, defineMetamodelExtension, defineProperties, dependencyEnds, dependencyLinksOf, disconnect, effectiveElementKind, effectiveLinkKind, elementBehavesLike, elementKindDefinition, enumOf, extendMetamodel, fileMetamodelExtension, fileMetamodelOf, getSourceLayout, hostMetamodelOf, inheritMetamodel, inheritSourceLayout, isActor, isActorIn, isActorKind, isActorKindIn, isActorLinkKind, isContributionLabel, isDependencyLinkKind, isElementKind, isLinkKind, isNode, isNodeIn, isNodeKind, isNodeKindIn, isNodeLinkKind, linkKindDefinition, linksOf, metamodelOf, moveElement, nestElement, number, parsePistar, prop, removeElement, removeElements, setHostMetamodel, shortKindName, string, toPistar, toPistarObject, updateDiagram, updateElement, updateLink, validateFileMetamodel, validateModel, validateModelProperties, withFileMetamodel, withMetamodel };
1845
2030
 
1846
2031
  //# sourceMappingURL=index.js.map