@istar-ts/core 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -138,61 +138,6 @@ function shortKindName(kind) {
138
138
  }
139
139
  //#endregion
140
140
  //#region src/metamodels.ts
141
- function shortName(kind) {
142
- const dot = kind.indexOf(".");
143
- return dot === -1 ? kind : kind.slice(dot + 1);
144
- }
145
- function buildIstar() {
146
- const elements = /* @__PURE__ */ new Map();
147
- for (const kind of ACTOR_KINDS) elements.set(kind, {
148
- kind,
149
- category: "actor",
150
- label: shortName(kind),
151
- size: DEFAULT_ELEMENT_SIZE[kind],
152
- pistarType: kind
153
- });
154
- for (const kind of NODE_KINDS) elements.set(kind, {
155
- kind,
156
- category: "node",
157
- label: shortName(kind),
158
- size: DEFAULT_ELEMENT_SIZE[kind],
159
- info: NODE_KIND_INFO[kind],
160
- pistarType: kind
161
- });
162
- const links = /* @__PURE__ */ new Map();
163
- const labels = {
164
- "istar.IsALink": "Is-A",
165
- "istar.ParticipatesInLink": "Participates-In",
166
- "istar.DependencyLink": "Dependency",
167
- "istar.AndRefinementLink": "And-Refinement",
168
- "istar.OrRefinementLink": "Or-Refinement",
169
- "istar.NeededByLink": "Needed-By",
170
- "istar.QualificationLink": "Qualification",
171
- "istar.ContributionLink": "Contribution"
172
- };
173
- const add = (kinds, category) => {
174
- for (const kind of kinds) links.set(kind, {
175
- kind,
176
- category,
177
- label: labels[kind],
178
- info: LINK_KIND_INFO[kind],
179
- pistarType: kind
180
- });
181
- };
182
- add(ACTOR_LINK_KINDS, "actor");
183
- add(DEPENDENCY_LINK_KINDS, "dependency");
184
- add(NODE_LINK_KINDS, "node");
185
- return {
186
- name: "istar-2.0",
187
- extensions: [],
188
- elements,
189
- links,
190
- elementsByPistarType: new Map([...elements.keys()].map((k) => [k, k])),
191
- linksByPistarType: new Map([...links.keys()].map((k) => [k, k]))
192
- };
193
- }
194
- /** iStar 2.0, as the piStar tool implements it. The default metamodel everywhere. */
195
- const ISTAR_2_0 = buildIstar();
196
141
  var MetamodelError = class extends Error {
197
142
  name = "MetamodelError";
198
143
  };
@@ -204,117 +149,337 @@ function defineMetamodelExtension(extension) {
204
149
  return extension;
205
150
  }
206
151
  const KIND_NAME = /^[A-Za-z_][\w-]*\.[A-Za-z_][\w-]*$/;
152
+ const PREFIX_NAME = /^[A-Za-z_][\w-]*$/;
207
153
  const CATEGORY_SELECTORS = /* @__PURE__ */ new Set([
208
154
  "node",
209
155
  "actor",
210
156
  "*"
211
157
  ]);
