@styx-api/core 0.7.0 → 0.8.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.
Files changed (44) hide show
  1. package/dist/index.cjs +430 -356
  2. package/dist/index.d.cts +1 -34
  3. package/dist/index.d.cts.map +1 -1
  4. package/dist/index.d.mts +1 -34
  5. package/dist/index.d.mts.map +1 -1
  6. package/dist/index.mjs +431 -352
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +2 -1
  9. package/src/backend/argtype/__fixtures__/microsyntax.argtype +0 -2
  10. package/src/backend/argtype/emit.ts +2 -51
  11. package/src/backend/boutiques/boutiques.ts +55 -47
  12. package/src/backend/field-defaults.ts +40 -0
  13. package/src/backend/find-struct-node.ts +7 -0
  14. package/src/backend/index.ts +1 -9
  15. package/src/backend/nipype/emit.ts +11 -3
  16. package/src/backend/python/arg-builder.ts +2 -27
  17. package/src/backend/python/emit.ts +18 -6
  18. package/src/backend/python/outputs-emit.ts +27 -42
  19. package/src/backend/python/python.ts +46 -22
  20. package/src/backend/python/validate-emit.ts +4 -1
  21. package/src/backend/resolve-output-tokens.ts +0 -76
  22. package/src/backend/typescript/arg-builder.ts +2 -24
  23. package/src/backend/typescript/emit.ts +7 -4
  24. package/src/backend/typescript/outputs-emit.ts +15 -39
  25. package/src/backend/typescript/typemap.ts +9 -1
  26. package/src/backend/typescript/typescript.ts +41 -18
  27. package/src/backend/typescript/validate-emit.ts +4 -1
  28. package/src/backend/union-variants.ts +41 -1
  29. package/src/frontend/argdump/parser.ts +20 -19
  30. package/src/frontend/argtype/__fixtures__/afni-3dtstat.argtype +41 -0
  31. package/src/frontend/argtype/__fixtures__/bet.argtype +0 -2
  32. package/src/frontend/argtype/__fixtures__/fsl-flirt.argtype +50 -0
  33. package/src/frontend/argtype/__fixtures__/wb-command-sub.argtype +39 -0
  34. package/src/frontend/argtype/ast.ts +14 -1
  35. package/src/frontend/argtype/frontmatter.ts +49 -5
  36. package/src/frontend/argtype/lexer.ts +20 -0
  37. package/src/frontend/argtype/lower.ts +115 -75
  38. package/src/frontend/argtype/parser-frontend.ts +2 -2
  39. package/src/frontend/argtype/parser.ts +70 -24
  40. package/src/frontend/boutiques/destruct-template.ts +24 -17
  41. package/src/frontend/boutiques/parser.ts +27 -8
  42. package/src/ir/passes/canonicalize.ts +18 -5
  43. package/src/ir/passes/simplify.ts +17 -2
  44. package/src/solver/solver.ts +2 -2
