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/README.md +9 -0
- package/dist/cli.cjs +813 -80
- package/dist/render.cjs +121 -118
- package/dist/server.cjs +70 -28
- package/package.json +1 -1
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 =
|
|
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
|
|
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
|
|
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:
|
|
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,
|
|
11873
|
-
const start =
|
|
11874
|
-
const header = parseBlockScalarHeader(
|
|
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 =
|
|
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 (
|
|
11891
|
-
end2 +=
|
|
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 =
|
|
11895
|
-
let offset =
|
|
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 +
|
|
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(
|
|
12057
|
-
const { offset, type, source: source2, end } =
|
|
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(
|
|
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
|
|
12291
|
+
let scalar2;
|
|
12292
12292
|
try {
|
|
12293
12293
|
const res = tag.resolve(value, (msg) => onError(tagToken ?? token, "TAG_RESOLVE_FAILED", msg), ctx.options);
|
|
12294
|
-
|
|
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
|
-
|
|
12298
|
+
scalar2 = new Scalar.Scalar(value);
|
|
12299
12299
|
}
|
|
12300
|
-
|
|
12301
|
-
|
|
12300
|
+
scalar2.range = range;
|
|
12301
|
+
scalar2.source = value;
|
|
12302
12302
|
if (type)
|
|
12303
|
-
|
|
12303
|
+
scalar2.type = type;
|
|
12304
12304
|
if (tagName)
|
|
12305
|
-
|
|
12305
|
+
scalar2.tag = tagName;
|
|
12306
12306
|
if (tag.format)
|
|
12307
|
-
|
|
12307
|
+
scalar2.format = tag.format;
|
|
12308
12308
|
if (comment)
|
|
12309
|
-
|
|
12310
|
-
return
|
|
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
|
|
13468
|
-
let end = this.buffer.indexOf(
|
|
13469
|
-
if (
|
|
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(
|
|
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 (
|
|
14144
|
-
sep =
|
|
14143
|
+
if (scalar2.end) {
|
|
14144
|
+
sep = scalar2.end;
|
|
14145
14145
|
sep.push(this.sourceToken);
|
|
14146
|
-
delete
|
|
14146
|
+
delete scalar2.end;
|
|
14147
14147
|
} else
|
|
14148
14148
|
sep = [this.sourceToken];
|
|
14149
14149
|
const map = {
|
|
14150
14150
|
type: "block-map",
|
|
14151
|
-
offset:
|
|
14152
|
-
indent:
|
|
14153
|
-
items: [{ start, key:
|
|
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(
|
|
14158
|
+
yield* this.lineEnd(scalar2);
|
|
14159
14159
|
}
|
|
14160
|
-
*blockScalar(
|
|
14160
|
+
*blockScalar(scalar2) {
|
|
14161
14161
|
switch (this.type) {
|
|
14162
14162
|
case "space":
|
|
14163
14163
|
case "comment":
|
|
14164
14164
|
case "newline":
|
|
14165
|
-
|
|
14165
|
+
scalar2.props.push(this.sourceToken);
|
|
14166
14166
|
return;
|
|
14167
14167
|
case "scalar":
|
|
14168
|
-
|
|
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
|
|
14801
|
-
var
|
|
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
|
|
15467
|
+
let quote3 = null;
|
|
15468
15468
|
for (let i = 0; i < text.length; i++) {
|
|
15469
15469
|
const ch = text[i];
|
|
15470
|
-
if (
|
|
15470
|
+
if (quote3) {
|
|
15471
15471
|
if (ch === "\\") i++;
|
|
15472
|
-
else if (ch ===
|
|
15472
|
+
else if (ch === quote3) quote3 = null;
|
|
15473
15473
|
continue;
|
|
15474
15474
|
}
|
|
15475
|
-
if (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
|
-
|
|
15483
|
-
|
|
15484
|
-
|
|
15485
|
-
|
|
15486
|
-
|
|
15487
|
-
|
|
15488
|
-
|
|
15489
|
-
|
|
15490
|
-
|
|
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
|
-
|
|
15493
|
-
else if (ch === "{" || ch === "[") depth++;
|
|
15494
|
-
else if (ch === "}" || ch === "]") depth--;
|
|
15503
|
+
return this.depth;
|
|
15495
15504
|
}
|
|
15496
|
-
|
|
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:
|
|
15882
|
-
if (!
|
|
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 =
|
|
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
|
-
|
|
16103
|
-
|
|
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
|
-
|
|
16137
|
+
const more = "\n" + this.lines[lastLine];
|
|
16138
|
+
label += more;
|
|
16139
|
+
depth = brackets.feed(more);
|
|
16106
16140
|
}
|
|
16107
|
-
if (
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
17242
|
-
|
|
17243
|
-
|
|
17244
|
-
while (
|
|
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
|
|
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.
|
|
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,
|
|
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,
|
|
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,
|
|
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";
|