Skip to content

functions: [{ name, handler }] (the array form) cannot survive objectstack build either — #4343/#4976 both fixed only the map form #6238

Description

@qq9340100

Blocked-by: #4976

Found while implementing #4976 (the map form's lowered declaration). The array spelling of functions has the same round-trip hole, and it has it in both of its shapes — bare and declared — so it has never been buildable at all.

The two halves disagree

stack.zod.ts declares two accepted shapes for functions and its own TSDoc calls both first-class:

 * - Map form (preferred): `{ my_handler: (ctx) =` `> {...} }`
* - Array form: `[{ name: 'my_handler', handler: (ctx) =` `> {...} }]`

lowerCallables lowers the array form and has a dedicated test pinning what it emits (packages/cli/src/utils/lower-callables.test.ts, "keeps effect on the array form too"):

{ name: 'syncBilling', handler: 'syncBilling', effect: 'writes' }

But the array member of the union in packages/spec/src/stack.zod.ts (~441) requires a callable handler, with no lowered alternative:

functions: z.union([
z.record(z.string(), FlowFunctionEntrySchema),
z.array(z.object({
name: z.string(),
handler: z.function(), // ← no string ref member
packageId: z.string().optional(),
effect: FlowFunctionEffectSchema.optional(),
})),
]).optional()

Measured on origin/main @ 773f80a

Driving the real build pipeline (defineStacknormalizeStackInputlowerCallablesObjectStackDefinitionSchema.safeParse):

map bare lowered -> OK
map declared lowered -> FAIL invalid_union@functions (#4976, being fixed)
array bare lowered -> FAIL invalid_union@functions (this issue)
array declared lowered -> FAIL invalid_union@functions (this issue)

Note the middle two lines: #4343 closed the map/bare case and #4976 closes map/declared, and neither touched the array member — so once #4976 lands, the array form is the only remaining spelling of functions that objectstack build refuses, with the same unreadable invalid_union: Invalid input and no path past functions.

Why it stayed invisible

Nothing in the repo authors the array form: grep -rn "functions: \[" examples/ content/docs/ skills/ finds no app and no doc example, only unit tests that stop at the emitted shape or start from a hand-built bundle. lower-callables.test.ts asserts what the lowering produces; the spec tests parse only authored shapes; no test crosses the boundary. That is the same gap #4976 names for the map form, one surface over.

Suggested direction

Symmetric with #4976: let the array member's handler be a callable or a lowered ref (z.union([z.function(), z.string().min(1)])), and extend the cross-boundary round-trip pin #4976 adds (packages/cli/src/utils/lower-callables.test.ts) to cover the array form so the two halves cannot drift apart a third time.

Worth deciding as part of it: whether the array form earns the fix or the retirement. It is a declared authoring surface with no measured consumer — no example app, no doc, no skill uses it — so "retire the array form, keep the map" is a legitimate reading of the startup-focus principle. Either answer closes the round-trip hole; shipping neither leaves a documented shape that fails at build time.

Filed unassigned per Prime Directive #10, out of #4976's scope (that issue's landing point is FlowFunctionEntrySchema; this member lives in stack.zod.ts).


Generated by Claude Code

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions