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