proschi 0.4.0 → 0.6.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/cli.cjs CHANGED
@@ -2986,7 +2986,7 @@ var require_compile = __commonJS({
2986
2986
  const schOrFunc = root.refs[ref];
2987
2987
  if (schOrFunc)
2988
2988
  return schOrFunc;
2989
- let _sch = resolve3.call(this, root, ref);
2989
+ let _sch = resolve4.call(this, root, ref);
2990
2990
  if (_sch === void 0) {
2991
2991
  const schema = (_a = root.localRefs) === null || _a === void 0 ? void 0 : _a[ref];
2992
2992
  const { schemaId } = this.opts;
@@ -3013,7 +3013,7 @@ var require_compile = __commonJS({
3013
3013
  function sameSchemaEnv(s1, s2) {
3014
3014
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
3015
3015
  }
3016
- function resolve3(root, ref) {
3016
+ function resolve4(root, ref) {
3017
3017
  let sch;
3018
3018
  while (typeof (sch = this.refs[ref]) == "string")
3019
3019
  ref = sch;
@@ -3843,7 +3843,7 @@ var require_fast_uri = __commonJS({
3843
3843
  }
3844
3844
  return uri;
3845
3845
  }
3846
- function resolve3(baseURI, relativeURI, options) {
3846
+ function resolve4(baseURI, relativeURI, options) {
3847
3847
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3848
3848
  const {
3849
3849
  parsed: baseParsed,
@@ -4212,7 +4212,7 @@ var require_fast_uri = __commonJS({
4212
4212
  var fastUri = {
4213
4213
  SCHEMES,
4214
4214
  normalize,
4215
- resolve: resolve3,
4215
+ resolve: resolve4,
4216
4216
  resolveComponent,
4217
4217
  equal,
4218
4218
  serialize,
@@ -11869,13 +11869,13 @@ var require_resolve_block_scalar = __commonJS({
11869
11869
  "node_modules/yaml/dist/compose/resolve-block-scalar.js"(exports2) {
11870
11870
  "use strict";
11871
11871
  var Scalar = require_Scalar();
11872
- function resolveBlockScalar(ctx, scalar, onError) {
11873
- const start = scalar.offset;
11874
- const header = parseBlockScalarHeader(scalar, ctx.options.strict, onError);
11872
+ function resolveBlockScalar(ctx, scalar2, onError) {
11873
+ const start = scalar2.offset;
11874
+ const header = parseBlockScalarHeader(scalar2, ctx.options.strict, onError);
11875
11875
  if (!header)
11876
11876
  return { value: "", type: null, comment: "", range: [start, start, start] };
11877
11877
  const type = header.mode === ">" ? Scalar.Scalar.BLOCK_FOLDED : Scalar.Scalar.BLOCK_LITERAL;
11878
- const lines = scalar.source ? splitLines(scalar.source) : [];
11878
+ const lines = scalar2.source ? splitLines(scalar2.source) : [];
11879
11879
  let chompStart = lines.length;
11880
11880
  for (let i = lines.length - 1; i >= 0; --i) {
11881
11881
  const content = lines[i][1];
@@ -11887,12 +11887,12 @@ var require_resolve_block_scalar = __commonJS({
11887
11887
  if (chompStart === 0) {
11888
11888
  const value2 = header.chomp === "+" && lines.length > 0 ? "\n".repeat(Math.max(1, lines.length - 1)) : "";
11889
11889
  let end2 = start + header.length;
11890
- if (scalar.source)
11891
- end2 += scalar.source.length;
11890
+ if (scalar2.source)
11891
+ end2 += scalar2.source.length;
11892
11892
  return { value: value2, type, comment: header.comment, range: [start, end2, end2] };
11893
11893
  }
11894
- let trimIndent = scalar.indent + header.indent;
11895
- let offset = scalar.offset + header.length;
11894
+ let trimIndent = scalar2.indent + header.indent;
11895
+ let offset = scalar2.offset + header.length;
11896
11896
  let contentStart = 0;
11897
11897
  for (let i = 0; i < chompStart; ++i) {
11898
11898
  const [indent2, content] = lines[i];
@@ -11970,7 +11970,7 @@ var require_resolve_block_scalar = __commonJS({
11970
11970
  default:
11971
11971
  value += "\n";
11972
11972
  }
11973
- const end = start + header.length + scalar.source.length;
11973
+ const end = start + header.length + scalar2.source.length;
11974
11974
  return { value, type, comment: header.comment, range: [start, end, end] };
11975
11975
  }
11976
11976
  function parseBlockScalarHeader({ offset, props }, strict, onError) {
@@ -12053,8 +12053,8 @@ var require_resolve_flow_scalar = __commonJS({
12053
12053
  "use strict";
12054
12054
  var Scalar = require_Scalar();
12055
12055
  var resolveEnd = require_resolve_end();
12056
- function resolveFlowScalar(scalar, strict, onError) {
12057
- const { offset, type, source: source2, end } = scalar;
12056
+ function resolveFlowScalar(scalar2, strict, onError) {
12057
+ const { offset, type, source: source2, end } = scalar2;
12058
12058
  let _type;
12059
12059
  let value;
12060
12060
  const _onError = (rel, code, msg) => onError(offset + rel, code, msg);
@@ -12073,7 +12073,7 @@ var require_resolve_flow_scalar = __commonJS({
12073
12073
  break;
12074
12074
  /* istanbul ignore next should not happen */
12075
12075
  default:
12076
- onError(scalar, "UNEXPECTED_TOKEN", `Expected a flow scalar value, but found: ${type}`);
12076
+ onError(scalar2, "UNEXPECTED_TOKEN", `Expected a flow scalar value, but found: ${type}`);
12077
12077
  return {
12078
12078
  value: "",
12079
12079
  type: null,
@@ -12288,26 +12288,26 @@ var require_compose_scalar = __commonJS({
12288
12288
  tag = findScalarTagByTest(ctx, value, token, onError);
12289
12289
  else
12290
12290
  tag = ctx.schema[identity.SCALAR];
12291
- let scalar;
12291
+ let scalar2;
12292
12292
  try {
12293
12293
  const res = tag.resolve(value, (msg) => onError(tagToken ?? token, "TAG_RESOLVE_FAILED", msg), ctx.options);
12294
- scalar = identity.isScalar(res) ? res : new Scalar.Scalar(res);
12294
+ scalar2 = identity.isScalar(res) ? res : new Scalar.Scalar(res);
12295
12295
  } catch (error) {
12296
12296
  const msg = error instanceof Error ? error.message : String(error);
12297
12297
  onError(tagToken ?? token, "TAG_RESOLVE_FAILED", msg);
12298
- scalar = new Scalar.Scalar(value);
12298
+ scalar2 = new Scalar.Scalar(value);
12299
12299
  }
12300
- scalar.range = range;
12301
- scalar.source = value;
12300
+ scalar2.range = range;
12301
+ scalar2.source = value;
12302
12302
  if (type)
12303
- scalar.type = type;
12303
+ scalar2.type = type;
12304
12304
  if (tagName)
12305
- scalar.tag = tagName;
12305
+ scalar2.tag = tagName;
12306
12306
  if (tag.format)
12307
- scalar.format = tag.format;
12307
+ scalar2.format = tag.format;
12308
12308
  if (comment)
12309
- scalar.comment = comment;
12310
- return scalar;
12309
+ scalar2.comment = comment;
12310
+ return scalar2;
12311
12311
  }
12312
12312
  function findScalarTagByName(schema, value, tagName, tagToken, onError) {
12313
12313
  if (tagName === "!")
@@ -13464,9 +13464,9 @@ var require_lexer = __commonJS({
13464
13464
  }
13465
13465
  }
13466
13466
  *parseQuotedScalar() {
13467
- const quote2 = this.charAt(0);
13468
- let end = this.buffer.indexOf(quote2, this.pos + 1);
13469
- if (quote2 === "'") {
13467
+ const quote3 = this.charAt(0);
13468
+ let end = this.buffer.indexOf(quote3, this.pos + 1);
13469
+ if (quote3 === "'") {
13470
13470
  while (end !== -1 && this.buffer[end + 1] === "'")
13471
13471
  end = this.buffer.indexOf("'", end + 2);
13472
13472
  } else {
@@ -14135,37 +14135,37 @@ var require_parser = __commonJS({
14135
14135
  };
14136
14136
  }
14137
14137
  }
14138
- *scalar(scalar) {
14138
+ *scalar(scalar2) {
14139
14139
  if (this.type === "map-value-ind") {
14140
14140
  const prev = getPrevProps(this.peek(2));
14141
14141
  const start = getFirstKeyStartProps(prev);
14142
14142
  let sep;
14143
- if (scalar.end) {
14144
- sep = scalar.end;
14143
+ if (scalar2.end) {
14144
+ sep = scalar2.end;
14145
14145
  sep.push(this.sourceToken);
14146
- delete scalar.end;
14146
+ delete scalar2.end;
14147
14147
  } else
14148
14148
  sep = [this.sourceToken];
14149
14149
  const map = {
14150
14150
  type: "block-map",
14151
- offset: scalar.offset,
14152
- indent: scalar.indent,
14153
- items: [{ start, key: scalar, sep }]
14151
+ offset: scalar2.offset,
14152
+ indent: scalar2.indent,
14153
+ items: [{ start, key: scalar2, sep }]
14154
14154
  };
14155
14155
  this.onKeyLine = true;
14156
14156
  this.stack[this.stack.length - 1] = map;
14157
14157
  } else
14158
- yield* this.lineEnd(scalar);
14158
+ yield* this.lineEnd(scalar2);
14159
14159
  }
14160
- *blockScalar(scalar) {
14160
+ *blockScalar(scalar2) {
14161
14161
  switch (this.type) {
14162
14162
  case "space":
14163
14163
  case "comment":
14164
14164
  case "newline":
14165
- scalar.props.push(this.sourceToken);
14165
+ scalar2.props.push(this.sourceToken);
14166
14166
  return;
14167
14167
  case "scalar":
14168
- scalar.source = this.source;
14168
+ scalar2.source = this.source;
14169
14169
  this.atNewLine = true;
14170
14170
  this.indent = 0;
14171
14171
  if (this.onNewLine) {
@@ -14797,8 +14797,8 @@ __export(cli_exports, {
14797
14797
  run: () => run
14798
14798
  });
14799
14799
  module.exports = __toCommonJS(cli_exports);
14800
- var import_node_fs5 = require("node:fs");
14801
- var import_node_path6 = require("node:path");
14800
+ var import_node_fs6 = require("node:fs");
14801
+ var import_node_path7 = require("node:path");
14802
14802
 
14803
14803
  // src/openapi/config.ts
14804
14804
  var import_node_fs2 = require("node:fs");
@@ -15464,37 +15464,45 @@ function readString(text, start) {
15464
15464
  }
15465
15465
  function stripTrailingComment(text) {
15466
15466
  let depth = 0;
15467
- let quote2 = null;
15467
+ let quote3 = null;
15468
15468
  for (let i = 0; i < text.length; i++) {
15469
15469
  const ch = text[i];
15470
- if (quote2) {
15470
+ if (quote3) {
15471
15471
  if (ch === "\\") i++;
15472
- else if (ch === quote2) quote2 = null;
15472
+ else if (ch === quote3) quote3 = null;
15473
15473
  continue;
15474
15474
  }
15475
- if (ch === '"') quote2 = ch;
15475
+ if (ch === '"') quote3 = ch;
15476
15476
  else if (ch === "{" || ch === "[") depth++;
15477
15477
  else if (ch === "}" || ch === "]") depth = Math.max(0, depth - 1);
15478
15478
  else if (ch === "#" && depth === 0 && (i === 0 || /\s/.test(text[i - 1]))) return text.slice(0, i);
15479
15479
  }
15480
15480
  return text;
15481
15481
  }
15482
- function bracketDepth(text) {
15483
- let depth = 0;
15484
- let inString = false;
15485
- for (let i = 0; i < text.length; i++) {
15486
- const ch = text[i];
15487
- if (inString) {
15488
- if (ch === "\\") i++;
15489
- else if (ch === '"') inString = false;
15490
- continue;
15482
+ var BracketCounter = class {
15483
+ depth = 0;
15484
+ inString = false;
15485
+ escaped = false;
15486
+ /** Adds `text` and returns the depth so far. */
15487
+ feed(text) {
15488
+ for (let i = 0; i < text.length; i++) {
15489
+ const ch = text[i];
15490
+ if (this.escaped) {
15491
+ this.escaped = false;
15492
+ continue;
15493
+ }
15494
+ if (this.inString) {
15495
+ if (ch === "\\") this.escaped = true;
15496
+ else if (ch === '"') this.inString = false;
15497
+ continue;
15498
+ }
15499
+ if (ch === '"') this.inString = true;
15500
+ else if (ch === "{" || ch === "[") this.depth++;
15501
+ else if (ch === "}" || ch === "]") this.depth--;
15491
15502
  }
15492
- if (ch === '"') inString = true;
15493
- else if (ch === "{" || ch === "[") depth++;
15494
- else if (ch === "}" || ch === "]") depth--;
15503
+ return this.depth;
15495
15504
  }
15496
- return depth;
15497
- }
15505
+ };
15498
15506
 
15499
15507
  // ../frontend/src/dsl/paths.ts
15500
15508
  var PARAM2 = /^\{[^{}/]+\}$/;
@@ -15677,6 +15685,7 @@ var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
15677
15685
  var DEFAULT_TECH = "Rectangle";
15678
15686
  var DEFAULT_GROUP_TECH = "Logical Group";
15679
15687
  var MAX_SCENARIOS = 32;
15688
+ var MAX_ALT_DEPTH = 16;
15680
15689
  var REPLICAS = /^x(\d+)$/;
15681
15690
  var PERCENTILES = { p50: 50, p90: 90, p95: 95, p99: 99, p999: 99.9 };
15682
15691
  var FIELD_FLAGS = ["key", "index", "unique", "optional"];
@@ -15704,8 +15713,12 @@ var Parser = class {
15704
15713
  file;
15705
15714
  stack = [];
15706
15715
  diagnostics = [];
15716
+ /** Warnings reported so far (see `warning`). */
15717
+ warned = /* @__PURE__ */ new Set();
15707
15718
  nodes = /* @__PURE__ */ new Map();
15708
15719
  edges = [];
15720
+ /** Connections so far per `from->to`, for the ids of repeated ones. */
15721
+ edgeCount = /* @__PURE__ */ new Map();
15709
15722
  useCases = [];
15710
15723
  references = [];
15711
15724
  imports = [];
@@ -15782,6 +15795,17 @@ var Parser = class {
15782
15795
  const loc = (t) => this.locOf(t, line);
15783
15796
  const top = this.top();
15784
15797
  if (top?.kind === "section") {
15798
+ if (top.skipped !== void 0) {
15799
+ const opens = tokens[tokens.length - 1].kind === "lbrace";
15800
+ if (first.kind !== "rbrace") top.nested = (top.nested ?? 0) + (opens ? 1 : 0);
15801
+ else if (top.nested) top.nested += opens ? 0 : -1;
15802
+ else {
15803
+ this.stack.pop();
15804
+ if (second?.kind === "ident" && second.value === "alt") this.parseAlt(tokens.slice(1), line);
15805
+ else if (second) this.error("Unexpected input after }", loc(second));
15806
+ }
15807
+ return index;
15808
+ }
15785
15809
  if (first.kind === "rbrace") {
15786
15810
  this.stack.pop();
15787
15811
  if (!top.discard) this.closeSection(top);
@@ -15878,14 +15902,14 @@ var Parser = class {
15878
15902
  const loc = this.locOf(pathToken, line);
15879
15903
  const entry = { path, loc };
15880
15904
  this.imports.push(entry);
15881
- const { resolve: resolve3 } = this.options;
15882
- if (!resolve3) {
15905
+ const { resolve: resolve4 } = this.options;
15906
+ if (!resolve4) {
15883
15907
  this.warning(`Imports are not resolved in this context; '${path}' was not loaded`, loc);
15884
15908
  return;
15885
15909
  }
15886
15910
  let target;
15887
15911
  try {
15888
- target = resolve3(path, this.file ?? this.options.path);
15912
+ target = resolve4(path, this.file ?? this.options.path);
15889
15913
  } catch {
15890
15914
  target = void 0;
15891
15915
  }
@@ -16006,6 +16030,12 @@ var Parser = class {
16006
16030
  return;
16007
16031
  }
16008
16032
  this.expectEnd(tokens, i + 1, line);
16033
+ if (this.stack.filter((f) => f.kind === "alt").length >= MAX_ALT_DEPTH) {
16034
+ const loc2 = this.locOf(keyword, line);
16035
+ this.error(`alt blocks can nest at most ${MAX_ALT_DEPTH} deep; this one is skipped`, loc2);
16036
+ this.stack.push({ kind: "section", section: "test", discard: true, skipped: `alt '${nameToken.value}'`, loc: loc2 });
16037
+ return;
16038
+ }
16009
16039
  const owner = top.body;
16010
16040
  let set = owner.openAlt;
16011
16041
  if (!set) {
@@ -16099,12 +16129,16 @@ var Parser = class {
16099
16129
  }
16100
16130
  let label = labelToken?.value ?? "";
16101
16131
  let lastLine = index;
16102
- if (bracketDepth(label) > 0) {
16103
- while (bracketDepth(label) > 0 && lastLine + 1 < this.lines.length) {
16132
+ const brackets = new BracketCounter();
16133
+ let depth = brackets.feed(label);
16134
+ if (depth > 0) {
16135
+ while (depth > 0 && lastLine + 1 < this.lines.length) {
16104
16136
  lastLine++;
16105
- label += "\n" + this.lines[lastLine];
16137
+ const more = "\n" + this.lines[lastLine];
16138
+ label += more;
16139
+ depth = brackets.feed(more);
16106
16140
  }
16107
- if (bracketDepth(label) > 0) this.error("Unclosed { or [ in payload", this.locOf(labelToken, line));
16141
+ if (depth > 0) this.error("Unclosed { or [ in payload", this.locOf(labelToken, line));
16108
16142
  label = label.trimEnd();
16109
16143
  }
16110
16144
  const loc = {
@@ -16139,7 +16173,8 @@ var Parser = class {
16139
16173
  this.error("'-x' marks a failed call and is only allowed in use case steps", this.locOf(arrowToken, line));
16140
16174
  } else {
16141
16175
  const base = `${from}->${to}`;
16142
- const count = this.edges.filter((e) => e.id === base || e.id.startsWith(`${base}#`)).length;
16176
+ const count = this.edgeCount.get(base) ?? 0;
16177
+ this.edgeCount.set(base, count + 1);
16143
16178
  this.edges.push({ id: count ? `${base}#${count + 1}` : base, source: from, target: to, label: unquote(label) || void 0, loc });
16144
16179
  }
16145
16180
  return lastLine;
@@ -17065,7 +17100,12 @@ var Parser = class {
17065
17100
  this.diagnostics.push({ severity: "error", message, ...loc });
17066
17101
  }
17067
17102
  warning(message, loc) {
17068
- if (this.diagnostics.some((d) => d.message === message && d.line === loc.line && d.col === loc.col && d.file === loc.file)) return;
17103
+ const key = `${loc.file ?? ""}
17104
+ ${loc.line}
17105
+ ${loc.col}
17106
+ ${message}`;
17107
+ if (this.warned.has(key)) return;
17108
+ this.warned.add(key);
17069
17109
  this.diagnostics.push({ severity: "warning", message, ...loc });
17070
17110
  }
17071
17111
  };
@@ -17098,6 +17138,7 @@ function isWord(token, word) {
17098
17138
  return token?.kind === "ident" && token.value === word;
17099
17139
  }
17100
17140
  function sectionName(frame) {
17141
+ if (frame.skipped) return frame.skipped;
17101
17142
  if (frame.entity) return `entity '${frame.entity.name}'`;
17102
17143
  if (frame.decision) return `decision '${frame.decision.title}'`;
17103
17144
  if (frame.test) return `test '${frame.test.name}'`;
@@ -17238,13 +17279,13 @@ function classify(lines) {
17238
17279
  }
17239
17280
  if (!inSection && first.kind === "ident" && second?.kind === "arrow" && third?.kind === "ident" && tokens.length <= 4 && (!fourth || fourth.kind === "label")) {
17240
17281
  const entry = { ...base, kind: "connection", from: first.value, arrow: second.value, to: third.value, label: fourth?.value, continuation: [] };
17241
- if (fourth && bracketDepth(fourth.value) > 0) {
17242
- const delta = depth * INDENT.length - width(leadingSpace(raw));
17243
- let label = fourth.value;
17244
- while (bracketDepth(label) > 0 && i + 1 < lines.length) {
17282
+ const brackets = new BracketCounter();
17283
+ if (fourth && brackets.feed(fourth.value) > 0) {
17284
+ const delta = indent(depth).length - width(leadingSpace(raw));
17285
+ while (i + 1 < lines.length) {
17245
17286
  const next = lines[++i];
17246
- label += "\n" + next;
17247
17287
  entry.continuation.push(reindent(next, delta));
17288
+ if (brackets.feed("\n" + next) <= 0) break;
17248
17289
  }
17249
17290
  while (entry.continuation[entry.continuation.length - 1] === "") entry.continuation.pop();
17250
17291
  }
@@ -17385,7 +17426,8 @@ var source = (raw, t) => {
17385
17426
  var withComment = (code, comment) => comment ? code + " " + comment : code;
17386
17427
  var leadingSpace = (line) => /^[ \t]*/.exec(line)[0];
17387
17428
  var width = (space) => [...space].reduce((n, ch) => n + (ch === " " ? INDENT.length : 1), 0);
17388
- var indent = (depth) => INDENT.repeat(depth);
17429
+ var MAX_INDENT = 32;
17430
+ var indent = (depth) => INDENT.repeat(Math.min(depth, MAX_INDENT));
17389
17431
  var len = (s) => [...s].length;
17390
17432
  var pad = (s, to) => s + " ".repeat(Math.max(0, to - len(s)));
17391
17433
 
@@ -18510,6 +18552,9 @@ function noPath(run2, from, to) {
18510
18552
  return pass(`No connection or step goes directly from ${x} to ${y}`);
18511
18553
  }
18512
18554
 
18555
+ // ../frontend/src/hld/engine.ts
18556
+ var defaultEngine = { available: true, analyze: (d) => analyze(d), runTests: (d, a) => runTests(d, a) };
18557
+
18513
18558
  // src/fmt.ts
18514
18559
  var FMT_USAGE = ` proschi fmt [--check] <file|dir>...`;
18515
18560
  var FMT_HELP = `fmt Rewrites files in the canonical layout and lists the ones it changed.
@@ -18755,14 +18800,700 @@ function checkFiles(files, extra) {
18755
18800
  return [...byFile].map(([file, diagnostics]) => ({ file, diagnostics: diagnostics.sort((x, y) => x.line - y.line || x.col - y.col) }));
18756
18801
  }
18757
18802
 
18803
+ // src/problem.ts
18804
+ var import_node_fs5 = require("node:fs");
18805
+ var import_node_path6 = require("node:path");
18806
+
18807
+ // ../frontend/src/practice/frontMatter.ts
18808
+ var FrontMatterError = class extends Error {
18809
+ /** 1-based line in the file. */
18810
+ line;
18811
+ constructor(message, line) {
18812
+ super(`line ${line}: ${message}`);
18813
+ this.name = "FrontMatterError";
18814
+ this.line = line;
18815
+ }
18816
+ };
18817
+ var KEY = /^([A-Za-z][A-Za-z0-9_-]*):(?:[ \t]+(.*))?$/;
18818
+ var NUMBER = /^-?(?:0|[1-9]\d*)(?:\.\d+)?$/;
18819
+ var AMBIGUOUS = /^(?:true|false|yes|no|on|off|null|~)$/i;
18820
+ var INDICATORS = "-?:,[]{}#&*!|>'\"%@`";
18821
+ function isPlainSafe(s, inFlow = false) {
18822
+ if (s === "" || s !== s.trim() || /[\n\r\t]/.test(s)) return false;
18823
+ if (INDICATORS.includes(s[0]) || s.includes(": ") || s.includes(" #") || s.endsWith(":")) return false;
18824
+ if (NUMBER.test(s) || AMBIGUOUS.test(s)) return false;
18825
+ return !(inFlow && /[,[\]{}]/.test(s));
18826
+ }
18827
+ function scalar(text, line, inFlow) {
18828
+ if (text.startsWith('"')) {
18829
+ if (!/^"(?:[^"\\]|\\.)*"$/.test(text)) throw new FrontMatterError(`Unterminated or malformed double-quoted string: ${text}`, line);
18830
+ try {
18831
+ return JSON.parse(text);
18832
+ } catch {
18833
+ throw new FrontMatterError(`Invalid escape in double-quoted string: ${text}`, line);
18834
+ }
18835
+ }
18836
+ if (text.startsWith("'")) {
18837
+ if (!/^'(?:[^']|'')*'$/.test(text)) throw new FrontMatterError(`Unterminated or malformed single-quoted string: ${text}`, line);
18838
+ return text.slice(1, -1).replace(/''/g, "'");
18839
+ }
18840
+ if (NUMBER.test(text)) return Number(text);
18841
+ if (!isPlainSafe(text, inFlow)) throw new FrontMatterError(`Ambiguous value ${JSON.stringify(text)}; put it in double quotes`, line);
18842
+ return text;
18843
+ }
18844
+ function stringItem(text, line, inFlow) {
18845
+ const value = scalar(text, line, inFlow);
18846
+ if (typeof value !== "string") throw new FrontMatterError(`List items must be strings; quote ${text}`, line);
18847
+ return value;
18848
+ }
18849
+ function flowItems(inner, line) {
18850
+ const items = [];
18851
+ let current = "";
18852
+ let quote3;
18853
+ for (let i = 0; i < inner.length; i++) {
18854
+ const c = inner[i];
18855
+ if (quote3) {
18856
+ current += c;
18857
+ if (quote3 === '"' && c === "\\") current += inner[++i] ?? "";
18858
+ else if (c === quote3) {
18859
+ if (quote3 === "'" && inner[i + 1] === "'") current += inner[++i];
18860
+ else quote3 = void 0;
18861
+ }
18862
+ } else if (c === '"' || c === "'") {
18863
+ quote3 = c;
18864
+ current += c;
18865
+ } else if (c === ",") {
18866
+ items.push(current.trim());
18867
+ current = "";
18868
+ } else current += c;
18869
+ }
18870
+ if (quote3) throw new FrontMatterError("Unterminated string in list", line);
18871
+ if (current.trim() !== "" || items.length > 0) items.push(current.trim());
18872
+ if (items.some((item) => item === "")) throw new FrontMatterError("Empty item in list", line);
18873
+ return items.map((item) => stringItem(item, line, true));
18874
+ }
18875
+ function parseFrontMatter(text) {
18876
+ const lines = text.replace(/\r\n?/g, "\n").split("\n");
18877
+ if (lines[0] !== "---") throw new FrontMatterError('The file must start with a line "---"', 1);
18878
+ const end = lines.indexOf("---", 1);
18879
+ if (end < 0) throw new FrontMatterError('No closing "---" line after the front matter', 1);
18880
+ const data = {};
18881
+ let list;
18882
+ for (let i = 1; i < end; i++) {
18883
+ const raw = lines[i];
18884
+ const line = i + 1;
18885
+ if (raw.trim() === "" || /^\s*#/.test(raw)) continue;
18886
+ if (/\t/.test(raw.match(/^\s*/)[0])) throw new FrontMatterError("Indent with spaces, not tabs", line);
18887
+ const item = raw.match(/^ +- (.*)$/) ?? raw.match(/^ +-()$/);
18888
+ if (item) {
18889
+ if (!list) throw new FrontMatterError('A list item must follow a key with no value, e.g. "hints:"', line);
18890
+ const value2 = item[1].trim();
18891
+ if (value2 === "") throw new FrontMatterError("Empty list item", line);
18892
+ list.items.push(stringItem(value2, line, false));
18893
+ continue;
18894
+ }
18895
+ if (/^\s/.test(raw)) throw new FrontMatterError(`Unexpected indented line: ${raw.trim()}`, line);
18896
+ list = void 0;
18897
+ const m = raw.match(KEY);
18898
+ if (!m) throw new FrontMatterError(`Expected "key: value", got: ${raw}`, line);
18899
+ const [, key, rest] = m;
18900
+ if (Object.prototype.hasOwnProperty.call(data, key)) throw new FrontMatterError(`Duplicate key '${key}'`, line);
18901
+ const value = (rest ?? "").trim();
18902
+ if (value === "") {
18903
+ list = { key, items: [] };
18904
+ data[key] = list.items;
18905
+ } else if (value.startsWith("[")) {
18906
+ if (!value.endsWith("]")) throw new FrontMatterError(`A list must close on the same line: ${value}`, line);
18907
+ data[key] = flowItems(value.slice(1, -1), line);
18908
+ } else {
18909
+ data[key] = scalar(value, line, false);
18910
+ }
18911
+ }
18912
+ for (const [key, value] of Object.entries(data)) {
18913
+ if (Array.isArray(value) && value.length === 0) {
18914
+ const line = lines.findIndex((l, i) => i > 0 && i < end && l.startsWith(`${key}:`)) + 1;
18915
+ throw new FrontMatterError(`'${key}' has no value`, line);
18916
+ }
18917
+ }
18918
+ return { data, body: lines.slice(end + 1).join("\n").trim() };
18919
+ }
18920
+
18921
+ // ../frontend/src/practice/types.ts
18922
+ var DIFFICULTIES = ["easy", "medium", "hard"];
18923
+
18924
+ // ../frontend/src/practice/problemFiles.ts
18925
+ var PROBLEM_MD = "problem.md";
18926
+ var GIVEN = "given.proschi";
18927
+ var STARTER = "starter.proschi";
18928
+ var SOLUTION = "solution.proschi";
18929
+ var WRONG_DIR = "wrong";
18930
+ var REQUIRED = [PROBLEM_MD, GIVEN, STARTER, SOLUTION];
18931
+ var ID = /^[a-z0-9]+(-[a-z0-9]+)*$/;
18932
+ var EXPECT_FAIL = /^#\s*expect-fail:\s*(.*?)\s*$/;
18933
+ var ProblemFolderError = class extends Error {
18934
+ folder;
18935
+ file;
18936
+ line;
18937
+ /** The message without the folder and file. */
18938
+ detail;
18939
+ constructor(folder, message, file, line) {
18940
+ super(`problems/${folder}: ${file ? `${file}${line ? `:${line}` : ""}: ` : ""}${message}`);
18941
+ this.name = "ProblemFolderError";
18942
+ this.detail = message;
18943
+ this.folder = folder;
18944
+ this.file = file;
18945
+ this.line = line;
18946
+ }
18947
+ };
18948
+ function expectFailLines(source2) {
18949
+ const names = [];
18950
+ for (const line of source2.split("\n")) {
18951
+ if (!line.startsWith("#")) break;
18952
+ const m = line.match(EXPECT_FAIL);
18953
+ if (m) names.push(m[1]);
18954
+ }
18955
+ return names;
18956
+ }
18957
+ var FIELDS = ["title", "summary", "difficulty", "tags", "order", "hints"];
18958
+ function readProblemMd(id, text) {
18959
+ const fail2 = (message, line) => {
18960
+ throw new ProblemFolderError(id, message, PROBLEM_MD, line);
18961
+ };
18962
+ let data;
18963
+ let body;
18964
+ try {
18965
+ ({ data, body } = parseFrontMatter(text));
18966
+ } catch (e) {
18967
+ if (e instanceof FrontMatterError) return fail2(e.message.replace(/^line \d+: /, ""), e.line);
18968
+ throw e;
18969
+ }
18970
+ for (const key of Object.keys(data)) if (!FIELDS.includes(key)) fail2(`Unknown front matter field '${key}' (use ${FIELDS.join(", ")})`);
18971
+ const str = (key) => {
18972
+ const v = data[key];
18973
+ if (typeof v !== "string" || v.trim() === "") return fail2(`'${key}' must be a non-empty string`);
18974
+ return v;
18975
+ };
18976
+ const list = (key) => {
18977
+ const v = data[key];
18978
+ if (!Array.isArray(v) || v.length === 0) return fail2(`'${key}' must be a list with at least one item`);
18979
+ return v;
18980
+ };
18981
+ const difficulty = str("difficulty");
18982
+ if (!DIFFICULTIES.includes(difficulty)) fail2(`'difficulty' must be one of ${DIFFICULTIES.join(", ")}`);
18983
+ const order = data.order;
18984
+ if (order !== void 0 && typeof order !== "number") fail2("'order' must be a number");
18985
+ if (body === "") fail2("The statement (the Markdown after the front matter) is empty");
18986
+ return {
18987
+ title: str("title"),
18988
+ summary: str("summary"),
18989
+ difficulty,
18990
+ tags: list("tags"),
18991
+ ...typeof order === "number" ? { order } : {},
18992
+ statement: body,
18993
+ hints: list("hints")
18994
+ };
18995
+ }
18996
+ function problemFromFiles(id, files) {
18997
+ const fail2 = (message) => {
18998
+ throw new ProblemFolderError(id, message);
18999
+ };
19000
+ if (!ID.test(id)) fail2('The folder name is the problem id and must be lowercase words joined by "-", e.g. url-shortener');
19001
+ for (const name of REQUIRED) if (files[name] === void 0) fail2(`Missing ${name}`);
19002
+ for (const name of Object.keys(files)) {
19003
+ const wrong2 = name.startsWith(`${WRONG_DIR}/`) ? name.slice(WRONG_DIR.length + 1) : void 0;
19004
+ if (!REQUIRED.includes(name) && !(wrong2 && /^[a-z0-9][a-z0-9-]*\.proschi$/.test(wrong2))) {
19005
+ fail2(`Unexpected file ${name}: a problem folder holds ${REQUIRED.join(", ")} and ${WRONG_DIR}/<name>.proschi (lowercase, digits and "-")`);
19006
+ }
19007
+ }
19008
+ const meta = readProblemMd(id, files[PROBLEM_MD]);
19009
+ const wrong = Object.keys(files).filter((name) => name.startsWith(`${WRONG_DIR}/`)).sort().map((name) => ({ name: name.slice(WRONG_DIR.length + 1, -".proschi".length), source: files[name], expectFail: expectFailLines(files[name]) }));
19010
+ return {
19011
+ id,
19012
+ ...meta,
19013
+ given: files[GIVEN],
19014
+ starter: files[STARTER],
19015
+ solution: files[SOLUTION],
19016
+ ...wrong.length ? { wrong } : {}
19017
+ };
19018
+ }
19019
+
19020
+ // ../frontend/src/dsl/imports.ts
19021
+ function joinImportPath(fromPath, importPath) {
19022
+ const parts = importPath.startsWith("/") || !fromPath ? [] : fromPath.split("/").slice(0, -1);
19023
+ for (const part of importPath.split("/")) {
19024
+ if (part === "" || part === ".") continue;
19025
+ if (part === ".." && parts.length > 0 && parts[parts.length - 1] !== "..") parts.pop();
19026
+ else parts.push(part);
19027
+ }
19028
+ return parts.join("/");
19029
+ }
19030
+ function mapResolver(files) {
19031
+ const paths = Object.keys(files);
19032
+ return (importPath, fromPath) => {
19033
+ const path = joinImportPath(fromPath, importPath);
19034
+ if (Object.prototype.hasOwnProperty.call(files, path)) return { path, source: files[path] };
19035
+ const name = path.split("/").pop();
19036
+ const sameName = paths.filter((p) => p.split("/").pop() === name);
19037
+ return sameName.length === 1 ? { path: sameName[0], source: files[sameName[0]] } : void 0;
19038
+ };
19039
+ }
19040
+
19041
+ // ../frontend/src/playground/imports.ts
19042
+ function filesResolver(files, rootPath) {
19043
+ const resolve4 = mapResolver(files);
19044
+ return (importPath, fromPath) => {
19045
+ if (joinImportPath(fromPath, importPath) === rootPath) return { path: rootPath, source: "" };
19046
+ return resolve4(importPath, fromPath);
19047
+ };
19048
+ }
19049
+
19050
+ // ../frontend/src/practice/workspace.ts
19051
+ var PROBLEM_FILE = "problem.proschi";
19052
+ var SOLUTION_FILE = "solution.proschi";
19053
+ var CAPACITY_MESSAGE = "capacity is set by the problem; change the design (replicas, shards, caching) instead";
19054
+ function problemResolver(problem) {
19055
+ return filesResolver({ [PROBLEM_FILE]: problem.given }, SOLUTION_FILE);
19056
+ }
19057
+ function enforceProblemCapacity(result) {
19058
+ const capacity = result.diagram.capacity;
19059
+ if (!capacity?.some((c) => c.loc.file !== PROBLEM_FILE)) return result;
19060
+ const kept = [];
19061
+ const errors = [];
19062
+ for (const c of capacity) {
19063
+ if (c.loc.file === PROBLEM_FILE) {
19064
+ kept.push(c);
19065
+ continue;
19066
+ }
19067
+ const { node, loc, shards, ...rest } = c;
19068
+ if (Object.keys(rest).length > 0) {
19069
+ errors.push({ severity: "error", message: `${CAPACITY_MESSAGE} (only \`shards <n>\` may be set in your file)`, ...loc });
19070
+ }
19071
+ if (shards !== void 0) kept.push({ node, shards, loc });
19072
+ }
19073
+ const diagram = { ...result.diagram };
19074
+ if (kept.length) diagram.capacity = kept;
19075
+ else delete diagram.capacity;
19076
+ return { ...result, diagram, diagnostics: [...result.diagnostics, ...errors] };
19077
+ }
19078
+ function parseSolution(problem, source2) {
19079
+ return enforceProblemCapacity(parse(source2, { path: SOLUTION_FILE, resolve: problemResolver(problem) }));
19080
+ }
19081
+ function runTests2(parsed, engine) {
19082
+ if (!engine.available) return { blocked: "no-engine", results: [], passed: 0, solved: false };
19083
+ if (parsed.diagnostics.some((d) => d.severity === "error")) return { blocked: "errors", results: [], passed: 0, solved: false };
19084
+ const analysis = engine.analyze(parsed.diagram);
19085
+ const results = engine.runTests(parsed.diagram, analysis);
19086
+ const passed = results.filter((r) => r.passed).length;
19087
+ return { results, passed, solved: results.length > 0 && passed === results.length };
19088
+ }
19089
+
19090
+ // ../frontend/src/practice/validate.ts
19091
+ var IMPORT_LINE = `import "${PROBLEM_FILE}"`;
19092
+ function trafficUseCases(given) {
19093
+ return [...(given.match(/traffic\s*\{([\s\S]*?)\}/)?.[1] ?? "").matchAll(/^\s*"([^"]+)"/gm)].map((m) => m[1]);
19094
+ }
19095
+ function firstCodeLine(source2) {
19096
+ const lines = source2.split("\n");
19097
+ const i = lines.findIndex((l) => !l.startsWith("#"));
19098
+ return i < 0 ? { text: "", line: lines.length } : { text: lines[i], line: i + 1 };
19099
+ }
19100
+ function validateProblem(problem, engine = defaultEngine) {
19101
+ const violations = [];
19102
+ const add = (file, message, line) => violations.push({ file, message, ...line ? { line } : {} });
19103
+ const fileOf = (d, own) => d.file === PROBLEM_FILE ? GIVEN : own;
19104
+ const report = (own, ds) => {
19105
+ for (const d of ds) add(fileOf(d, own), `${d.severity}: ${d.message}`, d.line);
19106
+ };
19107
+ if (!/^## Functional requirements$/m.test(problem.statement)) add(PROBLEM_MD, 'The statement needs a "## Functional requirements" section');
19108
+ const useCases = trafficUseCases(problem.given);
19109
+ if (useCases.length === 0) add(GIVEN, "The given needs a traffic block naming at least one use case");
19110
+ for (const name of useCases) {
19111
+ if (!problem.statement.includes(`**${name}**`)) add(PROBLEM_MD, `The statement must name the use case **${name}** in bold`);
19112
+ if (!problem.solution.includes(`usecase "${name}"`)) add(SOLUTION, `The solution must define usecase "${name}" from the traffic`);
19113
+ }
19114
+ const importLine = problem.given.split("\n").findIndex((l) => /^\s*import /.test(l));
19115
+ if (importLine >= 0) add(GIVEN, "The given must not import anything", importLine + 1);
19116
+ report(GIVEN, parse(problem.given).diagnostics.filter((d) => d.severity === "error"));
19117
+ if (format(problem.given) !== problem.given) add(GIVEN, "Not in canonical format; run proschi fmt");
19118
+ if (!problem.solution.startsWith(`${IMPORT_LINE}
19119
+ `)) add(SOLUTION, `Must start with ${IMPORT_LINE}`, 1);
19120
+ const solution = parseSolution(problem, problem.solution);
19121
+ report(SOLUTION, solution.diagnostics);
19122
+ if (format(problem.solution) !== problem.solution) add(SOLUTION, "Not in canonical format; run proschi fmt");
19123
+ const solutionRun = runTests2(solution, engine);
19124
+ const testNames = new Set(solutionRun.results.map((r) => r.name));
19125
+ if (solutionRun.blocked === "no-engine") add(SOLUTION, "No simulation to run the tests with");
19126
+ else if (solutionRun.blocked !== "errors") {
19127
+ if (solutionRun.results.length === 0) add(GIVEN, "The problem has no requirements or tests");
19128
+ for (const r of solutionRun.results.filter((r2) => !r2.passed)) add(SOLUTION, `Fails "${r.name}": ${r.message}`, r.loc?.file === void 0 ? r.loc?.line : void 0);
19129
+ }
19130
+ if (!problem.starter.startsWith(`${IMPORT_LINE}
19131
+ `)) add(STARTER, `Must start with ${IMPORT_LINE}`, 1);
19132
+ const starter = parseSolution(problem, problem.starter);
19133
+ report(STARTER, starter.diagnostics.filter((d) => d.severity === "error"));
19134
+ const starterRun = runTests2(starter, engine);
19135
+ const starterFails = starterRun.results.filter((r) => !r.passed).map((r) => r.name);
19136
+ if (!starterRun.blocked && starterFails.length === 0) add(STARTER, "The starter passes every test; it must leave something to solve");
19137
+ const wrong = (problem.wrong ?? []).map((w) => {
19138
+ const file = `${WRONG_DIR}/${w.name}.proschi`;
19139
+ const expectFail = w.expectFail.length ? w.expectFail : expectFailLines(w.source);
19140
+ if (expectFail.length === 0) add(file, 'Start the file with one or more "# expect-fail: <test name>" lines', 1);
19141
+ const code = firstCodeLine(w.source);
19142
+ if (code.text !== IMPORT_LINE) add(file, `After the comment lines the file must start with ${IMPORT_LINE}`, code.line);
19143
+ const parsed = parseSolution(problem, w.source);
19144
+ report(file, parsed.diagnostics.filter((d) => d.severity === "error"));
19145
+ const run2 = runTests2(parsed, engine);
19146
+ const failed = run2.results.filter((r) => !r.passed).map((r) => r.name);
19147
+ const missing = [];
19148
+ if (!run2.blocked) {
19149
+ const lines = w.source.split("\n");
19150
+ for (const name of expectFail) {
19151
+ const line = lines.findIndex((l) => l.startsWith("#") && l.includes(name)) + 1 || void 0;
19152
+ if (!testNames.has(name) && !failed.includes(name)) {
19153
+ add(file, `No test or requirement is named "${name}" (names: ${[...testNames].map((n) => JSON.stringify(n)).join(", ")})`, line);
19154
+ missing.push(name);
19155
+ } else if (!failed.includes(name)) {
19156
+ add(file, `Expected to fail "${name}", but it passes`, line);
19157
+ missing.push(name);
19158
+ }
19159
+ }
19160
+ }
19161
+ return { name: w.name, file, expectFail, failed, missing, alsoFails: failed.filter((n) => !expectFail.includes(n)) };
19162
+ });
19163
+ return { id: problem.id, tests: solutionRun.results.length, starterFails, wrong, violations };
19164
+ }
19165
+
19166
+ // src/problemTemplate.ts
19167
+ function problemTemplate(id) {
19168
+ const title = id.split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
19169
+ return {
19170
+ "problem.md": `---
19171
+ title: ${title}
19172
+ summary: TODO One line for the problem list and the landing page.
19173
+ difficulty: easy
19174
+ tags: [caching, todo]
19175
+ hints:
19176
+ - TODO A nudge, e.g. what answers most reads without the database?
19177
+ - TODO Closer, e.g. a miss reads the database, then fills the cache.
19178
+ - TODO Nearly the answer, e.g. two of everything survives any node failure.
19179
+ ---
19180
+
19181
+ TODO A short context: what the system does and for whom.
19182
+
19183
+ ## Functional requirements
19184
+
19185
+ - **Fetch item**: a user asks for an item by id and gets it back. Name its
19186
+ two scenarios \`"Cache hit"\` and \`"Cache miss"\`.
19187
+
19188
+ Use these use case and scenario names exactly: the traffic, requirements and
19189
+ tests in \`problem.proschi\` refer to them.
19190
+
19191
+ ## Scale
19192
+
19193
+ - Fetch item: **1k rps**, 90% of them for items read recently.
19194
+
19195
+ ## Constraints
19196
+
19197
+ - p99 of fetching an item under **100 ms**.
19198
+ - Available **99.9%** of the time.
19199
+ - Losing any single machine must not take the service down.
19200
+ - At most **$2,000 / month**.
19201
+
19202
+ ## What is given
19203
+
19204
+ \`problem.proschi\` declares the \`user\` and holds the traffic,
19205
+ requirements and tests. Your file imports it; add the components,
19206
+ connections and the use case.
19207
+ `,
19208
+ "given.proschi": `# TODO The problem as Proschi: the fixed nodes the solver connects to, the
19209
+ # traffic, the requirements and the tests that encode the key insight.
19210
+ title "${title}" "TODO One line"
19211
+
19212
+ user "User" [Actor]
19213
+
19214
+ traffic {
19215
+ "Fetch item" 1k rps mix "Cache hit" 90%, "Cache miss" 10%
19216
+ }
19217
+
19218
+ requirements {
19219
+ p99 "Fetch item" < 100ms
19220
+ availability "Fetch item" >= 99.9%
19221
+ survive any node failure
19222
+ cost <= 2000 usd/month
19223
+ }
19224
+
19225
+ test "Items are read from the cache first" {
19226
+ "Fetch item" calls any cache before any database
19227
+ "Fetch item" scenario "Cache hit" never calls any database
19228
+ }
19229
+ `,
19230
+ "starter.proschi": `import "problem.proschi"
19231
+
19232
+ # TODO A small, valid start: usually one use case with its first step.
19233
+ api "Item API" [REST API]
19234
+
19235
+ user -> api
19236
+
19237
+ usecase "Fetch item" {
19238
+ user -> api : GET /items/42
19239
+ api --> user : 200 item
19240
+ }
19241
+ `,
19242
+ "solution.proschi": `import "problem.proschi"
19243
+
19244
+ lb "Load Balancer" [AWS Load Balancer] x2
19245
+ api "Item API" [REST API] x2
19246
+ cache "Item Cache" [Redis] x2
19247
+ db "Items DB" [PostgreSQL] x2
19248
+
19249
+ user -> lb
19250
+ lb -> api : HTTPS
19251
+ api -> cache : GET / SET
19252
+ api -> db : SELECT
19253
+
19254
+ decision "Cache items in Redis" because "Most reads are for recent items; the cache answers them in a millisecond"
19255
+
19256
+ usecase "Fetch item" "Return one item" {
19257
+ user -> lb : GET /items/42
19258
+ lb -> api : GET /items/42
19259
+ api -> cache : GET item:42
19260
+
19261
+ alt "Cache hit" when "the item was read recently" {
19262
+ cache --> api : item
19263
+ } alt "Cache miss" when "the item is not cached" {
19264
+ cache --> api : nil
19265
+ api -> db : SELECT item 42
19266
+ db --> api : item
19267
+ api ->> cache : SET item:42
19268
+ }
19269
+ api --> lb : 200 item
19270
+ lb --> user : 200 item
19271
+ }
19272
+ `,
19273
+ "wrong/database-only.proschi": `# expect-fail: Items are read from the cache first
19274
+ # TODO A plausible wrong design (here: no cache) and the tests it must fail.
19275
+ import "problem.proschi"
19276
+
19277
+ lb "Load Balancer" [AWS Load Balancer] x2
19278
+ api "Item API" [REST API] x2
19279
+ db "Items DB" [PostgreSQL] x2
19280
+
19281
+ user -> lb
19282
+ lb -> api : HTTPS
19283
+ api -> db : SELECT
19284
+
19285
+ usecase "Fetch item" {
19286
+ user -> lb : GET /items/42
19287
+ lb -> api : GET /items/42
19288
+ api -> db : SELECT item 42
19289
+ db --> api : item
19290
+ api --> lb : 200 item
19291
+ lb --> user : 200 item
19292
+ }
19293
+ `
19294
+ };
19295
+ }
19296
+
19297
+ // src/problem.ts
19298
+ var PROBLEM_USAGE = ` proschi problem check [--format text|github|json] [dir]
19299
+ proschi problem new <id> [--dir <problems dir>]`;
19300
+ var PROBLEM_HELP = `problem Practice problems, one folder each (docs/PRACTICE.md).
19301
+ check validates every problem folder in dir (default: the repository's
19302
+ frontend/src/practice/problems when run inside it): front matter, the
19303
+ given (no errors, canonical format), the solution (no diagnostics,
19304
+ canonical, passes every test), the starter (fails a test) and each
19305
+ wrong/ design (fails every test its "# expect-fail:" lines name).
19306
+ Exits with 1 on any violation.
19307
+ new scaffolds a folder that already passes check, with TODOs.`;
19308
+ var REPO_PROBLEMS = (0, import_node_path6.join)("frontend", "src", "practice", "problems");
19309
+ function findProblemsDir(cwd = process.cwd()) {
19310
+ for (let dir = (0, import_node_path6.resolve)(cwd); ; dir = (0, import_node_path6.dirname)(dir)) {
19311
+ const candidate = (0, import_node_path6.join)(dir, REPO_PROBLEMS);
19312
+ if ((0, import_node_fs5.existsSync)(candidate) && (0, import_node_fs5.statSync)(candidate).isDirectory()) return candidate;
19313
+ if ((0, import_node_path6.dirname)(dir) === dir) return void 0;
19314
+ }
19315
+ }
19316
+ function readProblemFolder(folder) {
19317
+ const files = {};
19318
+ for (const entry of (0, import_node_fs5.readdirSync)(folder, { withFileTypes: true })) {
19319
+ if (entry.name.startsWith(".")) continue;
19320
+ if (entry.isFile()) files[entry.name] = (0, import_node_fs5.readFileSync)((0, import_node_path6.join)(folder, entry.name), "utf8");
19321
+ else if (entry.isDirectory()) {
19322
+ for (const sub of (0, import_node_fs5.readdirSync)((0, import_node_path6.join)(folder, entry.name), { withFileTypes: true })) {
19323
+ if (!sub.name.startsWith(".")) files[`${entry.name}/${sub.name}`] = sub.isFile() ? (0, import_node_fs5.readFileSync)((0, import_node_path6.join)(folder, entry.name, sub.name), "utf8") : "";
19324
+ }
19325
+ }
19326
+ }
19327
+ return files;
19328
+ }
19329
+ function checkProblems(dir) {
19330
+ return (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name).sort().map((id) => {
19331
+ const folder = displayPath3((0, import_node_path6.join)(dir, id));
19332
+ try {
19333
+ const problem = problemFromFiles(id, readProblemFolder((0, import_node_path6.join)(dir, id)));
19334
+ return { folder, loaded: true, ...validateProblem(problem) };
19335
+ } catch (e) {
19336
+ if (!(e instanceof ProblemFolderError)) throw e;
19337
+ const violation = { file: e.file ?? ".", message: e.detail, ...e.line ? { line: e.line } : {} };
19338
+ return { folder, loaded: false, id, tests: 0, starterFails: [], wrong: [], violations: [violation] };
19339
+ }
19340
+ });
19341
+ }
19342
+ var quote2 = (names) => names.map((n) => `"${n}"`).join(", ");
19343
+ var escape3 = (s) => s.replace(/%/g, "%25").replace(/\r?\n/g, "%0A");
19344
+ function formatProblemReports(dir, reports, style) {
19345
+ if (style === "json") return JSON.stringify({ dir: displayPath3(dir), problems: reports }, null, 2);
19346
+ const lines = [];
19347
+ const where = (r, v) => v.file === "." ? r.folder : (0, import_node_path6.join)(r.folder, v.file);
19348
+ for (const r of reports) {
19349
+ if (style === "github") {
19350
+ for (const v of r.violations) lines.push(`::error file=${where(r, v)},line=${v.line ?? 1},title=proschi problem ${r.id}::${escape3(v.message)}`);
19351
+ continue;
19352
+ }
19353
+ const ok = r.violations.length === 0;
19354
+ const wrong = r.wrong.length === 1 ? "1 wrong design" : `${r.wrong.length} wrong designs`;
19355
+ lines.push(`${ok ? "\u2713" : "\u2717"} ${r.id}${r.loaded ? `: ${r.tests} tests, the starter fails ${r.starterFails.length}, ${wrong}` : ""}`);
19356
+ for (const v of r.violations) lines.push(` ${where(r, v)}${v.line ? `:${v.line}` : ""}: ${v.message}`);
19357
+ for (const w of r.wrong) {
19358
+ if (w.missing.length) continue;
19359
+ lines.push(` ${WRONG_DIR}/${w.name}: fails ${quote2(w.expectFail)}${w.alsoFails.length ? `; also fails ${quote2(w.alsoFails)}` : ""}`);
19360
+ }
19361
+ }
19362
+ if (style === "text") {
19363
+ const violations = reports.reduce((n, r) => n + r.violations.length, 0);
19364
+ const wrong = reports.reduce((n, r) => n + r.wrong.length, 0);
19365
+ const bad = reports.filter((r) => r.violations.length).length;
19366
+ lines.push(
19367
+ `${reports.length} problem${reports.length === 1 ? "" : "s"}, ${wrong} wrong design${wrong === 1 ? "" : "s"}: ${violations ? `${violations} violation(s) in ${bad} problem${bad === 1 ? "" : "s"}` : "no violations"}`
19368
+ );
19369
+ }
19370
+ return lines.join("\n");
19371
+ }
19372
+ function problemCheck(args, out, err, cwd) {
19373
+ let style = "text";
19374
+ const paths = [];
19375
+ for (let i = 0; i < args.length; i++) {
19376
+ const arg = args[i];
19377
+ if (arg === "--format") {
19378
+ const value = args[++i];
19379
+ if (value !== "text" && value !== "github" && value !== "json") {
19380
+ err(`Unknown format '${value ?? ""}'; use text, github or json`);
19381
+ return 2;
19382
+ }
19383
+ style = value;
19384
+ } else if (arg.startsWith("-")) {
19385
+ err(`Unknown option ${arg}
19386
+
19387
+ Usage:
19388
+ ${PROBLEM_USAGE}`);
19389
+ return 2;
19390
+ } else paths.push(arg);
19391
+ }
19392
+ if (paths.length > 1) {
19393
+ err(`Give one problems directory
19394
+
19395
+ Usage:
19396
+ ${PROBLEM_USAGE}`);
19397
+ return 2;
19398
+ }
19399
+ const dir = paths[0] ? (0, import_node_path6.resolve)(cwd, paths[0]) : findProblemsDir(cwd);
19400
+ if (!dir) {
19401
+ err(`Not inside the Proschi repository; give the problems directory
19402
+
19403
+ Usage:
19404
+ ${PROBLEM_USAGE}`);
19405
+ return 2;
19406
+ }
19407
+ if (!(0, import_node_fs5.existsSync)(dir) || !(0, import_node_fs5.statSync)(dir).isDirectory()) {
19408
+ err(`No such directory: ${paths[0] ?? dir}`);
19409
+ return 2;
19410
+ }
19411
+ const reports = checkProblems(dir);
19412
+ if (reports.length === 0) {
19413
+ err(`No problem folders in ${displayPath3(dir)}`);
19414
+ return 1;
19415
+ }
19416
+ const text = formatProblemReports(dir, reports, style);
19417
+ if (text) out(text);
19418
+ return reports.some((r) => r.violations.length > 0) ? 1 : 0;
19419
+ }
19420
+ function problemNew(args, out, err, cwd) {
19421
+ let dirArg;
19422
+ const ids = [];
19423
+ for (let i = 0; i < args.length; i++) {
19424
+ const arg = args[i];
19425
+ if (arg === "--dir") {
19426
+ dirArg = args[++i];
19427
+ if (!dirArg) {
19428
+ err("Expected a directory after --dir");
19429
+ return 2;
19430
+ }
19431
+ } else if (arg.startsWith("-")) {
19432
+ err(`Unknown option ${arg}
19433
+
19434
+ Usage:
19435
+ ${PROBLEM_USAGE}`);
19436
+ return 2;
19437
+ } else ids.push(arg);
19438
+ }
19439
+ if (ids.length !== 1) {
19440
+ err(`Usage:
19441
+ ${PROBLEM_USAGE}`);
19442
+ return 2;
19443
+ }
19444
+ const id = ids[0];
19445
+ if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(id)) {
19446
+ err(`The id '${id}' must be lowercase words joined by "-", e.g. url-shortener (it is the folder name and the URL)`);
19447
+ return 2;
19448
+ }
19449
+ const dir = dirArg ? (0, import_node_path6.resolve)(cwd, dirArg) : findProblemsDir(cwd) ?? (0, import_node_path6.resolve)(cwd, "problems");
19450
+ const folder = (0, import_node_path6.join)(dir, id);
19451
+ if ((0, import_node_fs5.existsSync)(folder)) {
19452
+ err(`${displayPath3(folder)} already exists`);
19453
+ return 1;
19454
+ }
19455
+ const files = problemTemplate(id);
19456
+ for (const [name, text] of Object.entries(files)) {
19457
+ (0, import_node_fs5.mkdirSync)((0, import_node_path6.dirname)((0, import_node_path6.join)(folder, name)), { recursive: true });
19458
+ (0, import_node_fs5.writeFileSync)((0, import_node_path6.join)(folder, name), text);
19459
+ }
19460
+ const shown = displayPath3(folder);
19461
+ out(`Created ${shown}/
19462
+ ${Object.keys(files).join("\n ")}
19463
+
19464
+ It already passes \`proschi problem check\`; now make it your problem:
19465
+ 1. problem.md: title, summary, difficulty, tags, hints, and the statement
19466
+ (name every use case in bold and spell out the scenario names).
19467
+ 2. given.proschi: the fixed nodes, the traffic, the requirements and the
19468
+ tests that encode the key insight. Capacity overrides go here only.
19469
+ 3. solution.proschi: a good answer that passes every test; starter.proschi:
19470
+ a small valid start that fails at least one.
19471
+ 4. Calibrate: set the latency, cost and availability limits between the
19472
+ reference design and brute-force designs, and prove it with wrong/
19473
+ designs that start with "# expect-fail: <test name>".
19474
+ 5. Run: proschi problem check ${displayPath3(dir)}`);
19475
+ return 0;
19476
+ }
19477
+ function runProblem(args, out, err, cwd = process.cwd()) {
19478
+ const [sub, ...rest] = args;
19479
+ if (sub === "check") return problemCheck(rest, out, err, cwd);
19480
+ if (sub === "new") return problemNew(rest, out, err, cwd);
19481
+ err(`${sub ? `Unknown problem command '${sub}'
19482
+
19483
+ ` : ""}Usage:
19484
+ ${PROBLEM_USAGE}`);
19485
+ return 2;
19486
+ }
19487
+
18758
19488
  // src/cli.ts
18759
- var VERSION = true ? "0.4.0" : "dev";
19489
+ var VERSION = true ? "0.6.0" : "dev";
18760
19490
  var USAGE = `Usage:
18761
19491
  proschi check [--strict] [--format text|github|json] [--openapi <node>=<spec>]... <file|dir>...
18762
19492
  proschi parse <file>
18763
19493
  ${RENDER_USAGE}
18764
19494
  ${FMT_USAGE}
18765
19495
  ${SIM_USAGE}
19496
+ ${PROBLEM_USAGE}
18766
19497
  proschi --version
18767
19498
 
18768
19499
  check Reports errors and warnings. Directories are searched for *.proschi files.
@@ -18775,19 +19506,20 @@ parse Prints {"diagram", "diagnostics"} as JSON; the shape is described by
18775
19506
  schema/proschi-diagram.schema.json.
18776
19507
  ${RENDER_HELP}
18777
19508
  ${FMT_HELP}
18778
- ${SIM_HELP}`;
19509
+ ${SIM_HELP}
19510
+ ${PROBLEM_HELP}`;
18779
19511
  function collectFiles(paths) {
18780
19512
  const out = [];
18781
19513
  const walk = (dir) => {
18782
- for (const entry of (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
19514
+ for (const entry of (0, import_node_fs6.readdirSync)(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
18783
19515
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
18784
- const full = (0, import_node_path6.join)(dir, entry.name);
19516
+ const full = (0, import_node_path7.join)(dir, entry.name);
18785
19517
  if (entry.isDirectory()) walk(full);
18786
19518
  else if (entry.name.endsWith(".proschi")) out.push(full);
18787
19519
  }
18788
19520
  };
18789
19521
  for (const p of paths) {
18790
- if ((0, import_node_fs5.statSync)(p).isDirectory()) walk(p);
19522
+ if ((0, import_node_fs6.statSync)(p).isDirectory()) walk(p);
18791
19523
  else out.push(p);
18792
19524
  }
18793
19525
  return out;
@@ -18835,6 +19567,7 @@ function run(argv, out = console.log, err = console.error) {
18835
19567
  if (command === "fmt") return runFmt(rest, collectFiles, out, err);
18836
19568
  if (command === "test") return runTest(rest, collectFiles, out, err);
18837
19569
  if (command === "analyze") return runAnalyze(rest, out, err);
19570
+ if (command === "problem") return runProblem(rest, out, err);
18838
19571
  if (command === "check") {
18839
19572
  let strict = false;
18840
19573
  let style = "text";