@diagc/core 0.1.0 → 0.2.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 +3 -3
- package/dist/builder.d.ts +88 -2
- package/dist/builder.js +146 -1
- package/dist/commands.d.ts +25 -2
- package/dist/commands.js +25 -7
- package/dist/comments.d.ts +24 -0
- package/dist/comments.js +25 -0
- package/dist/dates.d.ts +1 -0
- package/dist/dates.js +12 -0
- package/dist/eject.js +2 -2
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/mutate.d.ts +27 -2
- package/dist/mutate.js +115 -17
- package/dist/plan.d.ts +105 -0
- package/dist/plan.js +172 -0
- package/dist/types.d.ts +31 -3
- package/dist/types.js +4 -2
- package/dist/validate.d.ts +1 -1
- package/dist/validate.js +121 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# @diagc/core
|
|
2
2
|
|
|
3
|
-
The diagram model behind [`diagc`](https://www.npmjs.com/package/
|
|
3
|
+
The diagram model behind [`diagc`](https://www.npmjs.com/package/@diagc/cli): the `DiagramModel` types, `validate()`, the builder DSL, and the view compiler. No React, no filesystem — it does not know it is going to be drawn.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm i -D @diagc/core
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
Install it alongside
|
|
9
|
+
Install it alongside the CLI (`@diagc/cli`) to get types and completion when authoring `.diagram.ts` files:
|
|
10
10
|
|
|
11
11
|
```ts
|
|
12
12
|
import { model } from '@diagc/core';
|
|
@@ -24,4 +24,4 @@ Docs and the full model reference: <https://github.com/Ferroman/diagc>
|
|
|
24
24
|
|
|
25
25
|
## License
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
[AGPL-3.0-only](https://github.com/Ferroman/diagc/blob/main/LICENSE), with additional permissions under section 7: diagrams you author, and the pages and images built from them, are yours to license however you like. A commercial license is available. See the [project README](https://github.com/Ferroman/diagc#license).
|
package/dist/builder.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { Column, DiagramLegend, DiagramModel, EdgeLabel, FontScale, LayerRule, NotationId, Polarity, RelationStyle, TextAlign, TextRun, Threat } from './types.js';
|
|
1
|
+
import type { Column, Comment, DiagramLegend, DiagramModel, EdgeLabel, FontScale, LayerRule, Link, NotationId, Polarity, RelationStyle, TextAlign, TextRun, Threat } from './types.js';
|
|
2
|
+
import { type ElementTarget } from './comments.js';
|
|
2
3
|
import { type FishbonePreset } from './fishbone.js';
|
|
3
4
|
import { type Valence } from './second-order.js';
|
|
4
5
|
import { type ThreatTarget } from './threat-model.js';
|
|
@@ -42,6 +43,10 @@ export interface NodeOpts {
|
|
|
42
43
|
columns?: Column[];
|
|
43
44
|
/** STRIDE findings (see DiagramNode.threats) */
|
|
44
45
|
threats?: Threat[];
|
|
46
|
+
/** remarks shown in this node's bubble (see DiagramNode.comments) */
|
|
47
|
+
comments?: Comment[];
|
|
48
|
+
/** resources this node points at, listed in its bubble (see DiagramNode.links) */
|
|
49
|
+
links?: Link[];
|
|
45
50
|
}
|
|
46
51
|
export interface RelateOpts {
|
|
47
52
|
kind: string;
|
|
@@ -63,11 +68,19 @@ export interface RelateOpts {
|
|
|
63
68
|
toColumn?: string;
|
|
64
69
|
/** STRIDE findings (see DiagramRelation.threats) */
|
|
65
70
|
threats?: Threat[];
|
|
71
|
+
/** remarks shown in this relation's bubble (see DiagramRelation.comments) */
|
|
72
|
+
comments?: Comment[];
|
|
66
73
|
}
|
|
67
74
|
/** A threat as authored: the id is synthesized (`t1`, `t2`, …) unless given. */
|
|
68
75
|
export type ThreatOpts = Omit<Threat, 'id'> & {
|
|
69
76
|
id?: string;
|
|
70
77
|
};
|
|
78
|
+
/** A comment as authored: the id is synthesized (`c1`, `c2`, …) unless given. */
|
|
79
|
+
export interface CommentOpts {
|
|
80
|
+
id?: string;
|
|
81
|
+
by?: string;
|
|
82
|
+
at?: string;
|
|
83
|
+
}
|
|
71
84
|
export interface ContainsOpts {
|
|
72
85
|
/** plane the containment belongs to; defaults to the model's first-declared plane */
|
|
73
86
|
plane?: string;
|
|
@@ -82,6 +95,10 @@ export declare class NodeRef {
|
|
|
82
95
|
* threat-model ones: threat-modelling an existing C4 or ER diagram annotates
|
|
83
96
|
* the nodes it already has. */
|
|
84
97
|
threat(opts: ThreatOpts): this;
|
|
98
|
+
/** A remark on this element, shown in its bubble. */
|
|
99
|
+
comment(text: string, opts?: CommentOpts): this;
|
|
100
|
+
/** A resource this element points at, listed in its bubble. */
|
|
101
|
+
link(label: string, url: string): this;
|
|
85
102
|
}
|
|
86
103
|
export interface CommitOpts {
|
|
87
104
|
/** default `${branchId}-${n}`, n = this branch's 1-based commit count */
|
|
@@ -189,12 +206,14 @@ export declare class FishboneBuilder {
|
|
|
189
206
|
/** the bones of a standard set, keyed by their slug ids (`presetId`) */
|
|
190
207
|
categories(preset: FishbonePreset): Record<string, CategoryRef>;
|
|
191
208
|
}
|
|
192
|
-
/** A data flow: a relation ref that takes threats, the way a NodeRef does. */
|
|
209
|
+
/** A data flow: a relation ref that takes threats and comments, the way a NodeRef does. */
|
|
193
210
|
export declare class FlowRef {
|
|
194
211
|
readonly id: string;
|
|
195
212
|
private readonly m;
|
|
196
213
|
constructor(id: string, m: ModelBuilder);
|
|
197
214
|
threat(opts: ThreatOpts): this;
|
|
215
|
+
/** A remark on this flow, shown in its bubble. */
|
|
216
|
+
comment(text: string, opts?: CommentOpts): this;
|
|
198
217
|
}
|
|
199
218
|
/** A DFD element's node options, minus the two its helper already supplies:
|
|
200
219
|
* `type` from the helper itself, `name` from its second argument. */
|
|
@@ -217,6 +236,58 @@ export declare class ThreatModelBuilder {
|
|
|
217
236
|
/** a data flow; a string is its label */
|
|
218
237
|
flow(from: NodeRef, to: NodeRef, labelOrOpts?: string | Omit<RelateOpts, 'kind'>): FlowRef;
|
|
219
238
|
}
|
|
239
|
+
/** A zone's node options: the two dates are required, `type`/`plane`/`metadata`
|
|
240
|
+
* are the builder's. */
|
|
241
|
+
export interface ZoneOpts extends Omit<NodeOpts, 'type' | 'metadata' | 'plane'> {
|
|
242
|
+
start: string;
|
|
243
|
+
end: string;
|
|
244
|
+
}
|
|
245
|
+
export interface EventOpts extends Omit<NodeOpts, 'type' | 'metadata' | 'plane'> {
|
|
246
|
+
at: string;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* A zone: a NodeRef (so comment/link/threat compose) whose helpers nest on the
|
|
250
|
+
* PLAN plane. Everything a zone creates is scoped to that plane — a plan added
|
|
251
|
+
* to an architecture model must not leak bars into the architecture view — and
|
|
252
|
+
* every containment edge names the plane, so the plan need not be the first
|
|
253
|
+
* plane declared.
|
|
254
|
+
*/
|
|
255
|
+
export declare class ZoneBuilder extends NodeRef {
|
|
256
|
+
private readonly m;
|
|
257
|
+
/** the plan plane's id */
|
|
258
|
+
readonly plane: string;
|
|
259
|
+
constructor(id: string, m: ModelBuilder,
|
|
260
|
+
/** the plan plane's id */
|
|
261
|
+
plane: string);
|
|
262
|
+
/** a nested zone */
|
|
263
|
+
zone(id: string, opts: ZoneOpts): ZoneBuilder;
|
|
264
|
+
/** an event inside this zone */
|
|
265
|
+
event(id: string, opts: EventOpts): NodeRef;
|
|
266
|
+
/** schedule any node (a C4 container, an ER table…) inside this zone */
|
|
267
|
+
contains(...nodes: NodeRef[]): this;
|
|
268
|
+
private role;
|
|
269
|
+
owner(n: NodeRef): this;
|
|
270
|
+
executor(n: NodeRef): this;
|
|
271
|
+
checker(n: NodeRef): this;
|
|
272
|
+
}
|
|
273
|
+
/** Root-level plan helpers; see ZoneBuilder for the nested ones. */
|
|
274
|
+
export declare class PlanBuilder {
|
|
275
|
+
private readonly m;
|
|
276
|
+
readonly plane: string;
|
|
277
|
+
constructor(m: ModelBuilder, plane: string);
|
|
278
|
+
/** a top-level zone */
|
|
279
|
+
zone(id: string, opts: ZoneOpts): ZoneBuilder;
|
|
280
|
+
/** a top-level event, drawn in the header */
|
|
281
|
+
event(id: string, opts: EventOpts): NodeRef;
|
|
282
|
+
/** a person to hand roles to (`zone.owner(p)` …). `plane` is omitted, not
|
|
283
|
+
* just overridden: a person is always scoped to the plan plane, and `...opts`
|
|
284
|
+
* spreads after `plane: this.plane`, so a merely-overridden plane would
|
|
285
|
+
* silently win over the forced one. */
|
|
286
|
+
person(id: string, name?: string, opts?: Omit<ElementOpts, 'plane'>): NodeRef;
|
|
287
|
+
/** a team to hand roles to, same deal as `person` — an actor that holds a
|
|
288
|
+
* role but is never an individual. Same forced-plane guard. */
|
|
289
|
+
team(id: string, name?: string, opts?: Omit<ElementOpts, 'plane'>): NodeRef;
|
|
290
|
+
}
|
|
220
291
|
export interface ActivityElementOpts {
|
|
221
292
|
color?: string;
|
|
222
293
|
}
|
|
@@ -281,6 +352,7 @@ export declare class ModelBuilder {
|
|
|
281
352
|
private so;
|
|
282
353
|
private fb;
|
|
283
354
|
private tm;
|
|
355
|
+
private pl;
|
|
284
356
|
constructor(id: string, name: string);
|
|
285
357
|
node(id: string, opts?: NodeOpts): NodeRef;
|
|
286
358
|
/** ER table: a node of type 'db-table' carrying `columns`. */
|
|
@@ -298,6 +370,11 @@ export declare class ModelBuilder {
|
|
|
298
370
|
/** internal — appends a threat to the node or relation `target` names; used by
|
|
299
371
|
* NodeRef.threat() and FlowRef.threat() */
|
|
300
372
|
addThreat(target: ThreatTarget, opts: ThreatOpts): void;
|
|
373
|
+
/** internal — appends a comment to the node or relation `target` names; used by
|
|
374
|
+
* NodeRef.comment() and FlowRef.comment() */
|
|
375
|
+
addComment(target: ElementTarget, text: string, opts: CommentOpts): void;
|
|
376
|
+
/** internal — appends a link to a node; used by NodeRef.link() */
|
|
377
|
+
addLink(nodeId: string, link: Link): void;
|
|
301
378
|
relate(from: NodeRef, to: NodeRef, opts: RelateOpts): this;
|
|
302
379
|
/** internal — like relate(), but returns the new relation's id (FlowRef needs
|
|
303
380
|
* it to hang threats off the flow) */
|
|
@@ -353,6 +430,15 @@ export declare class ModelBuilder {
|
|
|
353
430
|
plane?: string;
|
|
354
431
|
name?: string;
|
|
355
432
|
}): ThreatModelBuilder;
|
|
433
|
+
/**
|
|
434
|
+
* Declare a plan (schedule) plane. Always a plane — zones are containers with
|
|
435
|
+
* dates, and the plan's containment must not be the architecture's. Need not
|
|
436
|
+
* be the first plane: the intended use is a plan plane added to an existing
|
|
437
|
+
* model, scheduling that model's nodes inside its zones.
|
|
438
|
+
*/
|
|
439
|
+
plan(id?: string, opts?: {
|
|
440
|
+
name?: string;
|
|
441
|
+
}): PlanBuilder;
|
|
356
442
|
/** Declare an activity diagram: a framed swimlane flow. Repeatable — each
|
|
357
443
|
* call is one frame; frames are ordinary containers on whatever plane the
|
|
358
444
|
* model uses (no notation, no plane creation). */
|
package/dist/builder.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { nextCommentId } from './comments.js';
|
|
1
2
|
import { FB_CATEGORY_TYPE, FB_CAUSE_OF_KIND, FB_CAUSE_TYPE, FB_EFFECT_TYPE, FISHBONE_PRESETS, presetId } from './fishbone.js';
|
|
2
3
|
import { GIT_STAGE_TYPE } from './git.js';
|
|
3
4
|
import { SO_DECISION_TYPE, SO_LEADS_TO_KIND, consequenceTypeOf } from './second-order.js';
|
|
4
5
|
import { TM_BOUNDARY_TYPE, TM_ENTITY_TYPE, TM_FLOW_KIND, TM_NOTATION, TM_PROCESS_TYPE, TM_STORE_TYPE, } from './threat-model.js';
|
|
5
6
|
import { DiagramValidationError, validate } from './validate.js';
|
|
7
|
+
import { PLAN_EVENT_TYPE, PLAN_NOTATION, PLAN_PERSON_TYPE, PLAN_TEAM_TYPE, PLAN_ZONE_TYPE } from './plan.js';
|
|
6
8
|
export class NodeRef {
|
|
7
9
|
id;
|
|
8
10
|
builder;
|
|
@@ -27,6 +29,16 @@ export class NodeRef {
|
|
|
27
29
|
this.builder.addThreat({ node: this.id }, opts);
|
|
28
30
|
return this;
|
|
29
31
|
}
|
|
32
|
+
/** A remark on this element, shown in its bubble. */
|
|
33
|
+
comment(text, opts = {}) {
|
|
34
|
+
this.builder.addComment({ node: this.id }, text, opts);
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
/** A resource this element points at, listed in its bubble. */
|
|
38
|
+
link(label, url) {
|
|
39
|
+
this.builder.addLink(this.id, { label, url });
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
30
42
|
}
|
|
31
43
|
export class CommitRef extends NodeRef {
|
|
32
44
|
branch;
|
|
@@ -201,7 +213,7 @@ export class FishboneBuilder {
|
|
|
201
213
|
}));
|
|
202
214
|
}
|
|
203
215
|
}
|
|
204
|
-
/** A data flow: a relation ref that takes threats, the way a NodeRef does. */
|
|
216
|
+
/** A data flow: a relation ref that takes threats and comments, the way a NodeRef does. */
|
|
205
217
|
export class FlowRef {
|
|
206
218
|
id;
|
|
207
219
|
m;
|
|
@@ -213,6 +225,11 @@ export class FlowRef {
|
|
|
213
225
|
this.m.addThreat({ relation: this.id }, opts);
|
|
214
226
|
return this;
|
|
215
227
|
}
|
|
228
|
+
/** A remark on this flow, shown in its bubble. */
|
|
229
|
+
comment(text, opts = {}) {
|
|
230
|
+
this.m.addComment({ relation: this.id }, text, opts);
|
|
231
|
+
return this;
|
|
232
|
+
}
|
|
216
233
|
}
|
|
217
234
|
/** The four DFD element kinds plus flows. Everything it hands back is an
|
|
218
235
|
* ordinary NodeRef/FlowRef, so the rest of the builder — contains, relate,
|
|
@@ -247,6 +264,93 @@ export class ThreatModelBuilder {
|
|
|
247
264
|
return new FlowRef(this.m.addRelation(from, to, { kind: TM_FLOW_KIND, ...opts }), this.m);
|
|
248
265
|
}
|
|
249
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* A zone: a NodeRef (so comment/link/threat compose) whose helpers nest on the
|
|
269
|
+
* PLAN plane. Everything a zone creates is scoped to that plane — a plan added
|
|
270
|
+
* to an architecture model must not leak bars into the architecture view — and
|
|
271
|
+
* every containment edge names the plane, so the plan need not be the first
|
|
272
|
+
* plane declared.
|
|
273
|
+
*/
|
|
274
|
+
export class ZoneBuilder extends NodeRef {
|
|
275
|
+
m;
|
|
276
|
+
plane;
|
|
277
|
+
constructor(id, m,
|
|
278
|
+
/** the plan plane's id */
|
|
279
|
+
plane) {
|
|
280
|
+
super(id, m);
|
|
281
|
+
this.m = m;
|
|
282
|
+
this.plane = plane;
|
|
283
|
+
}
|
|
284
|
+
/** a nested zone */
|
|
285
|
+
zone(id, opts) {
|
|
286
|
+
const z = planZone(this.m, this.plane, id, opts);
|
|
287
|
+
this.m.addContainment(this.id, id, this.plane);
|
|
288
|
+
return z;
|
|
289
|
+
}
|
|
290
|
+
/** an event inside this zone */
|
|
291
|
+
event(id, opts) {
|
|
292
|
+
const e = planEvent(this.m, this.plane, id, opts);
|
|
293
|
+
this.m.addContainment(this.id, id, this.plane);
|
|
294
|
+
return e;
|
|
295
|
+
}
|
|
296
|
+
/** schedule any node (a C4 container, an ER table…) inside this zone */
|
|
297
|
+
contains(...nodes) {
|
|
298
|
+
for (const n of nodes)
|
|
299
|
+
this.m.addContainment(this.id, n.id, this.plane);
|
|
300
|
+
return this;
|
|
301
|
+
}
|
|
302
|
+
role(n, kind) {
|
|
303
|
+
this.m.addRelation(n, this, { kind });
|
|
304
|
+
return this;
|
|
305
|
+
}
|
|
306
|
+
owner(n) {
|
|
307
|
+
return this.role(n, 'owns');
|
|
308
|
+
}
|
|
309
|
+
executor(n) {
|
|
310
|
+
return this.role(n, 'executes');
|
|
311
|
+
}
|
|
312
|
+
checker(n) {
|
|
313
|
+
return this.role(n, 'checks');
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function planZone(m, plane, id, opts) {
|
|
317
|
+
const { start, end, ...rest } = opts;
|
|
318
|
+
m.node(id, { type: PLAN_ZONE_TYPE, plane, metadata: { start, end }, ...rest });
|
|
319
|
+
return new ZoneBuilder(id, m, plane);
|
|
320
|
+
}
|
|
321
|
+
function planEvent(m, plane, id, opts) {
|
|
322
|
+
const { at, ...rest } = opts;
|
|
323
|
+
return m.node(id, { type: PLAN_EVENT_TYPE, plane, metadata: { at }, ...rest });
|
|
324
|
+
}
|
|
325
|
+
/** Root-level plan helpers; see ZoneBuilder for the nested ones. */
|
|
326
|
+
export class PlanBuilder {
|
|
327
|
+
m;
|
|
328
|
+
plane;
|
|
329
|
+
constructor(m, plane) {
|
|
330
|
+
this.m = m;
|
|
331
|
+
this.plane = plane;
|
|
332
|
+
}
|
|
333
|
+
/** a top-level zone */
|
|
334
|
+
zone(id, opts) {
|
|
335
|
+
return planZone(this.m, this.plane, id, opts);
|
|
336
|
+
}
|
|
337
|
+
/** a top-level event, drawn in the header */
|
|
338
|
+
event(id, opts) {
|
|
339
|
+
return planEvent(this.m, this.plane, id, opts);
|
|
340
|
+
}
|
|
341
|
+
/** a person to hand roles to (`zone.owner(p)` …). `plane` is omitted, not
|
|
342
|
+
* just overridden: a person is always scoped to the plan plane, and `...opts`
|
|
343
|
+
* spreads after `plane: this.plane`, so a merely-overridden plane would
|
|
344
|
+
* silently win over the forced one. */
|
|
345
|
+
person(id, name, opts = {}) {
|
|
346
|
+
return this.m.node(id, { type: PLAN_PERSON_TYPE, plane: this.plane, ...(name !== undefined ? { name } : {}), ...opts });
|
|
347
|
+
}
|
|
348
|
+
/** a team to hand roles to, same deal as `person` — an actor that holds a
|
|
349
|
+
* role but is never an individual. Same forced-plane guard. */
|
|
350
|
+
team(id, name, opts = {}) {
|
|
351
|
+
return this.m.node(id, { type: PLAN_TEAM_TYPE, plane: this.plane, ...(name !== undefined ? { name } : {}), ...opts });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
250
354
|
/** Shared element surface of a lane and a region: each helper creates a typed
|
|
251
355
|
* node contained by this scope and hands back a plain NodeRef, so everything
|
|
252
356
|
* composes with the rest of the builder (relate, layers, contains). */
|
|
@@ -361,6 +465,7 @@ export class ModelBuilder {
|
|
|
361
465
|
so;
|
|
362
466
|
fb;
|
|
363
467
|
tm;
|
|
468
|
+
pl;
|
|
364
469
|
constructor(id, name) {
|
|
365
470
|
this.id = id;
|
|
366
471
|
this.name = name;
|
|
@@ -419,6 +524,33 @@ export class ModelBuilder {
|
|
|
419
524
|
}
|
|
420
525
|
element.threats = [...threats, threat];
|
|
421
526
|
}
|
|
527
|
+
/** internal — appends a comment to the node or relation `target` names; used by
|
|
528
|
+
* NodeRef.comment() and FlowRef.comment() */
|
|
529
|
+
addComment(target, text, opts) {
|
|
530
|
+
const element = 'node' in target
|
|
531
|
+
? this.nodes.find((n) => n.id === target.node)
|
|
532
|
+
: this.relations.find((r) => r.id === target.relation);
|
|
533
|
+
if (element === undefined) {
|
|
534
|
+
throw new Error('node' in target
|
|
535
|
+
? `comment(): unknown node '${target.node}'`
|
|
536
|
+
: `comment(): unknown relation '${target.relation}'`);
|
|
537
|
+
}
|
|
538
|
+
const comments = element.comments ?? [];
|
|
539
|
+
const { id, ...rest } = opts;
|
|
540
|
+
// per-element ids, as threats: two elements' first comments are both c1
|
|
541
|
+
const comment = { id: id ?? nextCommentId(comments), text, ...pruneUndefined(rest) };
|
|
542
|
+
if (comments.some((c) => c.id === comment.id)) {
|
|
543
|
+
throw new Error(`comment(): duplicate comment id '${comment.id}' on '${element.id}'`);
|
|
544
|
+
}
|
|
545
|
+
element.comments = [...comments, comment];
|
|
546
|
+
}
|
|
547
|
+
/** internal — appends a link to a node; used by NodeRef.link() */
|
|
548
|
+
addLink(nodeId, link) {
|
|
549
|
+
const node = this.nodes.find((n) => n.id === nodeId);
|
|
550
|
+
if (node === undefined)
|
|
551
|
+
throw new Error(`link(): unknown node '${nodeId}'`);
|
|
552
|
+
node.links = [...(node.links ?? []), link];
|
|
553
|
+
}
|
|
422
554
|
relate(from, to, opts) {
|
|
423
555
|
this.addRelation(from, to, opts);
|
|
424
556
|
return this;
|
|
@@ -523,6 +655,19 @@ export class ModelBuilder {
|
|
|
523
655
|
this.tm = new ThreatModelBuilder(this);
|
|
524
656
|
return this.tm;
|
|
525
657
|
}
|
|
658
|
+
/**
|
|
659
|
+
* Declare a plan (schedule) plane. Always a plane — zones are containers with
|
|
660
|
+
* dates, and the plan's containment must not be the architecture's. Need not
|
|
661
|
+
* be the first plane: the intended use is a plan plane added to an existing
|
|
662
|
+
* model, scheduling that model's nodes inside its zones.
|
|
663
|
+
*/
|
|
664
|
+
plan(id = 'plan', opts = {}) {
|
|
665
|
+
if (this.pl !== undefined)
|
|
666
|
+
throw new Error('plan() already declared');
|
|
667
|
+
this.plane(id, { name: opts.name ?? 'Plan', notation: PLAN_NOTATION });
|
|
668
|
+
this.pl = new PlanBuilder(this, id);
|
|
669
|
+
return this.pl;
|
|
670
|
+
}
|
|
526
671
|
/** Declare an activity diagram: a framed swimlane flow. Repeatable — each
|
|
527
672
|
* call is one frame; frames are ordinary containers on whatever plane the
|
|
528
673
|
* model uses (no notation, no plane creation). */
|
package/dist/commands.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { Column, DiagramLayer, DiagramLegend, DiagramModel, DiagramNode, DiagramPlane, Drawings, EdgeLabelPlacement, LayoutOverlay, LayoutSettings, Stroke, TextRun, Threat } from './types.js';
|
|
1
|
+
import type { Column, Comment, DiagramLayer, DiagramLegend, DiagramModel, DiagramNode, DiagramPlane, Drawings, EdgeLabelPlacement, LayoutOverlay, LayoutSettings, Stroke, TextRun, Threat } from './types.js';
|
|
2
2
|
import { type ThreatTarget } from './threat-model.js';
|
|
3
|
-
import { type NodeDetails, type RelationOptsInput, type RelationPatch, type ThreatPatch } from './mutate.js';
|
|
3
|
+
import { type CommentPatch, type NodeDetails, type PlanDates, type RelationOptsInput, type RelationPatch, type ThreatPatch } from './mutate.js';
|
|
4
4
|
export interface EditorState {
|
|
5
5
|
model: DiagramModel;
|
|
6
6
|
layout: LayoutOverlay;
|
|
@@ -18,9 +18,13 @@ export declare function openingPins(layout: LayoutOverlay | undefined, m: Diagra
|
|
|
18
18
|
export type EditorCommand = {
|
|
19
19
|
type: 'add-node';
|
|
20
20
|
node: DiagramNode;
|
|
21
|
+
/** `before`/`after` name a sibling to slot the new membership beside:
|
|
22
|
+
* child order is containment declaration order (activity lanes, git lanes) */
|
|
21
23
|
parent?: {
|
|
22
24
|
id: string;
|
|
23
25
|
plane?: string;
|
|
26
|
+
before?: string;
|
|
27
|
+
after?: string;
|
|
24
28
|
};
|
|
25
29
|
} | {
|
|
26
30
|
type: 'rename-node';
|
|
@@ -30,6 +34,10 @@ export type EditorCommand = {
|
|
|
30
34
|
type: 'set-node-details';
|
|
31
35
|
id: string;
|
|
32
36
|
details: NodeDetails;
|
|
37
|
+
} | {
|
|
38
|
+
type: 'set-plan-dates';
|
|
39
|
+
id: string;
|
|
40
|
+
dates: PlanDates;
|
|
33
41
|
} | {
|
|
34
42
|
type: 'set-node-rich';
|
|
35
43
|
id: string;
|
|
@@ -54,6 +62,21 @@ export type EditorCommand = {
|
|
|
54
62
|
type: 'remove-threat';
|
|
55
63
|
target: ThreatTarget;
|
|
56
64
|
id: string;
|
|
65
|
+
}
|
|
66
|
+
/** Comments ride on the element too — same target type as the threat trio. */
|
|
67
|
+
| {
|
|
68
|
+
type: 'add-comment';
|
|
69
|
+
target: ThreatTarget;
|
|
70
|
+
comment: Comment;
|
|
71
|
+
} | {
|
|
72
|
+
type: 'update-comment';
|
|
73
|
+
target: ThreatTarget;
|
|
74
|
+
id: string;
|
|
75
|
+
patch: CommentPatch;
|
|
76
|
+
} | {
|
|
77
|
+
type: 'remove-comment';
|
|
78
|
+
target: ThreatTarget;
|
|
79
|
+
id: string;
|
|
57
80
|
} | {
|
|
58
81
|
type: 'set-node-plane-hidden';
|
|
59
82
|
nodeId: string;
|
package/dist/commands.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { threatTargetKey } from './threat-model.js';
|
|
2
|
+
import { hasNoteContent } from './comments.js';
|
|
2
3
|
import { resolveContainmentPlane } from './view/compile.js';
|
|
3
4
|
import { addStroke, deleteStroke, pruneDrawingsPlane } from './drawings.js';
|
|
4
5
|
import { relationLabels } from './labels.js';
|
|
5
|
-
import { addContainment, addNode, addRelation, addThreat, CommandError, deleteLayer, deleteNode, deletePlane, deleteRelation, groupNodes, mergeLayers, removeContainment, removeThreat, renameNode, setDiagramLegend, setDiagramNotation, setDiagramStyle, setNodeDetails, setNodePlaneHidden, setNodeRich, setTableColumns, subtreeOf, updateRelation, updateThreat, upsertLayer, upsertPlane, } from './mutate.js';
|
|
6
|
+
import { addComment, addContainment, addNode, addRelation, addThreat, CommandError, deleteLayer, deleteNode, deletePlane, deleteRelation, groupNodes, mergeLayers, removeComment, removeContainment, removeThreat, renameNode, setDiagramLegend, setDiagramNotation, setDiagramStyle, setNodeDetails, setNodePlaneHidden, setNodeRich, setPlanDates, setTableColumns, subtreeOf, updateComment, updateRelation, updateThreat, upsertLayer, upsertPlane, } from './mutate.js';
|
|
6
7
|
export const emptyLayout = () => ({ version: 1, planes: {} });
|
|
7
8
|
export function layoutPlaneKey(m, plane) {
|
|
8
9
|
return resolveContainmentPlane(m, plane) ?? 'default';
|
|
@@ -122,19 +123,22 @@ function withOpen(p, open) {
|
|
|
122
123
|
}
|
|
123
124
|
/**
|
|
124
125
|
* Mirror hygiene for `notes` after a command changed the model: a note exists
|
|
125
|
-
* only while its element has
|
|
126
|
-
* its last threat — or was deleted — is dead data.
|
|
127
|
-
*
|
|
126
|
+
* only while its element has something to show, so an offset for an element
|
|
127
|
+
* that lost its last threat/comment/link — or was deleted — is dead data.
|
|
128
|
+
* `hasNoteContent` is the renderer's own test for drawing a bubble; reusing it
|
|
129
|
+
* is what stops a command that merely rewrote `nodes` (a rename, an edited
|
|
130
|
+
* comment) from throwing away a live bubble's saved place. Identity is kept
|
|
131
|
+
* when nothing is dropped, like pruneEdgeLabels.
|
|
128
132
|
*/
|
|
129
133
|
function pruneNotes(layout, before, after) {
|
|
130
134
|
if (layout.notes === undefined || (before.nodes === after.nodes && before.relations === after.relations))
|
|
131
135
|
return layout;
|
|
132
136
|
const alive = new Set();
|
|
133
137
|
for (const n of after.nodes)
|
|
134
|
-
if ((n
|
|
138
|
+
if (hasNoteContent(n))
|
|
135
139
|
alive.add(threatTargetKey({ node: n.id }));
|
|
136
140
|
for (const r of after.relations)
|
|
137
|
-
if ((r
|
|
141
|
+
if (hasNoteContent(r))
|
|
138
142
|
alive.add(threatTargetKey({ relation: r.id }));
|
|
139
143
|
let changed = false;
|
|
140
144
|
const planes = {};
|
|
@@ -241,7 +245,13 @@ function applyModelLayout(state, command) {
|
|
|
241
245
|
case 'add-node': {
|
|
242
246
|
let next = addNode(model, command.node);
|
|
243
247
|
if (command.parent !== undefined) {
|
|
244
|
-
|
|
248
|
+
const { id, plane, before, after } = command.parent;
|
|
249
|
+
const beside = before !== undefined
|
|
250
|
+
? { sibling: before, side: 'before' }
|
|
251
|
+
: after !== undefined
|
|
252
|
+
? { sibling: after, side: 'after' }
|
|
253
|
+
: undefined;
|
|
254
|
+
next = addContainment(next, id, command.node.id, plane, beside);
|
|
245
255
|
}
|
|
246
256
|
return { model: next, layout };
|
|
247
257
|
}
|
|
@@ -249,6 +259,8 @@ function applyModelLayout(state, command) {
|
|
|
249
259
|
return { model: renameNode(model, command.id, command.name), layout };
|
|
250
260
|
case 'set-node-details':
|
|
251
261
|
return { model: setNodeDetails(model, command.id, command.details), layout };
|
|
262
|
+
case 'set-plan-dates':
|
|
263
|
+
return { model: setPlanDates(model, command.id, command.dates), layout };
|
|
252
264
|
case 'set-node-rich':
|
|
253
265
|
return { model: setNodeRich(model, command.id, command.runs), layout };
|
|
254
266
|
case 'set-table-columns':
|
|
@@ -259,6 +271,12 @@ function applyModelLayout(state, command) {
|
|
|
259
271
|
return { model: updateThreat(model, command.target, command.id, command.patch), layout };
|
|
260
272
|
case 'remove-threat':
|
|
261
273
|
return { model: removeThreat(model, command.target, command.id), layout };
|
|
274
|
+
case 'add-comment':
|
|
275
|
+
return { model: addComment(model, command.target, command.comment), layout };
|
|
276
|
+
case 'update-comment':
|
|
277
|
+
return { model: updateComment(model, command.target, command.id, command.patch), layout };
|
|
278
|
+
case 'remove-comment':
|
|
279
|
+
return { model: removeComment(model, command.target, command.id), layout };
|
|
262
280
|
case 'set-node-plane-hidden':
|
|
263
281
|
return { model: setNodePlaneHidden(model, command.nodeId, command.plane, command.hidden), layout };
|
|
264
282
|
case 'set-diagram-style':
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ThreatTarget } from './threat-model.js';
|
|
2
|
+
import type { Comment, DiagramModel } from './types.js';
|
|
3
|
+
/** Which element a comment command or a bubble refers to. The same two-way key
|
|
4
|
+
* threats use (`threatTargetKey` files both under `LayoutOverlay.notes`): an
|
|
5
|
+
* element has ONE bubble, whatever it holds. */
|
|
6
|
+
export type ElementTarget = ThreatTarget;
|
|
7
|
+
/** The element's comment list — `[]` when it carries none, undefined when there
|
|
8
|
+
* is no such element (the two are different answers; see threatsOf). */
|
|
9
|
+
export declare function commentsOf(m: DiagramModel, t: ElementTarget): readonly Comment[] | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Whether an element gets a note bubble: threats, comments or links. The
|
|
12
|
+
* renderer derives bubbles from this and layout hygiene keeps note entries by
|
|
13
|
+
* it — the two must never disagree, or an edit silently drops a bubble's saved
|
|
14
|
+
* place (which is what happened when they did). Structurally typed rather than
|
|
15
|
+
* taking `DiagramNode | DiagramRelation`, so a relation (which carries no
|
|
16
|
+
* `links`) answers the same question without a second predicate.
|
|
17
|
+
*/
|
|
18
|
+
export declare function hasNoteContent(el: {
|
|
19
|
+
threats?: readonly unknown[];
|
|
20
|
+
comments?: readonly unknown[];
|
|
21
|
+
links?: readonly unknown[];
|
|
22
|
+
}): boolean;
|
|
23
|
+
/** First free `c<n>` — scoped to the element, like threat ids. */
|
|
24
|
+
export declare function nextCommentId(comments: readonly Comment[]): string;
|
package/dist/comments.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** The element's comment list — `[]` when it carries none, undefined when there
|
|
2
|
+
* is no such element (the two are different answers; see threatsOf). */
|
|
3
|
+
export function commentsOf(m, t) {
|
|
4
|
+
const el = 'node' in t ? m.nodes.find((n) => n.id === t.node) : m.relations.find((r) => r.id === t.relation);
|
|
5
|
+
return el === undefined ? undefined : (el.comments ?? []);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Whether an element gets a note bubble: threats, comments or links. The
|
|
9
|
+
* renderer derives bubbles from this and layout hygiene keeps note entries by
|
|
10
|
+
* it — the two must never disagree, or an edit silently drops a bubble's saved
|
|
11
|
+
* place (which is what happened when they did). Structurally typed rather than
|
|
12
|
+
* taking `DiagramNode | DiagramRelation`, so a relation (which carries no
|
|
13
|
+
* `links`) answers the same question without a second predicate.
|
|
14
|
+
*/
|
|
15
|
+
export function hasNoteContent(el) {
|
|
16
|
+
return (el.threats?.length ?? 0) > 0 || (el.comments?.length ?? 0) > 0 || (el.links?.length ?? 0) > 0;
|
|
17
|
+
}
|
|
18
|
+
/** First free `c<n>` — scoped to the element, like threat ids. */
|
|
19
|
+
export function nextCommentId(comments) {
|
|
20
|
+
const taken = new Set(comments.map((c) => c.id));
|
|
21
|
+
let n = 1;
|
|
22
|
+
while (taken.has(`c${n}`))
|
|
23
|
+
n += 1;
|
|
24
|
+
return `c${n}`;
|
|
25
|
+
}
|
package/dist/dates.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isIsoDate(s: unknown): s is string;
|
package/dist/dates.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Calendar dates in the model are `YYYY-MM-DD` strings: readable in a diff,
|
|
2
|
+
* sortable as text, and free of the timezone a `Date` would smuggle in. Parsed
|
|
3
|
+
* as UTC so "2026-02-30" is caught by the round trip (Date.parse would roll it
|
|
4
|
+
* to March 2nd) rather than accepted. Shared by comments (`at`) today and by the
|
|
5
|
+
* plan notation's zones and events next. */
|
|
6
|
+
const ISO_DAY = /^\d{4}-\d{2}-\d{2}$/;
|
|
7
|
+
export function isIsoDate(s) {
|
|
8
|
+
if (typeof s !== 'string' || !ISO_DAY.test(s))
|
|
9
|
+
return false;
|
|
10
|
+
const t = Date.parse(`${s}T00:00:00Z`);
|
|
11
|
+
return !Number.isNaN(t) && new Date(t).toISOString().slice(0, 10) === s;
|
|
12
|
+
}
|
package/dist/eject.js
CHANGED
|
@@ -85,7 +85,7 @@ function quoted(s) {
|
|
|
85
85
|
const NODE_OPT_KEYS = [
|
|
86
86
|
'type', 'name', 'icon', 'shape', 'image', 'color', 'textColor', 'technology',
|
|
87
87
|
'link', 'description', 'rich', 'textAlign', 'fontScale', 'metadata', 'key', 'include',
|
|
88
|
-
'includePlane', 'includePlanes', 'plane', 'layer', 'columns', 'threats',
|
|
88
|
+
'includePlane', 'includePlanes', 'plane', 'layer', 'columns', 'threats', 'comments', 'links',
|
|
89
89
|
];
|
|
90
90
|
// Drift guard: a field added to DiagramNode without a matching entry above
|
|
91
91
|
// fails this line at `pnpm typecheck` — a future model field silently
|
|
@@ -98,7 +98,7 @@ void _nodeOptCoverage;
|
|
|
98
98
|
* — mirrors the interface declaration in builder.ts. */
|
|
99
99
|
const RELATE_OPT_KEYS = [
|
|
100
100
|
'label', 'labels', 'style', 'description', 'layer', 'polarity', 'delay',
|
|
101
|
-
'fromColumn', 'toColumn', 'threats',
|
|
101
|
+
'fromColumn', 'toColumn', 'threats', 'comments',
|
|
102
102
|
];
|
|
103
103
|
// Drift guard, same shape as _nodeOptCoverage above. `id` and `kind` are
|
|
104
104
|
// emitted explicitly ahead of the opts object; `from`/`to` are the node refs
|
package/dist/index.d.ts
CHANGED
|
@@ -2,10 +2,12 @@ export declare const CORE_VERSION = 1;
|
|
|
2
2
|
export { ejectSource } from './eject.js';
|
|
3
3
|
export * from './types.js';
|
|
4
4
|
export * from './mutate.js';
|
|
5
|
+
export { commentsOf, hasNoteContent, nextCommentId, type ElementTarget } from './comments.js';
|
|
6
|
+
export { isIsoDate } from './dates.js';
|
|
5
7
|
export { normalizeRuns, runsToPlainText } from './text.js';
|
|
6
8
|
export { relationLabels } from './labels.js';
|
|
7
9
|
export { defaultLayoutDirection, type LayoutDirection } from './layout-defaults.js';
|
|
8
|
-
export { model, ModelBuilder, NodeRef, BranchRef, CommitRef, GitGraphBuilder, ActivityBuilder, ActivityScope, LaneRef, RegionRef, ConsequenceRef, SecondOrderBuilder, FishboneBuilder, CategoryRef, CauseRef, ThreatModelBuilder, FlowRef, type NodeOpts, type RelateOpts, type CommitOpts, type StageOpts, type MergeOpts, type ActivityElementOpts, type ConsequenceOpts, type FishboneOpts, type ThreatOpts, type ElementOpts, } from './builder.js';
|
|
10
|
+
export { model, ModelBuilder, NodeRef, BranchRef, CommitRef, GitGraphBuilder, ActivityBuilder, ActivityScope, LaneRef, RegionRef, ConsequenceRef, SecondOrderBuilder, FishboneBuilder, CategoryRef, CauseRef, ThreatModelBuilder, FlowRef, PlanBuilder, ZoneBuilder, type NodeOpts, type RelateOpts, type CommitOpts, type StageOpts, type MergeOpts, type ActivityElementOpts, type ConsequenceOpts, type FishboneOpts, type ThreatOpts, type CommentOpts, type ElementOpts, type ZoneOpts, type EventOpts, } from './builder.js';
|
|
9
11
|
export { validate, DiagramValidationError, IMAGE_REF, LIBRARY_IMAGE_REF, type ValidationIssue } from './validate.js';
|
|
10
12
|
export { isDrawings, isLayoutOverlay } from './guards.js';
|
|
11
13
|
export { addStroke, deleteStroke, emptyDrawings, pruneDrawingsPlane, uniqueStrokeId } from './drawings.js';
|
|
@@ -23,3 +25,4 @@ export { GIT_KINDS, GIT_NOTATION, GIT_STAGE_TYPE, gapOf, gitGraph, isGitKind, la
|
|
|
23
25
|
export { SECOND_ORDER_NOTATION, SO_CONSEQUENCE_TYPES, SO_DECISION_TYPE, SO_LEADS_TO_KIND, consequenceOrders, consequenceTypeOf, isSecondOrderNode, valenceOf, type ConsequenceOrders, type Valence, } from './second-order.js';
|
|
24
26
|
export { FB_CATEGORY_TYPE, FB_CAUSE_OF_KIND, FB_CAUSE_TYPE, FB_EFFECT_TYPE, FISHBONE_NOTATION, FISHBONE_PRESET_NAMES, FISHBONE_PRESETS, FISHBONE_TYPES, fishboneParents, fishboneTree, isFishboneNode, presetId, type FishboneCategory, type FishboneCause, type FishbonePreset, type FishboneTree, } from './fishbone.js';
|
|
25
27
|
export { TM_NOTATION, TM_ENTITY_TYPE, TM_PROCESS_TYPE, TM_STORE_TYPE, TM_BOUNDARY_TYPE, TM_FLOW_KIND, TM_TYPES, STRIDE_NAMES, NEW_THREAT_TITLE, isThreatModelNode, isOpen, strideFor, boundaryOf, boundaryName, crossings, crossingLabel, threatRegister, threatSummary, threatTargetKey, threatsOf, nextThreatId, nextThreatStatus, allNotesOpen, type ThreatTarget, type Crossing, type ThreatRow, } from './threat-model.js';
|
|
28
|
+
export { PLAN_NOTATION, PLAN_ZONE_TYPE, PLAN_EVENT_TYPE, PLAN_PERSON_TYPE, PLAN_TEAM_TYPE, PLAN_ACTOR_TYPES, PLAN_TYPES, PLAN_ROLES, isPlanZone, isPlanEvent, isPlanActor, isPlanRole, dayOf, isoOf, spanOf, atOf, rolesOf, planGraph, planSubtree, type PlanRole, type PlanSpan, type PlanRoles, type PlanGraph, type PlanChildren, } from './plan.js';
|
package/dist/index.js
CHANGED
|
@@ -23,10 +23,12 @@ export const CORE_VERSION = 1;
|
|
|
23
23
|
export { ejectSource } from './eject.js';
|
|
24
24
|
export * from './types.js';
|
|
25
25
|
export * from './mutate.js';
|
|
26
|
+
export { commentsOf, hasNoteContent, nextCommentId } from './comments.js';
|
|
27
|
+
export { isIsoDate } from './dates.js';
|
|
26
28
|
export { normalizeRuns, runsToPlainText } from './text.js';
|
|
27
29
|
export { relationLabels } from './labels.js';
|
|
28
30
|
export { defaultLayoutDirection } from './layout-defaults.js';
|
|
29
|
-
export { model, ModelBuilder, NodeRef, BranchRef, CommitRef, GitGraphBuilder, ActivityBuilder, ActivityScope, LaneRef, RegionRef, ConsequenceRef, SecondOrderBuilder, FishboneBuilder, CategoryRef, CauseRef, ThreatModelBuilder, FlowRef, } from './builder.js';
|
|
31
|
+
export { model, ModelBuilder, NodeRef, BranchRef, CommitRef, GitGraphBuilder, ActivityBuilder, ActivityScope, LaneRef, RegionRef, ConsequenceRef, SecondOrderBuilder, FishboneBuilder, CategoryRef, CauseRef, ThreatModelBuilder, FlowRef, PlanBuilder, ZoneBuilder, } from './builder.js';
|
|
30
32
|
export { validate, DiagramValidationError, IMAGE_REF, LIBRARY_IMAGE_REF } from './validate.js';
|
|
31
33
|
export { isDrawings, isLayoutOverlay } from './guards.js';
|
|
32
34
|
export { addStroke, deleteStroke, emptyDrawings, pruneDrawingsPlane, uniqueStrokeId } from './drawings.js';
|
|
@@ -43,3 +45,4 @@ export { GIT_KINDS, GIT_NOTATION, GIT_STAGE_TYPE, gapOf, gitGraph, isGitKind, la
|
|
|
43
45
|
export { SECOND_ORDER_NOTATION, SO_CONSEQUENCE_TYPES, SO_DECISION_TYPE, SO_LEADS_TO_KIND, consequenceOrders, consequenceTypeOf, isSecondOrderNode, valenceOf, } from './second-order.js';
|
|
44
46
|
export { FB_CATEGORY_TYPE, FB_CAUSE_OF_KIND, FB_CAUSE_TYPE, FB_EFFECT_TYPE, FISHBONE_NOTATION, FISHBONE_PRESET_NAMES, FISHBONE_PRESETS, FISHBONE_TYPES, fishboneParents, fishboneTree, isFishboneNode, presetId, } from './fishbone.js';
|
|
45
47
|
export { TM_NOTATION, TM_ENTITY_TYPE, TM_PROCESS_TYPE, TM_STORE_TYPE, TM_BOUNDARY_TYPE, TM_FLOW_KIND, TM_TYPES, STRIDE_NAMES, NEW_THREAT_TITLE, isThreatModelNode, isOpen, strideFor, boundaryOf, boundaryName, crossings, crossingLabel, threatRegister, threatSummary, threatTargetKey, threatsOf, nextThreatId, nextThreatStatus, allNotesOpen, } from './threat-model.js';
|
|
48
|
+
export { PLAN_NOTATION, PLAN_ZONE_TYPE, PLAN_EVENT_TYPE, PLAN_PERSON_TYPE, PLAN_TEAM_TYPE, PLAN_ACTOR_TYPES, PLAN_TYPES, PLAN_ROLES, isPlanZone, isPlanEvent, isPlanActor, isPlanRole, dayOf, isoOf, spanOf, atOf, rolesOf, planGraph, planSubtree, } from './plan.js';
|