@istar-ts/core 0.10.1 → 0.12.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.cjs CHANGED
@@ -268,7 +268,9 @@ function extendMetamodel(base, extension) {
268
268
  } } : {},
269
269
  ...parent ? { behavesLike: parent.kind } : {},
270
270
  pistarType: type,
271
- extension: extension.name
271
+ extension: extension.name,
272
+ ...declaration.shape ? { shape: declaration.shape } : {},
273
+ ...declaration.textBox ? { textBox: declaration.textBox } : {}
272
274
  };
273
275
  elements.set(kind, definition);
274
276
  elementsByType.set(type, kind);
@@ -301,7 +303,8 @@ function extendMetamodel(base, extension) {
301
303
  ...declaration.rules ? { rules: declaration.rules } : {},
302
304
  ...declaration.check ? { check: declaration.check } : {},
303
305
  pistarType: type,
304
- extension: extension.name
306
+ extension: extension.name,
307
+ ...declaration.line ? { line: declaration.line } : {}
305
308
  };
306
309
  links.set(kind, definition);
307
310
  linksByType.set(type, kind);
@@ -392,10 +395,27 @@ function withMetamodel(model, metamodel) {
392
395
  else metamodels.set(model, metamodel);
393
396
  return model;
394
397
  }
398
+ /**
399
+ * For models whose file declares part of their metamodel (see `withFileMetamodel`): the
400
+ * metamodel the host supplied, without the file's kinds.
401
+ */
402
+ const hostMetamodels = /* @__PURE__ */ new WeakMap();
403
+ /** @internal Records the host part of a model's metamodel (used by serialization). */
404
+ function setHostMetamodel(model, metamodel) {
405
+ if (metamodel) hostMetamodels.set(model, metamodel);
406
+ else hostMetamodels.delete(model);
407
+ }
408
+ /** @internal The host part of a model's metamodel, if the file declares the rest. */
409
+ function hostMetamodelOf(model) {
410
+ return hostMetamodels.get(model);
411
+ }
395
412
  /** Gives `to` the metamodel of `from` (used for every model an operation derives). */
396
413
  function inheritMetamodel(from, to) {
414
+ if (from === to) return to;
397
415
  const metamodel = metamodels.get(from);
398
- if (metamodel && from !== to) metamodels.set(to, metamodel);
416
+ if (metamodel) metamodels.set(to, metamodel);
417
+ const host = hostMetamodels.get(from);
418
+ if (host) hostMetamodels.set(to, host);
399
419
  return to;
400
420
  }
401
421
  function createEmptyModel(diagram = DEFAULT_DIAGRAM, options = {}) {
@@ -467,7 +487,7 @@ function dependencyLinksOf(model, dependumId) {
467
487
  //#endregion
468
488
  //#region src/constraints.ts
469
489
  const OK = { ok: true };
470
- function fail(code, reason) {
490
+ function fail$1(code, reason) {
471
491
  return {
472
492
  ok: false,
473
493
  code,
@@ -488,10 +508,10 @@ function canLink(model, source, target, kind, options = {}) {
488
508
  const metamodel = options.metamodel ?? metamodelOf(model);
489
509
  const s = resolve(any, source);
490
510
  const t = resolve(any, target);
491
- if (!s) return fail("unknown-element", `unknown source element "${refId(source)}"`);
492
- if (!t) return fail("unknown-element", `unknown target element "${refId(target)}"`);
511
+ if (!s) return fail$1("unknown-element", `unknown source element "${refId(source)}"`);
512
+ if (!t) return fail$1("unknown-element", `unknown target element "${refId(target)}"`);
493
513
  const definition = metamodel.links.get(kind);
494
- if (!definition) return fail("unknown-link-kind", `unknown link kind "${String(kind)}"`);
514
+ if (!definition) return fail$1("unknown-link-kind", `unknown link kind "${String(kind)}"`);
495
515
  return checkWith(new Graph(any, metamodel), definition, s, t);
496
516
  }
497
517
  /** Runs the rule a link kind resolves to, then every extra predicate along its chain. */
@@ -503,7 +523,7 @@ function checkWith(graph, definition, source, target) {
503
523
  let result;
504
524
  if (root.rules) result = declarativeCheck(graph, definition, root.rules, root.kind, source, target);
505
525
  else if (builtIn) result = builtIn(graph, source, target);
506
- else result = fail("unknown-link-kind", `link kind "${definition.kind}" has no rules`);
526
+ else result = fail$1("unknown-link-kind", `link kind "${definition.kind}" has no rules`);
507
527
  if (!result.ok) return result;
508
528
  const context = graph.context(definition.kind, source, target);
509
529
  for (const step of chain.toReversed()) {
@@ -587,18 +607,18 @@ const is = (graph, element, ...shortNames) => {
587
607
  };
588
608
  const isDependum = (element) => element.isDependum === true;
589
609
  const isALink = (graph, source, target) => {
590
- 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).");
591
- 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).");
592
- 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).");
593
- if (source.id === target.id) return fail("self-link", "you cannot make Is-A links from an actor onto itself.");
594
- 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).");
610
+ 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).");
611
+ 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).");
612
+ 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).");
613
+ if (source.id === target.id) return fail$1("self-link", "you cannot make Is-A links from an actor onto itself.");
614
+ 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).");
595
615
  return OK;
