omegon 0.6.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.
Files changed (160) hide show
  1. package/.gitattributes +3 -0
  2. package/AGENTS.md +16 -0
  3. package/LICENSE +15 -0
  4. package/README.md +289 -0
  5. package/bin/pi.mjs +30 -0
  6. package/extensions/00-secrets/index.ts +1126 -0
  7. package/extensions/01-auth/auth.ts +401 -0
  8. package/extensions/01-auth/index.ts +289 -0
  9. package/extensions/auto-compact.ts +42 -0
  10. package/extensions/bootstrap/deps.ts +291 -0
  11. package/extensions/bootstrap/index.ts +811 -0
  12. package/extensions/chronos/chronos.sh +487 -0
  13. package/extensions/chronos/index.ts +148 -0
  14. package/extensions/cleave/assessment.ts +754 -0
  15. package/extensions/cleave/bridge.ts +31 -0
  16. package/extensions/cleave/conflicts.ts +250 -0
  17. package/extensions/cleave/dispatcher.ts +808 -0
  18. package/extensions/cleave/guardrails.ts +426 -0
  19. package/extensions/cleave/index.ts +3121 -0
  20. package/extensions/cleave/lifecycle-emitter.ts +20 -0
  21. package/extensions/cleave/openspec.ts +811 -0
  22. package/extensions/cleave/planner.ts +260 -0
  23. package/extensions/cleave/review.ts +579 -0
  24. package/extensions/cleave/skills.ts +355 -0
  25. package/extensions/cleave/types.ts +261 -0
  26. package/extensions/cleave/workspace.ts +861 -0
  27. package/extensions/cleave/worktree.ts +243 -0
  28. package/extensions/core-renderers.ts +253 -0
  29. package/extensions/dashboard/context-gauge.ts +58 -0
  30. package/extensions/dashboard/file-watch.ts +14 -0
  31. package/extensions/dashboard/footer.ts +1145 -0
  32. package/extensions/dashboard/git.ts +185 -0
  33. package/extensions/dashboard/index.ts +478 -0
  34. package/extensions/dashboard/memory-audit.ts +34 -0
  35. package/extensions/dashboard/overlay-data.ts +705 -0
  36. package/extensions/dashboard/overlay.ts +365 -0
  37. package/extensions/dashboard/render-utils.ts +54 -0
  38. package/extensions/dashboard/types.ts +191 -0
  39. package/extensions/dashboard/uri-helper.ts +45 -0
  40. package/extensions/debug.ts +69 -0
  41. package/extensions/defaults.ts +282 -0
  42. package/extensions/design-tree/dashboard-state.ts +161 -0
  43. package/extensions/design-tree/design-card.ts +362 -0
  44. package/extensions/design-tree/index.ts +2130 -0
  45. package/extensions/design-tree/lifecycle-emitter.ts +41 -0
  46. package/extensions/design-tree/tree.ts +1607 -0
  47. package/extensions/design-tree/types.ts +163 -0
  48. package/extensions/distill.ts +127 -0
  49. package/extensions/effort/index.ts +395 -0
  50. package/extensions/effort/tiers.ts +146 -0
  51. package/extensions/effort/types.ts +105 -0
  52. package/extensions/lib/git-state.ts +227 -0
  53. package/extensions/lib/local-models.ts +157 -0
  54. package/extensions/lib/model-preferences.ts +51 -0
  55. package/extensions/lib/model-routing.ts +720 -0
  56. package/extensions/lib/operator-fallback.ts +205 -0
  57. package/extensions/lib/operator-profile.ts +360 -0
  58. package/extensions/lib/slash-command-bridge.ts +253 -0
  59. package/extensions/lib/typebox-helpers.ts +16 -0
  60. package/extensions/local-inference/index.ts +727 -0
  61. package/extensions/mcp-bridge/README.md +220 -0
  62. package/extensions/mcp-bridge/index.ts +951 -0
  63. package/extensions/mcp-bridge/lib.ts +365 -0
  64. package/extensions/mcp-bridge/mcp.json +3 -0
  65. package/extensions/mcp-bridge/package.json +11 -0
  66. package/extensions/model-budget.ts +752 -0
  67. package/extensions/offline-driver.ts +403 -0
  68. package/extensions/openspec/archive-gate.ts +164 -0
  69. package/extensions/openspec/branch-cleanup.ts +64 -0
  70. package/extensions/openspec/dashboard-state.ts +50 -0
  71. package/extensions/openspec/index.ts +1917 -0
  72. package/extensions/openspec/lifecycle-emitter.ts +65 -0
  73. package/extensions/openspec/lifecycle-files.ts +70 -0
  74. package/extensions/openspec/lifecycle.ts +50 -0
  75. package/extensions/openspec/reconcile.ts +187 -0
  76. package/extensions/openspec/spec.ts +1385 -0
  77. package/extensions/openspec/types.ts +98 -0
  78. package/extensions/project-memory/DESIGN-global-mind.md +198 -0
  79. package/extensions/project-memory/README.md +202 -0
  80. package/extensions/project-memory/api-types.ts +382 -0
  81. package/extensions/project-memory/compaction-policy.ts +29 -0
  82. package/extensions/project-memory/core.ts +164 -0
  83. package/extensions/project-memory/embeddings.ts +230 -0
  84. package/extensions/project-memory/extraction-v2.ts +861 -0
  85. package/extensions/project-memory/factstore.ts +2177 -0
  86. package/extensions/project-memory/index.ts +3459 -0
  87. package/extensions/project-memory/injection-metrics.ts +91 -0
  88. package/extensions/project-memory/jsonl-io.ts +12 -0
  89. package/extensions/project-memory/lifecycle.ts +331 -0
  90. package/extensions/project-memory/migration.ts +293 -0
  91. package/extensions/project-memory/package.json +9 -0
  92. package/extensions/project-memory/sci-renderers.ts +7 -0
  93. package/extensions/project-memory/template.ts +103 -0
  94. package/extensions/project-memory/triggers.ts +52 -0
  95. package/extensions/project-memory/types.ts +102 -0
  96. package/extensions/render/composition/fonts/Inter-Bold.ttf +0 -0
  97. package/extensions/render/composition/fonts/Inter-Regular.ttf +0 -0
  98. package/extensions/render/composition/fonts/Tomorrow-Bold.ttf +0 -0
  99. package/extensions/render/composition/fonts/Tomorrow-Regular.ttf +0 -0
  100. package/extensions/render/composition/package-lock.json +534 -0
  101. package/extensions/render/composition/package.json +22 -0
  102. package/extensions/render/composition/render.mjs +246 -0
  103. package/extensions/render/composition/test-comp.tsx +87 -0
  104. package/extensions/render/composition/types.ts +24 -0
  105. package/extensions/render/excalidraw/UPSTREAM.md +81 -0
  106. package/extensions/render/excalidraw/elements.ts +764 -0
  107. package/extensions/render/excalidraw/index.ts +66 -0
  108. package/extensions/render/excalidraw/types.ts +223 -0
  109. package/extensions/render/excalidraw-renderer/pyproject.toml +8 -0
  110. package/extensions/render/excalidraw-renderer/render_excalidraw.py +182 -0
  111. package/extensions/render/excalidraw-renderer/render_template.html +59 -0
  112. package/extensions/render/index.ts +830 -0
  113. package/extensions/render/native-diagrams/index.ts +57 -0
  114. package/extensions/render/native-diagrams/motifs.ts +542 -0
  115. package/extensions/render/native-diagrams/raster.ts +8 -0
  116. package/extensions/render/native-diagrams/scene.ts +75 -0
  117. package/extensions/render/native-diagrams/spec.ts +204 -0
  118. package/extensions/render/native-diagrams/svg.ts +116 -0
  119. package/extensions/sci-ui.ts +304 -0
  120. package/extensions/session-log.ts +174 -0
  121. package/extensions/shared-state.ts +146 -0
  122. package/extensions/spinner-verbs.ts +91 -0
  123. package/extensions/style.ts +281 -0
  124. package/extensions/terminal-title.ts +191 -0
  125. package/extensions/tool-profile/index.ts +291 -0
  126. package/extensions/tool-profile/profiles.ts +290 -0
  127. package/extensions/types.d.ts +9 -0
  128. package/extensions/vault/index.ts +185 -0
  129. package/extensions/version-check.ts +90 -0
  130. package/extensions/view/index.ts +859 -0
  131. package/extensions/view/uri-resolver.ts +148 -0
  132. package/extensions/web-search/index.ts +182 -0
  133. package/extensions/web-search/providers.ts +121 -0
  134. package/extensions/web-ui/index.ts +110 -0
  135. package/extensions/web-ui/server.ts +265 -0
  136. package/extensions/web-ui/state.ts +462 -0
  137. package/extensions/web-ui/static/index.html +145 -0
  138. package/extensions/web-ui/types.ts +284 -0
  139. package/package.json +76 -0
  140. package/prompts/init.md +75 -0
  141. package/prompts/new-repo.md +54 -0
  142. package/prompts/oci-login.md +56 -0
  143. package/prompts/status.md +50 -0
  144. package/settings.json +4 -0
  145. package/skills/cleave/SKILL.md +218 -0
  146. package/skills/git/SKILL.md +209 -0
  147. package/skills/git/_reference/ci-validation.md +204 -0
  148. package/skills/oci/SKILL.md +338 -0
  149. package/skills/openspec/SKILL.md +346 -0
  150. package/skills/pi-extensions/SKILL.md +191 -0
  151. package/skills/pi-tui/SKILL.md +517 -0
  152. package/skills/python/SKILL.md +189 -0
  153. package/skills/rust/SKILL.md +268 -0
  154. package/skills/security/SKILL.md +206 -0
  155. package/skills/style/SKILL.md +264 -0
  156. package/skills/typescript/SKILL.md +225 -0
  157. package/skills/vault/SKILL.md +102 -0
  158. package/themes/alpharius-legacy.json +85 -0
  159. package/themes/alpharius.conf +59 -0
  160. package/themes/alpharius.json +88 -0
