proschi 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -15201,125 +15201,237 @@ var componentCatalog = [
15201
15201
  { type: "shape", techStack: "Diamond", category: "Generic Shapes", searchTerms: "diamond rhombus shape decision" },
15202
15202
  { type: "shape", techStack: "Cylinder", category: "Generic Shapes", searchTerms: "cylinder shape storage" },
15203
15203
  { type: "shape", techStack: "Cloud", category: "Generic Shapes", searchTerms: "cloud shape" },
15204
- { type: "shape", techStack: "Actor", category: "Generic Shapes", searchTerms: "actor user person human shape" },
15205
- { type: "shape", techStack: "Note", category: "Generic Shapes", searchTerms: "note document comment annotation" },
15204
+ { type: "shape", techStack: "Actor", category: "Generic Shapes", searchTerms: "actor user person human shape", aliases: ["User", "Client", "Customer", "Person"] },
15205
+ { type: "shape", techStack: "Browser", category: "Generic Shapes", searchTerms: "browser web client user", aliases: ["Web Browser", "Web Client"] },
15206
+ { type: "shape", techStack: "Mobile App", category: "Generic Shapes", searchTerms: "mobile app ios android client user", aliases: ["Mobile", "Mobile Client", "iOS", "iOS App", "Android", "Android App"] },
15207
+ { type: "shape", techStack: "Note", category: "Generic Shapes", searchTerms: "note document comment annotation", kind: "other" },
15208
+ // Generic components: one per kind, for a node whose product does not matter or is not listed.
15209
+ { type: "service", techStack: "Service", category: "Generic Components", searchTerms: "service microservice backend server application", aliases: ["Microservice", "Backend", "Backend Service", "App Server", "Application Server", "Server", "API Server"] },
15210
+ { type: "service", techStack: "Worker", category: "Generic Components", searchTerms: "worker background job consumer processor", aliases: ["Background Worker", "Job Worker", "Consumer", "Job Runner"] },
15211
+ { type: "database", techStack: "Database", category: "Generic Components", searchTerms: "database relational sql db", aliases: ["DB", "RDBMS", "SQL Database", "Relational Database", "SQL"] },
15212
+ { type: "database", techStack: "NoSQL Database", category: "Generic Components", searchTerms: "nosql key value document wide column database", profile: "nosql", aliases: ["NoSQL", "NoSQL DB", "Key-Value Store", "KV Store", "Document Store", "Document Database", "Wide-Column Store"] },
15213
+ { type: "cache", techStack: "Cache", category: "Generic Components", searchTerms: "cache in-memory key value", aliases: ["In-Memory Cache", "Distributed Cache"] },
15214
+ { type: "queue", techStack: "Message Queue", category: "Generic Components", searchTerms: "queue message broker topic stream event bus", aliases: ["Queue", "Message Broker", "Broker", "Event Bus", "Topic", "Event Stream", "Stream"] },
15215
+ { type: "storage", techStack: "Object Storage", category: "Generic Components", searchTerms: "object storage blob bucket files", aliases: ["Object Store", "Blob Store", "Blob Storage", "Bucket", "Storage", "File Storage"] },
15216
+ { type: "cdn", techStack: "CDN", category: "Generic Components", searchTerms: "cdn content delivery network edge cache", kind: "cdn", aliases: ["Content Delivery Network", "Edge Cache"] },
15217
+ { type: "cdn", techStack: "Load Balancer", category: "Generic Components", searchTerms: "load balancer lb reverse proxy", kind: "loadbalancer", aliases: ["LB", "Reverse Proxy", "L7 Load Balancer", "L4 Load Balancer"] },
15218
+ { type: "cdn", techStack: "API Gateway", category: "Generic Components", searchTerms: "api gateway", kind: "gateway", aliases: ["Gateway", "Edge Gateway"] },
15219
+ { type: "cdn", techStack: "DNS", category: "Generic Components", searchTerms: "dns domain name system", kind: "dns", aliases: ["Domain Name System", "DNS Server"] },
15220
+ { type: "cdn", techStack: "WAF", category: "Generic Components", searchTerms: "waf web application firewall edge", kind: "edge", aliases: ["Web Application Firewall", "Firewall"] },
15221
+ { type: "serverless", techStack: "Function", category: "Generic Components", searchTerms: "function serverless faas", aliases: ["Serverless Function", "Serverless", "FaaS", "Cloud Function"] },
15222
+ { type: "database", techStack: "Search Engine", category: "Generic Components", searchTerms: "search engine index full text", kind: "search", aliases: ["Search", "Search Index", "Full-Text Search", "Search Service"] },
15223
+ { type: "database", techStack: "Data Warehouse", category: "Generic Components", searchTerms: "data warehouse lake olap analytics", kind: "analytics", aliases: ["Warehouse", "Data Lake", "OLAP", "Analytics DB", "Analytics Database", "Time Series DB", "Time Series Database", "TSDB"] },
15206
15224
  // Services
15207
- { type: "service", techStack: "REST API", category: "Services", searchTerms: "rest api http service endpoint" },
15208
- { type: "service", techStack: "GraphQL", category: "Services", searchTerms: "graphql api service query" },
15209
- { type: "service", techStack: "gRPC", category: "Services", searchTerms: "grpc rpc service" },
15210
- { type: "service", techStack: "WebSocket", category: "Services", searchTerms: "websocket ws realtime service" },
15211
- { type: "service", techStack: "SOAP API", category: "Services", searchTerms: "soap api xml service" },
15225
+ { type: "service", techStack: "REST API", category: "Services", searchTerms: "rest api http service endpoint", aliases: ["REST", "API", "HTTP API", "Web API", "REST Service"] },
15226
+ { type: "service", techStack: "GraphQL", category: "Services", searchTerms: "graphql api service query", aliases: ["GraphQL API", "Apollo"] },
15227
+ { type: "service", techStack: "gRPC", category: "Services", searchTerms: "grpc rpc service", aliases: ["gRPC Service", "RPC"] },
15228
+ { type: "service", techStack: "WebSocket", category: "Services", searchTerms: "websocket ws realtime service", aliases: ["WebSockets", "WS", "Socket.IO", "WebSocket Server"] },
15229
+ { type: "service", techStack: "SOAP API", category: "Services", searchTerms: "soap api xml service", aliases: ["SOAP"] },
15230
+ // Languages and frameworks: a service written in it.
15231
+ { type: "service", techStack: "Spring Boot", category: "Languages & Frameworks", searchTerms: "spring boot java kotlin service", aliases: ["Spring", "Spring Framework", "Spring WebFlux"] },
15232
+ { type: "service", techStack: "Kotlin", category: "Languages & Frameworks", searchTerms: "kotlin ktor jvm service", aliases: ["Ktor"] },
15233
+ { type: "service", techStack: "Java", category: "Languages & Frameworks", searchTerms: "java jvm service", aliases: ["JVM", "Quarkus", "Micronaut"] },
15234
+ { type: "service", techStack: "Go", category: "Languages & Frameworks", searchTerms: "go golang service", aliases: ["Golang"] },
15235
+ { type: "service", techStack: "Node.js", category: "Languages & Frameworks", searchTerms: "node nodejs javascript typescript express nestjs service", aliases: ["Node", "Express", "Express.js", "NestJS", "TypeScript", "JavaScript"] },
15236
+ { type: "service", techStack: "Python", category: "Languages & Frameworks", searchTerms: "python service" },
15237
+ { type: "service", techStack: "Django", category: "Languages & Frameworks", searchTerms: "django python service" },
15238
+ { type: "service", techStack: "FastAPI", category: "Languages & Frameworks", searchTerms: "fastapi python service" },
15239
+ { type: "service", techStack: "Flask", category: "Languages & Frameworks", searchTerms: "flask python service" },
15240
+ { type: "service", techStack: "Ruby on Rails", category: "Languages & Frameworks", searchTerms: "ruby on rails service", aliases: ["Rails", "Ruby"] },
15241
+ { type: "service", techStack: ".NET", category: "Languages & Frameworks", searchTerms: "dotnet .net asp.net c# service", aliases: ["ASP.NET", "ASP.NET Core", "dotnet", "C#"] },
15242
+ { type: "service", techStack: "PHP", category: "Languages & Frameworks", searchTerms: "php laravel symfony service", aliases: ["Laravel", "Symfony"] },
15243
+ { type: "service", techStack: "Rust", category: "Languages & Frameworks", searchTerms: "rust axum actix service", aliases: ["Axum", "Actix"] },
15244
+ { type: "service", techStack: "Elixir", category: "Languages & Frameworks", searchTerms: "elixir phoenix erlang service", aliases: ["Phoenix", "Erlang"] },
15245
+ // Containers and platforms
15246
+ { type: "container", techStack: "Kubernetes", category: "Containers & Platforms", searchTerms: "kubernetes k8s container pod", aliases: ["K8s", "Kubernetes Pod", "Pod"] },
15247
+ { type: "container", techStack: "Docker", category: "Containers & Platforms", searchTerms: "docker container", aliases: ["Container", "Docker Container"] },
15248
+ { type: "compute", techStack: "VM", category: "Containers & Platforms", searchTerms: "vm virtual machine server instance", aliases: ["Virtual Machine", "Instance", "Bare Metal"] },
15249
+ { type: "serverless", techStack: "Cloudflare Workers", category: "Containers & Platforms", searchTerms: "cloudflare workers edge functions serverless", aliases: ["Workers"] },
15212
15250
  // AWS Serverless
15213
- { type: "serverless", techStack: "AWS Lambda", category: "AWS Services", searchTerms: "aws lambda serverless function" },
15214
- { type: "serverless", techStack: "AWS API Gateway", category: "AWS Services", searchTerms: "aws api gateway rest" },
15251
+ { type: "serverless", techStack: "AWS Lambda", category: "AWS Services", searchTerms: "aws lambda serverless function", aliases: ["Lambda", "Amazon Lambda"] },
15252
+ { type: "serverless", techStack: "AWS API Gateway", category: "AWS Services", searchTerms: "aws api gateway rest", kind: "gateway", aliases: ["Amazon API Gateway"] },
15215
15253
  // AWS Compute
15216
- { type: "compute", techStack: "AWS EC2", category: "AWS Services", searchTerms: "aws ec2 compute instance vm" },
15217
- { type: "container", techStack: "AWS ECS", category: "AWS Services", searchTerms: "aws ecs container docker" },
15218
- { type: "container", techStack: "AWS EKS", category: "AWS Services", searchTerms: "aws eks kubernetes k8s" },
15219
- { type: "container", techStack: "AWS Fargate", category: "AWS Services", searchTerms: "aws fargate serverless container" },
15254
+ { type: "compute", techStack: "AWS EC2", category: "AWS Services", searchTerms: "aws ec2 compute instance vm", aliases: ["EC2", "Amazon EC2"] },
15255
+ { type: "container", techStack: "AWS ECS", category: "AWS Services", searchTerms: "aws ecs container docker", aliases: ["ECS", "Amazon ECS"] },
15256
+ { type: "container", techStack: "AWS EKS", category: "AWS Services", searchTerms: "aws eks kubernetes k8s", aliases: ["EKS", "Amazon EKS"] },
15257
+ { type: "container", techStack: "AWS Fargate", category: "AWS Services", searchTerms: "aws fargate serverless container", aliases: ["Fargate"] },
15220
15258
  // AWS Storage
15221
- { type: "storage", techStack: "AWS S3", category: "AWS Services", searchTerms: "aws s3 storage bucket object" },
15222
- { type: "storage", techStack: "AWS EBS", category: "AWS Services", searchTerms: "aws ebs storage volume disk" },
15223
- { type: "storage", techStack: "AWS EFS", category: "AWS Services", searchTerms: "aws efs storage file system" },
15259
+ { type: "storage", techStack: "AWS S3", category: "AWS Services", searchTerms: "aws s3 storage bucket object", aliases: ["S3", "Amazon S3", "S3 Bucket"] },
15260
+ { type: "storage", techStack: "AWS EBS", category: "AWS Services", searchTerms: "aws ebs storage volume disk", aliases: ["EBS", "Amazon EBS"] },
15261
+ { type: "storage", techStack: "AWS EFS", category: "AWS Services", searchTerms: "aws efs storage file system", aliases: ["EFS", "Amazon EFS"] },
15224
15262
  // AWS Database
15225
- { type: "database", techStack: "AWS RDS", category: "AWS Services", searchTerms: "aws rds database relational sql" },
15226
- { type: "database", techStack: "AWS DynamoDB", category: "AWS Services", searchTerms: "aws dynamodb database nosql" },
15227
- { type: "cache", techStack: "AWS ElastiCache", category: "AWS Services", searchTerms: "aws elasticache redis memcached cache" },
15228
- { type: "database", techStack: "AWS Aurora", category: "AWS Services", searchTerms: "aws aurora database mysql postgresql" },
15263
+ { type: "database", techStack: "AWS RDS", category: "AWS Services", searchTerms: "aws rds database relational sql", aliases: ["RDS", "Amazon RDS"] },
15264
+ { type: "database", techStack: "AWS DynamoDB", category: "AWS Services", searchTerms: "aws dynamodb database nosql", profile: "nosql", aliases: ["Amazon DynamoDB", "Dynamo"] },
15265
+ { type: "cache", techStack: "AWS ElastiCache", category: "AWS Services", searchTerms: "aws elasticache redis memcached cache", aliases: ["ElastiCache", "Amazon ElastiCache"] },
15266
+ { type: "database", techStack: "AWS Aurora", category: "AWS Services", searchTerms: "aws aurora database mysql postgresql", aliases: ["Aurora", "Amazon Aurora", "Aurora PostgreSQL", "Aurora MySQL", "Aurora Serverless"] },
15267
+ { type: "database", techStack: "AWS DocumentDB", category: "AWS Services", searchTerms: "aws documentdb mongodb document database", aliases: ["DocumentDB", "Amazon DocumentDB"] },
15268
+ { type: "database", techStack: "AWS Keyspaces", category: "AWS Services", searchTerms: "aws keyspaces cassandra nosql", profile: "nosql", aliases: ["Keyspaces", "Amazon Keyspaces"] },
15269
+ { type: "database", techStack: "AWS Neptune", category: "AWS Services", searchTerms: "aws neptune graph database", aliases: ["Neptune", "Amazon Neptune"] },
15270
+ { type: "database", techStack: "AWS Redshift", category: "AWS Services", searchTerms: "aws redshift data warehouse analytics", kind: "analytics", aliases: ["Redshift", "Amazon Redshift"] },
15271
+ { type: "database", techStack: "AWS Athena", category: "AWS Services", searchTerms: "aws athena query s3 analytics", kind: "analytics", aliases: ["Athena", "Amazon Athena"] },
15272
+ { type: "database", techStack: "AWS OpenSearch", category: "AWS Services", searchTerms: "aws opensearch elasticsearch search", kind: "search", aliases: ["Amazon OpenSearch", "OpenSearch Service", "AWS Elasticsearch"] },
15229
15273
  // AWS Messaging
15230
- { type: "queue", techStack: "AWS SQS", category: "AWS Services", searchTerms: "aws sqs queue message" },
15231
- { type: "queue", techStack: "AWS SNS", category: "AWS Services", searchTerms: "aws sns notification topic pubsub" },
15232
- { type: "queue", techStack: "AWS EventBridge", category: "AWS Services", searchTerms: "aws eventbridge event bus" },
15233
- { type: "queue", techStack: "AWS Kinesis", category: "AWS Services", searchTerms: "aws kinesis stream data" },
15274
+ { type: "queue", techStack: "AWS SQS", category: "AWS Services", searchTerms: "aws sqs queue message", aliases: ["Amazon SQS", "Simple Queue Service"] },
15275
+ { type: "queue", techStack: "AWS SNS", category: "AWS Services", searchTerms: "aws sns notification topic pubsub", aliases: ["SNS", "Amazon SNS", "Simple Notification Service"] },
15276
+ { type: "queue", techStack: "AWS EventBridge", category: "AWS Services", searchTerms: "aws eventbridge event bus", aliases: ["EventBridge", "Amazon EventBridge"] },
15277
+ { type: "queue", techStack: "AWS Kinesis", category: "AWS Services", searchTerms: "aws kinesis stream data", aliases: ["Kinesis", "Amazon Kinesis", "Kinesis Data Streams"] },
15278
+ { type: "queue", techStack: "AWS MSK", category: "AWS Services", searchTerms: "aws msk managed kafka stream", aliases: ["MSK", "Amazon MSK", "Managed Streaming for Kafka"] },
15234
15279
  // AWS CDN/Network
15235
- { type: "cdn", techStack: "AWS CloudFront", category: "AWS Services", searchTerms: "aws cloudfront cdn edge" },
15236
- { type: "cdn", techStack: "AWS Route53", category: "AWS Services", searchTerms: "aws route53 dns domain" },
15237
- { type: "cdn", techStack: "AWS Load Balancer", category: "AWS Services", searchTerms: "aws alb elb load balancer" },
15280
+ { type: "cdn", techStack: "AWS CloudFront", category: "AWS Services", searchTerms: "aws cloudfront cdn edge", kind: "cdn", aliases: ["CloudFront", "Amazon CloudFront"] },
15281
+ { type: "cdn", techStack: "AWS Route53", category: "AWS Services", searchTerms: "aws route53 dns domain", kind: "dns", aliases: ["Route53", "Amazon Route 53"] },
15282
+ { type: "cdn", techStack: "AWS Load Balancer", category: "AWS Services", searchTerms: "aws alb elb nlb load balancer", kind: "loadbalancer", aliases: ["ALB", "NLB", "ELB", "AWS ALB", "AWS NLB", "AWS ELB", "Application Load Balancer", "Network Load Balancer", "Elastic Load Balancer", "Elastic Load Balancing"] },
15283
+ { type: "cdn", techStack: "AWS WAF", category: "AWS Services", searchTerms: "aws waf web application firewall", kind: "edge", aliases: ["Amazon WAF"] },
15284
+ { type: "cdn", techStack: "AWS Global Accelerator", category: "AWS Services", searchTerms: "aws global accelerator anycast edge", kind: "edge", aliases: ["Global Accelerator"] },
15285
+ // AWS third-party style services
15286
+ { type: "external", techStack: "AWS SES", category: "AWS Services", searchTerms: "aws ses email simple email service", aliases: ["SES", "Amazon SES", "Simple Email Service"] },
15287
+ { type: "external", techStack: "AWS Cognito", category: "AWS Services", searchTerms: "aws cognito auth identity users", aliases: ["Cognito", "Amazon Cognito"] },
15238
15288
  // GCP Serverless
15239
- { type: "serverless", techStack: "GCP Cloud Functions", category: "GCP Services", searchTerms: "gcp google cloud functions serverless" },
15240
- { type: "serverless", techStack: "GCP Cloud Run", category: "GCP Services", searchTerms: "gcp google cloud run serverless container" },
15241
- { type: "serverless", techStack: "GCP App Engine", category: "GCP Services", searchTerms: "gcp google app engine paas" },
15289
+ { type: "serverless", techStack: "GCP Cloud Functions", category: "GCP Services", searchTerms: "gcp google cloud functions serverless", aliases: ["Cloud Functions", "Google Cloud Functions"] },
15290
+ { type: "serverless", techStack: "GCP Cloud Run", category: "GCP Services", searchTerms: "gcp google cloud run serverless container", aliases: ["Cloud Run", "Google Cloud Run"] },
15291
+ { type: "serverless", techStack: "GCP App Engine", category: "GCP Services", searchTerms: "gcp google app engine paas", aliases: ["App Engine", "Google App Engine"] },
15242
15292
  // GCP Compute
15243
- { type: "compute", techStack: "GCP Compute Engine", category: "GCP Services", searchTerms: "gcp google compute engine vm instance" },
15244
- { type: "container", techStack: "GCP GKE", category: "GCP Services", searchTerms: "gcp google gke kubernetes k8s" },
15293
+ { type: "compute", techStack: "GCP Compute Engine", category: "GCP Services", searchTerms: "gcp google compute engine vm instance", aliases: ["Compute Engine", "GCE", "Google Compute Engine"] },
15294
+ { type: "container", techStack: "GCP GKE", category: "GCP Services", searchTerms: "gcp google gke kubernetes k8s", aliases: ["GKE", "Google Kubernetes Engine"] },
15245
15295
  // GCP Storage
15246
- { type: "storage", techStack: "GCP Cloud Storage", category: "GCP Services", searchTerms: "gcp google cloud storage bucket object" },
15247
- { type: "storage", techStack: "GCP Persistent Disk", category: "GCP Services", searchTerms: "gcp google persistent disk storage" },
15296
+ { type: "storage", techStack: "GCP Cloud Storage", category: "GCP Services", searchTerms: "gcp google cloud storage bucket object", aliases: ["GCS", "Cloud Storage", "Google Cloud Storage"] },
15297
+ { type: "storage", techStack: "GCP Persistent Disk", category: "GCP Services", searchTerms: "gcp google persistent disk storage", aliases: ["Persistent Disk"] },
15248
15298
  // GCP Database
15249
- { type: "database", techStack: "GCP Cloud SQL", category: "GCP Services", searchTerms: "gcp google cloud sql database" },
15250
- { type: "database", techStack: "GCP Firestore", category: "GCP Services", searchTerms: "gcp google firestore database nosql" },
15251
- { type: "database", techStack: "GCP Bigtable", category: "GCP Services", searchTerms: "gcp google bigtable database nosql" },
15252
- { type: "database", techStack: "GCP Spanner", category: "GCP Services", searchTerms: "gcp google spanner database sql" },
15253
- { type: "database", techStack: "GCP BigQuery", category: "GCP Services", searchTerms: "gcp google bigquery data warehouse analytics" },
15299
+ { type: "database", techStack: "GCP Cloud SQL", category: "GCP Services", searchTerms: "gcp google cloud sql database", aliases: ["Cloud SQL", "Google Cloud SQL"] },
15300
+ { type: "database", techStack: "GCP Firestore", category: "GCP Services", searchTerms: "gcp google firestore database nosql", profile: "nosqlStrong", aliases: ["Firestore", "Cloud Firestore", "Datastore"] },
15301
+ { type: "database", techStack: "GCP Bigtable", category: "GCP Services", searchTerms: "gcp google bigtable database nosql", profile: "nosqlStrong", aliases: ["Bigtable", "Cloud Bigtable"] },
15302
+ { type: "database", techStack: "GCP Spanner", category: "GCP Services", searchTerms: "gcp google spanner database sql", profile: "nosqlStrong", aliases: ["Spanner", "Cloud Spanner", "Google Spanner"] },
15303
+ { type: "database", techStack: "GCP BigQuery", category: "GCP Services", searchTerms: "gcp google bigquery data warehouse analytics", kind: "analytics", aliases: ["BigQuery", "Google BigQuery"] },
15304
+ { type: "cache", techStack: "GCP Memorystore", category: "GCP Services", searchTerms: "gcp google memorystore redis memcached cache", aliases: ["Memorystore"] },
15254
15305
  // GCP Messaging
15255
- { type: "queue", techStack: "GCP Pub/Sub", category: "GCP Services", searchTerms: "gcp google pubsub pub/sub message queue" },
15306
+ { type: "queue", techStack: "GCP Pub/Sub", category: "GCP Services", searchTerms: "gcp google pubsub pub/sub message queue", aliases: ["Pub/Sub", "Google Pub/Sub", "Cloud Pub/Sub"] },
15256
15307
  // GCP CDN/Network
15257
- { type: "cdn", techStack: "GCP Cloud CDN", category: "GCP Services", searchTerms: "gcp google cloud cdn edge" },
15258
- { type: "cdn", techStack: "GCP Cloud DNS", category: "GCP Services", searchTerms: "gcp google cloud dns domain" },
15259
- { type: "cdn", techStack: "GCP Load Balancing", category: "GCP Services", searchTerms: "gcp google load balancing" },
15308
+ { type: "cdn", techStack: "GCP Cloud CDN", category: "GCP Services", searchTerms: "gcp google cloud cdn edge", kind: "cdn", aliases: ["Cloud CDN", "Google Cloud CDN"] },
15309
+ { type: "cdn", techStack: "GCP Cloud DNS", category: "GCP Services", searchTerms: "gcp google cloud dns domain", kind: "dns", aliases: ["Cloud DNS", "Google Cloud DNS"] },
15310
+ { type: "cdn", techStack: "GCP Load Balancing", category: "GCP Services", searchTerms: "gcp google load balancing", kind: "loadbalancer", aliases: ["Cloud Load Balancing", "GCP Load Balancer", "Google Cloud Load Balancing"] },
15311
+ { type: "cdn", techStack: "GCP Apigee", category: "GCP Services", searchTerms: "gcp google apigee api gateway management", kind: "gateway", aliases: ["Apigee"] },
15260
15312
  // Azure Serverless
15261
- { type: "serverless", techStack: "Azure Functions", category: "Azure Services", searchTerms: "azure functions serverless" },
15262
- { type: "serverless", techStack: "Azure Logic Apps", category: "Azure Services", searchTerms: "azure logic apps workflow" },
15263
- { type: "serverless", techStack: "Azure API Management", category: "Azure Services", searchTerms: "azure api management gateway" },
15313
+ { type: "serverless", techStack: "Azure Functions", category: "Azure Services", searchTerms: "azure functions serverless", aliases: ["Azure Function"] },
15314
+ { type: "serverless", techStack: "Azure Logic Apps", category: "Azure Services", searchTerms: "azure logic apps workflow", aliases: ["Logic Apps"] },
15315
+ { type: "serverless", techStack: "Azure API Management", category: "Azure Services", searchTerms: "azure api management gateway", kind: "gateway", aliases: ["APIM", "Azure APIM"] },
15264
15316
  // Azure Compute
15265
- { type: "compute", techStack: "Azure VM", category: "Azure Services", searchTerms: "azure vm virtual machine compute" },
15266
- { type: "container", techStack: "Azure AKS", category: "Azure Services", searchTerms: "azure aks kubernetes k8s" },
15267
- { type: "container", techStack: "Azure Container Instances", category: "Azure Services", searchTerms: "azure container instances aci" },
15317
+ { type: "compute", techStack: "Azure VM", category: "Azure Services", searchTerms: "azure vm virtual machine compute", aliases: ["Azure Virtual Machine", "Azure Virtual Machines"] },
15318
+ { type: "container", techStack: "Azure AKS", category: "Azure Services", searchTerms: "azure aks kubernetes k8s", aliases: ["AKS", "Azure Kubernetes Service"] },
15319
+ { type: "container", techStack: "Azure Container Instances", category: "Azure Services", searchTerms: "azure container instances aci", aliases: ["ACI"] },
15320
+ { type: "service", techStack: "Azure App Service", category: "Azure Services", searchTerms: "azure app service web apps paas", aliases: ["App Service", "Azure Web Apps"] },
15268
15321
  // Azure Storage
15269
- { type: "storage", techStack: "Azure Blob Storage", category: "Azure Services", searchTerms: "azure blob storage object" },
15322
+ { type: "storage", techStack: "Azure Blob Storage", category: "Azure Services", searchTerms: "azure blob storage object", aliases: ["Azure Blob", "Azure Storage"] },
15270
15323
  { type: "storage", techStack: "Azure Files", category: "Azure Services", searchTerms: "azure files storage file system" },
15271
- { type: "storage", techStack: "Azure Disk Storage", category: "Azure Services", searchTerms: "azure disk storage volume" },
15324
+ { type: "storage", techStack: "Azure Disk Storage", category: "Azure Services", searchTerms: "azure disk storage volume", aliases: ["Azure Managed Disks"] },
15272
15325
  // Azure Database
15273
- { type: "database", techStack: "Azure SQL", category: "Azure Services", searchTerms: "azure sql database" },
15274
- { type: "database", techStack: "Azure Cosmos DB", category: "Azure Services", searchTerms: "azure cosmos db database nosql" },
15275
- { type: "database", techStack: "Azure Database for PostgreSQL", category: "Azure Services", searchTerms: "azure postgresql database" },
15276
- { type: "database", techStack: "Azure Database for MySQL", category: "Azure Services", searchTerms: "azure mysql database" },
15277
- { type: "cache", techStack: "Azure Cache for Redis", category: "Azure Services", searchTerms: "azure redis cache" },
15326
+ { type: "database", techStack: "Azure SQL", category: "Azure Services", searchTerms: "azure sql database", aliases: ["Azure SQL Database"] },
15327
+ { type: "database", techStack: "Azure Cosmos DB", category: "Azure Services", searchTerms: "azure cosmos db database nosql", profile: "nosql", aliases: ["Cosmos DB", "Cosmos"] },
15328
+ { type: "database", techStack: "Azure Database for PostgreSQL", category: "Azure Services", searchTerms: "azure postgresql database", aliases: ["Azure PostgreSQL", "Azure Postgres"] },
15329
+ { type: "database", techStack: "Azure Database for MySQL", category: "Azure Services", searchTerms: "azure mysql database", aliases: ["Azure MySQL"] },
15330
+ { type: "cache", techStack: "Azure Cache for Redis", category: "Azure Services", searchTerms: "azure redis cache", aliases: ["Azure Redis"] },
15331
+ { type: "database", techStack: "Azure AI Search", category: "Azure Services", searchTerms: "azure ai cognitive search", kind: "search", aliases: ["Azure Cognitive Search", "Azure Search"] },
15278
15332
  // Azure Messaging
15279
- { type: "queue", techStack: "Azure Service Bus", category: "Azure Services", searchTerms: "azure service bus queue message" },
15280
- { type: "queue", techStack: "Azure Event Hubs", category: "Azure Services", searchTerms: "azure event hubs stream" },
15281
- { type: "queue", techStack: "Azure Event Grid", category: "Azure Services", searchTerms: "azure event grid" },
15282
- { type: "queue", techStack: "Azure Queue Storage", category: "Azure Services", searchTerms: "azure queue storage message" },
15333
+ { type: "queue", techStack: "Azure Service Bus", category: "Azure Services", searchTerms: "azure service bus queue message", aliases: ["Service Bus"] },
15334
+ { type: "queue", techStack: "Azure Event Hubs", category: "Azure Services", searchTerms: "azure event hubs stream", aliases: ["Event Hubs", "Event Hub"] },
15335
+ { type: "queue", techStack: "Azure Event Grid", category: "Azure Services", searchTerms: "azure event grid", aliases: ["Event Grid"] },
15336
+ { type: "queue", techStack: "Azure Queue Storage", category: "Azure Services", searchTerms: "azure queue storage message", aliases: ["Azure Queue", "Azure Queues"] },
15283
15337
  // Azure CDN/Network
15284
- { type: "cdn", techStack: "Azure CDN", category: "Azure Services", searchTerms: "azure cdn edge" },
15285
- { type: "cdn", techStack: "Azure DNS", category: "Azure Services", searchTerms: "azure dns domain" },
15286
- { type: "cdn", techStack: "Azure Front Door", category: "Azure Services", searchTerms: "azure front door cdn" },
15338
+ { type: "cdn", techStack: "Azure CDN", category: "Azure Services", searchTerms: "azure cdn edge", kind: "cdn" },
15339
+ { type: "cdn", techStack: "Azure DNS", category: "Azure Services", searchTerms: "azure dns domain", kind: "dns" },
15340
+ { type: "cdn", techStack: "Azure Front Door", category: "Azure Services", searchTerms: "azure front door cdn", kind: "cdn", aliases: ["Front Door"] },
15341
+ { type: "cdn", techStack: "Azure Load Balancer", category: "Azure Services", searchTerms: "azure load balancer", kind: "loadbalancer" },
15342
+ { type: "cdn", techStack: "Azure Application Gateway", category: "Azure Services", searchTerms: "azure application gateway l7 load balancer", kind: "loadbalancer", aliases: ["Application Gateway", "Azure App Gateway"] },
15287
15343
  // Traditional Databases
15288
- { type: "database", techStack: "PostgreSQL", category: "Databases", searchTerms: "postgresql postgres sql database" },
15289
- { type: "database", techStack: "MySQL", category: "Databases", searchTerms: "mysql sql database" },
15290
- { type: "database", techStack: "MongoDB", category: "Databases", searchTerms: "mongodb mongo nosql database" },
15291
- { type: "database", techStack: "Redis", category: "Databases", searchTerms: "redis cache database key-value" },
15292
- { type: "database", techStack: "DynamoDB", category: "Databases", searchTerms: "dynamodb nosql database aws" },
15293
- { type: "database", techStack: "Cassandra", category: "Databases", searchTerms: "cassandra nosql database" },
15294
- { type: "database", techStack: "CouchDB", category: "Databases", searchTerms: "couchdb nosql database" },
15295
- { type: "database", techStack: "Elasticsearch", category: "Databases", searchTerms: "elasticsearch search database" },
15296
- { type: "database", techStack: "Neo4j", category: "Databases", searchTerms: "neo4j graph database" },
15297
- { type: "database", techStack: "InfluxDB", category: "Databases", searchTerms: "influxdb timeseries database" },
15298
- { type: "database", techStack: "TimescaleDB", category: "Databases", searchTerms: "timescaledb timeseries postgresql database" },
15299
- { type: "database", techStack: "MariaDB", category: "Databases", searchTerms: "mariadb mysql sql database" },
15344
+ { type: "database", techStack: "PostgreSQL", category: "Databases", searchTerms: "postgresql postgres sql database", aliases: ["Postgres", "PG", "PostGIS", "pgvector"] },
15345
+ { type: "database", techStack: "MySQL", category: "Databases", searchTerms: "mysql sql database", aliases: ["Percona"] },
15346
+ { type: "database", techStack: "MongoDB", category: "Databases", searchTerms: "mongodb mongo nosql database", profile: "nosqlStrong", aliases: ["Mongo", "MongoDB Atlas"] },
15347
+ { type: "database", techStack: "Redis", category: "Databases", searchTerms: "redis cache database key-value", kind: "cache", aliases: ["Redis Cluster", "Redis Cache"] },
15348
+ { type: "database", techStack: "DynamoDB", category: "Databases", searchTerms: "dynamodb nosql database aws", profile: "nosql" },
15349
+ { type: "database", techStack: "Cassandra", category: "Databases", searchTerms: "cassandra nosql database", profile: "nosql", aliases: ["Apache Cassandra", "DataStax"] },
15350
+ { type: "database", techStack: "CouchDB", category: "Databases", searchTerms: "couchdb nosql database", profile: "nosql", aliases: ["Apache CouchDB"] },
15351
+ { type: "database", techStack: "Elasticsearch", category: "Databases", searchTerms: "elasticsearch search database", kind: "search", aliases: ["Elastic", "Elastic Search", "ES"] },
15352
+ { type: "database", techStack: "Neo4j", category: "Databases", searchTerms: "neo4j graph database", aliases: ["Graph Database", "Graph DB"] },
15353
+ { type: "database", techStack: "InfluxDB", category: "Databases", searchTerms: "influxdb timeseries database", kind: "analytics", aliases: ["Influx"] },
15354
+ { type: "database", techStack: "TimescaleDB", category: "Databases", searchTerms: "timescaledb timeseries postgresql database", kind: "analytics", aliases: ["Timescale"] },
15355
+ { type: "database", techStack: "MariaDB", category: "Databases", searchTerms: "mariadb mysql sql database", aliases: ["Maria"] },
15300
15356
  { type: "database", techStack: "SQLite", category: "Databases", searchTerms: "sqlite sql database" },
15301
- { type: "database", techStack: "Oracle", category: "Databases", searchTerms: "oracle sql database" },
15302
- { type: "database", techStack: "SQL Server", category: "Databases", searchTerms: "sql server mssql database microsoft" },
15357
+ { type: "database", techStack: "Oracle", category: "Databases", searchTerms: "oracle sql database", aliases: ["Oracle DB", "Oracle Database"] },
15358
+ { type: "database", techStack: "SQL Server", category: "Databases", searchTerms: "sql server mssql database microsoft", aliases: ["MSSQL", "MS SQL", "Microsoft SQL Server"] },
15359
+ { type: "database", techStack: "CockroachDB", category: "Databases", searchTerms: "cockroachdb distributed sql database", profile: "nosqlStrong", aliases: ["Cockroach"] },
15360
+ { type: "database", techStack: "YugabyteDB", category: "Databases", searchTerms: "yugabytedb distributed sql database", profile: "nosqlStrong", aliases: ["Yugabyte"] },
15361
+ { type: "database", techStack: "TiDB", category: "Databases", searchTerms: "tidb distributed sql mysql database", profile: "nosqlStrong" },
15362
+ { type: "database", techStack: "Vitess", category: "Databases", searchTerms: "vitess sharded mysql planetscale database", profile: "nosqlStrong", aliases: ["PlanetScale"] },
15363
+ { type: "database", techStack: "ScyllaDB", category: "Databases", searchTerms: "scylladb cassandra nosql database", profile: "nosql", aliases: ["Scylla"] },
15364
+ { type: "database", techStack: "Couchbase", category: "Databases", searchTerms: "couchbase nosql document database", profile: "nosql" },
15365
+ { type: "database", techStack: "HBase", category: "Databases", searchTerms: "hbase hadoop wide column nosql database", profile: "nosqlStrong", aliases: ["Apache HBase"] },
15366
+ { type: "database", techStack: "etcd", category: "Databases", searchTerms: "etcd key value consensus configuration" },
15367
+ { type: "database", techStack: "ZooKeeper", category: "Databases", searchTerms: "zookeeper coordination consensus configuration", aliases: ["Apache ZooKeeper"] },
15303
15368
  // Cache & In-Memory
15304
- { type: "cache", techStack: "Memcached", category: "Cache & In-Memory", searchTerms: "memcached cache memory" },
15369
+ { type: "cache", techStack: "Memcached", category: "Cache & In-Memory", searchTerms: "memcached cache memory", aliases: ["Memcache"] },
15305
15370
  { type: "cache", techStack: "Hazelcast", category: "Cache & In-Memory", searchTerms: "hazelcast cache memory" },
15306
15371
  { type: "cache", techStack: "Aerospike", category: "Cache & In-Memory", searchTerms: "aerospike cache database" },
15372
+ { type: "cache", techStack: "Valkey", category: "Cache & In-Memory", searchTerms: "valkey redis cache memory" },
15373
+ { type: "cache", techStack: "KeyDB", category: "Cache & In-Memory", searchTerms: "keydb redis cache memory" },
15374
+ { type: "cache", techStack: "Dragonfly", category: "Cache & In-Memory", searchTerms: "dragonfly dragonflydb redis cache memory", aliases: ["DragonflyDB"] },
15307
15375
  // Message Queues
15308
- { type: "queue", techStack: "Kafka", category: "Message Queues", searchTerms: "kafka message queue stream" },
15309
- { type: "queue", techStack: "RabbitMQ", category: "Message Queues", searchTerms: "rabbitmq message queue amqp" },
15376
+ { type: "queue", techStack: "Kafka", category: "Message Queues", searchTerms: "kafka message queue stream", aliases: ["Apache Kafka", "Confluent", "Confluent Kafka", "Kafka Topic"] },
15377
+ { type: "queue", techStack: "RabbitMQ", category: "Message Queues", searchTerms: "rabbitmq message queue amqp", aliases: ["Rabbit", "AMQP"] },
15310
15378
  { type: "queue", techStack: "SQS", category: "Message Queues", searchTerms: "sqs queue aws message" },
15311
- { type: "queue", techStack: "Redis Queue", category: "Message Queues", searchTerms: "redis queue message" },
15312
- { type: "queue", techStack: "NATS", category: "Message Queues", searchTerms: "nats message queue" },
15313
- { type: "queue", techStack: "Apache Pulsar", category: "Message Queues", searchTerms: "pulsar message queue stream" },
15314
- { type: "queue", techStack: "ActiveMQ", category: "Message Queues", searchTerms: "activemq message queue" },
15315
- { type: "queue", techStack: "ZeroMQ", category: "Message Queues", searchTerms: "zeromq zmq message queue" },
15379
+ { type: "queue", techStack: "Redis Queue", category: "Message Queues", searchTerms: "redis queue message", aliases: ["Redis Streams", "Redis Pub/Sub"] },
15380
+ { type: "queue", techStack: "NATS", category: "Message Queues", searchTerms: "nats message queue", aliases: ["NATS JetStream", "JetStream"] },
15381
+ { type: "queue", techStack: "Apache Pulsar", category: "Message Queues", searchTerms: "pulsar message queue stream", aliases: ["Pulsar"] },
15382
+ { type: "queue", techStack: "ActiveMQ", category: "Message Queues", searchTerms: "activemq message queue", aliases: ["Apache ActiveMQ", "Amazon MQ"] },
15383
+ { type: "queue", techStack: "ZeroMQ", category: "Message Queues", searchTerms: "zeromq zmq message queue", aliases: ["ZMQ", "0MQ"] },
15384
+ { type: "queue", techStack: "Redpanda", category: "Message Queues", searchTerms: "redpanda kafka stream queue" },
15385
+ // Search
15386
+ { type: "database", techStack: "OpenSearch", category: "Search", searchTerms: "opensearch elasticsearch search", kind: "search" },
15387
+ { type: "database", techStack: "Solr", category: "Search", searchTerms: "solr lucene search", kind: "search", aliases: ["Apache Solr"] },
15388
+ { type: "database", techStack: "Meilisearch", category: "Search", searchTerms: "meilisearch search", kind: "search" },
15389
+ { type: "database", techStack: "Typesense", category: "Search", searchTerms: "typesense search", kind: "search" },
15390
+ { type: "database", techStack: "Algolia", category: "Search", searchTerms: "algolia hosted search", kind: "search" },
15391
+ // Analytics
15392
+ { type: "database", techStack: "Snowflake", category: "Analytics", searchTerms: "snowflake data warehouse analytics", kind: "analytics" },
15393
+ { type: "database", techStack: "ClickHouse", category: "Analytics", searchTerms: "clickhouse olap analytics columnar", kind: "analytics" },
15394
+ { type: "database", techStack: "Apache Druid", category: "Analytics", searchTerms: "druid olap analytics", kind: "analytics", aliases: ["Druid"] },
15395
+ { type: "database", techStack: "Apache Pinot", category: "Analytics", searchTerms: "pinot olap analytics", kind: "analytics", aliases: ["Pinot"] },
15396
+ { type: "database", techStack: "Databricks", category: "Analytics", searchTerms: "databricks lakehouse spark analytics", kind: "analytics" },
15397
+ { type: "database", techStack: "Prometheus", category: "Analytics", searchTerms: "prometheus metrics time series monitoring", kind: "analytics" },
15398
+ // Storage
15399
+ { type: "storage", techStack: "MinIO", category: "Storage", searchTerms: "minio s3 object storage" },
15400
+ { type: "storage", techStack: "Cloudflare R2", category: "Storage", searchTerms: "cloudflare r2 object storage", aliases: ["R2"] },
15401
+ { type: "storage", techStack: "Ceph", category: "Storage", searchTerms: "ceph object block storage" },
15402
+ { type: "storage", techStack: "HDFS", category: "Storage", searchTerms: "hdfs hadoop file system storage", aliases: ["Hadoop", "Hadoop HDFS"] },
15403
+ // CDN, proxies and gateways
15404
+ { type: "cdn", techStack: "Cloudflare", category: "CDN & Edge", searchTerms: "cloudflare cdn edge", kind: "cdn", aliases: ["Cloudflare CDN"] },
15405
+ { type: "cdn", techStack: "Fastly", category: "CDN & Edge", searchTerms: "fastly cdn edge", kind: "cdn" },
15406
+ { type: "cdn", techStack: "Akamai", category: "CDN & Edge", searchTerms: "akamai cdn edge", kind: "cdn" },
15407
+ { type: "cdn", techStack: "nginx", category: "CDN & Edge", searchTerms: "nginx reverse proxy load balancer web server", kind: "loadbalancer", aliases: ["NGINX Plus", "OpenResty"] },
15408
+ { type: "cdn", techStack: "Envoy", category: "CDN & Edge", searchTerms: "envoy proxy load balancer service mesh", kind: "loadbalancer", aliases: ["Envoy Proxy"] },
15409
+ { type: "cdn", techStack: "HAProxy", category: "CDN & Edge", searchTerms: "haproxy load balancer proxy", kind: "loadbalancer" },
15410
+ { type: "cdn", techStack: "Traefik", category: "CDN & Edge", searchTerms: "traefik reverse proxy load balancer", kind: "loadbalancer" },
15411
+ { type: "cdn", techStack: "Kubernetes Ingress", category: "CDN & Edge", searchTerms: "kubernetes ingress controller load balancer", kind: "loadbalancer", aliases: ["Ingress", "Ingress Controller", "K8s Ingress"] },
15412
+ { type: "cdn", techStack: "Kong", category: "CDN & Edge", searchTerms: "kong api gateway", kind: "gateway", aliases: ["Kong Gateway"] },
15413
+ { type: "cdn", techStack: "Spring Cloud Gateway", category: "CDN & Edge", searchTerms: "spring cloud gateway api gateway", kind: "gateway" },
15316
15414
  // External Systems
15317
- { type: "external", techStack: "Third Party API", category: "External Systems", searchTerms: "third party api external" },
15318
- { type: "external", techStack: "Payment Gateway", category: "External Systems", searchTerms: "payment gateway stripe paypal" },
15319
- { type: "external", techStack: "Auth Service", category: "External Systems", searchTerms: "auth authentication authorization" },
15320
- { type: "external", techStack: "Email Service", category: "External Systems", searchTerms: "email service sendgrid mailgun" },
15321
- { type: "external", techStack: "SMS Service", category: "External Systems", searchTerms: "sms service twilio" },
15322
- { type: "external", techStack: "Analytics Service", category: "External Systems", searchTerms: "analytics service google analytics" },
15415
+ { type: "external", techStack: "Third Party API", category: "External Systems", searchTerms: "third party api external", aliases: ["External API", "External Service", "Third-Party API", "3rd Party API", "Partner API", "SaaS", "Webhook"] },
15416
+ { type: "external", techStack: "Payment Gateway", category: "External Systems", searchTerms: "payment gateway stripe paypal", aliases: ["Payment Provider", "Payments API", "Payment Processor", "PSP"] },
15417
+ { type: "external", techStack: "Auth Service", category: "External Systems", searchTerms: "auth authentication authorization", aliases: ["Identity Provider", "IdP", "OAuth Provider", "SSO"] },
15418
+ { type: "external", techStack: "Email Service", category: "External Systems", searchTerms: "email service sendgrid mailgun", aliases: ["Email", "Email Provider", "SMTP"] },
15419
+ { type: "external", techStack: "SMS Service", category: "External Systems", searchTerms: "sms service twilio", aliases: ["SMS", "SMS Gateway", "SMS Provider"] },
15420
+ { type: "external", techStack: "Push Service", category: "External Systems", searchTerms: "push notification service mobile", aliases: ["Push Notifications", "Push Notification Service", "Push Provider"] },
15421
+ { type: "external", techStack: "Analytics Service", category: "External Systems", searchTerms: "analytics service google analytics", aliases: ["Google Analytics", "Segment", "Mixpanel", "Amplitude"] },
15422
+ { type: "external", techStack: "Stripe", category: "External Systems", searchTerms: "stripe payments" },
15423
+ { type: "external", techStack: "PayPal", category: "External Systems", searchTerms: "paypal payments" },
15424
+ { type: "external", techStack: "Adyen", category: "External Systems", searchTerms: "adyen payments" },
15425
+ { type: "external", techStack: "Braintree", category: "External Systems", searchTerms: "braintree payments" },
15426
+ { type: "external", techStack: "Twilio", category: "External Systems", searchTerms: "twilio sms voice" },
15427
+ { type: "external", techStack: "SendGrid", category: "External Systems", searchTerms: "sendgrid email" },
15428
+ { type: "external", techStack: "Mailgun", category: "External Systems", searchTerms: "mailgun email" },
15429
+ { type: "external", techStack: "Auth0", category: "External Systems", searchTerms: "auth0 identity login" },
15430
+ { type: "external", techStack: "Okta", category: "External Systems", searchTerms: "okta identity sso" },
15431
+ { type: "external", techStack: "Google Maps", category: "External Systems", searchTerms: "google maps geocoding routing", aliases: ["Google Maps API", "Maps API"] },
15432
+ { type: "external", techStack: "APNs", category: "External Systems", searchTerms: "apns apple push notification", aliases: ["Apple Push Notification Service", "Apple Push"] },
15433
+ { type: "external", techStack: "FCM", category: "External Systems", searchTerms: "fcm firebase cloud messaging push", aliases: ["Firebase Cloud Messaging", "Firebase Messaging"] },
15434
+ { type: "external", techStack: "OpenAI", category: "External Systems", searchTerms: "openai llm ai api", aliases: ["OpenAI API", "LLM API", "LLM"] },
15323
15435
  // Text & Annotations
15324
15436
  { type: "text", techStack: "Text Note", category: "Annotations", searchTerms: "text note annotation comment label" },
15325
15437
  { type: "text", techStack: "Sticky Note", category: "Annotations", searchTerms: "sticky note annotation comment" },
@@ -15330,6 +15442,54 @@ var componentCatalog = [
15330
15442
  { type: "group", techStack: "Security Zone", category: "Grouping", searchTerms: "group security zone boundary" },
15331
15443
  { type: "group", techStack: "Service Group", category: "Grouping", searchTerms: "group service container boundary" }
15332
15444
  ];
15445
+ var techKey = (name) => name.toLowerCase().replace(/[^a-z0-9#+]/g, "");
15446
+ var entries = componentCatalog;
15447
+ var byKey = /* @__PURE__ */ new Map();
15448
+ for (const c of entries) {
15449
+ for (const name of [c.techStack, ...c.aliases ?? []]) {
15450
+ const key = techKey(name);
15451
+ if (!byKey.has(key)) byKey.set(key, c);
15452
+ }
15453
+ }
15454
+ var VERSION = /[\s-]*v?\d+(\.(\d+|x))*\s*$/i;
15455
+ function findTech(name) {
15456
+ const exact = byKey.get(techKey(name));
15457
+ if (exact) return exact;
15458
+ const unversioned = name.replace(VERSION, "");
15459
+ return unversioned !== name && unversioned.trim() ? byKey.get(techKey(unversioned)) : void 0;
15460
+ }
15461
+ function editDistance(a, b) {
15462
+ const d = Array.from({ length: a.length + 1 }, (_, i) => Array.from({ length: b.length + 1 }, (_2, j) => i === 0 ? j : j === 0 ? i : 0));
15463
+ for (let i = 1; i <= a.length; i++) {
15464
+ for (let j = 1; j <= b.length; j++) {
15465
+ const cost2 = a[i - 1] === b[j - 1] ? 0 : 1;
15466
+ d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost2);
15467
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
15468
+ }
15469
+ }
15470
+ return d[a.length][b.length];
15471
+ }
15472
+ function suggestTech(name, accept = (c) => c.type !== "group" && c.type !== "text" && c.techStack !== "Note") {
15473
+ const key = techKey(name);
15474
+ if (!key) return void 0;
15475
+ let best;
15476
+ for (const c of entries) {
15477
+ if (!accept(c)) continue;
15478
+ for (const candidate of [c.techStack, ...c.aliases ?? []]) {
15479
+ const d = editDistance(key, techKey(candidate));
15480
+ if (!best || d < best.d) best = { tech: c.techStack, d };
15481
+ }
15482
+ }
15483
+ if (best && best.d <= Math.max(1, Math.floor(key.length / 4))) return best.tech;
15484
+ const words = name.toLowerCase().split(/[^a-z0-9.#+]+/).filter(Boolean);
15485
+ for (let n = Math.min(3, words.length); n >= 1; n--) {
15486
+ for (let i = 0; i + n <= words.length; i++) {
15487
+ const found = byKey.get(techKey(words.slice(i, i + n).join(" ")));
15488
+ if (found && accept(found) && techKey(words.slice(i, i + n).join("")).length >= 3) return found.techStack;
15489
+ }
15490
+ }
15491
+ return void 0;
15492
+ }
15333
15493
 
15334
15494
  // ../frontend/src/dsl/lexer.ts
15335
15495
  var IDENT_START = /[A-Za-z_]/;
@@ -15479,22 +15639,30 @@ function stripTrailingComment(text) {
15479
15639
  }
15480
15640
  return text;
15481
15641
  }
15482
- function bracketDepth(text) {
15483
- let depth = 0;
15484
- let inString = false;
15485
- for (let i = 0; i < text.length; i++) {
15486
- const ch = text[i];
15487
- if (inString) {
15488
- if (ch === "\\") i++;
15489
- else if (ch === '"') inString = false;
15490
- continue;
15642
+ var BracketCounter = class {
15643
+ depth = 0;
15644
+ inString = false;
15645
+ escaped = false;
15646
+ /** Adds `text` and returns the depth so far. */
15647
+ feed(text) {
15648
+ for (let i = 0; i < text.length; i++) {
15649
+ const ch = text[i];
15650
+ if (this.escaped) {
15651
+ this.escaped = false;
15652
+ continue;
15653
+ }
15654
+ if (this.inString) {
15655
+ if (ch === "\\") this.escaped = true;
15656
+ else if (ch === '"') this.inString = false;
15657
+ continue;
15658
+ }
15659
+ if (ch === '"') this.inString = true;
15660
+ else if (ch === "{" || ch === "[") this.depth++;
15661
+ else if (ch === "}" || ch === "]") this.depth--;
15491
15662
  }
15492
- if (ch === '"') inString = true;
15493
- else if (ch === "{" || ch === "[") depth++;
15494
- else if (ch === "}" || ch === "]") depth--;
15663
+ return this.depth;
15495
15664
  }
15496
- return depth;
15497
- }
15665
+ };
15498
15666
 
15499
15667
  // ../frontend/src/dsl/paths.ts
15500
15668
  var PARAM2 = /^\{[^{}/]+\}$/;
@@ -15532,26 +15700,7 @@ function endpointGroupKey(endpoint, all) {
15532
15700
  var KINDS = ["client", "edge", "cdn", "loadbalancer", "gateway", "dns", "service", "function", "cache", "database", "search", "analytics", "queue", "storage", "external", "other"];
15533
15701
  var DATA_STORE_KINDS = ["cache", "database", "search", "analytics", "queue", "storage"];
15534
15702
  var EDGE_KINDS = ["edge", "cdn", "loadbalancer", "gateway", "dns"];
15535
- var BY_TECH = {
15536
- "AWS CloudFront": "cdn",
15537
- "Azure CDN": "cdn",
15538
- "GCP Cloud CDN": "cdn",
15539
- "Azure Front Door": "cdn",
15540
- "AWS Load Balancer": "loadbalancer",
15541
- "GCP Load Balancing": "loadbalancer",
15542
- "AWS API Gateway": "gateway",
15543
- "Azure API Management": "gateway",
15544
- "AWS Route53": "dns",
15545
- "Azure DNS": "dns",
15546
- "GCP Cloud DNS": "dns",
15547
- Redis: "cache",
15548
- Elasticsearch: "search",
15549
- "GCP BigQuery": "analytics",
15550
- InfluxDB: "analytics",
15551
- TimescaleDB: "analytics",
15552
- // A note shape annotates the diagram; it is not a client.
15553
- Note: "other"
15554
- };
15703
+ var BY_TECH = new Map(componentCatalog.flatMap((c) => "kind" in c ? [[c.techStack, c.kind]] : []));
15555
15704
  var BY_TYPE = {
15556
15705
  shape: "client",
15557
15706
  cdn: "edge",
@@ -15567,7 +15716,44 @@ var BY_TYPE = {
15567
15716
  };
15568
15717
  function kindOf(node) {
15569
15718
  if (node.kind !== "component") return "other";
15570
- return BY_TECH[node.techStack] ?? BY_TYPE[node.type] ?? "other";
15719
+ return node.inferredKind ?? BY_TECH.get(node.techStack) ?? BY_TYPE[node.type] ?? "other";
15720
+ }
15721
+ var TYPE_OF_KIND = {
15722
+ client: "shape",
15723
+ edge: "cdn",
15724
+ cdn: "cdn",
15725
+ loadbalancer: "cdn",
15726
+ gateway: "cdn",
15727
+ dns: "cdn",
15728
+ service: "service",
15729
+ function: "serverless",
15730
+ cache: "cache",
15731
+ database: "database",
15732
+ search: "database",
15733
+ analytics: "database",
15734
+ queue: "queue",
15735
+ storage: "storage",
15736
+ external: "external",
15737
+ other: "shape"
15738
+ };
15739
+ var KIND_WORDS = [
15740
+ [/\bsms\b|e-?mail|third.?party|3rd.?party|external|saas|webhook/, "external"],
15741
+ [/\bcdn\b|edge.?cache|content.?delivery/, "cdn"],
15742
+ [/\bdns\b/, "dns"],
15743
+ [/load.?balanc|\blb\b|\balb\b|\bnlb\b|proxy|ingress/, "loadbalancer"],
15744
+ [/gateway|\bapim?\b.*manage/, "gateway"],
15745
+ [/\bwaf\b|firewall/, "edge"],
15746
+ [/cache|memcache/, "cache"],
15747
+ [/queue|\bmq\b|mq$|broker|event.?stream|\bbus\b|topic|pub.?sub|event.?hub|kafka/, "queue"],
15748
+ [/search|lucene|\bindex\b/, "search"],
15749
+ [/warehouse|analytic|olap|data.?lake|lakehouse|time.?series|\btsdb\b/, "analytics"],
15750
+ [/bucket|blob|object.?stor|storage|\bfs\b|file.?system|\bnas\b|\bdisk\b|volume/, "storage"],
15751
+ [/\bdb\b|db$|database|\bsql\b|sql$|nosql|\bstore\b|ledger|key.?value|\bkv\b/, "database"],
15752
+ [/lambda|function|serverless|\bfaas\b/, "function"]
15753
+ ];
15754
+ function kindFromName(tech) {
15755
+ const text = tech.toLowerCase();
15756
+ return KIND_WORDS.find(([words]) => words.test(text))?.[1];
15571
15757
  }
15572
15758
  function isKind(word) {
15573
15759
  return KINDS.includes(word);
@@ -15677,6 +15863,7 @@ var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
15677
15863
  var DEFAULT_TECH = "Rectangle";
15678
15864
  var DEFAULT_GROUP_TECH = "Logical Group";
15679
15865
  var MAX_SCENARIOS = 32;
15866
+ var MAX_ALT_DEPTH = 16;
15680
15867
  var REPLICAS = /^x(\d+)$/;
15681
15868
  var PERCENTILES = { p50: 50, p90: 90, p95: 95, p99: 99, p999: 99.9 };
15682
15869
  var FIELD_FLAGS = ["key", "index", "unique", "optional"];
@@ -15688,12 +15875,31 @@ var UNIT_EXAMPLES = {
15688
15875
  MBps: "a bandwidth, e.g. 500 MB/s or 1 GB/s",
15689
15876
  "usd/GB": "a price per gigabyte, e.g. 0.05 usd/GB"
15690
15877
  };
15691
- var CAPACITY_PARTS = "a rate, reads, writes, shards, latency, availability, cost, durable or volatile, consistency, bandwidth or egress";
15878
+ var CAPACITY_PARTS = "a rate, reads, writes, shards, latency, availability, cost, durable or volatile, consistency, bandwidth, egress or timeout";
15692
15879
  var ASSERTION_HELP = 'Expected an assertion: "Use case" calls <node>, "Use case" writes <node> before responding, <node> calls <node>, no path from <node> to <node>, <node> has replicas >= 2, \u2026';
15693
15880
  var ASSERTION_VERBS = "calls, every scenario calls, never calls, never waits for, writes, responds, starts at, has scenario or handles failure of";
15694
- var techByName = new Map(
15695
- componentCatalog.map((c) => [c.techStack.toLowerCase(), { type: c.type, techStack: c.techStack }])
15696
- );
15881
+ var GENERIC_TECH = {
15882
+ client: "Actor",
15883
+ edge: "WAF",
15884
+ cdn: "CDN",
15885
+ loadbalancer: "Load Balancer",
15886
+ gateway: "API Gateway",
15887
+ dns: "DNS",
15888
+ service: "Service",
15889
+ function: "Function",
15890
+ cache: "Cache",
15891
+ database: "Database",
15892
+ search: "Search Engine",
15893
+ analytics: "Data Warehouse",
15894
+ queue: "Message Queue",
15895
+ storage: "Object Storage",
15896
+ external: "Third Party API",
15897
+ other: "Note"
15898
+ };
15899
+ function unknownTechMessage(tech, kind, suggestion) {
15900
+ const simulated = `simulated as a generic ${kind}${suggestion === GENERIC_TECH[kind] ? "" : `, like [${GENERIC_TECH[kind]}]`}`;
15901
+ return suggestion ? `Unknown tech stack '${tech}'. Did you mean '${suggestion}'? Until then it is ${simulated}` : `Unknown tech stack '${tech}'; ${simulated}`;
15902
+ }
15697
15903
  function parse(source2, options = {}) {
15698
15904
  return new Parser(source2, options).run();
15699
15905
  }
@@ -15704,8 +15910,12 @@ var Parser = class {
15704
15910
  file;
15705
15911
  stack = [];
15706
15912
  diagnostics = [];
15913
+ /** Warnings reported so far (see `warning`). */
15914
+ warned = /* @__PURE__ */ new Set();
15707
15915
  nodes = /* @__PURE__ */ new Map();
15708
15916
  edges = [];
15917
+ /** Connections so far per `from->to`, for the ids of repeated ones. */
15918
+ edgeCount = /* @__PURE__ */ new Map();
15709
15919
  useCases = [];
15710
15920
  references = [];
15711
15921
  imports = [];
@@ -15782,6 +15992,17 @@ var Parser = class {
15782
15992
  const loc = (t) => this.locOf(t, line);
15783
15993
  const top = this.top();
15784
15994
  if (top?.kind === "section") {
15995
+ if (top.skipped !== void 0) {
15996
+ const opens = tokens[tokens.length - 1].kind === "lbrace";
15997
+ if (first.kind !== "rbrace") top.nested = (top.nested ?? 0) + (opens ? 1 : 0);
15998
+ else if (top.nested) top.nested += opens ? 0 : -1;
15999
+ else {
16000
+ this.stack.pop();
16001
+ if (second?.kind === "ident" && second.value === "alt") this.parseAlt(tokens.slice(1), line);
16002
+ else if (second) this.error("Unexpected input after }", loc(second));
16003
+ }
16004
+ return index;
16005
+ }
15785
16006
  if (first.kind === "rbrace") {
15786
16007
  this.stack.pop();
15787
16008
  if (!top.discard) this.closeSection(top);
@@ -15930,8 +16151,8 @@ var Parser = class {
15930
16151
  if (tokens[i]?.kind === "string") name = tokens[i++].value;
15931
16152
  if (tokens[i]?.kind === "tech") {
15932
16153
  const tech = tokens[i++];
15933
- const resolved = this.resolveTech(tech, line);
15934
- if (resolved.type === "group") techStack = resolved.techStack;
16154
+ const resolved = findTech(tech.value);
16155
+ if (resolved?.type === "group") techStack = resolved.techStack;
15935
16156
  else this.warning(`'${tech.value}' is not a group style; using ${DEFAULT_GROUP_TECH}`, this.locOf(tech, line));
15936
16157
  }
15937
16158
  let position;
@@ -16006,6 +16227,12 @@ var Parser = class {
16006
16227
  return;
16007
16228
  }
16008
16229
  this.expectEnd(tokens, i + 1, line);
16230
+ if (this.stack.filter((f) => f.kind === "alt").length >= MAX_ALT_DEPTH) {
16231
+ const loc2 = this.locOf(keyword, line);
16232
+ this.error(`alt blocks can nest at most ${MAX_ALT_DEPTH} deep; this one is skipped`, loc2);
16233
+ this.stack.push({ kind: "section", section: "test", discard: true, skipped: `alt '${nameToken.value}'`, loc: loc2 });
16234
+ return;
16235
+ }
16009
16236
  const owner = top.body;
16010
16237
  let set = owner.openAlt;
16011
16238
  if (!set) {
@@ -16058,6 +16285,7 @@ var Parser = class {
16058
16285
  const resolved = this.resolveTech(t, line);
16059
16286
  node.type = resolved.type;
16060
16287
  node.techStack = resolved.techStack;
16288
+ if (resolved.inferredKind) node.inferredKind = resolved.inferredKind;
16061
16289
  node.kind = resolved.type === "text" ? "text" : resolved.type === "group" ? "group" : "component";
16062
16290
  } else if (t.kind === "team") {
16063
16291
  node.ownerTeam = t.value;
@@ -16099,12 +16327,16 @@ var Parser = class {
16099
16327
  }
16100
16328
  let label = labelToken?.value ?? "";
16101
16329
  let lastLine = index;
16102
- if (bracketDepth(label) > 0) {
16103
- while (bracketDepth(label) > 0 && lastLine + 1 < this.lines.length) {
16330
+ const brackets = new BracketCounter();
16331
+ let depth = brackets.feed(label);
16332
+ if (depth > 0) {
16333
+ while (depth > 0 && lastLine + 1 < this.lines.length) {
16104
16334
  lastLine++;
16105
- label += "\n" + this.lines[lastLine];
16335
+ const more = "\n" + this.lines[lastLine];
16336
+ label += more;
16337
+ depth = brackets.feed(more);
16106
16338
  }
16107
- if (bracketDepth(label) > 0) this.error("Unclosed { or [ in payload", this.locOf(labelToken, line));
16339
+ if (depth > 0) this.error("Unclosed { or [ in payload", this.locOf(labelToken, line));
16108
16340
  label = label.trimEnd();
16109
16341
  }
16110
16342
  const loc = {
@@ -16139,7 +16371,8 @@ var Parser = class {
16139
16371
  this.error("'-x' marks a failed call and is only allowed in use case steps", this.locOf(arrowToken, line));
16140
16372
  } else {
16141
16373
  const base = `${from}->${to}`;
16142
- const count = this.edges.filter((e) => e.id === base || e.id.startsWith(`${base}#`)).length;
16374
+ const count = this.edgeCount.get(base) ?? 0;
16375
+ this.edgeCount.set(base, count + 1);
16143
16376
  this.edges.push({ id: count ? `${base}#${count + 1}` : base, source: from, target: to, label: unquote(label) || void 0, loc });
16144
16377
  }
16145
16378
  return lastLine;
@@ -16407,12 +16640,13 @@ var Parser = class {
16407
16640
  if (rps === void 0) return;
16408
16641
  override.rps = rps;
16409
16642
  i++;
16410
- } else if (isWord(t, "latency") || isWord(t, "availability") || isWord(t, "cost")) {
16643
+ } else if (isWord(t, "latency") || isWord(t, "availability") || isWord(t, "cost") || isWord(t, "timeout")) {
16411
16644
  part = t.value;
16412
- const unit = part === "latency" ? "ms" : part === "availability" ? "%" : "usd/month";
16645
+ const unit = part === "latency" || part === "timeout" ? "ms" : part === "availability" ? "%" : "usd/month";
16413
16646
  const value = this.quantityOf(tokens[i + 1], unit, t, line);
16414
16647
  if (value === void 0) return;
16415
16648
  if (part === "latency") override.latencyMs = value;
16649
+ else if (part === "timeout") override.timeoutMs = value;
16416
16650
  else if (part === "availability") override.availability = value;
16417
16651
  else override.costUsd = value;
16418
16652
  i += 2;
@@ -16820,7 +17054,7 @@ var Parser = class {
16820
17054
  parseSelectorAtom(tokens, i, line) {
16821
17055
  const t = tokens[i];
16822
17056
  if (t?.kind === "tech") {
16823
- const tech = techByName.get(t.value.toLowerCase())?.techStack ?? t.value;
17057
+ const tech = findTech(t.value)?.techStack ?? t.value;
16824
17058
  return { selector: { tech }, next: i + 1, token: t, loc: this.locOf(t, line) };
16825
17059
  }
16826
17060
  if (isWord(t, "any")) {
@@ -16845,7 +17079,10 @@ var Parser = class {
16845
17079
  /** Warns about a selector naming a tech stack that does not exist, and (once every file is read) a node that does not. */
16846
17080
  checkSelectorLater({ atoms }) {
16847
17081
  for (const { selector, token, loc } of atoms) {
16848
- if ("tech" in selector && !techByName.has(token.value.toLowerCase())) this.warning(`Unknown tech stack '${token.value}'`, loc);
17082
+ if ("tech" in selector && !findTech(token.value)) {
17083
+ const suggestion = suggestTech(token.value);
17084
+ this.warning(`Unknown tech stack '${token.value}'${suggestion ? `. Did you mean '${suggestion}'?` : ""}`, loc);
17085
+ }
16849
17086
  if ("node" in selector) {
16850
17087
  this.deferred.push(() => {
16851
17088
  if (!this.nodes.has(selector.node)) this.warning(`Unknown node '${selector.node}'`, loc);
@@ -17005,11 +17242,27 @@ var Parser = class {
17005
17242
  where(first, later) {
17006
17243
  return first.file === later.file ? `on line ${first.line}` : `in ${fileName(first.file ?? this.options.path ?? "the root document")} on line ${first.line}`;
17007
17244
  }
17245
+ /**
17246
+ * A node's `[Tech]`: the catalog entry it names (name or alias), or for an
17247
+ * unknown tech the text as written, with a warning that names the closest
17248
+ * catalog tech. It is drawn and simulated as the kind its name suggests
17249
+ * (`TigerBeetle DB`: database), else the kind of that closest tech
17250
+ * (`Postgress`: database), else a service; never a free, infinitely fast
17251
+ * client.
17252
+ */
17008
17253
  resolveTech(token, line) {
17009
- const resolved = techByName.get(token.value.toLowerCase());
17010
- if (resolved) return resolved;
17011
- this.warning(`Unknown tech stack '${token.value}'; drawing a ${DEFAULT_TECH}`, this.locOf(token, line));
17012
- return { type: "shape", techStack: DEFAULT_TECH };
17254
+ const found = findTech(token.value);
17255
+ if (found) return { type: found.type, techStack: found.techStack };
17256
+ const text = token.value.trim();
17257
+ if (!text) {
17258
+ this.warning(`Empty tech stack; drawing a ${DEFAULT_TECH}`, this.locOf(token, line));
17259
+ return { type: "shape", techStack: DEFAULT_TECH };
17260
+ }
17261
+ const suggestion = suggestTech(text);
17262
+ const closest = suggestion && findTech(suggestion);
17263
+ const kind = kindFromName(text) ?? (closest ? kindOf({ kind: "component", type: closest.type, techStack: closest.techStack }) : void 0) ?? "service";
17264
+ this.warning(unknownTechMessage(text, kind, suggestion), this.locOf(token, line));
17265
+ return { type: TYPE_OF_KIND[kind], techStack: text, inferredKind: kind };
17013
17266
  }
17014
17267
  useCaseId(name) {
17015
17268
  const base = slug(name) || "usecase";
@@ -17065,7 +17318,12 @@ var Parser = class {
17065
17318
  this.diagnostics.push({ severity: "error", message, ...loc });
17066
17319
  }
17067
17320
  warning(message, loc) {
17068
- if (this.diagnostics.some((d) => d.message === message && d.line === loc.line && d.col === loc.col && d.file === loc.file)) return;
17321
+ const key = `${loc.file ?? ""}
17322
+ ${loc.line}
17323
+ ${loc.col}
17324
+ ${message}`;
17325
+ if (this.warned.has(key)) return;
17326
+ this.warned.add(key);
17069
17327
  this.diagnostics.push({ severity: "warning", message, ...loc });
17070
17328
  }
17071
17329
  };
@@ -17098,6 +17356,7 @@ function isWord(token, word) {
17098
17356
  return token?.kind === "ident" && token.value === word;
17099
17357
  }
17100
17358
  function sectionName(frame) {
17359
+ if (frame.skipped) return frame.skipped;
17101
17360
  if (frame.entity) return `entity '${frame.entity.name}'`;
17102
17361
  if (frame.decision) return `decision '${frame.decision.title}'`;
17103
17362
  if (frame.test) return `test '${frame.test.name}'`;
@@ -17189,14 +17448,14 @@ function describe(token) {
17189
17448
 
17190
17449
  // ../frontend/src/dsl/format.ts
17191
17450
  function format(source2) {
17192
- const entries = classify(source2.split("\n").map((l) => l.endsWith("\r") ? l.slice(0, -1) : l));
17451
+ const entries2 = classify(source2.split("\n").map((l) => l.endsWith("\r") ? l.slice(0, -1) : l));
17193
17452
  const out = [];
17194
17453
  let run2 = [];
17195
17454
  const flush = () => {
17196
17455
  if (run2.length) out.push(...renderRun(run2));
17197
17456
  run2 = [];
17198
17457
  };
17199
- tidyBlankLines(entries).forEach((entry) => {
17458
+ tidyBlankLines(entries2).forEach((entry) => {
17200
17459
  const last = run2[run2.length - 1];
17201
17460
  if (last && (entry.kind !== last.kind || entry.depth !== last.depth || last.kind === "connection" && last.continuation.length || last.kind === "cells" && entry.kind === "cells" && entry.section !== last.section))
17202
17461
  flush();
@@ -17212,19 +17471,19 @@ function format(source2) {
17212
17471
  var INDENT = " ";
17213
17472
  var KEYWORDS = /* @__PURE__ */ new Set(["title", "import", "group", "usecase", "par", "alt"]);
17214
17473
  function classify(lines) {
17215
- const entries = [];
17474
+ const entries2 = [];
17216
17475
  let depth = 0;
17217
17476
  let section;
17218
17477
  for (let i = 0; i < lines.length; i++) {
17219
17478
  const raw = lines[i];
17220
17479
  const original = raw.trim();
17221
17480
  if (!original) {
17222
- entries.push({ kind: "blank", depth, text: "" });
17481
+ entries2.push({ kind: "blank", depth, text: "" });
17223
17482
  continue;
17224
17483
  }
17225
17484
  const { tokens, diagnostics } = tokenizeLine(raw, i + 1);
17226
17485
  if (diagnostics.some((d) => d.severity === "error") || tokens.length === 0) {
17227
- entries.push({ kind: "line", depth, text: original });
17486
+ entries2.push({ kind: "line", depth, text: original });
17228
17487
  continue;
17229
17488
  }
17230
17489
  const comment = commentOf(raw, tokens);
@@ -17233,22 +17492,22 @@ function classify(lines) {
17233
17492
  const inSection = section !== void 0 && depth === 1 && first.kind !== "rbrace";
17234
17493
  const cells = inSection ? sectionCells(section, raw, tokens) : void 0;
17235
17494
  if (cells) {
17236
- entries.push({ ...base, kind: "cells", section, cells });
17495
+ entries2.push({ ...base, kind: "cells", section, cells });
17237
17496
  continue;
17238
17497
  }
17239
17498
  if (!inSection && first.kind === "ident" && second?.kind === "arrow" && third?.kind === "ident" && tokens.length <= 4 && (!fourth || fourth.kind === "label")) {
17240
17499
  const entry = { ...base, kind: "connection", from: first.value, arrow: second.value, to: third.value, label: fourth?.value, continuation: [] };
17241
- if (fourth && bracketDepth(fourth.value) > 0) {
17242
- const delta = depth * INDENT.length - width(leadingSpace(raw));
17243
- let label = fourth.value;
17244
- while (bracketDepth(label) > 0 && i + 1 < lines.length) {
17500
+ const brackets = new BracketCounter();
17501
+ if (fourth && brackets.feed(fourth.value) > 0) {
17502
+ const delta = indent(depth).length - width(leadingSpace(raw));
17503
+ while (i + 1 < lines.length) {
17245
17504
  const next = lines[++i];
17246
- label += "\n" + next;
17247
17505
  entry.continuation.push(reindent(next, delta));
17506
+ if (brackets.feed("\n" + next) <= 0) break;
17248
17507
  }
17249
17508
  while (entry.continuation[entry.continuation.length - 1] === "") entry.continuation.pop();
17250
17509
  }
17251
- entries.push(entry);
17510
+ entries2.push(entry);
17252
17511
  continue;
17253
17512
  }
17254
17513
  const oneLineDecision = first.value === "decision" && second?.kind === "string" && third?.kind === "ident" && third.value === "because";
@@ -17256,19 +17515,19 @@ function classify(lines) {
17256
17515
  let k = 1;
17257
17516
  const take = (kind) => tokens[k]?.kind === kind ? source(raw, tokens[k++]) : "";
17258
17517
  const cells2 = [first.value, take("string"), take("tech"), take("team"), join2(raw, tokens.slice(k))];
17259
- entries.push({ ...base, kind: "node", cells: cells2 });
17518
+ entries2.push({ ...base, kind: "node", cells: cells2 });
17260
17519
  continue;
17261
17520
  }
17262
17521
  const closes = first.kind === "rbrace";
17263
17522
  const opens = tokens[tokens.length - 1].kind === "lbrace";
17264
17523
  const code = join2(raw, tokens);
17265
17524
  const text = withComment(code, comment);
17266
- entries.push({ kind: "line", depth: Math.max(0, depth - (closes ? 1 : 0)), text: sameTokens(text, tokens) ? text : original, opens, closes });
17525
+ entries2.push({ kind: "line", depth: Math.max(0, depth - (closes ? 1 : 0)), text: sameTokens(text, tokens) ? text : original, opens, closes });
17267
17526
  if (depth === 0) section = sectionOpenedBy(tokens);
17268
17527
  for (const t of tokens) depth = Math.max(0, depth + (t.kind === "lbrace" ? 1 : t.kind === "rbrace" ? -1 : 0));
17269
17528
  if (depth === 0) section = void 0;
17270
17529
  }
17271
- return entries;
17530
+ return entries2;
17272
17531
  }
17273
17532
  function sectionOpenedBy(tokens) {
17274
17533
  const [first, second] = tokens;
@@ -17302,9 +17561,9 @@ function sectionCells(section, raw, tokens) {
17302
17561
  return void 0;
17303
17562
  }
17304
17563
  }
17305
- function tidyBlankLines(entries) {
17564
+ function tidyBlankLines(entries2) {
17306
17565
  const kept = [];
17307
- for (const entry of entries) {
17566
+ for (const entry of entries2) {
17308
17567
  if (entry.kind !== "blank") {
17309
17568
  if (entry.kind === "line" && entry.closes) while (kept[kept.length - 1]?.kind === "blank") kept.pop();
17310
17569
  kept.push(entry);
@@ -17385,7 +17644,8 @@ var source = (raw, t) => {
17385
17644
  var withComment = (code, comment) => comment ? code + " " + comment : code;
17386
17645
  var leadingSpace = (line) => /^[ \t]*/.exec(line)[0];
17387
17646
  var width = (space) => [...space].reduce((n, ch) => n + (ch === " " ? INDENT.length : 1), 0);
17388
- var indent = (depth) => INDENT.repeat(depth);
17647
+ var MAX_INDENT = 32;
17648
+ var indent = (depth) => INDENT.repeat(Math.min(depth, MAX_INDENT));
17389
17649
  var len = (s) => [...s].length;
17390
17650
  var pad = (s, to) => s + " ".repeat(Math.max(0, to - len(s)));
17391
17651
 
@@ -17535,6 +17795,8 @@ function accessIn(diagram) {
17535
17795
 
17536
17796
  // ../frontend/src/sim/profiles.ts
17537
17797
  var NEUTRAL = { rps: Infinity, latencyMs: 0, availability: 1, costUsd: 0, durable: false };
17798
+ var INTERNET_EGRESS_USD_PER_GB = 0.09;
17799
+ var CDN_EGRESS_USD_PER_GB = 0.02;
17538
17800
  var RELATIONAL = {
17539
17801
  rps: 2e4,
17540
17802
  readRps: 2e4,
@@ -17548,8 +17810,9 @@ var RELATIONAL = {
17548
17810
  };
17549
17811
  var KIND_PROFILES = {
17550
17812
  client: { ...NEUTRAL, bandwidthMBps: 10 },
17813
+ // A firewall or accelerator in front of everything else (WAF, Global Accelerator); `any edge` also selects the four below.
17551
17814
  edge: { rps: 1e5, latencyMs: 2, availability: 0.9999, costUsd: 50, durable: false },
17552
- cdn: { rps: 2e5, latencyMs: 5, availability: 0.9999, costUsd: 100, durable: false, egressUsdPerGb: 0.02 },
17815
+ cdn: { rps: 2e5, latencyMs: 5, availability: 0.9999, costUsd: 100, durable: false, egressUsdPerGb: CDN_EGRESS_USD_PER_GB },
17553
17816
  loadbalancer: { rps: 1e5, latencyMs: 2, availability: 0.9999, costUsd: 50, durable: false },
17554
17817
  gateway: { rps: 1e4, latencyMs: 10, availability: 0.9995, costUsd: 100, durable: false },
17555
17818
  // Name resolution happens before the request and is cached: off the request path.
@@ -17561,23 +17824,16 @@ var KIND_PROFILES = {
17561
17824
  search: { rps: 3e3, latencyMs: 15, availability: 0.999, costUsd: 400, durable: true, consistency: "eventual" },
17562
17825
  analytics: { rps: 200, latencyMs: 500, availability: 0.999, costUsd: 300, durable: true, consistency: "eventual" },
17563
17826
  queue: { rps: 5e4, latencyMs: 5, availability: 0.9999, costUsd: 200, durable: true, consistency: "strong" },
17564
- storage: { rps: 5e3, latencyMs: 30, availability: 0.9999, costUsd: 50, durable: true, consistency: "eventual", egressUsdPerGb: 0.09 },
17827
+ storage: { rps: 5e3, latencyMs: 30, availability: 0.9999, costUsd: 50, durable: true, consistency: "eventual" },
17565
17828
  external: { rps: 1e3, latencyMs: 200, availability: 0.999, costUsd: 0, durable: false },
17566
17829
  other: NEUTRAL
17567
17830
  };
17568
17831
  var NOSQL = { rps: 2e4, latencyMs: 5, availability: 0.9999, costUsd: 500, durable: true, consistency: "eventual" };
17569
17832
  var NOSQL_STRONG = { ...NOSQL, consistency: "strong" };
17570
- var TECH_PROFILES = {
17571
- DynamoDB: NOSQL,
17572
- "AWS DynamoDB": NOSQL,
17573
- Cassandra: NOSQL,
17574
- CouchDB: NOSQL,
17575
- "Azure Cosmos DB": NOSQL,
17576
- MongoDB: NOSQL_STRONG,
17577
- "GCP Bigtable": NOSQL_STRONG,
17578
- "GCP Firestore": NOSQL_STRONG,
17579
- "GCP Spanner": NOSQL_STRONG
17580
- };
17833
+ var PROFILE_ROWS = { nosql: NOSQL, nosqlStrong: NOSQL_STRONG };
17834
+ var TECH_PROFILES = Object.fromEntries(
17835
+ componentCatalog.flatMap((c) => "profile" in c ? [[c.techStack, PROFILE_ROWS[c.profile]]] : [])
17836
+ );
17581
17837
  var BANDWIDTH = {
17582
17838
  client: 10,
17583
17839
  edge: 1e3,
@@ -17588,9 +17844,10 @@ var BANDWIDTH = {
17588
17844
  service: 200
17589
17845
  };
17590
17846
  var DEFAULT_BANDWIDTH_MBPS = 100;
17847
+ var CHARGES_EGRESS = (kind) => kind !== "client" && kind !== "external" && kind !== "dns" && kind !== "other";
17591
17848
  function profileOf(node, override) {
17592
17849
  const kind = kindOf(node);
17593
- const base = TECH_PROFILES[node.techStack] ?? KIND_PROFILES[kind];
17850
+ const base = (Object.hasOwn(TECH_PROFILES, node.techStack) ? TECH_PROFILES[node.techStack] : void 0) ?? KIND_PROFILES[kind];
17594
17851
  const baseRead = base.readRps ?? base.rps;
17595
17852
  const baseWrite = base.writeRps ?? base.rps;
17596
17853
  const readRps = override?.readRps ?? override?.rps ?? baseRead;
@@ -17609,7 +17866,8 @@ function profileOf(node, override) {
17609
17866
  durable: override?.durable ?? base.durable,
17610
17867
  ...consistency && isDataStore(kind) ? { consistency } : {},
17611
17868
  bandwidthMBps: override?.bandwidthMBps ?? base.bandwidthMBps ?? BANDWIDTH[kind] ?? DEFAULT_BANDWIDTH_MBPS,
17612
- egressUsdPerGb: override?.egressUsdPerGb ?? base.egressUsdPerGb ?? 0
17869
+ egressUsdPerGb: override?.egressUsdPerGb ?? base.egressUsdPerGb ?? (CHARGES_EGRESS(kind) ? INTERNET_EGRESS_USD_PER_GB : 0),
17870
+ ...override?.timeoutMs !== void 0 ? { timeoutMs: override.timeoutMs } : {}
17613
17871
  };
17614
17872
  }
17615
17873
  function capacityOf(p, replicas) {
@@ -17762,10 +18020,11 @@ function formatUsd(usd) {
17762
18020
  var DEFAULT_TIMEOUT_MS = 1e3;
17763
18021
  var HOT = 0.7;
17764
18022
  var QUEUE_CAP = 0.95;
18023
+ var FAILOVER_SHARE = 0.1;
18024
+ var SERVICE_SPREAD = 0.5;
17765
18025
  var SECONDS_PER_MONTH = 2592e3;
17766
18026
  var BYTES_PER_MB = 1e6;
17767
18027
  var BYTES_PER_GB = 1e9;
17768
- var PERCENTILE_FACTORS = { 50: 1, 90: 1.6, 95: 2, 99: 3, 99.9: 5 };
17769
18028
  var PERCENTILE_KEYS = { 50: "p50", 90: "p90", 95: "p95", 99: "p99", 99.9: "p999" };
17770
18029
  var PERCENTILES2 = Object.keys(PERCENTILE_KEYS).map(Number);
17771
18030
  function components(diagram) {
@@ -17806,7 +18065,9 @@ function forEachStepRate(diagram, traffic, visit) {
17806
18065
  const { rps, shares } = traffic.get(u.id) ?? { rps: 0, shares: [] };
17807
18066
  u.scenarios.forEach((s, i) => {
17808
18067
  const rate = rps * (shares[i] ?? 0);
17809
- if (rate > 0) for (const step of s.steps) visit(step, rate);
18068
+ if (rate > 0) {
18069
+ for (const step of s.steps) if (!step.failed) visit(step, rate);
18070
+ }
17810
18071
  });
17811
18072
  }
17812
18073
  }
@@ -17820,35 +18081,76 @@ function computeLoad(diagram, traffic) {
17820
18081
  });
17821
18082
  return load;
17822
18083
  }
17823
- var egressSource = (step, access) => access === "read" ? step.toServiceId : step.fromServiceId;
17824
- function computeEgressGb(diagram, traffic) {
18084
+ var payloadDirection = (step, access) => access === "read" ? { from: step.toServiceId, to: step.fromServiceId } : { from: step.fromServiceId, to: step.toServiceId };
18085
+ var OUTSIDE = (kind) => kind === "client" || kind === "external";
18086
+ function computeEgressGb(diagram, traffic, kindOf2 = (id) => profileOfId(diagram, id)) {
17825
18087
  const gb = /* @__PURE__ */ new Map();
17826
18088
  const access = accessIn(diagram);
17827
18089
  forEachStepRate(diagram, traffic, (step, rate) => {
17828
18090
  if (!step.sizeBytes || step.sizeBytes <= 0) return;
17829
- const source2 = egressSource(step, access(step));
17830
- gb.set(source2, (gb.get(source2) ?? 0) + rate * multiplierOf(step) * step.sizeBytes * SECONDS_PER_MONTH / BYTES_PER_GB);
18091
+ const { from, to } = payloadDirection(step, access(step));
18092
+ if (OUTSIDE(kindOf2(from)) || !OUTSIDE(kindOf2(to))) return;
18093
+ gb.set(from, (gb.get(from) ?? 0) + rate * multiplierOf(step) * step.sizeBytes * SECONDS_PER_MONTH / BYTES_PER_GB);
17831
18094
  });
17832
18095
  return gb;
17833
18096
  }
18097
+ var BANDWIDTH_BOUND = (kind) => !["client", "external", "dns", "other", "storage", "cdn"].includes(kind);
18098
+ function computeBytesPerSecond(diagram, traffic) {
18099
+ const bytes = /* @__PURE__ */ new Map();
18100
+ forEachStepRate(diagram, traffic, (step, rate) => {
18101
+ if (!step.sizeBytes || step.sizeBytes <= 0) return;
18102
+ const b = rate * multiplierOf(step) * step.sizeBytes;
18103
+ for (const id of /* @__PURE__ */ new Set([step.fromServiceId, step.toServiceId])) bytes.set(id, (bytes.get(id) ?? 0) + b);
18104
+ });
18105
+ return bytes;
18106
+ }
18107
+ function profileOfId(diagram, id) {
18108
+ const node = diagram.nodes.find((n) => n.id === id);
18109
+ return node ? profileOf(node).kind : "client";
18110
+ }
17834
18111
  function transferMs(step, bandwidthOf) {
17835
18112
  if (!step.sizeBytes || step.sizeBytes <= 0) return 0;
17836
18113
  const mbps = Math.min(bandwidthOf(step.fromServiceId), bandwidthOf(step.toServiceId));
17837
- return mbps > 0 && Number.isFinite(mbps) ? step.sizeBytes / BYTES_PER_MB / mbps * 1e3 : 0;
18114
+ return mbps > 0 && Number.isFinite(mbps) ? multiplierOf(step) * step.sizeBytes / BYTES_PER_MB / mbps * 1e3 : 0;
17838
18115
  }
17839
18116
  var utilizationOf = (load, capacity) => Number.isFinite(capacity) && capacity > 0 ? load / capacity : 0;
17840
- var hopLatency = (baseMs, utilization) => baseMs / (1 - Math.min(utilization, QUEUE_CAP));
18117
+ function erlangC(servers, rho) {
18118
+ if (rho <= 0) return 0;
18119
+ if (rho >= 1) return 1;
18120
+ const c = Math.max(1, Math.round(servers));
18121
+ const a = c * rho;
18122
+ let b = 1;
18123
+ for (let k = 1; k <= c; k++) b = a * b / (k + a * b);
18124
+ return b / (1 - rho * (1 - b));
18125
+ }
18126
+ function hopModel(baseMs, rho, servers = 1) {
18127
+ const r = Math.min(Math.max(rho, 0), QUEUE_CAP);
18128
+ const c = Math.max(1, Math.round(servers));
18129
+ const waitProbability = erlangC(c, r);
18130
+ const meanMs = baseMs + waitProbability * baseMs / (c * (1 - r));
18131
+ const fixedMs = (1 - SERVICE_SPREAD) * baseMs;
18132
+ return { fixedMs, tailMs: meanMs - fixedMs, waitProbability };
18133
+ }
18134
+ var hopMean = (h) => h.fixedMs + h.tailMs;
18135
+ var tailFactor = (p) => -Math.log(1 - p);
17841
18136
  var replicatedAvailability = (a, replicas) => 1 - (1 - a) ** replicas;
17842
- function usageOf(p, replicas, readLoadRps, writeLoadRps) {
18137
+ var primaryAvailability = (a, replicas) => replicas >= 2 ? 1 - (1 - a) * FAILOVER_SHARE : a;
18138
+ function usageOf(p, replicas, readLoadRps, writeLoadRps, bytesPerSecond = 0) {
17843
18139
  const { readRps: readCapacityRps, writeRps: writeCapacityRps } = capacityOf(p, replicas);
17844
18140
  const readUtilization = utilizationOf(readLoadRps, readCapacityRps);
17845
18141
  const writeUtilization = utilizationOf(writeLoadRps, writeCapacityRps);
17846
- const utilization = combinedUtilization(p, readUtilization, writeUtilization);
18142
+ const requestUtilization = combinedUtilization(p, readUtilization, writeUtilization);
17847
18143
  const loadRps = readLoadRps + writeLoadRps;
18144
+ const bound = BANDWIDTH_BOUND(p.kind);
18145
+ const bandwidthLoadMBps = bound ? bytesPerSecond / BYTES_PER_MB : 0;
18146
+ const bandwidthCapacityMBps = bound ? p.bandwidthMBps * replicas * p.shards : 0;
18147
+ const bandwidthUtilization = utilizationOf(bandwidthLoadMBps, bandwidthCapacityMBps);
18148
+ const utilization = Math.max(requestUtilization, bandwidthUtilization);
17848
18149
  return {
17849
18150
  loadRps,
17850
- capacityRps: readCapacityRps === writeCapacityRps || utilization === 0 ? readCapacityRps : loadRps / utilization,
18151
+ capacityRps: readCapacityRps === writeCapacityRps || requestUtilization === 0 ? readCapacityRps : loadRps / requestUtilization,
17851
18152
  utilization,
18153
+ requestUtilization,
17852
18154
  saturated: utilization >= 1,
17853
18155
  readLoadRps,
17854
18156
  writeLoadRps,
@@ -17856,27 +18158,68 @@ function usageOf(p, replicas, readLoadRps, writeLoadRps) {
17856
18158
  writeCapacityRps,
17857
18159
  readUtilization,
17858
18160
  writeUtilization,
18161
+ bandwidthLoadMBps,
18162
+ bandwidthCapacityMBps,
18163
+ bandwidthUtilization,
17859
18164
  writeScaling: p.writeScaling
17860
18165
  };
17861
18166
  }
18167
+ var bandwidthBound = (n) => n.bandwidthUtilization > n.requestUtilization;
18168
+ var serversOf = (n, replicas) => writeBound(n) ? 1 : replicas;
17862
18169
  function usageText(n) {
17863
18170
  const part = (load, capacity, rho) => `${formatRps(load)} of ${formatRps(capacity)}, ${formatPercent(rho)}`;
18171
+ if (bandwidthBound(n)) {
18172
+ const mb = (x) => `${Number(x.toPrecision(3)).toLocaleString("en-US")} MB/s`;
18173
+ return `bandwidth ${mb(n.bandwidthLoadMBps)} of ${mb(n.bandwidthCapacityMBps)}, ${formatPercent(n.bandwidthUtilization)}`;
18174
+ }
17864
18175
  if (n.writeScaling === "shards" && n.writeLoadRps > 0) {
17865
18176
  return `reads ${part(n.readLoadRps, n.readCapacityRps, n.readUtilization)}; writes ${part(n.writeLoadRps, n.writeCapacityRps, n.writeUtilization)}`;
17866
18177
  }
17867
- return part(n.loadRps, n.capacityRps, n.utilization);
18178
+ return part(n.loadRps, n.capacityRps, n.requestUtilization);
17868
18179
  }
17869
18180
  var writeBound = (n) => n.writeScaling === "shards" && n.writeUtilization >= n.readUtilization && n.writeLoadRps > 0;
17870
- function useCasePercentile(scenarios, q) {
17871
- const tail = 1 - q / 100 - 1e-9;
17872
- const heavy = scenarios.filter((s) => s.share >= tail && s.share > 0);
17873
- if (heavy.length) return Math.max(...heavy.map((s) => s.value));
17874
- let covered = 0;
17875
- for (const s of [...scenarios].sort((a, b) => b.value - a.value)) {
17876
- covered += s.share;
17877
- if (covered >= tail) return s.value;
17878
- }
17879
- return scenarios.length ? Math.max(...scenarios.map((s) => s.value)) : 0;
18181
+ function pathMean(path) {
18182
+ return path.parts.reduce((sum, part) => sum + Math.max(0, ...part.map((i) => i.fixedMs + i.tailMs)), 0);
18183
+ }
18184
+ function pathAt(path, l) {
18185
+ let sum = 0;
18186
+ for (const part of path.parts) {
18187
+ let most = 0;
18188
+ for (const i of part) most = Math.max(most, i.fixedMs + i.tailMs * l);
18189
+ sum += most;
18190
+ }
18191
+ return sum;
18192
+ }
18193
+ var pathQuantile = (path, p) => pathAt(path, tailFactor(p));
18194
+ function pathCdf(path, t) {
18195
+ if (pathAt(path, 0) > t) return 0;
18196
+ let hi = 1;
18197
+ while (pathAt(path, hi) <= t) {
18198
+ hi *= 2;
18199
+ if (hi > 1e6) return 1;
18200
+ }
18201
+ let lo = 0;
18202
+ for (let i = 0; i < 50; i++) {
18203
+ const mid = (lo + hi) / 2;
18204
+ if (pathAt(path, mid) <= t) lo = mid;
18205
+ else hi = mid;
18206
+ }
18207
+ return 1 - Math.exp(-lo);
18208
+ }
18209
+ function mixtureQuantile(paths, q) {
18210
+ const live = paths.filter((s) => s.share > 0);
18211
+ if (live.length === 0) return 0;
18212
+ if (live.length === 1) return pathQuantile(live[0].path, q);
18213
+ const total = live.reduce((sum, s) => sum + s.share, 0);
18214
+ const cdf = (t) => live.reduce((sum, s) => sum + s.share * pathCdf(s.path, t), 0) / total;
18215
+ let lo = Math.min(...live.map((s) => pathQuantile(s.path, 0)));
18216
+ let hi = Math.max(...live.map((s) => pathQuantile(s.path, q)));
18217
+ for (let i = 0; i < 60 && hi - lo > 1e-9 * Math.max(1, hi); i++) {
18218
+ const mid = (lo + hi) / 2;
18219
+ if (cdf(mid) >= q) hi = mid;
18220
+ else lo = mid;
18221
+ }
18222
+ return hi;
17880
18223
  }
17881
18224
  function mainScenarioIndex(shares) {
17882
18225
  let best = 0;
@@ -17893,20 +18236,27 @@ function profilesOf(diagram, warnings = []) {
17893
18236
  return new Map(nodes.map((n) => [n.id, profileOf(n, overrides.get(n.id))]));
17894
18237
  }
17895
18238
  function analyze(diagram, options = {}) {
17896
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
18239
+ const defaultTimeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
17897
18240
  const warnings = [];
17898
18241
  const nodes = components(diagram);
17899
18242
  const profiles = profilesOf(diagram, warnings);
18243
+ const kindOf2 = (id) => profiles.get(id)?.kind ?? "client";
17900
18244
  const traffic = resolveTraffic(diagram, warnings);
17901
18245
  const load = computeLoad(diagram, traffic);
17902
- const egress = computeEgressGb(diagram, traffic);
18246
+ const egress = computeEgressGb(diagram, traffic, kindOf2);
18247
+ const bytes = computeBytesPerSecond(diagram, traffic);
18248
+ const hops = /* @__PURE__ */ new Map();
17903
18249
  const nodeResults = nodes.map((n) => {
17904
18250
  const p = profiles.get(n.id);
17905
- const replicas = replicasOf(n);
18251
+ const replicas = Math.max(1, options.replicas?.get(n.id) ?? replicasOf(n));
17906
18252
  const { read, write } = load.get(n.id) ?? { read: 0, write: 0 };
17907
- const usage = usageOf(p, replicas, read, write);
18253
+ const usage = usageOf(p, replicas, read, write, bytes.get(n.id) ?? 0);
18254
+ const servers = serversOf(usage, replicas);
18255
+ const hop = hopModel(p.latencyMs, usage.utilization, servers);
18256
+ hops.set(n.id, hop);
17908
18257
  const egressGbPerMonth = egress.get(n.id) ?? 0;
17909
18258
  const egressUsd = egressGbPerMonth * p.egressUsdPerGb;
18259
+ const availability2 = replicatedAvailability(p.availability, replicas);
17910
18260
  return {
17911
18261
  id: n.id,
17912
18262
  kind: p.kind,
@@ -17914,8 +18264,11 @@ function analyze(diagram, options = {}) {
17914
18264
  shards: p.shards,
17915
18265
  ...usage,
17916
18266
  ...p.consistency ? { consistency: p.consistency } : {},
17917
- latencyMs: hopLatency(p.latencyMs, usage.utilization),
17918
- availability: replicatedAvailability(p.availability, replicas),
18267
+ servers,
18268
+ waitProbability: hop.waitProbability,
18269
+ latencyMs: hopMean(hop),
18270
+ availability: availability2,
18271
+ writeAvailability: p.writeScaling === "shards" ? primaryAvailability(p.availability, replicas) : availability2,
17919
18272
  costUsd: replicas * p.shards * p.costUsd + egressUsd,
17920
18273
  egressGbPerMonth,
17921
18274
  egressUsd,
@@ -17923,28 +18276,49 @@ function analyze(diagram, options = {}) {
17923
18276
  };
17924
18277
  });
17925
18278
  const nodeById = new Map(nodeResults.map((n) => [n.id, n]));
17926
- const hopMs = (id) => nodeById.get(id)?.latencyMs ?? 0;
17927
- const availabilityOf = (id) => nodeById.get(id)?.availability ?? 1;
17928
18279
  const bandwidthOf = (id) => profiles.get(id)?.bandwidthMBps ?? Infinity;
18280
+ const timeoutOf = (id) => profiles.get(id)?.timeoutMs ?? defaultTimeoutMs;
17929
18281
  for (const n of nodeResults) {
17930
18282
  const usage = usageText(n);
17931
18283
  if (n.saturated) warnings.push(`'${n.id}' is saturated: ${usage}. ${writeBound(n) ? "Add shards for writes" : "Add replicas"} or take load off it.`);
17932
18284
  else if (n.utilization > HOT) warnings.push(`'${n.id}' runs hot: ${usage}.`);
17933
18285
  }
18286
+ const access = accessIn(diagram);
18287
+ const pathOf = (hopsOfPath) => ({
18288
+ parts: hopsOfPath.map(
18289
+ (part) => part.map((h) => {
18290
+ if (h.failed) return { fixedMs: timeoutOf(h.target), tailMs: 0 };
18291
+ const hop = hops.get(h.target) ?? { fixedMs: 0, tailMs: 0 };
18292
+ return { fixedMs: transferMs(h.step, bandwidthOf) + hop.fixedMs, tailMs: hop.tailMs };
18293
+ })
18294
+ )
18295
+ });
17934
18296
  const useCases = diagram.useCases.map((u) => {
17935
18297
  const { rps, shares } = traffic.get(u.id);
17936
- const scenarios = u.scenarios.map((s, i) => {
17937
- const meanMs = criticalPath(u, s).reduce(
17938
- (sum, part) => sum + Math.max(...part.map((hop) => hop.failed ? timeoutMs : hopMs(hop.target) + transferMs(hop.step, bandwidthOf))),
17939
- 0
17940
- );
17941
- return { id: s.id, name: s.name, share: shares[i] ?? 0, meanMs, percentiles: percentilesOf(meanMs) };
17942
- });
17943
- const percentiles = Object.fromEntries(
17944
- PERCENTILES2.map((q) => [PERCENTILE_KEYS[q], useCasePercentile(scenarios.map((s) => ({ share: s.share, value: s.percentiles[PERCENTILE_KEYS[q]] })), q)])
18298
+ const paths = u.scenarios.map((s) => pathOf(criticalPath(u, s)));
18299
+ const scenarios = u.scenarios.map((s, i) => ({
18300
+ id: s.id,
18301
+ name: s.name,
18302
+ share: shares[i] ?? 0,
18303
+ meanMs: pathMean(paths[i]),
18304
+ percentiles: Object.fromEntries(PERCENTILES2.map((q) => [PERCENTILE_KEYS[q], pathQuantile(paths[i], q / 100)]))
18305
+ }));
18306
+ const weighted = paths.map((path, i) => ({ share: shares[i] ?? 0, path }));
18307
+ const percentiles = Object.fromEntries(PERCENTILES2.map((q) => [PERCENTILE_KEYS[q], mixtureQuantile(weighted, q / 100)]));
18308
+ const tailScenario = Object.fromEntries(
18309
+ PERCENTILES2.map((q) => {
18310
+ const t = percentiles[PERCENTILE_KEYS[q]];
18311
+ const above = weighted.map((s) => s.share > 0 ? s.share * (1 - pathCdf(s.path, t * (1 - 1e-9))) : -1);
18312
+ return [PERCENTILE_KEYS[q], Math.max(0, above.indexOf(Math.max(...above)))];
18313
+ })
17945
18314
  );
17946
18315
  if (rps > 0 && u.scenarios.length === 0) warnings.push(`"${u.name}" has traffic but no steps`);
17947
- return { id: u.id, name: u.name, rps, scenarios, percentiles, availability: useCaseAvailability(u, shares, availabilityOf) };
18316
+ const writes = mainWrites(u, shares, (step) => access(step) === "write");
18317
+ const availabilityOf = (id) => {
18318
+ const n = nodeById.get(id);
18319
+ return !n ? 1 : writes.has(id) ? n.writeAvailability : n.availability;
18320
+ };
18321
+ return { id: u.id, name: u.name, rps, scenarios, percentiles, tailScenario, availability: useCaseAvailability(u, shares, availabilityOf) };
17948
18322
  });
17949
18323
  return {
17950
18324
  nodes: nodeResults,
@@ -17955,8 +18329,10 @@ function analyze(diagram, options = {}) {
17955
18329
  warnings
17956
18330
  };
17957
18331
  }
17958
- function percentilesOf(meanMs) {
17959
- return Object.fromEntries(PERCENTILES2.map((q) => [PERCENTILE_KEYS[q], meanMs * PERCENTILE_FACTORS[q]]));
18332
+ function mainWrites(useCase, shares, isWrite) {
18333
+ if (useCase.scenarios.length === 0) return /* @__PURE__ */ new Set();
18334
+ const path = criticalPath(useCase, useCase.scenarios[mainScenarioIndex(shares)]).flat();
18335
+ return new Set(path.filter((h) => !h.failed && !h.async && isWrite(h.step)).map((h) => h.target));
17960
18336
  }
17961
18337
  function useCaseAvailability(useCase, shares, availabilityOf) {
17962
18338
  if (useCase.scenarios.length === 0) return 1;
@@ -17983,9 +18359,9 @@ function singlePointsOfFailure(diagram) {
17983
18359
  // ../frontend/src/sim/tests.ts
17984
18360
  var pass = (message) => ({ passed: true, message });
17985
18361
  var fail = (message, hint) => ({ passed: false, message, hint });
17986
- function runTests(diagram, analysis = analyze(diagram)) {
18362
+ function makeRun(diagram, analysis) {
17987
18363
  const access = accessIn(diagram);
17988
- const run2 = {
18364
+ return {
17989
18365
  diagram,
17990
18366
  analysis,
17991
18367
  nodes: new Map(analysis.nodes.map((n) => [n.id, n])),
@@ -17994,6 +18370,9 @@ function runTests(diagram, analysis = analyze(diagram)) {
17994
18370
  profiles: profilesOf(diagram),
17995
18371
  isWrite: (step) => access(step) === "write"
17996
18372
  };
18373
+ }
18374
+ function runTests(diagram, analysis = analyze(diagram)) {
18375
+ const run2 = makeRun(diagram, analysis);
17997
18376
  const results = (diagram.requirements ?? []).map((r, i) => {
17998
18377
  const { passed, message, hint } = requirement(run2, r);
17999
18378
  return { id: `req:${i + 1}`, name: requirementName(r), category: CATEGORY[r.kind], passed, message, hint, loc: r.loc };
@@ -18085,7 +18464,7 @@ function latency(run2, u, q, maxMs) {
18085
18464
  const value = `${key} of ${u.name} is ${formatMs(ms)}${result.rps > 0 ? "" : " with no traffic"}`;
18086
18465
  const message = `${value} (limit ${formatMs(maxMs)})`;
18087
18466
  if (ms < maxMs) return { ...pass(message), value };
18088
- const index = Math.max(0, result.scenarios.findIndex((s) => s.percentiles[key] === ms));
18467
+ const index = Math.min(result.tailScenario[key] ?? 0, u.scenarios.length - 1);
18089
18468
  const lead = u.scenarios.length > 1 ? `The "${u.scenarios[index].name}" path (${formatPercent(result.scenarios[index].share)} of traffic) sets ${key}. ` : "";
18090
18469
  return { ...fail(message, lead + latencyHint(run2, u, u.scenarios[index])), value };
18091
18470
  }
@@ -18104,6 +18483,10 @@ function scenarioLatency(run2, useCase, scenario, q, maxMs) {
18104
18483
  }
18105
18484
  function saturationHint(run2, n) {
18106
18485
  const label = nodeLabel(run2.diagram, n.id);
18486
+ if (bandwidthBound(n)) {
18487
+ const replicas2 = Math.ceil(n.bandwidthUtilization * n.replicas / HOT);
18488
+ return `${label} moves more payload bytes than its network carries. Send the bytes around it (clients upload to and download from object storage or a CDN directly, with presigned URLs), or add replicas: x${replicas2} keeps its bandwidth under ${formatPercent(HOT)}`;
18489
+ }
18107
18490
  if (writeBound(n)) {
18108
18491
  const perShard = n.writeCapacityRps / n.shards;
18109
18492
  const shards = Math.ceil(n.writeLoadRps / (perShard * HOT));
@@ -18115,11 +18498,12 @@ function saturationHint(run2, n) {
18115
18498
  }
18116
18499
  function latencyHint(run2, u, scenario) {
18117
18500
  const hops = criticalPath(u, scenario).flat();
18118
- const cost2 = (h) => h.failed ? DEFAULT_TIMEOUT_MS : run2.nodes.get(h.target)?.latencyMs ?? 0;
18501
+ const timeout = (id) => run2.profiles.get(id)?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
18502
+ const cost2 = (h) => h.failed ? timeout(h.target) : run2.nodes.get(h.target)?.latencyMs ?? 0;
18119
18503
  const slowest = hops.reduce((best, h) => !best || cost2(h) > cost2(best) ? h : best, void 0);
18120
18504
  if (!slowest) return "Shorten the synchronous path of the request.";
18121
18505
  const label = nodeLabel(run2.diagram, slowest.target);
18122
- if (slowest.failed) return `It waits ${formatMs(DEFAULT_TIMEOUT_MS)} for the failed call to ${label}; give that path a smaller share of traffic or avoid the call.`;
18506
+ if (slowest.failed) return `It waits ${formatMs(timeout(slowest.target))} for the failed call to ${label}; give that path a smaller share of traffic, avoid the call, or time out sooner (capacity { ${slowest.target} timeout 200ms }).`;
18123
18507
  const sized = hops.find((h) => (h.step.sizeBytes ?? 0) > 0);
18124
18508
  const node = run2.nodes.get(slowest.target);
18125
18509
  if (node && node.utilization > HOT) return `${label} runs at ${formatPercent(node.utilization)} and queues requests; add replicas to it.`;
@@ -18133,8 +18517,11 @@ function availability(run2, u, minPercent) {
18133
18517
  if (result.availability * 100 >= minPercent - 1e-9) return { ...pass(message), value };
18134
18518
  const shares = run2.shares.get(u.id) ?? [];
18135
18519
  const main = u.scenarios.length ? pathNodes(u, u.scenarios[mainScenarioIndex(shares)]) : [];
18136
- const weakest = main.filter((id) => fallbacksFor(u, id).length === 0).map((id) => run2.nodes.get(id)).filter((n) => !!n && n.availability < 1).sort((a, b) => a.availability - b.availability)[0];
18137
- const hint = weakest ? `The weakest link is ${nodeLabel(run2.diagram, weakest.id)} at ${formatAvailability(weakest.availability)}: add a replica (x${weakest.replicas + 1}) or a fallback scenario that calls it with -x and still completes` : "Add replicas to the nodes on the main path, or fallback scenarios that complete without them";
18520
+ const writes = mainWrites(u, shares, run2.isWrite);
18521
+ const effective = (n) => writes.has(n.id) ? n.writeAvailability : n.availability;
18522
+ const weakest = main.filter((id) => fallbacksFor(u, id).length === 0).map((id) => run2.nodes.get(id)).filter((n) => !!n && effective(n) < 1).sort((a, b) => effective(a) - effective(b))[0];
18523
+ const primaryBound = weakest && writes.has(weakest.id) && weakest.writeAvailability < weakest.availability;
18524
+ const hint = !weakest ? "Add replicas to the nodes on the main path, or fallback scenarios that complete without them" : primaryBound ? `The weakest link is writing to ${nodeLabel(run2.diagram, weakest.id)}: writes need its primary, ${formatAvailability(weakest.writeAvailability)} with failover${weakest.replicas < 2 ? " (none without a second replica; add one)" : ` (a replica takes over, but about ${formatPercent(FAILOVER_SHARE)} of each outage is lost)`}. Move the writes to a partitioned store, or add a fallback scenario that calls it with -x and still completes` : `The weakest link is ${nodeLabel(run2.diagram, weakest.id)} at ${formatAvailability(effective(weakest))}: add a replica (x${weakest.replicas + 1}) or a fallback scenario that calls it with -x and still completes`;
18138
18525
  return { ...fail(message, hint), value };
18139
18526
  }
18140
18527
  function writeGap(run2, u, s, accept) {
@@ -18192,20 +18579,34 @@ function survive(run2, target) {
18192
18579
  if (selected.length === 0) {
18193
18580
  return target === "any" ? pass("No nodes to lose") : fail(`No node matches ${what}`, "Use a node id, [Tech] or any <kind>");
18194
18581
  }
18582
+ const latencies = (run2.diagram.requirements ?? []).filter((r) => r.kind === "latency" && requirement(run2, r).passed);
18195
18583
  const failures = [];
18196
18584
  for (const node of selected) {
18197
18585
  const n = run2.nodes.get(node.id);
18198
18586
  const label = nodeLabel(run2.diagram, node.id);
18199
18587
  const replicas = replicasOf(node);
18200
18588
  if (replicas >= 2) {
18201
- const left = usageOf(run2.profiles.get(node.id), replicas - 1, n.readLoadRps, n.writeLoadRps);
18589
+ const degraded = makeRun(run2.diagram, analyze(run2.diagram, { replicas: /* @__PURE__ */ new Map([[node.id, replicas - 1]]) }));
18590
+ const left = degraded.nodes.get(node.id);
18591
+ const lost = n.shards > 1 ? `one of the ${replicas} replicas of a ${node.id} shard` : `one of ${replicas} ${node.id} replicas`;
18202
18592
  if (left.saturated) {
18593
+ const leaves = bandwidthBound(left) ? `leaves ${usageText(left)}` : n.shards > 1 ? `leaves that shard ${formatRps(left.capacityRps / n.shards)} for ${formatRps(n.loadRps / n.shards)}` : `leaves ${formatRps(left.capacityRps)} for ${formatRps(n.loadRps)}`;
18203
18594
  failures.push(
18204
18595
  fail(
18205
- `Losing one of ${replicas} ${node.id} replicas leaves ${formatRps(left.capacityRps)} for ${formatRps(n.loadRps)} (${formatPercent(left.utilization)})`,
18596
+ `Losing ${lost} ${leaves}${bandwidthBound(left) ? "" : ` (${formatPercent(left.utilization)})`}`,
18206
18597
  writeBound(left) ? saturationHint(run2, { ...n, ...left }) : `Add a replica to ${label} (x${replicas + 1}) so the rest carry the load`
18207
18598
  )
18208
18599
  );
18600
+ continue;
18601
+ }
18602
+ const broken = latencies.map((r) => requirement(degraded, r)).find((c) => !c.passed);
18603
+ if (broken) {
18604
+ failures.push(
18605
+ fail(
18606
+ `Losing ${lost} breaks a latency limit: ${broken.message}`,
18607
+ `Add a replica to ${label} (x${replicas + 1}) so the rest keep it under ${formatPercent(HOT)}, or take load off it`
18608
+ )
18609
+ );
18209
18610
  }
18210
18611
  continue;
18211
18612
  }
@@ -18467,16 +18868,16 @@ function startsAt(run2, a) {
18467
18868
  const ids = idsOf(run2, a.target);
18468
18869
  const what = selectorText(a.target);
18469
18870
  if (ids.size === 0) return noMatch(a.target);
18470
- const entries = u.scenarios.map((s) => s.steps[0]).filter((s) => !!s);
18471
- if (entries.length === 0) return fail(`${quote(u.name)} has no steps`, `Start "${u.name}" with a step sent by ${what}`);
18472
- const wrong = entries.find((s) => !ids.has(s.fromServiceId));
18871
+ const entries2 = u.scenarios.map((s) => s.steps[0]).filter((s) => !!s);
18872
+ if (entries2.length === 0) return fail(`${quote(u.name)} has no steps`, `Start "${u.name}" with a step sent by ${what}`);
18873
+ const wrong = entries2.find((s) => !ids.has(s.fromServiceId));
18473
18874
  if (wrong) {
18474
18875
  return fail(
18475
18876
  `${quote(u.name)} starts at ${nodeLabel(run2.diagram, wrong.fromServiceId)}, not ${what}: ${stepText(wrong)}`,
18476
18877
  `Make the first step of "${u.name}" a step sent by ${what}`
18477
18878
  );
18478
18879
  }
18479
- return pass(`${quote(u.name)} starts at ${nodeLabel(run2.diagram, entries[0].fromServiceId)}`);
18880
+ return pass(`${quote(u.name)} starts at ${nodeLabel(run2.diagram, entries2[0].fromServiceId)}`);
18480
18881
  }
18481
18882
  function statusMatches(status, wanted) {
18482
18883
  if (status === void 0) return false;
@@ -19444,7 +19845,7 @@ ${PROBLEM_USAGE}`);
19444
19845
  }
19445
19846
 
19446
19847
  // src/cli.ts
19447
- var VERSION = true ? "0.5.0" : "dev";
19848
+ var VERSION2 = true ? "0.7.0" : "dev";
19448
19849
  var USAGE = `Usage:
19449
19850
  proschi check [--strict] [--format text|github|json] [--openapi <node>=<spec>]... <file|dir>...
19450
19851
  proschi parse <file>
@@ -19506,7 +19907,7 @@ function format2(results, style) {
19506
19907
  function run(argv, out = console.log, err = console.error) {
19507
19908
  const [command, ...rest] = argv;
19508
19909
  if (command === "--version" || command === "-v") {
19509
- out(VERSION);
19910
+ out(VERSION2);
19510
19911
  return 0;
19511
19912
  }
19512
19913
  if (!command || command === "--help" || command === "-h" || command === "help") {