596
616
  };
597
617
  const participatesInLink = (graph, source, target) => {
598
- 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)");
599
- 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)");
600
- if (source.id === target.id) return fail("self-link", "you cannot make a Participates-In link from an actor onto itself.");
601
- 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).");
618
+ 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)");
619
+ 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)");
620
+ if (source.id === target.id) return fail$1("self-link", "you cannot make a Participates-In link from an actor onto itself.");
621
+ 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).");
602
622
  return OK;
603
623
  };
604
624
  /** The actor an element belongs to: itself for actors, its parent for inner elements. */
@@ -606,56 +626,56 @@ function owningActorId(graph, element) {
606
626
  return graph.isActor(element) ? element.id : element.parent;
607
627
  }
608
628
  const dependencyLink = (graph, source, target) => {
609
- if (source.id === target.id) return fail("self-link", "a Dependency link cannot link an element onto itself");
610
- if (isDependum(source)) return fail("dependum-endpoint", "a Dependency link cannot start from a dependum");
611
- if (isDependum(target)) return fail("dependum-endpoint", "a Dependency link cannot end in a dependum");
612
- 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)");
613
- 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.");
614
- 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.");
629
+ if (source.id === target.id) return fail$1("self-link", "a Dependency link cannot link an element onto itself");
630
+ if (isDependum(source)) return fail$1("dependum-endpoint", "a Dependency link cannot start from a dependum");
631
+ if (isDependum(target)) return fail$1("dependum-endpoint", "a Dependency link cannot end in a dependum");
632
+ 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)");
633
+ 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.");
634
+ 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.");
615
635
  return OK;
616
636
  };
617
637
  function refinementLink(label) {
618
638
  const other = label === "AND" ? "istar.OrRefinementLink" : "istar.AndRefinementLink";
619
639
  return (graph, source, target) => {
620
- 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)`);
621
- 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)`);
622
- if (source.id === target.id) return fail("self-link", `you cannot make an ${label}-refinement link from an element onto itself`);
623
- 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)`);
624
- 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)`);
625
- if (graph.isThereLinkBetween(source, target)) return fail("duplicate-link", "there can only be one refinement link between the same two elements");
626
- 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.");
627
- 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).");
640
+ 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)`);
641
+ 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)`);
642
+ if (source.id === target.id) return fail$1("self-link", `you cannot make an ${label}-refinement link from an element onto itself`);
643
+ 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)`);
644
+ 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)`);
645
+ if (graph.isThereLinkBetween(source, target)) return fail$1("duplicate-link", "there can only be one refinement link between the same two elements");
646
+ 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.");
647
+ 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).");
628
648
  return OK;
629
649
  };
630
650
  }
631
651
  const neededByLink = (graph, source, target) => {
632
- 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)");
633
- 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)");
634
- if (source.id === target.id) return fail("self-link", "you cannot make a Needed-By link from an element onto itself");
635
- 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)");
636
- 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)");
637
- if (graph.isThereLinkBetween(source, target)) return fail("duplicate-link", "there can only be one Needed-By link between the same two elements");
652
+ 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)");
653
+ 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)");
654
+ if (source.id === target.id) return fail$1("self-link", "you cannot make a Needed-By link from an element onto itself");
655
+ 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)");
656
+ 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)");
657
+ if (graph.isThereLinkBetween(source, target)) return fail$1("duplicate-link", "there can only be one Needed-By link between the same two elements");
638
658
  return OK;
639
659
  };
