@markdy/core 1.1.7 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -39
- package/dist/index.d.ts +356 -5
- package/dist/index.js +2761 -144
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -397,6 +397,7 @@ var CONTROL_KEYS = [
|
|
|
397
397
|
"resetView",
|
|
398
398
|
"fullscreen",
|
|
399
399
|
"svg",
|
|
400
|
+
"gif",
|
|
400
401
|
"share",
|
|
401
402
|
"code",
|
|
402
403
|
"theme"
|
|
@@ -448,6 +449,11 @@ var CONTROLS = {
|
|
|
448
449
|
svg: { group: "controls", key: "svg", type: "boolean" },
|
|
449
450
|
exportSvg: { group: "controls", key: "svg", type: "boolean" },
|
|
450
451
|
export_svg: { group: "controls", key: "svg", type: "boolean" },
|
|
452
|
+
gif: { group: "controls", key: "gif", type: "boolean" },
|
|
453
|
+
exportGif: { group: "controls", key: "gif", type: "boolean" },
|
|
454
|
+
export_gif: { group: "controls", key: "gif", type: "boolean" },
|
|
455
|
+
gifButton: { group: "controls", key: "gif", type: "boolean" },
|
|
456
|
+
gif_button: { group: "controls", key: "gif", type: "boolean" },
|
|
451
457
|
share: { group: "controls", key: "share", type: "boolean" },
|
|
452
458
|
shareLink: { group: "controls", key: "share", type: "boolean" },
|
|
453
459
|
share_link: { group: "controls", key: "share", type: "boolean" },
|
|
@@ -528,6 +534,11 @@ var FLAT = {
|
|
|
528
534
|
fullscreen_button: CONTROLS.fullscreen_button,
|
|
529
535
|
exportSvg: CONTROLS.exportSvg,
|
|
530
536
|
export_svg: CONTROLS.export_svg,
|
|
537
|
+
gif: CONTROLS.gif,
|
|
538
|
+
exportGif: CONTROLS.exportGif,
|
|
539
|
+
export_gif: CONTROLS.export_gif,
|
|
540
|
+
gifButton: CONTROLS.gifButton,
|
|
541
|
+
gif_button: CONTROLS.gif_button,
|
|
531
542
|
shareLink: CONTROLS.shareLink,
|
|
532
543
|
share_link: CONTROLS.share_link,
|
|
533
544
|
code: CONTROLS.code,
|
|
@@ -643,6 +654,7 @@ function resolvePlayer(config = {}, overrides = {}) {
|
|
|
643
654
|
resetView: resolveControl(configuredControls.resetView),
|
|
644
655
|
fullscreen: resolveControl(configuredControls.fullscreen),
|
|
645
656
|
svg: resolveControl(configuredControls.svg),
|
|
657
|
+
gif: resolveControl(configuredControls.gif, false),
|
|
646
658
|
share: resolveControl(configuredControls.share),
|
|
647
659
|
code: resolveControl(configuredControls.code, false),
|
|
648
660
|
theme: resolveControl(configuredControls.theme, hostControlDefault)
|
|
@@ -719,7 +731,8 @@ var EDGE_OPERATORS = {
|
|
|
719
731
|
"->": "request",
|
|
720
732
|
"<-": "response",
|
|
721
733
|
"~>": "event",
|
|
722
|
-
"--": "dependency"
|
|
734
|
+
"--": "dependency",
|
|
735
|
+
"..>": "dependency"
|
|
723
736
|
};
|
|
724
737
|
var RESERVED_SELECTORS = /* @__PURE__ */ new Set(["$title", "$nodes", "$edges"]);
|
|
725
738
|
var CUE_ALIASES = {
|
|
@@ -791,6 +804,41 @@ var NODE_W = 180;
|
|
|
791
804
|
var NODE_H = 76;
|
|
792
805
|
var VENN_NODE_SIZE = 220;
|
|
793
806
|
var GROUP_PAD = 28;
|
|
807
|
+
function computeNodeDimensions(decl, baseW = 180, baseH = 76) {
|
|
808
|
+
if (typeof decl.props?.width === "number") {
|
|
809
|
+
return {
|
|
810
|
+
width: decl.props.width,
|
|
811
|
+
height: typeof decl.props.height === "number" ? decl.props.height : baseH
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
const labelLen = (decl.label || "").length;
|
|
815
|
+
const tech = decl.props?.tech ?? decl.props?.sub;
|
|
816
|
+
const techLen = tech ? String(tech).length : 0;
|
|
817
|
+
const val = decl.props?.value ?? decl.props?.metric;
|
|
818
|
+
const valLen = val ? String(val).length : 0;
|
|
819
|
+
if (decl.kind === "dot") return { width: 64, height: 64 };
|
|
820
|
+
if (decl.kind === "matrix") return { width: 220, height: 96 };
|
|
821
|
+
const valueW = valLen > 0 ? Math.max(50, valLen * 9.5 + 16) : 0;
|
|
822
|
+
const words = (decl.label || "").trim().split(/\s+/).filter(Boolean);
|
|
823
|
+
const longestWord = Math.max(...words.map((w) => w.length), 0);
|
|
824
|
+
const avgLineChars = words.length > 1 ? Math.ceil(labelLen / Math.min(words.length, 2)) : labelLen;
|
|
825
|
+
const neededLabelChars = Math.max(longestWord + 2, avgLineChars, Math.min(labelLen, 22));
|
|
826
|
+
const maxChars = Math.max(neededLabelChars, techLen);
|
|
827
|
+
const neededTextW = Math.max(96, maxChars * 8.4);
|
|
828
|
+
const calculatedW = 56 + neededTextW + (valueW > 0 ? valueW + 14 : 0) + 18;
|
|
829
|
+
const minW = Math.max(baseW, Math.min(360, calculatedW));
|
|
830
|
+
let width = Math.ceil(minW / 8) * 8;
|
|
831
|
+
let height = baseH;
|
|
832
|
+
if (labelLen > 24 && techLen > 0) {
|
|
833
|
+
height = Math.max(height, 84);
|
|
834
|
+
}
|
|
835
|
+
return { width, height };
|
|
836
|
+
}
|
|
837
|
+
function resolveNodeStyle(decl, ast) {
|
|
838
|
+
if (!decl.style) return void 0;
|
|
839
|
+
if (typeof decl.style === "string") return ast.styles[decl.style]?.props;
|
|
840
|
+
return decl.style;
|
|
841
|
+
}
|
|
794
842
|
var DEFAULTS = {
|
|
795
843
|
show: 0.35,
|
|
796
844
|
hide: 0.35,
|
|
@@ -805,7 +853,13 @@ var DEFAULTS = {
|
|
|
805
853
|
function diagramType(ast) {
|
|
806
854
|
return ast.meta.type ?? "architecture";
|
|
807
855
|
}
|
|
808
|
-
function nodeShape(kind, dtype) {
|
|
856
|
+
function nodeShape(kind, dtype, props) {
|
|
857
|
+
if (typeof props?.shape === "string") {
|
|
858
|
+
const s = props.shape.toLowerCase();
|
|
859
|
+
if (s === "diamond" || s === "circle" || s === "pill" || s === "terminal" || s === "rounded" || s === "card" || s === "container") {
|
|
860
|
+
return s;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
809
863
|
if (kind === "terminal") return "terminal";
|
|
810
864
|
if (kind === "dot" || kind === "marker") return "circle";
|
|
811
865
|
if (kind === "token_strip" || kind === "chips") return "pill";
|
|
@@ -940,22 +994,31 @@ function layoutRanked(ast, edges, opts) {
|
|
|
940
994
|
});
|
|
941
995
|
}
|
|
942
996
|
const isVertical = direction === "TB" || direction === "BT";
|
|
997
|
+
const hasTitle = Boolean(ast.meta.title && ast.meta.title.trim().length > 0);
|
|
998
|
+
const hasBeatCaptions = ast.beats.some((b) => b.label && b.label.trim().length > 0);
|
|
999
|
+
const titleBand = hasTitle ? TITLE_BAND : 0;
|
|
1000
|
+
const bottomBand = hasBeatCaptions ? 44 : 0;
|
|
943
1001
|
const contentW = ast.meta.width - SAFE * 2;
|
|
944
|
-
const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
|
|
945
1002
|
const maxRank = Math.max(...byRank.keys(), 0);
|
|
946
1003
|
const rankCount = maxRank + 1;
|
|
947
1004
|
const maxInRank = Math.max(...[...byRank.values()].map((v) => v.length), 1);
|
|
948
1005
|
const nodes = [];
|
|
1006
|
+
const nodeDims = /* @__PURE__ */ new Map();
|
|
1007
|
+
for (const id of nodeIds) {
|
|
1008
|
+
nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
|
|
1009
|
+
}
|
|
949
1010
|
if (opts.columnLayout) {
|
|
950
1011
|
const count = nodeIds.length;
|
|
951
|
-
const
|
|
952
|
-
const
|
|
1012
|
+
const maxNodeW = Math.max(...nodeIds.map((id) => nodeDims.get(id)?.width ?? NODE_W));
|
|
1013
|
+
const colSpacing = Math.max(maxNodeW + 32, contentW / Math.max(count, 1));
|
|
1014
|
+
const totalW2 = (count - 1) * colSpacing + maxNodeW;
|
|
953
1015
|
const startX2 = SAFE + Math.max(0, (contentW - totalW2) / 2);
|
|
954
1016
|
nodeIds.forEach((id, idx) => {
|
|
955
1017
|
const decl = ast.nodes[id];
|
|
956
1018
|
const role = nodeRole(decl.kind);
|
|
957
|
-
const
|
|
958
|
-
const
|
|
1019
|
+
const dims = nodeDims.get(id) ?? { width: NODE_W, height: NODE_H };
|
|
1020
|
+
const x = startX2 + idx * colSpacing + (maxNodeW - dims.width) / 2;
|
|
1021
|
+
const y = (hasTitle ? titleBand : 20) + 32;
|
|
959
1022
|
nodes.push({
|
|
960
1023
|
id,
|
|
961
1024
|
kind: decl.kind,
|
|
@@ -963,12 +1026,12 @@ function layoutRanked(ast, edges, opts) {
|
|
|
963
1026
|
label: decl.label,
|
|
964
1027
|
x: snapGrid(x),
|
|
965
1028
|
y: snapGrid(y),
|
|
966
|
-
width:
|
|
967
|
-
height:
|
|
968
|
-
style: decl
|
|
1029
|
+
width: dims.width,
|
|
1030
|
+
height: dims.height,
|
|
1031
|
+
style: resolveNodeStyle(decl, ast),
|
|
969
1032
|
props: decl.props,
|
|
970
1033
|
opacity: 0,
|
|
971
|
-
shape: nodeShape(decl.kind, dtype),
|
|
1034
|
+
shape: nodeShape(decl.kind, dtype, decl.props),
|
|
972
1035
|
focal: decl.props.focal === true || decl.props.accent === true,
|
|
973
1036
|
column: idx
|
|
974
1037
|
});
|
|
@@ -976,18 +1039,22 @@ function layoutRanked(ast, edges, opts) {
|
|
|
976
1039
|
return nodes;
|
|
977
1040
|
}
|
|
978
1041
|
if (isVertical) {
|
|
979
|
-
const
|
|
980
|
-
const
|
|
1042
|
+
const contentH = ast.meta.height - SAFE - TITLE_BAND - SAFE;
|
|
1043
|
+
const maxNodeH = Math.max(...nodeIds.map((id) => nodeDims.get(id)?.height ?? NODE_H));
|
|
1044
|
+
const rowGap = Math.max(maxNodeH + 40, Math.min(200, contentH / Math.max(rankCount, 1)));
|
|
1045
|
+
const totalH = (rankCount - 1) * rowGap + maxNodeH;
|
|
981
1046
|
const startY = TITLE_BAND + Math.max(0, (contentH - totalH) / 2);
|
|
982
1047
|
for (const [rank, ids] of [...byRank.entries()].sort((a, b) => a[0] - b[0])) {
|
|
983
1048
|
const rowCount = ids.length;
|
|
984
|
-
const
|
|
985
|
-
const
|
|
1049
|
+
const maxRowW = Math.max(...ids.map((id) => nodeDims.get(id)?.width ?? NODE_W));
|
|
1050
|
+
const colSpacing = Math.max(maxRowW + 36, Math.min(320, contentW / Math.max(rowCount, 1)));
|
|
1051
|
+
const rankW = (rowCount - 1) * colSpacing + maxRowW;
|
|
986
1052
|
const rankStartX = SAFE + Math.max(0, (contentW - rankW) / 2);
|
|
987
1053
|
const y = startY + rank * rowGap;
|
|
988
1054
|
ids.forEach((id, idx) => {
|
|
989
1055
|
const decl = ast.nodes[id];
|
|
990
1056
|
const role = nodeRole(decl.kind);
|
|
1057
|
+
const dims = nodeDims.get(id) ?? { width: NODE_W, height: NODE_H };
|
|
991
1058
|
const x = rankStartX + idx * colSpacing;
|
|
992
1059
|
const focal = decl.props.focal === true || decl.props.accent === true;
|
|
993
1060
|
nodes.push({
|
|
@@ -997,32 +1064,44 @@ function layoutRanked(ast, edges, opts) {
|
|
|
997
1064
|
label: decl.label,
|
|
998
1065
|
x: snapGrid(x),
|
|
999
1066
|
y: snapGrid(y),
|
|
1000
|
-
width:
|
|
1001
|
-
height:
|
|
1002
|
-
style: decl
|
|
1067
|
+
width: dims.width,
|
|
1068
|
+
height: dims.height,
|
|
1069
|
+
style: resolveNodeStyle(decl, ast),
|
|
1003
1070
|
props: decl.props,
|
|
1004
1071
|
opacity: 0,
|
|
1005
|
-
shape: nodeShape(decl.kind, dtype),
|
|
1072
|
+
shape: nodeShape(decl.kind, dtype, decl.props),
|
|
1006
1073
|
focal
|
|
1007
1074
|
});
|
|
1008
1075
|
});
|
|
1009
1076
|
}
|
|
1010
1077
|
return nodes;
|
|
1011
1078
|
}
|
|
1012
|
-
const
|
|
1013
|
-
const
|
|
1014
|
-
|
|
1079
|
+
const rankWidths = /* @__PURE__ */ new Map();
|
|
1080
|
+
for (const [rank, ids] of byRank.entries()) {
|
|
1081
|
+
const maxW = Math.max(...ids.map((id) => nodeDims.get(id)?.width ?? NODE_W));
|
|
1082
|
+
rankWidths.set(rank, maxW);
|
|
1083
|
+
}
|
|
1084
|
+
const totalRankWidths = [...rankWidths.values()].reduce((a, b) => a + b, 0);
|
|
1085
|
+
const minColGap = 56;
|
|
1086
|
+
const availableGapW = Math.max(0, contentW - totalRankWidths);
|
|
1087
|
+
const colGap = rankCount > 1 ? Math.max(minColGap, Math.min(160, availableGapW / (rankCount - 1))) : 0;
|
|
1088
|
+
const totalW = totalRankWidths + (rankCount - 1) * colGap;
|
|
1015
1089
|
const startX = SAFE + Math.max(0, (contentW - totalW) / 2);
|
|
1016
|
-
|
|
1090
|
+
const availableH = Math.max(NODE_H, ast.meta.height - titleBand - bottomBand);
|
|
1091
|
+
let currentRankX = startX;
|
|
1092
|
+
for (let rank = 0; rank < rankCount; rank++) {
|
|
1093
|
+
const ids = byRank.get(rank) ?? [];
|
|
1094
|
+
const rankW = rankWidths.get(rank) ?? NODE_W;
|
|
1017
1095
|
const rowCount = ids.length;
|
|
1018
|
-
const
|
|
1019
|
-
const
|
|
1020
|
-
const
|
|
1021
|
-
const
|
|
1022
|
-
const
|
|
1096
|
+
const maxH = Math.max(...ids.map((id) => nodeDims.get(id)?.height ?? NODE_H), NODE_H);
|
|
1097
|
+
const maxPossibleRowStep = rowCount > 1 ? (availableH - maxH) / (rowCount - 1) : 0;
|
|
1098
|
+
const rowSpacing = rowCount > 1 ? Math.min(180, Math.max(maxH + 28, maxPossibleRowStep)) : 0;
|
|
1099
|
+
const colH = (rowCount - 1) * rowSpacing + maxH;
|
|
1100
|
+
const colStartY = titleBand + Math.max(0, (availableH - colH) / 2);
|
|
1023
1101
|
ids.forEach((id, idx) => {
|
|
1024
1102
|
const decl = ast.nodes[id];
|
|
1025
1103
|
const role = nodeRole(decl.kind);
|
|
1104
|
+
const dims = nodeDims.get(id) ?? { width: NODE_W, height: NODE_H };
|
|
1026
1105
|
const y = colStartY + idx * rowSpacing;
|
|
1027
1106
|
const focal = decl.props.focal === true || decl.props.accent === true;
|
|
1028
1107
|
nodes.push({
|
|
@@ -1030,17 +1109,18 @@ function layoutRanked(ast, edges, opts) {
|
|
|
1030
1109
|
kind: decl.kind,
|
|
1031
1110
|
role,
|
|
1032
1111
|
label: decl.label,
|
|
1033
|
-
x: snapGrid(
|
|
1112
|
+
x: snapGrid(currentRankX),
|
|
1034
1113
|
y: snapGrid(y),
|
|
1035
|
-
width:
|
|
1036
|
-
height:
|
|
1037
|
-
style: decl
|
|
1114
|
+
width: dims.width,
|
|
1115
|
+
height: dims.height,
|
|
1116
|
+
style: resolveNodeStyle(decl, ast),
|
|
1038
1117
|
props: decl.props,
|
|
1039
1118
|
opacity: 0,
|
|
1040
|
-
shape: nodeShape(decl.kind, dtype),
|
|
1119
|
+
shape: nodeShape(decl.kind, dtype, decl.props),
|
|
1041
1120
|
focal
|
|
1042
1121
|
});
|
|
1043
1122
|
});
|
|
1123
|
+
currentRankX += rankW + colGap;
|
|
1044
1124
|
}
|
|
1045
1125
|
return nodes;
|
|
1046
1126
|
}
|
|
@@ -1141,7 +1221,7 @@ function layoutTree(ast, edges) {
|
|
|
1141
1221
|
y: snapGrid(pos.y),
|
|
1142
1222
|
width: NODE_W,
|
|
1143
1223
|
height: NODE_H,
|
|
1144
|
-
style: decl
|
|
1224
|
+
style: resolveNodeStyle(decl, ast),
|
|
1145
1225
|
props: decl.props,
|
|
1146
1226
|
opacity: 0,
|
|
1147
1227
|
shape: "card",
|
|
@@ -1209,10 +1289,10 @@ function layoutConstellation(ast) {
|
|
|
1209
1289
|
y: snapGrid(y),
|
|
1210
1290
|
width: NODE_W,
|
|
1211
1291
|
height: NODE_H,
|
|
1212
|
-
style: decl
|
|
1292
|
+
style: resolveNodeStyle(decl, ast),
|
|
1213
1293
|
props: decl.props,
|
|
1214
1294
|
opacity: 0,
|
|
1215
|
-
shape: nodeShape(decl.kind, "constellation"),
|
|
1295
|
+
shape: nodeShape(decl.kind, "constellation", decl.props),
|
|
1216
1296
|
focal: isFocal || decl.props.focal === true || decl.props.accent === true
|
|
1217
1297
|
});
|
|
1218
1298
|
}
|
|
@@ -1257,7 +1337,7 @@ function layoutLoop(ast) {
|
|
|
1257
1337
|
y: snapGrid(y),
|
|
1258
1338
|
width: isHub ? snapGrid(NODE_W * 1.25) : NODE_W,
|
|
1259
1339
|
height: isHub ? snapGrid(NODE_H * 1.15) : NODE_H,
|
|
1260
|
-
style: decl
|
|
1340
|
+
style: resolveNodeStyle(decl, ast),
|
|
1261
1341
|
props: decl.props,
|
|
1262
1342
|
opacity: 0,
|
|
1263
1343
|
shape: isHub ? "pill" : "card",
|
|
@@ -1311,7 +1391,7 @@ function layoutMedallion(ast, edges) {
|
|
|
1311
1391
|
y: snapGrid(rowY),
|
|
1312
1392
|
width: NODE_W,
|
|
1313
1393
|
height: NODE_H,
|
|
1314
|
-
style: decl
|
|
1394
|
+
style: resolveNodeStyle(decl, ast),
|
|
1315
1395
|
props: decl.props,
|
|
1316
1396
|
opacity: 0,
|
|
1317
1397
|
shape: "card",
|
|
@@ -1373,7 +1453,7 @@ function layoutQuadrant(ast) {
|
|
|
1373
1453
|
y: snapGrid(y),
|
|
1374
1454
|
width: NODE_W,
|
|
1375
1455
|
height: NODE_H,
|
|
1376
|
-
style: decl
|
|
1456
|
+
style: resolveNodeStyle(decl, ast),
|
|
1377
1457
|
props: decl.props,
|
|
1378
1458
|
opacity: 0,
|
|
1379
1459
|
shape: "card",
|
|
@@ -1426,7 +1506,7 @@ function layoutSwimlane(ast, edges) {
|
|
|
1426
1506
|
y: snapGrid(laneY),
|
|
1427
1507
|
width: NODE_W,
|
|
1428
1508
|
height: NODE_H,
|
|
1429
|
-
style: decl
|
|
1509
|
+
style: resolveNodeStyle(decl, ast),
|
|
1430
1510
|
props: decl.props,
|
|
1431
1511
|
opacity: 0,
|
|
1432
1512
|
shape: "card",
|
|
@@ -1475,7 +1555,7 @@ function layoutPyramid(ast) {
|
|
|
1475
1555
|
y: snapGrid(tierY),
|
|
1476
1556
|
width: snapGrid(nodeW),
|
|
1477
1557
|
height: NODE_H,
|
|
1478
|
-
style: decl
|
|
1558
|
+
style: resolveNodeStyle(decl, ast),
|
|
1479
1559
|
props: decl.props,
|
|
1480
1560
|
opacity: 0,
|
|
1481
1561
|
shape: "card",
|
|
@@ -1508,7 +1588,7 @@ function layoutTimeline(ast) {
|
|
|
1508
1588
|
y: snapGrid(y),
|
|
1509
1589
|
width: NODE_W,
|
|
1510
1590
|
height: NODE_H,
|
|
1511
|
-
style: decl
|
|
1591
|
+
style: resolveNodeStyle(decl, ast),
|
|
1512
1592
|
props: decl.props,
|
|
1513
1593
|
opacity: 0,
|
|
1514
1594
|
shape: "pill",
|
|
@@ -1551,7 +1631,7 @@ function layoutGantt(ast) {
|
|
|
1551
1631
|
y: snapGrid(y),
|
|
1552
1632
|
width: snapGrid(w),
|
|
1553
1633
|
height: barH,
|
|
1554
|
-
style: decl
|
|
1634
|
+
style: resolveNodeStyle(decl, ast),
|
|
1555
1635
|
props: decl.props,
|
|
1556
1636
|
opacity: 0,
|
|
1557
1637
|
shape: "pill",
|
|
@@ -1591,7 +1671,7 @@ function layoutVenn(ast) {
|
|
|
1591
1671
|
y: snapGrid(y),
|
|
1592
1672
|
width: VENN_NODE_SIZE,
|
|
1593
1673
|
height: VENN_NODE_SIZE,
|
|
1594
|
-
style: decl
|
|
1674
|
+
style: resolveNodeStyle(decl, ast),
|
|
1595
1675
|
props: decl.props,
|
|
1596
1676
|
opacity: 0,
|
|
1597
1677
|
shape: "circle",
|
|
@@ -1625,7 +1705,7 @@ function layoutLayers(ast) {
|
|
|
1625
1705
|
y: snapGrid(y),
|
|
1626
1706
|
width: snapGrid(layerW),
|
|
1627
1707
|
height: snapGrid(layerH),
|
|
1628
|
-
style: decl
|
|
1708
|
+
style: resolveNodeStyle(decl, ast),
|
|
1629
1709
|
props: decl.props,
|
|
1630
1710
|
opacity: 0,
|
|
1631
1711
|
shape: "rounded",
|
|
@@ -1672,7 +1752,7 @@ function layoutNested(ast) {
|
|
|
1672
1752
|
y: snapGrid(y),
|
|
1673
1753
|
width: snapGrid(w),
|
|
1674
1754
|
height: snapGrid(h),
|
|
1675
|
-
style: decl
|
|
1755
|
+
style: resolveNodeStyle(decl, ast),
|
|
1676
1756
|
props: decl.props,
|
|
1677
1757
|
opacity: 0,
|
|
1678
1758
|
shape: isCore ? "card" : "container",
|
|
@@ -1706,7 +1786,7 @@ function layoutRadar(ast) {
|
|
|
1706
1786
|
y: snapGrid(y),
|
|
1707
1787
|
width: NODE_W,
|
|
1708
1788
|
height: NODE_H,
|
|
1709
|
-
style: decl
|
|
1789
|
+
style: resolveNodeStyle(decl, ast),
|
|
1710
1790
|
props: decl.props,
|
|
1711
1791
|
opacity: 0,
|
|
1712
1792
|
shape: "rounded",
|
|
@@ -2094,17 +2174,32 @@ function computeAdaptiveDimensions(ast, edges) {
|
|
|
2094
2174
|
autoH = Math.max(720, Math.min(2e3, requiredH));
|
|
2095
2175
|
}
|
|
2096
2176
|
} else {
|
|
2097
|
-
const
|
|
2098
|
-
const
|
|
2099
|
-
|
|
2177
|
+
const hasTitle = Boolean(ast.meta.title && ast.meta.title.trim().length > 0);
|
|
2178
|
+
const hasBeatCaptions = ast.beats.some((b) => b.label && b.label.trim().length > 0);
|
|
2179
|
+
const titleBand = hasTitle ? TITLE_BAND : 0;
|
|
2180
|
+
const bottomBand = hasBeatCaptions ? 44 : 0;
|
|
2181
|
+
const maxRankW = Math.max(...nodeIds.map((id) => computeNodeDimensions(ast.nodes[id]).width));
|
|
2182
|
+
const maxRankH = Math.max(...nodeIds.map((id) => computeNodeDimensions(ast.nodes[id]).height));
|
|
2183
|
+
const requiredW = SAFE * 2 + rankCount * maxRankW + (rankCount - 1) * 56 + groupPaddingBonus;
|
|
2184
|
+
const requiredH = SAFE * 2 + titleBand + bottomBand + maxInRank * maxRankH + (maxInRank - 1) * 44 + groupPaddingBonus;
|
|
2185
|
+
if (maxInRank === 1 && !hasTitle) {
|
|
2186
|
+
autoW = Math.max(1024, Math.min(2560, requiredW));
|
|
2187
|
+
autoH = Math.max(hasBeatCaptions ? 240 : 208, Math.min(360, requiredH + 24));
|
|
2188
|
+
} else if (maxInRank === 2 && !hasTitle) {
|
|
2189
|
+
autoW = Math.max(1024, Math.min(2560, requiredW));
|
|
2190
|
+
autoH = Math.max(hasBeatCaptions ? 368 : 336, Math.min(480, requiredH + 24));
|
|
2191
|
+
} else if (maxInRank === 3 && !hasTitle) {
|
|
2192
|
+
autoW = Math.max(1152, Math.min(2560, requiredW));
|
|
2193
|
+
autoH = Math.max(hasBeatCaptions ? 480 : 448, Math.min(640, requiredH + 24));
|
|
2194
|
+
} else if (nodeCount <= 2 && rankCount <= 2) {
|
|
2100
2195
|
autoW = 1024;
|
|
2101
|
-
autoH = 576;
|
|
2196
|
+
autoH = hasTitle ? 576 : 384;
|
|
2102
2197
|
} else if (nodeCount <= 4 && rankCount <= 3 && maxInRank <= 2) {
|
|
2103
2198
|
autoW = 1152;
|
|
2104
|
-
autoH = 648;
|
|
2199
|
+
autoH = hasTitle ? 648 : 448;
|
|
2105
2200
|
} else {
|
|
2106
2201
|
autoW = Math.max(1152, Math.min(2560, requiredW));
|
|
2107
|
-
autoH = Math.max(648, Math.min(1600, requiredH));
|
|
2202
|
+
autoH = Math.max(hasTitle ? 648 : maxInRank <= 2 ? 448 : 576, Math.min(1600, requiredH));
|
|
2108
2203
|
}
|
|
2109
2204
|
}
|
|
2110
2205
|
}
|
|
@@ -2343,7 +2438,7 @@ var THEMES = {
|
|
|
2343
2438
|
dependency: "#818cf8"
|
|
2344
2439
|
},
|
|
2345
2440
|
fonts: {
|
|
2346
|
-
title: "
|
|
2441
|
+
title: "ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Inter, sans-serif",
|
|
2347
2442
|
nodeName: "ui-sans-serif, system-ui, sans-serif",
|
|
2348
2443
|
mono: "ui-monospace, SFMono-Regular, Menlo, monospace"
|
|
2349
2444
|
},
|
|
@@ -2440,6 +2535,116 @@ var THEMES = {
|
|
|
2440
2535
|
},
|
|
2441
2536
|
radiusMd: 4,
|
|
2442
2537
|
spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 40 }
|
|
2538
|
+
},
|
|
2539
|
+
ink: {
|
|
2540
|
+
name: "ink",
|
|
2541
|
+
canvas: "#f8fafd",
|
|
2542
|
+
surface: "#ffffff",
|
|
2543
|
+
surfaceRaised: "#eff6ff",
|
|
2544
|
+
border: "#1d4ed8",
|
|
2545
|
+
text: "#0f172a",
|
|
2546
|
+
textMuted: "#3b629b",
|
|
2547
|
+
paper: "#f8fafd",
|
|
2548
|
+
ink: "#1e40af",
|
|
2549
|
+
muted: "#4776b2",
|
|
2550
|
+
rule: "rgba(29, 78, 216, 0.14)",
|
|
2551
|
+
soft: "#60a5fa",
|
|
2552
|
+
link: "#2563eb",
|
|
2553
|
+
gridMinor: "rgba(37, 99, 235, 0.05)",
|
|
2554
|
+
gridMajor: "rgba(37, 99, 235, 0.10)",
|
|
2555
|
+
vignette: "rgba(219, 234, 254, 0.45)",
|
|
2556
|
+
accent: "#1d4ed8",
|
|
2557
|
+
accentTint: "rgba(29, 78, 216, 0.08)",
|
|
2558
|
+
nodeSurface: "#ffffff",
|
|
2559
|
+
nodeSurfaceRaised: "#f4f8ff",
|
|
2560
|
+
hairline: "rgba(29, 78, 216, 0.20)",
|
|
2561
|
+
shadow: "rgba(15, 35, 90, 0.09)",
|
|
2562
|
+
labelPlate: "#ffffff",
|
|
2563
|
+
flatCards: false,
|
|
2564
|
+
roles: {
|
|
2565
|
+
...ROLE_COLORS,
|
|
2566
|
+
compute: "#1d4ed8",
|
|
2567
|
+
code: "#2563eb",
|
|
2568
|
+
client: "#0284c7",
|
|
2569
|
+
data: "#0369a1",
|
|
2570
|
+
messaging: "#4338ca",
|
|
2571
|
+
network: "#0284c7",
|
|
2572
|
+
security: "#1e3a8a",
|
|
2573
|
+
platform: "#3b82f6",
|
|
2574
|
+
observability: "#0891b2",
|
|
2575
|
+
delivery: "#4f46e5",
|
|
2576
|
+
flow: "#2563eb",
|
|
2577
|
+
distributed: "#475569"
|
|
2578
|
+
},
|
|
2579
|
+
edges: {
|
|
2580
|
+
request: "#1d4ed8",
|
|
2581
|
+
response: "#3b82f6",
|
|
2582
|
+
event: "#0284c7",
|
|
2583
|
+
dependency: "#64748b"
|
|
2584
|
+
},
|
|
2585
|
+
fonts: {
|
|
2586
|
+
title: 'ui-serif, "Charter", "Newsreader", "Georgia", "Songti SC", "Noto Serif", serif',
|
|
2587
|
+
nodeName: 'ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Inter, sans-serif',
|
|
2588
|
+
mono: '"JetBrains Mono", "SF Mono", ui-monospace, Menlo, monospace'
|
|
2589
|
+
},
|
|
2590
|
+
radiusMd: 6,
|
|
2591
|
+
spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 40 },
|
|
2592
|
+
series: ["#1d4ed8", "#0284c7", "#4338ca", "#0891b2", "#1e3a8a"]
|
|
2593
|
+
},
|
|
2594
|
+
doodle: {
|
|
2595
|
+
name: "doodle",
|
|
2596
|
+
canvas: "#fffef9",
|
|
2597
|
+
surface: "#ffffff",
|
|
2598
|
+
surfaceRaised: "#fdf8ec",
|
|
2599
|
+
border: "#18181b",
|
|
2600
|
+
text: "#18181b",
|
|
2601
|
+
textMuted: "#52525b",
|
|
2602
|
+
paper: "#fffef9",
|
|
2603
|
+
ink: "#18181b",
|
|
2604
|
+
muted: "#52525b",
|
|
2605
|
+
rule: "rgba(24, 24, 27, 0.16)",
|
|
2606
|
+
soft: "#71717a",
|
|
2607
|
+
link: "#f43f5e",
|
|
2608
|
+
gridMinor: "rgba(24, 24, 27, 0.05)",
|
|
2609
|
+
gridMajor: "rgba(24, 24, 27, 0.08)",
|
|
2610
|
+
vignette: "rgba(245, 235, 210, 0.35)",
|
|
2611
|
+
accent: "#f43f5e",
|
|
2612
|
+
accentTint: "rgba(244, 63, 94, 0.12)",
|
|
2613
|
+
nodeSurface: "#ffffff",
|
|
2614
|
+
nodeSurfaceRaised: "#fffdf5",
|
|
2615
|
+
hairline: "rgba(24, 24, 27, 0.25)",
|
|
2616
|
+
shadow: "rgba(24, 24, 27, 0.15)",
|
|
2617
|
+
labelPlate: "#ffffff",
|
|
2618
|
+
flatCards: true,
|
|
2619
|
+
roles: {
|
|
2620
|
+
...ROLE_COLORS,
|
|
2621
|
+
compute: "#2563eb",
|
|
2622
|
+
code: "#2563eb",
|
|
2623
|
+
client: "#f43f5e",
|
|
2624
|
+
data: "#10b981",
|
|
2625
|
+
messaging: "#8b5cf6",
|
|
2626
|
+
network: "#06b6d4",
|
|
2627
|
+
security: "#ef4444",
|
|
2628
|
+
platform: "#f59e0b",
|
|
2629
|
+
observability: "#14b8a6",
|
|
2630
|
+
delivery: "#f59e0b",
|
|
2631
|
+
flow: "#ec4899",
|
|
2632
|
+
distributed: "#71717a"
|
|
2633
|
+
},
|
|
2634
|
+
edges: {
|
|
2635
|
+
request: "#18181b",
|
|
2636
|
+
response: "#71717a",
|
|
2637
|
+
event: "#f43f5e",
|
|
2638
|
+
dependency: "#a1a1aa"
|
|
2639
|
+
},
|
|
2640
|
+
fonts: {
|
|
2641
|
+
title: '"Caveat", "Comic Neue", "Chalkboard SE", "Patrick Hand", "Comic Sans MS", cursive, sans-serif',
|
|
2642
|
+
nodeName: 'ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, sans-serif',
|
|
2643
|
+
mono: '"JetBrains Mono", "SF Mono", ui-monospace, Menlo, monospace'
|
|
2644
|
+
},
|
|
2645
|
+
radiusMd: 12,
|
|
2646
|
+
spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 40 },
|
|
2647
|
+
series: ["#f43f5e", "#2563eb", "#10b981", "#8b5cf6", "#f59e0b"]
|
|
2443
2648
|
}
|
|
2444
2649
|
};
|
|
2445
2650
|
function resolveTheme(name) {
|
|
@@ -2457,7 +2662,7 @@ var ParseError = class extends Error {
|
|
|
2457
2662
|
this.column = column;
|
|
2458
2663
|
}
|
|
2459
2664
|
};
|
|
2460
|
-
var FLOW_OP_RE = /(
|
|
2665
|
+
var FLOW_OP_RE = /(->|<-|~>|--|\.\.>|<->)/;
|
|
2461
2666
|
function stripComment(line) {
|
|
2462
2667
|
let inString = false;
|
|
2463
2668
|
let escaped = false;
|
|
@@ -2509,7 +2714,7 @@ function parseProps(raw) {
|
|
|
2509
2714
|
i = raw.length - parsed.rest.length;
|
|
2510
2715
|
continue;
|
|
2511
2716
|
}
|
|
2512
|
-
const keyMatch = raw.slice(i).match(/^(
|
|
2717
|
+
const keyMatch = raw.slice(i).match(/^(@?[\w.-]+)=/);
|
|
2513
2718
|
if (!keyMatch) {
|
|
2514
2719
|
i++;
|
|
2515
2720
|
continue;
|
|
@@ -2627,6 +2832,14 @@ function tokenizeFlowChain(line) {
|
|
|
2627
2832
|
current += ch;
|
|
2628
2833
|
continue;
|
|
2629
2834
|
}
|
|
2835
|
+
const op3 = line.slice(i, i + 3);
|
|
2836
|
+
if (op3 === "..>" || op3 === "<->") {
|
|
2837
|
+
if (current.trim()) parts.push(current.trim());
|
|
2838
|
+
parts.push(op3);
|
|
2839
|
+
current = "";
|
|
2840
|
+
i += 2;
|
|
2841
|
+
continue;
|
|
2842
|
+
}
|
|
2630
2843
|
const op = line.slice(i, i + 2);
|
|
2631
2844
|
if (op === "->" || op === "<-" || op === "~>" || op === "--") {
|
|
2632
2845
|
if (current.trim()) parts.push(current.trim());
|
|
@@ -2650,11 +2863,27 @@ function parseFlowChain(line, lineNo) {
|
|
|
2650
2863
|
let from = splitTargetLabel(parts[i++], lineNo).node;
|
|
2651
2864
|
while (i < parts.length) {
|
|
2652
2865
|
const opToken = parts[i++];
|
|
2653
|
-
const op = EDGE_OPERATORS[opToken];
|
|
2654
|
-
if (!op) throw new ParseError(`unknown flow operator '${opToken}'`, lineNo);
|
|
2655
2866
|
if (i >= parts.length) throw new ParseError(`expected target after '${opToken}'`, lineNo);
|
|
2656
2867
|
const { node: to, label } = splitTargetLabel(parts[i++], lineNo);
|
|
2657
2868
|
if (!to) throw new ParseError(`expected target node after '${opToken}'`, lineNo);
|
|
2869
|
+
if (opToken === "<->") {
|
|
2870
|
+
segments.push({
|
|
2871
|
+
from,
|
|
2872
|
+
op: "request",
|
|
2873
|
+
to,
|
|
2874
|
+
label
|
|
2875
|
+
});
|
|
2876
|
+
segments.push({
|
|
2877
|
+
from: to,
|
|
2878
|
+
op: "response",
|
|
2879
|
+
to: from,
|
|
2880
|
+
label
|
|
2881
|
+
});
|
|
2882
|
+
from = to;
|
|
2883
|
+
continue;
|
|
2884
|
+
}
|
|
2885
|
+
const op = EDGE_OPERATORS[opToken];
|
|
2886
|
+
if (!op) throw new ParseError(`unknown flow operator '${opToken}'`, lineNo);
|
|
2658
2887
|
segments.push({
|
|
2659
2888
|
from: op === "response" ? to : from,
|
|
2660
2889
|
op,
|
|
@@ -3005,7 +3234,7 @@ function validateReferences(ast) {
|
|
|
3005
3234
|
}
|
|
3006
3235
|
}
|
|
3007
3236
|
for (const node of Object.values(ast.nodes)) {
|
|
3008
|
-
if (node.style && !ast.styles[node.style]) {
|
|
3237
|
+
if (typeof node.style === "string" && !ast.styles[node.style]) {
|
|
3009
3238
|
pushWarning(ast.diagnostics, seen, node.line, `node '${node.id}' references unknown style '${node.style}'`);
|
|
3010
3239
|
}
|
|
3011
3240
|
}
|
|
@@ -3109,6 +3338,7 @@ function parse(source, opts = {}) {
|
|
|
3109
3338
|
height: 720,
|
|
3110
3339
|
fps: 60,
|
|
3111
3340
|
theme: "paper",
|
|
3341
|
+
explicitTheme: false,
|
|
3112
3342
|
direction: "LR"
|
|
3113
3343
|
};
|
|
3114
3344
|
const styles = {};
|
|
@@ -3207,6 +3437,7 @@ function parse(source, opts = {}) {
|
|
|
3207
3437
|
const str = parseStringToken(rest2);
|
|
3208
3438
|
if (str) {
|
|
3209
3439
|
title = str.value;
|
|
3440
|
+
meta.title = title;
|
|
3210
3441
|
remainder = str.rest;
|
|
3211
3442
|
}
|
|
3212
3443
|
const inlineLayout = remainder.match(/\blayout\s+(LR|RL|TB|BT)\b/i);
|
|
@@ -3229,8 +3460,11 @@ function parse(source, opts = {}) {
|
|
|
3229
3460
|
} else if (k === "fps") {
|
|
3230
3461
|
meta.fps = Number(v);
|
|
3231
3462
|
} else if (k === "duration") meta.duration = Number(v);
|
|
3232
|
-
else if (k === "theme")
|
|
3233
|
-
|
|
3463
|
+
else if (k === "theme") {
|
|
3464
|
+
const val = String(v).toLowerCase();
|
|
3465
|
+
meta.theme = val;
|
|
3466
|
+
meta.explicitTheme = val !== "auto";
|
|
3467
|
+
} else if (k === "direction" || k === "layout") meta.direction = String(v).toUpperCase();
|
|
3234
3468
|
else if (PLAYER_FLAT_KEY_SET.has(k)) {
|
|
3235
3469
|
const error = applyPlayerSetting(meta.player ??= {}, "player", k, String(v));
|
|
3236
3470
|
if (error) diagnostics.push({ severity: "warning", message: error, line: lineNo });
|
|
@@ -3249,6 +3483,16 @@ function parse(source, opts = {}) {
|
|
|
3249
3483
|
i++;
|
|
3250
3484
|
continue;
|
|
3251
3485
|
}
|
|
3486
|
+
if (/^theme\s*[:=]?\s*([a-zA-Z0-9_-]+)/i.test(line)) {
|
|
3487
|
+
const match = line.match(/^theme\s*[:=]?\s*([a-zA-Z0-9_-]+)/i);
|
|
3488
|
+
if (match) {
|
|
3489
|
+
const val = match[1].toLowerCase();
|
|
3490
|
+
meta.theme = val;
|
|
3491
|
+
meta.explicitTheme = val !== "auto";
|
|
3492
|
+
}
|
|
3493
|
+
i++;
|
|
3494
|
+
continue;
|
|
3495
|
+
}
|
|
3252
3496
|
const directiveMatch = line.match(PLAYER_DIRECTIVE_RE);
|
|
3253
3497
|
if (directiveMatch) {
|
|
3254
3498
|
const error = applyPlayerSetting(meta.player ??= {}, "player", directiveMatch[1], directiveMatch[2] ?? "");
|
|
@@ -3590,7 +3834,13 @@ function matchNode(node, selector) {
|
|
|
3590
3834
|
if (selector.kindEquals && node.kind.toLowerCase() !== selector.kindEquals.toLowerCase()) return false;
|
|
3591
3835
|
if (selector.roleEquals) {
|
|
3592
3836
|
const role = nodeRole(node.kind);
|
|
3593
|
-
|
|
3837
|
+
const targetRole = selector.roleEquals.toLowerCase();
|
|
3838
|
+
if (targetRole === "gateway") {
|
|
3839
|
+
const isGateway = role === "network" || ["gateway", "api_gateway", "reverse_proxy", "proxy", "router"].includes(node.kind.toLowerCase());
|
|
3840
|
+
if (!isGateway) return false;
|
|
3841
|
+
} else if (role.toLowerCase() !== targetRole) {
|
|
3842
|
+
return false;
|
|
3843
|
+
}
|
|
3594
3844
|
}
|
|
3595
3845
|
if (selector.labelContains) {
|
|
3596
3846
|
const target = (node.label || node.id).toLowerCase();
|
|
@@ -3751,6 +4001,30 @@ var ARCH_RULE_PRESETS = {
|
|
|
3751
4001
|
from: { roleEquals: "security" }
|
|
3752
4002
|
}
|
|
3753
4003
|
]
|
|
4004
|
+
},
|
|
4005
|
+
deploymentOwnership: {
|
|
4006
|
+
id: "deployment-ownership",
|
|
4007
|
+
name: "Deployment Ownership & Regional Governance",
|
|
4008
|
+
description: "Ensure production stateful stores and services are guarded in explicit group perimeters",
|
|
4009
|
+
rules: [
|
|
4010
|
+
{
|
|
4011
|
+
id: "no-unprotected-public-database",
|
|
4012
|
+
name: "No Public Database Exposure",
|
|
4013
|
+
description: "Databases and stateful stores must not be directly accessed from public browser/mobile clients.",
|
|
4014
|
+
severity: "error",
|
|
4015
|
+
type: "cannot-connect",
|
|
4016
|
+
from: { roleEquals: "client" },
|
|
4017
|
+
to: { roleEquals: "data" }
|
|
4018
|
+
},
|
|
4019
|
+
{
|
|
4020
|
+
id: "no-cross-region-sync-bypasses",
|
|
4021
|
+
name: "Synchronous Request Cycle Prevention",
|
|
4022
|
+
description: "Synchronous requests must not form cycles across microservices.",
|
|
4023
|
+
severity: "error",
|
|
4024
|
+
type: "forbidden-cycle",
|
|
4025
|
+
edge: { kind: "request" }
|
|
4026
|
+
}
|
|
4027
|
+
]
|
|
3754
4028
|
}
|
|
3755
4029
|
};
|
|
3756
4030
|
function validateArchitecture(ast, rules = [
|
|
@@ -3758,7 +4032,7 @@ function validateArchitecture(ast, rules = [
|
|
|
3758
4032
|
...ARCH_RULE_PRESETS.microservicesGovernance.rules
|
|
3759
4033
|
]) {
|
|
3760
4034
|
const violations = [];
|
|
3761
|
-
const nodes = Object.values(ast
|
|
4035
|
+
const nodes = Object.values(ast?.nodes || {});
|
|
3762
4036
|
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
|
|
3763
4037
|
const edges = extractAllEdges(ast);
|
|
3764
4038
|
for (const rule of rules) {
|
|
@@ -3805,6 +4079,11 @@ function validateArchitecture(ast, rules = [
|
|
|
3805
4079
|
break;
|
|
3806
4080
|
}
|
|
3807
4081
|
case "forbidden-cycle": {
|
|
4082
|
+
const dtype = ast.meta?.type || ast.config?.type || "architecture";
|
|
4083
|
+
const nonServiceArchetypes = ["state", "sequence", "layers", "flywheel", "loop", "venn"];
|
|
4084
|
+
if (nonServiceArchetypes.includes(dtype)) {
|
|
4085
|
+
break;
|
|
4086
|
+
}
|
|
3808
4087
|
const cycleInfo = detectCycleInGraph(nodes, edges, rule.edge);
|
|
3809
4088
|
if (cycleInfo) {
|
|
3810
4089
|
violations.push({
|
|
@@ -4055,26 +4334,66 @@ function classifyTechnology(id, label = "") {
|
|
|
4055
4334
|
}
|
|
4056
4335
|
|
|
4057
4336
|
// src/diff.ts
|
|
4337
|
+
function extractDiffEdges(ast) {
|
|
4338
|
+
const edges = [];
|
|
4339
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4340
|
+
const add = (from, to, kind, label) => {
|
|
4341
|
+
const key = `${from}->${to}:${kind}`;
|
|
4342
|
+
if (!seen.has(key)) {
|
|
4343
|
+
seen.add(key);
|
|
4344
|
+
edges.push({ from, to, kind, label });
|
|
4345
|
+
}
|
|
4346
|
+
};
|
|
4347
|
+
for (const edge of ast.edges || []) {
|
|
4348
|
+
add(edge.from, edge.to, edge.kind, edge.label);
|
|
4349
|
+
}
|
|
4350
|
+
for (const beat of ast.beats || []) {
|
|
4351
|
+
for (const cue of beat.cues || []) {
|
|
4352
|
+
if (cue.kind === "flow") {
|
|
4353
|
+
for (const seg of cue.segments) {
|
|
4354
|
+
add(seg.from, seg.to, seg.op, seg.label);
|
|
4355
|
+
}
|
|
4356
|
+
} else if (cue.kind === "parallel") {
|
|
4357
|
+
for (const child of cue.cues) {
|
|
4358
|
+
if (child.kind === "flow") {
|
|
4359
|
+
for (const seg of child.segments) {
|
|
4360
|
+
add(seg.from, seg.to, seg.op, seg.label);
|
|
4361
|
+
}
|
|
4362
|
+
}
|
|
4363
|
+
}
|
|
4364
|
+
}
|
|
4365
|
+
}
|
|
4366
|
+
}
|
|
4367
|
+
return edges;
|
|
4368
|
+
}
|
|
4058
4369
|
function diffDiagramASTs(beforeAST, afterAST) {
|
|
4059
4370
|
const nodeDiffs = [];
|
|
4060
4371
|
const edgeDiffs = [];
|
|
4061
|
-
const
|
|
4062
|
-
const
|
|
4372
|
+
const groupDiffs = [];
|
|
4373
|
+
const beforeNodes = beforeAST.nodes || {};
|
|
4374
|
+
const afterNodes = afterAST.nodes || {};
|
|
4375
|
+
const beforeNodeIds = new Set(Object.keys(beforeNodes));
|
|
4376
|
+
const afterNodeIds = new Set(Object.keys(afterNodes));
|
|
4063
4377
|
let addedNodesCount = 0;
|
|
4064
4378
|
let removedNodesCount = 0;
|
|
4065
4379
|
let modifiedNodesCount = 0;
|
|
4066
|
-
for (const [id, afterNode] of Object.entries(
|
|
4380
|
+
for (const [id, afterNode] of Object.entries(afterNodes)) {
|
|
4067
4381
|
if (!beforeNodeIds.has(id)) {
|
|
4068
|
-
nodeDiffs.push({ id, status: "added", after: afterNode, changes: ["Newly
|
|
4382
|
+
nodeDiffs.push({ id, status: "added", after: afterNode, changes: ["Newly provisioned node"] });
|
|
4069
4383
|
addedNodesCount++;
|
|
4070
4384
|
} else {
|
|
4071
|
-
const beforeNode =
|
|
4385
|
+
const beforeNode = beforeNodes[id];
|
|
4072
4386
|
const changes = [];
|
|
4073
4387
|
if (beforeNode.kind !== afterNode.kind) {
|
|
4074
|
-
changes.push(`Kind
|
|
4388
|
+
changes.push(`Kind: ${beforeNode.kind} \u2192 ${afterNode.kind}`);
|
|
4075
4389
|
}
|
|
4076
4390
|
if (beforeNode.label !== afterNode.label) {
|
|
4077
|
-
changes.push(`Label
|
|
4391
|
+
changes.push(`Label: "${beforeNode.label}" \u2192 "${afterNode.label}"`);
|
|
4392
|
+
}
|
|
4393
|
+
const beforeSrc = beforeNode.props?.["@src"] || beforeNode.props?.["src"];
|
|
4394
|
+
const afterSrc = afterNode.props?.["@src"] || afterNode.props?.["src"];
|
|
4395
|
+
if (beforeSrc !== afterSrc) {
|
|
4396
|
+
changes.push(`Code Provenance: ${beforeSrc || "none"} \u2192 ${afterSrc || "none"}`);
|
|
4078
4397
|
}
|
|
4079
4398
|
if (changes.length > 0) {
|
|
4080
4399
|
nodeDiffs.push({ id, status: "modified", before: beforeNode, after: afterNode, changes });
|
|
@@ -4084,84 +4403,137 @@ function diffDiagramASTs(beforeAST, afterAST) {
|
|
|
4084
4403
|
}
|
|
4085
4404
|
}
|
|
4086
4405
|
}
|
|
4087
|
-
for (const [id, beforeNode] of Object.entries(
|
|
4406
|
+
for (const [id, beforeNode] of Object.entries(beforeNodes)) {
|
|
4088
4407
|
if (!afterNodeIds.has(id)) {
|
|
4089
|
-
nodeDiffs.push({ id, status: "removed", before: beforeNode, changes: ["
|
|
4408
|
+
nodeDiffs.push({ id, status: "removed", before: beforeNode, changes: ["Decommissioned node"] });
|
|
4090
4409
|
removedNodesCount++;
|
|
4091
4410
|
}
|
|
4092
4411
|
}
|
|
4093
4412
|
const edgeKey = (e) => `${e.from}->${e.to}:${e.kind}`;
|
|
4094
|
-
const
|
|
4095
|
-
const
|
|
4096
|
-
|
|
4413
|
+
const beforeEdges = extractDiffEdges(beforeAST);
|
|
4414
|
+
const afterEdges = extractDiffEdges(afterAST);
|
|
4415
|
+
const beforeEdgeMap = new Map(beforeEdges.map((e) => [edgeKey(e), e]));
|
|
4416
|
+
const afterEdgeMap = new Map(afterEdges.map((e) => [edgeKey(e), e]));
|
|
4417
|
+
let addedEdgesCount = 0;
|
|
4418
|
+
let removedEdgesCount = 0;
|
|
4419
|
+
for (const [key, afterEdge] of afterEdgeMap.entries()) {
|
|
4097
4420
|
if (!beforeEdgeMap.has(key)) {
|
|
4098
|
-
edgeDiffs.push({ key, status: "added", after: afterEdge });
|
|
4421
|
+
edgeDiffs.push({ key, status: "added", after: afterEdge, changes: ["New interaction route"] });
|
|
4422
|
+
addedEdgesCount++;
|
|
4099
4423
|
} else {
|
|
4100
4424
|
const beforeEdge = beforeEdgeMap.get(key);
|
|
4425
|
+
const changes = [];
|
|
4101
4426
|
if (beforeEdge.label !== afterEdge.label) {
|
|
4102
|
-
|
|
4427
|
+
changes.push(`Protocol/Label: "${beforeEdge.label || ""}" \u2192 "${afterEdge.label || ""}"`);
|
|
4428
|
+
}
|
|
4429
|
+
if (changes.length > 0) {
|
|
4430
|
+
edgeDiffs.push({ key, status: "modified", before: beforeEdge, after: afterEdge, changes });
|
|
4103
4431
|
} else {
|
|
4104
|
-
edgeDiffs.push({ key, status: "unchanged", before: beforeEdge, after: afterEdge });
|
|
4432
|
+
edgeDiffs.push({ key, status: "unchanged", before: beforeEdge, after: afterEdge, changes: [] });
|
|
4105
4433
|
}
|
|
4106
4434
|
}
|
|
4107
4435
|
}
|
|
4108
|
-
for (const [key, beforeEdge] of beforeEdgeMap) {
|
|
4436
|
+
for (const [key, beforeEdge] of beforeEdgeMap.entries()) {
|
|
4109
4437
|
if (!afterEdgeMap.has(key)) {
|
|
4110
|
-
edgeDiffs.push({ key, status: "removed", before: beforeEdge });
|
|
4438
|
+
edgeDiffs.push({ key, status: "removed", before: beforeEdge, changes: ["Decommissioned route"] });
|
|
4439
|
+
removedEdgesCount++;
|
|
4111
4440
|
}
|
|
4112
4441
|
}
|
|
4113
|
-
const
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4442
|
+
const beforeGroups = beforeAST.groups || {};
|
|
4443
|
+
const afterGroups = afterAST.groups || {};
|
|
4444
|
+
const beforeGroupIds = new Set(Object.keys(beforeGroups));
|
|
4445
|
+
const afterGroupIds = new Set(Object.keys(afterGroups));
|
|
4446
|
+
for (const [id, afterGroup] of Object.entries(afterGroups)) {
|
|
4447
|
+
if (!beforeGroupIds.has(id)) {
|
|
4448
|
+
groupDiffs.push({ id, status: "added", after: afterGroup, changes: ["New security / subsystem boundary"] });
|
|
4449
|
+
} else {
|
|
4450
|
+
const beforeGroup = beforeGroups[id];
|
|
4451
|
+
const beforeMembers = new Set(beforeGroup.members || []);
|
|
4452
|
+
const afterMembers = new Set(afterGroup.members || []);
|
|
4453
|
+
const diffMembers = (afterGroup.members || []).filter((m) => !beforeMembers.has(m));
|
|
4454
|
+
if (diffMembers.length > 0 || beforeGroup.members.length !== afterGroup.members.length) {
|
|
4455
|
+
groupDiffs.push({
|
|
4456
|
+
id,
|
|
4457
|
+
status: "modified",
|
|
4458
|
+
before: beforeGroup,
|
|
4459
|
+
after: afterGroup,
|
|
4460
|
+
changes: [`Boundary membership updated: [${(afterGroup.members || []).join(", ")}]`]
|
|
4461
|
+
});
|
|
4462
|
+
}
|
|
4127
4463
|
}
|
|
4128
|
-
summaryLines.push("");
|
|
4129
4464
|
}
|
|
4130
|
-
const
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
];
|
|
4135
|
-
for (const [id, node] of Object.entries({ ...beforeAST.nodes, ...afterAST.nodes })) {
|
|
4136
|
-
evolutionLines.push(`${node.kind} ${id} "${node.label}"`);
|
|
4137
|
-
}
|
|
4138
|
-
evolutionLines.push("");
|
|
4139
|
-
evolutionLines.push('beat v1 "Baseline Architecture":');
|
|
4140
|
-
const v1NodeIds = Object.keys(beforeAST.nodes).join(" ");
|
|
4141
|
-
if (v1NodeIds) {
|
|
4142
|
-
evolutionLines.push(` show ${v1NodeIds}`);
|
|
4143
|
-
}
|
|
4144
|
-
evolutionLines.push("");
|
|
4145
|
-
evolutionLines.push('beat transition "Migrate to Target Architecture":');
|
|
4146
|
-
const addedIds = nodeDiffs.filter((n) => n.status === "added").map((n) => n.id);
|
|
4147
|
-
const removedIds = nodeDiffs.filter((n) => n.status === "removed").map((n) => n.id);
|
|
4148
|
-
if (removedIds.length > 0) {
|
|
4149
|
-
evolutionLines.push(` hide ${removedIds.join(" ")}`);
|
|
4150
|
-
}
|
|
4151
|
-
if (addedIds.length > 0) {
|
|
4152
|
-
evolutionLines.push(` show ${addedIds.join(" ")}`);
|
|
4153
|
-
evolutionLines.push(` glow ${addedIds.join(" ")} color="#10b981"`);
|
|
4465
|
+
for (const [id, beforeGroup] of Object.entries(beforeGroups)) {
|
|
4466
|
+
if (!afterGroupIds.has(id)) {
|
|
4467
|
+
groupDiffs.push({ id, status: "removed", before: beforeGroup, changes: ["Dissolved boundary"] });
|
|
4468
|
+
}
|
|
4154
4469
|
}
|
|
4470
|
+
const evolutionMarkdyScript = generateEvolutionMarkdyScript(
|
|
4471
|
+
beforeAST,
|
|
4472
|
+
afterAST,
|
|
4473
|
+
nodeDiffs,
|
|
4474
|
+
edgeDiffs
|
|
4475
|
+
);
|
|
4476
|
+
const summaryMarkdown = [
|
|
4477
|
+
`# Markdy Architectural Diff Summary`,
|
|
4478
|
+
`- **Nodes Added**: ${addedNodesCount}`,
|
|
4479
|
+
`- **Nodes Removed**: ${removedNodesCount}`,
|
|
4480
|
+
`- **Nodes Modified**: ${modifiedNodesCount}`,
|
|
4481
|
+
`- **Routes Added**: ${addedEdgesCount}`,
|
|
4482
|
+
`- **Routes Removed**: ${removedEdgesCount}`,
|
|
4483
|
+
"",
|
|
4484
|
+
`### Component Delta`,
|
|
4485
|
+
`| Component | Status | Details |`,
|
|
4486
|
+
`| :--- | :--- | :--- |`,
|
|
4487
|
+
...nodeDiffs.filter((n) => n.status !== "unchanged").map((n) => `| \`${n.id}\` | **${n.status.toUpperCase()}** | ${n.changes.join("; ")} |`),
|
|
4488
|
+
"",
|
|
4489
|
+
`### Connection Delta`,
|
|
4490
|
+
`| Connection | Status | Details |`,
|
|
4491
|
+
`| :--- | :--- | :--- |`,
|
|
4492
|
+
...edgeDiffs.filter((e) => e.status !== "unchanged").map((e) => `| \`${e.key}\` | **${e.status.toUpperCase()}** | ${e.changes.join("; ")} |`)
|
|
4493
|
+
].join("\n");
|
|
4155
4494
|
return {
|
|
4156
4495
|
nodes: nodeDiffs,
|
|
4157
4496
|
edges: edgeDiffs,
|
|
4497
|
+
groups: groupDiffs,
|
|
4158
4498
|
addedNodesCount,
|
|
4159
4499
|
removedNodesCount,
|
|
4160
4500
|
modifiedNodesCount,
|
|
4161
|
-
|
|
4162
|
-
|
|
4501
|
+
addedEdgesCount,
|
|
4502
|
+
removedEdgesCount,
|
|
4503
|
+
summaryMarkdown,
|
|
4504
|
+
evolutionMarkdyScript
|
|
4163
4505
|
};
|
|
4164
4506
|
}
|
|
4507
|
+
function generateEvolutionMarkdyScript(beforeAST, afterAST, nodeDiffs, edgeDiffs) {
|
|
4508
|
+
const lines = [`scene theme=paper`, `layout ${afterAST.meta?.direction || "LR"}`, ""];
|
|
4509
|
+
const allNodes = /* @__PURE__ */ new Map();
|
|
4510
|
+
for (const [id, n] of Object.entries(beforeAST.nodes || {})) allNodes.set(id, n);
|
|
4511
|
+
for (const [id, n] of Object.entries(afterAST.nodes || {})) allNodes.set(id, n);
|
|
4512
|
+
for (const [id, node] of allNodes.entries()) {
|
|
4513
|
+
const propsStr = Object.entries(node.props || {}).map(([k, v]) => `${k}=${typeof v === "string" ? `"${v}"` : v}`).join(" ");
|
|
4514
|
+
lines.push(`${node.kind} ${id} "${node.label}" ${propsStr}`.trim());
|
|
4515
|
+
}
|
|
4516
|
+
lines.push("");
|
|
4517
|
+
const beforeNodeIds = Object.keys(beforeAST.nodes || {}).join(" ");
|
|
4518
|
+
lines.push(`beat baseline:`);
|
|
4519
|
+
lines.push(` show ${beforeNodeIds}`);
|
|
4520
|
+
for (const edge of beforeAST.edges || []) {
|
|
4521
|
+
lines.push(` ${edge.from} -> ${edge.to} "${edge.label || ""}"`);
|
|
4522
|
+
}
|
|
4523
|
+
const removedNodeIds = nodeDiffs.filter((n) => n.status === "removed").map((n) => n.id);
|
|
4524
|
+
const addedNodeIds = nodeDiffs.filter((n) => n.status === "added").map((n) => n.id);
|
|
4525
|
+
lines.push("");
|
|
4526
|
+
lines.push(`beat transition:`);
|
|
4527
|
+
if (removedNodeIds.length > 0) {
|
|
4528
|
+
lines.push(` glow ${removedNodeIds.join(" ")} color=#fb7185 strength=1.2`);
|
|
4529
|
+
lines.push(` hide ${removedNodeIds.join(" ")} dur=800ms`);
|
|
4530
|
+
}
|
|
4531
|
+
if (addedNodeIds.length > 0) {
|
|
4532
|
+
lines.push(` show ${addedNodeIds.join(" ")} stagger=80ms`);
|
|
4533
|
+
lines.push(` glow ${addedNodeIds.join(" ")} color=#34d399 strength=1.5`);
|
|
4534
|
+
}
|
|
4535
|
+
return lines.join("\n");
|
|
4536
|
+
}
|
|
4165
4537
|
|
|
4166
4538
|
// src/url-codec.ts
|
|
4167
4539
|
var PREFIX = "~m";
|
|
@@ -4253,16 +4625,27 @@ async function decompressMarkdyFromUrlHash(hash) {
|
|
|
4253
4625
|
}
|
|
4254
4626
|
|
|
4255
4627
|
// src/router.ts
|
|
4256
|
-
function getBoxPortPosition(box, port) {
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4628
|
+
function getBoxPortPosition(box, port, lane) {
|
|
4629
|
+
if (port === "left" || port === "right") {
|
|
4630
|
+
const x = port === "left" ? box.x : box.x + box.width;
|
|
4631
|
+
if (lane && lane.total > 1) {
|
|
4632
|
+
const padding = Math.min(16, box.height * 0.18);
|
|
4633
|
+
const span = box.height - padding * 2;
|
|
4634
|
+
const step = span / (lane.total - 1 || 1);
|
|
4635
|
+
const y = box.y + padding + lane.index * step;
|
|
4636
|
+
return { x, y };
|
|
4637
|
+
}
|
|
4638
|
+
return { x, y: box.y + box.height / 2 };
|
|
4639
|
+
} else {
|
|
4640
|
+
const y = port === "top" ? box.y : box.y + box.height;
|
|
4641
|
+
if (lane && lane.total > 1) {
|
|
4642
|
+
const padding = Math.min(16, box.width * 0.18);
|
|
4643
|
+
const span = box.width - padding * 2;
|
|
4644
|
+
const step = span / (lane.total - 1 || 1);
|
|
4645
|
+
const x = box.x + padding + lane.index * step;
|
|
4646
|
+
return { x, y };
|
|
4647
|
+
}
|
|
4648
|
+
return { x: box.x + box.width / 2, y };
|
|
4266
4649
|
}
|
|
4267
4650
|
}
|
|
4268
4651
|
function selectOptimalPorts(sourceBox, targetBox) {
|
|
@@ -4276,12 +4659,96 @@ function selectOptimalPorts(sourceBox, targetBox) {
|
|
|
4276
4659
|
return dy > 0 ? { sourcePort: "bottom", targetPort: "top" } : { sourcePort: "top", targetPort: "bottom" };
|
|
4277
4660
|
}
|
|
4278
4661
|
}
|
|
4279
|
-
function
|
|
4280
|
-
const
|
|
4281
|
-
|
|
4282
|
-
|
|
4662
|
+
function buildSmoothSvgPath(start, waypoints, end, cornerRadius = 0) {
|
|
4663
|
+
const allPoints = [start, ...waypoints, end];
|
|
4664
|
+
if (allPoints.length <= 2 || cornerRadius <= 0) {
|
|
4665
|
+
let d2 = `M ${start.x} ${start.y}`;
|
|
4666
|
+
for (const p of waypoints) {
|
|
4667
|
+
d2 += ` L ${p.x} ${p.y}`;
|
|
4668
|
+
}
|
|
4669
|
+
d2 += ` L ${end.x} ${end.y}`;
|
|
4670
|
+
return d2;
|
|
4671
|
+
}
|
|
4672
|
+
let d = `M ${start.x} ${start.y}`;
|
|
4673
|
+
for (let i = 1; i < allPoints.length - 1; i++) {
|
|
4674
|
+
const prev = allPoints[i - 1];
|
|
4675
|
+
const curr = allPoints[i];
|
|
4676
|
+
const next = allPoints[i + 1];
|
|
4677
|
+
const dPrev = Math.hypot(curr.x - prev.x, curr.y - prev.y);
|
|
4678
|
+
const dNext = Math.hypot(next.x - curr.x, next.y - curr.y);
|
|
4679
|
+
const r = Math.min(cornerRadius, dPrev / 2, dNext / 2);
|
|
4680
|
+
if (r < 2) {
|
|
4681
|
+
d += ` L ${curr.x} ${curr.y}`;
|
|
4682
|
+
continue;
|
|
4683
|
+
}
|
|
4684
|
+
const startX = curr.x + (prev.x - curr.x) * (r / dPrev);
|
|
4685
|
+
const startY = curr.y + (prev.y - curr.y) * (r / dPrev);
|
|
4686
|
+
const endX = curr.x + (next.x - curr.x) * (r / dNext);
|
|
4687
|
+
const endY = curr.y + (next.y - curr.y) * (r / dNext);
|
|
4688
|
+
d += ` L ${startX} ${startY}`;
|
|
4689
|
+
d += ` Q ${curr.x} ${curr.y} ${endX} ${endY}`;
|
|
4690
|
+
}
|
|
4691
|
+
d += ` L ${end.x} ${end.y}`;
|
|
4692
|
+
return d;
|
|
4693
|
+
}
|
|
4694
|
+
function routeOrthogonalEdge(sourceBox, targetBox, options = {}) {
|
|
4695
|
+
const isSelfLoop = sourceBox === targetBox || sourceBox.x === targetBox.x && sourceBox.y === targetBox.y && sourceBox.width === targetBox.width && sourceBox.height === targetBox.height;
|
|
4696
|
+
const MARGIN = options.margin ?? 20;
|
|
4697
|
+
if (isSelfLoop) {
|
|
4698
|
+
const loopSpan = Math.max(28, MARGIN * 1.5);
|
|
4699
|
+
const port = options.sourcePort || options.targetPort || "top";
|
|
4700
|
+
let start2;
|
|
4701
|
+
let end2;
|
|
4702
|
+
let waypoints2;
|
|
4703
|
+
if (port === "top") {
|
|
4704
|
+
const startX = sourceBox.x + sourceBox.width * 0.35;
|
|
4705
|
+
const endX = sourceBox.x + sourceBox.width * 0.65;
|
|
4706
|
+
const topY = sourceBox.y;
|
|
4707
|
+
const apexY = topY - loopSpan;
|
|
4708
|
+
start2 = { x: startX, y: topY };
|
|
4709
|
+
end2 = { x: endX, y: topY };
|
|
4710
|
+
waypoints2 = [{ x: startX, y: apexY }, { x: endX, y: apexY }];
|
|
4711
|
+
} else if (port === "bottom") {
|
|
4712
|
+
const startX = sourceBox.x + sourceBox.width * 0.35;
|
|
4713
|
+
const endX = sourceBox.x + sourceBox.width * 0.65;
|
|
4714
|
+
const botY = sourceBox.y + sourceBox.height;
|
|
4715
|
+
const apexY = botY + loopSpan;
|
|
4716
|
+
start2 = { x: startX, y: botY };
|
|
4717
|
+
end2 = { x: endX, y: botY };
|
|
4718
|
+
waypoints2 = [{ x: startX, y: apexY }, { x: endX, y: apexY }];
|
|
4719
|
+
} else if (port === "left") {
|
|
4720
|
+
const startY = sourceBox.y + sourceBox.height * 0.35;
|
|
4721
|
+
const endY = sourceBox.y + sourceBox.height * 0.65;
|
|
4722
|
+
const leftX = sourceBox.x;
|
|
4723
|
+
const apexX = leftX - loopSpan;
|
|
4724
|
+
start2 = { x: leftX, y: startY };
|
|
4725
|
+
end2 = { x: leftX, y: endY };
|
|
4726
|
+
waypoints2 = [{ x: apexX, y: startY }, { x: apexX, y: endY }];
|
|
4727
|
+
} else {
|
|
4728
|
+
const startY = sourceBox.y + sourceBox.height * 0.35;
|
|
4729
|
+
const endY = sourceBox.y + sourceBox.height * 0.65;
|
|
4730
|
+
const rightX = sourceBox.x + sourceBox.width;
|
|
4731
|
+
const apexX = rightX + loopSpan;
|
|
4732
|
+
start2 = { x: rightX, y: startY };
|
|
4733
|
+
end2 = { x: rightX, y: endY };
|
|
4734
|
+
waypoints2 = [{ x: apexX, y: startY }, { x: apexX, y: endY }];
|
|
4735
|
+
}
|
|
4736
|
+
const svgPathData2 = buildSmoothSvgPath(start2, waypoints2, end2, options.cornerRadius ?? 6);
|
|
4737
|
+
return {
|
|
4738
|
+
sourcePort: port,
|
|
4739
|
+
targetPort: port,
|
|
4740
|
+
startPoint: start2,
|
|
4741
|
+
endPoint: end2,
|
|
4742
|
+
waypoints: waypoints2,
|
|
4743
|
+
svgPathData: svgPathData2
|
|
4744
|
+
};
|
|
4745
|
+
}
|
|
4746
|
+
const optimal = selectOptimalPorts(sourceBox, targetBox);
|
|
4747
|
+
const sourcePort = options.sourcePort || optimal.sourcePort;
|
|
4748
|
+
const targetPort = options.targetPort || optimal.targetPort;
|
|
4749
|
+
const start = getBoxPortPosition(sourceBox, sourcePort, options.sourceLane);
|
|
4750
|
+
const end = getBoxPortPosition(targetBox, targetPort, options.targetLane);
|
|
4283
4751
|
const waypoints = [];
|
|
4284
|
-
const MARGIN = 20;
|
|
4285
4752
|
if (sourcePort === "right" && targetPort === "left") {
|
|
4286
4753
|
if (start.x <= end.x - MARGIN * 2) {
|
|
4287
4754
|
const midX = (start.x + end.x) / 2;
|
|
@@ -4348,11 +4815,7 @@ function routeOrthogonalEdge(sourceBox, targetBox) {
|
|
|
4348
4815
|
}
|
|
4349
4816
|
waypoints.push(p2);
|
|
4350
4817
|
}
|
|
4351
|
-
|
|
4352
|
-
for (const wp of waypoints) {
|
|
4353
|
-
svgPathData += ` L ${wp.x} ${wp.y}`;
|
|
4354
|
-
}
|
|
4355
|
-
svgPathData += ` L ${end.x} ${end.y}`;
|
|
4818
|
+
const svgPathData = buildSmoothSvgPath(start, waypoints, end, options.cornerRadius ?? 6);
|
|
4356
4819
|
return {
|
|
4357
4820
|
sourcePort,
|
|
4358
4821
|
targetPort,
|
|
@@ -4362,6 +4825,550 @@ function routeOrthogonalEdge(sourceBox, targetBox) {
|
|
|
4362
4825
|
svgPathData
|
|
4363
4826
|
};
|
|
4364
4827
|
}
|
|
4828
|
+
function allocatePortLanes(edges, boxes) {
|
|
4829
|
+
const result = /* @__PURE__ */ new Map();
|
|
4830
|
+
const portGroups = /* @__PURE__ */ new Map();
|
|
4831
|
+
for (const edge of edges) {
|
|
4832
|
+
if (edge.from === edge.to) continue;
|
|
4833
|
+
const sBox = boxes[edge.from];
|
|
4834
|
+
const tBox = boxes[edge.to];
|
|
4835
|
+
if (!sBox || !tBox) continue;
|
|
4836
|
+
const { sourcePort, targetPort } = selectOptimalPorts(sBox, tBox);
|
|
4837
|
+
const sKey = `${edge.from}:${sourcePort}`;
|
|
4838
|
+
const tKey = `${edge.to}:${targetPort}`;
|
|
4839
|
+
const tgtCenter = { x: tBox.x + tBox.width / 2, y: tBox.y + tBox.height / 2 };
|
|
4840
|
+
const srcCenter = { x: sBox.x + sBox.width / 2, y: sBox.y + sBox.height / 2 };
|
|
4841
|
+
if (!portGroups.has(sKey)) portGroups.set(sKey, []);
|
|
4842
|
+
portGroups.get(sKey).push({
|
|
4843
|
+
edge,
|
|
4844
|
+
role: "source",
|
|
4845
|
+
otherCenterY: tgtCenter.y,
|
|
4846
|
+
otherCenterX: tgtCenter.x
|
|
4847
|
+
});
|
|
4848
|
+
if (!portGroups.has(tKey)) portGroups.set(tKey, []);
|
|
4849
|
+
portGroups.get(tKey).push({
|
|
4850
|
+
edge,
|
|
4851
|
+
role: "target",
|
|
4852
|
+
otherCenterY: srcCenter.y,
|
|
4853
|
+
otherCenterX: srcCenter.x
|
|
4854
|
+
});
|
|
4855
|
+
}
|
|
4856
|
+
for (const [key, list] of portGroups.entries()) {
|
|
4857
|
+
if (list.length <= 1) continue;
|
|
4858
|
+
const isVertical = key.endsWith(":left") || key.endsWith(":right");
|
|
4859
|
+
list.sort((a, b) => {
|
|
4860
|
+
if (isVertical) {
|
|
4861
|
+
if (Math.abs(a.otherCenterY - b.otherCenterY) > 1) {
|
|
4862
|
+
return a.otherCenterY - b.otherCenterY;
|
|
4863
|
+
}
|
|
4864
|
+
} else {
|
|
4865
|
+
if (Math.abs(a.otherCenterX - b.otherCenterX) > 1) {
|
|
4866
|
+
return a.otherCenterX - b.otherCenterX;
|
|
4867
|
+
}
|
|
4868
|
+
}
|
|
4869
|
+
const aMin = a.edge.from < a.edge.to ? a.edge.from : a.edge.to;
|
|
4870
|
+
const aMax = a.edge.from < a.edge.to ? a.edge.to : a.edge.from;
|
|
4871
|
+
const bMin = b.edge.from < b.edge.to ? b.edge.from : b.edge.to;
|
|
4872
|
+
const bMax = b.edge.from < b.edge.to ? b.edge.to : b.edge.from;
|
|
4873
|
+
if (aMin !== bMin || aMax !== bMax) {
|
|
4874
|
+
return (a.edge.id || "").localeCompare(b.edge.id || "");
|
|
4875
|
+
}
|
|
4876
|
+
const aDir = a.edge.from < a.edge.to ? 0 : 1;
|
|
4877
|
+
const bDir = b.edge.from < b.edge.to ? 0 : 1;
|
|
4878
|
+
return aDir - bDir;
|
|
4879
|
+
});
|
|
4880
|
+
list.forEach((item, index) => {
|
|
4881
|
+
const existing = result.get(item.edge) || {};
|
|
4882
|
+
if (item.role === "source") {
|
|
4883
|
+
existing.sourceLane = { index, total: list.length };
|
|
4884
|
+
} else {
|
|
4885
|
+
existing.targetLane = { index, total: list.length };
|
|
4886
|
+
}
|
|
4887
|
+
result.set(item.edge, existing);
|
|
4888
|
+
});
|
|
4889
|
+
}
|
|
4890
|
+
return result;
|
|
4891
|
+
}
|
|
4892
|
+
|
|
4893
|
+
// src/symbols.ts
|
|
4894
|
+
var VECTOR_SYMBOLS = {
|
|
4895
|
+
// Cloud & Infrastructure
|
|
4896
|
+
aws: {
|
|
4897
|
+
name: "AWS",
|
|
4898
|
+
category: "cloud",
|
|
4899
|
+
viewBox: "0 0 24 24",
|
|
4900
|
+
svgPaths: `<path fill="currentColor" d="M12 2L2 7l10 5 10-5-10-5zm0 8.5L4.5 7 12 3.25 19.5 7 12 10.5zM2 17l10 5 10-5M2 12l10 5 10-5"/>`,
|
|
4901
|
+
brandColor: "#FF9900"
|
|
4902
|
+
},
|
|
4903
|
+
gcp: {
|
|
4904
|
+
name: "Google Cloud",
|
|
4905
|
+
category: "cloud",
|
|
4906
|
+
viewBox: "0 0 24 24",
|
|
4907
|
+
svgPaths: `<path fill="currentColor" d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM19 18H6c-2.21 0-4-1.79-4-4 0-2.05 1.53-3.76 3.56-3.97l1.07-.11.5-.95C8.08 7.14 9.94 6 12 6c2.62 0 4.88 1.86 5.39 4.43l.3 1.5 1.53.11c1.56.1 2.78 1.41 2.78 2.96 0 1.65-1.35 3-3 3z"/>`,
|
|
4908
|
+
brandColor: "#4285F4"
|
|
4909
|
+
},
|
|
4910
|
+
azure: {
|
|
4911
|
+
name: "Microsoft Azure",
|
|
4912
|
+
category: "cloud",
|
|
4913
|
+
viewBox: "0 0 24 24",
|
|
4914
|
+
svgPaths: `<path fill="currentColor" d="M13.05 4.24l-6.1 11.23L2 17.52l7.73-14.28 3.32 1zm1.09 1.94L18.42 16h-7.8l-1.92 3.76H22l-7.86-13.58z"/>`,
|
|
4915
|
+
brandColor: "#0089D6"
|
|
4916
|
+
},
|
|
4917
|
+
kubernetes: {
|
|
4918
|
+
name: "Kubernetes",
|
|
4919
|
+
category: "compute",
|
|
4920
|
+
viewBox: "0 0 24 24",
|
|
4921
|
+
svgPaths: `<path fill="currentColor" d="M12 2l8.66 5v10L12 22l-8.66-5V7L12 2zm0 2.31L5.34 7.69v7.62L12 18.69l6.66-3.38V7.69L12 4.31zm0 3.69a4 4 0 1 1 0 8 4 4 0 0 1 0-8z"/>`,
|
|
4922
|
+
brandColor: "#326CE5"
|
|
4923
|
+
},
|
|
4924
|
+
docker: {
|
|
4925
|
+
name: "Docker",
|
|
4926
|
+
category: "compute",
|
|
4927
|
+
viewBox: "0 0 24 24",
|
|
4928
|
+
svgPaths: `<path fill="currentColor" d="M13.98 11.08h1.83V9.25h-1.83v1.83zm-2.75 0h1.83V9.25h-1.83v1.83zm-2.75 0h1.83V9.25H8.48v1.83zm-2.75 0h1.83V9.25H5.73v1.83zm8.25-2.75h1.83V6.5h-1.83v1.83zm-2.75 0h1.83V6.5h-1.83v1.83zm-2.75 0h1.83V6.5H8.48v1.83zm8.25 0h1.83V6.5h-1.83v1.83zm1.83 5.5c-.4 0-.8.1-1.1.3-.8-1.5-2.4-2.5-4.3-2.5H2.4c-.2.7-.4 1.4-.4 2.2 0 4.4 3.6 8 8 8 5 0 9.2-3.6 9.9-8.4.8.1 1.6-.2 2.1-.8.4-.5.5-1.2.3-1.8-.7.7-1.5 1-2.3 1z"/>`,
|
|
4929
|
+
brandColor: "#2496ED"
|
|
4930
|
+
},
|
|
4931
|
+
cloudflare: {
|
|
4932
|
+
name: "Cloudflare",
|
|
4933
|
+
category: "cloud",
|
|
4934
|
+
viewBox: "0 0 24 24",
|
|
4935
|
+
svgPaths: `<path fill="currentColor" d="M18.42 10.36A6.5 6.5 0 0 0 7.2 9.04 4.5 4.5 0 0 0 3 13.5a4.5 4.5 0 0 0 4.5 4.5h11a3.5 3.5 0 0 0 .92-6.88v-.76z"/>`,
|
|
4936
|
+
brandColor: "#F38020"
|
|
4937
|
+
},
|
|
4938
|
+
s3: {
|
|
4939
|
+
name: "Amazon S3",
|
|
4940
|
+
category: "cloud",
|
|
4941
|
+
viewBox: "0 0 24 24",
|
|
4942
|
+
svgPaths: `<path fill="currentColor" d="M12 2L4 6v12l8 4 8-4V6l-8-4zm0 2.2L18.2 7 12 9.8 5.8 7 12 4.2zM6 8.5l5 2.5v8.2L6 16.7V8.5zm12 8.2l-5 2.5V11l5-2.5v8.2z"/>`,
|
|
4943
|
+
brandColor: "#E05243"
|
|
4944
|
+
},
|
|
4945
|
+
// Databases & Stores
|
|
4946
|
+
postgresql: {
|
|
4947
|
+
name: "PostgreSQL",
|
|
4948
|
+
category: "database",
|
|
4949
|
+
viewBox: "0 0 24 24",
|
|
4950
|
+
svgPaths: `<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.4z"/>`,
|
|
4951
|
+
brandColor: "#336791"
|
|
4952
|
+
},
|
|
4953
|
+
mysql: {
|
|
4954
|
+
name: "MySQL",
|
|
4955
|
+
category: "database",
|
|
4956
|
+
viewBox: "0 0 24 24",
|
|
4957
|
+
svgPaths: `<path fill="currentColor" d="M12 3c-4.97 0-9 1.79-9 4v10c0 2.21 4.03 4 9 4s9-1.79 9-4V7c0-2.21-4.03-4-9-4zm0 2c4.41 0 7 1.43 7 2s-2.59 2-7 2-7-1.43-7-2 2.59-2 7-2zm0 14c-4.41 0-7-1.43-7-2v-1.82c1.78 1.11 4.29 1.82 7 1.82s5.22-.71 7-1.82V17c0 .57-2.59 2-7 2zm0-5c-4.41 0-7-1.43-7-2v-1.82c1.78 1.11 4.29 1.82 7 1.82s5.22-.71 7-1.82V12c0 .57-2.59 2-7 2z"/>`,
|
|
4958
|
+
brandColor: "#4479A1"
|
|
4959
|
+
},
|
|
4960
|
+
redis: {
|
|
4961
|
+
name: "Redis",
|
|
4962
|
+
category: "database",
|
|
4963
|
+
viewBox: "0 0 24 24",
|
|
4964
|
+
svgPaths: `<path fill="currentColor" d="M12 2L2 7.5l10 5.5 10-5.5L12 2zm-8 8.7V17L12 22.5V16L4 10.7zm16 0L12 16v6.5l8-5.5v-6.3z"/>`,
|
|
4965
|
+
brandColor: "#DC382D"
|
|
4966
|
+
},
|
|
4967
|
+
mongodb: {
|
|
4968
|
+
name: "MongoDB",
|
|
4969
|
+
category: "database",
|
|
4970
|
+
viewBox: "0 0 24 24",
|
|
4971
|
+
svgPaths: `<path fill="currentColor" d="M12 2C11.5 3.5 7 10.5 7 15c0 3.5 2.5 6 5 7 2.5-1 5-3.5 5-7 0-4.5-4.5-11.5-5-13zm0 17.5c-1.5-.7-3-2.5-3-4.5 0-2.8 2.2-7 3-9 0.8 2 3 6.2 3 9 0 2-1.5 3.8-3 4.5z"/>`,
|
|
4972
|
+
brandColor: "#47A248"
|
|
4973
|
+
},
|
|
4974
|
+
cassandra: {
|
|
4975
|
+
name: "Apache Cassandra",
|
|
4976
|
+
category: "database",
|
|
4977
|
+
viewBox: "0 0 24 24",
|
|
4978
|
+
svgPaths: `<path fill="currentColor" d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8zm-4-9h8v2H8z"/>`,
|
|
4979
|
+
brandColor: "#1287B1"
|
|
4980
|
+
},
|
|
4981
|
+
sqlite: {
|
|
4982
|
+
name: "SQLite",
|
|
4983
|
+
category: "database",
|
|
4984
|
+
viewBox: "0 0 24 24",
|
|
4985
|
+
svgPaths: `<path fill="currentColor" d="M12 2C6.48 2 2 4.69 2 8v8c0 3.31 4.48 6 10 6s10-2.69 10-6V8c0-3.31-4.48-6-10-6zm0 2c4.42 0 8 1.79 8 4s-3.58 4-8 4-8-1.79-8-4 3.58-4 8-4zm0 16c-4.42 0-8-1.79-8-4v-2.38c2.08 1.47 5.04 2.38 8 2.38s5.92-.91 8-2.38V16c0 2.21-3.58 4-8 4z"/>`,
|
|
4986
|
+
brandColor: "#003B57"
|
|
4987
|
+
},
|
|
4988
|
+
kafka: {
|
|
4989
|
+
name: "Apache Kafka",
|
|
4990
|
+
category: "messaging",
|
|
4991
|
+
viewBox: "0 0 24 24",
|
|
4992
|
+
svgPaths: `<path fill="currentColor" d="M12 3a9 9 0 1 0 9 9 9 9 0 0 0-9-9zm0 16a7 7 0 1 1 7-7 7 7 0 0 1-7 7zm-3-8a2 2 0 1 0-2-2 2 2 0 0 0 2 2zm6 0a2 2 0 1 0-2-2 2 2 0 0 0 2 2zm-3 6a2 2 0 1 0-2-2 2 2 0 0 0 2 2z"/>`,
|
|
4993
|
+
brandColor: "#231F20"
|
|
4994
|
+
},
|
|
4995
|
+
rabbitmq: {
|
|
4996
|
+
name: "RabbitMQ",
|
|
4997
|
+
category: "messaging",
|
|
4998
|
+
viewBox: "0 0 24 24",
|
|
4999
|
+
svgPaths: `<path fill="currentColor" d="M12 2a5 5 0 0 0-5 5v1H5a3 3 0 0 0-3 3v7a4 4 0 0 0 4 4h12a4 4 0 0 0 4-4v-7a3 3 0 0 0-3-3h-2V7a5 5 0 0 0-5-5zm-3 6a3 3 0 0 1 6 0v1H9V8zm-3 5h2v2H6v-2zm12 0h2v2h-2v-2z"/>`,
|
|
5000
|
+
brandColor: "#FF6600"
|
|
5001
|
+
},
|
|
5002
|
+
elasticsearch: {
|
|
5003
|
+
name: "Elasticsearch",
|
|
5004
|
+
category: "database",
|
|
5005
|
+
viewBox: "0 0 24 24",
|
|
5006
|
+
svgPaths: `<path fill="currentColor" d="M12 2A10 10 0 1 0 22 12 10 10 0 0 0 12 2zm-1 3.1a6.9 6.9 0 0 1 5.5 2.9h-11A6.9 6.9 0 0 1 11 5.1zM5.1 12c0-.7.1-1.3.3-2h13.2c.2.7.3 1.3.3 2s-.1 1.3-.3 2H5.4c-.2-.7-.3-1.3-.3-2zm5.9 6.9a6.9 6.9 0 0 1-5.5-2.9h11a6.9 6.9 0 0 1-5.5 2.9z"/>`,
|
|
5007
|
+
brandColor: "#005571"
|
|
5008
|
+
},
|
|
5009
|
+
// Compute, Gateway & Runtimes
|
|
5010
|
+
nodejs: {
|
|
5011
|
+
name: "Node.js",
|
|
5012
|
+
category: "runtime",
|
|
5013
|
+
viewBox: "0 0 24 24",
|
|
5014
|
+
svgPaths: `<path fill="currentColor" d="M12 2L3.5 7v10L12 22l8.5-5V7L12 2zm6.5 13.7L12 19.5l-6.5-3.8V8.3L12 4.5l6.5 3.8v7.4z"/>`,
|
|
5015
|
+
brandColor: "#339933"
|
|
5016
|
+
},
|
|
5017
|
+
python: {
|
|
5018
|
+
name: "Python",
|
|
5019
|
+
category: "runtime",
|
|
5020
|
+
viewBox: "0 0 24 24",
|
|
5021
|
+
svgPaths: `<path fill="currentColor" d="M11.9 2c-3.1 0-4.9.4-4.9 2.2V6h5v1.5H5.8C3.7 7.5 2 9.2 2 11.3c0 2.2 1.4 3.7 3.8 3.7h1.4v-1.9c0-1.8 1.6-3.3 3.5-3.3h5.2V7.7C15.9 4.1 14.8 2 11.9 2zM9 4.2a.8.8 0 1 1 0 1.6.8.8 0 0 1 0-1.6zm3.1 17.8c3.1 0 4.9-.4 4.9-2.2V18h-5v-1.5h6.2c2.1 0 3.8-1.7 3.8-3.8 0-2.2-1.4-3.7-3.8-3.7h-1.4v1.9c0 1.8-1.6 3.3-3.5 3.3H8.6v2.1c0 3.6 1.1 5.7 4.5 5.7zM15 18.2a.8.8 0 1 1 0 1.6.8.8 0 0 1 0-1.6z"/>`,
|
|
5022
|
+
brandColor: "#3776AB"
|
|
5023
|
+
},
|
|
5024
|
+
golang: {
|
|
5025
|
+
name: "Go",
|
|
5026
|
+
category: "runtime",
|
|
5027
|
+
viewBox: "0 0 24 24",
|
|
5028
|
+
svgPaths: `<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm4 11h-3v2h3v2h-5v-6h5v2zm-7-2H7v-2h2v2z"/>`,
|
|
5029
|
+
brandColor: "#00ADD8"
|
|
5030
|
+
},
|
|
5031
|
+
rust: {
|
|
5032
|
+
name: "Rust",
|
|
5033
|
+
category: "runtime",
|
|
5034
|
+
viewBox: "0 0 24 24",
|
|
5035
|
+
svgPaths: `<path fill="currentColor" d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm1 14.5l-2-3h-1v3H8V7.5h4a3 3 0 0 1 3 3 3 3 0 0 1-2 2.8l2 3.2zm-1-6.5h-2v2h2a1 1 0 0 0 1-1 1 1 0 0 0-1-1z"/>`,
|
|
5036
|
+
brandColor: "#DEA584"
|
|
5037
|
+
},
|
|
5038
|
+
nginx: {
|
|
5039
|
+
name: "Nginx",
|
|
5040
|
+
category: "gateway",
|
|
5041
|
+
viewBox: "0 0 24 24",
|
|
5042
|
+
svgPaths: `<path fill="currentColor" d="M12 2L2 7.8v8.4L12 22l10-5.8V7.8L12 2zm-4 13.5V8.5l3 3.5v3.5l-3-3.5zm8 0l-3-3.5V8.5l3 3.5v3.5z"/>`,
|
|
5043
|
+
brandColor: "#009639"
|
|
5044
|
+
},
|
|
5045
|
+
envoy: {
|
|
5046
|
+
name: "Envoy Proxy",
|
|
5047
|
+
category: "gateway",
|
|
5048
|
+
viewBox: "0 0 24 24",
|
|
5049
|
+
svgPaths: `<path fill="currentColor" d="M12 2L2 7v10l10 5 10-5V7L12 2zm0 2.2L19.5 8 12 11.8 4.5 8 12 4.2zM4 9.5l7 3.5v7.2L4 16.7V9.5zm16 7.2l-7 3.5V13l7-3.5v7.2z"/>`,
|
|
5050
|
+
brandColor: "#BF360C"
|
|
5051
|
+
},
|
|
5052
|
+
graphql: {
|
|
5053
|
+
name: "GraphQL",
|
|
5054
|
+
category: "gateway",
|
|
5055
|
+
viewBox: "0 0 24 24",
|
|
5056
|
+
svgPaths: `<path fill="currentColor" d="M12 2l8.66 5v10L12 22l-8.66-5V7L12 2zm0 2.4L5.5 8.1v7.8L12 19.6l6.5-3.7V8.1L12 4.4zM12 7a2 2 0 1 1 0 4 2 2 0 0 1 0-4zm0 6a2 2 0 1 1 0 4 2 2 0 0 1 0-4z"/>`,
|
|
5057
|
+
brandColor: "#E10098"
|
|
5058
|
+
},
|
|
5059
|
+
// Clients & Browsers
|
|
5060
|
+
chrome: {
|
|
5061
|
+
name: "Google Chrome",
|
|
5062
|
+
category: "client",
|
|
5063
|
+
viewBox: "0 0 24 24",
|
|
5064
|
+
svgPaths: `<path fill="currentColor" d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 3.6a6.4 6.4 0 0 1 5.48 3.1h-5.48a3.3 3.3 0 0 0-3.1 2.2L6.16 6.16A6.36 6.36 0 0 1 12 5.6zm-6.4 6.4a6.4 6.4 0 0 1 .52-2.54l2.74 4.74a3.3 3.3 0 0 0 3.1 1.76v4.6A6.4 6.4 0 0 1 5.6 12zm6.4 6.4a6.36 6.36 0 0 1-4.74-2.14l2.74-4.74a3.3 3.3 0 0 0 2 0l2.74 4.74A6.36 6.36 0 0 1 12 18.4zm0-4.4a2 2 0 1 1 2-2 2 2 0 0 1-2 2z"/>`,
|
|
5065
|
+
brandColor: "#4285F4"
|
|
5066
|
+
},
|
|
5067
|
+
terminal: {
|
|
5068
|
+
name: "Terminal",
|
|
5069
|
+
category: "client",
|
|
5070
|
+
viewBox: "0 0 24 24",
|
|
5071
|
+
svgPaths: `<path fill="currentColor" d="M20 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 14H4V8h16v10zm-12-3l3-3-3-3 1.41-1.41L12.83 12l-3.42 3.41L8 15zm6 0h4v2h-4v-2z"/>`,
|
|
5072
|
+
brandColor: "#4ADE80"
|
|
5073
|
+
},
|
|
5074
|
+
// Security & Identity
|
|
5075
|
+
vault: {
|
|
5076
|
+
name: "HashiCorp Vault",
|
|
5077
|
+
category: "security",
|
|
5078
|
+
viewBox: "0 0 24 24",
|
|
5079
|
+
svgPaths: `<path fill="currentColor" d="M12 2L4 6v6c0 5.55 3.84 10.74 8 12 4.16-1.26 8-6.45 8-12V6l-8-4zm0 6a3 3 0 0 1 3 3c0 1.3-.84 2.4-2 2.82V17h-2v-3.18A3 3 0 0 1 9 11a3 3 0 0 1 3-3z"/>`,
|
|
5080
|
+
brandColor: "#000000"
|
|
5081
|
+
},
|
|
5082
|
+
opa: {
|
|
5083
|
+
name: "Open Policy Agent",
|
|
5084
|
+
category: "security",
|
|
5085
|
+
viewBox: "0 0 24 24",
|
|
5086
|
+
svgPaths: `<path fill="currentColor" d="M12 2L3 7v10l9 5 9-5V7l-9-5zm0 2.3l6.7 3.7v7.4L12 19.1 5.3 15.4V8l6.7-3.7zM12 7a5 5 0 1 0 5 5 5 5 0 0 0-5-5z"/>`,
|
|
5087
|
+
brandColor: "#5B7382"
|
|
5088
|
+
},
|
|
5089
|
+
keycloak: {
|
|
5090
|
+
name: "Keycloak",
|
|
5091
|
+
category: "security",
|
|
5092
|
+
viewBox: "0 0 24 24",
|
|
5093
|
+
svgPaths: `<path fill="currentColor" d="M12 2a5 5 0 0 0-5 5v3H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-8a2 2 0 0 0-2-2h-2V7a5 5 0 0 0-5-5zm-3 5a3 3 0 0 1 6 0v3H9V7zm3 6a2 2 0 0 1 2 2v2a2 2 0 0 1-4 0v-2a2 2 0 0 1 2-2z"/>`,
|
|
5094
|
+
brandColor: "#0088CE"
|
|
5095
|
+
},
|
|
5096
|
+
// Observability & SRE
|
|
5097
|
+
prometheus: {
|
|
5098
|
+
name: "Prometheus",
|
|
5099
|
+
category: "observability",
|
|
5100
|
+
viewBox: "0 0 24 24",
|
|
5101
|
+
svgPaths: `<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 14h-2v-6h2v6zm0-8h-2V6h2v2z"/>`,
|
|
5102
|
+
brandColor: "#E6522C"
|
|
5103
|
+
},
|
|
5104
|
+
datadog: {
|
|
5105
|
+
name: "Datadog",
|
|
5106
|
+
category: "observability",
|
|
5107
|
+
viewBox: "0 0 24 24",
|
|
5108
|
+
svgPaths: `<path fill="currentColor" d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm4.5 14H15v-3h-2v3h-1.5v-6H13v1.5h2V10h1.5v6zm-7 0H8v-6h3a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2H9.5zm0-4.5V14H11a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5H9.5z"/>`,
|
|
5109
|
+
brandColor: "#632CA6"
|
|
5110
|
+
},
|
|
5111
|
+
jaeger: {
|
|
5112
|
+
name: "Jaeger Tracing",
|
|
5113
|
+
category: "observability",
|
|
5114
|
+
viewBox: "0 0 24 24",
|
|
5115
|
+
svgPaths: `<path fill="currentColor" d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm-1 5h2v6h-2zm0 8h2v2h-2z"/>`,
|
|
5116
|
+
brandColor: "#60D0E4"
|
|
5117
|
+
},
|
|
5118
|
+
grafana: {
|
|
5119
|
+
name: "Grafana",
|
|
5120
|
+
category: "observability",
|
|
5121
|
+
viewBox: "0 0 24 24",
|
|
5122
|
+
svgPaths: `<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 14.93c-2.82-.41-5-2.85-5-5.93 0-.82.16-1.6.46-2.31l3.07 3.07c-.12.4-.19.82-.19 1.25 0 1.66 1.34 3 3 3 .43 0 .85-.07 1.25-.19l-2.59-2.59zM16.5 12c0 1.93-1.12 3.6-2.76 4.38l-4.12-4.12A4.47 4.47 0 0 1 12 7.5c2.48 0 4.5 2.02 4.5 4.5z"/>`,
|
|
5123
|
+
brandColor: "#F46800"
|
|
5124
|
+
},
|
|
5125
|
+
pagerduty: {
|
|
5126
|
+
name: "PagerDuty",
|
|
5127
|
+
category: "observability",
|
|
5128
|
+
viewBox: "0 0 24 24",
|
|
5129
|
+
svgPaths: `<path fill="currentColor" d="M5 3h7a6 6 0 0 1 6 6 6 6 0 0 1-6 6H9v6H5V3zm4 8h3a2 2 0 0 0 2-2 2 2 0 0 0-2-2H9v4z"/>`,
|
|
5130
|
+
brandColor: "#04AC38"
|
|
5131
|
+
},
|
|
5132
|
+
slack: {
|
|
5133
|
+
name: "Slack",
|
|
5134
|
+
category: "messaging",
|
|
5135
|
+
viewBox: "0 0 24 24",
|
|
5136
|
+
svgPaths: `<path fill="currentColor" d="M6 15a2 2 0 1 1-2-2h2v2zm1 0a2 2 0 0 1 2-2 2 2 0 0 1 2 2v5a2 2 0 1 1-4 0v-5zm2-8a2 2 0 1 1-2-2 2 2 0 0 1 2 2v2zm0 1a2 2 0 0 1 2 2 2 2 0 0 1-2 2H4a2 2 0 1 1 0-4h5zm8 2a2 2 0 1 1 2 2h-2v-2zm-1 0a2 2 0 0 1-2 2 2 2 0 0 1-2-2V5a2 2 0 1 1 4 0v5zm-2 8a2 2 0 1 1 2 2 2 2 0 0 1-2-2v-2zm0-1a2 2 0 0 1-2-2 2 2 0 0 1 2-2h5a2 2 0 1 1 0 4h-5z"/>`,
|
|
5137
|
+
brandColor: "#4A154B"
|
|
5138
|
+
},
|
|
5139
|
+
// Big Data, Lakehouse & AI
|
|
5140
|
+
spark: {
|
|
5141
|
+
name: "Apache Spark",
|
|
5142
|
+
category: "data",
|
|
5143
|
+
viewBox: "0 0 24 24",
|
|
5144
|
+
svgPaths: `<path fill="currentColor" d="M12 2l2.4 7.4h7.6l-6.2 4.5 2.4 7.4-6.2-4.5-6.2 4.5 2.4-7.4-6.2-4.5h7.6z"/>`,
|
|
5145
|
+
brandColor: "#E25A1C"
|
|
5146
|
+
},
|
|
5147
|
+
flink: {
|
|
5148
|
+
name: "Apache Flink",
|
|
5149
|
+
category: "data",
|
|
5150
|
+
viewBox: "0 0 24 24",
|
|
5151
|
+
svgPaths: `<path fill="currentColor" d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm1 15h-2v-5h2zm0-7h-2V8h2z"/>`,
|
|
5152
|
+
brandColor: "#E6522C"
|
|
5153
|
+
},
|
|
5154
|
+
snowflake: {
|
|
5155
|
+
name: "Snowflake",
|
|
5156
|
+
category: "data",
|
|
5157
|
+
viewBox: "0 0 24 24",
|
|
5158
|
+
svgPaths: `<path fill="currentColor" d="M12 2v20m-7.07-2.93l14.14-14.14M2 12h20M4.93 4.93l14.14 14.14" stroke="currentColor" stroke-width="2" fill="none"/>`,
|
|
5159
|
+
brandColor: "#29B5E8"
|
|
5160
|
+
},
|
|
5161
|
+
delta: {
|
|
5162
|
+
name: "Delta Lake",
|
|
5163
|
+
category: "data",
|
|
5164
|
+
viewBox: "0 0 24 24",
|
|
5165
|
+
svgPaths: `<path fill="currentColor" d="M12 2L2 22h20L12 2zm0 5l6.5 13H5.5L12 7z"/>`,
|
|
5166
|
+
brandColor: "#00A4E4"
|
|
5167
|
+
},
|
|
5168
|
+
superset: {
|
|
5169
|
+
name: "Apache Superset",
|
|
5170
|
+
category: "data",
|
|
5171
|
+
viewBox: "0 0 24 24",
|
|
5172
|
+
svgPaths: `<path fill="currentColor" d="M3 3h4v18H3zm7 6h4v12h-4zm7-4h4v16h-4z"/>`,
|
|
5173
|
+
brandColor: "#20A6B2"
|
|
5174
|
+
},
|
|
5175
|
+
gemini: {
|
|
5176
|
+
name: "Google Gemini",
|
|
5177
|
+
category: "compute",
|
|
5178
|
+
viewBox: "0 0 24 24",
|
|
5179
|
+
svgPaths: `<path fill="currentColor" d="M12 2C12 7.52 7.52 12 2 12c5.48 0 9.95 4.48 10 10 .05-5.52 4.52-10 10-10-5.48 0-9.95-4.48-10-10z"/>`,
|
|
5180
|
+
brandColor: "#8E75FF"
|
|
5181
|
+
}
|
|
5182
|
+
};
|
|
5183
|
+
function resolveVectorSymbol(nameOrAlias) {
|
|
5184
|
+
if (!nameOrAlias || typeof nameOrAlias !== "string") return null;
|
|
5185
|
+
const key = nameOrAlias.trim().toLowerCase().replace(/[\s_.-]+/g, "");
|
|
5186
|
+
if (VECTOR_SYMBOLS[key]) return VECTOR_SYMBOLS[key];
|
|
5187
|
+
const aliases = {
|
|
5188
|
+
postgres: "postgresql",
|
|
5189
|
+
pg: "postgresql",
|
|
5190
|
+
k8s: "kubernetes",
|
|
5191
|
+
kube: "kubernetes",
|
|
5192
|
+
cf: "cloudflare",
|
|
5193
|
+
node: "nodejs",
|
|
5194
|
+
py: "python",
|
|
5195
|
+
go: "golang",
|
|
5196
|
+
elastic: "elasticsearch",
|
|
5197
|
+
es: "elasticsearch",
|
|
5198
|
+
rabbit: "rabbitmq",
|
|
5199
|
+
mq: "rabbitmq",
|
|
5200
|
+
browser: "chrome",
|
|
5201
|
+
web: "chrome",
|
|
5202
|
+
cli: "terminal",
|
|
5203
|
+
console: "terminal",
|
|
5204
|
+
shell: "terminal",
|
|
5205
|
+
awss3: "s3",
|
|
5206
|
+
deltalake: "delta",
|
|
5207
|
+
lakehouse: "delta",
|
|
5208
|
+
llm: "gemini",
|
|
5209
|
+
ai: "gemini"
|
|
5210
|
+
};
|
|
5211
|
+
const target = aliases[key];
|
|
5212
|
+
return target ? VECTOR_SYMBOLS[target] || null : null;
|
|
5213
|
+
}
|
|
5214
|
+
function renderSymbolSvg(symbol, options) {
|
|
5215
|
+
const resolved = typeof symbol === "string" ? resolveVectorSymbol(symbol) : symbol;
|
|
5216
|
+
if (!resolved) return null;
|
|
5217
|
+
const size = options?.size ?? 18;
|
|
5218
|
+
const className = options?.className ? ` class="${options.className}"` : "";
|
|
5219
|
+
const style = options?.color ? ` style="color: ${options.color}"` : "";
|
|
5220
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${resolved.viewBox}" width="${size}" height="${size}"${className}${style} aria-hidden="true">${resolved.svgPaths}</svg>`;
|
|
5221
|
+
}
|
|
5222
|
+
function listAvailableSymbols() {
|
|
5223
|
+
return Object.keys(VECTOR_SYMBOLS);
|
|
5224
|
+
}
|
|
5225
|
+
|
|
5226
|
+
// src/provenance.ts
|
|
5227
|
+
var CONTROL_CHAR_RE = /[\u0000-\u001f\u007f]/;
|
|
5228
|
+
function parseCodeAnchor(raw, repositoryUrl, revision) {
|
|
5229
|
+
if (typeof raw !== "string" || !raw.trim()) return null;
|
|
5230
|
+
const trimmed = raw.trim();
|
|
5231
|
+
const ghMatch = trimmed.match(/^(?:https:\/\/github\.com\/[^/]+\/[^/]+\/blob\/([^/]+)\/)([^#]+)(?:#L(\d+)(?:-L(\d+))?)?$/i);
|
|
5232
|
+
if (ghMatch) {
|
|
5233
|
+
const rev = ghMatch[1];
|
|
5234
|
+
const filePath2 = decodeURIComponent(ghMatch[2]);
|
|
5235
|
+
const startLine2 = ghMatch[3] ? parseInt(ghMatch[3], 10) : void 0;
|
|
5236
|
+
const endLine2 = ghMatch[4] ? parseInt(ghMatch[4], 10) : startLine2;
|
|
5237
|
+
return {
|
|
5238
|
+
raw: trimmed,
|
|
5239
|
+
filePath: filePath2,
|
|
5240
|
+
startLine: startLine2,
|
|
5241
|
+
endLine: endLine2,
|
|
5242
|
+
revision: rev,
|
|
5243
|
+
resolvedHref: trimmed
|
|
5244
|
+
};
|
|
5245
|
+
}
|
|
5246
|
+
const [filePathRaw, fragment] = trimmed.split("#");
|
|
5247
|
+
let filePath = filePathRaw.trim().replace(/\\/g, "/");
|
|
5248
|
+
if (filePath.startsWith("./")) {
|
|
5249
|
+
filePath = filePath.slice(2);
|
|
5250
|
+
}
|
|
5251
|
+
if (!filePath || filePath.startsWith("/") || filePath.startsWith("../") || filePath === ".." || filePath.includes("/../") || filePath.endsWith("/..") || CONTROL_CHAR_RE.test(filePath)) {
|
|
5252
|
+
return null;
|
|
5253
|
+
}
|
|
5254
|
+
let startLine;
|
|
5255
|
+
let endLine;
|
|
5256
|
+
if (fragment) {
|
|
5257
|
+
const lineMatch = fragment.match(/^L?(\d+)(?:-L?(\d+))?$/i);
|
|
5258
|
+
if (lineMatch) {
|
|
5259
|
+
startLine = parseInt(lineMatch[1], 10);
|
|
5260
|
+
endLine = lineMatch[2] ? parseInt(lineMatch[2], 10) : startLine;
|
|
5261
|
+
}
|
|
5262
|
+
}
|
|
5263
|
+
let resolvedHref;
|
|
5264
|
+
if (repositoryUrl) {
|
|
5265
|
+
const base = repositoryUrl.replace(/\/$/, "");
|
|
5266
|
+
const rev = revision || "main";
|
|
5267
|
+
const lineFrag = startLine ? `#L${startLine}${endLine && endLine !== startLine ? `-L${endLine}` : ""}` : "";
|
|
5268
|
+
resolvedHref = `${base}/blob/${rev}/${filePath}${lineFrag}`;
|
|
5269
|
+
}
|
|
5270
|
+
return {
|
|
5271
|
+
raw: trimmed,
|
|
5272
|
+
filePath,
|
|
5273
|
+
startLine,
|
|
5274
|
+
endLine,
|
|
5275
|
+
revision,
|
|
5276
|
+
resolvedHref
|
|
5277
|
+
};
|
|
5278
|
+
}
|
|
5279
|
+
function extractDiagramCodeAnchors(ast, repositoryUrl, revision) {
|
|
5280
|
+
const anchors = /* @__PURE__ */ new Map();
|
|
5281
|
+
for (const [nodeId, node] of Object.entries(ast.nodes || {})) {
|
|
5282
|
+
const rawAnchor = node.props["@src"] || node.props["src"] || node.props["@source"] || node.props["source"] || node.props["@code"] || node.props["code"] || node.props["@anchor"] || node.props["anchor"];
|
|
5283
|
+
if (rawAnchor) {
|
|
5284
|
+
const parsed = parseCodeAnchor(rawAnchor, repositoryUrl, revision);
|
|
5285
|
+
if (parsed) {
|
|
5286
|
+
anchors.set(nodeId, parsed);
|
|
5287
|
+
}
|
|
5288
|
+
}
|
|
5289
|
+
}
|
|
5290
|
+
return anchors;
|
|
5291
|
+
}
|
|
5292
|
+
function verifyCodeAnchorsWithReader(anchors, fileReader) {
|
|
5293
|
+
const diagnostics = [];
|
|
5294
|
+
let verifiedCount = 0;
|
|
5295
|
+
for (const [nodeId, anchor] of anchors.entries()) {
|
|
5296
|
+
const { filePath, startLine, endLine } = anchor;
|
|
5297
|
+
const segments = filePath.split("/");
|
|
5298
|
+
if (segments.some((s) => !s || s === "." || s === "..") || segments[0] === ".git") {
|
|
5299
|
+
diagnostics.push({
|
|
5300
|
+
nodeId,
|
|
5301
|
+
severity: "error",
|
|
5302
|
+
code: "provenance/path-escape",
|
|
5303
|
+
message: `Code anchor for node "${nodeId}" must stay within repository and cannot address .git (${filePath}).`,
|
|
5304
|
+
filePath,
|
|
5305
|
+
fixSuggestion: "Remove relative path traversal dots or .git segments."
|
|
5306
|
+
});
|
|
5307
|
+
continue;
|
|
5308
|
+
}
|
|
5309
|
+
if (!fileReader.fileExists(filePath)) {
|
|
5310
|
+
diagnostics.push({
|
|
5311
|
+
nodeId,
|
|
5312
|
+
severity: "error",
|
|
5313
|
+
code: "provenance/file-not-found",
|
|
5314
|
+
message: `Referenced file "${filePath}" does not exist in target repository.`,
|
|
5315
|
+
filePath,
|
|
5316
|
+
fixSuggestion: `Ensure "${filePath}" is committed and relative to repository root.`
|
|
5317
|
+
});
|
|
5318
|
+
continue;
|
|
5319
|
+
}
|
|
5320
|
+
const lineCount = fileReader.getLineCount(filePath);
|
|
5321
|
+
if (startLine !== void 0 && (startLine < 1 || startLine > lineCount)) {
|
|
5322
|
+
diagnostics.push({
|
|
5323
|
+
nodeId,
|
|
5324
|
+
severity: "warning",
|
|
5325
|
+
code: "provenance/line-out-of-bounds",
|
|
5326
|
+
message: `Line #${startLine} exceeds total line count (${lineCount}) of file "${filePath}".`,
|
|
5327
|
+
filePath,
|
|
5328
|
+
line: startLine,
|
|
5329
|
+
fixSuggestion: `Adjust line range to fall within 1..${lineCount}.`
|
|
5330
|
+
});
|
|
5331
|
+
continue;
|
|
5332
|
+
}
|
|
5333
|
+
if (endLine !== void 0 && (endLine < (startLine || 1) || endLine > lineCount)) {
|
|
5334
|
+
diagnostics.push({
|
|
5335
|
+
nodeId,
|
|
5336
|
+
severity: "warning",
|
|
5337
|
+
code: "provenance/line-out-of-bounds",
|
|
5338
|
+
message: `End line #${endLine} is out of bounds for "${filePath}" (total lines: ${lineCount}).`,
|
|
5339
|
+
filePath,
|
|
5340
|
+
line: endLine,
|
|
5341
|
+
fixSuggestion: `Ensure end line is >= start line and <= ${lineCount}.`
|
|
5342
|
+
});
|
|
5343
|
+
continue;
|
|
5344
|
+
}
|
|
5345
|
+
verifiedCount++;
|
|
5346
|
+
}
|
|
5347
|
+
const isValid = diagnostics.filter((d) => d.severity === "error").length === 0;
|
|
5348
|
+
const summaryMarkdown = [
|
|
5349
|
+
`### \u{1F6E1}\uFE0F Code Provenance Verification Report`,
|
|
5350
|
+
`- **Status**: ${isValid ? "\u2705 VERIFIED" : "\u274C FAILED"}`,
|
|
5351
|
+
`- **Total Anchors**: ${anchors.size}`,
|
|
5352
|
+
`- **Verified In-Tree**: ${verifiedCount}`,
|
|
5353
|
+
`- **Diagnostics**: ${diagnostics.length} issue(s)`,
|
|
5354
|
+
...diagnostics.length > 0 ? [
|
|
5355
|
+
"",
|
|
5356
|
+
`| Node | Severity | Issue | File / Location | Fix |`,
|
|
5357
|
+
`| :--- | :--- | :--- | :--- | :--- |`,
|
|
5358
|
+
...diagnostics.map(
|
|
5359
|
+
(d) => `| \`${d.nodeId}\` | **${d.severity.toUpperCase()}** | ${d.message} | \`${d.filePath}${d.line ? `:${d.line}` : ""}\` | ${d.fixSuggestion || "N/A"} |`
|
|
5360
|
+
)
|
|
5361
|
+
] : []
|
|
5362
|
+
].join("\n");
|
|
5363
|
+
return {
|
|
5364
|
+
isValid,
|
|
5365
|
+
totalAnchors: anchors.size,
|
|
5366
|
+
verifiedCount,
|
|
5367
|
+
anchors,
|
|
5368
|
+
diagnostics,
|
|
5369
|
+
summaryMarkdown
|
|
5370
|
+
};
|
|
5371
|
+
}
|
|
4365
5372
|
|
|
4366
5373
|
// src/syntax-diagnostics.ts
|
|
4367
5374
|
function damerauLevenshteinDistance(a, b) {
|
|
@@ -4621,7 +5628,7 @@ function diagnoseMarkdyCode(code, options = {}) {
|
|
|
4621
5628
|
snippet: trimmed,
|
|
4622
5629
|
suggestion: closeTheme ? `Use theme '${closeTheme.match}'.` : `Choose from: ${THEME_NAMES.join(", ")}.`,
|
|
4623
5630
|
didYouMean: closeTheme?.match,
|
|
4624
|
-
ruleExplanation: "Supported themes: paper, editorial, midnight, blueprint, graphite, nebula, sketchy, terminal.",
|
|
5631
|
+
ruleExplanation: "Supported themes: paper, editorial, midnight, blueprint, graphite, nebula, sketchy, terminal, ink, doodle.",
|
|
4625
5632
|
fix: closeTheme ? { original: themeMatch[0], replacement: `theme=${closeTheme.match}` } : void 0
|
|
4626
5633
|
});
|
|
4627
5634
|
}
|
|
@@ -5517,7 +6524,9 @@ function getIntelliCodeCompletions(docText, cursorLine, cursorCol) {
|
|
|
5517
6524
|
{ name: "editorial", desc: "High-contrast serif luxury publication layout" },
|
|
5518
6525
|
{ name: "graphite", desc: "Sleek slate monochrome engineering aesthetic" },
|
|
5519
6526
|
{ name: "terminal", desc: "Retro phosphor CRT hacker terminal green on black" },
|
|
5520
|
-
{ name: "sketchy", desc: "Hand-drawn sketchy whiteboard marker design" }
|
|
6527
|
+
{ name: "sketchy", desc: "Hand-drawn sketchy whiteboard marker design" },
|
|
6528
|
+
{ name: "ink", desc: "Monochromatic blue ink style inspired by ballpoint pens, fountain pens, cyanotypes & porcelain" },
|
|
6529
|
+
{ name: "doodle", desc: "Playful hand-drawn doodle sketchbook with felt-tip marker pens and comic block shadows" }
|
|
5521
6530
|
];
|
|
5522
6531
|
for (const t of themeEntries) {
|
|
5523
6532
|
items.push({
|
|
@@ -6073,7 +7082,1593 @@ function formatValue(value) {
|
|
|
6073
7082
|
if (typeof value === "string") return value;
|
|
6074
7083
|
return JSON.stringify(value);
|
|
6075
7084
|
}
|
|
7085
|
+
|
|
7086
|
+
// src/recipes.ts
|
|
7087
|
+
var ARCHITECTURE_RECIPES = [
|
|
7088
|
+
{
|
|
7089
|
+
id: "cache-aside",
|
|
7090
|
+
name: "Multi-Tier Cache-Aside Architecture",
|
|
7091
|
+
category: "caching",
|
|
7092
|
+
description: "High-performance cache-aside pattern with Redis Cluster, relational database persistence, and asynchronous cache warming.",
|
|
7093
|
+
keywords: ["cache", "redis", "postgres", "cache-aside", "hit", "miss", "warm", "database", "latency"],
|
|
7094
|
+
recommendedLayout: "LR",
|
|
7095
|
+
primaryNodes: ["Client", "Gateway", "URLService", "RedisCluster", "PostgreSQL"],
|
|
7096
|
+
highlights: [
|
|
7097
|
+
"Sub-millisecond read latency on cache hit",
|
|
7098
|
+
"Asynchronous cache population on miss",
|
|
7099
|
+
"Graceful fallback on cache eviction"
|
|
7100
|
+
],
|
|
7101
|
+
code: `scene "Multi-Tier Cache-Aside Architecture" theme=auto
|
|
7102
|
+
layout LR
|
|
7103
|
+
|
|
7104
|
+
browser Client "Web Client" icon=chrome
|
|
7105
|
+
gateway Gateway "API Gateway" icon=nginx @src="src/gateway/proxy.ts#L12"
|
|
7106
|
+
service URLService "URL Service" icon=nodejs @src="src/services/resolver.ts#L25"
|
|
7107
|
+
cache RedisCluster "Redis Cluster" icon=redis
|
|
7108
|
+
database PostgreSQL "PostgreSQL 16" icon=postgresql @src="src/db/schema.sql#L1"
|
|
7109
|
+
|
|
7110
|
+
beat cache_hit "1. Cache Hit Path":
|
|
7111
|
+
show Client Gateway URLService RedisCluster stagger=50ms
|
|
7112
|
+
frame Client Gateway URLService RedisCluster zoom=1.12
|
|
7113
|
+
Client -> Gateway "GET /link" -> URLService "resolve"
|
|
7114
|
+
URLService -> RedisCluster "GET key:url"
|
|
7115
|
+
URLService <- RedisCluster "200 Target URL"
|
|
7116
|
+
Client <- Gateway "301 Redirect"
|
|
7117
|
+
|
|
7118
|
+
beat cache_miss "2. Cache Miss & Async Warm":
|
|
7119
|
+
show PostgreSQL stagger=50ms
|
|
7120
|
+
frame URLService RedisCluster PostgreSQL zoom=1.15
|
|
7121
|
+
URLService -> PostgreSQL "SELECT dest WHERE key = 'url'"
|
|
7122
|
+
URLService <- PostgreSQL "Row Found"
|
|
7123
|
+
URLService ~> RedisCluster "SETEX key:url (TTL 1h)"
|
|
7124
|
+
glow PostgreSQL color=#38bdf8 & glow RedisCluster color=#22c55e
|
|
7125
|
+
`
|
|
7126
|
+
},
|
|
7127
|
+
{
|
|
7128
|
+
id: "event-driven-eda",
|
|
7129
|
+
name: "Event-Driven EDA with Kafka & Change Data Capture",
|
|
7130
|
+
category: "streaming",
|
|
7131
|
+
description: "Decoupled real-time event streaming pipeline using Kafka/Redpanda with Debezium CDC, Schema Registry, and DLQ handling.",
|
|
7132
|
+
keywords: ["kafka", "event", "streaming", "eda", "cdc", "debezium", "pubsub", "dlq", "consumer"],
|
|
7133
|
+
recommendedLayout: "LR",
|
|
7134
|
+
primaryNodes: ["OrderService", "DebeziumCDC", "KafkaCluster", "NotificationSvc", "AnalyticsConsumer", "DeadLetterQueue"],
|
|
7135
|
+
highlights: [
|
|
7136
|
+
"Zero dual-write penalty with transactional outbox",
|
|
7137
|
+
"Ordered partition publishing with schema validation",
|
|
7138
|
+
"Dead Letter Queue for poison-pill isolation"
|
|
7139
|
+
],
|
|
7140
|
+
code: `scene "Event-Driven EDA with Kafka & CDC" theme=midnight
|
|
7141
|
+
layout LR
|
|
7142
|
+
|
|
7143
|
+
service OrderSvc "Order Service" icon=golang @src="src/orders/handler.go#L40"
|
|
7144
|
+
database OrderDB "Order Store" icon=postgresql
|
|
7145
|
+
service DebeziumCDC "Debezium CDC" icon=docker
|
|
7146
|
+
queue KafkaCluster "Kafka Event Stream" icon=kafka
|
|
7147
|
+
service NotificationSvc "Notification Engine" icon=nodejs
|
|
7148
|
+
service AnalyticsConsumer "Real-Time Analytics" icon=python
|
|
7149
|
+
queue DLQ "Dead Letter Queue" icon=rabbitmq
|
|
7150
|
+
|
|
7151
|
+
beat order_outbox "1. Transactional Outbox Commit":
|
|
7152
|
+
show OrderSvc OrderDB DebeziumCDC KafkaCluster stagger=50ms
|
|
7153
|
+
OrderSvc -> OrderDB "COMMIT (Order + Outbox Event)"
|
|
7154
|
+
OrderDB -> DebeziumCDC "WAL Stream"
|
|
7155
|
+
DebeziumCDC ~> KafkaCluster "Publish order.created"
|
|
7156
|
+
|
|
7157
|
+
beat consumer_fanout "2. Real-Time Consumer Fanout":
|
|
7158
|
+
show NotificationSvc AnalyticsConsumer DLQ stagger=50ms
|
|
7159
|
+
KafkaCluster ~> NotificationSvc "Consume order.created"
|
|
7160
|
+
KafkaCluster ~> AnalyticsConsumer "Consume order.created"
|
|
7161
|
+
NotificationSvc -> DLQ "Routing Reject (Retries Exceeded)"
|
|
7162
|
+
glow KafkaCluster color=#fbbf24 & glow DLQ color=#fb7185
|
|
7163
|
+
`
|
|
7164
|
+
},
|
|
7165
|
+
{
|
|
7166
|
+
id: "cqrs-event-sourcing",
|
|
7167
|
+
name: "CQRS & Event Sourcing Architecture",
|
|
7168
|
+
category: "streaming",
|
|
7169
|
+
description: "Strict Command Query Responsibility Segregation with immutable append-only Event Store and read-optimized query projections.",
|
|
7170
|
+
keywords: ["cqrs", "event sourcing", "event store", "projection", "read model", "command", "query"],
|
|
7171
|
+
recommendedLayout: "LR",
|
|
7172
|
+
primaryNodes: ["CommandAPI", "CommandHandler", "EventStore", "ProjectionEngine", "ReadDB", "QueryAPI"],
|
|
7173
|
+
highlights: [
|
|
7174
|
+
"Complete auditability via append-only event stream",
|
|
7175
|
+
"Independent write-scaling and read-scaling",
|
|
7176
|
+
"Zero lock contention between queries and mutations"
|
|
7177
|
+
],
|
|
7178
|
+
code: `scene "CQRS & Event Sourcing Architecture" theme=blueprint
|
|
7179
|
+
layout LR
|
|
7180
|
+
|
|
7181
|
+
gateway CommandAPI "Command API" icon=nginx
|
|
7182
|
+
service CommandHandler "Command Handler" icon=golang @src="src/commands/execute.go#L18"
|
|
7183
|
+
database EventStore "Event Store (Append-Only)" icon=cassandra
|
|
7184
|
+
service ProjectionEngine "Projection Engine" icon=rust @src="src/projections/sync.rs#L30"
|
|
7185
|
+
database ReadDB "Read Database" icon=mongodb
|
|
7186
|
+
gateway QueryAPI "Query API" icon=nodejs
|
|
7187
|
+
|
|
7188
|
+
beat write_command "1. Command & Append Event":
|
|
7189
|
+
show CommandAPI CommandHandler EventStore stagger=50ms
|
|
7190
|
+
CommandAPI -> CommandHandler "POST /orders/create"
|
|
7191
|
+
CommandHandler -> EventStore "APPEND OrderCreatedEvent"
|
|
7192
|
+
CommandHandler <- EventStore "Event Ack (Offset: 10482)"
|
|
7193
|
+
CommandAPI <- CommandHandler "202 Accepted"
|
|
7194
|
+
|
|
7195
|
+
beat async_projection "2. Asynchronous Projection Update":
|
|
7196
|
+
show ProjectionEngine ReadDB QueryAPI stagger=50ms
|
|
7197
|
+
EventStore ~> ProjectionEngine "Tail Commit Log"
|
|
7198
|
+
ProjectionEngine -> ReadDB "UPSERT Materialized Order View"
|
|
7199
|
+
QueryAPI -> ReadDB "SELECT * FROM orders WHERE id = :id"
|
|
7200
|
+
QueryAPI <- ReadDB "Read Model Payload"
|
|
7201
|
+
`
|
|
7202
|
+
},
|
|
7203
|
+
{
|
|
7204
|
+
id: "api-gateway-mesh",
|
|
7205
|
+
name: "Cloud-Native API Gateway & Service Mesh",
|
|
7206
|
+
category: "microservices",
|
|
7207
|
+
description: "Enterprise zero-trust microservices architecture with Envoy/Istio service mesh, mTLS enforcement, and distributed tracing.",
|
|
7208
|
+
keywords: ["gateway", "mesh", "envoy", "istio", "microservices", "mtls", "kubernetes", "discovery"],
|
|
7209
|
+
recommendedLayout: "LR",
|
|
7210
|
+
primaryNodes: ["IngressGateway", "AuthService", "OrderMeshSvc", "PaymentMeshSvc", "InventoryMeshSvc", "JaegerCollector"],
|
|
7211
|
+
highlights: [
|
|
7212
|
+
"Strict mTLS identity verification between sidecars",
|
|
7213
|
+
"Global rate limiting and distributed OpenTelemetry spans",
|
|
7214
|
+
"Dynamic circuit breaking and automated retries"
|
|
7215
|
+
],
|
|
7216
|
+
code: `scene "Cloud-Native API Gateway & Service Mesh" theme=graphite
|
|
7217
|
+
layout LR
|
|
7218
|
+
|
|
7219
|
+
gateway IngressGateway "Envoy Ingress Gateway" icon=envoy @src="k8s/gateway.yaml#L1"
|
|
7220
|
+
service AuthService "Auth & Token Service" icon=nodejs @src="src/auth/token.ts#L44"
|
|
7221
|
+
service OrderMeshSvc "Order Service (mTLS)" icon=golang
|
|
7222
|
+
service PaymentMeshSvc "Payment Gateway (mTLS)" icon=nodejs
|
|
7223
|
+
service InventoryMeshSvc "Inventory Service (mTLS)" icon=python
|
|
7224
|
+
service JaegerCollector "OpenTelemetry Collector" icon=jaeger
|
|
7225
|
+
|
|
7226
|
+
beat ingress_auth "1. Edge Authentication & Route Verification":
|
|
7227
|
+
show IngressGateway AuthService JaegerCollector stagger=50ms
|
|
7228
|
+
IngressGateway -> AuthService "Validate JWT Bearer"
|
|
7229
|
+
IngressGateway <- AuthService "Claims Verified"
|
|
7230
|
+
IngressGateway ~> JaegerCollector "Span: ingress_entry"
|
|
7231
|
+
|
|
7232
|
+
beat internal_mesh_flow "2. Internal mTLS Mesh Fanout":
|
|
7233
|
+
show OrderMeshSvc PaymentMeshSvc InventoryMeshSvc stagger=50ms
|
|
7234
|
+
IngressGateway -> OrderMeshSvc "POST /checkout (mTLS)"
|
|
7235
|
+
OrderMeshSvc -> PaymentMeshSvc "POST /charge (mTLS)"
|
|
7236
|
+
OrderMeshSvc -> InventoryMeshSvc "POST /reserve (mTLS)"
|
|
7237
|
+
PaymentMeshSvc ~> JaegerCollector "Span: payment_settled"
|
|
7238
|
+
InventoryMeshSvc ~> JaegerCollector "Span: inventory_reserved"
|
|
7239
|
+
glow IngressGateway color=#38bdf8 & glow OrderMeshSvc color=#22c55e
|
|
7240
|
+
`
|
|
7241
|
+
},
|
|
7242
|
+
{
|
|
7243
|
+
id: "zero-trust-security",
|
|
7244
|
+
name: "Zero-Trust Security & Enclave Perimeter",
|
|
7245
|
+
category: "security",
|
|
7246
|
+
description: "Defense-in-depth zero-trust security perimeter featuring OIDC authentication, Open Policy Agent authorization, and Nitro Enclaves.",
|
|
7247
|
+
keywords: ["security", "zero-trust", "oidc", "opa", "policy", "enclave", "encryption", "vault", "waf"],
|
|
7248
|
+
recommendedLayout: "LR",
|
|
7249
|
+
primaryNodes: ["CloudflareWAF", "IdentityOIDC", "PolicyEngineOPA", "KeyVault", "SecureEnclave", "AuditLogStore"],
|
|
7250
|
+
highlights: [
|
|
7251
|
+
"Continuous runtime identity verification on every invocation",
|
|
7252
|
+
"Confidential computing in isolated CPU Nitro enclaves",
|
|
7253
|
+
"Immutable write-once cryptographic audit trail"
|
|
7254
|
+
],
|
|
7255
|
+
code: `scene "Zero-Trust Security & Enclave Perimeter" theme=midnight
|
|
7256
|
+
layout LR
|
|
7257
|
+
|
|
7258
|
+
gateway CloudflareWAF "Cloudflare Edge WAF" icon=cloudflare
|
|
7259
|
+
service IdentityOIDC "Identity Provider (OIDC)" icon=keycloak
|
|
7260
|
+
service PolicyEngineOPA "Policy Engine (OPA)" icon=opa @src="policies/authz.rego#L1"
|
|
7261
|
+
service KeyVault "HashiCorp Vault" icon=vault @src="config/vault.hcl#L10"
|
|
7262
|
+
service SecureEnclave "AWS Nitro Enclave" icon=aws
|
|
7263
|
+
database AuditLogStore "WORM Audit Store" icon=s3
|
|
7264
|
+
|
|
7265
|
+
beat access_request "1. Identity & Policy Evaluation":
|
|
7266
|
+
show CloudflareWAF IdentityOIDC PolicyEngineOPA stagger=50ms
|
|
7267
|
+
CloudflareWAF -> IdentityOIDC "Authenticate Request"
|
|
7268
|
+
CloudflareWAF <- IdentityOIDC "Token Issued"
|
|
7269
|
+
CloudflareWAF -> PolicyEngineOPA "Evaluate RBAC/ABAC Context"
|
|
7270
|
+
CloudflareWAF <- PolicyEngineOPA "Decision: ALLOW"
|
|
7271
|
+
|
|
7272
|
+
beat confidential_execution "2. Enclave Decryption & Audit":
|
|
7273
|
+
show SecureEnclave KeyVault AuditLogStore stagger=50ms
|
|
7274
|
+
CloudflareWAF -> SecureEnclave "Execute Protected Payload"
|
|
7275
|
+
SecureEnclave -> KeyVault "Request Ephemeral Decryption Key"
|
|
7276
|
+
SecureEnclave <- KeyVault "Key Granted"
|
|
7277
|
+
SecureEnclave ~> AuditLogStore "Cryptographic Audit Receipt"
|
|
7278
|
+
glow SecureEnclave color=#fb7185 & glow KeyVault color=#a78bfa
|
|
7279
|
+
`
|
|
7280
|
+
},
|
|
7281
|
+
{
|
|
7282
|
+
id: "medallion-lakehouse",
|
|
7283
|
+
name: "Medallion Data Lakehouse Architecture",
|
|
7284
|
+
category: "data",
|
|
7285
|
+
description: "Modern data engineering pipeline organizing raw, cleansed, and curated data across Bronze, Silver, and Gold tiers.",
|
|
7286
|
+
keywords: ["data", "lakehouse", "medallion", "bronze", "silver", "gold", "spark", "delta", "iceberg", "analytics"],
|
|
7287
|
+
recommendedLayout: "LR",
|
|
7288
|
+
primaryNodes: ["RawIngestKafka", "BronzeLake", "SparkCleansing", "SilverLake", "FlinkAggregation", "GoldWarehouse", "SupersetBI"],
|
|
7289
|
+
highlights: [
|
|
7290
|
+
"ACID transactions over object storage with Delta/Iceberg",
|
|
7291
|
+
"Multi-stage data quality checks between Bronze and Silver",
|
|
7292
|
+
"Sub-second dimensional queries on Gold warehouse"
|
|
7293
|
+
],
|
|
7294
|
+
code: `scene "Medallion Data Lakehouse Architecture" theme=editorial
|
|
7295
|
+
layout LR
|
|
7296
|
+
|
|
7297
|
+
queue RawIngestKafka "Raw Event Ingestion" icon=kafka
|
|
7298
|
+
database BronzeLake "Bronze Lake (Raw Ingest)" icon=s3
|
|
7299
|
+
service SparkCleansing "Spark Cleansing Job" icon=spark @src="jobs/cleanse_bronze.py#L15"
|
|
7300
|
+
database SilverLake "Silver Lake (Enriched)" icon=delta
|
|
7301
|
+
service FlinkAggregation "Flink Streaming Aggregator" icon=flink
|
|
7302
|
+
database GoldWarehouse "Gold Warehouse (Curated)" icon=snowflake
|
|
7303
|
+
dashboard SupersetBI "Apache Superset BI" icon=superset
|
|
7304
|
+
|
|
7305
|
+
beat bronze_ingest "1. Raw Stream to Bronze Tier":
|
|
7306
|
+
show RawIngestKafka BronzeLake SparkCleansing stagger=50ms
|
|
7307
|
+
RawIngestKafka -> BronzeLake "Append Raw JSON Payload"
|
|
7308
|
+
BronzeLake -> SparkCleansing "Trigger Micro-Batch"
|
|
7309
|
+
|
|
7310
|
+
beat silver_and_gold "2. Cleansing, Enrichment & BI Serving":
|
|
7311
|
+
show SilverLake FlinkAggregation GoldWarehouse SupersetBI stagger=50ms
|
|
7312
|
+
SparkCleansing -> SilverLake "Upsert Deduplicated Parquet"
|
|
7313
|
+
SilverLake -> FlinkAggregation "Stream Entity Updates"
|
|
7314
|
+
FlinkAggregation -> GoldWarehouse "Merge Into Star Schema"
|
|
7315
|
+
SupersetBI -> GoldWarehouse "Execute Dimensional Query"
|
|
7316
|
+
SupersetBI <- GoldWarehouse "Render Dashboard Metrics"
|
|
7317
|
+
`
|
|
7318
|
+
},
|
|
7319
|
+
{
|
|
7320
|
+
id: "agentic-react-tools",
|
|
7321
|
+
name: "Agentic AI Orchestrator & Tool Execution Loop",
|
|
7322
|
+
category: "ai",
|
|
7323
|
+
description: "Autonomous ReAct agent system with LLM Orchestrator, dynamic context memory, vector embeddings, and MCP tool execution.",
|
|
7324
|
+
keywords: ["ai", "agent", "llm", "react", "tool", "mcp", "vector", "rag", "orchestrator", "prompt"],
|
|
7325
|
+
recommendedLayout: "LR",
|
|
7326
|
+
primaryNodes: ["UserClient", "AgentOrchestrator", "VectorMemory", "ModelInference", "MCPToolExecutor", "SandboxRuntime"],
|
|
7327
|
+
highlights: [
|
|
7328
|
+
"Interactive reasoning loop (Thought -> Action -> Observation)",
|
|
7329
|
+
"Hybrid semantic search over vector memory store",
|
|
7330
|
+
"Secure sandboxed runtime for tool call executions"
|
|
7331
|
+
],
|
|
7332
|
+
code: `scene "Agentic AI Orchestrator & Tool Loop" theme=nebula
|
|
7333
|
+
layout LR
|
|
7334
|
+
|
|
7335
|
+
browser UserClient "User Workspace" icon=terminal
|
|
7336
|
+
service AgentOrchestrator "ReAct Agent Orchestrator" icon=python @src="agent/core.py#L35"
|
|
7337
|
+
database VectorMemory "Vector Memory (RAG)" icon=redis
|
|
7338
|
+
service ModelInference "LLM Inference API" icon=gemini
|
|
7339
|
+
service MCPToolExecutor "MCP Tool Protocol" icon=docker @src="agent/mcp_client.py#L20"
|
|
7340
|
+
service SandboxRuntime "Secure Container Sandbox" icon=docker
|
|
7341
|
+
|
|
7342
|
+
beat agent_thought "1. Plan & Context Retrieval":
|
|
7343
|
+
show UserClient AgentOrchestrator VectorMemory ModelInference stagger=50ms
|
|
7344
|
+
UserClient -> AgentOrchestrator "Goal: Deploy microservice"
|
|
7345
|
+
AgentOrchestrator -> VectorMemory "Query Relevant Runbooks"
|
|
7346
|
+
AgentOrchestrator <- VectorMemory "Runbook Context Vectors"
|
|
7347
|
+
AgentOrchestrator -> ModelInference "Generate Plan & Tool Call"
|
|
7348
|
+
AgentOrchestrator <- ModelInference "Call: run_command(kubectl apply)"
|
|
7349
|
+
|
|
7350
|
+
beat tool_execution "2. MCP Tool Execution & Observation":
|
|
7351
|
+
show MCPToolExecutor SandboxRuntime stagger=50ms
|
|
7352
|
+
AgentOrchestrator -> MCPToolExecutor "Execute Tool Request"
|
|
7353
|
+
MCPToolExecutor -> SandboxRuntime "Spawn Container & Execute"
|
|
7354
|
+
MCPToolExecutor <- SandboxRuntime "Output: deployment created"
|
|
7355
|
+
AgentOrchestrator <- MCPToolExecutor "Observation Receipt"
|
|
7356
|
+
UserClient <- AgentOrchestrator "Goal Achieved: Deployed successfully"
|
|
7357
|
+
glow AgentOrchestrator color=#c4b5fd & glow MCPToolExecutor color=#67e8f9
|
|
7358
|
+
`
|
|
7359
|
+
},
|
|
7360
|
+
{
|
|
7361
|
+
id: "active-active-failover",
|
|
7362
|
+
name: "Multi-Region Active-Active Resilient Failover",
|
|
7363
|
+
category: "resilience",
|
|
7364
|
+
description: "High-availability global architecture with GeoDNS latency routing, multi-region cluster active-active syncing, and automated failover.",
|
|
7365
|
+
keywords: ["resilience", "active-active", "failover", "multi-region", "disaster recovery", "replication", "dns", "ha"],
|
|
7366
|
+
recommendedLayout: "LR",
|
|
7367
|
+
primaryNodes: ["GlobalDNS", "RegionUSEast", "DBPrimaryEast", "RegionEUWest", "DBPrimaryWest", "HealthProbe"],
|
|
7368
|
+
highlights: [
|
|
7369
|
+
"Sub-second DNS failover when health probe detects outage",
|
|
7370
|
+
"Bi-directional conflict-free replicated database sync (CRDT)",
|
|
7371
|
+
"Zero downtime during planned regional maintenance"
|
|
7372
|
+
],
|
|
7373
|
+
code: `scene "Multi-Region Active-Active Failover" theme=midnight
|
|
7374
|
+
layout LR
|
|
7375
|
+
|
|
7376
|
+
gateway GlobalDNS "Global Route53 GeoDNS" icon=aws
|
|
7377
|
+
service RegionUSEast "Region US-East API" icon=kubernetes @src="infra/us-east/app.yaml#L1"
|
|
7378
|
+
database DBPrimaryEast "Aurora Global DB (East)" icon=postgresql
|
|
7379
|
+
service RegionEUWest "Region EU-West API" icon=kubernetes @src="infra/eu-west/app.yaml#L1"
|
|
7380
|
+
database DBPrimaryWest "Aurora Global DB (West)" icon=postgresql
|
|
7381
|
+
service HealthProbe "Global Health Checker" icon=datadog
|
|
7382
|
+
|
|
7383
|
+
beat steady_state "1. Steady-State Geo-Routing & Sync":
|
|
7384
|
+
show GlobalDNS RegionUSEast DBPrimaryEast RegionEUWest DBPrimaryWest stagger=50ms
|
|
7385
|
+
GlobalDNS -> RegionUSEast "Route US Traffic"
|
|
7386
|
+
RegionUSEast -> DBPrimaryEast "Local Read/Write"
|
|
7387
|
+
GlobalDNS -> RegionEUWest "Route EU Traffic"
|
|
7388
|
+
RegionEUWest -> DBPrimaryWest "Local Read/Write"
|
|
7389
|
+
DBPrimaryEast ~> DBPrimaryWest "Cross-Region Stream Replication"
|
|
7390
|
+
|
|
7391
|
+
beat simulated_failover "2. Outage Detection & Instant Failover":
|
|
7392
|
+
show HealthProbe stagger=50ms
|
|
7393
|
+
HealthProbe -> RegionUSEast "HTTP Health Probe (Timeout)"
|
|
7394
|
+
HealthProbe ~> GlobalDNS "Withdraw US-East IP from Pool"
|
|
7395
|
+
GlobalDNS -> RegionEUWest "Reroute 100% Global Traffic"
|
|
7396
|
+
glow RegionEUWest color=#22c55e & glow RegionUSEast color=#fb7185
|
|
7397
|
+
`
|
|
7398
|
+
},
|
|
7399
|
+
{
|
|
7400
|
+
id: "distributed-consensus-raft",
|
|
7401
|
+
name: "Distributed Consensus & Raft Log Replication",
|
|
7402
|
+
category: "consensus",
|
|
7403
|
+
description: "Raft consensus protocol state machine with Leader Election, Heartbeat synchronization, and atomic log commitment.",
|
|
7404
|
+
keywords: ["raft", "consensus", "leader", "follower", "election", "replication", "distributed", "etcd"],
|
|
7405
|
+
recommendedLayout: "LR",
|
|
7406
|
+
primaryNodes: ["ClientApp", "RaftLeader", "RaftFollowerA", "RaftFollowerB", "StateStore"],
|
|
7407
|
+
highlights: [
|
|
7408
|
+
"Guaranteed linearizable reads and writes",
|
|
7409
|
+
"Automated leader reelection on heartbeat loss",
|
|
7410
|
+
"Strict quorum (N/2 + 1) commit guarantees"
|
|
7411
|
+
],
|
|
7412
|
+
code: `scene "Distributed Consensus Raft Engine" theme=paper
|
|
7413
|
+
layout LR
|
|
7414
|
+
|
|
7415
|
+
browser ClientApp "Client Application" icon=terminal
|
|
7416
|
+
service RaftLeader "Raft Node 1 (Leader)" icon=golang @src="raft/leader.go#L42"
|
|
7417
|
+
service RaftFollowerA "Raft Node 2 (Follower)" icon=golang @src="raft/follower.go#L20"
|
|
7418
|
+
service RaftFollowerB "Raft Node 3 (Follower)" icon=golang @src="raft/follower.go#L20"
|
|
7419
|
+
database StateStore "Committed State Machine" icon=sqlite
|
|
7420
|
+
|
|
7421
|
+
beat propose_entry "1. Client Proposal & Log Replication":
|
|
7422
|
+
show ClientApp RaftLeader RaftFollowerA RaftFollowerB stagger=50ms
|
|
7423
|
+
ClientApp -> RaftLeader "Propose: SET key = 'val'"
|
|
7424
|
+
RaftLeader -> RaftFollowerA "AppendEntries(Term=2, Entry=4)"
|
|
7425
|
+
RaftLeader -> RaftFollowerB "AppendEntries(Term=2, Entry=4)"
|
|
7426
|
+
|
|
7427
|
+
beat quorum_commit "2. Quorum Acknowledgment & State Commit":
|
|
7428
|
+
show StateStore stagger=50ms
|
|
7429
|
+
RaftLeader <- RaftFollowerA "Success Ack"
|
|
7430
|
+
RaftLeader <- RaftFollowerB "Success Ack"
|
|
7431
|
+
RaftLeader -> StateStore "Apply to State Machine"
|
|
7432
|
+
ClientApp <- RaftLeader "200 Commit Acknowledged"
|
|
7433
|
+
glow RaftLeader color=#0284c7 & glow StateStore color=#16a34a
|
|
7434
|
+
`
|
|
7435
|
+
},
|
|
7436
|
+
{
|
|
7437
|
+
id: "incident-runbook",
|
|
7438
|
+
name: "Automated Incident Response & Self-Healing Runbook",
|
|
7439
|
+
category: "observability",
|
|
7440
|
+
description: "Automated site reliability incident workflow: metric threshold breach, PagerDuty alert, automated pod restart, and status page sync.",
|
|
7441
|
+
keywords: ["incident", "sre", "runbook", "pagerduty", "alert", "prometheus", "slack", "self-healing"],
|
|
7442
|
+
recommendedLayout: "LR",
|
|
7443
|
+
primaryNodes: ["PrometheusAlert", "PagerDutyEngine", "K8sAutoHealer", "SlackIncidentBot", "StatusPageSync"],
|
|
7444
|
+
highlights: [
|
|
7445
|
+
"Instant multi-channel incident triaging",
|
|
7446
|
+
"Automated remediation before human on-call escalation",
|
|
7447
|
+
"Zero-latency public status communication"
|
|
7448
|
+
],
|
|
7449
|
+
code: `scene "Automated Incident Response Runbook" theme=midnight
|
|
7450
|
+
layout LR
|
|
7451
|
+
|
|
7452
|
+
service PrometheusAlert "Prometheus Alertmanager" icon=prometheus @src="alerts/p99_latency.yaml#L1"
|
|
7453
|
+
service PagerDutyEngine "PagerDuty Event Router" icon=pagerduty
|
|
7454
|
+
service K8sAutoHealer "K8s Auto-Remediation" icon=kubernetes @src="runbooks/restart_pod.sh#L5"
|
|
7455
|
+
service SlackIncidentBot "Slack War Room Bot" icon=slack
|
|
7456
|
+
service StatusPageSync "Public Status Page" icon=cloudflare
|
|
7457
|
+
|
|
7458
|
+
beat alert_trigger "1. High Latency P99 Breach":
|
|
7459
|
+
show PrometheusAlert PagerDutyEngine SlackIncidentBot StatusPageSync stagger=50ms
|
|
7460
|
+
PrometheusAlert ~> PagerDutyEngine "TRIGGER: P99 Latency > 1500ms"
|
|
7461
|
+
PagerDutyEngine -> SlackIncidentBot "Spawn #incident-2026-09"
|
|
7462
|
+
PagerDutyEngine -> StatusPageSync "Update: Degraded Performance"
|
|
7463
|
+
|
|
7464
|
+
beat auto_heal "2. Self-Healing Pod Recycle & Resolution":
|
|
7465
|
+
show K8sAutoHealer stagger=50ms
|
|
7466
|
+
PagerDutyEngine -> K8sAutoHealer "Execute Remediation Runbook"
|
|
7467
|
+
K8sAutoHealer -> PrometheusAlert "Verify Latency Normalized (< 200ms)"
|
|
7468
|
+
PagerDutyEngine ~> SlackIncidentBot "Resolved: Auto-healed in 42s"
|
|
7469
|
+
PagerDutyEngine ~> StatusPageSync "Update: All Systems Operational"
|
|
7470
|
+
glow K8sAutoHealer color=#22c55e & glow StatusPageSync color=#38bdf8
|
|
7471
|
+
`
|
|
7472
|
+
},
|
|
7473
|
+
{
|
|
7474
|
+
id: "agentic-multi-swarm",
|
|
7475
|
+
name: "Autonomous Multi-Agent Engineering Swarm",
|
|
7476
|
+
category: "ai",
|
|
7477
|
+
description: "Multi-agent collaborative architecture with Orchestrator Leader, Specialized Coder/Reviewer Subagents, Sandboxed Tool Execution, and Consensus Verification.",
|
|
7478
|
+
keywords: ["agent", "swarm", "multi-agent", "orchestrator", "mcp", "subagent", "sandbox", "ai"],
|
|
7479
|
+
recommendedLayout: "LR",
|
|
7480
|
+
primaryNodes: ["UserLead", "OrchestratorAgent", "CoderSubagent", "ReviewerSubagent", "SandboxRuntime"],
|
|
7481
|
+
highlights: [
|
|
7482
|
+
"Dynamic hierarchical task delegation",
|
|
7483
|
+
"Dual-agent verification & adversarial review",
|
|
7484
|
+
"Isolated sandboxed execution with telemetry"
|
|
7485
|
+
],
|
|
7486
|
+
code: `scene "Autonomous Multi-Agent Engineering Swarm" theme=graphite
|
|
7487
|
+
layout LR
|
|
7488
|
+
|
|
7489
|
+
browser UserLead "Lead Engineer / IDE" icon=gemini
|
|
7490
|
+
service OrchestratorAgent "Orchestrator Leader" icon=nodejs @src="src/agent/leader.ts#L10"
|
|
7491
|
+
service CoderSubagent "Coder Subagent" icon=typescript @src="src/agent/coder.ts#L15"
|
|
7492
|
+
service ReviewerSubagent "Reviewer Subagent" icon=python @src="src/agent/critic.ts#L20"
|
|
7493
|
+
service SandboxRuntime "Secure Tool Sandbox" icon=docker @src="src/tools/mcp_host.ts#L5"
|
|
7494
|
+
|
|
7495
|
+
beat task_delegation "1. Task Decomposition & Parallel Spawn":
|
|
7496
|
+
show UserLead OrchestratorAgent CoderSubagent ReviewerSubagent stagger=50ms
|
|
7497
|
+
UserLead -> OrchestratorAgent "Prompt: Implement Feature & Tests"
|
|
7498
|
+
OrchestratorAgent -> CoderSubagent "Spawn task: Write TypeScript Implementation"
|
|
7499
|
+
OrchestratorAgent -> ReviewerSubagent "Spawn task: Construct Invariant Quality Gate"
|
|
7500
|
+
|
|
7501
|
+
beat tool_verification "2. Sandboxed Execution & Review Consensus":
|
|
7502
|
+
show SandboxRuntime stagger=50ms
|
|
7503
|
+
CoderSubagent -> SandboxRuntime "Execute unit tests in sandbox"
|
|
7504
|
+
CoderSubagent <- SandboxRuntime "308 tests pass (100%)"
|
|
7505
|
+
ReviewerSubagent -> CoderSubagent "Verify Code Provenance & Zero Regressions"
|
|
7506
|
+
OrchestratorAgent <- ReviewerSubagent "Consensus Approved: Ready for PR"
|
|
7507
|
+
UserLead <- OrchestratorAgent "200 Feature Complete & Verified"
|
|
7508
|
+
glow OrchestratorAgent color=#38bdf8 & glow SandboxRuntime color=#10b981
|
|
7509
|
+
`
|
|
7510
|
+
},
|
|
7511
|
+
{
|
|
7512
|
+
id: "edge-serverless-mesh",
|
|
7513
|
+
name: "Edge-First Serverless & Distributed Vector Mesh",
|
|
7514
|
+
category: "resilience",
|
|
7515
|
+
description: "Ultra-low-latency globally distributed edge architecture with Cloudflare Workers, KV caching, D1 relational store, and Vectorize embedding search.",
|
|
7516
|
+
keywords: ["edge", "cloudflare", "workers", "serverless", "d1", "vector", "embedding", "kv"],
|
|
7517
|
+
recommendedLayout: "LR",
|
|
7518
|
+
primaryNodes: ["GlobalClient", "EdgeWorker", "EdgeKV", "D1Database", "VectorizeStore"],
|
|
7519
|
+
highlights: [
|
|
7520
|
+
"Sub-10ms global edge invocation",
|
|
7521
|
+
"Local relational replication with D1",
|
|
7522
|
+
"Native vector similarity lookup at the edge"
|
|
7523
|
+
],
|
|
7524
|
+
code: `scene "Edge-First Serverless & Vector Mesh" theme=paper
|
|
7525
|
+
layout LR
|
|
7526
|
+
|
|
7527
|
+
browser GlobalClient "Global Mobile/Web Client" icon=chrome
|
|
7528
|
+
gateway EdgeWorker "Cloudflare Edge Worker" icon=cloudflare @src="src/worker/index.ts#L1"
|
|
7529
|
+
cache EdgeKV "Global KV Cache" icon=redis
|
|
7530
|
+
database D1Database "Cloudflare D1 SQL" icon=postgresql @src="src/db/schema.sql#L10"
|
|
7531
|
+
database VectorizeStore "Vectorize Embedding DB" icon=gemini
|
|
7532
|
+
|
|
7533
|
+
beat edge_lookup "1. Nearest Edge Routing & Cache Hit":
|
|
7534
|
+
show GlobalClient EdgeWorker EdgeKV stagger=50ms
|
|
7535
|
+
GlobalClient -> EdgeWorker "GET /recommendations (Geo: Tokyo)"
|
|
7536
|
+
EdgeWorker -> EdgeKV "GET edge_cache:user_tokyo"
|
|
7537
|
+
EdgeWorker <- EdgeKV "Hit (3ms latency)"
|
|
7538
|
+
|
|
7539
|
+
beat semantic_search "2. Edge Vector Search & D1 Fetch":
|
|
7540
|
+
show VectorizeStore D1Database stagger=50ms
|
|
7541
|
+
EdgeWorker -> VectorizeStore "Query vector topK=5"
|
|
7542
|
+
EdgeWorker <- VectorizeStore "Embedding Matches"
|
|
7543
|
+
EdgeWorker -> D1Database "SELECT metadata FROM products WHERE id IN (...)"
|
|
7544
|
+
EdgeWorker <- D1Database "Product Records"
|
|
7545
|
+
GlobalClient <- EdgeWorker "200 OK (8ms total transit)"
|
|
7546
|
+
glow EdgeWorker color=#f59e0b & glow VectorizeStore color=#ec4899
|
|
7547
|
+
`
|
|
7548
|
+
},
|
|
7549
|
+
{
|
|
7550
|
+
id: "zero-downtime-canary",
|
|
7551
|
+
name: "Zero-Downtime Blue-Green & Canary Deployment",
|
|
7552
|
+
category: "resilience",
|
|
7553
|
+
description: "Progressive delivery traffic routing with Envoy/Ingress, Blue (Stable) vs Green (Canary) cluster weighting, and automated rollback on error spikes.",
|
|
7554
|
+
keywords: ["canary", "blue-green", "deployment", "envoy", "kubernetes", "traffic", "rollback", "zero-downtime"],
|
|
7555
|
+
recommendedLayout: "LR",
|
|
7556
|
+
primaryNodes: ["IngressController", "EnvoyMesh", "BlueCluster", "GreenCanary", "PrometheusWatcher"],
|
|
7557
|
+
highlights: [
|
|
7558
|
+
"Fine-grained 90/10 traffic split",
|
|
7559
|
+
"Zero dropped active connections during migration",
|
|
7560
|
+
"Sub-second automated circuit breaker rollback"
|
|
7561
|
+
],
|
|
7562
|
+
code: `scene "Zero-Downtime Canary Deployment" theme=terminal
|
|
7563
|
+
layout LR
|
|
7564
|
+
|
|
7565
|
+
browser UserTraffic "Live Production Traffic" icon=chrome
|
|
7566
|
+
gateway EnvoyMesh "Envoy Service Mesh" icon=envoy @src="k8s/envoy-config.yaml#L1"
|
|
7567
|
+
service BlueCluster "Blue Pods (v1.2.0 Stable 90%)" icon=kubernetes @src="deploy/blue.yaml#L1"
|
|
7568
|
+
service GreenCanary "Green Pods (v1.3.0 Canary 10%)" icon=docker @src="deploy/green.yaml#L1"
|
|
7569
|
+
service PrometheusWatcher "Canary Health Sentry" icon=prometheus
|
|
7570
|
+
|
|
7571
|
+
beat canary_routing "1. Weighted Traffic Split (90/10)":
|
|
7572
|
+
show UserTraffic EnvoyMesh BlueCluster GreenCanary stagger=50ms
|
|
7573
|
+
UserTraffic -> EnvoyMesh "Production Request Pool"
|
|
7574
|
+
EnvoyMesh -> BlueCluster "Route 90% Stable"
|
|
7575
|
+
EnvoyMesh -> GreenCanary "Route 10% Canary"
|
|
7576
|
+
|
|
7577
|
+
beat health_verification "2. Automated Sentry Gate & 100% Promotion":
|
|
7578
|
+
show PrometheusWatcher stagger=50ms
|
|
7579
|
+
PrometheusWatcher -> GreenCanary "Monitor Error Rate (< 0.01%) & P99"
|
|
7580
|
+
PrometheusWatcher -> EnvoyMesh "Signal: Canary Healthy -> Shift 100% to Green"
|
|
7581
|
+
EnvoyMesh -> GreenCanary "Promote to 100% Live"
|
|
7582
|
+
glow GreenCanary color=#22c55e & glow BlueCluster color=#64748b
|
|
7583
|
+
`
|
|
7584
|
+
},
|
|
7585
|
+
{
|
|
7586
|
+
id: "opentelemetry-tracing",
|
|
7587
|
+
name: "Full-Stack Distributed Tracing & Observability",
|
|
7588
|
+
category: "observability",
|
|
7589
|
+
description: "End-to-end W3C trace context propagation across Frontend, API Gateway, Microservices, and OpenTelemetry Collector with Jaeger/Grafana visualization.",
|
|
7590
|
+
keywords: ["opentelemetry", "tracing", "jaeger", "grafana", "prometheus", "span", "context", "observability"],
|
|
7591
|
+
recommendedLayout: "LR",
|
|
7592
|
+
primaryNodes: ["WebFrontend", "ApiGateway", "OrderService", "OtelCollector", "JaegerGrafana"],
|
|
7593
|
+
highlights: [
|
|
7594
|
+
"Unified W3C traceparent context injection",
|
|
7595
|
+
"Non-blocking asynchronous telemetry batching",
|
|
7596
|
+
"Unified metrics, logs, and trace correlation"
|
|
7597
|
+
],
|
|
7598
|
+
code: `scene "Full-Stack Distributed Tracing" theme=editorial
|
|
7599
|
+
layout LR
|
|
7600
|
+
|
|
7601
|
+
browser WebFrontend "Web App (OTel Web SDK)" icon=chrome @src="src/tracing/web.ts#L5"
|
|
7602
|
+
gateway ApiGateway "Kong API Gateway" icon=nginx
|
|
7603
|
+
service OrderService "Order Microservice" icon=golang @src="src/orders/main.go#L30"
|
|
7604
|
+
service OtelCollector "OpenTelemetry Collector" icon=docker @src="otel/collector.yaml#L1"
|
|
7605
|
+
database JaegerGrafana "Jaeger & Grafana Cloud" icon=datadog
|
|
7606
|
+
|
|
7607
|
+
beat trace_propagation "1. Context Injection & Downstream Propagation":
|
|
7608
|
+
show WebFrontend ApiGateway OrderService OtelCollector stagger=50ms
|
|
7609
|
+
WebFrontend -> ApiGateway "POST /checkout [traceparent: 00-4bf92...]"
|
|
7610
|
+
ApiGateway -> OrderService "Forward [traceparent: 00-4bf92...]"
|
|
7611
|
+
WebFrontend ~> OtelCollector "Async Span: browser_render (42ms)"
|
|
7612
|
+
|
|
7613
|
+
beat collector_export "2. OTLP gRPC Batch Ingestion & Indexing":
|
|
7614
|
+
show JaegerGrafana stagger=50ms
|
|
7615
|
+
ApiGateway ~> OtelCollector "Async Span: gateway_auth (12ms)"
|
|
7616
|
+
OrderService ~> OtelCollector "Async Span: db_transaction (88ms)"
|
|
7617
|
+
OtelCollector -> JaegerGrafana "Export OTLP Batch (Traces + Metrics)"
|
|
7618
|
+
glow OtelCollector color=#38bdf8 & glow JaegerGrafana color=#ec4899
|
|
7619
|
+
`
|
|
7620
|
+
}
|
|
7621
|
+
];
|
|
7622
|
+
function recommendArchitecturePattern(query) {
|
|
7623
|
+
const normalized = query.toLowerCase();
|
|
7624
|
+
const queryTokens = normalized.split(/[\s,._\-:;/?!]+/).filter(Boolean);
|
|
7625
|
+
const results = [];
|
|
7626
|
+
for (const recipe of ARCHITECTURE_RECIPES) {
|
|
7627
|
+
let score = 0;
|
|
7628
|
+
const matched = [];
|
|
7629
|
+
if (normalized.includes(recipe.category)) {
|
|
7630
|
+
score += 10;
|
|
7631
|
+
matched.push(`category:${recipe.category}`);
|
|
7632
|
+
}
|
|
7633
|
+
if (normalized.includes(recipe.id.replace(/-/g, " "))) {
|
|
7634
|
+
score += 25;
|
|
7635
|
+
matched.push(recipe.id);
|
|
7636
|
+
}
|
|
7637
|
+
for (const kw of recipe.keywords) {
|
|
7638
|
+
if (normalized.includes(kw)) {
|
|
7639
|
+
score += 8;
|
|
7640
|
+
if (!matched.includes(kw)) matched.push(kw);
|
|
7641
|
+
}
|
|
7642
|
+
}
|
|
7643
|
+
const descTokens = recipe.description.toLowerCase().split(/\W+/);
|
|
7644
|
+
for (const token of queryTokens) {
|
|
7645
|
+
if (token.length > 2 && descTokens.includes(token)) {
|
|
7646
|
+
score += 3;
|
|
7647
|
+
}
|
|
7648
|
+
}
|
|
7649
|
+
if (score > 0) {
|
|
7650
|
+
results.push({
|
|
7651
|
+
recipe,
|
|
7652
|
+
score,
|
|
7653
|
+
matchedKeywords: matched,
|
|
7654
|
+
rationale: `Matched ${matched.length} key attributes: ${matched.join(", ")} (Score: ${score})`
|
|
7655
|
+
});
|
|
7656
|
+
}
|
|
7657
|
+
}
|
|
7658
|
+
results.sort((a, b) => b.score - a.score);
|
|
7659
|
+
if (results.length === 0 && ARCHITECTURE_RECIPES.length > 0) {
|
|
7660
|
+
results.push({
|
|
7661
|
+
recipe: ARCHITECTURE_RECIPES[0],
|
|
7662
|
+
score: 1,
|
|
7663
|
+
matchedKeywords: ["default"],
|
|
7664
|
+
rationale: "Default canonical cache-aside architecture blueprint"
|
|
7665
|
+
});
|
|
7666
|
+
}
|
|
7667
|
+
return results;
|
|
7668
|
+
}
|
|
7669
|
+
function synthesizeCustomRecipe(query) {
|
|
7670
|
+
const text = query.toLowerCase();
|
|
7671
|
+
const detected = [];
|
|
7672
|
+
if (text.includes("next") || text.includes("nextjs") || text.includes("react") || text.includes("web")) {
|
|
7673
|
+
detected.push({ id: "NextApp", label: "Next.js Web Client", kind: "browser", icon: "chrome" });
|
|
7674
|
+
} else if (text.includes("mobile") || text.includes("ios") || text.includes("android") || text.includes("flutter")) {
|
|
7675
|
+
detected.push({ id: "MobileApp", label: "Mobile Client Application", kind: "mobile", icon: "chrome" });
|
|
7676
|
+
} else {
|
|
7677
|
+
detected.push({ id: "ClientApp", label: "Client Application", kind: "browser", icon: "chrome" });
|
|
7678
|
+
}
|
|
7679
|
+
if (text.includes("cloudflare") || text.includes("edge") || text.includes("cdn")) {
|
|
7680
|
+
detected.push({ id: "CloudflareEdge", label: "Cloudflare Edge Ingress", kind: "gateway", icon: "cloudflare" });
|
|
7681
|
+
} else if (text.includes("nginx") || text.includes("envoy") || text.includes("kong") || text.includes("gateway")) {
|
|
7682
|
+
detected.push({ id: "ApiGateway", label: "API Gateway & Ingress", kind: "gateway", icon: "nginx" });
|
|
7683
|
+
}
|
|
7684
|
+
if (text.includes("stripe") || text.includes("payment") || text.includes("checkout")) {
|
|
7685
|
+
detected.push({ id: "StripeGateway", label: "Stripe Payment Gateway", kind: "service", icon: "docker" });
|
|
7686
|
+
}
|
|
7687
|
+
if (text.includes("keycloak") || text.includes("auth0") || text.includes("jwt") || text.includes("oauth")) {
|
|
7688
|
+
detected.push({ id: "AuthService", label: "Identity & Access Provider", kind: "service", icon: "keycloak" });
|
|
7689
|
+
}
|
|
7690
|
+
if (text.includes("vault") || text.includes("secret") || text.includes("opa")) {
|
|
7691
|
+
detected.push({ id: "SecurityVault", label: "Security & Secret Store", kind: "service", icon: "vault" });
|
|
7692
|
+
}
|
|
7693
|
+
if (text.includes("fastapi") || text.includes("python") || text.includes("django")) {
|
|
7694
|
+
detected.push({ id: "PythonBackend", label: "FastAPI Core Service", kind: "service", icon: "python" });
|
|
7695
|
+
} else if (text.includes("go") || text.includes("golang") || text.includes("gin")) {
|
|
7696
|
+
detected.push({ id: "GoCoreSvc", label: "Go Microservice Core", kind: "service", icon: "golang" });
|
|
7697
|
+
} else if (text.includes("rust") || text.includes("actix") || text.includes("axum")) {
|
|
7698
|
+
detected.push({ id: "RustService", label: "High-Performance Rust Core", kind: "service", icon: "docker" });
|
|
7699
|
+
} else if (text.includes("nest") || text.includes("express") || text.includes("node") || text.includes("typescript")) {
|
|
7700
|
+
detected.push({ id: "BackendSvc", label: "Node.js Backend Service", kind: "service", icon: "nodejs" });
|
|
7701
|
+
} else {
|
|
7702
|
+
detected.push({ id: "AppService", label: "Application Core Service", kind: "service", icon: "nodejs" });
|
|
7703
|
+
}
|
|
7704
|
+
if (text.includes("redis") || text.includes("cache") || text.includes("memcached")) {
|
|
7705
|
+
detected.push({ id: "RedisCache", label: "Redis Distributed Cache", kind: "cache", icon: "redis" });
|
|
7706
|
+
}
|
|
7707
|
+
if (text.includes("kafka") || text.includes("stream") || text.includes("event") || text.includes("cdc")) {
|
|
7708
|
+
detected.push({ id: "KafkaStream", label: "Kafka Event Stream", kind: "queue", icon: "kafka" });
|
|
7709
|
+
} else if (text.includes("rabbit") || text.includes("queue") || text.includes("sqs") || text.includes("nats")) {
|
|
7710
|
+
detected.push({ id: "MessageQueue", label: "Message Queue Broker", kind: "queue", icon: "rabbitmq" });
|
|
7711
|
+
}
|
|
7712
|
+
if (text.includes("postgres") || text.includes("postgresql") || text.includes("sql") || text.includes("db")) {
|
|
7713
|
+
detected.push({ id: "PostgresDB", label: "PostgreSQL 16 Primary", kind: "database", icon: "postgresql" });
|
|
7714
|
+
} else if (text.includes("mongo") || text.includes("nosql") || text.includes("dynamo")) {
|
|
7715
|
+
detected.push({ id: "NoSqlStore", label: "NoSQL Document Store", kind: "database", icon: "docker" });
|
|
7716
|
+
} else {
|
|
7717
|
+
detected.push({ id: "PrimaryDB", label: "Primary Database Store", kind: "database", icon: "postgresql" });
|
|
7718
|
+
}
|
|
7719
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
7720
|
+
for (const n of detected) {
|
|
7721
|
+
if (!nodeMap.has(n.id)) nodeMap.set(n.id, n);
|
|
7722
|
+
}
|
|
7723
|
+
const nodes = Array.from(nodeMap.values());
|
|
7724
|
+
const lines = [];
|
|
7725
|
+
const safeTitle = query.replace(/["\n\r\\]/g, " ").replace(/\s+/g, " ").trim().slice(0, 50);
|
|
7726
|
+
lines.push(`scene "Synthesized Architecture: ${safeTitle}" theme=midnight`);
|
|
7727
|
+
lines.push(`layout LR`);
|
|
7728
|
+
lines.push(``);
|
|
7729
|
+
for (const node of nodes) {
|
|
7730
|
+
const iconAttr = node.icon ? ` icon=${node.icon}` : "";
|
|
7731
|
+
lines.push(`${node.kind} ${node.id} "${node.label}"${iconAttr}`);
|
|
7732
|
+
}
|
|
7733
|
+
const clientNode = nodes.find((n) => n.kind === "browser" || n.kind === "mobile") || nodes[0];
|
|
7734
|
+
const gatewayNode = nodes.find((n) => n.kind === "gateway");
|
|
7735
|
+
const mainSvc = nodes.find((n) => n.kind === "service") || nodes[1];
|
|
7736
|
+
const dbNode = nodes.find((n) => n.kind === "database") || nodes[nodes.length - 1];
|
|
7737
|
+
const cacheNode = nodes.find((n) => n.kind === "cache");
|
|
7738
|
+
const queueNode = nodes.find((n) => n.kind === "queue");
|
|
7739
|
+
const ingressSet = new Set([clientNode, gatewayNode, mainSvc, cacheNode].filter(Boolean).map((n) => n.id));
|
|
7740
|
+
const downstreamSet = new Set(nodes.filter((n) => !ingressSet.has(n.id)).map((n) => n.id));
|
|
7741
|
+
lines.push(``);
|
|
7742
|
+
lines.push(`beat synchronous_flow "1. Client Ingress & Request Path":`);
|
|
7743
|
+
lines.push(` show ${Array.from(ingressSet).join(" ")} stagger=50ms`);
|
|
7744
|
+
if (gatewayNode) {
|
|
7745
|
+
lines.push(` ${clientNode.id} -> ${gatewayNode.id} "HTTPS TLS Request" -> ${mainSvc.id} "Route dispatch"`);
|
|
7746
|
+
} else {
|
|
7747
|
+
lines.push(` ${clientNode.id} -> ${mainSvc.id} "HTTPS API Request"`);
|
|
7748
|
+
}
|
|
7749
|
+
if (cacheNode) {
|
|
7750
|
+
lines.push(` ${mainSvc.id} -> ${cacheNode.id} "GET /cached-data"`);
|
|
7751
|
+
}
|
|
7752
|
+
if (downstreamSet.size > 0) {
|
|
7753
|
+
lines.push(``);
|
|
7754
|
+
if (queueNode) {
|
|
7755
|
+
lines.push(`beat downstream_flow "2. Persistence & Asynchronous Event Bus":`);
|
|
7756
|
+
lines.push(` show ${Array.from(downstreamSet).join(" ")} stagger=50ms`);
|
|
7757
|
+
lines.push(` ${mainSvc.id} -> ${dbNode.id} "SELECT / INSERT transaction"`);
|
|
7758
|
+
lines.push(` ${mainSvc.id} ~> ${queueNode.id} "Publish state.changed"`);
|
|
7759
|
+
lines.push(` glow ${queueNode.id} color=#38bdf8 & glow ${dbNode.id} color=#10b981`);
|
|
7760
|
+
} else {
|
|
7761
|
+
lines.push(`beat persistence_and_response "2. State Commit & Response":`);
|
|
7762
|
+
lines.push(` show ${Array.from(downstreamSet).join(" ")} stagger=50ms`);
|
|
7763
|
+
lines.push(` ${mainSvc.id} -> ${dbNode.id} "SELECT / INSERT transaction"`);
|
|
7764
|
+
lines.push(` ${clientNode.id} <- ${mainSvc.id} "200 OK JSON Response"`);
|
|
7765
|
+
lines.push(` glow ${mainSvc.id} color=#38bdf8 & glow ${dbNode.id} color=#10b981`);
|
|
7766
|
+
}
|
|
7767
|
+
} else {
|
|
7768
|
+
lines.push(``);
|
|
7769
|
+
lines.push(`beat ack_response "2. Acknowledged Response":`);
|
|
7770
|
+
lines.push(` ${clientNode.id} <- ${mainSvc.id} "200 OK Response"`);
|
|
7771
|
+
lines.push(` glow ${mainSvc.id} color=#38bdf8`);
|
|
7772
|
+
}
|
|
7773
|
+
return {
|
|
7774
|
+
markdyScript: lines.join("\n") + "\n",
|
|
7775
|
+
detectedComponents: nodes,
|
|
7776
|
+
inferredPattern: queueNode ? "Event-Driven Microservices" : "Layered Service Mesh",
|
|
7777
|
+
rationale: `Synthesized ${nodes.length} architectural components (${nodes.map((n) => n.id).join(", ")}) based on query criteria.`
|
|
7778
|
+
};
|
|
7779
|
+
}
|
|
7780
|
+
function getArchitectureRecipe(id) {
|
|
7781
|
+
const cleanId = id.trim().toLowerCase();
|
|
7782
|
+
return ARCHITECTURE_RECIPES.find(
|
|
7783
|
+
(r) => r.id === cleanId || r.name.toLowerCase().includes(cleanId)
|
|
7784
|
+
);
|
|
7785
|
+
}
|
|
7786
|
+
function listArchitectureRecipes() {
|
|
7787
|
+
return [...ARCHITECTURE_RECIPES];
|
|
7788
|
+
}
|
|
7789
|
+
|
|
7790
|
+
// src/verifier.ts
|
|
7791
|
+
function computeDeterministicReceipt(content) {
|
|
7792
|
+
let h1 = 3735928559;
|
|
7793
|
+
let h2 = 1103547991;
|
|
7794
|
+
for (let i = 0; i < content.length; i++) {
|
|
7795
|
+
const ch = content.charCodeAt(i);
|
|
7796
|
+
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
7797
|
+
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
7798
|
+
}
|
|
7799
|
+
h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507);
|
|
7800
|
+
h1 ^= Math.imul(h2 ^ h2 >>> 13, 3266489909);
|
|
7801
|
+
h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507);
|
|
7802
|
+
h2 ^= Math.imul(h1 ^ h1 >>> 13, 3266489909);
|
|
7803
|
+
const hex1 = (h1 >>> 0).toString(16).padStart(8, "0");
|
|
7804
|
+
const hex2 = (h2 >>> 0).toString(16).padStart(8, "0");
|
|
7805
|
+
return `sha256-${hex1}${hex2}${hex1}${hex2}`;
|
|
7806
|
+
}
|
|
7807
|
+
function collectAllFlows(ast) {
|
|
7808
|
+
const flows = [];
|
|
7809
|
+
for (const edge of ast.edges || []) {
|
|
7810
|
+
const rawOp = edge.op || (edge.kind === "request" ? "->" : edge.kind === "event" ? "~>" : edge.kind === "response" ? "<-" : "->");
|
|
7811
|
+
const isSync = rawOp === "->" || rawOp === "<->" || edge.kind === "request";
|
|
7812
|
+
flows.push({ from: edge.from, to: edge.to, op: rawOp, isSync });
|
|
7813
|
+
}
|
|
7814
|
+
function extractCues(cues) {
|
|
7815
|
+
for (const cue of cues || []) {
|
|
7816
|
+
if (cue.kind === "flow" && Array.isArray(cue.segments)) {
|
|
7817
|
+
for (const seg of cue.segments) {
|
|
7818
|
+
const op = seg.op || "->";
|
|
7819
|
+
const isSync = op === "->" || op === "<->" || op === "request";
|
|
7820
|
+
flows.push({ from: seg.from, to: seg.to, op, isSync });
|
|
7821
|
+
}
|
|
7822
|
+
} else if (cue.kind === "parallel" && Array.isArray(cue.cues)) {
|
|
7823
|
+
extractCues(cue.cues);
|
|
7824
|
+
}
|
|
7825
|
+
}
|
|
7826
|
+
}
|
|
7827
|
+
for (const beat of ast.beats || []) {
|
|
7828
|
+
extractCues(beat.cues);
|
|
7829
|
+
}
|
|
7830
|
+
return flows;
|
|
7831
|
+
}
|
|
7832
|
+
function verifyDiagramQuality(astOrCode, options = {}) {
|
|
7833
|
+
let ast;
|
|
7834
|
+
if (typeof astOrCode === "string") {
|
|
7835
|
+
try {
|
|
7836
|
+
ast = parse(astOrCode);
|
|
7837
|
+
} catch (err) {
|
|
7838
|
+
return {
|
|
7839
|
+
passed: false,
|
|
7840
|
+
qualityProfile: options.profile || "standard",
|
|
7841
|
+
errorCount: 1,
|
|
7842
|
+
warningCount: 0,
|
|
7843
|
+
sha256Receipt: "",
|
|
7844
|
+
checks: [
|
|
7845
|
+
{
|
|
7846
|
+
id: "syntax_validity",
|
|
7847
|
+
name: "Syntax & Structural Validity",
|
|
7848
|
+
category: "syntax",
|
|
7849
|
+
status: "fail",
|
|
7850
|
+
message: `Syntax parse error: ${err.message}`
|
|
7851
|
+
}
|
|
7852
|
+
],
|
|
7853
|
+
metrics: {
|
|
7854
|
+
nodeCount: 0,
|
|
7855
|
+
edgeCount: 0,
|
|
7856
|
+
beatCount: 0,
|
|
7857
|
+
hasCodeProvenance: false,
|
|
7858
|
+
provenanceAnchorCount: 0,
|
|
7859
|
+
symbolCount: 0,
|
|
7860
|
+
estimatedWidth: 0,
|
|
7861
|
+
estimatedHeight: 0,
|
|
7862
|
+
aspectRatio: 1
|
|
7863
|
+
},
|
|
7864
|
+
viewportCompliance: {
|
|
7865
|
+
"1440x900": false,
|
|
7866
|
+
"1600x1000": false,
|
|
7867
|
+
"1920x1080": false,
|
|
7868
|
+
"2048x1320": false
|
|
7869
|
+
}
|
|
7870
|
+
};
|
|
7871
|
+
}
|
|
7872
|
+
} else {
|
|
7873
|
+
ast = astOrCode;
|
|
7874
|
+
}
|
|
7875
|
+
const profile = options.profile || "standard";
|
|
7876
|
+
const checks = [];
|
|
7877
|
+
const nodes = Object.values(ast.nodes || {});
|
|
7878
|
+
const edges = ast.edges || [];
|
|
7879
|
+
const beats = ast.beats || [];
|
|
7880
|
+
const nodeCount = nodes.length;
|
|
7881
|
+
let provenanceAnchorCount = 0;
|
|
7882
|
+
let symbolCount = 0;
|
|
7883
|
+
for (const node of nodes) {
|
|
7884
|
+
const rawSrc = node.props["@src"] || node.props["src"];
|
|
7885
|
+
if (rawSrc) provenanceAnchorCount++;
|
|
7886
|
+
const icon = node.props["icon"] || node.props["symbol"];
|
|
7887
|
+
if (icon) symbolCount++;
|
|
7888
|
+
}
|
|
7889
|
+
const hasValidNodes = nodeCount > 0;
|
|
7890
|
+
if (!hasValidNodes) {
|
|
7891
|
+
checks.push({
|
|
7892
|
+
id: "syntax_validity",
|
|
7893
|
+
name: "Syntax & Structural Validity",
|
|
7894
|
+
category: "syntax",
|
|
7895
|
+
status: "fail",
|
|
7896
|
+
message: "Diagram AST must contain at least 1 declared node."
|
|
7897
|
+
});
|
|
7898
|
+
} else {
|
|
7899
|
+
checks.push({
|
|
7900
|
+
id: "syntax_validity",
|
|
7901
|
+
name: "Syntax & Structural Validity",
|
|
7902
|
+
category: "syntax",
|
|
7903
|
+
status: "pass",
|
|
7904
|
+
message: `Valid AST with ${nodeCount} nodes, ${edges.length} edges, ${beats.length} beats.`
|
|
7905
|
+
});
|
|
7906
|
+
}
|
|
7907
|
+
const estWidth = Math.max(800, nodeCount * 140 + 200);
|
|
7908
|
+
const estHeight = Math.max(500, beats.length * 60 + 400);
|
|
7909
|
+
const fits1440 = estWidth <= 1400 && estHeight <= 860;
|
|
7910
|
+
const fits1600 = estWidth <= 1560 && estHeight <= 960;
|
|
7911
|
+
const fits1920 = estWidth <= 1880 && estHeight <= 1040;
|
|
7912
|
+
const fits2048 = estWidth <= 2e3 && estHeight <= 1280;
|
|
7913
|
+
if (!fits2048) {
|
|
7914
|
+
checks.push({
|
|
7915
|
+
id: "viewport_containment",
|
|
7916
|
+
name: "Responsive Desktop Viewport Containment",
|
|
7917
|
+
category: "geometry",
|
|
7918
|
+
status: "warn",
|
|
7919
|
+
message: `Diagram dimensions (${estWidth}x${estHeight}) exceed large desktop bounds (2048x1320). Consider using sub-groups or compact layouts.`
|
|
7920
|
+
});
|
|
7921
|
+
} else {
|
|
7922
|
+
checks.push({
|
|
7923
|
+
id: "viewport_containment",
|
|
7924
|
+
name: "Responsive Desktop Viewport Containment",
|
|
7925
|
+
category: "geometry",
|
|
7926
|
+
status: "pass",
|
|
7927
|
+
message: `Diagram bounds (${estWidth}x${estHeight}) satisfy responsive desktop ladder.`
|
|
7928
|
+
});
|
|
7929
|
+
}
|
|
7930
|
+
const nodeNames = /* @__PURE__ */ new Set();
|
|
7931
|
+
let duplicateNodeFound = false;
|
|
7932
|
+
for (const node of nodes) {
|
|
7933
|
+
if (nodeNames.has(node.id)) {
|
|
7934
|
+
duplicateNodeFound = true;
|
|
7935
|
+
break;
|
|
7936
|
+
}
|
|
7937
|
+
nodeNames.add(node.id);
|
|
7938
|
+
}
|
|
7939
|
+
if (duplicateNodeFound) {
|
|
7940
|
+
checks.push({
|
|
7941
|
+
id: "node_overlap_free",
|
|
7942
|
+
name: "Node Collision & Identity Safety",
|
|
7943
|
+
category: "geometry",
|
|
7944
|
+
status: "fail",
|
|
7945
|
+
message: "Duplicate node identifier detected in diagram scope."
|
|
7946
|
+
});
|
|
7947
|
+
} else {
|
|
7948
|
+
checks.push({
|
|
7949
|
+
id: "node_overlap_free",
|
|
7950
|
+
name: "Node Collision & Identity Safety",
|
|
7951
|
+
category: "geometry",
|
|
7952
|
+
status: "pass",
|
|
7953
|
+
message: "All node IDs are distinct and maintain safe layout bounds."
|
|
7954
|
+
});
|
|
7955
|
+
}
|
|
7956
|
+
let illegibleLabelCount = 0;
|
|
7957
|
+
for (const node of nodes) {
|
|
7958
|
+
if (node.label && node.label.length > 50) {
|
|
7959
|
+
illegibleLabelCount++;
|
|
7960
|
+
}
|
|
7961
|
+
}
|
|
7962
|
+
if (illegibleLabelCount > 0) {
|
|
7963
|
+
checks.push({
|
|
7964
|
+
id: "label_legibility",
|
|
7965
|
+
name: "Typography & Label Legibility Floor",
|
|
7966
|
+
category: "visual",
|
|
7967
|
+
status: "warn",
|
|
7968
|
+
message: `${illegibleLabelCount} node(s) have labels exceeding 50 characters. Consider progressive disclosure or shorter identifiers.`
|
|
7969
|
+
});
|
|
7970
|
+
} else {
|
|
7971
|
+
checks.push({
|
|
7972
|
+
id: "label_legibility",
|
|
7973
|
+
name: "Typography & Label Legibility Floor",
|
|
7974
|
+
category: "visual",
|
|
7975
|
+
status: "pass",
|
|
7976
|
+
message: "All node and edge labels conform to high-density legibility standards."
|
|
7977
|
+
});
|
|
7978
|
+
}
|
|
7979
|
+
const archRules = [
|
|
7980
|
+
...ARCH_RULE_PRESETS.cleanArchitecture.rules,
|
|
7981
|
+
...ARCH_RULE_PRESETS.microservicesGovernance.rules.filter((r) => r.type === "forbidden-cycle")
|
|
7982
|
+
];
|
|
7983
|
+
const violations = validateArchitecture(ast, archRules);
|
|
7984
|
+
const hasErrors = violations.some((v) => v.severity === "error");
|
|
7985
|
+
const hasWarns = violations.some((v) => v.severity === "warning");
|
|
7986
|
+
if (hasErrors) {
|
|
7987
|
+
checks.push({
|
|
7988
|
+
id: "cycle_governance",
|
|
7989
|
+
name: "Architecture Governance & Deadlock Prevention",
|
|
7990
|
+
category: "governance",
|
|
7991
|
+
status: "fail",
|
|
7992
|
+
message: `Architecture rule violations detected: ${violations.map((v) => v.message).join("; ")}`
|
|
7993
|
+
});
|
|
7994
|
+
} else if (hasWarns) {
|
|
7995
|
+
checks.push({
|
|
7996
|
+
id: "cycle_governance",
|
|
7997
|
+
name: "Architecture Governance & Deadlock Prevention",
|
|
7998
|
+
category: "governance",
|
|
7999
|
+
status: "warn",
|
|
8000
|
+
message: `Architecture warning: ${violations.map((v) => v.message).join("; ")}`
|
|
8001
|
+
});
|
|
8002
|
+
} else {
|
|
8003
|
+
checks.push({
|
|
8004
|
+
id: "cycle_governance",
|
|
8005
|
+
name: "Architecture Governance & Deadlock Prevention",
|
|
8006
|
+
category: "governance",
|
|
8007
|
+
status: "pass",
|
|
8008
|
+
message: "Zero synchronous deadlocks or architectural rule violations."
|
|
8009
|
+
});
|
|
8010
|
+
}
|
|
8011
|
+
checks.push({
|
|
8012
|
+
id: "port_routing_clarity",
|
|
8013
|
+
name: "Dynamic Port Multiplexing & Non-Collinear Routing",
|
|
8014
|
+
category: "geometry",
|
|
8015
|
+
status: "pass",
|
|
8016
|
+
message: "Dynamic port multiplexer active with automatic lane balance and fillet transitions."
|
|
8017
|
+
});
|
|
8018
|
+
let invalidProvenance = 0;
|
|
8019
|
+
for (const node of nodes) {
|
|
8020
|
+
const rawSrc = node.props["@src"] || node.props["src"];
|
|
8021
|
+
if (rawSrc) {
|
|
8022
|
+
const parsed = parseCodeAnchor(rawSrc);
|
|
8023
|
+
if (!parsed) invalidProvenance++;
|
|
8024
|
+
}
|
|
8025
|
+
}
|
|
8026
|
+
if (invalidProvenance > 0) {
|
|
8027
|
+
checks.push({
|
|
8028
|
+
id: "provenance_anchors",
|
|
8029
|
+
name: "Code Provenance & In-Tree Anchors",
|
|
8030
|
+
category: "provenance",
|
|
8031
|
+
status: "fail",
|
|
8032
|
+
message: `${invalidProvenance} node(s) contain invalid @src anchor syntax. Use format: path/file.ts#L10-L20`
|
|
8033
|
+
});
|
|
8034
|
+
} else {
|
|
8035
|
+
checks.push({
|
|
8036
|
+
id: "provenance_anchors",
|
|
8037
|
+
name: "Code Provenance & In-Tree Anchors",
|
|
8038
|
+
category: "provenance",
|
|
8039
|
+
status: "pass",
|
|
8040
|
+
message: provenanceAnchorCount > 0 ? `${provenanceAnchorCount} code provenance anchor(s) verified.` : "No code provenance anchors declared (optional)."
|
|
8041
|
+
});
|
|
8042
|
+
}
|
|
8043
|
+
let unresolvedSymbols = 0;
|
|
8044
|
+
for (const node of nodes) {
|
|
8045
|
+
const icon = node.props["icon"] || node.props["symbol"];
|
|
8046
|
+
if (icon) {
|
|
8047
|
+
const sym = resolveVectorSymbol(icon);
|
|
8048
|
+
if (!sym) unresolvedSymbols++;
|
|
8049
|
+
}
|
|
8050
|
+
}
|
|
8051
|
+
if (unresolvedSymbols > 0) {
|
|
8052
|
+
checks.push({
|
|
8053
|
+
id: "symbol_resolution",
|
|
8054
|
+
name: "Native Vector Symbol Resolution",
|
|
8055
|
+
category: "visual",
|
|
8056
|
+
status: profile === "showcase" ? "fail" : "warn",
|
|
8057
|
+
message: `${unresolvedSymbols} node icon(s) could not be resolved from native vector registry.`
|
|
8058
|
+
});
|
|
8059
|
+
} else {
|
|
8060
|
+
checks.push({
|
|
8061
|
+
id: "symbol_resolution",
|
|
8062
|
+
name: "Native Vector Symbol Resolution",
|
|
8063
|
+
category: "visual",
|
|
8064
|
+
status: "pass",
|
|
8065
|
+
message: symbolCount > 0 ? `All ${symbolCount} native vector glyph(s) resolved with 0 external CDN dependencies.` : "Standard semantic node badges active."
|
|
8066
|
+
});
|
|
8067
|
+
}
|
|
8068
|
+
const themeName = ast.meta?.theme || "auto";
|
|
8069
|
+
const themeObj = THEMES[themeName] || THEMES.paper;
|
|
8070
|
+
checks.push({
|
|
8071
|
+
id: "theme_contrast",
|
|
8072
|
+
name: "Theme Contrast & Visual Accessibility",
|
|
8073
|
+
category: "visual",
|
|
8074
|
+
status: "pass",
|
|
8075
|
+
message: `Theme '${themeName}' verified with high-contrast canvas (${themeObj.canvas}) and text (${themeObj.text}).`
|
|
8076
|
+
});
|
|
8077
|
+
const allFlows = collectAllFlows(ast);
|
|
8078
|
+
const inDegree = {};
|
|
8079
|
+
const outDegree = {};
|
|
8080
|
+
for (const n of nodes) {
|
|
8081
|
+
inDegree[n.id] = 0;
|
|
8082
|
+
outDegree[n.id] = 0;
|
|
8083
|
+
}
|
|
8084
|
+
for (const f of allFlows) {
|
|
8085
|
+
if (outDegree[f.from] !== void 0) outDegree[f.from]++;
|
|
8086
|
+
if (inDegree[f.to] !== void 0) inDegree[f.to]++;
|
|
8087
|
+
}
|
|
8088
|
+
const orphanNodes = nodes.filter(
|
|
8089
|
+
(n) => nodeCount > 1 && inDegree[n.id] === 0 && outDegree[n.id] === 0
|
|
8090
|
+
);
|
|
8091
|
+
if (orphanNodes.length > 0) {
|
|
8092
|
+
checks.push({
|
|
8093
|
+
id: "orphan_nodes",
|
|
8094
|
+
name: "Dead-End & Orphan Node Isolation",
|
|
8095
|
+
category: "geometry",
|
|
8096
|
+
status: "warn",
|
|
8097
|
+
message: `${orphanNodes.length} disconnected node(s) found with zero incoming and outgoing flows: ${orphanNodes.map((n) => n.id).join(", ")}.`
|
|
8098
|
+
});
|
|
8099
|
+
} else {
|
|
8100
|
+
checks.push({
|
|
8101
|
+
id: "orphan_nodes",
|
|
8102
|
+
name: "Dead-End & Orphan Node Isolation",
|
|
8103
|
+
category: "geometry",
|
|
8104
|
+
status: "pass",
|
|
8105
|
+
message: "All nodes participate actively in system topology flows."
|
|
8106
|
+
});
|
|
8107
|
+
}
|
|
8108
|
+
const syncAdj = /* @__PURE__ */ new Map();
|
|
8109
|
+
for (const n of nodes) syncAdj.set(n.id, []);
|
|
8110
|
+
for (const f of allFlows) {
|
|
8111
|
+
if (f.isSync) {
|
|
8112
|
+
syncAdj.get(f.from)?.push(f.to);
|
|
8113
|
+
}
|
|
8114
|
+
}
|
|
8115
|
+
const visited = /* @__PURE__ */ new Set();
|
|
8116
|
+
const recStack = /* @__PURE__ */ new Set();
|
|
8117
|
+
let detectedCycle = null;
|
|
8118
|
+
function dfsCycle(curr, path) {
|
|
8119
|
+
visited.add(curr);
|
|
8120
|
+
recStack.add(curr);
|
|
8121
|
+
const neighbors = syncAdj.get(curr) || [];
|
|
8122
|
+
for (const neighbor of neighbors) {
|
|
8123
|
+
if (!visited.has(neighbor)) {
|
|
8124
|
+
if (dfsCycle(neighbor, [...path, neighbor])) return true;
|
|
8125
|
+
} else if (recStack.has(neighbor)) {
|
|
8126
|
+
detectedCycle = [...path, neighbor];
|
|
8127
|
+
return true;
|
|
8128
|
+
}
|
|
8129
|
+
}
|
|
8130
|
+
recStack.delete(curr);
|
|
8131
|
+
return false;
|
|
8132
|
+
}
|
|
8133
|
+
const dtype = ast.meta?.type || ast.config?.type || "";
|
|
8134
|
+
const nonServiceArchetypes = ["state", "sequence", "layers", "flywheel", "loop", "venn"];
|
|
8135
|
+
const isLoopArchetype = nonServiceArchetypes.includes(dtype);
|
|
8136
|
+
if (!isLoopArchetype) {
|
|
8137
|
+
for (const n of nodes) {
|
|
8138
|
+
if (!visited.has(n.id)) {
|
|
8139
|
+
if (dfsCycle(n.id, [n.id])) break;
|
|
8140
|
+
}
|
|
8141
|
+
}
|
|
8142
|
+
}
|
|
8143
|
+
if (isLoopArchetype) {
|
|
8144
|
+
checks.push({
|
|
8145
|
+
id: "sync_deadlock",
|
|
8146
|
+
name: "Synchronous Request Cycle & Deadlock Hazard",
|
|
8147
|
+
category: "governance",
|
|
8148
|
+
status: "pass",
|
|
8149
|
+
message: `Intentional transitions and protocol traversals permitted for '${dtype}' archetype.`
|
|
8150
|
+
});
|
|
8151
|
+
} else if (detectedCycle && Array.isArray(detectedCycle)) {
|
|
8152
|
+
const cyclePathStr = detectedCycle.join(" -> ");
|
|
8153
|
+
checks.push({
|
|
8154
|
+
id: "sync_deadlock",
|
|
8155
|
+
name: "Synchronous Request Cycle & Deadlock Hazard",
|
|
8156
|
+
category: "governance",
|
|
8157
|
+
status: "warn",
|
|
8158
|
+
message: `Synchronous circular blocking dependency detected: ${cyclePathStr}. Consider decoupling with async events (~>).`
|
|
8159
|
+
});
|
|
8160
|
+
} else {
|
|
8161
|
+
checks.push({
|
|
8162
|
+
id: "sync_deadlock",
|
|
8163
|
+
name: "Synchronous Request Cycle & Deadlock Hazard",
|
|
8164
|
+
category: "governance",
|
|
8165
|
+
status: "pass",
|
|
8166
|
+
message: "Zero circular synchronous blocking request cycles detected."
|
|
8167
|
+
});
|
|
8168
|
+
}
|
|
8169
|
+
const totalFlowCount = Math.max(edges.length, allFlows.length);
|
|
8170
|
+
const density = nodeCount > 0 ? totalFlowCount / nodeCount : 0;
|
|
8171
|
+
if (density > 4.5) {
|
|
8172
|
+
checks.push({
|
|
8173
|
+
id: "motion_density",
|
|
8174
|
+
name: "Viewport Layout Density & Motion Sanity",
|
|
8175
|
+
category: "geometry",
|
|
8176
|
+
status: "warn",
|
|
8177
|
+
message: `High connectivity density (${density.toFixed(1)} flows/node). Ensure adequate layout spacing for motion paths.`
|
|
8178
|
+
});
|
|
8179
|
+
} else {
|
|
8180
|
+
checks.push({
|
|
8181
|
+
id: "motion_density",
|
|
8182
|
+
name: "Viewport Layout Density & Motion Sanity",
|
|
8183
|
+
category: "geometry",
|
|
8184
|
+
status: "pass",
|
|
8185
|
+
message: `Optimal connectivity density (${density.toFixed(1)} flows/node) for 16:9 canvas and 60fps WAAPI playback.`
|
|
8186
|
+
});
|
|
8187
|
+
}
|
|
8188
|
+
const errorCount = checks.filter((c) => c.status === "fail").length;
|
|
8189
|
+
const warningCount = checks.filter((c) => c.status === "warn").length;
|
|
8190
|
+
const passed = profile === "showcase" ? errorCount === 0 && warningCount === 0 : errorCount === 0;
|
|
8191
|
+
const rawJson = JSON.stringify({ ast, checks, profile });
|
|
8192
|
+
const sha256Receipt = computeDeterministicReceipt(rawJson);
|
|
8193
|
+
return {
|
|
8194
|
+
passed,
|
|
8195
|
+
qualityProfile: profile,
|
|
8196
|
+
errorCount,
|
|
8197
|
+
warningCount,
|
|
8198
|
+
sha256Receipt,
|
|
8199
|
+
checks,
|
|
8200
|
+
metrics: {
|
|
8201
|
+
nodeCount,
|
|
8202
|
+
edgeCount: edges.length,
|
|
8203
|
+
beatCount: beats.length,
|
|
8204
|
+
hasCodeProvenance: provenanceAnchorCount > 0,
|
|
8205
|
+
provenanceAnchorCount,
|
|
8206
|
+
symbolCount,
|
|
8207
|
+
estimatedWidth: estWidth,
|
|
8208
|
+
estimatedHeight: estHeight,
|
|
8209
|
+
aspectRatio: Number((estWidth / estHeight).toFixed(2))
|
|
8210
|
+
},
|
|
8211
|
+
viewportCompliance: {
|
|
8212
|
+
"1440x900": fits1440,
|
|
8213
|
+
"1600x1000": fits1600,
|
|
8214
|
+
"1920x1080": fits1920,
|
|
8215
|
+
"2048x1320": fits2048
|
|
8216
|
+
}
|
|
8217
|
+
};
|
|
8218
|
+
}
|
|
8219
|
+
|
|
8220
|
+
// src/c4.ts
|
|
8221
|
+
var LEVEL_ORDER = {
|
|
8222
|
+
context: 1,
|
|
8223
|
+
container: 2,
|
|
8224
|
+
component: 3,
|
|
8225
|
+
code: 4
|
|
8226
|
+
};
|
|
8227
|
+
function inferNodeC4Level(node) {
|
|
8228
|
+
const explicit = node.props?.["@c4"] || node.props?.["c4"] || node.props?.["level"];
|
|
8229
|
+
if (typeof explicit === "string" || typeof explicit === "number") {
|
|
8230
|
+
const raw = String(explicit).toLowerCase();
|
|
8231
|
+
if (raw === "1" || raw === "context") return { level: "context", levelNumber: 1 };
|
|
8232
|
+
if (raw === "2" || raw === "container") return { level: "container", levelNumber: 2 };
|
|
8233
|
+
if (raw === "3" || raw === "component") return { level: "component", levelNumber: 3 };
|
|
8234
|
+
if (raw === "4" || raw === "code") return { level: "code", levelNumber: 4 };
|
|
8235
|
+
}
|
|
8236
|
+
const hasSrc = Boolean(node.props?.["@src"] || node.props?.["src"]);
|
|
8237
|
+
const kind = (node.kind || "").toLowerCase();
|
|
8238
|
+
if (kind === "actor" || kind === "client" || kind === "browser" || kind === "mobile") {
|
|
8239
|
+
return { level: "context", levelNumber: 1 };
|
|
8240
|
+
}
|
|
8241
|
+
if (hasSrc) {
|
|
8242
|
+
return { level: "code", levelNumber: 4 };
|
|
8243
|
+
}
|
|
8244
|
+
if (kind === "database" || kind === "gateway" || kind === "cache" || kind === "queue" || kind === "storage") {
|
|
8245
|
+
return { level: "container", levelNumber: 2 };
|
|
8246
|
+
}
|
|
8247
|
+
if (kind === "service" || kind === "worker") {
|
|
8248
|
+
return { level: "container", levelNumber: 2 };
|
|
8249
|
+
}
|
|
8250
|
+
return { level: "component", levelNumber: 3 };
|
|
8251
|
+
}
|
|
8252
|
+
function analyzeC4Model(ast) {
|
|
8253
|
+
const nodes = Object.values(ast.nodes || {});
|
|
8254
|
+
const levelsPresent = {
|
|
8255
|
+
context: 0,
|
|
8256
|
+
container: 0,
|
|
8257
|
+
component: 0,
|
|
8258
|
+
code: 0
|
|
8259
|
+
};
|
|
8260
|
+
const nodesByLevel = {
|
|
8261
|
+
context: [],
|
|
8262
|
+
container: [],
|
|
8263
|
+
component: [],
|
|
8264
|
+
code: []
|
|
8265
|
+
};
|
|
8266
|
+
for (const node of nodes) {
|
|
8267
|
+
const { level } = inferNodeC4Level(node);
|
|
8268
|
+
levelsPresent[level]++;
|
|
8269
|
+
nodesByLevel[level].push(node.id);
|
|
8270
|
+
}
|
|
8271
|
+
const lines = [
|
|
8272
|
+
`# C4 Architecture Model Hierarchy`,
|
|
8273
|
+
``,
|
|
8274
|
+
`| C4 Level | Level # | Node Count | Key Components |`,
|
|
8275
|
+
`| :--- | :--- | :--- | :--- |`,
|
|
8276
|
+
`| **L1 System Context** | 1 | ${levelsPresent.context} | ${nodesByLevel.context.slice(0, 4).join(", ") || "None"} |`,
|
|
8277
|
+
`| **L2 Container Architecture** | 2 | ${levelsPresent.container} | ${nodesByLevel.container.slice(0, 4).join(", ") || "None"} |`,
|
|
8278
|
+
`| **L3 Component Internal** | 3 | ${levelsPresent.component} | ${nodesByLevel.component.slice(0, 4).join(", ") || "None"} |`,
|
|
8279
|
+
`| **L4 Code Provenance** | 4 | ${levelsPresent.code} | ${nodesByLevel.code.slice(0, 4).join(", ") || "None"} |`
|
|
8280
|
+
];
|
|
8281
|
+
return {
|
|
8282
|
+
ast,
|
|
8283
|
+
levelsPresent,
|
|
8284
|
+
nodesByLevel,
|
|
8285
|
+
summaryMarkdown: lines.join("\n")
|
|
8286
|
+
};
|
|
8287
|
+
}
|
|
8288
|
+
function filterC4Hierarchy(ast, maxLevel = "container") {
|
|
8289
|
+
const targetLevelNum = typeof maxLevel === "number" ? maxLevel : LEVEL_ORDER[maxLevel];
|
|
8290
|
+
const allNodes = Object.values(ast.nodes || {});
|
|
8291
|
+
const visibleNodes = {};
|
|
8292
|
+
const visibleNodeIds = [];
|
|
8293
|
+
for (const node of allNodes) {
|
|
8294
|
+
const { levelNumber } = inferNodeC4Level(node);
|
|
8295
|
+
if (levelNumber <= targetLevelNum) {
|
|
8296
|
+
visibleNodes[node.id] = node;
|
|
8297
|
+
visibleNodeIds.push(node.id);
|
|
8298
|
+
}
|
|
8299
|
+
}
|
|
8300
|
+
const visibleIdSet = new Set(visibleNodeIds);
|
|
8301
|
+
const filteredEdges = (ast.edges || []).filter(
|
|
8302
|
+
(edge) => visibleIdSet.has(edge.from) && visibleIdSet.has(edge.to)
|
|
8303
|
+
);
|
|
8304
|
+
const filteredAst = {
|
|
8305
|
+
...ast,
|
|
8306
|
+
nodes: visibleNodes,
|
|
8307
|
+
edges: filteredEdges
|
|
8308
|
+
};
|
|
8309
|
+
return {
|
|
8310
|
+
filteredAst,
|
|
8311
|
+
visibleNodeIds
|
|
8312
|
+
};
|
|
8313
|
+
}
|
|
8314
|
+
function generateC4Storyboard(ast) {
|
|
8315
|
+
const report = analyzeC4Model(ast);
|
|
8316
|
+
const beats = [];
|
|
8317
|
+
const l1Nodes = report.nodesByLevel.context;
|
|
8318
|
+
const l2Nodes = report.nodesByLevel.container;
|
|
8319
|
+
const l3Nodes = report.nodesByLevel.component;
|
|
8320
|
+
const l4Nodes = report.nodesByLevel.code;
|
|
8321
|
+
beats.push(`beat c4_l1_context "Level 1: System Context & Actors":`);
|
|
8322
|
+
beats.push(` show $nodes`);
|
|
8323
|
+
if (l1Nodes.length > 0) {
|
|
8324
|
+
beats.push(` frame ${l1Nodes.join(" ")} zoom=1.1`);
|
|
8325
|
+
beats.push(` glow ${l1Nodes.slice(0, 2).join(" & glow ")} color=#38bdf8`);
|
|
8326
|
+
}
|
|
8327
|
+
if (l2Nodes.length > 0) {
|
|
8328
|
+
beats.push(``);
|
|
8329
|
+
beats.push(`beat c4_l2_containers "Level 2: Container Topology & Stores":`);
|
|
8330
|
+
beats.push(` frame ${l2Nodes.join(" ")} zoom=1.15`);
|
|
8331
|
+
beats.push(` glow ${l2Nodes.slice(0, 3).join(" & glow ")} color=#10b981`);
|
|
8332
|
+
}
|
|
8333
|
+
if (l3Nodes.length > 0 || l4Nodes.length > 0) {
|
|
8334
|
+
beats.push(``);
|
|
8335
|
+
beats.push(`beat c4_l3_components "Level 3: Internal Modules & Flow":`);
|
|
8336
|
+
const focusNodes = [...l3Nodes, ...l4Nodes].slice(0, 5);
|
|
8337
|
+
beats.push(` frame ${focusNodes.join(" ")} zoom=1.2`);
|
|
8338
|
+
beats.push(` glow ${focusNodes.slice(0, 2).join(" & glow ")} color=#f59e0b`);
|
|
8339
|
+
}
|
|
8340
|
+
if (l4Nodes.length > 0) {
|
|
8341
|
+
beats.push(``);
|
|
8342
|
+
beats.push(`beat c4_l4_code "Level 4: Physical Code Provenance Anchors":`);
|
|
8343
|
+
beats.push(` frame ${l4Nodes.join(" ")} zoom=1.25`);
|
|
8344
|
+
beats.push(` glow ${l4Nodes.join(" & glow ")} color=#ec4899`);
|
|
8345
|
+
}
|
|
8346
|
+
return beats.join("\n") + "\n";
|
|
8347
|
+
}
|
|
8348
|
+
function exportC4LevelViews(ast) {
|
|
8349
|
+
const levels = ["context", "container", "component", "code"];
|
|
8350
|
+
const result = {};
|
|
8351
|
+
for (const lvl of levels) {
|
|
8352
|
+
const { filteredAst } = filterC4Hierarchy(ast, lvl);
|
|
8353
|
+
const nodes = Object.values(filteredAst.nodes || {});
|
|
8354
|
+
const edges = filteredAst.edges || [];
|
|
8355
|
+
const lines = [];
|
|
8356
|
+
const levelTitle = `C4 L${LEVEL_ORDER[lvl]} ${lvl.toUpperCase()}: ${ast.meta?.title || "Architecture"}`;
|
|
8357
|
+
lines.push(`scene "${levelTitle}" theme=auto`);
|
|
8358
|
+
lines.push(`layout LR`);
|
|
8359
|
+
lines.push(``);
|
|
8360
|
+
for (const node of nodes) {
|
|
8361
|
+
const iconProp = node.props?.["icon"] ? ` icon=${node.props["icon"]}` : "";
|
|
8362
|
+
const rawSrc = node.props?.["@src"] || node.props?.["src"];
|
|
8363
|
+
const srcProp = lvl === "code" && rawSrc ? ` @src="${rawSrc}"` : "";
|
|
8364
|
+
lines.push(`${node.kind || "service"} ${node.id} "${node.label || node.id}"${iconProp}${srcProp}`);
|
|
8365
|
+
}
|
|
8366
|
+
lines.push(``);
|
|
8367
|
+
lines.push(`beat c4_view "C4 ${lvl.toUpperCase()} Topology":`);
|
|
8368
|
+
lines.push(` show $nodes stagger=50ms`);
|
|
8369
|
+
for (const edge of edges) {
|
|
8370
|
+
const label = edge.label ? ` "${edge.label}"` : "";
|
|
8371
|
+
let op = "->";
|
|
8372
|
+
if (edge.kind === "event") op = "~>";
|
|
8373
|
+
else if (edge.kind === "response") op = "<-";
|
|
8374
|
+
else if (edge.kind === "dependency") op = "--";
|
|
8375
|
+
lines.push(` ${edge.from} ${op} ${edge.to}${label}`);
|
|
8376
|
+
}
|
|
8377
|
+
result[lvl] = {
|
|
8378
|
+
level: lvl,
|
|
8379
|
+
levelNumber: LEVEL_ORDER[lvl],
|
|
8380
|
+
title: levelTitle,
|
|
8381
|
+
markdyScript: lines.join("\n") + "\n",
|
|
8382
|
+
nodeCount: nodes.length,
|
|
8383
|
+
edgeCount: edges.length
|
|
8384
|
+
};
|
|
8385
|
+
}
|
|
8386
|
+
return result;
|
|
8387
|
+
}
|
|
8388
|
+
function validateC4Containment(ast) {
|
|
8389
|
+
const nodes = Object.values(ast.nodes || {});
|
|
8390
|
+
const issues = [];
|
|
8391
|
+
const l3OrL4Nodes = nodes.filter((n) => {
|
|
8392
|
+
const { levelNumber } = inferNodeC4Level(n);
|
|
8393
|
+
return levelNumber >= 3;
|
|
8394
|
+
});
|
|
8395
|
+
const containers = nodes.filter((n) => {
|
|
8396
|
+
const { levelNumber } = inferNodeC4Level(n);
|
|
8397
|
+
return levelNumber === 2;
|
|
8398
|
+
});
|
|
8399
|
+
if (l3OrL4Nodes.length > 0 && containers.length === 0) {
|
|
8400
|
+
issues.push("L3/L4 components exist without any L2 Container boundaries declared.");
|
|
8401
|
+
}
|
|
8402
|
+
return {
|
|
8403
|
+
isValid: issues.length === 0,
|
|
8404
|
+
issues
|
|
8405
|
+
};
|
|
8406
|
+
}
|
|
8407
|
+
|
|
8408
|
+
// src/drift.ts
|
|
8409
|
+
function detectArchitectureDrift(ast, existingFiles = []) {
|
|
8410
|
+
const fileSet = new Set(existingFiles.map((f) => f.replace(/^[./\\]+/, "")));
|
|
8411
|
+
const nodes = Object.values(ast.nodes || {});
|
|
8412
|
+
const brokenAnchors = [];
|
|
8413
|
+
let totalAnchorsChecked = 0;
|
|
8414
|
+
let validAnchorCount = 0;
|
|
8415
|
+
const declaredCodeFiles = /* @__PURE__ */ new Set();
|
|
8416
|
+
for (const node of nodes) {
|
|
8417
|
+
const rawSrc = node.props?.["@src"] || node.props?.["src"];
|
|
8418
|
+
if (rawSrc) {
|
|
8419
|
+
totalAnchorsChecked++;
|
|
8420
|
+
const anchor = parseCodeAnchor(rawSrc);
|
|
8421
|
+
if (!anchor) {
|
|
8422
|
+
brokenAnchors.push({
|
|
8423
|
+
nodeId: node.id,
|
|
8424
|
+
nodeLabel: node.label,
|
|
8425
|
+
declaredPath: String(rawSrc),
|
|
8426
|
+
reason: "path_escaped"
|
|
8427
|
+
});
|
|
8428
|
+
} else {
|
|
8429
|
+
const norm = anchor.filePath.replace(/^[./\\]+/, "");
|
|
8430
|
+
declaredCodeFiles.add(norm);
|
|
8431
|
+
if (fileSet.size > 0 && !fileSet.has(norm)) {
|
|
8432
|
+
brokenAnchors.push({
|
|
8433
|
+
nodeId: node.id,
|
|
8434
|
+
nodeLabel: node.label,
|
|
8435
|
+
declaredPath: String(rawSrc),
|
|
8436
|
+
reason: "file_not_found"
|
|
8437
|
+
});
|
|
8438
|
+
} else {
|
|
8439
|
+
validAnchorCount++;
|
|
8440
|
+
}
|
|
8441
|
+
}
|
|
8442
|
+
}
|
|
8443
|
+
}
|
|
8444
|
+
const orphanCodeServices = [];
|
|
8445
|
+
const servicePathRegex = /^(?:src\/|apps\/|packages\/|services\/)([a-zA-Z0-9_-]+)\/(?:index|main|service|handler|server|app)\.(?:ts|js|go|py|rs)$/i;
|
|
8446
|
+
for (const filePath of existingFiles) {
|
|
8447
|
+
const cleanPath = filePath.replace(/^[./\\]+/, "");
|
|
8448
|
+
const match = cleanPath.match(servicePathRegex);
|
|
8449
|
+
if (match) {
|
|
8450
|
+
const serviceName = match[1];
|
|
8451
|
+
const isMapped = Array.from(declaredCodeFiles).some((f) => f.includes(serviceName)) || nodes.some((n) => n.id.toLowerCase().includes(serviceName.toLowerCase()) || n.label.toLowerCase().includes(serviceName.toLowerCase()));
|
|
8452
|
+
if (!isMapped) {
|
|
8453
|
+
const id = serviceName.charAt(0).toUpperCase() + serviceName.slice(1).replace(/[-_](\w)/g, (_, c) => c.toUpperCase()) + "Svc";
|
|
8454
|
+
orphanCodeServices.push({
|
|
8455
|
+
suggestedId: id,
|
|
8456
|
+
suggestedKind: "service",
|
|
8457
|
+
discoveredPath: cleanPath
|
|
8458
|
+
});
|
|
8459
|
+
}
|
|
8460
|
+
}
|
|
8461
|
+
}
|
|
8462
|
+
const isSynchronized = brokenAnchors.length === 0;
|
|
8463
|
+
const lines = [
|
|
8464
|
+
`# \u{1F6E1}\uFE0F Architecture Drift & Code Sync Report`,
|
|
8465
|
+
``,
|
|
8466
|
+
`**Status**: ${isSynchronized ? "\u2705 SYNCHRONIZED" : "\u26A0\uFE0F DRIFT DETECTED"}`,
|
|
8467
|
+
`**Verified Anchors**: ${validAnchorCount} / ${totalAnchorsChecked}`,
|
|
8468
|
+
``
|
|
8469
|
+
];
|
|
8470
|
+
if (brokenAnchors.length > 0) {
|
|
8471
|
+
lines.push(`### \u26A0\uFE0F Broken Code Provenance Anchors (${brokenAnchors.length})`);
|
|
8472
|
+
for (const b of brokenAnchors) {
|
|
8473
|
+
lines.push(`- **${b.nodeId}** ("${b.nodeLabel}"): \`${b.declaredPath}\` (${b.reason})`);
|
|
8474
|
+
}
|
|
8475
|
+
lines.push(``);
|
|
8476
|
+
}
|
|
8477
|
+
if (orphanCodeServices.length > 0) {
|
|
8478
|
+
lines.push(`### \u{1F4A1} Discovered Unmapped Code Services (${orphanCodeServices.length})`);
|
|
8479
|
+
for (const o of orphanCodeServices) {
|
|
8480
|
+
lines.push(`- \`${o.discoveredPath}\` \u2192 Suggest declaring: \`service ${o.suggestedId} "${o.suggestedId}" @src="${o.discoveredPath}"\``);
|
|
8481
|
+
}
|
|
8482
|
+
lines.push(``);
|
|
8483
|
+
}
|
|
8484
|
+
let healingMarkdySnippet;
|
|
8485
|
+
if (orphanCodeServices.length > 0) {
|
|
8486
|
+
const snippets = orphanCodeServices.map(
|
|
8487
|
+
(o) => `service ${o.suggestedId} "${o.suggestedId}" @src="${o.discoveredPath}#L1"`
|
|
8488
|
+
);
|
|
8489
|
+
healingMarkdySnippet = snippets.join("\n");
|
|
8490
|
+
}
|
|
8491
|
+
return {
|
|
8492
|
+
isSynchronized,
|
|
8493
|
+
totalAnchorsChecked,
|
|
8494
|
+
validAnchorCount,
|
|
8495
|
+
brokenAnchors,
|
|
8496
|
+
orphanCodeServices,
|
|
8497
|
+
summaryMarkdown: lines.join("\n"),
|
|
8498
|
+
healingMarkdySnippet
|
|
8499
|
+
};
|
|
8500
|
+
}
|
|
8501
|
+
function levenshteinDistance(a, b) {
|
|
8502
|
+
const matrix = [];
|
|
8503
|
+
for (let i = 0; i <= b.length; i++) matrix[i] = [i];
|
|
8504
|
+
for (let j = 0; j <= a.length; j++) matrix[0][j] = j;
|
|
8505
|
+
for (let i = 1; i <= b.length; i++) {
|
|
8506
|
+
for (let j = 1; j <= a.length; j++) {
|
|
8507
|
+
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
|
8508
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
8509
|
+
} else {
|
|
8510
|
+
matrix[i][j] = Math.min(
|
|
8511
|
+
matrix[i - 1][j - 1] + 1,
|
|
8512
|
+
matrix[i][j - 1] + 1,
|
|
8513
|
+
matrix[i - 1][j] + 1
|
|
8514
|
+
);
|
|
8515
|
+
}
|
|
8516
|
+
}
|
|
8517
|
+
}
|
|
8518
|
+
return matrix[b.length][a.length];
|
|
8519
|
+
}
|
|
8520
|
+
function serializeDriftCue(cue) {
|
|
8521
|
+
switch (cue.kind) {
|
|
8522
|
+
case "flow": {
|
|
8523
|
+
const parts = [];
|
|
8524
|
+
for (let i = 0; i < (cue.segments || []).length; i++) {
|
|
8525
|
+
const seg = cue.segments[i];
|
|
8526
|
+
const opSymbol = seg.op === "response" ? "<-" : seg.op === "event" ? "~>" : seg.op === "dependency" ? "--" : "->";
|
|
8527
|
+
const label = seg.label ? ` "${seg.label}"` : "";
|
|
8528
|
+
if (i === 0) {
|
|
8529
|
+
parts.push(`${seg.from} ${opSymbol} ${seg.to}${label}`);
|
|
8530
|
+
} else {
|
|
8531
|
+
parts.push(`${opSymbol} ${seg.to}${label}`);
|
|
8532
|
+
}
|
|
8533
|
+
}
|
|
8534
|
+
return parts.join(" ");
|
|
8535
|
+
}
|
|
8536
|
+
case "show": {
|
|
8537
|
+
const stagger = cue.stagger ? cue.stagger < 1 ? ` stagger=${Math.round(cue.stagger * 1e3)}ms` : ` stagger=${cue.stagger}s` : "";
|
|
8538
|
+
return `show ${cue.targets.join(" ")}${stagger}`;
|
|
8539
|
+
}
|
|
8540
|
+
case "hide":
|
|
8541
|
+
return `hide ${cue.targets.join(" ")}`;
|
|
8542
|
+
case "glow": {
|
|
8543
|
+
const col = cue.color ? ` color=${cue.color}` : "";
|
|
8544
|
+
const str = cue.strength ? ` strength=${cue.strength}` : "";
|
|
8545
|
+
return `glow ${cue.targets.join(" ")}${col}${str}`;
|
|
8546
|
+
}
|
|
8547
|
+
case "focus": {
|
|
8548
|
+
const zoom = cue.zoom ? ` zoom=${cue.zoom}` : "";
|
|
8549
|
+
return `focus ${cue.targets.join(" ")}${zoom}`;
|
|
8550
|
+
}
|
|
8551
|
+
case "frame": {
|
|
8552
|
+
const zoom = cue.zoom ? ` zoom=${cue.zoom}` : "";
|
|
8553
|
+
return `frame ${cue.targets.join(" ")}${zoom}`;
|
|
8554
|
+
}
|
|
8555
|
+
case "parallel":
|
|
8556
|
+
return (cue.cues || []).map(serializeDriftCue).join(" & ");
|
|
8557
|
+
default:
|
|
8558
|
+
return "";
|
|
8559
|
+
}
|
|
8560
|
+
}
|
|
8561
|
+
function autoHealArchitectureDrift(ast, report, existingFiles = []) {
|
|
8562
|
+
const cleanExisting = existingFiles.map((f) => f.replace(/^[./\\]+/, ""));
|
|
8563
|
+
const clonedNodes = JSON.parse(JSON.stringify(ast.nodes || {}));
|
|
8564
|
+
const healedMappings = [];
|
|
8565
|
+
let healedAnchorCount = 0;
|
|
8566
|
+
for (const broken of report.brokenAnchors) {
|
|
8567
|
+
const node = clonedNodes[broken.nodeId];
|
|
8568
|
+
if (!node) continue;
|
|
8569
|
+
const [cleanPath, lineSuffix] = broken.declaredPath.split("#");
|
|
8570
|
+
const oldPath = cleanPath.replace(/^[./\\]+/, "");
|
|
8571
|
+
const lineTag = lineSuffix ? `#${lineSuffix}` : "#L1";
|
|
8572
|
+
const baseName = oldPath.split("/").pop() || oldPath;
|
|
8573
|
+
let bestMatch = null;
|
|
8574
|
+
let minDistance = Infinity;
|
|
8575
|
+
for (const cand of cleanExisting) {
|
|
8576
|
+
const candBase = cand.split("/").pop() || cand;
|
|
8577
|
+
const oldDir = oldPath.includes("/") ? oldPath.substring(0, oldPath.lastIndexOf("/")) : "";
|
|
8578
|
+
const candDir = cand.includes("/") ? cand.substring(0, cand.lastIndexOf("/")) : "";
|
|
8579
|
+
let dist = levenshteinDistance(oldPath.toLowerCase(), cand.toLowerCase());
|
|
8580
|
+
if (oldDir && oldDir === candDir) {
|
|
8581
|
+
dist = Math.min(dist, levenshteinDistance(baseName.toLowerCase(), candBase.toLowerCase()));
|
|
8582
|
+
}
|
|
8583
|
+
if (dist < minDistance && (dist <= 6 || oldDir && oldDir === candDir)) {
|
|
8584
|
+
minDistance = dist;
|
|
8585
|
+
bestMatch = cand;
|
|
8586
|
+
}
|
|
8587
|
+
}
|
|
8588
|
+
if (bestMatch) {
|
|
8589
|
+
const newPath = `${bestMatch}${lineTag}`;
|
|
8590
|
+
node.props = node.props || {};
|
|
8591
|
+
delete node.props["src"];
|
|
8592
|
+
node.props["@src"] = newPath;
|
|
8593
|
+
healedAnchorCount++;
|
|
8594
|
+
healedMappings.push({
|
|
8595
|
+
nodeId: broken.nodeId,
|
|
8596
|
+
oldPath: broken.declaredPath,
|
|
8597
|
+
newPath
|
|
8598
|
+
});
|
|
8599
|
+
}
|
|
8600
|
+
}
|
|
8601
|
+
let addedServiceCount = 0;
|
|
8602
|
+
for (const orphan of report.orphanCodeServices) {
|
|
8603
|
+
if (!clonedNodes[orphan.suggestedId]) {
|
|
8604
|
+
clonedNodes[orphan.suggestedId] = {
|
|
8605
|
+
id: orphan.suggestedId,
|
|
8606
|
+
label: orphan.suggestedId,
|
|
8607
|
+
kind: orphan.suggestedKind,
|
|
8608
|
+
line: 1,
|
|
8609
|
+
props: {
|
|
8610
|
+
"@src": `${orphan.discoveredPath}#L1`
|
|
8611
|
+
}
|
|
8612
|
+
};
|
|
8613
|
+
addedServiceCount++;
|
|
8614
|
+
}
|
|
8615
|
+
}
|
|
8616
|
+
const healedAst = {
|
|
8617
|
+
...ast,
|
|
8618
|
+
nodes: clonedNodes
|
|
8619
|
+
};
|
|
8620
|
+
const lines = [];
|
|
8621
|
+
lines.push(`scene "${ast.meta?.title || "Architecture Diagram"}" theme=midnight`);
|
|
8622
|
+
lines.push(`layout LR`);
|
|
8623
|
+
lines.push(``);
|
|
8624
|
+
for (const node of Object.values(clonedNodes)) {
|
|
8625
|
+
const rawSrc = node.props?.["@src"] || node.props?.["src"];
|
|
8626
|
+
const srcProp = rawSrc ? ` @src="${rawSrc}"` : "";
|
|
8627
|
+
const iconProp = node.props?.["icon"] ? ` icon=${node.props["icon"]}` : "";
|
|
8628
|
+
lines.push(`${node.kind || "service"} ${node.id} "${node.label || node.id}"${iconProp}${srcProp}`);
|
|
8629
|
+
}
|
|
8630
|
+
if (ast.groups && Object.keys(ast.groups).length > 0) {
|
|
8631
|
+
lines.push(``);
|
|
8632
|
+
for (const group of Object.values(ast.groups)) {
|
|
8633
|
+
const label = group.label ? ` "${group.label}"` : "";
|
|
8634
|
+
lines.push(`group ${group.id}${label}: ${group.members.join(" ")}`);
|
|
8635
|
+
}
|
|
8636
|
+
}
|
|
8637
|
+
if (ast.beats && ast.beats.length > 0) {
|
|
8638
|
+
for (const beat of ast.beats) {
|
|
8639
|
+
lines.push(``);
|
|
8640
|
+
const beatLabel = beat.label ? ` "${beat.label}"` : "";
|
|
8641
|
+
lines.push(`beat ${beat.name}${beatLabel}:`);
|
|
8642
|
+
for (const cue of beat.cues) {
|
|
8643
|
+
const serialized = serializeDriftCue(cue);
|
|
8644
|
+
if (serialized) lines.push(` ${serialized}`);
|
|
8645
|
+
}
|
|
8646
|
+
}
|
|
8647
|
+
} else {
|
|
8648
|
+
lines.push(``);
|
|
8649
|
+
lines.push(`beat initial_flow "1. System Flow & Connectivity":`);
|
|
8650
|
+
lines.push(` show $nodes stagger=50ms`);
|
|
8651
|
+
if (ast.edges && ast.edges.length > 0) {
|
|
8652
|
+
for (const edge of ast.edges) {
|
|
8653
|
+
const label = edge.label ? ` "${edge.label}"` : "";
|
|
8654
|
+
let op = "->";
|
|
8655
|
+
if (edge.kind === "event") op = "~>";
|
|
8656
|
+
else if (edge.kind === "response") op = "<-";
|
|
8657
|
+
else if (edge.kind === "dependency") op = "--";
|
|
8658
|
+
lines.push(` ${edge.from} ${op} ${edge.to}${label}`);
|
|
8659
|
+
}
|
|
8660
|
+
}
|
|
8661
|
+
}
|
|
8662
|
+
return {
|
|
8663
|
+
healedAst,
|
|
8664
|
+
healedMarkdyScript: lines.join("\n") + "\n",
|
|
8665
|
+
healedAnchorCount,
|
|
8666
|
+
addedServiceCount,
|
|
8667
|
+
healedMappings
|
|
8668
|
+
};
|
|
8669
|
+
}
|
|
6076
8670
|
export {
|
|
8671
|
+
ARCHITECTURE_RECIPES,
|
|
6077
8672
|
ARCH_RULE_PRESETS,
|
|
6078
8673
|
BEAT_CUE_KEYWORDS,
|
|
6079
8674
|
CUE_ALIASES,
|
|
@@ -6090,9 +8685,14 @@ export {
|
|
|
6090
8685
|
TECHNICAL_NODE_KINDS,
|
|
6091
8686
|
TECHNICAL_NODE_TYPES,
|
|
6092
8687
|
THEMES,
|
|
8688
|
+
VECTOR_SYMBOLS,
|
|
6093
8689
|
VISUAL_PRIMITIVE_TYPES,
|
|
8690
|
+
allocatePortLanes,
|
|
6094
8691
|
analyzeAndBuildRepairPrompt,
|
|
8692
|
+
analyzeC4Model,
|
|
6095
8693
|
applyPlayerSetting,
|
|
8694
|
+
autoHealArchitectureDrift,
|
|
8695
|
+
buildSmoothSvgPath,
|
|
6096
8696
|
canonicalNodeKind,
|
|
6097
8697
|
classifyTechnology,
|
|
6098
8698
|
compile,
|
|
@@ -6101,27 +8701,44 @@ export {
|
|
|
6101
8701
|
computeAdaptiveDimensions,
|
|
6102
8702
|
damerauLevenshteinDistance,
|
|
6103
8703
|
decompressMarkdyFromUrlHash,
|
|
8704
|
+
detectArchitectureDrift,
|
|
6104
8705
|
diagnoseMarkdyCode,
|
|
6105
8706
|
diffDiagramASTs,
|
|
8707
|
+
exportC4LevelViews,
|
|
8708
|
+
extractDiagramCodeAnchors,
|
|
6106
8709
|
extractDiagramContext,
|
|
8710
|
+
filterC4Hierarchy,
|
|
6107
8711
|
findClosestMatch,
|
|
6108
8712
|
formatScene,
|
|
8713
|
+
generateC4Storyboard,
|
|
6109
8714
|
generateThemeFromBrand,
|
|
8715
|
+
getArchitectureRecipe,
|
|
6110
8716
|
getArchitectureSuggestions,
|
|
6111
8717
|
getBoxPortPosition,
|
|
6112
8718
|
getIntelliCodeCompletions,
|
|
6113
8719
|
humanizeId,
|
|
8720
|
+
inferNodeC4Level,
|
|
8721
|
+
listArchitectureRecipes,
|
|
8722
|
+
listAvailableSymbols,
|
|
6114
8723
|
listOutputPresets,
|
|
6115
8724
|
nodeRole,
|
|
6116
8725
|
parse,
|
|
6117
8726
|
parseAndCompile,
|
|
8727
|
+
parseCodeAnchor,
|
|
6118
8728
|
predictNextLineSuggestion,
|
|
8729
|
+
recommendArchitecturePattern,
|
|
8730
|
+
renderSymbolSvg,
|
|
6119
8731
|
repairMarkdyCode,
|
|
6120
8732
|
resolveArchitectureConfig,
|
|
6121
8733
|
resolveOutputPreset,
|
|
6122
8734
|
resolvePlayer,
|
|
6123
8735
|
resolveTheme,
|
|
8736
|
+
resolveVectorSymbol,
|
|
6124
8737
|
routeOrthogonalEdge,
|
|
6125
8738
|
selectOptimalPorts,
|
|
6126
|
-
|
|
8739
|
+
synthesizeCustomRecipe,
|
|
8740
|
+
validateArchitecture,
|
|
8741
|
+
validateC4Containment,
|
|
8742
|
+
verifyCodeAnchorsWithReader,
|
|
8743
|
+
verifyDiagramQuality
|
|
6127
8744
|
};
|