Found while implementing #16038 (PR #16438), outside that card's ruled surface and deliberately not fixed there. Filed by the os-dev execution seat, session 01XpTx2tbq3pZRYAdoGt6E6Y. Not assigned; domain:*, type and priority are triage's.
The gap
AutomationEngine.evaluateValueEnvelope(envelope, variables, where) in packages/services/service-automation/src/engine.ts is the run-time half of #15137's ruling, and it is a genuinely good one: it derives its verdict from valueEnvelopeRefusals, the same call registerFlow makes, so the reject set of registration and the reject set of evaluation are one set by construction. Its docblock says so.
That shared refusal covers every malformed envelope. It does not cover null / undefined. After valueEnvelopeRefusals returns no findings, the method reads
const source = envelope.source ?? '';
and on null / undefined that read is a bare TypeError — no where, no source, no rule.
Measured, driven (worktree at origin/main 3e7ef9c23)
new AutomationEngine(logger).evaluateValueEnvelope(null, new Map(), 'probe')
new AutomationEngine(logger).evaluateValueEnvelope(undefined, new Map(), 'probe')
TypeError: Cannot read properties of null (reading 'source')
TypeError: Cannot read properties of undefined (reading 'source')
Every other shape driven in the same run threw an attributed Error instead, which is what makes this one the outlier rather than the rule:
| Input |
Result |
{ source: 1 } |
attributed — leads with ASSIGNMENT_VALUE_ENVELOPE_REFUSAL |
{ dialect: 'cel', source: 1 } |
attributed |
{ dialect: 'cel', source: {} } |
attributed |
{ ast, source: 1 } |
attributed |
{ dialect: 'cel' } |
attributed |
42 / ['a'] / {} |
attributed — located CEL fault, carries the source |
null / undefined |
raw TypeError |
Why it is not urgent, stated rather than left implicit
Unreachable from the only production call site. packages/services/service-automation/src/builtin/logic-nodes.ts guards the call with isExpressionEnvelopeShaped(value), which null and undefined do not satisfy, so the assignment executor cannot deliver either. What is left is a direct caller: evaluateValueEnvelope is a public method on an exported class, the same property that made #16038 worth its own card.
Why it was not fixed in passing on #16038
The maintainer's ruling on #16038 (decision batch #57, option A) is specifically that evaluateCondition must refuse a malformed condition shape with STRUCTURAL_CONDITION_SHAPE_REFUSAL. This site is the sibling value path, whose refusal vocabulary is a different published sentence — ASSIGNMENT_VALUE_ENVELOPE_REFUSAL, derived by valueEnvelopeRefusals. Applying the condition refusal here would be wrong; applying the value refusal here is a question about a reject set the ruling does not name, so it is triage's, not the implementing seat's.
Note also that null / undefined are admitted on the condition side on purpose — "not authored" is not a malformed predicate, and structuralConditionRefusal returns nothing for them. Whether the value path should say the same thing, or should refuse an absent envelope outright, is exactly the decision here.
Two defensible shapes, for whoever triages
- A —
valueEnvelopeRefusals refuses null / undefined with ASSIGNMENT_VALUE_ENVELOPE_REFUSAL, so registration and evaluation keep sharing one set and both refuse an absent envelope. Costs: registerFlow would start refusing an authored null in a declared value slot, which nothing measures today.
- B —
evaluateValueEnvelope treats an absent envelope the way the condition path treats an unauthored condition, returning undefined rather than throwing. Costs: a silent undefined in a variable surfaces three nodes later as an empty notification, which the method's own docblock argues against for computed values.
⚠️ Un-measured, stated as a gap: whether any caller outside this repo passes an absent envelope, and whether a value slot can hold an authored null after FlowSchema.parse.
Related, not a duplicate: #16038 is the predicate path and a different refusal sentence.
Found while implementing #16038 (PR #16438), outside that card's ruled surface and deliberately not fixed there. Filed by the
os-devexecution seat, session01XpTx2tbq3pZRYAdoGt6E6Y. Not assigned;domain:*, type and priority are triage's.The gap
AutomationEngine.evaluateValueEnvelope(envelope, variables, where)inpackages/services/service-automation/src/engine.tsis the run-time half of #15137's ruling, and it is a genuinely good one: it derives its verdict fromvalueEnvelopeRefusals, the same callregisterFlowmakes, so the reject set of registration and the reject set of evaluation are one set by construction. Its docblock says so.That shared refusal covers every malformed envelope. It does not cover
null/undefined. AftervalueEnvelopeRefusalsreturns no findings, the method readsand on
null/undefinedthat read is a bareTypeError— nowhere, no source, no rule.Measured, driven (worktree at
origin/main3e7ef9c23)Every other shape driven in the same run threw an attributed
Errorinstead, which is what makes this one the outlier rather than the rule:{ source: 1 }ASSIGNMENT_VALUE_ENVELOPE_REFUSAL{ dialect: 'cel', source: 1 }{ dialect: 'cel', source: {} }{ ast, source: 1 }{ dialect: 'cel' }42/['a']/{}null/undefinedTypeErrorWhy it is not urgent, stated rather than left implicit
Unreachable from the only production call site.
packages/services/service-automation/src/builtin/logic-nodes.tsguards the call withisExpressionEnvelopeShaped(value), whichnullandundefineddo not satisfy, so the assignment executor cannot deliver either. What is left is a direct caller:evaluateValueEnvelopeis a public method on an exported class, the same property that made #16038 worth its own card.Why it was not fixed in passing on #16038
The maintainer's ruling on #16038 (decision batch #57, option A) is specifically that
evaluateConditionmust refuse a malformed condition shape withSTRUCTURAL_CONDITION_SHAPE_REFUSAL. This site is the sibling value path, whose refusal vocabulary is a different published sentence —ASSIGNMENT_VALUE_ENVELOPE_REFUSAL, derived byvalueEnvelopeRefusals. Applying the condition refusal here would be wrong; applying the value refusal here is a question about a reject set the ruling does not name, so it is triage's, not the implementing seat's.Note also that
null/undefinedare admitted on the condition side on purpose — "not authored" is not a malformed predicate, andstructuralConditionRefusalreturns nothing for them. Whether the value path should say the same thing, or should refuse an absent envelope outright, is exactly the decision here.Two defensible shapes, for whoever triages
valueEnvelopeRefusalsrefusesnull/undefinedwithASSIGNMENT_VALUE_ENVELOPE_REFUSAL, so registration and evaluation keep sharing one set and both refuse an absent envelope. Costs:registerFlowwould start refusing an authorednullin a declaredvalueslot, which nothing measures today.evaluateValueEnvelopetreats an absent envelope the way the condition path treats an unauthored condition, returningundefinedrather than throwing. Costs: a silentundefinedin a variable surfaces three nodes later as an empty notification, which the method's own docblock argues against for computed values.valueslot can hold an authorednullafterFlowSchema.parse.Related, not a duplicate: #16038 is the predicate path and a different refusal sentence.