@@ -0,0 +1,57 @@
1
+ import type { Scene } from "./scene.ts";
2
+ import { compileMotifToScene, extractTextElements } from "./motifs.ts";
3
+ import { rasterizeSvgToPng } from "./raster.ts";
4
+ import {
5
+ NATIVE_DIAGRAM_DIRECTIONS,
6
+ NATIVE_DIAGRAM_MOTIFS,
7
+ NATIVE_NODE_KINDS,
8
+ parseNativeDiagramSpec,
9
+ validateNativeDiagramSpec,
10
+ type NativeDiagramDirection,
11
+ type NativeDiagramEdgeSpec,
12
+ type NativeDiagramMotif,
13
+ type NativeDiagramNodeSpec,
14
+ type NativeDiagramPanelSpec,
15
+ type NativeDiagramSpec,
16
+ type NativeNodeKind,
17
+ } from "./spec.ts";
18
+ import { serializeSceneToSvg } from "./svg.ts";
19
+
20
+ export function composeNativeDiagram(spec: NativeDiagramSpec): { scene: Scene; svg: string } {
21
+ const errors = validateNativeDiagramSpec(spec);
22
+ if (errors.length > 0) {
23
+ throw new Error(errors.join("\n"));
24
+ }
25
+ const scene = compileMotifToScene(spec);
26
+ const svg = serializeSceneToSvg(scene);
27
+ return { scene, svg };
28
+ }
29
+
30
+ export function parseAndComposeNativeDiagram(input: unknown): { spec: NativeDiagramSpec; scene: Scene; svg: string } {
31
+ const spec = parseNativeDiagramSpec(input);
32
+ const { scene, svg } = composeNativeDiagram(spec);
33
+ return { spec, scene, svg };
34
+ }
35
+
36
+ export {
37
+ compileMotifToScene,
38
+ extractTextElements,
39
+ rasterizeSvgToPng,
40
+ parseNativeDiagramSpec,
41
+ serializeSceneToSvg,
42
+ validateNativeDiagramSpec,
43
+ NATIVE_DIAGRAM_DIRECTIONS,
44
+ NATIVE_DIAGRAM_MOTIFS,
45
+ NATIVE_NODE_KINDS,
46
+ };
47
+
48
+ export type {
49
+ Scene,
50
+ NativeDiagramDirection,
51
+ NativeDiagramEdgeSpec,
52
+ NativeDiagramMotif,
53
+ NativeDiagramNodeSpec,
54
+ NativeDiagramPanelSpec,
55
+ NativeDiagramSpec,
56
+ NativeNodeKind,
57
+ };
@@ -0,0 +1,542 @@
1
+ import { SEMANTIC_COLORS, TEXT_COLORS, type SemanticPurpose } from "../excalidraw/types.ts";
2
+ import type { Scene, SceneElement, ScenePath, SceneText } from "./scene.ts";
3
+ import {
4
+ type NativeDiagramDirection,
5
+ type NativeDiagramEdgeSpec,
6
+ type NativeDiagramMotif,
7
+ type NativeDiagramNodeSpec,
8
+ type NativeDiagramPanelSpec,
9
+ type NativeDiagramSpec,
10
+ type NativeNodeKind,
11
+ } from "./spec.ts";
12
+
13
+ interface SizedNode extends NativeDiagramNodeSpec {
14
+ kind: NativeNodeKind;
15
+ semantic: SemanticPurpose;
16
+ width: number;
17
+ height: number;
18
+ }
19
+
20
+ interface PositionedNode extends SizedNode {
21
+ x: number;
22
+ y: number;
23
+ }
24
+
25
+ interface PositionedPanel extends NativeDiagramPanelSpec {
26
+ x: number;
27
+ y: number;
28
+ width: number;
29
+ height: number;
30
+ headerHeight: number;
31
+ }
32
+
33
+ interface EdgeRuntime {
34
+ edge: NativeDiagramEdgeSpec;
35
+ outgoingIndex: number;
36
+ outgoingCount: number;
37
+ incomingIndex: number;
38
+ incomingCount: number;
39
+ }
40
+
41
+ type Side = "left" | "right" | "top" | "bottom";
42
+
43
+ const DEFAULT_WIDTH: Record<NativeNodeKind, number> = {
44
+ process: 190,
45
+ decision: 190,
46
+ start: 150,
47
+ end: 150,
48
+ data: 200,
49
+ };
50
+
51
+ const DEFAULT_HEIGHT: Record<NativeNodeKind, number> = {
52
+ process: 92,
53
+ decision: 112,
54
+ start: 92,
55
+ end: 92,
56
+ data: 92,
57
+ };
58
+
59
+ const DEFAULT_SEMANTIC: Record<NativeNodeKind, SemanticPurpose> = {
60
+ process: "primary",
61
+ decision: "decision",
62
+ start: "start",
63
+ end: "end",
64
+ data: "evidence",
65
+ };
66
+
67
+ const DEFAULT_BACKGROUND = "#ffffff";
68
+ const DEFAULT_PADDING = 56;
69
+ const TITLE_HEIGHT = 58;
70
+ const TITLE_FONT_SIZE = 24;
71
+ const TITLE_Y = 36;
72
+ const NODE_GAP = 84;
73
+ const FANOUT_COLUMN_GAP = 180;
74
+ const FANOUT_ROW_GAP = 46;
75
+ const PANEL_GAP = 72;
76
+ const PANEL_PADDING = 42;
77
+ const PANEL_HEADER_HEIGHT = 54;
78
+ const PANEL_LABEL_SIZE = 18;
79
+ const EDGE_LABEL_SIZE = 16;
80
+ const EDGE_LABEL_OFFSET = 18;
81
+ const SLOT_SPREAD = 18;
82
+ const FONT_FAMILY = "Cascadia Code, Cascadia Mono, Menlo, monospace";
83
+
84
+ function approxTextWidth(text: string, fontSize: number): number {
85
+ return text.length * fontSize * 0.6;
86
+ }
87
+
88
+ function textColorForFill(fill: string): string {
89
+ return fill === SEMANTIC_COLORS.evidence.fill || fill === SEMANTIC_COLORS.primary.fill || fill === SEMANTIC_COLORS.secondary.fill
90
+ ? TEXT_COLORS.onDark
91
+ : TEXT_COLORS.onLight;
92
+ }
93
+
94
+ function sortNodes<T extends NativeDiagramNodeSpec>(nodes: readonly T[]): T[] {
95
+ return [...nodes].sort((a, b) => {
96
+ const orderA = a.order ?? Number.MAX_SAFE_INTEGER;
97
+ const orderB = b.order ?? Number.MAX_SAFE_INTEGER;
98
+ if (orderA !== orderB) return orderA - orderB;
99
+ return a.id.localeCompare(b.id);
100
+ });
101
+ }
102
+
103
+ function withDefaults(node: NativeDiagramNodeSpec): SizedNode {
104
+ const kind = node.kind ?? "process";
105
+ return {
106
+ ...node,
107
+ kind,
108
+ semantic: node.semantic ?? DEFAULT_SEMANTIC[kind],
109
+ width: DEFAULT_WIDTH[kind],
110
+ height: DEFAULT_HEIGHT[kind],
111
+ };
112
+ }
113
+
114
+ function nodeSlot(node: PositionedNode, side: Side, index: number, count: number): [number, number] {
115
+ const slotCount = Math.max(1, count);
116
+ const offset = (index - (slotCount - 1) / 2) * SLOT_SPREAD;
117
+ switch (side) {
118
+ case "left":
119
+ return [node.x, node.y + node.height / 2 + offset];
120
+ case "right":
121
+ return [node.x + node.width, node.y + node.height / 2 + offset];
122
+ case "top":
123
+ return [node.x + node.width / 2 + offset, node.y];
124
+ case "bottom":
125
+ return [node.x + node.width / 2 + offset, node.y + node.height];
126
+ }
127
+ }
128
+
129
+ function buildEdgeRuntimes(edges: readonly NativeDiagramEdgeSpec[]): EdgeRuntime[] {
130
+ const outgoingCounts = new Map<string, number>();
131
+ const incomingCounts = new Map<string, number>();
132
+ for (const edge of edges) {
133
+ outgoingCounts.set(edge.from, (outgoingCounts.get(edge.from) ?? 0) + 1);
134
+ incomingCounts.set(edge.to, (incomingCounts.get(edge.to) ?? 0) + 1);
135
+ }
136
+ const outgoingSeen = new Map<string, number>();
137
+ const incomingSeen = new Map<string, number>();
138
+ return edges.map((edge) => {
139
+ const outgoingIndex = outgoingSeen.get(edge.from) ?? 0;
140
+ const incomingIndex = incomingSeen.get(edge.to) ?? 0;
141
+ outgoingSeen.set(edge.from, outgoingIndex + 1);
142
+ incomingSeen.set(edge.to, incomingIndex + 1);
143
+ return {
144
+ edge,
145
+ outgoingIndex,
146
+ outgoingCount: outgoingCounts.get(edge.from) ?? 1,
147
+ incomingIndex,
148
+ incomingCount: incomingCounts.get(edge.to) ?? 1,
149
+ };
150
+ });
151
+ }
152
+
153
+ function pathWithLabel(
154
+ elements: SceneElement[],
155
+ d: string,
156
+ label: string | undefined,
157
+ labelX: number,
158
+ labelY: number,
159
+ options: { dashed?: boolean; stroke?: string } = {},
160
+ ): void {
161
+ const path: ScenePath = {
162
+ kind: "path",
163
+ d,
164
+ stroke: options.stroke ?? "#111827",
165
+ strokeWidth: 3,
166
+ fill: "none",
167
+ markerEnd: "arrow",
168
+ strokeDasharray: options.dashed ? "14 12" : undefined,
169
+ };
170
+ elements.push(path);
171
+ if (label) {
172
+ const labelWidth = approxTextWidth(label, EDGE_LABEL_SIZE);
173
+ elements.push({
174
+ kind: "rect",
175
+ x: labelX - labelWidth / 2 - 8,
176
+ y: labelY - EDGE_LABEL_SIZE + 2,
177
+ width: labelWidth + 16,
178
+ height: EDGE_LABEL_SIZE + 8,
179
+ rx: 8,
180
+ ry: 8,
181
+ fill: DEFAULT_BACKGROUND,
182
+ stroke: "none",
183
+ });
184
+ elements.push({
185
+ kind: "text",
186
+ x: labelX,
187
+ y: labelY,
188
+ text: label,
189
+ fontSize: EDGE_LABEL_SIZE,
190
+ textAnchor: "middle",
191
+ fill: "#64748b",
192
+ fontFamily: FONT_FAMILY,
193
+ });
194
+ }
195
+ }
196
+
197
+ function renderNode(node: PositionedNode, elements: SceneElement[]): void {
198
+ const colors = SEMANTIC_COLORS[node.semantic];
199
+ const labelColor = textColorForFill(colors.fill);
200
+ if (node.kind === "decision") {
201
+ elements.push({
202
+ kind: "diamond",
203
+ x: node.x,
204
+ y: node.y,
205
+ width: node.width,
206
+ height: node.height,
207
+ fill: colors.fill,
208
+ stroke: colors.stroke,
209
+ strokeWidth: 3,
210
+ });
211
+ } else if (node.kind === "start" || node.kind === "end") {
212
+ elements.push({
213
+ kind: "ellipse",
214
+ cx: node.x + node.width / 2,
215
+ cy: node.y + node.height / 2,
216
+ rx: node.width / 2,
217
+ ry: node.height / 2,
218
+ fill: colors.fill,
219
+ stroke: colors.stroke,
220
+ strokeWidth: 3,
221
+ });
222
+ } else {
223
+ elements.push({
224
+ kind: "rect",
225
+ x: node.x,
226
+ y: node.y,
227
+ width: node.width,
228
+ height: node.height,
229
+ rx: 22,
230
+ ry: 22,
231
+ fill: colors.fill,
232
+ stroke: colors.stroke,
233
+ strokeWidth: 3,
234
+ });
235
+ }
236
+ elements.push({
237
+ kind: "text",
238
+ x: node.x + node.width / 2,
239
+ y: node.y + node.height / 2 + 8,
240
+ text: node.label,
241
+ fontSize: 18,
242
+ textAnchor: "middle",
243
+ fill: labelColor,
244
+ fontFamily: FONT_FAMILY,
245
+ fontWeight: "600",
246
+ });
247
+ }
248
+
249
+ function layoutPipeline(nodes: readonly SizedNode[], direction: NativeDiagramDirection, originX: number, originY: number): PositionedNode[] {
250
+ const ordered = sortNodes(nodes);
251
+ const maxHeight = Math.max(...ordered.map((node) => node.height));
252
+ const maxWidth = Math.max(...ordered.map((node) => node.width));
253
+ let cursor = 0;
254
+ return ordered.map((node) => {
255
+ const positioned: PositionedNode = direction === "horizontal"
256
+ ? { ...node, x: originX + cursor, y: originY + (maxHeight - node.height) / 2 }
257
+ : { ...node, x: originX + (maxWidth - node.width) / 2, y: originY + cursor };
258
+ cursor += (direction === "horizontal" ? node.width : node.height) + NODE_GAP;
259
+ return positioned;
260
+ });
261
+ }
262
+
263
+ function layoutFanout(nodes: readonly SizedNode[], direction: NativeDiagramDirection, originX: number, originY: number): PositionedNode[] {
264
+ const ordered = sortNodes(nodes);
265
+ const anchor = ordered[0];
266
+ const leaves = ordered.slice(1);
267
+ const positioned: PositionedNode[] = [];
268
+ if (direction === "horizontal") {
269
+ const totalHeight = leaves.reduce((sum, node) => sum + node.height, 0) + Math.max(0, leaves.length - 1) * FANOUT_ROW_GAP;
270
+ const anchorY = originY + Math.max(0, (totalHeight - anchor.height) / 2);
271
+ positioned.push({ ...anchor, x: originX, y: anchorY });
272
+ let cursorY = originY;
273
+ for (const node of leaves) {
274
+ positioned.push({ ...node, x: originX + anchor.width + FANOUT_COLUMN_GAP, y: cursorY });
275
+ cursorY += node.height + FANOUT_ROW_GAP;
276
+ }
277
+ return positioned;
278
+ }
279
+ const totalWidth = leaves.reduce((sum, node) => sum + node.width, 0) + Math.max(0, leaves.length - 1) * FANOUT_ROW_GAP;
280
+ const anchorX = originX + Math.max(0, (totalWidth - anchor.width) / 2);
281
+ positioned.push({ ...anchor, x: anchorX, y: originY });
282
+ let cursorX = originX;
283
+ for (const node of leaves) {
284
+ positioned.push({ ...node, x: cursorX, y: originY + anchor.height + FANOUT_COLUMN_GAP });
285
+ cursorX += node.width + FANOUT_ROW_GAP;
286
+ }
287
+ return positioned;
288
+ }
289
+
290
+ function bounds(nodes: readonly PositionedNode[]): { width: number; height: number } {
291
+ const maxX = Math.max(...nodes.map((node) => node.x + node.width));
292
+ const maxY = Math.max(...nodes.map((node) => node.y + node.height));
293
+ const minX = Math.min(...nodes.map((node) => node.x));
294
+ const minY = Math.min(...nodes.map((node) => node.y));
295
+ return { width: maxX - minX, height: maxY - minY };
296
+ }
297
+
298
+ function renderTitle(elements: SceneElement[], title: string): void {
299
+ elements.push({
300
+ kind: "text",
301
+ x: DEFAULT_PADDING,
302
+ y: TITLE_Y,
303
+ text: title,
304
+ fontSize: TITLE_FONT_SIZE,
305
+ fill: "#1e40af",
306
+ fontFamily: FONT_FAMILY,
307
+ fontWeight: "600",
308
+ });
309
+ }
310
+
311
+ function renderPipelineScene(spec: NativeDiagramSpec): Scene {
312
+ const direction = spec.direction ?? "horizontal";
313
+ const nodes = spec.nodes.map(withDefaults);
314
+ const originX = DEFAULT_PADDING;
315
+ const originY = DEFAULT_PADDING + (spec.title ? TITLE_HEIGHT : 0) + 18;
316
+ const positioned = layoutPipeline(nodes, direction, originX, originY);
317
+ const layoutBounds = bounds(positioned);
318
+ const elements: SceneElement[] = [];
319
+ if (spec.title) renderTitle(elements, spec.title);
320
+ for (const node of positioned) renderNode(node, elements);
321
+
322
+ for (const runtime of buildEdgeRuntimes(spec.edges ?? [])) {
323
+ const from = positioned.find((node) => node.id === runtime.edge.from);
324
+ const to = positioned.find((node) => node.id === runtime.edge.to);
325
+ if (!from || !to) continue;
326
+ if (direction === "horizontal") {
327
+ const start = nodeSlot(from, "right", runtime.outgoingIndex, runtime.outgoingCount);
328
+ const end = nodeSlot(to, "left", runtime.incomingIndex, runtime.incomingCount);
329
+ pathWithLabel(
330
+ elements,
331
+ `M ${start[0]} ${start[1]} L ${end[0]} ${end[1]}`,
332
+ runtime.edge.label,
333
+ (start[0] + end[0]) / 2,
334
+ start[1] - EDGE_LABEL_OFFSET,
335
+ { dashed: runtime.edge.dashed },
336
+ );
337
+ } else {
338
+ const start = nodeSlot(from, "bottom", runtime.outgoingIndex, runtime.outgoingCount);
339
+ const end = nodeSlot(to, "top", runtime.incomingIndex, runtime.incomingCount);
340
+ pathWithLabel(
341
+ elements,
342
+ `M ${start[0]} ${start[1]} L ${end[0]} ${end[1]}`,
343
+ runtime.edge.label,
344
+ start[0] + EDGE_LABEL_OFFSET,
345
+ (start[1] + end[1]) / 2,
346
+ { dashed: runtime.edge.dashed },
347
+ );
348
+ }
349
+ }
350
+
351
+ return {
352
+ width: layoutBounds.width + DEFAULT_PADDING * 2,
353
+ height: layoutBounds.height + DEFAULT_PADDING * 2 + (spec.title ? TITLE_HEIGHT : 0),
354
+ background: spec.canvas?.background ?? DEFAULT_BACKGROUND,
355
+ elements,
356
+ };
357
+ }
358
+
359
+ function renderFanoutScene(spec: NativeDiagramSpec): Scene {
360
+ const direction = spec.direction ?? "horizontal";
361
+ const nodes = spec.nodes.map(withDefaults);
362
+ const originX = DEFAULT_PADDING;
363
+ const originY = DEFAULT_PADDING + (spec.title ? TITLE_HEIGHT : 0) + 24;
364
+ const positioned = layoutFanout(nodes, direction, originX, originY);
365
+ const layoutBounds = bounds(positioned);
366
+ const elements: SceneElement[] = [];
367
+ if (spec.title) renderTitle(elements, spec.title);
368
+ for (const node of positioned) renderNode(node, elements);
369
+
370
+ for (const runtime of buildEdgeRuntimes(spec.edges ?? [])) {
371
+ const from = positioned.find((node) => node.id === runtime.edge.from);
372
+ const to = positioned.find((node) => node.id === runtime.edge.to);
373
+ if (!from || !to) continue;
374
+ if (direction === "horizontal") {
375
+ const start = nodeSlot(from, "right", runtime.outgoingIndex, runtime.outgoingCount);
376
+ const end = nodeSlot(to, "left", runtime.incomingIndex, runtime.incomingCount);
377
+ const bendX = from.x + from.width + 72 + runtime.outgoingIndex * 34;
378
+ const labelX = bendX + 8;
379
+ const labelY = start[1] - EDGE_LABEL_OFFSET;
380
+ pathWithLabel(
381
+ elements,
382
+ `M ${start[0]} ${start[1]} C ${bendX} ${start[1]}, ${bendX} ${end[1]}, ${end[0]} ${end[1]}`,
383
+ runtime.edge.label,
384
+ labelX,
385
+ labelY,
386
+ { dashed: runtime.edge.dashed },
387
+ );
388
+ } else {
389
+ const start = nodeSlot(from, "bottom", runtime.outgoingIndex, runtime.outgoingCount);
390
+ const end = nodeSlot(to, "top", runtime.incomingIndex, runtime.incomingCount);
391
+ const bendY = from.y + from.height + 72 + runtime.outgoingIndex * 34;
392
+ pathWithLabel(
393
+ elements,
394
+ `M ${start[0]} ${start[1]} C ${start[0]} ${bendY}, ${end[0]} ${bendY}, ${end[0]} ${end[1]}`,
395
+ runtime.edge.label,
396
+ start[0] + 14,
397
+ bendY - 12,
398
+ { dashed: runtime.edge.dashed },
399
+ );
400
+ }
401
+ }
402
+
403
+ return {
404
+ width: layoutBounds.width + DEFAULT_PADDING * 2 + 40,
405
+ height: layoutBounds.height + DEFAULT_PADDING * 2 + (spec.title ? TITLE_HEIGHT : 0),
406
+ background: spec.canvas?.background ?? DEFAULT_BACKGROUND,
407
+ elements,
408
+ };
409
+ }
410
+
411
+ function layoutPanelRows(
412
+ spec: NativeDiagramSpec,
413
+ panels: readonly NativeDiagramPanelSpec[],
414
+ ): { panels: PositionedPanel[]; nodes: PositionedNode[]; width: number; height: number } {
415
+ let cursorY = DEFAULT_PADDING + (spec.title ? TITLE_HEIGHT : 0) + 16;
416
+ let maxWidth = 0;
417
+ const panelLayouts: PositionedPanel[] = [];
418
+ const positionedNodes: PositionedNode[] = [];
419
+ for (const panel of panels) {
420
+ const panelNodes = sortNodes(spec.nodes.filter((node) => node.panel === panel.id).map(withDefaults));
421
+ const layout = layoutPipeline(panelNodes, "horizontal", DEFAULT_PADDING + PANEL_PADDING, cursorY + PANEL_HEADER_HEIGHT + PANEL_PADDING);
422
+ const layoutBounds = bounds(layout);
423
+ const panelWidth = Math.max(layoutBounds.width + PANEL_PADDING * 2, 560);
424
+ const panelHeight = layoutBounds.height + PANEL_PADDING * 2 + PANEL_HEADER_HEIGHT;
425
+ panelLayouts.push({
426
+ ...panel,
427
+ x: DEFAULT_PADDING,
428
+ y: cursorY,
429
+ width: panelWidth,
430
+ height: panelHeight,
431
+ headerHeight: PANEL_HEADER_HEIGHT,
432
+ });
433
+ positionedNodes.push(...layout);
434
+ maxWidth = Math.max(maxWidth, panelWidth);
435
+ cursorY += panelHeight + PANEL_GAP;
436
+ }
437
+ for (const panel of panelLayouts) panel.width = maxWidth;
438
+ return {
439
+ panels: panelLayouts,
440
+ nodes: positionedNodes,
441
+ width: maxWidth + DEFAULT_PADDING * 2,
442
+ height: cursorY - PANEL_GAP + DEFAULT_PADDING,
443
+ };
444
+ }
445
+
446
+ function renderPanelSplitScene(spec: NativeDiagramSpec): Scene {
447
+ const panels = spec.panels ?? [];
448
+ const { panels: positionedPanels, nodes, width, height } = layoutPanelRows(spec, panels);
449
+ const elements: SceneElement[] = [];
450
+ if (spec.title) renderTitle(elements, spec.title);
451
+
452
+ for (const panel of positionedPanels) {
453
+ elements.push({
454
+ kind: "rect",
455
+ x: panel.x,
456
+ y: panel.y,
457
+ width: panel.width,
458
+ height: panel.height,
459
+ rx: 28,
460
+ ry: 28,
461
+ fill: "#f8fafc",
462
+ stroke: "#94a3b8",
463
+ strokeWidth: 2,
464
+ });
465
+ elements.push({
466
+ kind: "text",
467
+ x: panel.x + 30,
468
+ y: panel.y + 34,
469
+ text: panel.label,
470
+ fontSize: PANEL_LABEL_SIZE,
471
+ fill: "#64748b",
472
+ fontFamily: FONT_FAMILY,
473
+ fontWeight: "600",
474
+ });
475
+ elements.push({
476
+ kind: "line",
477
+ x1: panel.x + 22,
478
+ y1: panel.y + panel.headerHeight,
479
+ x2: panel.x + panel.width - 22,
480
+ y2: panel.y + panel.headerHeight,
481
+ stroke: "#cbd5e1",
482
+ strokeWidth: 1,
483
+ });
484
+ }
485
+
486
+ for (const node of nodes) renderNode(node, elements);
487
+
488
+ for (const runtime of buildEdgeRuntimes(spec.edges ?? [])) {
489
+ const from = nodes.find((node) => node.id === runtime.edge.from);
490
+ const to = nodes.find((node) => node.id === runtime.edge.to);
491
+ if (!from || !to) continue;
492
+ if (from.panel && to.panel && from.panel !== to.panel) {
493
+ const fromPanel = positionedPanels.find((panel) => panel.id === from.panel);
494
+ const toPanel = positionedPanels.find((panel) => panel.id === to.panel);
495
+ if (!fromPanel || !toPanel) continue;
496
+ const start = nodeSlot(from, "bottom", runtime.outgoingIndex, runtime.outgoingCount);
497
+ const end = nodeSlot(to, "top", runtime.incomingIndex, runtime.incomingCount);
498
+ const corridorY = fromPanel.y + fromPanel.height + (toPanel.y - (fromPanel.y + fromPanel.height)) / 2;
499
+ pathWithLabel(
500
+ elements,
501
+ `M ${start[0]} ${start[1]} L ${start[0]} ${corridorY} L ${end[0]} ${corridorY} L ${end[0]} ${end[1]}`,
502
+ runtime.edge.label,
503
+ (start[0] + end[0]) / 2,
504
+ corridorY - 10,
505
+ { dashed: runtime.edge.dashed },
506
+ );
507
+ continue;
508
+ }
509
+ const start = nodeSlot(from, "right", runtime.outgoingIndex, runtime.outgoingCount);
510
+ const end = nodeSlot(to, "left", runtime.incomingIndex, runtime.incomingCount);
511
+ pathWithLabel(
512
+ elements,
513
+ `M ${start[0]} ${start[1]} L ${end[0]} ${end[1]}`,
514
+ runtime.edge.label,
515
+ (start[0] + end[0]) / 2,
516
+ start[1] - EDGE_LABEL_OFFSET,
517
+ { dashed: runtime.edge.dashed },
518
+ );
519
+ }
520
+
521
+ return {
522
+ width,
523
+ height,
524
+ background: spec.canvas?.background ?? DEFAULT_BACKGROUND,
525
+ elements,
526
+ };
527
+ }
528
+
529
+ export function compileMotifToScene(spec: NativeDiagramSpec): Scene {
530
+ switch (spec.motif as NativeDiagramMotif) {
531
+ case "pipeline":
532
+ return renderPipelineScene(spec);
533
+ case "fanout":
534
+ return renderFanoutScene(spec);
535
+ case "panel-split":
536
+ return renderPanelSplitScene(spec);
537
+ }
538
+ }
539
+
540
+ export function extractTextElements(scene: Scene): SceneText[] {
541
+ return scene.elements.filter((element): element is SceneText => element.kind === "text");
542
+ }
@@ -0,0 +1,8 @@
1
+ import { Resvg } from "@resvg/resvg-js";
2
+
3
+ export function rasterizeSvgToPng(svg: string): Buffer {
4
+ const resvg = new Resvg(svg, {
5
+ fitTo: { mode: "original" },
6
+ });
7
+ return resvg.render().asPng();
8
+ }
@@ -0,0 +1,75 @@
1
+ export interface Scene {
2
+ width: number;
3
+ height: number;
4
+ background: string;
5
+ elements: SceneElement[];
6
+ }
7
+
8
+ interface SceneBase {
9
+ id?: string;
10
+ fill?: string;
11
+ stroke?: string;
12
+ strokeWidth?: number;
13
+ strokeDasharray?: string;
14
+ opacity?: number;
15
+ }
16
+
17
+ export interface SceneRect extends SceneBase {
18
+ kind: "rect";
19
+ x: number;
20
+ y: number;
21
+ width: number;
22
+ height: number;
23
+ rx?: number;
24
+ ry?: number;
25
+ }
26
+
27
+ export interface SceneEllipse extends SceneBase {
28
+ kind: "ellipse";
29
+ cx: number;
30
+ cy: number;
31
+ rx: number;
32
+ ry: number;
33
+ }
34
+
35
+ export interface SceneDiamond extends SceneBase {
36
+ kind: "diamond";
37
+ x: number;
38
+ y: number;
39
+ width: number;
40
+ height: number;
41
+ }
42
+
43
+ export interface ScenePath extends SceneBase {
44
+ kind: "path";
45
+ d: string;
46
+ markerEnd?: "arrow";
47
+ markerStart?: "arrow";
48
+ }
49
+
50
+ export interface SceneText extends SceneBase {
51
+ kind: "text";
52
+ x: number;
53
+ y: number;
54
+ text: string;
55
+ fontSize: number;
56
+ fontFamily?: string;
57
+ textAnchor?: "start" | "middle" | "end";
58
+ fontWeight?: string;
59
+ }
60
+
61
+ export interface SceneLine extends SceneBase {
62
+ kind: "line";
63
+ x1: number;
64
+ y1: number;
65
+ x2: number;
66
+ y2: number;
67
+ }
68
+
69
+ export type SceneElement =
70
+ | SceneRect
71
+ | SceneEllipse
72
+ | SceneDiamond
73
+ | ScenePath
74
+ | SceneText
75
+ | SceneLine;