@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/README.md +134 -2
- package/dist/index.cjs +864 -198
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +242 -22
- package/dist/index.d.ts +242 -22
- package/dist/index.js +846 -199
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
216
|
-
*
|
|
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
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
|
236
|
-
|
|
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:
|
|
426
|
-
istar:
|
|
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
|
-
/**
|
|
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
|
-
|
|
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)))
|
|
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
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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
|
-
|
|
1039
|
-
if (!isRecord(raw)) throw new PistarParseError(
|
|
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(
|
|
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(
|
|
1044
|
-
if (typeof source !== "string" || typeof target !== "string") throw new PistarParseError(
|
|
1045
|
-
if (elements.has(id) || links.has(id)) throw new PistarParseError(
|
|
1046
|
-
|
|
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())
|
|
1118
|
-
|
|
1119
|
-
if (
|
|
1120
|
-
|
|
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
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
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
|
-
}
|
|
1139
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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.
|
|
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;
|
|
@@ -2196,6 +2843,6 @@ function fromNodeIdDiagnostics(diagnostics) {
|
|
|
2196
2843
|
return result;
|
|
2197
2844
|
}
|
|
2198
2845
|
//#endregion
|
|
2199
|
-
export { ACTOR_KINDS, ACTOR_LINK_KINDS, CONTRIBUTION_LABELS, DEFAULT_DIAGRAM, DEFAULT_ELEMENT_SIZE, DEPENDENCY_LINK_KINDS, DIAGNOSTIC_SEVERITIES, 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, compareSeverity, connect, createDiagnosticsStore, createEmptyModel, createModelStore, defaultIdGenerator, defineMetamodelExtension, defineProperties, dependencyEnds, dependencyLinksOf, disconnect, effectiveElementKind, effectiveLinkKind, elementBehavesLike, elementKindDefinition, enumOf, extendMetamodel, fileMetamodelExtension, fileMetamodelOf, fromLspDiagnostic, fromLspDiagnostics, fromNodeIdDiagnostic, fromNodeIdDiagnostics, getSourceLayout, groupDiagnostics, hostMetamodelOf, inheritMetamodel, inheritSourceLayout, isActor, isActorIn, isActorKind, isActorKindIn, isActorLinkKind, isContributionLabel, isDependencyLinkKind, isElementKind, isLinkKind, isNode, isNodeIn, isNodeKind, isNodeKindIn, isNodeLinkKind, linkKindDefinition, linksOf, mergeDiagnostics, metamodelOf, moveElement, nestElement, number, parsePistar, prop, removeElement, removeElements, setHostMetamodel, shortKindName, string, toPistar, toPistarObject, updateDiagram, updateElement, updateLink, validateFileMetamodel, validateModel, validateModelProperties, withFileMetamodel, withMetamodel, worstDiagnosticSeverity };
|
|
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 };
|
|
2200
2847
|
|
|
2201
2848
|
//# sourceMappingURL=index.js.map
|