Hit while implementing #4774's health-sweep job in examples/app-showcase. Authoring the declared functions entry — the spelling #4396 introduced so a writer keeps its run's metrics honest — makes objectstack build fail. The showcase works around it there by authoring the bare form, with a comment pointing here.
What happens
defineStack({functions: {sweepProjectHealth: {handler: sweepProjectHealth,effect: 'writes'},},});pnpm --filter @objectstack/example-showcase build:
→ Lowering inline handlers...
→ Validating protocol compliance...
✗ Validation failed
functions:
✗ functions
invalid_union: Invalid input
1 validation error(s) total
Loading from source is fine — pnpm dev, objectstack validate, and the vitest suite all pass. Only the built path fails, which is the same asymmetry #4343 fixed for the bare form.
Why
lowerCallables correctly lowers a declared entry, keeping the declaration and replacing the callable with a ref. Its own test states the contract (packages/cli/src/utils/lower-callables.test.ts):
it('lowers a declared entry and keeps what it declared',()=>{constresult=lowerCallables({functions: {syncBilling: {handler: ()=>'synced',effect: 'writes'}},});expect(functionsOf(result).syncBilling).toEqual({handler: 'syncBilling',effect: 'writes'});});So the value reaching the parse is { handler: 'syncBilling', effect: 'writes' } — a declaration whose handler is a string.
FlowFunctionEntrySchema (packages/spec/src/automation/flow-function.zod.ts ~137) has three members, and none accepts it:
exportconstFlowFunctionEntrySchema=lazySchema(()=>z.union([z.function(),// bare callable — authoredFlowFunctionDeclarationSchema,// { handler: z.function(), effect } — handler must be a CALLABLEz.string().min(1),// lowered BARE ref]));Confirmed directly against the built spec:
lowered declared entry parses? false
invalid_union → [expected function, received object], [expected function, ...]
lowered BARE entry parses? true
#4396 taught the lowering step about declared entries; the schema union was not extended in the same change, so the fourth member — the lowered declaration — is missing. The module's own docblock describes the exact bug for the third member and stops one shape short:
> The first two are what an author writes. The third is what objectstack build produces and was, until #4343, the reason defineStack({ functions }) could not survive a build at all … The build failed on a mechanism its own docs call first-class.
Why it matters beyond the build failure
effect: 'writes' exists so an undeclared writer is not silently "counted as having written nothing" (#4396, #4354). Today an author who follows that guidance gets a build failure whose message (invalid_union: Invalid input, no path past functions) does not name the key, the entry, or the reason — so the practical outcome is that they delete the declaration and ship an undeclared writer, which is precisely the state the declaration exists to prevent.
Suggested direction
Add the lowered-declaration member to the union — a declaration whose handler may be a string ref, mirroring how the third member relates to the first. normalizeFlowFunctionEntry already drops string handlers (the callable rides in the sibling ESM module and is merged by name in collectBundleFunctionEntries), so the runtime half should need no change; worth confirming it keeps effect off the lowered record rather than dropping the entry.
A pin test in the shape of the existing lowering tests — lower a declared entry, then parse the result through FlowFunctionEntrySchema — would have caught this and would keep the two halves from drifting again. Right now no test crosses that boundary: lower-callables.test.ts asserts the emitted shape, and the spec tests parse only authored shapes.
Repro
cd examples/app-showcase
# author functions: { sweepProjectHealth: { handler: sweepProjectHealth, effect: 'writes' } }
pnpm build # ✗ functions: invalid_union
pnpm validate # ✓ passes — source path onlyObserved on main @ f61c8cf.
Filed unassigned per Prime Directive #10.
Hit while implementing #4774's health-sweep job in
examples/app-showcase. Authoring the declaredfunctionsentry — the spelling #4396 introduced so a writer keeps its run's metrics honest — makesobjectstack buildfail. The showcase works around it there by authoring the bare form, with a comment pointing here.What happens
pnpm --filter @objectstack/example-showcase build:Loading from source is fine —
pnpm dev,objectstack validate, and the vitest suite all pass. Only the built path fails, which is the same asymmetry #4343 fixed for the bare form.Why
lowerCallablescorrectly lowers a declared entry, keeping the declaration and replacing the callable with a ref. Its own test states the contract (packages/cli/src/utils/lower-callables.test.ts):So the value reaching the parse is
{ handler: 'syncBilling', effect: 'writes' }— a declaration whosehandleris a string.FlowFunctionEntrySchema(packages/spec/src/automation/flow-function.zod.ts~137) has three members, and none accepts it:Confirmed directly against the built spec:
#4396taught the lowering step about declared entries; the schema union was not extended in the same change, so the fourth member — the lowered declaration — is missing. The module's own docblock describes the exact bug for the third member and stops one shape short:> The first two are what an author writes. The third is what
objectstack buildproduces and was, until #4343, the reasondefineStack({ functions })could not survive a build at all … The build failed on a mechanism its own docs call first-class.Why it matters beyond the build failure
effect: 'writes'exists so an undeclared writer is not silently "counted as having written nothing" (#4396, #4354). Today an author who follows that guidance gets a build failure whose message (invalid_union: Invalid input, no path pastfunctions) does not name the key, the entry, or the reason — so the practical outcome is that they delete the declaration and ship an undeclared writer, which is precisely the state the declaration exists to prevent.Suggested direction
Add the lowered-declaration member to the union — a declaration whose
handlermay be a string ref, mirroring how the third member relates to the first.normalizeFlowFunctionEntryalready drops string handlers (the callable rides in the sibling ESM module and is merged by name incollectBundleFunctionEntries), so the runtime half should need no change; worth confirming it keepseffectoff the lowered record rather than dropping the entry.A pin test in the shape of the existing lowering tests — lower a declared entry, then parse the result through
FlowFunctionEntrySchema— would have caught this and would keep the two halves from drifting again. Right now no test crosses that boundary:lower-callables.test.tsasserts the emitted shape, and the spec tests parse only authored shapes.Repro
Observed on
main@ f61c8cf.Filed unassigned per Prime Directive #10.