640
660
  const contributionLink = (graph, source, target) => {
641
- 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)");
642
- 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)");
643
- 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)");
644
- if (isDependum(source) || isDependum(target)) return fail("dependum-endpoint", "you cannot make a Contribution link with a dependum (iStar 2.0 Guide, Page 14)");
645
- 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)");
646
- if (graph.isThereLinkBetween(source, target, "istar.ContributionLink")) return fail("duplicate-link", "there can only be one Contribution link between the same two elements");
647
- 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)");
648
- 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.");
661
+ 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)");
662
+ 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)");
663
+ 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)");
664
+ 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)");
665
+ 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)");
666
+ if (graph.isThereLinkBetween(source, target, "istar.ContributionLink")) return fail$1("duplicate-link", "there can only be one Contribution link between the same two elements");
667
+ 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)");
668
+ 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.");
649
669
  return OK;
650
670
  };
651
671
  const qualificationLink = (graph, source, target) => {
652
- 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)");
653
- 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)");
654
- if (source.id === target.id) return fail("self-link", "you cannot make a Qualification link from an element onto itself");
655
- if (isDependum(source) || isDependum(target)) return fail("dependum-endpoint", "you cannot make a Qualification link with a dependum (iStar 2.0 Guide, Page 14)");
656
- 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)");
657
- if (graph.isThereLinkBetween(source, target, "istar.QualificationLink")) return fail("duplicate-link", "there can only be one Qualification link between the same two elements");
658
- 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)");
672
+ 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)");
673
+ 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)");
674
+ if (source.id === target.id) return fail$1("self-link", "you cannot make a Qualification link from an element onto itself");
675
+ 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)");
676
+ 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)");
677
+ if (graph.isThereLinkBetween(source, target, "istar.QualificationLink")) return fail$1("duplicate-link", "there can only be one Qualification link between the same two elements");
678
+ 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)");
659
679
  return OK;
660
680
  };
661
681
  /** The words used for a list of kind selectors in messages: "a Goal or a Task". */
@@ -683,15 +703,15 @@ function matchesSelector(graph, element, selector) {
683
703
  function declarativeCheck(graph, definition, rules, rulesKind, source, target) {
684
704
  const name = definition.label;
685
705
  const article = /^[aeiou]/i.test(name) ? "an" : "a";
686
- 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)}`);
687
- 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)}`);
688
- if (!rules.allowSelf && source.id === target.id) return fail("self-link", `you cannot make ${article} ${name} link from an element onto itself`);
706
+ 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)}`);
707
+ 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)}`);
708
+ if (!rules.allowSelf && source.id === target.id) return fail$1("self-link", `you cannot make ${article} ${name} link from an element onto itself`);
689
709
  if (definition.category === "node") {
690
- if (!rules.allowDependum && (isDependum(source) || isDependum(target))) return fail("dependum-endpoint", `you cannot make ${article} ${name} link with a dependum`);
691
- 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)`);
710
+ if (!rules.allowDependum && (isDependum(source) || isDependum(target))) return fail$1("dependum-endpoint", `you cannot make ${article} ${name} link with a dependum`);
711
+ 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)`);
692
712
  }
693
713
  const unique = rules.unique ?? "kind";
694
- 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`);
714
+ 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`);
695
715
  return OK;
696
716
  }
