proschi 0.8.0 → 0.10.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
@@ -24992,304 +24992,6 @@ var import_node_fs = require("node:fs");
24992
24992
  var import_node_path = require("node:path");
24993
24993
  var import__ = __toESM(require__(), 1);
24994
24994
  var import_yaml = __toESM(require_dist(), 1);
24995
- var METHODS = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
24996
- var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
24997
- var esc = (key) => key.replace(/~/g, "~0").replace(/\//g, "~1");
24998
- var OpenApiSpec = class {
24999
- constructor(file, doc) {
25000
- this.file = file;
25001
- this.name = (0, import_node_path.basename)(file);
25002
- const version = String(doc.openapi ?? "");
25003
- if (!/^3\./.test(version)) {
25004
- throw new Error(doc.swagger ? "Swagger 2.0 is not supported; convert it to OpenAPI 3" : "not an OpenAPI 3 document (no openapi: 3.x field)");
25005
- }
25006
- this.doc = version.startsWith("3.0") ? from30(structuredClone(doc)) : doc;
25007
- this.prefix = serverPrefix(this.doc.servers);
25008
- this.ajv = new import__.Ajv2020({ strict: false, allErrors: true, validateSchema: false, validateFormats: false });
25009
- this.ajv.addSchema(this.doc, "openapi:spec");
25010
- }
25011
- file;
25012
- /** The file name, for messages. */
25013
- name;
25014
- /** Path prefix from `servers[0].url`, e.g. `/v1`; empty when there is none. */
25015
- prefix;
25016
- doc;
25017
- ajv;
25018
- validators = /* @__PURE__ */ new Map();
25019
- /** Path templates the spec documents. */
25020
- get templates() {
25021
- return isObject(this.doc.paths) ? Object.keys(this.doc.paths) : [];
25022
- }
25023
- /** Methods (upper case) documented for a path template. */
25024
- methods(path) {
25025
- const item = this.resolve(`/paths/${esc(path)}`)?.value;
25026
- return isObject(item) ? METHODS.filter((m) => isObject(item[m])).map((m) => m.toUpperCase()) : [];
25027
- }
25028
- operation(path, method) {
25029
- const item = this.resolve(`/paths/${esc(path)}`);
25030
- if (!item || !isObject(item.value)) return void 0;
25031
- const m = method.toLowerCase();
25032
- const value = item.value[m];
25033
- return isObject(value) ? { path, method: method.toUpperCase(), pointer: `${item.pointer}/${m}`, value } : void 0;
25034
- }
25035
- /** Documented response keys of an operation: `201`, `4XX`, `default`. */
25036
- statuses(op) {
25037
- const responses = op.value.responses;
25038
- return isObject(responses) ? Object.keys(responses) : [];
25039
- }
25040
- /** The response key that covers a status code: exact, then a `4XX` range, then `default`. */
25041
- responseKey(op, status) {
25042
- const keys = this.statuses(op);
25043
- return keys.find((k) => k === String(status)) ?? keys.find((k) => k.toUpperCase() === `${String(status)[0]}XX`) ?? keys.find((k) => k === "default");
25044
- }
25045
- /** Validates a request body; undefined when the operation has no JSON schema for it. */
25046
- validateRequest(op, body) {
25047
- const requestBody = this.resolve(`${op.pointer}/requestBody`);
25048
- return requestBody && this.validateContent(requestBody.pointer, body);
25049
- }
25050
- validateResponse(op, key, body) {
25051
- const response2 = this.resolve(`${op.pointer}/responses/${esc(key)}`);
25052
- return response2 && this.validateContent(response2.pointer, body);
25053
- }
25054
- /** Validates against the `application/json` (or `+json`) schema of a request body or response object. */
25055
- validateContent(pointer, body) {
25056
- const owner = this.resolve(pointer)?.value;
25057
- if (!isObject(owner) || !isObject(owner.content)) return void 0;
25058
- const type = Object.keys(owner.content).find((t) => /^application\/([\w.-]+\+)?json\b/i.test(t));
25059
- if (!type || !isObject(owner.content[type]) || !("schema" in owner.content[type])) return void 0;
25060
- const schemaPointer = `${pointer}/content/${esc(type)}/schema`;
25061
- let validate2 = this.validators.get(schemaPointer);
25062
- if (validate2 === void 0) {
25063
- try {
25064
- validate2 = this.ajv.compile({ $ref: `openapi:spec#${schemaPointer}` });
25065
- } catch {
25066
- validate2 = null;
25067
- }
25068
- this.validators.set(schemaPointer, validate2);
25069
- }
25070
- if (!validate2) return void 0;
25071
- return validate2(body) ? [] : (validate2.errors ?? []).map(describeError);
25072
- }
25073
- /** Follows local `$ref`s from the value at a JSON pointer; returns the final pointer and value. */
25074
- resolve(pointer) {
25075
- let value = this.at(pointer);
25076
- for (let hops = 0; isObject(value) && typeof value.$ref === "string" && hops < 20; hops++) {
25077
- if (!value.$ref.startsWith("#")) return void 0;
25078
- pointer = decodeURIComponent(value.$ref.slice(1));
25079
- value = this.at(pointer);
25080
- }
25081
- return value === void 0 ? void 0 : { pointer, value };
25082
- }
25083
- at(pointer) {
25084
- let value = this.doc;
25085
- for (const raw of pointer.split("/").slice(1)) {
25086
- const key = raw.replace(/~1/g, "/").replace(/~0/g, "~");
25087
- if (Array.isArray(value)) value = value[Number(key)];
25088
- else if (isObject(value)) value = value[key];
25089
- else return void 0;
25090
- }
25091
- return value;
25092
- }
25093
- };
25094
- function describeError(e) {
25095
- const where = e.instancePath || "(root)";
25096
- const p = e.params;
25097
- let message = e.message ?? "is invalid";
25098
- if (e.keyword === "additionalProperties" || e.keyword === "unevaluatedProperties") message += ` ('${p.additionalProperty ?? p.unevaluatedProperty}')`;
25099
- if (e.keyword === "enum" && Array.isArray(p.allowedValues)) message += `: ${p.allowedValues.map((v) => JSON.stringify(v)).join(", ")}`;
25100
- return `${where} ${message}`;
25101
- }
25102
- function serverPrefix(servers) {
25103
- const server = Array.isArray(servers) ? servers[0] : void 0;
25104
- if (!isObject(server) || typeof server.url !== "string") return "";
25105
- const vars = isObject(server.variables) ? server.variables : {};
25106
- const url = server.url.replace(/\{([^}]+)\}/g, (_, name) => {
25107
- const v = vars[name];
25108
- return isObject(v) && v.default !== void 0 ? String(v.default) : "";
25109
- });
25110
- const path = url.match(/^[a-z][a-z0-9+.-]*:\/\/[^/]*(\/.*)?$/i)?.[1] ?? (url.startsWith("/") ? url : "");
25111
- return path.replace(/[?#].*$/, "").replace(/\/+$/, "");
25112
- }
25113
- function from30(value) {
25114
- if (Array.isArray(value)) return value.map(from30);
25115
- if (!isObject(value)) return value;
25116
- const out = {};
25117
- for (const [k, v] of Object.entries(value)) out[k] = from30(v);
25118
- for (const [bool, bound] of [
25119
- ["exclusiveMinimum", "minimum"],
25120
- ["exclusiveMaximum", "maximum"]
25121
- ]) {
25122
- if (typeof out[bool] !== "boolean") continue;
25123
- if (out[bool] && typeof out[bound] === "number") {
25124
- out[bool] = out[bound];
25125
- delete out[bound];
25126
- } else delete out[bool];
25127
- }
25128
- if (typeof out.nullable === "boolean") {
25129
- const nullable = out.nullable;
25130
- delete out.nullable;
25131
- if (nullable) {
25132
- if (typeof out.type === "string" && !("$ref" in out)) {
25133
- out.type = [out.type, "null"];
25134
- if (Array.isArray(out.enum) && !out.enum.includes(null)) out.enum = [...out.enum, null];
25135
- } else return { anyOf: [out, { type: "null" }] };
25136
- }
25137
- }
25138
- return out;
25139
- }
25140
- var cache = /* @__PURE__ */ new Map();
25141
- function loadSpec(file) {
25142
- const { mtimeMs } = (0, import_node_fs.statSync)(file);
25143
- const cached = cache.get(file);
25144
- if (cached?.mtimeMs === mtimeMs) return cached.spec;
25145
- const text = (0, import_node_fs.readFileSync)(file, "utf8");
25146
- const doc = file.endsWith(".json") ? JSON.parse(text) : (0, import_yaml.parse)(text);
25147
- if (!isObject(doc)) throw new Error("not an OpenAPI document");
25148
- const spec = new OpenApiSpec(file, doc);
25149
- cache.set(file, { mtimeMs, spec });
25150
- return spec;
25151
- }
25152
-
25153
- // src/openapi/check.ts
25154
- var MAX_SCHEMA_ERRORS = 3;
25155
- function checkOpenApi(diagram, specFiles) {
25156
- const diagnostics = [];
25157
- const add = (severity, message, loc) => {
25158
- if (!diagnostics.some((d) => d.message === message && d.line === loc.line && d.col === loc.col && d.file === loc.file)) diagnostics.push({ severity, message, ...loc });
25159
- };
25160
- const specs = /* @__PURE__ */ new Map();
25161
- const specOf = (nodeId) => {
25162
- if (specs.has(nodeId)) return specs.get(nodeId);
25163
- let spec = null;
25164
- try {
25165
- spec = loadSpec(specFiles[nodeId]);
25166
- } catch (e) {
25167
- const node = diagram.nodes.find((n) => n.id === nodeId);
25168
- add("error", `Cannot read OpenAPI spec '${displayPath(specFiles[nodeId])}' for '${nodeId}': ${e.message}`, node.loc);
25169
- }
25170
- specs.set(nodeId, spec);
25171
- return spec;
25172
- };
25173
- for (const useCase of diagram.useCases) {
25174
- for (const scenario of useCase.scenarios) {
25175
- for (const step of scenario.steps) {
25176
- if (!step.httpMethod || !Object.hasOwn(specFiles, step.toServiceId)) continue;
25177
- const spec = specOf(step.toServiceId);
25178
- if (spec) checkStep(spec, step, (message, loc) => add("warning", message, loc));
25179
- }
25180
- }
25181
- }
25182
- return diagnostics.sort((a, b) => a.line - b.line || a.col - b.col);
25183
- }
25184
- function checkStep(spec, step, warn) {
25185
- const call = `${step.httpMethod} ${step.endpoint}`;
25186
- const path = normalizePath(step.endpoint);
25187
- const candidates = [path, stripPrefix(path, spec.prefix)].filter((p) => p !== void 0);
25188
- const templates = [...new Set(candidates.flatMap((p) => matchTemplates(spec.templates, p)))];
25189
- if (templates.length === 0) {
25190
- const shown = stripPrefix(path, spec.prefix) ?? path;
25191
- const near = closestTemplate(spec.templates, shown);
25192
- const nearMethod = near && (spec.methods(near).includes(step.httpMethod) ? step.httpMethod : spec.methods(near)[0]);
25193
- const hint = near ? `; did you mean ${nearMethod ? `${nearMethod} ` : ""}${near}?` : "";
25194
- warn(`${call}: '${spec.name}' has no path ${shown}${hint}`, step.loc);
25195
- return;
25196
- }
25197
- let op;
25198
- for (const t of templates) op ??= spec.operation(t, step.httpMethod);
25199
- if (!op) {
25200
- const documented = templates.flatMap((t) => spec.methods(t).map((m) => `${m} ${t}`));
25201
- const hint = documented.length === 1 ? `; did you mean ${documented[0]}?` : documented.length ? ` (documented: ${documented.join(", ")})` : "";
25202
- warn(`${call}: '${spec.name}' has no ${step.httpMethod} ${templates[0]}${hint}`, step.loc);
25203
- return;
25204
- }
25205
- if (step.failed) return;
25206
- const label = `${op.method} ${op.path}`;
25207
- const request2 = jsonBody(step.requestFormat, step.requestBody);
25208
- if (request2 !== void 0) {
25209
- const errors = spec.validateRequest(op, request2.value);
25210
- if (errors?.length) warn(`Request body does not match the schema of ${label}: ${summarize(errors)}`, step.loc);
25211
- }
25212
- if (step.statusCode === void 0) return;
25213
- const responseLoc = step.responseLoc ?? step.loc;
25214
- const key = spec.responseKey(op, step.statusCode);
25215
- if (!key) {
25216
- const documented = spec.statuses(op);
25217
- warn(`Status ${step.statusCode} is not documented for ${label} (documented: ${documented.join(", ") || "none"})`, responseLoc);
25218
- return;
25219
- }
25220
- const response2 = jsonBody(step.responseFormat, step.responseBody);
25221
- if (response2 !== void 0) {
25222
- const errors = spec.validateResponse(op, key, response2.value);
25223
- if (errors?.length) warn(`${step.statusCode} response body does not match the schema of ${label}: ${summarize(errors)}`, responseLoc);
25224
- }
25225
- }
25226
- function displayPath(file) {
25227
- const rel = (0, import_node_path2.relative)(process.cwd(), file);
25228
- return rel && !rel.startsWith("..") && !(0, import_node_path2.isAbsolute)(rel) ? rel : file;
25229
- }
25230
- function jsonBody(format2, body) {
25231
- if (format2 !== "JSON" || !body) return void 0;
25232
- try {
25233
- return { value: JSON.parse(body) };
25234
- } catch {
25235
- return void 0;
25236
- }
25237
- }
25238
- function summarize(errors) {
25239
- const shown = errors.slice(0, MAX_SCHEMA_ERRORS).join("; ");
25240
- return errors.length > MAX_SCHEMA_ERRORS ? `${shown} (and ${errors.length - MAX_SCHEMA_ERRORS} more)` : shown;
25241
- }
25242
-
25243
- // src/openapi/config.ts
25244
- var CONFIG_FILE = "proschi.json";
25245
- function findConfigFile(dir) {
25246
- for (let current = (0, import_node_path3.resolve)(dir); ; current = (0, import_node_path3.dirname)(current)) {
25247
- const candidate = (0, import_node_path3.join)(current, CONFIG_FILE);
25248
- if ((0, import_node_fs2.existsSync)(candidate) && (0, import_node_fs2.statSync)(candidate).isFile()) return candidate;
25249
- if ((0, import_node_path3.dirname)(current) === current) return void 0;
25250
- }
25251
- }
25252
- var used = /* @__PURE__ */ new Set();
25253
- function watchedFiles() {
25254
- return [...used];
25255
- }
25256
- var configs = /* @__PURE__ */ new Map();
25257
- function readConfig(file) {
25258
- const { mtimeMs } = (0, import_node_fs2.statSync)(file);
25259
- const cached = configs.get(file);
25260
- if (cached?.mtimeMs === mtimeMs) return cached.config;
25261
- let json;
25262
- try {
25263
- json = JSON.parse((0, import_node_fs2.readFileSync)(file, "utf8"));
25264
- } catch (e) {
25265
- throw new Error(`Invalid ${file}: ${e.message}`);
25266
- }
25267
- const openapi = json?.openapi ?? {};
25268
- if (typeof openapi !== "object" || openapi === null || Array.isArray(openapi) || Object.values(openapi).some((v) => typeof v !== "string")) {
25269
- throw new Error(`Invalid ${file}: "openapi" must map node ids to spec paths, e.g. {"openapi": {"orders": "specs/orders.yaml"}}`);
25270
- }
25271
- const config = {
25272
- file,
25273
- openapi: Object.fromEntries(Object.entries(openapi).map(([id, path]) => [id, (0, import_node_path3.resolve)((0, import_node_path3.dirname)(file), path)]))
25274
- };
25275
- configs.set(file, { mtimeMs, config });
25276
- return config;
25277
- }
25278
- function openApiDiagnostics(file, diagram, overrides = {}) {
25279
- let specs = {};
25280
- const configFile = findConfigFile((0, import_node_path3.dirname)((0, import_node_path3.resolve)(file)));
25281
- if (configFile) {
25282
- used.add(configFile);
25283
- try {
25284
- specs = readConfig(configFile).openapi;
25285
- } catch (e) {
25286
- return [{ severity: "error", message: e.message, line: 1, col: 1, length: 0 }];
25287
- }
25288
- }
25289
- specs = { ...specs, ...overrides };
25290
- for (const spec of Object.values(specs)) used.add(spec);
25291
- return Object.keys(specs).length ? checkOpenApi(diagram, specs) : [];
25292
- }
25293
24995
 
25294
24996
  // ../frontend/src/catalog/componentCatalog.ts
25295
24997
  var componentCatalog = [
@@ -25762,37 +25464,208 @@ var BracketCounter = class {
25762
25464
  }
25763
25465
  };
25764
25466
 
25765
- // ../frontend/src/dsl/paths.ts
25766
- var PARAM2 = /^\{[^{}/]+\}$/;
25767
- var ID_SEGMENT = /^(\d+|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
25768
- function segments(path) {
25769
- return path.split("?")[0].split("/");
25467
+ // ../frontend/src/dsl/format.ts
25468
+ function format(source2) {
25469
+ const entries2 = classify(source2.split("\n").map((l) => l.endsWith("\r") ? l.slice(0, -1) : l));
25470
+ const out = [];
25471
+ let run = [];
25472
+ const flush = () => {
25473
+ if (run.length) out.push(...renderRun(run));
25474
+ run = [];
25475
+ };
25476
+ tidyBlankLines(entries2).forEach((entry) => {
25477
+ const last = run[run.length - 1];
25478
+ if (last && (entry.kind !== last.kind || entry.depth !== last.depth || last.kind === "connection" && last.continuation.length || last.kind === "cells" && entry.kind === "cells" && entry.section !== last.section))
25479
+ flush();
25480
+ if (entry.kind === "node" || entry.kind === "connection" || entry.kind === "cells") run.push(entry);
25481
+ else {
25482
+ flush();
25483
+ out.push(entry.kind === "blank" ? "" : indent(entry.depth) + entry.text);
25484
+ }
25485
+ });
25486
+ flush();
25487
+ return out.length ? out.join("\n") + "\n" : "";
25770
25488
  }
25771
- function isTemplate(path) {
25772
- return segments(path).some((s) => PARAM2.test(s));
25489
+ var INDENT = " ";
25490
+ var KEYWORDS = /* @__PURE__ */ new Set(["title", "import", "group", "usecase", "par", "alt"]);
25491
+ function classify(lines) {
25492
+ const entries2 = [];
25493
+ let depth = 0;
25494
+ let section;
25495
+ for (let i = 0; i < lines.length; i++) {
25496
+ const raw = lines[i];
25497
+ const original = raw.trim();
25498
+ if (!original) {
25499
+ entries2.push({ kind: "blank", depth, text: "" });
25500
+ continue;
25501
+ }
25502
+ const { tokens, diagnostics } = tokenizeLine(raw, i + 1);
25503
+ if (diagnostics.some((d) => d.severity === "error") || tokens.length === 0) {
25504
+ entries2.push({ kind: "line", depth, text: original });
25505
+ continue;
25506
+ }
25507
+ const comment = commentOf(raw, tokens);
25508
+ const base = { depth, original, tokens, comment };
25509
+ const [first, second, third, fourth] = tokens;
25510
+ const inSection = section !== void 0 && depth === 1 && first.kind !== "rbrace";
25511
+ const cells = inSection ? sectionCells(section, raw, tokens) : void 0;
25512
+ if (cells) {
25513
+ entries2.push({ ...base, kind: "cells", section, cells });
25514
+ continue;
25515
+ }
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: [] };
25518
+ const brackets = new BracketCounter();
25519
+ if (fourth && brackets.feed(fourth.value) > 0) {
25520
+ const delta = indent(depth).length - width(leadingSpace(raw));
25521
+ while (i + 1 < lines.length) {
25522
+ const next = lines[++i];
25523
+ entry.continuation.push(reindent(next, delta));
25524
+ if (brackets.feed("\n" + next) <= 0) break;
25525
+ }
25526
+ while (entry.continuation[entry.continuation.length - 1] === "") entry.continuation.pop();
25527
+ }
25528
+ entries2.push(entry);
25529
+ continue;
25530
+ }
25531
+ 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))) {
25533
+ let k = 1;
25534
+ const take = (kind) => tokens[k]?.kind === kind ? source(raw, tokens[k++]) : "";
25535
+ const cells2 = [first.value, take("string"), take("tech"), take("team"), join(raw, tokens.slice(k))];
25536
+ entries2.push({ ...base, kind: "node", cells: cells2 });
25537
+ continue;
25538
+ }
25539
+ const closes = first.kind === "rbrace";
25540
+ const opens = tokens[tokens.length - 1].kind === "lbrace";
25541
+ const code = join(raw, tokens);
25542
+ const text = withComment(code, comment);
25543
+ entries2.push({ kind: "line", depth: Math.max(0, depth - (closes ? 1 : 0)), text: sameTokens(text, tokens) ? text : original, opens, closes });
25544
+ if (depth === 0) section = sectionOpenedBy(tokens);
25545
+ for (const t of tokens) depth = Math.max(0, depth + (t.kind === "lbrace" ? 1 : t.kind === "rbrace" ? -1 : 0));
25546
+ if (depth === 0) section = void 0;
25547
+ }
25548
+ return entries2;
25773
25549
  }
