@@ -294,6 +294,22 @@ describe('the entrances the stack declares (A′)', () => {
294294 expect ( userRefusals ( flowStack ( { edgeCondition : USER , objectName : 'acct' } ) ) ) . toHaveLength ( 1 ) ;
295295 } ) ;
296296
297+ it ( 'a record trigger that names no start config.objectName stands down; the same trigger on a declared object is judged' , ( ) => {
298+ // With no `objectName` the record-change trigger registers its hook with no
299+ // object filter (`record-change-trigger.ts`), so it fires on a write to ANY
300+ // object — one this stack does not declare included — and hands that row.
301+ expect ( userRefusals ( flowStack ( { edgeCondition : USER , triggerType : 'record-after-update' } ) ) ) . toEqual ( [ ] ) ;
302+ expect ( userRefusals ( flowStack ( { edgeCondition : USER , triggerType : 'record-after-update' , objectName : 'acct' } ) ) ) . toHaveLength ( 1 ) ;
303+ // And with no `record-*` trigger there is no such entrance: the flow is judged.
304+ expect ( userRefusals ( flowStack ( { edgeCondition : USER } ) ) ) . toHaveLength ( 1 ) ;
305+ const entrance = flowCelEntrances (
306+ { flows : [ { name : 'f' , type : 'autolaunched' , nodes : [ { id : 'start' , type : 'start' , config : { triggerType : 'record-after-create' } } ] } ] } ,
307+ new Map ( [ [ 'acct' , [ 'status' ] ] ] ) ,
308+ ) . get ( 'f' ) ;
309+ expect ( entrance ?. openedBy ) . toContain ( "'record-after-create'" ) ;
310+ expect ( entrance ?. bound ) . toEqual ( [ 'record' ] ) ;
311+ } ) ;
312+
297313 it ( 'a time-relative sweep: an undeclared config.timeRelative.object stands down; a declared one is judged' , ( ) => {
298314 const sweep = ( object : string ) : AnyRec => {
299315 const stack = flowStack ( { edgeCondition : USER } ) ;
@@ -361,6 +377,25 @@ describe('the entrances the stack declares (A′)', () => {
361377 expect ( userRefusals ( withFlows ( flowStack ( { edgeCondition : USER } ) , judgedParent ) ) ) . toHaveLength ( 1 ) ;
362378 } ) ;
363379
380+ it ( 'a parent that is itself open: its `map` child stands down, to a fixpoint; a judged parent leaves the child judged' , ( ) => {
381+ // `map-node.ts` sets the child's `record` only for an id-bearing item; any
382+ // other item leaves the child holding the parent's record. The declared
383+ // `itemObject` keeps the node from opening `f` itself, so only the parent's
384+ // openness can.
385+ const each = ( child : string ) : AnyRec => ( { id : 'each' , type : 'map' , config : { collection : '{rows}' , flowName : child , itemObject : 'acct' } } ) ;
386+ const openParent = parentFlow ( { type : 'api' , node : each ( 'f' ) } ) ;
387+ expect ( userRefusals ( withFlows ( flowStack ( { edgeCondition : USER } ) , openParent ) ) ) . toEqual ( [ ] ) ;
388+ expect ( flowCelEntrances (
389+ { flows : [ { name : 'f' , type : 'autolaunched' , nodes : [ { id : 'start' , type : 'start' } ] } , openParent ] } ,
390+ new Map ( [ [ 'acct' , [ 'status' ] ] ] ) ,
391+ ) . get ( 'f' ) ?. openedBy ) . toMatch ( / ^ m a p n o d e ' e a c h ' i n f l o w ' g ' .* ' g ' i s i t s e l f o p e n $ / ) ;
392+ // Two levels: g (the inbound hook) → map → h → map → f.
393+ const middle = { ...parentFlow ( { node : each ( 'f' ) } ) , name : 'h' , label : 'H' } ;
394+ expect ( userRefusals ( withFlows ( flowStack ( { edgeCondition : USER } ) , parentFlow ( { type : 'api' , node : each ( 'h' ) } ) , middle ) ) ) . toEqual ( [ ] ) ;
395+ // RED CONTROL — the same `map` node in a parent no entrance opens leaves the child judged.
396+ expect ( userRefusals ( withFlows ( flowStack ( { edgeCondition : USER } ) , parentFlow ( { node : each ( 'f' ) } ) ) ) ) . toHaveLength ( 1 ) ;
397+ } ) ;
398+
364399 it ( 'the reader names the entrance' , ( ) => {
365400 const entrances = flowCelEntrances (
366401 {
@@ -377,9 +412,14 @@ describe('the entrances the stack declares (A′)', () => {
377412 * S (triage `6094850599`): at the runtime publish gate the per-write snapshot
378413 * holds the written flow alone, so no entrance is visible and this one judgment
379414 * stands down there. Every other expression verdict at that door is unchanged.
380- * #22636 widens the snapshot; when it lands, this stand-down goes.
415+ * By design, not a hold-out: who launches a flow and what record it is handed
416+ * are read from other items of the stack, a whole-stack judgment that belongs to
417+ * `objectstack validate` (ADR-0109 Decision §3's boundary), and the snapshot
418+ * stays as it is (ruling `5791822697` on #19527, item 3; ruling `6104584601` on
419+ * #22636, letter B). These pins hold the asymmetry: the gate stands down, the
420+ * build door refuses.
381421 */
382- describe ( 'the runtime publish gate (S)' , ( ) => {
422+ describe ( 'the runtime publish gate stands down by design (S)' , ( ) => {
383423 const written = {
384424 name : 'f' ,
385425 label : 'F' ,
@@ -415,7 +455,8 @@ describe('the runtime publish gate (S)', () => {
415455 * binds `context.record` and `celScope` binds no `record` of its own. So the
416456 * build door binds it only where an entrance the stack declares hands the flow a
417457 * record, or the flow binds a variable named `record`; otherwise `record.X` is
418- * refused. The runtime publish gate's stand-down (S) covers it until #22636.
458+ * refused. The runtime publish gate's stand-down (S) covers it, by design (the S
459+ * block above).
419460 */
420461describe ( '`record` — bound only where an entrance hands the flow a record (#22677)' , ( ) => {
421462 const READ = 'record.status == "open"' ;
0 commit comments