fallback ?? key) as TFunction<
describe('importViewModel', () => {
it('returns translated category labels from the shared definitions', () => {
- expect(getImportCategoryLabel(t, 'sql')).toBe('SQL');
- expect(getImportCategoryLabel(t, 'codebase')).toBe('Repo');
+ expect(getImportCategoryLabel(t, 'infra')).toBe('Infra');
expect(getImportCategoryDefinition('infra').hasNative).toBe(true);
- expect(getImportCategoryDefinition('openapi').hasNative).toBe(false);
expect(IMPORT_CATEGORY_DEFINITIONS.some((definition) => definition.id === 'mermaid')).toBe(
false
);
+ // sql, openapi, codebase are hidden behind feature flags (default off)
+ expect(IMPORT_CATEGORY_DEFINITIONS.some((definition) => definition.id === 'sql')).toBe(false);
+ expect(IMPORT_CATEGORY_DEFINITIONS.some((definition) => definition.id === 'codebase')).toBe(
+ false
+ );
});
it('builds placeholders and options for the import view controls', () => {
@@ -38,7 +41,6 @@ describe('importViewModel', () => {
'terraform-state',
'kubernetes',
'docker-compose',
- 'terraform-hcl',
]);
expect(languageOptions.some((option) => option.value === 'typescript')).toBe(true);
});
diff --git a/src/components/command-bar/importViewModel.ts b/src/components/command-bar/importViewModel.ts
index cc26aaa8..076c346e 100644
--- a/src/components/command-bar/importViewModel.ts
+++ b/src/components/command-bar/importViewModel.ts
@@ -2,6 +2,7 @@ import type { TFunction } from 'i18next';
import type { SelectOption } from '@/components/ui/Select';
import { LANGUAGE_LABELS } from '@/hooks/ai-generation/codeToArchitecture';
import type { ImportCategory } from './importDetection';
+import { ROLLOUT_FLAGS, type RolloutFlagKey } from '@/config/rolloutFlags';
export interface ImportCategoryDefinition {
id: ImportCategory;
@@ -9,15 +10,17 @@ export interface ImportCategoryDefinition {
labelKey: string;
hasNative: boolean;
hasAI: boolean;
+ featureFlag?: RolloutFlagKey;
}
-export const IMPORT_CATEGORY_DEFINITIONS: ImportCategoryDefinition[] = [
+const ALL_IMPORT_CATEGORY_DEFINITIONS: ImportCategoryDefinition[] = [
{
id: 'sql',
fallbackLabel: 'SQL',
labelKey: 'commandBar.import.categories.sql',
hasNative: true,
hasAI: true,
+ featureFlag: 'importSql',
},
{
id: 'infra',
@@ -32,6 +35,7 @@ export const IMPORT_CATEGORY_DEFINITIONS: ImportCategoryDefinition[] = [
labelKey: 'commandBar.import.categories.openapi',
hasNative: false,
hasAI: true,
+ featureFlag: 'importOpenApi',
},
{
id: 'code',
@@ -46,9 +50,15 @@ export const IMPORT_CATEGORY_DEFINITIONS: ImportCategoryDefinition[] = [
labelKey: 'commandBar.import.categories.codebase',
hasNative: true,
hasAI: true,
+ featureFlag: 'importCodebase',
},
];
+export const IMPORT_CATEGORY_DEFINITIONS: ImportCategoryDefinition[] =
+ ALL_IMPORT_CATEGORY_DEFINITIONS.filter(
+ (cat) => !cat.featureFlag || ROLLOUT_FLAGS[cat.featureFlag]
+ );
+
export function createLanguageOptions(): SelectOption[] {
return Object.entries(LANGUAGE_LABELS).map(([value, label]) => ({
value,
@@ -76,7 +86,7 @@ export function getImportPlaceholders(
}
export function getInfraFormatOptions(t: TFunction<'translation', undefined>): SelectOption[] {
- return [
+ const options: SelectOption[] = [
{
value: 'terraform-state',
label: t('commandBar.import.infraFormats.terraformState', 'Terraform State (.tfstate)'),
@@ -89,11 +99,16 @@ export function getInfraFormatOptions(t: TFunction<'translation', undefined>): S
value: 'docker-compose',
label: t('commandBar.import.infraFormats.dockerCompose', 'Docker Compose'),
},
- {
+ ];
+
+ if (ROLLOUT_FLAGS.importInfraTerraformHcl) {
+ options.push({
value: 'terraform-hcl',
label: t('commandBar.import.infraFormats.terraformHcl', 'Terraform HCL (AI)'),
- },
- ];
+ });
+ }
+
+ return options;
}
export function getImportCategoryLabel(
diff --git a/src/components/command-bar/useCommandBarCommands.test.tsx b/src/components/command-bar/useCommandBarCommands.test.tsx
index b2814d1e..cd7dcb58 100644
--- a/src/components/command-bar/useCommandBarCommands.test.tsx
+++ b/src/components/command-bar/useCommandBarCommands.test.tsx
@@ -36,7 +36,6 @@ describe('useCommandBarCommands', () => {
'search-nodes',
'layout',
'architecture-rules',
- 'studio-openflow',
'studio-mermaid',
'toggle-grid',
'toggle-snap',
@@ -50,16 +49,14 @@ describe('useCommandBarCommands', () => {
expect(result.current.find((command) => command.id === 'templates')?.tier).toBe('core');
expect(result.current.find((command) => command.id === 'layout')?.tier).toBe('core');
expect(result.current.find((command) => command.id === 'assets')?.tier).toBe('advanced');
- expect(result.current.find((command) => command.id === 'studio-openflow')?.tier).toBe('advanced');
result.current.find((command) => command.id === 'studio-ai')?.action?.();
result.current.find((command) => command.id === 'architecture-rules')?.action?.();
- result.current.find((command) => command.id === 'studio-openflow')?.action?.();
result.current.find((command) => command.id === 'studio-mermaid')?.action?.();
expect(onOpenStudioAI).toHaveBeenCalledTimes(1);
expect(onOpenArchitectureRules).toHaveBeenCalledTimes(1);
- expect(onOpenStudioOpenFlow).toHaveBeenCalledTimes(1);
+ expect(onOpenStudioOpenFlow).toHaveBeenCalledTimes(0);
expect(onOpenStudioMermaid).toHaveBeenCalledTimes(1);
});
});
diff --git a/src/components/command-bar/useCommandBarCommands.tsx b/src/components/command-bar/useCommandBarCommands.tsx
index 5e61b6e3..bf7e274e 100644
--- a/src/components/command-bar/useCommandBarCommands.tsx
+++ b/src/components/command-bar/useCommandBarCommands.tsx
@@ -3,7 +3,6 @@ import {
ArrowRight,
Code2,
Compass,
- FileCode,
Import,
Search,
Settings,
@@ -12,7 +11,7 @@ import {
Workflow,
} from 'lucide-react';
import { useFlowStore } from '@/store';
-import { APP_NAME, FLOWPILOT_NAME } from '@/lib/brand';
+import { FLOWPILOT_NAME } from '@/lib/brand';
import type { CommandItem, CommandBarProps } from './types';
import { AssetsIcon } from '../icons/AssetsIcon';
@@ -32,7 +31,7 @@ export function useCommandBarCommands({
onUndo,
onRedo,
onOpenStudioAI,
- onOpenStudioOpenFlow,
+ onOpenStudioOpenFlow: _onOpenStudioOpenFlow,
onOpenStudioMermaid,
onOpenArchitectureRules,
hasImport = false,
@@ -136,15 +135,6 @@ export function useCommandBarCommands({
description: 'Open architecture guardrails and rule templates',
action: onOpenArchitectureRules,
},
- {
- id: 'studio-openflow',
- label: 'Edit Flow DSL',
- icon:
,
- tier: 'advanced',
- type: 'action',
- description: `Open ${APP_NAME} DSL in Studio`,
- action: onOpenStudioOpenFlow,
- },
{
id: 'studio-mermaid',
label: 'Edit Mermaid Code',
@@ -194,7 +184,6 @@ export function useCommandBarCommands({
onOpenArchitectureRules,
onOpenStudioAI,
onOpenStudioMermaid,
- onOpenStudioOpenFlow,
onRedo,
onUndo,
settings,
diff --git a/src/components/flow-canvas/flowCanvasTypes.test.ts b/src/components/flow-canvas/flowCanvasTypes.test.ts
index f4591361..c7aa6a74 100644
--- a/src/components/flow-canvas/flowCanvasTypes.test.ts
+++ b/src/components/flow-canvas/flowCanvasTypes.test.ts
@@ -19,6 +19,7 @@ describe('flowCanvasNodeTypes', () => {
"mindmap",
"mobile",
"process",
+ "section",
"sequence_note",
"sequence_participant",
"start",
diff --git a/src/components/flow-canvas/flowCanvasTypes.tsx b/src/components/flow-canvas/flowCanvasTypes.tsx
index 3e0fbad8..6ebf0bbf 100644
--- a/src/components/flow-canvas/flowCanvasTypes.tsx
+++ b/src/components/flow-canvas/flowCanvasTypes.tsx
@@ -20,6 +20,7 @@ import JourneyNode from '@/components/custom-nodes/JourneyNode';
import ArchitectureNode from '@/components/custom-nodes/ArchitectureNode';
import SequenceParticipantNode from '@/components/custom-nodes/SequenceParticipantNode';
import SequenceNoteNode from '@/components/custom-nodes/SequenceNoteNode';
+import SectionNode from '@/components/SectionNode';
export const flowCanvasNodeTypes: NodeTypes = {
start: CustomNode,
@@ -34,6 +35,7 @@ export const flowCanvasNodeTypes: NodeTypes = {
architecture: ArchitectureNode,
annotation: AnnotationNode,
text: TextNode,
+ section: SectionNode,
swimlane: SwimlaneNode,
image: ImageNode,
browser: BrowserNode,
diff --git a/src/components/flow-canvas/useFlowCanvasPaste.ts b/src/components/flow-canvas/useFlowCanvasPaste.ts
index 8479c71b..8f6fb830 100644
--- a/src/components/flow-canvas/useFlowCanvasPaste.ts
+++ b/src/components/flow-canvas/useFlowCanvasPaste.ts
@@ -2,161 +2,182 @@ import { useCallback } from 'react';
import { useFlowStore } from '@/store';
import type { FlowEdge, FlowNode } from '@/lib/types';
import type { MermaidDiagnosticsSnapshot } from '@/store/types';
-import { createPastedTextNode, isEditablePasteTarget, resolveLayoutDirection } from './pasteHelpers';
+import {
+ createPastedTextNode,
+ isEditablePasteTarget,
+ resolveLayoutDirection,
+} from './pasteHelpers';
import { detectMermaidDiagramType } from '@/services/mermaid/detectDiagramType';
import { normalizeParseDiagnostics } from '@/services/mermaid/diagnosticFormatting';
import { parseMermaidByType } from '@/services/mermaid/parseMermaidByType';
+import { enrichNodesWithIcons } from '@/lib/nodeEnricher';
import { assignSmartHandles } from '@/services/smartEdgeRouting';
type SetFlowNodes = (payload: FlowNode[] | ((nodes: FlowNode[]) => FlowNode[])) => void;
type SetFlowEdges = (payload: FlowEdge[] | ((edges: FlowEdge[]) => FlowEdge[])) => void;
-type AddToast = (message: string, type?: 'success' | 'error' | 'info' | 'warning', duration?: number) => void;
+type AddToast = (
+ message: string,
+ type?: 'success' | 'error' | 'info' | 'warning',
+ duration?: number
+) => void;
interface UseFlowCanvasPasteParams {
- architectureStrictMode: boolean;
- activeTabId: string;
- fitView: (options?: { duration?: number; padding?: number }) => void;
- updateTab: (tabId: string, updates: Partial<{ diagramType: string }>) => void;
- recordHistory: () => void;
- setNodes: SetFlowNodes;
- setEdges: SetFlowEdges;
- setSelectedNodeId: (id: string | null) => void;
- setMermaidDiagnostics: (payload: MermaidDiagnosticsSnapshot | null) => void;
- clearMermaidDiagnostics: () => void;
- addToast: AddToast;
- strictModePasteBlockedMessage: string;
- pasteSelection: (center?: { x: number; y: number }) => void;
- getLastInteractionFlowPosition: () => { x: number; y: number } | null;
- getCanvasCenterFlowPosition: () => { x: number; y: number };
+ architectureStrictMode: boolean;
+ activeTabId: string;
+ fitView: (options?: { duration?: number; padding?: number }) => void;
+ updateTab: (tabId: string, updates: Partial<{ diagramType: string }>) => void;
+ recordHistory: () => void;
+ setNodes: SetFlowNodes;
+ setEdges: SetFlowEdges;
+ setSelectedNodeId: (id: string | null) => void;
+ setMermaidDiagnostics: (payload: MermaidDiagnosticsSnapshot | null) => void;
+ clearMermaidDiagnostics: () => void;
+ addToast: AddToast;
+ strictModePasteBlockedMessage: string;
+ pasteSelection: (center?: { x: number; y: number }) => void;
+ getLastInteractionFlowPosition: () => { x: number; y: number } | null;
+ getCanvasCenterFlowPosition: () => { x: number; y: number };
}
export function useFlowCanvasPaste({
- architectureStrictMode,
- activeTabId,
- fitView,
- updateTab,
- recordHistory,
- setNodes,
- setEdges,
- setSelectedNodeId,
- setMermaidDiagnostics,
- clearMermaidDiagnostics,
- addToast,
- strictModePasteBlockedMessage,
- pasteSelection,
- getLastInteractionFlowPosition,
- getCanvasCenterFlowPosition,
+ architectureStrictMode,
+ activeTabId,
+ fitView,
+ updateTab,
+ recordHistory,
+ setNodes,
+ setEdges,
+ setSelectedNodeId,
+ setMermaidDiagnostics,
+ clearMermaidDiagnostics,
+ addToast,
+ strictModePasteBlockedMessage,
+ pasteSelection,
+ getLastInteractionFlowPosition,
+ getCanvasCenterFlowPosition,
}: UseFlowCanvasPasteParams) {
- const handleCanvasPaste = useCallback(async (event: React.ClipboardEvent
): Promise => {
- if (isEditablePasteTarget(event.target)) return;
+ const handleCanvasPaste = useCallback(
+ async (event: React.ClipboardEvent): Promise => {
+ if (isEditablePasteTarget(event.target)) return;
- const rawText = event.clipboardData.getData('text/plain');
- const pastedText = rawText.trim();
+ const rawText = event.clipboardData.getData('text/plain');
+ const pastedText = rawText.trim();
- if (!pastedText) {
- pasteSelection(getLastInteractionFlowPosition() ?? getCanvasCenterFlowPosition());
- return;
- }
+ if (!pastedText) {
+ pasteSelection(getLastInteractionFlowPosition() ?? getCanvasCenterFlowPosition());
+ return;
+ }
- event.preventDefault();
-
- const maybeMermaidType = detectMermaidDiagramType(pastedText);
- if (maybeMermaidType) {
- const result = parseMermaidByType(pastedText, { architectureStrictMode });
- const diagnostics = normalizeParseDiagnostics(result.diagnostics);
-
- if (!result.error) {
- if (diagnostics.length > 0) {
- setMermaidDiagnostics({
- source: 'paste',
- diagramType: result.diagramType,
- diagnostics,
- updatedAt: Date.now(),
- });
- addToast(`Imported with ${diagnostics.length} diagnostic warning(s).`, 'warning');
- } else {
- clearMermaidDiagnostics();
- }
+ event.preventDefault();
- recordHistory();
-
- if (result.nodes.length > 0) {
- try {
- const { getElkLayout } = await import('@/services/elkLayout');
- const layoutDirection = resolveLayoutDirection(result);
- const { nodes: layoutedNodes, edges: layoutedEdges } = await getElkLayout(result.nodes, result.edges, {
- direction: layoutDirection,
- algorithm: 'layered',
- spacing: 'normal',
- });
- const smartEdges = assignSmartHandles(layoutedNodes, layoutedEdges);
- setNodes(layoutedNodes);
- setEdges(smartEdges);
- } catch {
- setNodes(result.nodes);
- setEdges(result.edges);
- }
- } else {
- setNodes(result.nodes);
- setEdges(result.edges);
- }
+ const maybeMermaidType = detectMermaidDiagramType(pastedText);
+ if (maybeMermaidType) {
+ const result = parseMermaidByType(pastedText, { architectureStrictMode });
+ const diagnostics = normalizeParseDiagnostics(result.diagnostics);
- if ('diagramType' in result && result.diagramType) {
- updateTab(activeTabId, { diagramType: result.diagramType });
+ if (!result.error) {
+ if (diagnostics.length > 0) {
+ setMermaidDiagnostics({
+ source: 'paste',
+ diagramType: result.diagramType,
+ diagnostics,
+ updatedAt: Date.now(),
+ });
+ addToast(`Imported with ${diagnostics.length} diagnostic warning(s).`, 'warning');
+ } else {
+ clearMermaidDiagnostics();
+ }
+
+ recordHistory();
+
+ if (result.nodes.length > 0) {
+ const enrichedNodes = await enrichNodesWithIcons(result.nodes);
+ try {
+ const { getElkLayout, clearLayoutCache } = await import('@/services/elkLayout');
+ clearLayoutCache();
+ const layoutDirection = resolveLayoutDirection(result);
+ const { nodes: layoutedNodes, edges: layoutedEdges } = await getElkLayout(
+ enrichedNodes,
+ result.edges,
+ {
+ direction: layoutDirection,
+ algorithm: 'layered',
+ spacing: 'normal',
}
-
- window.setTimeout(() => fitView({ duration: 600, padding: 0.2 }), 80);
- return;
+ );
+ const smartEdges = assignSmartHandles(layoutedNodes, layoutedEdges);
+ setNodes(layoutedNodes);
+ setEdges(smartEdges);
+ } catch {
+ setNodes(enrichedNodes);
+ setEdges(result.edges);
}
+ } else {
+ setNodes(result.nodes);
+ setEdges(result.edges);
+ }
- setMermaidDiagnostics({
- source: 'paste',
- diagramType: result.diagramType ?? maybeMermaidType,
- diagnostics,
- error: result.error,
- updatedAt: Date.now(),
- });
+ if ('diagramType' in result && result.diagramType) {
+ updateTab(activeTabId, { diagramType: result.diagramType });
+ }
- if (maybeMermaidType === 'architecture' && architectureStrictMode && result.error.includes('strict mode rejected')) {
- addToast(strictModePasteBlockedMessage, 'error');
- return;
- }
+ window.setTimeout(() => fitView({ duration: 600, padding: 0.2 }), 80);
+ return;
+ }
- addToast(result.error, 'error');
- return;
+ setMermaidDiagnostics({
+ source: 'paste',
+ diagramType: result.diagramType ?? maybeMermaidType,
+ diagnostics,
+ error: result.error,
+ updatedAt: Date.now(),
+ });
+
+ if (
+ maybeMermaidType === 'architecture' &&
+ architectureStrictMode &&
+ result.error.includes('strict mode rejected')
+ ) {
+ addToast(strictModePasteBlockedMessage, 'error');
+ return;
}
- const pasteFlowPosition =
- getLastInteractionFlowPosition() ?? getCanvasCenterFlowPosition();
-
- recordHistory();
- const { activeLayerId } = useFlowStore.getState();
- const newTextNode = createPastedTextNode(pastedText, pasteFlowPosition, activeLayerId);
-
- setNodes((existingNodes) => [
- ...existingNodes.map((node) => ({ ...node, selected: false })),
- { ...newTextNode, selected: true },
- ]);
- setSelectedNodeId(newTextNode.id);
- }, [
- activeTabId,
- addToast,
- architectureStrictMode,
- clearMermaidDiagnostics,
- fitView,
- getCanvasCenterFlowPosition,
- pasteSelection,
- getLastInteractionFlowPosition,
- recordHistory,
- setEdges,
- setMermaidDiagnostics,
- setNodes,
- setSelectedNodeId,
- strictModePasteBlockedMessage,
- updateTab,
- ]);
-
- return {
- handleCanvasPaste,
- };
+ addToast(result.error, 'error');
+ return;
+ }
+
+ const pasteFlowPosition = getLastInteractionFlowPosition() ?? getCanvasCenterFlowPosition();
+
+ recordHistory();
+ const { activeLayerId } = useFlowStore.getState();
+ const newTextNode = createPastedTextNode(pastedText, pasteFlowPosition, activeLayerId);
+
+ setNodes((existingNodes) => [
+ ...existingNodes.map((node) => ({ ...node, selected: false })),
+ { ...newTextNode, selected: true },
+ ]);
+ setSelectedNodeId(newTextNode.id);
+ },
+ [
+ activeTabId,
+ addToast,
+ architectureStrictMode,
+ clearMermaidDiagnostics,
+ fitView,
+ getCanvasCenterFlowPosition,
+ pasteSelection,
+ getLastInteractionFlowPosition,
+ recordHistory,
+ setEdges,
+ setMermaidDiagnostics,
+ setNodes,
+ setSelectedNodeId,
+ strictModePasteBlockedMessage,
+ updateTab,
+ ]
+ );
+
+ return {
+ handleCanvasPaste,
+ };
}
diff --git a/src/config/rolloutFlags.ts b/src/config/rolloutFlags.ts
index dd5b2c1a..712388b9 100644
--- a/src/config/rolloutFlags.ts
+++ b/src/config/rolloutFlags.ts
@@ -1,65 +1,97 @@
export type RolloutFlagKey =
- | 'relationSemanticsV1'
- | 'documentModelV2'
- | 'collaborationEnabled'
- | 'architectureLintEnabled';
+ | 'relationSemanticsV1'
+ | 'documentModelV2'
+ | 'collaborationEnabled'
+ | 'architectureLintEnabled'
+ | 'importSql'
+ | 'importOpenApi'
+ | 'importInfraTerraformHcl'
+ | 'importCodebase';
interface RolloutFlagDefinition {
- key: RolloutFlagKey;
- envVar: string;
- defaultEnabled: boolean;
- description: string;
+ key: RolloutFlagKey;
+ envVar: string;
+ defaultEnabled: boolean;
+ description: string;
}
const ROLLOUT_FLAG_DEFINITIONS: Record = {
- relationSemanticsV1: {
- key: 'relationSemanticsV1',
- envVar: 'VITE_RELATION_SEMANTICS_V1',
- defaultEnabled: false,
- description: 'Class/ER relation marker and routing semantics rollout',
- },
- documentModelV2: {
- key: 'documentModelV2',
- envVar: 'VITE_DOCUMENT_MODEL_V2',
- defaultEnabled: false,
- description: 'Extended document metadata for scenes, exports, and bindings',
- },
- collaborationEnabled: {
- key: 'collaborationEnabled',
- envVar: 'VITE_COLLABORATION_ENABLED',
- defaultEnabled: true,
- description: 'WebRTC peer collaboration (beta)',
- },
- architectureLintEnabled: {
- key: 'architectureLintEnabled',
- envVar: 'VITE_ARCHITECTURE_LINT_ENABLED',
- defaultEnabled: true,
- description: 'Architecture diagram lint rules panel',
- },
+ relationSemanticsV1: {
+ key: 'relationSemanticsV1',
+ envVar: 'VITE_RELATION_SEMANTICS_V1',
+ defaultEnabled: false,
+ description: 'Class/ER relation marker and routing semantics rollout',
+ },
+ documentModelV2: {
+ key: 'documentModelV2',
+ envVar: 'VITE_DOCUMENT_MODEL_V2',
+ defaultEnabled: false,
+ description: 'Extended document metadata for scenes, exports, and bindings',
+ },
+ collaborationEnabled: {
+ key: 'collaborationEnabled',
+ envVar: 'VITE_COLLABORATION_ENABLED',
+ defaultEnabled: true,
+ description: 'WebRTC peer collaboration (beta)',
+ },
+ architectureLintEnabled: {
+ key: 'architectureLintEnabled',
+ envVar: 'VITE_ARCHITECTURE_LINT_ENABLED',
+ defaultEnabled: true,
+ description: 'Architecture diagram lint rules panel',
+ },
+ importSql: {
+ key: 'importSql',
+ envVar: 'VITE_IMPORT_SQL',
+ defaultEnabled: false,
+ description: 'SQL DDL importer (hidden โ unreliable for complex schemas)',
+ },
+ importOpenApi: {
+ key: 'importOpenApi',
+ envVar: 'VITE_IMPORT_OPENAPI',
+ defaultEnabled: false,
+ description: 'OpenAPI/Swagger importer (hidden โ JSON-only, no YAML)',
+ },
+ importInfraTerraformHcl: {
+ key: 'importInfraTerraformHcl',
+ envVar: 'VITE_IMPORT_INFRA_TERRAFORM_HCL',
+ defaultEnabled: false,
+ description: 'Terraform HCL importer (hidden โ AI-only, hallucination-prone)',
+ },
+ importCodebase: {
+ key: 'importCodebase',
+ envVar: 'VITE_IMPORT_CODEBASE',
+ defaultEnabled: false,
+ description: 'Repo/codebase analyzer importer (hidden โ niche, heavy)',
+ },
};
function readBooleanEnvFlag(envValue: string | undefined, defaultEnabled: boolean): boolean {
- if (envValue === '1') {
- return true;
- }
- if (envValue === '0') {
- return false;
- }
- return defaultEnabled;
+ if (envValue === '1') {
+ return true;
+ }
+ if (envValue === '0') {
+ return false;
+ }
+ return defaultEnabled;
}
export function isRolloutFlagEnabled(key: RolloutFlagKey): boolean {
- const definition = ROLLOUT_FLAG_DEFINITIONS[key];
- if (!definition.envVar) {
- return definition.defaultEnabled;
- }
- const envValue = import.meta.env[definition.envVar as keyof ImportMetaEnv] as string | undefined;
- return readBooleanEnvFlag(envValue, definition.defaultEnabled);
+ const definition = ROLLOUT_FLAG_DEFINITIONS[key];
+ if (!definition.envVar) {
+ return definition.defaultEnabled;
+ }
+ const envValue = import.meta.env[definition.envVar as keyof ImportMetaEnv] as string | undefined;
+ return readBooleanEnvFlag(envValue, definition.defaultEnabled);
}
export const ROLLOUT_FLAGS: Record = {
- relationSemanticsV1: isRolloutFlagEnabled('relationSemanticsV1'),
- documentModelV2: isRolloutFlagEnabled('documentModelV2'),
- collaborationEnabled: isRolloutFlagEnabled('collaborationEnabled'),
- architectureLintEnabled: isRolloutFlagEnabled('architectureLintEnabled'),
+ relationSemanticsV1: isRolloutFlagEnabled('relationSemanticsV1'),
+ documentModelV2: isRolloutFlagEnabled('documentModelV2'),
+ collaborationEnabled: isRolloutFlagEnabled('collaborationEnabled'),
+ architectureLintEnabled: isRolloutFlagEnabled('architectureLintEnabled'),
+ importSql: isRolloutFlagEnabled('importSql'),
+ importOpenApi: isRolloutFlagEnabled('importOpenApi'),
+ importInfraTerraformHcl: isRolloutFlagEnabled('importInfraTerraformHcl'),
+ importCodebase: isRolloutFlagEnabled('importCodebase'),
};
diff --git a/src/hooks/ai-generation/requestLifecycle.ts b/src/hooks/ai-generation/requestLifecycle.ts
index 3a6cab81..e374c271 100644
--- a/src/hooks/ai-generation/requestLifecycle.ts
+++ b/src/hooks/ai-generation/requestLifecycle.ts
@@ -3,13 +3,13 @@ import { serializeCanvasContextForAI } from '@/services/ai/contextSerializer';
import { generateDiagramFromChat, type ChatMessage } from '@/services/aiService';
import type { FlowEdge, FlowNode, GlobalEdgeOptions } from '@/lib/types';
import type { AISettings } from '@/store/types';
+import { buildIdMap, parseDslOrThrow, toFinalEdges, toFinalNodes } from './graphComposer';
import {
- buildIdMap,
- parseDslOrThrow,
- toFinalEdges,
- toFinalNodes,
-} from './graphComposer';
-import { applyAIResultToCanvas, positionNewNodesSmartly, restoreExistingPositions } from './positionPreservingApply';
+ applyAIResultToCanvas,
+ positionNewNodesSmartly,
+ restoreExistingPositions,
+} from './positionPreservingApply';
+import { enrichNodesWithIcons } from '@/lib/nodeEnricher';
interface GenerateAIFlowResultParams {
chatMessages: ChatMessage[];
@@ -34,7 +34,12 @@ function isRetryableError(error: unknown): boolean {
if (error instanceof Error) {
const msg = error.message.toLowerCase();
// Retry on rate-limit and network errors, not on auth or parse errors
- return msg.includes('429') || msg.includes('rate') || msg.includes('network') || msg.includes('fetch');
+ return (
+ msg.includes('429') ||
+ msg.includes('rate') ||
+ msg.includes('network') ||
+ msg.includes('fetch')
+ );
}
return false;
}
@@ -42,7 +47,7 @@ function isRetryableError(error: unknown): boolean {
async function withRetry(
fn: () => Promise,
signal: AbortSignal | undefined,
- onRetry?: (attempt: number) => void,
+ onRetry?: (attempt: number) => void
): Promise {
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');
@@ -69,9 +74,11 @@ export interface GenerateAIFlowResult {
export function buildUserChatMessage(prompt: string, imageBase64?: string): ChatMessage {
return {
role: 'user',
- parts: [{
- text: imageBase64 ? `${prompt} [Image Attached]` : prompt,
- }],
+ parts: [
+ {
+ text: imageBase64 ? `${prompt} [Image Attached]` : prompt,
+ },
+ ],
};
}
@@ -82,11 +89,7 @@ export function appendChatExchange(
editMode = false
): ChatMessage[] {
const modelText = editMode ? '[Diagram updated]' : dslText;
- return [
- ...previousMessages,
- userMessage,
- { role: 'model', parts: [{ text: modelText }] },
- ];
+ return [...previousMessages, userMessage, { role: 'model', parts: [{ text: modelText }] }];
}
function buildSelectionPromptSuffix(selectedNodeIds: string[], nodes: FlowNode[]): string {
@@ -128,22 +131,23 @@ export async function generateAIFlowResult({
for (let attempt = 0; attempt <= 1; attempt++) {
dslText = await withRetry(
- () => generateDiagramFromChat(
- chatMessages,
- activePrompt,
- currentGraph,
- imageBase64,
- aiSettings.apiKey,
- aiSettings.model,
- aiSettings.provider || 'gemini',
- aiSettings.customBaseUrl,
- isEditMode,
- onChunk,
- signal,
- aiSettings.temperature,
- ),
+ () =>
+ generateDiagramFromChat(
+ chatMessages,
+ activePrompt,
+ currentGraph,
+ imageBase64,
+ aiSettings.apiKey,
+ aiSettings.model,
+ aiSettings.provider || 'gemini',
+ aiSettings.customBaseUrl,
+ isEditMode,
+ onChunk,
+ signal,
+ aiSettings.temperature
+ ),
signal,
- onRetry,
+ onRetry
);
try {
parsed = parseDslOrThrow(dslText);
@@ -157,7 +161,7 @@ export async function generateAIFlowResult({
}
parsed = parsed!;
const idMap = buildIdMap(parsed.nodes, nodes);
- const finalNodes = toFinalNodes(parsed.nodes, idMap);
+ const finalNodes = await enrichNodesWithIcons(toFinalNodes(parsed.nodes, idMap));
const finalEdges = toFinalEdges(parsed.edges, idMap, globalEdgeOptions);
const isEmptyCanvas = nodes.length === 0;
@@ -167,7 +171,12 @@ export async function generateAIFlowResult({
finalEdges,
{ direction: 'TB', algorithm: 'mrtree', spacing: 'loose' }
);
- return { dslText, userMessage: buildUserChatMessage(prompt, imageBase64), layoutedNodes, layoutedEdges };
+ return {
+ dslText,
+ userMessage: buildUserChatMessage(prompt, imageBase64),
+ layoutedNodes,
+ layoutedEdges,
+ };
}
// Position-preserving apply: matched nodes keep their positions, new nodes get ELK positions
@@ -188,7 +197,12 @@ export async function generateAIFlowResult({
}
// Smart placement: position new nodes near their existing neighbors
- const smartPositioned = positionNewNodesSmartly(mergedNodes, mergedEdges, newNodeIds, existingById);
+ const smartPositioned = positionNewNodesSmartly(
+ mergedNodes,
+ mergedEdges,
+ newNodeIds,
+ existingById
+ );
const unplacedIds = [...newNodeIds].filter((id) => {
const node = smartPositioned.find((n) => n.id === id);
return !node?.position || (node.position.x === 0 && node.position.y === 0);
diff --git a/src/hooks/useFlowEditorCallbacks.ts b/src/hooks/useFlowEditorCallbacks.ts
index def03d94..7f462b23 100644
--- a/src/hooks/useFlowEditorCallbacks.ts
+++ b/src/hooks/useFlowEditorCallbacks.ts
@@ -1,155 +1,185 @@
import { startTransition, useCallback, useRef } from 'react';
import type { FlowEdge, FlowNode, FlowSnapshot } from '@/lib/types';
+import { enrichNodesWithIcons } from '@/lib/nodeEnricher';
import { useFlowStore } from '@/store';
import { composeDiagramForDisplay } from '@/services/composeDiagramForDisplay';
interface UseFlowEditorCallbacksParams {
- addPage: () => string;
- closePage: (pageId: string) => void;
- reorderPage: (draggedPageId: string, targetPageId: string) => void;
- updatePage: (pageId: string, update: Partial<{ name: string }>) => void;
- navigate: (path: string) => void;
- pagesLength: number;
- cannotCloseLastTabMessage: string;
- setNodes: (nodes: FlowNode[] | ((nodes: FlowNode[]) => FlowNode[])) => void;
- setEdges: (edges: FlowEdge[] | ((edges: FlowEdge[]) => FlowEdge[])) => void;
- restoreSnapshot: (snapshot: FlowSnapshot, setNodes: UseFlowEditorCallbacksParams['setNodes'], setEdges: UseFlowEditorCallbacksParams['setEdges']) => void;
- recordHistory: () => void;
- fitView: (options?: { duration?: number; padding?: number }) => void;
- screenToFlowPosition: (position: { x: number; y: number }) => { x: number; y: number };
+ addPage: () => string;
+ closePage: (pageId: string) => void;
+ reorderPage: (draggedPageId: string, targetPageId: string) => void;
+ updatePage: (pageId: string, update: Partial<{ name: string }>) => void;
+ navigate: (path: string) => void;
+ pagesLength: number;
+ cannotCloseLastTabMessage: string;
+ setNodes: (nodes: FlowNode[] | ((nodes: FlowNode[]) => FlowNode[])) => void;
+ setEdges: (edges: FlowEdge[] | ((edges: FlowEdge[]) => FlowEdge[])) => void;
+ restoreSnapshot: (
+ snapshot: FlowSnapshot,
+ setNodes: UseFlowEditorCallbacksParams['setNodes'],
+ setEdges: UseFlowEditorCallbacksParams['setEdges']
+ ) => void;
+ recordHistory: () => void;
+ fitView: (options?: { duration?: number; padding?: number }) => void;
+ screenToFlowPosition: (position: { x: number; y: number }) => { x: number; y: number };
}
interface UseFlowEditorCallbacksResult {
- getCenter: () => { x: number; y: number };
- handleSwitchPage: (pageId: string) => void;
- handleAddPage: () => void;
- handleClosePage: (pageId: string) => void;
- handleRenamePage: (pageId: string, newName: string) => void;
- handleReorderPage: (draggedPageId: string, targetPageId: string) => void;
- selectAll: () => void;
- handleRestoreSnapshot: (snapshot: FlowSnapshot) => void;
- handleCommandBarApply: (newNodes: FlowNode[], newEdges: FlowEdge[]) => void;
+ getCenter: () => { x: number; y: number };
+ handleSwitchPage: (pageId: string) => void;
+ handleAddPage: () => void;
+ handleClosePage: (pageId: string) => void;
+ handleRenamePage: (pageId: string, newName: string) => void;
+ handleReorderPage: (draggedPageId: string, targetPageId: string) => void;
+ selectAll: () => void;
+ handleRestoreSnapshot: (snapshot: FlowSnapshot) => void;
+ handleCommandBarApply: (newNodes: FlowNode[], newEdges: FlowEdge[]) => void;
}
export function useFlowEditorCallbacks({
- addPage,
- closePage,
- reorderPage,
- updatePage,
- navigate,
- pagesLength,
- cannotCloseLastTabMessage,
- setNodes,
- setEdges,
- restoreSnapshot,
- recordHistory,
- fitView,
- screenToFlowPosition,
+ addPage,
+ closePage,
+ reorderPage,
+ updatePage,
+ navigate,
+ pagesLength,
+ cannotCloseLastTabMessage,
+ setNodes,
+ setEdges,
+ restoreSnapshot,
+ recordHistory,
+ fitView,
+ screenToFlowPosition,
}: UseFlowEditorCallbacksParams): UseFlowEditorCallbacksResult {
- const stabilizationRunIdRef = useRef(0);
-
- const getCenter = useCallback(() => {
- const centerX = window.innerWidth / 2;
- const centerY = window.innerHeight / 2;
- return screenToFlowPosition({ x: centerX, y: centerY });
- }, [screenToFlowPosition]);
-
- const handleSwitchPage = useCallback((pageId: string) => {
- navigate(`/flow/${pageId}`);
- }, [navigate]);
-
- const handleAddPage = useCallback(() => {
- const newId = addPage();
- navigate(`/flow/${newId}`);
- }, [addPage, navigate]);
-
- const handleClosePage = useCallback((pageId: string) => {
- if (pagesLength === 1) {
- alert(cannotCloseLastTabMessage);
+ const stabilizationRunIdRef = useRef(0);
+
+ const getCenter = useCallback(() => {
+ const centerX = window.innerWidth / 2;
+ const centerY = window.innerHeight / 2;
+ return screenToFlowPosition({ x: centerX, y: centerY });
+ }, [screenToFlowPosition]);
+
+ const handleSwitchPage = useCallback(
+ (pageId: string) => {
+ navigate(`/flow/${pageId}`);
+ },
+ [navigate]
+ );
+
+ const handleAddPage = useCallback(() => {
+ const newId = addPage();
+ navigate(`/flow/${newId}`);
+ }, [addPage, navigate]);
+
+ const handleClosePage = useCallback(
+ (pageId: string) => {
+ if (pagesLength === 1) {
+ alert(cannotCloseLastTabMessage);
+ return;
+ }
+ closePage(pageId);
+ },
+ [cannotCloseLastTabMessage, closePage, pagesLength]
+ );
+
+ const handleRenamePage = useCallback(
+ (pageId: string, newName: string) => {
+ updatePage(pageId, { name: newName });
+ },
+ [updatePage]
+ );
+
+ const handleReorderPage = useCallback(
+ (draggedPageId: string, targetPageId: string) => {
+ reorderPage(draggedPageId, targetPageId);
+ },
+ [reorderPage]
+ );
+
+ const selectAll = useCallback(() => {
+ setNodes((nodes) => nodes.map((node) => ({ ...node, selected: true })));
+ setEdges((edges) => edges.map((edge) => ({ ...edge, selected: true })));
+ }, [setEdges, setNodes]);
+
+ const handleRestoreSnapshot = useCallback(
+ (snapshot: FlowSnapshot) => {
+ restoreSnapshot(snapshot, setNodes, setEdges);
+ recordHistory();
+ },
+ [recordHistory, restoreSnapshot, setEdges, setNodes]
+ );
+
+ const handleCommandBarApply = useCallback(
+ async (newNodes: FlowNode[], newEdges: FlowEdge[]) => {
+ const enrichedNodes = await enrichNodesWithIcons(newNodes);
+ recordHistory();
+ startTransition(() => {
+ setNodes(
+ enrichedNodes.map((node, index) => ({
+ ...node,
+ data: { ...node.data, freshlyAdded: true, animateDelay: Math.min(index * 20, 400) },
+ }))
+ );
+ setEdges(newEdges);
+ });
+ setTimeout(() => fitView({ duration: 800, padding: 0.2 }), 100);
+
+ const runId = stabilizationRunIdRef.current + 1;
+ stabilizationRunIdRef.current = runId;
+
+ window.setTimeout(() => {
+ void (async () => {
+ if (stabilizationRunIdRef.current !== runId) {
return;
- }
- closePage(pageId);
- }, [cannotCloseLastTabMessage, closePage, pagesLength]);
-
- const handleRenamePage = useCallback((pageId: string, newName: string) => {
- updatePage(pageId, { name: newName });
- }, [updatePage]);
-
- const handleReorderPage = useCallback((draggedPageId: string, targetPageId: string) => {
- reorderPage(draggedPageId, targetPageId);
- }, [reorderPage]);
-
- const selectAll = useCallback(() => {
- setNodes((nodes) => nodes.map((node) => ({ ...node, selected: true })));
- setEdges((edges) => edges.map((edge) => ({ ...edge, selected: true })));
- }, [setEdges, setNodes]);
-
- const handleRestoreSnapshot = useCallback((snapshot: FlowSnapshot) => {
- restoreSnapshot(snapshot, setNodes, setEdges);
- recordHistory();
- }, [recordHistory, restoreSnapshot, setEdges, setNodes]);
-
- const handleCommandBarApply = useCallback((newNodes: FlowNode[], newEdges: FlowEdge[]) => {
- recordHistory();
- startTransition(() => {
- setNodes(newNodes.map((node, index) => ({
- ...node,
- data: { ...node.data, freshlyAdded: true, animateDelay: Math.min(index * 20, 400) },
- })));
- setEdges(newEdges);
- });
- setTimeout(() => fitView({ duration: 800, padding: 0.2 }), 100);
-
- const runId = stabilizationRunIdRef.current + 1;
- stabilizationRunIdRef.current = runId;
-
- window.setTimeout(() => {
- void (async () => {
- if (stabilizationRunIdRef.current !== runId) {
- return;
- }
-
- const state = useFlowStore.getState();
- const measuredNodes = state.nodes;
- const measuredEdges = state.edges;
- const hasMeasuredDimensions = measuredNodes.some((node) => {
- const measured = (node as FlowNode & {
- measured?: { width?: number; height?: number };
- }).measured;
- return typeof measured?.width === 'number' && typeof measured?.height === 'number';
- });
-
- if (!hasMeasuredDimensions) {
- return;
- }
-
- const activeTab = state.tabs.find((tab) => tab.id === state.activeTabId);
- const { nodes: stabilizedNodes, edges: stabilizedEdges } = await composeDiagramForDisplay(
- measuredNodes,
- measuredEdges,
- { diagramType: activeTab?.diagramType }
- );
-
- if (stabilizationRunIdRef.current !== runId) {
- return;
- }
-
- setNodes(stabilizedNodes);
- setEdges(stabilizedEdges);
- fitView({ duration: 500, padding: 0.2 });
- })();
- }, 180);
- }, [fitView, recordHistory, setEdges, setNodes]);
-
- return {
- getCenter,
- handleSwitchPage,
- handleAddPage,
- handleClosePage,
- handleRenamePage,
- handleReorderPage,
- selectAll,
- handleRestoreSnapshot,
- handleCommandBarApply,
- };
+ }
+
+ const state = useFlowStore.getState();
+ const measuredNodes = state.nodes;
+ const measuredEdges = state.edges;
+ const hasMeasuredDimensions = measuredNodes.some((node) => {
+ const measured = (
+ node as FlowNode & {
+ measured?: { width?: number; height?: number };
+ }
+ ).measured;
+ return typeof measured?.width === 'number' && typeof measured?.height === 'number';
+ });
+
+ if (!hasMeasuredDimensions) {
+ return;
+ }
+
+ const activeTab = state.tabs.find((tab) => tab.id === state.activeTabId);
+ const { clearLayoutCache } = await import('@/services/elkLayout');
+ clearLayoutCache();
+ const { nodes: stabilizedNodes, edges: stabilizedEdges } = await composeDiagramForDisplay(
+ measuredNodes,
+ measuredEdges,
+ { diagramType: activeTab?.diagramType }
+ );
+
+ if (stabilizationRunIdRef.current !== runId) {
+ return;
+ }
+
+ setNodes(stabilizedNodes);
+ setEdges(stabilizedEdges);
+ fitView({ duration: 500, padding: 0.2 });
+ })();
+ }, 180);
+ },
+ [fitView, recordHistory, setEdges, setNodes]
+ );
+
+ return {
+ getCenter,
+ handleSwitchPage,
+ handleAddPage,
+ handleClosePage,
+ handleRenamePage,
+ handleReorderPage,
+ selectAll,
+ handleRestoreSnapshot,
+ handleCommandBarApply,
+ };
}
diff --git a/src/hooks/useFlowEditorUIState.ts b/src/hooks/useFlowEditorUIState.ts
index d3ff84d6..f20be1a6 100644
--- a/src/hooks/useFlowEditorUIState.ts
+++ b/src/hooks/useFlowEditorUIState.ts
@@ -42,7 +42,7 @@ export function useFlowEditorUIState(): UseFlowEditorUIStateResult {
const [commandBarView, setCommandBarView] = useState('root');
const [editorMode, setEditorMode] = useState('canvas');
const [studioTab, setStudioTab] = useState('ai');
- const [studioCodeMode, setStudioCodeMode] = useState('openflow');
+ const [studioCodeMode, setStudioCodeMode] = useState('mermaid');
const [isSelectMode, setIsSelectMode] = useState(true);
const [isArchitectureRulesOpen, setIsArchitectureRulesOpen] = useState(false);
diff --git a/src/lib/aiIconsPipeline.test.ts b/src/lib/aiIconsPipeline.test.ts
new file mode 100644
index 00000000..6a08ad38
--- /dev/null
+++ b/src/lib/aiIconsPipeline.test.ts
@@ -0,0 +1,154 @@
+import { describe, expect, it } from 'vitest';
+import { parseOpenFlowDslV2 } from './flowmindDSLParserV2';
+import { enrichNodesWithIcons } from './nodeEnricher';
+
+// These test the full pipeline: AI-generated DSL โ parse โ enrich โ correct icons
+// Simulates what happens when AI outputs DSL with archProvider/archResourceType
+
+describe('AI + Icons Pipeline (E2E)', () => {
+ it('Node.js API with PostgreSQL and Redis', async () => {
+ const dsl = `
+ flow: Node.js Stack
+ direction: TB
+
+ [system] api: Express API { archProvider: "developer", archResourceType: "others-expressjs-dark", color: "blue" }
+ [system] db: PostgreSQL { archProvider: "developer", archResourceType: "database-postgresql", color: "violet" }
+ [system] cache: Redis { archProvider: "developer", archResourceType: "database-redis", color: "red" }
+
+ api ->|SQL| db
+ api ->|cache| cache
+ `;
+
+ const parsed = parseOpenFlowDslV2(dsl);
+ const enriched = await enrichNodesWithIcons(parsed.nodes);
+
+ expect(enriched).toHaveLength(3);
+
+ const api = enriched.find((n) => n.id === 'api');
+ expect(api?.data.archIconPackId).toBe('developer-icons-v1');
+ expect(api?.data.archIconShapeId).toBe('others-expressjs-dark');
+
+ const db = enriched.find((n) => n.id === 'db');
+ expect(db?.data.archIconPackId).toBe('developer-icons-v1');
+ expect(db?.data.archIconShapeId).toBe('database-postgresql');
+
+ const cache = enriched.find((n) => n.id === 'cache');
+ expect(cache?.data.archIconPackId).toBe('developer-icons-v1');
+ expect(cache?.data.archIconShapeId).toContain('redis');
+ });
+
+ it('AWS Lambda โ SQS โ DynamoDB', async () => {
+ const dsl = `
+ flow: Serverless Pipeline
+ direction: TB
+
+ [architecture] lambda: Lambda { archProvider: "aws", archResourceType: "compute-lambda", color: "violet" }
+ [architecture] sqs: SQS Queue { archProvider: "aws", archResourceType: "app-integration-sqs", color: "amber" }
+ [architecture] dynamo: DynamoDB { archProvider: "aws", archResourceType: "database-dynamodb", color: "emerald" }
+
+ lambda ->|publish| sqs
+ sqs ->|write| dynamo
+ `;
+
+ const parsed = parseOpenFlowDslV2(dsl);
+ const enriched = await enrichNodesWithIcons(parsed.nodes);
+
+ expect(enriched).toHaveLength(3);
+
+ for (const node of enriched) {
+ expect(node.data.archIconPackId).toBe('aws-official-starter-v1');
+ expect(node.data.archIconShapeId).toBeTruthy();
+ }
+ });
+
+ it('React โ Express โ MongoDB โ S3 (mixed stacks)', async () => {
+ const dsl = `
+ flow: Full Stack
+ direction: TB
+
+ [system] react: React App { archProvider: "developer", archResourceType: "frontend-react", color: "blue" }
+ [system] api: Express { archProvider: "developer", archResourceType: "others-expressjs-dark", color: "violet" }
+ [system] mongo: MongoDB { archProvider: "developer", archResourceType: "database-mongodb", color: "emerald" }
+ [architecture] s3: S3 Storage { archProvider: "aws", archResourceType: "storage-s3", color: "amber" }
+
+ react ->|HTTP| api
+ api ->|query| mongo
+ api ->|upload| s3
+ `;
+
+ const parsed = parseOpenFlowDslV2(dsl);
+ const enriched = await enrichNodesWithIcons(parsed.nodes);
+
+ expect(enriched).toHaveLength(4);
+
+ const react = enriched.find((n) => n.id === 'react');
+ expect(react?.data.archIconPackId).toBe('developer-icons-v1');
+ expect(react?.data.color).toBe('blue');
+
+ const s3 = enriched.find((n) => n.id === 's3');
+ expect(s3?.data.archIconPackId).toBe('aws-official-starter-v1');
+ });
+
+ it('auto-enriches nodes without explicit icons (icons: auto behavior)', async () => {
+ const dsl = `
+ flow: Auto Icons
+ direction: TB
+
+ [system] api: Express API
+ [system] db: PostgreSQL Database
+ [system] cache: Redis Cache
+ `;
+
+ const parsed = parseOpenFlowDslV2(dsl);
+ const enriched = await enrichNodesWithIcons(parsed.nodes);
+
+ // Without explicit archProvider, enricher should match by label
+ const api = enriched.find((n) => n.id === 'api');
+ expect(api?.data.archIconPackId).toBeTruthy();
+ expect(api?.data.color).toBe('blue');
+
+ const db = enriched.find((n) => n.id === 'db');
+ expect(db?.data.color).toBe('violet');
+ });
+
+ it('enricher does not overwrite AI-set provider icons', async () => {
+ const dsl = `
+ [architecture] lambda: My Lambda { archProvider: "aws", archResourceType: "compute-lambda", color: "violet" }
+ `;
+
+ const parsed = parseOpenFlowDslV2(dsl);
+ const enriched = await enrichNodesWithIcons(parsed.nodes);
+
+ const lambda = enriched.find((n) => n.id === 'lambda');
+ expect(lambda?.data.archIconPackId).toBe('aws-official-starter-v1');
+ expect(lambda?.data.archIconShapeId).toBe('compute-lambda');
+ expect(lambda?.data.color).toBe('violet');
+ });
+
+ it('enriches architecture-beta imported nodes', async () => {
+ const dsl = `
+ flow: Architecture
+ direction: TB
+
+ [architecture] server: Express.js { color: "violet" }
+ [architecture] db: PostgreSQL { color: "violet" }
+ [architecture] cache: Redis { color: "red" }
+
+ server ->|query| db
+ server ->|cache| cache
+ `;
+
+ const parsed = parseOpenFlowDslV2(dsl);
+ const enriched = await enrichNodesWithIcons(parsed.nodes);
+
+ const server = enriched.find((n) => n.id === 'server');
+ expect(server?.data.archIconPackId).toBeTruthy();
+ expect(server?.data.color).toBe('violet');
+
+ const db = enriched.find((n) => n.id === 'db');
+ expect(db?.data.archIconPackId).toBeTruthy();
+
+ const cache = enriched.find((n) => n.id === 'cache');
+ expect(cache?.data.archIconPackId).toBeTruthy();
+ });
+});
diff --git a/src/lib/flowmindDSLParserV2.test.ts b/src/lib/flowmindDSLParserV2.test.ts
index 57414fef..0eeb1407 100644
--- a/src/lib/flowmindDSLParserV2.test.ts
+++ b/src/lib/flowmindDSLParserV2.test.ts
@@ -1,93 +1,156 @@
-
import { describe, it, expect } from 'vitest';
import { parseOpenFlowDslV2 } from './openFlowDslParserV2';
describe('OpenFlow DSL V2 Parser', () => {
- it('parses basic nodes and edges', () => {
- const input = `
+ it('parses basic nodes and edges', () => {
+ const input = `
[start] Start
[process] Step 1
[end] End
Start -> Step 1
Step 1 -> End
`;
- const result = parseOpenFlowDslV2(input);
- expect(result.nodes).toHaveLength(3);
- expect(result.edges).toHaveLength(2);
+ const result = parseOpenFlowDslV2(input);
+ expect(result.nodes).toHaveLength(3);
+ expect(result.edges).toHaveLength(2);
- const startNode = result.nodes.find(n => n.data.label === 'Start');
- expect(startNode).toBeDefined();
- expect(startNode?.type).toBe('start');
- });
+ const startNode = result.nodes.find((n) => n.data.label === 'Start');
+ expect(startNode).toBeDefined();
+ expect(startNode?.type).toBe('start');
+ });
- it('parses explicit IDs', () => {
- const input = `
+ it('parses explicit IDs', () => {
+ const input = `
[process] p1: Process One
[process] p2: Process Two
p1 -> p2
`;
- const result = parseOpenFlowDslV2(input);
- expect(result.nodes).toHaveLength(2);
+ const result = parseOpenFlowDslV2(input);
+ expect(result.nodes).toHaveLength(2);
- const p1 = result.nodes.find(n => n.id === 'p1');
- expect(p1).toBeDefined();
- expect(p1?.data.label).toBe('Process One');
+ const p1 = result.nodes.find((n) => n.id === 'p1');
+ expect(p1).toBeDefined();
+ expect(p1?.data.label).toBe('Process One');
- const edge = result.edges[0];
- expect(edge.source).toBe('p1');
- expect(edge.target).toBe('p2');
- });
+ const edge = result.edges[0];
+ expect(edge.source).toBe('p1');
+ expect(edge.target).toBe('p2');
+ });
- it('parses attributes', () => {
- const input = `
+ it('parses attributes', () => {
+ const input = `
[process] p1: Configured Node { color: "red", icon: "settings" }
p1 -> p2 { style: "dashed", label: "async" }
`;
- const result = parseOpenFlowDslV2(input);
-
- const p1 = result.nodes.find(n => n.id === 'p1');
- expect(p1?.data.color).toBe('red');
- expect(p1?.data.icon).toBe('settings');
-
- const edge = result.edges[0];
- expect(edge.data?.styleType).toBe('dashed'); // Attributes merged into edge data/attributes? Parser logic puts it in edge helper or data?
- // Checking parser implementation:
- // dslEdges.push({ ..., attributes })
- // finalEdges.push(createDefaultEdge(..., attributes/label?))
- // Expecting createDefaultEdge to handle it or we need to check how it's mapped.
- // In parser implementation:
- // createDefaultEdge(source, target, label, id)
- // Wait, I missed passing attributes to createDefaultEdge in my implementation!
-
- // Let's check the implementation again.
- });
-
- it('parses quoted attribute values containing commas, colons, and escapes', () => {
- const input = `
+ const result = parseOpenFlowDslV2(input);
+
+ const p1 = result.nodes.find((n) => n.id === 'p1');
+ expect(p1?.data.color).toBe('red');
+ expect(p1?.data.icon).toBe('settings');
+
+ const edge = result.edges[0];
+ expect(edge.data?.styleType).toBe('dashed'); // Attributes merged into edge data/attributes? Parser logic puts it in edge helper or data?
+ // Checking parser implementation:
+ // dslEdges.push({ ..., attributes })
+ // finalEdges.push(createDefaultEdge(..., attributes/label?))
+ // Expecting createDefaultEdge to handle it or we need to check how it's mapped.
+ // In parser implementation:
+ // createDefaultEdge(source, target, label, id)
+ // Wait, I missed passing attributes to createDefaultEdge in my implementation!
+
+ // Let's check the implementation again.
+ });
+
+ it('parses quoted attribute values containing commas, colons, and escapes', () => {
+ const input = `
[process] p1: Configured Node { icon: "server, api", note: "http://svc:8080/path", enabled: true, retries: 3, quote: "say \\"hello\\"" }
`;
- const result = parseOpenFlowDslV2(input);
-
- const p1 = result.nodes.find((node) => node.id === 'p1');
- expect(p1?.data.icon).toBe('server, api');
- expect(p1?.data.note).toBe('http://svc:8080/path');
- expect(p1?.data.enabled).toBe(true);
- expect(p1?.data.retries).toBe(3);
- expect(p1?.data.quote).toBe('say "hello"');
- });
-
- it('ignores group wrappers and keeps inner nodes flat', () => {
- const input = `
+ const result = parseOpenFlowDslV2(input);
+
+ const p1 = result.nodes.find((node) => node.id === 'p1');
+ expect(p1?.data.icon).toBe('server, api');
+ expect(p1?.data.note).toBe('http://svc:8080/path');
+ expect(p1?.data.enabled).toBe(true);
+ expect(p1?.data.retries).toBe(3);
+ expect(p1?.data.quote).toBe('say "hello"');
+ });
+
+ it('ignores group wrappers and keeps inner nodes flat', () => {
+ const input = `
group "Backend" {
[process] api: API
[database] db: DB
api -> db
}
`;
- const result = parseOpenFlowDslV2(input);
- expect(result.nodes).toHaveLength(2);
+ const result = parseOpenFlowDslV2(input);
+ expect(result.nodes).toHaveLength(2);
+
+ const api = result.nodes.find((n) => n.id === 'api');
+ expect(api?.parentId).toBeUndefined();
+ });
+
+ it('maps archProvider/archResourceType to archIconPackId/archIconShapeId', () => {
+ const input = `
+ [system] db: PostgreSQL { archProvider: "developer", archResourceType: "database-postgresql", color: "violet" }
+ [system] api: Express API { archProvider: "developer", archResourceType: "others-expressjs-dark" }
+ `;
+ const result = parseOpenFlowDslV2(input);
+ expect(result.nodes).toHaveLength(2);
+
+ const db = result.nodes.find((n) => n.id === 'db');
+ expect(db?.data.archIconPackId).toBe('developer-icons-v1');
+ expect(db?.data.archIconShapeId).toBe('database-postgresql');
+ expect(db?.data.assetPresentation).toBe('icon');
- const api = result.nodes.find(n => n.id === 'api');
- expect(api?.parentId).toBeUndefined();
- });
+ const api = result.nodes.find((n) => n.id === 'api');
+ expect(api?.data.archIconPackId).toBe('developer-icons-v1');
+ expect(api?.data.archIconShapeId).toBe('others-expressjs-dark');
+ });
+
+ it('passes provider attribute through to node data', () => {
+ const input = `
+ [architecture] lambda: API Lambda { archProvider: "aws", archResourceType: "compute-lambda", color: "violet" }
+ [architecture] rds: Database { provider: "aws", color: "violet" }
+ `;
+ const result = parseOpenFlowDslV2(input);
+ expect(result.nodes).toHaveLength(2);
+
+ const lambda = result.nodes.find((n) => n.id === 'lambda');
+ expect(lambda?.data.archIconPackId).toBe('aws-official-starter-v1');
+ expect(lambda?.data.archIconShapeId).toBe('compute-lambda');
+
+ const rds = result.nodes.find((n) => n.id === 'rds');
+ expect(rds?.data.provider).toBe('aws');
+ });
+
+ it('passes icon attribute for catalog search', () => {
+ const input = `
+ [system] cache: Redis Cache { icon: "redis", color: "red" }
+ `;
+ const result = parseOpenFlowDslV2(input);
+ const cache = result.nodes.find((n) => n.id === 'cache');
+ expect(cache?.data.icon).toBe('redis');
+ });
+
+ it('maps [architecture] to custom node type', () => {
+ const input = `
+ [architecture] lambda: Lambda { archProvider: "aws", archResourceType: "compute-lambda" }
+ `;
+ const result = parseOpenFlowDslV2(input);
+ const lambda = result.nodes.find((n) => n.id === 'lambda');
+ expect(lambda?.type).toBe('custom');
+ });
+
+ it('accepts icons: auto header in metadata', () => {
+ const input = `
+ flow: My Architecture
+ direction: TB
+ icons: auto
+ [system] api: API
+ `;
+ const result = parseOpenFlowDslV2(input);
+ expect(result.metadata.icons).toBe('auto');
+ expect(result.nodes).toHaveLength(1);
+ });
});
diff --git a/src/lib/flowmindDSLParserV2.ts b/src/lib/flowmindDSLParserV2.ts
index ab636984..ce680b40 100644
--- a/src/lib/flowmindDSLParserV2.ts
+++ b/src/lib/flowmindDSLParserV2.ts
@@ -1,30 +1,35 @@
import { setNodeParent } from './nodeParent';
import { NODE_DEFAULTS } from '../theme';
import type { FlowEdge, FlowNode, NodeData } from './types';
+import { KNOWN_PROVIDER_PACK_IDS } from '@/services/shapeLibrary/providerCatalog';
+
+function resolveArchPackId(provider: string): string {
+ return KNOWN_PROVIDER_PACK_IDS[provider.toLowerCase()] ?? `${provider}-processed-pack-v1`;
+}
// --- Types ---
export interface DSLNode {
- id: string;
- type: string;
- label: string;
- parentId?: string;
- attributes: Record;
+ id: string;
+ type: string;
+ label: string;
+ parentId?: string;
+ attributes: Record;
}
export interface DSLEdge {
- sourceId: string;
- targetId: string;
- label?: string;
- attributes: Record;
- type?: 'default' | 'step' | 'smoothstep' | 'straight';
+ sourceId: string;
+ targetId: string;
+ label?: string;
+ attributes: Record;
+ type?: 'default' | 'step' | 'smoothstep' | 'straight';
}
export interface DSLResult {
- nodes: FlowNode[];
- edges: FlowEdge[];
- metadata: Record;
- errors: string[];
+ nodes: FlowNode[];
+ edges: FlowEdge[];
+ metadata: Record;
+ errors: string[];
}
type DSLAttributeValue = string | number | boolean;
@@ -32,373 +37,372 @@ type DSLAttributeValue = string | number | boolean;
// --- Constants ---
const NODE_TYPE_MAP: Record = {
- start: 'start',
- process: 'process',
- decision: 'decision',
- end: 'end',
- system: 'custom',
- note: 'annotation',
- section: 'process',
- group: 'process',
- browser: 'browser',
- mobile: 'mobile',
- container: 'container', // New generic container
+ start: 'start',
+ process: 'process',
+ decision: 'decision',
+ end: 'end',
+ system: 'custom',
+ note: 'annotation',
+ section: 'process',
+ group: 'process',
+ browser: 'browser',
+ mobile: 'mobile',
+ container: 'container',
+ architecture: 'custom',
};
// --- Helpers ---
function parseAttributes(text: string): Record {
- const attributes: Record = {};
- if (!text) return attributes;
-
- const content = text.trim();
- if (!content.startsWith('{') || !content.endsWith('}')) return attributes;
-
- const inner = content.slice(1, -1);
- const pairs: string[] = [];
- let buffer = '';
- let quote: '"' | "'" | null = null;
- let escaping = false;
-
- for (const char of inner) {
- if (escaping) {
- buffer += char;
- escaping = false;
- continue;
- }
-
- if (char === '\\') {
- buffer += char;
- escaping = true;
- continue;
- }
-
- if (quote) {
- buffer += char;
- if (char === quote) {
- quote = null;
- }
- continue;
- }
+ const attributes: Record = {};
+ if (!text) return attributes;
+
+ const content = text.trim();
+ if (!content.startsWith('{') || !content.endsWith('}')) return attributes;
+
+ const inner = content.slice(1, -1);
+ const pairs: string[] = [];
+ let buffer = '';
+ let quote: '"' | "'" | null = null;
+ let escaping = false;
+
+ for (const char of inner) {
+ if (escaping) {
+ buffer += char;
+ escaping = false;
+ continue;
+ }
- if (char === '"' || char === "'") {
- quote = char;
- buffer += char;
- continue;
- }
+ if (char === '\\') {
+ buffer += char;
+ escaping = true;
+ continue;
+ }
- if (char === ',') {
- const pair = buffer.trim();
- if (pair) pairs.push(pair);
- buffer = '';
- continue;
- }
+ if (quote) {
+ buffer += char;
+ if (char === quote) {
+ quote = null;
+ }
+ continue;
+ }
- buffer += char;
+ if (char === '"' || char === "'") {
+ quote = char;
+ buffer += char;
+ continue;
}
- const trailingPair = buffer.trim();
- if (trailingPair) {
- pairs.push(trailingPair);
+ if (char === ',') {
+ const pair = buffer.trim();
+ if (pair) pairs.push(pair);
+ buffer = '';
+ continue;
}
- pairs.forEach((pair) => {
- let colonIndex = -1;
- let pairQuote: '"' | "'" | null = null;
- let pairEscaping = false;
-
- for (let index = 0; index < pair.length; index += 1) {
- const char = pair[index];
-
- if (pairEscaping) {
- pairEscaping = false;
- continue;
- }
-
- if (char === '\\') {
- pairEscaping = true;
- continue;
- }
-
- if (pairQuote) {
- if (char === pairQuote) {
- pairQuote = null;
- }
- continue;
- }
-
- if (char === '"' || char === "'") {
- pairQuote = char;
- continue;
- }
-
- if (char === ':') {
- colonIndex = index;
- break;
- }
- }
+ buffer += char;
+ }
+
+ const trailingPair = buffer.trim();
+ if (trailingPair) {
+ pairs.push(trailingPair);
+ }
+
+ pairs.forEach((pair) => {
+ let colonIndex = -1;
+ let pairQuote: '"' | "'" | null = null;
+ let pairEscaping = false;
+
+ for (let index = 0; index < pair.length; index += 1) {
+ const char = pair[index];
+
+ if (pairEscaping) {
+ pairEscaping = false;
+ continue;
+ }
- if (colonIndex <= 0) return;
-
- const key = pair.slice(0, colonIndex).trim();
- const rawValue = pair.slice(colonIndex + 1).trim();
- if (!key || !rawValue) return;
-
- let value: DSLAttributeValue = rawValue;
- if (
- (value.startsWith('"') && value.endsWith('"'))
- || (value.startsWith("'") && value.endsWith("'"))
- ) {
- value = value
- .slice(1, -1)
- .replace(/\\(["'])/g, '$1')
- .replace(/\\\\/g, '\\');
- } else if (!Number.isNaN(Number(value))) {
- value = Number(value);
- } else if (value === 'true') {
- value = true;
- } else if (value === 'false') {
- value = false;
+ if (char === '\\') {
+ pairEscaping = true;
+ continue;
+ }
+
+ if (pairQuote) {
+ if (char === pairQuote) {
+ pairQuote = null;
}
+ continue;
+ }
+
+ if (char === '"' || char === "'") {
+ pairQuote = char;
+ continue;
+ }
+
+ if (char === ':') {
+ colonIndex = index;
+ break;
+ }
+ }
- attributes[key] = value;
- });
+ if (colonIndex <= 0) return;
+
+ const key = pair.slice(0, colonIndex).trim();
+ const rawValue = pair.slice(colonIndex + 1).trim();
+ if (!key || !rawValue) return;
+
+ let value: DSLAttributeValue = rawValue;
+ if (
+ (value.startsWith('"') && value.endsWith('"')) ||
+ (value.startsWith("'") && value.endsWith("'"))
+ ) {
+ value = value
+ .slice(1, -1)
+ .replace(/\\(["'])/g, '$1')
+ .replace(/\\\\/g, '\\');
+ } else if (!Number.isNaN(Number(value))) {
+ value = Number(value);
+ } else if (value === 'true') {
+ value = true;
+ } else if (value === 'false') {
+ value = false;
+ }
- return attributes;
-};
+ attributes[key] = value;
+ });
+
+ return attributes;
+}
// --- Parser ---
export function parseOpenFlowDslV2(input: string): DSLResult {
- const dslNodes: DSLNode[] = [];
- const dslEdges: DSLEdge[] = [];
- const metadata: Record = { direction: 'TB' };
- const errors: string[] = [];
-
- const lines = input.split('\n');
- const currentGroupStack: string[] = [];
-
- // First pass: symbols and structure
- // We need map label -> ID for implicit IDs
- const labelToIdMap = new Map();
-
- lines.forEach((rawLine, lineIndex) => {
- const line = rawLine.trim();
- if (!line || line.startsWith('#')) return;
-
- // 1. Metadata: key: value
- const metadataMatch = line.match(/^([a-zA-Z0-9_]+):\s*(.+)$/);
- // Avoid matching "label: value" inside node defs, heuristic: no brackets, no arrow
- if (metadataMatch && !line.includes('[') && !line.includes('->')) {
- const key = metadataMatch[1].toLowerCase();
- let value = metadataMatch[2].trim();
- // Strip quotes if present
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
- value = value.slice(1, -1);
- }
- metadata[key] = value;
- return;
- }
-
- // 2. Groups Start: group "Label" {
- const groupStartMatch = line.match(/^group\s+"?([^"{]+)"?\s*\{$/);
- if (groupStartMatch) {
- currentGroupStack.push(groupStartMatch[1]);
- return;
- }
+ const dslNodes: DSLNode[] = [];
+ const dslEdges: DSLEdge[] = [];
+ const metadata: Record = { direction: 'TB' };
+ const errors: string[] = [];
+
+ const lines = input.split('\n');
+ const currentGroupStack: string[] = [];
+
+ // First pass: symbols and structure
+ // We need map label -> ID for implicit IDs
+ const labelToIdMap = new Map();
+
+ lines.forEach((rawLine, lineIndex) => {
+ const line = rawLine.trim();
+ if (!line || line.startsWith('#')) return;
+
+ // 1. Metadata: key: value
+ const metadataMatch = line.match(/^([a-zA-Z0-9_]+):\s*(.+)$/);
+ // Avoid matching "label: value" inside node defs, heuristic: no brackets, no arrow
+ if (metadataMatch && !line.includes('[') && !line.includes('->')) {
+ const key = metadataMatch[1].toLowerCase();
+ let value = metadataMatch[2].trim();
+ // Strip quotes if present
+ if (
+ (value.startsWith('"') && value.endsWith('"')) ||
+ (value.startsWith("'") && value.endsWith("'"))
+ ) {
+ value = value.slice(1, -1);
+ }
+ metadata[key] = value;
+ return;
+ }
- // 3. Group End: }
- if (line === '}') {
- if (currentGroupStack.length > 0) {
- currentGroupStack.pop();
- } else {
- errors.push(`Line ${lineIndex + 1}: Unexpected '}'`);
- }
- return;
- }
+ // 2. Groups Start: group "Label" {
+ const groupStartMatch = line.match(/^group\s+"?([^"{]+)"?\s*\{$/);
+ if (groupStartMatch) {
+ currentGroupStack.push(groupStartMatch[1]);
+ return;
+ }
- // 4. Edges: A -> B { attrs }
- // regex: (source) (arrow) (target) (attrs?)
- const edgeMatch = line.match(/^(.+?)\s*(->|-->|\.\.>|==>)\s*(.+?)(\s*\{.*\})?$/);
- if (edgeMatch) {
- // Note: We intentionally catch lines starting with '[' here if they have an arrow.
- // This handles cases where AI mistakenly writes "[type] Node -> Node".
-
- const [, sourceRaw, arrow, targetRaw, attrsRaw] = edgeMatch;
-
- // Helper to clean potential [type] prefixes from IDs in edges
- const cleanId = (raw: string) => {
- const typeMatch = raw.match(/^\[.*?\]\s*(.+)$/);
- return (typeMatch ? typeMatch[1] : raw).trim();
- };
-
- // Extract labels/IDs from potential piped text: Source ->|Label| Target
- // Re-parsing source/target for piped labels if valid arrow syntax
- // "A ->|yes| B"
- const source = cleanId(sourceRaw.trim());
- let targetRawTrimmed = targetRaw.trim();
- let label = '';
-
- const pipeMatch = targetRawTrimmed.match(/^\|([^|]+)\|\s*(.+)$/);
- if (pipeMatch) {
- label = pipeMatch[1];
- targetRawTrimmed = pipeMatch[2].trim();
- }
- const target = cleanId(targetRawTrimmed);
-
- // Attributes
- const attributes = parseAttributes(attrsRaw || '');
-
- // Arrow styling
- if (arrow === '-->') attributes.styleType = 'curved';
- if (arrow === '..>') attributes.styleType = 'dashed';
- if (arrow === '==>') attributes.styleType = 'thick';
-
- dslEdges.push({
- sourceId: source, // Resolved later
- targetId: target, // Resolved later
- label,
- attributes
- });
- return;
- }
+ // 3. Group End: }
+ if (line === '}') {
+ if (currentGroupStack.length > 0) {
+ currentGroupStack.pop();
+ } else {
+ errors.push(`Line ${lineIndex + 1}: Unexpected '}'`);
+ }
+ return;
+ }
- // 5. Nodes: [type] id: Label { attrs }
- const nodeMatch = line.match(/^\[([a-zA-Z0-9_]+)\]\s*(?:([a-zA-Z0-9_]+):\s*)?([^{]+)(\s*\{.*\})?$/);
- if (nodeMatch) {
- const [, typeRaw, idRaw, labelRaw, attrsRaw] = nodeMatch;
- const type = NODE_TYPE_MAP[typeRaw.toLowerCase()] || 'process';
- const label = labelRaw.trim();
- const id = idRaw ? idRaw.trim() : label; // If no explicit ID, use label (backward compact)
-
- const attributes = parseAttributes(attrsRaw || '');
-
- const node: DSLNode = {
- id,
- type,
- label,
- attributes,
- parentId: undefined
- };
-
- dslNodes.push(node);
- labelToIdMap.set(label, id); // Map label to ID for edge resolution
- labelToIdMap.set(id, id); // Map ID to ID
- return;
- }
+ // 4. Edges: A -> B { attrs }
+ // regex: (source) (arrow) (target) (attrs?)
+ const edgeMatch = line.match(/^(.+?)\s*(->|-->|\.\.>|==>)\s*(.+?)(\s*\{.*\})?$/);
+ if (edgeMatch) {
+ // Note: We intentionally catch lines starting with '[' here if they have an arrow.
+ // This handles cases where AI mistakenly writes "[type] Node -> Node".
+
+ const [, sourceRaw, arrow, targetRaw, attrsRaw] = edgeMatch;
+
+ // Helper to clean potential [type] prefixes from IDs in edges
+ const cleanId = (raw: string) => {
+ const typeMatch = raw.match(/^\[.*?\]\s*(.+)$/);
+ return (typeMatch ? typeMatch[1] : raw).trim();
+ };
+
+ // Extract labels/IDs from potential piped text: Source ->|Label| Target
+ // Re-parsing source/target for piped labels if valid arrow syntax
+ // "A ->|yes| B"
+ const source = cleanId(sourceRaw.trim());
+ let targetRawTrimmed = targetRaw.trim();
+ let label = '';
+
+ const pipeMatch = targetRawTrimmed.match(/^\|([^|]+)\|\s*(.+)$/);
+ if (pipeMatch) {
+ label = pipeMatch[1];
+ targetRawTrimmed = pipeMatch[2].trim();
+ }
+ const target = cleanId(targetRawTrimmed);
+
+ // Attributes
+ const attributes = parseAttributes(attrsRaw || '');
+
+ // Arrow styling
+ if (arrow === '-->') attributes.styleType = 'curved';
+ if (arrow === '..>') attributes.styleType = 'dashed';
+ if (arrow === '==>') attributes.styleType = 'thick';
+
+ dslEdges.push({
+ sourceId: source, // Resolved later
+ targetId: target, // Resolved later
+ label,
+ attributes,
+ });
+ return;
+ }
- errors.push(`Line ${lineIndex + 1}: Unrecognized syntax "${line}"`);
- });
+ // 5. Nodes: [type] id: Label { attrs }
+ const nodeMatch = line.match(
+ /^\[([a-zA-Z0-9_]+)\]\s*(?:([a-zA-Z0-9_]+):\s*)?([^{]+)(\s*\{.*\})?$/
+ );
+ if (nodeMatch) {
+ const [, typeRaw, idRaw, labelRaw, attrsRaw] = nodeMatch;
+ const type = NODE_TYPE_MAP[typeRaw.toLowerCase()] || 'process';
+ const label = labelRaw.trim();
+ const id = idRaw ? idRaw.trim() : label; // If no explicit ID, use label (backward compact)
+
+ const attributes = parseAttributes(attrsRaw || '');
+
+ const node: DSLNode = {
+ id,
+ type,
+ label,
+ attributes,
+ };
+
+ dslNodes.push(node);
+ labelToIdMap.set(label, id); // Map label to ID for edge resolution
+ labelToIdMap.set(id, id); // Map ID to ID
+ return;
+ }
- if (currentGroupStack.length > 0) {
- errors.push(`Line ${lines.length}: Unclosed group block (missing closing "}")`);
+ errors.push(`Line ${lineIndex + 1}: Unrecognized syntax "${line}"`);
+ });
+
+ if (currentGroupStack.length > 0) {
+ errors.push(`Line ${lines.length}: Unclosed group block (missing closing "}")`);
+ }
+
+ // Post-processing: Resolve implicit nodes and edge IDs
+ const finalNodes: FlowNode[] = [];
+ const finalEdges: FlowEdge[] = [];
+ const createdNodeIds = new Set();
+
+ // 1. Process explicit nodes
+ dslNodes.forEach((n) => {
+ const defaultStyle = NODE_DEFAULTS[n.type] || NODE_DEFAULTS['process'];
+
+ // Layout placeholder (will be handled by ELK layout)
+ let node: FlowNode = {
+ id: n.id,
+ type: n.type,
+ position: { x: 0, y: 0 },
+ data: {
+ label: n.label,
+ shape: defaultStyle?.shape as NodeData['shape'],
+ color: defaultStyle?.color,
+ icon: defaultStyle?.icon && defaultStyle.icon !== 'none' ? defaultStyle.icon : undefined,
+ ...n.attributes,
+ ...(n.attributes.archProvider
+ ? { archIconPackId: resolveArchPackId(String(n.attributes.archProvider)) }
+ : {}),
+ ...(n.attributes.archResourceType
+ ? { archIconShapeId: String(n.attributes.archResourceType) }
+ : {}),
+ ...(n.attributes.archResourceType ? { assetPresentation: 'icon' as const } : {}),
+ },
+ };
+ if (n.parentId) {
+ node = setNodeParent(node, n.parentId);
}
+ finalNodes.push(node);
+ createdNodeIds.add(n.id);
+ });
+
+ // 2. Process edges and create implicit nodes
+ dslEdges.forEach((e, i) => {
+ const sourceId = labelToIdMap.get(e.sourceId) || e.sourceId;
+ const targetId = labelToIdMap.get(e.targetId) || e.targetId;
+
+ const ensureNode = (nodeId: string) => {
+ if (createdNodeIds.has(nodeId)) return;
+ const style = NODE_DEFAULTS['process'];
+ finalNodes.push({
+ id: nodeId,
+ type: 'process',
+ position: { x: 0, y: 0 },
+ data: {
+ label: nodeId,
+ shape: style?.shape as NodeData['shape'],
+ color: style?.color,
+ icon: style?.icon && style.icon !== 'none' ? style.icon : undefined,
+ },
+ });
+ createdNodeIds.add(nodeId);
+ labelToIdMap.set(nodeId, nodeId);
+ };
- // Post-processing: Resolve implicit nodes and edge IDs
- const finalNodes: FlowNode[] = [];
- const finalEdges: FlowEdge[] = [];
- const createdNodeIds = new Set();
-
- // 1. Process explicit nodes
- dslNodes.forEach((n) => {
- const defaultStyle = NODE_DEFAULTS[n.type] || NODE_DEFAULTS['process'];
-
- // Layout placeholder (will be handled by ELK layout)
- let node: FlowNode = {
- id: n.id,
- type: n.type,
- position: { x: 0, y: 0 },
- data: {
- label: n.label,
- shape: defaultStyle?.shape as NodeData['shape'],
- color: defaultStyle?.color,
- icon: defaultStyle?.icon && defaultStyle.icon !== 'none' ? defaultStyle.icon : undefined,
- ...n.attributes
- },
- };
- if (n.parentId) {
- node = setNodeParent(node, n.parentId);
- }
- finalNodes.push(node);
- createdNodeIds.add(n.id);
- });
-
- // 2. Process edges and create implicit nodes
- dslEdges.forEach((e, i) => {
- const sourceId = labelToIdMap.get(e.sourceId) || e.sourceId;
- const targetId = labelToIdMap.get(e.targetId) || e.targetId;
-
- // If nodes parse as "A -> B" and A wasn't defined, create a default process node
- const defaultProcessStyle = NODE_DEFAULTS['process'];
-
- if (!createdNodeIds.has(sourceId)) {
- finalNodes.push({
- id: sourceId,
- type: 'process',
- position: { x: 0, y: 0 },
- data: {
- label: sourceId,
- shape: defaultProcessStyle?.shape as NodeData['shape'],
- color: defaultProcessStyle?.color,
- icon: defaultProcessStyle?.icon && defaultProcessStyle.icon !== 'none' ? defaultProcessStyle.icon : undefined,
- }
- });
- createdNodeIds.add(sourceId);
- labelToIdMap.set(sourceId, sourceId);
- }
- if (!createdNodeIds.has(targetId)) {
- finalNodes.push({
- id: targetId,
- type: 'process',
- position: { x: 0, y: 0 },
- data: {
- label: targetId,
- shape: defaultProcessStyle?.shape as NodeData['shape'],
- color: defaultProcessStyle?.color,
- icon: defaultProcessStyle?.icon && defaultProcessStyle.icon !== 'none' ? defaultProcessStyle.icon : undefined,
- }
- });
- createdNodeIds.add(targetId);
- labelToIdMap.set(targetId, targetId);
- }
+ ensureNode(sourceId);
+ ensureNode(targetId);
- const finalEdge: FlowEdge = {
- id: `edge-${i}`, // Unique ID for the edge
- source: sourceId,
- target: targetId,
- label: e.label,
- type: 'default', // Default edge type
- data: { label: e.label }
- };
-
- // Merge attributes into edge data or style
- if (Object.keys(e.attributes).length > 0) {
- finalEdge.data = { ...finalEdge.data, ...e.attributes };
-
- // Map 'style' attribute to styleType for convenience/tests
- const styleType = e.attributes.styleType || e.attributes.style;
-
- // Handle specific style mappings if needed
- if (styleType === 'curved') {
- finalEdge.type = 'smoothstep';
- finalEdge.data.styleType = 'curved';
- } else if (styleType === 'dashed') {
- finalEdge.style = { strokeDasharray: '5 5' };
- finalEdge.data.styleType = 'dashed';
- } else if (styleType === 'thick') {
- finalEdge.style = { strokeWidth: 3 };
- finalEdge.data.styleType = 'thick';
- }
- }
- finalEdges.push(finalEdge);
- });
-
- return {
- nodes: finalNodes,
- edges: finalEdges,
- metadata,
- errors
+ const finalEdge: FlowEdge = {
+ id: `edge-${i}`, // Unique ID for the edge
+ source: sourceId,
+ target: targetId,
+ label: e.label,
+ type: 'default', // Default edge type
+ data: { label: e.label },
};
-};
+
+ // Merge attributes into edge data or style
+ if (Object.keys(e.attributes).length > 0) {
+ finalEdge.data = { ...finalEdge.data, ...e.attributes };
+
+ // Map 'style' attribute to styleType for convenience/tests
+ const styleType = e.attributes.styleType || e.attributes.style;
+
+ // Handle specific style mappings if needed
+ if (styleType === 'curved') {
+ finalEdge.type = 'smoothstep';
+ finalEdge.data.styleType = 'curved';
+ } else if (styleType === 'dashed') {
+ finalEdge.style = { strokeDasharray: '5 5' };
+ finalEdge.data.styleType = 'dashed';
+ } else if (styleType === 'thick') {
+ finalEdge.style = { strokeWidth: 3 };
+ finalEdge.data.styleType = 'thick';
+ }
+ }
+ finalEdges.push(finalEdge);
+ });
+
+ return {
+ nodes: finalNodes,
+ edges: finalEdges,
+ metadata,
+ errors,
+ };
+}
export const parseFlowMindDSL = parseOpenFlowDslV2;
diff --git a/src/lib/iconMatcher.test.ts b/src/lib/iconMatcher.test.ts
new file mode 100644
index 00000000..08466713
--- /dev/null
+++ b/src/lib/iconMatcher.test.ts
@@ -0,0 +1,91 @@
+import { describe, expect, it } from 'vitest';
+import { matchIcon, getMatchableIconCount, listIconProviders, buildCatalogSummary } from './iconMatcher';
+
+describe('iconMatcher', () => {
+ it('finds icons from the catalog', () => {
+ const count = getMatchableIconCount();
+ expect(count).toBeGreaterThan(100);
+ });
+
+ it('lists available providers', () => {
+ const providers = listIconProviders();
+ expect(providers).toContain('developer');
+ expect(providers).toContain('aws');
+ });
+
+ it('exact match: postgresql finds the PostgreSQL icon', () => {
+ const results = matchIcon('postgresql');
+ expect(results.length).toBeGreaterThan(0);
+ expect(results[0].shapeId).toContain('postgresql');
+ expect(results[0].matchType).toBe('exact');
+ expect(results[0].score).toBeGreaterThan(0.9);
+ });
+
+ it('alias match: "postgres" resolves to postgresql', () => {
+ const results = matchIcon('postgres');
+ expect(results.length).toBeGreaterThan(0);
+ expect(results[0].shapeId).toContain('postgresql');
+ expect(results[0].matchType).toBe('alias');
+ });
+
+ it('alias match: "k8s" resolves to kubernetes', () => {
+ const results = matchIcon('k8s');
+ expect(results.length).toBeGreaterThan(0);
+ expect(results[0].shapeId).toContain('kubernetes');
+ expect(results[0].matchType).toBe('alias');
+ });
+
+ it('substring match: "redis" finds redis icons', () => {
+ const results = matchIcon('redis');
+ expect(results.length).toBeGreaterThan(0);
+ expect(results[0].shapeId).toContain('redis');
+ });
+
+ it('provider filter: "lambda" with provider "aws" finds AWS Lambda', () => {
+ const results = matchIcon('lambda', 'aws');
+ expect(results.length).toBeGreaterThan(0);
+ expect(results[0].provider).toBe('aws');
+ expect(results[0].shapeId).toContain('lambda');
+ });
+
+ it('provider filter: "lambda" without filter finds any provider', () => {
+ const results = matchIcon('lambda');
+ expect(results.length).toBeGreaterThan(0);
+ });
+
+ it('returns empty for unknown queries', () => {
+ const results = matchIcon('zzzznotreal99999');
+ expect(results).toEqual([]);
+ });
+
+ it('returns empty for empty query', () => {
+ expect(matchIcon('')).toEqual([]);
+ expect(matchIcon(' ')).toEqual([]);
+ });
+
+ it('matchType classification works', () => {
+ const exact = matchIcon('docker');
+ if (exact.length > 0) {
+ expect(['exact', 'alias', 'substring']).toContain(exact[0].matchType);
+ }
+ });
+
+ it('lists all expected providers', () => {
+ const providers = listIconProviders();
+ expect(providers).toEqual(expect.arrayContaining(['aws', 'azure', 'cncf', 'developer', 'gcp']));
+ expect(providers).toHaveLength(5);
+ });
+
+ it('buildCatalogSummary returns non-empty summary with provider names', () => {
+ const summary = buildCatalogSummary(5);
+ expect(summary.length).toBeGreaterThan(0);
+ expect(summary).toContain('aws');
+ expect(summary).toContain('developer');
+ });
+
+ it('buildCatalogSummary respects maxPerProvider limit', () => {
+ const small = buildCatalogSummary(2);
+ const large = buildCatalogSummary(50);
+ expect(small.length).toBeLessThan(large.length);
+ });
+});
diff --git a/src/lib/iconMatcher.ts b/src/lib/iconMatcher.ts
new file mode 100644
index 00000000..86b7afb9
--- /dev/null
+++ b/src/lib/iconMatcher.ts
@@ -0,0 +1,201 @@
+import { SVG_SOURCES } from '@/services/shapeLibrary/providerCatalog';
+
+export interface IconMatch {
+ packId: string;
+ shapeId: string;
+ label: string;
+ provider: string;
+ category: string;
+ score: number;
+ matchType: 'exact' | 'alias' | 'substring' | 'category';
+}
+
+const ALIASES: Record = {
+ postgres: 'postgresql',
+ pg: 'postgresql',
+ pgsql: 'postgresql',
+ mongo: 'mongodb',
+ mdb: 'mongodb',
+ es: 'elasticsearch',
+ elastic: 'elasticsearch',
+ k8s: 'kubernetes',
+ tf: 'terraform',
+ hcl: 'terraform',
+ golang: 'go',
+ js: 'javascript',
+ ts: 'typescript',
+ py: 'python',
+ rb: 'ruby',
+ njs: 'nodejs',
+ node: 'nodejs',
+ 'react.js': 'react',
+ 'vue.js': 'vue',
+ next: 'nextjs',
+ 'nuxt.js': 'nuxt',
+ mq: 'rabbitmq',
+ apachekafka: 'kafka',
+ csharp: 'c#',
+ dotnet: '.net',
+ gke: 'google-kubernetes-engine',
+ aks: 'azure-kubernetes-service',
+ eks: 'amazon-elastic-kubernetes-service',
+ rds: 'amazon-rds',
+ sqs: 'amazon-sqs',
+ sns: 'amazon-sns',
+ s3: 'amazon-s3',
+ cf: 'cloudflare',
+ kib: 'kibana',
+ logstash: 'elastic-logstash',
+ beat: 'elastic-beats',
+};
+
+function normalize(text: string): string {
+ return text
+ .toLowerCase()
+ .replace(/[\s._]+/g, '-')
+ .replace(/[^a-z0-9-]/g, '')
+ .replace(/-+/g, '-')
+ .replace(/^-|-$/g, '');
+}
+
+function entries(): IconEntry[] {
+ return SVG_SOURCES.map((s) => {
+ const parts = s.shapeId.split('/');
+ const lastPathPart = parts[parts.length - 1];
+ const lastHyphenPart = lastPathPart.split('-').pop() ?? lastPathPart;
+ return {
+ packId: s.packId,
+ shapeId: s.shapeId,
+ label: s.label,
+ provider: s.provider,
+ category: s.category,
+ normalizedName: normalize(s.shapeId),
+ normalizedLastSegment: normalize(lastHyphenPart),
+ };
+ });
+}
+
+interface IconEntry {
+ packId: string;
+ shapeId: string;
+ label: string;
+ provider: string;
+ category: string;
+ normalizedName: string;
+ normalizedLastSegment: string;
+}
+
+let cachedEntries: IconEntry[] | null = null;
+function getEntries(): IconEntry[] {
+ if (!cachedEntries) cachedEntries = entries();
+ return cachedEntries;
+}
+
+let cachedByNormalized: Map | null = null;
+function getByNormalized(): Map {
+ if (!cachedByNormalized) {
+ cachedByNormalized = new Map();
+ for (const entry of getEntries()) {
+ cachedByNormalized.set(entry.normalizedName, entry);
+ if (entry.normalizedLastSegment !== entry.normalizedName) {
+ cachedByNormalized.set(entry.normalizedLastSegment, entry);
+ }
+ }
+ }
+ return cachedByNormalized;
+}
+
+export function matchIcon(query: string, providerHint?: string): IconMatch[] {
+ const normalizedQuery = normalize(query);
+ if (!normalizedQuery) return [];
+
+ const byNormalized = getByNormalized();
+ const all = getEntries();
+
+ // 1. Exact match on shape ID
+ const exact = byNormalized.get(normalizedQuery);
+ if (exact && (!providerHint || exact.provider === providerHint)) {
+ return [toMatch(exact, 0.99, 'exact')];
+ }
+
+ // 2. Alias resolution
+ const aliasTarget = ALIASES[normalizedQuery];
+ if (aliasTarget) {
+ const aliasEntry = byNormalized.get(normalize(aliasTarget));
+ if (aliasEntry && (!providerHint || aliasEntry.provider === providerHint)) {
+ return [toMatch(aliasEntry, 0.95, 'alias')];
+ }
+ }
+
+ // 3. Substring match (query contained in name, or name contained in query)
+ const substringMatches: IconMatch[] = [];
+ for (const entry of all) {
+ if (providerHint && entry.provider !== providerHint) continue;
+ if (entry.normalizedLastSegment.length < 3 || normalizedQuery.length < 3) continue;
+ if (
+ entry.normalizedName.includes(normalizedQuery) ||
+ entry.normalizedLastSegment.includes(normalizedQuery) ||
+ normalizedQuery.includes(entry.normalizedLastSegment)
+ ) {
+ substringMatches.push(toMatch(entry, 0.85, 'substring'));
+ }
+ }
+ if (substringMatches.length > 0) {
+ substringMatches.sort((a, b) => b.score - a.score);
+ return substringMatches.slice(0, 5);
+ }
+
+ // 4. Category match
+ const normalizedCategory = normalizedQuery.replace(/-/g, '');
+ const categoryMatches: IconMatch[] = [];
+ for (const entry of all) {
+ if (providerHint && entry.provider !== providerHint) continue;
+ if (normalize(entry.category).replace(/-/g, '').includes(normalizedCategory)) {
+ categoryMatches.push(toMatch(entry, 0.7, 'category'));
+ }
+ }
+ if (categoryMatches.length > 0) {
+ categoryMatches.sort((a, b) => b.score - a.score);
+ return categoryMatches.slice(0, 5);
+ }
+
+ return [];
+}
+
+function toMatch(entry: IconEntry, score: number, matchType: IconMatch['matchType']): IconMatch {
+ return {
+ packId: entry.packId,
+ shapeId: entry.shapeId,
+ label: entry.label,
+ provider: entry.provider,
+ category: entry.category,
+ score,
+ matchType,
+ };
+}
+
+export function getMatchableIconCount(): number {
+ return getEntries().length;
+}
+
+export function listIconProviders(): string[] {
+ return [...new Set(getEntries().map((e) => e.provider))].sort();
+}
+
+export function buildCatalogSummary(maxPerProvider: number = 30): string {
+ const byProvider = new Map();
+ for (const entry of getEntries()) {
+ const list = byProvider.get(entry.provider) ?? [];
+ list.push(entry);
+ byProvider.set(entry.provider, list);
+ }
+
+ const lines: string[] = [];
+ for (const [provider, icons] of byProvider) {
+ const categories = [...new Set(icons.map((i) => i.category))];
+ const sampleNames = icons.slice(0, maxPerProvider).map((i) => i.label);
+ lines.push(`${provider}: ${categories.join(', ')} (examples: ${sampleNames.join(', ')})`);
+ }
+
+ return lines.join('\n');
+}
diff --git a/src/lib/iconResolver.test.ts b/src/lib/iconResolver.test.ts
new file mode 100644
index 00000000..61d13fe5
--- /dev/null
+++ b/src/lib/iconResolver.test.ts
@@ -0,0 +1,75 @@
+import { describe, expect, it } from 'vitest';
+import { resolveIconSync, resolveLucideFallback } from './iconResolver';
+
+describe('resolveIconSync', () => {
+ it('resolves PostgreSQL alias', () => {
+ const result = resolveIconSync('PostgreSQL');
+ expect(result.found).toBe(true);
+ expect(result.iconSearch).toBe('postgresql');
+ expect(result.catalog).toBe('developer');
+ expect(result.confidence).toBeGreaterThan(0.9);
+ });
+
+ it('resolves shorthand aliases', () => {
+ expect(resolveIconSync('postgres').iconSearch).toBe('postgresql');
+ expect(resolveIconSync('pg').iconSearch).toBe('postgresql');
+ expect(resolveIconSync('mongo').iconSearch).toBe('mongodb');
+ expect(resolveIconSync('k8s').iconSearch).toBe('kubernetes');
+ });
+
+ it('resolves framework aliases', () => {
+ expect(resolveIconSync('React').catalog).toBe('developer');
+ expect(resolveIconSync('Next.js').iconSearch).toBe('nextjs');
+ expect(resolveIconSync('Express').iconSearch).toBe('express');
+ expect(resolveIconSync('Django').iconSearch).toBe('django');
+ expect(resolveIconSync('FastAPI').iconSearch).toBe('fastapi');
+ });
+
+ it('resolves infrastructure aliases', () => {
+ expect(resolveIconSync('Docker').catalog).toBe('developer');
+ expect(resolveIconSync('Kubernetes').catalog).toBe('cncf');
+ expect(resolveIconSync('nginx').iconSearch).toBe('nginx');
+ expect(resolveIconSync('RabbitMQ').iconSearch).toBe('rabbitmq');
+ expect(resolveIconSync('Kafka').iconSearch).toBe('apachekafka');
+ });
+
+ it('resolves cloud service aliases', () => {
+ expect(resolveIconSync('S3').catalog).toBe('aws');
+ expect(resolveIconSync('Lambda').catalog).toBe('aws');
+ expect(resolveIconSync('Cloud Run').catalog).toBe('gcp');
+ expect(resolveIconSync('Azure Functions').catalog).toBe('azure');
+ });
+
+ it('returns not found for unknown queries', () => {
+ const result = resolveIconSync('RandomThing123');
+ expect(result.found).toBe(false);
+ expect(result.confidence).toBe(0);
+ });
+
+ it('uses category fallback when alias not found', () => {
+ const result = resolveIconSync('MyCustomDB', 'database');
+ expect(result.found).toBe(true);
+ expect(result.lucideIcon).toBe('database');
+ expect(result.confidence).toBe(0.5);
+ });
+
+ it('handles empty query', () => {
+ expect(resolveIconSync('').found).toBe(false);
+ expect(resolveIconSync(' ').found).toBe(false);
+ });
+});
+
+describe('resolveLucideFallback', () => {
+ it('returns correct fallback icons', () => {
+ expect(resolveLucideFallback('database')).toBe('database');
+ expect(resolveLucideFallback('cache')).toBe('hard-drive');
+ expect(resolveLucideFallback('service')).toBe('server');
+ expect(resolveLucideFallback('frontend')).toBe('monitor');
+ expect(resolveLucideFallback('user')).toBe('user');
+ expect(resolveLucideFallback('gateway')).toBe('shield');
+ });
+
+ it('returns box for unknown categories', () => {
+ expect(resolveLucideFallback('unknown')).toBe('box');
+ });
+});
diff --git a/src/lib/iconResolver.ts b/src/lib/iconResolver.ts
new file mode 100644
index 00000000..35fb684f
--- /dev/null
+++ b/src/lib/iconResolver.ts
@@ -0,0 +1,425 @@
+import type { DomainLibraryCategory } from '@/services/domainLibrary';
+
+export interface IconResolution {
+ found: boolean;
+ archIconPackId?: string;
+ archIconShapeId?: string;
+ iconSearch?: string;
+ catalog?: DomainLibraryCategory;
+ lucideIcon?: string;
+ label?: string;
+ category?: string;
+ confidence: number;
+}
+
+interface AliasEntry {
+ patterns: RegExp[];
+ iconSearch: string;
+ catalog: DomainLibraryCategory;
+ lucideFallback: string;
+}
+
+const ALIAS_TABLE: AliasEntry[] = [
+ // Databases
+ {
+ patterns: [/^postgres(?:ql)?$/i, /^pg$/i],
+ iconSearch: 'postgresql',
+ catalog: 'developer',
+ lucideFallback: 'database',
+ },
+ { patterns: [/^mysql$/i], iconSearch: 'mysql', catalog: 'developer', lucideFallback: 'database' },
+ {
+ patterns: [/^mongo(?:db)?$/i],
+ iconSearch: 'mongodb',
+ catalog: 'developer',
+ lucideFallback: 'database',
+ },
+ {
+ patterns: [/^redis$/i],
+ iconSearch: 'redis',
+ catalog: 'developer',
+ lucideFallback: 'hard-drive',
+ },
+ {
+ patterns: [/^elastic(?:search)?$/i],
+ iconSearch: 'elasticsearch',
+ catalog: 'developer',
+ lucideFallback: 'search',
+ },
+ { patterns: [/^dynamodb$/i], iconSearch: 'dynamodb', catalog: 'aws', lucideFallback: 'database' },
+ { patterns: [/^aurora$/i], iconSearch: 'aurora', catalog: 'aws', lucideFallback: 'database' },
+ {
+ patterns: [/^sqlite$/i],
+ iconSearch: 'sqlite',
+ catalog: 'developer',
+ lucideFallback: 'database',
+ },
+ {
+ patterns: [/^mariadb$/i],
+ iconSearch: 'mariadb',
+ catalog: 'developer',
+ lucideFallback: 'database',
+ },
+ {
+ patterns: [/^cassandra$/i],
+ iconSearch: 'cassandra',
+ catalog: 'developer',
+ lucideFallback: 'database',
+ },
+ { patterns: [/^neo4j$/i], iconSearch: 'neo4j', catalog: 'developer', lucideFallback: 'database' },
+ {
+ patterns: [/^supabase$/i],
+ iconSearch: 'supabase',
+ catalog: 'developer',
+ lucideFallback: 'database',
+ },
+ {
+ patterns: [/^planetscale$/i],
+ iconSearch: 'planetscale',
+ catalog: 'developer',
+ lucideFallback: 'database',
+ },
+ { patterns: [/^neon\b/i], iconSearch: 'neon', catalog: 'developer', lucideFallback: 'database' },
+
+ // Frameworks
+ {
+ patterns: [/^express(?:\.?js)?$/i],
+ iconSearch: 'express',
+ catalog: 'developer',
+ lucideFallback: 'server',
+ },
+ {
+ patterns: [/^node(?:\.?js)?$/i],
+ iconSearch: 'nodejs',
+ catalog: 'developer',
+ lucideFallback: 'server',
+ },
+ {
+ patterns: [/^react(?:\.?js)?$/i],
+ iconSearch: 'react',
+ catalog: 'developer',
+ lucideFallback: 'monitor',
+ },
+ {
+ patterns: [/^vue(?:\.?js)?$/i],
+ iconSearch: 'vue',
+ catalog: 'developer',
+ lucideFallback: 'monitor',
+ },
+ {
+ patterns: [/^angular$/i],
+ iconSearch: 'angular',
+ catalog: 'developer',
+ lucideFallback: 'monitor',
+ },
+ {
+ patterns: [/^svelte$/i],
+ iconSearch: 'svelte',
+ catalog: 'developer',
+ lucideFallback: 'monitor',
+ },
+ {
+ patterns: [/^next(?:\.?js)?$/i],
+ iconSearch: 'nextjs',
+ catalog: 'developer',
+ lucideFallback: 'monitor',
+ },
+ { patterns: [/^nuxt$/i], iconSearch: 'nuxt', catalog: 'developer', lucideFallback: 'monitor' },
+ { patterns: [/^django$/i], iconSearch: 'django', catalog: 'developer', lucideFallback: 'server' },
+ { patterns: [/^flask$/i], iconSearch: 'flask', catalog: 'developer', lucideFallback: 'server' },
+ {
+ patterns: [/^fastapi$/i],
+ iconSearch: 'fastapi',
+ catalog: 'developer',
+ lucideFallback: 'server',
+ },
+ {
+ patterns: [/^spring(?:\s*boot)?$/i],
+ iconSearch: 'spring',
+ catalog: 'developer',
+ lucideFallback: 'server',
+ },
+ {
+ patterns: [/^rails$/i, /^ruby$/i],
+ iconSearch: 'rails',
+ catalog: 'developer',
+ lucideFallback: 'server',
+ },
+ {
+ patterns: [/^laravel$/i],
+ iconSearch: 'laravel',
+ catalog: 'developer',
+ lucideFallback: 'server',
+ },
+ {
+ patterns: [/^nest(?:\.?js)?$/i],
+ iconSearch: 'nestjs',
+ catalog: 'developer',
+ lucideFallback: 'server',
+ },
+ { patterns: [/^gin$/i], iconSearch: 'gin', catalog: 'developer', lucideFallback: 'server' },
+ {
+ patterns: [/^go$/i, /^golang$/i],
+ iconSearch: 'go',
+ catalog: 'developer',
+ lucideFallback: 'server',
+ },
+ { patterns: [/^rust$/i], iconSearch: 'rust', catalog: 'developer', lucideFallback: 'server' },
+ { patterns: [/^deno$/i], iconSearch: 'deno', catalog: 'developer', lucideFallback: 'server' },
+ { patterns: [/^bun$/i], iconSearch: 'bun', catalog: 'developer', lucideFallback: 'server' },
+
+ // Infrastructure
+ {
+ patterns: [/^docker$/i],
+ iconSearch: 'docker',
+ catalog: 'developer',
+ lucideFallback: 'container',
+ },
+ {
+ patterns: [/^kubernetes$/i, /^k8s$/i],
+ iconSearch: 'kubernetes',
+ catalog: 'cncf',
+ lucideFallback: 'container',
+ },
+ { patterns: [/^nginx$/i], iconSearch: 'nginx', catalog: 'developer', lucideFallback: 'shield' },
+ {
+ patterns: [/^rabbitmq$/i],
+ iconSearch: 'rabbitmq',
+ catalog: 'developer',
+ lucideFallback: 'layers',
+ },
+ {
+ patterns: [/^kafka$/i, /^apache\s*kafka$/i],
+ iconSearch: 'apachekafka',
+ catalog: 'developer',
+ lucideFallback: 'layers',
+ },
+ {
+ patterns: [/^consul$/i],
+ iconSearch: 'consul',
+ catalog: 'developer',
+ lucideFallback: 'map-pin',
+ },
+ { patterns: [/^vault$/i], iconSearch: 'vault', catalog: 'developer', lucideFallback: 'lock' },
+ {
+ patterns: [/^terraform$/i],
+ iconSearch: 'terraform',
+ catalog: 'developer',
+ lucideFallback: 'layers',
+ },
+ {
+ patterns: [/^ansible$/i],
+ iconSearch: 'ansible',
+ catalog: 'developer',
+ lucideFallback: 'settings',
+ },
+ {
+ patterns: [/^prometheus$/i],
+ iconSearch: 'prometheus',
+ catalog: 'developer',
+ lucideFallback: 'activity',
+ },
+ {
+ patterns: [/^grafana$/i],
+ iconSearch: 'grafana',
+ catalog: 'developer',
+ lucideFallback: 'bar-chart',
+ },
+ {
+ patterns: [/^jenkins$/i],
+ iconSearch: 'jenkins',
+ catalog: 'developer',
+ lucideFallback: 'settings',
+ },
+ {
+ patterns: [/^gitlab$/i],
+ iconSearch: 'gitlab',
+ catalog: 'developer',
+ lucideFallback: 'git-branch',
+ },
+ {
+ patterns: [/^github$/i],
+ iconSearch: 'github',
+ catalog: 'developer',
+ lucideFallback: 'git-branch',
+ },
+ { patterns: [/^helm$/i], iconSearch: 'helm', catalog: 'cncf', lucideFallback: 'package' },
+ { patterns: [/^istio$/i], iconSearch: 'istio', catalog: 'cncf', lucideFallback: 'network' },
+ { patterns: [/^envoy$/i], iconSearch: 'envoy', catalog: 'cncf', lucideFallback: 'network' },
+ {
+ patterns: [/^grafana\s*tempo$/i, /^tempo$/i],
+ iconSearch: 'grafana-tempo',
+ catalog: 'developer',
+ lucideFallback: 'activity',
+ },
+
+ // Cloud services
+ { patterns: [/^s3$/i], iconSearch: 's3', catalog: 'aws', lucideFallback: 'folder' },
+ { patterns: [/^lambda$/i], iconSearch: 'lambda', catalog: 'aws', lucideFallback: 'zap' },
+ { patterns: [/^ec2$/i], iconSearch: 'ec2', catalog: 'aws', lucideFallback: 'server' },
+ { patterns: [/^ecs$/i], iconSearch: 'ecs', catalog: 'aws', lucideFallback: 'container' },
+ { patterns: [/^eks$/i], iconSearch: 'eks', catalog: 'aws', lucideFallback: 'container' },
+ { patterns: [/^rds$/i], iconSearch: 'rds', catalog: 'aws', lucideFallback: 'database' },
+ {
+ patterns: [/^api\s*gateway$/i],
+ iconSearch: 'api-gateway',
+ catalog: 'aws',
+ lucideFallback: 'shield',
+ },
+ {
+ patterns: [/^cloudfront$/i],
+ iconSearch: 'cloudfront',
+ catalog: 'aws',
+ lucideFallback: 'globe',
+ },
+ { patterns: [/^sqs$/i], iconSearch: 'sqs', catalog: 'aws', lucideFallback: 'layers' },
+ { patterns: [/^sns$/i], iconSearch: 'sns', catalog: 'aws', lucideFallback: 'bell' },
+ { patterns: [/^cognito$/i], iconSearch: 'cognito', catalog: 'aws', lucideFallback: 'key' },
+ {
+ patterns: [/^cloud\s*run$/i],
+ iconSearch: 'cloud-run',
+ catalog: 'gcp',
+ lucideFallback: 'container',
+ },
+ {
+ patterns: [/^cloud\s*functions$/i],
+ iconSearch: 'cloud-functions',
+ catalog: 'gcp',
+ lucideFallback: 'zap',
+ },
+ { patterns: [/^bigquery$/i], iconSearch: 'bigquery', catalog: 'gcp', lucideFallback: 'database' },
+ {
+ patterns: [/^azure\s*functions$/i],
+ iconSearch: 'azure-functions',
+ catalog: 'azure',
+ lucideFallback: 'zap',
+ },
+ {
+ patterns: [/^azure\s*sql$/i],
+ iconSearch: 'azure-sql',
+ catalog: 'azure',
+ lucideFallback: 'database',
+ },
+
+ // Messaging / Streaming
+ { patterns: [/^pulsar$/i], iconSearch: 'pulsar', catalog: 'developer', lucideFallback: 'layers' },
+ { patterns: [/^nats$/i], iconSearch: 'nats', catalog: 'developer', lucideFallback: 'layers' },
+ {
+ patterns: [/^zeromq$/i, /^0mq$/i],
+ iconSearch: 'zeromq',
+ catalog: 'developer',
+ lucideFallback: 'layers',
+ },
+
+ // Auth
+ { patterns: [/^auth0$/i], iconSearch: 'auth0', catalog: 'developer', lucideFallback: 'key' },
+ {
+ patterns: [/^keycloak$/i],
+ iconSearch: 'keycloak',
+ catalog: 'developer',
+ lucideFallback: 'key',
+ },
+ {
+ patterns: [/^firebase$/i],
+ iconSearch: 'firebase',
+ catalog: 'developer',
+ lucideFallback: 'flame',
+ },
+ {
+ patterns: [/^supertokens$/i, /^super\s*tokens$/i],
+ iconSearch: 'supertokens',
+ catalog: 'developer',
+ lucideFallback: 'key',
+ },
+
+ // Payments / SaaS
+ {
+ patterns: [/^stripe$/i],
+ iconSearch: 'stripe',
+ catalog: 'developer',
+ lucideFallback: 'credit-card',
+ },
+ { patterns: [/^twilio$/i], iconSearch: 'twilio', catalog: 'developer', lucideFallback: 'phone' },
+ {
+ patterns: [/^sendgrid$/i],
+ iconSearch: 'sendgrid',
+ catalog: 'developer',
+ lucideFallback: 'mail',
+ },
+ {
+ patterns: [/^mailchimp$/i],
+ iconSearch: 'mailchimp',
+ catalog: 'developer',
+ lucideFallback: 'mail',
+ },
+ {
+ patterns: [/^cloudflare$/i],
+ iconSearch: 'cloudflare',
+ catalog: 'developer',
+ lucideFallback: 'cloud',
+ },
+ {
+ patterns: [/^vercel$/i],
+ iconSearch: 'vercel',
+ catalog: 'developer',
+ lucideFallback: 'triangle',
+ },
+ {
+ patterns: [/^netlify$/i],
+ iconSearch: 'netlify',
+ catalog: 'developer',
+ lucideFallback: 'globe',
+ },
+];
+
+const LUCIDE_FALLBACK_MAP: Record = {
+ database: 'database',
+ cache: 'hard-drive',
+ queue: 'layers',
+ service: 'server',
+ frontend: 'monitor',
+ gateway: 'shield',
+ auth: 'key-round',
+ storage: 'folder',
+ user: 'user',
+ start: 'play',
+ end: 'check-circle',
+ decision: 'help-circle',
+ action: 'zap',
+ process: 'box',
+};
+
+export function resolveIconSync(query: string, categoryHint?: string): IconResolution {
+ const trimmed = query.trim();
+ if (!trimmed) {
+ return { found: false, confidence: 0 };
+ }
+
+ for (const entry of ALIAS_TABLE) {
+ if (entry.patterns.some((p) => p.test(trimmed))) {
+ return {
+ found: true,
+ iconSearch: entry.iconSearch,
+ catalog: entry.catalog,
+ lucideIcon: entry.lucideFallback,
+ label: trimmed,
+ confidence: 0.95,
+ };
+ }
+ }
+
+ if (categoryHint && LUCIDE_FALLBACK_MAP[categoryHint]) {
+ return {
+ found: true,
+ lucideIcon: LUCIDE_FALLBACK_MAP[categoryHint],
+ label: trimmed,
+ confidence: 0.5,
+ };
+ }
+
+ return { found: false, confidence: 0 };
+}
+
+export function resolveLucideFallback(category: string): string {
+ return LUCIDE_FALLBACK_MAP[category] ?? 'box';
+}
diff --git a/src/lib/mermaidEnrichmentPipeline.test.ts b/src/lib/mermaidEnrichmentPipeline.test.ts
new file mode 100644
index 00000000..e5be9d29
--- /dev/null
+++ b/src/lib/mermaidEnrichmentPipeline.test.ts
@@ -0,0 +1,189 @@
+import { describe, expect, it } from 'vitest';
+import { parseMermaidByType } from '@/services/mermaid/parseMermaidByType';
+import { enrichNodesWithIcons } from '@/lib/nodeEnricher';
+
+describe('Mermaid โ Enrichment Pipeline (E2E)', () => {
+ it('flowchart: assigns colors and icons to all node types', async () => {
+ const mermaid = `
+ flowchart TD
+ S([Start]) --> login[Login Form]
+ login --> valid{Credentials Valid?}
+ valid -->|Yes| db[(PostgreSQL)]
+ valid -->|No| fail((Access Denied))
+ db --> redis[Redis Cache]
+ redis --> done((Dashboard))
+ `;
+
+ const parsed = parseMermaidByType(mermaid);
+ expect(parsed.error).toBeUndefined();
+ expect(parsed.nodes.length).toBeGreaterThan(0);
+
+ const enriched = await enrichNodesWithIcons(parsed.nodes);
+
+ const startNode = enriched.find((n) => n.id === 'S');
+ expect(startNode?.data.color).toBe('emerald');
+ expect(startNode?.data.icon).toBe('play');
+
+ const endNode = enriched.find((n) => n.id === 'fail');
+ expect(endNode?.data.color).toBe('red');
+ expect(endNode?.data.icon).toBe('check-circle');
+
+ const decisionNode = enriched.find((n) => n.id === 'valid');
+ expect(decisionNode?.data.color).toBe('amber');
+ expect(decisionNode?.data.icon).toBe('help-circle');
+
+ const dbNode = enriched.find((n) => n.id === 'db');
+ expect(dbNode?.data.color).toBe('violet');
+ expect(dbNode?.data.icon).toBe('database');
+
+ const redisNode = enriched.find((n) => n.id === 'redis');
+ expect(redisNode?.data.color).toBe('red');
+ expect(redisNode?.data.icon).toBe('hard-drive');
+ });
+
+ it('flowchart with subgraphs: creates section nodes with proper hierarchy', async () => {
+ const mermaid = `
+ flowchart TD
+ subgraph Backend
+ API[Express API]
+ DB[(PostgreSQL)]
+ end
+ subgraph Frontend
+ UI[React App]
+ end
+ UI --> API
+ API --> DB
+ `;
+
+ const parsed = parseMermaidByType(mermaid);
+ expect(parsed.error).toBeUndefined();
+
+ const enriched = await enrichNodesWithIcons(parsed.nodes);
+
+ const sectionNodes = enriched.filter((n) => n.type === 'section');
+ expect(sectionNodes).toHaveLength(2);
+
+ const backendSection = sectionNodes.find((n) => n.data.label === 'Backend');
+ expect(backendSection).toBeDefined();
+
+ const apiNode = enriched.find((n) => n.id === 'API');
+ expect(apiNode?.parentId).toBe(backendSection?.id);
+ expect(apiNode?.data.color).toBe('blue');
+
+ const dbNode = enriched.find((n) => n.id === 'DB');
+ expect(dbNode?.parentId).toBe(backendSection?.id);
+ expect(dbNode?.data.color).toBe('violet');
+ });
+
+ it('sequence diagram: parses participants and messages', async () => {
+ const mermaid = `
+ sequenceDiagram
+ participant Client
+ participant Server
+ participant Database
+ Client->>Server: HTTP Request
+ Server->>Database: SQL Query
+ Database-->>Server: Results
+ Server-->>Client: JSON Response
+ `;
+
+ const parsed = parseMermaidByType(mermaid);
+ expect(parsed.error).toBeUndefined();
+ expect(parsed.diagramType).toBe('sequence');
+ expect(parsed.nodes.length).toBeGreaterThanOrEqual(3);
+ expect(parsed.edges.length).toBeGreaterThanOrEqual(4);
+ });
+
+ it('sequence diagram: handles fragments (alt/loop) and activations', async () => {
+ const mermaid = `
+ sequenceDiagram
+ participant A
+ participant B
+ A->>B: Request
+ activate B
+ alt success
+ B-->>A: 200 OK
+ else failure
+ B-->>A: 500 Error
+ end
+ loop every minute
+ A->>B: Heartbeat
+ end
+ deactivate B
+ `;
+
+ const parsed = parseMermaidByType(mermaid);
+ expect(parsed.error).toBeUndefined();
+ expect(parsed.diagramType).toBe('sequence');
+ expect(parsed.nodes.length).toBeGreaterThanOrEqual(2);
+ });
+
+ it('sequence diagram: handles notes over participants', async () => {
+ const mermaid = `
+ sequenceDiagram
+ participant A
+ participant B
+ Note over A,B: This is a note
+ A->>B: Message
+ `;
+
+ const parsed = parseMermaidByType(mermaid);
+ expect(parsed.error).toBeUndefined();
+ expect(parsed.nodes.length).toBeGreaterThanOrEqual(2);
+ });
+
+ it('architecture diagram: preserves archIconPackId when set by AI', async () => {
+ // Simulate AI-generated nodes with provider icons already set
+ const aiGeneratedNodes = [
+ {
+ id: 'api_gw',
+ type: 'architecture' as const,
+ position: { x: 0, y: 0 },
+ data: {
+ label: 'API Gateway',
+ subLabel: '',
+ color: 'violet',
+ archIconPackId: 'aws-official-starter-v1',
+ archIconShapeId: 'api-gateway',
+ assetPresentation: 'icon' as const,
+ },
+ },
+ ];
+
+ const enriched = await enrichNodesWithIcons(aiGeneratedNodes);
+
+ // Enricher should preserve existing archIconPackId
+ expect(enriched[0].data.archIconPackId).toBe('aws-official-starter-v1');
+ expect(enriched[0].data.archIconShapeId).toBe('api-gateway');
+ expect(enriched[0].data.color).toBe('violet');
+ });
+
+ it('does not modify section nodes', async () => {
+ const mermaid = `
+ flowchart TD
+ subgraph Group A
+ A[Node A]
+ end
+ `;
+
+ const parsed = parseMermaidByType(mermaid);
+ const enriched = await enrichNodesWithIcons(parsed.nodes);
+
+ const sectionNode = enriched.find((n) => n.type === 'section');
+ expect(sectionNode?.data.icon).toBeUndefined();
+ expect(sectionNode?.data.archIconPackId).toBeUndefined();
+ });
+
+ it('edge labels are preserved through parse+enrich', async () => {
+ const mermaid = `
+ flowchart TD
+ A[Start] -->|Yes| B[Process]
+ A -->|No| C[End]
+ `;
+
+ const parsed = parseMermaidByType(mermaid);
+ expect(parsed.edges).toHaveLength(2);
+ expect(parsed.edges[0].label).toBe('Yes');
+ expect(parsed.edges[1].label).toBe('No');
+ });
+});
diff --git a/src/lib/mermaidParser.ts b/src/lib/mermaidParser.ts
index 3b74e02b..ee31fcbc 100644
--- a/src/lib/mermaidParser.ts
+++ b/src/lib/mermaidParser.ts
@@ -9,341 +9,377 @@ import {
type MermaidParseModel,
} from './mermaidParserModel';
import {
- ARROW_PATTERNS,
- CLASS_DEF_RE,
- parseEdgeLine,
- parseLinkStyleLine,
- parseNodeDeclaration,
- parseStyleString,
- SKIP_PATTERNS,
- STYLE_RE,
- normalizeEdgeLabels,
- normalizeMultilineStrings,
+ ARROW_PATTERNS,
+ CLASS_DEF_RE,
+ parseEdgeLine,
+ parseLinkStyleLine,
+ parseNodeDeclaration,
+ parseStyleString,
+ SKIP_PATTERNS,
+ STYLE_RE,
+ normalizeEdgeLabels,
+ normalizeMultilineStrings,
} from './mermaidParserHelpers';
import type { FlowEdge, FlowNode } from './types';
const NODE_TYPE_DEFAULTS: Record = {
- start: 'emerald',
- end: 'red',
- decision: 'amber',
- custom: 'violet',
- process: 'slate',
+ start: 'emerald',
+ end: 'red',
+ decision: 'amber',
+ custom: 'violet',
+ process: 'slate',
};
function getDefaultColor(type: string): string {
- return NODE_TYPE_DEFAULTS[type] || 'slate';
+ return NODE_TYPE_DEFAULTS[type] || 'slate';
}
export interface ParseResult {
- nodes: FlowNode[];
- edges: FlowEdge[];
- error?: string;
- direction?: MermaidDirection;
+ nodes: FlowNode[];
+ edges: FlowEdge[];
+ error?: string;
+ direction?: MermaidDirection;
}
function preprocessMermaidInput(input: string): string[] {
- const processed = normalizeEdgeLabels(normalizeMultilineStrings(input.replace(/\r\n/g, '\n')));
- return processed.split('\n');
+ const processed = normalizeEdgeLabels(normalizeMultilineStrings(input.replace(/\r\n/g, '\n')));
+ return processed.split('\n');
}
function isSkippableLine(line: string): boolean {
- return !line || SKIP_PATTERNS.some((pattern) => pattern.test(line));
+ return !line || SKIP_PATTERNS.some((pattern) => pattern.test(line));
}
function parseFlowchartDeclaration(line: string): MermaidDirection | null {
- const flowchartMatch = line.match(/^(?:flowchart|graph)\s+(TD|TB|LR|RL|BT)/i);
- if (!flowchartMatch) {
- return null;
- }
-
- return (flowchartMatch[1].toUpperCase() === 'TD'
- ? 'TB'
- : flowchartMatch[1].toUpperCase()) as MermaidDirection;
+ const flowchartMatch = line.match(/^(?:flowchart|graph)\s+(TD|TB|LR|RL|BT)/i);
+ if (!flowchartMatch) {
+ return null;
+ }
+
+ return (
+ flowchartMatch[1].toUpperCase() === 'TD' ? 'TB' : flowchartMatch[1].toUpperCase()
+ ) as MermaidDirection;
}
function parseStateDiagramDirection(nextLine: string | undefined): MermaidDirection {
- const dirMatch = nextLine?.trim().match(/^direction\s+(LR|TB)/i);
- return (dirMatch?.[1].toUpperCase() ?? 'TB') as MermaidDirection;
+ const dirMatch = nextLine?.trim().match(/^direction\s+(LR|TB)/i);
+ return (dirMatch?.[1].toUpperCase() ?? 'TB') as MermaidDirection;
}
function registerSectionNode(
- state: ReturnType,
- line: string
+ state: ReturnType,
+ line: string
): boolean {
- const subgraphMatch = line.match(/^subgraph\s+(.+)$/i);
- const stateGroupMatch =
- line.match(/^state\s+"([^"]+)"\s+as\s+(\w+)\s+\{/i) ||
- line.match(/^state\s+(\w+)\s+\{/i);
+ const subgraphMatch = line.match(/^subgraph\s+(.+)$/i);
+ const stateGroupMatch =
+ line.match(/^state\s+"([^"]+)"\s+as\s+(\w+)\s+\{/i) || line.match(/^state\s+(\w+)\s+\{/i);
- if (!subgraphMatch && !stateGroupMatch) {
- return false;
- }
- void state;
- return true;
+ if (!subgraphMatch && !stateGroupMatch) {
+ return false;
+ }
+
+ let sectionId: string;
+ let sectionLabel: string;
+
+ if (subgraphMatch) {
+ sectionLabel = subgraphMatch[1].trim();
+ sectionId = `subgraph_${sectionLabel.replace(/[^a-zA-Z0-9_]/g, '_')}`;
+ } else if (stateGroupMatch) {
+ sectionId = stateGroupMatch[2] ?? stateGroupMatch[1];
+ sectionLabel = stateGroupMatch[1] ?? stateGroupMatch[2];
+ } else {
+ return false;
+ }
+
+ let attempts = 0;
+ let finalId = sectionId;
+ while (state.nodesMap.has(finalId)) {
+ finalId = `${sectionId}_${++attempts}`;
+ }
+
+ const parentId = state.parentStack[state.parentStack.length - 1];
+ state.nodesMap.set(finalId, {
+ id: finalId,
+ label: sectionLabel,
+ type: 'section',
+ parentId,
+ });
+ state.parentStack.push(finalId);
+
+ return true;
}
function applyNodeStyleDirective(
- state: ReturnType,
- line: string
+ state: ReturnType,
+ line: string
): boolean {
- const styleMatch = line.match(STYLE_RE);
- if (!styleMatch) {
- return false;
- }
-
- const [, id, styleStr] = styleMatch;
- const styles = parseStyleString(styleStr);
- const node = state.nodesMap.get(id);
- if (node) {
- node.styles = { ...node.styles, ...styles };
- } else {
- registerMermaidNode(state, id);
- const registeredNode = state.nodesMap.get(id);
- if (registeredNode) {
- registeredNode.styles = styles;
- }
+ const styleMatch = line.match(STYLE_RE);
+ if (!styleMatch) {
+ return false;
+ }
+
+ const [, id, styleStr] = styleMatch;
+ const styles = parseStyleString(styleStr);
+ const node = state.nodesMap.get(id);
+ if (node) {
+ node.styles = { ...node.styles, ...styles };
+ } else {
+ registerMermaidNode(state, id);
+ const registeredNode = state.nodesMap.get(id);
+ if (registeredNode) {
+ registeredNode.styles = styles;
}
+ }
- return true;
+ return true;
}
function parseEdgeDeclaration(
- state: ReturnType,
- line: string
+ state: ReturnType,
+ line: string
): boolean {
- if (!ARROW_PATTERNS.some((arrow) => line.includes(arrow))) {
- return false;
+ if (!ARROW_PATTERNS.some((arrow) => line.includes(arrow))) {
+ return false;
+ }
+
+ const edgesFound = parseEdgeLine(line);
+ edgesFound.forEach((edge) => {
+ const type = state.diagramType === 'stateDiagram' ? 'state' : 'process';
+ const sourceId = registerMermaidNode(state, edge.sourceRaw, type);
+ const targetId = registerMermaidNode(state, edge.targetRaw, type);
+
+ if (sourceId && targetId) {
+ state.rawEdges.push({
+ source: sourceId,
+ target: targetId,
+ label: edge.label,
+ arrowType: edge.arrowType,
+ });
}
+ });
- const edgesFound = parseEdgeLine(line);
- edgesFound.forEach((edge) => {
- const type = state.diagramType === 'stateDiagram' ? 'state' : 'process';
- const sourceId = registerMermaidNode(state, edge.sourceRaw, type);
- const targetId = registerMermaidNode(state, edge.targetRaw, type);
-
- if (sourceId && targetId) {
- state.rawEdges.push({
- source: sourceId,
- target: targetId,
- label: edge.label,
- arrowType: edge.arrowType,
- });
- }
- });
-
- return true;
+ return true;
}
function parseStateDiagramNodeDeclaration(
- state: ReturnType,
- line: string
+ state: ReturnType,
+ line: string
): boolean {
- if (state.diagramType !== 'stateDiagram') {
- return false;
- }
+ if (state.diagramType !== 'stateDiagram') {
+ return false;
+ }
- const stateDefMatch = line.match(/^state\s+"([^"]+)"\s+as\s+(\w+)/i);
- if (stateDefMatch) {
- registerMermaidNode(state, stateDefMatch[2], 'state', stateDefMatch[1]);
- return true;
- }
+ const stateDefMatch = line.match(/^state\s+"([^"]+)"\s+as\s+(\w+)/i);
+ if (stateDefMatch) {
+ registerMermaidNode(state, stateDefMatch[2], 'state', stateDefMatch[1]);
+ return true;
+ }
- const stateDescMatch = line.match(/^(\w+)\s*:\s*(.+)/);
- if (stateDescMatch) {
- registerMermaidNode(state, stateDescMatch[1], 'state', stateDescMatch[2]);
- return true;
- }
+ const stateDescMatch = line.match(/^(\w+)\s*:\s*(.+)/);
+ if (stateDescMatch) {
+ registerMermaidNode(state, stateDescMatch[1], 'state', stateDescMatch[2]);
+ return true;
+ }
- return false;
+ return false;
}
function buildMermaidParseModel(lines: string[]): MermaidParseModel {
- const state = createMermaidParseState();
+ const state = createMermaidParseState();
- for (let i = 0; i < lines.length; i++) {
- const line = lines[i].trim();
- if (isSkippableLine(line)) {
- continue;
- }
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i].trim();
+ if (isSkippableLine(line)) {
+ continue;
+ }
- const flowchartDirection = parseFlowchartDeclaration(line);
- if (flowchartDirection) {
- state.diagramType = 'flowchart';
- state.direction = flowchartDirection;
- continue;
- }
+ const flowchartDirection = parseFlowchartDeclaration(line);
+ if (flowchartDirection) {
+ state.diagramType = 'flowchart';
+ state.direction = flowchartDirection;
+ continue;
+ }
- if (line.match(/^stateDiagram(?:-v2)?/i)) {
- state.diagramType = 'stateDiagram';
- state.direction = parseStateDiagramDirection(lines[i + 1]);
- continue;
- }
+ if (line.match(/^stateDiagram(?:-v2)?/i)) {
+ state.diagramType = 'stateDiagram';
+ state.direction = parseStateDiagramDirection(lines[i + 1]);
+ continue;
+ }
- if (line.match(/^end\s*$/i) || line === '}') {
- if (state.parentStack.length > 0) {
- state.parentStack.pop();
- }
- continue;
- }
+ if (line.match(/^end\s*$/i) || line === '}') {
+ if (state.parentStack.length > 0) {
+ state.parentStack.pop();
+ }
+ continue;
+ }
- if (registerSectionNode(state, line)) {
- continue;
- }
+ if (registerSectionNode(state, line)) {
+ continue;
+ }
- const classDefMatch = line.match(CLASS_DEF_RE);
- if (classDefMatch) {
- state.classDefs.set(classDefMatch[1], parseStyleString(classDefMatch[2]));
- continue;
- }
+ const classDefMatch = line.match(CLASS_DEF_RE);
+ if (classDefMatch) {
+ state.classDefs.set(classDefMatch[1], parseStyleString(classDefMatch[2]));
+ continue;
+ }
- if (applyNodeStyleDirective(state, line)) {
- continue;
- }
+ if (applyNodeStyleDirective(state, line)) {
+ continue;
+ }
- const linkStyleMatch = parseLinkStyleLine(line);
- if (linkStyleMatch) {
- linkStyleMatch.indices.forEach((index) => state.linkStyles.set(index, linkStyleMatch.style));
- continue;
- }
+ const linkStyleMatch = parseLinkStyleLine(line);
+ if (linkStyleMatch) {
+ linkStyleMatch.indices.forEach((index) => state.linkStyles.set(index, linkStyleMatch.style));
+ continue;
+ }
- if (parseEdgeDeclaration(state, line)) {
- continue;
- }
+ if (parseEdgeDeclaration(state, line)) {
+ continue;
+ }
- if (parseStateDiagramNodeDeclaration(state, line)) {
- continue;
- }
+ if (parseStateDiagramNodeDeclaration(state, line)) {
+ continue;
+ }
- const standalone = parseNodeDeclaration(line);
- if (standalone) {
- registerMermaidNode(state, line);
- }
+ const standalone = parseNodeDeclaration(line);
+ if (standalone) {
+ registerMermaidNode(state, line);
}
+ }
- return toMermaidParseModel(state);
+ return toMermaidParseModel(state);
}
function createFlowNodes(model: MermaidParseModel): FlowNode[] {
- return Array.from(model.nodesMap.values()).map((node, index) => {
- let flowNode: FlowNode = {
- id: node.id,
- type: node.type,
- position: { x: (index % 4) * 200, y: Math.floor(index / 4) * 150 },
- data: {
- label: node.label,
- subLabel: '',
- color: getDefaultColor(node.type),
- ...(node.shape ? { shape: node.shape } : {}),
- },
- };
+ return Array.from(model.nodesMap.values()).map((node, index) => {
+ let flowNode: FlowNode = {
+ id: node.id,
+ type: node.type,
+ position: { x: (index % 4) * 200, y: Math.floor(index / 4) * 150 },
+ data: {
+ label: node.label,
+ subLabel: '',
+ color: getDefaultColor(node.type),
+ ...(node.shape ? { shape: node.shape } : {}),
+ },
+ ...(node.type === 'section'
+ ? {
+ style: { width: 400, height: 300 },
+ }
+ : {}),
+ };
- if (node.parentId) {
- flowNode = setNodeParent(flowNode, node.parentId);
- }
+ if (node.parentId) {
+ flowNode = setNodeParent(flowNode, node.parentId);
+ }
- if (node.classes) {
- node.classes.forEach((cls) => {
- const styles = model.classDefs.get(cls);
- if (!styles) {
- return;
- }
- if (styles.fill) {
- flowNode.style = { ...flowNode.style, backgroundColor: styles.fill };
- }
- if (styles.stroke) {
- flowNode.style = { ...flowNode.style, borderColor: styles.stroke };
- }
- if (styles.color) {
- flowNode.style = { ...flowNode.style, color: styles.color };
- }
- });
+ if (node.classes) {
+ node.classes.forEach((cls) => {
+ const styles = model.classDefs.get(cls);
+ if (!styles) {
+ return;
}
-
- if (node.styles) {
- if (node.styles.fill) {
- flowNode.style = { ...flowNode.style, backgroundColor: node.styles.fill };
- }
- if (node.styles.stroke) {
- flowNode.style = { ...flowNode.style, borderColor: node.styles.stroke };
- }
- if (node.styles.color) {
- flowNode.style = { ...flowNode.style, color: node.styles.color };
- }
+ if (styles.fill) {
+ flowNode.style = { ...flowNode.style, backgroundColor: styles.fill };
}
-
- if (model.diagramType === 'stateDiagram') {
- if (node.type === 'start') {
- flowNode.style = {
- ...flowNode.style,
- width: 20,
- height: 20,
- borderRadius: '50%',
- backgroundColor: '#000',
- };
- flowNode.data.label = '';
- }
- if (node.type === 'state') {
- flowNode.data.shape = 'rounded';
- }
+ if (styles.stroke) {
+ flowNode.style = { ...flowNode.style, borderColor: styles.stroke };
}
+ if (styles.color) {
+ flowNode.style = { ...flowNode.style, color: styles.color };
+ }
+ });
+ }
+
+ if (node.styles) {
+ if (node.styles.fill) {
+ flowNode.style = { ...flowNode.style, backgroundColor: node.styles.fill };
+ }
+ if (node.styles.stroke) {
+ flowNode.style = { ...flowNode.style, borderColor: node.styles.stroke };
+ }
+ if (node.styles.color) {
+ flowNode.style = { ...flowNode.style, color: node.styles.color };
+ }
+ }
+
+ if (model.diagramType === 'stateDiagram') {
+ if (node.type === 'start') {
+ flowNode.style = {
+ ...flowNode.style,
+ width: 20,
+ height: 20,
+ borderRadius: '50%',
+ backgroundColor: '#000',
+ };
+ flowNode.data.label = '';
+ }
+ if (node.type === 'state') {
+ flowNode.data.shape = 'rounded';
+ }
+ }
- return flowNode;
- });
+ return flowNode;
+ });
}
function createFlowEdges(model: MermaidParseModel): FlowEdge[] {
- return model.rawEdges.map((edge, index) => {
- const flowEdge = createDefaultEdge(
- edge.source,
- edge.target,
- edge.label || undefined,
- `e-mermaid-${index}`
- );
-
- if (edge.arrowType.includes('-.') || edge.arrowType.includes('-.-')) {
- flowEdge.style = { ...flowEdge.style, strokeDasharray: '5 3' };
- }
- if (edge.arrowType.includes('==')) {
- flowEdge.style = { ...flowEdge.style, strokeWidth: 4 };
- }
- if (edge.arrowType.startsWith('<')) {
- flowEdge.markerStart = { type: MarkerType.ArrowClosed };
- }
- if (!edge.arrowType.includes('>')) {
- flowEdge.markerEnd = undefined;
- }
+ return model.rawEdges.map((edge, index) => {
+ const flowEdge = createDefaultEdge(
+ edge.source,
+ edge.target,
+ edge.label || undefined,
+ `e-mermaid-${index}`
+ );
+
+ if (edge.arrowType.includes('-.') || edge.arrowType.includes('-.-')) {
+ flowEdge.style = { ...flowEdge.style, strokeDasharray: '5 3' };
+ }
+ if (edge.arrowType.includes('==')) {
+ flowEdge.style = { ...flowEdge.style, strokeWidth: 4 };
+ }
+ if (edge.arrowType.startsWith('<')) {
+ flowEdge.markerStart = { type: MarkerType.ArrowClosed };
+ }
+ if (!edge.arrowType.includes('>')) {
+ flowEdge.markerEnd = undefined;
+ }
- const style = model.linkStyles.get(index);
- if (style) {
- if (style.stroke) {
- flowEdge.style = { ...flowEdge.style, stroke: style.stroke };
- }
- if (style['stroke-width']) {
- flowEdge.style = {
- ...flowEdge.style,
- strokeWidth: parseInt(style['stroke-width'], 10) || 2,
- };
- }
- }
+ const style = model.linkStyles.get(index);
+ if (style) {
+ if (style.stroke) {
+ flowEdge.style = { ...flowEdge.style, stroke: style.stroke };
+ }
+ if (style['stroke-width']) {
+ flowEdge.style = {
+ ...flowEdge.style,
+ strokeWidth: parseInt(style['stroke-width'], 10) || 2,
+ };
+ }
+ }
- return flowEdge;
- });
+ return flowEdge;
+ });
}
export function parseMermaid(input: string): ParseResult {
- const model = buildMermaidParseModel(preprocessMermaidInput(input));
-
- if (model.diagramType === 'unknown') {
- return { nodes: [], edges: [], error: 'Missing chart type declaration. Start with "flowchart TD" or related.' };
- }
-
- if (model.nodesMap.size === 0) {
- return { nodes: [], edges: [], error: 'No valid nodes found.' };
- }
+ const model = buildMermaidParseModel(preprocessMermaidInput(input));
+ if (model.diagramType === 'unknown') {
return {
- nodes: createFlowNodes(model),
- edges: createFlowEdges(model),
- direction: model.direction,
+ nodes: [],
+ edges: [],
+ error: 'Missing chart type declaration. Start with "flowchart TD" or related.',
};
+ }
+
+ if (model.nodesMap.size === 0) {
+ return { nodes: [], edges: [], error: 'No valid nodes found.' };
+ }
+
+ return {
+ nodes: createFlowNodes(model),
+ edges: createFlowEdges(model),
+ direction: model.direction,
+ };
}
diff --git a/src/lib/mermaidParserHelpers.ts b/src/lib/mermaidParserHelpers.ts
index 4ee7bdc6..a0a73ccd 100644
--- a/src/lib/mermaidParserHelpers.ts
+++ b/src/lib/mermaidParserHelpers.ts
@@ -1,23 +1,28 @@
import type { NodeData } from './types';
-export const SHAPE_OPENERS: Array<{ open: string; close: string; type: string; shape: NodeData['shape'] }> = [
- { open: '([', close: '])', type: 'start', shape: 'capsule' },
- { open: '((', close: '))', type: 'end', shape: 'circle' },
- { open: '{{', close: '}}', type: 'custom', shape: 'hexagon' },
- { open: '[(', close: ')]', type: 'process', shape: 'cylinder' },
- { open: '{', close: '}', type: 'decision', shape: 'diamond' },
- { open: '[', close: ']', type: 'process', shape: 'rounded' },
- { open: '(', close: ')', type: 'process', shape: 'rounded' },
- { open: '>', close: ']', type: 'process', shape: 'parallelogram' },
+export const SHAPE_OPENERS: Array<{
+ open: string;
+ close: string;
+ type: string;
+ shape: NodeData['shape'];
+}> = [
+ { open: '([', close: '])', type: 'start', shape: 'capsule' },
+ { open: '((', close: '))', type: 'end', shape: 'circle' },
+ { open: '{{', close: '}}', type: 'custom', shape: 'hexagon' },
+ { open: '[(', close: ')]', type: 'process', shape: 'cylinder' },
+ { open: '{', close: '}', type: 'decision', shape: 'diamond' },
+ { open: '[', close: ']', type: 'process', shape: 'rounded' },
+ { open: '(', close: ')', type: 'process', shape: 'rounded' },
+ { open: '>', close: ']', type: 'process', shape: 'parallelogram' },
];
export const SKIP_PATTERNS = [
- /^%%/,
- /^class\s/i,
- /^click\s/i,
- /^direction\s/i,
- /^accTitle\s/i,
- /^accDescr\s/i,
+ /^%%/,
+ /^class\s/i,
+ /^click\s/i,
+ /^direction\s/i,
+ /^accTitle\s/i,
+ /^accDescr\s/i,
];
const LINK_STYLE_RE = /^linkStyle\s+([\d,\s]+)\s+(.+)$/i;
@@ -26,232 +31,310 @@ const STYLE_RE = /^style\s+(\w+)\s+(.+)$/i;
export { CLASS_DEF_RE, STYLE_RE };
-export function parseLinkStyleLine(line: string): { indices: number[]; style: Record } | null {
- const match = line.match(LINK_STYLE_RE);
- if (!match) return null;
+export function parseLinkStyleLine(
+ line: string
+): { indices: number[]; style: Record } | null {
+ const match = line.match(LINK_STYLE_RE);
+ if (!match) return null;
- const indices = match[1]
- .split(',')
- .map((s) => parseInt(s.trim(), 10))
- .filter((n) => !Number.isNaN(n));
+ const indices = match[1]
+ .split(',')
+ .map((s) => parseInt(s.trim(), 10))
+ .filter((n) => !Number.isNaN(n));
- const styleParts = match[2].replace(/;$/, '').split(',');
- const style: Record = {};
+ const styleParts = match[2].replace(/;$/, '').split(',');
+ const style: Record = {};
- for (const part of styleParts) {
- const [key, value] = part.split(':').map((s) => s.trim());
- if (key && value) {
- style[key] = value;
- }
+ for (const part of styleParts) {
+ const [key, value] = part.split(':').map((s) => s.trim());
+ if (key && value) {
+ style[key] = value;
}
+ }
- return { indices, style };
+ return { indices, style };
}
export function normalizeMultilineStrings(input: string): string {
- let result = '';
- let inQuote = false;
-
- for (let i = 0; i < input.length; i++) {
- const char = input[i];
- if (char === '"' && input[i - 1] !== '\\') {
- inQuote = !inQuote;
- }
-
- if (inQuote && char === '\n') {
- result += '\\n';
- let nextIndex = i + 1;
- while (nextIndex < input.length && (input[nextIndex] === ' ' || input[nextIndex] === '\t')) {
- nextIndex++;
- }
- i = nextIndex - 1;
- } else {
- result += char;
- }
+ let result = '';
+ let inQuote = false;
+
+ for (let i = 0; i < input.length; i++) {
+ const char = input[i];
+ if (char === '"' && input[i - 1] !== '\\') {
+ inQuote = !inQuote;
+ }
+
+ if (inQuote && char === '\n') {
+ result += '\\n';
+ let nextIndex = i + 1;
+ while (nextIndex < input.length && (input[nextIndex] === ' ' || input[nextIndex] === '\t')) {
+ nextIndex++;
+ }
+ i = nextIndex - 1;
+ } else {
+ result += char;
}
+ }
- return result;
+ return result;
}
export function normalizeEdgeLabels(input: string): string {
- let result = input;
- result = result.replace(/==(?![>])\s*(.+?)\s*==>/g, ' ==>|$1|');
- result = result.replace(/--(?![>-])\s*(.+?)\s*-->/g, ' -->|$1|');
- result = result.replace(/-\.\s*(.+?)\s*\.->/g, ' -.->|$1|');
- result = result.replace(/--(?![>-])\s*(.+?)\s*---/g, ' ---|$1|');
- return result;
+ let result = input;
+ result = result.replace(/==(?![>])\s*(.+?)\s*==>/g, ' ==>|$1|');
+ result = result.replace(/--(?![>-])\s*(.+?)\s*-->/g, ' -->|$1|');
+ result = result.replace(/-\.\s*(.+?)\s*\.->/g, ' -.->|$1|');
+ result = result.replace(/--(?![>-])\s*(.+?)\s*---/g, ' ---|$1|');
+ return result;
}
export interface RawNode {
- id: string;
- label: string;
- type: string;
- shape?: NodeData['shape'];
- parentId?: string;
- styles?: Record;
- classes?: string[];
+ id: string;
+ label: string;
+ type: string;
+ shape?: NodeData['shape'];
+ parentId?: string;
+ styles?: Record;
+ classes?: string[];
+}
+
+const MODERN_SHAPE_MAP: Record = {
+ cyl: { type: 'process', shape: 'cylinder' },
+ cylinder: { type: 'process', shape: 'cylinder' },
+ circle: { type: 'end', shape: 'circle' },
+ circle2: { type: 'end', shape: 'circle' },
+ cloud: { type: 'process', shape: 'rounded' },
+ diamond: { type: 'decision', shape: 'diamond' },
+ hexagon: { type: 'custom', shape: 'hexagon' },
+ 'lean-r': { type: 'process', shape: 'parallelogram' },
+ 'lean-l': { type: 'process', shape: 'parallelogram' },
+ stadium: { type: 'start', shape: 'capsule' },
+ rounded: { type: 'process', shape: 'rounded' },
+ rect: { type: 'process', shape: 'rounded' },
+ square: { type: 'process', shape: 'rounded' },
+ doublecircle: { type: 'end', shape: 'circle' },
+};
+
+interface ModernShapeAnnotation {
+ shapeKey?: string;
+ labelOverride?: string;
+ cleanInput: string;
+}
+
+function extractModernAnnotation(input: string): ModernShapeAnnotation {
+ const match = input.match(/^(\w+)@\{([^}]+)\}/);
+ if (!match) return { cleanInput: input };
+
+ const id = match[1];
+ const attrs = match[2];
+ const rest = input.substring(match[0].length);
+
+ const shapeMatch = attrs.match(/\bshape:\s*(\w+)/);
+ const labelMatch = attrs.match(/\blabel:\s*"([^"]+)"/);
+
+ return {
+ shapeKey: shapeMatch?.[1]?.toLowerCase(),
+ labelOverride: labelMatch?.[1],
+ cleanInput: `${id}${rest}`,
+ };
+}
+
+function stripMarkdown(label: string): string {
+ return label
+ .replace(/\*\*(.+?)\*\*/g, '$1')
+ .replace(/\*(.+?)\*/g, '$1')
+ .replace(/__(.+?)__/g, '$1')
+ .replace(/_(.+?)_/g, '$1')
+ .replace(/~~(.+?)~~/g, '$1')
+ .replace(/`(.+?)`/g, '$1');
}
function stripFaIcons(label: string): string {
- const stripped = label.replace(/fa:fa-[\w-]+\s*/g, '').trim();
- if (stripped) return stripped;
- const iconMatch = label.match(/fa:fa-([\w-]+)/);
- return iconMatch ? iconMatch[1].replace(/-/g, ' ') : label;
+ const stripped = label.replace(/fa:fa-[\w-]+\s*/g, '').trim();
+ if (stripped) return stripped;
+ const iconMatch = label.match(/fa:fa-([\w-]+)/);
+ return iconMatch ? iconMatch[1].replace(/-/g, ' ') : label;
}
function tryParseWithShape(
- input: string,
- shape: { open: string; close: string; type: string; shape: NodeData['shape'] }
+ input: string,
+ shape: { open: string; close: string; type: string; shape: NodeData['shape'] }
): RawNode | null {
- const openIndex = input.indexOf(shape.open);
- if (openIndex < 1) return null;
- if (openIndex > 0 && input[openIndex - 1] === shape.open[0]) return null;
-
- const id = input.substring(0, openIndex).trim();
- if (!/^[a-zA-Z0-9_][\w-]*$/.test(id)) return null;
-
- const afterOpen = input.substring(openIndex + shape.open.length);
- const closeIndex = afterOpen.lastIndexOf(shape.close);
- if (closeIndex < 0) return null;
-
- const afterClose = afterOpen.substring(closeIndex + shape.close.length).trim();
- let classes: string[] = [];
- if (afterClose.startsWith(':::')) {
- classes = afterClose.substring(3).split(/,\s*/);
- } else if (afterClose) {
- return null;
- }
-
- let label = afterOpen.substring(0, closeIndex).trim();
- if ((label.startsWith('"') && label.endsWith('"')) || (label.startsWith("'") && label.endsWith("'"))) {
- label = label.slice(1, -1);
- }
- label = label.replace(/\\n/g, '\n');
- label = stripFaIcons(label);
- if (!label) label = id;
-
- return { id, label, type: shape.type, shape: shape.shape, classes: classes.length ? classes : undefined };
+ const openIndex = input.indexOf(shape.open);
+ if (openIndex < 1) return null;
+ if (openIndex > 0 && input[openIndex - 1] === shape.open[0]) return null;
+
+ const id = input.substring(0, openIndex).trim();
+ if (!/^[a-zA-Z0-9_][\w-]*$/.test(id)) return null;
+
+ const afterOpen = input.substring(openIndex + shape.open.length);
+ const closeIndex = afterOpen.lastIndexOf(shape.close);
+ if (closeIndex < 0) return null;
+
+ const afterClose = afterOpen.substring(closeIndex + shape.close.length).trim();
+ let classes: string[] = [];
+ if (afterClose.startsWith(':::')) {
+ classes = afterClose.substring(3).split(/,\s*/);
+ } else if (afterClose) {
+ return null;
+ }
+
+ let label = afterOpen.substring(0, closeIndex).trim();
+ if (
+ (label.startsWith('"') && label.endsWith('"')) ||
+ (label.startsWith("'") && label.endsWith("'"))
+ ) {
+ label = label.slice(1, -1);
+ }
+ label = label.replace(/\\n/g, '\n');
+ label = stripFaIcons(label);
+ label = stripMarkdown(label);
+ if (!label) label = id;
+
+ return {
+ id,
+ label,
+ type: shape.type,
+ shape: shape.shape,
+ classes: classes.length ? classes : undefined,
+ };
}
export function parseNodeDeclaration(raw: string): RawNode | null {
- const trimmed = raw.trim();
- if (!trimmed) return null;
-
- for (const shape of SHAPE_OPENERS) {
- const result = tryParseWithShape(trimmed, shape);
- if (result) return result;
+ const trimmed = raw.trim();
+ if (!trimmed) return null;
+
+ const annotation = extractModernAnnotation(trimmed);
+ const input = annotation.cleanInput;
+
+ for (const shape of SHAPE_OPENERS) {
+ const result = tryParseWithShape(input, shape);
+ if (result) {
+ if (annotation.shapeKey && MODERN_SHAPE_MAP[annotation.shapeKey]) {
+ const override = MODERN_SHAPE_MAP[annotation.shapeKey];
+ result.type = override.type;
+ result.shape = override.shape;
+ }
+ if (annotation.labelOverride) {
+ result.label = annotation.labelOverride;
+ }
+ result.label = stripMarkdown(result.label);
+ return result;
}
+ }
- let id = trimmed;
- let classes: string[] = [];
- if (id.includes(':::')) {
- const parts = id.split(':::');
- id = parts[0];
- classes = parts[1].split(/,\s*/);
- }
+ let id = input;
+ let classes: string[] = [];
+ if (id.includes(':::')) {
+ const parts = id.split(':::');
+ id = parts[0];
+ classes = parts[1].split(/,\s*/);
+ }
- if (/^[a-zA-Z0-9_][\w-]*$/.test(id)) {
- return { id, label: id, type: 'process', classes: classes.length ? classes : undefined };
- }
+ if (/^[a-zA-Z0-9_][\w-]*$/.test(id)) {
+ return { id, label: id, type: 'process', classes: classes.length ? classes : undefined };
+ }
- return null;
+ return null;
}
export const ARROW_PATTERNS = [
- '<==>',
- '<-.->',
- '<-->',
- '<==',
- '<-.',
- '<--',
- '===>',
- '-.->',
- '--->',
- '-->',
- '===',
- '---',
- '==>',
- '-.-',
- '--',
+ '<==>',
+ '<-.->',
+ '<-->',
+ '<==',
+ '<-.',
+ '<--',
+ '===>',
+ '-.->',
+ '--->',
+ '-->',
+ '===',
+ '---',
+ '==>',
+ '-.-',
+ '--',
];
function findArrowInLine(line: string): { arrow: string; before: string; after: string } | null {
- for (const arrow of ARROW_PATTERNS) {
- const index = line.indexOf(arrow);
- if (index >= 0) {
- return {
- arrow,
- before: line.substring(0, index).trim(),
- after: line.substring(index + arrow.length).trim(),
- };
- }
+ for (const arrow of ARROW_PATTERNS) {
+ const index = line.indexOf(arrow);
+ if (index >= 0) {
+ return {
+ arrow,
+ before: line.substring(0, index).trim(),
+ after: line.substring(index + arrow.length).trim(),
+ };
}
- return null;
+ }
+ return null;
}
export function parseEdgeLine(line: string): Array<{
- sourceRaw: string;
- targetRaw: string;
- label: string;
- arrowType: string;
+ sourceRaw: string;
+ targetRaw: string;
+ label: string;
+ arrowType: string;
}> {
- const edges: Array<{ sourceRaw: string; targetRaw: string; label: string; arrowType: string }> = [];
- let remaining = line;
- let lastNodeRaw: string | null = null;
-
- while (remaining.trim()) {
- const arrowMatch = findArrowInLine(remaining);
- if (!arrowMatch) break;
-
- const { arrow, before, after } = arrowMatch;
- const sourceRaw = lastNodeRaw || before;
- let label = '';
- let targetAndRest = after;
-
- const labelMatch = targetAndRest.match(/^\|"?([^"|]*)"?\|\s*/);
- if (labelMatch) {
- label = labelMatch[1].trim();
- targetAndRest = targetAndRest.substring(labelMatch[0].length);
- }
-
- const nextArrowMatch = findArrowInLine(targetAndRest);
- let targetRaw: string;
-
- if (nextArrowMatch) {
- targetRaw = nextArrowMatch.before;
- remaining = targetAndRest;
- } else {
- targetRaw = targetAndRest;
- remaining = '';
- }
-
- let source = sourceRaw.trim();
- let target = targetRaw.trim();
-
- if (source.includes(':::')) source = source.split(':::')[0];
- if (target.includes(':::')) target = target.split(':::')[0];
-
- if (source && target) {
- edges.push({ sourceRaw: source, targetRaw: target, label, arrowType: arrow });
- }
-
- lastNodeRaw = targetRaw.trim();
- if (!nextArrowMatch) break;
+ const edges: Array<{ sourceRaw: string; targetRaw: string; label: string; arrowType: string }> =
+ [];
+ let remaining = line;
+ let lastNodeRaw: string | null = null;
+
+ while (remaining.trim()) {
+ const arrowMatch = findArrowInLine(remaining);
+ if (!arrowMatch) break;
+
+ const { arrow, before, after } = arrowMatch;
+ const sourceRaw = lastNodeRaw || before;
+ let label = '';
+ let targetAndRest = after;
+
+ const labelMatch = targetAndRest.match(/^\|"?([^"|]*)"?\|\s*/);
+ if (labelMatch) {
+ label = labelMatch[1].trim();
+ targetAndRest = targetAndRest.substring(labelMatch[0].length);
}
- return edges;
+ const nextArrowMatch = findArrowInLine(targetAndRest);
+ let targetRaw: string;
+
+ if (nextArrowMatch) {
+ targetRaw = nextArrowMatch.before;
+ remaining = targetAndRest;
+ } else {
+ targetRaw = targetAndRest;
+ remaining = '';
+ }
+
+ let source = sourceRaw.trim();
+ let target = targetRaw.trim();
+
+ if (source.includes(':::')) source = source.split(':::')[0];
+ if (target.includes(':::')) target = target.split(':::')[0];
+
+ if (source && target) {
+ edges.push({ sourceRaw: source, targetRaw: target, label, arrowType: arrow });
+ }
+
+ lastNodeRaw = targetRaw.trim();
+ if (!nextArrowMatch) break;
+ }
+
+ return edges;
}
export function parseStyleString(styleStr: string): Record {
- const styles: Record = {};
- const parts = styleStr.split(',');
-
- for (const part of parts) {
- const [key, value] = part.split(':').map((s) => s.trim());
- if (key && value) {
- styles[key] = value.replace(/;$/, '');
- }
+ const styles: Record = {};
+ const parts = styleStr.split(',');
+
+ for (const part of parts) {
+ const [key, value] = part.split(':').map((s) => s.trim());
+ if (key && value) {
+ styles[key] = value.replace(/;$/, '');
}
+ }
- return styles;
+ return styles;
}
diff --git a/src/lib/nodeEnricher.test.ts b/src/lib/nodeEnricher.test.ts
new file mode 100644
index 00000000..8e39467a
--- /dev/null
+++ b/src/lib/nodeEnricher.test.ts
@@ -0,0 +1,163 @@
+import { describe, expect, it } from 'vitest';
+import { enrichNodesWithIcons } from './nodeEnricher';
+import type { FlowNode } from './types';
+
+function makeNode(id: string, label: string, overrides?: Partial): FlowNode {
+ return {
+ id,
+ type: 'process',
+ position: { x: 0, y: 0 },
+ data: { label, color: 'slate' },
+ ...overrides,
+ } as FlowNode;
+}
+
+describe('enrichNodesWithIcons', () => {
+ it('assigns color based on semantic classification', async () => {
+ const nodes = [
+ makeNode('start', 'Start'),
+ makeNode('end', 'End'),
+ makeNode('db', 'PostgreSQL'),
+ makeNode('check', 'Is Valid?', {
+ data: { label: 'Is Valid?', color: 'slate', shape: 'diamond' },
+ }),
+ ];
+
+ const enriched = await enrichNodesWithIcons(nodes);
+
+ expect(enriched[0].data.color).toBe('emerald');
+ expect(enriched[1].data.color).toBe('red');
+ expect(enriched[2].data.color).toBe('violet');
+ expect(enriched[3].data.color).toBe('amber');
+ });
+
+ it('assigns icons for known technologies', async () => {
+ const nodes = [
+ makeNode('db', 'PostgreSQL'),
+ makeNode('cache', 'Redis Cache'),
+ makeNode('api', 'Express API'),
+ ];
+
+ const enriched = await enrichNodesWithIcons(nodes);
+
+ // All three should get provider icons (any catalog)
+ expect(enriched[0].data.archIconPackId).toBeTruthy();
+ expect(enriched[0].data.archIconShapeId).toContain('postgresql');
+
+ expect(enriched[1].data.archIconPackId).toBeTruthy();
+ expect(enriched[1].data.archIconShapeId).toContain('redis');
+
+ expect(enriched[2].data.archIconPackId).toBeTruthy();
+ });
+
+ it('skips section and group nodes', async () => {
+ const nodes = [
+ { ...makeNode('grp', 'Group'), type: 'section' as const },
+ makeNode('x', 'Something'),
+ ];
+
+ const enriched = await enrichNodesWithIcons(nodes);
+
+ expect(enriched[0].data.color).toBe('slate');
+ expect(enriched[0].data.icon).toBeUndefined();
+ });
+
+ it('preserves existing non-slate colors', async () => {
+ const nodes = [
+ {
+ id: 'a',
+ type: 'process',
+ position: { x: 0, y: 0 },
+ data: { label: 'Start', color: 'pink' },
+ },
+ ];
+
+ const enriched = await enrichNodesWithIcons(nodes as FlowNode[]);
+
+ expect(enriched[0].data.color).toBe('pink');
+ });
+
+ it('preserves existing icons', async () => {
+ const nodes = [
+ {
+ id: 'a',
+ type: 'process',
+ position: { x: 0, y: 0 },
+ data: { label: 'PostgreSQL', color: 'violet', icon: 'my-icon' },
+ },
+ ];
+
+ const enriched = await enrichNodesWithIcons(nodes as FlowNode[]);
+
+ expect(enriched[0].data.icon).toBe('my-icon');
+ });
+
+ it('handles empty node array', async () => {
+ const enriched = await enrichNodesWithIcons([]);
+ expect(enriched).toEqual([]);
+ });
+
+ it('preserves nodes with no changes', async () => {
+ const nodes = [
+ {
+ id: 'a',
+ type: 'process',
+ position: { x: 0, y: 0 },
+ data: { label: 'Something Random', color: 'blue', icon: 'Box' },
+ },
+ ];
+
+ const enriched = await enrichNodesWithIcons(nodes as FlowNode[]);
+ expect(enriched[0]).toEqual(nodes[0]);
+ });
+
+ it('classifies decision shape correctly', async () => {
+ const nodes = [
+ makeNode('check', 'Validate?', {
+ data: { label: 'Validate?', color: 'slate', shape: 'diamond' },
+ }),
+ ];
+
+ const enriched = await enrichNodesWithIcons(nodes);
+ expect(enriched[0].data.color).toBe('amber');
+ });
+
+ it('classifies cylinder shape as database', async () => {
+ const nodes = [
+ makeNode('pg', 'PostgreSQL DB', {
+ data: { label: 'PostgreSQL DB', color: 'slate', shape: 'cylinder' },
+ }),
+ ];
+
+ const enriched = await enrichNodesWithIcons(nodes);
+ expect(enriched[0].data.color).toBe('violet');
+ if (!enriched[0].data.archIconPackId) {
+ expect(enriched[0].data.icon).toBe('database');
+ }
+ });
+
+ it('uses icon attribute for explicit catalog search', async () => {
+ const nodes = [
+ makeNode('cache', 'My Cache', {
+ data: { label: 'My Cache', color: 'slate', icon: 'redis' },
+ }),
+ ];
+
+ const enriched = await enrichNodesWithIcons(nodes);
+ expect(enriched[0].data.archIconPackId).toBeTruthy();
+ expect(enriched[0].data.archIconShapeId).toContain('redis');
+ });
+
+ it('uses provider filter when set', async () => {
+ const nodes = [
+ makeNode('db', 'Database', {
+ data: { label: 'Database', color: 'slate', provider: 'aws' },
+ }),
+ ];
+
+ const enriched = await enrichNodesWithIcons(nodes);
+ if (enriched[0].data.archIconPackId) {
+ expect(enriched[0].data.archIconPackId).toBe('aws-official-starter-v1');
+ }
+ });
+});
diff --git a/src/lib/nodeEnricher.ts b/src/lib/nodeEnricher.ts
new file mode 100644
index 00000000..20eba472
--- /dev/null
+++ b/src/lib/nodeEnricher.ts
@@ -0,0 +1,125 @@
+import type { FlowNode } from '@/lib/types';
+import { classifyNode } from '@/lib/semanticClassifier';
+import { matchIcon, type IconMatch } from '@/lib/iconMatcher';
+
+export function enrichNodesWithIcons(nodes: FlowNode[]): FlowNode[] {
+ return nodes.map(enrichSingleNode);
+}
+
+function enrichSingleNode(node: FlowNode): FlowNode {
+ if (node.type === 'section' || node.type === 'group' || node.type === 'swimlane') {
+ return node;
+ }
+
+ const label = node.data?.label ?? '';
+ const nodeColor = node.data?.color;
+ const isDefaultColor = !nodeColor || nodeColor === 'slate' || nodeColor === 'white';
+ const hasExplicitColor = !isDefaultColor;
+ const hasExplicitProviderIcon = Boolean(node.data?.archIconPackId);
+ const hasAnyIcon = Boolean(node.data?.icon) || hasExplicitProviderIcon;
+
+ if (hasExplicitColor && hasAnyIcon) {
+ return node;
+ }
+
+ const hint = classifyNode({ id: node.id, label, shape: node.data?.shape });
+ const dataUpdates: Record = {};
+
+ if (!hasExplicitColor) {
+ applyColor(node, hint.color, dataUpdates);
+ }
+
+ if (!hasExplicitProviderIcon) {
+ applyIcon(node, label, hint, dataUpdates);
+ }
+
+ if (Object.keys(dataUpdates).length === 0) {
+ return node;
+ }
+
+ return {
+ ...node,
+ data: {
+ ...node.data,
+ ...dataUpdates,
+ },
+ };
+}
+
+function applyColor(
+ node: FlowNode,
+ classifierColor: string,
+ updates: Record
+): void {
+ if (node.type === 'start') {
+ updates.color = 'emerald';
+ } else if (node.type === 'end') {
+ updates.color = 'red';
+ } else if (node.type === 'decision') {
+ updates.color = 'amber';
+ } else {
+ updates.color = classifierColor;
+ }
+}
+
+function applyIcon(
+ node: FlowNode,
+ label: string,
+ hint: { iconQuery: string; lucideFallback: string; category: string },
+ updates: Record
+): void {
+ const explicitIcon = node.data?.icon;
+ const provider = node.data?.provider;
+ const providerHint = typeof provider === 'string' ? provider : undefined;
+
+ // Priority 1: Explicit icon attribute (e.g., icon: "redis")
+ if (explicitIcon && typeof explicitIcon === 'string' && explicitIcon !== 'none') {
+ const match = findBestMatch(explicitIcon, providerHint);
+ if (match) {
+ updates.archIconPackId = match.packId;
+ updates.archIconShapeId = match.shapeId;
+ updates.assetPresentation = 'icon';
+ }
+ return;
+ }
+
+ // Priority 2: Classifier icon query (e.g., label contains "PostgreSQL")
+ if (hint.iconQuery) {
+ const match = findBestMatch(hint.iconQuery, providerHint);
+ if (match) {
+ updates.archIconPackId = match.packId;
+ updates.archIconShapeId = match.shapeId;
+ updates.assetPresentation = 'icon';
+ updates.icon = hint.lucideFallback;
+ return;
+ }
+ }
+
+ // Priority 3: Label-based fallback (icons: auto โ match by node label)
+ // Only when node has NO icon at all
+ if (label && !node.data?.icon) {
+ const match = findBestMatch(label, providerHint);
+ if (match) {
+ updates.archIconPackId = match.packId;
+ updates.archIconShapeId = match.shapeId;
+ updates.assetPresentation = 'icon';
+ }
+ }
+
+ // Lucide icon fallback
+ if (hint.lucideFallback && hint.lucideFallback !== 'box') {
+ updates.icon = hint.lucideFallback;
+ } else if (node.type === 'start') {
+ updates.icon = 'play';
+ } else if (node.type === 'end') {
+ updates.icon = 'check-circle';
+ } else if (node.type === 'decision') {
+ updates.icon = 'help-circle';
+ }
+}
+
+function findBestMatch(query: string, providerHint?: string): IconMatch | undefined {
+ const matches = matchIcon(query, providerHint);
+ const best = matches[0];
+ return best && best.score >= 0.8 ? best : undefined;
+}
diff --git a/src/lib/semanticClassifier.test.ts b/src/lib/semanticClassifier.test.ts
new file mode 100644
index 00000000..b77d9bbb
--- /dev/null
+++ b/src/lib/semanticClassifier.test.ts
@@ -0,0 +1,92 @@
+import { describe, expect, it } from 'vitest';
+import { classifyNode } from './semanticClassifier';
+
+describe('classifyNode', () => {
+ it('classifies start nodes', () => {
+ expect(classifyNode({ id: 'start', label: 'Start' }).category).toBe('start');
+ expect(classifyNode({ id: 'begin', label: 'Begin' }).category).toBe('start');
+ expect(classifyNode({ id: 'entry', label: 'Entry Point' }).category).toBe('start');
+ expect(classifyNode({ id: 'x', label: 'Order Start' }).category).toBe('start');
+ });
+
+ it('classifies end nodes', () => {
+ expect(classifyNode({ id: 'end', label: 'End' }).category).toBe('end');
+ expect(classifyNode({ id: 'done', label: 'Done' }).category).toBe('end');
+ expect(classifyNode({ id: 'finish', label: 'Complete' }).category).toBe('end');
+ });
+
+ it('classifies decision nodes by shape', () => {
+ const hint = classifyNode({ id: 'check', label: 'Is Valid?', shape: 'diamond' });
+ expect(hint.category).toBe('decision');
+ expect(hint.color).toBe('amber');
+ });
+
+ it('classifies database nodes', () => {
+ const pg = classifyNode({ id: 'db', label: 'PostgreSQL' });
+ expect(pg.category).toBe('database');
+ expect(pg.color).toBe('violet');
+ expect(pg.iconQuery).toMatch(/postgres/i);
+
+ const mongo = classifyNode({ id: 'db', label: 'MongoDB' });
+ expect(mongo.category).toBe('database');
+ expect(mongo.iconQuery).toMatch(/mongo/i);
+ });
+
+ it('classifies cylinder shape as database', () => {
+ const hint = classifyNode({ id: 'db', label: 'Users DB', shape: 'cylinder' });
+ expect(hint.category).toBe('database');
+ expect(hint.color).toBe('violet');
+ });
+
+ it('classifies cache nodes', () => {
+ const hint = classifyNode({ id: 'cache', label: 'Redis Cache' });
+ expect(hint.category).toBe('cache');
+ expect(hint.iconQuery).toMatch(/redis/i);
+ });
+
+ it('classifies queue nodes', () => {
+ const hint = classifyNode({ id: 'mq', label: 'RabbitMQ' });
+ expect(hint.category).toBe('queue');
+ expect(hint.iconQuery).toMatch(/rabbitmq/i);
+ });
+
+ it('classifies user nodes', () => {
+ const hint = classifyNode({ id: 'user', label: 'User' });
+ expect(hint.category).toBe('user');
+ expect(hint.color).toBe('blue');
+ });
+
+ it('classifies gateway nodes', () => {
+ const hint = classifyNode({ id: 'gw', label: 'API Gateway' });
+ expect(hint.category).toBe('gateway');
+
+ const nginx = classifyNode({ id: 'proxy', label: 'Nginx' });
+ expect(nginx.category).toBe('gateway');
+ });
+
+ it('classifies frontend nodes', () => {
+ const hint = classifyNode({ id: 'fe', label: 'React App' });
+ expect(hint.category).toBe('frontend');
+ expect(hint.iconQuery).toMatch(/react/i);
+ });
+
+ it('classifies service nodes', () => {
+ const hint = classifyNode({ id: 'api', label: 'Express API' });
+ expect(hint.category).toBe('service');
+ expect(hint.iconQuery).toMatch(/express/i);
+
+ const node = classifyNode({ id: 'be', label: 'Node.js Backend' });
+ expect(node.category).toBe('service');
+ });
+
+ it('classifies auth nodes', () => {
+ const hint = classifyNode({ id: 'auth', label: 'OAuth Login' });
+ expect(hint.category).toBe('auth');
+ });
+
+ it('returns process as default', () => {
+ const hint = classifyNode({ id: 'x', label: 'Something Random' });
+ expect(hint.category).toBe('process');
+ expect(hint.color).toBe('slate');
+ });
+});
diff --git a/src/lib/semanticClassifier.ts b/src/lib/semanticClassifier.ts
new file mode 100644
index 00000000..4d3f04eb
--- /dev/null
+++ b/src/lib/semanticClassifier.ts
@@ -0,0 +1,274 @@
+import type { NodeColorKey } from '@/theme';
+
+export type SemanticCategory =
+ | 'start'
+ | 'end'
+ | 'decision'
+ | 'database'
+ | 'cache'
+ | 'queue'
+ | 'service'
+ | 'frontend'
+ | 'user'
+ | 'action'
+ | 'gateway'
+ | 'auth'
+ | 'storage'
+ | 'process';
+
+export interface SemanticHint {
+ category: SemanticCategory;
+ color: NodeColorKey;
+ iconQuery: string;
+ lucideFallback: string;
+}
+
+interface ClassifierRule {
+ patterns: RegExp[];
+ category: SemanticCategory;
+ color: NodeColorKey;
+ lucideFallback: string;
+ extractQuery?: (text: string, id: string) => string;
+}
+
+const RULES: ClassifierRule[] = [
+ {
+ patterns: [/\bstart\b/i, /\bbegin\b/i, /\binit\b/i, /\bentry\b/i, /\blaunch\b/i],
+ category: 'start',
+ color: 'emerald',
+ lucideFallback: 'play',
+ },
+ {
+ patterns: [/\bend\b/i, /\bfinish\b/i, /\bdone\b/i, /\bcomplete\b/i, /\bstop\b/i, /\bexit\b/i],
+ category: 'end',
+ color: 'red',
+ lucideFallback: 'check-circle',
+ },
+ {
+ patterns: [
+ /\bdb\b/i,
+ /\bdatabase\b/i,
+ /\bsql\b/i,
+ /\bpostgres/i,
+ /\bmysql\b/i,
+ /\bmongo/i,
+ /\bdynamodb\b/i,
+ /\baurora\b/i,
+ /\bsqlite\b/i,
+ /\bmariadb\b/i,
+ /\bcockroach\b/i,
+ /\bsupabase\b/i,
+ ],
+ category: 'database',
+ color: 'violet',
+ lucideFallback: 'database',
+ extractQuery: (text) => {
+ const m = text.match(
+ /(postgres(?:ql)?|mysql|mongo(?:db)?|dynamodb|aurora|sqlite|mariadb|cockroach|supabase)/i
+ );
+ return m ? m[1] : text.split(/\s+/)[0];
+ },
+ },
+ {
+ patterns: [/\bredis\b/i, /\bmemcache/i, /\bcache\b/i, /\belasticache\b/i],
+ category: 'cache',
+ color: 'red',
+ lucideFallback: 'hard-drive',
+ extractQuery: (text) => {
+ const m = text.match(/(redis|memcache(?:d)?|elasticache)/i);
+ return m ? m[1] : 'cache';
+ },
+ },
+ {
+ patterns: [
+ /\bkafka\b/i,
+ /\brabbitmq\b/i,
+ /\bsqs\b/i,
+ /\bpulsar\b/i,
+ /\bnats\b/i,
+ /\bqueue\b/i,
+ /\bbus\b/i,
+ ],
+ category: 'queue',
+ color: 'amber',
+ lucideFallback: 'layers',
+ extractQuery: (text) => {
+ const m = text.match(/(kafka|rabbitmq|sqs|pulsar|nats)/i);
+ return m ? m[1] : 'queue';
+ },
+ },
+ {
+ patterns: [
+ /\buser\b/i,
+ /\bactor\b/i,
+ /\bcustomer\b/i,
+ /\badmin\b/i,
+ /\bclient\b/i,
+ /\bperson\b/i,
+ /\bviewer\b/i,
+ ],
+ category: 'user',
+ color: 'blue',
+ lucideFallback: 'user',
+ },
+ {
+ patterns: [
+ /\bapi[- ]?gateway\b/i,
+ /\bgateway\b/i,
+ /\bload[- ]?balancer\b/i,
+ /\bnginx\b/i,
+ /\bhaproxy\b/i,
+ /\balb\b/i,
+ /\bcloudfront\b/i,
+ /\bingress\b/i,
+ /\benvoy\b/i,
+ ],
+ category: 'gateway',
+ color: 'slate',
+ lucideFallback: 'shield',
+ extractQuery: (text) => {
+ const m = text.match(/(api[- ]?gateway|nginx|haproxy|alb|cloudfront|ingress|envoy)/i);
+ return m ? m[1] : 'gateway';
+ },
+ },
+ {
+ patterns: [
+ /\bauth\b/i,
+ /\blogin\b/i,
+ /\bsign[- ]?in\b/i,
+ /\boauth\b/i,
+ /\bjwt\b/i,
+ /\bsso\b/i,
+ /\bcognito\b/i,
+ /\bidentity\b/i,
+ ],
+ category: 'auth',
+ color: 'amber',
+ lucideFallback: 'key-round',
+ },
+ {
+ patterns: [/\bs3\b/i, /\bblob\b/i, /\bstorage\b/i, /\buploads?\b/i, /\bcdn\b/i],
+ category: 'storage',
+ color: 'yellow',
+ lucideFallback: 'folder',
+ },
+ {
+ patterns: [
+ /\breact\b/i,
+ /\bvue\b/i,
+ /\bangular\b/i,
+ /\bsvelte\b/i,
+ /\bnext\.?js\b/i,
+ /\bnuxt\b/i,
+ /\bfrontend\b/i,
+ /\bui\b/i,
+ /\bweb\s*app\b/i,
+ /\bclient[- ]?app\b/i,
+ /\bhtml\b/i,
+ /\bcss\b/i,
+ ],
+ category: 'frontend',
+ color: 'blue',
+ lucideFallback: 'monitor',
+ extractQuery: (text) => {
+ const m = text.match(/(react|vue|angular|svelte|next(?:\.?js)?|nuxt)/i);
+ return m ? m[1] : 'frontend';
+ },
+ },
+ {
+ patterns: [
+ /\bexpress\b/i,
+ /\bnode\.?js\b/i,
+ /\bdjango\b/i,
+ /\bflask\b/i,
+ /\bfastapi\b/i,
+ /\bspring\b/i,
+ /\brails\b/i,
+ /\blaravel\b/i,
+ /\bgin\b/i,
+ /\bactix\b/i,
+ /\bnest\.?js\b/i,
+ /\bapi\b/i,
+ /\bservice\b/i,
+ /\bbackend\b/i,
+ /\bserver\b/i,
+ /\bmicroservice\b/i,
+ ],
+ category: 'service',
+ color: 'blue',
+ lucideFallback: 'server',
+ extractQuery: (text) => {
+ const m = text.match(
+ /(express|node\.?js|django|flask|fastapi|spring|rails|laravel|gin|actix|nest\.?js)/i
+ );
+ return m ? m[1] : text.split(/\s+/)[0];
+ },
+ },
+ {
+ patterns: [
+ /\bdocker\b/i,
+ /\bkubernetes\b/i,
+ /\bk8s\b/i,
+ /\becs\b/i,
+ /\beks\b/i,
+ /\bcloud\s*run\b/i,
+ /\bcontainer\b/i,
+ ],
+ category: 'service',
+ color: 'blue',
+ lucideFallback: 'container',
+ extractQuery: (text) => {
+ const m = text.match(/(docker|kubernetes|k8s|ecs|eks|cloud\s*run)/i);
+ return m ? m[1] : 'container';
+ },
+ },
+];
+
+const DEFAULT_HINT: SemanticHint = {
+ category: 'process',
+ color: 'slate',
+ iconQuery: '',
+ lucideFallback: 'box',
+};
+
+export function classifyNode(node: { id: string; label: string; shape?: string }): SemanticHint {
+ if (node.shape === 'diamond') {
+ return { category: 'decision', color: 'amber', iconQuery: '', lucideFallback: 'help-circle' };
+ }
+
+ if (node.shape === 'cylinder') {
+ const text = `${node.id} ${node.label}`;
+ const m = text.match(/(postgres(?:ql)?|mysql|mongo(?:db)?|redis|dynamodb|aurora)/i);
+ return {
+ category: 'database',
+ color: 'violet',
+ iconQuery: m ? m[1] : node.label,
+ lucideFallback: 'database',
+ };
+ }
+
+ const text = `${node.id} ${node.label}`;
+
+ for (const rule of RULES) {
+ if (rule.patterns.some((p) => p.test(text))) {
+ return {
+ category: rule.category,
+ color: rule.color,
+ iconQuery: rule.extractQuery ? rule.extractQuery(text, node.id) : node.label,
+ lucideFallback: rule.lucideFallback,
+ };
+ }
+ }
+
+ return DEFAULT_HINT;
+}
+
+export function classifyNodes(
+ nodes: Array<{ id: string; label: string; shape?: string }>
+): Map {
+ const results = new Map();
+ for (const node of nodes) {
+ results.set(node.id, classifyNode(node));
+ }
+ return results;
+}
diff --git a/src/lib/types.ts b/src/lib/types.ts
index ff67ae7e..8626c8ab 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -17,7 +17,6 @@ export const DIAGRAM_TYPES = [
'stateDiagram',
'classDiagram',
'erDiagram',
- 'gitGraph',
'mindmap',
'journey',
'architecture',
@@ -165,18 +164,19 @@ export interface SectionNodeData {
sectionCollapsed?: boolean;
}
-export interface NodeData extends
- NodeLabelData,
- NodeIconData,
- NodeVisualStyleData,
- NodeCanvasMetadata,
- ClassNodeData,
- EntityNodeData,
- JourneyNodeData,
- MindmapNodeData,
- ArchitectureNodeData,
- SequenceNodeData,
- SectionNodeData {
+export interface NodeData
+ extends
+ NodeLabelData,
+ NodeIconData,
+ NodeVisualStyleData,
+ NodeCanvasMetadata,
+ ClassNodeData,
+ EntityNodeData,
+ JourneyNodeData,
+ MindmapNodeData,
+ ArchitectureNodeData,
+ SequenceNodeData,
+ SectionNodeData {
[key: string]: unknown;
}
diff --git a/src/services/export/formatting.ts b/src/services/export/formatting.ts
index 667d504b..58dc360d 100644
--- a/src/services/export/formatting.ts
+++ b/src/services/export/formatting.ts
@@ -1,5 +1,9 @@
export function sanitizeLabel(label: string): string {
- return label.replace(/['"()]/g, '').trim() || 'Node';
+ return label.replace(/"/g, "'").trim() || 'Node';
+}
+
+export function sanitizeEdgeLabel(label: string): string {
+ return label.replace(/"/g, "'").replace(/[{}]/g, '').trim();
}
export function sanitizeId(id: string): string {
diff --git a/src/services/export/mermaid/architectureMermaid.ts b/src/services/export/mermaid/architectureMermaid.ts
index 1d9906ad..90a0d656 100644
--- a/src/services/export/mermaid/architectureMermaid.ts
+++ b/src/services/export/mermaid/architectureMermaid.ts
@@ -1,6 +1,6 @@
import type { FlowEdge, FlowNode } from '@/lib/types';
import { handleIdToSide as handleIdToFlowSide } from '@/lib/nodeHandles';
-import { sanitizeId, sanitizeLabel } from '../formatting';
+import { sanitizeId, sanitizeLabel, sanitizeEdgeLabel } from '../formatting';
function normalizeArchitectureDirection(direction: string | undefined): '-->' | '<--' | '<-->' {
if (direction === '<--' || direction === '<-->') return direction;
@@ -66,7 +66,7 @@ export function toArchitectureMermaid(nodes: FlowNode[], edges: FlowEdge[]): str
| undefined;
const protocol = edgeData?.archProtocol;
const port = edgeData?.archPort;
- const label = edge.label ? sanitizeLabel(String(edge.label)) : undefined;
+ const label = edge.label ? sanitizeEdgeLabel(String(edge.label)) : undefined;
const sourceSide =
normalizeArchitectureSide(edgeData?.archSourceSide) || handleIdToSide(edge.sourceHandle);
const targetSide =
diff --git a/src/services/export/mermaid/stateDiagramMermaid.ts b/src/services/export/mermaid/stateDiagramMermaid.ts
index 3cc9e7e6..74bd23c6 100644
--- a/src/services/export/mermaid/stateDiagramMermaid.ts
+++ b/src/services/export/mermaid/stateDiagramMermaid.ts
@@ -14,19 +14,15 @@ function escapeStateLabel(label: string): string {
}
function isStateDiagramNodeType(type: string | undefined): boolean {
- return type === 'state' || type === 'start' || type === 'process';
+ return type === 'state' || type === 'start' || type === 'process' || type === 'section';
}
export function looksLikeStateDiagram(nodes: FlowNode[]): boolean {
if (nodes.length === 0) return false;
const hasStateStartNode = nodes.some((node) => node.id.startsWith('state_start_'));
const hasExplicitStateNode = nodes.some((node) => node.type === 'state');
- const hasCompositeParenting = nodes.some((node) => {
- const parentId = getNodeParentId(node);
- return parentId.length > 0;
- });
- if (!hasStateStartNode && !hasExplicitStateNode && !hasCompositeParenting) {
+ if (!hasStateStartNode && !hasExplicitStateNode) {
return false;
}
@@ -120,7 +116,8 @@ export function toStateDiagramMermaid(nodes: FlowNode[], edges: FlowEdge[]): str
const sourceParentId = sourceNode ? getNodeParentId(sourceNode) : '';
const targetParentId = targetNode ? getNodeParentId(targetNode) : '';
const shouldEmitInsideParent =
- (sourceParentId === node.id && (targetParentId === node.id || edge.target.startsWith('state_start_'))) ||
+ (sourceParentId === node.id &&
+ (targetParentId === node.id || edge.target.startsWith('state_start_'))) ||
(targetParentId === node.id && edge.source.startsWith('state_start_'));
if (!shouldEmitInsideParent) {
@@ -146,7 +143,10 @@ export function toStateDiagramMermaid(nodes: FlowNode[], edges: FlowEdge[]): str
const targetNode = nodeById.get(edge.target);
if (!sourceNode || !targetNode) return;
- if (getNodeParentId(sourceNode) && getNodeParentId(sourceNode) === getNodeParentId(targetNode)) {
+ if (
+ getNodeParentId(sourceNode) &&
+ getNodeParentId(sourceNode) === getNodeParentId(targetNode)
+ ) {
return;
}
diff --git a/src/services/export/mermaidBuilder.ts b/src/services/export/mermaidBuilder.ts
index 300a5519..987aed5a 100644
--- a/src/services/export/mermaidBuilder.ts
+++ b/src/services/export/mermaidBuilder.ts
@@ -1,5 +1,5 @@
import type { FlowEdge, FlowNode } from '@/lib/types';
-import { sanitizeId, sanitizeLabel } from './formatting';
+import { sanitizeId, sanitizeLabel, sanitizeEdgeLabel } from './formatting';
import { toArchitectureMermaid } from './mermaid/architectureMermaid';
import { toMindmapMermaid } from './mermaid/mindmapMermaid';
import { toJourneyMermaid } from './mermaid/journeyMermaid';
@@ -48,53 +48,119 @@ function resolveFlowchartConnector(edge: FlowEdge): string {
return body;
}
-function toFlowchartMermaid(nodes: FlowNode[], edges: FlowEdge[]): string {
- let mermaid = 'flowchart TD\n';
-
- nodes.forEach((node) => {
- const label = sanitizeLabel(node.data.label);
- const id = sanitizeId(node.id);
- let shapeStart = '[';
- let shapeEnd = ']';
-
- const shape = node.data.shape || 'rounded';
- const type = node.type;
-
- if (shape === 'diamond') {
- shapeStart = '{';
- shapeEnd = '}';
- } else if (shape === 'hexagon') {
- shapeStart = '{{';
- shapeEnd = '}}';
- } else if (shape === 'cylinder') {
- shapeStart = '[(';
- shapeEnd = ')]';
- } else if (shape === 'ellipse') {
- shapeStart = '([';
- shapeEnd = '])';
- } else if (shape === 'circle') {
- shapeStart = '((';
- shapeEnd = '))';
- } else if (shape === 'parallelogram') {
- shapeStart = '>';
- shapeEnd = ']';
- } else if (type === 'decision') {
- shapeStart = '{';
- shapeEnd = '}';
- } else if (type === 'start' || type === 'end') {
- shapeStart = '([';
- shapeEnd = '])';
+function resolveShapeBrackets(
+ shape: string | undefined,
+ type: string | undefined
+): { start: string; end: string } {
+ switch (shape) {
+ case 'diamond':
+ return { start: '{', end: '}' };
+ case 'hexagon':
+ return { start: '{{', end: '}}' };
+ case 'cylinder':
+ return { start: '[(', end: ')]' };
+ case 'circle':
+ return { start: '((', end: '))' };
+ case 'ellipse':
+ return { start: '([', end: '])' };
+ case 'capsule':
+ return { start: '([', end: '])' };
+ case 'parallelogram':
+ return { start: '>', end: ']' };
+ case 'rounded':
+ return { start: '(', end: ')' };
+ default:
+ break;
+ }
+
+ if (type === 'decision') return { start: '{', end: '}' };
+ if (type === 'start' || type === 'end') return { start: '([', end: '])' };
+
+ return { start: '[', end: ']' };
+}
+
+function collectSectionTree(nodes: FlowNode[]): {
+ roots: FlowNode[];
+ childrenByParent: Map;
+} {
+ const childrenByParent = new Map();
+ const roots: FlowNode[] = [];
+
+ for (const node of nodes) {
+ const parentId = node.parentId;
+ if (parentId) {
+ const children = childrenByParent.get(parentId) ?? [];
+ children.push(node);
+ childrenByParent.set(parentId, children);
+ } else if (node.type !== 'section' && node.type !== 'group') {
+ roots.push(node);
}
+ }
- mermaid += ` ${id}${shapeStart}"${label}"${shapeEnd}\n`;
- });
+ return { roots, childrenByParent };
+}
+
+function emitFlowchartNode(node: FlowNode, indent: string): string {
+ const label = sanitizeLabel(node.data.label);
+ const id = sanitizeId(node.id);
+ const { start, end } = resolveShapeBrackets(node.data.shape, node.type);
+ return `${indent}${id}${start}"${label}"${end}\n`;
+}
+
+function emitSectionBlock(
+ section: FlowNode,
+ children: FlowNode[],
+ childrenByParent: Map,
+ indent: string
+): string {
+ const label = sanitizeLabel(section.data.label);
+ let out = `${indent}subgraph ${label}\n`;
+
+ for (const child of children) {
+ if (child.type === 'section' || child.type === 'group') {
+ const grandChildren = childrenByParent.get(child.id) ?? [];
+ out += emitSectionBlock(child, grandChildren, childrenByParent, indent + ' ');
+ } else {
+ out += emitFlowchartNode(child, indent + ' ');
+ }
+ }
+
+ out += `${indent}end\n`;
+ return out;
+}
+
+function toFlowchartMermaid(nodes: FlowNode[], edges: FlowEdge[], direction?: string): string {
+ const dir = direction ?? 'TD';
+ let mermaid = `flowchart ${dir}\n`;
+
+ const sectionNodes = nodes.filter((n) => n.type === 'section' || n.type === 'group');
+ const hasSubgraphs = sectionNodes.length > 0;
+
+ if (hasSubgraphs) {
+ const { roots, childrenByParent } = collectSectionTree(nodes);
+
+ for (const section of sectionNodes) {
+ if (!section.parentId) {
+ const children = childrenByParent.get(section.id) ?? [];
+ mermaid += emitSectionBlock(section, children, childrenByParent, ' ');
+ }
+ }
+
+ for (const node of roots) {
+ mermaid += emitFlowchartNode(node, ' ');
+ }
+ } else {
+ for (const node of nodes) {
+ mermaid += emitFlowchartNode(node, ' ');
+ }
+ }
edges.forEach((edge) => {
const source = sanitizeId(edge.source);
const target = sanitizeId(edge.target);
const connector = resolveFlowchartConnector(edge);
if (edge.label) {
- const label = sanitizeLabel(edge.label as string);
+ const label = sanitizeEdgeLabel(edge.label as string);
mermaid += ` ${source} ${connector}|"${label}"| ${target}\n`;
} else {
mermaid += ` ${source} ${connector} ${target}\n`;
@@ -104,7 +170,7 @@ function toFlowchartMermaid(nodes: FlowNode[], edges: FlowEdge[]): string {
return mermaid;
}
-export function toMermaid(nodes: FlowNode[], edges: FlowEdge[]): string {
+export function toMermaid(nodes: FlowNode[], edges: FlowEdge[], direction?: string): string {
const architectureNodeCount = nodes.filter((node) => node.type === 'architecture').length;
if (nodes.length > 0 && architectureNodeCount === nodes.length) {
return toArchitectureMermaid(nodes, edges);
@@ -141,5 +207,5 @@ export function toMermaid(nodes: FlowNode[], edges: FlowEdge[]): string {
return toStateDiagramMermaid(nodes, edges);
}
- return toFlowchartMermaid(nodes, edges);
+ return toFlowchartMermaid(nodes, edges, direction);
}
diff --git a/src/services/export/mermaidExportQuality.test.ts b/src/services/export/mermaidExportQuality.test.ts
new file mode 100644
index 00000000..a00a477e
--- /dev/null
+++ b/src/services/export/mermaidExportQuality.test.ts
@@ -0,0 +1,178 @@
+import { describe, expect, it } from 'vitest';
+import { parseMermaidByType } from '@/services/mermaid/parseMermaidByType';
+import { enrichNodesWithIcons } from '@/lib/nodeEnricher';
+import { toMermaid } from '@/services/export/mermaidBuilder';
+import type { FlowNode, FlowEdge } from '@/lib/types';
+
+describe('Mermaid Export Quality', () => {
+ it('exports rounded shape as (label) not [label]', async () => {
+ const input = `flowchart TD
+ A("Rounded Node")
+
+ A --> B["Rectangle Node"]`;
+
+ const parsed = parseMermaidByType(input);
+ const enriched = await enrichNodesWithIcons(parsed.nodes);
+ const exported = toMermaid(enriched, parsed.edges);
+
+ expect(exported).toContain('("Rounded Node")');
+ });
+
+ it('exports start/end as stadium ([label])', async () => {
+ const input = `flowchart TD
+ S(["Start"])
+ E(("End"))
+
+ S --> E`;
+
+ const parsed = parseMermaidByType(input);
+ const enriched = await enrichNodesWithIcons(parsed.nodes);
+ const exported = toMermaid(enriched, parsed.edges);
+
+ expect(exported).toContain('(["Start"])');
+ expect(exported).toContain('(("End"))');
+ });
+
+ it('exports subgraph blocks', async () => {
+ const input = `flowchart TD
+ subgraph Frontend
+ UI["React App"]
+ end
+ subgraph Backend
+ API["Express API"]
+ DB[("PostgreSQL")]
+ end
+ UI --> API
+ API --> DB`;
+
+ const parsed = parseMermaidByType(input);
+ const enriched = await enrichNodesWithIcons(parsed.nodes);
+ const exported = toMermaid(enriched, parsed.edges);
+
+ expect(exported).toContain('subgraph Frontend');
+ expect(exported).toContain('subgraph Backend');
+ expect(exported).toContain('React App');
+ expect(exported).toContain('Express API');
+ expect(exported).toContain('end');
+ });
+
+ it('preserves direction when passed', async () => {
+ const input = `flowchart LR
+ A["Left"] --> B["Right"]`;
+
+ const parsed = parseMermaidByType(input);
+ const enriched = await enrichNodesWithIcons(parsed.nodes);
+ const exported = toMermaid(enriched, parsed.edges, 'LR');
+
+ expect(exported).toContain('flowchart LR');
+ });
+
+ it('defaults to TD when no direction specified', async () => {
+ const input = `flowchart TD
+ A["A"] --> B["B"]`;
+
+ const parsed = parseMermaidByType(input);
+ const enriched = await enrichNodesWithIcons(parsed.nodes);
+ const exported = toMermaid(enriched, parsed.edges);
+
+ expect(exported).toContain('flowchart TD');
+ });
+
+ it('exports all shape types correctly', async () => {
+ const nodes = [
+ {
+ id: 'a',
+ type: 'process',
+ position: { x: 0, y: 0 },
+ data: { label: 'Rounded', shape: 'rounded', color: 'slate' },
+ },
+ {
+ id: 'b',
+ type: 'process',
+ position: { x: 0, y: 0 },
+ data: { label: 'Rect', shape: undefined, color: 'slate' },
+ },
+ {
+ id: 'c',
+ type: 'process',
+ position: { x: 0, y: 0 },
+ data: { label: 'Diamond', shape: 'diamond', color: 'slate' },
+ },
+ {
+ id: 'd',
+ type: 'process',
+ position: { x: 0, y: 0 },
+ data: { label: 'Cylinder', shape: 'cylinder', color: 'slate' },
+ },
+ {
+ id: 'e',
+ type: 'process',
+ position: { x: 0, y: 0 },
+ data: { label: 'Circle', shape: 'circle', color: 'slate' },
+ },
+ {
+ id: 'f',
+ type: 'process',
+ position: { x: 0, y: 0 },
+ data: { label: 'Capsule', shape: 'capsule', color: 'slate' },
+ },
+ {
+ id: 'g',
+ type: 'process',
+ position: { x: 0, y: 0 },
+ data: { label: 'Hexagon', shape: 'hexagon', color: 'slate' },
+ },
+ ];
+
+ const exported = toMermaid(nodes as unknown as FlowNode[], [] as unknown as FlowEdge[]);
+
+ expect(exported).toContain('("Rounded")');
+ expect(exported).toContain('["Rect"]');
+ expect(exported).toContain('{"Diamond"}');
+ expect(exported).toContain('[("Cylinder")]');
+ expect(exported).toContain('(("Circle"))');
+ expect(exported).toContain('(["Capsule"])');
+ expect(exported).toContain('{{"Hexagon"}}');
+ });
+
+ it('roundtrips basic flowchart with shapes', async () => {
+ const input = `flowchart TD
+ S(["Start"])
+ P["Process"]
+ D{"Decision"}
+ E(("End"))
+
+ S --> P
+ P --> D
+ D -->|"Yes"| E`;
+
+ const parsed = parseMermaidByType(input);
+ const enriched = await enrichNodesWithIcons(parsed.nodes);
+ const exported = toMermaid(enriched, parsed.edges);
+
+ expect(exported).toContain('flowchart');
+ expect(exported).toContain('Start');
+ expect(exported).toContain('Process');
+ expect(exported).toContain('Decision');
+ expect(exported).toContain('End');
+ expect(exported).toContain('Yes');
+ });
+
+ it('preserves parens and apostrophes in labels', async () => {
+ const input = `flowchart TD
+ A["Parse (tokens)"] --> B["O'Brien"]
+ B --> C["Say \\"hello\\""]`;
+
+ const parsed = parseMermaidByType(input);
+ const enriched = await enrichNodesWithIcons(parsed.nodes);
+ const exported = toMermaid(enriched, parsed.edges);
+
+ expect(exported).toContain('Parse (tokens)');
+ expect(exported).toContain("O'Brien");
+ });
+
+ it('handles empty diagram', () => {
+ const exported = toMermaid([], []);
+ expect(exported).toContain('flowchart');
+ });
+});
diff --git a/src/services/export/plantumlBuilder.ts b/src/services/export/plantumlBuilder.ts
index 9e636ec4..cb5b33b0 100644
--- a/src/services/export/plantumlBuilder.ts
+++ b/src/services/export/plantumlBuilder.ts
@@ -1,5 +1,5 @@
import type { FlowEdge, FlowNode } from '@/lib/types';
-import { sanitizeId, sanitizeLabel } from './formatting';
+import { sanitizeId, sanitizeLabel, sanitizeEdgeLabel } from './formatting';
export function toPlantUML(nodes: FlowNode[], edges: FlowEdge[]): string {
let plantuml = '@startuml\n\n';
@@ -28,7 +28,7 @@ export function toPlantUML(nodes: FlowNode[], edges: FlowEdge[]): string {
edges.forEach((edge) => {
const source = sanitizeId(edge.source);
const target = sanitizeId(edge.target);
- const label = edge.label ? ` : ${sanitizeLabel(edge.label as string)}` : '';
+ const label = edge.label ? ` : ${sanitizeEdgeLabel(edge.label as string)}` : '';
plantuml += `${source} --> ${target}${label}\n`;
});
diff --git a/src/services/exportService.ts b/src/services/exportService.ts
index cd261588..c80a023b 100644
--- a/src/services/exportService.ts
+++ b/src/services/exportService.ts
@@ -2,8 +2,8 @@ import type { FlowEdge, FlowNode } from '@/lib/types';
import { toMermaid as toMermaidBuilder } from './export/mermaidBuilder';
import { toPlantUML as toPlantUMLBuilder } from './export/plantumlBuilder';
-export function toMermaid(nodes: FlowNode[], edges: FlowEdge[]): string {
- return toMermaidBuilder(nodes, edges);
+export function toMermaid(nodes: FlowNode[], edges: FlowEdge[], direction?: string): string {
+ return toMermaidBuilder(nodes, edges, direction);
}
export function toPlantUML(nodes: FlowNode[], edges: FlowEdge[]): string {
diff --git a/src/services/flowchartRoundTrip.test.ts b/src/services/flowchartRoundTrip.test.ts
index d0b6fe80..92f00673 100644
--- a/src/services/flowchartRoundTrip.test.ts
+++ b/src/services/flowchartRoundTrip.test.ts
@@ -22,8 +22,8 @@ describe('flowchart round-trip', () => {
expect(first.edges[2].markerStart).toBeDefined();
expect(first.edges[2].markerEnd).toBeUndefined();
- const exported = toMermaid(first.nodes, first.edges);
- expect(exported.startsWith('flowchart TD')).toBe(true);
+ const exported = toMermaid(first.nodes, first.edges, first.direction);
+ expect(exported.startsWith('flowchart TB')).toBe(true);
expect(exported).toContain('A -.->|"warmup"| B');
expect(exported).toContain('B ==> C');
expect(exported).toContain('C <-- D');
@@ -62,4 +62,20 @@ describe('flowchart round-trip', () => {
expect(second.edges[0].markerStart).toBeDefined();
expect(second.edges[0].markerEnd).toBeDefined();
});
+
+ it('preserves direction through parse/export/parse', () => {
+ const source = `
+ flowchart LR
+ A["Left"] --> B["Right"]
+ `;
+
+ const first = parseMermaidByType(source);
+ expect(first.direction).toBe('LR');
+
+ const exported = toMermaid(first.nodes, first.edges, first.direction);
+ expect(exported).toContain('flowchart LR');
+
+ const second = parseMermaidByType(exported);
+ expect(second.direction).toBe('LR');
+ });
});
diff --git a/src/services/flowpilot/assetGrounding.ts b/src/services/flowpilot/assetGrounding.ts
index 98877e7f..5f1d0f5e 100644
--- a/src/services/flowpilot/assetGrounding.ts
+++ b/src/services/flowpilot/assetGrounding.ts
@@ -12,25 +12,155 @@ const ALL_GROUNDING_CATEGORIES: DomainLibraryCategory[] = [
];
const SERVICE_ALIASES: Array<{ query: string; categories?: DomainLibraryCategory[] }> = [
+ // AWS Services
{ query: 'API Gateway', categories: ['aws'] },
{ query: 'Lambda', categories: ['aws'] },
{ query: 'S3', categories: ['aws'] },
{ query: 'RDS', categories: ['aws'] },
{ query: 'ElastiCache', categories: ['aws'] },
{ query: 'Cognito', categories: ['aws'] },
+ { query: 'DynamoDB', categories: ['aws'] },
+ { query: 'Aurora', categories: ['aws'] },
+ { query: 'EC2', categories: ['aws'] },
+ { query: 'ECS', categories: ['aws'] },
+ { query: 'EKS', categories: ['aws'] },
+ { query: 'SQS', categories: ['aws'] },
+ { query: 'SNS', categories: ['aws'] },
+ { query: 'CloudFront', categories: ['aws'] },
+ { query: 'ALB', categories: ['aws'] },
+ { query: 'EventBridge', categories: ['aws'] },
+ { query: 'Step Functions', categories: ['aws'] },
+ { query: 'CloudWatch', categories: ['aws'] },
+ { query: 'Secrets Manager', categories: ['aws'] },
+ { query: 'Kinesis', categories: ['aws'] },
+ { query: 'Redshift', categories: ['aws'] },
+ { query: 'Glue', categories: ['aws'] },
+ { query: 'SageMaker', categories: ['aws'] },
+
+ // Azure Services
{ query: 'Azure Functions', categories: ['azure'] },
{ query: 'Azure SQL', categories: ['azure'] },
{ query: 'Storage Account', categories: ['azure'] },
{ query: 'API Management', categories: ['azure'] },
+ { query: 'Service Bus', categories: ['azure'] },
+ { query: 'Event Hubs', categories: ['azure'] },
+ { query: 'Cosmos DB', categories: ['azure'] },
+ { query: 'Front Door', categories: ['azure'] },
+ { query: 'Key Vault', categories: ['azure'] },
+ { query: 'Azure Monitor', categories: ['azure'] },
+ { query: 'Azure Kubernetes', categories: ['azure'] },
+ { query: 'App Service', categories: ['azure'] },
+ { query: 'Azure Cache', categories: ['azure'] },
+
+ // GCP Services
{ query: 'Cloud Run', categories: ['gcp'] },
{ query: 'Cloud SQL', categories: ['gcp'] },
{ query: 'Cloud Storage', categories: ['gcp'] },
+ { query: 'Cloud Functions', categories: ['gcp'] },
+ { query: 'BigQuery', categories: ['gcp'] },
+ { query: 'Pub/Sub', categories: ['gcp'] },
+ { query: 'Cloud CDN', categories: ['gcp'] },
+ { query: 'Firestore', categories: ['gcp'] },
+ { query: 'Cloud Build', categories: ['gcp'] },
+ { query: 'Vertex AI', categories: ['gcp'] },
+ { query: 'Memorystore', categories: ['gcp'] },
+ { query: 'GKE', categories: ['gcp'] },
+ { query: 'Cloud Armor', categories: ['gcp'] },
+
+ // CNCF / Kubernetes
{ query: 'Kubernetes', categories: ['cncf'] },
{ query: 'Ingress', categories: ['cncf'] },
- { query: 'Redis' },
- { query: 'Postgres' },
+ { query: 'Envoy', categories: ['cncf'] },
+ { query: 'Istio', categories: ['cncf'] },
+ { query: 'Helm', categories: ['cncf'] },
+ { query: 'Prometheus', categories: ['cncf'] },
+ { query: 'Containerd', categories: ['cncf'] },
+ { query: 'Fluentd', categories: ['cncf'] },
+ { query: 'CoreDNS', categories: ['cncf'] },
+ { query: 'etcd', categories: ['cncf'] },
+ { query: 'Argo', categories: ['cncf'] },
+ { query: 'Linkerd', categories: ['cncf'] },
+
+ // Databases (developer catalog)
+ { query: 'PostgreSQL', categories: ['developer'] },
+ { query: 'Postgres', categories: ['developer'] },
+ { query: 'MySQL', categories: ['developer'] },
+ { query: 'MongoDB', categories: ['developer'] },
+ { query: 'Redis', categories: ['developer'] },
+ { query: 'Elasticsearch', categories: ['developer'] },
+ { query: 'SQLite', categories: ['developer'] },
+ { query: 'MariaDB', categories: ['developer'] },
+ { query: 'Cassandra', categories: ['developer'] },
+ { query: 'Neo4j', categories: ['developer'] },
+ { query: 'Supabase', categories: ['developer'] },
+ { query: 'PlanetScale', categories: ['developer'] },
+
+ // Frameworks & Runtimes
+ { query: 'Express', categories: ['developer'] },
+ { query: 'Node.js', categories: ['developer'] },
+ { query: 'React', categories: ['developer'] },
+ { query: 'Vue', categories: ['developer'] },
+ { query: 'Angular', categories: ['developer'] },
+ { query: 'Svelte', categories: ['developer'] },
+ { query: 'Next.js', categories: ['developer'] },
+ { query: 'Nuxt', categories: ['developer'] },
+ { query: 'Django', categories: ['developer'] },
+ { query: 'Flask', categories: ['developer'] },
+ { query: 'FastAPI', categories: ['developer'] },
+ { query: 'Spring', categories: ['developer'] },
+ { query: 'Rails', categories: ['developer'] },
+ { query: 'Laravel', categories: ['developer'] },
+ { query: 'NestJS', categories: ['developer'] },
+ { query: 'Deno', categories: ['developer'] },
+ { query: 'Bun', categories: ['developer'] },
+ { query: 'Go', categories: ['developer'] },
+ { query: 'Rust', categories: ['developer'] },
+ { query: 'Python', categories: ['developer'] },
+ { query: 'TypeScript', categories: ['developer'] },
+
+ // Infrastructure & DevOps
+ { query: 'Docker', categories: ['developer'] },
+ { query: 'Nginx', categories: ['developer'] },
+ { query: 'RabbitMQ', categories: ['developer'] },
+ { query: 'Kafka', categories: ['developer'] },
+ { query: 'Terraform', categories: ['developer'] },
+ { query: 'Ansible', categories: ['developer'] },
+ { query: 'Jenkins', categories: ['developer'] },
+ { query: 'GitHub', categories: ['developer'] },
+ { query: 'GitLab', categories: ['developer'] },
+ { query: 'Grafana', categories: ['developer'] },
+ { query: 'Consul', categories: ['developer'] },
+ { query: 'Vault', categories: ['developer'] },
+ { query: 'Pulsar', categories: ['developer'] },
+ { query: 'NATS', categories: ['developer'] },
+
+ // Auth & Payments
+ { query: 'Auth0', categories: ['developer'] },
+ { query: 'Keycloak', categories: ['developer'] },
+ { query: 'Firebase', categories: ['developer'] },
+ { query: 'Stripe', categories: ['developer'] },
+ { query: 'Twilio', categories: ['developer'] },
+ { query: 'SendGrid', categories: ['developer'] },
+ { query: 'Cloudflare', categories: ['developer'] },
+ { query: 'Vercel', categories: ['developer'] },
+ { query: 'Netlify', categories: ['developer'] },
+
+ // Generic terms (search all categories)
{ query: 'Queue' },
{ query: 'Database' },
+ { query: 'Cache' },
+ { query: 'Load Balancer' },
+ { query: 'CDN' },
+ { query: 'Storage' },
+ { query: 'Auth' },
+ { query: 'API' },
+ { query: 'Gateway' },
+ { query: 'Monitoring' },
+ { query: 'Logging' },
+ { query: 'Search' },
+ { query: 'Analytics' },
+ { query: 'ML' },
+ { query: 'AI' },
];
function scoreMatch(item: DomainLibraryItem, query: string): number {
@@ -72,8 +202,12 @@ function toGroundingMatch(item: DomainLibraryItem, query: string): AssetGroundin
};
}
-function inferQueriesFromPrompt(prompt: string): Array<{ query: string; categories?: DomainLibraryCategory[] }> {
- const matches = SERVICE_ALIASES.filter((entry) => prompt.toLowerCase().includes(entry.query.toLowerCase()));
+function inferQueriesFromPrompt(
+ prompt: string
+): Array<{ query: string; categories?: DomainLibraryCategory[] }> {
+ const matches = SERVICE_ALIASES.filter((entry) =>
+ prompt.toLowerCase().includes(entry.query.toLowerCase())
+ );
if (matches.length > 0) {
return matches;
}
diff --git a/src/services/geminiSystemInstruction.ts b/src/services/geminiSystemInstruction.ts
index 39df5e56..4157265b 100644
--- a/src/services/geminiSystemInstruction.ts
+++ b/src/services/geminiSystemInstruction.ts
@@ -1,10 +1,12 @@
+import { buildCatalogSummary } from '@/lib/iconMatcher';
+
const EDIT_MODE_PREAMBLE = `
## EDIT MODE โ MODIFYING AN EXISTING DIAGRAM
A CURRENT DIAGRAM block will be provided in OpenFlow DSL. You MUST:
1. Output the COMPLETE updated diagram in OpenFlow DSL โ not just the changed parts
-2. Preserve every node that should remain โ copy its id, type, label, icon, color, and all attributes EXACTLY as they appear in CURRENT DIAGRAM
-3. Use the EXACT same node id for every unchanged node (e.g. if CURRENT DIAGRAM has \`node-abc123: Login Service\`, your output must also use \`node-abc123\`)
+2. Preserve every node that should remain โ copy its id, type, label, and all attributes EXACTLY as they appear in CURRENT DIAGRAM
+3. Use the EXACT same node id for every unchanged node
4. Only change what the user explicitly requested
5. New nodes should have short descriptive IDs (e.g. \`redis_cache\`, \`auth_v2\`)
6. Do NOT re-layout or restructure nodes not affected by the change
@@ -17,193 +19,93 @@ A CURRENT DIAGRAM block will be provided in OpenFlow DSL. You MUST:
const BASE_SYSTEM_INSTRUCTION = `
# OpenFlow DSL Generation System
-You are an expert diagram assistant that converts plain language into **OpenFlow DSL**.
-
-Your job:
-- Read any description of a process, system, or flow โ casual or technical.
-- Use conversation history for context and refinements.
-- If an image is provided, convert the diagram/sketch into OpenFlow DSL.
-- Infer obvious missing steps.
-- Always output **only valid OpenFlow DSL** โ no prose, no explanations, no markdown wrappers.
+You convert plain language into **OpenFlow DSL** diagrams. Output ONLY valid OpenFlow DSL โ no prose, no markdown wrappers.
---
-## Structure Rules
-
-1. Start every diagram with a header:
- \`\`\`
- flow: Title Here
- direction: TB
- \`\`\`
- - Default to \`TB\` (top-to-bottom) for most diagrams.
- - Use \`LR\` (left-to-right) for pipelines, timelines, stages, workflows, or CI/CD.
-
-2. Define all **Nodes first**, then all **Edges**. Never mix them.
- - INVALID: \`[start] A -> [end] B\`
- - VALID: define nodes, then \`A -> B\`
+## Structure
-3. Node ID rules:
- - Short labels โ use label as ID: \`[process] Login { icon: "LogIn" }\`
- - Long labels โ use ID prefix: \`[process] login_step: User enters credentials { icon: "LogIn" }\`
+1. Header: \`flow: Title\` + \`direction: TB\` (default) or \`LR\` (pipelines, CI/CD).
+2. Define ALL nodes first, then ALL edges.
+3. Node IDs: simple labels can be the ID. Long labels need a prefix: \`[process] login_step: User enters credentials\`
---
## Node Types
-| Type | When to use |
+| Type | Use for |
|---|---|
| \`[start]\` | Entry point |
-| \`[end]\` | Terminal state (success or failure) |
-| \`[process]\` | Any action, step, or task |
+| \`[end]\` | Terminal state |
+| \`[process]\` | Action, step, task |
| \`[decision]\` | Branch / conditional |
-| \`[system]\` | Application-level backend service, internal API, business logic component |
-| \`[architecture]\` | Cloud or infrastructure resource such as AWS, Azure, GCP, Kubernetes, network, or security components |
-| \`[browser]\` | Web page / frontend screen |
+| \`[system]\` | Backend service, internal API, business logic |
+| \`[architecture]\` | Cloud/infra resource (AWS, Azure, GCP, K8s) |
+| \`[browser]\` | Web page / frontend |
| \`[mobile]\` | Mobile screen |
| \`[note]\` | Callout / annotation |
---
-## Edge Styles โ use these semantically
+## Edges
-| Syntax | Style | When to use |
-|---|---|---|
-| \`->\` | Normal arrow | Default connection |
-| \`->|label|\` | Labeled arrow | Decision branches โ ALWAYS label Yes/No, Pass/Fail etc. |
-| \`==>\` | **Thick** | Primary happy path / critical route |
-| \`-->\` | Curved | Soft / secondary flow |
-| \`..>\` | Dashed | Optional, error path, alternative, async |
+| Syntax | When |
+|---|---|
+| \`->\` | Default |
+| \`->|label|\` | Decision branches (Yes/No, Pass/Fail) |
+| \`==>\` | Primary/critical path |
+| \`-->\` | Secondary/soft flow |
+| \`..\` | Async, error, optional |
---
-## Node Attributes โ ALWAYS add \`icon\` and \`color\` to every non-start/end node
+## Attributes
+
+Syntax: \`[type] id: Label { icon: "IconName", color: "color", subLabel: "subtitle" }\`
-Syntax: \`[type] id: Label { icon: "IconName", color: "color", subLabel: "optional subtitle" }\`
+For \`[architecture]\` nodes: \`[architecture] id: Label { archProvider: "aws", archResourceType: "lambda", color: "violet" }\`
-For \`[architecture]\` nodes use:
-\`[architecture] id: Label { archProvider: "aws", archResourceType: "service", archIconPackId: "aws-official-starter-v1", color: "violet" }\`
+Colors: \`blue\` (frontend), \`violet\` (backend), \`emerald\` (data), \`amber\` (decisions/queues), \`red\` (errors/end), \`slate\` (generic), \`pink\` (third-party), \`yellow\` (cache).
-- Required attributes for \`[architecture]\`: \`archProvider\`, \`archResourceType\`
-- Optional attributes for \`[architecture]\`: \`archIconPackId\`, \`archIconShapeId\`, \`color\`, \`subLabel\`
-- Prefer \`[architecture]\` over \`[system]\` for cloud services, infrastructure, managed data stores, queues, gateways, network, and security resources
-- Prefer \`[system]\` for application services, internal APIs, controllers, workers, and business logic that belong to the product itself
+Icons are optional โ the system auto-assigns them. For known technologies, use \`archProvider\` and \`archResourceType\` to specify the icon directly:
-6. **subLabel** โ add a short subtitle for context on complex nodes:
- \`\`\`
- [process] auth: Authenticate { icon: "Lock", color: "blue", subLabel: "OAuth 2.0 + JWT" }
- [system] api: Payment API { icon: "CreditCard", color: "violet", subLabel: "Stripe v3" }
- \`\`\`
+\`[system] db: PostgreSQL { archProvider: "developer", archResourceType: "database-postgresql", color: "violet" }\`
-7. **Annotations** โ use \`[note]\` to add callouts for constraints, caveats, or SLAs. Connect with a dashed edge \`..>\`:
- \`\`\`
- [note] sla: 99.9% Uptime required { color: "slate" }
- api ..> sla
- \`\`\`
+Available icon catalog:
+${buildCatalogSummary(15)}
-8. **No container nodes** โ do not use \`[section]\` nodes or \`group {}\` blocks. Keep related nodes near each other and use labels or subtitles to imply layers such as frontend, backend, or data.
+Use exact shape IDs from the catalog when possible (e.g. \`database-postgresql\`, \`queue-rabbitmq\`). If unsure, omit \`archResourceType\` and the system will match by label.
---
-9. **Curated icon list** โ pick the MOST semantically appropriate icon from this list:
-
- Actions: \`Play\`, \`Pause\`, \`Stop\`, \`Check\`, \`X\`, \`Plus\`, \`Trash2\`, \`Edit3\`, \`Send\`, \`Upload\`, \`Download\`, \`Search\`, \`Filter\`, \`RefreshCw\`, \`LogIn\`, \`LogOut\`
-
- Data & Dev: \`Database\`, \`Server\`, \`Code2\`, \`Terminal\`, \`GitBranch\`, \`Zap\`, \`Settings\`, \`Key\`, \`Lock\`, \`Unlock\`, \`ShieldCheck\`, \`AlertTriangle\`
-
- People: \`User\`, \`Users\`, \`UserCheck\`, \`UserPlus\`, \`Bell\`, \`Mail\`, \`Phone\`, \`MessageSquare\`, \`Contact\`
-
- Commerce: \`ShoppingCart\`, \`CreditCard\`, \`Package\`, \`Store\`, \`Tag\`, \`Receipt\`, \`Truck\`
-
- Content: \`File\`, \`FileText\`, \`Folder\`, \`Image\`, \`Link\`, \`Globe\`, \`Rss\`
-
- Infrastructure: \`Cloud\`, \`Wifi\`, \`Smartphone\`, \`Monitor\`, \`HardDrive\`, \`Cpu\`
-
-10. **Cloud provider icons** โ when rendering infrastructure, use \`[architecture]\` nodes and these provider values:
- - AWS: \`archProvider: "aws"\`, prefer \`archIconPackId: "aws-official-starter-v1"\`
- Common services: EC2, S3, RDS, Lambda, DynamoDB, API Gateway, CloudFront, SQS, SNS, ECS, EKS, ElastiCache, Cognito, IAM
- - Azure: \`archProvider: "azure"\`, prefer \`archIconPackId: "azure-official-icons-v20"\`
- Common services: VM, Functions, Storage Account, Azure SQL, API Management, Front Door
- - GCP: \`archProvider: "gcp"\`
- Common services: Compute Engine, Cloud Functions, Cloud Storage, Cloud SQL, Load Balancer, Cloud Run
- - Kubernetes / CNCF: \`archProvider: "cncf"\`
- Common resources: Cluster, Node, Pod, Service, Ingress, ConfigMap
- - Network: \`archProvider: "network"\`
- Common resource types: \`load_balancer\`, \`router\`, \`switch\`, \`cdn\`, \`dns\`, \`service\`
- - Security: \`archProvider: "security"\`
- Common resource types: \`firewall\`, \`service\`, \`dns\`
-
-11. **Color semantics** โ use colors deliberately, not randomly:
- - \`blue\` โ frontend, user-facing, presentation layer
- - \`violet\` โ backend services, APIs, internal systems
- - \`emerald\` โ data stores, persistence, successful outcomes
- - \`amber\` โ queues, async workers, warning states, decisions
- - \`red\` โ security boundaries, firewalls, error, end, fail, danger, cancel
- - \`slate\` โ generic fallback, unknown services, neutral groups
- - \`pink\` โ third-party or external services
- - \`yellow\` โ cache, fast path, in-memory systems
-
-12. **Use node types intentionally**:
- - \`[architecture]\`: cloud services, infrastructure, managed databases, queues, gateways, DNS, CDN, VPN, firewalls
- - \`[system]\`: product-owned backend services, internal APIs, modules, business logic
- - \`[browser]\`: web apps, dashboards, admin panels, portals
- - \`[mobile]\`: iOS, Android, React Native, Flutter apps
- - \`[process]\`: operational steps, jobs, transformations, workflows
- - Do not use container or group nodes for layers, trust boundaries, VPCs, clusters, namespaces, or zones
-
-13. Label important edges with what flows across them, especially in architecture diagrams: \`HTTP/REST\`, \`SQL\`, \`gRPC\`, \`events\`, \`cache lookup\`, \`files\`
-
-14. Use comments \`#\` only when they add clarity.
-
-15. Do NOT explain the output. Do NOT add prose. Only output DSL.
-
-15b. **Diagram density** โ aim for the right density:
- - Flowcharts: 6โ15 nodes is ideal. More than 20 = simplify the diagram.
- - Architecture diagrams: 8โ20 nodes, with layers implied by labels, subtitles, and placement instead of containers.
- - Sequence/journey: 4โ10 steps in the happy path.
- - If a request is simple, keep the diagram simple. Do not pad with unnecessary detail.
-
-15c. **Layout quality rules**:
- - Happy path flows TOP โ BOTTOM (TB) or LEFT โ RIGHT (LR) in a straight line, with alternatives branching off the sides.
- - Decision nodes (\`[decision]\`) should have EXACTLY 2 outgoing labeled edges (e.g. \`->|Yes|\` and \`->|No|\`).
- - Avoid more than 3 incoming edges on any single node โ use a \`[process]\` aggregator if needed.
- - Keep tightly coupled nodes visually close without using container blocks.
- - Name architectural layers directly in node labels or subtitles instead of using container nodes.
- - Use \`==>\` (thick) for the critical path, \`->\` for normal flow, \`..>\` for async/optional, \`-->\` for soft/secondary.
-
-15d. **Self-describing diagrams** โ every diagram should be readable without a legend:
- - Include \`subLabel\` on complex nodes to explain protocols, versions, or constraints.
- - Label important edges with what flows across them: \`HTTP/REST\`, \`SQL query\`, \`JWT\`, \`events\`, \`file\`.
- - Use \`[note]\` nodes for critical constraints, SLAs, or caveats โ connect with \`..>\`.
-
-16. **Node IDs**:
- - If the label is simple (e.g., "Login"), you can use it as the ID: \`[process] Login { icon: "LogIn" }\`.
- - If the label is long, use an ID: \`[process] login_step: User enters credentials { icon: "LogIn" }\`.
-
-17. **Iterative editing โ preserve existing IDs**:
- - When a CURRENT CONTENT block is provided, it includes each node's exact \`id\` (e.g. \`"id": "node-abc123"\`).
- - For nodes that should REMAIN in the diagram, reuse their EXACT id as the node identifier in your DSL output.
- - Example: if context shows \`"id": "node-abc123", "label": "Login"\`, output \`[process] node-abc123: Login { icon: "LogIn", color: "blue" }\`
- - Only introduce new ids for genuinely new nodes you are adding.
- - Omit nodes that should be removed โ do not output them at all.
- - When a FOCUSED EDIT is specified (selected nodes), preserve all non-selected nodes verbatim with their exact IDs and properties.
+## Rules
+
+- Decisions: exactly 2 outgoing labeled edges
+- Max 3 incoming edges per node
+- Label edges with what flows: \`HTTP/REST\`, \`SQL\`, \`events\`, \`JWT\`
+- Use \`subLabel\` for protocols, versions, constraints
+- Use \`[note]\` for SLAs/caveats, connected with \`..\`
+- 6โ15 nodes for flowcharts, 8โ20 for architecture
+- Do NOT use container/group nodes
+- When editing, preserve existing node IDs exactly
---
## Examples
-### User Authentication
+### Authentication Flow
\`\`\`
flow: User Authentication
direction: TB
[start] Start
-[process] login: Login Form { icon: "LogIn", color: "blue", subLabel: "Email + password" }
-[decision] valid: Credentials valid? { icon: "ShieldCheck", color: "amber" }
-[process] mfa: MFA Check { icon: "Smartphone", color: "blue", subLabel: "TOTP / SMS" }
-[process] token: Issue JWT { icon: "Key", color: "violet" }
-[end] dashboard: Enter Dashboard { icon: "Monitor", color: "emerald" }
-[end] fail: Access Denied { icon: "X", color: "red" }
+[process] login: Login Form { icon: "LogIn", color: "blue" }
+[decision] valid: Credentials valid? { color: "amber" }
+[process] mfa: MFA Check { icon: "Smartphone", color: "blue" }
+[system] token: Issue JWT { icon: "Key", color: "violet" }
+[end] dashboard: Enter Dashboard { color: "emerald" }
+[end] fail: Access Denied { color: "red" }
Start ==> login
login -> valid
@@ -213,80 +115,40 @@ mfa ==> token
token ==> dashboard
\`\`\`
-### E-Commerce Checkout
+### AWS Serverless Architecture
\`\`\`
-flow: Checkout Flow
+flow: Serverless API
direction: TB
-[start] Start
-[process] cart: Review Cart { icon: "ShoppingCart", color: "blue" }
-[process] address: Shipping Address { icon: "Truck", color: "blue" }
-[process] payment: Payment Details { icon: "CreditCard", color: "blue", subLabel: "Stripe v3" }
-[decision] fraud: Fraud check { icon: "ShieldCheck", color: "amber" }
-[system] fulfil: Fulfilment Service { icon: "Package", color: "violet" }
-[process] notify: Send Confirmation { icon: "Mail", color: "emerald", subLabel: "Email + SMS" }
-[end] done: Order Complete { icon: "Check", color: "emerald" }
-[end] declined: Payment Declined { icon: "AlertTriangle", color: "red" }
-
-Start ==> cart
-cart ==> address
-address ==> payment
-payment -> fraud
-fraud ->|Pass| fulfil
-fraud ->|Fail| declined
-fulfil ==> notify
-notify ==> done
-\`\`\`
-
-### CI/CD Pipeline
+[architecture] cf: CloudFront { archProvider: "aws", archResourceType: "networking-cloudfront", color: "blue" }
+[architecture] apigw: API Gateway { archProvider: "aws", archResourceType: "app-integration-api-gateway", color: "violet" }
+[architecture] lambda: API Lambda { archProvider: "aws", archResourceType: "compute-lambda", color: "violet" }
+[architecture] dynamo: DynamoDB { archProvider: "aws", archResourceType: "database-dynamodb", color: "emerald" }
+[architecture] cache: ElastiCache { archProvider: "aws", archResourceType: "database-elasticache", color: "yellow" }
-\`\`\`
-flow: CI/CD Pipeline
-direction: LR
-
-[start] Push
-[process] build: Build { icon: "Code2", color: "blue", subLabel: "npm run build" }
-[process] test: Run Tests { icon: "Check", color: "blue", subLabel: "Jest + Playwright" }
-[decision] pass: All tests pass? { icon: "GitBranch", color: "amber" }
-[system] registry: Push to Registry { icon: "Cloud", color: "violet", subLabel: "Docker Hub" }
-[process] deploy: Deploy to Production { icon: "Zap", color: "emerald" }
-[process] slack_notify: Slack Notification { icon: "MessageSquare", color: "blue" }
-[end] live: Live { icon: "Globe", color: "emerald" }
-[end] failed: Build Failed { icon: "X", color: "red" }
-
-Push ==> build
-build ==> test
-test -> pass
-pass ->|Yes| registry
-pass ->|No| failed
-registry ==> deploy
-deploy ..> slack_notify
-slack_notify ==> live
+cf ->|HTTPS| apigw
+apigw ->|HTTP/REST| lambda
+lambda ->|query| dynamo
+lambda ->|cache lookup| cache
\`\`\`
-### Architecture Diagram
+### Full-Stack with Developer Icons
\`\`\`
-flow: Serverless API - AWS
+flow: E-Commerce Stack
direction: TB
-[architecture] cf: CloudFront { archProvider: "aws", archResourceType: "cdn", archIconPackId: "aws-official-starter-v1", color: "blue" }
-[architecture] apigw: API Gateway { archProvider: "aws", archResourceType: "service", archIconPackId: "aws-official-starter-v1", color: "violet", subLabel: "Edge Layer" }
-[architecture] auth_fn: Auth Lambda { archProvider: "aws", archResourceType: "service", archIconPackId: "aws-official-starter-v1", color: "violet" }
-[architecture] api_fn: API Lambda { archProvider: "aws", archResourceType: "service", archIconPackId: "aws-official-starter-v1", color: "violet", subLabel: "Compute Layer" }
-[architecture] dynamo: DynamoDB { archProvider: "aws", archResourceType: "database", archIconPackId: "aws-official-starter-v1", color: "emerald" }
-[architecture] cache: ElastiCache { archProvider: "aws", archResourceType: "service", archIconPackId: "aws-official-starter-v1", color: "yellow" }
-[architecture] s3: S3 Storage { archProvider: "aws", archResourceType: "service", archIconPackId: "aws-official-starter-v1", color: "emerald", subLabel: "Data Layer" }
-[architecture] cognito: Cognito { archProvider: "aws", archResourceType: "service", archIconPackId: "aws-official-starter-v1", color: "amber" }
+[system] react: React App { archProvider: "developer", archResourceType: "frontend-react", color: "blue" }
+[system] api: Express API { archProvider: "developer", archResourceType: "others-expressjs-dark", color: "violet" }
+[system] db: PostgreSQL { archProvider: "developer", archResourceType: "database-postgresql", color: "violet" }
+[system] cache: Redis { archProvider: "developer", archResourceType: "database-redis", color: "red" }
+[system] mq: RabbitMQ { archProvider: "developer", archResourceType: "queue-rabbitmq", color: "amber" }
-cf ->|HTTPS| apigw
-apigw ->|auth request| auth_fn
-apigw ->|HTTP/REST| api_fn
-auth_fn ->|identity| cognito
-api_fn ->|query| dynamo
-api_fn ->|cache lookup| cache
-api_fn ->|store files| s3
+react ->|HTTP/REST| api
+api ->|SQL| db
+api ->|cache lookup| cache
+api ->|publish| mq
\`\`\`
`;
diff --git a/src/services/mermaid/detectDiagramType.test.ts b/src/services/mermaid/detectDiagramType.test.ts
index 5afed97a..1e7333df 100644
--- a/src/services/mermaid/detectDiagramType.test.ts
+++ b/src/services/mermaid/detectDiagramType.test.ts
@@ -14,10 +14,10 @@ describe('detectMermaidDiagramType', () => {
it('detects target q2 families', () => {
expect(detectMermaidDiagramType('classDiagram\nA <|-- B')).toBe('classDiagram');
expect(detectMermaidDiagramType('erDiagram\nA ||--o{ B : has')).toBe('erDiagram');
- expect(detectMermaidDiagramType('gitGraph\ncommit')).toBe('gitGraph');
expect(detectMermaidDiagramType('mindmap\nroot')).toBe('mindmap');
expect(detectMermaidDiagramType('journey\ntitle Onboarding')).toBe('journey');
expect(detectMermaidDiagramType('architecture-beta\nservice api')).toBe('architecture');
+ expect(detectMermaidDiagramType('sequenceDiagram\nparticipant A')).toBe('sequence');
});
it('skips empty and comment lines', () => {
@@ -35,4 +35,3 @@ A --> B
expect(detectMermaidDiagramType('')).toBeNull();
});
});
-
diff --git a/src/services/mermaid/detectDiagramType.ts b/src/services/mermaid/detectDiagramType.ts
index b7d5a7b2..0be69d7f 100644
--- a/src/services/mermaid/detectDiagramType.ts
+++ b/src/services/mermaid/detectDiagramType.ts
@@ -15,14 +15,13 @@ export function detectMermaidDiagramType(input: string): DiagramType | null {
if (/^stateDiagram(?:-v2)?\b/i.test(line)) return 'stateDiagram';
if (/^classDiagram\b/i.test(line)) return 'classDiagram';
if (/^erDiagram\b/i.test(line)) return 'erDiagram';
- if (/^gitGraph\b/i.test(line)) return 'gitGraph';
if (/^mindmap\b/i.test(line)) return 'mindmap';
if (/^journey\b/i.test(line)) return 'journey';
if (/^architecture(?:-beta)?\b/i.test(line)) return 'architecture';
+ if (/^sequenceDiagram\b/i.test(line)) return 'sequence';
return null;
}
return null;
}
-
diff --git a/src/services/mermaid/parseMermaidByType.test.ts b/src/services/mermaid/parseMermaidByType.test.ts
index 01ce8d4b..23b34b38 100644
--- a/src/services/mermaid/parseMermaidByType.test.ts
+++ b/src/services/mermaid/parseMermaidByType.test.ts
@@ -67,8 +67,14 @@ describe('parseMermaidByType', () => {
expect(result.diagramType).toBe('classDiagram');
expect(result.error).toBeUndefined();
expect(result.nodes.length).toBeGreaterThan(0);
- expect(result.diagnostics?.some((message) => message.includes('Invalid class declaration at line'))).toBe(true);
- expect(result.diagnostics?.some((message) => message.includes('Invalid class relation syntax at line'))).toBe(true);
+ expect(
+ result.diagnostics?.some((message) => message.includes('Invalid class declaration at line'))
+ ).toBe(true);
+ expect(
+ result.diagnostics?.some((message) =>
+ message.includes('Invalid class relation syntax at line')
+ )
+ ).toBe(true);
});
it('parses erDiagram through plugin dispatcher', () => {
@@ -103,8 +109,14 @@ describe('parseMermaidByType', () => {
expect(result.diagramType).toBe('erDiagram');
expect(result.error).toBeUndefined();
expect(result.nodes.length).toBeGreaterThan(0);
- expect(result.diagnostics?.some((message) => message.includes('Invalid entity declaration at line'))).toBe(true);
- expect(result.diagnostics?.some((message) => message.includes('Invalid erDiagram relation syntax at line'))).toBe(true);
+ expect(
+ result.diagnostics?.some((message) => message.includes('Invalid entity declaration at line'))
+ ).toBe(true);
+ expect(
+ result.diagnostics?.some((message) =>
+ message.includes('Invalid erDiagram relation syntax at line')
+ )
+ ).toBe(true);
});
it('parses mindmap through plugin dispatcher', () => {
@@ -148,8 +160,14 @@ describe('parseMermaidByType', () => {
expect(result.diagramType).toBe('journey');
expect(result.error).toBeUndefined();
expect(result.nodes.length).toBeGreaterThan(0);
- expect(result.diagnostics?.some((message) => message.includes('Invalid journey section syntax at line'))).toBe(true);
- expect(result.diagnostics?.some((message) => message.includes('Invalid journey step syntax at line'))).toBe(true);
+ expect(
+ result.diagnostics?.some((message) =>
+ message.includes('Invalid journey section syntax at line')
+ )
+ ).toBe(true);
+ expect(
+ result.diagnostics?.some((message) => message.includes('Invalid journey step syntax at line'))
+ ).toBe(true);
});
it('returns mindmap diagnostics for malformed indentation/wrapper lines', () => {
@@ -164,8 +182,29 @@ describe('parseMermaidByType', () => {
expect(result.diagramType).toBe('mindmap');
expect(result.error).toBeUndefined();
expect(result.nodes.length).toBeGreaterThan(0);
- expect(result.diagnostics?.some((message) => message.includes('Mindmap indentation jump at line'))).toBe(true);
- expect(result.diagnostics?.some((message) => message.includes('Malformed mindmap wrapper syntax at line'))).toBe(true);
+ expect(
+ result.diagnostics?.some((message) => message.includes('Mindmap indentation jump at line'))
+ ).toBe(true);
+ expect(
+ result.diagnostics?.some((message) =>
+ message.includes('Malformed mindmap wrapper syntax at line')
+ )
+ ).toBe(true);
+ });
+
+ it('parses sequenceDiagram through plugin dispatcher', () => {
+ const result = parseMermaidByType(`
+ sequenceDiagram
+ participant Alice
+ participant Bob
+ Alice->>Bob: Hello
+ Bob-->>Alice: Hi
+ `);
+
+ expect(result.diagramType).toBe('sequence');
+ expect(result.error).toBeUndefined();
+ expect(result.nodes.length).toBeGreaterThan(0);
+ expect(result.edges.length).toBeGreaterThan(0);
});
it('parses architecture through plugin dispatcher', () => {
@@ -187,28 +226,32 @@ describe('parseMermaidByType', () => {
});
it('rejects architecture recovery diagnostics in strict mode', () => {
- const result = parseMermaidByType(`
+ const result = parseMermaidByType(
+ `
architecture-beta
service api(server)[API]
api --> cache
- `, { architectureStrictMode: true });
+ `,
+ { architectureStrictMode: true }
+ );
expect(result.diagramType).toBe('architecture');
expect(result.error).toContain('strict mode rejected');
expect(result.nodes).toHaveLength(0);
expect(result.edges).toHaveLength(0);
- expect(result.diagnostics?.some((d) => d.includes('Recovered implicit service node "cache"'))).toBe(true);
+ expect(
+ result.diagnostics?.some((d) => d.includes('Recovered implicit service node "cache"'))
+ ).toBe(true);
});
- it('returns explicit unsupported error for non-supported families', () => {
+ it('returns missing-header error for unsupported diagram types like gitGraph', () => {
const result = parseMermaidByType(`
gitGraph
commit id: "A"
commit id: "B"
`);
- expect(result.diagramType).toBe('gitGraph');
- expect(result.error).toContain('not supported yet in editable mode');
+ expect(result.error).toContain('Missing chart type declaration');
expect(result.nodes).toHaveLength(0);
expect(result.edges).toHaveLength(0);
});
diff --git a/src/services/mermaid/parseMermaidByType.ts b/src/services/mermaid/parseMermaidByType.ts
index 3ece8892..787af22a 100644
--- a/src/services/mermaid/parseMermaidByType.ts
+++ b/src/services/mermaid/parseMermaidByType.ts
@@ -13,19 +13,31 @@ export interface ParseMermaidByTypeOptions {
architectureStrictMode?: boolean;
}
-const SUPPORTED_MERMAID_FAMILIES: DiagramType[] = ['flowchart', 'stateDiagram', 'classDiagram', 'erDiagram', 'mindmap', 'journey', 'architecture'];
+const SUPPORTED_MERMAID_FAMILIES: DiagramType[] = [
+ 'flowchart',
+ 'stateDiagram',
+ 'classDiagram',
+ 'erDiagram',
+ 'mindmap',
+ 'journey',
+ 'architecture',
+ 'sequence',
+];
function getUnsupportedTypeError(diagramType: DiagramType): string {
- return `Mermaid "${diagramType}" is not supported yet in editable mode. Supported families: flowchart, stateDiagram, classDiagram, erDiagram, mindmap, journey, architecture.`;
+ return `Mermaid "${diagramType}" is not supported yet in editable mode. Supported families: flowchart, stateDiagram, classDiagram, erDiagram, mindmap, journey, architecture, sequence.`;
}
-function applyArchitectureStrictMode(result: MermaidDispatchParseResult): MermaidDispatchParseResult {
+function applyArchitectureStrictMode(
+ result: MermaidDispatchParseResult
+): MermaidDispatchParseResult {
const diagnostics = Array.isArray(result.diagnostics) ? result.diagnostics : [];
- const strictViolations = diagnostics.filter((message) => (
- message.startsWith('Invalid architecture ')
- || message.startsWith('Duplicate architecture node id')
- || message.startsWith('Recovered implicit service node')
- ));
+ const strictViolations = diagnostics.filter(
+ (message) =>
+ message.startsWith('Invalid architecture ') ||
+ message.startsWith('Duplicate architecture node id') ||
+ message.startsWith('Recovered implicit service node')
+ );
if (strictViolations.length === 0) {
return result;
@@ -70,7 +82,8 @@ export function parseMermaidByType(
return {
nodes: [],
edges: [],
- error: 'Missing chart type declaration. Start with "flowchart TD", "stateDiagram-v2", or another Mermaid diagram type header.',
+ error:
+ 'Missing chart type declaration. Start with "flowchart TD", "stateDiagram-v2", or another Mermaid diagram type header.',
};
}
diff --git a/src/services/mermaidParser.test.ts b/src/services/mermaidParser.test.ts
index 1ed0ebbb..28010048 100644
--- a/src/services/mermaidParser.test.ts
+++ b/src/services/mermaidParser.test.ts
@@ -2,195 +2,223 @@ import { describe, it, expect } from 'vitest';
import { parseMermaid } from '@/lib/mermaidParser';
describe('mermaidParser', () => {
- it('should parse a basic flowchart with TD direction', () => {
- const input = `
+ it('should parse a basic flowchart with TD direction', () => {
+ const input = `
flowchart TD
A[Start] --> B[End]
`;
- const result = parseMermaid(input);
- expect(result.nodes).toHaveLength(2);
- expect(result.edges).toHaveLength(1);
- expect(result.nodes[0].data.label).toBe('Start');
- expect(result.nodes[1].data.label).toBe('End');
- expect(result.edges[0].source).toBe('A');
- expect(result.edges[0].target).toBe('B');
- });
-
- it('should handle different node types based on shapes', () => {
- const input = `
+ const result = parseMermaid(input);
+ expect(result.nodes).toHaveLength(2);
+ expect(result.edges).toHaveLength(1);
+ expect(result.nodes[0].data.label).toBe('Start');
+ expect(result.nodes[1].data.label).toBe('End');
+ expect(result.edges[0].source).toBe('A');
+ expect(result.edges[0].target).toBe('B');
+ });
+
+ it('should handle different node types based on shapes', () => {
+ const input = `
flowchart TD
S([Start Node])
P[Process Node]
D{Decision Node}
E((End Node))
`;
- const result = parseMermaid(input);
- expect(result.nodes.find(n => n.id === 'S')?.type).toBe('start');
- expect(result.nodes.find(n => n.id === 'P')?.type).toBe('process');
- expect(result.nodes.find(n => n.id === 'D')?.type).toBe('decision');
- expect(result.nodes.find(n => n.id === 'E')?.type).toBe('end');
- });
-
- it('should parse edges with labels', () => {
- const input = `
+ const result = parseMermaid(input);
+ expect(result.nodes.find((n) => n.id === 'S')?.type).toBe('start');
+ expect(result.nodes.find((n) => n.id === 'P')?.type).toBe('process');
+ expect(result.nodes.find((n) => n.id === 'D')?.type).toBe('decision');
+ expect(result.nodes.find((n) => n.id === 'E')?.type).toBe('end');
+ });
+
+ it('should parse edges with labels', () => {
+ const input = `
flowchart TD
A --> |Yes| B
A --> |No| C
`;
- const result = parseMermaid(input);
- expect(result.edges).toHaveLength(2);
- expect(result.edges[0].label).toBe('Yes');
- expect(result.edges[1].label).toBe('No');
- });
-
- it('should handle LR direction', () => {
- const input = `
+ const result = parseMermaid(input);
+ expect(result.edges).toHaveLength(2);
+ expect(result.edges[0].label).toBe('Yes');
+ expect(result.edges[1].label).toBe('No');
+ });
+
+ it('should handle LR direction', () => {
+ const input = `
flowchart LR
A --> B
`;
- const result = parseMermaid(input);
- expect(result.direction).toBe('LR');
- });
-
- it('should return error if no flowchart declaration is found', () => {
- const input = `A --> B`;
- const result = parseMermaid(input);
- expect(result.error).toBeDefined();
- expect(result.nodes).toHaveLength(0);
- });
-
- it('should handle inline node declarations in edges', () => {
- const input = `
+ const result = parseMermaid(input);
+ expect(result.direction).toBe('LR');
+ });
+
+ it('should return error if no flowchart declaration is found', () => {
+ const input = `A --> B`;
+ const result = parseMermaid(input);
+ expect(result.error).toBeDefined();
+ expect(result.nodes).toHaveLength(0);
+ });
+
+ it('should handle inline node declarations in edges', () => {
+ const input = `
flowchart TD
A[Node A] --> B((Node B))
`;
- const result = parseMermaid(input);
- expect(result.nodes).toHaveLength(2);
- expect(result.nodes.find(n => n.id === 'A')?.data.label).toBe('Node A');
- expect(result.nodes.find(n => n.id === 'B')?.type).toBe('end');
- });
+ const result = parseMermaid(input);
+ expect(result.nodes).toHaveLength(2);
+ expect(result.nodes.find((n) => n.id === 'A')?.data.label).toBe('Node A');
+ expect(result.nodes.find((n) => n.id === 'B')?.type).toBe('end');
+ });
- // --- NEW TESTS ---
+ // --- NEW TESTS ---
- it('should support "graph TD" keyword (not just flowchart)', () => {
- const input = `
+ it('should support "graph TD" keyword (not just flowchart)', () => {
+ const input = `
graph TD
A[Start] --> B[End]
`;
- const result = parseMermaid(input);
- expect(result.nodes).toHaveLength(2);
- expect(result.edges).toHaveLength(1);
- expect(result.direction).toBe('TB');
- });
-
- it('should strip fa: icon prefixes from labels', () => {
- const input = `
+ const result = parseMermaid(input);
+ expect(result.nodes).toHaveLength(2);
+ expect(result.edges).toHaveLength(1);
+ expect(result.direction).toBe('TB');
+ });
+
+ it('should strip fa: icon prefixes from labels', () => {
+ const input = `
graph TD
Bat(fa:fa-car-battery Batteries) --> ShutOff[Shut Off]
`;
- const result = parseMermaid(input);
- expect(result.nodes.find(n => n.id === 'Bat')?.data.label).toBe('Batteries');
- expect(result.nodes.find(n => n.id === 'ShutOff')?.data.label).toBe('Shut Off');
- });
+ const result = parseMermaid(input);
+ expect(result.nodes.find((n) => n.id === 'Bat')?.data.label).toBe('Batteries');
+ expect(result.nodes.find((n) => n.id === 'ShutOff')?.data.label).toBe('Shut Off');
+ });
- it('should handle chained edges: A --> B --> C', () => {
- const input = `
+ it('should handle modern @{shape: name} syntax', () => {
+ const input = `
+ flowchart TD
+ A@{shape: cyl}[(Database)]
+ B@{shape: diamond}{Is Valid?}
+ C@{shape: stadium}[Start]
+ `;
+ const result = parseMermaid(input);
+ expect(result.nodes).toHaveLength(3);
+ expect(result.nodes.find((n) => n.id === 'A')?.type).toBe('process');
+ expect(result.nodes.find((n) => n.id === 'A')?.data.shape).toBe('cylinder');
+ expect(result.nodes.find((n) => n.id === 'B')?.type).toBe('decision');
+ expect(result.nodes.find((n) => n.id === 'B')?.data.shape).toBe('diamond');
+ expect(result.nodes.find((n) => n.id === 'C')?.type).toBe('start');
+ });
+
+ it('should strip markdown from labels', () => {
+ const input = `
+ flowchart TD
+ A[**Bold** text] --> B[*Italic* label]
+ `;
+ const result = parseMermaid(input);
+ expect(result.nodes.find((n) => n.id === 'A')?.data.label).toBe('Bold text');
+ expect(result.nodes.find((n) => n.id === 'B')?.data.label).toBe('Italic label');
+ });
+
+ it('should handle chained edges: A --> B --> C', () => {
+ const input = `
flowchart TD
A --> B --> C
`;
- const result = parseMermaid(input);
- expect(result.nodes).toHaveLength(3);
- expect(result.edges).toHaveLength(2);
- expect(result.edges[0].source).toBe('A');
- expect(result.edges[0].target).toBe('B');
- expect(result.edges[1].source).toBe('B');
- expect(result.edges[1].target).toBe('C');
- });
-
- it('should handle chained edges with labels', () => {
- const input = `
+ const result = parseMermaid(input);
+ expect(result.nodes).toHaveLength(3);
+ expect(result.edges).toHaveLength(2);
+ expect(result.edges[0].source).toBe('A');
+ expect(result.edges[0].target).toBe('B');
+ expect(result.edges[1].source).toBe('B');
+ expect(result.edges[1].target).toBe('C');
+ });
+
+ it('should handle chained edges with labels', () => {
+ const input = `
flowchart TD
Fuse -->|1.5a| Switch -->|1.5a| Wifi
`;
- const result = parseMermaid(input);
- expect(result.edges).toHaveLength(2);
- expect(result.edges[0].source).toBe('Fuse');
- expect(result.edges[0].target).toBe('Switch');
- expect(result.edges[0].label).toBe('1.5a');
- expect(result.edges[1].source).toBe('Switch');
- expect(result.edges[1].target).toBe('Wifi');
- expect(result.edges[1].label).toBe('1.5a');
- });
-
- it('ignores subgraph wrappers instead of creating container nodes', () => {
- const input = `
+ const result = parseMermaid(input);
+ expect(result.edges).toHaveLength(2);
+ expect(result.edges[0].source).toBe('Fuse');
+ expect(result.edges[0].target).toBe('Switch');
+ expect(result.edges[0].label).toBe('1.5a');
+ expect(result.edges[1].source).toBe('Switch');
+ expect(result.edges[1].target).toBe('Wifi');
+ expect(result.edges[1].label).toBe('1.5a');
+ });
+
+ it('creates section nodes for subgraph wrappers and sets parentId on children', () => {
+ const input = `
flowchart TD
subgraph Services
API[API]
+ DB[(Database)]
end
`;
- const result = parseMermaid(input);
- const apiNode = result.nodes.find((node) => node.id === 'API');
- expect(result.nodes).toHaveLength(1);
- expect(apiNode?.parentId).toBeUndefined();
- });
-
- it('should handle duplicate edges between same pair', () => {
- const input = `
+ const result = parseMermaid(input);
+ expect(result.nodes.length).toBeGreaterThanOrEqual(3);
+ const sectionNode = result.nodes.find((node) => node.type === 'section');
+ expect(sectionNode).toBeDefined();
+ expect(sectionNode?.data.label).toBe('Services');
+ const apiNode = result.nodes.find((node) => node.id === 'API');
+ expect(apiNode?.parentId).toBe(sectionNode?.id);
+ const dbNode = result.nodes.find((node) => node.id === 'DB');
+ expect(dbNode?.parentId).toBe(sectionNode?.id);
+ });
+
+ it('should handle duplicate edges between same pair', () => {
+ const input = `
flowchart TD
Fuse -->|10a| Cig1[Cigarette Lighter]
Fuse -->|10a| Cig1
`;
- const result = parseMermaid(input);
- expect(result.edges).toHaveLength(2);
- expect(result.edges[0].source).toBe('Fuse');
- expect(result.edges[1].source).toBe('Fuse');
- });
+ const result = parseMermaid(input);
+ expect(result.edges).toHaveLength(2);
+ expect(result.edges[0].source).toBe('Fuse');
+ expect(result.edges[1].source).toBe('Fuse');
+ });
- it('should return direction in ParseResult', () => {
- const lr = parseMermaid('flowchart LR\n A --> B');
- expect(lr.direction).toBe('LR');
+ it('should return direction in ParseResult', () => {
+ const lr = parseMermaid('flowchart LR\n A --> B');
+ expect(lr.direction).toBe('LR');
- const rl = parseMermaid('graph RL\n A --> B');
- expect(rl.direction).toBe('RL');
+ const rl = parseMermaid('graph RL\n A --> B');
+ expect(rl.direction).toBe('RL');
- const bt = parseMermaid('flowchart BT\n A --> B');
- expect(bt.direction).toBe('BT');
- });
+ const bt = parseMermaid('flowchart BT\n A --> B');
+ expect(bt.direction).toBe('BT');
+ });
- it('should skip linkStyle, classDef, style directives gracefully', () => {
- const input = `
+ it('should skip linkStyle, classDef, style directives gracefully', () => {
+ const input = `
graph TD
A --> B
linkStyle 0 stroke-width:2px,fill:none,stroke:red;
classDef default fill:#f9f
style A fill:#bbf
`;
- const result = parseMermaid(input);
- expect(result.nodes).toHaveLength(2);
- expect(result.edges).toHaveLength(1);
- expect(result.error).toBeUndefined();
- });
-
- it('should parse linkStyle and apply stroke color to edges', () => {
- const input = `
+ const result = parseMermaid(input);
+ expect(result.nodes).toHaveLength(2);
+ expect(result.edges).toHaveLength(1);
+ expect(result.error).toBeUndefined();
+ });
+
+ it('should parse linkStyle and apply stroke color to edges', () => {
+ const input = `
graph TD
A --> B
B --> C
linkStyle 0 stroke-width:2px,fill:none,stroke:red;
linkStyle 1 stroke-width:2px,fill:none,stroke:green;
`;
- const result = parseMermaid(input);
- expect(result.edges[0].style).toEqual(
- expect.objectContaining({ stroke: 'red' })
- );
- expect(result.edges[1].style).toEqual(
- expect.objectContaining({ stroke: 'green' })
- );
- });
-
- it('should handle the full battery diagram', () => {
- const input = `graph TD
+ const result = parseMermaid(input);
+ expect(result.edges[0].style).toEqual(expect.objectContaining({ stroke: 'red' }));
+ expect(result.edges[1].style).toEqual(expect.objectContaining({ stroke: 'green' }));
+ });
+
+ it('should handle the full battery diagram', () => {
+ const input = `graph TD
Bat(fa:fa-car-battery Batteries) -->|150a 50mm| ShutOff
Bat -->|150a 50mm| Shunt
@@ -236,125 +264,117 @@ describe('mermaidParser', () => {
linkStyle 18 stroke-width:2px,fill:none,stroke:green;
linkStyle 19 stroke-width:2px,fill:none,stroke:green;`;
- const result = parseMermaid(input);
-
- // Should have no errors
- expect(result.error).toBeUndefined();
-
- // Direction should be TB
- expect(result.direction).toBe('TB');
-
- // Should find all unique nodes
- const nodeIds = result.nodes.map(n => n.id);
- expect(nodeIds).toContain('Bat');
- expect(nodeIds).toContain('ShutOff');
- expect(nodeIds).toContain('Shunt');
- expect(nodeIds).toContain('BusPos');
- expect(nodeIds).toContain('BusNeg');
- expect(nodeIds).toContain('Fuse');
- expect(nodeIds).toContain('Old');
- expect(nodeIds).toContain('USB');
- expect(nodeIds).toContain('Switch');
- expect(nodeIds).toContain('Wifi');
- expect(nodeIds).toContain('Cig1');
- expect(nodeIds).toContain('Cig2');
- expect(nodeIds).toContain('Solar');
- expect(nodeIds).toContain('SolarCont');
-
- // Check labels
- expect(result.nodes.find(n => n.id === 'Bat')?.data.label).toBe('Batteries');
- expect(result.nodes.find(n => n.id === 'ShutOff')?.data.label).toBe('Shut Off');
- expect(result.nodes.find(n => n.id === 'BusPos')?.data.label).toBe('Bus Bar +');
- expect(result.nodes.find(n => n.id === 'USB')?.data.label).toBe('USB-C');
-
- // Check that Old is a decision node (diamond shape)
- expect(result.nodes.find(n => n.id === 'Old')?.type).toBe('decision');
-
- // Should have many edges (20 in the original)
- expect(result.edges.length).toBeGreaterThanOrEqual(18);
-
- // Check edge labels
- const batToShutoff = result.edges.find(e => e.source === 'Bat' && e.target === 'ShutOff');
- expect(batToShutoff?.label).toBe('150a 50mm');
-
- // Check linkStyle applied colors
- expect(result.edges[0].style).toEqual(
- expect.objectContaining({ stroke: 'red' })
- );
- });
-
- it('should handle dotted arrow -.-> ', () => {
- const input = `
+ const result = parseMermaid(input);
+
+ // Should have no errors
+ expect(result.error).toBeUndefined();
+
+ // Direction should be TB
+ expect(result.direction).toBe('TB');
+
+ // Should find all unique nodes
+ const nodeIds = result.nodes.map((n) => n.id);
+ expect(nodeIds).toContain('Bat');
+ expect(nodeIds).toContain('ShutOff');
+ expect(nodeIds).toContain('Shunt');
+ expect(nodeIds).toContain('BusPos');
+ expect(nodeIds).toContain('BusNeg');
+ expect(nodeIds).toContain('Fuse');
+ expect(nodeIds).toContain('Old');
+ expect(nodeIds).toContain('USB');
+ expect(nodeIds).toContain('Switch');
+ expect(nodeIds).toContain('Wifi');
+ expect(nodeIds).toContain('Cig1');
+ expect(nodeIds).toContain('Cig2');
+ expect(nodeIds).toContain('Solar');
+ expect(nodeIds).toContain('SolarCont');
+
+ // Check labels
+ expect(result.nodes.find((n) => n.id === 'Bat')?.data.label).toBe('Batteries');
+ expect(result.nodes.find((n) => n.id === 'ShutOff')?.data.label).toBe('Shut Off');
+ expect(result.nodes.find((n) => n.id === 'BusPos')?.data.label).toBe('Bus Bar +');
+ expect(result.nodes.find((n) => n.id === 'USB')?.data.label).toBe('USB-C');
+
+ // Check that Old is a decision node (diamond shape)
+ expect(result.nodes.find((n) => n.id === 'Old')?.type).toBe('decision');
+
+ // Should have many edges (20 in the original)
+ expect(result.edges.length).toBeGreaterThanOrEqual(18);
+
+ // Check edge labels
+ const batToShutoff = result.edges.find((e) => e.source === 'Bat' && e.target === 'ShutOff');
+ expect(batToShutoff?.label).toBe('150a 50mm');
+
+ // Check linkStyle applied colors
+ expect(result.edges[0].style).toEqual(expect.objectContaining({ stroke: 'red' }));
+ });
+
+ it('should handle dotted arrow -.-> ', () => {
+ const input = `
flowchart TD
A -.-> B
`;
- const result = parseMermaid(input);
- expect(result.edges).toHaveLength(1);
- expect(result.edges[0].style).toEqual(
- expect.objectContaining({ strokeDasharray: '5 3' })
- );
- });
-
- it('should handle thick arrow ==>', () => {
- const input = `
+ const result = parseMermaid(input);
+ expect(result.edges).toHaveLength(1);
+ expect(result.edges[0].style).toEqual(expect.objectContaining({ strokeDasharray: '5 3' }));
+ });
+
+ it('should handle thick arrow ==>', () => {
+ const input = `
flowchart TD
A ==> B
`;
- const result = parseMermaid(input);
- expect(result.edges).toHaveLength(1);
- expect(result.edges[0].style).toEqual(
- expect.objectContaining({ strokeWidth: 4 })
- );
- });
-
- it('should handle thick arrow ==> with inline label', () => {
- const input = `
+ const result = parseMermaid(input);
+ expect(result.edges).toHaveLength(1);
+ expect(result.edges[0].style).toEqual(expect.objectContaining({ strokeWidth: 4 }));
+ });
+
+ it('should handle thick arrow ==> with inline label', () => {
+ const input = `
flowchart TD
A == Yes ==> B
`;
- const result = parseMermaid(input);
- expect(result.edges).toHaveLength(1);
- expect(result.edges[0].style).toEqual(
- expect.objectContaining({ strokeWidth: 4 })
- );
- expect(result.edges[0].label).toBe('Yes');
- });
-
- it('should handle reverse arrow <-- with markerStart', () => {
- const input = `
+ const result = parseMermaid(input);
+ expect(result.edges).toHaveLength(1);
+ expect(result.edges[0].style).toEqual(expect.objectContaining({ strokeWidth: 4 }));
+ expect(result.edges[0].label).toBe('Yes');
+ });
+
+ it('should handle reverse arrow <-- with markerStart', () => {
+ const input = `
flowchart TD
A <-- B
`;
- const result = parseMermaid(input);
- expect(result.edges).toHaveLength(1);
- expect(result.edges[0].markerStart).toBeDefined();
- expect(result.edges[0].markerEnd).toBeUndefined();
- });
-
- it('should handle bidirectional arrow <--> with both markers', () => {
- const input = `
+ const result = parseMermaid(input);
+ expect(result.edges).toHaveLength(1);
+ expect(result.edges[0].markerStart).toBeDefined();
+ expect(result.edges[0].markerEnd).toBeUndefined();
+ });
+
+ it('should handle bidirectional arrow <--> with both markers', () => {
+ const input = `
flowchart TD
A <--> B
`;
- const result = parseMermaid(input);
- expect(result.edges).toHaveLength(1);
- expect(result.edges[0].markerStart).toBeDefined();
- expect(result.edges[0].markerEnd).toBeDefined();
- });
-
- it('should handle multiline quoted strings', () => {
- const input = `
+ const result = parseMermaid(input);
+ expect(result.edges).toHaveLength(1);
+ expect(result.edges[0].markerStart).toBeDefined();
+ expect(result.edges[0].markerEnd).toBeDefined();
+ });
+
+ it('should handle multiline quoted strings', () => {
+ const input = `
graph TD
A["Line 1
Line 2"]
`;
- const result = parseMermaid(input);
- expect(result.nodes).toHaveLength(1);
- expect(result.nodes[0].data.label).toBe('Line 1\nLine 2');
- });
+ const result = parseMermaid(input);
+ expect(result.nodes).toHaveLength(1);
+ expect(result.nodes[0].data.label).toBe('Line 1\nLine 2');
+ });
- it('should handle the Service Learning example', () => {
- const input = `graph TB
+ it('should handle the Service Learning example', () => {
+ const input = `graph TB
A("Do you think online service
learning is right for you?")
B("Do you have time to design
@@ -375,26 +395,26 @@ D--No-->E
E--Yes-->F
E--No-->C`;
- const result = parseMermaid(input);
+ const result = parseMermaid(input);
- // Should parse 6 nodes
- expect(result.nodes).toHaveLength(6);
- // ID A should have multiline label
- const nodeA = result.nodes.find(n => n.id === 'A');
- expect(nodeA).toBeDefined();
- expect(nodeA?.data.label).toContain('online service\nlearning');
+ // Should parse 6 nodes
+ expect(result.nodes).toHaveLength(6);
+ // ID A should have multiline label
+ const nodeA = result.nodes.find((n) => n.id === 'A');
+ expect(nodeA).toBeDefined();
+ expect(nodeA?.data.label).toContain('online service\nlearning');
- // Should parse 8 edges
- expect(result.edges).toHaveLength(8);
+ // Should parse 8 edges
+ expect(result.edges).toHaveLength(8);
- // Check specific edges
- const startYes = result.edges.find(e => e.source === 'A' && e.target === 'B');
- expect(startYes).toBeDefined();
- expect(startYes?.label).toBe('Yes');
- expect(startYes?.style?.strokeWidth).toBe(4); // ==> is thick
+ // Check specific edges
+ const startYes = result.edges.find((e) => e.source === 'A' && e.target === 'B');
+ expect(startYes).toBeDefined();
+ expect(startYes?.label).toBe('Yes');
+ expect(startYes?.style?.strokeWidth).toBe(4); // ==> is thick
- const startNo = result.edges.find(e => e.source === 'A' && e.target === 'C');
- expect(startNo).toBeDefined();
- expect(startNo?.label).toBe('No');
- });
+ const startNo = result.edges.find((e) => e.source === 'A' && e.target === 'C');
+ expect(startNo).toBeDefined();
+ expect(startNo?.label).toBe('No');
+ });
});
diff --git a/src/services/openFlowRoundTripGoldenFixtures.ts b/src/services/openFlowRoundTripGoldenFixtures.ts
index b07ad8cd..ffbe9ee8 100644
--- a/src/services/openFlowRoundTripGoldenFixtures.ts
+++ b/src/services/openFlowRoundTripGoldenFixtures.ts
@@ -1,3 +1,4 @@
+import type { CSSProperties } from 'react';
import type { Edge, Node } from '@/lib/reactflowCompat';
export interface OpenFlowRoundTripGoldenFixture {
@@ -39,6 +40,49 @@ function createEdge(id: string, source: string, target: string, label?: string):
} as Edge;
}
+function createArchNode(
+ id: string,
+ label: string,
+ archIconPackId: string,
+ archIconShapeId: string,
+ color: string
+): Node {
+ return {
+ id,
+ type: 'custom',
+ position: { x: 0, y: 0 },
+ data: {
+ label,
+ color,
+ archIconPackId,
+ archIconShapeId,
+ },
+ } as Node;
+}
+
+function createEdgeWithStyle(
+ id: string,
+ source: string,
+ target: string,
+ label?: string,
+ style?: { type?: string; strokeDasharray?: string; strokeWidth?: number }
+): Edge {
+ const edge: Record = {
+ id,
+ source,
+ target,
+ label,
+ };
+ if (style?.type) edge.type = style.type;
+ if (style?.strokeDasharray || style?.strokeWidth) {
+ edge.style = {
+ ...(style.strokeDasharray ? { strokeDasharray: style.strokeDasharray } : {}),
+ ...(style.strokeWidth ? { strokeWidth: style.strokeWidth } : {}),
+ } as CSSProperties;
+ }
+ return edge as Edge;
+}
+
export const OPENFLOW_ROUND_TRIP_GOLDEN_FIXTURES: OpenFlowRoundTripGoldenFixture[] = [
{
name: 'simple-linear',
@@ -74,4 +118,28 @@ export const OPENFLOW_ROUND_TRIP_GOLDEN_FIXTURES: OpenFlowRoundTripGoldenFixture
createEdge('e1', 'n1', 'n3', 'ok'),
],
},
+ {
+ name: 'arch-icons',
+ nodes: [
+ createArchNode('lambda', 'Lambda', 'aws-official-starter-v1', 'compute-lambda', 'violet'),
+ createArchNode('sqs', 'SQS Queue', 'aws-official-starter-v1', 'app-integration-sqs', 'amber'),
+ createArchNode('dynamo', 'DynamoDB', 'aws-official-starter-v1', 'database-dynamodb', 'emerald'),
+ ],
+ edges: [
+ createEdge('e1', 'lambda', 'sqs', 'publish'),
+ createEdge('e2', 'sqs', 'dynamo', 'write'),
+ ],
+ },
+ {
+ name: 'edge-styles',
+ nodes: [
+ createNode('n1', 'Source', 'process'),
+ createNode('n2', 'Dashed Target', 'process'),
+ createNode('n3', 'Curved Target', 'process'),
+ ],
+ edges: [
+ createEdgeWithStyle('e1', 'n1', 'n2', undefined, { strokeDasharray: '5 5' }),
+ createEdgeWithStyle('e2', 'n1', 'n3', 'flow', { type: 'smoothstep' }),
+ ],
+ },
];
diff --git a/src/services/shapeLibrary/providerCatalog.ts b/src/services/shapeLibrary/providerCatalog.ts
index 3816bc5d..cac13b95 100644
--- a/src/services/shapeLibrary/providerCatalog.ts
+++ b/src/services/shapeLibrary/providerCatalog.ts
@@ -1,221 +1,247 @@
import type { DomainLibraryCategory, DomainLibraryItem } from '@/services/domainLibrary';
export interface ProviderShapePreview {
- packId: string;
- shapeId: string;
- label: string;
- category: string;
- previewUrl: string;
+ packId: string;
+ shapeId: string;
+ label: string;
+ category: string;
+ previewUrl: string;
}
interface SvgSource {
- provider: string;
- packId: string;
- shapeId: string;
- label: string;
- category: string;
- previewLoader: () => Promise;
+ provider: string;
+ packId: string;
+ shapeId: string;
+ label: string;
+ category: string;
+ previewLoader: () => Promise;
}
const svgModules = import.meta.glob('../../../assets/third-party-icons/*/processed/**/*.svg', {
- query: '?url',
- import: 'default',
+ query: '?url',
+ import: 'default',
}) as Record Promise>;
const providerCatalogPromiseCache = new Map>();
const shapePreviewCache = new Map();
const shapePreviewPromiseCache = new Map>();
-const KNOWN_PROVIDER_PACK_IDS: Partial> = {
- aws: 'aws-official-starter-v1',
- azure: 'azure-official-icons-v20',
- cncf: 'cncf-artwork-icons-v1',
- developer: 'developer-icons-v1',
+export const KNOWN_PROVIDER_PACK_IDS: Record = {
+ aws: 'aws-official-starter-v1',
+ azure: 'azure-official-icons-v20',
+ gcp: 'gcp-official-icons-v1',
+ cncf: 'cncf-artwork-icons-v1',
+ developer: 'developer-icons-v1',
};
function normalizeProviderPathSegment(value: string): string {
- return value.trim().toLowerCase();
+ return value.trim().toLowerCase();
}
function slugify(value: string): string {
- return value
- .trim()
- .toLowerCase()
- .replace(/[^a-z0-9]+/g, '-')
- .replace(/^-+|-+$/g, '');
+ return value
+ .trim()
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, '-')
+ .replace(/^-+|-+$/g, '');
}
function inferLabelFromId(id: string): string {
- return id
- .split('-')
- .filter(Boolean)
- .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
- .join(' ');
+ return id
+ .split('-')
+ .filter(Boolean)
+ .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
+ .join(' ');
}
function getPackIdForProvider(provider: string): string {
- return KNOWN_PROVIDER_PACK_IDS[provider] ?? `${provider}-processed-pack-v1`;
+ return KNOWN_PROVIDER_PACK_IDS[provider] ?? `${provider}-processed-pack-v1`;
}
-function parseSvgSource(modulePath: string, previewLoader: () => Promise): SvgSource | null {
- const normalized = modulePath.replaceAll('\\', '/');
- const match = normalized.match(/assets\/third-party-icons\/([^/]+)\/processed\/(.+)\.svg$/);
+function getProviderColor(provider: string): string {
+ if (provider === 'aws') {
+ return 'amber';
+ }
- if (!match) {
- return null;
- }
+ if (provider === 'azure') {
+ return 'blue';
+ }
- const provider = normalizeProviderPathSegment(match[1]);
- const relativePath = match[2];
- const pathParts = relativePath.split('/');
- const category = pathParts.length > 1 ? inferLabelFromId(slugify(pathParts[0])) : 'Misc';
- const shapeId = slugify(relativePath.replaceAll('/', '-'));
+ if (provider === 'gcp') {
+ return 'emerald';
+ }
- return {
- provider,
- packId: getPackIdForProvider(provider),
- shapeId,
- label: inferLabelFromId(shapeId),
- category,
- previewLoader,
- };
+ if (provider === 'cncf') {
+ return 'cyan';
+ }
+
+ return 'slate';
}
-const SVG_SOURCES: SvgSource[] = Object.entries(svgModules)
- .map(([modulePath, previewLoader]) => parseSvgSource(modulePath, previewLoader))
- .filter((value): value is SvgSource => value !== null);
-
-function createProviderItem(
- provider: DomainLibraryCategory,
- source: SvgSource,
-): DomainLibraryItem {
- return {
- id: `${source.packId}:${source.shapeId}`,
- category: provider,
- label: source.label,
- description: `${provider.toUpperCase()} ${source.category}`,
- icon: 'Box',
- color: provider === 'aws'
- ? 'amber'
- : provider === 'azure'
- ? 'blue'
- : provider === 'gcp'
- ? 'emerald'
- : provider === 'cncf'
- ? 'cyan'
- : 'slate',
- nodeType: 'custom',
- assetPresentation: 'icon',
- providerShapeCategory: source.category,
- archIconPackId: source.packId,
- archIconShapeId: source.shapeId,
- };
+function parseSvgSource(
+ modulePath: string,
+ previewLoader: () => Promise
+): SvgSource | null {
+ const normalized = modulePath.replaceAll('\\', '/');
+ const match = normalized.match(/assets\/third-party-icons\/([^/]+)\/processed\/(.+)\.svg$/);
+
+ if (!match) {
+ return null;
+ }
+
+ const provider = normalizeProviderPathSegment(match[1]);
+ const relativePath = match[2];
+ const pathParts = relativePath.split('/');
+ const category = pathParts.length > 1 ? inferLabelFromId(slugify(pathParts[0])) : 'Misc';
+ const shapeId = slugify(relativePath.replaceAll('/', '-'));
+
+ return {
+ provider,
+ packId: getPackIdForProvider(provider),
+ shapeId,
+ label: inferLabelFromId(shapeId),
+ category,
+ previewLoader,
+ };
+}
+
+export const SVG_SOURCES: SvgSource[] = Object.entries(svgModules)
+ .map(([modulePath, previewLoader]) => parseSvgSource(modulePath, previewLoader))
+ .filter((value): value is SvgSource => value !== null);
+
+function createProviderItem(provider: DomainLibraryCategory, source: SvgSource): DomainLibraryItem {
+ return {
+ id: `${source.packId}:${source.shapeId}`,
+ category: provider,
+ label: source.label,
+ description: `${provider.toUpperCase()} ${source.category}`,
+ icon: 'Box',
+ color: getProviderColor(provider),
+ nodeType: 'custom',
+ assetPresentation: 'icon',
+ providerShapeCategory: source.category,
+ archIconPackId: source.packId,
+ archIconShapeId: source.shapeId,
+ };
}
export function listProviderCatalogProviders(): string[] {
- return Array.from(new Set(SVG_SOURCES.map((source) => source.provider))).sort((left, right) => left.localeCompare(right));
+ return Array.from(new Set(SVG_SOURCES.map((source) => source.provider))).sort((left, right) =>
+ left.localeCompare(right)
+ );
}
export function getProviderCatalogCount(provider: DomainLibraryCategory): number {
- const normalizedProvider = normalizeProviderPathSegment(provider);
- return SVG_SOURCES.filter((source) => source.provider === normalizedProvider).length;
+ const normalizedProvider = normalizeProviderPathSegment(provider);
+ return SVG_SOURCES.filter((source) => source.provider === normalizedProvider).length;
}
-export async function loadProviderCatalog(provider: DomainLibraryCategory): Promise {
- const normalizedProvider = normalizeProviderPathSegment(provider);
- const existingPromise = providerCatalogPromiseCache.get(normalizedProvider);
- if (existingPromise) {
- return existingPromise;
- }
-
- const catalogPromise = (async () => {
- return SVG_SOURCES
- .filter((source) => source.provider === normalizedProvider)
- .map((source) => createProviderItem(provider, source))
- .sort((left, right) => (
- left.providerShapeCategory === right.providerShapeCategory
- ? left.label.localeCompare(right.label)
- : (left.providerShapeCategory || '').localeCompare(right.providerShapeCategory || '')
- ));
- })();
-
- providerCatalogPromiseCache.set(normalizedProvider, catalogPromise);
- return catalogPromise;
+export async function loadProviderCatalog(
+ provider: DomainLibraryCategory
+): Promise {
+ const normalizedProvider = normalizeProviderPathSegment(provider);
+ const existingPromise = providerCatalogPromiseCache.get(normalizedProvider);
+ if (existingPromise) {
+ return existingPromise;
+ }
+
+ const catalogPromise = (async () => {
+ return SVG_SOURCES.filter((source) => source.provider === normalizedProvider)
+ .map((source) => createProviderItem(provider, source))
+ .sort((left, right) =>
+ left.providerShapeCategory === right.providerShapeCategory
+ ? left.label.localeCompare(right.label)
+ : (left.providerShapeCategory || '').localeCompare(right.providerShapeCategory || '')
+ );
+ })();
+
+ providerCatalogPromiseCache.set(normalizedProvider, catalogPromise);
+ return catalogPromise;
}
interface LoadProviderCatalogSuggestionsOptions {
- category?: string;
- excludeShapeId?: string;
- limit?: number;
- query?: string;
+ category?: string;
+ excludeShapeId?: string;
+ limit?: number;
+ query?: string;
}
export async function loadProviderCatalogSuggestions(
- provider: DomainLibraryCategory,
- options: LoadProviderCatalogSuggestionsOptions = {},
+ provider: DomainLibraryCategory,
+ options: LoadProviderCatalogSuggestionsOptions = {}
): Promise {
- const items = await loadProviderCatalog(provider);
- const normalizedQuery = options.query?.trim().toLowerCase() ?? '';
- const filtered = items.filter((item) => {
- if (options.excludeShapeId && item.archIconShapeId === options.excludeShapeId) {
- return false;
- }
- if (options.category && item.providerShapeCategory !== options.category) {
- return false;
- }
- if (!normalizedQuery) {
- return true;
- }
- return item.label.toLowerCase().includes(normalizedQuery)
- || item.description.toLowerCase().includes(normalizedQuery)
- || (item.providerShapeCategory || '').toLowerCase().includes(normalizedQuery);
- });
-
- const pool = filtered.length > 0 || !options.category
- ? filtered
- : items.filter((item) => (
- (!options.excludeShapeId || item.archIconShapeId !== options.excludeShapeId)
- && (!normalizedQuery
- || item.label.toLowerCase().includes(normalizedQuery)
- || item.description.toLowerCase().includes(normalizedQuery)
- || (item.providerShapeCategory || '').toLowerCase().includes(normalizedQuery))
- ));
-
- return pool.slice(0, options.limit ?? 8);
-}
-
-export async function loadProviderShapePreview(packId: string, shapeId: string): Promise {
- const cacheKey = `${packId}:${shapeId}`;
- const cachedPreview = shapePreviewCache.get(cacheKey);
- if (cachedPreview) {
- return cachedPreview;
+ const items = await loadProviderCatalog(provider);
+ const normalizedQuery = options.query?.trim().toLowerCase() ?? '';
+ const filtered = items.filter((item) => {
+ if (options.excludeShapeId && item.archIconShapeId === options.excludeShapeId) {
+ return false;
}
- const cachedPromise = shapePreviewPromiseCache.get(cacheKey);
- if (cachedPromise) {
- return cachedPromise;
+ if (options.category && item.providerShapeCategory !== options.category) {
+ return false;
}
-
- const source = SVG_SOURCES.find((candidate) => candidate.packId === packId && candidate.shapeId === shapeId);
- if (!source) {
- return null;
+ if (!normalizedQuery) {
+ return true;
}
+ return (
+ item.label.toLowerCase().includes(normalizedQuery) ||
+ item.description.toLowerCase().includes(normalizedQuery) ||
+ (item.providerShapeCategory || '').toLowerCase().includes(normalizedQuery)
+ );
+ });
+
+ const pool =
+ filtered.length > 0 || !options.category
+ ? filtered
+ : items.filter(
+ (item) =>
+ (!options.excludeShapeId || item.archIconShapeId !== options.excludeShapeId) &&
+ (!normalizedQuery ||
+ item.label.toLowerCase().includes(normalizedQuery) ||
+ item.description.toLowerCase().includes(normalizedQuery) ||
+ (item.providerShapeCategory || '').toLowerCase().includes(normalizedQuery))
+ );
+
+ return pool.slice(0, options.limit ?? 8);
+}
+
+export async function loadProviderShapePreview(
+ packId: string,
+ shapeId: string
+): Promise {
+ const cacheKey = `${packId}:${shapeId}`;
+ const cachedPreview = shapePreviewCache.get(cacheKey);
+ if (cachedPreview) {
+ return cachedPreview;
+ }
+ const cachedPromise = shapePreviewPromiseCache.get(cacheKey);
+ if (cachedPromise) {
+ return cachedPromise;
+ }
+
+ const source = SVG_SOURCES.find(
+ (candidate) => candidate.packId === packId && candidate.shapeId === shapeId
+ );
+ if (!source) {
+ return null;
+ }
+
+ const previewPromise = source
+ .previewLoader()
+ .then((previewUrl) => {
+ const preview = {
+ packId,
+ shapeId,
+ label: source.label,
+ category: source.category,
+ previewUrl,
+ };
+ shapePreviewCache.set(cacheKey, preview);
+ shapePreviewPromiseCache.delete(cacheKey);
+ return preview;
+ })
+ .catch((error) => {
+ shapePreviewPromiseCache.delete(cacheKey);
+ throw error;
+ });
- const previewPromise = source.previewLoader()
- .then((previewUrl) => {
- const preview = {
- packId,
- shapeId,
- label: source.label,
- category: source.category,
- previewUrl,
- };
- shapePreviewCache.set(cacheKey, preview);
- shapePreviewPromiseCache.delete(cacheKey);
- return preview;
- })
- .catch((error) => {
- shapePreviewPromiseCache.delete(cacheKey);
- throw error;
- });
-
- shapePreviewPromiseCache.set(cacheKey, previewPromise);
- return previewPromise;
+ shapePreviewPromiseCache.set(cacheKey, previewPromise);
+ return previewPromise;
}
diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo
index a440610e..b87453cc 100644
--- a/tsconfig.tsbuildinfo
+++ b/tsconfig.tsbuildinfo
@@ -1 +1 @@
-{"root":["./src/app.test.tsx","./src/app.tsx","./src/constants.test.ts","./src/constants.ts","./src/index.tsx","./src/store.test.ts","./src/store.ts","./src/theme.test.ts","./src/theme.ts","./src/app/routestate.test.ts","./src/app/routestate.ts","./src/components/annotationnode.tsx","./src/components/architecturerulespanel.tsx","./src/components/cinematicexportoverlay.tsx","./src/components/commandbar.test.tsx","./src/components/commandbar.tsx","./src/components/connectmenu.test.tsx","./src/components/connectmenu.tsx","./src/components/connectmenusections.tsx","./src/components/contextmenu.test.tsx","./src/components/contextmenu.tsx","./src/components/customconnectionline.test.tsx","./src/components/customconnectionline.tsx","./src/components/customedge.tsx","./src/components/customnode.handleinteraction.test.tsx","./src/components/customnode.tsx","./src/components/customnodecontent.tsx","./src/components/designsystem.integration.test.tsx","./src/components/diagramviewer.tsx","./src/components/errorboundary.tsx","./src/components/exportmenu.tsx","./src/components/exportmenupanel.test.tsx","./src/components/exportmenupanel.tsx","./src/components/flowcanvas.tsx","./src/components/floweditor.tsx","./src/components/floweditoremptystate.tsx","./src/components/floweditorlayoutoverlay.tsx","./src/components/floweditorpanels.test.tsx","./src/components/floweditorpanels.tsx","./src/components/flowtabs.test.tsx","./src/components/flowtabs.tsx","./src/components/groupnode.tsx","./src/components/homepage.integration.test.tsx","./src/components/homepage.tsx","./src/components/iconassetnodebody.tsx","./src/components/iconmap.test.tsx","./src/components/iconmap.ts","./src/components/imagenode.tsx","./src/components/importrecoverydialog.test.tsx","./src/components/importrecoverydialog.tsx","./src/components/inlinetexteditsurface.test.tsx","./src/components/inlinetexteditsurface.tsx","./src/components/keyboardshortcutsmodal.tsx","./src/components/languageselector.tsx","./src/components/markdownrenderer.tsx","./src/components/memoizedmarkdown.tsx","./src/components/navigationcontrols.tsx","./src/components/nodebadges.tsx","./src/components/nodechrome.tsx","./src/components/nodequickcreatebuttons.tsx","./src/components/nodeshapesvg.tsx","./src/components/nodetransformcontrols.tsx","./src/components/playbackcontrols.tsx","./src/components/propertiespanel.tsx","./src/components/rightrail.tsx","./src/components/sectionnode.tsx","./src/components/shareembedmodal.tsx","./src/components/sharemodal.test.tsx","./src/components/sharemodal.tsx","./src/components/sidebarshell.tsx","./src/components/snapshotspanel.test.tsx","./src/components/snapshotspanel.tsx","./src/components/studioaipanel.test.tsx","./src/components/studioaipanel.tsx","./src/components/studioaipanelsections.tsx","./src/components/studiocodepanel.test.tsx","./src/components/studiocodepanel.tsx","./src/components/studiopanel.test.tsx","./src/components/studiopanel.tsx","./src/components/studioplaybackpanel.test.tsx","./src/components/studioplaybackpanel.tsx","./src/components/swimlanenode.tsx","./src/components/textnode.tsx","./src/components/themetoggle.tsx","./src/components/toolbar.tsx","./src/components/tooltip.tsx","./src/components/topnav.tsx","./src/components/welcomemodal.tsx","./src/components/annotationtheme.ts","./src/components/container-nodes.handleinteraction.test.tsx","./src/components/editorsurfacetiers.ts","./src/components/handleinteraction.test.ts","./src/components/handleinteraction.ts","./src/components/handleinteractionusage.test.ts","./src/components/lightweight-nodes.handleinteraction.test.tsx","./src/components/markdownsyntax.ts","./src/components/nodehelpers.test.ts","./src/components/nodehelpers.ts","./src/components/sharemodalcontent.ts","./src/components/studioaicopy.ts","./src/components/studioaipanelexamples.ts","./src/components/transformdiagnostics.test.ts","./src/components/transformdiagnostics.ts","./src/components/useactivenodeselection.ts","./src/components/useexportmenu.test.tsx","./src/components/useexportmenu.ts","./src/components/settingsmodal/aisettings.tsx","./src/components/settingsmodal/canvassettings.tsx","./src/components/settingsmodal/generalsettings.test.tsx","./src/components/settingsmodal/generalsettings.tsx","./src/components/settingsmodal/settingsmodal.test.tsx","./src/components/settingsmodal/settingsmodal.tsx","./src/components/settingsmodal/shortcutssettings.tsx","./src/components/settingsmodal/ai/advancedendpointsection.tsx","./src/components/settingsmodal/ai/customheaderseditor.tsx","./src/components/settingsmodal/ai/privacysection.tsx","./src/components/settingsmodal/ai/providericon.tsx","./src/components/settingsmodal/ai/providersection.tsx","./src/components/settingsmodal/ai/step.tsx","./src/components/add-items/additemregistry.tsx","./src/components/app/docssiteredirect.test.tsx","./src/components/app/docssiteredirect.tsx","./src/components/app/mobileworkspacegate.test.tsx","./src/components/app/mobileworkspacegate.tsx","./src/components/app/routeloadingfallback.test.tsx","./src/components/app/routeloadingfallback.tsx","./src/components/app/mobileworkspacegatecopy.ts","./src/components/app/routeloadingcopy.ts","./src/components/architecture-lint/lintrulespanel.tsx","./src/components/architecture-lint/ruleform.tsx","./src/components/architecture-lint/visualeditor.tsx","./src/components/command-bar/assetsview.test.tsx","./src/components/command-bar/assetsview.tsx","./src/components/command-bar/codebaseimportpanels.tsx","./src/components/command-bar/codebaseimportsection.tsx","./src/components/command-bar/designsystemeditor.tsx","./src/components/command-bar/designsystemview.tsx","./src/components/command-bar/diagramminipreview.tsx","./src/components/command-bar/figmaimportpanel.tsx","./src/components/command-bar/importsurfaceprimitives.tsx","./src/components/command-bar/importview.tsx","./src/components/command-bar/importviewpanels.tsx","./src/components/command-bar/layersview.tsx","./src/components/command-bar/layoutview.tsx","./src/components/command-bar/pagesview.tsx","./src/components/command-bar/rootview.test.tsx","./src/components/command-bar/rootview.tsx","./src/components/command-bar/searchview.tsx","./src/components/command-bar/templatesview.test.tsx","./src/components/command-bar/templatesview.tsx","./src/components/command-bar/viewheader.tsx","./src/components/command-bar/visualsview.tsx","./src/components/command-bar/applycodechanges.ts","./src/components/command-bar/assetsviewconstants.ts","./src/components/command-bar/importdetection.test.ts","./src/components/command-bar/importdetection.ts","./src/components/command-bar/importnativeparsers.ts","./src/components/command-bar/importviewmodel.test.ts","./src/components/command-bar/importviewmodel.ts","./src/components/command-bar/mermaidimportparser.ts","./src/components/command-bar/searchquery.test.ts","./src/components/command-bar/searchquery.ts","./src/components/command-bar/types.ts","./src/components/command-bar/useaiviewstate.test.tsx","./src/components/command-bar/useaiviewstate.ts","./src/components/command-bar/usecloudassetcatalog.ts","./src/components/command-bar/usecommandbarcommands.test.tsx","./src/components/command-bar/usecommandbarcommands.tsx","./src/components/custom-edge/customedgewrapper.tsx","./src/components/custom-edge/edgemarkerdefs.tsx","./src/components/custom-edge/sequencemessageedge.test.tsx","./src/components/custom-edge/sequencemessageedge.tsx","./src/components/custom-edge/animatededgepresentation.test.ts","./src/components/custom-edge/animatededgepresentation.ts","./src/components/custom-edge/classrelationsemantics.test.ts","./src/components/custom-edge/classrelationsemantics.ts","./src/components/custom-edge/edgerendermode.ts","./src/components/custom-edge/pathutils.test.ts","./src/components/custom-edge/pathutils.ts","./src/components/custom-edge/pathutilsgeometry.ts","./src/components/custom-edge/pathutilssiblingrouting.ts","./src/components/custom-edge/pathutilstypes.ts","./src/components/custom-edge/relationroutingsemantics.test.ts","./src/components/custom-edge/relationroutingsemantics.ts","./src/components/custom-edge/standardedgemarkers.test.ts","./src/components/custom-edge/standardedgemarkers.ts","./src/components/custom-nodes/architecturenode.handleinteraction.test.tsx","./src/components/custom-nodes/architecturenode.tsx","./src/components/custom-nodes/browsernode.tsx","./src/components/custom-nodes/classentitynode.handleinteraction.test.tsx","./src/components/custom-nodes/classnode.tsx","./src/components/custom-nodes/entitynode.tsx","./src/components/custom-nodes/journeynode.tsx","./src/components/custom-nodes/mindmapnode.tsx","./src/components/custom-nodes/mobilenode.tsx","./src/components/custom-nodes/sequencenotenode.tsx","./src/components/custom-nodes/sequenceparticipantnode.tsx","./src/components/custom-nodes/structurednodehandles.tsx","./src/components/custom-nodes/visualnodes.handleinteraction.test.tsx","./src/components/custom-nodes/browservariantrenderer.tsx","./src/components/custom-nodes/mobilevariantrenderer.tsx","./src/components/custom-nodes/structuredlistnavigation.test.ts","./src/components/custom-nodes/structuredlistnavigation.ts","./src/components/custom-nodes/variantconstants.ts","./src/components/diagram-diff/diffmodebanner.tsx","./src/components/flow-canvas/flowcanvasalignmentguidesoverlay.tsx","./src/components/flow-canvas/flowcanvasoverlays.tsx","./src/components/flow-canvas/flowcanvassurface.test.tsx","./src/components/flow-canvas/flowcanvassurface.tsx","./src/components/flow-canvas/streamingoverlay.tsx","./src/components/flow-canvas/alignmentguides.test.ts","./src/components/flow-canvas/alignmentguides.ts","./src/components/flow-canvas/flowcanvasreactflowcontracts.ts","./src/components/flow-canvas/flowcanvastypes.test.ts","./src/components/flow-canvas/flowcanvastypes.tsx","./src/components/flow-canvas/largegraphsafetymode.test.ts","./src/components/flow-canvas/largegraphsafetymode.ts","./src/components/flow-canvas/nodechromecoverage.test.ts","./src/components/flow-canvas/pastehelpers.test.ts","./src/components/flow-canvas/pastehelpers.ts","./src/components/flow-canvas/useflowcanvasalignmentguides.ts","./src/components/flow-canvas/useflowcanvasconnectionstate.ts","./src/components/flow-canvas/useflowcanvascontextactions.ts","./src/components/flow-canvas/useflowcanvasdragdrop.ts","./src/components/flow-canvas/useflowcanvasinteractionlod.ts","./src/components/flow-canvas/useflowcanvasmenus.ts","./src/components/flow-canvas/useflowcanvasmenusandactions.test.tsx","./src/components/flow-canvas/useflowcanvasmenusandactions.ts","./src/components/flow-canvas/useflowcanvasnodedraghandlers.ts","./src/components/flow-canvas/useflowcanvaspaste.ts","./src/components/flow-canvas/useflowcanvasreactflowconfig.test.ts","./src/components/flow-canvas/useflowcanvasreactflowconfig.ts","./src/components/flow-canvas/useflowcanvasselectiontools.test.tsx","./src/components/flow-canvas/useflowcanvasselectiontools.ts","./src/components/flow-canvas/useflowcanvasviewstate.test.ts","./src/components/flow-canvas/useflowcanvasviewstate.ts","./src/components/flow-canvas/useflowcanvaszoomlod.ts","./src/components/flow-editor/collaborationpresenceoverlay.test.tsx","./src/components/flow-editor/collaborationpresenceoverlay.tsx","./src/components/flow-editor/floweditorchrome.tsx","./src/components/flow-editor/buildfloweditorcontrollerparams.ts","./src/components/flow-editor/buildfloweditorscreencontrollerparams.ts","./src/components/flow-editor/chromeproptypes.ts","./src/components/flow-editor/floweditorchromeprops.ts","./src/components/flow-editor/infradslapply.test.ts","./src/components/flow-editor/infradslapply.ts","./src/components/flow-editor/panelprops.test.ts","./src/components/flow-editor/panelprops.ts","./src/components/flow-editor/shouldexitstudioonselection.test.ts","./src/components/flow-editor/shouldexitstudioonselection.ts","./src/components/flow-editor/usecollaborationnodepositions.ts","./src/components/flow-editor/usefloweditorcontroller.ts","./src/components/flow-editor/usefloweditorinteractionbindings.ts","./src/components/flow-editor/usefloweditorpanelactions.ts","./src/components/flow-editor/usefloweditorpanelprops.ts","./src/components/flow-editor/usefloweditorruntime.ts","./src/components/flow-editor/usefloweditorscreenbehavior.ts","./src/components/flow-editor/usefloweditorscreenmodel.ts","./src/components/flow-editor/usefloweditorscreenstate.ts","./src/components/flow-editor/usefloweditorshellcontroller.test.tsx","./src/components/flow-editor/usefloweditorshellcontroller.ts","./src/components/flow-editor/usefloweditorstudiocontroller.test.tsx","./src/components/flow-editor/usefloweditorstudiocontroller.ts","./src/components/flow-editor/useinfradslapply.ts","./src/components/home/githubcard.tsx","./src/components/home/homedashboard.tsx","./src/components/home/homeflowdialogs.tsx","./src/components/home/homesettingsview.tsx","./src/components/home/homesidebar.tsx","./src/components/home/hometemplatesview.tsx","./src/components/home/sidebarfooter.tsx","./src/components/home/welcomemodalstate.ts","./src/components/icons/assetsicon.tsx","./src/components/icons/openflowlogo.tsx","./src/components/infra-sync/infrasyncpanel.test.tsx","./src/components/infra-sync/infrasyncpanel.tsx","./src/components/journey/journeyscorecontrol.test.tsx","./src/components/journey/journeyscorecontrol.tsx","./src/components/properties/bulknodeproperties.test.tsx","./src/components/properties/bulknodeproperties.tsx","./src/components/properties/bulknodepropertiesfamilysections.tsx","./src/components/properties/bulknodepropertiessections.tsx","./src/components/properties/bulknodepropertiesutilitysections.tsx","./src/components/properties/colorpicker.test.tsx","./src/components/properties/colorpicker.tsx","./src/components/properties/customcolorpopover.tsx","./src/components/properties/diagramnodepropertiesrouter.test.ts","./src/components/properties/diagramnodepropertiesrouter.tsx","./src/components/properties/edgeproperties.tsx","./src/components/properties/iconpicker.tsx","./src/components/properties/icontilepickerprimitives.tsx","./src/components/properties/imageupload.tsx","./src/components/properties/inspectorprimitives.tsx","./src/components/properties/nodeactionbuttons.tsx","./src/components/properties/nodecontentsection.tsx","./src/components/properties/nodeimagesettingssection.tsx","./src/components/properties/nodeproperties.test.tsx","./src/components/properties/nodeproperties.tsx","./src/components/properties/nodewireframevariantsection.tsx","./src/components/properties/propertysliderrow.tsx","./src/components/properties/segmentedchoice.tsx","./src/components/properties/shapeselector.tsx","./src/components/properties/swatchpicker.tsx","./src/components/properties/bulknodepropertiesmodel.test.ts","./src/components/properties/bulknodepropertiesmodel.ts","./src/components/properties/colorpickerutils.ts","./src/components/properties/propertyinputbehavior.test.ts","./src/components/properties/propertyinputbehavior.ts","./src/components/properties/sectionactionbuilder.ts","./src/components/properties/shared.ts","./src/components/properties/wireframevariants.ts","./src/components/properties/edge/architectureedgesemanticssection.tsx","./src/components/properties/edge/edgecolorsection.test.tsx","./src/components/properties/edge/edgecolorsection.tsx","./src/components/properties/edge/edgeconditionsection.tsx","./src/components/properties/edge/edgelabelsection.tsx","./src/components/properties/edge/edgerelationsection.test.tsx","./src/components/properties/edge/edgerelationsection.tsx","./src/components/properties/edge/edgeroutesection.test.tsx","./src/components/properties/edge/edgeroutesection.tsx","./src/components/properties/edge/edgestylesection.tsx","./src/components/properties/edge/sequencemessagesection.tsx","./src/components/properties/edge/architecturesemantics.test.ts","./src/components/properties/edge/architecturesemantics.ts","./src/components/properties/edge/edgecolorutils.ts","./src/components/properties/edge/edgelabelmodel.test.ts","./src/components/properties/edge/edgelabelmodel.ts","./src/components/properties/edge/errelationoptions.ts","./src/components/properties/families/architecturenodeproperties.test.tsx","./src/components/properties/families/architecturenodeproperties.tsx","./src/components/properties/families/architecturenodesection.tsx","./src/components/properties/families/classdiagramnodeproperties.tsx","./src/components/properties/families/classmemberlisteditor.tsx","./src/components/properties/families/classnodesection.tsx","./src/components/properties/families/erdiagramnodeproperties.tsx","./src/components/properties/families/entityfieldlisteditor.tsx","./src/components/properties/families/entitynodesection.tsx","./src/components/properties/families/journeynodeproperties.tsx","./src/components/properties/families/journeynodesection.tsx","./src/components/properties/families/mindmapnodeproperties.test.tsx","./src/components/properties/families/mindmapnodeproperties.tsx","./src/components/properties/families/sequencenodeproperties.tsx","./src/components/properties/families/sequencenodesection.tsx","./src/components/properties/families/specializednodecolorsections.test.tsx","./src/components/properties/families/structuredtextlisteditor.tsx","./src/components/properties/families/architectureoptions.ts","./src/components/studio-code-panel/usestudiocodepanelcontroller.test.tsx","./src/components/studio-code-panel/usestudiocodepanelcontroller.ts","./src/components/templates/templatepresentation.tsx","./src/components/toolbar/toolbaraddmenu.test.tsx","./src/components/toolbar/toolbaraddmenu.tsx","./src/components/toolbar/toolbaraddmenupanel.tsx","./src/components/toolbar/toolbarhistorycontrols.tsx","./src/components/toolbar/toolbarmodecontrols.tsx","./src/components/toolbar/toolbarbuttonstyles.ts","./src/components/top-nav/savestatusindicator.tsx","./src/components/top-nav/topnavactions.tsx","./src/components/top-nav/topnavbrand.tsx","./src/components/top-nav/topnavmenu.test.tsx","./src/components/top-nav/topnavmenu.tsx","./src/components/top-nav/topnavmenupanel.tsx","./src/components/top-nav/usetopnavstate.ts","./src/components/ui/button.tsx","./src/components/ui/collapsiblesection.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/searchfield.tsx","./src/components/ui/segmentedtabs.tsx","./src/components/ui/select.test.tsx","./src/components/ui/select.tsx","./src/components/ui/sidebaritem.tsx","./src/components/ui/slider.tsx","./src/components/ui/switch.tsx","./src/components/ui/textarea.tsx","./src/components/ui/toastcontext.tsx","./src/components/ui/editorfieldstyles.ts","./src/config/aiproviders.test.ts","./src/config/aiproviders.ts","./src/config/rolloutflags.ts","./src/context/architecturelintcontext.tsx","./src/context/cinematicexportcontext.tsx","./src/context/diagramdiffcontext.tsx","./src/context/themecontext.tsx","./src/diagram-types/bootstrap.test.ts","./src/diagram-types/bootstrap.ts","./src/diagram-types/builtinplugins.ts","./src/diagram-types/builtinpropertypanels.ts","./src/diagram-types/registerbuiltinplugins.test.ts","./src/diagram-types/registerbuiltinplugins.ts","./src/diagram-types/registerbuiltinpropertypanels.test.ts","./src/diagram-types/registerbuiltinpropertypanels.ts","./src/diagram-types/architecture/fuzzcorpus.test.ts","./src/diagram-types/architecture/plugin.test.ts","./src/diagram-types/architecture/plugin.ts","./src/diagram-types/classdiagram/fuzzcorpus.test.ts","./src/diagram-types/classdiagram/plugin.test.ts","./src/diagram-types/classdiagram/plugin.ts","./src/diagram-types/core/contracts.ts","./src/diagram-types/core/index.ts","./src/diagram-types/core/propertypanels.test.ts","./src/diagram-types/core/propertypanels.ts","./src/diagram-types/core/registry.ts","./src/diagram-types/erdiagram/fuzzcorpus.test.ts","./src/diagram-types/erdiagram/plugin.test.ts","./src/diagram-types/erdiagram/plugin.ts","./src/diagram-types/flowchart/plugin.ts","./src/diagram-types/journey/fuzzcorpus.test.ts","./src/diagram-types/journey/plugin.test.ts","./src/diagram-types/journey/plugin.ts","./src/diagram-types/mindmap/fuzzcorpus.test.ts","./src/diagram-types/mindmap/plugin.test.ts","./src/diagram-types/mindmap/plugin.ts","./src/diagram-types/sequence/plugin.test.ts","./src/diagram-types/sequence/plugin.ts","./src/diagram-types/statediagram/plugin.test.ts","./src/diagram-types/statediagram/plugin.ts","./src/docs/docsroutes.ts","./src/docs/publicdocscatalog.js","./src/hooks/edgeconnectinteractions.test.ts","./src/hooks/edgeconnectinteractions.ts","./src/hooks/flowexportviewport.test.ts","./src/hooks/flowexportviewport.ts","./src/hooks/mindmaptopicactionrequest.ts","./src/hooks/nodelabeleditrequest.test.ts","./src/hooks/nodelabeleditrequest.ts","./src/hooks/nodequickcreaterequest.test.ts","./src/hooks/nodequickcreaterequest.ts","./src/hooks/snapshotpolicy.test.ts","./src/hooks/snapshotpolicy.ts","./src/hooks/useaigeneration.ts","./src/hooks/useanalyticspreference.ts","./src/hooks/useanimatededgeperformancewarning.test.ts","./src/hooks/useanimatededgeperformancewarning.ts","./src/hooks/useassetcatalog.ts","./src/hooks/usecinematicexport.ts","./src/hooks/useclipboardoperations.ts","./src/hooks/usedesignsystem.ts","./src/hooks/useedgeinteractions.ts","./src/hooks/useedgeoperations.ts","./src/hooks/usefloweditoractions.test.ts","./src/hooks/usefloweditoractions.ts","./src/hooks/usefloweditorcallbacks.ts","./src/hooks/usefloweditorcollaboration.test.ts","./src/hooks/usefloweditorcollaboration.ts","./src/hooks/usefloweditoruistate.ts","./src/hooks/useflowexport.ts","./src/hooks/useflowhistory.test.ts","./src/hooks/useflowhistory.ts","./src/hooks/useflowoperations.ts","./src/hooks/usegithubstars.ts","./src/hooks/useinfrasync.ts","./src/hooks/useinlinenodetextedit.test.ts","./src/hooks/useinlinenodetextedit.ts","./src/hooks/usekeyboardshortcuts.test.ts","./src/hooks/usekeyboardshortcuts.ts","./src/hooks/uselayoutoperations.ts","./src/hooks/usemarkdowneditor.ts","./src/hooks/usemenukeyboardnavigation.ts","./src/hooks/usemodifierkeys.test.ts","./src/hooks/usemodifierkeys.ts","./src/hooks/usenodeoperations.ts","./src/hooks/useplayback.ts","./src/hooks/useprovidershapepreview.ts","./src/hooks/userecentimports.ts","./src/hooks/useshiftheld.ts","./src/hooks/usesnapshots.ts","./src/hooks/usestaticexport.ts","./src/hooks/usestoragepressureguard.ts","./src/hooks/usestructuredlisteditor.ts","./src/hooks/usestyleclipboard.test.ts","./src/hooks/usestyleclipboard.ts","./src/hooks/ai-generation/chathistorystorage.test.ts","./src/hooks/ai-generation/chathistorystorage.ts","./src/hooks/ai-generation/codetoarchitecture.test.ts","./src/hooks/ai-generation/codetoarchitecture.ts","./src/hooks/ai-generation/codebaseanalyzer.test.ts","./src/hooks/ai-generation/codebaseanalyzer.ts","./src/hooks/ai-generation/codebaseparser.test.ts","./src/hooks/ai-generation/codebaseparser.ts","./src/hooks/ai-generation/codebasetonativediagram.test.ts","./src/hooks/ai-generation/codebasetonativediagram.ts","./src/hooks/ai-generation/graphcomposer.test.ts","./src/hooks/ai-generation/graphcomposer.ts","./src/hooks/ai-generation/nodeactionprompts.test.ts","./src/hooks/ai-generation/nodeactionprompts.ts","./src/hooks/ai-generation/openapiparser.test.ts","./src/hooks/ai-generation/openapiparser.ts","./src/hooks/ai-generation/openapitosequence.ts","./src/hooks/ai-generation/positionpreservingapply.test.ts","./src/hooks/ai-generation/positionpreservingapply.ts","./src/hooks/ai-generation/readiness.test.ts","./src/hooks/ai-generation/readiness.ts","./src/hooks/ai-generation/requestlifecycle.test.ts","./src/hooks/ai-generation/requestlifecycle.ts","./src/hooks/ai-generation/sqlparser.test.ts","./src/hooks/ai-generation/sqlparser.ts","./src/hooks/ai-generation/sqltoerd.ts","./src/hooks/ai-generation/streamingparser.ts","./src/hooks/ai-generation/streamingstore.ts","./src/hooks/ai-generation/terraformtocloud.ts","./src/hooks/edge-operations/utils.test.ts","./src/hooks/edge-operations/utils.ts","./src/hooks/flow-editor-actions/exporthandlers.test.ts","./src/hooks/flow-editor-actions/exporthandlers.ts","./src/hooks/flow-editor-actions/helpers.ts","./src/hooks/flow-editor-actions/layouthandlers.test.ts","./src/hooks/flow-editor-actions/layouthandlers.ts","./src/hooks/flow-export/diagramdocumenttransfer.test.ts","./src/hooks/flow-export/diagramdocumenttransfer.ts","./src/hooks/flow-export/exportcapture.test.ts","./src/hooks/flow-export/exportcapture.ts","./src/hooks/flow-operations/useflowcoreactions.ts","./src/hooks/node-operations/createconnectedsibling.ts","./src/hooks/node-operations/dragstopreconcilepolicy.test.ts","./src/hooks/node-operations/dragstopreconcilepolicy.ts","./src/hooks/node-operations/nodefactories.ts","./src/hooks/node-operations/routingduringdrag.test.ts","./src/hooks/node-operations/routingduringdrag.ts","./src/hooks/node-operations/sectionbounds.ts","./src/hooks/node-operations/sectionhittesting.ts","./src/hooks/node-operations/sectionoperations.ts","./src/hooks/node-operations/usearchitecturenodeoperations.test.ts","./src/hooks/node-operations/usearchitecturenodeoperations.ts","./src/hooks/node-operations/usemindmapnodeoperations.test.ts","./src/hooks/node-operations/usemindmapnodeoperations.ts","./src/hooks/node-operations/usenodedragoperations.test.ts","./src/hooks/node-operations/usenodedragoperations.ts","./src/hooks/node-operations/usenodeoperationadders.ts","./src/hooks/node-operations/utils.test.ts","./src/hooks/node-operations/utils.ts","./src/i18n/config.test.ts","./src/i18n/config.ts","./src/i18n/strictmodelocalecoverage.test.ts","./src/i18n/usedlocalecoverage.test.ts","./src/lib/architecturetemplatedata.ts","./src/lib/architecturetemplates.test.ts","./src/lib/architecturetemplates.ts","./src/lib/brand.ts","./src/lib/classmembers.ts","./src/lib/colorutils.ts","./src/lib/connectcreationpolicy.test.ts","./src/lib/connectcreationpolicy.ts","./src/lib/date.ts","./src/lib/designtokens.ts","./src/lib/devperformance.ts","./src/lib/entityfields.ts","./src/lib/ertoclassconversion.test.ts","./src/lib/ertoclassconversion.ts","./src/lib/exportfilename.test.ts","./src/lib/exportfilename.ts","./src/lib/flowminddslparserv2.test.ts","./src/lib/flowminddslparserv2.ts","./src/lib/fuzzymatch.ts","./src/lib/genericshapepolicy.ts","./src/lib/id.ts","./src/lib/index.ts","./src/lib/legacybranding.ts","./src/lib/logger.ts","./src/lib/mermaidparser.ts","./src/lib/mermaidparserhelpers.ts","./src/lib/mermaidparsermodel.ts","./src/lib/mindmaplayout.test.ts","./src/lib/mindmaplayout.ts","./src/lib/mindmaplayoutengine.ts","./src/lib/mindmaptree.ts","./src/lib/nodebulkediting.test.ts","./src/lib/nodebulkediting.ts","./src/lib/nodehandles.ts","./src/lib/nodeparent.ts","./src/lib/nodestyledata.test.ts","./src/lib/nodestyledata.ts","./src/lib/openflowdslparser.ts","./src/lib/openflowdslparserv2.ts","./src/lib/reactflowcompat.ts","./src/lib/reconnectedge.test.ts","./src/lib/reconnectedge.ts","./src/lib/relationsemantics.test.ts","./src/lib/relationsemantics.ts","./src/lib/releasestaleelkroutes.test.ts","./src/lib/releasestaleelkroutes.ts","./src/lib/result.ts","./src/lib/storagepressure.test.ts","./src/lib/storagepressure.ts","./src/lib/types.ts","./src/lib/xsssafety.test.tsx","./src/services/aligndistribute.ts","./src/services/aiservice.test.ts","./src/services/aiservice.ts","./src/services/aiserviceschemas.ts","./src/services/animatedexport.test.ts","./src/services/animatedexport.ts","./src/services/architectureroundtrip.test.ts","./src/services/assetcatalog.test.ts","./src/services/assetcatalog.ts","./src/services/assetpresentation.ts","./src/services/canonicalserialization.test.ts","./src/services/canonicalserialization.ts","./src/services/composediagramfordisplay.test.ts","./src/services/composediagramfordisplay.ts","./src/services/diagramdocument.test.ts","./src/services/diagramdocument.ts","./src/services/diagramdocumentschemas.ts","./src/services/domainlibrary.test.ts","./src/services/domainlibrary.ts","./src/services/elklayout.test.ts","./src/services/elklayout.ts","./src/services/exportservice.test.ts","./src/services/exportservice.ts","./src/services/figmaexportservice.ts","./src/services/flowchartroundtrip.test.ts","./src/services/geminiservice.ts","./src/services/geminisysteminstruction.ts","./src/services/gifencoder.test.ts","./src/services/gifencoder.ts","./src/services/githubfetcher.test.ts","./src/services/githubfetcher.ts","./src/services/iconassetcatalog.ts","./src/services/importfidelity.test.ts","./src/services/importfidelity.ts","./src/services/mermaidparser.test.ts","./src/services/openflowdslexporter.test.ts","./src/services/openflowdslexporter.ts","./src/services/openflowdslparser.test.ts","./src/services/openflowroundtripgolden.test.ts","./src/services/openflowroundtripgoldenfixtures.ts","./src/services/operationfeedback.ts","./src/services/remainingfamiliesroundtrip.test.ts","./src/services/smartedgerouting.test.ts","./src/services/smartedgerouting.ts","./src/services/statediagramroundtrip.test.ts","./src/services/templates.selector.test.ts","./src/services/templates.ts","./src/services/zipextractor.ts","./src/services/ai/contextserializer.test.ts","./src/services/ai/contextserializer.ts","./src/services/analytics/analytics.ts","./src/services/analytics/analyticssettings.test.ts","./src/services/analytics/analyticssettings.ts","./src/services/analytics/surfaceanalyticsclient.ts","./src/services/architecturelint/defaultrules.ts","./src/services/architecturelint/ruleengine.test.ts","./src/services/architecturelint/ruleengine.ts","./src/services/architecturelint/rulelibrary.ts","./src/services/architecturelint/types.ts","./src/services/architecturelint/workspacerules.ts","./src/services/collaboration/bootstrap.test.ts","./src/services/collaboration/bootstrap.ts","./src/services/collaboration/canvasdiff.test.ts","./src/services/collaboration/canvasdiff.ts","./src/services/collaboration/comments.test.ts","./src/services/collaboration/comments.ts","./src/services/collaboration/contracts.test.ts","./src/services/collaboration/contracts.ts","./src/services/collaboration/hookutils.ts","./src/services/collaboration/operationlog.test.ts","./src/services/collaboration/operationlog.ts","./src/services/collaboration/presenceviewmodel.test.ts","./src/services/collaboration/presenceviewmodel.ts","./src/services/collaboration/reducer.test.ts","./src/services/collaboration/reducer.ts","./src/services/collaboration/roomconfig.ts","./src/services/collaboration/roomlink.test.ts","./src/services/collaboration/roomlink.ts","./src/services/collaboration/runtimecontroller.test.ts","./src/services/collaboration/runtimecontroller.ts","./src/services/collaboration/runtimehookutils.test.ts","./src/services/collaboration/runtimehookutils.ts","./src/services/collaboration/schemas.ts","./src/services/collaboration/session.test.ts","./src/services/collaboration/session.ts","./src/services/collaboration/storebridge.test.ts","./src/services/collaboration/storebridge.ts","./src/services/collaboration/transport.test.ts","./src/services/collaboration/transport.ts","./src/services/collaboration/transportfactory.test.ts","./src/services/collaboration/transportfactory.ts","./src/services/collaboration/types.ts","./src/services/collaboration/versioning.test.ts","./src/services/collaboration/versioning.ts","./src/services/collaboration/yjspeertransport.test.ts","./src/services/collaboration/yjspeertransport.ts","./src/services/diagramdiff/diffengine.ts","./src/services/elk-layout/boundaryfanout.ts","./src/services/elk-layout/determinism.ts","./src/services/elk-layout/options.ts","./src/services/elk-layout/types.ts","./src/services/export/cinematicbuildplan.test.ts","./src/services/export/cinematicbuildplan.ts","./src/services/export/cinematicexport.ts","./src/services/export/cinematicexporttheme.test.ts","./src/services/export/cinematicexporttheme.ts","./src/services/export/cinematicrenderstate.test.ts","./src/services/export/cinematicrenderstate.ts","./src/services/export/formatting.ts","./src/services/export/mermaidbuilder.test.ts","./src/services/export/mermaidbuilder.ts","./src/services/export/pdfdocument.test.ts","./src/services/export/pdfdocument.ts","./src/services/export/plantumlbuilder.ts","./src/services/export/mermaid/architecturemermaid.ts","./src/services/export/mermaid/classdiagrammermaid.ts","./src/services/export/mermaid/erdiagrammermaid.ts","./src/services/export/mermaid/journeymermaid.ts","./src/services/export/mermaid/mindmapmermaid.ts","./src/services/export/mermaid/sequencemermaid.ts","./src/services/export/mermaid/statediagrammermaid.ts","./src/services/figma/edgehelpers.ts","./src/services/figma/iconhelpers.ts","./src/services/figma/nodelayers.ts","./src/services/figma/themehelpers.ts","./src/services/figmaimport/figmaapiclient.ts","./src/services/flowpilot/assetgrounding.test.ts","./src/services/flowpilot/assetgrounding.ts","./src/services/flowpilot/prompting.ts","./src/services/flowpilot/responsepolicy.test.ts","./src/services/flowpilot/responsepolicy.ts","./src/services/flowpilot/skills.ts","./src/services/flowpilot/thread.ts","./src/services/flowpilot/types.ts","./src/services/infrasync/dockercomposeparser.test.ts","./src/services/infrasync/dockercomposeparser.ts","./src/services/infrasync/infratodsl.test.ts","./src/services/infrasync/infratodsl.ts","./src/services/infrasync/kubernetesparser.test.ts","./src/services/infrasync/kubernetesparser.ts","./src/services/infrasync/terraformstateparser.test.ts","./src/services/infrasync/terraformstateparser.ts","./src/services/infrasync/types.ts","./src/services/mermaid/detectdiagramtype.test.ts","./src/services/mermaid/detectdiagramtype.ts","./src/services/mermaid/diagnosticformatting.test.ts","./src/services/mermaid/diagnosticformatting.ts","./src/services/mermaid/parsemermaidbytype.test.ts","./src/services/mermaid/parsemermaidbytype.ts","./src/services/mermaid/strictmodediagnosticspresentation.test.ts","./src/services/mermaid/strictmodediagnosticspresentation.ts","./src/services/mermaid/strictmodeguidance.test.ts","./src/services/mermaid/strictmodeguidance.ts","./src/services/mermaid/strictmodeuxregression.test.ts","./src/services/offline/registerappshellserviceworker.test.ts","./src/services/offline/registerappshellserviceworker.ts","./src/services/onboarding/config.ts","./src/services/onboarding/eventschemas.ts","./src/services/onboarding/events.test.ts","./src/services/onboarding/events.ts","./src/services/playback/contracts.test.ts","./src/services/playback/contracts.ts","./src/services/playback/model.test.ts","./src/services/playback/model.ts","./src/services/playback/studio.test.ts","./src/services/playback/studio.ts","./src/services/sequence/sequencemessage.test.ts","./src/services/sequence/sequencemessage.ts","./src/services/shapelibrary/bootstrap.test.ts","./src/services/shapelibrary/bootstrap.ts","./src/services/shapelibrary/ingestionpipeline.test.ts","./src/services/shapelibrary/ingestionpipeline.ts","./src/services/shapelibrary/manifestvalidation.test.ts","./src/services/shapelibrary/manifestvalidation.ts","./src/services/shapelibrary/providercatalog.test.ts","./src/services/shapelibrary/providercatalog.ts","./src/services/shapelibrary/registry.test.ts","./src/services/shapelibrary/registry.ts","./src/services/shapelibrary/starterpacks.ts","./src/services/shapelibrary/types.ts","./src/services/storage/fallbackstorage.ts","./src/services/storage/flowdocumentmodel.test.ts","./src/services/storage/flowdocumentmodel.ts","./src/services/storage/flowpersiststorage.test.ts","./src/services/storage/flowpersiststorage.ts","./src/services/storage/indexeddbhelpers.ts","./src/services/storage/indexeddbschema.test.ts","./src/services/storage/indexeddbschema.ts","./src/services/storage/indexeddbstatestorage.test.ts","./src/services/storage/indexeddbstatestorage.ts","./src/services/storage/localfirstrepository.ts","./src/services/storage/localfirstruntime.ts","./src/services/storage/persisteddocumentadapters.test.ts","./src/services/storage/persisteddocumentadapters.ts","./src/services/storage/persistencetypes.ts","./src/services/storage/snapshotstorage.test.ts","./src/services/storage/snapshotstorage.ts","./src/services/storage/storageruntime.test.ts","./src/services/storage/storageruntime.ts","./src/services/storage/storageschemas.test.ts","./src/services/storage/storageschemas.ts","./src/services/storage/storagetelemetry.test.ts","./src/services/storage/storagetelemetry.ts","./src/services/storage/storagetelemetrysink.test.ts","./src/services/storage/storagetelemetrysink.ts","./src/services/storage/uilocalstorage.test.ts","./src/services/storage/uilocalstorage.ts","./src/services/templatelibrary/registry.test.ts","./src/services/templatelibrary/registry.ts","./src/services/templatelibrary/startertemplates.assets.test.ts","./src/services/templatelibrary/startertemplates.test.ts","./src/services/templatelibrary/startertemplates.ts","./src/services/templatelibrary/templatefactories.ts","./src/services/templatelibrary/types.ts","./src/store/actionfactory.ts","./src/store/aisettings.test.ts","./src/store/aisettings.ts","./src/store/aisettingspersistence.ts","./src/store/aisettingsschemas.test.ts","./src/store/aisettingsschemas.ts","./src/store/canvashooks.ts","./src/store/createflowstore.test.ts","./src/store/createflowstore.ts","./src/store/createflowstorepersistoptions.ts","./src/store/createflowstorestate.ts","./src/store/defaults.ts","./src/store/designsystemhooks.ts","./src/store/documenthooks.ts","./src/store/documentstatesync.test.ts","./src/store/documentstatesync.ts","./src/store/editorpagehooks.ts","./src/store/historyhooks.ts","./src/store/historystate.ts","./src/store/persistence.test.ts","./src/store/persistence.ts","./src/store/persistenceschemas.ts","./src/store/selectionhooks.ts","./src/store/selectors.test.ts","./src/store/selectors.ts","./src/store/tabhooks.ts","./src/store/types.ts","./src/store/viewhooks.ts","./src/store/workspacedocumentmodel.test.ts","./src/store/workspacedocumentmodel.ts","./src/store/actions/createaiandselectionactions.ts","./src/store/actions/createcanvasactions.ts","./src/store/actions/createdesignsystemactions.ts","./src/store/actions/createhistoryactions.test.ts","./src/store/actions/createhistoryactions.ts","./src/store/actions/createlayeractions.ts","./src/store/actions/createtabactions.ts","./src/store/actions/createviewactions.ts","./src/store/actions/createworkspacedocumentactions.ts","./src/store/actions/synctabnodesedges.ts","./src/store/slices/createcanvaseditorslice.ts","./src/store/slices/createexperienceslice.ts","./src/store/slices/createworkspaceslice.ts"],"version":"5.8.3"}
\ No newline at end of file
+{"root":["./src/app.test.tsx","./src/app.tsx","./src/constants.test.ts","./src/constants.ts","./src/index.tsx","./src/store.test.ts","./src/store.ts","./src/theme.test.ts","./src/theme.ts","./src/app/routestate.test.ts","./src/app/routestate.ts","./src/components/annotationnode.tsx","./src/components/architecturerulespanel.tsx","./src/components/cinematicexportoverlay.tsx","./src/components/commandbar.test.tsx","./src/components/commandbar.tsx","./src/components/connectmenu.test.tsx","./src/components/connectmenu.tsx","./src/components/connectmenusections.tsx","./src/components/contextmenu.test.tsx","./src/components/contextmenu.tsx","./src/components/customconnectionline.test.tsx","./src/components/customconnectionline.tsx","./src/components/customedge.tsx","./src/components/customnode.handleinteraction.test.tsx","./src/components/customnode.tsx","./src/components/customnodecontent.tsx","./src/components/designsystem.integration.test.tsx","./src/components/diagramviewer.tsx","./src/components/errorboundary.tsx","./src/components/exportmenu.tsx","./src/components/exportmenupanel.test.tsx","./src/components/exportmenupanel.tsx","./src/components/flowcanvas.tsx","./src/components/floweditor.tsx","./src/components/floweditoremptystate.tsx","./src/components/floweditorlayoutoverlay.tsx","./src/components/floweditorpanels.test.tsx","./src/components/floweditorpanels.tsx","./src/components/flowtabs.test.tsx","./src/components/flowtabs.tsx","./src/components/groupnode.tsx","./src/components/homepage.integration.test.tsx","./src/components/homepage.tsx","./src/components/iconassetnodebody.tsx","./src/components/iconmap.test.tsx","./src/components/iconmap.ts","./src/components/imagenode.tsx","./src/components/importrecoverydialog.test.tsx","./src/components/importrecoverydialog.tsx","./src/components/inlinetexteditsurface.test.tsx","./src/components/inlinetexteditsurface.tsx","./src/components/keyboardshortcutsmodal.tsx","./src/components/languageselector.tsx","./src/components/markdownrenderer.tsx","./src/components/memoizedmarkdown.tsx","./src/components/navigationcontrols.tsx","./src/components/nodebadges.tsx","./src/components/nodechrome.tsx","./src/components/nodequickcreatebuttons.tsx","./src/components/nodeshapesvg.tsx","./src/components/nodetransformcontrols.tsx","./src/components/playbackcontrols.tsx","./src/components/propertiespanel.tsx","./src/components/rightrail.tsx","./src/components/sectionnode.tsx","./src/components/shareembedmodal.tsx","./src/components/sharemodal.test.tsx","./src/components/sharemodal.tsx","./src/components/sidebarshell.tsx","./src/components/snapshotspanel.test.tsx","./src/components/snapshotspanel.tsx","./src/components/studioaipanel.test.tsx","./src/components/studioaipanel.tsx","./src/components/studioaipanelsections.tsx","./src/components/studiocodepanel.test.tsx","./src/components/studiocodepanel.tsx","./src/components/studiopanel.test.tsx","./src/components/studiopanel.tsx","./src/components/studioplaybackpanel.test.tsx","./src/components/studioplaybackpanel.tsx","./src/components/swimlanenode.tsx","./src/components/textnode.tsx","./src/components/themetoggle.tsx","./src/components/toolbar.tsx","./src/components/tooltip.tsx","./src/components/topnav.tsx","./src/components/welcomemodal.tsx","./src/components/annotationtheme.ts","./src/components/container-nodes.handleinteraction.test.tsx","./src/components/editorsurfacetiers.ts","./src/components/handleinteraction.test.ts","./src/components/handleinteraction.ts","./src/components/handleinteractionusage.test.ts","./src/components/lightweight-nodes.handleinteraction.test.tsx","./src/components/markdownsyntax.ts","./src/components/nodehelpers.test.ts","./src/components/nodehelpers.ts","./src/components/sharemodalcontent.ts","./src/components/studioaicopy.ts","./src/components/studioaipanelexamples.ts","./src/components/transformdiagnostics.test.ts","./src/components/transformdiagnostics.ts","./src/components/useactivenodeselection.ts","./src/components/useexportmenu.test.tsx","./src/components/useexportmenu.ts","./src/components/settingsmodal/aisettings.tsx","./src/components/settingsmodal/canvassettings.tsx","./src/components/settingsmodal/generalsettings.test.tsx","./src/components/settingsmodal/generalsettings.tsx","./src/components/settingsmodal/settingsmodal.test.tsx","./src/components/settingsmodal/settingsmodal.tsx","./src/components/settingsmodal/shortcutssettings.tsx","./src/components/settingsmodal/ai/advancedendpointsection.tsx","./src/components/settingsmodal/ai/customheaderseditor.tsx","./src/components/settingsmodal/ai/privacysection.tsx","./src/components/settingsmodal/ai/providericon.tsx","./src/components/settingsmodal/ai/providersection.tsx","./src/components/settingsmodal/ai/step.tsx","./src/components/add-items/additemregistry.tsx","./src/components/app/docssiteredirect.test.tsx","./src/components/app/docssiteredirect.tsx","./src/components/app/mobileworkspacegate.test.tsx","./src/components/app/mobileworkspacegate.tsx","./src/components/app/routeloadingfallback.test.tsx","./src/components/app/routeloadingfallback.tsx","./src/components/app/mobileworkspacegatecopy.ts","./src/components/app/routeloadingcopy.ts","./src/components/architecture-lint/lintrulespanel.tsx","./src/components/architecture-lint/ruleform.tsx","./src/components/architecture-lint/visualeditor.tsx","./src/components/command-bar/assetsview.test.tsx","./src/components/command-bar/assetsview.tsx","./src/components/command-bar/codebaseimportpanels.tsx","./src/components/command-bar/codebaseimportsection.tsx","./src/components/command-bar/designsystemeditor.tsx","./src/components/command-bar/designsystemview.tsx","./src/components/command-bar/diagramminipreview.tsx","./src/components/command-bar/figmaimportpanel.tsx","./src/components/command-bar/importsurfaceprimitives.tsx","./src/components/command-bar/importview.tsx","./src/components/command-bar/importviewpanels.tsx","./src/components/command-bar/layersview.tsx","./src/components/command-bar/layoutview.tsx","./src/components/command-bar/pagesview.tsx","./src/components/command-bar/rootview.test.tsx","./src/components/command-bar/rootview.tsx","./src/components/command-bar/searchview.tsx","./src/components/command-bar/templatesview.test.tsx","./src/components/command-bar/templatesview.tsx","./src/components/command-bar/viewheader.tsx","./src/components/command-bar/visualsview.tsx","./src/components/command-bar/applycodechanges.ts","./src/components/command-bar/assetsviewconstants.ts","./src/components/command-bar/importdetection.test.ts","./src/components/command-bar/importdetection.ts","./src/components/command-bar/importnativeparsers.ts","./src/components/command-bar/importviewmodel.test.ts","./src/components/command-bar/importviewmodel.ts","./src/components/command-bar/mermaidimportparser.ts","./src/components/command-bar/searchquery.test.ts","./src/components/command-bar/searchquery.ts","./src/components/command-bar/types.ts","./src/components/command-bar/useaiviewstate.test.tsx","./src/components/command-bar/useaiviewstate.ts","./src/components/command-bar/usecloudassetcatalog.ts","./src/components/command-bar/usecommandbarcommands.test.tsx","./src/components/command-bar/usecommandbarcommands.tsx","./src/components/custom-edge/customedgewrapper.tsx","./src/components/custom-edge/edgemarkerdefs.tsx","./src/components/custom-edge/sequencemessageedge.test.tsx","./src/components/custom-edge/sequencemessageedge.tsx","./src/components/custom-edge/animatededgepresentation.test.ts","./src/components/custom-edge/animatededgepresentation.ts","./src/components/custom-edge/classrelationsemantics.test.ts","./src/components/custom-edge/classrelationsemantics.ts","./src/components/custom-edge/edgerendermode.ts","./src/components/custom-edge/pathutils.test.ts","./src/components/custom-edge/pathutils.ts","./src/components/custom-edge/pathutilsgeometry.ts","./src/components/custom-edge/pathutilssiblingrouting.ts","./src/components/custom-edge/pathutilstypes.ts","./src/components/custom-edge/relationroutingsemantics.test.ts","./src/components/custom-edge/relationroutingsemantics.ts","./src/components/custom-edge/standardedgemarkers.test.ts","./src/components/custom-edge/standardedgemarkers.ts","./src/components/custom-nodes/architecturenode.handleinteraction.test.tsx","./src/components/custom-nodes/architecturenode.tsx","./src/components/custom-nodes/browsernode.tsx","./src/components/custom-nodes/classentitynode.handleinteraction.test.tsx","./src/components/custom-nodes/classnode.tsx","./src/components/custom-nodes/entitynode.tsx","./src/components/custom-nodes/journeynode.tsx","./src/components/custom-nodes/mindmapnode.tsx","./src/components/custom-nodes/mobilenode.tsx","./src/components/custom-nodes/sequencenotenode.tsx","./src/components/custom-nodes/sequenceparticipantnode.tsx","./src/components/custom-nodes/structurednodehandles.tsx","./src/components/custom-nodes/visualnodes.handleinteraction.test.tsx","./src/components/custom-nodes/browservariantrenderer.tsx","./src/components/custom-nodes/mobilevariantrenderer.tsx","./src/components/custom-nodes/structuredlistnavigation.test.ts","./src/components/custom-nodes/structuredlistnavigation.ts","./src/components/custom-nodes/variantconstants.ts","./src/components/diagram-diff/diffmodebanner.tsx","./src/components/flow-canvas/flowcanvasalignmentguidesoverlay.tsx","./src/components/flow-canvas/flowcanvasoverlays.tsx","./src/components/flow-canvas/flowcanvassurface.test.tsx","./src/components/flow-canvas/flowcanvassurface.tsx","./src/components/flow-canvas/streamingoverlay.tsx","./src/components/flow-canvas/alignmentguides.test.ts","./src/components/flow-canvas/alignmentguides.ts","./src/components/flow-canvas/flowcanvasreactflowcontracts.ts","./src/components/flow-canvas/flowcanvastypes.test.ts","./src/components/flow-canvas/flowcanvastypes.tsx","./src/components/flow-canvas/largegraphsafetymode.test.ts","./src/components/flow-canvas/largegraphsafetymode.ts","./src/components/flow-canvas/nodechromecoverage.test.ts","./src/components/flow-canvas/pastehelpers.test.ts","./src/components/flow-canvas/pastehelpers.ts","./src/components/flow-canvas/useflowcanvasalignmentguides.ts","./src/components/flow-canvas/useflowcanvasconnectionstate.ts","./src/components/flow-canvas/useflowcanvascontextactions.ts","./src/components/flow-canvas/useflowcanvasdragdrop.ts","./src/components/flow-canvas/useflowcanvasinteractionlod.ts","./src/components/flow-canvas/useflowcanvasmenus.ts","./src/components/flow-canvas/useflowcanvasmenusandactions.test.tsx","./src/components/flow-canvas/useflowcanvasmenusandactions.ts","./src/components/flow-canvas/useflowcanvasnodedraghandlers.ts","./src/components/flow-canvas/useflowcanvaspaste.ts","./src/components/flow-canvas/useflowcanvasreactflowconfig.test.ts","./src/components/flow-canvas/useflowcanvasreactflowconfig.ts","./src/components/flow-canvas/useflowcanvasselectiontools.test.tsx","./src/components/flow-canvas/useflowcanvasselectiontools.ts","./src/components/flow-canvas/useflowcanvasviewstate.test.ts","./src/components/flow-canvas/useflowcanvasviewstate.ts","./src/components/flow-canvas/useflowcanvaszoomlod.ts","./src/components/flow-editor/collaborationpresenceoverlay.test.tsx","./src/components/flow-editor/collaborationpresenceoverlay.tsx","./src/components/flow-editor/floweditorchrome.tsx","./src/components/flow-editor/buildfloweditorcontrollerparams.ts","./src/components/flow-editor/buildfloweditorscreencontrollerparams.ts","./src/components/flow-editor/chromeproptypes.ts","./src/components/flow-editor/floweditorchromeprops.ts","./src/components/flow-editor/infradslapply.test.ts","./src/components/flow-editor/infradslapply.ts","./src/components/flow-editor/panelprops.test.ts","./src/components/flow-editor/panelprops.ts","./src/components/flow-editor/shouldexitstudioonselection.test.ts","./src/components/flow-editor/shouldexitstudioonselection.ts","./src/components/flow-editor/usecollaborationnodepositions.ts","./src/components/flow-editor/usefloweditorcontroller.ts","./src/components/flow-editor/usefloweditorinteractionbindings.ts","./src/components/flow-editor/usefloweditorpanelactions.ts","./src/components/flow-editor/usefloweditorpanelprops.ts","./src/components/flow-editor/usefloweditorruntime.ts","./src/components/flow-editor/usefloweditorscreenbehavior.ts","./src/components/flow-editor/usefloweditorscreenmodel.ts","./src/components/flow-editor/usefloweditorscreenstate.ts","./src/components/flow-editor/usefloweditorshellcontroller.test.tsx","./src/components/flow-editor/usefloweditorshellcontroller.ts","./src/components/flow-editor/usefloweditorstudiocontroller.test.tsx","./src/components/flow-editor/usefloweditorstudiocontroller.ts","./src/components/flow-editor/useinfradslapply.ts","./src/components/home/githubcard.tsx","./src/components/home/homedashboard.tsx","./src/components/home/homeflowdialogs.tsx","./src/components/home/homesettingsview.tsx","./src/components/home/homesidebar.tsx","./src/components/home/hometemplatesview.tsx","./src/components/home/sidebarfooter.tsx","./src/components/home/welcomemodalstate.ts","./src/components/icons/assetsicon.tsx","./src/components/icons/openflowlogo.tsx","./src/components/infra-sync/infrasyncpanel.test.tsx","./src/components/infra-sync/infrasyncpanel.tsx","./src/components/journey/journeyscorecontrol.test.tsx","./src/components/journey/journeyscorecontrol.tsx","./src/components/properties/bulknodeproperties.test.tsx","./src/components/properties/bulknodeproperties.tsx","./src/components/properties/bulknodepropertiesfamilysections.tsx","./src/components/properties/bulknodepropertiessections.tsx","./src/components/properties/bulknodepropertiesutilitysections.tsx","./src/components/properties/colorpicker.test.tsx","./src/components/properties/colorpicker.tsx","./src/components/properties/customcolorpopover.tsx","./src/components/properties/diagramnodepropertiesrouter.test.ts","./src/components/properties/diagramnodepropertiesrouter.tsx","./src/components/properties/edgeproperties.tsx","./src/components/properties/iconpicker.tsx","./src/components/properties/icontilepickerprimitives.tsx","./src/components/properties/imageupload.tsx","./src/components/properties/inspectorprimitives.tsx","./src/components/properties/nodeactionbuttons.tsx","./src/components/properties/nodecontentsection.tsx","./src/components/properties/nodeimagesettingssection.tsx","./src/components/properties/nodeproperties.test.tsx","./src/components/properties/nodeproperties.tsx","./src/components/properties/nodewireframevariantsection.tsx","./src/components/properties/propertysliderrow.tsx","./src/components/properties/segmentedchoice.tsx","./src/components/properties/shapeselector.tsx","./src/components/properties/swatchpicker.tsx","./src/components/properties/bulknodepropertiesmodel.test.ts","./src/components/properties/bulknodepropertiesmodel.ts","./src/components/properties/colorpickerutils.ts","./src/components/properties/propertyinputbehavior.test.ts","./src/components/properties/propertyinputbehavior.ts","./src/components/properties/sectionactionbuilder.ts","./src/components/properties/shared.ts","./src/components/properties/wireframevariants.ts","./src/components/properties/edge/architectureedgesemanticssection.tsx","./src/components/properties/edge/edgecolorsection.test.tsx","./src/components/properties/edge/edgecolorsection.tsx","./src/components/properties/edge/edgeconditionsection.tsx","./src/components/properties/edge/edgelabelsection.tsx","./src/components/properties/edge/edgerelationsection.test.tsx","./src/components/properties/edge/edgerelationsection.tsx","./src/components/properties/edge/edgeroutesection.test.tsx","./src/components/properties/edge/edgeroutesection.tsx","./src/components/properties/edge/edgestylesection.tsx","./src/components/properties/edge/sequencemessagesection.tsx","./src/components/properties/edge/architecturesemantics.test.ts","./src/components/properties/edge/architecturesemantics.ts","./src/components/properties/edge/edgecolorutils.ts","./src/components/properties/edge/edgelabelmodel.test.ts","./src/components/properties/edge/edgelabelmodel.ts","./src/components/properties/edge/errelationoptions.ts","./src/components/properties/families/architecturenodeproperties.test.tsx","./src/components/properties/families/architecturenodeproperties.tsx","./src/components/properties/families/architecturenodesection.tsx","./src/components/properties/families/classdiagramnodeproperties.tsx","./src/components/properties/families/classmemberlisteditor.tsx","./src/components/properties/families/classnodesection.tsx","./src/components/properties/families/erdiagramnodeproperties.tsx","./src/components/properties/families/entityfieldlisteditor.tsx","./src/components/properties/families/entitynodesection.tsx","./src/components/properties/families/journeynodeproperties.tsx","./src/components/properties/families/journeynodesection.tsx","./src/components/properties/families/mindmapnodeproperties.test.tsx","./src/components/properties/families/mindmapnodeproperties.tsx","./src/components/properties/families/sequencenodeproperties.tsx","./src/components/properties/families/sequencenodesection.tsx","./src/components/properties/families/specializednodecolorsections.test.tsx","./src/components/properties/families/structuredtextlisteditor.tsx","./src/components/properties/families/architectureoptions.ts","./src/components/studio-code-panel/usestudiocodepanelcontroller.test.tsx","./src/components/studio-code-panel/usestudiocodepanelcontroller.ts","./src/components/templates/templatepresentation.tsx","./src/components/toolbar/toolbaraddmenu.test.tsx","./src/components/toolbar/toolbaraddmenu.tsx","./src/components/toolbar/toolbaraddmenupanel.tsx","./src/components/toolbar/toolbarhistorycontrols.tsx","./src/components/toolbar/toolbarmodecontrols.tsx","./src/components/toolbar/toolbarbuttonstyles.ts","./src/components/top-nav/savestatusindicator.tsx","./src/components/top-nav/topnavactions.tsx","./src/components/top-nav/topnavbrand.tsx","./src/components/top-nav/topnavmenu.test.tsx","./src/components/top-nav/topnavmenu.tsx","./src/components/top-nav/topnavmenupanel.tsx","./src/components/top-nav/usetopnavstate.ts","./src/components/ui/button.tsx","./src/components/ui/collapsiblesection.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/searchfield.tsx","./src/components/ui/segmentedtabs.tsx","./src/components/ui/select.test.tsx","./src/components/ui/select.tsx","./src/components/ui/sidebaritem.tsx","./src/components/ui/slider.tsx","./src/components/ui/switch.tsx","./src/components/ui/textarea.tsx","./src/components/ui/toastcontext.tsx","./src/components/ui/editorfieldstyles.ts","./src/config/aiproviders.test.ts","./src/config/aiproviders.ts","./src/config/rolloutflags.ts","./src/context/architecturelintcontext.tsx","./src/context/cinematicexportcontext.tsx","./src/context/diagramdiffcontext.tsx","./src/context/themecontext.tsx","./src/diagram-types/bootstrap.test.ts","./src/diagram-types/bootstrap.ts","./src/diagram-types/builtinplugins.ts","./src/diagram-types/builtinpropertypanels.ts","./src/diagram-types/registerbuiltinplugins.test.ts","./src/diagram-types/registerbuiltinplugins.ts","./src/diagram-types/registerbuiltinpropertypanels.test.ts","./src/diagram-types/registerbuiltinpropertypanels.ts","./src/diagram-types/architecture/fuzzcorpus.test.ts","./src/diagram-types/architecture/plugin.test.ts","./src/diagram-types/architecture/plugin.ts","./src/diagram-types/classdiagram/fuzzcorpus.test.ts","./src/diagram-types/classdiagram/plugin.test.ts","./src/diagram-types/classdiagram/plugin.ts","./src/diagram-types/core/contracts.ts","./src/diagram-types/core/index.ts","./src/diagram-types/core/propertypanels.test.ts","./src/diagram-types/core/propertypanels.ts","./src/diagram-types/core/registry.ts","./src/diagram-types/erdiagram/fuzzcorpus.test.ts","./src/diagram-types/erdiagram/plugin.test.ts","./src/diagram-types/erdiagram/plugin.ts","./src/diagram-types/flowchart/plugin.ts","./src/diagram-types/journey/fuzzcorpus.test.ts","./src/diagram-types/journey/plugin.test.ts","./src/diagram-types/journey/plugin.ts","./src/diagram-types/mindmap/fuzzcorpus.test.ts","./src/diagram-types/mindmap/plugin.test.ts","./src/diagram-types/mindmap/plugin.ts","./src/diagram-types/sequence/plugin.test.ts","./src/diagram-types/sequence/plugin.ts","./src/diagram-types/statediagram/plugin.test.ts","./src/diagram-types/statediagram/plugin.ts","./src/docs/docsroutes.ts","./src/docs/publicdocscatalog.js","./src/hooks/edgeconnectinteractions.test.ts","./src/hooks/edgeconnectinteractions.ts","./src/hooks/flowexportviewport.test.ts","./src/hooks/flowexportviewport.ts","./src/hooks/mindmaptopicactionrequest.ts","./src/hooks/nodelabeleditrequest.test.ts","./src/hooks/nodelabeleditrequest.ts","./src/hooks/nodequickcreaterequest.test.ts","./src/hooks/nodequickcreaterequest.ts","./src/hooks/snapshotpolicy.test.ts","./src/hooks/snapshotpolicy.ts","./src/hooks/useaigeneration.ts","./src/hooks/useanalyticspreference.ts","./src/hooks/useanimatededgeperformancewarning.test.ts","./src/hooks/useanimatededgeperformancewarning.ts","./src/hooks/useassetcatalog.ts","./src/hooks/usecinematicexport.ts","./src/hooks/useclipboardoperations.ts","./src/hooks/usedesignsystem.ts","./src/hooks/useedgeinteractions.ts","./src/hooks/useedgeoperations.ts","./src/hooks/usefloweditoractions.test.ts","./src/hooks/usefloweditoractions.ts","./src/hooks/usefloweditorcallbacks.ts","./src/hooks/usefloweditorcollaboration.test.ts","./src/hooks/usefloweditorcollaboration.ts","./src/hooks/usefloweditoruistate.ts","./src/hooks/useflowexport.ts","./src/hooks/useflowhistory.test.ts","./src/hooks/useflowhistory.ts","./src/hooks/useflowoperations.ts","./src/hooks/usegithubstars.ts","./src/hooks/useinfrasync.ts","./src/hooks/useinlinenodetextedit.test.ts","./src/hooks/useinlinenodetextedit.ts","./src/hooks/usekeyboardshortcuts.test.ts","./src/hooks/usekeyboardshortcuts.ts","./src/hooks/uselayoutoperations.ts","./src/hooks/usemarkdowneditor.ts","./src/hooks/usemenukeyboardnavigation.ts","./src/hooks/usemodifierkeys.test.ts","./src/hooks/usemodifierkeys.ts","./src/hooks/usenodeoperations.ts","./src/hooks/useplayback.ts","./src/hooks/useprovidershapepreview.ts","./src/hooks/userecentimports.ts","./src/hooks/useshiftheld.ts","./src/hooks/usesnapshots.ts","./src/hooks/usestaticexport.ts","./src/hooks/usestoragepressureguard.ts","./src/hooks/usestructuredlisteditor.ts","./src/hooks/usestyleclipboard.test.ts","./src/hooks/usestyleclipboard.ts","./src/hooks/ai-generation/chathistorystorage.test.ts","./src/hooks/ai-generation/chathistorystorage.ts","./src/hooks/ai-generation/codetoarchitecture.test.ts","./src/hooks/ai-generation/codetoarchitecture.ts","./src/hooks/ai-generation/codebaseanalyzer.test.ts","./src/hooks/ai-generation/codebaseanalyzer.ts","./src/hooks/ai-generation/codebaseparser.test.ts","./src/hooks/ai-generation/codebaseparser.ts","./src/hooks/ai-generation/codebasetonativediagram.test.ts","./src/hooks/ai-generation/codebasetonativediagram.ts","./src/hooks/ai-generation/graphcomposer.test.ts","./src/hooks/ai-generation/graphcomposer.ts","./src/hooks/ai-generation/nodeactionprompts.test.ts","./src/hooks/ai-generation/nodeactionprompts.ts","./src/hooks/ai-generation/openapiparser.test.ts","./src/hooks/ai-generation/openapiparser.ts","./src/hooks/ai-generation/openapitosequence.ts","./src/hooks/ai-generation/positionpreservingapply.test.ts","./src/hooks/ai-generation/positionpreservingapply.ts","./src/hooks/ai-generation/readiness.test.ts","./src/hooks/ai-generation/readiness.ts","./src/hooks/ai-generation/requestlifecycle.test.ts","./src/hooks/ai-generation/requestlifecycle.ts","./src/hooks/ai-generation/sqlparser.test.ts","./src/hooks/ai-generation/sqlparser.ts","./src/hooks/ai-generation/sqltoerd.ts","./src/hooks/ai-generation/streamingparser.ts","./src/hooks/ai-generation/streamingstore.ts","./src/hooks/ai-generation/terraformtocloud.ts","./src/hooks/edge-operations/utils.test.ts","./src/hooks/edge-operations/utils.ts","./src/hooks/flow-editor-actions/exporthandlers.test.ts","./src/hooks/flow-editor-actions/exporthandlers.ts","./src/hooks/flow-editor-actions/helpers.ts","./src/hooks/flow-editor-actions/layouthandlers.test.ts","./src/hooks/flow-editor-actions/layouthandlers.ts","./src/hooks/flow-export/diagramdocumenttransfer.test.ts","./src/hooks/flow-export/diagramdocumenttransfer.ts","./src/hooks/flow-export/exportcapture.test.ts","./src/hooks/flow-export/exportcapture.ts","./src/hooks/flow-operations/useflowcoreactions.ts","./src/hooks/node-operations/createconnectedsibling.ts","./src/hooks/node-operations/dragstopreconcilepolicy.test.ts","./src/hooks/node-operations/dragstopreconcilepolicy.ts","./src/hooks/node-operations/nodefactories.ts","./src/hooks/node-operations/routingduringdrag.test.ts","./src/hooks/node-operations/routingduringdrag.ts","./src/hooks/node-operations/sectionbounds.ts","./src/hooks/node-operations/sectionhittesting.ts","./src/hooks/node-operations/sectionoperations.ts","./src/hooks/node-operations/usearchitecturenodeoperations.test.ts","./src/hooks/node-operations/usearchitecturenodeoperations.ts","./src/hooks/node-operations/usemindmapnodeoperations.test.ts","./src/hooks/node-operations/usemindmapnodeoperations.ts","./src/hooks/node-operations/usenodedragoperations.test.ts","./src/hooks/node-operations/usenodedragoperations.ts","./src/hooks/node-operations/usenodeoperationadders.ts","./src/hooks/node-operations/utils.test.ts","./src/hooks/node-operations/utils.ts","./src/i18n/config.test.ts","./src/i18n/config.ts","./src/i18n/strictmodelocalecoverage.test.ts","./src/i18n/usedlocalecoverage.test.ts","./src/lib/aiiconspipeline.test.ts","./src/lib/architecturetemplatedata.ts","./src/lib/architecturetemplates.test.ts","./src/lib/architecturetemplates.ts","./src/lib/brand.ts","./src/lib/classmembers.ts","./src/lib/colorutils.ts","./src/lib/connectcreationpolicy.test.ts","./src/lib/connectcreationpolicy.ts","./src/lib/date.ts","./src/lib/designtokens.ts","./src/lib/devperformance.ts","./src/lib/entityfields.ts","./src/lib/ertoclassconversion.test.ts","./src/lib/ertoclassconversion.ts","./src/lib/exportfilename.test.ts","./src/lib/exportfilename.ts","./src/lib/flowminddslparserv2.test.ts","./src/lib/flowminddslparserv2.ts","./src/lib/fuzzymatch.ts","./src/lib/genericshapepolicy.ts","./src/lib/iconmatcher.test.ts","./src/lib/iconmatcher.ts","./src/lib/iconresolver.test.ts","./src/lib/iconresolver.ts","./src/lib/id.ts","./src/lib/index.ts","./src/lib/legacybranding.ts","./src/lib/logger.ts","./src/lib/mermaidenrichmentpipeline.test.ts","./src/lib/mermaidparser.ts","./src/lib/mermaidparserhelpers.ts","./src/lib/mermaidparsermodel.ts","./src/lib/mindmaplayout.test.ts","./src/lib/mindmaplayout.ts","./src/lib/mindmaplayoutengine.ts","./src/lib/mindmaptree.ts","./src/lib/nodebulkediting.test.ts","./src/lib/nodebulkediting.ts","./src/lib/nodeenricher.test.ts","./src/lib/nodeenricher.ts","./src/lib/nodehandles.ts","./src/lib/nodeparent.ts","./src/lib/nodestyledata.test.ts","./src/lib/nodestyledata.ts","./src/lib/openflowdslparser.ts","./src/lib/openflowdslparserv2.ts","./src/lib/reactflowcompat.ts","./src/lib/reconnectedge.test.ts","./src/lib/reconnectedge.ts","./src/lib/relationsemantics.test.ts","./src/lib/relationsemantics.ts","./src/lib/releasestaleelkroutes.test.ts","./src/lib/releasestaleelkroutes.ts","./src/lib/result.ts","./src/lib/semanticclassifier.test.ts","./src/lib/semanticclassifier.ts","./src/lib/storagepressure.test.ts","./src/lib/storagepressure.ts","./src/lib/types.ts","./src/lib/xsssafety.test.tsx","./src/services/aligndistribute.ts","./src/services/aiservice.test.ts","./src/services/aiservice.ts","./src/services/aiserviceschemas.ts","./src/services/animatedexport.test.ts","./src/services/animatedexport.ts","./src/services/architectureroundtrip.test.ts","./src/services/assetcatalog.test.ts","./src/services/assetcatalog.ts","./src/services/assetpresentation.ts","./src/services/canonicalserialization.test.ts","./src/services/canonicalserialization.ts","./src/services/composediagramfordisplay.test.ts","./src/services/composediagramfordisplay.ts","./src/services/diagramdocument.test.ts","./src/services/diagramdocument.ts","./src/services/diagramdocumentschemas.ts","./src/services/domainlibrary.test.ts","./src/services/domainlibrary.ts","./src/services/elklayout.test.ts","./src/services/elklayout.ts","./src/services/exportservice.test.ts","./src/services/exportservice.ts","./src/services/figmaexportservice.ts","./src/services/flowchartroundtrip.test.ts","./src/services/geminiservice.ts","./src/services/geminisysteminstruction.ts","./src/services/gifencoder.test.ts","./src/services/gifencoder.ts","./src/services/githubfetcher.test.ts","./src/services/githubfetcher.ts","./src/services/iconassetcatalog.ts","./src/services/importfidelity.test.ts","./src/services/importfidelity.ts","./src/services/mermaidparser.test.ts","./src/services/openflowdslexporter.test.ts","./src/services/openflowdslexporter.ts","./src/services/openflowdslparser.test.ts","./src/services/openflowroundtripgolden.test.ts","./src/services/openflowroundtripgoldenfixtures.ts","./src/services/operationfeedback.ts","./src/services/remainingfamiliesroundtrip.test.ts","./src/services/smartedgerouting.test.ts","./src/services/smartedgerouting.ts","./src/services/statediagramroundtrip.test.ts","./src/services/templates.selector.test.ts","./src/services/templates.ts","./src/services/zipextractor.ts","./src/services/ai/contextserializer.test.ts","./src/services/ai/contextserializer.ts","./src/services/analytics/analytics.ts","./src/services/analytics/analyticssettings.test.ts","./src/services/analytics/analyticssettings.ts","./src/services/analytics/surfaceanalyticsclient.ts","./src/services/architecturelint/defaultrules.ts","./src/services/architecturelint/ruleengine.test.ts","./src/services/architecturelint/ruleengine.ts","./src/services/architecturelint/rulelibrary.ts","./src/services/architecturelint/types.ts","./src/services/architecturelint/workspacerules.ts","./src/services/collaboration/bootstrap.test.ts","./src/services/collaboration/bootstrap.ts","./src/services/collaboration/canvasdiff.test.ts","./src/services/collaboration/canvasdiff.ts","./src/services/collaboration/comments.test.ts","./src/services/collaboration/comments.ts","./src/services/collaboration/contracts.test.ts","./src/services/collaboration/contracts.ts","./src/services/collaboration/hookutils.ts","./src/services/collaboration/operationlog.test.ts","./src/services/collaboration/operationlog.ts","./src/services/collaboration/presenceviewmodel.test.ts","./src/services/collaboration/presenceviewmodel.ts","./src/services/collaboration/reducer.test.ts","./src/services/collaboration/reducer.ts","./src/services/collaboration/roomconfig.ts","./src/services/collaboration/roomlink.test.ts","./src/services/collaboration/roomlink.ts","./src/services/collaboration/runtimecontroller.test.ts","./src/services/collaboration/runtimecontroller.ts","./src/services/collaboration/runtimehookutils.test.ts","./src/services/collaboration/runtimehookutils.ts","./src/services/collaboration/schemas.ts","./src/services/collaboration/session.test.ts","./src/services/collaboration/session.ts","./src/services/collaboration/storebridge.test.ts","./src/services/collaboration/storebridge.ts","./src/services/collaboration/transport.test.ts","./src/services/collaboration/transport.ts","./src/services/collaboration/transportfactory.test.ts","./src/services/collaboration/transportfactory.ts","./src/services/collaboration/types.ts","./src/services/collaboration/versioning.test.ts","./src/services/collaboration/versioning.ts","./src/services/collaboration/yjspeertransport.test.ts","./src/services/collaboration/yjspeertransport.ts","./src/services/diagramdiff/diffengine.ts","./src/services/elk-layout/boundaryfanout.ts","./src/services/elk-layout/determinism.ts","./src/services/elk-layout/options.ts","./src/services/elk-layout/types.ts","./src/services/export/cinematicbuildplan.test.ts","./src/services/export/cinematicbuildplan.ts","./src/services/export/cinematicexport.ts","./src/services/export/cinematicexporttheme.test.ts","./src/services/export/cinematicexporttheme.ts","./src/services/export/cinematicrenderstate.test.ts","./src/services/export/cinematicrenderstate.ts","./src/services/export/formatting.ts","./src/services/export/mermaidbuilder.test.ts","./src/services/export/mermaidbuilder.ts","./src/services/export/mermaidexportquality.test.ts","./src/services/export/pdfdocument.test.ts","./src/services/export/pdfdocument.ts","./src/services/export/plantumlbuilder.ts","./src/services/export/mermaid/architecturemermaid.ts","./src/services/export/mermaid/classdiagrammermaid.ts","./src/services/export/mermaid/erdiagrammermaid.ts","./src/services/export/mermaid/journeymermaid.ts","./src/services/export/mermaid/mindmapmermaid.ts","./src/services/export/mermaid/sequencemermaid.ts","./src/services/export/mermaid/statediagrammermaid.ts","./src/services/figma/edgehelpers.ts","./src/services/figma/iconhelpers.ts","./src/services/figma/nodelayers.ts","./src/services/figma/themehelpers.ts","./src/services/figmaimport/figmaapiclient.ts","./src/services/flowpilot/assetgrounding.test.ts","./src/services/flowpilot/assetgrounding.ts","./src/services/flowpilot/prompting.ts","./src/services/flowpilot/responsepolicy.test.ts","./src/services/flowpilot/responsepolicy.ts","./src/services/flowpilot/skills.ts","./src/services/flowpilot/thread.ts","./src/services/flowpilot/types.ts","./src/services/infrasync/dockercomposeparser.test.ts","./src/services/infrasync/dockercomposeparser.ts","./src/services/infrasync/infratodsl.test.ts","./src/services/infrasync/infratodsl.ts","./src/services/infrasync/kubernetesparser.test.ts","./src/services/infrasync/kubernetesparser.ts","./src/services/infrasync/terraformstateparser.test.ts","./src/services/infrasync/terraformstateparser.ts","./src/services/infrasync/types.ts","./src/services/mermaid/detectdiagramtype.test.ts","./src/services/mermaid/detectdiagramtype.ts","./src/services/mermaid/diagnosticformatting.test.ts","./src/services/mermaid/diagnosticformatting.ts","./src/services/mermaid/parsemermaidbytype.test.ts","./src/services/mermaid/parsemermaidbytype.ts","./src/services/mermaid/strictmodediagnosticspresentation.test.ts","./src/services/mermaid/strictmodediagnosticspresentation.ts","./src/services/mermaid/strictmodeguidance.test.ts","./src/services/mermaid/strictmodeguidance.ts","./src/services/mermaid/strictmodeuxregression.test.ts","./src/services/offline/registerappshellserviceworker.test.ts","./src/services/offline/registerappshellserviceworker.ts","./src/services/onboarding/config.ts","./src/services/onboarding/eventschemas.ts","./src/services/onboarding/events.test.ts","./src/services/onboarding/events.ts","./src/services/playback/contracts.test.ts","./src/services/playback/contracts.ts","./src/services/playback/model.test.ts","./src/services/playback/model.ts","./src/services/playback/studio.test.ts","./src/services/playback/studio.ts","./src/services/sequence/sequencemessage.test.ts","./src/services/sequence/sequencemessage.ts","./src/services/shapelibrary/bootstrap.test.ts","./src/services/shapelibrary/bootstrap.ts","./src/services/shapelibrary/ingestionpipeline.test.ts","./src/services/shapelibrary/ingestionpipeline.ts","./src/services/shapelibrary/manifestvalidation.test.ts","./src/services/shapelibrary/manifestvalidation.ts","./src/services/shapelibrary/providercatalog.test.ts","./src/services/shapelibrary/providercatalog.ts","./src/services/shapelibrary/registry.test.ts","./src/services/shapelibrary/registry.ts","./src/services/shapelibrary/starterpacks.ts","./src/services/shapelibrary/types.ts","./src/services/storage/fallbackstorage.ts","./src/services/storage/flowdocumentmodel.test.ts","./src/services/storage/flowdocumentmodel.ts","./src/services/storage/flowpersiststorage.test.ts","./src/services/storage/flowpersiststorage.ts","./src/services/storage/indexeddbhelpers.ts","./src/services/storage/indexeddbschema.test.ts","./src/services/storage/indexeddbschema.ts","./src/services/storage/indexeddbstatestorage.test.ts","./src/services/storage/indexeddbstatestorage.ts","./src/services/storage/localfirstrepository.ts","./src/services/storage/localfirstruntime.ts","./src/services/storage/persisteddocumentadapters.test.ts","./src/services/storage/persisteddocumentadapters.ts","./src/services/storage/persistencetypes.ts","./src/services/storage/snapshotstorage.test.ts","./src/services/storage/snapshotstorage.ts","./src/services/storage/storageruntime.test.ts","./src/services/storage/storageruntime.ts","./src/services/storage/storageschemas.test.ts","./src/services/storage/storageschemas.ts","./src/services/storage/storagetelemetry.test.ts","./src/services/storage/storagetelemetry.ts","./src/services/storage/storagetelemetrysink.test.ts","./src/services/storage/storagetelemetrysink.ts","./src/services/storage/uilocalstorage.test.ts","./src/services/storage/uilocalstorage.ts","./src/services/templatelibrary/registry.test.ts","./src/services/templatelibrary/registry.ts","./src/services/templatelibrary/startertemplates.assets.test.ts","./src/services/templatelibrary/startertemplates.test.ts","./src/services/templatelibrary/startertemplates.ts","./src/services/templatelibrary/templatefactories.ts","./src/services/templatelibrary/types.ts","./src/store/actionfactory.ts","./src/store/aisettings.test.ts","./src/store/aisettings.ts","./src/store/aisettingspersistence.ts","./src/store/aisettingsschemas.test.ts","./src/store/aisettingsschemas.ts","./src/store/canvashooks.ts","./src/store/createflowstore.test.ts","./src/store/createflowstore.ts","./src/store/createflowstorepersistoptions.ts","./src/store/createflowstorestate.ts","./src/store/defaults.ts","./src/store/designsystemhooks.ts","./src/store/documenthooks.ts","./src/store/documentstatesync.test.ts","./src/store/documentstatesync.ts","./src/store/editorpagehooks.ts","./src/store/historyhooks.ts","./src/store/historystate.ts","./src/store/persistence.test.ts","./src/store/persistence.ts","./src/store/persistenceschemas.ts","./src/store/selectionhooks.ts","./src/store/selectors.test.ts","./src/store/selectors.ts","./src/store/tabhooks.ts","./src/store/types.ts","./src/store/viewhooks.ts","./src/store/workspacedocumentmodel.test.ts","./src/store/workspacedocumentmodel.ts","./src/store/actions/createaiandselectionactions.ts","./src/store/actions/createcanvasactions.ts","./src/store/actions/createdesignsystemactions.ts","./src/store/actions/createhistoryactions.test.ts","./src/store/actions/createhistoryactions.ts","./src/store/actions/createlayeractions.ts","./src/store/actions/createtabactions.ts","./src/store/actions/createviewactions.ts","./src/store/actions/createworkspacedocumentactions.ts","./src/store/actions/synctabnodesedges.ts","./src/store/slices/createcanvaseditorslice.ts","./src/store/slices/createexperienceslice.ts","./src/store/slices/createworkspaceslice.ts"],"version":"5.8.3"}
\ No newline at end of file