@styx-api/core 0.6.0 → 0.7.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/index.mjs CHANGED
@@ -732,6 +732,972 @@ var ArgdumpParser = class {
732
732
  }
733
733
  };
734
734
 
735
+ //#endregion
736
+ //#region src/frontend/argtype/lexer.ts
737
+ const PUNCT = {
738
+ ":": "colon",
739
+ "=": "eq",
740
+ "|": "pipe",
741
+ ".": "dot",
742
+ ",": "comma",
743
+ "(": "lparen",
744
+ ")": "rparen"
745
+ };
746
+ function isIdentStart(ch) {
747
+ return /[A-Za-z_]/.test(ch);
748
+ }
749
+ function isIdentPart(ch) {
750
+ return /[A-Za-z0-9_]/.test(ch);
751
+ }
752
+ function isDigit(ch) {
753
+ return ch >= "0" && ch <= "9";
754
+ }
755
+ function lex(source) {
756
+ const tokens = [];
757
+ const errors = [];
758
+ let i = 0;
759
+ let line = 1;
760
+ let col = 1;
761
+ const peek = (o = 0) => source[i + o] ?? "";
762
+ const advance = () => {
763
+ const ch = source[i++];
764
+ if (ch === "\n") {
765
+ line++;
766
+ col = 1;
767
+ } else col++;
768
+ return ch;
769
+ };
770
+ const push = (kind, value, l, c) => {
771
+ tokens.push({
772
+ kind,
773
+ value,
774
+ line: l,
775
+ column: c
776
+ });
777
+ };
778
+ while (i < source.length) {
779
+ const ch = peek();
780
+ const startLine = line;
781
+ const startCol = col;
782
+ if (ch === " " || ch === " " || ch === "\r" || ch === "\n") {
783
+ advance();
784
+ continue;
785
+ }
786
+ if (ch === "/" && peek(1) === "/") {
787
+ const isDoc = peek(2) === "/";
788
+ advance();
789
+ advance();
790
+ if (isDoc) advance();
791
+ let text = "";
792
+ while (i < source.length && peek() !== "\n") text += advance();
793
+ if (isDoc) push("doc", text.trim(), startLine, startCol);
794
+ continue;
795
+ }
796
+ if (ch === "\"") {
797
+ advance();
798
+ let str = "";
799
+ let closed = false;
800
+ while (i < source.length) {
801
+ const c = advance();
802
+ if (c === "\\") {
803
+ const esc = i < source.length ? advance() : "";
804
+ str += unescape(esc);
805
+ } else if (c === "\"") {
806
+ closed = true;
807
+ break;
808
+ } else if (c === "\n") break;
809
+ else str += c;
810
+ }
811
+ if (!closed) errors.push({
812
+ message: "Unterminated string literal",
813
+ line: startLine,
814
+ column: startCol
815
+ });
816
+ push("string", str, startLine, startCol);
817
+ continue;
818
+ }
819
+ if (ch === "`") {
820
+ advance();
821
+ let body = "";
822
+ let closed = false;
823
+ while (i < source.length) {
824
+ const c = advance();
825
+ if (c === "\\") {
826
+ body += c;
827
+ if (i < source.length) body += advance();
828
+ continue;
829
+ }
830
+ if (c === "`") {
831
+ closed = true;
832
+ break;
833
+ }
834
+ body += c;
835
+ }
836
+ if (!closed) errors.push({
837
+ message: "Unterminated template literal",
838
+ line: startLine,
839
+ column: startCol
840
+ });
841
+ push("template", body, startLine, startCol);
842
+ continue;
843
+ }
844
+ if (isDigit(ch) || ch === "-" && isDigit(peek(1))) {
845
+ let num = advance();
846
+ while (i < source.length && isDigit(peek())) num += advance();
847
+ if (peek() === "." && isDigit(peek(1))) {
848
+ num += advance();
849
+ while (i < source.length && isDigit(peek())) num += advance();
850
+ }
851
+ if (peek() === "e" || peek() === "E") {
852
+ const signed = peek(1) === "+" || peek(1) === "-";
853
+ if (isDigit(peek(1)) || signed && isDigit(peek(2))) {
854
+ num += advance();
855
+ if (peek() === "+" || peek() === "-") num += advance();
856
+ while (i < source.length && isDigit(peek())) num += advance();
857
+ } else {
858
+ num += advance();
859
+ errors.push({
860
+ message: `Malformed number '${num}': exponent has no digits`,
861
+ line: startLine,
862
+ column: startCol
863
+ });
864
+ }
865
+ }
866
+ push("number", num, startLine, startCol);
867
+ continue;
868
+ }
869
+ if (isIdentStart(ch)) {
870
+ let id = advance();
871
+ while (i < source.length && isIdentPart(peek())) id += advance();
872
+ push("ident", id, startLine, startCol);
873
+ continue;
874
+ }
875
+ const punct = PUNCT[ch];
876
+ if (punct) {
877
+ advance();
878
+ push(punct, ch, startLine, startCol);
879
+ continue;
880
+ }
881
+ errors.push({
882
+ message: `Unexpected character '${ch}'`,
883
+ line: startLine,
884
+ column: startCol
885
+ });
886
+ advance();
887
+ }
888
+ push("eof", "", line, col);
889
+ return {
890
+ tokens,
891
+ errors
892
+ };
893
+ }
894
+ /** Minimal escape handling inside double-quoted strings. */
895
+ function unescape(esc) {
896
+ switch (esc) {
897
+ case "n": return "\n";
898
+ case "t": return " ";
899
+ case "r": return "\r";
900
+ case "\"": return "\"";
901
+ case "\\": return "\\";
902
+ case "": return "";
903
+ default: return esc;
904
+ }
905
+ }
906
+
907
+ //#endregion
908
+ //#region src/frontend/argtype/frontmatter.ts
909
+ function splitFrontmatter(source) {
910
+ const lines = source.split("\n");
911
+ let start = 0;
912
+ while (start < lines.length && lines[start].trim() === "") start++;
913
+ if (lines[start]?.trim() !== "---") return {
914
+ body: source,
915
+ errors: []
916
+ };
917
+ let end = -1;
918
+ for (let i = start + 1; i < lines.length; i++) if (lines[i].trim() === "---") {
919
+ end = i;
920
+ break;
921
+ }
922
+ if (end === -1) return {
923
+ body: source,
924
+ errors: ["Unterminated frontmatter block (missing closing '---')"]
925
+ };
926
+ const { value, errors } = parseYamlish(lines.slice(start + 1, end));
927
+ return {
928
+ frontmatter: value,
929
+ body: lines.map((line, i) => i >= start && i <= end ? "" : line).join("\n"),
930
+ errors
931
+ };
932
+ }
933
+ function tokenizeLines(raw) {
934
+ const out = [];
935
+ for (const line of raw) {
936
+ const noComment = stripComment(line);
937
+ if (noComment.trim() === "") continue;
938
+ const indent = noComment.length - noComment.trimStart().length;
939
+ out.push({
940
+ indent,
941
+ content: noComment.trim()
942
+ });
943
+ }
944
+ return out;
945
+ }
946
+ /** Strip a trailing `#` comment that is not inside quotes. Following YAML, a
947
+ * `#` only starts a comment at the start of the line or after whitespace, so an
948
+ * unquoted value containing `#` (e.g. a URL fragment `https://x/#frag`) is kept
949
+ * intact. */
950
+ function stripComment(line) {
951
+ let inQuote = null;
952
+ for (let i = 0; i < line.length; i++) {
953
+ const ch = line[i];
954
+ if (inQuote === "\"" && ch === "\\") {
955
+ i++;
956
+ continue;
957
+ }
958
+ if (inQuote) {
959
+ if (ch === inQuote) inQuote = null;
960
+ } else if (ch === "\"" || ch === "'") inQuote = ch;
961
+ else if (ch === "#" && (i === 0 || line[i - 1] === " " || line[i - 1] === " ")) return line.slice(0, i);
962
+ }
963
+ return line;
964
+ }
965
+ function parseYamlish(raw) {
966
+ const lines = tokenizeLines(raw);
967
+ const errors = [];
968
+ let pos = 0;
969
+ function parseBlock(minIndent) {
970
+ if (pos < lines.length && lines[pos].indent >= minIndent && lines[pos].content.startsWith("- ")) {
971
+ const seq = [];
972
+ const indent = lines[pos].indent;
973
+ while (pos < lines.length && lines[pos].indent === indent && lines[pos].content.startsWith("- ")) {
974
+ seq.push(parseScalar(lines[pos].content.slice(2).trim()));
975
+ pos++;
976
+ }
977
+ return seq;
978
+ }
979
+ const map = {};
980
+ if (pos >= lines.length) return map;
981
+ const indent = lines[pos].indent;
982
+ while (pos < lines.length && lines[pos].indent === indent && !lines[pos].content.startsWith("- ")) {
983
+ const { content } = lines[pos];
984
+ const colon = findColon(content);
985
+ if (colon === -1) {
986
+ errors.push(`Malformed frontmatter line: '${content}'`);
987
+ pos++;
988
+ continue;
989
+ }
990
+ const key = content.slice(0, colon).trim();
991
+ const valueText = content.slice(colon + 1).trim();
992
+ pos++;
993
+ if (valueText !== "") map[key] = parseScalar(valueText);
994
+ else if (pos < lines.length && lines[pos].indent > indent) map[key] = parseBlock(lines[pos].indent);
995
+ else map[key] = null;
996
+ }
997
+ return map;
998
+ }
999
+ const value = parseBlock(0);
1000
+ return {
1001
+ value: isRecord$2(value) ? value : { _root: value },
1002
+ errors
1003
+ };
1004
+ }
1005
+ function findColon(s) {
1006
+ let inQuote = null;
1007
+ for (let i = 0; i < s.length; i++) {
1008
+ const ch = s[i];
1009
+ if (inQuote === "\"" && ch === "\\") {
1010
+ i++;
1011
+ continue;
1012
+ }
1013
+ if (inQuote) {
1014
+ if (ch === inQuote) inQuote = null;
1015
+ } else if (ch === "\"" || ch === "'") inQuote = ch;
1016
+ else if (ch === ":") return i;
1017
+ }
1018
+ return -1;
1019
+ }
1020
+ /** Reverse the backslash escapes an emitter applies to a quoted scalar
1021
+ * (`\n`, `\t`, `\r`, `\"`, `\\`), so values with newlines/quotes round-trip. */
1022
+ function unescapeScalar(s) {
1023
+ return s.replace(/\\(.)/g, (_, c) => c === "n" ? "\n" : c === "t" ? " " : c === "r" ? "\r" : c);
1024
+ }
1025
+ function parseScalar(text) {
1026
+ if (text.startsWith("\"") && text.endsWith("\"") && text.length >= 2) return unescapeScalar(text.slice(1, -1));
1027
+ if (text.startsWith("'") && text.endsWith("'") && text.length >= 2) return text.slice(1, -1);
1028
+ if (text === "true") return true;
1029
+ if (text === "false") return false;
1030
+ if (text === "null" || text === "~") return null;
1031
+ if (/^-?\d+(\.\d+)?$/.test(text)) return Number(text);
1032
+ return text;
1033
+ }
1034
+ function isRecord$2(x) {
1035
+ return typeof x === "object" && x !== null && !Array.isArray(x);
1036
+ }
1037
+
1038
+ //#endregion
1039
+ //#region src/frontend/argtype/template.ts
1040
+ /**
1041
+ * Index of the `}` that closes an interpolation opened at `open` (the position
1042
+ * just past the `{`), or -1 if unterminated. A naive `indexOf("}")` would stop
1043
+ * at the first `}` even when it sits inside a quoted ref name (`{"a}b"}`) or a
1044
+ * quoted operation argument (`{a.or("{b}")}`); this scan skips double-quoted
1045
+ * spans (honoring `\"`) and balances nested `{}` so those round-trip.
1046
+ */
1047
+ function interpolationEnd(body, open) {
1048
+ let depth = 1;
1049
+ let inQuote = false;
1050
+ for (let j = open; j < body.length; j++) {
1051
+ const c = body[j];
1052
+ if (inQuote) {
1053
+ if (c === "\\") j++;
1054
+ else if (c === "\"") inQuote = false;
1055
+ continue;
1056
+ }
1057
+ if (c === "\"") inQuote = true;
1058
+ else if (c === "{") depth++;
1059
+ else if (c === "}" && --depth === 0) return j;
1060
+ }
1061
+ return -1;
1062
+ }
1063
+ function parseTemplate(body) {
1064
+ const tokens = [];
1065
+ const errors = [];
1066
+ let lit = "";
1067
+ let i = 0;
1068
+ const flushLit = () => {
1069
+ if (lit.length > 0) {
1070
+ tokens.push({
1071
+ kind: "literal",
1072
+ value: lit
1073
+ });
1074
+ lit = "";
1075
+ }
1076
+ };
1077
+ while (i < body.length) {
1078
+ const ch = body[i];
1079
+ if (ch === "\\" && i + 1 < body.length) {
1080
+ const next = body[i + 1];
1081
+ lit += next === "{" || next === "}" || next === "`" || next === "\\" ? next : "\\" + next;
1082
+ i += 2;
1083
+ continue;
1084
+ }
1085
+ if (ch === "{") {
1086
+ const close = interpolationEnd(body, i + 1);
1087
+ if (close === -1) {
1088
+ errors.push("Unterminated '{' in output template");
1089
+ lit += body.slice(i);
1090
+ break;
1091
+ }
1092
+ flushLit();
1093
+ const { token, error } = parseRef(body.slice(i + 1, close).trim());
1094
+ if (error) errors.push(error);
1095
+ tokens.push(token);
1096
+ i = close + 1;
1097
+ } else {
1098
+ lit += ch;
1099
+ i++;
1100
+ }
1101
+ }
1102
+ flushLit();
1103
+ return {
1104
+ tokens,
1105
+ errors
1106
+ };
1107
+ }
1108
+ /** Parse the inside of `{...}`: an optional name followed by `.op(arg)` calls.
1109
+ * The name is a bare identifier, a double-quoted string (for a non-identifier
1110
+ * target name, e.g. `{"4d_output"}`), or absent for a self-reference (`{}`). */
1111
+ function parseRef(inner) {
1112
+ const token = { kind: "ref" };
1113
+ let rest = inner;
1114
+ const quoted = /^"((?:[^"\\]|\\.)*)"/.exec(inner);
1115
+ if (quoted) {
1116
+ token.name = unescapeArg(quoted[1]);
1117
+ rest = inner.slice(quoted[0].length);
1118
+ } else {
1119
+ const nameMatch = /^[A-Za-z_][A-Za-z0-9_]*/.exec(inner);
1120
+ if (nameMatch) {
1121
+ token.name = nameMatch[0];
1122
+ rest = inner.slice(nameMatch[0].length);
1123
+ }
1124
+ }
1125
+ const opRe = /^\.\s*([A-Za-z_]+)\s*\(\s*(?:"((?:[^"\\]|\\.)*)")?\s*\)/;
1126
+ let error;
1127
+ while (rest.length > 0) {
1128
+ const m = opRe.exec(rest);
1129
+ if (!m) {
1130
+ error = `Unrecognized output-reference operation near '${rest}'`;
1131
+ break;
1132
+ }
1133
+ const op = m[1];
1134
+ const arg = m[2];
1135
+ switch (op) {
1136
+ case "strip_suffix":
1137
+ if (arg !== void 0) (token.stripSuffix ??= []).push(unescapeArg(arg));
1138
+ break;
1139
+ case "strip_prefix":
1140
+ if (arg !== void 0) (token.stripPrefix ??= []).push(unescapeArg(arg));
1141
+ break;
1142
+ case "basename":
1143
+ token.basename = true;
1144
+ break;
1145
+ case "or":
1146
+ if (arg !== void 0) token.or = unescapeArg(arg);
1147
+ break;
1148
+ default: error = `Unknown output-reference operation '${op}'`;
1149
+ }
1150
+ rest = rest.slice(m[0].length).trimStart();
1151
+ }
1152
+ return {
1153
+ token,
1154
+ ...error && { error }
1155
+ };
1156
+ }
1157
+ function unescapeArg(s) {
1158
+ return s.replace(/\\(.)/g, "$1");
1159
+ }
1160
+
1161
+ //#endregion
1162
+ //#region src/frontend/argtype/doc.ts
1163
+ /**
1164
+ * Reflow a `///` description as Markdown-style prose: a single line break is a
1165
+ * soft wrap (the lines join with a space) and a blank line starts a new
1166
+ * paragraph (kept as `\n\n`). This lets an author wrap a long description across
1167
+ * several `///` lines for readability without forcing hard breaks, while real
1168
+ * paragraph structure survives. (It does not preserve Markdown lists or indented
1169
+ * code blocks - their single line breaks are joined like ordinary prose.)
1170
+ */
1171
+ function reflowProse(text) {
1172
+ return text.split(/\n{2,}/).map((para) => para.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join(" ")).filter((para) => para.length > 0).join("\n\n");
1173
+ }
1174
+ /**
1175
+ * Split a `///` doc block into title + description. A leading Markdown H1
1176
+ * (`# Title` on the first line) is the title; everything after it is the
1177
+ * description. Without a leading `# `, the whole block is the description (no
1178
+ * title). The description is reflowed as prose (see `reflowProse`): single line
1179
+ * breaks soft-wrap, blank lines separate paragraphs.
1180
+ */
1181
+ function splitDocText(raw) {
1182
+ const newline = raw.indexOf("\n");
1183
+ const firstLine = (newline === -1 ? raw : raw.slice(0, newline)).trim();
1184
+ if (firstLine.startsWith("# ")) {
1185
+ const title = firstLine.slice(2).trim();
1186
+ const description = reflowProse(newline === -1 ? "" : raw.slice(newline + 1));
1187
+ return {
1188
+ ...title && { title },
1189
+ ...description && { description }
1190
+ };
1191
+ }
1192
+ const description = reflowProse(raw);
1193
+ return description ? { description } : {};
1194
+ }
1195
+
1196
+ //#endregion
1197
+ //#region src/frontend/argtype/parser.ts
1198
+ /** Split a `///` block and attach its title/description to a node. Per the spec,
1199
+ * the block "is applied first and wins": for each field the block provides it
1200
+ * overrides a value a chained `.title()`/`.description()` already set, while a
1201
+ * field the block leaves unset keeps whatever the chain supplied. */
1202
+ function attachDocText(target, raw) {
1203
+ const { title, description } = splitDocText(raw);
1204
+ if (title !== void 0) target.title = title;
1205
+ if (description !== void 0) target.description = description;
1206
+ }
1207
+ const COMBINATORS = new Set([
1208
+ "seq",
1209
+ "set",
1210
+ "opt",
1211
+ "rep",
1212
+ "alt",
1213
+ "any"
1214
+ ]);
1215
+ const TERMINALS = new Set([
1216
+ "int",
1217
+ "float",
1218
+ "str",
1219
+ "path"
1220
+ ]);
1221
+ /** Core + supported-extension chaining methods. Anything else is an extension
1222
+ * we don't implement and is parsed-and-ignored (the spec's "ignorable" rule). */
1223
+ const KNOWN_METHODS = new Set([
1224
+ "name",
1225
+ "title",
1226
+ "description",
1227
+ "default",
1228
+ "min",
1229
+ "max",
1230
+ "join",
1231
+ "count",
1232
+ "countMin",
1233
+ "countMax",
1234
+ "mediaType",
1235
+ "mutable",
1236
+ "resolveParent"
1237
+ ]);
1238
+ var Parser = class {
1239
+ tokens;
1240
+ pos = 0;
1241
+ errors = [];
1242
+ warnings = [];
1243
+ constructor(tokens) {
1244
+ this.tokens = tokens;
1245
+ }
1246
+ peek(o = 0) {
1247
+ return this.tokens[Math.min(this.pos + o, this.tokens.length - 1)];
1248
+ }
1249
+ at(kind) {
1250
+ return this.peek().kind === kind;
1251
+ }
1252
+ next() {
1253
+ return this.tokens[this.pos++] ?? this.tokens[this.tokens.length - 1];
1254
+ }
1255
+ expect(kind, what) {
1256
+ if (this.at(kind)) return this.next();
1257
+ const tok = this.peek();
1258
+ this.error(`Expected ${what} but found '${tok.value || tok.kind}'`, tok);
1259
+ return tok;
1260
+ }
1261
+ error(message, tok = this.peek()) {
1262
+ this.errors.push({
1263
+ message,
1264
+ line: tok.line,
1265
+ column: tok.column
1266
+ });
1267
+ }
1268
+ warn(message, tok = this.peek()) {
1269
+ this.warnings.push({
1270
+ message,
1271
+ line: tok.line,
1272
+ column: tok.column
1273
+ });
1274
+ }
1275
+ parseDocument(frontmatter) {
1276
+ const aliases = [];
1277
+ let rootName;
1278
+ let root;
1279
+ while (!this.at("eof")) {
1280
+ const docs = this.collectDocs();
1281
+ if (this.at("eof")) break;
1282
+ const labelLike = this.at("ident") || this.at("string");
1283
+ const following = this.peek(1).kind;
1284
+ if (labelLike && following === "eq") {
1285
+ const nameTok = this.next();
1286
+ if (nameTok.kind === "string") this.error("Alias names must be identifiers, not quoted strings", nameTok);
1287
+ this.next();
1288
+ const expr = this.parseElement();
1289
+ if (docs) attachDocText(expr, docs);
1290
+ aliases.push({
1291
+ name: nameTok.value,
1292
+ expr
1293
+ });
1294
+ continue;
1295
+ }
1296
+ if (labelLike && following === "colon") {
1297
+ const nameTok = this.next();
1298
+ this.next();
1299
+ const expr = this.parseElement();
1300
+ if (docs) attachDocText(expr, docs);
1301
+ if (root) this.error(`Multiple root definitions; '${nameTok.value}' ignored (already have '${rootName ?? "<anonymous>"}')`, nameTok);
1302
+ else {
1303
+ rootName = nameTok.value;
1304
+ root = expr;
1305
+ if (!root.name) root.name = nameTok.value;
1306
+ }
1307
+ continue;
1308
+ }
1309
+ if (!this.at("ident") && !this.at("string") && !this.at("lparen")) {
1310
+ this.error("Expected a definition (name: expr), an alias (Name = expr), or a root expression");
1311
+ break;
1312
+ }
1313
+ const expr = this.parseElement();
1314
+ if (docs) attachDocText(expr, docs);
1315
+ if (root) {
1316
+ this.error("Multiple root definitions; a second top-level expression is not allowed");
1317
+ break;
1318
+ }
1319
+ root = expr;
1320
+ }
1321
+ if (!root) {
1322
+ this.error("No root definition (expected `name: expr` or a bare root expression)");
1323
+ return;
1324
+ }
1325
+ return {
1326
+ ...frontmatter && { frontmatter },
1327
+ aliases,
1328
+ ...rootName !== void 0 && { rootName },
1329
+ root
1330
+ };
1331
+ }
1332
+ /** Collect consecutive `///` doc lines, joined with newlines. */
1333
+ collectDocs() {
1334
+ const lines = [];
1335
+ while (this.at("doc")) lines.push(this.next().value);
1336
+ return lines.length > 0 ? lines.join("\n") : void 0;
1337
+ }
1338
+ /**
1339
+ * An element is the unit inside a comma list: an optionally-named expression.
1340
+ * `label:` is looser than `|`, so a name binds the whole alternative that
1341
+ * follows it.
1342
+ */
1343
+ parseElement() {
1344
+ if ((this.at("ident") || this.at("string")) && this.peek(1).kind === "colon") {
1345
+ const nameTok = this.next();
1346
+ this.next();
1347
+ const inner = this.parseElement();
1348
+ inner.name = nameTok.value;
1349
+ return inner;
1350
+ }
1351
+ return this.parseAlt();
1352
+ }
1353
+ parseAlt() {
1354
+ const first = this.parseChain();
1355
+ if (!this.at("pipe")) return first;
1356
+ const alts = [first];
1357
+ while (this.at("pipe")) {
1358
+ this.next();
1359
+ alts.push(this.parseChain());
1360
+ }
1361
+ return {
1362
+ kind: "comb",
1363
+ op: "alt",
1364
+ children: alts
1365
+ };
1366
+ }
1367
+ parseChain() {
1368
+ const node = this.parsePrimary();
1369
+ let chained = false;
1370
+ while (this.at("dot")) {
1371
+ this.next();
1372
+ this.applyMethod(node);
1373
+ chained = true;
1374
+ }
1375
+ if (this.at("eq")) {
1376
+ const eqTok = this.next();
1377
+ const value = this.parseValue();
1378
+ if (node.kind === "terminal" && !chained) node.default = value;
1379
+ else this.error("`= value` default is only allowed on a bare terminal; use `.default(...)` after a method chain", eqTok);
1380
+ }
1381
+ return node;
1382
+ }
1383
+ parsePrimary() {
1384
+ const tok = this.peek();
1385
+ if (tok.kind === "string") {
1386
+ this.next();
1387
+ return {
1388
+ kind: "literal",
1389
+ value: tok.value
1390
+ };
1391
+ }
1392
+ if (tok.kind === "lparen") return {
1393
+ kind: "comb",
1394
+ op: "seq",
1395
+ children: this.parseParenList()
1396
+ };
1397
+ if (tok.kind === "ident") {
1398
+ if (COMBINATORS.has(tok.value) && this.peek(1).kind === "lparen") {
1399
+ this.next();
1400
+ const children = this.parseParenList();
1401
+ return {
1402
+ kind: "comb",
1403
+ op: tok.value,
1404
+ children
1405
+ };
1406
+ }
1407
+ if (TERMINALS.has(tok.value)) {
1408
+ this.next();
1409
+ return {
1410
+ kind: "terminal",
1411
+ terminal: tok.value
1412
+ };
1413
+ }
1414
+ this.next();
1415
+ return {
1416
+ kind: "ref",
1417
+ refName: tok.value
1418
+ };
1419
+ }
1420
+ this.error(`Unexpected token '${tok.value || tok.kind}' in expression`, tok);
1421
+ this.next();
1422
+ return {
1423
+ kind: "literal",
1424
+ value: ""
1425
+ };
1426
+ }
1427
+ /** Parse `( elem, elem, ... )` with optional leading docs and trailing comma. */
1428
+ parseParenList() {
1429
+ this.expect("lparen", "'('");
1430
+ const items = [];
1431
+ while (!this.at("rparen") && !this.at("eof")) {
1432
+ const docs = this.collectDocs();
1433
+ if (this.at("rparen")) break;
1434
+ const elem = this.parseElement();
1435
+ if (docs) attachDocText(elem, docs);
1436
+ items.push(elem);
1437
+ if (this.at("comma")) this.next();
1438
+ else break;
1439
+ }
1440
+ this.expect("rparen", "')'");
1441
+ return items;
1442
+ }
1443
+ applyMethod(node) {
1444
+ const nameTok = this.expect("ident", "a method name");
1445
+ const method = nameTok.value;
1446
+ if (method === "output") {
1447
+ node.outputs = [...node.outputs ?? [], ...this.parseOutputArgs()];
1448
+ return;
1449
+ }
1450
+ if (!KNOWN_METHODS.has(method)) {
1451
+ this.skipBalancedArgs();
1452
+ this.warn(`Ignoring unsupported method '.${method}()'`, nameTok);
1453
+ return;
1454
+ }
1455
+ const args = this.parseScalarArgs();
1456
+ switch (method) {
1457
+ case "name":
1458
+ if (typeof args[0] === "string") node.name = args[0];
1459
+ break;
1460
+ case "title":
1461
+ if (typeof args[0] === "string") node.title = args[0];
1462
+ break;
1463
+ case "description":
1464
+ if (typeof args[0] === "string") node.description = args[0];
1465
+ break;
1466
+ case "default":
1467
+ if (args[0] !== void 0) node.default = args[0];
1468
+ break;
1469
+ case "min":
1470
+ if (typeof args[0] === "number") node.min = args[0];
1471
+ break;
1472
+ case "max":
1473
+ if (typeof args[0] === "number") node.max = args[0];
1474
+ break;
1475
+ case "join":
1476
+ node.join = typeof args[0] === "string" ? args[0] : "";
1477
+ break;
1478
+ case "count":
1479
+ if (typeof args[0] === "number") {
1480
+ node.countMin = args[0];
1481
+ node.countMax = args[0];
1482
+ }
1483
+ break;
1484
+ case "countMin":
1485
+ if (typeof args[0] === "number") node.countMin = args[0];
1486
+ break;
1487
+ case "countMax":
1488
+ if (typeof args[0] === "number") node.countMax = args[0];
1489
+ break;
1490
+ case "mediaType":
1491
+ if (typeof args[0] === "string") (node.mediaTypes ??= []).push(args[0]);
1492
+ break;
1493
+ case "mutable":
1494
+ node.mutable = true;
1495
+ break;
1496
+ case "resolveParent":
1497
+ node.resolveParent = true;
1498
+ break;
1499
+ }
1500
+ }
1501
+ /** Parse a parenthesized list of plain scalar arguments (numbers / strings). */
1502
+ parseScalarArgs() {
1503
+ this.expect("lparen", "'('");
1504
+ const args = [];
1505
+ while (!this.at("rparen") && !this.at("eof")) {
1506
+ args.push(this.parseValue());
1507
+ if (this.at("comma")) this.next();
1508
+ else break;
1509
+ }
1510
+ this.expect("rparen", "')'");
1511
+ return args;
1512
+ }
1513
+ /** Consume a balanced `( ... )` group without interpreting its contents.
1514
+ * Used to skip the arguments of an unsupported extension method. */
1515
+ skipBalancedArgs() {
1516
+ if (!this.at("lparen")) return;
1517
+ let depth = 0;
1518
+ do {
1519
+ const tok = this.next();
1520
+ if (tok.kind === "lparen") depth++;
1521
+ else if (tok.kind === "rparen") depth--;
1522
+ else if (tok.kind === "eof") break;
1523
+ } while (depth > 0);
1524
+ }
1525
+ /** Parse the argument list of `.output(...)`: one or more template expressions. */
1526
+ parseOutputArgs() {
1527
+ this.expect("lparen", "'('");
1528
+ const outputs = [];
1529
+ while (!this.at("rparen") && !this.at("eof")) {
1530
+ const docs = this.collectDocs();
1531
+ if (this.at("rparen")) break;
1532
+ const out = this.parseOutputTemplate();
1533
+ if (docs) attachDocText(out, docs);
1534
+ outputs.push(out);
1535
+ if (this.at("comma")) this.next();
1536
+ else break;
1537
+ }
1538
+ this.expect("rparen", "')'");
1539
+ return outputs;
1540
+ }
1541
+ /** `label: \`tpl\`` or `\`tpl\`` followed by optional `.name(...)` / `.or(...)`. */
1542
+ parseOutputTemplate() {
1543
+ let name;
1544
+ if ((this.at("ident") || this.at("string")) && this.peek(1).kind === "colon") {
1545
+ name = this.next().value;
1546
+ this.next();
1547
+ }
1548
+ const out = { tokens: [] };
1549
+ if (this.at("template")) {
1550
+ const tmplTok = this.next();
1551
+ const { tokens, errors } = parseTemplate(tmplTok.value);
1552
+ out.tokens = tokens;
1553
+ for (const e of errors) this.error(e, tmplTok);
1554
+ } else this.error(`Expected an output template literal`);
1555
+ const CHAIN = new Set([
1556
+ "name",
1557
+ "or",
1558
+ "title",
1559
+ "description"
1560
+ ]);
1561
+ while (this.at("dot")) {
1562
+ this.next();
1563
+ const m = this.expect("ident", "a template method");
1564
+ if (CHAIN.has(m.value)) {
1565
+ const arg = this.parseScalarArgs()[0];
1566
+ if (typeof arg === "string") if (m.value === "name") name = arg;
1567
+ else if (m.value === "or") out.fallback = arg;
1568
+ else if (m.value === "title") out.title = arg;
1569
+ else out.description = arg;
1570
+ } else {
1571
+ this.skipBalancedArgs();
1572
+ this.warn(`Ignoring unsupported output-template method '.${m.value}()'`, m);
1573
+ }
1574
+ }
1575
+ if (name !== void 0) out.name = name;
1576
+ return out;
1577
+ }
1578
+ parseValue() {
1579
+ const tok = this.peek();
1580
+ if (tok.kind === "string") {
1581
+ this.next();
1582
+ return tok.value;
1583
+ }
1584
+ if (tok.kind === "number") {
1585
+ this.next();
1586
+ return Number(tok.value);
1587
+ }
1588
+ this.error(`Expected a value (string or number) but found '${tok.value || tok.kind}'`, tok);
1589
+ this.next();
1590
+ return "";
1591
+ }
1592
+ };
1593
+ function parseArgtype(source) {
1594
+ const { frontmatter, body, errors: fmErrors } = splitFrontmatter(source);
1595
+ const { tokens, errors: lexErrors } = lex(body);
1596
+ const parser = new Parser(tokens);
1597
+ const doc = parser.parseDocument(frontmatter);
1598
+ const errors = [
1599
+ ...fmErrors.map((m) => ({ message: m })),
1600
+ ...lexErrors.map((e) => ({
1601
+ message: e.message,
1602
+ line: e.line,
1603
+ column: e.column
1604
+ })),
1605
+ ...parser.errors
1606
+ ];
1607
+ return {
1608
+ ...doc && { doc },
1609
+ errors,
1610
+ warnings: parser.warnings
1611
+ };
1612
+ }
1613
+
1614
+ //#endregion
1615
+ //#region src/ir/builders.ts
1616
+ function lit(str) {
1617
+ return {
1618
+ kind: "literal",
1619
+ attrs: { str }
1620
+ };
1621
+ }
1622
+ function str(meta) {
1623
+ return {
1624
+ kind: "str",
1625
+ attrs: {},
1626
+ meta: normalizeMeta(meta)
1627
+ };
1628
+ }
1629
+ function int(meta) {
1630
+ return {
1631
+ kind: "int",
1632
+ attrs: {},
1633
+ meta: normalizeMeta(meta)
1634
+ };
1635
+ }
1636
+ function float(meta) {
1637
+ return {
1638
+ kind: "float",
1639
+ attrs: {},
1640
+ meta: normalizeMeta(meta)
1641
+ };
1642
+ }
1643
+ function path(meta) {
1644
+ return {
1645
+ kind: "path",
1646
+ attrs: {},
1647
+ meta: normalizeMeta(meta)
1648
+ };
1649
+ }
1650
+ function seq(...nodes) {
1651
+ return {
1652
+ kind: "sequence",
1653
+ attrs: { nodes }
1654
+ };
1655
+ }
1656
+ function seqJoin(join, ...nodes) {
1657
+ return {
1658
+ kind: "sequence",
1659
+ attrs: {
1660
+ nodes,
1661
+ join
1662
+ }
1663
+ };
1664
+ }
1665
+ function opt(node, meta) {
1666
+ return {
1667
+ kind: "optional",
1668
+ attrs: { node },
1669
+ meta: normalizeMeta(meta)
1670
+ };
1671
+ }
1672
+ function rep(node, meta) {
1673
+ return {
1674
+ kind: "repeat",
1675
+ attrs: { node },
1676
+ meta: normalizeMeta(meta)
1677
+ };
1678
+ }
1679
+ function repJoin(join, node, meta) {
1680
+ return {
1681
+ kind: "repeat",
1682
+ attrs: {
1683
+ node,
1684
+ join
1685
+ },
1686
+ meta: normalizeMeta(meta)
1687
+ };
1688
+ }
1689
+ function alt(...alts) {
1690
+ return {
1691
+ kind: "alternative",
1692
+ attrs: { alts }
1693
+ };
1694
+ }
1695
+ function normalizeMeta(meta) {
1696
+ if (meta === void 0) return void 0;
1697
+ if (typeof meta === "string") return { name: meta };
1698
+ return meta;
1699
+ }
1700
+
735
1701
  //#endregion
