proschi 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +1526 -279
- package/dist/render.cjs +346 -203
- package/dist/server.cjs +1101 -126
- package/grammar/proschi.tmLanguage.json +90 -0
- package/package.json +7 -6
package/dist/cli.cjs
CHANGED
|
@@ -11422,10 +11422,10 @@ var require_resolve_block_map = __commonJS({
|
|
|
11422
11422
|
let offset = bm.offset;
|
|
11423
11423
|
let commentEnd = null;
|
|
11424
11424
|
for (const collItem of bm.items) {
|
|
11425
|
-
const { start, key, sep, value } = collItem;
|
|
11425
|
+
const { start, key, sep: sep2, value } = collItem;
|
|
11426
11426
|
const keyProps = resolveProps.resolveProps(start, {
|
|
11427
11427
|
indicator: "explicit-key-ind",
|
|
11428
|
-
next: key ??
|
|
11428
|
+
next: key ?? sep2?.[0],
|
|
11429
11429
|
offset,
|
|
11430
11430
|
onError,
|
|
11431
11431
|
parentIndent: bm.indent,
|
|
@@ -11439,7 +11439,7 @@ var require_resolve_block_map = __commonJS({
|
|
|
11439
11439
|
else if ("indent" in key && key.indent !== bm.indent)
|
|
11440
11440
|
onError(offset, "BAD_INDENT", startColMsg);
|
|
11441
11441
|
}
|
|
11442
|
-
if (!keyProps.anchor && !keyProps.tag && !
|
|
11442
|
+
if (!keyProps.anchor && !keyProps.tag && !sep2) {
|
|
11443
11443
|
commentEnd = keyProps.end;
|
|
11444
11444
|
if (keyProps.comment) {
|
|
11445
11445
|
if (map.comment)
|
|
@@ -11463,7 +11463,7 @@ var require_resolve_block_map = __commonJS({
|
|
|
11463
11463
|
ctx.atKey = false;
|
|
11464
11464
|
if (utilMapIncludes.mapIncludes(ctx, map.items, keyNode))
|
|
11465
11465
|
onError(keyStart, "DUPLICATE_KEY", "Map keys must be unique");
|
|
11466
|
-
const valueProps = resolveProps.resolveProps(
|
|
11466
|
+
const valueProps = resolveProps.resolveProps(sep2 ?? [], {
|
|
11467
11467
|
indicator: "map-value-ind",
|
|
11468
11468
|
next: value,
|
|
11469
11469
|
offset: keyNode.range[2],
|
|
@@ -11479,7 +11479,7 @@ var require_resolve_block_map = __commonJS({
|
|
|
11479
11479
|
if (ctx.options.strict && keyProps.start < valueProps.found.offset - 1024)
|
|
11480
11480
|
onError(keyNode.range, "KEY_OVER_1024_CHARS", "The : indicator must be at most 1024 chars after the start of an implicit block mapping key");
|
|
11481
11481
|
}
|
|
11482
|
-
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : composeEmptyNode(ctx, offset,
|
|
11482
|
+
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : composeEmptyNode(ctx, offset, sep2, null, valueProps, onError);
|
|
11483
11483
|
if (ctx.schema.compat)
|
|
11484
11484
|
utilFlowIndentCheck.flowIndentCheck(bm.indent, value, onError);
|
|
11485
11485
|
offset = valueNode.range[2];
|
|
@@ -11570,7 +11570,7 @@ var require_resolve_end = __commonJS({
|
|
|
11570
11570
|
let comment = "";
|
|
11571
11571
|
if (end) {
|
|
11572
11572
|
let hasSpace = false;
|
|
11573
|
-
let
|
|
11573
|
+
let sep2 = "";
|
|
11574
11574
|
for (const token of end) {
|
|
11575
11575
|
const { source: source2, type } = token;
|
|
11576
11576
|
switch (type) {
|
|
@@ -11584,13 +11584,13 @@ var require_resolve_end = __commonJS({
|
|
|
11584
11584
|
if (!comment)
|
|
11585
11585
|
comment = cb;
|
|
11586
11586
|
else
|
|
11587
|
-
comment +=
|
|
11588
|
-
|
|
11587
|
+
comment += sep2 + cb;
|
|
11588
|
+
sep2 = "";
|
|
11589
11589
|
break;
|
|
11590
11590
|
}
|
|
11591
11591
|
case "newline":
|
|
11592
11592
|
if (comment)
|
|
11593
|
-
|
|
11593
|
+
sep2 += source2;
|
|
11594
11594
|
hasSpace = true;
|
|
11595
11595
|
break;
|
|
11596
11596
|
default:
|
|
@@ -11633,18 +11633,18 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
11633
11633
|
let offset = fc.offset + fc.start.source.length;
|
|
11634
11634
|
for (let i = 0; i < fc.items.length; ++i) {
|
|
11635
11635
|
const collItem = fc.items[i];
|
|
11636
|
-
const { start, key, sep, value } = collItem;
|
|
11636
|
+
const { start, key, sep: sep2, value } = collItem;
|
|
11637
11637
|
const props = resolveProps.resolveProps(start, {
|
|
11638
11638
|
flow: fcName,
|
|
11639
11639
|
indicator: "explicit-key-ind",
|
|
11640
|
-
next: key ??
|
|
11640
|
+
next: key ?? sep2?.[0],
|
|
11641
11641
|
offset,
|
|
11642
11642
|
onError,
|
|
11643
11643
|
parentIndent: fc.indent,
|
|
11644
11644
|
startOnNewline: false
|
|
11645
11645
|
});
|
|
11646
11646
|
if (!props.found) {
|
|
11647
|
-
if (!props.anchor && !props.tag && !
|
|
11647
|
+
if (!props.anchor && !props.tag && !sep2 && !value) {
|
|
11648
11648
|
if (i === 0 && props.comma)
|
|
11649
11649
|
onError(props.comma, "UNEXPECTED_TOKEN", `Unexpected , in ${fcName}`);
|
|
11650
11650
|
else if (i < fc.items.length - 1)
|
|
@@ -11698,8 +11698,8 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
11698
11698
|
}
|
|
11699
11699
|
}
|
|
11700
11700
|
}
|
|
11701
|
-
if (!isMap && !
|
|
11702
|
-
const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end,
|
|
11701
|
+
if (!isMap && !sep2 && !props.found) {
|
|
11702
|
+
const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, sep2, null, props, onError);
|
|
11703
11703
|
coll.items.push(valueNode);
|
|
11704
11704
|
offset = valueNode.range[2];
|
|
11705
11705
|
if (isBlock(value))
|
|
@@ -11711,7 +11711,7 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
11711
11711
|
if (isBlock(key))
|
|
11712
11712
|
onError(keyNode.range, "BLOCK_IN_FLOW", blockMsg);
|
|
11713
11713
|
ctx.atKey = false;
|
|
11714
|
-
const valueProps = resolveProps.resolveProps(
|
|
11714
|
+
const valueProps = resolveProps.resolveProps(sep2 ?? [], {
|
|
11715
11715
|
flow: fcName,
|
|
11716
11716
|
indicator: "map-value-ind",
|
|
11717
11717
|
next: value,
|
|
@@ -11722,8 +11722,8 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
11722
11722
|
});
|
|
11723
11723
|
if (valueProps.found) {
|
|
11724
11724
|
if (!isMap && !props.found && ctx.options.strict) {
|
|
11725
|
-
if (
|
|
11726
|
-
for (const st of
|
|
11725
|
+
if (sep2)
|
|
11726
|
+
for (const st of sep2) {
|
|
11727
11727
|
if (st === valueProps.found)
|
|
11728
11728
|
break;
|
|
11729
11729
|
if (st.type === "newline") {
|
|
@@ -11740,7 +11740,7 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
11740
11740
|
else
|
|
11741
11741
|
onError(valueProps.start, "MISSING_CHAR", `Missing , or : between ${fcName} items`);
|
|
11742
11742
|
}
|
|
11743
|
-
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode(ctx, valueProps.end,
|
|
11743
|
+
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode(ctx, valueProps.end, sep2, null, valueProps, onError) : null;
|
|
11744
11744
|
if (valueNode) {
|
|
11745
11745
|
if (isBlock(value))
|
|
11746
11746
|
onError(valueNode.range, "BLOCK_IN_FLOW", blockMsg);
|
|
@@ -11920,7 +11920,7 @@ var require_resolve_block_scalar = __commonJS({
|
|
|
11920
11920
|
chompStart = i + 1;
|
|
11921
11921
|
}
|
|
11922
11922
|
let value = "";
|
|
11923
|
-
let
|
|
11923
|
+
let sep2 = "";
|
|
11924
11924
|
let prevMoreIndented = false;
|
|
11925
11925
|
for (let i = 0; i < contentStart; ++i)
|
|
11926
11926
|
value += lines[i][0].slice(trimIndent) + "\n";
|
|
@@ -11937,24 +11937,24 @@ var require_resolve_block_scalar = __commonJS({
|
|
|
11937
11937
|
indent2 = "";
|
|
11938
11938
|
}
|
|
11939
11939
|
if (type === Scalar.Scalar.BLOCK_LITERAL) {
|
|
11940
|
-
value +=
|
|
11941
|
-
|
|
11940
|
+
value += sep2 + indent2.slice(trimIndent) + content;
|
|
11941
|
+
sep2 = "\n";
|
|
11942
11942
|
} else if (indent2.length > trimIndent || content[0] === " ") {
|
|
11943
|
-
if (
|
|
11944
|
-
|
|
11945
|
-
else if (!prevMoreIndented &&
|
|
11946
|
-
|
|
11947
|
-
value +=
|
|
11948
|
-
|
|
11943
|
+
if (sep2 === " ")
|
|
11944
|
+
sep2 = "\n";
|
|
11945
|
+
else if (!prevMoreIndented && sep2 === "\n")
|
|
11946
|
+
sep2 = "\n\n";
|
|
11947
|
+
value += sep2 + indent2.slice(trimIndent) + content;
|
|
11948
|
+
sep2 = "\n";
|
|
11949
11949
|
prevMoreIndented = true;
|
|
11950
11950
|
} else if (content === "") {
|
|
11951
|
-
if (
|
|
11951
|
+
if (sep2 === "\n")
|
|
11952
11952
|
value += "\n";
|
|
11953
11953
|
else
|
|
11954
|
-
|
|
11954
|
+
sep2 = "\n";
|
|
11955
11955
|
} else {
|
|
11956
|
-
value +=
|
|
11957
|
-
|
|
11956
|
+
value += sep2 + content;
|
|
11957
|
+
sep2 = " ";
|
|
11958
11958
|
prevMoreIndented = false;
|
|
11959
11959
|
}
|
|
11960
11960
|
}
|
|
@@ -12137,25 +12137,25 @@ var require_resolve_flow_scalar = __commonJS({
|
|
|
12137
12137
|
trimBoth = /^[ \t]+|[ \t]+$/g;
|
|
12138
12138
|
}
|
|
12139
12139
|
let res = match[1].replace(trimEnd, "");
|
|
12140
|
-
let
|
|
12140
|
+
let sep2 = " ";
|
|
12141
12141
|
let pos = line.lastIndex;
|
|
12142
12142
|
while (match = line.exec(source2)) {
|
|
12143
12143
|
const lm = match[1].replace(trimBoth, "");
|
|
12144
12144
|
if (lm === "") {
|
|
12145
|
-
if (
|
|
12146
|
-
res +=
|
|
12145
|
+
if (sep2 === "\n")
|
|
12146
|
+
res += sep2;
|
|
12147
12147
|
else
|
|
12148
|
-
|
|
12148
|
+
sep2 = "\n";
|
|
12149
12149
|
} else {
|
|
12150
|
-
res +=
|
|
12151
|
-
|
|
12150
|
+
res += sep2 + lm;
|
|
12151
|
+
sep2 = " ";
|
|
12152
12152
|
}
|
|
12153
12153
|
pos = line.lastIndex;
|
|
12154
12154
|
}
|
|
12155
12155
|
const last = /[ \t]*(.*)/sy;
|
|
12156
12156
|
last.lastIndex = pos;
|
|
12157
12157
|
match = last.exec(source2);
|
|
12158
|
-
return res +
|
|
12158
|
+
return res + sep2 + (match?.[1] ?? "");
|
|
12159
12159
|
}
|
|
12160
12160
|
function doubleQuotedValue(source2, onError) {
|
|
12161
12161
|
let res = "";
|
|
@@ -12965,14 +12965,14 @@ var require_cst_stringify = __commonJS({
|
|
|
12965
12965
|
}
|
|
12966
12966
|
}
|
|
12967
12967
|
}
|
|
12968
|
-
function stringifyItem({ start, key, sep, value }) {
|
|
12968
|
+
function stringifyItem({ start, key, sep: sep2, value }) {
|
|
12969
12969
|
let res = "";
|
|
12970
12970
|
for (const st of start)
|
|
12971
12971
|
res += st.source;
|
|
12972
12972
|
if (key)
|
|
12973
12973
|
res += stringifyToken(key);
|
|
12974
|
-
if (
|
|
12975
|
-
for (const st of
|
|
12974
|
+
if (sep2)
|
|
12975
|
+
for (const st of sep2)
|
|
12976
12976
|
res += st.source;
|
|
12977
12977
|
if (value)
|
|
12978
12978
|
res += stringifyToken(value);
|
|
@@ -14139,18 +14139,18 @@ var require_parser = __commonJS({
|
|
|
14139
14139
|
if (this.type === "map-value-ind") {
|
|
14140
14140
|
const prev = getPrevProps(this.peek(2));
|
|
14141
14141
|
const start = getFirstKeyStartProps(prev);
|
|
14142
|
-
let
|
|
14142
|
+
let sep2;
|
|
14143
14143
|
if (scalar2.end) {
|
|
14144
|
-
|
|
14145
|
-
|
|
14144
|
+
sep2 = scalar2.end;
|
|
14145
|
+
sep2.push(this.sourceToken);
|
|
14146
14146
|
delete scalar2.end;
|
|
14147
14147
|
} else
|
|
14148
|
-
|
|
14148
|
+
sep2 = [this.sourceToken];
|
|
14149
14149
|
const map = {
|
|
14150
14150
|
type: "block-map",
|
|
14151
14151
|
offset: scalar2.offset,
|
|
14152
14152
|
indent: scalar2.indent,
|
|
14153
|
-
items: [{ start, key: scalar2, sep }]
|
|
14153
|
+
items: [{ start, key: scalar2, sep: sep2 }]
|
|
14154
14154
|
};
|
|
14155
14155
|
this.onKeyLine = true;
|
|
14156
14156
|
this.stack[this.stack.length - 1] = map;
|
|
@@ -14303,15 +14303,15 @@ var require_parser = __commonJS({
|
|
|
14303
14303
|
} else if (isFlowToken(it.key) && !includesToken(it.sep, "newline")) {
|
|
14304
14304
|
const start2 = getFirstKeyStartProps(it.start);
|
|
14305
14305
|
const key = it.key;
|
|
14306
|
-
const
|
|
14307
|
-
|
|
14306
|
+
const sep2 = it.sep;
|
|
14307
|
+
sep2.push(this.sourceToken);
|
|
14308
14308
|
delete it.key;
|
|
14309
14309
|
delete it.sep;
|
|
14310
14310
|
this.stack.push({
|
|
14311
14311
|
type: "block-map",
|
|
14312
14312
|
offset: this.offset,
|
|
14313
14313
|
indent: this.indent,
|
|
14314
|
-
items: [{ start: start2, key, sep }]
|
|
14314
|
+
items: [{ start: start2, key, sep: sep2 }]
|
|
14315
14315
|
});
|
|
14316
14316
|
} else if (start.length > 0) {
|
|
14317
14317
|
it.sep = it.sep.concat(start, this.sourceToken);
|
|
@@ -14505,13 +14505,13 @@ var require_parser = __commonJS({
|
|
|
14505
14505
|
const prev = getPrevProps(parent);
|
|
14506
14506
|
const start = getFirstKeyStartProps(prev);
|
|
14507
14507
|
fixFlowSeqItems(fc);
|
|
14508
|
-
const
|
|
14509
|
-
|
|
14508
|
+
const sep2 = fc.end.splice(1, fc.end.length);
|
|
14509
|
+
sep2.push(this.sourceToken);
|
|
14510
14510
|
const map = {
|
|
14511
14511
|
type: "block-map",
|
|
14512
14512
|
offset: fc.offset,
|
|
14513
14513
|
indent: fc.indent,
|
|
14514
|
-
items: [{ start, key: fc, sep }]
|
|
14514
|
+
items: [{ start, key: fc, sep: sep2 }]
|
|
14515
14515
|
};
|
|
14516
14516
|
this.onKeyLine = true;
|
|
14517
14517
|
this.stack[this.stack.length - 1] = map;
|
|
@@ -15253,8 +15253,8 @@ __export(cli_exports, {
|
|
|
15253
15253
|
run: () => run
|
|
15254
15254
|
});
|
|
15255
15255
|
module.exports = __toCommonJS(cli_exports);
|
|
15256
|
-
var
|
|
15257
|
-
var
|
|
15256
|
+
var import_node_fs14 = require("node:fs");
|
|
15257
|
+
var import_node_path13 = require("node:path");
|
|
15258
15258
|
|
|
15259
15259
|
// src/openapi/config.ts
|
|
15260
15260
|
var import_node_fs2 = require("node:fs");
|
|
@@ -16278,6 +16278,8 @@ function editDistance2(a, b) {
|
|
|
16278
16278
|
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
16279
16279
|
var DEFAULT_TECH = "Rectangle";
|
|
16280
16280
|
var DEFAULT_GROUP_TECH = "Logical Group";
|
|
16281
|
+
var SYSTEM_TECH = "Service";
|
|
16282
|
+
var STUB_HELP = "a rate, p99, latency or availability, e.g. capacity { 2k rps p99 150ms availability 99.95% }";
|
|
16281
16283
|
var MAX_SCENARIOS = 32;
|
|
16282
16284
|
var MAX_ALT_DEPTH = 16;
|
|
16283
16285
|
var DIRECTIONS = ["down", "right", "up", "left"];
|
|
@@ -16321,7 +16323,7 @@ function unknownTechMessage(tech, kind, suggestion) {
|
|
|
16321
16323
|
function parse(source2, options = {}) {
|
|
16322
16324
|
return new Parser(source2, options).run();
|
|
16323
16325
|
}
|
|
16324
|
-
var Parser = class {
|
|
16326
|
+
var Parser = class _Parser {
|
|
16325
16327
|
// Per-file state, swapped while an imported file is read.
|
|
16326
16328
|
lines;
|
|
16327
16329
|
/** Path of the imported file being read; undefined for the root document. */
|
|
@@ -16342,7 +16344,12 @@ var Parser = class {
|
|
|
16342
16344
|
/** Files being read, outermost first, to find import cycles. */
|
|
16343
16345
|
chain;
|
|
16344
16346
|
/** Diagnostics are sorted by file in the order the files were first read, the root first. */
|
|
16345
|
-
fileOrder
|
|
16347
|
+
fileOrder;
|
|
16348
|
+
/** The file read as the root: undefined for a document, the path for a system's team file (its own scope). */
|
|
16349
|
+
rootFile;
|
|
16350
|
+
teamFiles;
|
|
16351
|
+
systems = [];
|
|
16352
|
+
systemById = /* @__PURE__ */ new Map();
|
|
16346
16353
|
/** `a|b` for every connection, both directions; filled before use cases are built. */
|
|
16347
16354
|
connected = /* @__PURE__ */ new Set();
|
|
16348
16355
|
title;
|
|
@@ -16364,14 +16371,18 @@ var Parser = class {
|
|
|
16364
16371
|
deferred = [];
|
|
16365
16372
|
built = [];
|
|
16366
16373
|
options;
|
|
16367
|
-
constructor(source2, options) {
|
|
16374
|
+
constructor(source2, options, scope2) {
|
|
16368
16375
|
this.options = options;
|
|
16369
16376
|
this.lines = source2.split("\n");
|
|
16370
|
-
this.chain = [options.path];
|
|
16377
|
+
this.chain = scope2 ? scope2.chain : [options.path];
|
|
16378
|
+
this.teamFiles = scope2?.teamFiles ?? /* @__PURE__ */ new Map();
|
|
16379
|
+
if (scope2) this.rootFile = this.file = options.path;
|
|
16380
|
+
this.fileOrder = /* @__PURE__ */ new Map([[this.rootFile, 0]]);
|
|
16371
16381
|
if (options.path !== void 0) this.included.add(options.path);
|
|
16372
16382
|
}
|
|
16373
16383
|
run() {
|
|
16374
16384
|
this.parseFile();
|
|
16385
|
+
this.resolvePorts();
|
|
16375
16386
|
this.createImplicitNodes();
|
|
16376
16387
|
this.resolveTags();
|
|
16377
16388
|
for (const e of this.edges) this.connected.add(`${e.source}|${e.target}`).add(`${e.target}|${e.source}`);
|
|
@@ -16394,7 +16405,8 @@ var Parser = class {
|
|
|
16394
16405
|
...this.decisions.length ? { decisions: this.decisions } : {},
|
|
16395
16406
|
...this.tests.length ? { tests: this.tests } : {},
|
|
16396
16407
|
...this.legend.length ? { legend: this.legend } : {},
|
|
16397
|
-
...Object.keys(this.layout).length ? { layout: this.layout } : {}
|
|
16408
|
+
...Object.keys(this.layout).length ? { layout: this.layout } : {},
|
|
16409
|
+
...this.systems.length ? { systems: this.systems } : {}
|
|
16398
16410
|
};
|
|
16399
16411
|
const order = (d) => this.fileOrder.get(d.file) ?? 0;
|
|
16400
16412
|
this.diagnostics.sort((a, b) => order(a) - order(b) || a.line - b.line || a.col - b.col);
|
|
@@ -16411,14 +16423,16 @@ var Parser = class {
|
|
|
16411
16423
|
i = this.parseLine(i);
|
|
16412
16424
|
}
|
|
16413
16425
|
for (const frame of this.stack) {
|
|
16414
|
-
const what = frame.kind === "group" ? `group '${frame.id}'` : frame.kind === "usecase" ? `usecase '${frame.useCase.name}'` : frame.kind === "alt" ? `alt '${frame.name}'` : frame.kind === "par" ? "par block" : sectionName(frame);
|
|
16426
|
+
const what = frame.kind === "group" ? `group '${frame.id}'` : frame.kind === "usecase" ? `usecase '${frame.useCase.name}'` : frame.kind === "alt" ? `alt '${frame.name}'` : frame.kind === "par" ? "par block" : frame.kind === "system" ? `system '${frame.system.id}'` : frame.kind === "stub" ? `the capacity of system '${frame.system.id}'` : sectionName(frame);
|
|
16415
16427
|
this.error(`Missing } to close ${what}`, frame.loc);
|
|
16428
|
+
if (frame.kind === "system") this.finishSystem(frame);
|
|
16416
16429
|
}
|
|
16417
16430
|
}
|
|
16418
16431
|
/** Parses the line at `index` and returns the index of the last line it consumed. */
|
|
16419
16432
|
parseLine(index) {
|
|
16420
16433
|
const line = index + 1;
|
|
16421
|
-
const { tokens, diagnostics } = tokenizeLine(this.lines[index], line);
|
|
16434
|
+
const { tokens: lexed, diagnostics } = tokenizeLine(this.lines[index], line);
|
|
16435
|
+
const tokens = qualify(lexed);
|
|
16422
16436
|
this.diagnostics.push(...diagnostics.map((d) => ({ ...d, ...this.fileField() })));
|
|
16423
16437
|
if (tokens.length === 0 || diagnostics.some((d) => d.severity === "error")) return index;
|
|
16424
16438
|
const [first, second, third] = tokens;
|
|
@@ -16439,13 +16453,17 @@ var Parser = class {
|
|
|
16439
16453
|
if (first.kind === "rbrace") {
|
|
16440
16454
|
this.stack.pop();
|
|
16441
16455
|
if (!top.discard) this.closeSection(top);
|
|
16442
|
-
if (!top.discard && this.file ===
|
|
16456
|
+
if (!top.discard && this.file === this.rootFile) this.blocks.push({ kind: top.section, start: top.loc.line, end: line });
|
|
16443
16457
|
if (second) this.error("Unexpected input after }", loc(second));
|
|
16444
16458
|
} else if (!top.discard) {
|
|
16445
16459
|
this.parseSectionLine(top, tokens, line);
|
|
16446
16460
|
}
|
|
16447
16461
|
return index;
|
|
16448
16462
|
}
|
|
16463
|
+
if (top?.kind === "system" || top?.kind === "stub") {
|
|
16464
|
+
this.parseSystemLine(top, tokens, line);
|
|
16465
|
+
return index;
|
|
16466
|
+
}
|
|
16449
16467
|
if (first.kind === "rbrace") {
|
|
16450
16468
|
if (this.stack.length === 0) this.error("Unmatched }", loc(first));
|
|
16451
16469
|
else this.closeFrame();
|
|
@@ -16467,6 +16485,11 @@ var Parser = class {
|
|
|
16467
16485
|
case "group":
|
|
16468
16486
|
this.parseGroup(tokens, line);
|
|
16469
16487
|
break;
|
|
16488
|
+
// A keyword only as `system id … {`, so a node may still be called system.
|
|
16489
|
+
case "system":
|
|
16490
|
+
if (second?.kind === "ident" && tokens[tokens.length - 1].kind === "lbrace") this.parseSystem(tokens, line);
|
|
16491
|
+
else this.parseNode(tokens, line);
|
|
16492
|
+
break;
|
|
16470
16493
|
case "usecase":
|
|
16471
16494
|
this.parseUseCase(tokens, line);
|
|
16472
16495
|
break;
|
|
@@ -16521,7 +16544,7 @@ var Parser = class {
|
|
|
16521
16544
|
return;
|
|
16522
16545
|
}
|
|
16523
16546
|
const summary = tokens[2]?.kind === "string" ? tokens[2] : void 0;
|
|
16524
|
-
if (this.file ===
|
|
16547
|
+
if (this.file === this.rootFile) {
|
|
16525
16548
|
this.title = value.value;
|
|
16526
16549
|
if (summary) this.summary = summary.value;
|
|
16527
16550
|
}
|
|
@@ -16553,7 +16576,7 @@ var Parser = class {
|
|
|
16553
16576
|
this.error("direction is only allowed at the top level or inside a group", this.locOf(keyword, line));
|
|
16554
16577
|
return;
|
|
16555
16578
|
}
|
|
16556
|
-
if (this.file !==
|
|
16579
|
+
if (this.file !== this.rootFile) return;
|
|
16557
16580
|
if (this.layoutLocs.direction) {
|
|
16558
16581
|
this.warning(`direction is already set ${this.where(this.layoutLocs.direction, loc)}; this one is ignored`, this.locOf(keyword, line));
|
|
16559
16582
|
return;
|
|
@@ -16573,7 +16596,7 @@ var Parser = class {
|
|
|
16573
16596
|
this.error("layout ordered applies to the whole diagram; write it at the top level", this.locOf(keyword, line));
|
|
16574
16597
|
return;
|
|
16575
16598
|
}
|
|
16576
|
-
if (this.file !==
|
|
16599
|
+
if (this.file !== this.rootFile) return;
|
|
16577
16600
|
if (this.layoutLocs.ordered) {
|
|
16578
16601
|
this.warning(`layout ordered is already set ${this.where(this.layoutLocs.ordered, loc)}`, this.locOf(keyword, line));
|
|
16579
16602
|
return;
|
|
@@ -16628,16 +16651,8 @@ var Parser = class {
|
|
|
16628
16651
|
this.warning(`Imports are not resolved in this context; '${path}' was not loaded`, loc);
|
|
16629
16652
|
return;
|
|
16630
16653
|
}
|
|
16631
|
-
|
|
16632
|
-
|
|
16633
|
-
target = resolve8(path, this.file ?? this.options.path);
|
|
16634
|
-
} catch {
|
|
16635
|
-
target = void 0;
|
|
16636
|
-
}
|
|
16637
|
-
if (!target) {
|
|
16638
|
-
this.error(`Cannot find '${path}'`, loc);
|
|
16639
|
-
return;
|
|
16640
|
-
}
|
|
16654
|
+
const target = this.resolveFile(path, loc);
|
|
16655
|
+
if (!target) return;
|
|
16641
16656
|
entry.resolved = target.path;
|
|
16642
16657
|
const cycleStart = this.chain.indexOf(target.path);
|
|
16643
16658
|
if (cycleStart !== -1) {
|
|
@@ -16657,6 +16672,25 @@ var Parser = class {
|
|
|
16657
16672
|
this.chain.pop();
|
|
16658
16673
|
({ lines: this.lines, file: this.file, stack: this.stack } = outer);
|
|
16659
16674
|
}
|
|
16675
|
+
/**
|
|
16676
|
+
* Loads `path` through the resolver, relative to the file being read. A
|
|
16677
|
+
* resolver error named `ImportError` (outside the project root, not synced,
|
|
16678
|
+
* …) is reported with its own message; anything else is "Cannot find".
|
|
16679
|
+
*/
|
|
16680
|
+
resolveFile(path, loc) {
|
|
16681
|
+
let target;
|
|
16682
|
+
try {
|
|
16683
|
+
target = this.options.resolve?.(path, this.file ?? this.options.path);
|
|
16684
|
+
} catch (e) {
|
|
16685
|
+
if (e instanceof Error && e.name === "ImportError") {
|
|
16686
|
+
this.error(e.message, loc);
|
|
16687
|
+
return void 0;
|
|
16688
|
+
}
|
|
16689
|
+
target = void 0;
|
|
16690
|
+
}
|
|
16691
|
+
if (!target) this.error(`Cannot find '${path}'`, loc);
|
|
16692
|
+
return target;
|
|
16693
|
+
}
|
|
16660
16694
|
parseGroup(tokens, line) {
|
|
16661
16695
|
const keyword = tokens[0];
|
|
16662
16696
|
const top = this.top();
|
|
@@ -16716,6 +16750,273 @@ var Parser = class {
|
|
|
16716
16750
|
this.addNode(group);
|
|
16717
16751
|
this.stack.push({ kind: "group", id: idToken.value, loc });
|
|
16718
16752
|
}
|
|
16753
|
+
// ---- Systems: a team's design behind a contract (docs/design/team-systems-and-imports.md) ----
|
|
16754
|
+
/** `system id ["Name"] [@team] [pos x,y] [color c] [.tag] [rank N] {` */
|
|
16755
|
+
parseSystem(tokens, line) {
|
|
16756
|
+
const [keyword, idToken] = tokens;
|
|
16757
|
+
const top = this.top();
|
|
16758
|
+
if (top && top.kind !== "group") {
|
|
16759
|
+
this.error("A system is declared at the top level or inside a group", this.locOf(keyword, line));
|
|
16760
|
+
this.skipSection("test", tokens, line);
|
|
16761
|
+
return;
|
|
16762
|
+
}
|
|
16763
|
+
const loc = this.locOf(idToken, line);
|
|
16764
|
+
const node = { id: idToken.value, kind: "system", name: idToken.value, type: "service", techStack: SYSTEM_TECH, parent: this.currentGroup(), loc };
|
|
16765
|
+
const style = {};
|
|
16766
|
+
let named = false;
|
|
16767
|
+
let i = 2;
|
|
16768
|
+
while (i < tokens.length - 1) {
|
|
16769
|
+
const t = tokens[i];
|
|
16770
|
+
const styleUsed = this.readStylePart(tokens, i, line, style, false);
|
|
16771
|
+
const used2 = styleUsed === 0 ? this.readRank(tokens, i, line, node) : styleUsed;
|
|
16772
|
+
if (used2 === void 0) return this.skipSection("test", tokens, line);
|
|
16773
|
+
if (used2 > 0) {
|
|
16774
|
+
i += used2;
|
|
16775
|
+
continue;
|
|
16776
|
+
}
|
|
16777
|
+
if (t.kind === "string" && !named) {
|
|
16778
|
+
node.name = t.value;
|
|
16779
|
+
named = true;
|
|
16780
|
+
} else if (t.kind === "string" && node.description === void 0) {
|
|
16781
|
+
node.description = t.value;
|
|
16782
|
+
} else if (t.kind === "team") {
|
|
16783
|
+
node.ownerTeam = t.value;
|
|
16784
|
+
} else if (isWord(t, "pos")) {
|
|
16785
|
+
const [x, comma, y] = [tokens[i + 1], tokens[i + 2], tokens[i + 3]];
|
|
16786
|
+
if (x?.kind !== "number" || comma?.kind !== "comma" || y?.kind !== "number") {
|
|
16787
|
+
this.error("Expected pos x,y", this.locOf(t, line));
|
|
16788
|
+
return this.skipSection("test", tokens, line);
|
|
16789
|
+
}
|
|
16790
|
+
node.position = { x: Number(x.value), y: Number(y.value) };
|
|
16791
|
+
i += 3;
|
|
16792
|
+
} else {
|
|
16793
|
+
this.error(`Unexpected ${describe(t)} in system header; write system id "Name" @Team {`, this.locOf(t, line));
|
|
16794
|
+
return this.skipSection("test", tokens, line);
|
|
16795
|
+
}
|
|
16796
|
+
i++;
|
|
16797
|
+
}
|
|
16798
|
+
this.applyStyle(node, style);
|
|
16799
|
+
const system = { id: node.id, entries: [], loc };
|
|
16800
|
+
if (!this.nodes.has(node.id)) {
|
|
16801
|
+
this.systems.push(system);
|
|
16802
|
+
this.systemById.set(system.id, system);
|
|
16803
|
+
}
|
|
16804
|
+
this.addNode(node);
|
|
16805
|
+
this.stack.push({ kind: "system", system, useCaseLocs: /* @__PURE__ */ new Map(), loc });
|
|
16806
|
+
}
|
|
16807
|
+
/** A line inside `system … { }` or its multi-line `capacity { }`. */
|
|
16808
|
+
parseSystemLine(frame, tokens, line) {
|
|
16809
|
+
const [first, second] = tokens;
|
|
16810
|
+
const { system } = frame;
|
|
16811
|
+
if (first.kind === "rbrace") {
|
|
16812
|
+
this.stack.pop();
|
|
16813
|
+
if (frame.kind === "system") this.finishSystem(frame);
|
|
16814
|
+
if (second) this.error("Unexpected input after }", this.locOf(second, line));
|
|
16815
|
+
return;
|
|
16816
|
+
}
|
|
16817
|
+
if (frame.kind === "stub") {
|
|
16818
|
+
this.readStub(system.stub, tokens, 0, tokens.length, line);
|
|
16819
|
+
return;
|
|
16820
|
+
}
|
|
16821
|
+
if (isWord(first, "from")) {
|
|
16822
|
+
if (second?.kind !== "string" || !second.value.trim()) {
|
|
16823
|
+
this.error('Expected the team file, e.g. from "teams/orders.proschi"', this.locOf(second ?? first, line));
|
|
16824
|
+
return;
|
|
16825
|
+
}
|
|
16826
|
+
if (system.from !== void 0) {
|
|
16827
|
+
this.error(`System '${system.id}' already has from "${system.from}"`, this.locOf(first, line));
|
|
16828
|
+
return;
|
|
16829
|
+
}
|
|
16830
|
+
this.expectEnd(tokens, 2, line);
|
|
16831
|
+
system.from = second.value;
|
|
16832
|
+
frame.fromLoc = this.locOf(second, line);
|
|
16833
|
+
return;
|
|
16834
|
+
}
|
|
16835
|
+
if (isWord(first, "expose")) return this.parseExpose(frame, tokens, line);
|
|
16836
|
+
if (isWord(first, "capacity") && second?.kind === "lbrace") {
|
|
16837
|
+
const opens = tokens[tokens.length - 1].kind !== "rbrace";
|
|
16838
|
+
if (system.stub) {
|
|
16839
|
+
this.error(`System '${system.id}' already has a capacity`, this.locOf(first, line));
|
|
16840
|
+
if (opens) this.stack.push({ kind: "section", section: "capacity", discard: true, loc: this.locOf(first, line) });
|
|
16841
|
+
return;
|
|
16842
|
+
}
|
|
16843
|
+
const stub = { loc: this.lineLoc(tokens, line) };
|
|
16844
|
+
system.stub = stub;
|
|
16845
|
+
this.readStub(stub, tokens, 2, opens ? tokens.length : tokens.length - 1, line);
|
|
16846
|
+
if (opens) this.stack.push({ kind: "stub", system, loc: this.locOf(first, line) });
|
|
16847
|
+
return;
|
|
16848
|
+
}
|
|
16849
|
+
this.error('Inside a system, write from "file", expose <id> "Name" -> "Use case" or capacity { \u2026 }', this.locOf(first, line));
|
|
16850
|
+
if (tokens[tokens.length - 1].kind === "lbrace") this.stack.push({ kind: "section", section: "test", discard: true, loc: this.locOf(first, line) });
|
|
16851
|
+
}
|
|
16852
|
+
/** `expose id ["Name"] [-> "Use case"]` */
|
|
16853
|
+
parseExpose(frame, tokens, line) {
|
|
16854
|
+
const [keyword, idToken] = tokens;
|
|
16855
|
+
if (idToken?.kind !== "ident") {
|
|
16856
|
+
this.error('Expected an entry point id, e.g. expose api "Orders API" -> "Place order"', this.locOf(idToken ?? keyword, line));
|
|
16857
|
+
return;
|
|
16858
|
+
}
|
|
16859
|
+
const entry = { id: idToken.value, name: idToken.value, loc: this.locOf(idToken, line) };
|
|
16860
|
+
let i = 2;
|
|
16861
|
+
if (tokens[i]?.kind === "string") entry.name = tokens[i++].value;
|
|
16862
|
+
if (tokens[i]?.kind === "arrow") {
|
|
16863
|
+
const useCase = tokens[i + 1];
|
|
16864
|
+
if (tokens[i].value !== "->" || useCase?.kind !== "string" && useCase?.kind !== "ident") {
|
|
16865
|
+
this.error('Expected -> "Use case" after the entry point', this.locOf(useCase ?? tokens[i], line));
|
|
16866
|
+
return;
|
|
16867
|
+
}
|
|
16868
|
+
entry.useCase = useCase.value;
|
|
16869
|
+
frame.useCaseLocs.set(entry, this.locOf(useCase, line));
|
|
16870
|
+
i += 2;
|
|
16871
|
+
}
|
|
16872
|
+
if (!this.expectEnd(tokens, i, line)) return;
|
|
16873
|
+
if (frame.system.entries.some((e) => e.id === entry.id)) {
|
|
16874
|
+
this.error(`System '${frame.system.id}' already exposes '${entry.id}'`, entry.loc);
|
|
16875
|
+
return;
|
|
16876
|
+
}
|
|
16877
|
+
frame.system.entries.push(entry);
|
|
16878
|
+
}
|
|
16879
|
+
/** The parts of a system's `capacity { … }` in `tokens[from, to)`. */
|
|
16880
|
+
readStub(stub, tokens, from, to, line) {
|
|
16881
|
+
for (let i = from; i < to; ) {
|
|
16882
|
+
const t = tokens[i];
|
|
16883
|
+
if (t.kind === "quantity" || t.kind === "number") {
|
|
16884
|
+
const rps = this.quantityOf(t, "rps", t, line);
|
|
16885
|
+
if (rps === void 0) return;
|
|
16886
|
+
stub.rps = rps;
|
|
16887
|
+
i++;
|
|
16888
|
+
} else if (isWord(t, "p99") || isWord(t, "latency") || isWord(t, "availability")) {
|
|
16889
|
+
const value = this.quantityOf(i + 1 < to ? tokens[i + 1] : void 0, t.value === "availability" ? "%" : "ms", t, line);
|
|
16890
|
+
if (value === void 0) return;
|
|
16891
|
+
if (t.value === "p99") stub.p99Ms = value;
|
|
16892
|
+
else if (t.value === "latency") stub.latencyMs = value;
|
|
16893
|
+
else stub.availability = value;
|
|
16894
|
+
i += 2;
|
|
16895
|
+
} else {
|
|
16896
|
+
this.error(`Expected ${STUB_HELP}`, this.locOf(t, line));
|
|
16897
|
+
return;
|
|
16898
|
+
}
|
|
16899
|
+
}
|
|
16900
|
+
}
|
|
16901
|
+
/** At the system's `}`: loads the team file and checks the entry points against its use cases. */
|
|
16902
|
+
finishSystem(frame) {
|
|
16903
|
+
const { system } = frame;
|
|
16904
|
+
if (system.from !== void 0) {
|
|
16905
|
+
const team2 = this.loadTeam(system.from, frame.fromLoc ?? system.loc);
|
|
16906
|
+
if (team2) {
|
|
16907
|
+
system.resolved = team2.path;
|
|
16908
|
+
system.diagram = team2.diagram;
|
|
16909
|
+
}
|
|
16910
|
+
if (system.stub) {
|
|
16911
|
+
this.warning(`System '${system.id}' has a from file, which sets its contract; this capacity is ignored`, system.stub.loc);
|
|
16912
|
+
if (team2) delete system.stub;
|
|
16913
|
+
}
|
|
16914
|
+
} else if (!system.stub) {
|
|
16915
|
+
this.warning(`System '${system.id}' has no from "file" and no capacity { \u2026 }; it is drawn but not simulated`, system.loc);
|
|
16916
|
+
}
|
|
16917
|
+
const team = system.diagram;
|
|
16918
|
+
if (!team) return;
|
|
16919
|
+
const name = fileName(system.resolved ?? "");
|
|
16920
|
+
for (const entry of system.entries) {
|
|
16921
|
+
const wanted = entry.useCase;
|
|
16922
|
+
if (wanted !== void 0) {
|
|
16923
|
+
const u = findUseCaseIn(team, wanted);
|
|
16924
|
+
if (u) entry.useCaseId = u.id;
|
|
16925
|
+
else {
|
|
16926
|
+
const near = team.useCases.find((x) => editDistance2(x.name.toLowerCase(), wanted.toLowerCase()) <= Math.max(1, Math.floor(wanted.length / 4)));
|
|
16927
|
+
const all = team.useCases.map((x) => `"${x.name}"`).join(", ");
|
|
16928
|
+
const help = near ? ` Did you mean "${near.name}"?` : all ? ` Its use cases are ${all}` : "";
|
|
16929
|
+
this.error(`${name} has no use case "${wanted}".${help}`, frame.useCaseLocs.get(entry) ?? entry.loc);
|
|
16930
|
+
}
|
|
16931
|
+
} else if (team.useCases.length === 1) {
|
|
16932
|
+
entry.useCaseId = team.useCases[0].id;
|
|
16933
|
+
} else if (team.useCases.length === 0) {
|
|
16934
|
+
this.error(`${name} has no use case for '${entry.id}' to enter`, entry.loc);
|
|
16935
|
+
} else {
|
|
16936
|
+
this.error(`Say which use case '${entry.id}' enters, e.g. expose ${entry.id} -> "${team.useCases[0].name}"`, entry.loc);
|
|
16937
|
+
}
|
|
16938
|
+
}
|
|
16939
|
+
if (system.entries.length === 0) this.warning(`System '${system.id}' exposes no entry point; add expose <id> -> "Use case" so others can call it`, system.loc);
|
|
16940
|
+
}
|
|
16941
|
+
/** Parses a team file in its own scope (once per root parse), through the import resolver. */
|
|
16942
|
+
loadTeam(path, loc) {
|
|
16943
|
+
if (!this.options.resolve) {
|
|
16944
|
+
this.warning(`Files are not resolved in this context; '${path}' was not loaded`, loc);
|
|
16945
|
+
return void 0;
|
|
16946
|
+
}
|
|
16947
|
+
const target = this.resolveFile(path, loc);
|
|
16948
|
+
if (!target) return void 0;
|
|
16949
|
+
const cycleStart = this.chain.indexOf(target.path);
|
|
16950
|
+
if (cycleStart !== -1) {
|
|
16951
|
+
const names = [...this.chain.slice(cycleStart), target.path].map((p) => fileName(p ?? ""));
|
|
16952
|
+
this.error(`System cycle: ${names.join(" \u2192 ")}`, loc);
|
|
16953
|
+
return void 0;
|
|
16954
|
+
}
|
|
16955
|
+
let result = this.teamFiles.get(target.path);
|
|
16956
|
+
if (!result) {
|
|
16957
|
+
const scope2 = { chain: [...this.chain, target.path], teamFiles: this.teamFiles };
|
|
16958
|
+
result = new _Parser(target.source, { path: target.path, resolve: this.options.resolve }, scope2).run();
|
|
16959
|
+
this.teamFiles.set(target.path, result);
|
|
16960
|
+
for (const d of result.diagnostics) {
|
|
16961
|
+
if (!this.fileOrder.has(d.file)) this.fileOrder.set(d.file, this.fileOrder.size);
|
|
16962
|
+
this.diagnostics.push(d);
|
|
16963
|
+
}
|
|
16964
|
+
}
|
|
16965
|
+
return { path: target.path, diagram: result.diagram };
|
|
16966
|
+
}
|
|
16967
|
+
/**
|
|
16968
|
+
* Resolves `orders.api` references once every file is read: the entry point
|
|
16969
|
+
* `api` of system `orders`. Edges and references then name the system, with
|
|
16970
|
+
* the entry point in `sourcePort` / `targetPort`. On a connection to a
|
|
16971
|
+
* non-system, `b.planned` stays what it always was: `b` with a tag.
|
|
16972
|
+
*/
|
|
16973
|
+
resolvePorts() {
|
|
16974
|
+
for (const ref of this.references) {
|
|
16975
|
+
const dot = ref.id.indexOf(".");
|
|
16976
|
+
if (dot === -1) continue;
|
|
16977
|
+
const prefix = ref.id.slice(0, dot);
|
|
16978
|
+
const port = ref.id.slice(dot + 1);
|
|
16979
|
+
const system = this.systemById.get(prefix);
|
|
16980
|
+
if (system) {
|
|
16981
|
+
if (!system.entries.some((e) => e.id === port)) this.error(this.unexposedMessage(system, port), ref.loc);
|
|
16982
|
+
} else if (ref.edgeTarget) {
|
|
16983
|
+
ref.edgeTarget.tag = port;
|
|
16984
|
+
this.tagRefs.push({ tag: port, loc: { ...ref.loc, col: ref.loc.col + dot, length: ref.loc.length - dot }, target: ref.edgeTarget });
|
|
16985
|
+
} else {
|
|
16986
|
+
this.error(`'${prefix}' is not a system, so '${ref.id}' names nothing; write ${prefix}, or declare system ${prefix} { \u2026 }`, ref.loc);
|
|
16987
|
+
}
|
|
16988
|
+
ref.id = prefix;
|
|
16989
|
+
}
|
|
16990
|
+
for (const edge of this.edges) {
|
|
16991
|
+
const [source2, sourcePort] = this.splitRef(edge.source);
|
|
16992
|
+
const [target, targetPort] = this.splitRef(edge.target);
|
|
16993
|
+
edge.source = source2;
|
|
16994
|
+
edge.target = target;
|
|
16995
|
+
if (sourcePort !== void 0) edge.sourcePort = sourcePort;
|
|
16996
|
+
if (targetPort !== void 0) edge.targetPort = targetPort;
|
|
16997
|
+
}
|
|
16998
|
+
}
|
|
16999
|
+
unexposedMessage(system, port) {
|
|
17000
|
+
const entries2 = system.entries.map((e) => `${system.id}.${e.id}`);
|
|
17001
|
+
const known = entries2.length ? `; connect to ${entries2.join(" or ")}` : "";
|
|
17002
|
+
const internal = system.diagram?.nodes.some((n) => n.id === port);
|
|
17003
|
+
return internal ? `'${port}' is inside system '${system.id}' but is not an entry point; only entry points are reachable from outside: add expose ${port} -> "Use case"${known}` : `System '${system.id}' has no entry point '${port}'${known}`;
|
|
17004
|
+
}
|
|
17005
|
+
/** `orders.api` → `['orders', 'api']` when `orders` is a system; else the id up to its dot, without a port. */
|
|
17006
|
+
splitRef(id) {
|
|
17007
|
+
const dot = id.indexOf(".");
|
|
17008
|
+
if (dot === -1) return [id, void 0];
|
|
17009
|
+
const prefix = id.slice(0, dot);
|
|
17010
|
+
return [prefix, this.systemById.has(prefix) ? id.slice(dot + 1) : void 0];
|
|
17011
|
+
}
|
|
17012
|
+
/** A step to a whole system enters its only entry point; with several it must name one. */
|
|
17013
|
+
onlyEntry(id, loc) {
|
|
17014
|
+
const system = this.systemById.get(id);
|
|
17015
|
+
if (!system || system.entries.length === 0) return void 0;
|
|
17016
|
+
if (system.entries.length === 1) return system.entries[0].id;
|
|
17017
|
+
this.errorOnce(`Say which entry point of '${id}' this step enters: ${system.entries.map((e) => `${id}.${e.id}`).join(" or ")}`, loc);
|
|
17018
|
+
return void 0;
|
|
17019
|
+
}
|
|
16719
17020
|
parseUseCase(tokens, line) {
|
|
16720
17021
|
const keyword = tokens[0];
|
|
16721
17022
|
if (this.stack.length > 0) {
|
|
@@ -16905,7 +17206,8 @@ var Parser = class {
|
|
|
16905
17206
|
const from = fromToken.value;
|
|
16906
17207
|
const to = toToken.value;
|
|
16907
17208
|
const arrow = arrowToken.value;
|
|
16908
|
-
|
|
17209
|
+
const toRef = { id: to, loc: this.locOf(toToken, line) };
|
|
17210
|
+
this.references.push({ id: from, loc: this.locOf(fromToken, line) }, toRef);
|
|
16909
17211
|
const top = this.top();
|
|
16910
17212
|
const inUseCase = top?.kind === "usecase" || top?.kind === "alt" || top?.kind === "par";
|
|
16911
17213
|
if (tagToken && inUseCase) {
|
|
@@ -16940,6 +17242,7 @@ var Parser = class {
|
|
|
16940
17242
|
edge.tag = tagToken.value;
|
|
16941
17243
|
this.tagRefs.push({ tag: tagToken.value, loc: this.locOf(tagToken, line), target: edge });
|
|
16942
17244
|
}
|
|
17245
|
+
if (!tagToken) toRef.edgeTarget = edge;
|
|
16943
17246
|
this.edges.push(edge);
|
|
16944
17247
|
}
|
|
16945
17248
|
return lastLine;
|
|
@@ -17850,7 +18153,9 @@ var Parser = class {
|
|
|
17850
18153
|
for (const step of raw) {
|
|
17851
18154
|
this.checkConnection(step);
|
|
17852
18155
|
if (step.arrow === "-->") {
|
|
17853
|
-
const
|
|
18156
|
+
const [respFrom] = this.splitRef(step.from);
|
|
18157
|
+
const [respTo] = this.splitRef(step.to);
|
|
18158
|
+
const request2 = [...steps2].reverse().find((s) => s.fromServiceId === respTo && s.toServiceId === respFrom && !answered.has(s) && !s.failed);
|
|
17854
18159
|
if (!request2) {
|
|
17855
18160
|
this.warning(`Response has no matching request from '${step.to}' to '${step.from}'`, step.loc);
|
|
17856
18161
|
continue;
|
|
@@ -17869,14 +18174,17 @@ var Parser = class {
|
|
|
17869
18174
|
continue;
|
|
17870
18175
|
}
|
|
17871
18176
|
const label = parseStepLabel(step.label);
|
|
17872
|
-
const
|
|
18177
|
+
const [from, fromPort] = this.splitRef(step.from);
|
|
18178
|
+
const [to, toPort] = this.splitRef(step.to);
|
|
18179
|
+
const port = toPort ?? this.onlyEntry(to, step.loc);
|
|
18180
|
+
const target = this.nodes.get(to);
|
|
17873
18181
|
const isAsync = step.arrow === "->>";
|
|
17874
18182
|
steps2.push({
|
|
17875
18183
|
id: `${prefix}-${steps2.length + 1}`,
|
|
17876
18184
|
stepOrder: steps2.length,
|
|
17877
18185
|
stepName: label.name || `${step.from} \u2192 ${step.to}`,
|
|
17878
|
-
fromServiceId:
|
|
17879
|
-
toServiceId:
|
|
18186
|
+
fromServiceId: from,
|
|
18187
|
+
toServiceId: to,
|
|
17880
18188
|
protocol: inferProtocol(isAsync, label.method, target),
|
|
17881
18189
|
httpMethod: label.method ?? "",
|
|
17882
18190
|
endpoint: label.endpoint ?? "",
|
|
@@ -17892,7 +18200,9 @@ var Parser = class {
|
|
|
17892
18200
|
loc: step.loc,
|
|
17893
18201
|
...step.multiplier !== void 0 ? { multiplier: step.multiplier } : {},
|
|
17894
18202
|
...step.sizeBytes !== void 0 ? { sizeBytes: step.sizeBytes } : {},
|
|
17895
|
-
access: accessOf(label.method, step.label)
|
|
18203
|
+
access: accessOf(label.method, step.label),
|
|
18204
|
+
...port !== void 0 ? { toPort: port } : {},
|
|
18205
|
+
...fromPort !== void 0 ? { fromPort } : {}
|
|
17896
18206
|
});
|
|
17897
18207
|
}
|
|
17898
18208
|
return steps2;
|
|
@@ -17902,8 +18212,10 @@ var Parser = class {
|
|
|
17902
18212
|
* Documents without any connection are use-case-only sketches; they are left alone.
|
|
17903
18213
|
*/
|
|
17904
18214
|
checkConnection(step) {
|
|
17905
|
-
|
|
17906
|
-
const [
|
|
18215
|
+
const [a] = this.splitRef(step.from);
|
|
18216
|
+
const [b] = this.splitRef(step.to);
|
|
18217
|
+
if (this.edges.length === 0 || a === b || this.connected.has(`${a}|${b}`)) return;
|
|
18218
|
+
const [from, to] = step.arrow === "-->" ? [b, a] : [a, b];
|
|
17907
18219
|
this.warning(`No connection between '${from}' and '${to}' in the architecture; add '${from} -> ${to}'`, step.loc);
|
|
17908
18220
|
}
|
|
17909
18221
|
createImplicitNodes() {
|
|
@@ -17999,6 +18311,17 @@ var Parser = class {
|
|
|
17999
18311
|
error(message, loc) {
|
|
18000
18312
|
this.diagnostics.push({ severity: "error", message, ...loc });
|
|
18001
18313
|
}
|
|
18314
|
+
/** An error for a step that every scenario sharing it would report again. */
|
|
18315
|
+
errorOnce(message, loc) {
|
|
18316
|
+
const key = `error
|
|
18317
|
+
${loc.file ?? ""}
|
|
18318
|
+
${loc.line}
|
|
18319
|
+
${loc.col}
|
|
18320
|
+
${message}`;
|
|
18321
|
+
if (this.warned.has(key)) return;
|
|
18322
|
+
this.warned.add(key);
|
|
18323
|
+
this.error(message, loc);
|
|
18324
|
+
}
|
|
18002
18325
|
warning(message, loc) {
|
|
18003
18326
|
const key = `${loc.file ?? ""}
|
|
18004
18327
|
${loc.line}
|
|
@@ -18034,6 +18357,23 @@ function expand(items) {
|
|
|
18034
18357
|
}
|
|
18035
18358
|
return paths;
|
|
18036
18359
|
}
|
|
18360
|
+
function qualify(tokens) {
|
|
18361
|
+
let out;
|
|
18362
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
18363
|
+
const [t, next] = [tokens[i], tokens[i + 1]];
|
|
18364
|
+
const joined = t.kind === "ident" && next?.kind === "tag" && next.col === t.col + t.length;
|
|
18365
|
+
if (joined && (tokens[i + 2]?.kind === "arrow" || tokens[i - 1]?.kind === "arrow")) {
|
|
18366
|
+
out ??= tokens.slice(0, i);
|
|
18367
|
+
out.push({ ...t, value: `${t.value}.${next.value}`, length: t.length + next.length });
|
|
18368
|
+
i++;
|
|
18369
|
+
} else out?.push(t);
|
|
18370
|
+
}
|
|
18371
|
+
return out ?? tokens;
|
|
18372
|
+
}
|
|
18373
|
+
function findUseCaseIn(diagram, name) {
|
|
18374
|
+
const lower = name.toLowerCase();
|
|
18375
|
+
return diagram.useCases.find((u) => u.name === name) ?? diagram.useCases.find((u) => u.name.toLowerCase() === lower) ?? diagram.useCases.find((u) => u.id === name);
|
|
18376
|
+
}
|
|
18037
18377
|
function isWord(token, word) {
|
|
18038
18378
|
return token?.kind === "ident" && token.value === word;
|
|
18039
18379
|
}
|
|
@@ -18701,13 +19041,17 @@ function openApiDiagnostics(file, diagram, overrides = {}) {
|
|
|
18701
19041
|
}
|
|
18702
19042
|
|
|
18703
19043
|
// src/render/usage.ts
|
|
18704
|
-
var RENDER_USAGE = ` proschi render [--out <dir>] [--format svg|md|html|hld-md|hld-html] <file>`;
|
|
19044
|
+
var RENDER_USAGE = ` proschi render [--out <dir>] [--format svg|md|html|hld-md|hld-html] [--expand <id>[,<id>\u2026]] [--focus <id>] <file>`;
|
|
18705
19045
|
var RENDER_HELP = `render Writes the diagram as static files into --out (default: the current
|
|
18706
19046
|
directory). svg (default): architecture.svg and <usecase>--<scenario>.svg
|
|
18707
19047
|
per scenario; md: <file>.md with Mermaid blocks; html: <file>.html with
|
|
18708
19048
|
the SVGs; hld-md / hld-html: <file>.hld.md / <file>.hld.html, the
|
|
18709
19049
|
high-level design document (overview, requirements, capacity,
|
|
18710
19050
|
components, data model, APIs, scenarios, decisions, risks).
|
|
19051
|
+
Systems are drawn as one box each, with the team's promise and what
|
|
19052
|
+
they are sent against their headroom; --expand opens them in place
|
|
19053
|
+
(orders.warehouse names one inside another), --focus draws one
|
|
19054
|
+
system's design alone. An id that names no system exits with 2.
|
|
18711
19055
|
Refuses (exit 1) when the document has errors.`;
|
|
18712
19056
|
|
|
18713
19057
|
// src/fmt.ts
|
|
@@ -19090,6 +19434,340 @@ function formatUsd(usd) {
|
|
|
19090
19434
|
return `$${Math.round(usd).toLocaleString("en-US")}/month`;
|
|
19091
19435
|
}
|
|
19092
19436
|
|
|
19437
|
+
// ../frontend/src/sim/overlay.ts
|
|
19438
|
+
function flowsOf(diagram, traffic) {
|
|
19439
|
+
const flows = /* @__PURE__ */ new Map();
|
|
19440
|
+
for (const t of traffic) {
|
|
19441
|
+
const u = findUseCase(diagram, t.useCase);
|
|
19442
|
+
if (!u || t.rps <= 0) continue;
|
|
19443
|
+
for (const sc of u.scenarios) {
|
|
19444
|
+
const share = t.mix ? t.mix.find((m) => m.scenario === sc.name)?.share ?? 0 : sc === u.scenarios[0] ? 1 : 0;
|
|
19445
|
+
if (share <= 0) continue;
|
|
19446
|
+
for (const step of sc.steps) {
|
|
19447
|
+
if (step.failed) continue;
|
|
19448
|
+
const key = `${step.fromServiceId}>${step.toServiceId}`;
|
|
19449
|
+
const f = flows.get(key) ?? { from: step.fromServiceId, to: step.toServiceId, rps: 0, async: step.executionType !== "SYNC_REQUEST_RESPONSE" };
|
|
19450
|
+
f.rps += t.rps * share * (step.multiplier ?? 1);
|
|
19451
|
+
flows.set(key, f);
|
|
19452
|
+
}
|
|
19453
|
+
}
|
|
19454
|
+
}
|
|
19455
|
+
return [...flows.values()];
|
|
19456
|
+
}
|
|
19457
|
+
function analysisOverlay(diagram, analysis) {
|
|
19458
|
+
const nodes = {};
|
|
19459
|
+
for (const n of analysis.nodes) {
|
|
19460
|
+
const works = Number.isFinite(n.capacityRps) && n.kind !== "client";
|
|
19461
|
+
nodes[n.id] = {
|
|
19462
|
+
...works ? { utilization: n.utilization, saturated: n.saturated } : {},
|
|
19463
|
+
...n.replicas > 1 ? { replicas: n.replicas } : {},
|
|
19464
|
+
...n.shards > 1 ? { shards: n.shards } : {}
|
|
19465
|
+
};
|
|
19466
|
+
}
|
|
19467
|
+
return { nodes, flows: flowsOf(diagram, diagram.traffic ?? []) };
|
|
19468
|
+
}
|
|
19469
|
+
|
|
19470
|
+
// ../frontend/src/sim/systems.ts
|
|
19471
|
+
var teamKey = (s) => s.resolved ?? s.diagram;
|
|
19472
|
+
var contracts = /* @__PURE__ */ new WeakMap();
|
|
19473
|
+
function contractOf(system, options = {}) {
|
|
19474
|
+
let contract = contracts.get(system);
|
|
19475
|
+
if (!contract) {
|
|
19476
|
+
contract = system.diagram ? designContract(system, system.diagram, options) : stubContract(system);
|
|
19477
|
+
contracts.set(system, contract);
|
|
19478
|
+
}
|
|
19479
|
+
return contract;
|
|
19480
|
+
}
|
|
19481
|
+
function stubContract(system) {
|
|
19482
|
+
const stub = system.stub;
|
|
19483
|
+
if (!stub) return { source: "none", entries: system.entries.map((e) => emptyEntry(e.id, e.name)) };
|
|
19484
|
+
const rps = stub.rps ?? Infinity;
|
|
19485
|
+
return {
|
|
19486
|
+
source: "stub",
|
|
19487
|
+
entries: system.entries.map((e) => ({
|
|
19488
|
+
...emptyEntry(e.id, e.name),
|
|
19489
|
+
latency: stub.p99Ms !== void 0 ? [{ percentile: 99, maxMs: stub.p99Ms }] : [],
|
|
19490
|
+
...stub.availability !== void 0 ? { availabilityPercent: stub.availability } : {},
|
|
19491
|
+
saturatesAtRps: rps,
|
|
19492
|
+
hotAtRps: rps * HOT
|
|
19493
|
+
}))
|
|
19494
|
+
};
|
|
19495
|
+
}
|
|
19496
|
+
var emptyEntry = (entry, name) => ({ entry, name, ownRps: 0, latency: [], saturatesAtRps: Infinity, hotAtRps: Infinity });
|
|
19497
|
+
function designContract(system, team, options) {
|
|
19498
|
+
const traffic = resolveTraffic(team);
|
|
19499
|
+
const base = computeLoad(team, traffic);
|
|
19500
|
+
const baseBytes = computeBytesPerSecond(team, traffic);
|
|
19501
|
+
const profiles = profilesOf(team);
|
|
19502
|
+
const nodes = components(team);
|
|
19503
|
+
const entries2 = system.entries.map((e) => {
|
|
19504
|
+
const u = team.useCases.find((x) => x.id === e.useCaseId);
|
|
19505
|
+
if (!u) return emptyEntry(e.id, e.name);
|
|
19506
|
+
const own = traffic.get(u.id);
|
|
19507
|
+
const unit = /* @__PURE__ */ new Map([[u.id, { rps: 1, shares: own.shares }]]);
|
|
19508
|
+
const delta = computeLoad(team, unit);
|
|
19509
|
+
const deltaBytes = computeBytesPerSecond(team, unit);
|
|
19510
|
+
const result = { ...emptyEntry(e.id, e.name), useCase: u.name, ownRps: own.rps };
|
|
19511
|
+
for (const n of nodes) {
|
|
19512
|
+
const d = delta.get(n.id);
|
|
19513
|
+
const db = deltaBytes.get(n.id) ?? 0;
|
|
19514
|
+
if (!d && !db) continue;
|
|
19515
|
+
const p = profiles.get(n.id);
|
|
19516
|
+
const replicas = replicasOf(n);
|
|
19517
|
+
const b = base.get(n.id) ?? { read: 0, write: 0 };
|
|
19518
|
+
const bb = baseBytes.get(n.id) ?? 0;
|
|
19519
|
+
const u0 = usageOf(p, replicas, b.read, b.write, bb);
|
|
19520
|
+
const u1 = usageOf(p, replicas, b.read + (d?.read ?? 0), b.write + (d?.write ?? 0), bb + db);
|
|
19521
|
+
const sat = crossing(u0, u1, 1);
|
|
19522
|
+
const hot = crossing(u0, u1, HOT);
|
|
19523
|
+
if (sat < result.saturatesAtRps) {
|
|
19524
|
+
result.saturatesAtRps = sat;
|
|
19525
|
+
result.bottleneck = n.id;
|
|
19526
|
+
}
|
|
19527
|
+
if (hot < result.hotAtRps) {
|
|
19528
|
+
result.hotAtRps = hot;
|
|
19529
|
+
result.hotNode = n.id;
|
|
19530
|
+
}
|
|
19531
|
+
}
|
|
19532
|
+
for (const r of team.requirements ?? []) {
|
|
19533
|
+
if (r.kind !== "latency" && r.kind !== "availability") continue;
|
|
19534
|
+
if (r.useCase !== void 0 && findUseCase(team, r.useCase) !== u) continue;
|
|
19535
|
+
if (r.kind === "latency") result.latency.push({ percentile: r.percentile, maxMs: r.maxMs, ...r.scenario !== void 0 ? { scenario: r.scenario } : {} });
|
|
19536
|
+
else result.availabilityPercent = Math.max(result.availabilityPercent ?? 0, r.minPercent);
|
|
19537
|
+
}
|
|
19538
|
+
return result;
|
|
19539
|
+
});
|
|
19540
|
+
return { source: "design", entries: entries2, baseline: analyze(team, { timeoutMs: options.timeoutMs }) };
|
|
19541
|
+
}
|
|
19542
|
+
function crossing(u0, u1, theta) {
|
|
19543
|
+
const terms = u0.writeScaling === "shards" ? [
|
|
19544
|
+
[u0.readUtilization, u1.readUtilization],
|
|
19545
|
+
[u0.writeUtilization, u1.writeUtilization]
|
|
19546
|
+
] : [[u0.requestUtilization, u1.requestUtilization]];
|
|
19547
|
+
terms.push([u0.bandwidthUtilization, u1.bandwidthUtilization]);
|
|
19548
|
+
let best = Infinity;
|
|
19549
|
+
for (const [a, b] of terms) {
|
|
19550
|
+
const slope = b - a;
|
|
19551
|
+
if (slope <= 1e-15) continue;
|
|
19552
|
+
if (a >= theta) return 0;
|
|
19553
|
+
best = Math.min(best, (theta - a) / slope);
|
|
19554
|
+
}
|
|
19555
|
+
return best;
|
|
19556
|
+
}
|
|
19557
|
+
function inboundLoad(diagram, traffic) {
|
|
19558
|
+
const result = /* @__PURE__ */ new Map();
|
|
19559
|
+
forEachStepRate(diagram, traffic, (step, rate) => {
|
|
19560
|
+
if (step.toPort === void 0) return;
|
|
19561
|
+
const ports = result.get(step.toServiceId) ?? /* @__PURE__ */ new Map();
|
|
19562
|
+
ports.set(step.toPort, (ports.get(step.toPort) ?? 0) + rate * multiplierOf(step));
|
|
19563
|
+
result.set(step.toServiceId, ports);
|
|
19564
|
+
});
|
|
19565
|
+
return result;
|
|
19566
|
+
}
|
|
19567
|
+
function hopFrom(p50, p99) {
|
|
19568
|
+
const l50 = tailFactor(0.5);
|
|
19569
|
+
const l99 = tailFactor(0.99);
|
|
19570
|
+
let tailMs = Math.max(0, (p99 - p50) / (l99 - l50));
|
|
19571
|
+
let fixedMs = p50 - tailMs * l50;
|
|
19572
|
+
if (fixedMs < 0) {
|
|
19573
|
+
fixedMs = 0;
|
|
19574
|
+
tailMs = p99 / l99;
|
|
19575
|
+
}
|
|
19576
|
+
return { fixedMs, tailMs, waitProbability: 0 };
|
|
19577
|
+
}
|
|
19578
|
+
function analyzeSystems(diagram, traffic, options = {}) {
|
|
19579
|
+
const inbound = inboundLoad(diagram, traffic);
|
|
19580
|
+
if (!options.sharedSystems) options = { ...options, sharedSystems: sharedLoad(diagram, inbound) };
|
|
19581
|
+
const hops = /* @__PURE__ */ new Map();
|
|
19582
|
+
const availabilities = /* @__PURE__ */ new Map();
|
|
19583
|
+
const key = (id, port) => `${id}
|
|
19584
|
+
${port}`;
|
|
19585
|
+
const systems = (diagram.systems ?? []).map((system) => {
|
|
19586
|
+
const result = analyzeSystem(system, inbound.get(system.id) ?? /* @__PURE__ */ new Map(), options);
|
|
19587
|
+
for (const e of result.entries) {
|
|
19588
|
+
hops.set(key(system.id, e.entry), hopFrom(e.p50Ms, e.p99Ms));
|
|
19589
|
+
availabilities.set(key(system.id, e.entry), e.availability);
|
|
19590
|
+
}
|
|
19591
|
+
return result;
|
|
19592
|
+
});
|
|
19593
|
+
return {
|
|
19594
|
+
systems,
|
|
19595
|
+
hop: (id, port) => port === void 0 ? void 0 : hops.get(key(id, port)),
|
|
19596
|
+
availability: (id, port) => port === void 0 ? void 0 : availabilities.get(key(id, port))
|
|
19597
|
+
};
|
|
19598
|
+
}
|
|
19599
|
+
function sentByUseCase(system, sent, into) {
|
|
19600
|
+
let total = 0;
|
|
19601
|
+
for (const e of system.entries) {
|
|
19602
|
+
const rps = sent.get(e.id) ?? 0;
|
|
19603
|
+
if (rps <= 0 || e.useCaseId === void 0) continue;
|
|
19604
|
+
into.set(e.useCaseId, (into.get(e.useCaseId) ?? 0) + rps);
|
|
19605
|
+
total += rps;
|
|
19606
|
+
}
|
|
19607
|
+
return total;
|
|
19608
|
+
}
|
|
19609
|
+
function sharedLoad(root, rootInbound) {
|
|
19610
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
19611
|
+
const designs = /* @__PURE__ */ new Map();
|
|
19612
|
+
const order = [];
|
|
19613
|
+
const visit = (d, prefix) => {
|
|
19614
|
+
for (const s of d.systems ?? []) {
|
|
19615
|
+
if (!s.diagram) continue;
|
|
19616
|
+
const key = teamKey(s);
|
|
19617
|
+
if (designs.has(key)) continue;
|
|
19618
|
+
const path = prefix ? `${prefix}.${s.id}` : s.id;
|
|
19619
|
+
designs.set(key, { diagram: s.diagram, path, loc: s.loc });
|
|
19620
|
+
visit(s.diagram, path);
|
|
19621
|
+
order.push(key);
|
|
19622
|
+
}
|
|
19623
|
+
};
|
|
19624
|
+
visit(root, "");
|
|
19625
|
+
const send = (d, prefix, inbound) => {
|
|
19626
|
+
for (const s of d.systems ?? []) {
|
|
19627
|
+
if (!s.diagram) continue;
|
|
19628
|
+
const key = teamKey(s);
|
|
19629
|
+
const load = byFile.get(key) ?? { useCases: /* @__PURE__ */ new Map(), callers: [] };
|
|
19630
|
+
byFile.set(key, load);
|
|
19631
|
+
const rps = sentByUseCase(s, inbound.get(s.id) ?? /* @__PURE__ */ new Map(), load.useCases);
|
|
19632
|
+
if (rps > 0) load.callers.push({ path: prefix ? `${prefix}.${s.id}` : s.id, ...d.title !== void 0 ? { from: d.title } : {}, rps });
|
|
19633
|
+
}
|
|
19634
|
+
};
|
|
19635
|
+
send(root, "", rootInbound);
|
|
19636
|
+
for (const key of order.reverse()) {
|
|
19637
|
+
const { diagram, path, loc } = designs.get(key);
|
|
19638
|
+
if (!diagram.systems?.length) continue;
|
|
19639
|
+
const combined = withExtraTraffic(diagram, byFile.get(key)?.useCases ?? /* @__PURE__ */ new Map(), loc);
|
|
19640
|
+
send(diagram, path, inboundLoad(combined, resolveTraffic(combined)));
|
|
19641
|
+
}
|
|
19642
|
+
return { byFile, analyses: /* @__PURE__ */ new Map() };
|
|
19643
|
+
}
|
|
19644
|
+
function analyzeSystem(system, sent, options) {
|
|
19645
|
+
const contract = contractOf(system, options);
|
|
19646
|
+
const inbound = system.entries.map((e) => ({ entry: e.id, rps: sent.get(e.id) ?? 0 }));
|
|
19647
|
+
const totalInboundRps = inbound.reduce((sum, e) => sum + e.rps, 0);
|
|
19648
|
+
const result = { id: system.id, contract, inbound, totalInboundRps, utilization: 0, saturated: false, hot: false, breaches: [], entries: [], warnings: [] };
|
|
19649
|
+
const shared = system.diagram ? options.sharedSystems?.byFile.get(teamKey(system)) : void 0;
|
|
19650
|
+
const sending = inbound.filter((e) => e.rps > 0).map((e) => {
|
|
19651
|
+
const c = contract.entries.find((x) => x.entry === e.entry);
|
|
19652
|
+
const useCaseId = system.entries.find((x) => x.id === e.entry)?.useCaseId;
|
|
19653
|
+
const gets = useCaseId !== void 0 ? shared?.useCases.get(useCaseId) ?? 0 : 0;
|
|
19654
|
+
const rps = gets > e.rps * (1 + 1e-9) ? `${formatRps(e.rps).replace(/ rps$/, "")} of the ${formatRps(gets)} it gets` : formatRps(e.rps);
|
|
19655
|
+
return `${rps} into ${c?.useCase ? `"${c.useCase}" (${system.id}.${e.entry})` : `${system.id}.${e.entry}`}`;
|
|
19656
|
+
}).join(" and ");
|
|
19657
|
+
const label = `'${system.id}'`;
|
|
19658
|
+
if (contract.source === "stub") {
|
|
19659
|
+
const stub = system.stub;
|
|
19660
|
+
const rps = stub.rps ?? Infinity;
|
|
19661
|
+
result.utilization = Number.isFinite(rps) && rps > 0 ? totalInboundRps / rps : 0;
|
|
19662
|
+
result.saturated = result.utilization >= 1;
|
|
19663
|
+
result.hot = !result.saturated && result.utilization > HOT;
|
|
19664
|
+
const base = stub.latencyMs ?? (stub.p99Ms !== void 0 ? stub.p99Ms / (1 - SERVICE_SPREAD + SERVICE_SPREAD * tailFactor(0.99)) : 0);
|
|
19665
|
+
const hop = hopModel(base, 0);
|
|
19666
|
+
const at = (p) => hop.fixedMs + hop.tailMs * tailFactor(p);
|
|
19667
|
+
const availability2 = stub.availability !== void 0 ? stub.availability / 100 : 1;
|
|
19668
|
+
result.entries = system.entries.map((e) => ({ entry: e.id, p50Ms: at(0.5), p99Ms: at(0.99), availability: availability2 }));
|
|
19669
|
+
if (result.saturated) {
|
|
19670
|
+
result.warnings.push(`${label} cannot take what it is sent: you send ${sending}; it takes at most ${formatRps(rps)}.`);
|
|
19671
|
+
if (stub.p99Ms !== void 0) result.breaches.push({ requirement: `p99 < ${formatMs(stub.p99Ms)}`, message: `p99 is not held past ${formatRps(rps)}` });
|
|
19672
|
+
} else if (result.hot) result.warnings.push(`${label} runs hot under what it is sent: you send ${sending}; it runs hot past ${formatRps(rps * HOT)} and takes at most ${formatRps(rps)}.`);
|
|
19673
|
+
return result;
|
|
19674
|
+
}
|
|
19675
|
+
if (contract.source === "none" || !system.diagram) {
|
|
19676
|
+
result.entries = system.entries.map((e) => ({ entry: e.id, p50Ms: 0, p99Ms: 0, availability: 1 }));
|
|
19677
|
+
return result;
|
|
19678
|
+
}
|
|
19679
|
+
const team = system.diagram;
|
|
19680
|
+
const key = teamKey(system);
|
|
19681
|
+
const sentToTeam = shared?.useCases ?? /* @__PURE__ */ new Map();
|
|
19682
|
+
if (!shared) sentByUseCase(system, sent, sentToTeam);
|
|
19683
|
+
const sharedInboundRps = [...sentToTeam.values()].reduce((sum, rps) => sum + rps, 0);
|
|
19684
|
+
result.callers = shared?.callers ?? (totalInboundRps > 0 ? [{ path: system.id, rps: totalInboundRps }] : []);
|
|
19685
|
+
result.sharedInboundRps = sharedInboundRps;
|
|
19686
|
+
let cached = options.sharedSystems?.analyses.get(key);
|
|
19687
|
+
if (!cached) {
|
|
19688
|
+
const combined = sharedInboundRps > 0 ? withExtraTraffic(team, sentToTeam, system.loc) : team;
|
|
19689
|
+
const nested = { timeoutMs: options.timeoutMs, ...options.sharedSystems ? { sharedSystems: options.sharedSystems } : {} };
|
|
19690
|
+
cached = { combined, analysis: sharedInboundRps > 0 ? analyze(combined, nested) : contract.baseline };
|
|
19691
|
+
options.sharedSystems?.analyses.set(key, cached);
|
|
19692
|
+
}
|
|
19693
|
+
const { analysis } = cached;
|
|
19694
|
+
result.analysis = analysis;
|
|
19695
|
+
result.overlay = analysisOverlay(cached.combined, analysis);
|
|
19696
|
+
const busiest = analysis.nodes.reduce((best, n) => !best || n.utilization > best.utilization ? n : best, void 0);
|
|
19697
|
+
result.utilization = busiest?.utilization ?? 0;
|
|
19698
|
+
result.busiest = busiest?.id;
|
|
19699
|
+
result.saturated = !!busiest?.saturated;
|
|
19700
|
+
result.hot = !result.saturated && result.utilization > HOT;
|
|
19701
|
+
const byId = new Map(analysis.useCases.map((u) => [u.id, u]));
|
|
19702
|
+
result.entries = system.entries.map((e) => {
|
|
19703
|
+
const u = e.useCaseId !== void 0 ? byId.get(e.useCaseId) : void 0;
|
|
19704
|
+
return { entry: e.id, p50Ms: u?.percentiles.p50 ?? 0, p99Ms: u?.percentiles.p99 ?? 0, availability: u?.availability ?? 1 };
|
|
19705
|
+
});
|
|
19706
|
+
if (totalInboundRps === 0) return result;
|
|
19707
|
+
result.breaches = breachesOf(team, sentToTeam, analysis);
|
|
19708
|
+
const first = contract.entries.find((c) => inbound.some((e) => e.entry === c.entry && e.rps > 0));
|
|
19709
|
+
if (result.saturated && busiest) {
|
|
19710
|
+
const onOwn = first && first.saturatesAtRps === 0;
|
|
19711
|
+
const limit = first && Number.isFinite(first.saturatesAtRps) ? `; it takes at most ${formatRps(first.saturatesAtRps)} before its ${first.bottleneck} saturates` : "";
|
|
19712
|
+
result.warnings.push(
|
|
19713
|
+
onOwn ? `${label} is saturated on its own traffic, before what it is sent: its ${busiest.id} is at ${usageText(busiest)}.` : `${label} cannot take what it is sent: you send ${sending}${limit}. Its ${busiest.id} is at ${usageText(busiest)}.`
|
|
19714
|
+
);
|
|
19715
|
+
} else if (result.hot && busiest) {
|
|
19716
|
+
const limit = first && Number.isFinite(first.hotAtRps) ? `; it runs hot past ${formatRps(first.hotAtRps)}` : "";
|
|
19717
|
+
result.warnings.push(`${label} runs hot under what it is sent: you send ${sending}${limit}. Its ${busiest.id} is at ${usageText(busiest)}.`);
|
|
19718
|
+
}
|
|
19719
|
+
for (const b of result.breaches) result.warnings.push(`${label} breaks its SLA under what it is sent: ${b.message}.`);
|
|
19720
|
+
return result;
|
|
19721
|
+
}
|
|
19722
|
+
function withExtraTraffic(team, extra, loc) {
|
|
19723
|
+
const traffic = [...team.traffic ?? []];
|
|
19724
|
+
for (const [id, rps] of extra) {
|
|
19725
|
+
const u = team.useCases.find((x) => x.id === id);
|
|
19726
|
+
if (!u || rps <= 0) continue;
|
|
19727
|
+
const i = traffic.findIndex((t) => findUseCase(team, t.useCase) === u);
|
|
19728
|
+
if (i >= 0) traffic[i] = { ...traffic[i], rps: traffic[i].rps + rps };
|
|
19729
|
+
else traffic.push({ useCase: u.name, rps, loc });
|
|
19730
|
+
}
|
|
19731
|
+
return { ...team, traffic };
|
|
19732
|
+
}
|
|
19733
|
+
function breachesOf(team, sent, analysis) {
|
|
19734
|
+
const entered = /* @__PURE__ */ new Set();
|
|
19735
|
+
for (const [id, rps] of sent) {
|
|
19736
|
+
const u = team.useCases.find((x) => x.id === id);
|
|
19737
|
+
if (u && rps > 0) entered.add(u);
|
|
19738
|
+
}
|
|
19739
|
+
const nodes = new Map(analysis.nodes.map((n) => [n.id, n]));
|
|
19740
|
+
const results = new Map(analysis.useCases.map((u) => [u.id, u]));
|
|
19741
|
+
const breaches = [];
|
|
19742
|
+
for (const r of team.requirements ?? []) {
|
|
19743
|
+
if (r.kind !== "latency" && r.kind !== "availability") continue;
|
|
19744
|
+
const targets2 = r.useCase === void 0 ? [...entered] : [findUseCase(team, r.useCase)].filter((u) => !!u && entered.has(u));
|
|
19745
|
+
for (const u of targets2) {
|
|
19746
|
+
const ua = results.get(u.id);
|
|
19747
|
+
const under = `under ${formatRps(ua.rps)}`;
|
|
19748
|
+
if (r.kind === "availability") {
|
|
19749
|
+
const name2 = `availability of "${u.name}" \u2265 ${r.minPercent}%`;
|
|
19750
|
+
if (ua.availability * 100 < r.minPercent - 1e-9) breaches.push({ requirement: name2, message: `availability of "${u.name}" is ${formatAvailability(ua.availability)} ${under} (limit ${r.minPercent}%)` });
|
|
19751
|
+
continue;
|
|
19752
|
+
}
|
|
19753
|
+
const key = PERCENTILE_KEYS[r.percentile];
|
|
19754
|
+
const scenario = r.scenario !== void 0 ? findScenario2(u, r.scenario) : void 0;
|
|
19755
|
+
if (r.scenario !== void 0 && !scenario) continue;
|
|
19756
|
+
const what = `${key} of "${u.name}"${scenario ? ` scenario "${scenario.name}"` : ""}`;
|
|
19757
|
+
const name = `${what} < ${formatMs(r.maxMs)}`;
|
|
19758
|
+
const steps2 = scenario ? scenario.steps : u.scenarios.flatMap((s) => s.steps);
|
|
19759
|
+
const saturated = steps2.map((s) => nodes.get(s.toServiceId)).reduce((best, n) => n?.saturated && (!best || n.utilization > best.utilization) ? n : best, void 0);
|
|
19760
|
+
if (saturated) {
|
|
19761
|
+
breaches.push({ requirement: name, message: `${what}: ${saturated.id} is saturated (${usageText(saturated)}) ${under} (limit ${formatMs(r.maxMs)})` });
|
|
19762
|
+
continue;
|
|
19763
|
+
}
|
|
19764
|
+
const ms = scenario ? ua.scenarios[u.scenarios.indexOf(scenario)].percentiles[key] : ua.percentiles[key];
|
|
19765
|
+
if (ms >= r.maxMs) breaches.push({ requirement: name, message: `${what} is ${formatMs(ms)} ${under} (limit ${formatMs(r.maxMs)})` });
|
|
19766
|
+
}
|
|
19767
|
+
}
|
|
19768
|
+
return breaches;
|
|
19769
|
+
}
|
|
19770
|
+
|
|
19093
19771
|
// ../frontend/src/sim/analyze.ts
|
|
19094
19772
|
var DEFAULT_TIMEOUT_MS = 1e3;
|
|
19095
19773
|
var HOT = 0.7;
|
|
@@ -19247,6 +19925,7 @@ function usageText(n) {
|
|
|
19247
19925
|
return `bandwidth ${mb(n.bandwidthLoadMBps)} of ${mb(n.bandwidthCapacityMBps)}, ${formatPercent(n.bandwidthUtilization)}`;
|
|
19248
19926
|
}
|
|
19249
19927
|
if (n.writeScaling === "shards" && n.writeLoadRps > 0) {
|
|
19928
|
+
if (n.readLoadRps <= 0) return `writes ${part(n.writeLoadRps, n.writeCapacityRps, n.writeUtilization)}`;
|
|
19250
19929
|
return `reads ${part(n.readLoadRps, n.readCapacityRps, n.readUtilization)}; writes ${part(n.writeLoadRps, n.writeCapacityRps, n.writeUtilization)}`;
|
|
19251
19930
|
}
|
|
19252
19931
|
return part(n.loadRps, n.capacityRps, n.requestUtilization);
|
|
@@ -19332,6 +20011,7 @@ function analyze(diagram, options = {}) {
|
|
|
19332
20011
|
const load = computeLoad(diagram, traffic);
|
|
19333
20012
|
const egress = computeEgressGb(diagram, traffic, kindOf2);
|
|
19334
20013
|
const bytes = computeBytesPerSecond(diagram, traffic);
|
|
20014
|
+
const systems = diagram.systems?.length ? analyzeSystems(diagram, traffic, options) : void 0;
|
|
19335
20015
|
const hops = /* @__PURE__ */ new Map();
|
|
19336
20016
|
const nodeResults = nodes.map((n) => {
|
|
19337
20017
|
const p = profiles.get(n.id);
|
|
@@ -19370,12 +20050,13 @@ function analyze(diagram, options = {}) {
|
|
|
19370
20050
|
if (n.saturated) warnings.push(`'${n.id}' is saturated: ${usage}. ${writeBound(n) ? "Add shards for writes" : "Add replicas"} or take load off it.`);
|
|
19371
20051
|
else if (n.utilization > HOT) warnings.push(`'${n.id}' runs hot: ${usage}.`);
|
|
19372
20052
|
}
|
|
20053
|
+
for (const s of systems?.systems ?? []) warnings.push(...s.warnings);
|
|
19373
20054
|
const access = accessIn(diagram);
|
|
19374
20055
|
const pathOf = (hopsOfPath) => ({
|
|
19375
20056
|
parts: hopsOfPath.map(
|
|
19376
20057
|
(part) => part.map((h) => {
|
|
19377
20058
|
if (h.failed) return { fixedMs: timeoutOf(h.target), tailMs: 0 };
|
|
19378
|
-
const hop = hops.get(h.target) ?? { fixedMs: 0, tailMs: 0 };
|
|
20059
|
+
const hop = systems?.hop(h.target, h.step.toPort) ?? hops.get(h.target) ?? { fixedMs: 0, tailMs: 0 };
|
|
19379
20060
|
return { fixedMs: transferMs(h.step, bandwidthOf) + hop.fixedMs, tailMs: hop.tailMs };
|
|
19380
20061
|
})
|
|
19381
20062
|
)
|
|
@@ -19405,6 +20086,7 @@ function analyze(diagram, options = {}) {
|
|
|
19405
20086
|
const writes = mainWrites(u, shares, (step) => access(step) === "write");
|
|
19406
20087
|
const availabilityOf = (id) => {
|
|
19407
20088
|
const n = nodeById.get(id);
|
|
20089
|
+
if (!n && systems) return systemAvailability(u, id, systems.availability);
|
|
19408
20090
|
return !n ? 1 : writes.has(id) ? n.writeAvailability : n.availability;
|
|
19409
20091
|
};
|
|
19410
20092
|
return { id: u.id, name: u.name, rps, scenarios, percentiles, tailScenario, availability: useCaseAvailability(u, shares, availabilityOf) };
|
|
@@ -19415,9 +20097,15 @@ function analyze(diagram, options = {}) {
|
|
|
19415
20097
|
totalCostUsd: nodeResults.reduce((sum, n) => sum + n.costUsd, 0),
|
|
19416
20098
|
totalEgressUsd: nodeResults.reduce((sum, n) => sum + n.egressUsd, 0),
|
|
19417
20099
|
singlePointsOfFailure: singlePointsOfFailure(diagram),
|
|
19418
|
-
warnings
|
|
20100
|
+
warnings,
|
|
20101
|
+
...systems ? { systems: systems.systems } : {}
|
|
19419
20102
|
};
|
|
19420
20103
|
}
|
|
20104
|
+
function systemAvailability(u, id, availability2) {
|
|
20105
|
+
let result = 1;
|
|
20106
|
+
for (const s of u.scenarios) for (const step of s.steps) if (step.toServiceId === id) result = Math.min(result, availability2(id, step.toPort) ?? 1);
|
|
20107
|
+
return result;
|
|
20108
|
+
}
|
|
19421
20109
|
function mainWrites(useCase, shares, isWrite) {
|
|
19422
20110
|
if (useCase.scenarios.length === 0) return /* @__PURE__ */ new Set();
|
|
19423
20111
|
const path = criticalPath(useCase, useCase.scenarios[mainScenarioIndex(shares)]).flat();
|
|
@@ -19467,6 +20155,7 @@ function runTests(diagram, analysis = analyze(diagram), options = {}) {
|
|
|
19467
20155
|
const { passed, message, hint } = requirement(run2, r);
|
|
19468
20156
|
return { id: `req:${i + 1}`, name: requirementName(r), category: CATEGORY[r.kind], passed, message, hint, loc: r.loc };
|
|
19469
20157
|
});
|
|
20158
|
+
results.push(...systemResults(diagram, analysis));
|
|
19470
20159
|
const names = /* @__PURE__ */ new Map();
|
|
19471
20160
|
for (const test of diagram.tests ?? []) {
|
|
19472
20161
|
const n = (names.get(test.name) ?? 0) + 1;
|
|
@@ -19475,6 +20164,26 @@ function runTests(diagram, analysis = analyze(diagram), options = {}) {
|
|
|
19475
20164
|
}
|
|
19476
20165
|
return results.map(({ hint, ...r }) => r.passed || !hint ? r : { ...r, hint });
|
|
19477
20166
|
}
|
|
20167
|
+
function systemResults(diagram, analysis) {
|
|
20168
|
+
return (analysis.systems ?? []).filter((s) => s.totalInboundRps > 0 && s.contract.source !== "none").map((s) => {
|
|
20169
|
+
const system = diagram.systems?.find((x) => x.id === s.id);
|
|
20170
|
+
const node = diagram.nodes.find((n) => n.id === s.id);
|
|
20171
|
+
const team = node?.ownerTeam ? `the ${node.ownerTeam} team` : `the owners of ${s.id}`;
|
|
20172
|
+
const problem = s.warnings.find((w) => !w.includes(" runs hot "));
|
|
20173
|
+
const sat = s.contract.entries.find((e) => s.inbound.some((i) => i.entry === e.entry && i.rps > 0));
|
|
20174
|
+
const room = sat && Number.isFinite(sat.saturatesAtRps) ? `; it takes at most ${formatRps(sat.saturatesAtRps)}` : "";
|
|
20175
|
+
const bottleneck = s.saturated && s.busiest ? ` (their bottleneck is ${s.busiest})` : "";
|
|
20176
|
+
return {
|
|
20177
|
+
id: `system:${s.id}`,
|
|
20178
|
+
name: `${s.id} takes what it is sent`,
|
|
20179
|
+
category: s.breaches.some((b) => b.requirement.startsWith("availability")) && !s.saturated ? "availability" : "latency",
|
|
20180
|
+
passed: !problem,
|
|
20181
|
+
message: problem ?? `'${s.id}' takes ${formatRps(s.totalInboundRps)}${(s.sharedInboundRps ?? 0) > s.totalInboundRps * (1 + 1e-9) ? ` (of the ${formatRps(s.sharedInboundRps)} it gets)` : ""}${s.contract.source === "stub" ? "" : " on top of its own traffic"} and holds its requirements${room}`,
|
|
20182
|
+
...problem ? { hint: `Send it less, or ask ${team} to add capacity for it${bottleneck}` } : {},
|
|
20183
|
+
...system ? { loc: system.loc } : {}
|
|
20184
|
+
};
|
|
20185
|
+
});
|
|
20186
|
+
}
|
|
19478
20187
|
var CATEGORY = {
|
|
19479
20188
|
latency: "latency",
|
|
19480
20189
|
availability: "availability",
|
|
@@ -20049,8 +20758,126 @@ function displayPath2(file) {
|
|
|
20049
20758
|
}
|
|
20050
20759
|
|
|
20051
20760
|
// src/imports.ts
|
|
20052
|
-
var
|
|
20053
|
-
var
|
|
20761
|
+
var import_node_fs5 = require("node:fs");
|
|
20762
|
+
var import_node_path6 = require("node:path");
|
|
20763
|
+
|
|
20764
|
+
// ../frontend/src/dsl/imports.ts
|
|
20765
|
+
var ImportError = class extends Error {
|
|
20766
|
+
name = "ImportError";
|
|
20767
|
+
};
|
|
20768
|
+
var MAX_SPEC_LENGTH = 1024;
|
|
20769
|
+
var SEGMENT = /^[A-Za-z0-9_][A-Za-z0-9._ +-]*$/;
|
|
20770
|
+
var HOST = /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)*(?::\d{1,5})?$/;
|
|
20771
|
+
var REPO_SEGMENT = /^[A-Za-z0-9_][A-Za-z0-9._-]*$/;
|
|
20772
|
+
var REF = /^[A-Za-z0-9_][A-Za-z0-9._/-]*$/;
|
|
20773
|
+
var SCHEME = /^([A-Za-z][A-Za-z0-9+.-]+):/;
|
|
20774
|
+
function isRemoteSpec(spec) {
|
|
20775
|
+
return SCHEME.test(spec);
|
|
20776
|
+
}
|
|
20777
|
+
function isAbsolutePath(path) {
|
|
20778
|
+
return /^[\\/]/.test(path) || /^[A-Za-z]:/.test(path);
|
|
20779
|
+
}
|
|
20780
|
+
function checkRef(ref, spec) {
|
|
20781
|
+
if (!REF.test(ref) || ref.length > 200 || ref.includes("..") || ref.includes("//") || ref.endsWith("/") || ref.endsWith(".") || ref.endsWith(".lock") || ref.split("/").some((p) => p.startsWith("."))) {
|
|
20782
|
+
throw new ImportError(`'${spec}' has an invalid ref '${ref}'; use a tag, branch or commit made of letters, digits, '.', '_', '-' and '/'`);
|
|
20783
|
+
}
|
|
20784
|
+
}
|
|
20785
|
+
function repoPathSegments(path, spec) {
|
|
20786
|
+
const parts = path.split("/");
|
|
20787
|
+
if (!path || parts.some((p) => !SEGMENT.test(p) || p === "." || p === "..")) {
|
|
20788
|
+
throw new ImportError(`'${spec}' has an invalid path in the repository; use a relative path like dir/file.proschi, without '..'`);
|
|
20789
|
+
}
|
|
20790
|
+
return parts;
|
|
20791
|
+
}
|
|
20792
|
+
function parseRemoteSpec(spec) {
|
|
20793
|
+
const scheme = SCHEME.exec(spec)?.[1].toLowerCase();
|
|
20794
|
+
if (!scheme) return void 0;
|
|
20795
|
+
if (spec.length > MAX_SPEC_LENGTH) throw new ImportError(`Import source is longer than ${MAX_SPEC_LENGTH} characters`);
|
|
20796
|
+
if (/[\s\0\\]/.test(spec)) throw new ImportError(`'${spec}' contains whitespace or a backslash`);
|
|
20797
|
+
if (scheme === "git") {
|
|
20798
|
+
const body = spec.slice(4);
|
|
20799
|
+
const at = body.lastIndexOf("@");
|
|
20800
|
+
const split2 = body.indexOf("//");
|
|
20801
|
+
if (at === -1 || split2 === -1 || split2 > at) {
|
|
20802
|
+
throw new ImportError(`'${spec}' is not a git source; write git:<host>/<org>/<repo>//<path/in/repo>@<ref>`);
|
|
20803
|
+
}
|
|
20804
|
+
const repo = body.slice(0, split2);
|
|
20805
|
+
const path = body.slice(split2 + 2, at);
|
|
20806
|
+
const ref = body.slice(at + 1);
|
|
20807
|
+
const [host, ...names] = repo.split("/");
|
|
20808
|
+
if (!HOST.test(host) || names.length < 2 || names.some((n) => !REPO_SEGMENT.test(n) || n.endsWith("."))) {
|
|
20809
|
+
throw new ImportError(`'${spec}' names an invalid repository '${repo}'; write <host>/<org>/<repo>`);
|
|
20810
|
+
}
|
|
20811
|
+
repoPathSegments(path, spec);
|
|
20812
|
+
checkRef(ref, spec);
|
|
20813
|
+
return { kind: "git", repo, path, ref };
|
|
20814
|
+
}
|
|
20815
|
+
if (scheme === "http") throw new ImportError(`'${spec}' uses http://; only https:// imports are allowed`);
|
|
20816
|
+
if (scheme !== "https") throw new ImportError(`'${spec}' is not a supported import source; use a relative path, git: or https://`);
|
|
20817
|
+
let url;
|
|
20818
|
+
try {
|
|
20819
|
+
url = new URL(spec);
|
|
20820
|
+
} catch {
|
|
20821
|
+
throw new ImportError(`'${spec}' is not a valid URL`);
|
|
20822
|
+
}
|
|
20823
|
+
if (url.username || url.password) throw new ImportError(`'${spec}' contains credentials; git and https imports use your own credentials, never ones in the diagram`);
|
|
20824
|
+
if (url.hash) throw new ImportError(`'${spec}' has a #fragment; remove it`);
|
|
20825
|
+
if (!HOST.test(url.host)) throw new ImportError(`'${spec}' has an invalid host`);
|
|
20826
|
+
return { kind: "https", url: url.href };
|
|
20827
|
+
}
|
|
20828
|
+
function formatRemoteSpec(spec) {
|
|
20829
|
+
return spec.kind === "git" ? `git:${spec.repo}//${spec.path}@${spec.ref}` : spec.url;
|
|
20830
|
+
}
|
|
20831
|
+
function resolveImportPath(fromPath, importPath) {
|
|
20832
|
+
if (importPath.includes("\0")) throw new ImportError(`'${importPath.replace(/\0/g, "")}' contains a NUL character`);
|
|
20833
|
+
const remote = parseRemoteSpec(importPath);
|
|
20834
|
+
if (remote) return formatRemoteSpec(remote);
|
|
20835
|
+
if (isAbsolutePath(importPath)) throw new ImportError(`'${importPath}' is an absolute path; imports must be relative to the importing file`);
|
|
20836
|
+
const from = fromPath && isRemoteSpec(fromPath) ? parseRemoteSpec(fromPath) : void 0;
|
|
20837
|
+
if (from?.kind === "git") {
|
|
20838
|
+
const parts2 = from.path.split("/").slice(0, -1);
|
|
20839
|
+
for (const part of importPath.split("/")) {
|
|
20840
|
+
if (part === "" || part === ".") continue;
|
|
20841
|
+
if (part !== "..") parts2.push(part);
|
|
20842
|
+
else if (parts2.length > 0) parts2.pop();
|
|
20843
|
+
else throw new ImportError(`'${importPath}' leaves the repository ${from.repo}`);
|
|
20844
|
+
}
|
|
20845
|
+
repoPathSegments(parts2.join("/"), importPath);
|
|
20846
|
+
return formatRemoteSpec({ ...from, path: parts2.join("/") });
|
|
20847
|
+
}
|
|
20848
|
+
if (from?.kind === "https") {
|
|
20849
|
+
const base = new URL(from.url);
|
|
20850
|
+
const url = new URL(importPath, base);
|
|
20851
|
+
if (url.origin !== base.origin) throw new ImportError(`'${importPath}' leaves ${base.origin}`);
|
|
20852
|
+
return formatRemoteSpec(parseRemoteSpec(url.href));
|
|
20853
|
+
}
|
|
20854
|
+
const parts = !fromPath ? [] : fromPath.split("/").slice(0, -1);
|
|
20855
|
+
for (const part of importPath.split("/")) {
|
|
20856
|
+
if (part === "" || part === ".") continue;
|
|
20857
|
+
if (part === ".." && parts.length > 0 && parts[parts.length - 1] !== "..") parts.pop();
|
|
20858
|
+
else parts.push(part);
|
|
20859
|
+
}
|
|
20860
|
+
return parts.join("/");
|
|
20861
|
+
}
|
|
20862
|
+
function joinImportPath(fromPath, importPath) {
|
|
20863
|
+
try {
|
|
20864
|
+
return resolveImportPath(fromPath, importPath);
|
|
20865
|
+
} catch (e) {
|
|
20866
|
+
if (e instanceof ImportError) return void 0;
|
|
20867
|
+
throw e;
|
|
20868
|
+
}
|
|
20869
|
+
}
|
|
20870
|
+
function mapResolver(files) {
|
|
20871
|
+
const paths = Object.keys(files).filter((p) => !isRemoteSpec(p));
|
|
20872
|
+
return (importPath, fromPath) => {
|
|
20873
|
+
const path = resolveImportPath(fromPath, importPath);
|
|
20874
|
+
if (Object.prototype.hasOwnProperty.call(files, path)) return { path, source: files[path] };
|
|
20875
|
+
if (isRemoteSpec(path)) return void 0;
|
|
20876
|
+
const name = path.split("/").pop();
|
|
20877
|
+
const sameName = paths.filter((p) => p.split("/").pop() === name);
|
|
20878
|
+
return sameName.length === 1 ? { path: sameName[0], source: files[sameName[0]] } : void 0;
|
|
20879
|
+
};
|
|
20880
|
+
}
|
|
20054
20881
|
|
|
20055
20882
|
// src/simulation.ts
|
|
20056
20883
|
var SIM_USAGE = ` proschi test [--format text|github|json] <file|dir>...
|
|
@@ -20217,21 +21044,201 @@ function capacityReport(diagram, analysis, file) {
|
|
|
20217
21044
|
return lines.join("\n");
|
|
20218
21045
|
}
|
|
20219
21046
|
|
|
21047
|
+
// src/lock.ts
|
|
21048
|
+
var import_node_crypto = require("node:crypto");
|
|
21049
|
+
var import_node_fs4 = require("node:fs");
|
|
21050
|
+
var import_node_path5 = require("node:path");
|
|
21051
|
+
|
|
21052
|
+
// src/lockFormat.ts
|
|
21053
|
+
var LOCK_FILE = "proschi.lock";
|
|
21054
|
+
var CACHE_DIR = ".proschi/cache";
|
|
21055
|
+
var HASH = /^[0-9a-f]{64}$/;
|
|
21056
|
+
var COMMIT = /^[0-9a-f]{40}([0-9a-f]{24})?$/;
|
|
21057
|
+
var emptyLock = () => ({ version: 1, git: {}, https: {} });
|
|
21058
|
+
var gitKey = (repo, ref) => `${repo}@${ref}`;
|
|
21059
|
+
var remoteError = {
|
|
21060
|
+
noLock: (spec) => new ImportError(`'${spec}' is a remote import and there is no ${LOCK_FILE}; run proschi imports sync`),
|
|
21061
|
+
notLocked: (spec) => new ImportError(`'${spec}' is not in ${LOCK_FILE}; run proschi imports sync`),
|
|
21062
|
+
notCached: (spec) => new ImportError(`'${spec}' is not in the import cache (${CACHE_DIR}); run proschi imports sync`),
|
|
21063
|
+
altered: (spec) => new ImportError(`The cached copy of '${spec}' does not match its hash in ${LOCK_FILE}; run proschi imports sync`)
|
|
21064
|
+
};
|
|
21065
|
+
var isRecord = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
21066
|
+
function parseLock(text) {
|
|
21067
|
+
const invalid = (why) => new ImportError(`Invalid ${LOCK_FILE}: ${why}; delete it and run proschi imports sync`);
|
|
21068
|
+
let json2;
|
|
21069
|
+
try {
|
|
21070
|
+
json2 = JSON.parse(text);
|
|
21071
|
+
} catch (e) {
|
|
21072
|
+
throw invalid(e.message);
|
|
21073
|
+
}
|
|
21074
|
+
if (!isRecord(json2) || json2.version !== 1) throw invalid('expected {"version": 1, \u2026}');
|
|
21075
|
+
const lock = emptyLock();
|
|
21076
|
+
const git = json2.git ?? {};
|
|
21077
|
+
const https = json2.https ?? {};
|
|
21078
|
+
if (!isRecord(git) || !isRecord(https)) throw invalid('"git" and "https" must be objects');
|
|
21079
|
+
for (const [key, entry] of Object.entries(git)) {
|
|
21080
|
+
const at = key.lastIndexOf("@");
|
|
21081
|
+
const valid = at > 0 && isRecord(entry) && typeof entry.commit === "string" && COMMIT.test(entry.commit) && isRecord(entry.files) && Object.entries(entry.files).every(([path, hash]) => typeof hash === "string" && HASH.test(hash) && isGitSpec(`git:${key.slice(0, at)}//${path}@${key.slice(at + 1)}`));
|
|
21082
|
+
if (!valid) throw invalid(`bad git entry '${key}'`);
|
|
21083
|
+
lock.git[key] = { commit: entry.commit, files: { ...entry.files } };
|
|
21084
|
+
}
|
|
21085
|
+
for (const [url, hash] of Object.entries(https)) {
|
|
21086
|
+
if (typeof hash !== "string" || !HASH.test(hash) || parseRemoteSpec(url)?.kind !== "https") throw invalid(`bad https entry '${url}'`);
|
|
21087
|
+
lock.https[url] = hash;
|
|
21088
|
+
}
|
|
21089
|
+
return lock;
|
|
21090
|
+
}
|
|
21091
|
+
function isGitSpec(spec) {
|
|
21092
|
+
try {
|
|
21093
|
+
return parseRemoteSpec(spec)?.kind === "git";
|
|
21094
|
+
} catch {
|
|
21095
|
+
return false;
|
|
21096
|
+
}
|
|
21097
|
+
}
|
|
21098
|
+
function lockedHash(lock, spec) {
|
|
21099
|
+
const remote = parseRemoteSpec(spec);
|
|
21100
|
+
if (remote?.kind === "git") return lock.git[gitKey(remote.repo, remote.ref)]?.files[remote.path];
|
|
21101
|
+
if (remote?.kind === "https") return lock.https[remote.url];
|
|
21102
|
+
return void 0;
|
|
21103
|
+
}
|
|
21104
|
+
|
|
21105
|
+
// src/lock.ts
|
|
21106
|
+
var sha256 = (bytes) => (0, import_node_crypto.createHash)("sha256").update(bytes).digest("hex");
|
|
21107
|
+
function readLock(root) {
|
|
21108
|
+
const file = (0, import_node_path5.join)(root, LOCK_FILE);
|
|
21109
|
+
return (0, import_node_fs4.existsSync)(file) ? parseLock((0, import_node_fs4.readFileSync)(file, "utf8")) : void 0;
|
|
21110
|
+
}
|
|
21111
|
+
var sorted = (o, map = (v) => v) => Object.fromEntries(Object.keys(o).sort().map((k) => [k, map(o[k])]));
|
|
21112
|
+
function formatLock(lock) {
|
|
21113
|
+
return JSON.stringify({ version: 1, git: sorted(lock.git, (e) => ({ commit: e.commit, files: sorted(e.files) })), https: sorted(lock.https) }, null, 2) + "\n";
|
|
21114
|
+
}
|
|
21115
|
+
function writeLock(root, lock) {
|
|
21116
|
+
(0, import_node_fs4.writeFileSync)((0, import_node_path5.join)(root, LOCK_FILE), formatLock(lock));
|
|
21117
|
+
}
|
|
21118
|
+
var cacheFile = (root, hash) => (0, import_node_path5.join)(root, CACHE_DIR, hash);
|
|
21119
|
+
function readCached(root, hash, spec) {
|
|
21120
|
+
if (!HASH.test(hash)) return void 0;
|
|
21121
|
+
const file = cacheFile(root, hash);
|
|
21122
|
+
if (!(0, import_node_fs4.existsSync)(file)) return void 0;
|
|
21123
|
+
const bytes = (0, import_node_fs4.readFileSync)(file);
|
|
21124
|
+
if (sha256(bytes) !== hash) throw remoteError.altered(spec);
|
|
21125
|
+
return bytes;
|
|
21126
|
+
}
|
|
21127
|
+
function writeCached(root, bytes) {
|
|
21128
|
+
const hash = sha256(bytes);
|
|
21129
|
+
const dir = (0, import_node_path5.join)(root, CACHE_DIR);
|
|
21130
|
+
(0, import_node_fs4.mkdirSync)(dir, { recursive: true });
|
|
21131
|
+
const tmp = (0, import_node_path5.join)(dir, `${hash}.${process.pid}.tmp`);
|
|
21132
|
+
(0, import_node_fs4.writeFileSync)(tmp, bytes);
|
|
21133
|
+
(0, import_node_fs4.renameSync)(tmp, cacheFile(root, hash));
|
|
21134
|
+
return hash;
|
|
21135
|
+
}
|
|
21136
|
+
|
|
20220
21137
|
// src/imports.ts
|
|
20221
21138
|
function displayPath3(file, cwd = process.cwd()) {
|
|
20222
|
-
const abs = (0,
|
|
20223
|
-
const rel = (0,
|
|
20224
|
-
return rel && !rel.startsWith("..") && !(0,
|
|
20225
|
-
}
|
|
20226
|
-
|
|
20227
|
-
|
|
20228
|
-
|
|
20229
|
-
|
|
20230
|
-
|
|
21139
|
+
const abs = (0, import_node_path6.resolve)(cwd, file);
|
|
21140
|
+
const rel = (0, import_node_path6.relative)(cwd, abs);
|
|
21141
|
+
return rel && !rel.startsWith("..") && !(0, import_node_path6.isAbsolute)(rel) ? rel : abs;
|
|
21142
|
+
}
|
|
21143
|
+
var MAX_IMPORT_BYTES = 1e6;
|
|
21144
|
+
var MAX_IMPORT_FILES = 500;
|
|
21145
|
+
var MAX_IMPORT_DEPTH = 32;
|
|
21146
|
+
function projectRoot(entry) {
|
|
21147
|
+
const dir = (0, import_node_path6.dirname)((0, import_node_path6.resolve)(entry));
|
|
21148
|
+
const config = findConfigFile(dir);
|
|
21149
|
+
if (config) return (0, import_node_fs5.realpathSync)((0, import_node_path6.dirname)(config));
|
|
21150
|
+
for (let current = dir; ; current = (0, import_node_path6.dirname)(current)) {
|
|
21151
|
+
if ((0, import_node_fs5.existsSync)((0, import_node_path6.join)(current, ".git"))) return (0, import_node_fs5.realpathSync)(current);
|
|
21152
|
+
if ((0, import_node_path6.dirname)(current) === current) break;
|
|
21153
|
+
}
|
|
21154
|
+
return (0, import_node_fs5.realpathSync)((0, import_node_fs5.existsSync)(dir) ? dir : process.cwd());
|
|
21155
|
+
}
|
|
21156
|
+
var isInside = (root, path) => {
|
|
21157
|
+
const rel = (0, import_node_path6.relative)(root, path);
|
|
21158
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${import_node_path6.sep}`) && !(0, import_node_path6.isAbsolute)(rel);
|
|
21159
|
+
};
|
|
21160
|
+
function realLocation(path) {
|
|
21161
|
+
let existing = path;
|
|
21162
|
+
while (!(0, import_node_fs5.existsSync)(existing) && (0, import_node_path6.dirname)(existing) !== existing) existing = (0, import_node_path6.dirname)(existing);
|
|
21163
|
+
return (0, import_node_path6.join)((0, import_node_fs5.realpathSync)(existing), (0, import_node_path6.relative)(existing, path));
|
|
21164
|
+
}
|
|
21165
|
+
var tooLarge = (what) => new ImportError(`'${what}' is larger than ${MAX_IMPORT_BYTES / 1e6} MB`);
|
|
21166
|
+
function fileResolver({ entry, read = () => void 0, toPath = (p) => p, remote } = {}) {
|
|
21167
|
+
const root = projectRoot(entry ?? (0, import_node_path6.join)(process.cwd(), "stdin.proschi"));
|
|
21168
|
+
const errors = /* @__PURE__ */ new Map();
|
|
21169
|
+
const levels = /* @__PURE__ */ new Map();
|
|
21170
|
+
let lock = null;
|
|
21171
|
+
const fromCache = (spec) => {
|
|
21172
|
+
if (lock === null) lock = readLock(root);
|
|
21173
|
+
if (!lock) throw remoteError.noLock(spec);
|
|
21174
|
+
const hash = lockedHash(lock, spec);
|
|
21175
|
+
if (!hash) throw remoteError.notLocked(spec);
|
|
21176
|
+
const bytes = readCached(root, hash, spec);
|
|
21177
|
+
if (!bytes) throw remoteError.notCached(spec);
|
|
21178
|
+
return bytes.toString("utf8");
|
|
20231
21179
|
};
|
|
21180
|
+
const loadRemote = remote ?? fromCache;
|
|
21181
|
+
const found = (path, source2, level) => {
|
|
21182
|
+
if (!levels.has(path)) {
|
|
21183
|
+
if (levels.size >= MAX_IMPORT_FILES) throw new ImportError(`More than ${MAX_IMPORT_FILES} imported files; split the diagram`);
|
|
21184
|
+
levels.set(path, level);
|
|
21185
|
+
}
|
|
21186
|
+
return { path, source: source2 };
|
|
21187
|
+
};
|
|
21188
|
+
const resolveOne = (importPath, fromPath) => {
|
|
21189
|
+
const level = (fromPath === void 0 ? 0 : levels.get(fromPath) ?? 0) + 1;
|
|
21190
|
+
if (level > MAX_IMPORT_DEPTH) throw new ImportError(`Imports nest more than ${MAX_IMPORT_DEPTH} files deep`);
|
|
21191
|
+
if (isRemoteSpec(importPath) || fromPath !== void 0 && isRemoteSpec(fromPath)) {
|
|
21192
|
+
const spec = resolveImportPath(fromPath, importPath);
|
|
21193
|
+
const source2 = loadRemote(spec);
|
|
21194
|
+
if (source2 === void 0) return void 0;
|
|
21195
|
+
if (Buffer.byteLength(source2) > MAX_IMPORT_BYTES) throw tooLarge(spec);
|
|
21196
|
+
return found(spec, source2, level);
|
|
21197
|
+
}
|
|
21198
|
+
resolveImportPath(void 0, importPath);
|
|
21199
|
+
const abs = (0, import_node_path6.resolve)(fromPath ? (0, import_node_path6.dirname)((0, import_node_path6.resolve)(fromPath)) : root, importPath);
|
|
21200
|
+
if (!isInside(root, realLocation(abs))) {
|
|
21201
|
+
const link = (0, import_node_fs5.existsSync)(abs) && (0, import_node_fs5.lstatSync)(abs).isSymbolicLink();
|
|
21202
|
+
throw new ImportError(
|
|
21203
|
+
link ? `'${importPath}' is a symlink to a file outside the project root ${displayPath3(root)}` : `'${importPath}' is outside the project root ${displayPath3(root)}; imports must stay inside the folder of proschi.json (else the git root)`
|
|
21204
|
+
);
|
|
21205
|
+
}
|
|
21206
|
+
const text = read(abs);
|
|
21207
|
+
if (text !== void 0) {
|
|
21208
|
+
if (Buffer.byteLength(text) > MAX_IMPORT_BYTES) throw tooLarge(importPath);
|
|
21209
|
+
return found(toPath(abs), text, level);
|
|
21210
|
+
}
|
|
21211
|
+
if (!(0, import_node_fs5.existsSync)(abs)) return void 0;
|
|
21212
|
+
const stat = (0, import_node_fs5.statSync)(abs);
|
|
21213
|
+
if (!stat.isFile()) return void 0;
|
|
21214
|
+
if (stat.size > MAX_IMPORT_BYTES) throw tooLarge(importPath);
|
|
21215
|
+
return found(toPath(abs), (0, import_node_fs5.readFileSync)((0, import_node_fs5.realpathSync)(abs), "utf8"), level);
|
|
21216
|
+
};
|
|
21217
|
+
const resolver = ((importPath, fromPath) => {
|
|
21218
|
+
try {
|
|
21219
|
+
return resolveOne(importPath, fromPath);
|
|
21220
|
+
} catch (e) {
|
|
21221
|
+
if (e instanceof ImportError) errors.set(`${fromPath ?? ""}\0${importPath}`, e.message);
|
|
21222
|
+
throw e;
|
|
21223
|
+
}
|
|
21224
|
+
});
|
|
21225
|
+
resolver.root = root;
|
|
21226
|
+
resolver.errors = errors;
|
|
21227
|
+
resolver.explain = (result) => {
|
|
21228
|
+
for (const i of result.imports ?? []) {
|
|
21229
|
+
if (i.resolved !== void 0) continue;
|
|
21230
|
+
const reason = errors.get(`${i.loc.file ?? entry ?? ""}\0${i.path}`);
|
|
21231
|
+
const d = reason && result.diagnostics.find((d2) => d2.file === i.loc.file && d2.line === i.loc.line && d2.col === i.loc.col && d2.message === `Cannot find '${i.path}'`);
|
|
21232
|
+
if (d) d.message = reason;
|
|
21233
|
+
}
|
|
21234
|
+
return result;
|
|
21235
|
+
};
|
|
21236
|
+
return resolver;
|
|
20232
21237
|
}
|
|
20233
21238
|
function parseFile(file) {
|
|
20234
|
-
|
|
21239
|
+
const path = displayPath3(file);
|
|
21240
|
+
const resolver = fileResolver({ entry: path, toPath: (p) => displayPath3(p) });
|
|
21241
|
+
return resolver.explain(parse((0, import_node_fs5.readFileSync)(file, "utf8"), { path, resolve: resolver }));
|
|
20235
21242
|
}
|
|
20236
21243
|
function checkFiles(files, extra) {
|
|
20237
21244
|
const byFile = /* @__PURE__ */ new Map();
|
|
@@ -20253,8 +21260,8 @@ function checkFiles(files, extra) {
|
|
|
20253
21260
|
}
|
|
20254
21261
|
|
|
20255
21262
|
// src/problem.ts
|
|
20256
|
-
var
|
|
20257
|
-
var
|
|
21263
|
+
var import_node_fs6 = require("node:fs");
|
|
21264
|
+
var import_node_path7 = require("node:path");
|
|
20258
21265
|
|
|
20259
21266
|
// ../frontend/src/practice/frontMatter.ts
|
|
20260
21267
|
var FrontMatterError = class extends Error {
|
|
@@ -20370,27 +21377,6 @@ function parseFrontMatter(text) {
|
|
|
20370
21377
|
return { data, body: lines.slice(end + 1).join("\n").trim() };
|
|
20371
21378
|
}
|
|
20372
21379
|
|
|
20373
|
-
// ../frontend/src/dsl/imports.ts
|
|
20374
|
-
function joinImportPath(fromPath, importPath) {
|
|
20375
|
-
const parts = importPath.startsWith("/") || !fromPath ? [] : fromPath.split("/").slice(0, -1);
|
|
20376
|
-
for (const part of importPath.split("/")) {
|
|
20377
|
-
if (part === "" || part === ".") continue;
|
|
20378
|
-
if (part === ".." && parts.length > 0 && parts[parts.length - 1] !== "..") parts.pop();
|
|
20379
|
-
else parts.push(part);
|
|
20380
|
-
}
|
|
20381
|
-
return parts.join("/");
|
|
20382
|
-
}
|
|
20383
|
-
function mapResolver(files) {
|
|
20384
|
-
const paths = Object.keys(files);
|
|
20385
|
-
return (importPath, fromPath) => {
|
|
20386
|
-
const path = joinImportPath(fromPath, importPath);
|
|
20387
|
-
if (Object.prototype.hasOwnProperty.call(files, path)) return { path, source: files[path] };
|
|
20388
|
-
const name = path.split("/").pop();
|
|
20389
|
-
const sameName = paths.filter((p) => p.split("/").pop() === name);
|
|
20390
|
-
return sameName.length === 1 ? { path: sameName[0], source: files[sameName[0]] } : void 0;
|
|
20391
|
-
};
|
|
20392
|
-
}
|
|
20393
|
-
|
|
20394
21380
|
// ../frontend/src/playground/imports.ts
|
|
20395
21381
|
function filesResolver(files, rootPath) {
|
|
20396
21382
|
const resolve8 = mapResolver(files);
|
|
@@ -21276,7 +22262,7 @@ function readTopics(text, file = "tags.json") {
|
|
|
21276
22262
|
return { id: topic.id, title: topic.title, summary: topic.summary };
|
|
21277
22263
|
});
|
|
21278
22264
|
}
|
|
21279
|
-
function
|
|
22265
|
+
function readLock2(text) {
|
|
21280
22266
|
const ids = [];
|
|
21281
22267
|
const lines = /* @__PURE__ */ new Map();
|
|
21282
22268
|
text.split("\n").forEach((raw, i) => {
|
|
@@ -21291,7 +22277,7 @@ var LOCK_HEADER = `# Every card id ever published, sorted. Review history is sto
|
|
|
21291
22277
|
# id is never deleted or reused: retire a card with \`status: retired\` instead.
|
|
21292
22278
|
# \`proschi cards lock\` adds new cards' ids (docs/CARDS.md).
|
|
21293
22279
|
`;
|
|
21294
|
-
function
|
|
22280
|
+
function writeLock2(existing, ids) {
|
|
21295
22281
|
return LOCK_HEADER + [.../* @__PURE__ */ new Set([...existing, ...ids])].sort().join("\n") + "\n";
|
|
21296
22282
|
}
|
|
21297
22283
|
function compareCards(a, b) {
|
|
@@ -21729,43 +22715,43 @@ var PROBLEM_HELP = `problem Practice problems, one folder each (docs/PRACTICE.md
|
|
|
21729
22715
|
passes).
|
|
21730
22716
|
Exits with 1 on any violation.
|
|
21731
22717
|
new scaffolds a folder that already passes check, with TODOs.`;
|
|
21732
|
-
var REPO_PROBLEMS = (0,
|
|
22718
|
+
var REPO_PROBLEMS = (0, import_node_path7.join)("frontend", "src", "practice", "problems");
|
|
21733
22719
|
function findProblemsDir(cwd = process.cwd()) {
|
|
21734
|
-
for (let dir = (0,
|
|
21735
|
-
const candidate = (0,
|
|
21736
|
-
if ((0,
|
|
21737
|
-
if ((0,
|
|
22720
|
+
for (let dir = (0, import_node_path7.resolve)(cwd); ; dir = (0, import_node_path7.dirname)(dir)) {
|
|
22721
|
+
const candidate = (0, import_node_path7.join)(dir, REPO_PROBLEMS);
|
|
22722
|
+
if ((0, import_node_fs6.existsSync)(candidate) && (0, import_node_fs6.statSync)(candidate).isDirectory()) return candidate;
|
|
22723
|
+
if ((0, import_node_path7.dirname)(dir) === dir) return void 0;
|
|
21738
22724
|
}
|
|
21739
22725
|
}
|
|
21740
22726
|
function readProblemFolder(folder) {
|
|
21741
22727
|
const files = {};
|
|
21742
|
-
for (const entry of (0,
|
|
22728
|
+
for (const entry of (0, import_node_fs6.readdirSync)(folder, { withFileTypes: true })) {
|
|
21743
22729
|
if (entry.name.startsWith(".")) continue;
|
|
21744
|
-
if (entry.isFile()) files[entry.name] = (0,
|
|
22730
|
+
if (entry.isFile()) files[entry.name] = (0, import_node_fs6.readFileSync)((0, import_node_path7.join)(folder, entry.name), "utf8");
|
|
21745
22731
|
else if (entry.isDirectory()) {
|
|
21746
|
-
for (const sub of (0,
|
|
21747
|
-
if (!sub.name.startsWith(".")) files[`${entry.name}/${sub.name}`] = sub.isFile() ? (0,
|
|
22732
|
+
for (const sub of (0, import_node_fs6.readdirSync)((0, import_node_path7.join)(folder, entry.name), { withFileTypes: true })) {
|
|
22733
|
+
if (!sub.name.startsWith(".")) files[`${entry.name}/${sub.name}`] = sub.isFile() ? (0, import_node_fs6.readFileSync)((0, import_node_path7.join)(folder, entry.name, sub.name), "utf8") : "";
|
|
21748
22734
|
}
|
|
21749
22735
|
}
|
|
21750
22736
|
}
|
|
21751
22737
|
return files;
|
|
21752
22738
|
}
|
|
21753
22739
|
function siblingCardIds(problemsDir) {
|
|
21754
|
-
const cards = (0,
|
|
21755
|
-
if (!(0,
|
|
22740
|
+
const cards = (0, import_node_path7.join)(problemsDir, "..", "cards");
|
|
22741
|
+
if (!(0, import_node_fs6.existsSync)((0, import_node_path7.join)(cards, "tags.json"))) return void 0;
|
|
21756
22742
|
const ids = /* @__PURE__ */ new Set();
|
|
21757
|
-
for (const topic of (0,
|
|
22743
|
+
for (const topic of (0, import_node_fs6.readdirSync)(cards, { withFileTypes: true })) {
|
|
21758
22744
|
if (!topic.isDirectory()) continue;
|
|
21759
|
-
for (const file of (0,
|
|
22745
|
+
for (const file of (0, import_node_fs6.readdirSync)((0, import_node_path7.join)(cards, topic.name))) if (file.endsWith(".md")) ids.add(file.slice(0, -".md".length));
|
|
21760
22746
|
}
|
|
21761
22747
|
return ids;
|
|
21762
22748
|
}
|
|
21763
22749
|
function checkProblems(dir) {
|
|
21764
22750
|
const cardIds = siblingCardIds(dir);
|
|
21765
|
-
return (0,
|
|
21766
|
-
const folder = displayPath3((0,
|
|
22751
|
+
return (0, import_node_fs6.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name).sort().map((id) => {
|
|
22752
|
+
const folder = displayPath3((0, import_node_path7.join)(dir, id));
|
|
21767
22753
|
try {
|
|
21768
|
-
const problem = problemFromFiles(id, readProblemFolder((0,
|
|
22754
|
+
const problem = problemFromFiles(id, readProblemFolder((0, import_node_path7.join)(dir, id)));
|
|
21769
22755
|
return { folder, loaded: true, ...validateProblem(problem, void 0, { cardIds }) };
|
|
21770
22756
|
} catch (e) {
|
|
21771
22757
|
if (!(e instanceof ProblemFolderError)) throw e;
|
|
@@ -21779,7 +22765,7 @@ var escape3 = (s) => s.replace(/%/g, "%25").replace(/\r?\n/g, "%0A");
|
|
|
21779
22765
|
function formatProblemReports(dir, reports, style) {
|
|
21780
22766
|
if (style === "json") return JSON.stringify({ dir: displayPath3(dir), problems: reports }, null, 2);
|
|
21781
22767
|
const lines = [];
|
|
21782
|
-
const where = (r, v) => v.file === "." ? r.folder : (0,
|
|
22768
|
+
const where = (r, v) => v.file === "." ? r.folder : (0, import_node_path7.join)(r.folder, v.file);
|
|
21783
22769
|
for (const r of reports) {
|
|
21784
22770
|
if (style === "github") {
|
|
21785
22771
|
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)}`);
|
|
@@ -21833,7 +22819,7 @@ Usage:
|
|
|
21833
22819
|
${PROBLEM_USAGE}`);
|
|
21834
22820
|
return 2;
|
|
21835
22821
|
}
|
|
21836
|
-
const dir = paths[0] ? (0,
|
|
22822
|
+
const dir = paths[0] ? (0, import_node_path7.resolve)(cwd, paths[0]) : findProblemsDir(cwd);
|
|
21837
22823
|
if (!dir) {
|
|
21838
22824
|
err(`Not inside the Proschi repository; give the problems directory
|
|
21839
22825
|
|
|
@@ -21841,7 +22827,7 @@ Usage:
|
|
|
21841
22827
|
${PROBLEM_USAGE}`);
|
|
21842
22828
|
return 2;
|
|
21843
22829
|
}
|
|
21844
|
-
if (!(0,
|
|
22830
|
+
if (!(0, import_node_fs6.existsSync)(dir) || !(0, import_node_fs6.statSync)(dir).isDirectory()) {
|
|
21845
22831
|
err(`No such directory: ${paths[0] ?? dir}`);
|
|
21846
22832
|
return 2;
|
|
21847
22833
|
}
|
|
@@ -21883,16 +22869,16 @@ ${PROBLEM_USAGE}`);
|
|
|
21883
22869
|
err(`The id '${id}' must be lowercase words joined by "-", e.g. url-shortener (it is the folder name and the URL)`);
|
|
21884
22870
|
return 2;
|
|
21885
22871
|
}
|
|
21886
|
-
const dir = dirArg ? (0,
|
|
21887
|
-
const folder = (0,
|
|
21888
|
-
if ((0,
|
|
22872
|
+
const dir = dirArg ? (0, import_node_path7.resolve)(cwd, dirArg) : findProblemsDir(cwd) ?? (0, import_node_path7.resolve)(cwd, "problems");
|
|
22873
|
+
const folder = (0, import_node_path7.join)(dir, id);
|
|
22874
|
+
if ((0, import_node_fs6.existsSync)(folder)) {
|
|
21889
22875
|
err(`${displayPath3(folder)} already exists`);
|
|
21890
22876
|
return 1;
|
|
21891
22877
|
}
|
|
21892
22878
|
const files = problemTemplate(id);
|
|
21893
22879
|
for (const [name, text] of Object.entries(files)) {
|
|
21894
|
-
(0,
|
|
21895
|
-
(0,
|
|
22880
|
+
(0, import_node_fs6.mkdirSync)((0, import_node_path7.dirname)((0, import_node_path7.join)(folder, name)), { recursive: true });
|
|
22881
|
+
(0, import_node_fs6.writeFileSync)((0, import_node_path7.join)(folder, name), text);
|
|
21896
22882
|
}
|
|
21897
22883
|
const shown = displayPath3(folder);
|
|
21898
22884
|
out(`Created ${shown}/
|
|
@@ -21924,12 +22910,12 @@ ${PROBLEM_USAGE}`);
|
|
|
21924
22910
|
}
|
|
21925
22911
|
|
|
21926
22912
|
// src/cards.ts
|
|
21927
|
-
var
|
|
21928
|
-
var
|
|
22913
|
+
var import_node_fs7 = require("node:fs");
|
|
22914
|
+
var import_node_path8 = require("node:path");
|
|
21929
22915
|
|
|
21930
22916
|
// ../frontend/src/learn/cardCheck.ts
|
|
21931
22917
|
var TAGS_FILE = "tags.json";
|
|
21932
|
-
var
|
|
22918
|
+
var LOCK_FILE2 = "ids.lock";
|
|
21933
22919
|
var DUPLICATE_THRESHOLD = 0.4;
|
|
21934
22920
|
var STOP = new Set(
|
|
21935
22921
|
"a an the and or of to in on for with is are was be it its this that what which when why how does do you your from by as at than then into can would should".split(" ")
|
|
@@ -21980,9 +22966,9 @@ function checkCardFiles(files, options = {}) {
|
|
|
21980
22966
|
const topicIds = new Set(topics.map((t) => t.id));
|
|
21981
22967
|
const cards = [];
|
|
21982
22968
|
for (const file of Object.keys(files).sort()) {
|
|
21983
|
-
if (file === TAGS_FILE || file ===
|
|
22969
|
+
if (file === TAGS_FILE || file === LOCK_FILE2) continue;
|
|
21984
22970
|
if (!/^[^/]+\/[^/]+\.md$/.test(file)) {
|
|
21985
|
-
add(file, `Unexpected file: the cards folder holds ${TAGS_FILE}, ${
|
|
22971
|
+
add(file, `Unexpected file: the cards folder holds ${TAGS_FILE}, ${LOCK_FILE2} and <topic>/<id>.md`);
|
|
21986
22972
|
continue;
|
|
21987
22973
|
}
|
|
21988
22974
|
try {
|
|
@@ -22018,16 +23004,16 @@ function checkCardFiles(files, options = {}) {
|
|
|
22018
23004
|
for (const card of cards) {
|
|
22019
23005
|
for (const other of card.distinctFrom) if (!byId.has(other)) add(fileOf(card), `'distinct-from' names "${other}", which is not a card`);
|
|
22020
23006
|
}
|
|
22021
|
-
if (files[
|
|
23007
|
+
if (files[LOCK_FILE2] === void 0) add(LOCK_FILE2, "Missing: run `proschi cards lock` to create it");
|
|
22022
23008
|
else {
|
|
22023
|
-
const { ids, lines } =
|
|
22024
|
-
const
|
|
22025
|
-
if (ids.join("\n") !==
|
|
23009
|
+
const { ids, lines } = readLock2(files[LOCK_FILE2]);
|
|
23010
|
+
const sorted2 = [...new Set(ids)].sort();
|
|
23011
|
+
if (ids.join("\n") !== sorted2.join("\n")) add(LOCK_FILE2, "Not sorted, or an id appears twice; run `proschi cards lock`");
|
|
22026
23012
|
for (const card of cards) {
|
|
22027
|
-
if (!lines.has(card.id)) add(fileOf(card), `New card: add its id to ${
|
|
23013
|
+
if (!lines.has(card.id)) add(fileOf(card), `New card: add its id to ${LOCK_FILE2} with \`proschi cards lock\``);
|
|
22028
23014
|
}
|
|
22029
23015
|
for (const id of lines.keys()) {
|
|
22030
|
-
if (!byId.has(id)) add(
|
|
23016
|
+
if (!byId.has(id)) add(LOCK_FILE2, `"${id}" has no card file: never delete a card, set \`status: retired\` in its front matter instead`, lines.get(id));
|
|
22031
23017
|
}
|
|
22032
23018
|
}
|
|
22033
23019
|
for (const { a, b, score } of overlaps(cards, options.threshold)) {
|
|
@@ -22052,34 +23038,34 @@ var CARDS_HELP = `cards Practice cards, one Markdown file each (docs/CARDS.md)
|
|
|
22052
23038
|
phone, and no two cards that ask nearly the same thing.
|
|
22053
23039
|
Exits with 1 on any violation.
|
|
22054
23040
|
lock adds the ids of new cards to ids.lock.`;
|
|
22055
|
-
var REPO_CARDS = (0,
|
|
23041
|
+
var REPO_CARDS = (0, import_node_path8.join)("frontend", "src", "practice", "cards");
|
|
22056
23042
|
function findCardsDir(cwd = process.cwd()) {
|
|
22057
|
-
for (let dir = (0,
|
|
22058
|
-
const candidate = (0,
|
|
22059
|
-
if ((0,
|
|
22060
|
-
if ((0,
|
|
23043
|
+
for (let dir = (0, import_node_path8.resolve)(cwd); ; dir = (0, import_node_path8.dirname)(dir)) {
|
|
23044
|
+
const candidate = (0, import_node_path8.join)(dir, REPO_CARDS);
|
|
23045
|
+
if ((0, import_node_fs7.existsSync)(candidate) && (0, import_node_fs7.statSync)(candidate).isDirectory()) return candidate;
|
|
23046
|
+
if ((0, import_node_path8.dirname)(dir) === dir) return void 0;
|
|
22061
23047
|
}
|
|
22062
23048
|
}
|
|
22063
23049
|
function readCardsFolder(dir) {
|
|
22064
23050
|
const files = {};
|
|
22065
|
-
for (const entry of (0,
|
|
23051
|
+
for (const entry of (0, import_node_fs7.readdirSync)(dir, { withFileTypes: true })) {
|
|
22066
23052
|
if (entry.name.startsWith(".")) continue;
|
|
22067
|
-
if (entry.isFile()) files[entry.name] = (0,
|
|
23053
|
+
if (entry.isFile()) files[entry.name] = (0, import_node_fs7.readFileSync)((0, import_node_path8.join)(dir, entry.name), "utf8");
|
|
22068
23054
|
else if (entry.isDirectory()) {
|
|
22069
|
-
for (const sub of (0,
|
|
22070
|
-
if (!sub.name.startsWith(".")) files[`${entry.name}/${sub.name}`] = sub.isFile() ? (0,
|
|
23055
|
+
for (const sub of (0, import_node_fs7.readdirSync)((0, import_node_path8.join)(dir, entry.name), { withFileTypes: true })) {
|
|
23056
|
+
if (!sub.name.startsWith(".")) files[`${entry.name}/${sub.name}`] = sub.isFile() ? (0, import_node_fs7.readFileSync)((0, import_node_path8.join)(dir, entry.name, sub.name), "utf8") : "";
|
|
22071
23057
|
}
|
|
22072
23058
|
}
|
|
22073
23059
|
}
|
|
22074
23060
|
return files;
|
|
22075
23061
|
}
|
|
22076
23062
|
function problemIdsIn(problemsDir) {
|
|
22077
|
-
return (0,
|
|
23063
|
+
return (0, import_node_fs7.readdirSync)(problemsDir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
|
|
22078
23064
|
}
|
|
22079
23065
|
var escape4 = (s) => s.replace(/%/g, "%25").replace(/\r?\n/g, "%0A");
|
|
22080
23066
|
function formatCardCheck(dir, check, style) {
|
|
22081
23067
|
const shown = displayPath3(dir);
|
|
22082
|
-
const where = (v) => (0,
|
|
23068
|
+
const where = (v) => (0, import_node_path8.join)(shown, v.file);
|
|
22083
23069
|
if (style === "json") {
|
|
22084
23070
|
return JSON.stringify({ dir: shown, cards: check.cards.length, topics: check.topics.length, violations: check.violations }, null, 2);
|
|
22085
23071
|
}
|
|
@@ -22134,7 +23120,7 @@ ${CARDS_USAGE}`);
|
|
|
22134
23120
|
return parsed;
|
|
22135
23121
|
}
|
|
22136
23122
|
function resolveDir(dirArg, cwd, err) {
|
|
22137
|
-
const dir = dirArg ? (0,
|
|
23123
|
+
const dir = dirArg ? (0, import_node_path8.resolve)(cwd, dirArg) : findCardsDir(cwd);
|
|
22138
23124
|
if (!dir) {
|
|
22139
23125
|
err(`Not inside the Proschi repository; give the cards directory
|
|
22140
23126
|
|
|
@@ -22142,7 +23128,7 @@ Usage:
|
|
|
22142
23128
|
${CARDS_USAGE}`);
|
|
22143
23129
|
return void 0;
|
|
22144
23130
|
}
|
|
22145
|
-
if (!(0,
|
|
23131
|
+
if (!(0, import_node_fs7.existsSync)(dir) || !(0, import_node_fs7.statSync)(dir).isDirectory()) {
|
|
22146
23132
|
err(`No such directory: ${dirArg ?? dir}`);
|
|
22147
23133
|
return void 0;
|
|
22148
23134
|
}
|
|
@@ -22153,9 +23139,9 @@ function cardsCheck(args, out, err, cwd) {
|
|
|
22153
23139
|
if (!parsed) return 2;
|
|
22154
23140
|
const dir = resolveDir(parsed.dir, cwd, err);
|
|
22155
23141
|
if (!dir) return 2;
|
|
22156
|
-
const besideCards = (0,
|
|
22157
|
-
const problemsDir = parsed.problems ? (0,
|
|
22158
|
-
if (parsed.problems && !((0,
|
|
23142
|
+
const besideCards = (0, import_node_path8.join)(dir, "..", "problems");
|
|
23143
|
+
const problemsDir = parsed.problems ? (0, import_node_path8.resolve)(cwd, parsed.problems) : (0, import_node_fs7.existsSync)(besideCards) ? besideCards : findProblemsDir(cwd);
|
|
23144
|
+
if (parsed.problems && !((0, import_node_fs7.existsSync)(problemsDir) && (0, import_node_fs7.statSync)(problemsDir).isDirectory())) {
|
|
22159
23145
|
err(`No such directory: ${parsed.problems}`);
|
|
22160
23146
|
return 2;
|
|
22161
23147
|
}
|
|
@@ -22180,11 +23166,11 @@ function cardsLock(args, out, err, cwd) {
|
|
|
22180
23166
|
ids.push(file.slice(file.indexOf("/") + 1, -".md".length));
|
|
22181
23167
|
}
|
|
22182
23168
|
}
|
|
22183
|
-
const lockPath = (0,
|
|
22184
|
-
const existing = (0,
|
|
23169
|
+
const lockPath = (0, import_node_path8.join)(dir, LOCK_FILE2);
|
|
23170
|
+
const existing = (0, import_node_fs7.existsSync)(lockPath) ? readLock2((0, import_node_fs7.readFileSync)(lockPath, "utf8")).ids : [];
|
|
22185
23171
|
const added = [...new Set(ids)].filter((id) => !existing.includes(id)).sort();
|
|
22186
|
-
const text =
|
|
22187
|
-
if (!(0,
|
|
23172
|
+
const text = writeLock2(existing, ids);
|
|
23173
|
+
if (!(0, import_node_fs7.existsSync)(lockPath) || (0, import_node_fs7.readFileSync)(lockPath, "utf8") !== text) (0, import_node_fs7.writeFileSync)(lockPath, text);
|
|
22188
23174
|
out(added.length ? `Added ${added.length} id${added.length === 1 ? "" : "s"} to ${displayPath3(lockPath)}:
|
|
22189
23175
|
${added.join("\n ")}` : `${displayPath3(lockPath)} already lists every card`);
|
|
22190
23176
|
return 0;
|
|
@@ -22201,8 +23187,8 @@ ${CARDS_USAGE}`);
|
|
|
22201
23187
|
}
|
|
22202
23188
|
|
|
22203
23189
|
// src/achievements.ts
|
|
22204
|
-
var
|
|
22205
|
-
var
|
|
23190
|
+
var import_node_fs8 = require("node:fs");
|
|
23191
|
+
var import_node_path9 = require("node:path");
|
|
22206
23192
|
|
|
22207
23193
|
// ../frontend/src/learn/fsrs.ts
|
|
22208
23194
|
var FACTOR = 19 / 81;
|
|
@@ -22282,7 +23268,7 @@ function checkAchievements(raw, context) {
|
|
|
22282
23268
|
if (violations.length === before2) achievements.push(a);
|
|
22283
23269
|
});
|
|
22284
23270
|
if (context.lock !== void 0) {
|
|
22285
|
-
const { ids: locked, lines } =
|
|
23271
|
+
const { ids: locked, lines } = readLock2(context.lock);
|
|
22286
23272
|
if (locked.join("\n") !== [...new Set(locked)].sort().join("\n")) violations.push({ inLock: true, message: `${ACHIEVEMENTS_LOCK_FILE} is not sorted, or an id appears twice; run \`proschi achievements lock\`` });
|
|
22287
23273
|
raw.forEach((item, index) => {
|
|
22288
23274
|
const id = item && typeof item === "object" ? item.id : void 0;
|
|
@@ -22395,21 +23381,21 @@ var ACHIEVEMENTS_HELP = `achievements
|
|
|
22395
23381
|
the file, which lists no deleted achievement (retire one with
|
|
22396
23382
|
"retired": true instead). Exits with 1 on any violation. lock adds
|
|
22397
23383
|
the ids of new achievements to achievements.lock.`;
|
|
22398
|
-
var REPO_ACHIEVEMENTS = (0,
|
|
23384
|
+
var REPO_ACHIEVEMENTS = (0, import_node_path9.join)("frontend", "src", "practice", "achievements.json");
|
|
22399
23385
|
function findAchievementsFile(cwd = process.cwd()) {
|
|
22400
|
-
for (let dir = (0,
|
|
22401
|
-
const candidate = (0,
|
|
22402
|
-
if ((0,
|
|
22403
|
-
if ((0,
|
|
23386
|
+
for (let dir = (0, import_node_path9.resolve)(cwd); ; dir = (0, import_node_path9.dirname)(dir)) {
|
|
23387
|
+
const candidate = (0, import_node_path9.join)(dir, REPO_ACHIEVEMENTS);
|
|
23388
|
+
if ((0, import_node_fs8.existsSync)(candidate) && (0, import_node_fs8.statSync)(candidate).isFile()) return candidate;
|
|
23389
|
+
if ((0, import_node_path9.dirname)(dir) === dir) return void 0;
|
|
22404
23390
|
}
|
|
22405
23391
|
}
|
|
22406
23392
|
function problemsIn(dir) {
|
|
22407
23393
|
const out = [];
|
|
22408
|
-
for (const entry of (0,
|
|
22409
|
-
const file = (0,
|
|
22410
|
-
if (!entry.isDirectory() || entry.name.startsWith(".") || !(0,
|
|
23394
|
+
for (const entry of (0, import_node_fs8.readdirSync)(dir, { withFileTypes: true })) {
|
|
23395
|
+
const file = (0, import_node_path9.join)(dir, entry.name, PROBLEM_MD);
|
|
23396
|
+
if (!entry.isDirectory() || entry.name.startsWith(".") || !(0, import_node_fs8.existsSync)(file)) continue;
|
|
22411
23397
|
try {
|
|
22412
|
-
const { difficulty, tags } = readProblemMd(entry.name, (0,
|
|
23398
|
+
const { difficulty, tags } = readProblemMd(entry.name, (0, import_node_fs8.readFileSync)(file, "utf8"));
|
|
22413
23399
|
out.push({ id: entry.name, difficulty, tags });
|
|
22414
23400
|
} catch {
|
|
22415
23401
|
}
|
|
@@ -22417,7 +23403,7 @@ function problemsIn(dir) {
|
|
|
22417
23403
|
return out;
|
|
22418
23404
|
}
|
|
22419
23405
|
function lineOf2(text, lock, v) {
|
|
22420
|
-
if (v.inLock) return v.id !== void 0 && lock !== void 0 ?
|
|
23406
|
+
if (v.inLock) return v.id !== void 0 && lock !== void 0 ? readLock2(lock).lines.get(v.id) : void 0;
|
|
22421
23407
|
if (v.id === void 0) return void 0;
|
|
22422
23408
|
const needle = `"id": ${JSON.stringify(v.id)}`;
|
|
22423
23409
|
const at = v.message.startsWith("Duplicate id") ? text.lastIndexOf(needle) : text.indexOf(needle);
|
|
@@ -22461,11 +23447,11 @@ ${ACHIEVEMENTS_USAGE}`);
|
|
|
22461
23447
|
parsed.file = paths[0];
|
|
22462
23448
|
return parsed;
|
|
22463
23449
|
}
|
|
22464
|
-
var isDir = (p) => (0,
|
|
23450
|
+
var isDir = (p) => (0, import_node_fs8.existsSync)(p) && (0, import_node_fs8.statSync)(p).isDirectory();
|
|
22465
23451
|
function achievementsCheck(args, out, err, cwd) {
|
|
22466
23452
|
const parsed = parseArgs3(args, err);
|
|
22467
23453
|
if (!parsed) return 2;
|
|
22468
|
-
const file = parsed.file ? (0,
|
|
23454
|
+
const file = parsed.file ? (0, import_node_path9.resolve)(cwd, parsed.file) : findAchievementsFile(cwd);
|
|
22469
23455
|
if (!file) {
|
|
22470
23456
|
err(`Not inside the Proschi repository; give the achievements file
|
|
22471
23457
|
|
|
@@ -22473,26 +23459,26 @@ Usage:
|
|
|
22473
23459
|
${ACHIEVEMENTS_USAGE}`);
|
|
22474
23460
|
return 2;
|
|
22475
23461
|
}
|
|
22476
|
-
if (!(0,
|
|
23462
|
+
if (!(0, import_node_fs8.existsSync)(file) || !(0, import_node_fs8.statSync)(file).isFile()) {
|
|
22477
23463
|
err(`No such file: ${parsed.file ?? file}`);
|
|
22478
23464
|
return 2;
|
|
22479
23465
|
}
|
|
22480
|
-
const problemsDir = parsed.problems ? (0,
|
|
22481
|
-
const cardsDir = parsed.cards ? (0,
|
|
23466
|
+
const problemsDir = parsed.problems ? (0, import_node_path9.resolve)(cwd, parsed.problems) : (0, import_node_path9.join)((0, import_node_path9.dirname)(file), "problems");
|
|
23467
|
+
const cardsDir = parsed.cards ? (0, import_node_path9.resolve)(cwd, parsed.cards) : (0, import_node_path9.join)((0, import_node_path9.dirname)(file), "cards");
|
|
22482
23468
|
for (const [flag, dir] of [["--problems", problemsDir], ["--cards", cardsDir]]) {
|
|
22483
23469
|
if (!isDir(dir)) {
|
|
22484
23470
|
err(`No such directory: ${dir}${parsed[flag === "--problems" ? "problems" : "cards"] ? "" : ` (give ${flag} <dir>)`}`);
|
|
22485
23471
|
return 2;
|
|
22486
23472
|
}
|
|
22487
23473
|
}
|
|
22488
|
-
const text = (0,
|
|
22489
|
-
const lockPath = (0,
|
|
22490
|
-
const lock = (0,
|
|
23474
|
+
const text = (0, import_node_fs8.readFileSync)(file, "utf8");
|
|
23475
|
+
const lockPath = (0, import_node_path9.join)((0, import_node_path9.dirname)(file), ACHIEVEMENTS_LOCK_FILE);
|
|
23476
|
+
const lock = (0, import_node_fs8.existsSync)(lockPath) ? (0, import_node_fs8.readFileSync)(lockPath, "utf8") : void 0;
|
|
22491
23477
|
let topics;
|
|
22492
23478
|
try {
|
|
22493
|
-
topics = readTopics((0,
|
|
23479
|
+
topics = readTopics((0, import_node_fs8.readFileSync)((0, import_node_path9.join)(cardsDir, "tags.json"), "utf8")).map((t) => t.id);
|
|
22494
23480
|
} catch (e) {
|
|
22495
|
-
err(e instanceof CardFileError ? `${(0,
|
|
23481
|
+
err(e instanceof CardFileError ? `${(0, import_node_path9.join)(displayPath3(cardsDir), "tags.json")}: ${e.detail}` : String(e));
|
|
22496
23482
|
return 2;
|
|
22497
23483
|
}
|
|
22498
23484
|
let raw;
|
|
@@ -22535,7 +23521,7 @@ Usage:
|
|
|
22535
23521
|
${ACHIEVEMENTS_USAGE}`);
|
|
22536
23522
|
return 2;
|
|
22537
23523
|
}
|
|
22538
|
-
const file = args[0] ? (0,
|
|
23524
|
+
const file = args[0] ? (0, import_node_path9.resolve)(cwd, args[0]) : findAchievementsFile(cwd);
|
|
22539
23525
|
if (!file) {
|
|
22540
23526
|
err(`Not inside the Proschi repository; give the achievements file
|
|
22541
23527
|
|
|
@@ -22543,13 +23529,13 @@ Usage:
|
|
|
22543
23529
|
${ACHIEVEMENTS_USAGE}`);
|
|
22544
23530
|
return 2;
|
|
22545
23531
|
}
|
|
22546
|
-
if (!(0,
|
|
23532
|
+
if (!(0, import_node_fs8.existsSync)(file) || !(0, import_node_fs8.statSync)(file).isFile()) {
|
|
22547
23533
|
err(`No such file: ${args[0] ?? file}`);
|
|
22548
23534
|
return 2;
|
|
22549
23535
|
}
|
|
22550
23536
|
let raw;
|
|
22551
23537
|
try {
|
|
22552
|
-
raw = JSON.parse((0,
|
|
23538
|
+
raw = JSON.parse((0, import_node_fs8.readFileSync)(file, "utf8"));
|
|
22553
23539
|
} catch (e) {
|
|
22554
23540
|
err(`${displayPath3(file)}: not valid JSON: ${e.message}`);
|
|
22555
23541
|
return 1;
|
|
@@ -22559,12 +23545,12 @@ ${ACHIEVEMENTS_USAGE}`);
|
|
|
22559
23545
|
return 1;
|
|
22560
23546
|
}
|
|
22561
23547
|
const ids = raw.flatMap((a) => a && typeof a === "object" && typeof a.id === "string" ? [a.id] : []);
|
|
22562
|
-
const lockPath = (0,
|
|
22563
|
-
const before2 = (0,
|
|
22564
|
-
const existing = before2 !== void 0 ?
|
|
23548
|
+
const lockPath = (0, import_node_path9.join)((0, import_node_path9.dirname)(file), ACHIEVEMENTS_LOCK_FILE);
|
|
23549
|
+
const before2 = (0, import_node_fs8.existsSync)(lockPath) ? (0, import_node_fs8.readFileSync)(lockPath, "utf8") : void 0;
|
|
23550
|
+
const existing = before2 !== void 0 ? readLock2(before2).ids : [];
|
|
22565
23551
|
const added = [...new Set(ids)].filter((id) => !existing.includes(id)).sort();
|
|
22566
23552
|
const text = writeAchievementsLock(existing, ids);
|
|
22567
|
-
if (before2 !== text) (0,
|
|
23553
|
+
if (before2 !== text) (0, import_node_fs8.writeFileSync)(lockPath, text);
|
|
22568
23554
|
out(added.length ? `Added ${added.length} id${added.length === 1 ? "" : "s"} to ${displayPath3(lockPath)}:
|
|
22569
23555
|
${added.join("\n ")}` : `${displayPath3(lockPath)} already lists every achievement`);
|
|
22570
23556
|
return 0;
|
|
@@ -22581,8 +23567,8 @@ ${ACHIEVEMENTS_USAGE}`);
|
|
|
22581
23567
|
}
|
|
22582
23568
|
|
|
22583
23569
|
// src/game.ts
|
|
22584
|
-
var
|
|
22585
|
-
var
|
|
23570
|
+
var import_node_fs9 = require("node:fs");
|
|
23571
|
+
var import_node_path10 = require("node:path");
|
|
22586
23572
|
|
|
22587
23573
|
// ../frontend/src/game/engine/compile.ts
|
|
22588
23574
|
var BACKFILL_PREFIX = "backfill_";
|
|
@@ -24341,27 +25327,6 @@ function ownerOf(t) {
|
|
|
24341
25327
|
return void 0;
|
|
24342
25328
|
}
|
|
24343
25329
|
|
|
24344
|
-
// ../frontend/src/sim/overlay.ts
|
|
24345
|
-
function flowsOf(diagram, traffic) {
|
|
24346
|
-
const flows = /* @__PURE__ */ new Map();
|
|
24347
|
-
for (const t of traffic) {
|
|
24348
|
-
const u = findUseCase(diagram, t.useCase);
|
|
24349
|
-
if (!u || t.rps <= 0) continue;
|
|
24350
|
-
for (const sc of u.scenarios) {
|
|
24351
|
-
const share = t.mix ? t.mix.find((m) => m.scenario === sc.name)?.share ?? 0 : sc === u.scenarios[0] ? 1 : 0;
|
|
24352
|
-
if (share <= 0) continue;
|
|
24353
|
-
for (const step of sc.steps) {
|
|
24354
|
-
if (step.failed) continue;
|
|
24355
|
-
const key = `${step.fromServiceId}>${step.toServiceId}`;
|
|
24356
|
-
const f = flows.get(key) ?? { from: step.fromServiceId, to: step.toServiceId, rps: 0, async: step.executionType !== "SYNC_REQUEST_RESPONSE" };
|
|
24357
|
-
f.rps += t.rps * share * (step.multiplier ?? 1);
|
|
24358
|
-
flows.set(key, f);
|
|
24359
|
-
}
|
|
24360
|
-
}
|
|
24361
|
-
}
|
|
24362
|
-
return [...flows.values()];
|
|
24363
|
-
}
|
|
24364
|
-
|
|
24365
25330
|
// ../frontend/src/game/engine/tick.ts
|
|
24366
25331
|
var LOC = { line: 0, col: 0, length: 0 };
|
|
24367
25332
|
var PROFILE_CACHE = /* @__PURE__ */ new Map();
|
|
@@ -25250,7 +26215,7 @@ function structuredCloneAction(a) {
|
|
|
25250
26215
|
}
|
|
25251
26216
|
|
|
25252
26217
|
// ../frontend/src/game/engine/check.ts
|
|
25253
|
-
var
|
|
26218
|
+
var LOCK_FILE3 = "ids.lock";
|
|
25254
26219
|
function applyPlay(board, play) {
|
|
25255
26220
|
const b = cloneBoard(board);
|
|
25256
26221
|
for (const id of play.remove ?? []) {
|
|
@@ -25346,13 +26311,13 @@ function checkGame(files, ctx) {
|
|
|
25346
26311
|
const violations = errors.map((e) => ({ file: e.file, message: e.message.replace(/^[^:]*: /, ""), ...e.line ? { line: e.line } : {} }));
|
|
25347
26312
|
const v = (file, message) => violations.push({ file, message });
|
|
25348
26313
|
const runs = [];
|
|
25349
|
-
const lockText = files[
|
|
26314
|
+
const lockText = files[LOCK_FILE3];
|
|
25350
26315
|
const ids = publishedIds(content);
|
|
25351
|
-
if (lockText === void 0) v(
|
|
26316
|
+
if (lockText === void 0) v(LOCK_FILE3, "Missing: run `proschi game lock`");
|
|
25352
26317
|
else {
|
|
25353
26318
|
const locked = new Set(lockText.split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !l.startsWith("#")));
|
|
25354
|
-
for (const id of ids) if (!locked.has(id)) v(
|
|
25355
|
-
for (const id of locked) if (!ids.includes(id)) v(
|
|
26319
|
+
for (const id of ids) if (!locked.has(id)) v(LOCK_FILE3, `'${id}' is not in ids.lock: run \`proschi game lock\``);
|
|
26320
|
+
for (const id of locked) if (!ids.includes(id)) v(LOCK_FILE3, `'${id}' was published and is gone: ids are never deleted (progress and replays refer to them)`);
|
|
25356
26321
|
}
|
|
25357
26322
|
const learn = (file, list) => {
|
|
25358
26323
|
for (const id of list) if (!ctx.cards.has(id)) v(file, `Review card '${id}' does not exist`);
|
|
@@ -25679,22 +26644,22 @@ var GAME_HELP = `game Scale or Fail, the system design game (docs/GAME.md).
|
|
|
25679
26644
|
sim plays a scenario's reference run (or --run) and prints every
|
|
25680
26645
|
wave: traffic, cost, revenue, score, Trust and what broke. --basic
|
|
25681
26646
|
plays it under the basic rules of a first run.`;
|
|
25682
|
-
var REPO_GAME = (0,
|
|
26647
|
+
var REPO_GAME = (0, import_node_path10.join)("frontend", "src", "game", "content");
|
|
25683
26648
|
function findGameDir(cwd = process.cwd()) {
|
|
25684
|
-
for (let dir = (0,
|
|
25685
|
-
const candidate = (0,
|
|
25686
|
-
if ((0,
|
|
25687
|
-
if ((0,
|
|
26649
|
+
for (let dir = (0, import_node_path10.resolve)(cwd); ; dir = (0, import_node_path10.dirname)(dir)) {
|
|
26650
|
+
const candidate = (0, import_node_path10.join)(dir, REPO_GAME);
|
|
26651
|
+
if ((0, import_node_fs9.existsSync)(candidate) && (0, import_node_fs9.statSync)(candidate).isDirectory()) return candidate;
|
|
26652
|
+
if ((0, import_node_path10.dirname)(dir) === dir) return void 0;
|
|
25688
26653
|
}
|
|
25689
26654
|
}
|
|
25690
26655
|
function readGameFolder(dir) {
|
|
25691
26656
|
const files = {};
|
|
25692
26657
|
const walk = (d) => {
|
|
25693
|
-
for (const entry of (0,
|
|
26658
|
+
for (const entry of (0, import_node_fs9.readdirSync)(d, { withFileTypes: true })) {
|
|
25694
26659
|
if (entry.name.startsWith(".")) continue;
|
|
25695
|
-
const full = (0,
|
|
26660
|
+
const full = (0, import_node_path10.join)(d, entry.name);
|
|
25696
26661
|
if (entry.isDirectory()) walk(full);
|
|
25697
|
-
else files[(0,
|
|
26662
|
+
else files[(0, import_node_path10.relative)(dir, full).split("\\").join("/")] = (0, import_node_fs9.readFileSync)(full, "utf8");
|
|
25698
26663
|
}
|
|
25699
26664
|
};
|
|
25700
26665
|
walk(dir);
|
|
@@ -25706,22 +26671,22 @@ function cardContext(dir, cwd) {
|
|
|
25706
26671
|
const cards = /* @__PURE__ */ new Set();
|
|
25707
26672
|
const topics = /* @__PURE__ */ new Set();
|
|
25708
26673
|
if (cardsDir) {
|
|
25709
|
-
for (const topic of (0,
|
|
26674
|
+
for (const topic of (0, import_node_fs9.readdirSync)(cardsDir, { withFileTypes: true })) {
|
|
25710
26675
|
if (!topic.isDirectory()) continue;
|
|
25711
|
-
for (const f of (0,
|
|
26676
|
+
for (const f of (0, import_node_fs9.readdirSync)((0, import_node_path10.join)(cardsDir, topic.name))) if (f.endsWith(".md")) cards.add(f.slice(0, -3));
|
|
25712
26677
|
}
|
|
25713
|
-
const tags = (0,
|
|
25714
|
-
if ((0,
|
|
26678
|
+
const tags = (0, import_node_path10.join)(cardsDir, "tags.json");
|
|
26679
|
+
if ((0, import_node_fs9.existsSync)(tags)) for (const t of readTopics((0, import_node_fs9.readFileSync)(tags, "utf8"))) topics.add(t.id);
|
|
25715
26680
|
}
|
|
25716
|
-
const problems = new Set(problemsDir ? (0,
|
|
26681
|
+
const problems = new Set(problemsDir ? (0, import_node_fs9.readdirSync)(problemsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name) : []);
|
|
25717
26682
|
return { cards, topics, problems };
|
|
25718
26683
|
}
|
|
25719
26684
|
var escape6 = (s) => s.replace(/%/g, "%25").replace(/\r?\n/g, "%0A");
|
|
25720
26685
|
function formatGameCheck(dir, check, style) {
|
|
25721
26686
|
const shown = displayPath3(dir);
|
|
25722
26687
|
if (style === "json") return JSON.stringify({ dir: shown, scenarios: check.content.scenarios.length, runs: check.runs, violations: check.violations }, null, 2);
|
|
25723
|
-
if (style === "github") return check.violations.map((v) => `::error file=${(0,
|
|
25724
|
-
const lines = check.violations.map((v) => `${(0,
|
|
26688
|
+
if (style === "github") return check.violations.map((v) => `::error file=${(0, import_node_path10.join)(shown, v.file)},line=${v.line ?? 1},title=proschi game::${escape6(v.message)}`).join("\n");
|
|
26689
|
+
const lines = check.violations.map((v) => `${(0, import_node_path10.join)(shown, v.file)}${v.line ? `:${v.line}` : ""}: ${v.message}`);
|
|
25725
26690
|
for (const r of check.runs) lines.push(` ${r.scenario}/${r.name}: ${r.outcome ?? "running"} after ${r.waves} wave(s), score ${r.score}, ${Math.round(r.ms)} ms`);
|
|
25726
26691
|
const c = check.content;
|
|
25727
26692
|
lines.push(
|
|
@@ -25771,7 +26736,7 @@ ${GAME_USAGE}`);
|
|
|
25771
26736
|
return a;
|
|
25772
26737
|
}
|
|
25773
26738
|
function resolveDir2(dirArg, cwd, err) {
|
|
25774
|
-
const dir = dirArg ? (0,
|
|
26739
|
+
const dir = dirArg ? (0, import_node_path10.resolve)(cwd, dirArg) : findGameDir(cwd);
|
|
25775
26740
|
if (!dir) {
|
|
25776
26741
|
err(`Not inside the Proschi repository; give the game content directory
|
|
25777
26742
|
|
|
@@ -25779,7 +26744,7 @@ Usage:
|
|
|
25779
26744
|
${GAME_USAGE}`);
|
|
25780
26745
|
return void 0;
|
|
25781
26746
|
}
|
|
25782
|
-
if (!(0,
|
|
26747
|
+
if (!(0, import_node_fs9.existsSync)(dir) || !(0, import_node_fs9.statSync)(dir).isDirectory()) {
|
|
25783
26748
|
err(`No such directory: ${dirArg ?? dir}`);
|
|
25784
26749
|
return void 0;
|
|
25785
26750
|
}
|
|
@@ -25820,13 +26785,13 @@ function gameLock(args, out, err, cwd) {
|
|
|
25820
26785
|
if (!a) return 2;
|
|
25821
26786
|
const dir = resolveDir2(a.positional[0], cwd, err);
|
|
25822
26787
|
if (!dir) return 2;
|
|
25823
|
-
const lockPath = (0,
|
|
25824
|
-
const existing = (0,
|
|
26788
|
+
const lockPath = (0, import_node_path10.join)(dir, LOCK_FILE3);
|
|
26789
|
+
const existing = (0, import_node_fs9.existsSync)(lockPath) ? (0, import_node_fs9.readFileSync)(lockPath, "utf8").split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !l.startsWith("#")) : [];
|
|
25825
26790
|
const ids = contentIds(readGameFolder(dir));
|
|
25826
26791
|
const added = ids.filter((id) => !existing.includes(id)).sort();
|
|
25827
26792
|
const all = [.../* @__PURE__ */ new Set([...existing, ...ids])].sort();
|
|
25828
26793
|
const text = LOCK_HEADER2 + all.join("\n") + "\n";
|
|
25829
|
-
if (!(0,
|
|
26794
|
+
if (!(0, import_node_fs9.existsSync)(lockPath) || (0, import_node_fs9.readFileSync)(lockPath, "utf8") !== text) (0, import_node_fs9.writeFileSync)(lockPath, text);
|
|
25830
26795
|
out(added.length ? `Added ${added.length} id${added.length === 1 ? "" : "s"} to ${displayPath3(lockPath)}:
|
|
25831
26796
|
${added.join("\n ")}` : `${displayPath3(lockPath)} already lists every id`);
|
|
25832
26797
|
return 0;
|
|
@@ -25854,8 +26819,8 @@ ${GAME_USAGE}`);
|
|
|
25854
26819
|
err(`No scenario '${scenario}'; there are ${content.scenarios.map((s2) => s2.id).join(", ")}`);
|
|
25855
26820
|
return 2;
|
|
25856
26821
|
}
|
|
25857
|
-
const runPath = a.run ? (0,
|
|
25858
|
-
const text = runPath ? (0,
|
|
26822
|
+
const runPath = a.run ? (0, import_node_path10.resolve)(cwd, a.run) : void 0;
|
|
26823
|
+
const text = runPath ? (0, import_node_fs9.existsSync)(runPath) ? (0, import_node_fs9.readFileSync)(runPath, "utf8") : void 0 : files[`scenarios/${scenario}/reference.json`];
|
|
25859
26824
|
if (text === void 0) {
|
|
25860
26825
|
err(runPath ? `No such file: ${a.run}` : `Scenario '${scenario}' has no reference.json; give one with --run`);
|
|
25861
26826
|
return 2;
|
|
@@ -25907,8 +26872,8 @@ ${GAME_USAGE}`);
|
|
|
25907
26872
|
}
|
|
25908
26873
|
|
|
25909
26874
|
// src/share.ts
|
|
25910
|
-
var
|
|
25911
|
-
var
|
|
26875
|
+
var import_node_fs10 = require("node:fs");
|
|
26876
|
+
var import_node_path11 = require("node:path");
|
|
25912
26877
|
|
|
25913
26878
|
// ../frontend/src/playground/share.ts
|
|
25914
26879
|
var import_lz_string = __toESM(require_lz_string(), 1);
|
|
@@ -25934,24 +26899,31 @@ var SHARE_HELP = `share-link
|
|
|
25934
26899
|
Prints a link that opens the file (with the files it imports) in the
|
|
25935
26900
|
web editor; nothing is uploaded, the diagram is in the link. --base
|
|
25936
26901
|
sets the editor's address (default ${DEFAULT_BASE}).`;
|
|
25937
|
-
function shareLink(
|
|
26902
|
+
function shareLink(file, base = DEFAULT_BASE) {
|
|
26903
|
+
const entry = (0, import_node_path11.resolve)(file);
|
|
26904
|
+
const name = (0, import_node_path11.basename)(entry);
|
|
26905
|
+
const source2 = (0, import_node_fs10.readFileSync)(entry, "utf8");
|
|
26906
|
+
const disk = fileResolver({ entry });
|
|
26907
|
+
const where = /* @__PURE__ */ new Map([[name, entry]]);
|
|
25938
26908
|
const read = {};
|
|
25939
26909
|
const result = parse(source2, {
|
|
25940
26910
|
path: name,
|
|
25941
|
-
resolve: (importPath,
|
|
25942
|
-
const
|
|
26911
|
+
resolve: (importPath, fromKey) => {
|
|
26912
|
+
const found = disk(importPath, fromKey === void 0 ? entry : where.get(fromKey));
|
|
26913
|
+
const key = resolveImportPath(fromKey, importPath);
|
|
25943
26914
|
if (key === name) return { path: key, source: "" };
|
|
25944
|
-
|
|
25945
|
-
|
|
25946
|
-
read[key] ??=
|
|
26915
|
+
if (!found) return void 0;
|
|
26916
|
+
where.set(key, found.path);
|
|
26917
|
+
read[key] ??= found.source;
|
|
25947
26918
|
return { path: key, source: read[key] };
|
|
25948
26919
|
}
|
|
25949
26920
|
});
|
|
26921
|
+
if (disk.errors.size) return { errors: [...new Set(disk.errors.values())] };
|
|
25950
26922
|
const imports = {};
|
|
25951
26923
|
for (const { resolved } of result.imports ?? []) {
|
|
25952
26924
|
if (resolved !== void 0 && resolved !== name && Object.prototype.hasOwnProperty.call(read, resolved)) imports[resolved] = read[resolved];
|
|
25953
26925
|
}
|
|
25954
|
-
return base.replace(/#.*$/, "") + encodeShareHash(source2, void 0, imports);
|
|
26926
|
+
return { url: base.replace(/#.*$/, "") + encodeShareHash(source2, void 0, imports) };
|
|
25955
26927
|
}
|
|
25956
26928
|
function runShareLink(args, out, err) {
|
|
25957
26929
|
let base = DEFAULT_BASE;
|
|
@@ -25978,20 +26950,23 @@ ${SHARE_USAGE}`);
|
|
|
25978
26950
|
${SHARE_USAGE}`);
|
|
25979
26951
|
return 2;
|
|
25980
26952
|
}
|
|
25981
|
-
|
|
25982
|
-
let source2;
|
|
26953
|
+
let link;
|
|
25983
26954
|
try {
|
|
25984
|
-
|
|
26955
|
+
link = shareLink(paths[0], base);
|
|
25985
26956
|
} catch (e) {
|
|
25986
26957
|
err(String(e.message));
|
|
25987
26958
|
return 2;
|
|
25988
26959
|
}
|
|
25989
|
-
|
|
26960
|
+
if ("errors" in link) {
|
|
26961
|
+
err(link.errors.map((e) => `error: ${e}`).join("\n"));
|
|
26962
|
+
return 1;
|
|
26963
|
+
}
|
|
26964
|
+
out(link.url);
|
|
25990
26965
|
return 0;
|
|
25991
26966
|
}
|
|
25992
26967
|
|
|
25993
26968
|
// src/importCommand.ts
|
|
25994
|
-
var
|
|
26969
|
+
var import_node_fs11 = require("node:fs");
|
|
25995
26970
|
var import_yaml2 = __toESM(require_dist(), 1);
|
|
25996
26971
|
|
|
25997
26972
|
// ../frontend/src/dsl/importMermaid.ts
|
|
@@ -26591,20 +27566,289 @@ ${IMPORT_USAGE}`);
|
|
|
26591
27566
|
}
|
|
26592
27567
|
let text;
|
|
26593
27568
|
try {
|
|
26594
|
-
text = (0,
|
|
27569
|
+
text = (0, import_node_fs11.readFileSync)(file, "utf8");
|
|
26595
27570
|
} catch (e) {
|
|
26596
27571
|
err(`Cannot read ${file}: ${e.message}`);
|
|
26597
27572
|
return 2;
|
|
26598
27573
|
}
|
|
26599
27574
|
const result = format3 === "mermaid" ? fromMermaid(text) : openApiFromText(text, (t) => (0, import_yaml2.parse)(t));
|
|
26600
27575
|
for (const w of result.warnings) err(`${file}:${w.line ? `${w.line}:` : ""} warning: ${w.message}`);
|
|
26601
|
-
if (output) (0,
|
|
27576
|
+
if (output) (0, import_node_fs11.writeFileSync)(output, result.source);
|
|
26602
27577
|
else out(result.source.trimEnd());
|
|
26603
27578
|
return 0;
|
|
26604
27579
|
}
|
|
26605
27580
|
|
|
27581
|
+
// src/sync.ts
|
|
27582
|
+
var import_node_fs13 = require("node:fs");
|
|
27583
|
+
|
|
27584
|
+
// src/fetchers.ts
|
|
27585
|
+
var import_node_child_process = require("node:child_process");
|
|
27586
|
+
var import_node_fs12 = require("node:fs");
|
|
27587
|
+
var import_node_os = require("node:os");
|
|
27588
|
+
var import_node_path12 = require("node:path");
|
|
27589
|
+
var GIT_TIMEOUT_MS = 12e4;
|
|
27590
|
+
var HTTPS_TIMEOUT_MS = 3e4;
|
|
27591
|
+
var MAX_REDIRECTS = 5;
|
|
27592
|
+
var defaultGitUrl = (repo) => `https://${repo}.git`;
|
|
27593
|
+
function hardening(noHooks) {
|
|
27594
|
+
return [
|
|
27595
|
+
"-c",
|
|
27596
|
+
`core.hooksPath=${noHooks}`,
|
|
27597
|
+
"-c",
|
|
27598
|
+
"core.fsmonitor=false",
|
|
27599
|
+
"-c",
|
|
27600
|
+
"protocol.ext.allow=never",
|
|
27601
|
+
"-c",
|
|
27602
|
+
"fetch.recurseSubmodules=false",
|
|
27603
|
+
"-c",
|
|
27604
|
+
"submodule.recurse=false",
|
|
27605
|
+
"-c",
|
|
27606
|
+
"transfer.fsckObjects=true"
|
|
27607
|
+
];
|
|
27608
|
+
}
|
|
27609
|
+
function gitEnv() {
|
|
27610
|
+
const env = { ...process.env };
|
|
27611
|
+
for (const name of ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_OBJECT_DIRECTORY", "GIT_ALTERNATE_OBJECT_DIRECTORIES", "GIT_COMMON_DIR", "GIT_NAMESPACE"]) delete env[name];
|
|
27612
|
+
return env;
|
|
27613
|
+
}
|
|
27614
|
+
var GitScratch = class {
|
|
27615
|
+
dir = (0, import_node_fs12.mkdtempSync)((0, import_node_path12.join)((0, import_node_os.tmpdir)(), "proschi-git-"));
|
|
27616
|
+
repo = (0, import_node_path12.join)(this.dir, "repo.git");
|
|
27617
|
+
noHooks = (0, import_node_path12.join)(this.dir, "no-hooks");
|
|
27618
|
+
constructor() {
|
|
27619
|
+
(0, import_node_fs12.mkdirSync)(this.noHooks);
|
|
27620
|
+
this.git(["init", "--bare", "--quiet", this.repo], false);
|
|
27621
|
+
}
|
|
27622
|
+
git(args, inRepo = true, encoding = "utf8") {
|
|
27623
|
+
try {
|
|
27624
|
+
return (0, import_node_child_process.execFileSync)("git", [...hardening(this.noHooks), ...inRepo ? ["-C", this.repo] : [], ...args], {
|
|
27625
|
+
env: gitEnv(),
|
|
27626
|
+
timeout: GIT_TIMEOUT_MS,
|
|
27627
|
+
maxBuffer: MAX_IMPORT_BYTES + 1024 * 1024,
|
|
27628
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
27629
|
+
encoding: encoding === "utf8" ? "utf8" : "buffer"
|
|
27630
|
+
});
|
|
27631
|
+
} catch (e) {
|
|
27632
|
+
const stderr = String(e.stderr ?? "").trim();
|
|
27633
|
+
throw new ImportError(`git ${args[0]} failed${stderr ? `: ${stderr.split("\n").pop()}` : ""}`);
|
|
27634
|
+
}
|
|
27635
|
+
}
|
|
27636
|
+
/** Fetches one ref (or commit) shallowly and returns the commit it points at. */
|
|
27637
|
+
fetch(url, ref) {
|
|
27638
|
+
this.git(["fetch", "--quiet", "--depth=1", "--no-tags", "--no-recurse-submodules", "--", url, ref]);
|
|
27639
|
+
return String(this.git(["rev-parse", "--verify", "FETCH_HEAD^{commit}"])).trim();
|
|
27640
|
+
}
|
|
27641
|
+
/** A regular file at `path` in `commit`: never a symlink or submodule, at most MAX_IMPORT_BYTES. */
|
|
27642
|
+
read(commit, path, spec) {
|
|
27643
|
+
const entry = String(this.git(["ls-tree", "-z", commit, "--", path]));
|
|
27644
|
+
const match = /^(\d{6}) (\w+) ([0-9a-f]{40,64})\t/.exec(entry);
|
|
27645
|
+
if (!match) throw new ImportError(`'${spec}': no such file in the repository at ${commit.slice(0, 12)}`);
|
|
27646
|
+
const [, mode, type, oid] = match;
|
|
27647
|
+
if (type !== "blob" || mode !== "100644" && mode !== "100755") throw new ImportError(`'${spec}' is not a regular file (a symlink, folder or submodule)`);
|
|
27648
|
+
const size = Number(String(this.git(["cat-file", "-s", oid])).trim());
|
|
27649
|
+
if (size > MAX_IMPORT_BYTES) throw new ImportError(`'${spec}' is larger than ${MAX_IMPORT_BYTES / 1e6} MB`);
|
|
27650
|
+
return this.git(["cat-file", "blob", oid], true, "buffer");
|
|
27651
|
+
}
|
|
27652
|
+
dispose() {
|
|
27653
|
+
(0, import_node_fs12.rmSync)(this.dir, { recursive: true, force: true });
|
|
27654
|
+
}
|
|
27655
|
+
};
|
|
27656
|
+
async function fetchHttps(url, fetchImpl = fetch) {
|
|
27657
|
+
let current = new URL(url);
|
|
27658
|
+
for (let redirects = 0; ; redirects++) {
|
|
27659
|
+
if (current.protocol !== "https:") throw new ImportError(`'${url}' redirects to ${current.protocol}//; only https:// is allowed`);
|
|
27660
|
+
if (current.username || current.password) throw new ImportError(`'${url}' redirects to a URL with credentials`);
|
|
27661
|
+
let response2;
|
|
27662
|
+
try {
|
|
27663
|
+
response2 = await fetchImpl(current.href, { redirect: "manual", signal: AbortSignal.timeout(HTTPS_TIMEOUT_MS) });
|
|
27664
|
+
} catch (e) {
|
|
27665
|
+
throw new ImportError(`Cannot download '${url}': ${e.message}`);
|
|
27666
|
+
}
|
|
27667
|
+
if (response2.status >= 300 && response2.status < 400) {
|
|
27668
|
+
const location = response2.headers.get("location");
|
|
27669
|
+
if (!location || redirects >= MAX_REDIRECTS) throw new ImportError(`'${url}' redirects too often or without a location`);
|
|
27670
|
+
current = new URL(location, current);
|
|
27671
|
+
continue;
|
|
27672
|
+
}
|
|
27673
|
+
if (!response2.ok) throw new ImportError(`Cannot download '${url}': HTTP ${response2.status}`);
|
|
27674
|
+
if (Number(response2.headers.get("content-length") ?? 0) > MAX_IMPORT_BYTES) throw new ImportError(`'${url}' is larger than ${MAX_IMPORT_BYTES / 1e6} MB`);
|
|
27675
|
+
const chunks = [];
|
|
27676
|
+
let size = 0;
|
|
27677
|
+
const reader = response2.body?.getReader();
|
|
27678
|
+
for (; ; ) {
|
|
27679
|
+
const chunk = reader ? await reader.read() : { done: true, value: void 0 };
|
|
27680
|
+
if (chunk.done) break;
|
|
27681
|
+
size += chunk.value.byteLength;
|
|
27682
|
+
if (size > MAX_IMPORT_BYTES) {
|
|
27683
|
+
await reader?.cancel();
|
|
27684
|
+
throw new ImportError(`'${url}' is larger than ${MAX_IMPORT_BYTES / 1e6} MB`);
|
|
27685
|
+
}
|
|
27686
|
+
chunks.push(chunk.value);
|
|
27687
|
+
}
|
|
27688
|
+
return Buffer.concat(chunks);
|
|
27689
|
+
}
|
|
27690
|
+
}
|
|
27691
|
+
|
|
27692
|
+
// src/sync.ts
|
|
27693
|
+
var IMPORTS_USAGE = ` proschi imports sync [--frozen] [<file|dir>...]`;
|
|
27694
|
+
var IMPORTS_HELP = `imports sync
|
|
27695
|
+
Downloads the git: and https:// imports of the files (default: the
|
|
27696
|
+
current folder) into .proschi/cache and pins them in proschi.lock.
|
|
27697
|
+
No other command uses the network. --frozen fails instead of
|
|
27698
|
+
changing proschi.lock (for CI).`;
|
|
27699
|
+
var staleLock = (what) => new ImportError(`${LOCK_FILE} is out of date: ${what}; run proschi imports sync and commit ${LOCK_FILE}`);
|
|
27700
|
+
async function syncImports(entries2, { frozen = false, fetch: fetch2, gitUrl = defaultGitUrl } = {}) {
|
|
27701
|
+
const roots = [...new Set(entries2.map((e) => projectRoot(e)))];
|
|
27702
|
+
if (roots.length !== 1) {
|
|
27703
|
+
const errors2 = roots.length ? [`The files belong to different project roots (${roots.map((r) => displayPath3(r)).join(", ")}); sync each separately`] : ["No .proschi files to sync"];
|
|
27704
|
+
return { root: roots[0] ?? "", lock: emptyLock(), errors: errors2, written: false };
|
|
27705
|
+
}
|
|
27706
|
+
const [root] = roots;
|
|
27707
|
+
const lock = emptyLock();
|
|
27708
|
+
let old;
|
|
27709
|
+
try {
|
|
27710
|
+
old = readLock(root);
|
|
27711
|
+
} catch (e) {
|
|
27712
|
+
return { root, lock, errors: [e.message], written: false };
|
|
27713
|
+
}
|
|
27714
|
+
if (frozen && !old) return { root, lock, errors: [`${LOCK_FILE} is missing; run proschi imports sync and commit it`], written: false };
|
|
27715
|
+
const texts = /* @__PURE__ */ new Map();
|
|
27716
|
+
const failed = /* @__PURE__ */ new Map();
|
|
27717
|
+
const scratches = /* @__PURE__ */ new Map();
|
|
27718
|
+
const commits = /* @__PURE__ */ new Map();
|
|
27719
|
+
const obtain = async (spec) => {
|
|
27720
|
+
const remote = parseRemoteSpec(spec);
|
|
27721
|
+
const cached = (hash) => {
|
|
27722
|
+
try {
|
|
27723
|
+
return hash ? readCached(root, hash, spec) : void 0;
|
|
27724
|
+
} catch {
|
|
27725
|
+
return void 0;
|
|
27726
|
+
}
|
|
27727
|
+
};
|
|
27728
|
+
const changed = (pin) => new ImportError(`'${spec}' does not match its sha256 in ${LOCK_FILE} (${pin}); its content changed. If that is expected, delete its entry from ${LOCK_FILE} and sync again`);
|
|
27729
|
+
if (remote.kind === "https") {
|
|
27730
|
+
const pinned = old?.https[remote.url];
|
|
27731
|
+
if (frozen && !pinned) throw staleLock(`'${spec}' is not locked`);
|
|
27732
|
+
let bytes2 = cached(pinned);
|
|
27733
|
+
if (!bytes2) {
|
|
27734
|
+
bytes2 = await fetchHttps(remote.url, fetch2);
|
|
27735
|
+
if (pinned && sha256(bytes2) !== pinned) throw changed(`sha256 ${pinned.slice(0, 12)}`);
|
|
27736
|
+
writeCached(root, bytes2);
|
|
27737
|
+
}
|
|
27738
|
+
lock.https[remote.url] = sha256(bytes2);
|
|
27739
|
+
return bytes2;
|
|
27740
|
+
}
|
|
27741
|
+
const key = gitKey(remote.repo, remote.ref);
|
|
27742
|
+
const pinnedEntry = old?.git[key];
|
|
27743
|
+
const pinnedHash = pinnedEntry?.files[remote.path];
|
|
27744
|
+
if (frozen && !pinnedHash) throw staleLock(`'${spec}' is not locked`);
|
|
27745
|
+
let bytes = cached(pinnedHash);
|
|
27746
|
+
let commit = pinnedEntry?.commit;
|
|
27747
|
+
if (!bytes) {
|
|
27748
|
+
let scratch = scratches.get(remote.repo);
|
|
27749
|
+
if (!scratch) scratches.set(remote.repo, scratch = new GitScratch());
|
|
27750
|
+
if (!commits.has(key)) {
|
|
27751
|
+
const got = scratch.fetch(gitUrl(remote.repo), commit ?? remote.ref);
|
|
27752
|
+
if (commit && got !== commit) throw new ImportError(`'${spec}': fetching the locked commit ${commit} gave ${got}`);
|
|
27753
|
+
if (/^[0-9a-f]{40}$/.test(remote.ref) && got !== remote.ref) throw new ImportError(`'${spec}': the ref is a commit but resolved to ${got}`);
|
|
27754
|
+
commits.set(key, got);
|
|
27755
|
+
}
|
|
27756
|
+
commit = commits.get(key);
|
|
27757
|
+
bytes = scratch.read(commit, remote.path, spec);
|
|
27758
|
+
if (pinnedHash && sha256(bytes) !== pinnedHash) throw changed(`commit ${commit.slice(0, 12)}`);
|
|
27759
|
+
writeCached(root, bytes);
|
|
27760
|
+
}
|
|
27761
|
+
lock.git[key] ??= { commit, files: {} };
|
|
27762
|
+
lock.git[key].files[remote.path] = sha256(bytes);
|
|
27763
|
+
return bytes;
|
|
27764
|
+
};
|
|
27765
|
+
let errors = [];
|
|
27766
|
+
try {
|
|
27767
|
+
for (let round2 = 0; round2 <= MAX_IMPORT_DEPTH + 1; round2++) {
|
|
27768
|
+
const pending = /* @__PURE__ */ new Set();
|
|
27769
|
+
const found = /* @__PURE__ */ new Set();
|
|
27770
|
+
for (const entry of entries2) {
|
|
27771
|
+
const resolver = fileResolver({
|
|
27772
|
+
entry,
|
|
27773
|
+
remote: (spec) => {
|
|
27774
|
+
if (texts.has(spec)) return texts.get(spec);
|
|
27775
|
+
if (!failed.has(spec)) pending.add(spec);
|
|
27776
|
+
return void 0;
|
|
27777
|
+
}
|
|
27778
|
+
});
|
|
27779
|
+
parse((0, import_node_fs13.readFileSync)(entry, "utf8"), { path: entry, resolve: resolver });
|
|
27780
|
+
for (const [where, message] of resolver.errors) {
|
|
27781
|
+
const from = where.split("\0")[0] || entry;
|
|
27782
|
+
found.add(`${isRemoteSpec(from) ? from : displayPath3(from)}: ${message}`);
|
|
27783
|
+
}
|
|
27784
|
+
}
|
|
27785
|
+
if (pending.size === 0) {
|
|
27786
|
+
errors = [...found];
|
|
27787
|
+
break;
|
|
27788
|
+
}
|
|
27789
|
+
for (const spec of pending) {
|
|
27790
|
+
try {
|
|
27791
|
+
texts.set(spec, (await obtain(spec)).toString("utf8"));
|
|
27792
|
+
} catch (e) {
|
|
27793
|
+
if (!(e instanceof ImportError)) throw e;
|
|
27794
|
+
failed.set(spec, e.message);
|
|
27795
|
+
}
|
|
27796
|
+
}
|
|
27797
|
+
}
|
|
27798
|
+
} finally {
|
|
27799
|
+
for (const scratch of scratches.values()) scratch.dispose();
|
|
27800
|
+
}
|
|
27801
|
+
errors.push(...failed.values());
|
|
27802
|
+
if (errors.length) return { root, lock, errors, written: false };
|
|
27803
|
+
const text = formatLock(lock);
|
|
27804
|
+
if (old && text !== formatLock(old)) {
|
|
27805
|
+
if (frozen) return { root, lock, errors: [staleLock("it locks imports that are no longer used").message], written: false };
|
|
27806
|
+
}
|
|
27807
|
+
const write = !frozen && (old ? text !== formatLock(old) : Object.keys(lock.git).length + Object.keys(lock.https).length > 0);
|
|
27808
|
+
if (write) writeLock(root, lock);
|
|
27809
|
+
return { root, lock, errors, written: write };
|
|
27810
|
+
}
|
|
27811
|
+
async function runImports(args, collectFiles2, out, err, options = {}) {
|
|
27812
|
+
const [sub, ...rest] = args;
|
|
27813
|
+
if (sub !== "sync") {
|
|
27814
|
+
err(`Usage:
|
|
27815
|
+
${IMPORTS_USAGE}`);
|
|
27816
|
+
return 2;
|
|
27817
|
+
}
|
|
27818
|
+
let frozen = false;
|
|
27819
|
+
const paths = [];
|
|
27820
|
+
for (const arg of rest) {
|
|
27821
|
+
if (arg === "--frozen") frozen = true;
|
|
27822
|
+
else if (arg.startsWith("-")) {
|
|
27823
|
+
err(`Unknown option ${arg}
|
|
27824
|
+
|
|
27825
|
+
Usage:
|
|
27826
|
+
${IMPORTS_USAGE}`);
|
|
27827
|
+
return 2;
|
|
27828
|
+
} else paths.push(arg);
|
|
27829
|
+
}
|
|
27830
|
+
let files;
|
|
27831
|
+
try {
|
|
27832
|
+
files = collectFiles2(paths.length ? paths : ["."]);
|
|
27833
|
+
} catch (e) {
|
|
27834
|
+
err(String(e.message));
|
|
27835
|
+
return 2;
|
|
27836
|
+
}
|
|
27837
|
+
const result = await syncImports(files, { ...options, frozen });
|
|
27838
|
+
if (result.errors.length) {
|
|
27839
|
+
err(result.errors.map((e) => `error: ${e}`).join("\n"));
|
|
27840
|
+
return 1;
|
|
27841
|
+
}
|
|
27842
|
+
const sources = Object.keys(result.lock.git).length + Object.keys(result.lock.https).length;
|
|
27843
|
+
const filesLocked = Object.values(result.lock.git).reduce((n, g) => n + Object.keys(g.files).length, 0) + Object.keys(result.lock.https).length;
|
|
27844
|
+
const lockPath = displayPath3(`${result.root}/${LOCK_FILE}`);
|
|
27845
|
+
if (!sources) out("No git: or https:// imports to sync");
|
|
27846
|
+
else out(`${filesLocked} remote file(s) from ${sources} source(s) ${result.written ? "locked in" : "match"} ${lockPath}`);
|
|
27847
|
+
return 0;
|
|
27848
|
+
}
|
|
27849
|
+
|
|
26606
27850
|
// src/cli.ts
|
|
26607
|
-
var VERSION2 = true ? "0.
|
|
27851
|
+
var VERSION2 = true ? "0.13.0" : "dev";
|
|
26608
27852
|
var USAGE = `Usage:
|
|
26609
27853
|
proschi check [--strict] [--format text|github|json] [--openapi <node>=<spec>]... <file|dir>...
|
|
26610
27854
|
proschi parse <file>
|
|
@@ -26617,6 +27861,7 @@ ${ACHIEVEMENTS_USAGE}
|
|
|
26617
27861
|
${GAME_USAGE}
|
|
26618
27862
|
${SHARE_USAGE}
|
|
26619
27863
|
${IMPORT_USAGE}
|
|
27864
|
+
${IMPORTS_USAGE}
|
|
26620
27865
|
proschi --version
|
|
26621
27866
|
|
|
26622
27867
|
check Reports errors and warnings. Directories are searched for *.proschi files.
|
|
@@ -26635,19 +27880,20 @@ ${CARDS_HELP}
|
|
|
26635
27880
|
${ACHIEVEMENTS_HELP}
|
|
26636
27881
|
${GAME_HELP}
|
|
26637
27882
|
${SHARE_HELP}
|
|
26638
|
-
${IMPORT_HELP}
|
|
27883
|
+
${IMPORT_HELP}
|
|
27884
|
+
${IMPORTS_HELP}`;
|
|
26639
27885
|
function collectFiles(paths) {
|
|
26640
27886
|
const out = [];
|
|
26641
27887
|
const walk = (dir) => {
|
|
26642
|
-
for (const entry of (0,
|
|
27888
|
+
for (const entry of (0, import_node_fs14.readdirSync)(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
26643
27889
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
26644
|
-
const full = (0,
|
|
27890
|
+
const full = (0, import_node_path13.join)(dir, entry.name);
|
|
26645
27891
|
if (entry.isDirectory()) walk(full);
|
|
26646
27892
|
else if (entry.name.endsWith(".proschi")) out.push(full);
|
|
26647
27893
|
}
|
|
26648
27894
|
};
|
|
26649
27895
|
for (const p of paths) {
|
|
26650
|
-
if ((0,
|
|
27896
|
+
if ((0, import_node_fs14.statSync)(p).isDirectory()) walk(p);
|
|
26651
27897
|
else out.push(p);
|
|
26652
27898
|
}
|
|
26653
27899
|
return out;
|
|
@@ -26701,6 +27947,7 @@ function run(argv, out = console.log, err = console.error) {
|
|
|
26701
27947
|
if (command === "game") return runGame(rest, out, err);
|
|
26702
27948
|
if (command === "share-link") return runShareLink(rest, out, err);
|
|
26703
27949
|
if (command === "import") return runImport(rest, out, err);
|
|
27950
|
+
if (command === "imports") return runImports(rest, collectFiles, out, err);
|
|
26704
27951
|
if (command === "check") {
|
|
26705
27952
|
let strict = false;
|
|
26706
27953
|
let style = "text";
|