@styx-api/core 0.4.0 → 0.5.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
@@ -3387,196 +3387,6 @@ function resolveTypeName(namedTypes) {
3387
3387
  };
3388
3388
  }
3389
3389
 
3390
- //#endregion
3391
- //#region src/backend/styxdefs-compat.ts
3392
- /**
3393
- * Runtime version floors baked into generated dependency metadata.
3394
- *
3395
- * styx2-generated code calls `mutable_copy` / `mutableCopy` (introduced in the
3396
- * styxdefs 0.7.0 / styxdefs-js 0.2.0 release), so emitted packages genuinely
3397
- * require that runtime floor. This is the single source of truth: bump here and
3398
- * both the Python and TypeScript backends pick it up.
3399
- */
3400
- const STYXDEFS_COMPAT = {
3401
- python: ">=0.7.0,<0.8.0",
3402
- npm: "^0.2.0"
3403
- };
3404
- /**
3405
- * Extra Python runtime packages the root distribution pulls in (container +
3406
- * graph runners). Left unpinned - styxdefs's floor constrains them transitively
3407
- * via their own inter-package pins.
3408
- */
3409
- const PYTHON_RUNNER_DEPS = [
3410
- "styxdocker",
3411
- "styxsingularity",
3412
- "styxgraph"
3413
- ];
3414
-
3415
- //#endregion
3416
- //#region src/backend/python/packaging.ts
3417
- const REQUIRES_PYTHON = ">=3.10";
3418
- const BUILD_SYSTEM = `[build-system]
3419
- requires = ["setuptools>=61"]
3420
- build-backend = "setuptools.build_meta"`;
3421
- /** Escape a value for embedding in a TOML basic string. */
3422
- function tomlStr(s) {
3423
- return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/[\r\n]+/g, " ").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "").trim();
3424
- }
3425
- /** Suite directory / importable package name; matches the CLI's `pkgDir` fallback. */
3426
- function pkgDir(pkg) {
3427
- return pkg.name ?? "package";
3428
- }
3429
- /** Distribution (PyPI) name for a package: `<project>_<package>`, or just `<package>`. */
3430
- function pyDistName(proj, pkg) {
3431
- const name = pkgDir(pkg);
3432
- return proj.name ? `${proj.name}_${name}` : name;
3433
- }
3434
- const SUMMARY_MAX_LEN = 512;
3435
- /**
3436
- * Clamp a description to a <=512-char one-line summary. Prefer cutting at the
3437
- * last complete sentence that fits (clean, no dangling fragment); if there is no
3438
- * sentence boundary early enough, cut at a word boundary and mark the elision.
3439
- */
3440
- function clampSummary(s) {
3441
- if (s.length <= SUMMARY_MAX_LEN) return s;
3442
- const window = s.slice(0, SUMMARY_MAX_LEN);
3443
- const lastSentence = window.lastIndexOf(". ");
3444
- if (lastSentence >= 0) return s.slice(0, lastSentence + 1);
3445
- const ellipsis = "...";
3446
- const body = window.slice(0, SUMMARY_MAX_LEN - 3);
3447
- const lastSpace = body.lastIndexOf(" ");
3448
- return (lastSpace > 0 ? body.slice(0, lastSpace) : body).replace(/[.,;:\s]+$/, "") + ellipsis;
3449
- }
3450
- function description$1(doc, fallbackName) {
3451
- return clampSummary(doc?.description ?? `Styx generated wrappers for ${doc?.title ?? fallbackName ?? "tools"}.`);
3452
- }
3453
- function authorsField(doc) {
3454
- return `[${(doc?.authors?.length ? doc.authors : ["unknown"]).map((a) => `{ name = "${tomlStr(a)}" }`).join(", ")}]`;
3455
- }
3456
- function licenseField(proj) {
3457
- return `{ text = "${tomlStr(proj.license?.description ?? "unknown")}" }`;
3458
- }
3459
- /**
3460
- * Per-suite `pyproject.toml`. The flat layout (`python/<pkg>/bet.py`) makes the
3461
- * directory itself the importable package, so setuptools' `package-dir` maps the
3462
- * import name (`<pkg>`) onto the distribution's root directory. The styxdefs
3463
- * floor is the only runtime dependency.
3464
- *
3465
- * Precondition: `pkg.name` must be a valid Python identifier - the flat layout's
3466
- * relative imports (`from .bet import *`) already require this, and the CLI uses
3467
- * it verbatim as the directory name, so this stays consistent with that.
3468
- */
3469
- function generateSubPyproject(proj, pkg) {
3470
- const importName = pkgDir(pkg);
3471
- const cb = new CodeBuilder(" ");
3472
- cb.line("[project]");
3473
- cb.line(`name = "${tomlStr(pyDistName(proj, pkg))}"`);
3474
- cb.line(`version = "${tomlStr(proj.version ?? "0.0.0")}"`);
3475
- cb.line(`description = "${tomlStr(description$1(pkg.doc, pkg.name))}"`);
3476
- cb.line(`readme = "README.md"`);
3477
- cb.line(`license = ${licenseField(proj)}`);
3478
- cb.line(`authors = ${authorsField(pkg.doc ?? proj.doc)}`);
3479
- cb.line(`requires-python = "${REQUIRES_PYTHON}"`);
3480
- cb.line("dependencies = [");
3481
- cb.line(` "styxdefs${STYXDEFS_COMPAT.python}",`);
3482
- cb.line("]");
3483
- cb.blank();
3484
- cb.line("[tool.setuptools]");
3485
- cb.line(`packages = ["${importName}"]`);
3486
- cb.line(`package-dir = { "${importName}" = "." }`);
3487
- cb.blank();
3488
- cb.line("[tool.setuptools.package-data]");
3489
- cb.line(`"${importName}" = ["py.typed"]`);
3490
- cb.blank();
3491
- cb.line(BUILD_SYSTEM);
3492
- return cb.toString() + "\n";
3493
- }
3494
- /**
3495
- * Root `pyproject.toml`: a metapackage depending on each per-suite distribution
3496
- * plus the container/graph runner packages. `packages = []` keeps setuptools
3497
- * from sweeping the sibling suite directories into this distribution.
3498
- */
3499
- function generateRootPyproject(proj, distNames) {
3500
- const cb = new CodeBuilder(" ");
3501
- cb.line("[project]");
3502
- cb.line(`name = "${tomlStr(proj.name ?? "project")}"`);
3503
- cb.line(`version = "${tomlStr(proj.version ?? "0.0.0")}"`);
3504
- cb.line(`description = "${tomlStr(description$1(proj.doc, proj.name))}"`);
3505
- cb.line(`readme = "README.md"`);
3506
- cb.line(`license = ${licenseField(proj)}`);
3507
- cb.line(`authors = ${authorsField(proj.doc)}`);
3508
- cb.line(`requires-python = "${REQUIRES_PYTHON}"`);
3509
- cb.line("dependencies = [");
3510
- for (const dep of PYTHON_RUNNER_DEPS) cb.line(` "${dep}",`);
3511
- for (const dist of distNames) cb.line(` "${tomlStr(dist)}",`);
3512
- cb.line("]");
3513
- cb.blank();
3514
- cb.line("[tool.setuptools]");
3515
- cb.line("packages = []");
3516
- cb.blank();
3517
- cb.line(BUILD_SYSTEM);
3518
- return cb.toString() + "\n";
3519
- }
3520
- /** Per-suite README crediting the upstream tool authors. */
3521
- function generateSubReadme(proj, pkg) {
3522
- const projectTitle = proj.doc?.title ?? proj.name ?? "Styx";
3523
- const packageTitle = pkg.doc?.title ?? pkg.name ?? "package";
3524
- const url = pkg.doc?.urls?.[0];
3525
- const titleMd = url ? `[${packageTitle}](${url})` : packageTitle;
3526
- const credits = pkg.doc?.authors?.length ? pkg.doc.authors.join(", ") : pkg.doc?.urls?.join(", ") ?? "unknown";
3527
- return `# ${projectTitle} wrappers for ${titleMd}${pkg.doc?.description ? `\n\n${pkg.doc.description}` : ""}\n\n${packageTitle} is made by ${credits}.\n\nThis package contains wrappers only and has no affiliation with the original authors.\n`;
3528
- }
3529
- /** Root README listing the bundled per-suite distributions. */
3530
- function generateRootReadme(proj, distNames) {
3531
- return `# ${proj.doc?.title ?? proj.name ?? "Styx"}\n${proj.doc?.description ? `\n${proj.doc.description}\n` : ""}\nAuto-generated Styx wrappers. This project bundles the following packages:\n\n${distNames.map((d) => `- ${d}`).join("\n")}\n`;
3532
- }
3533
- /** Local-install manifest: each suite directory first, then the root metapackage. */
3534
- function generateRequirementsTxt(pkgDirs) {
3535
- return [...pkgDirs.map((d) => `./${d}`), "./"].join("\n") + "\n";
3536
- }
3537
-
3538
- //#endregion
3539
- //#region src/backend/sig-entries.ts
3540
- /**
3541
- * Build per-field signature entries for the kwarg wrapper and params factory.
3542
- * Skips `@type` (the factory injects it as a constant). Required-no-default
3543
- * entries are placed before defaulted ones so the resulting signature is
3544
- * syntactically valid in both Python and TS.
3545
- *
3546
- * `registerLocal` is called once per field with the wire key; it must return
3547
- * a scrubbed, unique host identifier (typically by combining a language-aware
3548
- * scrub function with `Scope.add()`). The caller's scope should already have
3549
- * the function's other locals (`params`, `runner`, ...) reserved so this
3550
- * registration cannot collide with them.
3551
- */
3552
- function buildSigEntries(rootType, fieldInfo, registerLocal, opts) {
3553
- const entries = [];
3554
- for (const [fieldName, fieldType] of Object.entries(rootType.fields)) {
3555
- if (fieldType.kind === "literal") continue;
3556
- const fi = fieldInfo.get(fieldName);
3557
- const isOptional = fieldType.kind === "optional";
3558
- const inner = isOptional ? fieldType.inner : fieldType;
3559
- let sigType = opts.renderType(inner);
3560
- if (isOptional) sigType += opts.nullableSuffix;
3561
- let sigDefault;
3562
- const hasExplicitDefault = fi?.defaultValue !== void 0;
3563
- if (hasExplicitDefault) sigDefault = opts.renderDefault(fi.defaultValue);
3564
- else if (isOptional) sigDefault = opts.nullableDefault;
3565
- entries.push({
3566
- name: registerLocal(fieldName),
3567
- wireKey: fieldName,
3568
- sigType,
3569
- sigDefault,
3570
- isOptional,
3571
- hasExplicitDefault,
3572
- doc: fi?.doc
3573
- });
3574
- }
3575
- const required = entries.filter((e) => e.sigDefault === void 0);
3576
- const defaulted = entries.filter((e) => e.sigDefault !== void 0);
3577
- return [...required, ...defaulted];
3578
- }
3579
-
3580
3390
  //#endregion
