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

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions