@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
@@ -372,12 +372,21 @@ export class BoutiquesParser implements Frontend {
372
372
  case InputTypePrimitive.Integer: {
373
373
  const node: Int = { kind: "int", attrs: {} };
374
374
  if (isNumber(btInput.minimum)) {
375
- node.attrs.minValue = Math.floor(btInput.minimum);
376
- if (btInput["exclusive-minimum"] === true) node.attrs.minValue += 1;
375
+ // Smallest valid integer: for a fractional bound, `>= 5.7` allows 6
376
+ // (ceil) and `> 5.7` allows 6 (floor + 1); both differ from a plain
377
+ // floor.
378
+ node.attrs.minValue =
379
+ btInput["exclusive-minimum"] === true
380
+ ? Math.floor(btInput.minimum) + 1
381
+ : Math.ceil(btInput.minimum);
377
382
  }
378
383
  if (isNumber(btInput.maximum)) {
379
- node.attrs.maxValue = Math.floor(btInput.maximum);
380
- if (btInput["exclusive-maximum"] === true) node.attrs.maxValue -= 1;
384
+ // Largest valid integer: `<= 5.7` allows 5 (floor) and `< 5.7` allows
385
+ // 5 (ceil - 1).
386
+ node.attrs.maxValue =
387
+ btInput["exclusive-maximum"] === true
388
+ ? Math.ceil(btInput.maximum) - 1
389
+ : Math.floor(btInput.maximum);
381
390
  }
382
391
  if (meta) node.meta = meta;
383
392
  return node;
@@ -424,7 +433,10 @@ export class BoutiquesParser implements Frontend {
424
433
  return null;
425
434
  }
426
435
  const node = this.parseDescriptor(nested);
427
- if (node && meta) node.meta = meta;
436
+ // Merge (not overwrite) so the subcommand's own meta - notably its
437
+ // `output-files`, attached by `parseDescriptor` - survives; the input's
438
+ // meta (name/doc) layers on top.
439
+ if (node && meta) node.meta = { ...node.meta, ...meta };
428
440
  return node;
429
441
  }
430
442
 
@@ -553,12 +565,19 @@ export class BoutiquesParser implements Frontend {
553
565
  }
554
566
 
555
567
  // Hoist metadata (doc, default) to outermost wrapper so backends find it
556
- // on the binding node. Keep name on inner for solver's findDeepName.
568
+ // on the binding node. Keep `name` on inner for the solver's findDeepName,
569
+ // and keep `outputs` there too: outputs are a scope concept declared on the
570
+ // subcommand struct, not a property of the list/flag wrapper - hoisting them
571
+ // onto a repeat would move the output scope off the struct and make backends
572
+ // (which resolve outputs against the struct) silently drop them.
557
573
  if (node !== inner && inner.meta) {
558
- const { name, ...rest } = inner.meta;
574
+ const { name, outputs, ...rest } = inner.meta;
559
575
  if (Object.keys(rest).length > 0) {
560
576
  node.meta = { ...node.meta, ...rest };
561
- inner.meta = name ? { name } : undefined;
577
+ const kept: NodeMeta = {};
578
+ if (name) kept.name = name;
579
+ if (outputs) kept.outputs = outputs;
580
+ inner.meta = Object.keys(kept).length > 0 ? kept : undefined;
562
581
  }
563
582
  }
564
583
 
@@ -38,6 +38,19 @@ export const canonicalize: Pass = {
38
38
  }
39
39
  }
40
40
 
