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 (defineStack → normalizeStackInput → lowerCallables → ObjectStackDefinitionSchema.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
Blocked-by: #4976
Found while implementing #4976 (the map form's lowered declaration). The array spelling of
functionshas 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.tsdeclares two accepted shapes forfunctionsand its own TSDoc calls both first-class:lowerCallableslowers the array form and has a dedicated test pinning what it emits (packages/cli/src/utils/lower-callables.test.ts, "keepseffecton the array form too"):But the array member of the union in
packages/spec/src/stack.zod.ts(~441) requires a callable handler, with no lowered alternative:Measured on
origin/main@ 773f80aDriving the real build pipeline (
defineStack→normalizeStackInput→lowerCallables→ObjectStackDefinitionSchema.safeParse):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
functionsthatobjectstack buildrefuses, with the same unreadableinvalid_union: Invalid inputand no path pastfunctions.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.tsasserts 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
handlerbe 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
buildtime.Filed unassigned per Prime Directive #10, out of #4976's scope (that issue's landing point is
FlowFunctionEntrySchema; this member lives instack.zod.ts).Generated by Claude Code