25774
- function matchesTemplate(template, path) {
25775
- const t = segments(template);
25776
- const p = segments(path);
25777
- return t.length === p.length && t.every((seg, i) => PARAM2.test(seg) ? p[i] !== "" : seg === p[i]);
25550
+ function sectionOpenedBy(tokens) {
25551
+ const [first, second] = tokens;
25552
+ if (first.kind !== "ident" || tokens[tokens.length - 1].kind !== "lbrace") return void 0;
25553
+ switch (first.value) {
25554
+ case "traffic":
25555
+ case "requirements":
25556
+ case "capacity":
25557
+ return second?.kind === "lbrace" ? first.value : void 0;
25558
+ case "entity":
25559
+ return second?.kind === "ident" ? "entity" : void 0;
25560
+ case "decision":
25561
+ case "test":
25562
+ return second?.kind === "string" ? first.value : void 0;
25563
+ default:
25564
+ return void 0;
25565
+ }
25778
25566
  }
25779
- function templatize(path) {
25780
- return segments(path).map((s) => ID_SEGMENT.test(s) ? "{id}" : s).join("/");
25567
+ function sectionCells(section, raw, tokens) {
25568
+ const [first, second] = tokens;
25569
+ if (tokens.some((t) => t.kind === "lbrace" || t.kind === "rbrace" || t.kind === "arrow" || t.kind === "label")) return void 0;
25570
+ const rest = (from) => tokens.length > from ? [join(raw, tokens.slice(from))] : [];
25571
+ switch (section) {
25572
+ case "traffic":
25573
+ return first.kind === "string" && (second?.kind === "quantity" || second?.kind === "number") ? [source(raw, first), source(raw, second), ...rest(2)] : void 0;
25574
+ case "capacity":
25575
+ return first.kind === "ident" && tokens.length > 1 ? [first.value, ...rest(1)] : void 0;
25576
+ case "entity":
25577
+ return first.kind === "ident" && second?.kind === "ident" ? [first.value, second.value, ...rest(2)] : void 0;
25578
+ default:
25579
+ return void 0;
25580
+ }
25781
25581
  }
25782
- function split(endpoint) {
25783
- const space = endpoint.indexOf(" ");
25784
- return space === -1 ? { method: "", path: endpoint } : { method: endpoint.slice(0, space), path: endpoint.slice(space + 1) };
25582
+ function tidyBlankLines(entries2) {
25583
+ const kept = [];
25584
+ for (const entry of entries2) {
25585
+ if (entry.kind !== "blank") {
25586
+ if (entry.kind === "line" && entry.closes) while (kept[kept.length - 1]?.kind === "blank") kept.pop();
25587
+ kept.push(entry);
25588
+ continue;
25589
+ }
25590
+ const prev = kept[kept.length - 1];
25591
+ if (!prev || prev.kind === "blank" || prev.kind === "line" && prev.opens) continue;
25592
+ kept.push(entry);
25593
+ }
25594
+ while (kept[kept.length - 1]?.kind === "blank") kept.pop();
25595
+ return kept;
25785
25596
  }
25786
- function endpointGroupKey(endpoint, all) {
25787
- if (isTemplate(endpoint)) return endpoint;
25788
- const { method, path } = split(endpoint);
25789
- const template = all.find((other) => {
25790
- const o = split(other);
25791
- return o.method === method && isTemplate(o.path) && matchesTemplate(o.path, path);
25597
+ function renderRun(run) {
25598
+ const codes = run[0].kind === "node" ? alignNodes(run) : run[0].kind === "cells" ? alignCells(run) : alignConnections(run);
25599
+ const items = run;
25600
+ const commented = items.filter((e) => e.comment);
25601
+ const commentCol = commented.length > 1 ? Math.max(...items.map((e, i) => e.comment ? len(codes[i]) : 0)) + 1 : 0;
25602
+ return items.flatMap((entry, i) => {
25603
+ const code = entry.comment && commentCol ? pad(codes[i], commentCol) + entry.comment : withComment(codes[i], entry.comment);
25604
+ const text = sameTokens(code, entry.tokens) ? code : entry.original;
25605
+ return [indent(entry.depth) + text, ...entry.kind === "connection" ? entry.continuation : []];
25792
25606
  });
25793
- if (template) return template;
25794
- return method ? `${method} ${templatize(path)}` : templatize(path);
25795
25607
  }
25608
+ function alignNodes(run) {
25609
+ const widths = [0, 1, 2, 3].map((c) => Math.max(...run.map((e) => len(e.cells[c]))));
25610
+ return run.map(({ cells }) => {
25611
+ let line = "";
25612
+ const columns = !cells[3] && cells[4] ? 3 : 4;
25613
+ for (let c = 0; c < columns; c++) if (widths[c]) line += pad(cells[c], widths[c] + 1);
25614
+ return (line + cells[4]).trimEnd();
25615
+ });
25616
+ }
25617
+ function alignCells(run) {
25618
+ const widths = [];
25619
+ for (const { cells } of run) cells.slice(0, -1).forEach((c, i) => widths[i] = Math.max(widths[i] ?? 0, len(c)));
25620
+ return run.map(({ cells }) => cells.map((c, i) => i < cells.length - 1 ? pad(c, widths[i] + 1) : c).join(""));
25621
+ }
25622
+ function alignConnections(run) {
25623
+ const head = Math.max(...run.map((e) => len(e.from) + 1 + e.arrow.length));
25624
+ const labelled = run.filter((e) => e.label !== void 0);
25625
+ const target = labelled.length ? Math.max(...labelled.map((e) => len(e.to))) : 0;
25626
+ return run.map((e) => {
25627
+ 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 : "");
25630
+ });
25631
+ }
25632
+ function commentOf(raw, tokens) {
25633
+ const last = tokens[tokens.length - 1];
25634
+ if (last.kind === "label") {
25635
+ const after2 = raw.slice(last.col);
25636
+ return after2.slice(stripTrailingComment(after2).length).trim();
25637
+ }
25638
+ return raw.slice(last.col - 1 + last.length).trim();
25639
+ }
25640
+ function join(raw, tokens) {
25641
+ let out = "";
25642
+ for (const [i, t] of tokens.entries()) {
25643
+ const text = t.kind === "label" ? ":" + (t.value ? " " + t.value : "") : source(raw, t);
25644
+ const tight = t.kind === "comma" || tokens[i - 1]?.kind === "comma" && t.kind === "number";
25645
+ out += (i === 0 || tight ? "" : " ") + text;
25646
+ }
25647
+ return out;
25648
+ }
25649
+ function sameTokens(text, tokens) {
25650
+ const { tokens: again, diagnostics } = tokenizeLine(text, 1);
25651
+ return diagnostics.length === 0 && again.length === tokens.length && again.every((t, i) => t.kind === tokens[i].kind && t.value === tokens[i].value);
25652
+ }
25653
+ function reindent(line, delta) {
25654
+ const lead = leadingSpace(line);
25655
+ const body = line.slice(lead.length).trimEnd();
25656
+ return body ? " ".repeat(Math.max(0, width(lead) + delta)) + body : "";
25657
+ }
25658
+ var source = (raw, t) => {
25659
+ const text = raw.slice(t.col - 1, t.col - 1 + t.length);
25660
+ return t.kind === "quantity" ? text.replace(/[ \t]+/g, " ") : text;
25661
+ };
25662
+ var withComment = (code, comment) => comment ? code + " " + comment : code;
25663
+ var leadingSpace = (line) => /^[ \t]*/.exec(line)[0];
25664
+ var width = (space) => [...space].reduce((n, ch) => n + (ch === " " ? INDENT.length : 1), 0);
25665
+ var MAX_INDENT = 32;
25666
+ var indent = (depth) => INDENT.repeat(Math.min(depth, MAX_INDENT));
25667
+ var len = (s) => [...s].length;
25668
+ var pad = (s, to) => s + " ".repeat(Math.max(0, to - len(s)));
25796
25669
 
25797
25670
  // ../frontend/src/dsl/kinds.ts
25798
25671
  var KINDS = ["client", "edge", "cdn", "loadbalancer", "gateway", "dns", "service", "function", "cache", "database", "search", "analytics", "queue", "storage", "external", "other"];
@@ -25866,6 +25739,38 @@ function kindMatches(actual, wanted) {
25866
25739
  return actual === wanted || wanted === "edge" && isEdge(actual);
25867
25740
  }
25868
25741
 
25742
+ // ../frontend/src/dsl/paths.ts
25743
+ var PARAM2 = /^\{[^{}/]+\}$/;
25744
+ var ID_SEGMENT = /^(\d+|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
25745
+ function segments(path) {
25746
+ return path.split("?")[0].split("/");
25747
+ }
25748
+ function isTemplate(path) {
25749
+ return segments(path).some((s) => PARAM2.test(s));
25750
+ }
25751
+ function matchesTemplate(template, path) {
25752
+ const t = segments(template);
25753
+ const p = segments(path);
25754
+ return t.length === p.length && t.every((seg, i) => PARAM2.test(seg) ? p[i] !== "" : seg === p[i]);
25755
+ }
25756
+ function templatize(path) {
25757
+ return segments(path).map((s) => ID_SEGMENT.test(s) ? "{id}" : s).join("/");
25758
+ }
25759
+ function split(endpoint) {
25760
+ const space = endpoint.indexOf(" ");
25761
+ return space === -1 ? { method: "", path: endpoint } : { method: endpoint.slice(0, space), path: endpoint.slice(space + 1) };
25762
+ }
25763
+ function endpointGroupKey(endpoint, all) {
25764
+ if (isTemplate(endpoint)) return endpoint;
25765
+ const { method, path } = split(endpoint);
25766
+ const template = all.find((other) => {
25767
+ const o = split(other);
25768
+ return o.method === method && isTemplate(o.path) && matchesTemplate(o.path, path);
25769
+ });
25770
+ if (template) return template;
25771
+ return method ? `${method} ${templatize(path)}` : templatize(path);
25772
+ }
25773
+
25869
25774
  // ../frontend/src/dsl/quantity.ts
25870
25775
  function parseQuantity(text) {
25871
25776
  const m = /^(\d+(?:\.\d+)?)(.*)$/.exec(text.replace(/\s+/g, ""));
@@ -25973,7 +25878,7 @@ var UNIT_EXAMPLES = {
25973
25878
  MBps: "a bandwidth, e.g. 500 MB/s or 1 GB/s",
25974
25879
  "usd/GB": "a price per gigabyte, e.g. 0.05 usd/GB"
25975
25880
  };
25976
- var CAPACITY_PARTS = "a rate, reads, writes, shards, latency, availability, cost, durable or volatile, consistency, bandwidth, egress or timeout";
25881
+ var CAPACITY_PARTS = "a rate, reads, writes, shards, size, latency, availability, cost, durable or volatile, consistency, bandwidth, egress or timeout";
25977
25882
  var ASSERTION_HELP = 'Expected an assertion: "Use case" calls <node>, "Use case" writes <node> before responding, <node> calls <node>, no path from <node> to <node>, <node> has replicas >= 2, \u2026';
25978
25883
  var ASSERTION_VERBS = "calls, every scenario calls, never calls, never waits for, writes, responds, starts at, has scenario or handles failure of";
25979
25884
  var GENERIC_TECH = {
@@ -26034,6 +25939,7 @@ var Parser = class {
26034
25939
  entities = [];
26035
25940
  decisions = [];
26036
25941
  tests = [];
25942
+ blocks = [];
26037
25943
  /** Checks of use case, scenario and node names, run once every file is read and use cases are built. */
26038
25944
  deferred = [];
26039
25945
  built = [];
@@ -26069,7 +25975,12 @@ var Parser = class {
26069
25975
  };
26070
25976
  const order = (d) => this.fileOrder.get(d.file) ?? 0;
26071
25977
  this.diagnostics.sort((a, b) => order(a) - order(b) || a.line - b.line || a.col - b.col);
26072
- return { diagram, diagnostics: this.diagnostics, ...this.imports.length ? { imports: this.imports } : {} };
25978
+ return {
25979
+ diagram,
25980
+ diagnostics: this.diagnostics,
25981
+ ...this.imports.length ? { imports: this.imports } : {},
25982
+ ...this.blocks.length ? { blocks: this.blocks } : {}
25983
+ };
26073
25984
  }
26074
25985
  /** Reads the current file's lines and reports blocks it leaves open. */
26075
25986
  parseFile() {
@@ -26105,6 +26016,7 @@ var Parser = class {
26105
26016
  if (first.kind === "rbrace") {
26106
26017
  this.stack.pop();
26107
26018
  if (!top.discard) this.closeSection(top);
26019
+ if (!top.discard && this.file === void 0) this.blocks.push({ kind: top.section, start: top.loc.line, end: line });
26108
26020
  if (second) this.error("Unexpected input after }", loc(second));
26109
26021
  } else if (!top.discard) {
26110
26022
  this.parseSectionLine(top, tokens, line);
@@ -26769,6 +26681,15 @@ var Parser = class {
26769
26681
  }
26770
26682
  override.shards = Number(n.value);
26771
26683
  i += 2;
26684
+ } else if (isWord(t, "size")) {
26685
+ part = "size";
26686
+ const value = tokens[i + 1];
26687
+ if (!isWord(value, "S") && !isWord(value, "M") && !isWord(value, "L")) {
26688
+ this.error(`Expected S, M or L after size${value ? `, not ${describe(value)}` : ""}`, this.locOf(value ?? t, line));
26689
+ return;
26690
+ }
26691
+ override.size = value.value;
26692
+ i += 2;
26772
26693
  } else if (isWord(t, "consistency")) {
26773
26694
  part = "consistency";
26774
26695
  const value = tokens[i + 1];
@@ -27406,143 +27327,446 @@ var Parser = class {
27406
27327
  const last = tokens[tokens.length - 1];
27407
27328
  return { line, col: first.col, length: last.col + last.length - first.col, ...this.fileField() };
27408
27329
  }
27409
- locOf(token, line) {
27410
- return token ? { line, col: token.col, length: token.length, ...this.fileField() } : { line, col: 1, length: 1, ...this.fileField() };
27330
+ locOf(token, line) {
27331
+ return token ? { line, col: token.col, length: token.length, ...this.fileField() } : { line, col: 1, length: 1, ...this.fileField() };
27332
+ }
27333
+ /** `{ file }` inside an imported file; nothing for the root document, whose locations stay as they were. */
27334
+ fileField() {
27335
+ return this.file === void 0 ? {} : { file: this.file };
27336
+ }
27337
+ error(message, loc) {
27338
+ this.diagnostics.push({ severity: "error", message, ...loc });
27339
+ }
27340
+ warning(message, loc) {
27341
+ const key = `${loc.file ?? ""}
27342
+ ${loc.line}
27343
+ ${loc.col}
27344
+ ${message}`;
27345
+ if (this.warned.has(key)) return;
27346
+ this.warned.add(key);
27347
+ this.diagnostics.push({ severity: "warning", message, ...loc });
27348
+ }
27349
+ };
27350
+ function expand(items) {
27351
+ let paths = [{ names: [], conditions: [], locs: [], steps: [] }];
27352
+ for (const item of items) {
27353
+ if (item.kind === "step") {
27354
+ for (const path of paths) path.steps.push(item.step);
27355
+ continue;
27356
+ }
27357
+ const next = [];
27358
+ for (const path of paths) {
27359
+ for (const branch of item.branches) {
27360
+ for (const sub of expand(branch.items)) {
27361
+ next.push({
27362
+ names: [...path.names, branch.name, ...sub.names],
27363
+ conditions: [...path.conditions, ...branch.condition ? [branch.condition] : [], ...sub.conditions],
27364
+ locs: [...path.locs, branch.loc, ...sub.locs],
27365
+ steps: [...path.steps, ...sub.steps]
27366
+ });
27367
+ }
27368
+ }
27369
+ if (next.length > MAX_SCENARIOS * 4) break;
27370
+ }
27371
+ paths = next;
27372
+ }
27373
+ return paths;
27374
+ }
27375
+ function isWord(token, word) {
27376
+ return token?.kind === "ident" && token.value === word;
27377
+ }
27378
+ function sectionName(frame) {
27379
+ if (frame.skipped) return frame.skipped;
27380
+ if (frame.entity) return `entity '${frame.entity.name}'`;
27381
+ if (frame.decision) return `decision '${frame.decision.title}'`;
27382
+ if (frame.test) return `test '${frame.test.name}'`;
27383
+ return `${frame.section} block`;
27384
+ }
27385
+ function fileName(path) {
27386
+ return path.split(/[\\/]/).pop() || path;
27387
+ }
27388
+ function slug(text) {
27389
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
27390
+ }
27391
+ function splitPayload(input) {
27392
+ const text = input.trim();
27393
+ const formats = { json: "JSON", xml: "XML", text: "FREE_TEXT" };
27394
+ const leading = text.match(/^(json|xml|text)\s+([\s\S]+)$/i);
27395
+ if (leading) return { text: "", format: formats[leading[1].toLowerCase()], body: leading[2].trim() };
27396
+ const keyed = text.match(/^([\s\S]*?)\s+(json|xml)\s+([{[<][\s\S]*)$/i);
27397
+ if (keyed) return { text: keyed[1].trim(), format: formats[keyed[2].toLowerCase()], body: keyed[3].trim() };
27398
+ const bare = text.search(/(^|\s)[{[<]/);
27399
+ if (bare !== -1) {
27400
+ const body = text.slice(bare).trim();
27401
+ return { text: text.slice(0, bare).trim(), format: body.startsWith("<") ? "XML" : "JSON", body };
27402
+ }
27403
+ return { text: unquote(text), format: "JSON" };
27404
+ }
27405
+ function parseStepLabel(input) {
27406
+ const http = input.trim().match(/^([A-Z]+)\s+(\S+)\s*([\s\S]*)$/);
27407
+ if (http && HTTP_METHODS.includes(http[1])) {
27408
+ const payload2 = splitPayload(http[3]);
27409
+ return {
27410
+ name: `${http[1]} ${http[2]}`,
27411
+ method: http[1],
27412
+ endpoint: http[2],
27413
+ description: payload2.text || void 0,
27414
+ format: payload2.body ? payload2.format : void 0,
27415
+ body: payload2.body
27416
+ };
27417
+ }
27418
+ const payload = splitPayload(input);
27419
+ return { name: payload.text, format: payload.body ? payload.format : void 0, body: payload.body };
27420
+ }
27421
+ var SIZE_UNITS = { B: 1, KB: 1e3, MB: 1e6, GB: 1e9, TB: 1e12 };
27422
+ function parseSize(text) {
27423
+ const m = /^(\d+(?:\.\d+)?)([A-Za-z]*)$/.exec(text);
27424
+ if (!m) return { error: `Expected a payload size after ~, e.g. ~2MB, ~500KB or ~1.5GB${text ? `, not '~${text}'` : ""}` };
27425
+ const factor = SIZE_UNITS[m[2]];
27426
+ if (factor === void 0) return { error: m[2] ? `Unknown size unit '${m[2]}' in '~${text}'; use B, KB, MB, GB or TB` : `'~${text}' needs a unit: B, KB, MB, GB or TB` };
27427
+ const value = Math.round(Number(m[1]) * factor);
27428
+ if (value <= 0) return { error: `A payload size must be more than 0, not '~${text}'` };
27429
+ return { value };
27430
+ }
27431
+ function inferProtocol(isAsync, method, target) {
27432
+ if (isAsync || target?.type === "queue") return "MESSAGING";
27433
+ if (!method) return "OTHER";
27434
+ switch (target?.techStack) {
27435
+ case "GraphQL":
27436
+ return "GRAPHQL";
27437
+ case "gRPC":
27438
+ return "GRPC";
27439
+ case "SOAP API":
27440
+ return "SOAP";
27441
+ default:
27442
+ return "REST";
27443
+ }
27444
+ }
27445
+ function unquote(text) {
27446
+ const t = text.trim();
27447
+ return t.length >= 2 && t.startsWith('"') && t.endsWith('"') ? t.slice(1, -1) : t;
27448
+ }
27449
+ function describe(token) {
27450
+ switch (token.kind) {
27451
+ case "string":
27452
+ return `string "${token.value}"`;
27453
+ case "tech":
27454
+ return `[${token.value}]`;
27455
+ case "team":
27456
+ return `@${token.value}`;
27457
+ case "label":
27458
+ return "':'";
27459
+ case "lbrace":
27460
+ case "rbrace":
27461
+ case "comma":
27462
+ case "arrow":
27463
+ return `'${token.value}'`;
27464
+ default:
27465
+ return `'${token.value}'`;
27466
+ }
27467
+ }
27468
+
27469
+ // ../frontend/src/dsl/importOpenApi.ts
27470
+ var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
27471
+ function serverPrefix(servers) {
27472
+ const server = Array.isArray(servers) ? servers[0] : void 0;
27473
+ if (!isObject(server) || typeof server.url !== "string") return "";
27474
+ const vars = isObject(server.variables) ? server.variables : {};
27475
+ const url = server.url.replace(/\{([^}]+)\}/g, (_, name) => {
27476
+ const v = vars[name];
27477
+ return isObject(v) && v.default !== void 0 ? String(v.default) : "";
27478
+ });
27479
+ const path = url.match(/^[a-z][a-z0-9+.-]*:\/\/[^/]*(\/.*)?$/i)?.[1] ?? (url.startsWith("/") ? url : "");
27480
+ return path.replace(/[?#].*$/, "").replace(/\/+$/, "");
27481
+ }
27482
+
27483
+ // src/openapi/spec.ts
27484
+ var METHODS = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
27485
+ var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
27486
+ var esc = (key) => key.replace(/~/g, "~0").replace(/\//g, "~1");
27487
+ var OpenApiSpec = class {
27488
+ constructor(file, doc) {
27489
+ this.file = file;
27490
+ this.name = (0, import_node_path.basename)(file);
27491
+ const version = String(doc.openapi ?? "");
27492
+ if (!/^3\./.test(version)) {
27493
+ throw new Error(doc.swagger ? "Swagger 2.0 is not supported; convert it to OpenAPI 3" : "not an OpenAPI 3 document (no openapi: 3.x field)");
27494
+ }
27495
+ this.doc = version.startsWith("3.0") ? from30(structuredClone(doc)) : doc;
27496
+ this.prefix = serverPrefix(this.doc.servers);
27497
+ this.ajv = new import__.Ajv2020({ strict: false, allErrors: true, validateSchema: false, validateFormats: false });
27498
+ this.ajv.addSchema(this.doc, "openapi:spec");
27499
+ }
27500
+ file;
27501
+ /** The file name, for messages. */
27502
+ name;
27503
+ /** Path prefix from `servers[0].url`, e.g. `/v1`; empty when there is none. */
27504
+ prefix;
27505
+ doc;
27506
+ ajv;
27507
+ validators = /* @__PURE__ */ new Map();
27508
+ /** Path templates the spec documents. */
27509
+ get templates() {
27510
+ return isObject2(this.doc.paths) ? Object.keys(this.doc.paths) : [];
27511
+ }
27512
+ /** Methods (upper case) documented for a path template. */
27513
+ methods(path) {
27514
+ const item = this.resolve(`/paths/${esc(path)}`)?.value;
27515
+ return isObject2(item) ? METHODS.filter((m) => isObject2(item[m])).map((m) => m.toUpperCase()) : [];
27516
+ }
27517
+ operation(path, method) {
27518
+ const item = this.resolve(`/paths/${esc(path)}`);
27519
+ if (!item || !isObject2(item.value)) return void 0;
27520
+ const m = method.toLowerCase();
27521
+ const value = item.value[m];
27522
+ return isObject2(value) ? { path, method: method.toUpperCase(), pointer: `${item.pointer}/${m}`, value } : void 0;
27523
+ }
27524
+ /** Documented response keys of an operation: `201`, `4XX`, `default`. */
27525
+ statuses(op) {
27526
+ const responses = op.value.responses;
27527
+ return isObject2(responses) ? Object.keys(responses) : [];
27528
+ }
27529
+ /** The response key that covers a status code: exact, then a `4XX` range, then `default`. */
27530
+ responseKey(op, status) {
27531
+ const keys = this.statuses(op);
27532
+ return keys.find((k) => k === String(status)) ?? keys.find((k) => k.toUpperCase() === `${String(status)[0]}XX`) ?? keys.find((k) => k === "default");
27533
+ }
27534
+ /** Validates a request body; undefined when the operation has no JSON schema for it. */
27535
+ validateRequest(op, body) {
27536
+ const requestBody = this.resolve(`${op.pointer}/requestBody`);
27537
+ return requestBody && this.validateContent(requestBody.pointer, body);
27538
+ }
27539
+ validateResponse(op, key, body) {
27540
+ const response2 = this.resolve(`${op.pointer}/responses/${esc(key)}`);
27541
+ return response2 && this.validateContent(response2.pointer, body);
27542
+ }
27543
+ /** Validates against the `application/json` (or `+json`) schema of a request body or response object. */
27544
+ validateContent(pointer, body) {
27545
+ const owner = this.resolve(pointer)?.value;
27546
+ if (!isObject2(owner) || !isObject2(owner.content)) return void 0;
27547
+ const type = Object.keys(owner.content).find((t) => /^application\/([\w.-]+\+)?json\b/i.test(t));
27548
+ if (!type || !isObject2(owner.content[type]) || !("schema" in owner.content[type])) return void 0;
27549
+ const schemaPointer = `${pointer}/content/${esc(type)}/schema`;
27550
+ let validate2 = this.validators.get(schemaPointer);
27551
+ if (validate2 === void 0) {
27552
+ try {
27553
+ validate2 = this.ajv.compile({ $ref: `openapi:spec#${schemaPointer}` });
27554
+ } catch {
27555
+ validate2 = null;
27556
+ }
27557
+ this.validators.set(schemaPointer, validate2);
27558
+ }
27559
+ if (!validate2) return void 0;
27560
+ return validate2(body) ? [] : (validate2.errors ?? []).map(describeError);
27561
+ }
27562
+ /** Follows local `$ref`s from the value at a JSON pointer; returns the final pointer and value. */
27563
+ resolve(pointer) {
27564
+ let value = this.at(pointer);
27565
+ for (let hops = 0; isObject2(value) && typeof value.$ref === "string" && hops < 20; hops++) {
27566
+ if (!value.$ref.startsWith("#")) return void 0;
27567
+ pointer = decodeURIComponent(value.$ref.slice(1));
27568
+ value = this.at(pointer);
27569
+ }
27570
+ return value === void 0 ? void 0 : { pointer, value };
27411
27571
  }
27412
- /** `{ file }` inside an imported file; nothing for the root document, whose locations stay as they were. */
27413
- fileField() {
27414
- return this.file === void 0 ? {} : { file: this.file };
27572
+ at(pointer) {
27573
+ let value = this.doc;
27574
+ for (const raw of pointer.split("/").slice(1)) {
27575
+ const key = raw.replace(/~1/g, "/").replace(/~0/g, "~");
27576
+ if (Array.isArray(value)) value = value[Number(key)];
27577
+ else if (isObject2(value)) value = value[key];
27578
+ else return void 0;
27579
+ }
27580
+ return value;
27415
27581
  }
27416
- error(message, loc) {
27417
- this.diagnostics.push({ severity: "error", message, ...loc });
27582
+ };
27583
+ function describeError(e) {
27584
+ const where = e.instancePath || "(root)";
27585
+ const p = e.params;
27586
+ let message = e.message ?? "is invalid";
27587
+ if (e.keyword === "additionalProperties" || e.keyword === "unevaluatedProperties") message += ` ('${p.additionalProperty ?? p.unevaluatedProperty}')`;
27588
+ if (e.keyword === "enum" && Array.isArray(p.allowedValues)) message += `: ${p.allowedValues.map((v) => JSON.stringify(v)).join(", ")}`;
27589
+ return `${where} ${message}`;
27590
+ }
27591
+ function from30(value) {
27592
+ if (Array.isArray(value)) return value.map(from30);
27593
+ if (!isObject2(value)) return value;
27594
+ const out = {};
27595
+ for (const [k, v] of Object.entries(value)) out[k] = from30(v);
27596
+ for (const [bool, bound] of [
27597
+ ["exclusiveMinimum", "minimum"],
27598
+ ["exclusiveMaximum", "maximum"]
27599
+ ]) {
27600
+ if (typeof out[bool] !== "boolean") continue;
27601
+ if (out[bool] && typeof out[bound] === "number") {
27602
+ out[bool] = out[bound];
27603
+ delete out[bound];
27604
+ } else delete out[bool];
27418
27605
  }
27419
- warning(message, loc) {
27420
- const key = `${loc.file ?? ""}
27421
- ${loc.line}
27422
- ${loc.col}
27423
- ${message}`;
27424
- if (this.warned.has(key)) return;
27425
- this.warned.add(key);
27426
- this.diagnostics.push({ severity: "warning", message, ...loc });
27606
+ if (typeof out.nullable === "boolean") {
27607
+ const nullable = out.nullable;
27608
+ delete out.nullable;
27609
+ if (nullable) {
27610
+ if (typeof out.type === "string" && !("$ref" in out)) {
27611
+ out.type = [out.type, "null"];
27612
+ if (Array.isArray(out.enum) && !out.enum.includes(null)) out.enum = [...out.enum, null];
27613
+ } else return { anyOf: [out, { type: "null" }] };
27614
+ }
27427
27615
  }
27428
- };
27429
- function expand(items) {
27430
- let paths = [{ names: [], conditions: [], locs: [], steps: [] }];
27431
- for (const item of items) {
27432
- if (item.kind === "step") {
27433
- for (const path of paths) path.steps.push(item.step);
27434
- continue;
27616
+ return out;
27617
+ }
27618
+ var cache = /* @__PURE__ */ new Map();
27619
+ function loadSpec(file) {
27620
+ const { mtimeMs } = (0, import_node_fs.statSync)(file);
27621
+ const cached = cache.get(file);
27622
+ if (cached?.mtimeMs === mtimeMs) return cached.spec;
27623
+ const text = (0, import_node_fs.readFileSync)(file, "utf8");
27624
+ const doc = file.endsWith(".json") ? JSON.parse(text) : (0, import_yaml.parse)(text);
27625
+ if (!isObject2(doc)) throw new Error("not an OpenAPI document");
27626
+ const spec = new OpenApiSpec(file, doc);
27627
+ cache.set(file, { mtimeMs, spec });
27628
+ return spec;
27629
+ }
27630
+
27631
+ // src/openapi/check.ts
27632
+ var MAX_SCHEMA_ERRORS = 3;
27633
+ function checkOpenApi(diagram, specFiles) {
27634
+ const diagnostics = [];
27635
+ const add = (severity, message, loc) => {
27636
+ if (!diagnostics.some((d) => d.message === message && d.line === loc.line && d.col === loc.col && d.file === loc.file)) diagnostics.push({ severity, message, ...loc });
27637
+ };
27638
+ const specs = /* @__PURE__ */ new Map();
27639
+ const specOf = (nodeId) => {
27640
+ if (specs.has(nodeId)) return specs.get(nodeId);
27641
+ let spec = null;
27642
+ try {
27643
+ spec = loadSpec(specFiles[nodeId]);
27644
+ } catch (e) {
27645
+ const node = diagram.nodes.find((n) => n.id === nodeId);
27646
+ add("error", `Cannot read OpenAPI spec '${displayPath(specFiles[nodeId])}' for '${nodeId}': ${e.message}`, node.loc);
27435
27647
  }
27436
- const next = [];
27437
- for (const path of paths) {
27438
- for (const branch of item.branches) {
27439
- for (const sub of expand(branch.items)) {
27440
- next.push({
27441
- names: [...path.names, branch.name, ...sub.names],
27442
- conditions: [...path.conditions, ...branch.condition ? [branch.condition] : [], ...sub.conditions],
27443
- locs: [...path.locs, branch.loc, ...sub.locs],
27444
- steps: [...path.steps, ...sub.steps]
27445
- });
27446
- }
27648
+ specs.set(nodeId, spec);
27649
+ return spec;
27650
+ };
27651
+ for (const useCase of diagram.useCases) {
27652
+ for (const scenario of useCase.scenarios) {
27653
+ for (const step of scenario.steps) {
27654
+ if (!step.httpMethod || !Object.hasOwn(specFiles, step.toServiceId)) continue;
27655
+ const spec = specOf(step.toServiceId);
27656
+ if (spec) checkStep(spec, step, (message, loc) => add("warning", message, loc));
27447
27657
  }
27448
- if (next.length > MAX_SCENARIOS * 4) break;
27449
27658
  }
27450
- paths = next;
27451
27659
  }
27452
- return paths;
27453
- }
27454
- function isWord(token, word) {
27455
- return token?.kind === "ident" && token.value === word;
27456
- }
27457
- function sectionName(frame) {
27458
- if (frame.skipped) return frame.skipped;
27459
- if (frame.entity) return `entity '${frame.entity.name}'`;
27460
- if (frame.decision) return `decision '${frame.decision.title}'`;
27461
- if (frame.test) return `test '${frame.test.name}'`;
27462
- return `${frame.section} block`;
27660
+ return diagnostics.sort((a, b) => a.line - b.line || a.col - b.col);
27463
27661
  }
27464
- function fileName(path) {
27465
- return path.split(/[\\/]/).pop() || path;
27662
+ function checkStep(spec, step, warn) {
27663
+ const call = `${step.httpMethod} ${step.endpoint}`;
27664
+ const path = normalizePath(step.endpoint);
27665
+ const candidates = [path, stripPrefix(path, spec.prefix)].filter((p) => p !== void 0);
27666
+ const templates = [...new Set(candidates.flatMap((p) => matchTemplates(spec.templates, p)))];
27667
+ if (templates.length === 0) {
27668
+ const shown = stripPrefix(path, spec.prefix) ?? path;
27669
+ const near = closestTemplate(spec.templates, shown);
27670
+ const nearMethod = near && (spec.methods(near).includes(step.httpMethod) ? step.httpMethod : spec.methods(near)[0]);
27671
+ const hint = near ? `; did you mean ${nearMethod ? `${nearMethod} ` : ""}${near}?` : "";
27672
+ warn(`${call}: '${spec.name}' has no path ${shown}${hint}`, step.loc);
27673
+ return;
27674
+ }
27675
+ let op;
27676
+ for (const t of templates) op ??= spec.operation(t, step.httpMethod);
27677
+ if (!op) {
27678
+ const documented = templates.flatMap((t) => spec.methods(t).map((m) => `${m} ${t}`));
27679
+ const hint = documented.length === 1 ? `; did you mean ${documented[0]}?` : documented.length ? ` (documented: ${documented.join(", ")})` : "";
27680
+ warn(`${call}: '${spec.name}' has no ${step.httpMethod} ${templates[0]}${hint}`, step.loc);
27681
+ return;
27682
+ }
27683
+ if (step.failed) return;
27684
+ const label = `${op.method} ${op.path}`;
27685
+ const request2 = jsonBody(step.requestFormat, step.requestBody);
27686
+ if (request2 !== void 0) {
27687
+ const errors = spec.validateRequest(op, request2.value);
27688
+ if (errors?.length) warn(`Request body does not match the schema of ${label}: ${summarize(errors)}`, step.loc);
27689
+ }
27690
+ if (step.statusCode === void 0) return;
27691
+ const responseLoc = step.responseLoc ?? step.loc;
27692
+ const key = spec.responseKey(op, step.statusCode);
27693
+ if (!key) {
27694
+ const documented = spec.statuses(op);
27695
+ warn(`Status ${step.statusCode} is not documented for ${label} (documented: ${documented.join(", ") || "none"})`, responseLoc);
27696
+ return;
27697
+ }
27698
+ const response2 = jsonBody(step.responseFormat, step.responseBody);
27699
+ if (response2 !== void 0) {
27700
+ const errors = spec.validateResponse(op, key, response2.value);
27701
+ if (errors?.length) warn(`${step.statusCode} response body does not match the schema of ${label}: ${summarize(errors)}`, responseLoc);
27702
+ }
27466
27703
  }
27467
- function slug(text) {
27468
- return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
27704
+ function displayPath(file) {
27705
+ const rel = (0, import_node_path2.relative)(process.cwd(), file);
27706
+ return rel && !rel.startsWith("..") && !(0, import_node_path2.isAbsolute)(rel) ? rel : file;
27469
27707
  }
27470
- function splitPayload(input) {
27471
- const text = input.trim();
27472
- const formats = { json: "JSON", xml: "XML", text: "FREE_TEXT" };
27473
- const leading = text.match(/^(json|xml|text)\s+([\s\S]+)$/i);
27474
- if (leading) return { text: "", format: formats[leading[1].toLowerCase()], body: leading[2].trim() };
27475
- const keyed = text.match(/^([\s\S]*?)\s+(json|xml)\s+([{[<][\s\S]*)$/i);
27476
- if (keyed) return { text: keyed[1].trim(), format: formats[keyed[2].toLowerCase()], body: keyed[3].trim() };
27477
- const bare = text.search(/(^|\s)[{[<]/);
27478
- if (bare !== -1) {
27479
- const body = text.slice(bare).trim();
27480
- return { text: text.slice(0, bare).trim(), format: body.startsWith("<") ? "XML" : "JSON", body };
27708
+ function jsonBody(format2, body) {
27709
+ if (format2 !== "JSON" || !body) return void 0;
27710
+ try {
27711
+ return { value: JSON.parse(body) };
27712
+ } catch {
27713
+ return void 0;
27481
27714
  }
27482
- return { text: unquote(text), format: "JSON" };
27483
27715
  }
27484
- function parseStepLabel(input) {
27485
- const http = input.trim().match(/^([A-Z]+)\s+(\S+)\s*([\s\S]*)$/);
27486
- if (http && HTTP_METHODS.includes(http[1])) {
27487
- const payload2 = splitPayload(http[3]);
27488
- return {
27489
- name: `${http[1]} ${http[2]}`,
27490
- method: http[1],
27491
- endpoint: http[2],
27492
- description: payload2.text || void 0,
27493
- format: payload2.body ? payload2.format : void 0,
27494
- body: payload2.body
27495
- };
27716
+ function summarize(errors) {
27717
+ const shown = errors.slice(0, MAX_SCHEMA_ERRORS).join("; ");
27718
+ return errors.length > MAX_SCHEMA_ERRORS ? `${shown} (and ${errors.length - MAX_SCHEMA_ERRORS} more)` : shown;
27719
+ }
27720
+
27721
+ // src/openapi/config.ts
27722
+ var CONFIG_FILE = "proschi.json";
27723
+ function findConfigFile(dir) {
27724
+ for (let current = (0, import_node_path3.resolve)(dir); ; current = (0, import_node_path3.dirname)(current)) {
27725
+ const candidate = (0, import_node_path3.join)(current, CONFIG_FILE);
27726
+ if ((0, import_node_fs2.existsSync)(candidate) && (0, import_node_fs2.statSync)(candidate).isFile()) return candidate;
27727
+ if ((0, import_node_path3.dirname)(current) === current) return void 0;
27496
27728
  }
27497
- const payload = splitPayload(input);
27498
- return { name: payload.text, format: payload.body ? payload.format : void 0, body: payload.body };
27499
27729
  }
27500
- var SIZE_UNITS = { B: 1, KB: 1e3, MB: 1e6, GB: 1e9, TB: 1e12 };
27501
- function parseSize(text) {
27502
- const m = /^(\d+(?:\.\d+)?)([A-Za-z]*)$/.exec(text);
27503
- if (!m) return { error: `Expected a payload size after ~, e.g. ~2MB, ~500KB or ~1.5GB${text ? `, not '~${text}'` : ""}` };
27504
- const factor = SIZE_UNITS[m[2]];
27505
- if (factor === void 0) return { error: m[2] ? `Unknown size unit '${m[2]}' in '~${text}'; use B, KB, MB, GB or TB` : `'~${text}' needs a unit: B, KB, MB, GB or TB` };
27506
- const value = Math.round(Number(m[1]) * factor);
27507
- if (value <= 0) return { error: `A payload size must be more than 0, not '~${text}'` };
27508
- return { value };
27730
+ var used = /* @__PURE__ */ new Set();
27731
+ function watchedFiles() {
27732
+ return [...used];
27509
27733
  }
27510
- function inferProtocol(isAsync, method, target) {
27511
- if (isAsync || target?.type === "queue") return "MESSAGING";
27512
- if (!method) return "OTHER";
27513
- switch (target?.techStack) {
27514
- case "GraphQL":
27515
- return "GRAPHQL";
27516
- case "gRPC":
27517
- return "GRPC";
27518
- case "SOAP API":
27519
- return "SOAP";
27520
- default:
27521
- return "REST";
27734
+ var configs = /* @__PURE__ */ new Map();
27735
+ function readConfig(file) {
27736
+ const { mtimeMs } = (0, import_node_fs2.statSync)(file);
27737
+ const cached = configs.get(file);
27738
+ if (cached?.mtimeMs === mtimeMs) return cached.config;
27739
+ let json;
27740
+ try {
27741
+ json = JSON.parse((0, import_node_fs2.readFileSync)(file, "utf8"));
27742
+ } catch (e) {
27743
+ throw new Error(`Invalid ${file}: ${e.message}`);
27522
27744
  }
27745
+ const openapi = json?.openapi ?? {};
27746
+ if (typeof openapi !== "object" || openapi === null || Array.isArray(openapi) || Object.values(openapi).some((v) => typeof v !== "string")) {
27747
+ throw new Error(`Invalid ${file}: "openapi" must map node ids to spec paths, e.g. {"openapi": {"orders": "specs/orders.yaml"}}`);
27748
+ }
27749
+ const config = {
27750
+ file,
27751
+ openapi: Object.fromEntries(Object.entries(openapi).map(([id, path]) => [id, (0, import_node_path3.resolve)((0, import_node_path3.dirname)(file), path)]))
27752
+ };
27753
+ configs.set(file, { mtimeMs, config });
27754
+ return config;
27523
27755
  }
27524
- function unquote(text) {
27525
- const t = text.trim();
27526
- return t.length >= 2 && t.startsWith('"') && t.endsWith('"') ? t.slice(1, -1) : t;
27527
- }
27528
- function describe(token) {
27529
- switch (token.kind) {
27530
- case "string":
27531
- return `string "${token.value}"`;
27532
- case "tech":
27533
- return `[${token.value}]`;
27534
- case "team":
27535
- return `@${token.value}`;
27536
- case "label":
27537
- return "':'";
27538
- case "lbrace":
27539
- case "rbrace":
27540
- case "comma":
27541
- case "arrow":
27542
- return `'${token.value}'`;
27543
- default:
27544
- return `'${token.value}'`;
27756
+ function openApiDiagnostics(file, diagram, overrides = {}) {
27757
+ let specs = {};
27758
+ const configFile = findConfigFile((0, import_node_path3.dirname)((0, import_node_path3.resolve)(file)));
27759
+ if (configFile) {
27760
+ used.add(configFile);
27761
+ try {
27762
+ specs = readConfig(configFile).openapi;
27763
+ } catch (e) {
27764
+ return [{ severity: "error", message: e.message, line: 1, col: 1, length: 0 }];
27765
+ }
27545
27766
  }
27767
+ specs = { ...specs, ...overrides };
27768
+ for (const spec of Object.values(specs)) used.add(spec);
27769
+ return Object.keys(specs).length ? checkOpenApi(diagram, specs) : [];
27546
27770
  }
27547
27771
 
27548
27772
  // ../frontend/src/dsl/edit.ts
@@ -27569,209 +27793,6 @@ ${text}
27569
27793
  `;
27570
27794
  }
27571
27795
 
27572
- // ../frontend/src/dsl/format.ts
27573
- function format(source2) {
27574
- const entries2 = classify(source2.split("\n").map((l) => l.endsWith("\r") ? l.slice(0, -1) : l));
27575
- const out = [];
27576
- let run = [];
27577
- const flush = () => {
27578
- if (run.length) out.push(...renderRun(run));
27579
- run = [];
27580
- };
27581
- tidyBlankLines(entries2).forEach((entry) => {
27582
- const last = run[run.length - 1];
27583
- if (last && (entry.kind !== last.kind || entry.depth !== last.depth || last.kind === "connection" && last.continuation.length || last.kind === "cells" && entry.kind === "cells" && entry.section !== last.section))
27584
- flush();
27585
- if (entry.kind === "node" || entry.kind === "connection" || entry.kind === "cells") run.push(entry);
27586
- else {
27587
- flush();
27588
- out.push(entry.kind === "blank" ? "" : indent(entry.depth) + entry.text);
27589
- }
27590
- });
27591
- flush();
27592
- return out.length ? out.join("\n") + "\n" : "";
27593
- }
27594
- var INDENT = " ";
27595
- var KEYWORDS = /* @__PURE__ */ new Set(["title", "import", "group", "usecase", "par", "alt"]);
27596
- function classify(lines) {
27597
- const entries2 = [];
27598
- let depth = 0;
27599
- let section;
27600
- for (let i = 0; i < lines.length; i++) {
27601
- const raw = lines[i];
27602
- const original = raw.trim();
27603
- if (!original) {
27604
- entries2.push({ kind: "blank", depth, text: "" });
27605
- continue;
27606
- }
27607
- const { tokens, diagnostics } = tokenizeLine(raw, i + 1);
27608
- if (diagnostics.some((d) => d.severity === "error") || tokens.length === 0) {
27609
- entries2.push({ kind: "line", depth, text: original });
27610
- continue;
27611
- }
27612
- const comment = commentOf(raw, tokens);
27613
- const base = { depth, original, tokens, comment };
27614
- const [first, second, third, fourth] = tokens;
27615
- const inSection = section !== void 0 && depth === 1 && first.kind !== "rbrace";
27616
- const cells = inSection ? sectionCells(section, raw, tokens) : void 0;
27617
- if (cells) {
27618
- entries2.push({ ...base, kind: "cells", section, cells });
27619
- continue;
27620
- }
27621
- if (!inSection && first.kind === "ident" && second?.kind === "arrow" && third?.kind === "ident" && tokens.length <= 4 && (!fourth || fourth.kind === "label")) {
27622
- const entry = { ...base, kind: "connection", from: first.value, arrow: second.value, to: third.value, label: fourth?.value, continuation: [] };
27623
- const brackets = new BracketCounter();
27624
- if (fourth && brackets.feed(fourth.value) > 0) {
27625
- const delta = indent(depth).length - width(leadingSpace(raw));
27626
- while (i + 1 < lines.length) {
27627
- const next = lines[++i];
27628
- entry.continuation.push(reindent(next, delta));
27629
- if (brackets.feed("\n" + next) <= 0) break;
27630
- }
27631
- while (entry.continuation[entry.continuation.length - 1] === "") entry.continuation.pop();
27632
- }
27633
- entries2.push(entry);
27634
- continue;
27635
- }
27636
- const oneLineDecision = first.value === "decision" && second?.kind === "string" && third?.kind === "ident" && third.value === "because";
27637
- if (!inSection && first.kind === "ident" && !KEYWORDS.has(first.value) && !oneLineDecision && tokens.slice(1).every((t) => ["string", "tech", "team", "ident", "number", "comma"].includes(t.kind))) {
27638
- let k = 1;
27639
- const take = (kind) => tokens[k]?.kind === kind ? source(raw, tokens[k++]) : "";
27640
- const cells2 = [first.value, take("string"), take("tech"), take("team"), join2(raw, tokens.slice(k))];
27641
- entries2.push({ ...base, kind: "node", cells: cells2 });
27642
- continue;
27643
- }
27644
- const closes = first.kind === "rbrace";
27645
- const opens = tokens[tokens.length - 1].kind === "lbrace";
27646
- const code = join2(raw, tokens);
27647
- const text = withComment(code, comment);
27648
- entries2.push({ kind: "line", depth: Math.max(0, depth - (closes ? 1 : 0)), text: sameTokens(text, tokens) ? text : original, opens, closes });
27649
- if (depth === 0) section = sectionOpenedBy(tokens);
27650
- for (const t of tokens) depth = Math.max(0, depth + (t.kind === "lbrace" ? 1 : t.kind === "rbrace" ? -1 : 0));
27651
- if (depth === 0) section = void 0;
27652
- }
27653
- return entries2;
27654
- }
27655
- function sectionOpenedBy(tokens) {
27656
- const [first, second] = tokens;
27657
- if (first.kind !== "ident" || tokens[tokens.length - 1].kind !== "lbrace") return void 0;
27658
- switch (first.value) {
27659
- case "traffic":
27660
- case "requirements":
27661
- case "capacity":
27662
- return second?.kind === "lbrace" ? first.value : void 0;
27663
- case "entity":
27664
- return second?.kind === "ident" ? "entity" : void 0;
27665
- case "decision":
27666
- case "test":
27667
- return second?.kind === "string" ? first.value : void 0;
27668
- default:
27669
- return void 0;
27670
- }
27671
- }
27672
- function sectionCells(section, raw, tokens) {
27673
- const [first, second] = tokens;
27674
- if (tokens.some((t) => t.kind === "lbrace" || t.kind === "rbrace" || t.kind === "arrow" || t.kind === "label")) return void 0;
27675
- const rest = (from) => tokens.length > from ? [join2(raw, tokens.slice(from))] : [];
27676
- switch (section) {
27677
- case "traffic":
27678
- return first.kind === "string" && (second?.kind === "quantity" || second?.kind === "number") ? [source(raw, first), source(raw, second), ...rest(2)] : void 0;
27679
- case "capacity":
27680
- return first.kind === "ident" && tokens.length > 1 ? [first.value, ...rest(1)] : void 0;
27681
- case "entity":
27682
- return first.kind === "ident" && second?.kind === "ident" ? [first.value, second.value, ...rest(2)] : void 0;
27683
- default:
27684
- return void 0;
27685
- }
27686
- }
27687
- function tidyBlankLines(entries2) {
27688
- const kept = [];
27689
- for (const entry of entries2) {
27690
- if (entry.kind !== "blank") {
27691
- if (entry.kind === "line" && entry.closes) while (kept[kept.length - 1]?.kind === "blank") kept.pop();
27692
- kept.push(entry);
27693
- continue;
27694
- }
27695
- const prev = kept[kept.length - 1];
27696
- if (!prev || prev.kind === "blank" || prev.kind === "line" && prev.opens) continue;
27697
- kept.push(entry);
27698
- }
27699
- while (kept[kept.length - 1]?.kind === "blank") kept.pop();
27700
- return kept;
27701
- }
27702
- function renderRun(run) {
27703
- const codes = run[0].kind === "node" ? alignNodes(run) : run[0].kind === "cells" ? alignCells(run) : alignConnections(run);
27704
- const items = run;
27705
- const commented = items.filter((e) => e.comment);
27706
- const commentCol = commented.length > 1 ? Math.max(...items.map((e, i) => e.comment ? len(codes[i]) : 0)) + 1 : 0;
27707
- return items.flatMap((entry, i) => {
27708
- const code = entry.comment && commentCol ? pad(codes[i], commentCol) + entry.comment : withComment(codes[i], entry.comment);
27709
- const text = sameTokens(code, entry.tokens) ? code : entry.original;
27710
- return [indent(entry.depth) + text, ...entry.kind === "connection" ? entry.continuation : []];
27711
- });
27712
- }
27713
- function alignNodes(run) {
27714
- const widths = [0, 1, 2, 3].map((c) => Math.max(...run.map((e) => len(e.cells[c]))));
27715
- return run.map(({ cells }) => {
27716
- let line = "";
27717
- const columns = !cells[3] && cells[4] ? 3 : 4;
27718
- for (let c = 0; c < columns; c++) if (widths[c]) line += pad(cells[c], widths[c] + 1);
27719
- return (line + cells[4]).trimEnd();
27720
- });
27721
- }
27722
- function alignCells(run) {
27723
- const widths = [];
27724
- for (const { cells } of run) cells.slice(0, -1).forEach((c, i) => widths[i] = Math.max(widths[i] ?? 0, len(c)));
27725
- return run.map(({ cells }) => cells.map((c, i) => i < cells.length - 1 ? pad(c, widths[i] + 1) : c).join(""));
27726
- }
27727
- function alignConnections(run) {
27728
- const head = Math.max(...run.map((e) => len(e.from) + 1 + e.arrow.length));
27729
- const labelled = run.filter((e) => e.label !== void 0);
27730
- const target = labelled.length ? Math.max(...labelled.map((e) => len(e.to))) : 0;
27731
- return run.map((e) => {
27732
- const start = e.from + " ".repeat(head - len(e.from) - e.arrow.length) + e.arrow + " ";
27733
- if (e.label === void 0) return start + e.to;
27734
- return start + pad(e.to, target) + " :" + (e.label ? " " + e.label : "");
27735
- });
27736
- }
27737
- function commentOf(raw, tokens) {
27738
- const last = tokens[tokens.length - 1];
27739
- if (last.kind === "label") {
27740
- const after2 = raw.slice(last.col);
27741
- return after2.slice(stripTrailingComment(after2).length).trim();
27742
- }
27743
- return raw.slice(last.col - 1 + last.length).trim();
27744
- }
27745
- function join2(raw, tokens) {
27746
- let out = "";
27747
- for (const [i, t] of tokens.entries()) {
27748
- const text = t.kind === "label" ? ":" + (t.value ? " " + t.value : "") : source(raw, t);
27749
- const tight = t.kind === "comma" || tokens[i - 1]?.kind === "comma" && t.kind === "number";
27750
- out += (i === 0 || tight ? "" : " ") + text;
27751
- }
27752
- return out;
27753
- }
27754
- function sameTokens(text, tokens) {
27755
- const { tokens: again, diagnostics } = tokenizeLine(text, 1);
27756
- return diagnostics.length === 0 && again.length === tokens.length && again.every((t, i) => t.kind === tokens[i].kind && t.value === tokens[i].value);
27757
- }
27758
- function reindent(line, delta) {
27759
- const lead = leadingSpace(line);
27760
- const body = line.slice(lead.length).trimEnd();
27761
- return body ? " ".repeat(Math.max(0, width(lead) + delta)) + body : "";
27762
- }
27763
- var source = (raw, t) => {
27764
- const text = raw.slice(t.col - 1, t.col - 1 + t.length);
27765
- return t.kind === "quantity" ? text.replace(/[ \t]+/g, " ") : text;
27766
- };
27767
- var withComment = (code, comment) => comment ? code + " " + comment : code;
27768
- var leadingSpace = (line) => /^[ \t]*/.exec(line)[0];
27769
- var width = (space) => [...space].reduce((n, ch) => n + (ch === " " ? INDENT.length : 1), 0);
27770
- var MAX_INDENT = 32;
27771
- var indent = (depth) => INDENT.repeat(Math.min(depth, MAX_INDENT));
27772
- var len = (s) => [...s].length;
27773
- var pad = (s, to) => s + " ".repeat(Math.max(0, to - len(s)));
27774
-
27775
27796
  // ../frontend/src/dsl/sequence.ts
27776
27797
  var PAYLOAD_LIMIT = 40;
27777
27798
  function shorten(text, max = PAYLOAD_LIMIT) {
@@ -27968,11 +27989,17 @@ var BANDWIDTH = {
27968
27989
  };
27969
27990
  var DEFAULT_BANDWIDTH_MBPS = 100;
27970
27991
  var CHARGES_EGRESS = (kind) => kind !== "client" && kind !== "external" && kind !== "dns" && kind !== "other";
27992
+ var SIZES = {
27993
+ S: { capacity: 1, cost: 1 },
27994
+ M: { capacity: 2, cost: 1.8 },
27995
+ L: { capacity: 4, cost: 3.5 }
27996
+ };
27971
27997
  function profileOf(node, override) {
27972
27998
  const kind = kindOf(node);
27973
27999
  const base = (Object.hasOwn(TECH_PROFILES, node.techStack) ? TECH_PROFILES[node.techStack] : void 0) ?? KIND_PROFILES[kind];
27974
- const baseRead = base.readRps ?? base.rps;
27975
- const baseWrite = base.writeRps ?? base.rps;
28000
+ const size = SIZES[override?.size ?? "S"];
28001
+ const baseRead = (base.readRps ?? base.rps) * size.capacity;
28002
+ const baseWrite = (base.writeRps ?? base.rps) * size.capacity;
27976
28003
  const readRps = override?.readRps ?? override?.rps ?? baseRead;
27977
28004
  const writeRps = override?.writeRps ?? override?.rps ?? baseWrite;
27978
28005
  const consistency = override?.consistency ?? base.consistency;
@@ -27985,7 +28012,7 @@ function profileOf(node, override) {
27985
28012
  shards: Math.max(1, Math.floor(override?.shards ?? 1)),
27986
28013
  latencyMs: override?.latencyMs ?? base.latencyMs,
27987
28014
  availability: override?.availability !== void 0 ? override.availability / 100 : base.availability,
27988
- costUsd: override?.costUsd ?? base.costUsd,
28015
+ costUsd: override?.costUsd ?? base.costUsd * size.cost,
27989
28016
  durable: override?.durable ?? base.durable,
27990
28017
  ...consistency && isDataStore(kind) ? { consistency } : {},
27991
28018
  bandwidthMBps: override?.bandwidthMBps ?? base.bandwidthMBps ?? BANDWIDTH[kind] ?? DEFAULT_BANDWIDTH_MBPS,
@@ -28316,6 +28343,19 @@ function pathAt(path, l) {
28316
28343
  var pathQuantile = (path, p) => pathAt(path, tailFactor(p));
28317
28344
  function pathCdf(path, t) {
28318
28345
  if (pathAt(path, 0) > t) return 0;
28346
+ if (path.parts.every((part) => part.length <= 1)) {
28347
+ let fixed = 0;
28348
+ let tail = 0;
28349
+ for (const part of path.parts) {
28350
+ if (part.length) {
28351
+ fixed += part[0].fixedMs;
28352
+ tail += part[0].tailMs;
28353
+ }
28354
+ }
28355
+ if (tail <= 0) return 1;
28356
+ const l = (t - fixed) / tail;
28357
+ return l > 1e6 ? 1 : 1 - Math.exp(-l);
28358
+ }
28319
28359
  let hi = 1;
28320
28360
  while (pathAt(path, hi) <= t) {
28321
28361
  hi *= 2;
@@ -28427,9 +28467,11 @@ function analyze(diagram, options = {}) {
28427
28467
  percentiles: Object.fromEntries(PERCENTILES2.map((q) => [PERCENTILE_KEYS[q], pathQuantile(paths[i], q / 100)]))
28428
28468
  }));
28429
28469
  const weighted = paths.map((path, i) => ({ share: shares[i] ?? 0, path }));
28430
- const percentiles = Object.fromEntries(PERCENTILES2.map((q) => [PERCENTILE_KEYS[q], mixtureQuantile(weighted, q / 100)]));
28470
+ const wanted = new Set(options.percentiles ?? PERCENTILES2);
28471
+ const percentiles = Object.fromEntries(PERCENTILES2.map((q) => [PERCENTILE_KEYS[q], wanted.has(q) ? mixtureQuantile(weighted, q / 100) : NaN]));
28431
28472
  const tailScenario = Object.fromEntries(
28432
28473
  PERCENTILES2.map((q) => {
28474
+ if (!wanted.has(q)) return [PERCENTILE_KEYS[q], 0];
28433
28475
  const t = percentiles[PERCENTILE_KEYS[q]];
28434
28476
  const above = weighted.map((s) => s.share > 0 ? s.share * (1 - pathCdf(s.path, t * (1 - 1e-9))) : -1);
28435
28477
  return [PERCENTILE_KEYS[q], Math.max(0, above.indexOf(Math.max(...above)))];
@@ -28482,7 +28524,7 @@ function singlePointsOfFailure(diagram) {
28482
28524
  // ../frontend/src/sim/tests.ts
28483
28525
  var pass = (message) => ({ passed: true, message });
28484
28526
  var fail = (message, hint) => ({ passed: false, message, hint });
28485
- function makeRun(diagram, analysis) {
28527
+ function makeRun(diagram, analysis, options = {}) {
28486
28528
  const access = accessIn(diagram);
28487
28529
  return {
28488
28530
  diagram,
@@ -28491,11 +28533,12 @@ function makeRun(diagram, analysis) {
28491
28533
  useCases: new Map(analysis.useCases.map((u) => [u.id, u])),
28492
28534
  shares: new Map([...resolveTraffic(diagram)].map(([id, t]) => [id, t.shares])),
28493
28535
  profiles: profilesOf(diagram),
28494
- isWrite: (step) => access(step) === "write"
28536
+ isWrite: (step) => access(step) === "write",
28537
+ options
28495
28538
  };
28496
28539
  }
28497
- function runTests(diagram, analysis = analyze(diagram)) {
28498
- const run = makeRun(diagram, analysis);
28540
+ function runTests(diagram, analysis = analyze(diagram), options = {}) {
28541
+ const run = makeRun(diagram, analysis, options);
28499
28542
  const results = (diagram.requirements ?? []).map((r, i) => {
28500
28543
  const { passed, message, hint } = requirement(run, r);
28501
28544
  return { id: `req:${i + 1}`, name: requirementName(r), category: CATEGORY[r.kind], passed, message, hint, loc: r.loc };
@@ -28709,7 +28752,8 @@ function survive(run, target) {
28709
28752
  const label = nodeLabel(run.diagram, node.id);
28710
28753
  const replicas = replicasOf(node);
28711
28754
  if (replicas >= 2) {
28712
- const degraded = makeRun(run.diagram, analyze(run.diagram, { replicas: /* @__PURE__ */ new Map([[node.id, replicas - 1]]) }));
28755
+ const degradedOptions = { ...run.options, replicas: new Map([...run.options.replicas ?? [], [node.id, replicas - 1]]) };
28756
+ const degraded = makeRun(run.diagram, analyze(run.diagram, degradedOptions), degradedOptions);
28713
28757
  const left = degraded.nodes.get(node.id);
28714
28758
  const lost = n.shards > 1 ? `one of the ${replicas} replicas of a ${node.id} shard` : `one of ${replicas} ${node.id} replicas`;
28715
28759
  if (left.saturated) {
@@ -29388,7 +29432,7 @@ function positionAt(text, offset) {
29388
29432
 
29389
29433
  // src/server.ts
29390
29434
  if (process.argv.includes("--version")) {
29391
- console.log(true ? "0.8.0" : "dev");
29435
+ console.log(true ? "0.10.0" : "dev");
29392
29436
  process.exit(0);
29393
29437
  }
29394
29438
  var connection = (0, import_node.createConnection)(import_node.ProposedFeatures.all);
@@ -29429,7 +29473,7 @@ connection.onInitialize(() => ({
29429
29473
  documentFormattingProvider: true,
29430
29474
  codeActionProvider: { codeActionKinds: [import_node.CodeActionKind.QuickFix] }
29431
29475
  },
29432
- serverInfo: { name: "proschi-language-server", version: true ? "0.8.0" : "dev" }
29476
+ serverInfo: { name: "proschi-language-server", version: true ? "0.10.0" : "dev" }
29433
29477
  }));
29434
29478
  function validate(document) {
29435
29479
  const analysis = analysisOf(document);