proschi 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.cjs CHANGED
@@ -1303,8 +1303,8 @@ var require_semaphore = __commonJS({
1303
1303
  this._waiting = [];
1304
1304
  }
1305
1305
  lock(thunk) {
1306
- return new Promise((resolve4, reject) => {
1307
- this._waiting.push({ thunk, resolve: resolve4, reject });
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 (resolve4, reject) => {
2933
+ return new Promise(async (resolve3, reject) => {
2934
2934
  const resolveWithCleanup = (r) => {
2935
- resolve4(r);
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((resolve4, reject) => {
8578
- value.then((resolved) => resolve4(resolved), (error) => reject(error));
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 = resolve4;
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 resolve4(moduleName, nodePath, cwd, tracer) {
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((resolve5, reject) => {
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
- resolve5(message2.r);
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 resolve4(moduleName, nodePath, nodePath, tracer).then((value) => {
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 resolve4(moduleName, resolveGlobalNodePath(tracer), workspaceRoot, tracer);
9077
+ return resolve3(moduleName, resolveGlobalNodePath(tracer), workspaceRoot, tracer);
9078
9078
  });
9079
9079
  } else {
9080
- return resolve4(moduleName, resolveGlobalNodePath(tracer), workspaceRoot, tracer);
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((resolve4, reject) => {
9162
+ return new Promise((resolve3, reject) => {
9163
9163
  const callback = (error) => {
9164
9164
  if (error === void 0 || error === null) {
9165
- resolve4();
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((resolve4, _reject) => {
9458
- connectResolve = resolve4;
9457
+ const connected = new Promise((resolve3, _reject) => {
9458
+ connectResolve = resolve3;
9459
9459
  });
9460
- return new Promise((resolve4, reject) => {
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
- resolve4({
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((resolve4, _reject) => {
9489
- connectResolve = resolve4;
9488
+ const connected = new Promise((resolve3, _reject) => {
9489
+ connectResolve = resolve3;
9490
9490
  });
9491
- return new Promise((resolve4, reject) => {
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
- resolve4({
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 = resolve4.call(this, root, ref);
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 resolve4(root, ref) {
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 resolve4(baseURI, relativeURI, options) {
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: resolve4,
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 ?? sep?.[0],
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 && !sep) {
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(sep ?? [], {
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, sep, null, valueProps, onError);
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 sep = "";
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 += sep + cb;
21458
- sep = "";
21457
+ comment += sep2 + cb;
21458
+ sep2 = "";
21459
21459
  break;
21460
21460
  }
21461
21461
  case "newline":
21462
21462
  if (comment)
21463
- sep += source2;
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 ?? sep?.[0],
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 && !sep && !value) {
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 && !sep && !props.found) {
21572
- const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, sep, null, props, onError);
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(sep ?? [], {
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 (sep)
21596
- for (const st of sep) {
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, sep, null, valueProps, onError) : null;
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 sep = "";
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 += sep + indent2.slice(trimIndent) + content;
21811
- sep = "\n";
21810
+ value += sep2 + indent2.slice(trimIndent) + content;
21811
+ sep2 = "\n";
21812
21812
  } else if (indent2.length > trimIndent || content[0] === " ") {
21813
- if (sep === " ")
21814
- sep = "\n";
21815
- else if (!prevMoreIndented && sep === "\n")
21816
- sep = "\n\n";
21817
- value += sep + indent2.slice(trimIndent) + content;
21818
- sep = "\n";
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 (sep === "\n")
21821
+ if (sep2 === "\n")
21822
21822
  value += "\n";
21823
21823
  else
21824
- sep = "\n";
21824
+ sep2 = "\n";
21825
21825
  } else {
21826
- value += sep + content;
21827
- sep = " ";
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 sep = " ";
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 (sep === "\n")
22016
- res += sep;
22015
+ if (sep2 === "\n")
22016
+ res += sep2;
22017
22017
  else
22018
- sep = "\n";
22018
+ sep2 = "\n";
22019
22019
  } else {
22020
- res += sep + lm;
22021
- sep = " ";
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 + sep + (match?.[1] ?? "");
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 (sep)
22845
- for (const st of sep)
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 sep;
24012
+ let sep2;
24013
24013
  if (scalar.end) {
24014
- sep = scalar.end;
24015
- sep.push(this.sourceToken);
24014
+ sep2 = scalar.end;
24015
+ sep2.push(this.sourceToken);
24016
24016
  delete scalar.end;
24017
24017
  } else
24018
- sep = [this.sourceToken];
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 sep = it.sep;
24177
- sep.push(this.sourceToken);
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 sep = fc.end.splice(1, fc.end.length);
24379
- sep.push(this.sourceToken);
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 import_node_fs4 = require("node:fs");
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");
@@ -25933,6 +25933,8 @@ function editDistance2(a, b) {
25933
25933
  var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
25934
25934
  var DEFAULT_TECH = "Rectangle";
25935
25935
  var DEFAULT_GROUP_TECH = "Logical Group";
25936
+ var SYSTEM_TECH = "Service";
25937
+ var STUB_HELP = "a rate, p99, latency or availability, e.g. capacity { 2k rps p99 150ms availability 99.95% }";
25936
25938
  var MAX_SCENARIOS = 32;
25937
25939
  var MAX_ALT_DEPTH = 16;
25938
25940
  var DIRECTIONS = ["down", "right", "up", "left"];
@@ -25977,7 +25979,7 @@ var DID_YOU_MEAN = /^Unknown tech stack '(.*)'\. Did you mean '([^']+)'\?/;
25977
25979
  function parse(source2, options = {}) {
25978
25980
  return new Parser(source2, options).run();
25979
25981
  }
25980
- var Parser = class {
25982
+ var Parser = class _Parser {
25981
25983
  // Per-file state, swapped while an imported file is read.
25982
25984
  lines;
25983
25985
  /** Path of the imported file being read; undefined for the root document. */
@@ -25998,7 +26000,12 @@ var Parser = class {
25998
26000
  /** Files being read, outermost first, to find import cycles. */
25999
26001
  chain;
26000
26002
  /** Diagnostics are sorted by file in the order the files were first read, the root first. */
26001
- fileOrder = /* @__PURE__ */ new Map([[void 0, 0]]);
26003
+ fileOrder;
26004
+ /** The file read as the root: undefined for a document, the path for a system's team file (its own scope). */
26005
+ rootFile;
26006
+ teamFiles;
26007
+ systems = [];
26008
+ systemById = /* @__PURE__ */ new Map();
26002
26009
  /** `a|b` for every connection, both directions; filled before use cases are built. */
26003
26010
  connected = /* @__PURE__ */ new Set();
26004
26011
  title;
@@ -26020,14 +26027,18 @@ var Parser = class {
26020
26027
  deferred = [];
26021
26028
  built = [];
26022
26029
  options;
26023
- constructor(source2, options) {
26030
+ constructor(source2, options, scope2) {
26024
26031
  this.options = options;
26025
26032
  this.lines = source2.split("\n");
26026
- this.chain = [options.path];
26033
+ this.chain = scope2 ? scope2.chain : [options.path];
26034
+ this.teamFiles = scope2?.teamFiles ?? /* @__PURE__ */ new Map();
26035
+ if (scope2) this.rootFile = this.file = options.path;
26036
+ this.fileOrder = /* @__PURE__ */ new Map([[this.rootFile, 0]]);
26027
26037
  if (options.path !== void 0) this.included.add(options.path);
26028
26038
  }
26029
26039
  run() {
26030
26040
  this.parseFile();
26041
+ this.resolvePorts();
26031
26042
  this.createImplicitNodes();
26032
26043
  this.resolveTags();
26033
26044
  for (const e of this.edges) this.connected.add(`${e.source}|${e.target}`).add(`${e.target}|${e.source}`);
@@ -26050,7 +26061,8 @@ var Parser = class {
26050
26061
  ...this.decisions.length ? { decisions: this.decisions } : {},
26051
26062
  ...this.tests.length ? { tests: this.tests } : {},
26052
26063
  ...this.legend.length ? { legend: this.legend } : {},
26053
- ...Object.keys(this.layout).length ? { layout: this.layout } : {}
26064
+ ...Object.keys(this.layout).length ? { layout: this.layout } : {},
26065
+ ...this.systems.length ? { systems: this.systems } : {}
26054
26066
  };
26055
26067
  const order = (d) => this.fileOrder.get(d.file) ?? 0;
26056
26068
  this.diagnostics.sort((a, b) => order(a) - order(b) || a.line - b.line || a.col - b.col);
@@ -26067,14 +26079,16 @@ var Parser = class {
26067
26079
  i = this.parseLine(i);
26068
26080
  }
26069
26081
  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);
26082
+ 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
26083
  this.error(`Missing } to close ${what}`, frame.loc);
26084
+ if (frame.kind === "system") this.finishSystem(frame);
26072
26085
  }
26073
26086
  }
26074
26087
  /** Parses the line at `index` and returns the index of the last line it consumed. */
26075
26088
  parseLine(index) {
26076
26089
  const line = index + 1;
26077
- const { tokens, diagnostics } = tokenizeLine(this.lines[index], line);
26090
+ const { tokens: lexed, diagnostics } = tokenizeLine(this.lines[index], line);
26091
+ const tokens = qualify(lexed);
26078
26092
  this.diagnostics.push(...diagnostics.map((d) => ({ ...d, ...this.fileField() })));
26079
26093
  if (tokens.length === 0 || diagnostics.some((d) => d.severity === "error")) return index;
26080
26094
  const [first, second, third] = tokens;
@@ -26095,13 +26109,17 @@ var Parser = class {
26095
26109
  if (first.kind === "rbrace") {
26096
26110
  this.stack.pop();
26097
26111
  if (!top.discard) this.closeSection(top);
26098
- if (!top.discard && this.file === void 0) this.blocks.push({ kind: top.section, start: top.loc.line, end: line });
26112
+ if (!top.discard && this.file === this.rootFile) this.blocks.push({ kind: top.section, start: top.loc.line, end: line });
26099
26113
  if (second) this.error("Unexpected input after }", loc(second));
26100
26114
  } else if (!top.discard) {
26101
26115
  this.parseSectionLine(top, tokens, line);
26102
26116
  }
26103
26117
  return index;
26104
26118
  }
26119
+ if (top?.kind === "system" || top?.kind === "stub") {
26120
+ this.parseSystemLine(top, tokens, line);
26121
+ return index;
26122
+ }
26105
26123
  if (first.kind === "rbrace") {
26106
26124
  if (this.stack.length === 0) this.error("Unmatched }", loc(first));
26107
26125
  else this.closeFrame();
@@ -26123,6 +26141,11 @@ var Parser = class {
26123
26141
  case "group":
26124
26142
  this.parseGroup(tokens, line);
26125
26143
  break;
26144
+ // A keyword only as `system id … {`, so a node may still be called system.
26145
+ case "system":
26146
+ if (second?.kind === "ident" && tokens[tokens.length - 1].kind === "lbrace") this.parseSystem(tokens, line);
26147
+ else this.parseNode(tokens, line);
26148
+ break;
26126
26149
  case "usecase":
26127
26150
  this.parseUseCase(tokens, line);
26128
26151
  break;
@@ -26177,7 +26200,7 @@ var Parser = class {
26177
26200
  return;
26178
26201
  }
26179
26202
  const summary = tokens[2]?.kind === "string" ? tokens[2] : void 0;
26180
- if (this.file === void 0) {
26203
+ if (this.file === this.rootFile) {
26181
26204
  this.title = value.value;
26182
26205
  if (summary) this.summary = summary.value;
26183
26206
  }
@@ -26209,7 +26232,7 @@ var Parser = class {
26209
26232
  this.error("direction is only allowed at the top level or inside a group", this.locOf(keyword, line));
26210
26233
  return;
26211
26234
  }
26212
- if (this.file !== void 0) return;
26235
+ if (this.file !== this.rootFile) return;
26213
26236
  if (this.layoutLocs.direction) {
26214
26237
  this.warning(`direction is already set ${this.where(this.layoutLocs.direction, loc)}; this one is ignored`, this.locOf(keyword, line));
26215
26238
  return;
@@ -26229,7 +26252,7 @@ var Parser = class {
26229
26252
  this.error("layout ordered applies to the whole diagram; write it at the top level", this.locOf(keyword, line));
26230
26253
  return;
26231
26254
  }
26232
- if (this.file !== void 0) return;
26255
+ if (this.file !== this.rootFile) return;
26233
26256
  if (this.layoutLocs.ordered) {
26234
26257
  this.warning(`layout ordered is already set ${this.where(this.layoutLocs.ordered, loc)}`, this.locOf(keyword, line));
26235
26258
  return;
@@ -26279,21 +26302,13 @@ var Parser = class {
26279
26302
  const loc = this.locOf(pathToken, line);
26280
26303
  const entry = { path, loc };
26281
26304
  this.imports.push(entry);
26282
- const { resolve: resolve4 } = this.options;
26283
- if (!resolve4) {
26305
+ const { resolve: resolve3 } = this.options;
26306
+ if (!resolve3) {
26284
26307
  this.warning(`Imports are not resolved in this context; '${path}' was not loaded`, loc);
26285
26308
  return;
26286
26309
  }
26287
- let target;
26288
- try {
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
- }
26310
+ const target = this.resolveFile(path, loc);
26311
+ if (!target) return;
26297
26312
  entry.resolved = target.path;
26298
26313
  const cycleStart = this.chain.indexOf(target.path);
26299
26314
  if (cycleStart !== -1) {
@@ -26313,6 +26328,25 @@ var Parser = class {
26313
26328
  this.chain.pop();
26314
26329
  ({ lines: this.lines, file: this.file, stack: this.stack } = outer);
26315
26330
  }
26331
+ /**
26332
+ * Loads `path` through the resolver, relative to the file being read. A
26333
+ * resolver error named `ImportError` (outside the project root, not synced,
26334
+ * …) is reported with its own message; anything else is "Cannot find".
26335
+ */
26336
+ resolveFile(path, loc) {
26337
+ let target;
26338
+ try {
26339
+ target = this.options.resolve?.(path, this.file ?? this.options.path);
26340
+ } catch (e) {
26341
+ if (e instanceof Error && e.name === "ImportError") {
26342
+ this.error(e.message, loc);
26343
+ return void 0;
26344
+ }
26345
+ target = void 0;
26346
+ }
26347
+ if (!target) this.error(`Cannot find '${path}'`, loc);
26348
+ return target;
26349
+ }
26316
26350
  parseGroup(tokens, line) {
26317
26351
  const keyword = tokens[0];
26318
26352
  const top = this.top();
@@ -26372,6 +26406,273 @@ var Parser = class {
26372
26406
  this.addNode(group);
26373
26407
  this.stack.push({ kind: "group", id: idToken.value, loc });
26374
26408
  }
26409
+ // ---- Systems: a team's design behind a contract (docs/design/team-systems-and-imports.md) ----
26410
+ /** `system id ["Name"] [@team] [pos x,y] [color c] [.tag] [rank N] {` */
26411
+ parseSystem(tokens, line) {
26412
+ const [keyword, idToken] = tokens;
26413
+ const top = this.top();
26414
+ if (top && top.kind !== "group") {
26415
+ this.error("A system is declared at the top level or inside a group", this.locOf(keyword, line));
26416
+ this.skipSection("test", tokens, line);
26417
+ return;
26418
+ }
26419
+ const loc = this.locOf(idToken, line);
26420
+ const node = { id: idToken.value, kind: "system", name: idToken.value, type: "service", techStack: SYSTEM_TECH, parent: this.currentGroup(), loc };
26421
+ const style = {};
26422
+ let named = false;
26423
+ let i = 2;
26424
+ while (i < tokens.length - 1) {
26425
+ const t = tokens[i];
26426
+ const styleUsed = this.readStylePart(tokens, i, line, style, false);
26427
+ const used2 = styleUsed === 0 ? this.readRank(tokens, i, line, node) : styleUsed;
26428
+ if (used2 === void 0) return this.skipSection("test", tokens, line);
26429
+ if (used2 > 0) {
26430
+ i += used2;
26431
+ continue;
26432
+ }
26433
+ if (t.kind === "string" && !named) {
26434
+ node.name = t.value;
26435
+ named = true;
26436
+ } else if (t.kind === "string" && node.description === void 0) {
26437
+ node.description = t.value;
26438
+ } else if (t.kind === "team") {
26439
+ node.ownerTeam = t.value;
26440
+ } else if (isWord(t, "pos")) {
26441
+ const [x, comma, y] = [tokens[i + 1], tokens[i + 2], tokens[i + 3]];
26442
+ if (x?.kind !== "number" || comma?.kind !== "comma" || y?.kind !== "number") {
26443
+ this.error("Expected pos x,y", this.locOf(t, line));
26444
+ return this.skipSection("test", tokens, line);
26445
+ }
26446
+ node.position = { x: Number(x.value), y: Number(y.value) };
26447
+ i += 3;
26448
+ } else {
26449
+ this.error(`Unexpected ${describe(t)} in system header; write system id "Name" @Team {`, this.locOf(t, line));
26450
+ return this.skipSection("test", tokens, line);
26451
+ }
26452
+ i++;
26453
+ }
26454
+ this.applyStyle(node, style);
26455
+ const system = { id: node.id, entries: [], loc };
26456
+ if (!this.nodes.has(node.id)) {
26457
+ this.systems.push(system);
26458
+ this.systemById.set(system.id, system);
26459
+ }
26460
+ this.addNode(node);
26461
+ this.stack.push({ kind: "system", system, useCaseLocs: /* @__PURE__ */ new Map(), loc });
26462
+ }
26463
+ /** A line inside `system … { }` or its multi-line `capacity { }`. */
26464
+ parseSystemLine(frame, tokens, line) {
26465
+ const [first, second] = tokens;
26466
+ const { system } = frame;
26467
+ if (first.kind === "rbrace") {
26468
+ this.stack.pop();
26469
+ if (frame.kind === "system") this.finishSystem(frame);
26470
+ if (second) this.error("Unexpected input after }", this.locOf(second, line));
26471
+ return;
26472
+ }
26473
+ if (frame.kind === "stub") {
26474
+ this.readStub(system.stub, tokens, 0, tokens.length, line);
26475
+ return;
26476
+ }
26477
+ if (isWord(first, "from")) {
26478
+ if (second?.kind !== "string" || !second.value.trim()) {
26479
+ this.error('Expected the team file, e.g. from "teams/orders.proschi"', this.locOf(second ?? first, line));
26480
+ return;
26481
+ }
26482
+ if (system.from !== void 0) {
26483
+ this.error(`System '${system.id}' already has from "${system.from}"`, this.locOf(first, line));
26484
+ return;
26485
+ }
26486
+ this.expectEnd(tokens, 2, line);
26487
+ system.from = second.value;
26488
+ frame.fromLoc = this.locOf(second, line);
26489
+ return;
26490
+ }
26491
+ if (isWord(first, "expose")) return this.parseExpose(frame, tokens, line);
26492
+ if (isWord(first, "capacity") && second?.kind === "lbrace") {
26493
+ const opens = tokens[tokens.length - 1].kind !== "rbrace";
26494
+ if (system.stub) {
26495
+ this.error(`System '${system.id}' already has a capacity`, this.locOf(first, line));
26496
+ if (opens) this.stack.push({ kind: "section", section: "capacity", discard: true, loc: this.locOf(first, line) });
26497
+ return;
26498
+ }
26499
+ const stub = { loc: this.lineLoc(tokens, line) };
26500
+ system.stub = stub;
26501
+ this.readStub(stub, tokens, 2, opens ? tokens.length : tokens.length - 1, line);
26502
+ if (opens) this.stack.push({ kind: "stub", system, loc: this.locOf(first, line) });
26503
+ return;
26504
+ }
26505
+ this.error('Inside a system, write from "file", expose <id> "Name" -> "Use case" or capacity { \u2026 }', this.locOf(first, line));
26506
+ if (tokens[tokens.length - 1].kind === "lbrace") this.stack.push({ kind: "section", section: "test", discard: true, loc: this.locOf(first, line) });
26507
+ }
26508
+ /** `expose id ["Name"] [-> "Use case"]` */
26509
+ parseExpose(frame, tokens, line) {
26510
+ const [keyword, idToken] = tokens;
26511
+ if (idToken?.kind !== "ident") {
26512
+ this.error('Expected an entry point id, e.g. expose api "Orders API" -> "Place order"', this.locOf(idToken ?? keyword, line));
26513
+ return;
26514
+ }
26515
+ const entry = { id: idToken.value, name: idToken.value, loc: this.locOf(idToken, line) };
26516
+ let i = 2;
26517
+ if (tokens[i]?.kind === "string") entry.name = tokens[i++].value;
26518
+ if (tokens[i]?.kind === "arrow") {
26519
+ const useCase = tokens[i + 1];
26520
+ if (tokens[i].value !== "->" || useCase?.kind !== "string" && useCase?.kind !== "ident") {
26521
+ this.error('Expected -> "Use case" after the entry point', this.locOf(useCase ?? tokens[i], line));
26522
+ return;
26523
+ }
26524
+ entry.useCase = useCase.value;
26525
+ frame.useCaseLocs.set(entry, this.locOf(useCase, line));
26526
+ i += 2;
26527
+ }
26528
+ if (!this.expectEnd(tokens, i, line)) return;
26529
+ if (frame.system.entries.some((e) => e.id === entry.id)) {
26530
+ this.error(`System '${frame.system.id}' already exposes '${entry.id}'`, entry.loc);
26531
+ return;
26532
+ }
26533
+ frame.system.entries.push(entry);
26534
+ }
26535
+ /** The parts of a system's `capacity { … }` in `tokens[from, to)`. */
26536
+ readStub(stub, tokens, from, to, line) {
26537
+ for (let i = from; i < to; ) {
26538
+ const t = tokens[i];
26539
+ if (t.kind === "quantity" || t.kind === "number") {
26540
+ const rps = this.quantityOf(t, "rps", t, line);
26541
+ if (rps === void 0) return;
26542
+ stub.rps = rps;
26543
+ i++;
26544
+ } else if (isWord(t, "p99") || isWord(t, "latency") || isWord(t, "availability")) {
26545
+ const value = this.quantityOf(i + 1 < to ? tokens[i + 1] : void 0, t.value === "availability" ? "%" : "ms", t, line);
26546
+ if (value === void 0) return;
26547
+ if (t.value === "p99") stub.p99Ms = value;
26548
+ else if (t.value === "latency") stub.latencyMs = value;
26549
+ else stub.availability = value;
26550
+ i += 2;
26551
+ } else {
26552
+ this.error(`Expected ${STUB_HELP}`, this.locOf(t, line));
26553
+ return;
26554
+ }
26555
+ }
26556
+ }
26557
+ /** At the system's `}`: loads the team file and checks the entry points against its use cases. */
26558
+ finishSystem(frame) {
26559
+ const { system } = frame;
26560
+ if (system.from !== void 0) {
26561
+ const team2 = this.loadTeam(system.from, frame.fromLoc ?? system.loc);
26562
+ if (team2) {
26563
+ system.resolved = team2.path;
26564
+ system.diagram = team2.diagram;
26565
+ }
26566
+ if (system.stub) {
26567
+ this.warning(`System '${system.id}' has a from file, which sets its contract; this capacity is ignored`, system.stub.loc);
26568
+ if (team2) delete system.stub;
26569
+ }
26570
+ } else if (!system.stub) {
26571
+ this.warning(`System '${system.id}' has no from "file" and no capacity { \u2026 }; it is drawn but not simulated`, system.loc);
26572
+ }
26573
+ const team = system.diagram;
26574
+ if (!team) return;
26575
+ const name = fileName(system.resolved ?? "");
26576
+ for (const entry of system.entries) {
26577
+ const wanted = entry.useCase;
26578
+ if (wanted !== void 0) {
26579
+ const u = findUseCaseIn(team, wanted);
26580
+ if (u) entry.useCaseId = u.id;
26581
+ else {
26582
+ const near = team.useCases.find((x) => editDistance2(x.name.toLowerCase(), wanted.toLowerCase()) <= Math.max(1, Math.floor(wanted.length / 4)));
26583
+ const all = team.useCases.map((x) => `"${x.name}"`).join(", ");
26584
+ const help = near ? ` Did you mean "${near.name}"?` : all ? ` Its use cases are ${all}` : "";
26585
+ this.error(`${name} has no use case "${wanted}".${help}`, frame.useCaseLocs.get(entry) ?? entry.loc);
26586
+ }
26587
+ } else if (team.useCases.length === 1) {
26588
+ entry.useCaseId = team.useCases[0].id;
26589
+ } else if (team.useCases.length === 0) {
26590
+ this.error(`${name} has no use case for '${entry.id}' to enter`, entry.loc);
26591
+ } else {
26592
+ this.error(`Say which use case '${entry.id}' enters, e.g. expose ${entry.id} -> "${team.useCases[0].name}"`, entry.loc);
26593
+ }
26594
+ }
26595
+ 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);
26596
+ }
26597
+ /** Parses a team file in its own scope (once per root parse), through the import resolver. */
26598
+ loadTeam(path, loc) {
26599
+ if (!this.options.resolve) {
26600
+ this.warning(`Files are not resolved in this context; '${path}' was not loaded`, loc);
26601
+ return void 0;
26602
+ }
26603
+ const target = this.resolveFile(path, loc);
26604
+ if (!target) return void 0;
26605
+ const cycleStart = this.chain.indexOf(target.path);
26606
+ if (cycleStart !== -1) {
26607
+ const names = [...this.chain.slice(cycleStart), target.path].map((p) => fileName(p ?? ""));
26608
+ this.error(`System cycle: ${names.join(" \u2192 ")}`, loc);
26609
+ return void 0;
26610
+ }
26611
+ let result = this.teamFiles.get(target.path);
26612
+ if (!result) {
26613
+ const scope2 = { chain: [...this.chain, target.path], teamFiles: this.teamFiles };
26614
+ result = new _Parser(target.source, { path: target.path, resolve: this.options.resolve }, scope2).run();
26615
+ this.teamFiles.set(target.path, result);
26616
+ for (const d of result.diagnostics) {
26617
+ if (!this.fileOrder.has(d.file)) this.fileOrder.set(d.file, this.fileOrder.size);
26618
+ this.diagnostics.push(d);
26619
+ }
26620
+ }
26621
+ return { path: target.path, diagram: result.diagram };
26622
+ }
26623
+ /**
26624
+ * Resolves `orders.api` references once every file is read: the entry point
26625
+ * `api` of system `orders`. Edges and references then name the system, with
26626
+ * the entry point in `sourcePort` / `targetPort`. On a connection to a
26627
+ * non-system, `b.planned` stays what it always was: `b` with a tag.
26628
+ */
26629
+ resolvePorts() {
26630
+ for (const ref of this.references) {
26631
+ const dot = ref.id.indexOf(".");
26632
+ if (dot === -1) continue;
26633
+ const prefix = ref.id.slice(0, dot);
26634
+ const port = ref.id.slice(dot + 1);
26635
+ const system = this.systemById.get(prefix);
26636
+ if (system) {
26637
+ if (!system.entries.some((e) => e.id === port)) this.error(this.unexposedMessage(system, port), ref.loc);
26638
+ } else if (ref.edgeTarget) {
26639
+ ref.edgeTarget.tag = port;
26640
+ this.tagRefs.push({ tag: port, loc: { ...ref.loc, col: ref.loc.col + dot, length: ref.loc.length - dot }, target: ref.edgeTarget });
26641
+ } else {
26642
+ this.error(`'${prefix}' is not a system, so '${ref.id}' names nothing; write ${prefix}, or declare system ${prefix} { \u2026 }`, ref.loc);
26643
+ }
26644
+ ref.id = prefix;
26645
+ }
26646
+ for (const edge of this.edges) {
26647
+ const [source2, sourcePort] = this.splitRef(edge.source);
26648
+ const [target, targetPort] = this.splitRef(edge.target);
26649
+ edge.source = source2;
26650
+ edge.target = target;
26651
+ if (sourcePort !== void 0) edge.sourcePort = sourcePort;
26652
+ if (targetPort !== void 0) edge.targetPort = targetPort;
26653
+ }
26654
+ }
26655
+ unexposedMessage(system, port) {
26656
+ const entries2 = system.entries.map((e) => `${system.id}.${e.id}`);
26657
+ const known = entries2.length ? `; connect to ${entries2.join(" or ")}` : "";
26658
+ const internal = system.diagram?.nodes.some((n) => n.id === port);
26659
+ 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}`;
26660
+ }
26661
+ /** `orders.api` → `['orders', 'api']` when `orders` is a system; else the id up to its dot, without a port. */
26662
+ splitRef(id) {
26663
+ const dot = id.indexOf(".");
26664
+ if (dot === -1) return [id, void 0];
26665
+ const prefix = id.slice(0, dot);
26666
+ return [prefix, this.systemById.has(prefix) ? id.slice(dot + 1) : void 0];
26667
+ }
26668
+ /** A step to a whole system enters its only entry point; with several it must name one. */
26669
+ onlyEntry(id, loc) {
26670
+ const system = this.systemById.get(id);
26671
+ if (!system || system.entries.length === 0) return void 0;
26672
+ if (system.entries.length === 1) return system.entries[0].id;
26673
+ this.errorOnce(`Say which entry point of '${id}' this step enters: ${system.entries.map((e) => `${id}.${e.id}`).join(" or ")}`, loc);
26674
+ return void 0;
26675
+ }
26375
26676
  parseUseCase(tokens, line) {
26376
26677
  const keyword = tokens[0];
26377
26678
  if (this.stack.length > 0) {
@@ -26561,7 +26862,8 @@ var Parser = class {
26561
26862
  const from = fromToken.value;
26562
26863
  const to = toToken.value;
26563
26864
  const arrow = arrowToken.value;
26564
- this.references.push({ id: from, loc: this.locOf(fromToken, line) }, { id: to, loc: this.locOf(toToken, line) });
26865
+ const toRef = { id: to, loc: this.locOf(toToken, line) };
26866
+ this.references.push({ id: from, loc: this.locOf(fromToken, line) }, toRef);
26565
26867
  const top = this.top();
26566
26868
  const inUseCase = top?.kind === "usecase" || top?.kind === "alt" || top?.kind === "par";
26567
26869
  if (tagToken && inUseCase) {
@@ -26596,6 +26898,7 @@ var Parser = class {
26596
26898
  edge.tag = tagToken.value;
26597
26899
  this.tagRefs.push({ tag: tagToken.value, loc: this.locOf(tagToken, line), target: edge });
26598
26900
  }
26901
+ if (!tagToken) toRef.edgeTarget = edge;
26599
26902
  this.edges.push(edge);
26600
26903
  }
26601
26904
  return lastLine;
@@ -27506,7 +27809,9 @@ var Parser = class {
27506
27809
  for (const step of raw) {
27507
27810
  this.checkConnection(step);
27508
27811
  if (step.arrow === "-->") {
27509
- const request2 = [...steps].reverse().find((s) => s.fromServiceId === step.to && s.toServiceId === step.from && !answered.has(s) && !s.failed);
27812
+ const [respFrom] = this.splitRef(step.from);
27813
+ const [respTo] = this.splitRef(step.to);
27814
+ const request2 = [...steps].reverse().find((s) => s.fromServiceId === respTo && s.toServiceId === respFrom && !answered.has(s) && !s.failed);
27510
27815
  if (!request2) {
27511
27816
  this.warning(`Response has no matching request from '${step.to}' to '${step.from}'`, step.loc);
27512
27817
  continue;
@@ -27525,14 +27830,17 @@ var Parser = class {
27525
27830
  continue;
27526
27831
  }
27527
27832
  const label = parseStepLabel(step.label);
27528
- const target = this.nodes.get(step.to);
27833
+ const [from, fromPort] = this.splitRef(step.from);
27834
+ const [to, toPort] = this.splitRef(step.to);
27835
+ const port = toPort ?? this.onlyEntry(to, step.loc);
27836
+ const target = this.nodes.get(to);
27529
27837
  const isAsync = step.arrow === "->>";
27530
27838
  steps.push({
27531
27839
  id: `${prefix}-${steps.length + 1}`,
27532
27840
  stepOrder: steps.length,
27533
27841
  stepName: label.name || `${step.from} \u2192 ${step.to}`,
27534
- fromServiceId: step.from,
27535
- toServiceId: step.to,
27842
+ fromServiceId: from,
27843
+ toServiceId: to,
27536
27844
  protocol: inferProtocol(isAsync, label.method, target),
27537
27845
  httpMethod: label.method ?? "",
27538
27846
  endpoint: label.endpoint ?? "",
@@ -27548,7 +27856,9 @@ var Parser = class {
27548
27856
  loc: step.loc,
27549
27857
  ...step.multiplier !== void 0 ? { multiplier: step.multiplier } : {},
27550
27858
  ...step.sizeBytes !== void 0 ? { sizeBytes: step.sizeBytes } : {},
27551
- access: accessOf(label.method, step.label)
27859
+ access: accessOf(label.method, step.label),
27860
+ ...port !== void 0 ? { toPort: port } : {},
27861
+ ...fromPort !== void 0 ? { fromPort } : {}
27552
27862
  });
27553
27863
  }
27554
27864
  return steps;
@@ -27558,8 +27868,10 @@ var Parser = class {
27558
27868
  * Documents without any connection are use-case-only sketches; they are left alone.
27559
27869
  */
27560
27870
  checkConnection(step) {
27561
- if (this.edges.length === 0 || step.from === step.to || this.connected.has(`${step.from}|${step.to}`)) return;
27562
- const [from, to] = step.arrow === "-->" ? [step.to, step.from] : [step.from, step.to];
27871
+ const [a] = this.splitRef(step.from);
27872
+ const [b] = this.splitRef(step.to);
27873
+ if (this.edges.length === 0 || a === b || this.connected.has(`${a}|${b}`)) return;
27874
+ const [from, to] = step.arrow === "-->" ? [b, a] : [a, b];
27563
27875
  this.warning(`No connection between '${from}' and '${to}' in the architecture; add '${from} -> ${to}'`, step.loc);
27564
27876
  }
27565
27877
  createImplicitNodes() {
@@ -27655,6 +27967,17 @@ var Parser = class {
27655
27967
  error(message, loc) {
27656
27968
  this.diagnostics.push({ severity: "error", message, ...loc });
27657
27969
  }
27970
+ /** An error for a step that every scenario sharing it would report again. */
27971
+ errorOnce(message, loc) {
27972
+ const key = `error
27973
+ ${loc.file ?? ""}
27974
+ ${loc.line}
27975
+ ${loc.col}
27976
+ ${message}`;
27977
+ if (this.warned.has(key)) return;
27978
+ this.warned.add(key);
27979
+ this.error(message, loc);
27980
+ }
27658
27981
  warning(message, loc) {
27659
27982
  const key = `${loc.file ?? ""}
27660
27983
  ${loc.line}
@@ -27690,6 +28013,23 @@ function expand(items) {
27690
28013
  }
27691
28014
  return paths;
27692
28015
  }
28016
+ function qualify(tokens) {
28017
+ let out;
28018
+ for (let i = 0; i < tokens.length; i++) {
28019
+ const [t, next] = [tokens[i], tokens[i + 1]];
28020
+ const joined = t.kind === "ident" && next?.kind === "tag" && next.col === t.col + t.length;
28021
+ if (joined && (tokens[i + 2]?.kind === "arrow" || tokens[i - 1]?.kind === "arrow")) {
28022
+ out ??= tokens.slice(0, i);
28023
+ out.push({ ...t, value: `${t.value}.${next.value}`, length: t.length + next.length });
28024
+ i++;
28025
+ } else out?.push(t);
28026
+ }
28027
+ return out ?? tokens;
28028
+ }
28029
+ function findUseCaseIn(diagram, name) {
28030
+ const lower = name.toLowerCase();
28031
+ return diagram.useCases.find((u) => u.name === name) ?? diagram.useCases.find((u) => u.name.toLowerCase() === lower) ?? diagram.useCases.find((u) => u.id === name);
28032
+ }
27693
28033
  function isWord(token, word) {
27694
28034
  return token?.kind === "ident" && token.value === word;
27695
28035
  }
@@ -28489,6 +28829,340 @@ function formatUsd(usd) {
28489
28829
  return `$${Math.round(usd).toLocaleString("en-US")}/month`;
28490
28830
  }
28491
28831
 
28832
+ // ../frontend/src/sim/overlay.ts
28833
+ function flowsOf(diagram, traffic) {
28834
+ const flows = /* @__PURE__ */ new Map();
28835
+ for (const t of traffic) {
28836
+ const u = findUseCase(diagram, t.useCase);
28837
+ if (!u || t.rps <= 0) continue;
28838
+ for (const sc of u.scenarios) {
28839
+ const share = t.mix ? t.mix.find((m) => m.scenario === sc.name)?.share ?? 0 : sc === u.scenarios[0] ? 1 : 0;
28840
+ if (share <= 0) continue;
28841
+ for (const step of sc.steps) {
28842
+ if (step.failed) continue;
28843
+ const key = `${step.fromServiceId}>${step.toServiceId}`;
28844
+ const f = flows.get(key) ?? { from: step.fromServiceId, to: step.toServiceId, rps: 0, async: step.executionType !== "SYNC_REQUEST_RESPONSE" };
28845
+ f.rps += t.rps * share * (step.multiplier ?? 1);
28846
+ flows.set(key, f);
28847
+ }
28848
+ }
28849
+ }
28850
+ return [...flows.values()];
28851
+ }
28852
+ function analysisOverlay(diagram, analysis) {
28853
+ const nodes = {};
28854
+ for (const n of analysis.nodes) {
28855
+ const works = Number.isFinite(n.capacityRps) && n.kind !== "client";
28856
+ nodes[n.id] = {
28857
+ ...works ? { utilization: n.utilization, saturated: n.saturated } : {},
28858
+ ...n.replicas > 1 ? { replicas: n.replicas } : {},
28859
+ ...n.shards > 1 ? { shards: n.shards } : {}
28860
+ };
28861
+ }
28862
+ return { nodes, flows: flowsOf(diagram, diagram.traffic ?? []) };
28863
+ }
28864
+
28865
+ // ../frontend/src/sim/systems.ts
28866
+ var teamKey = (s) => s.resolved ?? s.diagram;
28867
+ var contracts = /* @__PURE__ */ new WeakMap();
28868
+ function contractOf(system, options = {}) {
28869
+ let contract = contracts.get(system);
28870
+ if (!contract) {
28871
+ contract = system.diagram ? designContract(system, system.diagram, options) : stubContract(system);
28872
+ contracts.set(system, contract);
28873
+ }
28874
+ return contract;
28875
+ }
28876
+ function stubContract(system) {
28877
+ const stub = system.stub;
28878
+ if (!stub) return { source: "none", entries: system.entries.map((e) => emptyEntry(e.id, e.name)) };
28879
+ const rps = stub.rps ?? Infinity;
28880
+ return {
28881
+ source: "stub",
28882
+ entries: system.entries.map((e) => ({
28883
+ ...emptyEntry(e.id, e.name),
28884
+ latency: stub.p99Ms !== void 0 ? [{ percentile: 99, maxMs: stub.p99Ms }] : [],
28885
+ ...stub.availability !== void 0 ? { availabilityPercent: stub.availability } : {},
28886
+ saturatesAtRps: rps,
28887
+ hotAtRps: rps * HOT
28888
+ }))
28889
+ };
28890
+ }
28891
+ var emptyEntry = (entry, name) => ({ entry, name, ownRps: 0, latency: [], saturatesAtRps: Infinity, hotAtRps: Infinity });
28892
+ function designContract(system, team, options) {
28893
+ const traffic = resolveTraffic(team);
28894
+ const base = computeLoad(team, traffic);
28895
+ const baseBytes = computeBytesPerSecond(team, traffic);
28896
+ const profiles = profilesOf(team);
28897
+ const nodes = components(team);
28898
+ const entries2 = system.entries.map((e) => {
28899
+ const u = team.useCases.find((x) => x.id === e.useCaseId);
28900
+ if (!u) return emptyEntry(e.id, e.name);
28901
+ const own = traffic.get(u.id);
28902
+ const unit = /* @__PURE__ */ new Map([[u.id, { rps: 1, shares: own.shares }]]);
28903
+ const delta = computeLoad(team, unit);
28904
+ const deltaBytes = computeBytesPerSecond(team, unit);
28905
+ const result = { ...emptyEntry(e.id, e.name), useCase: u.name, ownRps: own.rps };
28906
+ for (const n of nodes) {
28907
+ const d = delta.get(n.id);
28908
+ const db = deltaBytes.get(n.id) ?? 0;
28909
+ if (!d && !db) continue;
28910
+ const p = profiles.get(n.id);
28911
+ const replicas = replicasOf(n);
28912
+ const b = base.get(n.id) ?? { read: 0, write: 0 };
28913
+ const bb = baseBytes.get(n.id) ?? 0;
28914
+ const u0 = usageOf(p, replicas, b.read, b.write, bb);
28915
+ const u1 = usageOf(p, replicas, b.read + (d?.read ?? 0), b.write + (d?.write ?? 0), bb + db);
28916
+ const sat = crossing(u0, u1, 1);
28917
+ const hot = crossing(u0, u1, HOT);
28918
+ if (sat < result.saturatesAtRps) {
28919
+ result.saturatesAtRps = sat;
28920
+ result.bottleneck = n.id;
28921
+ }
28922
+ if (hot < result.hotAtRps) {
28923
+ result.hotAtRps = hot;
28924
+ result.hotNode = n.id;
28925
+ }
28926
+ }
28927
+ for (const r of team.requirements ?? []) {
28928
+ if (r.kind !== "latency" && r.kind !== "availability") continue;
28929
+ if (r.useCase !== void 0 && findUseCase(team, r.useCase) !== u) continue;
28930
+ if (r.kind === "latency") result.latency.push({ percentile: r.percentile, maxMs: r.maxMs, ...r.scenario !== void 0 ? { scenario: r.scenario } : {} });
28931
+ else result.availabilityPercent = Math.max(result.availabilityPercent ?? 0, r.minPercent);
28932
+ }
28933
+ return result;
28934
+ });
28935
+ return { source: "design", entries: entries2, baseline: analyze(team, { timeoutMs: options.timeoutMs }) };
28936
+ }
28937
+ function crossing(u0, u1, theta) {
28938
+ const terms = u0.writeScaling === "shards" ? [
28939
+ [u0.readUtilization, u1.readUtilization],
28940
+ [u0.writeUtilization, u1.writeUtilization]
28941
+ ] : [[u0.requestUtilization, u1.requestUtilization]];
28942
+ terms.push([u0.bandwidthUtilization, u1.bandwidthUtilization]);
28943
+ let best = Infinity;
28944
+ for (const [a, b] of terms) {
28945
+ const slope = b - a;
28946
+ if (slope <= 1e-15) continue;
28947
+ if (a >= theta) return 0;
28948
+ best = Math.min(best, (theta - a) / slope);
28949
+ }
28950
+ return best;
28951
+ }
28952
+ function inboundLoad(diagram, traffic) {
28953
+ const result = /* @__PURE__ */ new Map();
28954
+ forEachStepRate(diagram, traffic, (step, rate) => {
28955
+ if (step.toPort === void 0) return;
28956
+ const ports = result.get(step.toServiceId) ?? /* @__PURE__ */ new Map();
28957
+ ports.set(step.toPort, (ports.get(step.toPort) ?? 0) + rate * multiplierOf(step));
28958
+ result.set(step.toServiceId, ports);
28959
+ });
28960
+ return result;
28961
+ }
28962
+ function hopFrom(p50, p99) {
28963
+ const l50 = tailFactor(0.5);
28964
+ const l99 = tailFactor(0.99);
28965
+ let tailMs = Math.max(0, (p99 - p50) / (l99 - l50));
28966
+ let fixedMs = p50 - tailMs * l50;
28967
+ if (fixedMs < 0) {
28968
+ fixedMs = 0;
28969
+ tailMs = p99 / l99;
28970
+ }
28971
+ return { fixedMs, tailMs, waitProbability: 0 };
28972
+ }
28973
+ function analyzeSystems(diagram, traffic, options = {}) {
28974
+ const inbound = inboundLoad(diagram, traffic);
28975
+ if (!options.sharedSystems) options = { ...options, sharedSystems: sharedLoad(diagram, inbound) };
28976
+ const hops = /* @__PURE__ */ new Map();
28977
+ const availabilities = /* @__PURE__ */ new Map();
28978
+ const key = (id, port) => `${id}
28979
+ ${port}`;
28980
+ const systems = (diagram.systems ?? []).map((system) => {
28981
+ const result = analyzeSystem(system, inbound.get(system.id) ?? /* @__PURE__ */ new Map(), options);
28982
+ for (const e of result.entries) {
28983
+ hops.set(key(system.id, e.entry), hopFrom(e.p50Ms, e.p99Ms));
28984
+ availabilities.set(key(system.id, e.entry), e.availability);
28985
+ }
28986
+ return result;
28987
+ });
28988
+ return {
28989
+ systems,
28990
+ hop: (id, port) => port === void 0 ? void 0 : hops.get(key(id, port)),
28991
+ availability: (id, port) => port === void 0 ? void 0 : availabilities.get(key(id, port))
28992
+ };
28993
+ }
28994
+ function sentByUseCase(system, sent, into) {
28995
+ let total = 0;
28996
+ for (const e of system.entries) {
28997
+ const rps = sent.get(e.id) ?? 0;
28998
+ if (rps <= 0 || e.useCaseId === void 0) continue;
28999
+ into.set(e.useCaseId, (into.get(e.useCaseId) ?? 0) + rps);
29000
+ total += rps;
29001
+ }
29002
+ return total;
29003
+ }
29004
+ function sharedLoad(root, rootInbound) {
29005
+ const byFile = /* @__PURE__ */ new Map();
29006
+ const designs = /* @__PURE__ */ new Map();
29007
+ const order = [];
29008
+ const visit = (d, prefix) => {
29009
+ for (const s of d.systems ?? []) {
29010
+ if (!s.diagram) continue;
29011
+ const key = teamKey(s);
29012
+ if (designs.has(key)) continue;
29013
+ const path = prefix ? `${prefix}.${s.id}` : s.id;
29014
+ designs.set(key, { diagram: s.diagram, path, loc: s.loc });
29015
+ visit(s.diagram, path);
29016
+ order.push(key);
29017
+ }
29018
+ };
29019
+ visit(root, "");
29020
+ const send = (d, prefix, inbound) => {
29021
+ for (const s of d.systems ?? []) {
29022
+ if (!s.diagram) continue;
29023
+ const key = teamKey(s);
29024
+ const load = byFile.get(key) ?? { useCases: /* @__PURE__ */ new Map(), callers: [] };
29025
+ byFile.set(key, load);
29026
+ const rps = sentByUseCase(s, inbound.get(s.id) ?? /* @__PURE__ */ new Map(), load.useCases);
29027
+ if (rps > 0) load.callers.push({ path: prefix ? `${prefix}.${s.id}` : s.id, ...d.title !== void 0 ? { from: d.title } : {}, rps });
29028
+ }
29029
+ };
29030
+ send(root, "", rootInbound);
29031
+ for (const key of order.reverse()) {
29032
+ const { diagram, path, loc } = designs.get(key);
29033
+ if (!diagram.systems?.length) continue;
29034
+ const combined = withExtraTraffic(diagram, byFile.get(key)?.useCases ?? /* @__PURE__ */ new Map(), loc);
29035
+ send(diagram, path, inboundLoad(combined, resolveTraffic(combined)));
29036
+ }
29037
+ return { byFile, analyses: /* @__PURE__ */ new Map() };
29038
+ }
29039
+ function analyzeSystem(system, sent, options) {
29040
+ const contract = contractOf(system, options);
29041
+ const inbound = system.entries.map((e) => ({ entry: e.id, rps: sent.get(e.id) ?? 0 }));
29042
+ const totalInboundRps = inbound.reduce((sum, e) => sum + e.rps, 0);
29043
+ const result = { id: system.id, contract, inbound, totalInboundRps, utilization: 0, saturated: false, hot: false, breaches: [], entries: [], warnings: [] };
29044
+ const shared = system.diagram ? options.sharedSystems?.byFile.get(teamKey(system)) : void 0;
29045
+ const sending = inbound.filter((e) => e.rps > 0).map((e) => {
29046
+ const c = contract.entries.find((x) => x.entry === e.entry);
29047
+ const useCaseId = system.entries.find((x) => x.id === e.entry)?.useCaseId;
29048
+ const gets = useCaseId !== void 0 ? shared?.useCases.get(useCaseId) ?? 0 : 0;
29049
+ const rps = gets > e.rps * (1 + 1e-9) ? `${formatRps(e.rps).replace(/ rps$/, "")} of the ${formatRps(gets)} it gets` : formatRps(e.rps);
29050
+ return `${rps} into ${c?.useCase ? `"${c.useCase}" (${system.id}.${e.entry})` : `${system.id}.${e.entry}`}`;
29051
+ }).join(" and ");
29052
+ const label = `'${system.id}'`;
29053
+ if (contract.source === "stub") {
29054
+ const stub = system.stub;
29055
+ const rps = stub.rps ?? Infinity;
29056
+ result.utilization = Number.isFinite(rps) && rps > 0 ? totalInboundRps / rps : 0;
29057
+ result.saturated = result.utilization >= 1;
29058
+ result.hot = !result.saturated && result.utilization > HOT;
29059
+ const base = stub.latencyMs ?? (stub.p99Ms !== void 0 ? stub.p99Ms / (1 - SERVICE_SPREAD + SERVICE_SPREAD * tailFactor(0.99)) : 0);
29060
+ const hop = hopModel(base, 0);
29061
+ const at = (p) => hop.fixedMs + hop.tailMs * tailFactor(p);
29062
+ const availability2 = stub.availability !== void 0 ? stub.availability / 100 : 1;
29063
+ result.entries = system.entries.map((e) => ({ entry: e.id, p50Ms: at(0.5), p99Ms: at(0.99), availability: availability2 }));
29064
+ if (result.saturated) {
29065
+ result.warnings.push(`${label} cannot take what it is sent: you send ${sending}; it takes at most ${formatRps(rps)}.`);
29066
+ if (stub.p99Ms !== void 0) result.breaches.push({ requirement: `p99 < ${formatMs(stub.p99Ms)}`, message: `p99 is not held past ${formatRps(rps)}` });
29067
+ } 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)}.`);
29068
+ return result;
29069
+ }
29070
+ if (contract.source === "none" || !system.diagram) {
29071
+ result.entries = system.entries.map((e) => ({ entry: e.id, p50Ms: 0, p99Ms: 0, availability: 1 }));
29072
+ return result;
29073
+ }
29074
+ const team = system.diagram;
29075
+ const key = teamKey(system);
29076
+ const sentToTeam = shared?.useCases ?? /* @__PURE__ */ new Map();
29077
+ if (!shared) sentByUseCase(system, sent, sentToTeam);
29078
+ const sharedInboundRps = [...sentToTeam.values()].reduce((sum, rps) => sum + rps, 0);
29079
+ result.callers = shared?.callers ?? (totalInboundRps > 0 ? [{ path: system.id, rps: totalInboundRps }] : []);
29080
+ result.sharedInboundRps = sharedInboundRps;
29081
+ let cached = options.sharedSystems?.analyses.get(key);
29082
+ if (!cached) {
29083
+ const combined = sharedInboundRps > 0 ? withExtraTraffic(team, sentToTeam, system.loc) : team;
29084
+ const nested = { timeoutMs: options.timeoutMs, ...options.sharedSystems ? { sharedSystems: options.sharedSystems } : {} };
29085
+ cached = { combined, analysis: sharedInboundRps > 0 ? analyze(combined, nested) : contract.baseline };
29086
+ options.sharedSystems?.analyses.set(key, cached);
29087
+ }
29088
+ const { analysis } = cached;
29089
+ result.analysis = analysis;
29090
+ result.overlay = analysisOverlay(cached.combined, analysis);
29091
+ const busiest = analysis.nodes.reduce((best, n) => !best || n.utilization > best.utilization ? n : best, void 0);
29092
+ result.utilization = busiest?.utilization ?? 0;
29093
+ result.busiest = busiest?.id;
29094
+ result.saturated = !!busiest?.saturated;
29095
+ result.hot = !result.saturated && result.utilization > HOT;
29096
+ const byId = new Map(analysis.useCases.map((u) => [u.id, u]));
29097
+ result.entries = system.entries.map((e) => {
29098
+ const u = e.useCaseId !== void 0 ? byId.get(e.useCaseId) : void 0;
29099
+ return { entry: e.id, p50Ms: u?.percentiles.p50 ?? 0, p99Ms: u?.percentiles.p99 ?? 0, availability: u?.availability ?? 1 };
29100
+ });
29101
+ if (totalInboundRps === 0) return result;
29102
+ result.breaches = breachesOf(team, sentToTeam, analysis);
29103
+ const first = contract.entries.find((c) => inbound.some((e) => e.entry === c.entry && e.rps > 0));
29104
+ if (result.saturated && busiest) {
29105
+ const onOwn = first && first.saturatesAtRps === 0;
29106
+ const limit = first && Number.isFinite(first.saturatesAtRps) ? `; it takes at most ${formatRps(first.saturatesAtRps)} before its ${first.bottleneck} saturates` : "";
29107
+ result.warnings.push(
29108
+ 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)}.`
29109
+ );
29110
+ } else if (result.hot && busiest) {
29111
+ const limit = first && Number.isFinite(first.hotAtRps) ? `; it runs hot past ${formatRps(first.hotAtRps)}` : "";
29112
+ result.warnings.push(`${label} runs hot under what it is sent: you send ${sending}${limit}. Its ${busiest.id} is at ${usageText(busiest)}.`);
29113
+ }
29114
+ for (const b of result.breaches) result.warnings.push(`${label} breaks its SLA under what it is sent: ${b.message}.`);
29115
+ return result;
29116
+ }
29117
+ function withExtraTraffic(team, extra, loc) {
29118
+ const traffic = [...team.traffic ?? []];
29119
+ for (const [id, rps] of extra) {
29120
+ const u = team.useCases.find((x) => x.id === id);
29121
+ if (!u || rps <= 0) continue;
29122
+ const i = traffic.findIndex((t) => findUseCase(team, t.useCase) === u);
29123
+ if (i >= 0) traffic[i] = { ...traffic[i], rps: traffic[i].rps + rps };
29124
+ else traffic.push({ useCase: u.name, rps, loc });
29125
+ }
29126
+ return { ...team, traffic };
29127
+ }
29128
+ function breachesOf(team, sent, analysis) {
29129
+ const entered = /* @__PURE__ */ new Set();
29130
+ for (const [id, rps] of sent) {
29131
+ const u = team.useCases.find((x) => x.id === id);
29132
+ if (u && rps > 0) entered.add(u);
29133
+ }
29134
+ const nodes = new Map(analysis.nodes.map((n) => [n.id, n]));
29135
+ const results = new Map(analysis.useCases.map((u) => [u.id, u]));
29136
+ const breaches = [];
29137
+ for (const r of team.requirements ?? []) {
29138
+ if (r.kind !== "latency" && r.kind !== "availability") continue;
29139
+ const targets = r.useCase === void 0 ? [...entered] : [findUseCase(team, r.useCase)].filter((u) => !!u && entered.has(u));
29140
+ for (const u of targets) {
29141
+ const ua = results.get(u.id);
29142
+ const under = `under ${formatRps(ua.rps)}`;
29143
+ if (r.kind === "availability") {
29144
+ const name2 = `availability of "${u.name}" \u2265 ${r.minPercent}%`;
29145
+ 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}%)` });
29146
+ continue;
29147
+ }
29148
+ const key = PERCENTILE_KEYS[r.percentile];
29149
+ const scenario = r.scenario !== void 0 ? findScenario2(u, r.scenario) : void 0;
29150
+ if (r.scenario !== void 0 && !scenario) continue;
29151
+ const what = `${key} of "${u.name}"${scenario ? ` scenario "${scenario.name}"` : ""}`;
29152
+ const name = `${what} < ${formatMs(r.maxMs)}`;
29153
+ const steps = scenario ? scenario.steps : u.scenarios.flatMap((s) => s.steps);
29154
+ const saturated = steps.map((s) => nodes.get(s.toServiceId)).reduce((best, n) => n?.saturated && (!best || n.utilization > best.utilization) ? n : best, void 0);
29155
+ if (saturated) {
29156
+ breaches.push({ requirement: name, message: `${what}: ${saturated.id} is saturated (${usageText(saturated)}) ${under} (limit ${formatMs(r.maxMs)})` });
29157
+ continue;
29158
+ }
29159
+ const ms = scenario ? ua.scenarios[u.scenarios.indexOf(scenario)].percentiles[key] : ua.percentiles[key];
29160
+ if (ms >= r.maxMs) breaches.push({ requirement: name, message: `${what} is ${formatMs(ms)} ${under} (limit ${formatMs(r.maxMs)})` });
29161
+ }
29162
+ }
29163
+ return breaches;
29164
+ }
29165
+
28492
29166
  // ../frontend/src/sim/analyze.ts
28493
29167
  var DEFAULT_TIMEOUT_MS = 1e3;
28494
29168
  var HOT = 0.7;
@@ -28646,6 +29320,7 @@ function usageText(n) {
28646
29320
  return `bandwidth ${mb(n.bandwidthLoadMBps)} of ${mb(n.bandwidthCapacityMBps)}, ${formatPercent(n.bandwidthUtilization)}`;
28647
29321
  }
28648
29322
  if (n.writeScaling === "shards" && n.writeLoadRps > 0) {
29323
+ if (n.readLoadRps <= 0) return `writes ${part(n.writeLoadRps, n.writeCapacityRps, n.writeUtilization)}`;
28649
29324
  return `reads ${part(n.readLoadRps, n.readCapacityRps, n.readUtilization)}; writes ${part(n.writeLoadRps, n.writeCapacityRps, n.writeUtilization)}`;
28650
29325
  }
28651
29326
  return part(n.loadRps, n.capacityRps, n.requestUtilization);
@@ -28731,6 +29406,7 @@ function analyze(diagram, options = {}) {
28731
29406
  const load = computeLoad(diagram, traffic);
28732
29407
  const egress = computeEgressGb(diagram, traffic, kindOf2);
28733
29408
  const bytes = computeBytesPerSecond(diagram, traffic);
29409
+ const systems = diagram.systems?.length ? analyzeSystems(diagram, traffic, options) : void 0;
28734
29410
  const hops = /* @__PURE__ */ new Map();
28735
29411
  const nodeResults = nodes.map((n) => {
28736
29412
  const p = profiles.get(n.id);
@@ -28769,12 +29445,13 @@ function analyze(diagram, options = {}) {
28769
29445
  if (n.saturated) warnings.push(`'${n.id}' is saturated: ${usage}. ${writeBound(n) ? "Add shards for writes" : "Add replicas"} or take load off it.`);
28770
29446
  else if (n.utilization > HOT) warnings.push(`'${n.id}' runs hot: ${usage}.`);
28771
29447
  }
29448
+ for (const s of systems?.systems ?? []) warnings.push(...s.warnings);
28772
29449
  const access = accessIn(diagram);
28773
29450
  const pathOf2 = (hopsOfPath) => ({
28774
29451
  parts: hopsOfPath.map(
28775
29452
  (part) => part.map((h) => {
28776
29453
  if (h.failed) return { fixedMs: timeoutOf(h.target), tailMs: 0 };
28777
- const hop = hops.get(h.target) ?? { fixedMs: 0, tailMs: 0 };
29454
+ const hop = systems?.hop(h.target, h.step.toPort) ?? hops.get(h.target) ?? { fixedMs: 0, tailMs: 0 };
28778
29455
  return { fixedMs: transferMs(h.step, bandwidthOf) + hop.fixedMs, tailMs: hop.tailMs };
28779
29456
  })
28780
29457
  )
@@ -28804,6 +29481,7 @@ function analyze(diagram, options = {}) {
28804
29481
  const writes = mainWrites(u, shares, (step) => access(step) === "write");
28805
29482
  const availabilityOf = (id) => {
28806
29483
  const n = nodeById.get(id);
29484
+ if (!n && systems) return systemAvailability(u, id, systems.availability);
28807
29485
  return !n ? 1 : writes.has(id) ? n.writeAvailability : n.availability;
28808
29486
  };
28809
29487
  return { id: u.id, name: u.name, rps, scenarios, percentiles, tailScenario, availability: useCaseAvailability(u, shares, availabilityOf) };
@@ -28814,9 +29492,15 @@ function analyze(diagram, options = {}) {
28814
29492
  totalCostUsd: nodeResults.reduce((sum, n) => sum + n.costUsd, 0),
28815
29493
  totalEgressUsd: nodeResults.reduce((sum, n) => sum + n.egressUsd, 0),
28816
29494
  singlePointsOfFailure: singlePointsOfFailure(diagram),
28817
- warnings
29495
+ warnings,
29496
+ ...systems ? { systems: systems.systems } : {}
28818
29497
  };
28819
29498
  }
29499
+ function systemAvailability(u, id, availability2) {
29500
+ let result = 1;
29501
+ 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);
29502
+ return result;
29503
+ }
28820
29504
  function mainWrites(useCase, shares, isWrite) {
28821
29505
  if (useCase.scenarios.length === 0) return /* @__PURE__ */ new Set();
28822
29506
  const path = criticalPath(useCase, useCase.scenarios[mainScenarioIndex(shares)]).flat();
@@ -28866,6 +29550,7 @@ function runTests(diagram, analysis = analyze(diagram), options = {}) {
28866
29550
  const { passed, message, hint } = requirement(run, r);
28867
29551
  return { id: `req:${i + 1}`, name: requirementName(r), category: CATEGORY[r.kind], passed, message, hint, loc: r.loc };
28868
29552
  });
29553
+ results.push(...systemResults(diagram, analysis));
28869
29554
  const names = /* @__PURE__ */ new Map();
28870
29555
  for (const test of diagram.tests ?? []) {
28871
29556
  const n = (names.get(test.name) ?? 0) + 1;
@@ -28874,6 +29559,26 @@ function runTests(diagram, analysis = analyze(diagram), options = {}) {
28874
29559
  }
28875
29560
  return results.map(({ hint, ...r }) => r.passed || !hint ? r : { ...r, hint });
28876
29561
  }
29562
+ function systemResults(diagram, analysis) {
29563
+ return (analysis.systems ?? []).filter((s) => s.totalInboundRps > 0 && s.contract.source !== "none").map((s) => {
29564
+ const system = diagram.systems?.find((x) => x.id === s.id);
29565
+ const node = diagram.nodes.find((n) => n.id === s.id);
29566
+ const team = node?.ownerTeam ? `the ${node.ownerTeam} team` : `the owners of ${s.id}`;
29567
+ const problem = s.warnings.find((w) => !w.includes(" runs hot "));
29568
+ const sat = s.contract.entries.find((e) => s.inbound.some((i) => i.entry === e.entry && i.rps > 0));
29569
+ const room = sat && Number.isFinite(sat.saturatesAtRps) ? `; it takes at most ${formatRps(sat.saturatesAtRps)}` : "";
29570
+ const bottleneck = s.saturated && s.busiest ? ` (their bottleneck is ${s.busiest})` : "";
29571
+ return {
29572
+ id: `system:${s.id}`,
29573
+ name: `${s.id} takes what it is sent`,
29574
+ category: s.breaches.some((b) => b.requirement.startsWith("availability")) && !s.saturated ? "availability" : "latency",
29575
+ passed: !problem,
29576
+ 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}`,
29577
+ ...problem ? { hint: `Send it less, or ask ${team} to add capacity for it${bottleneck}` } : {},
29578
+ ...system ? { loc: system.loc } : {}
29579
+ };
29580
+ });
29581
+ }
28877
29582
  var CATEGORY = {
28878
29583
  latency: "latency",
28879
29584
  availability: "availability",
@@ -29412,7 +30117,7 @@ var KEYWORDS2 = [
29412
30117
  { label: "par", detail: "Steps that run in parallel", snippet: "par {\n $0\n}" },
29413
30118
  { label: "alt", detail: "One scenario of a use case", snippet: 'alt "${1:Scenario}" {\n $0\n}' },
29414
30119
  { label: "legend", detail: 'Styles that .tag applies: owned "Owned by us" green', snippet: 'legend {\n ${1:tag} "${2:What it means}" ${3:blue}$0\n}' },
29415
- // Layout hints (docs/LANGUAGE.md, "Steering the layout").
30120
+ // Layout hints (docs/STYLING.md, "Steering the layout").
29416
30121
  { 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
30122
  { label: "layout ordered", detail: "Lay edges out by declaration order (top level)", snippet: "layout ordered" },
29418
30123
  // High-level design sections (top level).
@@ -29443,14 +30148,282 @@ var ASSERTIONS = [
29443
30148
  ];
29444
30149
 
29445
30150
  // src/imports.ts
30151
+ var import_node_fs4 = require("node:fs");
30152
+ var import_node_path5 = require("node:path");
30153
+
30154
+ // ../frontend/src/dsl/imports.ts
30155
+ var ImportError = class extends Error {
30156
+ name = "ImportError";
30157
+ };
30158
+ var MAX_SPEC_LENGTH = 1024;
30159
+ var SEGMENT = /^[A-Za-z0-9_][A-Za-z0-9._ +-]*$/;
30160
+ 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})?$/;
30161
+ var REPO_SEGMENT = /^[A-Za-z0-9_][A-Za-z0-9._-]*$/;
30162
+ var REF = /^[A-Za-z0-9_][A-Za-z0-9._/-]*$/;
30163
+ var SCHEME = /^([A-Za-z][A-Za-z0-9+.-]+):/;
30164
+ function isRemoteSpec(spec) {
30165
+ return SCHEME.test(spec);
30166
+ }
30167
+ function isAbsolutePath(path) {
30168
+ return /^[\\/]/.test(path) || /^[A-Za-z]:/.test(path);
30169
+ }
30170
+ function checkRef(ref, spec) {
30171
+ if (!REF.test(ref) || ref.length > 200 || ref.includes("..") || ref.includes("//") || ref.endsWith("/") || ref.endsWith(".") || ref.endsWith(".lock") || ref.split("/").some((p) => p.startsWith("."))) {
30172
+ throw new ImportError(`'${spec}' has an invalid ref '${ref}'; use a tag, branch or commit made of letters, digits, '.', '_', '-' and '/'`);
30173
+ }
30174
+ }
30175
+ function repoPathSegments(path, spec) {
30176
+ const parts = path.split("/");
30177
+ if (!path || parts.some((p) => !SEGMENT.test(p) || p === "." || p === "..")) {
30178
+ throw new ImportError(`'${spec}' has an invalid path in the repository; use a relative path like dir/file.proschi, without '..'`);
30179
+ }
30180
+ return parts;
30181
+ }
30182
+ function parseRemoteSpec(spec) {
30183
+ const scheme = SCHEME.exec(spec)?.[1].toLowerCase();
30184
+ if (!scheme) return void 0;
30185
+ if (spec.length > MAX_SPEC_LENGTH) throw new ImportError(`Import source is longer than ${MAX_SPEC_LENGTH} characters`);
30186
+ if (/[\s\0\\]/.test(spec)) throw new ImportError(`'${spec}' contains whitespace or a backslash`);
30187
+ if (scheme === "git") {
30188
+ const body = spec.slice(4);
30189
+ const at = body.lastIndexOf("@");
30190
+ const split2 = body.indexOf("//");
30191
+ if (at === -1 || split2 === -1 || split2 > at) {
30192
+ throw new ImportError(`'${spec}' is not a git source; write git:<host>/<org>/<repo>//<path/in/repo>@<ref>`);
30193
+ }
30194
+ const repo = body.slice(0, split2);
30195
+ const path = body.slice(split2 + 2, at);
30196
+ const ref = body.slice(at + 1);
30197
+ const [host, ...names] = repo.split("/");
30198
+ if (!HOST.test(host) || names.length < 2 || names.some((n) => !REPO_SEGMENT.test(n) || n.endsWith("."))) {
30199
+ throw new ImportError(`'${spec}' names an invalid repository '${repo}'; write <host>/<org>/<repo>`);
30200
+ }
30201
+ repoPathSegments(path, spec);
30202
+ checkRef(ref, spec);
30203
+ return { kind: "git", repo, path, ref };
30204
+ }
30205
+ if (scheme === "http") throw new ImportError(`'${spec}' uses http://; only https:// imports are allowed`);
30206
+ if (scheme !== "https") throw new ImportError(`'${spec}' is not a supported import source; use a relative path, git: or https://`);
30207
+ let url;
30208
+ try {
30209
+ url = new URL(spec);
30210
+ } catch {
30211
+ throw new ImportError(`'${spec}' is not a valid URL`);
30212
+ }
30213
+ if (url.username || url.password) throw new ImportError(`'${spec}' contains credentials; git and https imports use your own credentials, never ones in the diagram`);
30214
+ if (url.hash) throw new ImportError(`'${spec}' has a #fragment; remove it`);
30215
+ if (!HOST.test(url.host)) throw new ImportError(`'${spec}' has an invalid host`);
30216
+ return { kind: "https", url: url.href };
30217
+ }
30218
+ function formatRemoteSpec(spec) {
30219
+ return spec.kind === "git" ? `git:${spec.repo}//${spec.path}@${spec.ref}` : spec.url;
30220
+ }
30221
+ function resolveImportPath(fromPath, importPath) {
30222
+ if (importPath.includes("\0")) throw new ImportError(`'${importPath.replace(/\0/g, "")}' contains a NUL character`);
30223
+ const remote = parseRemoteSpec(importPath);
30224
+ if (remote) return formatRemoteSpec(remote);
30225
+ if (isAbsolutePath(importPath)) throw new ImportError(`'${importPath}' is an absolute path; imports must be relative to the importing file`);
30226
+ const from = fromPath && isRemoteSpec(fromPath) ? parseRemoteSpec(fromPath) : void 0;
30227
+ if (from?.kind === "git") {
30228
+ const parts2 = from.path.split("/").slice(0, -1);
30229
+ for (const part of importPath.split("/")) {
30230
+ if (part === "" || part === ".") continue;
30231
+ if (part !== "..") parts2.push(part);
30232
+ else if (parts2.length > 0) parts2.pop();
30233
+ else throw new ImportError(`'${importPath}' leaves the repository ${from.repo}`);
30234
+ }
30235
+ repoPathSegments(parts2.join("/"), importPath);
30236
+ return formatRemoteSpec({ ...from, path: parts2.join("/") });
30237
+ }
30238
+ if (from?.kind === "https") {
30239
+ const base = new URL(from.url);
30240
+ const url = new URL(importPath, base);
30241
+ if (url.origin !== base.origin) throw new ImportError(`'${importPath}' leaves ${base.origin}`);
30242
+ return formatRemoteSpec(parseRemoteSpec(url.href));
30243
+ }
30244
+ const parts = !fromPath ? [] : fromPath.split("/").slice(0, -1);
30245
+ for (const part of importPath.split("/")) {
30246
+ if (part === "" || part === ".") continue;
30247
+ if (part === ".." && parts.length > 0 && parts[parts.length - 1] !== "..") parts.pop();
30248
+ else parts.push(part);
30249
+ }
30250
+ return parts.join("/");
30251
+ }
30252
+
30253
+ // src/lock.ts
30254
+ var import_node_crypto = require("node:crypto");
29446
30255
  var import_node_fs3 = require("node:fs");
29447
30256
  var import_node_path4 = require("node:path");
29448
- function fileResolver(read = () => void 0, toPath = (p) => p) {
29449
- return (importPath, fromPath) => {
29450
- const abs = (0, import_node_path4.resolve)(fromPath ? (0, import_node_path4.dirname)(fromPath) : process.cwd(), importPath);
29451
- const text = read(abs) ?? ((0, import_node_fs3.existsSync)(abs) && (0, import_node_fs3.statSync)(abs).isFile() ? (0, import_node_fs3.readFileSync)(abs, "utf8") : void 0);
29452
- return text === void 0 ? void 0 : { path: toPath(abs), source: text };
30257
+
30258
+ // src/lockFormat.ts
30259
+ var LOCK_FILE = "proschi.lock";
30260
+ var CACHE_DIR = ".proschi/cache";
30261
+ var HASH = /^[0-9a-f]{64}$/;
30262
+ var COMMIT = /^[0-9a-f]{40}([0-9a-f]{24})?$/;
30263
+ var emptyLock = () => ({ version: 1, git: {}, https: {} });
30264
+ var gitKey = (repo, ref) => `${repo}@${ref}`;
30265
+ var remoteError = {
30266
+ noLock: (spec) => new ImportError(`'${spec}' is a remote import and there is no ${LOCK_FILE}; run proschi imports sync`),
30267
+ notLocked: (spec) => new ImportError(`'${spec}' is not in ${LOCK_FILE}; run proschi imports sync`),
30268
+ notCached: (spec) => new ImportError(`'${spec}' is not in the import cache (${CACHE_DIR}); run proschi imports sync`),
30269
+ altered: (spec) => new ImportError(`The cached copy of '${spec}' does not match its hash in ${LOCK_FILE}; run proschi imports sync`)
30270
+ };
30271
+ var isRecord = (v) => !!v && typeof v === "object" && !Array.isArray(v);
30272
+ function parseLock(text) {
30273
+ const invalid = (why) => new ImportError(`Invalid ${LOCK_FILE}: ${why}; delete it and run proschi imports sync`);
30274
+ let json;
30275
+ try {
30276
+ json = JSON.parse(text);
30277
+ } catch (e) {
30278
+ throw invalid(e.message);
30279
+ }
30280
+ if (!isRecord(json) || json.version !== 1) throw invalid('expected {"version": 1, \u2026}');
30281
+ const lock = emptyLock();
30282
+ const git = json.git ?? {};
30283
+ const https = json.https ?? {};
30284
+ if (!isRecord(git) || !isRecord(https)) throw invalid('"git" and "https" must be objects');
30285
+ for (const [key, entry] of Object.entries(git)) {
30286
+ const at = key.lastIndexOf("@");
30287
+ 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)}`));
30288
+ if (!valid) throw invalid(`bad git entry '${key}'`);
30289
+ lock.git[key] = { commit: entry.commit, files: { ...entry.files } };
30290
+ }
30291
+ for (const [url, hash] of Object.entries(https)) {
30292
+ if (typeof hash !== "string" || !HASH.test(hash) || parseRemoteSpec(url)?.kind !== "https") throw invalid(`bad https entry '${url}'`);
30293
+ lock.https[url] = hash;
30294
+ }
30295
+ return lock;
30296
+ }
30297
+ function isGitSpec(spec) {
30298
+ try {
30299
+ return parseRemoteSpec(spec)?.kind === "git";
30300
+ } catch {
30301
+ return false;
30302
+ }
30303
+ }
30304
+ function lockedHash(lock, spec) {
30305
+ const remote = parseRemoteSpec(spec);
30306
+ if (remote?.kind === "git") return lock.git[gitKey(remote.repo, remote.ref)]?.files[remote.path];
30307
+ if (remote?.kind === "https") return lock.https[remote.url];
30308
+ return void 0;
30309
+ }
30310
+
30311
+ // src/lock.ts
30312
+ var sha256 = (bytes) => (0, import_node_crypto.createHash)("sha256").update(bytes).digest("hex");
30313
+ function readLock(root) {
30314
+ const file = (0, import_node_path4.join)(root, LOCK_FILE);
30315
+ return (0, import_node_fs3.existsSync)(file) ? parseLock((0, import_node_fs3.readFileSync)(file, "utf8")) : void 0;
30316
+ }
30317
+ var cacheFile = (root, hash) => (0, import_node_path4.join)(root, CACHE_DIR, hash);
30318
+ function readCached(root, hash, spec) {
30319
+ if (!HASH.test(hash)) return void 0;
30320
+ const file = cacheFile(root, hash);
30321
+ if (!(0, import_node_fs3.existsSync)(file)) return void 0;
30322
+ const bytes = (0, import_node_fs3.readFileSync)(file);
30323
+ if (sha256(bytes) !== hash) throw remoteError.altered(spec);
30324
+ return bytes;
30325
+ }
30326
+
30327
+ // src/imports.ts
30328
+ function displayPath2(file, cwd = process.cwd()) {
30329
+ const abs = (0, import_node_path5.resolve)(cwd, file);
30330
+ const rel = (0, import_node_path5.relative)(cwd, abs);
30331
+ return rel && !rel.startsWith("..") && !(0, import_node_path5.isAbsolute)(rel) ? rel : abs;
30332
+ }
30333
+ var MAX_IMPORT_BYTES = 1e6;
30334
+ var MAX_IMPORT_FILES = 500;
30335
+ var MAX_IMPORT_DEPTH = 32;
30336
+ function projectRoot(entry) {
30337
+ const dir = (0, import_node_path5.dirname)((0, import_node_path5.resolve)(entry));
30338
+ const config = findConfigFile(dir);
30339
+ if (config) return (0, import_node_fs4.realpathSync)((0, import_node_path5.dirname)(config));
30340
+ for (let current = dir; ; current = (0, import_node_path5.dirname)(current)) {
30341
+ if ((0, import_node_fs4.existsSync)((0, import_node_path5.join)(current, ".git"))) return (0, import_node_fs4.realpathSync)(current);
30342
+ if ((0, import_node_path5.dirname)(current) === current) break;
30343
+ }
30344
+ return (0, import_node_fs4.realpathSync)((0, import_node_fs4.existsSync)(dir) ? dir : process.cwd());
30345
+ }
30346
+ var isInside = (root, path) => {
30347
+ const rel = (0, import_node_path5.relative)(root, path);
30348
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${import_node_path5.sep}`) && !(0, import_node_path5.isAbsolute)(rel);
30349
+ };
30350
+ function realLocation(path) {
30351
+ let existing = path;
30352
+ while (!(0, import_node_fs4.existsSync)(existing) && (0, import_node_path5.dirname)(existing) !== existing) existing = (0, import_node_path5.dirname)(existing);
30353
+ return (0, import_node_path5.join)((0, import_node_fs4.realpathSync)(existing), (0, import_node_path5.relative)(existing, path));
30354
+ }
30355
+ var tooLarge = (what) => new ImportError(`'${what}' is larger than ${MAX_IMPORT_BYTES / 1e6} MB`);
30356
+ function fileResolver({ entry, read = () => void 0, toPath = (p) => p, remote } = {}) {
30357
+ const root = projectRoot(entry ?? (0, import_node_path5.join)(process.cwd(), "stdin.proschi"));
30358
+ const errors = /* @__PURE__ */ new Map();
30359
+ const levels = /* @__PURE__ */ new Map();
30360
+ let lock = null;
30361
+ const fromCache = (spec) => {
30362
+ if (lock === null) lock = readLock(root);
30363
+ if (!lock) throw remoteError.noLock(spec);
30364
+ const hash = lockedHash(lock, spec);
30365
+ if (!hash) throw remoteError.notLocked(spec);
30366
+ const bytes = readCached(root, hash, spec);
30367
+ if (!bytes) throw remoteError.notCached(spec);
30368
+ return bytes.toString("utf8");
30369
+ };
30370
+ const loadRemote = remote ?? fromCache;
30371
+ const found = (path, source2, level) => {
30372
+ if (!levels.has(path)) {
30373
+ if (levels.size >= MAX_IMPORT_FILES) throw new ImportError(`More than ${MAX_IMPORT_FILES} imported files; split the diagram`);
30374
+ levels.set(path, level);
30375
+ }
30376
+ return { path, source: source2 };
30377
+ };
30378
+ const resolveOne = (importPath, fromPath) => {
30379
+ const level = (fromPath === void 0 ? 0 : levels.get(fromPath) ?? 0) + 1;
30380
+ if (level > MAX_IMPORT_DEPTH) throw new ImportError(`Imports nest more than ${MAX_IMPORT_DEPTH} files deep`);
30381
+ if (isRemoteSpec(importPath) || fromPath !== void 0 && isRemoteSpec(fromPath)) {
30382
+ const spec = resolveImportPath(fromPath, importPath);
30383
+ const source2 = loadRemote(spec);
30384
+ if (source2 === void 0) return void 0;
30385
+ if (Buffer.byteLength(source2) > MAX_IMPORT_BYTES) throw tooLarge(spec);
30386
+ return found(spec, source2, level);
30387
+ }
30388
+ resolveImportPath(void 0, importPath);
30389
+ const abs = (0, import_node_path5.resolve)(fromPath ? (0, import_node_path5.dirname)((0, import_node_path5.resolve)(fromPath)) : root, importPath);
30390
+ if (!isInside(root, realLocation(abs))) {
30391
+ const link = (0, import_node_fs4.existsSync)(abs) && (0, import_node_fs4.lstatSync)(abs).isSymbolicLink();
30392
+ throw new ImportError(
30393
+ 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)`
30394
+ );
30395
+ }
30396
+ const text = read(abs);
30397
+ if (text !== void 0) {
30398
+ if (Buffer.byteLength(text) > MAX_IMPORT_BYTES) throw tooLarge(importPath);
30399
+ return found(toPath(abs), text, level);
30400
+ }
30401
+ if (!(0, import_node_fs4.existsSync)(abs)) return void 0;
30402
+ const stat = (0, import_node_fs4.statSync)(abs);
30403
+ if (!stat.isFile()) return void 0;
30404
+ if (stat.size > MAX_IMPORT_BYTES) throw tooLarge(importPath);
30405
+ return found(toPath(abs), (0, import_node_fs4.readFileSync)((0, import_node_fs4.realpathSync)(abs), "utf8"), level);
30406
+ };
30407
+ const resolver = ((importPath, fromPath) => {
30408
+ try {
30409
+ return resolveOne(importPath, fromPath);
30410
+ } catch (e) {
30411
+ if (e instanceof ImportError) errors.set(`${fromPath ?? ""}\0${importPath}`, e.message);
30412
+ throw e;
30413
+ }
30414
+ });
30415
+ resolver.root = root;
30416
+ resolver.errors = errors;
30417
+ resolver.explain = (result) => {
30418
+ for (const i of result.imports ?? []) {
30419
+ if (i.resolved !== void 0) continue;
30420
+ const reason = errors.get(`${i.loc.file ?? entry ?? ""}\0${i.path}`);
30421
+ 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}'`);
30422
+ if (d) d.message = reason;
30423
+ }
30424
+ return result;
29453
30425
  };
30426
+ return resolver;
29454
30427
  }
29455
30428
  function reachableFrom(result, start) {
29456
30429
  const reached = /* @__PURE__ */ new Set([start]);
@@ -29472,12 +30445,12 @@ function ownDiagnostics(result) {
29472
30445
  if (i.loc.file !== void 0 || i.resolved === void 0) continue;
29473
30446
  const files = reachableFrom(result, i.resolved);
29474
30447
  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, import_node_path4.basename)(i.path)}' has ${errors} error${errors === 1 ? "" : "s"}`, ...i.loc });
30448
+ if (errors) own.push({ severity: "error", message: `'${(0, import_node_path5.basename)(i.path)}' has ${errors} error${errors === 1 ? "" : "s"}`, ...i.loc });
29476
30449
  }
29477
30450
  return own.sort((a, b) => a.line - b.line || a.col - b.col);
29478
30451
  }
29479
30452
  function importLinks(result) {
29480
- return (result.imports ?? []).filter((i) => i.loc.file === void 0 && i.resolved !== void 0).map((i) => {
30453
+ return (result.imports ?? []).filter((i) => i.loc.file === void 0 && i.resolved !== void 0 && !isRemoteSpec(i.resolved)).map((i) => {
29481
30454
  const range = toRange(i.loc);
29482
30455
  return {
29483
30456
  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 +30757,7 @@ function positionAt(text, offset) {
29784
30757
 
29785
30758
  // src/server.ts
29786
30759
  if (process.argv.includes("--version")) {
29787
- console.log(true ? "0.11.0" : "dev");
30760
+ console.log(true ? "0.13.0" : "dev");
29788
30761
  process.exit(0);
29789
30762
  }
29790
30763
  var connection = (0, import_node.createConnection)(import_node.ProposedFeatures.all);
@@ -29795,11 +30768,13 @@ var uriOf = (path) => (0, import_node_url.pathToFileURL)(path).href;
29795
30768
  function openText(path) {
29796
30769
  return documents.all().find((d) => pathOf(d.uri) === path)?.getText();
29797
30770
  }
29798
- var resolve3 = fileResolver(openText);
29799
30771
  function analysisOf(doc) {
29800
30772
  const cached = cache2.get(doc.uri);
29801
30773
  if (cached?.version === doc.version) return cached.analysis;
29802
- const analysis = analyze2(doc.getText(), { path: pathOf(doc.uri), resolve: resolve3 });
30774
+ const path = pathOf(doc.uri);
30775
+ const resolve3 = fileResolver({ entry: path, read: openText });
30776
+ const analysis = analyze2(doc.getText(), { path, resolve: resolve3 });
30777
+ resolve3.explain(analysis.result);
29803
30778
  const files = new Set((analysis.result.imports ?? []).flatMap((i) => i.resolved ? [i.resolved] : []));
29804
30779
  cache2.set(doc.uri, { version: doc.version, analysis, files });
29805
30780
  return analysis;
@@ -29828,7 +30803,7 @@ connection.onInitialize(() => ({
29828
30803
  codeActionProvider: { codeActionKinds: [import_node.CodeActionKind.QuickFix] },
29829
30804
  semanticTokensProvider: { legend: { tokenTypes: TOKEN_TYPES, tokenModifiers: [] }, full: true }
29830
30805
  },
29831
- serverInfo: { name: "proschi-language-server", version: true ? "0.11.0" : "dev" }
30806
+ serverInfo: { name: "proschi-language-server", version: true ? "0.13.0" : "dev" }
29832
30807
  }));
29833
30808
  function validate(document) {
29834
30809
  const analysis = analysisOf(document);
@@ -29872,7 +30847,7 @@ documents.onDidChangeContent(({ document }) => {
29872
30847
  function watchedStamp() {
29873
30848
  return watchedFiles().map((file) => {
29874
30849
  try {
29875
- return `${file}:${(0, import_node_fs4.statSync)(file).mtimeMs}`;
30850
+ return `${file}:${(0, import_node_fs5.statSync)(file).mtimeMs}`;
29876
30851
  } catch {
29877
30852
  return `${file}:missing`;
29878
30853
  }