@markdy/core 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/dist/index.d.ts +135 -2
- package/dist/index.js +1465 -40
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -611,7 +611,18 @@ function unquote(raw) {
|
|
|
611
611
|
}
|
|
612
612
|
function resolvePlayer(config = {}, overrides = {}) {
|
|
613
613
|
const playback = config.playback ?? {};
|
|
614
|
-
const
|
|
614
|
+
const overrideControls = typeof overrides.controls === "object" && overrides.controls !== null ? overrides.controls : void 0;
|
|
615
|
+
const configuredControls = {
|
|
616
|
+
...config.controls ?? {},
|
|
617
|
+
...overrideControls ? {
|
|
618
|
+
...overrideControls,
|
|
619
|
+
...overrideControls.playback === true ? {
|
|
620
|
+
play: overrideControls.play ?? true,
|
|
621
|
+
restart: overrideControls.restart ?? true,
|
|
622
|
+
seek: overrideControls.seek ?? true
|
|
623
|
+
} : {}
|
|
624
|
+
} : {}
|
|
625
|
+
};
|
|
615
626
|
const interaction = config.interaction ?? {};
|
|
616
627
|
const chrome = config.chrome ?? {};
|
|
617
628
|
const controlsAllowed = overrides.controls !== false;
|
|
@@ -4294,54 +4305,849 @@ function routeOrthogonalEdge(sourceBox, targetBox) {
|
|
|
4294
4305
|
};
|
|
4295
4306
|
}
|
|
4296
4307
|
|
|
4297
|
-
// src/
|
|
4298
|
-
function
|
|
4299
|
-
const
|
|
4300
|
-
|
|
4308
|
+
// src/syntax-diagnostics.ts
|
|
4309
|
+
function damerauLevenshteinDistance(a, b) {
|
|
4310
|
+
const al = a.length;
|
|
4311
|
+
const bl = b.length;
|
|
4312
|
+
if (al === 0) return bl;
|
|
4313
|
+
if (bl === 0) return al;
|
|
4314
|
+
const matrix = [];
|
|
4315
|
+
for (let i = 0; i <= al; i++) {
|
|
4316
|
+
matrix[i] = [i];
|
|
4317
|
+
}
|
|
4318
|
+
for (let j = 0; j <= bl; j++) {
|
|
4319
|
+
matrix[0][j] = j;
|
|
4320
|
+
}
|
|
4321
|
+
for (let i = 1; i <= al; i++) {
|
|
4322
|
+
for (let j = 1; j <= bl; j++) {
|
|
4323
|
+
const cost = a[i - 1].toLowerCase() === b[j - 1].toLowerCase() ? 0 : 1;
|
|
4324
|
+
let min = Math.min(
|
|
4325
|
+
matrix[i - 1][j] + 1,
|
|
4326
|
+
// deletion
|
|
4327
|
+
matrix[i][j - 1] + 1,
|
|
4328
|
+
// insertion
|
|
4329
|
+
matrix[i - 1][j - 1] + cost
|
|
4330
|
+
// substitution
|
|
4331
|
+
);
|
|
4332
|
+
if (i > 1 && j > 1 && a[i - 1].toLowerCase() === b[j - 2].toLowerCase() && a[i - 2].toLowerCase() === b[j - 1].toLowerCase()) {
|
|
4333
|
+
min = Math.min(min, matrix[i - 2][j - 2] + cost);
|
|
4334
|
+
}
|
|
4335
|
+
matrix[i][j] = min;
|
|
4336
|
+
}
|
|
4337
|
+
}
|
|
4338
|
+
return matrix[al][bl];
|
|
4339
|
+
}
|
|
4340
|
+
var COMMON_CONTRACTIONS = {
|
|
4341
|
+
svc: "service",
|
|
4342
|
+
srv: "service",
|
|
4343
|
+
gw: "gateway",
|
|
4344
|
+
db: "database",
|
|
4345
|
+
app: "application",
|
|
4346
|
+
msg: "message",
|
|
4347
|
+
repo: "repository",
|
|
4348
|
+
cfg: "config",
|
|
4349
|
+
auth: "authenticator",
|
|
4350
|
+
mgr: "manager",
|
|
4351
|
+
fn: "function"
|
|
4352
|
+
};
|
|
4353
|
+
function splitIdentifierIntoTokens(s) {
|
|
4354
|
+
return s.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_\-.]+/g, " ").toLowerCase().trim().split(/\s+/).filter(Boolean);
|
|
4355
|
+
}
|
|
4356
|
+
function expandContractions(s) {
|
|
4357
|
+
const norm = s.toLowerCase().trim();
|
|
4358
|
+
const tokens = splitIdentifierIntoTokens(s);
|
|
4359
|
+
if (tokens.length === 0) return norm;
|
|
4360
|
+
const expandedTokens = tokens.map((t) => COMMON_CONTRACTIONS[t] ?? t);
|
|
4361
|
+
let res = expandedTokens.join("");
|
|
4362
|
+
if (tokens.length === 1) {
|
|
4363
|
+
for (const [k, v] of Object.entries(COMMON_CONTRACTIONS)) {
|
|
4364
|
+
if (k.length >= 2 && norm.length > k.length + 2 && norm.endsWith(k)) {
|
|
4365
|
+
res = norm.slice(0, -k.length) + v;
|
|
4366
|
+
break;
|
|
4367
|
+
}
|
|
4368
|
+
}
|
|
4369
|
+
}
|
|
4370
|
+
return res;
|
|
4371
|
+
}
|
|
4372
|
+
function findClosestMatch(word, candidates, maxDistance = 4) {
|
|
4373
|
+
const normalized = word.toLowerCase().trim();
|
|
4374
|
+
if (!normalized || !/^[a-zA-Z_][\w.-]*$/.test(normalized)) return null;
|
|
4375
|
+
const expandedWord = expandContractions(normalized);
|
|
4376
|
+
let bestMatch = null;
|
|
4377
|
+
let bestDistance = Infinity;
|
|
4378
|
+
const wordConsonants = normalized.replace(/[aeiou_-]/g, "");
|
|
4379
|
+
for (const candidate of candidates) {
|
|
4380
|
+
const candNorm = candidate.toLowerCase().trim();
|
|
4381
|
+
const expandedCand = expandContractions(candNorm);
|
|
4382
|
+
if (candNorm === normalized || expandedCand === expandedWord) {
|
|
4383
|
+
return { match: candidate, distance: 0 };
|
|
4384
|
+
}
|
|
4385
|
+
if (candNorm.startsWith(normalized) || normalized.startsWith(candNorm) || expandedCand.startsWith(expandedWord) || expandedWord.startsWith(expandedCand)) {
|
|
4386
|
+
const dist2 = Math.abs(candNorm.length - normalized.length);
|
|
4387
|
+
if (dist2 < bestDistance) {
|
|
4388
|
+
bestDistance = dist2;
|
|
4389
|
+
bestMatch = candidate;
|
|
4390
|
+
continue;
|
|
4391
|
+
}
|
|
4392
|
+
}
|
|
4393
|
+
if (wordConsonants.length >= 3) {
|
|
4394
|
+
const candConsonants = candNorm.replace(/[aeiou_-]/g, "");
|
|
4395
|
+
if (candConsonants.startsWith(wordConsonants) || wordConsonants.startsWith(candConsonants)) {
|
|
4396
|
+
const dist2 = 1;
|
|
4397
|
+
if (dist2 < bestDistance) {
|
|
4398
|
+
bestDistance = dist2;
|
|
4399
|
+
bestMatch = candidate;
|
|
4400
|
+
continue;
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
4403
|
+
}
|
|
4404
|
+
const dist = Math.min(
|
|
4405
|
+
damerauLevenshteinDistance(normalized, candNorm),
|
|
4406
|
+
damerauLevenshteinDistance(expandedWord, expandedCand)
|
|
4407
|
+
);
|
|
4408
|
+
const dynamicMax = Math.max(2, Math.min(maxDistance, Math.floor(Math.max(candidate.length, word.length) * 0.5)));
|
|
4409
|
+
if (dist <= dynamicMax && dist < bestDistance) {
|
|
4410
|
+
bestDistance = dist;
|
|
4411
|
+
bestMatch = candidate;
|
|
4412
|
+
}
|
|
4413
|
+
}
|
|
4414
|
+
return bestMatch ? { match: bestMatch, distance: bestDistance } : null;
|
|
4415
|
+
}
|
|
4416
|
+
var TOP_LEVEL_KEYWORDS = [
|
|
4417
|
+
"scene",
|
|
4418
|
+
"layout",
|
|
4419
|
+
"player",
|
|
4420
|
+
"var",
|
|
4421
|
+
"group",
|
|
4422
|
+
"beat",
|
|
4423
|
+
"style",
|
|
4424
|
+
"pattern",
|
|
4425
|
+
"edge",
|
|
4426
|
+
"annotation",
|
|
4427
|
+
"use"
|
|
4428
|
+
];
|
|
4429
|
+
var CUE_KEYWORDS = [
|
|
4430
|
+
"show",
|
|
4431
|
+
"hide",
|
|
4432
|
+
"glow",
|
|
4433
|
+
"focus",
|
|
4434
|
+
"frame",
|
|
4435
|
+
"use",
|
|
4436
|
+
"pulse",
|
|
4437
|
+
"highlight",
|
|
4438
|
+
"emphasize"
|
|
4439
|
+
];
|
|
4440
|
+
var LAYOUT_DIRECTIONS = ["LR", "RL", "TB", "BT"];
|
|
4441
|
+
var THEME_NAMES = Object.keys(THEMES);
|
|
4442
|
+
var ALL_NODE_KINDS = Array.from(
|
|
4443
|
+
/* @__PURE__ */ new Set([...Array.from(NODE_KINDS), ...Object.keys(NODE_ALIASES)])
|
|
4444
|
+
);
|
|
4445
|
+
var FOREIGN_SYNTAX_PATTERNS = [
|
|
4446
|
+
{
|
|
4447
|
+
type: "Mermaid",
|
|
4448
|
+
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,
|
|
4449
|
+
hint: `Mermaid syntax detected. Convert using 'transpile_to_markdy(source, format="mermaid")'.`
|
|
4450
|
+
},
|
|
4451
|
+
{
|
|
4452
|
+
type: "PlantUML",
|
|
4453
|
+
pattern: /^\s*(@startuml|@startmindmap|@startgantt|@startwbs)/i,
|
|
4454
|
+
hint: "PlantUML syntax detected. Markdy uses declarative 'scene', node declarations, and 'beat' blocks."
|
|
4455
|
+
},
|
|
4456
|
+
{
|
|
4457
|
+
type: "Graphviz DOT",
|
|
4458
|
+
pattern: /^\s*(digraph|strict\s+digraph|graph)\s+\w*\s*\{/i,
|
|
4459
|
+
hint: `Graphviz DOT syntax detected. Markdy uses 'scene', '<kind> <Id> ["Label"]', and 'beat' blocks.`
|
|
4460
|
+
},
|
|
4461
|
+
{
|
|
4462
|
+
type: "D2",
|
|
4463
|
+
pattern: /^\s*(direction\s*:|vars\s*:|classes\s*:)/i,
|
|
4464
|
+
hint: "D2 syntax detected. Markdy uses 'layout LR|TB' and 'beat <id>:' blocks."
|
|
4465
|
+
}
|
|
4466
|
+
];
|
|
4467
|
+
var INVALID_FLOW_OP_MAP = {
|
|
4468
|
+
"-->": "->",
|
|
4469
|
+
"->>": "->",
|
|
4470
|
+
"==>": "->",
|
|
4471
|
+
"<--": "<-",
|
|
4472
|
+
"<<-": "<-",
|
|
4473
|
+
"<==": "<-",
|
|
4474
|
+
"~~>": "~>",
|
|
4475
|
+
"~>>": "~>",
|
|
4476
|
+
"---": "--",
|
|
4477
|
+
"--->": "->",
|
|
4478
|
+
"-.-": "--",
|
|
4479
|
+
"-..->": "~>",
|
|
4480
|
+
">--": "->",
|
|
4481
|
+
">-": "->"
|
|
4482
|
+
};
|
|
4483
|
+
function diagnoseMarkdyCode(code, options = {}) {
|
|
4484
|
+
const issues = [];
|
|
4485
|
+
const lines = code.replace(/\r\n/g, "\n").split("\n");
|
|
4486
|
+
const declaredNodes = /* @__PURE__ */ new Set();
|
|
4487
|
+
const referencedNodes = /* @__PURE__ */ new Set();
|
|
4488
|
+
const checkArchitecture = options.checkArchitecture ?? true;
|
|
4489
|
+
for (const foreign of FOREIGN_SYNTAX_PATTERNS) {
|
|
4490
|
+
if (foreign.pattern.test(code)) {
|
|
4491
|
+
issues.push({
|
|
4492
|
+
line: 1,
|
|
4493
|
+
severity: "error",
|
|
4494
|
+
code: "FOREIGN_DIAGRAM_SYNTAX",
|
|
4495
|
+
message: `${foreign.type} syntax detected instead of MarkdyScript.`,
|
|
4496
|
+
snippet: lines.slice(0, 3).join("\n"),
|
|
4497
|
+
suggestion: foreign.hint,
|
|
4498
|
+
ruleExplanation: `MarkdyScript diagrams begin with 'scene [theme=paper layout=LR]', followed by node declarations ('<kind> <Id> ["Label"]') and dynamic 'beat' blocks.`
|
|
4499
|
+
});
|
|
4500
|
+
break;
|
|
4501
|
+
}
|
|
4502
|
+
}
|
|
4503
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4504
|
+
const line = lines[i].trim();
|
|
4505
|
+
if (!line || line.startsWith("//") || line.startsWith("#")) continue;
|
|
4506
|
+
const nodeMatch = line.match(/^(\w[\w.-]*)\s+(\w[\w.-]*)/);
|
|
4507
|
+
if (nodeMatch) {
|
|
4508
|
+
const head = nodeMatch[1].toLowerCase();
|
|
4509
|
+
const id = nodeMatch[2];
|
|
4510
|
+
if (!TOP_LEVEL_KEYWORDS.includes(head) && !CUE_KEYWORDS.includes(head) && !["show", "hide", "glow", "focus", "frame"].includes(head)) {
|
|
4511
|
+
declaredNodes.add(id);
|
|
4512
|
+
}
|
|
4513
|
+
}
|
|
4514
|
+
}
|
|
4515
|
+
let insideBeat = false;
|
|
4516
|
+
let insideGroup = false;
|
|
4517
|
+
let insidePattern = false;
|
|
4518
|
+
let beatIndent = 0;
|
|
4519
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
4520
|
+
const rawLine = lines[idx];
|
|
4521
|
+
const lineNo = idx + 1;
|
|
4522
|
+
const trimmed = rawLine.trim();
|
|
4523
|
+
if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("#")) {
|
|
4524
|
+
continue;
|
|
4525
|
+
}
|
|
4526
|
+
const indent = rawLine.match(/^\s*/)?.[0].length ?? 0;
|
|
4527
|
+
if (insideBeat && indent <= beatIndent && !trimmed.startsWith("&")) {
|
|
4528
|
+
insideBeat = false;
|
|
4529
|
+
}
|
|
4530
|
+
if (insideGroup && indent === 0) {
|
|
4531
|
+
insideGroup = false;
|
|
4532
|
+
}
|
|
4533
|
+
if (insidePattern && indent === 0) {
|
|
4534
|
+
insidePattern = false;
|
|
4535
|
+
}
|
|
4536
|
+
const tokens = trimmed.split(/\s+/);
|
|
4537
|
+
const firstWord = tokens[0];
|
|
4538
|
+
const firstWordLower = firstWord.toLowerCase();
|
|
4539
|
+
if (firstWordLower.startsWith("scen") || firstWordLower === "secne" || firstWordLower === "scence") {
|
|
4540
|
+
if (firstWord !== "scene") {
|
|
4541
|
+
issues.push({
|
|
4542
|
+
line: lineNo,
|
|
4543
|
+
severity: "error",
|
|
4544
|
+
code: "TYPO_KEYWORD",
|
|
4545
|
+
message: `Unknown keyword '${firstWord}'. Did you mean 'scene'?`,
|
|
4546
|
+
snippet: trimmed,
|
|
4547
|
+
suggestion: `Replace '${firstWord}' with 'scene'.`,
|
|
4548
|
+
didYouMean: "scene",
|
|
4549
|
+
ruleExplanation: "Every Markdy diagram starts with a 'scene' directive: 'scene theme=paper width=1280 height=720'.",
|
|
4550
|
+
fix: { original: firstWord, replacement: "scene" }
|
|
4551
|
+
});
|
|
4552
|
+
}
|
|
4553
|
+
const themeMatch = trimmed.match(/\btheme=(\w+)/);
|
|
4554
|
+
if (themeMatch) {
|
|
4555
|
+
const themeVal = themeMatch[1].toLowerCase();
|
|
4556
|
+
if (!THEME_NAMES.includes(themeVal)) {
|
|
4557
|
+
const closeTheme = findClosestMatch(themeVal, THEME_NAMES);
|
|
4558
|
+
issues.push({
|
|
4559
|
+
line: lineNo,
|
|
4560
|
+
severity: "warning",
|
|
4561
|
+
code: "UNKNOWN_THEME_OR_PROPERTY",
|
|
4562
|
+
message: `Unknown theme '${themeMatch[1]}'. Available themes: ${THEME_NAMES.join(", ")}.`,
|
|
4563
|
+
snippet: trimmed,
|
|
4564
|
+
suggestion: closeTheme ? `Use theme '${closeTheme.match}'.` : `Choose from: ${THEME_NAMES.join(", ")}.`,
|
|
4565
|
+
didYouMean: closeTheme?.match,
|
|
4566
|
+
ruleExplanation: "Supported themes: paper, editorial, midnight, blueprint, graphite, nebula, sketchy, terminal.",
|
|
4567
|
+
fix: closeTheme ? { original: themeMatch[0], replacement: `theme=${closeTheme.match}` } : void 0
|
|
4568
|
+
});
|
|
4569
|
+
}
|
|
4570
|
+
}
|
|
4571
|
+
const layoutMatch = trimmed.match(/\blayout\s*=?\s*(\w+)/i);
|
|
4572
|
+
if (layoutMatch) {
|
|
4573
|
+
const layoutVal = layoutMatch[1].toUpperCase();
|
|
4574
|
+
if (!LAYOUT_DIRECTIONS.includes(layoutVal)) {
|
|
4575
|
+
issues.push({
|
|
4576
|
+
line: lineNo,
|
|
4577
|
+
severity: "warning",
|
|
4578
|
+
code: "UNKNOWN_THEME_OR_PROPERTY",
|
|
4579
|
+
message: `Invalid layout direction '${layoutMatch[1]}'. Must be one of: LR, RL, TB, BT.`,
|
|
4580
|
+
snippet: trimmed,
|
|
4581
|
+
suggestion: "Use layout LR (left-to-right) or TB (top-to-bottom).",
|
|
4582
|
+
didYouMean: "LR",
|
|
4583
|
+
ruleExplanation: "Layout directions must be LR (Left-to-Right), RL, TB (Top-to-Bottom), or BT.",
|
|
4584
|
+
fix: { original: layoutMatch[0], replacement: "layout LR" }
|
|
4585
|
+
});
|
|
4586
|
+
}
|
|
4587
|
+
}
|
|
4588
|
+
continue;
|
|
4589
|
+
}
|
|
4590
|
+
if (firstWordLower === "layput" || firstWordLower === "layour" || firstWordLower === "layuot" || firstWordLower === "directon") {
|
|
4591
|
+
issues.push({
|
|
4592
|
+
line: lineNo,
|
|
4593
|
+
severity: "error",
|
|
4594
|
+
code: "TYPO_KEYWORD",
|
|
4595
|
+
message: `Typo in layout directive '${firstWord}'. Did you mean 'layout'?`,
|
|
4596
|
+
snippet: trimmed,
|
|
4597
|
+
suggestion: `Change '${firstWord}' to 'layout'.`,
|
|
4598
|
+
didYouMean: "layout",
|
|
4599
|
+
ruleExplanation: "Use 'layout LR' or 'layout TB' to define the diagram orientation.",
|
|
4600
|
+
fix: { original: firstWord, replacement: "layout" }
|
|
4601
|
+
});
|
|
4602
|
+
continue;
|
|
4603
|
+
}
|
|
4604
|
+
if (firstWordLower.startsWith("beat") || firstWordLower === "bea" || firstWordLower === "beats") {
|
|
4605
|
+
insideBeat = true;
|
|
4606
|
+
beatIndent = indent;
|
|
4607
|
+
if (firstWord !== "beat") {
|
|
4608
|
+
issues.push({
|
|
4609
|
+
line: lineNo,
|
|
4610
|
+
severity: "error",
|
|
4611
|
+
code: "TYPO_KEYWORD",
|
|
4612
|
+
message: `Unknown keyword '${firstWord}'. Did you mean 'beat'?`,
|
|
4613
|
+
snippet: trimmed,
|
|
4614
|
+
suggestion: `Replace '${firstWord}' with 'beat'.`,
|
|
4615
|
+
didYouMean: "beat",
|
|
4616
|
+
ruleExplanation: `Storyboard narrative steps start with 'beat <id> ["Caption"]:' followed by indented cues.`,
|
|
4617
|
+
fix: { original: firstWord, replacement: "beat" }
|
|
4618
|
+
});
|
|
4619
|
+
}
|
|
4620
|
+
if (!trimmed.endsWith(":") && !trimmed.endsWith("{")) {
|
|
4621
|
+
issues.push({
|
|
4622
|
+
line: lineNo,
|
|
4623
|
+
severity: "error",
|
|
4624
|
+
code: "MISSING_COLON",
|
|
4625
|
+
message: `Missing colon ':' at the end of beat header.`,
|
|
4626
|
+
snippet: trimmed,
|
|
4627
|
+
suggestion: `Append ':' to the beat declaration: '${trimmed}:'`,
|
|
4628
|
+
ruleExplanation: "Every beat block must end with a colon ':', e.g. `beat checkout \"Process Payment\":`.",
|
|
4629
|
+
fix: { original: trimmed, replacement: `${trimmed}:` }
|
|
4630
|
+
});
|
|
4631
|
+
}
|
|
4632
|
+
continue;
|
|
4633
|
+
}
|
|
4634
|
+
if (firstWordLower === "group" || firstWordLower === "groop" || firstWordLower === "grp") {
|
|
4635
|
+
insideGroup = true;
|
|
4636
|
+
if (firstWord !== "group") {
|
|
4637
|
+
issues.push({
|
|
4638
|
+
line: lineNo,
|
|
4639
|
+
severity: "error",
|
|
4640
|
+
code: "TYPO_KEYWORD",
|
|
4641
|
+
message: `Typo in group keyword '${firstWord}'. Did you mean 'group'?`,
|
|
4642
|
+
snippet: trimmed,
|
|
4643
|
+
suggestion: `Change '${firstWord}' to 'group'.`,
|
|
4644
|
+
didYouMean: "group",
|
|
4645
|
+
fix: { original: firstWord, replacement: "group" }
|
|
4646
|
+
});
|
|
4647
|
+
}
|
|
4648
|
+
if (!trimmed.includes(":")) {
|
|
4649
|
+
issues.push({
|
|
4650
|
+
line: lineNo,
|
|
4651
|
+
severity: "error",
|
|
4652
|
+
code: "MISSING_COLON",
|
|
4653
|
+
message: `Missing colon ':' in group definition.`,
|
|
4654
|
+
snippet: trimmed,
|
|
4655
|
+
suggestion: `Add a colon before group members: e.g. 'group <id> "Label": Node1 Node2'`,
|
|
4656
|
+
ruleExplanation: 'Groups require a colon: `group clients "User Tier": WebApp MobileApp`.'
|
|
4657
|
+
});
|
|
4658
|
+
} else {
|
|
4659
|
+
const colonIdx = trimmed.indexOf(":");
|
|
4660
|
+
const membersRaw = trimmed.slice(colonIdx + 1).trim();
|
|
4661
|
+
if (membersRaw) {
|
|
4662
|
+
const members = membersRaw.split(/\s+/).filter((m) => m && !m.includes("=") && !m.startsWith("$") && /^[a-zA-Z_][\w.-]*$/.test(m));
|
|
4663
|
+
for (const mem of members) {
|
|
4664
|
+
referencedNodes.add(mem);
|
|
4665
|
+
if (declaredNodes.size > 0 && !declaredNodes.has(mem)) {
|
|
4666
|
+
const closest = findClosestMatch(mem, declaredNodes);
|
|
4667
|
+
issues.push({
|
|
4668
|
+
line: lineNo,
|
|
4669
|
+
severity: "warning",
|
|
4670
|
+
code: "UNDEFINED_NODE_REFERENCE",
|
|
4671
|
+
message: `Group references undefined node '${mem}'.${closest ? ` Did you mean '${closest.match}'?` : ""}`,
|
|
4672
|
+
snippet: trimmed,
|
|
4673
|
+
suggestion: closest ? `Replace '${mem}' with declared node '${closest.match}'.` : `Ensure node '${mem}' is declared before adding it to a group.`,
|
|
4674
|
+
didYouMean: closest?.match,
|
|
4675
|
+
fix: closest ? { original: mem, replacement: closest.match } : void 0
|
|
4676
|
+
});
|
|
4677
|
+
}
|
|
4678
|
+
}
|
|
4679
|
+
}
|
|
4680
|
+
}
|
|
4681
|
+
continue;
|
|
4682
|
+
}
|
|
4683
|
+
if (firstWordLower === "annotation") {
|
|
4684
|
+
const targetMatch = trimmed.match(/\btarget=([\w.-]+)/i);
|
|
4685
|
+
if (targetMatch) {
|
|
4686
|
+
const target = targetMatch[1];
|
|
4687
|
+
referencedNodes.add(target);
|
|
4688
|
+
if (declaredNodes.size > 0 && !declaredNodes.has(target)) {
|
|
4689
|
+
const closest = findClosestMatch(target, declaredNodes);
|
|
4690
|
+
issues.push({
|
|
4691
|
+
line: lineNo,
|
|
4692
|
+
severity: "warning",
|
|
4693
|
+
code: "UNDEFINED_NODE_REFERENCE",
|
|
4694
|
+
message: `Annotation targets undefined node '${target}'.${closest ? ` Did you mean '${closest.match}'?` : ""}`,
|
|
4695
|
+
snippet: trimmed,
|
|
4696
|
+
suggestion: closest ? `Replace target='${target}' with target='${closest.match}'.` : void 0,
|
|
4697
|
+
didYouMean: closest?.match,
|
|
4698
|
+
fix: closest ? { original: target, replacement: closest.match } : void 0
|
|
4699
|
+
});
|
|
4700
|
+
}
|
|
4701
|
+
}
|
|
4702
|
+
continue;
|
|
4703
|
+
}
|
|
4704
|
+
if (!insideBeat) {
|
|
4705
|
+
const isCueWord = CUE_KEYWORDS.includes(firstWordLower) || firstWordLower === "shwo" || firstWordLower === "fram" || firstWordLower === "focsu" || firstWordLower === "gloww";
|
|
4706
|
+
const isFlowLine = /(-->|->|<-|<--|~>|--)/.test(trimmed);
|
|
4707
|
+
if (isCueWord || isFlowLine) {
|
|
4708
|
+
issues.push({
|
|
4709
|
+
line: lineNo,
|
|
4710
|
+
severity: "error",
|
|
4711
|
+
code: "CUE_OUTSIDE_BEAT",
|
|
4712
|
+
message: `Action cue or flow line '${trimmed}' found at top-level outside a 'beat' block.`,
|
|
4713
|
+
snippet: trimmed,
|
|
4714
|
+
suggestion: `Wrap this line inside an indented beat block:
|
|
4715
|
+
beat main "System Flow":
|
|
4716
|
+
${trimmed}`,
|
|
4717
|
+
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."
|
|
4718
|
+
});
|
|
4719
|
+
continue;
|
|
4720
|
+
}
|
|
4721
|
+
}
|
|
4722
|
+
for (const [invalidOp, validOp] of Object.entries(INVALID_FLOW_OP_MAP)) {
|
|
4723
|
+
if (trimmed.includes(invalidOp)) {
|
|
4724
|
+
issues.push({
|
|
4725
|
+
line: lineNo,
|
|
4726
|
+
severity: "error",
|
|
4727
|
+
code: "INVALID_FLOW_OPERATOR",
|
|
4728
|
+
message: `Invalid flow operator '${invalidOp}'. Did you mean '${validOp}'?`,
|
|
4729
|
+
snippet: trimmed,
|
|
4730
|
+
suggestion: `Replace '${invalidOp}' with '${validOp}'.`,
|
|
4731
|
+
didYouMean: validOp,
|
|
4732
|
+
ruleExplanation: "Markdy uses 4 clean flow operators: '->' (request/call), '<-' (response/return), '~>' (asynchronous/event), and '--' (static dependency).",
|
|
4733
|
+
fix: { original: invalidOp, replacement: validOp }
|
|
4734
|
+
});
|
|
4735
|
+
}
|
|
4736
|
+
}
|
|
4737
|
+
if (!insideBeat && !insideGroup && !insidePattern) {
|
|
4738
|
+
const match = trimmed.match(/^(\w[\w.-]*)\s+(\w[\w.-]*)(.*)$/);
|
|
4739
|
+
if (match) {
|
|
4740
|
+
const rawKind = match[1];
|
|
4741
|
+
const rawKindLower = rawKind.toLowerCase();
|
|
4742
|
+
const id = match[2];
|
|
4743
|
+
const rest = match[3].trim();
|
|
4744
|
+
if (!TOP_LEVEL_KEYWORDS.includes(rawKindLower)) {
|
|
4745
|
+
const canonical = canonicalNodeKind(rawKindLower);
|
|
4746
|
+
if (!NODE_KINDS.has(canonical)) {
|
|
4747
|
+
const matchKind = findClosestMatch(rawKindLower, ALL_NODE_KINDS);
|
|
4748
|
+
const resolvedKind = matchKind ? canonicalNodeKind(matchKind.match) : void 0;
|
|
4749
|
+
issues.push({
|
|
4750
|
+
line: lineNo,
|
|
4751
|
+
severity: "error",
|
|
4752
|
+
code: "TYPO_NODE_KIND",
|
|
4753
|
+
message: `Unknown node kind '${rawKind}'.${resolvedKind ? ` Did you mean '${resolvedKind}'?` : ""}`,
|
|
4754
|
+
snippet: trimmed,
|
|
4755
|
+
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.`,
|
|
4756
|
+
didYouMean: resolvedKind,
|
|
4757
|
+
ruleExplanation: "Markdy requires semantic node kinds (e.g. browser, gateway, service, database, cache, queue, worker, cloud, storage, pod, cluster).",
|
|
4758
|
+
fix: resolvedKind ? { original: rawKind, replacement: resolvedKind } : void 0
|
|
4759
|
+
});
|
|
4760
|
+
}
|
|
4761
|
+
if (rest && !rest.startsWith('"') && !rest.startsWith("style=") && !rest.startsWith("icon=")) {
|
|
4762
|
+
const hasMultipleWords = rest.split(/\s+/).length > 1;
|
|
4763
|
+
if (hasMultipleWords && !rest.includes("=")) {
|
|
4764
|
+
issues.push({
|
|
4765
|
+
line: lineNo,
|
|
4766
|
+
severity: "error",
|
|
4767
|
+
code: "UNQUOTED_STRING_LABEL",
|
|
4768
|
+
message: `Unquoted multi-word label '${rest}'. String labels with spaces must be enclosed in double quotes.`,
|
|
4769
|
+
snippet: trimmed,
|
|
4770
|
+
suggestion: `Wrap the label in quotes: '${rawKind} ${id} "${rest}"'`,
|
|
4771
|
+
ruleExplanation: 'String labels containing spaces must be enclosed in double quotes `"..."`.',
|
|
4772
|
+
fix: { original: `${id} ${rest}`, replacement: `${id} "${rest}"` }
|
|
4773
|
+
});
|
|
4774
|
+
}
|
|
4775
|
+
}
|
|
4776
|
+
const quoteCount = (rest.match(/"/g) || []).length;
|
|
4777
|
+
if (quoteCount % 2 !== 0) {
|
|
4778
|
+
issues.push({
|
|
4779
|
+
line: lineNo,
|
|
4780
|
+
severity: "error",
|
|
4781
|
+
code: "UNTERMINATED_STRING",
|
|
4782
|
+
message: `Unterminated string quote in node declaration.`,
|
|
4783
|
+
snippet: trimmed,
|
|
4784
|
+
suggestion: `Add closing quote: '${trimmed}"'`,
|
|
4785
|
+
fix: { original: trimmed, replacement: `${trimmed}"` }
|
|
4786
|
+
});
|
|
4787
|
+
}
|
|
4788
|
+
}
|
|
4789
|
+
}
|
|
4790
|
+
}
|
|
4791
|
+
if (insideBeat) {
|
|
4792
|
+
if (trimmed.includes("->") || trimmed.includes("<-") || trimmed.includes("~>") || trimmed.includes("--")) {
|
|
4793
|
+
const cleanFlow = trimmed.replace(/"[^"]*"/g, " ");
|
|
4794
|
+
const flowTokens = cleanFlow.split(/->|<-|~>|--|&|\s+/).filter(Boolean);
|
|
4795
|
+
for (const token of flowTokens) {
|
|
4796
|
+
if (token.includes("=") || token.startsWith("$") || !/^[a-zA-Z_][\w.-]*$/.test(token) || ["and", "show", "hide", "glow", "focus", "frame", "pulse", "use"].includes(token.toLowerCase())) {
|
|
4797
|
+
continue;
|
|
4798
|
+
}
|
|
4799
|
+
referencedNodes.add(token);
|
|
4800
|
+
if (declaredNodes.size > 0 && !declaredNodes.has(token)) {
|
|
4801
|
+
const closest = findClosestMatch(token, declaredNodes);
|
|
4802
|
+
issues.push({
|
|
4803
|
+
line: lineNo,
|
|
4804
|
+
severity: "error",
|
|
4805
|
+
code: "UNDEFINED_NODE_REFERENCE",
|
|
4806
|
+
message: `Flow references undefined node '${token}'.${closest ? ` Did you mean '${closest.match}'?` : ""}`,
|
|
4807
|
+
snippet: trimmed,
|
|
4808
|
+
suggestion: closest ? `Replace '${token}' with declared node '${closest.match}'.` : `Declare node '${token}' at top of diagram before referencing it.`,
|
|
4809
|
+
didYouMean: closest?.match,
|
|
4810
|
+
ruleExplanation: "All nodes used in flow transitions must be declared beforehand (e.g. `service ${token}`).",
|
|
4811
|
+
fix: closest ? { original: token, replacement: closest.match } : void 0
|
|
4812
|
+
});
|
|
4813
|
+
}
|
|
4814
|
+
}
|
|
4815
|
+
}
|
|
4816
|
+
const cueMatch = trimmed.match(/^(show|hide|glow|focus|frame)\s+(.+)$/i);
|
|
4817
|
+
if (cueMatch) {
|
|
4818
|
+
const cueKind = cueMatch[1].toLowerCase();
|
|
4819
|
+
const targetsRaw = cueMatch[2].replace(/\b\w+=[^\s]+/g, "").trim();
|
|
4820
|
+
const targets = targetsRaw.split(/\s+/).filter((t) => t && !t.startsWith("$") && !t.startsWith('"') && /^[a-zA-Z_][\w.-]*$/.test(t));
|
|
4821
|
+
for (const target of targets) {
|
|
4822
|
+
referencedNodes.add(target);
|
|
4823
|
+
if (declaredNodes.size > 0 && !declaredNodes.has(target)) {
|
|
4824
|
+
const closest = findClosestMatch(target, declaredNodes);
|
|
4825
|
+
issues.push({
|
|
4826
|
+
line: lineNo,
|
|
4827
|
+
severity: "warning",
|
|
4828
|
+
code: "UNDEFINED_NODE_REFERENCE",
|
|
4829
|
+
message: `${cueKind} cue targets undefined node '${target}'.${closest ? ` Did you mean '${closest.match}'?` : ""}`,
|
|
4830
|
+
snippet: trimmed,
|
|
4831
|
+
suggestion: closest ? `Replace '${target}' with '${closest.match}'.` : `Ensure node '${target}' is declared before targeting it.`,
|
|
4832
|
+
didYouMean: closest?.match,
|
|
4833
|
+
fix: closest ? { original: target, replacement: closest.match } : void 0
|
|
4834
|
+
});
|
|
4835
|
+
}
|
|
4836
|
+
}
|
|
4837
|
+
}
|
|
4838
|
+
}
|
|
4839
|
+
}
|
|
4301
4840
|
try {
|
|
4302
|
-
ast = parse(
|
|
4841
|
+
const ast = parse(code);
|
|
4842
|
+
if (checkArchitecture) {
|
|
4843
|
+
const archViolations = validateArchitecture(ast);
|
|
4844
|
+
for (const v of archViolations) {
|
|
4845
|
+
issues.push({
|
|
4846
|
+
line: v.line ?? 1,
|
|
4847
|
+
severity: v.severity === "error" ? "error" : "warning",
|
|
4848
|
+
code: "ARCH_RULE_VIOLATION",
|
|
4849
|
+
message: `[${v.ruleName}] ${v.message}`,
|
|
4850
|
+
suggestion: `Refactor architecture to comply with Well-Architected governance: ${v.ruleName}.`,
|
|
4851
|
+
ruleExplanation: `Architecture Rule Preset: ${v.ruleName}`
|
|
4852
|
+
});
|
|
4853
|
+
}
|
|
4854
|
+
}
|
|
4855
|
+
for (const d of ast.diagnostics) {
|
|
4856
|
+
if (d.message.includes("flow cycle detected")) {
|
|
4857
|
+
issues.push({
|
|
4858
|
+
line: d.line,
|
|
4859
|
+
severity: "warning",
|
|
4860
|
+
code: "FLOW_CYCLE_RETURN_EDGE",
|
|
4861
|
+
message: d.message,
|
|
4862
|
+
suggestion: "Use '<-' for return/response edges instead of '->' or '~>'.",
|
|
4863
|
+
ruleExplanation: "Cycle Safety: Always use '<-' when returning calls to callers to prevent cyclical layout rank collisions."
|
|
4864
|
+
});
|
|
4865
|
+
} else {
|
|
4866
|
+
const isDuplicate = issues.some((iss) => iss.line === d.line && iss.message === d.message);
|
|
4867
|
+
if (!isDuplicate) {
|
|
4868
|
+
issues.push({
|
|
4869
|
+
line: d.line,
|
|
4870
|
+
severity: d.severity === "error" ? "error" : "warning",
|
|
4871
|
+
code: "SYNTAX_ERROR",
|
|
4872
|
+
message: d.message
|
|
4873
|
+
});
|
|
4874
|
+
}
|
|
4875
|
+
}
|
|
4876
|
+
}
|
|
4303
4877
|
} catch (err) {
|
|
4304
|
-
|
|
4878
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4879
|
+
const lineMatch = errMsg.match(/line\s+(\d+):\s*(.*)/);
|
|
4880
|
+
const errLine = lineMatch ? Number(lineMatch[1]) : 1;
|
|
4881
|
+
const cleanMsg = lineMatch ? lineMatch[2] : errMsg;
|
|
4882
|
+
const alreadyCaptured = issues.some((iss) => iss.line === errLine && iss.severity === "error");
|
|
4883
|
+
if (!alreadyCaptured) {
|
|
4884
|
+
issues.push({
|
|
4885
|
+
line: errLine,
|
|
4886
|
+
severity: "error",
|
|
4887
|
+
code: "SYNTAX_ERROR",
|
|
4888
|
+
message: cleanMsg,
|
|
4889
|
+
snippet: lines[errLine - 1] ?? "",
|
|
4890
|
+
suggestion: "Review Markdy 4-step structure: Directives -> Semantic Nodes -> Groups -> Beats."
|
|
4891
|
+
});
|
|
4892
|
+
}
|
|
4305
4893
|
}
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4894
|
+
const autoRepair = repairMarkdyCode(code, {
|
|
4895
|
+
transpileMermaid: options.transpileMermaid,
|
|
4896
|
+
precomputedIssues: issues
|
|
4897
|
+
});
|
|
4898
|
+
const repairPrompt = buildAIHealingPrompt(code, issues, autoRepair.repairedCode);
|
|
4899
|
+
const errorCount = issues.filter((i) => i.severity === "error").length;
|
|
4900
|
+
const warningCount = issues.filter((i) => i.severity === "warning").length;
|
|
4901
|
+
const isValid = errorCount === 0;
|
|
4902
|
+
const summary = isValid ? `\u2705 Markdy code is valid with ${warningCount} warning(s).` : `\u274C Markdy code has ${errorCount} syntax error(s) and ${warningCount} warning(s).`;
|
|
4903
|
+
return {
|
|
4904
|
+
isValid,
|
|
4905
|
+
errorCount,
|
|
4906
|
+
warningCount,
|
|
4907
|
+
issues,
|
|
4908
|
+
repairedCode: autoRepair.repairedCode !== code ? autoRepair.repairedCode : void 0,
|
|
4909
|
+
repairPrompt,
|
|
4910
|
+
summary,
|
|
4911
|
+
declaredNodes: Array.from(declaredNodes),
|
|
4912
|
+
referencedNodes: Array.from(referencedNodes)
|
|
4913
|
+
};
|
|
4914
|
+
}
|
|
4915
|
+
function escapeRegex(s) {
|
|
4916
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4917
|
+
}
|
|
4918
|
+
function repairMarkdyCode(code, options) {
|
|
4919
|
+
const changes = [];
|
|
4920
|
+
if (options?.transpileMermaid && /^\s*(graph\s+(TB|TD|BT|RL|LR)|flowchart\s+(TB|TD|BT|RL|LR))/i.test(code)) {
|
|
4921
|
+
try {
|
|
4922
|
+
const transpiledCode = options.transpileMermaid(code);
|
|
4923
|
+
changes.push("Transpiled Mermaid flowchart to canonical MarkdyScript.");
|
|
4924
|
+
return {
|
|
4925
|
+
repairedCode: transpiledCode,
|
|
4926
|
+
changes,
|
|
4927
|
+
isFixed: true
|
|
4928
|
+
};
|
|
4929
|
+
} catch {
|
|
4930
|
+
}
|
|
4321
4931
|
}
|
|
4322
|
-
const
|
|
4323
|
-
|
|
4324
|
-
|
|
4932
|
+
const lines = code.replace(/\r\n/g, "\n").split("\n");
|
|
4933
|
+
const repairedLines = [];
|
|
4934
|
+
const declaredNodes = /* @__PURE__ */ new Set();
|
|
4935
|
+
for (const line of lines) {
|
|
4936
|
+
const trimmed = line.trim();
|
|
4937
|
+
const nodeMatch = trimmed.match(/^(\w[\w.-]*)\s+(\w[\w.-]*)/);
|
|
4938
|
+
if (nodeMatch && !TOP_LEVEL_KEYWORDS.includes(nodeMatch[1].toLowerCase())) {
|
|
4939
|
+
declaredNodes.add(nodeMatch[2]);
|
|
4940
|
+
}
|
|
4325
4941
|
}
|
|
4942
|
+
let inBeat = false;
|
|
4943
|
+
let beatIndent = 0;
|
|
4944
|
+
let hasBareCues = false;
|
|
4945
|
+
const bareCuesBuffer = [];
|
|
4946
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
4947
|
+
let line = lines[idx];
|
|
4948
|
+
const trimmed = line.trim();
|
|
4949
|
+
if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("#")) {
|
|
4950
|
+
repairedLines.push(line);
|
|
4951
|
+
continue;
|
|
4952
|
+
}
|
|
4953
|
+
const indent = line.match(/^\s*/)?.[0] ?? "";
|
|
4954
|
+
const indentLen = indent.length;
|
|
4955
|
+
let processed = trimmed;
|
|
4956
|
+
if (inBeat && indentLen <= beatIndent && !trimmed.startsWith("&") && !trimmed.startsWith("beat ") && !trimmed.startsWith("bea ")) {
|
|
4957
|
+
inBeat = false;
|
|
4958
|
+
}
|
|
4959
|
+
processed = processed.replace(/^scen\b|^secne\b|^scence\b/i, "scene");
|
|
4960
|
+
processed = processed.replace(/^layput\b|^layour\b|^layuot\b/i, "layout");
|
|
4961
|
+
processed = processed.replace(/^groop\b|^grp\b/i, "group");
|
|
4962
|
+
processed = processed.replace(/^bea\b|^beats\b/i, "beat");
|
|
4963
|
+
processed = processed.replace(/^patern\b/i, "pattern");
|
|
4964
|
+
for (const [inv, valid] of Object.entries(INVALID_FLOW_OP_MAP)) {
|
|
4965
|
+
if (processed.includes(inv)) {
|
|
4966
|
+
processed = processed.replaceAll(inv, valid);
|
|
4967
|
+
changes.push(`Line ${idx + 1}: Fixed invalid flow operator '${inv}' -> '${valid}'.`);
|
|
4968
|
+
}
|
|
4969
|
+
}
|
|
4970
|
+
if (/^beat\s+/i.test(processed)) {
|
|
4971
|
+
inBeat = true;
|
|
4972
|
+
beatIndent = indentLen;
|
|
4973
|
+
if (/^beat\s+[\w.-]+(\s+"[^"]*")?\s*$/i.test(processed)) {
|
|
4974
|
+
processed = `${processed}:`;
|
|
4975
|
+
changes.push(`Line ${idx + 1}: Added missing colon ':' to beat header.`);
|
|
4976
|
+
}
|
|
4977
|
+
}
|
|
4978
|
+
if (/^group\s+/i.test(processed)) {
|
|
4979
|
+
if (!processed.includes(":")) {
|
|
4980
|
+
const groupMatch = processed.match(/^group\s+([\w.-]+)(?:\s+"([^"]*)")?\s+(.+)$/i);
|
|
4981
|
+
if (groupMatch) {
|
|
4982
|
+
const gid = groupMatch[1];
|
|
4983
|
+
const label = groupMatch[2] ? ` "${groupMatch[2]}"` : "";
|
|
4984
|
+
const rest = groupMatch[3];
|
|
4985
|
+
processed = `group ${gid}${label}: ${rest}`;
|
|
4986
|
+
changes.push(`Line ${idx + 1}: Added missing colon ':' to group header.`);
|
|
4987
|
+
}
|
|
4988
|
+
}
|
|
4989
|
+
}
|
|
4990
|
+
if (!inBeat && !processed.startsWith("scene") && !processed.startsWith("layout") && !processed.startsWith("group")) {
|
|
4991
|
+
const match = processed.match(/^(\w[\w.-]*)\s+(\w[\w.-]*)(.*)$/);
|
|
4992
|
+
if (match) {
|
|
4993
|
+
const rawKind = match[1];
|
|
4994
|
+
const rawKindLower = rawKind.toLowerCase();
|
|
4995
|
+
const id = match[2];
|
|
4996
|
+
let rest = match[3].trim();
|
|
4997
|
+
const canonical = canonicalNodeKind(rawKindLower);
|
|
4998
|
+
if (!NODE_KINDS.has(canonical)) {
|
|
4999
|
+
const matchKind = findClosestMatch(rawKindLower, ALL_NODE_KINDS);
|
|
5000
|
+
if (matchKind) {
|
|
5001
|
+
const canonicalFix = canonicalNodeKind(matchKind.match);
|
|
5002
|
+
changes.push(`Line ${idx + 1}: Repaired node kind '${rawKind}' -> '${canonicalFix}'.`);
|
|
5003
|
+
processed = `${canonicalFix} ${id}${rest ? ` ${rest}` : ""}`;
|
|
5004
|
+
}
|
|
5005
|
+
}
|
|
5006
|
+
if (rest && !rest.startsWith('"') && !rest.startsWith("style=") && !rest.startsWith("icon=") && rest.split(/\s+/).length > 1 && !rest.includes("=")) {
|
|
5007
|
+
changes.push(`Line ${idx + 1}: Enclosed multi-word label in quotes: "${rest}".`);
|
|
5008
|
+
processed = `${canonicalNodeKind(rawKindLower)} ${id} "${rest}"`;
|
|
5009
|
+
}
|
|
5010
|
+
}
|
|
5011
|
+
}
|
|
5012
|
+
if ((inBeat || processed.startsWith("group ")) && declaredNodes.size > 0) {
|
|
5013
|
+
if (processed.includes("->") || processed.includes("<-") || processed.includes("~>") || processed.includes("--") || processed.startsWith("group ")) {
|
|
5014
|
+
const cleanFlow = processed.replace(/"[^"]*"/g, '""');
|
|
5015
|
+
const flowTokens = cleanFlow.split(/->|<-|~>|--|&|:|\s+/).filter(Boolean);
|
|
5016
|
+
for (const tok of flowTokens) {
|
|
5017
|
+
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)) {
|
|
5018
|
+
const match = findClosestMatch(tok, declaredNodes);
|
|
5019
|
+
if (match) {
|
|
5020
|
+
const regex = new RegExp(`\\b${escapeRegex(tok)}\\b`, "g");
|
|
5021
|
+
processed = processed.replace(regex, match.match);
|
|
5022
|
+
changes.push(`Line ${idx + 1}: Corrected typo in node reference '${tok}' -> '${match.match}'.`);
|
|
5023
|
+
}
|
|
5024
|
+
}
|
|
5025
|
+
}
|
|
5026
|
+
}
|
|
5027
|
+
}
|
|
5028
|
+
if (!inBeat) {
|
|
5029
|
+
const isCue = CUE_KEYWORDS.includes(processed.split(/\s+/)[0].toLowerCase());
|
|
5030
|
+
const isFlow = /(-->|->|<-|<--|~>|--)/.test(processed);
|
|
5031
|
+
if (isCue || isFlow) {
|
|
5032
|
+
hasBareCues = true;
|
|
5033
|
+
bareCuesBuffer.push(` ${processed}`);
|
|
5034
|
+
changes.push(`Line ${idx + 1}: Wrapped bare cue '${processed}' into 'beat main'.`);
|
|
5035
|
+
continue;
|
|
5036
|
+
}
|
|
5037
|
+
}
|
|
5038
|
+
repairedLines.push(`${indent}${processed}`);
|
|
5039
|
+
}
|
|
5040
|
+
if (hasBareCues && !lines.some((l) => l.trim().startsWith("beat "))) {
|
|
5041
|
+
repairedLines.push('\nbeat main "Flow":');
|
|
5042
|
+
repairedLines.push(...bareCuesBuffer);
|
|
5043
|
+
}
|
|
5044
|
+
const repairedCode = repairedLines.join("\n");
|
|
5045
|
+
let isFixed = false;
|
|
5046
|
+
try {
|
|
5047
|
+
parse(repairedCode);
|
|
5048
|
+
isFixed = true;
|
|
5049
|
+
} catch {
|
|
5050
|
+
isFixed = false;
|
|
5051
|
+
}
|
|
5052
|
+
return {
|
|
5053
|
+
repairedCode,
|
|
5054
|
+
changes,
|
|
5055
|
+
isFixed
|
|
5056
|
+
};
|
|
5057
|
+
}
|
|
5058
|
+
function buildAIHealingPrompt(sourceCode, issues, suggestedRepair) {
|
|
5059
|
+
if (issues.length === 0) {
|
|
5060
|
+
return "The MarkdyScript diagram is completely valid. No healing required.";
|
|
5061
|
+
}
|
|
5062
|
+
const errorIssues = issues.filter((i) => i.severity === "error" && i.code !== "ARCH_RULE_VIOLATION");
|
|
5063
|
+
const warningIssues = issues.filter((i) => i.severity === "warning" && i.code !== "ARCH_RULE_VIOLATION");
|
|
5064
|
+
const archIssues = issues.filter((i) => i.code === "ARCH_RULE_VIOLATION");
|
|
4326
5065
|
const promptSections = [
|
|
4327
|
-
"
|
|
4328
|
-
""
|
|
4329
|
-
"### Diagnostics:",
|
|
4330
|
-
...ast.diagnostics.map((d) => ` - Line ${d.line}: ${d.message}`),
|
|
4331
|
-
"",
|
|
4332
|
-
"### Architectural Violations:",
|
|
4333
|
-
...archViolations.map((v) => ` - [${v.severity.toUpperCase()}] ${v.ruleName}: ${v.message}`),
|
|
4334
|
-
"",
|
|
4335
|
-
"Please revise the diagram code to resolve all issues while preserving semantic nodes and beats:",
|
|
4336
|
-
"```markdy",
|
|
4337
|
-
sourceCode,
|
|
4338
|
-
"```"
|
|
5066
|
+
"### Markdy Diagram Syntax & Architectural Diagnostics",
|
|
5067
|
+
""
|
|
4339
5068
|
];
|
|
5069
|
+
if (errorIssues.length > 0) {
|
|
5070
|
+
promptSections.push("The following MarkdyScript failed to parse with syntax errors:");
|
|
5071
|
+
promptSections.push("");
|
|
5072
|
+
promptSections.push("### Diagnostics & Critical Errors:");
|
|
5073
|
+
for (const err of errorIssues) {
|
|
5074
|
+
const lineStr = err.line ? ` - Line ${err.line}: ` : " - ";
|
|
5075
|
+
promptSections.push(`${lineStr}${err.message}`);
|
|
5076
|
+
if (err.snippet) {
|
|
5077
|
+
promptSections.push(` *Problem:* \`${err.snippet}\``);
|
|
5078
|
+
}
|
|
5079
|
+
if (err.suggestion) {
|
|
5080
|
+
promptSections.push(` *Recommendation:* ${err.suggestion}`);
|
|
5081
|
+
}
|
|
5082
|
+
if (err.didYouMean) {
|
|
5083
|
+
promptSections.push(` *Did you mean:* \`${err.didYouMean}\`?`);
|
|
5084
|
+
}
|
|
5085
|
+
}
|
|
5086
|
+
promptSections.push("");
|
|
5087
|
+
}
|
|
5088
|
+
if (archIssues.length > 0) {
|
|
5089
|
+
promptSections.push("### Architectural Violations:");
|
|
5090
|
+
for (const v of archIssues) {
|
|
5091
|
+
const lineStr = v.line ? ` (line ${v.line})` : "";
|
|
5092
|
+
promptSections.push(` - [${v.severity.toUpperCase()}] ${v.message}${lineStr}`);
|
|
5093
|
+
if (v.suggestion) {
|
|
5094
|
+
promptSections.push(` *Recommendation:* ${v.suggestion}`);
|
|
5095
|
+
}
|
|
5096
|
+
}
|
|
5097
|
+
promptSections.push("");
|
|
5098
|
+
} else if (warningIssues.length > 0) {
|
|
5099
|
+
promptSections.push("### Diagnostics & Warnings:");
|
|
5100
|
+
for (const w of warningIssues) {
|
|
5101
|
+
const lineStr = w.line ? ` - Line ${w.line}: ` : " - ";
|
|
5102
|
+
promptSections.push(`${lineStr}${w.message}`);
|
|
5103
|
+
if (w.suggestion) {
|
|
5104
|
+
promptSections.push(` *Recommendation:* ${w.suggestion}`);
|
|
5105
|
+
}
|
|
5106
|
+
}
|
|
5107
|
+
promptSections.push("");
|
|
5108
|
+
}
|
|
5109
|
+
promptSections.push("### Canonical Markdy Mental Model Guidelines:");
|
|
5110
|
+
promptSections.push("1. **Directives**: `scene theme=paper width=1280 height=720` and `layout LR` (or TB).");
|
|
5111
|
+
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.');
|
|
5112
|
+
promptSections.push('3. **Groups**: `group <id> "<Label>": Node1 Node2`');
|
|
5113
|
+
promptSections.push('4. **Beats**: `beat <id> "<Caption>":` with indented flows and cues.');
|
|
5114
|
+
promptSections.push("5. **Cycle Safety**: Always use `<-` for response/return edges to prevent layout rank cycles. Never use `->` to return to a previous node.");
|
|
5115
|
+
promptSections.push("");
|
|
5116
|
+
if (suggestedRepair && suggestedRepair !== sourceCode) {
|
|
5117
|
+
promptSections.push("### Proposed Repaired MarkdyScript:");
|
|
5118
|
+
promptSections.push("```markdy");
|
|
5119
|
+
promptSections.push(suggestedRepair);
|
|
5120
|
+
promptSections.push("```");
|
|
5121
|
+
promptSections.push("");
|
|
5122
|
+
}
|
|
5123
|
+
promptSections.push("Please revise the diagram code to resolve all issues while preserving semantic nodes and beats:");
|
|
5124
|
+
promptSections.push("```markdy");
|
|
5125
|
+
promptSections.push(sourceCode);
|
|
5126
|
+
promptSections.push("```");
|
|
5127
|
+
return promptSections.join("\n");
|
|
5128
|
+
}
|
|
5129
|
+
|
|
5130
|
+
// src/ai-healing.ts
|
|
5131
|
+
function analyzeAndBuildRepairPrompt(sourceCode) {
|
|
5132
|
+
const report = diagnoseMarkdyCode(sourceCode, { checkArchitecture: true });
|
|
5133
|
+
const syntaxErrors = report.issues.filter((i) => i.severity === "error").map((i) => i.line ? `Line ${i.line}: ${i.message}` : i.message);
|
|
5134
|
+
const archViolations = report.issues.filter((i) => i.code === "ARCH_RULE_VIOLATION").map((i) => ({
|
|
5135
|
+
ruleId: i.code ?? "ARCH_RULE_VIOLATION",
|
|
5136
|
+
ruleName: i.ruleExplanation?.replace("Architecture Rule Preset: ", "") ?? "ArchitectureGovernance",
|
|
5137
|
+
severity: i.severity === "error" ? "error" : "warning",
|
|
5138
|
+
message: i.message,
|
|
5139
|
+
nodeIds: [],
|
|
5140
|
+
edgeKeys: [],
|
|
5141
|
+
line: i.line
|
|
5142
|
+
}));
|
|
4340
5143
|
return {
|
|
4341
|
-
isValid:
|
|
4342
|
-
|
|
5144
|
+
isValid: report.isValid && report.warningCount === 0,
|
|
5145
|
+
repairPrompt: report.repairPrompt,
|
|
5146
|
+
syntaxErrors,
|
|
4343
5147
|
archViolations,
|
|
4344
|
-
|
|
5148
|
+
issues: report.issues,
|
|
5149
|
+
repairedCode: report.repairedCode,
|
|
5150
|
+
report
|
|
4345
5151
|
};
|
|
4346
5152
|
}
|
|
4347
5153
|
|
|
@@ -4418,6 +5224,616 @@ function resolveOutputPreset(name) {
|
|
|
4418
5224
|
function listOutputPresets() {
|
|
4419
5225
|
return Object.keys(OUTPUT_PRESETS);
|
|
4420
5226
|
}
|
|
5227
|
+
|
|
5228
|
+
// src/intellicode.ts
|
|
5229
|
+
var POPULAR_TECHS = [
|
|
5230
|
+
// Databases
|
|
5231
|
+
{ name: "PostgreSQL", aliases: ["postgres", "postgresql", "psql"], kind: "database", defaultId: "Postgres", label: "PostgreSQL 16", desc: "ACID relational SQL database", category: "database" },
|
|
5232
|
+
{ name: "MySQL", aliases: ["mysql", "mariadb"], kind: "database", defaultId: "MySQL", label: "MySQL 8.0", desc: "Relational database", category: "database" },
|
|
5233
|
+
{ name: "MongoDB", aliases: ["mongo", "mongodb"], kind: "database", defaultId: "MongoDB", label: "MongoDB Atlas", desc: "Document NoSQL database", category: "database" },
|
|
5234
|
+
{ name: "DynamoDB", aliases: ["dynamo", "dynamodb"], kind: "database", defaultId: "DynamoDB", label: "AWS DynamoDB", desc: "Serverless key-value NoSQL", category: "database" },
|
|
5235
|
+
{ name: "ClickHouse", aliases: ["clickhouse"], kind: "database", defaultId: "ClickHouse", label: "ClickHouse OLAP", desc: "Real-time columnar analytics", category: "database" },
|
|
5236
|
+
{ name: "Snowflake", aliases: ["snowflake"], kind: "database", defaultId: "Snowflake", label: "Snowflake Data Warehouse", desc: "Cloud data warehouse", category: "database" },
|
|
5237
|
+
{ name: "Neo4j", aliases: ["neo4j", "graphdb"], kind: "database", defaultId: "Neo4j", label: "Neo4j Graph Database", desc: "Property graph engine", category: "database" },
|
|
5238
|
+
{ name: "Pinecone", aliases: ["pinecone", "weaviate", "qdrant", "chroma"], kind: "database", defaultId: "Pinecone", label: "Pinecone Vector DB", desc: "Vector similarity search for AI/RAG", category: "ai" },
|
|
5239
|
+
// Caching
|
|
5240
|
+
{ name: "Redis", aliases: ["redis", "valkey", "dragonfly"], kind: "cache", defaultId: "Redis", label: "Redis Cluster", desc: "In-memory sub-millisecond cache", category: "cache" },
|
|
5241
|
+
{ name: "Memcached", aliases: ["memcached"], kind: "cache", defaultId: "Memcached", label: "Memcached", desc: "Distributed memory object cache", category: "cache" },
|
|
5242
|
+
// Messaging & Streams
|
|
5243
|
+
{ name: "Kafka", aliases: ["kafka", "redpanda", "confluent"], kind: "queue", defaultId: "Kafka", label: "Kafka Event Stream", desc: "High-throughput event bus", category: "queue" },
|
|
5244
|
+
{ name: "RabbitMQ", aliases: ["rabbitmq", "amqp"], kind: "queue", defaultId: "RabbitMQ", label: "RabbitMQ Broker", desc: "AMQP message queue", category: "queue" },
|
|
5245
|
+
{ name: "AWS SQS", aliases: ["sqs"], kind: "queue", defaultId: "SQS", label: "Amazon SQS", desc: "Managed queue service", category: "queue" },
|
|
5246
|
+
{ name: "NATS", aliases: ["nats"], kind: "queue", defaultId: "NATS", label: "NATS JetStream", desc: "Lightweight cloud native pub/sub", category: "queue" },
|
|
5247
|
+
// Ingress & Gateways
|
|
5248
|
+
{ name: "Kong", aliases: ["kong"], kind: "gateway", defaultId: "KongGateway", label: "Kong API Gateway", desc: "Cloud native API gateway", category: "gateway" },
|
|
5249
|
+
{ name: "Envoy", aliases: ["envoy"], kind: "gateway", defaultId: "EnvoyProxy", label: "Envoy Service Proxy", desc: "High-performance edge/service proxy", category: "gateway" },
|
|
5250
|
+
{ name: "Nginx", aliases: ["nginx"], kind: "gateway", defaultId: "Nginx", label: "Nginx Ingress", desc: "Reverse proxy & load balancer", category: "gateway" },
|
|
5251
|
+
{ name: "Traefik", aliases: ["traefik"], kind: "gateway", defaultId: "Traefik", label: "Traefik Proxy", desc: "Dynamic reverse proxy for microservices", category: "gateway" },
|
|
5252
|
+
{ name: "Cloudflare", aliases: ["cloudflare"], kind: "cdn", defaultId: "Cloudflare", label: "Cloudflare Edge / WAF", desc: "Edge CDN & security perimeter", category: "security" },
|
|
5253
|
+
// Clients & Frontends
|
|
5254
|
+
{ 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" },
|
|
5255
|
+
{ 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" },
|
|
5256
|
+
// Compute & Microservices
|
|
5257
|
+
{ name: "API Service", aliases: ["service", "api", "backend", "microservice"], kind: "service", defaultId: "ApiService", label: "API Core Service", desc: "Backend REST/gRPC microservice", category: "compute" },
|
|
5258
|
+
{ name: "Auth Service", aliases: ["auth", "auth0", "keycloak", "clerk", "cognito"], kind: "service", defaultId: "AuthService", label: "Auth & Identity Provider", desc: "OAuth2 / OIDC authentication service", category: "security" },
|
|
5259
|
+
{ name: "Order Service", aliases: ["order", "ordersvc"], kind: "service", defaultId: "OrderService", label: "Order Processing Service", desc: "Transactional order service", category: "compute" },
|
|
5260
|
+
{ name: "Payment Gateway", aliases: ["payment", "stripe", "paypal"], kind: "service", defaultId: "PaymentService", label: "Stripe Payment Gateway", desc: "Payment processing integration", category: "compute" },
|
|
5261
|
+
{ name: "Async Worker", aliases: ["worker", "celery", "sidekiq"], kind: "worker", defaultId: "AsyncWorker", label: "Background Job Worker", desc: "Asynchronous task consumer worker", category: "compute" },
|
|
5262
|
+
{ name: "Serverless Function", aliases: ["lambda", "function", "cloudfunction"], kind: "lambda", defaultId: "LambdaFunc", label: "AWS Lambda / Cloud Function", desc: "Event-driven serverless function", category: "compute" },
|
|
5263
|
+
{ name: "Kubernetes Pod", aliases: ["k8s", "pod", "kubernetes"], kind: "pod", defaultId: "AppPod", label: "Kubernetes Pod Cluster", desc: "Containerized workload pod", category: "compute" },
|
|
5264
|
+
// AI & LLMs
|
|
5265
|
+
{ 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" },
|
|
5266
|
+
// Storage & Backend as a Service
|
|
5267
|
+
{ 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" },
|
|
5268
|
+
{ name: "Supabase", aliases: ["supabase"], kind: "database", defaultId: "SupabaseDB", label: "Supabase PostgreSQL", desc: "Open-source Firebase alternative with Postgres", category: "database" },
|
|
5269
|
+
{ name: "Firebase Firestore", aliases: ["firebase", "firestore"], kind: "database", defaultId: "Firestore", label: "Firebase Firestore", desc: "Serverless real-time document database", category: "database" },
|
|
5270
|
+
// Observability & Telemetry
|
|
5271
|
+
{ name: "Prometheus", aliases: ["prometheus", "metrics"], kind: "metric", defaultId: "Prometheus", label: "Prometheus Metrics", desc: "Time-series metrics monitoring engine", category: "compute" },
|
|
5272
|
+
{ name: "Grafana", aliases: ["grafana", "dashboard"], kind: "surface", defaultId: "GrafanaUI", label: "Grafana Dashboard", desc: "Visualization & telemetry analytics dashboard", category: "compute" },
|
|
5273
|
+
{ name: "OpenSearch / Elasticsearch", aliases: ["opensearch", "elasticsearch", "elastic"], kind: "database", defaultId: "OpenSearch", label: "OpenSearch Cluster", desc: "Distributed search & log analytics engine", category: "database" },
|
|
5274
|
+
{ name: "Sentry", aliases: ["sentry", "errors"], kind: "service", defaultId: "Sentry", label: "Sentry Error Tracker", desc: "Application monitoring and error tracking", category: "compute" }
|
|
5275
|
+
];
|
|
5276
|
+
function extractDiagramContext(text, cursorLine = 0, cursorCol = 0) {
|
|
5277
|
+
const lines = text.split(/\r?\n/);
|
|
5278
|
+
const declaredNodes = [];
|
|
5279
|
+
const declaredGroups = [];
|
|
5280
|
+
const declaredBeats = [];
|
|
5281
|
+
let theme;
|
|
5282
|
+
let layout;
|
|
5283
|
+
let diagramType2;
|
|
5284
|
+
let insideBeat = false;
|
|
5285
|
+
let currentBeatName;
|
|
5286
|
+
let insideGroup = false;
|
|
5287
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5288
|
+
const rawLine = lines[i];
|
|
5289
|
+
const trimmed = rawLine.trim();
|
|
5290
|
+
if (!trimmed || trimmed.startsWith("//")) continue;
|
|
5291
|
+
const themeMatch = /theme\s*=\s*([a-zA-Z0-9_-]+)/.exec(trimmed);
|
|
5292
|
+
if (themeMatch) theme = themeMatch[1];
|
|
5293
|
+
const layoutMatch = /layout\s+([A-Z]{2})/.exec(trimmed);
|
|
5294
|
+
if (layoutMatch) layout = layoutMatch[1];
|
|
5295
|
+
const typeMatch = /type\s*=\s*([a-zA-Z0-9_-]+)/.exec(trimmed);
|
|
5296
|
+
if (typeMatch) diagramType2 = typeMatch[1];
|
|
5297
|
+
const beatMatch = /^beat\s+([\w.-]+)(?:\s+"([^"]*)")?:/i.exec(trimmed);
|
|
5298
|
+
if (beatMatch) {
|
|
5299
|
+
declaredBeats.push({ id: beatMatch[1], label: beatMatch[2] || beatMatch[1], line: i });
|
|
5300
|
+
if (i <= cursorLine) {
|
|
5301
|
+
insideBeat = true;
|
|
5302
|
+
currentBeatName = beatMatch[1];
|
|
5303
|
+
}
|
|
5304
|
+
continue;
|
|
5305
|
+
}
|
|
5306
|
+
if (insideBeat && i === cursorLine && !rawLine.startsWith(" ") && !rawLine.startsWith(" ") && trimmed) {
|
|
5307
|
+
if (!trimmed.startsWith("beat")) {
|
|
5308
|
+
insideBeat = false;
|
|
5309
|
+
currentBeatName = void 0;
|
|
5310
|
+
}
|
|
5311
|
+
}
|
|
5312
|
+
const groupMatch = /^group\s+([\w.-]+)(?:\s+"([^"]*)")?:\s*(.*)$/i.exec(trimmed);
|
|
5313
|
+
if (groupMatch) {
|
|
5314
|
+
const members = groupMatch[3] ? groupMatch[3].trim().split(/\s+/) : [];
|
|
5315
|
+
declaredGroups.push({ id: groupMatch[1], label: groupMatch[2] || groupMatch[1], members, line: i });
|
|
5316
|
+
continue;
|
|
5317
|
+
}
|
|
5318
|
+
const nodeMatch = /^(\w[\w.-]*)\s+(\w[\w.-]*)(?:\s+"([^"]*)")?/i.exec(trimmed);
|
|
5319
|
+
if (nodeMatch) {
|
|
5320
|
+
const kind = nodeMatch[1].toLowerCase();
|
|
5321
|
+
const id = nodeMatch[2];
|
|
5322
|
+
const label = nodeMatch[3] || humanizeId(id);
|
|
5323
|
+
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))) {
|
|
5324
|
+
declaredNodes.push({ id, kind, label, line: i });
|
|
5325
|
+
}
|
|
5326
|
+
}
|
|
5327
|
+
}
|
|
5328
|
+
const currentLineText = lines[cursorLine] ?? "";
|
|
5329
|
+
const linePrefix = currentLineText.slice(0, cursorCol);
|
|
5330
|
+
const tokenMatch = /[\w$.-><~]*$/.exec(linePrefix);
|
|
5331
|
+
const tokenPrefix = tokenMatch ? tokenMatch[0] : "";
|
|
5332
|
+
let cursorInsideBeat = false;
|
|
5333
|
+
for (let i = cursorLine; i >= 0; i--) {
|
|
5334
|
+
const l = lines[i] ?? "";
|
|
5335
|
+
const trimL = l.trim();
|
|
5336
|
+
if (trimL.startsWith("//") || !trimL) continue;
|
|
5337
|
+
if (/^beat\s+[\w.-]+.*:/i.test(trimL)) {
|
|
5338
|
+
cursorInsideBeat = true;
|
|
5339
|
+
break;
|
|
5340
|
+
}
|
|
5341
|
+
if (/^(scene|layout|group|style|pattern|service|database|cache|queue|gateway|browser|mobile|client|worker|storage|cdn|pod|lambda|user)\b/i.test(trimL)) {
|
|
5342
|
+
cursorInsideBeat = false;
|
|
5343
|
+
break;
|
|
5344
|
+
}
|
|
5345
|
+
}
|
|
5346
|
+
return {
|
|
5347
|
+
declaredNodes,
|
|
5348
|
+
declaredGroups,
|
|
5349
|
+
declaredBeats,
|
|
5350
|
+
theme,
|
|
5351
|
+
layout,
|
|
5352
|
+
diagramType: diagramType2,
|
|
5353
|
+
insideBeat: cursorInsideBeat,
|
|
5354
|
+
currentBeatName,
|
|
5355
|
+
insideGroup,
|
|
5356
|
+
lineNo: cursorLine,
|
|
5357
|
+
lineText: currentLineText,
|
|
5358
|
+
linePrefix,
|
|
5359
|
+
tokenPrefix
|
|
5360
|
+
};
|
|
5361
|
+
}
|
|
5362
|
+
function isClientRole(kind) {
|
|
5363
|
+
return nodeRole(kind) === "client";
|
|
5364
|
+
}
|
|
5365
|
+
function isGatewayRole(kind) {
|
|
5366
|
+
const c = canonicalNodeKind(kind);
|
|
5367
|
+
return ["gateway", "api_gateway", "proxy", "load_balancer", "cdn", "firewall", "waf"].includes(c);
|
|
5368
|
+
}
|
|
5369
|
+
function isComputeRole(kind) {
|
|
5370
|
+
return nodeRole(kind) === "compute";
|
|
5371
|
+
}
|
|
5372
|
+
function isDatabaseKind(kind) {
|
|
5373
|
+
const c = canonicalNodeKind(kind);
|
|
5374
|
+
return ["database", "db", "sql", "nosql", "warehouse", "table", "lake"].includes(c);
|
|
5375
|
+
}
|
|
5376
|
+
function isCacheKind(kind) {
|
|
5377
|
+
const c = canonicalNodeKind(kind);
|
|
5378
|
+
return ["cache"].includes(c);
|
|
5379
|
+
}
|
|
5380
|
+
function isQueueRole(kind) {
|
|
5381
|
+
return nodeRole(kind) === "messaging";
|
|
5382
|
+
}
|
|
5383
|
+
function getIntelliCodeCompletions(docText, cursorLine, cursorCol) {
|
|
5384
|
+
const ctx = extractDiagramContext(docText, cursorLine, cursorCol);
|
|
5385
|
+
const items = [];
|
|
5386
|
+
const flowMatch = /([\w.-]+)\s*(->|<-|~>|<->|\.\.>)\s*([\w.-]*)$/.exec(ctx.linePrefix);
|
|
5387
|
+
if (flowMatch) {
|
|
5388
|
+
const sourceNodeId = flowMatch[1];
|
|
5389
|
+
const op = flowMatch[2];
|
|
5390
|
+
const filter = flowMatch[3].toLowerCase();
|
|
5391
|
+
const sourceNode = ctx.declaredNodes.find((n) => n.id === sourceNodeId);
|
|
5392
|
+
const sourceKind = sourceNode ? sourceNode.kind : "service";
|
|
5393
|
+
for (const node of ctx.declaredNodes) {
|
|
5394
|
+
if (node.id === sourceNodeId && op !== "<->") continue;
|
|
5395
|
+
let boost = 2;
|
|
5396
|
+
if (isClientRole(sourceKind)) {
|
|
5397
|
+
if (isGatewayRole(node.kind)) boost = 10;
|
|
5398
|
+
else if (isComputeRole(node.kind)) boost = 8;
|
|
5399
|
+
else boost = 2;
|
|
5400
|
+
} else if (isGatewayRole(sourceKind)) {
|
|
5401
|
+
if (isComputeRole(node.kind)) boost = 10;
|
|
5402
|
+
else boost = 3;
|
|
5403
|
+
} else if (isComputeRole(sourceKind)) {
|
|
5404
|
+
if (isDatabaseKind(node.kind) || isCacheKind(node.kind) || isQueueRole(node.kind)) boost = 10;
|
|
5405
|
+
else if (isComputeRole(node.kind)) boost = 7;
|
|
5406
|
+
else boost = 3;
|
|
5407
|
+
} else if (isQueueRole(sourceKind)) {
|
|
5408
|
+
if (isComputeRole(node.kind)) boost = 10;
|
|
5409
|
+
else boost = 2;
|
|
5410
|
+
}
|
|
5411
|
+
if (op === "<-") {
|
|
5412
|
+
boost = isClientRole(node.kind) || isGatewayRole(node.kind) ? 10 : 3;
|
|
5413
|
+
}
|
|
5414
|
+
items.push({
|
|
5415
|
+
label: node.id,
|
|
5416
|
+
insertText: `${node.id} `,
|
|
5417
|
+
kind: "node",
|
|
5418
|
+
detail: `${node.kind} \xB7 "${node.label}"`,
|
|
5419
|
+
documentation: `Connect flow ${sourceNodeId} ${op} ${node.id}`,
|
|
5420
|
+
boost
|
|
5421
|
+
});
|
|
5422
|
+
}
|
|
5423
|
+
if (!filter || filter.startsWith('"')) {
|
|
5424
|
+
const sourceRole = nodeRole(sourceKind);
|
|
5425
|
+
const sampleLabels = getCommonEdgeLabels(sourceRole, op);
|
|
5426
|
+
for (const lbl of sampleLabels) {
|
|
5427
|
+
items.push({
|
|
5428
|
+
label: `"${lbl}"`,
|
|
5429
|
+
insertText: `"${lbl}"`,
|
|
5430
|
+
kind: "snippet",
|
|
5431
|
+
detail: "Flow description label",
|
|
5432
|
+
boost: 1
|
|
5433
|
+
});
|
|
5434
|
+
}
|
|
5435
|
+
}
|
|
5436
|
+
return items;
|
|
5437
|
+
}
|
|
5438
|
+
const nodeStartMatch = /^\s*([\w.-]+)\s+$/.exec(ctx.linePrefix);
|
|
5439
|
+
if (nodeStartMatch) {
|
|
5440
|
+
const candidateId = nodeStartMatch[1];
|
|
5441
|
+
const isDeclared = ctx.declaredNodes.some((n) => n.id === candidateId);
|
|
5442
|
+
if (isDeclared) {
|
|
5443
|
+
items.push(
|
|
5444
|
+
{ label: "->", insertText: "-> ", kind: "flowOp", detail: "Sync request / call", documentation: "Synchronous HTTP, gRPC, or RPC call.", boost: 10 },
|
|
5445
|
+
{ label: "~>", insertText: "~> ", kind: "flowOp", detail: "Async event / pub-sub", documentation: "Asynchronous event stream or message queue dispatch.", boost: 9 },
|
|
5446
|
+
{ label: "<-", insertText: "<- ", kind: "flowOp", detail: "Response / return payload", documentation: "Synchronous return response payload.", boost: 8 },
|
|
5447
|
+
{ label: "<->", insertText: "<-> ", kind: "flowOp", detail: "Bidirectional streaming / mTLS", documentation: "Two-way streaming WebSocket or mutual TLS socket.", boost: 7 },
|
|
5448
|
+
{ label: "..>", insertText: "..> ", kind: "flowOp", detail: "Weak dependency link", documentation: "Dashed architectural dependency relationship.", boost: 6 }
|
|
5449
|
+
);
|
|
5450
|
+
return items;
|
|
5451
|
+
}
|
|
5452
|
+
}
|
|
5453
|
+
if (/(theme\s*=\s*|theme\s+)\w*$/i.test(ctx.linePrefix)) {
|
|
5454
|
+
const themeEntries = [
|
|
5455
|
+
{ name: "paper", desc: "Warm editorial white canvas with crisp modern typography" },
|
|
5456
|
+
{ name: "midnight", desc: "Deep OLED dark theme with vibrant neon kinetic pulses" },
|
|
5457
|
+
{ name: "blueprint", desc: "Architectural blueprint cyan graph styling" },
|
|
5458
|
+
{ name: "nebula", desc: "Futuristic cosmic purple & indigo glow palette" },
|
|
5459
|
+
{ name: "editorial", desc: "High-contrast serif luxury publication layout" },
|
|
5460
|
+
{ name: "graphite", desc: "Sleek slate monochrome engineering aesthetic" },
|
|
5461
|
+
{ name: "terminal", desc: "Retro phosphor CRT hacker terminal green on black" },
|
|
5462
|
+
{ name: "sketchy", desc: "Hand-drawn sketchy whiteboard marker design" }
|
|
5463
|
+
];
|
|
5464
|
+
for (const t of themeEntries) {
|
|
5465
|
+
items.push({
|
|
5466
|
+
label: t.name,
|
|
5467
|
+
insertText: t.name,
|
|
5468
|
+
kind: "theme",
|
|
5469
|
+
detail: `theme=${t.name}`,
|
|
5470
|
+
documentation: t.desc,
|
|
5471
|
+
boost: 10
|
|
5472
|
+
});
|
|
5473
|
+
}
|
|
5474
|
+
return items;
|
|
5475
|
+
}
|
|
5476
|
+
if (/(layout\s+|direction\s*=\s*)\w*$/i.test(ctx.linePrefix)) {
|
|
5477
|
+
items.push(
|
|
5478
|
+
{ label: "LR", insertText: "LR", kind: "layout", detail: "Left-to-Right", documentation: "Horizontal left-to-right system flow layout.", boost: 10 },
|
|
5479
|
+
{ label: "TB", insertText: "TB", kind: "layout", detail: "Top-to-Bottom", documentation: "Vertical top-to-bottom hierarchy layout.", boost: 9 },
|
|
5480
|
+
{ label: "RL", insertText: "RL", kind: "layout", detail: "Right-to-Left", documentation: "Right-to-left reverse topology layout.", boost: 5 },
|
|
5481
|
+
{ label: "BT", insertText: "BT", kind: "layout", detail: "Bottom-to-Top", documentation: "Bottom-to-top inverted stack layout.", boost: 5 }
|
|
5482
|
+
);
|
|
5483
|
+
return items;
|
|
5484
|
+
}
|
|
5485
|
+
if (/type\s*=\s*\w*$/i.test(ctx.linePrefix)) {
|
|
5486
|
+
for (const t of DIAGRAM_TYPES) {
|
|
5487
|
+
items.push({
|
|
5488
|
+
label: t,
|
|
5489
|
+
insertText: t,
|
|
5490
|
+
kind: "diagramType",
|
|
5491
|
+
detail: `type=${t}`,
|
|
5492
|
+
documentation: `Specialized layout engine: ${t}`,
|
|
5493
|
+
boost: 8
|
|
5494
|
+
});
|
|
5495
|
+
}
|
|
5496
|
+
return items;
|
|
5497
|
+
}
|
|
5498
|
+
const cueMatch = /^\s*(show|glow|pulse|focus|frame|hide)\s+([\w$]*)$/i.exec(ctx.linePrefix);
|
|
5499
|
+
if (cueMatch) {
|
|
5500
|
+
const cueName = cueMatch[1].toLowerCase();
|
|
5501
|
+
items.push(
|
|
5502
|
+
{ label: "$nodes", insertText: "$nodes stagger=60ms", kind: "selector", detail: "All diagram nodes", documentation: "Selects all semantic nodes in the scene.", boost: 10 },
|
|
5503
|
+
{ label: "$title", insertText: "$title", kind: "selector", detail: "Diagram title banner", documentation: "Selects the title node.", boost: 8 },
|
|
5504
|
+
{ label: "$edges", insertText: "$edges", kind: "selector", detail: "All edge connectors", documentation: "Selects all flow connectors.", boost: 7 }
|
|
5505
|
+
);
|
|
5506
|
+
for (const node of ctx.declaredNodes) {
|
|
5507
|
+
items.push({
|
|
5508
|
+
label: node.id,
|
|
5509
|
+
insertText: `${node.id} `,
|
|
5510
|
+
kind: "node",
|
|
5511
|
+
detail: `${node.kind} \xB7 "${node.label}"`,
|
|
5512
|
+
boost: 9
|
|
5513
|
+
});
|
|
5514
|
+
}
|
|
5515
|
+
for (const group of ctx.declaredGroups) {
|
|
5516
|
+
items.push({
|
|
5517
|
+
label: group.id,
|
|
5518
|
+
insertText: cueName === "frame" ? `${group.id} zoom=1.2 dur=600ms` : `${group.id} `,
|
|
5519
|
+
kind: "group",
|
|
5520
|
+
detail: `group "${group.label}"`,
|
|
5521
|
+
documentation: `Group boundary with ${group.members.length} nodes.`,
|
|
5522
|
+
boost: 8
|
|
5523
|
+
});
|
|
5524
|
+
}
|
|
5525
|
+
if (cueName === "show" || cueName === "hide") {
|
|
5526
|
+
items.push({ label: "stagger=60ms", insertText: "stagger=60ms", kind: "attribute", detail: "Stagger delay", boost: 5 });
|
|
5527
|
+
} else if (cueName === "glow" || cueName === "pulse") {
|
|
5528
|
+
items.push(
|
|
5529
|
+
{ label: "color=#38bdf8", insertText: "color=#38bdf8", kind: "attribute", detail: "Sky blue kinetic glow", boost: 5 },
|
|
5530
|
+
{ label: "color=#e11d48", insertText: "color=#e11d48", kind: "attribute", detail: "Alert crimson pulse", boost: 5 },
|
|
5531
|
+
{ label: "color=#10b981", insertText: "color=#10b981", kind: "attribute", detail: "Success emerald glow", boost: 5 }
|
|
5532
|
+
);
|
|
5533
|
+
} else if (cueName === "frame") {
|
|
5534
|
+
items.push(
|
|
5535
|
+
{ label: "zoom=1.2 dur=600ms", insertText: "zoom=1.2 dur=600ms", kind: "attribute", detail: "Camera focus & zoom", boost: 5 }
|
|
5536
|
+
);
|
|
5537
|
+
}
|
|
5538
|
+
return items;
|
|
5539
|
+
}
|
|
5540
|
+
if (ctx.insideBeat) {
|
|
5541
|
+
for (const cue of BEAT_CUE_KEYWORDS) {
|
|
5542
|
+
const aliasTarget = cue === "pulse" ? "focus" : cue === "highlight" ? "glow" : cue;
|
|
5543
|
+
items.push({
|
|
5544
|
+
label: cue,
|
|
5545
|
+
insertText: cue === "show" ? "show $nodes stagger=60ms" : `${cue} `,
|
|
5546
|
+
kind: "cue",
|
|
5547
|
+
detail: `Choreography cue (${aliasTarget})`,
|
|
5548
|
+
documentation: `Execute kinetic choreography action: ${cue}`,
|
|
5549
|
+
boost: 8
|
|
5550
|
+
});
|
|
5551
|
+
}
|
|
5552
|
+
for (const node of ctx.declaredNodes) {
|
|
5553
|
+
items.push({
|
|
5554
|
+
label: node.id,
|
|
5555
|
+
insertText: `${node.id} -> `,
|
|
5556
|
+
kind: "node",
|
|
5557
|
+
detail: `Start flow from ${node.kind} "${node.label}"`,
|
|
5558
|
+
boost: 9
|
|
5559
|
+
});
|
|
5560
|
+
}
|
|
5561
|
+
items.push(
|
|
5562
|
+
{ label: "$nodes", insertText: "$nodes", kind: "selector", detail: "Selector for all nodes", boost: 6 },
|
|
5563
|
+
{ label: "$title", insertText: "$title", kind: "selector", detail: "Diagram title", boost: 5 }
|
|
5564
|
+
);
|
|
5565
|
+
return items;
|
|
5566
|
+
}
|
|
5567
|
+
items.push(
|
|
5568
|
+
{ label: "scene", insertText: "scene theme=paper layout=LR\n", kind: "keyword", detail: "Scene directive", documentation: "Declare scene configuration, theme, and layout direction.", boost: 10 },
|
|
5569
|
+
{ label: "layout LR", insertText: "layout LR\n", kind: "directive", detail: "Horizontal layout", documentation: "Left-to-right topology layout.", boost: 10 },
|
|
5570
|
+
{ label: "layout TB", insertText: "layout TB\n", kind: "directive", detail: "Vertical hierarchy layout", documentation: "Top-to-bottom hierarchy layout.", boost: 9 },
|
|
5571
|
+
{ label: "theme midnight", insertText: "theme=midnight\n", kind: "directive", detail: "Dark theme", documentation: "High-contrast dark mode with neon accents.", boost: 8 },
|
|
5572
|
+
{ label: "theme paper", insertText: "theme=paper\n", kind: "directive", detail: "Editorial light theme", documentation: "Clean light mode with high-legibility typography.", boost: 8 },
|
|
5573
|
+
{ 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 },
|
|
5574
|
+
{ 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 },
|
|
5575
|
+
{ 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 },
|
|
5576
|
+
{ 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 },
|
|
5577
|
+
{ label: "style", insertText: "style ${1:NodeId} fill=${2:#38bdf8} stroke=${3:#0284c7}\n", kind: "snippet", isSnippet: true, detail: "Custom node styling", boost: 5 },
|
|
5578
|
+
{ label: "var", insertText: "var ${1:key} = ${2:value}\n", kind: "snippet", isSnippet: true, detail: "Declare variable", boost: 4 }
|
|
5579
|
+
);
|
|
5580
|
+
const nodeKindsList = [
|
|
5581
|
+
{ kind: "service", id: "ApiService", label: "API Service", desc: "Microservice / REST / gRPC backend" },
|
|
5582
|
+
{ kind: "database", id: "Database", label: "PostgreSQL Database", desc: "Relational or NoSQL database store" },
|
|
5583
|
+
{ kind: "cache", id: "RedisCache", label: "In-Memory Cache", desc: "High-speed in-memory cache" },
|
|
5584
|
+
{ kind: "queue", id: "MessageQueue", label: "Kafka Event Stream", desc: "Pub/Sub message broker or event stream" },
|
|
5585
|
+
{ kind: "gateway", id: "ApiGateway", label: "API Gateway", desc: "Ingress proxy and perimeter router" },
|
|
5586
|
+
{ kind: "browser", id: "WebApp", label: "Web Client", desc: "End-user web browser application" },
|
|
5587
|
+
{ kind: "mobile", id: "MobileApp", label: "Mobile Application", desc: "iOS / Android native mobile app" },
|
|
5588
|
+
{ kind: "worker", id: "BackgroundWorker", label: "Async Task Worker", desc: "Background job execution worker" },
|
|
5589
|
+
{ kind: "storage", id: "ObjectStorage", label: "S3 Object Store", desc: "Blob / document cloud storage" },
|
|
5590
|
+
{ kind: "cdn", id: "EdgeCDN", label: "Cloudflare Edge", desc: "Global content delivery network" },
|
|
5591
|
+
{ kind: "firewall", id: "WAF", label: "Security Perimeter", desc: "Web application firewall & DDoS shield" },
|
|
5592
|
+
{ kind: "lambda", id: "ServerlessFunc", label: "Serverless Function", desc: "Event-driven serverless function" },
|
|
5593
|
+
{ kind: "pod", id: "AppPod", label: "Kubernetes Pod", desc: "Containerized workload pod" },
|
|
5594
|
+
{ kind: "user", id: "Customer", label: "End User", desc: "Human user / customer actor" }
|
|
5595
|
+
];
|
|
5596
|
+
for (const n of nodeKindsList) {
|
|
5597
|
+
items.push({
|
|
5598
|
+
label: n.kind,
|
|
5599
|
+
insertText: `${n.kind} \${1:${n.id}} "\${2:${n.label}}"`,
|
|
5600
|
+
kind: "nodeKind",
|
|
5601
|
+
isSnippet: true,
|
|
5602
|
+
detail: `Node kind \u2192 ${n.kind}`,
|
|
5603
|
+
documentation: n.desc,
|
|
5604
|
+
boost: 7
|
|
5605
|
+
});
|
|
5606
|
+
}
|
|
5607
|
+
for (const vp of VISUAL_PRIMITIVE_TYPES) {
|
|
5608
|
+
items.push({
|
|
5609
|
+
label: vp,
|
|
5610
|
+
insertText: `${vp} \${1:${humanizeId(vp).replace(/\s+/g, "")}} "\${2:${humanizeId(vp)}}"`,
|
|
5611
|
+
kind: "nodeKind",
|
|
5612
|
+
isSnippet: true,
|
|
5613
|
+
detail: `Visual primitive \u2192 ${vp}`,
|
|
5614
|
+
boost: 4
|
|
5615
|
+
});
|
|
5616
|
+
}
|
|
5617
|
+
for (const tech of POPULAR_TECHS) {
|
|
5618
|
+
items.push({
|
|
5619
|
+
label: tech.name,
|
|
5620
|
+
insertText: `${tech.kind} ${tech.defaultId} "${tech.label}"
|
|
5621
|
+
`,
|
|
5622
|
+
kind: "tech",
|
|
5623
|
+
detail: `\u26A1 ${tech.kind} \xB7 ${tech.name}`,
|
|
5624
|
+
documentation: `${tech.desc}
|
|
5625
|
+
|
|
5626
|
+
Inserts semantic ${tech.kind} node definition.`,
|
|
5627
|
+
filterText: `${tech.name} ${tech.aliases.join(" ")} ${tech.kind}`,
|
|
5628
|
+
boost: 6
|
|
5629
|
+
});
|
|
5630
|
+
}
|
|
5631
|
+
for (const node of ctx.declaredNodes) {
|
|
5632
|
+
items.push({
|
|
5633
|
+
label: node.id,
|
|
5634
|
+
insertText: node.id,
|
|
5635
|
+
kind: "node",
|
|
5636
|
+
detail: `${node.kind} \xB7 "${node.label}"`,
|
|
5637
|
+
boost: 5
|
|
5638
|
+
});
|
|
5639
|
+
}
|
|
5640
|
+
return items;
|
|
5641
|
+
}
|
|
5642
|
+
function getCommonEdgeLabels(sourceKind, op) {
|
|
5643
|
+
if (op === "<-") {
|
|
5644
|
+
return ["200 OK", "201 Created", "Cached Response", "Result Payload", "JWT Token"];
|
|
5645
|
+
}
|
|
5646
|
+
if (op === "~>") {
|
|
5647
|
+
return ["order.created", "user.signup", "event.published", "telemetry.metric", "task.enqueue"];
|
|
5648
|
+
}
|
|
5649
|
+
if (op === "<->") {
|
|
5650
|
+
return ["WebSocket / Streaming", "mTLS Bidirectional", "gRPC Bidirectional Stream"];
|
|
5651
|
+
}
|
|
5652
|
+
if (op === "..>") {
|
|
5653
|
+
return ["depends on", "reads schema", "replicates to", "monitors"];
|
|
5654
|
+
}
|
|
5655
|
+
if (isClientRole(sourceKind)) {
|
|
5656
|
+
return ["POST /api/v1/checkout", "GET /api/v1/user", "POST /auth/login", "GraphQL Query"];
|
|
5657
|
+
}
|
|
5658
|
+
if (isGatewayRole(sourceKind)) {
|
|
5659
|
+
return ["Route /v1/orders", "Authorize & Forward", "Proxy Request"];
|
|
5660
|
+
}
|
|
5661
|
+
if (isComputeRole(sourceKind)) {
|
|
5662
|
+
return ["SELECT * FROM orders", "SET cache:key", "GET cache:key", "Publish Event", "Verify Token"];
|
|
5663
|
+
}
|
|
5664
|
+
if (isQueueRole(sourceKind)) {
|
|
5665
|
+
return ["Consume Message", "Process Batch", "Deliver Payload"];
|
|
5666
|
+
}
|
|
5667
|
+
return ["call", "request", "query", "sync"];
|
|
5668
|
+
}
|
|
5669
|
+
function predictNextLineSuggestion(docText, cursorLine) {
|
|
5670
|
+
const ctx = extractDiagramContext(docText, cursorLine, 0);
|
|
5671
|
+
const totalNodes = ctx.declaredNodes.length;
|
|
5672
|
+
if (totalNodes === 0) {
|
|
5673
|
+
return {
|
|
5674
|
+
text: 'browser WebApp "Web Application"',
|
|
5675
|
+
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',
|
|
5676
|
+
description: "Start with an entry client and API Gateway with entrance choreography",
|
|
5677
|
+
type: "next-node"
|
|
5678
|
+
};
|
|
5679
|
+
}
|
|
5680
|
+
if (totalNodes >= 2 && ctx.declaredBeats.length === 0 && !ctx.insideBeat) {
|
|
5681
|
+
const n1 = ctx.declaredNodes[0];
|
|
5682
|
+
const n2 = ctx.declaredNodes[1];
|
|
5683
|
+
return {
|
|
5684
|
+
text: `beat main "System Flow":
|
|
5685
|
+
show $nodes stagger=60ms
|
|
5686
|
+
${n1.id} -> ${n2.id} "call"`,
|
|
5687
|
+
insertText: `beat main "System Flow":
|
|
5688
|
+
show $nodes stagger=60ms
|
|
5689
|
+
${n1.id} -> ${n2.id} "call"
|
|
5690
|
+
`,
|
|
5691
|
+
description: "Initialize choreography with entrance animation and primary flow",
|
|
5692
|
+
type: "init-beat"
|
|
5693
|
+
};
|
|
5694
|
+
}
|
|
5695
|
+
if (ctx.insideBeat) {
|
|
5696
|
+
const lines = docText.split(/\r?\n/);
|
|
5697
|
+
const prevLine = lines[cursorLine - 1]?.trim() ?? "";
|
|
5698
|
+
const reqMatch = /^([\w.-]+)\s*->\s*([\w.-]+)(?:\s+"([^"]*)")?/i.exec(prevLine);
|
|
5699
|
+
if (reqMatch) {
|
|
5700
|
+
const src = reqMatch[1];
|
|
5701
|
+
const tgt = reqMatch[2];
|
|
5702
|
+
return {
|
|
5703
|
+
text: ` ${src} <- ${tgt} "200 OK"`,
|
|
5704
|
+
insertText: ` ${src} <- ${tgt} "200 OK"
|
|
5705
|
+
`,
|
|
5706
|
+
description: `Add response flow returning from ${tgt} to ${src}`,
|
|
5707
|
+
type: "next-flow"
|
|
5708
|
+
};
|
|
5709
|
+
}
|
|
5710
|
+
const eventMatch = /^([\w.-]+)\s*~>\s*([\w.-]+)/i.exec(prevLine);
|
|
5711
|
+
if (eventMatch) {
|
|
5712
|
+
const queueId = eventMatch[2];
|
|
5713
|
+
const worker = ctx.declaredNodes.find((n) => nodeRole(n.kind) === "compute" && n.id !== eventMatch[1]);
|
|
5714
|
+
if (worker) {
|
|
5715
|
+
return {
|
|
5716
|
+
text: ` ${queueId} ~> ${worker.id} "process"`,
|
|
5717
|
+
insertText: ` ${queueId} ~> ${worker.id} "process"
|
|
5718
|
+
`,
|
|
5719
|
+
description: `Deliver queued event from ${queueId} to worker ${worker.id}`,
|
|
5720
|
+
type: "next-flow"
|
|
5721
|
+
};
|
|
5722
|
+
}
|
|
5723
|
+
}
|
|
5724
|
+
const keyNode = ctx.declaredNodes.find((n) => nodeRole(n.kind) === "compute" || nodeRole(n.kind) === "database") || ctx.declaredNodes[0];
|
|
5725
|
+
if (keyNode) {
|
|
5726
|
+
return {
|
|
5727
|
+
text: ` pulse ${keyNode.id} color=#38bdf8`,
|
|
5728
|
+
insertText: ` pulse ${keyNode.id} color=#38bdf8
|
|
5729
|
+
`,
|
|
5730
|
+
description: `Add kinetic highlight pulse to ${keyNode.id}`,
|
|
5731
|
+
type: "beat-cue"
|
|
5732
|
+
};
|
|
5733
|
+
}
|
|
5734
|
+
}
|
|
5735
|
+
const hasClient = ctx.declaredNodes.some((n) => isClientRole(n.kind));
|
|
5736
|
+
const hasCompute = ctx.declaredNodes.some((n) => isComputeRole(n.kind) || isGatewayRole(n.kind));
|
|
5737
|
+
const hasDB = ctx.declaredNodes.some((n) => isDatabaseKind(n.kind));
|
|
5738
|
+
const hasCache = ctx.declaredNodes.some((n) => isCacheKind(n.kind));
|
|
5739
|
+
const hasQueue = ctx.declaredNodes.some((n) => isQueueRole(n.kind));
|
|
5740
|
+
if (hasCompute && !hasDB) {
|
|
5741
|
+
return {
|
|
5742
|
+
text: 'database Postgres "PostgreSQL 16"',
|
|
5743
|
+
insertText: 'database Postgres "PostgreSQL 16"\n',
|
|
5744
|
+
description: "Add a database tier to persist state for your services",
|
|
5745
|
+
type: "next-node"
|
|
5746
|
+
};
|
|
5747
|
+
}
|
|
5748
|
+
if (hasCompute && hasDB && !hasCache) {
|
|
5749
|
+
return {
|
|
5750
|
+
text: 'cache Redis "Redis Cluster"',
|
|
5751
|
+
insertText: 'cache Redis "Redis Cluster"\n',
|
|
5752
|
+
description: "Add an in-memory caching tier to accelerate query latency",
|
|
5753
|
+
type: "next-node"
|
|
5754
|
+
};
|
|
5755
|
+
}
|
|
5756
|
+
if (hasCompute && hasDB && !hasQueue) {
|
|
5757
|
+
return {
|
|
5758
|
+
text: 'queue Kafka "Kafka Event Stream"',
|
|
5759
|
+
insertText: 'queue Kafka "Kafka Event Stream"\nworker AsyncWorker "Background Worker"\n',
|
|
5760
|
+
description: "Add an asynchronous event queue and worker for decoupled processing",
|
|
5761
|
+
type: "next-node"
|
|
5762
|
+
};
|
|
5763
|
+
}
|
|
5764
|
+
return null;
|
|
5765
|
+
}
|
|
5766
|
+
function getArchitectureSuggestions(docText) {
|
|
5767
|
+
const ctx = extractDiagramContext(docText, 0, 0);
|
|
5768
|
+
const recommendations = [];
|
|
5769
|
+
const nodeCount = ctx.declaredNodes.length;
|
|
5770
|
+
if (nodeCount === 0) return recommendations;
|
|
5771
|
+
const hasClient = ctx.declaredNodes.some((n) => isClientRole(n.kind));
|
|
5772
|
+
const hasGateway = ctx.declaredNodes.some((n) => isGatewayRole(n.kind));
|
|
5773
|
+
const hasServices = ctx.declaredNodes.filter((n) => isComputeRole(n.kind));
|
|
5774
|
+
const hasDatabases = ctx.declaredNodes.filter((n) => isDatabaseKind(n.kind));
|
|
5775
|
+
const hasCache = ctx.declaredNodes.some((n) => isCacheKind(n.kind));
|
|
5776
|
+
const hasQueue = ctx.declaredNodes.some((n) => isQueueRole(n.kind));
|
|
5777
|
+
const hasGroups = ctx.declaredGroups.length > 0;
|
|
5778
|
+
const hasBeats = ctx.declaredBeats.length > 0;
|
|
5779
|
+
if (hasClient && (hasServices.length > 0 || hasDatabases.length > 0) && !hasGateway) {
|
|
5780
|
+
recommendations.push({
|
|
5781
|
+
id: "add-gateway",
|
|
5782
|
+
title: "Add API Gateway Perimeter",
|
|
5783
|
+
desc: "Direct client-to-service connections bypass centralized rate-limiting and auth. Insert an API Gateway.",
|
|
5784
|
+
snippet: 'gateway ApiGateway "Kong / Envoy Gateway"\n\nbeat route:\n WebApp -> ApiGateway "HTTPS"\n',
|
|
5785
|
+
category: "security",
|
|
5786
|
+
actionLabel: "Insert Gateway"
|
|
5787
|
+
});
|
|
5788
|
+
}
|
|
5789
|
+
if (hasDatabases.length > 0 && !hasCache) {
|
|
5790
|
+
recommendations.push({
|
|
5791
|
+
id: "add-cache",
|
|
5792
|
+
title: "Add In-Memory Cache (Redis)",
|
|
5793
|
+
desc: "Offload frequent read queries from your database with a sub-millisecond Redis cluster.",
|
|
5794
|
+
snippet: 'cache Redis "Redis Cluster"\n\nbeat cache_layer:\n ApiService -> Redis "cache.get"\n',
|
|
5795
|
+
category: "performance",
|
|
5796
|
+
actionLabel: "Insert Redis Cache"
|
|
5797
|
+
});
|
|
5798
|
+
}
|
|
5799
|
+
if (hasServices.length >= 2 && !hasGroups) {
|
|
5800
|
+
const memberIds = hasServices.map((s) => s.id).join(" ");
|
|
5801
|
+
recommendations.push({
|
|
5802
|
+
id: "add-group-boundary",
|
|
5803
|
+
title: "Enclose Microservices in Group Boundary",
|
|
5804
|
+
desc: "Group related backend microservices inside a secure VPC boundary perimeter.",
|
|
5805
|
+
snippet: `group backend "Backend Microservices VPC": ${memberIds}
|
|
5806
|
+
`,
|
|
5807
|
+
category: "structure",
|
|
5808
|
+
actionLabel: "Add Group Boundary"
|
|
5809
|
+
});
|
|
5810
|
+
}
|
|
5811
|
+
if (!hasBeats && nodeCount >= 2) {
|
|
5812
|
+
const firstTwo = ctx.declaredNodes.slice(0, 2);
|
|
5813
|
+
recommendations.push({
|
|
5814
|
+
id: "add-entrance-beat",
|
|
5815
|
+
title: "Add Animated Entrance Choreography",
|
|
5816
|
+
desc: "Bring your architecture to life with staggered node reveals and motion flows.",
|
|
5817
|
+
snippet: `beat reveal "System Entrance":
|
|
5818
|
+
show $nodes stagger=60ms
|
|
5819
|
+
${firstTwo[0].id} -> ${firstTwo[1].id} "GET /api"
|
|
5820
|
+
`,
|
|
5821
|
+
category: "choreography",
|
|
5822
|
+
actionLabel: "Add Narrative Beat"
|
|
5823
|
+
});
|
|
5824
|
+
}
|
|
5825
|
+
if (hasServices.length >= 2 && !hasQueue) {
|
|
5826
|
+
recommendations.push({
|
|
5827
|
+
id: "add-event-queue",
|
|
5828
|
+
title: "Decouple Services with Event Queue",
|
|
5829
|
+
desc: "Use an asynchronous event stream (Kafka/SQS) for resilient pub/sub event handling.",
|
|
5830
|
+
snippet: 'queue Kafka "Kafka Event Bus"\nworker Worker "Event Consumer"\n\nbeat async_event:\n ApiService ~> Kafka "event.published"\n Kafka ~> Worker "process"\n',
|
|
5831
|
+
category: "reliability",
|
|
5832
|
+
actionLabel: "Add Kafka Queue"
|
|
5833
|
+
});
|
|
5834
|
+
}
|
|
5835
|
+
return recommendations;
|
|
5836
|
+
}
|
|
4421
5837
|
export {
|
|
4422
5838
|
ARCH_RULE_PRESETS,
|
|
4423
5839
|
BEAT_CUE_KEYWORDS,
|
|
@@ -4429,6 +5845,7 @@ export {
|
|
|
4429
5845
|
OUTPUT_PRESETS,
|
|
4430
5846
|
PLAYER_FLAT_KEYS,
|
|
4431
5847
|
PLAYER_GROUPS,
|
|
5848
|
+
POPULAR_TECHS,
|
|
4432
5849
|
ParseError,
|
|
4433
5850
|
SCENE_KEYS,
|
|
4434
5851
|
TECHNICAL_NODE_KINDS,
|
|
@@ -4443,15 +5860,23 @@ export {
|
|
|
4443
5860
|
compilePlan,
|
|
4444
5861
|
compressMarkdyToUrlHash,
|
|
4445
5862
|
computeAdaptiveDimensions,
|
|
5863
|
+
damerauLevenshteinDistance,
|
|
4446
5864
|
decompressMarkdyFromUrlHash,
|
|
5865
|
+
diagnoseMarkdyCode,
|
|
4447
5866
|
diffDiagramASTs,
|
|
5867
|
+
extractDiagramContext,
|
|
5868
|
+
findClosestMatch,
|
|
4448
5869
|
generateThemeFromBrand,
|
|
5870
|
+
getArchitectureSuggestions,
|
|
4449
5871
|
getBoxPortPosition,
|
|
5872
|
+
getIntelliCodeCompletions,
|
|
4450
5873
|
humanizeId,
|
|
4451
5874
|
listOutputPresets,
|
|
4452
5875
|
nodeRole,
|
|
4453
5876
|
parse,
|
|
4454
5877
|
parseAndCompile,
|
|
5878
|
+
predictNextLineSuggestion,
|
|
5879
|
+
repairMarkdyCode,
|
|
4455
5880
|
resolveArchitectureConfig,
|
|
4456
5881
|
resolveOutputPreset,
|
|
4457
5882
|
resolvePlayer,
|