697
717
  const CHECKS = {
@@ -750,6 +770,126 @@ function validateModel(model, options) {
750
770
  return issues;
751
771
  }
752
772
  //#endregion
773
+ //#region src/file-metamodel.ts
774
+ /** The top-level key of the block in piStar JSON. */
775
+ const FILE_METAMODEL_KEY = "metamodel";
776
+ function isObject(value) {
777
+ return typeof value === "object" && value !== null && !Array.isArray(value);
778
+ }
779
+ function fail(path, message) {
780
+ throw new MetamodelError(`${path} ${message}`);
781
+ }
782
+ function optionalString(entry, key, path) {
783
+ if (entry[key] !== void 0 && typeof entry[key] !== "string") fail(`${path}.${key}`, "must be a string");
784
+ }
785
+ function optionalBoolean(entry, key, path) {
786
+ if (entry[key] !== void 0 && typeof entry[key] !== "boolean") fail(`${path}.${key}`, "must be true or false");
787
+ }
788
+ function checkSize(value, path) {
789
+ if (value === void 0) return;
790
+ if (!isObject(value)) fail(path, "must be an object { width, height }");
791
+ for (const key of ["width", "height"]) {
792
+ const n = value[key];
793
+ if (typeof n !== "number" || !Number.isFinite(n) || n <= 0) fail(`${path}.${key}`, "must be a positive number");
794
+ }
795
+ }
796
+ function checkShape(value, path) {
797
+ if (value === void 0) return;
798
+ if (!isObject(value)) fail(path, "must be an object { path, viewBox? }");
799
+ if (typeof value.path !== "string" || value.path.trim() === "") fail(`${path}.path`, "must be SVG path data");
800
+ optionalString(value, "viewBox", path);
801
+ }
802
+ function checkTextBox(value, path) {
803
+ if (value === void 0) return;
804
+ if (!isObject(value)) fail(path, "must be an object { top, right, bottom, left }");
805
+ for (const key of [
806
+ "top",
807
+ "right",
808
+ "bottom",
809
+ "left"
810
+ ]) {
811
+ const n = value[key];
812
+ if (typeof n !== "number" || n < 0 || n >= 1) fail(`${path}.${key}`, "must be a fraction from 0 (inclusive) to 1 (exclusive)");
813
+ }
814
+ }
815
+ function checkLine(value, path) {
816
+ if (value === void 0) return;
817
+ if (!isObject(value)) fail(path, "must be an object { dash?, marker?, markerFilled? }");
818
+ optionalString(value, "dash", path);
819
+ if (value.marker !== void 0 && value.marker !== false && typeof value.marker !== "string") fail(`${path}.marker`, "must be SVG path data or false");
820
+ optionalBoolean(value, "markerFilled", path);
821
+ }
822
+ function checkStringList(value, path) {
823
+ 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");
824
+ }
825
+ function checkRules(value, path) {
826
+ if (!isObject(value)) fail(path, "must be an object { sources, targets, … }");
827
+ checkStringList(value.sources, `${path}.sources`);
828
+ checkStringList(value.targets, `${path}.targets`);
829
+ for (const key of [
830
+ "sameActor",
831
+ "allowDependum",
832
+ "allowSelf"
833
+ ]) optionalBoolean(value, key, path);
834
+ if (value.unique !== void 0 && ![
835
+ "kind",
836
+ "any",
837
+ false
838
+ ].includes(value.unique)) fail(`${path}.unique`, "must be \"kind\", \"any\" or false");
839
+ }
840
+ /**
841
+ * Checks that `value` is a well-formed `"metamodel"` block and returns it typed. Throws
842
+ * `MetamodelError` naming the offending path (`metamodel.elements[1].kind must be a string`).
843
+ * Whether its kinds fit a given metamodel (namespacing, collisions, `behavesLike` targets) is
844
+ * checked when it is applied, by `extendMetamodel`.
845
+ */
846
+ function validateFileMetamodel(value, path = FILE_METAMODEL_KEY) {
847
+ if (!isObject(value)) fail(path, "must be an object { name, elements?, links? }");
848
+ if (typeof value.name !== "string" || value.name === "") fail(`${path}.name`, "must be a non-empty string");
849
+ for (const [listKey, kind] of [["elements", "element"], ["links", "link"]]) {
850
+ const list = value[listKey];
851
+ if (list === void 0) continue;
852
+ if (!Array.isArray(list)) fail(`${path}.${listKey}`, "must be a list");
853
+ list.forEach((entry, i) => {
854
+ const at = `${path}.${listKey}[${i}]`;
855
+ if (!isObject(entry)) fail(at, `must be an object describing a ${kind} kind`);
856
+ if (typeof entry.kind !== "string" || entry.kind === "") fail(`${at}.kind`, "must be a string");
857
+ optionalString(entry, "label", at);
858
+ optionalString(entry, "behavesLike", at);
859
+ optionalString(entry, "pistarType", at);
860
+ if (kind === "element") {
861
+ if (entry.category !== void 0 && entry.category !== "node" && entry.category !== "actor") fail(`${at}.category`, "must be \"node\" or \"actor\"");
862
+ checkSize(entry.size, `${at}.size`);
863
+ if (entry.info !== void 0) {
864
+ if (!isObject(entry.info)) fail(`${at}.info`, "must be an object");
865
+ for (const key of [
866
+ "canBeInnerElement",
867
+ "canBeDependum",
868
+ "canBeOnPaper"
869
+ ]) optionalBoolean(entry.info, key, `${at}.info`);
870
+ }
871
+ checkShape(entry.shape, `${at}.shape`);
872
+ checkTextBox(entry.textBox, `${at}.textBox`);
873
+ } else {
874
+ if (entry.category !== void 0 && ![
875
+ "node",
876
+ "actor",
877
+ "dependency"
878
+ ].includes(entry.category)) fail(`${at}.category`, "must be \"node\", \"actor\" or \"dependency\"");
879
+ if (entry.rules !== void 0) checkRules(entry.rules, `${at}.rules`);
880
+ if ("check" in entry) fail(`${at}.check`, "cannot be stored in a file (it is code)");
881
+ if (entry.info !== void 0 && !isObject(entry.info)) fail(`${at}.info`, "must be an object");
882
+ checkLine(entry.line, `${at}.line`);
883
+ }
884
+ });
885
+ }
886
+ return value;
887
+ }
888
+ /** The block as an extension `extendMetamodel` applies (it ignores the extra keys). */
889
+ function fileMetamodelExtension(block) {
890
+ return block;
891
+ }
892
+ //#endregion
753
893
  //#region src/serialization.ts
754
894
  var PistarParseError = class extends Error {
755
895
  name = "PistarParseError";
@@ -827,7 +967,7 @@ function asArray(value, where) {
827
967
  return value;
828
968
  }
829
969
  function parsePistar(input, options = {}) {
830
- const metamodel = options.metamodel ?? ISTAR_2_0;
970
+ let metamodel = options.metamodel ?? ISTAR_2_0;
831
971
  const categoryOf = (kind) => metamodel.elements.get(kind)?.category;
832
972
  let json;
833
973
  if (typeof input === "string") try {
@@ -837,6 +977,12 @@ function parsePistar(input, options = {}) {
837
977
  }
838
978
  else json = structuredClone(input);
839
979
  if (!isRecord(json)) throw new PistarParseError("a piStar model must be a JSON object");
980
+ let host;
981
+ if (options.fileMetamodel && json["metamodel"] !== void 0) {
982
+ const block = validateFileMetamodel(json[FILE_METAMODEL_KEY]);
983
+ host = metamodel;
984
+ metamodel = extendMetamodel(metamodel, fileMetamodelExtension(block));
985
+ }
840
986
  const display = isRecord(json.display) ? json.display : {};
841
987
  const usedDisplay = /* @__PURE__ */ new Set();
842
988
  const elements = /* @__PURE__ */ new Map();
@@ -937,6 +1083,7 @@ function parsePistar(input, options = {}) {
937
1083
  keys: Object.keys(json),
938
1084
  display: Object.keys(display)
939
1085
  });
1086
+ setHostMetamodel(model, host);
940
1087
  return withMetamodel(model, metamodel);
941
1088
  }
942
1089
  function optional(key, value) {
@@ -1063,6 +1210,44 @@ function toPistarObject(input, options = {}) {
1063
1210
  function toPistar(model, options) {
1064
1211
  return JSON.stringify(toPistarObject(model, options), null, 2);
1065
1212
  }
1213
+ /**
1214
+ * The constructs the model's file declares in its `"metamodel"` block, when they were applied
1215
+ * (read with `fileMetamodel: true`, or set with `withFileMetamodel`); `undefined` otherwise.
1216
+ * Kinds the host supplied are never part of it.
1217
+ */
1218
+ function fileMetamodelOf(model) {
1219
+ if (!hostMetamodelOf(model)) return void 0;
1220
+ return model.extra?.[FILE_METAMODEL_KEY];
1221
+ }
1222
+ /**
1223
+ * A copy of `model` whose file declares `block` (`null` removes the block): its metamodel is the
1224
+ * host's extended by `block`, and `toPistar` writes the block. This is how a tool's "Add new"
1225
+ * construct joins a model. Throws `MetamodelError` if the block is malformed, collides with the
1226
+ * host's kinds, or leaves out a kind the model still uses.
1227
+ */
1228
+ function withFileMetamodel(model, block) {
1229
+ const any = model;
1230
+ const host = hostMetamodelOf(any) ?? metamodelOf(any);
1231
+ let next = host;
1232
+ if (block) {
1233
+ validateFileMetamodel(block);
1234
+ next = extendMetamodel(host, fileMetamodelExtension(block));
1235
+ }
1236
+ 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`);
1237
+ 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`);
1238
+ const { [FILE_METAMODEL_KEY]: _previous, ...others } = any.extra ?? {};
1239
+ const extra = block ? {
1240
+ ...any.extra,
1241
+ [FILE_METAMODEL_KEY]: structuredClone(block)
1242
+ } : others;
1243
+ const { extra: _extra, ...rest } = any;
1244
+ const result = inheritSourceLayout(any, {
1245
+ ...rest,
1246
+ ...Object.keys(extra).length > 0 ? { extra } : {}
1247
+ });
1248
+ setHostMetamodel(result, block ? host : void 0);
1249
+ return withMetamodel(result, next);
1250
+ }
1066
1251
  //#endregion
1067
1252
  //#region src/operations.ts
1068
1253
  var ModelOperationError = class extends Error {
@@ -1842,13 +2027,185 @@ function validateModelProperties(model, schemas) {
1842
2027
  return issues;
1843
2028
  }
1844
2029
  //#endregion
2030
+ //#region src/diagnostics.ts
2031
+ /** Most severe first. */
2032
+ const DIAGNOSTIC_SEVERITIES = [
2033
+ "error",
2034
+ "warning",
2035
+ "info",
2036
+ "hint"
2037
+ ];
2038
+ const RANK = {
2039
+ error: 0,
2040
+ warning: 1,
2041
+ info: 2,
2042
+ hint: 3
2043
+ };
2044
+ /** Negative when `a` is more severe than `b`. */
2045
+ function compareSeverity(a, b) {
2046
+ return RANK[a] - RANK[b];
2047
+ }
2048
+ /** The most severe severity in a list (what a badge shows), or `undefined` if it is empty. */
2049
+ function worstDiagnosticSeverity(diagnostics) {
2050
+ let worst;
2051
+ for (const d of diagnostics ?? []) {
2052
+ if (worst === void 0 || RANK[d.severity] < RANK[worst]) worst = d.severity;
2053
+ if (worst === "error") break;
2054
+ }
2055
+ return worst;
2056
+ }
2057
+ function identity(d) {
2058
+ return `${d.elementId}\u0000${d.key ?? ""}\u0000${d.message}`;
2059
+ }
2060
+ /**
2061
+ * The union of several lists, in order, without duplicates: diagnostics with the same
2062
+ * `(elementId, key, message)` count once, keeping the first one's position and the most severe
2063
+ * severity among them.
2064
+ */
2065
+ function mergeDiagnostics(lists) {
2066
+ const result = [];
2067
+ const index = /* @__PURE__ */ new Map();
2068
+ for (const list of lists) for (const d of list ?? []) {
2069
+ const id = identity(d);
2070
+ const at = index.get(id);
2071
+ if (at === void 0) {
2072
+ index.set(id, result.length);
2073
+ result.push(d);
2074
+ } else if (RANK[d.severity] < RANK[result[at].severity]) result[at] = {
2075
+ ...result[at],
2076
+ severity: d.severity
2077
+ };
2078
+ }
2079
+ return result;
2080
+ }
2081
+ /** Diagnostics by element (or link) id, each list in the input order. */
2082
+ function groupDiagnostics(diagnostics) {
2083
+ const map = /* @__PURE__ */ new Map();
2084
+ for (const d of diagnostics ?? []) {
2085
+ const list = map.get(d.elementId);
2086
+ if (list) list.push(d);
2087
+ else map.set(d.elementId, [d]);
2088
+ }
2089
+ return map;
2090
+ }
2091
+ const NONE = [];
2092
+ function createDiagnosticsStore() {
2093
+ const bySource = /* @__PURE__ */ new Map();
2094
+ const listeners = /* @__PURE__ */ new Set();
2095
+ let merged = NONE;
2096
+ const changed = () => {
2097
+ merged = bySource.size === 0 ? NONE : mergeDiagnostics(bySource.values());
2098
+ for (const listener of listeners) listener();
2099
+ };
2100
+ return {
2101
+ publish(source, diagnostics) {
2102
+ if (diagnostics.length === 0) {
2103
+ if (bySource.delete(source)) changed();
2104
+ return;
2105
+ }
2106
+ bySource.set(source, diagnostics.map((d) => d.source === void 0 ? {
2107
+ ...d,
2108
+ source
2109
+ } : d));
2110
+ changed();
2111
+ },
2112
+ clear(source) {
2113
+ if (source === void 0) {
2114
+ if (bySource.size === 0) return;
2115
+ bySource.clear();
2116
+ } else if (!bySource.delete(source)) return;
2117
+ changed();
2118
+ },
2119
+ sources: () => [...bySource.keys()],
2120
+ get: (source) => bySource.get(source) ?? NONE,
2121
+ getAll: () => merged,
2122
+ subscribe(listener) {
2123
+ listeners.add(listener);
2124
+ return () => listeners.delete(listener);
2125
+ }
2126
+ };
2127
+ }
2128
+ const LSP_SEVERITIES = [
2129
+ "error",
2130
+ "warning",
2131
+ "info",
2132
+ "hint"
2133
+ ];
2134
+ function anchorOf(data) {
2135
+ if (typeof data !== "object" || data === null) return {};
2136
+ const record = data;
2137
+ const elementId = typeof record.elementId === "string" ? record.elementId : typeof record.nodeId === "string" ? record.nodeId : void 0;
2138
+ return {
2139
+ ...elementId ? { elementId } : {},
2140
+ ...typeof record.key === "string" ? { key: record.key } : {}
2141
+ };
2142
+ }
2143
+ function convertLsp(d, options) {
2144
+ const anchor = anchorOf(d.data);
2145
+ const elementId = anchor.elementId ?? options.elementIdFor?.(d);
2146
+ if (!elementId) return void 0;
2147
+ const base = options.severityNumbering === "vscode" ? 0 : 1;
2148
+ const severity = d.severity === void 0 ? "error" : LSP_SEVERITIES[d.severity - base] ?? "error";
2149
+ const code = typeof d.code === "object" ? d.code.value : d.code;
2150
+ return {
2151
+ elementId,
2152
+ ...anchor.key !== void 0 ? { key: anchor.key } : {},
2153
+ severity,
2154
+ message: typeof d.message === "string" ? d.message : d.message.value,
2155
+ ...d.source !== void 0 ? { source: d.source } : {},
2156
+ ...d.range ? { range: d.range } : {},
2157
+ ...code !== void 0 ? { code: String(code) } : {},
2158
+ ...d.data !== void 0 ? { data: d.data } : {}
2159
+ };
2160
+ }
2161
+ /**
2162
+ * One LSP diagnostic anchored by `data.elementId` (or `data.nodeId`), or `undefined` when it
2163
+ * names no element. For range-anchored diagnostics use `fromLspDiagnostics` with `elementIdFor`.
2164
+ */
2165
+ function fromLspDiagnostic(diagnostic) {
2166
+ return convertLsp(diagnostic, {});
2167
+ }
2168
+ /** LSP diagnostics as goal diagnostics; those that can't be anchored to an element are dropped. */
2169
+ function fromLspDiagnostics(diagnostics, options = {}) {
2170
+ const result = [];
2171
+ for (const d of diagnostics) {
2172
+ const converted = convertLsp(d, options);
2173
+ if (converted) result.push(converted);
2174
+ }
2175
+ return result;
2176
+ }
2177
+ /** One `{ nodeId, severity, message }` diagnostic, or `undefined` without a `nodeId`. */
2178
+ function fromNodeIdDiagnostic(diagnostic) {
2179
+ const { nodeId, severity, ...rest } = diagnostic;
2180
+ if (!nodeId) return void 0;
2181
+ return {
2182
+ ...rest,
2183
+ elementId: nodeId,
2184
+ severity: Object.hasOwn(RANK, severity) ? severity : "info"
2185
+ };
2186
+ }
2187
+ /**
2188
+ * `{ nodeId, severity, message }` diagnostics (MutRoSe) as goal diagnostics, dropping those
2189
+ * without a node: `store.publish('mutrose', fromNodeIdDiagnostics(message.diagnostics))`.
2190
+ */
2191
+ function fromNodeIdDiagnostics(diagnostics) {
2192
+ const result = [];
2193
+ for (const d of diagnostics) {
2194
+ const converted = fromNodeIdDiagnostic(d);
2195
+ if (converted) result.push(converted);
2196
+ }
2197
+ return result;
2198
+ }
2199
+ //#endregion
1845
2200
  exports.ACTOR_KINDS = ACTOR_KINDS;
1846
2201
  exports.ACTOR_LINK_KINDS = ACTOR_LINK_KINDS;
1847
2202
  exports.CONTRIBUTION_LABELS = CONTRIBUTION_LABELS;
1848
2203
  exports.DEFAULT_DIAGRAM = DEFAULT_DIAGRAM;
1849
2204
  exports.DEFAULT_ELEMENT_SIZE = DEFAULT_ELEMENT_SIZE;
1850
2205
  exports.DEPENDENCY_LINK_KINDS = DEPENDENCY_LINK_KINDS;
2206
+ exports.DIAGNOSTIC_SEVERITIES = DIAGNOSTIC_SEVERITIES;
1851
2207
  exports.ELEMENT_KINDS = ELEMENT_KINDS;
2208
+ exports.FILE_METAMODEL_KEY = FILE_METAMODEL_KEY;
1852
2209
  exports.ISTAR_2_0 = ISTAR_2_0;
1853
2210
  exports.ISTAR_PREFIX = ISTAR_PREFIX;
1854
2211
  exports.ISTAR_VERSION = ISTAR_VERSION;
@@ -1868,7 +2225,9 @@ exports.addElement = addElement;
1868
2225
  exports.boolean = boolean;
1869
2226
  exports.canLink = canLink;
1870
2227
  exports.childrenOf = childrenOf;
2228
+ exports.compareSeverity = compareSeverity;
1871
2229
  exports.connect = connect;
2230
+ exports.createDiagnosticsStore = createDiagnosticsStore;
1872
2231
  exports.createEmptyModel = createEmptyModel;
1873
2232
  exports.createModelStore = createModelStore;
1874
2233
  exports.defaultIdGenerator = defaultIdGenerator;
@@ -1883,7 +2242,15 @@ exports.elementBehavesLike = elementBehavesLike;
1883
2242
  exports.elementKindDefinition = elementKindDefinition;
1884
2243
  exports.enumOf = enumOf;
1885
2244
  exports.extendMetamodel = extendMetamodel;
2245
+ exports.fileMetamodelExtension = fileMetamodelExtension;
2246
+ exports.fileMetamodelOf = fileMetamodelOf;
2247
+ exports.fromLspDiagnostic = fromLspDiagnostic;
2248
+ exports.fromLspDiagnostics = fromLspDiagnostics;
2249
+ exports.fromNodeIdDiagnostic = fromNodeIdDiagnostic;
2250
+ exports.fromNodeIdDiagnostics = fromNodeIdDiagnostics;
1886
2251
  exports.getSourceLayout = getSourceLayout;
2252
+ exports.groupDiagnostics = groupDiagnostics;
2253
+ exports.hostMetamodelOf = hostMetamodelOf;
1887
2254
  exports.inheritMetamodel = inheritMetamodel;
1888
2255
  exports.inheritSourceLayout = inheritSourceLayout;
1889
2256
  exports.isActor = isActor;
@@ -1902,6 +2269,7 @@ exports.isNodeKindIn = isNodeKindIn;
1902
2269
  exports.isNodeLinkKind = isNodeLinkKind;
1903
2270
  exports.linkKindDefinition = linkKindDefinition;
1904
2271
  exports.linksOf = linksOf;
2272
+ exports.mergeDiagnostics = mergeDiagnostics;
1905
2273
  exports.metamodelOf = metamodelOf;
1906
2274
  exports.moveElement = moveElement;
1907
2275
  exports.nestElement = nestElement;
@@ -1910,6 +2278,7 @@ exports.parsePistar = parsePistar;
1910
2278
  exports.prop = prop;
1911
2279
  exports.removeElement = removeElement;
1912
2280
  exports.removeElements = removeElements;
2281
+ exports.setHostMetamodel = setHostMetamodel;
1913
2282
  exports.shortKindName = shortKindName;
1914
2283
  exports.string = string;
1915
2284
  exports.toPistar = toPistar;
@@ -1917,8 +2286,11 @@ exports.toPistarObject = toPistarObject;
1917
2286
  exports.updateDiagram = updateDiagram;
1918
2287
  exports.updateElement = updateElement;
1919
2288
  exports.updateLink = updateLink;
2289
+ exports.validateFileMetamodel = validateFileMetamodel;
1920
2290
  exports.validateModel = validateModel;
1921
2291
  exports.validateModelProperties = validateModelProperties;
2292
+ exports.withFileMetamodel = withFileMetamodel;
1922
2293
  exports.withMetamodel = withMetamodel;
2294
+ exports.worstDiagnosticSeverity = worstDiagnosticSeverity;
1923
2295
 
1924
2296
  //# sourceMappingURL=index.cjs.map