736
1702
  //#region src/ir/meta.ts
737
1703
  /** Construct a NodeRef from a node name. */
@@ -751,6 +1717,473 @@ function effectiveOutputName(output, index) {
751
1717
  return output.name ?? `output_${index}`;
752
1718
  }
753
1719
 
1720
+ //#endregion
1721
+ //#region src/frontend/argtype/lower.ts
1722
+ /**
1723
+ * Lower an `AstDocument` to Styx IR (`Expr` + `AppMeta`).
1724
+ *
1725
+ * Mapping highlights:
1726
+ * - Combinators map 1:1 except `set` -> `sequence` (order-not-meaningful is not
1727
+ * modeled in the IR) and `any` -> its first branch (the spec's "emit branch 0"
1728
+ * rule; the interchangeable alternatives are dropped with a warning).
1729
+ * - Aliases are resolved by substitution with cycle detection.
1730
+ * - `.output(...)` declarations attach to the nearest enclosing sequence scope,
1731
+ * so an output nested in a repeat / subcommand keeps its list / struct shape
1732
+ * (per-output gating is recovered downstream from each ref binding's gate).
1733
+ */
1734
+ /** Build IR `Documentation` from an AST node's already-split title/description. */
1735
+ function docFrom(node) {
1736
+ const doc = {
1737
+ ...node.title && { title: node.title },
1738
+ ...node.description && { description: node.description }
1739
+ };
1740
+ return Object.keys(doc).length > 0 ? doc : void 0;
1741
+ }
1742
+ var Lowerer = class {
1743
+ errors = [];
1744
+ warnings = [];
1745
+ /** Extensions whose annotations the document actually uses, collected during
1746
+ * lowering so `lowerDocument` can flag any used-but-undeclared in frontmatter. */
1747
+ usedExtensions = /* @__PURE__ */ new Set();
1748
+ aliases = /* @__PURE__ */ new Map();
1749
+ constructor(aliases) {
1750
+ for (const a of aliases) {
1751
+ if (this.aliases.has(a.name)) this.warnings.push(`Duplicate alias '${a.name}'; last definition wins`);
1752
+ this.aliases.set(a.name, a.expr);
1753
+ }
1754
+ }
1755
+ lower(doc) {
1756
+ const rootSink = [];
1757
+ const expr = this.lowerNode(doc.root, /* @__PURE__ */ new Set(), void 0, rootSink);
1758
+ const root = expr.kind === "sequence" ? expr : seq(expr);
1759
+ if (root !== expr) root.meta = { ...doc.root.name && { name: doc.root.name } };
1760
+ if (rootSink.length > 0) root.meta = {
1761
+ ...root.meta,
1762
+ outputs: [...root.meta?.outputs ?? [], ...rootSink]
1763
+ };
1764
+ return {
1765
+ expr: root,
1766
+ errors: this.errors,
1767
+ warnings: this.warnings
1768
+ };
1769
+ }
1770
+ /**
1771
+ * @param expanding - alias names currently being expanded (cycle guard).
1772
+ * @param selfName - nearest enclosing named node, for `{}` self-references.
1773
+ * @param sink - outputs array of the nearest enclosing sequence; a `.output()`
1774
+ * on this node attaches here (seq/set nodes instead own their outputs).
1775
+ */
1776
+ lowerNode(node, expanding, selfName, sink) {
1777
+ if (node.kind === "ref") return this.lowerRef(node, expanding, selfName, sink);
1778
+ const name = node.name ?? selfName;
1779
+ const joinable = node.kind === "comb" && (node.op === "seq" || node.op === "set" || node.op === "rep" || node.op === "opt");
1780
+ if (node.join !== void 0 && !joinable) this.warnings.push("`.join()` is only supported on seq/set/rep/opt; ignored here");
1781
+ const isNumericTerminal = node.kind === "terminal" && (node.terminal === "int" || node.terminal === "float");
1782
+ const isRep = node.kind === "comb" && node.op === "rep";
1783
+ const isPath = node.kind === "terminal" && node.terminal === "path";
1784
+ if ((node.min !== void 0 || node.max !== void 0) && !isNumericTerminal) this.warnings.push("`.min()`/`.max()` is only supported on int/float terminals; ignored here");
1785
+ if ((node.countMin !== void 0 || node.countMax !== void 0) && !isRep) this.warnings.push("`.count()`/`.countMin()`/`.countMax()` is only supported on rep; ignored here");
1786
+ if ((node.mutable || node.resolveParent) && !isPath) this.warnings.push("`.mutable()`/`.resolveParent()` is only supported on path; ignored here");
1787
+ if (node.mediaTypes?.length && !isPath) this.warnings.push("`.mediaType()` is only supported on path; ignored here");
1788
+ if (node.outputs?.length) this.usedExtensions.add("outputs");
1789
+ if (node.mediaTypes?.length && isPath) this.usedExtensions.add("mediatypes");
1790
+ if ((node.mutable || node.resolveParent) && isPath) this.usedExtensions.add("paths");
1791
+ const isSeqSet = node.kind === "comb" && (node.op === "seq" || node.op === "set");
1792
+ if (node.outputs?.length && !isSeqSet) for (const o of node.outputs) sink.push(this.toOutput(o, name));
1793
+ switch (node.kind) {
1794
+ case "literal": {
1795
+ const e = lit(node.value ?? "");
1796
+ this.applyMeta(e, node);
1797
+ return e;
1798
+ }
1799
+ case "terminal": return this.lowerTerminal(node);
1800
+ case "comb": return this.lowerComb(node, expanding, name, sink);
1801
+ default:
1802
+ this.errors.push(`Unknown AST node kind '${node.kind}'`);
1803
+ return seq();
1804
+ }
1805
+ }
1806
+ lowerRef(node, expanding, selfName, sink) {
1807
+ const target = node.refName;
1808
+ const aliasExpr = this.aliases.get(target);
1809
+ if (!aliasExpr) {
1810
+ this.errors.push(`Unknown alias '${target}'`);
1811
+ return seq();
1812
+ }
1813
+ if (expanding.has(target)) {
1814
+ this.errors.push(`Recursive alias '${target}' is not allowed`);
1815
+ return seq();
1816
+ }
1817
+ const clone = structuredClone(aliasExpr);
1818
+ clone.name = node.name ?? clone.name;
1819
+ clone.title = node.title ?? clone.title;
1820
+ clone.description = node.description ?? clone.description;
1821
+ if (node.default !== void 0) clone.default = node.default;
1822
+ if (node.min !== void 0) clone.min = node.min;
1823
+ if (node.max !== void 0) clone.max = node.max;
1824
+ if (node.join !== void 0) clone.join = node.join;
1825
+ if (node.countMin !== void 0) clone.countMin = node.countMin;
1826
+ if (node.countMax !== void 0) clone.countMax = node.countMax;
1827
+ if (node.mediaTypes?.length) clone.mediaTypes = [...clone.mediaTypes ?? [], ...node.mediaTypes];
1828
+ if (node.mutable) clone.mutable = true;
1829
+ if (node.resolveParent) clone.resolveParent = true;
1830
+ if (node.outputs?.length) clone.outputs = [...clone.outputs ?? [], ...node.outputs];
1831
+ const next = new Set(expanding);
1832
+ next.add(target);
1833
+ return this.lowerNode(clone, next, selfName, sink);
1834
+ }
1835
+ lowerTerminal(node) {
1836
+ switch (node.terminal) {
1837
+ case "int": {
1838
+ const e = int();
1839
+ this.checkBounds(node.min, node.max, "value", "min", "max");
1840
+ if (node.min !== void 0) e.attrs.minValue = node.min;
1841
+ if (node.max !== void 0) e.attrs.maxValue = node.max;
1842
+ this.applyMeta(e, node);
1843
+ return e;
1844
+ }
1845
+ case "float": {
1846
+ const e = float();
1847
+ this.checkBounds(node.min, node.max, "value", "min", "max");
1848
+ if (node.min !== void 0) e.attrs.minValue = node.min;
1849
+ if (node.max !== void 0) e.attrs.maxValue = node.max;
1850
+ this.applyMeta(e, node);
1851
+ return e;
1852
+ }
1853
+ case "str": {
1854
+ const e = str();
1855
+ this.applyMeta(e, node);
1856
+ return e;
1857
+ }
1858
+ case "path": {
1859
+ const e = path();
1860
+ if (node.mediaTypes?.length) e.attrs.mediaTypes = node.mediaTypes;
1861
+ if (node.mutable) e.attrs.mutable = true;
1862
+ if (node.resolveParent) e.attrs.resolveParent = true;
1863
+ this.applyMeta(e, node);
1864
+ return e;
1865
+ }
1866
+ default:
1867
+ this.errors.push(`Unknown terminal '${String(node.terminal)}'`);
1868
+ return str();
1869
+ }
1870
+ }
1871
+ lowerComb(node, expanding, name, sink) {
1872
+ const children = node.children ?? [];
1873
+ const lowerChildren = (s) => children.map((c) => this.lowerNode(c, expanding, name, s));
1874
+ switch (node.op) {
1875
+ case "seq":
1876
+ case "set": {
1877
+ const selfOutputs = [];
1878
+ for (const o of node.outputs ?? []) selfOutputs.push(this.toOutput(o, name));
1879
+ const lowered = lowerChildren(selfOutputs);
1880
+ this.dedupeSiblingNames(lowered);
1881
+ const e = seq(...lowered);
1882
+ if (node.join !== void 0) e.attrs.join = node.join;
1883
+ this.applyMeta(e, node);
1884
+ if (selfOutputs.length > 0) e.meta = {
1885
+ ...e.meta,
1886
+ outputs: [...e.meta?.outputs ?? [], ...selfOutputs]
1887
+ };
1888
+ return e;
1889
+ }
1890
+ case "alt": {
1891
+ const arms = lowerChildren(sink);
1892
+ this.dedupeSiblingNames(arms);
1893
+ const e = alt(...arms);
1894
+ this.applyMeta(e, node);
1895
+ return e;
1896
+ }
1897
+ case "any": {
1898
+ if (children.length === 0) {
1899
+ this.errors.push("`any(...)` requires at least one branch");
1900
+ return seq();
1901
+ }
1902
+ const e = this.lowerNode(children[0], expanding, name, sink);
1903
+ if (node.name) e.meta = {
1904
+ ...e.meta,
1905
+ name: node.name
1906
+ };
1907
+ {
1908
+ const d = docFrom(node);
1909
+ if (d) e.meta = {
1910
+ ...e.meta,
1911
+ doc: d
1912
+ };
1913
+ }
1914
+ return e;
1915
+ }
1916
+ case "opt": {
1917
+ const inner = this.wrapChildren(children, expanding, name, sink);
1918
+ if (node.join !== void 0 && (inner.kind === "sequence" || inner.kind === "repeat")) inner.attrs.join = node.join;
1919
+ const e = opt(inner);
1920
+ this.applyMeta(e, node);
1921
+ if (inner.kind === "literal") e.meta = {
1922
+ ...e.meta,
1923
+ defaultValue: false
1924
+ };
1925
+ return e;
1926
+ }
1927
+ case "rep": {
1928
+ const e = rep(this.wrapChildren(children, expanding, name, sink));
1929
+ if (node.join !== void 0) e.attrs.join = node.join;
1930
+ this.checkBounds(node.countMin, node.countMax, "repetition count", "countMin", "countMax");
1931
+ if (node.countMin !== void 0) e.attrs.countMin = node.countMin;
1932
+ if (node.countMax !== void 0) e.attrs.countMax = node.countMax;
1933
+ this.applyMeta(e, node);
1934
+ return e;
1935
+ }
1936
+ default:
1937
+ this.errors.push(`Unknown combinator '${String(node.op)}'`);
1938
+ return seq();
1939
+ }
1940
+ }
1941
+ /** Warn on an inverted `[min, max]` pair (a lower bound above its upper
1942
+ * bound), which yields an unsatisfiable constraint downstream. */
1943
+ checkBounds(min, max, what, minLabel, maxLabel) {
1944
+ if (min !== void 0 && max !== void 0 && min > max) this.warnings.push(`Inverted ${what} bounds: ${minLabel} (${min}) > ${maxLabel} (${max})`);
1945
+ }
1946
+ /**
1947
+ * Disambiguate duplicate explicit names among the direct children of a
1948
+ * sequence/set (struct fields) or the arms of an alternative (union
1949
+ * discriminants). The solver turns each named child into a struct field / union
1950
+ * variant keyed by its name; two siblings with the same name would silently
1951
+ * collapse (the second overwrites the first, dropping a parameter). argtype is
1952
+ * hand-authored
1953
+ * and gives no uniqueness guarantee, so - like the mrtrix frontend - we rename
1954
+ * collisions (`name_2`, `name_3`, ...) and warn. (This catches duplicate
1955
+ * explicit labels; it does not detect collisions between solver-derived names
1956
+ * of otherwise-unnamed children, which the solver also does not guard.)
1957
+ */
1958
+ dedupeSiblingNames(children) {
1959
+ const used = /* @__PURE__ */ new Set();
1960
+ for (const child of children) {
1961
+ const nm = child.meta?.name;
1962
+ if (nm === void 0) continue;
1963
+ if (!used.has(nm)) {
1964
+ used.add(nm);
1965
+ continue;
1966
+ }
1967
+ let n = 2;
1968
+ while (used.has(`${nm}_${n}`)) n++;
1969
+ const renamed = `${nm}_${n}`;
1970
+ used.add(renamed);
1971
+ child.meta = {
1972
+ ...child.meta,
1973
+ name: renamed
1974
+ };
1975
+ this.warnings.push(`Duplicate sibling name '${nm}'; renamed one occurrence to '${renamed}'`);
1976
+ }
1977
+ }
1978
+ /** `opt`/`rep` with multiple children implicitly wrap them in a sequence,
1979
+ * which - like any sequence - is the output scope for those children. */
1980
+ wrapChildren(children, expanding, name, sink) {
1981
+ if (children.length === 1) return this.lowerNode(children[0], expanding, name, sink);
1982
+ const selfOutputs = [];
1983
+ const e = seq(...children.map((c) => this.lowerNode(c, expanding, name, selfOutputs)));
1984
+ if (selfOutputs.length > 0) e.meta = {
1985
+ ...e.meta,
1986
+ outputs: selfOutputs
1987
+ };
1988
+ return e;
1989
+ }
1990
+ /** Fold an AST node's name/doc/default decorations onto an IR node's meta. */
1991
+ applyMeta(e, node) {
1992
+ const meta = {};
1993
+ if (node.name) meta.name = node.name;
1994
+ const doc = docFrom(node);
1995
+ if (doc) meta.doc = doc;
1996
+ if (node.default !== void 0) meta.defaultValue = node.default;
1997
+ if (Object.keys(meta).length > 0) e.meta = {
1998
+ ...e.meta,
1999
+ ...meta
2000
+ };
2001
+ }
2002
+ toOutput(o, selfName) {
2003
+ const tokens = [];
2004
+ for (const t of o.tokens) {
2005
+ if (t.kind === "literal") {
2006
+ tokens.push({
2007
+ kind: "literal",
2008
+ value: t.value
2009
+ });
2010
+ continue;
2011
+ }
2012
+ const targetName = t.name ?? selfName;
2013
+ if (!targetName) {
2014
+ this.warnings.push("Output self-reference '{}' has no named node to resolve to; emitting empty");
2015
+ tokens.push({
2016
+ kind: "literal",
2017
+ value: ""
2018
+ });
2019
+ continue;
2020
+ }
2021
+ if (t.stripPrefix?.length) this.warnings.push("Output ref op 'strip_prefix' is not supported by the IR; ignored");
2022
+ if (t.basename) this.warnings.push("Output ref op 'basename' is not supported by the IR; ignored");
2023
+ tokens.push({
2024
+ kind: "ref",
2025
+ target: nodeRef(targetName),
2026
+ ...t.stripSuffix?.length && { stripExtensions: t.stripSuffix },
2027
+ ...t.or !== void 0 && { fallback: t.or }
2028
+ });
2029
+ }
2030
+ if (o.fallback !== void 0) this.warnings.push("Output-level '.or(...)' fallback is not supported by the IR; ignored");
2031
+ const doc = docFrom(o);
2032
+ return {
2033
+ ...o.name && { name: o.name },
2034
+ ...doc && { doc },
2035
+ tokens
2036
+ };
2037
+ }
2038
+ };
2039
+ function buildAppMeta(fm, rootDoc, rootName) {
2040
+ const warnings = [];
2041
+ const rootTitleDesc = docFrom(rootDoc);
2042
+ if (!fm) {
2043
+ const meta = {
2044
+ ...rootName && { id: rootName },
2045
+ ...rootTitleDesc && { doc: rootTitleDesc }
2046
+ };
2047
+ return Object.keys(meta).length > 0 ? {
2048
+ meta,
2049
+ warnings
2050
+ } : { warnings };
2051
+ }
2052
+ const asStr = (x) => typeof x === "string" && x.length > 0 ? x : void 0;
2053
+ const id = asStr(rootName) ?? asStr(fm.exe) ?? asStr(fm.id);
2054
+ const version = asStr(fm.version) ?? (typeof fm.version === "number" ? String(fm.version) : void 0);
2055
+ const strList = (x) => Array.isArray(x) ? x.filter((a) => typeof a === "string") : [];
2056
+ const authors = strList(fm.authors);
2057
+ const urls = strList(fm.urls);
2058
+ const references = strList(fm.references);
2059
+ const doc = {
2060
+ ...rootTitleDesc,
2061
+ ...authors.length > 0 && { authors },
2062
+ ...urls.length > 0 && { urls },
2063
+ ...references.length > 0 && { literature: references }
2064
+ };
2065
+ const meta = {
2066
+ ...id && { id },
2067
+ ...version && { version },
2068
+ ...Object.keys(doc).length > 0 && { doc }
2069
+ };
2070
+ if (isRecord$1(fm.container)) {
2071
+ const image = asStr(fm.container.image);
2072
+ const type = asStr(fm.container.type);
2073
+ if (image) meta.container = {
2074
+ image,
2075
+ ...type === "docker" || type === "singularity" ? { type } : {}
2076
+ };
2077
+ }
2078
+ meta.stdout = streamFrom(fm.stdout, warnings, "stdout");
2079
+ meta.stderr = streamFrom(fm.stderr, warnings, "stderr");
2080
+ if (!meta.stdout) delete meta.stdout;
2081
+ if (!meta.stderr) delete meta.stderr;
2082
+ return {
2083
+ meta,
2084
+ warnings
2085
+ };
2086
+ }
2087
+ function streamFrom(x, warnings, key) {
2088
+ if (x === void 0 || x === null) return void 0;
2089
+ if (isRecord$1(x)) {
2090
+ const name = typeof x.name === "string" ? x.name : void 0;
2091
+ if (!name) {
2092
+ warnings.push(`Frontmatter '${key}' is missing a 'name'; ignored`);
2093
+ return;
2094
+ }
2095
+ const description = typeof x.description === "string" ? x.description : void 0;
2096
+ return {
2097
+ name,
2098
+ ...description && { doc: { description } }
2099
+ };
2100
+ }
2101
+ if (typeof x === "string") return { name: x };
2102
+ warnings.push(`Frontmatter '${key}' has an unexpected shape; ignored`);
2103
+ }
2104
+ function isRecord$1(x) {
2105
+ return typeof x === "object" && x !== null && !Array.isArray(x);
2106
+ }
2107
+ function lowerDocument(doc) {
2108
+ const lowerer = new Lowerer(doc.aliases);
2109
+ const result = lowerer.lower(doc);
2110
+ const exe = typeof doc.frontmatter?.exe === "string" ? doc.frontmatter.exe : void 0;
2111
+ if (exe) result.expr.attrs.nodes.unshift(lit(exe));
2112
+ const { meta, warnings } = buildAppMeta(doc.frontmatter, {
2113
+ title: doc.root.title,
2114
+ description: doc.root.description
2115
+ }, doc.rootName);
2116
+ const extWarnings = [];
2117
+ if (doc.frontmatter) {
2118
+ const declared = new Set(Array.isArray(doc.frontmatter.extensions) ? doc.frontmatter.extensions.filter((e) => typeof e === "string") : []);
2119
+ for (const used of lowerer.usedExtensions) if (!declared.has(used)) extWarnings.push(`Uses the '${used}' extension but does not declare it in frontmatter (add it to 'extensions:')`);
2120
+ }
2121
+ return {
2122
+ ...meta && { meta },
2123
+ expr: result.expr,
2124
+ errors: result.errors,
2125
+ warnings: [
2126
+ ...result.warnings,
2127
+ ...warnings,
2128
+ ...extWarnings
2129
+ ]
2130
+ };
2131
+ }
2132
+
2133
+ //#endregion
2134
+ //#region src/frontend/argtype/parser-frontend.ts
2135
+ /** A fresh empty root sequence for error returns (IR passes mutate in place). */
2136
+ function emptyExpr$2() {
2137
+ return {
2138
+ kind: "sequence",
2139
+ attrs: { nodes: [] }
2140
+ };
2141
+ }
2142
+ /**
2143
+ * Frontend for the argtype sugar DSL - the hand-authored, TypeScript-types-like
2144
+ * language for describing CLI argument grammars (see the argtype spec). Parses
2145
+ * the text into an AST, then lowers it to Styx IR + AppMeta.
2146
+ *
2147
+ * Supported today: the argtype core (combinators, terminals, literals, naming,
2148
+ * aliases, value constraints, `.join`/`.count`/`.default`, doc comments,
2149
+ * frontmatter) plus the `outputs`, `mediatypes`, and `paths` extensions. `set`
2150
+ * lowers to a sequence and `any` to its first branch; the `constraints`
2151
+ * extension is parsed-and-ignored.
2152
+ */
2153
+ var ArgtypeParser = class {
2154
+ name = "argtype";
2155
+ extensions = ["argtype"];
2156
+ parse(source, _filename) {
2157
+ const { doc, errors: parseErrors, warnings: parseWarnings } = parseArgtype(source);
2158
+ const toLocation = (e) => e.line !== void 0 ? { location: {
2159
+ line: e.line,
2160
+ column: e.column
2161
+ } } : {};
2162
+ const errors = parseErrors.map((e) => ({
2163
+ message: e.message,
2164
+ ...toLocation(e)
2165
+ }));
2166
+ const warnings = parseWarnings.map((e) => ({
2167
+ message: e.message,
2168
+ ...toLocation(e)
2169
+ }));
2170
+ if (!doc) return {
2171
+ expr: emptyExpr$2(),
2172
+ errors,
2173
+ warnings
2174
+ };
2175
+ const lowered = lowerDocument(doc);
2176
+ warnings.push(...lowered.warnings.map((message) => ({ message })));
2177
+ errors.push(...lowered.errors.map((message) => ({ message })));
2178
+ return {
2179
+ ...lowered.meta && { meta: lowered.meta },
2180
+ expr: lowered.expr,
2181
+ errors,
2182
+ warnings
2183
+ };
2184
+ }
2185
+ };
2186
+
754
2187
  //#endregion
