feat: decode composite step inputs

This commit is contained in:
lda
2026-08-13 19:49:39 +07:00 Verified
parent 938864b4d2
commit 1bb684bfcd
22 changed files with 1339 additions and 119 deletions
@@ -6,6 +6,10 @@ import {
workflowRpcOperationNames,
workflowOperationNames,
type OperationName,
type InputExpression,
type InputExpressionBinding,
type InputPathBinding,
type InputValueBinding,
type WorkflowOperationParams,
type WorkflowOperationResult,
} from "../index.js";
@@ -75,4 +79,30 @@ describe("generated workflow contract", () => {
store_root: string;
}>();
});
it("exposes recursive node-local inputs without widening workflow outputs", () => {
const expressionBinding = {
target: "request",
expression: {
kind: "object",
fields: {
value: { kind: "path", path: "state.foo" },
label: { kind: "literal", value: "wowcool" },
},
},
} satisfies InputExpressionBinding;
const expression: InputExpression = expressionBinding.expression;
expect(expression.kind).toBe("object");
expectTypeOf<
WorkflowOperationParams<"workflow.draft_workspaces.set_step_input_bindings">
>().toMatchTypeOf<{
bindings: Array<InputPathBinding | InputValueBinding | InputExpressionBinding>;
}>();
expectTypeOf<
WorkflowOperationParams<"workflow.draft_workspaces.set_step_output_bindings">
>().toMatchTypeOf<{
bindings: Array<{ source: string; target: string }>;
}>();
});
});
@@ -173,6 +173,11 @@ export type JsonValue =
[k: string]: JsonValue;
}
| null;
/**
* This interface was referenced by `WorkflowContractMap`'s JSON-Schema
* via the `definition` "InputExpression".
*/
export type InputExpression = LiteralExpression | PathExpression | ArrayExpression | ObjectExpression;
/**
* This interface was referenced by `WorkflowContractMap`'s JSON-Schema
* via the `definition` "CompileDraftWorkspaceResult".
@@ -444,7 +449,7 @@ export interface WorkflowContractMap {
input_map?: {
[k: string]: string;
} | null;
input_bindings?: (InputPathBinding | InputValueBinding)[] | null;
input_bindings?: (InputPathBinding | InputValueBinding | InputExpressionBinding)[] | null;
bind_outputs?: {
[k: string]: string;
};
@@ -535,8 +540,8 @@ export interface WorkflowContractMap {
output_schema?: {
[k: string]: unknown;
} | null;
input?: unknown[] | null;
output?: unknown[] | null;
input?: (InputPathBinding | InputValueBinding | InputExpressionBinding)[] | null;
output?: OutputBinding[] | null;
input_map?: {
[k: string]: string;
} | null;
@@ -688,7 +693,7 @@ export interface WorkflowContractMap {
workspace_id: string;
revision: number;
step_id: string;
bindings: (InputPathBinding | InputValueBinding)[];
bindings: (InputPathBinding | InputValueBinding | InputExpressionBinding)[];
};
result: DraftWorkspaceResult;
};
@@ -774,8 +779,8 @@ export interface WorkflowContractMap {
output_schema?: {
[k: string]: unknown;
} | null;
input?: unknown[] | null;
output?: unknown[] | null;
input?: (InputPathBinding | InputValueBinding | InputExpressionBinding)[] | null;
output?: OutputBinding[] | null;
input_map?: {
[k: string]: string;
} | null;
@@ -1493,7 +1498,7 @@ export interface DraftUseStep {
/**
* Canonical input bindings for this capability. Use path bindings for graph-to-local input and value bindings for literals.
*/
input?: (InputPathBinding | InputValueBinding)[];
input?: (InputPathBinding | InputValueBinding | InputExpressionBinding)[];
/**
* Canonical output bindings from node-local output paths to workflow state destinations.
*/
@@ -1566,6 +1571,80 @@ export interface InputValueBinding {
}
| null;
}
/**
* Assign one recursively composed expression to a node-local target.
*
* This interface was referenced by `WorkflowContractMap`'s JSON-Schema
* via the `definition` "InputExpressionBinding".
*/
export interface InputExpressionBinding {
expression: InputExpression;
/**
* Node-local path. Use the root marker `.` for the whole payload.
*/
target:
| string
| {
/**
* @minItems 0
*/
parts: string[];
root: "local";
};
}
/**
* A strict JSON literal embedded in a node-local input expression.
*
* This interface was referenced by `WorkflowContractMap`'s JSON-Schema
* via the `definition` "LiteralExpression".
*/
export interface LiteralExpression {
kind: "literal";
value: JsonValue;
}
/**
* Read one graph source path while resolving a composite input.
*
* This interface was referenced by `WorkflowContractMap`'s JSON-Schema
* via the `definition` "PathExpression".
*/
export interface PathExpression {
kind: "path";
/**
* Readable graph path rooted at input, state, or context.
*/
path:
| string
| {
/**
* @minItems 0
*/
parts: string[];
root: "input" | "state" | "context";
};
}
/**
* Resolve ordered child expressions into one JSON array.
*
* This interface was referenced by `WorkflowContractMap`'s JSON-Schema
* via the `definition` "ArrayExpression".
*/
export interface ArrayExpression {
items: InputExpression[];
kind: "array";
}
/**
* Resolve named child expressions into one JSON object.
*
* This interface was referenced by `WorkflowContractMap`'s JSON-Schema
* via the `definition` "ObjectExpression".
*/
export interface ObjectExpression {
fields: {
[k: string]: InputExpression;
};
kind: "object";
}
/**
* Map one node-local output path into one workflow state path.
*
@@ -1689,7 +1768,7 @@ export interface DraftInterruptStep {
export interface DraftInterruptPayload {
kind: string;
outcomes?: string[];
request?: (InputPathBinding | InputValueBinding)[];
request?: (InputPathBinding | InputValueBinding | InputExpressionBinding)[];
request_schema?: SchemaRef | null;
resume?: OutputBinding[];
resume_schema?: SchemaRef | null;
@@ -1936,7 +2015,7 @@ export interface DraftSubgraphStep {
*/
export interface DraftSubgraphPayload {
desc?: string | null;
input?: (InputPathBinding | InputValueBinding)[];
input?: (InputPathBinding | InputValueBinding | InputExpressionBinding)[];
input_schema?: SchemaRef;
/**
* @minItems 1
@@ -2135,7 +2214,7 @@ export interface ListDraftWorkspacesResult {
*/
export interface CapabilityStepUpdate {
desc?: string | null;
input?: (InputPathBinding | InputValueBinding)[] | null;
input?: (InputPathBinding | InputValueBinding | InputExpressionBinding)[] | null;
retry?: number | null;
timeout_seconds?: number | null;
}
@@ -2592,6 +2671,27 @@ export type WorkflowOperationResult<Name extends WorkflowOperationName> =
// Runtime JSON Schema is limited to parity-verified authored RPCs.
export const workflowRuntimeContract = {
"components": {
"ArrayExpression": {
"additionalProperties": false,
"description": "Resolve ordered child expressions into one JSON array.",
"properties": {
"items": {
"items": {
"$ref": "#/components/schemas/InputExpression"
},
"type": "array"
},
"kind": {
"const": "array",
"type": "string"
}
},
"required": [
"kind",
"items"
],
"type": "object"
},
"ArtifactCatalogEntryPayload": {
"description": "Compact artifact row returned by discovery operations.",
"properties": {
@@ -2776,9 +2876,11 @@ export const workflowRuntimeContract = {
},
{
"$ref": "#/components/schemas/InputValueBinding"
},
{
"$ref": "#/components/schemas/InputExpressionBinding"
}
],
"description": "Canonical node input binding. Use either a path binding with `path`, or a literal binding with `value`; do not provide both."
]
},
"type": "array"
},
@@ -3085,6 +3187,77 @@ export const workflowRuntimeContract = {
],
"type": "object"
},
"InputExpression": {
"discriminator": {
"mapping": {
"array": "#/components/schemas/ArrayExpression",
"literal": "#/components/schemas/LiteralExpression",
"object": "#/components/schemas/ObjectExpression",
"path": "#/components/schemas/PathExpression"
},
"propertyName": "kind"
},
"oneOf": [
{
"$ref": "#/components/schemas/LiteralExpression"
},
{
"$ref": "#/components/schemas/PathExpression"
},
{
"$ref": "#/components/schemas/ArrayExpression"
},
{
"$ref": "#/components/schemas/ObjectExpression"
}
]
},
"InputExpressionBinding": {
"additionalProperties": false,
"description": "Assign one recursively composed expression to a node-local target.",
"properties": {
"expression": {
"$ref": "#/components/schemas/InputExpression"
},
"target": {
"description": "Node-local path. Use the root marker `.` for the whole payload.",
"anyOf": [
{
"description": "Canonical TOML-key path string.",
"type": "string"
},
{
"additionalProperties": false,
"description": "Structural path object accepted as input.",
"properties": {
"parts": {
"items": {
"minLength": 1,
"type": "string"
},
"minItems": 0,
"type": "array"
},
"root": {
"const": "local",
"type": "string"
}
},
"required": [
"root",
"parts"
],
"type": "object"
}
]
}
},
"required": [
"target",
"expression"
],
"type": "object"
},
"InputPathBinding": {
"additionalProperties": false,
"description": "Map one workflow graph source path into one node-local input path.",
@@ -3522,6 +3695,24 @@ export const workflowRuntimeContract = {
],
"type": "object"
},
"LiteralExpression": {
"additionalProperties": false,
"description": "A strict JSON literal embedded in a node-local input expression.",
"properties": {
"kind": {
"const": "literal",
"type": "string"
},
"value": {
"$ref": "#/components/schemas/JsonValue"
}
},
"required": [
"kind",
"value"
],
"type": "object"
},
"NextActionPatchExamplePayload": {
"description": "Concrete follow-up operation suggested to an API caller.",
"properties": {
@@ -3710,6 +3901,27 @@ export const workflowRuntimeContract = {
],
"type": "object"
},
"ObjectExpression": {
"additionalProperties": false,
"description": "Resolve named child expressions into one JSON object.",
"properties": {
"fields": {
"additionalProperties": {
"$ref": "#/components/schemas/InputExpression"
},
"type": "object"
},
"kind": {
"const": "object",
"type": "string"
}
},
"required": [
"kind",
"fields"
],
"type": "object"
},
"OutputBinding": {
"additionalProperties": false,
"description": "Map one node-local output path into one workflow state path.",
@@ -3785,6 +3997,57 @@ export const workflowRuntimeContract = {
],
"type": "object"
},
"PathExpression": {
"additionalProperties": false,
"description": "Read one graph source path while resolving a composite input.",
"properties": {
"kind": {
"const": "path",
"type": "string"
},
"path": {
"description": "Readable graph path rooted at input, state, or context.",
"anyOf": [
{
"description": "Canonical TOML-key path string.",
"type": "string"
},
{
"additionalProperties": false,
"description": "Structural path object accepted as input.",
"properties": {
"parts": {
"items": {
"minLength": 1,
"type": "string"
},
"minItems": 0,
"type": "array"
},
"root": {
"enum": [
"input",
"state",
"context"
],
"type": "string"
}
},
"required": [
"root",
"parts"
],
"type": "object"
}
]
}
},
"required": [
"kind",
"path"
],
"type": "object"
},
"RequiredCapabilityPayload": {
"description": "Saved dependency contract for one artifact capability reference.",
"properties": {
@@ -5206,9 +5469,11 @@ export const workflowRuntimeContract = {
},
{
"$ref": "#/components/schemas/InputValueBinding"
},
{
"$ref": "#/components/schemas/InputExpressionBinding"
}
],
"description": "Canonical node input binding. Use either a path binding with `path`, or a literal binding with `value`; do not provide both."
]
},
"type": "array"
},
@@ -5422,7 +5687,19 @@ export const workflowRuntimeContract = {
"input": {
"anyOf": [
{
"items": {},
"items": {
"anyOf": [
{
"$ref": "#/components/schemas/InputPathBinding"
},
{
"$ref": "#/components/schemas/InputValueBinding"
},
{
"$ref": "#/components/schemas/InputExpressionBinding"
}
]
},
"type": "array"
},
{
@@ -5434,7 +5711,9 @@ export const workflowRuntimeContract = {
"output": {
"anyOf": [
{
"items": {},
"items": {
"$ref": "#/components/schemas/OutputBinding"
},
"type": "array"
},
{
@@ -5586,9 +5865,11 @@ export const workflowRuntimeContract = {
},
{
"$ref": "#/components/schemas/InputValueBinding"
},
{
"$ref": "#/components/schemas/InputExpressionBinding"
}
],
"description": "Canonical node input binding. Use either a path binding with `path`, or a literal binding with `value`; do not provide both."
]
},
"type": "array"
}
@@ -295,10 +295,84 @@ const InputValueBindingSchema = Schema.Struct({
value: JsonValueSchema,
});
type GraphSourcePath =
| string
| {
readonly root: "input" | "state" | "context";
readonly parts: ReadonlyArray<string>;
};
type InputExpression =
| { readonly kind: "literal"; readonly value: JsonValue }
| { readonly kind: "path"; readonly path: GraphSourcePath }
| { readonly kind: "array"; readonly items: ReadonlyArray<InputExpression> }
| { readonly kind: "object"; readonly fields: Readonly<Record<string, InputExpression>> };
const GraphSourcePathSchema = Schema.Union(
Schema.String,
Schema.Struct({
parts: StructuralPathPartsSchema,
root: Schema.Literal("input", "state", "context"),
}),
);
const LiteralExpressionSchema = Schema.Struct({
kind: Schema.Literal("literal"),
value: JsonValueSchema,
});
const PathExpressionSchema = Schema.Struct({
kind: Schema.Literal("path"),
path: GraphSourcePathSchema,
});
const ArrayExpressionSchema = Schema.Struct({
kind: Schema.Literal("array"),
items: Schema.Array(
Schema.suspend(
(): Schema.Schema<InputExpression, unknown, never> => InputExpressionSchema,
),
),
});
const ObjectExpressionSchema = Schema.Struct({
kind: Schema.Literal("object"),
fields: Schema.Record({
key: Schema.String,
value: Schema.suspend(
(): Schema.Schema<InputExpression, unknown, never> => InputExpressionSchema,
),
}),
});
// Keep the recursive authored decoder separate from the generated contract so
// parity tests can catch drift in either direction.
const InputExpressionSchema: Schema.Schema<InputExpression, unknown, never> =
Schema.suspend(
(): Schema.Schema<InputExpression, unknown, never> =>
Schema.Union(
LiteralExpressionSchema,
PathExpressionSchema,
ArrayExpressionSchema,
ObjectExpressionSchema,
),
);
const InputExpressionBindingSchema = Schema.Struct({
target: Schema.Union(
Schema.String,
Schema.Struct({
parts: StructuralPathPartsSchema,
root: Schema.Literal("local"),
}),
),
expression: InputExpressionSchema,
});
const InputBindingSchema = Schema.Union(
InputPathBindingSchema,
InputValueBindingSchema,
);
const StepInputBindingSchema = Schema.Union(
InputPathBindingSchema,
InputValueBindingSchema,
InputExpressionBindingSchema,
);
const OutputBindingSchema = Schema.Struct({
source: Schema.Union(
@@ -319,7 +393,7 @@ const OutputBindingSchema = Schema.Struct({
const CapabilityStepUpdateSchema = Schema.Struct({
desc: Schema.optional(Schema.NullOr(Schema.String.pipe(Schema.minLength(1)))),
input: Schema.optional(Schema.NullOr(Schema.Array(InputBindingSchema))),
input: Schema.optional(Schema.NullOr(Schema.Array(StepInputBindingSchema))),
retry: Schema.optional(Schema.NullOr(NonNegativeIntegerSchema)),
timeout_seconds: Schema.optional(Schema.NullOr(PositiveIntegerSchema)),
});
@@ -446,7 +520,7 @@ export const authoredRpcSchemas = {
Schema.NullOr(Schema.Record({ key: Schema.String, value: Schema.String })),
),
input_bindings: Schema.optional(
Schema.NullOr(Schema.Array(InputBindingSchema)),
Schema.NullOr(Schema.Array(StepInputBindingSchema)),
),
bind_outputs: Schema.optional(
Schema.Record({ key: Schema.String, value: Schema.String }),
@@ -481,7 +555,7 @@ export const authoredRpcSchemas = {
workspace_id: Schema.String.pipe(Schema.minLength(1)),
revision: PositiveIntegerSchema,
step_id: Schema.String.pipe(Schema.minLength(1)),
bindings: Schema.Array(InputBindingSchema),
bindings: Schema.Array(StepInputBindingSchema),
}),
success: DraftWorkspaceSchema,
},
@@ -492,8 +492,8 @@ const parityCases: ReadonlyArray<ParityCase> = [
input_schema: { type: "object" },
state_schema: { type: "object" },
output_schema: { type: "object" },
input: [{ text: "hello" }],
output: [{ text: "state.text" }],
input: [{ path: "input.text", target: "text" }],
output: [{ source: "text", target: "state.text" }],
input_map: { "input.text": "text" },
output_map: { text: "state.text" },
error_message_source: "state.error_message",
@@ -1096,6 +1096,58 @@ describe("authored RPC and manifest schema parity", () => {
})).toBe(false);
});
it("accepts nested authored expressions and rejects over-specified variants", () => {
const basePayload = {
workspace_id: "console.demo",
revision: 3,
step_id: "concat",
};
const expression = {
kind: "object",
fields: {
items: {
kind: "array",
items: [
{ kind: "path", path: "state.foo" },
{ kind: "literal", value: "wowcool" },
],
},
separator: { kind: "literal", value: " " },
},
};
expect(
accepts(WorkflowDraftWorkspacesSetStepInputBindingsPayloadSchema, {
...basePayload,
bindings: [{ target: "request", expression }],
}),
).toBe(true);
expect(
accepts(authoredRpcSchemas["workflow.draft_workspaces.set_step_input_bindings"].payload, {
...basePayload,
bindings: [{ target: "request", expression }],
}),
).toBe(true);
expect(
accepts(WorkflowDraftWorkspacesSetStepInputBindingsPayloadSchema, {
...basePayload,
bindings: [{ target: "request", expression: { ...expression, extra: true } }],
}),
).toBe(false);
expect(
accepts(authoredRpcSchemas["workflow.draft_workspaces.set_step_input_bindings"].payload, {
...basePayload,
bindings: [{ target: "request", expression: { ...expression, extra: true } }],
}),
).toBe(false);
expect(
accepts(authoredRpcSchemas["workflow.draft_workspaces.set_step_output_bindings"].payload, {
...basePayload,
bindings: [{ target: "state.output", expression }],
}),
).toBe(false);
});
it("catalogs every authored RPC exactly once", () => {
const expectedMethods = [
"workflow.health",
@@ -1143,6 +1195,7 @@ describe("authored RPC and manifest schema parity", () => {
it("reports the exact remaining translator blockers", () => {
expect(parityReport().blockers).toEqual([
"workflow.draft_workspaces.create_from_capability:payload:oneOf@#/components/schemas/InputPathBinding.properties.path",
"workflow.draft_workspaces.add_step_from_capability:payload:oneOf@#/components/schemas/InputPathBinding.properties.path",
"workflow.draft_workspaces.update_capability_step:payload:oneOf@#/components/schemas/InputPathBinding.properties.path",
"workflow.draft_workspaces.set_step_input_bindings:payload:oneOf@#/components/schemas/InputPathBinding.properties.path",
@@ -165,6 +165,87 @@ describe("translateJsonSchema", () => {
);
});
it("translates a recursive discriminated oneOf contract", () => {
const components = {
InputExpression: {
discriminator: {
mapping: {
array: "#/components/schemas/ArrayExpression",
literal: "#/components/schemas/LiteralExpression",
object: "#/components/schemas/ObjectExpression",
path: "#/components/schemas/PathExpression",
},
propertyName: "kind",
},
oneOf: [
{ $ref: "#/components/schemas/LiteralExpression" },
{ $ref: "#/components/schemas/PathExpression" },
{ $ref: "#/components/schemas/ArrayExpression" },
{ $ref: "#/components/schemas/ObjectExpression" },
],
},
LiteralExpression: {
additionalProperties: false,
properties: {
kind: { const: "literal", type: "string" },
value: { type: "string" },
},
required: ["kind", "value"],
type: "object",
},
PathExpression: {
additionalProperties: false,
properties: {
kind: { const: "path", type: "string" },
path: { type: "string" },
},
required: ["kind", "path"],
type: "object",
},
ArrayExpression: {
additionalProperties: false,
properties: {
kind: { const: "array", type: "string" },
items: {
items: { $ref: "#/components/schemas/InputExpression" },
type: "array",
},
},
required: ["kind", "items"],
type: "object",
},
ObjectExpression: {
additionalProperties: false,
properties: {
fields: {
additionalProperties: { $ref: "#/components/schemas/InputExpression" },
type: "object",
},
kind: { const: "object", type: "string" },
},
required: ["kind", "fields"],
type: "object",
},
};
const schema = translatedSchema(
{ $ref: "#/components/schemas/InputExpression" },
components,
);
expect(accepts(schema, { kind: "literal", value: "hello" })).toBe(true);
expect(
accepts(schema, {
kind: "object",
fields: {
nested: { kind: "array", items: [{ kind: "path", path: "state.foo" }] },
},
}),
).toBe(true);
expect(accepts(schema, { kind: "literal", value: "hello", extra: true })).toBe(
false,
);
});
it("rejects unproductive component reference cycles", () => {
const components = {
Loop: { $ref: "#/components/schemas/Loop" },
+60 -1
View File
@@ -270,7 +270,7 @@ class Translator {
return failure(path, "JSON Schema must be a boolean or object");
}
for (const keyword of ["allOf", "if", "not", "oneOf", "then", "else"]) {
for (const keyword of ["allOf", "if", "not", "then", "else"]) {
if (keyword in value) {
return failure(
path,
@@ -283,6 +283,9 @@ class Translator {
if ("$ref" in value) {
return this.#translateRef(value, path, structuralDepth);
}
if ("oneOf" in value) {
return this.#translateDiscriminatedOneOf(value, path, structuralDepth);
}
if ("anyOf" in value) {
return this.#translateAnyOf(value, path, structuralDepth);
}
@@ -398,6 +401,62 @@ class Translator {
return Either.right(Schema.Union(...members));
}
#translateDiscriminatedOneOf(
value: Readonly<Record<string, unknown>>,
path: string,
structuralDepth: number,
): Either.Either<Schema.Schema.AnyNoContext, JsonSchemaTranslationError> {
const unsupported = unsupportedKeyword(
value,
new Set(["discriminator", "oneOf"]),
path,
);
if (unsupported !== null) return Either.left(unsupported);
if (!Array.isArray(value.oneOf) || value.oneOf.length === 0) {
return failure(path, "oneOf must be a non-empty array", "oneOf");
}
if (!isRecord(value.discriminator)) {
return failure(
path,
"oneOf is supported only with a discriminator",
"oneOf",
);
}
if (
typeof value.discriminator.propertyName !== "string" ||
value.discriminator.propertyName.length === 0
) {
return failure(
path,
"discriminator.propertyName must be a non-empty string",
"discriminator",
);
}
const mapping = value.discriminator.mapping;
if (!isRecord(mapping) || Object.values(mapping).some((ref) => typeof ref !== "string")) {
return failure(
path,
"discriminator.mapping must map tags to local references",
"discriminator",
);
}
const members: Schema.Schema.AnyNoContext[] = [];
for (const [index, member] of value.oneOf.entries()) {
const translated = this.translate(
member,
`${path}.oneOf[${index}]`,
structuralDepth,
);
if (Either.isLeft(translated)) return translated;
members.push(translated.right);
}
// The generated InputExpression branches are disjoint by their tag, so a
// runtime union preserves the discriminated contract without weakening
// arbitrary oneOf schemas into an overlapping union.
return Either.right(Schema.Union(...members));
}
#translateConst(
value: Readonly<Record<string, unknown>>,
path: string,
@@ -522,6 +522,43 @@ describe("run operation registry", () => {
);
});
it("does not flatten composite bindings into a fake inline CLI value", async () => {
const { getOperationMeta } = await import("./method-registry.js");
const operation = getOperationMeta(
"workflow.draft_workspaces.set_step_input_bindings",
);
if (operation === undefined) throw new Error("missing set-input operation");
const cli = operation.equivalentCli({
workspace_id: "console.demo",
revision: 3,
step_id: "concat",
bindings: [
{
target: "request",
expression: {
kind: "object",
fields: {
items: {
kind: "array",
items: [
{ kind: "path", path: "state.foo" },
{ kind: "literal", value: "wowcool" },
],
},
separator: { kind: "literal", value: " " },
},
},
},
],
});
expect(cli).toContain(
"[non-equivalent: unavailable CLI representation for input_bindings (use --bindings-file)]",
);
expect(cli).not.toContain("--value");
});
it("explains unavailable inline schema and capability fields", async () => {
const { getOperationMeta } = await import("./method-registry.js");
const createEmpty = getOperationMeta(
+25 -4
View File
@@ -1,6 +1,9 @@
import { Schema } from "effect";
import type {
NodeSpecCapabilitySummary,
InputExpressionBinding,
InputPathBinding,
InputValueBinding,
WorkflowOperationName,
WrapperArtifactCapabilitySummary,
} from "./generated/workflow-contract.js";
@@ -54,6 +57,11 @@ import {
WorkflowRunsTraceResultSchema,
} from "./rpcs.js";
type StepInputBinding =
| InputPathBinding
| InputValueBinding
| InputExpressionBinding;
export type OperationMeta = {
readonly method: WorkflowOperationName;
readonly label: string;
@@ -174,17 +182,21 @@ const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
const inputBindingCliArgs = (
bindings: readonly unknown[],
bindings: readonly StepInputBinding[],
pathFlag: "--input" | "--map" = "--input",
): { readonly args: readonly string[]; readonly unavailable: readonly string[] } => {
const args: string[] = [];
const unavailable: string[] = [];
for (const binding of bindings) {
if (!isRecord(binding) || typeof binding.target !== "string") {
if (typeof binding.target !== "string") {
unavailable.push("input_bindings (use --bindings-file)");
continue;
}
if (typeof binding.path === "string") {
if ("path" in binding) {
if (typeof binding.path !== "string") {
unavailable.push("input_bindings (use --bindings-file)");
continue;
}
args.push(pathFlag, shellArg(`${binding.path}=${binding.target}`));
continue;
}
@@ -192,9 +204,18 @@ const inputBindingCliArgs = (
const serialized = JSON.stringify(binding.value);
if (serialized !== undefined) {
args.push("--value", shellArg(`${binding.target}=${serialized}`));
continue;
} else {
unavailable.push("input_bindings (use --bindings-file)");
}
continue;
}
if ("expression" in binding) {
// Composite expressions have no lossless inline CLI equivalent.
unavailable.push("input_bindings (use --bindings-file)");
continue;
}
const exhaustive: never = binding;
void exhaustive;
unavailable.push("input_bindings (use --bindings-file)");
}
return { args, unavailable };