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/server.cjs CHANGED
@@ -25011,7 +25011,8 @@ var componentCatalog = [
25011
25011
  { type: "database", techStack: "Database", category: "Generic Components", searchTerms: "database relational sql db", aliases: ["DB", "RDBMS", "SQL Database", "Relational Database", "SQL"] },
25012
25012
  { 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"] },
25013
25013
  { type: "cache", techStack: "Cache", category: "Generic Components", searchTerms: "cache in-memory key value", aliases: ["In-Memory Cache", "Distributed Cache"] },
25014
- { 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"] },
25014
+ { type: "queue", techStack: "Message Queue", category: "Generic Components", searchTerms: "queue message job task", aliases: ["Queue", "Job Queue", "Task Queue"] },
25015
+ { 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"] },
25015
25016
  { 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"] },
25016
25017
  { type: "cdn", techStack: "CDN", category: "Generic Components", searchTerms: "cdn content delivery network edge cache", kind: "cdn", aliases: ["Content Delivery Network", "Edge Cache"] },
25017
25018
  { 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"] },
@@ -25072,10 +25073,10 @@ var componentCatalog = [
25072
25073
  { type: "database", techStack: "AWS OpenSearch", category: "AWS Services", searchTerms: "aws opensearch elasticsearch search", kind: "search", aliases: ["Amazon OpenSearch", "OpenSearch Service", "AWS Elasticsearch"] },
25073
25074
  // AWS Messaging
25074
25075
  { type: "queue", techStack: "AWS SQS", category: "AWS Services", searchTerms: "aws sqs queue message", aliases: ["Amazon SQS", "Simple Queue Service"] },
25075
- { type: "queue", techStack: "AWS SNS", category: "AWS Services", searchTerms: "aws sns notification topic pubsub", aliases: ["SNS", "Amazon SNS", "Simple Notification Service"] },
25076
- { type: "queue", techStack: "AWS EventBridge", category: "AWS Services", searchTerms: "aws eventbridge event bus", aliases: ["EventBridge", "Amazon EventBridge"] },
25077
- { type: "queue", techStack: "AWS Kinesis", category: "AWS Services", searchTerms: "aws kinesis stream data", aliases: ["Kinesis", "Amazon Kinesis", "Kinesis Data Streams"] },
25078
- { type: "queue", techStack: "AWS MSK", category: "AWS Services", searchTerms: "aws msk managed kafka stream", aliases: ["MSK", "Amazon MSK", "Managed Streaming for Kafka"] },
25076
+ { type: "broker", techStack: "AWS SNS", category: "AWS Services", searchTerms: "aws sns notification topic pubsub", aliases: ["SNS", "Amazon SNS", "Simple Notification Service"] },
25077
+ { type: "broker", techStack: "AWS EventBridge", category: "AWS Services", searchTerms: "aws eventbridge event bus", aliases: ["EventBridge", "Amazon EventBridge"] },
25078
+ { type: "broker", techStack: "AWS Kinesis", category: "AWS Services", searchTerms: "aws kinesis stream data", aliases: ["Kinesis", "Amazon Kinesis", "Kinesis Data Streams"] },
25079
+ { type: "broker", techStack: "AWS MSK", category: "AWS Services", searchTerms: "aws msk managed kafka stream", aliases: ["MSK", "Amazon MSK", "Managed Streaming for Kafka"] },
25079
25080
  // AWS CDN/Network
25080
25081
  { type: "cdn", techStack: "AWS CloudFront", category: "AWS Services", searchTerms: "aws cloudfront cdn edge", kind: "cdn", aliases: ["CloudFront", "Amazon CloudFront"] },
25081
25082
  { type: "cdn", techStack: "AWS Route53", category: "AWS Services", searchTerms: "aws route53 dns domain", kind: "dns", aliases: ["Route53", "Amazon Route 53"] },
@@ -25103,7 +25104,7 @@ var componentCatalog = [
25103
25104
  { type: "database", techStack: "GCP BigQuery", category: "GCP Services", searchTerms: "gcp google bigquery data warehouse analytics", kind: "analytics", aliases: ["BigQuery", "Google BigQuery"] },
25104
25105
  { type: "cache", techStack: "GCP Memorystore", category: "GCP Services", searchTerms: "gcp google memorystore redis memcached cache", aliases: ["Memorystore"] },
25105
25106
  // GCP Messaging
25106
- { 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"] },
25107
+ { 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"] },
25107
25108
  // GCP CDN/Network
25108
25109
  { type: "cdn", techStack: "GCP Cloud CDN", category: "GCP Services", searchTerms: "gcp google cloud cdn edge", kind: "cdn", aliases: ["Cloud CDN", "Google Cloud CDN"] },
25109
25110
  { type: "cdn", techStack: "GCP Cloud DNS", category: "GCP Services", searchTerms: "gcp google cloud dns domain", kind: "dns", aliases: ["Cloud DNS", "Google Cloud DNS"] },
@@ -25130,9 +25131,9 @@ var componentCatalog = [
25130
25131
  { type: "cache", techStack: "Azure Cache for Redis", category: "Azure Services", searchTerms: "azure redis cache", aliases: ["Azure Redis"] },
25131
25132
  { type: "database", techStack: "Azure AI Search", category: "Azure Services", searchTerms: "azure ai cognitive search", kind: "search", aliases: ["Azure Cognitive Search", "Azure Search"] },
25132
25133
  // Azure Messaging
25133
- { type: "queue", techStack: "Azure Service Bus", category: "Azure Services", searchTerms: "azure service bus queue message", aliases: ["Service Bus"] },
25134
- { type: "queue", techStack: "Azure Event Hubs", category: "Azure Services", searchTerms: "azure event hubs stream", aliases: ["Event Hubs", "Event Hub"] },
25135
- { type: "queue", techStack: "Azure Event Grid", category: "Azure Services", searchTerms: "azure event grid", aliases: ["Event Grid"] },
25134
+ { type: "broker", techStack: "Azure Service Bus", category: "Azure Services", searchTerms: "azure service bus queue message", aliases: ["Service Bus"] },
25135
+ { type: "broker", techStack: "Azure Event Hubs", category: "Azure Services", searchTerms: "azure event hubs stream", aliases: ["Event Hubs", "Event Hub"] },
25136
+ { type: "broker", techStack: "Azure Event Grid", category: "Azure Services", searchTerms: "azure event grid", aliases: ["Event Grid"] },
25136
25137
  { type: "queue", techStack: "Azure Queue Storage", category: "Azure Services", searchTerms: "azure queue storage message", aliases: ["Azure Queue", "Azure Queues"] },
25137
25138
  // Azure CDN/Network
25138
25139
  { type: "cdn", techStack: "Azure CDN", category: "Azure Services", searchTerms: "azure cdn edge", kind: "cdn" },
@@ -25172,16 +25173,16 @@ var componentCatalog = [
25172
25173
  { type: "cache", techStack: "Valkey", category: "Cache & In-Memory", searchTerms: "valkey redis cache memory" },
25173
25174
  { type: "cache", techStack: "KeyDB", category: "Cache & In-Memory", searchTerms: "keydb redis cache memory" },
25174
25175
  { type: "cache", techStack: "Dragonfly", category: "Cache & In-Memory", searchTerms: "dragonfly dragonflydb redis cache memory", aliases: ["DragonflyDB"] },
25175
- // Message Queues
25176
- { type: "queue", techStack: "Kafka", category: "Message Queues", searchTerms: "kafka message queue stream", aliases: ["Apache Kafka", "Confluent", "Confluent Kafka", "Kafka Topic"] },
25177
- { type: "queue", techStack: "RabbitMQ", category: "Message Queues", searchTerms: "rabbitmq message queue amqp", aliases: ["Rabbit", "AMQP"] },
25178
- { type: "queue", techStack: "SQS", category: "Message Queues", searchTerms: "sqs queue aws message" },
25179
- { type: "queue", techStack: "Redis Queue", category: "Message Queues", searchTerms: "redis queue message", aliases: ["Redis Streams", "Redis Pub/Sub"] },
25180
- { type: "queue", techStack: "NATS", category: "Message Queues", searchTerms: "nats message queue", aliases: ["NATS JetStream", "JetStream"] },
25181
- { type: "queue", techStack: "Apache Pulsar", category: "Message Queues", searchTerms: "pulsar message queue stream", aliases: ["Pulsar"] },
25182
- { type: "queue", techStack: "ActiveMQ", category: "Message Queues", searchTerms: "activemq message queue", aliases: ["Apache ActiveMQ", "Amazon MQ"] },
25183
- { type: "queue", techStack: "ZeroMQ", category: "Message Queues", searchTerms: "zeromq zmq message queue", aliases: ["ZMQ", "0MQ"] },
25184
- { type: "queue", techStack: "Redpanda", category: "Message Queues", searchTerms: "redpanda kafka stream queue" },
25176
+ // Queues & Brokers: a queue hands each message to one consumer; a broker routes or logs it for many.
25177
+ { type: "broker", techStack: "Kafka", category: "Queues & Brokers", searchTerms: "kafka message broker event stream log topic", aliases: ["Apache Kafka", "Confluent", "Confluent Kafka", "Kafka Topic"] },
25178
+ { type: "broker", techStack: "RabbitMQ", category: "Queues & Brokers", searchTerms: "rabbitmq message broker queue amqp", aliases: ["Rabbit", "AMQP"] },
25179
+ { type: "queue", techStack: "SQS", category: "Queues & Brokers", searchTerms: "sqs queue aws message" },
25180
+ { type: "queue", techStack: "Redis Queue", category: "Queues & Brokers", searchTerms: "redis queue message", aliases: ["Redis Streams", "Redis Pub/Sub"] },
25181
+ { type: "broker", techStack: "NATS", category: "Queues & Brokers", searchTerms: "nats message broker stream", aliases: ["NATS JetStream", "JetStream"] },
25182
+ { type: "broker", techStack: "Apache Pulsar", category: "Queues & Brokers", searchTerms: "pulsar message broker stream", aliases: ["Pulsar"] },
25183
+ { type: "broker", techStack: "ActiveMQ", category: "Queues & Brokers", searchTerms: "activemq message broker queue", aliases: ["Apache ActiveMQ", "Amazon MQ"] },
25184
+ { type: "queue", techStack: "ZeroMQ", category: "Queues & Brokers", searchTerms: "zeromq zmq message queue", aliases: ["ZMQ", "0MQ"] },
25185
+ { type: "broker", techStack: "Redpanda", category: "Queues & Brokers", searchTerms: "redpanda kafka message broker stream" },
25185
25186
  // Search
25186
25187
  { type: "database", techStack: "OpenSearch", category: "Search", searchTerms: "opensearch elasticsearch search", kind: "search" },
25187
25188
  { type: "database", techStack: "Solr", category: "Search", searchTerms: "solr lucene search", kind: "search", aliases: ["Apache Solr"] },
@@ -25297,6 +25298,7 @@ var IDENT_PART = /[A-Za-z0-9_]/;
25297
25298
  var TEAM_PART = /[A-Za-z0-9_-]/;
25298
25299
  var DIGIT = /[0-9]/;
25299
25300
  var LETTER = /[A-Za-z]/;
25301
+ var HEX_COLOR = /^#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})(?![A-Za-z0-9_])/;
25300
25302
  var SEPARATE_UNIT = /^[ \t]+(rps|rpm|rpd|ms|s|%|usd\/month|usd\/GB|MB\/s|GB\/s)(?![A-Za-z0-9_/])/;
25301
25303
  function tokenizeLine(text, line) {
25302
25304
  const tokens = [];
@@ -25309,7 +25311,14 @@ function tokenizeLine(text, line) {
25309
25311
  i++;
25310
25312
  continue;
25311
25313
  }
25312
- if (ch === "#") break;
25314
+ if (ch === "#") {
25315
+ const prev = tokens[tokens.length - 1];
25316
+ const hex = prev?.kind === "ident" && prev.value === "color" ? HEX_COLOR.exec(text.slice(i)) : null;
25317
+ if (!hex) break;
25318
+ tokens.push({ kind: "hex", value: hex[0], col: i + 1, length: hex[0].length });
25319
+ i += hex[0].length;
25320
+ continue;
25321
+ }
25313
25322
  const start = i;
25314
25323
  if (ch === ":") {
25315
25324
  const raw = stripTrailingComment(text.slice(i + 1));
@@ -25375,6 +25384,12 @@ function tokenizeLine(text, line) {
25375
25384
  tokens.push({ kind: "ident", value: text.slice(start, i), col: start + 1, length: i - start });
25376
25385
  continue;
25377
25386
  }
25387
+ if (ch === "." && IDENT_START.test(text[i + 1] ?? "")) {
25388
+ i++;
25389
+ while (i < text.length && IDENT_PART.test(text[i])) i++;
25390
+ tokens.push({ kind: "tag", value: text.slice(start + 1, i), col: start + 1, length: i - start });
25391
+ continue;
25392
+ }
25378
25393
  if (ch === "{" || ch === "}" || ch === ",") {
25379
25394
  const kind = ch === "{" ? "lbrace" : ch === "}" ? "rbrace" : "comma";
25380
25395
  tokens.push({ kind, value: ch, col: start + 1, length: 1 });
@@ -25513,10 +25528,12 @@ function classify(lines) {
25513
25528
  entries2.push({ ...base, kind: "cells", section, cells });
25514
25529
  continue;
25515
25530
  }
25516
- if (!inSection && first.kind === "ident" && second?.kind === "arrow" && third?.kind === "ident" && tokens.length <= 4 && (!fourth || fourth.kind === "label")) {
25517
- const entry = { ...base, kind: "connection", from: first.value, arrow: second.value, to: third.value, label: fourth?.value, continuation: [] };
25531
+ const tag = fourth?.kind === "tag" ? fourth : void 0;
25532
+ const labelToken = tokens[tag ? 4 : 3];
25533
+ if (!inSection && first.kind === "ident" && second?.kind === "arrow" && third?.kind === "ident" && tokens.length <= (tag ? 5 : 4) && (!labelToken || labelToken.kind === "label")) {
25534
+ const entry = { ...base, kind: "connection", from: first.value, arrow: second.value, to: third.value, tag: tag?.value, label: labelToken?.value, continuation: [] };
25518
25535
  const brackets = new BracketCounter();
25519
- if (fourth && brackets.feed(fourth.value) > 0) {
25536
+ if (labelToken && brackets.feed(labelToken.value) > 0) {
25520
25537
  const delta = indent(depth).length - width(leadingSpace(raw));
25521
25538
  while (i + 1 < lines.length) {
25522
25539
  const next = lines[++i];
@@ -25529,7 +25546,7 @@ function classify(lines) {
25529
25546
  continue;
25530
25547
  }
25531
25548
  const oneLineDecision = first.value === "decision" && second?.kind === "string" && third?.kind === "ident" && third.value === "because";
25532
- if (!inSection && first.kind === "ident" && !KEYWORDS.has(first.value) && !oneLineDecision && tokens.slice(1).every((t) => ["string", "tech", "team", "ident", "number", "comma"].includes(t.kind))) {
25549
+ 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))) {
25533
25550
  let k = 1;
25534
25551
  const take = (kind) => tokens[k]?.kind === kind ? source(raw, tokens[k++]) : "";
25535
25552
  const cells2 = [first.value, take("string"), take("tech"), take("team"), join(raw, tokens.slice(k))];
@@ -25547,6 +25564,10 @@ function classify(lines) {
25547
25564
  }
25548
25565
  return entries2;
25549
25566
  }
25567
+ function isLayoutHint(tokens) {
25568
+ const [first, second] = tokens;
25569
+ 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);
25570
+ }
25550
25571
  function sectionOpenedBy(tokens) {
25551
25572
  const [first, second] = tokens;
25552
25573
  if (first.kind !== "ident" || tokens[tokens.length - 1].kind !== "lbrace") return void 0;
@@ -25621,12 +25642,13 @@ function alignCells(run) {
25621
25642
  }
25622
25643
  function alignConnections(run) {
25623
25644
  const head = Math.max(...run.map((e) => len(e.from) + 1 + e.arrow.length));
25645
+ const end = (e) => e.tag ? `${e.to} .${e.tag}` : e.to;
25624
25646
  const labelled = run.filter((e) => e.label !== void 0);
25625
- const target = labelled.length ? Math.max(...labelled.map((e) => len(e.to))) : 0;
25647
+ const target = labelled.length ? Math.max(...labelled.map((e) => len(end(e)))) : 0;
25626
25648
  return run.map((e) => {
25627
25649
  const start = e.from + " ".repeat(head - len(e.from) - e.arrow.length) + e.arrow + " ";
25628
- if (e.label === void 0) return start + e.to;
25629
- return start + pad(e.to, target) + " :" + (e.label ? " " + e.label : "");
25650
+ if (e.label === void 0) return start + end(e);
25651
+ return start + pad(end(e), target) + " :" + (e.label ? " " + e.label : "");
25630
25652
  });
25631
25653
  }
25632
25654
  function commentOf(raw, tokens) {
@@ -25668,8 +25690,8 @@ var len = (s) => [...s].length;
25668
25690
  var pad = (s, to) => s + " ".repeat(Math.max(0, to - len(s)));
25669
25691
 
25670
25692
  // ../frontend/src/dsl/kinds.ts
25671
- var KINDS = ["client", "edge", "cdn", "loadbalancer", "gateway", "dns", "service", "function", "cache", "database", "search", "analytics", "queue", "storage", "external", "other"];
25672
- var DATA_STORE_KINDS = ["cache", "database", "search", "analytics", "queue", "storage"];
25693
+ var KINDS = ["client", "edge", "cdn", "loadbalancer", "gateway", "dns", "service", "function", "cache", "database", "search", "analytics", "queue", "broker", "storage", "external", "other"];
25694
+ var DATA_STORE_KINDS = ["cache", "database", "search", "analytics", "queue", "broker", "storage"];
25673
25695
  var EDGE_KINDS = ["edge", "cdn", "loadbalancer", "gateway", "dns"];
25674
25696
  var BY_TECH = new Map(componentCatalog.flatMap((c) => "kind" in c ? [[c.techStack, c.kind]] : []));
25675
25697
  var BY_TYPE = {
@@ -25682,6 +25704,7 @@ var BY_TYPE = {
25682
25704
  cache: "cache",
25683
25705
  database: "database",
25684
25706
  queue: "queue",
25707
+ broker: "broker",
25685
25708
  storage: "storage",
25686
25709
  external: "external"
25687
25710
  };
@@ -25703,6 +25726,7 @@ var TYPE_OF_KIND = {
25703
25726
  search: "database",
25704
25727
  analytics: "database",
25705
25728
  queue: "queue",
25729
+ broker: "broker",
25706
25730
  storage: "storage",
25707
25731
  external: "external",
25708
25732
  other: "shape"
@@ -25715,7 +25739,8 @@ var KIND_WORDS = [
25715
25739
  [/gateway|\bapim?\b.*manage/, "gateway"],
25716
25740
  [/\bwaf\b|firewall/, "edge"],
25717
25741
  [/cache|memcache/, "cache"],
25718
- [/queue|\bmq\b|mq$|broker|event.?stream|\bbus\b|topic|pub.?sub|event.?hub|kafka/, "queue"],
25742
+ [/broker|event.?stream|event.?log|\bbus\b|topic|pub.?sub|event.?hub|kafka|kinesis|pulsar|redpanda|jetstream|\bnats\b/, "broker"],
25743
+ [/queue|\bmq\b|mq$/, "queue"],
25719
25744
  [/search|lucene|\bindex\b/, "search"],
25720
25745
  [/warehouse|analytic|olap|data.?lake|lakehouse|time.?series|\btsdb\b/, "analytics"],
25721
25746
  [/bucket|blob|object.?stor|storage|\bfs\b|file.?system|\bnas\b|\bdisk\b|volume/, "storage"],
@@ -25736,7 +25761,7 @@ function isEdge(kind) {
25736
25761
  return EDGE_KINDS.includes(kind);
25737
25762
  }
25738
25763
  function kindMatches(actual, wanted) {
25739
- return actual === wanted || wanted === "edge" && isEdge(actual);
25764
+ return actual === wanted || wanted === "edge" && isEdge(actual) || wanted === "queue" && actual === "broker";
25740
25765
  }
25741
25766
 
25742
25767
  // ../frontend/src/dsl/paths.ts
@@ -25861,12 +25886,56 @@ function accessOf(httpMethod, label) {
25861
25886
  return word && WRITE_VERBS.includes(word.toUpperCase()) ? "write" : "read";
25862
25887
  }
25863
25888
 
25889
+ // ../frontend/src/dsl/palette.ts
25890
+ var PALETTE = {
25891
+ gray: "#6b7280",
25892
+ blue: "#3b82f6",
25893
+ teal: "#14b8a6",
25894
+ green: "#22c55e",
25895
+ yellow: "#eab308",
25896
+ orange: "#f97316",
25897
+ red: "#ef4444",
25898
+ purple: "#a855f7",
25899
+ pink: "#ec4899"
25900
+ };
25901
+ var COLOR_NAMES = Object.keys(PALETTE);
25902
+ var HEX = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
25903
+ function resolveColor(text) {
25904
+ if (Object.prototype.hasOwnProperty.call(PALETTE, text)) return PALETTE[text];
25905
+ if (!HEX.test(text)) return void 0;
25906
+ const digits = text.slice(1).toLowerCase();
25907
+ return "#" + (digits.length === 3 ? [...digits].map((d) => d + d).join("") : digits);
25908
+ }
25909
+ function suggestColor(text) {
25910
+ const word = text.toLowerCase();
25911
+ let best;
25912
+ for (const name of COLOR_NAMES) {
25913
+ const distance = editDistance2(word, name);
25914
+ if (distance <= 2 && (!best || distance < best.distance)) best = { name, distance };
25915
+ }
25916
+ return best?.name;
25917
+ }
25918
+ function editDistance2(a, b) {
25919
+ const row = Array.from({ length: b.length + 1 }, (_, i) => i);
25920
+ for (let i = 1; i <= a.length; i++) {
25921
+ let prev = row[0];
25922
+ row[0] = i;
25923
+ for (let j = 1; j <= b.length; j++) {
25924
+ const next = row[j];
25925
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
25926
+ prev = next;
25927
+ }
25928
+ }
25929
+ return row[b.length];
25930
+ }
25931
+
25864
25932
  // ../frontend/src/dsl/parser.ts
25865
25933
  var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
25866
25934
  var DEFAULT_TECH = "Rectangle";
25867
25935
  var DEFAULT_GROUP_TECH = "Logical Group";
25868
25936
  var MAX_SCENARIOS = 32;
25869
25937
  var MAX_ALT_DEPTH = 16;
25938
+ var DIRECTIONS = ["down", "right", "up", "left"];
25870
25939
  var REPLICAS = /^x(\d+)$/;
25871
25940
  var PERCENTILES = { p50: 50, p90: 90, p95: 95, p99: 99, p999: 99.9 };
25872
25941
  var FIELD_FLAGS = ["key", "index", "unique", "optional"];
@@ -25895,6 +25964,7 @@ var GENERIC_TECH = {
25895
25964
  search: "Search Engine",
25896
25965
  analytics: "Data Warehouse",
25897
25966
  queue: "Message Queue",
25967
+ broker: "Message Broker",
25898
25968
  storage: "Object Storage",
25899
25969
  external: "Third Party API",
25900
25970
  other: "Note"
@@ -25940,6 +26010,12 @@ var Parser = class {
25940
26010
  decisions = [];
25941
26011
  tests = [];
25942
26012
  blocks = [];
26013
+ legend = [];
26014
+ layout = {};
26015
+ /** Where each top-level layout hint was set, for the duplicate warning. */
26016
+ layoutLocs = {};
26017
+ /** `.tag` uses, resolved against the legend once every file is read. */
26018
+ tagRefs = [];
25943
26019
  /** Checks of use case, scenario and node names, run once every file is read and use cases are built. */
25944
26020
  deferred = [];
25945
26021
  built = [];
@@ -25953,6 +26029,7 @@ var Parser = class {
25953
26029
  run() {
25954
26030
  this.parseFile();
25955
26031
  this.createImplicitNodes();
26032
+ this.resolveTags();
25956
26033
  for (const e of this.edges) this.connected.add(`${e.source}|${e.target}`).add(`${e.target}|${e.source}`);
25957
26034
  const useCases = this.useCases.map(({ useCase, body }) => this.buildUseCase(useCase, body));
25958
26035
  const endpoints = useCases.flatMap((u) => u.endpoint ? [u.endpoint] : []);
@@ -25971,7 +26048,9 @@ var Parser = class {
25971
26048
  ...this.capacity.length ? { capacity: this.capacity } : {},
25972
26049
  ...this.entities.length ? { entities: this.entities } : {},
25973
26050
  ...this.decisions.length ? { decisions: this.decisions } : {},
25974
- ...this.tests.length ? { tests: this.tests } : {}
26051
+ ...this.tests.length ? { tests: this.tests } : {},
26052
+ ...this.legend.length ? { legend: this.legend } : {},
26053
+ ...Object.keys(this.layout).length ? { layout: this.layout } : {}
25975
26054
  };
25976
26055
  const order = (d) => this.fileOrder.get(d.file) ?? 0;
25977
26056
  this.diagnostics.sort((a, b) => order(a) - order(b) || a.line - b.line || a.col - b.col);
@@ -26060,6 +26139,7 @@ var Parser = class {
26060
26139
  case "traffic":
26061
26140
  case "requirements":
26062
26141
  case "capacity":
26142
+ case "legend":
26063
26143
  if (second?.kind === "lbrace") this.openSection(first.value, tokens, line);
26064
26144
  else this.parseNode(tokens, line);
26065
26145
  break;
@@ -26076,6 +26156,15 @@ var Parser = class {
26076
26156
  if (second?.kind === "string" && tokens[tokens.length - 1].kind === "lbrace") this.parseTest(tokens, line);
26077
26157
  else this.parseNode(tokens, line);
26078
26158
  break;
26159
+ // Keywords only as `direction <word>` and `layout <word>`, so a node may still be called direction or layout.
26160
+ case "direction":
26161
+ if (this.isHint(tokens)) this.parseDirection(tokens, line);
26162
+ else this.parseNode(tokens, line);
26163
+ break;
26164
+ case "layout":
26165
+ if (this.isHint(tokens)) this.parseLayoutMode(tokens, line);
26166
+ else this.parseNode(tokens, line);
26167
+ break;
26079
26168
  default:
26080
26169
  this.parseNode(tokens, line);
26081
26170
  }
@@ -26094,6 +26183,86 @@ var Parser = class {
26094
26183
  }
26095
26184
  this.expectEnd(tokens, summary ? 3 : 2, line);
26096
26185
  }
26186
+ /** `direction right`, `layout ordered`: a keyword and one plain word that is not a node part. */
26187
+ isHint(tokens) {
26188
+ const second = tokens[1];
26189
+ return tokens.length === 2 && second.kind === "ident" && !["pos", "critical", "color", "rank"].includes(second.value) && !REPLICAS.test(second.value);
26190
+ }
26191
+ /** `direction down|right|up|left`: at the top level for the whole diagram, inside a group for its members. */
26192
+ parseDirection(tokens, line) {
26193
+ const [keyword, value] = tokens;
26194
+ const loc = this.locOf(value, line);
26195
+ if (!DIRECTIONS.includes(value.value)) {
26196
+ this.error(`Unknown direction '${value.value}'; use direction down, right, up or left`, loc);
26197
+ return;
26198
+ }
26199
+ const direction = value.value;
26200
+ const top = this.top();
26201
+ if (top?.kind === "group") {
26202
+ const group = this.nodes.get(top.id);
26203
+ if (!group || group.loc !== top.loc) return;
26204
+ if (group.direction) this.warning(`Group '${group.id}' already has direction ${group.direction}; this one is ignored`, this.locOf(keyword, line));
26205
+ else group.direction = direction;
26206
+ return;
26207
+ }
26208
+ if (top) {
26209
+ this.error("direction is only allowed at the top level or inside a group", this.locOf(keyword, line));
26210
+ return;
26211
+ }
26212
+ if (this.file !== void 0) return;
26213
+ if (this.layoutLocs.direction) {
26214
+ this.warning(`direction is already set ${this.where(this.layoutLocs.direction, loc)}; this one is ignored`, this.locOf(keyword, line));
26215
+ return;
26216
+ }
26217
+ this.layout.direction = direction;
26218
+ this.layoutLocs.direction = loc;
26219
+ }
26220
+ /** `layout ordered`: lay edges out by declaration order. */
26221
+ parseLayoutMode(tokens, line) {
26222
+ const [keyword, value] = tokens;
26223
+ const loc = this.locOf(value, line);
26224
+ if (value.value !== "ordered") {
26225
+ this.error(`Unknown layout mode '${value.value}'; the only one is layout ordered`, loc);
26226
+ return;
26227
+ }
26228
+ if (this.top()) {
26229
+ this.error("layout ordered applies to the whole diagram; write it at the top level", this.locOf(keyword, line));
26230
+ return;
26231
+ }
26232
+ if (this.file !== void 0) return;
26233
+ if (this.layoutLocs.ordered) {
26234
+ this.warning(`layout ordered is already set ${this.where(this.layoutLocs.ordered, loc)}`, this.locOf(keyword, line));
26235
+ return;
26236
+ }
26237
+ this.layout.ordered = true;
26238
+ this.layoutLocs.ordered = loc;
26239
+ }
26240
+ /**
26241
+ * `rank 2`, `rank first`, `rank last` at `i` in a node or group header.
26242
+ * Returns how many tokens it used (0 when the token is something else), or
26243
+ * undefined after reporting an error.
26244
+ */
26245
+ readRank(tokens, i, line, current) {
26246
+ const t = tokens[i];
26247
+ if (!isWord(t, "rank")) return 0;
26248
+ const value = tokens[i + 1];
26249
+ const help = "a layer number from 0 (rank 0, rank 1, \u2026), rank first or rank last";
26250
+ if (current.rank !== void 0) {
26251
+ this.error(`A node takes one rank; rank ${current.rank} is already set`, this.locOf(t, line));
26252
+ return void 0;
26253
+ }
26254
+ if (value?.kind === "number" && /^\d+$/.test(value.value)) {
26255
+ current.rank = Number(value.value);
26256
+ return 2;
26257
+ }
26258
+ if (value?.kind === "ident" && (value.value === "first" || value.value === "last")) {
26259
+ current.rank = value.value;
26260
+ return 2;
26261
+ }
26262
+ const numeric = value?.kind === "number" || value?.kind === "quantity";
26263
+ this.error(numeric ? `'${value.value}' is not a layer number; use ${help}` : `Expected ${help} after rank`, this.locOf(value ?? t, line));
26264
+ return void 0;
26265
+ }
26097
26266
  parseImport(tokens, line) {
26098
26267
  const keyword = tokens[0];
26099
26268
  if (this.stack.length > 0) {
@@ -26167,14 +26336,29 @@ var Parser = class {
26167
26336
  else this.warning(`'${tech.value}' is not a group style; using ${DEFAULT_GROUP_TECH}`, this.locOf(tech, line));
26168
26337
  }
26169
26338
  let position;
26170
- if (tokens[i]?.kind === "ident" && tokens[i].value === "pos") {
26171
- const [x, comma, y] = [tokens[i + 1], tokens[i + 2], tokens[i + 3]];
26172
- if (x?.kind !== "number" || comma?.kind !== "comma" || y?.kind !== "number") {
26173
- this.error("Expected pos x,y", this.locOf(tokens[i], line));
26174
- return;
26339
+ const style = {};
26340
+ const ranked = {};
26341
+ while (i < tokens.length && tokens[i].kind !== "lbrace") {
26342
+ const rankUsed = this.readRank(tokens, i, line, ranked);
26343
+ if (rankUsed === void 0) return;
26344
+ if (rankUsed) {
26345
+ i += rankUsed;
26346
+ continue;
26347
+ }
26348
+ if (tokens[i].kind === "ident" && tokens[i].value === "pos") {
26349
+ const [x, comma, y] = [tokens[i + 1], tokens[i + 2], tokens[i + 3]];
26350
+ if (x?.kind !== "number" || comma?.kind !== "comma" || y?.kind !== "number") {
26351
+ this.error("Expected pos x,y", this.locOf(tokens[i], line));
26352
+ return;
26353
+ }
26354
+ position = { x: Number(x.value), y: Number(y.value) };
26355
+ i += 4;
26356
+ continue;
26175
26357
  }
26176
- position = { x: Number(x.value), y: Number(y.value) };
26177
- i += 4;
26358
+ const used2 = this.readStylePart(tokens, i, line, style, false);
26359
+ if (used2 === void 0) return;
26360
+ if (used2 === 0) break;
26361
+ i += used2;
26178
26362
  }
26179
26363
  if (tokens[i]?.kind !== "lbrace") {
26180
26364
  this.error("Expected { after group header", this.locOf(tokens[i] ?? tokens[i - 1], line));
@@ -26182,7 +26366,10 @@ var Parser = class {
26182
26366
  }
26183
26367
  this.expectEnd(tokens, i + 1, line);
26184
26368
  const loc = this.locOf(idToken, line);
26185
- this.addNode({ id: idToken.value, kind: "group", name, type: "group", techStack, parent: this.currentGroup(), position, loc });
26369
+ const group = { id: idToken.value, kind: "group", name, type: "group", techStack, parent: this.currentGroup(), position, loc };
26370
+ if (ranked.rank !== void 0) group.rank = ranked.rank;
26371
+ this.applyStyle(group, style);
26372
+ this.addNode(group);
26186
26373
  this.stack.push({ kind: "group", id: idToken.value, loc });
26187
26374
  }
26188
26375
  parseUseCase(tokens, line) {
@@ -26287,8 +26474,16 @@ var Parser = class {
26287
26474
  }
26288
26475
  const node = { id: idToken.value, kind: "component", name: idToken.value, type: "shape", techStack: DEFAULT_TECH, parent: this.currentGroup(), loc };
26289
26476
  let strings = 0;
26477
+ const style = {};
26290
26478
  for (let i = 1; i < tokens.length; i++) {
26291
26479
  const t = tokens[i];
26480
+ const styleUsed = this.readStylePart(tokens, i, line, style, true);
26481
+ const used2 = styleUsed === 0 ? this.readRank(tokens, i, line, node) : styleUsed;
26482
+ if (used2 === void 0) return;
26483
+ if (used2 > 0) {
26484
+ i += used2 - 1;
26485
+ continue;
26486
+ }
26292
26487
  if (t.kind === "string" && strings < 2) {
26293
26488
  if (strings++ === 0) node.name = t.value;
26294
26489
  else node.description = t.value;
@@ -26323,16 +26518,23 @@ var Parser = class {
26323
26518
  return;
26324
26519
  }
26325
26520
  }
26521
+ this.applyStyle(node, style);
26326
26522
  this.addNode(node);
26327
26523
  }
26328
26524
  parseConnection(tokens, index) {
26329
26525
  const line = index + 1;
26330
- const [fromToken, arrowToken, toToken, labelToken] = tokens;
26526
+ const [fromToken, arrowToken, toToken] = tokens;
26527
+ const tagToken = tokens[3]?.kind === "tag" ? tokens[3] : void 0;
26528
+ const labelToken = tokens[tagToken ? 4 : 3];
26331
26529
  if (toToken?.kind !== "ident") {
26332
26530
  this.error("Expected a target id after the arrow", this.locOf(toToken ?? arrowToken, line));
26333
26531
  return index;
26334
26532
  }
26335
- if (tokens.length > 4 || labelToken && labelToken.kind !== "label") {
26533
+ if (tagToken && tokens[4]?.kind === "tag") {
26534
+ this.error("A connection takes one tag", this.locOf(tokens[4], line));
26535
+ return index;
26536
+ }
26537
+ if (tokens.length > (tagToken ? 5 : 4) || labelToken && labelToken.kind !== "label") {
26336
26538
  this.error("Expected : before the connection label", this.locOf(labelToken, line));
26337
26539
  return index;
26338
26540
  }
@@ -26361,7 +26563,12 @@ var Parser = class {
26361
26563
  const arrow = arrowToken.value;
26362
26564
  this.references.push({ id: from, loc: this.locOf(fromToken, line) }, { id: to, loc: this.locOf(toToken, line) });
26363
26565
  const top = this.top();
26364
- if (top?.kind === "usecase" || top?.kind === "alt" || top?.kind === "par") {
26566
+ const inUseCase = top?.kind === "usecase" || top?.kind === "alt" || top?.kind === "par";
26567
+ if (tagToken && inUseCase) {
26568
+ this.error("Tags style connections in the architecture, not use case steps", this.locOf(tagToken, line));
26569
+ return lastLine;
26570
+ }
26571
+ if (inUseCase) {
26365
26572
  const body = this.currentBody();
26366
26573
  body.openAlt = void 0;
26367
26574
  const prefixes = arrow === "-->" ? { rest: label } : this.readLabelPrefixes(label, labelToken, line);
@@ -26384,7 +26591,12 @@ var Parser = class {
26384
26591
  const base = `${from}->${to}`;
26385
26592
  const count = this.edgeCount.get(base) ?? 0;
26386
26593
  this.edgeCount.set(base, count + 1);
26387
- this.edges.push({ id: count ? `${base}#${count + 1}` : base, source: from, target: to, label: unquote(label) || void 0, loc });
26594
+ const edge = { id: count ? `${base}#${count + 1}` : base, source: from, target: to, label: unquote(label) || void 0, loc };
26595
+ if (tagToken) {
26596
+ edge.tag = tagToken.value;
26597
+ this.tagRefs.push({ tag: tagToken.value, loc: this.locOf(tagToken, line), target: edge });
26598
+ }
26599
+ this.edges.push(edge);
26388
26600
  }
26389
26601
  return lastLine;
26390
26602
  }
@@ -26438,7 +26650,7 @@ var Parser = class {
26438
26650
  this.skipSection(section, tokens, line);
26439
26651
  return false;
26440
26652
  }
26441
- if (section === "traffic" || section === "requirements" || section === "capacity") this.expectEnd(tokens, 2, line);
26653
+ if (section === "traffic" || section === "requirements" || section === "capacity" || section === "legend") this.expectEnd(tokens, 2, line);
26442
26654
  this.stack.push({ kind: "section", section, ...fill, loc });
26443
26655
  return true;
26444
26656
  }
@@ -26457,6 +26669,8 @@ var Parser = class {
26457
26669
  return this.parseRequirement(tokens, line);
26458
26670
  case "capacity":
26459
26671
  return this.parseCapacityLine(tokens, line);
26672
+ case "legend":
26673
+ return this.parseLegendLine(tokens, line);
26460
26674
  case "entity":
26461
26675
  return this.parseField(frame.entity, tokens, line);
26462
26676
  case "decision":
@@ -26468,6 +26682,110 @@ var Parser = class {
26468
26682
  }
26469
26683
  }
26470
26684
  }
26685
+ /**
26686
+ * Reads one style part of a node or group header at `i`: `color <name|#hex>`,
26687
+ * `.tag` and, on nodes, `critical`. Returns how many tokens it used (0 when
26688
+ * the token is something else), or undefined after reporting an error.
26689
+ */
26690
+ readStylePart(tokens, i, line, parts, isNode) {
26691
+ const t = tokens[i];
26692
+ if (t.kind === "tag") {
26693
+ if (parts.tag) {
26694
+ this.error(`A ${isNode ? "node" : "group"} takes one tag; '.${parts.tag.value}' is already set`, this.locOf(t, line));
26695
+ return void 0;
26696
+ }
26697
+ parts.tag = { value: t.value, loc: this.locOf(t, line) };
26698
+ return 1;
26699
+ }
26700
+ if (isWord(t, "color")) {
26701
+ const value = this.readColor(tokens[i + 1], t, line);
26702
+ if (value === void 0) return void 0;
26703
+ parts.color = value;
26704
+ return 2;
26705
+ }
26706
+ if (isNode && isWord(t, "critical")) {
26707
+ parts.critical = true;
26708
+ return 1;
26709
+ }
26710
+ return 0;
26711
+ }
26712
+ /** The color in `token`, a palette name or `#hex`, or undefined after reporting an error. */
26713
+ readColor(token, after2, line) {
26714
+ const choices = `${COLOR_NAMES.join(", ")}, or a hex value after color, like color #3b82f6`;
26715
+ if (token?.kind !== "ident" && token?.kind !== "hex") {
26716
+ this.error(`Expected a color after ${after2.value}: ${choices}`, this.locOf(token ?? after2, line));
26717
+ return void 0;
26718
+ }
26719
+ const hex = resolveColor(token.value);
26720
+ if (!hex) {
26721
+ const suggestion = suggestColor(token.value);
26722
+ this.error(`Unknown color '${token.value}'.${suggestion ? ` Did you mean '${suggestion}'?` : ""} Use ${choices}`, this.locOf(token, line));
26723
+ return void 0;
26724
+ }
26725
+ return { name: token.value, hex };
26726
+ }
26727
+ applyStyle(node, parts) {
26728
+ if (parts.color) {
26729
+ node.color = parts.color.name;
26730
+ node.style = { color: parts.color.hex };
26731
+ }
26732
+ if (parts.critical) node.critical = true;
26733
+ if (parts.tag) {
26734
+ node.tag = parts.tag.value;
26735
+ this.tagRefs.push({ tag: parts.tag.value, loc: parts.tag.loc, target: node });
26736
+ }
26737
+ }
26738
+ /** `owned "Owned by Order Fulfillment" green dashed`: an id, a label, then a color and a dash in any order. */
26739
+ parseLegendLine(tokens, line) {
26740
+ const [idToken] = tokens;
26741
+ if (idToken.kind !== "ident") {
26742
+ this.error('Expected a legend entry, e.g. owned "Owned by us" green', this.locOf(idToken, line));
26743
+ return;
26744
+ }
26745
+ const loc = this.locOf(idToken, line);
26746
+ const entry = { id: idToken.value, label: idToken.value, loc };
26747
+ let i = 1;
26748
+ if (tokens[i]?.kind === "string") entry.label = tokens[i++].value;
26749
+ for (; i < tokens.length; i++) {
26750
+ const t = tokens[i];
26751
+ if (isWord(t, "dashed") || isWord(t, "dotted")) {
26752
+ entry.dash = t.value;
26753
+ continue;
26754
+ }
26755
+ if (t.kind === "ident") {
26756
+ const keyword = t.value === "color";
26757
+ const color = this.readColor(keyword ? tokens[i + 1] : t, t, line);
26758
+ if (!color) return;
26759
+ entry.colorName = color.name;
26760
+ entry.color = color.hex;
26761
+ if (keyword) i++;
26762
+ continue;
26763
+ }
26764
+ this.error(`Unexpected ${describe(t)} in legend entry; expected a color, dashed or dotted`, this.locOf(t, line));
26765
+ return;
26766
+ }
26767
+ const existing = this.legend.find((e) => e.id === entry.id);
26768
+ if (existing) {
26769
+ this.error(`Duplicate legend entry '${entry.id}' (first declared ${this.where(existing.loc, loc)})`, loc);
26770
+ return;
26771
+ }
26772
+ this.legend.push(entry);
26773
+ }
26774
+ /** Applies each `.tag` once every file is read; an unknown tag is an error. An explicit `color` wins over the tag's (D4). */
26775
+ resolveTags() {
26776
+ const entries2 = new Map(this.legend.map((e) => [e.id, e]));
26777
+ for (const ref of this.tagRefs) {
26778
+ const entry = entries2.get(ref.tag);
26779
+ if (!entry) {
26780
+ const close = [...entries2.keys()].find((id) => editDistance2(id, ref.tag) <= 2);
26781
+ const hint = close ? ` Did you mean '.${close}'?` : "";
26782
+ this.error(`Unknown tag '.${ref.tag}'; add it to the legend, e.g. legend { ${ref.tag} "What it means" blue }.${hint}`, ref.loc);
26783
+ continue;
26784
+ }
26785
+ const color = ref.target.style?.color ?? entry.color;
26786
+ if (color || entry.dash) ref.target.style = { ...color ? { color } : {}, ...entry.dash ? { dash: entry.dash } : {} };
26787
+ }
26788
+ }
26471
26789
  /** `"Redirect" 100k rps mix "Cache hit" 90%, "Cache miss" 10%` */
26472
26790
  parseTrafficLine(tokens, line) {
26473
26791
  const [name, rate] = tokens;
@@ -27429,7 +27747,7 @@ function parseSize(text) {
27429
27747
  return { value };
27430
27748
  }
27431
27749
  function inferProtocol(isAsync, method, target) {
27432
- if (isAsync || target?.type === "queue") return "MESSAGING";
27750
+ if (isAsync || target?.type === "queue" || target?.type === "broker") return "MESSAGING";
27433
27751
  if (!method) return "OTHER";
27434
27752
  switch (target?.techStack) {
27435
27753
  case "GraphQL":
@@ -27454,6 +27772,8 @@ function describe(token) {
27454
27772
  return `[${token.value}]`;
27455
27773
  case "team":
27456
27774
  return `@${token.value}`;
27775
+ case "tag":
27776
+ return `tag '.${token.value}'`;
27457
27777
  case "label":
27458
27778
  return "':'";
27459
27779
  case "lbrace":
@@ -27933,7 +28253,7 @@ function stepAccess(step) {
27933
28253
  return step.access ?? accessOf(step.httpMethod, step.stepName ?? "");
27934
28254
  }
27935
28255
  function accessIn(diagram) {
27936
- const queues = new Set(diagram.nodes.filter((n) => kindOf(n) === "queue").map((n) => n.id));
28256
+ const queues = new Set(diagram.nodes.filter((n) => kindMatches(kindOf(n), "queue")).map((n) => n.id));
27937
28257
  return (step) => queues.has(step.toServiceId) ? "write" : stepAccess(step);
27938
28258
  }
27939
28259
 
@@ -27952,6 +28272,7 @@ var RELATIONAL = {
27952
28272
  durable: true,
27953
28273
  consistency: "strong"
27954
28274
  };
28275
+ var QUEUE = { rps: 5e4, latencyMs: 5, availability: 0.9999, costUsd: 200, durable: true, consistency: "strong" };
27955
28276
  var KIND_PROFILES = {
27956
28277
  client: { ...NEUTRAL, bandwidthMBps: 10 },
27957
28278
  // A firewall or accelerator in front of everything else (WAF, Global Accelerator); `any edge` also selects the four below.
@@ -27967,7 +28288,9 @@ var KIND_PROFILES = {
27967
28288
  database: RELATIONAL,
27968
28289
  search: { rps: 3e3, latencyMs: 15, availability: 0.999, costUsd: 400, durable: true, consistency: "eventual" },
27969
28290
  analytics: { rps: 200, latencyMs: 500, availability: 0.999, costUsd: 300, durable: true, consistency: "eventual" },
27970
- queue: { rps: 5e4, latencyMs: 5, availability: 0.9999, costUsd: 200, durable: true, consistency: "strong" },
28291
+ queue: QUEUE,
28292
+ // Same numbers as a queue for now: both take a message and hand it on later.
28293
+ broker: QUEUE,
27971
28294
  storage: { rps: 5e3, latencyMs: 30, availability: 0.9999, costUsd: 50, durable: true, consistency: "eventual" },
27972
28295
  external: { rps: 1e3, latencyMs: 200, availability: 0.999, costUsd: 0, durable: false },
27973
28296
  other: NEUTRAL
@@ -28610,8 +28933,10 @@ function overUseCases(run, name, check, limit, anyWithoutTraffic = false) {
28610
28933
  return fail(failed.length === 1 ? failed[0].message : `${failed.map((c) => c.value ?? c.message).join("; ")} ${limit}`, failed[0].hint);
28611
28934
  }
28612
28935
  function unknownUseCase(run, name) {
28613
- const known = run.diagram.useCases.map((u) => `"${u.name}"`);
28614
- return { ...fail(`No use case named "${name}"`, known.length ? `Use one of ${known.join(", ")}` : 'Add a usecase "\u2026" { \u2026 } block'), missing: `use case ${name}` };
28936
+ const lower = name.toLowerCase();
28937
+ const near = run.diagram.useCases.filter((u) => editDistance(u.name.toLowerCase(), lower) <= Math.max(1, Math.floor(name.length / 4)));
28938
+ const hint = near.length ? `Use one of ${near.map((u) => quote(u.name)).join(", ")}` : `Add usecase ${quote(name)} { \u2026 }`;
28939
+ return { ...fail(`No use case named ${quote(name)}`, hint), missing: `use case ${name}` };
28615
28940
  }
28616
28941
  function saturatedOn(run, ids, value, maxMs) {
28617
28942
  const n = [...new Set(ids)].map((id) => run.nodes.get(id)).find((x) => !!x?.saturated);
@@ -29086,6 +29411,10 @@ var KEYWORDS2 = [
29086
29411
  { label: "usecase", detail: 'usecase "Name" { steps }', snippet: 'usecase "${1:Name}" {\n $0\n}' },
29087
29412
  { label: "par", detail: "Steps that run in parallel", snippet: "par {\n $0\n}" },
29088
29413
  { label: "alt", detail: "One scenario of a use case", snippet: 'alt "${1:Scenario}" {\n $0\n}' },
29414
+ { label: "legend", detail: 'Styles that .tag applies: owned "Owned by us" green', snippet: 'legend {\n ${1:tag} "${2:What it means}" ${3:blue}$0\n}' },
29415
+ // Layout hints (docs/LANGUAGE.md, "Steering the layout").
29416
+ { label: "direction", detail: "Where edges flow: down, right, up or left (top level or in a group)", snippet: "direction ${1|down,right,up,left|}" },
29417
+ { label: "layout ordered", detail: "Lay edges out by declaration order (top level)", snippet: "layout ordered" },
29089
29418
  // High-level design sections (top level).
29090
29419
  { label: "traffic", detail: 'Requests per use case: "Use case" 100 rps', snippet: 'traffic {\n "${1:Use case}" ${2:100 rps}$0\n}' },
29091
29420
  { label: "requirements", detail: "Latency, availability, durability, resilience, cost", snippet: "requirements {\n p99 < ${1:200ms}$0\n}" },
@@ -29217,17 +29546,40 @@ function inLabel(before2) {
29217
29546
  return false;
29218
29547
  }
29219
29548
  function insidePayload(lines, line) {
29549
+ return payloadLines(lines.slice(0, line + 1))[line];
29550
+ }
29551
+ function payloadLines(lines) {
29220
29552
  let depth = 0;
29221
- for (let i = 0; i < line; i++) {
29222
- const text = lines[i];
29223
- const colon = depth > 0 ? -1 : labelStart(text);
29224
- if (depth === 0 && colon < 0) continue;
29225
- for (const ch of text.slice(colon + 1)) {
29226
- if (ch === "{" || ch === "[") depth++;
29227
- else if (ch === "}" || ch === "]") depth = Math.max(0, depth - 1);
29553
+ return lines.map((text) => {
29554
+ const inside = depth > 0;
29555
+ const colon = inside ? -1 : labelStart(text);
29556
+ if (inside || colon >= 0) {
29557
+ for (const ch of text.slice(colon + 1)) {
29558
+ if (ch === "{" || ch === "[") depth++;
29559
+ else if (ch === "}" || ch === "]") depth = Math.max(0, depth - 1);
29560
+ }
29228
29561
  }
29229
- }
29230
- return depth > 0;
29562
+ return inside;
29563
+ });
29564
+ }
29565
+ var TOKEN = /"(?:[^"\\]|\\.)*"?|(?<=\bcolor\s+)#[0-9a-fA-F]{3,6}(?![A-Za-z0-9_])|#.*|:.*|->>|-->|->|-x(?![A-Za-z0-9_])|\[[^\]\n]*\]|@[A-Za-z0-9_-]+|\.[A-Za-z_][A-Za-z0-9_]*|[A-Za-z_][A-Za-z0-9_]*/g;
29566
+ function semanticTokens(analysis) {
29567
+ const nodes = new Set(analysis.diagram.nodes.map((n) => n.id));
29568
+ const legend = analysis.diagram.legend ?? [];
29569
+ const tags = new Set(legend.map((e) => e.id));
29570
+ const entryAt = new Map(legend.filter((e) => e.loc.file === void 0).map((e) => [e.loc.line - 1, e.loc.col - 1]));
29571
+ const payload = payloadLines(analysis.lines);
29572
+ const out = [];
29573
+ analysis.lines.forEach((text, line) => {
29574
+ if (payload[line]) return;
29575
+ for (const m of text.matchAll(TOKEN)) {
29576
+ const word = m[0];
29577
+ const at = m.index ?? 0;
29578
+ const kind = word[0] === "[" ? "tech" : word[0] === "@" ? "team" : word[0] === "." ? tags.has(word.slice(1)) ? "tag" : null : entryAt.get(line) === at ? "tag" : /^[A-Za-z_]/.test(word) && nodes.has(word) ? "node" : null;
29579
+ if (kind) out.push({ line, character: at, length: word.length, kind });
29580
+ }
29581
+ });
29582
+ return out;
29231
29583
  }
29232
29584
  function labelStart(text) {
29233
29585
  let quoted = false;
@@ -29432,7 +29784,7 @@ function positionAt(text, offset) {
29432
29784
 
29433
29785
  // src/server.ts
29434
29786
  if (process.argv.includes("--version")) {
29435
- console.log(true ? "0.10.0" : "dev");
29787
+ console.log(true ? "0.11.0" : "dev");
29436
29788
  process.exit(0);
29437
29789
  }
29438
29790
  var connection = (0, import_node.createConnection)(import_node.ProposedFeatures.all);
@@ -29461,6 +29813,8 @@ function publishDependents(uri) {
29461
29813
  validate(doc);
29462
29814
  }
29463
29815
  }
29816
+ var TOKEN_TYPE = { node: "variable", tech: "type", team: "decorator", tag: "enumMember" };
29817
+ var TOKEN_TYPES = Object.values(TOKEN_TYPE);
29464
29818
  connection.onInitialize(() => ({
29465
29819
  capabilities: {
29466
29820
  textDocumentSync: import_node.TextDocumentSyncKind.Incremental,
@@ -29471,9 +29825,10 @@ connection.onInitialize(() => ({
29471
29825
  documentSymbolProvider: true,
29472
29826
  documentLinkProvider: { resolveProvider: false },
29473
29827
  documentFormattingProvider: true,
29474
- codeActionProvider: { codeActionKinds: [import_node.CodeActionKind.QuickFix] }
29828
+ codeActionProvider: { codeActionKinds: [import_node.CodeActionKind.QuickFix] },
29829
+ semanticTokensProvider: { legend: { tokenTypes: TOKEN_TYPES, tokenModifiers: [] }, full: true }
29475
29830
  },
29476
- serverInfo: { name: "proschi-language-server", version: true ? "0.10.0" : "dev" }
29831
+ serverInfo: { name: "proschi-language-server", version: true ? "0.11.0" : "dev" }
29477
29832
  }));
29478
29833
  function validate(document) {
29479
29834
  const analysis = analysisOf(document);
@@ -29618,5 +29973,17 @@ connection.onCodeAction((params) => {
29618
29973
  }
29619
29974
  return actions;
29620
29975
  });
29976
+ connection.languages.semanticTokens.on((params) => {
29977
+ const doc = documents.get(params.textDocument.uri);
29978
+ const data = [];
29979
+ let line = 0;
29980
+ let character = 0;
29981
+ for (const t of doc ? semanticTokens(analysisOf(doc)) : []) {
29982
+ data.push(t.line - line, t.line === line ? t.character - character : t.character, t.length, TOKEN_TYPES.indexOf(TOKEN_TYPE[t.kind]), 0);
29983
+ line = t.line;
29984
+ character = t.character;
29985
+ }
29986
+ return { data };
29987
+ });
29621
29988
  documents.listen(connection);
29622
29989
  connection.listen();