@diagrammo/dgmo 0.4.2 → 0.4.3
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/.claude/skills/dgmo-chart/SKILL.md +28 -0
- package/.claude/skills/dgmo-generate/SKILL.md +1 -0
- package/.claude/skills/dgmo-sequence/SKILL.md +24 -1
- package/.cursorrules +27 -2
- package/.github/copilot-instructions.md +36 -3
- package/.windsurfrules +27 -2
- package/README.md +12 -3
- package/dist/cli.cjs +197 -154
- package/dist/index.cjs +8371 -3200
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +502 -58
- package/dist/index.d.ts +502 -58
- package/dist/index.js +8594 -3444
- package/dist/index.js.map +1 -1
- package/docs/ai-integration.md +1 -1
- package/docs/language-reference.md +336 -17
- package/docs/migration-sequence-color-to-tags.md +98 -0
- package/package.json +1 -1
- package/src/c4/renderer.ts +1 -20
- package/src/class/renderer.ts +1 -11
- package/src/cli.ts +40 -0
- package/src/d3.ts +92 -2
- package/src/dgmo-router.ts +11 -0
- package/src/echarts.ts +74 -8
- package/src/er/parser.ts +29 -3
- package/src/er/renderer.ts +1 -15
- package/src/graph/flowchart-parser.ts +7 -30
- package/src/graph/flowchart-renderer.ts +62 -69
- package/src/graph/layout.ts +5 -0
- package/src/graph/state-parser.ts +388 -0
- package/src/graph/state-renderer.ts +496 -0
- package/src/graph/types.ts +4 -2
- package/src/index.ts +42 -1
- package/src/infra/compute.ts +1113 -0
- package/src/infra/layout.ts +575 -0
- package/src/infra/parser.ts +559 -0
- package/src/infra/renderer.ts +1509 -0
- package/src/infra/roles.ts +60 -0
- package/src/infra/serialize.ts +67 -0
- package/src/infra/types.ts +221 -0
- package/src/infra/validation.ts +192 -0
- package/src/initiative-status/layout.ts +56 -61
- package/src/initiative-status/renderer.ts +13 -13
- package/src/kanban/renderer.ts +1 -24
- package/src/org/layout.ts +28 -37
- package/src/org/parser.ts +16 -1
- package/src/org/renderer.ts +159 -121
- package/src/org/resolver.ts +90 -23
- package/src/palettes/color-utils.ts +30 -0
- package/src/render.ts +2 -0
- package/src/sequence/parser.ts +202 -42
- package/src/sequence/renderer.ts +576 -113
- package/src/sequence/tag-resolution.ts +163 -0
- package/src/sitemap/collapse.ts +187 -0
- package/src/sitemap/layout.ts +738 -0
- package/src/sitemap/parser.ts +489 -0
- package/src/sitemap/renderer.ts +774 -0
- package/src/sitemap/types.ts +42 -0
- package/src/utils/tag-groups.ts +119 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Sequence Diagram Tag Resolution
|
|
3
|
+
// ============================================================
|
|
4
|
+
//
|
|
5
|
+
// Resolves effective tag values for participants and messages
|
|
6
|
+
// using the priority chain: explicit > group > receiver-inherit > default > neutral
|
|
7
|
+
|
|
8
|
+
import type { TagGroup } from '../utils/tag-groups';
|
|
9
|
+
import type {
|
|
10
|
+
ParsedSequenceDgmo,
|
|
11
|
+
SequenceParticipant,
|
|
12
|
+
SequenceMessage,
|
|
13
|
+
SequenceGroup,
|
|
14
|
+
} from './parser';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolved tag values for all sequence elements.
|
|
18
|
+
* Used by the renderer to look up colors via tag group entries.
|
|
19
|
+
*/
|
|
20
|
+
export interface ResolvedTagMap {
|
|
21
|
+
/** participantId → resolved tag value for the active group */
|
|
22
|
+
participants: Map<string, string | undefined>;
|
|
23
|
+
/** message lineNumber → tag value from explicit metadata */
|
|
24
|
+
messages: Map<number, string | undefined>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Propagate group-level tag metadata to contained participants.
|
|
29
|
+
* Only sets values for keys not already present on the participant.
|
|
30
|
+
*
|
|
31
|
+
* Mutates `participantMeta` in-place.
|
|
32
|
+
*/
|
|
33
|
+
export function propagateGroupTags(
|
|
34
|
+
participantMeta: Map<string, Record<string, string>>,
|
|
35
|
+
groups: ReadonlyArray<SequenceGroup>,
|
|
36
|
+
): void {
|
|
37
|
+
for (const group of groups) {
|
|
38
|
+
if (!group.metadata) continue;
|
|
39
|
+
for (const id of group.participantIds) {
|
|
40
|
+
const meta = participantMeta.get(id);
|
|
41
|
+
if (!meta) continue;
|
|
42
|
+
for (const [key, value] of Object.entries(group.metadata)) {
|
|
43
|
+
if (!(key in meta)) {
|
|
44
|
+
meta[key] = value;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Compute receiver inheritance for a specific tag group key.
|
|
53
|
+
*
|
|
54
|
+
* For each participant without an explicit value for `groupKey`,
|
|
55
|
+
* collect values from all incoming tagged messages. If exactly one
|
|
56
|
+
* unique value exists, the participant inherits it.
|
|
57
|
+
*
|
|
58
|
+
* Returns a map of participantId → inherited value.
|
|
59
|
+
*/
|
|
60
|
+
export function computeReceiverInheritance(
|
|
61
|
+
participants: ReadonlyArray<SequenceParticipant>,
|
|
62
|
+
messages: ReadonlyArray<SequenceMessage>,
|
|
63
|
+
groupKey: string,
|
|
64
|
+
participantMeta: Map<string, Record<string, string>>,
|
|
65
|
+
): Map<string, string> {
|
|
66
|
+
const inheritance = new Map<string, string>();
|
|
67
|
+
|
|
68
|
+
for (const p of participants) {
|
|
69
|
+
const meta = participantMeta.get(p.id);
|
|
70
|
+
// Skip if already has a value for this group key
|
|
71
|
+
if (meta?.[groupKey]) continue;
|
|
72
|
+
|
|
73
|
+
const incomingValues = new Set<string>();
|
|
74
|
+
for (const msg of messages) {
|
|
75
|
+
if (msg.to === p.id && msg.metadata?.[groupKey]) {
|
|
76
|
+
incomingValues.add(msg.metadata[groupKey]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (incomingValues.size === 1) {
|
|
81
|
+
inheritance.set(p.id, [...incomingValues][0]);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return inheritance;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Full tag resolution for sequence diagrams.
|
|
90
|
+
*
|
|
91
|
+
* Priority: explicit metadata > group propagation > receiver inheritance > default > neutral
|
|
92
|
+
*
|
|
93
|
+
* Pure function — does not mutate `parsed`.
|
|
94
|
+
*/
|
|
95
|
+
export function resolveSequenceTags(
|
|
96
|
+
parsed: ParsedSequenceDgmo,
|
|
97
|
+
activeTagGroup: string,
|
|
98
|
+
): ResolvedTagMap {
|
|
99
|
+
const result: ResolvedTagMap = {
|
|
100
|
+
participants: new Map(),
|
|
101
|
+
messages: new Map(),
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Find the active tag group
|
|
105
|
+
const group = parsed.tagGroups.find(
|
|
106
|
+
(g) => g.name.toLowerCase() === activeTagGroup.toLowerCase(),
|
|
107
|
+
);
|
|
108
|
+
if (!group) {
|
|
109
|
+
// No matching group — all neutral
|
|
110
|
+
for (const p of parsed.participants) {
|
|
111
|
+
result.participants.set(p.id, undefined);
|
|
112
|
+
}
|
|
113
|
+
for (const m of parsed.messages) {
|
|
114
|
+
result.messages.set(m.lineNumber, undefined);
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const groupKey = group.name.toLowerCase();
|
|
120
|
+
|
|
121
|
+
// Clone participant metadata (don't mutate parsed data)
|
|
122
|
+
const participantMeta = new Map<string, Record<string, string>>();
|
|
123
|
+
for (const p of parsed.participants) {
|
|
124
|
+
participantMeta.set(p.id, { ...(p.metadata || {}) });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Step 1: Group propagation
|
|
128
|
+
propagateGroupTags(participantMeta, parsed.groups);
|
|
129
|
+
|
|
130
|
+
// Step 2: Receiver inheritance
|
|
131
|
+
const inherited = computeReceiverInheritance(
|
|
132
|
+
parsed.participants,
|
|
133
|
+
parsed.messages,
|
|
134
|
+
groupKey,
|
|
135
|
+
participantMeta,
|
|
136
|
+
);
|
|
137
|
+
for (const [id, value] of inherited) {
|
|
138
|
+
const meta = participantMeta.get(id)!;
|
|
139
|
+
meta[groupKey] = value;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Step 3: Default value injection
|
|
143
|
+
if (group.defaultValue) {
|
|
144
|
+
for (const p of parsed.participants) {
|
|
145
|
+
const meta = participantMeta.get(p.id)!;
|
|
146
|
+
if (!(groupKey in meta)) {
|
|
147
|
+
meta[groupKey] = group.defaultValue;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Build result maps
|
|
153
|
+
for (const p of parsed.participants) {
|
|
154
|
+
const meta = participantMeta.get(p.id)!;
|
|
155
|
+
result.participants.set(p.id, meta[groupKey] || undefined);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
for (const msg of parsed.messages) {
|
|
159
|
+
result.messages.set(msg.lineNumber, msg.metadata?.[groupKey] || undefined);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Sitemap Collapse/Expand — prune subtrees + re-terminate arrows
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
import type { SitemapNode, SitemapEdge, ParsedSitemap } from './types';
|
|
6
|
+
|
|
7
|
+
// ============================================================
|
|
8
|
+
// Types
|
|
9
|
+
// ============================================================
|
|
10
|
+
|
|
11
|
+
export interface CollapsedSitemapResult {
|
|
12
|
+
/** ParsedSitemap with collapsed subtrees pruned (deep-cloned, never mutates original) */
|
|
13
|
+
parsed: ParsedSitemap;
|
|
14
|
+
/** nodeId → count of hidden descendants */
|
|
15
|
+
hiddenCounts: Map<string, number>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ============================================================
|
|
19
|
+
// Helpers
|
|
20
|
+
// ============================================================
|
|
21
|
+
|
|
22
|
+
function cloneNode(node: SitemapNode): SitemapNode {
|
|
23
|
+
return {
|
|
24
|
+
id: node.id,
|
|
25
|
+
label: node.label,
|
|
26
|
+
metadata: { ...node.metadata },
|
|
27
|
+
children: node.children.map(cloneNode),
|
|
28
|
+
parentId: node.parentId,
|
|
29
|
+
isContainer: node.isContainer,
|
|
30
|
+
lineNumber: node.lineNumber,
|
|
31
|
+
color: node.color,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function countDescendants(node: SitemapNode): number {
|
|
36
|
+
let count = 0;
|
|
37
|
+
for (const child of node.children) {
|
|
38
|
+
count += (child.isContainer ? 0 : 1) + countDescendants(child);
|
|
39
|
+
}
|
|
40
|
+
return count;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Compute hidden counts from the ORIGINAL (unpruned) tree. */
|
|
44
|
+
function computeHiddenCounts(
|
|
45
|
+
nodes: SitemapNode[],
|
|
46
|
+
collapsedIds: Set<string>,
|
|
47
|
+
hiddenCounts: Map<string, number>,
|
|
48
|
+
): void {
|
|
49
|
+
for (const node of nodes) {
|
|
50
|
+
if (collapsedIds.has(node.id) && node.children.length > 0) {
|
|
51
|
+
hiddenCounts.set(node.id, countDescendants(node));
|
|
52
|
+
}
|
|
53
|
+
computeHiddenCounts(node.children, collapsedIds, hiddenCounts);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Remove children of collapsed nodes on the cloned tree. */
|
|
58
|
+
function pruneCollapsed(node: SitemapNode, collapsedIds: Set<string>): void {
|
|
59
|
+
for (const child of node.children) {
|
|
60
|
+
pruneCollapsed(child, collapsedIds);
|
|
61
|
+
}
|
|
62
|
+
if (collapsedIds.has(node.id) && node.children.length > 0) {
|
|
63
|
+
node.children = [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Collect all node IDs reachable in a tree. */
|
|
68
|
+
function collectNodeIds(nodes: SitemapNode[], ids: Set<string>): void {
|
|
69
|
+
for (const node of nodes) {
|
|
70
|
+
ids.add(node.id);
|
|
71
|
+
collectNodeIds(node.children, ids);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Find the outermost visible ancestor for a hidden node.
|
|
77
|
+
* Walk up the original tree to find which collapsed container should absorb the arrow.
|
|
78
|
+
*/
|
|
79
|
+
function findVisibleAncestor(
|
|
80
|
+
nodeId: string,
|
|
81
|
+
parentMap: Map<string, string>,
|
|
82
|
+
visibleIds: Set<string>,
|
|
83
|
+
collapsedIds: Set<string>,
|
|
84
|
+
): string | null {
|
|
85
|
+
let current = nodeId;
|
|
86
|
+
while (true) {
|
|
87
|
+
const parentId = parentMap.get(current);
|
|
88
|
+
if (!parentId) return null;
|
|
89
|
+
// If the parent is visible and is a collapsed container, re-terminate here
|
|
90
|
+
if (visibleIds.has(parentId) && collapsedIds.has(parentId)) {
|
|
91
|
+
return parentId;
|
|
92
|
+
}
|
|
93
|
+
current = parentId;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Build nodeId → parentId map from the original tree. */
|
|
98
|
+
function buildParentMap(nodes: SitemapNode[], map: Map<string, string>): void {
|
|
99
|
+
for (const node of nodes) {
|
|
100
|
+
for (const child of node.children) {
|
|
101
|
+
map.set(child.id, node.id);
|
|
102
|
+
buildParentMap([child], map);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ============================================================
|
|
108
|
+
// Main
|
|
109
|
+
// ============================================================
|
|
110
|
+
|
|
111
|
+
export function collapseSitemapTree(
|
|
112
|
+
original: ParsedSitemap,
|
|
113
|
+
collapsedIds: Set<string>,
|
|
114
|
+
): CollapsedSitemapResult {
|
|
115
|
+
const hiddenCounts = new Map<string, number>();
|
|
116
|
+
|
|
117
|
+
if (collapsedIds.size === 0) {
|
|
118
|
+
return { parsed: original, hiddenCounts };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Compute counts from the ORIGINAL tree before pruning
|
|
122
|
+
computeHiddenCounts(original.roots, collapsedIds, hiddenCounts);
|
|
123
|
+
|
|
124
|
+
// Deep-clone roots and prune collapsed subtrees
|
|
125
|
+
const clonedRoots = original.roots.map(cloneNode);
|
|
126
|
+
for (const root of clonedRoots) {
|
|
127
|
+
pruneCollapsed(root, collapsedIds);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Collect visible node IDs after pruning
|
|
131
|
+
const visibleIds = new Set<string>();
|
|
132
|
+
collectNodeIds(clonedRoots, visibleIds);
|
|
133
|
+
|
|
134
|
+
// Build parent map from the ORIGINAL tree for ancestor lookup
|
|
135
|
+
const parentMap = new Map<string, string>();
|
|
136
|
+
buildParentMap(original.roots, parentMap);
|
|
137
|
+
|
|
138
|
+
// Re-terminate edges that reference hidden nodes.
|
|
139
|
+
// No deduplication — multiple edges between the same collapsed pair are kept
|
|
140
|
+
// so each retains its own label (e.g. "settings" and "billing" both show).
|
|
141
|
+
// Layout uses dagre multigraph to route each edge separately.
|
|
142
|
+
const newEdges: SitemapEdge[] = [];
|
|
143
|
+
|
|
144
|
+
for (const edge of original.edges) {
|
|
145
|
+
let sourceId = edge.sourceId;
|
|
146
|
+
let targetId = edge.targetId;
|
|
147
|
+
|
|
148
|
+
const sourceVisible = visibleIds.has(sourceId);
|
|
149
|
+
const targetVisible = visibleIds.has(targetId);
|
|
150
|
+
|
|
151
|
+
if (sourceVisible && targetVisible) {
|
|
152
|
+
// Both visible — keep as-is
|
|
153
|
+
newEdges.push({ ...edge });
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Re-terminate hidden endpoints
|
|
158
|
+
if (!sourceVisible) {
|
|
159
|
+
const ancestor = findVisibleAncestor(sourceId, parentMap, visibleIds, collapsedIds);
|
|
160
|
+
if (!ancestor) continue; // both endpoints hidden with no visible ancestor
|
|
161
|
+
sourceId = ancestor;
|
|
162
|
+
}
|
|
163
|
+
if (!targetVisible) {
|
|
164
|
+
const ancestor = findVisibleAncestor(targetId, parentMap, visibleIds, collapsedIds);
|
|
165
|
+
if (!ancestor) continue;
|
|
166
|
+
targetId = ancestor;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Remove self-loops (both endpoints re-terminated to same collapsed group)
|
|
170
|
+
if (sourceId === targetId) continue;
|
|
171
|
+
|
|
172
|
+
newEdges.push({
|
|
173
|
+
...edge,
|
|
174
|
+
sourceId,
|
|
175
|
+
targetId,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
parsed: {
|
|
181
|
+
...original,
|
|
182
|
+
roots: clonedRoots,
|
|
183
|
+
edges: newEdges,
|
|
184
|
+
},
|
|
185
|
+
hiddenCounts,
|
|
186
|
+
};
|
|
187
|
+
}
|