@styx-api/core 0.1.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 (88) hide show
  1. package/dist/index.cjs +7947 -0
  2. package/dist/index.d.cts +1143 -0
  3. package/dist/index.d.cts.map +1 -0
  4. package/dist/index.d.mts +1143 -0
  5. package/dist/index.d.mts.map +1 -0
  6. package/dist/index.mjs +7877 -0
  7. package/dist/index.mjs.map +1 -0
  8. package/package.json +55 -0
  9. package/src/backend/backend.ts +95 -0
  10. package/src/backend/boutiques/boutiques.ts +1049 -0
  11. package/src/backend/boutiques/index.ts +1 -0
  12. package/src/backend/code-builder.ts +49 -0
  13. package/src/backend/collect-field-info.ts +50 -0
  14. package/src/backend/collect-named-types.ts +103 -0
  15. package/src/backend/collect-output-fields.ts +222 -0
  16. package/src/backend/find-doc.ts +38 -0
  17. package/src/backend/find-struct-node.ts +66 -0
  18. package/src/backend/index.ts +39 -0
  19. package/src/backend/python/arg-builder.ts +454 -0
  20. package/src/backend/python/emit.ts +638 -0
  21. package/src/backend/python/index.ts +9 -0
  22. package/src/backend/python/outputs-emit.ts +430 -0
  23. package/src/backend/python/packaging.ts +173 -0
  24. package/src/backend/python/python.ts +558 -0
  25. package/src/backend/python/snippet.ts +84 -0
  26. package/src/backend/python/typemap.ts +131 -0
  27. package/src/backend/python/types.ts +8 -0
  28. package/src/backend/python/validate-emit.ts +356 -0
  29. package/src/backend/resolve-field-binding.ts +41 -0
  30. package/src/backend/resolve-output-tokens.ts +80 -0
  31. package/src/backend/schema/index.ts +2 -0
  32. package/src/backend/schema/jsonschema.ts +303 -0
  33. package/src/backend/scope.ts +50 -0
  34. package/src/backend/sig-entries.ts +97 -0
  35. package/src/backend/snippet-core.ts +185 -0
  36. package/src/backend/string-case.ts +30 -0
  37. package/src/backend/styxdefs-compat.ts +21 -0
  38. package/src/backend/type-keys.ts +52 -0
  39. package/src/backend/typescript/arg-builder.ts +420 -0
  40. package/src/backend/typescript/emit.ts +450 -0
  41. package/src/backend/typescript/index.ts +10 -0
  42. package/src/backend/typescript/outputs-emit.ts +389 -0
  43. package/src/backend/typescript/packaging.ts +130 -0
  44. package/src/backend/typescript/snippet.ts +60 -0
  45. package/src/backend/typescript/typemap.ts +47 -0
  46. package/src/backend/typescript/types.ts +8 -0
  47. package/src/backend/typescript/typescript.ts +507 -0
  48. package/src/backend/typescript/validate-emit.ts +341 -0
  49. package/src/backend/union-variants.ts +42 -0
  50. package/src/backend/validate-walk.ts +111 -0
  51. package/src/bindings/binding.ts +77 -0
  52. package/src/bindings/format.ts +176 -0
  53. package/src/bindings/index.ts +16 -0
  54. package/src/bindings/output-gate.ts +50 -0
  55. package/src/bindings/resolved-output.ts +56 -0
  56. package/src/bindings/types.ts +16 -0
  57. package/src/frontend/argdump/index.ts +1 -0
  58. package/src/frontend/argdump/parser.ts +914 -0
  59. package/src/frontend/boutiques/destruct-template.ts +50 -0
  60. package/src/frontend/boutiques/index.ts +1 -0
  61. package/src/frontend/boutiques/parser.ts +676 -0
  62. package/src/frontend/boutiques/split-command.ts +69 -0
  63. package/src/frontend/detect-format.ts +42 -0
  64. package/src/frontend/frontend.ts +31 -0
  65. package/src/frontend/index.ts +9 -0
  66. package/src/frontend/workbench/index.ts +1 -0
  67. package/src/frontend/workbench/parser.ts +351 -0
  68. package/src/index.ts +41 -0
  69. package/src/ir/builders.ts +69 -0
  70. package/src/ir/format.ts +157 -0
  71. package/src/ir/index.ts +32 -0
  72. package/src/ir/meta.ts +91 -0
  73. package/src/ir/node.ts +95 -0
  74. package/src/ir/passes/canonicalize.ts +108 -0
  75. package/src/ir/passes/flatten.ts +73 -0
  76. package/src/ir/passes/index.ts +7 -0
  77. package/src/ir/passes/pass.ts +86 -0
  78. package/src/ir/passes/pipeline.ts +21 -0
  79. package/src/ir/passes/remove-empty.ts +76 -0
  80. package/src/ir/passes/simplify.ts +179 -0
  81. package/src/ir/types.ts +15 -0
  82. package/src/manifest/context.ts +36 -0
  83. package/src/manifest/index.ts +3 -0
  84. package/src/manifest/types.ts +15 -0
  85. package/src/solver/assign-access.ts +218 -0
  86. package/src/solver/index.ts +4 -0
  87. package/src/solver/resolve-outputs.ts +233 -0
  88. package/src/solver/solver.ts +319 -0