158
+ /** The `targets` selector for any link (see `LinkRules`). */
159
+ const LINK_SELECTOR = "link";
160
+ /** piStar-ext's defaults for an extension's new nodes (tool/language/metamodel.js). */
161
+ const EXTENSION_NODE_INFO = {
162
+ canBeInnerElement: true,
163
+ canBeDependum: true,
164
+ canBeOnPaper: false
165
+ };
166
+ /** DSM3-goals' defaults for a root's nodes (istarcore metamodelManager). */
167
+ /** A hub sits on the paper, outside any actor, and is never a dependum. */
168
+ const HUB_INFO = {
169
+ canBeInnerElement: false,
170
+ canBeDependum: false,
171
+ canBeOnPaper: true
172
+ };
173
+ /** A circle filling the hub's box (DSM3-goals draws a circle of radius 10 in 20×20). */
174
+ const HUB_SHAPE = {
175
+ path: "M 0 10 A 10 10 0 1 0 20 10 A 10 10 0 1 0 0 10 Z",
176
+ viewBox: "0 0 20 20"
177
+ };
178
+ const ROOT_NODE_INFO = {
179
+ canBeInnerElement: true,
180
+ canBeDependum: true,
181
+ canBeOnPaper: true
182
+ };
183
+ function shortName(kind) {
184
+ const dot = kind.indexOf(".");
185
+ return dot === -1 ? kind : kind.slice(dot + 1);
186
+ }
187
+ function claimName(table, kind, what) {
188
+ const { where } = table;
189
+ 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"`);
190
+ 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`);
191
+ if (table.elements.has(kind) || table.links.has(kind)) throw new MetamodelError(`${where}: kind "${kind}" already exists`);
192
+ }
193
+ function claimType(table, type, kind) {
194
+ const { where } = table;
195
+ if (typeof type !== "string" || type === "") throw new MetamodelError(`${where}: kind "${kind}" has an empty pistarType`);
196
+ 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`);
197
+ }
198
+ function isCompleteNodeInfo(info) {
199
+ return info !== void 0 && typeof info.canBeInnerElement === "boolean" && typeof info.canBeDependum === "boolean" && typeof info.canBeOnPaper === "boolean";
200
+ }
201
+ function addElementKind(table, declaration) {
202
+ const { where } = table;
203
+ const { kind } = declaration;
204
+ claimName(table, kind, "element");
205
+ let parent;
206
+ if (declaration.behavesLike !== void 0) {
207
+ parent = table.elements.get(declaration.behavesLike);
208
+ if (!parent) throw new MetamodelError(`${where}: "${kind}" behaves like unknown element kind "${declaration.behavesLike}"`);
209
+ }
210
+ const category = declaration.category ?? parent?.category;
211
+ if (category !== "actor" && category !== "node") throw new MetamodelError(`${where}: element kind "${kind}" needs a category ("node" or "actor") or behavesLike`);
212
+ if (parent && parent.category !== category) throw new MetamodelError(`${where}: "${kind}" is a ${category} but behaves like "${parent.kind}", a ${parent.category}`);
213
+ const type = declaration.pistarType ?? kind;
214
+ claimType(table, type, kind);
215
+ const info = isCompleteNodeInfo(declaration.info) ? declaration.info : {
216
+ ...parent?.info ?? (table.extension === void 0 ? ROOT_NODE_INFO : EXTENSION_NODE_INFO),
217
+ ...declaration.info
218
+ };
219
+ const definition = {
220
+ kind,
221
+ category,
222
+ label: declaration.label ?? shortName(kind),
223
+ size: declaration.size ?? parent?.size ?? (category === "actor" ? {
224
+ width: 200,
225
+ height: 120
226
+ } : {
227
+ width: 90,
228
+ height: 55
229
+ }),
230
+ ...category === "node" ? { info } : {},
231
+ ...parent ? { behavesLike: parent.kind } : {},
232
+ pistarType: type,
233
+ ...table.extension !== void 0 ? { extension: table.extension } : {},
234
+ ...declaration.shape ? { shape: declaration.shape } : {},
235
+ ...declaration.textBox ? { textBox: declaration.textBox } : {}
236
+ };
237
+ table.elements.set(kind, definition);
238
+ table.elementsByType.set(type, kind);
239
+ }
240
+ function addLinkKind(table, declaration) {
241
+ const { where } = table;
242
+ const { kind } = declaration;
243
+ claimName(table, kind, "link");
244
+ const isRoot = table.extension === void 0;
245
+ 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`);
246
+ if (!isRoot && declaration.behavesLike === void 0 && declaration.rules === void 0) throw new MetamodelError(`${where}: link kind "${kind}" needs exactly one of behavesLike or rules`);
247
+ let parent;
248
+ if (declaration.behavesLike !== void 0) {
249
+ parent = table.links.get(declaration.behavesLike);
250
+ if (!parent) throw new MetamodelError(`${where}: "${kind}" behaves like unknown link kind "${declaration.behavesLike}"`);
251
+ }
252
+ const category = declaration.category ?? parent?.category ?? "node";
253
+ if (category !== "node" && category !== "actor" && category !== "dependency") throw new MetamodelError(`${where}: link kind "${kind}" has an unknown category "${category}"`);
254
+ if (declaration.arity !== void 0) {
255
+ if (declaration.arity !== "n") throw new MetamodelError(`${where}: link kind "${kind}" has an unknown arity ${JSON.stringify(declaration.arity)}`);
256
+ if (!declaration.rules || category !== "node") throw new MetamodelError(`${where}: n-ary link kind "${kind}" needs rules and the "node" category`);
257
+ }
258
+ if (parent && parent.category !== category) throw new MetamodelError(`${where}: "${kind}" is a ${category} link but behaves like "${parent.kind}", a ${parent.category} link`);
259
+ 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)`);
260
+ if (declaration.rules) for (const [side, selectors] of [["sources", declaration.rules.sources], ["targets", declaration.rules.targets]]) {
261
+ if (!Array.isArray(selectors) || selectors.length === 0) throw new MetamodelError(`${where}: link kind "${kind}" needs at least one of ${side}`);
262
+ 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}"`);
263
+ }
264
+ const type = declaration.pistarType ?? kind;
265
+ claimType(table, type, kind);
266
+ const definition = {
267
+ kind,
268
+ category,
269
+ label: declaration.label ?? shortName(kind),
270
+ info: declaration.info ?? parent?.info ?? {},
271
+ ...parent ? { behavesLike: parent.kind } : {},
272
+ ...declaration.rules ? { rules: declaration.rules } : {},
273
+ ...declaration.check ? { check: declaration.check } : {},
274
+ pistarType: type,
275
+ ...table.extension !== void 0 ? { extension: table.extension } : {},
276
+ ...declaration.line ? { line: declaration.line } : {}
277
+ };
278
+ if (declaration.arity !== "n") {
279
+ table.links.set(kind, definition);
280
+ table.linksByType.set(type, kind);
281
+ return;
282
+ }
283
+ const parts = {
284
+ hub: declaration.parts?.hub ?? `${kind}Hub`,
285
+ leg: declaration.parts?.leg ?? `${kind}Start`,
286
+ end: declaration.parts?.end ?? `${kind}End`
287
+ };
288
+ table.links.set(kind, {
289
+ ...definition,
290
+ arity: "n",
291
+ parts
292
+ });
293
+ table.linksByType.set(type, kind);
294
+ addHubKinds(table, table.links.get(kind), parts, declaration.hubSize);
295
+ }
296
+ /**
297
+ * Adds the kinds an n-ary link kind is made of, as DSM3-goals' `setupSplitLinks` generates them:
298
+ * the hub (an element), the legs (each source → the hub, at most one per source) and the end
299
+ * (the hub → the target).
300
+ */
301
+ function addHubKinds(table, base, parts, hubSize) {
302
+ const rules = base.rules;
303
+ const claim = (kind, type, what) => {
304
+ try {
305
+ claimName(table, kind, what);
306
+ claimType(table, type, kind);
307
+ } catch (error) {
308
+ if (!(error instanceof MetamodelError)) throw error;
309
+ throw new MetamodelError(`${error.message} (generated for n-ary link kind "${base.kind}")`);
310
+ }
311
+ };
312
+ const owner = table.extension !== void 0 ? { extension: table.extension } : {};
313
+ claim(parts.hub, `${base.pistarType}Hub`, "element");
314
+ table.elements.set(parts.hub, {
315
+ kind: parts.hub,
316
+ category: "node",
317
+ label: `${base.label} hub`,
318
+ size: hubSize ?? {
319
+ width: 20,
320
+ height: 20
321
+ },
322
+ info: HUB_INFO,
323
+ pistarType: `${base.pistarType}Hub`,
324
+ ...owner,
325
+ shape: HUB_SHAPE,
326
+ role: "hub",
327
+ of: base.kind
328
+ });
329
+ table.elementsByType.set(`${base.pistarType}Hub`, parts.hub);
330
+ const common = {
331
+ category: "node",
332
+ ...owner,
333
+ of: base.kind
334
+ };
335
+ claim(parts.leg, `${base.pistarType}Start`, "link");
336
+ table.links.set(parts.leg, {
337
+ ...common,
338
+ kind: parts.leg,
339
+ label: `${base.label} leg`,
340
+ info: {},
341
+ rules: {
342
+ sources: rules.sources,
343
+ targets: [parts.hub],
344
+ sameActor: false,
345
+ ...rules.allowDependum !== void 0 ? { allowDependum: rules.allowDependum } : {},
346
+ unique: "kind"
347
+ },
348
+ pistarType: `${base.pistarType}Start`,
349
+ role: "leg"
350
+ });
351
+ table.linksByType.set(`${base.pistarType}Start`, parts.leg);
352
+ claim(parts.end, `${base.pistarType}End`, "link");
353
+ table.links.set(parts.end, {
354
+ ...common,
355
+ kind: parts.end,
356
+ label: `${base.label} end`,
357
+ info: base.info,
358
+ rules: {
359
+ sources: [parts.hub],
360
+ targets: rules.targets,
361
+ sameActor: false,
362
+ ...rules.allowDependum !== void 0 ? { allowDependum: rules.allowDependum } : {},
363
+ unique: "kind"
364
+ },
365
+ pistarType: `${base.pistarType}End`,
366
+ ...base.line ? { line: base.line } : {},
367
+ role: "end"
368
+ });
369
+ table.linksByType.set(`${base.pistarType}End`, parts.end);
370
+ }
371
+ /**
372
+ * Builds a root metamodel: a modelling language of its own, such as iStar 2.0, KAOS or NFR.
373
+ * Kinds are namespaced (any namespace; usually `prefix`), declared in order: `behavesLike` and
374
+ * link rules refer to kinds declared before them. Extensions of the result may not use `prefix`.
375
+ *
376
+ * Throws `MetamodelError` on the same declaration errors as `extendMetamodel`.
377
+ */
378
+ function defineMetamodel(declaration) {
379
+ const { name, prefix } = declaration;
380
+ if (!name || typeof name !== "string") throw new MetamodelError("a root metamodel needs a name");
381
+ const where = `metamodel "${name}"`;
382
+ 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"`);
383
+ const table = {
384
+ where,
385
+ namespace: prefix,
386
+ elements: /* @__PURE__ */ new Map(),
387
+ links: /* @__PURE__ */ new Map(),
388
+ elementsByType: /* @__PURE__ */ new Map(),
389
+ linksByType: /* @__PURE__ */ new Map()
390
+ };
391
+ for (const element of declaration.elements) addElementKind(table, element);
392
+ for (const link of declaration.links) addLinkKind(table, link);
393
+ return {
394
+ name,
395
+ framework: name,
396
+ prefix,
397
+ file: declaration.file ?? {},
398
+ extensions: [],
399
+ elements: table.elements,
400
+ links: table.links,
401
+ elementsByPistarType: table.elementsByType,
402
+ linksByPistarType: table.linksByType
403
+ };
404
+ }
405
+ const ISTAR_LINK_LABELS = {
406
+ "istar.IsALink": "Is-A",
407
+ "istar.ParticipatesInLink": "Participates-In",
408
+ "istar.DependencyLink": "Dependency",
409
+ "istar.AndRefinementLink": "And-Refinement",
410
+ "istar.OrRefinementLink": "Or-Refinement",
411
+ "istar.NeededByLink": "Needed-By",
412
+ "istar.QualificationLink": "Qualification",
413
+ "istar.ContributionLink": "Contribution"
414
+ };
415
+ function istarLinks(kinds, category) {
416
+ return kinds.map((kind) => ({
417
+ kind,
418
+ category,
419
+ label: ISTAR_LINK_LABELS[kind],
420
+ info: LINK_KIND_INFO[kind]
421
+ }));
422
+ }
423
+ /** iStar 2.0's declaration, built from the constants in `metamodel.ts`. */
424
+ const ISTAR_2_0_DECLARATION = {
425
+ name: "istar-2.0",
426
+ prefix: ISTAR_PREFIX,
427
+ elements: [...ACTOR_KINDS.map((kind) => ({
428
+ kind,
429
+ category: "actor",
430
+ size: DEFAULT_ELEMENT_SIZE[kind]
431
+ })), ...NODE_KINDS.map((kind) => ({
432
+ kind,
433
+ category: "node",
434
+ size: DEFAULT_ELEMENT_SIZE[kind],
435
+ info: NODE_KIND_INFO[kind]
436
+ }))],
437
+ links: [
438
+ ...istarLinks(ACTOR_LINK_KINDS, "actor"),
439
+ ...istarLinks(DEPENDENCY_LINK_KINDS, "dependency"),
440
+ ...istarLinks(NODE_LINK_KINDS, "node")
441
+ ],
442
+ file: {
443
+ tool: "pistar.2.1.0",
444
+ istar: "2.0"
445
+ }
446
+ };
447
+ /** iStar 2.0, as the piStar tool implements it. The default metamodel everywhere. */
448
+ const ISTAR_2_0 = defineMetamodel(ISTAR_2_0_DECLARATION);
212
449
  /**
213
- * Returns a new metamodel with the extension's kinds added. `base` is not modified.
450
+ * Returns a new metamodel with the extension's kinds added. `base` is not modified. The result
451
+ * keeps `base`'s `framework`, `prefix` and `file`.
214
452
  *
215
- * Throws `MetamodelError` when a kind name isn't namespaced, uses the reserved `istar.`
216
- * prefix, collides with an existing kind or piStar type, or refers to an unknown kind.
453
+ * Throws `MetamodelError` when a kind name isn't namespaced, uses the root's reserved prefix
454
+ * (`istar.` for iStar 2.0), collides with an existing kind or piStar type, or refers to an
455
+ * unknown kind.
217
456
  */