755
2188
  //#region src/frontend/boutiques/destruct-template.ts
756
2189
  /**
@@ -1242,12 +2675,17 @@ var BoutiquesParser = class {
1242
2675
  wrapWithFlag(node, btInput) {
1243
2676
  const flag = btInput["command-line-flag"];
1244
2677
  if (!isString$2(flag)) return node;
2678
+ const flagSep = btInput["command-line-flag-separator"];
2679
+ const fused = isString$2(flagSep) && flagSep !== " ";
1245
2680
  return {
1246
2681
  kind: "sequence",
1247
- attrs: { nodes: [{
1248
- kind: "literal",
1249
- attrs: { str: flag + (btInput["command-line-flag-separator"] ?? "") }
1250
- }, node] }
2682
+ attrs: {
2683
+ nodes: [{
2684
+ kind: "literal",
2685
+ attrs: { str: fused ? flag + flagSep : flag }
2686
+ }, node],
2687
+ ...fused && { join: "" }
2688
+ }
1251
2689
  };
1252
2690
  }
1253
2691
  wrapWithOptional(node) {
@@ -1362,100 +2800,13 @@ var BoutiquesParser = class {
1362
2800
  name: baseMeta.id
1363
2801
  };
1364
2802
  return {
1365
- meta: baseMeta,
1366
- expr,
1367
- errors: this.errors,
1368
- warnings: this.warnings
1369
- };
1370
- }
1371
- };
1372
-
1373
- //#endregion
1374
- //#region src/ir/builders.ts
1375
- function lit(str) {
1376
- return {
1377
- kind: "literal",
1378
- attrs: { str }
1379
- };
1380
- }
1381
- function str(meta) {
1382
- return {
1383
- kind: "str",
1384
- attrs: {},
1385
- meta: normalizeMeta(meta)
1386
- };
1387
- }
1388
- function int(meta) {
1389
- return {
1390
- kind: "int",
1391
- attrs: {},
1392
- meta: normalizeMeta(meta)
1393
- };
1394
- }
1395
- function float(meta) {
1396
- return {
1397
- kind: "float",
1398
- attrs: {},
1399
- meta: normalizeMeta(meta)
1400
- };
1401
- }
1402
- function path(meta) {
1403
- return {
1404
- kind: "path",
1405
- attrs: {},
1406
- meta: normalizeMeta(meta)
1407
- };
1408
- }
1409
- function seq(...nodes) {
1410
- return {
1411
- kind: "sequence",
1412
- attrs: { nodes }
1413
- };
1414
- }
1415
- function seqJoin(join, ...nodes) {
1416
- return {
1417
- kind: "sequence",
1418
- attrs: {
1419
- nodes,
1420
- join
1421
- }
1422
- };
1423
- }
1424
- function opt(node, meta) {
1425
- return {
1426
- kind: "optional",
1427
- attrs: { node },
1428
- meta: normalizeMeta(meta)
1429
- };
1430
- }
1431
- function rep(node, meta) {
1432
- return {
1433
- kind: "repeat",
1434
- attrs: { node },
1435
- meta: normalizeMeta(meta)
1436
- };
1437
- }
1438
- function repJoin(join, node, meta) {
1439
- return {
1440
- kind: "repeat",
1441
- attrs: {
1442
- node,
1443
- join
1444
- },
1445
- meta: normalizeMeta(meta)
1446
- };
1447
- }
1448
- function alt(...alts) {
1449
- return {
1450
- kind: "alternative",
1451
- attrs: { alts }
1452
- };
1453
- }
1454
- function normalizeMeta(meta) {
1455
- if (meta === void 0) return void 0;
1456
- if (typeof meta === "string") return { name: meta };
1457
- return meta;
1458
- }
2803
+ meta: baseMeta,
2804
+ expr,
2805
+ errors: this.errors,
2806
+ warnings: this.warnings
2807
+ };
2808
+ }
2809
+ };
1459
2810
 
1460
2811
  //#endregion
1461
2812
  //#region src/backend/string-case.ts
@@ -2206,11 +3557,33 @@ function extractVersion(versionInfo) {
2206
3557
 
2207
3558
  //#endregion
2208
3559
  //#region src/frontend/detect-format.ts
3560
+ /** The first non-whitespace character of a source ("" if it is all blank). */
3561
+ function firstNonBlankChar(source) {
3562
+ const match = source.match(/\S/);
3563
+ return match ? match[0] : "";
3564
+ }
2209
3565
  /**
2210
- * Auto-detect the format of a JSON descriptor source string.
2211
- * Returns null if the format cannot be determined.
3566
+ * Auto-detect the format of a descriptor source string.
3567
+ *
3568
+ * Every JSON frontend (boutiques, argdump, workbench, mrtrix) is a top-level
3569
+ * object, so the first non-blank character being `{` is the signal for "some
3570
+ * JSON format"; we then inspect its keys to pick which one. Anything else
3571
+ * non-blank is treated as the argtype DSL, whose sources open with a terminal
3572
+ * (`int`), a literal (`"hello"`), a combinator (`seq(...)`), a `name: expr`
3573
+ * definition, or a `---` frontmatter fence - never with `{`.
3574
+ *
3575
+ * Deciding on the leading character (rather than trying `JSON.parse` first) is
3576
+ * what lets standalone argtype snippets like `"hello"` or `42` be recognized:
3577
+ * those are *also* valid JSON scalars, so a parse-first approach would swallow
3578
+ * them and then reject them as "not an object".
3579
+ *
3580
+ * Returns null only when the source is blank, or opens with `{` but matches no
3581
+ * known JSON format (ambiguous, or still being typed).
2212
3582
  */
