proschi 0.10.0 → 0.11.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
@@ -15353,7 +15353,8 @@ var componentCatalog = [
15353
15353
  { type: "database", techStack: "Database", category: "Generic Components", searchTerms: "database relational sql db", aliases: ["DB", "RDBMS", "SQL Database", "Relational Database", "SQL"] },
15354
15354
  { 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"] },
15355
15355
  { type: "cache", techStack: "Cache", category: "Generic Components", searchTerms: "cache in-memory key value", aliases: ["In-Memory Cache", "Distributed Cache"] },
15356
- { 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"] },
15356
+ { type: "queue", techStack: "Message Queue", category: "Generic Components", searchTerms: "queue message job task", aliases: ["Queue", "Job Queue", "Task Queue"] },
15357
+ { type: "broker", techStack: "Message Broker", category: "Generic Components", searchTerms: "message broker topic stream event bus log pub sub", aliases: ["Broker", "Event Bus", "Topic", "Event Stream", "Stream", "Event Log"] },
15357
15358
  { 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"] },
15358
15359
  { type: "cdn", techStack: "CDN", category: "Generic Components", searchTerms: "cdn content delivery network edge cache", kind: "cdn", aliases: ["Content Delivery Network", "Edge Cache"] },
15359
15360
  { 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"] },
@@ -15414,10 +15415,10 @@ var componentCatalog = [
15414
15415
  { type: "database", techStack: "AWS OpenSearch", category: "AWS Services", searchTerms: "aws opensearch elasticsearch search", kind: "search", aliases: ["Amazon OpenSearch", "OpenSearch Service", "AWS Elasticsearch"] },
15415
15416
  // AWS Messaging
15416
15417
  { type: "queue", techStack: "AWS SQS", category: "AWS Services", searchTerms: "aws sqs queue message", aliases: ["Amazon SQS", "Simple Queue Service"] },
15417
- { type: "queue", techStack: "AWS SNS", category: "AWS Services", searchTerms: "aws sns notification topic pubsub", aliases: ["SNS", "Amazon SNS", "Simple Notification Service"] },
15418
- { type: "queue", techStack: "AWS EventBridge", category: "AWS Services", searchTerms: "aws eventbridge event bus", aliases: ["EventBridge", "Amazon EventBridge"] },
15419
- { type: "queue", techStack: "AWS Kinesis", category: "AWS Services", searchTerms: "aws kinesis stream data", aliases: ["Kinesis", "Amazon Kinesis", "Kinesis Data Streams"] },
15420
- { type: "queue", techStack: "AWS MSK", category: "AWS Services", searchTerms: "aws msk managed kafka stream", aliases: ["MSK", "Amazon MSK", "Managed Streaming for Kafka"] },
15418
+ { type: "broker", techStack: "AWS SNS", category: "AWS Services", searchTerms: "aws sns notification topic pubsub", aliases: ["SNS", "Amazon SNS", "Simple Notification Service"] },
15419
+ { type: "broker", techStack: "AWS EventBridge", category: "AWS Services", searchTerms: "aws eventbridge event bus", aliases: ["EventBridge", "Amazon EventBridge"] },
15420
+ { type: "broker", techStack: "AWS Kinesis", category: "AWS Services", searchTerms: "aws kinesis stream data", aliases: ["Kinesis", "Amazon Kinesis", "Kinesis Data Streams"] },
15421
+ { type: "broker", techStack: "AWS MSK", category: "AWS Services", searchTerms: "aws msk managed kafka stream", aliases: ["MSK", "Amazon MSK", "Managed Streaming for Kafka"] },
15421
15422
  // AWS CDN/Network
15422
15423
  { type: "cdn", techStack: "AWS CloudFront", category: "AWS Services", searchTerms: "aws cloudfront cdn edge", kind: "cdn", aliases: ["CloudFront", "Amazon CloudFront"] },
15423
15424
  { type: "cdn", techStack: "AWS Route53", category: "AWS Services", searchTerms: "aws route53 dns domain", kind: "dns", aliases: ["Route53", "Amazon Route 53"] },
@@ -15445,7 +15446,7 @@ var componentCatalog = [
15445
15446
  { type: "database", techStack: "GCP BigQuery", category: "GCP Services", searchTerms: "gcp google bigquery data warehouse analytics", kind: "analytics", aliases: ["BigQuery", "Google BigQuery"] },
15446
15447
  { type: "cache", techStack: "GCP Memorystore", category: "GCP Services", searchTerms: "gcp google memorystore redis memcached cache", aliases: ["Memorystore"] },
15447
15448
  // GCP Messaging
15448
- { 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"] },
15449
+ { type: "broker", techStack: "GCP Pub/Sub", category: "GCP Services", searchTerms: "gcp google pubsub pub/sub message queue", aliases: ["Pub/Sub", "Google Pub/Sub", "Cloud Pub/Sub"] },
15449
15450
  // GCP CDN/Network
15450
15451
  { type: "cdn", techStack: "GCP Cloud CDN", category: "GCP Services", searchTerms: "gcp google cloud cdn edge", kind: "cdn", aliases: ["Cloud CDN", "Google Cloud CDN"] },
15451
15452
  { type: "cdn", techStack: "GCP Cloud DNS", category: "GCP Services", searchTerms: "gcp google cloud dns domain", kind: "dns", aliases: ["Cloud DNS", "Google Cloud DNS"] },
@@ -15472,9 +15473,9 @@ var componentCatalog = [
15472
15473
  { type: "cache", techStack: "Azure Cache for Redis", category: "Azure Services", searchTerms: "azure redis cache", aliases: ["Azure Redis"] },
15473
15474
  { type: "database", techStack: "Azure AI Search", category: "Azure Services", searchTerms: "azure ai cognitive search", kind: "search", aliases: ["Azure Cognitive Search", "Azure Search"] },
15474
15475
  // Azure Messaging
15475
- { type: "queue", techStack: "Azure Service Bus", category: "Azure Services", searchTerms: "azure service bus queue message", aliases: ["Service Bus"] },
15476
- { type: "queue", techStack: "Azure Event Hubs", category: "Azure Services", searchTerms: "azure event hubs stream", aliases: ["Event Hubs", "Event Hub"] },
15477
- { type: "queue", techStack: "Azure Event Grid", category: "Azure Services", searchTerms: "azure event grid", aliases: ["Event Grid"] },
15476
+ { type: "broker", techStack: "Azure Service Bus", category: "Azure Services", searchTerms: "azure service bus queue message", aliases: ["Service Bus"] },
15477
+ { type: "broker", techStack: "Azure Event Hubs", category: "Azure Services", searchTerms: "azure event hubs stream", aliases: ["Event Hubs", "Event Hub"] },
15478
+ { type: "broker", techStack: "Azure Event Grid", category: "Azure Services", searchTerms: "azure event grid", aliases: ["Event Grid"] },
15478
15479
  { type: "queue", techStack: "Azure Queue Storage", category: "Azure Services", searchTerms: "azure queue storage message", aliases: ["Azure Queue", "Azure Queues"] },
15479
15480
  // Azure CDN/Network
15480
15481
  { type: "cdn", techStack: "Azure CDN", category: "Azure Services", searchTerms: "azure cdn edge", kind: "cdn" },
@@ -15514,16 +15515,16 @@ var componentCatalog = [
15514
15515
  { type: "cache", techStack: "Valkey", category: "Cache & In-Memory", searchTerms: "valkey redis cache memory" },
15515
15516
  { type: "cache", techStack: "KeyDB", category: "Cache & In-Memory", searchTerms: "keydb redis cache memory" },
15516
15517
  { type: "cache", techStack: "Dragonfly", category: "Cache & In-Memory", searchTerms: "dragonfly dragonflydb redis cache memory", aliases: ["DragonflyDB"] },
15517
- // Message Queues
15518
- { type: "queue", techStack: "Kafka", category: "Message Queues", searchTerms: "kafka message queue stream", aliases: ["Apache Kafka", "Confluent", "Confluent Kafka", "Kafka Topic"] },
15519
- { type: "queue", techStack: "RabbitMQ", category: "Message Queues", searchTerms: "rabbitmq message queue amqp", aliases: ["Rabbit", "AMQP"] },
15520
- { type: "queue", techStack: "SQS", category: "Message Queues", searchTerms: "sqs queue aws message" },
15521
- { type: "queue", techStack: "Redis Queue", category: "Message Queues", searchTerms: "redis queue message", aliases: ["Redis Streams", "Redis Pub/Sub"] },
15522
- { type: "queue", techStack: "NATS", category: "Message Queues", searchTerms: "nats message queue", aliases: ["NATS JetStream", "JetStream"] },
15523
- { type: "queue", techStack: "Apache Pulsar", category: "Message Queues", searchTerms: "pulsar message queue stream", aliases: ["Pulsar"] },
15524
- { type: "queue", techStack: "ActiveMQ", category: "Message Queues", searchTerms: "activemq message queue", aliases: ["Apache ActiveMQ", "Amazon MQ"] },
15525
- { type: "queue", techStack: "ZeroMQ", category: "Message Queues", searchTerms: "zeromq zmq message queue", aliases: ["ZMQ", "0MQ"] },
15526
- { type: "queue", techStack: "Redpanda", category: "Message Queues", searchTerms: "redpanda kafka stream queue" },
15518
+ // Queues & Brokers: a queue hands each message to one consumer; a broker routes or logs it for many.
15519
+ { type: "broker", techStack: "Kafka", category: "Queues & Brokers", searchTerms: "kafka message broker event stream log topic", aliases: ["Apache Kafka", "Confluent", "Confluent Kafka", "Kafka Topic"] },
15520
+ { type: "broker", techStack: "RabbitMQ", category: "Queues & Brokers", searchTerms: "rabbitmq message broker queue amqp", aliases: ["Rabbit", "AMQP"] },
15521
+ { type: "queue", techStack: "SQS", category: "Queues & Brokers", searchTerms: "sqs queue aws message" },
15522
+ { type: "queue", techStack: "Redis Queue", category: "Queues & Brokers", searchTerms: "redis queue message", aliases: ["Redis Streams", "Redis Pub/Sub"] },
15523
+ { type: "broker", techStack: "NATS", category: "Queues & Brokers", searchTerms: "nats message broker stream", aliases: ["NATS JetStream", "JetStream"] },
15524
+ { type: "broker", techStack: "Apache Pulsar", category: "Queues & Brokers", searchTerms: "pulsar message broker stream", aliases: ["Pulsar"] },
15525
+ { type: "broker", techStack: "ActiveMQ", category: "Queues & Brokers", searchTerms: "activemq message broker queue", aliases: ["Apache ActiveMQ", "Amazon MQ"] },
15526
+ { type: "queue", techStack: "ZeroMQ", category: "Queues & Brokers", searchTerms: "zeromq zmq message queue", aliases: ["ZMQ", "0MQ"] },
15527
+ { type: "broker", techStack: "Redpanda", category: "Queues & Brokers", searchTerms: "redpanda kafka message broker stream" },
15527
15528
  // Search
15528
15529
  { type: "database", techStack: "OpenSearch", category: "Search", searchTerms: "opensearch elasticsearch search", kind: "search" },
15529
15530
  { type: "database", techStack: "Solr", category: "Search", searchTerms: "solr lucene search", kind: "search", aliases: ["Apache Solr"] },
@@ -15639,6 +15640,7 @@ var IDENT_PART = /[A-Za-z0-9_]/;
15639
15640
  var TEAM_PART = /[A-Za-z0-9_-]/;
15640
15641
  var DIGIT = /[0-9]/;
15641
15642
  var LETTER = /[A-Za-z]/;
15643
+ var HEX_COLOR = /^#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})(?![A-Za-z0-9_])/;
15642
15644
  var SEPARATE_UNIT = /^[ \t]+(rps|rpm|rpd|ms|s|%|usd\/month|usd\/GB|MB\/s|GB\/s)(?![A-Za-z0-9_/])/;
15643
15645
  function tokenizeLine(text, line) {
15644
15646
  const tokens = [];
@@ -15651,7 +15653,14 @@ function tokenizeLine(text, line) {
15651
15653
  i++;
15652
15654
  continue;
15653
15655
  }
15654
- if (ch === "#") break;
15656
+ if (ch === "#") {
15657
+ const prev = tokens[tokens.length - 1];
15658
+ const hex = prev?.kind === "ident" && prev.value === "color" ? HEX_COLOR.exec(text.slice(i)) : null;
15659
+ if (!hex) break;
15660
+ tokens.push({ kind: "hex", value: hex[0], col: i + 1, length: hex[0].length });
15661
+ i += hex[0].length;
15662
+ continue;
15663
+ }
15655
15664
  const start = i;
15656
15665
  if (ch === ":") {
15657
15666
  const raw = stripTrailingComment(text.slice(i + 1));
@@ -15717,6 +15726,12 @@ function tokenizeLine(text, line) {
15717
15726
  tokens.push({ kind: "ident", value: text.slice(start, i), col: start + 1, length: i - start });
15718
15727
  continue;
15719
15728
  }
15729
+ if (ch === "." && IDENT_START.test(text[i + 1] ?? "")) {
15730
+ i++;
15731
+ while (i < text.length && IDENT_PART.test(text[i])) i++;
15732
+ tokens.push({ kind: "tag", value: text.slice(start + 1, i), col: start + 1, length: i - start });
15733
+ continue;
15734
+ }
15720
15735
  if (ch === "{" || ch === "}" || ch === ",") {
15721
15736
  const kind = ch === "{" ? "lbrace" : ch === "}" ? "rbrace" : "comma";
15722
15737
  tokens.push({ kind, value: ch, col: start + 1, length: 1 });
@@ -15858,10 +15873,12 @@ function classify(lines) {
15858
15873
  entries2.push({ ...base, kind: "cells", section: section2, cells });
15859
15874
  continue;
15860
15875
  }
15861
- if (!inSection && first.kind === "ident" && second?.kind === "arrow" && third?.kind === "ident" && tokens.length <= 4 && (!fourth || fourth.kind === "label")) {
15862
- const entry = { ...base, kind: "connection", from: first.value, arrow: second.value, to: third.value, label: fourth?.value, continuation: [] };
15876
+ const tag = fourth?.kind === "tag" ? fourth : void 0;
15877
+ const labelToken = tokens[tag ? 4 : 3];
15878
+ if (!inSection && first.kind === "ident" && second?.kind === "arrow" && third?.kind === "ident" && tokens.length <= (tag ? 5 : 4) && (!labelToken || labelToken.kind === "label")) {
15879
+ const entry = { ...base, kind: "connection", from: first.value, arrow: second.value, to: third.value, tag: tag?.value, label: labelToken?.value, continuation: [] };
15863
15880
  const brackets = new BracketCounter();
15864
- if (fourth && brackets.feed(fourth.value) > 0) {
15881
+ if (labelToken && brackets.feed(labelToken.value) > 0) {
15865
15882
  const delta = indent(depth).length - width(leadingSpace(raw));
15866
15883
  while (i + 1 < lines.length) {
15867
15884
  const next = lines[++i];
@@ -15874,7 +15891,7 @@ function classify(lines) {
15874
15891
  continue;
15875
15892
  }
15876
15893
  const oneLineDecision = first.value === "decision" && second?.kind === "string" && third?.kind === "ident" && third.value === "because";
15877
- if (!inSection && first.kind === "ident" && !KEYWORDS.has(first.value) && !oneLineDecision && tokens.slice(1).every((t) => ["string", "tech", "team", "ident", "number", "comma"].includes(t.kind))) {
15894
+ if (!inSection && first.kind === "ident" && !KEYWORDS.has(first.value) && !oneLineDecision && !isLayoutHint(tokens) && tokens.slice(1).every((t) => ["string", "tech", "team", "ident", "number", "comma", "tag", "hex"].includes(t.kind))) {
15878
15895
  let k = 1;
15879
15896
  const take = (kind) => tokens[k]?.kind === kind ? source(raw, tokens[k++]) : "";
15880
15897
  const cells2 = [first.value, take("string"), take("tech"), take("team"), join(raw, tokens.slice(k))];
@@ -15892,6 +15909,10 @@ function classify(lines) {
15892
15909
  }
15893
15910
  return entries2;
15894
15911
  }
15912
+ function isLayoutHint(tokens) {
15913
+ const [first, second] = tokens;
15914
+ return tokens.length === 2 && (first.value === "direction" || first.value === "layout") && second.kind === "ident" && !["pos", "critical", "color", "rank"].includes(second.value) && !/^x\d+$/.test(second.value);
15915
+ }
15895
15916
  function sectionOpenedBy(tokens) {
15896
15917
  const [first, second] = tokens;
15897
15918
  if (first.kind !== "ident" || tokens[tokens.length - 1].kind !== "lbrace") return void 0;
@@ -15966,12 +15987,13 @@ function alignCells(run2) {
15966
15987
  }
15967
15988
  function alignConnections(run2) {
15968
15989
  const head = Math.max(...run2.map((e) => len(e.from) + 1 + e.arrow.length));
15990
+ const end = (e) => e.tag ? `${e.to} .${e.tag}` : e.to;
15969
15991
  const labelled = run2.filter((e) => e.label !== void 0);
15970
- const target = labelled.length ? Math.max(...labelled.map((e) => len(e.to))) : 0;
15992
+ const target = labelled.length ? Math.max(...labelled.map((e) => len(end(e)))) : 0;
15971
15993
  return run2.map((e) => {
15972
15994
  const start = e.from + " ".repeat(head - len(e.from) - e.arrow.length) + e.arrow + " ";
15973
- if (e.label === void 0) return start + e.to;
15974
- return start + pad(e.to, target) + " :" + (e.label ? " " + e.label : "");
15995
+ if (e.label === void 0) return start + end(e);
15996
+ return start + pad(end(e), target) + " :" + (e.label ? " " + e.label : "");
15975
15997
  });
15976
15998
  }
15977
15999
  function commentOf(raw, tokens) {
@@ -16013,8 +16035,8 @@ var len = (s) => [...s].length;
16013
16035
  var pad = (s, to) => s + " ".repeat(Math.max(0, to - len(s)));
16014
16036
 
16015
16037
  // ../frontend/src/dsl/kinds.ts
16016
- var KINDS = ["client", "edge", "cdn", "loadbalancer", "gateway", "dns", "service", "function", "cache", "database", "search", "analytics", "queue", "storage", "external", "other"];
16017
- var DATA_STORE_KINDS = ["cache", "database", "search", "analytics", "queue", "storage"];
16038
+ var KINDS = ["client", "edge", "cdn", "loadbalancer", "gateway", "dns", "service", "function", "cache", "database", "search", "analytics", "queue", "broker", "storage", "external", "other"];
16039
+ var DATA_STORE_KINDS = ["cache", "database", "search", "analytics", "queue", "broker", "storage"];
16018
16040
  var EDGE_KINDS = ["edge", "cdn", "loadbalancer", "gateway", "dns"];
16019
16041
  var BY_TECH = new Map(componentCatalog.flatMap((c) => "kind" in c ? [[c.techStack, c.kind]] : []));
16020
16042
  var BY_TYPE = {
@@ -16027,6 +16049,7 @@ var BY_TYPE = {
16027
16049
  cache: "cache",
16028
16050
  database: "database",
16029
16051
  queue: "queue",
16052
+ broker: "broker",
16030
16053
  storage: "storage",
16031
16054
  external: "external"
16032
16055
  };
@@ -16048,6 +16071,7 @@ var TYPE_OF_KIND = {
16048
16071
  search: "database",
16049
16072
  analytics: "database",
16050
16073
  queue: "queue",
16074
+ broker: "broker",
16051
16075
  storage: "storage",
16052
16076
  external: "external",
16053
16077
  other: "shape"
@@ -16060,7 +16084,8 @@ var KIND_WORDS = [
16060
16084
  [/gateway|\bapim?\b.*manage/, "gateway"],
16061
16085
  [/\bwaf\b|firewall/, "edge"],
16062
16086
  [/cache|memcache/, "cache"],
16063
- [/queue|\bmq\b|mq$|broker|event.?stream|\bbus\b|topic|pub.?sub|event.?hub|kafka/, "queue"],
16087
+ [/broker|event.?stream|event.?log|\bbus\b|topic|pub.?sub|event.?hub|kafka|kinesis|pulsar|redpanda|jetstream|\bnats\b/, "broker"],
16088
+ [/queue|\bmq\b|mq$/, "queue"],
16064
16089
  [/search|lucene|\bindex\b/, "search"],
16065
16090
  [/warehouse|analytic|olap|data.?lake|lakehouse|time.?series|\btsdb\b/, "analytics"],
16066
16091
  [/bucket|blob|object.?stor|storage|\bfs\b|file.?system|\bnas\b|\bdisk\b|volume/, "storage"],
@@ -16081,7 +16106,7 @@ function isEdge(kind) {
16081
16106
  return EDGE_KINDS.includes(kind);
16082
16107
  }
16083
16108
  function kindMatches(actual, wanted) {
16084
- return actual === wanted || wanted === "edge" && isEdge(actual);
16109
+ return actual === wanted || wanted === "edge" && isEdge(actual) || wanted === "queue" && actual === "broker";
16085
16110
  }
16086
16111
 
16087
16112
  // ../frontend/src/dsl/paths.ts
@@ -16206,12 +16231,56 @@ function accessOf(httpMethod, label) {
16206
16231
  return word && WRITE_VERBS.includes(word.toUpperCase()) ? "write" : "read";
16207
16232
  }
16208
16233
 
16234
+ // ../frontend/src/dsl/palette.ts
16235
+ var PALETTE = {
16236
+ gray: "#6b7280",
16237
+ blue: "#3b82f6",
16238
+ teal: "#14b8a6",
16239
+ green: "#22c55e",
16240
+ yellow: "#eab308",
16241
+ orange: "#f97316",
16242
+ red: "#ef4444",
16243
+ purple: "#a855f7",
16244
+ pink: "#ec4899"
16245
+ };
16246
+ var COLOR_NAMES = Object.keys(PALETTE);
16247
+ var HEX = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
16248
+ function resolveColor(text) {
16249
+ if (Object.prototype.hasOwnProperty.call(PALETTE, text)) return PALETTE[text];
16250
+ if (!HEX.test(text)) return void 0;
16251
+ const digits = text.slice(1).toLowerCase();
16252
+ return "#" + (digits.length === 3 ? [...digits].map((d) => d + d).join("") : digits);
16253
+ }
16254
+ function suggestColor(text) {
16255
+ const word = text.toLowerCase();
16256
+ let best;
16257
+ for (const name of COLOR_NAMES) {
16258
+ const distance = editDistance2(word, name);
16259
+ if (distance <= 2 && (!best || distance < best.distance)) best = { name, distance };
16260
+ }
16261
+ return best?.name;
16262
+ }
16263
+ function editDistance2(a, b) {
16264
+ const row = Array.from({ length: b.length + 1 }, (_, i) => i);
16265
+ for (let i = 1; i <= a.length; i++) {
16266
+ let prev = row[0];
16267
+ row[0] = i;
16268
+ for (let j = 1; j <= b.length; j++) {
16269
+ const next = row[j];
16270
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
16271
+ prev = next;
16272
+ }
16273
+ }
16274
+ return row[b.length];
16275
+ }
16276
+
16209
16277
  // ../frontend/src/dsl/parser.ts
16210
16278
  var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
16211
16279
  var DEFAULT_TECH = "Rectangle";
16212
16280
  var DEFAULT_GROUP_TECH = "Logical Group";
16213
16281
  var MAX_SCENARIOS = 32;
16214
16282
  var MAX_ALT_DEPTH = 16;
16283
+ var DIRECTIONS = ["down", "right", "up", "left"];
16215
16284
  var REPLICAS = /^x(\d+)$/;
16216
16285
  var PERCENTILES = { p50: 50, p90: 90, p95: 95, p99: 99, p999: 99.9 };
16217
16286
  var FIELD_FLAGS = ["key", "index", "unique", "optional"];
@@ -16240,6 +16309,7 @@ var GENERIC_TECH = {
16240
16309
  search: "Search Engine",
16241
16310
  analytics: "Data Warehouse",
16242
16311
  queue: "Message Queue",
16312
+ broker: "Message Broker",
16243
16313
  storage: "Object Storage",
16244
16314
  external: "Third Party API",
16245
16315
  other: "Note"
@@ -16284,6 +16354,12 @@ var Parser = class {
16284
16354
  decisions = [];
16285
16355
  tests = [];
16286
16356
  blocks = [];
16357
+ legend = [];
16358
+ layout = {};
16359
+ /** Where each top-level layout hint was set, for the duplicate warning. */
16360
+ layoutLocs = {};
16361
+ /** `.tag` uses, resolved against the legend once every file is read. */
16362
+ tagRefs = [];
16287
16363
  /** Checks of use case, scenario and node names, run once every file is read and use cases are built. */
16288
16364
  deferred = [];
16289
16365
  built = [];
@@ -16297,6 +16373,7 @@ var Parser = class {
16297
16373
  run() {
16298
16374
  this.parseFile();
16299
16375
  this.createImplicitNodes();
16376
+ this.resolveTags();
16300
16377
  for (const e of this.edges) this.connected.add(`${e.source}|${e.target}`).add(`${e.target}|${e.source}`);
16301
16378
  const useCases = this.useCases.map(({ useCase, body }) => this.buildUseCase(useCase, body));
16302
16379
  const endpoints = useCases.flatMap((u) => u.endpoint ? [u.endpoint] : []);
@@ -16315,7 +16392,9 @@ var Parser = class {
16315
16392
  ...this.capacity.length ? { capacity: this.capacity } : {},
16316
16393
  ...this.entities.length ? { entities: this.entities } : {},
16317
16394
  ...this.decisions.length ? { decisions: this.decisions } : {},
16318
- ...this.tests.length ? { tests: this.tests } : {}
16395
+ ...this.tests.length ? { tests: this.tests } : {},
16396
+ ...this.legend.length ? { legend: this.legend } : {},
16397
+ ...Object.keys(this.layout).length ? { layout: this.layout } : {}
16319
16398
  };
16320
16399
  const order = (d) => this.fileOrder.get(d.file) ?? 0;
16321
16400
  this.diagnostics.sort((a, b) => order(a) - order(b) || a.line - b.line || a.col - b.col);
@@ -16404,6 +16483,7 @@ var Parser = class {
16404
16483
  case "traffic":
16405
16484
  case "requirements":
16406
16485
  case "capacity":
16486
+ case "legend":
16407
16487
  if (second?.kind === "lbrace") this.openSection(first.value, tokens, line);
16408
16488
  else this.parseNode(tokens, line);
16409
16489
  break;
@@ -16420,6 +16500,15 @@ var Parser = class {
16420
16500
  if (second?.kind === "string" && tokens[tokens.length - 1].kind === "lbrace") this.parseTest(tokens, line);
16421
16501
  else this.parseNode(tokens, line);
16422
16502
  break;
16503
+ // Keywords only as `direction <word>` and `layout <word>`, so a node may still be called direction or layout.
16504
+ case "direction":
16505
+ if (this.isHint(tokens)) this.parseDirection(tokens, line);
16506
+ else this.parseNode(tokens, line);
16507
+ break;
16508
+ case "layout":
16509
+ if (this.isHint(tokens)) this.parseLayoutMode(tokens, line);
16510
+ else this.parseNode(tokens, line);
16511
+ break;
16423
16512
  default:
16424
16513
  this.parseNode(tokens, line);
16425
16514
  }
@@ -16438,6 +16527,86 @@ var Parser = class {
16438
16527
  }
16439
16528
  this.expectEnd(tokens, summary ? 3 : 2, line);
16440
16529
  }
16530
+ /** `direction right`, `layout ordered`: a keyword and one plain word that is not a node part. */
16531
+ isHint(tokens) {
16532
+ const second = tokens[1];
16533
+ return tokens.length === 2 && second.kind === "ident" && !["pos", "critical", "color", "rank"].includes(second.value) && !REPLICAS.test(second.value);
16534
+ }
16535
+ /** `direction down|right|up|left`: at the top level for the whole diagram, inside a group for its members. */
16536
+ parseDirection(tokens, line) {
16537
+ const [keyword, value] = tokens;
16538
+ const loc = this.locOf(value, line);
16539
+ if (!DIRECTIONS.includes(value.value)) {
16540
+ this.error(`Unknown direction '${value.value}'; use direction down, right, up or left`, loc);
16541
+ return;
16542
+ }
16543
+ const direction = value.value;
16544
+ const top = this.top();
16545
+ if (top?.kind === "group") {
16546
+ const group = this.nodes.get(top.id);
16547
+ if (!group || group.loc !== top.loc) return;
16548
+ if (group.direction) this.warning(`Group '${group.id}' already has direction ${group.direction}; this one is ignored`, this.locOf(keyword, line));
16549
+ else group.direction = direction;
16550
+ return;
16551
+ }
16552
+ if (top) {
16553
+ this.error("direction is only allowed at the top level or inside a group", this.locOf(keyword, line));
16554
+ return;
16555
+ }
16556
+ if (this.file !== void 0) return;
16557
+ if (this.layoutLocs.direction) {
16558
+ this.warning(`direction is already set ${this.where(this.layoutLocs.direction, loc)}; this one is ignored`, this.locOf(keyword, line));
16559
+ return;
16560
+ }
16561
+ this.layout.direction = direction;
16562
+ this.layoutLocs.direction = loc;
16563
+ }
16564
+ /** `layout ordered`: lay edges out by declaration order. */
16565
+ parseLayoutMode(tokens, line) {
16566
+ const [keyword, value] = tokens;
16567
+ const loc = this.locOf(value, line);
16568
+ if (value.value !== "ordered") {
16569
+ this.error(`Unknown layout mode '${value.value}'; the only one is layout ordered`, loc);
16570
+ return;
16571
+ }
16572
+ if (this.top()) {
16573
+ this.error("layout ordered applies to the whole diagram; write it at the top level", this.locOf(keyword, line));
16574
+ return;
16575
+ }
16576
+ if (this.file !== void 0) return;
16577
+ if (this.layoutLocs.ordered) {
16578
+ this.warning(`layout ordered is already set ${this.where(this.layoutLocs.ordered, loc)}`, this.locOf(keyword, line));
16579
+ return;
16580
+ }
16581
+ this.layout.ordered = true;
16582
+ this.layoutLocs.ordered = loc;
16583
+ }
16584
+ /**
16585
+ * `rank 2`, `rank first`, `rank last` at `i` in a node or group header.
16586
+ * Returns how many tokens it used (0 when the token is something else), or
16587
+ * undefined after reporting an error.
16588
+ */
16589
+ readRank(tokens, i, line, current) {
16590
+ const t = tokens[i];
16591
+ if (!isWord(t, "rank")) return 0;
16592
+ const value = tokens[i + 1];
16593
+ const help = "a layer number from 0 (rank 0, rank 1, \u2026), rank first or rank last";
16594
+ if (current.rank !== void 0) {
16595
+ this.error(`A node takes one rank; rank ${current.rank} is already set`, this.locOf(t, line));
16596
+ return void 0;
16597
+ }
16598
+ if (value?.kind === "number" && /^\d+$/.test(value.value)) {
16599
+ current.rank = Number(value.value);
16600
+ return 2;
16601
+ }
16602
+ if (value?.kind === "ident" && (value.value === "first" || value.value === "last")) {
16603
+ current.rank = value.value;
16604
+ return 2;
16605
+ }
16606
+ const numeric = value?.kind === "number" || value?.kind === "quantity";
16607
+ this.error(numeric ? `'${value.value}' is not a layer number; use ${help}` : `Expected ${help} after rank`, this.locOf(value ?? t, line));
16608
+ return void 0;
16609
+ }
16441
16610
  parseImport(tokens, line) {
16442
16611
  const keyword = tokens[0];
16443
16612
  if (this.stack.length > 0) {
@@ -16511,14 +16680,29 @@ var Parser = class {
16511
16680
  else this.warning(`'${tech.value}' is not a group style; using ${DEFAULT_GROUP_TECH}`, this.locOf(tech, line));
16512
16681
  }
16513
16682
  let position;
16514
- if (tokens[i]?.kind === "ident" && tokens[i].value === "pos") {
16515
- const [x, comma, y] = [tokens[i + 1], tokens[i + 2], tokens[i + 3]];
16516
- if (x?.kind !== "number" || comma?.kind !== "comma" || y?.kind !== "number") {
16517
- this.error("Expected pos x,y", this.locOf(tokens[i], line));
16518
- return;
16683
+ const style = {};
16684
+ const ranked = {};
16685
+ while (i < tokens.length && tokens[i].kind !== "lbrace") {
16686
+ const rankUsed = this.readRank(tokens, i, line, ranked);
16687
+ if (rankUsed === void 0) return;
16688
+ if (rankUsed) {
16689
+ i += rankUsed;
16690
+ continue;
16519
16691
  }
16520
- position = { x: Number(x.value), y: Number(y.value) };
16521
- i += 4;
16692
+ if (tokens[i].kind === "ident" && tokens[i].value === "pos") {
16693
+ const [x, comma, y] = [tokens[i + 1], tokens[i + 2], tokens[i + 3]];
16694
+ if (x?.kind !== "number" || comma?.kind !== "comma" || y?.kind !== "number") {
16695
+ this.error("Expected pos x,y", this.locOf(tokens[i], line));
16696
+ return;
16697
+ }
16698
+ position = { x: Number(x.value), y: Number(y.value) };
16699
+ i += 4;
16700
+ continue;
16701
+ }
16702
+ const used2 = this.readStylePart(tokens, i, line, style, false);
16703
+ if (used2 === void 0) return;
16704
+ if (used2 === 0) break;
16705
+ i += used2;
16522
16706
  }
16523
16707
  if (tokens[i]?.kind !== "lbrace") {
16524
16708
  this.error("Expected { after group header", this.locOf(tokens[i] ?? tokens[i - 1], line));
@@ -16526,7 +16710,10 @@ var Parser = class {
16526
16710
  }
16527
16711
  this.expectEnd(tokens, i + 1, line);
16528
16712
  const loc = this.locOf(idToken, line);
16529
- this.addNode({ id: idToken.value, kind: "group", name, type: "group", techStack, parent: this.currentGroup(), position, loc });
16713
+ const group = { id: idToken.value, kind: "group", name, type: "group", techStack, parent: this.currentGroup(), position, loc };
16714
+ if (ranked.rank !== void 0) group.rank = ranked.rank;
16715
+ this.applyStyle(group, style);
16716
+ this.addNode(group);
16530
16717
  this.stack.push({ kind: "group", id: idToken.value, loc });
16531
16718
  }
16532
16719
  parseUseCase(tokens, line) {
@@ -16631,8 +16818,16 @@ var Parser = class {
16631
16818
  }
16632
16819
  const node = { id: idToken.value, kind: "component", name: idToken.value, type: "shape", techStack: DEFAULT_TECH, parent: this.currentGroup(), loc };
16633
16820
  let strings = 0;
16821
+ const style = {};
16634
16822
  for (let i = 1; i < tokens.length; i++) {
16635
16823
  const t = tokens[i];
16824
+ const styleUsed = this.readStylePart(tokens, i, line, style, true);
16825
+ const used2 = styleUsed === 0 ? this.readRank(tokens, i, line, node) : styleUsed;
16826
+ if (used2 === void 0) return;
16827
+ if (used2 > 0) {
16828
+ i += used2 - 1;
16829
+ continue;
16830
+ }
16636
16831
  if (t.kind === "string" && strings < 2) {
16637
16832
  if (strings++ === 0) node.name = t.value;
16638
16833
  else node.description = t.value;
@@ -16667,16 +16862,23 @@ var Parser = class {
16667
16862
  return;
16668
16863
  }
16669
16864
  }
16865
+ this.applyStyle(node, style);
16670
16866
  this.addNode(node);
16671
16867
  }
16672
16868
  parseConnection(tokens, index) {
16673
16869
  const line = index + 1;
16674
- const [fromToken, arrowToken, toToken, labelToken] = tokens;
16870
+ const [fromToken, arrowToken, toToken] = tokens;
16871
+ const tagToken = tokens[3]?.kind === "tag" ? tokens[3] : void 0;
16872
+ const labelToken = tokens[tagToken ? 4 : 3];
16675
16873
  if (toToken?.kind !== "ident") {
16676
16874
  this.error("Expected a target id after the arrow", this.locOf(toToken ?? arrowToken, line));
16677
16875
  return index;
16678
16876
  }
16679
- if (tokens.length > 4 || labelToken && labelToken.kind !== "label") {
16877
+ if (tagToken && tokens[4]?.kind === "tag") {
16878
+ this.error("A connection takes one tag", this.locOf(tokens[4], line));
16879
+ return index;
16880
+ }
16881
+ if (tokens.length > (tagToken ? 5 : 4) || labelToken && labelToken.kind !== "label") {
16680
16882
  this.error("Expected : before the connection label", this.locOf(labelToken, line));
16681
16883
  return index;
16682
16884
  }
@@ -16705,7 +16907,12 @@ var Parser = class {
16705
16907
  const arrow = arrowToken.value;
16706
16908
  this.references.push({ id: from, loc: this.locOf(fromToken, line) }, { id: to, loc: this.locOf(toToken, line) });
16707
16909
  const top = this.top();
16708
- if (top?.kind === "usecase" || top?.kind === "alt" || top?.kind === "par") {
16910
+ const inUseCase = top?.kind === "usecase" || top?.kind === "alt" || top?.kind === "par";
16911
+ if (tagToken && inUseCase) {
16912
+ this.error("Tags style connections in the architecture, not use case steps", this.locOf(tagToken, line));
16913
+ return lastLine;
16914
+ }
16915
+ if (inUseCase) {
16709
16916
  const body = this.currentBody();
16710
16917
  body.openAlt = void 0;
16711
16918
  const prefixes = arrow === "-->" ? { rest: label } : this.readLabelPrefixes(label, labelToken, line);
@@ -16728,7 +16935,12 @@ var Parser = class {
16728
16935
  const base = `${from}->${to}`;
16729
16936
  const count = this.edgeCount.get(base) ?? 0;
16730
16937
  this.edgeCount.set(base, count + 1);
16731
- this.edges.push({ id: count ? `${base}#${count + 1}` : base, source: from, target: to, label: unquote(label) || void 0, loc });
16938
+ const edge = { id: count ? `${base}#${count + 1}` : base, source: from, target: to, label: unquote(label) || void 0, loc };
16939
+ if (tagToken) {
16940
+ edge.tag = tagToken.value;
16941
+ this.tagRefs.push({ tag: tagToken.value, loc: this.locOf(tagToken, line), target: edge });
16942
+ }
16943
+ this.edges.push(edge);
16732
16944
  }
16733
16945
  return lastLine;
16734
16946
  }
@@ -16782,7 +16994,7 @@ var Parser = class {
16782
16994
  this.skipSection(section2, tokens, line);
16783
16995
  return false;
16784
16996
  }
16785
- if (section2 === "traffic" || section2 === "requirements" || section2 === "capacity") this.expectEnd(tokens, 2, line);
16997
+ if (section2 === "traffic" || section2 === "requirements" || section2 === "capacity" || section2 === "legend") this.expectEnd(tokens, 2, line);
16786
16998
  this.stack.push({ kind: "section", section: section2, ...fill, loc });
16787
16999
  return true;
16788
17000
  }
@@ -16801,6 +17013,8 @@ var Parser = class {
16801
17013
  return this.parseRequirement(tokens, line);
16802
17014
  case "capacity":
16803
17015
  return this.parseCapacityLine(tokens, line);
17016
+ case "legend":
17017
+ return this.parseLegendLine(tokens, line);
16804
17018
  case "entity":
16805
17019
  return this.parseField(frame.entity, tokens, line);
16806
17020
  case "decision":
@@ -16812,6 +17026,110 @@ var Parser = class {
16812
17026
  }
16813
17027
  }
16814
17028
  }
17029
+ /**
17030
+ * Reads one style part of a node or group header at `i`: `color <name|#hex>`,
17031
+ * `.tag` and, on nodes, `critical`. Returns how many tokens it used (0 when
17032
+ * the token is something else), or undefined after reporting an error.
17033
+ */
17034
+ readStylePart(tokens, i, line, parts, isNode) {
17035
+ const t = tokens[i];
17036
+ if (t.kind === "tag") {
17037
+ if (parts.tag) {
17038
+ this.error(`A ${isNode ? "node" : "group"} takes one tag; '.${parts.tag.value}' is already set`, this.locOf(t, line));
17039
+ return void 0;
17040
+ }
17041
+ parts.tag = { value: t.value, loc: this.locOf(t, line) };
17042
+ return 1;
17043
+ }
17044
+ if (isWord(t, "color")) {
17045
+ const value = this.readColor(tokens[i + 1], t, line);
17046
+ if (value === void 0) return void 0;
17047
+ parts.color = value;
17048
+ return 2;
17049
+ }
17050
+ if (isNode && isWord(t, "critical")) {
17051
+ parts.critical = true;
17052
+ return 1;
17053
+ }
17054
+ return 0;
17055
+ }
17056
+ /** The color in `token`, a palette name or `#hex`, or undefined after reporting an error. */
17057
+ readColor(token, after2, line) {
17058
+ const choices = `${COLOR_NAMES.join(", ")}, or a hex value after color, like color #3b82f6`;
17059
+ if (token?.kind !== "ident" && token?.kind !== "hex") {
17060
+ this.error(`Expected a color after ${after2.value}: ${choices}`, this.locOf(token ?? after2, line));
17061
+ return void 0;
17062
+ }
17063
+ const hex = resolveColor(token.value);
17064
+ if (!hex) {
17065
+ const suggestion = suggestColor(token.value);
17066
+ this.error(`Unknown color '${token.value}'.${suggestion ? ` Did you mean '${suggestion}'?` : ""} Use ${choices}`, this.locOf(token, line));
17067
+ return void 0;
17068
+ }
17069
+ return { name: token.value, hex };
17070
+ }
17071
+ applyStyle(node, parts) {
17072
+ if (parts.color) {
17073
+ node.color = parts.color.name;
17074
+ node.style = { color: parts.color.hex };
17075
+ }
17076
+ if (parts.critical) node.critical = true;
17077
+ if (parts.tag) {
17078
+ node.tag = parts.tag.value;
17079
+ this.tagRefs.push({ tag: parts.tag.value, loc: parts.tag.loc, target: node });
17080
+ }
17081
+ }
17082
+ /** `owned "Owned by Order Fulfillment" green dashed`: an id, a label, then a color and a dash in any order. */
17083
+ parseLegendLine(tokens, line) {
17084
+ const [idToken] = tokens;
17085
+ if (idToken.kind !== "ident") {
17086
+ this.error('Expected a legend entry, e.g. owned "Owned by us" green', this.locOf(idToken, line));
17087
+ return;
17088
+ }
17089
+ const loc = this.locOf(idToken, line);
17090
+ const entry = { id: idToken.value, label: idToken.value, loc };
17091
+ let i = 1;
17092
+ if (tokens[i]?.kind === "string") entry.label = tokens[i++].value;
17093
+ for (; i < tokens.length; i++) {
17094
+ const t = tokens[i];
17095
+ if (isWord(t, "dashed") || isWord(t, "dotted")) {
17096
+ entry.dash = t.value;
17097
+ continue;
17098
+ }
17099
+ if (t.kind === "ident") {
17100
+ const keyword = t.value === "color";
17101
+ const color = this.readColor(keyword ? tokens[i + 1] : t, t, line);
17102
+ if (!color) return;
17103
+ entry.colorName = color.name;
17104
+ entry.color = color.hex;
17105
+ if (keyword) i++;
17106
+ continue;
17107
+ }
17108
+ this.error(`Unexpected ${describe(t)} in legend entry; expected a color, dashed or dotted`, this.locOf(t, line));
17109
+ return;
17110
+ }
17111
+ const existing = this.legend.find((e) => e.id === entry.id);
17112
+ if (existing) {
17113
+ this.error(`Duplicate legend entry '${entry.id}' (first declared ${this.where(existing.loc, loc)})`, loc);
17114
+ return;
17115
+ }
17116
+ this.legend.push(entry);
17117
+ }
17118
+ /** Applies each `.tag` once every file is read; an unknown tag is an error. An explicit `color` wins over the tag's (D4). */
17119
+ resolveTags() {
17120
+ const entries2 = new Map(this.legend.map((e) => [e.id, e]));
17121
+ for (const ref of this.tagRefs) {
17122
+ const entry = entries2.get(ref.tag);
17123
+ if (!entry) {
17124
+ const close = [...entries2.keys()].find((id) => editDistance2(id, ref.tag) <= 2);
17125
+ const hint = close ? ` Did you mean '.${close}'?` : "";
17126
+ this.error(`Unknown tag '.${ref.tag}'; add it to the legend, e.g. legend { ${ref.tag} "What it means" blue }.${hint}`, ref.loc);
17127
+ continue;
17128
+ }
17129
+ const color = ref.target.style?.color ?? entry.color;
17130
+ if (color || entry.dash) ref.target.style = { ...color ? { color } : {}, ...entry.dash ? { dash: entry.dash } : {} };
17131
+ }
17132
+ }
16815
17133
  /** `"Redirect" 100k rps mix "Cache hit" 90%, "Cache miss" 10%` */
16816
17134
  parseTrafficLine(tokens, line) {
16817
17135
  const [name, rate] = tokens;
@@ -17773,7 +18091,7 @@ function parseSize(text) {
17773
18091
  return { value };
17774
18092
  }
17775
18093
  function inferProtocol(isAsync, method, target) {
17776
- if (isAsync || target?.type === "queue") return "MESSAGING";
18094
+ if (isAsync || target?.type === "queue" || target?.type === "broker") return "MESSAGING";
17777
18095
  if (!method) return "OTHER";
17778
18096
  switch (target?.techStack) {
17779
18097
  case "GraphQL":
@@ -17798,6 +18116,8 @@ function describe(token) {
17798
18116
  return `[${token.value}]`;
17799
18117
  case "team":
17800
18118
  return `@${token.value}`;
18119
+ case "tag":
18120
+ return `tag '.${token.value}'`;
17801
18121
  case "label":
17802
18122
  return "':'";
17803
18123
  case "lbrace":
@@ -17846,6 +18166,7 @@ var GENERIC_TECH2 = {
17846
18166
  search: "Search Engine",
17847
18167
  analytics: "Data Warehouse",
17848
18168
  queue: "Message Queue",
18169
+ broker: "Message Broker",
17849
18170
  storage: "Object Storage",
17850
18171
  external: "Third Party API",
17851
18172
  other: "Service"
@@ -18533,7 +18854,7 @@ function stepAccess(step) {
18533
18854
  return step.access ?? accessOf(step.httpMethod, step.stepName ?? "");
18534
18855
  }
18535
18856
  function accessIn(diagram) {
18536
- const queues = new Set(diagram.nodes.filter((n) => kindOf(n) === "queue").map((n) => n.id));
18857
+ const queues = new Set(diagram.nodes.filter((n) => kindMatches(kindOf(n), "queue")).map((n) => n.id));
18537
18858
  return (step) => queues.has(step.toServiceId) ? "write" : stepAccess(step);
18538
18859
  }
18539
18860
 
@@ -18552,6 +18873,7 @@ var RELATIONAL = {
18552
18873
  durable: true,
18553
18874
  consistency: "strong"
18554
18875
  };
18876
+ var QUEUE = { rps: 5e4, latencyMs: 5, availability: 0.9999, costUsd: 200, durable: true, consistency: "strong" };
18555
18877
  var KIND_PROFILES = {
18556
18878
  client: { ...NEUTRAL, bandwidthMBps: 10 },
18557
18879
  // A firewall or accelerator in front of everything else (WAF, Global Accelerator); `any edge` also selects the four below.
@@ -18567,7 +18889,9 @@ var KIND_PROFILES = {
18567
18889
  database: RELATIONAL,
18568
18890
  search: { rps: 3e3, latencyMs: 15, availability: 0.999, costUsd: 400, durable: true, consistency: "eventual" },
18569
18891
  analytics: { rps: 200, latencyMs: 500, availability: 0.999, costUsd: 300, durable: true, consistency: "eventual" },
18570
- queue: { rps: 5e4, latencyMs: 5, availability: 0.9999, costUsd: 200, durable: true, consistency: "strong" },
18892
+ queue: QUEUE,
18893
+ // Same numbers as a queue for now: both take a message and hand it on later.
18894
+ broker: QUEUE,
18571
18895
  storage: { rps: 5e3, latencyMs: 30, availability: 0.9999, costUsd: 50, durable: true, consistency: "eventual" },
18572
18896
  external: { rps: 1e3, latencyMs: 200, availability: 0.999, costUsd: 0, durable: false },
18573
18897
  other: NEUTRAL
@@ -19210,8 +19534,10 @@ function overUseCases(run2, name, check, limit, anyWithoutTraffic = false) {
19210
19534
  return fail(failed.length === 1 ? failed[0].message : `${failed.map((c) => c.value ?? c.message).join("; ")} ${limit}`, failed[0].hint);
19211
19535
  }
19212
19536
  function unknownUseCase(run2, name) {
19213
- const known = run2.diagram.useCases.map((u) => `"${u.name}"`);
19214
- return { ...fail(`No use case named "${name}"`, known.length ? `Use one of ${known.join(", ")}` : 'Add a usecase "\u2026" { \u2026 } block'), missing: `use case ${name}` };
19537
+ const lower = name.toLowerCase();
19538
+ const near = run2.diagram.useCases.filter((u) => editDistance(u.name.toLowerCase(), lower) <= Math.max(1, Math.floor(name.length / 4)));
19539
+ const hint = near.length ? `Use one of ${near.map((u) => quote2(u.name)).join(", ")}` : `Add usecase ${quote2(name)} { \u2026 }`;
19540
+ return { ...fail(`No use case named ${quote2(name)}`, hint), missing: `use case ${name}` };
19215
19541
  }
19216
19542
  function saturatedOn(run2, ids, value, maxMs) {
19217
19543
  const n = [...new Set(ids)].map((id) => run2.nodes.get(id)).find((x) => !!x?.saturated);
@@ -20370,7 +20696,7 @@ function blockIssues(source2, cardIds) {
20370
20696
  return issues;
20371
20697
  }
20372
20698
 
20373
- // ../frontend/src/practice/markdown.ts
20699
+ // ../frontend/src/practice/markdownReader.ts
20374
20700
  function safeHref(href) {
20375
20701
  const trimmed = href.trim();
20376
20702
  if (/^(https?:|mailto:)/i.test(trimmed) || /^[#./]/.test(trimmed)) return trimmed;
@@ -26278,7 +26604,7 @@ ${IMPORT_USAGE}`);
26278
26604
  }
26279
26605
 
26280
26606
  // src/cli.ts
26281
- var VERSION2 = true ? "0.10.0" : "dev";
26607
+ var VERSION2 = true ? "0.11.0" : "dev";
26282
26608
  var USAGE = `Usage:
26283
26609
  proschi check [--strict] [--format text|github|json] [--openapi <node>=<spec>]... <file|dir>...
26284
26610
  proschi parse <file>