218
457
  function extendMetamodel(base, extension) {
219
- const where = `extension "${extension.name}"`;
220
458
  if (!extension.name || typeof extension.name !== "string") throw new MetamodelError("an extension needs a name");
459
+ const where = `extension "${extension.name}"`;
221
460
  if (base.extensions.includes(extension.name)) throw new MetamodelError(`${where} is already applied`);
222
- const elements = new Map(base.elements);
223
- const links = new Map(base.links);
224
- const elementsByType = new Map(base.elementsByPistarType);
225
- const linksByType = new Map(base.linksByPistarType);
226
- const claimName = (kind, what) => {
227
- 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"`);
228
- 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`);
229
- if (elements.has(kind) || links.has(kind)) throw new MetamodelError(`${where}: kind "${kind}" already exists`);
230
- };
231
- const claimType = (type, kind) => {
232
- if (typeof type !== "string" || type === "") throw new MetamodelError(`${where}: kind "${kind}" has an empty pistarType`);
233
- 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`);
461
+ const table = {
462
+ where,
463
+ namespace: extension.name,
464
+ reserved: base.prefix,
465
+ extension: extension.name,
466
+ elements: new Map(base.elements),
467
+ links: new Map(base.links),
468
+ elementsByType: new Map(base.elementsByPistarType),
469
+ linksByType: new Map(base.linksByPistarType)
234
470
  };
235
- for (const declaration of extension.elements ?? []) {
236
- const { kind } = declaration;
237
- claimName(kind, "element");
238
- let parent;
239
- if (declaration.behavesLike !== void 0) {
240
- parent = elements.get(declaration.behavesLike);
241
- if (!parent) throw new MetamodelError(`${where}: "${kind}" behaves like unknown element kind "${declaration.behavesLike}"`);
242
- }
243
- const category = declaration.category ?? parent?.category;
244
- if (category !== "actor" && category !== "node") throw new MetamodelError(`${where}: element kind "${kind}" needs a category ("node" or "actor") or behavesLike`);
245
- if (parent && parent.category !== category) throw new MetamodelError(`${where}: "${kind}" is a ${category} but behaves like "${parent.kind}", a ${parent.category}`);
246
- const type = declaration.pistarType ?? kind;
247
- claimType(type, kind);
248
- const baseInfo = parent?.info ?? {
249
- canBeInnerElement: true,
250
- canBeDependum: true,
251
- canBeOnPaper: false
252
- };
253
- const definition = {
254
- kind,
255
- category,
256
- label: declaration.label ?? shortName(kind),
257
- size: declaration.size ?? parent?.size ?? (category === "actor" ? {
258
- width: 200,
259
- height: 120
260
- } : {
261
- width: 90,
262
- height: 55
263
- }),
264
- ...category === "node" ? { info: {
265
- ...baseInfo,
266
- ...declaration.info
267
- } } : {},
268
- ...parent ? { behavesLike: parent.kind } : {},
269
- pistarType: type,
270
- extension: extension.name,
271
- ...declaration.shape ? { shape: declaration.shape } : {},
272
- ...declaration.textBox ? { textBox: declaration.textBox } : {}
273
- };
274
- elements.set(kind, definition);
275
- elementsByType.set(type, kind);
276
- }
277
- for (const declaration of extension.links ?? []) {
278
- const { kind } = declaration;
279
- claimName(kind, "link");
280
- if (declaration.behavesLike === void 0 === (declaration.rules === void 0)) throw new MetamodelError(`${where}: link kind "${kind}" needs exactly one of behavesLike or rules`);
281
- let parent;
282
- if (declaration.behavesLike !== void 0) {
283
- parent = links.get(declaration.behavesLike);
284
- if (!parent) throw new MetamodelError(`${where}: "${kind}" behaves like unknown link kind "${declaration.behavesLike}"`);
285
- }
286
- const category = declaration.category ?? parent?.category ?? "node";
287
- if (category !== "node" && category !== "actor" && category !== "dependency") throw new MetamodelError(`${where}: link kind "${kind}" has an unknown category "${category}"`);
288
- if (parent && parent.category !== category) throw new MetamodelError(`${where}: "${kind}" is a ${category} link but behaves like "${parent.kind}", a ${parent.category} link`);
289
- if (category === "dependency" && !parent) throw new MetamodelError(`${where}: dependency link kind "${kind}" must behave like a dependency link (its dependum makes declarative rules ambiguous)`);
290
- if (declaration.rules) for (const [side, selectors] of [["sources", declaration.rules.sources], ["targets", declaration.rules.targets]]) {
291
- if (!Array.isArray(selectors) || selectors.length === 0) throw new MetamodelError(`${where}: link kind "${kind}" needs at least one of ${side}`);
292
- 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}"`);
293
- }
294
- const type = declaration.pistarType ?? kind;
295
- claimType(type, kind);
296
- const definition = {
297
- kind,
298
- category,
299
- label: declaration.label ?? shortName(kind),
300
- info: declaration.info ?? parent?.info ?? {},
301
- ...parent ? { behavesLike: parent.kind } : {},
302
- ...declaration.rules ? { rules: declaration.rules } : {},
303
- ...declaration.check ? { check: declaration.check } : {},
304
- pistarType: type,
305
- extension: extension.name,
306
- ...declaration.line ? { line: declaration.line } : {}
307
- };
308
- links.set(kind, definition);
309
- linksByType.set(type, kind);
310
- }
471
+ for (const element of extension.elements ?? []) addElementKind(table, element);
472
+ for (const link of extension.links ?? []) addLinkKind(table, link);
311
473
  return {
312
474
  name: `${base.name}+${extension.name}`,
475
+ framework: base.framework,
476
+ prefix: base.prefix,
477
+ file: base.file,
313
478
  extensions: [...base.extensions, extension.name],
314
- elements,
315
- links,
316
- elementsByPistarType: elementsByType,
317
- linksByPistarType: linksByType
479
+ elements: table.elements,
480
+ links: table.links,
481
+ elementsByPistarType: table.elementsByType,
482
+ linksByPistarType: table.linksByType
318
483
  };
319
484
  }
320
485
  function elementKindDefinition(metamodel, kind) {
@@ -358,6 +523,21 @@ function effectiveLinkKind(metamodel, kind) {
358
523
  }
359
524
  return current?.kind ?? kind;
360
525
  }
526
+ /** True when link `kind` is `ancestor` or (transitively) behaves like it. */
527
+ function linkBehavesLike(metamodel, kind, ancestor) {
528
+ let current = kind;
529
+ const seen = /* @__PURE__ */ new Set();
530
+ while (current !== void 0 && !seen.has(current)) {
531
+ if (current === ancestor) return true;
532
+ seen.add(current);
533
+ current = metamodel.links.get(current)?.behavesLike;
534
+ }
535
+ return false;
536
+ }
537
+ /** True when a link kind's rules let it target links (`link` or a link kind in `targets`). */
538
+ function targetsLinks(metamodel, kind) {
539
+ return metamodel.links.get(effectiveLinkKind(metamodel, kind))?.rules?.targets.some((selector) => selector === "link" || metamodel.links.has(selector)) ?? false;
540
+ }
361
541
  /** True when `kind` is `ancestor` or (transitively) behaves like it. */
