@@ -42,14 +42,28 @@ import type { Weighted } from './incrementalization-law.js'
4242 *
4343 * The generated domain uses small JSON tuples with integer weights. Named
4444 * cases force empty batches, duplicate weights, replacements, cancellation,
45- * presence changes, boundary ties, and a zero-width window. Fault controls
46- * prove that the checker rejects missing, sign-flipped, and wrong-member output.
45+ * presence changes, falsey group extrema, boundary ties, and a zero-width
46+ * window. Fault controls prove that the checker rejects missing, sign-flipped,
47+ * wrong-member, and truthiness-filtered aggregate output.
48+ *
49+ * Before this repair the groupBy branch observed only sums. Truthiness defects
50+ * in `min` and `max` were therefore outside both its model and its assertions.
51+ * The small direct reducer cases remain readable replay witnesses; this suite
52+ * owns the generated incremental-versus-recompute law.
4753 */
4854
4955type Keyed = [ number , number ]
5056type JoinOutput = [ number , [ number , number ] ]
5157type OuterJoinOutput = [ number , [ number | null , number | null ] ]
52- type GroupedOutput = [ string , { bucket : number ; total : number } ]
58+ type GroupedOutput = [
59+ string ,
60+ {
61+ bucket : number
62+ total : number
63+ minimum : number | undefined
64+ maximum : number | undefined
65+ } ,
66+ ]
5367
5468const FIXED_SEED = 1741
5569type GeneratedCampaign = {
@@ -219,14 +233,32 @@ function joinedSums(
219233 return [ ...sums ] . map ( ( [ key , value ] ) => [ [ key , value ] , 1 ] )
220234}
221235
222- function groupedSums ( input : Weighted < Keyed > ) : Weighted < GroupedOutput > {
223- const groups = new Map < number , number > ( )
236+ /**
237+ * Group extrema come from every retained defined value. Zero is a value, not
238+ * absence. The model recomputes from plain weighted rows and shares no
239+ * aggregate reducer with production.
240+ */
241+ function groupedAggregates ( input : Weighted < Keyed > ) : Weighted < GroupedOutput > {
242+ const groups = new Map <
243+ number ,
244+ { total : number ; minimum : number ; maximum : number }
245+ > ( )
224246 for ( const [ [ key , value ] , weight ] of keyedIdentity ( input ) ) {
225247 const bucket = key % 2
226- groups . set ( bucket , ( groups . get ( bucket ) ?? 0 ) + value * weight )
248+ const current = groups . get ( bucket )
249+ groups . set (
250+ bucket ,
251+ current === undefined
252+ ? { total : value * weight , minimum : value , maximum : value }
253+ : {
254+ total : current . total + value * weight ,
255+ minimum : Math . min ( current . minimum , value ) ,
256+ maximum : Math . max ( current . maximum , value ) ,
257+ } ,
258+ )
227259 }
228- return [ ...groups ] . map ( ( [ bucket , total ] ) => [
229- [ JSON . stringify ( { bucket } ) , { bucket, total } ] ,
260+ return [ ...groups ] . map ( ( [ bucket , aggregates ] ) => [
261+ [ JSON . stringify ( { bucket } ) , { bucket, ... aggregates } ] ,
230262 1 ,
231263 ] )
232264}
@@ -332,9 +364,11 @@ describe(`DBSP incrementalization laws`, () => {
332364 input . pipe (
333365 groupBy ( ( [ key ] ) => ( { bucket : key % 2 } ) , {
334366 total : groupByOperators . sum ( ( [ , value ] ) => value ) ,
367+ minimum : groupByOperators . min ( ( [ , value ] ) => value ) ,
368+ maximum : groupByOperators . max ( ( [ , value ] ) => value ) ,
335369 } ) ,
336370 ) ,
337- evaluate : groupedSums ,
371+ evaluate : groupedAggregates ,
338372 } )
339373 assertUnaryIncrementalization ( {
340374 name : `top-K` ,
@@ -555,6 +589,35 @@ describe(`DBSP incrementalization laws`, () => {
555589 splitDeliveries : 4 ,
556590 } )
557591
592+ const extremaReach = assertUnaryIncrementalization ( {
593+ name : `grouped falsey extrema` ,
594+ initial : [
595+ [ [ 0 , 5 ] , 1 ] ,
596+ [ [ 2 , 0 ] , 1 ] ,
597+ [ [ 1 , - 2 ] , 1 ] ,
598+ [ [ 3 , 0 ] , 1 ] ,
599+ ] ,
600+ batches : [ ] ,
601+ inputPolicy : keyedPolicy ,
602+ outputPolicy : groupedPolicy ,
603+ splitDomain : uniqueRowSplitDomain ,
604+ build : ( input ) =>
605+ input . pipe (
606+ groupBy ( ( [ key ] ) => ( { bucket : key % 2 } ) , {
607+ total : groupByOperators . sum ( ( [ , value ] ) => value ) ,
608+ minimum : groupByOperators . min ( ( [ , value ] ) => value ) ,
609+ maximum : groupByOperators . max ( ( [ , value ] ) => value ) ,
610+ } ) ,
611+ ) ,
612+ evaluate : groupedAggregates ,
613+ } )
614+ expect ( extremaReach ) . toEqual ( {
615+ atomicCheckpoints : 1 ,
616+ atomicDeliveries : 1 ,
617+ splitCheckpoints : 1 ,
618+ splitDeliveries : 4 ,
619+ } )
620+
558621 assertUnaryIncrementalization ( {
559622 name : `grouped-order boundary tie` ,
560623 initial : [
@@ -617,7 +680,7 @@ describe(`DBSP incrementalization laws`, () => {
617680 } )
618681 } )
619682
620- it ( `rejects omitted, sign-flipped, wrong-member, and wrong-window output` , ( ) => {
683+ it ( `rejects omitted, sign-flipped, wrong-member, wrong-window, and truthiness-filtered output` , ( ) => {
621684 expect ( ( ) =>
622685 assertUnaryIncrementalization ( {
623686 name : `omitted output fault` ,
@@ -674,5 +737,56 @@ describe(`DBSP incrementalization laws`, () => {
674737 evaluate : firstThree ,
675738 } ) ,
676739 ) . toThrow ( / o u t p u t d e l t a d i v e r g e d / )
740+
741+ const truthinessMinimum = {
742+ preMap : ( [ , value ] : Keyed ) : number | undefined => value ,
743+ reduce : ( values : Array < [ number | undefined , number ] > ) => {
744+ let minimum : number | undefined
745+ for ( const [ value ] of values ) {
746+ if ( ! minimum || ( value !== undefined && value && value < minimum ) ) {
747+ minimum = value
748+ }
749+ }
750+ return minimum
751+ } ,
752+ postMap : ( result : number | undefined ) => result ,
753+ }
754+ const truthinessMaximum = {
755+ preMap : ( [ , value ] : Keyed ) : number | undefined => value ,
756+ reduce : ( values : Array < [ number | undefined , number ] > ) => {
757+ let maximum : number | undefined
758+ for ( const [ value ] of values ) {
759+ if ( ! maximum || ( value !== undefined && value && value > maximum ) ) {
760+ maximum = value
761+ }
762+ }
763+ return maximum
764+ } ,
765+ postMap : ( result : number | undefined ) => result ,
766+ }
767+ expect ( ( ) =>
768+ assertUnaryIncrementalization ( {
769+ name : `truthiness-filtered extrema fault` ,
770+ initial : [
771+ [ [ 0 , 5 ] , 1 ] ,
772+ [ [ 2 , 0 ] , 1 ] ,
773+ [ [ 1 , - 2 ] , 1 ] ,
774+ [ [ 3 , 0 ] , 1 ] ,
775+ ] ,
776+ batches : [ ] ,
777+ inputPolicy : keyedPolicy ,
778+ outputPolicy : groupedPolicy ,
779+ splitDomain : uniqueRowSplitDomain ,
780+ build : ( input ) =>
781+ input . pipe (
782+ groupBy ( ( [ key ] ) => ( { bucket : key % 2 } ) , {
783+ total : groupByOperators . sum ( ( [ , value ] ) => value ) ,
784+ minimum : truthinessMinimum ,
785+ maximum : truthinessMaximum ,
786+ } ) ,
787+ ) ,
788+ evaluate : groupedAggregates ,
789+ } ) ,
790+ ) . toThrow ( / o u t p u t d e l t a d i v e r g e d / )
677791 } )
678792} )
0 commit comments