1#[cfg(feature = "ir")]
34use crate::expr::evaluate as evaluate_expression;
35use crate::expr::ExprFailure;
36#[cfg(feature = "ir")]
37use crate::plan::{run_plan, ExecutionPlanSpec, OpSpec, RelationKind};
38use crate::plan::{ExecOutcome, PlanFailure};
39use crate::value::Value;
40#[cfg(feature = "ir")]
41use serde_json::Value as J;
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum BehaviorFailureCode {
46 UnknownComponent,
47 UnknownNodeKind,
48 MapOverNotArray,
49 MapIntoElementNotObject,
50 MapBatchResultMismatch,
51 FanoutOverNotArray,
52 FanoutBatchResultMismatch,
53 UnknownEntry,
54}
55
56impl BehaviorFailureCode {
57 pub fn as_str(self) -> &'static str {
58 match self {
59 BehaviorFailureCode::UnknownComponent => "UNKNOWN_COMPONENT",
60 BehaviorFailureCode::UnknownNodeKind => "UNKNOWN_NODE_KIND",
61 BehaviorFailureCode::MapOverNotArray => "MAP_OVER_NOT_ARRAY",
62 BehaviorFailureCode::MapIntoElementNotObject => "MAP_INTO_ELEMENT_NOT_OBJECT",
63 BehaviorFailureCode::MapBatchResultMismatch => "MAP_BATCH_RESULT_MISMATCH",
64 BehaviorFailureCode::FanoutOverNotArray => "FANOUT_OVER_NOT_ARRAY",
65 BehaviorFailureCode::FanoutBatchResultMismatch => "FANOUT_BATCH_RESULT_MISMATCH",
66 BehaviorFailureCode::UnknownEntry => "UNKNOWN_ENTRY",
67 }
68 }
69}
70
71#[derive(Debug, Clone)]
74pub struct BehaviorError {
75 code: String,
76 pub message: String,
77}
78
79impl BehaviorError {
80 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
85 BehaviorError {
86 code: code.into(),
87 message: message.into(),
88 }
89 }
90
91 pub fn code(&self) -> &str {
93 &self.code
94 }
95}
96
97impl std::fmt::Display for BehaviorError {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 write!(f, "{}: {}", self.code, self.message)
100 }
101}
102impl std::error::Error for BehaviorError {}
103
104impl From<ExprFailure> for BehaviorError {
105 fn from(e: ExprFailure) -> Self {
106 BehaviorError {
107 code: e.code.as_str().to_string(),
108 message: e.message,
109 }
110 }
111}
112impl From<PlanFailure> for BehaviorError {
113 fn from(e: PlanFailure) -> Self {
114 BehaviorError {
115 code: e.code.as_str().to_string(),
116 message: e.message,
117 }
118 }
119}
120
121#[cfg(feature = "ir")]
122fn bfail<T>(code: BehaviorFailureCode, message: impl Into<String>) -> Result<T, BehaviorError> {
123 Err(BehaviorError {
124 code: code.as_str().to_string(),
125 message: message.into(),
126 })
127}
128
129pub trait ComponentExec {
134 fn exec(
135 &mut self,
136 component: &str,
137 ports: &[(String, Value)],
138 bound: Option<&Value>,
139 ) -> Option<ExecOutcome>;
140
141 fn exec_ctx(
147 &mut self,
148 node_id: &str,
149 component: &str,
150 ports: &[(String, Value)],
151 bound: Option<&Value>,
152 ) -> Option<ExecOutcome> {
153 let _ = node_id;
154 self.exec(component, ports, bound)
155 }
156}
157
158impl ComponentExec for &mut dyn ComponentExec {
175 fn exec(
176 &mut self,
177 component: &str,
178 ports: &[(String, Value)],
179 bound: Option<&Value>,
180 ) -> Option<ExecOutcome> {
181 (**self).exec(component, ports, bound)
182 }
183
184 fn exec_ctx(
185 &mut self,
186 node_id: &str,
187 component: &str,
188 ports: &[(String, Value)],
189 bound: Option<&Value>,
190 ) -> Option<ExecOutcome> {
191 (**self).exec_ctx(node_id, component, ports, bound)
192 }
193}
194
195#[cfg(feature = "ir")]
196fn scope_get<'a>(scope: &'a [(String, Value)], key: &str) -> Option<&'a Value> {
197 scope.iter().find(|(k, _)| k == key).map(|(_, v)| v)
198}
199
200#[cfg(feature = "ir")]
206fn fanout_dedup_drop(
207 aligned_bodies: Vec<Value>,
208 dedupe_key: &str,
209 drop: &str,
210 implicit_source: Option<&str>,
211) -> Vec<Value> {
212 use std::collections::HashSet;
213 let mut items: Vec<Value> = Vec::with_capacity(aligned_bodies.len());
214 let mut seen: HashSet<String> = HashSet::new();
215 for body in aligned_bodies.into_iter() {
216 let fields = match &body {
217 Value::Obj(f) => Some(f),
218 _ => None,
219 };
220 let key_val = fields.and_then(|f| f.iter().find(|(k, _)| k == dedupe_key).map(|(_, v)| v));
221 let has_key = matches!(key_val, Some(v) if !matches!(v, Value::Null));
222 if !has_key {
223 if drop == "dangling" {
224 continue;
225 }
226 items.push(body); continue;
228 }
229 let seen_key = match key_val.unwrap() {
230 Value::Str(s) => format!("s:{s}"),
231 other => format!("j:{other:?}"),
232 };
233 if seen.contains(&seen_key) {
234 continue;
235 }
236 seen.insert(seen_key);
237 match (implicit_source, fields) {
238 (Some(src), Some(f)) if f.iter().any(|(k, _)| k == src) => {
239 let stripped: Vec<(String, Value)> =
240 f.iter().filter(|(k, _)| k != src).cloned().collect();
241 items.push(Value::Obj(stripped));
242 }
243 _ => items.push(body),
244 }
245 }
246 items
247}
248
249#[cfg(feature = "ir")]
250fn node_kind(n: &J) -> Result<&'static str, BehaviorError> {
251 if n.get("fanout").is_some() {
252 Ok("fanout")
253 } else if n.get("map").is_some() {
254 Ok("map")
255 } else if n.get("cond").is_some() {
256 Ok("cond")
257 } else if n.get("component").is_some() {
258 Ok("componentRef")
259 } else {
260 bfail(
261 BehaviorFailureCode::UnknownNodeKind,
262 format!(
263 "body node '{}' is not componentRef/map/cond/fanout",
264 n.get("id").and_then(|v| v.as_str()).unwrap_or("?")
265 ),
266 )
267 }
268}
269
270#[cfg(feature = "ir")]
271fn node_sub(n: &J) -> &J {
272 if let Some(f) = n.get("fanout") {
273 f
274 } else if let Some(m) = n.get("map") {
275 m
276 } else if let Some(c) = n.get("cond") {
277 c
278 } else {
279 n
280 }
281}
282
283#[cfg(feature = "ir")]
284fn node_parent(n: &J) -> Option<&str> {
285 node_sub(n).get("parent").and_then(|v| v.as_str())
286}
287
288#[cfg(feature = "ir")]
289fn node_bind_field(n: &J) -> Option<String> {
290 if n.get("map").is_some() || n.get("cond").is_some() || n.get("fanout").is_some() {
291 return None;
292 }
293 n.get("bindField")
294 .and_then(|v| v.as_str())
295 .map(str::to_string)
296}
297
298#[cfg(feature = "ir")]
299fn node_relation_kind(n: &J) -> Option<RelationKind> {
300 if n.get("cond").is_some() {
301 return None;
302 }
303 let sub = if n.get("map").is_some() || n.get("fanout").is_some() {
304 node_sub(n)
305 } else {
306 n
307 };
308 match sub.get("relationKind").and_then(|v| v.as_str()) {
309 Some("connection") => Some(RelationKind::Connection),
310 Some(_) => Some(RelationKind::Single),
311 None => None,
312 }
313}
314
315#[cfg(feature = "ir")]
316fn node_relation_kind_str(n: &J) -> Option<&str> {
317 if n.get("cond").is_some() {
318 return None;
319 }
320 let sub = if n.get("map").is_some() || n.get("fanout").is_some() {
321 node_sub(n)
322 } else {
323 n
324 };
325 sub.get("relationKind").and_then(|v| v.as_str())
326}
327
328#[cfg(feature = "ir")]
329fn node_policy(n: &J) -> Option<String> {
330 if n.get("cond").is_some() {
331 return None;
332 }
333 let sub = if n.get("map").is_some() || n.get("fanout").is_some() {
334 node_sub(n)
335 } else {
336 n
337 };
338 sub.get("policy")
339 .and_then(|v| v.as_str())
340 .map(str::to_string)
341}
342
343#[cfg(feature = "ir")]
344fn eval_ports(ports: &J, scope: &[(String, Value)]) -> Result<Vec<(String, Value)>, BehaviorError> {
345 let obj = ports.as_object().ok_or_else(|| BehaviorError {
346 code: BehaviorFailureCode::UnknownNodeKind.as_str().to_string(),
347 message: "ports must be an object".into(),
348 })?;
349 let mut out = Vec::with_capacity(obj.len());
350 for (k, v) in obj {
351 out.push((k.clone(), evaluate_expression(v, scope)?));
352 }
353 Ok(out)
354}
355
356#[cfg(feature = "ir")]
358fn parse_plan(plan: Option<&J>) -> Option<ExecutionPlanSpec> {
359 let p = plan?;
360 if p.is_null() {
361 return None;
362 }
363 let groups = p
364 .get("groups")?
365 .as_array()?
366 .iter()
367 .map(|g| {
368 g.as_array()
369 .map(|row| {
370 row.iter()
371 .filter_map(|i| i.as_u64().map(|x| x as usize))
372 .collect()
373 })
374 .unwrap_or_default()
375 })
376 .collect();
377 let concurrency = p.get("concurrency").and_then(|c| c.as_i64()).unwrap_or(1);
378 Some(ExecutionPlanSpec {
379 groups,
380 concurrency,
381 })
382}
383
384#[cfg(feature = "ir")]
393pub fn run_behavior(
394 ir: &J,
395 handlers: &mut dyn ComponentExec,
396 input: &[(String, Value)],
397 entry: Option<&str>,
398) -> Result<Value, BehaviorError> {
399 let components = ir
400 .get("components")
401 .and_then(|c| c.as_array())
402 .ok_or_else(|| BehaviorError {
403 code: BehaviorFailureCode::UnknownEntry.as_str().to_string(),
404 message: "IR.components must be an array".into(),
405 })?;
406 let comp = match entry {
407 Some(name) => components
408 .iter()
409 .find(|c| c.get("name").and_then(|n| n.as_str()) == Some(name)),
410 None => components.first(),
411 };
412 let comp = match comp {
413 Some(c) => c,
414 None => {
415 return bfail(
416 BehaviorFailureCode::UnknownEntry,
417 format!("component '{}' not found in IR", entry.unwrap_or("<first>")),
418 )
419 }
420 };
421
422 let body: Vec<J> = comp
423 .get("body")
424 .and_then(|b| b.as_array())
425 .cloned()
426 .unwrap_or_default();
427
428 let index_of = |id: &str| {
429 body.iter()
430 .position(|n| n.get("id").and_then(|v| v.as_str()) == Some(id))
431 };
432
433 let mut ops: Vec<OpSpec> = Vec::with_capacity(body.len());
435 for n in &body {
436 let parent = node_parent(n).and_then(index_of);
437 ops.push(OpSpec {
438 id: n
439 .get("id")
440 .and_then(|v| v.as_str())
441 .unwrap_or_default()
442 .to_string(),
443 parent,
444 bind_field: node_bind_field(n),
445 relation_kind: node_relation_kind(n),
446 policy: node_policy(n),
447 });
448 }
449
450 run_behavior_inner(
451 &body,
452 &ops,
453 parse_plan(comp.get("plan")),
454 comp.get("output").cloned().unwrap_or(J::Null),
455 &bind_input_scope(comp, input),
456 handlers,
457 )
458}
459
460#[cfg(feature = "ir")]
472fn bind_input_scope(comp: &J, input: &[(String, Value)]) -> Vec<(String, Value)> {
473 let mut scope: Vec<(String, Value)> = input.to_vec();
474 if let Some(ports) = comp.get("inputPorts").and_then(|p| p.as_object()) {
475 for (name, schema) in ports {
476 let optional = schema.get("required").and_then(|r| r.as_bool()) == Some(false);
477 if optional && !scope.iter().any(|(k, _)| k == name) {
478 scope.push((name.clone(), Value::Null));
479 }
480 }
481 }
482 scope
483}
484
485#[cfg(feature = "ir")]
489fn run_behavior_inner(
490 body: &[J],
491 ops: &[OpSpec],
492 plan: Option<ExecutionPlanSpec>,
493 output: J,
494 input: &[(String, Value)],
495 handlers: &mut dyn ComponentExec,
496) -> Result<Value, BehaviorError> {
497 let index_of = |id: &str| {
498 body.iter()
499 .position(|n| n.get("id").and_then(|v| v.as_str()) == Some(id))
500 };
501
502 let mut results: Vec<(String, Value)> = Vec::new();
503 let mut pending_err: Option<BehaviorError> = None;
504
505 let run = {
506 let results_cell = &mut results;
507 let err_cell = &mut pending_err;
508 let exec = |op: &OpSpec, _bound: Option<&Value>| -> ExecOutcome {
509 if err_cell.is_some() {
510 return ExecOutcome::Error("aborted".into());
511 }
512 let idx = match index_of(&op.id) {
513 Some(i) => i,
514 None => {
515 *err_cell = Some(BehaviorError {
516 code: BehaviorFailureCode::UnknownNodeKind.as_str().to_string(),
517 message: format!("no body node for op '{}'", op.id),
518 });
519 return ExecOutcome::Error("aborted".into());
520 }
521 };
522 let node = &body[idx];
523
524 let base_scope = |extra: Option<(&str, &Value)>| -> Vec<(String, Value)> {
525 let mut s: Vec<(String, Value)> = input.to_vec();
526 for (k, v) in results_cell.iter() {
527 s.push((k.clone(), v.clone()));
528 }
529 if let Some((k, v)) = extra {
530 s.push((k.to_string(), v.clone()));
531 }
532 s
533 };
534
535 let outcome = match node_kind(node) {
536 Err(e) => {
537 *err_cell = Some(e);
538 return ExecOutcome::Error("aborted".into());
539 }
540 Ok("cond") => {
541 let c = node.get("cond").unwrap();
542 let cond_expr = serde_json::json!({
543 "cond": [c.get("if"), c.get("then"), c.get("else")]
544 });
545 match evaluate_expression(&cond_expr, &base_scope(None)) {
546 Ok(v) => ExecOutcome::Ok(v),
547 Err(e) => {
548 *err_cell = Some(e.into());
549 return ExecOutcome::Error("aborted".into());
550 }
551 }
552 }
553 Ok("map") => {
554 let m = node.get("map").unwrap();
555 let over = match evaluate_expression(
556 m.get("over").unwrap_or(&J::Null),
557 &base_scope(None),
558 ) {
559 Ok(v) => v,
560 Err(e) => {
561 *err_cell = Some(e.into());
562 return ExecOutcome::Error("aborted".into());
563 }
564 };
565 let arr = match &over {
566 Value::Arr(a) => a.clone(),
567 _ => {
568 *err_cell = Some(BehaviorError {
569 code: BehaviorFailureCode::MapOverNotArray.as_str().to_string(),
570 message: format!("map '{}': 'over' is not an array", op.id),
571 });
572 return ExecOutcome::Error("aborted".into());
573 }
574 };
575 let component = m.get("component").and_then(|v| v.as_str()).unwrap_or("");
576 let as_name = m.get("as").and_then(|v| v.as_str()).unwrap_or("$");
577 let ports_j = m
578 .get("ports")
579 .cloned()
580 .unwrap_or(J::Object(Default::default()));
581 let when = m.get("when");
582 let batched = m.get("batched").and_then(|v| v.as_bool()).unwrap_or(false);
583 let into = m.get("into").and_then(|v| v.as_str());
584
585 let keep = |scope: &[(String, Value)]| -> Result<bool, BehaviorError> {
588 match when {
589 None => Ok(true),
590 Some(w) => {
591 let cond_expr = serde_json::json!({ "cond": [w, true, false] });
592 Ok(matches!(
593 evaluate_expression(&cond_expr, scope)?,
594 Value::Bool(true)
595 ))
596 }
597 }
598 };
599
600 let mut kept_idx: Vec<usize> = Vec::new(); let collected: Vec<Value>;
602 if batched {
603 let mut items: Vec<Value> = Vec::new();
605 for (i, el) in arr.iter().enumerate() {
606 let scope = base_scope(Some((as_name, el)));
607 match keep(&scope) {
608 Ok(false) => continue,
609 Ok(true) => {}
610 Err(e) => {
611 *err_cell = Some(e);
612 return ExecOutcome::Error("aborted".into());
613 }
614 }
615 match eval_ports(&ports_j, &scope) {
616 Ok(p) => items.push(Value::Obj(p)),
617 Err(e) => {
618 *err_cell = Some(e);
619 return ExecOutcome::Error("aborted".into());
620 }
621 }
622 kept_idx.push(i);
623 }
624 if items.is_empty() {
625 collected = Vec::new(); } else {
627 let want = items.len();
628 let batch_ports = vec![("items".to_string(), Value::Arr(items))];
629 match handlers.exec_ctx(&op.id, component, &batch_ports, None) {
630 None => {
631 *err_cell = Some(BehaviorError {
632 code: BehaviorFailureCode::UnknownComponent
633 .as_str()
634 .to_string(),
635 message: format!(
636 "component '{component}' has no handler (fail-closed)"
637 ),
638 });
639 return ExecOutcome::Error("aborted".into());
640 }
641 Some(ExecOutcome::Error(e)) => return ExecOutcome::Error(e),
642 Some(ExecOutcome::Ok(Value::Arr(r))) if r.len() == want => {
643 collected = r;
644 }
645 Some(ExecOutcome::Ok(_)) => {
646 *err_cell = Some(BehaviorError {
647 code: BehaviorFailureCode::MapBatchResultMismatch
648 .as_str()
649 .to_string(),
650 message: format!(
651 "map '{}': batched handler must return a list aligned to items (want {want})",
652 op.id
653 ),
654 });
655 return ExecOutcome::Error("aborted".into());
656 }
657 }
658 }
659 } else {
660 let mut out: Vec<Value> = Vec::with_capacity(arr.len());
661 for (i, el) in arr.iter().enumerate() {
662 let scope = base_scope(Some((as_name, el)));
663 match keep(&scope) {
664 Ok(false) => continue,
665 Ok(true) => {}
666 Err(e) => {
667 *err_cell = Some(e);
668 return ExecOutcome::Error("aborted".into());
669 }
670 }
671 let ports = match eval_ports(&ports_j, &scope) {
672 Ok(p) => p,
673 Err(e) => {
674 *err_cell = Some(e);
675 return ExecOutcome::Error("aborted".into());
676 }
677 };
678 match handlers.exec_ctx(&op.id, component, &ports, Some(el)) {
679 None => {
680 *err_cell = Some(BehaviorError {
681 code: BehaviorFailureCode::UnknownComponent
682 .as_str()
683 .to_string(),
684 message: format!(
685 "component '{component}' has no handler (fail-closed)"
686 ),
687 });
688 return ExecOutcome::Error("aborted".into());
689 }
690 Some(ExecOutcome::Error(e)) => return ExecOutcome::Error(e),
691 Some(ExecOutcome::Ok(v)) => out.push(v),
692 }
693 kept_idx.push(i);
694 }
695 collected = out;
696 }
697
698 match into {
699 None => ExecOutcome::Ok(Value::Arr(collected)),
700 Some(key) => {
701 let mut augmented: Vec<Value> = Vec::with_capacity(arr.len());
704 let mut k = 0usize;
705 for (i, el) in arr.iter().enumerate() {
706 if k < kept_idx.len() && kept_idx[k] == i {
707 let fields = match el {
708 Value::Obj(f) => f.clone(),
709 _ => {
710 *err_cell = Some(BehaviorError {
711 code: BehaviorFailureCode::MapIntoElementNotObject
712 .as_str()
713 .to_string(),
714 message: format!(
715 "map '{}': 'into' requires object elements (element {i} is not an object)",
716 op.id
717 ),
718 });
719 return ExecOutcome::Error("aborted".into());
720 }
721 };
722 let mut fields = fields;
723 let val = collected[k].clone();
724 match fields.iter_mut().find(|(fk, _)| fk == key) {
725 Some(slot) => slot.1 = val,
726 None => fields.push((key.to_string(), val)),
727 }
728 augmented.push(Value::Obj(fields));
729 k += 1;
730 } else {
731 augmented.push(el.clone());
732 }
733 }
734 ExecOutcome::Ok(Value::Arr(augmented))
735 }
736 }
737 }
738 Ok("fanout") => {
739 let f = node.get("fanout").unwrap();
743 let over = match evaluate_expression(
744 f.get("over").unwrap_or(&J::Null),
745 &base_scope(None),
746 ) {
747 Ok(v) => v,
748 Err(e) => {
749 *err_cell = Some(e.into());
750 return ExecOutcome::Error("aborted".into());
751 }
752 };
753 let arr = match &over {
754 Value::Arr(a) => a.clone(),
755 _ => {
756 *err_cell = Some(BehaviorError {
757 code: BehaviorFailureCode::FanoutOverNotArray.as_str().to_string(),
758 message: format!("fanout '{}': 'over' is not an array", op.id),
759 });
760 return ExecOutcome::Error("aborted".into());
761 }
762 };
763 let component = f.get("component").and_then(|v| v.as_str()).unwrap_or("");
764 let as_name = f.get("as").and_then(|v| v.as_str()).unwrap_or("$");
765 let ports_j = f
766 .get("ports")
767 .cloned()
768 .unwrap_or(J::Object(Default::default()));
769 let dedupe_key = f.get("dedupeKey").and_then(|v| v.as_str()).unwrap_or("");
770 let drop = f.get("drop").and_then(|v| v.as_str()).unwrap_or("dangling");
771 let implicit_source = f.get("implicitSource").and_then(|v| v.as_str());
772 let mut items: Vec<Value> = Vec::with_capacity(arr.len());
773 for el in arr.iter() {
774 let scope = base_scope(Some((as_name, el)));
775 match eval_ports(&ports_j, &scope) {
776 Ok(p) => items.push(Value::Obj(p)),
777 Err(e) => {
778 *err_cell = Some(e);
779 return ExecOutcome::Error("aborted".into());
780 }
781 }
782 }
783 if items.is_empty() {
784 ExecOutcome::Ok(Value::Obj(vec![
785 ("items".into(), Value::Arr(vec![])),
786 ("cursor".into(), Value::Null),
787 ]))
788 } else {
789 let want = items.len();
790 let batch_ports = vec![("items".to_string(), Value::Arr(items))];
791 match handlers.exec_ctx(&op.id, component, &batch_ports, None) {
792 None => {
793 *err_cell = Some(BehaviorError {
794 code: BehaviorFailureCode::UnknownComponent
795 .as_str()
796 .to_string(),
797 message: format!(
798 "component '{component}' has no handler (fail-closed)"
799 ),
800 });
801 ExecOutcome::Error("aborted".into())
802 }
803 Some(ExecOutcome::Error(e)) => ExecOutcome::Error(e),
804 Some(ExecOutcome::Ok(Value::Arr(r))) if r.len() == want => {
805 let deduped =
806 fanout_dedup_drop(r, dedupe_key, drop, implicit_source);
807 ExecOutcome::Ok(Value::Obj(vec![
808 ("items".into(), Value::Arr(deduped)),
809 ("cursor".into(), Value::Null),
810 ]))
811 }
812 Some(ExecOutcome::Ok(_)) => {
813 *err_cell = Some(BehaviorError {
814 code: BehaviorFailureCode::FanoutBatchResultMismatch
815 .as_str()
816 .to_string(),
817 message: format!(
818 "fanout '{}': batched handler must return a list aligned to the deduped id list (want {want})",
819 op.id
820 ),
821 });
822 ExecOutcome::Error("aborted".into())
823 }
824 }
825 }
826 }
827 Ok(_) => {
828 let component = node.get("component").and_then(|v| v.as_str()).unwrap_or("");
829 let ports_j = node
830 .get("ports")
831 .cloned()
832 .unwrap_or(J::Object(Default::default()));
833 let ports = match eval_ports(&ports_j, &base_scope(None)) {
834 Ok(p) => p,
835 Err(e) => {
836 *err_cell = Some(e);
837 return ExecOutcome::Error("aborted".into());
838 }
839 };
840 match handlers.exec_ctx(&op.id, component, &ports, None) {
841 None => {
842 *err_cell = Some(BehaviorError {
843 code: BehaviorFailureCode::UnknownComponent.as_str().to_string(),
844 message: format!(
845 "component '{component}' has no handler (fail-closed)"
846 ),
847 });
848 return ExecOutcome::Error("aborted".into());
849 }
850 Some(o) => o,
851 }
852 }
853 };
854
855 if let ExecOutcome::Ok(v) = &outcome {
856 results_cell.push((op.id.clone(), v.clone()));
857 }
858 outcome
859 };
860
861 run_plan(plan.as_ref(), ops, exec)
862 };
863
864 if let Some(e) = pending_err {
867 return Err(e);
868 }
869 let run = run?;
870
871 for (i, id_val) in run.skipped.iter().enumerate() {
874 let _ = i;
875 if let Some(idx) = index_of(id_val) {
876 let rk = node_relation_kind_str(&body[idx]);
877 let unproduced = if rk == Some("connection") {
878 Value::Obj(vec![
879 ("items".into(), Value::Arr(vec![])),
880 ("cursor".into(), Value::Null),
881 ])
882 } else {
883 Value::Null
884 };
885 if scope_get(&results, id_val).is_none() {
886 results.push((id_val.clone(), unproduced));
887 }
888 }
889 }
890
891 let scope: Vec<(String, Value)> = {
892 let mut s: Vec<(String, Value)> = input.to_vec();
893 s.extend(results.iter().cloned());
894 s
895 };
896 Ok(evaluate_expression(&output, &scope)?)
897}