proschi 0.12.0 → 0.14.0

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