proschi 0.12.0 → 0.14.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 +33 -0
- package/dist/cli.cjs +3047 -862
- package/dist/render.cjs +347 -204
- package/dist/server.cjs +1171 -128
- package/grammar/proschi.tmLanguage.json +90 -0
- package/package.json +2 -1
package/dist/server.cjs
CHANGED
|
@@ -1303,8 +1303,8 @@ var require_semaphore = __commonJS({
|
|
|
1303
1303
|
this._waiting = [];
|
|
1304
1304
|
}
|
|
1305
1305
|
lock(thunk) {
|
|
1306
|
-
return new Promise((
|
|
1307
|
-
this._waiting.push({ thunk, resolve:
|
|
1306
|
+
return new Promise((resolve3, reject) => {
|
|
1307
|
+
this._waiting.push({ thunk, resolve: resolve3, reject });
|
|
1308
1308
|
this.runNext();
|
|
1309
1309
|
});
|
|
1310
1310
|
}
|
|
@@ -2930,9 +2930,9 @@ ${JSON.stringify(message, null, 4)}`);
|
|
|
2930
2930
|
if (typeof cancellationStrategy.sender.enableCancellation === "function") {
|
|
2931
2931
|
cancellationStrategy.sender.enableCancellation(requestMessage);
|
|
2932
2932
|
}
|
|
2933
|
-
return new Promise(async (
|
|
2933
|
+
return new Promise(async (resolve3, reject) => {
|
|
2934
2934
|
const resolveWithCleanup = (r) => {
|
|
2935
|
-
|
|
2935
|
+
resolve3(r);
|
|
2936
2936
|
cancellationStrategy.sender.cleanup(id);
|
|
2937
2937
|
disposable?.dispose();
|
|
2938
2938
|
};
|
|
@@ -8574,8 +8574,8 @@ var require_server = __commonJS({
|
|
|
8574
8574
|
if (value instanceof Promise) {
|
|
8575
8575
|
return value;
|
|
8576
8576
|
} else if (Is2.thenable(value)) {
|
|
8577
|
-
return new Promise((
|
|
8578
|
-
value.then((resolved) =>
|
|
8577
|
+
return new Promise((resolve3, reject) => {
|
|
8578
|
+
value.then((resolved) => resolve3(resolved), (error) => reject(error));
|
|
8579
8579
|
});
|
|
8580
8580
|
} else {
|
|
8581
8581
|
return Promise.resolve(value);
|
|
@@ -8859,7 +8859,7 @@ var require_files = __commonJS({
|
|
|
8859
8859
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
8860
8860
|
exports2.FileSystem = void 0;
|
|
8861
8861
|
exports2.uriToFilePath = uriToFilePath;
|
|
8862
|
-
exports2.resolve =
|
|
8862
|
+
exports2.resolve = resolve3;
|
|
8863
8863
|
exports2.resolveGlobalNodePath = resolveGlobalNodePath;
|
|
8864
8864
|
exports2.resolveGlobalYarnPath = resolveGlobalYarnPath;
|
|
8865
8865
|
exports2.resolveModulePath = resolveModulePath;
|
|
@@ -8888,7 +8888,7 @@ var require_files = __commonJS({
|
|
|
8888
8888
|
function isWindows() {
|
|
8889
8889
|
return process.platform === "win32";
|
|
8890
8890
|
}
|
|
8891
|
-
function
|
|
8891
|
+
function resolve3(moduleName, nodePath, cwd, tracer) {
|
|
8892
8892
|
const nodePathKey = "NODE_PATH";
|
|
8893
8893
|
const app = [
|
|
8894
8894
|
"var p = process;",
|
|
@@ -8907,7 +8907,7 @@ var require_files = __commonJS({
|
|
|
8907
8907
|
"}",
|
|
8908
8908
|
"});"
|
|
8909
8909
|
].join("");
|
|
8910
|
-
return new Promise((
|
|
8910
|
+
return new Promise((resolve4, reject) => {
|
|
8911
8911
|
const env = process.env;
|
|
8912
8912
|
const newEnv = /* @__PURE__ */ Object.create(null);
|
|
8913
8913
|
Object.keys(env).forEach((key) => newEnv[key] = env[key]);
|
|
@@ -8939,7 +8939,7 @@ var require_files = __commonJS({
|
|
|
8939
8939
|
if (message2.c === "r") {
|
|
8940
8940
|
cp.send({ c: "e" });
|
|
8941
8941
|
if (message2.s) {
|
|
8942
|
-
|
|
8942
|
+
resolve4(message2.r);
|
|
8943
8943
|
} else {
|
|
8944
8944
|
reject(new Error(`Failed to resolve module: ${moduleName}`));
|
|
8945
8945
|
}
|
|
@@ -9067,17 +9067,17 @@ var require_files = __commonJS({
|
|
|
9067
9067
|
if (!path.isAbsolute(nodePath)) {
|
|
9068
9068
|
nodePath = path.join(workspaceRoot, nodePath);
|
|
9069
9069
|
}
|
|
9070
|
-
return
|
|
9070
|
+
return resolve3(moduleName, nodePath, nodePath, tracer).then((value) => {
|
|
9071
9071
|
if (FileSystem.isParent(nodePath, value)) {
|
|
9072
9072
|
return value;
|
|
9073
9073
|
} else {
|
|
9074
9074
|
return Promise.reject(new Error(`Failed to load ${moduleName} from node path location.`));
|
|
9075
9075
|
}
|
|
9076
9076
|
}).then(void 0, (_error) => {
|
|
9077
|
-
return
|
|
9077
|
+
return resolve3(moduleName, resolveGlobalNodePath(tracer), workspaceRoot, tracer);
|
|
9078
9078
|
});
|
|
9079
9079
|
} else {
|
|
9080
|
-
return
|
|
9080
|
+
return resolve3(moduleName, resolveGlobalNodePath(tracer), workspaceRoot, tracer);
|
|
9081
9081
|
}
|
|
9082
9082
|
}
|
|
9083
9083
|
}
|
|
@@ -9159,10 +9159,10 @@ var require_ril = __commonJS({
|
|
|
9159
9159
|
return api_1.Disposable.create(() => this.stream.off("end", listener));
|
|
9160
9160
|
}
|
|
9161
9161
|
write(data, encoding) {
|
|
9162
|
-
return new Promise((
|
|
9162
|
+
return new Promise((resolve3, reject) => {
|
|
9163
9163
|
const callback = (error) => {
|
|
9164
9164
|
if (error === void 0 || error === null) {
|
|
9165
|
-
|
|
9165
|
+
resolve3();
|
|
9166
9166
|
} else {
|
|
9167
9167
|
reject(error);
|
|
9168
9168
|
}
|
|
@@ -9454,10 +9454,10 @@ var require_main = __commonJS({
|
|
|
9454
9454
|
}
|
|
9455
9455
|
function createClientPipeTransport(pipeName, encoding = "utf-8") {
|
|
9456
9456
|
let connectResolve;
|
|
9457
|
-
const connected = new Promise((
|
|
9458
|
-
connectResolve =
|
|
9457
|
+
const connected = new Promise((resolve3, _reject) => {
|
|
9458
|
+
connectResolve = resolve3;
|
|
9459
9459
|
});
|
|
9460
|
-
return new Promise((
|
|
9460
|
+
return new Promise((resolve3, reject) => {
|
|
9461
9461
|
const server = (0, net_1.createServer)((socket) => {
|
|
9462
9462
|
server.close();
|
|
9463
9463
|
connectResolve([
|
|
@@ -9468,7 +9468,7 @@ var require_main = __commonJS({
|
|
|
9468
9468
|
server.on("error", reject);
|
|
9469
9469
|
server.listen(pipeName, () => {
|
|
9470
9470
|
server.removeListener("error", reject);
|
|
9471
|
-
|
|
9471
|
+
resolve3({
|
|
9472
9472
|
onConnected: () => {
|
|
9473
9473
|
return connected;
|
|
9474
9474
|
}
|
|
@@ -9485,10 +9485,10 @@ var require_main = __commonJS({
|
|
|
9485
9485
|
}
|
|
9486
9486
|
function createClientSocketTransport(port, encoding = "utf-8") {
|
|
9487
9487
|
let connectResolve;
|
|
9488
|
-
const connected = new Promise((
|
|
9489
|
-
connectResolve =
|
|
9488
|
+
const connected = new Promise((resolve3, _reject) => {
|
|
9489
|
+
connectResolve = resolve3;
|
|
9490
9490
|
});
|
|
9491
|
-
return new Promise((
|
|
9491
|
+
return new Promise((resolve3, reject) => {
|
|
9492
9492
|
const server = (0, net_1.createServer)((socket) => {
|
|
9493
9493
|
server.close();
|
|
9494
9494
|
connectResolve([
|
|
@@ -9505,7 +9505,7 @@ var require_main = __commonJS({
|
|
|
9505
9505
|
return;
|
|
9506
9506
|
}
|
|
9507
9507
|
const boundPort = address.port;
|
|
9508
|
-
|
|
9508
|
+
resolve3({
|
|
9509
9509
|
port: () => boundPort,
|
|
9510
9510
|
onConnected: () => {
|
|
9511
9511
|
return connected;
|
|
@@ -12856,7 +12856,7 @@ var require_compile = __commonJS({
|
|
|
12856
12856
|
const schOrFunc = root.refs[ref];
|
|
12857
12857
|
if (schOrFunc)
|
|
12858
12858
|
return schOrFunc;
|
|
12859
|
-
let _sch =
|
|
12859
|
+
let _sch = resolve3.call(this, root, ref);
|
|
12860
12860
|
if (_sch === void 0) {
|
|
12861
12861
|
const schema = (_a = root.localRefs) === null || _a === void 0 ? void 0 : _a[ref];
|
|
12862
12862
|
const { schemaId } = this.opts;
|
|
@@ -12883,7 +12883,7 @@ var require_compile = __commonJS({
|
|
|
12883
12883
|
function sameSchemaEnv(s1, s2) {
|
|
12884
12884
|
return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
|
|
12885
12885
|
}
|
|
12886
|
-
function
|
|
12886
|
+
function resolve3(root, ref) {
|
|
12887
12887
|
let sch;
|
|
12888
12888
|
while (typeof (sch = this.refs[ref]) == "string")
|
|
12889
12889
|
ref = sch;
|
|
@@ -13713,7 +13713,7 @@ var require_fast_uri = __commonJS({
|
|
|
13713
13713
|
}
|
|
13714
13714
|
return uri;
|
|
13715
13715
|
}
|
|
13716
|
-
function
|
|
13716
|
+
function resolve3(baseURI, relativeURI, options) {
|
|
13717
13717
|
const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
|
|
13718
13718
|
const {
|
|
13719
13719
|
parsed: baseParsed,
|
|
@@ -14082,7 +14082,7 @@ var require_fast_uri = __commonJS({
|
|
|
14082
14082
|
var fastUri = {
|
|
14083
14083
|
SCHEMES,
|
|
14084
14084
|
normalize,
|
|
14085
|
-
resolve:
|
|
14085
|
+
resolve: resolve3,
|
|
14086
14086
|
resolveComponent,
|
|
14087
14087
|
equal,
|
|
14088
14088
|
serialize,
|
|
@@ -21292,10 +21292,10 @@ var require_resolve_block_map = __commonJS({
|
|
|
21292
21292
|
let offset = bm.offset;
|
|
21293
21293
|
let commentEnd = null;
|
|
21294
21294
|
for (const collItem of bm.items) {
|
|
21295
|
-
const { start, key, sep, value } = collItem;
|
|
21295
|
+
const { start, key, sep: sep2, value } = collItem;
|
|
21296
21296
|
const keyProps = resolveProps.resolveProps(start, {
|
|
21297
21297
|
indicator: "explicit-key-ind",
|
|
21298
|
-
next: key ??
|
|
21298
|
+
next: key ?? sep2?.[0],
|
|
21299
21299
|
offset,
|
|
21300
21300
|
onError,
|
|
21301
21301
|
parentIndent: bm.indent,
|
|
@@ -21309,7 +21309,7 @@ var require_resolve_block_map = __commonJS({
|
|
|
21309
21309
|
else if ("indent" in key && key.indent !== bm.indent)
|
|
21310
21310
|
onError(offset, "BAD_INDENT", startColMsg);
|
|
21311
21311
|
}
|
|
21312
|
-
if (!keyProps.anchor && !keyProps.tag && !
|
|
21312
|
+
if (!keyProps.anchor && !keyProps.tag && !sep2) {
|
|
21313
21313
|
commentEnd = keyProps.end;
|
|
21314
21314
|
if (keyProps.comment) {
|
|
21315
21315
|
if (map.comment)
|
|
@@ -21333,7 +21333,7 @@ var require_resolve_block_map = __commonJS({
|
|
|
21333
21333
|
ctx.atKey = false;
|
|
21334
21334
|
if (utilMapIncludes.mapIncludes(ctx, map.items, keyNode))
|
|
21335
21335
|
onError(keyStart, "DUPLICATE_KEY", "Map keys must be unique");
|
|
21336
|
-
const valueProps = resolveProps.resolveProps(
|
|
21336
|
+
const valueProps = resolveProps.resolveProps(sep2 ?? [], {
|
|
21337
21337
|
indicator: "map-value-ind",
|
|
21338
21338
|
next: value,
|
|
21339
21339
|
offset: keyNode.range[2],
|
|
@@ -21349,7 +21349,7 @@ var require_resolve_block_map = __commonJS({
|
|
|
21349
21349
|
if (ctx.options.strict && keyProps.start < valueProps.found.offset - 1024)
|
|
21350
21350
|
onError(keyNode.range, "KEY_OVER_1024_CHARS", "The : indicator must be at most 1024 chars after the start of an implicit block mapping key");
|
|
21351
21351
|
}
|
|
21352
|
-
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : composeEmptyNode(ctx, offset,
|
|
21352
|
+
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : composeEmptyNode(ctx, offset, sep2, null, valueProps, onError);
|
|
21353
21353
|
if (ctx.schema.compat)
|
|
21354
21354
|
utilFlowIndentCheck.flowIndentCheck(bm.indent, value, onError);
|
|
21355
21355
|
offset = valueNode.range[2];
|
|
@@ -21440,7 +21440,7 @@ var require_resolve_end = __commonJS({
|
|
|
21440
21440
|
let comment = "";
|
|
21441
21441
|
if (end) {
|
|
21442
21442
|
let hasSpace = false;
|
|
21443
|
-
let
|
|
21443
|
+
let sep2 = "";
|
|
21444
21444
|
for (const token of end) {
|
|
21445
21445
|
const { source: source2, type } = token;
|
|
21446
21446
|
switch (type) {
|
|
@@ -21454,13 +21454,13 @@ var require_resolve_end = __commonJS({
|
|
|
21454
21454
|
if (!comment)
|
|
21455
21455
|
comment = cb;
|
|
21456
21456
|
else
|
|
21457
|
-
comment +=
|
|
21458
|
-
|
|
21457
|
+
comment += sep2 + cb;
|
|
21458
|
+
sep2 = "";
|
|
21459
21459
|
break;
|
|
21460
21460
|
}
|
|
21461
21461
|
case "newline":
|
|
21462
21462
|
if (comment)
|
|
21463
|
-
|
|
21463
|
+
sep2 += source2;
|
|
21464
21464
|
hasSpace = true;
|
|
21465
21465
|
break;
|
|
21466
21466
|
default:
|
|
@@ -21503,18 +21503,18 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
21503
21503
|
let offset = fc.offset + fc.start.source.length;
|
|
21504
21504
|
for (let i = 0; i < fc.items.length; ++i) {
|
|
21505
21505
|
const collItem = fc.items[i];
|
|
21506
|
-
const { start, key, sep, value } = collItem;
|
|
21506
|
+
const { start, key, sep: sep2, value } = collItem;
|
|
21507
21507
|
const props = resolveProps.resolveProps(start, {
|
|
21508
21508
|
flow: fcName,
|
|
21509
21509
|
indicator: "explicit-key-ind",
|
|
21510
|
-
next: key ??
|
|
21510
|
+
next: key ?? sep2?.[0],
|
|
21511
21511
|
offset,
|
|
21512
21512
|
onError,
|
|
21513
21513
|
parentIndent: fc.indent,
|
|
21514
21514
|
startOnNewline: false
|
|
21515
21515
|
});
|
|
21516
21516
|
if (!props.found) {
|
|
21517
|
-
if (!props.anchor && !props.tag && !
|
|
21517
|
+
if (!props.anchor && !props.tag && !sep2 && !value) {
|
|
21518
21518
|
if (i === 0 && props.comma)
|
|
21519
21519
|
onError(props.comma, "UNEXPECTED_TOKEN", `Unexpected , in ${fcName}`);
|
|
21520
21520
|
else if (i < fc.items.length - 1)
|
|
@@ -21568,8 +21568,8 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
21568
21568
|
}
|
|
21569
21569
|
}
|
|
21570
21570
|
}
|
|
21571
|
-
if (!isMap && !
|
|
21572
|
-
const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end,
|
|
21571
|
+
if (!isMap && !sep2 && !props.found) {
|
|
21572
|
+
const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, sep2, null, props, onError);
|
|
21573
21573
|
coll.items.push(valueNode);
|
|
21574
21574
|
offset = valueNode.range[2];
|
|
21575
21575
|
if (isBlock(value))
|
|
@@ -21581,7 +21581,7 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
21581
21581
|
if (isBlock(key))
|
|
21582
21582
|
onError(keyNode.range, "BLOCK_IN_FLOW", blockMsg);
|
|
21583
21583
|
ctx.atKey = false;
|
|
21584
|
-
const valueProps = resolveProps.resolveProps(
|
|
21584
|
+
const valueProps = resolveProps.resolveProps(sep2 ?? [], {
|
|
21585
21585
|
flow: fcName,
|
|
21586
21586
|
indicator: "map-value-ind",
|
|
21587
21587
|
next: value,
|
|
@@ -21592,8 +21592,8 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
21592
21592
|
});
|
|
21593
21593
|
if (valueProps.found) {
|
|
21594
21594
|
if (!isMap && !props.found && ctx.options.strict) {
|
|
21595
|
-
if (
|
|
21596
|
-
for (const st of
|
|
21595
|
+
if (sep2)
|
|
21596
|
+
for (const st of sep2) {
|
|
21597
21597
|
if (st === valueProps.found)
|
|
21598
21598
|
break;
|
|
21599
21599
|
if (st.type === "newline") {
|
|
@@ -21610,7 +21610,7 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
21610
21610
|
else
|
|
21611
21611
|
onError(valueProps.start, "MISSING_CHAR", `Missing , or : between ${fcName} items`);
|
|
21612
21612
|
}
|
|
21613
|
-
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode(ctx, valueProps.end,
|
|
21613
|
+
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode(ctx, valueProps.end, sep2, null, valueProps, onError) : null;
|
|
21614
21614
|
if (valueNode) {
|
|
21615
21615
|
if (isBlock(value))
|
|
21616
21616
|
onError(valueNode.range, "BLOCK_IN_FLOW", blockMsg);
|
|
@@ -21790,7 +21790,7 @@ var require_resolve_block_scalar = __commonJS({
|
|
|
21790
21790
|
chompStart = i + 1;
|
|
21791
21791
|
}
|
|
21792
21792
|
let value = "";
|
|
21793
|
-
let
|
|
21793
|
+
let sep2 = "";
|
|
21794
21794
|
let prevMoreIndented = false;
|
|
21795
21795
|
for (let i = 0; i < contentStart; ++i)
|
|
21796
21796
|
value += lines[i][0].slice(trimIndent) + "\n";
|
|
@@ -21807,24 +21807,24 @@ var require_resolve_block_scalar = __commonJS({
|
|
|
21807
21807
|
indent2 = "";
|
|
21808
21808
|
}
|
|
21809
21809
|
if (type === Scalar.Scalar.BLOCK_LITERAL) {
|
|
21810
|
-
value +=
|
|
21811
|
-
|
|
21810
|
+
value += sep2 + indent2.slice(trimIndent) + content;
|
|
21811
|
+
sep2 = "\n";
|
|
21812
21812
|
} else if (indent2.length > trimIndent || content[0] === " ") {
|
|
21813
|
-
if (
|
|
21814
|
-
|
|
21815
|
-
else if (!prevMoreIndented &&
|
|
21816
|
-
|
|
21817
|
-
value +=
|
|
21818
|
-
|
|
21813
|
+
if (sep2 === " ")
|
|
21814
|
+
sep2 = "\n";
|
|
21815
|
+
else if (!prevMoreIndented && sep2 === "\n")
|
|
21816
|
+
sep2 = "\n\n";
|
|
21817
|
+
value += sep2 + indent2.slice(trimIndent) + content;
|
|
21818
|
+
sep2 = "\n";
|
|
21819
21819
|
prevMoreIndented = true;
|
|
21820
21820
|
} else if (content === "") {
|
|
21821
|
-
if (
|
|
21821
|
+
if (sep2 === "\n")
|
|
21822
21822
|
value += "\n";
|
|
21823
21823
|
else
|
|
21824
|
-
|
|
21824
|
+
sep2 = "\n";
|
|
21825
21825
|
} else {
|
|
21826
|
-
value +=
|
|
21827
|
-
|
|
21826
|
+
value += sep2 + content;
|
|
21827
|
+
sep2 = " ";
|
|
21828
21828
|
prevMoreIndented = false;
|
|
21829
21829
|
}
|
|
21830
21830
|
}
|
|
@@ -22007,25 +22007,25 @@ var require_resolve_flow_scalar = __commonJS({
|
|
|
22007
22007
|
trimBoth = /^[ \t]+|[ \t]+$/g;
|
|
22008
22008
|
}
|
|
22009
22009
|
let res = match[1].replace(trimEnd, "");
|
|
22010
|
-
let
|
|
22010
|
+
let sep2 = " ";
|
|
22011
22011
|
let pos = line.lastIndex;
|
|
22012
22012
|
while (match = line.exec(source2)) {
|
|
22013
22013
|
const lm = match[1].replace(trimBoth, "");
|
|
22014
22014
|
if (lm === "") {
|
|
22015
|
-
if (
|
|
22016
|
-
res +=
|
|
22015
|
+
if (sep2 === "\n")
|
|
22016
|
+
res += sep2;
|
|
22017
22017
|
else
|
|
22018
|
-
|
|
22018
|
+
sep2 = "\n";
|
|
22019
22019
|
} else {
|
|
22020
|
-
res +=
|
|
22021
|
-
|
|
22020
|
+
res += sep2 + lm;
|
|
22021
|
+
sep2 = " ";
|
|
22022
22022
|
}
|
|
22023
22023
|
pos = line.lastIndex;
|
|
22024
22024
|
}
|
|
22025
22025
|
const last = /[ \t]*(.*)/sy;
|
|
22026
22026
|
last.lastIndex = pos;
|
|
22027
22027
|
match = last.exec(source2);
|
|
22028
|
-
return res +
|
|
22028
|
+
return res + sep2 + (match?.[1] ?? "");
|
|
22029
22029
|
}
|
|
22030
22030
|
function doubleQuotedValue(source2, onError) {
|
|
22031
22031
|
let res = "";
|
|
@@ -22835,14 +22835,14 @@ var require_cst_stringify = __commonJS({
|
|
|
22835
22835
|
}
|
|
22836
22836
|
}
|
|
22837
22837
|
}
|
|
22838
|
-
function stringifyItem({ start, key, sep, value }) {
|
|
22838
|
+
function stringifyItem({ start, key, sep: sep2, value }) {
|
|
22839
22839
|
let res = "";
|
|
22840
22840
|
for (const st of start)
|
|
22841
22841
|
res += st.source;
|
|
22842
22842
|
if (key)
|
|
22843
22843
|
res += stringifyToken(key);
|
|
22844
|
-
if (
|
|
22845
|
-
for (const st of
|
|
22844
|
+
if (sep2)
|
|
22845
|
+
for (const st of sep2)
|
|
22846
22846
|
res += st.source;
|
|
22847
22847
|
if (value)
|
|
22848
22848
|
res += stringifyToken(value);
|
|
@@ -24009,18 +24009,18 @@ var require_parser = __commonJS({
|
|
|
24009
24009
|
if (this.type === "map-value-ind") {
|
|
24010
24010
|
const prev = getPrevProps(this.peek(2));
|
|
24011
24011
|
const start = getFirstKeyStartProps(prev);
|
|
24012
|
-
let
|
|
24012
|
+
let sep2;
|
|
24013
24013
|
if (scalar.end) {
|
|
24014
|
-
|
|
24015
|
-
|
|
24014
|
+
sep2 = scalar.end;
|
|
24015
|
+
sep2.push(this.sourceToken);
|
|
24016
24016
|
delete scalar.end;
|
|
24017
24017
|
} else
|
|
24018
|
-
|
|
24018
|
+
sep2 = [this.sourceToken];
|
|
24019
24019
|
const map = {
|
|
24020
24020
|
type: "block-map",
|
|
24021
24021
|
offset: scalar.offset,
|
|
24022
24022
|
indent: scalar.indent,
|
|
24023
|
-
items: [{ start, key: scalar, sep }]
|
|
24023
|
+
items: [{ start, key: scalar, sep: sep2 }]
|
|
24024
24024
|
};
|
|
24025
24025
|
this.onKeyLine = true;
|
|
24026
24026
|
this.stack[this.stack.length - 1] = map;
|
|
@@ -24173,15 +24173,15 @@ var require_parser = __commonJS({
|
|
|
24173
24173
|
} else if (isFlowToken(it.key) && !includesToken(it.sep, "newline")) {
|
|
24174
24174
|
const start2 = getFirstKeyStartProps(it.start);
|
|
24175
24175
|
const key = it.key;
|
|
24176
|
-
const
|
|
24177
|
-
|
|
24176
|
+
const sep2 = it.sep;
|
|
24177
|
+
sep2.push(this.sourceToken);
|
|
24178
24178
|
delete it.key;
|
|
24179
24179
|
delete it.sep;
|
|
24180
24180
|
this.stack.push({
|
|
24181
24181
|
type: "block-map",
|
|
24182
24182
|
offset: this.offset,
|
|
24183
24183
|
indent: this.indent,
|
|
24184
|
-
items: [{ start: start2, key, sep }]
|
|
24184
|
+
items: [{ start: start2, key, sep: sep2 }]
|
|
24185
24185
|
});
|
|
24186
24186
|
} else if (start.length > 0) {
|
|
24187
24187
|
it.sep = it.sep.concat(start, this.sourceToken);
|
|
@@ -24375,13 +24375,13 @@ var require_parser = __commonJS({
|
|
|
24375
24375
|
const prev = getPrevProps(parent);
|
|
24376
24376
|
const start = getFirstKeyStartProps(prev);
|
|
24377
24377
|
fixFlowSeqItems(fc);
|
|
24378
|
-
const
|
|
24379
|
-
|
|
24378
|
+
const sep2 = fc.end.splice(1, fc.end.length);
|
|
24379
|
+
sep2.push(this.sourceToken);
|
|
24380
24380
|
const map = {
|
|
24381
24381
|
type: "block-map",
|
|
24382
24382
|
offset: fc.offset,
|
|
24383
24383
|
indent: fc.indent,
|
|
24384
|
-
items: [{ start, key: fc, sep }]
|
|
24384
|
+
items: [{ start, key: fc, sep: sep2 }]
|
|
24385
24385
|
};
|
|
24386
24386
|
this.onKeyLine = true;
|
|
24387
24387
|
this.stack[this.stack.length - 1] = map;
|
|
@@ -24912,7 +24912,7 @@ function getWellformedEdit(textEdit) {
|
|
|
24912
24912
|
}
|
|
24913
24913
|
|
|
24914
24914
|
// src/server.ts
|
|
24915
|
-
var
|
|
24915
|
+
var import_node_fs5 = require("node:fs");
|
|
24916
24916
|
|
|
24917
24917
|
// src/openapi/config.ts
|
|
24918
24918
|
var import_node_fs2 = require("node:fs");
|
|
@@ -25478,6 +25478,19 @@ var BracketCounter = class {
|
|
|
25478
25478
|
return this.depth;
|
|
25479
25479
|
}
|
|
25480
25480
|
};
|
|
25481
|
+
function qualify(tokens) {
|
|
25482
|
+
let out;
|
|
25483
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
25484
|
+
const [t, next] = [tokens[i], tokens[i + 1]];
|
|
25485
|
+
const joined = t.kind === "ident" && next?.kind === "tag" && next.col === t.col + t.length;
|
|
25486
|
+
if (joined && (tokens[i + 2]?.kind === "arrow" || tokens[i - 1]?.kind === "arrow")) {
|
|
25487
|
+
out ??= tokens.slice(0, i);
|
|
25488
|
+
out.push({ ...t, value: `${t.value}.${next.value}`, length: t.length + next.length });
|
|
25489
|
+
i++;
|
|
25490
|
+
} else out?.push(t);
|
|
25491
|
+
}
|
|
25492
|
+
return out ?? tokens;
|
|
25493
|
+
}
|
|
25481
25494
|
|
|
25482
25495
|
// ../frontend/src/dsl/format.ts
|
|
25483
25496
|
function format(source2) {
|
|
@@ -25514,7 +25527,8 @@ function classify(lines) {
|
|
|
25514
25527
|
entries2.push({ kind: "blank", depth, text: "" });
|
|
25515
25528
|
continue;
|
|
25516
25529
|
}
|
|
25517
|
-
const { tokens, diagnostics } = tokenizeLine(raw, i + 1);
|
|
25530
|
+
const { tokens: lexed, diagnostics } = tokenizeLine(raw, i + 1);
|
|
25531
|
+
const tokens = qualify(lexed);
|
|
25518
25532
|
if (diagnostics.some((d) => d.severity === "error") || tokens.length === 0) {
|
|
25519
25533
|
entries2.push({ kind: "line", depth, text: original });
|
|
25520
25534
|
continue;
|
|
@@ -25669,7 +25683,8 @@ function join(raw, tokens) {
|
|
|
25669
25683
|
return out;
|
|
25670
25684
|
}
|
|
25671
25685
|
function sameTokens(text, tokens) {
|
|
25672
|
-
const { tokens:
|
|
25686
|
+
const { tokens: lexed, diagnostics } = tokenizeLine(text, 1);
|
|
25687
|
+
const again = qualify(lexed);
|
|
25673
25688
|
return diagnostics.length === 0 && again.length === tokens.length && again.every((t, i) => t.kind === tokens[i].kind && t.value === tokens[i].value);
|
|
25674
25689
|
}
|
|
25675
25690
|
function reindent(line, delta) {
|
|
@@ -25933,6 +25948,8 @@ function editDistance2(a, b) {
|
|
|
25933
25948
|
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
25934
25949
|
var DEFAULT_TECH = "Rectangle";
|
|
25935
25950
|
var DEFAULT_GROUP_TECH = "Logical Group";
|
|
25951
|
+
var SYSTEM_TECH = "Service";
|
|
25952
|
+
var STUB_HELP = "a rate, p99, latency or availability, e.g. capacity { 2k rps p99 150ms availability 99.95% }";
|
|
25936
25953
|
var MAX_SCENARIOS = 32;
|
|
25937
25954
|
var MAX_ALT_DEPTH = 16;
|
|
25938
25955
|
var DIRECTIONS = ["down", "right", "up", "left"];
|
|
@@ -25977,7 +25994,7 @@ var DID_YOU_MEAN = /^Unknown tech stack '(.*)'\. Did you mean '([^']+)'\?/;
|
|
|
25977
25994
|
function parse(source2, options = {}) {
|
|
25978
25995
|
return new Parser(source2, options).run();
|
|
25979
25996
|
}
|
|
25980
|
-
var Parser = class {
|
|
25997
|
+
var Parser = class _Parser {
|
|
25981
25998
|
// Per-file state, swapped while an imported file is read.
|
|
25982
25999
|
lines;
|
|
25983
26000
|
/** Path of the imported file being read; undefined for the root document. */
|
|
@@ -25998,7 +26015,12 @@ var Parser = class {
|
|
|
25998
26015
|
/** Files being read, outermost first, to find import cycles. */
|
|
25999
26016
|
chain;
|
|
26000
26017
|
/** Diagnostics are sorted by file in the order the files were first read, the root first. */
|
|
26001
|
-
fileOrder
|
|
26018
|
+
fileOrder;
|
|
26019
|
+
/** The file read as the root: undefined for a document, the path for a system's team file (its own scope). */
|
|
26020
|
+
rootFile;
|
|
26021
|
+
teamFiles;
|
|
26022
|
+
systems = [];
|
|
26023
|
+
systemById = /* @__PURE__ */ new Map();
|
|
26002
26024
|
/** `a|b` for every connection, both directions; filled before use cases are built. */
|
|
26003
26025
|
connected = /* @__PURE__ */ new Set();
|
|
26004
26026
|
title;
|
|
@@ -26020,14 +26042,18 @@ var Parser = class {
|
|
|
26020
26042
|
deferred = [];
|
|
26021
26043
|
built = [];
|
|
26022
26044
|
options;
|
|
26023
|
-
constructor(source2, options) {
|
|
26045
|
+
constructor(source2, options, scope2) {
|
|
26024
26046
|
this.options = options;
|
|
26025
26047
|
this.lines = source2.split("\n");
|
|
26026
|
-
this.chain = [options.path];
|
|
26048
|
+
this.chain = scope2 ? scope2.chain : [options.path];
|
|
26049
|
+
this.teamFiles = scope2?.teamFiles ?? /* @__PURE__ */ new Map();
|
|
26050
|
+
if (scope2) this.rootFile = this.file = options.path;
|
|
26051
|
+
this.fileOrder = /* @__PURE__ */ new Map([[this.rootFile, 0]]);
|
|
26027
26052
|
if (options.path !== void 0) this.included.add(options.path);
|
|
26028
26053
|
}
|
|
26029
26054
|
run() {
|
|
26030
26055
|
this.parseFile();
|
|
26056
|
+
this.resolvePorts();
|
|
26031
26057
|
this.createImplicitNodes();
|
|
26032
26058
|
this.resolveTags();
|
|
26033
26059
|
for (const e of this.edges) this.connected.add(`${e.source}|${e.target}`).add(`${e.target}|${e.source}`);
|
|
@@ -26050,7 +26076,8 @@ var Parser = class {
|
|
|
26050
26076
|
...this.decisions.length ? { decisions: this.decisions } : {},
|
|
26051
26077
|
...this.tests.length ? { tests: this.tests } : {},
|
|
26052
26078
|
...this.legend.length ? { legend: this.legend } : {},
|
|
26053
|
-
...Object.keys(this.layout).length ? { layout: this.layout } : {}
|
|
26079
|
+
...Object.keys(this.layout).length ? { layout: this.layout } : {},
|
|
26080
|
+
...this.systems.length ? { systems: this.systems } : {}
|
|
26054
26081
|
};
|
|
26055
26082
|
const order = (d) => this.fileOrder.get(d.file) ?? 0;
|
|
26056
26083
|
this.diagnostics.sort((a, b) => order(a) - order(b) || a.line - b.line || a.col - b.col);
|
|
@@ -26067,14 +26094,16 @@ var Parser = class {
|
|
|
26067
26094
|
i = this.parseLine(i);
|
|
26068
26095
|
}
|
|
26069
26096
|
for (const frame of this.stack) {
|
|
26070
|
-
const what = frame.kind === "group" ? `group '${frame.id}'` : frame.kind === "usecase" ? `usecase '${frame.useCase.name}'` : frame.kind === "alt" ? `alt '${frame.name}'` : frame.kind === "par" ? "par block" : sectionName(frame);
|
|
26097
|
+
const what = frame.kind === "group" ? `group '${frame.id}'` : frame.kind === "usecase" ? `usecase '${frame.useCase.name}'` : frame.kind === "alt" ? `alt '${frame.name}'` : frame.kind === "par" ? "par block" : frame.kind === "system" ? `system '${frame.system.id}'` : frame.kind === "stub" ? `the capacity of system '${frame.system.id}'` : sectionName(frame);
|
|
26071
26098
|
this.error(`Missing } to close ${what}`, frame.loc);
|
|
26099
|
+
if (frame.kind === "system") this.finishSystem(frame);
|
|
26072
26100
|
}
|
|
26073
26101
|
}
|
|
26074
26102
|
/** Parses the line at `index` and returns the index of the last line it consumed. */
|
|
26075
26103
|
parseLine(index) {
|
|
26076
26104
|
const line = index + 1;
|
|
26077
|
-
const { tokens, diagnostics } = tokenizeLine(this.lines[index], line);
|
|
26105
|
+
const { tokens: lexed, diagnostics } = tokenizeLine(this.lines[index], line);
|
|
26106
|
+
const tokens = qualify(lexed);
|
|
26078
26107
|
this.diagnostics.push(...diagnostics.map((d) => ({ ...d, ...this.fileField() })));
|
|
26079
26108
|
if (tokens.length === 0 || diagnostics.some((d) => d.severity === "error")) return index;
|
|
26080
26109
|
const [first, second, third] = tokens;
|
|
@@ -26095,13 +26124,17 @@ var Parser = class {
|
|
|
26095
26124
|
if (first.kind === "rbrace") {
|
|
26096
26125
|
this.stack.pop();
|
|
26097
26126
|
if (!top.discard) this.closeSection(top);
|
|
26098
|
-
if (!top.discard && this.file ===
|
|
26127
|
+
if (!top.discard && this.file === this.rootFile) this.blocks.push({ kind: top.section, start: top.loc.line, end: line });
|
|
26099
26128
|
if (second) this.error("Unexpected input after }", loc(second));
|
|
26100
26129
|
} else if (!top.discard) {
|
|
26101
26130
|
this.parseSectionLine(top, tokens, line);
|
|
26102
26131
|
}
|
|
26103
26132
|
return index;
|
|
26104
26133
|
}
|
|
26134
|
+
if (top?.kind === "system" || top?.kind === "stub") {
|
|
26135
|
+
this.parseSystemLine(top, tokens, line);
|
|
26136
|
+
return index;
|
|
26137
|
+
}
|
|
26105
26138
|
if (first.kind === "rbrace") {
|
|
26106
26139
|
if (this.stack.length === 0) this.error("Unmatched }", loc(first));
|
|
26107
26140
|
else this.closeFrame();
|
|
@@ -26123,6 +26156,11 @@ var Parser = class {
|
|
|
26123
26156
|
case "group":
|
|
26124
26157
|
this.parseGroup(tokens, line);
|
|
26125
26158
|
break;
|
|
26159
|
+
// A keyword only as `system id … {`, so a node may still be called system.
|
|
26160
|
+
case "system":
|
|
26161
|
+
if (second?.kind === "ident" && tokens[tokens.length - 1].kind === "lbrace") this.parseSystem(tokens, line);
|
|
26162
|
+
else this.parseNode(tokens, line);
|
|
26163
|
+
break;
|
|
26126
26164
|
case "usecase":
|
|
26127
26165
|
this.parseUseCase(tokens, line);
|
|
26128
26166
|
break;
|
|
@@ -26177,7 +26215,7 @@ var Parser = class {
|
|
|
26177
26215
|
return;
|
|
26178
26216
|
}
|
|
26179
26217
|
const summary = tokens[2]?.kind === "string" ? tokens[2] : void 0;
|
|
26180
|
-
if (this.file ===
|
|
26218
|
+
if (this.file === this.rootFile) {
|
|
26181
26219
|
this.title = value.value;
|
|
26182
26220
|
if (summary) this.summary = summary.value;
|
|
26183
26221
|
}
|
|
@@ -26209,7 +26247,7 @@ var Parser = class {
|
|
|
26209
26247
|
this.error("direction is only allowed at the top level or inside a group", this.locOf(keyword, line));
|
|
26210
26248
|
return;
|
|
26211
26249
|
}
|
|
26212
|
-
if (this.file !==
|
|
26250
|
+
if (this.file !== this.rootFile) return;
|
|
26213
26251
|
if (this.layoutLocs.direction) {
|
|
26214
26252
|
this.warning(`direction is already set ${this.where(this.layoutLocs.direction, loc)}; this one is ignored`, this.locOf(keyword, line));
|
|
26215
26253
|
return;
|
|
@@ -26229,7 +26267,7 @@ var Parser = class {
|
|
|
26229
26267
|
this.error("layout ordered applies to the whole diagram; write it at the top level", this.locOf(keyword, line));
|
|
26230
26268
|
return;
|
|
26231
26269
|
}
|
|
26232
|
-
if (this.file !==
|
|
26270
|
+
if (this.file !== this.rootFile) return;
|
|
26233
26271
|
if (this.layoutLocs.ordered) {
|
|
26234
26272
|
this.warning(`layout ordered is already set ${this.where(this.layoutLocs.ordered, loc)}`, this.locOf(keyword, line));
|
|
26235
26273
|
return;
|
|
@@ -26279,21 +26317,13 @@ var Parser = class {
|
|
|
26279
26317
|
const loc = this.locOf(pathToken, line);
|
|
26280
26318
|
const entry = { path, loc };
|
|
26281
26319
|
this.imports.push(entry);
|
|
26282
|
-
const { resolve:
|
|
26283
|
-
if (!
|
|
26320
|
+
const { resolve: resolve3 } = this.options;
|
|
26321
|
+
if (!resolve3) {
|
|
26284
26322
|
this.warning(`Imports are not resolved in this context; '${path}' was not loaded`, loc);
|
|
26285
26323
|
return;
|
|
26286
26324
|
}
|
|
26287
|
-
|
|
26288
|
-
|
|
26289
|
-
target = resolve4(path, this.file ?? this.options.path);
|
|
26290
|
-
} catch {
|
|
26291
|
-
target = void 0;
|
|
26292
|
-
}
|
|
26293
|
-
if (!target) {
|
|
26294
|
-
this.error(`Cannot find '${path}'`, loc);
|
|
26295
|
-
return;
|
|
26296
|
-
}
|
|
26325
|
+
const target = this.resolveFile(path, loc);
|
|
26326
|
+
if (!target) return;
|
|
26297
26327
|
entry.resolved = target.path;
|
|
26298
26328
|
const cycleStart = this.chain.indexOf(target.path);
|
|
26299
26329
|
if (cycleStart !== -1) {
|
|
@@ -26313,6 +26343,25 @@ var Parser = class {
|
|
|
26313
26343
|
this.chain.pop();
|
|
26314
26344
|
({ lines: this.lines, file: this.file, stack: this.stack } = outer);
|
|
26315
26345
|
}
|
|
26346
|
+
/**
|
|
26347
|
+
* Loads `path` through the resolver, relative to the file being read. A
|
|
26348
|
+
* resolver error named `ImportError` (outside the project root, not synced,
|
|
26349
|
+
* …) is reported with its own message; anything else is "Cannot find".
|
|
26350
|
+
*/
|
|
26351
|
+
resolveFile(path, loc) {
|
|
26352
|
+
let target;
|
|
26353
|
+
try {
|
|
26354
|
+
target = this.options.resolve?.(path, this.file ?? this.options.path);
|
|
26355
|
+
} catch (e) {
|
|
26356
|
+
if (e instanceof Error && e.name === "ImportError") {
|
|
26357
|
+
this.error(e.message, loc);
|
|
26358
|
+
return void 0;
|
|
26359
|
+
}
|
|
26360
|
+
target = void 0;
|
|
26361
|
+
}
|
|
26362
|
+
if (!target) this.error(`Cannot find '${path}'`, loc);
|
|
26363
|
+
return target;
|
|
26364
|
+
}
|
|
26316
26365
|
parseGroup(tokens, line) {
|
|
26317
26366
|
const keyword = tokens[0];
|
|
26318
26367
|
const top = this.top();
|
|
@@ -26372,6 +26421,273 @@ var Parser = class {
|
|
|
26372
26421
|
this.addNode(group);
|
|
26373
26422
|
this.stack.push({ kind: "group", id: idToken.value, loc });
|
|
26374
26423
|
}
|
|
26424
|
+
// ---- Systems: a team's design behind a contract (docs/design/team-systems-and-imports.md) ----
|
|
26425
|
+
/** `system id ["Name"] [@team] [pos x,y] [color c] [.tag] [rank N] {` */
|
|
26426
|
+
parseSystem(tokens, line) {
|
|
26427
|
+
const [keyword, idToken] = tokens;
|
|
26428
|
+
const top = this.top();
|
|
26429
|
+
if (top && top.kind !== "group") {
|
|
26430
|
+
this.error("A system is declared at the top level or inside a group", this.locOf(keyword, line));
|
|
26431
|
+
this.skipSection("test", tokens, line);
|
|
26432
|
+
return;
|
|
26433
|
+
}
|
|
26434
|
+
const loc = this.locOf(idToken, line);
|
|
26435
|
+
const node = { id: idToken.value, kind: "system", name: idToken.value, type: "service", techStack: SYSTEM_TECH, parent: this.currentGroup(), loc };
|
|
26436
|
+
const style = {};
|
|
26437
|
+
let named = false;
|
|
26438
|
+
let i = 2;
|
|
26439
|
+
while (i < tokens.length - 1) {
|
|
26440
|
+
const t = tokens[i];
|
|
26441
|
+
const styleUsed = this.readStylePart(tokens, i, line, style, false);
|
|
26442
|
+
const used2 = styleUsed === 0 ? this.readRank(tokens, i, line, node) : styleUsed;
|
|
26443
|
+
if (used2 === void 0) return this.skipSection("test", tokens, line);
|
|
26444
|
+
if (used2 > 0) {
|
|
26445
|
+
i += used2;
|
|
26446
|
+
continue;
|
|
26447
|
+
}
|
|
26448
|
+
if (t.kind === "string" && !named) {
|
|
26449
|
+
node.name = t.value;
|
|
26450
|
+
named = true;
|
|
26451
|
+
} else if (t.kind === "string" && node.description === void 0) {
|
|
26452
|
+
node.description = t.value;
|
|
26453
|
+
} else if (t.kind === "team") {
|
|
26454
|
+
node.ownerTeam = t.value;
|
|
26455
|
+
} else if (isWord(t, "pos")) {
|
|
26456
|
+
const [x, comma, y] = [tokens[i + 1], tokens[i + 2], tokens[i + 3]];
|
|
26457
|
+
if (x?.kind !== "number" || comma?.kind !== "comma" || y?.kind !== "number") {
|
|
26458
|
+
this.error("Expected pos x,y", this.locOf(t, line));
|
|
26459
|
+
return this.skipSection("test", tokens, line);
|
|
26460
|
+
}
|
|
26461
|
+
node.position = { x: Number(x.value), y: Number(y.value) };
|
|
26462
|
+
i += 3;
|
|
26463
|
+
} else {
|
|
26464
|
+
this.error(`Unexpected ${describe(t)} in system header; write system id "Name" @Team {`, this.locOf(t, line));
|
|
26465
|
+
return this.skipSection("test", tokens, line);
|
|
26466
|
+
}
|
|
26467
|
+
i++;
|
|
26468
|
+
}
|
|
26469
|
+
this.applyStyle(node, style);
|
|
26470
|
+
const system = { id: node.id, entries: [], loc };
|
|
26471
|
+
if (!this.nodes.has(node.id)) {
|
|
26472
|
+
this.systems.push(system);
|
|
26473
|
+
this.systemById.set(system.id, system);
|
|
26474
|
+
}
|
|
26475
|
+
this.addNode(node);
|
|
26476
|
+
this.stack.push({ kind: "system", system, useCaseLocs: /* @__PURE__ */ new Map(), loc });
|
|
26477
|
+
}
|
|
26478
|
+
/** A line inside `system … { }` or its multi-line `capacity { }`. */
|
|
26479
|
+
parseSystemLine(frame, tokens, line) {
|
|
26480
|
+
const [first, second] = tokens;
|
|
26481
|
+
const { system } = frame;
|
|
26482
|
+
if (first.kind === "rbrace") {
|
|
26483
|
+
this.stack.pop();
|
|
26484
|
+
if (frame.kind === "system") this.finishSystem(frame);
|
|
26485
|
+
if (second) this.error("Unexpected input after }", this.locOf(second, line));
|
|
26486
|
+
return;
|
|
26487
|
+
}
|
|
26488
|
+
if (frame.kind === "stub") {
|
|
26489
|
+
this.readStub(system.stub, tokens, 0, tokens.length, line);
|
|
26490
|
+
return;
|
|
26491
|
+
}
|
|
26492
|
+
if (isWord(first, "from")) {
|
|
26493
|
+
if (second?.kind !== "string" || !second.value.trim()) {
|
|
26494
|
+
this.error('Expected the team file, e.g. from "teams/orders.proschi"', this.locOf(second ?? first, line));
|
|
26495
|
+
return;
|
|
26496
|
+
}
|
|
26497
|
+
if (system.from !== void 0) {
|
|
26498
|
+
this.error(`System '${system.id}' already has from "${system.from}"`, this.locOf(first, line));
|
|
26499
|
+
return;
|
|
26500
|
+
}
|
|
26501
|
+
this.expectEnd(tokens, 2, line);
|
|
26502
|
+
system.from = second.value;
|
|
26503
|
+
frame.fromLoc = this.locOf(second, line);
|
|
26504
|
+
return;
|
|
26505
|
+
}
|
|
26506
|
+
if (isWord(first, "expose")) return this.parseExpose(frame, tokens, line);
|
|
26507
|
+
if (isWord(first, "capacity") && second?.kind === "lbrace") {
|
|
26508
|
+
const opens = tokens[tokens.length - 1].kind !== "rbrace";
|
|
26509
|
+
if (system.stub) {
|
|
26510
|
+
this.error(`System '${system.id}' already has a capacity`, this.locOf(first, line));
|
|
26511
|
+
if (opens) this.stack.push({ kind: "section", section: "capacity", discard: true, loc: this.locOf(first, line) });
|
|
26512
|
+
return;
|
|
26513
|
+
}
|
|
26514
|
+
const stub = { loc: this.lineLoc(tokens, line) };
|
|
26515
|
+
system.stub = stub;
|
|
26516
|
+
this.readStub(stub, tokens, 2, opens ? tokens.length : tokens.length - 1, line);
|
|
26517
|
+
if (opens) this.stack.push({ kind: "stub", system, loc: this.locOf(first, line) });
|
|
26518
|
+
return;
|
|
26519
|
+
}
|
|
26520
|
+
this.error('Inside a system, write from "file", expose <id> "Name" -> "Use case" or capacity { \u2026 }', this.locOf(first, line));
|
|
26521
|
+
if (tokens[tokens.length - 1].kind === "lbrace") this.stack.push({ kind: "section", section: "test", discard: true, loc: this.locOf(first, line) });
|
|
26522
|
+
}
|
|
26523
|
+
/** `expose id ["Name"] [-> "Use case"]` */
|
|
26524
|
+
parseExpose(frame, tokens, line) {
|
|
26525
|
+
const [keyword, idToken] = tokens;
|
|
26526
|
+
if (idToken?.kind !== "ident") {
|
|
26527
|
+
this.error('Expected an entry point id, e.g. expose api "Orders API" -> "Place order"', this.locOf(idToken ?? keyword, line));
|
|
26528
|
+
return;
|
|
26529
|
+
}
|
|
26530
|
+
const entry = { id: idToken.value, name: idToken.value, loc: this.locOf(idToken, line) };
|
|
26531
|
+
let i = 2;
|
|
26532
|
+
if (tokens[i]?.kind === "string") entry.name = tokens[i++].value;
|
|
26533
|
+
if (tokens[i]?.kind === "arrow") {
|
|
26534
|
+
const useCase = tokens[i + 1];
|
|
26535
|
+
if (tokens[i].value !== "->" || useCase?.kind !== "string" && useCase?.kind !== "ident") {
|
|
26536
|
+
this.error('Expected -> "Use case" after the entry point', this.locOf(useCase ?? tokens[i], line));
|
|
26537
|
+
return;
|
|
26538
|
+
}
|
|
26539
|
+
entry.useCase = useCase.value;
|
|
26540
|
+
frame.useCaseLocs.set(entry, this.locOf(useCase, line));
|
|
26541
|
+
i += 2;
|
|
26542
|
+
}
|
|
26543
|
+
if (!this.expectEnd(tokens, i, line)) return;
|
|
26544
|
+
if (frame.system.entries.some((e) => e.id === entry.id)) {
|
|
26545
|
+
this.error(`System '${frame.system.id}' already exposes '${entry.id}'`, entry.loc);
|
|
26546
|
+
return;
|
|
26547
|
+
}
|
|
26548
|
+
frame.system.entries.push(entry);
|
|
26549
|
+
}
|
|
26550
|
+
/** The parts of a system's `capacity { … }` in `tokens[from, to)`. */
|
|
26551
|
+
readStub(stub, tokens, from, to, line) {
|
|
26552
|
+
for (let i = from; i < to; ) {
|
|
26553
|
+
const t = tokens[i];
|
|
26554
|
+
if (t.kind === "quantity" || t.kind === "number") {
|
|
26555
|
+
const rps = this.quantityOf(t, "rps", t, line);
|
|
26556
|
+
if (rps === void 0) return;
|
|
26557
|
+
stub.rps = rps;
|
|
26558
|
+
i++;
|
|
26559
|
+
} else if (isWord(t, "p99") || isWord(t, "latency") || isWord(t, "availability")) {
|
|
26560
|
+
const value = this.quantityOf(i + 1 < to ? tokens[i + 1] : void 0, t.value === "availability" ? "%" : "ms", t, line);
|
|
26561
|
+
if (value === void 0) return;
|
|
26562
|
+
if (t.value === "p99") stub.p99Ms = value;
|
|
26563
|
+
else if (t.value === "latency") stub.latencyMs = value;
|
|
26564
|
+
else stub.availability = value;
|
|
26565
|
+
i += 2;
|
|
26566
|
+
} else {
|
|
26567
|
+
this.error(`Expected ${STUB_HELP}`, this.locOf(t, line));
|
|
26568
|
+
return;
|
|
26569
|
+
}
|
|
26570
|
+
}
|
|
26571
|
+
}
|
|
26572
|
+
/** At the system's `}`: loads the team file and checks the entry points against its use cases. */
|
|
26573
|
+
finishSystem(frame) {
|
|
26574
|
+
const { system } = frame;
|
|
26575
|
+
if (system.from !== void 0) {
|
|
26576
|
+
const team2 = this.loadTeam(system.from, frame.fromLoc ?? system.loc);
|
|
26577
|
+
if (team2) {
|
|
26578
|
+
system.resolved = team2.path;
|
|
26579
|
+
system.diagram = team2.diagram;
|
|
26580
|
+
}
|
|
26581
|
+
if (system.stub) {
|
|
26582
|
+
this.warning(`System '${system.id}' has a from file, which sets its contract; this capacity is ignored`, system.stub.loc);
|
|
26583
|
+
if (team2) delete system.stub;
|
|
26584
|
+
}
|
|
26585
|
+
} else if (!system.stub) {
|
|
26586
|
+
this.warning(`System '${system.id}' has no from "file" and no capacity { \u2026 }; it is drawn but not simulated`, system.loc);
|
|
26587
|
+
}
|
|
26588
|
+
const team = system.diagram;
|
|
26589
|
+
if (!team) return;
|
|
26590
|
+
const name = fileName(system.resolved ?? "");
|
|
26591
|
+
for (const entry of system.entries) {
|
|
26592
|
+
const wanted = entry.useCase;
|
|
26593
|
+
if (wanted !== void 0) {
|
|
26594
|
+
const u = findUseCaseIn(team, wanted);
|
|
26595
|
+
if (u) entry.useCaseId = u.id;
|
|
26596
|
+
else {
|
|
26597
|
+
const near = team.useCases.find((x) => editDistance2(x.name.toLowerCase(), wanted.toLowerCase()) <= Math.max(1, Math.floor(wanted.length / 4)));
|
|
26598
|
+
const all = team.useCases.map((x) => `"${x.name}"`).join(", ");
|
|
26599
|
+
const help = near ? ` Did you mean "${near.name}"?` : all ? ` Its use cases are ${all}` : "";
|
|
26600
|
+
this.error(`${name} has no use case "${wanted}".${help}`, frame.useCaseLocs.get(entry) ?? entry.loc);
|
|
26601
|
+
}
|
|
26602
|
+
} else if (team.useCases.length === 1) {
|
|
26603
|
+
entry.useCaseId = team.useCases[0].id;
|
|
26604
|
+
} else if (team.useCases.length === 0) {
|
|
26605
|
+
this.error(`${name} has no use case for '${entry.id}' to enter`, entry.loc);
|
|
26606
|
+
} else {
|
|
26607
|
+
this.error(`Say which use case '${entry.id}' enters, e.g. expose ${entry.id} -> "${team.useCases[0].name}"`, entry.loc);
|
|
26608
|
+
}
|
|
26609
|
+
}
|
|
26610
|
+
if (system.entries.length === 0) this.warning(`System '${system.id}' exposes no entry point; add expose <id> -> "Use case" so others can call it`, system.loc);
|
|
26611
|
+
}
|
|
26612
|
+
/** Parses a team file in its own scope (once per root parse), through the import resolver. */
|
|
26613
|
+
loadTeam(path, loc) {
|
|
26614
|
+
if (!this.options.resolve) {
|
|
26615
|
+
this.warning(`Files are not resolved in this context; '${path}' was not loaded`, loc);
|
|
26616
|
+
return void 0;
|
|
26617
|
+
}
|
|
26618
|
+
const target = this.resolveFile(path, loc);
|
|
26619
|
+
if (!target) return void 0;
|
|
26620
|
+
const cycleStart = this.chain.indexOf(target.path);
|
|
26621
|
+
if (cycleStart !== -1) {
|
|
26622
|
+
const names = [...this.chain.slice(cycleStart), target.path].map((p) => fileName(p ?? ""));
|
|
26623
|
+
this.error(`System cycle: ${names.join(" \u2192 ")}`, loc);
|
|
26624
|
+
return void 0;
|
|
26625
|
+
}
|
|
26626
|
+
let result = this.teamFiles.get(target.path);
|
|
26627
|
+
if (!result) {
|
|
26628
|
+
const scope2 = { chain: [...this.chain, target.path], teamFiles: this.teamFiles };
|
|
26629
|
+
result = new _Parser(target.source, { path: target.path, resolve: this.options.resolve }, scope2).run();
|
|
26630
|
+
this.teamFiles.set(target.path, result);
|
|
26631
|
+
for (const d of result.diagnostics) {
|
|
26632
|
+
if (!this.fileOrder.has(d.file)) this.fileOrder.set(d.file, this.fileOrder.size);
|
|
26633
|
+
this.diagnostics.push(d);
|
|
26634
|
+
}
|
|
26635
|
+
}
|
|
26636
|
+
return { path: target.path, diagram: result.diagram };
|
|
26637
|
+
}
|
|
26638
|
+
/**
|
|
26639
|
+
* Resolves `orders.api` references once every file is read: the entry point
|
|
26640
|
+
* `api` of system `orders`. Edges and references then name the system, with
|
|
26641
|
+
* the entry point in `sourcePort` / `targetPort`. On a connection to a
|
|
26642
|
+
* non-system, `b.planned` stays what it always was: `b` with a tag.
|
|
26643
|
+
*/
|
|
26644
|
+
resolvePorts() {
|
|
26645
|
+
for (const ref of this.references) {
|
|
26646
|
+
const dot = ref.id.indexOf(".");
|
|
26647
|
+
if (dot === -1) continue;
|
|
26648
|
+
const prefix = ref.id.slice(0, dot);
|
|
26649
|
+
const port = ref.id.slice(dot + 1);
|
|
26650
|
+
const system = this.systemById.get(prefix);
|
|
26651
|
+
if (system) {
|
|
26652
|
+
if (!system.entries.some((e) => e.id === port)) this.error(this.unexposedMessage(system, port), ref.loc);
|
|
26653
|
+
} else if (ref.edgeTarget) {
|
|
26654
|
+
ref.edgeTarget.tag = port;
|
|
26655
|
+
this.tagRefs.push({ tag: port, loc: { ...ref.loc, col: ref.loc.col + dot, length: ref.loc.length - dot }, target: ref.edgeTarget });
|
|
26656
|
+
} else {
|
|
26657
|
+
this.error(`'${prefix}' is not a system, so '${ref.id}' names nothing; write ${prefix}, or declare system ${prefix} { \u2026 }`, ref.loc);
|
|
26658
|
+
}
|
|
26659
|
+
ref.id = prefix;
|
|
26660
|
+
}
|
|
26661
|
+
for (const edge of this.edges) {
|
|
26662
|
+
const [source2, sourcePort] = this.splitRef(edge.source);
|
|
26663
|
+
const [target, targetPort] = this.splitRef(edge.target);
|
|
26664
|
+
edge.source = source2;
|
|
26665
|
+
edge.target = target;
|
|
26666
|
+
if (sourcePort !== void 0) edge.sourcePort = sourcePort;
|
|
26667
|
+
if (targetPort !== void 0) edge.targetPort = targetPort;
|
|
26668
|
+
}
|
|
26669
|
+
}
|
|
26670
|
+
unexposedMessage(system, port) {
|
|
26671
|
+
const entries2 = system.entries.map((e) => `${system.id}.${e.id}`);
|
|
26672
|
+
const known = entries2.length ? `; connect to ${entries2.join(" or ")}` : "";
|
|
26673
|
+
const internal = system.diagram?.nodes.some((n) => n.id === port);
|
|
26674
|
+
return internal ? `'${port}' is inside system '${system.id}' but is not an entry point; only entry points are reachable from outside: add expose ${port} -> "Use case"${known}` : `System '${system.id}' has no entry point '${port}'${known}`;
|
|
26675
|
+
}
|
|
26676
|
+
/** `orders.api` → `['orders', 'api']` when `orders` is a system; else the id up to its dot, without a port. */
|
|
26677
|
+
splitRef(id) {
|
|
26678
|
+
const dot = id.indexOf(".");
|
|
26679
|
+
if (dot === -1) return [id, void 0];
|
|
26680
|
+
const prefix = id.slice(0, dot);
|
|
26681
|
+
return [prefix, this.systemById.has(prefix) ? id.slice(dot + 1) : void 0];
|
|
26682
|
+
}
|
|
26683
|
+
/** A step to a whole system enters its only entry point; with several it must name one. */
|
|
26684
|
+
onlyEntry(id, loc) {
|
|
26685
|
+
const system = this.systemById.get(id);
|
|
26686
|
+
if (!system || system.entries.length === 0) return void 0;
|
|
26687
|
+
if (system.entries.length === 1) return system.entries[0].id;
|
|
26688
|
+
this.errorOnce(`Say which entry point of '${id}' this step enters: ${system.entries.map((e) => `${id}.${e.id}`).join(" or ")}`, loc);
|
|
26689
|
+
return void 0;
|
|
26690
|
+
}
|
|
26375
26691
|
parseUseCase(tokens, line) {
|
|
26376
26692
|
const keyword = tokens[0];
|
|
26377
26693
|
if (this.stack.length > 0) {
|
|
@@ -26561,7 +26877,8 @@ var Parser = class {
|
|
|
26561
26877
|
const from = fromToken.value;
|
|
26562
26878
|
const to = toToken.value;
|
|
26563
26879
|
const arrow = arrowToken.value;
|
|
26564
|
-
|
|
26880
|
+
const toRef = { id: to, loc: this.locOf(toToken, line) };
|
|
26881
|
+
this.references.push({ id: from, loc: this.locOf(fromToken, line) }, toRef);
|
|
26565
26882
|
const top = this.top();
|
|
26566
26883
|
const inUseCase = top?.kind === "usecase" || top?.kind === "alt" || top?.kind === "par";
|
|
26567
26884
|
if (tagToken && inUseCase) {
|
|
@@ -26596,6 +26913,7 @@ var Parser = class {
|
|
|
26596
26913
|
edge.tag = tagToken.value;
|
|
26597
26914
|
this.tagRefs.push({ tag: tagToken.value, loc: this.locOf(tagToken, line), target: edge });
|
|
26598
26915
|
}
|
|
26916
|
+
if (!tagToken) toRef.edgeTarget = edge;
|
|
26599
26917
|
this.edges.push(edge);
|
|
26600
26918
|
}
|
|
26601
26919
|
return lastLine;
|
|
@@ -27506,7 +27824,9 @@ var Parser = class {
|
|
|
27506
27824
|
for (const step of raw) {
|
|
27507
27825
|
this.checkConnection(step);
|
|
27508
27826
|
if (step.arrow === "-->") {
|
|
27509
|
-
const
|
|
27827
|
+
const [respFrom] = this.splitRef(step.from);
|
|
27828
|
+
const [respTo] = this.splitRef(step.to);
|
|
27829
|
+
const request2 = [...steps].reverse().find((s) => s.fromServiceId === respTo && s.toServiceId === respFrom && !answered.has(s) && !s.failed);
|
|
27510
27830
|
if (!request2) {
|
|
27511
27831
|
this.warning(`Response has no matching request from '${step.to}' to '${step.from}'`, step.loc);
|
|
27512
27832
|
continue;
|
|
@@ -27525,14 +27845,17 @@ var Parser = class {
|
|
|
27525
27845
|
continue;
|
|
27526
27846
|
}
|
|
27527
27847
|
const label = parseStepLabel(step.label);
|
|
27528
|
-
const
|
|
27848
|
+
const [from, fromPort] = this.splitRef(step.from);
|
|
27849
|
+
const [to, toPort] = this.splitRef(step.to);
|
|
27850
|
+
const port = toPort ?? this.onlyEntry(to, step.loc);
|
|
27851
|
+
const target = this.nodes.get(to);
|
|
27529
27852
|
const isAsync = step.arrow === "->>";
|
|
27530
27853
|
steps.push({
|
|
27531
27854
|
id: `${prefix}-${steps.length + 1}`,
|
|
27532
27855
|
stepOrder: steps.length,
|
|
27533
27856
|
stepName: label.name || `${step.from} \u2192 ${step.to}`,
|
|
27534
|
-
fromServiceId:
|
|
27535
|
-
toServiceId:
|
|
27857
|
+
fromServiceId: from,
|
|
27858
|
+
toServiceId: to,
|
|
27536
27859
|
protocol: inferProtocol(isAsync, label.method, target),
|
|
27537
27860
|
httpMethod: label.method ?? "",
|
|
27538
27861
|
endpoint: label.endpoint ?? "",
|
|
@@ -27548,7 +27871,9 @@ var Parser = class {
|
|
|
27548
27871
|
loc: step.loc,
|
|
27549
27872
|
...step.multiplier !== void 0 ? { multiplier: step.multiplier } : {},
|
|
27550
27873
|
...step.sizeBytes !== void 0 ? { sizeBytes: step.sizeBytes } : {},
|
|
27551
|
-
access: accessOf(label.method, step.label)
|
|
27874
|
+
access: accessOf(label.method, step.label),
|
|
27875
|
+
...port !== void 0 ? { toPort: port } : {},
|
|
27876
|
+
...fromPort !== void 0 ? { fromPort } : {}
|
|
27552
27877
|
});
|
|
27553
27878
|
}
|
|
27554
27879
|
return steps;
|
|
@@ -27558,8 +27883,10 @@ var Parser = class {
|
|
|
27558
27883
|
* Documents without any connection are use-case-only sketches; they are left alone.
|
|
27559
27884
|
*/
|
|
27560
27885
|
checkConnection(step) {
|
|
27561
|
-
|
|
27562
|
-
const [
|
|
27886
|
+
const [a] = this.splitRef(step.from);
|
|
27887
|
+
const [b] = this.splitRef(step.to);
|
|
27888
|
+
if (this.edges.length === 0 || a === b || this.connected.has(`${a}|${b}`)) return;
|
|
27889
|
+
const [from, to] = step.arrow === "-->" ? [b, a] : [a, b];
|
|
27563
27890
|
this.warning(`No connection between '${from}' and '${to}' in the architecture; add '${from} -> ${to}'`, step.loc);
|
|
27564
27891
|
}
|
|
27565
27892
|
createImplicitNodes() {
|
|
@@ -27655,6 +27982,17 @@ var Parser = class {
|
|
|
27655
27982
|
error(message, loc) {
|
|
27656
27983
|
this.diagnostics.push({ severity: "error", message, ...loc });
|
|
27657
27984
|
}
|
|
27985
|
+
/** An error for a step that every scenario sharing it would report again. */
|
|
27986
|
+
errorOnce(message, loc) {
|
|
27987
|
+
const key = `error
|
|
27988
|
+
${loc.file ?? ""}
|
|
27989
|
+
${loc.line}
|
|
27990
|
+
${loc.col}
|
|
27991
|
+
${message}`;
|
|
27992
|
+
if (this.warned.has(key)) return;
|
|
27993
|
+
this.warned.add(key);
|
|
27994
|
+
this.error(message, loc);
|
|
27995
|
+
}
|
|
27658
27996
|
warning(message, loc) {
|
|
27659
27997
|
const key = `${loc.file ?? ""}
|
|
27660
27998
|
${loc.line}
|
|
@@ -27690,6 +28028,10 @@ function expand(items) {
|
|
|
27690
28028
|
}
|
|
27691
28029
|
return paths;
|
|
27692
28030
|
}
|
|
28031
|
+
function findUseCaseIn(diagram, name) {
|
|
28032
|
+
const lower = name.toLowerCase();
|
|
28033
|
+
return diagram.useCases.find((u) => u.name === name) ?? diagram.useCases.find((u) => u.name.toLowerCase() === lower) ?? diagram.useCases.find((u) => u.id === name);
|
|
28034
|
+
}
|
|
27693
28035
|
function isWord(token, word) {
|
|
27694
28036
|
return token?.kind === "ident" && token.value === word;
|
|
27695
28037
|
}
|
|
@@ -28489,6 +28831,340 @@ function formatUsd(usd) {
|
|
|
28489
28831
|
return `$${Math.round(usd).toLocaleString("en-US")}/month`;
|
|
28490
28832
|
}
|
|
28491
28833
|
|
|
28834
|
+
// ../frontend/src/sim/overlay.ts
|
|
28835
|
+
function flowsOf(diagram, traffic) {
|
|
28836
|
+
const flows = /* @__PURE__ */ new Map();
|
|
28837
|
+
for (const t of traffic) {
|
|
28838
|
+
const u = findUseCase(diagram, t.useCase);
|
|
28839
|
+
if (!u || t.rps <= 0) continue;
|
|
28840
|
+
for (const sc of u.scenarios) {
|
|
28841
|
+
const share = t.mix ? t.mix.find((m) => m.scenario === sc.name)?.share ?? 0 : sc === u.scenarios[0] ? 1 : 0;
|
|
28842
|
+
if (share <= 0) continue;
|
|
28843
|
+
for (const step of sc.steps) {
|
|
28844
|
+
if (step.failed) continue;
|
|
28845
|
+
const key = `${step.fromServiceId}>${step.toServiceId}`;
|
|
28846
|
+
const f = flows.get(key) ?? { from: step.fromServiceId, to: step.toServiceId, rps: 0, async: step.executionType !== "SYNC_REQUEST_RESPONSE" };
|
|
28847
|
+
f.rps += t.rps * share * (step.multiplier ?? 1);
|
|
28848
|
+
flows.set(key, f);
|
|
28849
|
+
}
|
|
28850
|
+
}
|
|
28851
|
+
}
|
|
28852
|
+
return [...flows.values()];
|
|
28853
|
+
}
|
|
28854
|
+
function analysisOverlay(diagram, analysis) {
|
|
28855
|
+
const nodes = {};
|
|
28856
|
+
for (const n of analysis.nodes) {
|
|
28857
|
+
const works = Number.isFinite(n.capacityRps) && n.kind !== "client";
|
|
28858
|
+
nodes[n.id] = {
|
|
28859
|
+
...works ? { utilization: n.utilization, saturated: n.saturated } : {},
|
|
28860
|
+
...n.replicas > 1 ? { replicas: n.replicas } : {},
|
|
28861
|
+
...n.shards > 1 ? { shards: n.shards } : {}
|
|
28862
|
+
};
|
|
28863
|
+
}
|
|
28864
|
+
return { nodes, flows: flowsOf(diagram, diagram.traffic ?? []) };
|
|
28865
|
+
}
|
|
28866
|
+
|
|
28867
|
+
// ../frontend/src/sim/systems.ts
|
|
28868
|
+
var teamKey = (s) => s.resolved ?? s.diagram;
|
|
28869
|
+
var contracts = /* @__PURE__ */ new WeakMap();
|
|
28870
|
+
function contractOf(system, options = {}) {
|
|
28871
|
+
let contract = contracts.get(system);
|
|
28872
|
+
if (!contract) {
|
|
28873
|
+
contract = system.diagram ? designContract(system, system.diagram, options) : stubContract(system);
|
|
28874
|
+
contracts.set(system, contract);
|
|
28875
|
+
}
|
|
28876
|
+
return contract;
|
|
28877
|
+
}
|
|
28878
|
+
function stubContract(system) {
|
|
28879
|
+
const stub = system.stub;
|
|
28880
|
+
if (!stub) return { source: "none", entries: system.entries.map((e) => emptyEntry(e.id, e.name)) };
|
|
28881
|
+
const rps = stub.rps ?? Infinity;
|
|
28882
|
+
return {
|
|
28883
|
+
source: "stub",
|
|
28884
|
+
entries: system.entries.map((e) => ({
|
|
28885
|
+
...emptyEntry(e.id, e.name),
|
|
28886
|
+
latency: stub.p99Ms !== void 0 ? [{ percentile: 99, maxMs: stub.p99Ms }] : [],
|
|
28887
|
+
...stub.availability !== void 0 ? { availabilityPercent: stub.availability } : {},
|
|
28888
|
+
saturatesAtRps: rps,
|
|
28889
|
+
hotAtRps: rps * HOT
|
|
28890
|
+
}))
|
|
28891
|
+
};
|
|
28892
|
+
}
|
|
28893
|
+
var emptyEntry = (entry, name) => ({ entry, name, ownRps: 0, latency: [], saturatesAtRps: Infinity, hotAtRps: Infinity });
|
|
28894
|
+
function designContract(system, team, options) {
|
|
28895
|
+
const traffic = resolveTraffic(team);
|
|
28896
|
+
const base = computeLoad(team, traffic);
|
|
28897
|
+
const baseBytes = computeBytesPerSecond(team, traffic);
|
|
28898
|
+
const profiles = profilesOf(team);
|
|
28899
|
+
const nodes = components(team);
|
|
28900
|
+
const entries2 = system.entries.map((e) => {
|
|
28901
|
+
const u = team.useCases.find((x) => x.id === e.useCaseId);
|
|
28902
|
+
if (!u) return emptyEntry(e.id, e.name);
|
|
28903
|
+
const own = traffic.get(u.id);
|
|
28904
|
+
const unit = /* @__PURE__ */ new Map([[u.id, { rps: 1, shares: own.shares }]]);
|
|
28905
|
+
const delta = computeLoad(team, unit);
|
|
28906
|
+
const deltaBytes = computeBytesPerSecond(team, unit);
|
|
28907
|
+
const result = { ...emptyEntry(e.id, e.name), useCase: u.name, ownRps: own.rps };
|
|
28908
|
+
for (const n of nodes) {
|
|
28909
|
+
const d = delta.get(n.id);
|
|
28910
|
+
const db = deltaBytes.get(n.id) ?? 0;
|
|
28911
|
+
if (!d && !db) continue;
|
|
28912
|
+
const p = profiles.get(n.id);
|
|
28913
|
+
const replicas = replicasOf(n);
|
|
28914
|
+
const b = base.get(n.id) ?? { read: 0, write: 0 };
|
|
28915
|
+
const bb = baseBytes.get(n.id) ?? 0;
|
|
28916
|
+
const u0 = usageOf(p, replicas, b.read, b.write, bb);
|
|
28917
|
+
const u1 = usageOf(p, replicas, b.read + (d?.read ?? 0), b.write + (d?.write ?? 0), bb + db);
|
|
28918
|
+
const sat = crossing(u0, u1, 1);
|
|
28919
|
+
const hot = crossing(u0, u1, HOT);
|
|
28920
|
+
if (sat < result.saturatesAtRps) {
|
|
28921
|
+
result.saturatesAtRps = sat;
|
|
28922
|
+
result.bottleneck = n.id;
|
|
28923
|
+
}
|
|
28924
|
+
if (hot < result.hotAtRps) {
|
|
28925
|
+
result.hotAtRps = hot;
|
|
28926
|
+
result.hotNode = n.id;
|
|
28927
|
+
}
|
|
28928
|
+
}
|
|
28929
|
+
for (const r of team.requirements ?? []) {
|
|
28930
|
+
if (r.kind !== "latency" && r.kind !== "availability") continue;
|
|
28931
|
+
if (r.useCase !== void 0 && findUseCase(team, r.useCase) !== u) continue;
|
|
28932
|
+
if (r.kind === "latency") result.latency.push({ percentile: r.percentile, maxMs: r.maxMs, ...r.scenario !== void 0 ? { scenario: r.scenario } : {} });
|
|
28933
|
+
else result.availabilityPercent = Math.max(result.availabilityPercent ?? 0, r.minPercent);
|
|
28934
|
+
}
|
|
28935
|
+
return result;
|
|
28936
|
+
});
|
|
28937
|
+
return { source: "design", entries: entries2, baseline: analyze(team, { timeoutMs: options.timeoutMs }) };
|
|
28938
|
+
}
|
|
28939
|
+
function crossing(u0, u1, theta) {
|
|
28940
|
+
const terms = u0.writeScaling === "shards" ? [
|
|
28941
|
+
[u0.readUtilization, u1.readUtilization],
|
|
28942
|
+
[u0.writeUtilization, u1.writeUtilization]
|
|
28943
|
+
] : [[u0.requestUtilization, u1.requestUtilization]];
|
|
28944
|
+
terms.push([u0.bandwidthUtilization, u1.bandwidthUtilization]);
|
|
28945
|
+
let best = Infinity;
|
|
28946
|
+
for (const [a, b] of terms) {
|
|
28947
|
+
const slope = b - a;
|
|
28948
|
+
if (slope <= 1e-15) continue;
|
|
28949
|
+
if (a >= theta) return 0;
|
|
28950
|
+
best = Math.min(best, (theta - a) / slope);
|
|
28951
|
+
}
|
|
28952
|
+
return best;
|
|
28953
|
+
}
|
|
28954
|
+
function inboundLoad(diagram, traffic) {
|
|
28955
|
+
const result = /* @__PURE__ */ new Map();
|
|
28956
|
+
forEachStepRate(diagram, traffic, (step, rate) => {
|
|
28957
|
+
if (step.toPort === void 0) return;
|
|
28958
|
+
const ports = result.get(step.toServiceId) ?? /* @__PURE__ */ new Map();
|
|
28959
|
+
ports.set(step.toPort, (ports.get(step.toPort) ?? 0) + rate * multiplierOf(step));
|
|
28960
|
+
result.set(step.toServiceId, ports);
|
|
28961
|
+
});
|
|
28962
|
+
return result;
|
|
28963
|
+
}
|
|
28964
|
+
function hopFrom(p50, p99) {
|
|
28965
|
+
const l50 = tailFactor(0.5);
|
|
28966
|
+
const l99 = tailFactor(0.99);
|
|
28967
|
+
let tailMs = Math.max(0, (p99 - p50) / (l99 - l50));
|
|
28968
|
+
let fixedMs = p50 - tailMs * l50;
|
|
28969
|
+
if (fixedMs < 0) {
|
|
28970
|
+
fixedMs = 0;
|
|
28971
|
+
tailMs = p99 / l99;
|
|
28972
|
+
}
|
|
28973
|
+
return { fixedMs, tailMs, waitProbability: 0 };
|
|
28974
|
+
}
|
|
28975
|
+
function analyzeSystems(diagram, traffic, options = {}) {
|
|
28976
|
+
const inbound = inboundLoad(diagram, traffic);
|
|
28977
|
+
if (!options.sharedSystems) options = { ...options, sharedSystems: sharedLoad(diagram, inbound) };
|
|
28978
|
+
const hops = /* @__PURE__ */ new Map();
|
|
28979
|
+
const availabilities = /* @__PURE__ */ new Map();
|
|
28980
|
+
const key = (id, port) => `${id}
|
|
28981
|
+
${port}`;
|
|
28982
|
+
const systems = (diagram.systems ?? []).map((system) => {
|
|
28983
|
+
const result = analyzeSystem(system, inbound.get(system.id) ?? /* @__PURE__ */ new Map(), options);
|
|
28984
|
+
for (const e of result.entries) {
|
|
28985
|
+
hops.set(key(system.id, e.entry), hopFrom(e.p50Ms, e.p99Ms));
|
|
28986
|
+
availabilities.set(key(system.id, e.entry), e.availability);
|
|
28987
|
+
}
|
|
28988
|
+
return result;
|
|
28989
|
+
});
|
|
28990
|
+
return {
|
|
28991
|
+
systems,
|
|
28992
|
+
hop: (id, port) => port === void 0 ? void 0 : hops.get(key(id, port)),
|
|
28993
|
+
availability: (id, port) => port === void 0 ? void 0 : availabilities.get(key(id, port))
|
|
28994
|
+
};
|
|
28995
|
+
}
|
|
28996
|
+
function sentByUseCase(system, sent, into) {
|
|
28997
|
+
let total = 0;
|
|
28998
|
+
for (const e of system.entries) {
|
|
28999
|
+
const rps = sent.get(e.id) ?? 0;
|
|
29000
|
+
if (rps <= 0 || e.useCaseId === void 0) continue;
|
|
29001
|
+
into.set(e.useCaseId, (into.get(e.useCaseId) ?? 0) + rps);
|
|
29002
|
+
total += rps;
|
|
29003
|
+
}
|
|
29004
|
+
return total;
|
|
29005
|
+
}
|
|
29006
|
+
function sharedLoad(root, rootInbound) {
|
|
29007
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
29008
|
+
const designs = /* @__PURE__ */ new Map();
|
|
29009
|
+
const order = [];
|
|
29010
|
+
const visit = (d, prefix) => {
|
|
29011
|
+
for (const s of d.systems ?? []) {
|
|
29012
|
+
if (!s.diagram) continue;
|
|
29013
|
+
const key = teamKey(s);
|
|
29014
|
+
if (designs.has(key)) continue;
|
|
29015
|
+
const path = prefix ? `${prefix}.${s.id}` : s.id;
|
|
29016
|
+
designs.set(key, { diagram: s.diagram, path, loc: s.loc });
|
|
29017
|
+
visit(s.diagram, path);
|
|
29018
|
+
order.push(key);
|
|
29019
|
+
}
|
|
29020
|
+
};
|
|
29021
|
+
visit(root, "");
|
|
29022
|
+
const send = (d, prefix, inbound) => {
|
|
29023
|
+
for (const s of d.systems ?? []) {
|
|
29024
|
+
if (!s.diagram) continue;
|
|
29025
|
+
const key = teamKey(s);
|
|
29026
|
+
const load = byFile.get(key) ?? { useCases: /* @__PURE__ */ new Map(), callers: [] };
|
|
29027
|
+
byFile.set(key, load);
|
|
29028
|
+
const rps = sentByUseCase(s, inbound.get(s.id) ?? /* @__PURE__ */ new Map(), load.useCases);
|
|
29029
|
+
if (rps > 0) load.callers.push({ path: prefix ? `${prefix}.${s.id}` : s.id, ...d.title !== void 0 ? { from: d.title } : {}, rps });
|
|
29030
|
+
}
|
|
29031
|
+
};
|
|
29032
|
+
send(root, "", rootInbound);
|
|
29033
|
+
for (const key of order.reverse()) {
|
|
29034
|
+
const { diagram, path, loc } = designs.get(key);
|
|
29035
|
+
if (!diagram.systems?.length) continue;
|
|
29036
|
+
const combined = withExtraTraffic(diagram, byFile.get(key)?.useCases ?? /* @__PURE__ */ new Map(), loc);
|
|
29037
|
+
send(diagram, path, inboundLoad(combined, resolveTraffic(combined)));
|
|
29038
|
+
}
|
|
29039
|
+
return { byFile, analyses: /* @__PURE__ */ new Map() };
|
|
29040
|
+
}
|
|
29041
|
+
function analyzeSystem(system, sent, options) {
|
|
29042
|
+
const contract = contractOf(system, options);
|
|
29043
|
+
const inbound = system.entries.map((e) => ({ entry: e.id, rps: sent.get(e.id) ?? 0 }));
|
|
29044
|
+
const totalInboundRps = inbound.reduce((sum, e) => sum + e.rps, 0);
|
|
29045
|
+
const result = { id: system.id, contract, inbound, totalInboundRps, utilization: 0, saturated: false, hot: false, breaches: [], entries: [], warnings: [] };
|
|
29046
|
+
const shared = system.diagram ? options.sharedSystems?.byFile.get(teamKey(system)) : void 0;
|
|
29047
|
+
const sending = inbound.filter((e) => e.rps > 0).map((e) => {
|
|
29048
|
+
const c = contract.entries.find((x) => x.entry === e.entry);
|
|
29049
|
+
const useCaseId = system.entries.find((x) => x.id === e.entry)?.useCaseId;
|
|
29050
|
+
const gets = useCaseId !== void 0 ? shared?.useCases.get(useCaseId) ?? 0 : 0;
|
|
29051
|
+
const rps = gets > e.rps * (1 + 1e-9) ? `${formatRps(e.rps).replace(/ rps$/, "")} of the ${formatRps(gets)} it gets` : formatRps(e.rps);
|
|
29052
|
+
return `${rps} into ${c?.useCase ? `"${c.useCase}" (${system.id}.${e.entry})` : `${system.id}.${e.entry}`}`;
|
|
29053
|
+
}).join(" and ");
|
|
29054
|
+
const label = `'${system.id}'`;
|
|
29055
|
+
if (contract.source === "stub") {
|
|
29056
|
+
const stub = system.stub;
|
|
29057
|
+
const rps = stub.rps ?? Infinity;
|
|
29058
|
+
result.utilization = Number.isFinite(rps) && rps > 0 ? totalInboundRps / rps : 0;
|
|
29059
|
+
result.saturated = result.utilization >= 1;
|
|
29060
|
+
result.hot = !result.saturated && result.utilization > HOT;
|
|
29061
|
+
const base = stub.latencyMs ?? (stub.p99Ms !== void 0 ? stub.p99Ms / (1 - SERVICE_SPREAD + SERVICE_SPREAD * tailFactor(0.99)) : 0);
|
|
29062
|
+
const hop = hopModel(base, 0);
|
|
29063
|
+
const at = (p) => hop.fixedMs + hop.tailMs * tailFactor(p);
|
|
29064
|
+
const availability2 = stub.availability !== void 0 ? stub.availability / 100 : 1;
|
|
29065
|
+
result.entries = system.entries.map((e) => ({ entry: e.id, p50Ms: at(0.5), p99Ms: at(0.99), availability: availability2 }));
|
|
29066
|
+
if (result.saturated) {
|
|
29067
|
+
result.warnings.push(`${label} cannot take what it is sent: you send ${sending}; it takes at most ${formatRps(rps)}.`);
|
|
29068
|
+
if (stub.p99Ms !== void 0) result.breaches.push({ requirement: `p99 < ${formatMs(stub.p99Ms)}`, message: `p99 is not held past ${formatRps(rps)}` });
|
|
29069
|
+
} else if (result.hot) result.warnings.push(`${label} runs hot under what it is sent: you send ${sending}; it runs hot past ${formatRps(rps * HOT)} and takes at most ${formatRps(rps)}.`);
|
|
29070
|
+
return result;
|
|
29071
|
+
}
|
|
29072
|
+
if (contract.source === "none" || !system.diagram) {
|
|
29073
|
+
result.entries = system.entries.map((e) => ({ entry: e.id, p50Ms: 0, p99Ms: 0, availability: 1 }));
|
|
29074
|
+
return result;
|
|
29075
|
+
}
|
|
29076
|
+
const team = system.diagram;
|
|
29077
|
+
const key = teamKey(system);
|
|
29078
|
+
const sentToTeam = shared?.useCases ?? /* @__PURE__ */ new Map();
|
|
29079
|
+
if (!shared) sentByUseCase(system, sent, sentToTeam);
|
|
29080
|
+
const sharedInboundRps = [...sentToTeam.values()].reduce((sum, rps) => sum + rps, 0);
|
|
29081
|
+
result.callers = shared?.callers ?? (totalInboundRps > 0 ? [{ path: system.id, rps: totalInboundRps }] : []);
|
|
29082
|
+
result.sharedInboundRps = sharedInboundRps;
|
|
29083
|
+
let cached = options.sharedSystems?.analyses.get(key);
|
|
29084
|
+
if (!cached) {
|
|
29085
|
+
const combined = sharedInboundRps > 0 ? withExtraTraffic(team, sentToTeam, system.loc) : team;
|
|
29086
|
+
const nested = { timeoutMs: options.timeoutMs, ...options.sharedSystems ? { sharedSystems: options.sharedSystems } : {} };
|
|
29087
|
+
cached = { combined, analysis: sharedInboundRps > 0 ? analyze(combined, nested) : contract.baseline };
|
|
29088
|
+
options.sharedSystems?.analyses.set(key, cached);
|
|
29089
|
+
}
|
|
29090
|
+
const { analysis } = cached;
|
|
29091
|
+
result.analysis = analysis;
|
|
29092
|
+
result.overlay = analysisOverlay(cached.combined, analysis);
|
|
29093
|
+
const busiest = analysis.nodes.reduce((best, n) => !best || n.utilization > best.utilization ? n : best, void 0);
|
|
29094
|
+
result.utilization = busiest?.utilization ?? 0;
|
|
29095
|
+
result.busiest = busiest?.id;
|
|
29096
|
+
result.saturated = !!busiest?.saturated;
|
|
29097
|
+
result.hot = !result.saturated && result.utilization > HOT;
|
|
29098
|
+
const byId = new Map(analysis.useCases.map((u) => [u.id, u]));
|
|
29099
|
+
result.entries = system.entries.map((e) => {
|
|
29100
|
+
const u = e.useCaseId !== void 0 ? byId.get(e.useCaseId) : void 0;
|
|
29101
|
+
return { entry: e.id, p50Ms: u?.percentiles.p50 ?? 0, p99Ms: u?.percentiles.p99 ?? 0, availability: u?.availability ?? 1 };
|
|
29102
|
+
});
|
|
29103
|
+
if (totalInboundRps === 0) return result;
|
|
29104
|
+
result.breaches = breachesOf(team, sentToTeam, analysis);
|
|
29105
|
+
const first = contract.entries.find((c) => inbound.some((e) => e.entry === c.entry && e.rps > 0));
|
|
29106
|
+
if (result.saturated && busiest) {
|
|
29107
|
+
const onOwn = first && first.saturatesAtRps === 0;
|
|
29108
|
+
const limit = first && Number.isFinite(first.saturatesAtRps) ? `; it takes at most ${formatRps(first.saturatesAtRps)} before its ${first.bottleneck} saturates` : "";
|
|
29109
|
+
result.warnings.push(
|
|
29110
|
+
onOwn ? `${label} is saturated on its own traffic, before what it is sent: its ${busiest.id} is at ${usageText(busiest)}.` : `${label} cannot take what it is sent: you send ${sending}${limit}. Its ${busiest.id} is at ${usageText(busiest)}.`
|
|
29111
|
+
);
|
|
29112
|
+
} else if (result.hot && busiest) {
|
|
29113
|
+
const limit = first && Number.isFinite(first.hotAtRps) ? `; it runs hot past ${formatRps(first.hotAtRps)}` : "";
|
|
29114
|
+
result.warnings.push(`${label} runs hot under what it is sent: you send ${sending}${limit}. Its ${busiest.id} is at ${usageText(busiest)}.`);
|
|
29115
|
+
}
|
|
29116
|
+
for (const b of result.breaches) result.warnings.push(`${label} breaks its SLA under what it is sent: ${b.message}.`);
|
|
29117
|
+
return result;
|
|
29118
|
+
}
|
|
29119
|
+
function withExtraTraffic(team, extra, loc) {
|
|
29120
|
+
const traffic = [...team.traffic ?? []];
|
|
29121
|
+
for (const [id, rps] of extra) {
|
|
29122
|
+
const u = team.useCases.find((x) => x.id === id);
|
|
29123
|
+
if (!u || rps <= 0) continue;
|
|
29124
|
+
const i = traffic.findIndex((t) => findUseCase(team, t.useCase) === u);
|
|
29125
|
+
if (i >= 0) traffic[i] = { ...traffic[i], rps: traffic[i].rps + rps };
|
|
29126
|
+
else traffic.push({ useCase: u.name, rps, loc });
|
|
29127
|
+
}
|
|
29128
|
+
return { ...team, traffic };
|
|
29129
|
+
}
|
|
29130
|
+
function breachesOf(team, sent, analysis) {
|
|
29131
|
+
const entered = /* @__PURE__ */ new Set();
|
|
29132
|
+
for (const [id, rps] of sent) {
|
|
29133
|
+
const u = team.useCases.find((x) => x.id === id);
|
|
29134
|
+
if (u && rps > 0) entered.add(u);
|
|
29135
|
+
}
|
|
29136
|
+
const nodes = new Map(analysis.nodes.map((n) => [n.id, n]));
|
|
29137
|
+
const results = new Map(analysis.useCases.map((u) => [u.id, u]));
|
|
29138
|
+
const breaches = [];
|
|
29139
|
+
for (const r of team.requirements ?? []) {
|
|
29140
|
+
if (r.kind !== "latency" && r.kind !== "availability") continue;
|
|
29141
|
+
const targets = r.useCase === void 0 ? [...entered] : [findUseCase(team, r.useCase)].filter((u) => !!u && entered.has(u));
|
|
29142
|
+
for (const u of targets) {
|
|
29143
|
+
const ua = results.get(u.id);
|
|
29144
|
+
const under = `under ${formatRps(ua.rps)}`;
|
|
29145
|
+
if (r.kind === "availability") {
|
|
29146
|
+
const name2 = `availability of "${u.name}" \u2265 ${r.minPercent}%`;
|
|
29147
|
+
if (ua.availability * 100 < r.minPercent - 1e-9) breaches.push({ requirement: name2, message: `availability of "${u.name}" is ${formatAvailability(ua.availability)} ${under} (limit ${r.minPercent}%)` });
|
|
29148
|
+
continue;
|
|
29149
|
+
}
|
|
29150
|
+
const key = PERCENTILE_KEYS[r.percentile];
|
|
29151
|
+
const scenario = r.scenario !== void 0 ? findScenario2(u, r.scenario) : void 0;
|
|
29152
|
+
if (r.scenario !== void 0 && !scenario) continue;
|
|
29153
|
+
const what = `${key} of "${u.name}"${scenario ? ` scenario "${scenario.name}"` : ""}`;
|
|
29154
|
+
const name = `${what} < ${formatMs(r.maxMs)}`;
|
|
29155
|
+
const steps = scenario ? scenario.steps : u.scenarios.flatMap((s) => s.steps);
|
|
29156
|
+
const saturated = steps.map((s) => nodes.get(s.toServiceId)).reduce((best, n) => n?.saturated && (!best || n.utilization > best.utilization) ? n : best, void 0);
|
|
29157
|
+
if (saturated) {
|
|
29158
|
+
breaches.push({ requirement: name, message: `${what}: ${saturated.id} is saturated (${usageText(saturated)}) ${under} (limit ${formatMs(r.maxMs)})` });
|
|
29159
|
+
continue;
|
|
29160
|
+
}
|
|
29161
|
+
const ms = scenario ? ua.scenarios[u.scenarios.indexOf(scenario)].percentiles[key] : ua.percentiles[key];
|
|
29162
|
+
if (ms >= r.maxMs) breaches.push({ requirement: name, message: `${what} is ${formatMs(ms)} ${under} (limit ${formatMs(r.maxMs)})` });
|
|
29163
|
+
}
|
|
29164
|
+
}
|
|
29165
|
+
return breaches;
|
|
29166
|
+
}
|
|
29167
|
+
|
|
28492
29168
|
// ../frontend/src/sim/analyze.ts
|
|
28493
29169
|
var DEFAULT_TIMEOUT_MS = 1e3;
|
|
28494
29170
|
var HOT = 0.7;
|
|
@@ -28646,6 +29322,7 @@ function usageText(n) {
|
|
|
28646
29322
|
return `bandwidth ${mb(n.bandwidthLoadMBps)} of ${mb(n.bandwidthCapacityMBps)}, ${formatPercent(n.bandwidthUtilization)}`;
|
|
28647
29323
|
}
|
|
28648
29324
|
if (n.writeScaling === "shards" && n.writeLoadRps > 0) {
|
|
29325
|
+
if (n.readLoadRps <= 0) return `writes ${part(n.writeLoadRps, n.writeCapacityRps, n.writeUtilization)}`;
|
|
28649
29326
|
return `reads ${part(n.readLoadRps, n.readCapacityRps, n.readUtilization)}; writes ${part(n.writeLoadRps, n.writeCapacityRps, n.writeUtilization)}`;
|
|
28650
29327
|
}
|
|
28651
29328
|
return part(n.loadRps, n.capacityRps, n.requestUtilization);
|
|
@@ -28731,6 +29408,7 @@ function analyze(diagram, options = {}) {
|
|
|
28731
29408
|
const load = computeLoad(diagram, traffic);
|
|
28732
29409
|
const egress = computeEgressGb(diagram, traffic, kindOf2);
|
|
28733
29410
|
const bytes = computeBytesPerSecond(diagram, traffic);
|
|
29411
|
+
const systems = diagram.systems?.length ? analyzeSystems(diagram, traffic, options) : void 0;
|
|
28734
29412
|
const hops = /* @__PURE__ */ new Map();
|
|
28735
29413
|
const nodeResults = nodes.map((n) => {
|
|
28736
29414
|
const p = profiles.get(n.id);
|
|
@@ -28769,12 +29447,13 @@ function analyze(diagram, options = {}) {
|
|
|
28769
29447
|
if (n.saturated) warnings.push(`'${n.id}' is saturated: ${usage}. ${writeBound(n) ? "Add shards for writes" : "Add replicas"} or take load off it.`);
|
|
28770
29448
|
else if (n.utilization > HOT) warnings.push(`'${n.id}' runs hot: ${usage}.`);
|
|
28771
29449
|
}
|
|
29450
|
+
for (const s of systems?.systems ?? []) warnings.push(...s.warnings);
|
|
28772
29451
|
const access = accessIn(diagram);
|
|
28773
29452
|
const pathOf2 = (hopsOfPath) => ({
|
|
28774
29453
|
parts: hopsOfPath.map(
|
|
28775
29454
|
(part) => part.map((h) => {
|
|
28776
29455
|
if (h.failed) return { fixedMs: timeoutOf(h.target), tailMs: 0 };
|
|
28777
|
-
const hop = hops.get(h.target) ?? { fixedMs: 0, tailMs: 0 };
|
|
29456
|
+
const hop = systems?.hop(h.target, h.step.toPort) ?? hops.get(h.target) ?? { fixedMs: 0, tailMs: 0 };
|
|
28778
29457
|
return { fixedMs: transferMs(h.step, bandwidthOf) + hop.fixedMs, tailMs: hop.tailMs };
|
|
28779
29458
|
})
|
|
28780
29459
|
)
|
|
@@ -28804,6 +29483,7 @@ function analyze(diagram, options = {}) {
|
|
|
28804
29483
|
const writes = mainWrites(u, shares, (step) => access(step) === "write");
|
|
28805
29484
|
const availabilityOf = (id) => {
|
|
28806
29485
|
const n = nodeById.get(id);
|
|
29486
|
+
if (!n && systems) return systemAvailability(u, id, systems.availability);
|
|
28807
29487
|
return !n ? 1 : writes.has(id) ? n.writeAvailability : n.availability;
|
|
28808
29488
|
};
|
|
28809
29489
|
return { id: u.id, name: u.name, rps, scenarios, percentiles, tailScenario, availability: useCaseAvailability(u, shares, availabilityOf) };
|
|
@@ -28814,9 +29494,15 @@ function analyze(diagram, options = {}) {
|
|
|
28814
29494
|
totalCostUsd: nodeResults.reduce((sum, n) => sum + n.costUsd, 0),
|
|
28815
29495
|
totalEgressUsd: nodeResults.reduce((sum, n) => sum + n.egressUsd, 0),
|
|
28816
29496
|
singlePointsOfFailure: singlePointsOfFailure(diagram),
|
|
28817
|
-
warnings
|
|
29497
|
+
warnings,
|
|
29498
|
+
...systems ? { systems: systems.systems } : {}
|
|
28818
29499
|
};
|
|
28819
29500
|
}
|
|
29501
|
+
function systemAvailability(u, id, availability2) {
|
|
29502
|
+
let result = 1;
|
|
29503
|
+
for (const s of u.scenarios) for (const step of s.steps) if (step.toServiceId === id) result = Math.min(result, availability2(id, step.toPort) ?? 1);
|
|
29504
|
+
return result;
|
|
29505
|
+
}
|
|
28820
29506
|
function mainWrites(useCase, shares, isWrite) {
|
|
28821
29507
|
if (useCase.scenarios.length === 0) return /* @__PURE__ */ new Set();
|
|
28822
29508
|
const path = criticalPath(useCase, useCase.scenarios[mainScenarioIndex(shares)]).flat();
|
|
@@ -28866,6 +29552,7 @@ function runTests(diagram, analysis = analyze(diagram), options = {}) {
|
|
|
28866
29552
|
const { passed, message, hint } = requirement(run, r);
|
|
28867
29553
|
return { id: `req:${i + 1}`, name: requirementName(r), category: CATEGORY[r.kind], passed, message, hint, loc: r.loc };
|
|
28868
29554
|
});
|
|
29555
|
+
results.push(...systemResults(diagram, analysis));
|
|
28869
29556
|
const names = /* @__PURE__ */ new Map();
|
|
28870
29557
|
for (const test of diagram.tests ?? []) {
|
|
28871
29558
|
const n = (names.get(test.name) ?? 0) + 1;
|
|
@@ -28874,6 +29561,26 @@ function runTests(diagram, analysis = analyze(diagram), options = {}) {
|
|
|
28874
29561
|
}
|
|
28875
29562
|
return results.map(({ hint, ...r }) => r.passed || !hint ? r : { ...r, hint });
|
|
28876
29563
|
}
|
|
29564
|
+
function systemResults(diagram, analysis) {
|
|
29565
|
+
return (analysis.systems ?? []).filter((s) => s.totalInboundRps > 0 && s.contract.source !== "none").map((s) => {
|
|
29566
|
+
const system = diagram.systems?.find((x) => x.id === s.id);
|
|
29567
|
+
const node = diagram.nodes.find((n) => n.id === s.id);
|
|
29568
|
+
const team = node?.ownerTeam ? `the ${node.ownerTeam} team` : `the owners of ${s.id}`;
|
|
29569
|
+
const problem = s.warnings.find((w) => !w.includes(" runs hot "));
|
|
29570
|
+
const sat = s.contract.entries.find((e) => s.inbound.some((i) => i.entry === e.entry && i.rps > 0));
|
|
29571
|
+
const room = sat && Number.isFinite(sat.saturatesAtRps) ? `; it takes at most ${formatRps(sat.saturatesAtRps)}` : "";
|
|
29572
|
+
const bottleneck = s.saturated && s.busiest ? ` (their bottleneck is ${s.busiest})` : "";
|
|
29573
|
+
return {
|
|
29574
|
+
id: `system:${s.id}`,
|
|
29575
|
+
name: `${s.id} takes what it is sent`,
|
|
29576
|
+
category: s.breaches.some((b) => b.requirement.startsWith("availability")) && !s.saturated ? "availability" : "latency",
|
|
29577
|
+
passed: !problem,
|
|
29578
|
+
message: problem ?? `'${s.id}' takes ${formatRps(s.totalInboundRps)}${(s.sharedInboundRps ?? 0) > s.totalInboundRps * (1 + 1e-9) ? ` (of the ${formatRps(s.sharedInboundRps)} it gets)` : ""}${s.contract.source === "stub" ? "" : " on top of its own traffic"} and holds its requirements${room}`,
|
|
29579
|
+
...problem ? { hint: `Send it less, or ask ${team} to add capacity for it${bottleneck}` } : {},
|
|
29580
|
+
...system ? { loc: system.loc } : {}
|
|
29581
|
+
};
|
|
29582
|
+
});
|
|
29583
|
+
}
|
|
28877
29584
|
var CATEGORY = {
|
|
28878
29585
|
latency: "latency",
|
|
28879
29586
|
availability: "availability",
|
|
@@ -29412,7 +30119,7 @@ var KEYWORDS2 = [
|
|
|
29412
30119
|
{ label: "par", detail: "Steps that run in parallel", snippet: "par {\n $0\n}" },
|
|
29413
30120
|
{ label: "alt", detail: "One scenario of a use case", snippet: 'alt "${1:Scenario}" {\n $0\n}' },
|
|
29414
30121
|
{ label: "legend", detail: 'Styles that .tag applies: owned "Owned by us" green', snippet: 'legend {\n ${1:tag} "${2:What it means}" ${3:blue}$0\n}' },
|
|
29415
|
-
// Layout hints (docs/
|
|
30122
|
+
// Layout hints (docs/STYLING.md, "Steering the layout").
|
|
29416
30123
|
{ label: "direction", detail: "Where edges flow: down, right, up or left (top level or in a group)", snippet: "direction ${1|down,right,up,left|}" },
|
|
29417
30124
|
{ label: "layout ordered", detail: "Lay edges out by declaration order (top level)", snippet: "layout ordered" },
|
|
29418
30125
|
// High-level design sections (top level).
|
|
@@ -29443,14 +30150,348 @@ var ASSERTIONS = [
|
|
|
29443
30150
|
];
|
|
29444
30151
|
|
|
29445
30152
|
// src/imports.ts
|
|
30153
|
+
var import_node_fs4 = require("node:fs");
|
|
30154
|
+
var import_node_path5 = require("node:path");
|
|
30155
|
+
|
|
30156
|
+
// ../frontend/src/dsl/imports.ts
|
|
30157
|
+
var ImportError = class extends Error {
|
|
30158
|
+
name = "ImportError";
|
|
30159
|
+
};
|
|
30160
|
+
var LATEST_TAG = "latest";
|
|
30161
|
+
var VERSION_TAG = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
30162
|
+
var CLOUD_NAME_SEGMENT = /^[A-Za-z0-9_][A-Za-z0-9._+-]*$/;
|
|
30163
|
+
var MAX_CLOUD_NAME = 200;
|
|
30164
|
+
var MAX_SPEC_LENGTH = 1024;
|
|
30165
|
+
var SEGMENT = /^[A-Za-z0-9_][A-Za-z0-9._ +-]*$/;
|
|
30166
|
+
var HOST = /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)*(?::\d{1,5})?$/;
|
|
30167
|
+
var REPO_SEGMENT = /^[A-Za-z0-9_][A-Za-z0-9._-]*$/;
|
|
30168
|
+
var REF = /^[A-Za-z0-9_][A-Za-z0-9._/-]*$/;
|
|
30169
|
+
var SCHEME = /^([A-Za-z][A-Za-z0-9+.-]+):/;
|
|
30170
|
+
function isRemoteSpec(spec) {
|
|
30171
|
+
return SCHEME.test(spec);
|
|
30172
|
+
}
|
|
30173
|
+
function isAbsolutePath(path) {
|
|
30174
|
+
return /^[\\/]/.test(path) || /^[A-Za-z]:/.test(path);
|
|
30175
|
+
}
|
|
30176
|
+
function checkRef(ref, spec) {
|
|
30177
|
+
if (!REF.test(ref) || ref.length > 200 || ref.includes("..") || ref.includes("//") || ref.endsWith("/") || ref.endsWith(".") || ref.endsWith(".lock") || ref.split("/").some((p) => p.startsWith("."))) {
|
|
30178
|
+
throw new ImportError(`'${spec}' has an invalid ref '${ref}'; use a tag, branch or commit made of letters, digits, '.', '_', '-' and '/'`);
|
|
30179
|
+
}
|
|
30180
|
+
}
|
|
30181
|
+
function repoPathSegments(path, spec) {
|
|
30182
|
+
const parts = path.split("/");
|
|
30183
|
+
if (!path || parts.some((p) => !SEGMENT.test(p) || p === "." || p === "..")) {
|
|
30184
|
+
throw new ImportError(`'${spec}' has an invalid path in the repository; use a relative path like dir/file.proschi, without '..'`);
|
|
30185
|
+
}
|
|
30186
|
+
return parts;
|
|
30187
|
+
}
|
|
30188
|
+
function parseRemoteSpec(spec) {
|
|
30189
|
+
const scheme = SCHEME.exec(spec)?.[1].toLowerCase();
|
|
30190
|
+
if (!scheme) return void 0;
|
|
30191
|
+
if (spec.length > MAX_SPEC_LENGTH) throw new ImportError(`Import source is longer than ${MAX_SPEC_LENGTH} characters`);
|
|
30192
|
+
if (/[\s\0\\]/.test(spec)) throw new ImportError(`'${spec}' contains whitespace or a backslash`);
|
|
30193
|
+
if (scheme === "git") {
|
|
30194
|
+
const body = spec.slice(4);
|
|
30195
|
+
const at = body.lastIndexOf("@");
|
|
30196
|
+
const split2 = body.indexOf("//");
|
|
30197
|
+
if (at === -1 || split2 === -1 || split2 > at) {
|
|
30198
|
+
throw new ImportError(`'${spec}' is not a git source; write git:<host>/<org>/<repo>//<path/in/repo>@<ref>`);
|
|
30199
|
+
}
|
|
30200
|
+
const repo = body.slice(0, split2);
|
|
30201
|
+
const path = body.slice(split2 + 2, at);
|
|
30202
|
+
const ref = body.slice(at + 1);
|
|
30203
|
+
const [host, ...names] = repo.split("/");
|
|
30204
|
+
if (!HOST.test(host) || names.length < 2 || names.some((n) => !REPO_SEGMENT.test(n) || n.endsWith("."))) {
|
|
30205
|
+
throw new ImportError(`'${spec}' names an invalid repository '${repo}'; write <host>/<org>/<repo>`);
|
|
30206
|
+
}
|
|
30207
|
+
repoPathSegments(path, spec);
|
|
30208
|
+
checkRef(ref, spec);
|
|
30209
|
+
return { kind: "git", repo, path, ref };
|
|
30210
|
+
}
|
|
30211
|
+
if (scheme === "cloud") return parseCloudSpec(spec);
|
|
30212
|
+
if (scheme === "http") throw new ImportError(`'${spec}' uses http://; only https:// imports are allowed`);
|
|
30213
|
+
if (scheme !== "https") throw new ImportError(`'${spec}' is not a supported import source; use a relative path, git:, https:// or cloud:`);
|
|
30214
|
+
let url;
|
|
30215
|
+
try {
|
|
30216
|
+
url = new URL(spec);
|
|
30217
|
+
} catch {
|
|
30218
|
+
throw new ImportError(`'${spec}' is not a valid URL`);
|
|
30219
|
+
}
|
|
30220
|
+
if (url.username || url.password) throw new ImportError(`'${spec}' contains credentials; git and https imports use your own credentials, never ones in the diagram`);
|
|
30221
|
+
if (url.hash) throw new ImportError(`'${spec}' has a #fragment; remove it`);
|
|
30222
|
+
if (!HOST.test(url.host)) throw new ImportError(`'${spec}' has an invalid host`);
|
|
30223
|
+
return { kind: "https", url: url.href };
|
|
30224
|
+
}
|
|
30225
|
+
function parseCloudSpec(spec) {
|
|
30226
|
+
const body = spec.slice("cloud:".length);
|
|
30227
|
+
const split2 = body.indexOf("//");
|
|
30228
|
+
const head = split2 === -1 ? body : body.slice(0, split2);
|
|
30229
|
+
const parts = head.split(":");
|
|
30230
|
+
if (parts.length > 2) throw new ImportError(`'${spec}' is not a cloud source; write cloud:<name> or cloud:<name>:<tag>`);
|
|
30231
|
+
const name = parts[0].endsWith(".proschi") ? parts[0].slice(0, -".proschi".length) : parts[0];
|
|
30232
|
+
if (!name || name.length > MAX_CLOUD_NAME || name.split("/").some((p) => !CLOUD_NAME_SEGMENT.test(p) || p === "." || p === "..")) {
|
|
30233
|
+
throw new ImportError(
|
|
30234
|
+
`'${spec}' names an invalid diagram '${parts[0]}'; use the diagram's file name (letters, digits, '.', '_', '+', '-' and '/', no spaces), with or without .proschi`
|
|
30235
|
+
);
|
|
30236
|
+
}
|
|
30237
|
+
const tag = parts.length === 2 ? parts[1] : LATEST_TAG;
|
|
30238
|
+
if (!VERSION_TAG.test(tag)) {
|
|
30239
|
+
throw new ImportError(`'${spec}' has an invalid version tag '${tag}'; use up to 64 letters, digits, '.', '_' and '-', starting with a letter or digit, e.g. 2026-11-01`);
|
|
30240
|
+
}
|
|
30241
|
+
if (split2 === -1) return { kind: "cloud", name, tag };
|
|
30242
|
+
return { kind: "cloud", name, tag, path: repoPathSegments(body.slice(split2 + 2), spec).join("/") };
|
|
30243
|
+
}
|
|
30244
|
+
function formatRemoteSpec(spec) {
|
|
30245
|
+
if (spec.kind === "cloud") return `cloud:${spec.name}:${spec.tag}${spec.path ? `//${spec.path}` : ""}`;
|
|
30246
|
+
return spec.kind === "git" ? `git:${spec.repo}//${spec.path}@${spec.ref}` : spec.url;
|
|
30247
|
+
}
|
|
30248
|
+
function resolveImportPath(fromPath, importPath) {
|
|
30249
|
+
if (importPath.includes("\0")) throw new ImportError(`'${importPath.replace(/\0/g, "")}' contains a NUL character`);
|
|
30250
|
+
const remote = parseRemoteSpec(importPath);
|
|
30251
|
+
if (remote?.kind === "cloud" && fromPath && isRemoteSpec(fromPath) && parseRemoteSpec(fromPath)?.kind !== "cloud") {
|
|
30252
|
+
throw new ImportError(`'${importPath}' is a cloud import inside ${fromPath}; cloud: sources only work from your own diagrams`);
|
|
30253
|
+
}
|
|
30254
|
+
if (remote) return formatRemoteSpec(remote);
|
|
30255
|
+
if (isAbsolutePath(importPath)) throw new ImportError(`'${importPath}' is an absolute path; imports must be relative to the importing file`);
|
|
30256
|
+
const from = fromPath && isRemoteSpec(fromPath) ? parseRemoteSpec(fromPath) : void 0;
|
|
30257
|
+
if (from?.kind === "cloud") {
|
|
30258
|
+
const parts2 = (from.path ?? "").split("/").slice(0, -1);
|
|
30259
|
+
for (const part of importPath.split("/")) {
|
|
30260
|
+
if (part === "" || part === ".") continue;
|
|
30261
|
+
if (part !== "..") parts2.push(part);
|
|
30262
|
+
else if (parts2.length > 0) parts2.pop();
|
|
30263
|
+
else throw new ImportError(`'${importPath}' leaves the files saved with cloud:${from.name}:${from.tag}`);
|
|
30264
|
+
}
|
|
30265
|
+
return formatRemoteSpec({ ...from, path: repoPathSegments(parts2.join("/"), importPath).join("/") });
|
|
30266
|
+
}
|
|
30267
|
+
if (from?.kind === "git") {
|
|
30268
|
+
const parts2 = from.path.split("/").slice(0, -1);
|
|
30269
|
+
for (const part of importPath.split("/")) {
|
|
30270
|
+
if (part === "" || part === ".") continue;
|
|
30271
|
+
if (part !== "..") parts2.push(part);
|
|
30272
|
+
else if (parts2.length > 0) parts2.pop();
|
|
30273
|
+
else throw new ImportError(`'${importPath}' leaves the repository ${from.repo}`);
|
|
30274
|
+
}
|
|
30275
|
+
repoPathSegments(parts2.join("/"), importPath);
|
|
30276
|
+
return formatRemoteSpec({ ...from, path: parts2.join("/") });
|
|
30277
|
+
}
|
|
30278
|
+
if (from?.kind === "https") {
|
|
30279
|
+
const base = new URL(from.url);
|
|
30280
|
+
const url = new URL(importPath, base);
|
|
30281
|
+
if (url.origin !== base.origin) throw new ImportError(`'${importPath}' leaves ${base.origin}`);
|
|
30282
|
+
return formatRemoteSpec(parseRemoteSpec(url.href));
|
|
30283
|
+
}
|
|
30284
|
+
const parts = !fromPath ? [] : fromPath.split("/").slice(0, -1);
|
|
30285
|
+
for (const part of importPath.split("/")) {
|
|
30286
|
+
if (part === "" || part === ".") continue;
|
|
30287
|
+
if (part === ".." && parts.length > 0 && parts[parts.length - 1] !== "..") parts.pop();
|
|
30288
|
+
else parts.push(part);
|
|
30289
|
+
}
|
|
30290
|
+
return parts.join("/");
|
|
30291
|
+
}
|
|
30292
|
+
|
|
30293
|
+
// src/lock.ts
|
|
30294
|
+
var import_node_crypto = require("node:crypto");
|
|
29446
30295
|
var import_node_fs3 = require("node:fs");
|
|
29447
30296
|
var import_node_path4 = require("node:path");
|
|
29448
|
-
|
|
29449
|
-
|
|
29450
|
-
|
|
29451
|
-
|
|
29452
|
-
|
|
30297
|
+
|
|
30298
|
+
// src/lockFormat.ts
|
|
30299
|
+
var LOCK_FILE = "proschi.lock";
|
|
30300
|
+
var CACHE_DIR = ".proschi/cache";
|
|
30301
|
+
var HASH = /^[0-9a-f]{64}$/;
|
|
30302
|
+
var COMMIT = /^[0-9a-f]{40}([0-9a-f]{24})?$/;
|
|
30303
|
+
var emptyLock = () => ({ version: 1, git: {}, https: {}, cloud: {} });
|
|
30304
|
+
var gitKey = (repo, ref) => `${repo}@${ref}`;
|
|
30305
|
+
var cloudKey = (name, tag) => `${name}:${tag}`;
|
|
30306
|
+
var remoteError = {
|
|
30307
|
+
noLock: (spec) => new ImportError(`'${spec}' is a remote import and there is no ${LOCK_FILE}; run proschi imports sync`),
|
|
30308
|
+
notLocked: (spec) => spec.startsWith("cloud:") ? remoteError.cloud(spec) : new ImportError(`'${spec}' is not in ${LOCK_FILE}; run proschi imports sync`),
|
|
30309
|
+
/** A cloud version that is not pinned yet (docs/design/versions-and-cloud.md). */
|
|
30310
|
+
cloud: (spec) => new ImportError(`'${spec}' is a version in your Proschi account; run proschi imports sync to pin it (cloud sources need proschi login)`),
|
|
30311
|
+
notCached: (spec) => new ImportError(`'${spec}' is not in the import cache (${CACHE_DIR}); run proschi imports sync`),
|
|
30312
|
+
altered: (spec) => new ImportError(`The cached copy of '${spec}' does not match its hash in ${LOCK_FILE}; run proschi imports sync`)
|
|
30313
|
+
};
|
|
30314
|
+
var isRecord = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
30315
|
+
function parseLock(text) {
|
|
30316
|
+
const invalid = (why) => new ImportError(`Invalid ${LOCK_FILE}: ${why}; delete it and run proschi imports sync`);
|
|
30317
|
+
let json;
|
|
30318
|
+
try {
|
|
30319
|
+
json = JSON.parse(text);
|
|
30320
|
+
} catch (e) {
|
|
30321
|
+
throw invalid(e.message);
|
|
30322
|
+
}
|
|
30323
|
+
if (!isRecord(json) || json.version !== 1) throw invalid('expected {"version": 1, \u2026}');
|
|
30324
|
+
const lock = emptyLock();
|
|
30325
|
+
const git = json.git ?? {};
|
|
30326
|
+
const https = json.https ?? {};
|
|
30327
|
+
const cloud = json.cloud ?? {};
|
|
30328
|
+
if (!isRecord(git) || !isRecord(https) || !isRecord(cloud)) throw invalid('"git", "https" and "cloud" must be objects');
|
|
30329
|
+
for (const [key, entry] of Object.entries(git)) {
|
|
30330
|
+
const at = key.lastIndexOf("@");
|
|
30331
|
+
const valid = at > 0 && isRecord(entry) && typeof entry.commit === "string" && COMMIT.test(entry.commit) && isRecord(entry.files) && Object.entries(entry.files).every(([path, hash]) => typeof hash === "string" && HASH.test(hash) && isGitSpec(`git:${key.slice(0, at)}//${path}@${key.slice(at + 1)}`));
|
|
30332
|
+
if (!valid) throw invalid(`bad git entry '${key}'`);
|
|
30333
|
+
lock.git[key] = { commit: entry.commit, files: { ...entry.files } };
|
|
30334
|
+
}
|
|
30335
|
+
for (const [url, hash] of Object.entries(https)) {
|
|
30336
|
+
if (typeof hash !== "string" || !HASH.test(hash) || parseRemoteSpec(url)?.kind !== "https") throw invalid(`bad https entry '${url}'`);
|
|
30337
|
+
lock.https[url] = hash;
|
|
30338
|
+
}
|
|
30339
|
+
for (const [key, entry] of Object.entries(cloud)) {
|
|
30340
|
+
const hash = (v) => typeof v === "string" && HASH.test(v);
|
|
30341
|
+
const valid = isCanonicalSpec(`cloud:${key}`, "cloud") && isRecord(entry) && typeof entry.tag === "string" && VERSION_TAG.test(entry.tag) && entry.tag !== LATEST_TAG && Number.isSafeInteger(entry.seq) && entry.seq > 0 && hash(entry.sha256) && (entry.source === void 0 || hash(entry.source)) && isRecord(entry.files) && Object.entries(entry.files).every(([path, h]) => hash(h) && isCanonicalSpec(`cloud:${key}//${path}`, "cloud"));
|
|
30342
|
+
if (!valid) throw invalid(`bad cloud entry '${key}'`);
|
|
30343
|
+
lock.cloud[key] = {
|
|
30344
|
+
tag: entry.tag,
|
|
30345
|
+
seq: entry.seq,
|
|
30346
|
+
sha256: entry.sha256,
|
|
30347
|
+
...entry.source === void 0 ? {} : { source: entry.source },
|
|
30348
|
+
files: { ...entry.files }
|
|
30349
|
+
};
|
|
30350
|
+
}
|
|
30351
|
+
return lock;
|
|
30352
|
+
}
|
|
30353
|
+
function isCanonicalSpec(spec, kind) {
|
|
30354
|
+
try {
|
|
30355
|
+
const remote = parseRemoteSpec(spec);
|
|
30356
|
+
return remote?.kind === kind && formatRemoteSpec(remote) === spec;
|
|
30357
|
+
} catch {
|
|
30358
|
+
return false;
|
|
30359
|
+
}
|
|
30360
|
+
}
|
|
30361
|
+
function isGitSpec(spec) {
|
|
30362
|
+
try {
|
|
30363
|
+
return parseRemoteSpec(spec)?.kind === "git";
|
|
30364
|
+
} catch {
|
|
30365
|
+
return false;
|
|
30366
|
+
}
|
|
30367
|
+
}
|
|
30368
|
+
function lockedHash(lock, spec) {
|
|
30369
|
+
const remote = parseRemoteSpec(spec);
|
|
30370
|
+
if (remote?.kind === "git") return lock.git[gitKey(remote.repo, remote.ref)]?.files[remote.path];
|
|
30371
|
+
if (remote?.kind === "https") return lock.https[remote.url];
|
|
30372
|
+
if (remote?.kind === "cloud") {
|
|
30373
|
+
const entry = lock.cloud[cloudKey(remote.name, remote.tag)];
|
|
30374
|
+
return remote.path === void 0 ? entry?.source : entry?.files[remote.path];
|
|
30375
|
+
}
|
|
30376
|
+
return void 0;
|
|
30377
|
+
}
|
|
30378
|
+
|
|
30379
|
+
// src/lock.ts
|
|
30380
|
+
var sha256 = (bytes) => (0, import_node_crypto.createHash)("sha256").update(bytes).digest("hex");
|
|
30381
|
+
function readLock(root) {
|
|
30382
|
+
const file = (0, import_node_path4.join)(root, LOCK_FILE);
|
|
30383
|
+
return (0, import_node_fs3.existsSync)(file) ? parseLock((0, import_node_fs3.readFileSync)(file, "utf8")) : void 0;
|
|
30384
|
+
}
|
|
30385
|
+
var cacheFile = (root, hash) => (0, import_node_path4.join)(root, CACHE_DIR, hash);
|
|
30386
|
+
function readCached(root, hash, spec) {
|
|
30387
|
+
if (!HASH.test(hash)) return void 0;
|
|
30388
|
+
const file = cacheFile(root, hash);
|
|
30389
|
+
if (!(0, import_node_fs3.existsSync)(file)) return void 0;
|
|
30390
|
+
const bytes = (0, import_node_fs3.readFileSync)(file);
|
|
30391
|
+
if (sha256(bytes) !== hash) throw remoteError.altered(spec);
|
|
30392
|
+
return bytes;
|
|
30393
|
+
}
|
|
30394
|
+
|
|
30395
|
+
// src/imports.ts
|
|
30396
|
+
function displayPath2(file, cwd = process.cwd()) {
|
|
30397
|
+
const abs = (0, import_node_path5.resolve)(cwd, file);
|
|
30398
|
+
const rel = (0, import_node_path5.relative)(cwd, abs);
|
|
30399
|
+
return rel && !rel.startsWith("..") && !(0, import_node_path5.isAbsolute)(rel) ? rel : abs;
|
|
30400
|
+
}
|
|
30401
|
+
var MAX_IMPORT_BYTES = 1e6;
|
|
30402
|
+
var MAX_IMPORT_FILES = 500;
|
|
30403
|
+
var MAX_IMPORT_DEPTH = 32;
|
|
30404
|
+
function projectRoot(entry) {
|
|
30405
|
+
const dir = (0, import_node_path5.dirname)((0, import_node_path5.resolve)(entry));
|
|
30406
|
+
const config = findConfigFile(dir);
|
|
30407
|
+
if (config) return (0, import_node_fs4.realpathSync)((0, import_node_path5.dirname)(config));
|
|
30408
|
+
for (let current = dir; ; current = (0, import_node_path5.dirname)(current)) {
|
|
30409
|
+
if ((0, import_node_fs4.existsSync)((0, import_node_path5.join)(current, ".git"))) return (0, import_node_fs4.realpathSync)(current);
|
|
30410
|
+
if ((0, import_node_path5.dirname)(current) === current) break;
|
|
30411
|
+
}
|
|
30412
|
+
return (0, import_node_fs4.realpathSync)((0, import_node_fs4.existsSync)(dir) ? dir : process.cwd());
|
|
30413
|
+
}
|
|
30414
|
+
var isInside = (root, path) => {
|
|
30415
|
+
const rel = (0, import_node_path5.relative)(root, path);
|
|
30416
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${import_node_path5.sep}`) && !(0, import_node_path5.isAbsolute)(rel);
|
|
30417
|
+
};
|
|
30418
|
+
function realLocation(path) {
|
|
30419
|
+
let existing = path;
|
|
30420
|
+
while (!(0, import_node_fs4.existsSync)(existing) && (0, import_node_path5.dirname)(existing) !== existing) existing = (0, import_node_path5.dirname)(existing);
|
|
30421
|
+
return (0, import_node_path5.join)((0, import_node_fs4.realpathSync)(existing), (0, import_node_path5.relative)(existing, path));
|
|
30422
|
+
}
|
|
30423
|
+
var tooLarge = (what) => new ImportError(`'${what}' is larger than ${MAX_IMPORT_BYTES / 1e6} MB`);
|
|
30424
|
+
function fileResolver({ entry, read = () => void 0, toPath = (p) => p, remote } = {}) {
|
|
30425
|
+
const root = projectRoot(entry ?? (0, import_node_path5.join)(process.cwd(), "stdin.proschi"));
|
|
30426
|
+
const errors = /* @__PURE__ */ new Map();
|
|
30427
|
+
const levels = /* @__PURE__ */ new Map();
|
|
30428
|
+
let lock = null;
|
|
30429
|
+
const fromCache = (spec) => {
|
|
30430
|
+
if (lock === null) lock = readLock(root);
|
|
30431
|
+
if (!lock) throw remoteError.noLock(spec);
|
|
30432
|
+
const hash = lockedHash(lock, spec);
|
|
30433
|
+
if (!hash) throw remoteError.notLocked(spec);
|
|
30434
|
+
const bytes = readCached(root, hash, spec);
|
|
30435
|
+
if (!bytes) throw remoteError.notCached(spec);
|
|
30436
|
+
return bytes.toString("utf8");
|
|
30437
|
+
};
|
|
30438
|
+
const loadRemote = remote ?? fromCache;
|
|
30439
|
+
const found = (path, source2, level) => {
|
|
30440
|
+
if (!levels.has(path)) {
|
|
30441
|
+
if (levels.size >= MAX_IMPORT_FILES) throw new ImportError(`More than ${MAX_IMPORT_FILES} imported files; split the diagram`);
|
|
30442
|
+
levels.set(path, level);
|
|
30443
|
+
}
|
|
30444
|
+
return { path, source: source2 };
|
|
30445
|
+
};
|
|
30446
|
+
const resolveOne = (importPath, fromPath) => {
|
|
30447
|
+
const level = (fromPath === void 0 ? 0 : levels.get(fromPath) ?? 0) + 1;
|
|
30448
|
+
if (level > MAX_IMPORT_DEPTH) throw new ImportError(`Imports nest more than ${MAX_IMPORT_DEPTH} files deep`);
|
|
30449
|
+
if (isRemoteSpec(importPath) || fromPath !== void 0 && isRemoteSpec(fromPath)) {
|
|
30450
|
+
const spec = resolveImportPath(fromPath, importPath);
|
|
30451
|
+
const source2 = loadRemote(spec);
|
|
30452
|
+
if (source2 === void 0) return void 0;
|
|
30453
|
+
if (Buffer.byteLength(source2) > MAX_IMPORT_BYTES) throw tooLarge(spec);
|
|
30454
|
+
return found(spec, source2, level);
|
|
30455
|
+
}
|
|
30456
|
+
resolveImportPath(void 0, importPath);
|
|
30457
|
+
const abs = (0, import_node_path5.resolve)(fromPath ? (0, import_node_path5.dirname)((0, import_node_path5.resolve)(fromPath)) : root, importPath);
|
|
30458
|
+
if (!isInside(root, realLocation(abs))) {
|
|
30459
|
+
const link = (0, import_node_fs4.existsSync)(abs) && (0, import_node_fs4.lstatSync)(abs).isSymbolicLink();
|
|
30460
|
+
throw new ImportError(
|
|
30461
|
+
link ? `'${importPath}' is a symlink to a file outside the project root ${displayPath2(root)}` : `'${importPath}' is outside the project root ${displayPath2(root)}; imports must stay inside the folder of proschi.json (else the git root)`
|
|
30462
|
+
);
|
|
30463
|
+
}
|
|
30464
|
+
const text = read(abs);
|
|
30465
|
+
if (text !== void 0) {
|
|
30466
|
+
if (Buffer.byteLength(text) > MAX_IMPORT_BYTES) throw tooLarge(importPath);
|
|
30467
|
+
return found(toPath(abs), text, level);
|
|
30468
|
+
}
|
|
30469
|
+
if (!(0, import_node_fs4.existsSync)(abs)) return void 0;
|
|
30470
|
+
const stat = (0, import_node_fs4.statSync)(abs);
|
|
30471
|
+
if (!stat.isFile()) return void 0;
|
|
30472
|
+
if (stat.size > MAX_IMPORT_BYTES) throw tooLarge(importPath);
|
|
30473
|
+
return found(toPath(abs), (0, import_node_fs4.readFileSync)((0, import_node_fs4.realpathSync)(abs), "utf8"), level);
|
|
30474
|
+
};
|
|
30475
|
+
const resolver = ((importPath, fromPath) => {
|
|
30476
|
+
try {
|
|
30477
|
+
return resolveOne(importPath, fromPath);
|
|
30478
|
+
} catch (e) {
|
|
30479
|
+
if (e instanceof ImportError) errors.set(`${fromPath ?? ""}\0${importPath}`, e.message);
|
|
30480
|
+
throw e;
|
|
30481
|
+
}
|
|
30482
|
+
});
|
|
30483
|
+
resolver.root = root;
|
|
30484
|
+
resolver.errors = errors;
|
|
30485
|
+
resolver.explain = (result) => {
|
|
30486
|
+
for (const i of result.imports ?? []) {
|
|
30487
|
+
if (i.resolved !== void 0) continue;
|
|
30488
|
+
const reason = errors.get(`${i.loc.file ?? entry ?? ""}\0${i.path}`);
|
|
30489
|
+
const d = reason && result.diagnostics.find((d2) => d2.file === i.loc.file && d2.line === i.loc.line && d2.col === i.loc.col && d2.message === `Cannot find '${i.path}'`);
|
|
30490
|
+
if (d) d.message = reason;
|
|
30491
|
+
}
|
|
30492
|
+
return result;
|
|
29453
30493
|
};
|
|
30494
|
+
return resolver;
|
|
29454
30495
|
}
|
|
29455
30496
|
function reachableFrom(result, start) {
|
|
29456
30497
|
const reached = /* @__PURE__ */ new Set([start]);
|
|
@@ -29472,12 +30513,12 @@ function ownDiagnostics(result) {
|
|
|
29472
30513
|
if (i.loc.file !== void 0 || i.resolved === void 0) continue;
|
|
29473
30514
|
const files = reachableFrom(result, i.resolved);
|
|
29474
30515
|
const errors = result.diagnostics.filter((d) => d.severity === "error" && d.file !== void 0 && files.has(d.file)).length;
|
|
29475
|
-
if (errors) own.push({ severity: "error", message: `'${(0,
|
|
30516
|
+
if (errors) own.push({ severity: "error", message: `'${(0, import_node_path5.basename)(i.path)}' has ${errors} error${errors === 1 ? "" : "s"}`, ...i.loc });
|
|
29476
30517
|
}
|
|
29477
30518
|
return own.sort((a, b) => a.line - b.line || a.col - b.col);
|
|
29478
30519
|
}
|
|
29479
30520
|
function importLinks(result) {
|
|
29480
|
-
return (result.imports ?? []).filter((i) => i.loc.file === void 0 && i.resolved !== void 0).map((i) => {
|
|
30521
|
+
return (result.imports ?? []).filter((i) => i.loc.file === void 0 && i.resolved !== void 0 && !isRemoteSpec(i.resolved)).map((i) => {
|
|
29481
30522
|
const range = toRange(i.loc);
|
|
29482
30523
|
return {
|
|
29483
30524
|
range: { start: { ...range.start, character: range.start.character + 1 }, end: { ...range.end, character: Math.max(range.start.character + 1, range.end.character - 1) } },
|
|
@@ -29784,7 +30825,7 @@ function positionAt(text, offset) {
|
|
|
29784
30825
|
|
|
29785
30826
|
// src/server.ts
|
|
29786
30827
|
if (process.argv.includes("--version")) {
|
|
29787
|
-
console.log(true ? "0.
|
|
30828
|
+
console.log(true ? "0.14.0" : "dev");
|
|
29788
30829
|
process.exit(0);
|
|
29789
30830
|
}
|
|
29790
30831
|
var connection = (0, import_node.createConnection)(import_node.ProposedFeatures.all);
|
|
@@ -29795,11 +30836,13 @@ var uriOf = (path) => (0, import_node_url.pathToFileURL)(path).href;
|
|
|
29795
30836
|
function openText(path) {
|
|
29796
30837
|
return documents.all().find((d) => pathOf(d.uri) === path)?.getText();
|
|
29797
30838
|
}
|
|
29798
|
-
var resolve3 = fileResolver(openText);
|
|
29799
30839
|
function analysisOf(doc) {
|
|
29800
30840
|
const cached = cache2.get(doc.uri);
|
|
29801
30841
|
if (cached?.version === doc.version) return cached.analysis;
|
|
29802
|
-
const
|
|
30842
|
+
const path = pathOf(doc.uri);
|
|
30843
|
+
const resolve3 = fileResolver({ entry: path, read: openText });
|
|
30844
|
+
const analysis = analyze2(doc.getText(), { path, resolve: resolve3 });
|
|
30845
|
+
resolve3.explain(analysis.result);
|
|
29803
30846
|
const files = new Set((analysis.result.imports ?? []).flatMap((i) => i.resolved ? [i.resolved] : []));
|
|
29804
30847
|
cache2.set(doc.uri, { version: doc.version, analysis, files });
|
|
29805
30848
|
return analysis;
|
|
@@ -29828,7 +30871,7 @@ connection.onInitialize(() => ({
|
|
|
29828
30871
|
codeActionProvider: { codeActionKinds: [import_node.CodeActionKind.QuickFix] },
|
|
29829
30872
|
semanticTokensProvider: { legend: { tokenTypes: TOKEN_TYPES, tokenModifiers: [] }, full: true }
|
|
29830
30873
|
},
|
|
29831
|
-
serverInfo: { name: "proschi-language-server", version: true ? "0.
|
|
30874
|
+
serverInfo: { name: "proschi-language-server", version: true ? "0.14.0" : "dev" }
|
|
29832
30875
|
}));
|
|
29833
30876
|
function validate(document) {
|
|
29834
30877
|
const analysis = analysisOf(document);
|
|
@@ -29872,7 +30915,7 @@ documents.onDidChangeContent(({ document }) => {
|
|
|
29872
30915
|
function watchedStamp() {
|
|
29873
30916
|
return watchedFiles().map((file) => {
|
|
29874
30917
|
try {
|
|
29875
|
-
return `${file}:${(0,
|
|
30918
|
+
return `${file}:${(0, import_node_fs5.statSync)(file).mtimeMs}`;
|
|
29876
30919
|
} catch {
|
|
29877
30920
|
return `${file}:missing`;
|
|
29878
30921
|
}
|