41
+ // Identity for dedup: structure plus the meta that makes two structurally
42
+ // identical union arms semantically distinct - the variant tag (@type
43
+ // discriminant), name, and attached outputs. Deduping on structure alone
44
+ // would silently drop a distinct arm (e.g. two sub-commands with the same
45
+ // inner shape but different @type), making its variant unreachable.
46
+ function identityKey(node: Expr): string {
47
+ const m = node.meta;
48
+ const metaKey = m
49
+ ? [m.name ?? "", m.variantTag ?? "", m.outputs ? JSON.stringify(m.outputs) : ""].join("|")
50
+ : "";
51
+ return `${structuralHash(node)}#${metaKey}`;
52
+ }
53
+
41
54
  function sortKey(node: Expr): string {
42
55
  const name = node.meta?.name ?? "";
43
56
  return `${node.kind}:${name}:${structuralHash(node)}`;
@@ -51,13 +64,13 @@ export const canonicalize: Pass = {
51
64
  // Sort alternatives
52
65
  const sorted = [...children].sort((a, b) => sortKey(a).localeCompare(sortKey(b)));
53
66
 
54
- // Deduplicate by structural hash
67
+ // Deduplicate identical arms (structure + discriminating meta).
55
68
  const seen = new Set<string>();
56
69
  const alts: Expr[] = [];
57
70
  for (const child of sorted) {
58
- const hash = structuralHash(child);
59
- if (!seen.has(hash)) {
60
- seen.add(hash);
71
+ const key = identityKey(child);
72
+ if (!seen.has(key)) {
73
+ seen.add(key);
61
74
  alts.push(child);
62
75
  } else {
63
76
  changed = true;
@@ -67,7 +80,7 @@ export const canonicalize: Pass = {
67
80
  // Check if order changed
68
81
  if (
69
82
  alts.length !== children.length ||
70
- alts.some((alt, i) => structuralHash(alt) !== structuralHash(children[i]!))
83
+ alts.some((alt, i) => identityKey(alt) !== identityKey(children[i]!))
71
84
  ) {
72
85
  changed = true;
73
86
  }
@@ -127,15 +127,30 @@ export const simplify: Pass = {
127
127
  node.attrs.join === ""
128
128
  ) {
129
129
  changed = true;
130
- prev.attrs.str += child.attrs.str;
130
+ // Replace with a fresh node - `prev` is the original literal object
131
+ // (the `literal` case returns `node` unchanged), so mutating it in
132
+ // place would corrupt the caller's IR and double-append on rerun.
133
+ nodes[nodes.length - 1] = {
134
+ ...prev,
135
+ attrs: { ...prev.attrs, str: prev.attrs.str + child.attrs.str },
136
+ };
131
137
  } else {
132
138
  nodes.push(child);
133
139
  }
134
140
  }
135
141
 
136
- // seq(T) -> T, carrying the seq's meta down onto the child.
142
+ // seq(T) -> T, carrying the seq's meta down onto the child. Skip the
143
+ // collapse when the sequence declares outputs and the sole child is a
144
+ // literal: outputs need a scope binding, which the solver only forces
145
+ // on sequences (not on literals, which are never bound). Collapsing a
146
+ // parameterless output-bearing command (e.g. `seq(lit("run"))` with an
147
+ // output-file) onto its literal would orphan the output. A field child
148
+ // (str/path) does get bound, so those still collapse.
137
149
  if (nodes.length === 1) {
138
150
  const child = nodes[0]!;
151
+ if (node.meta?.outputs?.length && child.kind === "literal") {
152
+ return { ...node, attrs: { ...node.attrs, nodes } };
153
+ }
139
154
  changed = true;
140
155
  const mergedMeta = mergeMeta(node.meta, child.meta);
141
156
  return mergedMeta ? { ...child, meta: mergedMeta } : child;
@@ -45,11 +45,11 @@ function isBooleanLiteralPair(variants: Array<{ type: BoundType }>): boolean {
45
45
  return false;
46
46
  }
47
47
  const [a, b] = variants.map((v) => (v.type.kind === "literal" ? v.type.value : null));
48
+ // `literalFromNode` canonicalizes clean-int literals, so "0"/"1" arrive as the
49
+ // numbers 0/1 (never strings); only "false"/"true" survive as strings.
48
50
  return (
49
51
  (a === 0 && b === 1) ||
50
52
  (a === 1 && b === 0) ||
51
- (a === "0" && b === "1") ||
52
- (a === "1" && b === "0") ||
53
53
  (a === "false" && b === "true") ||
54
54
  (a === "true" && b === "false")
55
55
  );