|
| 1 | +--- |
| 2 | +"@objectstack/spec": minor |
| 3 | +--- |
| 4 | + |
| 5 | +fix(spec): `functions: { fn: { handler, effect: 'writes' } }` survives `objectstack build` (#4976) |
| 6 | + |
| 7 | +`FlowFunctionEntrySchema` gains a fourth union member — the **lowered |
| 8 | +declaration**, a `functions` entry whose `handler` has been replaced by the |
| 9 | +string ref `objectstack build` emits: |
| 10 | + |
| 11 | +``` |
| 12 | +functions: { |
| 13 | + sweepProjectHealth: { handler: 'sweepProjectHealth', effect: 'writes' }, |
| 14 | +} |
| 15 | +``` |
| 16 | + |
| 17 | +Nothing an author writes changes. This shape is produced by the CLI, not typed |
| 18 | +by a person: `lowerCallables` replaces every inline callable with a serialisable |
| 19 | +ref before the stack is parsed (it must — `z.function()` wraps callables and |
| 20 | +would break the ref mapping), and since #4396 it keeps the declaration beside |
| 21 | +the ref so what a function said about itself survives into the artifact. The |
| 22 | +union was not extended in that change, so the artifact it started emitting was |
| 23 | +rejected by the very schema it had to pass: |
| 24 | + |
| 25 | +``` |
| 26 | + ✗ Validation failed |
| 27 | +
|
| 28 | + functions: |
| 29 | + ✗ functions |
| 30 | + invalid_union: Invalid input |
| 31 | +``` |
| 32 | + |
| 33 | +Loading from source was unaffected — `objectstack dev`, `objectstack validate` |
| 34 | +and the test suite all passed — so the failure appeared only at build, on the |
| 35 | +one spelling the platform asks writers to use. That is the same asymmetry #4343 |
| 36 | +fixed for the bare handler ref, one shape over. |
| 37 | + |
| 38 | +**Why this was worse than a failed build.** `effect: 'writes'` exists so a |
| 39 | +function that writes is not counted as having written nothing (#4396, #4354): a |
| 40 | +`script` step reports no record metrics *because* flow functions are |
| 41 | +contractually pure, and a declared writer instead reports `unmeasuredEffect` so |
| 42 | +the run's broken-sweep query (`selected > 0 AND acted = 0 AND unmeasured = 0`) |
| 43 | +stays off it. The error above names no key, no entry and no reason, so the |
| 44 | +practical repair an author reaches for is deleting the declaration — shipping an |
| 45 | +undeclared writer, which is exactly the state it exists to prevent, recorded |
| 46 | +permanently in `sys_automation_run`. |
| 47 | + |
| 48 | +**One behaviour change worth stating.** `{ handler: 'someName' }` written by |
| 49 | +hand now parses where it used to be rejected as "handler is not callable". The |
| 50 | +rejection could not survive this member and should not have: a bare string entry |
| 51 | +(`functions: { foo: 'foo' }`) has been accepted since #4343 with the caveat that |
| 52 | +it registers nothing, so refusing the record spelling of the same mistake while |
| 53 | +accepting the string spelling was two dialects for one contract. Both fail the |
| 54 | +same way, loudly, at execute: `no function named '…' is registered` (#1870). |
| 55 | +Everything else stays strict — the lowered member is *derived* from the authored |
| 56 | +declaration rather than re-typed beside it, so `{ handler: 'fn', efect: 'writes' }` |
| 57 | +still raises the named surface and the `` `efect` → `effect` `` prescription, an |
| 58 | +unknown `effect` value is still refused, and an empty ref is still not a name. |
| 59 | + |
| 60 | +**Runtime is unchanged and was already correct.** `normalizeFlowFunctionEntry` |
| 61 | +returns `undefined` for a lowered entry in both its shapes, because neither |
| 62 | +carries a callable; `mergeRuntimeModule` re-attaches the sidecar module's |
| 63 | +function to the declaration the JSON carried *before* any collector runs, so |
| 64 | +`effect` reaches `collectBundleFunctionEntries` intact on the built path. |
| 65 | + |
| 66 | +The two halves are now pinned against each other by a round-trip test that |
| 67 | +drives the real pipeline (`defineStack` → `normalizeStackInput` → |
| 68 | +`lowerCallables` → parse) instead of a hand-written sample of what the lowering |
| 69 | +is believed to emit — the crossing neither side previously made, which is why |
| 70 | +both stayed green while the build failed on the join. |
0 commit comments