@@ -618,7 +618,16 @@ export class ExecutionTree implements TreeContext {
618618 w . to . field === field &&
619619 pathEquals ( w . to . path , prefix ) ,
620620 ) ;
621- if ( exactWires . length > 0 ) {
621+
622+ // Separate spread wires from regular wires
623+ const spreadWires = exactWires . filter (
624+ ( w ) => "from" in w && "spread" in w && w . spread ,
625+ ) ;
626+ const regularWires = exactWires . filter (
627+ ( w ) => ! ( "from" in w && "spread" in w && w . spread ) ,
628+ ) ;
629+
630+ if ( regularWires . length > 0 ) {
622631 // Check for array mapping: exact wires (the array source) PLUS
623632 // element-level wires deeper than prefix (the field mappings).
624633 // E.g. `o.entries <- src[] as x { .id <- x.item_id }` produces
@@ -639,15 +648,16 @@ export class ExecutionTree implements TreeContext {
639648 if ( hasElementWires ) {
640649 // Array mapping on a sub-field: resolve the array source,
641650 // create shadow trees, and materialise with field mappings.
642- const resolved = await this . resolveWires ( exactWires ) ;
651+ const resolved = await this . resolveWires ( regularWires ) ;
643652 if ( ! Array . isArray ( resolved ) ) return resolved ;
644653 const shadows = this . createShadowArray ( resolved ) ;
645654 return this . materializeShadows ( shadows , prefix ) ;
646655 }
647656
648- return this . resolveWires ( exactWires ) ;
657+ return this . resolveWires ( regularWires ) ;
649658 }
650659
660+ // Collect sub-fields from deeper wires
651661 const subFields = new Set < string > ( ) ;
652662 for ( const wire of bridge . wires ) {
653663 const p = wire . to . path ;
@@ -661,6 +671,37 @@ export class ExecutionTree implements TreeContext {
661671 subFields . add ( p [ prefix . length ] ! ) ;
662672 }
663673 }
674+
675+ // Spread wires: resolve and merge, then overlay sub-field wires
676+ if ( spreadWires . length > 0 ) {
677+ const result : Record < string , unknown > = { } ;
678+
679+ // First resolve spread sources (in order)
680+ for ( const wire of spreadWires ) {
681+ const spreadValue = await this . resolveWires ( [ wire ] ) ;
682+ if ( spreadValue != null && typeof spreadValue === "object" ) {
683+ Object . assign ( result , spreadValue ) ;
684+ }
685+ }
686+
687+ // Then resolve sub-fields and overlay on spread result
688+ const prefixStr = prefix . join ( "." ) ;
689+ const activeSubFields = this . requestedFields
690+ ? [ ...subFields ] . filter ( ( sub ) => {
691+ const fullPath = prefixStr ? `${ prefixStr } .${ sub } ` : sub ;
692+ return matchesRequestedFields ( fullPath , this . requestedFields ) ;
693+ } )
694+ : [ ...subFields ] ;
695+
696+ await Promise . all (
697+ activeSubFields . map ( async ( sub ) => {
698+ result [ sub ] = await this . resolveNestedField ( [ ...prefix , sub ] ) ;
699+ } ) ,
700+ ) ;
701+
702+ return result ;
703+ }
704+
664705 if ( subFields . size === 0 ) return undefined ;
665706
666707 // Apply sparse fieldset filter at nested level
@@ -792,8 +833,8 @@ export class ExecutionTree implements TreeContext {
792833
793834 const { type, field } = this . trunk ;
794835
795- // Is there a root-level wire targeting the output with path []?
796- const hasRootWire = bridge . wires . some (
836+ // Separate root-level wires into passthrough vs spread
837+ const rootWires = bridge . wires . filter (
797838 ( w ) =>
798839 "from" in w &&
799840 w . to . module === SELF_MODULE &&
@@ -802,6 +843,18 @@ export class ExecutionTree implements TreeContext {
802843 w . to . path . length === 0 ,
803844 ) ;
804845
846+ // Passthrough wire: root wire without spread flag
847+ const hasPassthroughWire = rootWires . some (
848+ ( w ) => "from" in w && ! ( "spread" in w && w . spread ) ,
849+ ) ;
850+
851+ // Spread wires: root wires with spread flag
852+ const spreadWires = rootWires . filter (
853+ ( w ) => "from" in w && "spread" in w && w . spread ,
854+ ) ;
855+
856+ const hasRootWire = rootWires . length > 0 ;
857+
805858 // Array-mapped output (`o <- items[] as x { ... }`) has BOTH a root wire
806859 // AND element-level wires (from.element === true). A plain passthrough
807860 // (`o <- api.user`) only has the root wire.
@@ -827,8 +880,8 @@ export class ExecutionTree implements TreeContext {
827880 return this . materializeShadows ( shadows , [ ] ) ;
828881 }
829882
830- // Whole-object passthrough: `o <- api.user`
831- if ( hasRootWire ) {
883+ // Whole-object passthrough: `o <- api.user` (non-spread root wire)
884+ if ( hasPassthroughWire ) {
832885 const [ result ] = await Promise . all ( [
833886 this . pullOutputField ( [ ] ) ,
834887 ...forcePromises ,
@@ -849,7 +902,11 @@ export class ExecutionTree implements TreeContext {
849902 }
850903 }
851904
852- if ( outputFields . size === 0 ) {
905+ // Spread wires: resolve and merge source objects
906+ // Later field wires will override spread properties
907+ const hasSpreadWires = spreadWires . length > 0 ;
908+
909+ if ( outputFields . size === 0 && ! hasSpreadWires ) {
853910 throw new Error (
854911 `Bridge "${ type } .${ field } " has no output wires. ` +
855912 `Ensure at least one wire targets the output (e.g. \`o.field <- ...\`).` ,
@@ -861,6 +918,16 @@ export class ExecutionTree implements TreeContext {
861918
862919 const result : Record < string , unknown > = { } ;
863920
921+ // First resolve spread wires (in order) to build base object
922+ // Each spread source's properties are merged into result
923+ for ( const wire of spreadWires ) {
924+ const spreadValue = await this . resolveWires ( [ wire ] ) ;
925+ if ( spreadValue != null && typeof spreadValue === "object" ) {
926+ Object . assign ( result , spreadValue ) ;
927+ }
928+ }
929+
930+ // Then resolve explicit field wires - these override spread properties
864931 await Promise . all ( [
865932 ...[ ...activeFields ] . map ( async ( name ) => {
866933 result [ name ] = await this . resolveNestedField ( [ name ] ) ;
0 commit comments