@@ -66,11 +66,18 @@ pub struct ExecutionResult {
6666 pub status : SolveStatus ,
6767 pub objective_sense : ObjectiveSense ,
6868 pub objective : MappedScalarResult ,
69- pub reports : Vec < MappedScalarResult > ,
69+ pub reports : Vec < ReportResult > ,
7070 pub variables : Vec < MappedVariableResult > ,
7171 pub dual_reports : Vec < DualReportResult > ,
7272}
7373
74+ #[ derive( Debug , Clone , PartialEq ) ]
75+ pub struct ReportResult {
76+ pub name : String ,
77+ pub index : Vec < String > ,
78+ pub values : Vec < BTreeMap < String , serde_json:: Value > > ,
79+ }
80+
7481#[ derive( Debug , Clone , PartialEq ) ]
7582pub struct MappedScalarResult {
7683 pub dsl_name : String ,
@@ -603,23 +610,59 @@ pub fn execute_problem_with_options(
603610 . map ( |variable| ( variable. compiled_name . clone ( ) , variable) )
604611 . collect :: < BTreeMap < _ , _ > > ( ) ;
605612
606- let reports = problem
607- . reports
608- . iter ( )
609- . map ( |report| {
610- let value = report_values. get ( & report. name ) . copied ( ) . ok_or_else ( || {
611- ExecutionError :: MissingReportValue {
612- backend : backend. to_string ( ) ,
613- compiled_name : report. name . clone ( ) ,
614- }
615- } ) ?;
616- Ok ( MappedScalarResult {
617- dsl_name : report. name . clone ( ) ,
613+ // Build unified reports: expression reports (scalar) and variable reports
614+ let mut reports = Vec :: new ( ) ;
615+
616+ for report in & problem. reports {
617+ let value = report_values. get ( & report. name ) . copied ( ) . ok_or_else ( || {
618+ ExecutionError :: MissingReportValue {
619+ backend : backend. to_string ( ) ,
618620 compiled_name : report. name . clone ( ) ,
619- value,
620- } )
621- } )
622- . collect :: < Result < Vec < _ > , _ > > ( ) ?;
621+ }
622+ } ) ?;
623+ let mut record = BTreeMap :: new ( ) ;
624+ record. insert ( "value" . to_string ( ) , serde_json:: Value :: from ( value) ) ;
625+ reports. push ( ReportResult {
626+ name : report. name . clone ( ) ,
627+ index : Vec :: new ( ) ,
628+ values : vec ! [ record] ,
629+ } ) ;
630+ }
631+
632+ for vr in & problem. variable_reports {
633+ let family_key = format ! ( "{}[{}]" , vr. control_name, vr. indices. join( "," ) ) ;
634+ if let Some ( family) = variable_values. get ( & family_key) {
635+ let values = family
636+ . values
637+ . iter ( )
638+ . filter_map ( |v| {
639+ let ( raw, typed) = extract_index_parts ( & vr. control_name , & v. compiled_name ) ;
640+ if let Some ( ref filter) = vr. filter {
641+ let bindings: BTreeMap < & str , & str > = vr
642+ . indices
643+ . iter ( )
644+ . zip ( raw. iter ( ) )
645+ . map ( |( k, v) | ( k. as_str ( ) , v. as_str ( ) ) )
646+ . collect ( ) ;
647+ if !eval_filter ( filter, & bindings) {
648+ return None ;
649+ }
650+ }
651+ let mut record = BTreeMap :: new ( ) ;
652+ for ( idx_name, idx_val) in vr. indices . iter ( ) . zip ( typed) {
653+ record. insert ( idx_name. clone ( ) , idx_val) ;
654+ }
655+ record. insert ( "value" . to_string ( ) , serde_json:: Value :: from ( v. value ) ) ;
656+ Some ( record)
657+ } )
658+ . collect ( ) ;
659+ reports. push ( ReportResult {
660+ name : vr. control_name . clone ( ) ,
661+ index : vr. indices . clone ( ) ,
662+ values,
663+ } ) ;
664+ }
665+ }
623666
624667 let variables = problem
625668 . variables
@@ -900,6 +943,77 @@ fn map_solver_status(status: ArcoSolverStatus) -> SolveStatus {
900943 }
901944}
902945
946+ /// Split a variable instance name into raw string parts and typed JSON values.
947+ /// E.g. `("pc", "pc[1,Li-Ion]")` → `(["1", "Li-Ion"], [Number(1), String("Li-Ion")])`.
948+ fn extract_index_parts (
949+ family_name : & str ,
950+ instance_name : & str ,
951+ ) -> ( Vec < String > , Vec < serde_json:: Value > ) {
952+ let inner = instance_name
953+ . strip_prefix ( family_name)
954+ . and_then ( |s| s. strip_prefix ( '[' ) )
955+ . and_then ( |s| s. strip_suffix ( ']' ) )
956+ . unwrap_or ( "" ) ;
957+ if inner. is_empty ( ) {
958+ return ( Vec :: new ( ) , Vec :: new ( ) ) ;
959+ }
960+ let parts: Vec < & str > = inner. split ( ',' ) . collect ( ) ;
961+ let strings = parts. iter ( ) . map ( |s| s. to_string ( ) ) . collect ( ) ;
962+ let typed = parts
963+ . iter ( )
964+ . map ( |part| {
965+ if let Ok ( n) = part. parse :: < i64 > ( ) {
966+ serde_json:: Value :: Number ( n. into ( ) )
967+ } else if let Some ( n) = part
968+ . parse :: < f64 > ( )
969+ . ok ( )
970+ . and_then ( serde_json:: Number :: from_f64)
971+ {
972+ serde_json:: Value :: Number ( n)
973+ } else {
974+ serde_json:: Value :: String ( part. to_string ( ) )
975+ }
976+ } )
977+ . collect ( ) ;
978+ ( strings, typed)
979+ }
980+
981+ /// Evaluate a simple filter expression against index bindings.
982+ /// Evaluate a comparison filter against index bindings. Unsupported expression
983+ /// types fail closed (exclude the row) to avoid silent pass-through.
984+ fn eval_filter ( expr : & arco_kdl:: algebra:: Expr , bindings : & BTreeMap < & str , & str > ) -> bool {
985+ use arco_kdl:: algebra:: { ComparisonOp , Expr } ;
986+ match expr {
987+ Expr :: Comparison { op, left, right } => {
988+ let lhs = match left. as_ref ( ) {
989+ Expr :: Identifier ( name) => bindings. get ( name. as_str ( ) ) . copied ( ) ,
990+ Expr :: String ( s) => Some ( s. as_str ( ) ) ,
991+ Expr :: Number ( n) => Some ( n. as_str ( ) ) ,
992+ _ => None ,
993+ } ;
994+ let rhs = match right. as_ref ( ) {
995+ Expr :: Identifier ( name) => bindings. get ( name. as_str ( ) ) . copied ( ) ,
996+ Expr :: String ( s) => Some ( s. as_str ( ) ) ,
997+ Expr :: Number ( n) => Some ( n. as_str ( ) ) ,
998+ _ => None ,
999+ } ;
1000+ match ( lhs, rhs) {
1001+ ( Some ( l) , Some ( r) ) => match op {
1002+ ComparisonOp :: Equal | ComparisonOp :: DoubleEqual => l == r,
1003+ ComparisonOp :: NotEqual => l != r,
1004+ ComparisonOp :: Less => l < r,
1005+ ComparisonOp :: LessEqual => l <= r,
1006+ ComparisonOp :: Greater => l > r,
1007+ ComparisonOp :: GreaterEqual => l >= r,
1008+ } ,
1009+ _ => false ,
1010+ }
1011+ }
1012+ // Unsupported filter expressions fail closed
1013+ _ => false ,
1014+ }
1015+ }
1016+
9031017#[ cfg( test) ]
9041018mod tests {
9051019 use crate :: execution:: { MockArcoAdapter , execute_problem_with_options} ;
@@ -923,6 +1037,7 @@ mod tests {
9231037 expression : "0" . to_string ( ) ,
9241038 } ,
9251039 reports : Vec :: new ( ) ,
1040+ variable_reports : Vec :: new ( ) ,
9261041 dual_reports : Vec :: new ( ) ,
9271042 traceability : Vec :: new ( ) ,
9281043 algebra : AlgebraicProblem {
0 commit comments