@istar-ts/core 0.12.0 → 0.13.1

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
@@ -139,61 +139,6 @@ function shortKindName(kind) {
139
139
  }
140
140
  //#endregion
141
141
  //#region src/metamodels.ts
142
- function shortName(kind) {
143
- const dot = kind.indexOf(".");
144
- return dot === -1 ? kind : kind.slice(dot + 1);
145
- }
146
- function buildIstar() {
147
- const elements = /* @__PURE__ */ new Map();
148
- for (const kind of ACTOR_KINDS) elements.set(kind, {
149
- kind,
150
- category: "actor",
151
- label: shortName(kind),
152
- size: DEFAULT_ELEMENT_SIZE[kind],
153
- pistarType: kind
154
- });
155
- for (const kind of NODE_KINDS) elements.set(kind, {
156
- kind,
157
- category: "node",
158
- label: shortName(kind),
159
- size: DEFAULT_ELEMENT_SIZE[kind],
160
- info: NODE_KIND_INFO[kind],
161
- pistarType: kind
162
- });
163
- const links = /* @__PURE__ */ new Map();
164
- const labels = {
165
- "istar.IsALink": "Is-A",
166
- "istar.ParticipatesInLink": "Participates-In",
167
- "istar.DependencyLink": "Dependency",
168
- "istar.AndRefinementLink": "And-Refinement",
169
- "istar.OrRefinementLink": "Or-Refinement",
170
- "istar.NeededByLink": "Needed-By",
171
- "istar.QualificationLink": "Qualification",
172
- "istar.ContributionLink": "Contribution"
173
- };
174
- const add = (kinds, category) => {
175
- for (const kind of kinds) links.set(kind, {
176
- kind,
177
- category,
178
- label: labels[kind],
179
- info: LINK_KIND_INFO[kind],
180
- pistarType: kind
181
- });
182
- };
183
- add(ACTOR_LINK_KINDS, "actor");
184
- add(DEPENDENCY_LINK_KINDS, "dependency");
185
- add(NODE_LINK_KINDS, "node");
186
- return {
187
- name: "istar-2.0",
188
- extensions: [],
189
- elements,
190
- links,
191
- elementsByPistarType: new Map([...elements.keys()].map((k) => [k, k])),
192
- linksByPistarType: new Map([...links.keys()].map((k) => [k, k]))
193
- };
194
- }
195
- /** iStar 2.0, as the piStar tool implements it. The default metamodel everywhere. */
196
- const ISTAR_2_0 = buildIstar();
197
142
  var MetamodelError = class extends Error {
198
143
  name = "MetamodelError";
199
144
  };
@@ -205,117 +150,337 @@ function defineMetamodelExtension(extension) {
205
150
  return extension;
206
151
  }
207
152
  const KIND_NAME = /^[A-Za-z_][\w-]*\.[A-Za-z_][\w-]*$/;
153
+ const PREFIX_NAME = /^[A-Za-z_][\w-]*$/;
208
154
  const CATEGORY_SELECTORS = /* @__PURE__ */ new Set([
209
155
  "node",
210
156
  "actor",
211
157
  "*"
212
158
  ]);
