Skip to main content

pharos_native/runtime/
dynamic.rs

1use std::cmp::Ordering;
2use std::collections::BTreeMap;
3use std::env;
4use std::fs;
5use std::path::Path;
6use std::time::{SystemTime, UNIX_EPOCH};
7
8use serde_json::{Map, Value};
9use sha2::{Digest, Sha256};
10
11use crate::config::AppRuntime;
12
13use super::connection::NativeTransportType;
14use super::http::HttpRequest;
15use super::json_commands;
16
17/// Public constant `DYNAMIC_HOST_PROFILE` exposed by this module.
18///
19/// # Examples
20///
21/// ```rust,ignore
22/// let _ = pharos_native::runtime::dynamic::DYNAMIC_HOST_PROFILE;
23/// ```
24///
25/// # Related
26///
27/// - [`crate::runtime::dynamic`] for the surrounding module surface.
28pub const DYNAMIC_HOST_PROFILE: &str = "dynamic-app";
29
30/// Performs dynamic runtime host profile.
31///
32/// # Returns
33///
34/// Returns the computed value produced by [`crate::runtime::dynamic::dynamic_runtime_host_profile`].
35///
36/// # Examples
37///
38/// ```rust,ignore
39/// let _result = pharos_native::runtime::dynamic::dynamic_runtime_host_profile();
40/// ```
41///
42/// # Related
43///
44/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
45pub fn dynamic_runtime_host_profile() -> &'static str {
46    DYNAMIC_HOST_PROFILE
47}
48
49/// Performs request has session cookie.
50///
51/// # Parameters
52///
53/// - `cookie_header`: Input consumed by [`crate::runtime::dynamic::request_has_session_cookie`].
54///
55/// # Returns
56///
57/// Returns `true` when the condition checked by [`crate::runtime::dynamic::request_has_session_cookie`] holds and `false` otherwise.
58///
59/// # Examples
60///
61/// ```rust,ignore
62/// let _result = pharos_native::runtime::dynamic::request_has_session_cookie("session=abc");
63/// ```
64///
65/// # Related
66///
67/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
68pub fn request_has_session_cookie(cookie_header: &str) -> bool {
69    pharos_runtime_security::request_has_session_cookie(cookie_header)
70}
71
72/// Performs mut JSON obj get string.
73///
74/// # Parameters
75///
76/// - `object`: Input consumed by [`crate::runtime::dynamic::mut_json_obj_get_string`].
77/// - `key`: Input consumed by [`crate::runtime::dynamic::mut_json_obj_get_string`].
78///
79/// # Returns
80///
81/// Returns [`Some`] when [`crate::runtime::dynamic::mut_json_obj_get_string`] can produce a value and [`None`] otherwise.
82///
83/// # Examples
84///
85/// ```rust,ignore
86/// let _result = pharos_native::runtime::dynamic::mut_json_obj_get_string(todo!(), "key");
87/// ```
88///
89/// # Related
90///
91/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
92pub fn mut_json_obj_get_string(object: &Value, key: &str) -> Option<String> {
93    object
94        .as_object()
95        .and_then(|map| map.get(key))
96        .and_then(Value::as_str)
97        .map(ToOwned::to_owned)
98}
99
100/// Performs generated surface label.
101///
102/// # Parameters
103///
104/// - `field_name`: Input consumed by [`crate::runtime::dynamic::generated_surface_label`].
105///
106/// # Returns
107///
108/// Returns [`Some`] when [`crate::runtime::dynamic::generated_surface_label`] can produce a value and [`None`] otherwise.
109///
110/// # Performance
111///
112/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::generated_surface_label`].
113///
114/// # Examples
115///
116/// ```rust,ignore
117/// let _result = pharos_native::runtime::dynamic::generated_surface_label(todo!());
118/// ```
119///
120/// # Related
121///
122/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
123pub fn generated_surface_label(field_name: Option<&str>) -> Option<String> {
124    let field_name = field_name.unwrap_or("value");
125    let mut next_word = true;
126    Some(
127        field_name
128            .chars()
129            .map(|ch| {
130                let mut out = if ch == '_' { ' ' } else { ch };
131                if next_word && out.is_ascii_lowercase() {
132                    out = out.to_ascii_uppercase();
133                }
134                next_word = out == ' ';
135                out
136            })
137            .collect(),
138    )
139}
140
141#[derive(Debug, Default, Clone, Eq, PartialEq)]
142/// Public struct `RuntimeJsonValueList` used by this module.
143///
144/// # Fields
145///
146/// - `items`: Stored field on `RuntimeJsonValueList`.
147///
148/// # Examples
149///
150/// ```rust,ignore
151/// let _ = std::mem::size_of::<pharos_native::runtime::dynamic::RuntimeJsonValueList>();
152/// ```
153///
154/// # Related
155///
156/// - [`crate::runtime::dynamic`] for constructors, helpers, and neighboring runtime types.
157pub struct RuntimeJsonValueList {
158    pub items: Vec<Value>,
159}
160
161/// Performs runtime JSON value list free.
162///
163/// # Parameters
164///
165/// - `list`: Input consumed by [`crate::runtime::dynamic::runtime_json_value_list_free`].
166///
167/// # Returns
168///
169/// Returns after [`crate::runtime::dynamic::runtime_json_value_list_free`] applies its in-place effect.
170///
171/// # Examples
172///
173/// ```rust,ignore
174/// let _result = pharos_native::runtime::dynamic::runtime_json_value_list_free(todo!());
175/// ```
176///
177/// # Related
178///
179/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
180pub fn runtime_json_value_list_free(list: &mut RuntimeJsonValueList) {
181    list.items.clear();
182}
183
184/// Performs runtime JSON value list append.
185///
186/// # Parameters
187///
188/// - `list`: Input consumed by [`crate::runtime::dynamic::runtime_json_value_list_append`].
189/// - `item`: Input consumed by [`crate::runtime::dynamic::runtime_json_value_list_append`].
190///
191/// # Returns
192///
193/// Returns `true` when the condition checked by [`crate::runtime::dynamic::runtime_json_value_list_append`] holds and `false` otherwise.
194///
195/// # Examples
196///
197/// ```rust,ignore
198/// let _result = pharos_native::runtime::dynamic::runtime_json_value_list_append(todo!(), todo!());
199/// ```
200///
201/// # Related
202///
203/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
204pub fn runtime_json_value_list_append(list: &mut RuntimeJsonValueList, item: &Value) -> bool {
205    list.items.push(item.clone());
206    true
207}
208
209/// Performs runtime text buffer append format.
210///
211/// # Parameters
212///
213/// - `buffer`: Input consumed by [`crate::runtime::dynamic::runtime_text_buffer_append_format`].
214/// - `formatted_text`: Input consumed by [`crate::runtime::dynamic::runtime_text_buffer_append_format`].
215///
216/// # Returns
217///
218/// Returns `true` when the condition checked by [`crate::runtime::dynamic::runtime_text_buffer_append_format`] holds and `false` otherwise.
219///
220/// # Examples
221///
222/// ```rust,ignore
223/// let _result = pharos_native::runtime::dynamic::runtime_text_buffer_append_format(&mut [], todo!());
224/// ```
225///
226/// # Related
227///
228/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
229pub fn runtime_text_buffer_append_format(buffer: &mut String, formatted_text: &str) -> bool {
230    buffer.push_str(formatted_text);
231    true
232}
233
234/// Performs HTML escape.
235fn html_escape(input: &str) -> String {
236    input
237        .replace('&', "&amp;")
238        .replace('<', "&lt;")
239        .replace('>', "&gt;")
240        .replace('"', "&quot;")
241}
242
243/// Performs runtime text buffer append HTML escaped.
244///
245/// # Parameters
246///
247/// - `buffer`: Input consumed by [`crate::runtime::dynamic::runtime_text_buffer_append_html_escaped`].
248/// - `text`: Input consumed by [`crate::runtime::dynamic::runtime_text_buffer_append_html_escaped`].
249///
250/// # Returns
251///
252/// Returns `true` when the condition checked by [`crate::runtime::dynamic::runtime_text_buffer_append_html_escaped`] holds and `false` otherwise.
253///
254/// # Examples
255///
256/// ```rust,ignore
257/// let _result = pharos_native::runtime::dynamic::runtime_text_buffer_append_html_escaped(&mut [], "{}");
258/// ```
259///
260/// # Related
261///
262/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
263pub fn runtime_text_buffer_append_html_escaped(buffer: &mut String, text: Option<&str>) -> bool {
264    buffer.push_str(&html_escape(text.unwrap_or("")));
265    true
266}
267
268/// Normalizes a public origin and rejects control characters or unsupported schemes.
269///
270/// # Parameters
271///
272/// - `configured_origin`: Input consumed by [`crate::runtime::dynamic::normalized_public_origin`].
273///
274/// # Returns
275///
276/// Returns [`Some`] when [`crate::runtime::dynamic::normalized_public_origin`] can produce a value and [`None`] otherwise.
277///
278/// # Performance
279///
280/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::normalized_public_origin`].
281///
282/// # Examples
283///
284/// ```rust,ignore
285/// let _result = pharos_native::runtime::dynamic::normalized_public_origin(todo!());
286/// ```
287///
288/// # Related
289///
290/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
291pub fn normalized_public_origin(configured_origin: &str) -> Option<String> {
292    pharos_runtime_dynamic::normalized_public_origin(configured_origin)
293}
294
295/// Returns the scheme trusted for one request transport.
296///
297/// # Parameters
298///
299/// - `request`: Input consumed by [`crate::runtime::dynamic::request_origin_scheme`].
300/// - `transport_type`: Input consumed by [`crate::runtime::dynamic::request_origin_scheme`].
301/// - `direct_client_ip`: Input consumed by [`crate::runtime::dynamic::request_origin_scheme`].
302/// - `trusted_proxy_cidrs`: Input consumed by [`crate::runtime::dynamic::request_origin_scheme`].
303///
304/// # Returns
305///
306/// Returns the computed value produced by [`crate::runtime::dynamic::request_origin_scheme`].
307///
308/// # Performance
309///
310/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::request_origin_scheme`].
311///
312/// # Examples
313///
314/// ```rust,ignore
315/// let _result = pharos_native::runtime::dynamic::request_origin_scheme(todo!(), todo!(), todo!(), "127.0.0.1/32");
316/// ```
317///
318/// # Related
319///
320/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
321pub fn request_origin_scheme(
322    request: Option<&HttpRequest>,
323    transport_type: NativeTransportType,
324    direct_client_ip: Option<&str>,
325    trusted_proxy_cidrs: Option<&str>,
326) -> &'static str {
327    let Some(request) = request else {
328        return "http";
329    };
330    let forwarded_https = request.headers.lines().any(|line| {
331        let (name, value) = line.split_once(':').unwrap_or(("", ""));
332        name.trim().eq_ignore_ascii_case("x-forwarded-proto")
333            && value.trim().eq_ignore_ascii_case("https")
334    });
335    let origin_https = request.headers.lines().any(|line| {
336        let (name, value) = line.split_once(':').unwrap_or(("", ""));
337        name.trim().eq_ignore_ascii_case("origin") && value.trim_start().starts_with("https://")
338    });
339    let trusted_forward = match transport_type {
340        NativeTransportType::Unix => forwarded_https,
341        NativeTransportType::Tcp => {
342            forwarded_https
343                && direct_client_ip.is_some()
344                && trusted_proxy_cidrs.is_some_and(|value| !value.trim().is_empty())
345        }
346        NativeTransportType::Unknown => false,
347    };
348    if trusted_forward || origin_https {
349        "https"
350    } else {
351        "http"
352    }
353}
354
355/// Builds the app-relative verification route after validating the token.
356///
357/// # Parameters
358///
359/// - `app`: Input consumed by [`crate::runtime::dynamic::verification_token_path`].
360/// - `token`: Input consumed by [`crate::runtime::dynamic::verification_token_path`].
361///
362/// # Returns
363///
364/// Returns [`Some`] when [`crate::runtime::dynamic::verification_token_path`] can produce a value and [`None`] otherwise.
365///
366/// # Performance
367///
368/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::verification_token_path`].
369///
370/// # Examples
371///
372/// ```rust,ignore
373/// let _result = pharos_native::runtime::dynamic::verification_token_path(todo!(), todo!());
374/// ```
375///
376/// # Related
377///
378/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
379pub fn verification_token_path(app: &AppRuntime, token: &str) -> Option<String> {
380    if token.is_empty()
381        || token.len() > 512
382        || token.bytes().any(|byte| !byte.is_ascii_alphanumeric())
383    {
384        return None;
385    }
386    let base = app
387        .base_path
388        .as_deref()
389        .filter(|value| !value.is_empty() && *value != "/")
390        .unwrap_or("");
391    Some(format!(
392        "{base}/accounts/verify-email/{token}/?app_id={}",
393        app.app_id.as_deref().unwrap_or_default()
394    ))
395}
396
397/// Builds a verification URL from configured origin or a validated Host header.
398///
399/// # Parameters
400///
401/// - `app`: Input consumed by [`crate::runtime::dynamic::verification_url`].
402/// - `request`: Input consumed by [`crate::runtime::dynamic::verification_url`].
403/// - `token`: Input consumed by [`crate::runtime::dynamic::verification_url`].
404///
405/// # Returns
406///
407/// Returns [`Some`] when [`crate::runtime::dynamic::verification_url`] can produce a value and [`None`] otherwise.
408///
409/// # Performance
410///
411/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::verification_url`].
412///
413/// # Examples
414///
415/// ```rust,ignore
416/// let _result = pharos_native::runtime::dynamic::verification_url(todo!(), todo!(), todo!());
417/// ```
418///
419/// # Related
420///
421/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
422pub fn verification_url(app: &AppRuntime, request: &HttpRequest, token: &str) -> Option<String> {
423    let path = verification_token_path(app, token)?;
424    let origin = app
425        .runtime_conf_path
426        .as_deref()
427        .and_then(|path| {
428            json_commands::dynamic_config_value(
429                Path::new(app.app_root.as_deref()?),
430                Some(Path::new(path)),
431                "base_url",
432            )
433        })
434        .and_then(|value| normalized_public_origin(&value));
435    let origin = origin.or_else(|| {
436        let host = request.headers.lines().find_map(|line| {
437            let (name, value) = line.split_once(':')?;
438            name.trim()
439                .eq_ignore_ascii_case("host")
440                .then_some(value.trim())
441        })?;
442        let scheme = request_origin_scheme(Some(request), NativeTransportType::Tcp, None, None);
443        normalized_public_origin(&format!("{scheme}://{host}"))
444    })?;
445    Some(format!("{origin}{path}"))
446}
447
448/// Extracts one form value from the request body with multipart support delegated to the bounded parser.
449///
450/// # Parameters
451///
452/// - `request`: Input consumed by [`crate::runtime::dynamic::request_form_value`].
453/// - `field`: Input consumed by [`crate::runtime::dynamic::request_form_value`].
454///
455/// # Returns
456///
457/// Returns [`Some`] when [`crate::runtime::dynamic::request_form_value`] can produce a value and [`None`] otherwise.
458///
459/// # Examples
460///
461/// ```rust,ignore
462/// let _result = pharos_native::runtime::dynamic::request_form_value(todo!(), "field");
463/// ```
464///
465/// # Related
466///
467/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
468pub fn request_form_value(request: &HttpRequest, field: &str) -> Option<String> {
469    if field.is_empty() {
470        return None;
471    }
472    let body = String::from_utf8_lossy(&request.body);
473    if request.body_file_path.is_none()
474        && request
475            .content_type
476            .as_deref()
477            .is_some_and(|value| value.starts_with("multipart/form-data"))
478    {
479        return request.content_type.as_deref().and_then(|content_type| {
480            json_commands::request_multipart_field_value(content_type, &request.body, field)
481        });
482    }
483    json_commands::request_form_field_value(
484        request.content_type.as_deref(),
485        Some(body.as_ref()),
486        request.body_file_path.as_deref().map(Path::new),
487        field,
488    )
489}
490
491/// Extracts one decoded query parameter from the request target.
492///
493/// # Parameters
494///
495/// - `request`: Input consumed by [`crate::runtime::dynamic::request_query_value`].
496/// - `field`: Input consumed by [`crate::runtime::dynamic::request_query_value`].
497///
498/// # Returns
499///
500/// Returns [`Some`] when [`crate::runtime::dynamic::request_query_value`] can produce a value and [`None`] otherwise.
501///
502/// # Examples
503///
504/// ```rust,ignore
505/// let _result = pharos_native::runtime::dynamic::request_query_value(todo!(), "field");
506/// ```
507///
508/// # Related
509///
510/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
511pub fn request_query_value(request: &HttpRequest, field: &str) -> Option<String> {
512    request
513        .target
514        .split_once('?')
515        .and_then(|(_, query)| json_commands::request_query_value(query, field))
516}
517
518/// Resolves one bounded derived input from the request envelope.
519fn dynamic_derived_input_value(spec: &Value, request: &HttpRequest) -> Option<String> {
520    let source = spec
521        .get("source")
522        .and_then(Value::as_str)
523        .unwrap_or_default();
524    match source {
525        "literal" => spec
526            .get("value")
527            .map(value_to_text)
528            .filter(|value| !value.is_empty()),
529        "header" => spec
530            .get("header")
531            .and_then(Value::as_str)
532            .and_then(|header| request_header_value(request, header))
533            .filter(|value| !value.is_empty()),
534        "query" => spec
535            .get("param")
536            .or_else(|| spec.get("query_param"))
537            .and_then(Value::as_str)
538            .and_then(|field| request_query_value(request, field))
539            .filter(|value| !value.is_empty()),
540        "form" => spec
541            .get("from_field")
542            .or_else(|| spec.get("input_field"))
543            .and_then(Value::as_str)
544            .and_then(|field| request_form_value(request, field))
545            .filter(|value| !value.is_empty()),
546        "context_method" => (!request.method.is_empty()).then(|| request.method.clone()),
547        "context_path" => {
548            let path = request_path(Some(request));
549            (!path.is_empty()).then_some(path)
550        }
551        _ => None,
552    }
553}
554
555/// Performs dynamic apply default input hooks.
556fn dynamic_apply_default_input_hooks(
557    overrides: &mut BTreeMap<String, Option<String>>,
558    action: &Value,
559    request: &HttpRequest,
560) -> Result<(), i32> {
561    let Some(rows) = action
562        .get("hook_contract")
563        .and_then(|value| value.get("default_inputs"))
564        .and_then(Value::as_array)
565    else {
566        return Ok(());
567    };
568    if rows.len() > 32 {
569        return Err(422);
570    }
571    for row in rows {
572        let field = row
573            .get("field")
574            .and_then(Value::as_str)
575            .filter(|value| !value.is_empty() && value.len() <= 128)
576            .ok_or(422)?;
577        if dynamic_action_input_value(request, overrides, field)
578            .is_some_and(|value| !value.is_empty())
579        {
580            continue;
581        }
582        overrides.insert(field.to_owned(), dynamic_derived_input_value(row, request));
583    }
584    Ok(())
585}
586
587/// Returns one stable action-owned input override map after applying declared normalization hooks.
588fn dynamic_action_input_overrides(
589    action: &Value,
590    request: &HttpRequest,
591) -> Result<BTreeMap<String, Option<String>>, i32> {
592    let mut overrides = BTreeMap::new();
593    let Some(hook_contract) = action.get("hook_contract").and_then(Value::as_object) else {
594        return Ok(overrides);
595    };
596    if let Some(rows) = hook_contract
597        .get("derived_inputs")
598        .and_then(Value::as_array)
599    {
600        if rows.len() > 32 {
601            return Err(422);
602        }
603        for row in rows {
604            let field = row
605                .get("field")
606                .and_then(Value::as_str)
607                .filter(|value| !value.is_empty() && value.len() <= 128)
608                .ok_or(422)?;
609            if row
610                .get("preserve_existing")
611                .and_then(Value::as_bool)
612                .unwrap_or(false)
613                && dynamic_action_input_value(request, &overrides, field)
614                    .is_some_and(|value| !value.is_empty())
615            {
616                continue;
617            }
618            overrides.insert(field.to_owned(), dynamic_derived_input_value(row, request));
619        }
620    }
621    dynamic_apply_default_input_hooks(&mut overrides, action, request)?;
622    let Some(hooks) = hook_contract
623        .get("normalize_inputs")
624        .and_then(Value::as_array)
625    else {
626        return Ok(overrides);
627    };
628    for hook in hooks {
629        let kind = hook.get("kind").and_then(Value::as_str).ok_or(422)?;
630        let fields = hook.get("fields").and_then(Value::as_array).ok_or(422)?;
631        for field in fields {
632            let field = field.as_str().ok_or(422)?;
633            if field.is_empty() || field.len() > 128 {
634                return Err(422);
635            }
636            let current = overrides
637                .get(field)
638                .cloned()
639                .unwrap_or_else(|| request_form_value(request, field));
640            let next = match kind {
641                "trim" => current.map(|value| value.trim().to_owned()),
642                "collapse_whitespace" => current.map(|value| {
643                    value
644                        .split_whitespace()
645                        .filter(|segment| !segment.is_empty())
646                        .collect::<Vec<_>>()
647                        .join(" ")
648                }),
649                "lowercase" => current.map(|value| value.to_ascii_lowercase()),
650                "uppercase" => current.map(|value| value.to_ascii_uppercase()),
651                "empty_to_null" => {
652                    current.and_then(|value| (!value.trim().is_empty()).then_some(value))
653                }
654                _ => return Err(422),
655            };
656            overrides.insert(field.to_owned(), next);
657        }
658    }
659    Ok(overrides)
660}
661
662/// Resolves one request input value after applying any declared action normalization hooks.
663fn dynamic_action_input_value(
664    request: &HttpRequest,
665    overrides: &BTreeMap<String, Option<String>>,
666    field: &str,
667) -> Option<String> {
668    if field.is_empty() {
669        return None;
670    }
671    overrides
672        .get(field)
673        .cloned()
674        .unwrap_or_else(|| request_form_value(request, field))
675}
676
677/// Builds one bounded action-report annotation map from declared success hooks.
678fn dynamic_action_report_annotations(
679    action: &Value,
680    request: &HttpRequest,
681    input_overrides: &BTreeMap<String, Option<String>>,
682    action_id: &str,
683    entity_id: &str,
684    event_kind: &str,
685    anchor_record_id: i64,
686) -> Result<Map<String, Value>, i32> {
687    dynamic_action_annotations(
688        action,
689        "report_annotations",
690        request,
691        input_overrides,
692        action_id,
693        entity_id,
694        event_kind,
695        anchor_record_id,
696    )
697}
698
699/// Builds one bounded action-report annotation map from a declared hook key.
700fn dynamic_action_annotations(
701    action: &Value,
702    hook_key: &str,
703    request: &HttpRequest,
704    input_overrides: &BTreeMap<String, Option<String>>,
705    action_id: &str,
706    entity_id: &str,
707    event_kind: &str,
708    anchor_record_id: i64,
709) -> Result<Map<String, Value>, i32> {
710    let mut annotations = Map::new();
711    let Some(rows) = action
712        .get("hook_contract")
713        .and_then(|value| value.get(hook_key))
714        .and_then(Value::as_array)
715    else {
716        return Ok(annotations);
717    };
718    if rows.len() > 16 {
719        return Err(422);
720    }
721    for row in rows {
722        let field = row
723            .get("field")
724            .and_then(Value::as_str)
725            .filter(|value| !value.is_empty() && value.len() <= 128)
726            .ok_or(422)?;
727        let value = dynamic_notification_parameter_value(
728            row.get("value").unwrap_or(&Value::Null),
729            request,
730            input_overrides,
731            action_id,
732            entity_id,
733            event_kind,
734            "",
735            "",
736            anchor_record_id,
737        )
738        .unwrap_or(Value::Null);
739        annotations.insert(field.to_owned(), value);
740    }
741    Ok(annotations)
742}
743
744/// Builds one bounded action-report annotation map from declared failure hooks.
745fn dynamic_action_failure_annotations(
746    action: &Value,
747    request: &HttpRequest,
748    input_overrides: &BTreeMap<String, Option<String>>,
749    action_id: &str,
750    entity_id: &str,
751    event_kind: &str,
752    anchor_record_id: i64,
753) -> Result<Map<String, Value>, i32> {
754    dynamic_action_annotations(
755        action,
756        "failure_annotations",
757        request,
758        input_overrides,
759        action_id,
760        entity_id,
761        event_kind,
762        anchor_record_id,
763    )
764}
765
766/// Performs dynamic replace records from input list primitive.
767fn dynamic_replace_records_from_input_list_primitive(
768    root: &mut Value,
769    request: &HttpRequest,
770    input_overrides: &BTreeMap<String, Option<String>>,
771    primitive: &Value,
772    matched_record_count: &mut i64,
773    created_record_count: &mut i64,
774    updated_record_count: &mut i64,
775    skipped_record_count: &mut i64,
776    removed_record_count: &mut i64,
777) -> Result<(), i32> {
778    let collection = primitive
779        .get("collection")
780        .and_then(Value::as_str)
781        .ok_or(422)?;
782    let scope_field = primitive
783        .get("scope_match")
784        .and_then(Value::as_object)
785        .and_then(|map| map.get("field"))
786        .and_then(Value::as_str)
787        .ok_or(422)?;
788    let scope_input = primitive
789        .get("scope_match")
790        .and_then(Value::as_object)
791        .and_then(|map| map.get("from_input"))
792        .and_then(Value::as_str)
793        .ok_or(422)?;
794    let wanted =
795        dynamic_action_input_value(request, input_overrides, scope_input).unwrap_or_default();
796    let delimiter = primitive
797        .get("list_source")
798        .and_then(Value::as_object)
799        .and_then(|map| map.get("delimiter"))
800        .and_then(Value::as_str)
801        .unwrap_or(",");
802    let source_field = primitive
803        .get("list_source")
804        .and_then(Value::as_object)
805        .and_then(|map| map.get("from_input"))
806        .and_then(Value::as_str)
807        .ok_or(422)?;
808    let raw_input =
809        dynamic_action_input_value(request, input_overrides, source_field).unwrap_or_default();
810    let parsed_items = raw_input
811        .split(delimiter)
812        .map(str::trim)
813        .filter(|value| !value.is_empty())
814        .collect::<Vec<_>>();
815    let max_records = primitive
816        .get("max_records")
817        .and_then(Value::as_i64)
818        .unwrap_or(parsed_items.len() as i64)
819        .max(0) as usize;
820    let overflow_policy = primitive
821        .get("overflow_policy")
822        .and_then(Value::as_str)
823        .unwrap_or("truncate");
824    if parsed_items.len() > max_records && overflow_policy == "fail" {
825        return Err(422);
826    }
827    let items = parsed_items
828        .into_iter()
829        .take(max_records)
830        .collect::<Vec<_>>();
831    *skipped_record_count += raw_input
832        .split(delimiter)
833        .map(str::trim)
834        .filter(|value| !value.is_empty())
835        .count()
836        .saturating_sub(items.len()) as i64;
837    let order_field = primitive
838        .get("order_field")
839        .and_then(Value::as_str)
840        .unwrap_or("id");
841    let updates = primitive
842        .get("updates")
843        .and_then(Value::as_object)
844        .ok_or(422)?;
845    let record_contract = primitive
846        .get("record_contract")
847        .and_then(Value::as_object)
848        .ok_or(422)?;
849    let existing_match_count = root
850        .get(collection)
851        .and_then(Value::as_array)
852        .map(|rows| {
853            rows.iter()
854                .filter(|row| row.get(scope_field).map(value_to_text).unwrap_or_default() == wanted)
855                .count()
856        })
857        .unwrap_or(0);
858    let create_count = items.len().saturating_sub(existing_match_count);
859    let mut reserved_ids = (0..create_count)
860        .map(|_| dynamic_take_next_global_id(root))
861        .collect::<Vec<_>>();
862    reserved_ids.reverse();
863    let Some(rows) = dynamic_state_rows_mut(root, collection) else {
864        return Err(500);
865    };
866    let mut matching_indexes = rows
867        .iter()
868        .enumerate()
869        .filter(|(_, row)| row.get(scope_field).map(value_to_text).unwrap_or_default() == wanted)
870        .map(|(index, _)| index)
871        .collect::<Vec<_>>();
872    matching_indexes.sort_by(|left, right| {
873        dynamic_value_order_cmp(rows[*left].get(order_field), rows[*right].get(order_field))
874    });
875    *matched_record_count += matching_indexes.len() as i64;
876    for (position, item) in items.iter().enumerate() {
877        if let Some(index) = matching_indexes.get(position).copied() {
878            let Some(object) = rows.get_mut(index).and_then(Value::as_object_mut) else {
879                return Err(500);
880            };
881            for (field_name, update_spec) in updates {
882                let source = update_spec
883                    .get("source")
884                    .and_then(Value::as_str)
885                    .unwrap_or_default();
886                let value = match source {
887                    "input" => Value::String(
888                        dynamic_action_input_value(
889                            request,
890                            input_overrides,
891                            update_spec
892                                .get("field")
893                                .and_then(Value::as_str)
894                                .unwrap_or_default(),
895                        )
896                        .unwrap_or_default(),
897                    ),
898                    "literal" => update_spec.get("value").cloned().unwrap_or(Value::Null),
899                    "list_item" => Value::String((*item).to_owned()),
900                    "list_index" => Value::from((position + 1) as i64),
901                    _ => Value::Null,
902                };
903                object.insert(field_name.to_owned(), value);
904            }
905            *updated_record_count += 1;
906        } else {
907            let mut record = Map::new();
908            let mut reserved_next_id = None;
909            for (field_name, field_spec) in record_contract {
910                let source = field_spec
911                    .get("source")
912                    .and_then(Value::as_str)
913                    .unwrap_or_default();
914                let value = match source {
915                    "next_id" => {
916                        let next_id = *reserved_next_id
917                            .get_or_insert_with(|| reserved_ids.pop().unwrap_or(0));
918                        Value::from(next_id)
919                    }
920                    "input" => Value::String(
921                        dynamic_action_input_value(
922                            request,
923                            input_overrides,
924                            field_spec
925                                .get("field")
926                                .and_then(Value::as_str)
927                                .unwrap_or_default(),
928                        )
929                        .unwrap_or_default(),
930                    ),
931                    "literal" => field_spec.get("value").cloned().unwrap_or(Value::Null),
932                    "context_anchor_created_id" => Value::from(0),
933                    "list_item" => Value::String((*item).to_owned()),
934                    "list_index" => Value::from((position + 1) as i64),
935                    _ => Value::Null,
936                };
937                record.insert(field_name.to_owned(), value);
938            }
939            rows.push(Value::Object(record));
940            *created_record_count += 1;
941        }
942    }
943    let mut remove_indexes = matching_indexes
944        .into_iter()
945        .skip(items.len())
946        .collect::<Vec<_>>();
947    remove_indexes.sort_unstable_by(|left, right| right.cmp(left));
948    for index in remove_indexes {
949        rows.remove(index);
950        *removed_record_count += 1;
951    }
952    Ok(())
953}
954
955/// Performs dynamic append record primitive.
956fn dynamic_append_record_primitive(
957    root: &mut Value,
958    request: &HttpRequest,
959    input_overrides: &BTreeMap<String, Option<String>>,
960    primitive: &Value,
961    anchor_record_id: &mut i64,
962    created_record_count: &mut i64,
963) -> Result<(), i32> {
964    let collection = primitive
965        .get("collection")
966        .and_then(Value::as_str)
967        .ok_or(422)?;
968    let mut record = Map::new();
969    let record_contract = primitive
970        .get("record_contract")
971        .and_then(Value::as_object)
972        .ok_or(422)?;
973    for (field_name, field_spec) in record_contract {
974        let source = field_spec
975            .get("source")
976            .and_then(Value::as_str)
977            .unwrap_or_default();
978        let value = match source {
979            "next_id" => Value::from(dynamic_take_next_global_id(root)),
980            "input" => Value::String(
981                dynamic_action_input_value(
982                    request,
983                    input_overrides,
984                    field_spec
985                        .get("field")
986                        .and_then(Value::as_str)
987                        .unwrap_or_default(),
988                )
989                .unwrap_or_default(),
990            ),
991            "literal" => field_spec.get("value").cloned().unwrap_or(Value::Null),
992            "context_anchor_created_id" => Value::from(*anchor_record_id),
993            _ => Value::Null,
994        };
995        record.insert(field_name.to_owned(), value);
996    }
997    let created_id = record.get("id").and_then(Value::as_i64).unwrap_or(0);
998    dynamic_state_rows_mut(root, collection)
999        .ok_or(500)?
1000        .push(Value::Object(record));
1001    *created_record_count += 1;
1002    if *anchor_record_id <= 0 {
1003        *anchor_record_id = created_id;
1004    }
1005    Ok(())
1006}
1007
1008/// Performs dynamic set root fields primitive.
1009fn dynamic_set_root_fields_primitive(
1010    app: &AppRuntime,
1011    root: &mut Value,
1012    request: &HttpRequest,
1013    input_overrides: &BTreeMap<String, Option<String>>,
1014    primitive: &Value,
1015) -> Result<(), i32> {
1016    let ref_text = primitive.get("fields").and_then(Value::as_str).ok_or(422)?;
1017    let fields_root = dynamic_resolve_ref_target(app, ref_text).ok_or(422)?;
1018    let fields = fields_root
1019        .as_object()
1020        .or_else(|| fields_root.get("fields").and_then(Value::as_object))
1021        .ok_or(422)?;
1022    let Some(root_object) = root.as_object_mut() else {
1023        return Err(500);
1024    };
1025    for (field_name, field_spec) in fields {
1026        let source = field_spec
1027            .get("source")
1028            .and_then(Value::as_str)
1029            .unwrap_or_default();
1030        let value = match source {
1031            "literal" => field_spec.get("value").cloned().unwrap_or(Value::Null),
1032            "input" => Value::String(
1033                dynamic_action_input_value(
1034                    request,
1035                    input_overrides,
1036                    field_spec
1037                        .get("field")
1038                        .and_then(Value::as_str)
1039                        .unwrap_or_default(),
1040                )
1041                .unwrap_or_default(),
1042            ),
1043            _ => Value::Null,
1044        };
1045        root_object.insert(field_name.to_owned(), value);
1046    }
1047    Ok(())
1048}
1049
1050/// Performs dynamic update record primitive.
1051fn dynamic_update_record_primitive(
1052    root: &mut Value,
1053    request: &HttpRequest,
1054    input_overrides: &BTreeMap<String, Option<String>>,
1055    primitive: &Value,
1056    matched_record_count: &mut i64,
1057    updated_record_count: &mut i64,
1058) -> Result<(), i32> {
1059    let collection = primitive
1060        .get("collection")
1061        .and_then(Value::as_str)
1062        .ok_or(422)?;
1063    let match_field = primitive
1064        .get("match")
1065        .and_then(Value::as_object)
1066        .and_then(|map| map.get("field"))
1067        .and_then(Value::as_str)
1068        .ok_or(422)?;
1069    let match_input = primitive
1070        .get("match")
1071        .and_then(Value::as_object)
1072        .and_then(|map| map.get("from_input"))
1073        .and_then(Value::as_str)
1074        .ok_or(422)?;
1075    let wanted =
1076        dynamic_action_input_value(request, input_overrides, match_input).unwrap_or_default();
1077    let updates = primitive
1078        .get("updates")
1079        .and_then(Value::as_object)
1080        .ok_or(422)?;
1081    let Some(row) = dynamic_state_rows_mut(root, collection).and_then(|rows| {
1082        rows.iter_mut()
1083            .find(|row| row.get(match_field).map(value_to_text).unwrap_or_default() == wanted)
1084    }) else {
1085        return Err(404);
1086    };
1087    let Some(object) = row.as_object_mut() else {
1088        return Err(500);
1089    };
1090    for (field_name, update_spec) in updates {
1091        let source = update_spec
1092            .get("source")
1093            .and_then(Value::as_str)
1094            .unwrap_or_default();
1095        let value = match source {
1096            "literal" => update_spec.get("value").cloned().unwrap_or(Value::Null),
1097            "input" => Value::String(
1098                dynamic_action_input_value(
1099                    request,
1100                    input_overrides,
1101                    update_spec
1102                        .get("field")
1103                        .and_then(Value::as_str)
1104                        .unwrap_or_default(),
1105                )
1106                .unwrap_or_default(),
1107            ),
1108            "toggle_boolean" => Value::Bool(
1109                !object
1110                    .get(
1111                        update_spec
1112                            .get("field")
1113                            .and_then(Value::as_str)
1114                            .unwrap_or_default(),
1115                    )
1116                    .and_then(Value::as_bool)
1117                    .unwrap_or(false),
1118            ),
1119            _ => Value::Null,
1120        };
1121        object.insert(field_name.to_owned(), value);
1122    }
1123    *updated_record_count += 1;
1124    *matched_record_count += 1;
1125    Ok(())
1126}
1127
1128/// Performs dynamic append records from input list primitive.
1129fn dynamic_append_records_from_input_list_primitive(
1130    root: &mut Value,
1131    request: &HttpRequest,
1132    input_overrides: &BTreeMap<String, Option<String>>,
1133    primitive: &Value,
1134    anchor_record_id: i64,
1135    matched_record_count: i64,
1136    updated_record_count: i64,
1137    skipped_record_count: &mut i64,
1138    created_record_count: &mut i64,
1139) -> Result<(), i32> {
1140    let collection = primitive
1141        .get("collection")
1142        .and_then(Value::as_str)
1143        .ok_or(422)?;
1144    let delimiter = primitive
1145        .get("list_source")
1146        .and_then(Value::as_object)
1147        .and_then(|map| map.get("delimiter"))
1148        .and_then(Value::as_str)
1149        .unwrap_or(",");
1150    let source_field = primitive
1151        .get("list_source")
1152        .and_then(Value::as_object)
1153        .and_then(|map| map.get("from_input"))
1154        .and_then(Value::as_str)
1155        .ok_or(422)?;
1156    let input_value =
1157        dynamic_action_input_value(request, input_overrides, source_field).unwrap_or_default();
1158    let items = input_value
1159        .split(delimiter)
1160        .map(str::trim)
1161        .filter(|value| !value.is_empty())
1162        .collect::<Vec<_>>();
1163    let max_records = primitive
1164        .get("max_records")
1165        .and_then(Value::as_i64)
1166        .unwrap_or(items.len() as i64)
1167        .max(0) as usize;
1168    let overflow_policy = primitive
1169        .get("overflow_policy")
1170        .and_then(Value::as_str)
1171        .unwrap_or("truncate");
1172    if items.len() > max_records && overflow_policy == "fail" {
1173        return Err(422);
1174    }
1175    *skipped_record_count += items.len().saturating_sub(max_records) as i64;
1176    let record_contract = primitive
1177        .get("record_contract")
1178        .and_then(Value::as_object)
1179        .ok_or(422)?;
1180    for (position, item) in items.into_iter().take(max_records).enumerate() {
1181        let mut record = Map::new();
1182        for (field_name, field_spec) in record_contract {
1183            let source = field_spec
1184                .get("source")
1185                .and_then(Value::as_str)
1186                .unwrap_or_default();
1187            let value = match source {
1188                "next_id"
1189                | "input"
1190                | "literal"
1191                | "context_anchor_created_id"
1192                | "list_item"
1193                | "list_index" => dynamic_record_contract_value(
1194                    root,
1195                    request,
1196                    input_overrides,
1197                    field_spec,
1198                    anchor_record_id,
1199                    Some(item),
1200                    Some(position),
1201                    matched_record_count,
1202                    updated_record_count,
1203                    *skipped_record_count,
1204                ),
1205                _ => Value::Null,
1206            };
1207            record.insert(field_name.to_owned(), value);
1208        }
1209        dynamic_state_rows_mut(root, collection)
1210            .ok_or(500)?
1211            .push(Value::Object(record));
1212        *created_record_count += 1;
1213    }
1214    Ok(())
1215}
1216
1217/// Performs dynamic append records from related collection primitive.
1218fn dynamic_append_records_from_related_collection_primitive(
1219    root: &mut Value,
1220    request: &HttpRequest,
1221    input_overrides: &BTreeMap<String, Option<String>>,
1222    primitive: &Value,
1223    matched_record_count: &mut i64,
1224    skipped_record_count: &mut i64,
1225    created_record_count: &mut i64,
1226) -> Result<(), i32> {
1227    let collection = primitive
1228        .get("collection")
1229        .and_then(Value::as_str)
1230        .ok_or(422)?;
1231    let source_collection = primitive
1232        .get("source_collection")
1233        .and_then(Value::as_str)
1234        .ok_or(422)?;
1235    let scope_field = primitive
1236        .get("scope_match")
1237        .and_then(Value::as_object)
1238        .and_then(|map| map.get("field"))
1239        .and_then(Value::as_str)
1240        .ok_or(422)?;
1241    let scope_input = primitive
1242        .get("scope_match")
1243        .and_then(Value::as_object)
1244        .and_then(|map| map.get("from_input"))
1245        .and_then(Value::as_str)
1246        .ok_or(422)?;
1247    let wanted =
1248        dynamic_action_input_value(request, input_overrides, scope_input).unwrap_or_default();
1249    let source_rows = root
1250        .get(source_collection)
1251        .and_then(Value::as_array)
1252        .cloned()
1253        .unwrap_or_default();
1254    let matched = source_rows
1255        .into_iter()
1256        .filter(|row| row.get(scope_field).map(value_to_text).unwrap_or_default() == wanted)
1257        .collect::<Vec<_>>();
1258    let max_records = primitive
1259        .get("max_records")
1260        .and_then(Value::as_i64)
1261        .unwrap_or(matched.len() as i64)
1262        .max(0) as usize;
1263    *matched_record_count += matched.len() as i64;
1264    *skipped_record_count += matched.len().saturating_sub(max_records) as i64;
1265    let record_contract = primitive
1266        .get("record_contract")
1267        .and_then(Value::as_object)
1268        .ok_or(422)?;
1269    for source_row in matched.into_iter().take(max_records) {
1270        let mut record = Map::new();
1271        for (field_name, field_spec) in record_contract {
1272            let source = field_spec
1273                .get("source")
1274                .and_then(Value::as_str)
1275                .unwrap_or_default();
1276            let value = match source {
1277                "next_id" => Value::from(dynamic_take_next_global_id(root)),
1278                "source_record_field" => source_row
1279                    .get(
1280                        field_spec
1281                            .get("field")
1282                            .and_then(Value::as_str)
1283                            .unwrap_or_default(),
1284                    )
1285                    .cloned()
1286                    .unwrap_or(Value::Null),
1287                "input" => Value::String(
1288                    dynamic_action_input_value(
1289                        request,
1290                        input_overrides,
1291                        field_spec
1292                            .get("field")
1293                            .and_then(Value::as_str)
1294                            .unwrap_or_default(),
1295                    )
1296                    .unwrap_or_default(),
1297                ),
1298                "literal" => field_spec.get("value").cloned().unwrap_or(Value::Null),
1299                _ => Value::Null,
1300            };
1301            record.insert(field_name.to_owned(), value);
1302        }
1303        dynamic_state_rows_mut(root, collection)
1304            .ok_or(500)?
1305            .push(Value::Object(record));
1306        *created_record_count += 1;
1307    }
1308    Ok(())
1309}
1310
1311/// Performs dynamic apply scoped assignment or bulk update primitive.
1312fn dynamic_apply_scoped_assignment_or_bulk_update_primitive(
1313    root: &mut Value,
1314    request: &HttpRequest,
1315    input_overrides: &BTreeMap<String, Option<String>>,
1316    primitive: &Value,
1317    primitive_kind: &str,
1318    matched_record_count: &mut i64,
1319    updated_record_count: &mut i64,
1320    skipped_record_count: &mut i64,
1321) -> Result<(), i32> {
1322    let collection = primitive
1323        .get("collection")
1324        .and_then(Value::as_str)
1325        .ok_or(422)?;
1326    let scope_field = primitive
1327        .get("scope_match")
1328        .and_then(Value::as_object)
1329        .and_then(|map| map.get("field"))
1330        .and_then(Value::as_str)
1331        .ok_or(422)?;
1332    let scope_input = primitive
1333        .get("scope_match")
1334        .and_then(Value::as_object)
1335        .and_then(|map| map.get("from_input"))
1336        .and_then(Value::as_str)
1337        .ok_or(422)?;
1338    let wanted =
1339        dynamic_action_input_value(request, input_overrides, scope_input).unwrap_or_default();
1340    let max_records = primitive
1341        .get("max_records")
1342        .and_then(Value::as_i64)
1343        .unwrap_or(i64::MAX)
1344        .max(0) as usize;
1345    let overflow_policy = primitive
1346        .get("overflow_policy")
1347        .and_then(Value::as_str)
1348        .unwrap_or("truncate");
1349    let Some(rows) = dynamic_state_rows_mut(root, collection) else {
1350        return Err(500);
1351    };
1352    let matching_indexes = rows
1353        .iter()
1354        .enumerate()
1355        .filter(|(_, row)| row.get(scope_field).map(value_to_text).unwrap_or_default() == wanted)
1356        .map(|(index, _)| index)
1357        .collect::<Vec<_>>();
1358    *matched_record_count += matching_indexes.len() as i64;
1359    if matching_indexes.len() > max_records && overflow_policy == "fail" {
1360        return Err(422);
1361    }
1362    *skipped_record_count += matching_indexes.len().saturating_sub(max_records) as i64;
1363    for index in matching_indexes.into_iter().take(max_records) {
1364        let Some(object) = rows.get_mut(index).and_then(Value::as_object_mut) else {
1365            return Err(500);
1366        };
1367        if primitive_kind == "scoped_reassignment" {
1368            let target_field = primitive
1369                .get("target_field")
1370                .and_then(Value::as_str)
1371                .ok_or(422)?;
1372            let value = dynamic_action_input_value(
1373                request,
1374                input_overrides,
1375                primitive
1376                    .get("value_contract")
1377                    .and_then(Value::as_object)
1378                    .and_then(|map| map.get("field"))
1379                    .and_then(Value::as_str)
1380                    .unwrap_or_default(),
1381            )
1382            .unwrap_or_default();
1383            object.insert(target_field.to_owned(), Value::String(value));
1384        } else {
1385            let updates = primitive
1386                .get("updates")
1387                .and_then(Value::as_object)
1388                .ok_or(422)?;
1389            for (field_name, update_spec) in updates {
1390                let source = update_spec
1391                    .get("source")
1392                    .and_then(Value::as_str)
1393                    .unwrap_or_default();
1394                let value = match source {
1395                    "literal" => update_spec.get("value").cloned().unwrap_or(Value::Null),
1396                    _ => Value::Null,
1397                };
1398                object.insert(field_name.to_owned(), value);
1399            }
1400        }
1401        *updated_record_count += 1;
1402    }
1403    Ok(())
1404}
1405
1406/// Performs dynamic apply scoped status transition primitive.
1407fn dynamic_apply_scoped_status_transition_primitive(
1408    root: &mut Value,
1409    request: &HttpRequest,
1410    input_overrides: &BTreeMap<String, Option<String>>,
1411    primitive: &Value,
1412    matched_record_count: &mut i64,
1413    updated_record_count: &mut i64,
1414    skipped_record_count: &mut i64,
1415) -> Result<(), i32> {
1416    let collection = primitive
1417        .get("collection")
1418        .and_then(Value::as_str)
1419        .ok_or(422)?;
1420    let scope_field = primitive
1421        .get("scope_match")
1422        .and_then(Value::as_object)
1423        .and_then(|map| map.get("field"))
1424        .and_then(Value::as_str)
1425        .ok_or(422)?;
1426    let scope_input = primitive
1427        .get("scope_match")
1428        .and_then(Value::as_object)
1429        .and_then(|map| map.get("from_input"))
1430        .and_then(Value::as_str)
1431        .ok_or(422)?;
1432    let wanted =
1433        dynamic_action_input_value(request, input_overrides, scope_input).unwrap_or_default();
1434    let status_field = primitive
1435        .get("status_field")
1436        .and_then(Value::as_str)
1437        .filter(|value| !value.is_empty())
1438        .unwrap_or("status");
1439    let workflow_state_field = primitive
1440        .get("workflow_state_field")
1441        .and_then(Value::as_str)
1442        .filter(|value| !value.is_empty())
1443        .unwrap_or("workflow_state");
1444    let to_status = primitive
1445        .get("to_status")
1446        .and_then(Value::as_str)
1447        .filter(|value| !value.is_empty())
1448        .ok_or(422)?;
1449    let to_workflow_state = primitive
1450        .get("to_workflow_state")
1451        .and_then(Value::as_str)
1452        .filter(|value| !value.is_empty())
1453        .unwrap_or(to_status);
1454    let allowed_from_statuses = primitive
1455        .get("from_statuses")
1456        .and_then(Value::as_array)
1457        .map(|rows| {
1458            rows.iter()
1459                .filter_map(Value::as_str)
1460                .filter(|value| !value.is_empty())
1461                .collect::<Vec<_>>()
1462        })
1463        .unwrap_or_default();
1464    let max_records = primitive
1465        .get("max_records")
1466        .and_then(Value::as_i64)
1467        .unwrap_or(i64::MAX)
1468        .max(0) as usize;
1469    let overflow_policy = primitive
1470        .get("overflow_policy")
1471        .and_then(Value::as_str)
1472        .unwrap_or("truncate");
1473    let now = now_iso_utc();
1474    let actor_value = primitive
1475        .get("actor_input")
1476        .and_then(Value::as_str)
1477        .and_then(|field| dynamic_action_input_value(request, input_overrides, field));
1478    let Some(rows) = dynamic_state_rows_mut(root, collection) else {
1479        return Err(500);
1480    };
1481    let matching_indexes = rows
1482        .iter()
1483        .enumerate()
1484        .filter(|(_, row)| row.get(scope_field).map(value_to_text).unwrap_or_default() == wanted)
1485        .map(|(index, _)| index)
1486        .collect::<Vec<_>>();
1487    *matched_record_count += matching_indexes.len() as i64;
1488    if matching_indexes.len() > max_records && overflow_policy == "fail" {
1489        return Err(422);
1490    }
1491    *skipped_record_count += matching_indexes.len().saturating_sub(max_records) as i64;
1492    for index in matching_indexes.into_iter().take(max_records) {
1493        let Some(object) = rows.get_mut(index).and_then(Value::as_object_mut) else {
1494            return Err(500);
1495        };
1496        let current_status = object
1497            .get(status_field)
1498            .and_then(Value::as_str)
1499            .or_else(|| object.get(workflow_state_field).and_then(Value::as_str))
1500            .unwrap_or_default();
1501        if !allowed_from_statuses.is_empty() && !allowed_from_statuses.contains(&current_status) {
1502            *skipped_record_count += 1;
1503            continue;
1504        }
1505        object.insert(status_field.to_owned(), Value::String(to_status.to_owned()));
1506        object.insert(
1507            workflow_state_field.to_owned(),
1508            Value::String(to_workflow_state.to_owned()),
1509        );
1510        if let Some(field) = primitive
1511            .get("timestamp_field")
1512            .and_then(Value::as_str)
1513            .filter(|value| !value.is_empty())
1514        {
1515            object.insert(field.to_owned(), Value::String(now.clone()));
1516        }
1517        if let Some(field) = primitive
1518            .get("actor_field")
1519            .and_then(Value::as_str)
1520            .filter(|value| !value.is_empty())
1521        {
1522            object.insert(
1523                field.to_owned(),
1524                Value::String(actor_value.clone().unwrap_or_default()),
1525            );
1526        }
1527        if let Some(updates) = primitive.get("updates").and_then(Value::as_object) {
1528            for (field_name, update_spec) in updates {
1529                let source = update_spec
1530                    .get("source")
1531                    .and_then(Value::as_str)
1532                    .unwrap_or_default();
1533                let value = match source {
1534                    "literal" => update_spec.get("value").cloned().unwrap_or(Value::Null),
1535                    "input" => Value::String(
1536                        dynamic_action_input_value(
1537                            request,
1538                            input_overrides,
1539                            update_spec
1540                                .get("field")
1541                                .and_then(Value::as_str)
1542                                .unwrap_or_default(),
1543                        )
1544                        .unwrap_or_default(),
1545                    ),
1546                    _ => Value::Null,
1547                };
1548                object.insert(field_name.to_owned(), value);
1549            }
1550        }
1551        *updated_record_count += 1;
1552    }
1553    Ok(())
1554}
1555
1556/// Performs dynamic upsert record primitive.
1557fn dynamic_upsert_record_primitive(
1558    root: &mut Value,
1559    request: &HttpRequest,
1560    input_overrides: &BTreeMap<String, Option<String>>,
1561    primitive: &Value,
1562    anchor_record_id: i64,
1563    matched_record_count: &mut i64,
1564    updated_record_count: &mut i64,
1565    skipped_record_count: i64,
1566    created_record_count: &mut i64,
1567) -> Result<(), i32> {
1568    let collection = primitive
1569        .get("collection")
1570        .and_then(Value::as_str)
1571        .ok_or(422)?;
1572    let match_field = primitive
1573        .get("match")
1574        .and_then(Value::as_object)
1575        .and_then(|map| map.get("field"))
1576        .and_then(Value::as_str)
1577        .ok_or(422)?;
1578    let wanted = if let Some(input_name) = primitive
1579        .get("match")
1580        .and_then(Value::as_object)
1581        .and_then(|map| map.get("from_input"))
1582        .and_then(Value::as_str)
1583    {
1584        dynamic_action_input_value(request, input_overrides, input_name).unwrap_or_default()
1585    } else if let Some(value) = primitive
1586        .get("match")
1587        .and_then(Value::as_object)
1588        .and_then(|map| map.get("value"))
1589    {
1590        value_to_text(value)
1591    } else {
1592        return Err(422);
1593    };
1594    let updates = primitive
1595        .get("updates")
1596        .and_then(Value::as_object)
1597        .ok_or(422)?;
1598    let record_contract = primitive
1599        .get("record_contract")
1600        .and_then(Value::as_object)
1601        .ok_or(422)?;
1602    let existing_index = root
1603        .get(collection)
1604        .and_then(Value::as_array)
1605        .and_then(|rows| {
1606            rows.iter().position(|row| {
1607                row.get(match_field).map(value_to_text).unwrap_or_default() == wanted
1608            })
1609        });
1610    if let Some(index) = existing_index {
1611        *matched_record_count += 1;
1612        let resolved_updates = updates
1613            .iter()
1614            .map(|(field_name, update_spec)| {
1615                (
1616                    field_name.to_owned(),
1617                    dynamic_record_contract_value(
1618                        root,
1619                        request,
1620                        input_overrides,
1621                        update_spec,
1622                        anchor_record_id,
1623                        None,
1624                        None,
1625                        *matched_record_count,
1626                        *updated_record_count,
1627                        skipped_record_count,
1628                    ),
1629                )
1630            })
1631            .collect::<Vec<_>>();
1632        let Some(object) = dynamic_state_rows_mut(root, collection)
1633            .and_then(|rows| rows.get_mut(index))
1634            .and_then(Value::as_object_mut)
1635        else {
1636            return Err(500);
1637        };
1638        for (field_name, value) in resolved_updates {
1639            object.insert(field_name.to_owned(), value);
1640        }
1641        *updated_record_count += 1;
1642    } else {
1643        let mut record = Map::new();
1644        for (field_name, field_spec) in record_contract {
1645            let value = dynamic_record_contract_value(
1646                root,
1647                request,
1648                input_overrides,
1649                field_spec,
1650                anchor_record_id,
1651                None,
1652                None,
1653                *matched_record_count,
1654                *updated_record_count,
1655                skipped_record_count,
1656            );
1657            record.insert(field_name.to_owned(), value);
1658        }
1659        dynamic_state_rows_mut(root, collection)
1660            .ok_or(500)?
1661            .push(Value::Object(record));
1662        *created_record_count += 1;
1663    }
1664    Ok(())
1665}
1666
1667/// Resolves the best available resource identity for one declarative action report or effect.
1668fn dynamic_action_resource_id(
1669    action: &Value,
1670    request: &HttpRequest,
1671    input_overrides: &BTreeMap<String, Option<String>>,
1672    anchor_record_id: i64,
1673) -> String {
1674    let entity_id = action
1675        .get("entity_id")
1676        .and_then(Value::as_str)
1677        .unwrap_or("record");
1678    if anchor_record_id > 0 {
1679        return format!("{entity_id}:{anchor_record_id}");
1680    }
1681    if let Some(input_name) = action
1682        .get("mutation_primitives")
1683        .and_then(Value::as_array)
1684        .and_then(|rows| {
1685            rows.iter().find_map(|primitive| {
1686                primitive
1687                    .get("match")
1688                    .and_then(Value::as_object)
1689                    .and_then(|map| map.get("from_input"))
1690                    .and_then(Value::as_str)
1691                    .or_else(|| {
1692                        primitive
1693                            .get("scope_match")
1694                            .and_then(Value::as_object)
1695                            .and_then(|map| map.get("from_input"))
1696                            .and_then(Value::as_str)
1697                    })
1698            })
1699        })
1700        .filter(|value| !value.is_empty())
1701        && let Some(value) = dynamic_action_input_value(request, input_overrides, input_name)
1702            .filter(|value| !value.is_empty())
1703    {
1704        return format!("{entity_id}:{value}");
1705    }
1706    if let Some(value) =
1707        dynamic_action_input_value(request, input_overrides, "id").filter(|value| !value.is_empty())
1708    {
1709        return format!("{entity_id}:{value}");
1710    }
1711    format!("{entity_id}:0")
1712}
1713
1714/// Prepares an app-declared file effect without touching the filesystem.
1715fn prepare_file_record(
1716    integrations_root: &Value,
1717    action: &Value,
1718    file_record: &Value,
1719    request: &HttpRequest,
1720    input_overrides: &BTreeMap<String, Option<String>>,
1721    action_id: &str,
1722    entity_id: &str,
1723    event_kind: &str,
1724    anchor_record_id: i64,
1725) -> Option<(String, String, Value)> {
1726    let adapter_id = file_record
1727        .get("adapter_id")
1728        .and_then(Value::as_str)
1729        .filter(|value| !value.is_empty())
1730        .unwrap_or_default();
1731    let adapter = dynamic_find_integration_adapter(integrations_root, adapter_id)?;
1732    let adapter_kind = adapter
1733        .get("adapter_kind")
1734        .and_then(Value::as_str)
1735        .unwrap_or_default();
1736    if !dynamic_integration_file_adapter_kind(adapter_kind) {
1737        return None;
1738    }
1739    let relative_path = adapter
1740        .get("relative_path")
1741        .and_then(Value::as_str)
1742        .filter(|value| !value.is_empty())
1743        .unwrap_or_default();
1744    if relative_path.is_empty()
1745        || Path::new(relative_path)
1746            .components()
1747            .any(|part| !matches!(part, std::path::Component::Normal(_)))
1748    {
1749        return None;
1750    }
1751    let mut record = serde_json::Map::from_iter([
1752        (
1753            "adapter_id".to_owned(),
1754            Value::String(adapter_id.to_owned()),
1755        ),
1756        (
1757            "adapter_kind".to_owned(),
1758            Value::String(adapter_kind.to_owned()),
1759        ),
1760        ("action_id".to_owned(), Value::String(action_id.to_owned())),
1761        ("entity_id".to_owned(), Value::String(entity_id.to_owned())),
1762        (
1763            "event_kind".to_owned(),
1764            Value::String(event_kind.to_owned()),
1765        ),
1766        (
1767            "resource_id".to_owned(),
1768            Value::String(dynamic_action_resource_id(
1769                action,
1770                request,
1771                input_overrides,
1772                anchor_record_id,
1773            )),
1774        ),
1775        ("created_at".to_owned(), Value::String(now_iso_utc())),
1776    ]);
1777    if let Some(fields) = file_record.get("record").and_then(Value::as_object) {
1778        if fields.len() > 32 {
1779            return None;
1780        }
1781        for (field_name, field_spec) in fields {
1782            if field_name.is_empty() || field_name.len() > 128 {
1783                return None;
1784            }
1785            let value = dynamic_notification_parameter_value(
1786                field_spec,
1787                request,
1788                input_overrides,
1789                action_id,
1790                entity_id,
1791                event_kind,
1792                "",
1793                "",
1794                anchor_record_id,
1795            )
1796            .unwrap_or(Value::Null);
1797            record.insert(field_name.to_owned(), value);
1798        }
1799    }
1800    Some((
1801        adapter_kind.to_owned(),
1802        relative_path.to_owned(),
1803        Value::Object(record),
1804    ))
1805}
1806
1807/// Queues the local effect in the same document commit as its domain mutation.
1808fn enqueue_file_record(root: &mut Value, prepared: (String, String, Value)) -> bool {
1809    let (kind, path, record) = prepared;
1810    let persisted = match root.get("_local_file_delivery_sequence") {
1811        None => 0,
1812        Some(value) => match value.as_i64().filter(|value| *value >= 0) {
1813            Some(value) => value,
1814            None => return false,
1815        },
1816    };
1817    let retained_maximum = root
1818        .get("local_file_deliveries")
1819        .and_then(Value::as_array)
1820        .into_iter()
1821        .flatten()
1822        .filter_map(|row| row.get("id").and_then(Value::as_i64))
1823        .max()
1824        .unwrap_or(0);
1825    let Some(id) = persisted.max(retained_maximum).checked_add(1) else {
1826        return false;
1827    };
1828
1829    let Ok(encoded) = serde_json::to_vec(&record) else {
1830        return false;
1831    };
1832    let key = pharos_runtime_database::operations::bytes_fingerprint(&encoded);
1833    let Some(rows) = root.as_object_mut().and_then(|object| {
1834        object
1835            .entry("local_file_deliveries")
1836            .or_insert_with(|| Value::Array(Vec::new()))
1837            .as_array_mut()
1838    }) else {
1839        return false;
1840    };
1841    let mut terminal_count = rows
1842        .iter()
1843        .filter(|row| {
1844            matches!(
1845                row.get("delivery_status").and_then(Value::as_str),
1846                Some("delivered" | "failed")
1847            )
1848        })
1849        .count();
1850    rows.retain(|row| {
1851        if terminal_count > 64
1852            && matches!(
1853                row.get("delivery_status").and_then(Value::as_str),
1854                Some("delivered" | "failed")
1855            )
1856        {
1857            terminal_count -= 1;
1858            false
1859        } else {
1860            true
1861        }
1862    });
1863    if rows.len() >= 1024 {
1864        return false;
1865    }
1866
1867    rows.push(serde_json::json!({"id": id, "adapter_kind": kind, "relative_path": path, "record": record, "delivery_key": format!("{id}:{key}"), "delivery_status": "queued", "attempt_count": 0, "next_attempt_at": ""}));
1868    let Some(object) = root.as_object_mut() else {
1869        return false;
1870    };
1871    object.insert("_local_file_delivery_sequence".to_owned(), Value::from(id));
1872
1873    true
1874}
1875
1876/// Delivers one committed local outbox record. Replayed JSONL writes use the
1877/// persisted delivery key, and JSON replacement remains naturally idempotent.
1878pub(super) fn process_one_due_file_delivery(app: &AppRuntime, root: &mut Value) -> bool {
1879    let now = current_epoch_seconds();
1880    let Some(rows) = root
1881        .get_mut("local_file_deliveries")
1882        .and_then(Value::as_array_mut)
1883    else {
1884        return false;
1885    };
1886    let mut pending_paths = std::collections::BTreeSet::new();
1887    let Some(row) = rows.iter_mut().find(|row| {
1888        let status = row.get("delivery_status").and_then(Value::as_str);
1889        if !matches!(status, Some("queued" | "retryable")) {
1890            return false;
1891        }
1892        let Some(path) = row.get("relative_path").and_then(Value::as_str) else {
1893            return false;
1894        };
1895        if !pending_paths.insert(path.to_owned()) {
1896            return false;
1897        }
1898        status == Some("queued")
1899            || (row.get("delivery_status").and_then(Value::as_str) == Some("retryable")
1900                && row
1901                    .get("next_attempt_at")
1902                    .and_then(Value::as_str)
1903                    .and_then(|text| text.parse::<u64>().ok())
1904                    .is_some_and(|due| due <= now))
1905    }) else {
1906        return false;
1907    };
1908    let state_root = app
1909        .state_path
1910        .as_deref()
1911        .and_then(|path| Path::new(path).parent());
1912    let delivered = state_root.is_some_and(|state_root| deliver_file_record(state_root, row));
1913    let attempts = row
1914        .get("attempt_count")
1915        .and_then(Value::as_u64)
1916        .unwrap_or(0)
1917        .saturating_add(1);
1918    row["attempt_count"] = Value::from(attempts);
1919    row["delivery_status"] = Value::String(
1920        if delivered {
1921            "delivered"
1922        } else if attempts >= 5 {
1923            "failed"
1924        } else {
1925            "retryable"
1926        }
1927        .to_owned(),
1928    );
1929    row["next_attempt_at"] = Value::String(if delivered || attempts >= 5 {
1930        String::new()
1931    } else {
1932        now.saturating_add(30 * attempts).to_string()
1933    });
1934    true
1935}
1936
1937/// Executes only a validated, committed file-record envelope below its data root.
1938fn deliver_file_record(state_root: &Path, envelope: &Value) -> bool {
1939    let Some(relative) = envelope.get("relative_path").and_then(Value::as_str) else {
1940        return false;
1941    };
1942    if relative.is_empty()
1943        || Path::new(relative)
1944            .components()
1945            .any(|part| !matches!(part, std::path::Component::Normal(_)))
1946    {
1947        return false;
1948    }
1949    let Some(record) = envelope.get("record") else {
1950        return false;
1951    };
1952    let output_path = state_root.join(relative);
1953    match envelope.get("adapter_kind").and_then(Value::as_str) {
1954        Some("append_jsonl_local") => envelope
1955            .get("delivery_key")
1956            .and_then(Value::as_str)
1957            .is_some_and(|key| {
1958                pharos_runtime_core::fs::append_json_record_once(
1959                    state_root,
1960                    &output_path,
1961                    key,
1962                    record,
1963                    0o640,
1964                )
1965                .is_ok()
1966            }),
1967        Some("write_json_local") => serde_json::to_vec_pretty(record).ok().is_some_and(|bytes| {
1968            pharos_runtime_core::fs::write_file_within(state_root, &output_path, &bytes, 0o640)
1969                .is_ok()
1970        }),
1971        _ => false,
1972    }
1973}
1974
1975/// Exercises preparation and delivery independently in focused adapter tests.
1976#[cfg(test)]
1977fn dynamic_append_file_record(
1978    state_path: &Path,
1979    integrations_root: &Value,
1980    action: &Value,
1981    file_record: &Value,
1982    request: &HttpRequest,
1983    input_overrides: &BTreeMap<String, Option<String>>,
1984    action_id: &str,
1985    entity_id: &str,
1986    event_kind: &str,
1987    anchor_record_id: i64,
1988) -> bool {
1989    let Some(prepared) = prepare_file_record(
1990        integrations_root,
1991        action,
1992        file_record,
1993        request,
1994        input_overrides,
1995        action_id,
1996        entity_id,
1997        event_kind,
1998        anchor_record_id,
1999    ) else {
2000        return false;
2001    };
2002    let mut root = serde_json::json!({});
2003    if !enqueue_file_record(&mut root, prepared) {
2004        return false;
2005    }
2006    state_path
2007        .parent()
2008        .is_some_and(|parent| deliver_file_record(parent, &root["local_file_deliveries"][0]))
2009}
2010
2011/// Performs dynamic apply approval transition.
2012fn dynamic_apply_approval_transition(
2013    object: &mut Map<String, Value>,
2014    primitive: &Value,
2015    request: &HttpRequest,
2016    input_overrides: &BTreeMap<String, Option<String>>,
2017) -> Result<(), i32> {
2018    let current_status = object
2019        .get("status")
2020        .and_then(Value::as_str)
2021        .or_else(|| object.get("workflow_state").and_then(Value::as_str))
2022        .unwrap_or_default();
2023    let transition_kind = primitive
2024        .get("transition_kind")
2025        .and_then(Value::as_str)
2026        .unwrap_or_default();
2027    let now = now_iso_utc();
2028    match transition_kind {
2029        "submit" => {
2030            if current_status != "draft" {
2031                return Err(422);
2032            }
2033            object.insert("status".to_owned(), Value::String("submitted".to_owned()));
2034            object.insert(
2035                "workflow_state".to_owned(),
2036                Value::String("submitted".to_owned()),
2037            );
2038            if let Some(field) = primitive
2039                .get("submitted_at_field")
2040                .and_then(Value::as_str)
2041                .filter(|value| !value.is_empty())
2042            {
2043                object.insert(field.to_owned(), Value::String(now));
2044            }
2045        }
2046        "approve" => {
2047            if current_status != "submitted" {
2048                return Err(422);
2049            }
2050            let approver = dynamic_action_input_value(
2051                request,
2052                input_overrides,
2053                primitive
2054                    .get("approver_input")
2055                    .and_then(Value::as_str)
2056                    .unwrap_or("approver"),
2057            )
2058            .unwrap_or_default();
2059            if approver.trim().is_empty() {
2060                return Err(422);
2061            }
2062            let approved_by_field = primitive
2063                .get("approved_by_field")
2064                .and_then(Value::as_str)
2065                .filter(|value| !value.is_empty())
2066                .unwrap_or("approved_by");
2067            object.insert(approved_by_field.to_owned(), Value::String(approver));
2068            if let Some(field) = primitive
2069                .get("approved_at_field")
2070                .and_then(Value::as_str)
2071                .filter(|value| !value.is_empty())
2072            {
2073                object.insert(field.to_owned(), Value::String(now.clone()));
2074            }
2075            object.insert("status".to_owned(), Value::String("approved".to_owned()));
2076            object.insert(
2077                "workflow_state".to_owned(),
2078                Value::String("approved".to_owned()),
2079            );
2080            let rejected_by_field = primitive
2081                .get("rejected_by_field")
2082                .and_then(Value::as_str)
2083                .filter(|value| !value.is_empty())
2084                .unwrap_or("rejected_by");
2085            let rejection_reason_field = primitive
2086                .get("rejection_reason_field")
2087                .and_then(Value::as_str)
2088                .filter(|value| !value.is_empty())
2089                .unwrap_or("rejection_reason");
2090            object.insert(rejected_by_field.to_owned(), Value::String(String::new()));
2091            object.insert(
2092                rejection_reason_field.to_owned(),
2093                Value::String(String::new()),
2094            );
2095            if let Some(field) = primitive
2096                .get("rejected_at_field")
2097                .and_then(Value::as_str)
2098                .filter(|value| !value.is_empty())
2099            {
2100                object.insert(field.to_owned(), Value::String(String::new()));
2101            }
2102        }
2103        "reject" => {
2104            if current_status != "submitted" {
2105                return Err(422);
2106            }
2107            let approver = dynamic_action_input_value(
2108                request,
2109                input_overrides,
2110                primitive
2111                    .get("approver_input")
2112                    .and_then(Value::as_str)
2113                    .unwrap_or("approver"),
2114            )
2115            .unwrap_or_default();
2116            let reason = dynamic_action_input_value(
2117                request,
2118                input_overrides,
2119                primitive
2120                    .get("reason_input")
2121                    .and_then(Value::as_str)
2122                    .unwrap_or("reason"),
2123            )
2124            .unwrap_or_default();
2125            if approver.trim().is_empty() || reason.trim().is_empty() {
2126                return Err(422);
2127            }
2128            let rejected_by_field = primitive
2129                .get("rejected_by_field")
2130                .and_then(Value::as_str)
2131                .filter(|value| !value.is_empty())
2132                .unwrap_or("approved_by");
2133            let rejection_reason_field = primitive
2134                .get("rejection_reason_field")
2135                .and_then(Value::as_str)
2136                .filter(|value| !value.is_empty())
2137                .unwrap_or("rejection_reason");
2138            let approved_by_field = primitive
2139                .get("approved_by_field")
2140                .and_then(Value::as_str)
2141                .filter(|value| !value.is_empty())
2142                .unwrap_or("approved_by");
2143            object.insert(rejected_by_field.to_owned(), Value::String(approver));
2144            object.insert(rejection_reason_field.to_owned(), Value::String(reason));
2145            object.insert(approved_by_field.to_owned(), Value::String(String::new()));
2146            if let Some(field) = primitive
2147                .get("approved_at_field")
2148                .and_then(Value::as_str)
2149                .filter(|value| !value.is_empty())
2150            {
2151                object.insert(field.to_owned(), Value::String(String::new()));
2152            }
2153            if let Some(field) = primitive
2154                .get("rejected_at_field")
2155                .and_then(Value::as_str)
2156                .filter(|value| !value.is_empty())
2157            {
2158                object.insert(field.to_owned(), Value::String(now));
2159            }
2160            object.insert("status".to_owned(), Value::String("rejected".to_owned()));
2161            object.insert(
2162                "workflow_state".to_owned(),
2163                Value::String("rejected".to_owned()),
2164            );
2165        }
2166        _ => return Err(422),
2167    }
2168    Ok(())
2169}
2170
2171/// Performs dynamic append audit record.
2172fn dynamic_append_audit_record(
2173    root: &mut Value,
2174    integrations_root: &Value,
2175    action: &Value,
2176    audit: &Value,
2177    request: &HttpRequest,
2178    input_overrides: &BTreeMap<String, Option<String>>,
2179    action_id: &str,
2180    entity_id: &str,
2181    event_kind: &str,
2182    anchor_record_id: i64,
2183) -> bool {
2184    let adapter_id = audit
2185        .get("adapter_id")
2186        .and_then(Value::as_str)
2187        .filter(|value| !value.is_empty())
2188        .unwrap_or_default();
2189    let Some(adapter) = dynamic_find_integration_adapter(integrations_root, adapter_id) else {
2190        return false;
2191    };
2192    let adapter_kind = adapter
2193        .get("adapter_kind")
2194        .and_then(Value::as_str)
2195        .unwrap_or_default();
2196    if !dynamic_integration_audit_adapter_kind(adapter_kind) {
2197        return false;
2198    }
2199    let event = dynamic_notification_parameter_value(
2200        audit.get("event").unwrap_or(&Value::Null),
2201        request,
2202        input_overrides,
2203        action_id,
2204        entity_id,
2205        event_kind,
2206        "",
2207        "",
2208        anchor_record_id,
2209    )
2210    .map(|value| value_to_text(&value))
2211    .filter(|value| !value.is_empty())
2212    .unwrap_or_else(|| event_kind.to_owned());
2213    let level = dynamic_notification_parameter_value(
2214        audit.get("level").unwrap_or(&Value::Null),
2215        request,
2216        input_overrides,
2217        action_id,
2218        entity_id,
2219        event_kind,
2220        "",
2221        "",
2222        anchor_record_id,
2223    )
2224    .map(|value| value_to_text(&value))
2225    .filter(|value| !value.is_empty())
2226    .unwrap_or_else(|| "info".to_owned());
2227    let message = dynamic_notification_parameter_value(
2228        audit.get("message").unwrap_or(&Value::Null),
2229        request,
2230        input_overrides,
2231        action_id,
2232        entity_id,
2233        event_kind,
2234        "",
2235        "",
2236        anchor_record_id,
2237    )
2238    .map(|value| value_to_text(&value))
2239    .unwrap_or_default();
2240    let mut annotations = Map::new();
2241    if let Some(rows) = audit.get("annotations").and_then(Value::as_array) {
2242        if rows.len() > 16 {
2243            return false;
2244        }
2245        for row in rows {
2246            let Some(field) = row
2247                .get("field")
2248                .and_then(Value::as_str)
2249                .filter(|value| !value.is_empty() && value.len() <= 128)
2250            else {
2251                return false;
2252            };
2253            let value = dynamic_notification_parameter_value(
2254                row.get("value").unwrap_or(&Value::Null),
2255                request,
2256                input_overrides,
2257                action_id,
2258                entity_id,
2259                event_kind,
2260                "",
2261                "",
2262                anchor_record_id,
2263            )
2264            .unwrap_or(Value::Null);
2265            annotations.insert(field.to_owned(), value);
2266        }
2267    }
2268    let audit_id = dynamic_take_next_section_id(root, "audit_log");
2269    let resource_id =
2270        dynamic_action_resource_id(action, request, input_overrides, anchor_record_id);
2271    let Some(rows) = root
2272        .as_object_mut()
2273        .map(|object| {
2274            object
2275                .entry("audit_log".to_owned())
2276                .or_insert_with(|| Value::Array(Vec::new()))
2277        })
2278        .and_then(Value::as_array_mut)
2279    else {
2280        return false;
2281    };
2282    let mut record = serde_json::Map::from_iter([
2283        ("id".to_owned(), Value::from(audit_id)),
2284        (
2285            "adapter_id".to_owned(),
2286            Value::String(adapter_id.to_owned()),
2287        ),
2288        (
2289            "adapter_kind".to_owned(),
2290            Value::String(adapter_kind.to_owned()),
2291        ),
2292        ("event".to_owned(), Value::String(event)),
2293        ("level".to_owned(), Value::String(level)),
2294        ("message".to_owned(), Value::String(message)),
2295        ("action_id".to_owned(), Value::String(action_id.to_owned())),
2296        ("entity_id".to_owned(), Value::String(entity_id.to_owned())),
2297        (
2298            "event_kind".to_owned(),
2299            Value::String(event_kind.to_owned()),
2300        ),
2301        ("resource_id".to_owned(), Value::String(resource_id)),
2302        ("created_at".to_owned(), Value::String(now_iso_utc())),
2303    ]);
2304    if !annotations.is_empty() {
2305        record.insert("annotations".to_owned(), Value::Object(annotations));
2306    }
2307    rows.push(Value::Object(record));
2308    true
2309}
2310
2311/// Maps one action failure status to a stable bounded failure code.
2312fn dynamic_action_failure_code(status: i32) -> &'static str {
2313    match status {
2314        400 => "bad_request",
2315        403 => "forbidden",
2316        404 => "not_found",
2317        409 => "conflict",
2318        415 => "unsupported_media_type",
2319        422 => "validation_failed",
2320        500 => "internal_error",
2321        _ => "action_failed",
2322    }
2323}
2324
2325/// Returns the request path without a query string.
2326///
2327/// # Parameters
2328///
2329/// - `request`: Input consumed by [`crate::runtime::dynamic::request_path`].
2330///
2331/// # Returns
2332///
2333/// Returns the computed value produced by [`crate::runtime::dynamic::request_path`].
2334///
2335/// # Examples
2336///
2337/// ```rust,ignore
2338/// let _result = pharos_native::runtime::dynamic::request_path(todo!());
2339/// ```
2340///
2341/// # Related
2342///
2343/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
2344pub fn request_path(request: Option<&HttpRequest>) -> String {
2345    request
2346        .and_then(|request| super::http::request_path_without_query(&request.target))
2347        .unwrap_or_else(|| "/".to_owned())
2348}
2349
2350/// Detects the HTMX request header using case-insensitive line parsing.
2351///
2352/// # Parameters
2353///
2354/// - `request`: Input consumed by [`crate::runtime::dynamic::request_is_hx`].
2355///
2356/// # Returns
2357///
2358/// Returns `true` when the condition checked by [`crate::runtime::dynamic::request_is_hx`] holds and `false` otherwise.
2359///
2360/// # Performance
2361///
2362/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::request_is_hx`].
2363///
2364/// # Examples
2365///
2366/// ```rust,ignore
2367/// let _result = pharos_native::runtime::dynamic::request_is_hx(todo!());
2368/// ```
2369///
2370/// # Related
2371///
2372/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
2373pub fn request_is_hx(request: Option<&HttpRequest>) -> bool {
2374    request.is_some_and(|request| {
2375        request.headers.lines().any(|line| {
2376            let (name, value) = line.split_once(':').unwrap_or(("", ""));
2377            name.trim().eq_ignore_ascii_case("hx-request")
2378                && value.trim().eq_ignore_ascii_case("true")
2379        })
2380    })
2381}
2382
2383/// Performs cookie value.
2384fn cookie_value(cookie_header: Option<&str>, name: &str) -> Option<String> {
2385    pharos_runtime_security::cookie_value(cookie_header, name)
2386}
2387
2388/// Performs constant time equal.
2389fn constant_time_equal(left: &[u8], right: &[u8]) -> bool {
2390    pharos_runtime_security::constant_time_equal(left, right)
2391}
2392
2393/// Performs hex digest.
2394///
2395/// # Parameters
2396///
2397/// - `bytes`: Raw bytes to hash into a lowercase hexadecimal SHA-256 digest.
2398///
2399/// # Returns
2400///
2401/// Returns the computed value produced by [`crate::runtime::dynamic::hex_digest`].
2402///
2403/// # Examples
2404///
2405/// ```rust,ignore
2406/// let _result = pharos_native::runtime::dynamic::hex_digest(todo!());
2407/// ```
2408///
2409/// # Related
2410///
2411/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
2412pub(crate) fn hex_digest(bytes: &[u8]) -> String {
2413    pharos_runtime_dynamic::hex_digest(bytes)
2414}
2415
2416/// Computes HMAC-SHA256 with a fixed-size key block and no external process.
2417///
2418/// # Parameters
2419///
2420/// - `key`: Input consumed by [`crate::runtime::dynamic::hmac_sha256`].
2421/// - `data`: Input consumed by [`crate::runtime::dynamic::hmac_sha256`].
2422///
2423/// # Returns
2424///
2425/// Returns the computed value produced by [`crate::runtime::dynamic::hmac_sha256`].
2426///
2427/// # Examples
2428///
2429/// ```rust,ignore
2430/// let _result = pharos_native::runtime::dynamic::hmac_sha256("key", b"");
2431/// ```
2432///
2433/// # Related
2434///
2435/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
2436pub fn hmac_sha256(key: &[u8], data: &[u8]) -> [u8; 32] {
2437    pharos_runtime_dynamic::hmac_sha256(key, data)
2438}
2439
2440/// Verifies the native PBKDF2 or development password format without leaking timing information.
2441///
2442/// # Parameters
2443///
2444/// - `stored_hash`: Input consumed by [`crate::runtime::dynamic::verify_password_hash`].
2445/// - `password`: Input consumed by [`crate::runtime::dynamic::verify_password_hash`].
2446/// - `username`: Input consumed by [`crate::runtime::dynamic::verify_password_hash`].
2447///
2448/// # Returns
2449///
2450/// Returns `true` when the condition checked by [`crate::runtime::dynamic::verify_password_hash`] holds and `false` otherwise.
2451///
2452/// # Examples
2453///
2454/// ```rust,ignore
2455/// let _result = pharos_native::runtime::dynamic::verify_password_hash(todo!(), todo!(), todo!());
2456/// ```
2457///
2458/// # Related
2459///
2460/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
2461pub fn verify_password_hash(stored_hash: &str, password: &str, username: Option<&str>) -> bool {
2462    pharos_runtime_dynamic::verify_password_hash(stored_hash, password, username)
2463}
2464
2465/// Generates a cryptographically random secret encoded as lowercase hexadecimal text.
2466///
2467/// # Parameters
2468///
2469/// - `byte_count`: Input consumed by [`crate::runtime::dynamic::random_secret_hex`].
2470///
2471/// # Returns
2472///
2473/// Returns the successful value produced by [`crate::runtime::dynamic::random_secret_hex`] or an error when the operation fails.
2474///
2475/// # Errors
2476///
2477/// Returns an error value when [`crate::runtime::dynamic::random_secret_hex`] cannot complete successfully.
2478///
2479/// # Examples
2480///
2481/// ```rust,ignore
2482/// let _result = pharos_native::runtime::dynamic::random_secret_hex(todo!());
2483/// ```
2484///
2485/// # Related
2486///
2487/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
2488pub fn random_secret_hex(byte_count: usize) -> Result<String, i32> {
2489    pharos_runtime_dynamic::random_secret_hex(byte_count).map_err(|_| 69)
2490}
2491
2492/// Creates a salted production PBKDF2-SHA256 password hash.
2493///
2494/// # Parameters
2495///
2496/// - `password`: Input consumed by [`crate::runtime::dynamic::hash_password`].
2497///
2498/// # Returns
2499///
2500/// Returns the successful value produced by [`crate::runtime::dynamic::hash_password`] or an error when the operation fails.
2501///
2502/// # Errors
2503///
2504/// Returns an error value when [`crate::runtime::dynamic::hash_password`] cannot complete successfully.
2505///
2506/// # Examples
2507///
2508/// ```rust,ignore
2509/// let _result = pharos_native::runtime::dynamic::hash_password(todo!());
2510/// ```
2511///
2512/// # Related
2513///
2514/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
2515pub fn hash_password(password: &str) -> Result<String, i32> {
2516    pharos_runtime_dynamic::hash_password(password).map_err(|_| 69)
2517}
2518
2519/// Returns the active user id for a session cookie, rejecting expired or revoked rows.
2520///
2521/// # Parameters
2522///
2523/// - `state_path`: Input consumed by [`crate::runtime::dynamic::active_user_id_from_cookie`].
2524/// - `cookie_header`: Input consumed by [`crate::runtime::dynamic::active_user_id_from_cookie`].
2525///
2526/// # Returns
2527///
2528/// Returns the computed value produced by [`crate::runtime::dynamic::active_user_id_from_cookie`].
2529///
2530/// # Performance
2531///
2532/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::active_user_id_from_cookie`].
2533///
2534/// # Examples
2535///
2536/// ```rust,ignore
2537/// let _result = pharos_native::runtime::dynamic::active_user_id_from_cookie(todo!(), "session=abc");
2538/// ```
2539///
2540/// # Related
2541///
2542/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
2543pub fn active_user_id_from_cookie(state_path: &Path, cookie_header: Option<&str>) -> i64 {
2544    let Some(token) = cookie_value(cookie_header, pharos_runtime_security::SESSION_COOKIE_NAME)
2545        .filter(|value| !value.is_empty())
2546    else {
2547        return 0;
2548    };
2549    let token_hash = hex_digest(&Sha256::digest(token.as_bytes()));
2550    let Some(root) = fs::read_to_string(state_path)
2551        .ok()
2552        .and_then(|text| serde_json::from_str::<Value>(&text).ok())
2553    else {
2554        return 0;
2555    };
2556    let now_epoch = current_epoch_seconds();
2557    let now_timestamp = super::current_timestamp_utc();
2558    root.get("sessions")
2559        .and_then(Value::as_array)
2560        .and_then(|rows| {
2561            rows.iter().find(|row| {
2562                row.get("session_token_hash").and_then(Value::as_str) == Some(token_hash.as_str())
2563                    && row
2564                        .get("revoked_at")
2565                        .is_none_or(|value| value.is_null() || value.as_str() == Some(""))
2566                    && row
2567                        .get("expires_at")
2568                        .and_then(Value::as_str)
2569                        .is_some_and(|value| {
2570                            value
2571                                .parse::<u64>()
2572                                .map_or(value > now_timestamp.as_str(), |expires| {
2573                                    expires > now_epoch
2574                                })
2575                        })
2576            })
2577        })
2578        .and_then(|row| row.get("user_id"))
2579        .and_then(|value| value.as_i64().or_else(|| value.as_str()?.parse().ok()))
2580        .unwrap_or(0)
2581}
2582
2583/// Performs current epoch seconds.
2584fn current_epoch_seconds() -> u64 {
2585    SystemTime::now()
2586        .duration_since(UNIX_EPOCH)
2587        .map(|value| value.as_secs())
2588        .unwrap_or(0)
2589}
2590
2591/// Performs now iso utc.
2592fn now_iso_utc() -> String {
2593    current_epoch_seconds().to_string()
2594}
2595
2596/// Parses one bounded runtime timestamp from either epoch seconds or UTC text.
2597///
2598/// # Parameters
2599///
2600/// - `request`: Input consumed by [`crate::runtime::dynamic::csrf_request_valid`].
2601/// - `form_body`: Input consumed by [`crate::runtime::dynamic::csrf_request_valid`].
2602///
2603/// # Returns
2604///
2605/// Returns `true` when the condition checked by [`crate::runtime::dynamic::csrf_request_valid`] holds and `false` otherwise.
2606///
2607/// # Performance
2608///
2609/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::csrf_request_valid`].
2610///
2611/// # Examples
2612///
2613/// ```rust,ignore
2614/// let _result = pharos_native::runtime::dynamic::csrf_request_valid(todo!(), "name=value");
2615/// ```
2616///
2617/// # Related
2618///
2619/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
2620pub fn csrf_request_valid(request: &HttpRequest, form_body: Option<&str>) -> bool {
2621    let cookie = cookie_value(request.cookie.as_deref(), "pharos_csrf");
2622    let form = request_form_value(request, "csrf_token").or_else(|| {
2623        form_body.and_then(|body| json_commands::request_form_value(body, "csrf_token"))
2624    });
2625    let header = request.headers.lines().find_map(|line| {
2626        let (name, value) = line.split_once(':')?;
2627        name.trim()
2628            .eq_ignore_ascii_case("x-csrf-token")
2629            .then_some(value.trim().to_owned())
2630    });
2631    let request_token = form.or(header);
2632    cookie
2633        .as_deref()
2634        .zip(request_token.as_deref())
2635        .is_some_and(|(left, right)| constant_time_equal(left.as_bytes(), right.as_bytes()))
2636}
2637
2638/// Formats a scoped session cookie with secure defaults and an optional clear operation.
2639///
2640/// # Parameters
2641///
2642/// - `app`: Input consumed by [`crate::runtime::dynamic::session_cookie_header`].
2643/// - `request`: Input consumed by [`crate::runtime::dynamic::session_cookie_header`].
2644/// - `token`: Input consumed by [`crate::runtime::dynamic::session_cookie_header`].
2645/// - `clear`: Input consumed by [`crate::runtime::dynamic::session_cookie_header`].
2646///
2647/// # Returns
2648///
2649/// Returns the computed value produced by [`crate::runtime::dynamic::session_cookie_header`].
2650///
2651/// # Examples
2652///
2653/// ```rust,ignore
2654/// let _result = pharos_native::runtime::dynamic::session_cookie_header(todo!(), todo!(), todo!(), todo!());
2655/// ```
2656///
2657/// # Related
2658///
2659/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
2660pub fn session_cookie_header(
2661    app: &AppRuntime,
2662    request: Option<&HttpRequest>,
2663    token: Option<&str>,
2664    clear: bool,
2665) -> String {
2666    let path = app
2667        .base_path
2668        .as_deref()
2669        .filter(|value| !value.is_empty())
2670        .unwrap_or("/");
2671    let secure = request_origin_scheme(request, NativeTransportType::Tcp, None, None) == "https";
2672    pharos_runtime_security::session_cookie_header(path, token, clear, secure)
2673}
2674
2675/// Returns a page title from declared metadata or a body parameter fallback.
2676///
2677/// # Parameters
2678///
2679/// - `page_spec`: Input consumed by [`crate::runtime::dynamic::page_title`].
2680/// - `body_params`: Input consumed by [`crate::runtime::dynamic::page_title`].
2681///
2682/// # Returns
2683///
2684/// Returns the computed value produced by [`crate::runtime::dynamic::page_title`].
2685///
2686/// # Examples
2687///
2688/// ```rust,ignore
2689/// let _result = pharos_native::runtime::dynamic::page_title(todo!(), todo!());
2690/// ```
2691///
2692/// # Related
2693///
2694/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
2695pub fn page_title(page_spec: &Value, body_params: &Value) -> String {
2696    page_spec
2697        .get("title")
2698        .and_then(Value::as_str)
2699        .filter(|value| !value.is_empty())
2700        .map(ToOwned::to_owned)
2701        .or_else(|| {
2702            page_spec
2703                .get("title_mode")
2704                .and_then(Value::as_str)
2705                .and_then(|key| body_params.get(key))
2706                .map(value_to_text)
2707        })
2708        .unwrap_or_default()
2709}
2710
2711/// Performs value to text.
2712fn value_to_text(value: &Value) -> String {
2713    match value {
2714        Value::Null => String::new(),
2715        Value::String(value) => value.clone(),
2716        Value::Bool(value) => value.to_string(),
2717        Value::Number(value) => value.to_string(),
2718        _ => serde_json::to_string(value).unwrap_or_default(),
2719    }
2720}
2721
2722/// Performs dynamic value order cmp.
2723fn dynamic_value_order_cmp(left: Option<&Value>, right: Option<&Value>) -> Ordering {
2724    match (
2725        left.and_then(Value::as_i64),
2726        right.and_then(Value::as_i64),
2727        left.map(value_to_text),
2728        right.map(value_to_text),
2729    ) {
2730        (Some(left_num), Some(right_num), _, _) => left_num.cmp(&right_num),
2731        (_, _, Some(left_text), Some(right_text)) => left_text.cmp(&right_text),
2732        (Some(_), None, _, _) => Ordering::Less,
2733        (None, Some(_), _, _) => Ordering::Greater,
2734        _ => Ordering::Equal,
2735    }
2736}
2737
2738/// Performs dynamic record contract value.
2739fn dynamic_record_contract_value(
2740    root: &mut Value,
2741    request: &HttpRequest,
2742    input_overrides: &BTreeMap<String, Option<String>>,
2743    field_spec: &Value,
2744    anchor_record_id: i64,
2745    list_item: Option<&str>,
2746    list_index: Option<usize>,
2747    matched_record_count: i64,
2748    updated_record_count: i64,
2749    skipped_record_count: i64,
2750) -> Value {
2751    let source = field_spec
2752        .get("source")
2753        .and_then(Value::as_str)
2754        .unwrap_or_default();
2755    match source {
2756        "next_id" => Value::from(dynamic_take_next_global_id(root)),
2757        "input" => Value::String(
2758            dynamic_action_input_value(
2759                request,
2760                input_overrides,
2761                field_spec
2762                    .get("field")
2763                    .and_then(Value::as_str)
2764                    .unwrap_or_default(),
2765            )
2766            .unwrap_or_default(),
2767        ),
2768        "literal" => field_spec.get("value").cloned().unwrap_or(Value::Null),
2769        "context_anchor_created_id" => Value::from(anchor_record_id),
2770        "list_item" => Value::String(list_item.unwrap_or_default().to_owned()),
2771        "list_index" => Value::from(list_index.unwrap_or(0).saturating_add(1) as i64),
2772        "context_matched_count" => Value::from(matched_record_count.max(0)),
2773        "context_updated_count" => Value::from(updated_record_count.max(0)),
2774        "context_skipped_count" => Value::from(skipped_record_count.max(0)),
2775        "now" => Value::String(now_iso_utc()),
2776        _ => Value::Null,
2777    }
2778}
2779
2780/// Persists a complete state JSON document through the bounded filesystem helper.
2781///
2782/// # Parameters
2783///
2784/// - `app`: Input consumed by [`crate::runtime::dynamic::persist_app_state_text`].
2785/// - `state_json`: Input consumed by [`crate::runtime::dynamic::persist_app_state_text`].
2786///
2787/// # Returns
2788///
2789/// Returns the computed value produced by [`crate::runtime::dynamic::persist_app_state_text`].
2790///
2791/// # Performance
2792///
2793/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::persist_app_state_text`].
2794///
2795/// # Examples
2796///
2797/// ```rust,ignore
2798/// let _result = pharos_native::runtime::dynamic::persist_app_state_text(todo!(), todo!());
2799/// ```
2800///
2801/// # Related
2802///
2803/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
2804pub fn persist_app_state_text(app: &AppRuntime, state_json: &str) -> i32 {
2805    let Some(path) = app.state_path.as_deref().filter(|value| !value.is_empty()) else {
2806        return 69;
2807    };
2808    if state_json.len() > 16 * 1024 * 1024 || serde_json::from_str::<Value>(state_json).is_err() {
2809        return 64;
2810    }
2811    super::fs::write_text_file(Path::new(path), state_json)
2812}
2813
2814/// Appends one rate-limit event with a monotonic state-local identifier.
2815///
2816/// # Parameters
2817///
2818/// - `app`: Input consumed by [`crate::runtime::dynamic::append_rate_limit_event_runtime_local`].
2819/// - `policy_key`: Input consumed by [`crate::runtime::dynamic::append_rate_limit_event_runtime_local`].
2820/// - `bucket_key`: Input consumed by [`crate::runtime::dynamic::append_rate_limit_event_runtime_local`].
2821/// - `outcome`: Input consumed by [`crate::runtime::dynamic::append_rate_limit_event_runtime_local`].
2822///
2823/// # Returns
2824///
2825/// Returns the computed value produced by [`crate::runtime::dynamic::append_rate_limit_event_runtime_local`].
2826///
2827/// # Performance
2828///
2829/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::append_rate_limit_event_runtime_local`].
2830///
2831/// # Examples
2832///
2833/// ```rust,ignore
2834/// let _result = pharos_native::runtime::dynamic::append_rate_limit_event_runtime_local(todo!(), todo!(), todo!(), todo!());
2835/// ```
2836///
2837/// # Related
2838///
2839/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
2840pub fn append_rate_limit_event_runtime_local(
2841    app: &AppRuntime,
2842    policy_key: &str,
2843    bucket_key: &str,
2844    outcome: &str,
2845) -> i32 {
2846    if policy_key.is_empty() || bucket_key.len() > 512 || outcome.len() > 128 {
2847        return 64;
2848    }
2849    let Some(path) = app.state_path.as_deref() else {
2850        return 69;
2851    };
2852    let Ok(mut root) = fs::read_to_string(path)
2853        .ok()
2854        .and_then(|text| serde_json::from_str::<Value>(&text).ok())
2855        .ok_or(66)
2856    else {
2857        return 66;
2858    };
2859    let id = root
2860        .get("rate_limit_events")
2861        .and_then(Value::as_array)
2862        .into_iter()
2863        .flatten()
2864        .filter_map(|row| row.get("id").and_then(Value::as_i64))
2865        .max()
2866        .unwrap_or(0)
2867        + 1;
2868    let events = root.as_object_mut().and_then(|object| {
2869        let value = object
2870            .entry("rate_limit_events".to_owned())
2871            .or_insert_with(|| Value::Array(Vec::new()));
2872        if !value.is_array() {
2873            *value = Value::Array(Vec::new());
2874        }
2875        value.as_array_mut()
2876    });
2877    let Some(events) = events else {
2878        return 69;
2879    };
2880    let created_epoch = SystemTime::now()
2881        .duration_since(UNIX_EPOCH)
2882        .map(|value| value.as_secs())
2883        .unwrap_or(0);
2884    events.push(serde_json::json!({"id": id, "policy_key": policy_key, "bucket_key": bucket_key, "outcome": outcome, "created_at": now_iso_utc(), "created_epoch": created_epoch}));
2885    persist_app_state_text(app, &serde_json::to_string(&root).unwrap_or_default())
2886}
2887
2888/// Atomically admits one request under a bounded state-backed rate-limit window.
2889///
2890/// # Parameters
2891///
2892/// - `app`: Input consumed by [`crate::runtime::dynamic::admit_rate_limited_request_runtime_local`].
2893/// - `policy_key`: Input consumed by [`crate::runtime::dynamic::admit_rate_limited_request_runtime_local`].
2894/// - `bucket_key`: Input consumed by [`crate::runtime::dynamic::admit_rate_limited_request_runtime_local`].
2895/// - `maximum`: Input consumed by [`crate::runtime::dynamic::admit_rate_limited_request_runtime_local`].
2896/// - `window_seconds`: Input consumed by [`crate::runtime::dynamic::admit_rate_limited_request_runtime_local`].
2897///
2898/// # Returns
2899///
2900/// Returns the successful value produced by [`crate::runtime::dynamic::admit_rate_limited_request_runtime_local`] or an error when the operation fails.
2901///
2902/// # Errors
2903///
2904/// Returns an error value when [`crate::runtime::dynamic::admit_rate_limited_request_runtime_local`] cannot complete successfully.
2905///
2906/// # Performance
2907///
2908/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::admit_rate_limited_request_runtime_local`].
2909///
2910/// # Examples
2911///
2912/// ```rust,ignore
2913/// let _result = pharos_native::runtime::dynamic::admit_rate_limited_request_runtime_local(todo!(), todo!(), todo!(), todo!(), todo!());
2914/// ```
2915///
2916/// # Related
2917///
2918/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
2919pub fn admit_rate_limited_request_runtime_local(
2920    app: &AppRuntime,
2921    policy_key: &str,
2922    bucket_key: &str,
2923    maximum: usize,
2924    window_seconds: u64,
2925) -> Result<bool, i32> {
2926    if policy_key.is_empty()
2927        || policy_key.len() > 128
2928        || bucket_key.is_empty()
2929        || bucket_key.len() > 512
2930        || maximum == 0
2931        || maximum > 100_000
2932        || window_seconds == 0
2933        || window_seconds > 86_400
2934    {
2935        return Err(64);
2936    }
2937    let path = app
2938        .state_path
2939        .as_deref()
2940        .filter(|value| !value.is_empty())
2941        .map(Path::new)
2942        .ok_or(69)?;
2943    let mut root = fs::read_to_string(path)
2944        .ok()
2945        .and_then(|text| serde_json::from_str::<Value>(&text).ok())
2946        .filter(Value::is_object)
2947        .ok_or(66)?;
2948    let now = SystemTime::now()
2949        .duration_since(UNIX_EPOCH)
2950        .map_err(|_| 70)?
2951        .as_secs();
2952    let cutoff = now.saturating_sub(window_seconds);
2953    let events = root
2954        .as_object_mut()
2955        .ok_or(66)?
2956        .entry("rate_limit_events".to_owned())
2957        .or_insert_with(|| Value::Array(Vec::new()));
2958    if !events.is_array() {
2959        *events = Value::Array(Vec::new());
2960    }
2961    let events = events.as_array_mut().ok_or(66)?;
2962    events.retain(|event| {
2963        event
2964            .get("created_epoch")
2965            .and_then(Value::as_u64)
2966            .is_some_and(|created| created >= cutoff)
2967    });
2968    let observed = events
2969        .iter()
2970        .filter(|event| {
2971            event.get("policy_key").and_then(Value::as_str) == Some(policy_key)
2972                && event.get("bucket_key").and_then(Value::as_str) == Some(bucket_key)
2973                && event
2974                    .get("created_epoch")
2975                    .and_then(Value::as_u64)
2976                    .is_some_and(|created| created >= cutoff)
2977        })
2978        .count();
2979    if observed >= maximum {
2980        return Ok(false);
2981    }
2982    if events.len() >= app.resource_budgets.max_state_section_items {
2983        return Err(69);
2984    }
2985    let next_id = events
2986        .iter()
2987        .filter_map(|event| event.get("id").and_then(Value::as_i64))
2988        .max()
2989        .unwrap_or(0)
2990        .saturating_add(1);
2991    events.push(serde_json::json!({
2992        "id": next_id,
2993        "policy_key": policy_key,
2994        "bucket_key": bucket_key,
2995        "outcome": "admitted",
2996        "created_at": now_iso_utc(),
2997        "created_epoch": now,
2998    }));
2999    let encoded = serde_json::to_string(&root).map_err(|_| 70)?;
3000    (super::fs::write_text_file(path, &encoded) == 0)
3001        .then_some(true)
3002        .ok_or(69)
3003}
3004
3005/// Updates one notification delivery row and persists the validated state document.
3006///
3007/// # Parameters
3008///
3009/// - `app`: Input consumed by [`crate::runtime::dynamic::update_notification_delivery_status`].
3010/// - `delivery_id`: Input consumed by [`crate::runtime::dynamic::update_notification_delivery_status`].
3011/// - `status`: Input consumed by [`crate::runtime::dynamic::update_notification_delivery_status`].
3012/// - `delivered_at`: Input consumed by [`crate::runtime::dynamic::update_notification_delivery_status`].
3013///
3014/// # Returns
3015///
3016/// Returns the computed value produced by [`crate::runtime::dynamic::update_notification_delivery_status`].
3017///
3018/// # Performance
3019///
3020/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::update_notification_delivery_status`].
3021///
3022/// # Examples
3023///
3024/// ```rust,ignore
3025/// let _result = pharos_native::runtime::dynamic::update_notification_delivery_status(todo!(), todo!(), 400, todo!());
3026/// ```
3027///
3028/// # Related
3029///
3030/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
3031pub fn update_notification_delivery_status(
3032    app: &AppRuntime,
3033    delivery_id: i64,
3034    status: &str,
3035    delivered_at: Option<&str>,
3036) -> i32 {
3037    if delivery_id <= 0 || status.is_empty() || status.len() > 64 {
3038        return 64;
3039    }
3040    let Some(path) = app.state_path.as_deref() else {
3041        return 69;
3042    };
3043    let Ok(mut root) = fs::read_to_string(path)
3044        .ok()
3045        .and_then(|text| serde_json::from_str::<Value>(&text).ok())
3046        .ok_or(66)
3047    else {
3048        return 66;
3049    };
3050    let Some(row) = root
3051        .get_mut("notification_deliveries")
3052        .and_then(Value::as_array_mut)
3053        .and_then(|rows| {
3054            rows.iter_mut()
3055                .find(|row| row.get("id").and_then(Value::as_i64) == Some(delivery_id))
3056        })
3057    else {
3058        return 66;
3059    };
3060    let Some(object) = row.as_object_mut() else {
3061        return 69;
3062    };
3063    object.insert(
3064        "delivery_status".to_owned(),
3065        Value::String(status.to_owned()),
3066    );
3067    object.insert("updated_at".to_owned(), Value::String(now_iso_utc()));
3068    if let Some(delivered_at) = delivered_at {
3069        object.insert(
3070            "delivered_at".to_owned(),
3071            Value::String(delivered_at.to_owned()),
3072        );
3073    }
3074    persist_app_state_text(app, &serde_json::to_string(&root).unwrap_or_default())
3075}
3076
3077/// Renders a bounded JSON error HTTP response without interpolating unescaped data.
3078///
3079/// # Parameters
3080///
3081/// - `status`: Input consumed by [`crate::runtime::dynamic::write_json_error`].
3082/// - `error_type`: Input consumed by [`crate::runtime::dynamic::write_json_error`].
3083/// - `message`: Input consumed by [`crate::runtime::dynamic::write_json_error`].
3084///
3085/// # Returns
3086///
3087/// Returns the computed value produced by [`crate::runtime::dynamic::write_json_error`].
3088///
3089/// # Examples
3090///
3091/// ```rust,ignore
3092/// let _result = pharos_native::runtime::dynamic::write_json_error(400, todo!(), "example");
3093/// ```
3094///
3095/// # Related
3096///
3097/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
3098pub fn write_json_error(status: u16, error_type: &str, message: &str) -> String {
3099    let code = pharos_runtime_core::error::ErrorCode::from_http_status(status);
3100    let mut envelope = pharos_runtime_core::error::envelope(code, message, None);
3101    if let Some(error) = envelope.get_mut("error").and_then(Value::as_object_mut) {
3102        error.insert("context".to_owned(), Value::String(error_type.to_owned()));
3103    }
3104    let body = serde_json::to_string(&envelope).unwrap_or_else(|_| {
3105        "{\"schema\":\"pharos.error.v1\",\"error\":{\"code\":\"internal_failure\",\"message\":\"response encoding failed\",\"retryable\":false,\"correlation_id\":\"\"}}".to_owned()
3106    });
3107    format!(
3108        "HTTP/1.1 {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nCache-Control: no-store\r\nConnection: close\r\n\r\n{}",
3109        super::http::http_status_text(status),
3110        body.len(),
3111        body
3112    )
3113}
3114
3115/// Builds an HTMX fragment response with a scoped base path and CSRF cookie.
3116///
3117/// # Parameters
3118///
3119/// - `app`: Input consumed by [`crate::runtime::dynamic::write_hx_fragment_response_with_csrf`].
3120/// - `request`: Input consumed by [`crate::runtime::dynamic::write_hx_fragment_response_with_csrf`].
3121/// - `fragment_html`: Input consumed by [`crate::runtime::dynamic::write_hx_fragment_response_with_csrf`].
3122///
3123/// # Returns
3124///
3125/// Returns the computed value produced by [`crate::runtime::dynamic::write_hx_fragment_response_with_csrf`].
3126///
3127/// # Examples
3128///
3129/// ```rust,ignore
3130/// let _result = pharos_native::runtime::dynamic::write_hx_fragment_response_with_csrf(todo!(), todo!(), todo!());
3131/// ```
3132///
3133/// # Related
3134///
3135/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
3136pub fn write_hx_fragment_response_with_csrf(
3137    app: &AppRuntime,
3138    request: Option<&HttpRequest>,
3139    fragment_html: &str,
3140) -> String {
3141    let scoped = json_commands::template_apply_base_path(fragment_html, app.base_path.as_deref())
3142        .unwrap_or_default();
3143    let token = cookie_value(
3144        request.and_then(|request| request.cookie.as_deref()),
3145        "pharos_csrf",
3146    )
3147    .unwrap_or_default();
3148    format!(
3149        "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: {}\r\nSet-Cookie: pharos_csrf={}; Path=/; SameSite=Lax\r\nConnection: close\r\n\r\n{}",
3150        scoped.len(),
3151        token,
3152        scoped
3153    )
3154}
3155
3156/// Renders one declared page into a complete response string with bounded headers.
3157///
3158/// # Parameters
3159///
3160/// - `app_root`: Input consumed by [`crate::runtime::dynamic::render_declared_page_response_with_headers`].
3161/// - `page_id`: Input consumed by [`crate::runtime::dynamic::render_declared_page_response_with_headers`].
3162/// - `body_params_json`: Input consumed by [`crate::runtime::dynamic::render_declared_page_response_with_headers`].
3163/// - `extra_headers`: Input consumed by [`crate::runtime::dynamic::render_declared_page_response_with_headers`].
3164///
3165/// # Returns
3166///
3167/// Returns the successful value produced by [`crate::runtime::dynamic::render_declared_page_response_with_headers`] or an error when the operation fails.
3168///
3169/// # Errors
3170///
3171/// Returns an error value when [`crate::runtime::dynamic::render_declared_page_response_with_headers`] cannot complete successfully.
3172///
3173/// # Performance
3174///
3175/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::render_declared_page_response_with_headers`].
3176///
3177/// # Examples
3178///
3179/// ```rust,ignore
3180/// let _result = pharos_native::runtime::dynamic::render_declared_page_response_with_headers(std::path::Path::new("apps/dev_docs"), "home", todo!(), todo!());
3181/// ```
3182///
3183/// # Related
3184///
3185/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
3186pub fn render_declared_page_response_with_headers(
3187    app_root: &Path,
3188    page_id: &str,
3189    body_params_json: &str,
3190    extra_headers: Option<&str>,
3191) -> Result<String, i32> {
3192    let page = json_commands::template_declared_page_contract(app_root, page_id).ok_or(404)?;
3193    let page_spec: Value = serde_json::from_str(&page).map_err(|_| 500)?;
3194    let template_path = page_spec
3195        .get("template_path")
3196        .and_then(Value::as_str)
3197        .ok_or(500)?;
3198    let params: Value = serde_json::from_str(body_params_json).map_err(|_| 400)?;
3199    let html = json_commands::render_file_template(Path::new(template_path), &params).ok_or(500)?;
3200    let headers = extra_headers.unwrap_or("").replace(['\r', '\n'], " ");
3201    Ok(format!(
3202        "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: {}\r\n{}\r\nConnection: close\r\n\r\n{}",
3203        html.len(),
3204        headers,
3205        html
3206    ))
3207}
3208
3209/// Renders a declared page without additional response headers.
3210///
3211/// # Parameters
3212///
3213/// - `app_root`: Input consumed by [`crate::runtime::dynamic::render_declared_page_response`].
3214/// - `page_id`: Input consumed by [`crate::runtime::dynamic::render_declared_page_response`].
3215/// - `body_params_json`: Input consumed by [`crate::runtime::dynamic::render_declared_page_response`].
3216///
3217/// # Returns
3218///
3219/// Returns the successful value produced by [`crate::runtime::dynamic::render_declared_page_response`] or an error when the operation fails.
3220///
3221/// # Errors
3222///
3223/// Returns an error value when [`crate::runtime::dynamic::render_declared_page_response`] cannot complete successfully.
3224///
3225/// # Examples
3226///
3227/// ```rust,ignore
3228/// let _result = pharos_native::runtime::dynamic::render_declared_page_response(std::path::Path::new("apps/dev_docs"), "home", todo!());
3229/// ```
3230///
3231/// # Related
3232///
3233/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
3234pub fn render_declared_page_response(
3235    app_root: &Path,
3236    page_id: &str,
3237    body_params_json: &str,
3238) -> Result<String, i32> {
3239    render_declared_page_response_with_headers(app_root, page_id, body_params_json, None)
3240}
3241
3242/// Resolves the small page parameter contract needed by the final Rust renderer.
3243///
3244/// # Parameters
3245///
3246/// - `page_id`: Input consumed by [`crate::runtime::dynamic::resolve_declared_page_params_json`].
3247/// - `request_path`: Input consumed by [`crate::runtime::dynamic::resolve_declared_page_params_json`].
3248///
3249/// # Returns
3250///
3251/// Returns the computed value produced by [`crate::runtime::dynamic::resolve_declared_page_params_json`].
3252///
3253/// # Examples
3254///
3255/// ```rust,ignore
3256/// let _result = pharos_native::runtime::dynamic::resolve_declared_page_params_json("home", "/");
3257/// ```
3258///
3259/// # Related
3260///
3261/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
3262pub fn resolve_declared_page_params_json(page_id: &str, request_path: &str) -> String {
3263    serde_json::to_string(&serde_json::json!({"page_id": page_id, "request_path": request_path, "error_html": "", "next_input": ""})).unwrap_or_else(|_| "{}".to_owned())
3264}
3265
3266/// Handles registration by validating form input and appending a password-hash user row.
3267///
3268/// # Parameters
3269///
3270/// - `state_path`: Input consumed by [`crate::runtime::dynamic::handle_register_account_action`].
3271/// - `request`: Input consumed by [`crate::runtime::dynamic::handle_register_account_action`].
3272///
3273/// # Returns
3274///
3275/// Returns the successful value produced by [`crate::runtime::dynamic::handle_register_account_action`] or an error when the operation fails.
3276///
3277/// # Errors
3278///
3279/// Returns an error value when [`crate::runtime::dynamic::handle_register_account_action`] cannot complete successfully.
3280///
3281/// # Performance
3282///
3283/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::handle_register_account_action`].
3284///
3285/// # Examples
3286///
3287/// ```rust,ignore
3288/// let _result = pharos_native::runtime::dynamic::handle_register_account_action(todo!(), todo!());
3289/// ```
3290///
3291/// # Related
3292///
3293/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
3294pub fn handle_register_account_action(
3295    state_path: &Path,
3296    request: &HttpRequest,
3297) -> Result<String, i32> {
3298    let username = request_form_value(request, "username").ok_or(422)?;
3299    let email = request_form_value(request, "email").ok_or(422)?;
3300    let password = request_form_value(request, "password").ok_or(422)?;
3301    let confirmation = request_form_value(request, "password_confirm").unwrap_or_default();
3302    if username.is_empty()
3303        || !json_commands::generated_surface_text_is_email(&email)
3304        || password.len() < 8
3305        || password != confirmation
3306    {
3307        return Err(422);
3308    }
3309    let mut root: Value =
3310        serde_json::from_str(&fs::read_to_string(state_path).map_err(|_| 66)?).map_err(|_| 66)?;
3311    let users = root
3312        .as_object_mut()
3313        .ok_or(66)?
3314        .entry("users".to_owned())
3315        .or_insert_with(|| Value::Array(Vec::new()));
3316    let rows = users.as_array_mut().ok_or(69)?;
3317    if rows.iter().any(|row| {
3318        row.get("username").and_then(Value::as_str) == Some(username.as_str())
3319            || row
3320                .get("email")
3321                .and_then(Value::as_str)
3322                .is_some_and(|value| value.eq_ignore_ascii_case(&email))
3323    }) {
3324        return Err(409);
3325    }
3326    let id = rows
3327        .iter()
3328        .filter_map(|row| row.get("id").and_then(Value::as_i64))
3329        .max()
3330        .unwrap_or(0)
3331        + 1;
3332    let password_hash = hash_password(&password)?;
3333    rows.push(serde_json::json!({"id": id, "username": username, "email": email, "password_hash": password_hash, "is_active": true, "email_verified": false}));
3334    super::fs::write_text_file(state_path, &serde_json::to_string(&root).map_err(|_| 75)?)
3335        .eq(&0)
3336        .then_some(format!("{{\"user_id\":{id}}}"))
3337        .ok_or(69)
3338}
3339
3340/// Claims an active email verification row and marks its user verified.
3341///
3342/// # Parameters
3343///
3344/// - `state_path`: Input consumed by [`crate::runtime::dynamic::handle_verify_email_action`].
3345/// - `token_hash`: Input consumed by [`crate::runtime::dynamic::handle_verify_email_action`].
3346/// - `now`: Input consumed by [`crate::runtime::dynamic::handle_verify_email_action`].
3347///
3348/// # Returns
3349///
3350/// Returns the computed value produced by [`crate::runtime::dynamic::handle_verify_email_action`].
3351///
3352/// # Performance
3353///
3354/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::handle_verify_email_action`].
3355///
3356/// # Examples
3357///
3358/// ```rust,ignore
3359/// let _result = pharos_native::runtime::dynamic::handle_verify_email_action(todo!(), todo!(), todo!());
3360/// ```
3361///
3362/// # Related
3363///
3364/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
3365pub fn handle_verify_email_action(state_path: &Path, token_hash: &str, now: &str) -> i32 {
3366    let Ok(mut root) = fs::read_to_string(state_path)
3367        .ok()
3368        .and_then(|text| serde_json::from_str::<Value>(&text).ok())
3369        .ok_or(66)
3370    else {
3371        return 66;
3372    };
3373    let Some(reset) = root
3374        .get("email_verifications")
3375        .and_then(Value::as_array)
3376        .and_then(|rows| {
3377            rows.iter().find(|row| {
3378                row.get("token_hash").and_then(Value::as_str) == Some(token_hash)
3379                    && row
3380                        .get("claimed_at")
3381                        .is_none_or(|value| value.is_null() || value.as_str() == Some(""))
3382                    && row
3383                        .get("expires_at")
3384                        .and_then(Value::as_str)
3385                        .is_some_and(|value| value > now)
3386            })
3387        })
3388        .cloned()
3389    else {
3390        return 404;
3391    };
3392    let user_id = reset.get("user_id").and_then(Value::as_i64).unwrap_or(0);
3393    let Some(object) = root.as_object_mut() else {
3394        return 66;
3395    };
3396    if let Some(row) = object
3397        .get_mut("email_verifications")
3398        .and_then(Value::as_array_mut)
3399        .and_then(|rows| {
3400            rows.iter_mut()
3401                .find(|row| row.get("token_hash").and_then(Value::as_str) == Some(token_hash))
3402        })
3403    {
3404        row.as_object_mut()
3405            .map(|row| row.insert("claimed_at".to_owned(), Value::String(now.to_owned())));
3406    }
3407    if let Some(row) = object
3408        .get_mut("users")
3409        .and_then(Value::as_array_mut)
3410        .and_then(|rows| {
3411            rows.iter_mut()
3412                .find(|row| row.get("id").and_then(Value::as_i64) == Some(user_id))
3413        })
3414    {
3415        row.as_object_mut()
3416            .map(|row| row.insert("email_verified".to_owned(), Value::Bool(true)));
3417    }
3418    super::fs::write_text_file(
3419        state_path,
3420        &serde_json::to_string(&root).unwrap_or_default(),
3421    )
3422}
3423
3424/// Returns a password-reset request outcome without sending email or invoking scripts.
3425///
3426/// # Parameters
3427///
3428/// - `_state_path`: Input consumed by [`crate::runtime::dynamic::handle_password_reset_request_action`].
3429/// - `email`: Input consumed by [`crate::runtime::dynamic::handle_password_reset_request_action`].
3430///
3431/// # Returns
3432///
3433/// Returns the successful value produced by [`crate::runtime::dynamic::handle_password_reset_request_action`] or an error when the operation fails.
3434///
3435/// # Errors
3436///
3437/// Returns an error value when [`crate::runtime::dynamic::handle_password_reset_request_action`] cannot complete successfully.
3438///
3439/// # Examples
3440///
3441/// ```rust,ignore
3442/// let _result = pharos_native::runtime::dynamic::handle_password_reset_request_action(todo!(), todo!());
3443/// ```
3444///
3445/// # Related
3446///
3447/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
3448pub fn handle_password_reset_request_action(
3449    _state_path: &Path,
3450    email: &str,
3451) -> Result<String, i32> {
3452    if !json_commands::generated_surface_text_is_email(email) {
3453        return Err(422);
3454    }
3455    random_secret_hex(32)
3456}
3457
3458/// Updates the password for a reset token in a persisted state document.
3459///
3460/// # Parameters
3461///
3462/// - `state_path`: Input consumed by [`crate::runtime::dynamic::handle_password_reset_complete_action`].
3463/// - `presented_token`: Input consumed by [`crate::runtime::dynamic::handle_password_reset_complete_action`].
3464/// - `password`: Input consumed by [`crate::runtime::dynamic::handle_password_reset_complete_action`].
3465/// - `now`: Input consumed by [`crate::runtime::dynamic::handle_password_reset_complete_action`].
3466///
3467/// # Returns
3468///
3469/// Returns the computed value produced by [`crate::runtime::dynamic::handle_password_reset_complete_action`].
3470///
3471/// # Performance
3472///
3473/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::handle_password_reset_complete_action`].
3474///
3475/// # Examples
3476///
3477/// ```rust,ignore
3478/// let _result = pharos_native::runtime::dynamic::handle_password_reset_complete_action(todo!(), todo!(), todo!(), todo!());
3479/// ```
3480///
3481/// # Related
3482///
3483/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
3484pub fn handle_password_reset_complete_action(
3485    state_path: &Path,
3486    presented_token: &str,
3487    password: &str,
3488    now: &str,
3489) -> i32 {
3490    if password.len() < 8 || presented_token.is_empty() {
3491        return 422;
3492    }
3493    let token_hash = hex_digest(&Sha256::digest(presented_token.as_bytes()));
3494    let Ok(mut root) = fs::read_to_string(state_path)
3495        .ok()
3496        .and_then(|text| serde_json::from_str::<Value>(&text).ok())
3497        .ok_or(66)
3498    else {
3499        return 66;
3500    };
3501    let user_id = root
3502        .get("password_resets")
3503        .and_then(Value::as_array)
3504        .and_then(|rows| {
3505            rows.iter().find(|row| {
3506                row.get("token_hash").and_then(Value::as_str) == Some(token_hash.as_str())
3507                    && row
3508                        .get("claimed_at")
3509                        .is_none_or(|value| value.is_null() || value.as_str() == Some(""))
3510                    && row
3511                        .get("expires_at")
3512                        .and_then(Value::as_str)
3513                        .is_some_and(|value| value > now)
3514            })
3515        })
3516        .and_then(|row| row.get("user_id"))
3517        .and_then(Value::as_i64)
3518        .unwrap_or(0);
3519    if user_id <= 0 {
3520        return 404;
3521    }
3522    let Ok(password_hash) = hash_password(password) else {
3523        return 69;
3524    };
3525    if let Some(reset) = root
3526        .get_mut("password_resets")
3527        .and_then(Value::as_array_mut)
3528        .and_then(|rows| {
3529            rows.iter_mut().find(|row| {
3530                row.get("token_hash").and_then(Value::as_str) == Some(token_hash.as_str())
3531            })
3532        })
3533    {
3534        reset["claimed_at"] = Value::String(now.to_owned());
3535    }
3536    if let Some(row) = root
3537        .get_mut("users")
3538        .and_then(Value::as_array_mut)
3539        .and_then(|rows| {
3540            rows.iter_mut()
3541                .find(|row| row.get("id").and_then(Value::as_i64) == Some(user_id))
3542        })
3543    {
3544        row.as_object_mut()
3545            .map(|row| row.insert("password_hash".to_owned(), Value::String(password_hash)));
3546    } else {
3547        return 404;
3548    }
3549    if let Some(rows) = root.get_mut("sessions").and_then(Value::as_array_mut) {
3550        for row in rows {
3551            if row.get("user_id").and_then(Value::as_i64) == Some(user_id)
3552                && row
3553                    .get("revoked_at")
3554                    .is_none_or(|value| value.is_null() || value.as_str() == Some(""))
3555                && let Some(object) = row.as_object_mut()
3556            {
3557                object.insert("revoked_at".to_owned(), Value::String(now.to_owned()));
3558            }
3559        }
3560    }
3561    super::fs::write_text_file(
3562        state_path,
3563        &serde_json::to_string(&root).unwrap_or_default(),
3564    )
3565}
3566
3567/// Returns a bounded numeric resource identifier from a job resource key.
3568///
3569/// # Parameters
3570///
3571/// - `resource_id`: Input consumed by [`crate::runtime::dynamic::resource_numeric_id_jobs_logic_local`].
3572///
3573/// # Returns
3574///
3575/// Returns the computed value produced by [`crate::runtime::dynamic::resource_numeric_id_jobs_logic_local`].
3576///
3577/// # Performance
3578///
3579/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::resource_numeric_id_jobs_logic_local`].
3580///
3581/// # Examples
3582///
3583/// ```rust,ignore
3584/// let _result = pharos_native::runtime::dynamic::resource_numeric_id_jobs_logic_local(todo!());
3585/// ```
3586///
3587/// # Related
3588///
3589/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
3590pub fn resource_numeric_id_jobs_logic_local(resource_id: &str) -> i64 {
3591    resource_id
3592        .rsplit([':', '/', '-'])
3593        .next()
3594        .and_then(|value| value.parse::<i64>().ok())
3595        .filter(|value| *value > 0)
3596        .unwrap_or(0)
3597}
3598
3599/// Builds a deterministic cancellation report without a shell or external process.
3600///
3601/// # Parameters
3602///
3603/// - `app`: Input consumed by [`crate::runtime::dynamic::dynamic_job_cancel_report_json`].
3604/// - `job_id`: Input consumed by [`crate::runtime::dynamic::dynamic_job_cancel_report_json`].
3605///
3606/// # Returns
3607///
3608/// Returns [`Some`] when [`crate::runtime::dynamic::dynamic_job_cancel_report_json`] can produce a value and [`None`] otherwise.
3609///
3610/// # Examples
3611///
3612/// ```rust,ignore
3613/// let _result = pharos_native::runtime::dynamic::dynamic_job_cancel_report_json(todo!(), todo!());
3614/// ```
3615///
3616/// # Related
3617///
3618/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
3619pub fn dynamic_job_cancel_report_json(app: &AppRuntime, job_id: i64) -> Option<String> {
3620    (job_id > 0).then(|| serde_json::json!({"report_id":"pharos-job-cancel-v1","app_id":app.app_id,"job_id":job_id,"status":"cancel_requested"}).to_string())
3621}
3622
3623/// Builds a deterministic retry report without a shell or external process.
3624///
3625/// # Parameters
3626///
3627/// - `app`: Input consumed by [`crate::runtime::dynamic::dynamic_job_retry_report_json`].
3628/// - `job_id`: Input consumed by [`crate::runtime::dynamic::dynamic_job_retry_report_json`].
3629///
3630/// # Returns
3631///
3632/// Returns [`Some`] when [`crate::runtime::dynamic::dynamic_job_retry_report_json`] can produce a value and [`None`] otherwise.
3633///
3634/// # Examples
3635///
3636/// ```rust,ignore
3637/// let _result = pharos_native::runtime::dynamic::dynamic_job_retry_report_json(todo!(), todo!());
3638/// ```
3639///
3640/// # Related
3641///
3642/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
3643pub fn dynamic_job_retry_report_json(app: &AppRuntime, job_id: i64) -> Option<String> {
3644    (job_id > 0).then(|| serde_json::json!({"report_id":"pharos-job-retry-v1","app_id":app.app_id,"job_id":job_id,"status":"retry_requested"}).to_string())
3645}
3646
3647/// Loads one app contract document from the active app root.
3648pub(super) fn dynamic_contract_doc(app: &AppRuntime, contract_key: &str) -> Option<Value> {
3649    let app_root = Path::new(app.app_root.as_deref()?);
3650    let manifest_path = if app_root.join("pharos.app.json").is_file() {
3651        app_root.join("pharos.app.json")
3652    } else {
3653        app_root.join("pharos.app.yml")
3654    };
3655    let manifest = json_commands::json_doc_load_file(&manifest_path)?;
3656    let relative = manifest
3657        .get("contracts")
3658        .and_then(Value::as_object)?
3659        .get(contract_key)
3660        .and_then(Value::as_str)?;
3661    if relative.is_empty() || relative.contains("..") || Path::new(relative).is_absolute() {
3662        return None;
3663    }
3664    json_commands::json_doc_load_file(&app_root.join(relative))
3665}
3666
3667/// Loads the active app integration contract from the app root.
3668///
3669/// # Parameters
3670///
3671/// - `app`: Input consumed by [`crate::runtime::dynamic::integration_contract_doc`].
3672///
3673/// # Returns
3674///
3675/// Returns [`Some`] when [`crate::runtime::dynamic::integration_contract_doc`] can produce a value and [`None`] otherwise.
3676///
3677/// # Examples
3678///
3679/// ```rust,ignore
3680/// let _result = pharos_native::runtime::dynamic::integration_contract_doc(todo!());
3681/// ```
3682///
3683/// # Related
3684///
3685/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
3686pub fn integration_contract_doc(app: &AppRuntime) -> Option<Value> {
3687    dynamic_contract_doc(app, "integration_contract")
3688}
3689
3690/// Finds one declarative action contract by id.
3691fn dynamic_find_action_contract<'a>(actions_root: &'a Value, action_id: &str) -> Option<&'a Value> {
3692    actions_root
3693        .get("actions")
3694        .and_then(Value::as_array)?
3695        .iter()
3696        .find(|action| action.get("action_id").and_then(Value::as_str) == Some(action_id))
3697}
3698
3699/// Finds one workflow transition contract by action id.
3700fn dynamic_find_workflow_transition<'a>(
3701    workflow_root: &'a Value,
3702    action_id: &str,
3703) -> Option<&'a Value> {
3704    workflow_root
3705        .get("workflows")
3706        .and_then(Value::as_array)?
3707        .iter()
3708        .find_map(|workflow| {
3709            workflow
3710                .get("transitions")
3711                .and_then(Value::as_array)?
3712                .iter()
3713                .find(|transition| {
3714                    transition.get("action_id").and_then(Value::as_str) == Some(action_id)
3715                })
3716        })
3717}
3718
3719/// Returns one mutable array section, creating it when absent.
3720fn dynamic_state_rows_mut<'a>(root: &'a mut Value, section: &str) -> Option<&'a mut Vec<Value>> {
3721    root.as_object_mut()?
3722        .entry(section.to_owned())
3723        .or_insert_with(|| Value::Array(Vec::new()))
3724        .as_array_mut()
3725}
3726
3727/// Returns the next global state id for domain records and advances the root counter.
3728fn dynamic_take_next_global_id(root: &mut Value) -> i64 {
3729    let next = root
3730        .get("next_id")
3731        .and_then(Value::as_i64)
3732        .filter(|value| *value > 0)
3733        .unwrap_or(1);
3734    if let Some(object) = root.as_object_mut() {
3735        object.insert("next_id".to_owned(), Value::from(next.saturating_add(1)));
3736    }
3737    next
3738}
3739
3740/// Returns the next per-section identifier without consuming the root domain record counter.
3741fn dynamic_take_next_section_id(root: &mut Value, section: &str) -> i64 {
3742    root.get(section)
3743        .and_then(Value::as_array)
3744        .into_iter()
3745        .flatten()
3746        .filter_map(|row| row.get("id").and_then(Value::as_i64))
3747        .max()
3748        .unwrap_or(0)
3749        .saturating_add(1)
3750}
3751
3752/// Reads one text-valued presentation field from a contract node.
3753fn dynamic_presentation_text(node: &Value, key: &str) -> String {
3754    node.get("presentation")
3755        .and_then(Value::as_object)
3756        .and_then(|presentation| presentation.get(key))
3757        .and_then(Value::as_str)
3758        .unwrap_or_default()
3759        .to_owned()
3760}
3761
3762/// Reads one text field from a workflow transition presentation node.
3763fn dynamic_transition_presentation_text(node: Option<&Value>, key: &str) -> String {
3764    node.and_then(|transition| transition.get("presentation"))
3765        .and_then(Value::as_object)
3766        .and_then(|presentation| presentation.get(key))
3767        .and_then(Value::as_str)
3768        .unwrap_or_default()
3769        .to_owned()
3770}
3771
3772/// Resolves one entity-field reference from the built app contract tree.
3773fn dynamic_resolve_ref_target(app: &AppRuntime, ref_text: &str) -> Option<Value> {
3774    let app_root = Path::new(app.app_root.as_deref()?);
3775    super::declarative::declarative_source_load_ref_target(app_root, ref_text)
3776}
3777
3778/// Percent-encodes one bounded form or query component.
3779fn dynamic_url_encode_component(value: &str) -> String {
3780    const HEX: &[u8; 16] = b"0123456789ABCDEF";
3781    let mut encoded = String::with_capacity(value.len());
3782    for byte in value.bytes() {
3783        if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
3784            encoded.push(char::from(byte));
3785        } else {
3786            encoded.push('%');
3787            encoded.push(char::from(HEX[usize::from(byte >> 4)]));
3788            encoded.push(char::from(HEX[usize::from(byte & 0x0f)]));
3789        }
3790    }
3791    encoded
3792}
3793
3794/// Builds one HTTP webhook payload for integration delivery.
3795fn dynamic_webhook_payload_json(adapter_id: &str, action_id: &str, resource_id: &str) -> String {
3796    serde_json::json!({
3797        "adapter_id": adapter_id,
3798        "action_id": action_id,
3799        "resource_id": resource_id,
3800    })
3801    .to_string()
3802}
3803
3804/// Builds one bounded form-encoded HTTP payload from a flat JSON object.
3805fn dynamic_http_form_payload(payload_json: &str) -> Option<String> {
3806    let payload = serde_json::from_str::<Value>(payload_json).ok()?;
3807    let object = payload.as_object()?;
3808    let mut encoded = String::new();
3809    for (index, (key, value)) in object.iter().enumerate() {
3810        if key.is_empty() {
3811            return None;
3812        }
3813        let value_text = match value {
3814            Value::Null => String::new(),
3815            Value::Bool(boolean) => boolean.to_string(),
3816            Value::Number(number) => number.to_string(),
3817            Value::String(text) => text.clone(),
3818            _ => return None,
3819        };
3820        if index > 0 {
3821            encoded.push('&');
3822        }
3823        encoded.push_str(&dynamic_url_encode_component(key));
3824        encoded.push('=');
3825        encoded.push_str(&dynamic_url_encode_component(&value_text));
3826    }
3827    Some(encoded)
3828}
3829
3830/// Resolves the outbound HTTP request body for one declared adapter kind.
3831fn dynamic_delivery_payload_body(adapter_kind: &str, payload_json: &str) -> Option<String> {
3832    match adapter_kind {
3833        "webhook" | "http_json" => Some(payload_json.to_owned()),
3834        "http_form" => dynamic_http_form_payload(payload_json),
3835        _ => None,
3836    }
3837}
3838
3839/// Sends one bounded, policy-validated webhook POST.
3840fn dynamic_http_post_status(
3841    url: &str,
3842    content_type: &str,
3843    payload_body: &str,
3844    extra_headers: &[(&str, String)],
3845    timeout_seconds: u64,
3846    max_response_bytes: usize,
3847    allow_plaintext_local: bool,
3848) -> Option<u16> {
3849    let timeout = std::time::Duration::from_secs(timeout_seconds.clamp(1, 300));
3850    pharos_runtime_integrations::webhook::post_status(
3851        url,
3852        content_type,
3853        payload_body,
3854        extra_headers,
3855        pharos_runtime_integrations::webhook::WebhookLimits {
3856            connect_timeout: timeout.min(std::time::Duration::from_secs(30)),
3857            operation_timeout: timeout,
3858            max_response_bytes: max_response_bytes.clamp(64, 16 * 1024 * 1024),
3859            allow_plaintext_local,
3860        },
3861    )
3862}
3863
3864/// Appends one workflow execution row and returns its id.
3865fn dynamic_append_workflow_execution(
3866    root: &mut Value,
3867    action_id: &str,
3868    transition: Option<&Value>,
3869) -> Option<i64> {
3870    let execution_id = dynamic_take_next_section_id(root, "workflow_executions");
3871    let workflow_id = transition
3872        .and_then(|value| value.get("action_id"))
3873        .and_then(Value::as_str)
3874        .unwrap_or(action_id)
3875        .to_owned();
3876    dynamic_state_rows_mut(root, "workflow_executions")?.push(serde_json::json!({
3877        "id": execution_id,
3878        "workflow_id": workflow_id,
3879        "transition_id": transition.and_then(|value| value.get("transition_id")).and_then(Value::as_str).unwrap_or_default(),
3880        "from_state": dynamic_transition_presentation_text(transition, "from_label"),
3881        "to_state": transition.and_then(|value| value.get("to_state")).and_then(Value::as_str).unwrap_or_default(),
3882        "execution_status": "applied",
3883        "queued_job_count": transition.and_then(|value| value.get("scheduled_jobs")).and_then(Value::as_array).map(|rows| rows.len()).unwrap_or(0),
3884        "created_at": now_iso_utc(),
3885        "completed_at": now_iso_utc(),
3886    }));
3887    Some(execution_id)
3888}
3889
3890/// Appends two minimal workflow events for the applied transition.
3891fn dynamic_append_workflow_events(
3892    root: &mut Value,
3893    workflow_execution_id: i64,
3894    transition: Option<&Value>,
3895) -> bool {
3896    let first_id = dynamic_take_next_section_id(root, "workflow_execution_events");
3897    let second_id = first_id.saturating_add(1);
3898    let Some(rows) = dynamic_state_rows_mut(root, "workflow_execution_events") else {
3899        return false;
3900    };
3901    rows.push(serde_json::json!({
3902        "id": first_id,
3903        "workflow_execution_id": workflow_execution_id,
3904        "event_kind": "action_applied",
3905        "event_key": transition.and_then(|value| value.get("action_id")).and_then(Value::as_str).unwrap_or_default(),
3906        "created_at": now_iso_utc(),
3907    }));
3908    rows.push(serde_json::json!({
3909        "id": second_id,
3910        "workflow_execution_id": workflow_execution_id,
3911        "event_kind": transition.and_then(|value| value.get("emits_event")).and_then(Value::as_str).unwrap_or("action_event"),
3912        "event_key": transition.and_then(|value| value.get("transition_id")).and_then(Value::as_str).unwrap_or_default(),
3913        "created_at": now_iso_utc(),
3914    }));
3915    true
3916}
3917
3918/// Appends one bounded workflow execution event row tied to a queued or processed job.
3919fn dynamic_append_workflow_execution_event(
3920    root: &mut Value,
3921    workflow_execution_id: i64,
3922    event_kind: &str,
3923    event_key: &str,
3924    payload: Value,
3925) -> bool {
3926    if workflow_execution_id <= 0
3927        || event_kind.is_empty()
3928        || event_kind.len() > 128
3929        || event_key.len() > 128
3930    {
3931        return false;
3932    }
3933    let event_id = dynamic_take_next_section_id(root, "workflow_execution_events");
3934    let Some(rows) = dynamic_state_rows_mut(root, "workflow_execution_events") else {
3935        return false;
3936    };
3937    rows.push(serde_json::json!({
3938        "id": event_id,
3939        "workflow_execution_id": workflow_execution_id,
3940        "event_kind": event_kind,
3941        "event_key": event_key,
3942        "payload_json": payload.to_string(),
3943        "created_at": now_iso_utc(),
3944    }));
3945    true
3946}
3947
3948/// Appends one durable queued scheduled-job execution for the background worker.
3949fn dynamic_append_scheduled_job(
3950    root: &mut Value,
3951    workflow_execution_id: i64,
3952    job_id: &str,
3953    queue_name: &str,
3954    delay_seconds: i64,
3955    max_attempts: i64,
3956    backoff_seconds: i64,
3957    lease_seconds: i64,
3958) -> bool {
3959    if job_id.is_empty() || queue_name.is_empty() || queue_name.len() > 128 {
3960        return false;
3961    }
3962    let now_epoch = current_epoch_seconds();
3963    let scheduled_epoch = now_epoch.saturating_add(delay_seconds.max(0) as u64);
3964    let execution_id = dynamic_take_next_section_id(root, "job_executions");
3965    let Some(job_rows) = dynamic_state_rows_mut(root, "job_executions") else {
3966        return false;
3967    };
3968    job_rows.push(serde_json::json!({
3969        "id": execution_id,
3970        "workflow_execution_id": workflow_execution_id,
3971        "queue_name": queue_name,
3972        "job_id": job_id,
3973        "handler_id": job_id,
3974        "execution_status": "queued",
3975        "attempt_count": 0,
3976        "max_attempts": max_attempts.clamp(1, 100),
3977        "backoff_seconds": backoff_seconds.max(0),
3978        "lease_seconds": lease_seconds.clamp(1, 3600),
3979        "created_at": now_epoch.to_string(),
3980        "scheduled_for": scheduled_epoch.to_string(),
3981        "available_at": scheduled_epoch.to_string(),
3982        "idempotency_key": format!("{workflow_execution_id}:{job_id}"),
3983    }));
3984    let _ = dynamic_append_workflow_execution_event(
3985        root,
3986        workflow_execution_id,
3987        "job_queued",
3988        job_id,
3989        serde_json::json!({
3990            "job_id": job_id,
3991            "queue_name": queue_name,
3992            "delay_seconds": delay_seconds.max(0),
3993            "available_at": scheduled_epoch.to_string(),
3994        }),
3995    );
3996    true
3997}
3998
3999/// Appends one local notification delivery row.
4000fn dynamic_append_notification(
4001    root: &mut Value,
4002    integrations_root: &Value,
4003    adapter_id: &str,
4004    channel: &str,
4005    recipient: &str,
4006    template_id: &str,
4007    template_params: &Value,
4008) -> bool {
4009    let adapter = integrations_root
4010        .get("adapters")
4011        .and_then(Value::as_array)
4012        .into_iter()
4013        .flatten()
4014        .find(|row| row.get("adapter_id").and_then(Value::as_str) == Some(adapter_id));
4015    let adapter_kind = adapter
4016        .and_then(|row| row.get("adapter_kind").and_then(Value::as_str))
4017        .unwrap_or_default();
4018    if adapter_id.is_empty()
4019        || recipient.is_empty()
4020        || template_id.is_empty()
4021        || !template_params.is_object()
4022        || !(adapter_kind == "notification_local" || adapter_kind == "notification_email")
4023    {
4024        return false;
4025    }
4026    let notification_id = dynamic_take_next_section_id(root, "notification_deliveries");
4027    let Some(rows) = dynamic_state_rows_mut(root, "notification_deliveries") else {
4028        return false;
4029    };
4030    rows.push(serde_json::json!({
4031        "id": notification_id,
4032        "adapter_id": adapter_id,
4033        "adapter_kind": adapter_kind,
4034        "delivery_kind": "action_notification",
4035        "channel": if channel.is_empty() { "email" } else { channel },
4036        "recipient": recipient,
4037        "template_id": template_id,
4038        "template_params": template_params,
4039        "delivery_status": "queued",
4040        "attempt_count": 0,
4041        "max_attempts": adapter.and_then(|row| row.get("max_attempts").and_then(Value::as_i64)).unwrap_or(if adapter_kind == "notification_email" { 2 } else { 1 }).clamp(1, 100),
4042        "backoff_seconds": adapter.and_then(|row| row.get("backoff_seconds").and_then(Value::as_i64)).unwrap_or(0).max(0),
4043        "relay": adapter.and_then(|row| row.get("relay").and_then(Value::as_str)).unwrap_or("primary"),
4044        "last_smtp_code": 0,
4045        "last_error": "",
4046        "next_attempt_at": "",
4047        "created_at": now_iso_utc(),
4048        "updated_at": now_iso_utc(),
4049        "completed_at": "",
4050    }));
4051    true
4052}
4053
4054/// Performs dynamic integration HTTP adapter kind.
4055fn dynamic_integration_http_adapter_kind(adapter_kind: &str) -> bool {
4056    matches!(adapter_kind, "webhook" | "http_json" | "http_form")
4057}
4058
4059/// Performs dynamic integration audit adapter kind.
4060fn dynamic_integration_audit_adapter_kind(adapter_kind: &str) -> bool {
4061    adapter_kind == "audit_local"
4062}
4063
4064/// Performs dynamic integration file adapter kind.
4065fn dynamic_integration_file_adapter_kind(adapter_kind: &str) -> bool {
4066    adapter_kind == "append_jsonl_local" || adapter_kind == "write_json_local"
4067}
4068
4069/// Performs dynamic delete records by scope primitive.
4070fn dynamic_delete_records_by_scope_primitive(
4071    root: &mut Value,
4072    request: &HttpRequest,
4073    input_overrides: &BTreeMap<String, Option<String>>,
4074    primitive: &Value,
4075    matched_record_count: &mut i64,
4076    skipped_record_count: &mut i64,
4077    removed_record_count: &mut i64,
4078) -> Result<(), i32> {
4079    let collection = primitive
4080        .get("collection")
4081        .and_then(Value::as_str)
4082        .ok_or(422)?;
4083    let scope_field = primitive
4084        .get("scope_match")
4085        .and_then(Value::as_object)
4086        .and_then(|map| map.get("field"))
4087        .and_then(Value::as_str)
4088        .ok_or(422)?;
4089    let scope_input = primitive
4090        .get("scope_match")
4091        .and_then(Value::as_object)
4092        .and_then(|map| map.get("from_input"))
4093        .and_then(Value::as_str)
4094        .ok_or(422)?;
4095    let wanted =
4096        dynamic_action_input_value(request, input_overrides, scope_input).unwrap_or_default();
4097    let max_records = primitive
4098        .get("max_records")
4099        .and_then(Value::as_i64)
4100        .unwrap_or(i64::MAX)
4101        .max(0) as usize;
4102    let overflow_policy = primitive
4103        .get("overflow_policy")
4104        .and_then(Value::as_str)
4105        .unwrap_or("truncate");
4106    let Some(rows) = dynamic_state_rows_mut(root, collection) else {
4107        return Err(500);
4108    };
4109    let matching_indexes = rows
4110        .iter()
4111        .enumerate()
4112        .filter(|(_, row)| row.get(scope_field).map(value_to_text).unwrap_or_default() == wanted)
4113        .map(|(index, _)| index)
4114        .collect::<Vec<_>>();
4115    *matched_record_count += matching_indexes.len() as i64;
4116    if matching_indexes.len() > max_records && overflow_policy == "fail" {
4117        return Err(422);
4118    }
4119    *skipped_record_count += matching_indexes.len().saturating_sub(max_records) as i64;
4120    let mut removed = 0usize;
4121    let mut keep = Vec::with_capacity(rows.len());
4122    for (index, row) in rows.iter().cloned().enumerate() {
4123        let selected = matching_indexes
4124            .iter()
4125            .take(max_records)
4126            .any(|candidate| *candidate == index);
4127        if selected {
4128            removed += 1;
4129        } else {
4130            keep.push(row);
4131        }
4132    }
4133    *rows = keep;
4134    *removed_record_count += removed as i64;
4135    Ok(())
4136}
4137
4138/// Finds one declared outbound adapter by adapter id.
4139fn dynamic_find_integration_adapter<'a>(
4140    integrations_root: &'a Value,
4141    adapter_id: &str,
4142) -> Option<&'a Value> {
4143    integrations_root
4144        .get("adapters")
4145        .and_then(Value::as_array)?
4146        .iter()
4147        .find(|row| row.get("adapter_id").and_then(Value::as_str) == Some(adapter_id))
4148}
4149
4150/// Performs dynamic state rows.
4151fn dynamic_state_rows<'a>(root: &'a Value, section: &str) -> &'a [Value] {
4152    root.get(section)
4153        .and_then(Value::as_array)
4154        .map(Vec::as_slice)
4155        .unwrap_or(&[])
4156}
4157
4158/// Performs dynamic row i64.
4159fn dynamic_row_i64(row: &Value, field: &str) -> i64 {
4160    row.get(field)
4161        .and_then(Value::as_i64)
4162        .or_else(|| {
4163            row.get(field)
4164                .and_then(Value::as_str)
4165                .and_then(|value| value.parse::<i64>().ok())
4166        })
4167        .unwrap_or(0)
4168}
4169
4170/// Resolves one bounded notification parameter value from action input or runtime context.
4171fn dynamic_notification_parameter_value(
4172    spec: &Value,
4173    request: &HttpRequest,
4174    input_overrides: &BTreeMap<String, Option<String>>,
4175    action_id: &str,
4176    entity_id: &str,
4177    event_kind: &str,
4178    recipient: &str,
4179    template_id: &str,
4180    anchor_record_id: i64,
4181) -> Option<Value> {
4182    let source = spec
4183        .get("source")
4184        .and_then(Value::as_str)
4185        .unwrap_or("literal");
4186    match source {
4187        "literal" => spec.get("value").cloned(),
4188        "input" => spec.get("field").and_then(Value::as_str).map(|field| {
4189            Value::String(
4190                dynamic_action_input_value(request, input_overrides, field).unwrap_or_default(),
4191            )
4192        }),
4193        "context_anchor_id" => Some(Value::from(anchor_record_id.max(0))),
4194        "action_id" => Some(Value::String(action_id.to_owned())),
4195        "entity_id" => Some(Value::String(entity_id.to_owned())),
4196        "event_kind" => Some(Value::String(event_kind.to_owned())),
4197        "recipient" => Some(Value::String(recipient.to_owned())),
4198        "template_id" => Some(Value::String(template_id.to_owned())),
4199        _ => None,
4200    }
4201}
4202
4203/// Builds the flat template-parameter map for one declarative notification row.
4204fn dynamic_notification_template_params(
4205    notification: &Value,
4206    request: &HttpRequest,
4207    input_overrides: &BTreeMap<String, Option<String>>,
4208    action_id: &str,
4209    entity_id: &str,
4210    event_kind: &str,
4211    recipient: &str,
4212    template_id: &str,
4213    anchor_record_id: i64,
4214) -> Value {
4215    let mut params = Map::new();
4216    params.insert("action_id".to_owned(), Value::String(action_id.to_owned()));
4217    params.insert("entity_id".to_owned(), Value::String(entity_id.to_owned()));
4218    params.insert(
4219        "event_kind".to_owned(),
4220        Value::String(event_kind.to_owned()),
4221    );
4222    params.insert("recipient".to_owned(), Value::String(recipient.to_owned()));
4223    params.insert(
4224        "template_id".to_owned(),
4225        Value::String(template_id.to_owned()),
4226    );
4227    params.insert(
4228        "anchor_record_id".to_owned(),
4229        Value::from(anchor_record_id.max(0)),
4230    );
4231    if let Some(explicit) = notification.get("params").and_then(Value::as_object) {
4232        for (key, spec) in explicit {
4233            if key.is_empty() || key.len() > 128 {
4234                continue;
4235            }
4236            if let Some(value) = dynamic_notification_parameter_value(
4237                spec,
4238                request,
4239                input_overrides,
4240                action_id,
4241                entity_id,
4242                event_kind,
4243                recipient,
4244                template_id,
4245                anchor_record_id,
4246            ) {
4247                params.insert(key.to_owned(), value);
4248            }
4249        }
4250    }
4251    Value::Object(params)
4252}
4253
4254/// Performs dynamic notification template ID from row.
4255fn dynamic_notification_template_id_from_row(row: &Value) -> Option<&str> {
4256    if let Some(template_id) = row.get("template_id").and_then(Value::as_str) {
4257        return (!template_id.is_empty()).then_some(template_id);
4258    }
4259    match row
4260        .get("delivery_kind")
4261        .and_then(Value::as_str)
4262        .unwrap_or_default()
4263    {
4264        "business_invite" => Some("business_invite"),
4265        "password_reset" => Some("password_reset"),
4266        _ => None,
4267    }
4268}
4269
4270/// Performs dynamic notification origin.
4271fn dynamic_notification_origin(app: &AppRuntime) -> Option<String> {
4272    let app_root = Path::new(app.app_root.as_deref()?);
4273    let runtime_conf = app.runtime_conf_path.as_deref().map(Path::new);
4274    json_commands::dynamic_config_value(app_root, runtime_conf, "base_url")
4275        .and_then(|value| normalized_public_origin(&value))
4276}
4277
4278/// Performs dynamic notification absolute URL.
4279fn dynamic_notification_absolute_url(app: &AppRuntime, relative_path: &str) -> String {
4280    let scoped_relative = format!(
4281        "{}{}",
4282        app.base_path
4283            .as_deref()
4284            .filter(|value| !value.is_empty() && *value != "/")
4285            .unwrap_or(""),
4286        relative_path
4287    );
4288    dynamic_notification_origin(app)
4289        .map(|origin| format!("{origin}{scoped_relative}"))
4290        .unwrap_or(scoped_relative)
4291}
4292
4293/// Performs dynamic framework notification params.
4294fn dynamic_framework_notification_params(app: &AppRuntime, root: &Value, row: &Value) -> Value {
4295    let mut params = Map::new();
4296    params.insert(
4297        "app_id".to_owned(),
4298        Value::String(app.app_id.clone().unwrap_or_default()),
4299    );
4300    let recipient = row
4301        .get("recipient")
4302        .and_then(Value::as_str)
4303        .unwrap_or_default();
4304    params.insert("recipient".to_owned(), Value::String(recipient.to_owned()));
4305    match row
4306        .get("delivery_kind")
4307        .and_then(Value::as_str)
4308        .unwrap_or_default()
4309    {
4310        "business_invite" => {
4311            let secret = row
4312                .get("secret")
4313                .and_then(Value::as_str)
4314                .unwrap_or_default();
4315            params.insert(
4316                "invite_url".to_owned(),
4317                Value::String(dynamic_notification_absolute_url(
4318                    app,
4319                    &format!("/accounts/invite/{secret}/"),
4320                )),
4321            );
4322            let token_hash = hex_digest(&Sha256::digest(secret.as_bytes()));
4323            if let Some(invite) =
4324                dynamic_state_rows(root, "business_invites")
4325                    .iter()
4326                    .find(|invite| {
4327                        invite.get("token_hash").and_then(Value::as_str)
4328                            == Some(token_hash.as_str())
4329                    })
4330            {
4331                let business_id = dynamic_row_i64(invite, "business_id");
4332                let business_name = dynamic_state_rows(root, "businesses")
4333                    .iter()
4334                    .find(|business| dynamic_row_i64(business, "id") == business_id)
4335                    .and_then(|business| business.get("name").and_then(Value::as_str))
4336                    .unwrap_or("the business");
4337                let account_type = invite
4338                    .get("account_type")
4339                    .and_then(Value::as_str)
4340                    .map(super::business::dynamic_invite_account_type_label)
4341                    .unwrap_or("member");
4342                params.insert(
4343                    "business_name".to_owned(),
4344                    Value::String(business_name.to_owned()),
4345                );
4346                params.insert(
4347                    "account_type".to_owned(),
4348                    Value::String(account_type.to_owned()),
4349                );
4350            }
4351        }
4352        "password_reset" => {
4353            let secret = row
4354                .get("secret")
4355                .and_then(Value::as_str)
4356                .unwrap_or_default();
4357            let token_hash = hex_digest(&Sha256::digest(secret.as_bytes()));
4358            let password_reset_user_id = dynamic_state_rows(root, "password_resets")
4359                .iter()
4360                .find(|reset| {
4361                    reset.get("token_hash").and_then(Value::as_str) == Some(token_hash.as_str())
4362                })
4363                .map(|reset| dynamic_row_i64(reset, "user_id"))
4364                .unwrap_or(0);
4365            params.insert(
4366                "reset_url".to_owned(),
4367                Value::String(dynamic_notification_absolute_url(
4368                    app,
4369                    &format!("/accounts/password-reset/complete/{secret}/"),
4370                )),
4371            );
4372            let full_name = dynamic_state_rows(root, "users")
4373                .iter()
4374                .find(|user| {
4375                    user.get("email").and_then(Value::as_str) == Some(recipient)
4376                        || dynamic_row_i64(user, "id") == password_reset_user_id
4377                })
4378                .and_then(|user| {
4379                    user.get("display_name")
4380                        .and_then(Value::as_str)
4381                        .filter(|value| !value.is_empty())
4382                        .or_else(|| user.get("username").and_then(Value::as_str))
4383                })
4384                .unwrap_or("there");
4385            params.insert("full_name".to_owned(), Value::String(full_name.to_owned()));
4386        }
4387        _ => {}
4388    }
4389    Value::Object(params)
4390}
4391
4392/// Performs dynamic update notification delivery.
4393fn dynamic_update_notification_delivery(
4394    delivery: &mut Value,
4395    attempt_number: i64,
4396    status: &str,
4397    last_error: &str,
4398    next_attempt_at: Option<u64>,
4399    last_smtp_code: i64,
4400    completed: bool,
4401    rendered_subject: Option<&str>,
4402    rendered_body: Option<&str>,
4403) {
4404    let now = now_iso_utc();
4405    if let Some(object) = delivery.as_object_mut() {
4406        object.insert(
4407            "attempt_count".to_owned(),
4408            Value::from(attempt_number.max(0)),
4409        );
4410        object.insert(
4411            "delivery_status".to_owned(),
4412            Value::String(status.to_owned()),
4413        );
4414        object.insert("updated_at".to_owned(), Value::String(now.clone()));
4415        object.insert(
4416            "last_error".to_owned(),
4417            Value::String(last_error.to_owned()),
4418        );
4419        object.insert(
4420            "last_smtp_code".to_owned(),
4421            Value::from(last_smtp_code.max(0)),
4422        );
4423        object.insert(
4424            "next_attempt_at".to_owned(),
4425            Value::String(
4426                next_attempt_at
4427                    .map(|value| value.to_string())
4428                    .unwrap_or_default(),
4429            ),
4430        );
4431        if completed {
4432            object.insert("completed_at".to_owned(), Value::String(now));
4433        }
4434        if let Some(subject) = rendered_subject {
4435            object.insert(
4436                "rendered_subject".to_owned(),
4437                Value::String(subject.to_owned()),
4438            );
4439        }
4440        if let Some(body) = rendered_body {
4441            object.insert("rendered_body".to_owned(), Value::String(body.to_owned()));
4442        }
4443    }
4444}
4445
4446/// Processes one due queued or retryable notification delivery in a worker-owned state transaction.
4447///
4448/// # Parameters
4449///
4450/// - `app`: Input consumed by [`crate::runtime::dynamic::process_one_due_notification_delivery`].
4451/// - `root`: Input consumed by [`crate::runtime::dynamic::process_one_due_notification_delivery`].
4452/// - `integrations_root`: Input consumed by [`crate::runtime::dynamic::process_one_due_notification_delivery`].
4453///
4454/// # Returns
4455///
4456/// Returns `true` when the condition checked by [`crate::runtime::dynamic::process_one_due_notification_delivery`] holds and `false` otherwise.
4457///
4458/// # Performance
4459///
4460/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::process_one_due_notification_delivery`].
4461///
4462/// # Examples
4463///
4464/// ```rust,ignore
4465/// let _result = pharos_native::runtime::dynamic::process_one_due_notification_delivery(todo!(), std::path::Path::new("."), todo!());
4466/// ```
4467///
4468/// # Related
4469///
4470/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
4471pub fn process_one_due_notification_delivery(
4472    app: &AppRuntime,
4473    root: &mut Value,
4474    integrations_root: &Value,
4475) -> bool {
4476    let now = current_epoch_seconds();
4477    let delivery_id = root
4478        .get("notification_deliveries")
4479        .and_then(Value::as_array)
4480        .and_then(|rows| {
4481            rows.iter().find(|row| {
4482                row.get("delivery_status").and_then(Value::as_str) == Some("queued")
4483                    || (row.get("delivery_status").and_then(Value::as_str) == Some("retryable")
4484                        && row
4485                            .get("next_attempt_at")
4486                            .and_then(Value::as_str)
4487                            .and_then(|value| value.parse::<u64>().ok())
4488                            .is_some_and(|available| available <= now))
4489            })
4490        })
4491        .and_then(|row| row.get("id").and_then(Value::as_i64));
4492    let Some(delivery_id) = delivery_id else {
4493        return false;
4494    };
4495    let delivery_snapshot = root
4496        .get("notification_deliveries")
4497        .and_then(Value::as_array)
4498        .and_then(|rows| {
4499            rows.iter()
4500                .find(|row| row.get("id").and_then(Value::as_i64) == Some(delivery_id))
4501        })
4502        .cloned();
4503    let framework_params = delivery_snapshot
4504        .as_ref()
4505        .map(|row| dynamic_framework_notification_params(app, root, row));
4506    let Some(rows) = root
4507        .get_mut("notification_deliveries")
4508        .and_then(Value::as_array_mut)
4509    else {
4510        return false;
4511    };
4512    let Some(delivery) = rows
4513        .iter_mut()
4514        .find(|row| row.get("id").and_then(Value::as_i64) == Some(delivery_id))
4515    else {
4516        return false;
4517    };
4518    let adapter_id = delivery
4519        .get("adapter_id")
4520        .and_then(Value::as_str)
4521        .unwrap_or_default();
4522    let adapter_kind = delivery
4523        .get("adapter_kind")
4524        .and_then(Value::as_str)
4525        .filter(|value| !value.is_empty())
4526        .map(ToOwned::to_owned)
4527        .or_else(|| {
4528            dynamic_find_integration_adapter(integrations_root, adapter_id)
4529                .and_then(|adapter| adapter.get("adapter_kind").and_then(Value::as_str))
4530                .map(ToOwned::to_owned)
4531        })
4532        .unwrap_or_else(|| {
4533            if delivery
4534                .get("delivery_kind")
4535                .and_then(Value::as_str)
4536                .is_some()
4537            {
4538                "notification_email".to_owned()
4539            } else {
4540                "notification_local".to_owned()
4541            }
4542        });
4543    let recipient = delivery
4544        .get("recipient")
4545        .and_then(Value::as_str)
4546        .unwrap_or_default()
4547        .to_owned();
4548    let template_id = dynamic_notification_template_id_from_row(delivery)
4549        .unwrap_or_default()
4550        .to_owned();
4551    let params = delivery
4552        .get("template_params")
4553        .filter(|value| value.is_object())
4554        .cloned()
4555        .or(framework_params)
4556        .unwrap_or_else(|| serde_json::json!({}));
4557    let attempt_number = delivery
4558        .get("attempt_count")
4559        .and_then(Value::as_i64)
4560        .unwrap_or(0)
4561        .saturating_add(1);
4562    let max_attempts = delivery
4563        .get("max_attempts")
4564        .and_then(Value::as_i64)
4565        .unwrap_or(if adapter_kind == "notification_email" {
4566            2
4567        } else {
4568            1
4569        })
4570        .max(1);
4571    let backoff_seconds = delivery
4572        .get("backoff_seconds")
4573        .and_then(Value::as_i64)
4574        .unwrap_or(0)
4575        .max(0) as u64;
4576    if recipient.is_empty() || template_id.is_empty() {
4577        dynamic_update_notification_delivery(
4578            delivery,
4579            attempt_number,
4580            "failed",
4581            "notification delivery is incomplete",
4582            None,
4583            0,
4584            true,
4585            None,
4586            None,
4587        );
4588        return true;
4589    }
4590    let Some(message) =
4591        super::business::load_notification_email_message(app, &recipient, &template_id, &params)
4592    else {
4593        dynamic_update_notification_delivery(
4594            delivery,
4595            attempt_number,
4596            "failed",
4597            "notification template is unavailable",
4598            None,
4599            0,
4600            true,
4601            None,
4602            None,
4603        );
4604        return true;
4605    };
4606    match adapter_kind.as_str() {
4607        "notification_local" => {
4608            dynamic_update_notification_delivery(
4609                delivery,
4610                attempt_number,
4611                "succeeded",
4612                "",
4613                None,
4614                0,
4615                true,
4616                Some(&message.subject),
4617                Some(&message.text_body),
4618            );
4619            true
4620        }
4621        "notification_email" => {
4622            let relay_prefix = if delivery.get("relay").and_then(Value::as_str) == Some("secondary")
4623            {
4624                Some("smtp2_")
4625            } else {
4626                Some("smtp_")
4627            };
4628            let config = super::business::email_load_relay_config(
4629                Path::new(app.app_root.as_deref().unwrap_or_default()),
4630                app.runtime_conf_path.as_deref().map(Path::new),
4631                relay_prefix,
4632            );
4633            let Ok(config) = config else {
4634                dynamic_update_notification_delivery(
4635                    delivery,
4636                    attempt_number,
4637                    "skipped",
4638                    "smtp configuration unavailable",
4639                    None,
4640                    0,
4641                    true,
4642                    Some(&message.subject),
4643                    Some(&message.text_body),
4644                );
4645                return true;
4646            };
4647            let mut result = super::business::EmailResult::default();
4648            let _ = super::business::email_send_text(&config, &message, &mut result);
4649            if result.delivered {
4650                dynamic_update_notification_delivery(
4651                    delivery,
4652                    attempt_number,
4653                    "succeeded",
4654                    "",
4655                    None,
4656                    result.smtp_code,
4657                    true,
4658                    Some(&message.subject),
4659                    Some(&message.text_body),
4660                );
4661            } else if result.retryable && attempt_number < max_attempts {
4662                dynamic_update_notification_delivery(
4663                    delivery,
4664                    attempt_number,
4665                    "retryable",
4666                    result
4667                        .error_message
4668                        .as_deref()
4669                        .unwrap_or("smtp delivery failed"),
4670                    Some(now.saturating_add(backoff_seconds.max(1))),
4671                    result.smtp_code,
4672                    false,
4673                    Some(&message.subject),
4674                    Some(&message.text_body),
4675                );
4676            } else {
4677                let status = if result.skipped { "skipped" } else { "failed" };
4678                dynamic_update_notification_delivery(
4679                    delivery,
4680                    attempt_number,
4681                    status,
4682                    result
4683                        .error_message
4684                        .as_deref()
4685                        .unwrap_or("smtp delivery failed"),
4686                    None,
4687                    result.smtp_code,
4688                    true,
4689                    Some(&message.subject),
4690                    Some(&message.text_body),
4691                );
4692            }
4693            true
4694        }
4695        _ => {
4696            dynamic_update_notification_delivery(
4697                delivery,
4698                attempt_number,
4699                "failed",
4700                "unsupported notification adapter kind",
4701                None,
4702                0,
4703                true,
4704                None,
4705                None,
4706            );
4707            true
4708        }
4709    }
4710}
4711
4712/// Appends one webhook delivery row for the durable background worker.
4713fn dynamic_append_webhook_delivery(
4714    root: &mut Value,
4715    integrations_root: &Value,
4716    adapter_id: &str,
4717    action_id: &str,
4718    resource_id: &str,
4719) -> Option<()> {
4720    let adapter = integrations_root
4721        .get("adapters")
4722        .and_then(Value::as_array)?
4723        .iter()
4724        .find(|row| row.get("adapter_id").and_then(Value::as_str) == Some(adapter_id))?;
4725    let adapter_kind = adapter
4726        .get("adapter_kind")
4727        .and_then(Value::as_str)
4728        .unwrap_or_default();
4729    if !dynamic_integration_http_adapter_kind(adapter_kind) {
4730        return None;
4731    }
4732    let target_url = adapter
4733        .get("target_url")
4734        .and_then(Value::as_str)
4735        .filter(|value| !value.is_empty())
4736        .map(ToOwned::to_owned)
4737        .or_else(|| {
4738            adapter
4739                .get("target_url_env")
4740                .and_then(Value::as_str)
4741                .and_then(|name| env::var(name).ok())
4742                .filter(|value| !value.is_empty())
4743        })
4744        .unwrap_or_default();
4745    let content_type = adapter
4746        .get("content_type")
4747        .and_then(Value::as_str)
4748        .unwrap_or("application/json");
4749    let payload_json = dynamic_webhook_payload_json(adapter_id, action_id, resource_id);
4750    let delivery_id = dynamic_take_next_section_id(root, "integration_deliveries");
4751    dynamic_state_rows_mut(root, "integration_deliveries")?.push(serde_json::json!({
4752        "id": delivery_id,
4753        "adapter_id": adapter_id,
4754        "adapter_kind": adapter_kind,
4755        "event_kind": action_id,
4756        "resource_id": resource_id,
4757        "subject_id": "",
4758        "tenant_id": "",
4759        "target_url": target_url,
4760        "content_type": content_type,
4761        "payload_json": payload_json,
4762        "event_header": adapter.get("event_header").and_then(Value::as_str).unwrap_or_default(),
4763        "auth_header": adapter.get("auth_header").and_then(Value::as_str).unwrap_or_default(),
4764        "auth_scheme": adapter.get("auth_scheme").and_then(Value::as_str).unwrap_or_default(),
4765        "auth_token_env": adapter.get("auth_token_env").and_then(Value::as_str).unwrap_or_default(),
4766        "delivery_status": "queued",
4767        "attempt_count": 0,
4768        "max_attempts": adapter.get("max_attempts").and_then(Value::as_i64).unwrap_or(1),
4769        "backoff_seconds": adapter.get("backoff_seconds").and_then(Value::as_i64).unwrap_or(0),
4770        "timeout_seconds": adapter.get("timeout_seconds").and_then(Value::as_i64).unwrap_or(0),
4771        "max_response_bytes": adapter.get("max_response_bytes").and_then(Value::as_i64).unwrap_or(8192),
4772        "allow_plaintext_local": adapter.get("allow_plaintext_local").and_then(Value::as_bool).unwrap_or(false),
4773        "last_http_status": 0,
4774        "last_error": "",
4775        "next_attempt_at": "",
4776        "completed_at": "",
4777        "created_at": now_iso_utc(),
4778        "updated_at": now_iso_utc(),
4779    }));
4780    Some(())
4781}
4782
4783/// Processes one queued or retryable webhook delivery in place.
4784fn dynamic_process_webhook_delivery(root: &mut Value, delivery_id: i64) -> bool {
4785    let Some(delivery) = dynamic_state_rows_mut(root, "integration_deliveries").and_then(|rows| {
4786        rows.iter_mut()
4787            .find(|row| row.get("id").and_then(Value::as_i64) == Some(delivery_id))
4788    }) else {
4789        return false;
4790    };
4791    let target_url = delivery
4792        .get("target_url")
4793        .and_then(Value::as_str)
4794        .unwrap_or_default()
4795        .to_owned();
4796    let content_type = delivery
4797        .get("content_type")
4798        .and_then(Value::as_str)
4799        .unwrap_or("application/json")
4800        .to_owned();
4801    let event_kind = delivery
4802        .get("event_kind")
4803        .and_then(Value::as_str)
4804        .unwrap_or_default()
4805        .to_owned();
4806    let adapter_kind = delivery
4807        .get("adapter_kind")
4808        .and_then(Value::as_str)
4809        .unwrap_or("webhook")
4810        .to_owned();
4811    let payload_json = delivery
4812        .get("payload_json")
4813        .map(|payload| {
4814            payload.as_str().map(ToOwned::to_owned).unwrap_or_else(|| {
4815                serde_json::to_string(payload).unwrap_or_else(|_| "{}".to_owned())
4816            })
4817        })
4818        .unwrap_or_else(|| "{}".to_owned());
4819    let attempt_number = delivery
4820        .get("attempt_count")
4821        .and_then(Value::as_i64)
4822        .unwrap_or(0)
4823        .saturating_add(1);
4824    let max_attempts = delivery
4825        .get("max_attempts")
4826        .and_then(Value::as_i64)
4827        .unwrap_or(1)
4828        .max(1);
4829    let backoff_seconds = delivery
4830        .get("backoff_seconds")
4831        .and_then(Value::as_i64)
4832        .unwrap_or(0)
4833        .max(0);
4834    let now = now_iso_utc();
4835    let timestamp = now_iso_utc();
4836    let delivery_id_text = delivery_id.to_string();
4837    let signature = delivery
4838        .get("signature_secret_env")
4839        .and_then(Value::as_str)
4840        .and_then(|secret_env| {
4841            webhook_signature_header_value_dup_logic_local(secret_env, &timestamp, &payload_json)
4842        });
4843    let signature_header = delivery
4844        .get("signature_header")
4845        .and_then(Value::as_str)
4846        .unwrap_or("X-Pharos-Signature")
4847        .to_owned();
4848    let timestamp_header = delivery
4849        .get("timestamp_header")
4850        .and_then(Value::as_str)
4851        .unwrap_or("X-Pharos-Timestamp")
4852        .to_owned();
4853    let mut headers = vec![
4854        ("X-Pharos-Delivery-Id", delivery_id_text),
4855        (timestamp_header.as_str(), timestamp.clone()),
4856    ];
4857    let event_header = delivery
4858        .get("event_header")
4859        .and_then(Value::as_str)
4860        .unwrap_or_default()
4861        .to_owned();
4862    if !event_header.is_empty() && !event_kind.is_empty() {
4863        headers.push((event_header.as_str(), event_kind.clone()));
4864    }
4865    let auth_header = delivery
4866        .get("auth_header")
4867        .and_then(Value::as_str)
4868        .filter(|value| !value.is_empty())
4869        .unwrap_or("Authorization")
4870        .to_owned();
4871    let auth_token_env = delivery
4872        .get("auth_token_env")
4873        .and_then(Value::as_str)
4874        .unwrap_or_default()
4875        .to_owned();
4876    let auth_scheme = delivery
4877        .get("auth_scheme")
4878        .and_then(Value::as_str)
4879        .unwrap_or("Bearer")
4880        .to_owned();
4881    let auth_required = !auth_token_env.is_empty();
4882    let auth_header_value = (!auth_token_env.is_empty())
4883        .then(|| env::var(&auth_token_env).ok())
4884        .flatten()
4885        .filter(|value| !value.is_empty())
4886        .map(|token| {
4887            if auth_scheme.is_empty() {
4888                token
4889            } else {
4890                format!("{auth_scheme} {token}")
4891            }
4892        });
4893    if let Some(auth_value) = auth_header_value.as_ref() {
4894        headers.push((auth_header.as_str(), auth_value.clone()));
4895    }
4896    if let Some(signature_value) = signature.as_ref() {
4897        headers.push((signature_header.as_str(), signature_value.clone()));
4898    }
4899    let signature_required = delivery
4900        .get("signature_secret_env")
4901        .and_then(Value::as_str)
4902        .is_some_and(|value| !value.is_empty());
4903    let payload_body = dynamic_delivery_payload_body(&adapter_kind, &payload_json);
4904    let delivery_error = if !dynamic_integration_http_adapter_kind(&adapter_kind) {
4905        "unsupported integration adapter kind"
4906    } else if payload_body.is_none() {
4907        "integration adapter payload is incompatible"
4908    } else if auth_required && auth_header_value.is_none() {
4909        "integration adapter auth unavailable"
4910    } else if signature_required && signature.is_none() {
4911        "webhook signature secret unavailable"
4912    } else {
4913        ""
4914    };
4915    let status_code = if !delivery_error.is_empty() {
4916        None
4917    } else {
4918        dynamic_http_post_status(
4919            &target_url,
4920            &content_type,
4921            payload_body.as_deref().unwrap_or_default(),
4922            &headers,
4923            delivery
4924                .get("timeout_seconds")
4925                .and_then(Value::as_u64)
4926                .unwrap_or(15),
4927            delivery
4928                .get("max_response_bytes")
4929                .and_then(Value::as_u64)
4930                .unwrap_or(8 * 1024) as usize,
4931            delivery
4932                .get("allow_plaintext_local")
4933                .and_then(Value::as_bool)
4934                .unwrap_or(false),
4935        )
4936    };
4937    let success = status_code.is_some_and(|status| (200..300).contains(&status));
4938    if let Some(object) = delivery.as_object_mut() {
4939        object.insert("attempt_count".to_owned(), Value::from(attempt_number));
4940        object.insert("updated_at".to_owned(), Value::String(now.clone()));
4941        object.insert(
4942            "last_http_status".to_owned(),
4943            Value::from(status_code.unwrap_or(0) as i64),
4944        );
4945        if success {
4946            object.insert(
4947                "delivery_status".to_owned(),
4948                Value::String("succeeded".to_owned()),
4949            );
4950            object.insert("completed_at".to_owned(), Value::String(now.clone()));
4951            object.insert("next_attempt_at".to_owned(), Value::String(String::new()));
4952            object.insert("last_error".to_owned(), Value::String(String::new()));
4953        } else if attempt_number < max_attempts {
4954            object.insert(
4955                "delivery_status".to_owned(),
4956                Value::String("retryable".to_owned()),
4957            );
4958            object.insert(
4959                "next_attempt_at".to_owned(),
4960                Value::String((now.parse::<i64>().unwrap_or(0) + backoff_seconds).to_string()),
4961            );
4962            object.insert(
4963                "last_error".to_owned(),
4964                Value::String(if delivery_error.is_empty() {
4965                    "outbound webhook transport unavailable".to_owned()
4966                } else {
4967                    delivery_error.to_owned()
4968                }),
4969            );
4970        } else {
4971            object.insert(
4972                "delivery_status".to_owned(),
4973                Value::String("failed".to_owned()),
4974            );
4975            object.insert("next_attempt_at".to_owned(), Value::String(String::new()));
4976            object.insert(
4977                "last_error".to_owned(),
4978                Value::String(if delivery_error.is_empty() {
4979                    "outbound webhook transport unavailable".to_owned()
4980                } else {
4981                    delivery_error.to_owned()
4982                }),
4983            );
4984        }
4985    }
4986    let attempt_id = dynamic_take_next_section_id(root, "integration_delivery_attempts");
4987    if let Some(rows) = dynamic_state_rows_mut(root, "integration_delivery_attempts") {
4988        rows.push(serde_json::json!({
4989            "id": attempt_id,
4990            "delivery_id": delivery_id,
4991            "attempt_status": if success { "succeeded" } else { "failed" },
4992            "http_status": status_code.unwrap_or(0),
4993            "error_message": if success {
4994                ""
4995            } else if delivery_error.is_empty() {
4996                "outbound webhook transport unavailable"
4997            } else {
4998                delivery_error
4999            },
5000            "started_at": now,
5001            "finished_at": now_iso_utc(),
5002        }));
5003    }
5004    success
5005}
5006
5007/// Queues one declaratively configured webhook delivery for background execution.
5008///
5009/// # Parameters
5010///
5011/// - `root`: Input consumed by [`crate::runtime::dynamic::deliver_integration_webhook`].
5012/// - `integrations_root`: Input consumed by [`crate::runtime::dynamic::deliver_integration_webhook`].
5013/// - `adapter_id`: Input consumed by [`crate::runtime::dynamic::deliver_integration_webhook`].
5014/// - `event_kind`: Input consumed by [`crate::runtime::dynamic::deliver_integration_webhook`].
5015/// - `resource_id`: Input consumed by [`crate::runtime::dynamic::deliver_integration_webhook`].
5016/// - `tenant_id`: Input consumed by [`crate::runtime::dynamic::deliver_integration_webhook`].
5017/// - `subject_id`: Input consumed by [`crate::runtime::dynamic::deliver_integration_webhook`].
5018/// - `payload_json`: Input consumed by [`crate::runtime::dynamic::deliver_integration_webhook`].
5019///
5020/// # Returns
5021///
5022/// Returns [`Some`] when [`crate::runtime::dynamic::deliver_integration_webhook`] can produce a value and [`None`] otherwise.
5023///
5024/// # Performance
5025///
5026/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::deliver_integration_webhook`].
5027///
5028/// # Examples
5029///
5030/// ```rust,ignore
5031/// let _result = pharos_native::runtime::dynamic::deliver_integration_webhook(std::path::Path::new("."), todo!(), todo!(), todo!(), todo!(), todo!(), todo!(), todo!());
5032/// ```
5033///
5034/// # Related
5035///
5036/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
5037pub fn deliver_integration_webhook(
5038    root: &mut Value,
5039    integrations_root: &Value,
5040    adapter_id: &str,
5041    event_kind: Option<&str>,
5042    resource_id: Option<&str>,
5043    tenant_id: Option<&str>,
5044    subject_id: Option<&str>,
5045    payload_json: &str,
5046) -> Option<i64> {
5047    if adapter_id.is_empty()
5048        || payload_json.len() > 256 * 1024
5049        || !serde_json::from_str::<Value>(payload_json)
5050            .ok()?
5051            .is_object()
5052    {
5053        return None;
5054    }
5055    let adapter = integrations_root
5056        .get("adapters")
5057        .and_then(Value::as_array)?
5058        .iter()
5059        .find(|row| row.get("adapter_id").and_then(Value::as_str) == Some(adapter_id))?;
5060    let adapter_kind = adapter
5061        .get("adapter_kind")
5062        .and_then(Value::as_str)
5063        .unwrap_or_default();
5064    if !dynamic_integration_http_adapter_kind(adapter_kind) {
5065        return None;
5066    }
5067    let target_url = adapter
5068        .get("target_url")
5069        .and_then(Value::as_str)
5070        .filter(|value| !value.is_empty())
5071        .map(ToOwned::to_owned)
5072        .or_else(|| {
5073            adapter
5074                .get("target_url_env")
5075                .and_then(Value::as_str)
5076                .and_then(|name| env::var(name).ok())
5077                .filter(|value| !value.is_empty())
5078        })?;
5079    let signature = adapter.get("signature").and_then(Value::as_object);
5080    let delivery_id = dynamic_take_next_section_id(root, "integration_deliveries");
5081    dynamic_state_rows_mut(root, "integration_deliveries")?.push(serde_json::json!({
5082        "id": delivery_id,
5083        "adapter_id": adapter_id,
5084        "adapter_kind": adapter_kind,
5085        "event_kind": event_kind.filter(|value| !value.is_empty()).unwrap_or("integration_event"),
5086        "resource_id": resource_id.unwrap_or_default(),
5087        "tenant_id": tenant_id.unwrap_or_default(),
5088        "subject_id": subject_id.unwrap_or_default(),
5089        "target_url": target_url,
5090        "content_type": adapter.get("content_type").and_then(Value::as_str).unwrap_or("application/json"),
5091        "payload_json": payload_json,
5092        "event_header": adapter.get("event_header").and_then(Value::as_str).unwrap_or_default(),
5093        "auth_header": adapter.get("auth_header").and_then(Value::as_str).unwrap_or_default(),
5094        "auth_scheme": adapter.get("auth_scheme").and_then(Value::as_str).unwrap_or_default(),
5095        "auth_token_env": adapter.get("auth_token_env").and_then(Value::as_str).unwrap_or_default(),
5096        "signature_header": signature.and_then(|value| value.get("header")).and_then(Value::as_str).unwrap_or("X-Pharos-Signature"),
5097        "timestamp_header": signature.and_then(|value| value.get("timestamp_header")).and_then(Value::as_str).unwrap_or("X-Pharos-Timestamp"),
5098        "signature_secret_env": signature.and_then(|value| value.get("secret_env")).and_then(Value::as_str).unwrap_or_default(),
5099        "delivery_status": "queued",
5100        "attempt_count": 0,
5101        "max_attempts": adapter.get("max_attempts").and_then(Value::as_i64).unwrap_or(1).max(1),
5102        "backoff_seconds": adapter.get("backoff_seconds").and_then(Value::as_i64).unwrap_or(0).max(0),
5103        "timeout_seconds": adapter.get("timeout_seconds").and_then(Value::as_i64).unwrap_or(0).max(0),
5104        "max_response_bytes": adapter.get("max_response_bytes").and_then(Value::as_u64).unwrap_or(8 * 1024).clamp(64, 16 * 1024 * 1024),
5105        "allow_plaintext_local": adapter.get("allow_plaintext_local").and_then(Value::as_bool).unwrap_or(false),
5106        "last_http_status": 0,
5107        "last_error": "",
5108        "next_attempt_at": "",
5109        "completed_at": "",
5110        "created_at": now_iso_utc(),
5111        "updated_at": now_iso_utc(),
5112    }));
5113    Some(delivery_id)
5114}
5115
5116/// Processes one due queued or retryable integration delivery in a worker-owned state transaction.
5117///
5118/// # Parameters
5119///
5120/// - `root`: Input consumed by [`crate::runtime::dynamic::process_one_due_integration_delivery`].
5121///
5122/// # Returns
5123///
5124/// Returns `true` when the condition checked by [`crate::runtime::dynamic::process_one_due_integration_delivery`] holds and `false` otherwise.
5125///
5126/// # Performance
5127///
5128/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::process_one_due_integration_delivery`].
5129///
5130/// # Examples
5131///
5132/// ```rust,ignore
5133/// let _result = pharos_native::runtime::dynamic::process_one_due_integration_delivery(std::path::Path::new("."));
5134/// ```
5135///
5136/// # Related
5137///
5138/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
5139pub fn process_one_due_integration_delivery(root: &mut Value) -> bool {
5140    let now = current_epoch_seconds();
5141    let delivery_id = root
5142        .get("integration_deliveries")
5143        .and_then(Value::as_array)
5144        .and_then(|rows| {
5145            rows.iter().find(|row| {
5146                row.get("delivery_status").and_then(Value::as_str) == Some("queued")
5147                    || (row.get("delivery_status").and_then(Value::as_str) == Some("retryable")
5148                        && row
5149                            .get("next_attempt_at")
5150                            .and_then(Value::as_str)
5151                            .and_then(|value| value.parse::<u64>().ok())
5152                            .is_some_and(|available| available <= now))
5153            })
5154        })
5155        .and_then(|row| row.get("id").and_then(Value::as_i64));
5156    delivery_id.is_some_and(|delivery_id| {
5157        let _ = dynamic_process_webhook_delivery(root, delivery_id);
5158        true
5159    })
5160}
5161
5162/// Appends the last action report and mirrors it into the action report list.
5163fn dynamic_store_last_action_report(root: &mut Value, mut report: Value) -> bool {
5164    let report_id = dynamic_take_next_section_id(root, "action_reports");
5165    if let Some(object) = report.as_object_mut() {
5166        object.insert("id".to_owned(), Value::from(report_id));
5167    }
5168    let Some(object) = root.as_object_mut() else {
5169        return false;
5170    };
5171    object.insert("last_action_report".to_owned(), report.clone());
5172    object.insert(
5173        "last_action".to_owned(),
5174        report
5175            .get("event_kind")
5176            .cloned()
5177            .unwrap_or(Value::String(String::new())),
5178    );
5179    dynamic_state_rows_mut(root, "action_reports").is_some_and(|rows| {
5180        rows.push(report);
5181        true
5182    })
5183}
5184
5185/// Builds the failure report without selecting or writing a persistence medium.
5186fn dynamic_action_failure_report_root(
5187    mut root: Value,
5188    action: &Value,
5189    request: &HttpRequest,
5190    input_overrides: &BTreeMap<String, Option<String>>,
5191    action_id: &str,
5192    entity_id: &str,
5193    transition: Option<&Value>,
5194    anchor_record_id: i64,
5195    status: i32,
5196) -> Value {
5197    let event_kind = format!("{action_id}_failed");
5198    let mut report = serde_json::json!({
5199        "action_id": action_id,
5200        "event_kind": event_kind,
5201        "execution_status": "failed",
5202        "failure_status": status,
5203        "failure_code": dynamic_action_failure_code(status),
5204        "created_record_count": 0,
5205        "updated_record_count": 0,
5206        "matched_record_count": 0,
5207        "skipped_record_count": 0,
5208        "removed_record_count": 0,
5209        "queued_notification_count": 0,
5210        "queued_webhook_count": 0,
5211        "emitted_audit_count": 0,
5212        "emitted_file_record_count": 0,
5213        "delivered_webhook_count": 0,
5214        "retryable_webhook_count": 0,
5215        "failed_webhook_count": 0,
5216        "retried_delivery_count": 0,
5217        "anchor_record_id": anchor_record_id,
5218        "operator_message": dynamic_presentation_text(action, "failure_message"),
5219        "operator_summary": dynamic_presentation_text(action, "operator_summary"),
5220        "transition_label": dynamic_transition_presentation_text(transition, "transition_label"),
5221        "to_state": transition.and_then(|value| value.get("to_state")).and_then(Value::as_str).unwrap_or_default(),
5222        "overflow_policy": action
5223            .get("mutation_primitives")
5224            .and_then(Value::as_array)
5225            .and_then(|rows| rows.iter().find_map(|row| row.get("overflow_policy").and_then(Value::as_str)))
5226            .unwrap_or_default(),
5227    });
5228    let annotations = dynamic_action_failure_annotations(
5229        action,
5230        request,
5231        input_overrides,
5232        action_id,
5233        entity_id,
5234        &event_kind,
5235        anchor_record_id,
5236    )
5237    .unwrap_or_default();
5238    if let Some(object) = report.as_object_mut()
5239        && !annotations.is_empty()
5240    {
5241        object.insert("annotations".to_owned(), Value::Object(annotations));
5242    }
5243    let _ = dynamic_store_last_action_report(&mut root, report);
5244    root
5245}
5246
5247/// Declared document-action response captured from the successfully committed view.
5248pub enum DeclarativeActionResponse {
5249    Redirect(String),
5250    Json(u16, Value),
5251}
5252
5253/// Executes the bounded declarative dynamic-app action contract against persisted state.
5254///
5255/// # Parameters
5256///
5257/// - `app`: Input consumed by [`crate::runtime::dynamic::dynamic_execute_declarative_action`].
5258/// - `state_path`: Input consumed by [`crate::runtime::dynamic::dynamic_execute_declarative_action`].
5259/// - `request`: Input consumed by [`crate::runtime::dynamic::dynamic_execute_declarative_action`].
5260/// - `action_id`: Input consumed by [`crate::runtime::dynamic::dynamic_execute_declarative_action`].
5261///
5262/// # Returns
5263///
5264/// Returns the successful value produced by [`crate::runtime::dynamic::dynamic_execute_declarative_action`] or an error when the operation fails.
5265///
5266/// # Errors
5267///
5268/// Returns an error value when [`crate::runtime::dynamic::dynamic_execute_declarative_action`] cannot complete successfully.
5269///
5270/// # Performance
5271///
5272/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::dynamic_execute_declarative_action`].
5273///
5274/// # Examples
5275///
5276/// ```rust,ignore
5277/// let _result = pharos_native::runtime::dynamic::dynamic_execute_declarative_action(todo!(), todo!(), todo!(), todo!());
5278/// ```
5279///
5280/// # Related
5281///
5282/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
5283pub fn dynamic_execute_declarative_action(
5284    app: &AppRuntime,
5285    state_path: Option<&Path>,
5286    request: &HttpRequest,
5287    action_id: &str,
5288) -> Result<DeclarativeActionResponse, i32> {
5289    let deadline = std::time::Instant::now() + std::time::Duration::from_secs(3);
5290    for attempt in 0..32 {
5291        let mut rejected_write = false;
5292        let result = dynamic_execute_declarative_action_attempt(
5293            app,
5294            state_path,
5295            request,
5296            action_id,
5297            &mut rejected_write,
5298        );
5299        if !rejected_write || attempt == 31 || std::time::Instant::now() >= deadline {
5300            return result;
5301        }
5302        // The compare-and-write was definitely rejected. Re-read and re-plan
5303        // without delivering external effects or replaying uncertain writes.
5304        std::thread::sleep(std::time::Duration::from_millis(25 + attempt * 5));
5305    }
5306    Err(409)
5307}
5308
5309/// Plans one document action; marks only definitely rejected commits for retry.
5310fn dynamic_execute_declarative_action_attempt(
5311    app: &AppRuntime,
5312    state_path: Option<&Path>,
5313    request: &HttpRequest,
5314    action_id: &str,
5315    rejected_write: &mut bool,
5316) -> Result<DeclarativeActionResponse, i32> {
5317    if action_id.is_empty() || app.app_id.as_deref().is_none_or(str::is_empty) {
5318        return Err(400);
5319    }
5320    let actions_root = dynamic_contract_doc(app, "action_contract").ok_or(404)?;
5321    let workflows_root = dynamic_contract_doc(app, "workflow_contract").ok_or(404)?;
5322    let integrations_root = dynamic_contract_doc(app, "integration_contract")
5323        .unwrap_or_else(|| serde_json::json!({"adapters": []}));
5324    let action = dynamic_find_action_contract(&actions_root, action_id).ok_or(404)?;
5325    let input_overrides = dynamic_action_input_overrides(action, request)?;
5326    let response = action.get("response_contract").ok_or(422)?;
5327    let response_kind = response
5328        .get("response_kind")
5329        .and_then(Value::as_str)
5330        .unwrap_or("redirect");
5331    if !matches!(response_kind, "redirect" | "json") {
5332        return Err(422);
5333    }
5334    let response_status = if response_kind == "json" {
5335        response
5336            .get("status")
5337            .and_then(Value::as_u64)
5338            .and_then(|status| u16::try_from(status).ok())
5339            .filter(|status| (200..=299).contains(status))
5340            .ok_or(422)?
5341    } else {
5342        303
5343    };
5344
5345    let transition = dynamic_find_workflow_transition(&workflows_root, action_id);
5346    let compiled_effects = app
5347        .application_ir
5348        .as_ref()
5349        .and_then(|ir| {
5350            ir.actions
5351                .values()
5352                .find(|candidate| candidate.id.as_str() == action_id)
5353        })
5354        .map(|action| &action.effects)
5355        .ok_or(403)?;
5356    let executable_effects =
5357        pharos_runtime_core::application_ir::compile_action_effects_for_runtime(action, transition)
5358            .map_err(|_| 422)?;
5359    if compiled_effects != &executable_effects {
5360        return Err(403);
5361    }
5362    let document_store = if super::app_uses_file_backed_runtime_state(app) {
5363        None
5364    } else {
5365        let store = super::database::runtime_logical_app_store(app).map_err(|_| 503)?;
5366        let (entity, field, identity) = declared_document_storage(app).ok_or(422)?;
5367        let snapshot = pharos_runtime_database::document::read_document(
5368            store.adapter.as_ref(),
5369            &store.config,
5370            &store.schema,
5371            &entity,
5372            identity,
5373            &field,
5374        )
5375        .map_err(|error| match error {
5376            pharos_runtime_database::operations::DatabaseError::MissingRecord => 404,
5377            pharos_runtime_database::operations::DatabaseError::Conflict => 409,
5378            _ => 503,
5379        })?;
5380        Some((store, snapshot))
5381    };
5382    let mut root = match &document_store {
5383        Some((_, snapshot)) => snapshot.value.clone(),
5384        None => fs::read_to_string(state_path.ok_or(503)?)
5385            .ok()
5386            .and_then(|text| serde_json::from_str::<Value>(&text).ok())
5387            .ok_or(404)?,
5388    };
5389    let original_root = root.clone();
5390    let result = (|| {
5391        let retried_delivery_count = 0;
5392        let failed_retried_delivery_count = 0;
5393        let mut created_record_count = 0_i64;
5394        let mut updated_record_count = 0_i64;
5395        let mut matched_record_count = 0_i64;
5396        let mut skipped_record_count = 0_i64;
5397        let mut removed_record_count = 0_i64;
5398        let mut anchor_record_id = 0_i64;
5399        for primitive in action
5400            .get("mutation_primitives")
5401            .and_then(Value::as_array)
5402            .into_iter()
5403            .flatten()
5404        {
5405            let primitive_kind = primitive
5406                .get("primitive_kind")
5407                .and_then(Value::as_str)
5408                .unwrap_or_default();
5409            match primitive_kind {
5410                "append_record" => {
5411                    dynamic_append_record_primitive(
5412                        &mut root,
5413                        request,
5414                        &input_overrides,
5415                        primitive,
5416                        &mut anchor_record_id,
5417                        &mut created_record_count,
5418                    )?;
5419                }
5420                "set_root_fields" => {
5421                    dynamic_set_root_fields_primitive(
5422                        app,
5423                        &mut root,
5424                        request,
5425                        &input_overrides,
5426                        primitive,
5427                    )?;
5428                }
5429                "update_record" => {
5430                    dynamic_update_record_primitive(
5431                        &mut root,
5432                        request,
5433                        &input_overrides,
5434                        primitive,
5435                        &mut matched_record_count,
5436                        &mut updated_record_count,
5437                    )?;
5438                }
5439                "approval_transition" => {
5440                    let collection = primitive
5441                        .get("collection")
5442                        .and_then(Value::as_str)
5443                        .unwrap_or("approval_requests");
5444                    let match_field = primitive
5445                        .get("match")
5446                        .and_then(Value::as_object)
5447                        .and_then(|map| map.get("field"))
5448                        .and_then(Value::as_str)
5449                        .unwrap_or("id");
5450                    let match_input = primitive
5451                        .get("match")
5452                        .and_then(Value::as_object)
5453                        .and_then(|map| map.get("from_input"))
5454                        .and_then(Value::as_str)
5455                        .unwrap_or("id");
5456                    let wanted = dynamic_action_input_value(request, &input_overrides, match_input)
5457                        .unwrap_or_default();
5458                    let transition_kind = primitive
5459                        .get("transition_kind")
5460                        .and_then(Value::as_str)
5461                        .unwrap_or_default();
5462                    let Some(row) =
5463                        dynamic_state_rows_mut(&mut root, collection).and_then(|rows| {
5464                            rows.iter_mut().find(|row| {
5465                                row.get(match_field).map(value_to_text).unwrap_or_default()
5466                                    == wanted
5467                            })
5468                        })
5469                    else {
5470                        return Err(404);
5471                    };
5472                    let Some(object) = row.as_object_mut() else {
5473                        return Err(500);
5474                    };
5475                    if transition_kind.is_empty() {
5476                        return Err(422);
5477                    }
5478                    dynamic_apply_approval_transition(
5479                        object,
5480                        primitive,
5481                        request,
5482                        &input_overrides,
5483                    )?;
5484                    updated_record_count += 1;
5485                    matched_record_count += 1;
5486                }
5487                "reservation_transition" => {
5488                    let collection = primitive
5489                        .get("collection")
5490                        .and_then(Value::as_str)
5491                        .ok_or(422)?;
5492                    let scope_field = primitive
5493                        .get("scope_match")
5494                        .and_then(Value::as_object)
5495                        .and_then(|map| map.get("field"))
5496                        .and_then(Value::as_str)
5497                        .ok_or(422)?;
5498                    let scope_input = primitive
5499                        .get("scope_match")
5500                        .and_then(Value::as_object)
5501                        .and_then(|map| map.get("from_input"))
5502                        .and_then(Value::as_str)
5503                        .ok_or(422)?;
5504                    let wanted = dynamic_action_input_value(request, &input_overrides, scope_input)
5505                        .unwrap_or_default();
5506                    let reservation_state_field = primitive
5507                        .get("reservation_state_field")
5508                        .and_then(Value::as_str)
5509                        .filter(|value| !value.is_empty())
5510                        .unwrap_or("reservation_state");
5511                    let reserved_by_field = primitive
5512                        .get("reserved_by_field")
5513                        .and_then(Value::as_str)
5514                        .filter(|value| !value.is_empty())
5515                        .unwrap_or("reserved_by");
5516                    let reservation_key_field = primitive
5517                        .get("reservation_key_field")
5518                        .and_then(Value::as_str)
5519                        .filter(|value| !value.is_empty())
5520                        .unwrap_or("reservation_key");
5521                    let reserved_at_field = primitive
5522                        .get("reserved_at_field")
5523                        .and_then(Value::as_str)
5524                        .filter(|value| !value.is_empty())
5525                        .unwrap_or("reserved_at");
5526                    let released_at_field = primitive
5527                        .get("released_at_field")
5528                        .and_then(Value::as_str)
5529                        .filter(|value| !value.is_empty())
5530                        .unwrap_or("released_at");
5531                    let released_by_field = primitive
5532                        .get("released_by_field")
5533                        .and_then(Value::as_str)
5534                        .filter(|value| !value.is_empty());
5535                    let transition_kind = primitive
5536                        .get("transition_kind")
5537                        .and_then(Value::as_str)
5538                        .unwrap_or_default();
5539                    let max_records = primitive
5540                        .get("max_records")
5541                        .and_then(Value::as_i64)
5542                        .unwrap_or(i64::MAX)
5543                        .max(0) as usize;
5544                    let overflow_policy = primitive
5545                        .get("overflow_policy")
5546                        .and_then(Value::as_str)
5547                        .unwrap_or("truncate");
5548                    let actor_value = primitive
5549                        .get("actor_input")
5550                        .and_then(Value::as_str)
5551                        .and_then(|field| {
5552                            dynamic_action_input_value(request, &input_overrides, field)
5553                        })
5554                        .unwrap_or_default();
5555                    let reservation_key = primitive
5556                        .get("key_input")
5557                        .and_then(Value::as_str)
5558                        .and_then(|field| {
5559                            dynamic_action_input_value(request, &input_overrides, field)
5560                        })
5561                        .unwrap_or_default();
5562                    let released_state = primitive
5563                        .get("released_state")
5564                        .and_then(Value::as_str)
5565                        .filter(|value| !value.is_empty())
5566                        .unwrap_or("available");
5567                    let Some(rows) = dynamic_state_rows_mut(&mut root, collection) else {
5568                        return Err(500);
5569                    };
5570                    let matching_indexes = rows
5571                        .iter()
5572                        .enumerate()
5573                        .filter(|(_, row)| {
5574                            row.get(scope_field).map(value_to_text).unwrap_or_default() == wanted
5575                        })
5576                        .map(|(index, _)| index)
5577                        .collect::<Vec<_>>();
5578                    matched_record_count += matching_indexes.len() as i64;
5579                    if matching_indexes.len() > max_records && overflow_policy == "fail" {
5580                        return Err(422);
5581                    }
5582                    skipped_record_count +=
5583                        matching_indexes.len().saturating_sub(max_records) as i64;
5584                    if transition_kind == "reserve" && actor_value.trim().is_empty() {
5585                        return Err(422);
5586                    }
5587                    let now = now_iso_utc();
5588                    for index in matching_indexes.into_iter().take(max_records) {
5589                        let Some(object) = rows.get_mut(index).and_then(Value::as_object_mut)
5590                        else {
5591                            return Err(500);
5592                        };
5593                        let current_state = object
5594                            .get(reservation_state_field)
5595                            .and_then(Value::as_str)
5596                            .unwrap_or_default();
5597                        let current_key = object
5598                            .get(reservation_key_field)
5599                            .map(value_to_text)
5600                            .unwrap_or_default();
5601                        match transition_kind {
5602                            "reserve" => {
5603                                if current_state == "reserved"
5604                                    && !current_key.is_empty()
5605                                    && current_key != reservation_key
5606                                {
5607                                    return Err(409);
5608                                }
5609                                object.insert(
5610                                    reservation_state_field.to_owned(),
5611                                    Value::String("reserved".to_owned()),
5612                                );
5613                                object.insert(
5614                                    reserved_by_field.to_owned(),
5615                                    Value::String(actor_value.clone()),
5616                                );
5617                                object.insert(
5618                                    reservation_key_field.to_owned(),
5619                                    Value::String(reservation_key.clone()),
5620                                );
5621                                object.insert(
5622                                    reserved_at_field.to_owned(),
5623                                    Value::String(now.clone()),
5624                                );
5625                                object.insert(
5626                                    released_at_field.to_owned(),
5627                                    Value::String(String::new()),
5628                                );
5629                                if let Some(field) = released_by_field {
5630                                    object.insert(field.to_owned(), Value::String(String::new()));
5631                                }
5632                            }
5633                            "release" => {
5634                                if current_state == "reserved"
5635                                    && !reservation_key.is_empty()
5636                                    && !current_key.is_empty()
5637                                    && current_key != reservation_key
5638                                {
5639                                    return Err(409);
5640                                }
5641                                object.insert(
5642                                    reservation_state_field.to_owned(),
5643                                    Value::String(released_state.to_owned()),
5644                                );
5645                                object.insert(
5646                                    reserved_by_field.to_owned(),
5647                                    Value::String(String::new()),
5648                                );
5649                                object.insert(
5650                                    reservation_key_field.to_owned(),
5651                                    Value::String(String::new()),
5652                                );
5653                                object.insert(
5654                                    released_at_field.to_owned(),
5655                                    Value::String(now.clone()),
5656                                );
5657                                if let Some(field) = released_by_field {
5658                                    object.insert(
5659                                        field.to_owned(),
5660                                        Value::String(actor_value.clone()),
5661                                    );
5662                                }
5663                            }
5664                            _ => return Err(422),
5665                        }
5666                        updated_record_count += 1;
5667                    }
5668                }
5669                "scoped_status_transition" => {
5670                    dynamic_apply_scoped_status_transition_primitive(
5671                        &mut root,
5672                        request,
5673                        &input_overrides,
5674                        primitive,
5675                        &mut matched_record_count,
5676                        &mut updated_record_count,
5677                        &mut skipped_record_count,
5678                    )?;
5679                }
5680                "append_records_from_input_list" => {
5681                    dynamic_append_records_from_input_list_primitive(
5682                        &mut root,
5683                        request,
5684                        &input_overrides,
5685                        primitive,
5686                        anchor_record_id,
5687                        matched_record_count,
5688                        updated_record_count,
5689                        &mut skipped_record_count,
5690                        &mut created_record_count,
5691                    )?;
5692                }
5693                "replace_records_from_input_list" => {
5694                    dynamic_replace_records_from_input_list_primitive(
5695                        &mut root,
5696                        request,
5697                        &input_overrides,
5698                        primitive,
5699                        &mut matched_record_count,
5700                        &mut created_record_count,
5701                        &mut updated_record_count,
5702                        &mut skipped_record_count,
5703                        &mut removed_record_count,
5704                    )?;
5705                }
5706                "delete_records_by_scope" => {
5707                    dynamic_delete_records_by_scope_primitive(
5708                        &mut root,
5709                        request,
5710                        &input_overrides,
5711                        primitive,
5712                        &mut matched_record_count,
5713                        &mut skipped_record_count,
5714                        &mut removed_record_count,
5715                    )?;
5716                }
5717                "append_records_from_related_collection" => {
5718                    dynamic_append_records_from_related_collection_primitive(
5719                        &mut root,
5720                        request,
5721                        &input_overrides,
5722                        primitive,
5723                        &mut matched_record_count,
5724                        &mut skipped_record_count,
5725                        &mut created_record_count,
5726                    )?;
5727                }
5728                "scoped_reassignment" | "bulk_update" => {
5729                    dynamic_apply_scoped_assignment_or_bulk_update_primitive(
5730                        &mut root,
5731                        request,
5732                        &input_overrides,
5733                        primitive,
5734                        primitive_kind,
5735                        &mut matched_record_count,
5736                        &mut updated_record_count,
5737                        &mut skipped_record_count,
5738                    )?;
5739                }
5740                "upsert_record" => {
5741                    dynamic_upsert_record_primitive(
5742                        &mut root,
5743                        request,
5744                        &input_overrides,
5745                        primitive,
5746                        anchor_record_id,
5747                        &mut matched_record_count,
5748                        &mut updated_record_count,
5749                        skipped_record_count,
5750                        &mut created_record_count,
5751                    )?;
5752                }
5753                _ => return Err(422),
5754            }
5755        }
5756        let (
5757            event_kind,
5758            queued_notification_count,
5759            queued_webhook_count,
5760            emitted_audit_count,
5761            queued_file_record_count,
5762            failed_effect_count,
5763        ) = prepare_action_effects(
5764            app,
5765            &mut root,
5766            action,
5767            transition,
5768            &integrations_root,
5769            request,
5770            &input_overrides,
5771            action_id,
5772            anchor_record_id,
5773        )?;
5774        let entity_id = action
5775            .get("entity_id")
5776            .and_then(Value::as_str)
5777            .unwrap_or("record");
5778        let delivered_webhook_count = 0_i64;
5779        let retryable_webhook_count = 0_i64;
5780        let failed_webhook_count = failed_retried_delivery_count + failed_effect_count;
5781        let report = serde_json::json!({
5782            "action_id": action_id,
5783            "event_kind": event_kind,
5784            "created_record_count": created_record_count,
5785            "updated_record_count": updated_record_count,
5786            "matched_record_count": matched_record_count,
5787            "skipped_record_count": skipped_record_count,
5788            "removed_record_count": removed_record_count,
5789            "queued_notification_count": queued_notification_count,
5790            "queued_webhook_count": queued_webhook_count,
5791            "emitted_audit_count": emitted_audit_count,
5792            "emitted_file_record_count": 0,
5793            "queued_file_record_count": queued_file_record_count,
5794            "delivered_webhook_count": delivered_webhook_count,
5795            "retryable_webhook_count": retryable_webhook_count,
5796            "failed_webhook_count": failed_webhook_count,
5797            "retried_delivery_count": retried_delivery_count,
5798            "anchor_record_id": anchor_record_id,
5799            "operator_message": dynamic_presentation_text(action, "success_message"),
5800            "operator_summary": dynamic_presentation_text(action, "operator_summary"),
5801            "transition_label": dynamic_transition_presentation_text(transition, "transition_label"),
5802            "to_state": transition.and_then(|value| value.get("to_state")).and_then(Value::as_str).unwrap_or_default(),
5803            "overflow_policy": action
5804                .get("mutation_primitives")
5805                .and_then(Value::as_array)
5806                .and_then(|rows| rows.iter().find_map(|row| row.get("overflow_policy").and_then(Value::as_str)))
5807                .unwrap_or_default(),
5808        });
5809        let report_annotations = dynamic_action_report_annotations(
5810            action,
5811            request,
5812            &input_overrides,
5813            action_id,
5814            entity_id,
5815            &event_kind,
5816            anchor_record_id,
5817        )?;
5818        let mut report = report;
5819        if let Some(object) = report.as_object_mut()
5820            && !report_annotations.is_empty()
5821        {
5822            object.insert("annotations".to_owned(), Value::Object(report_annotations));
5823        }
5824        if !dynamic_store_last_action_report(&mut root, report) {
5825            return Err(500);
5826        }
5827        if let Some((store, snapshot)) = &document_store {
5828            pharos_runtime_database::document::write_document(
5829                store.adapter.as_ref(),
5830                &store.config,
5831                &store.schema,
5832                snapshot,
5833                &root,
5834            )
5835            .map_err(|error| match error {
5836                pharos_runtime_database::operations::DatabaseError::Conflict => {
5837                    *rejected_write = true;
5838                    409
5839                }
5840                pharos_runtime_database::operations::DatabaseError::InvalidInput => 422,
5841                _ => 503,
5842            })?;
5843        }
5844        if document_store.is_none() {
5845            let encoded = serde_json::to_string(&root).map_err(|_| 500)?;
5846            if super::fs::write_text_file(state_path.ok_or(503)?, &encoded) != 0 {
5847                return Err(500);
5848            }
5849        }
5850        if response_kind == "json" {
5851            let mut body = serde_json::json!({
5852                "schema": "pharos.action.result.v1",
5853                "action_id": action_id,
5854                "report": root.get("last_action_report").cloned().unwrap_or(Value::Null),
5855            });
5856            if response.get("include_state").and_then(Value::as_bool) == Some(true) {
5857                if let Some(object) = root.as_object_mut() {
5858                    object.remove("_database_worker_lease");
5859                }
5860                body["state"] = root;
5861            }
5862            Ok(DeclarativeActionResponse::Json(response_status, body))
5863        } else {
5864            Ok(DeclarativeActionResponse::Redirect(
5865                response
5866                    .get("location")
5867                    .and_then(Value::as_str)
5868                    .unwrap_or("/")
5869                    .to_owned(),
5870            ))
5871        }
5872    })();
5873    if let Err(status) = result
5874        && !*rejected_write
5875    {
5876        let entity_id = action
5877            .get("entity_id")
5878            .and_then(Value::as_str)
5879            .unwrap_or("record");
5880        let failure_root = dynamic_action_failure_report_root(
5881            original_root,
5882            action,
5883            request,
5884            &input_overrides,
5885            action_id,
5886            entity_id,
5887            transition,
5888            0,
5889            status,
5890        );
5891        if let Some((store, snapshot)) = &document_store {
5892            // The original compare-and-swap token prevents a failure report
5893            // from overwriting another action that committed concurrently.
5894            match pharos_runtime_database::document::write_document(
5895                store.adapter.as_ref(),
5896                &store.config,
5897                &store.schema,
5898                snapshot,
5899                &failure_root,
5900            ) {
5901                Ok(()) => {}
5902                Err(pharos_runtime_database::operations::DatabaseError::Conflict) => {
5903                    *rejected_write = true;
5904                }
5905                Err(_) => return Err(503),
5906            }
5907        } else if let Some(state_path) = state_path
5908            && let Ok(encoded) = serde_json::to_string(&failure_root)
5909        {
5910            let _ = super::fs::write_text_file(state_path, &encoded);
5911        }
5912    }
5913    result
5914}
5915
5916/// Prepares declared effects in memory so their persistence shares the domain
5917/// transaction. It performs no network request or local-file write.
5918fn prepare_action_effects(
5919    app: &AppRuntime,
5920    root: &mut Value,
5921    action: &Value,
5922    transition: Option<&Value>,
5923    integrations_root: &Value,
5924    request: &HttpRequest,
5925    input_overrides: &BTreeMap<String, Option<String>>,
5926    action_id: &str,
5927    anchor_record_id: i64,
5928) -> Result<(String, i64, i64, i64, i64, i64), i32> {
5929    let workflow_execution_id =
5930        dynamic_append_workflow_execution(root, action_id, transition).ok_or(500)?;
5931    if !dynamic_append_workflow_events(root, workflow_execution_id, transition) {
5932        return Err(422);
5933    }
5934    let scheduled_jobs = transition
5935        .and_then(|value| value.get("scheduled_jobs"))
5936        .and_then(Value::as_array)
5937        .cloned()
5938        .unwrap_or_default();
5939    for job in &scheduled_jobs {
5940        let job_id = job
5941            .as_str()
5942            .or_else(|| job.get("job_id").and_then(Value::as_str))
5943            .unwrap_or_default();
5944        let queue_name = job
5945            .get("queue_name")
5946            .and_then(Value::as_str)
5947            .unwrap_or("default");
5948        let delay_seconds = job
5949            .get("delay_seconds")
5950            .and_then(Value::as_i64)
5951            .unwrap_or(0);
5952        let max_attempts = job
5953            .get("max_attempts")
5954            .and_then(Value::as_i64)
5955            .unwrap_or(app.queue_max_attempts.max(1));
5956        let backoff_seconds = job
5957            .get("backoff_seconds")
5958            .and_then(Value::as_i64)
5959            .unwrap_or(app.queue_retry_seconds.max(0));
5960        let lease_seconds = job
5961            .get("lease_seconds")
5962            .and_then(Value::as_i64)
5963            .unwrap_or(30);
5964        if !dynamic_append_scheduled_job(
5965            root,
5966            workflow_execution_id,
5967            job_id,
5968            queue_name,
5969            delay_seconds,
5970            max_attempts,
5971            backoff_seconds,
5972            lease_seconds,
5973        ) {
5974            return Err(422);
5975        }
5976    }
5977    let mut queued_notification_count = 0_i64;
5978    let mut queued_webhook_count = 0_i64;
5979    let mut emitted_audit_count = 0_i64;
5980    let mut queued_file_record_count = 0_i64;
5981
5982    let mut failed_webhook_count = 0_i64;
5983    let event_kind = transition
5984        .and_then(|value| value.get("emits_event"))
5985        .and_then(Value::as_str)
5986        .map(ToOwned::to_owned)
5987        .or_else(|| {
5988            root.get("last_action")
5989                .and_then(Value::as_str)
5990                .filter(|value| !value.is_empty())
5991                .map(ToOwned::to_owned)
5992        })
5993        .unwrap_or_else(|| action_id.to_owned());
5994    let entity_id = action
5995        .get("entity_id")
5996        .and_then(Value::as_str)
5997        .unwrap_or("record");
5998    for notification in action
5999        .get("effect_hooks")
6000        .and_then(Value::as_object)
6001        .and_then(|hooks| hooks.get("notifications"))
6002        .and_then(Value::as_array)
6003        .into_iter()
6004        .flatten()
6005    {
6006        let adapter_id = notification
6007            .get("adapter_id")
6008            .and_then(Value::as_str)
6009            .unwrap_or("local-mail");
6010        let channel = notification
6011            .get("channel")
6012            .and_then(Value::as_str)
6013            .unwrap_or("email");
6014        let recipient = notification
6015            .get("recipient")
6016            .and_then(Value::as_object)
6017            .and_then(|spec| {
6018                dynamic_notification_parameter_value(
6019                    &Value::Object(spec.clone()),
6020                    request,
6021                    input_overrides,
6022                    action_id,
6023                    entity_id,
6024                    &event_kind,
6025                    "",
6026                    "",
6027                    anchor_record_id,
6028                )
6029            })
6030            .and_then(|value| value.as_str().map(ToOwned::to_owned))
6031            .unwrap_or_default();
6032        let template_id = notification
6033            .get("template_id")
6034            .and_then(Value::as_object)
6035            .and_then(|spec| {
6036                dynamic_notification_parameter_value(
6037                    &Value::Object(spec.clone()),
6038                    request,
6039                    input_overrides,
6040                    action_id,
6041                    entity_id,
6042                    &event_kind,
6043                    &recipient,
6044                    "",
6045                    anchor_record_id,
6046                )
6047            })
6048            .and_then(|value| value.as_str().map(ToOwned::to_owned))
6049            .unwrap_or_default();
6050        let template_params = dynamic_notification_template_params(
6051            notification,
6052            request,
6053            input_overrides,
6054            action_id,
6055            entity_id,
6056            &event_kind,
6057            &recipient,
6058            &template_id,
6059            anchor_record_id,
6060        );
6061        if dynamic_append_notification(
6062            root,
6063            integrations_root,
6064            adapter_id,
6065            channel,
6066            &recipient,
6067            &template_id,
6068            &template_params,
6069        ) {
6070            queued_notification_count += 1;
6071        } else {
6072            return Err(422);
6073        }
6074    }
6075    let resource_id =
6076        dynamic_action_resource_id(action, request, input_overrides, anchor_record_id);
6077    for adapter_id in action
6078        .get("effect_hooks")
6079        .and_then(Value::as_object)
6080        .and_then(|hooks| hooks.get("webhook_dispatch"))
6081        .and_then(Value::as_array)
6082        .into_iter()
6083        .flatten()
6084        .filter_map(Value::as_str)
6085    {
6086        if dynamic_append_webhook_delivery(
6087            root,
6088            integrations_root,
6089            adapter_id,
6090            action_id,
6091            &resource_id,
6092        )
6093        .is_some()
6094        {
6095            queued_webhook_count += 1;
6096        } else {
6097            failed_webhook_count += 1;
6098        }
6099    }
6100    for audit in action
6101        .get("effect_hooks")
6102        .and_then(Value::as_object)
6103        .and_then(|hooks| hooks.get("audit_records"))
6104        .and_then(Value::as_array)
6105        .into_iter()
6106        .flatten()
6107    {
6108        if dynamic_append_audit_record(
6109            root,
6110            integrations_root,
6111            action,
6112            audit,
6113            request,
6114            input_overrides,
6115            action_id,
6116            entity_id,
6117            &event_kind,
6118            anchor_record_id,
6119        ) {
6120            emitted_audit_count += 1;
6121        } else {
6122            return Err(422);
6123        }
6124    }
6125    for file_record in action
6126        .get("effect_hooks")
6127        .and_then(Value::as_object)
6128        .and_then(|hooks| hooks.get("file_records"))
6129        .and_then(Value::as_array)
6130        .into_iter()
6131        .flatten()
6132    {
6133        let prepared = prepare_file_record(
6134            integrations_root,
6135            action,
6136            file_record,
6137            request,
6138            input_overrides,
6139            action_id,
6140            entity_id,
6141            &event_kind,
6142            anchor_record_id,
6143        )
6144        .ok_or(422)?;
6145        if !enqueue_file_record(root, prepared) {
6146            return Err(422);
6147        }
6148        queued_file_record_count += 1;
6149    }
6150    Ok((
6151        event_kind,
6152        queued_notification_count,
6153        queued_webhook_count,
6154        emitted_audit_count,
6155        queued_file_record_count,
6156        failed_webhook_count,
6157    ))
6158}
6159
6160/// Resolves a declared action's resource label without a vendor or table guess.
6161pub(super) fn action_resource_binding(
6162    app: &AppRuntime,
6163    schema: &pharos_runtime_database::operations::AppSchema,
6164    action_id: &str,
6165) -> Option<pharos_runtime_database::effects::ResourceBinding> {
6166    let actions = dynamic_contract_doc(app, "action_contract")?;
6167    let action = dynamic_find_action_contract(&actions, action_id)?;
6168    let label = action.get("entity_id")?.as_str()?;
6169    let entities = dynamic_contract_doc(app, "entity_contract");
6170    let name = entities
6171        .as_ref()
6172        .and_then(|root| root.get("entities"))
6173        .and_then(Value::as_array)
6174        .and_then(|rows| {
6175            rows.iter()
6176                .find(|row| row.get("entity_id").and_then(Value::as_str) == Some(label))
6177        })
6178        .and_then(|row| row.get("table"))
6179        .and_then(Value::as_str)
6180        .unwrap_or(label);
6181    schema.entities.iter().find(|entity| entity.name == name)?;
6182    Some(pharos_runtime_database::effects::ResourceBinding {
6183        entity: name.to_owned(),
6184        prefix: format!("{label}:"),
6185    })
6186}
6187
6188/// Converts the shared action-effect preparation into insertion-only logical
6189/// operations. This reads declarations, never an app-state projection.
6190pub(super) fn declared_effect_steps(
6191    app: &AppRuntime,
6192    schema: &pharos_runtime_database::operations::AppSchema,
6193    request: &HttpRequest,
6194    action_id: &str,
6195    input_overrides: &BTreeMap<String, Option<String>>,
6196    anchor: i64,
6197    actor_id: i64,
6198) -> Result<Vec<pharos_runtime_database::operations::Mutation>, i32> {
6199    use pharos_runtime_database::operations::{FieldType, Mutation, WriteValue};
6200    let Some(actions) = dynamic_contract_doc(app, "action_contract") else {
6201        return Ok(Vec::new());
6202    };
6203    let Some(action) = dynamic_find_action_contract(&actions, action_id) else {
6204        return Ok(Vec::new());
6205    };
6206    let workflows = dynamic_contract_doc(app, "workflow_contract");
6207    let transition = workflows
6208        .as_ref()
6209        .and_then(|root| dynamic_find_workflow_transition(root, action_id));
6210    if transition.is_none() && action.get("effect_hooks").is_none() {
6211        return Ok(Vec::new());
6212    }
6213    let integrations =
6214        integration_contract_doc(app).unwrap_or_else(|| serde_json::json!({"adapters": []}));
6215    let mut prepared = serde_json::json!({});
6216    let (_, _, _, _, _, failures) = prepare_action_effects(
6217        app,
6218        &mut prepared,
6219        action,
6220        transition,
6221        &integrations,
6222        request,
6223        input_overrides,
6224        action_id,
6225        anchor,
6226    )?;
6227    if failures != 0 {
6228        return Err(422);
6229    }
6230    static EFFECT_SEQUENCE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
6231    let sequence = EFFECT_SEQUENCE.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
6232    let token = hex_digest(&Sha256::digest(
6233        format!(
6234            "{}:{}:{}:{}",
6235            action_id,
6236            std::process::id(),
6237            std::time::SystemTime::now()
6238                .duration_since(std::time::UNIX_EPOCH)
6239                .map_err(|_| 503)?
6240                .as_nanos(),
6241            sequence
6242        )
6243        .as_bytes(),
6244    ));
6245    let mut steps = Vec::new();
6246    let mut workflow_step = None;
6247    for (section, entity_name) in [
6248        ("workflow_executions", "workflow_execution"),
6249        ("workflow_execution_events", "workflow_execution_event"),
6250        ("job_executions", "job_execution"),
6251        ("notification_deliveries", "notification_delivery"),
6252        ("integration_deliveries", "integration_delivery"),
6253        ("audit_log", "audit_event"),
6254        ("local_file_deliveries", "local_file_delivery"),
6255    ] {
6256        let Some(rows) = prepared.get(section).and_then(Value::as_array) else {
6257            continue;
6258        };
6259        if rows.is_empty() {
6260            continue;
6261        }
6262        let entity = schema
6263            .entities
6264            .iter()
6265            .find(|entity| entity.name == entity_name)
6266            .ok_or(422)?;
6267        for row in rows {
6268            let mut row = row.as_object().cloned().ok_or(422)?;
6269            if entity_name == "workflow_execution" {
6270                row.insert("idempotency_key".to_owned(), Value::String(token.clone()));
6271                row.insert("replay_trace_id".to_owned(), Value::String(token.clone()));
6272            }
6273            if entity_name == "audit_event" {
6274                row.insert("payload_json".to_owned(), Value::Object(row.clone()));
6275                row.insert(
6276                    "entity_type".to_owned(),
6277                    action.get("entity_id").cloned().unwrap_or(Value::Null),
6278                );
6279                row.insert(
6280                    "entity_id".to_owned(),
6281                    if anchor > 0 {
6282                        Value::from(anchor)
6283                    } else {
6284                        Value::Null
6285                    },
6286                );
6287                row.insert(
6288                    "subject_user_id".to_owned(),
6289                    if actor_id > 0 {
6290                        Value::from(actor_id)
6291                    } else {
6292                        Value::Null
6293                    },
6294                );
6295            }
6296            if entity_name == "workflow_execution" {
6297                row.insert(
6298                    "resource_id".to_owned(),
6299                    Value::String(dynamic_action_resource_id(
6300                        action,
6301                        request,
6302                        input_overrides,
6303                        anchor,
6304                    )),
6305                );
6306                row.insert(
6307                    "executed_by_user_id".to_owned(),
6308                    if actor_id > 0 {
6309                        Value::from(actor_id)
6310                    } else {
6311                        Value::Null
6312                    },
6313                );
6314            }
6315            if entity_name == "integration_delivery"
6316                && let Some(header) = row.get("timestamp_header").cloned()
6317            {
6318                row.insert("signature_timestamp_header".to_owned(), header);
6319            }
6320            if entity_name == "job_execution" {
6321                row.insert(
6322                    "idempotency_key".to_owned(),
6323                    Value::String(format!("{}:{}", token, steps.len())),
6324                );
6325                row.insert("replay_trace_id".to_owned(), Value::String(token.clone()));
6326            }
6327            let mut values = BTreeMap::new();
6328            for field in &entity.fields {
6329                if entity.identity.generated_field() == Some(field.name.as_str()) {
6330                    continue;
6331                }
6332                let Some(mut value) = row.get(&field.name).cloned() else {
6333                    continue;
6334                };
6335                if field.name == "workflow_execution_id" {
6336                    values.insert(
6337                        field.name.clone(),
6338                        WriteValue::GeneratedId(workflow_step.ok_or(422)?),
6339                    );
6340                    continue;
6341                }
6342                if field.nullable && value.as_str() == Some("") {
6343                    value = Value::Null;
6344                }
6345                if field.kind == FieldType::Timestamp
6346                    && let Some(text) = value.as_str()
6347                    && let Ok(epoch) = text.parse::<i64>()
6348                {
6349                    let seconds = u64::try_from(epoch)
6350                        .ok()
6351                        .filter(|seconds| *seconds <= 253_402_300_799)
6352                        .ok_or(422)?;
6353                    value = Value::String(pharos_runtime_core::time::timestamp_from_epoch_seconds(
6354                        seconds,
6355                    ));
6356                }
6357                if field.kind == FieldType::Json
6358                    && let Some(text) = value.as_str()
6359                {
6360                    value = serde_json::from_str(text).map_err(|_| 422)?;
6361                }
6362                values.insert(field.name.clone(), WriteValue::Literal(value));
6363            }
6364            if entity_name == "workflow_execution" {
6365                workflow_step = Some(steps.len());
6366            }
6367            steps.push(Mutation::Insert {
6368                entity: entity_name.to_owned(),
6369                values,
6370            });
6371        }
6372    }
6373    Ok(steps)
6374}
6375
6376/// Identifies an app-declared document mutation without choosing a database vendor.
6377pub fn is_declarative_document_action(app: &AppRuntime, action_id: &str) -> bool {
6378    if !super::app_uses_file_backed_runtime_state(app) && declared_document_storage(app).is_none() {
6379        return false;
6380    }
6381    dynamic_contract_doc(app, "action_contract")
6382        .and_then(|document| dynamic_find_action_contract(&document, action_id).cloned())
6383        .is_some_and(|action| {
6384            action
6385                .get("mutation_primitives")
6386                .and_then(Value::as_array)
6387                .is_some_and(|steps| !steps.is_empty())
6388        })
6389}
6390
6391/// Document persistence requires an explicit app declaration; relational actions cannot opt in implicitly.
6392pub(super) fn declared_document_storage(
6393    app: &AppRuntime,
6394) -> Option<(
6395    String,
6396    String,
6397    pharos_runtime_database::operations::Predicate,
6398)> {
6399    let root = app.app_root.as_deref().map(Path::new)?;
6400    let manifest = super::declarative::declarative_source_load_fixture(root, "pharos.app.yml")?;
6401    let declaration = manifest.get("database")?.get("document_storage")?;
6402    let entity = declaration.get("entity")?.as_str()?;
6403    let field = declaration.get("field")?.as_str()?;
6404    let identity = declaration.get("identity")?.as_object()?;
6405    if entity.is_empty() || field.is_empty() || identity.is_empty() || identity.len() > 16 {
6406        return None;
6407    }
6408    let predicates = identity
6409        .iter()
6410        .map(|(name, value)| {
6411            if name.is_empty() || value.is_null() || value.is_array() || value.is_object() {
6412                return None;
6413            }
6414            Some(pharos_runtime_database::operations::Predicate::Equal(
6415                name.clone(),
6416                value.clone(),
6417            ))
6418        })
6419        .collect::<Option<Vec<_>>>()?;
6420    Some((
6421        entity.to_owned(),
6422        field.to_owned(),
6423        pharos_runtime_database::operations::Predicate::All(predicates),
6424    ))
6425}
6426
6427/// Returns whether a route pattern matches a request path using bounded segments.
6428///
6429/// # Parameters
6430///
6431/// - `pattern`: Input consumed by [`crate::runtime::dynamic::pattern_matches`].
6432/// - `path`: Input consumed by [`crate::runtime::dynamic::pattern_matches`].
6433///
6434/// # Returns
6435///
6436/// Returns `true` when the condition checked by [`crate::runtime::dynamic::pattern_matches`] holds and `false` otherwise.
6437///
6438/// # Performance
6439///
6440/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::pattern_matches`].
6441///
6442/// # Examples
6443///
6444/// ```rust,ignore
6445/// let _result = pharos_native::runtime::dynamic::pattern_matches(todo!(), std::path::Path::new("fixture.json"));
6446/// ```
6447///
6448/// # Related
6449///
6450/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
6451pub fn pattern_matches(pattern: &str, path: &str) -> bool {
6452    if pattern.is_empty() || path.is_empty() {
6453        return false;
6454    }
6455    let pattern = pattern.trim_matches('/').split('/').collect::<Vec<_>>();
6456    let path = path
6457        .split('?')
6458        .next()
6459        .unwrap_or(path)
6460        .trim_matches('/')
6461        .split('/')
6462        .collect::<Vec<_>>();
6463    pattern.len() == path.len()
6464        && pattern.iter().zip(path).all(|(expected, actual)| {
6465            expected.starts_with(':') || *expected == "*" || *expected == actual
6466        })
6467}
6468
6469/// Scores a route pattern by its number of literal segments.
6470///
6471/// # Parameters
6472///
6473/// - `pattern`: Input consumed by [`crate::runtime::dynamic::route_pattern_specificity_score`].
6474///
6475/// # Returns
6476///
6477/// Returns the computed value produced by [`crate::runtime::dynamic::route_pattern_specificity_score`].
6478///
6479/// # Performance
6480///
6481/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::route_pattern_specificity_score`].
6482///
6483/// # Examples
6484///
6485/// ```rust,ignore
6486/// let _result = pharos_native::runtime::dynamic::route_pattern_specificity_score(todo!());
6487/// ```
6488///
6489/// # Related
6490///
6491/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
6492pub fn route_pattern_specificity_score(pattern: &str) -> usize {
6493    pattern
6494        .trim_matches('/')
6495        .split('/')
6496        .filter(|segment| !segment.is_empty() && !segment.starts_with(':') && *segment != "*")
6497        .count()
6498}
6499
6500/// Resolves a route fixture below an app root with traversal protection.
6501///
6502/// # Parameters
6503///
6504/// - `artifact_dir`: Input consumed by [`crate::runtime::dynamic::resolve_route_fixture_path`].
6505/// - `app_id`: Input consumed by [`crate::runtime::dynamic::resolve_route_fixture_path`].
6506///
6507/// # Returns
6508///
6509/// Returns [`Some`] when [`crate::runtime::dynamic::resolve_route_fixture_path`] can produce a value and [`None`] otherwise.
6510///
6511/// # Performance
6512///
6513/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::resolve_route_fixture_path`].
6514///
6515/// # Examples
6516///
6517/// ```rust,ignore
6518/// let _result = pharos_native::runtime::dynamic::resolve_route_fixture_path(std::path::Path::new("build/dev_docs"), todo!());
6519/// ```
6520///
6521/// # Related
6522///
6523/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
6524pub fn resolve_route_fixture_path(artifact_dir: &Path, app_id: &str) -> Option<String> {
6525    if artifact_dir.as_os_str().is_empty() || app_id.is_empty() || app_id.contains(['/', '\\']) {
6526        return None;
6527    }
6528    let candidates = [
6529        artifact_dir.join("routes.json"),
6530        artifact_dir.join("data/contracts/routes.json"),
6531        artifact_dir.join("apps").join(app_id).join("routes.json"),
6532    ];
6533    candidates
6534        .into_iter()
6535        .find(|path| path.is_file())
6536        .map(|path| path.to_string_lossy().into_owned())
6537}
6538
6539/// Releases an owned dynamic route match value.
6540///
6541/// # Parameters
6542///
6543/// - `_value`: Input consumed by [`crate::runtime::dynamic::dynamic_dispatch_free_match`].
6544///
6545/// # Returns
6546///
6547/// Returns after [`crate::runtime::dynamic::dynamic_dispatch_free_match`] applies its in-place effect.
6548///
6549/// # Examples
6550///
6551/// ```rust,ignore
6552/// let _result = pharos_native::runtime::dynamic::dynamic_dispatch_free_match(todo!());
6553/// ```
6554///
6555/// # Related
6556///
6557/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
6558pub fn dynamic_dispatch_free_match<T>(_value: Option<T>) {}
6559
6560/// Validates a webhook endpoint identifier against the framework-owned route shape.
6561///
6562/// # Parameters
6563///
6564/// - `path`: Input consumed by [`crate::runtime::dynamic::framework_webhook_endpoint_id`].
6565///
6566/// # Returns
6567///
6568/// Returns [`Some`] when [`crate::runtime::dynamic::framework_webhook_endpoint_id`] can produce a value and [`None`] otherwise.
6569///
6570/// # Performance
6571///
6572/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::framework_webhook_endpoint_id`].
6573///
6574/// # Examples
6575///
6576/// ```rust,ignore
6577/// let _result = pharos_native::runtime::dynamic::framework_webhook_endpoint_id(std::path::Path::new("fixture.json"));
6578/// ```
6579///
6580/// # Related
6581///
6582/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
6583pub fn framework_webhook_endpoint_id(path: &str) -> Option<String> {
6584    path.strip_prefix("/_pharos/webhooks/")
6585        .map(|rest| rest.strip_suffix('/').unwrap_or(rest))
6586        .filter(|value| {
6587            !value.is_empty()
6588                && value.len() <= 128
6589                && !value.contains('/')
6590                && value
6591                    .bytes()
6592                    .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'-')
6593        })
6594        .map(ToOwned::to_owned)
6595}
6596
6597/// Resolves the app-owned integration fixture without leaving the app root.
6598///
6599/// # Parameters
6600///
6601/// - `app`: Input consumed by [`crate::runtime::dynamic::integration_fixture_path`].
6602///
6603/// # Returns
6604///
6605/// Returns [`Some`] when [`crate::runtime::dynamic::integration_fixture_path`] can produce a value and [`None`] otherwise.
6606///
6607/// # Performance
6608///
6609/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::integration_fixture_path`].
6610///
6611/// # Examples
6612///
6613/// ```rust,ignore
6614/// let _result = pharos_native::runtime::dynamic::integration_fixture_path(todo!());
6615/// ```
6616///
6617/// # Related
6618///
6619/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
6620pub fn integration_fixture_path(app: &AppRuntime) -> Option<String> {
6621    let root = Path::new(app.app_root.as_deref()?);
6622    let value = fs::read_to_string(root.join("pharos.app.json"))
6623        .ok()
6624        .and_then(|text| serde_json::from_str::<Value>(&text).ok())?;
6625    let relative = value
6626        .get("contracts")
6627        .and_then(Value::as_object)?
6628        .get("integration_contract")
6629        .and_then(Value::as_str)?;
6630    if relative.is_empty()
6631        || Path::new(relative).is_absolute()
6632        || relative.split('/').any(|segment| segment == "..")
6633    {
6634        return None;
6635    }
6636    Some(root.join(relative).to_string_lossy().into_owned())
6637}
6638
6639/// Validates a state-backed capability for the current user and tenant.
6640///
6641/// # Parameters
6642///
6643/// - `state_root`: Input consumed by [`crate::runtime::dynamic::generated_surface_user_has_capability`].
6644/// - `_app`: Input consumed by [`crate::runtime::dynamic::generated_surface_user_has_capability`].
6645/// - `user_id`: Input consumed by [`crate::runtime::dynamic::generated_surface_user_has_capability`].
6646/// - `tenant_id`: Input consumed by [`crate::runtime::dynamic::generated_surface_user_has_capability`].
6647/// - `capability`: Input consumed by [`crate::runtime::dynamic::generated_surface_user_has_capability`].
6648///
6649/// # Returns
6650///
6651/// Returns `true` when the condition checked by [`crate::runtime::dynamic::generated_surface_user_has_capability`] holds and `false` otherwise.
6652///
6653/// # Performance
6654///
6655/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::generated_surface_user_has_capability`].
6656///
6657/// # Examples
6658///
6659/// ```rust,ignore
6660/// let _result = pharos_native::runtime::dynamic::generated_surface_user_has_capability(todo!(), todo!(), 1, todo!(), todo!());
6661/// ```
6662///
6663/// # Related
6664///
6665/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
6666pub fn generated_surface_user_has_capability(
6667    state_root: &Value,
6668    _app: &AppRuntime,
6669    user_id: i64,
6670    tenant_id: i64,
6671    capability: &str,
6672) -> bool {
6673    user_id > 0
6674        && tenant_id > 0
6675        && !capability.is_empty()
6676        && state_root
6677            .get("capabilities")
6678            .and_then(Value::as_array)
6679            .is_some_and(|rows| {
6680                rows.iter().any(|row| {
6681                    row.get("user_id").and_then(Value::as_i64) == Some(user_id)
6682                        && row.get("tenant_id").and_then(Value::as_i64) == Some(tenant_id)
6683                        && row.get("capability").and_then(Value::as_str) == Some(capability)
6684                })
6685            })
6686}
6687
6688/// Produces a bounded JSON error envelope for direct HTTP handling.
6689///
6690/// # Parameters
6691///
6692/// - `status`: Input consumed by [`crate::runtime::dynamic::write_error_envelope`].
6693/// - `status_text`: Input consumed by [`crate::runtime::dynamic::write_error_envelope`].
6694/// - `error_type`: Input consumed by [`crate::runtime::dynamic::write_error_envelope`].
6695/// - `error_code`: Input consumed by [`crate::runtime::dynamic::write_error_envelope`].
6696/// - `message`: Input consumed by [`crate::runtime::dynamic::write_error_envelope`].
6697/// - `actor_scope`: Input consumed by [`crate::runtime::dynamic::write_error_envelope`].
6698/// - `tenant_scope`: Input consumed by [`crate::runtime::dynamic::write_error_envelope`].
6699///
6700/// # Returns
6701///
6702/// Returns the computed value produced by [`crate::runtime::dynamic::write_error_envelope`].
6703///
6704/// # Examples
6705///
6706/// ```rust,ignore
6707/// let _result = pharos_native::runtime::dynamic::write_error_envelope(400, "400 Bad Request", todo!(), todo!(), "example", todo!(), todo!());
6708/// ```
6709///
6710/// # Related
6711///
6712/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
6713pub fn write_error_envelope(
6714    status: u16,
6715    status_text: &str,
6716    error_type: &str,
6717    error_code: &str,
6718    message: &str,
6719    actor_scope: Option<&str>,
6720    tenant_scope: Option<&str>,
6721) -> String {
6722    serde_json::json!({"status":status,"status_text":status_text,"error_type":error_type,"error_code":error_code,"message":message,"actor_scope":actor_scope.unwrap_or_default(),"tenant_scope":tenant_scope.unwrap_or_default()}).to_string()
6723}
6724
6725/// Applies a route operation response directly as a JSON document.
6726///
6727/// # Parameters
6728///
6729/// - `surface_root`: Input consumed by [`crate::runtime::dynamic::write_generated_surface_api_schema_response`].
6730///
6731/// # Returns
6732///
6733/// Returns [`Some`] when [`crate::runtime::dynamic::write_generated_surface_api_schema_response`] can produce a value and [`None`] otherwise.
6734///
6735/// # Examples
6736///
6737/// ```rust,ignore
6738/// let _result = pharos_native::runtime::dynamic::write_generated_surface_api_schema_response(todo!());
6739/// ```
6740///
6741/// # Related
6742///
6743/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
6744pub fn write_generated_surface_api_schema_response(surface_root: &Value) -> Option<String> {
6745    serde_json::to_string(surface_root).ok()
6746}
6747
6748/// Builds one generated API item response with a bounded status envelope.
6749///
6750/// # Parameters
6751///
6752/// - `status`: Input consumed by [`crate::runtime::dynamic::write_generated_surface_api_item_response`].
6753/// - `status_text`: Input consumed by [`crate::runtime::dynamic::write_generated_surface_api_item_response`].
6754/// - `surface_root`: Input consumed by [`crate::runtime::dynamic::write_generated_surface_api_item_response`].
6755/// - `item_root`: Input consumed by [`crate::runtime::dynamic::write_generated_surface_api_item_response`].
6756///
6757/// # Returns
6758///
6759/// Returns [`Some`] when [`crate::runtime::dynamic::write_generated_surface_api_item_response`] can produce a value and [`None`] otherwise.
6760///
6761/// # Examples
6762///
6763/// ```rust,ignore
6764/// let _result = pharos_native::runtime::dynamic::write_generated_surface_api_item_response(400, "400 Bad Request", todo!(), todo!());
6765/// ```
6766///
6767/// # Related
6768///
6769/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
6770pub fn write_generated_surface_api_item_response(
6771    status: u16,
6772    status_text: &str,
6773    surface_root: &Value,
6774    item_root: &Value,
6775) -> Option<String> {
6776    serde_json::to_string(&serde_json::json!({"status":status,"status_text":status_text,"surface":surface_root,"item":item_root})).ok()
6777}
6778
6779/// Builds a generated API list response from an array-valued source document.
6780///
6781/// # Parameters
6782///
6783/// - `items`: Input consumed by [`crate::runtime::dynamic::write_generated_surface_api_list_response`].
6784/// - `raw_query`: Input consumed by [`crate::runtime::dynamic::write_generated_surface_api_list_response`].
6785///
6786/// # Returns
6787///
6788/// Returns [`Some`] when [`crate::runtime::dynamic::write_generated_surface_api_list_response`] can produce a value and [`None`] otherwise.
6789///
6790/// # Performance
6791///
6792/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::write_generated_surface_api_list_response`].
6793///
6794/// # Examples
6795///
6796/// ```rust,ignore
6797/// let _result = pharos_native::runtime::dynamic::write_generated_surface_api_list_response(todo!(), todo!());
6798/// ```
6799///
6800/// # Related
6801///
6802/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
6803pub fn write_generated_surface_api_list_response(
6804    items: &[Value],
6805    raw_query: Option<&str>,
6806) -> Option<String> {
6807    let query = raw_query.unwrap_or_default().to_ascii_lowercase();
6808    let filtered = items
6809        .iter()
6810        .filter(|item| query.is_empty() || item.to_string().to_ascii_lowercase().contains(&query))
6811        .cloned()
6812        .collect::<Vec<_>>();
6813    serde_json::to_string(&serde_json::json!({"items":filtered,"count":filtered.len()})).ok()
6814}
6815
6816/// Reads a numeric object field with a bounded fallback.
6817///
6818/// # Parameters
6819///
6820/// - `object`: Input consumed by [`crate::runtime::dynamic::mut_json_obj_get_long_default`].
6821/// - `key`: Input consumed by [`crate::runtime::dynamic::mut_json_obj_get_long_default`].
6822/// - `default_value`: Input consumed by [`crate::runtime::dynamic::mut_json_obj_get_long_default`].
6823///
6824/// # Returns
6825///
6826/// Returns the computed value produced by [`crate::runtime::dynamic::mut_json_obj_get_long_default`].
6827///
6828/// # Examples
6829///
6830/// ```rust,ignore
6831/// let _result = pharos_native::runtime::dynamic::mut_json_obj_get_long_default(todo!(), "key", todo!());
6832/// ```
6833///
6834/// # Related
6835///
6836/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
6837pub fn mut_json_obj_get_long_default(object: &Value, key: &str, default_value: i64) -> i64 {
6838    object
6839        .get(key)
6840        .and_then(Value::as_i64)
6841        .or_else(|| {
6842            object
6843                .get(key)
6844                .and_then(Value::as_str)
6845                .and_then(|value| value.parse().ok())
6846        })
6847        .unwrap_or(default_value)
6848}
6849
6850/// Compares two numeric JSON values without coercing arbitrary objects.
6851///
6852/// # Parameters
6853///
6854/// - `value`: Input consumed by [`crate::runtime::dynamic::value_long_equals_runtime_local`].
6855/// - `expected`: Input consumed by [`crate::runtime::dynamic::value_long_equals_runtime_local`].
6856///
6857/// # Returns
6858///
6859/// Returns `true` when the condition checked by [`crate::runtime::dynamic::value_long_equals_runtime_local`] holds and `false` otherwise.
6860///
6861/// # Examples
6862///
6863/// ```rust,ignore
6864/// let _result = pharos_native::runtime::dynamic::value_long_equals_runtime_local("value", "value");
6865/// ```
6866///
6867/// # Related
6868///
6869/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
6870pub fn value_long_equals_runtime_local(value: &Value, expected: i64) -> bool {
6871    mut_json_obj_get_long_default(value, "value", i64::MIN) == expected
6872        || value.as_i64() == Some(expected)
6873}
6874
6875/// Revokes a matching session cookie from a persisted state document.
6876///
6877/// # Parameters
6878///
6879/// - `state_path`: Input consumed by [`crate::runtime::dynamic::revoke_session_by_cookie`].
6880/// - `cookie`: Input consumed by [`crate::runtime::dynamic::revoke_session_by_cookie`].
6881///
6882/// # Returns
6883///
6884/// Returns the computed value produced by [`crate::runtime::dynamic::revoke_session_by_cookie`].
6885///
6886/// # Performance
6887///
6888/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::revoke_session_by_cookie`].
6889///
6890/// # Examples
6891///
6892/// ```rust,ignore
6893/// let _result = pharos_native::runtime::dynamic::revoke_session_by_cookie(todo!(), todo!());
6894/// ```
6895///
6896/// # Related
6897///
6898/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
6899pub fn revoke_session_by_cookie(state_path: &Path, cookie: &str) -> i32 {
6900    if cookie.is_empty() {
6901        return 64;
6902    }
6903    let Ok(mut root) = fs::read_to_string(state_path)
6904        .ok()
6905        .and_then(|text| serde_json::from_str::<Value>(&text).ok())
6906        .ok_or(66)
6907    else {
6908        return 66;
6909    };
6910    let Some(rows) = root.get_mut("sessions").and_then(Value::as_array_mut) else {
6911        return 69;
6912    };
6913    for row in rows {
6914        if row.get("cookie").and_then(Value::as_str) == Some(cookie)
6915            && let Some(object) = row.as_object_mut()
6916        {
6917            object.insert("revoked_at".to_owned(), Value::String(now_iso_utc()));
6918        }
6919    }
6920    super::fs::write_text_file(
6921        state_path,
6922        &serde_json::to_string(&root).unwrap_or_default(),
6923    )
6924}
6925
6926/// Serves a bounded route fixture file directly as response bytes.
6927///
6928/// # Parameters
6929///
6930/// - `root`: Input consumed by [`crate::runtime::dynamic::dispatch_response_file_for_request`].
6931/// - `method`: Input consumed by [`crate::runtime::dynamic::dispatch_response_file_for_request`].
6932/// - `path`: Input consumed by [`crate::runtime::dynamic::dispatch_response_file_for_request`].
6933///
6934/// # Returns
6935///
6936/// Returns [`Some`] when [`crate::runtime::dynamic::dispatch_response_file_for_request`] can produce a value and [`None`] otherwise.
6937///
6938/// # Performance
6939///
6940/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::dispatch_response_file_for_request`].
6941///
6942/// # Examples
6943///
6944/// ```rust,ignore
6945/// let _result = pharos_native::runtime::dynamic::dispatch_response_file_for_request(std::path::Path::new("."), todo!(), std::path::Path::new("fixture.json"));
6946/// ```
6947///
6948/// # Related
6949///
6950/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
6951pub fn dispatch_response_file_for_request(root: &Path, method: &str, path: &str) -> Option<String> {
6952    if !method.eq_ignore_ascii_case("GET") && !method.eq_ignore_ascii_case("HEAD") {
6953        return None;
6954    }
6955    let relative = path.trim_start_matches('/');
6956    if relative.is_empty() || relative.split('/').any(|segment| segment == "..") {
6957        return None;
6958    }
6959    let target = root.join(relative);
6960    let text = fs::read_to_string(target).ok()?;
6961    (text.len() <= 16 * 1024 * 1024).then_some(text)
6962}
6963
6964/// Finds one framework webhook endpoint in a declarative integration document.
6965///
6966/// # Parameters
6967///
6968/// - `integration_root`: Input consumed by [`crate::runtime::dynamic::find_webhook_endpoint`].
6969/// - `endpoint_id`: Input consumed by [`crate::runtime::dynamic::find_webhook_endpoint`].
6970///
6971/// # Returns
6972///
6973/// Returns [`Some`] when [`crate::runtime::dynamic::find_webhook_endpoint`] can produce a value and [`None`] otherwise.
6974///
6975/// # Performance
6976///
6977/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::find_webhook_endpoint`].
6978///
6979/// # Examples
6980///
6981/// ```rust,ignore
6982/// let _result = pharos_native::runtime::dynamic::find_webhook_endpoint(todo!(), "webhook");
6983/// ```
6984///
6985/// # Related
6986///
6987/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
6988pub fn find_webhook_endpoint<'a>(
6989    integration_root: &'a Value,
6990    endpoint_id: &str,
6991) -> Option<&'a Value> {
6992    if endpoint_id.is_empty() {
6993        return None;
6994    }
6995    integration_root
6996        .get("webhook_endpoints")
6997        .and_then(Value::as_array)
6998        .and_then(|rows| {
6999            rows.iter()
7000                .find(|row| row.get("endpoint_id").and_then(Value::as_str) == Some(endpoint_id))
7001        })
7002}
7003
7004/// Verifies a declarative webhook signature and timestamp without exposing secret material.
7005///
7006/// # Parameters
7007///
7008/// - `endpoint`: Input consumed by [`crate::runtime::dynamic::webhook_request_signature_valid`].
7009/// - `request`: Input consumed by [`crate::runtime::dynamic::webhook_request_signature_valid`].
7010/// - `payload_json`: Input consumed by [`crate::runtime::dynamic::webhook_request_signature_valid`].
7011///
7012/// # Returns
7013///
7014/// Returns `true` when the condition checked by [`crate::runtime::dynamic::webhook_request_signature_valid`] holds and `false` otherwise.
7015///
7016/// # Examples
7017///
7018/// ```rust,ignore
7019/// let _result = pharos_native::runtime::dynamic::webhook_request_signature_valid("127.0.0.1:8080", todo!(), todo!());
7020/// ```
7021///
7022/// # Related
7023///
7024/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
7025pub fn webhook_request_signature_valid(
7026    endpoint: &Value,
7027    request: &HttpRequest,
7028    payload_json: &str,
7029) -> bool {
7030    let Some(signature) = endpoint.get("signature").and_then(Value::as_object) else {
7031        return false;
7032    };
7033    if signature.get("mode").and_then(Value::as_str) != Some("hmac_sha256") {
7034        return false;
7035    }
7036    let Some(signature_header) = signature.get("header").and_then(Value::as_str) else {
7037        return false;
7038    };
7039    let Some(timestamp_header) = signature.get("timestamp_header").and_then(Value::as_str) else {
7040        return false;
7041    };
7042    let Some(secret_env) = signature.get("secret_env").and_then(Value::as_str) else {
7043        return false;
7044    };
7045    let max_age = signature
7046        .get("max_age_seconds")
7047        .and_then(Value::as_u64)
7048        .filter(|value| *value > 0)
7049        .unwrap_or(300);
7050    let Some(timestamp_text) = request_header_value(request, timestamp_header) else {
7051        return false;
7052    };
7053    let Some(timestamp) = timestamp_text.parse::<u64>().ok() else {
7054        return false;
7055    };
7056    let now = SystemTime::now()
7057        .duration_since(UNIX_EPOCH)
7058        .map(|value| value.as_secs())
7059        .unwrap_or(0);
7060    if now.abs_diff(timestamp) > max_age {
7061        return false;
7062    }
7063    let Some(provided) = request_header_value(request, signature_header) else {
7064        return false;
7065    };
7066    let Some(secret) = env::var(secret_env).ok().filter(|value| !value.is_empty()) else {
7067        return false;
7068    };
7069    let message = format!("{timestamp_text}.{payload_json}");
7070    let expected = format!(
7071        "sha256={}",
7072        hex_digest(&hmac_sha256(secret.as_bytes(), message.as_bytes()))
7073    );
7074    constant_time_equal(provided.as_bytes(), expected.as_bytes())
7075}
7076
7077/// Records one webhook receipt in the app state document with bounded payload size.
7078///
7079/// # Parameters
7080///
7081/// - `state_path`: Input consumed by [`crate::runtime::dynamic::append_webhook_receipt`].
7082/// - `endpoint_id`: Input consumed by [`crate::runtime::dynamic::append_webhook_receipt`].
7083/// - `verification_status`: Input consumed by [`crate::runtime::dynamic::append_webhook_receipt`].
7084/// - `payload_json`: Input consumed by [`crate::runtime::dynamic::append_webhook_receipt`].
7085///
7086/// # Returns
7087///
7088/// Returns the computed value produced by [`crate::runtime::dynamic::append_webhook_receipt`].
7089///
7090/// # Performance
7091///
7092/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::append_webhook_receipt`].
7093///
7094/// # Examples
7095///
7096/// ```rust,ignore
7097/// let _result = pharos_native::runtime::dynamic::append_webhook_receipt(todo!(), "webhook", todo!(), todo!());
7098/// ```
7099///
7100/// # Related
7101///
7102/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
7103pub fn append_webhook_receipt(
7104    state_path: &Path,
7105    endpoint_id: &str,
7106    verification_status: &str,
7107    payload_json: &str,
7108) -> i32 {
7109    if endpoint_id.is_empty() || verification_status.is_empty() || payload_json.len() > 1_048_576 {
7110        return 64;
7111    }
7112    let Ok(mut root) = fs::read_to_string(state_path)
7113        .ok()
7114        .and_then(|text| serde_json::from_str::<Value>(&text).ok())
7115        .ok_or(66)
7116    else {
7117        return 66;
7118    };
7119    let Some(rows) = root
7120        .as_object_mut()
7121        .map(|object| {
7122            object
7123                .entry("webhook_receipts".to_owned())
7124                .or_insert_with(|| Value::Array(Vec::new()))
7125        })
7126        .and_then(Value::as_array_mut)
7127    else {
7128        return 69;
7129    };
7130    rows.push(serde_json::json!({"endpoint_id":endpoint_id,"verification_status":verification_status,"payload":payload_json,"created_at":now_iso_utc()}));
7131    super::fs::write_text_file(
7132        state_path,
7133        &serde_json::to_string(&root).unwrap_or_default(),
7134    )
7135}
7136
7137/// Builds the HMAC signature header for an outbound webhook payload.
7138///
7139/// # Parameters
7140///
7141/// - `secret_env`: Input consumed by [`crate::runtime::dynamic::webhook_signature_header_value_dup_logic_local`].
7142/// - `timestamp_text`: Input consumed by [`crate::runtime::dynamic::webhook_signature_header_value_dup_logic_local`].
7143/// - `payload_json`: Input consumed by [`crate::runtime::dynamic::webhook_signature_header_value_dup_logic_local`].
7144///
7145/// # Returns
7146///
7147/// Returns [`Some`] when [`crate::runtime::dynamic::webhook_signature_header_value_dup_logic_local`] can produce a value and [`None`] otherwise.
7148///
7149/// # Examples
7150///
7151/// ```rust,ignore
7152/// let _result = pharos_native::runtime::dynamic::webhook_signature_header_value_dup_logic_local(todo!(), todo!(), todo!());
7153/// ```
7154///
7155/// # Related
7156///
7157/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
7158pub fn webhook_signature_header_value_dup_logic_local(
7159    secret_env: &str,
7160    timestamp_text: &str,
7161    payload_json: &str,
7162) -> Option<String> {
7163    if secret_env.is_empty() || timestamp_text.is_empty() || payload_json.is_empty() {
7164        return None;
7165    }
7166    let secret = env::var(secret_env)
7167        .ok()
7168        .filter(|value| !value.is_empty())?;
7169    let message = format!("{timestamp_text}.{payload_json}");
7170    Some(format!(
7171        "sha256={}",
7172        hex_digest(&hmac_sha256(secret.as_bytes(), message.as_bytes()))
7173    ))
7174}
7175
7176/// Appends bytes to a bounded mutable byte buffer.
7177///
7178/// # Parameters
7179///
7180/// - `output`: Input consumed by [`crate::runtime::dynamic::append_bytes_logic_local`].
7181/// - `bytes`: Input consumed by [`crate::runtime::dynamic::append_bytes_logic_local`].
7182///
7183/// # Returns
7184///
7185/// Returns `true` when the condition checked by [`crate::runtime::dynamic::append_bytes_logic_local`] holds and `false` otherwise.
7186///
7187/// # Examples
7188///
7189/// ```rust,ignore
7190/// let _result = pharos_native::runtime::dynamic::append_bytes_logic_local(todo!(), b"");
7191/// ```
7192///
7193/// # Related
7194///
7195/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
7196pub fn append_bytes_logic_local(output: &mut Vec<u8>, bytes: &[u8]) -> bool {
7197    if output.len().saturating_add(bytes.len()) > 16 * 1024 * 1024 {
7198        return false;
7199    }
7200    output.extend_from_slice(bytes);
7201    true
7202}
7203
7204/// Reads one named scalar from a JSON object as owned text.
7205///
7206/// # Parameters
7207///
7208/// - `object`: Input consumed by [`crate::runtime::dynamic::input_field_value_dup_logic_local`].
7209/// - `field_name`: Input consumed by [`crate::runtime::dynamic::input_field_value_dup_logic_local`].
7210///
7211/// # Returns
7212///
7213/// Returns [`Some`] when [`crate::runtime::dynamic::input_field_value_dup_logic_local`] can produce a value and [`None`] otherwise.
7214///
7215/// # Examples
7216///
7217/// ```rust,ignore
7218/// let _result = pharos_native::runtime::dynamic::input_field_value_dup_logic_local(todo!(), todo!());
7219/// ```
7220///
7221/// # Related
7222///
7223/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
7224pub fn input_field_value_dup_logic_local(object: &Value, field_name: &str) -> Option<String> {
7225    object
7226        .get(field_name)
7227        .map(value_to_text)
7228        .filter(|value| !value.is_empty())
7229}
7230
7231/// Inserts one validated field value into a mutable JSON object.
7232///
7233/// # Parameters
7234///
7235/// - `object`: Input consumed by [`crate::runtime::dynamic::mut_object_field_logic_local`].
7236/// - `field_name`: Input consumed by [`crate::runtime::dynamic::mut_object_field_logic_local`].
7237/// - `value`: Input consumed by [`crate::runtime::dynamic::mut_object_field_logic_local`].
7238///
7239/// # Returns
7240///
7241/// Returns `true` when the condition checked by [`crate::runtime::dynamic::mut_object_field_logic_local`] holds and `false` otherwise.
7242///
7243/// # Examples
7244///
7245/// ```rust,ignore
7246/// let _result = pharos_native::runtime::dynamic::mut_object_field_logic_local(todo!(), todo!(), "value");
7247/// ```
7248///
7249/// # Related
7250///
7251/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
7252pub fn mut_object_field_logic_local(
7253    object: &mut Map<String, Value>,
7254    field_name: &str,
7255    value: Value,
7256) -> bool {
7257    if field_name.is_empty() || field_name.len() > 256 || field_name.contains('\0') {
7258        return false;
7259    }
7260    object.insert(field_name.to_owned(), value);
7261    true
7262}
7263
7264/// Adds the generated-surface context binding to a mutable object.
7265///
7266/// # Parameters
7267///
7268/// - `object`: Input consumed by [`crate::runtime::dynamic::dynamic_generated_admin_context_binding_mut`].
7269/// - `binding`: Input consumed by [`crate::runtime::dynamic::dynamic_generated_admin_context_binding_mut`].
7270///
7271/// # Returns
7272///
7273/// Returns `true` when the condition checked by [`crate::runtime::dynamic::dynamic_generated_admin_context_binding_mut`] holds and `false` otherwise.
7274///
7275/// # Examples
7276///
7277/// ```rust,ignore
7278/// let _result = pharos_native::runtime::dynamic::dynamic_generated_admin_context_binding_mut(todo!(), todo!());
7279/// ```
7280///
7281/// # Related
7282///
7283/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
7284pub fn dynamic_generated_admin_context_binding_mut(
7285    object: &mut Map<String, Value>,
7286    binding: &Value,
7287) -> bool {
7288    let Some(binding) = binding.as_object() else {
7289        return false;
7290    };
7291    if binding.get("param").and_then(Value::as_str).is_none() {
7292        return false;
7293    }
7294    object.insert("context_binding".to_owned(), Value::Object(binding.clone()));
7295    true
7296}
7297
7298/// Returns the current Unix epoch as decimal text.
7299///
7300/// # Returns
7301///
7302/// Returns the computed value produced by [`crate::runtime::dynamic::now_unix_epoch_text_logic_local`].
7303///
7304/// # Examples
7305///
7306/// ```rust,ignore
7307/// let _result = pharos_native::runtime::dynamic::now_unix_epoch_text_logic_local();
7308/// ```
7309///
7310/// # Related
7311///
7312/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
7313pub fn now_unix_epoch_text_logic_local() -> String {
7314    SystemTime::now()
7315        .duration_since(UNIX_EPOCH)
7316        .map(|value| value.as_secs().to_string())
7317        .unwrap_or_else(|_| "0".to_owned())
7318}
7319
7320/// Parses and validates an HTTP URL into scheme, host, and optional port.
7321///
7322/// # Parameters
7323///
7324/// - `url`: Input consumed by [`crate::runtime::dynamic::parse_http_url_logic_local`].
7325/// - `String`: Input consumed by [`crate::runtime::dynamic::parse_http_url_logic_local`].
7326/// - `Option<u16>`: Input consumed by [`crate::runtime::dynamic::parse_http_url_logic_local`].
7327///
7328/// # Returns
7329///
7330/// Returns [`Some`] when [`crate::runtime::dynamic::parse_http_url_logic_local`] can produce a value and [`None`] otherwise.
7331///
7332/// # Performance
7333///
7334/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::parse_http_url_logic_local`].
7335///
7336/// # Examples
7337///
7338/// ```rust,ignore
7339/// let _result = pharos_native::runtime::dynamic::parse_http_url_logic_local(todo!(), todo!(), todo!());
7340/// ```
7341///
7342/// # Related
7343///
7344/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
7345pub fn parse_http_url_logic_local(url: &str) -> Option<(String, String, Option<u16>)> {
7346    let (scheme, rest) = url.split_once("://")?;
7347    if !matches!(scheme.to_ascii_lowercase().as_str(), "http" | "https")
7348        || rest.is_empty()
7349        || rest.contains(['/', '?', '#', '\\', '\0'])
7350    {
7351        return None;
7352    }
7353    let (host, port) = if let Some((host, port)) = rest.split_once(':') {
7354        (host, Some(port.parse::<u16>().ok()?))
7355    } else {
7356        (rest, None)
7357    };
7358    if host.is_empty() || host.bytes().any(|byte| byte < 0x20 || byte == 0x7f) {
7359        return None;
7360    }
7361    Some((scheme.to_ascii_lowercase(), host.to_owned(), port))
7362}
7363
7364/// Compares a JSON record field with a bounded text value.
7365///
7366/// # Parameters
7367///
7368/// - `record`: Input consumed by [`crate::runtime::dynamic::record_field_matches_text_value_logic_local`].
7369/// - `field_name`: Input consumed by [`crate::runtime::dynamic::record_field_matches_text_value_logic_local`].
7370/// - `expected`: Input consumed by [`crate::runtime::dynamic::record_field_matches_text_value_logic_local`].
7371///
7372/// # Returns
7373///
7374/// Returns `true` when the condition checked by [`crate::runtime::dynamic::record_field_matches_text_value_logic_local`] holds and `false` otherwise.
7375///
7376/// # Examples
7377///
7378/// ```rust,ignore
7379/// let _result = pharos_native::runtime::dynamic::record_field_matches_text_value_logic_local(todo!(), todo!(), "value");
7380/// ```
7381///
7382/// # Related
7383///
7384/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
7385pub fn record_field_matches_text_value_logic_local(
7386    record: &Value,
7387    field_name: &str,
7388    expected: &str,
7389) -> bool {
7390    !field_name.is_empty() && record.get(field_name).map(value_to_text).as_deref() == Some(expected)
7391}
7392
7393/// Performs an ASCII case-insensitive string comparison.
7394///
7395/// # Parameters
7396///
7397/// - `left`: Input consumed by [`crate::runtime::dynamic::streq_casefold_management_local`].
7398/// - `right`: Input consumed by [`crate::runtime::dynamic::streq_casefold_management_local`].
7399///
7400/// # Returns
7401///
7402/// Returns `true` when the condition checked by [`crate::runtime::dynamic::streq_casefold_management_local`] holds and `false` otherwise.
7403///
7404/// # Examples
7405///
7406/// ```rust,ignore
7407/// let _result = pharos_native::runtime::dynamic::streq_casefold_management_local(todo!(), todo!());
7408/// ```
7409///
7410/// # Related
7411///
7412/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
7413pub fn streq_casefold_management_local(left: &str, right: &str) -> bool {
7414    left.eq_ignore_ascii_case(right)
7415}
7416
7417/// Ensures the managed media subtree exists below an app root.
7418///
7419/// # Parameters
7420///
7421/// - `app_root`: Input consumed by [`crate::runtime::dynamic::ensure_media_subtree`].
7422/// - `business_slug`: Input consumed by [`crate::runtime::dynamic::ensure_media_subtree`].
7423///
7424/// # Returns
7425///
7426/// Returns [`Some`] when [`crate::runtime::dynamic::ensure_media_subtree`] can produce a value and [`None`] otherwise.
7427///
7428/// # Examples
7429///
7430/// ```rust,ignore
7431/// let _result = pharos_native::runtime::dynamic::ensure_media_subtree(std::path::Path::new("apps/dev_docs"), "demo");
7432/// ```
7433///
7434/// # Related
7435///
7436/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
7437pub fn ensure_media_subtree(app_root: &Path, business_slug: &str) -> Option<String> {
7438    if app_root.as_os_str().is_empty()
7439        || business_slug.is_empty()
7440        || business_slug.contains(['/', '\\', '\0'])
7441    {
7442        return None;
7443    }
7444    let path = super::config::app_media_root(app_root)?
7445        .join("businesses")
7446        .join(business_slug);
7447    super::fs::ensure_directory(&path)
7448        .eq(&0)
7449        .then(|| path.to_string_lossy().into_owned())
7450}
7451
7452/// Resolves a validated media URL to a file below the app media tree.
7453///
7454/// # Parameters
7455///
7456/// - `app_root`: Input consumed by [`crate::runtime::dynamic::media_disk_path`].
7457/// - `business_slug`: Input consumed by [`crate::runtime::dynamic::media_disk_path`].
7458/// - `media_url`: Input consumed by [`crate::runtime::dynamic::media_disk_path`].
7459///
7460/// # Returns
7461///
7462/// Returns [`Some`] when [`crate::runtime::dynamic::media_disk_path`] can produce a value and [`None`] otherwise.
7463///
7464/// # Performance
7465///
7466/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::media_disk_path`].
7467///
7468/// # Examples
7469///
7470/// ```rust,ignore
7471/// let _result = pharos_native::runtime::dynamic::media_disk_path(std::path::Path::new("apps/dev_docs"), "demo", todo!());
7472/// ```
7473///
7474/// # Related
7475///
7476/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
7477pub fn media_disk_path(app_root: &Path, business_slug: &str, media_url: &str) -> Option<String> {
7478    if app_root.as_os_str().is_empty() || business_slug.is_empty() || media_url.is_empty() {
7479        return None;
7480    }
7481    let relative = media_url
7482        .strip_prefix("/media/")
7483        .or_else(|| media_url.strip_prefix("media/"))?;
7484    if relative.is_empty()
7485        || relative
7486            .split('/')
7487            .any(|segment| segment.is_empty() || segment == "..")
7488    {
7489        return None;
7490    }
7491    Some(
7492        super::config::app_media_root(app_root)?
7493            .join(business_slug)
7494            .join(relative)
7495            .to_string_lossy()
7496            .into_owned(),
7497    )
7498}
7499
7500/// Normalizes an uploaded filename to one safe basename.
7501///
7502/// # Parameters
7503///
7504/// - `filename`: Input consumed by [`crate::runtime::dynamic::safe_upload_filename`].
7505///
7506/// # Returns
7507///
7508/// Returns [`Some`] when [`crate::runtime::dynamic::safe_upload_filename`] can produce a value and [`None`] otherwise.
7509///
7510/// # Performance
7511///
7512/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::safe_upload_filename`].
7513///
7514/// # Examples
7515///
7516/// ```rust,ignore
7517/// let _result = pharos_native::runtime::dynamic::safe_upload_filename(todo!());
7518/// ```
7519///
7520/// # Related
7521///
7522/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
7523pub fn safe_upload_filename(filename: &str) -> Option<String> {
7524    let basename = filename.rsplit(['/', '\\']).next()?.trim();
7525    if basename.is_empty()
7526        || basename.len() > 255
7527        || basename == "."
7528        || basename == ".."
7529        || basename
7530            .bytes()
7531            .any(|byte| byte < 0x20 || byte == 0x7f || byte == 0)
7532    {
7533        return None;
7534    }
7535    Some(
7536        basename
7537            .chars()
7538            .filter(|character| {
7539                character.is_ascii_alphanumeric() || matches!(character, '.' | '-' | '_')
7540            })
7541            .collect(),
7542    )
7543}
7544
7545/// Returns the request form value from the already buffered request body.
7546///
7547/// # Parameters
7548///
7549/// - `request`: Input consumed by [`crate::runtime::dynamic::dynamic_form_value`].
7550/// - `field_name`: Input consumed by [`crate::runtime::dynamic::dynamic_form_value`].
7551///
7552/// # Returns
7553///
7554/// Returns [`Some`] when [`crate::runtime::dynamic::dynamic_form_value`] can produce a value and [`None`] otherwise.
7555///
7556/// # Examples
7557///
7558/// ```rust,ignore
7559/// let _result = pharos_native::runtime::dynamic::dynamic_form_value(todo!(), todo!());
7560/// ```
7561///
7562/// # Related
7563///
7564/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
7565pub fn dynamic_form_value(request: &HttpRequest, field_name: &str) -> Option<String> {
7566    request_form_value(request, field_name)
7567}
7568
7569/// Returns the value of a cookie field using bounded case-sensitive parsing.
7570///
7571/// # Parameters
7572///
7573/// - `cookie_header`: Input consumed by [`crate::runtime::dynamic::dynamic_cookie_value`].
7574/// - `name`: Input consumed by [`crate::runtime::dynamic::dynamic_cookie_value`].
7575///
7576/// # Returns
7577///
7578/// Returns [`Some`] when [`crate::runtime::dynamic::dynamic_cookie_value`] can produce a value and [`None`] otherwise.
7579///
7580/// # Examples
7581///
7582/// ```rust,ignore
7583/// let _result = pharos_native::runtime::dynamic::dynamic_cookie_value("session=abc", "name");
7584/// ```
7585///
7586/// # Related
7587///
7588/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
7589pub fn dynamic_cookie_value(cookie_header: &str, name: &str) -> Option<String> {
7590    cookie_value(Some(cookie_header), name)
7591}
7592
7593/// Extracts the CSRF token from the cookie header or creates a fresh token.
7594///
7595/// # Parameters
7596///
7597/// - `cookie_header`: Input consumed by [`crate::runtime::dynamic::csrf_token_for_response`].
7598///
7599/// # Returns
7600///
7601/// Returns the computed value produced by [`crate::runtime::dynamic::csrf_token_for_response`].
7602///
7603/// # Examples
7604///
7605/// ```rust,ignore
7606/// let _result = pharos_native::runtime::dynamic::csrf_token_for_response("session=abc");
7607/// ```
7608///
7609/// # Related
7610///
7611/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
7612pub fn csrf_token_for_response(cookie_header: Option<&str>) -> String {
7613    pharos_runtime_security::csrf_token_for_response(cookie_header)
7614}
7615
7616/// Generates a cryptographically sourced hexadecimal token, failing closed on entropy errors.
7617///
7618/// # Parameters
7619///
7620/// - `byte_count`: Input consumed by [`crate::runtime::dynamic::csrf_generate_random_hex`].
7621///
7622/// # Returns
7623///
7624/// Returns the computed value produced by [`crate::runtime::dynamic::csrf_generate_random_hex`].
7625///
7626/// # Examples
7627///
7628/// ```rust,ignore
7629/// let _result = pharos_native::runtime::dynamic::csrf_generate_random_hex(todo!());
7630/// ```
7631///
7632/// # Related
7633///
7634/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
7635pub fn csrf_generate_random_hex(byte_count: usize) -> String {
7636    pharos_runtime_security::csrf_generate_random_hex(byte_count)
7637}
7638
7639/// Returns the CSRF cookie token when present.
7640///
7641/// # Parameters
7642///
7643/// - `cookie_header`: Input consumed by [`crate::runtime::dynamic::csrf_token_from_cookie`].
7644///
7645/// # Returns
7646///
7647/// Returns [`Some`] when [`crate::runtime::dynamic::csrf_token_from_cookie`] can produce a value and [`None`] otherwise.
7648///
7649/// # Examples
7650///
7651/// ```rust,ignore
7652/// let _result = pharos_native::runtime::dynamic::csrf_token_from_cookie("session=abc");
7653/// ```
7654///
7655/// # Related
7656///
7657/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
7658pub fn csrf_token_from_cookie(cookie_header: Option<&str>) -> Option<String> {
7659    pharos_runtime_security::csrf_token_from_cookie(cookie_header)
7660}
7661
7662/// Extracts an address-like value from peer metadata cookie text.
7663///
7664/// # Parameters
7665///
7666/// - `cookie_header`: Input consumed by [`crate::runtime::dynamic::extract_ip_from_peer_info_cookie`].
7667///
7668/// # Returns
7669///
7670/// Returns [`Some`] when [`crate::runtime::dynamic::extract_ip_from_peer_info_cookie`] can produce a value and [`None`] otherwise.
7671///
7672/// # Examples
7673///
7674/// ```rust,ignore
7675/// let _result = pharos_native::runtime::dynamic::extract_ip_from_peer_info_cookie("session=abc");
7676/// ```
7677///
7678/// # Related
7679///
7680/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
7681pub fn extract_ip_from_peer_info_cookie(cookie_header: &str) -> Option<String> {
7682    cookie_value(Some(cookie_header), "pharos_peer_ip")
7683        .filter(|value| value.parse::<std::net::IpAddr>().is_ok())
7684}
7685
7686/// Returns a bounded copy of the request body.
7687///
7688/// # Parameters
7689///
7690/// - `request`: Input consumed by [`crate::runtime::dynamic::request_body_bytes`].
7691///
7692/// # Returns
7693///
7694/// Returns [`Some`] when [`crate::runtime::dynamic::request_body_bytes`] can produce a value and [`None`] otherwise.
7695///
7696/// # Examples
7697///
7698/// ```rust,ignore
7699/// let _result = pharos_native::runtime::dynamic::request_body_bytes(todo!());
7700/// ```
7701///
7702/// # Related
7703///
7704/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
7705pub fn request_body_bytes(request: &HttpRequest) -> Option<Vec<u8>> {
7706    (request.body.len() <= super::http::PHAROS_HTTP_MAX_MULTIPART_BODY_BYTES)
7707        .then(|| request.body.clone())
7708}
7709
7710/// Returns the request body as UTF-8 text when valid.
7711///
7712/// # Parameters
7713///
7714/// - `request`: Input consumed by [`crate::runtime::dynamic::request_body_text`].
7715///
7716/// # Returns
7717///
7718/// Returns [`Some`] when [`crate::runtime::dynamic::request_body_text`] can produce a value and [`None`] otherwise.
7719///
7720/// # Examples
7721///
7722/// ```rust,ignore
7723/// let _result = pharos_native::runtime::dynamic::request_body_text(todo!());
7724/// ```
7725///
7726/// # Related
7727///
7728/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
7729pub fn request_body_text(request: &HttpRequest) -> Option<String> {
7730    String::from_utf8(request_body_bytes(request)?).ok()
7731}
7732
7733/// Resolves one request header without accepting folded lines.
7734///
7735/// # Parameters
7736///
7737/// - `request`: Input consumed by [`crate::runtime::dynamic::request_header_value`].
7738/// - `header_name`: Input consumed by [`crate::runtime::dynamic::request_header_value`].
7739///
7740/// # Returns
7741///
7742/// Returns [`Some`] when [`crate::runtime::dynamic::request_header_value`] can produce a value and [`None`] otherwise.
7743///
7744/// # Performance
7745///
7746/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::request_header_value`].
7747///
7748/// # Examples
7749///
7750/// ```rust,ignore
7751/// let _result = pharos_native::runtime::dynamic::request_header_value(todo!(), "content-type");
7752/// ```
7753///
7754/// # Related
7755///
7756/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
7757pub fn request_header_value(request: &HttpRequest, header_name: &str) -> Option<String> {
7758    request.headers.lines().find_map(|line| {
7759        let (name, value) = line.split_once(':')?;
7760        name.trim()
7761            .eq_ignore_ascii_case(header_name)
7762            .then(|| value.trim().to_owned())
7763    })
7764}
7765
7766/// Persists a validated state document at the app-owned state path.
7767///
7768/// # Parameters
7769///
7770/// - `app`: Input consumed by [`crate::runtime::dynamic::write_app_state_text_file_runtime_local`].
7771/// - `state_text`: Input consumed by [`crate::runtime::dynamic::write_app_state_text_file_runtime_local`].
7772///
7773/// # Returns
7774///
7775/// Returns the computed value produced by [`crate::runtime::dynamic::write_app_state_text_file_runtime_local`].
7776///
7777/// # Performance
7778///
7779/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::dynamic::write_app_state_text_file_runtime_local`].
7780///
7781/// # Examples
7782///
7783/// ```rust,ignore
7784/// let _result = pharos_native::runtime::dynamic::write_app_state_text_file_runtime_local(todo!(), todo!());
7785/// ```
7786///
7787/// # Related
7788///
7789/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
7790pub fn write_app_state_text_file_runtime_local(app: &AppRuntime, state_text: &str) -> i32 {
7791    let path = app
7792        .state_path
7793        .as_deref()
7794        .filter(|value| !value.is_empty())
7795        .map(Path::new);
7796    let Some(path) = path else {
7797        return 69;
7798    };
7799    if state_text.len() > 16 * 1024 * 1024 || serde_json::from_str::<Value>(state_text).is_err() {
7800        return 64;
7801    }
7802    super::fs::write_text_file(path, state_text)
7803}
7804
7805/// Builds a complete bounded HTML response envelope.
7806///
7807/// # Parameters
7808///
7809/// - `status`: Input consumed by [`crate::runtime::dynamic::write_html_response`].
7810/// - `status_text`: Input consumed by [`crate::runtime::dynamic::write_html_response`].
7811/// - `html`: Input consumed by [`crate::runtime::dynamic::write_html_response`].
7812/// - `csrf_token`: Input consumed by [`crate::runtime::dynamic::write_html_response`].
7813///
7814/// # Returns
7815///
7816/// Returns the computed value produced by [`crate::runtime::dynamic::write_html_response`].
7817///
7818/// # Examples
7819///
7820/// ```rust,ignore
7821/// let _result = pharos_native::runtime::dynamic::write_html_response(400, "400 Bad Request", todo!(), todo!());
7822/// ```
7823///
7824/// # Related
7825///
7826/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
7827pub fn write_html_response(
7828    status: u16,
7829    status_text: &str,
7830    html: &str,
7831    csrf_token: Option<&str>,
7832) -> String {
7833    let status_text = if status_text.is_empty() {
7834        super::http::http_status_text(status)
7835    } else {
7836        status_text
7837    };
7838    let cookie = csrf_token
7839        .map(|token| format!("Set-Cookie: pharos_csrf={token}; Path=/; SameSite=Lax\r\n"))
7840        .unwrap_or_default();
7841    format!(
7842        "HTTP/1.1 {status} {status_text}\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: {}\r\nCache-Control: no-store\r\nConnection: close\r\n{cookie}\r\n{html}",
7843        html.len()
7844    )
7845}
7846
7847/// Adds the CSRF hidden-input fragment to POST forms without altering other markup.
7848///
7849/// # Parameters
7850///
7851/// - `_app_root`: Input consumed by [`crate::runtime::dynamic::csrf_inject_hidden_inputs`].
7852/// - `html`: Input consumed by [`crate::runtime::dynamic::csrf_inject_hidden_inputs`].
7853/// - `csrf_token`: Input consumed by [`crate::runtime::dynamic::csrf_inject_hidden_inputs`].
7854///
7855/// # Returns
7856///
7857/// Returns the computed value produced by [`crate::runtime::dynamic::csrf_inject_hidden_inputs`].
7858///
7859/// # Examples
7860///
7861/// ```rust,ignore
7862/// let _result = pharos_native::runtime::dynamic::csrf_inject_hidden_inputs(todo!(), todo!(), todo!());
7863/// ```
7864///
7865/// # Related
7866///
7867/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
7868pub fn csrf_inject_hidden_inputs(_app_root: &Path, html: &str, csrf_token: &str) -> String {
7869    let escaped_token = super::json_commands::html_escape_text(csrf_token);
7870    pharos_runtime_dynamic::inject_csrf_hidden_inputs(html, &escaped_token)
7871}
7872
7873/// Builds an HTML response with caller-supplied validated header lines.
7874///
7875/// # Parameters
7876///
7877/// - `status`: Input consumed by [`crate::runtime::dynamic::write_html_response_with_headers`].
7878/// - `status_text`: Input consumed by [`crate::runtime::dynamic::write_html_response_with_headers`].
7879/// - `html`: Input consumed by [`crate::runtime::dynamic::write_html_response_with_headers`].
7880/// - `csrf_token`: Input consumed by [`crate::runtime::dynamic::write_html_response_with_headers`].
7881/// - `extra_headers`: Input consumed by [`crate::runtime::dynamic::write_html_response_with_headers`].
7882///
7883/// # Returns
7884///
7885/// Returns the computed value produced by [`crate::runtime::dynamic::write_html_response_with_headers`].
7886///
7887/// # Examples
7888///
7889/// ```rust,ignore
7890/// let _result = pharos_native::runtime::dynamic::write_html_response_with_headers(400, "400 Bad Request", todo!(), todo!(), todo!());
7891/// ```
7892///
7893/// # Related
7894///
7895/// - [`crate::runtime::dynamic`] for adjacent helpers that share the same runtime surface.
7896pub fn write_html_response_with_headers(
7897    status: u16,
7898    status_text: &str,
7899    html: &str,
7900    csrf_token: Option<&str>,
7901    extra_headers: &str,
7902) -> String {
7903    let safe_headers = extra_headers.replace(['\r', '\n'], " ");
7904    let mut response = write_html_response(status, status_text, html, csrf_token);
7905    if !safe_headers.is_empty() {
7906        response = response.replacen("\r\n\r\n", &format!("\r\n{safe_headers}\r\n\r\n"), 1);
7907    }
7908    response
7909}
7910
7911#[cfg(test)]
7912mod tests {
7913    /// Pruning completed work preserves durable delivery identity and active work.
7914    #[test]
7915    fn local_outbox_retention_preserves_delivery_sequence() {
7916        let mut root = json!({});
7917        for index in 0..1100 {
7918            assert!(super::enqueue_file_record(
7919                &mut root,
7920                (
7921                    "write_json_local".to_owned(),
7922                    "test.json".to_owned(),
7923                    json!({"revision": index})
7924                )
7925            ));
7926            let last = root["local_file_deliveries"]
7927                .as_array_mut()
7928                .expect("deliveries")
7929                .last_mut()
7930                .expect("new delivery");
7931            last["delivery_status"] = json!("delivered");
7932        }
7933        assert_eq!(root["_local_file_delivery_sequence"], json!(1100));
7934        assert!(
7935            root["local_file_deliveries"]
7936                .as_array()
7937                .expect("history")
7938                .len()
7939                <= 65
7940        );
7941        assert!(super::enqueue_file_record(
7942            &mut root,
7943            (
7944                "write_json_local".to_owned(),
7945                "test.json".to_owned(),
7946                json!({"revision": 1100})
7947            )
7948        ));
7949        assert_eq!(
7950            root["local_file_deliveries"]
7951                .as_array()
7952                .expect("history")
7953                .last()
7954                .expect("new delivery")["id"],
7955            json!(1101)
7956        );
7957        root["_local_file_delivery_sequence"] = json!("invalid");
7958        assert!(!super::enqueue_file_record(
7959            &mut root,
7960            (
7961                "write_json_local".to_owned(),
7962                "test.json".to_owned(),
7963                json!({})
7964            )
7965        ));
7966    }
7967
7968    /// A delayed earlier replacement blocks a later replacement of the same
7969    /// path, so a retry cannot overwrite newer output after it becomes visible.
7970    #[test]
7971    fn local_outbox_replacements_do_not_overtake_retries() {
7972        let mut root = json!({"local_file_deliveries": [
7973            {"id": 1, "relative_path": "same.json", "delivery_status": "retryable", "next_attempt_at": "99999999999"},
7974            {"id": 2, "relative_path": "same.json", "delivery_status": "queued"}
7975        ]});
7976        assert!(!super::process_one_due_file_delivery(
7977            &AppRuntime::default(),
7978            &mut root
7979        ));
7980        assert_eq!(
7981            root["local_file_deliveries"][1]["delivery_status"],
7982            json!("queued")
7983        );
7984    }
7985
7986    use super::{
7987        append_webhook_receipt, deliver_integration_webhook, dynamic_action_failure_annotations,
7988        dynamic_action_failure_report_root, dynamic_action_input_overrides,
7989        dynamic_action_report_annotations, dynamic_append_file_record, dynamic_append_notification,
7990        dynamic_append_record_primitive, dynamic_append_records_from_input_list_primitive,
7991        dynamic_append_records_from_related_collection_primitive,
7992        dynamic_apply_approval_transition,
7993        dynamic_apply_scoped_assignment_or_bulk_update_primitive,
7994        dynamic_apply_scoped_status_transition_primitive,
7995        dynamic_delete_records_by_scope_primitive,
7996        dynamic_replace_records_from_input_list_primitive, dynamic_set_root_fields_primitive,
7997        dynamic_store_last_action_report, dynamic_update_record_primitive,
7998        dynamic_upsert_record_primitive, process_one_due_notification_delivery,
7999        webhook_request_signature_valid, webhook_signature_header_value_dup_logic_local,
8000    };
8001    use crate::config::AppRuntime;
8002    use crate::runtime::http::HttpRequest;
8003    use serde_json::json;
8004    use std::collections::BTreeMap;
8005    use std::fs;
8006    use std::path::PathBuf;
8007    use std::time::{SystemTime, UNIX_EPOCH};
8008
8009    /// Performs unique test dir.
8010    fn unique_test_dir(label: &str) -> PathBuf {
8011        let stamp = SystemTime::now()
8012            .duration_since(UNIX_EPOCH)
8013            .expect("test clock")
8014            .as_nanos();
8015        std::env::temp_dir().join(format!("pharos-{label}-{stamp}"))
8016    }
8017
8018    /// Performs request with form.
8019    fn request_with_form(headers: &str, body: &str) -> HttpRequest {
8020        HttpRequest {
8021            method: "POST".to_owned(),
8022            target: "/actions/test".to_owned(),
8023            cookie: None,
8024            content_type: Some("application/x-www-form-urlencoded".to_owned()),
8025            headers: headers.to_owned(),
8026            body: body.as_bytes().to_vec(),
8027            body_file_path: None,
8028            body_len: body.len(),
8029            is_multipart: false,
8030        }
8031    }
8032
8033    #[test]
8034    /// Performs webhook request signature validation accepts valid current signature and rejects bad one.
8035    fn webhook_request_signature_validation_accepts_valid_current_signature_and_rejects_bad_one() {
8036        let payload = r#"{"event":"local_echo"}"#;
8037        let timestamp = SystemTime::now()
8038            .duration_since(UNIX_EPOCH)
8039            .expect("clock")
8040            .as_secs()
8041            .to_string();
8042        let secret_env = "PATH";
8043        assert!(
8044            std::env::var(secret_env)
8045                .ok()
8046                .is_some_and(|value| !value.is_empty())
8047        );
8048        let signature =
8049            webhook_signature_header_value_dup_logic_local(secret_env, &timestamp, payload)
8050                .expect("signature");
8051        let endpoint = json!({
8052            "endpoint_id": "local-echo",
8053            "signature": {
8054                "mode": "hmac_sha256",
8055                "header": "X-Pharos-Signature",
8056                "timestamp_header": "X-Pharos-Timestamp",
8057                "secret_env": secret_env,
8058                "max_age_seconds": 300
8059            }
8060        });
8061        let valid_request = HttpRequest {
8062            method: "POST".to_owned(),
8063            target: "/_pharos/webhooks/local-echo/".to_owned(),
8064            cookie: None,
8065            content_type: Some("application/json".to_owned()),
8066            headers: format!(
8067                "X-Pharos-Timestamp: {timestamp}\r\nX-Pharos-Signature: {signature}\r\n"
8068            ),
8069            body: payload.as_bytes().to_vec(),
8070            body_file_path: None,
8071            body_len: payload.len(),
8072            is_multipart: false,
8073        };
8074        assert!(webhook_request_signature_valid(
8075            &endpoint,
8076            &valid_request,
8077            payload
8078        ));
8079        let invalid_request = HttpRequest {
8080            headers: format!(
8081                "X-Pharos-Timestamp: {timestamp}\r\nX-Pharos-Signature: sha256=deadbeef\r\n"
8082            ),
8083            ..valid_request
8084        };
8085        assert!(!webhook_request_signature_valid(
8086            &endpoint,
8087            &invalid_request,
8088            payload
8089        ));
8090    }
8091
8092    #[test]
8093    /// Appends webhook receipt persists verified payload.
8094    fn append_webhook_receipt_persists_verified_payload() {
8095        let root = unique_test_dir("dynamic-webhook-receipt");
8096        fs::create_dir_all(&root).expect("create receipt root");
8097        let state_path = root.join("runtime-state.json");
8098        fs::write(&state_path, r#"{"webhook_receipts":[]}"#).expect("write state");
8099        assert_eq!(
8100            append_webhook_receipt(&state_path, "local-echo", "verified", r#"{"ok":true}"#),
8101            0
8102        );
8103        let persisted: serde_json::Value =
8104            serde_json::from_str(&fs::read_to_string(&state_path).expect("read state"))
8105                .expect("parse state");
8106        let receipts = persisted["webhook_receipts"].as_array().expect("receipts");
8107        assert_eq!(receipts.len(), 1);
8108        assert_eq!(receipts[0]["endpoint_id"], "local-echo");
8109        assert_eq!(receipts[0]["verification_status"], "verified");
8110        assert_eq!(receipts[0]["payload"], r#"{"ok":true}"#);
8111        assert!(
8112            receipts[0]["created_at"]
8113                .as_str()
8114                .is_some_and(|value| !value.is_empty())
8115        );
8116        fs::remove_dir_all(root).expect("remove receipt root");
8117    }
8118
8119    #[test]
8120    /// Performs notification email delivery marks missing SMTP config as skipped.
8121    fn notification_email_delivery_marks_missing_smtp_config_as_skipped() {
8122        let root = unique_test_dir("dynamic-notification-email");
8123        fs::create_dir_all(root.join("templates/email")).expect("create template dir");
8124        fs::write(
8125            root.join("templates/email/account-proof.yml"),
8126            "subject: Account proof for {{recipient}}\nbody: Hello {{recipient}}, this is a proof email.\n",
8127        )
8128        .expect("write email template");
8129        let app = AppRuntime {
8130            app_root: Some(root.to_string_lossy().into_owned()),
8131            app_id: Some("ucal".to_owned()),
8132            ..AppRuntime::default()
8133        };
8134        let integrations = json!({
8135            "adapters": [{
8136                "adapter_id": "account-mail",
8137                "adapter_kind": "notification_email",
8138                "relay": "primary",
8139                "max_attempts": 2,
8140                "backoff_seconds": 1
8141            }]
8142        });
8143        let mut state = json!({"notification_deliveries": []});
8144        assert!(dynamic_append_notification(
8145            &mut state,
8146            &integrations,
8147            "account-mail",
8148            "email",
8149            "ops@llight.io",
8150            "account-proof",
8151            &json!({"recipient":"ops@llight.io"})
8152        ));
8153        assert!(process_one_due_notification_delivery(
8154            &app,
8155            &mut state,
8156            &integrations
8157        ));
8158        let delivery = &state["notification_deliveries"][0];
8159        assert_eq!(delivery["adapter_kind"], "notification_email");
8160        assert_eq!(delivery["delivery_status"], "skipped");
8161        assert_eq!(delivery["attempt_count"], 1);
8162        assert_eq!(delivery["last_error"], "email disabled");
8163        assert_eq!(
8164            delivery["rendered_subject"],
8165            "Account proof for ops@llight.io"
8166        );
8167        assert_eq!(
8168            delivery["rendered_body"],
8169            "Hello ops@llight.io, this is a proof email."
8170        );
8171        assert!(
8172            delivery["completed_at"]
8173                .as_str()
8174                .is_some_and(|value| !value.is_empty())
8175        );
8176        fs::remove_dir_all(root).expect("remove notification root");
8177    }
8178
8179    #[test]
8180    /// Returns the default inputs fill missing values without overwriting submitted ones.
8181    fn default_inputs_fill_missing_values_without_overwriting_submitted_ones() {
8182        let action = json!({
8183            "hook_contract": {
8184                "default_inputs": [
8185                    { "field": "snapshot_label", "source": "literal", "value": "initial-proof" },
8186                    { "field": "actor", "source": "header", "header": "X-Pharos-Actor" }
8187                ]
8188            }
8189        });
8190        let request_missing = request_with_form("X-Pharos-Actor: casey", "board_id=4");
8191        let overrides_missing =
8192            dynamic_action_input_overrides(&action, &request_missing).expect("default overrides");
8193        assert_eq!(
8194            super::dynamic_action_input_value(
8195                &request_missing,
8196                &overrides_missing,
8197                "snapshot_label"
8198            )
8199            .as_deref(),
8200            Some("initial-proof")
8201        );
8202        assert_eq!(
8203            super::dynamic_action_input_value(&request_missing, &overrides_missing, "actor")
8204                .as_deref(),
8205            Some("casey")
8206        );
8207
8208        let request_present = request_with_form("X-Pharos-Actor: casey", "snapshot_label=manual");
8209        let overrides_present =
8210            dynamic_action_input_overrides(&action, &request_present).expect("submitted value");
8211        assert_eq!(
8212            super::dynamic_action_input_value(
8213                &request_present,
8214                &overrides_present,
8215                "snapshot_label"
8216            )
8217            .as_deref(),
8218            Some("manual")
8219        );
8220    }
8221
8222    #[test]
8223    /// Deletes records by scope removes matching rows and counts skips.
8224    fn delete_records_by_scope_removes_matching_rows_and_counts_skips() {
8225        let mut root = json!({
8226            "board_task_snapshots": [
8227                { "id": 8, "board_id": 4, "snapshot_label": "initial-proof" },
8228                { "id": 9, "board_id": 4, "snapshot_label": "initial-proof" },
8229                { "id": 10, "board_id": 4, "snapshot_label": "initial-proof" },
8230                { "id": 11, "board_id": 8, "snapshot_label": "keep" }
8231            ]
8232        });
8233        let primitive = json!({
8234            "collection": "board_task_snapshots",
8235            "scope_match": { "field": "board_id", "from_input": "board_id" },
8236            "max_records": 2,
8237            "overflow_policy": "truncate"
8238        });
8239        let request = request_with_form("", "board_id=4");
8240        let mut matched = 0;
8241        let mut skipped = 0;
8242        let mut removed = 0;
8243        dynamic_delete_records_by_scope_primitive(
8244            &mut root,
8245            &request,
8246            &BTreeMap::new(),
8247            &primitive,
8248            &mut matched,
8249            &mut skipped,
8250            &mut removed,
8251        )
8252        .expect("delete primitive");
8253        let rows = root["board_task_snapshots"]
8254            .as_array()
8255            .expect("snapshot rows");
8256        assert_eq!(matched, 3);
8257        assert_eq!(skipped, 1);
8258        assert_eq!(removed, 2);
8259        assert_eq!(rows.len(), 2);
8260        assert!(rows.iter().any(|row| row["board_id"] == 8));
8261    }
8262
8263    #[test]
8264    /// Writes JSON local overwrites one bounded snapshot file.
8265    fn write_json_local_overwrites_one_bounded_snapshot_file() {
8266        let root = unique_test_dir("dynamic-json-file-record");
8267        let state_root = root.join("state");
8268        fs::create_dir_all(state_root.join("exports")).expect("create exports");
8269        let state_path = state_root.join("runtime-state.json");
8270        fs::write(&state_path, "{}").expect("write state");
8271        let integrations = json!({
8272            "adapters": [{
8273                "adapter_id": "local-board-snapshot",
8274                "adapter_kind": "write_json_local",
8275                "relative_path": "exports/board-snapshot.json"
8276            }]
8277        });
8278        let action = json!({ "entity_id": "board_task_snapshot" });
8279        let file_record = json!({
8280            "adapter_id": "local-board-snapshot",
8281            "record": {
8282                "event": { "source": "literal", "value": "board_task_snapshots_purged" },
8283                "board_id": { "source": "input", "field": "board_id" }
8284            }
8285        });
8286        let request = request_with_form("", "board_id=4");
8287        assert!(dynamic_append_file_record(
8288            &state_path,
8289            &integrations,
8290            &action,
8291            &file_record,
8292            &request,
8293            &BTreeMap::new(),
8294            "purge_board_task_snapshots",
8295            "board_task_snapshot",
8296            "board_task_snapshots_purged",
8297            0,
8298        ));
8299        let output = fs::read_to_string(state_root.join("exports/board-snapshot.json"))
8300            .expect("read snapshot json");
8301        let persisted: serde_json::Value =
8302            serde_json::from_str(&output).expect("parse snapshot json");
8303        assert_eq!(persisted["adapter_kind"], "write_json_local");
8304        assert_eq!(persisted["board_id"], "4");
8305        assert_eq!(persisted["event"], "board_task_snapshots_purged");
8306        fs::remove_dir_all(root).expect("remove test root");
8307    }
8308
8309    /// Preserves explicit local-transport and response-budget policy on queued deliveries.
8310    #[test]
8311    fn webhook_delivery_carries_bounded_transport_policy() {
8312        let mut state = json!({"integration_deliveries": []});
8313        let integrations = json!({
8314            "adapters": [{
8315                "adapter_id": "local_webhook",
8316                "adapter_kind": "webhook",
8317                "target_url": "http://127.0.0.1:18781/capture",
8318                "allow_plaintext_local": true,
8319                "max_response_bytes": 4096
8320            }]
8321        });
8322
8323        assert_eq!(
8324            deliver_integration_webhook(
8325                &mut state,
8326                &integrations,
8327                "local_webhook",
8328                Some("test"),
8329                Some("appointment:1"),
8330                None,
8331                None,
8332                r#"{"event":"probe"}"#,
8333            ),
8334            Some(1)
8335        );
8336        let delivery = &state["integration_deliveries"][0];
8337        assert_eq!(delivery["allow_plaintext_local"], true);
8338        assert_eq!(delivery["max_response_bytes"], 4096);
8339    }
8340
8341    /// Keeps plaintext local delivery disabled when the adapter does not opt in.
8342    #[test]
8343    fn webhook_delivery_defaults_to_secure_transport_policy() {
8344        let mut state = json!({"integration_deliveries": []});
8345        let integrations = json!({
8346            "adapters": [{
8347                "adapter_id": "secure_webhook",
8348                "adapter_kind": "webhook",
8349                "target_url": "https://example.test/capture"
8350            }]
8351        });
8352
8353        assert_eq!(
8354            deliver_integration_webhook(
8355                &mut state,
8356                &integrations,
8357                "secure_webhook",
8358                None,
8359                None,
8360                None,
8361                None,
8362                r#"{"event":"probe"}"#,
8363            ),
8364            Some(1)
8365        );
8366        let delivery = &state["integration_deliveries"][0];
8367        assert_eq!(delivery["allow_plaintext_local"], false);
8368        assert_eq!(delivery["max_response_bytes"], 8 * 1024);
8369    }
8370
8371    /// Prefers the first duplicate cookie so mounted apps keep the scoped session token.
8372    #[test]
8373    fn cookie_lookup_prefers_first_duplicate_name() {
8374        let cookie_header =
8375            "pharos_session=scoped-token; other=value; pharos_session=stale-root-token";
8376        assert_eq!(
8377            super::dynamic_cookie_value(cookie_header, "pharos_session").as_deref(),
8378            Some("scoped-token")
8379        );
8380    }
8381
8382    /// Carries explicit auth and event-header policy for the broader HTTP JSON adapter family.
8383    #[test]
8384    fn http_json_delivery_carries_auth_and_event_header_policy() {
8385        let mut state = json!({"integration_deliveries": []});
8386        let integrations = json!({
8387            "adapters": [{
8388                "adapter_id": "board_api",
8389                "adapter_kind": "http_json",
8390                "target_url": "http://127.0.0.1:18782/capture",
8391                "auth_header": "Authorization",
8392                "auth_scheme": "Bearer",
8393                "auth_token_env": "PHAROS_BOARD_API_TOKEN",
8394                "event_header": "X-Pharos-Event",
8395                "allow_plaintext_local": true
8396            }]
8397        });
8398
8399        assert_eq!(
8400            deliver_integration_webhook(
8401                &mut state,
8402                &integrations,
8403                "board_api",
8404                Some("board_created"),
8405                Some("board:4"),
8406                None,
8407                None,
8408                r#"{"event":"probe"}"#,
8409            ),
8410            Some(1)
8411        );
8412        let delivery = &state["integration_deliveries"][0];
8413        assert_eq!(delivery["adapter_kind"], "http_json");
8414        assert_eq!(delivery["auth_header"], "Authorization");
8415        assert_eq!(delivery["auth_scheme"], "Bearer");
8416        assert_eq!(delivery["auth_token_env"], "PHAROS_BOARD_API_TOKEN");
8417        assert_eq!(delivery["event_header"], "X-Pharos-Event");
8418    }
8419
8420    /// Carries the declared form-payload adapter policy for flat object delivery bodies.
8421    #[test]
8422    fn http_form_delivery_carries_form_adapter_policy() {
8423        let mut state = json!({"integration_deliveries": []});
8424        let integrations = json!({
8425            "adapters": [{
8426                "adapter_id": "board_form",
8427                "adapter_kind": "http_form",
8428                "target_url": "http://127.0.0.1:18783/capture",
8429                "content_type": "application/x-www-form-urlencoded",
8430                "auth_header": "X-Api-Key",
8431                "auth_scheme": "",
8432                "auth_token_env": "PHAROS_BOARD_FORM_KEY",
8433                "event_header": "X-Pharos-Event",
8434                "allow_plaintext_local": true
8435            }]
8436        });
8437
8438        assert_eq!(
8439            deliver_integration_webhook(
8440                &mut state,
8441                &integrations,
8442                "board_form",
8443                Some("board_completed"),
8444                Some("board:4"),
8445                None,
8446                None,
8447                r#"{"event":"probe","attempt":1}"#,
8448            ),
8449            Some(1)
8450        );
8451        let delivery = &state["integration_deliveries"][0];
8452        assert_eq!(delivery["adapter_kind"], "http_form");
8453        assert_eq!(
8454            delivery["content_type"],
8455            "application/x-www-form-urlencoded"
8456        );
8457        assert_eq!(delivery["auth_header"], "X-Api-Key");
8458        assert_eq!(delivery["auth_scheme"], "");
8459        assert_eq!(delivery["auth_token_env"], "PHAROS_BOARD_FORM_KEY");
8460        assert_eq!(delivery["event_header"], "X-Pharos-Event");
8461    }
8462
8463    #[test]
8464    /// Performs action report annotations are persisted in last action report.
8465    fn action_report_annotations_are_persisted_in_last_action_report() {
8466        let action = json!({
8467            "hook_contract": {
8468                "normalize_inputs": [
8469                    { "kind": "trim", "fields": ["title"] },
8470                    { "kind": "collapse_whitespace", "fields": ["title"] }
8471                ],
8472                "report_annotations": [
8473                    { "field": "normalized_title", "value": { "source": "input", "field": "title" } },
8474                    { "field": "request_transport", "value": { "source": "literal", "value": "form" } }
8475                ]
8476            }
8477        });
8478        let request = request_with_form("", "title=%20%20Native%20%20Proof%20Task%20%20");
8479        let overrides = dynamic_action_input_overrides(&action, &request).expect("input overrides");
8480        let annotations = dynamic_action_report_annotations(
8481            &action,
8482            &request,
8483            &overrides,
8484            "create_task",
8485            "task",
8486            "create_task_succeeded",
8487            41,
8488        )
8489        .expect("report annotations");
8490        let mut root = json!({"action_reports": []});
8491        assert!(dynamic_store_last_action_report(
8492            &mut root,
8493            json!({
8494                "action_id": "create_task",
8495                "event_kind": "create_task_succeeded",
8496                "annotations": annotations
8497            }),
8498        ));
8499        assert_eq!(
8500            root["last_action_report"]["annotations"]["normalized_title"],
8501            "Native Proof Task"
8502        );
8503        assert_eq!(
8504            root["last_action_report"]["annotations"]["request_transport"],
8505            "form"
8506        );
8507        assert_eq!(
8508            root["action_reports"]
8509                .as_array()
8510                .expect("action reports")
8511                .len(),
8512            1
8513        );
8514    }
8515
8516    #[test]
8517    /// Performs action failure annotations are persisted in failure report.
8518    fn action_failure_annotations_are_preserved_in_failure_report() {
8519        let action = json!({
8520            "hook_contract": {
8521                "failure_annotations": [
8522                    { "field": "attempted_transition", "value": { "source": "literal", "value": "reject" } },
8523                    { "field": "approval_request_id", "value": { "source": "input", "field": "id" } },
8524                    { "field": "approver", "value": { "source": "input", "field": "approver" } }
8525                ]
8526            }
8527        });
8528        let request = request_with_form("", "id=17&approver=owner1");
8529        let overrides = dynamic_action_input_overrides(&action, &request).expect("input overrides");
8530        let annotations = dynamic_action_failure_annotations(
8531            &action,
8532            &request,
8533            &overrides,
8534            "reject_approval",
8535            "approval_request",
8536            "reject_approval_failed",
8537            17,
8538        )
8539        .expect("failure annotations");
8540        assert_eq!(annotations["attempted_transition"], "reject");
8541        let persisted = dynamic_action_failure_report_root(
8542            json!({"action_reports": []}),
8543            &action,
8544            &request,
8545            &overrides,
8546            "reject_approval",
8547            "approval_request",
8548            None,
8549            17,
8550            422,
8551        );
8552        assert_eq!(
8553            persisted["last_action_report"]["annotations"]["attempted_transition"],
8554            "reject"
8555        );
8556        assert_eq!(
8557            persisted["last_action_report"]["annotations"]["approval_request_id"],
8558            "17"
8559        );
8560        assert_eq!(
8561            persisted["last_action_report"]["annotations"]["approver"],
8562            "owner1"
8563        );
8564    }
8565
8566    #[test]
8567    /// Appends record creates anchor row.
8568    fn append_record_creates_anchor_row() {
8569        let mut root = json!({
8570            "next_id": 7,
8571            "boards": []
8572        });
8573        let primitive = json!({
8574            "collection": "boards",
8575            "record_contract": {
8576                "id": { "source": "next_id" },
8577                "name": { "source": "input", "field": "board_name" },
8578                "workflow_state": { "source": "literal", "value": "created" }
8579            }
8580        });
8581        let request = request_with_form("", "board_name=Roadmap");
8582        let mut anchor_id = 0;
8583        let mut created = 0;
8584        dynamic_append_record_primitive(
8585            &mut root,
8586            &request,
8587            &BTreeMap::new(),
8588            &primitive,
8589            &mut anchor_id,
8590            &mut created,
8591        )
8592        .expect("append record");
8593        assert_eq!(created, 1);
8594        assert_eq!(anchor_id, 7);
8595        assert_eq!(root["boards"][0]["id"], 7);
8596        assert_eq!(root["boards"][0]["name"], "Roadmap");
8597        assert_eq!(root["boards"][0]["workflow_state"], "created");
8598    }
8599
8600    #[test]
8601    /// Sets root fields applies declared literal and input values.
8602    fn set_root_fields_applies_declared_literal_and_input_values() {
8603        let root_dir = unique_test_dir("dynamic-set-root-fields");
8604        fs::create_dir_all(root_dir.join("data/contracts")).expect("create contract dir");
8605        fs::write(
8606            root_dir.join("data/contracts/test-fields.json"),
8607            r#"{
8608              "fields": {
8609                "last_action": { "source": "literal", "value": "board_created" },
8610                "last_board_name": { "source": "input", "field": "board_name" }
8611              }
8612            }"#,
8613        )
8614        .expect("write field contract");
8615        let app = AppRuntime {
8616            app_root: Some(root_dir.to_string_lossy().into_owned()),
8617            ..AppRuntime::default()
8618        };
8619        let primitive = json!({
8620            "fields": "test-fields.json#fields"
8621        });
8622        let request = request_with_form("", "board_name=Roadmap");
8623        let mut root = json!({});
8624        dynamic_set_root_fields_primitive(&app, &mut root, &request, &BTreeMap::new(), &primitive)
8625            .expect("set root fields");
8626        assert_eq!(root["last_action"], "board_created");
8627        assert_eq!(root["last_board_name"], "Roadmap");
8628        fs::remove_dir_all(root_dir).expect("remove test root");
8629    }
8630
8631    #[test]
8632    /// Updates record updates matching row and missing match fails closed.
8633    fn update_record_updates_matching_row_and_missing_match_fails_closed() {
8634        let primitive = json!({
8635            "collection": "tasks",
8636            "match": { "field": "id", "from_input": "id" },
8637            "updates": {
8638                "done": { "source": "toggle_boolean", "field": "done" },
8639                "title": { "source": "input", "field": "title" }
8640            }
8641        });
8642        let request = request_with_form("", "id=2&title=Retitled");
8643        let mut root = json!({
8644            "tasks": [
8645                { "id": "1", "title": "One", "done": false },
8646                { "id": "2", "title": "Two", "done": false }
8647            ]
8648        });
8649        let mut matched = 0;
8650        let mut updated = 0;
8651        dynamic_update_record_primitive(
8652            &mut root,
8653            &request,
8654            &BTreeMap::new(),
8655            &primitive,
8656            &mut matched,
8657            &mut updated,
8658        )
8659        .expect("update matching row");
8660        assert_eq!(matched, 1);
8661        assert_eq!(updated, 1);
8662        assert_eq!(root["tasks"][1]["title"], "Retitled");
8663        assert_eq!(root["tasks"][1]["done"], true);
8664        let missing_request = request_with_form("", "id=99&title=Missing");
8665        let missing = dynamic_update_record_primitive(
8666            &mut root,
8667            &missing_request,
8668            &BTreeMap::new(),
8669            &primitive,
8670            &mut matched,
8671            &mut updated,
8672        );
8673        assert_eq!(missing, Err(404));
8674    }
8675
8676    #[test]
8677    /// Appends records from input list creates rows with anchor and skip counts.
8678    fn append_records_from_input_list_creates_rows_with_anchor_and_skip_counts() {
8679        let mut root = json!({
8680            "next_id": 9,
8681            "board_tasks": []
8682        });
8683        let primitive = json!({
8684            "collection": "board_tasks",
8685            "list_source": { "from_input": "task_titles", "delimiter": "," },
8686            "max_records": 2,
8687            "overflow_policy": "truncate",
8688            "record_contract": {
8689                "id": { "source": "next_id" },
8690                "board_id": { "source": "context_anchor_created_id" },
8691                "title": { "source": "list_item" },
8692                "sort_order": { "source": "list_index" },
8693                "workflow_state": { "source": "literal", "value": "created" }
8694            }
8695        });
8696        let request = request_with_form("", "task_titles=One,Two,Three");
8697        let mut skipped = 0;
8698        let mut created = 0;
8699        dynamic_append_records_from_input_list_primitive(
8700            &mut root,
8701            &request,
8702            &BTreeMap::new(),
8703            &primitive,
8704            44,
8705            0,
8706            0,
8707            &mut skipped,
8708            &mut created,
8709        )
8710        .expect("append records from input list");
8711        let rows = root["board_tasks"].as_array().expect("board tasks array");
8712        assert_eq!(created, 2);
8713        assert_eq!(skipped, 1);
8714        assert_eq!(rows.len(), 2);
8715        assert_eq!(rows[0]["id"], 9);
8716        assert_eq!(rows[0]["board_id"], 44);
8717        assert_eq!(rows[0]["title"], "One");
8718        assert_eq!(rows[0]["sort_order"], 1);
8719        assert_eq!(rows[1]["id"], 10);
8720        assert_eq!(rows[1]["title"], "Two");
8721        assert_eq!(rows[1]["sort_order"], 2);
8722    }
8723
8724    #[test]
8725    /// Appends records from related collection copies matched rows and tracks overflow.
8726    fn append_records_from_related_collection_copies_matched_rows_and_tracks_overflow() {
8727        let mut root = json!({
8728            "next_id": 20,
8729            "board_tasks": [
8730                { "id": 1, "board_id": 4, "title": "One", "assignee": "sam", "status": "pending" },
8731                { "id": 2, "board_id": 4, "title": "Two", "assignee": "lee", "status": "pending" },
8732                { "id": 3, "board_id": 8, "title": "Other", "assignee": "ivy", "status": "done" }
8733            ],
8734            "board_task_snapshots": []
8735        });
8736        let primitive = json!({
8737            "collection": "board_task_snapshots",
8738            "source_collection": "board_tasks",
8739            "scope_match": { "field": "board_id", "from_input": "board_id" },
8740            "max_records": 1,
8741            "record_contract": {
8742                "id": { "source": "next_id" },
8743                "board_id": { "source": "source_record_field", "field": "board_id" },
8744                "source_task_id": { "source": "source_record_field", "field": "id" },
8745                "title": { "source": "source_record_field", "field": "title" },
8746                "snapshot_label": { "source": "input", "field": "snapshot_label" }
8747            }
8748        });
8749        let request = request_with_form("", "board_id=4&snapshot_label=Nightly");
8750        let mut matched = 0;
8751        let mut skipped = 0;
8752        let mut created = 0;
8753        dynamic_append_records_from_related_collection_primitive(
8754            &mut root,
8755            &request,
8756            &BTreeMap::new(),
8757            &primitive,
8758            &mut matched,
8759            &mut skipped,
8760            &mut created,
8761        )
8762        .expect("append records from related collection");
8763        let rows = root["board_task_snapshots"]
8764            .as_array()
8765            .expect("snapshot rows array");
8766        assert_eq!(matched, 2);
8767        assert_eq!(skipped, 1);
8768        assert_eq!(created, 1);
8769        assert_eq!(rows.len(), 1);
8770        assert_eq!(rows[0]["id"], 20);
8771        assert_eq!(rows[0]["board_id"], 4);
8772        assert_eq!(rows[0]["source_task_id"], 1);
8773        assert_eq!(rows[0]["title"], "One");
8774        assert_eq!(rows[0]["snapshot_label"], "Nightly");
8775    }
8776
8777    #[test]
8778    /// Performs scoped reassignment updates matching rows and preserves others.
8779    fn scoped_reassignment_updates_matching_rows_and_preserves_others() {
8780        let mut root = json!({
8781            "board_tasks": [
8782                { "id": 1, "board_id": 4, "assignee": "sam" },
8783                { "id": 2, "board_id": 4, "assignee": "lee" },
8784                { "id": 3, "board_id": 8, "assignee": "ivy" }
8785            ]
8786        });
8787        let primitive = json!({
8788            "collection": "board_tasks",
8789            "scope_match": { "field": "board_id", "from_input": "board_id" },
8790            "target_field": "assignee",
8791            "value_contract": { "source": "input", "field": "new_assignee" },
8792            "max_records": 1,
8793            "overflow_policy": "truncate"
8794        });
8795        let request = request_with_form("", "board_id=4&new_assignee=alex");
8796        let mut matched = 0;
8797        let mut updated = 0;
8798        let mut skipped = 0;
8799        dynamic_apply_scoped_assignment_or_bulk_update_primitive(
8800            &mut root,
8801            &request,
8802            &BTreeMap::new(),
8803            &primitive,
8804            "scoped_reassignment",
8805            &mut matched,
8806            &mut updated,
8807            &mut skipped,
8808        )
8809        .expect("scoped reassignment");
8810        let rows = root["board_tasks"].as_array().expect("board tasks");
8811        assert_eq!(matched, 2);
8812        assert_eq!(updated, 1);
8813        assert_eq!(skipped, 1);
8814        assert_eq!(rows[0]["assignee"], "alex");
8815        assert_eq!(rows[1]["assignee"], "lee");
8816        assert_eq!(rows[2]["assignee"], "ivy");
8817    }
8818
8819    #[test]
8820    /// Performs bulk update fails closed when scope exceeds strict bound.
8821    fn bulk_update_fails_closed_when_scope_exceeds_strict_bound() {
8822        let mut root = json!({
8823            "board_tasks": [
8824                { "id": 1, "board_id": 4, "done": false, "status": "pending" },
8825                { "id": 2, "board_id": 4, "done": false, "status": "pending" }
8826            ]
8827        });
8828        let primitive = json!({
8829            "collection": "board_tasks",
8830            "scope_match": { "field": "board_id", "from_input": "board_id" },
8831            "updates": {
8832                "done": { "source": "literal", "value": true },
8833                "status": { "source": "literal", "value": "completed" }
8834            },
8835            "max_records": 1,
8836            "overflow_policy": "fail"
8837        });
8838        let request = request_with_form("", "board_id=4");
8839        let mut matched = 0;
8840        let mut updated = 0;
8841        let mut skipped = 0;
8842        let result = dynamic_apply_scoped_assignment_or_bulk_update_primitive(
8843            &mut root,
8844            &request,
8845            &BTreeMap::new(),
8846            &primitive,
8847            "bulk_update",
8848            &mut matched,
8849            &mut updated,
8850            &mut skipped,
8851        );
8852        assert_eq!(result, Err(422));
8853        assert_eq!(matched, 2);
8854        assert_eq!(updated, 0);
8855        assert_eq!(skipped, 0);
8856        assert_eq!(root["board_tasks"][0]["done"], false);
8857        assert_eq!(root["board_tasks"][1]["status"], "pending");
8858    }
8859
8860    #[test]
8861    /// Performs scoped status transition updates allowed rows and reports skips.
8862    fn scoped_status_transition_updates_allowed_rows_and_reports_skips() {
8863        let mut root = json!({
8864            "board_tasks": [
8865                { "id": 1, "board_id": 4, "status": "pending", "workflow_state": "created", "done": false },
8866                { "id": 2, "board_id": 4, "status": "assigned", "workflow_state": "assigned", "done": false },
8867                { "id": 3, "board_id": 4, "status": "completed", "workflow_state": "done", "done": true },
8868                { "id": 4, "board_id": 8, "status": "pending", "workflow_state": "created", "done": false }
8869            ]
8870        });
8871        let primitive = json!({
8872            "collection": "board_tasks",
8873            "scope_match": { "field": "board_id", "from_input": "board_id" },
8874            "from_statuses": ["pending", "assigned"],
8875            "to_status": "completed",
8876            "to_workflow_state": "done",
8877            "timestamp_field": "completed_at",
8878            "actor_field": "completed_by",
8879            "actor_input": "actor",
8880            "updates": {
8881                "done": { "source": "literal", "value": true }
8882            }
8883        });
8884        let request = request_with_form("", "board_id=4&actor=ops-user");
8885        let mut matched = 0;
8886        let mut updated = 0;
8887        let mut skipped = 0;
8888        dynamic_apply_scoped_status_transition_primitive(
8889            &mut root,
8890            &request,
8891            &BTreeMap::new(),
8892            &primitive,
8893            &mut matched,
8894            &mut updated,
8895            &mut skipped,
8896        )
8897        .expect("scoped status transition");
8898        let rows = root["board_tasks"].as_array().expect("board tasks");
8899        assert_eq!(matched, 3);
8900        assert_eq!(updated, 2);
8901        assert_eq!(skipped, 1);
8902        assert_eq!(rows[0]["status"], "completed");
8903        assert_eq!(rows[0]["workflow_state"], "done");
8904        assert_eq!(rows[0]["done"], true);
8905        assert_eq!(rows[0]["completed_by"], "ops-user");
8906        assert!(
8907            rows[0]["completed_at"]
8908                .as_str()
8909                .is_some_and(|value| !value.is_empty())
8910        );
8911        assert_eq!(rows[2]["status"], "completed");
8912        assert_eq!(rows[3]["status"], "pending");
8913    }
8914
8915    #[test]
8916    /// Performs upsert record updates existing row and creates missing row.
8917    fn upsert_record_updates_existing_row_and_creates_missing_row() {
8918        let primitive = json!({
8919            "collection": "board_status_summaries",
8920            "match": { "field": "board_id", "from_input": "board_id" },
8921            "updates": {
8922                "board_id": { "source": "input", "field": "board_id" },
8923                "board_status": { "source": "literal", "value": "completed" },
8924                "completed_task_count": { "source": "context_updated_count" },
8925                "updated_at": { "source": "now" }
8926            },
8927            "record_contract": {
8928                "id": { "source": "next_id" },
8929                "board_id": { "source": "input", "field": "board_id" },
8930                "board_status": { "source": "literal", "value": "completed" },
8931                "completed_task_count": { "source": "context_updated_count" },
8932                "created_at": { "source": "now" },
8933                "updated_at": { "source": "now" }
8934            }
8935        });
8936
8937        let mut existing_root = json!({
8938            "next_id": 12,
8939            "board_status_summaries": [
8940                { "id": 3, "board_id": "4", "board_status": "pending", "completed_task_count": 1, "updated_at": "stale" }
8941            ]
8942        });
8943        let request = request_with_form("", "board_id=4");
8944        let mut matched = 0;
8945        let mut updated = 2;
8946        let mut created = 0;
8947        dynamic_upsert_record_primitive(
8948            &mut existing_root,
8949            &request,
8950            &BTreeMap::new(),
8951            &primitive,
8952            0,
8953            &mut matched,
8954            &mut updated,
8955            0,
8956            &mut created,
8957        )
8958        .expect("upsert existing row");
8959        let existing = &existing_root["board_status_summaries"][0];
8960        assert_eq!(matched, 1);
8961        assert_eq!(updated, 3);
8962        assert_eq!(created, 0);
8963        assert_eq!(existing["board_status"], "completed");
8964        assert_eq!(existing["completed_task_count"], 2);
8965        assert!(
8966            existing["updated_at"]
8967                .as_str()
8968                .is_some_and(|value| !value.is_empty() && value != "stale")
8969        );
8970
8971        let mut missing_root = json!({
8972            "next_id": 20,
8973            "board_status_summaries": []
8974        });
8975        let mut matched_missing = 0;
8976        let mut updated_missing = 4;
8977        let mut created_missing = 0;
8978        dynamic_upsert_record_primitive(
8979            &mut missing_root,
8980            &request,
8981            &BTreeMap::new(),
8982            &primitive,
8983            0,
8984            &mut matched_missing,
8985            &mut updated_missing,
8986            1,
8987            &mut created_missing,
8988        )
8989        .expect("create missing upsert row");
8990        let created_row = &missing_root["board_status_summaries"][0];
8991        assert_eq!(matched_missing, 0);
8992        assert_eq!(updated_missing, 4);
8993        assert_eq!(created_missing, 1);
8994        assert_eq!(created_row["id"], 20);
8995        assert_eq!(created_row["board_id"], "4");
8996        assert_eq!(created_row["board_status"], "completed");
8997        assert_eq!(created_row["completed_task_count"], 4);
8998        assert!(
8999            created_row["created_at"]
9000                .as_str()
9001                .is_some_and(|value| !value.is_empty())
9002        );
9003    }
9004
9005    #[test]
9006    /// Performs replace records from input list replaces updates and removes in order.
9007    fn replace_records_from_input_list_replaces_updates_and_removes_in_order() {
9008        let mut root = json!({
9009            "next_id": 8,
9010            "board_tasks": [
9011                { "id": 3, "board_id": 4, "title": "Legacy 2", "sort_order": 2, "status": "old", "workflow_state": "old" },
9012                { "id": 1, "board_id": 4, "title": "Legacy 1", "sort_order": 1, "status": "old", "workflow_state": "old" },
9013                { "id": 9, "board_id": 8, "title": "Other Board", "sort_order": 1, "status": "keep", "workflow_state": "keep" },
9014                { "id": 2, "board_id": 4, "title": "Legacy 3", "sort_order": 3, "status": "old", "workflow_state": "old" }
9015            ]
9016        });
9017        let primitive = json!({
9018            "collection": "board_tasks",
9019            "scope_match": { "field": "board_id", "from_input": "board_id" },
9020            "list_source": { "from_input": "task_titles", "delimiter": "," },
9021            "order_field": "sort_order",
9022            "updates": {
9023                "title": { "source": "list_item" },
9024                "sort_order": { "source": "list_index" },
9025                "status": { "source": "literal", "value": "pending" },
9026                "workflow_state": { "source": "literal", "value": "created" }
9027            },
9028            "record_contract": {
9029                "id": { "source": "next_id" },
9030                "board_id": { "source": "input", "field": "board_id" },
9031                "title": { "source": "list_item" },
9032                "sort_order": { "source": "list_index" },
9033                "status": { "source": "literal", "value": "pending" },
9034                "workflow_state": { "source": "literal", "value": "created" }
9035            }
9036        });
9037        let request = request_with_form("", "board_id=4&task_titles=Replaced+One,Replaced+Two");
9038        let mut matched = 0;
9039        let mut created = 0;
9040        let mut updated = 0;
9041        let mut skipped = 0;
9042        let mut removed = 0;
9043        dynamic_replace_records_from_input_list_primitive(
9044            &mut root,
9045            &request,
9046            &BTreeMap::new(),
9047            &primitive,
9048            &mut matched,
9049            &mut created,
9050            &mut updated,
9051            &mut skipped,
9052            &mut removed,
9053        )
9054        .expect("replace primitive should succeed");
9055        let board_rows = root["board_tasks"]
9056            .as_array()
9057            .expect("board tasks should remain an array")
9058            .iter()
9059            .filter(|row| row["board_id"] == json!(4))
9060            .cloned()
9061            .collect::<Vec<_>>();
9062        assert_eq!(matched, 3);
9063        assert_eq!(created, 0);
9064        assert_eq!(updated, 2);
9065        assert_eq!(skipped, 0);
9066        assert_eq!(removed, 1);
9067        assert_eq!(board_rows.len(), 2);
9068        assert_eq!(board_rows[0]["id"], 3);
9069        assert_eq!(board_rows[0]["title"], "Replaced Two");
9070        assert_eq!(board_rows[0]["sort_order"], 2);
9071        assert_eq!(board_rows[1]["id"], 1);
9072        assert_eq!(board_rows[1]["title"], "Replaced One");
9073        assert_eq!(board_rows[1]["sort_order"], 1);
9074        assert!(
9075            root["board_tasks"]
9076                .as_array()
9077                .expect("board tasks array")
9078                .iter()
9079                .any(|row| row["board_id"] == json!(8) && row["title"] == "Other Board")
9080        );
9081    }
9082
9083    #[test]
9084    /// Performs replace records from input list counts truncated items as skipped.
9085    fn replace_records_from_input_list_counts_truncated_items_as_skipped() {
9086        let mut root = json!({
9087            "next_id": 4,
9088            "board_tasks": [
9089                { "id": 1, "board_id": 4, "title": "Legacy 1", "sort_order": 1 },
9090                { "id": 2, "board_id": 4, "title": "Legacy 2", "sort_order": 2 }
9091            ]
9092        });
9093        let primitive = json!({
9094            "collection": "board_tasks",
9095            "scope_match": { "field": "board_id", "from_input": "board_id" },
9096            "list_source": { "from_input": "task_titles", "delimiter": "," },
9097            "order_field": "sort_order",
9098            "max_records": 2,
9099            "overflow_policy": "truncate",
9100            "updates": {
9101                "title": { "source": "list_item" },
9102                "sort_order": { "source": "list_index" }
9103            },
9104            "record_contract": {
9105                "id": { "source": "next_id" },
9106                "board_id": { "source": "input", "field": "board_id" },
9107                "title": { "source": "list_item" },
9108                "sort_order": { "source": "list_index" }
9109            }
9110        });
9111        let request = request_with_form("", "board_id=4&task_titles=One,Two,Three,Four");
9112        let mut matched = 0;
9113        let mut created = 0;
9114        let mut updated = 0;
9115        let mut skipped = 0;
9116        let mut removed = 0;
9117        dynamic_replace_records_from_input_list_primitive(
9118            &mut root,
9119            &request,
9120            &BTreeMap::new(),
9121            &primitive,
9122            &mut matched,
9123            &mut created,
9124            &mut updated,
9125            &mut skipped,
9126            &mut removed,
9127        )
9128        .expect("replace primitive should truncate within bounds");
9129        assert_eq!(matched, 2);
9130        assert_eq!(created, 0);
9131        assert_eq!(updated, 2);
9132        assert_eq!(skipped, 2);
9133        assert_eq!(removed, 0);
9134    }
9135
9136    #[test]
9137    /// Performs approval transition sets expected actor reason and timestamp fields.
9138    fn approval_transition_sets_expected_actor_reason_and_timestamp_fields() {
9139        let mut approval = serde_json::Map::from_iter([
9140            ("id".to_owned(), json!(17)),
9141            ("status".to_owned(), json!("submitted")),
9142            ("workflow_state".to_owned(), json!("submitted")),
9143            ("approved_by".to_owned(), json!("")),
9144            ("approved_at".to_owned(), json!("")),
9145            ("rejected_by".to_owned(), json!("stale")),
9146            ("rejected_at".to_owned(), json!("stale")),
9147            ("rejection_reason".to_owned(), json!("stale")),
9148        ]);
9149        let approve_primitive = json!({
9150            "transition_kind": "approve",
9151            "approver_input": "approver",
9152            "approved_by_field": "approved_by",
9153            "approved_at_field": "approved_at",
9154            "rejected_by_field": "rejected_by",
9155            "rejected_at_field": "rejected_at",
9156            "rejection_reason_field": "rejection_reason"
9157        });
9158        let approve_request = request_with_form("", "approver=owner1");
9159        dynamic_apply_approval_transition(
9160            &mut approval,
9161            &approve_primitive,
9162            &approve_request,
9163            &BTreeMap::new(),
9164        )
9165        .expect("approve transition");
9166        assert_eq!(approval.get("status"), Some(&json!("approved")));
9167        assert_eq!(approval.get("workflow_state"), Some(&json!("approved")));
9168        assert_eq!(approval.get("approved_by"), Some(&json!("owner1")));
9169        assert_eq!(approval.get("rejected_by"), Some(&json!("")));
9170        assert_eq!(approval.get("rejected_at"), Some(&json!("")));
9171        assert_eq!(approval.get("rejection_reason"), Some(&json!("")));
9172        assert!(
9173            approval
9174                .get("approved_at")
9175                .and_then(|value| value.as_str())
9176                .is_some_and(|value| !value.is_empty())
9177        );
9178
9179        let mut rejection = serde_json::Map::from_iter([
9180            ("id".to_owned(), json!(18)),
9181            ("status".to_owned(), json!("submitted")),
9182            ("workflow_state".to_owned(), json!("submitted")),
9183            ("approved_by".to_owned(), json!("owner1")),
9184            ("approved_at".to_owned(), json!("stale")),
9185            ("rejected_by".to_owned(), json!("")),
9186            ("rejected_at".to_owned(), json!("")),
9187            ("rejection_reason".to_owned(), json!("")),
9188        ]);
9189        let reject_primitive = json!({
9190            "transition_kind": "reject",
9191            "approver_input": "approver",
9192            "reason_input": "reason",
9193            "approved_by_field": "approved_by",
9194            "approved_at_field": "approved_at",
9195            "rejected_by_field": "rejected_by",
9196            "rejected_at_field": "rejected_at",
9197            "rejection_reason_field": "rejection_reason"
9198        });
9199        let reject_request = request_with_form("", "approver=owner2&reason=missing+context");
9200        dynamic_apply_approval_transition(
9201            &mut rejection,
9202            &reject_primitive,
9203            &reject_request,
9204            &BTreeMap::new(),
9205        )
9206        .expect("reject transition");
9207        assert_eq!(rejection.get("status"), Some(&json!("rejected")));
9208        assert_eq!(rejection.get("workflow_state"), Some(&json!("rejected")));
9209        assert_eq!(rejection.get("approved_by"), Some(&json!("")));
9210        assert_eq!(rejection.get("rejected_by"), Some(&json!("owner2")));
9211        assert_eq!(
9212            rejection.get("rejection_reason"),
9213            Some(&json!("missing context"))
9214        );
9215        assert!(
9216            rejection
9217                .get("rejected_at")
9218                .and_then(|value| value.as_str())
9219                .is_some_and(|value| !value.is_empty())
9220        );
9221    }
9222
9223    #[test]
9224    /// Performs local file record adapter returns false when append path is not writable file.
9225    fn local_file_record_adapter_returns_false_when_append_path_is_not_writable_file() {
9226        let root = unique_test_dir("dynamic-file-record");
9227        let state_root = root.join("state");
9228        fs::create_dir_all(state_root.join("exports")).expect("create exports directory");
9229        let state_path = state_root.join("runtime-state.json");
9230        fs::write(&state_path, "{}").expect("write state");
9231        let integrations = json!({
9232            "adapters": [{
9233                "adapter_id": "local-board-history",
9234                "adapter_kind": "append_jsonl_local",
9235                "relative_path": "exports"
9236            }]
9237        });
9238        let action = json!({ "entity_id": "board_task" });
9239        let file_record = json!({
9240            "adapter_id": "local-board-history",
9241            "record": {
9242                "board_id": { "source": "literal", "value": 4 }
9243            }
9244        });
9245        let request = request_with_form("", "");
9246        assert!(!dynamic_append_file_record(
9247            &state_path,
9248            &integrations,
9249            &action,
9250            &file_record,
9251            &request,
9252            &BTreeMap::new(),
9253            "append_history",
9254            "board_task",
9255            "append_history_succeeded",
9256            0,
9257        ));
9258        fs::remove_dir_all(root).expect("remove test root");
9259    }
9260}