@diagc/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +709 -0
- package/README.md +27 -0
- package/dist/builder.d.ts +381 -0
- package/dist/builder.js +590 -0
- package/dist/children.d.ts +21 -0
- package/dist/children.js +45 -0
- package/dist/commands.d.ts +219 -0
- package/dist/commands.js +474 -0
- package/dist/compose.d.ts +19 -0
- package/dist/compose.js +246 -0
- package/dist/drawings.d.ts +13 -0
- package/dist/drawings.js +36 -0
- package/dist/eject.d.ts +20 -0
- package/dist/eject.js +260 -0
- package/dist/fishbone.d.ts +66 -0
- package/dist/fishbone.js +95 -0
- package/dist/git.d.ts +65 -0
- package/dist/git.js +159 -0
- package/dist/guards.d.ts +10 -0
- package/dist/guards.js +98 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +45 -0
- package/dist/labels.d.ts +5 -0
- package/dist/labels.js +10 -0
- package/dist/layout-defaults.d.ts +20 -0
- package/dist/layout-defaults.js +20 -0
- package/dist/mutate.d.ts +106 -0
- package/dist/mutate.js +547 -0
- package/dist/second-order.d.ts +39 -0
- package/dist/second-order.js +86 -0
- package/dist/text.d.ts +5 -0
- package/dist/text.js +25 -0
- package/dist/threat-model.d.ts +88 -0
- package/dist/threat-model.js +188 -0
- package/dist/types.d.ts +395 -0
- package/dist/types.js +27 -0
- package/dist/util.d.ts +11 -0
- package/dist/util.js +13 -0
- package/dist/validate.d.ts +19 -0
- package/dist/validate.js +736 -0
- package/dist/view/compile.d.ts +29 -0
- package/dist/view/compile.js +78 -0
- package/dist/view/edges.d.ts +4 -0
- package/dist/view/edges.js +118 -0
- package/dist/view/hierarchy.d.ts +41 -0
- package/dist/view/hierarchy.js +103 -0
- package/dist/view/layers.d.ts +7 -0
- package/dist/view/layers.js +17 -0
- package/dist/view/lod.d.ts +15 -0
- package/dist/view/lod.js +17 -0
- package/dist/view/scope.d.ts +23 -0
- package/dist/view/scope.js +106 -0
- package/dist/view/size.d.ts +8 -0
- package/dist/view/size.js +34 -0
- package/dist/view/tree.d.ts +12 -0
- package/dist/view/tree.js +147 -0
- package/dist/view/types.d.ts +68 -0
- package/dist/view/types.js +1 -0
- package/package.json +38 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
export function buildViewTree(m, h, lod) {
|
|
2
|
+
const stateOf = (id) => (h.childrenOf.get(id) ?? []).length === 0 ? 'leaf' : (lod[id] ?? 'collapsed');
|
|
3
|
+
const visible = new Set();
|
|
4
|
+
const hostOf = new Map();
|
|
5
|
+
const promoted = new Set();
|
|
6
|
+
for (const r of h.roots) {
|
|
7
|
+
visible.add(r);
|
|
8
|
+
hostOf.set(r, null);
|
|
9
|
+
}
|
|
10
|
+
const absorbersOf = (id, memo) => {
|
|
11
|
+
const hit = memo.get(id);
|
|
12
|
+
if (hit)
|
|
13
|
+
return hit;
|
|
14
|
+
const acc = new Set();
|
|
15
|
+
memo.set(id, acc); // pre-set guards diamond re-entry (containment is a DAG)
|
|
16
|
+
for (const p of h.parentsOf.get(id) ?? []) {
|
|
17
|
+
if (visible.has(p))
|
|
18
|
+
acc.add(p);
|
|
19
|
+
else
|
|
20
|
+
for (const a of absorbersOf(p, memo))
|
|
21
|
+
acc.add(a);
|
|
22
|
+
}
|
|
23
|
+
return acc;
|
|
24
|
+
};
|
|
25
|
+
const commonHost = (absorbers) => {
|
|
26
|
+
const chains = [...absorbers].map((a) => {
|
|
27
|
+
const chain = [];
|
|
28
|
+
let cur = hostOf.get(a) ?? null;
|
|
29
|
+
while (cur !== null) {
|
|
30
|
+
chain.push(cur);
|
|
31
|
+
cur = hostOf.get(cur) ?? null;
|
|
32
|
+
}
|
|
33
|
+
return chain;
|
|
34
|
+
});
|
|
35
|
+
const [first, ...rest] = chains;
|
|
36
|
+
for (const cand of first ?? []) {
|
|
37
|
+
if (rest.every((c) => c.includes(cand)))
|
|
38
|
+
return cand;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
};
|
|
42
|
+
// Reveal children of visible expanded nodes; then promote hidden shared
|
|
43
|
+
// nodes spanning >=2 visible groups; repeat until stable. Within a round,
|
|
44
|
+
// only "membership-maximal" candidates promote: a candidate with a hidden
|
|
45
|
+
// ancestor that is itself a candidate waits — once the ancestor is visible,
|
|
46
|
+
// the descendant usually resolves to a single absorber (it lives inside the
|
|
47
|
+
// promoted container) instead of being wrongly promoted next to it.
|
|
48
|
+
for (;;) {
|
|
49
|
+
let placed = true;
|
|
50
|
+
while (placed) {
|
|
51
|
+
placed = false;
|
|
52
|
+
for (const n of m.nodes) {
|
|
53
|
+
if (visible.has(n.id))
|
|
54
|
+
continue;
|
|
55
|
+
const host = (h.parentsOf.get(n.id) ?? []).find((p) => visible.has(p) && stateOf(p) === 'expanded');
|
|
56
|
+
if (host !== undefined) {
|
|
57
|
+
visible.add(n.id);
|
|
58
|
+
hostOf.set(n.id, host);
|
|
59
|
+
placed = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const memo = new Map();
|
|
64
|
+
const candidates = m.nodes.filter((n) => !visible.has(n.id) && absorbersOf(n.id, memo).size >= 2);
|
|
65
|
+
if (candidates.length === 0)
|
|
66
|
+
break;
|
|
67
|
+
const candidateIds = new Set(candidates.map((n) => n.id));
|
|
68
|
+
const hasCandidateAncestor = (id) => {
|
|
69
|
+
const seen = new Set();
|
|
70
|
+
const stack = [...(h.parentsOf.get(id) ?? [])];
|
|
71
|
+
while (stack.length > 0) {
|
|
72
|
+
const p = stack.pop();
|
|
73
|
+
if (p === undefined || seen.has(p))
|
|
74
|
+
continue;
|
|
75
|
+
seen.add(p);
|
|
76
|
+
if (candidateIds.has(p))
|
|
77
|
+
return true;
|
|
78
|
+
if (!visible.has(p))
|
|
79
|
+
stack.push(...(h.parentsOf.get(p) ?? []));
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
};
|
|
83
|
+
// A DAG always has a maximal candidate, so this round promotes >=1 node.
|
|
84
|
+
for (const n of candidates.filter((c) => !hasCandidateAncestor(c.id))) {
|
|
85
|
+
promoted.add(n.id);
|
|
86
|
+
visible.add(n.id);
|
|
87
|
+
hostOf.set(n.id, commonHost(absorbersOf(n.id, memo)));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Anchors for hidden nodes (unique by construction: >=2 would have promoted).
|
|
91
|
+
const anchorOf = new Map();
|
|
92
|
+
const finalMemo = new Map();
|
|
93
|
+
for (const n of m.nodes) {
|
|
94
|
+
if (visible.has(n.id))
|
|
95
|
+
continue;
|
|
96
|
+
const [anchor] = absorbersOf(n.id, finalMemo);
|
|
97
|
+
if (anchor !== undefined)
|
|
98
|
+
anchorOf.set(n.id, anchor);
|
|
99
|
+
}
|
|
100
|
+
// Assemble ViewNodes in model order.
|
|
101
|
+
const byId = new Map();
|
|
102
|
+
for (const n of m.nodes) {
|
|
103
|
+
if (!visible.has(n.id))
|
|
104
|
+
continue;
|
|
105
|
+
byId.set(n.id, {
|
|
106
|
+
id: n.id,
|
|
107
|
+
node: n,
|
|
108
|
+
state: stateOf(n.id),
|
|
109
|
+
children: [],
|
|
110
|
+
promoted: promoted.has(n.id),
|
|
111
|
+
sharedMembers: [],
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
// Two passes so promoted nodes land AFTER normally-placed siblings within
|
|
115
|
+
// each roots/children list, each group internally in model order.
|
|
116
|
+
const roots = [];
|
|
117
|
+
const placeInto = (vn) => {
|
|
118
|
+
const host = hostOf.get(vn.id) ?? null;
|
|
119
|
+
if (host === null)
|
|
120
|
+
roots.push(vn);
|
|
121
|
+
else
|
|
122
|
+
byId.get(host)?.children.push(vn);
|
|
123
|
+
};
|
|
124
|
+
for (const n of m.nodes) {
|
|
125
|
+
const vn = byId.get(n.id);
|
|
126
|
+
if (vn && !promoted.has(n.id))
|
|
127
|
+
placeInto(vn);
|
|
128
|
+
}
|
|
129
|
+
for (const n of m.nodes) {
|
|
130
|
+
const vn = byId.get(n.id);
|
|
131
|
+
if (vn && promoted.has(n.id))
|
|
132
|
+
placeInto(vn);
|
|
133
|
+
}
|
|
134
|
+
// Shared markers: visible parents that are not the placement host.
|
|
135
|
+
for (const n of m.nodes) {
|
|
136
|
+
if (!byId.has(n.id))
|
|
137
|
+
continue;
|
|
138
|
+
const parents = h.parentsOf.get(n.id) ?? [];
|
|
139
|
+
if (parents.length < 2)
|
|
140
|
+
continue;
|
|
141
|
+
for (const p of parents) {
|
|
142
|
+
if (p !== hostOf.get(n.id) && byId.has(p))
|
|
143
|
+
byId.get(p)?.sharedMembers.push(n.id);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { roots, byId, anchorOf, hostOf };
|
|
147
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { DiagramNode, DiagramRelation, EdgeLabel, Polarity, RelationStyle } from '../types.js';
|
|
2
|
+
export type NodeViewState = 'leaf' | 'expanded' | 'collapsed';
|
|
3
|
+
/** Collapse decisions for container nodes only; leaves never appear. */
|
|
4
|
+
export type LodState = Record<string, 'expanded' | 'collapsed'>;
|
|
5
|
+
export interface ViewportState {
|
|
6
|
+
/** active plane id; absent = the model's first-declared plane (or the implicit one) */
|
|
7
|
+
plane?: string;
|
|
8
|
+
/** container ids the viewer is zoomed into (renderer-computed focus chain) */
|
|
9
|
+
focus?: string[];
|
|
10
|
+
pins?: Record<string, 'expanded' | 'collapsed'>;
|
|
11
|
+
/** layer ids to draw. ABSENT = the host has no opinion, so the active plane's
|
|
12
|
+
* `layers` presets apply; PRESENT = the host's own choice, which replaces the
|
|
13
|
+
* presets entirely — an empty array therefore means "no layers at all", not
|
|
14
|
+
* "presets only". A host with a layer switch seeds its state from the plane's
|
|
15
|
+
* `layers` (on load and on every plane change) and owns it from then on. */
|
|
16
|
+
activeLayers?: string[];
|
|
17
|
+
/** nested-zoom drill root: when set, the view is scoped to this node's INTERIOR
|
|
18
|
+
* — its children become the top-level content (the node itself isn't drawn) and
|
|
19
|
+
* everything outside its subtree disappears. Relations crossing the boundary are
|
|
20
|
+
* re-pointed to an external stub node (see ViewNode.external). */
|
|
21
|
+
root?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface Size {
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
}
|
|
27
|
+
export interface ViewNode {
|
|
28
|
+
id: string;
|
|
29
|
+
node: DiagramNode;
|
|
30
|
+
state: NodeViewState;
|
|
31
|
+
children: ViewNode[];
|
|
32
|
+
promoted: boolean;
|
|
33
|
+
/** Ids of shared nodes contained here but rendered elsewhere. */
|
|
34
|
+
sharedMembers: string[];
|
|
35
|
+
/** Set on a drill-view stub node: the id of the real (off-frame) node it stands
|
|
36
|
+
* in for — an edge to something outside the current drill root. Clicking it
|
|
37
|
+
* navigates into that node. */
|
|
38
|
+
external?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface ViewEdge {
|
|
41
|
+
id: string;
|
|
42
|
+
from: string;
|
|
43
|
+
to: string;
|
|
44
|
+
kind: string;
|
|
45
|
+
label?: string;
|
|
46
|
+
/** the sole constituent's positioned labels; aggregated edges have none */
|
|
47
|
+
labels?: EdgeLabel[];
|
|
48
|
+
layer?: string;
|
|
49
|
+
tint?: string;
|
|
50
|
+
/** the sole constituent's per-relation style; aggregated edges have none */
|
|
51
|
+
style?: RelationStyle;
|
|
52
|
+
/** CLD polarity: a single constituent's sign, or an aggregate's shared sign when every constituent agrees; absent when constituents disagree or any lacks a sign */
|
|
53
|
+
polarity?: Polarity;
|
|
54
|
+
/** the sole constituent's CLD delay marker; aggregated edges have none */
|
|
55
|
+
delay?: boolean;
|
|
56
|
+
constituents: DiagramRelation[];
|
|
57
|
+
}
|
|
58
|
+
export interface CompiledView {
|
|
59
|
+
roots: ViewNode[];
|
|
60
|
+
/** the arrows to DRAW (filtered by active layers / plane) */
|
|
61
|
+
edges: ViewEdge[];
|
|
62
|
+
/** the arrows to LAYOUT BY (all layers + base) — stable across overlay
|
|
63
|
+
* toggles and structure-sharing planes, so boxes never jump */
|
|
64
|
+
layoutEdges: ViewEdge[];
|
|
65
|
+
lod: LodState;
|
|
66
|
+
/** drill view only: stub node id -> the real off-frame node it represents. */
|
|
67
|
+
externals?: Map<string, string>;
|
|
68
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@diagc/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Diagram model, validator, and builder DSL for diagc — no React, no filesystem.",
|
|
5
|
+
"license": "AGPL-3.0-only",
|
|
6
|
+
"author": "Bogdan Frankovskyi",
|
|
7
|
+
"homepage": "https://github.com/Ferroman/diagc#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Ferroman/diagc.git",
|
|
11
|
+
"directory": "packages/core"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"diagram",
|
|
15
|
+
"architecture",
|
|
16
|
+
"c4",
|
|
17
|
+
"causal-loop",
|
|
18
|
+
"dsl"
|
|
19
|
+
],
|
|
20
|
+
"type": "module",
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=22"
|
|
23
|
+
},
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"default": "./dist/index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {},
|
|
37
|
+
"types": "./dist/index.d.ts"
|
|
38
|
+
}
|