159
+ /** The `targets` selector for any link (see `LinkRules`). */
160
+ const LINK_SELECTOR = "link";
161
+ /** piStar-ext's defaults for an extension's new nodes (tool/language/metamodel.js). */
162
+ const EXTENSION_NODE_INFO = {
163
+ canBeInnerElement: true,
164
+ canBeDependum: true,
165
+ canBeOnPaper: false
166
+ };
167
+ /** DSM3-goals' defaults for a root's nodes (istarcore metamodelManager). */
168
+ /** A hub sits on the paper, outside any actor, and is never a dependum. */
169
+ const HUB_INFO = {
170
+ canBeInnerElement: false,
171
+ canBeDependum: false,
172
+ canBeOnPaper: true
173
+ };
174
+ /** A circle filling the hub's box (DSM3-goals draws a circle of radius 10 in 20×20). */
175
+ const HUB_SHAPE = {
176
+ path: "M 0 10 A 10 10 0 1 0 20 10 A 10 10 0 1 0 0 10 Z",
177
+ viewBox: "0 0 20 20"
178
+ };
179
+ const ROOT_NODE_INFO = {
180
+ canBeInnerElement: true,
181
+ canBeDependum: true,
182
+ canBeOnPaper: true
183
+ };
184
+ function shortName(kind) {
185
+ const dot = kind.indexOf(".");
186
+ return dot === -1 ? kind : kind.slice(dot + 1);
187
+ }
188
+ function claimName(table, kind, what) {
189
+ const { where } = table;
190
+ if (typeof kind !== "string" || !KIND_NAME.test(kind)) throw new MetamodelError(`${where}: ${what} kind ${JSON.stringify(kind)} must be namespaced, e.g. "${table.namespace}.Name"`);
191
+ if (table.reserved !== void 0 && kind.startsWith(`${table.reserved}.`)) throw new MetamodelError(`${where}: ${what} kind "${kind}" uses the reserved "${table.reserved}." prefix; name it "${table.namespace}.${shortName(kind)}" and set pistarType: "${kind}" to read files that use it`);
192
+ if (table.elements.has(kind) || table.links.has(kind)) throw new MetamodelError(`${where}: kind "${kind}" already exists`);
193
+ }
194
+ function claimType(table, type, kind) {
195
+ const { where } = table;
196
+ if (typeof type !== "string" || type === "") throw new MetamodelError(`${where}: kind "${kind}" has an empty pistarType`);
197
+ if (table.elementsByType.has(type) || table.linksByType.has(type) || table.elements.has(type) || table.links.has(type)) throw new MetamodelError(`${where}: pistarType "${type}" of kind "${kind}" is already used by another kind`);
198
+ }
199
+ function isCompleteNodeInfo(info) {
200
+ return info !== void 0 && typeof info.canBeInnerElement === "boolean" && typeof info.canBeDependum === "boolean" && typeof info.canBeOnPaper === "boolean";
201
+ }
202
+ function addElementKind(table, declaration) {
203
+ const { where } = table;
204
+ const { kind } = declaration;
205
+ claimName(table, kind, "element");
206
+ let parent;
207
+ if (declaration.behavesLike !== void 0) {
208
+ parent = table.elements.get(declaration.behavesLike);
209
+ if (!parent) throw new MetamodelError(`${where}: "${kind}" behaves like unknown element kind "${declaration.behavesLike}"`);
210
+ }
211
+ const category = declaration.category ?? parent?.category;
212
+ if (category !== "actor" && category !== "node") throw new MetamodelError(`${where}: element kind "${kind}" needs a category ("node" or "actor") or behavesLike`);
213
+ if (parent && parent.category !== category) throw new MetamodelError(`${where}: "${kind}" is a ${category} but behaves like "${parent.kind}", a ${parent.category}`);
214
+ const type = declaration.pistarType ?? kind;
215
+ claimType(table, type, kind);
216
+ const info = isCompleteNodeInfo(declaration.info) ? declaration.info : {
217
+ ...parent?.info ?? (table.extension === void 0 ? ROOT_NODE_INFO : EXTENSION_NODE_INFO),
218
+ ...declaration.info
219
+ };
220
+ const definition = {
221
+ kind,
222
+ category,
223
+ label: declaration.label ?? shortName(kind),
224
+ size: declaration.size ?? parent?.size ?? (category === "actor" ? {
225
+ width: 200,
226
+ height: 120
227
+ } : {
228
+ width: 90,
229
+ height: 55
230
+ }),
231
+ ...category === "node" ? { info } : {},
232
+ ...parent ? { behavesLike: parent.kind } : {},
233
+ pistarType: type,
234
+ ...table.extension !== void 0 ? { extension: table.extension } : {},
235
+ ...declaration.shape ? { shape: declaration.shape } : {},
236
+ ...declaration.textBox ? { textBox: declaration.textBox } : {}
237
+ };
238
+ table.elements.set(kind, definition);
239
+ table.elementsByType.set(type, kind);
240
+ }
241
+ function addLinkKind(table, declaration) {
242
+ const { where } = table;
243
+ const { kind } = declaration;
244
+ claimName(table, kind, "link");
245
+ const isRoot = table.extension === void 0;
246
+ if (declaration.behavesLike !== void 0 && declaration.rules !== void 0) throw new MetamodelError(isRoot ? `${where}: link kind "${kind}" needs at most one of behavesLike or rules` : `${where}: link kind "${kind}" needs exactly one of behavesLike or rules`);
247
+ if (!isRoot && declaration.behavesLike === void 0 && declaration.rules === void 0) throw new MetamodelError(`${where}: link kind "${kind}" needs exactly one of behavesLike or rules`);
248
+ let parent;
249
+ if (declaration.behavesLike !== void 0) {
250
+ parent = table.links.get(declaration.behavesLike);
251
+ if (!parent) throw new MetamodelError(`${where}: "${kind}" behaves like unknown link kind "${declaration.behavesLike}"`);
252
+ }
253
+ const category = declaration.category ?? parent?.category ?? "node";
254
+ if (category !== "node" && category !== "actor" && category !== "dependency") throw new MetamodelError(`${where}: link kind "${kind}" has an unknown category "${category}"`);
255
+ if (declaration.arity !== void 0) {
256
+ if (declaration.arity !== "n") throw new MetamodelError(`${where}: link kind "${kind}" has an unknown arity ${JSON.stringify(declaration.arity)}`);
257
+ if (!declaration.rules || category !== "node") throw new MetamodelError(`${where}: n-ary link kind "${kind}" needs rules and the "node" category`);
258
+ }
259
+ if (parent && parent.category !== category) throw new MetamodelError(`${where}: "${kind}" is a ${category} link but behaves like "${parent.kind}", a ${parent.category} link`);
260
+ if (category === "dependency" && !parent && (!isRoot || declaration.rules !== void 0)) throw new MetamodelError(`${where}: dependency link kind "${kind}" must behave like a dependency link (its dependum makes declarative rules ambiguous)`);
261
+ if (declaration.rules) for (const [side, selectors] of [["sources", declaration.rules.sources], ["targets", declaration.rules.targets]]) {
262
+ if (!Array.isArray(selectors) || selectors.length === 0) throw new MetamodelError(`${where}: link kind "${kind}" needs at least one of ${side}`);
263
+ for (const selector of selectors) if (!(CATEGORY_SELECTORS.has(selector) || table.elements.has(selector) || side === "targets" && (selector === "link" || table.links.has(selector) || selector === kind))) throw new MetamodelError(`${where}: ${side} of "${kind}" names unknown element kind "${selector}"`);
264
+ }
265
+ const type = declaration.pistarType ?? kind;
266
+ claimType(table, type, kind);
267
+ const definition = {
268
+ kind,
269
+ category,
270
+ label: declaration.label ?? shortName(kind),
271
+ info: declaration.info ?? parent?.info ?? {},
272
+ ...parent ? { behavesLike: parent.kind } : {},
273
+ ...declaration.rules ? { rules: declaration.rules } : {},
274
+ ...declaration.check ? { check: declaration.check } : {},
275
+ pistarType: type,
276
+ ...table.extension !== void 0 ? { extension: table.extension } : {},
277
+ ...declaration.line ? { line: declaration.line } : {}
278
+ };
279
+ if (declaration.arity !== "n") {
280
+ table.links.set(kind, definition);
281
+ table.linksByType.set(type, kind);
282
+ return;
283
+ }
284
+ const parts = {
285
+ hub: declaration.parts?.hub ?? `${kind}Hub`,
286
+ leg: declaration.parts?.leg ?? `${kind}Start`,
287
+ end: declaration.parts?.end ?? `${kind}End`
288
+ };
289
+ table.links.set(kind, {
290
+ ...definition,
291
+ arity: "n",
292
+ parts
293
+ });
294
+ table.linksByType.set(type, kind);
295
+ addHubKinds(table, table.links.get(kind), parts, declaration.hubSize);
296
+ }
297
+ /**
298
+ * Adds the kinds an n-ary link kind is made of, as DSM3-goals' `setupSplitLinks` generates them:
299
+ * the hub (an element), the legs (each source → the hub, at most one per source) and the end
300
+ * (the hub → the target).
301
+ */
302
+ function addHubKinds(table, base, parts, hubSize) {
303
+ const rules = base.rules;
304
+ const claim = (kind, type, what) => {
305
+ try {
306
+ claimName(table, kind, what);
307
+ claimType(table, type, kind);
308
+ } catch (error) {
309
+ if (!(error instanceof MetamodelError)) throw error;
310
+ throw new MetamodelError(`${error.message} (generated for n-ary link kind "${base.kind}")`);
311
+ }
312
+ };
313
+ const owner = table.extension !== void 0 ? { extension: table.extension } : {};
314
+ claim(parts.hub, `${base.pistarType}Hub`, "element");
315
+ table.elements.set(parts.hub, {
316
+ kind: parts.hub,
317
+ category: "node",
318
+ label: `${base.label} hub`,
319
+ size: hubSize ?? {
320
+ width: 20,
321
+ height: 20
322
+ },
323
+ info: HUB_INFO,
324
+ pistarType: `${base.pistarType}Hub`,
325
+ ...owner,
326
+ shape: HUB_SHAPE,
327
+ role: "hub",
328
+ of: base.kind
329
+ });
330
+ table.elementsByType.set(`${base.pistarType}Hub`, parts.hub);
331
+ const common = {
332
+ category: "node",
333
+ ...owner,
334
+ of: base.kind
335
+ };
336
+ claim(parts.leg, `${base.pistarType}Start`, "link");
337
+ table.links.set(parts.leg, {
338
+ ...common,
339
+ kind: parts.leg,
340
+ label: `${base.label} leg`,
341
+ info: {},
342
+ rules: {
343
+ sources: rules.sources,
344
+ targets: [parts.hub],
345
+ sameActor: false,
346
+ ...rules.allowDependum !== void 0 ? { allowDependum: rules.allowDependum } : {},
347
+ unique: "kind"
348
+ },
349
+ pistarType: `${base.pistarType}Start`,
350
+ role: "leg"
351
+ });
352
+ table.linksByType.set(`${base.pistarType}Start`, parts.leg);
353
+ claim(parts.end, `${base.pistarType}End`, "link");
354
+ table.links.set(parts.end, {
355
+ ...common,
356
+ kind: parts.end,
357
+ label: `${base.label} end`,
358
+ info: base.info,
359
+ rules: {
360
+ sources: [parts.hub],
361
+ targets: rules.targets,
362
+ sameActor: false,
363
+ ...rules.allowDependum !== void 0 ? { allowDependum: rules.allowDependum } : {},
364
+ unique: "kind"
365
+ },
366
+ pistarType: `${base.pistarType}End`,
367
+ ...base.line ? { line: base.line } : {},
368
+ role: "end"
369
+ });
370
+ table.linksByType.set(`${base.pistarType}End`, parts.end);
371
+ }
372
+ /**
373
+ * Builds a root metamodel: a modelling language of its own, such as iStar 2.0, KAOS or NFR.
374
+ * Kinds are namespaced (any namespace; usually `prefix`), declared in order: `behavesLike` and
375
+ * link rules refer to kinds declared before them. Extensions of the result may not use `prefix`.
376
+ *
377
+ * Throws `MetamodelError` on the same declaration errors as `extendMetamodel`.
378
+ */
379
+ function defineMetamodel(declaration) {
380
+ const { name, prefix } = declaration;
381
+ if (!name || typeof name !== "string") throw new MetamodelError("a root metamodel needs a name");
382
+ const where = `metamodel "${name}"`;
383
+ if (typeof prefix !== "string" || !PREFIX_NAME.test(prefix)) throw new MetamodelError(`${where}: prefix ${JSON.stringify(prefix)} must be a name without dots, e.g. "istar"`);
384
+ const table = {
385
+ where,
386
+ namespace: prefix,
387
+ elements: /* @__PURE__ */ new Map(),
388
+ links: /* @__PURE__ */ new Map(),
389
+ elementsByType: /* @__PURE__ */ new Map(),
390
+ linksByType: /* @__PURE__ */ new Map()
391
+ };
392
+ for (const element of declaration.elements) addElementKind(table, element);
393
+ for (const link of declaration.links) addLinkKind(table, link);
394
+ return {
395
+ name,
396
+ framework: name,
397
+ prefix,
398
+ file: declaration.file ?? {},
399
+ extensions: [],
400
+ elements: table.elements,
401
+ links: table.links,
402
+ elementsByPistarType: table.elementsByType,
403
+ linksByPistarType: table.linksByType
404
+ };
405
+ }
406
+ const ISTAR_LINK_LABELS = {
407
+ "istar.IsALink": "Is-A",
408
+ "istar.ParticipatesInLink": "Participates-In",
409
+ "istar.DependencyLink": "Dependency",
410
+ "istar.AndRefinementLink": "And-Refinement",
411
+ "istar.OrRefinementLink": "Or-Refinement",
412
+ "istar.NeededByLink": "Needed-By",
413
+ "istar.QualificationLink": "Qualification",
414
+ "istar.ContributionLink": "Contribution"
415
+ };
416
+ function istarLinks(kinds, category) {
417
+ return kinds.map((kind) => ({
418
+ kind,
419
+ category,
420
+ label: ISTAR_LINK_LABELS[kind],
421
+ info: LINK_KIND_INFO[kind]
422
+ }));
423
+ }
424
+ /** iStar 2.0's declaration, built from the constants in `metamodel.ts`. */
425
+ const ISTAR_2_0_DECLARATION = {
426
+ name: "istar-2.0",
427
+ prefix: ISTAR_PREFIX,
428
+ elements: [...ACTOR_KINDS.map((kind) => ({
429
+ kind,
430
+ category: "actor",
431
+ size: DEFAULT_ELEMENT_SIZE[kind]
432
+ })), ...NODE_KINDS.map((kind) => ({
433
+ kind,
434
+ category: "node",
435
+ size: DEFAULT_ELEMENT_SIZE[kind],
436
+ info: NODE_KIND_INFO[kind]
437
+ }))],
438
+ links: [
439
+ ...istarLinks(ACTOR_LINK_KINDS, "actor"),
440
+ ...istarLinks(DEPENDENCY_LINK_KINDS, "dependency"),
441
+ ...istarLinks(NODE_LINK_KINDS, "node")
442
+ ],
443
+ file: {
444
+ tool: "pistar.2.1.0",
445
+ istar: "2.0"
446
+ }
447
+ };
448
+ /** iStar 2.0, as the piStar tool implements it. The default metamodel everywhere. */
449
+ const ISTAR_2_0 = defineMetamodel(ISTAR_2_0_DECLARATION);
213
450
  /**
214
- * Returns a new metamodel with the extension's kinds added. `base` is not modified.
451
+ * Returns a new metamodel with the extension's kinds added. `base` is not modified. The result
452
+ * keeps `base`'s `framework`, `prefix` and `file`.
215
453
  *
216
- * Throws `MetamodelError` when a kind name isn't namespaced, uses the reserved `istar.`
217
- * prefix, collides with an existing kind or piStar type, or refers to an unknown kind.
454
+ * Throws `MetamodelError` when a kind name isn't namespaced, uses the root's reserved prefix
455
+ * (`istar.` for iStar 2.0), collides with an existing kind or piStar type, or refers to an
456
+ * unknown kind.
218
457
  */
