@styx-api/core 0.5.3 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1242,12 +1242,17 @@ var BoutiquesParser = class {
1242
1242
  wrapWithFlag(node, btInput) {
1243
1243
  const flag = btInput["command-line-flag"];
1244
1244
  if (!isString$2(flag)) return node;
1245
+ const flagSep = btInput["command-line-flag-separator"];
1246
+ const fused = isString$2(flagSep) && flagSep !== " ";
1245
1247
  return {
1246
1248
  kind: "sequence",
1247
- attrs: { nodes: [{
1248
- kind: "literal",
1249
- attrs: { str: flag + (btInput["command-line-flag-separator"] ?? "") }
1250
- }, node] }
1249
+ attrs: {
1250
+ nodes: [{
1251
+ kind: "literal",
1252
+ attrs: { str: fused ? flag + flagSep : flag }
1253
+ }, node],
1254
+ ...fused && { join: "" }
1255
+ }
1251
1256
  };
1252
1257
  }
1253
1258
  wrapWithOptional(node) {
@@ -2762,7 +2767,7 @@ var BoutiquesEmitter = class {
2762
2767
  }
2763
2768
  if (peeled.flag) {
2764
2769
  input["command-line-flag"] = peeled.flag;
2765
- if (peeled.flagSeparator) input["command-line-flag-separator"] = peeled.flagSeparator;
2770
+ if (peeled.flagSeparator !== void 0) input["command-line-flag-separator"] = peeled.flagSeparator;
2766
2771
  }
2767
2772
  if (mapped.integer) input.integer = true;
2768
2773
  if (mapped.minimum !== void 0) input.minimum = mapped.minimum;
@@ -2903,7 +2908,7 @@ var BoutiquesEmitter = class {
2903
2908
  }
2904
2909
  if (peeled.flag) {
2905
2910
  input["command-line-flag"] = peeled.flag;
2906
- if (peeled.flagSeparator) input["command-line-flag-separator"] = peeled.flagSeparator;
2911
+ if (peeled.flagSeparator !== void 0) input["command-line-flag-separator"] = peeled.flagSeparator;
2907
2912
  }
2908
2913
  if (mapped.integer) input.integer = true;
2909
2914
  if (mapped.minimum !== void 0) input.minimum = mapped.minimum;
@@ -2972,9 +2977,11 @@ var BoutiquesEmitter = class {
2972
2977
  const nodes = node.attrs.nodes;
2973
2978
  if (nodes.length === 2 && nodes[0].kind === "literal") {
2974
2979
  const flagLit = nodes[0].attrs.str;
2975
- const { flag, separator } = this.splitFlagLiteral(flagLit);
2976
- result.flag = flag;
2977
- if (separator) result.flagSeparator = separator;
2980
+ if (node.attrs.join !== void 0) {
2981
+ const { flag, separator } = this.splitFlagLiteral(flagLit);
2982
+ result.flag = flag;
2983
+ result.flagSeparator = separator;
2984
+ } else result.flag = flagLit;
2978
2985
  this.peelNodeInner(nodes[1], type, result);
2979
2986
  }
2980
2987
  break;
@@ -3495,6 +3502,10 @@ function resultToStmt$1(r) {
3495
3502
  return isExpr$1(r) ? `cargs.append(${r.expr})` : r.stmt;
3496
3503
  }
3497
3504
  function appendLines$1(cb, code) {
3505
+ if (code.trim() === "") {
3506
+ cb.line("pass");
3507
+ return;
3508
+ }
3498
3509
  for (const line of code.split("\n")) cb.line(line);
3499
3510
  }
3500
3511
  function toStringExpr$1(node, type, expr) {
@@ -5400,7 +5411,29 @@ function buildEmitModel(ctx, scope = new Scope(PY_RESERVED)) {
5400
5411
  names.params = (rootType.kind === "struct" ? namedTypes.get(structKey(rootType)) : void 0) ?? (rootType.kind === "union" ? namedTypes.get(unionKey(rootType)) : void 0) ?? names.params;
5401
5412
  const rootTypeTag = appId && pkg ? `${pkg}/${appId}` : void 0;
5402
5413
  const paramsType = rootType.kind === "struct" || rootType.kind === "union" ? names.params : mapType$1(rootType, resolveTypeName(namedTypes));
5414
+ const hostName = (childScope, wireKey) => childScope.add(pyScrubIdent(snakeCase(wireKey), PY_RESERVED));
5415
+ const resolve = resolveTypeName(namedTypes);
5403
5416
  const sigScope = scope.child(["params", "runner"]);
5417
+ const sigEntries = rootIsStruct && rootType.kind === "struct" ? buildSigEntries(rootType, collectFieldInfo(ctx, rootType), (wireKey) => hostName(sigScope, wireKey), pySigOptions(resolve)) : [];
5418
+ const rootStructKey = rootIsStruct && rootType.kind === "struct" ? structKey(rootType) : void 0;
5419
+ const nestedFactories = [];
5420
+ for (const decl of typeDecls) {
5421
+ if (decl.type.kind !== "struct") continue;
5422
+ const sKey = structKey(decl.type);
5423
+ if (sKey === rootStructKey) continue;
5424
+ const funcName = scope.add(pyScrubIdent(snakeCase(decl.name) + "_params", PY_RESERVED));
5425
+ const atType = decl.type.fields["@type"];
5426
+ const typeTag = atType && atType.kind === "literal" && typeof atType.value === "string" ? atType.value : void 0;
5427
+ const factoryScope = scope.child(["params"]);
5428
+ const factorySig = buildSigEntries(decl.type, collectFieldInfo(ctx, decl.type), (wireKey) => hostName(factoryScope, wireKey), pySigOptions(resolve));
5429
+ nestedFactories.push({
5430
+ typeName: decl.name,
5431
+ structKey: sKey,
5432
+ funcName,
5433
+ typeTag,
5434
+ sigEntries: factorySig
5435
+ });
5436
+ }
5404
5437
  return {
5405
5438
  appId,
5406
5439
  pkg,
@@ -5411,13 +5444,14 @@ function buildEmitModel(ctx, scope = new Scope(PY_RESERVED)) {
5411
5444
  typeDecls,
5412
5445
  rootTypeTag,
5413
5446
  paramsType,
5414
- sigEntries: rootIsStruct && rootType.kind === "struct" ? buildSigEntries(rootType, collectFieldInfo(ctx, rootType), (wireKey) => sigScope.add(pyScrubIdent(wireKey, PY_RESERVED)), pySigOptions(resolveTypeName(namedTypes))) : []
5447
+ sigEntries,
5448
+ nestedFactories
5415
5449
  };
5416
5450
  }
5417
5451
  function generatePython(ctx, packageScope) {
5418
5452
  const cb = new CodeBuilder(" ");
5419
5453
  const scope = packageScope ?? new Scope(PY_RESERVED);
5420
- const { names, rootType, rootIsStruct, namedTypes, typeDecls, rootTypeTag, paramsType, sigEntries } = buildEmitModel(ctx, scope);
5454
+ const { names, rootType, rootIsStruct, namedTypes, typeDecls, rootTypeTag, paramsType, sigEntries, nestedFactories } = buildEmitModel(ctx, scope);
5421
5455
  cb.comment("This file was auto generated by Styx.", "# ");
5422
5456
  cb.comment("Do not edit this file directly.", "# ");
5423
5457
  cb.blank();
@@ -5436,6 +5470,10 @@ function generatePython(ctx, packageScope) {
5436
5470
  emitParamsFactory$1(sigEntries, names.paramsFn, paramsType, rootTypeTag, cb);
5437
5471
  cb.blank();
5438
5472
  }
5473
+ for (const nf of nestedFactories) {
5474
+ emitParamsFactory$1(nf.sigEntries, nf.funcName, nf.typeName, nf.typeTag, cb);
5475
+ cb.blank();
5476
+ }
5439
5477
  emitValidate$1(ctx, rootType, ctx.expr, paramsType, names.validate, resolveTypeName(namedTypes), scope, cb);
5440
5478
  cb.blank();
5441
5479
  emitBuildCargs$1(ctx, rootType, paramsType, names.cargs, cb);
@@ -5455,6 +5493,7 @@ function generatePython(ctx, packageScope) {
5455
5493
  names.cargs,
5456
5494
  ...[names.outputsFn],
5457
5495
  ...rootIsStruct ? [names.paramsFn, names.execute] : [],
5496
+ ...nestedFactories.map((nf) => nf.funcName),
5458
5497
  names.validate,
5459
5498
  names.wrapper
5460
5499
  ];
@@ -5622,9 +5661,13 @@ function structAtType(type) {
5622
5661
  }
5623
5662
  /**
5624
5663
  * Render a struct config as a host object literal (Python dict / TS object).
5625
- * Keys are the Boutiques wire names (the generated TypedDict / interface keys) -
5626
- * nested structs have no constructor function in the generated code, so callers
5627
- * build them as plain object literals.
5664
+ * Keys are the Boutiques wire names (the generated TypedDict / interface keys).
5665
+ *
5666
+ * When the dialect supplies `structConstructor` (Python, which generates a
5667
+ * per-struct `_params` builder), the struct is rendered as that factory call
5668
+ * instead - e.g. `tool_corrected_output_params(file="...")`. Backends without
5669
+ * per-struct builders (TypeScript) leave the hook unset and build the plain
5670
+ * object literal below.
5628
5671
  *
5629
5672
  * `@type` is emitted from the struct's literal discriminator field when present
5630
5673
  * (union variants carry a required, load-bearing `@type`); for the root call the
@@ -5634,6 +5677,10 @@ function structAtType(type) {
5634
5677
  */
5635
5678
  function renderStructLiteral(value, type, indent, d, injectAtType) {
5636
5679
  const obj = isRecord(value) ? value : {};
5680
+ if (injectAtType === void 0 && d.structConstructor) {
5681
+ const call = d.structConstructor(type, obj, indent, d);
5682
+ if (call !== void 0) return call;
5683
+ }
5637
5684
  const inner = indent + d.indentUnit;
5638
5685
  const lines = [];
5639
5686
  const atType = structAtType(type) ?? injectAtType;
@@ -5709,6 +5756,41 @@ const pyDialect = {
5709
5756
  objKey: (k) => pyStr(k)
5710
5757
  };
5711
5758
  /**
5759
+ * Build a Python dialect that renders nested structs as `_params` factory calls
5760
+ * (matching the generated code's per-struct builders) rather than plain dicts.
5761
+ * Each factory is looked up by the struct's structural key; field values use the
5762
+ * factory's scrubbed host kwarg names (from the same `buildSigEntries` the
5763
+ * generated factory is built from), and the function name is package-qualified
5764
+ * so the call resolves through `from <root> import <pkg>`. A struct with no
5765
+ * registered factory (shouldn't happen for well-formed nested types) falls back
5766
+ * to the plain dict literal.
5767
+ */
5768
+ function pyDialectWithFactories(model, pkg) {
5769
+ const byKey = /* @__PURE__ */ new Map();
5770
+ for (const f of model.nestedFactories) byKey.set(f.structKey, {
5771
+ call: pkg ? `${pkg}.${f.funcName}` : f.funcName,
5772
+ nameByWire: new Map(f.sigEntries.map((e) => [e.wireKey, e.name]))
5773
+ });
5774
+ return {
5775
+ ...pyDialect,
5776
+ structConstructor(type, value, indent, d) {
5777
+ const entry = byKey.get(structKey(type));
5778
+ if (!entry) return void 0;
5779
+ const inner = indent + d.indentUnit;
5780
+ const lines = [];
5781
+ for (const [wireKey, fieldType] of Object.entries(type.fields)) {
5782
+ if (wireKey === "@type") continue;
5783
+ if (fieldType.kind === "literal") continue;
5784
+ if (!(wireKey in value)) continue;
5785
+ const name = entry.nameByWire.get(wireKey) ?? wireKey;
5786
+ lines.push(`${inner}${name}=${renderValue(value[wireKey], fieldType, inner, d)},`);
5787
+ }
5788
+ if (lines.length === 0) return `${entry.call}()`;
5789
+ return `${entry.call}(\n${lines.join("\n")}\n${indent})`;
5790
+ }
5791
+ };
5792
+ }
5793
+ /**
5712
5794
  * Render a Python call snippet for one tool from a config object (the params
5713
5795
  * dict the form produces, keyed by Boutiques wire names).
5714
5796
  *
@@ -5717,8 +5799,9 @@ const pyDialect = {
5717
5799
  * *scrubbed host* identifiers (`float` -> `float_`), not the wire keys; the
5718
5800
  * per-field mapping comes from the same `buildSigEntries` the generated wrapper
5719
5801
  * is built from, so the snippet matches the real signature. Nested structs /
5720
- * union variants / lists-of-structs have no constructor in the generated code,
5721
- * so they render as plain dict literals keyed by wire names.
5802
+ * union variants / lists-of-structs render as calls to their generated
5803
+ * `_params` factory (`fsl.bet_x_params(...)`), matching the per-struct builders
5804
+ * the Python backend emits.
5722
5805
  *
5723
5806
  * Union- (or otherwise non-struct-) rooted tools have no kwarg wrapper; the
5724
5807
  * single dict-style `<tool>` entry is called with one object-literal argument.
@@ -5739,21 +5822,22 @@ function renderPythonCall(ctx, config, opts = {}) {
5739
5822
  const model = buildEmitModel(ctx);
5740
5823
  const pkg = ctx.package?.name;
5741
5824
  const callee = pkg ? `${pkg}.${model.names.wrapper}` : model.names.wrapper;
5742
- const call = model.rootIsStruct && model.rootType.kind === "struct" ? renderKwargCall(callee, model.sigEntries, model.rootType, config) : `${callee}(${renderValue(config, model.rootType, "", pyDialect)})`;
5825
+ const dialect = pyDialectWithFactories(model, pkg);
5826
+ const call = model.rootIsStruct && model.rootType.kind === "struct" ? renderKwargCall(callee, model.sigEntries, model.rootType, config, dialect) : `${callee}(${renderValue(config, model.rootType, "", dialect)})`;
5743
5827
  if (opts.includeImport === false || !pkg) return call;
5744
5828
  const root = opts.packageRoot ?? ctx.project?.name;
5745
5829
  return `${root ? `from ${root} import ${pkg}` : `import ${pkg}`}\n\n${call}`;
5746
5830
  }
5747
5831
  /** Render `callee(name=value, ...)` for a struct root using scrubbed kwarg names. */
5748
- function renderKwargCall(callee, sigEntries, rootType, config) {
5832
+ function renderKwargCall(callee, sigEntries, rootType, config, dialect) {
5749
5833
  const nameFor = new Map(sigEntries.map((e) => [e.wireKey, e.name]));
5750
- const indent = pyDialect.indentUnit;
5834
+ const indent = dialect.indentUnit;
5751
5835
  const lines = [];
5752
5836
  for (const [wireKey, fieldType] of Object.entries(rootType.fields)) {
5753
5837
  if (fieldType.kind === "literal") continue;
5754
5838
  if (!(wireKey in config)) continue;
5755
5839
  const name = nameFor.get(wireKey) ?? wireKey;
5756
- lines.push(`${indent}${name}=${renderValue(config[wireKey], fieldType, indent, pyDialect)},`);
5840
+ lines.push(`${indent}${name}=${renderValue(config[wireKey], fieldType, indent, dialect)},`);
5757
5841
  }
5758
5842
  if (lines.length === 0) return `${callee}()`;
5759
5843
  return `${callee}(\n${lines.join("\n")}\n)`;