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