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