@@ -0,0 +1 @@
1
+ export { BoutiquesBackend, generateBoutiques } from "./boutiques.js";
@@ -0,0 +1,49 @@
1
+ /** Line-buffer abstraction for code emission. */
2
+ export class CodeBuilder {
3
+ private readonly lines: string[] = [];
4
+ private depth = 0;
5
+ private readonly indentStr: string;
6
+
7
+ constructor(indent = " ") {
8
+ this.indentStr = indent;
9
+ }
10
+
11
+ /** Append a line at the current indentation level. */
12
+ line(text: string): this {
13
+ this.lines.push(this.indentStr.repeat(this.depth) + text);
14
+ return this;
15
+ }
16
+
17
+ /** Append a blank line. */
18
+ blank(): this {
19
+ this.lines.push("");
20
+ return this;
21
+ }
22
+
23
+ /** Append a comment line. */
24
+ comment(text: string, prefix = "// "): this {
25
+ return this.line(`${prefix}${text}`);
26
+ }
27
+
28
+ /** Run a callback with increased indentation. */
29
+ indent(fn: () => void): this {
30
+ this.depth++;
31
+ fn();
32
+ this.depth--;
33
+ return this;
34
+ }
35
+
36
+ /** Append all lines from another CodeBuilder at the current indentation level. */
37
+ append(other: CodeBuilder): this {
38
+ const prefix = this.indentStr.repeat(this.depth);
39
+ for (const line of other.lines) {
40
+ this.lines.push(line === "" ? "" : prefix + line);
41
+ }
42
+ return this;
43
+ }
44
+
45
+ /** Return the built code as a string. */
46
+ toString(): string {
47
+ return this.lines.join("\n");
48
+ }
49
+ }
@@ -0,0 +1,50 @@
1
+ import type { BoundType } from "../bindings/index.js";
2
+ import type { CodegenContext } from "../manifest/index.js";
3
+ import { findDoc } from "./find-doc.js";
4
+ import { findStructNode } from "./find-struct-node.js";
5
+ import { resolveFieldBinding } from "./resolve-field-binding.js";
6
+
7
+ /**
8
+ * Metadata extracted for each field of a struct type.
9
+ *
10
+ * `doc` is the field's description, recovered from wrapper nodes via `findDoc`.
11
+ * `defaultValue` is pulled from the wrapper or binding node's metadata.
12
+ */
13
+ export interface FieldInfo {
14
+ doc?: string;
15
+ defaultValue?: string | number | boolean;
16
+ }
17
+
18
+ /**
19
+ * Collect field metadata (doc, defaultValue) for each field of a struct type.
20
+ *
21
+ * Walks the IR tree to find the sequence node containing the struct's fields,
22
+ * then resolves each child to its field binding. Metadata is recovered from both
23
+ * the wrapper node (where the parser hoists doc) and the binding node (where the
24
+ * solver places the binding after sequence collapse).
25
+ */
26
+ export function collectFieldInfo(
27
+ ctx: CodegenContext,
28
+ structType: Extract<BoundType, { kind: "struct" }>,
29
+ ): Map<string, FieldInfo> {
30
+ const info = new Map<string, FieldInfo>();
31
+
32
+ const structNode = findStructNode(ctx.expr, ctx, structType);
33
+ if (!structNode) return info;
34
+
35
+ for (const child of structNode.attrs.nodes) {
36
+ const match = resolveFieldBinding(child, ctx, structType);
37
+ if (!match) continue;
38
+ const { binding, wrapperNode } = match;
39
+ const fieldInfo: FieldInfo = {};
40
+ const fieldType = structType.fields[binding.name]!;
41
+ // Check wrapper node first (doc may be hoisted there), then binding node
42
+ const doc = findDoc(wrapperNode, fieldType) ?? findDoc(binding.node, fieldType);
43
+ if (doc) fieldInfo.doc = doc;
44
+ const defaultValue = wrapperNode.meta?.defaultValue ?? binding.node.meta?.defaultValue;
45
+ if (defaultValue !== undefined) fieldInfo.defaultValue = defaultValue;
46
+ info.set(binding.name, fieldInfo);
47
+ }
48
+
49
+ return info;
50
+ }
@@ -0,0 +1,103 @@
1
+ import type { BoundType } from "../bindings/index.js";
2
+ import { Scope } from "./scope.js";
3
+ import { structKey, unionKey } from "./type-keys.js";
4
+
5
+ /** A named compound type (struct or union) discovered during type collection. */
6
+ export interface NamedType {
7
+ name: string;
8
+ type: BoundType;
9
+ }
10
+
11
+ /**
12
+ * Recursively collect all struct/union types and assign unique names.
13
+ *
14
+ * Walks the BoundType tree depth-first, using structural keys (`structKey`,
15
+ * `unionKey`) to deduplicate types that appear multiple times in the tree.
16
+ * Each unique struct/union gets a name generated by applying `nameTransform`
17
+ * to a hint string (derived from the root name or field/variant names).
18
+ *
19
+ * @param rootType - The root BoundType to walk.
20
+ * @param rootName - The hint for the root type's name (already tool-qualified).
21
+ * @param scope - Symbol scope for collision avoidance.
22
+ * @param nameTransform - Converts a hint string to a type name (e.g. pascalCase, snake_case).
23
+ * @param nestedPrefix - Prepended to every NON-root type name so a suite's flat
24
+ * barrel (`export * from`/`from .x import *`) can't collide two tools' types
25
+ * that share a local field/variant name (e.g. `Outputtype`). Pass the tool's
26
+ * type prefix (its root name); defaults to "" for single-tool emission.
27
+ * @returns `namedTypes` maps structural keys to assigned names, `typeDecls` lists declarations in order.
28
+ */
29
+ export function collectNamedTypes(
30
+ rootType: BoundType,
31
+ rootName: string,
32
+ scope: Scope,
33
+ nameTransform: (hint: string) => string,
34
+ nestedPrefix = "",
35
+ ): { namedTypes: Map<string, string>; typeDecls: NamedType[] } {
36
+ const namedTypes = new Map<string, string>();
37
+ const typeDecls: NamedType[] = [];
38
+
39
+ function nameFor(hint: string, isRoot: boolean): string {
40
+ const cased = isRoot ? nameTransform(hint) : nestedPrefix + nameTransform(hint);
41
+ // Dedup in the cased (emitted) namespace, folding any disambiguation suffix
42
+ // back through nameTransform so a collision yields e.g. `Config2`, not the
43
+ // mixed-case `Config_2`. Uniqueness is still checked on the final form, so
44
+ // two hints that case-collide get distinct names.
45
+ return scope.add(cased, nameTransform);
46
+ }
47
+
48
+ function visit(type: BoundType, hint: string, isRoot: boolean): void {
49
+ switch (type.kind) {
50
+ case "struct": {
51
+ const key = structKey(type);
52
+ if (!namedTypes.has(key)) {
53
+ const name = nameFor(hint, isRoot);
54
+ namedTypes.set(key, name);
55
+ typeDecls.push({ name, type });
56
+ for (const [fieldName, fieldType] of Object.entries(type.fields)) {
57
+ visit(fieldType, fieldName, false);
58
+ }
59
+ }
60
+ break;
61
+ }
62
+ case "union": {
63
+ const key = unionKey(type);
64
+ if (!namedTypes.has(key)) {
65
+ const name = nameFor(hint, isRoot);
66
+ namedTypes.set(key, name);
67
+ typeDecls.push({ name, type });
68
+ for (const v of type.variants) {
69
+ visit(v.type, v.name ?? hint, false);
70
+ }
71
+ }
72
+ break;
73
+ }
74
+ // Wrappers are transparent: an optional/list at the root still names its
75
+ // inner type as the root (no prefix), matching the pre-prefix behavior.
76
+ case "optional":
77
+ visit(type.inner, hint, isRoot);
78
+ break;
79
+ case "list":
80
+ visit(type.item, hint, isRoot);
81
+ break;
82
+ default:
83
+ break;
84
+ }
85
+ }
86
+
87
+ visit(rootType, rootName, true);
88
+ return { namedTypes, typeDecls };
89
+ }
90
+
91
+ /**
92
+ * Create a type name resolver from a namedTypes map.
93
+ * Returns a function that maps a BoundType to its assigned name (if any).
94
+ */
95
+ export function resolveTypeName(
96
+ namedTypes: Map<string, string>,
97
+ ): (type: BoundType) => string | undefined {
98
+ return (type) => {
99
+ if (type.kind === "struct") return namedTypes.get(structKey(type));
100
+ if (type.kind === "union") return namedTypes.get(unionKey(type));
101
+ return undefined;
102
+ };
103
+ }
@@ -0,0 +1,222 @@
1
+ import type { BindingId, GateAtom, ResolvedOutput } from "../bindings/index.js";
2
+ import { outputGate } from "../bindings/index.js";
3
+ import type { Documentation, Expr } from "../ir/index.js";
4
+ import type { CodegenContext } from "../manifest/index.js";
5
+
6
+ /**
7
+ * Language-agnostic collection of a tool's Outputs object: the set of output
8
+ * files it produces (resolved outputs + mutable inputs surfaced as outputs)
9
+ * plus its captured stdout/stderr streams. Both the Python and TypeScript
10
+ * backends, and the JSON Schema backend, consume this single source of truth so
11
+ * the three describe the same Outputs shape (previously this logic was
12
+ * near-duplicated between the two `outputs-emit.ts` files). Identifier
13
+ * sanitization is the one language-specific concern, so callers pass an `idOf`
14
+ * mapping a raw output name to their target language's field identifier; the
15
+ * dedup space is keyed by that id, matching each backend's own field set.
16
+ */
17
+
18
+ /**
19
+ * A `ResolvedOutput` plus a `mutable` marker. A mutable input file is surfaced
20
+ * as an output: its single ref token points at the input binding, and the
21
+ * backend emits a writable-copy call (`mutable_copy` / `mutableCopy`) - the
22
+ * host path of the copy the runner staged for the matching mutable input -
23
+ * instead of `output_file` / `outputFile`.
24
+ */
25
+ export type EmittedOutput = ResolvedOutput & { mutable?: boolean };
26
+
27
+ /**
28
+ * The synthetic `root` output: the runner's output directory itself, surfaced as
29
+ * an always-present, non-gated `OutputPathType` field. Modeled as a regular
30
+ * `ResolvedOutput` with a single literal `"."` token so it flows through the
31
+ * exact same collection and emit machinery as every declared output - its empty
32
+ * gate makes it a required single, rendered as `output_file(".")` /
33
+ * `outputFile(".")`. Because every tool emits it, every tool returns a
34
+ * non-empty Outputs object (matching styx v1, which always carried `root`).
35
+ */
36
+ export function rootOutput(ctx: CodegenContext, idOf: (name: string) => string): EmittedOutput {
37
+ // Reserve a non-colliding field id. If a tool genuinely declares an output (or
38
+ // a mutable input surfaced as an output) whose id sanitizes to "root", the
39
+ // synthetic output-dir field dodges with a trailing "_" so it never silently
40
+ // clobbers that real output (or flips it optional via shape merging).
41
+ const taken = new Set<string>();
42
+ for (const scope of ctx.outputScopes) for (const o of scope.outputs) taken.add(idOf(o.name));
43
+ for (const o of collectMutableOutputs(ctx)) taken.add(idOf(o.name));
44
+ let name = "root";
45
+ while (taken.has(idOf(name))) name += "_";
46
+ return { name, tokens: [{ kind: "literal", value: "." }] };
47
+ }
48
+
49
+ /**
50
+ * Field shape for a single resolved output.
51
+ *
52
+ * - `single`: emitted at most once. Optional iff any `present`/`variant` atom
53
+ * appears in the gate (the value may be absent under that gate).
54
+ * - `list`: emitted once per element of an iterated binding (any `iter` atom in
55
+ * the gate). A gated list still types as a list - the empty list stands for
56
+ * "nothing produced".
57
+ */
58
+ export type OutputShape = { kind: "single"; optional: boolean } | { kind: "list" };
59
+
60
+ export function outputShape(gate: GateAtom[]): OutputShape {
61
+ const iter = gate.some((a) => a.kind === "iter");
62
+ if (iter) return { kind: "list" };
63
+ const optional = gate.some((a) => a.kind === "present" || a.kind === "variant");
64
+ return { kind: "single", optional };
65
+ }
66
+
67
+ /**
68
+ * Merge two shapes for outputs that share a field name across scopes/variants.
69
+ * Any iterated contributor makes the field a list; otherwise it is a single
70
+ * field that is optional if any contributor is gated.
71
+ */
72
+ export function mergeShape(a: OutputShape, b: OutputShape): OutputShape {
73
+ if (a.kind === "list" || b.kind === "list") return { kind: "list" };
74
+ return { kind: "single", optional: a.optional || b.optional };
75
+ }
76
+
77
+ /** One collected Outputs field, deduped across same-named outputs. */
78
+ export interface OutputField {
79
+ /** Backend-sanitized field identifier (via the caller's `idOf`). */
80
+ id: string;
81
+ /** First-seen raw output name (the descriptor's output id). */
82
+ name: string;
83
+ shape: OutputShape;
84
+ doc?: string;
85
+ }
86
+
87
+ /**
88
+ * Collect the unique Outputs fields in first-seen order, merging the shape and
89
+ * doc of any outputs that resolve to the same field id. Multiple scopes (e.g.
90
+ * the arms of a union output) routinely declare the same output name; without
91
+ * deduping a backend would emit duplicate fields (a Python SyntaxError, a TS
92
+ * duplicate member). `idOf` maps a raw output name to the target language's
93
+ * sanitized identifier, so two raw names that collapse to the same identifier
94
+ * merge into one field - exactly the field set the backend will emit.
95
+ */
96
+ export function collectOutputFields(
97
+ ctx: CodegenContext,
98
+ idOf: (name: string) => string,
99
+ ): OutputField[] {
100
+ const byId = new Map<string, OutputField>();
101
+ const add = (output: EmittedOutput, scopeGate: GateAtom[]): void => {
102
+ const gate = outputGate(scopeGate, output, ctx.bindings);
103
+ const shape = outputShape(gate);
104
+ const id = idOf(output.name);
105
+ const doc = output.doc?.description ?? output.doc?.title;
106
+ const existing = byId.get(id);
107
+ if (existing) {
108
+ existing.shape = mergeShape(existing.shape, shape);
109
+ if (!existing.doc && doc) existing.doc = doc;
110
+ } else {
111
+ byId.set(id, { id, name: output.name, shape, doc });
112
+ }
113
+ };
114
+ // The output directory itself, always present and ungated, listed first.
115
+ add(rootOutput(ctx, idOf), []);
116
+ for (const scope of ctx.outputScopes) {
117
+ const scopeBinding = ctx.bindings.get(scope.scope);
118
+ const scopeGate = scopeBinding?.gate ?? [];
119
+ for (const output of scope.outputs) add(output, scopeGate);
120
+ }
121
+ // Mutable inputs surface as outputs; their binding gate is absolute (rooted),
122
+ // so the scope gate is empty.
123
+ for (const output of collectMutableOutputs(ctx)) add(output, []);
124
+ return [...byId.values()];
125
+ }
126
+
127
+ /** Has any scope in the context attached at least one output? */
128
+ export function hasAnyOutputs(ctx: CodegenContext): boolean {
129
+ return ctx.outputScopes.some((s) => s.outputs.length > 0);
130
+ }
131
+
132
+ /** A captured stream (stdout/stderr), surfaced as a list-of-lines Outputs field. */
133
+ export interface StreamField {
134
+ /** First-seen raw stream name, bumped with a trailing `_` to dodge collisions. */
135
+ name: string;
136
+ /** Backend-sanitized identifier for `name` (via the caller's `idOf`). */
137
+ id: string;
138
+ doc?: string;
139
+ }
140
+
141
+ /**
142
+ * The stdout/stderr fields declared by the app metadata, in declaration order
143
+ * (stdout before stderr). Stream outputs are app-level: never gated (always
144
+ * present when the tool runs), so they bypass the solver/gating machinery and
145
+ * surface as plain list-of-string fields the wrapper appends to via the
146
+ * `handle_stdout` / `handle_stderr` (Python) or `handleStdout` / `handleStderr`
147
+ * (TS) callbacks. A stream whose sanitized id collides with a real output's id
148
+ * is bumped (raw name gains a trailing `_`, re-sanitized) so it never shadows a
149
+ * file output / emits a duplicate field.
150
+ */
151
+ export function streamFields(ctx: CodegenContext, idOf: (name: string) => string): StreamField[] {
152
+ const out: StreamField[] = [];
153
+ // Seed with the file/mutable output field ids so a stream whose name collides
154
+ // with a real output (e.g. an output literally named "stdout") is bumped
155
+ // rather than emitting a duplicate field / repeated constructor argument.
156
+ const used = new Set<string>(collectOutputFields(ctx, idOf).map((f) => f.id));
157
+ const add = (rawName: string, doc?: string): void => {
158
+ let name = rawName;
159
+ while (used.has(idOf(name))) name += "_";
160
+ used.add(idOf(name));
161
+ out.push({ name, id: idOf(name), doc });
162
+ };
163
+ const so = ctx.app?.stdout;
164
+ const se = ctx.app?.stderr;
165
+ if (so) add(so.name, so.doc?.description ?? so.doc?.title);
166
+ if (se) add(se.name, se.doc?.description ?? se.doc?.title);
167
+ return out;
168
+ }
169
+
170
+ /** Does the app declare any stdout/stderr stream output? */
171
+ export function hasStreamOutputs(ctx: CodegenContext): boolean {
172
+ return !!(ctx.app?.stdout || ctx.app?.stderr);
173
+ }
174
+
175
+ /**
176
+ * Synthesize one output per mutable file input. Each is a `ResolvedOutput` with
177
+ * a single ref token to the input binding and the `mutable` marker. The input
178
+ * binding's solver-assigned gate fully encodes its ancestry (optional/variant/
179
+ * iterated), so `outputGate([], ...)` yields the correct shape and gating for
180
+ * free - no scope bucket needed.
181
+ */
182
+ export function collectMutableOutputs(ctx: CodegenContext): EmittedOutput[] {
183
+ const out: EmittedOutput[] = [];
184
+ const seen = new Set<BindingId>();
185
+ const walk = (node: Expr, inheritedDoc?: Documentation): void => {
186
+ if (node.kind === "path") {
187
+ if (node.attrs.mutable) {
188
+ const binding = ctx.resolve(node);
189
+ if (binding && !seen.has(binding.id)) {
190
+ seen.add(binding.id);
191
+ const doc = node.meta?.doc ?? inheritedDoc;
192
+ out.push({
193
+ name: binding.name,
194
+ tokens: [{ kind: "ref", binding: binding.id }],
195
+ ...(doc && { doc }),
196
+ mutable: true,
197
+ });
198
+ }
199
+ }
200
+ return;
201
+ }
202
+ switch (node.kind) {
203
+ case "sequence":
204
+ for (const child of node.attrs.nodes) walk(child);
205
+ break;
206
+ case "optional":
207
+ case "repeat":
208
+ walk(node.attrs.node, node.meta?.doc ?? inheritedDoc);
209
+ break;
210
+ case "alternative":
211
+ for (const alt of node.attrs.alts) walk(alt, node.meta?.doc ?? inheritedDoc);
212
+ break;
213
+ }
214
+ };
215
+ walk(ctx.expr);
216
+ return out;
217
+ }
218
+
219
+ /** Does the tool have any mutable file input (surfaced as an output)? */
220
+ export function hasMutableInputs(ctx: CodegenContext): boolean {
221
+ return collectMutableOutputs(ctx).length > 0;
222
+ }
@@ -0,0 +1,38 @@
1
+ import type { BoundType } from "../bindings/index.js";
2
+ import type { Expr } from "../ir/index.js";
3
+
4
+ /**
5
+ * Find a description from an IR node, traversing through wrapper nodes.
6
+ *
7
+ * The parser's `wrapNode` hoists doc metadata to the outermost wrapper node,
8
+ * but the solver's simplify pass can collapse sequences, burying descriptions
9
+ * deeper in the tree.
10
+ *
11
+ * This traversal is type-aware: it only enters sequences when the corresponding
12
+ * BoundType is not a struct. Struct sequences have their own field collection
13
+ * call, so entering them would steal nested struct children's descriptions.
14
+ *
15
+ * @param node - The IR node to search for a description.
16
+ * @param fieldType - The BoundType of the field, used to determine traversal boundaries.
17
+ */
18
+ export function findDoc(node: Expr, fieldType: BoundType): string | undefined {
19
+ if (node.meta?.doc?.description) return node.meta.doc.description;
20
+ switch (node.kind) {
21
+ case "optional":
22
+ return findDoc(node.attrs.node, fieldType.kind === "optional" ? fieldType.inner : fieldType);
23
+ case "repeat":
24
+ return findDoc(node.attrs.node, fieldType.kind === "list" ? fieldType.item : fieldType);
25
+ case "sequence": {
26
+ // Only traverse into sequences that were collapsed (non-struct field types).
27
+ // Struct sequences have their own collectFieldInfo call for their children.
28
+ if (fieldType.kind === "struct") return undefined;
29
+ for (const child of node.attrs.nodes) {
30
+ const doc = findDoc(child, fieldType);
31
+ if (doc) return doc;
32
+ }
33
+ return undefined;
34
+ }
35
+ default:
36
+ return undefined;
37
+ }
38
+ }
@@ -0,0 +1,66 @@
1
+ import type { BoundType } from "../bindings/index.js";
2
+ import type { Expr } from "../ir/index.js";
3
+ import type { CodegenContext } from "../manifest/index.js";
4
+ import { resolveFieldBinding } from "./resolve-field-binding.js";
5
+
6
+ /**
7
+ * Find the sequence node whose child bindings match a struct type's fields.
8
+ *
9
+ * Traverses through optional, repeat, and alternative wrappers to find the
10
+ * sequence that directly contains the struct's field bindings. This is necessary
11
+ * because the solver may collapse `seq(lit("--flag"), terminal)` into the terminal,
12
+ * burying bindings deeper in the tree.
13
+ *
14
+ * Uses a two-phase check for sequences:
15
+ * 1. Direct binding check (`ctx.resolve`) - matches when bindings are on immediate children
16
+ * 2. Recursive binding check (`resolveFieldBinding`) - matches when solver collapsed
17
+ * a seq(lit, terminal) and the binding is buried deeper
18
+ *
19
+ * Phase 1 is tried first to avoid falsely matching an outer sequence when an inner
20
+ * sequence is the actual struct owner (e.g. `seq(lit("--flag"), seq(field1, field2))`).
21
+ */
22
+ export function findStructNode(
23
+ node: Expr,
24
+ ctx: CodegenContext,
25
+ structType: Extract<BoundType, { kind: "struct" }>,
26
+ ): Extract<Expr, { kind: "sequence" }> | undefined {
27
+ switch (node.kind) {
28
+ case "sequence": {
29
+ // Phase 1: Check if any direct child has a binding matching a struct field
30
+ for (const child of node.attrs.nodes) {
31
+ const binding = ctx.resolve(child);
32
+ if (
33
+ binding &&
34
+ binding.name in structType.fields &&
35
+ binding.type === structType.fields[binding.name]
36
+ ) {
37
+ return node;
38
+ }
39
+ }
40
+ // Recurse into child nodes first (prefer deeper matches)
41
+ for (const child of node.attrs.nodes) {
42
+ const result = findStructNode(child, ctx, structType);
43
+ if (result) return result;
44
+ }
45
+ // Phase 2: Check via resolveFieldBinding for collapsed sequences
46
+ // where bindings are buried inside collapsed seq(lit, terminal)
47
+ for (const child of node.attrs.nodes) {
48
+ if (resolveFieldBinding(child, ctx, structType)) return node;
49
+ }
50
+ return undefined;
51
+ }
52
+ case "optional":
53
+ return findStructNode(node.attrs.node, ctx, structType);
54
+ case "repeat":
55
+ return findStructNode(node.attrs.node, ctx, structType);
56
+ case "alternative": {
57
+ for (const alt of node.attrs.alts) {
58
+ const result = findStructNode(alt, ctx, structType);
59
+ if (result) return result;
60
+ }
61
+ return undefined;
62
+ }
63
+ default:
64
+ return undefined;
65
+ }
66
+ }
@@ -0,0 +1,39 @@
1
+ export type {
2
+ Backend,
3
+ EmitError,
4
+ EmitResult,
5
+ EmittedApp,
6
+ EmittedPackage,
7
+ EmitWarning,
8
+ TypeMap,
9
+ } from "./backend.js";
10
+ export { BoutiquesBackend, generateBoutiques } from "./boutiques/index.js";
11
+ export { CodeBuilder } from "./code-builder.js";
12
+ export type { FieldInfo } from "./collect-field-info.js";
13
+ export { collectFieldInfo } from "./collect-field-info.js";
14
+ export type { NamedType } from "./collect-named-types.js";
15
+ export { collectNamedTypes, resolveTypeName } from "./collect-named-types.js";
16
+ export { findDoc } from "./find-doc.js";
17
+ export { findStructNode } from "./find-struct-node.js";
18
+ export { resolveFieldBinding } from "./resolve-field-binding.js";
19
+ export type { OutputEmitPlan } from "./resolve-output-tokens.js";
20
+ export {
21
+ compactTokens,
22
+ isGated,
23
+ isIterated,
24
+ outputGate,
25
+ planOutput,
26
+ planScope,
27
+ } from "./resolve-output-tokens.js";
28
+ export { generatePython, PythonBackend, renderPythonCall } from "./python/index.js";
29
+ export { Scope } from "./scope.js";
30
+ export type { JsonSchema } from "./schema/index.js";
31
+ export { generateOutputsSchema, generateSchema, JsonSchemaBackend } from "./schema/index.js";
32
+ export { buildSigEntries } from "./sig-entries.js";
33
+ export type { SigEntry, SigOptions } from "./sig-entries.js";
34
+ export { renderStructLiteral, renderValue } from "./snippet-core.js";
35
+ export type { SnippetDialect, SnippetOptions } from "./snippet-core.js";
36
+ export { camelCase, pascalCase, screamingSnakeCase, snakeCase } from "./string-case.js";
37
+ export { PYTHON_RUNNER_DEPS, STYXDEFS_COMPAT } from "./styxdefs-compat.js";
38
+ export { structKey, typeKey, unionKey } from "./type-keys.js";
39
+ export { generateTypeScript, renderTypeScriptCall, TypeScriptBackend } from "./typescript/index.js";