Skip to main content

merman_core/
resources.rs

1//! Backend-independent Mermaid source and semantic-model resource policy.
2
3use crate::diagram::{ParsedDiagramRender, RenderSemanticModel};
4#[cfg(any(
5    feature = "diagram-flowchart",
6    feature = "diagram-swimlane",
7    feature = "diagram-agentflow"
8))]
9use crate::diagrams::flowchart::FlowchartModel;
10#[cfg(feature = "diagram-ishikawa")]
11use crate::diagrams::ishikawa::IshikawaDiagramRenderModel;
12#[cfg(feature = "diagram-kanban")]
13use crate::diagrams::kanban::KanbanDiagramRenderModel;
14#[cfg(feature = "diagram-mindmap")]
15use crate::diagrams::mindmap::MindmapDiagramRenderModel;
16#[cfg(feature = "diagram-radar")]
17use crate::diagrams::radar::RadarDiagramRenderModel;
18#[cfg(feature = "diagram-requirement")]
19use crate::diagrams::requirement::RequirementDiagramRenderModel;
20#[cfg(feature = "diagram-sequence")]
21use crate::diagrams::sequence::SequenceDiagramRenderModel;
22#[cfg(feature = "diagram-treemap")]
23use crate::diagrams::treemap::TreemapDiagramRenderModel;
24#[cfg(feature = "diagram-zenuml")]
25use crate::diagrams::zenuml::{ZenumlDiagramRenderModel, ZenumlStatementKind};
26#[cfg(feature = "diagram-class")]
27use crate::models::class_diagram::ClassDiagram;
28use serde::Serialize;
29use serde::ser::{
30    SerializeMap, SerializeSeq, SerializeStruct, SerializeStructVariant, SerializeTuple,
31    SerializeTupleStruct, SerializeTupleVariant,
32};
33
34const KIB: usize = 1024;
35const MIB: usize = 1024 * KIB;
36
37pub const RESOURCE_PROFILE_COUNT: usize = 4;
38pub const INPUT_RESOURCE_LIMIT_COUNT: usize = 4;
39
40#[repr(usize)]
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42pub enum ResourceProfile {
43    Interactive,
44    Constrained,
45    TrustedNative,
46    UnboundedForTrustedInput,
47}
48
49impl ResourceProfile {
50    pub const ALL: [Self; RESOURCE_PROFILE_COUNT] = [
51        Self::Interactive,
52        Self::Constrained,
53        Self::TrustedNative,
54        Self::UnboundedForTrustedInput,
55    ];
56
57    pub const fn descriptor(self) -> &'static ResourceProfileDescriptor {
58        &RESOURCE_PROFILE_DESCRIPTORS[self as usize]
59    }
60
61    pub const fn id(self) -> &'static str {
62        self.descriptor().id
63    }
64
65    pub fn from_id(id: &str) -> Option<Self> {
66        RESOURCE_PROFILE_DESCRIPTORS
67            .iter()
68            .find(|descriptor| descriptor.id == id)
69            .map(|descriptor| descriptor.profile)
70    }
71}
72
73impl std::fmt::Display for ResourceProfile {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        f.write_str(self.id())
76    }
77}
78
79impl std::str::FromStr for ResourceProfile {
80    type Err = String;
81
82    fn from_str(value: &str) -> Result<Self, Self::Err> {
83        Self::from_id(value).ok_or_else(|| {
84            let supported = RESOURCE_PROFILE_DESCRIPTORS
85                .iter()
86                .map(|descriptor| descriptor.id)
87                .collect::<Vec<_>>()
88                .join(", ");
89            format!("unsupported resource profile `{value}`; expected one of: {supported}")
90        })
91    }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95#[non_exhaustive]
96pub struct ResourceProfileDescriptor {
97    pub profile: ResourceProfile,
98    pub id: &'static str,
99    pub purpose: &'static str,
100    pub trust_assumption: &'static str,
101    pub recommended_binding_default: bool,
102}
103
104pub static RESOURCE_PROFILE_DESCRIPTORS: [ResourceProfileDescriptor; RESOURCE_PROFILE_COUNT] = [
105    ResourceProfileDescriptor {
106        profile: ResourceProfile::Interactive,
107        id: "interactive",
108        purpose: "General interactive applications and public binding surfaces",
109        trust_assumption: "Cooperative user-authored input; not a hostile or multi-tenant isolation boundary",
110        recommended_binding_default: true,
111    },
112    ResourceProfileDescriptor {
113        profile: ResourceProfile::Constrained,
114        id: "constrained",
115        purpose: "Constrained rendering for untrusted or publicly submitted documents",
116        trust_assumption: "The host must provide timeout, memory, concurrency, and preemption controls",
117        recommended_binding_default: false,
118    },
119    ResourceProfileDescriptor {
120        profile: ResourceProfile::TrustedNative,
121        id: "trusted-native",
122        purpose: "Local CLI and controlled native batch rendering",
123        trust_assumption: "Input is trusted and the native host controls the workload",
124        recommended_binding_default: false,
125    },
126    ResourceProfileDescriptor {
127        profile: ResourceProfile::UnboundedForTrustedInput,
128        id: "unbounded-for-trusted-input",
129        purpose: "Explicitly disable policy budgets while retaining hard backend capabilities",
130        trust_assumption: "Input is fully trusted and the host provides outer isolation",
131        recommended_binding_default: false,
132    },
133];
134
135pub const GENERAL_BINDING_DEFAULT_RESOURCE_PROFILE: ResourceProfile = ResourceProfile::Interactive;
136pub const CLI_DEFAULT_RESOURCE_PROFILE: ResourceProfile = ResourceProfile::TrustedNative;
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
139pub enum InputResourceLimitPhase {
140    Source,
141    Model,
142}
143
144impl InputResourceLimitPhase {
145    pub const fn as_str(self) -> &'static str {
146        match self {
147            Self::Source => "source",
148            Self::Model => "layout_model",
149        }
150    }
151}
152
153impl std::fmt::Display for InputResourceLimitPhase {
154    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155        f.write_str(self.as_str())
156    }
157}
158
159#[repr(usize)]
160#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
161pub enum InputResourceLimitId {
162    MaxSourceBytes,
163    MaxModelItems,
164    MaxModelTextBytes,
165    MaxModelNestingDepth,
166}
167
168impl InputResourceLimitId {
169    pub const ALL: [Self; INPUT_RESOURCE_LIMIT_COUNT] = [
170        Self::MaxSourceBytes,
171        Self::MaxModelItems,
172        Self::MaxModelTextBytes,
173        Self::MaxModelNestingDepth,
174    ];
175
176    pub const fn index(self) -> usize {
177        self as usize
178    }
179
180    pub fn from_stable_id(id: &str) -> Option<Self> {
181        INPUT_RESOURCE_LIMIT_DESCRIPTORS
182            .iter()
183            .find(|descriptor| descriptor.stable_id == id)
184            .map(|descriptor| descriptor.id)
185    }
186
187    pub const fn descriptor(self) -> &'static InputResourceLimitDescriptor {
188        &INPUT_RESOURCE_LIMIT_DESCRIPTORS[self.index()]
189    }
190
191    pub const fn as_str(self) -> &'static str {
192        self.descriptor().stable_id
193    }
194}
195
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197#[non_exhaustive]
198pub struct InputResourceLimitDescriptor {
199    pub id: InputResourceLimitId,
200    pub stable_id: &'static str,
201    pub phase: InputResourceLimitPhase,
202    pub description: &'static str,
203    pub overridable: bool,
204    pub minimum_value: usize,
205}
206
207macro_rules! input_limit_descriptors {
208    ($($id:ident => ($stable:literal, $phase:ident, $description:literal)),+ $(,)?) => {
209        pub static INPUT_RESOURCE_LIMIT_DESCRIPTORS:
210            [InputResourceLimitDescriptor; INPUT_RESOURCE_LIMIT_COUNT] = [
211                $(InputResourceLimitDescriptor {
212                    id: InputResourceLimitId::$id,
213                    stable_id: $stable,
214                    phase: InputResourceLimitPhase::$phase,
215                    description: $description,
216                    overridable: true,
217                    minimum_value: 1,
218                }),+
219            ];
220    };
221}
222
223input_limit_descriptors! {
224    MaxSourceBytes => ("max_source_bytes", Source, "Maximum UTF-8 Mermaid source bytes"),
225    MaxModelItems => ("max_model_items", Model, "Maximum semantic model entities and relationships across all diagram families"),
226    MaxModelTextBytes => ("max_model_text_bytes", Model, "Maximum aggregate UTF-8 text retained by a semantic model"),
227    MaxModelNestingDepth => ("max_model_nesting_depth", Model, "Maximum semantic nesting depth accepted before layout"),
228}
229
230const PROFILE_VALUES: [[Option<usize>; RESOURCE_PROFILE_COUNT]; INPUT_RESOURCE_LIMIT_COUNT] = [
231    [Some(2 * MIB), Some(MIB), Some(16 * MIB), None],
232    // Policy defaults, not Mermaid syntax limits. They deliberately preserve the
233    // aggregate headroom of the previous per-family cardinality budgets.
234    [Some(32_000), Some(16_000), Some(200_000), None],
235    [Some(2 * MIB), Some(MIB), Some(16 * MIB), None],
236    [Some(256), Some(128), Some(1_024), None],
237];
238
239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
240pub struct InputResourcePolicy {
241    profile: ResourceProfile,
242    base_values: [Option<usize>; INPUT_RESOURCE_LIMIT_COUNT],
243    effective_values: [Option<usize>; INPUT_RESOURCE_LIMIT_COUNT],
244    explicit_overrides: [Option<usize>; INPUT_RESOURCE_LIMIT_COUNT],
245}
246
247impl Default for InputResourcePolicy {
248    fn default() -> Self {
249        Self::for_profile(GENERAL_BINDING_DEFAULT_RESOURCE_PROFILE)
250    }
251}
252
253impl InputResourcePolicy {
254    pub const fn for_profile(profile: ResourceProfile) -> Self {
255        let mut values = [None; INPUT_RESOURCE_LIMIT_COUNT];
256        let mut index = 0;
257        while index < INPUT_RESOURCE_LIMIT_COUNT {
258            values[index] = PROFILE_VALUES[index][profile as usize];
259            index += 1;
260        }
261        Self {
262            profile,
263            base_values: values,
264            effective_values: values,
265            explicit_overrides: [None; INPUT_RESOURCE_LIMIT_COUNT],
266        }
267    }
268
269    pub const fn profile(self) -> ResourceProfile {
270        self.profile
271    }
272
273    pub const fn value(self, id: InputResourceLimitId) -> Option<usize> {
274        self.effective_values[id.index()]
275    }
276
277    pub const fn base_value(self, id: InputResourceLimitId) -> Option<usize> {
278        self.base_values[id.index()]
279    }
280
281    pub const fn explicit_override(self, id: InputResourceLimitId) -> Option<usize> {
282        self.explicit_overrides[id.index()]
283    }
284
285    pub fn explicit_overrides(&self) -> impl Iterator<Item = (InputResourceLimitId, usize)> + '_ {
286        InputResourceLimitId::ALL
287            .into_iter()
288            .filter_map(|id| self.explicit_override(id).map(|value| (id, value)))
289    }
290
291    pub fn apply_override(
292        &mut self,
293        stable_id: &str,
294        value: usize,
295    ) -> Result<(), InputResourceLimitOverrideError> {
296        let id = InputResourceLimitId::from_stable_id(stable_id)
297            .ok_or_else(|| InputResourceLimitOverrideError::UnknownLimit(stable_id.to_string()))?;
298        self.apply_limit(id, value)
299    }
300
301    pub fn apply_limit(
302        &mut self,
303        id: InputResourceLimitId,
304        value: usize,
305    ) -> Result<(), InputResourceLimitOverrideError> {
306        if value == 0 {
307            return Err(InputResourceLimitOverrideError::NonPositive(id.as_str()));
308        }
309        self.effective_values[id.index()] = Some(value);
310        self.explicit_overrides[id.index()] = Some(value);
311        Ok(())
312    }
313
314    pub fn with_limit(
315        mut self,
316        id: InputResourceLimitId,
317        value: usize,
318    ) -> Result<Self, InputResourceLimitOverrideError> {
319        self.apply_limit(id, value)?;
320        Ok(self)
321    }
322
323    fn check_limit(
324        &self,
325        phase: InputResourceLimitPhase,
326        id: InputResourceLimitId,
327        actual: usize,
328    ) -> Result<(), InputResourceLimitExceeded> {
329        let Some(max) = self.value(id) else {
330            return Ok(());
331        };
332        if actual <= max {
333            return Ok(());
334        }
335        Err(InputResourceLimitExceeded {
336            phase,
337            limit: id.as_str(),
338            actual,
339            max,
340            profile: self.profile,
341            explicit_overrides: self
342                .explicit_overrides()
343                .map(|(id, value)| InputResourceLimitOverride { id, value })
344                .collect(),
345        })
346    }
347
348    pub fn check_source_bytes(&self, source: &str) -> Result<(), InputResourceLimitExceeded> {
349        self.check_limit(
350            InputResourceLimitPhase::Source,
351            InputResourceLimitId::MaxSourceBytes,
352            source.len(),
353        )
354    }
355
356    /// Checks only the public semantic-model projection.
357    ///
358    /// Use [`Self::check_parsed_render`] for a parsed built-in render operation so parser-owned
359    /// render context is included in the text budget.
360    pub fn check_render_model(
361        &self,
362        model: &RenderSemanticModel,
363    ) -> Result<(), InputResourceLimitExceeded> {
364        self.check_model_complexity(ModelComplexity::from_render_model(model))
365    }
366
367    pub fn check_parsed_render(
368        &self,
369        parsed: &ParsedDiagramRender,
370    ) -> Result<(), InputResourceLimitExceeded> {
371        let mut complexity = ModelComplexity::from_render_model(parsed.model());
372        complexity.text_bytes = complexity
373            .text_bytes
374            .saturating_add(parsed.retained_render_context_bytes());
375        self.check_model_complexity(complexity)
376    }
377
378    pub fn check_model_complexity(
379        &self,
380        complexity: ModelComplexity,
381    ) -> Result<(), InputResourceLimitExceeded> {
382        self.check_limit(
383            InputResourceLimitPhase::Model,
384            InputResourceLimitId::MaxModelItems,
385            complexity.items,
386        )?;
387        self.check_limit(
388            InputResourceLimitPhase::Model,
389            InputResourceLimitId::MaxModelTextBytes,
390            complexity.text_bytes,
391        )?;
392        self.check_limit(
393            InputResourceLimitPhase::Model,
394            InputResourceLimitId::MaxModelNestingDepth,
395            complexity.nesting_depth,
396        )
397    }
398
399    #[cfg(any(
400        feature = "diagram-flowchart",
401        feature = "diagram-swimlane",
402        feature = "diagram-agentflow"
403    ))]
404    pub fn check_flowchart_complexity(
405        &self,
406        model: &FlowchartModel,
407    ) -> Result<FlowchartComplexity, InputResourceLimitExceeded> {
408        let complexity = FlowchartComplexity::from_model(model);
409        self.check_model_complexity(complexity.as_model_complexity())?;
410        Ok(complexity)
411    }
412
413    #[cfg(feature = "diagram-class")]
414    pub fn check_class_complexity(
415        &self,
416        model: &ClassDiagram,
417    ) -> Result<ClassComplexity, InputResourceLimitExceeded> {
418        let complexity = ClassComplexity::from_model(model);
419        self.check_model_complexity(complexity.as_model_complexity())?;
420        Ok(complexity)
421    }
422
423    #[cfg(feature = "diagram-mindmap")]
424    pub fn check_mindmap_complexity(
425        &self,
426        model: &MindmapDiagramRenderModel,
427    ) -> Result<MindmapComplexity, InputResourceLimitExceeded> {
428        let complexity = MindmapComplexity::from_model(model);
429        self.check_model_complexity(complexity.as_model_complexity())?;
430        Ok(complexity)
431    }
432
433    #[cfg(feature = "diagram-zenuml")]
434    pub fn check_zenuml_complexity(
435        &self,
436        model: &ZenumlDiagramRenderModel,
437    ) -> Result<ZenumlComplexity, InputResourceLimitExceeded> {
438        let complexity = ZenumlComplexity::from_model(model);
439        self.check_model_complexity(complexity.as_model_complexity())?;
440        Ok(complexity)
441    }
442
443    #[cfg(feature = "diagram-sequence")]
444    pub fn check_sequence_complexity(
445        &self,
446        model: &SequenceDiagramRenderModel,
447    ) -> Result<SequenceComplexity, InputResourceLimitExceeded> {
448        let complexity = SequenceComplexity::from_model(model);
449        self.check_model_complexity(complexity.as_model_complexity())?;
450        Ok(complexity)
451    }
452}
453
454#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
455pub enum InputResourceLimitOverrideError {
456    #[error("resource limit id `{0}` is not part of input resource contract schema 1")]
457    UnknownLimit(String),
458    #[error("resource limit `{0}` must be a positive integer")]
459    NonPositive(&'static str),
460}
461
462#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
463#[error("resource limit exceeded during {phase}: {limit} actual={actual} max={max}")]
464#[non_exhaustive]
465pub struct InputResourceLimitExceeded {
466    pub phase: InputResourceLimitPhase,
467    pub limit: &'static str,
468    pub actual: usize,
469    pub max: usize,
470    pub profile: ResourceProfile,
471    pub explicit_overrides: Vec<InputResourceLimitOverride>,
472}
473
474#[derive(Debug, Clone, Copy, PartialEq, Eq)]
475pub struct InputResourceLimitOverride {
476    pub id: InputResourceLimitId,
477    pub value: usize,
478}
479
480#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
481pub struct ModelComplexity {
482    pub items: usize,
483    pub text_bytes: usize,
484    pub nesting_depth: usize,
485}
486
487impl ModelComplexity {
488    pub const fn new(items: usize, text_bytes: usize, nesting_depth: usize) -> Self {
489        Self {
490            items,
491            text_bytes,
492            nesting_depth,
493        }
494    }
495
496    pub fn from_render_model(model: &RenderSemanticModel) -> Self {
497        match model {
498            RenderSemanticModel::Error(model) => Self::from_serializable(model),
499            RenderSemanticModel::CustomJson(model) => {
500                let mut complexity = Self::from_serializable(model.value());
501                complexity.text_bytes = complexity
502                    .text_bytes
503                    .saturating_add(model.model_name().len());
504                complexity
505            }
506            #[cfg(feature = "diagram-mindmap")]
507            RenderSemanticModel::Mindmap(model) => {
508                MindmapComplexity::from_model(model).as_model_complexity()
509            }
510            #[cfg(feature = "diagram-state")]
511            RenderSemanticModel::State(model) => Self::from_serializable(model),
512            #[cfg(feature = "diagram-sequence")]
513            RenderSemanticModel::Sequence(model) => {
514                SequenceComplexity::from_model(model).as_model_complexity()
515            }
516            #[cfg(feature = "diagram-zenuml")]
517            RenderSemanticModel::Zenuml(model) => {
518                ZenumlComplexity::from_model(model).as_model_complexity()
519            }
520            #[cfg(any(feature = "diagram-flowchart", feature = "diagram-swimlane"))]
521            RenderSemanticModel::Flowchart(model) => {
522                FlowchartComplexity::from_model(model).as_model_complexity()
523            }
524            #[cfg(feature = "diagram-architecture")]
525            RenderSemanticModel::Architecture(model) => Self::from_serializable(model),
526            #[cfg(feature = "diagram-class")]
527            RenderSemanticModel::Class(model) => {
528                ClassComplexity::from_model(model).as_model_complexity()
529            }
530            #[cfg(feature = "diagram-c4")]
531            RenderSemanticModel::C4(model) => Self::from_serializable(model),
532            #[cfg(feature = "diagram-cynefin")]
533            RenderSemanticModel::Cynefin(model) => Self::from_serializable(model),
534            #[cfg(feature = "diagram-railroad")]
535            RenderSemanticModel::Railroad(model) => Self::from_serializable(model),
536            #[cfg(feature = "diagram-kanban")]
537            RenderSemanticModel::Kanban(model) => Self::from_kanban(model),
538            #[cfg(feature = "diagram-gantt")]
539            RenderSemanticModel::Gantt(model) => Self::from_serializable(model),
540            #[cfg(feature = "diagram-pie")]
541            RenderSemanticModel::Pie(model) => Self::from_serializable(model),
542            #[cfg(feature = "diagram-packet")]
543            RenderSemanticModel::Packet(model) => Self::from_serializable(model),
544            #[cfg(feature = "diagram-timeline")]
545            RenderSemanticModel::Timeline(model) => Self::from_serializable(model),
546            #[cfg(feature = "diagram-journey")]
547            RenderSemanticModel::Journey(model) => Self::from_serializable(model),
548            #[cfg(feature = "diagram-requirement")]
549            RenderSemanticModel::Requirement(model) => Self::from_requirement(model),
550            #[cfg(feature = "diagram-sankey")]
551            RenderSemanticModel::Sankey(model) => Self::from_serializable(model),
552            #[cfg(feature = "diagram-radar")]
553            RenderSemanticModel::Radar(model) => Self::from_radar(model),
554            #[cfg(feature = "diagram-info")]
555            RenderSemanticModel::Info(model) => Self::from_serializable(model),
556            #[cfg(feature = "diagram-treemap")]
557            RenderSemanticModel::Treemap(model) => {
558                TreemapComplexity::from_model(model).as_model_complexity()
559            }
560            #[cfg(feature = "diagram-block")]
561            RenderSemanticModel::Block(model) => Self::from_serializable(model),
562            #[cfg(feature = "diagram-er")]
563            RenderSemanticModel::Er(model) => Self::from_serializable(model),
564            #[cfg(feature = "diagram-quadrant-chart")]
565            RenderSemanticModel::QuadrantChart(model) => Self::from_serializable(model),
566            #[cfg(feature = "diagram-xychart")]
567            RenderSemanticModel::XyChart(model) => Self::from_serializable(model),
568            #[cfg(feature = "diagram-git-graph")]
569            RenderSemanticModel::GitGraph(model) => Self::from_serializable(model),
570            #[cfg(feature = "diagram-tree-view")]
571            RenderSemanticModel::TreeView(model) => Self::from_serializable(model),
572            #[cfg(feature = "diagram-ishikawa")]
573            RenderSemanticModel::Ishikawa(model) => {
574                IshikawaComplexity::from_model(model).as_model_complexity()
575            }
576            #[cfg(feature = "diagram-event-modeling")]
577            RenderSemanticModel::EventModeling(model) => Self::from_serializable(model),
578            #[cfg(feature = "diagram-venn")]
579            RenderSemanticModel::Venn(model) => Self::from_serializable(model),
580            #[cfg(feature = "diagram-wardley")]
581            RenderSemanticModel::Wardley(model) => Self::from_serializable(model),
582            #[cfg(feature = "diagram-usecase")]
583            RenderSemanticModel::Usecase(model) => Self::from_serializable(model),
584            #[cfg(feature = "diagram-agentflow")]
585            RenderSemanticModel::Agentflow(model) => Self::from_serializable(model),
586        }
587    }
588
589    /// Counts the stable serialized model shape used by JSON-returning transport operations.
590    pub fn from_serializable<T: Serialize + ?Sized>(model: &T) -> Self {
591        let mut counter = ModelComplexitySerializer::default();
592        model
593            .serialize(&mut counter)
594            .expect("model complexity serialization is infallible");
595        counter.finish()
596    }
597
598    #[cfg(feature = "diagram-kanban")]
599    pub fn from_kanban(model: &KanbanDiagramRenderModel) -> Self {
600        let text_bytes = model.nodes.iter().fold(0usize, |total, node| {
601            [
602                Some(node.id.as_str()),
603                Some(node.label.as_str()),
604                node.parent_id.as_deref(),
605                node.ticket.as_deref(),
606                node.priority.as_deref(),
607                node.assigned.as_deref(),
608                node.icon.as_deref(),
609            ]
610            .into_iter()
611            .flatten()
612            .fold(total, |subtotal, value| {
613                subtotal.saturating_add(value.len())
614            })
615        });
616        let nesting_depth = kanban_nesting_depth(model);
617        Self::new(model.nodes.len(), text_bytes, nesting_depth)
618    }
619
620    #[cfg(feature = "diagram-radar")]
621    pub fn from_radar(model: &RadarDiagramRenderModel) -> Self {
622        let common_text_bytes = [
623            model.title.as_deref(),
624            model.acc_title.as_deref(),
625            model.acc_descr.as_deref(),
626            Some(model.options.graticule.as_str()),
627        ]
628        .into_iter()
629        .flatten()
630        .fold(0usize, |total, value| total.saturating_add(value.len()));
631        let axis_text_bytes = model.axes.iter().fold(0usize, |total, axis| {
632            total
633                .saturating_add(axis.name.len())
634                .saturating_add(axis.label.len())
635        });
636        let (curve_items, curve_text_bytes) =
637            model
638                .curves
639                .iter()
640                .fold((0usize, 0usize), |(items, text_bytes), curve| {
641                    (
642                        items.saturating_add(curve.entries.len()),
643                        text_bytes
644                            .saturating_add(curve.name.len())
645                            .saturating_add(curve.label.len()),
646                    )
647                });
648        Self::new(
649            model
650                .axes
651                .len()
652                .saturating_add(model.curves.len())
653                .saturating_add(curve_items),
654            common_text_bytes
655                .saturating_add(axis_text_bytes)
656                .saturating_add(curve_text_bytes),
657            0,
658        )
659    }
660
661    #[cfg(feature = "diagram-requirement")]
662    pub fn from_requirement(model: &RequirementDiagramRenderModel) -> Self {
663        let common_text_bytes = [
664            model.acc_title.as_deref(),
665            model.acc_descr.as_deref(),
666            Some(model.direction.as_str()),
667        ]
668        .into_iter()
669        .flatten()
670        .fold(0usize, |total, value| total.saturating_add(value.len()));
671        let requirement_text_bytes = model.requirements.iter().fold(0usize, |total, node| {
672            let total = [
673                node.name.as_str(),
674                node.node_type.as_str(),
675                node.requirement_id.as_str(),
676                node.text.as_str(),
677                node.risk.as_str(),
678                node.verify_method.as_str(),
679            ]
680            .into_iter()
681            .fold(total, |subtotal, value| {
682                subtotal.saturating_add(value.len())
683            });
684            node.css_styles
685                .iter()
686                .chain(&node.classes)
687                .fold(total, |subtotal, value| {
688                    subtotal.saturating_add(value.len())
689                })
690        });
691        let element_text_bytes = model.elements.iter().fold(0usize, |total, node| {
692            let total = [
693                node.name.as_str(),
694                node.element_type.as_str(),
695                node.doc_ref.as_str(),
696            ]
697            .into_iter()
698            .fold(total, |subtotal, value| {
699                subtotal.saturating_add(value.len())
700            });
701            node.css_styles
702                .iter()
703                .chain(&node.classes)
704                .fold(total, |subtotal, value| {
705                    subtotal.saturating_add(value.len())
706                })
707        });
708        let relationship_text_bytes =
709            model
710                .relationships
711                .iter()
712                .fold(0usize, |total, relationship| {
713                    [
714                        relationship.rel_type.as_str(),
715                        relationship.src.as_str(),
716                        relationship.dst.as_str(),
717                    ]
718                    .into_iter()
719                    .fold(total, |subtotal, value| {
720                        subtotal.saturating_add(value.len())
721                    })
722                });
723        let class_text_bytes = model.classes.iter().fold(0usize, |total, (key, class)| {
724            class.styles.iter().chain(&class.text_styles).fold(
725                total
726                    .saturating_add(key.len())
727                    .saturating_add(class.id.len()),
728                |subtotal, value| subtotal.saturating_add(value.len()),
729            )
730        });
731        Self::new(
732            model
733                .requirements
734                .len()
735                .saturating_add(model.elements.len())
736                .saturating_add(model.relationships.len())
737                .saturating_add(model.classes.len()),
738            common_text_bytes
739                .saturating_add(requirement_text_bytes)
740                .saturating_add(element_text_bytes)
741                .saturating_add(relationship_text_bytes)
742                .saturating_add(class_text_bytes),
743            0,
744        )
745    }
746}
747
748#[derive(Debug, Default)]
749struct ModelComplexitySerializer {
750    items: usize,
751    text_bytes: usize,
752    current_depth: usize,
753    max_depth: usize,
754}
755
756impl ModelComplexitySerializer {
757    fn enter(&mut self) {
758        self.current_depth = self.current_depth.saturating_add(1);
759        self.max_depth = self.max_depth.max(self.current_depth);
760    }
761
762    fn leave(&mut self) {
763        self.current_depth = self.current_depth.saturating_sub(1);
764    }
765
766    fn finish(self) -> ModelComplexity {
767        ModelComplexity::new(
768            self.items.max(1),
769            self.text_bytes,
770            self.max_depth.saturating_sub(1),
771        )
772    }
773}
774
775#[derive(Debug)]
776struct ModelComplexitySerializationError;
777
778impl std::fmt::Display for ModelComplexitySerializationError {
779    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
780        formatter.write_str("model complexity serialization failed")
781    }
782}
783
784impl std::error::Error for ModelComplexitySerializationError {}
785
786impl serde::ser::Error for ModelComplexitySerializationError {
787    fn custom<T: std::fmt::Display>(_message: T) -> Self {
788        Self
789    }
790}
791
792struct ModelCompound<'a> {
793    counter: &'a mut ModelComplexitySerializer,
794    count_items: bool,
795}
796
797impl<'a> ModelCompound<'a> {
798    fn new(counter: &'a mut ModelComplexitySerializer, count_items: bool) -> Self {
799        counter.enter();
800        Self {
801            counter,
802            count_items,
803        }
804    }
805
806    fn serialize_value<T: Serialize + ?Sized>(
807        &mut self,
808        value: &T,
809    ) -> Result<(), ModelComplexitySerializationError> {
810        if self.count_items {
811            self.counter.items = self.counter.items.saturating_add(1);
812        }
813        value.serialize(&mut *self.counter)
814    }
815}
816
817impl Drop for ModelCompound<'_> {
818    fn drop(&mut self) {
819        self.counter.leave();
820    }
821}
822
823impl SerializeSeq for ModelCompound<'_> {
824    type Ok = ();
825    type Error = ModelComplexitySerializationError;
826
827    fn serialize_element<T: Serialize + ?Sized>(
828        &mut self,
829        value: &T,
830    ) -> Result<Self::Ok, Self::Error> {
831        self.serialize_value(value)
832    }
833
834    fn end(self) -> Result<Self::Ok, Self::Error> {
835        Ok(())
836    }
837}
838
839impl SerializeTuple for ModelCompound<'_> {
840    type Ok = ();
841    type Error = ModelComplexitySerializationError;
842
843    fn serialize_element<T: Serialize + ?Sized>(
844        &mut self,
845        value: &T,
846    ) -> Result<Self::Ok, Self::Error> {
847        self.serialize_value(value)
848    }
849
850    fn end(self) -> Result<Self::Ok, Self::Error> {
851        Ok(())
852    }
853}
854
855impl SerializeTupleStruct for ModelCompound<'_> {
856    type Ok = ();
857    type Error = ModelComplexitySerializationError;
858
859    fn serialize_field<T: Serialize + ?Sized>(
860        &mut self,
861        value: &T,
862    ) -> Result<Self::Ok, Self::Error> {
863        self.serialize_value(value)
864    }
865
866    fn end(self) -> Result<Self::Ok, Self::Error> {
867        Ok(())
868    }
869}
870
871impl SerializeTupleVariant for ModelCompound<'_> {
872    type Ok = ();
873    type Error = ModelComplexitySerializationError;
874
875    fn serialize_field<T: Serialize + ?Sized>(
876        &mut self,
877        value: &T,
878    ) -> Result<Self::Ok, Self::Error> {
879        self.serialize_value(value)
880    }
881
882    fn end(self) -> Result<Self::Ok, Self::Error> {
883        Ok(())
884    }
885}
886
887impl SerializeMap for ModelCompound<'_> {
888    type Ok = ();
889    type Error = ModelComplexitySerializationError;
890
891    fn serialize_key<T: Serialize + ?Sized>(&mut self, key: &T) -> Result<Self::Ok, Self::Error> {
892        self.serialize_value(key)
893    }
894
895    fn serialize_value<T: Serialize + ?Sized>(
896        &mut self,
897        value: &T,
898    ) -> Result<Self::Ok, Self::Error> {
899        value.serialize(&mut *self.counter)
900    }
901
902    fn end(self) -> Result<Self::Ok, Self::Error> {
903        Ok(())
904    }
905}
906
907impl SerializeStruct for ModelCompound<'_> {
908    type Ok = ();
909    type Error = ModelComplexitySerializationError;
910
911    fn serialize_field<T: Serialize + ?Sized>(
912        &mut self,
913        _key: &'static str,
914        value: &T,
915    ) -> Result<Self::Ok, Self::Error> {
916        value.serialize(&mut *self.counter)
917    }
918
919    fn end(self) -> Result<Self::Ok, Self::Error> {
920        Ok(())
921    }
922}
923
924impl SerializeStructVariant for ModelCompound<'_> {
925    type Ok = ();
926    type Error = ModelComplexitySerializationError;
927
928    fn serialize_field<T: Serialize + ?Sized>(
929        &mut self,
930        _key: &'static str,
931        value: &T,
932    ) -> Result<Self::Ok, Self::Error> {
933        value.serialize(&mut *self.counter)
934    }
935
936    fn end(self) -> Result<Self::Ok, Self::Error> {
937        Ok(())
938    }
939}
940
941impl<'a> serde::Serializer for &'a mut ModelComplexitySerializer {
942    type Ok = ();
943    type Error = ModelComplexitySerializationError;
944    type SerializeSeq = ModelCompound<'a>;
945    type SerializeTuple = ModelCompound<'a>;
946    type SerializeTupleStruct = ModelCompound<'a>;
947    type SerializeTupleVariant = ModelCompound<'a>;
948    type SerializeMap = ModelCompound<'a>;
949    type SerializeStruct = ModelCompound<'a>;
950    type SerializeStructVariant = ModelCompound<'a>;
951
952    fn serialize_bool(self, _value: bool) -> Result<Self::Ok, Self::Error> {
953        Ok(())
954    }
955
956    fn serialize_i8(self, _value: i8) -> Result<Self::Ok, Self::Error> {
957        Ok(())
958    }
959
960    fn serialize_i16(self, _value: i16) -> Result<Self::Ok, Self::Error> {
961        Ok(())
962    }
963
964    fn serialize_i32(self, _value: i32) -> Result<Self::Ok, Self::Error> {
965        Ok(())
966    }
967
968    fn serialize_i64(self, _value: i64) -> Result<Self::Ok, Self::Error> {
969        Ok(())
970    }
971
972    fn serialize_i128(self, _value: i128) -> Result<Self::Ok, Self::Error> {
973        Ok(())
974    }
975
976    fn serialize_u8(self, _value: u8) -> Result<Self::Ok, Self::Error> {
977        Ok(())
978    }
979
980    fn serialize_u16(self, _value: u16) -> Result<Self::Ok, Self::Error> {
981        Ok(())
982    }
983
984    fn serialize_u32(self, _value: u32) -> Result<Self::Ok, Self::Error> {
985        Ok(())
986    }
987
988    fn serialize_u64(self, _value: u64) -> Result<Self::Ok, Self::Error> {
989        Ok(())
990    }
991
992    fn serialize_u128(self, _value: u128) -> Result<Self::Ok, Self::Error> {
993        Ok(())
994    }
995
996    fn serialize_f32(self, _value: f32) -> Result<Self::Ok, Self::Error> {
997        Ok(())
998    }
999
1000    fn serialize_f64(self, _value: f64) -> Result<Self::Ok, Self::Error> {
1001        Ok(())
1002    }
1003
1004    fn serialize_char(self, value: char) -> Result<Self::Ok, Self::Error> {
1005        self.text_bytes = self.text_bytes.saturating_add(value.len_utf8());
1006        Ok(())
1007    }
1008
1009    fn serialize_str(self, value: &str) -> Result<Self::Ok, Self::Error> {
1010        self.text_bytes = self.text_bytes.saturating_add(value.len());
1011        Ok(())
1012    }
1013
1014    fn serialize_bytes(self, value: &[u8]) -> Result<Self::Ok, Self::Error> {
1015        self.items = self.items.saturating_add(value.len());
1016        Ok(())
1017    }
1018
1019    fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
1020        Ok(())
1021    }
1022
1023    fn serialize_some<T: Serialize + ?Sized>(self, value: &T) -> Result<Self::Ok, Self::Error> {
1024        value.serialize(self)
1025    }
1026
1027    fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
1028        Ok(())
1029    }
1030
1031    fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok, Self::Error> {
1032        Ok(())
1033    }
1034
1035    fn serialize_unit_variant(
1036        self,
1037        _name: &'static str,
1038        _variant_index: u32,
1039        _variant: &'static str,
1040    ) -> Result<Self::Ok, Self::Error> {
1041        Ok(())
1042    }
1043
1044    fn serialize_newtype_struct<T: Serialize + ?Sized>(
1045        self,
1046        _name: &'static str,
1047        value: &T,
1048    ) -> Result<Self::Ok, Self::Error> {
1049        value.serialize(self)
1050    }
1051
1052    fn serialize_newtype_variant<T: Serialize + ?Sized>(
1053        self,
1054        _name: &'static str,
1055        _variant_index: u32,
1056        _variant: &'static str,
1057        value: &T,
1058    ) -> Result<Self::Ok, Self::Error> {
1059        value.serialize(self)
1060    }
1061
1062    fn serialize_seq(self, _length: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
1063        Ok(ModelCompound::new(self, true))
1064    }
1065
1066    fn serialize_tuple(self, _length: usize) -> Result<Self::SerializeTuple, Self::Error> {
1067        Ok(ModelCompound::new(self, true))
1068    }
1069
1070    fn serialize_tuple_struct(
1071        self,
1072        _name: &'static str,
1073        _length: usize,
1074    ) -> Result<Self::SerializeTupleStruct, Self::Error> {
1075        Ok(ModelCompound::new(self, true))
1076    }
1077
1078    fn serialize_tuple_variant(
1079        self,
1080        _name: &'static str,
1081        _variant_index: u32,
1082        _variant: &'static str,
1083        _length: usize,
1084    ) -> Result<Self::SerializeTupleVariant, Self::Error> {
1085        Ok(ModelCompound::new(self, true))
1086    }
1087
1088    fn serialize_map(self, _length: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
1089        Ok(ModelCompound::new(self, true))
1090    }
1091
1092    fn serialize_struct(
1093        self,
1094        _name: &'static str,
1095        _length: usize,
1096    ) -> Result<Self::SerializeStruct, Self::Error> {
1097        Ok(ModelCompound::new(self, false))
1098    }
1099
1100    fn serialize_struct_variant(
1101        self,
1102        _name: &'static str,
1103        _variant_index: u32,
1104        _variant: &'static str,
1105        _length: usize,
1106    ) -> Result<Self::SerializeStructVariant, Self::Error> {
1107        Ok(ModelCompound::new(self, false))
1108    }
1109
1110    fn collect_str<T: std::fmt::Display + ?Sized>(
1111        self,
1112        value: &T,
1113    ) -> Result<Self::Ok, Self::Error> {
1114        self.text_bytes = self.text_bytes.saturating_add(value.to_string().len());
1115        Ok(())
1116    }
1117
1118    fn is_human_readable(&self) -> bool {
1119        true
1120    }
1121}
1122
1123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1124#[cfg(any(
1125    feature = "diagram-flowchart",
1126    feature = "diagram-swimlane",
1127    feature = "diagram-agentflow"
1128))]
1129pub struct FlowchartComplexity {
1130    pub nodes: usize,
1131    pub edges: usize,
1132    pub subgraphs: usize,
1133    pub label_bytes: usize,
1134    pub subgraph_depth: usize,
1135}
1136
1137#[cfg(any(
1138    feature = "diagram-flowchart",
1139    feature = "diagram-swimlane",
1140    feature = "diagram-agentflow"
1141))]
1142impl FlowchartComplexity {
1143    pub fn from_model(model: &FlowchartModel) -> Self {
1144        Self {
1145            nodes: model.nodes.len().saturating_add(model.subgraphs.len()),
1146            edges: model.edges.len(),
1147            subgraphs: model.subgraphs.len(),
1148            label_bytes: flowchart_text_bytes(model),
1149            subgraph_depth: flowchart_subgraph_depth(model),
1150        }
1151    }
1152
1153    pub fn as_model_complexity(self) -> ModelComplexity {
1154        ModelComplexity::new(
1155            self.nodes.saturating_add(self.edges),
1156            self.label_bytes,
1157            self.subgraph_depth,
1158        )
1159    }
1160}
1161
1162/// Computes Treemap model complexity iteratively without serializing a user-controlled node tree.
1163#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1164#[cfg(feature = "diagram-treemap")]
1165pub struct TreemapComplexity {
1166    pub nodes: usize,
1167    pub classes: usize,
1168    pub label_bytes: usize,
1169    pub nesting_depth: usize,
1170}
1171
1172#[cfg(feature = "diagram-treemap")]
1173impl TreemapComplexity {
1174    pub fn from_model(model: &TreemapDiagramRenderModel) -> Self {
1175        let mut nodes = 0usize;
1176        let mut classes = 0usize;
1177        let mut label_bytes = [
1178            model.acc_title.as_deref(),
1179            model.acc_descr.as_deref(),
1180            model.title.as_deref(),
1181        ]
1182        .into_iter()
1183        .flatten()
1184        .fold(0usize, |total, value| total.saturating_add(value.len()));
1185        let mut nesting_depth = 0usize;
1186
1187        let mut pending = vec![(&model.root, 0usize)];
1188        while let Some((node, depth)) = pending.pop() {
1189            nodes = nodes.saturating_add(1);
1190            nesting_depth = nesting_depth.max(depth);
1191            label_bytes = label_bytes
1192                .saturating_add(node.name.len())
1193                .saturating_add(node.class_selector.as_deref().map_or(0, str::len))
1194                .saturating_add(
1195                    node.css_compiled_styles
1196                        .as_deref()
1197                        .unwrap_or(&[])
1198                        .iter()
1199                        .map(String::len)
1200                        .sum::<usize>(),
1201                );
1202            if let Some(value) = node.value.as_ref() {
1203                label_bytes = label_bytes.saturating_add(json_text_bytes(value));
1204                nodes = nodes.saturating_add(json_item_count(value));
1205            }
1206            if let Some(children) = node.children.as_ref() {
1207                for child in children.iter().rev() {
1208                    pending.push((child, depth.saturating_add(1)));
1209                }
1210            }
1211        }
1212
1213        for (class_name, class_def) in &model.classes {
1214            classes = classes.saturating_add(1);
1215            label_bytes = label_bytes
1216                .saturating_add(class_name.len())
1217                .saturating_add(class_def.id.len())
1218                .saturating_add(class_def.styles.iter().map(String::len).sum::<usize>())
1219                .saturating_add(class_def.text_styles.iter().map(String::len).sum::<usize>());
1220        }
1221
1222        Self {
1223            nodes,
1224            classes,
1225            label_bytes,
1226            nesting_depth,
1227        }
1228    }
1229
1230    pub fn as_model_complexity(self) -> ModelComplexity {
1231        ModelComplexity::new(
1232            self.nodes.saturating_add(self.classes),
1233            self.label_bytes,
1234            self.nesting_depth,
1235        )
1236    }
1237}
1238
1239/// Computes Ishikawa model complexity iteratively without growing the stack with cause depth.
1240#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1241#[cfg(feature = "diagram-ishikawa")]
1242pub struct IshikawaComplexity {
1243    pub nodes: usize,
1244    pub label_bytes: usize,
1245    pub nesting_depth: usize,
1246}
1247
1248#[cfg(feature = "diagram-ishikawa")]
1249impl IshikawaComplexity {
1250    pub fn from_model(model: &IshikawaDiagramRenderModel) -> Self {
1251        let mut complexity = Self {
1252            nodes: 0,
1253            label_bytes: [
1254                model.acc_title.as_deref(),
1255                model.acc_descr.as_deref(),
1256                model.title.as_deref(),
1257            ]
1258            .into_iter()
1259            .flatten()
1260            .fold(0usize, |total, value| total.saturating_add(value.len())),
1261            nesting_depth: 0,
1262        };
1263        let Some(root) = model.root.as_ref() else {
1264            return complexity;
1265        };
1266
1267        let mut pending = vec![(root, 0usize)];
1268        while let Some((node, depth)) = pending.pop() {
1269            complexity.nodes = complexity.nodes.saturating_add(1);
1270            complexity.label_bytes = complexity.label_bytes.saturating_add(node.text.len());
1271            complexity.nesting_depth = complexity.nesting_depth.max(depth);
1272            for child in node.children.iter().rev() {
1273                pending.push((child, depth.saturating_add(1)));
1274            }
1275        }
1276        complexity
1277    }
1278
1279    pub fn as_model_complexity(self) -> ModelComplexity {
1280        ModelComplexity::new(self.nodes, self.label_bytes, self.nesting_depth)
1281    }
1282}
1283
1284#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1285#[cfg(feature = "diagram-class")]
1286pub struct ClassComplexity {
1287    pub nodes: usize,
1288    pub edges: usize,
1289    pub namespaces: usize,
1290    pub label_bytes: usize,
1291    pub namespace_depth: usize,
1292}
1293
1294#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1295#[cfg(feature = "diagram-mindmap")]
1296pub struct MindmapComplexity {
1297    pub nodes: usize,
1298    pub edges: usize,
1299    pub label_bytes: usize,
1300    pub nesting_depth: usize,
1301}
1302
1303#[cfg(feature = "diagram-mindmap")]
1304impl MindmapComplexity {
1305    pub fn from_model(model: &MindmapDiagramRenderModel) -> Self {
1306        let node_label_bytes = model.nodes.iter().fold(0usize, |total, node| {
1307            let strings = [
1308                node.id.as_str(),
1309                node.dom_id.as_str(),
1310                node.label.as_str(),
1311                node.label_type.as_str(),
1312                node.shape.as_str(),
1313                node.css_classes.as_str(),
1314                node.look.as_str(),
1315                node.node_id.as_str(),
1316            ];
1317            let total = strings.into_iter().fold(total, |subtotal, value| {
1318                subtotal.saturating_add(value.len())
1319            });
1320            let total = node.css_styles.iter().fold(total, |subtotal, value| {
1321                subtotal.saturating_add(value.len())
1322            });
1323            total.saturating_add(node.icon.as_deref().map_or(0, str::len))
1324        });
1325        let edge_label_bytes = model.edges.iter().fold(0usize, |total, edge| {
1326            [
1327                edge.id.as_str(),
1328                edge.start.as_str(),
1329                edge.end.as_str(),
1330                edge.edge_type.as_str(),
1331                edge.curve.as_str(),
1332                edge.thickness.as_str(),
1333                edge.look.as_str(),
1334                edge.classes.as_str(),
1335            ]
1336            .into_iter()
1337            .fold(total, |subtotal, value| {
1338                subtotal.saturating_add(value.len())
1339            })
1340        });
1341
1342        Self {
1343            nodes: model.nodes.len(),
1344            edges: model.edges.len(),
1345            label_bytes: node_label_bytes.saturating_add(edge_label_bytes),
1346            nesting_depth: model
1347                .nodes
1348                .iter()
1349                .filter_map(|node| usize::try_from(node.level).ok())
1350                .max()
1351                .unwrap_or(0),
1352        }
1353    }
1354
1355    pub fn as_model_complexity(self) -> ModelComplexity {
1356        ModelComplexity::new(
1357            self.nodes.saturating_add(self.edges),
1358            self.label_bytes,
1359            self.nesting_depth,
1360        )
1361    }
1362}
1363
1364#[cfg(feature = "diagram-class")]
1365impl ClassComplexity {
1366    pub fn from_model(model: &ClassDiagram) -> Self {
1367        let class_label_bytes = model
1368            .classes
1369            .values()
1370            .map(|node| {
1371                node.id
1372                    .len()
1373                    .saturating_add(node.label.len())
1374                    .saturating_add(node.text.len())
1375                    .saturating_add(node.type_param.len())
1376                    .saturating_add(node.css_classes.len())
1377                    .saturating_add(node.tooltip.as_deref().map(str::len).unwrap_or(0))
1378                    .saturating_add(node.link.as_deref().map(str::len).unwrap_or(0))
1379                    .saturating_add(
1380                        node.members
1381                            .iter()
1382                            .chain(node.methods.iter())
1383                            .map(|member| {
1384                                member
1385                                    .display_text
1386                                    .len()
1387                                    .saturating_add(member.id.len())
1388                                    .saturating_add(member.parameters.len())
1389                                    .saturating_add(member.return_type.len())
1390                            })
1391                            .sum::<usize>(),
1392                    )
1393                    .saturating_add(node.annotations.iter().map(String::len).sum::<usize>())
1394                    .saturating_add(node.styles.iter().map(String::len).sum::<usize>())
1395            })
1396            .sum::<usize>();
1397        let relation_label_bytes = model
1398            .relations
1399            .iter()
1400            .map(|rel| {
1401                rel.id
1402                    .len()
1403                    .saturating_add(rel.id1.len())
1404                    .saturating_add(rel.id2.len())
1405                    .saturating_add(rel.title.len())
1406                    .saturating_add(rel.relation_title_1.as_deref().map_or(0, str::len))
1407                    .saturating_add(rel.relation_title_2.as_deref().map_or(0, str::len))
1408            })
1409            .sum::<usize>();
1410        let note_label_bytes = model
1411            .notes
1412            .iter()
1413            .map(|note| {
1414                note.id
1415                    .len()
1416                    .saturating_add(note.text.len())
1417                    .saturating_add(note.class_id.as_deref().map(str::len).unwrap_or(0))
1418            })
1419            .sum::<usize>();
1420        let interface_label_bytes = model
1421            .interfaces
1422            .iter()
1423            .map(|iface| {
1424                iface
1425                    .id
1426                    .len()
1427                    .saturating_add(iface.label.len())
1428                    .saturating_add(iface.class_id.len())
1429            })
1430            .sum::<usize>();
1431        let namespace_label_bytes = model
1432            .namespaces
1433            .values()
1434            .map(|namespace| {
1435                namespace
1436                    .id
1437                    .len()
1438                    .saturating_add(namespace.label.len())
1439                    .saturating_add(namespace.parent.as_deref().map(str::len).unwrap_or(0))
1440            })
1441            .sum::<usize>();
1442
1443        Self {
1444            nodes: model
1445                .classes
1446                .len()
1447                .saturating_add(model.notes.len())
1448                .saturating_add(model.interfaces.len())
1449                .saturating_add(model.namespaces.len()),
1450            edges: model.relations.len().saturating_add(
1451                model
1452                    .notes
1453                    .iter()
1454                    .filter(|note| note.class_id.is_some())
1455                    .count(),
1456            ),
1457            namespaces: model.namespaces.len(),
1458            label_bytes: class_label_bytes
1459                .saturating_add(relation_label_bytes)
1460                .saturating_add(note_label_bytes)
1461                .saturating_add(interface_label_bytes)
1462                .saturating_add(namespace_label_bytes),
1463            namespace_depth: class_namespace_depth(model),
1464        }
1465    }
1466
1467    pub fn as_model_complexity(self) -> ModelComplexity {
1468        ModelComplexity::new(
1469            self.nodes.saturating_add(self.edges),
1470            self.label_bytes,
1471            self.namespace_depth,
1472        )
1473    }
1474}
1475
1476#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1477#[cfg(feature = "diagram-zenuml")]
1478pub struct ZenumlComplexity {
1479    pub participants: usize,
1480    pub groups: usize,
1481    pub statements: usize,
1482    pub fragments: usize,
1483    pub label_bytes: usize,
1484    pub nesting_depth: usize,
1485}
1486
1487#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1488#[cfg(feature = "diagram-sequence")]
1489pub struct SequenceComplexity {
1490    pub messages: usize,
1491    pub block_depth: usize,
1492    pub items: usize,
1493    pub text_bytes: usize,
1494    pub nesting_depth: usize,
1495}
1496
1497#[cfg(feature = "diagram-sequence")]
1498impl SequenceComplexity {
1499    pub fn from_model(model: &SequenceDiagramRenderModel) -> Self {
1500        const ACTOR_PROPERTY_VALUE_DEPTH: usize = 4;
1501        const NOTE_ACTOR_VALUE_DEPTH: usize = 3;
1502
1503        let mut depth = 0usize;
1504        let mut block_depth = 0usize;
1505        for message in &model.messages {
1506            match message.message_type {
1507                10 | 12 | 15 | 19 | 22 | 27 | 30 | 32 => {
1508                    depth = depth.saturating_add(1);
1509                    block_depth = block_depth.max(depth);
1510                }
1511                11 | 14 | 16 | 21 | 23 | 29 | 31 => {
1512                    depth = depth.saturating_sub(1);
1513                }
1514                _ => {}
1515            }
1516        }
1517        let common_text_bytes = [
1518            model.acc_title.as_deref(),
1519            model.acc_descr.as_deref(),
1520            model.title.as_deref(),
1521        ]
1522        .into_iter()
1523        .flatten()
1524        .fold(0usize, |total, value| total.saturating_add(value.len()));
1525        let actor_order_text_bytes = model
1526            .actor_order
1527            .iter()
1528            .fold(0usize, |total, value| total.saturating_add(value.len()));
1529        let mut arbitrary_json = JsonValueComplexity::default();
1530        let actor_text_bytes = model.actors.iter().fold(0usize, |total, (key, actor)| {
1531            let links = json_map_complexity(&actor.links, ACTOR_PROPERTY_VALUE_DEPTH);
1532            let properties = json_map_complexity(&actor.properties, ACTOR_PROPERTY_VALUE_DEPTH);
1533            arbitrary_json.merge(links);
1534            arbitrary_json.merge(properties);
1535            total
1536                .saturating_add(key.len())
1537                .saturating_add(actor.name.len())
1538                .saturating_add(actor.description.len())
1539                .saturating_add(actor.actor_type.len())
1540                .saturating_add(links.text_bytes)
1541                .saturating_add(properties.text_bytes)
1542        });
1543        let box_text_bytes = model.boxes.iter().fold(0usize, |total, sequence_box| {
1544            sequence_box.actor_keys.iter().fold(
1545                total
1546                    .saturating_add(sequence_box.fill.len())
1547                    .saturating_add(sequence_box.name.as_deref().map_or(0, str::len)),
1548                |subtotal, actor| subtotal.saturating_add(actor.len()),
1549            )
1550        });
1551        let message_text_bytes = model.messages.iter().fold(0usize, |total, message| {
1552            total
1553                .saturating_add(message.id.len())
1554                .saturating_add(message.from.as_deref().map_or(0, str::len))
1555                .saturating_add(message.to.as_deref().map_or(0, str::len))
1556                .saturating_add(message.message_text().len())
1557        });
1558        let note_text_bytes = model.notes.iter().fold(0usize, |total, note| {
1559            let actor = json_value_complexity(&note.actor, NOTE_ACTOR_VALUE_DEPTH);
1560            arbitrary_json.merge(actor);
1561            total
1562                .saturating_add(actor.text_bytes)
1563                .saturating_add(note.message.len())
1564        });
1565        let lifecycle_text_bytes = model
1566            .created_actors
1567            .keys()
1568            .chain(model.destroyed_actors.keys())
1569            .fold(0usize, |total, value| total.saturating_add(value.len()));
1570        Self {
1571            messages: model.messages.len(),
1572            block_depth,
1573            items: model
1574                .actors
1575                .len()
1576                .saturating_add(model.boxes.len())
1577                .saturating_add(model.messages.len())
1578                .saturating_add(model.notes.len())
1579                .saturating_add(arbitrary_json.items),
1580            text_bytes: common_text_bytes
1581                .saturating_add(actor_order_text_bytes)
1582                .saturating_add(actor_text_bytes)
1583                .saturating_add(box_text_bytes)
1584                .saturating_add(message_text_bytes)
1585                .saturating_add(note_text_bytes)
1586                .saturating_add(lifecycle_text_bytes),
1587            nesting_depth: block_depth.max(arbitrary_json.nesting_depth),
1588        }
1589    }
1590
1591    pub fn as_model_complexity(self) -> ModelComplexity {
1592        ModelComplexity::new(self.items, self.text_bytes, self.nesting_depth)
1593    }
1594}
1595
1596#[cfg(feature = "diagram-zenuml")]
1597impl ZenumlComplexity {
1598    pub fn from_model(model: &ZenumlDiagramRenderModel) -> Self {
1599        let common_label_bytes = [model.title.as_deref(), model.starter.as_deref()]
1600            .into_iter()
1601            .flatten()
1602            .fold(0usize, |total, value| total.saturating_add(value.len()));
1603        let participant_label_bytes =
1604            model
1605                .participants
1606                .iter()
1607                .fold(0usize, |total, participant| {
1608                    [
1609                        Some(participant.name.as_str()),
1610                        participant.label.as_deref(),
1611                        participant.participant_type.as_deref(),
1612                        participant.stereotype.as_deref(),
1613                        participant.emoji.as_deref(),
1614                        participant.width_source.as_deref(),
1615                        participant.color.as_deref(),
1616                        participant.group_id.as_deref(),
1617                    ]
1618                    .into_iter()
1619                    .flatten()
1620                    .fold(total, |subtotal, value| {
1621                        subtotal.saturating_add(value.len())
1622                    })
1623                });
1624        let group_label_bytes = model.groups.iter().fold(0usize, |total, group| {
1625            group.participant_names.iter().fold(
1626                total.saturating_add(group.id.as_deref().map_or(0, str::len)),
1627                |subtotal, name| subtotal.saturating_add(name.len()),
1628            )
1629        });
1630        let mut complexity = Self {
1631            participants: model.participants.len(),
1632            groups: model.groups.len(),
1633            statements: 0,
1634            fragments: 0,
1635            label_bytes: common_label_bytes
1636                .saturating_add(participant_label_bytes)
1637                .saturating_add(group_label_bytes),
1638            nesting_depth: 0,
1639        };
1640        let mut pending = vec![(model.statements.as_slice(), 0usize)];
1641        while let Some((statements, nesting_depth)) = pending.pop() {
1642            complexity.nesting_depth = complexity.nesting_depth.max(nesting_depth);
1643            for statement in statements {
1644                complexity.statements = complexity.statements.saturating_add(1);
1645                complexity.label_bytes = complexity
1646                    .label_bytes
1647                    .saturating_add(statement.comment.as_deref().map_or(0, str::len));
1648                match &statement.kind {
1649                    ZenumlStatementKind::Message {
1650                        explicit_from,
1651                        resolved_from,
1652                        resolved_to,
1653                        label,
1654                        assignment,
1655                        body,
1656                        ..
1657                    } => {
1658                        complexity.label_bytes = complexity
1659                            .label_bytes
1660                            .saturating_add(explicit_from.as_deref().map_or(0, str::len))
1661                            .saturating_add(resolved_from.as_deref().map_or(0, str::len))
1662                            .saturating_add(resolved_to.as_deref().map_or(0, str::len))
1663                            .saturating_add(label.len())
1664                            .saturating_add(assignment.as_deref().map_or(0, str::len));
1665                        pending.push((body, nesting_depth.saturating_add(1)));
1666                    }
1667                    ZenumlStatementKind::Creation {
1668                        resolved_from,
1669                        resolved_to,
1670                        constructor,
1671                        parameters,
1672                        assignment,
1673                        label,
1674                        body,
1675                        ..
1676                    } => {
1677                        complexity.label_bytes = complexity
1678                            .label_bytes
1679                            .saturating_add(resolved_from.as_deref().map_or(0, str::len))
1680                            .saturating_add(resolved_to.len())
1681                            .saturating_add(constructor.len())
1682                            .saturating_add(parameters.len())
1683                            .saturating_add(assignment.as_deref().map_or(0, str::len))
1684                            .saturating_add(label.len());
1685                        pending.push((body, nesting_depth.saturating_add(1)));
1686                    }
1687                    ZenumlStatementKind::Return {
1688                        explicit_from,
1689                        resolved_from,
1690                        explicit_to,
1691                        resolved_to,
1692                        label,
1693                    } => {
1694                        complexity.label_bytes = complexity
1695                            .label_bytes
1696                            .saturating_add(explicit_from.as_deref().map_or(0, str::len))
1697                            .saturating_add(resolved_from.as_deref().map_or(0, str::len))
1698                            .saturating_add(explicit_to.as_deref().map_or(0, str::len))
1699                            .saturating_add(resolved_to.as_deref().map_or(0, str::len))
1700                            .saturating_add(label.len());
1701                    }
1702                    ZenumlStatementKind::Fragment {
1703                        label, sections, ..
1704                    } => {
1705                        complexity.fragments = complexity.fragments.saturating_add(1);
1706                        complexity.label_bytes = complexity
1707                            .label_bytes
1708                            .saturating_add(label.as_deref().map_or(0, str::len));
1709                        for section in sections {
1710                            complexity.label_bytes = complexity
1711                                .label_bytes
1712                                .saturating_add(section.label.as_deref().map_or(0, str::len));
1713                            pending.push((&section.statements, nesting_depth.saturating_add(1)));
1714                        }
1715                    }
1716                    ZenumlStatementKind::Reference {
1717                        participants,
1718                        label,
1719                    } => {
1720                        complexity.label_bytes = participants.iter().fold(
1721                            complexity.label_bytes.saturating_add(label.len()),
1722                            |total, participant| total.saturating_add(participant.len()),
1723                        );
1724                    }
1725                    ZenumlStatementKind::Divider { label } => {
1726                        complexity.label_bytes = complexity.label_bytes.saturating_add(label.len());
1727                    }
1728                }
1729            }
1730        }
1731        complexity
1732    }
1733
1734    pub fn as_model_complexity(self) -> ModelComplexity {
1735        ModelComplexity::new(
1736            self.participants
1737                .saturating_add(self.groups)
1738                .saturating_add(self.statements),
1739            self.label_bytes,
1740            self.nesting_depth,
1741        )
1742    }
1743}
1744
1745#[cfg(any(
1746    feature = "diagram-flowchart",
1747    feature = "diagram-swimlane",
1748    feature = "diagram-agentflow"
1749))]
1750fn flowchart_text_bytes(model: &FlowchartModel) -> usize {
1751    let mut total = [
1752        Some(model.keyword.as_str()),
1753        model.acc_title.as_deref(),
1754        model.acc_descr.as_deref(),
1755        model.direction.as_deref(),
1756    ]
1757    .into_iter()
1758    .flatten()
1759    .fold(0usize, |subtotal, value| {
1760        subtotal.saturating_add(value.len())
1761    });
1762    for (id, styles) in &model.class_defs {
1763        total = styles
1764            .iter()
1765            .fold(total.saturating_add(id.len()), |subtotal, value| {
1766                subtotal.saturating_add(value.len())
1767            });
1768    }
1769    total = model.vertex_calls.iter().fold(total, |subtotal, value| {
1770        subtotal.saturating_add(value.len())
1771    });
1772    if let Some(defaults) = &model.edge_defaults {
1773        total = total.saturating_add(defaults.interpolate.as_deref().map_or(0, str::len));
1774        total = defaults.style.iter().fold(total, |subtotal, value| {
1775            subtotal.saturating_add(value.len())
1776        });
1777    }
1778    for node in &model.nodes {
1779        for value in [
1780            Some(node.id.as_str()),
1781            node.label.as_deref(),
1782            node.label_type.as_deref(),
1783            node.layout_shape.as_deref(),
1784            node.shape.as_deref(),
1785            node.icon.as_deref(),
1786            node.form.as_deref(),
1787            node.pos.as_deref(),
1788            node.img.as_deref(),
1789            node.constraint.as_deref(),
1790            node.link.as_deref(),
1791            node.link_target.as_deref(),
1792        ]
1793        .into_iter()
1794        .flatten()
1795        {
1796            total = total.saturating_add(value.len());
1797        }
1798        total = node
1799            .classes
1800            .iter()
1801            .chain(&node.styles)
1802            .fold(total, |subtotal, value| {
1803                subtotal.saturating_add(value.len())
1804            });
1805    }
1806    for edge in &model.edges {
1807        for value in [
1808            Some(edge.id.as_str()),
1809            Some(edge.from.as_str()),
1810            Some(edge.to.as_str()),
1811            edge.label.as_deref(),
1812            edge.label_type.as_deref(),
1813            edge.edge_type.as_deref(),
1814            Some(edge.arrow.as_str()),
1815            edge.stroke.as_deref(),
1816            edge.interpolate.as_deref(),
1817            edge.animation.as_deref(),
1818        ]
1819        .into_iter()
1820        .flatten()
1821        {
1822            total = total.saturating_add(value.len());
1823        }
1824        total = edge
1825            .classes
1826            .iter()
1827            .chain(&edge.style)
1828            .fold(total, |subtotal, value| {
1829                subtotal.saturating_add(value.len())
1830            });
1831    }
1832    for subgraph in &model.subgraphs {
1833        for value in [
1834            Some(subgraph.id.as_str()),
1835            Some(subgraph.title.as_str()),
1836            subgraph.dir.as_deref(),
1837            subgraph.label_type.as_deref(),
1838        ]
1839        .into_iter()
1840        .flatten()
1841        {
1842            total = total.saturating_add(value.len());
1843        }
1844        total = subgraph
1845            .classes
1846            .iter()
1847            .chain(&subgraph.styles)
1848            .chain(&subgraph.nodes)
1849            .fold(total, |subtotal, value| {
1850                subtotal.saturating_add(value.len())
1851            });
1852    }
1853    total = model.tooltips.iter().fold(total, |subtotal, (id, value)| {
1854        subtotal
1855            .saturating_add(id.len())
1856            .saturating_add(value.len())
1857    });
1858    total
1859}
1860
1861#[cfg(any(
1862    feature = "diagram-flowchart",
1863    feature = "diagram-swimlane",
1864    feature = "diagram-agentflow"
1865))]
1866fn flowchart_subgraph_depth(model: &FlowchartModel) -> usize {
1867    if model.subgraphs.is_empty() {
1868        return 0;
1869    }
1870    let indices = model
1871        .subgraphs
1872        .iter()
1873        .enumerate()
1874        .map(|(index, subgraph)| (subgraph.id.as_str(), index))
1875        .collect::<std::collections::HashMap<_, _>>();
1876    let mut children = vec![Vec::new(); model.subgraphs.len()];
1877    let mut incoming = vec![0usize; model.subgraphs.len()];
1878    for (parent, subgraph) in model.subgraphs.iter().enumerate() {
1879        for member in &subgraph.nodes {
1880            let Some(&child) = indices.get(member.as_str()) else {
1881                continue;
1882            };
1883            children[parent].push(child);
1884            incoming[child] = incoming[child].saturating_add(1);
1885        }
1886    }
1887    let mut queue = std::collections::VecDeque::new();
1888    let mut depths = vec![1usize; model.subgraphs.len()];
1889    for (index, incoming) in incoming.iter().enumerate() {
1890        if *incoming == 0 {
1891            queue.push_back(index);
1892        }
1893    }
1894    while let Some(parent) = queue.pop_front() {
1895        for &child in &children[parent] {
1896            depths[child] = depths[child].max(depths[parent].saturating_add(1));
1897            incoming[child] = incoming[child].saturating_sub(1);
1898            if incoming[child] == 0 {
1899                queue.push_back(child);
1900            }
1901        }
1902    }
1903    depths.into_iter().max().unwrap_or(0)
1904}
1905
1906#[cfg(feature = "diagram-class")]
1907fn class_namespace_depth(model: &ClassDiagram) -> usize {
1908    if model.namespaces.is_empty() {
1909        return 0;
1910    }
1911    let indices = model
1912        .namespaces
1913        .keys()
1914        .enumerate()
1915        .map(|(index, id)| (id.as_str(), index))
1916        .collect::<std::collections::HashMap<_, _>>();
1917    let parents = model
1918        .namespaces
1919        .values()
1920        .map(|namespace| {
1921            namespace
1922                .parent
1923                .as_deref()
1924                .and_then(|parent| indices.get(parent).copied())
1925        })
1926        .collect::<Vec<_>>();
1927    let mut depths = vec![0usize; parents.len()];
1928    let mut completed = vec![false; parents.len()];
1929    for start in 0..parents.len() {
1930        if completed[start] {
1931            continue;
1932        }
1933        let mut path = Vec::new();
1934        let mut positions = std::collections::HashMap::new();
1935        let mut cursor = Some(start);
1936        while let Some(index) = cursor {
1937            if completed[index] || positions.contains_key(&index) {
1938                break;
1939            }
1940            positions.insert(index, path.len());
1941            path.push(index);
1942            cursor = parents[index];
1943        }
1944        let mut depth = cursor
1945            .filter(|index| completed[*index])
1946            .map(|index| depths[index])
1947            .unwrap_or(0);
1948        for index in path.into_iter().rev() {
1949            depth = depth.saturating_add(1);
1950            depths[index] = depth;
1951            completed[index] = true;
1952        }
1953    }
1954    depths.into_iter().max().unwrap_or(0)
1955}
1956
1957#[cfg(feature = "diagram-kanban")]
1958fn kanban_nesting_depth(model: &KanbanDiagramRenderModel) -> usize {
1959    let indices = model
1960        .nodes
1961        .iter()
1962        .enumerate()
1963        .map(|(index, node)| (node.id.as_str(), index))
1964        .collect::<std::collections::HashMap<_, _>>();
1965    let parents = model
1966        .nodes
1967        .iter()
1968        .map(|node| {
1969            node.parent_id
1970                .as_deref()
1971                .and_then(|parent| indices.get(parent).copied())
1972        })
1973        .collect::<Vec<_>>();
1974    let mut depths = vec![0usize; parents.len()];
1975    let mut completed = vec![false; parents.len()];
1976    for start in 0..parents.len() {
1977        if completed[start] {
1978            continue;
1979        }
1980        let mut path = Vec::new();
1981        let mut positions = std::collections::HashMap::new();
1982        let mut cursor = Some(start);
1983        while let Some(index) = cursor {
1984            if completed[index] || positions.contains_key(&index) {
1985                break;
1986            }
1987            positions.insert(index, path.len());
1988            path.push(index);
1989            cursor = parents[index];
1990        }
1991        let mut depth = cursor
1992            .filter(|index| completed[*index])
1993            .map(|index| depths[index])
1994            .unwrap_or(0);
1995        for index in path.into_iter().rev() {
1996            depth = depth.saturating_add(1);
1997            depths[index] = depth;
1998            completed[index] = true;
1999        }
2000    }
2001    depths.into_iter().max().unwrap_or(0)
2002}
2003
2004#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2005#[cfg(any(feature = "diagram-sequence", feature = "diagram-treemap"))]
2006struct JsonValueComplexity {
2007    items: usize,
2008    text_bytes: usize,
2009    nesting_depth: usize,
2010}
2011
2012#[cfg(feature = "diagram-sequence")]
2013impl JsonValueComplexity {
2014    fn merge(&mut self, other: Self) {
2015        self.items = self.items.saturating_add(other.items);
2016        self.text_bytes = self.text_bytes.saturating_add(other.text_bytes);
2017        self.nesting_depth = self.nesting_depth.max(other.nesting_depth);
2018    }
2019}
2020
2021#[cfg(any(feature = "diagram-sequence", feature = "diagram-treemap"))]
2022enum JsonTraversalFrame<'a> {
2023    Value(&'a serde_json::Value, usize),
2024    Array(std::slice::Iter<'a, serde_json::Value>, usize),
2025    Object(serde_json::map::Iter<'a>, usize),
2026}
2027
2028#[cfg(feature = "diagram-sequence")]
2029fn json_map_complexity(
2030    map: &serde_json::Map<String, serde_json::Value>,
2031    value_depth: usize,
2032) -> JsonValueComplexity {
2033    let mut complexity = JsonValueComplexity {
2034        items: map.len(),
2035        text_bytes: map
2036            .keys()
2037            .fold(0usize, |total, key| total.saturating_add(key.len())),
2038        nesting_depth: 0,
2039    };
2040    for value in map.values() {
2041        complexity.merge(json_value_complexity(value, value_depth));
2042    }
2043    complexity
2044}
2045
2046/// Counts an arbitrary JSON subtree without recursive calls or width-proportional scratch space.
2047#[cfg(any(feature = "diagram-sequence", feature = "diagram-treemap"))]
2048fn json_value_complexity(value: &serde_json::Value, initial_depth: usize) -> JsonValueComplexity {
2049    let mut complexity = JsonValueComplexity::default();
2050    let mut pending = vec![JsonTraversalFrame::Value(value, initial_depth)];
2051    while let Some(frame) = pending.pop() {
2052        match frame {
2053            JsonTraversalFrame::Value(value, depth) => {
2054                complexity.nesting_depth = complexity.nesting_depth.max(depth);
2055                match value {
2056                    serde_json::Value::String(value) => {
2057                        complexity.text_bytes = complexity.text_bytes.saturating_add(value.len());
2058                    }
2059                    serde_json::Value::Array(values) => {
2060                        complexity.items = complexity.items.saturating_add(values.len());
2061                        pending.push(JsonTraversalFrame::Array(
2062                            values.iter(),
2063                            depth.saturating_add(1),
2064                        ));
2065                    }
2066                    serde_json::Value::Object(values) => {
2067                        complexity.items = complexity.items.saturating_add(values.len());
2068                        pending.push(JsonTraversalFrame::Object(
2069                            values.iter(),
2070                            depth.saturating_add(1),
2071                        ));
2072                    }
2073                    serde_json::Value::Null
2074                    | serde_json::Value::Bool(_)
2075                    | serde_json::Value::Number(_) => {}
2076                }
2077            }
2078            JsonTraversalFrame::Array(mut values, depth) => {
2079                if let Some(value) = values.next() {
2080                    pending.push(JsonTraversalFrame::Array(values, depth));
2081                    pending.push(JsonTraversalFrame::Value(value, depth));
2082                }
2083            }
2084            JsonTraversalFrame::Object(mut values, depth) => {
2085                if let Some((key, value)) = values.next() {
2086                    complexity.text_bytes = complexity.text_bytes.saturating_add(key.len());
2087                    pending.push(JsonTraversalFrame::Object(values, depth));
2088                    pending.push(JsonTraversalFrame::Value(value, depth));
2089                }
2090            }
2091        }
2092    }
2093    complexity
2094}
2095
2096#[cfg(feature = "diagram-treemap")]
2097fn json_text_bytes(value: &serde_json::Value) -> usize {
2098    json_value_complexity(value, 0).text_bytes
2099}
2100
2101#[cfg(feature = "diagram-treemap")]
2102fn json_item_count(value: &serde_json::Value) -> usize {
2103    json_value_complexity(value, 0).items
2104}
2105
2106#[cfg(test)]
2107mod tests {
2108    use super::*;
2109
2110    #[test]
2111    fn profile_values_are_single_source_for_input_limits() {
2112        assert_eq!(
2113            RESOURCE_PROFILE_DESCRIPTORS.len(),
2114            ResourceProfile::ALL.len()
2115        );
2116        assert_eq!(
2117            INPUT_RESOURCE_LIMIT_DESCRIPTORS.len(),
2118            INPUT_RESOURCE_LIMIT_COUNT
2119        );
2120        for profile in ResourceProfile::ALL {
2121            let policy = InputResourcePolicy::for_profile(profile);
2122            for limit in InputResourceLimitId::ALL {
2123                assert_eq!(policy.base_value(limit), policy.value(limit));
2124            }
2125        }
2126
2127        let interactive = InputResourcePolicy::for_profile(ResourceProfile::Interactive);
2128        for (limit, expected) in [
2129            (InputResourceLimitId::MaxModelItems, Some(32_000)),
2130            (InputResourceLimitId::MaxModelTextBytes, Some(2 * MIB)),
2131            (InputResourceLimitId::MaxModelNestingDepth, Some(256)),
2132        ] {
2133            assert_eq!(interactive.value(limit), expected, "{limit:?}");
2134        }
2135    }
2136
2137    #[test]
2138    fn constrained_policy_rejects_source_bytes() {
2139        let source_error = InputResourcePolicy::for_profile(ResourceProfile::Constrained)
2140            .apply_for_test(InputResourceLimitId::MaxSourceBytes, 4)
2141            .check_source_bytes("12345")
2142            .unwrap_err();
2143        assert_eq!(source_error.limit, "max_source_bytes");
2144    }
2145
2146    #[cfg(feature = "diagram-flowchart")]
2147    #[test]
2148    fn constrained_policy_rejects_flowchart_cardinality() {
2149        let parsed = crate::Engine::new()
2150            .parse_diagram_for_render_model_sync(
2151                "flowchart TD\nA --> B",
2152                crate::ParseOptions::strict(),
2153            )
2154            .unwrap()
2155            .unwrap();
2156        let model_error = InputResourcePolicy::for_profile(ResourceProfile::Constrained)
2157            .apply_for_test(InputResourceLimitId::MaxModelItems, 2)
2158            .check_render_model(parsed.model())
2159            .unwrap_err();
2160        assert_eq!(model_error.limit, "max_model_items");
2161    }
2162
2163    #[cfg(feature = "diagram-flowchart")]
2164    #[test]
2165    fn parsed_render_resource_checks_include_parser_owned_flowchart_label_sources() {
2166        let parsed = crate::Engine::new()
2167            .parse_diagram_for_render_model_sync(
2168                "flowchart LR\nA[\"&nbsp;entity&nbsp;\"] --> B\n",
2169                crate::ParseOptions::strict(),
2170            )
2171            .unwrap()
2172            .unwrap();
2173        let model_complexity = ModelComplexity::from_render_model(parsed.model());
2174        assert!(parsed.retained_render_context_bytes() > 0);
2175
2176        let policy = InputResourcePolicy::for_profile(ResourceProfile::UnboundedForTrustedInput)
2177            .with_limit(
2178                InputResourceLimitId::MaxModelTextBytes,
2179                model_complexity.text_bytes,
2180            )
2181            .unwrap();
2182        policy
2183            .check_render_model(parsed.model())
2184            .expect("the public model alone fits its exact text budget");
2185        let error = policy.check_parsed_render(&parsed).unwrap_err();
2186        assert_eq!(error.limit, "max_model_text_bytes");
2187        assert!(error.actual > model_complexity.text_bytes);
2188    }
2189
2190    #[cfg(feature = "diagram-sequence")]
2191    #[test]
2192    fn sequence_complexity_bounds_messages_and_nested_frames() {
2193        let parsed = crate::Engine::new()
2194            .parse_diagram_for_render_model_sync(
2195                "sequenceDiagram\nloop outer\nrect rgb(240,240,240)\nA->>B: hi\nend\nend",
2196                crate::ParseOptions::strict(),
2197            )
2198            .unwrap()
2199            .unwrap();
2200        let crate::RenderSemanticModel::Sequence(model) = parsed.model() else {
2201            panic!("expected Sequence model");
2202        };
2203        let complexity = SequenceComplexity::from_model(model);
2204        assert_eq!(complexity.block_depth, 2);
2205        assert_eq!(complexity.messages, model.messages.len());
2206
2207        let error = InputResourcePolicy::for_profile(ResourceProfile::UnboundedForTrustedInput)
2208            .with_limit(InputResourceLimitId::MaxModelNestingDepth, 1)
2209            .unwrap()
2210            .check_render_model(parsed.model())
2211            .unwrap_err();
2212        assert_eq!(error.limit, "max_model_nesting_depth");
2213        assert_eq!(error.actual, 2);
2214        assert_eq!(error.max, 1);
2215    }
2216
2217    #[cfg(feature = "diagram-sequence")]
2218    #[test]
2219    fn sequence_complexity_counts_arbitrary_json_items_and_depth_iteratively() {
2220        let parsed = crate::Engine::new()
2221            .parse_diagram_for_render_model_sync(
2222                "sequenceDiagram\nparticipant A",
2223                crate::ParseOptions::strict(),
2224            )
2225            .unwrap()
2226            .unwrap();
2227        let (_, mut render_model) = parsed.into_parts();
2228        let RenderSemanticModel::Sequence(model) = &mut render_model else {
2229            panic!("expected Sequence model");
2230        };
2231        let mut nested = serde_json::Value::Null;
2232        for _ in 0..300 {
2233            nested = serde_json::Value::Array(vec![nested]);
2234        }
2235        model
2236            .actors
2237            .get_mut("A")
2238            .unwrap()
2239            .properties
2240            .insert("payload".to_string(), nested);
2241
2242        let complexity = SequenceComplexity::from_model(model);
2243        assert_eq!(complexity.nesting_depth, 304);
2244        assert_eq!(complexity.items, 302);
2245
2246        let exact = InputResourcePolicy::for_profile(ResourceProfile::UnboundedForTrustedInput)
2247            .with_limit(
2248                InputResourceLimitId::MaxModelNestingDepth,
2249                complexity.nesting_depth,
2250            )
2251            .unwrap();
2252        exact
2253            .check_render_model(&render_model)
2254            .expect("the exact JSON depth boundary should pass");
2255
2256        let below = InputResourcePolicy::for_profile(ResourceProfile::UnboundedForTrustedInput)
2257            .with_limit(
2258                InputResourceLimitId::MaxModelNestingDepth,
2259                complexity.nesting_depth - 1,
2260            )
2261            .unwrap();
2262        let error = below
2263            .check_render_model(&render_model)
2264            .expect_err("the N-1 JSON depth boundary should fail");
2265        assert_eq!(error.limit, "max_model_nesting_depth");
2266        assert_eq!(error.actual, complexity.nesting_depth);
2267        assert_eq!(error.max, complexity.nesting_depth - 1);
2268    }
2269
2270    #[cfg(feature = "diagram-mindmap")]
2271    #[test]
2272    fn mindmap_model_limits_cover_pre_layout_cardinality_and_labels() {
2273        let constrained = InputResourcePolicy::for_profile(ResourceProfile::Constrained);
2274        assert_eq!(
2275            constrained.value(InputResourceLimitId::MaxModelItems),
2276            Some(16_000)
2277        );
2278
2279        let parsed = crate::Engine::new()
2280            .parse_diagram_for_render_model_sync(
2281                "mindmap\n  Root\n    First child\n    Second child\n",
2282                crate::ParseOptions::strict(),
2283            )
2284            .unwrap()
2285            .unwrap();
2286        let RenderSemanticModel::Mindmap(model) = parsed.model() else {
2287            panic!("expected Mindmap model");
2288        };
2289        let complexity = MindmapComplexity::from_model(model);
2290        assert_eq!(complexity.nodes, 3);
2291        assert_eq!(complexity.edges, 2);
2292        assert!(complexity.label_bytes >= "RootFirst childSecond child".len());
2293
2294        for (id, max, expected_limit) in [
2295            (InputResourceLimitId::MaxModelItems, 4, "max_model_items"),
2296            (
2297                InputResourceLimitId::MaxModelTextBytes,
2298                1,
2299                "max_model_text_bytes",
2300            ),
2301        ] {
2302            let error = InputResourcePolicy::for_profile(ResourceProfile::UnboundedForTrustedInput)
2303                .with_limit(id, max)
2304                .unwrap()
2305                .check_render_model(parsed.model())
2306                .unwrap_err();
2307            assert_eq!(error.phase, InputResourceLimitPhase::Model);
2308            assert_eq!(error.limit, expected_limit);
2309        }
2310    }
2311
2312    #[cfg(any(
2313        feature = "diagram-kanban",
2314        feature = "diagram-radar",
2315        feature = "diagram-requirement"
2316    ))]
2317    #[test]
2318    fn generic_model_budget_covers_new_family_models_without_family_knobs() {
2319        let models = [
2320            #[cfg(feature = "diagram-kanban")]
2321            RenderSemanticModel::Kanban(KanbanDiagramRenderModel {
2322                nodes: vec![crate::diagrams::kanban::KanbanRenderNode::new(
2323                    "todo", "Todo",
2324                )],
2325            }),
2326            #[cfg(feature = "diagram-radar")]
2327            RenderSemanticModel::Radar({
2328                let mut model = RadarDiagramRenderModel::default();
2329                model.axes = vec![crate::diagrams::radar::RadarRenderAxis {
2330                    name: "speed".to_string(),
2331                    label: "Speed".to_string(),
2332                }];
2333                model
2334            }),
2335            #[cfg(feature = "diagram-requirement")]
2336            RenderSemanticModel::Requirement(RequirementDiagramRenderModel {
2337                direction: "TB".to_string(),
2338                requirements: vec![crate::diagrams::requirement::RequirementRenderNode {
2339                    name: "R1".to_string(),
2340                    node_type: "requirement".to_string(),
2341                    requirement_id: "1".to_string(),
2342                    text: "Must render".to_string(),
2343                    risk: "Low".to_string(),
2344                    verify_method: "Test".to_string(),
2345                    css_styles: Vec::new(),
2346                    classes: Vec::new(),
2347                }],
2348                acc_title: None,
2349                acc_descr: None,
2350                elements: Vec::new(),
2351                relationships: Vec::new(),
2352                classes: Default::default(),
2353            }),
2354        ];
2355        for model in models {
2356            let error = InputResourcePolicy::for_profile(ResourceProfile::UnboundedForTrustedInput)
2357                .with_limit(InputResourceLimitId::MaxModelTextBytes, 1)
2358                .unwrap()
2359                .check_render_model(&model)
2360                .unwrap_err();
2361            assert_eq!(error.limit, "max_model_text_bytes");
2362        }
2363    }
2364
2365    #[cfg(feature = "diagram-kanban")]
2366    #[test]
2367    fn kanban_complexity_accounts_for_parent_chain_depth_without_quadratic_scans() {
2368        let root = KanbanDiagramRenderModel {
2369            nodes: ["section", "lane", "card"]
2370                .into_iter()
2371                .enumerate()
2372                .map(|(index, id)| {
2373                    let mut node =
2374                        crate::diagrams::kanban::KanbanRenderNode::new(id, format!("Node {id}"));
2375                    if index > 0 {
2376                        node.parent_id = Some(["section", "lane"][index - 1].to_string());
2377                    }
2378                    node
2379                })
2380                .collect(),
2381        };
2382
2383        assert_eq!(ModelComplexity::from_kanban(&root).nesting_depth, 3);
2384    }
2385
2386    #[cfg(feature = "diagram-treemap")]
2387    #[test]
2388    fn treemap_complexity_handles_deep_typed_trees_without_serde_recursion() {
2389        let mut node = crate::diagrams::treemap::TreemapNodeRenderModel {
2390            name: "leaf".to_string(),
2391            ..Default::default()
2392        };
2393        for index in (0..1_500).rev() {
2394            node = crate::diagrams::treemap::TreemapNodeRenderModel {
2395                name: format!("section{index}"),
2396                children: Some(vec![node]),
2397                ..Default::default()
2398            };
2399        }
2400        let model = TreemapDiagramRenderModel {
2401            root: node,
2402            ..Default::default()
2403        };
2404
2405        let complexity = TreemapComplexity::from_model(&model);
2406
2407        assert_eq!(complexity.nodes, 1_501);
2408        assert_eq!(complexity.nesting_depth, 1_500);
2409        assert!(complexity.label_bytes >= "leaf".len());
2410    }
2411
2412    #[cfg(feature = "diagram-ishikawa")]
2413    #[test]
2414    fn ishikawa_complexity_handles_deep_typed_trees_without_serde_recursion() {
2415        let mut node = crate::diagrams::ishikawa::IshikawaNodeRenderModel {
2416            text: "leaf".to_string(),
2417            children: Vec::new(),
2418        };
2419        for index in (0..1_500).rev() {
2420            node = crate::diagrams::ishikawa::IshikawaNodeRenderModel {
2421                text: format!("cause{index}"),
2422                children: vec![node],
2423            };
2424        }
2425        let model = IshikawaDiagramRenderModel {
2426            root: Some(node),
2427            ..Default::default()
2428        };
2429
2430        let complexity = IshikawaComplexity::from_model(&model);
2431
2432        assert_eq!(complexity.nodes, 1_501);
2433        assert_eq!(complexity.nesting_depth, 1_500);
2434        assert!(complexity.label_bytes >= "leaf".len());
2435    }
2436
2437    impl InputResourcePolicy {
2438        fn apply_for_test(mut self, id: InputResourceLimitId, value: usize) -> Self {
2439            self.apply_limit(id, value).unwrap();
2440            self
2441        }
2442    }
2443}