@hugobatist/smartcode 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +292 -0
  3. package/dist/cli.js +4324 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/index.d.ts +374 -0
  6. package/dist/index.js +1167 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/static/annotations-panel.js +133 -0
  9. package/dist/static/annotations-svg.js +108 -0
  10. package/dist/static/annotations.css +367 -0
  11. package/dist/static/annotations.js +367 -0
  12. package/dist/static/app-init.js +497 -0
  13. package/dist/static/breakpoints.css +69 -0
  14. package/dist/static/breakpoints.js +197 -0
  15. package/dist/static/clipboard.js +94 -0
  16. package/dist/static/collapse-ui.js +325 -0
  17. package/dist/static/command-history.js +89 -0
  18. package/dist/static/context-menu.js +334 -0
  19. package/dist/static/custom-renderer.js +201 -0
  20. package/dist/static/dagre-layout.js +291 -0
  21. package/dist/static/diagram-dom.js +241 -0
  22. package/dist/static/diagram-editor.js +368 -0
  23. package/dist/static/editor-panel.js +107 -0
  24. package/dist/static/editor-popovers.js +187 -0
  25. package/dist/static/event-bus.js +57 -0
  26. package/dist/static/export.js +181 -0
  27. package/dist/static/file-tree.js +470 -0
  28. package/dist/static/ghost-paths.js +397 -0
  29. package/dist/static/heatmap.css +116 -0
  30. package/dist/static/heatmap.js +308 -0
  31. package/dist/static/icons.js +66 -0
  32. package/dist/static/inline-edit.js +294 -0
  33. package/dist/static/interaction-state.js +155 -0
  34. package/dist/static/interaction-tracker.js +93 -0
  35. package/dist/static/live.html +239 -0
  36. package/dist/static/main-layout.css +220 -0
  37. package/dist/static/main.css +334 -0
  38. package/dist/static/mcp-sessions.js +202 -0
  39. package/dist/static/modal.css +109 -0
  40. package/dist/static/modal.js +171 -0
  41. package/dist/static/node-drag.js +293 -0
  42. package/dist/static/pan-zoom.js +199 -0
  43. package/dist/static/renderer.js +280 -0
  44. package/dist/static/search.css +103 -0
  45. package/dist/static/search.js +304 -0
  46. package/dist/static/selection.js +353 -0
  47. package/dist/static/session-player.css +137 -0
  48. package/dist/static/session-player.js +411 -0
  49. package/dist/static/sidebar.css +248 -0
  50. package/dist/static/svg-renderer.js +313 -0
  51. package/dist/static/svg-shapes.js +218 -0
  52. package/dist/static/tokens.css +76 -0
  53. package/dist/static/vendor/dagre-bundle.js +43 -0
  54. package/dist/static/vendor/dagre.min.js +3 -0
  55. package/dist/static/vendor/graphlib.min.js +2 -0
  56. package/dist/static/viewport-transform.js +107 -0
  57. package/dist/static/workspace-switcher.js +202 -0
  58. package/dist/static/ws-client.js +71 -0
  59. package/dist/static/ws-handler.js +125 -0
  60. package/package.json +74 -0
package/dist/index.js ADDED
@@ -0,0 +1,1167 @@
1
+ // src/diagram/service.ts
2
+ import { readFile, writeFile, mkdir } from "fs/promises";
3
+ import { dirname } from "path";
4
+
5
+ // src/utils/logger.ts
6
+ import pc from "picocolors";
7
+ var log = {
8
+ info: (...args) => console.error(pc.blue("[smartcode]"), ...args),
9
+ warn: (...args) => console.error(pc.yellow("[smartcode]"), ...args),
10
+ error: (...args) => console.error(pc.red("[smartcode]"), ...args),
11
+ debug: (...args) => {
12
+ if (process.env["DEBUG"]) console.error(pc.dim("[smartcode]"), ...args);
13
+ }
14
+ };
15
+
16
+ // src/diagram/annotations.ts
17
+ var ANNOTATION_START = "%% --- ANNOTATIONS (auto-managed by SmartCode) ---";
18
+ var ANNOTATION_START_LEGACY = "%% --- ANNOTATIONS (auto-managed by SmartB Diagrams) ---";
19
+ var ANNOTATION_END = "%% --- END ANNOTATIONS ---";
20
+ function isAnnotationStart(line) {
21
+ return line === ANNOTATION_START || line === ANNOTATION_START_LEGACY;
22
+ }
23
+ var FLAG_REGEX = /^%%\s*@flag\s+(\S+)\s+"([^"]*)"$/;
24
+ var STATUS_REGEX = /^%%\s*@status\s+(\S+)\s+(\S+)$/;
25
+ var BREAKPOINT_REGEX = /^%%\s*@breakpoint\s+(\S+)$/;
26
+ var RISK_REGEX = /^%%\s*@risk\s+(\S+)\s+(high|medium|low)\s+"([^"]*)"$/;
27
+ var GHOST_REGEX = /^%%\s*@ghost\s+(\S+)\s+(\S+)\s+"([^"]*)"$/;
28
+ var VALID_STATUSES = ["ok", "problem", "in-progress", "discarded"];
29
+ function parseAllAnnotations(content) {
30
+ const flags = /* @__PURE__ */ new Map();
31
+ const statuses = /* @__PURE__ */ new Map();
32
+ const breakpoints = /* @__PURE__ */ new Set();
33
+ const risks = /* @__PURE__ */ new Map();
34
+ const ghosts = [];
35
+ const lines = content.split("\n");
36
+ let inBlock = false;
37
+ for (const line of lines) {
38
+ const trimmed = line.trim();
39
+ if (isAnnotationStart(trimmed)) {
40
+ inBlock = true;
41
+ continue;
42
+ }
43
+ if (trimmed === ANNOTATION_END) {
44
+ inBlock = false;
45
+ continue;
46
+ }
47
+ if (!inBlock || trimmed === "") continue;
48
+ let match = FLAG_REGEX.exec(trimmed);
49
+ if (match) {
50
+ flags.set(match[1], { nodeId: match[1], message: match[2] });
51
+ continue;
52
+ }
53
+ match = STATUS_REGEX.exec(trimmed);
54
+ if (match) {
55
+ const statusValue = match[2];
56
+ if (VALID_STATUSES.includes(statusValue)) {
57
+ statuses.set(match[1], statusValue);
58
+ } else {
59
+ log.debug(`Skipping invalid status value: ${statusValue}`);
60
+ }
61
+ continue;
62
+ }
63
+ match = BREAKPOINT_REGEX.exec(trimmed);
64
+ if (match) {
65
+ breakpoints.add(match[1]);
66
+ continue;
67
+ }
68
+ match = RISK_REGEX.exec(trimmed);
69
+ if (match) {
70
+ risks.set(match[1], { nodeId: match[1], level: match[2], reason: match[3] });
71
+ continue;
72
+ }
73
+ match = GHOST_REGEX.exec(trimmed);
74
+ if (match) {
75
+ ghosts.push({ fromNodeId: match[1], toNodeId: match[2], label: match[3] });
76
+ continue;
77
+ }
78
+ log.debug(`Skipping unrecognized annotation line: ${trimmed}`);
79
+ }
80
+ return { flags, statuses, breakpoints, risks, ghosts };
81
+ }
82
+ function parseFlags(content) {
83
+ return parseAllAnnotations(content).flags;
84
+ }
85
+ function parseStatuses(content) {
86
+ return parseAllAnnotations(content).statuses;
87
+ }
88
+ function stripAnnotations(content) {
89
+ const lines = content.split("\n");
90
+ const result = [];
91
+ let inBlock = false;
92
+ for (const line of lines) {
93
+ const trimmed = line.trim();
94
+ if (isAnnotationStart(trimmed)) {
95
+ inBlock = true;
96
+ continue;
97
+ }
98
+ if (trimmed === ANNOTATION_END) {
99
+ inBlock = false;
100
+ continue;
101
+ }
102
+ if (!inBlock) {
103
+ result.push(line);
104
+ }
105
+ }
106
+ while (result.length > 0 && result[result.length - 1].trim() === "") {
107
+ result.pop();
108
+ }
109
+ return result.join("\n") + "\n";
110
+ }
111
+ function injectAnnotations(content, flags, statuses, breakpoints, risks, ghosts) {
112
+ const clean = stripAnnotations(content);
113
+ const hasFlags = flags.size > 0;
114
+ const hasStatuses = statuses !== void 0 && statuses.size > 0;
115
+ const hasBreakpoints = breakpoints !== void 0 && breakpoints.size > 0;
116
+ const hasRisks = risks !== void 0 && risks.size > 0;
117
+ const hasGhosts = ghosts !== void 0 && ghosts.length > 0;
118
+ if (!hasFlags && !hasStatuses && !hasBreakpoints && !hasRisks && !hasGhosts) {
119
+ return clean;
120
+ }
121
+ const lines = [
122
+ "",
123
+ ANNOTATION_START
124
+ ];
125
+ for (const [nodeId, flag] of flags) {
126
+ const escapedMessage = flag.message.replace(/"/g, "''");
127
+ lines.push(`%% @flag ${nodeId} "${escapedMessage}"`);
128
+ }
129
+ if (hasStatuses) {
130
+ for (const [nodeId, status] of statuses) {
131
+ lines.push(`%% @status ${nodeId} ${status}`);
132
+ }
133
+ }
134
+ if (hasBreakpoints) {
135
+ for (const nodeId of breakpoints) {
136
+ lines.push(`%% @breakpoint ${nodeId}`);
137
+ }
138
+ }
139
+ if (hasRisks) {
140
+ for (const [nodeId, risk] of risks) {
141
+ const escapedReason = risk.reason.replace(/"/g, "''");
142
+ lines.push(`%% @risk ${nodeId} ${risk.level} "${escapedReason}"`);
143
+ }
144
+ }
145
+ if (hasGhosts) {
146
+ for (const ghost of ghosts) {
147
+ const escapedLabel = ghost.label.replace(/"/g, "''");
148
+ lines.push(`%% @ghost ${ghost.fromNodeId} ${ghost.toNodeId} "${escapedLabel}"`);
149
+ }
150
+ }
151
+ lines.push(ANNOTATION_END);
152
+ lines.push("");
153
+ return clean.trimEnd() + "\n" + lines.join("\n");
154
+ }
155
+
156
+ // src/diagram/constants.ts
157
+ var KNOWN_DIAGRAM_TYPES = [
158
+ "flowchart",
159
+ "graph",
160
+ "sequenceDiagram",
161
+ "classDiagram",
162
+ "stateDiagram",
163
+ "erDiagram",
164
+ "gantt",
165
+ "pie",
166
+ "gitgraph",
167
+ "mindmap",
168
+ "timeline"
169
+ ];
170
+ var SUBGRAPH_START = /^\s*subgraph\s+([^\s\[]+)(?:\s*\["([^"]+)"\])?/;
171
+ var SUBGRAPH_END = /^\s*end\s*$/;
172
+
173
+ // src/diagram/parser.ts
174
+ function parseDiagramType(content) {
175
+ const lines = content.split("\n");
176
+ for (const line of lines) {
177
+ const trimmed = line.trim();
178
+ if (trimmed === "" || trimmed.startsWith("%%")) continue;
179
+ for (const diagramType of KNOWN_DIAGRAM_TYPES) {
180
+ if (trimmed === diagramType || trimmed.startsWith(diagramType + " ")) {
181
+ return diagramType;
182
+ }
183
+ }
184
+ return void 0;
185
+ }
186
+ return void 0;
187
+ }
188
+ function parseDiagramContent(rawContent) {
189
+ const mermaidContent = stripAnnotations(rawContent);
190
+ const flags = parseFlags(rawContent);
191
+ const diagramType = parseDiagramType(mermaidContent);
192
+ return { mermaidContent, flags, diagramType };
193
+ }
194
+
195
+ // src/diagram/validator.ts
196
+ function validateMermaidSyntax(content) {
197
+ const errors = [];
198
+ const trimmedContent = content.trim();
199
+ if (trimmedContent === "") {
200
+ return { valid: false, errors: [{ message: "Empty diagram content" }] };
201
+ }
202
+ const diagramType = parseDiagramType(content);
203
+ if (!diagramType) {
204
+ const firstLine = trimmedContent.split("\n")[0]?.trim() ?? "";
205
+ errors.push({
206
+ message: `Unknown diagram type. First line: "${firstLine}". Expected one of: ${KNOWN_DIAGRAM_TYPES.join(", ")}`,
207
+ line: 1
208
+ });
209
+ return { valid: false, errors, diagramType: void 0 };
210
+ }
211
+ const bracketErrors = checkBracketMatching(content);
212
+ errors.push(...bracketErrors);
213
+ const danglingErrors = checkDanglingArrows(content);
214
+ errors.push(...danglingErrors);
215
+ return {
216
+ valid: errors.length === 0,
217
+ errors,
218
+ diagramType
219
+ };
220
+ }
221
+ function checkBracketMatching(content) {
222
+ const errors = [];
223
+ const lines = content.split("\n");
224
+ const pairs = [["[", "]"], ["(", ")"], ["{", "}"]];
225
+ for (let i = 0; i < lines.length; i++) {
226
+ const line = lines[i];
227
+ const lineNum = i + 1;
228
+ if (line.trim().startsWith("%%")) continue;
229
+ for (const [open, close] of pairs) {
230
+ let depth = 0;
231
+ for (const char of line) {
232
+ if (char === open) depth++;
233
+ if (char === close) depth--;
234
+ if (depth < 0) {
235
+ errors.push({
236
+ message: `Unexpected closing '${close}' without matching '${open}'`,
237
+ line: lineNum
238
+ });
239
+ break;
240
+ }
241
+ }
242
+ if (depth > 0) {
243
+ errors.push({
244
+ message: `Unclosed '${open}' -- missing '${close}'`,
245
+ line: lineNum
246
+ });
247
+ }
248
+ }
249
+ }
250
+ return errors;
251
+ }
252
+ function checkDanglingArrows(content) {
253
+ const errors = [];
254
+ const lines = content.split("\n");
255
+ const danglingArrowPattern = /-->\s*$/;
256
+ for (let i = 0; i < lines.length; i++) {
257
+ const line = lines[i];
258
+ const lineNum = i + 1;
259
+ const trimmed = line.trim();
260
+ if (trimmed === "" || trimmed.startsWith("%%")) continue;
261
+ if (danglingArrowPattern.test(trimmed)) {
262
+ errors.push({
263
+ message: `Dangling arrow -- line ends with '-->' but no target node`,
264
+ line: lineNum
265
+ });
266
+ }
267
+ }
268
+ return errors;
269
+ }
270
+
271
+ // src/diagram/graph-types.ts
272
+ var SHAPE_PATTERNS = [
273
+ { open: "([", close: "])", shape: "stadium" },
274
+ { open: "[[", close: "]]", shape: "subroutine" },
275
+ { open: "[(", close: ")]", shape: "cylinder" },
276
+ { open: "((", close: "))", shape: "circle" },
277
+ { open: "{{", close: "}}", shape: "hexagon" },
278
+ { open: "[/", close: "\\]", shape: "trapezoid" },
279
+ { open: "[\\", close: "/]", shape: "trapezoid-alt" },
280
+ { open: "[/", close: "/]", shape: "parallelogram" },
281
+ { open: "[\\", close: "\\]", shape: "parallelogram-alt" },
282
+ { open: ">", close: "]", shape: "asymmetric" },
283
+ { open: "{", close: "}", shape: "diamond" },
284
+ { open: "(", close: ")", shape: "rounded" },
285
+ { open: "[", close: "]", shape: "rect" }
286
+ ];
287
+ var EDGE_SYNTAX = {
288
+ arrow: "-->",
289
+ open: "---",
290
+ dotted: "-.->",
291
+ thick: "==>",
292
+ invisible: "~~~"
293
+ };
294
+
295
+ // src/diagram/graph-edge-parser.ts
296
+ var EDGE_OPS = [
297
+ // Bidirectional variants (must come before unidirectional)
298
+ { pattern: /\s*<==>\s*/, type: "thick", bidirectional: true },
299
+ { pattern: /\s*<-\.->\s*/, type: "dotted", bidirectional: true },
300
+ { pattern: /\s*<-->\s*/, type: "arrow", bidirectional: true },
301
+ // Labeled edges: pipe syntax -->|"label"| or -->|label|
302
+ { pattern: /\s*-->\|"([^"]*)"\|\s*/, type: "arrow", bidirectional: false },
303
+ { pattern: /\s*-->\|([^|]*)\|\s*/, type: "arrow", bidirectional: false },
304
+ // Labeled edges: inline syntax -- "label" -->
305
+ { pattern: /\s*--\s*"([^"]*)"\s*-->\s*/, type: "arrow", bidirectional: false },
306
+ // Unlabeled operators (order: longest first)
307
+ { pattern: /\s*~~~\s*/, type: "invisible", bidirectional: false },
308
+ { pattern: /\s*==>\s*/, type: "thick", bidirectional: false },
309
+ { pattern: /\s*-\.->\s*/, type: "dotted", bidirectional: false },
310
+ { pattern: /\s*---\s*/, type: "open", bidirectional: false },
311
+ { pattern: /\s*-->\s*/, type: "arrow", bidirectional: false }
312
+ ];
313
+ function stripInlineClass(ref) {
314
+ const classIdx = ref.indexOf(":::");
315
+ if (classIdx === -1) return { id: ref };
316
+ return {
317
+ id: ref.substring(0, classIdx),
318
+ className: ref.substring(classIdx + 3)
319
+ };
320
+ }
321
+ function parseNodeShape(definition) {
322
+ const trimmed = definition.trim();
323
+ if (!trimmed) return null;
324
+ const { id: withShape, className } = stripInlineClass(trimmed);
325
+ for (const sp of SHAPE_PATTERNS) {
326
+ const openIdx = withShape.indexOf(sp.open);
327
+ if (openIdx === -1) continue;
328
+ const nodeId = withShape.substring(0, openIdx).trim();
329
+ if (!nodeId || !/^[\w][\w\d_-]*$/.test(nodeId)) continue;
330
+ const afterOpen = withShape.substring(openIdx + sp.open.length);
331
+ if (!afterOpen.endsWith(sp.close)) continue;
332
+ const labelRaw = afterOpen.substring(0, afterOpen.length - sp.close.length);
333
+ let label = labelRaw;
334
+ if (label.startsWith('"') && label.endsWith('"')) {
335
+ label = label.substring(1, label.length - 1);
336
+ }
337
+ return { id: nodeId, label, shape: sp.shape, className };
338
+ }
339
+ return null;
340
+ }
341
+ function extractNodeSegments(line) {
342
+ let work = line;
343
+ for (const op of EDGE_OPS) {
344
+ work = work.replace(op.pattern, " \0 ");
345
+ }
346
+ return work.split("\0").map((s) => s.trim()).filter(Boolean);
347
+ }
348
+ function parseEdgesFromLine(line) {
349
+ const result = [];
350
+ let remaining = line;
351
+ let lastNode = null;
352
+ while (remaining.trim()) {
353
+ let earliest = null;
354
+ for (const op of EDGE_OPS) {
355
+ const match = op.pattern.exec(remaining);
356
+ if (match && (earliest === null || match.index < earliest.index)) {
357
+ earliest = {
358
+ index: match.index,
359
+ matchLen: match[0].length,
360
+ type: op.type,
361
+ bidirectional: op.bidirectional,
362
+ label: match[1]
363
+ };
364
+ }
365
+ }
366
+ if (!earliest) break;
367
+ const beforeOp = remaining.substring(0, earliest.index).trim();
368
+ remaining = remaining.substring(earliest.index + earliest.matchLen);
369
+ if (lastNode === null) {
370
+ lastNode = extractNodeId(beforeOp);
371
+ if (!lastNode) break;
372
+ }
373
+ const afterTrimmed = remaining.trim();
374
+ const nextNodeId = extractNodeId(afterTrimmed, "left");
375
+ if (!nextNodeId) break;
376
+ result.push({
377
+ from: lastNode,
378
+ to: nextNodeId,
379
+ type: earliest.type,
380
+ label: earliest.label,
381
+ bidirectional: earliest.bidirectional
382
+ });
383
+ lastNode = nextNodeId;
384
+ remaining = advancePastNode(remaining.trim(), nextNodeId);
385
+ }
386
+ return result;
387
+ }
388
+ function extractNodeId(text, direction = "right") {
389
+ if (!text.trim()) return null;
390
+ const { id } = stripInlineClass(text.trim());
391
+ const shaped = parseNodeShape(text.trim());
392
+ if (shaped) return shaped.id;
393
+ if (direction === "left") {
394
+ const match2 = /^([\w][\w\d_-]*)/.exec(id);
395
+ return match2 ? match2[1] : null;
396
+ }
397
+ const match = /([\w][\w\d_-]*)$/.exec(id);
398
+ return match ? match[1] : null;
399
+ }
400
+ function advancePastNode(text, nodeId) {
401
+ const { id: cleanText } = stripInlineClass(text);
402
+ for (const sp of SHAPE_PATTERNS) {
403
+ const expectedStart = nodeId + sp.open;
404
+ if (cleanText.startsWith(expectedStart)) {
405
+ const closeIdx = cleanText.indexOf(sp.close, expectedStart.length);
406
+ if (closeIdx !== -1) {
407
+ const afterClose = closeIdx + sp.close.length;
408
+ const afterWithClass = text.substring(afterClose);
409
+ const classMatch = /^:::\S+/.exec(afterWithClass);
410
+ return classMatch ? afterWithClass.substring(classMatch[0].length) : afterWithClass;
411
+ }
412
+ }
413
+ }
414
+ const safeId = regexSafe(nodeId);
415
+ const simplePattern = new RegExp(`^${safeId}(?::::\\S+)?`);
416
+ const simpleMatch = simplePattern.exec(text);
417
+ if (simpleMatch) {
418
+ return text.substring(simpleMatch[0].length);
419
+ }
420
+ return text.substring(nodeId.length);
421
+ }
422
+ function regexSafe(s) {
423
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
424
+ }
425
+
426
+ // src/diagram/graph-parser.ts
427
+ function parseMermaidToGraph(rawContent, filePath) {
428
+ const flags = parseFlags(rawContent);
429
+ const statuses = parseStatuses(rawContent);
430
+ const mermaidContent = stripAnnotations(rawContent);
431
+ const lines = mermaidContent.split("\n");
432
+ let diagramType = "flowchart";
433
+ let direction = "TB";
434
+ for (const line of lines) {
435
+ const trimmed = line.trim();
436
+ if (!trimmed || trimmed.startsWith("%%")) continue;
437
+ const dirMatch = /^(flowchart|graph)\s+(TB|TD|BT|LR|RL)/.exec(trimmed);
438
+ if (dirMatch) {
439
+ diagramType = dirMatch[1];
440
+ const rawDir = dirMatch[2];
441
+ direction = rawDir === "TD" ? "TB" : rawDir;
442
+ }
443
+ break;
444
+ }
445
+ const classDefs = /* @__PURE__ */ new Map();
446
+ const nodeStyles = /* @__PURE__ */ new Map();
447
+ const linkStyles = /* @__PURE__ */ new Map();
448
+ const classAssignments = /* @__PURE__ */ new Map();
449
+ const directiveLineIndices = /* @__PURE__ */ new Set();
450
+ parseStyleDirectives(
451
+ lines,
452
+ classDefs,
453
+ nodeStyles,
454
+ linkStyles,
455
+ classAssignments,
456
+ directiveLineIndices
457
+ );
458
+ const subgraphs = /* @__PURE__ */ new Map();
459
+ const lineToSubgraph = /* @__PURE__ */ new Map();
460
+ const subgraphLineIndices = /* @__PURE__ */ new Set();
461
+ parseSubgraphStructure(lines, subgraphs, lineToSubgraph, subgraphLineIndices);
462
+ const nodes = /* @__PURE__ */ new Map();
463
+ parseNodeDefinitions(
464
+ lines,
465
+ nodes,
466
+ classAssignments,
467
+ lineToSubgraph,
468
+ directiveLineIndices,
469
+ subgraphLineIndices
470
+ );
471
+ const edges = [];
472
+ parseEdgeDefinitions(
473
+ lines,
474
+ edges,
475
+ nodes,
476
+ subgraphs,
477
+ lineToSubgraph,
478
+ directiveLineIndices,
479
+ subgraphLineIndices
480
+ );
481
+ for (const [nodeId, flag] of flags) {
482
+ const node = nodes.get(nodeId);
483
+ if (node) node.flag = flag;
484
+ }
485
+ for (const [nodeId, status] of statuses) {
486
+ const node = nodes.get(nodeId);
487
+ if (node) node.status = status;
488
+ }
489
+ const validation = validateMermaidSyntax(mermaidContent);
490
+ return {
491
+ diagramType,
492
+ direction,
493
+ nodes,
494
+ edges,
495
+ subgraphs,
496
+ classDefs,
497
+ nodeStyles,
498
+ linkStyles,
499
+ classAssignments,
500
+ filePath,
501
+ flags,
502
+ statuses,
503
+ validation
504
+ };
505
+ }
506
+ function parseStyleDirectives(lines, classDefs, nodeStyles, linkStyles, classAssignments, directiveLineIndices) {
507
+ for (let i = 0; i < lines.length; i++) {
508
+ const trimmed = lines[i].trim();
509
+ const classDefMatch = /^classDef\s+(\S+)\s+(.+);?\s*$/.exec(trimmed);
510
+ if (classDefMatch) {
511
+ const name = classDefMatch[1];
512
+ classDefs.set(name, classDefMatch[2].replace(/;\s*$/, ""));
513
+ directiveLineIndices.add(i);
514
+ continue;
515
+ }
516
+ const styleMatch = /^style\s+(\S+)\s+(.+);?\s*$/.exec(trimmed);
517
+ if (styleMatch) {
518
+ nodeStyles.set(styleMatch[1], styleMatch[2].replace(/;\s*$/, ""));
519
+ directiveLineIndices.add(i);
520
+ continue;
521
+ }
522
+ const linkStyleMatch = /^linkStyle\s+(\d+)\s+(.+);?\s*$/.exec(trimmed);
523
+ if (linkStyleMatch) {
524
+ linkStyles.set(
525
+ parseInt(linkStyleMatch[1], 10),
526
+ linkStyleMatch[2].replace(/;\s*$/, "")
527
+ );
528
+ directiveLineIndices.add(i);
529
+ continue;
530
+ }
531
+ const classDirectiveMatch = /^class\s+(.+?)\s+(\S+);?\s*$/.exec(trimmed);
532
+ if (classDirectiveMatch) {
533
+ const className = classDirectiveMatch[2].replace(/;\s*$/, "");
534
+ const nodeIds = classDirectiveMatch[1].split(",").map((s) => s.trim());
535
+ for (const nid of nodeIds) {
536
+ classAssignments.set(nid, className);
537
+ }
538
+ directiveLineIndices.add(i);
539
+ continue;
540
+ }
541
+ }
542
+ }
543
+ function parseSubgraphStructure(lines, subgraphs, lineToSubgraph, subgraphLineIndices) {
544
+ const subgraphStack = [];
545
+ for (let i = 0; i < lines.length; i++) {
546
+ const line = lines[i];
547
+ const startMatch = SUBGRAPH_START.exec(line);
548
+ if (startMatch) {
549
+ const id = startMatch[1];
550
+ const label = startMatch[2] || id;
551
+ const parentId = subgraphStack.length > 0 ? subgraphStack[subgraphStack.length - 1] : null;
552
+ const sg = {
553
+ id,
554
+ label,
555
+ parentId,
556
+ nodeIds: [],
557
+ childSubgraphIds: []
558
+ };
559
+ subgraphs.set(id, sg);
560
+ if (parentId) {
561
+ const parent = subgraphs.get(parentId);
562
+ if (parent) parent.childSubgraphIds.push(id);
563
+ }
564
+ subgraphStack.push(id);
565
+ subgraphLineIndices.add(i);
566
+ continue;
567
+ }
568
+ if (SUBGRAPH_END.test(line) && subgraphStack.length > 0) {
569
+ subgraphStack.pop();
570
+ subgraphLineIndices.add(i);
571
+ continue;
572
+ }
573
+ if (subgraphStack.length > 0) {
574
+ lineToSubgraph.set(i, subgraphStack[subgraphStack.length - 1]);
575
+ }
576
+ }
577
+ }
578
+ var DIRECTION_LINE = /^(flowchart|graph)\s+(TB|TD|BT|LR|RL)/;
579
+ function isSkippableLine(trimmed, lineIdx, directiveIndices, subgraphIndices) {
580
+ if (directiveIndices.has(lineIdx)) return true;
581
+ if (subgraphIndices.has(lineIdx)) return true;
582
+ if (!trimmed || trimmed.startsWith("%%")) return true;
583
+ if (DIRECTION_LINE.test(trimmed)) return true;
584
+ return false;
585
+ }
586
+ function parseNodeDefinitions(lines, nodes, classAssignments, lineToSubgraph, directiveLineIndices, subgraphLineIndices) {
587
+ for (let i = 0; i < lines.length; i++) {
588
+ const trimmed = lines[i].trim();
589
+ if (isSkippableLine(trimmed, i, directiveLineIndices, subgraphLineIndices)) continue;
590
+ const nodeSegments = extractNodeSegments(trimmed);
591
+ for (const segment of nodeSegments) {
592
+ const parsed = parseNodeShape(segment);
593
+ if (parsed && !nodes.has(parsed.id)) {
594
+ const subgraphId = lineToSubgraph.get(i);
595
+ const cssClass = parsed.className || classAssignments.get(parsed.id);
596
+ nodes.set(parsed.id, {
597
+ id: parsed.id,
598
+ label: parsed.label,
599
+ shape: parsed.shape,
600
+ subgraphId,
601
+ cssClass
602
+ });
603
+ if (parsed.className) {
604
+ classAssignments.set(parsed.id, parsed.className);
605
+ }
606
+ } else if (!parsed) {
607
+ const { id: bareId, className } = stripInlineClass(segment.trim());
608
+ if (className && /^[\w][\w\d_-]*$/.test(bareId)) {
609
+ classAssignments.set(bareId, className);
610
+ if (!nodes.has(bareId)) {
611
+ nodes.set(bareId, {
612
+ id: bareId,
613
+ label: bareId,
614
+ shape: "rect",
615
+ subgraphId: lineToSubgraph.get(i),
616
+ cssClass: className
617
+ });
618
+ } else {
619
+ nodes.get(bareId).cssClass = className;
620
+ }
621
+ }
622
+ }
623
+ }
624
+ }
625
+ }
626
+ function parseEdgeDefinitions(lines, edges, nodes, subgraphs, lineToSubgraph, directiveLineIndices, subgraphLineIndices) {
627
+ const edgeIdCounts = /* @__PURE__ */ new Map();
628
+ for (let i = 0; i < lines.length; i++) {
629
+ const trimmed = lines[i].trim();
630
+ if (isSkippableLine(trimmed, i, directiveLineIndices, subgraphLineIndices)) continue;
631
+ const lineEdges = parseEdgesFromLine(trimmed);
632
+ for (const edgeInfo of lineEdges) {
633
+ const { id: fromId } = stripInlineClass(edgeInfo.from);
634
+ const { id: toId } = stripInlineClass(edgeInfo.to);
635
+ ensureNode(nodes, fromId, lineToSubgraph.get(i));
636
+ ensureNode(nodes, toId, lineToSubgraph.get(i));
637
+ const sgId = lineToSubgraph.get(i);
638
+ if (sgId) {
639
+ const sg = subgraphs.get(sgId);
640
+ if (sg) {
641
+ if (!sg.nodeIds.includes(fromId) && !subgraphs.has(fromId)) {
642
+ sg.nodeIds.push(fromId);
643
+ }
644
+ if (!sg.nodeIds.includes(toId) && !subgraphs.has(toId)) {
645
+ sg.nodeIds.push(toId);
646
+ }
647
+ }
648
+ }
649
+ const baseId = `${fromId}->${toId}`;
650
+ const count = edgeIdCounts.get(baseId) || 0;
651
+ const edgeId = count === 0 ? baseId : `${baseId}#${count}`;
652
+ edgeIdCounts.set(baseId, count + 1);
653
+ edges.push({
654
+ id: edgeId,
655
+ from: fromId,
656
+ to: toId,
657
+ type: edgeInfo.type,
658
+ label: edgeInfo.label,
659
+ bidirectional: edgeInfo.bidirectional || void 0
660
+ });
661
+ }
662
+ }
663
+ }
664
+ function ensureNode(nodes, id, subgraphId) {
665
+ if (nodes.has(id)) return;
666
+ nodes.set(id, { id, label: id, shape: "rect", subgraphId });
667
+ }
668
+
669
+ // src/utils/paths.ts
670
+ import path from "path";
671
+ import { existsSync } from "fs";
672
+ import { fileURLToPath } from "url";
673
+ var __dirname = path.dirname(fileURLToPath(import.meta.url));
674
+ function resolveProjectPath(projectRoot, filePath) {
675
+ const resolvedRoot = path.resolve(projectRoot);
676
+ const resolved = path.resolve(resolvedRoot, filePath);
677
+ if (!resolved.startsWith(resolvedRoot + path.sep) && resolved !== resolvedRoot) {
678
+ throw new Error(`Path traversal detected: ${filePath}`);
679
+ }
680
+ return resolved;
681
+ }
682
+
683
+ // src/project/discovery.ts
684
+ import { readdir } from "fs/promises";
685
+ import { join } from "path";
686
+ var EXCLUDED_DIRS = /* @__PURE__ */ new Set([
687
+ "node_modules",
688
+ ".git",
689
+ "test",
690
+ "dist",
691
+ ".planning",
692
+ ".smartcode"
693
+ ]);
694
+ async function discoverMmdFiles(directory) {
695
+ const entries = await readdir(directory, { recursive: true, withFileTypes: true });
696
+ const files = [];
697
+ for (const entry of entries) {
698
+ if (!entry.isFile() || !entry.name.endsWith(".mmd")) continue;
699
+ const parentPath = entry.parentPath ?? "";
700
+ const relativePath = parentPath ? join(parentPath, entry.name).slice(directory.length + 1) : entry.name;
701
+ const segments = relativePath.split("/");
702
+ const excluded = segments.some((seg) => EXCLUDED_DIRS.has(seg));
703
+ if (!excluded) {
704
+ files.push(relativePath);
705
+ }
706
+ }
707
+ return files.sort();
708
+ }
709
+
710
+ // src/diagram/service.ts
711
+ var DiagramService = class {
712
+ constructor(projectRoot) {
713
+ this.projectRoot = projectRoot;
714
+ }
715
+ /** Per-file write locks to serialize concurrent write operations */
716
+ writeLocks = /* @__PURE__ */ new Map();
717
+ /**
718
+ * Serialize write operations on a given file path.
719
+ * Each call waits for the previous write on the same file to finish before running.
720
+ * Cleans up lock entry when no further writes are queued.
721
+ */
722
+ async withWriteLock(filePath, fn) {
723
+ const prev = this.writeLocks.get(filePath) ?? Promise.resolve();
724
+ const current = prev.then(fn, fn);
725
+ const settled = current.then(() => {
726
+ }, () => {
727
+ });
728
+ this.writeLocks.set(filePath, settled);
729
+ const result = await current;
730
+ if (this.writeLocks.get(filePath) === settled) {
731
+ this.writeLocks.delete(filePath);
732
+ }
733
+ return result;
734
+ }
735
+ /**
736
+ * Read a .mmd file and parse all annotations + mermaid content in one pass.
737
+ */
738
+ async readAllAnnotations(filePath) {
739
+ const resolved = this.resolvePath(filePath);
740
+ const raw = await readFile(resolved, "utf-8");
741
+ const { mermaidContent } = parseDiagramContent(raw);
742
+ const { flags, statuses, breakpoints, risks, ghosts } = parseAllAnnotations(raw);
743
+ return { raw, mermaidContent, flags, statuses, breakpoints, risks, ghosts };
744
+ }
745
+ /**
746
+ * Read-modify-write cycle for annotation mutations.
747
+ * Acquires the write lock, reads all annotations, calls modifyFn to mutate them,
748
+ * then writes the result back.
749
+ */
750
+ async modifyAnnotation(filePath, modifyFn) {
751
+ return this.withWriteLock(filePath, async () => {
752
+ const data = await this.readAllAnnotations(filePath);
753
+ modifyFn(data);
754
+ await this._writeDiagramInternal(
755
+ filePath,
756
+ data.mermaidContent,
757
+ data.flags,
758
+ data.statuses,
759
+ data.breakpoints,
760
+ data.risks,
761
+ data.ghosts
762
+ );
763
+ });
764
+ }
765
+ /**
766
+ * Read and parse a .mmd file.
767
+ * Resolves path with traversal protection, parses content, and validates syntax.
768
+ */
769
+ async readDiagram(filePath) {
770
+ const resolved = this.resolvePath(filePath);
771
+ const raw = await readFile(resolved, "utf-8");
772
+ const { mermaidContent, diagramType } = parseDiagramContent(raw);
773
+ const { flags, statuses, breakpoints, risks, ghosts } = parseAllAnnotations(raw);
774
+ const validation = validateMermaidSyntax(mermaidContent);
775
+ if (diagramType && !validation.diagramType) {
776
+ validation.diagramType = diagramType;
777
+ }
778
+ return { raw, mermaidContent, flags, statuses, breakpoints, risks, ghosts, validation, filePath };
779
+ }
780
+ /**
781
+ * Read a .mmd file and parse it into a structured GraphModel.
782
+ */
783
+ async readGraph(filePath) {
784
+ const resolved = this.resolvePath(filePath);
785
+ const raw = await readFile(resolved, "utf-8");
786
+ return parseMermaidToGraph(raw, filePath);
787
+ }
788
+ /**
789
+ * Write a .mmd file. If flags or statuses are provided, injects annotation block.
790
+ * Creates parent directories if they don't exist.
791
+ */
792
+ async writeDiagram(filePath, content, flags, statuses, breakpoints, risks, ghosts) {
793
+ return this.withWriteLock(
794
+ filePath,
795
+ () => this._writeDiagramInternal(filePath, content, flags, statuses, breakpoints, risks, ghosts)
796
+ );
797
+ }
798
+ /**
799
+ * Write diagram content while preserving existing developer-owned annotations (flags, breakpoints).
800
+ * Reads existing annotations first, merges caller-provided statuses/risks/ghosts on top, preserves flags
801
+ * and breakpoints unconditionally, then writes the merged result atomically under the write lock.
802
+ *
803
+ * Merge semantics:
804
+ * - `content`: always replaces the Mermaid diagram body
805
+ * - `statuses`: if provided, replaces all statuses; if undefined, preserves existing
806
+ * - `risks`: if provided, replaces all risks; if undefined, preserves existing
807
+ * - `ghosts`: if provided, replaces all ghosts; if undefined, preserves existing
808
+ * - `flags`: always preserved from the file (developer-owned, never touched by MCP)
809
+ * - `breakpoints`: always preserved from the file (developer-owned, never touched by MCP)
810
+ */
811
+ async writeDiagramPreserving(filePath, content, statuses, risks, ghosts) {
812
+ return this.withWriteLock(filePath, async () => {
813
+ let existingFlags = /* @__PURE__ */ new Map();
814
+ let existingBreakpoints = /* @__PURE__ */ new Set();
815
+ let existingStatuses = /* @__PURE__ */ new Map();
816
+ let existingRisks = /* @__PURE__ */ new Map();
817
+ let existingGhosts = [];
818
+ try {
819
+ const data = await this.readAllAnnotations(filePath);
820
+ existingFlags = data.flags;
821
+ existingBreakpoints = data.breakpoints;
822
+ existingStatuses = data.statuses;
823
+ existingRisks = data.risks;
824
+ existingGhosts = data.ghosts;
825
+ } catch {
826
+ }
827
+ await this._writeDiagramInternal(
828
+ filePath,
829
+ content,
830
+ existingFlags,
831
+ // always preserve
832
+ statuses ?? existingStatuses,
833
+ // replace if provided, else preserve
834
+ existingBreakpoints,
835
+ // always preserve
836
+ risks ?? existingRisks,
837
+ // replace if provided, else preserve
838
+ ghosts ?? existingGhosts
839
+ // replace if provided, else preserve
840
+ );
841
+ });
842
+ }
843
+ /**
844
+ * Write raw content to a .mmd file under the write lock.
845
+ * Does NOT process annotations -- writes content as-is.
846
+ * Used by /save endpoint which receives pre-formatted content from the editor.
847
+ */
848
+ async writeRaw(filePath, content) {
849
+ return this.withWriteLock(filePath, async () => {
850
+ const resolved = this.resolvePath(filePath);
851
+ await mkdir(dirname(resolved), { recursive: true });
852
+ await writeFile(resolved, content, "utf-8");
853
+ });
854
+ }
855
+ /**
856
+ * Internal write without acquiring the lock.
857
+ * Used by methods that already hold the write lock for the same file.
858
+ */
859
+ async _writeDiagramInternal(filePath, content, flags, statuses, breakpoints, risks, ghosts) {
860
+ const resolved = this.resolvePath(filePath);
861
+ let output = content;
862
+ if (flags || statuses || breakpoints || risks || ghosts && ghosts.length > 0) {
863
+ output = injectAnnotations(content, flags ?? /* @__PURE__ */ new Map(), statuses, breakpoints, risks, ghosts);
864
+ }
865
+ await mkdir(dirname(resolved), { recursive: true });
866
+ await writeFile(resolved, output, "utf-8");
867
+ }
868
+ /** Get all flags from a .mmd file as an array. */
869
+ async getFlags(filePath) {
870
+ const diagram = await this.readDiagram(filePath);
871
+ return Array.from(diagram.flags.values());
872
+ }
873
+ /** Set (add or update) a flag on a specific node. */
874
+ async setFlag(filePath, nodeId, message) {
875
+ return this.modifyAnnotation(filePath, (data) => {
876
+ data.flags.set(nodeId, { nodeId, message });
877
+ });
878
+ }
879
+ /** Remove a flag from a specific node. */
880
+ async removeFlag(filePath, nodeId) {
881
+ return this.modifyAnnotation(filePath, (data) => {
882
+ data.flags.delete(nodeId);
883
+ });
884
+ }
885
+ /** Get all statuses from a .mmd file. */
886
+ async getStatuses(filePath) {
887
+ const diagram = await this.readDiagram(filePath);
888
+ return diagram.statuses;
889
+ }
890
+ /** Set (add or update) a status on a specific node. */
891
+ async setStatus(filePath, nodeId, status) {
892
+ return this.modifyAnnotation(filePath, (data) => {
893
+ data.statuses.set(nodeId, status);
894
+ });
895
+ }
896
+ /** Remove a status from a specific node. */
897
+ async removeStatus(filePath, nodeId) {
898
+ return this.modifyAnnotation(filePath, (data) => {
899
+ data.statuses.delete(nodeId);
900
+ });
901
+ }
902
+ /** Get all breakpoints from a .mmd file. */
903
+ async getBreakpoints(filePath) {
904
+ const resolved = this.resolvePath(filePath);
905
+ const raw = await readFile(resolved, "utf-8");
906
+ return parseAllAnnotations(raw).breakpoints;
907
+ }
908
+ /** Set (add) a breakpoint on a specific node. */
909
+ async setBreakpoint(filePath, nodeId) {
910
+ return this.modifyAnnotation(filePath, (data) => {
911
+ data.breakpoints.add(nodeId);
912
+ });
913
+ }
914
+ /** Remove a breakpoint from a specific node. */
915
+ async removeBreakpoint(filePath, nodeId) {
916
+ return this.modifyAnnotation(filePath, (data) => {
917
+ data.breakpoints.delete(nodeId);
918
+ });
919
+ }
920
+ /** Get all risk annotations from a .mmd file. */
921
+ async getRisks(filePath) {
922
+ const resolved = this.resolvePath(filePath);
923
+ const raw = await readFile(resolved, "utf-8");
924
+ return parseAllAnnotations(raw).risks;
925
+ }
926
+ /** Set (add or update) a risk annotation on a specific node. */
927
+ async setRisk(filePath, nodeId, level, reason) {
928
+ return this.modifyAnnotation(filePath, (data) => {
929
+ data.risks.set(nodeId, { nodeId, level, reason });
930
+ });
931
+ }
932
+ /** Remove a risk annotation from a specific node. */
933
+ async removeRisk(filePath, nodeId) {
934
+ return this.modifyAnnotation(filePath, (data) => {
935
+ data.risks.delete(nodeId);
936
+ });
937
+ }
938
+ /** Get all ghost path annotations from a .mmd file. */
939
+ async getGhosts(filePath) {
940
+ const resolved = this.resolvePath(filePath);
941
+ const raw = await readFile(resolved, "utf-8");
942
+ return parseAllAnnotations(raw).ghosts;
943
+ }
944
+ /** Add a ghost path annotation to a .mmd file. */
945
+ async addGhost(filePath, fromNodeId, toNodeId, label) {
946
+ return this.modifyAnnotation(filePath, (data) => {
947
+ data.ghosts.push({ fromNodeId, toNodeId, label });
948
+ });
949
+ }
950
+ /** Remove a specific ghost path annotation (by from+to+label exact match). */
951
+ async removeGhost(filePath, fromNodeId, toNodeId) {
952
+ return this.modifyAnnotation(filePath, (data) => {
953
+ data.ghosts = data.ghosts.filter(
954
+ (g) => !(g.fromNodeId === fromNodeId && g.toNodeId === toNodeId)
955
+ );
956
+ });
957
+ }
958
+ /** Clear all ghost path annotations from a .mmd file. */
959
+ async clearGhosts(filePath) {
960
+ return this.modifyAnnotation(filePath, (data) => {
961
+ data.ghosts = [];
962
+ });
963
+ }
964
+ /** Validate the Mermaid syntax of a .mmd file. */
965
+ async validate(filePath) {
966
+ const diagram = await this.readDiagram(filePath);
967
+ return diagram.validation;
968
+ }
969
+ /** List all .mmd files in the project root. */
970
+ async listFiles() {
971
+ return discoverMmdFiles(this.projectRoot);
972
+ }
973
+ /**
974
+ * Resolve a relative file path against the project root.
975
+ * Single chokepoint for path security -- rejects path traversal.
976
+ */
977
+ resolvePath(filePath) {
978
+ return resolveProjectPath(this.projectRoot, filePath);
979
+ }
980
+ };
981
+
982
+ // src/diagram/graph-serializer.ts
983
+ var SHAPE_BRACKETS = /* @__PURE__ */ new Map();
984
+ for (const sp of SHAPE_PATTERNS) {
985
+ if (!SHAPE_BRACKETS.has(sp.shape)) {
986
+ SHAPE_BRACKETS.set(sp.shape, { open: sp.open, close: sp.close });
987
+ }
988
+ }
989
+ function serializeGraphToMermaid(graph) {
990
+ const lines = [];
991
+ lines.push(`${graph.diagramType} ${graph.direction}`);
992
+ for (const [name, styles] of graph.classDefs) {
993
+ lines.push(` classDef ${name} ${styles}`);
994
+ }
995
+ const emittedNodes = /* @__PURE__ */ new Set();
996
+ const rootSubgraphs = [];
997
+ for (const sg of graph.subgraphs.values()) {
998
+ if (sg.parentId === null) {
999
+ rootSubgraphs.push(sg);
1000
+ }
1001
+ }
1002
+ for (const sg of rootSubgraphs) {
1003
+ emitSubgraph(graph, sg, lines, emittedNodes, 1);
1004
+ }
1005
+ for (const [id, node] of graph.nodes) {
1006
+ if (emittedNodes.has(id)) continue;
1007
+ lines.push(` ${serializeNode(id, node.label, node.shape)}`);
1008
+ }
1009
+ for (const edge of graph.edges) {
1010
+ const arrow = serializeEdgeOperator(edge.type, edge.bidirectional, edge.label);
1011
+ lines.push(` ${edge.from} ${arrow} ${edge.to}`);
1012
+ }
1013
+ for (const [nodeId, styles] of graph.nodeStyles) {
1014
+ lines.push(` style ${nodeId} ${styles}`);
1015
+ }
1016
+ const sortedLinkStyles = [...graph.linkStyles.entries()].sort((a, b) => a[0] - b[0]);
1017
+ for (const [idx, styles] of sortedLinkStyles) {
1018
+ lines.push(` linkStyle ${idx} ${styles}`);
1019
+ }
1020
+ const classGroups = /* @__PURE__ */ new Map();
1021
+ for (const [nodeId, className] of graph.classAssignments) {
1022
+ if (!classGroups.has(className)) {
1023
+ classGroups.set(className, []);
1024
+ }
1025
+ classGroups.get(className).push(nodeId);
1026
+ }
1027
+ for (const [className, nodeIds] of classGroups) {
1028
+ lines.push(` class ${nodeIds.join(",")} ${className}`);
1029
+ }
1030
+ return lines.join("\n") + "\n";
1031
+ }
1032
+ function emitSubgraph(graph, sg, lines, emittedNodes, depth) {
1033
+ const indent = " ".repeat(depth);
1034
+ if (sg.label !== sg.id) {
1035
+ lines.push(`${indent}subgraph ${sg.id}["${sg.label}"]`);
1036
+ } else {
1037
+ lines.push(`${indent}subgraph ${sg.id}`);
1038
+ }
1039
+ for (const childId of sg.childSubgraphIds) {
1040
+ const child = graph.subgraphs.get(childId);
1041
+ if (child) {
1042
+ emitSubgraph(graph, child, lines, emittedNodes, depth + 1);
1043
+ }
1044
+ }
1045
+ for (const nodeId of sg.nodeIds) {
1046
+ const node = graph.nodes.get(nodeId);
1047
+ if (node) {
1048
+ lines.push(`${indent} ${serializeNode(nodeId, node.label, node.shape)}`);
1049
+ emittedNodes.add(nodeId);
1050
+ }
1051
+ }
1052
+ for (const [id, node] of graph.nodes) {
1053
+ if (node.subgraphId === sg.id && !emittedNodes.has(id)) {
1054
+ lines.push(`${indent} ${serializeNode(id, node.label, node.shape)}`);
1055
+ emittedNodes.add(id);
1056
+ }
1057
+ }
1058
+ lines.push(`${indent}end`);
1059
+ }
1060
+ function serializeNode(id, label, shape) {
1061
+ if (label === id && shape === "rect") {
1062
+ return id;
1063
+ }
1064
+ const brackets = SHAPE_BRACKETS.get(shape);
1065
+ if (!brackets) {
1066
+ return `${id}["${label}"]`;
1067
+ }
1068
+ return `${id}${brackets.open}"${label}"${brackets.close}`;
1069
+ }
1070
+ function serializeEdgeOperator(type, bidirectional, label) {
1071
+ const baseSyntax = EDGE_SYNTAX[type] ?? "-->";
1072
+ if (bidirectional) {
1073
+ return `<${baseSyntax}`;
1074
+ }
1075
+ if (label) {
1076
+ return `${baseSyntax}|"${label}"|`;
1077
+ }
1078
+ return baseSyntax;
1079
+ }
1080
+ function serializeGraphModel(graph) {
1081
+ return {
1082
+ diagramType: graph.diagramType,
1083
+ direction: graph.direction,
1084
+ nodes: Object.fromEntries(graph.nodes),
1085
+ edges: graph.edges,
1086
+ subgraphs: Object.fromEntries(graph.subgraphs),
1087
+ classDefs: Object.fromEntries(graph.classDefs),
1088
+ nodeStyles: Object.fromEntries(graph.nodeStyles),
1089
+ linkStyles: Object.fromEntries(graph.linkStyles),
1090
+ classAssignments: Object.fromEntries(graph.classAssignments),
1091
+ filePath: graph.filePath,
1092
+ flags: Object.fromEntries(graph.flags),
1093
+ statuses: Object.fromEntries(graph.statuses)
1094
+ };
1095
+ }
1096
+
1097
+ // src/project/manager.ts
1098
+ import { statSync } from "fs";
1099
+ var ProjectManager = class {
1100
+ projects = /* @__PURE__ */ new Map();
1101
+ /**
1102
+ * Register a project directory and return its DiagramService.
1103
+ * If the directory is already registered, returns the existing service.
1104
+ * Throws if the directory does not exist.
1105
+ */
1106
+ addProject(rootDir) {
1107
+ const existing = this.projects.get(rootDir);
1108
+ if (existing) {
1109
+ return existing;
1110
+ }
1111
+ const stat = statSync(rootDir);
1112
+ if (!stat.isDirectory()) {
1113
+ throw new Error(`Not a directory: ${rootDir}`);
1114
+ }
1115
+ const service = new DiagramService(rootDir);
1116
+ this.projects.set(rootDir, service);
1117
+ return service;
1118
+ }
1119
+ /**
1120
+ * Remove a project from management.
1121
+ * Returns true if it was registered, false otherwise.
1122
+ */
1123
+ removeProject(rootDir) {
1124
+ return this.projects.delete(rootDir);
1125
+ }
1126
+ /**
1127
+ * Get the DiagramService for a registered project directory.
1128
+ * Returns undefined if the directory is not registered.
1129
+ */
1130
+ getProject(rootDir) {
1131
+ return this.projects.get(rootDir);
1132
+ }
1133
+ /**
1134
+ * List all registered project root directories.
1135
+ */
1136
+ listProjects() {
1137
+ return Array.from(this.projects.keys());
1138
+ }
1139
+ /**
1140
+ * Discover .mmd files across all registered projects.
1141
+ * Returns a map of rootDir to file lists.
1142
+ */
1143
+ async discoverAll() {
1144
+ const results = /* @__PURE__ */ new Map();
1145
+ for (const [rootDir, service] of this.projects) {
1146
+ const files = await service.listFiles();
1147
+ results.set(rootDir, files);
1148
+ }
1149
+ return results;
1150
+ }
1151
+ };
1152
+ export {
1153
+ DiagramService,
1154
+ ProjectManager,
1155
+ discoverMmdFiles,
1156
+ injectAnnotations,
1157
+ parseAllAnnotations,
1158
+ parseDiagramContent,
1159
+ parseDiagramType,
1160
+ parseFlags,
1161
+ parseMermaidToGraph,
1162
+ serializeGraphModel,
1163
+ serializeGraphToMermaid,
1164
+ stripAnnotations,
1165
+ validateMermaidSyntax
1166
+ };
1167
+ //# sourceMappingURL=index.js.map