362
542
  function elementBehavesLike(metamodel, kind, ancestor) {
363
543
  let current = kind;
@@ -374,6 +554,23 @@ function elementBehavesLike(metamodel, kind, ancestor) {
374
554
  /** Tool identifier written by piStar 2.1.0, whose file format this library emulates. */
375
555
  const PISTAR_TOOL = "pistar.2.1.0";
376
556
  const ISTAR_VERSION = "2.0";
557
+ /**
558
+ * DSM3-goals' file conventions, shared by its languages (KAOS, NFR; see `@istar-ts/dsm3-kaos`
559
+ * and `@istar-ts/dsm3-nfr`): the `tool` its editors write, and the order of their top-level keys
560
+ * (`fileManager.js`). Use them as a root's `file.tool` and `file.keys`.
561
+ */
562
+ const DSM3_TOOL = "dsm3-goals";
563
+ const DSM3_FILE_KEYS = [
564
+ "actors",
565
+ "orphans",
566
+ "dependencies",
567
+ "links",
568
+ "display",
569
+ "tool",
570
+ "metamodelVersion",
571
+ "saveDate",
572
+ "diagram"
573
+ ];
377
574
  const DEFAULT_DIAGRAM = {
378
575
  width: 2e3,
379
576
  height: 1300
@@ -418,12 +615,14 @@ function inheritMetamodel(from, to) {
418
615
  return to;
419
616
  }
420
617
  function createEmptyModel(diagram = DEFAULT_DIAGRAM, options = {}) {
618
+ const { file } = options.metamodel ?? ISTAR_2_0;
421
619
  const model = {
422
620
  elements: /* @__PURE__ */ new Map(),
423
621
  links: /* @__PURE__ */ new Map(),
424
622
  diagram,
425
- tool: PISTAR_TOOL,
426
- istar: "2.0"
623
+ tool: file.tool ?? "pistar.2.1.0",
624
+ ...file.istar !== void 0 ? { istar: file.istar } : {},
625
+ ...file.extra !== void 0 ? { extra: { ...file.extra } } : {}
427
626
  };
428
627
  return options.metamodel ? withMetamodel(model, options.metamodel) : model;
429
628
  }
@@ -462,7 +661,10 @@ function childrenOf(model, actorId) {
462
661
  for (const element of model.elements.values()) if (element.parent === actorId) result.push(element);
463
662
  return result;
464
663
  }
465
- /** Links that have the element as source or target. */
664
+ /**
665
+ * Links that have the element as source or target. Given a link id, the links that target that
666
+ * link (see `LinkRules`).
667
+ */
466
668
  function linksOf(model, elementId) {
467
669
  const result = [];
468
670
  for (const link of model.links.values()) if (link.source === elementId || link.target === elementId) result.push(link);
@@ -483,6 +685,63 @@ function dependencyLinksOf(model, dependumId) {
483
685
  outbound
484
686
  };
485
687
  }
688
+ /** What a link points at: an element, or a link for kinds whose rules target links. */
689
+ function linkTargetOf(model, link) {
690
+ return model.elements.get(link.target) ?? model.links.get(link.target);
691
+ }
692
+ /** True when `element` is the hub of an n-ary link. */
693
+ function isHubElement(metamodel, element) {
694
+ return element !== void 0 && metamodel.elements.get(element.kind)?.role === "hub";
695
+ }
696
+ /** The n-ary link whose hub is `hubId`; `undefined` when it isn't a hub. */
697
+ function hubLinkOf(model, hubId) {
698
+ const metamodel = metamodelOf(model);
699
+ const hub = model.elements.get(hubId);
700
+ if (!hub || !isHubElement(metamodel, hub)) return void 0;
701
+ const legs = [];
702
+ let end;
703
+ for (const link of model.links.values()) {
704
+ const role = metamodel.links.get(link.kind)?.role;
705
+ if (role === "leg" && link.target === hubId) legs.push(link);
706
+ else if (role === "end" && link.source === hubId) end ??= link;
707
+ }
708
+ return {
709
+ hub,
710
+ legs,
711
+ ...end ? { end } : {}
712
+ };
713
+ }
714
+ /** The hub a leg or end link belongs to. */
715
+ function hubOfLink(model, linkId) {
716
+ const metamodel = metamodelOf(model);
717
+ const link = model.links.get(linkId);
718
+ const role = link && metamodel.links.get(link.kind)?.role;
719
+ if (!link || !role) return void 0;
720
+ const hub = model.elements.get(role === "leg" ? link.target : link.source);
721
+ return isHubElement(metamodel, hub) ? hub : void 0;
722
+ }
723
+ /** The centre of an element's box: its display size, else its kind's default size. */
724
+ function elementCenter(model, element) {
725
+ const size = metamodelOf(model).elements.get(element.kind)?.size;
726
+ const width = element.display?.width ?? size?.width ?? 0;
727
+ const height = element.display?.height ?? size?.height ?? 0;
728
+ return {
729
+ x: element.x + width / 2,
730
+ y: element.y + height / 2
731
+ };
732
+ }
733
+ /**
734
+ * Where a new hub goes: centred between the source and the target, as DSM3-goals places it.
735
+ * Returns its top-left corner for a hub of `size`.
736
+ */
737
+ function defaultHubPosition(model, source, target, size) {
738
+ const a = elementCenter(model, source);
739
+ const b = elementCenter(model, target);
740
+ return {
741
+ x: (a.x + b.x) / 2 - size.width / 2,
742
+ y: (a.y + b.y) / 2 - size.height / 2
743
+ };
744
+ }
486
745
  //#endregion
487
746
  //#region src/constraints.ts
488
747
  const OK = { ok: true };
@@ -494,6 +753,14 @@ function fail$1(code, reason) {
494
753
  };
495
754
  }
496
755
  /**
756
+ * True for a link, as opposed to an element: the target of a link kind whose rules target links
757
+ * (see `LinkRules`) may be either.
758
+ */
759
+ function isLinkRef(value) {
760
+ const candidate = value;
761
+ return typeof candidate.source === "string" && typeof candidate.target === "string";
762
+ }
763
+ /**
497
764
  * Checks whether a link of `kind` may be added from `source` to `target` in `model`.
498
765
  *
499
766
  * For dependency links, `source` is the depender and `target` the dependee (an actor or one of
@@ -501,16 +768,22 @@ function fail$1(code, reason) {
501
768
  *
502
769
  * Extended kinds (see `extendMetamodel`) are checked with the rules of the kind they behave
503
770
  * like, or with their declarative rules, then with their extra predicate.
771
+ *
772
+ * `target` may be a link (or a link id) for kinds whose rules target links (`LinkRules`).
773
+ *
774
+ * For an n-ary kind (`arity: 'n'`), a `target` that is one of its hubs is checked as a new leg
775
+ * of that hub; any other target, as a new link from `source` to `target`.
504
776
  */
505
777
  function canLink(model, source, target, kind, options = {}) {
506
778
  const any = model;
507
779
  const metamodel = options.metamodel ?? metamodelOf(model);
508
780
  const s = resolve(any, source);
509
- const t = resolve(any, target);
781
+ const t = resolve(any, target) ?? any.links.get(refId(target));
510
782
  if (!s) return fail$1("unknown-element", `unknown source element "${refId(source)}"`);
511
783
  if (!t) return fail$1("unknown-element", `unknown target element "${refId(target)}"`);
512
- const definition = metamodel.links.get(kind);
784
+ let definition = metamodel.links.get(kind);
513
785
  if (!definition) return fail$1("unknown-link-kind", `unknown link kind "${String(kind)}"`);
786
+ if (definition.parts && !isLinkRef(t) && metamodel.elements.get(t.kind)?.of === definition.kind) definition = metamodel.links.get(definition.parts.leg) ?? definition;
514
787
  return checkWith(new Graph(any, metamodel), definition, s, t);
515
788
  }
516
789
  /** Runs the rule a link kind resolves to, then every extra predicate along its chain. */
@@ -521,9 +794,14 @@ function checkWith(graph, definition, source, target) {
521
794
  const builtIn = CHECKS[root.kind];
522
795
  let result;
523
796
  if (root.rules) result = declarativeCheck(graph, definition, root.rules, root.kind, source, target);
797
+ else if (isLinkRef(target)) result = elementTargetOnly(definition);
524
798
  else if (builtIn) result = builtIn(graph, source, target);
525
799
  else result = fail$1("unknown-link-kind", `link kind "${definition.kind}" has no rules`);
526
800
  if (!result.ok) return result;
801
+ if (definition.role !== void 0 && !isLinkRef(target)) {
802
+ const hubResult = hubCheck(graph, definition, source, target);
803
+ if (!hubResult.ok) return hubResult;
804
+ }
527
805
  const context = graph.context(definition.kind, source, target);
528
806
  for (const step of chain.toReversed()) {
529
807
  if (!step.check) continue;
@@ -532,6 +810,29 @@ function checkWith(graph, definition, source, target) {
532
810
  }
533
811
  return OK;
534
812
  }
813
+ /**
814
+ * What a leg or end must also respect: an n-ary link has one end, and the base kind's same-actor
815
+ * rule holds between each leg's source and the end's target (the hub belongs to no actor).
816
+ */
817
+ function hubCheck(graph, definition, source, target) {
818
+ const base = definition.of === void 0 ? void 0 : graph.metamodel.links.get(definition.of);
819
+ if (!base) return OK;
820
+ const roleOf = (link) => graph.metamodel.links.get(link.kind)?.role;
821
+ if (definition.role === "end") {
822
+ 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`);
823
+ return OK;
824
+ }
825
+ if (base.rules?.sameActor === false) return OK;
826
+ for (const link of graph.model.links.values()) {
827
+ if (link.source !== target.id || roleOf(link) !== "end") continue;
828
+ 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`);
829
+ }
830
+ return OK;
831
+ }
832
+ /** The built-in (iStar 2.0) rules only know element targets. */
833
+ function elementTargetOnly(definition) {
834
+ 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`);
835
+ }
535
836
  function refId(ref) {
536
837
  return typeof ref === "string" ? ref : ref.id;
537
838
  }
@@ -683,12 +984,18 @@ function describe(graph, selectors) {
683
984
  if (selector === "*") return "any element";
684
985
  if (selector === "node") return "an intentional element";
685
986
  if (selector === "actor") return "an actor";
987
+ if (selector === "link") return "a link";
988
+ const linkLabel = graph.metamodel.links.get(selector)?.label;
989
+ if (linkLabel !== void 0) return /^[aeiou]/i.test(linkLabel) ? `an ${linkLabel} link` : `a ${linkLabel} link`;
686
990
  const label = graph.metamodel.elements.get(selector)?.label ?? selector;
687
991
  return /^[aeiou]/i.test(label) ? `an ${label}` : `a ${label}`;
688
992
  });
689
993
  return names.length <= 2 ? names.join(" or ") : `${names.slice(0, -1).join(", ")} or ${names[names.length - 1]}`;
690
994
  }
691
995
  function matchesSelector(graph, element, selector) {
996
+ if (isLinkRef(element)) return selector === "link" || linkBehavesLike(graph.metamodel, element.kind, selector);
997
+ if (selector === "link") return false;
998
+ if (graph.metamodel.elements.get(element.kind)?.role === "hub") return element.kind === selector;
692
999
  if (selector === "*") return true;
693
1000
  const category = graph.metamodel.elements.get(element.kind)?.category;
694
1001
  if (selector === "node" || selector === "actor") return category === selector;
@@ -697,15 +1004,21 @@ function matchesSelector(graph, element, selector) {
697
1004
  /**
698
1005
  * Declarative rules of an extension link kind, checked in the order of the iStar 2.0 node-link
699
1006
  * rules (and of piStar-ext's generated `isValid`): source kind, target kind, self link,
700
- * dependum, same actor, duplicates.
1007
+ * dependum, same actor, duplicates. A link target skips the self, dependum and same-actor rules,
1008
+ * which are about elements.
701
1009
  */
702
1010
  function declarativeCheck(graph, definition, rules, rulesKind, source, target) {
703
1011
  const name = definition.label;
704
1012
  const article = /^[aeiou]/i.test(name) ? "an" : "a";
705
1013
  if (!rules.sources.some((selector) => matchesSelector(graph, source, selector))) return fail$1("invalid-source", `the source of ${article} ${name} link must be ${describe(graph, rules.sources)}`);
706
- if (!rules.targets.some((selector) => matchesSelector(graph, target, selector))) return fail$1("invalid-target", `the target of ${article} ${name} link must be ${describe(graph, rules.targets)}`);
1014
+ if (!rules.targets.some((selector) => matchesSelector(graph, target, selector))) {
1015
+ const targetKind = isLinkRef(target) ? void 0 : graph.metamodel.elements.get(target.kind);
1016
+ const hubOf = targetKind?.role === "hub" && targetKind.of !== void 0 ? graph.metamodel.links.get(targetKind.of) : void 0;
1017
+ 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`);
1018
+ return fail$1("invalid-target", `the target of ${article} ${name} link must be ${describe(graph, rules.targets)}`);
1019
+ }
707
1020
  if (!rules.allowSelf && source.id === target.id) return fail$1("self-link", `you cannot make ${article} ${name} link from an element onto itself`);
708
- if (definition.category === "node") {
1021
+ if (definition.category === "node" && !isLinkRef(target)) {
709
1022
  if (!rules.allowDependum && (isDependum(source) || isDependum(target))) return fail$1("dependum-endpoint", `you cannot make ${article} ${name} link with a dependum`);
710
1023
  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)`);
711
1024
  }
@@ -756,9 +1069,10 @@ function validateModel(model, options) {
756
1069
  if (inbound) accepted.set(inbound.id, inbound);
757
1070
  if (outbound) accepted.set(outbound.id, outbound);
758
1071
  }
759
- for (const link of any.links.values()) {
760
- if (isDependency(link)) continue;
761
- 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));
1072
+ const rest = [...any.links.values()].filter((link) => !isDependency(link));
1073
+ const targetsLink = (link) => !any.elements.has(link.target) && any.links.has(link.target);
1074
+ for (const link of [...rest.filter((l) => !targetsLink(l)), ...rest.filter((l) => targetsLink(l))]) {
1075
+ 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));
762
1076
  else report(link.id, link.kind, link.source, link.target, {
763
1077
  ok: false,
764
1078
  code: "unknown-element",
@@ -766,6 +1080,24 @@ function validateModel(model, options) {
766
1080
  });
767
1081
  accepted.set(link.id, link);
768
1082
  }
1083
+ for (const hub of any.elements.values()) {
1084
+ const hubKind = metamodel.elements.get(hub.kind);
1085
+ if (hubKind?.role !== "hub" || hubKind.of === void 0) continue;
1086
+ const base = metamodel.links.get(hubKind.of);
1087
+ const roleOf = (link) => metamodel.links.get(link.kind)?.role;
1088
+ const legs = rest.filter((l) => l.target === hub.id && roleOf(l) === "leg");
1089
+ const end = rest.find((l) => l.source === hub.id && roleOf(l) === "end");
1090
+ if (legs.length > 0 && end) continue;
1091
+ const label = base?.label ?? hubKind.of;
1092
+ issues.push({
1093
+ linkId: hub.id,
1094
+ kind: hubKind.of,
1095
+ source: legs[0]?.source ?? hub.id,
1096
+ target: end?.target ?? hub.id,
1097
+ code: "incomplete-hub",
1098
+ message: end ? `the hub of a ${label} link needs at least one leg` : `the hub of a ${label} link needs an end`
1099
+ });
1100
+ }
769
1101
  return issues;
770
1102
  }
771
1103
  //#endregion
@@ -890,6 +1222,11 @@ function fileMetamodelExtension(block) {
890
1222
  }
891
1223
  //#endregion
892
1224
  //#region src/serialization.ts
1225
+ /**
1226
+ * Key of a hub element's `extra` that holds its entry's unknown keys (those beside `hub`,
1227
+ * `starts` and `end`), so they are written back on the entry.
1228
+ */
1229
+ const HUB_ENTRY_KEY = "__hubEntry";
893
1230
  var PistarParseError = class extends Error {
894
1231
  name = "PistarParseError";
895
1232
  };
@@ -934,6 +1271,18 @@ const LINK_KEYS = /* @__PURE__ */ new Set([
934
1271
  "customProperties",
935
1272
  "label"
936
1273
  ]);
1274
+ const HUB_KEYS = /* @__PURE__ */ new Set([
1275
+ "id",
1276
+ "type",
1277
+ "x",
1278
+ "y",
1279
+ "customProperties"
1280
+ ]);
1281
+ const HUB_ENTRY_KEYS = /* @__PURE__ */ new Set([
1282
+ "hub",
1283
+ "starts",
1284
+ "end"
1285
+ ]);
937
1286
  const DIAGRAM_KEYS = /* @__PURE__ */ new Set([
938
1287
  "width",
939
1288
  "height",
@@ -1035,15 +1384,15 @@ function parsePistar(input, options = {}) {
1035
1384
  ...dependency
1036
1385
  });
1037
1386
  });
1038
- asArray(json.links, "links").forEach((raw, i) => {
1039
- if (!isRecord(raw)) throw new PistarParseError(`links[${i}]: expected an object`);
1387
+ const readLink = (raw, where) => {
1388
+ if (!isRecord(raw)) throw new PistarParseError(`${where}: expected an object`);
1040
1389
  const { id, type, source, target } = raw;
1041
- if (typeof id !== "string" || id === "") throw new PistarParseError(`links[${i}]: missing "id"`);
1390
+ if (typeof id !== "string" || id === "") throw new PistarParseError(`${where}: missing "id"`);
1042
1391
  const kind = typeof type === "string" ? metamodel.linksByPistarType.get(type) : void 0;
1043
- if (kind === void 0) throw new PistarParseError(`links[${i}] (${id}): unknown link type ${JSON.stringify(type)}`);
1044
- if (typeof source !== "string" || typeof target !== "string") throw new PistarParseError(`links[${i}] (${id}): missing "source" or "target"`);
1045
- if (elements.has(id) || links.has(id)) throw new PistarParseError(`links[${i}]: duplicated id "${id}"`);
1046
- links.set(id, {
1392
+ if (kind === void 0) throw new PistarParseError(`${where} (${id}): unknown link type ${JSON.stringify(type)}`);
1393
+ if (typeof source !== "string" || typeof target !== "string") throw new PistarParseError(`${where} (${id}): missing "source" or "target"`);
1394
+ if (elements.has(id) || links.has(id)) throw new PistarParseError(`${where}: duplicated id "${id}"`);
1395
+ const link = {
1047
1396
  id,
1048
1397
  kind,
1049
1398
  source,
@@ -1053,7 +1402,38 @@ function parsePistar(input, options = {}) {
1053
1402
  ...optional("label", typeof raw.label === "string" ? raw.label : void 0),
1054
1403
  ...optional("display", displayFor(id)),
1055
1404
  ...optional("extra", extraOf(raw, LINK_KEYS))
1405
+ };
1406
+ links.set(id, link);
1407
+ return link;
1408
+ };
1409
+ const readHubEntry = (raw, where) => {
1410
+ const hub = readElement(raw.hub, `${where}.hub`, HUB_KEYS, {});
1411
+ const hubKind = metamodel.elements.get(hub.kind);
1412
+ if (hubKind?.role !== "hub") throw new PistarParseError(`${where}.hub (${hub.id}): ${hub.kind} is not a hub kind`);
1413
+ const entryExtra = extraOf(raw, HUB_ENTRY_KEYS);
1414
+ if (entryExtra) elements.set(hub.id, {
1415
+ ...hub,
1416
+ extra: {
1417
+ ...hub.extra,
1418
+ [HUB_ENTRY_KEY]: entryExtra
1419
+ }
1056
1420
  });
1421
+ const roleOf = (link) => {
1422
+ const definition = metamodel.links.get(link.kind);
1423
+ return definition?.of === hubKind.of ? definition?.role : void 0;
1424
+ };
1425
+ asArray(raw.starts, `${where}.starts`).forEach((rawLeg, j) => {
1426
+ const leg = readLink(rawLeg, `${where}.starts[${j}]`);
1427
+ if (roleOf(leg) !== "leg" || leg.target !== hub.id) throw new PistarParseError(`${where}.starts[${j}] (${leg.id}): not a leg of the hub "${hub.id}"`);
1428
+ });
1429
+ if (raw.end !== void 0) {
1430
+ const end = readLink(raw.end, `${where}.end`);
1431
+ if (roleOf(end) !== "end" || end.source !== hub.id) throw new PistarParseError(`${where}.end (${end.id}): not the end of the hub "${hub.id}"`);
1432
+ }
1433
+ };
1434
+ asArray(json.links, "links").forEach((raw, i) => {
1435
+ if (isRecord(raw) && isRecord(raw.hub)) readHubEntry(raw, `links[${i}]`);
1436
+ else readLink(raw, `links[${i}]`);
1057
1437
  });
1058
1438
  let extraDisplay;
1059
1439
  for (const [id, value] of Object.entries(display)) if (!usedDisplay.has(id)) (extraDisplay ??= {})[id] = value;
@@ -1085,6 +1465,64 @@ function parsePistar(input, options = {}) {
1085
1465
  setHostMetamodel(model, host);
1086
1466
  return withMetamodel(model, metamodel);
1087
1467
  }
1468
+ function parseJson(json) {
1469
+ if (typeof json !== "string") return json;
1470
+ let value;
1471
+ try {
1472
+ value = JSON.parse(json);
1473
+ } catch (error) {
1474
+ throw new PistarParseError(`invalid JSON: ${error.message}`, { cause: error });
1475
+ }
1476
+ if (!isRecord(value)) throw new PistarParseError("a piStar model must be a JSON object");
1477
+ return value;
1478
+ }
1479
+ /** `kaos` for an object whose `type` is `kaos.Softgoal`. */
1480
+ function prefixOf(value) {
1481
+ const type = isRecord(value) ? value.type : void 0;
1482
+ const dot = typeof type === "string" ? type.indexOf(".") : -1;
1483
+ return dot > 0 ? type.slice(0, dot) : void 0;
1484
+ }
1485
+ function arrayOf(value) {
1486
+ return Array.isArray(value) ? value : [];
1487
+ }
1488
+ /**
1489
+ * The namespace of the first `type` in a file (`kaos` for `kaos.Softgoal`), looking at actors and
1490
+ * their nodes, orphans, dependencies, then links (hub entries included). `undefined` when no
1491
+ * element or link has a namespaced type. Throws `PistarParseError` for a string that isn't JSON.
1492
+ */
1493
+ function detectPrefix(json) {
1494
+ const file = parseJson(json);
1495
+ const candidates = function* () {
1496
+ for (const actor of arrayOf(file.actors)) {
1497
+ yield actor;
1498
+ if (isRecord(actor)) yield* arrayOf(actor.nodes);
1499
+ }
1500
+ yield* arrayOf(file.orphans);
1501
+ yield* arrayOf(file.dependencies);
1502
+ for (const link of arrayOf(file.links)) if (isRecord(link) && isRecord(link.hub)) {
1503
+ yield link.hub;
1504
+ yield* arrayOf(link.starts);
1505
+ yield link.end;
1506
+ } else yield link;
1507
+ };
1508
+ for (const candidate of candidates()) {
1509
+ const prefix = prefixOf(candidate);
1510
+ if (prefix !== void 0) return prefix;
1511
+ }
1512
+ }
1513
+ /**
1514
+ * Which of `roots` a file is written in: the one whose `prefix` is the file's (`detectPrefix`);
1515
+ * for a file without types, the only one whose `file.tool` is the file's `tool`; otherwise
1516
+ * `undefined`. Pass the result to `parsePistar(json, { metamodel })`.
1517
+ */
1518
+ function detectFramework(json, roots) {
1519
+ const file = parseJson(json);
1520
+ const prefix = detectPrefix(file);
1521
+ if (prefix !== void 0) return roots.find((root) => root.prefix === prefix);
1522
+ if (typeof file.tool !== "string") return void 0;
1523
+ const byTool = roots.filter((root) => root.file.tool === file.tool);
1524
+ return byTool.length === 1 ? byTool[0] : void 0;
1525
+ }
1088
1526
  function optional(key, value) {
1089
1527
  return value === void 0 ? {} : { [key]: value };
1090
1528
  }
@@ -1110,37 +1548,43 @@ function toPistarObject(input, options = {}) {
1110
1548
  const model = input;
1111
1549
  const metamodel = options.metamodel ?? metamodelOf(input);
1112
1550
  const isActorKind = (kind) => metamodel.elements.get(kind)?.category === "actor";
1551
+ const isHub = (element) => metamodel.elements.get(element.kind)?.role === "hub";
1113
1552
  const actors = [];
1114
1553
  const orphans = [];
1115
1554
  const dependencies = [];
1116
1555
  const children = /* @__PURE__ */ new Map();
1117
- for (const element of model.elements.values()) if (element.parent !== void 0 && !isActorKind(element.kind)) {
1118
- let list = children.get(element.parent);
1119
- if (!list) children.set(element.parent, list = []);
1120
- list.push({
1556
+ for (const element of model.elements.values()) {
1557
+ if (isHub(element)) continue;
1558
+ if (element.parent !== void 0 && !isActorKind(element.kind)) {
1559
+ let list = children.get(element.parent);
1560
+ if (!list) children.set(element.parent, list = []);
1561
+ list.push({
1562
+ ...elementJson(element, metamodel),
1563
+ ...element.extra
1564
+ });
1565
+ }
1566
+ }
1567
+ for (const element of model.elements.values()) {
1568
+ if (isHub(element)) continue;
1569
+ if (isActorKind(element.kind)) actors.push({
1121
1570
  ...elementJson(element, metamodel),
1571
+ nodes: children.get(element.id) ?? [],
1122
1572
  ...element.extra
1123
1573
  });
1124
- }
1125
- for (const element of model.elements.values()) if (isActorKind(element.kind)) actors.push({
1126
- ...elementJson(element, metamodel),
1127
- nodes: children.get(element.id) ?? [],
1128
- ...element.extra
1129
- });
1130
- else if (element.isDependum) {
1131
- const { inbound, outbound } = dependencyLinksOf(model, element.id);
1132
- dependencies.push({
1574
+ else if (element.isDependum) {
1575
+ const { inbound, outbound } = dependencyLinksOf(model, element.id);
1576
+ dependencies.push({
1577
+ ...elementJson(element, metamodel),
1578
+ source: inbound?.source ?? element.dependency?.source ?? "",
1579
+ target: outbound?.target ?? element.dependency?.target ?? "",
1580
+ ...element.extra
1581
+ });
1582
+ } else if (element.parent === void 0 || !model.elements.has(element.parent)) orphans.push({
1133
1583
  ...elementJson(element, metamodel),
1134
- source: inbound?.source ?? element.dependency?.source ?? "",
1135
- target: outbound?.target ?? element.dependency?.target ?? "",
1136
1584
  ...element.extra
1137
1585
  });
1138
- } else if (element.parent === void 0 || !model.elements.has(element.parent)) orphans.push({
1139
- ...elementJson(element, metamodel),
1140
- ...element.extra
1141
- });
1142
- const links = [];
1143
- for (const link of model.links.values()) links.push({
1586
+ }
1587
+ const linkJson = (link) => ({
1144
1588
  id: link.id,
1145
1589
  type: pistarTypeOf(metamodel.links.get(link.kind)?.pistarType, link.kind, metamodel),
1146
1590
  source: link.source,
@@ -1150,6 +1594,54 @@ function toPistarObject(input, options = {}) {
1150
1594
  ...link.label !== void 0 ? { label: link.label } : {},
1151
1595
  ...link.extra
1152
1596
  });
1597
+ const hubs = /* @__PURE__ */ new Map();
1598
+ const partsOf = (hubId) => {
1599
+ let parts = hubs.get(hubId);
1600
+ if (!parts) hubs.set(hubId, parts = { legs: [] });
1601
+ return parts;
1602
+ };
1603
+ const hubOf = (link) => {
1604
+ const role = metamodel.links.get(link.kind)?.role;
1605
+ const id = role === "leg" ? link.target : role === "end" ? link.source : void 0;
1606
+ const hub = id === void 0 ? void 0 : model.elements.get(id);
1607
+ return hub && isHub(hub) ? hub.id : void 0;
1608
+ };
1609
+ for (const link of model.links.values()) {
1610
+ const hubId = hubOf(link);
1611
+ if (hubId === void 0) continue;
1612
+ const parts = partsOf(hubId);
1613
+ if (metamodel.links.get(link.kind)?.role === "leg") parts.legs.push(link);
1614
+ else parts.end ??= link;
1615
+ }
1616
+ const hubEntry = (hub) => {
1617
+ const parts = hubs.get(hub.id) ?? { legs: [] };
1618
+ const { [HUB_ENTRY_KEY]: entryExtra, ...hubExtra } = hub.extra ?? {};
1619
+ return {
1620
+ hub: {
1621
+ id: hub.id,
1622
+ type: pistarTypeOf(metamodel.elements.get(hub.kind)?.pistarType, hub.kind, metamodel),
1623
+ x: hub.x,
1624
+ y: hub.y,
1625
+ ...hub.customProperties ? { customProperties: { ...hub.customProperties } } : {},
1626
+ ...hubExtra
1627
+ },
1628
+ starts: parts.legs.map(linkJson),
1629
+ ...parts.end ? { end: linkJson(parts.end) } : {},
1630
+ ...isRecord(entryExtra) ? entryExtra : {}
1631
+ };
1632
+ };
1633
+ const links = [];
1634
+ const written = /* @__PURE__ */ new Set();
1635
+ for (const link of model.links.values()) {
1636
+ const hubId = hubOf(link);
1637
+ const parts = hubId === void 0 ? void 0 : hubs.get(hubId);
1638
+ if (hubId === void 0 || !parts || !parts.legs.includes(link) && parts.end !== link) links.push(linkJson(link));
1639
+ else if (!written.has(hubId)) {
1640
+ written.add(hubId);
1641
+ links.push(hubEntry(model.elements.get(hubId)));
1642
+ }
1643
+ }
1644
+ for (const element of model.elements.values()) if (isHub(element) && !written.has(element.id)) links.push(hubEntry(element));
1153
1645
  const layout = layouts.get(model);
1154
1646
  const display = {};
1155
1647
  const displayOf = (id) => {
@@ -1197,7 +1689,7 @@ function toPistarObject(input, options = {}) {
1197
1689
  if ((Array.isArray(value) ? value.length === 0 : Object.keys(value).length === 0) && !layout.keys.includes(key)) values[key] = void 0;
1198
1690
  }
1199
1691
  const order = [
1200
- ...layout?.keys ?? [],
1692
+ ...layout?.keys ?? metamodel.file.keys ?? [],
1201
1693
  ...TOP_LEVEL_KEYS,
1202
1694
  ...Object.keys(model.extra ?? {})
1203
1695
  ];
@@ -1386,7 +1878,8 @@ function nestElement(model, id, parent) {
1386
1878
  }
1387
1879
  /**
1388
1880
  * Removes elements and everything that depends on them: inner elements of removed actors,
1389
- * links touching removed elements, and the rest of any dependency that loses a part.
1881
+ * links touching removed elements (and links targeting removed links), and the rest of any
1882
+ * dependency that loses a part.
1390
1883
  */
1391
1884
  function removeElements(model, ids) {
1392
1885
  return typed(removeCascade(any(model), new Set(ids), /* @__PURE__ */ new Set()));
@@ -1410,11 +1903,28 @@ function removeCascade(model, elementIds, linkIds) {
1410
1903
  }
1411
1904
  for (const link of model.links.values()) {
1412
1905
  if (linkIds.has(link.id)) continue;
1413
- if (elementIds.has(link.source) || elementIds.has(link.target)) {
1906
+ if (elementIds.has(link.source) || elementIds.has(link.target) || linkIds.has(link.target)) {
1414
1907
  linkIds.add(link.id);
1415
1908
  changed = true;
1416
1909
  }
1417
1910
  }
1911
+ for (const element of model.elements.values()) {
1912
+ if (elementIds.has(element.id) || metamodel.elements.get(element.kind)?.role !== "hub") continue;
1913
+ let legs = 0;
1914
+ let removedLegs = 0;
1915
+ let endRemoved = false;
1916
+ for (const link of model.links.values()) {
1917
+ const role = metamodel.links.get(link.kind)?.role;
1918
+ if (role === "leg" && link.target === element.id) {
1919
+ legs++;
1920
+ if (linkIds.has(link.id)) removedLegs++;
1921
+ } else if (role === "end" && link.source === element.id && linkIds.has(link.id)) endRemoved = true;
1922
+ }
1923
+ if (endRemoved || legs > 0 && removedLegs === legs) {
1924
+ elementIds.add(element.id);
1925
+ changed = true;
1926
+ }
1927
+ }
1418
1928
  for (const link of model.links.values()) {
1419
1929
  if (!linkIds.has(link.id) || !isDependencyHalf(link)) continue;
1420
1930
  for (const end of [link.source, link.target]) if (model.elements.get(end)?.isDependum && !elementIds.has(end)) {
@@ -1433,13 +1943,21 @@ function removeCascade(model, elementIds, linkIds) {
1433
1943
  links
1434
1944
  });
1435
1945
  }
1436
- /** Adds a link between two elements if the iStar constraints allow it. */
1946
+ /**
1947
+ * Adds a link between two elements if the iStar constraints allow it.
1948
+ *
1949
+ * With an n-ary kind (`arity: 'n'`), a `target` that is one of the kind's hubs gets a new leg
1950
+ * from `source` (see `addLeg`). Any other target gets a new hub, centred between source and
1951
+ * target, with a leg from `source` and an end to `target`. `input.id` and `customProperties`
1952
+ * then go to the hub, and `label` and `name` to the end.
1953
+ */
1437
1954
  function connect(model, input, options = {}) {
1438
1955
  const m = any(model);
1439
1956
  const metamodel = metaOf(m);
1440
1957
  const kind = input.kind;
1441
1958
  const definition = metamodel.links.get(kind);
1442
1959
  if (definition?.category === "dependency") throw new ModelOperationError("use addDependency to create dependencies");
1960
+ if (definition?.parts) return connectHub(m, definition, input, options);
1443
1961
  let { source, target } = input;
1444
1962
  let reversed = false;
1445
1963
  if (!options.force) {
@@ -1457,7 +1975,7 @@ function connect(model, input, options = {}) {
1457
1975
  } else {
1458
1976
  if (!definition) throw new ModelOperationError(`unknown link kind "${kind}" in ${metamodel.name}`);
1459
1977
  requireElement(m, source);
1460
- requireElement(m, target);
1978
+ if (!m.links.has(target)) requireElement(m, target);
1461
1979
  }
1462
1980
  const id = input.id ?? (options.createId ?? defaultIdGenerator)();
1463
1981
  if (m.elements.has(id) || m.links.has(id)) throw new ModelOperationError(`id "${id}" is already in use`);
@@ -1477,6 +1995,113 @@ function connect(model, input, options = {}) {
1477
1995
  reversed
1478
1996
  };
1479
1997
  }
1998
+ function connectHub(m, definition, input, options) {
1999
+ const metamodel = metaOf(m);
2000
+ const parts = definition.parts;
2001
+ const { source, target } = input;
2002
+ if (m.elements.get(target)?.kind === parts.hub) return addLeg(typed(m), target, source, options);
2003
+ if (!options.force) {
2004
+ const check = canLink(m, source, target, definition.kind);
2005
+ if (!check.ok) return check;
2006
+ }
2007
+ const from = requireElement(m, source);
2008
+ const to = requireElement(m, target);
2009
+ const createId = options.createId ?? defaultIdGenerator;
2010
+ const hubId = input.id ?? createId();
2011
+ const legId = createId();
2012
+ const endId = createId();
2013
+ for (const [i, id] of [
2014
+ hubId,
2015
+ legId,
2016
+ endId
2017
+ ].entries()) if (m.elements.has(id) || m.links.has(id) || [
2018
+ hubId,
2019
+ legId,
2020
+ endId
2021
+ ].indexOf(id) !== i) throw new ModelOperationError(`id "${id}" is already in use`);
2022
+ const position = defaultHubPosition(m, from, to, metamodel.elements.get(parts.hub)?.size ?? {
2023
+ width: 20,
2024
+ height: 20
2025
+ });
2026
+ const hub = withoutUndefined({
2027
+ id: hubId,
2028
+ kind: parts.hub,
2029
+ name: "",
2030
+ x: position.x,
2031
+ y: position.y,
2032
+ customProperties: input.customProperties
2033
+ });
2034
+ const leg = {
2035
+ id: legId,
2036
+ kind: parts.leg,
2037
+ source,
2038
+ target: hubId
2039
+ };
2040
+ const end = withoutUndefined({
2041
+ id: endId,
2042
+ kind: parts.end,
2043
+ source: hubId,
2044
+ target,
2045
+ label: input.label,
2046
+ name: input.name
2047
+ });
2048
+ return {
2049
+ ok: true,
2050
+ model: typed(derive(m, {
2051
+ elements: new Map(m.elements).set(hubId, hub),
2052
+ links: new Map(m.links).set(legId, leg).set(endId, end)
2053
+ })),
2054
+ link: end,
2055
+ reversed: false,
2056
+ hub: {
2057
+ hub,
2058
+ legs: [leg],
2059
+ end
2060
+ }
2061
+ };
2062
+ }
2063
+ /**
2064
+ * Adds a leg from `source` to the hub `hubId` of an n-ary link (one more source for it), if the
2065
+ * kind's rules allow it. `options.id` is the leg's id.
2066
+ */
2067
+ function addLeg(model, hubId, source, options = {}) {
2068
+ const m = any(model);
2069
+ const metamodel = metaOf(m);
2070
+ const hub = requireElement(m, hubId);
2071
+ const base = metamodel.links.get(metamodel.elements.get(hub.kind)?.of ?? "");
2072
+ if (metamodel.elements.get(hub.kind)?.role !== "hub" || !base?.parts) throw new ModelOperationError(`"${hubId}" is not the hub of an n-ary link`);
2073
+ const kind = base.parts.leg;
2074
+ if (!options.force) {
2075
+ const check = canLink(m, source, hubId, kind);
2076
+ if (!check.ok) return check;
2077
+ } else requireElement(m, source);
2078
+ const id = options.id ?? (options.createId ?? defaultIdGenerator)();
2079
+ if (m.elements.has(id) || m.links.has(id)) throw new ModelOperationError(`id "${id}" is already in use`);
2080
+ const leg = {
2081
+ id,
2082
+ kind,
2083
+ source,
2084
+ target: hubId
2085
+ };
2086
+ const next = derive(m, { links: new Map(m.links).set(id, leg) });
2087
+ return {
2088
+ ok: true,
2089
+ model: typed(next),
2090
+ link: leg,
2091
+ reversed: false,
2092
+ hub: hubLinkOf(typed(next), hubId)
2093
+ };
2094
+ }
2095
+ /**
2096
+ * Removes one leg of an n-ary link. Removing its last leg removes the whole link (hub and end),
2097
+ * as `disconnect` does.
2098
+ */
2099
+ function removeLeg(model, legId) {
2100
+ const m = any(model);
2101
+ const leg = requireLink(m, legId);
2102
+ if (metaOf(m).links.get(leg.kind)?.role !== "leg") throw new ModelOperationError(`"${legId}" is not a leg of an n-ary link`);
2103
+ return disconnect(model, legId);
2104
+ }
1480
2105
  /** Adds depender → dependum → dependee. */
1481
2106
  function addDependency(model, input, options = {}) {
1482
2107
  const m = any(model);
@@ -1529,7 +2154,10 @@ function addDependency(model, input, options = {}) {
1529
2154
  links: [first, second]
1530
2155
  };
1531
2156
  }
1532
- /** Removes a link. Removing either half of a dependency removes the whole dependency. */
2157
+ /**
2158
+ * Removes a link, and the links that target it. Removing either half of a dependency removes
2159
+ * the whole dependency.
2160
+ */
1533
2161
  function disconnect(model, id) {
1534
2162
  const m = any(model);
1535
2163
  requireLink(m, id);
@@ -1664,10 +2292,29 @@ function createModelStore(initial, options = {}) {
1664
2292
  });
1665
2293
  if (result.ok) commit(result.model, {
1666
2294
  type: "connect",
1667
- id: result.link.id
2295
+ id: result.link.id,
2296
+ ...result.hub ? { hubId: result.hub.hub.id } : {}
1668
2297
  });
1669
2298
  return result;
1670
2299
  },
2300
+ addLeg(hubId, source, legOptions) {
2301
+ const result = addLeg(model, hubId, source, {
2302
+ ...legOptions,
2303
+ ...ctx
2304
+ });
2305
+ if (result.ok) commit(result.model, {
2306
+ type: "connect",
2307
+ id: result.link.id,
2308
+ hubId
2309
+ });
2310
+ return result;
2311
+ },
2312
+ removeLeg(id) {
2313
+ commit(removeLeg(model, id), {
2314
+ type: "removeLeg",
2315
+ id
2316
+ });
2317
+ },
1671
2318
  addDependency(input, dependencyOptions) {
1672
2319
  const result = addDependency(model, input, {
1673
2320
  ...dependencyOptions,
@@ -1896,7 +2543,7 @@ const prop = {
1896
2543
  function isCustomPropertiesRecord(source) {
1897
2544
  if ("customProperties" in source) return false;
1898
2545
  const owner = source;
1899
- return !(typeof owner.kind === "string" && owner.kind.startsWith("istar.") && (typeof owner.x === "number" || owner.source !== void 0 && owner.target !== void 0));
2546
+ return !(typeof owner.kind === "string" && owner.kind.includes(".") && (typeof owner.x === "number" || typeof owner.source === "string" && typeof owner.target === "string"));
1900
2547
  }
1901
2548
  function resolveCustomProperties(source) {
1902
2549
  if (source === void 0) return void 0;
@@ -2026,6 +2673,176 @@ function validateModelProperties(model, schemas) {
2026
2673
  return issues;
2027
2674
  }
2028
2675
  //#endregion
2029
- export { ACTOR_KINDS, ACTOR_LINK_KINDS, CONTRIBUTION_LABELS, DEFAULT_DIAGRAM, DEFAULT_ELEMENT_SIZE, DEPENDENCY_LINK_KINDS, ELEMENT_KINDS, FILE_METAMODEL_KEY, ISTAR_2_0, ISTAR_PREFIX, ISTAR_VERSION, LINK_KINDS, LINK_KIND_INFO, METAMODEL_VERSION, MetamodelError, ModelOperationError, NODE_KINDS, NODE_KIND_INFO, NODE_LINK_KINDS, PISTAR_TOOL, PistarParseError, PistarWriteError, addDependency, addElement, boolean, canLink, childrenOf, connect, createEmptyModel, createModelStore, defaultIdGenerator, defineMetamodelExtension, defineProperties, dependencyEnds, dependencyLinksOf, disconnect, effectiveElementKind, effectiveLinkKind, elementBehavesLike, elementKindDefinition, enumOf, extendMetamodel, fileMetamodelExtension, fileMetamodelOf, getSourceLayout, hostMetamodelOf, inheritMetamodel, inheritSourceLayout, isActor, isActorIn, isActorKind, isActorKindIn, isActorLinkKind, isContributionLabel, isDependencyLinkKind, isElementKind, isLinkKind, isNode, isNodeIn, isNodeKind, isNodeKindIn, isNodeLinkKind, linkKindDefinition, linksOf, metamodelOf, moveElement, nestElement, number, parsePistar, prop, removeElement, removeElements, setHostMetamodel, shortKindName, string, toPistar, toPistarObject, updateDiagram, updateElement, updateLink, validateFileMetamodel, validateModel, validateModelProperties, withFileMetamodel, withMetamodel };
2676
+ //#region src/diagnostics.ts
2677
+ /** Most severe first. */
2678
+ const DIAGNOSTIC_SEVERITIES = [
2679
+ "error",
2680
+ "warning",
2681
+ "info",
2682
+ "hint"
2683
+ ];
2684
+ const RANK = {
2685
+ error: 0,
2686
+ warning: 1,
2687
+ info: 2,
2688
+ hint: 3
2689
+ };
2690
+ /** Negative when `a` is more severe than `b`. */
2691
+ function compareSeverity(a, b) {
2692
+ return RANK[a] - RANK[b];
2693
+ }
2694
+ /** The most severe severity in a list (what a badge shows), or `undefined` if it is empty. */
2695
+ function worstDiagnosticSeverity(diagnostics) {
2696
+ let worst;
2697
+ for (const d of diagnostics ?? []) {
2698
+ if (worst === void 0 || RANK[d.severity] < RANK[worst]) worst = d.severity;
2699
+ if (worst === "error") break;
2700
+ }
2701
+ return worst;
2702
+ }
2703
+ function identity(d) {
2704
+ return `${d.elementId}\u0000${d.key ?? ""}\u0000${d.message}`;
2705
+ }
2706
+ /**
2707
+ * The union of several lists, in order, without duplicates: diagnostics with the same
2708
+ * `(elementId, key, message)` count once, keeping the first one's position and the most severe
2709
+ * severity among them.
2710
+ */
2711
+ function mergeDiagnostics(lists) {
2712
+ const result = [];
2713
+ const index = /* @__PURE__ */ new Map();
2714
+ for (const list of lists) for (const d of list ?? []) {
2715
+ const id = identity(d);
2716
+ const at = index.get(id);
2717
+ if (at === void 0) {
2718
+ index.set(id, result.length);
2719
+ result.push(d);
2720
+ } else if (RANK[d.severity] < RANK[result[at].severity]) result[at] = {
2721
+ ...result[at],
2722
+ severity: d.severity
2723
+ };
2724
+ }
2725
+ return result;
2726
+ }
2727
+ /** Diagnostics by element (or link) id, each list in the input order. */
2728
+ function groupDiagnostics(diagnostics) {
2729
+ const map = /* @__PURE__ */ new Map();
2730
+ for (const d of diagnostics ?? []) {
2731
+ const list = map.get(d.elementId);
2732
+ if (list) list.push(d);
2733
+ else map.set(d.elementId, [d]);
2734
+ }
2735
+ return map;
2736
+ }
2737
+ const NONE = [];
2738
+ function createDiagnosticsStore() {
2739
+ const bySource = /* @__PURE__ */ new Map();
2740
+ const listeners = /* @__PURE__ */ new Set();
2741
+ let merged = NONE;
2742
+ const changed = () => {
2743
+ merged = bySource.size === 0 ? NONE : mergeDiagnostics(bySource.values());
2744
+ for (const listener of listeners) listener();
2745
+ };
2746
+ return {
2747
+ publish(source, diagnostics) {
2748
+ if (diagnostics.length === 0) {
2749
+ if (bySource.delete(source)) changed();
2750
+ return;
2751
+ }
2752
+ bySource.set(source, diagnostics.map((d) => d.source === void 0 ? {
2753
+ ...d,
2754
+ source
2755
+ } : d));
2756
+ changed();
2757
+ },
2758
+ clear(source) {
2759
+ if (source === void 0) {
2760
+ if (bySource.size === 0) return;
2761
+ bySource.clear();
2762
+ } else if (!bySource.delete(source)) return;
2763
+ changed();
2764
+ },
2765
+ sources: () => [...bySource.keys()],
2766
+ get: (source) => bySource.get(source) ?? NONE,
2767
+ getAll: () => merged,
2768
+ subscribe(listener) {
2769
+ listeners.add(listener);
2770
+ return () => listeners.delete(listener);
2771
+ }
2772
+ };
2773
+ }
2774
+ const LSP_SEVERITIES = [
2775
+ "error",
2776
+ "warning",
2777
+ "info",
2778
+ "hint"
2779
+ ];
2780
+ function anchorOf(data) {
2781
+ if (typeof data !== "object" || data === null) return {};
2782
+ const record = data;
2783
+ const elementId = typeof record.elementId === "string" ? record.elementId : typeof record.nodeId === "string" ? record.nodeId : void 0;
2784
+ return {
2785
+ ...elementId ? { elementId } : {},
2786
+ ...typeof record.key === "string" ? { key: record.key } : {}
2787
+ };
2788
+ }
2789
+ function convertLsp(d, options) {
2790
+ const anchor = anchorOf(d.data);
2791
+ const elementId = anchor.elementId ?? options.elementIdFor?.(d);
2792
+ if (!elementId) return void 0;
2793
+ const base = options.severityNumbering === "vscode" ? 0 : 1;
2794
+ const severity = d.severity === void 0 ? "error" : LSP_SEVERITIES[d.severity - base] ?? "error";
2795
+ const code = typeof d.code === "object" ? d.code.value : d.code;
2796
+ return {
2797
+ elementId,
2798
+ ...anchor.key !== void 0 ? { key: anchor.key } : {},
2799
+ severity,
2800
+ message: typeof d.message === "string" ? d.message : d.message.value,
2801
+ ...d.source !== void 0 ? { source: d.source } : {},
2802
+ ...d.range ? { range: d.range } : {},
2803
+ ...code !== void 0 ? { code: String(code) } : {},
2804
+ ...d.data !== void 0 ? { data: d.data } : {}
2805
+ };
2806
+ }
2807
+ /**
2808
+ * One LSP diagnostic anchored by `data.elementId` (or `data.nodeId`), or `undefined` when it
2809
+ * names no element. For range-anchored diagnostics use `fromLspDiagnostics` with `elementIdFor`.
2810
+ */
2811
+ function fromLspDiagnostic(diagnostic) {
2812
+ return convertLsp(diagnostic, {});
2813
+ }
2814
+ /** LSP diagnostics as goal diagnostics; those that can't be anchored to an element are dropped. */
2815
+ function fromLspDiagnostics(diagnostics, options = {}) {
2816
+ const result = [];
2817
+ for (const d of diagnostics) {
2818
+ const converted = convertLsp(d, options);
2819
+ if (converted) result.push(converted);
2820
+ }
2821
+ return result;
2822
+ }
2823
+ /** One `{ nodeId, severity, message }` diagnostic, or `undefined` without a `nodeId`. */
2824
+ function fromNodeIdDiagnostic(diagnostic) {
2825
+ const { nodeId, severity, ...rest } = diagnostic;
2826
+ if (!nodeId) return void 0;
2827
+ return {
2828
+ ...rest,
2829
+ elementId: nodeId,
2830
+ severity: Object.hasOwn(RANK, severity) ? severity : "info"
2831
+ };
2832
+ }
2833
+ /**
2834
+ * `{ nodeId, severity, message }` diagnostics (MutRoSe) as goal diagnostics, dropping those
2835
+ * without a node: `store.publish('mutrose', fromNodeIdDiagnostics(message.diagnostics))`.
2836
+ */
2837
+ function fromNodeIdDiagnostics(diagnostics) {
2838
+ const result = [];
2839
+ for (const d of diagnostics) {
2840
+ const converted = fromNodeIdDiagnostic(d);
2841
+ if (converted) result.push(converted);
2842
+ }
2843
+ return result;
2844
+ }
2845
+ //#endregion
2846
+ export { ACTOR_KINDS, ACTOR_LINK_KINDS, CONTRIBUTION_LABELS, DEFAULT_DIAGRAM, DEFAULT_ELEMENT_SIZE, DEPENDENCY_LINK_KINDS, DIAGNOSTIC_SEVERITIES, DSM3_FILE_KEYS, DSM3_TOOL, ELEMENT_KINDS, FILE_METAMODEL_KEY, HUB_ENTRY_KEY, ISTAR_2_0, ISTAR_2_0_DECLARATION, ISTAR_PREFIX, ISTAR_VERSION, LINK_KINDS, LINK_KIND_INFO, LINK_SELECTOR, METAMODEL_VERSION, MetamodelError, ModelOperationError, NODE_KINDS, NODE_KIND_INFO, NODE_LINK_KINDS, PISTAR_TOOL, PistarParseError, PistarWriteError, addDependency, addElement, addLeg, boolean, canLink, childrenOf, compareSeverity, connect, createDiagnosticsStore, createEmptyModel, createModelStore, defaultHubPosition, defaultIdGenerator, defineMetamodel, defineMetamodelExtension, defineProperties, dependencyEnds, dependencyLinksOf, detectFramework, detectPrefix, disconnect, effectiveElementKind, effectiveLinkKind, elementBehavesLike, elementCenter, elementKindDefinition, enumOf, extendMetamodel, fileMetamodelExtension, fileMetamodelOf, fromLspDiagnostic, fromLspDiagnostics, fromNodeIdDiagnostic, fromNodeIdDiagnostics, getSourceLayout, groupDiagnostics, hostMetamodelOf, hubLinkOf, hubOfLink, inheritMetamodel, inheritSourceLayout, isActor, isActorIn, isActorKind, isActorKindIn, isActorLinkKind, isContributionLabel, isDependencyLinkKind, isElementKind, isHubElement, isLinkKind, isLinkRef, isNode, isNodeIn, isNodeKind, isNodeKindIn, isNodeLinkKind, linkBehavesLike, linkKindDefinition, linkTargetOf, linksOf, mergeDiagnostics, metamodelOf, moveElement, nestElement, number, parsePistar, prop, removeElement, removeElements, removeLeg, setHostMetamodel, shortKindName, string, targetsLinks, toPistar, toPistarObject, updateDiagram, updateElement, updateLink, validateFileMetamodel, validateModel, validateModelProperties, withFileMetamodel, withMetamodel, worstDiagnosticSeverity };
2030
2847
 
2031
2848
  //# sourceMappingURL=index.js.map