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 +378 -52
- package/dist/render.cjs +204 -201
- package/dist/server.cjs +429 -62
- package/grammar/proschi.tmLanguage.json +113 -0
- package/package.json +1 -1
- package/schema/proschi-diagram.schema.json +132 -1
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
|
|
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: "
|
|
15418
|
-
{ type: "
|
|
15419
|
-
{ type: "
|
|
15420
|
-
{ type: "
|
|
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: "
|
|
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: "
|
|
15476
|
-
{ type: "
|
|
15477
|
-
{ type: "
|
|
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
|
-
//
|
|
15518
|
-
{ type: "
|
|
15519
|
-
{ type: "
|
|
15520
|
-
{ type: "queue", techStack: "SQS", category: "
|
|
15521
|
-
{ type: "queue", techStack: "Redis Queue", category: "
|
|
15522
|
-
{ type: "
|
|
15523
|
-
{ type: "
|
|
15524
|
-
{ type: "
|
|
15525
|
-
{ type: "queue", techStack: "ZeroMQ", category: "
|
|
15526
|
-
{ type: "
|
|
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 === "#")
|
|
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
|
-
|
|
15862
|
-
|
|
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 (
|
|
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
|
|
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
|
|
15974
|
-
return start + pad(e
|
|
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
|
-
[/
|
|
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
|
-
|
|
16515
|
-
|
|
16516
|
-
|
|
16517
|
-
|
|
16518
|
-
|
|
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
|
-
|
|
16521
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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:
|
|
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
|
|
19214
|
-
|
|
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/
|
|
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.
|
|
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>
|