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