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/README.md +3 -1
- package/dist/cli.cjs +7981 -1596
- package/dist/render.cjs +128 -128
- package/dist/server.cjs +700 -656
- package/grammar/proschi.tmLanguage.json +1 -1
- package/package.json +2 -1
- package/schema/proschi-diagram.schema.json +46 -0
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/
|
|
25766
|
-
|
|
25767
|
-
|
|
25768
|
-
|
|
25769
|
-
|
|
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
|
-
|
|
25772
|
-
|
|
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
|
|
25775
|
-
const
|
|
25776
|
-
|
|
25777
|
-
|
|
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
|
|
25780
|
-
|
|
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
|
|
25783
|
-
const
|
|
25784
|
-
|
|
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
|
|
25787
|
-
|
|
25788
|
-
const
|
|
25789
|
-
const
|
|
25790
|
-
|
|
25791
|
-
|
|
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 {
|
|
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
|
-
|
|
27413
|
-
|
|
27414
|
-
|
|
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
|
-
|
|
27417
|
-
|
|
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
|
-
|
|
27420
|
-
const
|
|
27421
|
-
|
|
27422
|
-
|
|
27423
|
-
${
|
|
27424
|
-
|
|
27425
|
-
|
|
27426
|
-
|
|
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
|
-
|
|
27430
|
-
|
|
27431
|
-
|
|
27432
|
-
|
|
27433
|
-
|
|
27434
|
-
|
|
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
|
-
|
|
27437
|
-
|
|
27438
|
-
|
|
27439
|
-
|
|
27440
|
-
|
|
27441
|
-
|
|
27442
|
-
|
|
27443
|
-
|
|
27444
|
-
|
|
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
|
|
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
|
|
27465
|
-
|
|
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
|
|
27468
|
-
|
|
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
|
|
27471
|
-
|
|
27472
|
-
|
|
27473
|
-
|
|
27474
|
-
|
|
27475
|
-
|
|
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
|
|
27485
|
-
const
|
|
27486
|
-
|
|
27487
|
-
|
|
27488
|
-
|
|
27489
|
-
|
|
27490
|
-
|
|
27491
|
-
|
|
27492
|
-
|
|
27493
|
-
|
|
27494
|
-
|
|
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
|
|
27501
|
-
function
|
|
27502
|
-
|
|
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
|
-
|
|
27511
|
-
|
|
27512
|
-
|
|
27513
|
-
|
|
27514
|
-
|
|
27515
|
-
|
|
27516
|
-
|
|
27517
|
-
|
|
27518
|
-
|
|
27519
|
-
|
|
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
|
|
27525
|
-
|
|
27526
|
-
|
|
27527
|
-
|
|
27528
|
-
|
|
27529
|
-
|
|
27530
|
-
|
|
27531
|
-
|
|
27532
|
-
|
|
27533
|
-
|
|
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
|
|
27975
|
-
const
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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);
|