@markdy/mcp-server 1.0.24 → 1.0.26
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 +147 -20
- package/dist/index.d.ts +14 -2
- package/dist/index.js +3326 -34
- package/package.json +27 -4
package/dist/index.js
CHANGED
|
@@ -5,22 +5,3053 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
6
|
import {
|
|
7
7
|
CallToolRequestSchema,
|
|
8
|
-
ListToolsRequestSchema
|
|
8
|
+
ListToolsRequestSchema,
|
|
9
|
+
ListResourcesRequestSchema,
|
|
10
|
+
ReadResourceRequestSchema,
|
|
11
|
+
ListPromptsRequestSchema,
|
|
12
|
+
GetPromptRequestSchema
|
|
9
13
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
10
14
|
|
|
15
|
+
// ../core/src/system-vocabulary.ts
|
|
16
|
+
var TECHNICAL_NODE_TYPES = [
|
|
17
|
+
"service",
|
|
18
|
+
"api",
|
|
19
|
+
"microservice",
|
|
20
|
+
"backend",
|
|
21
|
+
"server",
|
|
22
|
+
"worker",
|
|
23
|
+
"job",
|
|
24
|
+
"scheduler",
|
|
25
|
+
"cron",
|
|
26
|
+
"batch",
|
|
27
|
+
"function",
|
|
28
|
+
"lambda",
|
|
29
|
+
"edge",
|
|
30
|
+
"controller",
|
|
31
|
+
"handler",
|
|
32
|
+
"repository",
|
|
33
|
+
"module",
|
|
34
|
+
"package",
|
|
35
|
+
"library",
|
|
36
|
+
"sdk",
|
|
37
|
+
"cli",
|
|
38
|
+
"runtime",
|
|
39
|
+
"process",
|
|
40
|
+
"client",
|
|
41
|
+
"user",
|
|
42
|
+
"browser",
|
|
43
|
+
"web",
|
|
44
|
+
"mobile",
|
|
45
|
+
"desktop",
|
|
46
|
+
"frontend",
|
|
47
|
+
"app",
|
|
48
|
+
"page",
|
|
49
|
+
"view",
|
|
50
|
+
"component",
|
|
51
|
+
"store",
|
|
52
|
+
"db",
|
|
53
|
+
"database",
|
|
54
|
+
"sql",
|
|
55
|
+
"nosql",
|
|
56
|
+
"table",
|
|
57
|
+
"index",
|
|
58
|
+
"warehouse",
|
|
59
|
+
"lake",
|
|
60
|
+
"object_store",
|
|
61
|
+
"storage",
|
|
62
|
+
"bucket",
|
|
63
|
+
"blob",
|
|
64
|
+
"volume",
|
|
65
|
+
"disk",
|
|
66
|
+
"search",
|
|
67
|
+
"cache",
|
|
68
|
+
"queue",
|
|
69
|
+
"topic",
|
|
70
|
+
"stream",
|
|
71
|
+
"event",
|
|
72
|
+
"event_bus",
|
|
73
|
+
"bus",
|
|
74
|
+
"broker",
|
|
75
|
+
"pubsub",
|
|
76
|
+
"kafka",
|
|
77
|
+
"producer",
|
|
78
|
+
"consumer",
|
|
79
|
+
"dead_letter",
|
|
80
|
+
"dlq",
|
|
81
|
+
"webhook",
|
|
82
|
+
"cloud",
|
|
83
|
+
"region",
|
|
84
|
+
"vpc",
|
|
85
|
+
"subnet",
|
|
86
|
+
"network",
|
|
87
|
+
"internet",
|
|
88
|
+
"dns",
|
|
89
|
+
"cdn",
|
|
90
|
+
"proxy",
|
|
91
|
+
"gateway",
|
|
92
|
+
"api_gateway",
|
|
93
|
+
"load_balancer",
|
|
94
|
+
"reverse_proxy",
|
|
95
|
+
"router",
|
|
96
|
+
"switch",
|
|
97
|
+
"nat",
|
|
98
|
+
"firewall",
|
|
99
|
+
"waf",
|
|
100
|
+
"vpn",
|
|
101
|
+
"bastion",
|
|
102
|
+
"container",
|
|
103
|
+
"cluster",
|
|
104
|
+
"pod",
|
|
105
|
+
"node",
|
|
106
|
+
"deployment",
|
|
107
|
+
"replicaset",
|
|
108
|
+
"statefulset",
|
|
109
|
+
"daemonset",
|
|
110
|
+
"namespace",
|
|
111
|
+
"ingress",
|
|
112
|
+
"service_mesh",
|
|
113
|
+
"sidecar",
|
|
114
|
+
"image",
|
|
115
|
+
"registry",
|
|
116
|
+
"docker",
|
|
117
|
+
"compose",
|
|
118
|
+
"helm",
|
|
119
|
+
"chart",
|
|
120
|
+
"configmap",
|
|
121
|
+
"pvc",
|
|
122
|
+
"auth",
|
|
123
|
+
"identity",
|
|
124
|
+
"oauth",
|
|
125
|
+
"oidc",
|
|
126
|
+
"jwt",
|
|
127
|
+
"session",
|
|
128
|
+
"policy",
|
|
129
|
+
"role",
|
|
130
|
+
"permission",
|
|
131
|
+
"vault",
|
|
132
|
+
"secret",
|
|
133
|
+
"key",
|
|
134
|
+
"certificate",
|
|
135
|
+
"security",
|
|
136
|
+
"repo",
|
|
137
|
+
"branch",
|
|
138
|
+
"commit",
|
|
139
|
+
"pipeline",
|
|
140
|
+
"workflow",
|
|
141
|
+
"runner",
|
|
142
|
+
"build",
|
|
143
|
+
"test",
|
|
144
|
+
"artifact",
|
|
145
|
+
"deploy",
|
|
146
|
+
"release",
|
|
147
|
+
"environment",
|
|
148
|
+
"preview",
|
|
149
|
+
"monitor",
|
|
150
|
+
"metrics",
|
|
151
|
+
"logs",
|
|
152
|
+
"trace",
|
|
153
|
+
"alert",
|
|
154
|
+
"dashboard",
|
|
155
|
+
"probe",
|
|
156
|
+
"slo",
|
|
157
|
+
"start",
|
|
158
|
+
"end",
|
|
159
|
+
"state",
|
|
160
|
+
"decision",
|
|
161
|
+
"condition",
|
|
162
|
+
"step",
|
|
163
|
+
"loop",
|
|
164
|
+
"sequence",
|
|
165
|
+
"participant",
|
|
166
|
+
"hub",
|
|
167
|
+
"station",
|
|
168
|
+
"bronze",
|
|
169
|
+
"silver",
|
|
170
|
+
"gold",
|
|
171
|
+
"lane",
|
|
172
|
+
"replica",
|
|
173
|
+
"shard",
|
|
174
|
+
"leader",
|
|
175
|
+
"follower",
|
|
176
|
+
"quorum",
|
|
177
|
+
"consensus",
|
|
178
|
+
"lock",
|
|
179
|
+
"class",
|
|
180
|
+
"interface",
|
|
181
|
+
"method",
|
|
182
|
+
"object",
|
|
183
|
+
"enum",
|
|
184
|
+
"type"
|
|
185
|
+
];
|
|
186
|
+
var VISUAL_PRIMITIVE_TYPES = [
|
|
187
|
+
"panel",
|
|
188
|
+
"surface",
|
|
189
|
+
"terminal",
|
|
190
|
+
"metric",
|
|
191
|
+
"stat",
|
|
192
|
+
"grid",
|
|
193
|
+
"matrix",
|
|
194
|
+
"lane",
|
|
195
|
+
"track",
|
|
196
|
+
"marker",
|
|
197
|
+
"dot",
|
|
198
|
+
"token_strip",
|
|
199
|
+
"chips",
|
|
200
|
+
"glyph_card",
|
|
201
|
+
"glyph",
|
|
202
|
+
"external",
|
|
203
|
+
"optional"
|
|
204
|
+
];
|
|
205
|
+
var VISUAL_PRIMITIVE_KINDS = {
|
|
206
|
+
panel: "flow",
|
|
207
|
+
surface: "flow",
|
|
208
|
+
terminal: "flow",
|
|
209
|
+
metric: "observability",
|
|
210
|
+
stat: "observability",
|
|
211
|
+
grid: "flow",
|
|
212
|
+
matrix: "flow",
|
|
213
|
+
lane: "flow",
|
|
214
|
+
track: "flow",
|
|
215
|
+
marker: "flow",
|
|
216
|
+
dot: "flow",
|
|
217
|
+
token_strip: "flow",
|
|
218
|
+
chips: "flow",
|
|
219
|
+
glyph_card: "flow",
|
|
220
|
+
glyph: "flow",
|
|
221
|
+
external: "network",
|
|
222
|
+
optional: "flow"
|
|
223
|
+
};
|
|
224
|
+
var TECHNICAL_NODE_KINDS = {
|
|
225
|
+
service: "compute",
|
|
226
|
+
api: "compute",
|
|
227
|
+
microservice: "compute",
|
|
228
|
+
backend: "compute",
|
|
229
|
+
server: "compute",
|
|
230
|
+
worker: "compute",
|
|
231
|
+
job: "compute",
|
|
232
|
+
scheduler: "compute",
|
|
233
|
+
cron: "compute",
|
|
234
|
+
batch: "compute",
|
|
235
|
+
function: "compute",
|
|
236
|
+
lambda: "compute",
|
|
237
|
+
edge: "compute",
|
|
238
|
+
controller: "compute",
|
|
239
|
+
handler: "compute",
|
|
240
|
+
repository: "compute",
|
|
241
|
+
runtime: "compute",
|
|
242
|
+
process: "compute",
|
|
243
|
+
module: "code",
|
|
244
|
+
package: "code",
|
|
245
|
+
library: "code",
|
|
246
|
+
sdk: "code",
|
|
247
|
+
cli: "code",
|
|
248
|
+
class: "code",
|
|
249
|
+
interface: "code",
|
|
250
|
+
method: "code",
|
|
251
|
+
object: "code",
|
|
252
|
+
enum: "code",
|
|
253
|
+
type: "code",
|
|
254
|
+
client: "client",
|
|
255
|
+
user: "client",
|
|
256
|
+
browser: "client",
|
|
257
|
+
web: "client",
|
|
258
|
+
mobile: "client",
|
|
259
|
+
desktop: "client",
|
|
260
|
+
frontend: "client",
|
|
261
|
+
app: "client",
|
|
262
|
+
page: "client",
|
|
263
|
+
view: "client",
|
|
264
|
+
component: "client",
|
|
265
|
+
store: "client",
|
|
266
|
+
db: "data",
|
|
267
|
+
database: "data",
|
|
268
|
+
sql: "data",
|
|
269
|
+
nosql: "data",
|
|
270
|
+
table: "data",
|
|
271
|
+
index: "data",
|
|
272
|
+
warehouse: "data",
|
|
273
|
+
lake: "data",
|
|
274
|
+
object_store: "data",
|
|
275
|
+
storage: "data",
|
|
276
|
+
bucket: "data",
|
|
277
|
+
blob: "data",
|
|
278
|
+
volume: "data",
|
|
279
|
+
disk: "data",
|
|
280
|
+
search: "data",
|
|
281
|
+
cache: "data",
|
|
282
|
+
queue: "messaging",
|
|
283
|
+
topic: "messaging",
|
|
284
|
+
stream: "messaging",
|
|
285
|
+
event: "messaging",
|
|
286
|
+
event_bus: "messaging",
|
|
287
|
+
bus: "messaging",
|
|
288
|
+
broker: "messaging",
|
|
289
|
+
pubsub: "messaging",
|
|
290
|
+
kafka: "messaging",
|
|
291
|
+
producer: "messaging",
|
|
292
|
+
consumer: "messaging",
|
|
293
|
+
dead_letter: "messaging",
|
|
294
|
+
dlq: "messaging",
|
|
295
|
+
webhook: "messaging",
|
|
296
|
+
cloud: "network",
|
|
297
|
+
region: "network",
|
|
298
|
+
vpc: "network",
|
|
299
|
+
subnet: "network",
|
|
300
|
+
network: "network",
|
|
301
|
+
internet: "network",
|
|
302
|
+
dns: "network",
|
|
303
|
+
cdn: "network",
|
|
304
|
+
proxy: "network",
|
|
305
|
+
gateway: "network",
|
|
306
|
+
api_gateway: "network",
|
|
307
|
+
load_balancer: "network",
|
|
308
|
+
reverse_proxy: "network",
|
|
309
|
+
router: "network",
|
|
310
|
+
switch: "network",
|
|
311
|
+
nat: "network",
|
|
312
|
+
firewall: "network",
|
|
313
|
+
waf: "network",
|
|
314
|
+
vpn: "network",
|
|
315
|
+
bastion: "network",
|
|
316
|
+
container: "platform",
|
|
317
|
+
cluster: "platform",
|
|
318
|
+
pod: "platform",
|
|
319
|
+
node: "platform",
|
|
320
|
+
deployment: "platform",
|
|
321
|
+
replicaset: "platform",
|
|
322
|
+
statefulset: "platform",
|
|
323
|
+
daemonset: "platform",
|
|
324
|
+
namespace: "platform",
|
|
325
|
+
ingress: "platform",
|
|
326
|
+
service_mesh: "platform",
|
|
327
|
+
sidecar: "platform",
|
|
328
|
+
image: "platform",
|
|
329
|
+
registry: "platform",
|
|
330
|
+
docker: "platform",
|
|
331
|
+
compose: "platform",
|
|
332
|
+
helm: "platform",
|
|
333
|
+
chart: "platform",
|
|
334
|
+
configmap: "platform",
|
|
335
|
+
pvc: "platform",
|
|
336
|
+
auth: "security",
|
|
337
|
+
identity: "security",
|
|
338
|
+
oauth: "security",
|
|
339
|
+
oidc: "security",
|
|
340
|
+
jwt: "security",
|
|
341
|
+
session: "security",
|
|
342
|
+
policy: "security",
|
|
343
|
+
role: "security",
|
|
344
|
+
permission: "security",
|
|
345
|
+
vault: "security",
|
|
346
|
+
secret: "security",
|
|
347
|
+
key: "security",
|
|
348
|
+
certificate: "security",
|
|
349
|
+
security: "security",
|
|
350
|
+
repo: "delivery",
|
|
351
|
+
branch: "delivery",
|
|
352
|
+
commit: "delivery",
|
|
353
|
+
pipeline: "delivery",
|
|
354
|
+
workflow: "delivery",
|
|
355
|
+
runner: "delivery",
|
|
356
|
+
build: "delivery",
|
|
357
|
+
test: "delivery",
|
|
358
|
+
artifact: "delivery",
|
|
359
|
+
deploy: "delivery",
|
|
360
|
+
release: "delivery",
|
|
361
|
+
environment: "delivery",
|
|
362
|
+
preview: "delivery",
|
|
363
|
+
monitor: "observability",
|
|
364
|
+
metrics: "observability",
|
|
365
|
+
logs: "observability",
|
|
366
|
+
trace: "observability",
|
|
367
|
+
alert: "observability",
|
|
368
|
+
dashboard: "observability",
|
|
369
|
+
probe: "observability",
|
|
370
|
+
slo: "observability",
|
|
371
|
+
start: "flow",
|
|
372
|
+
end: "flow",
|
|
373
|
+
state: "flow",
|
|
374
|
+
decision: "flow",
|
|
375
|
+
condition: "flow",
|
|
376
|
+
step: "flow",
|
|
377
|
+
loop: "flow",
|
|
378
|
+
sequence: "flow",
|
|
379
|
+
participant: "flow",
|
|
380
|
+
hub: "data",
|
|
381
|
+
station: "compute",
|
|
382
|
+
bronze: "data",
|
|
383
|
+
silver: "data",
|
|
384
|
+
gold: "data",
|
|
385
|
+
lane: "flow",
|
|
386
|
+
replica: "distributed",
|
|
387
|
+
shard: "distributed",
|
|
388
|
+
leader: "distributed",
|
|
389
|
+
follower: "distributed",
|
|
390
|
+
quorum: "distributed",
|
|
391
|
+
consensus: "distributed",
|
|
392
|
+
lock: "distributed"
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// ../core/src/player.ts
|
|
396
|
+
function parseBooleanToken(raw) {
|
|
397
|
+
if (typeof raw === "boolean") return raw;
|
|
398
|
+
const s = String(raw).trim().toLowerCase();
|
|
399
|
+
if (["true", "on", "yes", "1"].includes(s)) return true;
|
|
400
|
+
if (["false", "off", "no", "0"].includes(s)) return false;
|
|
401
|
+
return void 0;
|
|
402
|
+
}
|
|
403
|
+
var CONTROL_KEYS = [
|
|
404
|
+
"play",
|
|
405
|
+
"restart",
|
|
406
|
+
"prevBeat",
|
|
407
|
+
"nextBeat",
|
|
408
|
+
"seek",
|
|
409
|
+
"speed",
|
|
410
|
+
"fit",
|
|
411
|
+
"resetView",
|
|
412
|
+
"fullscreen",
|
|
413
|
+
"svg",
|
|
414
|
+
"share",
|
|
415
|
+
"code",
|
|
416
|
+
"theme"
|
|
417
|
+
];
|
|
418
|
+
var INTERACTION_KEYS = ["zoom", "pan", "doubleClickToReset"];
|
|
419
|
+
var PLAYBACK = {
|
|
420
|
+
autoplay: { group: "playback", key: "autoplay", type: "boolean" },
|
|
421
|
+
loop: { group: "playback", key: "loop", type: "boolean" },
|
|
422
|
+
rate: { group: "playback", key: "rate", type: "rate" },
|
|
423
|
+
speed: { group: "playback", key: "rate", type: "rate" },
|
|
424
|
+
playbackRate: { group: "playback", key: "rate", type: "rate" },
|
|
425
|
+
playback_rate: { group: "playback", key: "rate", type: "rate" }
|
|
426
|
+
};
|
|
427
|
+
var CONTROLS = {
|
|
428
|
+
controls: { group: "controls", key: "*", type: "group" },
|
|
429
|
+
play: { group: "controls", key: "play", type: "boolean" },
|
|
430
|
+
playButton: { group: "controls", key: "play", type: "boolean" },
|
|
431
|
+
play_button: { group: "controls", key: "play", type: "boolean" },
|
|
432
|
+
restart: { group: "controls", key: "restart", type: "boolean" },
|
|
433
|
+
restartButton: { group: "controls", key: "restart", type: "boolean" },
|
|
434
|
+
restart_button: { group: "controls", key: "restart", type: "boolean" },
|
|
435
|
+
prevBeat: { group: "controls", key: "prevBeat", type: "boolean" },
|
|
436
|
+
prev_beat: { group: "controls", key: "prevBeat", type: "boolean" },
|
|
437
|
+
nextBeat: { group: "controls", key: "nextBeat", type: "boolean" },
|
|
438
|
+
next_beat: { group: "controls", key: "nextBeat", type: "boolean" },
|
|
439
|
+
speeds: { group: "controls", key: "speeds", type: "rates" },
|
|
440
|
+
speedOptions: { group: "controls", key: "speeds", type: "rates" },
|
|
441
|
+
speed_options: { group: "controls", key: "speeds", type: "rates" },
|
|
442
|
+
seek: { group: "controls", key: "seek", type: "boolean" },
|
|
443
|
+
seekBar: { group: "controls", key: "seek", type: "boolean" },
|
|
444
|
+
seek_bar: { group: "controls", key: "seek", type: "boolean" },
|
|
445
|
+
speed: { group: "controls", key: "speed", type: "boolean" },
|
|
446
|
+
speedControls: { group: "controls", key: "speed", type: "boolean" },
|
|
447
|
+
speed_controls: { group: "controls", key: "speed", type: "boolean" },
|
|
448
|
+
fit: { group: "controls", key: "fit", type: "boolean" },
|
|
449
|
+
fitView: { group: "controls", key: "fit", type: "boolean" },
|
|
450
|
+
fit_view: { group: "controls", key: "fit", type: "boolean" },
|
|
451
|
+
fitViewButton: { group: "controls", key: "fit", type: "boolean" },
|
|
452
|
+
fit_view_button: { group: "controls", key: "fit", type: "boolean" },
|
|
453
|
+
resetView: { group: "controls", key: "resetView", type: "boolean" },
|
|
454
|
+
reset_view: { group: "controls", key: "resetView", type: "boolean" },
|
|
455
|
+
resetViewButton: { group: "controls", key: "resetView", type: "boolean" },
|
|
456
|
+
reset_view_button: { group: "controls", key: "resetView", type: "boolean" },
|
|
457
|
+
fullscreen: { group: "controls", key: "fullscreen", type: "boolean" },
|
|
458
|
+
fullScreen: { group: "controls", key: "fullscreen", type: "boolean" },
|
|
459
|
+
full_screen: { group: "controls", key: "fullscreen", type: "boolean" },
|
|
460
|
+
fullscreenButton: { group: "controls", key: "fullscreen", type: "boolean" },
|
|
461
|
+
fullscreen_button: { group: "controls", key: "fullscreen", type: "boolean" },
|
|
462
|
+
svg: { group: "controls", key: "svg", type: "boolean" },
|
|
463
|
+
exportSvg: { group: "controls", key: "svg", type: "boolean" },
|
|
464
|
+
export_svg: { group: "controls", key: "svg", type: "boolean" },
|
|
465
|
+
share: { group: "controls", key: "share", type: "boolean" },
|
|
466
|
+
shareLink: { group: "controls", key: "share", type: "boolean" },
|
|
467
|
+
share_link: { group: "controls", key: "share", type: "boolean" },
|
|
468
|
+
code: { group: "controls", key: "code", type: "boolean" },
|
|
469
|
+
codeButton: { group: "controls", key: "code", type: "boolean" },
|
|
470
|
+
code_button: { group: "controls", key: "code", type: "boolean" },
|
|
471
|
+
exposeCode: { group: "controls", key: "code", type: "boolean" },
|
|
472
|
+
expose_code: { group: "controls", key: "code", type: "boolean" },
|
|
473
|
+
viewSource: { group: "controls", key: "code", type: "boolean" },
|
|
474
|
+
view_source: { group: "controls", key: "code", type: "boolean" },
|
|
475
|
+
theme: { group: "controls", key: "theme", type: "boolean" },
|
|
476
|
+
themeButton: { group: "controls", key: "theme", type: "boolean" },
|
|
477
|
+
theme_button: { group: "controls", key: "theme", type: "boolean" },
|
|
478
|
+
switchTheme: { group: "controls", key: "theme", type: "boolean" },
|
|
479
|
+
switch_theme: { group: "controls", key: "theme", type: "boolean" }
|
|
480
|
+
};
|
|
481
|
+
var INTERACTION = {
|
|
482
|
+
interactive: { group: "interaction", key: "*", type: "group" },
|
|
483
|
+
interactiveViewport: { group: "interaction", key: "*", type: "group" },
|
|
484
|
+
interactive_viewport: { group: "interaction", key: "*", type: "group" },
|
|
485
|
+
zoom: { group: "interaction", key: "zoom", type: "boolean" },
|
|
486
|
+
allowZoom: { group: "interaction", key: "zoom", type: "boolean" },
|
|
487
|
+
allow_zoom: { group: "interaction", key: "zoom", type: "boolean" },
|
|
488
|
+
pan: { group: "interaction", key: "pan", type: "boolean" },
|
|
489
|
+
allowPan: { group: "interaction", key: "pan", type: "boolean" },
|
|
490
|
+
allow_pan: { group: "interaction", key: "pan", type: "boolean" },
|
|
491
|
+
clickToPlay: { group: "interaction", key: "clickToPlay", type: "boolean" },
|
|
492
|
+
click_to_play: { group: "interaction", key: "clickToPlay", type: "boolean" },
|
|
493
|
+
keyboard: { group: "interaction", key: "keyboard", type: "boolean" },
|
|
494
|
+
shortcuts: { group: "interaction", key: "keyboard", type: "boolean" },
|
|
495
|
+
doubleClickToReset: { group: "interaction", key: "doubleClickToReset", type: "boolean" },
|
|
496
|
+
double_click_to_reset: { group: "interaction", key: "doubleClickToReset", type: "boolean" }
|
|
497
|
+
};
|
|
498
|
+
var CHROME = {
|
|
499
|
+
badge: { group: "chrome", key: "badge", type: "boolean" },
|
|
500
|
+
copyright: { group: "chrome", key: "badge", type: "boolean" },
|
|
501
|
+
progress: { group: "chrome", key: "progress", type: "progress" },
|
|
502
|
+
progressBar: { group: "chrome", key: "progress", type: "progress" },
|
|
503
|
+
progress_bar: { group: "chrome", key: "progress", type: "progress" },
|
|
504
|
+
sceneBoundaryProgress: { group: "chrome", key: "progress", type: "progress" },
|
|
505
|
+
progressColor: { group: "chrome", key: "progressColor", type: "color" },
|
|
506
|
+
progress_color: { group: "chrome", key: "progressColor", type: "color" },
|
|
507
|
+
progressBarColor: { group: "chrome", key: "progressColor", type: "color" },
|
|
508
|
+
progress_bar_color: { group: "chrome", key: "progressColor", type: "color" }
|
|
509
|
+
};
|
|
510
|
+
var FLAT = {
|
|
511
|
+
...PLAYBACK,
|
|
512
|
+
...CHROME,
|
|
513
|
+
controls: CONTROLS.controls,
|
|
514
|
+
playButton: CONTROLS.playButton,
|
|
515
|
+
play_button: CONTROLS.play_button,
|
|
516
|
+
restartButton: CONTROLS.restartButton,
|
|
517
|
+
restart_button: CONTROLS.restart_button,
|
|
518
|
+
seek: CONTROLS.seek,
|
|
519
|
+
seekBar: CONTROLS.seekBar,
|
|
520
|
+
seek_bar: CONTROLS.seek_bar,
|
|
521
|
+
speedControls: CONTROLS.speedControls,
|
|
522
|
+
speed_controls: CONTROLS.speed_controls,
|
|
523
|
+
speeds: CONTROLS.speeds,
|
|
524
|
+
speedOptions: CONTROLS.speedOptions,
|
|
525
|
+
speed_options: CONTROLS.speed_options,
|
|
526
|
+
prevBeat: CONTROLS.prevBeat,
|
|
527
|
+
prev_beat: CONTROLS.prev_beat,
|
|
528
|
+
nextBeat: CONTROLS.nextBeat,
|
|
529
|
+
next_beat: CONTROLS.next_beat,
|
|
530
|
+
keyboard: INTERACTION.keyboard,
|
|
531
|
+
shortcuts: INTERACTION.shortcuts,
|
|
532
|
+
fitView: CONTROLS.fitView,
|
|
533
|
+
fit_view: CONTROLS.fit_view,
|
|
534
|
+
fitViewButton: CONTROLS.fitViewButton,
|
|
535
|
+
fit_view_button: CONTROLS.fit_view_button,
|
|
536
|
+
resetViewButton: CONTROLS.resetViewButton,
|
|
537
|
+
reset_view_button: CONTROLS.reset_view_button,
|
|
538
|
+
fullscreen: CONTROLS.fullscreen,
|
|
539
|
+
fullScreen: CONTROLS.fullScreen,
|
|
540
|
+
full_screen: CONTROLS.full_screen,
|
|
541
|
+
fullscreenButton: CONTROLS.fullscreenButton,
|
|
542
|
+
fullscreen_button: CONTROLS.fullscreen_button,
|
|
543
|
+
exportSvg: CONTROLS.exportSvg,
|
|
544
|
+
export_svg: CONTROLS.export_svg,
|
|
545
|
+
shareLink: CONTROLS.shareLink,
|
|
546
|
+
share_link: CONTROLS.share_link,
|
|
547
|
+
code: CONTROLS.code,
|
|
548
|
+
codeButton: CONTROLS.codeButton,
|
|
549
|
+
code_button: CONTROLS.code_button,
|
|
550
|
+
exposeCode: CONTROLS.exposeCode,
|
|
551
|
+
expose_code: CONTROLS.expose_code,
|
|
552
|
+
viewSource: CONTROLS.viewSource,
|
|
553
|
+
view_source: CONTROLS.view_source,
|
|
554
|
+
interactive: INTERACTION.interactive,
|
|
555
|
+
interactiveViewport: INTERACTION.interactiveViewport,
|
|
556
|
+
interactive_viewport: INTERACTION.interactive_viewport,
|
|
557
|
+
allowZoom: INTERACTION.allowZoom,
|
|
558
|
+
allow_zoom: INTERACTION.allow_zoom,
|
|
559
|
+
allowPan: INTERACTION.allowPan,
|
|
560
|
+
allow_pan: INTERACTION.allow_pan,
|
|
561
|
+
clickToPlay: INTERACTION.clickToPlay,
|
|
562
|
+
click_to_play: INTERACTION.click_to_play,
|
|
563
|
+
doubleClickToReset: INTERACTION.doubleClickToReset,
|
|
564
|
+
double_click_to_reset: INTERACTION.double_click_to_reset
|
|
565
|
+
};
|
|
566
|
+
var SCOPES = {
|
|
567
|
+
player: FLAT,
|
|
568
|
+
playback: PLAYBACK,
|
|
569
|
+
controls: CONTROLS,
|
|
570
|
+
interaction: INTERACTION,
|
|
571
|
+
// `color` is unambiguous inside the block, but too generic at the root.
|
|
572
|
+
chrome: { ...CHROME, color: CHROME.progressColor }
|
|
573
|
+
};
|
|
574
|
+
var PLAYER_FLAT_KEYS = Object.keys(FLAT);
|
|
575
|
+
function applyPlayerSetting(config, scope, key, rawValue) {
|
|
576
|
+
const setting = SCOPES[scope][key];
|
|
577
|
+
if (!setting) return `unknown player property '${key}'`;
|
|
578
|
+
const value = unquote(rawValue).trim();
|
|
579
|
+
const group = config[setting.group] ??= {};
|
|
580
|
+
if (setting.type === "boolean" || setting.type === "group") {
|
|
581
|
+
const parsed = value ? parseBooleanToken(value) : true;
|
|
582
|
+
if (parsed === void 0) return `player property '${key}' expects true or false`;
|
|
583
|
+
if (setting.type === "boolean") group[setting.key] = parsed;
|
|
584
|
+
else for (const child of setting.group === "controls" ? CONTROL_KEYS : INTERACTION_KEYS) group[child] = parsed;
|
|
585
|
+
return void 0;
|
|
586
|
+
}
|
|
587
|
+
if (setting.type === "rate") {
|
|
588
|
+
const rate = Number(value);
|
|
589
|
+
if (!Number.isFinite(rate) || rate <= 0) return `player property '${key}' expects a positive number`;
|
|
590
|
+
group[setting.key] = rate;
|
|
591
|
+
return void 0;
|
|
592
|
+
}
|
|
593
|
+
if (setting.type === "rates") {
|
|
594
|
+
const rates = value.split(/[\s,]+/).filter(Boolean).map(Number);
|
|
595
|
+
if (!rates.length || rates.some((rate) => !Number.isFinite(rate) || rate <= 0)) {
|
|
596
|
+
return `player property '${key}' expects a list of positive numbers`;
|
|
597
|
+
}
|
|
598
|
+
group[setting.key] = rates;
|
|
599
|
+
return void 0;
|
|
600
|
+
}
|
|
601
|
+
if (setting.type === "color") {
|
|
602
|
+
if (value) group[setting.key] = value;
|
|
603
|
+
return void 0;
|
|
604
|
+
}
|
|
605
|
+
const mode = toProgressMode(value);
|
|
606
|
+
if (mode) group.progress = mode;
|
|
607
|
+
else if (value) group.progressColor = value;
|
|
608
|
+
return void 0;
|
|
609
|
+
}
|
|
610
|
+
function toProgressMode(value) {
|
|
611
|
+
const lower = value.toLowerCase();
|
|
612
|
+
if (lower === "none" || lower === "bar" || lower === "boundary") return lower;
|
|
613
|
+
const bool = parseBooleanToken(lower);
|
|
614
|
+
if (bool === true) return "boundary";
|
|
615
|
+
if (bool === false) return "none";
|
|
616
|
+
return void 0;
|
|
617
|
+
}
|
|
618
|
+
function unquote(raw) {
|
|
619
|
+
const value = raw.trim();
|
|
620
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
621
|
+
return value.slice(1, -1);
|
|
622
|
+
}
|
|
623
|
+
return value;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// ../core/src/registry.ts
|
|
627
|
+
var NODE_KINDS = /* @__PURE__ */ new Set([
|
|
628
|
+
...TECHNICAL_NODE_TYPES,
|
|
629
|
+
...VISUAL_PRIMITIVE_TYPES
|
|
630
|
+
]);
|
|
631
|
+
var DIAGRAM_TYPES = /* @__PURE__ */ new Set([
|
|
632
|
+
"architecture",
|
|
633
|
+
"flowchart",
|
|
634
|
+
"tree",
|
|
635
|
+
"state",
|
|
636
|
+
"sequence",
|
|
637
|
+
"constellation",
|
|
638
|
+
"loop",
|
|
639
|
+
"flywheel",
|
|
640
|
+
"medallion",
|
|
641
|
+
"quadrant",
|
|
642
|
+
"swimlane",
|
|
643
|
+
"pyramid",
|
|
644
|
+
"radar",
|
|
645
|
+
"timeline",
|
|
646
|
+
"gantt",
|
|
647
|
+
"venn",
|
|
648
|
+
"layers",
|
|
649
|
+
"nested"
|
|
650
|
+
]);
|
|
651
|
+
var EDGE_OPERATORS = {
|
|
652
|
+
"->": "request",
|
|
653
|
+
"<-": "response",
|
|
654
|
+
"~>": "event",
|
|
655
|
+
"--": "dependency"
|
|
656
|
+
};
|
|
657
|
+
var RESERVED_SELECTORS = /* @__PURE__ */ new Set(["$title", "$nodes", "$edges"]);
|
|
658
|
+
var CUE_ALIASES = {
|
|
659
|
+
pulse: "focus",
|
|
660
|
+
highlight: "glow",
|
|
661
|
+
emphasize: "glow"
|
|
662
|
+
};
|
|
663
|
+
var BEAT_CUE_KEYWORDS = /* @__PURE__ */ new Set([
|
|
664
|
+
"show",
|
|
665
|
+
"hide",
|
|
666
|
+
"glow",
|
|
667
|
+
"focus",
|
|
668
|
+
"frame",
|
|
669
|
+
"use",
|
|
670
|
+
...Object.keys(CUE_ALIASES)
|
|
671
|
+
]);
|
|
672
|
+
var SCENE_KEYS = /* @__PURE__ */ new Set([
|
|
673
|
+
"width",
|
|
674
|
+
"height",
|
|
675
|
+
"fps",
|
|
676
|
+
"theme",
|
|
677
|
+
"duration",
|
|
678
|
+
"direction",
|
|
679
|
+
"layout",
|
|
680
|
+
"type",
|
|
681
|
+
...PLAYER_FLAT_KEYS
|
|
682
|
+
]);
|
|
683
|
+
function nodeRole(kind) {
|
|
684
|
+
const canonical = canonicalNodeKind(kind);
|
|
685
|
+
return TECHNICAL_NODE_KINDS[canonical] ?? VISUAL_PRIMITIVE_KINDS[canonical] ?? "compute";
|
|
686
|
+
}
|
|
687
|
+
function humanizeId(id) {
|
|
688
|
+
const acronyms = /* @__PURE__ */ new Set(["api", "cdn", "db", "dns", "http", "https", "id", "jwt", "oidc", "sdk", "tls", "ui", "url"]);
|
|
689
|
+
const exactCase = /* @__PURE__ */ new Map([
|
|
690
|
+
["etcd", "etcd"],
|
|
691
|
+
["kubectl", "kubectl"]
|
|
692
|
+
]);
|
|
693
|
+
return id.replace(/[_-]+/g, " ").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/([a-z\d])([A-Z])/g, "$1 $2").split(/\s+/).filter(Boolean).map((word) => {
|
|
694
|
+
const lower = word.toLowerCase();
|
|
695
|
+
const exact = exactCase.get(lower);
|
|
696
|
+
if (exact) return exact;
|
|
697
|
+
if (acronyms.has(lower)) return lower.toUpperCase();
|
|
698
|
+
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
699
|
+
}).join(" ");
|
|
700
|
+
}
|
|
701
|
+
var NODE_ALIASES = {
|
|
702
|
+
db: "database",
|
|
703
|
+
api: "service",
|
|
704
|
+
gateway: "api_gateway",
|
|
705
|
+
mq: "queue",
|
|
706
|
+
k8s: "cluster",
|
|
707
|
+
lb: "load_balancer",
|
|
708
|
+
panel: "surface",
|
|
709
|
+
metric: "stat",
|
|
710
|
+
grid: "matrix",
|
|
711
|
+
lane: "track",
|
|
712
|
+
marker: "dot",
|
|
713
|
+
chips: "token_strip",
|
|
714
|
+
glyph: "glyph_card"
|
|
715
|
+
};
|
|
716
|
+
function canonicalNodeKind(kind) {
|
|
717
|
+
return NODE_ALIASES[kind] ?? kind;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// ../core/src/parser.ts
|
|
721
|
+
var ParseError = class extends Error {
|
|
722
|
+
line;
|
|
723
|
+
column;
|
|
724
|
+
constructor(message, line, column) {
|
|
725
|
+
super(`line ${line}: ${message}`);
|
|
726
|
+
this.name = "ParseError";
|
|
727
|
+
this.line = line;
|
|
728
|
+
this.column = column;
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
var FLOW_OP_RE = /(->|<-|~>|--)/;
|
|
732
|
+
function stripComment(line) {
|
|
733
|
+
let inString = false;
|
|
734
|
+
let escaped = false;
|
|
735
|
+
for (let i = 0; i < line.length; i++) {
|
|
736
|
+
const ch = line[i];
|
|
737
|
+
if (escaped) {
|
|
738
|
+
escaped = false;
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
if (inString && ch === "\\") {
|
|
742
|
+
escaped = true;
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
if (ch === '"') {
|
|
746
|
+
inString = !inString;
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
if (inString) continue;
|
|
750
|
+
if (ch === "/" && line[i + 1] === "/") return line.slice(0, i);
|
|
751
|
+
if (ch === "#" && (i === 0 || /\s/.test(line[i - 1])) && (i + 1 >= line.length || /\s/.test(line[i + 1]))) {
|
|
752
|
+
return line.slice(0, i);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return line;
|
|
756
|
+
}
|
|
757
|
+
function parsePropValue(raw) {
|
|
758
|
+
let val = raw;
|
|
759
|
+
if (raw.startsWith('"')) {
|
|
760
|
+
const parsed = parseStringToken(raw);
|
|
761
|
+
if (parsed && parsed.rest === "") val = parsed.value;
|
|
762
|
+
}
|
|
763
|
+
if (typeof val === "string") {
|
|
764
|
+
if (/^\d+(\.\d+)?$/.test(val)) val = Number(val);
|
|
765
|
+
else if (val === "true") val = true;
|
|
766
|
+
else if (val === "false") val = false;
|
|
767
|
+
else if (/^\d+ms$/.test(val)) val = Number(val.slice(0, -2)) / 1e3;
|
|
768
|
+
else if (/^\d+(\.\d+)?s$/.test(val)) val = Number(val.slice(0, -1));
|
|
769
|
+
}
|
|
770
|
+
return val;
|
|
771
|
+
}
|
|
772
|
+
function parseProps(raw) {
|
|
773
|
+
const props = {};
|
|
774
|
+
let i = 0;
|
|
775
|
+
while (i < raw.length) {
|
|
776
|
+
const ch = raw[i];
|
|
777
|
+
if (ch === '"') {
|
|
778
|
+
const parsed = parseStringToken(raw.slice(i));
|
|
779
|
+
if (!parsed) break;
|
|
780
|
+
i = raw.length - parsed.rest.length;
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
const keyMatch = raw.slice(i).match(/^(\w[\w.-]*)=/);
|
|
784
|
+
if (!keyMatch) {
|
|
785
|
+
i++;
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
const key = keyMatch[1];
|
|
789
|
+
i += key.length + 1;
|
|
790
|
+
let value = "";
|
|
791
|
+
if (raw[i] === '"') {
|
|
792
|
+
const start = i;
|
|
793
|
+
const parsed = parseStringToken(raw.slice(i));
|
|
794
|
+
if (!parsed) {
|
|
795
|
+
value = raw.slice(i);
|
|
796
|
+
i = raw.length;
|
|
797
|
+
} else {
|
|
798
|
+
i = raw.length - parsed.rest.length;
|
|
799
|
+
value = raw.slice(start, i).trim();
|
|
800
|
+
}
|
|
801
|
+
} else {
|
|
802
|
+
const start = i;
|
|
803
|
+
while (i < raw.length && !/\s/.test(raw[i])) i++;
|
|
804
|
+
value = raw.slice(start, i);
|
|
805
|
+
}
|
|
806
|
+
props[key] = parsePropValue(value);
|
|
807
|
+
}
|
|
808
|
+
return props;
|
|
809
|
+
}
|
|
810
|
+
function parseStringToken(raw) {
|
|
811
|
+
const trimmed = raw.trimStart();
|
|
812
|
+
if (!trimmed.startsWith('"')) return null;
|
|
813
|
+
let i = 1;
|
|
814
|
+
let value = "";
|
|
815
|
+
while (i < trimmed.length) {
|
|
816
|
+
const ch = trimmed[i];
|
|
817
|
+
if (ch === '"') return { value, rest: trimmed.slice(i + 1).trim() };
|
|
818
|
+
if (ch === "\\" && i + 1 < trimmed.length) {
|
|
819
|
+
value += trimmed[i + 1];
|
|
820
|
+
i += 2;
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
value += ch;
|
|
824
|
+
i++;
|
|
825
|
+
}
|
|
826
|
+
return null;
|
|
827
|
+
}
|
|
828
|
+
function splitTargets(raw) {
|
|
829
|
+
const tokens = raw.match(/"(?:[^"\\]|\\.)*"|[^\s,]+/g) ?? [];
|
|
830
|
+
return tokens.map((token) => token.trim()).filter((token) => token.length > 0 && !token.startsWith('"'));
|
|
831
|
+
}
|
|
832
|
+
function splitOutsideQuotes(raw, separator) {
|
|
833
|
+
const parts = [];
|
|
834
|
+
let start = 0;
|
|
835
|
+
let inString = false;
|
|
836
|
+
let escaped = false;
|
|
837
|
+
for (let i = 0; i < raw.length; i++) {
|
|
838
|
+
const ch = raw[i];
|
|
839
|
+
if (escaped) {
|
|
840
|
+
escaped = false;
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
if (inString && ch === "\\") {
|
|
844
|
+
escaped = true;
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
if (ch === '"') {
|
|
848
|
+
inString = !inString;
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
if (!inString && ch === separator && /\s/.test(raw[i - 1] ?? "") && /\s/.test(raw[i + 1] ?? "")) {
|
|
852
|
+
parts.push(raw.slice(start, i).trim());
|
|
853
|
+
start = i + 1;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
parts.push(raw.slice(start).trim());
|
|
857
|
+
return parts.filter(Boolean);
|
|
858
|
+
}
|
|
859
|
+
function stripCueProps(raw, keys) {
|
|
860
|
+
let inString = false;
|
|
861
|
+
let escaped = false;
|
|
862
|
+
for (let i = 0; i < raw.length; i++) {
|
|
863
|
+
const ch = raw[i];
|
|
864
|
+
if (escaped) {
|
|
865
|
+
escaped = false;
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
if (inString && ch === "\\") {
|
|
869
|
+
escaped = true;
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
if (ch === '"') {
|
|
873
|
+
inString = !inString;
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
if (inString || !/\s/.test(ch)) continue;
|
|
877
|
+
const rest = raw.slice(i + 1);
|
|
878
|
+
if (keys.some((key) => rest.startsWith(`${key}=`))) return raw.slice(0, i).trim();
|
|
879
|
+
}
|
|
880
|
+
return raw.trim();
|
|
881
|
+
}
|
|
882
|
+
function tokenizeFlowChain(line) {
|
|
883
|
+
const parts = [];
|
|
884
|
+
let current = "";
|
|
885
|
+
let inString = false;
|
|
886
|
+
let escaped = false;
|
|
887
|
+
for (let i = 0; i < line.length; i++) {
|
|
888
|
+
const ch = line[i];
|
|
889
|
+
if (inString) {
|
|
890
|
+
current += ch;
|
|
891
|
+
if (escaped) escaped = false;
|
|
892
|
+
else if (ch === "\\") escaped = true;
|
|
893
|
+
else if (ch === '"') inString = false;
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
if (ch === '"') {
|
|
897
|
+
inString = true;
|
|
898
|
+
current += ch;
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
const op = line.slice(i, i + 2);
|
|
902
|
+
if (op === "->" || op === "<-" || op === "~>" || op === "--") {
|
|
903
|
+
if (current.trim()) parts.push(current.trim());
|
|
904
|
+
parts.push(op);
|
|
905
|
+
current = "";
|
|
906
|
+
i += 1;
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
current += ch;
|
|
910
|
+
}
|
|
911
|
+
if (current.trim()) parts.push(current.trim());
|
|
912
|
+
return parts;
|
|
913
|
+
}
|
|
914
|
+
function parseFlowChain(line, lineNo) {
|
|
915
|
+
const segments = [];
|
|
916
|
+
const parts = tokenizeFlowChain(line);
|
|
917
|
+
if (parts.length < 3) {
|
|
918
|
+
throw new ParseError(`expected flow chain like A -> B "label"`, lineNo);
|
|
919
|
+
}
|
|
920
|
+
let i = 0;
|
|
921
|
+
let from = splitTargetLabel(parts[i++], lineNo).node;
|
|
922
|
+
while (i < parts.length) {
|
|
923
|
+
const opToken = parts[i++];
|
|
924
|
+
const op = EDGE_OPERATORS[opToken];
|
|
925
|
+
if (!op) throw new ParseError(`unknown flow operator '${opToken}'`, lineNo);
|
|
926
|
+
if (i >= parts.length) throw new ParseError(`expected target after '${opToken}'`, lineNo);
|
|
927
|
+
const { node: to, label } = splitTargetLabel(parts[i++], lineNo);
|
|
928
|
+
if (!to) throw new ParseError(`expected target node after '${opToken}'`, lineNo);
|
|
929
|
+
segments.push({
|
|
930
|
+
from: op === "response" ? to : from,
|
|
931
|
+
op,
|
|
932
|
+
to: op === "response" ? from : to,
|
|
933
|
+
label
|
|
934
|
+
});
|
|
935
|
+
from = op === "response" ? from : to;
|
|
936
|
+
}
|
|
937
|
+
return segments;
|
|
938
|
+
}
|
|
939
|
+
function splitTargetLabel(token, lineNo) {
|
|
940
|
+
const quoteIdx = token.indexOf('"');
|
|
941
|
+
if (quoteIdx < 0) return { node: token.trim() };
|
|
942
|
+
const parsed = parseStringToken(token.slice(quoteIdx));
|
|
943
|
+
if (!parsed) throw new ParseError("unterminated string in flow label", lineNo);
|
|
944
|
+
const before = token.slice(0, quoteIdx).trim();
|
|
945
|
+
const node = before || parsed.rest.trim();
|
|
946
|
+
return { node, label: parsed.value };
|
|
947
|
+
}
|
|
948
|
+
function parseCueLine(line, lineNo) {
|
|
949
|
+
const trimmed = line.trim();
|
|
950
|
+
const props = parseProps(trimmed);
|
|
951
|
+
if (/^@\+?\d/.test(trimmed) || /^\w[\w.-]*\.\w+\(/.test(trimmed) || /^camera\./.test(trimmed)) {
|
|
952
|
+
throw new ParseError(
|
|
953
|
+
"unsupported timeline command; use beat cues like show, frame, focus, glow, and flow lines",
|
|
954
|
+
lineNo
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
const parallelParts = splitOutsideQuotes(trimmed, "&");
|
|
958
|
+
if (parallelParts.length > 1) {
|
|
959
|
+
return {
|
|
960
|
+
kind: "parallel",
|
|
961
|
+
cues: parallelParts.map((p, idx) => parseCueLine(p, lineNo + idx * 1e-3)),
|
|
962
|
+
line: lineNo
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
if (FLOW_OP_RE.test(trimmed)) {
|
|
966
|
+
const chainPart = stripCueProps(trimmed, ["dur", "stagger", "color", "strength", "zoom", "after"]);
|
|
967
|
+
return {
|
|
968
|
+
kind: "flow",
|
|
969
|
+
segments: parseFlowChain(chainPart, lineNo),
|
|
970
|
+
dur: typeof props.dur === "number" ? props.dur : void 0,
|
|
971
|
+
line: lineNo
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
const [head, ...rest] = trimmed.split(/\s+/);
|
|
975
|
+
const rawKeyword = head.toLowerCase();
|
|
976
|
+
const keyword = CUE_ALIASES[rawKeyword] ?? rawKeyword;
|
|
977
|
+
if (keyword === "show" || keyword === "hide") {
|
|
978
|
+
const targetRaw = stripCueProps(rest.join(" "), ["dur", "stagger"]);
|
|
979
|
+
return {
|
|
980
|
+
kind: keyword,
|
|
981
|
+
targets: splitTargets(targetRaw),
|
|
982
|
+
stagger: typeof props.stagger === "number" ? props.stagger : void 0,
|
|
983
|
+
dur: typeof props.dur === "number" ? props.dur : void 0,
|
|
984
|
+
line: lineNo
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
if (keyword === "glow") {
|
|
988
|
+
const targetRaw = stripCueProps(rest.join(" "), ["color", "strength", "dur"]);
|
|
989
|
+
return {
|
|
990
|
+
kind: "glow",
|
|
991
|
+
targets: splitTargets(targetRaw),
|
|
992
|
+
color: typeof props.color === "string" ? props.color : void 0,
|
|
993
|
+
strength: typeof props.strength === "number" ? props.strength : void 0,
|
|
994
|
+
dur: typeof props.dur === "number" ? props.dur : void 0,
|
|
995
|
+
line: lineNo
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
if (keyword === "focus") {
|
|
999
|
+
const targetRaw = stripCueProps(rest.join(" "), ["zoom", "dur"]);
|
|
1000
|
+
return {
|
|
1001
|
+
kind: "focus",
|
|
1002
|
+
targets: splitTargets(targetRaw),
|
|
1003
|
+
zoom: typeof props.zoom === "number" ? props.zoom : void 0,
|
|
1004
|
+
dur: typeof props.dur === "number" ? props.dur : void 0,
|
|
1005
|
+
line: lineNo
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
if (keyword === "frame") {
|
|
1009
|
+
const targetRaw = stripCueProps(rest.join(" "), ["zoom", "dur"]);
|
|
1010
|
+
const targets = splitTargets(targetRaw);
|
|
1011
|
+
if (targets.length === 0) throw new ParseError(`expected frame target`, lineNo);
|
|
1012
|
+
return {
|
|
1013
|
+
kind: "frame",
|
|
1014
|
+
targets,
|
|
1015
|
+
zoom: typeof props.zoom === "number" ? props.zoom : void 0,
|
|
1016
|
+
dur: typeof props.dur === "number" ? props.dur : void 0,
|
|
1017
|
+
line: lineNo
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
if (keyword === "use") {
|
|
1021
|
+
const call = rest.join(" ");
|
|
1022
|
+
const m = call.match(/^(\w+)\s*\((.*)\)\s*$/);
|
|
1023
|
+
if (!m) throw new ParseError(`expected use pattern(args)`, lineNo);
|
|
1024
|
+
const args = {};
|
|
1025
|
+
if (m[2].trim()) {
|
|
1026
|
+
for (const part of m[2].split(",")) {
|
|
1027
|
+
const trimmed2 = part.trim();
|
|
1028
|
+
if (trimmed2.includes("=")) {
|
|
1029
|
+
const [k, v] = trimmed2.split("=").map((s) => s.trim());
|
|
1030
|
+
if (!k || !v) throw new ParseError(`invalid use argument '${part}'`, lineNo);
|
|
1031
|
+
args[k] = v;
|
|
1032
|
+
} else {
|
|
1033
|
+
args[`__pos_${Object.keys(args).length}`] = trimmed2;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
return { kind: "use", pattern: m[1], args, line: lineNo };
|
|
1038
|
+
}
|
|
1039
|
+
throw new ParseError(`unknown cue '${head}'; use show, hide, glow, focus, frame, or use`, lineNo);
|
|
1040
|
+
}
|
|
1041
|
+
function expandPatternCues(cues, patterns, line) {
|
|
1042
|
+
const out = [];
|
|
1043
|
+
for (const cue of cues) {
|
|
1044
|
+
if (cue.kind === "use") {
|
|
1045
|
+
const pat = patterns[cue.pattern];
|
|
1046
|
+
if (!pat) throw new ParseError(`unknown pattern '${cue.pattern}'`, line);
|
|
1047
|
+
const substituted = pat.body.map((c) => substitutePatternCue(c, pat.params, cue.args));
|
|
1048
|
+
out.push(...substituted);
|
|
1049
|
+
continue;
|
|
1050
|
+
}
|
|
1051
|
+
if (cue.kind === "parallel") {
|
|
1052
|
+
out.push({ ...cue, cues: expandPatternCues(cue.cues, patterns, line) });
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
out.push(cue);
|
|
1056
|
+
}
|
|
1057
|
+
return out;
|
|
1058
|
+
}
|
|
1059
|
+
function substitutePatternCue(cue, params, args) {
|
|
1060
|
+
const positional = Object.keys(args).filter((k) => k.startsWith("__pos_")).sort().map((k) => args[k]);
|
|
1061
|
+
const resolvedArgs = { ...args };
|
|
1062
|
+
params.forEach((p, i) => {
|
|
1063
|
+
if (resolvedArgs[p] === void 0 && positional[i]) resolvedArgs[p] = positional[i];
|
|
1064
|
+
});
|
|
1065
|
+
for (const key of Object.keys(resolvedArgs)) {
|
|
1066
|
+
if (key.startsWith("__pos_")) delete resolvedArgs[key];
|
|
1067
|
+
}
|
|
1068
|
+
const sub = (s) => {
|
|
1069
|
+
for (const p of params) {
|
|
1070
|
+
if (resolvedArgs[p]) s = s.replaceAll(`$${p}`, resolvedArgs[p]);
|
|
1071
|
+
}
|
|
1072
|
+
return s;
|
|
1073
|
+
};
|
|
1074
|
+
if (cue.kind === "flow") {
|
|
1075
|
+
return {
|
|
1076
|
+
...cue,
|
|
1077
|
+
segments: cue.segments.map((seg) => ({
|
|
1078
|
+
...seg,
|
|
1079
|
+
from: sub(seg.from),
|
|
1080
|
+
to: sub(seg.to),
|
|
1081
|
+
label: seg.label ? sub(seg.label) : void 0
|
|
1082
|
+
}))
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
if (cue.kind === "show" || cue.kind === "hide" || cue.kind === "glow" || cue.kind === "focus" || cue.kind === "frame") {
|
|
1086
|
+
return { ...cue, targets: cue.targets.map(sub) };
|
|
1087
|
+
}
|
|
1088
|
+
if (cue.kind === "parallel") {
|
|
1089
|
+
return { ...cue, cues: cue.cues.map((c) => substitutePatternCue(c, params, args)) };
|
|
1090
|
+
}
|
|
1091
|
+
return cue;
|
|
1092
|
+
}
|
|
1093
|
+
function readBlocks(lines) {
|
|
1094
|
+
const blocks = [];
|
|
1095
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1096
|
+
const raw = lines[i];
|
|
1097
|
+
if (!raw.trim() || raw.trim().startsWith("//")) continue;
|
|
1098
|
+
const indent = raw.match(/^\s*/)?.[0].length ?? 0;
|
|
1099
|
+
blocks.push({ line: i + 1, indent, text: raw.trim() });
|
|
1100
|
+
}
|
|
1101
|
+
return blocks;
|
|
1102
|
+
}
|
|
1103
|
+
function readIndentedBody(blocks, startIdx, parentIndent) {
|
|
1104
|
+
const body = [];
|
|
1105
|
+
let i = startIdx;
|
|
1106
|
+
while (i < blocks.length) {
|
|
1107
|
+
if (blocks[i].indent <= parentIndent) break;
|
|
1108
|
+
body.push(blocks[i]);
|
|
1109
|
+
i++;
|
|
1110
|
+
}
|
|
1111
|
+
return { body, nextIdx: i };
|
|
1112
|
+
}
|
|
1113
|
+
var PLAYER_KEYWORDS = [...PLAYER_FLAT_KEYS].sort((a, b) => b.length - a.length).join("|");
|
|
1114
|
+
var TOP_LEVEL_KEYWORDS_RE = new RegExp(
|
|
1115
|
+
`^(scene|player|layout|pattern|group|annotation|edge|beat|var|style|${PLAYER_KEYWORDS})\\b`
|
|
1116
|
+
);
|
|
1117
|
+
var PLAYER_DIRECTIVE_RE = new RegExp(`^(${PLAYER_KEYWORDS})\\b(?:\\s*[:=]?\\s*(.+))?$`);
|
|
1118
|
+
var PLAYER_GROUP_RE = /^(playback|controls|interaction|chrome)\s*:\s*$/;
|
|
1119
|
+
var PLAYER_SETTING_RE = /^(\w+)(?:\s*[:=]\s*|\s+)?(.*)$/;
|
|
1120
|
+
var PLAYER_FLAT_KEY_SET = new Set(PLAYER_FLAT_KEYS);
|
|
1121
|
+
function mirrorLegacySceneMeta(meta) {
|
|
1122
|
+
const player = meta.player;
|
|
1123
|
+
if (!player) return meta;
|
|
1124
|
+
const { playback, controls, interaction, chrome } = player;
|
|
1125
|
+
if (playback?.autoplay !== void 0) meta.autoplay = playback.autoplay;
|
|
1126
|
+
if (playback?.loop !== void 0) meta.loop = playback.loop;
|
|
1127
|
+
if (playback?.rate !== void 0) meta.playbackRate = playback.rate;
|
|
1128
|
+
if (controls) meta.controls = Object.values(controls).some(Boolean);
|
|
1129
|
+
if (interaction) {
|
|
1130
|
+
meta.interactiveViewport = Boolean(interaction.zoom ?? true) || Boolean(interaction.pan ?? true);
|
|
1131
|
+
}
|
|
1132
|
+
if (chrome?.badge !== void 0) meta.copyright = chrome.badge;
|
|
1133
|
+
if (chrome?.progress === "none") meta.progressColor = "none";
|
|
1134
|
+
else if (chrome?.progressColor !== void 0) meta.progressColor = chrome.progressColor;
|
|
1135
|
+
return meta;
|
|
1136
|
+
}
|
|
1137
|
+
function isTopLevelStatement(line) {
|
|
1138
|
+
if (line === "}") return true;
|
|
1139
|
+
if (TOP_LEVEL_KEYWORDS_RE.test(line)) return true;
|
|
1140
|
+
const nodeMatch = line.match(/^(\w[\w.-]*)\s+(\w[\w.-]*)/);
|
|
1141
|
+
if (!nodeMatch) return false;
|
|
1142
|
+
const kind = canonicalNodeKind(nodeMatch[1].toLowerCase());
|
|
1143
|
+
return NODE_KINDS.has(kind);
|
|
1144
|
+
}
|
|
1145
|
+
function readColonBody(blocks, startIdx, parentIndent, diagnostics, context, headerLine) {
|
|
1146
|
+
const indented = readIndentedBody(blocks, startIdx, parentIndent);
|
|
1147
|
+
if (indented.body.length > 0 || startIdx >= blocks.length) {
|
|
1148
|
+
return indented;
|
|
1149
|
+
}
|
|
1150
|
+
if (isTopLevelStatement(blocks[startIdx].text)) {
|
|
1151
|
+
return indented;
|
|
1152
|
+
}
|
|
1153
|
+
const body = [];
|
|
1154
|
+
let i = startIdx;
|
|
1155
|
+
while (i < blocks.length) {
|
|
1156
|
+
const block = blocks[i];
|
|
1157
|
+
if (block.indent < parentIndent) break;
|
|
1158
|
+
if (block.indent === parentIndent && isTopLevelStatement(block.text)) break;
|
|
1159
|
+
body.push(block);
|
|
1160
|
+
i++;
|
|
1161
|
+
}
|
|
1162
|
+
return { body, nextIdx: i };
|
|
1163
|
+
}
|
|
1164
|
+
function readBody(blocks, startIdx, parentIndent, braceDelimited, diagnostics = [], context = "block", headerLine = 1) {
|
|
1165
|
+
if (!braceDelimited) {
|
|
1166
|
+
return readColonBody(blocks, startIdx, parentIndent, diagnostics, context, headerLine);
|
|
1167
|
+
}
|
|
1168
|
+
const body = [];
|
|
1169
|
+
let i = startIdx;
|
|
1170
|
+
while (i < blocks.length) {
|
|
1171
|
+
if (blocks[i].text === "}") return { body, nextIdx: i + 1 };
|
|
1172
|
+
body.push(blocks[i]);
|
|
1173
|
+
i++;
|
|
1174
|
+
}
|
|
1175
|
+
return { body, nextIdx: i };
|
|
1176
|
+
}
|
|
1177
|
+
function normalizeCueBlocks(blocks) {
|
|
1178
|
+
const normalized = [];
|
|
1179
|
+
for (const block of blocks) {
|
|
1180
|
+
if (block.text.startsWith("& ")) {
|
|
1181
|
+
const prev = normalized[normalized.length - 1];
|
|
1182
|
+
if (!prev) throw new ParseError(`parallel continuation must follow a cue`, block.line);
|
|
1183
|
+
prev.text = `${prev.text} ${block.text}`;
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
normalized.push({ ...block });
|
|
1187
|
+
}
|
|
1188
|
+
return normalized;
|
|
1189
|
+
}
|
|
1190
|
+
function unsupportedSyntaxMessage(line) {
|
|
1191
|
+
if (/^actor\s+/.test(line) || /\bfigure\s*\(/.test(line) || /\bbox\s*\(/.test(line) || /\bat\s*\(/.test(line)) {
|
|
1192
|
+
return "unsupported manual drawing syntax; declare architecture nodes like service API, cache Redis, and database DB";
|
|
1193
|
+
}
|
|
1194
|
+
if (/^@\+?\d/.test(line)) {
|
|
1195
|
+
return "unsupported timeline command; put flow and cue lines inside beat blocks";
|
|
1196
|
+
}
|
|
1197
|
+
if (/^camera\b/.test(line)) {
|
|
1198
|
+
return "unsupported camera command; use frame NodeOrGroup zoom=... inside a beat";
|
|
1199
|
+
}
|
|
1200
|
+
return null;
|
|
1201
|
+
}
|
|
1202
|
+
var RESERVED_VAR_NAMES = /* @__PURE__ */ new Set(["nodes", "title", "edges"]);
|
|
1203
|
+
function extractVars(blocks, diagnostics) {
|
|
1204
|
+
const vars = /* @__PURE__ */ new Map();
|
|
1205
|
+
const rest = [];
|
|
1206
|
+
for (const block of blocks) {
|
|
1207
|
+
if (!/^var\b/.test(block.text)) {
|
|
1208
|
+
rest.push(block);
|
|
1209
|
+
continue;
|
|
1210
|
+
}
|
|
1211
|
+
const m = block.text.match(/^var\s+([A-Za-z_]\w*)\s*=\s*(.+)$/);
|
|
1212
|
+
if (!m) throw new ParseError("expected var name = value", block.line);
|
|
1213
|
+
const name = m[1];
|
|
1214
|
+
if (RESERVED_VAR_NAMES.has(name)) {
|
|
1215
|
+
diagnostics.push({ severity: "warning", message: `var '${name}' shadows a reserved selector and was ignored`, line: block.line });
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
const raw = m[2].trim();
|
|
1219
|
+
const str = parseStringToken(raw);
|
|
1220
|
+
vars.set(name, str && str.rest === "" ? str.value : raw);
|
|
1221
|
+
}
|
|
1222
|
+
return { vars, rest };
|
|
1223
|
+
}
|
|
1224
|
+
function applyVars(blocks, vars) {
|
|
1225
|
+
if (vars.size === 0) return blocks;
|
|
1226
|
+
return blocks.map((block) => {
|
|
1227
|
+
let text = block.text;
|
|
1228
|
+
for (const [name, value] of vars) {
|
|
1229
|
+
text = text.replace(new RegExp(`\\$${name}(?![\\w])`, "g"), value);
|
|
1230
|
+
}
|
|
1231
|
+
return { ...block, text };
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
function pushWarning(diagnostics, seen, line, message) {
|
|
1235
|
+
const key = `${line}:${message}`;
|
|
1236
|
+
if (seen.has(key)) return;
|
|
1237
|
+
seen.add(key);
|
|
1238
|
+
diagnostics.push({ severity: "warning", message, line });
|
|
1239
|
+
}
|
|
1240
|
+
function visitCues(cues, visit) {
|
|
1241
|
+
for (const cue of cues) {
|
|
1242
|
+
visit(cue);
|
|
1243
|
+
if (cue.kind === "parallel") visitCues(cue.cues, visit);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
function validateReferences(ast) {
|
|
1247
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1248
|
+
const hasNode = (id) => Boolean(ast.nodes[id]);
|
|
1249
|
+
const hasGroup = (id) => Boolean(ast.groups[id]);
|
|
1250
|
+
const isKnownTarget = (target) => {
|
|
1251
|
+
if (RESERVED_SELECTORS.has(target)) return true;
|
|
1252
|
+
if (target.startsWith("$")) return hasGroup(target.slice(1));
|
|
1253
|
+
return hasNode(target) || hasGroup(target);
|
|
1254
|
+
};
|
|
1255
|
+
for (const group of Object.values(ast.groups)) {
|
|
1256
|
+
for (const member of group.members) {
|
|
1257
|
+
if (!hasNode(member)) {
|
|
1258
|
+
pushWarning(ast.diagnostics, seen, group.line, `group '${group.id}' references unknown node '${member}'`);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
for (const node of Object.values(ast.nodes)) {
|
|
1263
|
+
if (node.style && !ast.styles[node.style]) {
|
|
1264
|
+
pushWarning(ast.diagnostics, seen, node.line, `node '${node.id}' references unknown style '${node.style}'`);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
const validateFlowEndpoint = (line, endpoint) => {
|
|
1268
|
+
if (!hasNode(endpoint)) {
|
|
1269
|
+
pushWarning(ast.diagnostics, seen, line, `flow references unknown node '${endpoint}'`);
|
|
1270
|
+
}
|
|
1271
|
+
};
|
|
1272
|
+
for (const edge of ast.edges) {
|
|
1273
|
+
validateFlowEndpoint(edge.line, edge.from);
|
|
1274
|
+
validateFlowEndpoint(edge.line, edge.to);
|
|
1275
|
+
}
|
|
1276
|
+
for (const ann of ast.annotations) {
|
|
1277
|
+
if (ann.target && !hasNode(ann.target)) {
|
|
1278
|
+
pushWarning(ast.diagnostics, seen, ann.line, `annotation references unknown target '${ann.target}'`);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
if (ast.annotations.length > 2) {
|
|
1282
|
+
pushWarning(ast.diagnostics, seen, ast.annotations[2].line, "more than 2 annotation callouts; editorial diagrams should use \u22642");
|
|
1283
|
+
}
|
|
1284
|
+
for (const beat of ast.beats) {
|
|
1285
|
+
visitCues(beat.cues, (cue) => {
|
|
1286
|
+
if (cue.kind === "flow") {
|
|
1287
|
+
for (const segment of cue.segments) {
|
|
1288
|
+
validateFlowEndpoint(cue.line, segment.from);
|
|
1289
|
+
validateFlowEndpoint(cue.line, segment.to);
|
|
1290
|
+
}
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
if (cue.kind === "show" || cue.kind === "hide" || cue.kind === "glow" || cue.kind === "focus" || cue.kind === "frame") {
|
|
1294
|
+
for (const target of cue.targets) {
|
|
1295
|
+
if (!isKnownTarget(target)) {
|
|
1296
|
+
pushWarning(ast.diagnostics, seen, cue.line, `${cue.kind} references unknown target '${target}'`);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
function detectFlowCycles(ast) {
|
|
1304
|
+
const dtype = ast.meta.type ?? "architecture";
|
|
1305
|
+
if (dtype !== "architecture" && dtype !== "flowchart") return;
|
|
1306
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1307
|
+
const adj = /* @__PURE__ */ new Map();
|
|
1308
|
+
const ensure = (id) => {
|
|
1309
|
+
if (!adj.has(id)) adj.set(id, []);
|
|
1310
|
+
};
|
|
1311
|
+
for (const id of Object.keys(ast.nodes)) ensure(id);
|
|
1312
|
+
const addEdge = (from, to, line) => {
|
|
1313
|
+
if (from === to) return;
|
|
1314
|
+
if (!ast.nodes[from] || !ast.nodes[to]) return;
|
|
1315
|
+
ensure(from);
|
|
1316
|
+
adj.get(from).push({ to, line });
|
|
1317
|
+
};
|
|
1318
|
+
for (const edge of ast.edges) {
|
|
1319
|
+
if (edge.kind !== "response") addEdge(edge.from, edge.to, edge.line);
|
|
1320
|
+
}
|
|
1321
|
+
for (const beat of ast.beats) {
|
|
1322
|
+
visitCues(beat.cues, (cue) => {
|
|
1323
|
+
if (cue.kind !== "flow") return;
|
|
1324
|
+
for (const segment of cue.segments) {
|
|
1325
|
+
if (segment.op !== "response") addEdge(segment.from, segment.to, cue.line);
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
const WHITE = 0;
|
|
1330
|
+
const GRAY = 1;
|
|
1331
|
+
const BLACK = 2;
|
|
1332
|
+
const color = /* @__PURE__ */ new Map();
|
|
1333
|
+
for (const id of adj.keys()) color.set(id, WHITE);
|
|
1334
|
+
const stack = [];
|
|
1335
|
+
const dfs = (node) => {
|
|
1336
|
+
color.set(node, GRAY);
|
|
1337
|
+
stack.push(node);
|
|
1338
|
+
for (const { to, line } of adj.get(node) ?? []) {
|
|
1339
|
+
if (color.get(to) === GRAY) {
|
|
1340
|
+
const idx = stack.indexOf(to);
|
|
1341
|
+
const cyclePath = [...stack.slice(idx), to].join(" -> ");
|
|
1342
|
+
pushWarning(
|
|
1343
|
+
ast.diagnostics,
|
|
1344
|
+
seen,
|
|
1345
|
+
line,
|
|
1346
|
+
`flow cycle detected: ${cyclePath} \u2014 if '${to}' is receiving a reply/return value here, use '<-' for this edge instead of '->'/'~>'/'--' (an unmarked cycle can crush the ranked layout and overlap nodes)`
|
|
1347
|
+
);
|
|
1348
|
+
} else if (color.get(to) === WHITE) {
|
|
1349
|
+
dfs(to);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
stack.pop();
|
|
1353
|
+
color.set(node, BLACK);
|
|
1354
|
+
};
|
|
1355
|
+
for (const id of adj.keys()) {
|
|
1356
|
+
if (color.get(id) === WHITE) dfs(id);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
function parse(source, opts = {}) {
|
|
1360
|
+
const lines = source.replace(/\r\n/g, "\n").split("\n");
|
|
1361
|
+
const diagnostics = [];
|
|
1362
|
+
const meta = {
|
|
1363
|
+
width: 1280,
|
|
1364
|
+
height: 720,
|
|
1365
|
+
fps: 60,
|
|
1366
|
+
theme: "paper",
|
|
1367
|
+
direction: "LR"
|
|
1368
|
+
};
|
|
1369
|
+
const styles = {};
|
|
1370
|
+
const nodes = {};
|
|
1371
|
+
const edges = [];
|
|
1372
|
+
const groups = {};
|
|
1373
|
+
const patterns = {};
|
|
1374
|
+
const beats = [];
|
|
1375
|
+
const annotations = [];
|
|
1376
|
+
let title = "";
|
|
1377
|
+
let edgeCounter = 0;
|
|
1378
|
+
let annotationCounter = 0;
|
|
1379
|
+
const rawBlocks = readBlocks(lines.map(stripComment));
|
|
1380
|
+
const { vars, rest } = extractVars(rawBlocks, diagnostics);
|
|
1381
|
+
const blocks = applyVars(rest, vars);
|
|
1382
|
+
let i = 0;
|
|
1383
|
+
while (i < blocks.length) {
|
|
1384
|
+
const block = blocks[i];
|
|
1385
|
+
const line = block.text;
|
|
1386
|
+
const lineNo = block.line;
|
|
1387
|
+
const unsupportedMessage = unsupportedSyntaxMessage(line);
|
|
1388
|
+
if (unsupportedMessage) throw new ParseError(unsupportedMessage, lineNo);
|
|
1389
|
+
if (/^player\s*:\s*$/.test(line)) {
|
|
1390
|
+
const { body, nextIdx } = readIndentedBody(blocks, i + 1, block.indent);
|
|
1391
|
+
if (body.length === 0) throw new ParseError("player block requires at least one setting", lineNo);
|
|
1392
|
+
const player = meta.player ??= {};
|
|
1393
|
+
const applySetting = (scope, setting) => {
|
|
1394
|
+
const match = setting.text.match(PLAYER_SETTING_RE);
|
|
1395
|
+
const error = applyPlayerSetting(player, scope, match?.[1] ?? "", match?.[2] ?? "");
|
|
1396
|
+
if (error) diagnostics.push({ severity: "warning", message: error, line: setting.line });
|
|
1397
|
+
};
|
|
1398
|
+
let settingIdx = 0;
|
|
1399
|
+
while (settingIdx < body.length) {
|
|
1400
|
+
const setting = body[settingIdx];
|
|
1401
|
+
const groupMatch = setting.text.match(PLAYER_GROUP_RE);
|
|
1402
|
+
if (groupMatch) {
|
|
1403
|
+
const scope = groupMatch[1];
|
|
1404
|
+
const { body: childBody, nextIdx: childNextIdx } = readIndentedBody(body, settingIdx + 1, setting.indent);
|
|
1405
|
+
if (childBody.length === 0) {
|
|
1406
|
+
throw new ParseError(`player ${scope} block requires at least one setting`, setting.line);
|
|
1407
|
+
}
|
|
1408
|
+
for (const child of childBody) applySetting(scope, child);
|
|
1409
|
+
settingIdx = childNextIdx;
|
|
1410
|
+
continue;
|
|
1411
|
+
}
|
|
1412
|
+
applySetting("player", setting);
|
|
1413
|
+
settingIdx++;
|
|
1414
|
+
}
|
|
1415
|
+
i = nextIdx;
|
|
1416
|
+
continue;
|
|
1417
|
+
}
|
|
1418
|
+
if (line.startsWith("scene")) {
|
|
1419
|
+
const rest2 = line.slice(5).trim();
|
|
1420
|
+
if (line.endsWith("{")) {
|
|
1421
|
+
throw new ParseError(`nested scene blocks are not supported; use one scene with multiple beat blocks`, lineNo);
|
|
1422
|
+
}
|
|
1423
|
+
let remainder = rest2;
|
|
1424
|
+
const str = parseStringToken(rest2);
|
|
1425
|
+
if (str) {
|
|
1426
|
+
title = str.value;
|
|
1427
|
+
remainder = str.rest;
|
|
1428
|
+
}
|
|
1429
|
+
const inlineLayout = remainder.match(/\blayout\s+(LR|RL|TB|BT)\b/i);
|
|
1430
|
+
if (inlineLayout) {
|
|
1431
|
+
meta.direction = inlineLayout[1].toUpperCase();
|
|
1432
|
+
remainder = remainder.replace(/\blayout\s+(LR|RL|TB|BT)\b/i, " ");
|
|
1433
|
+
}
|
|
1434
|
+
const props = parseProps(remainder);
|
|
1435
|
+
for (const [k, v] of Object.entries(props)) {
|
|
1436
|
+
if (!SCENE_KEYS.has(k)) {
|
|
1437
|
+
diagnostics.push({ severity: "warning", message: `unknown scene property '${k}'`, line: lineNo });
|
|
1438
|
+
continue;
|
|
1439
|
+
}
|
|
1440
|
+
if (k === "width") {
|
|
1441
|
+
meta.width = Number(v);
|
|
1442
|
+
meta.explicitWidth = true;
|
|
1443
|
+
} else if (k === "height") {
|
|
1444
|
+
meta.height = Number(v);
|
|
1445
|
+
meta.explicitHeight = true;
|
|
1446
|
+
} else if (k === "fps") {
|
|
1447
|
+
meta.fps = Number(v);
|
|
1448
|
+
} else if (k === "duration") meta.duration = Number(v);
|
|
1449
|
+
else if (k === "theme") meta.theme = String(v);
|
|
1450
|
+
else if (k === "direction" || k === "layout") meta.direction = String(v).toUpperCase();
|
|
1451
|
+
else if (PLAYER_FLAT_KEY_SET.has(k)) {
|
|
1452
|
+
const error = applyPlayerSetting(meta.player ??= {}, "player", k, String(v));
|
|
1453
|
+
if (error) diagnostics.push({ severity: "warning", message: error, line: lineNo });
|
|
1454
|
+
} else if (k === "type") {
|
|
1455
|
+
const t = String(v).toLowerCase();
|
|
1456
|
+
if (!DIAGRAM_TYPES.has(t)) {
|
|
1457
|
+
diagnostics.push({ severity: "warning", message: `unknown diagram type '${v}'`, line: lineNo });
|
|
1458
|
+
} else {
|
|
1459
|
+
meta.type = t;
|
|
1460
|
+
if (t === "flowchart" && !props.direction && !props.layout && !inlineLayout) {
|
|
1461
|
+
meta.direction = "TB";
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
i++;
|
|
1467
|
+
continue;
|
|
1468
|
+
}
|
|
1469
|
+
const directiveMatch = line.match(PLAYER_DIRECTIVE_RE);
|
|
1470
|
+
if (directiveMatch) {
|
|
1471
|
+
const error = applyPlayerSetting(meta.player ??= {}, "player", directiveMatch[1], directiveMatch[2] ?? "");
|
|
1472
|
+
if (error) diagnostics.push({ severity: "warning", message: error, line: lineNo });
|
|
1473
|
+
i++;
|
|
1474
|
+
continue;
|
|
1475
|
+
}
|
|
1476
|
+
if (/^layout\s+(LR|RL|TB|BT)\b/i.test(line)) {
|
|
1477
|
+
meta.direction = line.split(/\s+/)[1].toUpperCase();
|
|
1478
|
+
i++;
|
|
1479
|
+
continue;
|
|
1480
|
+
}
|
|
1481
|
+
if (line.startsWith("style ")) {
|
|
1482
|
+
const m = line.match(/^style\s+(\w+)\s*=\s*(.+)$/);
|
|
1483
|
+
if (!m) throw new ParseError(`expected style name = props`, lineNo);
|
|
1484
|
+
styles[m[1]] = { name: m[1], props: parseProps(m[2]), line: lineNo };
|
|
1485
|
+
i++;
|
|
1486
|
+
continue;
|
|
1487
|
+
}
|
|
1488
|
+
if (line.startsWith("pattern ")) {
|
|
1489
|
+
const m = line.match(/^pattern\s+(\w+)\s*\(([^)]*)\)\s*(?::|\{)\s*$/);
|
|
1490
|
+
if (!m) throw new ParseError(`expected pattern name(params):`, lineNo);
|
|
1491
|
+
const params = m[2].trim() ? m[2].split(",").map((p) => p.trim()) : [];
|
|
1492
|
+
const { body, nextIdx } = readBody(
|
|
1493
|
+
blocks,
|
|
1494
|
+
i + 1,
|
|
1495
|
+
block.indent,
|
|
1496
|
+
line.endsWith("{"),
|
|
1497
|
+
diagnostics,
|
|
1498
|
+
`pattern '${m[1]}'`,
|
|
1499
|
+
lineNo
|
|
1500
|
+
);
|
|
1501
|
+
const cues = normalizeCueBlocks(body).map((b) => parseCueLine(b.text, b.line));
|
|
1502
|
+
patterns[m[1]] = { name: m[1], params, body: cues, line: lineNo };
|
|
1503
|
+
i = nextIdx;
|
|
1504
|
+
continue;
|
|
1505
|
+
}
|
|
1506
|
+
if (line.startsWith("group ")) {
|
|
1507
|
+
const inline = line.match(/^group\s+(\w+)(?:\s+"([^"]*)")?\s*:\s*(.+)$/);
|
|
1508
|
+
if (inline) {
|
|
1509
|
+
groups[inline[1]] = {
|
|
1510
|
+
id: inline[1],
|
|
1511
|
+
label: inline[2],
|
|
1512
|
+
members: splitTargets(inline[3]),
|
|
1513
|
+
props: {},
|
|
1514
|
+
line: lineNo
|
|
1515
|
+
};
|
|
1516
|
+
i++;
|
|
1517
|
+
continue;
|
|
1518
|
+
}
|
|
1519
|
+
const header = line.match(/^group\s+(\w+)(?:\s+"([^"]*)")?\s*:\s*$/);
|
|
1520
|
+
if (!header) throw new ParseError(`expected group name: A B C`, lineNo);
|
|
1521
|
+
const { body, nextIdx } = readColonBody(
|
|
1522
|
+
blocks,
|
|
1523
|
+
i + 1,
|
|
1524
|
+
block.indent,
|
|
1525
|
+
diagnostics,
|
|
1526
|
+
`group '${header[1]}'`,
|
|
1527
|
+
lineNo
|
|
1528
|
+
);
|
|
1529
|
+
const members = body.flatMap((b) => splitTargets(b.text));
|
|
1530
|
+
if (members.length === 0) throw new ParseError(`group '${header[1]}' has no members`, lineNo);
|
|
1531
|
+
groups[header[1]] = {
|
|
1532
|
+
id: header[1],
|
|
1533
|
+
label: header[2],
|
|
1534
|
+
members,
|
|
1535
|
+
props: {},
|
|
1536
|
+
line: lineNo
|
|
1537
|
+
};
|
|
1538
|
+
i = nextIdx;
|
|
1539
|
+
continue;
|
|
1540
|
+
}
|
|
1541
|
+
if (line.startsWith("annotation ")) {
|
|
1542
|
+
const str = parseStringToken(line.slice("annotation ".length).trim());
|
|
1543
|
+
if (!str) throw new ParseError(`expected annotation "text" with optional target= position=`, lineNo);
|
|
1544
|
+
const props = parseProps(str.rest);
|
|
1545
|
+
const target = typeof props.target === "string" ? props.target : void 0;
|
|
1546
|
+
const position = typeof props.position === "string" ? props.position : void 0;
|
|
1547
|
+
annotations.push({
|
|
1548
|
+
id: `ann_${++annotationCounter}`,
|
|
1549
|
+
text: str.value,
|
|
1550
|
+
target,
|
|
1551
|
+
position,
|
|
1552
|
+
props,
|
|
1553
|
+
line: lineNo
|
|
1554
|
+
});
|
|
1555
|
+
i++;
|
|
1556
|
+
continue;
|
|
1557
|
+
}
|
|
1558
|
+
if (line.startsWith("edge ")) {
|
|
1559
|
+
const m = line.match(/^edge\s+(\w+)\s*:\s*(.+)$/);
|
|
1560
|
+
if (!m) throw new ParseError(`expected edge id: A -> B`, lineNo);
|
|
1561
|
+
const segments = parseFlowChain(m[2].split(/\s+\w+=/)[0], lineNo);
|
|
1562
|
+
for (const seg of segments) {
|
|
1563
|
+
edges.push({
|
|
1564
|
+
id: `edge_${++edgeCounter}`,
|
|
1565
|
+
kind: seg.op,
|
|
1566
|
+
from: seg.from,
|
|
1567
|
+
to: seg.to,
|
|
1568
|
+
label: seg.label,
|
|
1569
|
+
props: parseProps(m[2]),
|
|
1570
|
+
line: lineNo
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
i++;
|
|
1574
|
+
continue;
|
|
1575
|
+
}
|
|
1576
|
+
if (line.startsWith("beat ")) {
|
|
1577
|
+
const m = line.match(/^beat\s+([\w.-]+)(?:\s+"([^"]*)")?\s*(?::|\{)\s*$/);
|
|
1578
|
+
if (!m) throw new ParseError(`expected beat name:`, lineNo);
|
|
1579
|
+
const { body, nextIdx } = readBody(
|
|
1580
|
+
blocks,
|
|
1581
|
+
i + 1,
|
|
1582
|
+
block.indent,
|
|
1583
|
+
line.endsWith("{"),
|
|
1584
|
+
diagnostics,
|
|
1585
|
+
`beat '${m[1]}'`,
|
|
1586
|
+
lineNo
|
|
1587
|
+
);
|
|
1588
|
+
let cues = normalizeCueBlocks(body).map((b) => parseCueLine(b.text, b.line));
|
|
1589
|
+
cues = expandPatternCues(cues, patterns, lineNo);
|
|
1590
|
+
beats.push({ name: m[1], label: m[2], cues, line: lineNo });
|
|
1591
|
+
i = nextIdx;
|
|
1592
|
+
continue;
|
|
1593
|
+
}
|
|
1594
|
+
const nodeMatch = line.match(/^(\w[\w.-]*)\s+(\w[\w.-]*)(.*)$/);
|
|
1595
|
+
if (nodeMatch) {
|
|
1596
|
+
const rawKind = nodeMatch[1].toLowerCase();
|
|
1597
|
+
const kind = canonicalNodeKind(rawKind);
|
|
1598
|
+
const id = nodeMatch[2];
|
|
1599
|
+
let remainder = nodeMatch[3].trim();
|
|
1600
|
+
if (!NODE_KINDS.has(kind)) {
|
|
1601
|
+
throw new ParseError(`unknown node kind '${rawKind}'`, lineNo);
|
|
1602
|
+
}
|
|
1603
|
+
let label = humanizeId(id);
|
|
1604
|
+
const str = parseStringToken(remainder);
|
|
1605
|
+
if (str) {
|
|
1606
|
+
label = str.value;
|
|
1607
|
+
remainder = str.rest;
|
|
1608
|
+
}
|
|
1609
|
+
const styleMatch = remainder.match(/\bstyle=(\w+)/);
|
|
1610
|
+
const props = parseProps(remainder);
|
|
1611
|
+
nodes[id] = {
|
|
1612
|
+
kind,
|
|
1613
|
+
id,
|
|
1614
|
+
label,
|
|
1615
|
+
style: styleMatch?.[1],
|
|
1616
|
+
props,
|
|
1617
|
+
line: lineNo
|
|
1618
|
+
};
|
|
1619
|
+
i++;
|
|
1620
|
+
continue;
|
|
1621
|
+
}
|
|
1622
|
+
if (BEAT_CUE_KEYWORDS.has(line.split(/\s+/)[0].toLowerCase()) || FLOW_OP_RE.test(line)) {
|
|
1623
|
+
throw new ParseError(`top-level cues must be inside a beat block`, lineNo);
|
|
1624
|
+
}
|
|
1625
|
+
throw new ParseError(`unexpected statement`, lineNo);
|
|
1626
|
+
}
|
|
1627
|
+
const ast = {
|
|
1628
|
+
meta: { ...mirrorLegacySceneMeta(meta), title: title || void 0 },
|
|
1629
|
+
styles,
|
|
1630
|
+
nodes,
|
|
1631
|
+
edges,
|
|
1632
|
+
groups,
|
|
1633
|
+
annotations,
|
|
1634
|
+
patterns,
|
|
1635
|
+
beats,
|
|
1636
|
+
diagnostics
|
|
1637
|
+
};
|
|
1638
|
+
validateReferences(ast);
|
|
1639
|
+
detectFlowCycles(ast);
|
|
1640
|
+
const errors = ast.diagnostics.filter((d) => d.severity === "error");
|
|
1641
|
+
if (errors.length) {
|
|
1642
|
+
throw new ParseError(errors[0].message, errors[0].line, errors[0].column);
|
|
1643
|
+
}
|
|
1644
|
+
if (!opts.parseOnly && Object.keys(nodes).length === 0 && beats.length === 0) {
|
|
1645
|
+
}
|
|
1646
|
+
return ast;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
// ../core/src/arch-lint.ts
|
|
1650
|
+
function matchNode(node, selector) {
|
|
1651
|
+
if (!selector) return true;
|
|
1652
|
+
if (selector.id && node.id !== selector.id) return false;
|
|
1653
|
+
if (selector.kindEquals && node.kind.toLowerCase() !== selector.kindEquals.toLowerCase()) return false;
|
|
1654
|
+
if (selector.roleEquals) {
|
|
1655
|
+
const role = nodeRole(node.kind);
|
|
1656
|
+
if (role.toLowerCase() !== selector.roleEquals.toLowerCase()) return false;
|
|
1657
|
+
}
|
|
1658
|
+
if (selector.labelContains) {
|
|
1659
|
+
const target = (node.label || node.id).toLowerCase();
|
|
1660
|
+
if (!target.includes(selector.labelContains.toLowerCase())) return false;
|
|
1661
|
+
}
|
|
1662
|
+
return true;
|
|
1663
|
+
}
|
|
1664
|
+
function matchEdge(edge, selector) {
|
|
1665
|
+
if (!selector) return true;
|
|
1666
|
+
if (selector.kind && edge.kind !== selector.kind) return false;
|
|
1667
|
+
if (selector.labelContains) {
|
|
1668
|
+
const label = (edge.label || "").toLowerCase();
|
|
1669
|
+
if (!label.includes(selector.labelContains.toLowerCase())) return false;
|
|
1670
|
+
}
|
|
1671
|
+
return true;
|
|
1672
|
+
}
|
|
1673
|
+
function extractAllEdges(ast) {
|
|
1674
|
+
const edges = [];
|
|
1675
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1676
|
+
for (const edge of ast.edges) {
|
|
1677
|
+
const key = `${edge.from}->${edge.to}:${edge.kind}:${edge.label ?? ""}`;
|
|
1678
|
+
if (!seen.has(key)) {
|
|
1679
|
+
seen.add(key);
|
|
1680
|
+
edges.push({
|
|
1681
|
+
from: edge.from,
|
|
1682
|
+
to: edge.to,
|
|
1683
|
+
kind: edge.kind,
|
|
1684
|
+
label: edge.label,
|
|
1685
|
+
line: edge.line
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
for (const beat of ast.beats) {
|
|
1690
|
+
for (const cue of beat.cues) {
|
|
1691
|
+
if (cue.kind === "flow") {
|
|
1692
|
+
for (const seg of cue.segments) {
|
|
1693
|
+
const key = `${seg.from}->${seg.to}:${seg.op}:${seg.label ?? ""}`;
|
|
1694
|
+
if (!seen.has(key)) {
|
|
1695
|
+
seen.add(key);
|
|
1696
|
+
edges.push({
|
|
1697
|
+
from: seg.from,
|
|
1698
|
+
to: seg.to,
|
|
1699
|
+
kind: seg.op,
|
|
1700
|
+
label: seg.label,
|
|
1701
|
+
line: cue.line
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
return edges;
|
|
1709
|
+
}
|
|
1710
|
+
function detectCycleInGraph(nodes, edges, edgeFilter) {
|
|
1711
|
+
const candidateEdges = edges.filter((e) => matchEdge(e, edgeFilter));
|
|
1712
|
+
const adj = /* @__PURE__ */ new Map();
|
|
1713
|
+
for (const e of candidateEdges) {
|
|
1714
|
+
if (!adj.has(e.from)) adj.set(e.from, []);
|
|
1715
|
+
adj.get(e.from).push({ to: e.to, line: e.line });
|
|
1716
|
+
}
|
|
1717
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1718
|
+
const onStack = /* @__PURE__ */ new Set();
|
|
1719
|
+
const parentMap = /* @__PURE__ */ new Map();
|
|
1720
|
+
let foundCycle = null;
|
|
1721
|
+
const dfs = (curr) => {
|
|
1722
|
+
visited.add(curr);
|
|
1723
|
+
onStack.add(curr);
|
|
1724
|
+
for (const next of adj.get(curr) ?? []) {
|
|
1725
|
+
if (foundCycle) return;
|
|
1726
|
+
if (!visited.has(next.to)) {
|
|
1727
|
+
parentMap.set(next.to, curr);
|
|
1728
|
+
dfs(next.to);
|
|
1729
|
+
} else if (onStack.has(next.to)) {
|
|
1730
|
+
const cycle = [next.to, curr];
|
|
1731
|
+
let p = curr;
|
|
1732
|
+
while (p !== next.to && parentMap.has(p)) {
|
|
1733
|
+
p = parentMap.get(p);
|
|
1734
|
+
cycle.push(p);
|
|
1735
|
+
}
|
|
1736
|
+
foundCycle = {
|
|
1737
|
+
path: cycle.reverse(),
|
|
1738
|
+
line: next.line
|
|
1739
|
+
};
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
onStack.delete(curr);
|
|
1744
|
+
};
|
|
1745
|
+
for (const node of nodes) {
|
|
1746
|
+
if (!visited.has(node.id)) {
|
|
1747
|
+
dfs(node.id);
|
|
1748
|
+
if (foundCycle) return foundCycle;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
return null;
|
|
1752
|
+
}
|
|
1753
|
+
var ARCH_RULE_PRESETS = {
|
|
1754
|
+
cleanArchitecture: {
|
|
1755
|
+
id: "clean-architecture",
|
|
1756
|
+
name: "Clean / Layered Architecture",
|
|
1757
|
+
description: "Enforce strict dependency rules: Presentation cannot directly access Data Storage",
|
|
1758
|
+
rules: [
|
|
1759
|
+
{
|
|
1760
|
+
id: "no-presentation-to-database",
|
|
1761
|
+
name: "No Direct Client DB Access",
|
|
1762
|
+
description: "Presentation/Client components must communicate via backend services, never directly with databases.",
|
|
1763
|
+
severity: "error",
|
|
1764
|
+
type: "cannot-connect",
|
|
1765
|
+
from: { roleEquals: "client" },
|
|
1766
|
+
to: { roleEquals: "data" }
|
|
1767
|
+
},
|
|
1768
|
+
{
|
|
1769
|
+
id: "no-browser-to-internal-storage",
|
|
1770
|
+
name: "No Direct Browser Storage Access",
|
|
1771
|
+
description: "Browser nodes must not connect directly to private storage buckets.",
|
|
1772
|
+
severity: "error",
|
|
1773
|
+
type: "cannot-connect",
|
|
1774
|
+
from: { kindEquals: "browser" },
|
|
1775
|
+
to: { kindEquals: "storage" }
|
|
1776
|
+
}
|
|
1777
|
+
]
|
|
1778
|
+
},
|
|
1779
|
+
microservicesGovernance: {
|
|
1780
|
+
id: "microservices-governance",
|
|
1781
|
+
name: "Microservices Governance",
|
|
1782
|
+
description: "Prevent synchronous request cycles and shared database anti-patterns",
|
|
1783
|
+
rules: [
|
|
1784
|
+
{
|
|
1785
|
+
id: "no-sync-request-cycles",
|
|
1786
|
+
name: "Forbidden Request Cycle",
|
|
1787
|
+
description: "Synchronous request flows (->) must not form cyclic dependencies between services.",
|
|
1788
|
+
severity: "error",
|
|
1789
|
+
type: "forbidden-cycle",
|
|
1790
|
+
edge: { kind: "request" }
|
|
1791
|
+
},
|
|
1792
|
+
{
|
|
1793
|
+
id: "gateway-enforcement",
|
|
1794
|
+
name: "API Gateway Required",
|
|
1795
|
+
description: "Architecture diagrams with 3 or more services should declare an API gateway.",
|
|
1796
|
+
severity: "warning",
|
|
1797
|
+
type: "must-have-role",
|
|
1798
|
+
from: { roleEquals: "gateway" },
|
|
1799
|
+
min: 1
|
|
1800
|
+
}
|
|
1801
|
+
]
|
|
1802
|
+
},
|
|
1803
|
+
securityBoundaries: {
|
|
1804
|
+
id: "security-boundaries",
|
|
1805
|
+
name: "Zero-Trust Security Boundaries",
|
|
1806
|
+
description: "Ensure external clients pass through auth and edge gateway tiers",
|
|
1807
|
+
rules: [
|
|
1808
|
+
{
|
|
1809
|
+
id: "auth-service-presence",
|
|
1810
|
+
name: "Authentication Component Presence",
|
|
1811
|
+
description: "Public-facing architectures should explicitly define an Auth or Identity provider.",
|
|
1812
|
+
severity: "info",
|
|
1813
|
+
type: "must-have-role",
|
|
1814
|
+
from: { roleEquals: "security" }
|
|
1815
|
+
}
|
|
1816
|
+
]
|
|
1817
|
+
}
|
|
1818
|
+
};
|
|
1819
|
+
function validateArchitecture(ast, rules = [
|
|
1820
|
+
...ARCH_RULE_PRESETS.cleanArchitecture.rules,
|
|
1821
|
+
...ARCH_RULE_PRESETS.microservicesGovernance.rules
|
|
1822
|
+
]) {
|
|
1823
|
+
const violations = [];
|
|
1824
|
+
const nodes = Object.values(ast.nodes);
|
|
1825
|
+
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
|
|
1826
|
+
const edges = extractAllEdges(ast);
|
|
1827
|
+
for (const rule of rules) {
|
|
1828
|
+
switch (rule.type) {
|
|
1829
|
+
case "cannot-connect": {
|
|
1830
|
+
for (const edge of edges) {
|
|
1831
|
+
const sourceNode = nodeMap.get(edge.from);
|
|
1832
|
+
const targetNode = nodeMap.get(edge.to);
|
|
1833
|
+
if (!sourceNode || !targetNode) continue;
|
|
1834
|
+
if (matchNode(sourceNode, rule.from) && matchNode(targetNode, rule.to) && matchEdge(edge, rule.edge)) {
|
|
1835
|
+
violations.push({
|
|
1836
|
+
ruleId: rule.id,
|
|
1837
|
+
ruleName: rule.name,
|
|
1838
|
+
message: rule.description || `Node "${sourceNode.id}" is forbidden from connecting to "${targetNode.id}"`,
|
|
1839
|
+
severity: rule.severity,
|
|
1840
|
+
nodeIds: [sourceNode.id, targetNode.id],
|
|
1841
|
+
edgeKeys: [`${edge.from}->${edge.to}`],
|
|
1842
|
+
line: edge.line
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
break;
|
|
1847
|
+
}
|
|
1848
|
+
case "must-connect": {
|
|
1849
|
+
const sourceNodes = nodes.filter((n) => matchNode(n, rule.from));
|
|
1850
|
+
for (const src of sourceNodes) {
|
|
1851
|
+
const hasMatchingConnection = edges.some((edge) => {
|
|
1852
|
+
if (edge.from !== src.id) return false;
|
|
1853
|
+
const targetNode = nodeMap.get(edge.to);
|
|
1854
|
+
return targetNode ? matchNode(targetNode, rule.to) && matchEdge(edge, rule.edge) : false;
|
|
1855
|
+
});
|
|
1856
|
+
if (!hasMatchingConnection) {
|
|
1857
|
+
violations.push({
|
|
1858
|
+
ruleId: rule.id,
|
|
1859
|
+
ruleName: rule.name,
|
|
1860
|
+
message: rule.description || `Node "${src.id}" must connect to a matching downstream component`,
|
|
1861
|
+
severity: rule.severity,
|
|
1862
|
+
nodeIds: [src.id],
|
|
1863
|
+
edgeKeys: [],
|
|
1864
|
+
line: src.line
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
break;
|
|
1869
|
+
}
|
|
1870
|
+
case "forbidden-cycle": {
|
|
1871
|
+
const cycleInfo = detectCycleInGraph(nodes, edges, rule.edge);
|
|
1872
|
+
if (cycleInfo) {
|
|
1873
|
+
violations.push({
|
|
1874
|
+
ruleId: rule.id,
|
|
1875
|
+
ruleName: rule.name,
|
|
1876
|
+
message: `${rule.description} (Cycle path: ${cycleInfo.path.join(" -> ")})`,
|
|
1877
|
+
severity: rule.severity,
|
|
1878
|
+
nodeIds: Array.from(new Set(cycleInfo.path)),
|
|
1879
|
+
edgeKeys: [],
|
|
1880
|
+
line: cycleInfo.line
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
break;
|
|
1884
|
+
}
|
|
1885
|
+
case "must-have-role": {
|
|
1886
|
+
const matching = nodes.filter((n) => matchNode(n, rule.from));
|
|
1887
|
+
const min = rule.min ?? 1;
|
|
1888
|
+
if (matching.length < min) {
|
|
1889
|
+
violations.push({
|
|
1890
|
+
ruleId: rule.id,
|
|
1891
|
+
ruleName: rule.name,
|
|
1892
|
+
message: rule.description,
|
|
1893
|
+
severity: rule.severity,
|
|
1894
|
+
nodeIds: [],
|
|
1895
|
+
edgeKeys: [],
|
|
1896
|
+
line: 1
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1899
|
+
break;
|
|
1900
|
+
}
|
|
1901
|
+
case "role-count-limit": {
|
|
1902
|
+
const matching = nodes.filter((n) => matchNode(n, rule.from));
|
|
1903
|
+
if (rule.max !== void 0 && matching.length > rule.max) {
|
|
1904
|
+
violations.push({
|
|
1905
|
+
ruleId: rule.id,
|
|
1906
|
+
ruleName: rule.name,
|
|
1907
|
+
message: `${rule.description} (Found ${matching.length}, maximum allowed is ${rule.max})`,
|
|
1908
|
+
severity: rule.severity,
|
|
1909
|
+
nodeIds: matching.map((n) => n.id),
|
|
1910
|
+
edgeKeys: [],
|
|
1911
|
+
line: matching[0]?.line ?? 1
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
break;
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
return violations;
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// ../core/src/classifier.ts
|
|
1922
|
+
var TECH_CATALOG = [
|
|
1923
|
+
// ── DATABASES ───────────────────────────────────────────────────────
|
|
1924
|
+
{
|
|
1925
|
+
patterns: [/\b(postgres(ql)?|psql|cockroach(db)?|timescale)\b/i],
|
|
1926
|
+
kind: "database",
|
|
1927
|
+
role: "database",
|
|
1928
|
+
badge: "SQL"
|
|
1929
|
+
},
|
|
1930
|
+
{
|
|
1931
|
+
patterns: [/\b(mysql|mariadb|aurora|planetscale)\b/i],
|
|
1932
|
+
kind: "database",
|
|
1933
|
+
role: "database",
|
|
1934
|
+
badge: "MySQL"
|
|
1935
|
+
},
|
|
1936
|
+
{
|
|
1937
|
+
patterns: [/\b(mongodb|mongo|documentdb|couchdb)\b/i],
|
|
1938
|
+
kind: "database",
|
|
1939
|
+
role: "database",
|
|
1940
|
+
badge: "Document"
|
|
1941
|
+
},
|
|
1942
|
+
{
|
|
1943
|
+
patterns: [/\b(dynamodb|cassandra|scylla|hbase)\b/i],
|
|
1944
|
+
kind: "database",
|
|
1945
|
+
role: "database",
|
|
1946
|
+
badge: "NoSQL"
|
|
1947
|
+
},
|
|
1948
|
+
{
|
|
1949
|
+
patterns: [/\b(redis|memcached?|elasticache|dragonfly|valkey)\b/i],
|
|
1950
|
+
kind: "cache",
|
|
1951
|
+
role: "cache",
|
|
1952
|
+
badge: "Cache"
|
|
1953
|
+
},
|
|
1954
|
+
{
|
|
1955
|
+
patterns: [/\b(neo4j|memgraph|dgraph|graphdb)\b/i],
|
|
1956
|
+
kind: "database",
|
|
1957
|
+
role: "database",
|
|
1958
|
+
badge: "Graph"
|
|
1959
|
+
},
|
|
1960
|
+
{
|
|
1961
|
+
patterns: [/\b(clickhouse|snowflake|bigquery|redshift|duckdb)\b/i],
|
|
1962
|
+
kind: "database",
|
|
1963
|
+
role: "database",
|
|
1964
|
+
badge: "Analytics"
|
|
1965
|
+
},
|
|
1966
|
+
// ── MESSAGING & EVENT STREAMING ────────────────────────────────────
|
|
1967
|
+
{
|
|
1968
|
+
patterns: [/\b(kafka|confluent|redpanda)\b/i],
|
|
1969
|
+
kind: "queue",
|
|
1970
|
+
role: "event_stream",
|
|
1971
|
+
badge: "EventStream"
|
|
1972
|
+
},
|
|
1973
|
+
{
|
|
1974
|
+
patterns: [/\b(rabbitmq|sqs|activemq|pulsar|nats|eventgrid|servicebus)\b/i],
|
|
1975
|
+
kind: "queue",
|
|
1976
|
+
role: "queue",
|
|
1977
|
+
badge: "Queue"
|
|
1978
|
+
},
|
|
1979
|
+
// ── GATEWAYS & INGRESS ──────────────────────────────────────────────
|
|
1980
|
+
{
|
|
1981
|
+
patterns: [/\b(kong|envoy|nginx|traefik|caddy|haproxy|emissary)\b/i],
|
|
1982
|
+
kind: "api_gateway",
|
|
1983
|
+
role: "gateway",
|
|
1984
|
+
badge: "Gateway"
|
|
1985
|
+
},
|
|
1986
|
+
{
|
|
1987
|
+
patterns: [/\b(cloudfront|cloudflare|fastly|akamai|cdn)\b/i],
|
|
1988
|
+
kind: "cdn",
|
|
1989
|
+
role: "network",
|
|
1990
|
+
badge: "CDN"
|
|
1991
|
+
},
|
|
1992
|
+
{
|
|
1993
|
+
patterns: [/\b(alb|elb|nlb|load_?balancer|ingress)\b/i],
|
|
1994
|
+
kind: "load_balancer",
|
|
1995
|
+
role: "network",
|
|
1996
|
+
badge: "LB"
|
|
1997
|
+
},
|
|
1998
|
+
// ── CLIENT & PRESENTATION ──────────────────────────────────────────
|
|
1999
|
+
{
|
|
2000
|
+
patterns: [/\b(react|vue|angular|svelte|next(\.?js)?|nuxt|solid)\b/i],
|
|
2001
|
+
kind: "browser",
|
|
2002
|
+
role: "client",
|
|
2003
|
+
badge: "Web"
|
|
2004
|
+
},
|
|
2005
|
+
{
|
|
2006
|
+
patterns: [/\b(flutter|react_?native|ios|android|swift|kotlin|mobile)\b/i],
|
|
2007
|
+
kind: "browser",
|
|
2008
|
+
role: "client",
|
|
2009
|
+
badge: "Mobile"
|
|
2010
|
+
},
|
|
2011
|
+
// ── AI & LLM ────────────────────────────────────────────────────────
|
|
2012
|
+
{
|
|
2013
|
+
patterns: [/\b(gemini|gpt(-?[a-z0-9.]+)?|claude|openai|anthropic|bedrock|vertexai|mistral|ollama|deepseek)\b/i],
|
|
2014
|
+
kind: "service",
|
|
2015
|
+
role: "ai_model",
|
|
2016
|
+
badge: "AI/LLM"
|
|
2017
|
+
},
|
|
2018
|
+
{
|
|
2019
|
+
patterns: [/\b(pinecone|weaviate|qdrant|chroma|milvus)\b/i],
|
|
2020
|
+
kind: "database",
|
|
2021
|
+
role: "database",
|
|
2022
|
+
badge: "VectorDB"
|
|
2023
|
+
},
|
|
2024
|
+
// ── CLOUD & CONTAINER RUNTIMES ─────────────────────────────────────
|
|
2025
|
+
{
|
|
2026
|
+
patterns: [/\b(k8s|kubernetes|eks|gke|aks|helm|argocd)\b/i],
|
|
2027
|
+
kind: "cluster",
|
|
2028
|
+
role: "platform",
|
|
2029
|
+
badge: "K8s"
|
|
2030
|
+
},
|
|
2031
|
+
{
|
|
2032
|
+
patterns: [/\b(lambda|cloud_?functions?|azure_?functions?|serverless)\b/i],
|
|
2033
|
+
kind: "worker",
|
|
2034
|
+
role: "compute",
|
|
2035
|
+
badge: "Function"
|
|
2036
|
+
},
|
|
2037
|
+
{
|
|
2038
|
+
patterns: [/\b(docker|ecs|container|fargate|cloud_?run)\b/i],
|
|
2039
|
+
kind: "service",
|
|
2040
|
+
role: "compute",
|
|
2041
|
+
badge: "Container"
|
|
2042
|
+
},
|
|
2043
|
+
// ── STORAGE & BLOB ──────────────────────────────────────────────────
|
|
2044
|
+
{
|
|
2045
|
+
patterns: [/\b(s3|gcs|azure_?blob|minio|r2|ceph)\b/i],
|
|
2046
|
+
kind: "storage",
|
|
2047
|
+
role: "storage",
|
|
2048
|
+
badge: "ObjectStorage"
|
|
2049
|
+
},
|
|
2050
|
+
// ── SECURITY & AUTH ─────────────────────────────────────────────────
|
|
2051
|
+
{
|
|
2052
|
+
patterns: [/\b(auth0|clerk|keycloak|cognito|okta|vault|jwt|oauth|oidc)\b/i],
|
|
2053
|
+
kind: "service",
|
|
2054
|
+
role: "security",
|
|
2055
|
+
badge: "Security"
|
|
2056
|
+
}
|
|
2057
|
+
];
|
|
2058
|
+
function classifyTechnology(id, label = "") {
|
|
2059
|
+
const combined = `${id} ${label}`.trim();
|
|
2060
|
+
for (const entry of TECH_CATALOG) {
|
|
2061
|
+
if (entry.patterns.some((p) => p.test(combined))) {
|
|
2062
|
+
return {
|
|
2063
|
+
kind: entry.kind,
|
|
2064
|
+
role: entry.role,
|
|
2065
|
+
suggestedTheme: "paper",
|
|
2066
|
+
badge: entry.badge
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
return {
|
|
2071
|
+
kind: "service",
|
|
2072
|
+
role: "compute",
|
|
2073
|
+
suggestedTheme: "paper"
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
// ../core/src/ai-healing.ts
|
|
2078
|
+
function analyzeAndBuildRepairPrompt(sourceCode) {
|
|
2079
|
+
const syntaxErrors = [];
|
|
2080
|
+
let ast = null;
|
|
2081
|
+
try {
|
|
2082
|
+
ast = parse(sourceCode);
|
|
2083
|
+
} catch (err) {
|
|
2084
|
+
syntaxErrors.push(err instanceof Error ? err.message : String(err));
|
|
2085
|
+
}
|
|
2086
|
+
if (!ast) {
|
|
2087
|
+
return {
|
|
2088
|
+
isValid: false,
|
|
2089
|
+
syntaxErrors,
|
|
2090
|
+
archViolations: [],
|
|
2091
|
+
repairPrompt: [
|
|
2092
|
+
"The following MarkdyScript failed to parse with syntax errors:",
|
|
2093
|
+
...syntaxErrors.map((e) => ` - ${e}`),
|
|
2094
|
+
"",
|
|
2095
|
+
"Please fix the code below to follow valid MarkdyScript syntax:",
|
|
2096
|
+
"```markdy",
|
|
2097
|
+
sourceCode,
|
|
2098
|
+
"```"
|
|
2099
|
+
].join("\n")
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
const archViolations = validateArchitecture(ast);
|
|
2103
|
+
if (ast.diagnostics.length === 0 && archViolations.length === 0) {
|
|
2104
|
+
return { isValid: true, syntaxErrors: [], archViolations: [] };
|
|
2105
|
+
}
|
|
2106
|
+
const promptSections = [
|
|
2107
|
+
"The MarkdyScript diagram has the following compiler diagnostics and architectural rule violations:",
|
|
2108
|
+
"",
|
|
2109
|
+
"### Diagnostics:",
|
|
2110
|
+
...ast.diagnostics.map((d) => ` - Line ${d.line}: ${d.message}`),
|
|
2111
|
+
"",
|
|
2112
|
+
"### Architectural Violations:",
|
|
2113
|
+
...archViolations.map((v) => ` - [${v.severity.toUpperCase()}] ${v.ruleName}: ${v.message}`),
|
|
2114
|
+
"",
|
|
2115
|
+
"Please revise the diagram code to resolve all issues while preserving semantic nodes and beats:",
|
|
2116
|
+
"```markdy",
|
|
2117
|
+
sourceCode,
|
|
2118
|
+
"```"
|
|
2119
|
+
];
|
|
2120
|
+
return {
|
|
2121
|
+
isValid: false,
|
|
2122
|
+
syntaxErrors: [],
|
|
2123
|
+
archViolations,
|
|
2124
|
+
repairPrompt: promptSections.join("\n")
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
// ../compat/src/mermaid/mermaid-transpiler.ts
|
|
2129
|
+
function sanitizeId(id) {
|
|
2130
|
+
return id.trim().replace(/[^a-zA-Z0-9_]/g, "_");
|
|
2131
|
+
}
|
|
2132
|
+
function cleanLabel(raw) {
|
|
2133
|
+
return raw.trim().replace(/^["'\[\(\{]+/, "").replace(/["'\]\)\}]+$/, "").replace(/<br\s*\/?>/gi, " ");
|
|
2134
|
+
}
|
|
2135
|
+
function inferKindFromMermaid(id, label, shapeBracket) {
|
|
2136
|
+
const text = `${id} ${label}`.toLowerCase();
|
|
2137
|
+
if (shapeBracket === "[(" || shapeBracket === ")]" || /(db|database|sql|postgres|mongo|dynamo|redis)/.test(text)) {
|
|
2138
|
+
return "database";
|
|
2139
|
+
}
|
|
2140
|
+
if (shapeBracket === "{{" || shapeBracket === "}}" || /(queue|kafka|rabbitmq|sqs|event|bus)/.test(text)) {
|
|
2141
|
+
return "queue";
|
|
2142
|
+
}
|
|
2143
|
+
if (shapeBracket === "{" || shapeBracket === "}" || /(decision|check|valid|auth|gate)/.test(text)) {
|
|
2144
|
+
return "gateway";
|
|
2145
|
+
}
|
|
2146
|
+
if (shapeBracket === "([" || shapeBracket === "])" || /(client|user|browser|ui|app|web|frontend)/.test(text)) {
|
|
2147
|
+
return "browser";
|
|
2148
|
+
}
|
|
2149
|
+
if (/(cache|memcached|varnish)/.test(text)) return "cache";
|
|
2150
|
+
if (/(storage|s3|bucket|blob)/.test(text)) return "storage";
|
|
2151
|
+
if (/(worker|cron|job|lambda|function)/.test(text)) return "worker";
|
|
2152
|
+
return "service";
|
|
2153
|
+
}
|
|
2154
|
+
function transpileMermaidToMarkdy(mermaidSource, sceneTitle = "Imported Diagram") {
|
|
2155
|
+
const lines = mermaidSource.split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !l.startsWith("%%"));
|
|
2156
|
+
if (lines.length === 0) {
|
|
2157
|
+
return {
|
|
2158
|
+
code: `scene theme=paper
|
|
2159
|
+
layout LR
|
|
2160
|
+
`,
|
|
2161
|
+
diagramType: "architecture",
|
|
2162
|
+
nodeCount: 0,
|
|
2163
|
+
edgeCount: 0
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
const firstLine = lines[0].toLowerCase();
|
|
2167
|
+
if (firstLine.startsWith("sequencediagram")) {
|
|
2168
|
+
return transpileSequenceDiagram(lines.slice(1), sceneTitle);
|
|
2169
|
+
}
|
|
2170
|
+
return transpileFlowchart(lines, sceneTitle);
|
|
2171
|
+
}
|
|
2172
|
+
function transpileSequenceDiagram(lines, title) {
|
|
2173
|
+
const participants = /* @__PURE__ */ new Map();
|
|
2174
|
+
const messages = [];
|
|
2175
|
+
for (const line of lines) {
|
|
2176
|
+
const partMatch = /^(?:participant|actor)\s+([^\s]+)(?:\s+as\s+(.+))?$/i.exec(line);
|
|
2177
|
+
if (partMatch) {
|
|
2178
|
+
const id = sanitizeId(partMatch[1]);
|
|
2179
|
+
const label = partMatch[2] ? cleanLabel(partMatch[2]) : id;
|
|
2180
|
+
const kind = inferKindFromMermaid(id, label);
|
|
2181
|
+
participants.set(id, { id, label, kind });
|
|
2182
|
+
continue;
|
|
2183
|
+
}
|
|
2184
|
+
const msgMatch = /^([a-zA-Z0-9_]+)\s*(-->>|->>|->|-->|-\)|~>)\s*([a-zA-Z0-9_]+)\s*:\s*(.+)$/.exec(line);
|
|
2185
|
+
if (msgMatch) {
|
|
2186
|
+
const from = sanitizeId(msgMatch[1]);
|
|
2187
|
+
const arrow = msgMatch[2];
|
|
2188
|
+
const to = sanitizeId(msgMatch[3]);
|
|
2189
|
+
const label = cleanLabel(msgMatch[4]);
|
|
2190
|
+
if (!participants.has(from)) participants.set(from, { id: from, label: from, kind: inferKindFromMermaid(from, from) });
|
|
2191
|
+
if (!participants.has(to)) participants.set(to, { id: to, label: to, kind: inferKindFromMermaid(to, to) });
|
|
2192
|
+
let kind = "->";
|
|
2193
|
+
if (arrow === "-->>" || arrow === "-->" || arrow === "-.->") kind = "~>";
|
|
2194
|
+
messages.push({ from, to, label, kind });
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
const out = [];
|
|
2198
|
+
out.push(title ? `scene "${title}" type=sequence theme=paper` : `scene type=sequence theme=paper`);
|
|
2199
|
+
out.push("");
|
|
2200
|
+
for (const p of participants.values()) {
|
|
2201
|
+
out.push(`${p.kind} ${p.id} "${p.label}"`);
|
|
2202
|
+
}
|
|
2203
|
+
out.push("");
|
|
2204
|
+
out.push('beat main "Sequence Flow":');
|
|
2205
|
+
out.push(" show $nodes");
|
|
2206
|
+
for (const msg of messages) {
|
|
2207
|
+
out.push(` ${msg.from} ${msg.kind} ${msg.to} "${msg.label}"`);
|
|
2208
|
+
}
|
|
2209
|
+
return {
|
|
2210
|
+
code: out.join("\n"),
|
|
2211
|
+
diagramType: "sequence",
|
|
2212
|
+
nodeCount: participants.size,
|
|
2213
|
+
edgeCount: messages.length
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
function transpileFlowchart(lines, title) {
|
|
2217
|
+
let direction = "LR";
|
|
2218
|
+
const first = lines[0].toLowerCase();
|
|
2219
|
+
if (first.startsWith("graph") || first.startsWith("flowchart")) {
|
|
2220
|
+
const dirMatch = /\b(lr|rl|tb|td|bt)\b/i.exec(first);
|
|
2221
|
+
if (dirMatch) {
|
|
2222
|
+
direction = dirMatch[1].toUpperCase();
|
|
2223
|
+
if (direction === "TD") direction = "TB";
|
|
2224
|
+
}
|
|
2225
|
+
lines = lines.slice(1);
|
|
2226
|
+
}
|
|
2227
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
2228
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2229
|
+
const flows = [];
|
|
2230
|
+
let currentSubgraph = null;
|
|
2231
|
+
const explicitNodeRe = /([a-zA-Z0-9_-]+)\s*(\[\([^\n]*?\)\]|\[\[[^\n]*?\]\]|\(\[[^\n]*?\]\)|\(\([^\n]*?\)\)|\[[^\n]*?\]|\{[^\n]*?\}|\([^\n]*?\))/g;
|
|
2232
|
+
const flowPattern = /([a-zA-Z0-9_-]+)\s*(?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\})?\s*(-->|->|==>|-.->|--\s*([^-]+)\s*-->)\s*(?:\|([^|]+)\|)?\s*([a-zA-Z0-9_-]+)/;
|
|
2233
|
+
for (const line of lines) {
|
|
2234
|
+
const subMatch = /^subgraph\s+([a-zA-Z0-9_]+)?\s*(\[.*\]|".*")?/i.exec(line);
|
|
2235
|
+
if (subMatch) {
|
|
2236
|
+
const rawId = subMatch[1] || `group_${groups.size + 1}`;
|
|
2237
|
+
const id = sanitizeId(rawId);
|
|
2238
|
+
const label = subMatch[2] ? cleanLabel(subMatch[2]) : id;
|
|
2239
|
+
currentSubgraph = { id, label, members: [] };
|
|
2240
|
+
groups.set(id, currentSubgraph);
|
|
2241
|
+
continue;
|
|
2242
|
+
}
|
|
2243
|
+
if (line === "end" && currentSubgraph) {
|
|
2244
|
+
currentSubgraph = null;
|
|
2245
|
+
continue;
|
|
2246
|
+
}
|
|
2247
|
+
let match;
|
|
2248
|
+
while ((match = explicitNodeRe.exec(line)) !== null) {
|
|
2249
|
+
const rawId = match[1];
|
|
2250
|
+
const rawBody = match[2];
|
|
2251
|
+
const id = sanitizeId(rawId);
|
|
2252
|
+
const bracket = rawBody.slice(0, 2);
|
|
2253
|
+
const label = cleanLabel(rawBody) || id;
|
|
2254
|
+
const kind = inferKindFromMermaid(id, label, bracket);
|
|
2255
|
+
nodes.set(id, { id, label, kind });
|
|
2256
|
+
if (currentSubgraph && !currentSubgraph.members.includes(id)) {
|
|
2257
|
+
currentSubgraph.members.push(id);
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
let remainingLine = line;
|
|
2261
|
+
while (true) {
|
|
2262
|
+
const fMatch = flowPattern.exec(remainingLine);
|
|
2263
|
+
if (!fMatch) break;
|
|
2264
|
+
const from = sanitizeId(fMatch[1]);
|
|
2265
|
+
const arrow = fMatch[2];
|
|
2266
|
+
const inlineLabel = fMatch[3];
|
|
2267
|
+
const pipeLabel = fMatch[4];
|
|
2268
|
+
const to = sanitizeId(fMatch[5]);
|
|
2269
|
+
const label = pipeLabel ? cleanLabel(pipeLabel) : inlineLabel ? cleanLabel(inlineLabel) : void 0;
|
|
2270
|
+
if (!nodes.has(from)) nodes.set(from, { id: from, label: from, kind: inferKindFromMermaid(from, from) });
|
|
2271
|
+
if (!nodes.has(to)) nodes.set(to, { id: to, label: to, kind: inferKindFromMermaid(to, to) });
|
|
2272
|
+
let op = "->";
|
|
2273
|
+
if (arrow.includes("-.->") || arrow.includes("~")) op = "~>";
|
|
2274
|
+
flows.push({ from, to, op, label });
|
|
2275
|
+
const toIndex = fMatch.index + fMatch[0].lastIndexOf(fMatch[5]);
|
|
2276
|
+
if (toIndex === 0) break;
|
|
2277
|
+
remainingLine = remainingLine.substring(toIndex);
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
const out = [];
|
|
2281
|
+
out.push(title ? `scene "${title}" theme=paper` : `scene theme=paper`);
|
|
2282
|
+
out.push(`layout ${direction}`);
|
|
2283
|
+
out.push("");
|
|
2284
|
+
for (const g of groups.values()) {
|
|
2285
|
+
if (g.members.length > 0) {
|
|
2286
|
+
out.push(`group ${g.id} "${g.label}": ${g.members.join(" ")}`);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
for (const n of nodes.values()) {
|
|
2290
|
+
out.push(`${n.kind} ${n.id} "${n.label}"`);
|
|
2291
|
+
}
|
|
2292
|
+
out.push("");
|
|
2293
|
+
out.push('beat main "Render Diagram":');
|
|
2294
|
+
out.push(" show $nodes stagger=60ms");
|
|
2295
|
+
for (const flow of flows) {
|
|
2296
|
+
if (flow.label) {
|
|
2297
|
+
out.push(` ${flow.from} ${flow.op} ${flow.to} "${flow.label}"`);
|
|
2298
|
+
} else {
|
|
2299
|
+
out.push(` ${flow.from} ${flow.op} ${flow.to}`);
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
return {
|
|
2303
|
+
code: out.join("\n"),
|
|
2304
|
+
diagramType: "architecture",
|
|
2305
|
+
nodeCount: nodes.size,
|
|
2306
|
+
edgeCount: flows.length
|
|
2307
|
+
};
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
// ../compat/src/infra/docker-compose-transpiler.ts
|
|
2311
|
+
function sanitizeIdentifier(name) {
|
|
2312
|
+
return name.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
2313
|
+
}
|
|
2314
|
+
function inferSemanticKind(serviceName, image) {
|
|
2315
|
+
const combined = `${serviceName} ${image ?? ""}`.toLowerCase();
|
|
2316
|
+
if (/(postgres|mysql|mongo|mariadb|sqlite|cockroach|db)/.test(combined)) return "database";
|
|
2317
|
+
if (/(redis|memcache)/.test(combined)) return "cache";
|
|
2318
|
+
if (/(kafka|rabbitmq|sqs|pulsar|nats|queue)/.test(combined)) return "queue";
|
|
2319
|
+
if (/(nginx|envoy|traefik|caddy|gateway|haproxy)/.test(combined)) return "gateway";
|
|
2320
|
+
if (/(web|frontend|client|ui|react|vue|next)/.test(combined)) return "browser";
|
|
2321
|
+
if (/(worker|job|cron|consumer)/.test(combined)) return "worker";
|
|
2322
|
+
if (/(minio|s3|storage|blob)/.test(combined)) return "storage";
|
|
2323
|
+
return "service";
|
|
2324
|
+
}
|
|
2325
|
+
function parseSimpleYaml(content) {
|
|
2326
|
+
const result = {};
|
|
2327
|
+
const lines = content.split(/\r?\n/);
|
|
2328
|
+
const stack = [
|
|
2329
|
+
{ indent: -1, obj: result }
|
|
2330
|
+
];
|
|
2331
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2332
|
+
const rawLine = lines[i];
|
|
2333
|
+
if (!rawLine.trim() || rawLine.trim().startsWith("#")) continue;
|
|
2334
|
+
const indent = rawLine.length - rawLine.trimStart().length;
|
|
2335
|
+
const trimmed = rawLine.trim();
|
|
2336
|
+
while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
|
|
2337
|
+
stack.pop();
|
|
2338
|
+
}
|
|
2339
|
+
if (trimmed.startsWith("- ")) {
|
|
2340
|
+
const val = trimmed.slice(2).trim().replace(/^['"]|['"]$/g, "");
|
|
2341
|
+
const parent2 = stack[stack.length - 1].obj;
|
|
2342
|
+
if (Array.isArray(parent2)) {
|
|
2343
|
+
parent2.push(val);
|
|
2344
|
+
}
|
|
2345
|
+
continue;
|
|
2346
|
+
}
|
|
2347
|
+
const colonIdx = trimmed.indexOf(":");
|
|
2348
|
+
if (colonIdx === -1) continue;
|
|
2349
|
+
const key = trimmed.slice(0, colonIdx).trim().replace(/^['"]|['"]$/g, "");
|
|
2350
|
+
const valRaw = trimmed.slice(colonIdx + 1).trim();
|
|
2351
|
+
const parent = stack[stack.length - 1].obj;
|
|
2352
|
+
if (valRaw === "" || valRaw === "|" || valRaw === ">") {
|
|
2353
|
+
let isArray = false;
|
|
2354
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
2355
|
+
const nextLine = lines[j];
|
|
2356
|
+
if (!nextLine.trim() || nextLine.trim().startsWith("#")) continue;
|
|
2357
|
+
const nextIndent = nextLine.length - nextLine.trimStart().length;
|
|
2358
|
+
if (nextIndent > indent && nextLine.trim().startsWith("- ")) {
|
|
2359
|
+
isArray = true;
|
|
2360
|
+
}
|
|
2361
|
+
break;
|
|
2362
|
+
}
|
|
2363
|
+
if (isArray) {
|
|
2364
|
+
const child = [];
|
|
2365
|
+
parent[key] = child;
|
|
2366
|
+
stack.push({ indent, obj: child });
|
|
2367
|
+
} else {
|
|
2368
|
+
const child = {};
|
|
2369
|
+
parent[key] = child;
|
|
2370
|
+
stack.push({ indent, obj: child });
|
|
2371
|
+
}
|
|
2372
|
+
} else if (valRaw.startsWith("[") && valRaw.endsWith("]")) {
|
|
2373
|
+
const inner = valRaw.slice(1, -1).trim();
|
|
2374
|
+
const items = inner ? inner.split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean) : [];
|
|
2375
|
+
parent[key] = items;
|
|
2376
|
+
} else {
|
|
2377
|
+
parent[key] = valRaw.replace(/^['"]|['"]$/g, "");
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
return result;
|
|
2381
|
+
}
|
|
2382
|
+
function transpileDockerComposeToMarkdy(yamlContent, title = "Container Topology") {
|
|
2383
|
+
const parsed = parseSimpleYaml(yamlContent);
|
|
2384
|
+
const rawServices = parsed["services"] ?? {};
|
|
2385
|
+
const serviceList = [];
|
|
2386
|
+
for (const [svcName, svcConfig] of Object.entries(rawServices)) {
|
|
2387
|
+
if (typeof svcConfig !== "object" || svcConfig === null) continue;
|
|
2388
|
+
const cfg = svcConfig;
|
|
2389
|
+
let ports = [];
|
|
2390
|
+
if (Array.isArray(cfg["ports"])) {
|
|
2391
|
+
ports = cfg["ports"].map((p) => String(p).replace(/^['"]|['"]$/g, ""));
|
|
2392
|
+
} else if (typeof cfg["ports"] === "string") {
|
|
2393
|
+
const pStr = cfg["ports"].trim();
|
|
2394
|
+
if (pStr.startsWith("[") && pStr.endsWith("]")) {
|
|
2395
|
+
ports = pStr.slice(1, -1).split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean);
|
|
2396
|
+
} else {
|
|
2397
|
+
ports = [pStr.replace(/^['"]|['"]$/g, "")];
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
let dependsOn = [];
|
|
2401
|
+
if (Array.isArray(cfg["depends_on"])) {
|
|
2402
|
+
dependsOn = cfg["depends_on"].map((d) => String(d).replace(/^['"]|['"]$/g, ""));
|
|
2403
|
+
} else if (typeof cfg["depends_on"] === "object" && cfg["depends_on"] !== null) {
|
|
2404
|
+
dependsOn = Object.keys(cfg["depends_on"]);
|
|
2405
|
+
} else if (typeof cfg["depends_on"] === "string") {
|
|
2406
|
+
const dStr = cfg["depends_on"].trim();
|
|
2407
|
+
if (dStr.startsWith("[") && dStr.endsWith("]")) {
|
|
2408
|
+
dependsOn = dStr.slice(1, -1).split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean);
|
|
2409
|
+
} else {
|
|
2410
|
+
dependsOn = [dStr.replace(/^['"]|['"]$/g, "")];
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
serviceList.push({
|
|
2414
|
+
name: svcName,
|
|
2415
|
+
image: typeof cfg["image"] === "string" ? cfg["image"] : void 0,
|
|
2416
|
+
ports,
|
|
2417
|
+
dependsOn
|
|
2418
|
+
});
|
|
2419
|
+
}
|
|
2420
|
+
const out = [];
|
|
2421
|
+
out.push(title ? `scene "${title}" theme=paper` : `scene theme=paper`);
|
|
2422
|
+
out.push("layout LR");
|
|
2423
|
+
out.push("");
|
|
2424
|
+
for (const svc of serviceList) {
|
|
2425
|
+
const id = sanitizeIdentifier(svc.name);
|
|
2426
|
+
const kind = inferSemanticKind(svc.name, svc.image);
|
|
2427
|
+
const ports = svc.ports ?? [];
|
|
2428
|
+
const label = ports.length > 0 ? `${svc.name} :${ports[0]}` : svc.name;
|
|
2429
|
+
out.push(`${kind} ${id} "${label}"`);
|
|
2430
|
+
}
|
|
2431
|
+
out.push("");
|
|
2432
|
+
out.push('beat main "Initialize and connect services":');
|
|
2433
|
+
out.push(" show $nodes stagger=60ms");
|
|
2434
|
+
for (const svc of serviceList) {
|
|
2435
|
+
const sourceId = sanitizeIdentifier(svc.name);
|
|
2436
|
+
for (const dep of svc.dependsOn ?? []) {
|
|
2437
|
+
const targetId = sanitizeIdentifier(dep);
|
|
2438
|
+
out.push(` ${sourceId} -> ${targetId} "depends on"`);
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
return out.join("\n");
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
// ../compat/src/infra/k8s-transpiler.ts
|
|
2445
|
+
function sanitize(id) {
|
|
2446
|
+
return id.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
2447
|
+
}
|
|
2448
|
+
function transpileKubernetesManifestsToMarkdy(manifestContent, title = "Kubernetes Cluster Topology") {
|
|
2449
|
+
const docs = manifestContent.split(/^---/m).map((d) => d.trim()).filter((d) => d.length > 0);
|
|
2450
|
+
const manifests = [];
|
|
2451
|
+
for (const doc of docs) {
|
|
2452
|
+
const raw = parseSimpleYaml(doc);
|
|
2453
|
+
const kind = typeof raw["kind"] === "string" ? raw["kind"] : "";
|
|
2454
|
+
const meta = raw["metadata"] || {};
|
|
2455
|
+
const name = typeof meta["name"] === "string" ? meta["name"] : "";
|
|
2456
|
+
const namespace = typeof meta["namespace"] === "string" ? meta["namespace"] : "default";
|
|
2457
|
+
if (!kind || !name) continue;
|
|
2458
|
+
manifests.push({
|
|
2459
|
+
kind,
|
|
2460
|
+
name,
|
|
2461
|
+
namespace
|
|
2462
|
+
});
|
|
2463
|
+
}
|
|
2464
|
+
const out = [];
|
|
2465
|
+
out.push(title ? `scene "${title}" theme=paper` : `scene theme=paper`);
|
|
2466
|
+
out.push("layout TB");
|
|
2467
|
+
out.push("");
|
|
2468
|
+
const namespaces = new Set(manifests.map((m) => m.namespace || "default"));
|
|
2469
|
+
for (const ns of namespaces) {
|
|
2470
|
+
const nsMembers = manifests.filter((m) => (m.namespace || "default") === ns).map((m) => sanitize(`${m.kind}_${m.name}`));
|
|
2471
|
+
if (nsMembers.length > 0) {
|
|
2472
|
+
out.push(`group ns_${sanitize(ns)} "Namespace: ${ns}": ${nsMembers.join(" ")}`);
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
out.push("");
|
|
2476
|
+
for (const m of manifests) {
|
|
2477
|
+
const id = sanitize(`${m.kind}_${m.name}`);
|
|
2478
|
+
let kind = "service";
|
|
2479
|
+
if (m.kind === "Ingress") kind = "gateway";
|
|
2480
|
+
else if (m.kind === "Service") kind = "load_balancer";
|
|
2481
|
+
else if (m.kind === "StatefulSet") kind = "database";
|
|
2482
|
+
else if (m.kind === "CronJob" || m.kind === "Job") kind = "worker";
|
|
2483
|
+
else if (m.kind === "PersistentVolumeClaim") kind = "storage";
|
|
2484
|
+
out.push(`${kind} ${id} "${m.name} (${m.kind})"`);
|
|
2485
|
+
}
|
|
2486
|
+
out.push("");
|
|
2487
|
+
out.push('beat main "Cluster Ingress & Service Mesh":');
|
|
2488
|
+
out.push(" show $nodes stagger=50ms");
|
|
2489
|
+
const ingresses = manifests.filter((m) => m.kind === "Ingress");
|
|
2490
|
+
const services = manifests.filter((m) => m.kind === "Service");
|
|
2491
|
+
const workloads = manifests.filter((m) => ["Deployment", "StatefulSet", "DaemonSet"].includes(m.kind));
|
|
2492
|
+
for (const ing of ingresses) {
|
|
2493
|
+
for (const svc of services) {
|
|
2494
|
+
out.push(` ${sanitize(`${ing.kind}_${ing.name}`)} -> ${sanitize(`${svc.kind}_${svc.name}`)} "route"`);
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
for (const svc of services) {
|
|
2498
|
+
for (const wl of workloads) {
|
|
2499
|
+
out.push(` ${sanitize(`${svc.kind}_${svc.name}`)} -> ${sanitize(`${wl.kind}_${wl.name}`)} "balance"`);
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
return out.join("\n");
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
// ../compat/src/infra/terraform-transpiler.ts
|
|
2506
|
+
function sanitizeId2(raw) {
|
|
2507
|
+
return raw.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
2508
|
+
}
|
|
2509
|
+
function inferKindFromTfType(type) {
|
|
2510
|
+
if (type.includes("database") || type.includes("db_instance") || type.includes("rds") || type.includes("dynamodb")) {
|
|
2511
|
+
return "database";
|
|
2512
|
+
}
|
|
2513
|
+
if (type.includes("elasticache") || type.includes("redis") || type.includes("memcached")) {
|
|
2514
|
+
return "cache";
|
|
2515
|
+
}
|
|
2516
|
+
if (type.includes("sqs") || type.includes("pubsub") || type.includes("servicebus") || type.includes("queue")) {
|
|
2517
|
+
return "queue";
|
|
2518
|
+
}
|
|
2519
|
+
if (type.includes("s3_bucket") || type.includes("storage_bucket") || type.includes("blob")) {
|
|
2520
|
+
return "storage";
|
|
2521
|
+
}
|
|
2522
|
+
if (type.includes("lb") || type.includes("alb") || type.includes("apigateway") || type.includes("gateway")) {
|
|
2523
|
+
return "gateway";
|
|
2524
|
+
}
|
|
2525
|
+
if (type.includes("cloudfront") || type.includes("cdn")) {
|
|
2526
|
+
return "cdn";
|
|
2527
|
+
}
|
|
2528
|
+
if (type.includes("lambda") || type.includes("cloudfunctions") || type.includes("function_app")) {
|
|
2529
|
+
return "worker";
|
|
2530
|
+
}
|
|
2531
|
+
if (type.includes("eks") || type.includes("gke") || type.includes("aks") || type.includes("cluster")) {
|
|
2532
|
+
return "cluster";
|
|
2533
|
+
}
|
|
2534
|
+
return "service";
|
|
2535
|
+
}
|
|
2536
|
+
function transpileTerraformStateToMarkdy(tfstateContent, sceneTitle = "Cloud Infrastructure Architecture") {
|
|
2537
|
+
let parsed;
|
|
2538
|
+
try {
|
|
2539
|
+
parsed = JSON.parse(tfstateContent);
|
|
2540
|
+
} catch {
|
|
2541
|
+
throw new Error("Invalid Terraform state JSON");
|
|
2542
|
+
}
|
|
2543
|
+
if (!parsed.resources || !Array.isArray(parsed.resources)) {
|
|
2544
|
+
return sceneTitle ? `scene "${sceneTitle}" theme=paper
|
|
2545
|
+
layout LR
|
|
2546
|
+
` : `scene theme=paper
|
|
2547
|
+
layout LR
|
|
2548
|
+
`;
|
|
2549
|
+
}
|
|
2550
|
+
const nodes = [];
|
|
2551
|
+
const edges = [];
|
|
2552
|
+
const vpcGroups = /* @__PURE__ */ new Map();
|
|
2553
|
+
const arnToResId = /* @__PURE__ */ new Map();
|
|
2554
|
+
for (const res of parsed.resources) {
|
|
2555
|
+
if (res.type.startsWith("aws_iam_") || res.type.includes("route_table") || res.type.includes("security_group")) {
|
|
2556
|
+
continue;
|
|
2557
|
+
}
|
|
2558
|
+
const firstInst = res.instances?.[0]?.attributes;
|
|
2559
|
+
const resId = sanitizeId2(`${res.type}_${res.name}`);
|
|
2560
|
+
const kind = inferKindFromTfType(res.type);
|
|
2561
|
+
const label = firstInst?.tags?.["Name"] || firstInst?.name || `${res.type.split("_").slice(-1)[0]}: ${res.name}`;
|
|
2562
|
+
const vpcId = typeof firstInst?.vpc_id === "string" ? sanitizeId2(firstInst.vpc_id) : void 0;
|
|
2563
|
+
nodes.push({ id: resId, kind, label, vpcId });
|
|
2564
|
+
if (typeof firstInst?.arn === "string") {
|
|
2565
|
+
arnToResId.set(firstInst.arn, resId);
|
|
2566
|
+
}
|
|
2567
|
+
if (vpcId) {
|
|
2568
|
+
if (!vpcGroups.has(vpcId)) vpcGroups.set(vpcId, []);
|
|
2569
|
+
vpcGroups.get(vpcId).push(resId);
|
|
2570
|
+
}
|
|
2571
|
+
if (typeof firstInst?.load_balancer_arn === "string") {
|
|
2572
|
+
edges.push({ from: firstInst.load_balancer_arn, to: resId, label: "routes" });
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
for (let i = 0; i < edges.length; i++) {
|
|
2576
|
+
const edge = edges[i];
|
|
2577
|
+
if (edge.from.startsWith("arn:") && arnToResId.has(edge.from)) {
|
|
2578
|
+
edge.from = arnToResId.get(edge.from);
|
|
2579
|
+
} else if (edge.from.startsWith("arn:")) {
|
|
2580
|
+
edge.from = sanitizeId2(edge.from);
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
const out = [];
|
|
2584
|
+
out.push(sceneTitle ? `scene "${sceneTitle}" theme=paper` : `scene theme=paper`);
|
|
2585
|
+
out.push("layout LR");
|
|
2586
|
+
out.push("");
|
|
2587
|
+
for (const [vpc, members] of vpcGroups) {
|
|
2588
|
+
if (members.length > 1) {
|
|
2589
|
+
out.push(`group ${vpc} "VPC Network": ${members.join(" ")}`);
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
for (const n of nodes) {
|
|
2593
|
+
out.push(`${n.kind} ${n.id} "${n.label}"`);
|
|
2594
|
+
}
|
|
2595
|
+
out.push("");
|
|
2596
|
+
out.push('beat main "Provisioned Infrastructure Flow":');
|
|
2597
|
+
out.push(" show $nodes stagger=40ms");
|
|
2598
|
+
if (edges.length > 0) {
|
|
2599
|
+
for (const e of edges) {
|
|
2600
|
+
out.push(` ${e.from} -> ${e.to} "${e.label || "connect"}"`);
|
|
2601
|
+
}
|
|
2602
|
+
} else if (nodes.length >= 2) {
|
|
2603
|
+
out.push(` ${nodes[0].id} -> ${nodes[1].id} "traffic"`);
|
|
2604
|
+
}
|
|
2605
|
+
return out.join("\n");
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
// ../compat/src/infra/drawio-transpiler.ts
|
|
2609
|
+
function sanitizeId3(raw, fallback) {
|
|
2610
|
+
const cleaned = raw.replace(/[^a-zA-Z0-9_]/g, "");
|
|
2611
|
+
if (/^[0-9]/.test(cleaned) || cleaned.length === 0) {
|
|
2612
|
+
return `${fallback}_${cleaned}`;
|
|
2613
|
+
}
|
|
2614
|
+
return cleaned;
|
|
2615
|
+
}
|
|
2616
|
+
function stripHtml(raw) {
|
|
2617
|
+
return raw.replace(/<br\s*\/?>/gi, " ").replace(/<\/?[^>]+(>|$)/g, "").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').trim();
|
|
2618
|
+
}
|
|
2619
|
+
function inferKindFromStyleAndLabel(style, label) {
|
|
2620
|
+
const styleLower = style.toLowerCase();
|
|
2621
|
+
const labelLower = label.toLowerCase();
|
|
2622
|
+
if (styleLower.includes("shape=cylinder") || styleLower.includes("datastore") || styleLower.includes("database")) {
|
|
2623
|
+
return "database";
|
|
2624
|
+
}
|
|
2625
|
+
if (styleLower.includes("shape=cloud") || styleLower.includes("network")) {
|
|
2626
|
+
return "cloud";
|
|
2627
|
+
}
|
|
2628
|
+
if (styleLower.includes("shape=actor") || styleLower.includes("person") || styleLower.includes("user")) {
|
|
2629
|
+
return "user";
|
|
2630
|
+
}
|
|
2631
|
+
if (styleLower.includes("shape=hexagon") || styleLower.includes("gateway")) {
|
|
2632
|
+
return "gateway";
|
|
2633
|
+
}
|
|
2634
|
+
if (styleLower.includes("queue") || styleLower.includes("message") || styleLower.includes("kafka") || styleLower.includes("sqs")) {
|
|
2635
|
+
return "queue";
|
|
2636
|
+
}
|
|
2637
|
+
if (styleLower.includes("cache") || styleLower.includes("redis")) {
|
|
2638
|
+
return "cache";
|
|
2639
|
+
}
|
|
2640
|
+
if (styleLower.includes("storage") || styleLower.includes("bucket") || styleLower.includes("s3")) {
|
|
2641
|
+
return "storage";
|
|
2642
|
+
}
|
|
2643
|
+
const profile = classifyTechnology(labelLower);
|
|
2644
|
+
if (profile.kind && profile.kind !== "service") {
|
|
2645
|
+
return profile.kind;
|
|
2646
|
+
}
|
|
2647
|
+
return "service";
|
|
2648
|
+
}
|
|
2649
|
+
function parseDrawioXml(xml, defaultTitle = "Imported Draw.io") {
|
|
2650
|
+
const cells = [];
|
|
2651
|
+
const diagramTitleMatch = xml.match(/<diagram[^>]*name="([^"]+)"/i);
|
|
2652
|
+
const title = diagramTitleMatch ? diagramTitleMatch[1] : defaultTitle;
|
|
2653
|
+
const cellRegex = /<mxCell\s+([^>]+)(?:\/>|>([\s\S]*?)<\/mxCell>)/gi;
|
|
2654
|
+
let match;
|
|
2655
|
+
while ((match = cellRegex.exec(xml)) !== null) {
|
|
2656
|
+
const attrStr = match[1];
|
|
2657
|
+
const innerContent = match[2] || "";
|
|
2658
|
+
const idMatch = attrStr.match(/id="([^"]+)"/i);
|
|
2659
|
+
if (!idMatch) continue;
|
|
2660
|
+
const id = idMatch[1];
|
|
2661
|
+
if (id === "0" || id === "1") continue;
|
|
2662
|
+
const valueAttrMatch = attrStr.match(/value="([^"]*)"/i);
|
|
2663
|
+
let rawValue = valueAttrMatch ? valueAttrMatch[1] : "";
|
|
2664
|
+
if (!rawValue && innerContent) {
|
|
2665
|
+
rawValue = innerContent;
|
|
2666
|
+
}
|
|
2667
|
+
const cleanValue = stripHtml(rawValue);
|
|
2668
|
+
const styleMatch = attrStr.match(/style="([^"]*)"/i);
|
|
2669
|
+
const style = styleMatch ? styleMatch[1] : "";
|
|
2670
|
+
const isVertex = /vertex="1"/i.test(attrStr);
|
|
2671
|
+
const isEdge = /edge="1"/i.test(attrStr);
|
|
2672
|
+
const sourceMatch = attrStr.match(/source="([^"]+)"/i);
|
|
2673
|
+
const targetMatch = attrStr.match(/target="([^"]+)"/i);
|
|
2674
|
+
const parentMatch = attrStr.match(/parent="([^"]+)"/i);
|
|
2675
|
+
cells.push({
|
|
2676
|
+
id,
|
|
2677
|
+
value: cleanValue,
|
|
2678
|
+
style,
|
|
2679
|
+
isVertex,
|
|
2680
|
+
isEdge,
|
|
2681
|
+
source: sourceMatch ? sourceMatch[1] : void 0,
|
|
2682
|
+
target: targetMatch ? targetMatch[1] : void 0,
|
|
2683
|
+
parent: parentMatch ? parentMatch[1] : void 0
|
|
2684
|
+
});
|
|
2685
|
+
}
|
|
2686
|
+
return { title, cells };
|
|
2687
|
+
}
|
|
2688
|
+
async function transpileDrawioToMarkdy(source, customTitle) {
|
|
2689
|
+
let xml = source.trim();
|
|
2690
|
+
if (xml.includes("<diagram") && !xml.includes("<mxGraphModel>")) {
|
|
2691
|
+
const diagramMatch = xml.match(/<diagram[^>]*>([\s\S]*?)<\/diagram>/i);
|
|
2692
|
+
if (diagramMatch) {
|
|
2693
|
+
const payload = diagramMatch[1].trim();
|
|
2694
|
+
try {
|
|
2695
|
+
const binaryStr = atob(payload);
|
|
2696
|
+
if (binaryStr.includes("<mxGraphModel")) {
|
|
2697
|
+
xml = binaryStr;
|
|
2698
|
+
} else if (typeof DecompressionStream !== "undefined") {
|
|
2699
|
+
const uint8 = new Uint8Array(binaryStr.length);
|
|
2700
|
+
for (let i = 0; i < binaryStr.length; i++) {
|
|
2701
|
+
uint8[i] = binaryStr.charCodeAt(i);
|
|
2702
|
+
}
|
|
2703
|
+
try {
|
|
2704
|
+
const ds = new DecompressionStream("deflate-raw");
|
|
2705
|
+
const writer = ds.writable.getWriter();
|
|
2706
|
+
writer.write(uint8);
|
|
2707
|
+
writer.close();
|
|
2708
|
+
const reader = ds.readable.getReader();
|
|
2709
|
+
const chunks = [];
|
|
2710
|
+
let totalLen = 0;
|
|
2711
|
+
while (true) {
|
|
2712
|
+
const { done, value } = await reader.read();
|
|
2713
|
+
if (done) break;
|
|
2714
|
+
if (value) {
|
|
2715
|
+
chunks.push(value);
|
|
2716
|
+
totalLen += value.length;
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
const decompressed = new Uint8Array(totalLen);
|
|
2720
|
+
let offset = 0;
|
|
2721
|
+
for (const c of chunks) {
|
|
2722
|
+
decompressed.set(c, offset);
|
|
2723
|
+
offset += c.length;
|
|
2724
|
+
}
|
|
2725
|
+
const decodedStr = new TextDecoder().decode(decompressed);
|
|
2726
|
+
xml = decodeURIComponent(decodedStr);
|
|
2727
|
+
} catch (e) {
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
} catch {
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
const model = parseDrawioXml(xml, customTitle);
|
|
2735
|
+
const vertexCells = model.cells.filter((c) => c.isVertex);
|
|
2736
|
+
const edgeCells = model.cells.filter((c) => c.isEdge);
|
|
2737
|
+
const cellIdToNodeId = /* @__PURE__ */ new Map();
|
|
2738
|
+
const nodes = [];
|
|
2739
|
+
for (const cell of vertexCells) {
|
|
2740
|
+
const rawLabel = cell.value || `Component_${cell.id}`;
|
|
2741
|
+
const rawIdentifier = isNaN(Number(cell.id)) && cell.id.length > 0 ? cell.id : cell.value || `node_${cell.id}`;
|
|
2742
|
+
const nodeId = sanitizeId3(rawIdentifier, `node_${cell.id}`);
|
|
2743
|
+
const kind = inferKindFromStyleAndLabel(cell.style, rawLabel);
|
|
2744
|
+
cellIdToNodeId.set(cell.id, nodeId);
|
|
2745
|
+
nodes.push({ id: nodeId, kind, label: rawLabel });
|
|
2746
|
+
}
|
|
2747
|
+
const lines = [];
|
|
2748
|
+
const sceneName = customTitle || model.title;
|
|
2749
|
+
lines.push(sceneName ? `scene "${sceneName}" theme=paper` : `scene theme=paper`);
|
|
2750
|
+
lines.push(`layout LR`);
|
|
2751
|
+
lines.push(``);
|
|
2752
|
+
if (nodes.length > 0) {
|
|
2753
|
+
for (const node of nodes) {
|
|
2754
|
+
lines.push(`${node.kind} ${node.id} "${node.label}"`);
|
|
2755
|
+
}
|
|
2756
|
+
lines.push(``);
|
|
2757
|
+
}
|
|
2758
|
+
const flows = [];
|
|
2759
|
+
for (const edge of edgeCells) {
|
|
2760
|
+
if (edge.source && edge.target) {
|
|
2761
|
+
const sourceId = cellIdToNodeId.get(edge.source);
|
|
2762
|
+
const targetId = cellIdToNodeId.get(edge.target);
|
|
2763
|
+
if (sourceId && targetId) {
|
|
2764
|
+
flows.push({
|
|
2765
|
+
from: sourceId,
|
|
2766
|
+
to: targetId,
|
|
2767
|
+
label: edge.value || void 0
|
|
2768
|
+
});
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
lines.push(`beat main "System Flow":`);
|
|
2773
|
+
lines.push(` show $nodes stagger=60ms`);
|
|
2774
|
+
if (flows.length > 0) {
|
|
2775
|
+
for (const flow of flows) {
|
|
2776
|
+
if (flow.label) {
|
|
2777
|
+
lines.push(` ${flow.from} -> ${flow.to} "${flow.label}"`);
|
|
2778
|
+
} else {
|
|
2779
|
+
lines.push(` ${flow.from} -> ${flow.to}`);
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
return {
|
|
2784
|
+
code: lines.join("\n"),
|
|
2785
|
+
nodeCount: nodes.length,
|
|
2786
|
+
edgeCount: flows.length
|
|
2787
|
+
};
|
|
2788
|
+
}
|
|
2789
|
+
|
|
11
2790
|
// src/tools.ts
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
2791
|
+
var ARCHITECTURE_TEMPLATES = [
|
|
2792
|
+
{
|
|
2793
|
+
id: "microservices-db",
|
|
2794
|
+
title: "Cloud Microservices & Database Tier",
|
|
2795
|
+
category: "Cloud / Microservices",
|
|
2796
|
+
description: "Multi-tier microservices architecture with API gateway, auth, database, and Redis cache.",
|
|
2797
|
+
code: `scene theme=paper width=1440 height=760
|
|
2798
|
+
layout LR
|
|
2799
|
+
|
|
2800
|
+
browser WebApp "Web Application"
|
|
2801
|
+
mobile MobileApp "Mobile Client"
|
|
2802
|
+
gateway ApiGateway "Cloud Gateway"
|
|
2803
|
+
auth AuthService "Auth / OAuth2"
|
|
2804
|
+
service OrderService "Order Service"
|
|
2805
|
+
service PaymentService "Payment Gateway"
|
|
2806
|
+
database MainDB "PostgreSQL"
|
|
2807
|
+
cache RedisCache "Redis Cluster"
|
|
2808
|
+
|
|
2809
|
+
group clients "User Surfaces": WebApp MobileApp
|
|
2810
|
+
group backend "Service Tier": ApiGateway AuthService OrderService PaymentService
|
|
2811
|
+
group dataTier "Data Tier": MainDB RedisCache
|
|
2812
|
+
|
|
2813
|
+
beat reveal "System Overview":
|
|
2814
|
+
show $nodes stagger=40ms
|
|
2815
|
+
|
|
2816
|
+
beat authFlow "Authenticate Request":
|
|
2817
|
+
frame clients ApiGateway AuthService zoom=1.12
|
|
2818
|
+
WebApp -> ApiGateway "GET /profile" -> AuthService "validate_jwt"
|
|
2819
|
+
WebApp <- ApiGateway "200 OK (Claims)"
|
|
2820
|
+
|
|
2821
|
+
beat checkout "Process Order":
|
|
2822
|
+
frame ApiGateway OrderService PaymentService dataTier zoom=1.1
|
|
2823
|
+
MobileApp -> ApiGateway "POST /checkout" -> OrderService "create_order"
|
|
2824
|
+
OrderService -> RedisCache "check inventory"
|
|
2825
|
+
OrderService -> PaymentService "authorize charge"
|
|
2826
|
+
PaymentService -> MainDB "record transaction"
|
|
2827
|
+
MobileApp <- ApiGateway "201 Created"`
|
|
2828
|
+
},
|
|
2829
|
+
{
|
|
2830
|
+
id: "ai-rag-pipeline",
|
|
2831
|
+
title: "AI Agent & RAG Pipeline",
|
|
2832
|
+
category: "AI / Machine Learning",
|
|
2833
|
+
description: "Retrieval-Augmented Generation agent flow with embedding vector search, LLM synthesis, and sandbox tool execution.",
|
|
2834
|
+
code: `scene theme=editorial width=1440 height=760
|
|
2835
|
+
layout LR
|
|
2836
|
+
|
|
2837
|
+
user User "Engineer"
|
|
2838
|
+
browser ChatUI "Chat Interface"
|
|
2839
|
+
service Orchestrator "Agent Orchestrator"
|
|
2840
|
+
service Embedder "Embedding Model"
|
|
2841
|
+
database VectorDB "Vector Index (Qdrant)"
|
|
2842
|
+
service LLM "Claude 3.5 / Gemini"
|
|
2843
|
+
service Tools "Tool Execution Engine"
|
|
2844
|
+
|
|
2845
|
+
group aiCore "Intelligence Engine": Embedder VectorDB LLM
|
|
2846
|
+
group execution "Tools & Sandbox": Tools
|
|
2847
|
+
|
|
2848
|
+
beat init "System Reveal":
|
|
2849
|
+
show $nodes stagger=50ms
|
|
2850
|
+
|
|
2851
|
+
beat retrieve "Query & Vector Search":
|
|
2852
|
+
frame User ChatUI Orchestrator aiCore zoom=1.12
|
|
2853
|
+
User -> ChatUI "Ask technical question" -> Orchestrator "parse intent"
|
|
2854
|
+
Orchestrator -> Embedder "embed(query)" -> VectorDB "cosine search (k=5)"
|
|
2855
|
+
Orchestrator <- VectorDB "retrieved context chunks"
|
|
2856
|
+
|
|
2857
|
+
beat generate "Synthesis & Tool Execution":
|
|
2858
|
+
frame Orchestrator LLM Tools zoom=1.15
|
|
2859
|
+
Orchestrator -> LLM "prompt + context"
|
|
2860
|
+
LLM -> Tools "execute_code(sql)"
|
|
2861
|
+
LLM <- Tools "tool_result"
|
|
2862
|
+
ChatUI <- Orchestrator "streamed response with citations"
|
|
2863
|
+
glow ChatUI color=#10b981`
|
|
2864
|
+
},
|
|
2865
|
+
{
|
|
2866
|
+
id: "kafka-event-driven",
|
|
2867
|
+
title: "Event-Driven Architecture & Kafka Fan-Out",
|
|
2868
|
+
category: "Messaging / Streaming",
|
|
2869
|
+
description: "Event ingestion and parallel asynchronous consumer worker fan-out with dead-letter queue.",
|
|
2870
|
+
code: `scene theme=midnight width=1440 height=760
|
|
2871
|
+
layout LR
|
|
2872
|
+
|
|
2873
|
+
service IngestionAPI "Ingestion API"
|
|
2874
|
+
queue KafkaTopic "orders.events"
|
|
2875
|
+
worker InventoryWorker "Inventory Worker"
|
|
2876
|
+
worker NotificationWorker "Email/SMS Worker"
|
|
2877
|
+
worker AnalyticsWorker "Clickhouse Sink"
|
|
2878
|
+
database InventoryDB "Inventory DB"
|
|
2879
|
+
database AnalyticsDB "Clickhouse"
|
|
2880
|
+
queue DLQ "Dead Letter Queue"
|
|
2881
|
+
|
|
2882
|
+
group workers "Consumer Worker Group": InventoryWorker NotificationWorker AnalyticsWorker
|
|
2883
|
+
|
|
2884
|
+
beat reveal "Topology":
|
|
2885
|
+
show $nodes stagger=40ms
|
|
2886
|
+
|
|
2887
|
+
beat publish "Publish Event":
|
|
2888
|
+
frame IngestionAPI KafkaTopic zoom=1.15
|
|
2889
|
+
IngestionAPI ~> KafkaTopic "publish(OrderPlaced)"
|
|
2890
|
+
glow KafkaTopic color=#38bdf8
|
|
2891
|
+
|
|
2892
|
+
beat fanout "Parallel Fan-out Processing":
|
|
2893
|
+
frame KafkaTopic workers zoom=1.12
|
|
2894
|
+
KafkaTopic ~> InventoryWorker "consume event" & KafkaTopic ~> NotificationWorker "consume event" & KafkaTopic ~> AnalyticsWorker "consume event"
|
|
2895
|
+
InventoryWorker -> InventoryDB "UPDATE stock"
|
|
2896
|
+
AnalyticsWorker -> AnalyticsDB "INSERT analytics"`
|
|
2897
|
+
},
|
|
2898
|
+
{
|
|
2899
|
+
id: "k8s-ingress-cluster",
|
|
2900
|
+
title: "Kubernetes Cluster & Cloud Ingress",
|
|
2901
|
+
category: "DevOps / Infrastructure",
|
|
2902
|
+
description: "Cloudflare edge, Traefik ingress controller, pod deployments, ClusterIP routing, and persistent storage volumes.",
|
|
2903
|
+
code: `scene theme=blueprint width=1440 height=800
|
|
2904
|
+
layout LR
|
|
2905
|
+
|
|
2906
|
+
cloud CDN "Cloudflare CDN"
|
|
2907
|
+
network Ingress "Traefik Ingress Controller"
|
|
2908
|
+
pod WebPod1 "web-frontend-pod-1"
|
|
2909
|
+
pod WebPod2 "web-frontend-pod-2"
|
|
2910
|
+
service ClusterIP "api-service (ClusterIP)"
|
|
2911
|
+
pod ApiPod1 "api-backend-pod-1"
|
|
2912
|
+
pod ApiPod2 "api-backend-pod-2"
|
|
2913
|
+
storage PV "Ceph CSI Volume"
|
|
2914
|
+
|
|
2915
|
+
group frontendPods "Frontend Deployment": WebPod1 WebPod2
|
|
2916
|
+
group apiPods "API Deployment": ApiPod1 ApiPod2
|
|
2917
|
+
|
|
2918
|
+
beat reveal "Cluster Architecture":
|
|
2919
|
+
show $nodes stagger=40ms
|
|
2920
|
+
|
|
2921
|
+
beat routing "Ingress Traffic Routing":
|
|
2922
|
+
frame CDN Ingress frontendPods zoom=1.12
|
|
2923
|
+
CDN -> Ingress "HTTPS Request" -> WebPod1 "reverse proxy"
|
|
2924
|
+
WebPod1 -> ClusterIP "internal call" -> ApiPod1 "gRPC invocation"
|
|
2925
|
+
ApiPod1 -> PV "read/write volume"
|
|
2926
|
+
CDN <- Ingress "200 HTTP OK"`
|
|
2927
|
+
},
|
|
2928
|
+
{
|
|
2929
|
+
id: "cicd-gitops-pipeline",
|
|
2930
|
+
title: "CI/CD GitOps Delivery Pipeline",
|
|
2931
|
+
category: "DevOps / CI-CD",
|
|
2932
|
+
description: "GitHub commits, automated GitHub Actions testing, container registry push, and ArgoCD production deployment.",
|
|
2933
|
+
code: `scene theme=graphite width=1440 height=720
|
|
2934
|
+
layout LR
|
|
2935
|
+
|
|
2936
|
+
user Dev "Developer"
|
|
2937
|
+
service GitHub "GitHub Repository"
|
|
2938
|
+
worker Actions "GitHub Actions CI"
|
|
2939
|
+
registry DockerHub "Container Registry"
|
|
2940
|
+
service ArgoCD "ArgoCD Controller"
|
|
2941
|
+
cluster Production "Kubernetes Prod"
|
|
2942
|
+
|
|
2943
|
+
beat reveal "Pipeline Infrastructure":
|
|
2944
|
+
show $nodes stagger=45ms
|
|
2945
|
+
|
|
2946
|
+
beat build "Commit & Build Validation":
|
|
2947
|
+
frame Dev GitHub Actions DockerHub zoom=1.12
|
|
2948
|
+
Dev -> GitHub "git push origin main"
|
|
2949
|
+
GitHub ~> Actions "trigger workflow"
|
|
2950
|
+
Actions -> Actions "run unit & visual tests"
|
|
2951
|
+
Actions -> DockerHub "docker push image:v1.0.7"
|
|
2952
|
+
glow DockerHub color=#10b981
|
|
2953
|
+
|
|
2954
|
+
beat deploy "GitOps Sync & Deployment":
|
|
2955
|
+
frame DockerHub ArgoCD Production zoom=1.15
|
|
2956
|
+
ArgoCD -> GitHub "detect manifest drift"
|
|
2957
|
+
ArgoCD -> DockerHub "pull image:v1.0.7"
|
|
2958
|
+
ArgoCD -> Production "apply rollout"
|
|
2959
|
+
glow Production color=#22c55e`
|
|
2960
|
+
},
|
|
2961
|
+
{
|
|
2962
|
+
id: "oauth2-oidc-flow",
|
|
2963
|
+
title: "OAuth2 / OIDC Authentication Flow",
|
|
2964
|
+
category: "Security / Identity",
|
|
2965
|
+
description: "End-to-end authorization code grant flow with IdP redirect, consent, token exchange, and protected API access.",
|
|
2966
|
+
code: `scene theme=paper width=1440 height=720
|
|
2967
|
+
layout LR
|
|
2968
|
+
|
|
2969
|
+
browser User "End User Browser"
|
|
2970
|
+
service ClientApp "OAuth Client App"
|
|
2971
|
+
auth IdP "Identity Provider (Auth0/Okta)"
|
|
2972
|
+
service ResourceServer "Protected API Server"
|
|
2973
|
+
|
|
2974
|
+
beat reveal "System Overview":
|
|
2975
|
+
show $nodes stagger=50ms
|
|
2976
|
+
|
|
2977
|
+
beat redirect "Authorize & Consent":
|
|
2978
|
+
frame User ClientApp IdP zoom=1.15
|
|
2979
|
+
User -> ClientApp "click 'Login with IdP'"
|
|
2980
|
+
User <- ClientApp "302 Redirect to /authorize"
|
|
2981
|
+
User -> IdP "submit credentials & consent"
|
|
2982
|
+
User <- IdP "302 Redirect with ?code=AUTH_CODE"
|
|
2983
|
+
|
|
2984
|
+
beat exchange "Token Exchange & API Access":
|
|
2985
|
+
frame ClientApp IdP ResourceServer zoom=1.15
|
|
2986
|
+
ClientApp -> IdP "POST /token (code + secret)"
|
|
2987
|
+
ClientApp <- IdP "200 OK (access_token + id_token)"
|
|
2988
|
+
ClientApp -> ResourceServer "GET /userinfo (Bearer Token)"
|
|
2989
|
+
ClientApp <- ResourceServer "200 OK (User Profile)"
|
|
2990
|
+
glow ClientApp color=#10b981`
|
|
2991
|
+
},
|
|
2992
|
+
{
|
|
2993
|
+
id: "multi-region-ha-cache",
|
|
2994
|
+
title: "Resilient Multi-Region High Availability & Cache-Aside",
|
|
2995
|
+
category: "Distributed Systems",
|
|
2996
|
+
description: "GeoDNS global routing, primary/failover regions, Redis master/replica cache-aside pattern, and Aurora global storage replication.",
|
|
2997
|
+
code: `scene theme=midnight width=1440 height=760
|
|
2998
|
+
layout LR
|
|
2999
|
+
|
|
3000
|
+
gateway GeoDNS "Global Route53 / Anycast"
|
|
3001
|
+
gateway RegionEast "US-East Gateway"
|
|
3002
|
+
gateway RegionWest "US-West Gateway"
|
|
3003
|
+
cache RedisPrimary "Redis Master"
|
|
3004
|
+
cache RedisReplica "Redis Read Replica"
|
|
3005
|
+
database AuroraGlobal "Aurora Multi-Region DB"
|
|
3006
|
+
|
|
3007
|
+
group eastTier "US-East (Primary)": RegionEast RedisPrimary
|
|
3008
|
+
group westTier "US-West (Failover)": RegionWest RedisReplica
|
|
3009
|
+
|
|
3010
|
+
beat reveal "Global Infrastructure":
|
|
3011
|
+
show $nodes stagger=40ms
|
|
3012
|
+
|
|
3013
|
+
beat readCache "Cache-Aside Read Flow":
|
|
3014
|
+
frame GeoDNS eastTier AuroraGlobal zoom=1.12
|
|
3015
|
+
GeoDNS -> RegionEast "route nearest user" -> RedisPrimary "GET item:101"
|
|
3016
|
+
RegionEast <- RedisPrimary "cache miss"
|
|
3017
|
+
RegionEast -> AuroraGlobal "SELECT FROM db"
|
|
3018
|
+
RegionEast -> RedisPrimary "SET item:101 (TTL 60s)"
|
|
3019
|
+
GeoDNS <- RegionEast "200 OK (Payload)"
|
|
3020
|
+
|
|
3021
|
+
beat replication "Global Storage Replication":
|
|
3022
|
+
frame RedisPrimary RedisReplica AuroraGlobal zoom=1.15
|
|
3023
|
+
RedisPrimary ~> RedisReplica "async sync" & AuroraGlobal ~> AuroraGlobal "storage replication"`
|
|
3024
|
+
},
|
|
3025
|
+
{
|
|
3026
|
+
id: "decision-flowchart",
|
|
3027
|
+
title: "Quality Gate Decision Flowchart",
|
|
3028
|
+
category: "Workflows / Flowcharts",
|
|
3029
|
+
description: "Top-down pull request quality evaluation workflow with branch decision diamonds and fallback rejection states.",
|
|
3030
|
+
code: `scene theme=sketchy width=1280 height=720 type=flowchart
|
|
3031
|
+
layout TB
|
|
3032
|
+
|
|
3033
|
+
start PR "New Pull Request"
|
|
3034
|
+
decision LintCheck "Lint & Typecheck Passed?"
|
|
3035
|
+
decision TestCheck "All 142 Tests Passed?"
|
|
3036
|
+
decision A11yCheck "Lighthouse 100/100 Score?"
|
|
3037
|
+
step Merge "Merge into Main"
|
|
3038
|
+
end Reject "Reject & Post PR Feedback"
|
|
3039
|
+
|
|
3040
|
+
beat reveal "Quality Gates":
|
|
3041
|
+
show $nodes stagger=50ms
|
|
3042
|
+
|
|
3043
|
+
beat evaluate "Validation Pipeline":
|
|
3044
|
+
PR -> LintCheck "run eslint & tsc"
|
|
3045
|
+
LintCheck -> TestCheck "yes"
|
|
3046
|
+
TestCheck -> A11yCheck "yes"
|
|
3047
|
+
A11yCheck -> Merge "yes (approved)"
|
|
3048
|
+
glow Merge color=#10b981
|
|
3049
|
+
|
|
3050
|
+
beat failure "Fallback Reject Path":
|
|
3051
|
+
LintCheck -> Reject "no (syntax error)"
|
|
3052
|
+
TestCheck -> Reject "no (broken tests)"`
|
|
3053
|
+
}
|
|
3054
|
+
];
|
|
24
3055
|
function handleValidateMarkdy(code, checkArchitecture = true) {
|
|
25
3056
|
try {
|
|
26
3057
|
const ast = parse(code);
|
|
@@ -136,33 +3167,105 @@ function handleExplainArchitecture(code) {
|
|
|
136
3167
|
}
|
|
137
3168
|
function handleGenerateMarkdyPrompt(userGoal) {
|
|
138
3169
|
const prompt = [
|
|
139
|
-
`You are an expert system architecture designer specializing in MarkdyScript
|
|
3170
|
+
`You are an expert system architecture designer specializing in MarkdyScript syntax and DSL.`,
|
|
140
3171
|
`Goal: ${userGoal}`,
|
|
141
3172
|
``,
|
|
142
3173
|
`### Instructions & Authoritative Reference:`,
|
|
143
|
-
`1. Follow the canonical MarkdyScript
|
|
144
|
-
`2.
|
|
145
|
-
`
|
|
146
|
-
`
|
|
147
|
-
`
|
|
148
|
-
`
|
|
149
|
-
`
|
|
3174
|
+
`1. Follow the canonical MarkdyScript specification hosted at: https://markdy.com/AGENT.md`,
|
|
3175
|
+
`2. Structure the diagram linearly in 4 distinct steps:`,
|
|
3176
|
+
` - Step 1: Directives: \`scene theme=paper width=1280 height=720\` and \`layout LR\``,
|
|
3177
|
+
` - Step 2: Semantic Nodes: \`<kind> <Id> ["Human Label"]\` (e.g., \`browser Client "Shopper"\`, \`gateway Gateway "API Gateway"\`, \`service OrderService\`, \`database OrdersDB\`)`,
|
|
3178
|
+
` - Step 3: Groups (optional): \`group <id> "<Label>": <Node1> <Node2>\``,
|
|
3179
|
+
` - Step 4: Storyboard Beats: \`beat <id> "<Caption>":\` containing indented flows and cues.`,
|
|
3180
|
+
`3. Flow Operators & Cycle Safety:`,
|
|
3181
|
+
` - Use \`->\` for forward requests/calls.`,
|
|
3182
|
+
` - Use \`<-\` for responses/returns (prevents cyclical layout overlap!). Never use \`->\` for return paths.`,
|
|
3183
|
+
` - Use \`~>\` for asynchronous events and pub-sub messaging.`,
|
|
3184
|
+
`4. Visual Cues: Use canonical cues: \`show $nodes\`, \`hide\`, \`frame <targets> zoom=1.15\`, \`glow <targets> color=#hex\`, \`focus\`, and \`&\` for parallel execution.`,
|
|
3185
|
+
`5. Output self-contained, valid MarkdyScript only.`
|
|
150
3186
|
].join("\n");
|
|
151
3187
|
return {
|
|
152
3188
|
content: [{ type: "text", text: prompt }]
|
|
153
3189
|
};
|
|
154
3190
|
}
|
|
3191
|
+
function handleGetArchitectureCatalog(filterCategory) {
|
|
3192
|
+
const filtered = filterCategory ? ARCHITECTURE_TEMPLATES.filter(
|
|
3193
|
+
(t) => t.category.toLowerCase().includes(filterCategory.toLowerCase()) || t.id.toLowerCase().includes(filterCategory.toLowerCase())
|
|
3194
|
+
) : ARCHITECTURE_TEMPLATES;
|
|
3195
|
+
const text = [
|
|
3196
|
+
`### Markdy Architecture Templates Catalog (${filtered.length} templates)`,
|
|
3197
|
+
"",
|
|
3198
|
+
...filtered.map(
|
|
3199
|
+
(t) => [
|
|
3200
|
+
`#### ${t.title} (\`${t.id}\`)`,
|
|
3201
|
+
`- **Category:** ${t.category}`,
|
|
3202
|
+
`- **Description:** ${t.description}`,
|
|
3203
|
+
"```markdy",
|
|
3204
|
+
t.code,
|
|
3205
|
+
"```",
|
|
3206
|
+
""
|
|
3207
|
+
].join("\n")
|
|
3208
|
+
)
|
|
3209
|
+
].join("\n");
|
|
3210
|
+
return {
|
|
3211
|
+
content: [{ type: "text", text }]
|
|
3212
|
+
};
|
|
3213
|
+
}
|
|
3214
|
+
function handleReadResource(uri) {
|
|
3215
|
+
switch (uri) {
|
|
3216
|
+
case "markdy://spec/agent-reference":
|
|
3217
|
+
return {
|
|
3218
|
+
contents: [
|
|
3219
|
+
{
|
|
3220
|
+
uri,
|
|
3221
|
+
mimeType: "text/markdown",
|
|
3222
|
+
text: `# MarkdyScript Canonical Specification Summary
|
|
3223
|
+
|
|
3224
|
+
Canonical URL: https://markdy.com/AGENT.md
|
|
3225
|
+
|
|
3226
|
+
Core Mental Model: Directives -> Nodes -> Groups -> Beats.
|
|
3227
|
+
Cycle Safety: Always use <- for responses back to callers to prevent layout rank cycles.`
|
|
3228
|
+
}
|
|
3229
|
+
]
|
|
3230
|
+
};
|
|
3231
|
+
case "markdy://templates/catalog":
|
|
3232
|
+
return {
|
|
3233
|
+
contents: [
|
|
3234
|
+
{
|
|
3235
|
+
uri,
|
|
3236
|
+
mimeType: "application/json",
|
|
3237
|
+
text: JSON.stringify(ARCHITECTURE_TEMPLATES, null, 2)
|
|
3238
|
+
}
|
|
3239
|
+
]
|
|
3240
|
+
};
|
|
3241
|
+
case "markdy://governance/rules":
|
|
3242
|
+
return {
|
|
3243
|
+
contents: [
|
|
3244
|
+
{
|
|
3245
|
+
uri,
|
|
3246
|
+
mimeType: "application/json",
|
|
3247
|
+
text: JSON.stringify(ARCH_RULE_PRESETS, null, 2)
|
|
3248
|
+
}
|
|
3249
|
+
]
|
|
3250
|
+
};
|
|
3251
|
+
default:
|
|
3252
|
+
throw new Error(`Resource not found: ${uri}`);
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
155
3255
|
|
|
156
3256
|
// src/index.ts
|
|
3257
|
+
var MCP_SERVER_VERSION = "1.0.25";
|
|
157
3258
|
function createMarkdyMcpServer() {
|
|
158
3259
|
const server = new Server(
|
|
159
3260
|
{
|
|
160
3261
|
name: "markdy-mcp-server",
|
|
161
|
-
version:
|
|
3262
|
+
version: MCP_SERVER_VERSION
|
|
162
3263
|
},
|
|
163
3264
|
{
|
|
164
3265
|
capabilities: {
|
|
165
|
-
tools: {}
|
|
3266
|
+
tools: {},
|
|
3267
|
+
resources: {},
|
|
3268
|
+
prompts: {}
|
|
166
3269
|
}
|
|
167
3270
|
}
|
|
168
3271
|
);
|
|
@@ -171,12 +3274,18 @@ function createMarkdyMcpServer() {
|
|
|
171
3274
|
tools: [
|
|
172
3275
|
{
|
|
173
3276
|
name: "validate_markdy_code",
|
|
174
|
-
description: "Validates MarkdyScript syntax, detects architectural rule violations, and outputs
|
|
3277
|
+
description: "Validates MarkdyScript diagram syntax, detects architectural rule violations, and outputs diagnostic suggestions with AI healing prompts.",
|
|
175
3278
|
inputSchema: {
|
|
176
3279
|
type: "object",
|
|
177
3280
|
properties: {
|
|
178
|
-
code: {
|
|
179
|
-
|
|
3281
|
+
code: {
|
|
3282
|
+
type: "string",
|
|
3283
|
+
description: "The complete MarkdyScript (.markdy) diagram code starting with 'scene'."
|
|
3284
|
+
},
|
|
3285
|
+
checkArchitecture: {
|
|
3286
|
+
type: "boolean",
|
|
3287
|
+
description: "Whether to run Well-Architected governance rule checks (e.g. layer boundaries, cycle detection, gateway checks). Default: true."
|
|
3288
|
+
}
|
|
180
3289
|
},
|
|
181
3290
|
required: ["code"]
|
|
182
3291
|
}
|
|
@@ -187,38 +3296,63 @@ function createMarkdyMcpServer() {
|
|
|
187
3296
|
inputSchema: {
|
|
188
3297
|
type: "object",
|
|
189
3298
|
properties: {
|
|
190
|
-
source: {
|
|
3299
|
+
source: {
|
|
3300
|
+
type: "string",
|
|
3301
|
+
description: "The source code or markup content to transpile into MarkdyScript."
|
|
3302
|
+
},
|
|
191
3303
|
format: {
|
|
192
3304
|
type: "string",
|
|
193
3305
|
enum: ["mermaid", "docker-compose", "k8s", "terraform", "drawio"],
|
|
194
|
-
description: "The source format."
|
|
3306
|
+
description: "The source format to transpile from. Must be exactly one of: 'mermaid', 'docker-compose', 'k8s', 'terraform', 'drawio'."
|
|
195
3307
|
},
|
|
196
|
-
title: {
|
|
3308
|
+
title: {
|
|
3309
|
+
type: "string",
|
|
3310
|
+
description: "Optional human-readable title for the generated scene header."
|
|
3311
|
+
}
|
|
197
3312
|
},
|
|
198
3313
|
required: ["source", "format"]
|
|
199
3314
|
}
|
|
200
3315
|
},
|
|
201
3316
|
{
|
|
202
3317
|
name: "explain_architecture",
|
|
203
|
-
description: "Analyzes a MarkdyScript AST
|
|
3318
|
+
description: "Analyzes a MarkdyScript AST to output structural topology summaries, component role counts, and governance health metrics.",
|
|
204
3319
|
inputSchema: {
|
|
205
3320
|
type: "object",
|
|
206
3321
|
properties: {
|
|
207
|
-
code: {
|
|
3322
|
+
code: {
|
|
3323
|
+
type: "string",
|
|
3324
|
+
description: "The MarkdyScript diagram code to inspect."
|
|
3325
|
+
}
|
|
208
3326
|
},
|
|
209
3327
|
required: ["code"]
|
|
210
3328
|
}
|
|
211
3329
|
},
|
|
212
3330
|
{
|
|
213
3331
|
name: "generate_markdy_prompt",
|
|
214
|
-
description: "Generates optimal LLM system prompts and grammar constraints for building high-quality Markdy architecture animations.",
|
|
3332
|
+
description: "Generates optimal LLM system prompts and grammar constraints for building high-quality, hallucination-resistant Markdy architecture animations.",
|
|
215
3333
|
inputSchema: {
|
|
216
3334
|
type: "object",
|
|
217
3335
|
properties: {
|
|
218
|
-
userGoal: {
|
|
3336
|
+
userGoal: {
|
|
3337
|
+
type: "string",
|
|
3338
|
+
description: "The architecture, workflow, or system flow description the user wants to visualize."
|
|
3339
|
+
}
|
|
219
3340
|
},
|
|
220
3341
|
required: ["userGoal"]
|
|
221
3342
|
}
|
|
3343
|
+
},
|
|
3344
|
+
{
|
|
3345
|
+
name: "get_architecture_catalog",
|
|
3346
|
+
description: "Returns the catalog of production-grade golden architecture templates (Microservices, RAG, Kafka, Kubernetes Ingress, GitOps CI/CD, OAuth2, HA Multi-Region, Flowcharts) with full runnable MarkdyScript code.",
|
|
3347
|
+
inputSchema: {
|
|
3348
|
+
type: "object",
|
|
3349
|
+
properties: {
|
|
3350
|
+
filterCategory: {
|
|
3351
|
+
type: "string",
|
|
3352
|
+
description: "Optional filter string (e.g. 'Cloud', 'AI', 'Messaging', 'Security', 'DevOps')."
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
222
3356
|
}
|
|
223
3357
|
]
|
|
224
3358
|
};
|
|
@@ -242,10 +3376,164 @@ function createMarkdyMcpServer() {
|
|
|
242
3376
|
return handleExplainArchitecture(String(safeArgs.code ?? ""));
|
|
243
3377
|
case "generate_markdy_prompt":
|
|
244
3378
|
return handleGenerateMarkdyPrompt(String(safeArgs.userGoal ?? ""));
|
|
3379
|
+
case "get_architecture_catalog":
|
|
3380
|
+
return handleGetArchitectureCatalog(safeArgs.filterCategory ? String(safeArgs.filterCategory) : void 0);
|
|
245
3381
|
default:
|
|
246
3382
|
throw new Error(`Unknown tool: ${name}`);
|
|
247
3383
|
}
|
|
248
3384
|
});
|
|
3385
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
3386
|
+
return {
|
|
3387
|
+
resources: [
|
|
3388
|
+
{
|
|
3389
|
+
uri: "markdy://spec/agent-reference",
|
|
3390
|
+
name: "MarkdyScript AI Agent Specification",
|
|
3391
|
+
description: "Canonical reference for MarkdyScript syntax, closed node vocabularies, and cycle-safety rules.",
|
|
3392
|
+
mimeType: "text/markdown"
|
|
3393
|
+
},
|
|
3394
|
+
{
|
|
3395
|
+
uri: "markdy://templates/catalog",
|
|
3396
|
+
name: "Markdy Architecture Templates Catalog",
|
|
3397
|
+
description: "JSON catalog of curated golden architecture templates.",
|
|
3398
|
+
mimeType: "application/json"
|
|
3399
|
+
},
|
|
3400
|
+
{
|
|
3401
|
+
uri: "markdy://governance/rules",
|
|
3402
|
+
name: "Well-Architected Governance Rules",
|
|
3403
|
+
description: "Lint presets and architecture rules for validating cloud architectures.",
|
|
3404
|
+
mimeType: "application/json"
|
|
3405
|
+
}
|
|
3406
|
+
]
|
|
3407
|
+
};
|
|
3408
|
+
});
|
|
3409
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
3410
|
+
const { uri } = request.params;
|
|
3411
|
+
return handleReadResource(uri);
|
|
3412
|
+
});
|
|
3413
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
3414
|
+
return {
|
|
3415
|
+
prompts: [
|
|
3416
|
+
{
|
|
3417
|
+
name: "create_architecture_diagram",
|
|
3418
|
+
description: "Guided workflow to design an animated Markdy architecture diagram following the 4-step mental model.",
|
|
3419
|
+
arguments: [
|
|
3420
|
+
{
|
|
3421
|
+
name: "userGoal",
|
|
3422
|
+
description: "The system or architecture to visualize.",
|
|
3423
|
+
required: true
|
|
3424
|
+
},
|
|
3425
|
+
{
|
|
3426
|
+
name: "theme",
|
|
3427
|
+
description: "Preferred visual theme (paper, editorial, midnight, blueprint, graphite, nebula, sketchy, terminal).",
|
|
3428
|
+
required: false
|
|
3429
|
+
},
|
|
3430
|
+
{
|
|
3431
|
+
name: "layout",
|
|
3432
|
+
description: "Layout direction (LR, TB, RL, BT).",
|
|
3433
|
+
required: false
|
|
3434
|
+
}
|
|
3435
|
+
]
|
|
3436
|
+
},
|
|
3437
|
+
{
|
|
3438
|
+
name: "audit_architecture",
|
|
3439
|
+
description: "Review a MarkdyScript diagram for Well-Architected governance, cycle overlap, and layer violations.",
|
|
3440
|
+
arguments: [
|
|
3441
|
+
{
|
|
3442
|
+
name: "code",
|
|
3443
|
+
description: "The MarkdyScript code to audit.",
|
|
3444
|
+
required: true
|
|
3445
|
+
}
|
|
3446
|
+
]
|
|
3447
|
+
},
|
|
3448
|
+
{
|
|
3449
|
+
name: "transpile_architecture",
|
|
3450
|
+
description: "Migrate existing Mermaid, Docker Compose, Kubernetes, or Terraform configurations into animated MarkdyScript.",
|
|
3451
|
+
arguments: [
|
|
3452
|
+
{
|
|
3453
|
+
name: "source",
|
|
3454
|
+
description: "The source code to convert.",
|
|
3455
|
+
required: true
|
|
3456
|
+
},
|
|
3457
|
+
{
|
|
3458
|
+
name: "format",
|
|
3459
|
+
description: "Source format (mermaid, docker-compose, k8s, terraform, drawio).",
|
|
3460
|
+
required: true
|
|
3461
|
+
}
|
|
3462
|
+
]
|
|
3463
|
+
}
|
|
3464
|
+
]
|
|
3465
|
+
};
|
|
3466
|
+
});
|
|
3467
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
3468
|
+
const { name, arguments: args } = request.params;
|
|
3469
|
+
const safeArgs = args || {};
|
|
3470
|
+
switch (name) {
|
|
3471
|
+
case "create_architecture_diagram": {
|
|
3472
|
+
const theme = safeArgs.theme || "paper";
|
|
3473
|
+
const layout = safeArgs.layout || "LR";
|
|
3474
|
+
return {
|
|
3475
|
+
description: `Create an animated Markdy architecture diagram for: ${safeArgs.userGoal}`,
|
|
3476
|
+
messages: [
|
|
3477
|
+
{
|
|
3478
|
+
role: "user",
|
|
3479
|
+
content: {
|
|
3480
|
+
type: "text",
|
|
3481
|
+
text: `Design an animated Markdy architecture diagram for: ${safeArgs.userGoal}
|
|
3482
|
+
|
|
3483
|
+
Configuration:
|
|
3484
|
+
- Theme: ${theme}
|
|
3485
|
+
- Layout: ${layout}
|
|
3486
|
+
|
|
3487
|
+
Follow the canonical 4-step Markdy mental model:
|
|
3488
|
+
1. Directives (scene theme=${theme} layout=${layout})
|
|
3489
|
+
2. Node declarations (<kind> <Id> ["Human Label"])
|
|
3490
|
+
3. Groups (group <id> "<Label>": ...)
|
|
3491
|
+
4. Animated Storyboard beats with cycle-safe routing (use '<-' for return calls).`
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
]
|
|
3495
|
+
};
|
|
3496
|
+
}
|
|
3497
|
+
case "audit_architecture": {
|
|
3498
|
+
return {
|
|
3499
|
+
description: "Audit Markdy diagram against Well-Architected rules",
|
|
3500
|
+
messages: [
|
|
3501
|
+
{
|
|
3502
|
+
role: "user",
|
|
3503
|
+
content: {
|
|
3504
|
+
type: "text",
|
|
3505
|
+
text: `Please validate and audit the following MarkdyScript diagram for syntax integrity, cycle overlap, and Well-Architected rules:
|
|
3506
|
+
|
|
3507
|
+
\`\`\`markdy
|
|
3508
|
+
${safeArgs.code}
|
|
3509
|
+
\`\`\``
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
]
|
|
3513
|
+
};
|
|
3514
|
+
}
|
|
3515
|
+
case "transpile_architecture": {
|
|
3516
|
+
return {
|
|
3517
|
+
description: `Transpile ${safeArgs.format} to MarkdyScript`,
|
|
3518
|
+
messages: [
|
|
3519
|
+
{
|
|
3520
|
+
role: "user",
|
|
3521
|
+
content: {
|
|
3522
|
+
type: "text",
|
|
3523
|
+
text: `Please convert the following ${safeArgs.format} configuration into an animated Markdy diagram:
|
|
3524
|
+
|
|
3525
|
+
\`\`\`
|
|
3526
|
+
${safeArgs.source}
|
|
3527
|
+
\`\`\``
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
]
|
|
3531
|
+
};
|
|
3532
|
+
}
|
|
3533
|
+
default:
|
|
3534
|
+
throw new Error(`Unknown prompt: ${name}`);
|
|
3535
|
+
}
|
|
3536
|
+
});
|
|
249
3537
|
return server;
|
|
250
3538
|
}
|
|
251
3539
|
async function startMcpServer() {
|
|
@@ -253,16 +3541,20 @@ async function startMcpServer() {
|
|
|
253
3541
|
const transport = new StdioServerTransport();
|
|
254
3542
|
await server.connect(transport);
|
|
255
3543
|
}
|
|
256
|
-
|
|
3544
|
+
var isDirectExecution = typeof process !== "undefined" && Boolean(process.argv[1]) && (process.argv[1].endsWith("index.js") || process.argv[1].endsWith("index.mjs") || process.argv[1].endsWith("markdy-mcp") || process.argv[1].endsWith("markdy-mcp.js") || process.argv[1].endsWith("markdy-mcp.mjs") || process.argv[1].includes("mcp-server"));
|
|
3545
|
+
if (isDirectExecution) {
|
|
257
3546
|
startMcpServer().catch((err) => {
|
|
258
3547
|
console.error("Fatal MCP Server Error:", err);
|
|
259
3548
|
process.exit(1);
|
|
260
3549
|
});
|
|
261
3550
|
}
|
|
262
3551
|
export {
|
|
3552
|
+
MCP_SERVER_VERSION,
|
|
263
3553
|
createMarkdyMcpServer,
|
|
264
3554
|
handleExplainArchitecture,
|
|
265
3555
|
handleGenerateMarkdyPrompt,
|
|
3556
|
+
handleGetArchitectureCatalog,
|
|
3557
|
+
handleReadResource,
|
|
266
3558
|
handleTranspileToMarkdy,
|
|
267
3559
|
handleValidateMarkdy,
|
|
268
3560
|
startMcpServer
|