@markdy/mcp-server 1.0.29 → 1.0.30
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 +38 -25
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1952 -42
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -717,6 +717,289 @@ function canonicalNodeKind(kind) {
|
|
|
717
717
|
return NODE_ALIASES[kind] ?? kind;
|
|
718
718
|
}
|
|
719
719
|
|
|
720
|
+
// ../core/src/themes.ts
|
|
721
|
+
var ROLE_COLORS = {
|
|
722
|
+
compute: "#38bdf8",
|
|
723
|
+
code: "#38bdf8",
|
|
724
|
+
client: "#f59e0b",
|
|
725
|
+
data: "#22c55e",
|
|
726
|
+
messaging: "#a78bfa",
|
|
727
|
+
network: "#60a5fa",
|
|
728
|
+
platform: "#c084fc",
|
|
729
|
+
security: "#fb7185",
|
|
730
|
+
delivery: "#facc15",
|
|
731
|
+
observability: "#2dd4bf",
|
|
732
|
+
flow: "#e879f9",
|
|
733
|
+
distributed: "#94a3b8"
|
|
734
|
+
};
|
|
735
|
+
var EDGE_COLORS = {
|
|
736
|
+
request: "#38bdf8",
|
|
737
|
+
response: "#a78bfa",
|
|
738
|
+
event: "#f59e0b",
|
|
739
|
+
dependency: "#64748b"
|
|
740
|
+
};
|
|
741
|
+
var THEMES = {
|
|
742
|
+
midnight: {
|
|
743
|
+
name: "midnight",
|
|
744
|
+
canvas: "#070d18",
|
|
745
|
+
surface: "#0e1a2c",
|
|
746
|
+
surfaceRaised: "#152538",
|
|
747
|
+
border: "#28384f",
|
|
748
|
+
text: "#f4f7fb",
|
|
749
|
+
textMuted: "#93a4bb",
|
|
750
|
+
gridMinor: "rgba(148, 163, 184, 0.10)",
|
|
751
|
+
gridMajor: "rgba(148, 163, 184, 0.16)",
|
|
752
|
+
vignette: "rgba(2, 6, 23, 0.7)",
|
|
753
|
+
accent: "#38bdf8",
|
|
754
|
+
nodeSurface: "#111f33",
|
|
755
|
+
nodeSurfaceRaised: "#1a2c44",
|
|
756
|
+
hairline: "rgba(148, 163, 184, 0.16)",
|
|
757
|
+
shadow: "rgba(2, 6, 23, 0.6)",
|
|
758
|
+
labelPlate: "#0f1c2e",
|
|
759
|
+
roles: { ...ROLE_COLORS },
|
|
760
|
+
edges: { ...EDGE_COLORS }
|
|
761
|
+
},
|
|
762
|
+
paper: {
|
|
763
|
+
name: "paper",
|
|
764
|
+
canvas: "#f6f8fc",
|
|
765
|
+
surface: "#ffffff",
|
|
766
|
+
surfaceRaised: "#f0f4f9",
|
|
767
|
+
border: "#cbd5e1",
|
|
768
|
+
text: "#0f1c2e",
|
|
769
|
+
textMuted: "#475569",
|
|
770
|
+
gridMinor: "rgba(15, 23, 42, 0.06)",
|
|
771
|
+
gridMajor: "rgba(15, 23, 42, 0.10)",
|
|
772
|
+
vignette: "rgba(226, 232, 240, 0.6)",
|
|
773
|
+
accent: "#0284c7",
|
|
774
|
+
nodeSurface: "#ffffff",
|
|
775
|
+
nodeSurfaceRaised: "#f7fafd",
|
|
776
|
+
hairline: "rgba(15, 23, 42, 0.10)",
|
|
777
|
+
shadow: "rgba(15, 23, 42, 0.14)",
|
|
778
|
+
labelPlate: "#ffffff",
|
|
779
|
+
roles: { ...ROLE_COLORS },
|
|
780
|
+
edges: { ...EDGE_COLORS },
|
|
781
|
+
series: ["#7c8f6f", "#5e7a9b", "#b8915a", "#9c6b50", "#6e6479"]
|
|
782
|
+
},
|
|
783
|
+
blueprint: {
|
|
784
|
+
name: "blueprint",
|
|
785
|
+
canvas: "#0a1a3a",
|
|
786
|
+
surface: "#10214a",
|
|
787
|
+
surfaceRaised: "#182c5c",
|
|
788
|
+
border: "#2c477f",
|
|
789
|
+
text: "#eaf1ff",
|
|
790
|
+
textMuted: "#9fb6e0",
|
|
791
|
+
gridMinor: "rgba(125, 170, 255, 0.12)",
|
|
792
|
+
gridMajor: "rgba(125, 170, 255, 0.2)",
|
|
793
|
+
vignette: "rgba(4, 12, 34, 0.72)",
|
|
794
|
+
accent: "#5ba3ff",
|
|
795
|
+
nodeSurface: "#122452",
|
|
796
|
+
nodeSurfaceRaised: "#1b3068",
|
|
797
|
+
hairline: "rgba(125, 170, 255, 0.18)",
|
|
798
|
+
shadow: "rgba(3, 10, 30, 0.6)",
|
|
799
|
+
labelPlate: "#10214a",
|
|
800
|
+
roles: { ...ROLE_COLORS },
|
|
801
|
+
edges: { ...EDGE_COLORS }
|
|
802
|
+
},
|
|
803
|
+
graphite: {
|
|
804
|
+
name: "graphite",
|
|
805
|
+
canvas: "#111317",
|
|
806
|
+
surface: "#191c22",
|
|
807
|
+
surfaceRaised: "#22262e",
|
|
808
|
+
border: "#343a45",
|
|
809
|
+
text: "#eef1f5",
|
|
810
|
+
textMuted: "#9aa3b1",
|
|
811
|
+
gridMinor: "rgba(160, 170, 185, 0.08)",
|
|
812
|
+
gridMajor: "rgba(160, 170, 185, 0.14)",
|
|
813
|
+
vignette: "rgba(0, 0, 0, 0.6)",
|
|
814
|
+
accent: "#2dd4bf",
|
|
815
|
+
nodeSurface: "#1c2028",
|
|
816
|
+
nodeSurfaceRaised: "#262b34",
|
|
817
|
+
hairline: "rgba(160, 170, 185, 0.14)",
|
|
818
|
+
shadow: "rgba(0, 0, 0, 0.55)",
|
|
819
|
+
labelPlate: "#191c22",
|
|
820
|
+
roles: { ...ROLE_COLORS },
|
|
821
|
+
edges: { ...EDGE_COLORS }
|
|
822
|
+
},
|
|
823
|
+
editorial: {
|
|
824
|
+
name: "editorial",
|
|
825
|
+
canvas: "#fafafa",
|
|
826
|
+
surface: "#ffffff",
|
|
827
|
+
surfaceRaised: "#f8fafc",
|
|
828
|
+
border: "#e2e8f0",
|
|
829
|
+
text: "#0f172a",
|
|
830
|
+
textMuted: "#475569",
|
|
831
|
+
paper: "#fafafa",
|
|
832
|
+
ink: "#0f172a",
|
|
833
|
+
muted: "#6b7280",
|
|
834
|
+
rule: "#e2e8f0",
|
|
835
|
+
soft: "#94a3b8",
|
|
836
|
+
link: "#047857",
|
|
837
|
+
gridMinor: "rgba(15, 23, 42, 0.06)",
|
|
838
|
+
gridMajor: "rgba(15, 23, 42, 0.10)",
|
|
839
|
+
vignette: "rgba(226, 232, 240, 0.5)",
|
|
840
|
+
accent: "#047857",
|
|
841
|
+
accentTint: "rgba(4, 120, 87, 0.08)",
|
|
842
|
+
nodeSurface: "#ffffff",
|
|
843
|
+
nodeSurfaceRaised: "#f8fafc",
|
|
844
|
+
hairline: "#e2e8f0",
|
|
845
|
+
shadow: "rgba(15, 23, 42, 0.08)",
|
|
846
|
+
labelPlate: "#fafafa",
|
|
847
|
+
flatCards: true,
|
|
848
|
+
roles: { ...ROLE_COLORS, compute: "#0f172a", client: "#64748b", data: "#64748b" },
|
|
849
|
+
edges: {
|
|
850
|
+
request: "#334155",
|
|
851
|
+
response: "#64748b",
|
|
852
|
+
event: "#047857",
|
|
853
|
+
dependency: "#94a3b8"
|
|
854
|
+
},
|
|
855
|
+
fonts: {
|
|
856
|
+
title: "Georgia, Times New Roman, serif",
|
|
857
|
+
nodeName: "ui-sans-serif, system-ui, sans-serif",
|
|
858
|
+
mono: "ui-monospace, SFMono-Regular, Menlo, monospace"
|
|
859
|
+
},
|
|
860
|
+
radiusMd: 6,
|
|
861
|
+
spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 40 },
|
|
862
|
+
series: ["#7c8f6f", "#5e7a9b", "#b8915a", "#9c6b50", "#6e6479"]
|
|
863
|
+
},
|
|
864
|
+
nebula: {
|
|
865
|
+
name: "nebula",
|
|
866
|
+
canvas: "#090b1a",
|
|
867
|
+
surface: "#111735",
|
|
868
|
+
surfaceRaised: "#1a2250",
|
|
869
|
+
border: "rgba(167, 139, 250, 0.28)",
|
|
870
|
+
text: "#eef2ff",
|
|
871
|
+
textMuted: "#a5b4fc",
|
|
872
|
+
paper: "#090b1a",
|
|
873
|
+
ink: "#eef2ff",
|
|
874
|
+
muted: "#818cf8",
|
|
875
|
+
rule: "rgba(129, 140, 248, 0.32)",
|
|
876
|
+
soft: "#67e8f9",
|
|
877
|
+
link: "#67e8f9",
|
|
878
|
+
gridMinor: "rgba(129, 140, 248, 0.08)",
|
|
879
|
+
gridMajor: "rgba(103, 232, 249, 0.16)",
|
|
880
|
+
vignette: "rgba(3, 5, 18, 0.86)",
|
|
881
|
+
accent: "#c4b5fd",
|
|
882
|
+
accentTint: "rgba(196, 181, 253, 0.14)",
|
|
883
|
+
nodeSurface: "#111735",
|
|
884
|
+
nodeSurfaceRaised: "#202a5b",
|
|
885
|
+
hairline: "rgba(196, 181, 253, 0.32)",
|
|
886
|
+
shadow: "rgba(3, 5, 18, 0.72)",
|
|
887
|
+
labelPlate: "#12183b",
|
|
888
|
+
roles: {
|
|
889
|
+
...ROLE_COLORS,
|
|
890
|
+
compute: "#67e8f9",
|
|
891
|
+
client: "#f9a8d4",
|
|
892
|
+
data: "#a7f3d0",
|
|
893
|
+
network: "#93c5fd",
|
|
894
|
+
flow: "#c4b5fd"
|
|
895
|
+
},
|
|
896
|
+
edges: {
|
|
897
|
+
request: "#67e8f9",
|
|
898
|
+
response: "#c4b5fd",
|
|
899
|
+
event: "#f9a8d4",
|
|
900
|
+
dependency: "#818cf8"
|
|
901
|
+
},
|
|
902
|
+
fonts: {
|
|
903
|
+
title: "Georgia, Times New Roman, serif",
|
|
904
|
+
nodeName: "ui-sans-serif, system-ui, sans-serif",
|
|
905
|
+
mono: "ui-monospace, SFMono-Regular, Menlo, monospace"
|
|
906
|
+
},
|
|
907
|
+
radiusMd: 12,
|
|
908
|
+
spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 40 }
|
|
909
|
+
},
|
|
910
|
+
terminal: {
|
|
911
|
+
name: "terminal",
|
|
912
|
+
canvas: "#0a0a0a",
|
|
913
|
+
surface: "#141414",
|
|
914
|
+
surfaceRaised: "#1e1e1e",
|
|
915
|
+
border: "#2b2b2b",
|
|
916
|
+
text: "#f5f5f5",
|
|
917
|
+
textMuted: "#9a9a9a",
|
|
918
|
+
paper: "#141414",
|
|
919
|
+
ink: "#f5f5f5",
|
|
920
|
+
muted: "#9a9a9a",
|
|
921
|
+
rule: "#2b2b2b",
|
|
922
|
+
soft: "#5c5c5c",
|
|
923
|
+
link: "#ff5a36",
|
|
924
|
+
gridMinor: "rgba(255, 255, 255, 0.04)",
|
|
925
|
+
gridMajor: "rgba(255, 255, 255, 0.08)",
|
|
926
|
+
vignette: "rgba(0, 0, 0, 0.8)",
|
|
927
|
+
accent: "#ff5a36",
|
|
928
|
+
accentTint: "rgba(255, 90, 54, 0.15)",
|
|
929
|
+
nodeSurface: "#141414",
|
|
930
|
+
nodeSurfaceRaised: "#222222",
|
|
931
|
+
hairline: "#333333",
|
|
932
|
+
shadow: "rgba(0, 0, 0, 0.8)",
|
|
933
|
+
labelPlate: "#141414",
|
|
934
|
+
flatCards: true,
|
|
935
|
+
roles: {
|
|
936
|
+
...ROLE_COLORS,
|
|
937
|
+
compute: "#ff5a36",
|
|
938
|
+
client: "#f5f5f5",
|
|
939
|
+
data: "#9a9a9a",
|
|
940
|
+
network: "#38bdf8"
|
|
941
|
+
},
|
|
942
|
+
edges: {
|
|
943
|
+
request: "#ff5a36",
|
|
944
|
+
response: "#9a9a9a",
|
|
945
|
+
event: "#facc15",
|
|
946
|
+
dependency: "#5c5c5c"
|
|
947
|
+
},
|
|
948
|
+
fonts: {
|
|
949
|
+
title: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
950
|
+
nodeName: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
951
|
+
mono: "ui-monospace, SFMono-Regular, Menlo, monospace"
|
|
952
|
+
},
|
|
953
|
+
radiusMd: 6,
|
|
954
|
+
spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 40 }
|
|
955
|
+
},
|
|
956
|
+
sketchy: {
|
|
957
|
+
name: "sketchy",
|
|
958
|
+
canvas: "#fbfaf8",
|
|
959
|
+
surface: "#ffffff",
|
|
960
|
+
surfaceRaised: "#f6f3eb",
|
|
961
|
+
border: "#2d3142",
|
|
962
|
+
text: "#2d3142",
|
|
963
|
+
textMuted: "#4f5d75",
|
|
964
|
+
paper: "#fbfaf8",
|
|
965
|
+
ink: "#2d3142",
|
|
966
|
+
muted: "#4f5d75",
|
|
967
|
+
rule: "rgba(45, 49, 66, 0.18)",
|
|
968
|
+
soft: "#7a8399",
|
|
969
|
+
link: "#eb6c36",
|
|
970
|
+
gridMinor: "rgba(45, 49, 66, 0.05)",
|
|
971
|
+
gridMajor: "rgba(45, 49, 66, 0.08)",
|
|
972
|
+
vignette: "rgba(0, 0, 0, 0.06)",
|
|
973
|
+
accent: "#eb6c36",
|
|
974
|
+
accentTint: "rgba(235, 108, 54, 0.10)",
|
|
975
|
+
nodeSurface: "#ffffff",
|
|
976
|
+
nodeSurfaceRaised: "#fbfaf8",
|
|
977
|
+
hairline: "rgba(45, 49, 66, 0.35)",
|
|
978
|
+
shadow: "rgba(45, 49, 66, 0.08)",
|
|
979
|
+
labelPlate: "#ffffff",
|
|
980
|
+
flatCards: true,
|
|
981
|
+
roles: {
|
|
982
|
+
...ROLE_COLORS,
|
|
983
|
+
compute: "#2d3142",
|
|
984
|
+
client: "#4f5d75",
|
|
985
|
+
data: "#4f5d75"
|
|
986
|
+
},
|
|
987
|
+
edges: {
|
|
988
|
+
request: "#2d3142",
|
|
989
|
+
response: "#4f5d75",
|
|
990
|
+
event: "#eb6c36",
|
|
991
|
+
dependency: "#7a8399"
|
|
992
|
+
},
|
|
993
|
+
fonts: {
|
|
994
|
+
title: "Georgia, Times New Roman, serif",
|
|
995
|
+
nodeName: "ui-sans-serif, system-ui, sans-serif",
|
|
996
|
+
mono: "ui-monospace, SFMono-Regular, Menlo, monospace"
|
|
997
|
+
},
|
|
998
|
+
radiusMd: 4,
|
|
999
|
+
spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 40 }
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
|
|
720
1003
|
// ../core/src/parser.ts
|
|
721
1004
|
var ParseError = class extends Error {
|
|
722
1005
|
line;
|
|
@@ -2074,56 +2357,1437 @@ function classifyTechnology(id, label = "") {
|
|
|
2074
2357
|
};
|
|
2075
2358
|
}
|
|
2076
2359
|
|
|
2077
|
-
// ../core/src/
|
|
2078
|
-
function
|
|
2079
|
-
const
|
|
2080
|
-
|
|
2360
|
+
// ../core/src/syntax-diagnostics.ts
|
|
2361
|
+
function damerauLevenshteinDistance(a, b) {
|
|
2362
|
+
const al = a.length;
|
|
2363
|
+
const bl = b.length;
|
|
2364
|
+
if (al === 0) return bl;
|
|
2365
|
+
if (bl === 0) return al;
|
|
2366
|
+
const matrix = [];
|
|
2367
|
+
for (let i = 0; i <= al; i++) {
|
|
2368
|
+
matrix[i] = [i];
|
|
2369
|
+
}
|
|
2370
|
+
for (let j = 0; j <= bl; j++) {
|
|
2371
|
+
matrix[0][j] = j;
|
|
2372
|
+
}
|
|
2373
|
+
for (let i = 1; i <= al; i++) {
|
|
2374
|
+
for (let j = 1; j <= bl; j++) {
|
|
2375
|
+
const cost = a[i - 1].toLowerCase() === b[j - 1].toLowerCase() ? 0 : 1;
|
|
2376
|
+
let min = Math.min(
|
|
2377
|
+
matrix[i - 1][j] + 1,
|
|
2378
|
+
// deletion
|
|
2379
|
+
matrix[i][j - 1] + 1,
|
|
2380
|
+
// insertion
|
|
2381
|
+
matrix[i - 1][j - 1] + cost
|
|
2382
|
+
// substitution
|
|
2383
|
+
);
|
|
2384
|
+
if (i > 1 && j > 1 && a[i - 1].toLowerCase() === b[j - 2].toLowerCase() && a[i - 2].toLowerCase() === b[j - 1].toLowerCase()) {
|
|
2385
|
+
min = Math.min(min, matrix[i - 2][j - 2] + cost);
|
|
2386
|
+
}
|
|
2387
|
+
matrix[i][j] = min;
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
return matrix[al][bl];
|
|
2391
|
+
}
|
|
2392
|
+
var COMMON_CONTRACTIONS = {
|
|
2393
|
+
svc: "service",
|
|
2394
|
+
srv: "service",
|
|
2395
|
+
gw: "gateway",
|
|
2396
|
+
db: "database",
|
|
2397
|
+
app: "application",
|
|
2398
|
+
msg: "message",
|
|
2399
|
+
repo: "repository",
|
|
2400
|
+
cfg: "config",
|
|
2401
|
+
auth: "authenticator",
|
|
2402
|
+
mgr: "manager",
|
|
2403
|
+
fn: "function"
|
|
2404
|
+
};
|
|
2405
|
+
function splitIdentifierIntoTokens(s) {
|
|
2406
|
+
return s.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_\-.]+/g, " ").toLowerCase().trim().split(/\s+/).filter(Boolean);
|
|
2407
|
+
}
|
|
2408
|
+
function expandContractions(s) {
|
|
2409
|
+
const norm = s.toLowerCase().trim();
|
|
2410
|
+
const tokens = splitIdentifierIntoTokens(s);
|
|
2411
|
+
if (tokens.length === 0) return norm;
|
|
2412
|
+
const expandedTokens = tokens.map((t) => COMMON_CONTRACTIONS[t] ?? t);
|
|
2413
|
+
let res = expandedTokens.join("");
|
|
2414
|
+
if (tokens.length === 1) {
|
|
2415
|
+
for (const [k, v] of Object.entries(COMMON_CONTRACTIONS)) {
|
|
2416
|
+
if (k.length >= 2 && norm.length > k.length + 2 && norm.endsWith(k)) {
|
|
2417
|
+
res = norm.slice(0, -k.length) + v;
|
|
2418
|
+
break;
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
return res;
|
|
2423
|
+
}
|
|
2424
|
+
function findClosestMatch(word, candidates, maxDistance = 4) {
|
|
2425
|
+
const normalized = word.toLowerCase().trim();
|
|
2426
|
+
if (!normalized || !/^[a-zA-Z_][\w.-]*$/.test(normalized)) return null;
|
|
2427
|
+
const expandedWord = expandContractions(normalized);
|
|
2428
|
+
let bestMatch = null;
|
|
2429
|
+
let bestDistance = Infinity;
|
|
2430
|
+
const wordConsonants = normalized.replace(/[aeiou_-]/g, "");
|
|
2431
|
+
for (const candidate of candidates) {
|
|
2432
|
+
const candNorm = candidate.toLowerCase().trim();
|
|
2433
|
+
const expandedCand = expandContractions(candNorm);
|
|
2434
|
+
if (candNorm === normalized || expandedCand === expandedWord) {
|
|
2435
|
+
return { match: candidate, distance: 0 };
|
|
2436
|
+
}
|
|
2437
|
+
if (candNorm.startsWith(normalized) || normalized.startsWith(candNorm) || expandedCand.startsWith(expandedWord) || expandedWord.startsWith(expandedCand)) {
|
|
2438
|
+
const dist2 = Math.abs(candNorm.length - normalized.length);
|
|
2439
|
+
if (dist2 < bestDistance) {
|
|
2440
|
+
bestDistance = dist2;
|
|
2441
|
+
bestMatch = candidate;
|
|
2442
|
+
continue;
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
if (wordConsonants.length >= 3) {
|
|
2446
|
+
const candConsonants = candNorm.replace(/[aeiou_-]/g, "");
|
|
2447
|
+
if (candConsonants.startsWith(wordConsonants) || wordConsonants.startsWith(candConsonants)) {
|
|
2448
|
+
const dist2 = 1;
|
|
2449
|
+
if (dist2 < bestDistance) {
|
|
2450
|
+
bestDistance = dist2;
|
|
2451
|
+
bestMatch = candidate;
|
|
2452
|
+
continue;
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
const dist = Math.min(
|
|
2457
|
+
damerauLevenshteinDistance(normalized, candNorm),
|
|
2458
|
+
damerauLevenshteinDistance(expandedWord, expandedCand)
|
|
2459
|
+
);
|
|
2460
|
+
const dynamicMax = Math.max(2, Math.min(maxDistance, Math.floor(Math.max(candidate.length, word.length) * 0.5)));
|
|
2461
|
+
if (dist <= dynamicMax && dist < bestDistance) {
|
|
2462
|
+
bestDistance = dist;
|
|
2463
|
+
bestMatch = candidate;
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
return bestMatch ? { match: bestMatch, distance: bestDistance } : null;
|
|
2467
|
+
}
|
|
2468
|
+
var TOP_LEVEL_KEYWORDS = [
|
|
2469
|
+
"scene",
|
|
2470
|
+
"layout",
|
|
2471
|
+
"player",
|
|
2472
|
+
"var",
|
|
2473
|
+
"group",
|
|
2474
|
+
"beat",
|
|
2475
|
+
"style",
|
|
2476
|
+
"pattern",
|
|
2477
|
+
"edge",
|
|
2478
|
+
"annotation",
|
|
2479
|
+
"use"
|
|
2480
|
+
];
|
|
2481
|
+
var CUE_KEYWORDS = [
|
|
2482
|
+
"show",
|
|
2483
|
+
"hide",
|
|
2484
|
+
"glow",
|
|
2485
|
+
"focus",
|
|
2486
|
+
"frame",
|
|
2487
|
+
"use",
|
|
2488
|
+
"pulse",
|
|
2489
|
+
"highlight",
|
|
2490
|
+
"emphasize"
|
|
2491
|
+
];
|
|
2492
|
+
var LAYOUT_DIRECTIONS = ["LR", "RL", "TB", "BT"];
|
|
2493
|
+
var THEME_NAMES = Object.keys(THEMES);
|
|
2494
|
+
var ALL_NODE_KINDS = Array.from(
|
|
2495
|
+
/* @__PURE__ */ new Set([...Array.from(NODE_KINDS), ...Object.keys(NODE_ALIASES)])
|
|
2496
|
+
);
|
|
2497
|
+
var FOREIGN_SYNTAX_PATTERNS = [
|
|
2498
|
+
{
|
|
2499
|
+
type: "Mermaid",
|
|
2500
|
+
pattern: /^\s*(graph\s+(TB|TD|BT|RL|LR)|flowchart\s+(TB|TD|BT|RL|LR)|classDiagram|sequenceDiagram|erDiagram|stateDiagram|pie\s+title|gitGraph)/i,
|
|
2501
|
+
hint: `Mermaid syntax detected. Convert using 'transpile_to_markdy(source, format="mermaid")'.`
|
|
2502
|
+
},
|
|
2503
|
+
{
|
|
2504
|
+
type: "PlantUML",
|
|
2505
|
+
pattern: /^\s*(@startuml|@startmindmap|@startgantt|@startwbs)/i,
|
|
2506
|
+
hint: "PlantUML syntax detected. Markdy uses declarative 'scene', node declarations, and 'beat' blocks."
|
|
2507
|
+
},
|
|
2508
|
+
{
|
|
2509
|
+
type: "Graphviz DOT",
|
|
2510
|
+
pattern: /^\s*(digraph|strict\s+digraph|graph)\s+\w*\s*\{/i,
|
|
2511
|
+
hint: `Graphviz DOT syntax detected. Markdy uses 'scene', '<kind> <Id> ["Label"]', and 'beat' blocks.`
|
|
2512
|
+
},
|
|
2513
|
+
{
|
|
2514
|
+
type: "D2",
|
|
2515
|
+
pattern: /^\s*(direction\s*:|vars\s*:|classes\s*:)/i,
|
|
2516
|
+
hint: "D2 syntax detected. Markdy uses 'layout LR|TB' and 'beat <id>:' blocks."
|
|
2517
|
+
}
|
|
2518
|
+
];
|
|
2519
|
+
var INVALID_FLOW_OP_MAP = {
|
|
2520
|
+
"-->": "->",
|
|
2521
|
+
"->>": "->",
|
|
2522
|
+
"==>": "->",
|
|
2523
|
+
"<--": "<-",
|
|
2524
|
+
"<<-": "<-",
|
|
2525
|
+
"<==": "<-",
|
|
2526
|
+
"~~>": "~>",
|
|
2527
|
+
"~>>": "~>",
|
|
2528
|
+
"---": "--",
|
|
2529
|
+
"--->": "->",
|
|
2530
|
+
"-.-": "--",
|
|
2531
|
+
"-..->": "~>",
|
|
2532
|
+
">--": "->",
|
|
2533
|
+
">-": "->"
|
|
2534
|
+
};
|
|
2535
|
+
function diagnoseMarkdyCode(code, options = {}) {
|
|
2536
|
+
const issues = [];
|
|
2537
|
+
const lines = code.replace(/\r\n/g, "\n").split("\n");
|
|
2538
|
+
const declaredNodes = /* @__PURE__ */ new Set();
|
|
2539
|
+
const referencedNodes = /* @__PURE__ */ new Set();
|
|
2540
|
+
const checkArchitecture = options.checkArchitecture ?? true;
|
|
2541
|
+
for (const foreign of FOREIGN_SYNTAX_PATTERNS) {
|
|
2542
|
+
if (foreign.pattern.test(code)) {
|
|
2543
|
+
issues.push({
|
|
2544
|
+
line: 1,
|
|
2545
|
+
severity: "error",
|
|
2546
|
+
code: "FOREIGN_DIAGRAM_SYNTAX",
|
|
2547
|
+
message: `${foreign.type} syntax detected instead of MarkdyScript.`,
|
|
2548
|
+
snippet: lines.slice(0, 3).join("\n"),
|
|
2549
|
+
suggestion: foreign.hint,
|
|
2550
|
+
ruleExplanation: `MarkdyScript diagrams begin with 'scene [theme=paper layout=LR]', followed by node declarations ('<kind> <Id> ["Label"]') and dynamic 'beat' blocks.`
|
|
2551
|
+
});
|
|
2552
|
+
break;
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2556
|
+
const line = lines[i].trim();
|
|
2557
|
+
if (!line || line.startsWith("//") || line.startsWith("#")) continue;
|
|
2558
|
+
const nodeMatch = line.match(/^(\w[\w.-]*)\s+(\w[\w.-]*)/);
|
|
2559
|
+
if (nodeMatch) {
|
|
2560
|
+
const head = nodeMatch[1].toLowerCase();
|
|
2561
|
+
const id = nodeMatch[2];
|
|
2562
|
+
if (!TOP_LEVEL_KEYWORDS.includes(head) && !CUE_KEYWORDS.includes(head) && !["show", "hide", "glow", "focus", "frame"].includes(head)) {
|
|
2563
|
+
declaredNodes.add(id);
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
let insideBeat = false;
|
|
2568
|
+
let insideGroup = false;
|
|
2569
|
+
let insidePattern = false;
|
|
2570
|
+
let beatIndent = 0;
|
|
2571
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
2572
|
+
const rawLine = lines[idx];
|
|
2573
|
+
const lineNo = idx + 1;
|
|
2574
|
+
const trimmed = rawLine.trim();
|
|
2575
|
+
if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("#")) {
|
|
2576
|
+
continue;
|
|
2577
|
+
}
|
|
2578
|
+
const indent = rawLine.match(/^\s*/)?.[0].length ?? 0;
|
|
2579
|
+
if (insideBeat && indent <= beatIndent && !trimmed.startsWith("&")) {
|
|
2580
|
+
insideBeat = false;
|
|
2581
|
+
}
|
|
2582
|
+
if (insideGroup && indent === 0) {
|
|
2583
|
+
insideGroup = false;
|
|
2584
|
+
}
|
|
2585
|
+
if (insidePattern && indent === 0) {
|
|
2586
|
+
insidePattern = false;
|
|
2587
|
+
}
|
|
2588
|
+
const tokens = trimmed.split(/\s+/);
|
|
2589
|
+
const firstWord = tokens[0];
|
|
2590
|
+
const firstWordLower = firstWord.toLowerCase();
|
|
2591
|
+
if (firstWordLower.startsWith("scen") || firstWordLower === "secne" || firstWordLower === "scence") {
|
|
2592
|
+
if (firstWord !== "scene") {
|
|
2593
|
+
issues.push({
|
|
2594
|
+
line: lineNo,
|
|
2595
|
+
severity: "error",
|
|
2596
|
+
code: "TYPO_KEYWORD",
|
|
2597
|
+
message: `Unknown keyword '${firstWord}'. Did you mean 'scene'?`,
|
|
2598
|
+
snippet: trimmed,
|
|
2599
|
+
suggestion: `Replace '${firstWord}' with 'scene'.`,
|
|
2600
|
+
didYouMean: "scene",
|
|
2601
|
+
ruleExplanation: "Every Markdy diagram starts with a 'scene' directive: 'scene theme=paper width=1280 height=720'.",
|
|
2602
|
+
fix: { original: firstWord, replacement: "scene" }
|
|
2603
|
+
});
|
|
2604
|
+
}
|
|
2605
|
+
const themeMatch = trimmed.match(/\btheme=(\w+)/);
|
|
2606
|
+
if (themeMatch) {
|
|
2607
|
+
const themeVal = themeMatch[1].toLowerCase();
|
|
2608
|
+
if (!THEME_NAMES.includes(themeVal)) {
|
|
2609
|
+
const closeTheme = findClosestMatch(themeVal, THEME_NAMES);
|
|
2610
|
+
issues.push({
|
|
2611
|
+
line: lineNo,
|
|
2612
|
+
severity: "warning",
|
|
2613
|
+
code: "UNKNOWN_THEME_OR_PROPERTY",
|
|
2614
|
+
message: `Unknown theme '${themeMatch[1]}'. Available themes: ${THEME_NAMES.join(", ")}.`,
|
|
2615
|
+
snippet: trimmed,
|
|
2616
|
+
suggestion: closeTheme ? `Use theme '${closeTheme.match}'.` : `Choose from: ${THEME_NAMES.join(", ")}.`,
|
|
2617
|
+
didYouMean: closeTheme?.match,
|
|
2618
|
+
ruleExplanation: "Supported themes: paper, editorial, midnight, blueprint, graphite, nebula, sketchy, terminal.",
|
|
2619
|
+
fix: closeTheme ? { original: themeMatch[0], replacement: `theme=${closeTheme.match}` } : void 0
|
|
2620
|
+
});
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
const layoutMatch = trimmed.match(/\blayout\s*=?\s*(\w+)/i);
|
|
2624
|
+
if (layoutMatch) {
|
|
2625
|
+
const layoutVal = layoutMatch[1].toUpperCase();
|
|
2626
|
+
if (!LAYOUT_DIRECTIONS.includes(layoutVal)) {
|
|
2627
|
+
issues.push({
|
|
2628
|
+
line: lineNo,
|
|
2629
|
+
severity: "warning",
|
|
2630
|
+
code: "UNKNOWN_THEME_OR_PROPERTY",
|
|
2631
|
+
message: `Invalid layout direction '${layoutMatch[1]}'. Must be one of: LR, RL, TB, BT.`,
|
|
2632
|
+
snippet: trimmed,
|
|
2633
|
+
suggestion: "Use layout LR (left-to-right) or TB (top-to-bottom).",
|
|
2634
|
+
didYouMean: "LR",
|
|
2635
|
+
ruleExplanation: "Layout directions must be LR (Left-to-Right), RL, TB (Top-to-Bottom), or BT.",
|
|
2636
|
+
fix: { original: layoutMatch[0], replacement: "layout LR" }
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
continue;
|
|
2641
|
+
}
|
|
2642
|
+
if (firstWordLower === "layput" || firstWordLower === "layour" || firstWordLower === "layuot" || firstWordLower === "directon") {
|
|
2643
|
+
issues.push({
|
|
2644
|
+
line: lineNo,
|
|
2645
|
+
severity: "error",
|
|
2646
|
+
code: "TYPO_KEYWORD",
|
|
2647
|
+
message: `Typo in layout directive '${firstWord}'. Did you mean 'layout'?`,
|
|
2648
|
+
snippet: trimmed,
|
|
2649
|
+
suggestion: `Change '${firstWord}' to 'layout'.`,
|
|
2650
|
+
didYouMean: "layout",
|
|
2651
|
+
ruleExplanation: "Use 'layout LR' or 'layout TB' to define the diagram orientation.",
|
|
2652
|
+
fix: { original: firstWord, replacement: "layout" }
|
|
2653
|
+
});
|
|
2654
|
+
continue;
|
|
2655
|
+
}
|
|
2656
|
+
if (firstWordLower.startsWith("beat") || firstWordLower === "bea" || firstWordLower === "beats") {
|
|
2657
|
+
insideBeat = true;
|
|
2658
|
+
beatIndent = indent;
|
|
2659
|
+
if (firstWord !== "beat") {
|
|
2660
|
+
issues.push({
|
|
2661
|
+
line: lineNo,
|
|
2662
|
+
severity: "error",
|
|
2663
|
+
code: "TYPO_KEYWORD",
|
|
2664
|
+
message: `Unknown keyword '${firstWord}'. Did you mean 'beat'?`,
|
|
2665
|
+
snippet: trimmed,
|
|
2666
|
+
suggestion: `Replace '${firstWord}' with 'beat'.`,
|
|
2667
|
+
didYouMean: "beat",
|
|
2668
|
+
ruleExplanation: `Storyboard narrative steps start with 'beat <id> ["Caption"]:' followed by indented cues.`,
|
|
2669
|
+
fix: { original: firstWord, replacement: "beat" }
|
|
2670
|
+
});
|
|
2671
|
+
}
|
|
2672
|
+
if (!trimmed.endsWith(":") && !trimmed.endsWith("{")) {
|
|
2673
|
+
issues.push({
|
|
2674
|
+
line: lineNo,
|
|
2675
|
+
severity: "error",
|
|
2676
|
+
code: "MISSING_COLON",
|
|
2677
|
+
message: `Missing colon ':' at the end of beat header.`,
|
|
2678
|
+
snippet: trimmed,
|
|
2679
|
+
suggestion: `Append ':' to the beat declaration: '${trimmed}:'`,
|
|
2680
|
+
ruleExplanation: "Every beat block must end with a colon ':', e.g. `beat checkout \"Process Payment\":`.",
|
|
2681
|
+
fix: { original: trimmed, replacement: `${trimmed}:` }
|
|
2682
|
+
});
|
|
2683
|
+
}
|
|
2684
|
+
continue;
|
|
2685
|
+
}
|
|
2686
|
+
if (firstWordLower === "group" || firstWordLower === "groop" || firstWordLower === "grp") {
|
|
2687
|
+
insideGroup = true;
|
|
2688
|
+
if (firstWord !== "group") {
|
|
2689
|
+
issues.push({
|
|
2690
|
+
line: lineNo,
|
|
2691
|
+
severity: "error",
|
|
2692
|
+
code: "TYPO_KEYWORD",
|
|
2693
|
+
message: `Typo in group keyword '${firstWord}'. Did you mean 'group'?`,
|
|
2694
|
+
snippet: trimmed,
|
|
2695
|
+
suggestion: `Change '${firstWord}' to 'group'.`,
|
|
2696
|
+
didYouMean: "group",
|
|
2697
|
+
fix: { original: firstWord, replacement: "group" }
|
|
2698
|
+
});
|
|
2699
|
+
}
|
|
2700
|
+
if (!trimmed.includes(":")) {
|
|
2701
|
+
issues.push({
|
|
2702
|
+
line: lineNo,
|
|
2703
|
+
severity: "error",
|
|
2704
|
+
code: "MISSING_COLON",
|
|
2705
|
+
message: `Missing colon ':' in group definition.`,
|
|
2706
|
+
snippet: trimmed,
|
|
2707
|
+
suggestion: `Add a colon before group members: e.g. 'group <id> "Label": Node1 Node2'`,
|
|
2708
|
+
ruleExplanation: 'Groups require a colon: `group clients "User Tier": WebApp MobileApp`.'
|
|
2709
|
+
});
|
|
2710
|
+
} else {
|
|
2711
|
+
const colonIdx = trimmed.indexOf(":");
|
|
2712
|
+
const membersRaw = trimmed.slice(colonIdx + 1).trim();
|
|
2713
|
+
if (membersRaw) {
|
|
2714
|
+
const members = membersRaw.split(/\s+/).filter((m) => m && !m.includes("=") && !m.startsWith("$") && /^[a-zA-Z_][\w.-]*$/.test(m));
|
|
2715
|
+
for (const mem of members) {
|
|
2716
|
+
referencedNodes.add(mem);
|
|
2717
|
+
if (declaredNodes.size > 0 && !declaredNodes.has(mem)) {
|
|
2718
|
+
const closest = findClosestMatch(mem, declaredNodes);
|
|
2719
|
+
issues.push({
|
|
2720
|
+
line: lineNo,
|
|
2721
|
+
severity: "warning",
|
|
2722
|
+
code: "UNDEFINED_NODE_REFERENCE",
|
|
2723
|
+
message: `Group references undefined node '${mem}'.${closest ? ` Did you mean '${closest.match}'?` : ""}`,
|
|
2724
|
+
snippet: trimmed,
|
|
2725
|
+
suggestion: closest ? `Replace '${mem}' with declared node '${closest.match}'.` : `Ensure node '${mem}' is declared before adding it to a group.`,
|
|
2726
|
+
didYouMean: closest?.match,
|
|
2727
|
+
fix: closest ? { original: mem, replacement: closest.match } : void 0
|
|
2728
|
+
});
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
continue;
|
|
2734
|
+
}
|
|
2735
|
+
if (firstWordLower === "annotation") {
|
|
2736
|
+
const targetMatch = trimmed.match(/\btarget=([\w.-]+)/i);
|
|
2737
|
+
if (targetMatch) {
|
|
2738
|
+
const target = targetMatch[1];
|
|
2739
|
+
referencedNodes.add(target);
|
|
2740
|
+
if (declaredNodes.size > 0 && !declaredNodes.has(target)) {
|
|
2741
|
+
const closest = findClosestMatch(target, declaredNodes);
|
|
2742
|
+
issues.push({
|
|
2743
|
+
line: lineNo,
|
|
2744
|
+
severity: "warning",
|
|
2745
|
+
code: "UNDEFINED_NODE_REFERENCE",
|
|
2746
|
+
message: `Annotation targets undefined node '${target}'.${closest ? ` Did you mean '${closest.match}'?` : ""}`,
|
|
2747
|
+
snippet: trimmed,
|
|
2748
|
+
suggestion: closest ? `Replace target='${target}' with target='${closest.match}'.` : void 0,
|
|
2749
|
+
didYouMean: closest?.match,
|
|
2750
|
+
fix: closest ? { original: target, replacement: closest.match } : void 0
|
|
2751
|
+
});
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
continue;
|
|
2755
|
+
}
|
|
2756
|
+
if (!insideBeat) {
|
|
2757
|
+
const isCueWord = CUE_KEYWORDS.includes(firstWordLower) || firstWordLower === "shwo" || firstWordLower === "fram" || firstWordLower === "focsu" || firstWordLower === "gloww";
|
|
2758
|
+
const isFlowLine = /(-->|->|<-|<--|~>|--)/.test(trimmed);
|
|
2759
|
+
if (isCueWord || isFlowLine) {
|
|
2760
|
+
issues.push({
|
|
2761
|
+
line: lineNo,
|
|
2762
|
+
severity: "error",
|
|
2763
|
+
code: "CUE_OUTSIDE_BEAT",
|
|
2764
|
+
message: `Action cue or flow line '${trimmed}' found at top-level outside a 'beat' block.`,
|
|
2765
|
+
snippet: trimmed,
|
|
2766
|
+
suggestion: `Wrap this line inside an indented beat block:
|
|
2767
|
+
beat main "System Flow":
|
|
2768
|
+
${trimmed}`,
|
|
2769
|
+
ruleExplanation: "In MarkdyScript, top-level code is reserved for Directives, Nodes, and Groups. Animated flow transitions and visual cues (show, hide, glow, frame, focus) must reside inside a 'beat <name>:' block."
|
|
2770
|
+
});
|
|
2771
|
+
continue;
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
for (const [invalidOp, validOp] of Object.entries(INVALID_FLOW_OP_MAP)) {
|
|
2775
|
+
if (trimmed.includes(invalidOp)) {
|
|
2776
|
+
issues.push({
|
|
2777
|
+
line: lineNo,
|
|
2778
|
+
severity: "error",
|
|
2779
|
+
code: "INVALID_FLOW_OPERATOR",
|
|
2780
|
+
message: `Invalid flow operator '${invalidOp}'. Did you mean '${validOp}'?`,
|
|
2781
|
+
snippet: trimmed,
|
|
2782
|
+
suggestion: `Replace '${invalidOp}' with '${validOp}'.`,
|
|
2783
|
+
didYouMean: validOp,
|
|
2784
|
+
ruleExplanation: "Markdy uses 4 clean flow operators: '->' (request/call), '<-' (response/return), '~>' (asynchronous/event), and '--' (static dependency).",
|
|
2785
|
+
fix: { original: invalidOp, replacement: validOp }
|
|
2786
|
+
});
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
if (!insideBeat && !insideGroup && !insidePattern) {
|
|
2790
|
+
const match = trimmed.match(/^(\w[\w.-]*)\s+(\w[\w.-]*)(.*)$/);
|
|
2791
|
+
if (match) {
|
|
2792
|
+
const rawKind = match[1];
|
|
2793
|
+
const rawKindLower = rawKind.toLowerCase();
|
|
2794
|
+
const id = match[2];
|
|
2795
|
+
const rest = match[3].trim();
|
|
2796
|
+
if (!TOP_LEVEL_KEYWORDS.includes(rawKindLower)) {
|
|
2797
|
+
const canonical = canonicalNodeKind(rawKindLower);
|
|
2798
|
+
if (!NODE_KINDS.has(canonical)) {
|
|
2799
|
+
const matchKind = findClosestMatch(rawKindLower, ALL_NODE_KINDS);
|
|
2800
|
+
const resolvedKind = matchKind ? canonicalNodeKind(matchKind.match) : void 0;
|
|
2801
|
+
issues.push({
|
|
2802
|
+
line: lineNo,
|
|
2803
|
+
severity: "error",
|
|
2804
|
+
code: "TYPO_NODE_KIND",
|
|
2805
|
+
message: `Unknown node kind '${rawKind}'.${resolvedKind ? ` Did you mean '${resolvedKind}'?` : ""}`,
|
|
2806
|
+
snippet: trimmed,
|
|
2807
|
+
suggestion: resolvedKind ? `Change '${rawKind}' to '${resolvedKind}': '${resolvedKind} ${id} ...'` : `Use a valid node kind: service, database, cache, gateway, browser, mobile, worker, cloud, queue, storage, cluster, etc.`,
|
|
2808
|
+
didYouMean: resolvedKind,
|
|
2809
|
+
ruleExplanation: "Markdy requires semantic node kinds (e.g. browser, gateway, service, database, cache, queue, worker, cloud, storage, pod, cluster).",
|
|
2810
|
+
fix: resolvedKind ? { original: rawKind, replacement: resolvedKind } : void 0
|
|
2811
|
+
});
|
|
2812
|
+
}
|
|
2813
|
+
if (rest && !rest.startsWith('"') && !rest.startsWith("style=") && !rest.startsWith("icon=")) {
|
|
2814
|
+
const hasMultipleWords = rest.split(/\s+/).length > 1;
|
|
2815
|
+
if (hasMultipleWords && !rest.includes("=")) {
|
|
2816
|
+
issues.push({
|
|
2817
|
+
line: lineNo,
|
|
2818
|
+
severity: "error",
|
|
2819
|
+
code: "UNQUOTED_STRING_LABEL",
|
|
2820
|
+
message: `Unquoted multi-word label '${rest}'. String labels with spaces must be enclosed in double quotes.`,
|
|
2821
|
+
snippet: trimmed,
|
|
2822
|
+
suggestion: `Wrap the label in quotes: '${rawKind} ${id} "${rest}"'`,
|
|
2823
|
+
ruleExplanation: 'String labels containing spaces must be enclosed in double quotes `"..."`.',
|
|
2824
|
+
fix: { original: `${id} ${rest}`, replacement: `${id} "${rest}"` }
|
|
2825
|
+
});
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
const quoteCount = (rest.match(/"/g) || []).length;
|
|
2829
|
+
if (quoteCount % 2 !== 0) {
|
|
2830
|
+
issues.push({
|
|
2831
|
+
line: lineNo,
|
|
2832
|
+
severity: "error",
|
|
2833
|
+
code: "UNTERMINATED_STRING",
|
|
2834
|
+
message: `Unterminated string quote in node declaration.`,
|
|
2835
|
+
snippet: trimmed,
|
|
2836
|
+
suggestion: `Add closing quote: '${trimmed}"'`,
|
|
2837
|
+
fix: { original: trimmed, replacement: `${trimmed}"` }
|
|
2838
|
+
});
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
if (insideBeat) {
|
|
2844
|
+
if (trimmed.includes("->") || trimmed.includes("<-") || trimmed.includes("~>") || trimmed.includes("--")) {
|
|
2845
|
+
const cleanFlow = trimmed.replace(/"[^"]*"/g, " ");
|
|
2846
|
+
const flowTokens = cleanFlow.split(/->|<-|~>|--|&|\s+/).filter(Boolean);
|
|
2847
|
+
for (const token of flowTokens) {
|
|
2848
|
+
if (token.includes("=") || token.startsWith("$") || !/^[a-zA-Z_][\w.-]*$/.test(token) || ["and", "show", "hide", "glow", "focus", "frame", "pulse", "use"].includes(token.toLowerCase())) {
|
|
2849
|
+
continue;
|
|
2850
|
+
}
|
|
2851
|
+
referencedNodes.add(token);
|
|
2852
|
+
if (declaredNodes.size > 0 && !declaredNodes.has(token)) {
|
|
2853
|
+
const closest = findClosestMatch(token, declaredNodes);
|
|
2854
|
+
issues.push({
|
|
2855
|
+
line: lineNo,
|
|
2856
|
+
severity: "error",
|
|
2857
|
+
code: "UNDEFINED_NODE_REFERENCE",
|
|
2858
|
+
message: `Flow references undefined node '${token}'.${closest ? ` Did you mean '${closest.match}'?` : ""}`,
|
|
2859
|
+
snippet: trimmed,
|
|
2860
|
+
suggestion: closest ? `Replace '${token}' with declared node '${closest.match}'.` : `Declare node '${token}' at top of diagram before referencing it.`,
|
|
2861
|
+
didYouMean: closest?.match,
|
|
2862
|
+
ruleExplanation: "All nodes used in flow transitions must be declared beforehand (e.g. `service ${token}`).",
|
|
2863
|
+
fix: closest ? { original: token, replacement: closest.match } : void 0
|
|
2864
|
+
});
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
const cueMatch = trimmed.match(/^(show|hide|glow|focus|frame)\s+(.+)$/i);
|
|
2869
|
+
if (cueMatch) {
|
|
2870
|
+
const cueKind = cueMatch[1].toLowerCase();
|
|
2871
|
+
const targetsRaw = cueMatch[2].replace(/\b\w+=[^\s]+/g, "").trim();
|
|
2872
|
+
const targets = targetsRaw.split(/\s+/).filter((t) => t && !t.startsWith("$") && !t.startsWith('"') && /^[a-zA-Z_][\w.-]*$/.test(t));
|
|
2873
|
+
for (const target of targets) {
|
|
2874
|
+
referencedNodes.add(target);
|
|
2875
|
+
if (declaredNodes.size > 0 && !declaredNodes.has(target)) {
|
|
2876
|
+
const closest = findClosestMatch(target, declaredNodes);
|
|
2877
|
+
issues.push({
|
|
2878
|
+
line: lineNo,
|
|
2879
|
+
severity: "warning",
|
|
2880
|
+
code: "UNDEFINED_NODE_REFERENCE",
|
|
2881
|
+
message: `${cueKind} cue targets undefined node '${target}'.${closest ? ` Did you mean '${closest.match}'?` : ""}`,
|
|
2882
|
+
snippet: trimmed,
|
|
2883
|
+
suggestion: closest ? `Replace '${target}' with '${closest.match}'.` : `Ensure node '${target}' is declared before targeting it.`,
|
|
2884
|
+
didYouMean: closest?.match,
|
|
2885
|
+
fix: closest ? { original: target, replacement: closest.match } : void 0
|
|
2886
|
+
});
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2081
2892
|
try {
|
|
2082
|
-
ast = parse(
|
|
2893
|
+
const ast = parse(code);
|
|
2894
|
+
if (checkArchitecture) {
|
|
2895
|
+
const archViolations = validateArchitecture(ast);
|
|
2896
|
+
for (const v of archViolations) {
|
|
2897
|
+
issues.push({
|
|
2898
|
+
line: v.line ?? 1,
|
|
2899
|
+
severity: v.severity === "error" ? "error" : "warning",
|
|
2900
|
+
code: "ARCH_RULE_VIOLATION",
|
|
2901
|
+
message: `[${v.ruleName}] ${v.message}`,
|
|
2902
|
+
suggestion: `Refactor architecture to comply with Well-Architected governance: ${v.ruleName}.`,
|
|
2903
|
+
ruleExplanation: `Architecture Rule Preset: ${v.ruleName}`
|
|
2904
|
+
});
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
for (const d of ast.diagnostics) {
|
|
2908
|
+
if (d.message.includes("flow cycle detected")) {
|
|
2909
|
+
issues.push({
|
|
2910
|
+
line: d.line,
|
|
2911
|
+
severity: "warning",
|
|
2912
|
+
code: "FLOW_CYCLE_RETURN_EDGE",
|
|
2913
|
+
message: d.message,
|
|
2914
|
+
suggestion: "Use '<-' for return/response edges instead of '->' or '~>'.",
|
|
2915
|
+
ruleExplanation: "Cycle Safety: Always use '<-' when returning calls to callers to prevent cyclical layout rank collisions."
|
|
2916
|
+
});
|
|
2917
|
+
} else {
|
|
2918
|
+
const isDuplicate = issues.some((iss) => iss.line === d.line && iss.message === d.message);
|
|
2919
|
+
if (!isDuplicate) {
|
|
2920
|
+
issues.push({
|
|
2921
|
+
line: d.line,
|
|
2922
|
+
severity: d.severity === "error" ? "error" : "warning",
|
|
2923
|
+
code: "SYNTAX_ERROR",
|
|
2924
|
+
message: d.message
|
|
2925
|
+
});
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2083
2929
|
} catch (err) {
|
|
2084
|
-
|
|
2930
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2931
|
+
const lineMatch = errMsg.match(/line\s+(\d+):\s*(.*)/);
|
|
2932
|
+
const errLine = lineMatch ? Number(lineMatch[1]) : 1;
|
|
2933
|
+
const cleanMsg = lineMatch ? lineMatch[2] : errMsg;
|
|
2934
|
+
const alreadyCaptured = issues.some((iss) => iss.line === errLine && iss.severity === "error");
|
|
2935
|
+
if (!alreadyCaptured) {
|
|
2936
|
+
issues.push({
|
|
2937
|
+
line: errLine,
|
|
2938
|
+
severity: "error",
|
|
2939
|
+
code: "SYNTAX_ERROR",
|
|
2940
|
+
message: cleanMsg,
|
|
2941
|
+
snippet: lines[errLine - 1] ?? "",
|
|
2942
|
+
suggestion: "Review Markdy 4-step structure: Directives -> Semantic Nodes -> Groups -> Beats."
|
|
2943
|
+
});
|
|
2944
|
+
}
|
|
2085
2945
|
}
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2946
|
+
const autoRepair = repairMarkdyCode(code, {
|
|
2947
|
+
transpileMermaid: options.transpileMermaid,
|
|
2948
|
+
precomputedIssues: issues
|
|
2949
|
+
});
|
|
2950
|
+
const repairPrompt = buildAIHealingPrompt(code, issues, autoRepair.repairedCode);
|
|
2951
|
+
const errorCount = issues.filter((i) => i.severity === "error").length;
|
|
2952
|
+
const warningCount = issues.filter((i) => i.severity === "warning").length;
|
|
2953
|
+
const isValid = errorCount === 0;
|
|
2954
|
+
const summary = isValid ? `\u2705 Markdy code is valid with ${warningCount} warning(s).` : `\u274C Markdy code has ${errorCount} syntax error(s) and ${warningCount} warning(s).`;
|
|
2955
|
+
return {
|
|
2956
|
+
isValid,
|
|
2957
|
+
errorCount,
|
|
2958
|
+
warningCount,
|
|
2959
|
+
issues,
|
|
2960
|
+
repairedCode: autoRepair.repairedCode !== code ? autoRepair.repairedCode : void 0,
|
|
2961
|
+
repairPrompt,
|
|
2962
|
+
summary,
|
|
2963
|
+
declaredNodes: Array.from(declaredNodes),
|
|
2964
|
+
referencedNodes: Array.from(referencedNodes)
|
|
2965
|
+
};
|
|
2966
|
+
}
|
|
2967
|
+
function escapeRegex(s) {
|
|
2968
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2969
|
+
}
|
|
2970
|
+
function repairMarkdyCode(code, options) {
|
|
2971
|
+
const changes = [];
|
|
2972
|
+
if (options?.transpileMermaid && /^\s*(graph\s+(TB|TD|BT|RL|LR)|flowchart\s+(TB|TD|BT|RL|LR))/i.test(code)) {
|
|
2973
|
+
try {
|
|
2974
|
+
const transpiledCode = options.transpileMermaid(code);
|
|
2975
|
+
changes.push("Transpiled Mermaid flowchart to canonical MarkdyScript.");
|
|
2976
|
+
return {
|
|
2977
|
+
repairedCode: transpiledCode,
|
|
2978
|
+
changes,
|
|
2979
|
+
isFixed: true
|
|
2980
|
+
};
|
|
2981
|
+
} catch {
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
const lines = code.replace(/\r\n/g, "\n").split("\n");
|
|
2985
|
+
const repairedLines = [];
|
|
2986
|
+
const declaredNodes = /* @__PURE__ */ new Set();
|
|
2987
|
+
for (const line of lines) {
|
|
2988
|
+
const trimmed = line.trim();
|
|
2989
|
+
const nodeMatch = trimmed.match(/^(\w[\w.-]*)\s+(\w[\w.-]*)/);
|
|
2990
|
+
if (nodeMatch && !TOP_LEVEL_KEYWORDS.includes(nodeMatch[1].toLowerCase())) {
|
|
2991
|
+
declaredNodes.add(nodeMatch[2]);
|
|
2992
|
+
}
|
|
2101
2993
|
}
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2994
|
+
let inBeat = false;
|
|
2995
|
+
let beatIndent = 0;
|
|
2996
|
+
let hasBareCues = false;
|
|
2997
|
+
const bareCuesBuffer = [];
|
|
2998
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
2999
|
+
let line = lines[idx];
|
|
3000
|
+
const trimmed = line.trim();
|
|
3001
|
+
if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("#")) {
|
|
3002
|
+
repairedLines.push(line);
|
|
3003
|
+
continue;
|
|
3004
|
+
}
|
|
3005
|
+
const indent = line.match(/^\s*/)?.[0] ?? "";
|
|
3006
|
+
const indentLen = indent.length;
|
|
3007
|
+
let processed = trimmed;
|
|
3008
|
+
if (inBeat && indentLen <= beatIndent && !trimmed.startsWith("&") && !trimmed.startsWith("beat ") && !trimmed.startsWith("bea ")) {
|
|
3009
|
+
inBeat = false;
|
|
3010
|
+
}
|
|
3011
|
+
processed = processed.replace(/^scen\b|^secne\b|^scence\b/i, "scene");
|
|
3012
|
+
processed = processed.replace(/^layput\b|^layour\b|^layuot\b/i, "layout");
|
|
3013
|
+
processed = processed.replace(/^groop\b|^grp\b/i, "group");
|
|
3014
|
+
processed = processed.replace(/^bea\b|^beats\b/i, "beat");
|
|
3015
|
+
processed = processed.replace(/^patern\b/i, "pattern");
|
|
3016
|
+
for (const [inv, valid] of Object.entries(INVALID_FLOW_OP_MAP)) {
|
|
3017
|
+
if (processed.includes(inv)) {
|
|
3018
|
+
processed = processed.replaceAll(inv, valid);
|
|
3019
|
+
changes.push(`Line ${idx + 1}: Fixed invalid flow operator '${inv}' -> '${valid}'.`);
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
if (/^beat\s+/i.test(processed)) {
|
|
3023
|
+
inBeat = true;
|
|
3024
|
+
beatIndent = indentLen;
|
|
3025
|
+
if (/^beat\s+[\w.-]+(\s+"[^"]*")?\s*$/i.test(processed)) {
|
|
3026
|
+
processed = `${processed}:`;
|
|
3027
|
+
changes.push(`Line ${idx + 1}: Added missing colon ':' to beat header.`);
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
if (/^group\s+/i.test(processed)) {
|
|
3031
|
+
if (!processed.includes(":")) {
|
|
3032
|
+
const groupMatch = processed.match(/^group\s+([\w.-]+)(?:\s+"([^"]*)")?\s+(.+)$/i);
|
|
3033
|
+
if (groupMatch) {
|
|
3034
|
+
const gid = groupMatch[1];
|
|
3035
|
+
const label = groupMatch[2] ? ` "${groupMatch[2]}"` : "";
|
|
3036
|
+
const rest = groupMatch[3];
|
|
3037
|
+
processed = `group ${gid}${label}: ${rest}`;
|
|
3038
|
+
changes.push(`Line ${idx + 1}: Added missing colon ':' to group header.`);
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
if (!inBeat && !processed.startsWith("scene") && !processed.startsWith("layout") && !processed.startsWith("group")) {
|
|
3043
|
+
const match = processed.match(/^(\w[\w.-]*)\s+(\w[\w.-]*)(.*)$/);
|
|
3044
|
+
if (match) {
|
|
3045
|
+
const rawKind = match[1];
|
|
3046
|
+
const rawKindLower = rawKind.toLowerCase();
|
|
3047
|
+
const id = match[2];
|
|
3048
|
+
let rest = match[3].trim();
|
|
3049
|
+
const canonical = canonicalNodeKind(rawKindLower);
|
|
3050
|
+
if (!NODE_KINDS.has(canonical)) {
|
|
3051
|
+
const matchKind = findClosestMatch(rawKindLower, ALL_NODE_KINDS);
|
|
3052
|
+
if (matchKind) {
|
|
3053
|
+
const canonicalFix = canonicalNodeKind(matchKind.match);
|
|
3054
|
+
changes.push(`Line ${idx + 1}: Repaired node kind '${rawKind}' -> '${canonicalFix}'.`);
|
|
3055
|
+
processed = `${canonicalFix} ${id}${rest ? ` ${rest}` : ""}`;
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
if (rest && !rest.startsWith('"') && !rest.startsWith("style=") && !rest.startsWith("icon=") && rest.split(/\s+/).length > 1 && !rest.includes("=")) {
|
|
3059
|
+
changes.push(`Line ${idx + 1}: Enclosed multi-word label in quotes: "${rest}".`);
|
|
3060
|
+
processed = `${canonicalNodeKind(rawKindLower)} ${id} "${rest}"`;
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
if ((inBeat || processed.startsWith("group ")) && declaredNodes.size > 0) {
|
|
3065
|
+
if (processed.includes("->") || processed.includes("<-") || processed.includes("~>") || processed.includes("--") || processed.startsWith("group ")) {
|
|
3066
|
+
const cleanFlow = processed.replace(/"[^"]*"/g, '""');
|
|
3067
|
+
const flowTokens = cleanFlow.split(/->|<-|~>|--|&|:|\s+/).filter(Boolean);
|
|
3068
|
+
for (const tok of flowTokens) {
|
|
3069
|
+
if (!tok.startsWith("$") && !tok.includes("=") && /^[a-zA-Z_][\w.-]*$/.test(tok) && tok !== '""' && tok !== "group" && !["and", "show", "hide", "glow", "focus", "frame", "pulse", "use"].includes(tok.toLowerCase()) && !declaredNodes.has(tok)) {
|
|
3070
|
+
const match = findClosestMatch(tok, declaredNodes);
|
|
3071
|
+
if (match) {
|
|
3072
|
+
const regex = new RegExp(`\\b${escapeRegex(tok)}\\b`, "g");
|
|
3073
|
+
processed = processed.replace(regex, match.match);
|
|
3074
|
+
changes.push(`Line ${idx + 1}: Corrected typo in node reference '${tok}' -> '${match.match}'.`);
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
if (!inBeat) {
|
|
3081
|
+
const isCue = CUE_KEYWORDS.includes(processed.split(/\s+/)[0].toLowerCase());
|
|
3082
|
+
const isFlow = /(-->|->|<-|<--|~>|--)/.test(processed);
|
|
3083
|
+
if (isCue || isFlow) {
|
|
3084
|
+
hasBareCues = true;
|
|
3085
|
+
bareCuesBuffer.push(` ${processed}`);
|
|
3086
|
+
changes.push(`Line ${idx + 1}: Wrapped bare cue '${processed}' into 'beat main'.`);
|
|
3087
|
+
continue;
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
repairedLines.push(`${indent}${processed}`);
|
|
2105
3091
|
}
|
|
3092
|
+
if (hasBareCues && !lines.some((l) => l.trim().startsWith("beat "))) {
|
|
3093
|
+
repairedLines.push('\nbeat main "Flow":');
|
|
3094
|
+
repairedLines.push(...bareCuesBuffer);
|
|
3095
|
+
}
|
|
3096
|
+
const repairedCode = repairedLines.join("\n");
|
|
3097
|
+
let isFixed = false;
|
|
3098
|
+
try {
|
|
3099
|
+
parse(repairedCode);
|
|
3100
|
+
isFixed = true;
|
|
3101
|
+
} catch {
|
|
3102
|
+
isFixed = false;
|
|
3103
|
+
}
|
|
3104
|
+
return {
|
|
3105
|
+
repairedCode,
|
|
3106
|
+
changes,
|
|
3107
|
+
isFixed
|
|
3108
|
+
};
|
|
3109
|
+
}
|
|
3110
|
+
function buildAIHealingPrompt(sourceCode, issues, suggestedRepair) {
|
|
3111
|
+
if (issues.length === 0) {
|
|
3112
|
+
return "The MarkdyScript diagram is completely valid. No healing required.";
|
|
3113
|
+
}
|
|
3114
|
+
const errorIssues = issues.filter((i) => i.severity === "error" && i.code !== "ARCH_RULE_VIOLATION");
|
|
3115
|
+
const warningIssues = issues.filter((i) => i.severity === "warning" && i.code !== "ARCH_RULE_VIOLATION");
|
|
3116
|
+
const archIssues = issues.filter((i) => i.code === "ARCH_RULE_VIOLATION");
|
|
2106
3117
|
const promptSections = [
|
|
2107
|
-
"
|
|
2108
|
-
""
|
|
2109
|
-
"### Diagnostics:",
|
|
2110
|
-
...ast.diagnostics.map((d) => ` - Line ${d.line}: ${d.message}`),
|
|
2111
|
-
"",
|
|
2112
|
-
"### Architectural Violations:",
|
|
2113
|
-
...archViolations.map((v) => ` - [${v.severity.toUpperCase()}] ${v.ruleName}: ${v.message}`),
|
|
2114
|
-
"",
|
|
2115
|
-
"Please revise the diagram code to resolve all issues while preserving semantic nodes and beats:",
|
|
2116
|
-
"```markdy",
|
|
2117
|
-
sourceCode,
|
|
2118
|
-
"```"
|
|
3118
|
+
"### Markdy Diagram Syntax & Architectural Diagnostics",
|
|
3119
|
+
""
|
|
2119
3120
|
];
|
|
3121
|
+
if (errorIssues.length > 0) {
|
|
3122
|
+
promptSections.push("The following MarkdyScript failed to parse with syntax errors:");
|
|
3123
|
+
promptSections.push("");
|
|
3124
|
+
promptSections.push("### Diagnostics & Critical Errors:");
|
|
3125
|
+
for (const err of errorIssues) {
|
|
3126
|
+
const lineStr = err.line ? ` - Line ${err.line}: ` : " - ";
|
|
3127
|
+
promptSections.push(`${lineStr}${err.message}`);
|
|
3128
|
+
if (err.snippet) {
|
|
3129
|
+
promptSections.push(` *Problem:* \`${err.snippet}\``);
|
|
3130
|
+
}
|
|
3131
|
+
if (err.suggestion) {
|
|
3132
|
+
promptSections.push(` *Recommendation:* ${err.suggestion}`);
|
|
3133
|
+
}
|
|
3134
|
+
if (err.didYouMean) {
|
|
3135
|
+
promptSections.push(` *Did you mean:* \`${err.didYouMean}\`?`);
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
promptSections.push("");
|
|
3139
|
+
}
|
|
3140
|
+
if (archIssues.length > 0) {
|
|
3141
|
+
promptSections.push("### Architectural Violations:");
|
|
3142
|
+
for (const v of archIssues) {
|
|
3143
|
+
const lineStr = v.line ? ` (line ${v.line})` : "";
|
|
3144
|
+
promptSections.push(` - [${v.severity.toUpperCase()}] ${v.message}${lineStr}`);
|
|
3145
|
+
if (v.suggestion) {
|
|
3146
|
+
promptSections.push(` *Recommendation:* ${v.suggestion}`);
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
promptSections.push("");
|
|
3150
|
+
} else if (warningIssues.length > 0) {
|
|
3151
|
+
promptSections.push("### Diagnostics & Warnings:");
|
|
3152
|
+
for (const w of warningIssues) {
|
|
3153
|
+
const lineStr = w.line ? ` - Line ${w.line}: ` : " - ";
|
|
3154
|
+
promptSections.push(`${lineStr}${w.message}`);
|
|
3155
|
+
if (w.suggestion) {
|
|
3156
|
+
promptSections.push(` *Recommendation:* ${w.suggestion}`);
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
promptSections.push("");
|
|
3160
|
+
}
|
|
3161
|
+
promptSections.push("### Canonical Markdy Mental Model Guidelines:");
|
|
3162
|
+
promptSections.push("1. **Directives**: `scene theme=paper width=1280 height=720` and `layout LR` (or TB).");
|
|
3163
|
+
promptSections.push('2. **Nodes**: `<kind> <Id> ["Human Label"]` (e.g. `service Orders "Order Service"`, `database DB "PostgreSQL"`). Labels with spaces MUST be in double quotes.');
|
|
3164
|
+
promptSections.push('3. **Groups**: `group <id> "<Label>": Node1 Node2`');
|
|
3165
|
+
promptSections.push('4. **Beats**: `beat <id> "<Caption>":` with indented flows and cues.');
|
|
3166
|
+
promptSections.push("5. **Cycle Safety**: Always use `<-` for response/return edges to prevent layout rank cycles. Never use `->` to return to a previous node.");
|
|
3167
|
+
promptSections.push("");
|
|
3168
|
+
if (suggestedRepair && suggestedRepair !== sourceCode) {
|
|
3169
|
+
promptSections.push("### Proposed Repaired MarkdyScript:");
|
|
3170
|
+
promptSections.push("```markdy");
|
|
3171
|
+
promptSections.push(suggestedRepair);
|
|
3172
|
+
promptSections.push("```");
|
|
3173
|
+
promptSections.push("");
|
|
3174
|
+
}
|
|
3175
|
+
promptSections.push("Please revise the diagram code to resolve all issues while preserving semantic nodes and beats:");
|
|
3176
|
+
promptSections.push("```markdy");
|
|
3177
|
+
promptSections.push(sourceCode);
|
|
3178
|
+
promptSections.push("```");
|
|
3179
|
+
return promptSections.join("\n");
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
// ../core/src/intellicode.ts
|
|
3183
|
+
var POPULAR_TECHS = [
|
|
3184
|
+
// Databases
|
|
3185
|
+
{ name: "PostgreSQL", aliases: ["postgres", "postgresql", "psql"], kind: "database", defaultId: "Postgres", label: "PostgreSQL 16", desc: "ACID relational SQL database", category: "database" },
|
|
3186
|
+
{ name: "MySQL", aliases: ["mysql", "mariadb"], kind: "database", defaultId: "MySQL", label: "MySQL 8.0", desc: "Relational database", category: "database" },
|
|
3187
|
+
{ name: "MongoDB", aliases: ["mongo", "mongodb"], kind: "database", defaultId: "MongoDB", label: "MongoDB Atlas", desc: "Document NoSQL database", category: "database" },
|
|
3188
|
+
{ name: "DynamoDB", aliases: ["dynamo", "dynamodb"], kind: "database", defaultId: "DynamoDB", label: "AWS DynamoDB", desc: "Serverless key-value NoSQL", category: "database" },
|
|
3189
|
+
{ name: "ClickHouse", aliases: ["clickhouse"], kind: "database", defaultId: "ClickHouse", label: "ClickHouse OLAP", desc: "Real-time columnar analytics", category: "database" },
|
|
3190
|
+
{ name: "Snowflake", aliases: ["snowflake"], kind: "database", defaultId: "Snowflake", label: "Snowflake Data Warehouse", desc: "Cloud data warehouse", category: "database" },
|
|
3191
|
+
{ name: "Neo4j", aliases: ["neo4j", "graphdb"], kind: "database", defaultId: "Neo4j", label: "Neo4j Graph Database", desc: "Property graph engine", category: "database" },
|
|
3192
|
+
{ name: "Pinecone", aliases: ["pinecone", "weaviate", "qdrant", "chroma"], kind: "database", defaultId: "Pinecone", label: "Pinecone Vector DB", desc: "Vector similarity search for AI/RAG", category: "ai" },
|
|
3193
|
+
// Caching
|
|
3194
|
+
{ name: "Redis", aliases: ["redis", "valkey", "dragonfly"], kind: "cache", defaultId: "Redis", label: "Redis Cluster", desc: "In-memory sub-millisecond cache", category: "cache" },
|
|
3195
|
+
{ name: "Memcached", aliases: ["memcached"], kind: "cache", defaultId: "Memcached", label: "Memcached", desc: "Distributed memory object cache", category: "cache" },
|
|
3196
|
+
// Messaging & Streams
|
|
3197
|
+
{ name: "Kafka", aliases: ["kafka", "redpanda", "confluent"], kind: "queue", defaultId: "Kafka", label: "Kafka Event Stream", desc: "High-throughput event bus", category: "queue" },
|
|
3198
|
+
{ name: "RabbitMQ", aliases: ["rabbitmq", "amqp"], kind: "queue", defaultId: "RabbitMQ", label: "RabbitMQ Broker", desc: "AMQP message queue", category: "queue" },
|
|
3199
|
+
{ name: "AWS SQS", aliases: ["sqs"], kind: "queue", defaultId: "SQS", label: "Amazon SQS", desc: "Managed queue service", category: "queue" },
|
|
3200
|
+
{ name: "NATS", aliases: ["nats"], kind: "queue", defaultId: "NATS", label: "NATS JetStream", desc: "Lightweight cloud native pub/sub", category: "queue" },
|
|
3201
|
+
// Ingress & Gateways
|
|
3202
|
+
{ name: "Kong", aliases: ["kong"], kind: "gateway", defaultId: "KongGateway", label: "Kong API Gateway", desc: "Cloud native API gateway", category: "gateway" },
|
|
3203
|
+
{ name: "Envoy", aliases: ["envoy"], kind: "gateway", defaultId: "EnvoyProxy", label: "Envoy Service Proxy", desc: "High-performance edge/service proxy", category: "gateway" },
|
|
3204
|
+
{ name: "Nginx", aliases: ["nginx"], kind: "gateway", defaultId: "Nginx", label: "Nginx Ingress", desc: "Reverse proxy & load balancer", category: "gateway" },
|
|
3205
|
+
{ name: "Traefik", aliases: ["traefik"], kind: "gateway", defaultId: "Traefik", label: "Traefik Proxy", desc: "Dynamic reverse proxy for microservices", category: "gateway" },
|
|
3206
|
+
{ name: "Cloudflare", aliases: ["cloudflare"], kind: "cdn", defaultId: "Cloudflare", label: "Cloudflare Edge / WAF", desc: "Edge CDN & security perimeter", category: "security" },
|
|
3207
|
+
// Clients & Frontends
|
|
3208
|
+
{ name: "React Web App", aliases: ["react", "next", "nextjs", "frontend", "webapp"], kind: "browser", defaultId: "WebApp", label: "Next.js Web Client", desc: "React / Next.js web application", category: "client" },
|
|
3209
|
+
{ name: "Mobile App", aliases: ["ios", "android", "flutter", "reactnative", "mobile"], kind: "mobile", defaultId: "MobileApp", label: "iOS / Android Mobile App", desc: "Native mobile client application", category: "client" },
|
|
3210
|
+
// Compute & Microservices
|
|
3211
|
+
{ name: "API Service", aliases: ["service", "api", "backend", "microservice"], kind: "service", defaultId: "ApiService", label: "API Core Service", desc: "Backend REST/gRPC microservice", category: "compute" },
|
|
3212
|
+
{ name: "Auth Service", aliases: ["auth", "auth0", "keycloak", "clerk", "cognito"], kind: "service", defaultId: "AuthService", label: "Auth & Identity Provider", desc: "OAuth2 / OIDC authentication service", category: "security" },
|
|
3213
|
+
{ name: "Order Service", aliases: ["order", "ordersvc"], kind: "service", defaultId: "OrderService", label: "Order Processing Service", desc: "Transactional order service", category: "compute" },
|
|
3214
|
+
{ name: "Payment Gateway", aliases: ["payment", "stripe", "paypal"], kind: "service", defaultId: "PaymentService", label: "Stripe Payment Gateway", desc: "Payment processing integration", category: "compute" },
|
|
3215
|
+
{ name: "Async Worker", aliases: ["worker", "celery", "sidekiq"], kind: "worker", defaultId: "AsyncWorker", label: "Background Job Worker", desc: "Asynchronous task consumer worker", category: "compute" },
|
|
3216
|
+
{ name: "Serverless Function", aliases: ["lambda", "function", "cloudfunction"], kind: "lambda", defaultId: "LambdaFunc", label: "AWS Lambda / Cloud Function", desc: "Event-driven serverless function", category: "compute" },
|
|
3217
|
+
{ name: "Kubernetes Pod", aliases: ["k8s", "pod", "kubernetes"], kind: "pod", defaultId: "AppPod", label: "Kubernetes Pod Cluster", desc: "Containerized workload pod", category: "compute" },
|
|
3218
|
+
// AI & LLMs
|
|
3219
|
+
{ name: "Gemini / LLM Engine", aliases: ["gemini", "openai", "gpt", "claude", "llm", "ai"], kind: "service", defaultId: "AIEngine", label: "Gemini 2.5 Flash / AI Core", desc: "Large Language Model inference engine", category: "ai" },
|
|
3220
|
+
// Storage & Backend as a Service
|
|
3221
|
+
{ name: "S3 Object Storage", aliases: ["s3", "storage", "gcs", "blob", "r2"], kind: "storage", defaultId: "S3Storage", label: "AWS S3 / Cloud Storage", desc: "Object storage bucket", category: "storage" },
|
|
3222
|
+
{ name: "Supabase", aliases: ["supabase"], kind: "database", defaultId: "SupabaseDB", label: "Supabase PostgreSQL", desc: "Open-source Firebase alternative with Postgres", category: "database" },
|
|
3223
|
+
{ name: "Firebase Firestore", aliases: ["firebase", "firestore"], kind: "database", defaultId: "Firestore", label: "Firebase Firestore", desc: "Serverless real-time document database", category: "database" },
|
|
3224
|
+
// Observability & Telemetry
|
|
3225
|
+
{ name: "Prometheus", aliases: ["prometheus", "metrics"], kind: "metric", defaultId: "Prometheus", label: "Prometheus Metrics", desc: "Time-series metrics monitoring engine", category: "compute" },
|
|
3226
|
+
{ name: "Grafana", aliases: ["grafana", "dashboard"], kind: "surface", defaultId: "GrafanaUI", label: "Grafana Dashboard", desc: "Visualization & telemetry analytics dashboard", category: "compute" },
|
|
3227
|
+
{ name: "OpenSearch / Elasticsearch", aliases: ["opensearch", "elasticsearch", "elastic"], kind: "database", defaultId: "OpenSearch", label: "OpenSearch Cluster", desc: "Distributed search & log analytics engine", category: "database" },
|
|
3228
|
+
{ name: "Sentry", aliases: ["sentry", "errors"], kind: "service", defaultId: "Sentry", label: "Sentry Error Tracker", desc: "Application monitoring and error tracking", category: "compute" }
|
|
3229
|
+
];
|
|
3230
|
+
function extractDiagramContext(text, cursorLine = 0, cursorCol = 0) {
|
|
3231
|
+
const lines = text.split(/\r?\n/);
|
|
3232
|
+
const declaredNodes = [];
|
|
3233
|
+
const declaredGroups = [];
|
|
3234
|
+
const declaredBeats = [];
|
|
3235
|
+
let theme;
|
|
3236
|
+
let layout;
|
|
3237
|
+
let diagramType;
|
|
3238
|
+
let insideBeat = false;
|
|
3239
|
+
let currentBeatName;
|
|
3240
|
+
let insideGroup = false;
|
|
3241
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3242
|
+
const rawLine = lines[i];
|
|
3243
|
+
const trimmed = rawLine.trim();
|
|
3244
|
+
if (!trimmed || trimmed.startsWith("//")) continue;
|
|
3245
|
+
const themeMatch = /theme\s*=\s*([a-zA-Z0-9_-]+)/.exec(trimmed);
|
|
3246
|
+
if (themeMatch) theme = themeMatch[1];
|
|
3247
|
+
const layoutMatch = /layout\s+([A-Z]{2})/.exec(trimmed);
|
|
3248
|
+
if (layoutMatch) layout = layoutMatch[1];
|
|
3249
|
+
const typeMatch = /type\s*=\s*([a-zA-Z0-9_-]+)/.exec(trimmed);
|
|
3250
|
+
if (typeMatch) diagramType = typeMatch[1];
|
|
3251
|
+
const beatMatch = /^beat\s+([\w.-]+)(?:\s+"([^"]*)")?:/i.exec(trimmed);
|
|
3252
|
+
if (beatMatch) {
|
|
3253
|
+
declaredBeats.push({ id: beatMatch[1], label: beatMatch[2] || beatMatch[1], line: i });
|
|
3254
|
+
if (i <= cursorLine) {
|
|
3255
|
+
insideBeat = true;
|
|
3256
|
+
currentBeatName = beatMatch[1];
|
|
3257
|
+
}
|
|
3258
|
+
continue;
|
|
3259
|
+
}
|
|
3260
|
+
if (insideBeat && i === cursorLine && !rawLine.startsWith(" ") && !rawLine.startsWith(" ") && trimmed) {
|
|
3261
|
+
if (!trimmed.startsWith("beat")) {
|
|
3262
|
+
insideBeat = false;
|
|
3263
|
+
currentBeatName = void 0;
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
const groupMatch = /^group\s+([\w.-]+)(?:\s+"([^"]*)")?:\s*(.*)$/i.exec(trimmed);
|
|
3267
|
+
if (groupMatch) {
|
|
3268
|
+
const members = groupMatch[3] ? groupMatch[3].trim().split(/\s+/) : [];
|
|
3269
|
+
declaredGroups.push({ id: groupMatch[1], label: groupMatch[2] || groupMatch[1], members, line: i });
|
|
3270
|
+
continue;
|
|
3271
|
+
}
|
|
3272
|
+
const nodeMatch = /^(\w[\w.-]*)\s+(\w[\w.-]*)(?:\s+"([^"]*)")?/i.exec(trimmed);
|
|
3273
|
+
if (nodeMatch) {
|
|
3274
|
+
const kind = nodeMatch[1].toLowerCase();
|
|
3275
|
+
const id = nodeMatch[2];
|
|
3276
|
+
const label = nodeMatch[3] || humanizeId(id);
|
|
3277
|
+
if (!["scene", "layout", "group", "beat", "style", "pattern", "use", "var", "edge", "theme"].includes(kind) && (NODE_KINDS.has(kind) || NODE_KINDS.has(canonicalNodeKind(kind)) || TECHNICAL_NODE_TYPES.includes(kind))) {
|
|
3278
|
+
declaredNodes.push({ id, kind, label, line: i });
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
const currentLineText = lines[cursorLine] ?? "";
|
|
3283
|
+
const linePrefix = currentLineText.slice(0, cursorCol);
|
|
3284
|
+
const tokenMatch = /[\w$.-><~]*$/.exec(linePrefix);
|
|
3285
|
+
const tokenPrefix = tokenMatch ? tokenMatch[0] : "";
|
|
3286
|
+
let cursorInsideBeat = false;
|
|
3287
|
+
for (let i = cursorLine; i >= 0; i--) {
|
|
3288
|
+
const l = lines[i] ?? "";
|
|
3289
|
+
const trimL = l.trim();
|
|
3290
|
+
if (trimL.startsWith("//") || !trimL) continue;
|
|
3291
|
+
if (/^beat\s+[\w.-]+.*:/i.test(trimL)) {
|
|
3292
|
+
cursorInsideBeat = true;
|
|
3293
|
+
break;
|
|
3294
|
+
}
|
|
3295
|
+
if (/^(scene|layout|group|style|pattern|service|database|cache|queue|gateway|browser|mobile|client|worker|storage|cdn|pod|lambda|user)\b/i.test(trimL)) {
|
|
3296
|
+
cursorInsideBeat = false;
|
|
3297
|
+
break;
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
2120
3300
|
return {
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
3301
|
+
declaredNodes,
|
|
3302
|
+
declaredGroups,
|
|
3303
|
+
declaredBeats,
|
|
3304
|
+
theme,
|
|
3305
|
+
layout,
|
|
3306
|
+
diagramType,
|
|
3307
|
+
insideBeat: cursorInsideBeat,
|
|
3308
|
+
currentBeatName,
|
|
3309
|
+
insideGroup,
|
|
3310
|
+
lineNo: cursorLine,
|
|
3311
|
+
lineText: currentLineText,
|
|
3312
|
+
linePrefix,
|
|
3313
|
+
tokenPrefix
|
|
2125
3314
|
};
|
|
2126
3315
|
}
|
|
3316
|
+
function isClientRole(kind) {
|
|
3317
|
+
return nodeRole(kind) === "client";
|
|
3318
|
+
}
|
|
3319
|
+
function isGatewayRole(kind) {
|
|
3320
|
+
const c = canonicalNodeKind(kind);
|
|
3321
|
+
return ["gateway", "api_gateway", "proxy", "load_balancer", "cdn", "firewall", "waf"].includes(c);
|
|
3322
|
+
}
|
|
3323
|
+
function isComputeRole(kind) {
|
|
3324
|
+
return nodeRole(kind) === "compute";
|
|
3325
|
+
}
|
|
3326
|
+
function isDatabaseKind(kind) {
|
|
3327
|
+
const c = canonicalNodeKind(kind);
|
|
3328
|
+
return ["database", "db", "sql", "nosql", "warehouse", "table", "lake"].includes(c);
|
|
3329
|
+
}
|
|
3330
|
+
function isCacheKind(kind) {
|
|
3331
|
+
const c = canonicalNodeKind(kind);
|
|
3332
|
+
return ["cache"].includes(c);
|
|
3333
|
+
}
|
|
3334
|
+
function isQueueRole(kind) {
|
|
3335
|
+
return nodeRole(kind) === "messaging";
|
|
3336
|
+
}
|
|
3337
|
+
function getIntelliCodeCompletions(docText, cursorLine, cursorCol) {
|
|
3338
|
+
const ctx = extractDiagramContext(docText, cursorLine, cursorCol);
|
|
3339
|
+
const items = [];
|
|
3340
|
+
const flowMatch = /([\w.-]+)\s*(->|<-|~>|<->|\.\.>)\s*([\w.-]*)$/.exec(ctx.linePrefix);
|
|
3341
|
+
if (flowMatch) {
|
|
3342
|
+
const sourceNodeId = flowMatch[1];
|
|
3343
|
+
const op = flowMatch[2];
|
|
3344
|
+
const filter = flowMatch[3].toLowerCase();
|
|
3345
|
+
const sourceNode = ctx.declaredNodes.find((n) => n.id === sourceNodeId);
|
|
3346
|
+
const sourceKind = sourceNode ? sourceNode.kind : "service";
|
|
3347
|
+
for (const node of ctx.declaredNodes) {
|
|
3348
|
+
if (node.id === sourceNodeId && op !== "<->") continue;
|
|
3349
|
+
let boost = 2;
|
|
3350
|
+
if (isClientRole(sourceKind)) {
|
|
3351
|
+
if (isGatewayRole(node.kind)) boost = 10;
|
|
3352
|
+
else if (isComputeRole(node.kind)) boost = 8;
|
|
3353
|
+
else boost = 2;
|
|
3354
|
+
} else if (isGatewayRole(sourceKind)) {
|
|
3355
|
+
if (isComputeRole(node.kind)) boost = 10;
|
|
3356
|
+
else boost = 3;
|
|
3357
|
+
} else if (isComputeRole(sourceKind)) {
|
|
3358
|
+
if (isDatabaseKind(node.kind) || isCacheKind(node.kind) || isQueueRole(node.kind)) boost = 10;
|
|
3359
|
+
else if (isComputeRole(node.kind)) boost = 7;
|
|
3360
|
+
else boost = 3;
|
|
3361
|
+
} else if (isQueueRole(sourceKind)) {
|
|
3362
|
+
if (isComputeRole(node.kind)) boost = 10;
|
|
3363
|
+
else boost = 2;
|
|
3364
|
+
}
|
|
3365
|
+
if (op === "<-") {
|
|
3366
|
+
boost = isClientRole(node.kind) || isGatewayRole(node.kind) ? 10 : 3;
|
|
3367
|
+
}
|
|
3368
|
+
items.push({
|
|
3369
|
+
label: node.id,
|
|
3370
|
+
insertText: `${node.id} `,
|
|
3371
|
+
kind: "node",
|
|
3372
|
+
detail: `${node.kind} \xB7 "${node.label}"`,
|
|
3373
|
+
documentation: `Connect flow ${sourceNodeId} ${op} ${node.id}`,
|
|
3374
|
+
boost
|
|
3375
|
+
});
|
|
3376
|
+
}
|
|
3377
|
+
if (!filter || filter.startsWith('"')) {
|
|
3378
|
+
const sourceRole = nodeRole(sourceKind);
|
|
3379
|
+
const sampleLabels = getCommonEdgeLabels(sourceRole, op);
|
|
3380
|
+
for (const lbl of sampleLabels) {
|
|
3381
|
+
items.push({
|
|
3382
|
+
label: `"${lbl}"`,
|
|
3383
|
+
insertText: `"${lbl}"`,
|
|
3384
|
+
kind: "snippet",
|
|
3385
|
+
detail: "Flow description label",
|
|
3386
|
+
boost: 1
|
|
3387
|
+
});
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
return items;
|
|
3391
|
+
}
|
|
3392
|
+
const nodeStartMatch = /^\s*([\w.-]+)\s+$/.exec(ctx.linePrefix);
|
|
3393
|
+
if (nodeStartMatch) {
|
|
3394
|
+
const candidateId = nodeStartMatch[1];
|
|
3395
|
+
const isDeclared = ctx.declaredNodes.some((n) => n.id === candidateId);
|
|
3396
|
+
if (isDeclared) {
|
|
3397
|
+
items.push(
|
|
3398
|
+
{ label: "->", insertText: "-> ", kind: "flowOp", detail: "Sync request / call", documentation: "Synchronous HTTP, gRPC, or RPC call.", boost: 10 },
|
|
3399
|
+
{ label: "~>", insertText: "~> ", kind: "flowOp", detail: "Async event / pub-sub", documentation: "Asynchronous event stream or message queue dispatch.", boost: 9 },
|
|
3400
|
+
{ label: "<-", insertText: "<- ", kind: "flowOp", detail: "Response / return payload", documentation: "Synchronous return response payload.", boost: 8 },
|
|
3401
|
+
{ label: "<->", insertText: "<-> ", kind: "flowOp", detail: "Bidirectional streaming / mTLS", documentation: "Two-way streaming WebSocket or mutual TLS socket.", boost: 7 },
|
|
3402
|
+
{ label: "..>", insertText: "..> ", kind: "flowOp", detail: "Weak dependency link", documentation: "Dashed architectural dependency relationship.", boost: 6 }
|
|
3403
|
+
);
|
|
3404
|
+
return items;
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
if (/(theme\s*=\s*|theme\s+)\w*$/i.test(ctx.linePrefix)) {
|
|
3408
|
+
const themeEntries = [
|
|
3409
|
+
{ name: "paper", desc: "Warm editorial white canvas with crisp modern typography" },
|
|
3410
|
+
{ name: "midnight", desc: "Deep OLED dark theme with vibrant neon kinetic pulses" },
|
|
3411
|
+
{ name: "blueprint", desc: "Architectural blueprint cyan graph styling" },
|
|
3412
|
+
{ name: "nebula", desc: "Futuristic cosmic purple & indigo glow palette" },
|
|
3413
|
+
{ name: "editorial", desc: "High-contrast serif luxury publication layout" },
|
|
3414
|
+
{ name: "graphite", desc: "Sleek slate monochrome engineering aesthetic" },
|
|
3415
|
+
{ name: "terminal", desc: "Retro phosphor CRT hacker terminal green on black" },
|
|
3416
|
+
{ name: "sketchy", desc: "Hand-drawn sketchy whiteboard marker design" }
|
|
3417
|
+
];
|
|
3418
|
+
for (const t of themeEntries) {
|
|
3419
|
+
items.push({
|
|
3420
|
+
label: t.name,
|
|
3421
|
+
insertText: t.name,
|
|
3422
|
+
kind: "theme",
|
|
3423
|
+
detail: `theme=${t.name}`,
|
|
3424
|
+
documentation: t.desc,
|
|
3425
|
+
boost: 10
|
|
3426
|
+
});
|
|
3427
|
+
}
|
|
3428
|
+
return items;
|
|
3429
|
+
}
|
|
3430
|
+
if (/(layout\s+|direction\s*=\s*)\w*$/i.test(ctx.linePrefix)) {
|
|
3431
|
+
items.push(
|
|
3432
|
+
{ label: "LR", insertText: "LR", kind: "layout", detail: "Left-to-Right", documentation: "Horizontal left-to-right system flow layout.", boost: 10 },
|
|
3433
|
+
{ label: "TB", insertText: "TB", kind: "layout", detail: "Top-to-Bottom", documentation: "Vertical top-to-bottom hierarchy layout.", boost: 9 },
|
|
3434
|
+
{ label: "RL", insertText: "RL", kind: "layout", detail: "Right-to-Left", documentation: "Right-to-left reverse topology layout.", boost: 5 },
|
|
3435
|
+
{ label: "BT", insertText: "BT", kind: "layout", detail: "Bottom-to-Top", documentation: "Bottom-to-top inverted stack layout.", boost: 5 }
|
|
3436
|
+
);
|
|
3437
|
+
return items;
|
|
3438
|
+
}
|
|
3439
|
+
if (/type\s*=\s*\w*$/i.test(ctx.linePrefix)) {
|
|
3440
|
+
for (const t of DIAGRAM_TYPES) {
|
|
3441
|
+
items.push({
|
|
3442
|
+
label: t,
|
|
3443
|
+
insertText: t,
|
|
3444
|
+
kind: "diagramType",
|
|
3445
|
+
detail: `type=${t}`,
|
|
3446
|
+
documentation: `Specialized layout engine: ${t}`,
|
|
3447
|
+
boost: 8
|
|
3448
|
+
});
|
|
3449
|
+
}
|
|
3450
|
+
return items;
|
|
3451
|
+
}
|
|
3452
|
+
const cueMatch = /^\s*(show|glow|pulse|focus|frame|hide)\s+([\w$]*)$/i.exec(ctx.linePrefix);
|
|
3453
|
+
if (cueMatch) {
|
|
3454
|
+
const cueName = cueMatch[1].toLowerCase();
|
|
3455
|
+
items.push(
|
|
3456
|
+
{ label: "$nodes", insertText: "$nodes stagger=60ms", kind: "selector", detail: "All diagram nodes", documentation: "Selects all semantic nodes in the scene.", boost: 10 },
|
|
3457
|
+
{ label: "$title", insertText: "$title", kind: "selector", detail: "Diagram title banner", documentation: "Selects the title node.", boost: 8 },
|
|
3458
|
+
{ label: "$edges", insertText: "$edges", kind: "selector", detail: "All edge connectors", documentation: "Selects all flow connectors.", boost: 7 }
|
|
3459
|
+
);
|
|
3460
|
+
for (const node of ctx.declaredNodes) {
|
|
3461
|
+
items.push({
|
|
3462
|
+
label: node.id,
|
|
3463
|
+
insertText: `${node.id} `,
|
|
3464
|
+
kind: "node",
|
|
3465
|
+
detail: `${node.kind} \xB7 "${node.label}"`,
|
|
3466
|
+
boost: 9
|
|
3467
|
+
});
|
|
3468
|
+
}
|
|
3469
|
+
for (const group of ctx.declaredGroups) {
|
|
3470
|
+
items.push({
|
|
3471
|
+
label: group.id,
|
|
3472
|
+
insertText: cueName === "frame" ? `${group.id} zoom=1.2 dur=600ms` : `${group.id} `,
|
|
3473
|
+
kind: "group",
|
|
3474
|
+
detail: `group "${group.label}"`,
|
|
3475
|
+
documentation: `Group boundary with ${group.members.length} nodes.`,
|
|
3476
|
+
boost: 8
|
|
3477
|
+
});
|
|
3478
|
+
}
|
|
3479
|
+
if (cueName === "show" || cueName === "hide") {
|
|
3480
|
+
items.push({ label: "stagger=60ms", insertText: "stagger=60ms", kind: "attribute", detail: "Stagger delay", boost: 5 });
|
|
3481
|
+
} else if (cueName === "glow" || cueName === "pulse") {
|
|
3482
|
+
items.push(
|
|
3483
|
+
{ label: "color=#38bdf8", insertText: "color=#38bdf8", kind: "attribute", detail: "Sky blue kinetic glow", boost: 5 },
|
|
3484
|
+
{ label: "color=#e11d48", insertText: "color=#e11d48", kind: "attribute", detail: "Alert crimson pulse", boost: 5 },
|
|
3485
|
+
{ label: "color=#10b981", insertText: "color=#10b981", kind: "attribute", detail: "Success emerald glow", boost: 5 }
|
|
3486
|
+
);
|
|
3487
|
+
} else if (cueName === "frame") {
|
|
3488
|
+
items.push(
|
|
3489
|
+
{ label: "zoom=1.2 dur=600ms", insertText: "zoom=1.2 dur=600ms", kind: "attribute", detail: "Camera focus & zoom", boost: 5 }
|
|
3490
|
+
);
|
|
3491
|
+
}
|
|
3492
|
+
return items;
|
|
3493
|
+
}
|
|
3494
|
+
if (ctx.insideBeat) {
|
|
3495
|
+
for (const cue of BEAT_CUE_KEYWORDS) {
|
|
3496
|
+
const aliasTarget = cue === "pulse" ? "focus" : cue === "highlight" ? "glow" : cue;
|
|
3497
|
+
items.push({
|
|
3498
|
+
label: cue,
|
|
3499
|
+
insertText: cue === "show" ? "show $nodes stagger=60ms" : `${cue} `,
|
|
3500
|
+
kind: "cue",
|
|
3501
|
+
detail: `Choreography cue (${aliasTarget})`,
|
|
3502
|
+
documentation: `Execute kinetic choreography action: ${cue}`,
|
|
3503
|
+
boost: 8
|
|
3504
|
+
});
|
|
3505
|
+
}
|
|
3506
|
+
for (const node of ctx.declaredNodes) {
|
|
3507
|
+
items.push({
|
|
3508
|
+
label: node.id,
|
|
3509
|
+
insertText: `${node.id} -> `,
|
|
3510
|
+
kind: "node",
|
|
3511
|
+
detail: `Start flow from ${node.kind} "${node.label}"`,
|
|
3512
|
+
boost: 9
|
|
3513
|
+
});
|
|
3514
|
+
}
|
|
3515
|
+
items.push(
|
|
3516
|
+
{ label: "$nodes", insertText: "$nodes", kind: "selector", detail: "Selector for all nodes", boost: 6 },
|
|
3517
|
+
{ label: "$title", insertText: "$title", kind: "selector", detail: "Diagram title", boost: 5 }
|
|
3518
|
+
);
|
|
3519
|
+
return items;
|
|
3520
|
+
}
|
|
3521
|
+
items.push(
|
|
3522
|
+
{ label: "scene", insertText: "scene theme=paper layout=LR\n", kind: "keyword", detail: "Scene directive", documentation: "Declare scene configuration, theme, and layout direction.", boost: 10 },
|
|
3523
|
+
{ label: "layout LR", insertText: "layout LR\n", kind: "directive", detail: "Horizontal layout", documentation: "Left-to-right topology layout.", boost: 10 },
|
|
3524
|
+
{ label: "layout TB", insertText: "layout TB\n", kind: "directive", detail: "Vertical hierarchy layout", documentation: "Top-to-bottom hierarchy layout.", boost: 9 },
|
|
3525
|
+
{ label: "theme midnight", insertText: "theme=midnight\n", kind: "directive", detail: "Dark theme", documentation: "High-contrast dark mode with neon accents.", boost: 8 },
|
|
3526
|
+
{ label: "theme paper", insertText: "theme=paper\n", kind: "directive", detail: "Editorial light theme", documentation: "Clean light mode with high-legibility typography.", boost: 8 },
|
|
3527
|
+
{ label: "group", insertText: 'group ${1:subsystem} "${2:Subsystem Perimeter}": ${3:Node1} ${4:Node2}\n', kind: "snippet", isSnippet: true, detail: "Group boundary", documentation: "Perimeter enclosing related subsystems.", boost: 8 },
|
|
3528
|
+
{ label: "beat", insertText: 'beat ${1:main} "${2:System Flow}":\n show $nodes stagger=60ms\n ${0}\n', kind: "snippet", isSnippet: true, detail: "Kinetic narrative beat", documentation: "Defines an animated step in the diagram choreography.", boost: 9 },
|
|
3529
|
+
{ label: "pattern", insertText: 'pattern ${1:retry_flow} "${2:Retry Pattern}":\n ${3:Service} -> ${4:Queue} "retry"\n', kind: "snippet", isSnippet: true, detail: "Reusable flow pattern", documentation: "Defines a reusable architectural interaction pattern.", boost: 6 },
|
|
3530
|
+
{ label: "use pattern", insertText: "use pattern=${1:retry_flow}\n", kind: "directive", detail: "Instantiate pattern", documentation: "Reuses a declared flow pattern in the current scope.", boost: 6 },
|
|
3531
|
+
{ label: "style", insertText: "style ${1:NodeId} fill=${2:#38bdf8} stroke=${3:#0284c7}\n", kind: "snippet", isSnippet: true, detail: "Custom node styling", boost: 5 },
|
|
3532
|
+
{ label: "var", insertText: "var ${1:key} = ${2:value}\n", kind: "snippet", isSnippet: true, detail: "Declare variable", boost: 4 }
|
|
3533
|
+
);
|
|
3534
|
+
const nodeKindsList = [
|
|
3535
|
+
{ kind: "service", id: "ApiService", label: "API Service", desc: "Microservice / REST / gRPC backend" },
|
|
3536
|
+
{ kind: "database", id: "Database", label: "PostgreSQL Database", desc: "Relational or NoSQL database store" },
|
|
3537
|
+
{ kind: "cache", id: "RedisCache", label: "In-Memory Cache", desc: "High-speed in-memory cache" },
|
|
3538
|
+
{ kind: "queue", id: "MessageQueue", label: "Kafka Event Stream", desc: "Pub/Sub message broker or event stream" },
|
|
3539
|
+
{ kind: "gateway", id: "ApiGateway", label: "API Gateway", desc: "Ingress proxy and perimeter router" },
|
|
3540
|
+
{ kind: "browser", id: "WebApp", label: "Web Client", desc: "End-user web browser application" },
|
|
3541
|
+
{ kind: "mobile", id: "MobileApp", label: "Mobile Application", desc: "iOS / Android native mobile app" },
|
|
3542
|
+
{ kind: "worker", id: "BackgroundWorker", label: "Async Task Worker", desc: "Background job execution worker" },
|
|
3543
|
+
{ kind: "storage", id: "ObjectStorage", label: "S3 Object Store", desc: "Blob / document cloud storage" },
|
|
3544
|
+
{ kind: "cdn", id: "EdgeCDN", label: "Cloudflare Edge", desc: "Global content delivery network" },
|
|
3545
|
+
{ kind: "firewall", id: "WAF", label: "Security Perimeter", desc: "Web application firewall & DDoS shield" },
|
|
3546
|
+
{ kind: "lambda", id: "ServerlessFunc", label: "Serverless Function", desc: "Event-driven serverless function" },
|
|
3547
|
+
{ kind: "pod", id: "AppPod", label: "Kubernetes Pod", desc: "Containerized workload pod" },
|
|
3548
|
+
{ kind: "user", id: "Customer", label: "End User", desc: "Human user / customer actor" }
|
|
3549
|
+
];
|
|
3550
|
+
for (const n of nodeKindsList) {
|
|
3551
|
+
items.push({
|
|
3552
|
+
label: n.kind,
|
|
3553
|
+
insertText: `${n.kind} \${1:${n.id}} "\${2:${n.label}}"`,
|
|
3554
|
+
kind: "nodeKind",
|
|
3555
|
+
isSnippet: true,
|
|
3556
|
+
detail: `Node kind \u2192 ${n.kind}`,
|
|
3557
|
+
documentation: n.desc,
|
|
3558
|
+
boost: 7
|
|
3559
|
+
});
|
|
3560
|
+
}
|
|
3561
|
+
for (const vp of VISUAL_PRIMITIVE_TYPES) {
|
|
3562
|
+
items.push({
|
|
3563
|
+
label: vp,
|
|
3564
|
+
insertText: `${vp} \${1:${humanizeId(vp).replace(/\s+/g, "")}} "\${2:${humanizeId(vp)}}"`,
|
|
3565
|
+
kind: "nodeKind",
|
|
3566
|
+
isSnippet: true,
|
|
3567
|
+
detail: `Visual primitive \u2192 ${vp}`,
|
|
3568
|
+
boost: 4
|
|
3569
|
+
});
|
|
3570
|
+
}
|
|
3571
|
+
for (const tech of POPULAR_TECHS) {
|
|
3572
|
+
items.push({
|
|
3573
|
+
label: tech.name,
|
|
3574
|
+
insertText: `${tech.kind} ${tech.defaultId} "${tech.label}"
|
|
3575
|
+
`,
|
|
3576
|
+
kind: "tech",
|
|
3577
|
+
detail: `\u26A1 ${tech.kind} \xB7 ${tech.name}`,
|
|
3578
|
+
documentation: `${tech.desc}
|
|
3579
|
+
|
|
3580
|
+
Inserts semantic ${tech.kind} node definition.`,
|
|
3581
|
+
filterText: `${tech.name} ${tech.aliases.join(" ")} ${tech.kind}`,
|
|
3582
|
+
boost: 6
|
|
3583
|
+
});
|
|
3584
|
+
}
|
|
3585
|
+
for (const node of ctx.declaredNodes) {
|
|
3586
|
+
items.push({
|
|
3587
|
+
label: node.id,
|
|
3588
|
+
insertText: node.id,
|
|
3589
|
+
kind: "node",
|
|
3590
|
+
detail: `${node.kind} \xB7 "${node.label}"`,
|
|
3591
|
+
boost: 5
|
|
3592
|
+
});
|
|
3593
|
+
}
|
|
3594
|
+
return items;
|
|
3595
|
+
}
|
|
3596
|
+
function getCommonEdgeLabels(sourceKind, op) {
|
|
3597
|
+
if (op === "<-") {
|
|
3598
|
+
return ["200 OK", "201 Created", "Cached Response", "Result Payload", "JWT Token"];
|
|
3599
|
+
}
|
|
3600
|
+
if (op === "~>") {
|
|
3601
|
+
return ["order.created", "user.signup", "event.published", "telemetry.metric", "task.enqueue"];
|
|
3602
|
+
}
|
|
3603
|
+
if (op === "<->") {
|
|
3604
|
+
return ["WebSocket / Streaming", "mTLS Bidirectional", "gRPC Bidirectional Stream"];
|
|
3605
|
+
}
|
|
3606
|
+
if (op === "..>") {
|
|
3607
|
+
return ["depends on", "reads schema", "replicates to", "monitors"];
|
|
3608
|
+
}
|
|
3609
|
+
if (isClientRole(sourceKind)) {
|
|
3610
|
+
return ["POST /api/v1/checkout", "GET /api/v1/user", "POST /auth/login", "GraphQL Query"];
|
|
3611
|
+
}
|
|
3612
|
+
if (isGatewayRole(sourceKind)) {
|
|
3613
|
+
return ["Route /v1/orders", "Authorize & Forward", "Proxy Request"];
|
|
3614
|
+
}
|
|
3615
|
+
if (isComputeRole(sourceKind)) {
|
|
3616
|
+
return ["SELECT * FROM orders", "SET cache:key", "GET cache:key", "Publish Event", "Verify Token"];
|
|
3617
|
+
}
|
|
3618
|
+
if (isQueueRole(sourceKind)) {
|
|
3619
|
+
return ["Consume Message", "Process Batch", "Deliver Payload"];
|
|
3620
|
+
}
|
|
3621
|
+
return ["call", "request", "query", "sync"];
|
|
3622
|
+
}
|
|
3623
|
+
function predictNextLineSuggestion(docText, cursorLine) {
|
|
3624
|
+
const ctx = extractDiagramContext(docText, cursorLine, 0);
|
|
3625
|
+
const totalNodes = ctx.declaredNodes.length;
|
|
3626
|
+
if (totalNodes === 0) {
|
|
3627
|
+
return {
|
|
3628
|
+
text: 'browser WebApp "Web Application"',
|
|
3629
|
+
insertText: 'browser WebApp "Web Application"\nservice ApiGw "API Gateway Service"\n\nbeat main "System Entrance":\n show $nodes stagger=60ms\n WebApp -> ApiGw "POST /api"\n',
|
|
3630
|
+
description: "Start with an entry client and API Gateway with entrance choreography",
|
|
3631
|
+
type: "next-node"
|
|
3632
|
+
};
|
|
3633
|
+
}
|
|
3634
|
+
if (totalNodes >= 2 && ctx.declaredBeats.length === 0 && !ctx.insideBeat) {
|
|
3635
|
+
const n1 = ctx.declaredNodes[0];
|
|
3636
|
+
const n2 = ctx.declaredNodes[1];
|
|
3637
|
+
return {
|
|
3638
|
+
text: `beat main "System Flow":
|
|
3639
|
+
show $nodes stagger=60ms
|
|
3640
|
+
${n1.id} -> ${n2.id} "call"`,
|
|
3641
|
+
insertText: `beat main "System Flow":
|
|
3642
|
+
show $nodes stagger=60ms
|
|
3643
|
+
${n1.id} -> ${n2.id} "call"
|
|
3644
|
+
`,
|
|
3645
|
+
description: "Initialize choreography with entrance animation and primary flow",
|
|
3646
|
+
type: "init-beat"
|
|
3647
|
+
};
|
|
3648
|
+
}
|
|
3649
|
+
if (ctx.insideBeat) {
|
|
3650
|
+
const lines = docText.split(/\r?\n/);
|
|
3651
|
+
const prevLine = lines[cursorLine - 1]?.trim() ?? "";
|
|
3652
|
+
const reqMatch = /^([\w.-]+)\s*->\s*([\w.-]+)(?:\s+"([^"]*)")?/i.exec(prevLine);
|
|
3653
|
+
if (reqMatch) {
|
|
3654
|
+
const src = reqMatch[1];
|
|
3655
|
+
const tgt = reqMatch[2];
|
|
3656
|
+
return {
|
|
3657
|
+
text: ` ${src} <- ${tgt} "200 OK"`,
|
|
3658
|
+
insertText: ` ${src} <- ${tgt} "200 OK"
|
|
3659
|
+
`,
|
|
3660
|
+
description: `Add response flow returning from ${tgt} to ${src}`,
|
|
3661
|
+
type: "next-flow"
|
|
3662
|
+
};
|
|
3663
|
+
}
|
|
3664
|
+
const eventMatch = /^([\w.-]+)\s*~>\s*([\w.-]+)/i.exec(prevLine);
|
|
3665
|
+
if (eventMatch) {
|
|
3666
|
+
const queueId = eventMatch[2];
|
|
3667
|
+
const worker = ctx.declaredNodes.find((n) => nodeRole(n.kind) === "compute" && n.id !== eventMatch[1]);
|
|
3668
|
+
if (worker) {
|
|
3669
|
+
return {
|
|
3670
|
+
text: ` ${queueId} ~> ${worker.id} "process"`,
|
|
3671
|
+
insertText: ` ${queueId} ~> ${worker.id} "process"
|
|
3672
|
+
`,
|
|
3673
|
+
description: `Deliver queued event from ${queueId} to worker ${worker.id}`,
|
|
3674
|
+
type: "next-flow"
|
|
3675
|
+
};
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
const keyNode = ctx.declaredNodes.find((n) => nodeRole(n.kind) === "compute" || nodeRole(n.kind) === "database") || ctx.declaredNodes[0];
|
|
3679
|
+
if (keyNode) {
|
|
3680
|
+
return {
|
|
3681
|
+
text: ` pulse ${keyNode.id} color=#38bdf8`,
|
|
3682
|
+
insertText: ` pulse ${keyNode.id} color=#38bdf8
|
|
3683
|
+
`,
|
|
3684
|
+
description: `Add kinetic highlight pulse to ${keyNode.id}`,
|
|
3685
|
+
type: "beat-cue"
|
|
3686
|
+
};
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
const hasClient = ctx.declaredNodes.some((n) => isClientRole(n.kind));
|
|
3690
|
+
const hasCompute = ctx.declaredNodes.some((n) => isComputeRole(n.kind) || isGatewayRole(n.kind));
|
|
3691
|
+
const hasDB = ctx.declaredNodes.some((n) => isDatabaseKind(n.kind));
|
|
3692
|
+
const hasCache = ctx.declaredNodes.some((n) => isCacheKind(n.kind));
|
|
3693
|
+
const hasQueue = ctx.declaredNodes.some((n) => isQueueRole(n.kind));
|
|
3694
|
+
if (hasCompute && !hasDB) {
|
|
3695
|
+
return {
|
|
3696
|
+
text: 'database Postgres "PostgreSQL 16"',
|
|
3697
|
+
insertText: 'database Postgres "PostgreSQL 16"\n',
|
|
3698
|
+
description: "Add a database tier to persist state for your services",
|
|
3699
|
+
type: "next-node"
|
|
3700
|
+
};
|
|
3701
|
+
}
|
|
3702
|
+
if (hasCompute && hasDB && !hasCache) {
|
|
3703
|
+
return {
|
|
3704
|
+
text: 'cache Redis "Redis Cluster"',
|
|
3705
|
+
insertText: 'cache Redis "Redis Cluster"\n',
|
|
3706
|
+
description: "Add an in-memory caching tier to accelerate query latency",
|
|
3707
|
+
type: "next-node"
|
|
3708
|
+
};
|
|
3709
|
+
}
|
|
3710
|
+
if (hasCompute && hasDB && !hasQueue) {
|
|
3711
|
+
return {
|
|
3712
|
+
text: 'queue Kafka "Kafka Event Stream"',
|
|
3713
|
+
insertText: 'queue Kafka "Kafka Event Stream"\nworker AsyncWorker "Background Worker"\n',
|
|
3714
|
+
description: "Add an asynchronous event queue and worker for decoupled processing",
|
|
3715
|
+
type: "next-node"
|
|
3716
|
+
};
|
|
3717
|
+
}
|
|
3718
|
+
return null;
|
|
3719
|
+
}
|
|
3720
|
+
function getArchitectureSuggestions(docText) {
|
|
3721
|
+
const ctx = extractDiagramContext(docText, 0, 0);
|
|
3722
|
+
const recommendations = [];
|
|
3723
|
+
const nodeCount = ctx.declaredNodes.length;
|
|
3724
|
+
if (nodeCount === 0) return recommendations;
|
|
3725
|
+
const hasClient = ctx.declaredNodes.some((n) => isClientRole(n.kind));
|
|
3726
|
+
const hasGateway = ctx.declaredNodes.some((n) => isGatewayRole(n.kind));
|
|
3727
|
+
const hasServices = ctx.declaredNodes.filter((n) => isComputeRole(n.kind));
|
|
3728
|
+
const hasDatabases = ctx.declaredNodes.filter((n) => isDatabaseKind(n.kind));
|
|
3729
|
+
const hasCache = ctx.declaredNodes.some((n) => isCacheKind(n.kind));
|
|
3730
|
+
const hasQueue = ctx.declaredNodes.some((n) => isQueueRole(n.kind));
|
|
3731
|
+
const hasGroups = ctx.declaredGroups.length > 0;
|
|
3732
|
+
const hasBeats = ctx.declaredBeats.length > 0;
|
|
3733
|
+
if (hasClient && (hasServices.length > 0 || hasDatabases.length > 0) && !hasGateway) {
|
|
3734
|
+
recommendations.push({
|
|
3735
|
+
id: "add-gateway",
|
|
3736
|
+
title: "Add API Gateway Perimeter",
|
|
3737
|
+
desc: "Direct client-to-service connections bypass centralized rate-limiting and auth. Insert an API Gateway.",
|
|
3738
|
+
snippet: 'gateway ApiGateway "Kong / Envoy Gateway"\n\nbeat route:\n WebApp -> ApiGateway "HTTPS"\n',
|
|
3739
|
+
category: "security",
|
|
3740
|
+
actionLabel: "Insert Gateway"
|
|
3741
|
+
});
|
|
3742
|
+
}
|
|
3743
|
+
if (hasDatabases.length > 0 && !hasCache) {
|
|
3744
|
+
recommendations.push({
|
|
3745
|
+
id: "add-cache",
|
|
3746
|
+
title: "Add In-Memory Cache (Redis)",
|
|
3747
|
+
desc: "Offload frequent read queries from your database with a sub-millisecond Redis cluster.",
|
|
3748
|
+
snippet: 'cache Redis "Redis Cluster"\n\nbeat cache_layer:\n ApiService -> Redis "cache.get"\n',
|
|
3749
|
+
category: "performance",
|
|
3750
|
+
actionLabel: "Insert Redis Cache"
|
|
3751
|
+
});
|
|
3752
|
+
}
|
|
3753
|
+
if (hasServices.length >= 2 && !hasGroups) {
|
|
3754
|
+
const memberIds = hasServices.map((s) => s.id).join(" ");
|
|
3755
|
+
recommendations.push({
|
|
3756
|
+
id: "add-group-boundary",
|
|
3757
|
+
title: "Enclose Microservices in Group Boundary",
|
|
3758
|
+
desc: "Group related backend microservices inside a secure VPC boundary perimeter.",
|
|
3759
|
+
snippet: `group backend "Backend Microservices VPC": ${memberIds}
|
|
3760
|
+
`,
|
|
3761
|
+
category: "structure",
|
|
3762
|
+
actionLabel: "Add Group Boundary"
|
|
3763
|
+
});
|
|
3764
|
+
}
|
|
3765
|
+
if (!hasBeats && nodeCount >= 2) {
|
|
3766
|
+
const firstTwo = ctx.declaredNodes.slice(0, 2);
|
|
3767
|
+
recommendations.push({
|
|
3768
|
+
id: "add-entrance-beat",
|
|
3769
|
+
title: "Add Animated Entrance Choreography",
|
|
3770
|
+
desc: "Bring your architecture to life with staggered node reveals and motion flows.",
|
|
3771
|
+
snippet: `beat reveal "System Entrance":
|
|
3772
|
+
show $nodes stagger=60ms
|
|
3773
|
+
${firstTwo[0].id} -> ${firstTwo[1].id} "GET /api"
|
|
3774
|
+
`,
|
|
3775
|
+
category: "choreography",
|
|
3776
|
+
actionLabel: "Add Narrative Beat"
|
|
3777
|
+
});
|
|
3778
|
+
}
|
|
3779
|
+
if (hasServices.length >= 2 && !hasQueue) {
|
|
3780
|
+
recommendations.push({
|
|
3781
|
+
id: "add-event-queue",
|
|
3782
|
+
title: "Decouple Services with Event Queue",
|
|
3783
|
+
desc: "Use an asynchronous event stream (Kafka/SQS) for resilient pub/sub event handling.",
|
|
3784
|
+
snippet: 'queue Kafka "Kafka Event Bus"\nworker Worker "Event Consumer"\n\nbeat async_event:\n ApiService ~> Kafka "event.published"\n Kafka ~> Worker "process"\n',
|
|
3785
|
+
category: "reliability",
|
|
3786
|
+
actionLabel: "Add Kafka Queue"
|
|
3787
|
+
});
|
|
3788
|
+
}
|
|
3789
|
+
return recommendations;
|
|
3790
|
+
}
|
|
2127
3791
|
|
|
2128
3792
|
// ../compat/src/mermaid/mermaid-transpiler.ts
|
|
2129
3793
|
function sanitizeId(id) {
|
|
@@ -3075,7 +4739,10 @@ function handleValidateMarkdy(code, checkArchitecture = true) {
|
|
|
3075
4739
|
content: [{ type: "text", text: lines.join("\n") }]
|
|
3076
4740
|
};
|
|
3077
4741
|
} catch (error) {
|
|
3078
|
-
const
|
|
4742
|
+
const report = diagnoseMarkdyCode(code, {
|
|
4743
|
+
checkArchitecture,
|
|
4744
|
+
transpileMermaid: (src) => transpileMermaidToMarkdy(src, "Imported Scene").code
|
|
4745
|
+
});
|
|
3079
4746
|
return {
|
|
3080
4747
|
isError: true,
|
|
3081
4748
|
content: [
|
|
@@ -3084,12 +4751,87 @@ function handleValidateMarkdy(code, checkArchitecture = true) {
|
|
|
3084
4751
|
text: `\u274C Parse Error: ${error.message}
|
|
3085
4752
|
|
|
3086
4753
|
Suggested AI Healing Prompt:
|
|
3087
|
-
${
|
|
4754
|
+
${report.repairPrompt}`
|
|
3088
4755
|
}
|
|
3089
4756
|
]
|
|
3090
4757
|
};
|
|
3091
4758
|
}
|
|
3092
4759
|
}
|
|
4760
|
+
function handleDiagnoseMarkdy(code, checkArchitecture = true) {
|
|
4761
|
+
const report = diagnoseMarkdyCode(code, {
|
|
4762
|
+
checkArchitecture,
|
|
4763
|
+
transpileMermaid: (src) => transpileMermaidToMarkdy(src, "Imported Scene").code
|
|
4764
|
+
});
|
|
4765
|
+
const lines = [];
|
|
4766
|
+
lines.push(report.summary);
|
|
4767
|
+
if (report.issues.length > 0) {
|
|
4768
|
+
const errors = report.issues.filter((i) => i.severity === "error");
|
|
4769
|
+
const warnings = report.issues.filter((i) => i.severity === "warning");
|
|
4770
|
+
if (errors.length > 0) {
|
|
4771
|
+
lines.push("\n### \u274C Syntax Errors & Typos:");
|
|
4772
|
+
for (const e of errors) {
|
|
4773
|
+
lines.push(`- **[${e.code}]** Line ${e.line}: ${e.message}`);
|
|
4774
|
+
if (e.snippet) lines.push(` - *Problem:* \`${e.snippet}\``);
|
|
4775
|
+
if (e.suggestion) lines.push(` - *Recommendation:* ${e.suggestion}`);
|
|
4776
|
+
if (e.didYouMean) lines.push(` - *Did you mean:* \`${e.didYouMean}\`?`);
|
|
4777
|
+
}
|
|
4778
|
+
}
|
|
4779
|
+
if (warnings.length > 0) {
|
|
4780
|
+
lines.push("\n### \u26A0\uFE0F Warnings & Governance:");
|
|
4781
|
+
for (const w of warnings) {
|
|
4782
|
+
lines.push(`- **[${w.code}]** Line ${w.line}: ${w.message}`);
|
|
4783
|
+
if (w.suggestion) lines.push(` - *Recommendation:* ${w.suggestion}`);
|
|
4784
|
+
}
|
|
4785
|
+
}
|
|
4786
|
+
}
|
|
4787
|
+
if (report.repairedCode) {
|
|
4788
|
+
lines.push("\n### \u{1F6E0}\uFE0F Proposed Auto-Repaired Code:");
|
|
4789
|
+
lines.push("```markdy");
|
|
4790
|
+
lines.push(report.repairedCode);
|
|
4791
|
+
lines.push("```");
|
|
4792
|
+
}
|
|
4793
|
+
lines.push("\n### \u{1F4A1} AI Self-Healing Prompt for LLMs / Agents:");
|
|
4794
|
+
lines.push(report.repairPrompt);
|
|
4795
|
+
return {
|
|
4796
|
+
isError: !report.isValid,
|
|
4797
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
4798
|
+
};
|
|
4799
|
+
}
|
|
4800
|
+
function handleFixMarkdy(code) {
|
|
4801
|
+
const result = repairMarkdyCode(code, {
|
|
4802
|
+
transpileMermaid: (src) => transpileMermaidToMarkdy(src, "Imported Scene").code
|
|
4803
|
+
});
|
|
4804
|
+
const lines = [];
|
|
4805
|
+
if (result.isFixed) {
|
|
4806
|
+
lines.push(`\u2705 Markdy Code Repaired Successfully (${result.changes.length} change(s) applied):
|
|
4807
|
+
`);
|
|
4808
|
+
for (const c of result.changes) {
|
|
4809
|
+
lines.push(`- ${c}`);
|
|
4810
|
+
}
|
|
4811
|
+
lines.push("\n```markdy");
|
|
4812
|
+
lines.push(result.repairedCode);
|
|
4813
|
+
lines.push("```");
|
|
4814
|
+
} else {
|
|
4815
|
+
lines.push(`\u26A0\uFE0F Applied ${result.changes.length} automated repair(s), but additional manual corrections may be needed:
|
|
4816
|
+
`);
|
|
4817
|
+
for (const c of result.changes) {
|
|
4818
|
+
lines.push(`- ${c}`);
|
|
4819
|
+
}
|
|
4820
|
+
lines.push("\n```markdy");
|
|
4821
|
+
lines.push(result.repairedCode);
|
|
4822
|
+
lines.push("```");
|
|
4823
|
+
const diag = diagnoseMarkdyCode(result.repairedCode);
|
|
4824
|
+
if (!diag.isValid) {
|
|
4825
|
+
lines.push("\nRemaining issues to resolve:");
|
|
4826
|
+
for (const iss of diag.issues) {
|
|
4827
|
+
lines.push(`- Line ${iss.line}: ${iss.message}`);
|
|
4828
|
+
}
|
|
4829
|
+
}
|
|
4830
|
+
}
|
|
4831
|
+
return {
|
|
4832
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
4833
|
+
};
|
|
4834
|
+
}
|
|
3093
4835
|
async function handleTranspileToMarkdy(source, format, title = "Imported Scene") {
|
|
3094
4836
|
try {
|
|
3095
4837
|
let markdyCode = "";
|
|
@@ -3211,6 +4953,45 @@ function handleGetArchitectureCatalog(filterCategory) {
|
|
|
3211
4953
|
content: [{ type: "text", text }]
|
|
3212
4954
|
};
|
|
3213
4955
|
}
|
|
4956
|
+
function handleIntelliCode(code, line, column) {
|
|
4957
|
+
const lines = code.split(/\r?\n/);
|
|
4958
|
+
const targetLine = line !== void 0 ? line : Math.max(0, lines.length - 1);
|
|
4959
|
+
const targetCol = column !== void 0 ? column : lines[targetLine]?.length ?? 0;
|
|
4960
|
+
const completions = getIntelliCodeCompletions(code, targetLine, targetCol);
|
|
4961
|
+
const nextLinePrediction = predictNextLineSuggestion(code, targetLine);
|
|
4962
|
+
const archRecommendations = getArchitectureSuggestions(code);
|
|
4963
|
+
const output = {
|
|
4964
|
+
cursor: { line: targetLine, column: targetCol },
|
|
4965
|
+
completionsCount: completions.length,
|
|
4966
|
+
topCompletions: completions.slice(0, 20).map((c) => ({
|
|
4967
|
+
label: c.label,
|
|
4968
|
+
insertText: c.insertText,
|
|
4969
|
+
kind: c.kind,
|
|
4970
|
+
detail: c.detail,
|
|
4971
|
+
documentation: c.documentation
|
|
4972
|
+
})),
|
|
4973
|
+
nextLinePrediction: nextLinePrediction ? {
|
|
4974
|
+
suggestedText: nextLinePrediction.text,
|
|
4975
|
+
description: nextLinePrediction.description,
|
|
4976
|
+
type: nextLinePrediction.type
|
|
4977
|
+
} : null,
|
|
4978
|
+
architectureRecommendations: archRecommendations.map((r) => ({
|
|
4979
|
+
id: r.id,
|
|
4980
|
+
title: r.title,
|
|
4981
|
+
description: r.desc,
|
|
4982
|
+
category: r.category,
|
|
4983
|
+
actionSnippet: r.snippet
|
|
4984
|
+
}))
|
|
4985
|
+
};
|
|
4986
|
+
return {
|
|
4987
|
+
content: [
|
|
4988
|
+
{
|
|
4989
|
+
type: "text",
|
|
4990
|
+
text: JSON.stringify(output, null, 2)
|
|
4991
|
+
}
|
|
4992
|
+
]
|
|
4993
|
+
};
|
|
4994
|
+
}
|
|
3214
4995
|
function handleReadResource(uri) {
|
|
3215
4996
|
switch (uri) {
|
|
3216
4997
|
case "markdy://spec/agent-reference":
|
|
@@ -3248,6 +5029,30 @@ Cycle Safety: Always use <- for responses back to callers to prevent layout rank
|
|
|
3248
5029
|
}
|
|
3249
5030
|
]
|
|
3250
5031
|
};
|
|
5032
|
+
case "markdy://spec/grammar-rules":
|
|
5033
|
+
return {
|
|
5034
|
+
contents: [
|
|
5035
|
+
{
|
|
5036
|
+
uri,
|
|
5037
|
+
mimeType: "text/markdown",
|
|
5038
|
+
text: `# MarkdyScript Grammar & Diagnostic Rules
|
|
5039
|
+
|
|
5040
|
+
### 1. The 4-Step Mental Model
|
|
5041
|
+
1. **Directives**: \`scene theme=paper width=1280 height=720\` and \`layout LR\` (or TB).
|
|
5042
|
+
2. **Nodes**: \`<kind> <Id> ["Human Label"]\` (e.g. \`browser Client "Shopper"\`, \`gateway GW "API Gateway"\`, \`service Orders\`, \`database DB\`).
|
|
5043
|
+
3. **Groups**: \`group <id> "<Label>": Node1 Node2\`
|
|
5044
|
+
4. **Beats**: \`beat <id> "<Caption>":\` containing indented flow lines and cues.
|
|
5045
|
+
|
|
5046
|
+
### 2. Common Syntax Errors & Corrections
|
|
5047
|
+
- **Keyword typos**: \`scen\` -> \`scene\`, \`layput\` -> \`layout\`, \`groop\` -> \`group\`, \`bea\` -> \`beat\`.
|
|
5048
|
+
- **Node kind typos**: \`servce\` -> \`service\`, \`databse\` -> \`database\`, \`gateawy\` -> \`gateway\`, \`cach\` -> \`cache\`.
|
|
5049
|
+
- **Missing colons**: Always terminate \`beat <id> ["Caption"]:\` and \`group <id> ["Label"]:\` with a colon.
|
|
5050
|
+
- **Unquoted strings**: Labels containing spaces MUST be wrapped in double quotes (\`"...\`\`).
|
|
5051
|
+
- **Cycle safety**: NEVER use \`->\` for response or return paths. Use \`<-\` for return calls to prevent cycle overlap rank collapse.
|
|
5052
|
+
- **Cue placement**: Visual cues (\`show $nodes\`, \`glow\`, \`frame\`, \`focus\`) and flows must reside INSIDE an indented beat block.`
|
|
5053
|
+
}
|
|
5054
|
+
]
|
|
5055
|
+
};
|
|
3251
5056
|
default:
|
|
3252
5057
|
throw new Error(`Resource not found: ${uri}`);
|
|
3253
5058
|
}
|
|
@@ -3290,6 +5095,38 @@ function createMarkdyMcpServer() {
|
|
|
3290
5095
|
required: ["code"]
|
|
3291
5096
|
}
|
|
3292
5097
|
},
|
|
5098
|
+
{
|
|
5099
|
+
name: "diagnose_markdy_syntax",
|
|
5100
|
+
description: "Deep diagnostic tool that analyzes MarkdyScript diagram syntax, detects typos in keywords/node kinds/operators/nodes, identifies flow cycle overlaps, unquoted strings, and provides 'Did you mean?' suggestions with auto-repaired code.",
|
|
5101
|
+
inputSchema: {
|
|
5102
|
+
type: "object",
|
|
5103
|
+
properties: {
|
|
5104
|
+
code: {
|
|
5105
|
+
type: "string",
|
|
5106
|
+
description: "The MarkdyScript (.markdy) diagram code to inspect."
|
|
5107
|
+
},
|
|
5108
|
+
checkArchitecture: {
|
|
5109
|
+
type: "boolean",
|
|
5110
|
+
description: "Whether to include Well-Architected governance rules in the report. Default: true."
|
|
5111
|
+
}
|
|
5112
|
+
},
|
|
5113
|
+
required: ["code"]
|
|
5114
|
+
}
|
|
5115
|
+
},
|
|
5116
|
+
{
|
|
5117
|
+
name: "fix_markdy_code",
|
|
5118
|
+
description: "Automatically repairs common MarkdyScript syntax errors, typos, missing colons, invalid flow operators, unquoted strings, cycle return edges, and wraps top-level bare cues into a valid scene.",
|
|
5119
|
+
inputSchema: {
|
|
5120
|
+
type: "object",
|
|
5121
|
+
properties: {
|
|
5122
|
+
code: {
|
|
5123
|
+
type: "string",
|
|
5124
|
+
description: "The broken or draft MarkdyScript diagram code to automatically repair."
|
|
5125
|
+
}
|
|
5126
|
+
},
|
|
5127
|
+
required: ["code"]
|
|
5128
|
+
}
|
|
5129
|
+
},
|
|
3293
5130
|
{
|
|
3294
5131
|
name: "transpile_to_markdy",
|
|
3295
5132
|
description: "Converts external infrastructure or diagram code (Mermaid, Docker Compose, Kubernetes manifests, Terraform state, or Draw.io XML) into animated MarkdyScript scenes.",
|
|
@@ -3353,6 +5190,28 @@ function createMarkdyMcpServer() {
|
|
|
3353
5190
|
}
|
|
3354
5191
|
}
|
|
3355
5192
|
}
|
|
5193
|
+
},
|
|
5194
|
+
{
|
|
5195
|
+
name: "get_markdy_intellicode",
|
|
5196
|
+
description: "Provides zero-dependency intelligent code completion, predictive ghost-text next-line suggestions, and Well-Architected proactive recommendations for MarkdyScript diagrams.",
|
|
5197
|
+
inputSchema: {
|
|
5198
|
+
type: "object",
|
|
5199
|
+
properties: {
|
|
5200
|
+
code: {
|
|
5201
|
+
type: "string",
|
|
5202
|
+
description: "The MarkdyScript diagram code."
|
|
5203
|
+
},
|
|
5204
|
+
line: {
|
|
5205
|
+
type: "number",
|
|
5206
|
+
description: "Optional 0-indexed line number where the cursor is positioned."
|
|
5207
|
+
},
|
|
5208
|
+
column: {
|
|
5209
|
+
type: "number",
|
|
5210
|
+
description: "Optional 0-indexed column character where the cursor is positioned."
|
|
5211
|
+
}
|
|
5212
|
+
},
|
|
5213
|
+
required: ["code"]
|
|
5214
|
+
}
|
|
3356
5215
|
}
|
|
3357
5216
|
]
|
|
3358
5217
|
};
|
|
@@ -3366,6 +5225,13 @@ function createMarkdyMcpServer() {
|
|
|
3366
5225
|
String(safeArgs.code ?? ""),
|
|
3367
5226
|
safeArgs.checkArchitecture !== false
|
|
3368
5227
|
);
|
|
5228
|
+
case "diagnose_markdy_syntax":
|
|
5229
|
+
return handleDiagnoseMarkdy(
|
|
5230
|
+
String(safeArgs.code ?? ""),
|
|
5231
|
+
safeArgs.checkArchitecture !== false
|
|
5232
|
+
);
|
|
5233
|
+
case "fix_markdy_code":
|
|
5234
|
+
return handleFixMarkdy(String(safeArgs.code ?? ""));
|
|
3369
5235
|
case "transpile_to_markdy":
|
|
3370
5236
|
return await handleTranspileToMarkdy(
|
|
3371
5237
|
String(safeArgs.source ?? ""),
|
|
@@ -3378,6 +5244,12 @@ function createMarkdyMcpServer() {
|
|
|
3378
5244
|
return handleGenerateMarkdyPrompt(String(safeArgs.userGoal ?? ""));
|
|
3379
5245
|
case "get_architecture_catalog":
|
|
3380
5246
|
return handleGetArchitectureCatalog(safeArgs.filterCategory ? String(safeArgs.filterCategory) : void 0);
|
|
5247
|
+
case "get_markdy_intellicode":
|
|
5248
|
+
return handleIntelliCode(
|
|
5249
|
+
String(safeArgs.code ?? ""),
|
|
5250
|
+
typeof safeArgs.line === "number" ? safeArgs.line : void 0,
|
|
5251
|
+
typeof safeArgs.column === "number" ? safeArgs.column : void 0
|
|
5252
|
+
);
|
|
3381
5253
|
default:
|
|
3382
5254
|
throw new Error(`Unknown tool: ${name}`);
|
|
3383
5255
|
}
|
|
@@ -3391,6 +5263,12 @@ function createMarkdyMcpServer() {
|
|
|
3391
5263
|
description: "Canonical reference for MarkdyScript syntax, closed node vocabularies, and cycle-safety rules.",
|
|
3392
5264
|
mimeType: "text/markdown"
|
|
3393
5265
|
},
|
|
5266
|
+
{
|
|
5267
|
+
uri: "markdy://spec/grammar-rules",
|
|
5268
|
+
name: "MarkdyScript Grammar & Diagnostic Rules",
|
|
5269
|
+
description: "Grammar conventions, typo resolution guides, and cycle-safe routing rules.",
|
|
5270
|
+
mimeType: "text/markdown"
|
|
5271
|
+
},
|
|
3394
5272
|
{
|
|
3395
5273
|
uri: "markdy://templates/catalog",
|
|
3396
5274
|
name: "Markdy Architecture Templates Catalog",
|
|
@@ -3434,6 +5312,17 @@ function createMarkdyMcpServer() {
|
|
|
3434
5312
|
}
|
|
3435
5313
|
]
|
|
3436
5314
|
},
|
|
5315
|
+
{
|
|
5316
|
+
name: "debug_markdy_syntax",
|
|
5317
|
+
description: "Diagnose, troubleshoot, and heal broken MarkdyScript syntax, typos, undefined node references, or flow cycles.",
|
|
5318
|
+
arguments: [
|
|
5319
|
+
{
|
|
5320
|
+
name: "code",
|
|
5321
|
+
description: "The broken or erroneous MarkdyScript code to diagnose and repair.",
|
|
5322
|
+
required: true
|
|
5323
|
+
}
|
|
5324
|
+
]
|
|
5325
|
+
},
|
|
3437
5326
|
{
|
|
3438
5327
|
name: "audit_architecture",
|
|
3439
5328
|
description: "Review a MarkdyScript diagram for Well-Architected governance, cycle overlap, and layer violations.",
|
|
@@ -3494,6 +5383,24 @@ Follow the canonical 4-step Markdy mental model:
|
|
|
3494
5383
|
]
|
|
3495
5384
|
};
|
|
3496
5385
|
}
|
|
5386
|
+
case "debug_markdy_syntax": {
|
|
5387
|
+
return {
|
|
5388
|
+
description: "Diagnose and heal MarkdyScript syntax errors and typos",
|
|
5389
|
+
messages: [
|
|
5390
|
+
{
|
|
5391
|
+
role: "user",
|
|
5392
|
+
content: {
|
|
5393
|
+
type: "text",
|
|
5394
|
+
text: `Please diagnose and repair the following MarkdyScript code. Check for keyword typos, invalid node kinds, unquoted strings, missing colons, undefined node references, and flow cycles:
|
|
5395
|
+
|
|
5396
|
+
\`\`\`markdy
|
|
5397
|
+
${safeArgs.code}
|
|
5398
|
+
\`\`\``
|
|
5399
|
+
}
|
|
5400
|
+
}
|
|
5401
|
+
]
|
|
5402
|
+
};
|
|
5403
|
+
}
|
|
3497
5404
|
case "audit_architecture": {
|
|
3498
5405
|
return {
|
|
3499
5406
|
description: "Audit Markdy diagram against Well-Architected rules",
|
|
@@ -3551,9 +5458,12 @@ if (isDirectExecution) {
|
|
|
3551
5458
|
export {
|
|
3552
5459
|
MCP_SERVER_VERSION,
|
|
3553
5460
|
createMarkdyMcpServer,
|
|
5461
|
+
handleDiagnoseMarkdy,
|
|
3554
5462
|
handleExplainArchitecture,
|
|
5463
|
+
handleFixMarkdy,
|
|
3555
5464
|
handleGenerateMarkdyPrompt,
|
|
3556
5465
|
handleGetArchitectureCatalog,
|
|
5466
|
+
handleIntelliCode,
|
|
3557
5467
|
handleReadResource,
|
|
3558
5468
|
handleTranspileToMarkdy,
|
|
3559
5469
|
handleValidateMarkdy,
|