219
458
  function extendMetamodel(base, extension) {
220
- const where = `extension "${extension.name}"`;
221
459
  if (!extension.name || typeof extension.name !== "string") throw new MetamodelError("an extension needs a name");
460
+ const where = `extension "${extension.name}"`;
222
461
  if (base.extensions.includes(extension.name)) throw new MetamodelError(`${where} is already applied`);
223
- const elements = new Map(base.elements);
224
- const links = new Map(base.links);
225
- const elementsByType = new Map(base.elementsByPistarType);
226
- const linksByType = new Map(base.linksByPistarType);
227
- const claimName = (kind, what) => {
228
- if (typeof kind !== "string" || !KIND_NAME.test(kind)) throw new MetamodelError(`${where}: ${what} kind ${JSON.stringify(kind)} must be namespaced, e.g. "${extension.name}.Name"`);
229
- if (kind.startsWith(`istar.`)) throw new MetamodelError(`${where}: ${what} kind "${kind}" uses the reserved "${ISTAR_PREFIX}." prefix; name it "${extension.name}.${shortName(kind)}" and set pistarType: "${kind}" to read files that use it`);
230
- if (elements.has(kind) || links.has(kind)) throw new MetamodelError(`${where}: kind "${kind}" already exists`);
231
- };
232
- const claimType = (type, kind) => {
233
- if (typeof type !== "string" || type === "") throw new MetamodelError(`${where}: kind "${kind}" has an empty pistarType`);
234
- if (elementsByType.has(type) || linksByType.has(type) || elements.has(type) || links.has(type)) throw new MetamodelError(`${where}: pistarType "${type}" of kind "${kind}" is already used by another kind`);
462
+ const table = {
463
+ where,
464
+ namespace: extension.name,
465
+ reserved: base.prefix,
466
+ extension: extension.name,
467
+ elements: new Map(base.elements),
468
+ links: new Map(base.links),
469
+ elementsByType: new Map(base.elementsByPistarType),
470
+ linksByType: new Map(base.linksByPistarType)
235
471
  };
236
- for (const declaration of extension.elements ?? []) {
237
- const { kind } = declaration;
238
- claimName(kind, "element");
239
- let parent;
240
- if (declaration.behavesLike !== void 0) {
241
- parent = elements.get(declaration.behavesLike);
242
- if (!parent) throw new MetamodelError(`${where}: "${kind}" behaves like unknown element kind "${declaration.behavesLike}"`);
243
- }
244
- const category = declaration.category ?? parent?.category;
245
- if (category !== "actor" && category !== "node") throw new MetamodelError(`${where}: element kind "${kind}" needs a category ("node" or "actor") or behavesLike`);
246
- if (parent && parent.category !== category) throw new MetamodelError(`${where}: "${kind}" is a ${category} but behaves like "${parent.kind}", a ${parent.category}`);
247
- const type = declaration.pistarType ?? kind;
248
- claimType(type, kind);
249
- const baseInfo = parent?.info ?? {
250
- canBeInnerElement: true,
251
- canBeDependum: true,
252
- canBeOnPaper: false
253
- };
254
- const definition = {
255
- kind,
256
- category,
257
- label: declaration.label ?? shortName(kind),
258
- size: declaration.size ?? parent?.size ?? (category === "actor" ? {
259
- width: 200,
260
- height: 120
261
- } : {
262
- width: 90,
263
- height: 55
264
- }),
265
- ...category === "node" ? { info: {
266
- ...baseInfo,
267
- ...declaration.info
268
- } } : {},
269
- ...parent ? { behavesLike: parent.kind } : {},
270
- pistarType: type,
271
- extension: extension.name,
272
- ...declaration.shape ? { shape: declaration.shape } : {},
273
- ...declaration.textBox ? { textBox: declaration.textBox } : {}
274
- };
275
- elements.set(kind, definition);
276
- elementsByType.set(type, kind);
277
- }
278
- for (const declaration of extension.links ?? []) {
279
- const { kind } = declaration;
280
- claimName(kind, "link");
281
- if (declaration.behavesLike === void 0 === (declaration.rules === void 0)) throw new MetamodelError(`${where}: link kind "${kind}" needs exactly one of behavesLike or rules`);
282
- let parent;
283
- if (declaration.behavesLike !== void 0) {
284
- parent = links.get(declaration.behavesLike);
285
- if (!parent) throw new MetamodelError(`${where}: "${kind}" behaves like unknown link kind "${declaration.behavesLike}"`);
286
- }
287
- const category = declaration.category ?? parent?.category ?? "node";
288
- if (category !== "node" && category !== "actor" && category !== "dependency") throw new MetamodelError(`${where}: link kind "${kind}" has an unknown category "${category}"`);
289
- if (parent && parent.category !== category) throw new MetamodelError(`${where}: "${kind}" is a ${category} link but behaves like "${parent.kind}", a ${parent.category} link`);
290
- if (category === "dependency" && !parent) throw new MetamodelError(`${where}: dependency link kind "${kind}" must behave like a dependency link (its dependum makes declarative rules ambiguous)`);
291
- if (declaration.rules) for (const [side, selectors] of [["sources", declaration.rules.sources], ["targets", declaration.rules.targets]]) {
292
- if (!Array.isArray(selectors) || selectors.length === 0) throw new MetamodelError(`${where}: link kind "${kind}" needs at least one of ${side}`);
293
- for (const selector of selectors) if (!CATEGORY_SELECTORS.has(selector) && !elements.has(selector)) throw new MetamodelError(`${where}: ${side} of "${kind}" names unknown element kind "${selector}"`);
294
- }
295
- const type = declaration.pistarType ?? kind;
296
- claimType(type, kind);
297
- const definition = {
298
- kind,
299
- category,
300
- label: declaration.label ?? shortName(kind),
301
- info: declaration.info ?? parent?.info ?? {},
302
- ...parent ? { behavesLike: parent.kind } : {},
303
- ...declaration.rules ? { rules: declaration.rules } : {},
304
- ...declaration.check ? { check: declaration.check } : {},
305
- pistarType: type,
306
- extension: extension.name,
307
- ...declaration.line ? { line: declaration.line } : {}
308
- };
309
- links.set(kind, definition);
310
- linksByType.set(type, kind);
311
- }
472
+ for (const element of extension.elements ?? []) addElementKind(table, element);
473
+ for (const link of extension.links ?? []) addLinkKind(table, link);
312
474
  return {
313
475
  name: `${base.name}+${extension.name}`,
476
+ framework: base.framework,
477
+ prefix: base.prefix,
478
+ file: base.file,
314
479
  extensions: [...base.extensions, extension.name],
315
- elements,
316
- links,
317
- elementsByPistarType: elementsByType,
318
- linksByPistarType: linksByType
480
+ elements: table.elements,
481
+ links: table.links,
482
+ elementsByPistarType: table.elementsByType,
483
+ linksByPistarType: table.linksByType
319
484
  };
320
485
  }
321
486
  function elementKindDefinition(metamodel, kind) {
@@ -359,6 +524,21 @@ function effectiveLinkKind(metamodel, kind) {
359
524
  }
360
525
  return current?.kind ?? kind;
361
526
  }
527
+ /** True when link `kind` is `ancestor` or (transitively) behaves like it. */
528
+ function linkBehavesLike(metamodel, kind, ancestor) {
529
+ let current = kind;
530
+ const seen = /* @__PURE__ */ new Set();
531
+ while (current !== void 0 && !seen.has(current)) {
532
+ if (current === ancestor) return true;
533
+ seen.add(current);
534
+ current = metamodel.links.get(current)?.behavesLike;
535
+ }
536
+ return false;
537
+ }
538
+ /** True when a link kind's rules let it target links (`link` or a link kind in `targets`). */
539
+ function targetsLinks(metamodel, kind) {
540
+ return metamodel.links.get(effectiveLinkKind(metamodel, kind))?.rules?.targets.some((selector) => selector === "link" || metamodel.links.has(selector)) ?? false;
541
+ }
362
542
  /** True when `kind` is `ancestor` or (transitively) behaves like it. */
363
543
  function elementBehavesLike(metamodel, kind, ancestor) {
364
544
  let current = kind;
@@ -375,6 +555,23 @@ function elementBehavesLike(metamodel, kind, ancestor) {
375
555
  /** Tool identifier written by piStar 2.1.0, whose file format this library emulates. */
376
556
  const PISTAR_TOOL = "pistar.2.1.0";
377
557
  const ISTAR_VERSION = "2.0";
558
+ /**
559
+ * DSM3-goals' file conventions, shared by its languages (KAOS, NFR; see `@istar-ts/dsm3-kaos`
560
+ * and `@istar-ts/dsm3-nfr`): the `tool` its editors write, and the order of their top-level keys
561
+ * (`fileManager.js`). Use them as a root's `file.tool` and `file.keys`.
562
+ */
563
+ const DSM3_TOOL = "dsm3-goals";
564
+ const DSM3_FILE_KEYS = [
565
+ "actors",
566
+ "orphans",
567
+ "dependencies",
568
+ "links",
569
+ "display",
570
+ "tool",
571
+ "metamodelVersion",
572
+ "saveDate",
573
+ "diagram"
574
+ ];
378
575
  const DEFAULT_DIAGRAM = {
379
576
  width: 2e3,
380
577
  height: 1300
@@ -419,12 +616,14 @@ function inheritMetamodel(from, to) {
419
616
  return to;
420
617
  }
421
618
  function createEmptyModel(diagram = DEFAULT_DIAGRAM, options = {}) {
619
+ const { file } = options.metamodel ?? ISTAR_2_0;
422
620
  const model = {
423
621
  elements: /* @__PURE__ */ new Map(),
424
622
  links: /* @__PURE__ */ new Map(),
425
623
  diagram,
426
- tool: PISTAR_TOOL,
427
- istar: "2.0"
624
+ tool: file.tool ?? "pistar.2.1.0",
625
+ ...file.istar !== void 0 ? { istar: file.istar } : {},
626
+ ...file.extra !== void 0 ? { extra: { ...file.extra } } : {}
428
627
  };
429
628
  return options.metamodel ? withMetamodel(model, options.metamodel) : model;
430
629
  }
@@ -463,7 +662,10 @@ function childrenOf(model, actorId) {
463
662
  for (const element of model.elements.values()) if (element.parent === actorId) result.push(element);
464
663
  return result;
465
664
  }
466
- /** Links that have the element as source or target. */
665
+ /**
666
+ * Links that have the element as source or target. Given a link id, the links that target that
667
+ * link (see `LinkRules`).
668
+ */
467
669
  function linksOf(model, elementId) {
468
670
  const result = [];
469
671
  for (const link of model.links.values()) if (link.source === elementId || link.target === elementId) result.push(link);
@@ -484,6 +686,63 @@ function dependencyLinksOf(model, dependumId) {
484
686
  outbound
485
687
  };
486
688
  }
689
+ /** What a link points at: an element, or a link for kinds whose rules target links. */
690
+ function linkTargetOf(model, link) {
691
+ return model.elements.get(link.target) ?? model.links.get(link.target);
692
+ }
693
+ /** True when `element` is the hub of an n-ary link. */
694
+ function isHubElement(metamodel, element) {
695
+ return element !== void 0 && metamodel.elements.get(element.kind)?.role === "hub";
696
+ }
697
+ /** The n-ary link whose hub is `hubId`; `undefined` when it isn't a hub. */
698
+ function hubLinkOf(model, hubId) {
699
+ const metamodel = metamodelOf(model);
700
+ const hub = model.elements.get(hubId);
701
+ if (!hub || !isHubElement(metamodel, hub)) return void 0;
702
+ const legs = [];
703
+ let end;
704
+ for (const link of model.links.values()) {
705
+ const role = metamodel.links.get(link.kind)?.role;
706
+ if (role === "leg" && link.target === hubId) legs.push(link);
707
+ else if (role === "end" && link.source === hubId) end ??= link;
708
+ }
709
+ return {
710
+ hub,
711
+ legs,
712
+ ...end ? { end } : {}
713
+ };
714
+ }
715
+ /** The hub a leg or end link belongs to. */
716
+ function hubOfLink(model, linkId) {
717
+ const metamodel = metamodelOf(model);
718
+ const link = model.links.get(linkId);
719
+ const role = link && metamodel.links.get(link.kind)?.role;
720
+ if (!link || !role) return void 0;
721
+ const hub = model.elements.get(role === "leg" ? link.target : link.source);
722
+ return isHubElement(metamodel, hub) ? hub : void 0;
723
+ }
724
+ /** The centre of an element's box: its display size, else its kind's default size. */
725
+ function elementCenter(model, element) {
726
+ const size = metamodelOf(model).elements.get(element.kind)?.size;
727
+ const width = element.display?.width ?? size?.width ?? 0;
728
+ const height = element.display?.height ?? size?.height ?? 0;
729
+ return {
730
+ x: element.x + width / 2,
731
+ y: element.y + height / 2
732
+ };
733
+ }
734
+ /**
735
+ * Where a new hub goes: centred between the source and the target, as DSM3-goals places it.
736
+ * Returns its top-left corner for a hub of `size`.
737
+ */
738
+ function defaultHubPosition(model, source, target, size) {
739
+ const a = elementCenter(model, source);
740
+ const b = elementCenter(model, target);
741
+ return {
742
+ x: (a.x + b.x) / 2 - size.width / 2,
743
+ y: (a.y + b.y) / 2 - size.height / 2
744
+ };
745
+ }
487
746
  //#endregion
488
747
  //#region src/constraints.ts
489
748
  const OK = { ok: true };
@@ -495,6 +754,14 @@ function fail$1(code, reason) {
495
754
  };
496
755
  }
497
756
  /**
757
+ * True for a link, as opposed to an element: the target of a link kind whose rules target links
758
+ * (see `LinkRules`) may be either.
759
+ */
760
+ function isLinkRef(value) {
761
+ const candidate = value;
762
+ return typeof candidate.source === "string" && typeof candidate.target === "string";
763
+ }
764
+ /**
498
765
  * Checks whether a link of `kind` may be added from `source` to `target` in `model`.
499
766
  *
500
767
  * For dependency links, `source` is the depender and `target` the dependee (an actor or one of
@@ -502,16 +769,22 @@ function fail$1(code, reason) {
502
769
  *
503
770
  * Extended kinds (see `extendMetamodel`) are checked with the rules of the kind they behave
504
771
  * like, or with their declarative rules, then with their extra predicate.
772
+ *
773
+ * `target` may be a link (or a link id) for kinds whose rules target links (`LinkRules`).
774
+ *
775
+ * For an n-ary kind (`arity: 'n'`), a `target` that is one of its hubs is checked as a new leg
776
+ * of that hub; any other target, as a new link from `source` to `target`.
505
777
  */
506
778
  function canLink(model, source, target, kind, options = {}) {
507
779
  const any = model;
508
780
  const metamodel = options.metamodel ?? metamodelOf(model);
509
781
  const s = resolve(any, source);
510
- const t = resolve(any, target);
782
+ const t = resolve(any, target) ?? any.links.get(refId(target));
511
783
  if (!s) return fail$1("unknown-element", `unknown source element "${refId(source)}"`);
512
784
  if (!t) return fail$1("unknown-element", `unknown target element "${refId(target)}"`);
513
- const definition = metamodel.links.get(kind);
785
+ let definition = metamodel.links.get(kind);
514
786
  if (!definition) return fail$1("unknown-link-kind", `unknown link kind "${String(kind)}"`);
787
+ if (definition.parts && !isLinkRef(t) && metamodel.elements.get(t.kind)?.of === definition.kind) definition = metamodel.links.get(definition.parts.leg) ?? definition;
515
788
  return checkWith(new Graph(any, metamodel), definition, s, t);
516
789
  }
517
790
  /** Runs the rule a link kind resolves to, then every extra predicate along its chain. */
@@ -522,9 +795,14 @@ function checkWith(graph, definition, source, target) {
522
795
  const builtIn = CHECKS[root.kind];
523
796
  let result;
524
797
  if (root.rules) result = declarativeCheck(graph, definition, root.rules, root.kind, source, target);
798
+ else if (isLinkRef(target)) result = elementTargetOnly(definition);
525
799
  else if (builtIn) result = builtIn(graph, source, target);
526
800
  else result = fail$1("unknown-link-kind", `link kind "${definition.kind}" has no rules`);
527
801
  if (!result.ok) return result;
802
+ if (definition.role !== void 0 && !isLinkRef(target)) {
803
+ const hubResult = hubCheck(graph, definition, source, target);
804
+ if (!hubResult.ok) return hubResult;
805
+ }
528
806
  const context = graph.context(definition.kind, source, target);
529
807
  for (const step of chain.toReversed()) {
530
808
  if (!step.check) continue;
@@ -533,6 +811,29 @@ function checkWith(graph, definition, source, target) {
533
811
  }
534
812
  return OK;
535
813
  }
814
+ /**
815
+ * What a leg or end must also respect: an n-ary link has one end, and the base kind's same-actor
816
+ * rule holds between each leg's source and the end's target (the hub belongs to no actor).
817
+ */
818
+ function hubCheck(graph, definition, source, target) {
819
+ const base = definition.of === void 0 ? void 0 : graph.metamodel.links.get(definition.of);
820
+ if (!base) return OK;
821
+ const roleOf = (link) => graph.metamodel.links.get(link.kind)?.role;
822
+ if (definition.role === "end") {
823
+ for (const link of graph.model.links.values()) if (link.source === source.id && roleOf(link) === "end") return fail$1("duplicate-link", `the hub of a ${base.label} link has only one end`);
824
+ return OK;
825
+ }
826
+ if (base.rules?.sameActor === false) return OK;
827
+ for (const link of graph.model.links.values()) {
828
+ if (link.source !== target.id || roleOf(link) !== "end") continue;
829
+ if (graph.model.elements.get(link.target)?.parent !== source.parent) return fail$1("different-actors", `the sources and target of a ${base.label} link must pertain to the same actor`);
830
+ }
831
+ return OK;
832
+ }
833
+ /** The built-in (iStar 2.0) rules only know element targets. */
834
+ function elementTargetOnly(definition) {
835
+ return fail$1("invalid-target", `the target of ${/^[aeiou]/i.test(definition.label) ? "an" : "a"} ${definition.label} link must be an element, not a link`);
836
+ }
536
837
  function refId(ref) {
537
838
  return typeof ref === "string" ? ref : ref.id;
538
839
  }
@@ -684,12 +985,18 @@ function describe(graph, selectors) {
684
985
  if (selector === "*") return "any element";
685
986
  if (selector === "node") return "an intentional element";
686
987
  if (selector === "actor") return "an actor";
988
+ if (selector === "link") return "a link";
989
+ const linkLabel = graph.metamodel.links.get(selector)?.label;
990
+ if (linkLabel !== void 0) return /^[aeiou]/i.test(linkLabel) ? `an ${linkLabel} link` : `a ${linkLabel} link`;
687
991
  const label = graph.metamodel.elements.get(selector)?.label ?? selector;
688
992
  return /^[aeiou]/i.test(label) ? `an ${label}` : `a ${label}`;
689
993
  });
690
994
  return names.length <= 2 ? names.join(" or ") : `${names.slice(0, -1).join(", ")} or ${names[names.length - 1]}`;
691
995
  }
692
996
  function matchesSelector(graph, element, selector) {
997
+ if (isLinkRef(element)) return selector === "link" || linkBehavesLike(graph.metamodel, element.kind, selector);
998
+ if (selector === "link") return false;
999
+ if (graph.metamodel.elements.get(element.kind)?.role === "hub") return element.kind === selector;
693
1000
  if (selector === "*") return true;
694
1001
  const category = graph.metamodel.elements.get(element.kind)?.category;
695
1002
  if (selector === "node" || selector === "actor") return category === selector;
@@ -698,15 +1005,21 @@ function matchesSelector(graph, element, selector) {
698
1005
  /**
699
1006
  * Declarative rules of an extension link kind, checked in the order of the iStar 2.0 node-link
700
1007
  * rules (and of piStar-ext's generated `isValid`): source kind, target kind, self link,
701
- * dependum, same actor, duplicates.
1008
+ * dependum, same actor, duplicates. A link target skips the self, dependum and same-actor rules,
1009
+ * which are about elements.
702
1010
  */
703
1011
  function declarativeCheck(graph, definition, rules, rulesKind, source, target) {
704
1012
  const name = definition.label;
705
1013
  const article = /^[aeiou]/i.test(name) ? "an" : "a";
706
1014
  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)}`);
1015
+ if (!rules.targets.some((selector) => matchesSelector(graph, target, selector))) {
1016
+ const targetKind = isLinkRef(target) ? void 0 : graph.metamodel.elements.get(target.kind);
1017
+ const hubOf = targetKind?.role === "hub" && targetKind.of !== void 0 ? graph.metamodel.links.get(targetKind.of) : void 0;
1018
+ if (hubOf) return fail$1("hub-target", `you cannot make ${article} ${name} link towards the hub of ${/^[aeiou]/i.test(hubOf.label) ? "an" : "a"} ${hubOf.label} link`);
1019
+ return fail$1("invalid-target", `the target of ${article} ${name} link must be ${describe(graph, rules.targets)}`);
1020
+ }
708
1021
  if (!rules.allowSelf && source.id === target.id) return fail$1("self-link", `you cannot make ${article} ${name} link from an element onto itself`);
709
- if (definition.category === "node") {
1022
+ if (definition.category === "node" && !isLinkRef(target)) {
710
1023
  if (!rules.allowDependum && (isDependum(source) || isDependum(target))) return fail$1("dependum-endpoint", `you cannot make ${article} ${name} link with a dependum`);
711
1024
  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)`);
712
1025
  }
@@ -757,9 +1070,10 @@ function validateModel(model, options) {
757
1070
  if (inbound) accepted.set(inbound.id, inbound);
758
1071
  if (outbound) accepted.set(outbound.id, outbound);
759
1072
  }
760
- for (const link of any.links.values()) {
761
- if (isDependency(link)) continue;
762
- if (any.elements.has(link.source) && any.elements.has(link.target)) report(link.id, link.kind, link.source, link.target, check(link.source, link.target, link.kind));
1073
+ const rest = [...any.links.values()].filter((link) => !isDependency(link));
1074
+ const targetsLink = (link) => !any.elements.has(link.target) && any.links.has(link.target);
1075
+ for (const link of [...rest.filter((l) => !targetsLink(l)), ...rest.filter((l) => targetsLink(l))]) {
1076
+ if (any.elements.has(link.source) && (any.elements.has(link.target) || targetsLink(link))) report(link.id, link.kind, link.source, link.target, check(link.source, link.target, link.kind));
763
1077
  else report(link.id, link.kind, link.source, link.target, {
764
1078
  ok: false,
765
1079
  code: "unknown-element",
@@ -767,6 +1081,24 @@ function validateModel(model, options) {
767
1081
  });
768
1082
  accepted.set(link.id, link);
769
1083
  }
1084
+ for (const hub of any.elements.values()) {
1085
+ const hubKind = metamodel.elements.get(hub.kind);
1086
+ if (hubKind?.role !== "hub" || hubKind.of === void 0) continue;
1087
+ const base = metamodel.links.get(hubKind.of);
1088
+ const roleOf = (link) => metamodel.links.get(link.kind)?.role;
1089
+ const legs = rest.filter((l) => l.target === hub.id && roleOf(l) === "leg");
1090
+ const end = rest.find((l) => l.source === hub.id && roleOf(l) === "end");
1091
+ if (legs.length > 0 && end) continue;
1092
+ const label = base?.label ?? hubKind.of;
1093
+ issues.push({
1094
+ linkId: hub.id,
1095
+ kind: hubKind.of,
1096
+ source: legs[0]?.source ?? hub.id,
1097
+ target: end?.target ?? hub.id,
1098
+ code: "incomplete-hub",
1099
+ message: end ? `the hub of a ${label} link needs at least one leg` : `the hub of a ${label} link needs an end`
1100
+ });
1101
+ }
770
1102
  return issues;
771
1103
  }
772
1104
  //#endregion
@@ -891,6 +1223,11 @@ function fileMetamodelExtension(block) {
891
1223
  }
892
1224
  //#endregion
893
1225
  //#region src/serialization.ts
1226
+ /**
1227
+ * Key of a hub element's `extra` that holds its entry's unknown keys (those beside `hub`,
1228
+ * `starts` and `end`), so they are written back on the entry.
1229
+ */
1230
+ const HUB_ENTRY_KEY = "__hubEntry";
894
1231
  var PistarParseError = class extends Error {
895
1232
  name = "PistarParseError";
896
1233
  };
@@ -935,6 +1272,18 @@ const LINK_KEYS = /* @__PURE__ */ new Set([
935
1272
  "customProperties",
936
1273
  "label"
937
1274
  ]);
1275
+ const HUB_KEYS = /* @__PURE__ */ new Set([
1276
+ "id",
1277
+ "type",
1278
+ "x",
1279
+ "y",
1280
+ "customProperties"
1281
+ ]);
1282
+ const HUB_ENTRY_KEYS = /* @__PURE__ */ new Set([
1283
+ "hub",
1284
+ "starts",
1285
+ "end"
1286
+ ]);
938
1287
  const DIAGRAM_KEYS = /* @__PURE__ */ new Set([
939
1288
  "width",
940
1289
  "height",
@@ -1036,15 +1385,15 @@ function parsePistar(input, options = {}) {
1036
1385
  ...dependency
1037
1386
  });
1038
1387
  });
1039
- asArray(json.links, "links").forEach((raw, i) => {
1040
- if (!isRecord(raw)) throw new PistarParseError(`links[${i}]: expected an object`);
1388
+ const readLink = (raw, where) => {
1389
+ if (!isRecord(raw)) throw new PistarParseError(`${where}: expected an object`);
1041
1390
  const { id, type, source, target } = raw;
1042
- if (typeof id !== "string" || id === "") throw new PistarParseError(`links[${i}]: missing "id"`);
1391
+ if (typeof id !== "string" || id === "") throw new PistarParseError(`${where}: missing "id"`);
1043
1392
  const kind = typeof type === "string" ? metamodel.linksByPistarType.get(type) : void 0;
1044
- if (kind === void 0) throw new PistarParseError(`links[${i}] (${id}): unknown link type ${JSON.stringify(type)}`);
1045
- if (typeof source !== "string" || typeof target !== "string") throw new PistarParseError(`links[${i}] (${id}): missing "source" or "target"`);
1046
- if (elements.has(id) || links.has(id)) throw new PistarParseError(`links[${i}]: duplicated id "${id}"`);
1047
- links.set(id, {
1393
+ if (kind === void 0) throw new PistarParseError(`${where} (${id}): unknown link type ${JSON.stringify(type)}`);
1394
+ if (typeof source !== "string" || typeof target !== "string") throw new PistarParseError(`${where} (${id}): missing "source" or "target"`);
1395
+ if (elements.has(id) || links.has(id)) throw new PistarParseError(`${where}: duplicated id "${id}"`);
1396
+ const link = {
1048
1397
  id,
1049
1398
  kind,
1050
1399
  source,
@@ -1054,7 +1403,38 @@ function parsePistar(input, options = {}) {
1054
1403
  ...optional("label", typeof raw.label === "string" ? raw.label : void 0),
1055
1404
  ...optional("display", displayFor(id)),
1056
1405
  ...optional("extra", extraOf(raw, LINK_KEYS))
1406
+ };
1407
+ links.set(id, link);
1408
+ return link;
1409
+ };
1410
+ const readHubEntry = (raw, where) => {
1411
+ const hub = readElement(raw.hub, `${where}.hub`, HUB_KEYS, {});
1412
+ const hubKind = metamodel.elements.get(hub.kind);
1413
+ if (hubKind?.role !== "hub") throw new PistarParseError(`${where}.hub (${hub.id}): ${hub.kind} is not a hub kind`);
1414
+ const entryExtra = extraOf(raw, HUB_ENTRY_KEYS);
1415
+ if (entryExtra) elements.set(hub.id, {
1416
+ ...hub,
1417
+ extra: {
1418
+ ...hub.extra,
1419
+ [HUB_ENTRY_KEY]: entryExtra
1420
+ }
1057
1421
  });
1422
+ const roleOf = (link) => {
1423
+ const definition = metamodel.links.get(link.kind);
1424
+ return definition?.of === hubKind.of ? definition?.role : void 0;
1425
+ };
1426
+ asArray(raw.starts, `${where}.starts`).forEach((rawLeg, j) => {
1427
+ const leg = readLink(rawLeg, `${where}.starts[${j}]`);
1428
+ if (roleOf(leg) !== "leg" || leg.target !== hub.id) throw new PistarParseError(`${where}.starts[${j}] (${leg.id}): not a leg of the hub "${hub.id}"`);
1429
+ });
1430
+ if (raw.end !== void 0) {
1431
+ const end = readLink(raw.end, `${where}.end`);
1432
+ if (roleOf(end) !== "end" || end.source !== hub.id) throw new PistarParseError(`${where}.end (${end.id}): not the end of the hub "${hub.id}"`);
1433
+ }
1434
+ };
1435
+ asArray(json.links, "links").forEach((raw, i) => {
1436
+ if (isRecord(raw) && isRecord(raw.hub)) readHubEntry(raw, `links[${i}]`);
1437
+ else readLink(raw, `links[${i}]`);
1058
1438
  });
1059
1439
  let extraDisplay;
1060
1440
  for (const [id, value] of Object.entries(display)) if (!usedDisplay.has(id)) (extraDisplay ??= {})[id] = value;
@@ -1086,6 +1466,64 @@ function parsePistar(input, options = {}) {
1086
1466
  setHostMetamodel(model, host);
1087
1467
  return withMetamodel(model, metamodel);
1088
1468
  }
1469
+ function parseJson(json) {
1470
+ if (typeof json !== "string") return json;
1471
+ let value;
1472
+ try {
1473
+ value = JSON.parse(json);
1474
+ } catch (error) {
1475
+ throw new PistarParseError(`invalid JSON: ${error.message}`, { cause: error });
1476
+ }
1477
+ if (!isRecord(value)) throw new PistarParseError("a piStar model must be a JSON object");
1478
+ return value;
1479
+ }
1480
+ /** `kaos` for an object whose `type` is `kaos.Softgoal`. */
1481
+ function prefixOf(value) {
1482
+ const type = isRecord(value) ? value.type : void 0;
1483
+ const dot = typeof type === "string" ? type.indexOf(".") : -1;
1484
+ return dot > 0 ? type.slice(0, dot) : void 0;
1485
+ }
1486
+ function arrayOf(value) {
1487
+ return Array.isArray(value) ? value : [];
1488
+ }
1489
+ /**
1490
+ * The namespace of the first `type` in a file (`kaos` for `kaos.Softgoal`), looking at actors and
1491
+ * their nodes, orphans, dependencies, then links (hub entries included). `undefined` when no
1492
+ * element or link has a namespaced type. Throws `PistarParseError` for a string that isn't JSON.
1493
+ */
1494
+ function detectPrefix(json) {
1495
+ const file = parseJson(json);
1496
+ const candidates = function* () {
1497
+ for (const actor of arrayOf(file.actors)) {
1498
+ yield actor;
1499
+ if (isRecord(actor)) yield* arrayOf(actor.nodes);
1500
+ }
1501
+ yield* arrayOf(file.orphans);
1502
+ yield* arrayOf(file.dependencies);
1503
+ for (const link of arrayOf(file.links)) if (isRecord(link) && isRecord(link.hub)) {
1504
+ yield link.hub;
1505
+ yield* arrayOf(link.starts);
1506
+ yield link.end;
1507
+ } else yield link;
1508
+ };
1509
+ for (const candidate of candidates()) {
1510
+ const prefix = prefixOf(candidate);
1511
+ if (prefix !== void 0) return prefix;
1512
+ }
1513
+ }
1514
+ /**
1515
+ * Which of `roots` a file is written in: the one whose `prefix` is the file's (`detectPrefix`);
1516
+ * for a file without types, the only one whose `file.tool` is the file's `tool`; otherwise
1517
+ * `undefined`. Pass the result to `parsePistar(json, { metamodel })`.
1518
+ */
1519
+ function detectFramework(json, roots) {
1520
+ const file = parseJson(json);
1521
+ const prefix = detectPrefix(file);
1522
+ if (prefix !== void 0) return roots.find((root) => root.prefix === prefix);
1523
+ if (typeof file.tool !== "string") return void 0;
1524
+ const byTool = roots.filter((root) => root.file.tool === file.tool);
1525
+ return byTool.length === 1 ? byTool[0] : void 0;
1526
+ }
1089
1527
  function optional(key, value) {
1090
1528
  return value === void 0 ? {} : { [key]: value };
1091
1529
  }
@@ -1111,37 +1549,43 @@ function toPistarObject(input, options = {}) {
1111
1549
  const model = input;
1112
1550
  const metamodel = options.metamodel ?? metamodelOf(input);
1113
1551
  const isActorKind = (kind) => metamodel.elements.get(kind)?.category === "actor";
1552
+ const isHub = (element) => metamodel.elements.get(element.kind)?.role === "hub";
1114
1553
  const actors = [];
1115
1554
  const orphans = [];
1116
1555
  const dependencies = [];
1117
1556
  const children = /* @__PURE__ */ new Map();
1118
- for (const element of model.elements.values()) if (element.parent !== void 0 && !isActorKind(element.kind)) {
1119
- let list = children.get(element.parent);
1120
- if (!list) children.set(element.parent, list = []);
1121
- list.push({
1557
+ for (const element of model.elements.values()) {
1558
+ if (isHub(element)) continue;
1559
+ if (element.parent !== void 0 && !isActorKind(element.kind)) {
1560
+ let list = children.get(element.parent);
1561
+ if (!list) children.set(element.parent, list = []);
1562
+ list.push({
1563
+ ...elementJson(element, metamodel),
1564
+ ...element.extra
1565
+ });
1566
+ }
1567
+ }
1568
+ for (const element of model.elements.values()) {
1569
+ if (isHub(element)) continue;
1570
+ if (isActorKind(element.kind)) actors.push({
1122
1571
  ...elementJson(element, metamodel),
1572
+ nodes: children.get(element.id) ?? [],
1123
1573
  ...element.extra
1124
1574
  });
1125
- }
1126
- for (const element of model.elements.values()) if (isActorKind(element.kind)) actors.push({
1127
- ...elementJson(element, metamodel),
1128
- nodes: children.get(element.id) ?? [],
1129
- ...element.extra
1130
- });
1131
- else if (element.isDependum) {
1132
- const { inbound, outbound } = dependencyLinksOf(model, element.id);
1133
- dependencies.push({
1575
+ else if (element.isDependum) {
1576
+ const { inbound, outbound } = dependencyLinksOf(model, element.id);
1577
+ dependencies.push({
1578
+ ...elementJson(element, metamodel),
1579
+ source: inbound?.source ?? element.dependency?.source ?? "",
1580
+ target: outbound?.target ?? element.dependency?.target ?? "",
1581
+ ...element.extra
1582
+ });
1583
+ } else if (element.parent === void 0 || !model.elements.has(element.parent)) orphans.push({
1134
1584
  ...elementJson(element, metamodel),
1135
- source: inbound?.source ?? element.dependency?.source ?? "",
1136
- target: outbound?.target ?? element.dependency?.target ?? "",
1137
1585
  ...element.extra
1138
1586
  });
1139
- } else if (element.parent === void 0 || !model.elements.has(element.parent)) orphans.push({
1140
- ...elementJson(element, metamodel),
1141
- ...element.extra
1142
- });
1143
- const links = [];
1144
- for (const link of model.links.values()) links.push({
1587
+ }
1588
+ const linkJson = (link) => ({
1145
1589
  id: link.id,
1146
1590
  type: pistarTypeOf(metamodel.links.get(link.kind)?.pistarType, link.kind, metamodel),
1147
1591
  source: link.source,
@@ -1151,6 +1595,54 @@ function toPistarObject(input, options = {}) {
1151
1595
  ...link.label !== void 0 ? { label: link.label } : {},
1152
1596
  ...link.extra
1153
1597
  });
1598
+ const hubs = /* @__PURE__ */ new Map();
1599
+ const partsOf = (hubId) => {
1600
+ let parts = hubs.get(hubId);
1601
+ if (!parts) hubs.set(hubId, parts = { legs: [] });
1602
+ return parts;
1603
+ };
1604
+ const hubOf = (link) => {
1605
+ const role = metamodel.links.get(link.kind)?.role;
1606
+ const id = role === "leg" ? link.target : role === "end" ? link.source : void 0;
1607
+ const hub = id === void 0 ? void 0 : model.elements.get(id);
1608
+ return hub && isHub(hub) ? hub.id : void 0;
1609
+ };
1610
+ for (const link of model.links.values()) {
1611
+ const hubId = hubOf(link);
1612
+ if (hubId === void 0) continue;
1613
+ const parts = partsOf(hubId);
1614
+ if (metamodel.links.get(link.kind)?.role === "leg") parts.legs.push(link);
1615
+ else parts.end ??= link;
1616
+ }
1617
+ const hubEntry = (hub) => {
1618
+ const parts = hubs.get(hub.id) ?? { legs: [] };
1619
+ const { [HUB_ENTRY_KEY]: entryExtra, ...hubExtra } = hub.extra ?? {};
1620
+ return {
1621
+ hub: {
1622
+ id: hub.id,
1623
+ type: pistarTypeOf(metamodel.elements.get(hub.kind)?.pistarType, hub.kind, metamodel),
1624
+ x: hub.x,
1625
+ y: hub.y,
1626
+ ...hub.customProperties ? { customProperties: { ...hub.customProperties } } : {},
1627
+ ...hubExtra
1628
+ },
1629
+ starts: parts.legs.map(linkJson),
1630
+ ...parts.end ? { end: linkJson(parts.end) } : {},
1631
+ ...isRecord(entryExtra) ? entryExtra : {}
1632
+ };
1633
+ };
1634
+ const links = [];
1635
+ const written = /* @__PURE__ */ new Set();
1636
+ for (const link of model.links.values()) {
1637
+ const hubId = hubOf(link);
1638
+ const parts = hubId === void 0 ? void 0 : hubs.get(hubId);
1639
+ if (hubId === void 0 || !parts || !parts.legs.includes(link) && parts.end !== link) links.push(linkJson(link));
1640
+ else if (!written.has(hubId)) {
1641
+ written.add(hubId);
1642
+ links.push(hubEntry(model.elements.get(hubId)));
1643
+ }
1644
+ }
1645
+ for (const element of model.elements.values()) if (isHub(element) && !written.has(element.id)) links.push(hubEntry(element));
1154
1646
  const layout = layouts.get(model);
1155
1647
  const display = {};
1156
1648
  const displayOf = (id) => {
@@ -1198,7 +1690,7 @@ function toPistarObject(input, options = {}) {
1198
1690
  if ((Array.isArray(value) ? value.length === 0 : Object.keys(value).length === 0) && !layout.keys.includes(key)) values[key] = void 0;
1199
1691
  }
1200
1692
  const order = [
1201
- ...layout?.keys ?? [],
1693
+ ...layout?.keys ?? metamodel.file.keys ?? [],
1202
1694
  ...TOP_LEVEL_KEYS,
1203
1695
  ...Object.keys(model.extra ?? {})
1204
1696
  ];
@@ -1387,7 +1879,8 @@ function nestElement(model, id, parent) {
1387
1879
  }
1388
1880
  /**
1389
1881
  * Removes elements and everything that depends on them: inner elements of removed actors,
1390
- * links touching removed elements, and the rest of any dependency that loses a part.
1882
+ * links touching removed elements (and links targeting removed links), and the rest of any
1883
+ * dependency that loses a part.
1391
1884
  */
1392
1885
  function removeElements(model, ids) {
1393
1886
  return typed(removeCascade(any(model), new Set(ids), /* @__PURE__ */ new Set()));
@@ -1411,11 +1904,28 @@ function removeCascade(model, elementIds, linkIds) {
1411
1904
  }
1412
1905
  for (const link of model.links.values()) {
1413
1906
  if (linkIds.has(link.id)) continue;
1414
- if (elementIds.has(link.source) || elementIds.has(link.target)) {
1907
+ if (elementIds.has(link.source) || elementIds.has(link.target) || linkIds.has(link.target)) {
1415
1908
  linkIds.add(link.id);
1416
1909
  changed = true;
1417
1910
  }
1418
1911
  }
1912
+ for (const element of model.elements.values()) {
1913
+ if (elementIds.has(element.id) || metamodel.elements.get(element.kind)?.role !== "hub") continue;
1914
+ let legs = 0;
1915
+ let removedLegs = 0;
1916
+ let endRemoved = false;
1917
+ for (const link of model.links.values()) {
1918
+ const role = metamodel.links.get(link.kind)?.role;
1919
+ if (role === "leg" && link.target === element.id) {
1920
+ legs++;
1921
+ if (linkIds.has(link.id)) removedLegs++;
1922
+ } else if (role === "end" && link.source === element.id && linkIds.has(link.id)) endRemoved = true;
1923
+ }
1924
+ if (endRemoved || legs > 0 && removedLegs === legs) {
1925
+ elementIds.add(element.id);
1926
+ changed = true;
1927
+ }
1928
+ }
1419
1929
  for (const link of model.links.values()) {
1420
1930
  if (!linkIds.has(link.id) || !isDependencyHalf(link)) continue;
1421
1931
  for (const end of [link.source, link.target]) if (model.elements.get(end)?.isDependum && !elementIds.has(end)) {
@@ -1434,13 +1944,21 @@ function removeCascade(model, elementIds, linkIds) {
1434
1944
  links
1435
1945
  });
1436
1946
  }
1437
- /** Adds a link between two elements if the iStar constraints allow it. */
1947
+ /**
1948
+ * Adds a link between two elements if the iStar constraints allow it.
1949
+ *
1950
+ * With an n-ary kind (`arity: 'n'`), a `target` that is one of the kind's hubs gets a new leg
1951
+ * from `source` (see `addLeg`). Any other target gets a new hub, centred between source and
1952
+ * target, with a leg from `source` and an end to `target`. `input.id` and `customProperties`
1953
+ * then go to the hub, and `label` and `name` to the end.
1954
+ */
1438
1955
  function connect(model, input, options = {}) {
1439
1956
  const m = any(model);
1440
1957
  const metamodel = metaOf(m);
1441
1958
  const kind = input.kind;
1442
1959
  const definition = metamodel.links.get(kind);
1443
1960
  if (definition?.category === "dependency") throw new ModelOperationError("use addDependency to create dependencies");
1961
+ if (definition?.parts) return connectHub(m, definition, input, options);
1444
1962
  let { source, target } = input;
1445
1963
  let reversed = false;
1446
1964
  if (!options.force) {
@@ -1458,7 +1976,7 @@ function connect(model, input, options = {}) {
1458
1976
  } else {
1459
1977
  if (!definition) throw new ModelOperationError(`unknown link kind "${kind}" in ${metamodel.name}`);
1460
1978
  requireElement(m, source);
1461
- requireElement(m, target);
1979
+ if (!m.links.has(target)) requireElement(m, target);
1462
1980
  }
1463
1981
  const id = input.id ?? (options.createId ?? defaultIdGenerator)();
1464
1982
  if (m.elements.has(id) || m.links.has(id)) throw new ModelOperationError(`id "${id}" is already in use`);
@@ -1478,6 +1996,113 @@ function connect(model, input, options = {}) {
1478
1996
  reversed
1479
1997
  };
1480
1998
  }
1999
+ function connectHub(m, definition, input, options) {
2000
+ const metamodel = metaOf(m);
2001
+ const parts = definition.parts;
2002
+ const { source, target } = input;
2003
+ if (m.elements.get(target)?.kind === parts.hub) return addLeg(typed(m), target, source, options);
2004
+ if (!options.force) {
2005
+ const check = canLink(m, source, target, definition.kind);
2006
+ if (!check.ok) return check;
2007
+ }
2008
+ const from = requireElement(m, source);
2009
+ const to = requireElement(m, target);
2010
+ const createId = options.createId ?? defaultIdGenerator;
2011
+ const hubId = input.id ?? createId();
2012
+ const legId = createId();
2013
+ const endId = createId();
2014
+ for (const [i, id] of [
2015
+ hubId,
2016
+ legId,
2017
+ endId
2018
+ ].entries()) if (m.elements.has(id) || m.links.has(id) || [
2019
+ hubId,
2020
+ legId,
2021
+ endId
2022
+ ].indexOf(id) !== i) throw new ModelOperationError(`id "${id}" is already in use`);
2023
+ const position = defaultHubPosition(m, from, to, metamodel.elements.get(parts.hub)?.size ?? {
2024
+ width: 20,
2025
+ height: 20
2026
+ });
2027
+ const hub = withoutUndefined({
2028
+ id: hubId,
2029
+ kind: parts.hub,
2030
+ name: "",
2031
+ x: position.x,
2032
+ y: position.y,
2033
+ customProperties: input.customProperties
2034
+ });
2035
+ const leg = {
2036
+ id: legId,
2037
+ kind: parts.leg,
2038
+ source,
2039
+ target: hubId
2040
+ };
2041
+ const end = withoutUndefined({
2042
+ id: endId,
2043
+ kind: parts.end,
2044
+ source: hubId,
2045
+ target,
2046
+ label: input.label,
2047
+ name: input.name
2048
+ });
2049
+ return {
2050
+ ok: true,
2051
+ model: typed(derive(m, {
2052
+ elements: new Map(m.elements).set(hubId, hub),
2053
+ links: new Map(m.links).set(legId, leg).set(endId, end)
2054
+ })),
2055
+ link: end,
2056
+ reversed: false,
2057
+ hub: {
2058
+ hub,
2059
+ legs: [leg],
2060
+ end
2061
+ }
2062
+ };
2063
+ }
2064
+ /**
2065
+ * Adds a leg from `source` to the hub `hubId` of an n-ary link (one more source for it), if the
2066
+ * kind's rules allow it. `options.id` is the leg's id.
2067
+ */
2068
+ function addLeg(model, hubId, source, options = {}) {
2069
+ const m = any(model);
2070
+ const metamodel = metaOf(m);
2071
+ const hub = requireElement(m, hubId);
2072
+ const base = metamodel.links.get(metamodel.elements.get(hub.kind)?.of ?? "");
2073
+ if (metamodel.elements.get(hub.kind)?.role !== "hub" || !base?.parts) throw new ModelOperationError(`"${hubId}" is not the hub of an n-ary link`);
2074
+ const kind = base.parts.leg;
2075
+ if (!options.force) {
2076
+ const check = canLink(m, source, hubId, kind);
2077
+ if (!check.ok) return check;
2078
+ } else requireElement(m, source);
2079
+ const id = options.id ?? (options.createId ?? defaultIdGenerator)();
2080
+ if (m.elements.has(id) || m.links.has(id)) throw new ModelOperationError(`id "${id}" is already in use`);
2081
+ const leg = {
2082
+ id,
2083
+ kind,
2084
+ source,
2085
+ target: hubId
2086
+ };
2087
+ const next = derive(m, { links: new Map(m.links).set(id, leg) });
2088
+ return {
2089
+ ok: true,
2090
+ model: typed(next),
2091
+ link: leg,
2092
+ reversed: false,
2093
+ hub: hubLinkOf(typed(next), hubId)
2094
+ };
2095
+ }
2096
+ /**
2097
+ * Removes one leg of an n-ary link. Removing its last leg removes the whole link (hub and end),
2098
+ * as `disconnect` does.
2099
+ */
2100
+ function removeLeg(model, legId) {
2101
+ const m = any(model);
2102
+ const leg = requireLink(m, legId);
2103
+ if (metaOf(m).links.get(leg.kind)?.role !== "leg") throw new ModelOperationError(`"${legId}" is not a leg of an n-ary link`);
2104
+ return disconnect(model, legId);
2105
+ }
1481
2106
  /** Adds depender → dependum → dependee. */
1482
2107
  function addDependency(model, input, options = {}) {
1483
2108
  const m = any(model);
@@ -1530,7 +2155,10 @@ function addDependency(model, input, options = {}) {
1530
2155
  links: [first, second]
1531
2156
  };
1532
2157
  }
1533
- /** Removes a link. Removing either half of a dependency removes the whole dependency. */
2158
+ /**
2159
+ * Removes a link, and the links that target it. Removing either half of a dependency removes
2160
+ * the whole dependency.
2161
+ */
1534
2162
  function disconnect(model, id) {
1535
2163
  const m = any(model);
1536
2164
  requireLink(m, id);
@@ -1665,10 +2293,29 @@ function createModelStore(initial, options = {}) {
1665
2293
  });
1666
2294
  if (result.ok) commit(result.model, {
1667
2295
  type: "connect",
1668
- id: result.link.id
2296
+ id: result.link.id,
2297
+ ...result.hub ? { hubId: result.hub.hub.id } : {}
1669
2298
  });
1670
2299
  return result;
1671
2300
  },
2301
+ addLeg(hubId, source, legOptions) {
2302
+ const result = addLeg(model, hubId, source, {
2303
+ ...legOptions,
2304
+ ...ctx
2305
+ });
2306
+ if (result.ok) commit(result.model, {
2307
+ type: "connect",
2308
+ id: result.link.id,
2309
+ hubId
2310
+ });
2311
+ return result;
2312
+ },
2313
+ removeLeg(id) {
2314
+ commit(removeLeg(model, id), {
2315
+ type: "removeLeg",
2316
+ id
2317
+ });
2318
+ },
1672
2319
  addDependency(input, dependencyOptions) {
1673
2320
  const result = addDependency(model, input, {
1674
2321
  ...dependencyOptions,
@@ -1897,7 +2544,7 @@ const prop = {
1897
2544
  function isCustomPropertiesRecord(source) {
1898
2545
  if ("customProperties" in source) return false;
1899
2546
  const owner = source;
1900
- return !(typeof owner.kind === "string" && owner.kind.startsWith("istar.") && (typeof owner.x === "number" || owner.source !== void 0 && owner.target !== void 0));
2547
+ return !(typeof owner.kind === "string" && owner.kind.includes(".") && (typeof owner.x === "number" || typeof owner.source === "string" && typeof owner.target === "string"));
1901
2548
  }
1902
2549
  function resolveCustomProperties(source) {
1903
2550
  if (source === void 0) return void 0;
@@ -2204,13 +2851,18 @@ exports.DEFAULT_DIAGRAM = DEFAULT_DIAGRAM;
2204
2851
  exports.DEFAULT_ELEMENT_SIZE = DEFAULT_ELEMENT_SIZE;
2205
2852
  exports.DEPENDENCY_LINK_KINDS = DEPENDENCY_LINK_KINDS;
2206
2853
  exports.DIAGNOSTIC_SEVERITIES = DIAGNOSTIC_SEVERITIES;
2854
+ exports.DSM3_FILE_KEYS = DSM3_FILE_KEYS;
2855
+ exports.DSM3_TOOL = DSM3_TOOL;
2207
2856
  exports.ELEMENT_KINDS = ELEMENT_KINDS;
2208
2857
  exports.FILE_METAMODEL_KEY = FILE_METAMODEL_KEY;
2858
+ exports.HUB_ENTRY_KEY = HUB_ENTRY_KEY;
2209
2859
  exports.ISTAR_2_0 = ISTAR_2_0;
2860
+ exports.ISTAR_2_0_DECLARATION = ISTAR_2_0_DECLARATION;
2210
2861
  exports.ISTAR_PREFIX = ISTAR_PREFIX;
2211
2862
  exports.ISTAR_VERSION = ISTAR_VERSION;
2212
2863
  exports.LINK_KINDS = LINK_KINDS;
2213
2864
  exports.LINK_KIND_INFO = LINK_KIND_INFO;
2865
+ exports.LINK_SELECTOR = LINK_SELECTOR;
2214
2866
  exports.METAMODEL_VERSION = METAMODEL_VERSION;
2215
2867
  exports.MetamodelError = MetamodelError;
2216
2868
  exports.ModelOperationError = ModelOperationError;
@@ -2222,6 +2874,7 @@ exports.PistarParseError = PistarParseError;
2222
2874
  exports.PistarWriteError = PistarWriteError;
2223
2875
  exports.addDependency = addDependency;
2224
2876
  exports.addElement = addElement;
2877
+ exports.addLeg = addLeg;
2225
2878
  exports.boolean = boolean;
2226
2879
  exports.canLink = canLink;
2227
2880
  exports.childrenOf = childrenOf;
@@ -2230,15 +2883,20 @@ exports.connect = connect;
2230
2883
  exports.createDiagnosticsStore = createDiagnosticsStore;
2231
2884
  exports.createEmptyModel = createEmptyModel;
2232
2885
  exports.createModelStore = createModelStore;
2886
+ exports.defaultHubPosition = defaultHubPosition;
2233
2887
  exports.defaultIdGenerator = defaultIdGenerator;
2888
+ exports.defineMetamodel = defineMetamodel;
2234
2889
  exports.defineMetamodelExtension = defineMetamodelExtension;
2235
2890
  exports.defineProperties = defineProperties;
2236
2891
  exports.dependencyEnds = dependencyEnds;
2237
2892
  exports.dependencyLinksOf = dependencyLinksOf;
2893
+ exports.detectFramework = detectFramework;
2894
+ exports.detectPrefix = detectPrefix;
2238
2895
  exports.disconnect = disconnect;
2239
2896
  exports.effectiveElementKind = effectiveElementKind;
2240
2897
  exports.effectiveLinkKind = effectiveLinkKind;
2241
2898
  exports.elementBehavesLike = elementBehavesLike;
2899
+ exports.elementCenter = elementCenter;
2242
2900
  exports.elementKindDefinition = elementKindDefinition;
2243
2901
  exports.enumOf = enumOf;
2244
2902
  exports.extendMetamodel = extendMetamodel;
@@ -2251,6 +2909,8 @@ exports.fromNodeIdDiagnostics = fromNodeIdDiagnostics;
2251
2909
  exports.getSourceLayout = getSourceLayout;
2252
2910
  exports.groupDiagnostics = groupDiagnostics;
2253
2911
  exports.hostMetamodelOf = hostMetamodelOf;
2912
+ exports.hubLinkOf = hubLinkOf;
2913
+ exports.hubOfLink = hubOfLink;
2254
2914
  exports.inheritMetamodel = inheritMetamodel;
2255
2915
  exports.inheritSourceLayout = inheritSourceLayout;
2256
2916
  exports.isActor = isActor;
@@ -2261,13 +2921,17 @@ exports.isActorLinkKind = isActorLinkKind;
2261
2921
  exports.isContributionLabel = isContributionLabel;
2262
2922
  exports.isDependencyLinkKind = isDependencyLinkKind;
2263
2923
  exports.isElementKind = isElementKind;
2924
+ exports.isHubElement = isHubElement;
2264
2925
  exports.isLinkKind = isLinkKind;
2926
+ exports.isLinkRef = isLinkRef;
2265
2927
  exports.isNode = isNode;
2266
2928
  exports.isNodeIn = isNodeIn;
2267
2929
  exports.isNodeKind = isNodeKind;
2268
2930
  exports.isNodeKindIn = isNodeKindIn;
2269
2931
  exports.isNodeLinkKind = isNodeLinkKind;
2932
+ exports.linkBehavesLike = linkBehavesLike;
2270
2933
  exports.linkKindDefinition = linkKindDefinition;
2934
+ exports.linkTargetOf = linkTargetOf;
2271
2935
  exports.linksOf = linksOf;
2272
2936
  exports.mergeDiagnostics = mergeDiagnostics;
2273
2937
  exports.metamodelOf = metamodelOf;
@@ -2278,9 +2942,11 @@ exports.parsePistar = parsePistar;
2278
2942
  exports.prop = prop;
2279
2943
  exports.removeElement = removeElement;
2280
2944
  exports.removeElements = removeElements;
2945
+ exports.removeLeg = removeLeg;
2281
2946
  exports.setHostMetamodel = setHostMetamodel;
2282
2947
  exports.shortKindName = shortKindName;
2283
2948
  exports.string = string;
2949
+ exports.targetsLinks = targetsLinks;
2284
2950
  exports.toPistar = toPistar;
2285
2951
  exports.toPistarObject = toPistarObject;
2286
2952
  exports.updateDiagram = updateDiagram;