3581
3391
  //#region src/backend/union-variants.ts
3582
3392
  /**
@@ -4345,54 +4155,452 @@ function emitParamsFactory$1(entries, funcName, paramsType, typeTag, cb) {
4345
4155
  });
4346
4156
  }
4347
4157
  /**
4348
- * Emit the user-facing kwarg wrapper: takes the same kwargs as `_params()`
4349
- * plus `runner`, builds the params dict, and delegates to the dict-style
4350
- * execute function.
4158
+ * Emit the user-facing kwarg wrapper: takes the same kwargs as `_params()`
4159
+ * plus `runner`, builds the params dict, and delegates to the dict-style
4160
+ * execute function.
4161
+ */
4162
+ function emitKwargWrapper$1(ctx, entries, funcName, paramsFnName, executeFnName, outputsType, cb) {
4163
+ cb.line(`def ${funcName}(`);
4164
+ cb.indent(() => {
4165
+ emitSigParams$1(entries, cb);
4166
+ cb.line("runner: Runner | None = None,");
4167
+ });
4168
+ const returnType = outputsType ?? "None";
4169
+ cb.line(`) -> ${returnType}:`);
4170
+ cb.indent(() => {
4171
+ const appDoc = ctx.app?.doc;
4172
+ cb.line("\"\"\"");
4173
+ if (appDoc?.title) cb.line(appDoc.title);
4174
+ if (appDoc?.description) {
4175
+ if (appDoc?.title) cb.blank();
4176
+ cb.line(appDoc.description);
4177
+ }
4178
+ if (appDoc?.authors?.length) {
4179
+ cb.blank();
4180
+ cb.line(`Author: ${appDoc.authors.join(", ")}`);
4181
+ }
4182
+ if (appDoc?.urls?.length) {
4183
+ cb.blank();
4184
+ cb.line(`URL: ${appDoc.urls[0]}`);
4185
+ }
4186
+ cb.blank();
4187
+ emitArgsBlock([...entries, {
4188
+ name: "runner",
4189
+ doc: "Command runner (defaults to global runner)."
4190
+ }], cb);
4191
+ cb.blank();
4192
+ cb.line("Returns:");
4193
+ cb.line(outputsType ? " Tool outputs (paths to files produced by the tool)." : " None.");
4194
+ cb.line("\"\"\"");
4195
+ if (entries.length === 0) cb.line(`params = ${paramsFnName}()`);
4196
+ else {
4197
+ cb.line(`params = ${paramsFnName}(`);
4198
+ cb.indent(() => {
4199
+ for (const e of entries) cb.line(`${e.name}=${e.name},`);
4200
+ });
4201
+ cb.line(")");
4202
+ }
4203
+ if (outputsType) cb.line(`return ${executeFnName}(params, runner)`);
4204
+ else cb.line(`${executeFnName}(params, runner)`);
4205
+ });
4206
+ }
4207
+
4208
+ //#endregion
4209
+ //#region src/backend/nipype/emit.ts
4210
+ function call(ctor, args) {
4211
+ return `${ctor}(${args.join(", ")})`;
4212
+ }
4213
+ /**
4214
+ * Render a numeric literal, forcing a float form (e.g. `0.0`) for a float field.
4215
+ * `traits.Range` infers its numeric type from the bound literals, so integer
4216
+ * bounds on a float field (e.g. `low=0, high=1`) would build an *integer* range
4217
+ * that rejects `0.5`. Emitting `0.0`/`1.0` keeps it a float range.
4218
+ */
4219
+ function renderNum(value, asFloat) {
4220
+ if (asFloat && typeof value === "number" && Number.isInteger(value)) return `${value}.0`;
4221
+ return renderPyLiteral(value);
4222
+ }
4223
+ /** Human-readable `desc=` text: doc + degrade/media-type notes. */
4224
+ function descText(p) {
4225
+ const parts = [];
4226
+ if (p.doc) parts.push(p.doc);
4227
+ if (p.kind === "struct" || p.kind === "union") parts.push("(nested configuration; pass a dict)");
4228
+ if (p.mediaTypes && p.mediaTypes.length > 0) parts.push(`(media types: ${p.mediaTypes.join(", ")})`);
4229
+ return parts.length > 0 ? parts.join(" ") : void 0;
4230
+ }
4231
+ /** Map a list element descriptor to a (bare) nipype inner trait. */
4232
+ function renderItemTrait(item) {
4233
+ if (!item) return "traits.Any()";
4234
+ switch (item.kind) {
4235
+ case "path": return "File(exists=True)";
4236
+ case "int":
4237
+ case "count": return "traits.Int()";
4238
+ case "float": return "traits.Float()";
4239
+ case "str": return "traits.Str()";
4240
+ case "bool": return "traits.Bool()";
4241
+ case "enum": return call("traits.Enum", (item.choices ?? []).map((c) => renderPyLiteral(c)));
4242
+ default: return "traits.Any()";
4243
+ }
4244
+ }
4245
+ /** Map a parameter to its nipype input trait expression, carrying rich constraints. */
4246
+ function renderInputTrait(p) {
4247
+ const tail = [];
4248
+ if (p.mandatory) tail.push("mandatory=True");
4249
+ const desc = descText(p);
4250
+ if (desc) tail.push(`desc=${pyStr(desc)}`);
4251
+ const hasDef = p.hasDefault && p.default !== void 0;
4252
+ const def = p.default;
4253
+ switch (p.kind) {
4254
+ case "path": return call("File", ["exists=True", ...tail]);
4255
+ case "bool": return call("traits.Bool", [...hasDef ? [renderPyLiteral(def), "usedefault=True"] : [], ...tail]);
4256
+ case "count": return call("traits.Int", [...hasDef ? [renderPyLiteral(def), "usedefault=True"] : [], ...tail]);
4257
+ case "int":
4258
+ case "float": {
4259
+ const asFloat = p.kind === "float";
4260
+ if (p.range) {
4261
+ const a = [];
4262
+ if (hasDef) a.push(`value=${renderNum(def, asFloat)}`);
4263
+ if (p.range.min !== void 0) a.push(`low=${renderNum(p.range.min, asFloat)}`);
4264
+ if (p.range.max !== void 0) a.push(`high=${renderNum(p.range.max, asFloat)}`);
4265
+ if (hasDef) a.push("usedefault=True");
4266
+ return call("traits.Range", [...a, ...tail]);
4267
+ }
4268
+ return call(p.kind === "int" ? "traits.Int" : "traits.Float", [...hasDef ? [renderNum(def, asFloat), "usedefault=True"] : [], ...tail]);
4269
+ }
4270
+ case "str": return call("traits.Str", [...hasDef ? [renderPyLiteral(def), "usedefault=True"] : [], ...tail]);
4271
+ case "enum": {
4272
+ const choices = p.choices ?? [];
4273
+ return call("traits.Enum", [
4274
+ ...(hasDef ? [def, ...choices.filter((c) => c !== def)] : choices).map((c) => renderPyLiteral(c)),
4275
+ ...hasDef ? ["usedefault=True"] : [],
4276
+ ...tail
4277
+ ]);
4278
+ }
4279
+ case "list": {
4280
+ const inner = renderItemTrait(p.itemType);
4281
+ const bounds = [];
4282
+ if (p.listBounds?.min !== void 0) bounds.push(`minlen=${p.listBounds.min}`);
4283
+ if (p.listBounds?.max !== void 0) bounds.push(`maxlen=${p.listBounds.max}`);
4284
+ return call("traits.List", [
4285
+ inner,
4286
+ ...bounds,
4287
+ ...tail
4288
+ ]);
4289
+ }
4290
+ case "struct":
4291
+ case "union": return call("traits.Any", tail);
4292
+ }
4293
+ }
4294
+ /**
4295
+ * Map an output field to its nipype output trait expression. `isRoot` is the
4296
+ * synthetic output directory, typed as a `Directory` rather than a `File`.
4297
+ */
4298
+ function renderOutputTrait(f, isRoot) {
4299
+ const tail = f.doc ? [`desc=${pyStr(f.doc)}`] : [];
4300
+ if (f.shape.kind === "list") return call("traits.List", ["File()", ...tail]);
4301
+ if (isRoot) return call("Directory", tail);
4302
+ return call("File", tail);
4303
+ }
4304
+ /**
4305
+ * Emit the nipype interface module for one tool: a typed InputSpec/OutputSpec and
4306
+ * a BaseInterface that delegates execution to the co-emitted styx Python wrapper.
4307
+ */
4308
+ function emitNipypeInterface(ctx, spec, names) {
4309
+ const cb = new CodeBuilder(" ");
4310
+ cb.comment("This file was auto generated by Styx.", "# ");
4311
+ cb.comment("Do not edit this file directly.", "# ");
4312
+ cb.blank();
4313
+ cb.line("from nipype.interfaces.base import (");
4314
+ cb.indent(() => {
4315
+ cb.line("BaseInterface,");
4316
+ cb.line("BaseInterfaceInputSpec,");
4317
+ cb.line("Directory,");
4318
+ cb.line("File,");
4319
+ cb.line("TraitedSpec,");
4320
+ cb.line("isdefined,");
4321
+ cb.line("traits,");
4322
+ });
4323
+ cb.line(")");
4324
+ cb.blank();
4325
+ cb.line(`from .${names.styxStem} import ${spec.delegation.wrapperFn}, ${spec.delegation.outputsClass}`);
4326
+ cb.blank();
4327
+ cb.blank();
4328
+ cb.line(`class ${names.inputSpec}(BaseInterfaceInputSpec):`);
4329
+ cb.indent(() => {
4330
+ if (!spec.rootIsStruct || spec.params.length === 0) {
4331
+ cb.line("pass");
4332
+ return;
4333
+ }
4334
+ for (const p of spec.params) cb.line(`${p.hostName} = ${renderInputTrait(p)}`);
4335
+ });
4336
+ cb.blank();
4337
+ cb.blank();
4338
+ cb.line(`class ${names.outputSpec}(TraitedSpec):`);
4339
+ cb.indent(() => {
4340
+ if (spec.outputs.length === 0 && spec.streams.length === 0) {
4341
+ cb.line("pass");
4342
+ return;
4343
+ }
4344
+ spec.outputs.forEach((f, i) => cb.line(`${f.id} = ${renderOutputTrait(f, i === 0)}`));
4345
+ for (const s of spec.streams) {
4346
+ const tail = s.doc ? `, desc=${pyStr(s.doc)}` : "";
4347
+ cb.line(`${s.id} = traits.List(traits.Str()${tail})`);
4348
+ }
4349
+ });
4350
+ cb.blank();
4351
+ cb.blank();
4352
+ cb.line(`class ${names.cls}(BaseInterface):`);
4353
+ cb.indent(() => {
4354
+ emitDocstring(cb, [ctx.app?.doc?.title, ctx.app?.doc?.description].filter(Boolean).join("\n\n") || void 0);
4355
+ cb.line(`input_spec = ${names.inputSpec}`);
4356
+ cb.line(`output_spec = ${names.outputSpec}`);
4357
+ cb.blank();
4358
+ cb.line("def _run_interface(self, runtime):");
4359
+ cb.indent(() => {
4360
+ if (!spec.rootIsStruct) {
4361
+ cb.line("raise NotImplementedError(");
4362
+ cb.indent(() => cb.line(pyStr("styx nipype backend: tools with a non-struct root are not supported.")));
4363
+ cb.line(")");
4364
+ return;
4365
+ }
4366
+ cb.line("kwargs = {}");
4367
+ for (const p of spec.params) if (p.mandatory) cb.line(`kwargs[${pyStr(p.hostName)}] = self.inputs.${p.hostName}`);
4368
+ else {
4369
+ cb.line(`if isdefined(self.inputs.${p.hostName}):`);
4370
+ cb.indent(() => cb.line(`kwargs[${pyStr(p.hostName)}] = self.inputs.${p.hostName}`));
4371
+ }
4372
+ cb.line(`self._result: ${spec.delegation.outputsClass} = ${spec.delegation.wrapperFn}(**kwargs)`);
4373
+ cb.line("return runtime");
4374
+ });
4375
+ cb.blank();
4376
+ cb.line("def _list_outputs(self):");
4377
+ cb.indent(() => {
4378
+ if (!spec.rootIsStruct) {
4379
+ cb.line("return self._outputs().get()");
4380
+ return;
4381
+ }
4382
+ cb.line("result = self._result");
4383
+ cb.line("outputs = self._outputs().get()");
4384
+ for (const f of spec.outputs) if (f.shape.kind === "single" && f.shape.optional) {
4385
+ cb.line(`if result.${f.id} is not None:`);
4386
+ cb.indent(() => cb.line(`outputs[${pyStr(f.id)}] = result.${f.id}`));
4387
+ } else cb.line(`outputs[${pyStr(f.id)}] = result.${f.id}`);
4388
+ for (const s of spec.streams) cb.line(`outputs[${pyStr(s.id)}] = result.${s.id}`);
4389
+ cb.line("return outputs");
4390
+ });
4391
+ });
4392
+ return cb.toString();
4393
+ }
4394
+
4395
+ //#endregion
4396
+ //#region src/backend/styxdefs-compat.ts
4397
+ /**
4398
+ * Runtime version floors baked into generated dependency metadata.
4399
+ *
4400
+ * styx2-generated code calls `mutable_copy` / `mutableCopy` (introduced in the
4401
+ * styxdefs 0.7.0 / styxdefs-js 0.2.0 release), so emitted packages genuinely
4402
+ * require that runtime floor. This is the single source of truth: bump here and
4403
+ * both the Python and TypeScript backends pick it up.
4404
+ */
4405
+ const STYXDEFS_COMPAT = {
4406
+ python: ">=0.7.0,<0.8.0",
4407
+ npm: "^0.2.0"
4408
+ };
4409
+ /**
4410
+ * Extra Python runtime packages the root metapackage pulls in. `styxkit[all]`
4411
+ * provides the cross-backend runner-selection helpers (`use_docker`, `use_auto`,
4412
+ * ...) that the metapackage's `__init__` re-exports, and transitively installs
4413
+ * every container/graph runner backend. Left unpinned - styxkit's own styxdefs
4414
+ * floor constrains the stack.
4415
+ */
4416
+ const PYTHON_RUNNER_DEPS = ["styxkit[all]"];
4417
+
4418
+ //#endregion
4419
+ //#region src/backend/python/packaging.ts
4420
+ const REQUIRES_PYTHON = ">=3.10";
4421
+ const BUILD_SYSTEM = `[build-system]
4422
+ requires = ["setuptools>=61"]
4423
+ build-backend = "setuptools.build_meta"`;
4424
+ /** Escape a value for embedding in a TOML basic string. */
4425
+ function tomlStr(s) {
4426
+ return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/[\r\n]+/g, " ").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "").trim();
4427
+ }
4428
+ /** Suite directory / importable package name; matches the CLI's `pkgDir` fallback. */
4429
+ function pkgDir(pkg) {
4430
+ return pkg.name ?? "package";
4431
+ }
4432
+ /** Distribution (PyPI) name for a package: `<project>_<package>`, or just `<package>`. */
4433
+ function pyDistName(proj, pkg) {
4434
+ const name = pkgDir(pkg);
4435
+ return proj.name ? `${proj.name}_${name}` : name;
4436
+ }
4437
+ const SUMMARY_MAX_LEN = 512;
4438
+ /**
4439
+ * Clamp a description to a <=512-char one-line summary. Prefer cutting at the
4440
+ * last complete sentence that fits (clean, no dangling fragment); if there is no
4441
+ * sentence boundary early enough, cut at a word boundary and mark the elision.
4442
+ */
4443
+ function clampSummary(s) {
4444
+ if (s.length <= SUMMARY_MAX_LEN) return s;
4445
+ const window = s.slice(0, SUMMARY_MAX_LEN);
4446
+ const lastSentence = window.lastIndexOf(". ");
4447
+ if (lastSentence >= 0) return s.slice(0, lastSentence + 1);
4448
+ const ellipsis = "...";
4449
+ const body = window.slice(0, SUMMARY_MAX_LEN - 3);
4450
+ const lastSpace = body.lastIndexOf(" ");
4451
+ return (lastSpace > 0 ? body.slice(0, lastSpace) : body).replace(/[.,;:\s]+$/, "") + ellipsis;
4452
+ }
4453
+ function description$1(doc, fallbackName) {
4454
+ return clampSummary(doc?.description ?? `Styx generated wrappers for ${doc?.title ?? fallbackName ?? "tools"}.`);
4455
+ }
4456
+ function authorsField(doc) {
4457
+ return `[${(doc?.authors?.length ? doc.authors : ["unknown"]).map((a) => `{ name = "${tomlStr(a)}" }`).join(", ")}]`;
4458
+ }
4459
+ function licenseField(proj) {
4460
+ return `{ text = "${tomlStr(proj.license?.description ?? "unknown")}" }`;
4461
+ }
4462
+ /**
4463
+ * Per-suite `pyproject.toml`. The suite source stays in a flat directory
4464
+ * (`python/<pkg>/bet.py`), but setuptools' `package-dir` maps that directory onto
4465
+ * the dotted import package `<project>.<pkg>`, so every suite nests under the
4466
+ * metapackage's `<project>/` namespace. That restores `from <project> import
4467
+ * <pkg>` while keeping each suite a separately-installable distribution (and
4468
+ * leaves no top-level `<pkg>` polluting the global namespace). With no project
4469
+ * name the import name is the bare `<pkg>` (top-level fallback). The styxdefs
4470
+ * floor is the only runtime dependency.
4471
+ *
4472
+ * Precondition: `importName` is a valid (possibly dotted) Python package path -
4473
+ * the caller scrubs the project + package names into identifiers accordingly.
4474
+ */
4475
+ function generateSubPyproject(proj, pkg, importName) {
4476
+ const cb = new CodeBuilder(" ");
4477
+ cb.line("[project]");
4478
+ cb.line(`name = "${tomlStr(pyDistName(proj, pkg))}"`);
4479
+ cb.line(`version = "${tomlStr(proj.version ?? "0.0.0")}"`);
4480
+ cb.line(`description = "${tomlStr(description$1(pkg.doc, pkg.name))}"`);
4481
+ cb.line(`readme = "README.md"`);
4482
+ cb.line(`license = ${licenseField(proj)}`);
4483
+ cb.line(`authors = ${authorsField(pkg.doc ?? proj.doc)}`);
4484
+ cb.line(`requires-python = "${REQUIRES_PYTHON}"`);
4485
+ cb.line("dependencies = [");
4486
+ cb.line(` "styxdefs${STYXDEFS_COMPAT.python}",`);
4487
+ cb.line("]");
4488
+ cb.blank();
4489
+ cb.line("[tool.setuptools]");
4490
+ cb.line(`packages = ["${importName}"]`);
4491
+ cb.line(`package-dir = { "${importName}" = "." }`);
4492
+ cb.blank();
4493
+ cb.line("[tool.setuptools.package-data]");
4494
+ cb.line(`"${importName}" = ["py.typed"]`);
4495
+ cb.blank();
4496
+ cb.line(BUILD_SYSTEM);
4497
+ return cb.toString() + "\n";
4498
+ }
4499
+ /**
4500
+ * The metapackage's `__init__.py`. A thin re-export of styxkit so the runner
4501
+ * configuration helpers (`use_docker`, `use_local`, `set_global_runner`, ...)
4502
+ * are reachable as `<project>.<name>` - the v1 ergonomic that the v2 split into
4503
+ * per-suite distributions otherwise dropped. The logic lives in styxkit (pulled
4504
+ * in via `styxkit[all]`), so this stays a wildcard re-export and never re-emits
4505
+ * the runner-config code itself.
4506
+ */
4507
+ function generateRootInitPy() {
4508
+ return "# This file was auto generated by Styx.\n# Do not edit this file directly.\n\n# Re-export styxkit's runner-configuration helpers (use_docker, use_local,\n# use_auto, set_global_runner, get_global_runner, ...) so they are available\n# directly on this package, e.g. `import niwrap; niwrap.use_docker()`.\nfrom styxkit import * # noqa: F401,F403\n";
4509
+ }
4510
+ /**
4511
+ * Root `pyproject.toml`: a metapackage depending on `styxkit[all]` (the runner
4512
+ * stack it re-exports) plus each per-suite distribution. `packages` lists only
4513
+ * the metapackage's own module so setuptools ships the styxkit re-export without
4514
+ * sweeping the sibling suite directories into this distribution. The module is
4515
+ * the `<project>/` namespace package the suites nest into, so installing the
4516
+ * metapackage makes `<project>.use_docker()` reachable alongside the suites'
4517
+ * `from <project> import <pkg>`. `moduleDir` is the on-disk source directory; it
4518
+ * differs from the import `moduleName` only when a suite is named after the
4519
+ * project, in which case a `package-dir` remap keeps the import name intact.
4520
+ */
4521
+ function generateRootPyproject(proj, distNames, moduleName, moduleDir) {
4522
+ const cb = new CodeBuilder(" ");
4523
+ cb.line("[project]");
4524
+ cb.line(`name = "${tomlStr(proj.name ?? "project")}"`);
4525
+ cb.line(`version = "${tomlStr(proj.version ?? "0.0.0")}"`);
4526
+ cb.line(`description = "${tomlStr(description$1(proj.doc, proj.name))}"`);
4527
+ cb.line(`readme = "README.md"`);
4528
+ cb.line(`license = ${licenseField(proj)}`);
4529
+ cb.line(`authors = ${authorsField(proj.doc)}`);
4530
+ cb.line(`requires-python = "${REQUIRES_PYTHON}"`);
4531
+ cb.line("dependencies = [");
4532
+ for (const dep of PYTHON_RUNNER_DEPS) cb.line(` "${dep}",`);
4533
+ for (const dist of distNames) cb.line(` "${tomlStr(dist)}",`);
4534
+ cb.line("]");
4535
+ cb.blank();
4536
+ cb.line("[tool.setuptools]");
4537
+ cb.line(`packages = ["${moduleName}"]`);
4538
+ if (moduleDir !== moduleName) cb.line(`package-dir = { "${moduleName}" = "${moduleDir}" }`);
4539
+ cb.blank();
4540
+ cb.line("[tool.setuptools.package-data]");
4541
+ cb.line(`"${moduleName}" = ["py.typed"]`);
4542
+ cb.blank();
4543
+ cb.line(BUILD_SYSTEM);
4544
+ return cb.toString() + "\n";
4545
+ }
4546
+ /** Per-suite README crediting the upstream tool authors. */
4547
+ function generateSubReadme(proj, pkg) {
4548
+ const projectTitle = proj.doc?.title ?? proj.name ?? "Styx";
4549
+ const packageTitle = pkg.doc?.title ?? pkg.name ?? "package";
4550
+ const url = pkg.doc?.urls?.[0];
4551
+ const titleMd = url ? `[${packageTitle}](${url})` : packageTitle;
4552
+ const credits = pkg.doc?.authors?.length ? pkg.doc.authors.join(", ") : pkg.doc?.urls?.join(", ") ?? "unknown";
4553
+ return `# ${projectTitle} wrappers for ${titleMd}${pkg.doc?.description ? `\n\n${pkg.doc.description}` : ""}\n\n${packageTitle} is made by ${credits}.\n\nThis package contains wrappers only and has no affiliation with the original authors.\n`;
4554
+ }
4555
+ /** Root README listing the bundled per-suite distributions. */
4556
+ function generateRootReadme(proj, distNames) {
4557
+ return `# ${proj.doc?.title ?? proj.name ?? "Styx"}\n${proj.doc?.description ? `\n${proj.doc.description}\n` : ""}\nAuto-generated Styx wrappers. This project bundles the following packages:\n\n${distNames.map((d) => `- ${d}`).join("\n")}\n`;
4558
+ }
4559
+ /** Local-install manifest: each suite directory first, then the root metapackage. */
4560
+ function generateRequirementsTxt(pkgDirs) {
4561
+ return [...pkgDirs.map((d) => `./${d}`), "./"].join("\n") + "\n";
4562
+ }
4563
+
4564
+ //#endregion
4565
+ //#region src/backend/sig-entries.ts
4566
+ /**
4567
+ * Build per-field signature entries for the kwarg wrapper and params factory.
4568
+ * Skips `@type` (the factory injects it as a constant). Required-no-default
4569
+ * entries are placed before defaulted ones so the resulting signature is
4570
+ * syntactically valid in both Python and TS.
4571
+ *
4572
+ * `registerLocal` is called once per field with the wire key; it must return
4573
+ * a scrubbed, unique host identifier (typically by combining a language-aware
4574
+ * scrub function with `Scope.add()`). The caller's scope should already have
4575
+ * the function's other locals (`params`, `runner`, ...) reserved so this
4576
+ * registration cannot collide with them.
4351
4577
  */
