Skip to main content

merman_render/
resources.rs

1#[cfg(any(
2    feature = "diagram-flowchart",
3    feature = "diagram-swimlane",
4    feature = "diagram-agentflow"
5))]
6use merman_core::diagrams::flowchart::FlowchartModel;
7#[cfg(feature = "diagram-mindmap")]
8use merman_core::diagrams::mindmap::MindmapDiagramRenderModel;
9#[cfg(feature = "diagram-zenuml")]
10use merman_core::diagrams::zenuml::ZenumlDiagramRenderModel;
11#[cfg(feature = "diagram-class")]
12use merman_core::models::class_diagram::ClassDiagram;
13use merman_core::resources::{
14    InputResourceLimitExceeded, InputResourceLimitId, InputResourceLimitPhase, InputResourcePolicy,
15};
16pub use merman_core::resources::{
17    ModelComplexity, RESOURCE_PROFILE_DESCRIPTORS, ResourceProfile as RenderResourceProfile,
18    ResourceProfileDescriptor as RenderResourceProfileDescriptor,
19};
20use merman_core::{
21    OperationCancelled, OperationControl, OperationLedgerError, OperationPhase,
22    OperationResourceDomain, OperationResourceLimitExceeded, OperationResourceOverride,
23    OperationResourceProvenance, ParsedDiagramRender, RenderSemanticModel,
24};
25
26const KIB: usize = 1024;
27const MIB: usize = 1024 * KIB;
28
29/// Hard recursion cap used by the SVG backend on WebAssembly targets.
30///
31/// Icon-body admission is intentionally derived from this smallest supported backend cap so a
32/// registry accepted on native targets remains portable to WebAssembly.
33pub const WASM_RESVG_TREE_DEPTH_HARD_CAP: usize = 64;
34/// Maximum XML nesting accepted inside a portable icon body.
35///
36/// The icon renderer adds at least one wrapping `<g>` before whole-document validation, so this
37/// remains strictly below the WebAssembly backend hard cap.
38pub const MAX_PORTABLE_ICON_BODY_XML_DEPTH: usize = 32;
39const _: () = assert!(MAX_PORTABLE_ICON_BODY_XML_DEPTH < WASM_RESVG_TREE_DEPTH_HARD_CAP);
40
41#[cfg(not(target_arch = "wasm32"))]
42pub const MAX_RESVG_TREE_DEPTH: usize = 256;
43pub const SVG_BACKEND_TREE_DEPTH_HARD_CAP_ID: &str = "svg_backend_tree_depth";
44/// Maximum resolved usvg nodes accepted independently of caller-selected policies.
45///
46/// This matches usvg's own parser capability while rejecting expansion before it can allocate the
47/// upstream parser's full tree.
48pub const MAX_RESVG_TREE_NODES: usize = 1_000_000;
49pub const SVG_BACKEND_TREE_NODES_HARD_CAP_ID: &str = "svg_backend_tree_nodes";
50
51#[cfg(target_arch = "wasm32")]
52pub const MAX_RESVG_TREE_DEPTH: usize = WASM_RESVG_TREE_DEPTH_HARD_CAP;
53
54// Backend capability for recursively owned typed/compatibility trees, not a Mermaid syntax limit.
55// It remains active when policy budgets are disabled because increasing it is not stack-safe.
56#[cfg(not(target_arch = "wasm32"))]
57const MAX_RECURSIVE_MODEL_TREE_DEPTH: usize = merman_core::MAX_DIAGRAM_NESTING_DEPTH;
58
59#[cfg(target_arch = "wasm32")]
60const MAX_RECURSIVE_MODEL_TREE_DEPTH: usize = 64;
61
62pub const RESOURCE_PROFILE_COUNT: usize = merman_core::resources::RESOURCE_PROFILE_COUNT;
63const RENDER_RESOURCE_LIMIT_COUNT: usize = 5;
64pub const RESOURCE_LIMIT_COUNT: usize =
65    merman_core::resources::INPUT_RESOURCE_LIMIT_COUNT + RENDER_RESOURCE_LIMIT_COUNT;
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
68pub enum ResourceLimitPhase {
69    Source,
70    LayoutModel,
71    SvgOutput,
72    SvgPostprocess,
73}
74
75/// Stable reason why a resource check failed.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
77#[non_exhaustive]
78pub enum ResourceLimitCause {
79    /// The requested work exceeded the configured policy ceiling.
80    Ceiling,
81    /// Computing cumulative work overflowed the platform counter.
82    ArithmeticOverflow,
83}
84
85impl ResourceLimitCause {
86    pub const fn as_str(self) -> &'static str {
87        match self {
88            Self::Ceiling => "ceiling",
89            Self::ArithmeticOverflow => "arithmetic_overflow",
90        }
91    }
92}
93
94impl std::fmt::Display for ResourceLimitCause {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        f.write_str(self.as_str())
97    }
98}
99
100impl ResourceLimitPhase {
101    const ALL: [Self; 4] = [
102        Self::Source,
103        Self::LayoutModel,
104        Self::SvgOutput,
105        Self::SvgPostprocess,
106    ];
107
108    pub const fn as_str(self) -> &'static str {
109        match self {
110            Self::Source => "source",
111            Self::LayoutModel => "layout_model",
112            Self::SvgOutput => "svg_output",
113            Self::SvgPostprocess => "svg_postprocess",
114        }
115    }
116
117    fn from_stable_id(value: &str) -> Option<Self> {
118        Self::ALL.into_iter().find(|phase| phase.as_str() == value)
119    }
120}
121
122impl std::fmt::Display for ResourceLimitPhase {
123    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124        f.write_str(self.as_str())
125    }
126}
127
128#[repr(usize)]
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
130pub enum RenderResourceLimitId {
131    MaxSvgBytes,
132    MaxSvgElements,
133    MaxLayoutWorkUnits,
134    SvgBackendTreeNodes,
135    SvgBackendTreeDepth,
136}
137
138impl RenderResourceLimitId {
139    pub const ALL: [Self; RENDER_RESOURCE_LIMIT_COUNT] = [
140        Self::MaxSvgBytes,
141        Self::MaxSvgElements,
142        Self::MaxLayoutWorkUnits,
143        Self::SvgBackendTreeNodes,
144        Self::SvgBackendTreeDepth,
145    ];
146
147    const fn index(self) -> usize {
148        self as usize
149    }
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
153pub enum ResourceLimitId {
154    Input(InputResourceLimitId),
155    Render(RenderResourceLimitId),
156}
157
158#[allow(non_upper_case_globals)]
159impl ResourceLimitId {
160    pub const MaxSourceBytes: Self = Self::Input(InputResourceLimitId::MaxSourceBytes);
161    pub const MaxModelItems: Self = Self::Input(InputResourceLimitId::MaxModelItems);
162    pub const MaxModelTextBytes: Self = Self::Input(InputResourceLimitId::MaxModelTextBytes);
163    pub const MaxModelNestingDepth: Self = Self::Input(InputResourceLimitId::MaxModelNestingDepth);
164    pub const MaxSvgBytes: Self = Self::Render(RenderResourceLimitId::MaxSvgBytes);
165    pub const MaxSvgElements: Self = Self::Render(RenderResourceLimitId::MaxSvgElements);
166    pub const MaxLayoutWorkUnits: Self = Self::Render(RenderResourceLimitId::MaxLayoutWorkUnits);
167    pub const SvgBackendTreeNodes: Self = Self::Render(RenderResourceLimitId::SvgBackendTreeNodes);
168    pub const SvgBackendTreeDepth: Self = Self::Render(RenderResourceLimitId::SvgBackendTreeDepth);
169
170    pub const ALL: [Self; RESOURCE_LIMIT_COUNT] = [
171        Self::MaxSourceBytes,
172        Self::MaxModelItems,
173        Self::MaxModelTextBytes,
174        Self::MaxModelNestingDepth,
175        Self::MaxLayoutWorkUnits,
176        Self::MaxSvgBytes,
177        Self::MaxSvgElements,
178        Self::SvgBackendTreeNodes,
179        Self::SvgBackendTreeDepth,
180    ];
181
182    pub fn from_stable_id(id: &str) -> Option<Self> {
183        InputResourceLimitId::from_stable_id(id)
184            .map(Self::Input)
185            .or_else(|| {
186                RENDER_RESOURCE_LIMIT_DESCRIPTORS
187                    .iter()
188                    .find(|descriptor| descriptor.stable_id == id)
189                    .map(|descriptor| descriptor.id)
190            })
191    }
192
193    pub const fn descriptor(self) -> ResourceLimitDescriptor {
194        match self {
195            Self::Input(id) => input_descriptor(id),
196            Self::Render(id) => RENDER_RESOURCE_LIMIT_DESCRIPTORS[id.index()],
197        }
198    }
199
200    pub const fn as_str(self) -> &'static str {
201        self.descriptor().stable_id
202    }
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206#[non_exhaustive]
207pub struct ResourceLimitDescriptor {
208    pub id: ResourceLimitId,
209    pub stable_id: &'static str,
210    pub phase: ResourceLimitPhase,
211    pub description: &'static str,
212    pub overridable: bool,
213    pub hard_cap: bool,
214    pub minimum_value: usize,
215}
216
217const fn input_descriptor(id: InputResourceLimitId) -> ResourceLimitDescriptor {
218    let descriptor = id.descriptor();
219    ResourceLimitDescriptor {
220        id: ResourceLimitId::Input(id),
221        stable_id: descriptor.stable_id,
222        phase: match descriptor.phase {
223            InputResourceLimitPhase::Source => ResourceLimitPhase::Source,
224            InputResourceLimitPhase::Model => ResourceLimitPhase::LayoutModel,
225        },
226        description: descriptor.description,
227        overridable: descriptor.overridable,
228        hard_cap: false,
229        minimum_value: descriptor.minimum_value,
230    }
231}
232
233const RENDER_RESOURCE_LIMIT_DESCRIPTORS: [ResourceLimitDescriptor; RENDER_RESOURCE_LIMIT_COUNT] = [
234    ResourceLimitDescriptor {
235        id: ResourceLimitId::MaxSvgBytes,
236        stable_id: "max_svg_bytes",
237        phase: ResourceLimitPhase::SvgOutput,
238        description: "Maximum serialized SVG bytes",
239        overridable: true,
240        hard_cap: false,
241        minimum_value: 1,
242    },
243    ResourceLimitDescriptor {
244        id: ResourceLimitId::MaxSvgElements,
245        stable_id: "max_svg_elements",
246        phase: ResourceLimitPhase::SvgPostprocess,
247        description: "Maximum SVG element count",
248        overridable: true,
249        hard_cap: false,
250        minimum_value: 1,
251    },
252    ResourceLimitDescriptor {
253        id: ResourceLimitId::MaxLayoutWorkUnits,
254        stable_id: "max_layout_work_units",
255        phase: ResourceLimitPhase::LayoutModel,
256        description: "Maximum family-accounted derived layout and render geometry work units",
257        overridable: true,
258        hard_cap: false,
259        minimum_value: 1,
260    },
261    ResourceLimitDescriptor {
262        id: ResourceLimitId::SvgBackendTreeNodes,
263        stable_id: SVG_BACKEND_TREE_NODES_HARD_CAP_ID,
264        phase: ResourceLimitPhase::SvgPostprocess,
265        description: "Maximum SVG backend tree nodes accepted by rendering backends",
266        overridable: false,
267        hard_cap: true,
268        minimum_value: 1,
269    },
270    ResourceLimitDescriptor {
271        id: ResourceLimitId::SvgBackendTreeDepth,
272        stable_id: SVG_BACKEND_TREE_DEPTH_HARD_CAP_ID,
273        phase: ResourceLimitPhase::SvgPostprocess,
274        description: "Maximum SVG backend tree depth accepted by rendering backends",
275        overridable: false,
276        hard_cap: true,
277        minimum_value: 1,
278    },
279];
280
281pub static RESOURCE_LIMIT_DESCRIPTORS: [ResourceLimitDescriptor; RESOURCE_LIMIT_COUNT] = [
282    input_descriptor(InputResourceLimitId::MaxSourceBytes),
283    input_descriptor(InputResourceLimitId::MaxModelItems),
284    input_descriptor(InputResourceLimitId::MaxModelTextBytes),
285    input_descriptor(InputResourceLimitId::MaxModelNestingDepth),
286    RENDER_RESOURCE_LIMIT_DESCRIPTORS[2],
287    RENDER_RESOURCE_LIMIT_DESCRIPTORS[0],
288    RENDER_RESOURCE_LIMIT_DESCRIPTORS[1],
289    RENDER_RESOURCE_LIMIT_DESCRIPTORS[3],
290    RENDER_RESOURCE_LIMIT_DESCRIPTORS[4],
291];
292
293const RENDER_PROFILE_VALUES: [[Option<usize>; RESOURCE_PROFILE_COUNT];
294    RENDER_RESOURCE_LIMIT_COUNT] = [
295    [Some(24 * MIB), Some(12 * MIB), Some(128 * MIB), None],
296    [Some(250_000), Some(125_000), Some(1_000_000), None],
297    // A deterministic work-admission budget, not a Mermaid limit or response-time bound.
298    // Interactive and trusted-native admit ordinary diagrams, including nested ELK layouts;
299    // constrained remains a tighter host policy. Hosts control latency through cancellation,
300    // deadlines, and concurrency. See the dated layout-work calibration receipts in
301    // docs/performance for the corpus and the profile-specific rounding rules.
302    [Some(14_100_000), Some(125_000), Some(15_000_000), None],
303    [Some(MAX_RESVG_TREE_NODES); RESOURCE_PROFILE_COUNT],
304    [Some(MAX_RESVG_TREE_DEPTH); RESOURCE_PROFILE_COUNT],
305];
306
307pub const GENERAL_BINDING_DEFAULT_RESOURCE_PROFILE: RenderResourceProfile =
308    merman_core::resources::GENERAL_BINDING_DEFAULT_RESOURCE_PROFILE;
309pub const CLI_DEFAULT_RESOURCE_PROFILE: RenderResourceProfile =
310    merman_core::resources::CLI_DEFAULT_RESOURCE_PROFILE;
311
312pub const fn resource_profile_descriptors() -> &'static [RenderResourceProfileDescriptor] {
313    &merman_core::resources::RESOURCE_PROFILE_DESCRIPTORS
314}
315
316pub const fn resource_limit_descriptors() -> &'static [ResourceLimitDescriptor] {
317    &RESOURCE_LIMIT_DESCRIPTORS
318}
319
320#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
321pub enum ResourceLimitOverrideError {
322    #[error("resource limit id `{0}` is not part of resource contract schema 1")]
323    UnknownLimit(String),
324    #[error("resource limit `{0}` is a hard implementation capability and cannot be overridden")]
325    HardCap(&'static str),
326    #[error("resource limit `{0}` must be a positive integer")]
327    NonPositive(&'static str),
328}
329
330#[derive(Debug, Clone, Copy, PartialEq, Eq)]
331pub struct RenderResourcePolicy {
332    input: InputResourcePolicy,
333    render_base_values: [Option<usize>; RENDER_RESOURCE_LIMIT_COUNT],
334    render_effective_values: [Option<usize>; RENDER_RESOURCE_LIMIT_COUNT],
335    render_explicit_overrides: [Option<usize>; RENDER_RESOURCE_LIMIT_COUNT],
336}
337
338impl Default for RenderResourcePolicy {
339    fn default() -> Self {
340        Self::interactive()
341    }
342}
343
344impl RenderResourcePolicy {
345    pub const fn profile(self) -> RenderResourceProfile {
346        self.input.profile()
347    }
348
349    pub const fn interactive() -> Self {
350        Self::for_profile(RenderResourceProfile::Interactive)
351    }
352
353    pub const fn constrained() -> Self {
354        Self::for_profile(RenderResourceProfile::Constrained)
355    }
356
357    pub const fn trusted_native() -> Self {
358        Self::for_profile(RenderResourceProfile::TrustedNative)
359    }
360
361    pub const fn unbounded_for_trusted_input() -> Self {
362        Self::for_profile(RenderResourceProfile::UnboundedForTrustedInput)
363    }
364
365    pub const fn for_profile(profile: RenderResourceProfile) -> Self {
366        let mut render_values = [None; RENDER_RESOURCE_LIMIT_COUNT];
367        let mut index = 0;
368        while index < RENDER_RESOURCE_LIMIT_COUNT {
369            render_values[index] = RENDER_PROFILE_VALUES[index][profile as usize];
370            index += 1;
371        }
372        Self {
373            input: InputResourcePolicy::for_profile(profile),
374            render_base_values: render_values,
375            render_effective_values: render_values,
376            render_explicit_overrides: [None; RENDER_RESOURCE_LIMIT_COUNT],
377        }
378    }
379
380    pub const fn input_policy(&self) -> &InputResourcePolicy {
381        &self.input
382    }
383
384    pub const fn value(self, id: ResourceLimitId) -> Option<usize> {
385        match id {
386            ResourceLimitId::Input(id) => self.input.value(id),
387            ResourceLimitId::Render(id) => self.render_effective_values[id.index()],
388        }
389    }
390
391    pub const fn base_value(self, id: ResourceLimitId) -> Option<usize> {
392        match id {
393            ResourceLimitId::Input(id) => self.input.base_value(id),
394            ResourceLimitId::Render(id) => self.render_base_values[id.index()],
395        }
396    }
397
398    pub const fn explicit_override(self, id: ResourceLimitId) -> Option<usize> {
399        match id {
400            ResourceLimitId::Input(id) => self.input.explicit_override(id),
401            ResourceLimitId::Render(id) => self.render_explicit_overrides[id.index()],
402        }
403    }
404
405    pub fn explicit_overrides(&self) -> impl Iterator<Item = (ResourceLimitId, usize)> + '_ {
406        ResourceLimitId::ALL
407            .into_iter()
408            .filter_map(|id| self.explicit_override(id).map(|value| (id, value)))
409    }
410
411    pub fn apply_override(
412        &mut self,
413        stable_id: &str,
414        value: usize,
415    ) -> Result<(), ResourceLimitOverrideError> {
416        let id = ResourceLimitId::from_stable_id(stable_id)
417            .ok_or_else(|| ResourceLimitOverrideError::UnknownLimit(stable_id.to_string()))?;
418        self.apply_limit(id, value)
419    }
420
421    pub fn apply_limit(
422        &mut self,
423        id: ResourceLimitId,
424        value: usize,
425    ) -> Result<(), ResourceLimitOverrideError> {
426        match id {
427            ResourceLimitId::Input(id) => {
428                self.input
429                    .apply_limit(id, value)
430                    .map_err(|error| match error {
431                        merman_core::resources::InputResourceLimitOverrideError::UnknownLimit(
432                            id,
433                        ) => ResourceLimitOverrideError::UnknownLimit(id),
434                        merman_core::resources::InputResourceLimitOverrideError::NonPositive(
435                            id,
436                        ) => ResourceLimitOverrideError::NonPositive(id),
437                    })
438            }
439            ResourceLimitId::Render(id) => {
440                let descriptor = RENDER_RESOURCE_LIMIT_DESCRIPTORS[id.index()];
441                if descriptor.hard_cap || !descriptor.overridable {
442                    return Err(ResourceLimitOverrideError::HardCap(descriptor.stable_id));
443                }
444                if value == 0 {
445                    return Err(ResourceLimitOverrideError::NonPositive(
446                        descriptor.stable_id,
447                    ));
448                }
449                self.render_effective_values[id.index()] = Some(value);
450                self.render_explicit_overrides[id.index()] = Some(value);
451                Ok(())
452            }
453        }
454    }
455
456    pub fn with_override(
457        mut self,
458        stable_id: &str,
459        value: usize,
460    ) -> Result<Self, ResourceLimitOverrideError> {
461        self.apply_override(stable_id, value)?;
462        Ok(self)
463    }
464
465    pub fn with_limit(
466        mut self,
467        id: ResourceLimitId,
468        value: usize,
469    ) -> Result<Self, ResourceLimitOverrideError> {
470        self.apply_limit(id, value)?;
471        Ok(self)
472    }
473
474    fn check_render_limit(
475        &self,
476        phase: ResourceLimitPhase,
477        id: RenderResourceLimitId,
478        actual: usize,
479    ) -> Result<(), ResourceLimitExceeded> {
480        let Some(max) = self.render_effective_values[id.index()] else {
481            return Ok(());
482        };
483        if actual <= max {
484            return Ok(());
485        }
486        let limit = ResourceLimitId::Render(id);
487        Err(ResourceLimitExceeded {
488            cause: ResourceLimitCause::Ceiling,
489            phase,
490            limit: limit.as_str(),
491            actual,
492            max,
493            profile: self.profile(),
494            explicit_overrides: self
495                .explicit_overrides()
496                .map(|(id, value)| ResourceLimitOverride { id, value })
497                .collect(),
498        })
499    }
500
501    pub fn check_source_bytes(&self, source: &str) -> Result<(), ResourceLimitExceeded> {
502        self.input
503            .check_source_bytes(source)
504            .map_err(|error| ResourceLimitExceeded::from_input(self, error))
505    }
506
507    pub fn check_render_model(
508        &self,
509        model: &RenderSemanticModel,
510    ) -> Result<(), ResourceLimitExceeded> {
511        let complexity = ModelComplexity::from_render_model(model);
512        self.check_render_model_complexity(model, complexity)
513    }
514
515    pub fn check_parsed_render(
516        &self,
517        parsed: &ParsedDiagramRender,
518    ) -> Result<(), ResourceLimitExceeded> {
519        let mut complexity = ModelComplexity::from_render_model(parsed.model());
520        complexity.text_bytes = complexity
521            .text_bytes
522            .saturating_add(parsed.retained_render_context_bytes());
523        self.check_render_model_complexity(parsed.model(), complexity)
524    }
525
526    fn check_render_model_complexity(
527        &self,
528        model: &RenderSemanticModel,
529        complexity: ModelComplexity,
530    ) -> Result<(), ResourceLimitExceeded> {
531        self.check_model_complexity(complexity)?;
532
533        let recursive_tree = match model {
534            #[cfg(feature = "diagram-treemap")]
535            RenderSemanticModel::Treemap(_) => true,
536            #[cfg(feature = "diagram-ishikawa")]
537            RenderSemanticModel::Ishikawa(_) => true,
538            _ => false,
539        };
540        if recursive_tree && complexity.nesting_depth > MAX_RECURSIVE_MODEL_TREE_DEPTH {
541            return Err(ResourceLimitExceeded {
542                cause: ResourceLimitCause::Ceiling,
543                phase: ResourceLimitPhase::LayoutModel,
544                limit: "typed_model_tree_depth",
545                actual: complexity.nesting_depth,
546                max: MAX_RECURSIVE_MODEL_TREE_DEPTH,
547                profile: self.profile(),
548                explicit_overrides: self
549                    .explicit_overrides()
550                    .map(|(id, value)| ResourceLimitOverride { id, value })
551                    .collect(),
552            });
553        }
554
555        Ok(())
556    }
557
558    pub fn check_model_complexity(
559        &self,
560        complexity: ModelComplexity,
561    ) -> Result<(), ResourceLimitExceeded> {
562        self.input
563            .check_model_complexity(complexity)
564            .map_err(|error| ResourceLimitExceeded::from_input(self, error))
565    }
566
567    pub fn check_svg_bytes(
568        &self,
569        svg: &str,
570        phase: ResourceLimitPhase,
571    ) -> Result<(), ResourceLimitExceeded> {
572        self.check_svg_byte_count(svg.len(), phase)
573    }
574
575    pub(crate) fn check_svg_byte_count(
576        &self,
577        bytes: usize,
578        phase: ResourceLimitPhase,
579    ) -> Result<(), ResourceLimitExceeded> {
580        self.check_render_limit(phase, RenderResourceLimitId::MaxSvgBytes, bytes)
581    }
582
583    pub fn check_svg_structure(
584        &self,
585        elements: usize,
586        tree_depth: usize,
587    ) -> Result<(), ResourceLimitExceeded> {
588        self.check_render_limit(
589            ResourceLimitPhase::SvgPostprocess,
590            RenderResourceLimitId::MaxSvgElements,
591            elements,
592        )?;
593        self.check_render_limit(
594            ResourceLimitPhase::SvgPostprocess,
595            RenderResourceLimitId::SvgBackendTreeNodes,
596            elements,
597        )?;
598        self.check_render_limit(
599            ResourceLimitPhase::SvgPostprocess,
600            RenderResourceLimitId::SvgBackendTreeDepth,
601            tree_depth,
602        )
603    }
604
605    #[cfg(any(
606        feature = "diagram-flowchart",
607        feature = "diagram-swimlane",
608        feature = "diagram-agentflow"
609    ))]
610    pub fn check_flowchart_complexity(
611        &self,
612        model: &FlowchartModel,
613    ) -> Result<FlowchartComplexity, ResourceLimitExceeded> {
614        self.input
615            .check_flowchart_complexity(model)
616            .map_err(|error| ResourceLimitExceeded::from_input(self, error))
617    }
618
619    #[cfg(feature = "diagram-class")]
620    pub fn check_class_complexity(
621        &self,
622        model: &ClassDiagram,
623    ) -> Result<ClassComplexity, ResourceLimitExceeded> {
624        self.input
625            .check_class_complexity(model)
626            .map_err(|error| ResourceLimitExceeded::from_input(self, error))
627    }
628
629    #[cfg(feature = "diagram-mindmap")]
630    pub fn check_mindmap_complexity(
631        &self,
632        model: &MindmapDiagramRenderModel,
633    ) -> Result<MindmapComplexity, ResourceLimitExceeded> {
634        self.input
635            .check_mindmap_complexity(model)
636            .map_err(|error| ResourceLimitExceeded::from_input(self, error))
637    }
638
639    #[cfg(feature = "diagram-zenuml")]
640    pub fn check_zenuml_complexity(
641        &self,
642        model: &ZenumlDiagramRenderModel,
643    ) -> Result<ZenumlComplexity, ResourceLimitExceeded> {
644        self.input
645            .check_zenuml_complexity(model)
646            .map_err(|error| ResourceLimitExceeded::from_input(self, error))
647    }
648
649    #[cfg(feature = "diagram-sequence")]
650    pub fn check_sequence_complexity(
651        &self,
652        model: &merman_core::diagrams::sequence::SequenceDiagramRenderModel,
653    ) -> Result<SequenceComplexity, ResourceLimitExceeded> {
654        self.input
655            .check_sequence_complexity(model)
656            .map_err(|error| ResourceLimitExceeded::from_input(self, error))
657    }
658
659    pub fn check_layout_work_units(&self, work_units: usize) -> Result<(), ResourceLimitExceeded> {
660        self.check_render_limit(
661            ResourceLimitPhase::LayoutModel,
662            RenderResourceLimitId::MaxLayoutWorkUnits,
663            work_units,
664        )
665    }
666}
667
668/// One cumulative derived-geometry budget shared by layout and SVG emission.
669/// Failure returned by controlled render work accounting.
670///
671/// Cancellation is deliberately kept separate from a resource rejection. Adapter kernels may
672/// expose only a neutral interruption signal, so the owning work control retains this structured
673/// cause until the outer renderer maps it to [`crate::Error`].
674#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
675pub(crate) enum OperationWorkError {
676    #[error(transparent)]
677    Cancelled(#[from] OperationCancelled),
678    #[error(transparent)]
679    ResourceLimitExceeded(#[from] ResourceLimitExceeded),
680    #[error(transparent)]
681    ForeignResourceTerminal(OperationLedgerError),
682}
683
684#[cfg_attr(
685    not(feature = "all-diagrams"),
686    allow(
687        dead_code,
688        reason = "Shared operation support has different consumers in each diagram selection."
689    )
690)]
691pub(crate) struct OperationWorkMeter {
692    policy: RenderResourcePolicy,
693    control: OperationControl,
694    used: std::sync::atomic::AtomicUsize,
695    projected_svg_bytes: std::sync::atomic::AtomicUsize,
696}
697
698#[cfg_attr(
699    not(feature = "all-diagrams"),
700    allow(
701        dead_code,
702        reason = "Shared operation support has different consumers in each diagram selection."
703    )
704)]
705pub(crate) struct SvgByteReservation {
706    pub(crate) additional_bytes: usize,
707    pub(crate) limit_error: Option<ResourceLimitExceeded>,
708}
709
710#[cfg_attr(
711    not(feature = "all-diagrams"),
712    allow(
713        dead_code,
714        reason = "Shared operation support has different consumers in each diagram selection."
715    )
716)]
717impl OperationWorkMeter {
718    #[cfg_attr(not(feature = "layout-elk"), allow(dead_code))]
719    pub(crate) fn new(policy: RenderResourcePolicy) -> Self {
720        Self::new_with_control(policy, OperationControl::new())
721    }
722
723    pub(crate) fn new_with_control(
724        policy: RenderResourcePolicy,
725        control: OperationControl,
726    ) -> Self {
727        Self {
728            policy,
729            control,
730            used: std::sync::atomic::AtomicUsize::new(0),
731            projected_svg_bytes: std::sync::atomic::AtomicUsize::new(0),
732        }
733    }
734
735    pub(crate) const fn policy(&self) -> RenderResourcePolicy {
736        self.policy
737    }
738
739    /// Replays the operation's first cancellation, ceiling, or overflow terminal.
740    pub(crate) fn checkpoint(&self, phase: OperationPhase) -> Result<(), OperationWorkError> {
741        self.control
742            .terminal_checkpoint_at(phase)
743            .map_err(|error| self.map_terminal_error(error))
744    }
745
746    fn resource_checkpoint(&self, phase: OperationPhase) -> Result<(), OperationWorkError> {
747        self.checkpoint(phase)
748    }
749
750    pub(crate) fn preflight(&self, additional: usize) -> Result<(), OperationWorkError> {
751        let phase = OperationPhase::Layout;
752        self.resource_checkpoint(phase)?;
753        if additional == 0 {
754            return Ok(());
755        }
756        let used = self.used.load(std::sync::atomic::Ordering::Relaxed);
757        let Some(next) = used.checked_add(additional) else {
758            return Err(self.terminate_resource_error(
759                self.layout_work_overflow_details(),
760                phase,
761                used,
762                additional,
763            ));
764        };
765        match self.policy.check_layout_work_units(next) {
766            Ok(()) => Ok(()),
767            Err(error) => Err(self.terminate_resource_error(error, phase, used, additional)),
768        }
769    }
770
771    /// Charges work atomically. A rejected charge leaves the cumulative usage unchanged.
772    pub(crate) fn charge(&self, additional: usize) -> Result<(), OperationWorkError> {
773        let phase = OperationPhase::Layout;
774        self.resource_checkpoint(phase)?;
775        if additional == 0 {
776            return Ok(());
777        }
778        let mut used = self.used.load(std::sync::atomic::Ordering::Relaxed);
779        loop {
780            let Some(next) = used.checked_add(additional) else {
781                return Err(self.terminate_resource_error(
782                    self.layout_work_overflow_details(),
783                    phase,
784                    used,
785                    additional,
786                ));
787            };
788            if let Err(error) = self.policy.check_layout_work_units(next) {
789                return Err(self.terminate_resource_error(error, phase, used, additional));
790            }
791            match self.used.compare_exchange_weak(
792                used,
793                next,
794                std::sync::atomic::Ordering::Relaxed,
795                std::sync::atomic::Ordering::Relaxed,
796            ) {
797                Ok(_) => return Ok(()),
798                Err(actual) => {
799                    self.resource_checkpoint(phase)?;
800                    used = actual;
801                }
802            }
803        }
804    }
805
806    fn layout_work_overflow_details(&self) -> ResourceLimitExceeded {
807        accumulation_overflow(
808            self.policy,
809            ResourceLimitPhase::LayoutModel,
810            RenderResourceLimitId::MaxLayoutWorkUnits,
811        )
812    }
813
814    pub(crate) fn arithmetic_overflow(&self) -> OperationWorkError {
815        self.terminate_resource_error(
816            self.layout_work_overflow_details(),
817            OperationPhase::Layout,
818            self.used(),
819            1,
820        )
821    }
822
823    /// Reserves the projected serialized bytes contributed by external icon expansion.
824    ///
825    /// The final whole-document SVG check remains authoritative. This earlier cumulative charge
826    /// prevents repeated maximum-size icons from allocating their complete expanded strings before
827    /// the operation-level output policy can reject them.
828    pub(crate) fn charge_svg_bytes(&self, additional: usize) -> Result<(), OperationWorkError> {
829        let phase = OperationPhase::Emit;
830        self.resource_checkpoint(phase)?;
831        let mut used = self
832            .projected_svg_bytes
833            .load(std::sync::atomic::Ordering::Relaxed);
834        loop {
835            let Some(next) = used.checked_add(additional) else {
836                return Err(self.terminate_resource_error(
837                    accumulation_overflow(
838                        self.policy,
839                        ResourceLimitPhase::SvgOutput,
840                        RenderResourceLimitId::MaxSvgBytes,
841                    ),
842                    phase,
843                    used,
844                    additional,
845                ));
846            };
847            if let Err(error) = self
848                .policy
849                .check_svg_byte_count(next, ResourceLimitPhase::SvgOutput)
850            {
851                return Err(self.terminate_resource_error(error, phase, used, additional));
852            }
853            match self.projected_svg_bytes.compare_exchange_weak(
854                used,
855                next,
856                std::sync::atomic::Ordering::Relaxed,
857                std::sync::atomic::Ordering::Relaxed,
858            ) {
859                Ok(_) => return Ok(()),
860                Err(actual) => {
861                    self.resource_checkpoint(phase)?;
862                    used = actual;
863                }
864            }
865        }
866    }
867
868    /// Checks an absolute SVG byte projection without consuming the cumulative icon reservation.
869    ///
870    /// Whole-document postprocessors use this after a streaming size pass. A rejection is still an
871    /// operation terminal, but a successful preflight must not double-count icon bytes already
872    /// included in the projected document.
873    pub(crate) fn preflight_svg_byte_count(
874        &self,
875        actual: usize,
876        resource_phase: ResourceLimitPhase,
877        operation_phase: OperationPhase,
878    ) -> Result<(), OperationWorkError> {
879        self.resource_checkpoint(operation_phase)?;
880        self.policy
881            .check_svg_byte_count(actual, resource_phase)
882            .map_err(|error| self.terminate_resource_error(error, operation_phase, 0, actual))
883    }
884
885    pub(crate) fn preflight_svg_structure(
886        &self,
887        elements: usize,
888        tree_depth: usize,
889        operation_phase: OperationPhase,
890    ) -> Result<(), OperationWorkError> {
891        self.resource_checkpoint(operation_phase)?;
892        self.policy
893            .check_svg_structure(elements, tree_depth)
894            .map_err(|error| self.terminate_absolute_resource_error(error, operation_phase))
895    }
896
897    pub(crate) fn preflight_parsed_render(
898        &self,
899        parsed: &ParsedDiagramRender,
900        operation_phase: OperationPhase,
901    ) -> Result<(), OperationWorkError> {
902        self.resource_checkpoint(operation_phase)?;
903        let result = self.policy.check_parsed_render(parsed);
904        self.resource_checkpoint(operation_phase)?;
905        result.map_err(|error| self.terminate_absolute_resource_error(error, operation_phase))
906    }
907
908    #[cfg(feature = "diagram-class")]
909    pub(crate) fn preflight_class_complexity(
910        &self,
911        model: &ClassDiagram,
912        operation_phase: OperationPhase,
913    ) -> Result<ClassComplexity, OperationWorkError> {
914        self.resource_checkpoint(operation_phase)?;
915        let result = self.policy.check_class_complexity(model);
916        self.resource_checkpoint(operation_phase)?;
917        result.map_err(|error| self.terminate_absolute_resource_error(error, operation_phase))
918    }
919
920    #[cfg(feature = "diagram-sequence")]
921    pub(crate) fn preflight_sequence_complexity(
922        &self,
923        model: &merman_core::diagrams::sequence::SequenceDiagramRenderModel,
924        operation_phase: OperationPhase,
925    ) -> Result<SequenceComplexity, OperationWorkError> {
926        self.resource_checkpoint(operation_phase)?;
927        let result = self.policy.check_sequence_complexity(model);
928        self.resource_checkpoint(operation_phase)?;
929        result.map_err(|error| self.terminate_absolute_resource_error(error, operation_phase))
930    }
931
932    pub(crate) fn terminate_absolute_resource_error(
933        &self,
934        error: ResourceLimitExceeded,
935        operation_phase: OperationPhase,
936    ) -> OperationWorkError {
937        let actual = error.actual;
938        self.terminate_resource_error(error, operation_phase, 0, actual)
939    }
940
941    pub(crate) fn terminate_svg_byte_count_overflow(
942        &self,
943        resource_phase: ResourceLimitPhase,
944        operation_phase: OperationPhase,
945    ) -> OperationWorkError {
946        self.terminate_resource_error(
947            accumulation_overflow(
948                self.policy,
949                resource_phase,
950                RenderResourceLimitId::MaxSvgBytes,
951            ),
952            operation_phase,
953            0,
954            usize::MAX,
955        )
956    }
957
958    fn terminate_resource_error(
959        &self,
960        error: ResourceLimitExceeded,
961        phase: OperationPhase,
962        consumed: usize,
963        requested: usize,
964    ) -> OperationWorkError {
965        let provenance = self.operation_resource_provenance();
966        let terminal = match error.cause {
967            ResourceLimitCause::Ceiling => {
968                self.control
969                    .terminate_resource_limit(OperationResourceLimitExceeded {
970                        id: error.limit,
971                        phase,
972                        resource_phase: error.phase.as_str(),
973                        limit: saturating_u64(error.max),
974                        consumed: saturating_u64(consumed),
975                        requested: saturating_u64(requested),
976                        provenance,
977                    })
978            }
979            ResourceLimitCause::ArithmeticOverflow => self.control.terminate_resource_overflow(
980                error.limit,
981                phase,
982                error.phase.as_str(),
983                saturating_u64(error.actual),
984                saturating_u64(error.max),
985                provenance,
986            ),
987        };
988        self.map_terminal_error(terminal)
989    }
990
991    fn operation_resource_provenance(&self) -> OperationResourceProvenance {
992        OperationResourceProvenance::new(
993            OperationResourceDomain::Render,
994            Some(self.policy.profile()),
995            self.policy
996                .explicit_overrides()
997                .map(|(id, value)| OperationResourceOverride {
998                    id: id.as_str(),
999                    value: saturating_u64(value),
1000                }),
1001        )
1002    }
1003
1004    fn map_terminal_error(&self, error: OperationLedgerError) -> OperationWorkError {
1005        match error {
1006            OperationLedgerError::Cancelled(error) => OperationWorkError::Cancelled(error),
1007            OperationLedgerError::ResourceLimitExceeded(error) => self
1008                .project_resource_limit(&error)
1009                .map(OperationWorkError::ResourceLimitExceeded)
1010                .unwrap_or_else(|| {
1011                    OperationWorkError::ForeignResourceTerminal(
1012                        OperationLedgerError::ResourceLimitExceeded(error),
1013                    )
1014                }),
1015            OperationLedgerError::ArithmeticOverflow { .. } => self
1016                .project_resource_overflow(&error)
1017                .map(OperationWorkError::ResourceLimitExceeded)
1018                .unwrap_or(OperationWorkError::ForeignResourceTerminal(error)),
1019        }
1020    }
1021
1022    fn project_resource_limit(
1023        &self,
1024        error: &OperationResourceLimitExceeded,
1025    ) -> Option<ResourceLimitExceeded> {
1026        let (profile, explicit_overrides) = project_render_provenance(&error.provenance)?;
1027        Some(ResourceLimitExceeded {
1028            cause: ResourceLimitCause::Ceiling,
1029            phase: ResourceLimitPhase::from_stable_id(error.resource_phase)?,
1030            limit: error.id,
1031            actual: saturating_usize(error.consumed.saturating_add(error.requested)),
1032            max: saturating_usize(error.limit),
1033            profile,
1034            explicit_overrides,
1035        })
1036    }
1037
1038    fn project_resource_overflow(
1039        &self,
1040        error: &OperationLedgerError,
1041    ) -> Option<ResourceLimitExceeded> {
1042        let OperationLedgerError::ArithmeticOverflow {
1043            id,
1044            resource_phase,
1045            actual,
1046            maximum,
1047            provenance,
1048            ..
1049        } = error
1050        else {
1051            return None;
1052        };
1053        let (profile, explicit_overrides) = project_render_provenance(provenance)?;
1054        Some(ResourceLimitExceeded {
1055            cause: ResourceLimitCause::ArithmeticOverflow,
1056            phase: ResourceLimitPhase::from_stable_id(resource_phase)?,
1057            limit: id,
1058            actual: saturating_usize(*actual),
1059            max: saturating_usize(*maximum),
1060            profile,
1061            explicit_overrides,
1062        })
1063    }
1064
1065    /// Returns the unreserved SVG budget for bounded policies after all successful charges.
1066    #[cfg(test)]
1067    pub(crate) fn remaining_svg_bytes(&self) -> Option<usize> {
1068        let maximum = self.policy.value(ResourceLimitId::MaxSvgBytes)?;
1069        let used = self
1070            .projected_svg_bytes
1071            .load(std::sync::atomic::Ordering::Relaxed);
1072        Some(maximum.saturating_sub(used))
1073    }
1074
1075    /// Atomically reserves up to `requested` additional SVG bytes.
1076    ///
1077    /// Bounded policies return a deterministic limit error alongside a partial reservation when
1078    /// the full amount does not fit. The caller may still succeed if its bounded producer emits no
1079    /// more than the reserved bytes, then reconcile the conservative reservation to actual output.
1080    pub(crate) fn reserve_svg_bytes_up_to(
1081        &self,
1082        requested: usize,
1083    ) -> Result<SvgByteReservation, OperationWorkError> {
1084        let phase = OperationPhase::Emit;
1085        self.resource_checkpoint(phase)?;
1086        let Some(maximum) = self.policy.value(ResourceLimitId::MaxSvgBytes) else {
1087            self.charge_svg_bytes(requested)?;
1088            return Ok(SvgByteReservation {
1089                additional_bytes: requested,
1090                limit_error: None,
1091            });
1092        };
1093
1094        let mut used = self
1095            .projected_svg_bytes
1096            .load(std::sync::atomic::Ordering::Relaxed);
1097        loop {
1098            let available = maximum.saturating_sub(used);
1099            let additional_bytes = requested.min(available);
1100            let Some(next) = used.checked_add(additional_bytes) else {
1101                return Err(self.terminate_resource_error(
1102                    accumulation_overflow(
1103                        self.policy,
1104                        ResourceLimitPhase::SvgOutput,
1105                        RenderResourceLimitId::MaxSvgBytes,
1106                    ),
1107                    phase,
1108                    used,
1109                    additional_bytes,
1110                ));
1111            };
1112            match self.projected_svg_bytes.compare_exchange_weak(
1113                used,
1114                next,
1115                std::sync::atomic::Ordering::Relaxed,
1116                std::sync::atomic::Ordering::Relaxed,
1117            ) {
1118                Ok(_) => {
1119                    let limit_error = (additional_bytes < requested).then(|| {
1120                        svg_reservation_limit_error(self.policy, maximum, used, requested)
1121                    });
1122                    return Ok(SvgByteReservation {
1123                        additional_bytes,
1124                        limit_error,
1125                    });
1126                }
1127                Err(actual) => {
1128                    self.resource_checkpoint(phase)?;
1129                    used = actual;
1130                }
1131            }
1132        }
1133    }
1134
1135    /// Reconciles an earlier conservative SVG reservation with the bytes actually retained.
1136    pub(crate) fn reconcile_svg_bytes(
1137        &self,
1138        reserved: usize,
1139        actual: usize,
1140    ) -> Result<(), OperationWorkError> {
1141        self.resource_checkpoint(OperationPhase::Emit)?;
1142        if actual > reserved {
1143            return self.charge_svg_bytes(actual - reserved);
1144        }
1145
1146        let released = reserved - actual;
1147        if released != 0 {
1148            let previous = self
1149                .projected_svg_bytes
1150                .fetch_sub(released, std::sync::atomic::Ordering::Relaxed);
1151            debug_assert!(previous >= released);
1152        }
1153        Ok(())
1154    }
1155
1156    pub(crate) fn used(&self) -> usize {
1157        self.used.load(std::sync::atomic::Ordering::Relaxed)
1158    }
1159
1160    #[cfg(test)]
1161    pub(crate) fn projected_svg_bytes(&self) -> usize {
1162        self.projected_svg_bytes
1163            .load(std::sync::atomic::Ordering::Relaxed)
1164    }
1165}
1166
1167fn project_render_provenance(
1168    provenance: &OperationResourceProvenance,
1169) -> Option<(RenderResourceProfile, Vec<ResourceLimitOverride>)> {
1170    if provenance.domain != OperationResourceDomain::Render {
1171        return None;
1172    }
1173    let explicit_overrides = provenance
1174        .explicit_overrides
1175        .iter()
1176        .map(|override_| {
1177            Some(ResourceLimitOverride {
1178                id: ResourceLimitId::from_stable_id(override_.id)?,
1179                value: saturating_usize(override_.value),
1180            })
1181        })
1182        .collect::<Option<Vec<_>>>()?;
1183    Some((provenance.profile?, explicit_overrides))
1184}
1185
1186fn saturating_usize(value: u64) -> usize {
1187    usize::try_from(value).unwrap_or(usize::MAX)
1188}
1189
1190fn saturating_u64(value: usize) -> u64 {
1191    u64::try_from(value).unwrap_or(u64::MAX)
1192}
1193
1194fn accumulation_overflow(
1195    policy: RenderResourcePolicy,
1196    phase: ResourceLimitPhase,
1197    id: RenderResourceLimitId,
1198) -> ResourceLimitExceeded {
1199    let limit = ResourceLimitId::Render(id);
1200    ResourceLimitExceeded {
1201        cause: ResourceLimitCause::ArithmeticOverflow,
1202        phase,
1203        limit: limit.as_str(),
1204        actual: usize::MAX,
1205        max: policy.value(limit).unwrap_or(usize::MAX),
1206        profile: policy.profile(),
1207        explicit_overrides: policy
1208            .explicit_overrides()
1209            .map(|(id, value)| ResourceLimitOverride { id, value })
1210            .collect(),
1211    }
1212}
1213
1214#[cfg_attr(
1215    not(feature = "all-diagrams"),
1216    allow(
1217        dead_code,
1218        reason = "Shared operation support has different consumers in each diagram selection."
1219    )
1220)]
1221fn svg_byte_limit_error(policy: RenderResourcePolicy, maximum: usize) -> ResourceLimitExceeded {
1222    ResourceLimitExceeded {
1223        cause: ResourceLimitCause::Ceiling,
1224        phase: ResourceLimitPhase::SvgOutput,
1225        limit: ResourceLimitId::MaxSvgBytes.as_str(),
1226        actual: maximum.saturating_add(1),
1227        max: maximum,
1228        profile: policy.profile(),
1229        explicit_overrides: policy
1230            .explicit_overrides()
1231            .map(|(id, value)| ResourceLimitOverride { id, value })
1232            .collect(),
1233    }
1234}
1235
1236#[cfg_attr(
1237    not(feature = "all-diagrams"),
1238    allow(
1239        dead_code,
1240        reason = "Shared operation support has different consumers in each diagram selection."
1241    )
1242)]
1243fn svg_reservation_limit_error(
1244    policy: RenderResourcePolicy,
1245    maximum: usize,
1246    used: usize,
1247    requested: usize,
1248) -> ResourceLimitExceeded {
1249    if used.checked_add(requested).is_none() {
1250        return accumulation_overflow(
1251            policy,
1252            ResourceLimitPhase::SvgOutput,
1253            RenderResourceLimitId::MaxSvgBytes,
1254        );
1255    }
1256    svg_byte_limit_error(policy, maximum)
1257}
1258
1259#[derive(Debug, Clone, PartialEq, Eq)]
1260#[non_exhaustive]
1261pub struct ResourceLimitExceeded {
1262    pub cause: ResourceLimitCause,
1263    pub phase: ResourceLimitPhase,
1264    pub limit: &'static str,
1265    pub actual: usize,
1266    pub max: usize,
1267    pub profile: RenderResourceProfile,
1268    pub explicit_overrides: Vec<ResourceLimitOverride>,
1269}
1270
1271impl std::fmt::Display for ResourceLimitExceeded {
1272    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1273        write!(
1274            f,
1275            "resource limit exceeded during {}: {}",
1276            self.phase, self.limit
1277        )?;
1278        if self.cause == ResourceLimitCause::ArithmeticOverflow {
1279            write!(f, " cause={}", self.cause)?;
1280        }
1281        write!(f, " actual={} max={}", self.actual, self.max)
1282    }
1283}
1284
1285impl std::error::Error for ResourceLimitExceeded {}
1286
1287impl ResourceLimitExceeded {
1288    fn from_input(policy: &RenderResourcePolicy, error: InputResourceLimitExceeded) -> Self {
1289        Self {
1290            cause: ResourceLimitCause::Ceiling,
1291            phase: match error.phase {
1292                InputResourceLimitPhase::Source => ResourceLimitPhase::Source,
1293                InputResourceLimitPhase::Model => ResourceLimitPhase::LayoutModel,
1294            },
1295            limit: error.limit,
1296            actual: error.actual,
1297            max: error.max,
1298            profile: error.profile,
1299            explicit_overrides: policy
1300                .explicit_overrides()
1301                .map(|(id, value)| ResourceLimitOverride { id, value })
1302                .collect(),
1303        }
1304    }
1305}
1306
1307#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1308pub struct ResourceLimitOverride {
1309    pub id: ResourceLimitId,
1310    pub value: usize,
1311}
1312
1313#[cfg(all(test, feature = "all-diagrams"))]
1314mod tests {
1315    use super::*;
1316    use merman_core::diagrams::flowchart::{
1317        FlowEdge, FlowEdgeMarker, FlowEdgeStroke, FlowEdgeVisibility, FlowNode, FlowSubgraph,
1318    };
1319    use merman_core::{Engine, ParseOptions, RenderSemanticModel};
1320    use std::collections::HashSet;
1321
1322    #[test]
1323    fn layout_work_defaults_preserve_bounded_general_and_native_profiles() {
1324        for (profile, expected) in [
1325            (RenderResourceProfile::Constrained, Some(125_000)),
1326            (RenderResourceProfile::Interactive, Some(14_100_000)),
1327            (RenderResourceProfile::TrustedNative, Some(15_000_000)),
1328            (RenderResourceProfile::UnboundedForTrustedInput, None),
1329        ] {
1330            let policy = RenderResourcePolicy::for_profile(profile);
1331            assert_eq!(policy.value(ResourceLimitId::MaxLayoutWorkUnits), expected);
1332            assert!(policy.explicit_overrides().next().is_none());
1333        }
1334        assert_eq!(
1335            RenderResourcePolicy::default(),
1336            RenderResourcePolicy::interactive()
1337        );
1338    }
1339
1340    #[test]
1341    fn resource_contract_is_complete_unique_and_drives_every_profile() {
1342        assert_eq!(
1343            RESOURCE_PROFILE_DESCRIPTORS.len(),
1344            RenderResourceProfile::ALL.len()
1345        );
1346        assert_eq!(RESOURCE_LIMIT_DESCRIPTORS.len(), RESOURCE_LIMIT_COUNT);
1347
1348        let profile_ids = RESOURCE_PROFILE_DESCRIPTORS
1349            .iter()
1350            .map(|descriptor| descriptor.id)
1351            .collect::<HashSet<_>>();
1352        assert_eq!(profile_ids.len(), RESOURCE_PROFILE_DESCRIPTORS.len());
1353        assert_eq!(
1354            RESOURCE_PROFILE_DESCRIPTORS
1355                .iter()
1356                .filter(|descriptor| descriptor.recommended_binding_default)
1357                .map(|descriptor| descriptor.profile)
1358                .collect::<Vec<_>>(),
1359            vec![GENERAL_BINDING_DEFAULT_RESOURCE_PROFILE]
1360        );
1361        for profile in RenderResourceProfile::ALL {
1362            let descriptor = profile.descriptor();
1363            assert_eq!(RenderResourceProfile::from_id(descriptor.id), Some(profile));
1364            let policy = RenderResourcePolicy::for_profile(profile);
1365            for limit in RESOURCE_LIMIT_DESCRIPTORS {
1366                assert_eq!(policy.profile(), profile);
1367                if limit.hard_cap {
1368                    assert!(!limit.overridable);
1369                    assert!(policy.value(limit.id).is_some());
1370                }
1371            }
1372        }
1373
1374        let limit_ids = RESOURCE_LIMIT_DESCRIPTORS
1375            .iter()
1376            .map(|descriptor| descriptor.stable_id)
1377            .collect::<HashSet<_>>();
1378        assert_eq!(limit_ids.len(), RESOURCE_LIMIT_DESCRIPTORS.len());
1379        for descriptor in RESOURCE_LIMIT_DESCRIPTORS {
1380            assert_eq!(
1381                ResourceLimitId::from_stable_id(descriptor.stable_id),
1382                Some(descriptor.id)
1383            );
1384            assert_eq!(descriptor.id.descriptor(), descriptor);
1385        }
1386    }
1387
1388    #[test]
1389    fn resource_overrides_fail_closed_for_unknown_and_internal_ids() {
1390        let mut limits = RenderResourcePolicy::interactive();
1391        assert!(matches!(
1392            limits.apply_override("future_limit", 1),
1393            Err(ResourceLimitOverrideError::UnknownLimit(_))
1394        ));
1395        for hard_cap in [
1396            SVG_BACKEND_TREE_NODES_HARD_CAP_ID,
1397            SVG_BACKEND_TREE_DEPTH_HARD_CAP_ID,
1398        ] {
1399            assert_eq!(
1400                limits.apply_override(hard_cap, 1),
1401                Err(ResourceLimitOverrideError::HardCap(hard_cap))
1402            );
1403        }
1404        assert_eq!(
1405            limits.apply_override("max_svg_elements", 0),
1406            Err(ResourceLimitOverrideError::NonPositive("max_svg_elements"))
1407        );
1408        limits.apply_override("max_svg_elements", 7).unwrap();
1409        assert_eq!(limits.value(ResourceLimitId::MaxSvgElements), Some(7));
1410    }
1411
1412    #[test]
1413    fn resolved_svg_backend_hard_caps_remain_active_for_unbounded_policy() {
1414        let policy = RenderResourcePolicy::unbounded_for_trusted_input();
1415        for (elements, tree_depth, limit, actual, max) in [
1416            (
1417                MAX_RESVG_TREE_NODES + 1,
1418                0,
1419                SVG_BACKEND_TREE_NODES_HARD_CAP_ID,
1420                MAX_RESVG_TREE_NODES + 1,
1421                MAX_RESVG_TREE_NODES,
1422            ),
1423            (
1424                0,
1425                MAX_RESVG_TREE_DEPTH + 1,
1426                SVG_BACKEND_TREE_DEPTH_HARD_CAP_ID,
1427                MAX_RESVG_TREE_DEPTH + 1,
1428                MAX_RESVG_TREE_DEPTH,
1429            ),
1430        ] {
1431            let error = policy
1432                .check_svg_structure(elements, tree_depth)
1433                .unwrap_err();
1434
1435            assert_eq!(error.phase, ResourceLimitPhase::SvgPostprocess);
1436            assert_eq!(error.limit, limit);
1437            assert_eq!(error.actual, actual);
1438            assert_eq!(error.max, max);
1439        }
1440    }
1441
1442    #[test]
1443    fn source_limit_reports_structured_error() {
1444        let err = RenderResourcePolicy::unbounded_for_trusted_input()
1445            .with_limit(ResourceLimitId::MaxSourceBytes, 4)
1446            .unwrap()
1447            .with_limit(ResourceLimitId::MaxSvgBytes, 123)
1448            .unwrap()
1449            .check_source_bytes("12345")
1450            .unwrap_err();
1451
1452        assert_eq!(err.phase, ResourceLimitPhase::Source);
1453        assert_eq!(err.limit, "max_source_bytes");
1454        assert_eq!(err.actual, 5);
1455        assert_eq!(err.max, 4);
1456        assert_eq!(
1457            err.explicit_overrides,
1458            vec![
1459                ResourceLimitOverride {
1460                    id: ResourceLimitId::MaxSourceBytes,
1461                    value: 4,
1462                },
1463                ResourceLimitOverride {
1464                    id: ResourceLimitId::MaxSvgBytes,
1465                    value: 123,
1466                },
1467            ]
1468        );
1469    }
1470
1471    #[test]
1472    fn derived_layout_work_limits_report_the_owned_phase_and_metric() {
1473        let limits = RenderResourcePolicy::unbounded_for_trusted_input()
1474            .with_limit(ResourceLimitId::MaxLayoutWorkUnits, 7)
1475            .unwrap();
1476
1477        let error = limits.check_layout_work_units(8).unwrap_err();
1478        assert_eq!(error.phase, ResourceLimitPhase::LayoutModel);
1479        assert_eq!(error.limit, "max_layout_work_units");
1480        assert_eq!(error.cause, ResourceLimitCause::Ceiling);
1481        assert_eq!(error.actual, 8);
1482        assert_eq!(error.max, 7);
1483    }
1484
1485    #[test]
1486    fn operation_work_meter_preflight_does_not_consume_budget() {
1487        let policy = RenderResourcePolicy::unbounded_for_trusted_input()
1488            .with_limit(ResourceLimitId::MaxLayoutWorkUnits, 7)
1489            .unwrap();
1490        let meter = OperationWorkMeter::new(policy);
1491
1492        meter.preflight(7).unwrap();
1493        meter.preflight(7).unwrap();
1494        assert_eq!(meter.used(), 0);
1495        meter.charge(7).unwrap();
1496        assert_eq!(meter.used(), 7);
1497    }
1498
1499    #[test]
1500    fn operation_work_meter_checks_shared_control_before_charging() {
1501        let control = OperationControl::new();
1502        control.cancel();
1503        let meter = OperationWorkMeter::new_with_control(
1504            RenderResourcePolicy::unbounded_for_trusted_input()
1505                .with_limit(ResourceLimitId::MaxLayoutWorkUnits, 1)
1506                .unwrap(),
1507            control,
1508        );
1509
1510        let first = meter.charge(2).unwrap_err();
1511        let OperationWorkError::Cancelled(error) = &first else {
1512            panic!("expected cancellation to win before resource accounting");
1513        };
1514        assert_eq!(error.phase, OperationPhase::Layout);
1515        assert_eq!(meter.used(), 0);
1516        assert_eq!(meter.charge(2).unwrap_err(), first);
1517    }
1518
1519    #[test]
1520    fn operation_work_meter_checks_deadline_before_charging() {
1521        let control = OperationControl::new().with_deadline(std::time::Duration::ZERO);
1522        let meter = OperationWorkMeter::new_with_control(
1523            RenderResourcePolicy::unbounded_for_trusted_input(),
1524            control,
1525        );
1526
1527        let OperationWorkError::Cancelled(error) = meter.charge(1).unwrap_err() else {
1528            panic!("expected deadline cancellation before resource accounting");
1529        };
1530        assert_eq!(error.phase, OperationPhase::Layout);
1531        assert_eq!(error.reason, merman_core::CancelReason::DeadlineExceeded);
1532        assert_eq!(meter.used(), 0);
1533    }
1534
1535    #[test]
1536    fn model_preflight_observes_cancellation_before_latching_its_limit() {
1537        let parsed = Engine::new()
1538            .parse_diagram_for_render_model_sync("flowchart TD\nA --> B\n", ParseOptions::strict())
1539            .expect("the controlled fixture should parse")
1540            .expect("the controlled fixture should produce a render model");
1541        let policy = RenderResourcePolicy::unbounded_for_trusted_input()
1542            .with_limit(ResourceLimitId::MaxModelItems, 1)
1543            .expect("the model limit should be valid");
1544        let control = OperationControl::new();
1545        control.cancel_after_checkpoints(1);
1546        let meter = OperationWorkMeter::new_with_control(policy, control);
1547
1548        let first = meter
1549            .preflight_parsed_render(&parsed, OperationPhase::Layout)
1550            .expect_err("cancellation after the scan must beat its pending resource rejection");
1551        assert!(matches!(
1552            first,
1553            OperationWorkError::Cancelled(error)
1554                if error.phase == OperationPhase::Layout
1555                    && error.reason == merman_core::CancelReason::Requested
1556        ));
1557        assert_eq!(
1558            meter
1559                .preflight_parsed_render(&parsed, OperationPhase::Emit)
1560                .expect_err("the cancellation must remain the sticky terminal"),
1561            first
1562        );
1563    }
1564
1565    #[test]
1566    fn operation_svg_meter_checks_shared_control_before_reserving_bytes() {
1567        let control = OperationControl::new();
1568        control.cancel();
1569        let meter = OperationWorkMeter::new_with_control(
1570            RenderResourcePolicy::unbounded_for_trusted_input(),
1571            control,
1572        );
1573
1574        let OperationWorkError::Cancelled(error) = meter.charge_svg_bytes(1).unwrap_err() else {
1575            panic!("expected cancellation to win before SVG accounting");
1576        };
1577        assert_eq!(error.phase, OperationPhase::Emit);
1578        assert_eq!(meter.projected_svg_bytes(), 0);
1579    }
1580
1581    #[test]
1582    fn operation_work_meter_replays_first_resource_terminal_after_cancellation() {
1583        let policy = RenderResourcePolicy::unbounded_for_trusted_input()
1584            .with_limit(ResourceLimitId::MaxLayoutWorkUnits, 10)
1585            .unwrap();
1586        let control = OperationControl::new();
1587        let meter = OperationWorkMeter::new_with_control(policy, control.clone());
1588        meter.charge(8).unwrap();
1589
1590        let first = meter.charge(3).unwrap_err();
1591        let OperationWorkError::ResourceLimitExceeded(error) = &first else {
1592            panic!("expected a resource rejection");
1593        };
1594        assert_eq!(error.cause, ResourceLimitCause::Ceiling);
1595        assert_eq!(error.actual, 11);
1596        assert_eq!(error.max, 10);
1597        assert_eq!(meter.used(), 8);
1598
1599        control.cancel();
1600        assert_eq!(meter.charge(2).unwrap_err(), first);
1601        assert_eq!(meter.used(), 8);
1602    }
1603
1604    #[test]
1605    fn operation_work_meter_replays_the_originating_policy_provenance() {
1606        let originating_policy = RenderResourcePolicy::constrained()
1607            .with_limit(ResourceLimitId::MaxLayoutWorkUnits, 1)
1608            .unwrap()
1609            .with_limit(ResourceLimitId::MaxSvgBytes, 17)
1610            .unwrap();
1611        let observing_policy = RenderResourcePolicy::interactive()
1612            .with_limit(ResourceLimitId::MaxLayoutWorkUnits, 99)
1613            .unwrap();
1614        let control = OperationControl::new();
1615        let originating = OperationWorkMeter::new_with_control(originating_policy, control.clone());
1616        let observing = OperationWorkMeter::new_with_control(observing_policy, control);
1617
1618        let OperationWorkError::ResourceLimitExceeded(first) = originating.charge(2).unwrap_err()
1619        else {
1620            panic!("expected the originating ceiling");
1621        };
1622        let OperationWorkError::ResourceLimitExceeded(replayed) =
1623            observing.checkpoint(OperationPhase::Emit).unwrap_err()
1624        else {
1625            panic!("expected the stored render terminal to retain its typed projection");
1626        };
1627
1628        assert_eq!(replayed, first);
1629        assert_eq!(replayed.profile, RenderResourceProfile::Constrained);
1630        assert_eq!(
1631            replayed.explicit_overrides,
1632            vec![
1633                ResourceLimitOverride {
1634                    id: ResourceLimitId::MaxLayoutWorkUnits,
1635                    value: 1,
1636                },
1637                ResourceLimitOverride {
1638                    id: ResourceLimitId::MaxSvgBytes,
1639                    value: 17,
1640                },
1641            ]
1642        );
1643    }
1644
1645    #[test]
1646    fn operation_work_meter_preserves_a_foreign_ascii_domain() {
1647        let control = OperationControl::new();
1648        let terminal = control.terminate_resource_limit(OperationResourceLimitExceeded {
1649            id: "max_ascii_output_bytes",
1650            phase: OperationPhase::Emit,
1651            resource_phase: "ascii_output",
1652            limit: 7,
1653            consumed: 7,
1654            requested: 1,
1655            provenance: OperationResourceProvenance::new(
1656                OperationResourceDomain::Ascii,
1657                Some(RenderResourceProfile::Constrained),
1658                [OperationResourceOverride {
1659                    id: "max_ascii_output_bytes",
1660                    value: 7,
1661                }],
1662            ),
1663        });
1664        let observing =
1665            OperationWorkMeter::new_with_control(RenderResourcePolicy::interactive(), control);
1666
1667        assert_eq!(
1668            observing
1669                .checkpoint(OperationPhase::Layout)
1670                .expect_err("render must not project an ASCII resource terminal"),
1671            OperationWorkError::ForeignResourceTerminal(terminal)
1672        );
1673    }
1674
1675    #[test]
1676    fn operation_work_meter_overflow_fails_under_unlimited_policy() {
1677        let meter = OperationWorkMeter::new(RenderResourcePolicy::unbounded_for_trusted_input());
1678        meter.charge(usize::MAX).unwrap();
1679
1680        let preflight = meter.preflight(1).unwrap_err();
1681        let OperationWorkError::ResourceLimitExceeded(preflight_error) = &preflight else {
1682            panic!("expected a resource rejection");
1683        };
1684        assert_eq!(
1685            preflight_error.cause,
1686            ResourceLimitCause::ArithmeticOverflow
1687        );
1688        assert_eq!(preflight_error.limit, "max_layout_work_units");
1689        assert_eq!(preflight_error.max, usize::MAX);
1690        assert_eq!(meter.used(), usize::MAX);
1691
1692        assert_eq!(meter.charge(1).unwrap_err(), preflight);
1693        assert_eq!(meter.used(), usize::MAX);
1694    }
1695
1696    #[test]
1697    fn operation_svg_meter_accepts_exact_limit_and_rejects_one_more() {
1698        let policy = RenderResourcePolicy::unbounded_for_trusted_input()
1699            .with_limit(ResourceLimitId::MaxSvgBytes, 10)
1700            .unwrap();
1701        let control = OperationControl::new();
1702        let meter = OperationWorkMeter::new_with_control(policy, control.clone());
1703
1704        meter.charge_svg_bytes(10).unwrap();
1705        assert_eq!(meter.projected_svg_bytes(), 10);
1706        assert_eq!(meter.remaining_svg_bytes(), Some(0));
1707
1708        let first = meter.charge_svg_bytes(1).unwrap_err();
1709        let OperationWorkError::ResourceLimitExceeded(error) = &first else {
1710            panic!("expected a resource rejection");
1711        };
1712        assert_eq!(error.actual, 11);
1713        assert_eq!(error.max, 10);
1714        assert_eq!(meter.projected_svg_bytes(), 10);
1715
1716        control.cancel();
1717        assert_eq!(meter.charge_svg_bytes(1).unwrap_err(), first);
1718        assert_eq!(meter.projected_svg_bytes(), 10);
1719    }
1720
1721    #[test]
1722    fn operation_svg_meter_reconciles_conservative_reservations() {
1723        let policy = RenderResourcePolicy::unbounded_for_trusted_input()
1724            .with_limit(ResourceLimitId::MaxSvgBytes, 12)
1725            .unwrap();
1726        let meter = OperationWorkMeter::new(policy);
1727
1728        meter.charge_svg_bytes(8).unwrap();
1729        meter.reconcile_svg_bytes(8, 5).unwrap();
1730        assert_eq!(meter.projected_svg_bytes(), 5);
1731        assert_eq!(meter.remaining_svg_bytes(), Some(7));
1732
1733        meter.reconcile_svg_bytes(5, 12).unwrap();
1734        assert_eq!(meter.projected_svg_bytes(), 12);
1735        assert_eq!(meter.remaining_svg_bytes(), Some(0));
1736    }
1737
1738    #[test]
1739    fn operation_svg_meter_atomically_reserves_available_growth() {
1740        let policy = RenderResourcePolicy::unbounded_for_trusted_input()
1741            .with_limit(ResourceLimitId::MaxSvgBytes, 10)
1742            .unwrap();
1743        let meter = OperationWorkMeter::new(policy);
1744
1745        meter.charge_svg_bytes(7).unwrap();
1746        let reservation = meter.reserve_svg_bytes_up_to(5).unwrap();
1747        assert_eq!(reservation.additional_bytes, 3);
1748        let error = reservation
1749            .limit_error
1750            .expect("a partial reservation carries the deterministic limit error");
1751        assert_eq!(error.actual, 11);
1752        assert_eq!(error.max, 10);
1753        assert_eq!(meter.projected_svg_bytes(), 10);
1754
1755        meter.reconcile_svg_bytes(3, 1).unwrap();
1756        assert_eq!(meter.projected_svg_bytes(), 8);
1757        assert_eq!(meter.remaining_svg_bytes(), Some(2));
1758
1759        meter.charge_svg_bytes(2).unwrap();
1760        assert_eq!(meter.projected_svg_bytes(), 10);
1761    }
1762
1763    #[test]
1764    fn partial_svg_reservation_preserves_arithmetic_overflow_classification() {
1765        let policy = RenderResourcePolicy::unbounded_for_trusted_input()
1766            .with_limit(ResourceLimitId::MaxSvgBytes, usize::MAX)
1767            .unwrap();
1768        let control = OperationControl::new();
1769        let meter = OperationWorkMeter::new_with_control(policy, control.clone());
1770
1771        meter.charge_svg_bytes(usize::MAX).unwrap();
1772        let reservation = meter.reserve_svg_bytes_up_to(1).unwrap();
1773        assert_eq!(reservation.additional_bytes, 0);
1774        let error = reservation
1775            .limit_error
1776            .expect("an overflowing partial reservation must retain its error");
1777        assert_eq!(error.cause, ResourceLimitCause::ArithmeticOverflow);
1778        assert_eq!(error.actual, usize::MAX);
1779        assert_eq!(error.max, usize::MAX);
1780        assert_eq!(meter.projected_svg_bytes(), usize::MAX);
1781
1782        let first = meter.terminate_absolute_resource_error(error, OperationPhase::Emit);
1783        control.cancel();
1784        assert_eq!(
1785            meter
1786                .charge(1)
1787                .expect_err("the finalized overflow must remain the sticky terminal"),
1788            first
1789        );
1790    }
1791
1792    #[test]
1793    fn zenuml_complexity_includes_inline_decorations() {
1794        let parsed = Engine::new()
1795            .parse_diagram_for_render_model_sync(
1796                "zenuml\nA->[rocket]B.call()\n",
1797                ParseOptions::strict(),
1798            )
1799            .unwrap()
1800            .unwrap();
1801        let RenderSemanticModel::Zenuml(model) = parsed.model() else {
1802            panic!("expected ZenUML model");
1803        };
1804        let complexity = ZenumlComplexity::from_model(model);
1805
1806        assert_eq!(complexity.participants, 2);
1807        assert_eq!(complexity.statements, 1);
1808        let required = ["rocket", "call()"]
1809            .into_iter()
1810            .map(str::len)
1811            .sum::<usize>();
1812        assert!(complexity.label_bytes >= required);
1813    }
1814
1815    #[test]
1816    fn zenuml_uses_the_shared_model_budget() {
1817        let parsed = Engine::new()
1818            .parse_diagram_for_render_model_sync(
1819                "zenuml\nA.call() {\n  if(ok) {\n    if(inner) {\n      B.work()\n    }\n  }\n}\n",
1820                ParseOptions::strict(),
1821            )
1822            .unwrap()
1823            .unwrap();
1824        let RenderSemanticModel::Zenuml(model) = parsed.model() else {
1825            panic!("expected ZenUML model");
1826        };
1827
1828        let limits = RenderResourcePolicy::unbounded_for_trusted_input()
1829            .with_limit(ResourceLimitId::MaxModelItems, 1)
1830            .unwrap();
1831        let error = limits.check_zenuml_complexity(model).unwrap_err();
1832        assert_eq!(error.phase, ResourceLimitPhase::LayoutModel);
1833        assert_eq!(error.limit, "max_model_items");
1834    }
1835
1836    #[test]
1837    fn flowchart_complexity_counts_layout_nodes_and_labels() {
1838        let model = FlowchartModel {
1839            keyword: "graph".to_string(),
1840            acc_descr: None,
1841            acc_title: None,
1842            class_defs: Default::default(),
1843            direction: None,
1844            edge_defaults: None,
1845            vertex_calls: Vec::new(),
1846            nodes: vec![FlowNode {
1847                id: "A".to_string(),
1848                provenance: Default::default(),
1849                label: Some("Alpha".to_string()),
1850                label_type: None,
1851                layout_shape: None,
1852                shape: None,
1853                icon: None,
1854                form: None,
1855                pos: None,
1856                img: None,
1857                constraint: None,
1858                asset_width: None,
1859                asset_height: None,
1860                classes: Vec::new(),
1861                styles: Vec::new(),
1862                link: None,
1863                link_target: None,
1864                have_callback: false,
1865            }],
1866            edges: vec![FlowEdge {
1867                id: "L-A-B".to_string(),
1868                from: "A".to_string(),
1869                to: "B".to_string(),
1870                label: Some("edge".to_string()),
1871                label_type: None,
1872                edge_type: None,
1873                arrow: "-->".to_string(),
1874                start_marker: FlowEdgeMarker::None,
1875                end_marker: FlowEdgeMarker::Point,
1876                is_user_defined_id: false,
1877                stroke: None,
1878                stroke_kind: FlowEdgeStroke::Normal,
1879                visibility: FlowEdgeVisibility::Visible,
1880                interpolate: None,
1881                classes: Vec::new(),
1882                style: Vec::new(),
1883                animate: None,
1884                animation: None,
1885                length: 1,
1886            }],
1887            subgraphs: vec![FlowSubgraph {
1888                metadata: None,
1889                id: "cluster".to_string(),
1890                title: "Cluster".to_string(),
1891                dir: None,
1892                has_explicit_dir: false,
1893                label_type: None,
1894                classes: Vec::new(),
1895                styles: Vec::new(),
1896                nodes: vec!["A".to_string()],
1897            }],
1898            tooltips: Default::default(),
1899            warning_facts: Vec::new(),
1900        };
1901
1902        let complexity = FlowchartComplexity::from_model(&model);
1903        assert_eq!(complexity.nodes, 2);
1904        assert_eq!(complexity.edges, 1);
1905        assert_eq!(complexity.subgraphs, 1);
1906        assert!(complexity.label_bytes >= "AlphaedgeCluster".len());
1907    }
1908}
1909
1910#[cfg(feature = "diagram-class")]
1911pub use merman_core::resources::ClassComplexity;
1912
1913#[cfg(any(
1914    feature = "diagram-flowchart",
1915    feature = "diagram-swimlane",
1916    feature = "diagram-agentflow"
1917))]
1918pub use merman_core::resources::FlowchartComplexity;
1919
1920#[cfg(feature = "diagram-mindmap")]
1921pub use merman_core::resources::MindmapComplexity;
1922
1923#[cfg(feature = "diagram-sequence")]
1924pub use merman_core::resources::SequenceComplexity;
1925
1926#[cfg(feature = "diagram-zenuml")]
1927pub use merman_core::resources::ZenumlComplexity;