import { writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { Graphviz } from '@hpcc-js/wasm-graphviz'; const __dirname = dirname(fileURLToPath(import.meta.url)); const outputPath = join(__dirname, 'automat-graph.svg'); const dotOutputPath = join(__dirname, 'automat-graph.dot'); const COLORS = { background: '#1e2430', tableBorder: '#3f4c5f', headerBg: '#33415c', headerText: '#e2e8f0', rowBg: '#2a3242', rowBgAlt: '#272e3d', keyBg: '#2b3a52', text: '#d8dee9', muted: '#8b95a7', clusterFill: '#202a3a', }; const BADGE_COLORS = { STATE: { fg: '#c084fc', bg: '#2b1a42' }, // Purple ACTION: { fg: '#4ade80', bg: '#143826' }, // Green SELECT: { fg: '#38bdf8', bg: '#182b40' }, // Sky Blue API: { fg: '#fbbf24', bg: '#3b2c12' }, // Amber SSE: { fg: '#f87171', bg: '#3a181e' }, // Rose/Red HOOK: { fg: '#fb923c', bg: '#382414' }, // Orange/Gold }; const EDGE_COLORS = [ '#5aa7ff', // Blue '#6bdf9a', // Mint Green '#f5c15c', // Golden Amber '#ff7fc2', // Pink '#c79bff', // Lavender '#5cd6d6', // Cyan '#ff9f5a', // Coral '#8f95ff', // Indigo '#ff6b6b', // Soft Red '#58d18a', // Emerald '#4fc4e8', // Sky '#a98cff', // Violet '#ff8f66', // Tangerine '#4ecb8f', // Seafoam '#ff7aa8', // Rose '#e0b25a', // Sand '#56c8c8', // Aqua '#ff5f9e', // Fuchsia '#ffb25a', // Ochre '#66b3ff', // Periwinkle ]; const CLUSTERS = [ { id: 'session_realtime', label: 'Session & Real-Time Hub', automats: ['authAutomat', 'sseAutomat'], }, { id: 'catalog_products', label: 'Catalog & Products', automats: ['categoryAutomat', 'categoryViewAutomat', 'artikelAutomat', 'articleDetailAutomat', 'brandAutomat', 'unitAutomat'], }, { id: 'search_indexing', label: 'Search & Indexing', automats: ['articleIndexAutomat', 'articleSearchAutomat'], }, { id: 'properties_attributes', label: 'Properties & Attributes', automats: ['propertyAutomat', 'attributeAutomat'], }, { id: 'purchasing_logistics', label: 'Suppliers & Deliveries', automats: ['supplierAutomat', 'deliveryAutomat', 'deliveryEditorAutomat'], }, { id: 'sales_crm', label: 'Sales & CRM', automats: ['quoteAutomat', 'orderAutomat', 'entityAutomat', 'customergroupAutomat'], }, { id: 'inventory_storage', label: 'Inventory & Warehousing', automats: ['warehouseAutomat'], }, { id: 'finance_config', label: 'Company & Finance', automats: ['companyAutomat', 'accountAutomat', 'numberAutomat'], }, { id: 'hardware_pos', label: 'POS & Hardware', automats: ['posterminalAutomat'], }, { id: 'ai_workspace', label: 'AI Assistant & Workspace', automats: ['chatAutomat', 'vfsAutomat'], }, { id: 'audit_grids', label: 'Audit & Virtual Grids', automats: ['articleHistoryGridAutomat', 'globalHistoryGridAutomat'], }, { id: 'system_ui', label: 'UI, Settings & System', automats: ['navigationAutomat', 'userSettingsAutomat', 'helpAutomat', 'pwaAutomat', 'Legend'], }, ]; function escapeHtml(text) { return String(text) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function truncate(str, max = 30) { if (!str) return ''; return str.length > max ? `${str.slice(0, max - 1)}…` : str; } /** * Complete metadata for every Automat in Dashfront. */ const AUTOMAT_SPECS = [ // 1. Session & Real-Time Hub { id: 'authAutomat', title: 'authAutomat', subtitle: 'Session & Multi-Tenant Gateway', entries: [ { type: 'STATE', name: 'user: AuthUser', desc: 'Current user & company' }, { type: 'STATE', name: 'status', desc: '\'anon\' | \'authed\' | \'loading\'' }, { type: 'ACTION', name: 'bootstrapSession()', desc: 'Restore or fetch session' }, { type: 'ACTION', name: 'switchCompany(id)', desc: 'Multi-tenant switch', port: 'p_auth_switchCompany' }, { type: 'ACTION', name: 'logout()', desc: 'Clear session cookie' }, { type: 'SELECT', name: 'selectSelectedCompanyId()', desc: 'Tenant selector' }, { type: 'HOOK', name: 'auth/sessionEstablished', desc: 'Session ready lifecycle', port: 'p_auth_status' }, { type: 'HOOK', name: 'auth/companyChanged', desc: 'Company change signal', port: 'p_auth_user' }, { type: 'API', name: 'GET /api/auth/session', desc: 'Cookie session check' }, ], }, { id: 'sseAutomat', title: 'sseAutomat', subtitle: 'Single Persistent SSE Connection', entries: [ { type: 'STATE', name: 'status', desc: '\'connected\' | \'error\'' }, { type: 'STATE', name: 'clientId (cid)', desc: 'Tab-scoped UUID in session' }, { type: 'ACTION', name: 'reconnect()', desc: 'Cycle on company switch' }, { type: 'ACTION', name: 'on(event, handler)', desc: 'Register signal listener' }, { type: 'ACTION', name: 'watchArticle(sku)', desc: 'Attach article-' }, { type: 'SELECT', name: 'invalidateArticle(sku)', desc: 'Silent cache refresh' }, { type: 'SSE', name: 'category / articles', desc: 'Catalog mutation events', port: 'p_sse_events' }, { type: 'SSE', name: 'order / shipment', desc: 'Fulfillment events' }, { type: 'API', name: 'GET /api/events', desc: 'Persistent stream' }, ], }, // 2. Catalog & Products { id: 'categoryAutomat', title: 'categoryAutomat', subtitle: 'Hierarchical Category Tree', entries: [ { type: 'STATE', name: 'categories: Map', desc: 'ID -> Category Node Map', port: 'p_cat_categories' }, { type: 'STATE', name: 'rootCategoryId', desc: 'Company root node ID' }, { type: 'STATE', name: 'articleCount', desc: 'Total parent items' }, { type: 'ACTION', name: 'loadTree()', desc: 'Fetch tree & warm views', port: 'p_cat_loadTree' }, { type: 'ACTION', name: 'createCategory()', desc: 'Insert child node' }, { type: 'ACTION', name: 'moveCategory()', desc: 'Parent reparenting' }, { type: 'ACTION', name: 'deleteCategory()', desc: 'Prune subtree' }, { type: 'SELECT', name: 'getCategoryBreadcrumbs()', desc: 'Hierarchical path trace' }, { type: 'API', name: 'GET /api/category', desc: 'Recursive tree load' }, ], }, { id: 'categoryViewAutomat', title: 'categoryViewAutomat', subtitle: 'KeyedAutomat (Slug Path)', entries: [ { type: 'STATE', name: 'views: Map', desc: 'Articles & direct children' }, { type: 'ACTION', name: 'loadAllCategoryViews()', desc: 'Prewarm all tree slugs' }, { type: 'ACTION', name: 'reloadLoadedCategoryViews()', desc: 'Refresh cached views' }, { type: 'ACTION', name: 'invalidateAllCategoryViews()', desc: 'Clear on SSE signal' }, { type: 'SELECT', name: 'collectLoadedCategoryViewData()', desc: 'Extract union data', port: 'p_catview_collect' }, { type: 'SELECT', name: 'getOrCreateCategoryAutomat()', desc: 'Per-slug reactive store' }, { type: 'API', name: 'GET /api/category/:id/articles', desc: 'Category article batch' }, ], }, { id: 'artikelAutomat', title: 'artikelAutomat', subtitle: 'In-Memory Article Record Cache', entries: [ { type: 'STATE', name: 'articles: Map', desc: 'Deduplicated SKU cache', port: 'p_art_set' }, { type: 'ACTION', name: 'loadArticle(sku)', desc: 'Fetch or cached promise' }, { type: 'ACTION', name: 'reloadArticle(sku)', desc: 'Silent background sync' }, { type: 'ACTION', name: 'setArticleName(sku, name)', desc: 'Optimistic rename', port: 'p_art_setName' }, { type: 'ACTION', name: 'clearArticle(sku)', desc: 'Evict SKU from cache' }, { type: 'SELECT', name: 'getArticle(sku)', desc: 'Sync constructor read' }, { type: 'SELECT', name: 'createArticleSelector(sku)', desc: 'Fine-grained selector' }, { type: 'API', name: 'GET /api/article/:sku', desc: 'Full article document' }, ], }, { id: 'articleDetailAutomat', title: 'articleDetailAutomat', subtitle: 'KeyedAutomat (SKU)', entries: [ { type: 'STATE', name: 'articleHistory: Map', desc: 'Change history log', port: 'p_artdet_hist' }, { type: 'STATE', name: 'articleSupplier: Map', desc: 'Assigned supplier list' }, { type: 'ACTION', name: 'articleHistoryAutomat.load()', desc: 'Fetch SKU audit trail' }, { type: 'ACTION', name: 'articleSupplierAutomat.load()', desc: 'Fetch supplier catalog' }, { type: 'API', name: 'GET /api/article/:sku/history', desc: 'Audit change entries' }, { type: 'API', name: 'GET /api/article/:sku/supplier', desc: 'Vendor links & prices' }, ], }, { id: 'brandAutomat', title: 'brandAutomat', subtitle: 'Catalog Brands & Makers', entries: [ { type: 'STATE', name: 'brands: Brand[]', desc: 'Decoupled brand catalog' }, { type: 'STATE', name: 'filteredBrands: Brand[]', desc: 'Filtered search slice' }, { type: 'ACTION', name: 'load(silent)', desc: 'Deduplicated list fetch' }, { type: 'ACTION', name: 'create() / rename()', desc: 'Brand mutations' }, { type: 'ACTION', name: 'addImage() / deleteImage()', desc: 'Brand logo AVIF image' }, { type: 'API', name: 'GET /api/brand', desc: 'Paginated brand records' }, { type: 'SSE', name: 'brand', desc: 'SSE signal with sender tab' }, ], }, { id: 'unitAutomat', title: 'unitAutomat', subtitle: 'Units of Measure (UoM)', entries: [ { type: 'STATE', name: 'units: Unit[]', desc: 'Allowed SI & sales units' }, { type: 'STATE', name: 'status: \'idle\'|\'ready\'', desc: 'Load lifecycle state' }, { type: 'ACTION', name: 'load()', desc: 'Fetch localized units' }, { type: 'ACTION', name: 'upsert(body)', desc: 'Create or update unit' }, { type: 'ACTION', name: 'delete(id)', desc: 'Remove unused unit' }, { type: 'API', name: 'GET /api/unit', desc: 'Company unit directory' }, ], }, // 3. Search & Discovery { id: 'articleIndexAutomat', title: 'articleIndexAutomat', subtitle: 'Derived Client Search Index', entries: [ { type: 'STATE', name: 'items: ArticleIndexRecord[]', desc: 'De-duplicated article union', port: 'p_idx_items' }, { type: 'STATE', name: 'status: \'ready\'', desc: 'Index lifecycle flag' }, { type: 'ACTION', name: 'buildArticleIndex()', desc: 'Derive from category views' }, { type: 'ACTION', name: 'setArticleIndexName()', desc: 'Patch on rename' }, { type: 'ACTION', name: 'reload()', desc: 'Re-derive from views' }, { type: 'SELECT', name: 'selectArticleIndexItems()', desc: 'Fast record projection' }, { type: 'SELECT', name: 'ensureLoaded()', desc: 'Await warm views' }, ], }, { id: 'articleSearchAutomat', title: 'articleSearchAutomat', subtitle: 'In-Memory FlexSearch Engine', entries: [ { type: 'STATE', name: 'target: string', desc: 'Active search query' }, { type: 'STATE', name: 'filter: string', desc: 'Active category filter' }, { type: 'STATE', name: 'result: ArticleSearchResult', desc: 'Facets, recordset, count' }, { type: 'ACTION', name: 'search(target, filter)', desc: 'Synchronous indexed query' }, { type: 'SELECT', name: 'selectSearchResult()', desc: 'Reactive search outcome' }, ], }, // 4. Properties & Attributes { id: 'propertyAutomat', title: 'propertyAutomat', subtitle: 'Custom Metadata Properties', entries: [ { type: 'STATE', name: 'properties: PropertyName[]', desc: 'Sorted company properties' }, { type: 'ACTION', name: 'load()', desc: 'Fetch property definitions' }, { type: 'ACTION', name: 'createProperty()', desc: 'Add new property definition' }, { type: 'ACTION', name: 'updateProperty()', desc: 'Edit title & sort' }, { type: 'API', name: 'GET /api/propertyname', desc: 'Company properties' }, { type: 'SSE', name: 'propertyname', desc: 'Real-time property signal' }, ], }, { id: 'attributeAutomat', title: 'attributeAutomat', subtitle: 'Product Attributes & Discrete Values', entries: [ { type: 'STATE', name: 'attributes: AttributeName[]', desc: 'Attributes with sorted values' }, { type: 'ACTION', name: 'load()', desc: 'Fetch attribute definitions' }, { type: 'ACTION', name: 'createAttribute()', desc: 'Create attribute group' }, { type: 'ACTION', name: 'addValue() / deleteValue()', desc: 'Discrete value options' }, { type: 'API', name: 'GET /api/attributename', desc: 'Attributes & values' }, { type: 'SSE', name: 'attributename', desc: 'Real-time attribute signal' }, ], }, // 5. Suppliers & Deliveries { id: 'supplierAutomat', title: 'supplierAutomat', subtitle: 'Supplier Directory & Catalogs', entries: [ { type: 'STATE', name: 'suppliers: Supplier[]', desc: 'Active vendor list' }, { type: 'STATE', name: 'status: \'ready\'', desc: 'Load lifecycle state' }, { type: 'ACTION', name: 'load()', desc: 'Deduplicated fetch' }, { type: 'ACTION', name: 'reload()', desc: 'Silent background sync' }, { type: 'API', name: 'GET /api/supplier', desc: 'Vendor records' }, { type: 'SSE', name: 'supplier', desc: 'Real-time supplier signal' }, ], }, { id: 'deliveryAutomat', title: 'deliveryAutomat', subtitle: 'createListAutomat (Deliveries)', entries: [ { type: 'STATE', name: 'items: Delivery[]', desc: 'Company delivery receipts' }, { type: 'STATE', name: 'status: \'ready\'', desc: 'List lifecycle state' }, { type: 'ACTION', name: 'load() / reload()', desc: 'Deduplicated fetch & sync' }, { type: 'ACTION', name: 'reset()', desc: 'Clear on session switch' }, { type: 'API', name: 'GET /api/delivery', desc: 'Inbound delivery log' }, ], }, { id: 'deliveryEditorAutomat', title: 'deliveryEditorAutomat', subtitle: 'Inbound Receipt & OCR Machine', entries: [ { type: 'STATE', name: 'lines: DeliveryLine[]', desc: 'Uncommitted draft lines', port: 'p_deled_lines' }, { type: 'STATE', name: 'deliveryEntity', desc: 'Selected supplier ID', port: 'p_deled_supplier' }, { type: 'STATE', name: 'attachments: File[]', desc: 'Invoice PDFs & OCR results' }, { type: 'STATE', name: 'draftCount: number', desc: 'Uncommitted lines for badge' }, { type: 'ACTION', name: 'addLine() / updateLine()', desc: 'Interactive item editing' }, { type: 'ACTION', name: 'commitDelivery()', desc: 'Persist goods receipt', port: 'p_deled_commit' }, { type: 'SELECT', name: 'buildSupplierArticlesMap()', desc: 'Auto-match SKU/EAN' }, { type: 'API', name: 'POST /api/delivery', desc: 'Create goods receipt' }, ], }, // 6. Sales & CRM { id: 'quoteAutomat', title: 'quoteAutomat', subtitle: 'createListAutomat (Quotes)', entries: [ { type: 'STATE', name: 'items: Quote[]', desc: 'Sales quotes / Angebote', port: 'p_quote_items' }, { type: 'ACTION', name: 'load() / reload()', desc: 'Deduplicated fetch' }, { type: 'ACTION', name: 'reset()', desc: 'Reset on logout' }, { type: 'API', name: 'GET /api/quote', desc: 'Company quote list' }, ], }, { id: 'orderAutomat', title: 'orderAutomat', subtitle: 'Chunk-Paginated Order Engine', entries: [ { type: 'STATE', name: 'items: Order[]', desc: 'Sales orders & lines', port: 'p_order_items' }, { type: 'STATE', name: 'total: number', desc: 'Unfiltered order count' }, { type: 'STATE', name: 'onlyUnpaid / onlyUndelivered', desc: 'Fulfillment filters' }, { type: 'ACTION', name: 'loadChunk(index)', desc: 'Chunk size 50 prefetch' }, { type: 'ACTION', name: 'reload()', desc: 'Silent orders re-fetch' }, { type: 'SELECT', name: 'selectOrders() / selectTotal()', desc: 'Filter-applied slice' }, { type: 'API', name: 'GET /api/order', desc: 'Paged order stream' }, { type: 'SSE', name: 'order / shipment', desc: 'Fulfillment mutations' }, ], }, { id: 'entityAutomat', title: 'entityAutomat', subtitle: 'createListAutomat (Entities)', entries: [ { type: 'STATE', name: 'items: Entity[]', desc: 'Customers & partners' }, { type: 'ACTION', name: 'load() / reload()', desc: 'Customer directory load' }, { type: 'ACTION', name: 'reset()', desc: 'Reset on session switch' }, { type: 'API', name: 'GET /api/entity', desc: 'Directory records' }, ], }, { id: 'customergroupAutomat', title: 'customergroupAutomat', subtitle: 'createListAutomat (Groups)', entries: [ { type: 'STATE', name: 'items: Customergroup[]', desc: 'Pricing discount tiers' }, { type: 'ACTION', name: 'load() / reload()', desc: 'Discount group load' }, { type: 'API', name: 'GET /api/customergroup', desc: 'Pricing tiers' }, ], }, // 7. Inventory & Storage { id: 'warehouseAutomat', title: 'warehouseAutomat', subtitle: 'Warehouse Locations & Stock', entries: [ { type: 'STATE', name: 'warehouses: Warehouse[]', desc: 'Storage areas & bins' }, { type: 'STATE', name: 'filteredWarehouses', desc: 'Filter search slice' }, { type: 'ACTION', name: 'load() / reload()', desc: 'Deduplicated sync' }, { type: 'ACTION', name: 'setFilter(query)', desc: 'Local filter query' }, { type: 'API', name: 'GET /api/warehouse', desc: 'Warehouse listings' }, { type: 'SSE', name: 'warehouse', desc: 'Real-time stock movement' }, ], }, // 8. Company & Finance { id: 'companyAutomat', title: 'companyAutomat', subtitle: 'Active Company Configuration', entries: [ { type: 'STATE', name: 'current: Company', desc: 'Tax rates & ISO language', port: 'p_comp_curr' }, { type: 'ACTION', name: 'load() / reload()', desc: 'Fetch tenant settings' }, { type: 'SELECT', name: 'selectCompanyTaxrate()', desc: 'Standard tax rate' }, { type: 'SELECT', name: 'selectCompanyRootCategoryId()', desc: 'Root category pointer', port: 'p_comp_root' }, { type: 'SELECT', name: 'selectCompanyName()', desc: 'Company legal name' }, { type: 'API', name: 'GET /api/company/:id', desc: 'Legal & tax profile' }, ], }, { id: 'accountAutomat', title: 'accountAutomat', subtitle: 'createListAutomat (Accounts)', entries: [ { type: 'STATE', name: 'items: Account[]', desc: 'Bank & payment ledgers' }, { type: 'ACTION', name: 'load() / reload()', desc: 'Payment accounts load' }, { type: 'API', name: 'GET /api/account', desc: 'Chart of accounts' }, ], }, { id: 'numberAutomat', title: 'numberAutomat', subtitle: 'createListAutomat (Sequences)', entries: [ { type: 'STATE', name: 'items: NumberSeq[]', desc: 'Invoice/receipt counters' }, { type: 'ACTION', name: 'load() / reload()', desc: 'Number series sync' }, { type: 'API', name: 'GET /api/number', desc: 'Document counters' }, ], }, // 9. POS & Hardware { id: 'posterminalAutomat', title: 'posterminalAutomat', subtitle: 'POS Terminals & Gating', entries: [ { type: 'STATE', name: 'terminals: Posterminal[]', desc: 'Hardware terminals', port: 'p_pos_term' }, { type: 'ACTION', name: 'load()', desc: 'Fetch POS registers' }, { type: 'ACTION', name: 'create() / delete()', desc: 'Terminal registrations' }, { type: 'ACTION', name: 'toggleCategory()', desc: 'Gated category filters' }, { type: 'SELECT', name: 'selectPosterminals()', desc: 'Active registers' }, { type: 'API', name: 'GET /api/posterminal', desc: 'Terminal device configs' }, ], }, // 10. AI Assistant & Workspace { id: 'chatAutomat', title: 'chatAutomat', subtitle: 'AI Assistant Drawer & SSE', entries: [ { type: 'STATE', name: 'isOpen: boolean', desc: 'Docked chat drawer toggle' }, { type: 'STATE', name: 'conversations: []', desc: 'Conversation sessions' }, { type: 'STATE', name: 'messages: ModelMessage[]', desc: 'Live message transcript', port: 'p_chat_msg' }, { type: 'STATE', name: 'sseStatus: \'connected\'', desc: 'Token streaming stream', port: 'p_chat_sse' }, { type: 'ACTION', name: 'sendMessage(text)', desc: 'Prompt model with tools' }, { type: 'ACTION', name: 'answerAskUser()', desc: 'Interactive tool modal' }, { type: 'SELECT', name: 'userEnv / routeTools', desc: 'Client navigation tool', port: 'p_chat_tools' }, { type: 'API', name: 'POST /api/modelconversation', desc: 'AI conversation session' }, { type: 'SSE', name: 'modelconversation/events', desc: 'Live token stream' }, ], }, { id: 'vfsAutomat', title: 'vfsAutomat', subtitle: 'Virtual Filesystem Explorer', entries: [ { type: 'STATE', name: 'files: VfsFile[]', desc: 'Company VFS workspace' }, { type: 'STATE', name: 'selectedFileName', desc: 'Current active file' }, { type: 'STATE', name: 'fileContent / isDirty', desc: 'Editor buffer & diff' }, { type: 'ACTION', name: 'selectFile() / saveFile()', desc: 'Applet editor buffer' }, { type: 'ACTION', name: 'uploadFiles() / deleteFile()', desc: 'File manipulations' }, { type: 'API', name: 'GET /api/vfs', desc: 'VFS directory index' }, { type: 'SSE', name: 'vfs', desc: 'VFS mutation signal' }, ], }, // 11. Audit & Virtual Grids { id: 'articleHistoryGridAutomat', title: 'articleHistoryGridAutomat', subtitle: 'createGridAutomat (Article)', entries: [ { type: 'STATE', name: 'rows: [] / totalRows', desc: 'Block size 50 virtual rows' }, { type: 'ACTION', name: 'fetchRows(viewport)', desc: 'Infinite scroll query' }, { type: 'ACTION', name: 'reload()', desc: 'Reload on company switch' }, { type: 'API', name: 'GET /api/article/history', desc: 'Infinite audit window' }, ], }, { id: 'globalHistoryGridAutomat', title: 'globalHistoryGridAutomat', subtitle: 'createGridAutomat (Global)', entries: [ { type: 'STATE', name: 'rows: [] / totalRows', desc: 'Block size 50 virtual rows' }, { type: 'ACTION', name: 'fetchRows(viewport)', desc: 'Infinite scroll query' }, { type: 'ACTION', name: 'reload()', desc: 'Reload on company switch' }, { type: 'API', name: 'GET /api/globalhistory', desc: 'Company global audit' }, ], }, // 12. UI, Settings & System { id: 'navigationAutomat', title: 'navigationAutomat', subtitle: 'Cross-Section Navigation Memory', entries: [ { type: 'STATE', name: 'lastDeliveryPath', desc: 'Active receipt subpath', port: 'p_nav_delpath' }, { type: 'STATE', name: 'lastArticleSku', desc: 'Active SKU selection', port: 'p_nav_sku' }, { type: 'STATE', name: 'lastCategorySlug / Path', desc: 'Active category tree node', port: 'p_nav_catslug' }, { type: 'STATE', name: 'lastSearchTerm / Path', desc: 'Preserved search query' }, { type: 'ACTION', name: 'setLastDeliveryPath()', desc: 'Restore on sidebar click' }, { type: 'ACTION', name: 'setLastArticleSku()', desc: 'Restore SKU on section click' }, { type: 'ACTION', name: 'clearNavigation()', desc: 'Reset on company switch' }, ], }, { id: 'userSettingsAutomat', title: 'userSettingsAutomat', subtitle: 'IndexedDB User Preferences', entries: [ { type: 'STATE', name: 'viewerPosition: \'top\'|\'right\'', desc: 'Editor docking location' }, { type: 'STATE', name: 'chatDrawerWidth: 460', desc: 'AI drawer pixel width', port: 'p_usr_drawer' }, { type: 'ACTION', name: 'setViewerPosition()', desc: 'Persist docking' }, { type: 'ACTION', name: 'setChatDrawerWidth()', desc: 'Persist drawer width' }, { type: 'HOOK', name: 'persist: true', desc: 'Survives page reloads' }, ], }, { id: 'helpAutomat', title: 'helpAutomat', subtitle: 'IndexedDB Contextual Help', entries: [ { type: 'STATE', name: 'enabled: boolean', desc: 'Interactive help badges' }, { type: 'STATE', name: 'activeHelpId: string|null', desc: 'Active popper ID', port: 'p_help_active' }, { type: 'ACTION', name: 'toggle() / setEnabled()', desc: 'Toggle badge mode' }, { type: 'ACTION', name: 'openPopper() / closePopper()', desc: 'Single active popper' }, { type: 'HOOK', name: 'persist: true', desc: 'Survives page reloads' }, ], }, { id: 'pwaAutomat', title: 'pwaAutomat', subtitle: 'Service Worker & Offline Lifecycle', entries: [ { type: 'STATE', name: 'needRefresh: boolean', desc: 'New SW release pending' }, { type: 'STATE', name: 'offlineReady: boolean', desc: 'Offline assets cached' }, { type: 'STATE', name: 'dialogOpen / updating', desc: 'Prompt modal state' }, { type: 'ACTION', name: 'applyUpdate()', desc: 'Skip waiting & reload' }, { type: 'ACTION', name: 'openDialog() / closeDialog()', desc: 'Modal controls' }, ], }, ]; /** * High-fidelity relationships between Automats: * - Lifecycle synchronization (Auth / Session) * - Real-time SSE dispatch & invalidation * - Hierarchical catalog derivation & indexing * - Purchasing & inventory workflows * - AI tools & virtual workspace */ const EDGE_DEFS = [ // Session & Auth lifecycle { from: 'authAutomat', to: 'sseAutomat', label: 'bind session & cid', port: 'p_auth_user' }, { from: 'authAutomat', to: 'companyAutomat', label: 'load company profile', port: 'p_auth_switchCompany' }, { from: 'authAutomat', to: 'categoryAutomat', label: 'sync tree on authed', port: 'p_auth_status' }, { from: 'authAutomat', to: 'brandAutomat', label: 'sync brands on authed', port: 'p_auth_status' }, { from: 'authAutomat', to: 'unitAutomat', label: 'sync units on authed', port: 'p_auth_status' }, { from: 'authAutomat', to: 'attributeAutomat', label: 'sync attributes on authed', port: 'p_auth_status' }, { from: 'authAutomat', to: 'propertyAutomat', label: 'sync properties on authed', port: 'p_auth_status' }, { from: 'authAutomat', to: 'supplierAutomat', label: 'sync suppliers on authed', port: 'p_auth_status' }, { from: 'authAutomat', to: 'warehouseAutomat', label: 'sync warehouses on authed', port: 'p_auth_status' }, { from: 'authAutomat', to: 'posterminalAutomat', label: 'sync terminals on authed', port: 'p_auth_status' }, { from: 'authAutomat', to: 'deliveryAutomat', label: 'sync deliveries on authed', port: 'p_auth_status' }, { from: 'authAutomat', to: 'quoteAutomat', label: 'sync quotes on authed', port: 'p_auth_status' }, { from: 'authAutomat', to: 'orderAutomat', label: 'sync orders on authed', port: 'p_auth_status' }, { from: 'authAutomat', to: 'entityAutomat', label: 'sync entities on authed', port: 'p_auth_status' }, { from: 'authAutomat', to: 'customergroupAutomat', label: 'sync customer groups', port: 'p_auth_status' }, { from: 'authAutomat', to: 'numberAutomat', label: 'sync number series', port: 'p_auth_status' }, { from: 'authAutomat', to: 'accountAutomat', label: 'sync payment accounts', port: 'p_auth_status' }, { from: 'authAutomat', to: 'chatAutomat', label: 'auth chat & reload', port: 'p_auth_status' }, { from: 'authAutomat', to: 'vfsAutomat', label: 'load company vfs', port: 'p_auth_status' }, { from: 'authAutomat', to: 'articleHistoryGridAutomat', label: 'reload history on authed', port: 'p_auth_status' }, { from: 'authAutomat', to: 'globalHistoryGridAutomat', label: 'reload audit on authed', port: 'p_auth_status' }, // Real-time SSE dispatch { from: 'sseAutomat', to: 'categoryAutomat', label: '\'category\' sse trigger', port: 'p_sse_events' }, { from: 'sseAutomat', to: 'categoryViewAutomat', label: 'invalidate views on sse', port: 'p_sse_events' }, { from: 'sseAutomat', to: 'articleIndexAutomat', label: '\'articles\' sse trigger', port: 'p_sse_events' }, { from: 'sseAutomat', to: 'artikelAutomat', label: '\'article-\' sse reload', port: 'p_sse_events' }, { from: 'sseAutomat', to: 'articleDetailAutomat', label: 'invalidateArticle(sku)', port: 'p_sse_events' }, { from: 'sseAutomat', to: 'warehouseAutomat', label: '\'warehouse\' sse trigger', port: 'p_sse_events' }, { from: 'sseAutomat', to: 'supplierAutomat', label: '\'supplier\' sse trigger', port: 'p_sse_events' }, { from: 'sseAutomat', to: 'brandAutomat', label: '\'brand\' sse trigger', port: 'p_sse_events' }, { from: 'sseAutomat', to: 'attributeAutomat', label: '\'attributename\' sse', port: 'p_sse_events' }, { from: 'sseAutomat', to: 'propertyAutomat', label: '\'propertyname\' sse', port: 'p_sse_events' }, { from: 'sseAutomat', to: 'orderAutomat', label: '\'order\'/\'shipment\' sse', port: 'p_sse_events' }, { from: 'sseAutomat', to: 'vfsAutomat', label: '\'vfs\' sse mutation', port: 'p_sse_events' }, // Catalog, Indexing & Search pipeline { from: 'companyAutomat', to: 'categoryAutomat', label: 'rootCategoryId', port: 'p_comp_root' }, { from: 'categoryAutomat', to: 'categoryViewAutomat', label: 'attachTree & warm slugs', port: 'p_cat_loadTree' }, { from: 'categoryViewAutomat', to: 'articleIndexAutomat', label: 'derived union index', port: 'p_catview_collect' }, { from: 'articleIndexAutomat', to: 'articleSearchAutomat', label: 'query index items', port: 'p_idx_items' }, { from: 'categoryAutomat', to: 'articleSearchAutomat', label: 'categoryFacets', port: 'p_cat_categories' }, { from: 'artikelAutomat', to: 'articleIndexAutomat', label: 'setArticleIndexName (optimistic)', port: 'p_art_setName' }, { from: 'artikelAutomat', to: 'categoryAutomat', label: 'articleCount updates', port: 'p_art_set' }, { from: 'articleDetailAutomat', to: 'artikelAutomat', label: 'watch SKU details', port: 'p_artdet_hist' }, // Purchasing & Deliveries { from: 'deliveryEditorAutomat', to: 'supplierAutomat', label: 'match vendor catalog', port: 'p_deled_supplier' }, { from: 'deliveryEditorAutomat', to: 'artikelAutomat', label: 'lookup SKU & pack units', port: 'p_deled_lines' }, { from: 'deliveryEditorAutomat', to: 'deliveryAutomat', label: 'commit delivery receipt', port: 'p_deled_commit' }, // Sales & CRM { from: 'quoteAutomat', to: 'orderAutomat', label: 'convert quote to order', port: 'p_quote_items' }, { from: 'orderAutomat', to: 'entityAutomat', label: 'customer lookup', port: 'p_order_items' }, { from: 'companyAutomat', to: 'customergroupAutomat', label: 'poscustomergroup', port: 'p_comp_curr' }, // POS & Hardware { from: 'posterminalAutomat', to: 'categoryAutomat', label: 'filter enabled roots', port: 'p_pos_term' }, { from: 'companyAutomat', to: 'posterminalAutomat', label: 'company context', port: 'p_comp_curr' }, // AI Assistant & Workspace { from: 'chatAutomat', to: 'vfsAutomat', label: 'inspect / edit applets', port: 'p_chat_msg' }, { from: 'chatAutomat', to: 'navigationAutomat', label: 'set_user_route tool', port: 'p_chat_tools' }, { from: 'chatAutomat', to: 'sseAutomat', label: 'getSseClientId()', port: 'p_chat_sse' }, // UI Navigation & Preferences { from: 'navigationAutomat', to: 'artikelAutomat', label: 'restore last SKU', port: 'p_nav_sku' }, { from: 'navigationAutomat', to: 'categoryAutomat', label: 'restore last category', port: 'p_nav_catslug' }, { from: 'navigationAutomat', to: 'deliveryEditorAutomat', label: 'restore draft path', port: 'p_nav_delpath' }, { from: 'userSettingsAutomat', to: 'chatAutomat', label: 'chatDrawerWidth', port: 'p_usr_drawer' }, { from: 'helpAutomat', to: 'navigationAutomat', label: 'route-contextual help', port: 'p_help_active' }, // Self-loops { from: 'categoryAutomat', to: 'categoryAutomat', label: 'nested tree hierarchy', port: 'p_cat_categories' }, { from: 'artikelAutomat', to: 'artikelAutomat', label: 'variants & parent SKU', port: 'p_art_set' }, { from: 'chatAutomat', to: 'chatAutomat', label: 'stream delta appends', port: 'p_chat_msg' }, ]; function edgeKey(edge) { return `${edge.from}|${edge.to}|${edge.label}`; } function assignEdgeColors(edges) { const colors = new Map(); let next = 0; for (const edge of edges) { const key = edgeKey(edge); if (!colors.has(key)) { colors.set(key, EDGE_COLORS[next % EDGE_COLORS.length]); next++; } } return colors; } /** * Spanning ranking forest: only one primary outgoing edge per automat * is used for rank constraints. Other edges get constraint=false to avoid * vertical over-stretching. */ function selectRankingEdgeKeys(edges) { const ranking = new Set(); const bySource = new Map(); for (const edge of edges) { if (edge.from === edge.to) continue; if (!bySource.has(edge.from)) bySource.set(edge.from, []); bySource.get(edge.from).push(edge); } for (const group of bySource.values()) { group.sort((a, b) => a.label.localeCompare(b.label)); ranking.add(edgeKey(group[0])); } return ranking; } function buildLegendLabel() { const badgeRow = (type, desc) => { const c = BADGE_COLORS[type]; return ( `` + `${type}` + `${desc}` ); }; const row = (swatch, text) => `${swatch}` + `${text}`; return ( `<` + `` + badgeRow('STATE', 'Reactive store state property') + badgeRow('ACTION', 'State mutation action') + badgeRow('SELECT', 'Reactive PureComponent selector') + badgeRow('API', 'Backend REST/Fetch endpoint') + badgeRow('SSE', 'Inbound Server-Sent Event trigger') + badgeRow('HOOK', 'Auth / persistence lifecycle hook') + row( `in#`, 'Inbound arrival port on Automat header' ) + row( `▪—▶`, 'Reactive dataflow / upstream trigger' ) + `
` + `Automat Graph Legend
>` ); } function renderAutomatNode(spec, inboundCount = 0) { if (spec.id === 'Legend') { return buildLegendLabel(); } const titleFont = `${escapeHtml(spec.title)}` + `
${escapeHtml(spec.subtitle)}`; let headerInner; if (inboundCount > 0) { const portCells = []; for (let i = 0; i < inboundCount; i++) { portCells.push( `` ); } headerInner = `` + `${portCells.join('')}` + `` + `
${titleFont}
`; } else { headerInner = titleFont; } const rows = [ `${headerInner}`, ]; for (const [i, entry] of spec.entries.entries()) { const bColor = BADGE_COLORS[entry.type] ?? { fg: COLORS.text, bg: COLORS.rowBg }; const rowBg = i % 2 === 0 ? COLORS.rowBg : COLORS.rowBgAlt; const portAttr = entry.port ? ` PORT="${escapeHtml(entry.port)}"` : ''; rows.push( `` + `` + `` + `` + `` + `` + `
` + `${escapeHtml(entry.type)}` + `${escapeHtml(entry.name)}` + `${escapeHtml(truncate(entry.desc, 30))}
` ); } return ( `<` + rows.join('') + `
>` ); } function assignEdgeAnchors(edges, positions) { const headPort = new Map(); const tailCompass = new Map(); const inboundCounts = new Map(); const byTarget = new Map(); const bySource = new Map(); for (const edge of edges) { if (!byTarget.has(edge.to)) byTarget.set(edge.to, []); byTarget.get(edge.to).push(edge); if (!bySource.has(edge.from)) bySource.set(edge.from, []); bySource.get(edge.from).push(edge); } for (const [target, group] of byTarget) { group.sort((a, b) => { const ax = positions?.get(a.from)?.x ?? 0; const bx = positions?.get(b.from)?.x ?? 0; if (ax !== bx) return ax - bx; return a.label.localeCompare(b.label); }); inboundCounts.set(target, group.length); group.forEach((edge, i) => { headPort.set(edgeKey(edge), `in${i}`); }); } for (const group of bySource.values()) { group.forEach((edge, i) => { if (edge.from === edge.to) { tailCompass.set(edgeKey(edge), i % 2 === 0 ? 'ne' : 'nw'); return; } const from = positions?.get(edge.from); const to = positions?.get(edge.to); const fromX = from?.x ?? 0; const toX = to?.x ?? fromX; const goEast = toX >= fromX; tailCompass.set(edgeKey(edge), goEast ? 'e' : 'w'); }); } return { headPort, tailCompass, inboundCounts }; } function buildDot(specsMap, edges, positions, anchors) { const edgeColors = assignEdgeColors(edges); const rankingKeys = selectRankingEdgeKeys(edges); const { headPort, tailCompass, inboundCounts } = anchors ?? assignEdgeAnchors(edges, positions); const lines = [ 'digraph automats {', ' graph [rankdir=TB, splines=spline, overlap=false, bgcolor="#1e2430",', ' nodesep=0.95, ranksep=1.0, pad=0.5, fontname="Helvetica",', ' compound=true, mclimit=1.0];', ' node [shape=plaintext, fontname="Helvetica"];', ' edge [fontname="Helvetica", fontsize=9, penwidth=1.4, arrowsize=0.7];', '', ]; const clusteredAutomats = new Set(); for (const cluster of CLUSTERS) { const presentInCluster = cluster.automats.filter((id) => specsMap.has(id)); if (presentInCluster.length === 0) continue; lines.push(` subgraph cluster_${cluster.id} {`); lines.push(` label="${escapeHtml(cluster.label)}"; labeljust="l"; fontsize=15;`); lines.push(` style="rounded,filled"; fillcolor="#202a3a"; color="#3f4c5f";`); lines.push(` fontcolor="#e2e8f0"; margin=14;`); for (const autoId of presentInCluster) { clusteredAutomats.add(autoId); const spec = specsMap.get(autoId); const inCount = inboundCounts.get(autoId) ?? 0; const label = renderAutomatNode(spec, inCount); lines.push(` ${autoId} [label=${label}];`); } lines.push(' }'); } // Any unclustered automats for (const [autoId, spec] of specsMap) { if (clusteredAutomats.has(autoId)) continue; const inCount = inboundCounts.get(autoId) ?? 0; const label = renderAutomatNode(spec, inCount); lines.push(` ${autoId} [label=${label}];`); } lines.push(''); for (const edge of edges) { const key = edgeKey(edge); const color = edgeColors.get(key) ?? '#5aa7ff'; const isRanking = rankingKeys.has(key); const hp = headPort.get(key) ?? 'in0'; const tc = tailCompass.get(key) ?? 'e'; const attrs = [ `color="${color}"`, `fontcolor="${color}"`, `tooltip="${escapeHtml(`${edge.from} → ${edge.to} (${edge.label})`)}"`, `label=" ${escapeHtml(edge.label)} "`, ]; if (!isRanking && edge.from !== edge.to) { attrs.push('constraint=false', 'weight=1'); } else { attrs.push('weight=3'); } // Source port from row entry if available, else exit compass const fromPort = edge.port ? `:${edge.port}:${tc}` : `:${tc}`; const toPort = `:${hp}:n`; lines.push(` ${edge.from}${fromPort} -> ${edge.to}${toPort} [${attrs.join(', ')}];`); } lines.push('}'); return lines.join('\n'); } function parsePlainPositions(plain) { const positions = new Map(); for (const line of plain.split('\n')) { if (!line.startsWith('node ')) continue; const match = line.match(/^node\s+(\S+)\s+([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)/); if (!match) continue; positions.set(match[1], { x: Number(match[2]), y: Number(match[3]), width: Number(match[4]), height: Number(match[5]), }); } return positions; } function fixSelfLoopSvg(svg) { return svg.replace( /\s*(\w+)(?::\w+)?->\2(?::\w+)?<\/title>\s*<g id="(a_edge\d+)"><a ([^>]+)>\s*<path([^>]*) d="([^"]+)"\/>\s*<polygon([^>]*) points="([^"]+)"\/>\s*<\/a>\s*<\/g>\s*<\/g>/g, (match, edgeId, _table, aId, aAttrs, pathAttrs, d, polyAttrs) => { const pts = [...d.matchAll(/([-\d.]+)[,\s]+([-\d.]+)/g)].map((m) => [Number(m[1]), Number(m[2])]); if (pts.length < 2) return match; const [x0, y0] = pts[0]; const [x1, y1] = pts[pts.length - 1]; const east = Math.max(x0, x1) + 36; const north = Math.min(y0, y1) - 28; const tx = x1 - east; const ty = y1 - north; const len = Math.hypot(tx, ty) || 1; const dx = tx / len; const dy = ty / len; const arrowLen = 9; const arrowHalf = 3.5; const baseX = x1 - dx * arrowLen; const baseY = y1 - dy * arrowLen; const px = -dy * arrowHalf; const py = dx * arrowHalf; const fmt = (n) => (Math.round(n * 100) / 100).toString(); const newDFmt = `M${fmt(x0)},${fmt(y0)}C${fmt(east)},${fmt(y0)} ${fmt(east)},${fmt(north)} ${fmt(baseX)},${fmt(baseY)}`; const newPointsFmt = `${fmt(x1)},${fmt(y1)} ` + `${fmt(baseX + px)},${fmt(baseY + py)} ` + `${fmt(baseX - px)},${fmt(baseY - py)} ` + `${fmt(x1)},${fmt(y1)}`; return ( `<g id="${edgeId}" class="edge">\n` + `<title>${_table}->${_table}\n` + `\n` + `\n` + `\n` + `\n` + `\n` + `` ); } ); } console.log('Generating Dashfront Automat Graph…'); const specsMap = new Map(); for (const spec of AUTOMAT_SPECS) { specsMap.set(spec.id, spec); } // Add Legend pseudo-automat specsMap.set('Legend', { id: 'Legend', title: 'Legend', subtitle: '', entries: [], }); const edges = EDGE_DEFS.filter((def) => specsMap.has(def.from) && specsMap.has(def.to)); console.log(`Structured ${specsMap.size - 1} Automats across ${CLUSTERS.length} domain clusters with ${edges.length} relationships`); const graphviz = await Graphviz.load(); // Pass 1: Initial layout to learn coordinates console.log('Layout pass 1/2: computing node positions…'); const initialDot = buildDot(specsMap, edges, null, null); const plain = graphviz.layout(initialDot, 'plain', 'dot'); const positions = parsePlainPositions(plain); // Pass 2: Anchor assignment using learned positions console.log('Layout pass 2/2: assigning header arrival ports & exit directions…'); const anchors = assignEdgeAnchors(edges, positions); const finalDot = buildDot(specsMap, edges, positions, anchors); const rawSvg = graphviz.layout(finalDot, 'svg', 'dot'); const fixedSvg = fixSelfLoopSvg(rawSvg); // Ensure background color is set on svg tag const finalSvg = fixedSvg.replace( /]*?)>/, `` ); writeFileSync(dotOutputPath, finalDot, 'utf8'); writeFileSync(outputPath, finalSvg, 'utf8'); const dim = finalSvg.match(/width="([\d.]+)pt" height="([\d.]+)pt"/); console.log(`Written ${dotOutputPath}`); console.log(`Written ${outputPath}`); if (dim) { console.log(`Layout ${dim[1]}pt × ${dim[2]}pt`); }