2213
3583
  function detectFormat(source) {
3584
+ const first = firstNonBlankChar(source);
3585
+ if (first === "") return null;
3586
+ if (first !== "{") return "argtype";
2214
3587
  let parsed;
2215
3588
  try {
2216
3589
  parsed = JSON.parse(source);
@@ -2227,6 +3600,605 @@ function detectFormat(source) {
2227
3600
  return null;
2228
3601
  }
2229
3602
 
3603
+ //#endregion
3604
+ //#region src/backend/scope.ts
3605
+ /** Symbol collision avoidance for code generation. */
3606
+ var Scope = class Scope {
3607
+ reserved;
3608
+ used;
3609
+ parent;
3610
+ constructor(reserved = [], parent) {
3611
+ this.reserved = new Set(reserved);
3612
+ this.used = /* @__PURE__ */ new Set();
3613
+ this.parent = parent;
3614
+ }
3615
+ /** Check if a symbol is already taken (in this scope or any parent). */
3616
+ has(symbol) {
3617
+ return this.reserved.has(symbol) || this.used.has(symbol) || (this.parent?.has(symbol) ?? false);
3618
+ }
3619
+ /**
3620
+ * Add a symbol, appending a numeric suffix to avoid collisions. Returns the
3621
+ * safe name.
3622
+ *
3623
+ * When a `recase` transform is given, a disambiguated candidate is routed back
3624
+ * through it so the suffix is absorbed into the identifier's casing - e.g.
3625
+ * `pascalCase` folds `Config_2` into `Config2` - rather than leaving a
3626
+ * mixed-case `Config_2`. Uniqueness is always checked on the final emitted
3627
+ * form, so two hints that case-collide still get distinct names. Defaults to
3628
+ * identity (the bare `<name>_<n>` suffix) for callers that don't case-normalize.
3629
+ */
3630
+ add(candidate, recase = (s) => s) {
3631
+ if (!this.has(candidate)) {
3632
+ this.used.add(candidate);
3633
+ return candidate;
3634
+ }
3635
+ let suffix = 2;
3636
+ let safe = recase(`${candidate}_${suffix}`);
3637
+ while (this.has(safe)) {
3638
+ suffix++;
3639
+ safe = recase(`${candidate}_${suffix}`);
3640
+ }
3641
+ this.used.add(safe);
3642
+ return safe;
3643
+ }
3644
+ /** Create a child scope that inherits this scope's restrictions. */
3645
+ child(reserved = []) {
3646
+ return new Scope(reserved, this);
3647
+ }
3648
+ };
3649
+
3650
+ //#endregion
3651
+ //#region src/backend/argtype/emit.ts
3652
+ const INDENT = " ";
3653
+ /** Target content width (excluding the `/// ` prefix and indentation) for a
3654
+ * wrapped `///` description line. */
3655
+ const DOC_WIDTH = 80;
3656
+ function pad(level) {
3657
+ return INDENT.repeat(level);
3658
+ }
3659
+ /**
3660
+ * Wrap a description into `///`-ready content lines. Paragraphs (separated by a
3661
+ * blank line) are word-wrapped to `width`; an empty string marks a paragraph
3662
+ * break (the caller emits it as a bare `///`).
3663
+ *
3664
+ * The argtype frontend reflows a `///` block as prose - a single line break
3665
+ * rejoins with a space, a blank line separates paragraphs - so wrapping here is
3666
+ * the inverse: it round-trips paragraph text exactly (whitespace within a
3667
+ * paragraph is normalized to single spaces, matching the frontend's reflow), and
3668
+ * keeps a long description from emitting as one giant physical line.
3669
+ */
3670
+ function wrapDoc(text, width) {
3671
+ const lines = [];
3672
+ for (const paragraph of text.split(/\n{2,}/)) {
3673
+ const words = paragraph.split(/\s+/).filter((w) => w.length > 0);
3674
+ if (words.length === 0) continue;
3675
+ if (lines.length > 0) lines.push("");
3676
+ let cur = "";
3677
+ for (const word of words) if (cur === "") cur = word;
3678
+ else if (cur.length + 1 + word.length <= width) cur += ` ${word}`;
3679
+ else {
3680
+ lines.push(cur);
3681
+ cur = word;
3682
+ }
3683
+ if (cur !== "") lines.push(cur);
3684
+ }
3685
+ return lines;
3686
+ }
3687
+ function num(n) {
3688
+ return String(n);
3689
+ }
3690
+ /** Escape a string for a double-quoted argtype literal (matches the lexer). */
3691
+ function escapeString(s) {
3692
+ return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\t/g, "\\t").replace(/\r/g, "\\r");
3693
+ }
3694
+ function quote(s) {
3695
+ return `"${escapeString(s)}"`;
3696
+ }
3697
+ /** A value terminal kind whose `meta.defaultValue` argtype can express. */
3698
+ function isValueTerminal(node) {
3699
+ return node.kind === "int" || node.kind === "float" || node.kind === "str" || node.kind === "path";
3700
+ }
3701
+ /**
3702
+ * Find the single value terminal a wrapper's default should attach to. Descends
3703
+ * through optional/repeat and a sequence with exactly one non-literal child
3704
+ * (the flag-value pattern `seq(lit, value)`). Returns undefined when the value
3705
+ * slot is ambiguous (multiple non-literal children) or absent (a bare literal
3706
+ * flag), in which case the wrapper default has no terminal to sink onto.
3707
+ */
3708
+ function findValueTerminal(node) {
3709
+ switch (node.kind) {
3710
+ case "int":
3711
+ case "float":
3712
+ case "str":
3713
+ case "path": return node;
3714
+ case "optional": return findValueTerminal(node.attrs.node);
3715
+ case "repeat": return findValueTerminal(node.attrs.node);
3716
+ case "sequence": {
3717
+ const nonLiteral = node.attrs.nodes.filter((n) => n.kind !== "literal");
3718
+ return nonLiteral.length === 1 ? findValueTerminal(nonLiteral[0]) : void 0;
3719
+ }
3720
+ default: return;
3721
+ }
3722
+ }
3723
+ /**
3724
+ * Push a wrapper's `meta.defaultValue` down onto its inner value terminal so it
3725
+ * can be emitted as `int = 5` / `.default(...)`. Boutiques (and argparse) hoist
3726
+ * an input's default onto the outermost wrapper (`optional` / `sequence`); the
3727
+ * argtype surface only carries `.default()` on a terminal, so we relocate it.
3728
+ * Boolean defaults are left in place: those are the `opt(literal)` flag-false
3729
+ * convention, which the frontend regenerates for free and which has no terminal.
3730
+ */
3731
+ function sinkDefaults(expr) {
3732
+ const clone = structuredClone(expr);
3733
+ const visit = (node) => {
3734
+ switch (node.kind) {
3735
+ case "optional":
3736
+ visit(node.attrs.node);
3737
+ sinkInto(node);
3738
+ break;
3739
+ case "repeat":
3740
+ visit(node.attrs.node);
3741
+ sinkInto(node);
3742
+ break;
3743
+ case "sequence":
3744
+ for (const c of node.attrs.nodes) visit(c);
3745
+ sinkInto(node);
3746
+ break;
3747
+ case "alternative":
3748
+ for (const c of node.attrs.alts) visit(c);
3749
+ break;
3750
+ default: break;
3751
+ }
3752
+ };
3753
+ visit(clone);
3754
+ return clone;
3755
+ }
3756
+ function sinkInto(wrapper) {
3757
+ const dv = wrapper.meta?.defaultValue;
3758
+ if (dv === void 0 || typeof dv === "boolean") return;
3759
+ const terminal = findValueTerminal(wrapper);
3760
+ if (!terminal || !isValueTerminal(terminal) || terminal.meta?.defaultValue !== void 0) return;
3761
+ terminal.meta = {
3762
+ ...terminal.meta,
3763
+ defaultValue: dv
3764
+ };
3765
+ const meta = { ...wrapper.meta };
3766
+ delete meta.defaultValue;
3767
+ wrapper.meta = Object.keys(meta).length > 0 ? meta : void 0;
3768
+ }
3769
+ /**
3770
+ * Detect the synthetic single-child sequence the frontend wraps a non-sequence
3771
+ * root in. Such a wrapper carries only `name` (matching the child's name) plus
3772
+ * any collected `outputs`; the doc/default live on the inner node. Returns the
3773
+ * inner node and the wrapper's outputs so the caller can emit the inner node as
3774
+ * the definition body (the frontend re-wraps it identically on re-parse).
3775
+ */
3776
+ function syntheticWrap(root) {
3777
+ if (root.kind !== "sequence" || root.attrs.nodes.length !== 1 || root.attrs.join !== void 0) return;
3778
+ const meta = root.meta;
3779
+ if (meta?.doc || meta?.defaultValue !== void 0 || meta?.variantTag !== void 0) return;
3780
+ const child = root.attrs.nodes[0];
3781
+ if (child.kind === "sequence") return void 0;
3782
+ if ((child.meta?.name ?? void 0) !== (meta?.name ?? void 0)) return void 0;
3783
+ return {
3784
+ child,
3785
+ ...meta?.outputs && { outputs: meta.outputs }
3786
+ };
3787
+ }
3788
+ function hasOutputs(expr) {
3789
+ return walkSome(expr, (n) => (n.meta?.outputs?.length ?? 0) > 0);
3790
+ }
3791
+ function hasMediaTypes(expr) {
3792
+ return walkSome(expr, (n) => n.kind === "path" && (n.attrs.mediaTypes?.length ?? 0) > 0);
3793
+ }
3794
+ function hasPaths(expr) {
3795
+ return walkSome(expr, (n) => n.kind === "path" && (!!n.attrs.mutable || !!n.attrs.resolveParent));
3796
+ }
3797
+ function walkSome(node, pred) {
3798
+ if (pred(node)) return true;
3799
+ switch (node.kind) {
3800
+ case "sequence": return node.attrs.nodes.some((n) => walkSome(n, pred));
3801
+ case "alternative": return node.attrs.alts.some((n) => walkSome(n, pred));
3802
+ case "optional": return walkSome(node.attrs.node, pred);
3803
+ case "repeat": return walkSome(node.attrs.node, pred);
3804
+ default: return false;
3805
+ }
3806
+ }
3807
+ const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
3808
+ /** A name rendered where an identifier is expected: bare when it is a valid
3809
+ * identifier, otherwise a double-quoted form preserving it exactly. Used for
3810
+ * both `label:` prefixes and output-template `{ref}` targets (templates accept a
3811
+ * quoted name, `{"4d_output"}`), so the two always agree and no lossy
3812
+ * sanitization is needed. */
3813
+ function identOrQuoted(name) {
3814
+ return IDENT_RE.test(name) ? name : quote(name);
3815
+ }
3816
+ var ArgtypeEmitter = class {
3817
+ warnings = [];
3818
+ warn(message) {
3819
+ this.warnings.push({ message });
3820
+ }
3821
+ /** Render a name as it appears before a `:` label (bare identifier or quoted
3822
+ * label, e.g. `"1deval":`, `"1D":`). */
3823
+ labelFor(name) {
3824
+ return identOrQuoted(name);
3825
+ }
3826
+ /**
3827
+ * `///` doc lines for a `Documentation`. A title is written as a leading
3828
+ * Markdown H1 (`# Title`); the description follows after a blank `///` line.
3829
+ * Either alone emits just that part.
3830
+ */
3831
+ docLines(doc) {
3832
+ if (!doc) return [];
3833
+ const out = [];
3834
+ if (doc.title) out.push(`/// # ${doc.title}`);
3835
+ if (doc.title && doc.description) out.push("///");
3836
+ if (doc.description) for (const line of wrapDoc(doc.description, DOC_WIDTH)) out.push(line === "" ? "///" : `/// ${line}`);
3837
+ return out;
3838
+ }
3839
+ /**
3840
+ * Whether a `///` block would not round-trip, so the doc must instead be
3841
+ * emitted as `.title()` / `.description()` chaining (which sets the fields
3842
+ * verbatim). Three cases:
3843
+ * - a multi-line title (only its first line would survive `splitDocText`);
3844
+ * - a description with a lone line break (a single `\n`, not a blank-line
3845
+ * paragraph break), which a `///` block reflows to a space - chaining keeps
3846
+ * the hard break intact;
3847
+ * - a title-less description whose first line looks like an H1 heading
3848
+ * (`# ...`), which would be promoted to a spurious title.
3849
+ * A blank-line paragraph break is safe: the frontend reflows a `///` block back
3850
+ * into the same paragraphs, and a description under a title is safe too
3851
+ * (`splitDocText` consumes only the very first line as the title).
3852
+ */
3853
+ docNeedsChain(doc) {
3854
+ if (!doc) return false;
3855
+ if (doc.title?.includes("\n")) return true;
3856
+ const desc = doc.description;
3857
+ if (desc) {
3858
+ if (desc.split(/\n{2,}/).some((para) => para.includes("\n"))) return true;
3859
+ if (!doc.title && (desc.split("\n")[0] ?? "").trim().startsWith("# ")) return true;
3860
+ }
3861
+ return false;
3862
+ }
3863
+ /** `.title(...)` / `.description(...)` chaining for a doc that cannot round-trip
3864
+ * as a `///` block (see `docNeedsChain`). */
3865
+ docChain(doc) {
3866
+ if (!doc) return "";
3867
+ let chain = "";
3868
+ if (doc.title) chain += `.title(${quote(doc.title)})`;
3869
+ if (doc.description) chain += `.description(${quote(doc.description)})`;
3870
+ return chain;
3871
+ }
3872
+ /**
3873
+ * Warn for `Documentation` fields a node's `///` comment cannot carry. `title`
3874
+ * and `description` are emitted (see `docLines`); literature / comment /
3875
+ * authors / urls attached to an inner node have no surface, so surface them
3876
+ * rather than dropping silently.
3877
+ */
3878
+ warnUnrepresentableDoc(doc, where) {
3879
+ if (!doc) return;
3880
+ const lost = [];
3881
+ if (doc.literature?.length) lost.push("literature");
3882
+ if (doc.comment) lost.push("comment");
3883
+ if (doc.authors?.length) lost.push("authors");
3884
+ if (doc.urls?.length) lost.push("urls");
3885
+ if (lost.length > 0) this.warn(`Documentation ${lost.join("/")} on ${where} has no argtype node surface; ignored`);
3886
+ }
3887
+ emit(expr, app) {
3888
+ const raw = sinkDefaults(expr);
3889
+ const lines = [];
3890
+ let exe;
3891
+ let root = raw;
3892
+ if (raw.kind === "sequence" && raw.attrs.nodes[0]?.kind === "literal") {
3893
+ exe = raw.attrs.nodes[0].attrs.str;
3894
+ root = {
3895
+ kind: "sequence",
3896
+ attrs: {
3897
+ ...raw.attrs,
3898
+ nodes: raw.attrs.nodes.slice(1)
3899
+ }
3900
+ };
3901
+ if (raw.meta) root.meta = raw.meta;
3902
+ }
3903
+ const frontmatter = this.emitFrontmatter(app, exe, hasOutputs(root), hasMediaTypes(root), hasPaths(root));
3904
+ if (frontmatter) lines.push(frontmatter, "");
3905
+ const synthetic = syntheticWrap(root);
3906
+ const defNode = synthetic ? synthetic.child : root;
3907
+ const wrapperOutputs = synthetic ? synthetic.outputs : void 0;
3908
+ const rootDoc = defNode.meta?.doc ?? app?.doc;
3909
+ const rootChain = this.docNeedsChain(rootDoc);
3910
+ if (!rootChain) lines.push(...this.docLines(rootDoc));
3911
+ this.warnUnrepresentableDoc(defNode.meta?.doc, "the root node");
3912
+ const rootName = this.labelFor(defNode.meta?.name || app?.id || "tool");
3913
+ let body = this.emitNode(defNode, 0);
3914
+ if (rootChain) body += this.docChain(rootDoc);
3915
+ if (wrapperOutputs?.length) body += this.emitOutputs(wrapperOutputs, 0);
3916
+ lines.push(`${rootName}: ${body}`);
3917
+ return lines.join("\n") + "\n";
3918
+ }
3919
+ /**
3920
+ * Emit a node's core expression plus its node-local chains (value
3921
+ * constraints, default, join, count, media types) and any `.output(...)`.
3922
+ * The first line is not indented (the caller places it after a label or pad);
3923
+ * continuation lines are indented relative to `level`.
3924
+ */
3925
+ emitNode(expr, level) {
3926
+ let core;
3927
+ switch (expr.kind) {
3928
+ case "literal":
3929
+ core = quote(expr.attrs.str);
3930
+ break;
3931
+ case "str":
3932
+ core = "str" + this.defaultSuffix(expr, false);
3933
+ break;
3934
+ case "int":
3935
+ case "float": {
3936
+ core = expr.kind;
3937
+ let chained = false;
3938
+ const min = this.finiteNum(expr.attrs.minValue, "min");
3939
+ if (min !== void 0) {
3940
+ core += `.min(${min})`;
3941
+ chained = true;
3942
+ }
3943
+ const max = this.finiteNum(expr.attrs.maxValue, "max");
3944
+ if (max !== void 0) {
3945
+ core += `.max(${max})`;
3946
+ chained = true;
3947
+ }
3948
+ core += this.defaultSuffix(expr, chained);
3949
+ break;
3950
+ }
3951
+ case "path": {
3952
+ core = "path";
3953
+ const media = expr.attrs.mediaTypes ?? [];
3954
+ for (const m of media) core += `.mediaType(${quote(m)})`;
3955
+ if (expr.attrs.mutable) core += ".mutable()";
3956
+ if (expr.attrs.resolveParent) core += ".resolveParent()";
3957
+ const chained = media.length > 0 || !!expr.attrs.mutable || !!expr.attrs.resolveParent;
3958
+ core += this.defaultSuffix(expr, chained);
3959
+ break;
3960
+ }
3961
+ case "sequence":
3962
+ core = this.emitCombinator("seq", expr.attrs.nodes, level, expr.attrs.join);
3963
+ core += this.structuralDefaultSuffix(expr);
3964
+ break;
3965
+ case "optional":
3966
+ core = this.emitOptional(expr, level) + this.structuralDefaultSuffix(expr);
3967
+ break;
3968
+ case "repeat":
3969
+ core = this.emitRepeat(expr, level) + this.structuralDefaultSuffix(expr);
3970
+ break;
3971
+ case "alternative":
3972
+ for (const arm of expr.attrs.alts) {
3973
+ const tag = arm.meta?.variantTag;
3974
+ if (tag !== void 0 && tag !== arm.meta?.name) this.warn(`Union arm discriminator '${tag}' has no argtype surface and will be re-derived from the arm name '${arm.meta?.name ?? "<unnamed>"}' on re-parse`);
3975
+ }
3976
+ core = this.emitCombinator("alt", expr.attrs.alts, level) + this.structuralDefaultSuffix(expr);
3977
+ break;
3978
+ default: core = "";
3979
+ }
3980
+ if (expr.meta?.outputs?.length) core += this.emitOutputs(expr.meta.outputs, level);
3981
+ return core;
3982
+ }
3983
+ /** `String(n)` when `n` is finite; otherwise undefined with a warning, since
3984
+ * `Infinity`/`NaN` have no argtype number literal (emitting them would produce
3985
+ * an identifier that fails to re-parse). */
3986
+ finiteNum(n, where) {
3987
+ if (n === void 0) return void 0;
3988
+ if (!Number.isFinite(n)) {
3989
+ this.warn(`Non-finite number (${String(n)}) on ${where} has no argtype literal; ignored`);
3990
+ return;
3991
+ }
3992
+ return num(n);
3993
+ }
3994
+ /** `= value` on a bare terminal, else `.default(value)` once a chain started. */
3995
+ defaultSuffix(expr, chained) {
3996
+ const dv = expr.meta?.defaultValue;
3997
+ if (dv === void 0 || typeof dv === "boolean") return "";
3998
+ let value;
3999
+ if (typeof dv === "number") {
4000
+ const n = this.finiteNum(dv, "default");
4001
+ if (n === void 0) return "";
4002
+ value = n;
4003
+ } else value = quote(dv);
4004
+ return chained ? `.default(${value})` : ` = ${value}`;
4005
+ }
4006
+ /**
4007
+ * A `.default(value)` chained onto a structural node (e.g. an enum
4008
+ * `alternative`, or a wrapper whose default could not sink onto an inner
4009
+ * terminal). Booleans have no argtype value literal: the only legitimate one
4010
+ * is the `opt(literal)` flag-false convention, which the frontend regenerates,
4011
+ * so it is silently dropped; any other boolean default is warned and dropped.
4012
+ */
4013
+ structuralDefaultSuffix(expr) {
4014
+ const dv = expr.meta?.defaultValue;
4015
+ if (dv === void 0) return "";
4016
+ if (typeof dv === "boolean") {
4017
+ if (!(expr.kind === "optional" && expr.attrs.node.kind === "literal" && dv === false)) this.warn(`Boolean default on a ${expr.kind} cannot be expressed in argtype; ignored`);
4018
+ return "";
4019
+ }
4020
+ if (typeof dv === "number") {
4021
+ const n = this.finiteNum(dv, "default");
4022
+ return n === void 0 ? "" : `.default(${n})`;
4023
+ }
4024
+ return `.default(${quote(dv)})`;
4025
+ }
4026
+ emitOptional(expr, level) {
4027
+ const inner = expr.attrs.node;
4028
+ if (inner.kind === "sequence" && !inner.meta && inner.attrs.join === void 0) return this.emitCombinator("opt", inner.attrs.nodes, level);
4029
+ return this.emitCombinator("opt", [inner], level);
4030
+ }
4031
+ emitRepeat(expr, level) {
4032
+ const node = expr.attrs.node;
4033
+ let core;
4034
+ if (node.kind === "sequence" && !node.meta && node.attrs.join === void 0) core = this.emitCombinator("rep", node.attrs.nodes, level);
4035
+ else core = this.emitCombinator("rep", [node], level);
4036
+ if (expr.attrs.join !== void 0) core += `.join(${expr.attrs.join === "" ? "" : quote(expr.attrs.join)})`;
4037
+ const { countMin, countMax } = expr.attrs;
4038
+ if (countMin !== void 0 && countMin === countMax) core += `.count(${countMin})`;
4039
+ else {
4040
+ if (countMin !== void 0) core += `.countMin(${countMin})`;
4041
+ if (countMax !== void 0) core += `.countMax(${countMax})`;
4042
+ }
4043
+ return core;
4044
+ }
4045
+ emitCombinator(keyword, children, level, join) {
4046
+ let core;
4047
+ if (children.length === 0) core = `${keyword}()`;
4048
+ else {
4049
+ const items = children.map((c) => this.emitDecorated(c, level + 1));
4050
+ const inline = `${keyword}(${items.join(", ")})`;
4051
+ if (!items.some((i) => i.includes("\n")) && inline.length + level * 2 <= 80) core = inline;
4052
+ else core = `${keyword}(\n${items.map((i) => pad(level + 1) + i).join(",\n")},\n${pad(level)})`;
4053
+ }
4054
+ if (join !== void 0) core += `.join(${join === "" ? "" : quote(join)})`;
4055
+ return core;
4056
+ }
4057
+ /**
4058
+ * A list item: leading `///` doc lines, an optional `label:` prefix, then the
4059
+ * node. The first line is unindented (the caller pads it); continuation lines
4060
+ * are indented to `level`.
4061
+ */
4062
+ emitDecorated(expr, level) {
4063
+ const doc = expr.meta?.doc;
4064
+ const useChain = this.docNeedsChain(doc);
4065
+ const parts = useChain ? [] : [...this.docLines(doc)];
4066
+ this.warnUnrepresentableDoc(doc, `node '${expr.meta?.name ?? expr.kind}'`);
4067
+ const label = expr.meta?.name ? `${this.labelFor(expr.meta.name)}: ` : "";
4068
+ let node = this.emitNode(expr, level);
4069
+ if (useChain) node += this.docChain(doc);
4070
+ parts.push(label + node);
4071
+ return parts.join("\n" + pad(level));
4072
+ }
4073
+ emitOutputs(outputs, level) {
4074
+ return `.output(\n${outputs.map((o) => {
4075
+ const useChain = this.docNeedsChain(o.doc);
4076
+ const docs = useChain ? [] : this.docLines(o.doc).map((l) => pad(level + 1) + l);
4077
+ let template = pad(level + 1) + this.emitOutputTemplate(o);
4078
+ if (useChain) template += this.docChain(o.doc);
4079
+ return [...docs, template].join("\n");
4080
+ }).join(",\n")},\n${pad(level)})`;
4081
+ }
4082
+ emitOutputTemplate(output) {
4083
+ if (output.mediaTypes?.length) this.warn(`Output '${output.name ?? "<anon>"}' has media types, which have no argtype surface; ignored`);
4084
+ const label = output.name ? `${this.labelFor(output.name)}: ` : "";
4085
+ let body = "";
4086
+ for (const token of output.tokens) {
4087
+ if (token.kind === "literal") {
4088
+ body += token.value.replace(/[\\{}`]/g, (c) => `\\${c}`);
4089
+ continue;
4090
+ }
4091
+ let ref = identOrQuoted(token.target.name);
4092
+ for (const ext of token.stripExtensions ?? []) ref += `.strip_suffix(${quote(ext)})`;
4093
+ if (token.fallback !== void 0) ref += `.or(${quote(token.fallback)})`;
4094
+ body += `{${ref}}`;
4095
+ }
4096
+ return `${label}\`${body}\``;
4097
+ }
4098
+ /** Quote a frontmatter scalar (double-quoted, backslash-escaped). The
4099
+ * frontmatter parser unescapes symmetrically, so quotes/newlines round-trip. */
4100
+ fmScalar(value) {
4101
+ return quote(value);
4102
+ }
4103
+ emitFrontmatter(app, exe, outputs, mediaTypes, paths) {
4104
+ const lines = [];
4105
+ if (exe !== void 0) lines.push(`exe: ${this.fmScalar(exe)}`);
4106
+ if (app) {
4107
+ if (app.version) lines.push(`version: ${this.fmScalar(app.version)}`);
4108
+ const authors = app.doc?.authors ?? [];
4109
+ if (authors.length > 0) {
4110
+ lines.push("authors:");
4111
+ for (const a of authors) lines.push(` - ${this.fmScalar(a)}`);
4112
+ }
4113
+ const urls = app.doc?.urls ?? [];
4114
+ if (urls.length > 0) {
4115
+ lines.push("urls:");
4116
+ for (const u of urls) lines.push(` - ${this.fmScalar(u)}`);
4117
+ }
4118
+ const references = app.doc?.literature ?? [];
4119
+ if (references.length > 0) {
4120
+ lines.push("references:");
4121
+ for (const rf of references) lines.push(` - ${this.fmScalar(rf)}`);
4122
+ }
4123
+ if (app.container) {
4124
+ lines.push("container:");
4125
+ lines.push(` image: ${this.fmScalar(app.container.image)}`);
4126
+ if (app.container.type) lines.push(` type: ${this.fmScalar(app.container.type)}`);
4127
+ }
4128
+ if (app.stdout) this.emitStream(lines, "stdout", app.stdout);
4129
+ if (app.stderr) this.emitStream(lines, "stderr", app.stderr);
4130
+ if (app.doc?.comment) this.warn("AppMeta.doc.comment has no argtype surface; ignored");
4131
+ }
4132
+ const extensions = [];
4133
+ if (outputs) extensions.push("outputs");
4134
+ if (mediaTypes) extensions.push("mediatypes");
4135
+ if (paths) extensions.push("paths");
4136
+ if (extensions.length > 0) {
4137
+ lines.push("extensions:");
4138
+ for (const e of extensions) lines.push(` - ${e}`);
4139
+ }
4140
+ if (lines.length === 0) return void 0;
4141
+ return [
4142
+ "---",
4143
+ ...lines,
4144
+ "---"
4145
+ ].join("\n");
4146
+ }
4147
+ emitStream(lines, key, stream) {
4148
+ lines.push(`${key}:`);
4149
+ lines.push(` name: ${this.fmScalar(stream.name)}`);
4150
+ if (stream.doc?.description) lines.push(` description: ${this.fmScalar(stream.doc.description)}`);
4151
+ if (stream.doc?.title) this.warn(`${key} stream title has no argtype surface; ignored`);
4152
+ }
4153
+ };
4154
+ /** Emit argtype sugar-DSL source from an IR expression tree and optional `AppMeta`. */
4155
+ function generateArgtype(expr, app) {
4156
+ const emitter = new ArgtypeEmitter();
4157
+ return {
4158
+ source: emitter.emit(expr, app),
4159
+ warnings: emitter.warnings
4160
+ };
4161
+ }
4162
+
4163
+ //#endregion
4164
+ //#region src/backend/argtype/argtype.ts
4165
+ /**
4166
+ * A per-tool file stem, so co-located tools in a catalog build don't clobber one
4167
+ * another's `descriptor.argtype`. Standalone single-tool builds (no scope) keep
4168
+ * the bare name. Mirrors the JSON Schema backend's `schemaStem`.
4169
+ */
4170
+ function argtypeStem(ctx, scope) {
4171
+ const id = ctx.app?.id;
4172
+ if (!id || !scope) return void 0;
4173
+ return scope.add(snakeCase(id) || "descriptor");
4174
+ }
4175
+ /**
4176
+ * Serialization backend: emit argtype sugar-DSL source from the IR + `AppMeta`.
4177
+ *
4178
+ * The dogfooding / round-trip counterpart to the argtype frontend. Like the
4179
+ * Boutiques backend it only needs the IR (`ctx.expr`) and app metadata
4180
+ * (`ctx.app`), never the solved bindings, so it ignores the rest of the context.
4181
+ */
4182
+ var ArgtypeBackend = class {
4183
+ name = "argtype";
4184
+ target = "argtype";
4185
+ /** One scope per package so per-tool file stems stay unique in the suite dir. */
4186
+ newPackageScope() {
4187
+ return new Scope();
4188
+ }
4189
+ emitApp(ctx, scope) {
4190
+ const { source, warnings } = generateArgtype(ctx.expr, ctx.app);
4191
+ const stem = argtypeStem(ctx, scope);
4192
+ const filename = stem ? `${stem}.argtype` : "descriptor.argtype";
4193
+ return {
4194
+ meta: ctx.app,
4195
+ files: new Map([[filename, source]]),
4196
+ errors: [],
4197
+ warnings
4198
+ };
4199
+ }
4200
+ };
4201
+
2230
4202
  //#endregion
2231
4203
  //#region src/backend/find-doc.ts
2232
4204
  /**
@@ -2558,53 +4530,6 @@ function planScope(scope, scopeGate, bindings) {
2558
4530
  return scope.outputs.map((output) => planOutput(scopeGate, output, bindings));
2559
4531
  }
2560
4532
 
2561
- //#endregion
2562
- //#region src/backend/scope.ts
2563
- /** Symbol collision avoidance for code generation. */
2564
- var Scope = class Scope {
2565
- reserved;
2566
- used;
2567
- parent;
2568
- constructor(reserved = [], parent) {
2569
- this.reserved = new Set(reserved);
2570
- this.used = /* @__PURE__ */ new Set();
2571
- this.parent = parent;
2572
- }
2573
- /** Check if a symbol is already taken (in this scope or any parent). */
2574
- has(symbol) {
2575
- return this.reserved.has(symbol) || this.used.has(symbol) || (this.parent?.has(symbol) ?? false);
2576
- }
2577
- /**
2578
- * Add a symbol, appending a numeric suffix to avoid collisions. Returns the
2579
- * safe name.
2580
- *
2581
- * When a `recase` transform is given, a disambiguated candidate is routed back
2582
- * through it so the suffix is absorbed into the identifier's casing - e.g.
2583
- * `pascalCase` folds `Config_2` into `Config2` - rather than leaving a
2584
- * mixed-case `Config_2`. Uniqueness is always checked on the final emitted
2585
- * form, so two hints that case-collide still get distinct names. Defaults to
2586
- * identity (the bare `<name>_<n>` suffix) for callers that don't case-normalize.
2587
- */
2588
- add(candidate, recase = (s) => s) {
2589
- if (!this.has(candidate)) {
2590
- this.used.add(candidate);
2591
- return candidate;
2592
- }
2593
- let suffix = 2;
2594
- let safe = recase(`${candidate}_${suffix}`);
2595
- while (this.has(safe)) {
2596
- suffix++;
2597
- safe = recase(`${candidate}_${suffix}`);
2598
- }
2599
- this.used.add(safe);
2600
- return safe;
2601
- }
2602
- /** Create a child scope that inherits this scope's restrictions. */
2603
- child(reserved = []) {
2604
- return new Scope(reserved, this);
2605
- }
2606
- };
2607
-
2608
4533
  //#endregion
2609
4534
  //#region src/backend/boutiques/boutiques.ts
2610
4535
  var BoutiquesEmitter = class {
@@ -2762,7 +4687,7 @@ var BoutiquesEmitter = class {
2762
4687
  }
2763
4688
  if (peeled.flag) {
2764
4689
  input["command-line-flag"] = peeled.flag;
2765
- if (peeled.flagSeparator) input["command-line-flag-separator"] = peeled.flagSeparator;
4690
+ if (peeled.flagSeparator !== void 0) input["command-line-flag-separator"] = peeled.flagSeparator;
2766
4691
  }
2767
4692
  if (mapped.integer) input.integer = true;
2768
4693
  if (mapped.minimum !== void 0) input.minimum = mapped.minimum;
@@ -2903,7 +4828,7 @@ var BoutiquesEmitter = class {
2903
4828
  }
2904
4829
  if (peeled.flag) {
2905
4830
  input["command-line-flag"] = peeled.flag;
2906
- if (peeled.flagSeparator) input["command-line-flag-separator"] = peeled.flagSeparator;
4831
+ if (peeled.flagSeparator !== void 0) input["command-line-flag-separator"] = peeled.flagSeparator;
2907
4832
  }
2908
4833
  if (mapped.integer) input.integer = true;
2909
4834
  if (mapped.minimum !== void 0) input.minimum = mapped.minimum;
@@ -2972,9 +4897,11 @@ var BoutiquesEmitter = class {
2972
4897
  const nodes = node.attrs.nodes;
2973
4898
  if (nodes.length === 2 && nodes[0].kind === "literal") {
2974
4899
  const flagLit = nodes[0].attrs.str;
2975
- const { flag, separator } = this.splitFlagLiteral(flagLit);
2976
- result.flag = flag;
2977
- if (separator) result.flagSeparator = separator;
4900
+ if (node.attrs.join !== void 0) {
4901
+ const { flag, separator } = this.splitFlagLiteral(flagLit);
4902
+ result.flag = flag;
4903
+ result.flagSeparator = separator;
4904
+ } else result.flag = flagLit;
2978
4905
  this.peelNodeInner(nodes[1], type, result);
2979
4906
  }
2980
4907
  break;
@@ -3495,6 +5422,10 @@ function resultToStmt$1(r) {
3495
5422
  return isExpr$1(r) ? `cargs.append(${r.expr})` : r.stmt;
3496
5423
  }
3497
5424
  function appendLines$1(cb, code) {
5425
+ if (code.trim() === "") {
5426
+ cb.line("pass");
5427
+ return;
5428
+ }
3498
5429
  for (const line of code.split("\n")) cb.line(line);
3499
5430
  }
3500
5431
  function toStringExpr$1(node, type, expr) {
@@ -9078,7 +11009,8 @@ function solve(expr, options) {
9078
11009
  //#region src/index.ts
9079
11010
  function compile(source, filenameOrOptions) {
9080
11011
  const options = typeof filenameOrOptions === "string" ? { filename: filenameOrOptions } : filenameOrOptions ?? {};
9081
- const format = options.format ?? detectFormat(source);
11012
+ const byExtension = options.filename?.endsWith(".argtype") ? "argtype" : void 0;
11013
+ const format = options.format ?? byExtension ?? detectFormat(source);
9082
11014
  if (!format) return {
9083
11015
  expr: {
9084
11016
  kind: "sequence",
@@ -9087,9 +11019,18 @@ function compile(source, filenameOrOptions) {
9087
11019
  errors: [{ message: "Could not detect input format. Specify format explicitly." }],
9088
11020
  warnings: []
9089
11021
  };
9090
- return (format === "argdump" ? new ArgdumpParser() : format === "workbench" ? new WorkbenchParser() : format === "mrtrix" ? new MrtrixParser() : new BoutiquesParser()).parse(source, options.filename);
11022
+ return (() => {
11023
+ switch (format) {
11024
+ case "argdump": return new ArgdumpParser();
11025
+ case "argtype": return new ArgtypeParser();
11026
+ case "workbench": return new WorkbenchParser();
11027
+ case "mrtrix": return new MrtrixParser();
11028
+ case "boutiques": return new BoutiquesParser();
11029
+ default: return new BoutiquesParser();
11030
+ }
11031
+ })().parse(source, options.filename);
9091
11032
  }
9092
11033
 
9093
11034
  //#endregion
9094
- export { BoutiquesBackend, CodeBuilder, JsonSchemaBackend, NipypeBackend, PYTHON_RUNNER_DEPS, PassStatus, PydraBackend, PythonBackend, STYXDEFS_COMPAT, Scope, TypeScriptBackend, alt, appEntrypoint, atomKey, buildEmitModel, buildSigEntries, buildTypedSpec, camelCase, canonicalize, collectFieldInfo, collectNamedTypes, compactTokens, compile, compose, createContext, createPipeline, createRegistry, defaultNamingStrategy, defaultPipeline, detectFormat, effectiveOutputName, findDoc, findStructNode, fixpoint, flatten, float, format, formatSolveResult, generateBoutiques, generateNipype, generateOutputsSchema, generatePydra, generatePython, generateSchema, generateTypeScript, int, isGated, isIterated, isStructural, isTerminal, lit, nipypeNames, nodeRef, opt, outputGate, pascalCase, path, planOutput, planScope, pydraNames, removeEmpty, renderPythonCall, renderStructLiteral, renderTypeScriptCall, renderValue, rep, repJoin, resolveFieldBinding, resolveOutputs, resolveTypeName, screamingSnakeCase, seq, seqJoin, simplify, snakeCase, solve, str, structKey, typeKey, unionKey };
11035
+ export { ArgtypeBackend, BoutiquesBackend, CodeBuilder, JsonSchemaBackend, NipypeBackend, PYTHON_RUNNER_DEPS, PassStatus, PydraBackend, PythonBackend, STYXDEFS_COMPAT, Scope, TypeScriptBackend, alt, appEntrypoint, atomKey, buildEmitModel, buildSigEntries, buildTypedSpec, camelCase, canonicalize, collectFieldInfo, collectNamedTypes, compactTokens, compile, compose, createContext, createPipeline, createRegistry, defaultNamingStrategy, defaultPipeline, detectFormat, effectiveOutputName, findDoc, findStructNode, fixpoint, flatten, float, format, formatSolveResult, generateArgtype, generateBoutiques, generateNipype, generateOutputsSchema, generatePydra, generatePython, generateSchema, generateTypeScript, int, isGated, isIterated, isStructural, isTerminal, lit, nipypeNames, nodeRef, opt, outputGate, pascalCase, path, planOutput, planScope, pydraNames, removeEmpty, renderPythonCall, renderStructLiteral, renderTypeScriptCall, renderValue, rep, repJoin, resolveFieldBinding, resolveOutputs, resolveTypeName, screamingSnakeCase, seq, seqJoin, simplify, snakeCase, solve, str, structKey, typeKey, unionKey };
9095
11036
  //# sourceMappingURL=index.mjs.map