package/dist/index.cjs CHANGED
@@ -206,10 +206,7 @@ var ArgdumpParser = class {
206
206
  if (nargs.__argparse__ === "REMAINDER") return {
207
207
  kind: "repeat",
208
208
  attrs: {
209
- node: {
210
- kind: "str",
211
- attrs: {}
212
- },
209
+ node,
213
210
  countMin: 0
214
211
  }
215
212
  };
@@ -580,25 +577,24 @@ var ArgdumpParser = class {
580
577
  this.error(`No valid subparsers for '${action.dest}'`);
581
578
  return null;
582
579
  }
583
- if (alts.length === 1) return alts[0];
584
- const alt = {
580
+ const node = alts.length === 1 ? alts[0] : {
585
581
  kind: "alternative",
586
582
  attrs: { alts }
587
583
  };
588
- if (!(action.subparsers_required === true || action.required === true)) {
584
+ const isRequired = action.subparsers_required === true || action.required === true;
585
+ const meta = this.buildNodeMeta(action);
586
+ if (!isRequired) {
589
587
  const opt = {
590
588
  kind: "optional",
591
- attrs: { node: alt }
589
+ attrs: { node }
592
590
  };
593
- const meta = this.buildNodeMeta(action);
594
591
  if (meta) opt.meta = meta;
595
592
  return opt;
596
593
  }
597
- const meta = this.buildNodeMeta(action);
598
- if (meta) alt.meta = meta;
599
- return alt;
594
+ if (node.kind === "alternative" && meta) node.meta = meta;
595
+ return node;
600
596
  }
601
- applyMutualExclusion(actionsByDest, groups, nodes, nodesByDest) {
597
+ applyMutualExclusion(groups, nodes, nodesByDest) {
602
598
  const excluded = /* @__PURE__ */ new Set();
603
599
  for (const group of groups) {
604
600
  if (!isObject$3(group)) continue;
@@ -658,23 +654,19 @@ var ArgdumpParser = class {
658
654
  };
659
655
  const positionals = [];
660
656
  const optionals = [];
661
- const actionsByDest = /* @__PURE__ */ new Map();
662
657
  const nodesByDest = /* @__PURE__ */ new Map();
663
658
  for (const rawAction of actions) {
664
659
  if (!isObject$3(rawAction)) continue;
665
660
  const node = this.buildAction(rawAction);
666
661
  if (!node) continue;
667
662
  const dest = rawAction.dest;
668
- if (isString$3(dest)) {
669
- actionsByDest.set(dest, rawAction);
670
- nodesByDest.set(dest, node);
671
- }
663
+ if (isString$3(dest)) nodesByDest.set(dest, node);
672
664
  if (this.isPositional(rawAction) && rawAction.action_type !== "parsers") positionals.push(node);
673
665
  else optionals.push(node);
674
666
  }
675
667
  let allNodes = [...positionals, ...optionals];
676
668
  const mutexGroups = descriptor.mutually_exclusive_groups;
677
- if (isArray$3(mutexGroups) && mutexGroups.length > 0) allNodes = this.applyMutualExclusion(actionsByDest, mutexGroups, allNodes, nodesByDest);
669
+ if (isArray$3(mutexGroups) && mutexGroups.length > 0) allNodes = this.applyMutualExclusion(mutexGroups, allNodes, nodesByDest);
678
670
  return {
679
671
  kind: "sequence",
680
672
  attrs: { nodes: allNodes }
@@ -864,6 +856,18 @@ function lex(source) {
864
856
  });
865
857
  }
866
858
  }
859
+ if (/^\d+$/.test(num) && isIdentStart(peek())) {
860
+ let rest = "";
861
+ while (i < source.length && isIdentPart(peek())) rest += advance();
862
+ const full = num + rest;
863
+ errors.push({
864
+ message: `Identifier cannot start with a digit; quote it as "${full}"`,
865
+ line: startLine,
866
+ column: startCol
867
+ });
868
+ push("ident", full, startLine, startCol);
869
+ continue;
870
+ }
867
871
  push("number", num, startLine, startCol);
868
872
  continue;
869
873
  }
@@ -1023,7 +1027,44 @@ function findColon(s) {
1023
1027
  function unescapeScalar(s) {
1024
1028
  return s.replace(/\\(.)/g, (_, c) => c === "n" ? "\n" : c === "t" ? " " : c === "r" ? "\r" : c);
1025
1029
  }
1030
+ /** Split a flow-sequence body (`a, "b, c", [d, e]`) on top-level commas, keeping
1031
+ * commas inside quotes or a nested `[...]` intact. Empty segments (from a leading,
1032
+ * trailing, or doubled comma, or an empty `[]` body) are dropped, matching YAML;
1033
+ * a quoted empty string keeps its quotes and so survives. */
1034
+ function splitFlow(body) {
1035
+ const parts = [];
1036
+ let depth = 0;
1037
+ let inQuote = null;
1038
+ let cur = "";
1039
+ for (let i = 0; i < body.length; i++) {
1040
+ const ch = body[i];
1041
+ if (inQuote === "\"" && ch === "\\") {
1042
+ cur += ch + (body[i + 1] ?? "");
1043
+ i++;
1044
+ continue;
1045
+ }
1046
+ if (inQuote) {
1047
+ if (ch === inQuote) inQuote = null;
1048
+ cur += ch;
1049
+ } else if (ch === "\"" || ch === "'") {
1050
+ inQuote = ch;
1051
+ cur += ch;
1052
+ } else if (ch === "[") {
1053
+ depth++;
1054
+ cur += ch;
1055
+ } else if (ch === "]") {
1056
+ depth--;
1057
+ cur += ch;
1058
+ } else if (ch === "," && depth === 0) {
1059
+ parts.push(cur);
1060
+ cur = "";
1061
+ } else cur += ch;
1062
+ }
1063
+ parts.push(cur);
1064
+ return parts.map((p) => p.trim()).filter((p) => p !== "");
1065
+ }
1026
1066
  function parseScalar(text) {
1067
+ if (text.startsWith("[") && text.endsWith("]") && text.length >= 2) return splitFlow(text.slice(1, -1)).map(parseScalar);
1027
1068
  if (text.startsWith("\"") && text.endsWith("\"") && text.length >= 2) return unescapeScalar(text.slice(1, -1));
1028
1069
  if (text.startsWith("'") && text.endsWith("'") && text.length >= 2) return text.slice(1, -1);
1029
1070
  if (text === "true") return true;
@@ -1236,6 +1277,37 @@ const KNOWN_METHODS = new Set([
1236
1277
  "mutable",
1237
1278
  "resolveParent"
1238
1279
  ]);
1280
+ /** Levenshtein edit distance between two strings. */
1281
+ function editDistance(a, b) {
1282
+ const m = a.length;
1283
+ const n = b.length;
1284
+ let prev = Array.from({ length: n + 1 }, (_, j) => j);
1285
+ for (let i = 1; i <= m; i++) {
1286
+ const cur = [i];
1287
+ for (let j = 1; j <= n; j++) cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
1288
+ prev = cur;
1289
+ }
1290
+ return prev[n];
1291
+ }
1292
+ /** The closest candidate to `name` within a small edit distance, for a "did you
1293
+ * mean?" hint on an unknown method. Returns undefined when nothing is close
1294
+ * enough, so a deliberate unknown-extension method gets no spurious suggestion.
1295
+ * A typo (`reqires`, `mediaTpe`) lands within distance 2 of a real method; an
1296
+ * unrelated extension method (`conflicts`) does not. The `bestDist < name.length`
1297
+ * clause additionally stops a very short unknown name from matching a longer
1298
+ * method by pure insertion (a 1-char `.n()` is not "did you mean `.min()`"). */
1299
+ function suggestMethod(name, candidates) {
1300
+ let best;
1301
+ let bestDist = Infinity;
1302
+ for (const c of candidates) {
1303
+ const d = editDistance(name, c);
1304
+ if (d < bestDist) {
1305
+ bestDist = d;
1306
+ best = c;
1307
+ }
1308
+ }
1309
+ return best !== void 0 && bestDist <= 2 && bestDist < name.length ? best : void 0;
1310
+ }
1239
1311
  var Parser = class {
1240
1312
  tokens;
1241
1313
  pos = 0;
@@ -1362,38 +1434,41 @@ var Parser = class {
1362
1434
  return {
1363
1435
  kind: "comb",
1364
1436
  op: "alt",
1365
- children: alts
1437
+ children: alts,
1438
+ ...first.span && { span: first.span }
1366
1439
  };
1367
1440
  }
1368
1441
  parseChain() {
1369
1442
  const node = this.parsePrimary();
1370
- let chained = false;
1371
1443
  while (this.at("dot")) {
1372
1444
  this.next();
1373
1445
  this.applyMethod(node);
1374
- chained = true;
1375
1446
  }
1376
1447
  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);
1448
+ this.next();
1449
+ node.default = this.parseValue();
1381
1450
  }
1382
1451
  return node;
1383
1452
  }
1384
1453
  parsePrimary() {
1385
1454
  const tok = this.peek();
1455
+ const span = {
1456
+ line: tok.line,
1457
+ column: tok.column
1458
+ };
1386
1459
  if (tok.kind === "string") {
1387
1460
  this.next();
1388
1461
  return {
1389
1462
  kind: "literal",
1390
- value: tok.value
1463
+ value: tok.value,
1464
+ span
1391
1465
  };
1392
1466
  }
1393
1467
  if (tok.kind === "lparen") return {
1394
1468
  kind: "comb",
1395
1469
  op: "seq",
1396
- children: this.parseParenList()
1470
+ children: this.parseParenList(),
1471
+ span
1397
1472
  };
1398
1473
  if (tok.kind === "ident") {
1399
1474
  if (COMBINATORS.has(tok.value) && this.peek(1).kind === "lparen") {
@@ -1402,27 +1477,31 @@ var Parser = class {
1402
1477
  return {
1403
1478
  kind: "comb",
1404
1479
  op: tok.value,
1405
- children
1480
+ children,
1481
+ span
1406
1482
  };
1407
1483
  }
1408
1484
  if (TERMINALS.has(tok.value)) {
1409
1485
  this.next();
1410
1486
  return {
1411
1487
  kind: "terminal",
1412
- terminal: tok.value
1488
+ terminal: tok.value,
1489
+ span
1413
1490
  };
1414
1491
  }
1415
1492
  this.next();
1416
1493
  return {
1417
1494
  kind: "ref",
1418
- refName: tok.value
1495
+ refName: tok.value,
1496
+ span
1419
1497
  };
1420
1498
  }
1421
1499
  this.error(`Unexpected token '${tok.value || tok.kind}' in expression`, tok);
1422
1500
  this.next();
1423
1501
  return {
1424
1502
  kind: "literal",
1425
- value: ""
1503
+ value: "",
1504
+ span
1426
1505
  };
1427
1506
  }
1428
1507
  /** Parse `( elem, elem, ... )` with optional leading docs and trailing comma. */
@@ -1450,7 +1529,8 @@ var Parser = class {
1450
1529
  }
1451
1530
  if (!KNOWN_METHODS.has(method)) {
1452
1531
  this.skipBalancedArgs();
1453
- this.warn(`Ignoring unsupported method '.${method}()'`, nameTok);
1532
+ const hint = suggestMethod(method, [...KNOWN_METHODS, "output"]);
1533
+ this.warn(`Ignoring unsupported method '.${method}()'` + (hint ? ` (did you mean '.${hint}()'?)` : ""), nameTok);
1454
1534
  return;
1455
1535
  }
1456
1536
  const args = this.parseScalarArgs();
@@ -1570,7 +1650,8 @@ var Parser = class {
1570
1650
  else out.description = arg;
1571
1651
  } else {
1572
1652
  this.skipBalancedArgs();
1573
- this.warn(`Ignoring unsupported output-template method '.${m.value}()'`, m);
1653
+ const hint = suggestMethod(m.value, CHAIN);
1654
+ this.warn(`Ignoring unsupported output-template method '.${m.value}()'` + (hint ? ` (did you mean '.${hint}()'?)` : ""), m);
1574
1655
  }
1575
1656
  }
1576
1657
  if (name !== void 0) out.name = name;
@@ -1732,6 +1813,13 @@ function effectiveOutputName(output, index) {
1732
1813
  * so an output nested in a repeat / subcommand keeps its list / struct shape
1733
1814
  * (per-output gating is recovered downstream from each ref binding's gate).
1734
1815
  */
1816
+ /** Spread a node's span into a diagnostic (no-op when the node has no span). */
1817
+ function at(span) {
1818
+ return span ? {
1819
+ line: span.line,
1820
+ column: span.column
1821
+ } : {};
1822
+ }
1735
1823
  /** Build IR `Documentation` from an AST node's already-split title/description. */
1736
1824
  function docFrom(node) {
1737
1825
  const doc = {
@@ -1743,16 +1831,27 @@ function docFrom(node) {
1743
1831
  var Lowerer = class {
1744
1832
  errors = [];
1745
1833
  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
1834
  aliases = /* @__PURE__ */ new Map();
1750
1835
  constructor(aliases) {
1751
1836
  for (const a of aliases) {
1752
- if (this.aliases.has(a.name)) this.warnings.push(`Duplicate alias '${a.name}'; last definition wins`);
1837
+ if (this.aliases.has(a.name)) this.warn(`Duplicate alias '${a.name}'; last definition wins`, a.expr);
1753
1838
  this.aliases.set(a.name, a.expr);
1754
1839
  }
1755
1840
  }
1841
+ /** Record an error, located at `node`'s span when one is available. */
1842
+ err(message, node) {
1843
+ this.errors.push({
1844
+ message,
1845
+ ...at(node?.span)
1846
+ });
1847
+ }
1848
+ /** Record a warning, located at `node`'s span when one is available. */
1849
+ warn(message, node) {
1850
+ this.warnings.push({
1851
+ message,
1852
+ ...at(node?.span)
1853
+ });
1854
+ }
1756
1855
  lower(doc) {
1757
1856
  const rootSink = [];
1758
1857
  const expr = this.lowerNode(doc.root, /* @__PURE__ */ new Set(), void 0, rootSink);
@@ -1778,17 +1877,19 @@ var Lowerer = class {
1778
1877
  if (node.kind === "ref") return this.lowerRef(node, expanding, selfName, sink);
1779
1878
  const name = node.name ?? selfName;
1780
1879
  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");
1880
+ if (node.join !== void 0 && !joinable) this.err("`.join()` is only supported on seq/set/rep/opt", node);
1782
1881
  const isNumericTerminal = node.kind === "terminal" && (node.terminal === "int" || node.terminal === "float");
1783
1882
  const isRep = node.kind === "comb" && node.op === "rep";
1784
1883
  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");
1884
+ if ((node.min !== void 0 || node.max !== void 0) && !isNumericTerminal) this.err("`.min()`/`.max()` is only supported on int/float terminals", node);
1885
+ if ((node.countMin !== void 0 || node.countMax !== void 0) && !isRep) this.err("`.count()`/`.countMin()`/`.countMax()` is only supported on rep", node);
1886
+ if ((node.mutable || node.resolveParent) && !isPath) this.err("`.mutable()`/`.resolveParent()` is only supported on path", node);
1887
+ if (node.mediaTypes?.length && !isPath) this.err("`.mediaType()` is only supported on path", node);
1888
+ const isStruct = node.kind === "comb" && (node.op === "seq" || node.op === "set");
1889
+ if (node.default !== void 0 && isStruct) {
1890
+ this.warn("`= value` / `.default()` is not supported on a seq/set struct; ignored", node);
1891
+ node.default = void 0;
1892
+ }
1792
1893
  const isSeqSet = node.kind === "comb" && (node.op === "seq" || node.op === "set");
1793
1894
  if (node.outputs?.length && !isSeqSet) for (const o of node.outputs) sink.push(this.toOutput(o, name));
1794
1895
  switch (node.kind) {
@@ -1800,7 +1901,7 @@ var Lowerer = class {
1800
1901
  case "terminal": return this.lowerTerminal(node);
1801
1902
  case "comb": return this.lowerComb(node, expanding, name, sink);
1802
1903
  default:
1803
- this.errors.push(`Unknown AST node kind '${node.kind}'`);
1904
+ this.err(`Unknown AST node kind '${node.kind}'`, node);
1804
1905
  return seq();
1805
1906
  }
1806
1907
  }
@@ -1808,14 +1909,15 @@ var Lowerer = class {
1808
1909
  const target = node.refName;
1809
1910
  const aliasExpr = this.aliases.get(target);
1810
1911
  if (!aliasExpr) {
1811
- this.errors.push(`Unknown alias '${target}'`);
1912
+ this.err(`Unknown alias '${target}'`, node);
1812
1913
  return seq();
1813
1914
  }
1814
1915
  if (expanding.has(target)) {
1815
- this.errors.push(`Recursive alias '${target}' is not allowed`);
1916
+ this.err(`Recursive alias '${target}' is not allowed`, node);
1816
1917
  return seq();
1817
1918
  }
1818
1919
  const clone = structuredClone(aliasExpr);
1920
+ clone.span = node.span ?? clone.span;
1819
1921
  clone.name = node.name ?? clone.name;
1820
1922
  clone.title = node.title ?? clone.title;
1821
1923
  clone.description = node.description ?? clone.description;
@@ -1837,7 +1939,7 @@ var Lowerer = class {
1837
1939
  switch (node.terminal) {
1838
1940
  case "int": {
1839
1941
  const e = int();
1840
- this.checkBounds(node.min, node.max, "value", "min", "max");
1942
+ this.checkBounds(node.min, node.max, "value", "min", "max", node);
1841
1943
  if (node.min !== void 0) e.attrs.minValue = node.min;
1842
1944
  if (node.max !== void 0) e.attrs.maxValue = node.max;
1843
1945
  this.applyMeta(e, node);
@@ -1845,7 +1947,7 @@ var Lowerer = class {
1845
1947
  }
1846
1948
  case "float": {
1847
1949
  const e = float();
1848
- this.checkBounds(node.min, node.max, "value", "min", "max");
1950
+ this.checkBounds(node.min, node.max, "value", "min", "max", node);
1849
1951
  if (node.min !== void 0) e.attrs.minValue = node.min;
1850
1952
  if (node.max !== void 0) e.attrs.maxValue = node.max;
1851
1953
  this.applyMeta(e, node);
@@ -1865,7 +1967,7 @@ var Lowerer = class {
1865
1967
  return e;
1866
1968
  }
1867
1969
  default:
1868
- this.errors.push(`Unknown terminal '${String(node.terminal)}'`);
1970
+ this.err(`Unknown terminal '${String(node.terminal)}'`, node);
1869
1971
  return str();
1870
1972
  }
1871
1973
  }
@@ -1878,7 +1980,7 @@ var Lowerer = class {
1878
1980
  const selfOutputs = [];
1879
1981
  for (const o of node.outputs ?? []) selfOutputs.push(this.toOutput(o, name));
1880
1982
  const lowered = lowerChildren(selfOutputs);
1881
- this.dedupeSiblingNames(lowered);
1983
+ this.dedupeSiblingNames(lowered, node);
1882
1984
  const e = seq(...lowered);
1883
1985
  if (node.join !== void 0) e.attrs.join = node.join;
1884
1986
  this.applyMeta(e, node);
@@ -1890,14 +1992,14 @@ var Lowerer = class {
1890
1992
  }
1891
1993
  case "alt": {
1892
1994
  const arms = lowerChildren(sink);
1893
- this.dedupeSiblingNames(arms);
1995
+ this.dedupeSiblingNames(arms, node);
1894
1996
  const e = alt(...arms);
1895
1997
  this.applyMeta(e, node);
1896
1998
  return e;
1897
1999
  }
1898
2000
  case "any": {
1899
2001
  if (children.length === 0) {
1900
- this.errors.push("`any(...)` requires at least one branch");
2002
+ this.err("`any(...)` requires at least one branch", node);
1901
2003
  return seq();
1902
2004
  }
1903
2005
  const e = this.lowerNode(children[0], expanding, name, sink);
@@ -1928,21 +2030,21 @@ var Lowerer = class {
1928
2030
  case "rep": {
1929
2031
  const e = rep(this.wrapChildren(children, expanding, name, sink));
1930
2032
  if (node.join !== void 0) e.attrs.join = node.join;
1931
- this.checkBounds(node.countMin, node.countMax, "repetition count", "countMin", "countMax");
2033
+ this.checkBounds(node.countMin, node.countMax, "repetition count", "countMin", "countMax", node);
1932
2034
  if (node.countMin !== void 0) e.attrs.countMin = node.countMin;
1933
2035
  if (node.countMax !== void 0) e.attrs.countMax = node.countMax;
1934
2036
  this.applyMeta(e, node);
1935
2037
  return e;
1936
2038
  }
1937
2039
  default:
1938
- this.errors.push(`Unknown combinator '${String(node.op)}'`);
2040
+ this.err(`Unknown combinator '${String(node.op)}'`, node);
1939
2041
  return seq();
1940
2042
  }
1941
2043
  }
1942
2044
  /** Warn on an inverted `[min, max]` pair (a lower bound above its upper
1943
2045
  * 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})`);
2046
+ checkBounds(min, max, what, minLabel, maxLabel, node) {
2047
+ if (min !== void 0 && max !== void 0 && min > max) this.warn(`Inverted ${what} bounds: ${minLabel} (${min}) > ${maxLabel} (${max})`, node);
1946
2048
  }
1947
2049
  /**
1948
2050
  * Disambiguate duplicate explicit names among the direct children of a
@@ -1956,7 +2058,7 @@ var Lowerer = class {
1956
2058
  * explicit labels; it does not detect collisions between solver-derived names
1957
2059
  * of otherwise-unnamed children, which the solver also does not guard.)
1958
2060
  */
1959
- dedupeSiblingNames(children) {
2061
+ dedupeSiblingNames(children, parent) {
1960
2062
  const used = /* @__PURE__ */ new Set();
1961
2063
  for (const child of children) {
1962
2064
  const nm = child.meta?.name;
@@ -1973,7 +2075,7 @@ var Lowerer = class {
1973
2075
  ...child.meta,
1974
2076
  name: renamed
1975
2077
  };
1976
- this.warnings.push(`Duplicate sibling name '${nm}'; renamed one occurrence to '${renamed}'`);
2078
+ this.warn(`Duplicate sibling name '${nm}'; renamed one occurrence to '${renamed}'`, parent);
1977
2079
  }
1978
2080
  }
1979
2081
  /** `opt`/`rep` with multiple children implicitly wrap them in a sequence,
@@ -2012,15 +2114,15 @@ var Lowerer = class {
2012
2114
  }
2013
2115
  const targetName = t.name ?? selfName;
2014
2116
  if (!targetName) {
2015
- this.warnings.push("Output self-reference '{}' has no named node to resolve to; emitting empty");
2117
+ this.warn("Output self-reference '{}' has no named node to resolve to; emitting empty");
2016
2118
  tokens.push({
2017
2119
  kind: "literal",
2018
2120
  value: ""
2019
2121
  });
2020
2122
  continue;
2021
2123
  }
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");
2124
+ if (t.stripPrefix?.length) this.warn("Output ref op 'strip_prefix' is not supported by the IR; ignored");
2125
+ if (t.basename) this.warn("Output ref op 'basename' is not supported by the IR; ignored");
2024
2126
  tokens.push({
2025
2127
  kind: "ref",
2026
2128
  target: nodeRef(targetName),
@@ -2028,7 +2130,7 @@ var Lowerer = class {
2028
2130
  ...t.or !== void 0 && { fallback: t.or }
2029
2131
  });
2030
2132
  }
2031
- if (o.fallback !== void 0) this.warnings.push("Output-level '.or(...)' fallback is not supported by the IR; ignored");
2133
+ if (o.fallback !== void 0) this.warn("Output-level '.or(...)' fallback is not supported by the IR; ignored");
2032
2134
  const doc = docFrom(o);
2033
2135
  return {
2034
2136
  ...o.name && { name: o.name },
@@ -2057,6 +2159,17 @@ function buildAppMeta(fm, rootDoc, rootName) {
2057
2159
  const authors = strList(fm.authors);
2058
2160
  const urls = strList(fm.urls);
2059
2161
  const references = strList(fm.references);
2162
+ const checkList = (key, v) => {
2163
+ if (v !== void 0 && v !== null && !Array.isArray(v)) warnings.push(`Frontmatter '${key}' should be a list; got a scalar - ignored`);
2164
+ };
2165
+ checkList("authors", fm.authors);
2166
+ checkList("urls", fm.urls);
2167
+ checkList("references", fm.references);
2168
+ if (fm.version !== void 0 && fm.version !== null && version === void 0) warnings.push(`Frontmatter 'version' should be a string or number; ignored`);
2169
+ if (fm.container !== void 0 && fm.container !== null) {
2170
+ if (!isRecord$1(fm.container)) warnings.push(`Frontmatter 'container' should be a mapping with an 'image'; ignored`);
2171
+ else if (!asStr(fm.container.image)) warnings.push(`Frontmatter 'container' is missing an 'image'; ignored`);
2172
+ }
2060
2173
  const doc = {
2061
2174
  ...rootTitleDesc,
2062
2175
  ...authors.length > 0 && { authors },
@@ -2106,28 +2219,18 @@ function isRecord$1(x) {
2106
2219
  return typeof x === "object" && x !== null && !Array.isArray(x);
2107
2220
  }
2108
2221
  function lowerDocument(doc) {
2109
- const lowerer = new Lowerer(doc.aliases);
2110
- const result = lowerer.lower(doc);
2222
+ const result = new Lowerer(doc.aliases).lower(doc);
2111
2223
  const exe = typeof doc.frontmatter?.exe === "string" ? doc.frontmatter.exe : void 0;
2112
2224
  if (exe) result.expr.attrs.nodes.unshift(lit(exe));
2113
2225
  const { meta, warnings } = buildAppMeta(doc.frontmatter, {
2114
2226
  title: doc.root.title,
2115
2227
  description: doc.root.description
2116
2228
  }, 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
2229
  return {
2123
2230
  ...meta && { meta },
2124
2231
  expr: result.expr,
2125
2232
  errors: result.errors,
2126
- warnings: [
2127
- ...result.warnings,
2128
- ...warnings,
2129
- ...extWarnings
2130
- ]
2233
+ warnings: [...result.warnings, ...warnings.map((message) => ({ message }))]
2131
2234
  };
2132
2235
  }
2133
2236
 
@@ -2174,8 +2277,14 @@ var ArgtypeParser = class {
2174
2277
  warnings
2175
2278
  };
2176
2279
  const lowered = lowerDocument(doc);
2177
- warnings.push(...lowered.warnings.map((message) => ({ message })));
2178
- errors.push(...lowered.errors.map((message) => ({ message })));
2280
+ warnings.push(...lowered.warnings.map((d) => ({
2281
+ message: d.message,
2282
+ ...toLocation(d)
2283
+ })));
2284
+ errors.push(...lowered.errors.map((d) => ({
2285
+ message: d.message,
2286
+ ...toLocation(d)
2287
+ })));
2179
2288
  return {
2180
2289
  ...lowered.meta && { meta: lowered.meta },
2181
2290
  expr: lowered.expr,
@@ -2205,20 +2314,26 @@ function destructTemplate(template, lookup) {
2205
2314
  destructed.push(x);
2206
2315
  continue;
2207
2316
  }
2208
- let didSplit = false;
2317
+ let best = null;
2209
2318
  for (const [alias, replacement] of Object.entries(lookup)) {
2319
+ if (alias.length === 0) continue;
2210
2320
  const idx = x.indexOf(alias);
2211
- if (idx !== -1) {
2212
- const left = x.slice(0, idx);
2213
- const right = x.slice(idx + alias.length);
2214
- if (right.length > 0) stack.unshift(right);
2215
- stack.unshift(replacement);
2216
- if (left.length > 0) stack.unshift(left);
2217
- didSplit = true;
2218
- break;
2219
- }
2321
+ if (idx === -1) continue;
2322
+ if (best === null || idx < best.idx || idx === best.idx && alias.length > best.alias.length) best = {
2323
+ idx,
2324
+ alias,
2325
+ replacement
2326
+ };
2220
2327
  }
2221
- if (!didSplit) destructed.push(x);
2328
+ if (best === null) {
2329
+ destructed.push(x);
2330
+ continue;
2331
+ }
2332
+ const left = x.slice(0, best.idx);
2333
+ const right = x.slice(best.idx + best.alias.length);
2334
+ if (right.length > 0) stack.unshift(right);
2335
+ stack.unshift(best.replacement);
2336
+ if (left.length > 0) stack.unshift(left);
2222
2337
  }
2223
2338
  return destructed;
2224
2339
  }
@@ -2546,14 +2661,8 @@ var BoutiquesParser = class {
2546
2661
  kind: "int",
2547
2662
  attrs: {}
2548
2663
  };
2549
- if (isNumber$1(btInput.minimum)) {
2550
- node.attrs.minValue = Math.floor(btInput.minimum);
2551
- if (btInput["exclusive-minimum"] === true) node.attrs.minValue += 1;
2552
- }
2553
- if (isNumber$1(btInput.maximum)) {
2554
- node.attrs.maxValue = Math.floor(btInput.maximum);
2555
- if (btInput["exclusive-maximum"] === true) node.attrs.maxValue -= 1;
2556
- }
2664
+ if (isNumber$1(btInput.minimum)) node.attrs.minValue = btInput["exclusive-minimum"] === true ? Math.floor(btInput.minimum) + 1 : Math.ceil(btInput.minimum);
2665
+ if (isNumber$1(btInput.maximum)) node.attrs.maxValue = btInput["exclusive-maximum"] === true ? Math.ceil(btInput.maximum) - 1 : Math.floor(btInput.maximum);
2557
2666
  if (meta) node.meta = meta;
2558
2667
  return node;
2559
2668
  }
@@ -2603,7 +2712,10 @@ var BoutiquesParser = class {
2603
2712
  return null;
2604
2713
  }
2605
2714
  const node = this.parseDescriptor(nested);
2606
- if (node && meta) node.meta = meta;
2715
+ if (node && meta) node.meta = {
2716
+ ...node.meta,
2717
+ ...meta
2718
+ };
2607
2719
  return node;
2608
2720
  }
2609
2721
  case InputTypePrimitive.SubCommandUnion: {
@@ -2702,13 +2814,16 @@ var BoutiquesParser = class {
2702
2814
  node = this.wrapWithFlag(node, btInput);
2703
2815
  if (inputType.isOptional) node = this.wrapWithOptional(node);
2704
2816
  if (node !== inner && inner.meta) {
2705
- const { name, ...rest } = inner.meta;
2817
+ const { name, outputs, ...rest } = inner.meta;
2706
2818
  if (Object.keys(rest).length > 0) {
2707
2819
  node.meta = {
2708
2820
  ...node.meta,
2709
2821
  ...rest
2710
2822
  };
2711
- inner.meta = name ? { name } : void 0;
2823
+ const kept = {};
2824
+ if (name) kept.name = name;
2825
+ if (outputs) kept.outputs = outputs;
2826
+ inner.meta = Object.keys(kept).length > 0 ? kept : void 0;
2712
2827
  }
2713
2828
  }
2714
2829
  return node;
@@ -3786,25 +3901,6 @@ function syntheticWrap(root) {
3786
3901
  ...meta?.outputs && { outputs: meta.outputs }
3787
3902
  };
3788
3903
  }
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
3904
  const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
3809
3905
  /** A name rendered where an identifier is expected: bare when it is a valid
3810
3906
  * identifier, otherwise a double-quoted form preserving it exactly. Used for
@@ -3901,7 +3997,7 @@ var ArgtypeEmitter = class {
3901
3997
  };
3902
3998
  if (raw.meta) root.meta = raw.meta;
3903
3999
  }
3904
- const frontmatter = this.emitFrontmatter(app, exe, hasOutputs(root), hasMediaTypes(root), hasPaths(root));
4000
+ const frontmatter = this.emitFrontmatter(app, exe);
3905
4001
  if (frontmatter) lines.push(frontmatter, "");
3906
4002
  const synthetic = syntheticWrap(root);
3907
4003
  const defNode = synthetic ? synthetic.child : root;
@@ -4101,7 +4197,7 @@ var ArgtypeEmitter = class {
4101
4197
  fmScalar(value) {
4102
4198
  return quote(value);
4103
4199
  }
4104
- emitFrontmatter(app, exe, outputs, mediaTypes, paths) {
4200
+ emitFrontmatter(app, exe) {
4105
4201
  const lines = [];
4106
4202
  if (exe !== void 0) lines.push(`exe: ${this.fmScalar(exe)}`);
4107
4203
  if (app) {
@@ -4130,14 +4226,6 @@ var ArgtypeEmitter = class {
4130
4226
  if (app.stderr) this.emitStream(lines, "stderr", app.stderr);
4131
4227
  if (app.doc?.comment) this.warn("AppMeta.doc.comment has no argtype surface; ignored");
4132
4228
  }
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
4229
  if (lines.length === 0) return void 0;
4142
4230
  return [
4143
4231
  "---",
@@ -4279,6 +4367,7 @@ function resolveFieldBinding(node, ctx, structType, outermost) {
4279
4367
  * sequence is the actual struct owner (e.g. `seq(lit("--flag"), seq(field1, field2))`).
4280
4368
  */
4281
4369
  function findStructNode(node, ctx, structType) {
4370
+ if (node.kind === "sequence" && Object.keys(structType.fields).length === 0) return node;
4282
4371
  switch (node.kind) {
4283
4372
  case "sequence":
4284
4373
  for (const child of node.attrs.nodes) {
@@ -4484,53 +4573,6 @@ function formatType(type, indent = 0) {
4484
4573
  }
4485
4574
  }
4486
4575
 
4487
- //#endregion
4488
- //#region src/backend/resolve-output-tokens.ts
4489
- /**
4490
- * Compact consecutive literal tokens. Backends that emit string concatenation
4491
- * benefit from a shorter token stream; backends that template each token
4492
- * individually can ignore this and use `output.tokens` directly.
4493
- */
4494
- function compactTokens(tokens) {
4495
- const out = [];
4496
- for (const tok of tokens) {
4497
- const last = out[out.length - 1];
4498
- if (tok.kind === "literal" && last && last.kind === "literal") out[out.length - 1] = {
4499
- kind: "literal",
4500
- value: last.value + tok.value
4501
- };
4502
- else out.push(tok);
4503
- }
4504
- return out;
4505
- }
4506
- function planOutput(scopeGate, output, bindings) {
4507
- return {
4508
- name: output.name,
4509
- gate: outputGate(scopeGate, output, bindings),
4510
- tokens: compactTokens(output.tokens),
4511
- resolved: output
4512
- };
4513
- }
4514
- /**
4515
- * Does the output have any conditional wrapper? Equivalent to "is at least one
4516
- * atom a `present` or `variant`?". `iter` alone means the output emits a list
4517
- * and is not conditionally absent.
4518
- */
4519
- function isGated(plan) {
4520
- return plan.gate.some((a) => a.kind === "present" || a.kind === "variant");
4521
- }
4522
- /** Does the output iterate (emit zero-or-more values)? */
4523
- function isIterated(plan) {
4524
- return plan.gate.some((a) => a.kind === "iter");
4525
- }
4526
- /**
4527
- * Convenience for backends emitting all outputs of a scope at once. The caller
4528
- * provides the scope's gate (typically `bindings.get(scope.scope)?.gate ?? []`).
4529
- */
4530
- function planScope(scope, scopeGate, bindings) {
4531
- return scope.outputs.map((output) => planOutput(scopeGate, output, bindings));
4532
- }
4533
-
4534
4576
  //#endregion
4535
4577
  //#region src/backend/boutiques/boutiques.ts
4536
4578
  var BoutiquesEmitter = class {
@@ -4746,14 +4788,12 @@ var BoutiquesEmitter = class {
4746
4788
  if (flagStr) peeled.flag = flagStr;
4747
4789
  }
4748
4790
  const input = this.buildInput(binding, id, fieldType, valueKeyStr, peeled, fieldInfo, wrapperNode);
4749
- if (peeled.flag) if (fieldType.kind === "bool" || fieldType.kind === "optional" && this.isBool(fieldType)) commandParts.push(valueKeyStr);
4750
- else commandParts.push(valueKeyStr);
4751
- else commandParts.push(valueKeyStr);
4791
+ commandParts.push(valueKeyStr);
4752
4792
  inputs.push(input);
4753
4793
  }
4754
4794
  bt["command-line"] = commandParts.join(" ");
4755
4795
  bt.inputs = inputs;
4756
- this.emitOutputFiles(bt, expr, valueKeyByBinding, idScope);
4796
+ this.emitOutputFiles(bt, structNode, valueKeyByBinding, idScope);
4757
4797
  }
4758
4798
  emitOutputFiles(bt, scopeNode, valueKeys, idScope) {
4759
4799
  const scopeBinding = this.ctx.resolve(scopeNode);
@@ -4846,6 +4886,7 @@ var BoutiquesEmitter = class {
4846
4886
  }
4847
4887
  finalizeInput(input) {
4848
4888
  if (input.name === void 0) input.name = input.id;
4889
+ this.finalizeSubDescriptors(input);
4849
4890
  if (input.type === "String" && typeof input["default-value"] === "boolean" && input["value-choices"] === void 0) input.type = "Flag";
4850
4891
  if (input.type === "Flag") {
4851
4892
  delete input["default-value"];
@@ -4872,6 +4913,15 @@ var BoutiquesEmitter = class {
4872
4913
  if (Array.isArray(choices) && dv !== void 0 && !choices.some((c) => c === dv)) delete input["default-value"];
4873
4914
  this.mergeDefaultIntoDescription(input);
4874
4915
  }
4916
+ finalizeSubDescriptors(input) {
4917
+ const type = input.type;
4918
+ if (Array.isArray(type)) type.forEach((sub, i) => this.ensureSubDescriptorId(sub, `${input.id}_${i + 1}`));
4919
+ else if (typeof type === "object" && type !== null) this.ensureSubDescriptorId(type, input.id);
4920
+ }
4921
+ ensureSubDescriptorId(sub, fallback) {
4922
+ if (sub.id === void 0) sub.id = this.sanitizeId(fallback);
4923
+ if (sub.name === void 0) sub.name = sub.id;
4924
+ }
4875
4925
  peelNode(node, type) {
4876
4926
  const result = {
4877
4927
  isOptional: false,
@@ -4895,6 +4945,8 @@ var BoutiquesEmitter = class {
4895
4945
  this.peelNodeInner(node.attrs.node, type.kind === "list" ? type.item : type, result);
4896
4946
  break;
4897
4947
  case "sequence": {
4948
+ const structType = this.unwrapType(type);
4949
+ if (structType.kind === "struct" && findStructNode(node, this.ctx, structType) === node) break;
4898
4950
  const nodes = node.attrs.nodes;
4899
4951
  if (nodes.length === 2 && nodes[0].kind === "literal") {
4900
4952
  const flagLit = nodes[0].attrs.str;
@@ -4983,8 +5035,7 @@ var BoutiquesEmitter = class {
4983
5035
  type: "String",
4984
5036
  valueChoices: type.variants.map((v) => v.type.kind === "literal" ? v.type.value : "")
4985
5037
  };
4986
- if (type.variants.every((v) => v.type.kind === "struct")) return { type: this.buildSubCommandUnion(type, node) };
4987
- return { type: this.buildMixedUnionAsSubCommands(type, node) };
5038
+ return { type: this.buildUnionSubCommands(type, node) };
4988
5039
  }
4989
5040
  buildSubCommand(type, node) {
4990
5041
  const bt = {};
@@ -4995,22 +5046,7 @@ var BoutiquesEmitter = class {
4995
5046
  }
4996
5047
  return bt;
4997
5048
  }
4998
- buildSubCommandUnion(type, node) {
4999
- const alts = node.kind === "alternative" ? node.attrs.alts : [node];
5000
- return type.variants.map((variant, i) => {
5001
- const altNode = alts[i] ?? node;
5002
- if (variant.type.kind === "struct") {
5003
- const bt = this.buildSubCommand(variant.type, altNode);
5004
- if (variant.name) {
5005
- bt.name = variant.name;
5006
- bt.id = this.sanitizeId(variant.name);
5007
- }
5008
- return bt;
5009
- }
5010
- return this.wrapAsDescriptor(variant, altNode);
5011
- });
5012
- }
5013
- buildMixedUnionAsSubCommands(type, node) {
5049
+ buildUnionSubCommands(type, node) {
5014
5050
  const alts = node.kind === "alternative" ? node.attrs.alts : [node];
5015
5051
  return type.variants.map((variant, i) => {
5016
5052
  const altNode = alts[i] ?? node;
@@ -5109,7 +5145,8 @@ var BoutiquesEmitter = class {
5109
5145
  inputs: []
5110
5146
  },
5111
5147
  list: true,
5112
- minListEntries: countMin
5148
+ minListEntries: countMin,
5149
+ ...countMin === 0 && { optional: true }
5113
5150
  };
5114
5151
  }
5115
5152
  };
@@ -5315,9 +5352,65 @@ function resolveTypeName(namedTypes) {
5315
5352
  };
5316
5353
  }
5317
5354
 
5355
+ //#endregion
5356
+ //#region src/backend/field-defaults.ts
5357
+ /**
5358
+ * Build the field-name -> rendered-default map for a struct root (else empty).
5359
+ * Includes only non-optional defaulted fields (optional fields are
5360
+ * presence-guarded; their default comes from the factory's kwarg signature).
5361
+ *
5362
+ * `rootType` defaults to the resolved root type; callers that already have it
5363
+ * (the arg-builders) pass it to avoid re-resolving.
5364
+ */
5365
+ function collectDefaults(ctx, renderLiteral, rootType = ctx.resolve(ctx.expr)?.type) {
5366
+ const out = /* @__PURE__ */ new Map();
5367
+ if (rootType?.kind !== "struct") return out;
5368
+ for (const [name, fi] of collectFieldInfo(ctx, rootType)) {
5369
+ if (fi.defaultValue === void 0) continue;
5370
+ if (rootType.fields[name]?.kind === "optional") continue;
5371
+ out.set(name, renderLiteral(fi.defaultValue));
5372
+ }
5373
+ return out;
5374
+ }
5375
+ /** The rendered default for a binding iff it is a root-level defaulted field. */
5376
+ function rootFieldDefault(binding, defaults) {
5377
+ if (!binding) return void 0;
5378
+ const a = binding.access;
5379
+ if (a.length === 1 && a[0]?.kind === "field") return defaults.get(binding.name);
5380
+ }
5381
+
5318
5382
  //#endregion
5319
5383
  //#region src/backend/union-variants.ts
5320
5384
  /**
5385
+ * Whether a union is "mixed": it has at least one non-struct (bare-literal) arm
5386
+ * alongside its struct variants (e.g. ants `n4_correction = Literal[0] | N4On`).
5387
+ *
5388
+ * The `@type` discriminator only exists on the struct arms, so indexing
5389
+ * `value["@type"]` on the union value is unsound (a type error, and a runtime
5390
+ * KeyError if the literal arm is ever hit) until the value is narrowed to a
5391
+ * struct at runtime. Backends that dispatch on `@type` must first emit a shape
5392
+ * guard (Python `isinstance(value, dict)`, TS `typeof value === "object"`) when
5393
+ * this is true; a pure-struct union (every arm a struct) needs no guard.
5394
+ */
5395
+ function unionIsMixed(unionType) {
5396
+ return unionType.variants.some((v) => v.type.kind !== "struct");
5397
+ }
5398
+ /**
5399
+ * The union type bound by a `variant` gate atom, if the atom's binding resolves
5400
+ * to a union - used by the outputs emitters to decide whether the variant gate
5401
+ * needs a mixed-union shape guard before its `@type` check. Returns `undefined`
5402
+ * for a well-formed non-union (defensive; a variant atom should always name a
5403
+ * union binding).
5404
+ */
5405
+ function variantAtomUnion(atom, bindings) {
5406
+ return resolveUnion(bindings.get(atom.binding)?.type);
5407
+ }
5408
+ function resolveUnion(type) {
5409
+ if (!type) return void 0;
5410
+ if (type.kind === "optional") return resolveUnion(type.inner);
5411
+ if (type.kind === "union") return type;
5412
+ }
5413
+ /**
5321
5414
  * The struct variants of a union, each with its index into `variants`.
5322
5415
  *
5323
5416
  * A discriminated union dispatches on a unique `@type`, so two struct variants
@@ -5455,10 +5548,6 @@ function pathArg$1(node, expr) {
5455
5548
  * carrying a Boutiques default. Restricted to single-segment (root) field access
5456
5549
  * so a nested field can never accidentally pick up a same-named root default.
5457
5550
  */
5458
- function rootFieldDefault$3(binding, defaults) {
5459
- const a = binding.access;
5460
- if (a.length === 1 && a[0]?.kind === "field") return defaults.get(binding.name);
5461
- }
5462
5551
  /**
5463
5552
  * Render a binding's access for an UNCONDITIONAL value read (terminal, repeat
5464
5553
  * loop, alternative dispatch): substitutes the field's default via
@@ -5469,7 +5558,7 @@ function rootFieldDefault$3(binding, defaults) {
5469
5558
  * via `.get()`).
5470
5559
  */
5471
5560
  function readAccess$1(binding, arg) {
5472
- const def = rootFieldDefault$3(binding, arg.defaults);
5561
+ const def = rootFieldDefault(binding, arg.defaults);
5473
5562
  return accessOf$1(binding, arg, def !== void 0 ? { finalDefault: def } : {});
5474
5563
  }
5475
5564
  /**
@@ -5489,21 +5578,6 @@ function accessOf$1(binding, arg, opts = {}) {
5489
5578
  subst: arg.valueSubst
5490
5579
  });
5491
5580
  }
5492
- /**
5493
- * Build the field-name -> rendered-default map for a struct root (else empty).
5494
- * Includes only non-optional defaulted fields (optional fields are
5495
- * presence-guarded; their default comes from the factory's kwarg signature).
5496
- */
5497
- function collectDefaults$3(ctx, rootType) {
5498
- const out = /* @__PURE__ */ new Map();
5499
- if (rootType?.kind !== "struct") return out;
5500
- for (const [name, fi] of collectFieldInfo(ctx, rootType)) {
5501
- if (fi.defaultValue === void 0) continue;
5502
- if (rootType.fields[name]?.kind === "optional") continue;
5503
- out.set(name, renderPyLiteral(fi.defaultValue));
5504
- }
5505
- return out;
5506
- }
5507
5581
  let loopVarCounter$1 = 0;
5508
5582
  let optVarCounter = 0;
5509
5583
  /**
@@ -5519,7 +5593,7 @@ function buildArgs$1(rootExpr, ctx, rootType) {
5519
5593
  joinDepth: 0,
5520
5594
  loopVars: /* @__PURE__ */ new Map(),
5521
5595
  valueSubst: /* @__PURE__ */ new Map(),
5522
- defaults: collectDefaults$3(ctx, rootType)
5596
+ defaults: collectDefaults(ctx, renderPyLiteral, rootType)
5523
5597
  });
5524
5598
  }
5525
5599
  function walk$2(node, ctx, arg) {
@@ -5679,8 +5753,8 @@ function walkAlternative$1(node, ctx, arg) {
5679
5753
  */
5680
5754
  function emitDocstring(cb, text) {
5681
5755
  if (!text) return;
5682
- const lines = text.split("\n");
5683
- if (lines.length === 1 && !lines[0].includes("\"")) {
5756
+ const lines = text.replace(/"""/g, "\\\"\\\"\\\"").split("\n");
5757
+ if (lines.length === 1 && !lines[0].includes("\"") && !lines[0].endsWith("\\")) {
5684
5758
  cb.line(`"""${lines[0]}"""`);
5685
5759
  return;
5686
5760
  }
@@ -5688,20 +5762,18 @@ function emitDocstring(cb, text) {
5688
5762
  for (const line of lines) cb.line(line);
5689
5763
  cb.line(`"""`);
5690
5764
  }
5691
- function emitImports$1(cb, emitOutputs) {
5692
- cb.line("import dataclasses");
5765
+ function emitImports$1(cb) {
5693
5766
  cb.line("import pathlib");
5694
5767
  cb.line("import typing");
5695
5768
  cb.blank();
5696
- const fromStyxdefs = [
5769
+ cb.line(`from styxdefs import ${[
5697
5770
  "Execution",
5698
5771
  "InputPathType",
5699
5772
  "Metadata",
5700
5773
  "Runner",
5701
- "StyxValidationError"
5702
- ];
5703
- if (emitOutputs) fromStyxdefs.push("OutputPathType");
5704
- cb.line(`from styxdefs import ${fromStyxdefs.join(", ")}, get_global_runner`);
5774
+ "StyxValidationError",
5775
+ "OutputPathType"
5776
+ ].join(", ")}, get_global_runner`);
5705
5777
  }
5706
5778
  function emitMetadata$1(ctx, metaConst, cb) {
5707
5779
  const id = ctx.app?.id ?? "unknown";
@@ -6203,9 +6275,10 @@ function renderInputTrait(p) {
6203
6275
  case "str": return call("traits.Str", [...hasDef ? [renderPyLiteral(def), "usedefault=True"] : [], ...tail]);
6204
6276
  case "enum": {
6205
6277
  const choices = p.choices ?? [];
6278
+ const defChoice = hasDef && def !== void 0 && typeof def !== "boolean" && choices.includes(def) ? def : void 0;
6206
6279
  return call("traits.Enum", [
6207
- ...(hasDef ? [def, ...choices.filter((c) => c !== def)] : choices).map((c) => renderPyLiteral(c)),
6208
- ...hasDef ? ["usedefault=True"] : [],
6280
+ ...(defChoice !== void 0 ? [defChoice, ...choices.filter((c) => c !== defChoice)] : choices).map((c) => renderPyLiteral(c)),
6281
+ ...defChoice !== void 0 ? ["usedefault=True"] : [],
6209
6282
  ...tail
6210
6283
  ]);
6211
6284
  }
@@ -6685,7 +6758,7 @@ function emitValue$1(e, type, node, wireKey, valueExpr, expected) {
6685
6758
  const itemNode = findRepeatNode(node)?.attrs.node;
6686
6759
  const elem = e.scope.add("e");
6687
6760
  e.cb.line(`for ${elem} in ${valueExpr}:`);
6688
- e.cb.indent(() => emitValue$1(e, type.item, itemNode, wireKey, elem, expected));
6761
+ e.cb.indent(() => emitValue$1(e, type.item, itemNode, wireKey, elem, expectedType$1(e, type.item)));
6689
6762
  return;
6690
6763
  }
6691
6764
  case "struct":
@@ -6972,10 +7045,9 @@ function streamFieldIds$1(ctx) {
6972
7045
  if (ctx.app?.stderr) res.stderr = fields[idx++].id;
6973
7046
  return res;
6974
7047
  }
6975
- /** Emit `@dataclasses.dataclass\nclass <outputsType>:` declaration. */
7048
+ /** Emit `class <outputsType>(typing.NamedTuple):` declaration. */
6976
7049
  function emitOutputsClass(ctx, outputsType, cb) {
6977
- cb.line("@dataclasses.dataclass");
6978
- cb.line(`class ${outputsType}:`);
7050
+ cb.line(`class ${outputsType}(typing.NamedTuple):`);
6979
7051
  cb.indent(() => {
6980
7052
  emitDocstring(cb, "Output paths produced by the tool.");
6981
7053
  const fields = collectOutputFields(ctx, pyId);
@@ -6994,26 +7066,6 @@ function emitOutputsClass(ctx, outputsType, cb) {
6994
7066
  }
6995
7067
  });
6996
7068
  }
6997
- /** The rendered default for a binding iff it is a root-level defaulted field. */
6998
- function rootFieldDefault$2(binding, defaults) {
6999
- if (!binding) return void 0;
7000
- const a = binding.access;
7001
- if (a.length === 1 && a[0]?.kind === "field") return defaults.get(binding.name);
7002
- }
7003
- /** Build the field-name -> rendered-default map for the struct root (else empty).
7004
- * Includes only non-optional defaulted fields (optional fields are
7005
- * presence-guarded; their default comes from the factory's kwarg signature). */
7006
- function collectDefaults$2(ctx) {
7007
- const out = /* @__PURE__ */ new Map();
7008
- const rootType = ctx.resolve(ctx.expr)?.type;
7009
- if (rootType?.kind !== "struct") return out;
7010
- for (const [name, fi] of collectFieldInfo(ctx, rootType)) {
7011
- if (fi.defaultValue === void 0) continue;
7012
- if (rootType.fields[name]?.kind === "optional") continue;
7013
- out.set(name, renderPyLiteral(fi.defaultValue));
7014
- }
7015
- return out;
7016
- }
7017
7069
  let loopCounter$1 = 0;
7018
7070
  function renderWrapperOpen$1(atom, ec) {
7019
7071
  if (atom.kind === "iter") {
@@ -7024,7 +7076,13 @@ function renderWrapperOpen$1(atom, ec) {
7024
7076
  loopVar: v
7025
7077
  };
7026
7078
  }
7027
- if (atom.kind === "variant") return { open: `if ${bindingAccess$1(atom.binding, ec)}["@type"] == ${pyStr(atom.variant)}:` };
7079
+ if (atom.kind === "variant") {
7080
+ const access = bindingAccess$1(atom.binding, ec);
7081
+ const check = `${access}["@type"] == ${pyStr(atom.variant)}`;
7082
+ const union = variantAtomUnion(atom, ec.ctx.bindings);
7083
+ if (union && unionIsMixed(union)) return { open: `if isinstance(${access}, dict) and ${check}:` };
7084
+ return { open: `if ${check}:` };
7085
+ }
7028
7086
  const binding = ec.ctx.bindings.get(atom.binding);
7029
7087
  if (binding?.type.kind === "optional") {
7030
7088
  const subscriptAccess = bindingAccess$1(atom.binding, ec);
@@ -7080,7 +7138,7 @@ function renderToken$1(tok, ec) {
7080
7138
  return renderRefValue$1(tok, ec);
7081
7139
  }
7082
7140
  function renderRefValue$1(tok, ec) {
7083
- const def = rootFieldDefault$2(ec.ctx.bindings.get(tok.binding), ec.defaults);
7141
+ const def = rootFieldDefault(ec.ctx.bindings.get(tok.binding), ec.defaults);
7084
7142
  let expr = def !== void 0 && !ec.iter.has(tok.binding) ? bindingAccess$1(tok.binding, ec, false, def) : bindingAccess$1(tok.binding, ec);
7085
7143
  if (tok.fallback !== void 0) expr = `(${expr} if ${expr} is not None else ${pyStr(tok.fallback)})`;
7086
7144
  if (tok.stripExtensions && tok.stripExtensions.length > 0) {
@@ -7143,7 +7201,7 @@ function emitBuildOutputs$1(ctx, paramsType, outputsType, funcName, cb) {
7143
7201
  ctx,
7144
7202
  iter: /* @__PURE__ */ new Map(),
7145
7203
  subst: /* @__PURE__ */ new Map(),
7146
- defaults: collectDefaults$2(ctx)
7204
+ defaults: collectDefaults(ctx, renderPyLiteral)
7147
7205
  };
7148
7206
  const fields = collectOutputFields(ctx, pyId);
7149
7207
  const localVarOf = /* @__PURE__ */ new Map();
@@ -7180,11 +7238,16 @@ function emitBuildOutputs$1(ctx, paramsType, outputsType, funcName, cb) {
7180
7238
  }
7181
7239
  });
7182
7240
  }
7183
- /** Sanitize an output name to a valid Python identifier. */
7241
+ /**
7242
+ * Sanitize an output name to a valid Python identifier. Uses a *letter*-leading
7243
+ * prefix (`v_`) for digit-leading / empty names, never a leading underscore:
7244
+ * the Outputs type is a `typing.NamedTuple`, which raises `ValueError` at import
7245
+ * time for a field whose name starts with `_`. Matches styx1 and `pyScrubIdent`.
7246
+ * (A trailing underscore for keywords is fine - only leading underscores fail.)
7247
+ */
7184
7248
  function pyId(name) {
7185
7249
  let s = name.replace(/[^a-zA-Z0-9_]/g, "_");
7186
- if (/^\d/.test(s)) s = "_" + s;
7187
- if (s === "") s = "_";
7250
+ if (/^\d/.test(s) || s === "") s = "v_" + s;
7188
7251
  if (PY_KEYWORDS.has(s)) s = s + "_";
7189
7252
  return s;
7190
7253
  }
@@ -7370,13 +7433,22 @@ function buildEmitModel(ctx, scope = new Scope(PY_RESERVED)) {
7370
7433
  };
7371
7434
  }
7372
7435
  function generatePython(ctx, packageScope) {
7436
+ return generatePythonModule(ctx, packageScope).code;
7437
+ }
7438
+ /**
7439
+ * Emit the module and, alongside it, the dispatch entrypoint carrying the
7440
+ * *scope-registered* execute-function name. Computing the entrypoint here (not
7441
+ * via the scope-blind `appEntrypoint`) keeps the suite dispatcher in sync with
7442
+ * the actual emitted symbol when a shared package scope suffix-bumps a collision.
7443
+ */
7444
+ function generatePythonModule(ctx, packageScope) {
7373
7445
  const cb = new CodeBuilder(" ");
7374
7446
  const scope = packageScope ?? new Scope(PY_RESERVED);
7375
7447
  const { names, rootType, rootIsStruct, namedTypes, typeDecls, rootTypeTag, paramsType, sigEntries, nestedFactories } = buildEmitModel(ctx, scope);
7376
7448
  cb.comment("This file was auto generated by Styx.", "# ");
7377
7449
  cb.comment("Do not edit this file directly.", "# ");
7378
7450
  cb.blank();
7379
- emitImports$1(cb, true);
7451
+ emitImports$1(cb);
7380
7452
  cb.blank();
7381
7453
  emitMetadata$1(ctx, names.metadata, cb);
7382
7454
  cb.blank();
@@ -7401,7 +7473,8 @@ function generatePython(ctx, packageScope) {
7401
7473
  cb.blank();
7402
7474
  emitBuildOutputs$1(ctx, paramsType, names.outputs, names.outputsFn, cb);
7403
7475
  cb.blank();
7404
- emitWrapperFunction$1(ctx, paramsType, rootIsStruct ? names.execute : names.wrapper, names.metadata, names.cargs, names.outputsFn, names.outputs, names.validate, streamFieldIds$1(ctx), cb);
7476
+ const executeName = rootIsStruct ? names.execute : names.wrapper;
7477
+ emitWrapperFunction$1(ctx, paramsType, executeName, names.metadata, names.cargs, names.outputsFn, names.outputs, names.validate, streamFieldIds$1(ctx), cb);
7405
7478
  cb.blank();
7406
7479
  if (rootIsStruct) {
7407
7480
  emitKwargWrapper$1(ctx, sigEntries, names.wrapper, names.paramsFn, names.execute, names.outputs, cb);
@@ -7409,10 +7482,10 @@ function generatePython(ctx, packageScope) {
7409
7482
  }
7410
7483
  const publicSymbols = [
7411
7484
  names.params,
7412
- ...[names.outputs],
7485
+ names.outputs,
7413
7486
  names.metadata,
7414
7487
  names.cargs,
7415
- ...[names.outputsFn],
7488
+ names.outputsFn,
7416
7489
  ...rootIsStruct ? [names.paramsFn, names.execute] : [],
7417
7490
  ...nestedFactories.map((nf) => nf.funcName),
7418
7491
  names.validate,
@@ -7423,7 +7496,16 @@ function generatePython(ctx, packageScope) {
7423
7496
  for (const sym of publicSymbols) cb.line(`"${sym}",`);
7424
7497
  });
7425
7498
  cb.line("]");
7426
- return cb.toString();
7499
+ const appId = ctx.app?.id;
7500
+ const pkg = ctx.package?.name;
7501
+ const entrypoint = appId && pkg ? {
7502
+ type: `${pkg}/${appId}`,
7503
+ executeFn: executeName
7504
+ } : void 0;
7505
+ return {
7506
+ code: cb.toString(),
7507
+ entrypoint
7508
+ };
7427
7509
  }
7428
7510
  /**
7429
7511
  * Module name (file stem) for an app: snake_case of app.id, fallback `output`.
@@ -7435,22 +7517,6 @@ function appModuleName$1(meta) {
7435
7517
  return pyScrubIdent(snakeCase(meta.id), PY_RESERVED);
7436
7518
  }
7437
7519
  /**
7438
- * The dispatch entrypoint for one app: its root `@type` (`<package>/<app>`) and
7439
- * the dict-style execute function name. Returns undefined when the id or package
7440
- * is unknown (no stable `@type`), so the app is left out of the suite dispatcher.
7441
- */
7442
- function appEntrypoint$1(ctx) {
7443
- const appId = ctx.app?.id;
7444
- const pkg = ctx.package?.name;
7445
- if (!appId || !pkg) return void 0;
7446
- const publicNames = computePublicNames$1(appId);
7447
- const executeFn = pyScrubIdent(ctx.resolve(ctx.expr)?.type.kind === "struct" ? publicNames.execute : publicNames.wrapper, PY_RESERVED);
7448
- return {
7449
- type: `${pkg}/${appId}`,
7450
- executeFn
7451
- };
7452
- }
7453
- /**
7454
7520
  * Generate the suite-level `__init__.py` re-export for a package containing
7455
7521
  * multiple tool modules. Each tool module's public symbols are surfaced via
7456
7522
  * `from .bet import *` (each tool file defines `__all__`). When apps carry a
@@ -7487,10 +7553,11 @@ function emitPackageDispatch$1(cb, dispatch) {
7487
7553
  for (const e of dispatch) cb.line(`${JSON.stringify(e.type)}: ${e.executeFn},`);
7488
7554
  });
7489
7555
  cb.line("}");
7490
- cb.line("_fn = _dispatch.get(params[\"@type\"])");
7556
+ cb.line("_type = params.get(\"@type\")");
7557
+ cb.line("_fn = _dispatch.get(_type) if _type is not None else None");
7491
7558
  cb.line("if _fn is None:");
7492
7559
  cb.indent(() => {
7493
- cb.line(`raise ValueError(f"No tool registered for @type {params['@type']!r}")`);
7560
+ cb.line(`raise ValueError(f"No tool registered for @type {_type!r}")`);
7494
7561
  });
7495
7562
  cb.line("return _fn(params, runner)");
7496
7563
  });
@@ -7499,11 +7566,11 @@ var PythonBackend = class {
7499
7566
  name = "python";
7500
7567
  target = "python";
7501
7568
  emitApp(ctx, scope) {
7502
- const code = generatePython(ctx, scope);
7569
+ const { code, entrypoint } = generatePythonModule(ctx, scope);
7503
7570
  const fileName = `${appModuleName$1(ctx.app)}.py`;
7504
7571
  return {
7505
7572
  meta: ctx.app,
7506
- entrypoint: appEntrypoint$1(ctx),
7573
+ entrypoint,
7507
7574
  files: new Map([[fileName, code]]),
7508
7575
  errors: [],
7509
7576
  warnings: []
@@ -8497,6 +8564,10 @@ var JsonSchemaBackend = class {
8497
8564
 
8498
8565
  //#endregion
8499
8566
  //#region src/backend/typescript/typemap.ts
8567
+ const TS_IDENT_RE$1 = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
8568
+ function objKey(key) {
8569
+ return TS_IDENT_RE$1.test(key) ? key : JSON.stringify(key);
8570
+ }
8500
8571
  function mapType(type, resolve) {
8501
8572
  switch (type.kind) {
8502
8573
  case "scalar": return {
@@ -8516,7 +8587,7 @@ function mapType(type, resolve) {
8516
8587
  case "struct": {
8517
8588
  const name = resolve(type);
8518
8589
  if (name) return name;
8519
- return `{ ${Object.entries(type.fields).filter(([, v]) => v.kind !== "literal").map(([k, v]) => `${k}: ${mapType(v, resolve)}`).join("; ")} }`;
8590
+ return `{ ${Object.entries(type.fields).filter(([, v]) => v.kind !== "literal").map(([k, v]) => `${objKey(k)}: ${mapType(v, resolve)}`).join("; ")} }`;
8520
8591
  }
8521
8592
  case "union": {
8522
8593
  const name = resolve(type);
@@ -8678,10 +8749,6 @@ function accessOf(binding, arg) {
8678
8749
  * carrying a Boutiques default. Restricted to single-segment (root) field access
8679
8750
  * so a nested field can never accidentally pick up a same-named root default.
8680
8751
  */
8681
- function rootFieldDefault$1(binding, defaults) {
8682
- const a = binding.access;
8683
- if (a.length === 1 && a[0]?.kind === "field") return defaults.get(binding.name);
8684
- }
8685
8752
  /**
8686
8753
  * Render a binding's access for an UNCONDITIONAL value read (terminal, repeat
8687
8754
  * loop, alternative dispatch): substitutes the field's default via
@@ -8690,22 +8757,9 @@ function rootFieldDefault$1(binding, defaults) {
8690
8757
  * emitted code is byte-identical to before for the common case.
8691
8758
  */
8692
8759
  function readAccess(binding, arg) {
8693
- const def = rootFieldDefault$1(binding, arg.defaults);
8760
+ const def = rootFieldDefault(binding, arg.defaults);
8694
8761
  return def !== void 0 ? `(${accessOf(binding, arg)} ?? ${def})` : accessOf(binding, arg);
8695
8762
  }
8696
- /** Build the field-name -> rendered-default map for a struct root (else empty).
8697
- * Includes only non-optional defaulted fields (optional fields are
8698
- * presence-guarded; their default comes from the factory's kwarg signature). */
8699
- function collectDefaults$1(ctx, rootType) {
8700
- const out = /* @__PURE__ */ new Map();
8701
- if (rootType?.kind !== "struct") return out;
8702
- for (const [name, fi] of collectFieldInfo(ctx, rootType)) {
8703
- if (fi.defaultValue === void 0) continue;
8704
- if (rootType.fields[name]?.kind === "optional") continue;
8705
- out.set(name, renderTsLiteral(fi.defaultValue));
8706
- }
8707
- return out;
8708
- }
8709
8763
  let loopVarCounter = 0;
8710
8764
  /**
8711
8765
  * Build arg-building code for an IR tree via recursive descent.
@@ -8718,7 +8772,7 @@ function buildArgs(rootExpr, ctx, rootType) {
8718
8772
  return walk$1(rootExpr, ctx, {
8719
8773
  joinDepth: 0,
8720
8774
  loopVars: /* @__PURE__ */ new Map(),
8721
- defaults: collectDefaults$1(ctx, rootType)
8775
+ defaults: collectDefaults(ctx, renderTsLiteral, rootType)
8722
8776
  });
8723
8777
  }
8724
8778
  function walk$1(node, ctx, arg) {
@@ -8873,7 +8927,7 @@ function walkAlternative(node, ctx, arg) {
8873
8927
  //#region src/backend/typescript/emit.ts
8874
8928
  function emitJsDoc(cb, description) {
8875
8929
  if (!description) return;
8876
- const lines = description.split("\n");
8930
+ const lines = description.replace(/\*\//g, "*\\/").split("\n");
8877
8931
  if (lines.length === 1) cb.line(`/** ${lines[0]} */`);
8878
8932
  else {
8879
8933
  cb.line("/**");
@@ -8881,15 +8935,14 @@ function emitJsDoc(cb, description) {
8881
8935
  cb.line(" */");
8882
8936
  }
8883
8937
  }
8884
- function emitImports(cb, emitOutputs) {
8885
- const inputs = [
8938
+ function emitImports(cb) {
8939
+ cb.line(`import type { ${[
8886
8940
  "Runner",
8887
8941
  "Execution",
8888
8942
  "Metadata",
8889
- "InputPathType"
8890
- ];
8891
- if (emitOutputs) inputs.push("OutputPathType");
8892
- cb.line(`import type { ${inputs.join(", ")} } from "styxdefs";`);
8943
+ "InputPathType",
8944
+ "OutputPathType"
8945
+ ].join(", ")} } from "styxdefs";`);
8893
8946
  cb.line("import { getGlobalRunner, StyxValidationError } from \"styxdefs\";");
8894
8947
  }
8895
8948
  function emitMetadata(ctx, metaConst, cb) {
@@ -9233,7 +9286,7 @@ function emitValue(e, type, node, wireKey, access, expected) {
9233
9286
  const itemNode = findRepeatNode(node)?.attrs.node;
9234
9287
  const elem = e.scope.add("el");
9235
9288
  e.cb.line(`for (const ${elem} of ${access}) {`);
9236
- e.cb.indent(() => emitValue(e, type.item, itemNode, wireKey, elem, expected));
9289
+ e.cb.indent(() => emitValue(e, type.item, itemNode, wireKey, elem, expectedType(e, type.item)));
9237
9290
  e.cb.line("}");
9238
9291
  return;
9239
9292
  }
@@ -9368,26 +9421,6 @@ function emitOutputsInterface(ctx, outputsType, cb) {
9368
9421
  });
9369
9422
  cb.line(`}`);
9370
9423
  }
9371
- /** The rendered default for a binding iff it is a root-level defaulted field. */
9372
- function rootFieldDefault(binding, defaults) {
9373
- if (!binding) return void 0;
9374
- const a = binding.access;
9375
- if (a.length === 1 && a[0]?.kind === "field") return defaults.get(binding.name);
9376
- }
9377
- /** Build the field-name -> rendered-default map for the struct root (else empty).
9378
- * Includes only non-optional defaulted fields (optional fields are
9379
- * presence-guarded; their default comes from the factory's kwarg signature). */
9380
- function collectDefaults(ctx) {
9381
- const out = /* @__PURE__ */ new Map();
9382
- const rootType = ctx.resolve(ctx.expr)?.type;
9383
- if (rootType?.kind !== "struct") return out;
9384
- for (const [name, fi] of collectFieldInfo(ctx, rootType)) {
9385
- if (fi.defaultValue === void 0) continue;
9386
- if (rootType.fields[name]?.kind === "optional") continue;
9387
- out.set(name, renderTsLiteral(fi.defaultValue));
9388
- }
9389
- return out;
9390
- }
9391
9424
  /**
9392
9425
  * Render one output's wrapper stack and emit the assignment inside the
9393
9426
  * innermost wrapper. Nesting is done via recursive callbacks so the
@@ -9431,10 +9464,15 @@ function renderWrapperOpen(atom, ec) {
9431
9464
  loopVar: v
9432
9465
  };
9433
9466
  }
9434
- if (atom.kind === "variant") return {
9435
- open: `if (${bindingAccess(atom.binding, ec)}["@type"] === ${JSON.stringify(atom.variant)}) {`,
9436
- close: `}`
9437
- };
9467
+ if (atom.kind === "variant") {
9468
+ const access = bindingAccess(atom.binding, ec);
9469
+ const check = `${access}["@type"] === ${JSON.stringify(atom.variant)}`;
9470
+ const union = variantAtomUnion(atom, ec.ctx.bindings);
9471
+ return {
9472
+ open: `if (${union && unionIsMixed(union) ? `typeof ${access} === "object" && ${access} !== null && ${check}` : check}) {`,
9473
+ close: `}`
9474
+ };
9475
+ }
9438
9476
  const binding = ec.ctx.bindings.get(atom.binding);
9439
9477
  const access = bindingAccess(atom.binding, ec);
9440
9478
  return {
@@ -9506,7 +9544,7 @@ function emitBuildOutputs(ctx, paramsType, outputsType, funcName, cb) {
9506
9544
  ctx,
9507
9545
  iter: /* @__PURE__ */ new Map(),
9508
9546
  fieldShapes: new Map(fields.map((f) => [f.id, f.shape])),
9509
- defaults: collectDefaults(ctx)
9547
+ defaults: collectDefaults(ctx, renderTsLiteral)
9510
9548
  };
9511
9549
  cb.line(`const outputs: ${outputsType} = {`);
9512
9550
  cb.indent(() => {
@@ -9676,13 +9714,22 @@ function buildEmitModel$1(ctx, scope = new Scope(TS_RESERVED)) {
9676
9714
  };
9677
9715
  }
9678
9716
  function generateTypeScript(ctx, packageScope) {
9717
+ return generateTypeScriptModule(ctx, packageScope).code;
9718
+ }
9719
+ /**
9720
+ * Emit the module and, alongside it, the dispatch entrypoint carrying the
9721
+ * *scope-registered* execute-function name. Computing the entrypoint here (not
9722
+ * via the scope-blind `appEntrypoint`) keeps the suite dispatcher in sync with
9723
+ * the actual emitted symbol when a shared package scope suffix-bumps a collision.
9724
+ */
9725
+ function generateTypeScriptModule(ctx, packageScope) {
9679
9726
  const cb = new CodeBuilder(" ");
9680
9727
  const scope = packageScope ?? new Scope(TS_RESERVED);
9681
9728
  const { appId, pkg, names, rootType, rootIsStruct, namedTypes, typeDecls, rootTypeTag, paramsType, sigEntries } = buildEmitModel$1(ctx, scope);
9682
9729
  cb.comment("This file was auto generated by Styx.");
9683
9730
  cb.comment("Do not edit this file directly.");
9684
9731
  cb.blank();
9685
- emitImports(cb, true);
9732
+ emitImports(cb);
9686
9733
  cb.blank();
9687
9734
  emitMetadata(ctx, names.metadata, cb);
9688
9735
  cb.blank();
@@ -9703,13 +9750,23 @@ function generateTypeScript(ctx, packageScope) {
9703
9750
  cb.blank();
9704
9751
  emitBuildOutputs(ctx, paramsType, names.outputs, names.outputsFn, cb);
9705
9752
  cb.blank();
9706
- emitWrapperFunction(ctx, paramsType, rootIsStruct ? names.execute : names.wrapper, names.metadata, names.cargs, names.outputsFn, names.outputs, names.validate, streamFieldIds(ctx), cb);
9753
+ const executeName = rootIsStruct ? names.execute : names.wrapper;
9754
+ emitWrapperFunction(ctx, paramsType, executeName, names.metadata, names.cargs, names.outputsFn, names.outputs, names.validate, streamFieldIds(ctx), cb);
9707
9755
  cb.blank();
9708
9756
  if (rootIsStruct) {
9709
9757
  emitKwargWrapper(ctx, sigEntries, names.wrapper, names.paramsFn, names.execute, names.outputs, cb);
9710
9758
  cb.blank();
9711
9759
  }
9712
- return cb.toString();
9760
+ const entryAppId = ctx.app?.id;
9761
+ const entryPkg = ctx.package?.name;
9762
+ const entrypoint = entryAppId && entryPkg ? {
9763
+ type: `${entryPkg}/${entryAppId}`,
9764
+ executeFn: executeName
9765
+ } : void 0;
9766
+ return {
9767
+ code: cb.toString(),
9768
+ entrypoint
9769
+ };
9713
9770
  }
9714
9771
  /**
9715
9772
  * Module name (file stem) for an app: snake_case of app.id, fallback `output`.
@@ -9791,11 +9848,11 @@ var TypeScriptBackend = class {
9791
9848
  name = "typescript";
9792
9849
  target = "typescript";
9793
9850
  emitApp(ctx, scope) {
9794
- const code = generateTypeScript(ctx, scope);
9851
+ const { code, entrypoint } = generateTypeScriptModule(ctx, scope);
9795
9852
  const fileName = `${appModuleName(ctx.app)}.ts`;
9796
9853
  return {
9797
9854
  meta: ctx.app,
9798
- entrypoint: appEntrypoint(ctx),
9855
+ entrypoint,
9799
9856
  files: new Map([[fileName, code]]),
9800
9857
  errors: [],
9801
9858
  warnings: []
@@ -10061,6 +10118,15 @@ const canonicalize = {
10061
10118
  default: return "";
10062
10119
  }
10063
10120
  }
10121
+ function identityKey(node) {
10122
+ const m = node.meta;
10123
+ const metaKey = m ? [
10124
+ m.name ?? "",
10125
+ m.variantTag ?? "",
10126
+ m.outputs ? JSON.stringify(m.outputs) : ""
10127
+ ].join("|") : "";
10128
+ return `${structuralHash(node)}#${metaKey}`;
10129
+ }
10064
10130
  function sortKey(node) {
10065
10131
  const name = node.meta?.name ?? "";
10066
10132
  return `${node.kind}:${name}:${structuralHash(node)}`;
@@ -10073,13 +10139,13 @@ const canonicalize = {
10073
10139
  const seen = /* @__PURE__ */ new Set();
10074
10140
  const alts = [];
10075
10141
  for (const child of sorted) {
10076
- const hash = structuralHash(child);
10077
- if (!seen.has(hash)) {
10078
- seen.add(hash);
10142
+ const key = identityKey(child);
10143
+ if (!seen.has(key)) {
10144
+ seen.add(key);
10079
10145
  alts.push(child);
10080
10146
  } else changed = true;
10081
10147
  }
10082
- if (alts.length !== children.length || alts.some((alt, i) => structuralHash(alt) !== structuralHash(children[i]))) changed = true;
10148
+ if (alts.length !== children.length || alts.some((alt, i) => identityKey(alt) !== identityKey(children[i]))) changed = true;
10083
10149
  return {
10084
10150
  ...node,
10085
10151
  attrs: {
@@ -10362,11 +10428,24 @@ const simplify = {
10362
10428
  const prev = nodes[nodes.length - 1];
10363
10429
  if (prev?.kind === "literal" && child.kind === "literal" && !prev.meta && !child.meta && node.attrs.join === "") {
10364
10430
  changed = true;
10365
- prev.attrs.str += child.attrs.str;
10431
+ nodes[nodes.length - 1] = {
10432
+ ...prev,
10433
+ attrs: {
10434
+ ...prev.attrs,
10435
+ str: prev.attrs.str + child.attrs.str
10436
+ }
10437
+ };
10366
10438
  } else nodes.push(child);
10367
10439
  }
10368
10440
  if (nodes.length === 1) {
10369
10441
  const child = nodes[0];
10442
+ if (node.meta?.outputs?.length && child.kind === "literal") return {
10443
+ ...node,
10444
+ attrs: {
10445
+ ...node.attrs,
10446
+ nodes
10447
+ }
10448
+ };
10370
10449
  changed = true;
10371
10450
  const mergedMeta = mergeMeta(node.meta, child.meta);
10372
10451
  return mergedMeta ? {
@@ -10784,7 +10863,7 @@ function defaultNamingStrategy() {
10784
10863
  function isBooleanLiteralPair(variants) {
10785
10864
  if (variants.length !== 2 || !variants.every((v) => v.type.kind === "literal")) return false;
10786
10865
  const [a, b] = variants.map((v) => v.type.kind === "literal" ? v.type.value : null);
10787
- return a === 0 && b === 1 || a === 1 && b === 0 || a === "0" && b === "1" || a === "1" && b === "0" || a === "false" && b === "true" || a === "true" && b === "false";
10866
+ return a === 0 && b === 1 || a === 1 && b === 0 || a === "false" && b === "true" || a === "true" && b === "false";
10788
10867
  }
10789
10868
  function literalFromNode(node) {
10790
10869
  const str = node.attrs.str;
@@ -11055,7 +11134,6 @@ exports.camelCase = camelCase;
11055
11134
  exports.canonicalize = canonicalize;
11056
11135
  exports.collectFieldInfo = collectFieldInfo;
11057
11136
  exports.collectNamedTypes = collectNamedTypes;
11058
- exports.compactTokens = compactTokens;
11059
11137
  exports.compile = compile;
11060
11138
  exports.compose = compose;
11061
11139
  exports.createContext = createContext;
@@ -11081,8 +11159,6 @@ exports.generatePython = generatePython;
11081
11159
  exports.generateSchema = generateSchema;
11082
11160
  exports.generateTypeScript = generateTypeScript;
11083
11161
  exports.int = int;
11084
- exports.isGated = isGated;
11085
- exports.isIterated = isIterated;
11086
11162
  exports.isStructural = isStructural;
11087
11163
  exports.isTerminal = isTerminal;
11088
11164
  exports.lit = lit;
@@ -11092,8 +11168,6 @@ exports.opt = opt;
11092
11168
  exports.outputGate = outputGate;
11093
11169
  exports.pascalCase = pascalCase;
11094
11170
  exports.path = path;
11095
- exports.planOutput = planOutput;
11096
- exports.planScope = planScope;
11097
11171
  exports.pydraNames = pydraNames;
11098
11172
  exports.removeEmpty = removeEmpty;
11099
11173
  exports.renderPythonCall = renderPythonCall;