4352
- function emitKwargWrapper$1(ctx, entries, funcName, paramsFnName, executeFnName, outputsType, cb) {
4353
- cb.line(`def ${funcName}(`);
4354
- cb.indent(() => {
4355
- emitSigParams$1(entries, cb);
4356
- cb.line("runner: Runner | None = None,");
4357
- });
4358
- const returnType = outputsType ?? "None";
4359
- cb.line(`) -> ${returnType}:`);
4360
- cb.indent(() => {
4361
- const appDoc = ctx.app?.doc;
4362
- cb.line("\"\"\"");
4363
- if (appDoc?.title) cb.line(appDoc.title);
4364
- if (appDoc?.description) {
4365
- if (appDoc?.title) cb.blank();
4366
- cb.line(appDoc.description);
4367
- }
4368
- if (appDoc?.authors?.length) {
4369
- cb.blank();
4370
- cb.line(`Author: ${appDoc.authors.join(", ")}`);
4371
- }
4372
- if (appDoc?.urls?.length) {
4373
- cb.blank();
4374
- cb.line(`URL: ${appDoc.urls[0]}`);
4375
- }
4376
- cb.blank();
4377
- emitArgsBlock([...entries, {
4378
- name: "runner",
4379
- doc: "Command runner (defaults to global runner)."
4380
- }], cb);
4381
- cb.blank();
4382
- cb.line("Returns:");
4383
- cb.line(outputsType ? " Tool outputs (paths to files produced by the tool)." : " None.");
4384
- cb.line("\"\"\"");
4385
- if (entries.length === 0) cb.line(`params = ${paramsFnName}()`);
4386
- else {
4387
- cb.line(`params = ${paramsFnName}(`);
4388
- cb.indent(() => {
4389
- for (const e of entries) cb.line(`${e.name}=${e.name},`);
4390
- });
4391
- cb.line(")");
4392
- }
4393
- if (outputsType) cb.line(`return ${executeFnName}(params, runner)`);
4394
- else cb.line(`${executeFnName}(params, runner)`);
4395
- });
4578
+ function buildSigEntries(rootType, fieldInfo, registerLocal, opts) {
4579
+ const entries = [];
4580
+ for (const [fieldName, fieldType] of Object.entries(rootType.fields)) {
4581
+ if (fieldType.kind === "literal") continue;
4582
+ const fi = fieldInfo.get(fieldName);
4583
+ const isOptional = fieldType.kind === "optional";
4584
+ const inner = isOptional ? fieldType.inner : fieldType;
4585
+ let sigType = opts.renderType(inner);
4586
+ if (isOptional) sigType += opts.nullableSuffix;
4587
+ let sigDefault;
4588
+ const hasExplicitDefault = fi?.defaultValue !== void 0;
4589
+ if (hasExplicitDefault) sigDefault = opts.renderDefault(fi.defaultValue);
4590
+ else if (isOptional) sigDefault = opts.nullableDefault;
4591
+ entries.push({
4592
+ name: registerLocal(fieldName),
4593
+ wireKey: fieldName,
4594
+ sigType,
4595
+ sigDefault,
4596
+ isOptional,
4597
+ hasExplicitDefault,
4598
+ doc: fi?.doc
4599
+ });
4600
+ }
4601
+ const required = entries.filter((e) => e.sigDefault === void 0);
4602
+ const defaulted = entries.filter((e) => e.sigDefault !== void 0);
4603
+ return [...required, ...defaulted];
4396
4604
  }
4397
4605
 
4398
4606
  //#endregion
@@ -5156,7 +5364,7 @@ function computePublicNames$1(appId) {
5156
5364
  * (the `reg` registrations and the `sigScope` child), so passing the same scope
5157
5365
  * the emitter continues with keeps later local registrations consistent.
5158
5366
  */
5159
- function buildEmitModel$1(ctx, scope = new Scope(PY_RESERVED)) {
5367
+ function buildEmitModel(ctx, scope = new Scope(PY_RESERVED)) {
5160
5368
  const appId = ctx.app?.id;
5161
5369
  const pkg = ctx.package?.name;
5162
5370
  const publicNames = computePublicNames$1(appId);
@@ -5199,7 +5407,7 @@ function buildEmitModel$1(ctx, scope = new Scope(PY_RESERVED)) {
5199
5407
  function generatePython(ctx, packageScope) {
5200
5408
  const cb = new CodeBuilder(" ");
5201
5409
  const scope = packageScope ?? new Scope(PY_RESERVED);
5202
- const { names, rootType, rootIsStruct, namedTypes, typeDecls, rootTypeTag, paramsType, sigEntries } = buildEmitModel$1(ctx, scope);
5410
+ const { names, rootType, rootIsStruct, namedTypes, typeDecls, rootTypeTag, paramsType, sigEntries } = buildEmitModel(ctx, scope);
5203
5411
  cb.comment("This file was auto generated by Styx.", "# ");
5204
5412
  cb.comment("Do not edit this file directly.", "# ");
5205
5413
  cb.blank();
@@ -5346,21 +5554,33 @@ var PythonBackend = class {
5346
5554
  const files = /* @__PURE__ */ new Map();
5347
5555
  const distNames = [];
5348
5556
  const pkgDirs = [];
5557
+ const warnings = [];
5558
+ const nsName = proj.name && proj.name.trim() ? pyScrubIdent(proj.name, PY_RESERVED) : void 0;
5349
5559
  for (const p of packages) {
5350
5560
  const pkg = p.meta ?? {};
5351
5561
  const dir = pkg.name ?? "package";
5352
5562
  pkgDirs.push(dir);
5353
5563
  distNames.push(pyDistName(proj, pkg));
5354
- files.set(`${dir}/pyproject.toml`, generateSubPyproject(proj, pkg));
5564
+ const importPkg = nsName ? `${nsName}.${dir}` : dir;
5565
+ files.set(`${dir}/pyproject.toml`, generateSubPyproject(proj, pkg, importPkg));
5355
5566
  files.set(`${dir}/README.md`, generateSubReadme(proj, pkg));
5356
5567
  }
5357
- files.set("pyproject.toml", generateRootPyproject(proj, distNames));
5568
+ const metaImport = nsName ?? "project";
5569
+ let metaDir = metaImport;
5570
+ if (pkgDirs.includes(metaDir)) {
5571
+ const collided = metaDir;
5572
+ while (pkgDirs.includes(metaDir)) metaDir += "_";
5573
+ warnings.push({ message: `metapackage directory "${collided}" collides with a suite directory; emitting its sources in "${metaDir}" instead (import name stays "${metaImport}")` });
5574
+ }
5575
+ files.set(`${metaDir}/__init__.py`, generateRootInitPy());
5576
+ files.set(`${metaDir}/py.typed`, "");
5577
+ files.set("pyproject.toml", generateRootPyproject(proj, distNames, metaImport, metaDir));
5358
5578
  files.set("README.md", generateRootReadme(proj, distNames));
5359
5579
  files.set("requirements.txt", generateRequirementsTxt(pkgDirs));
5360
5580
  return {
5361
5581
  files,
5362
5582
  errors: [],
5363
- warnings: []
5583
+ warnings
5364
5584
  };
5365
5585
  }
5366
5586
  };
@@ -5506,7 +5726,7 @@ const pyDialect = {
5506
5726
  * @param opts - Import and package-root options.
5507
5727
  */
5508
5728
  function renderPythonCall(ctx, config, opts = {}) {
5509
- const model = buildEmitModel$1(ctx);
5729
+ const model = buildEmitModel(ctx);
5510
5730
  const pkg = ctx.package?.name;
5511
5731
  const callee = pkg ? `${pkg}.${model.names.wrapper}` : model.names.wrapper;
5512
5732
  const call = model.rootIsStruct && model.rootType.kind === "struct" ? renderKwargCall(callee, model.sigEntries, model.rootType, config) : `${callee}(${renderValue(config, model.rootType, "", pyDialect)})`;
@@ -5529,6 +5749,495 @@ function renderKwargCall(callee, sigEntries, rootType, config) {
5529
5749
  return `${callee}(\n${lines.join("\n")}\n)`;
5530
5750
  }
5531
5751
 
5752
+ //#endregion
5753
+ //#region src/backend/typed-spec.ts
5754
+ /** Project a tool's solved tree into the flat typed spec for delegation backends. */
5755
+ function buildTypedSpec(ctx) {
5756
+ const model = buildEmitModel(ctx);
5757
+ const delegation = {
5758
+ moduleName: appModuleName$1(ctx.app),
5759
+ wrapperFn: model.names.wrapper,
5760
+ outputsClass: model.names.outputs
5761
+ };
5762
+ const outputs = collectOutputFields(ctx, pyId);
5763
+ const streams = streamFields(ctx, pyId);
5764
+ if (!model.rootIsStruct || model.rootType.kind !== "struct") return {
5765
+ rootIsStruct: false,
5766
+ params: [],
5767
+ outputs,
5768
+ streams,
5769
+ delegation
5770
+ };
5771
+ const rootType = model.rootType;
5772
+ const fieldByName = new Map(structFields(ctx, rootType, ctx.expr).map((f) => [f.name, f]));
5773
+ const fieldInfo = collectFieldInfo(ctx, rootType);
5774
+ return {
5775
+ rootIsStruct: true,
5776
+ params: model.sigEntries.map((e) => {
5777
+ const fe = fieldByName.get(e.wireKey);
5778
+ const fi = fieldInfo.get(e.wireKey);
5779
+ return describeParam(e, fe?.type, fe?.node, fi?.defaultValue, fi?.doc);
5780
+ }),
5781
+ outputs,
5782
+ streams,
5783
+ delegation
5784
+ };
5785
+ }
5786
+ function describeParam(e, type, node, defaultValue, fallbackDoc) {
5787
+ const optional = e.isOptional;
5788
+ const hasDefault = e.hasExplicitDefault;
5789
+ const base = {
5790
+ hostName: e.name,
5791
+ wireKey: e.wireKey,
5792
+ optional,
5793
+ hasDefault,
5794
+ mandatory: !optional && !hasDefault,
5795
+ default: defaultValue,
5796
+ doc: e.doc ?? fallbackDoc
5797
+ };
5798
+ const inner = type && type.kind === "optional" ? type.inner : type;
5799
+ const choices = inner ? literalChoices(inner, node) : void 0;
5800
+ if (choices) return {
5801
+ ...base,
5802
+ kind: "enum",
5803
+ choices
5804
+ };
5805
+ switch (inner?.kind) {
5806
+ case "scalar":
5807
+ if (inner.scalar === "path") return {
5808
+ ...base,
5809
+ kind: "path",
5810
+ scalarKind: "path",
5811
+ mediaTypes: pathMediaTypes(node)
5812
+ };
5813
+ if (inner.scalar === "int" || inner.scalar === "float") return {
5814
+ ...base,
5815
+ kind: inner.scalar,
5816
+ scalarKind: inner.scalar,
5817
+ range: numericRange(node)
5818
+ };
5819
+ return {
5820
+ ...base,
5821
+ kind: "str",
5822
+ scalarKind: "str"
5823
+ };
5824
+ case "bool": return {
5825
+ ...base,
5826
+ kind: "bool"
5827
+ };
5828
+ case "count": return {
5829
+ ...base,
5830
+ kind: "count"
5831
+ };
5832
+ case "list": return {
5833
+ ...base,
5834
+ kind: "list",
5835
+ listBounds: listBounds(node),
5836
+ itemType: describeItem(inner.item, node)
5837
+ };
5838
+ case "struct": return {
5839
+ ...base,
5840
+ kind: "struct"
5841
+ };
5842
+ case "union": return {
5843
+ ...base,
5844
+ kind: "union"
5845
+ };
5846
+ default: return {
5847
+ ...base,
5848
+ kind: "str"
5849
+ };
5850
+ }
5851
+ }
5852
+ function describeItem(item, node) {
5853
+ const inner = item.kind === "optional" ? item.inner : item;
5854
+ const choices = literalChoices(inner, node);
5855
+ if (choices) return {
5856
+ kind: "enum",
5857
+ choices
5858
+ };
5859
+ switch (inner.kind) {
5860
+ case "scalar":
5861
+ if (inner.scalar === "path") return {
5862
+ kind: "path",
5863
+ scalarKind: "path",
5864
+ mediaTypes: pathMediaTypes(node)
5865
+ };
5866
+ return {
5867
+ kind: inner.scalar,
5868
+ scalarKind: inner.scalar
5869
+ };
5870
+ case "bool": return { kind: "bool" };
5871
+ case "count": return { kind: "count" };
5872
+ case "struct": return { kind: "struct" };
5873
+ case "union": return { kind: "union" };
5874
+ default: return { kind: "str" };
5875
+ }
5876
+ }
5877
+ /** Allowed values when `inner` is a literal or a union of literals (else undefined). */
5878
+ function literalChoices(inner, node) {
5879
+ if (inner.kind === "literal") return [inner.value];
5880
+ if (inner.kind === "union" && inner.variants.length > 0 && inner.variants.every((v) => v.type.kind === "literal")) return inner.variants.map((v) => v.type.value);
5881
+ const altNode = node ? findNode(node, (n) => n.kind === "alternative") : void 0;
5882
+ if (altNode && altNode.kind === "alternative") {
5883
+ const alts = altNode.attrs.alts;
5884
+ if (alts.length > 0 && alts.every((a) => a.kind === "literal")) return alts.map((a) => a.attrs.str);
5885
+ }
5886
+ }
5887
+ function numericRange(node) {
5888
+ const r = findRangeNode(node);
5889
+ if (!r) return void 0;
5890
+ const { minValue, maxValue } = r.attrs;
5891
+ if (minValue === void 0 && maxValue === void 0) return void 0;
5892
+ return {
5893
+ min: minValue,
5894
+ max: maxValue
5895
+ };
5896
+ }
5897
+ function listBounds(node) {
5898
+ const r = findRepeatNode(node);
5899
+ if (!r) return void 0;
5900
+ const { countMin, countMax } = r.attrs;
5901
+ if (countMin === void 0 && countMax === void 0) return void 0;
5902
+ return {
5903
+ min: countMin,
5904
+ max: countMax
5905
+ };
5906
+ }
5907
+ function pathMediaTypes(node) {
5908
+ const p = node ? findNode(node, (n) => n.kind === "path") : void 0;
5909
+ if (p && p.kind === "path") {
5910
+ const mt = p.attrs.mediaTypes;
5911
+ if (mt && mt.length > 0) return mt;
5912
+ }
5913
+ }
5914
+
5915
+ //#endregion
5916
+ //#region src/backend/nipype/nipype.ts
5917
+ /** Derive the per-tool nipype module/class names. */
5918
+ function nipypeNames(ctx) {
5919
+ const mod = appModuleName$1(ctx.app);
5920
+ const rawId = ctx.app?.id ?? "tool";
5921
+ const cls = pascalCase(/^[0-9]/.test(rawId) ? "v_" + rawId : rawId);
5922
+ return {
5923
+ styxStem: `_${mod}`,
5924
+ ifaceStem: mod,
5925
+ cls,
5926
+ inputSpec: `${cls}InputSpec`,
5927
+ outputSpec: `${cls}OutputSpec`
5928
+ };
5929
+ }
5930
+ /** Generate the nipype interface module source for one tool. */
5931
+ function generateNipype(ctx) {
5932
+ return emitNipypeInterface(ctx, buildTypedSpec(ctx), nipypeNames(ctx));
5933
+ }
5934
+ /**
5935
+ * Emits nipype `Interface` definitions whose typed InputSpec/OutputSpec carry
5936
+ * rich constraints (numeric ranges, list bounds, enum choices, file types) and
5937
+ * which delegate execution to the styx Python wrapper (Option B): no command-line
5938
+ * arg-building or output-path resolution is re-implemented here.
5939
+ *
5940
+ * Per tool, two co-located files are emitted so the output is a self-contained,
5941
+ * importable Python package: `_<tool>.py` (the styx Python module) and
5942
+ * `<tool>.py` (the interface, importing the wrapper via a relative import).
5943
+ */
5944
+ var NipypeBackend = class {
5945
+ name = "nipype";
5946
+ target = "nipype";
5947
+ emitApp(ctx, _scope) {
5948
+ const names = nipypeNames(ctx);
5949
+ const spec = buildTypedSpec(ctx);
5950
+ const warnings = [];
5951
+ if (!spec.rootIsStruct) warnings.push({ message: `nipype: '${ctx.app?.id ?? "?"}' has a non-struct root; emitted interface has no typed inputs and raises on run.` });
5952
+ const styxCode = generatePython(ctx);
5953
+ const ifaceCode = emitNipypeInterface(ctx, spec, names);
5954
+ return {
5955
+ meta: ctx.app,
5956
+ files: new Map([[`${names.styxStem}.py`, styxCode], [`${names.ifaceStem}.py`, ifaceCode]]),
5957
+ errors: [],
5958
+ warnings
5959
+ };
5960
+ }
5961
+ };
5962
+
5963
+ //#endregion
5964
+ //#region src/backend/pydra/emit.ts
5965
+ /** A scalar enum's Python type: int when every choice is numeric, else str. */
5966
+ function enumScalar(choices) {
5967
+ return (choices ?? []).every((c) => typeof c === "number") ? "int" : "str";
5968
+ }
5969
+ /** True when a parameter is a file path (a path scalar or a list of paths). */
5970
+ function isPathParam(p) {
5971
+ return p.kind === "path" || p.kind === "list" && p.itemType?.kind === "path";
5972
+ }
5973
+ function itemTypeStr(item, imp) {
5974
+ if (!item) {
5975
+ imp.typing = true;
5976
+ return "ty.Any";
5977
+ }
5978
+ switch (item.kind) {
5979
+ case "path":
5980
+ imp.file = true;
5981
+ return "File";
5982
+ case "int":
5983
+ case "count": return "int";
5984
+ case "float": return "float";
5985
+ case "str": return "str";
5986
+ case "bool": return "bool";
5987
+ case "enum": return enumScalar(item.choices);
5988
+ default:
5989
+ imp.typing = true;
5990
+ return "ty.Any";
5991
+ }
5992
+ }
5993
+ /** Base (non-optional) Python type expression for a parameter. */
5994
+ function baseType(p, imp) {
5995
+ switch (p.kind) {
5996
+ case "path":
5997
+ imp.file = true;
5998
+ return "File";
5999
+ case "int":
6000
+ case "count": return "int";
6001
+ case "float": return "float";
6002
+ case "str": return "str";
6003
+ case "bool": return "bool";
6004
+ case "enum": return enumScalar(p.choices);
6005
+ case "list": return `list[${itemTypeStr(p.itemType, imp)}]`;
6006
+ case "struct":
6007
+ case "union":
6008
+ imp.typing = true;
6009
+ return "ty.Any";
6010
+ }
6011
+ }
6012
+ /** Full input type, wrapping in `ty.Optional[...]` for an omittable-no-default field. */
6013
+ function inputType(p, imp) {
6014
+ const base = baseType(p, imp);
6015
+ if (p.optional && !p.hasDefault) {
6016
+ imp.typing = true;
6017
+ return `ty.Optional[${base}]`;
6018
+ }
6019
+ return base;
6020
+ }
6021
+ /** An attrs validator enforcing numeric range / list-length bounds, or undefined. */
6022
+ function validatorExpr(p, imp) {
6023
+ const checks = [];
6024
+ if (p.range) {
6025
+ if (p.range.min !== void 0) checks.push(`attrs.validators.ge(${renderPyLiteral(p.range.min)})`);
6026
+ if (p.range.max !== void 0) checks.push(`attrs.validators.le(${renderPyLiteral(p.range.max)})`);
6027
+ }
6028
+ if (p.listBounds) {
6029
+ if (p.listBounds.min !== void 0) checks.push(`attrs.validators.min_len(${p.listBounds.min})`);
6030
+ if (p.listBounds.max !== void 0) checks.push(`attrs.validators.max_len(${p.listBounds.max})`);
6031
+ }
6032
+ if (checks.length === 0) return void 0;
6033
+ imp.attrs = true;
6034
+ return `_styx_optional(${checks.length === 1 ? checks[0] : `attrs.validators.and_(${checks.join(", ")})`})`;
6035
+ }
6036
+ /** Help text: doc + degrade / media-type notes. */
6037
+ function helpText(p) {
6038
+ const parts = [];
6039
+ if (p.doc) parts.push(p.doc);
6040
+ if (p.kind === "struct" || p.kind === "union") parts.push("(nested configuration; pass a dict)");
6041
+ if (p.mediaTypes && p.mediaTypes.length > 0) parts.push(`(media types: ${p.mediaTypes.join(", ")})`);
6042
+ return parts.length > 0 ? parts.join(" ") : void 0;
6043
+ }
6044
+ /** Render a `python.arg(...)` field for one parameter. */
6045
+ function renderInputArg(p, imp) {
6046
+ const args = [`type=${inputType(p, imp)}`];
6047
+ if (p.mandatory) {} else if (p.optional && !p.hasDefault) args.push("default=None");
6048
+ else args.push(`default=${renderPyLiteral(p.default)}`);
6049
+ if (p.kind === "enum") args.push(`allowed_values=[${(p.choices ?? []).map((c) => renderPyLiteral(c)).join(", ")}]`);
6050
+ const v = validatorExpr(p, imp);
6051
+ if (v) args.push(`validator=${v}`);
6052
+ const h = helpText(p);
6053
+ if (h) args.push(`help=${pyStr(h)}`);
6054
+ return `python.arg(${args.join(", ")})`;
6055
+ }
6056
+ /** Python type for an output field. `isRoot` is the synthetic output directory. */
6057
+ function outputType(f, isRoot, imp) {
6058
+ if (f.shape.kind === "list") {
6059
+ imp.file = true;
6060
+ return "list[File]";
6061
+ }
6062
+ if (isRoot) {
6063
+ imp.directory = true;
6064
+ return "Directory";
6065
+ }
6066
+ imp.file = true;
6067
+ if (f.shape.optional) {
6068
+ imp.typing = true;
6069
+ return "ty.Optional[File]";
6070
+ }
6071
+ return "File";
6072
+ }
6073
+ function renderOutputArg(f, isRoot, imp) {
6074
+ const args = [`type=${outputType(f, isRoot, imp)}`];
6075
+ if (f.doc) args.push(`help=${pyStr(f.doc)}`);
6076
+ return `python.out(${args.join(", ")})`;
6077
+ }
6078
+ /**
6079
+ * Emit the pydra task module for one tool: a `@python.define` task whose typed
6080
+ * inputs/outputs carry rich constraints and whose body delegates execution to the
6081
+ * co-emitted styx Python wrapper (Option B). Targets the post-rewrite
6082
+ * `pydra.compose` API (pydra >= 1.0a, Python >= 3.11).
6083
+ */
6084
+ function emitPydraTask(ctx, spec, names) {
6085
+ const imp = {
6086
+ typing: false,
6087
+ attrs: false,
6088
+ file: false,
6089
+ directory: false
6090
+ };
6091
+ const inputEntries = spec.params.map((p) => ({
6092
+ key: p.hostName,
6093
+ expr: renderInputArg(p, imp)
6094
+ }));
6095
+ const outputEntries = spec.outputs.map((f, i) => ({
6096
+ key: f.id,
6097
+ expr: renderOutputArg(f, i === 0, imp)
6098
+ }));
6099
+ for (const s of spec.streams) outputEntries.push({
6100
+ key: s.id,
6101
+ expr: `python.out(type=list[str]${s.doc ? `, help=${pyStr(s.doc)}` : ""})`
6102
+ });
6103
+ const needsPath = spec.params.some(isPathParam);
6104
+ const cb = new CodeBuilder(" ");
6105
+ cb.comment("This file was auto generated by Styx.", "# ");
6106
+ cb.comment("Do not edit this file directly.", "# ");
6107
+ cb.comment("Targets the pydra.compose API (pydra >= 1.0a, Python >= 3.11).", "# ");
6108
+ cb.blank();
6109
+ if (needsPath) cb.line("import os");
6110
+ if (imp.typing) cb.line("import typing as ty");
6111
+ if (imp.attrs) cb.line("import attrs.validators");
6112
+ cb.line("from pydra.compose import python");
6113
+ const ff = [];
6114
+ if (imp.directory) ff.push("Directory");
6115
+ if (imp.file) ff.push("File");
6116
+ if (ff.length > 0) cb.line(`from fileformats.generic import ${ff.join(", ")}`);
6117
+ cb.blank();
6118
+ cb.line(`from .${names.styxStem} import ${spec.delegation.wrapperFn}`);
6119
+ cb.blank();
6120
+ cb.blank();
6121
+ if (imp.attrs) {
6122
+ cb.line("def _styx_optional(validator):");
6123
+ cb.indent(() => {
6124
+ cb.line("\"\"\"Apply an attrs validator only to a set (non-NOTHING), non-None value.\"\"\"");
6125
+ cb.line("def _check(instance, attribute, value):");
6126
+ cb.indent(() => {
6127
+ cb.line("if value is attrs.NOTHING or value is None:");
6128
+ cb.indent(() => cb.line("return"));
6129
+ cb.line("validator(instance, attribute, value)");
6130
+ });
6131
+ cb.line("return _check");
6132
+ });
6133
+ cb.blank();
6134
+ cb.blank();
6135
+ }
6136
+ if (needsPath) {
6137
+ cb.line("def _styx_path(value):");
6138
+ cb.indent(() => {
6139
+ cb.line("\"\"\"Convert fileformats / Path inputs into a styxdefs-accepted path.\"\"\"");
6140
+ cb.line("if value is None:");
6141
+ cb.indent(() => cb.line("return None"));
6142
+ cb.line("if isinstance(value, (list, tuple)):");
6143
+ cb.indent(() => cb.line("return [os.fspath(v) for v in value]"));
6144
+ cb.line("return os.fspath(value)");
6145
+ });
6146
+ cb.blank();
6147
+ cb.blank();
6148
+ }
6149
+ cb.line("@python.define(");
6150
+ cb.indent(() => {
6151
+ if (inputEntries.length === 0) cb.line("inputs={},");
6152
+ else {
6153
+ cb.line("inputs={");
6154
+ cb.indent(() => {
6155
+ for (const e of inputEntries) cb.line(`${pyStr(e.key)}: ${e.expr},`);
6156
+ });
6157
+ cb.line("},");
6158
+ }
6159
+ cb.line("outputs={");
6160
+ cb.indent(() => {
6161
+ for (const e of outputEntries) cb.line(`${pyStr(e.key)}: ${e.expr},`);
6162
+ });
6163
+ cb.line("},");
6164
+ });
6165
+ cb.line(")");
6166
+ const paramList = spec.params.map((p) => p.hostName);
6167
+ cb.line(`def ${names.cls}(${paramList.join(", ")}):`);
6168
+ cb.indent(() => {
6169
+ emitDocstring(cb, [ctx.app?.doc?.title, ctx.app?.doc?.description].filter(Boolean).join("\n\n") || void 0);
6170
+ if (!spec.rootIsStruct) {
6171
+ cb.line("raise NotImplementedError(");
6172
+ cb.indent(() => cb.line(pyStr("styx pydra backend: tools with a non-struct root are not supported.")));
6173
+ cb.line(")");
6174
+ return;
6175
+ }
6176
+ if (spec.params.length === 0) cb.line(`result = ${spec.delegation.wrapperFn}()`);
6177
+ else {
6178
+ cb.line(`result = ${spec.delegation.wrapperFn}(`);
6179
+ cb.indent(() => {
6180
+ for (const p of spec.params) {
6181
+ const expr = isPathParam(p) ? `_styx_path(${p.hostName})` : p.hostName;
6182
+ cb.line(`${p.hostName}=${expr},`);
6183
+ }
6184
+ });
6185
+ cb.line(")");
6186
+ }
6187
+ const returns = [...spec.outputs.map((f) => `result.${f.id}`), ...spec.streams.map((s) => `result.${s.id}`)];
6188
+ if (returns.length === 1) cb.line(`return ${returns[0]}`);
6189
+ else cb.line(`return ${returns.join(", ")}`);
6190
+ });
6191
+ return cb.toString();
6192
+ }
6193
+
6194
+ //#endregion
6195
+ //#region src/backend/pydra/pydra.ts
6196
+ /** Derive the per-tool pydra module/class names. */
6197
+ function pydraNames(ctx) {
6198
+ const mod = appModuleName$1(ctx.app);
6199
+ const rawId = ctx.app?.id ?? "tool";
6200
+ const safeId = /^[0-9]/.test(rawId) ? "v_" + rawId : rawId;
6201
+ return {
6202
+ styxStem: `_${mod}`,
6203
+ ifaceStem: mod,
6204
+ cls: pascalCase(safeId)
6205
+ };
6206
+ }
6207
+ /** Generate the pydra task module source for one tool. */
6208
+ function generatePydra(ctx) {
6209
+ return emitPydraTask(ctx, buildTypedSpec(ctx), pydraNames(ctx));
6210
+ }
6211
+ /**
6212
+ * Emits pydra tasks (`@python.define`, the post-rewrite pydra.compose API) whose
6213
+ * typed inputs/outputs carry rich constraints (numeric ranges and list bounds via
6214
+ * attrs validators, enum choices, file types, defaults) and which delegate
6215
+ * execution to the styx Python wrapper (Option B): no command-line arg-building
6216
+ * or output-path resolution is re-implemented here.
6217
+ *
6218
+ * Per tool, two co-located files are emitted so the output is a self-contained,
6219
+ * importable Python package: `_<tool>.py` (the styx Python module) and
6220
+ * `<tool>.py` (the task, importing the wrapper via a relative import).
6221
+ */
6222
+ var PydraBackend = class {
6223
+ name = "pydra";
6224
+ target = "pydra";
6225
+ emitApp(ctx, _scope) {
6226
+ const names = pydraNames(ctx);
6227
+ const spec = buildTypedSpec(ctx);
6228
+ const warnings = [];
6229
+ if (!spec.rootIsStruct) warnings.push({ message: `pydra: '${ctx.app?.id ?? "?"}' has a non-struct root; emitted task has no typed inputs and raises on run.` });
6230
+ const styxCode = generatePython(ctx);
6231
+ const taskCode = emitPydraTask(ctx, spec, names);
6232
+ return {
6233
+ meta: ctx.app,
6234
+ files: new Map([[`${names.styxStem}.py`, styxCode], [`${names.ifaceStem}.py`, taskCode]]),
6235
+ errors: [],
6236
+ warnings
6237
+ };
6238
+ }
6239
+ };
6240
+
5532
6241
  //#endregion
5533
6242
  //#region src/backend/schema/jsonschema.ts
5534
6243
  var SchemaBuilder = class {
@@ -5574,7 +6283,7 @@ var SchemaBuilder = class {
5574
6283
  case "literal": return { const: type.value };
5575
6284
  case "optional": return this.fromType(type.inner, node?.kind === "optional" ? node.attrs.node : void 0);
5576
6285
  case "list": {
5577
- const repeat = node?.kind === "repeat" ? node : void 0;
6286
+ const repeat = findRepeatNode(node);
5578
6287
  const schema = {
5579
6288
  type: "array",
5580
6289
  items: this.fromType(type.item, repeat?.attrs.node)
@@ -5584,7 +6293,7 @@ var SchemaBuilder = class {
5584
6293
  return schema;
5585
6294
  }
5586
6295
  case "struct": return this.structSchema(type, node);
5587
- case "union": return this.unionSchema(type);
6296
+ case "union": return this.unionSchema(type, node);
5588
6297
  }
5589
6298
  }
5590
6299
  findTerminal(node) {
@@ -5645,9 +6354,20 @@ var SchemaBuilder = class {
5645
6354
  if (required.length > 0) schema.required = required;
5646
6355
  return schema;
5647
6356
  }
5648
- unionSchema(type) {
6357
+ unionSchema(type, node) {
5649
6358
  if (type.variants.every((v) => v.type.kind === "literal")) return { enum: type.variants.map((v) => v.type.kind === "literal" ? v.type.value : "") };
5650
- return { oneOf: type.variants.map((v) => this.fromType(v.type)) };
6359
+ const altNode = findAlternativeNode(node);
6360
+ return { oneOf: type.variants.map((v, i) => {
6361
+ const schema = this.fromType(v.type, altNode?.attrs.alts[i]);
6362
+ if (v.type.kind === "struct" && "@type" in v.type.fields && schema.properties && !("@type" in schema.properties)) {
6363
+ schema.properties = {
6364
+ "@type": this.fromType(v.type.fields["@type"]),
6365
+ ...schema.properties
6366
+ };
6367
+ schema.required = ["@type", ...schema.required ?? []];
6368
+ }
6369
+ return schema;
6370
+ }) };
5651
6371
  }
5652
6372
  };
5653
6373
  function generateSchema(ctx) {
@@ -6899,7 +7619,7 @@ function computePublicNames(appId) {
6899
7619
  * (the `reg` registrations and the `sigScope` child), so passing the same scope
6900
7620
  * the emitter continues with keeps later local registrations consistent.
6901
7621
  */
6902
- function buildEmitModel(ctx, scope = new Scope(TS_RESERVED)) {
7622
+ function buildEmitModel$1(ctx, scope = new Scope(TS_RESERVED)) {
6903
7623
  const appId = ctx.app?.id;
6904
7624
  const pkg = ctx.package?.name ?? "unknown";
6905
7625
  const publicNames = computePublicNames(appId);
@@ -6942,7 +7662,7 @@ function buildEmitModel(ctx, scope = new Scope(TS_RESERVED)) {
6942
7662
  function generateTypeScript(ctx, packageScope) {
6943
7663
  const cb = new CodeBuilder(" ");
6944
7664
  const scope = packageScope ?? new Scope(TS_RESERVED);
6945
- const { appId, pkg, names, rootType, rootIsStruct, namedTypes, typeDecls, rootTypeTag, paramsType, sigEntries } = buildEmitModel(ctx, scope);
7665
+ const { appId, pkg, names, rootType, rootIsStruct, namedTypes, typeDecls, rootTypeTag, paramsType, sigEntries } = buildEmitModel$1(ctx, scope);
6946
7666
  cb.comment("This file was auto generated by Styx.");
6947
7667
  cb.comment("Do not edit this file directly.");
6948
7668
  cb.blank();
@@ -7125,7 +7845,7 @@ const tsDialect = {
7125
7845
  * @param opts - Import and package-root options.
7126
7846
  */
7127
7847
  function renderTypeScriptCall(ctx, config, opts = {}) {
7128
- const model = buildEmitModel(ctx);
7848
+ const model = buildEmitModel$1(ctx);
7129
7849
  const pkg = ctx.package?.name;
7130
7850
  const fnName = model.rootIsStruct ? model.names.execute : model.names.wrapper;
7131
7851
  const call = `${pkg ? `${pkg}.${fnName}` : fnName}(${model.rootIsStruct && model.rootType.kind === "struct" ? renderStructLiteral(config, model.rootType, "", tsDialect, model.rootTypeTag) : renderValue(config, model.rootType, "", tsDialect)})`;
@@ -8287,5 +9007,5 @@ function compile(source, filenameOrOptions) {
8287
9007
  }
8288
9008
 
8289
9009
  //#endregion
8290
- export { BoutiquesBackend, CodeBuilder, JsonSchemaBackend, PYTHON_RUNNER_DEPS, PassStatus, PythonBackend, STYXDEFS_COMPAT, Scope, TypeScriptBackend, alt, appEntrypoint, atomKey, buildSigEntries, camelCase, canonicalize, collectFieldInfo, collectNamedTypes, compactTokens, compile, compose, createContext, createPipeline, createRegistry, defaultNamingStrategy, defaultPipeline, detectFormat, effectiveOutputName, findDoc, findStructNode, fixpoint, flatten, float, format, formatSolveResult, generateBoutiques, generateOutputsSchema, generatePython, generateSchema, generateTypeScript, int, isGated, isIterated, isStructural, isTerminal, lit, nodeRef, opt, outputGate, pascalCase, path, planOutput, planScope, removeEmpty, renderPythonCall, renderStructLiteral, renderTypeScriptCall, renderValue, rep, repJoin, resolveFieldBinding, resolveOutputs, resolveTypeName, screamingSnakeCase, seq, seqJoin, simplify, snakeCase, solve, str, structKey, typeKey, unionKey };
9010
+ export { BoutiquesBackend, CodeBuilder, JsonSchemaBackend, NipypeBackend, PYTHON_RUNNER_DEPS, PassStatus, PydraBackend, PythonBackend, STYXDEFS_COMPAT, Scope, TypeScriptBackend, alt, appEntrypoint, atomKey, buildEmitModel, buildSigEntries, buildTypedSpec, camelCase, canonicalize, collectFieldInfo, collectNamedTypes, compactTokens, compile, compose, createContext, createPipeline, createRegistry, defaultNamingStrategy, defaultPipeline, detectFormat, effectiveOutputName, findDoc, findStructNode, fixpoint, flatten, float, format, formatSolveResult, generateBoutiques, generateNipype, generateOutputsSchema, generatePydra, generatePython, generateSchema, generateTypeScript, int, isGated, isIterated, isStructural, isTerminal, lit, nipypeNames, nodeRef, opt, outputGate, pascalCase, path, planOutput, planScope, pydraNames, removeEmpty, renderPythonCall, renderStructLiteral, renderTypeScriptCall, renderValue, rep, repJoin, resolveFieldBinding, resolveOutputs, resolveTypeName, screamingSnakeCase, seq, seqJoin, simplify, snakeCase, solve, str, structKey, typeKey, unionKey };
8291
9011
  //# sourceMappingURL=index.mjs.map