Skip to main content

wasm4pm_compat/
petri.rs

1//! Petri net, WF-net, and OC-Petri-net **shapes** — with soundness as a
2//! *typestate claim*, never a computed proof.
3//!
4//! This module models the place/transition canon: a [`crate::petri::PetriNet`] of [`crate::petri::Place`]s
5//! and [`crate::petri::Transition`]s joined by [`crate::petri::Arc`]s with a [`crate::petri::Marking`]; a [`crate::petri::WfNet`]
6//! (workflow net) specialization with a single source and sink and a soundness
7//! *claim* tracked at the type level; and an [`crate::petri::ObjectCentricPetriNet`] whose
8//! arcs are typed by object type and may be variable.
9//!
10//! ## Soundness is a *claim*, not a result
11//!
12//! WF-net **soundness** (option to complete, proper completion, no dead
13//! transitions) is a non-trivial property. This crate **does not compute it** —
14//! computing soundness is an engine and graduates to `wasm4pm`. Instead, a
15//! [`crate::petri::WfNet`] carries a *typestate token* recording at the type level whether
16//! soundness is [`crate::petri::SoundnessUnknown`] (default), merely [`crate::petri::SoundnessClaimed`]
17//! (asserted by a human/upstream, unproven here), or [`crate::petri::SoundnessWitnessed`]
18//! (carrying a witness obtained from `wasm4pm` and re-attached here).
19//!
20//! These three tokens are **empty enums** (uninhabited markers) used only as
21//! `PhantomData` type parameters — zero-cost, never constructed.
22//!
23//! ## Structure only
24//!
25//! [`crate::petri::PetriNet::validate`] and [`crate::petri::WfNet::validate`] check *structural* shape
26//! (arcs reference declared nodes; a WF-net has an initial and final marking).
27//! They never check reachability, boundedness, liveness, or soundness — those
28//! are `wasm4pm`.
29//!
30//! ## Graduation to `wasm4pm`
31//!
32//! Soundness witnessing, boundedness/safeness analysis, reachability, and
33//! token-game replay graduate to `wasm4pm`. A [`crate::petri::SoundnessWitnessed`] WF-net is
34//! the shape into which such a witness is re-attached for evidence-carrying
35//! interchange.
36//!
37//! [`SoundnessUnknown`]: crate::petri::SoundnessUnknown
38//! [`SoundnessClaimed`]: crate::petri::SoundnessClaimed
39//! [`SoundnessWitnessed`]: crate::petri::SoundnessWitnessed
40
41use core::marker::PhantomData;
42
43use crate::law::SoundnessState;
44
45// ── Place / Transition node-kind markers ─────────────────────────────────────
46
47/// A zero-sized type-level marker asserting that a type parameter represents a
48/// **place** node in a Petri net.
49///
50/// `PlaceNodeMarker` and [`TransitionNodeMarker`] are the runtime-facing
51/// counterparts of the [`crate::law::EndpointKind`] const-param enum. Use them
52/// as type parameters when you want the compiler to enforce that a slot is
53/// filled by a place kind rather than a transition kind.
54///
55/// Structure-only: carries no data, no token semantics.
56///
57/// ```
58/// use wasm4pm_compat::petri::PlaceNodeMarker;
59/// fn place_slot<P: wasm4pm_compat::petri::IsPlaceNode>() {}
60/// place_slot::<PlaceNodeMarker>();
61/// ```
62#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
63pub struct PlaceNodeMarker;
64
65/// A zero-sized type-level marker asserting that a type parameter represents a
66/// **transition** node in a Petri net.
67///
68/// Paired with [`PlaceNodeMarker`]; see its documentation.
69///
70/// ```
71/// use wasm4pm_compat::petri::TransitionNodeMarker;
72/// fn transition_slot<T: wasm4pm_compat::petri::IsTransitionNode>() {}
73/// transition_slot::<TransitionNodeMarker>();
74/// ```
75#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
76pub struct TransitionNodeMarker;
77
78mod node_marker_seal {
79    pub trait PlaceSeal {}
80    pub trait TransitionSeal {}
81    impl PlaceSeal for super::PlaceNodeMarker {}
82    impl TransitionSeal for super::TransitionNodeMarker {}
83}
84
85/// Sealed trait — only [`PlaceNodeMarker`] is a place node kind.
86///
87/// ```
88/// use wasm4pm_compat::petri::{PlaceNodeMarker, IsPlaceNode};
89/// fn needs_place<P: IsPlaceNode>(_: P) {}
90/// needs_place(PlaceNodeMarker);
91/// ```
92pub trait IsPlaceNode: node_marker_seal::PlaceSeal {}
93impl IsPlaceNode for PlaceNodeMarker {}
94
95/// Sealed trait — only [`TransitionNodeMarker`] is a transition node kind.
96///
97/// ```
98/// use wasm4pm_compat::petri::{TransitionNodeMarker, IsTransitionNode};
99/// fn needs_transition<T: IsTransitionNode>(_: T) {}
100/// needs_transition(TransitionNodeMarker);
101/// ```
102pub trait IsTransitionNode: node_marker_seal::TransitionSeal {}
103impl IsTransitionNode for TransitionNodeMarker {}
104
105// ── Bipartite arc type law ───────────────────────────────────────────────────
106
107/// A typed arc from a **place** to a **transition** — the only valid
108/// pre-incidence direction in a Petri net.
109///
110/// Paper: Murata (1989) §2 — F ⊆ (P×T) ∪ (T×P); arcs must be bipartite.
111/// There is **no** `PlaceToPlaceArc` or `TransitionToTransitionArc` type in
112/// this crate; they are unconstructible.
113///
114/// The generic kinds `P` and `T` are independent namespace markers; a place
115/// kind and a transition kind can be the same Rust type without breaking the
116/// law (the arc direction is carried by the struct type, not by the type params).
117///
118/// Structure-only: a typed directed edge. No token flow.
119///
120/// ```
121/// use wasm4pm_compat::petri::PlaceToTransitionArc;
122/// struct P1; struct T1;
123/// let arc = PlaceToTransitionArc::<P1, T1, u8>::new(0u8);
124/// assert_eq!(arc.weight(), 0);
125/// ```
126pub struct PlaceToTransitionArc<P, T, Weight> {
127    pub(crate) _from: PhantomData<P>,
128    pub(crate) _to: PhantomData<T>,
129    /// Arc weight (pre-incidence W⁻).
130    pub weight: Weight,
131}
132
133impl<P, T, Weight: Copy> PlaceToTransitionArc<P, T, Weight> {
134    /// Constructs a place→transition arc with the given weight.
135    ///
136    /// ```
137    /// use wasm4pm_compat::petri::PlaceToTransitionArc;
138    /// struct P1; struct T1;
139    /// let arc = PlaceToTransitionArc::<P1, T1, u32>::new(1u32);
140    /// assert_eq!(arc.weight(), 1u32);
141    /// ```
142    pub fn new(weight: Weight) -> Self {
143        PlaceToTransitionArc {
144            _from: PhantomData,
145            _to: PhantomData,
146            weight,
147        }
148    }
149
150    /// The arc weight.
151    ///
152    /// ```
153    /// use wasm4pm_compat::petri::PlaceToTransitionArc;
154    /// struct P1; struct T1;
155    /// assert_eq!(PlaceToTransitionArc::<P1, T1, u8>::new(3u8).weight(), 3);
156    /// ```
157    pub fn weight(&self) -> Weight {
158        self.weight
159    }
160}
161
162/// A typed arc from a **transition** to a **place** — the only valid
163/// post-incidence direction in a Petri net.
164///
165/// Paper: Murata (1989) §2 — bipartite arc law. Structure-only.
166///
167/// ```
168/// use wasm4pm_compat::petri::TransitionToPlaceArc;
169/// struct T1; struct P1;
170/// let arc = TransitionToPlaceArc::<T1, P1, u8>::new(1u8);
171/// assert_eq!(arc.weight(), 1);
172/// ```
173pub struct TransitionToPlaceArc<T, P, Weight> {
174    pub(crate) _from: PhantomData<T>,
175    pub(crate) _to: PhantomData<P>,
176    /// Arc weight (post-incidence W⁺).
177    pub weight: Weight,
178}
179
180impl<T, P, Weight: Copy> TransitionToPlaceArc<T, P, Weight> {
181    /// Constructs a transition→place arc with the given weight.
182    ///
183    /// ```
184    /// use wasm4pm_compat::petri::TransitionToPlaceArc;
185    /// struct T1; struct P1;
186    /// let arc = TransitionToPlaceArc::<T1, P1, u32>::new(1u32);
187    /// assert_eq!(arc.weight(), 1u32);
188    /// ```
189    pub fn new(weight: Weight) -> Self {
190        TransitionToPlaceArc {
191            _from: PhantomData,
192            _to: PhantomData,
193            weight,
194        }
195    }
196
197    /// The arc weight.
198    ///
199    /// ```
200    /// use wasm4pm_compat::petri::TransitionToPlaceArc;
201    /// struct T1; struct P1;
202    /// assert_eq!(TransitionToPlaceArc::<T1, P1, u8>::new(2u8).weight(), 2);
203    /// ```
204    pub fn weight(&self) -> Weight {
205        self.weight
206    }
207}
208
209// ── BipartiteArcConst: const-generic bipartite arc law ───────────────────────
210
211/// A bipartite arc with direction encoded as a const-generic [`crate::law::ArcDirectionConst`]
212/// parameter.
213///
214/// `BipartiteArcConst<{ArcDirectionConst::PlaceToTransition}, Weight>` and
215/// `BipartiteArcConst<{ArcDirectionConst::TransitionToPlace}, Weight>` are
216/// **distinct types** at compile time — a slot requiring a pre-incidence arc
217/// (`PlaceToTransition`) rejects a post-incidence arc (`TransitionToPlace`) with
218/// a type error.
219///
220/// Paper: Murata (1989) §2 — `F ⊆ (P×T) ∪ (T×P)`.
221///
222/// ## Difference from [`PlaceToTransitionArc`] / [`TransitionToPlaceArc`]
223///
224/// The named-type arcs use *struct shape* to enforce direction (no common base
225/// type). `BipartiteArcConst` provides a **single type** parameterised over
226/// direction — useful for generic containers that hold arcs of either direction
227/// while still encoding direction in the type.
228///
229/// Structure-only: a typed directed edge. No token flow.
230///
231/// ```
232/// use wasm4pm_compat::petri::BipartiteArcConst;
233/// use wasm4pm_compat::law::ArcDirectionConst;
234///
235/// let pre = BipartiteArcConst::<{ ArcDirectionConst::PlaceToTransition }, u8>::new("p0", "t0", 1);
236/// assert_eq!(pre.place_id(), "p0");
237/// assert_eq!(pre.weight(), 1u8);
238/// ```
239pub struct BipartiteArcConst<const DIR: crate::law::ArcDirectionConst, Weight> {
240    place_id: alloc::string::String,
241    transition_id: alloc::string::String,
242    /// Arc weight (multiplicity).
243    pub weight: Weight,
244}
245
246extern crate alloc;
247
248impl<const DIR: crate::law::ArcDirectionConst, Weight: Copy> BipartiteArcConst<DIR, Weight> {
249    /// Construct a bipartite arc with the given endpoints and weight.
250    ///
251    /// ```
252    /// use wasm4pm_compat::petri::BipartiteArcConst;
253    /// use wasm4pm_compat::law::ArcDirectionConst;
254    /// let post = BipartiteArcConst::<{ ArcDirectionConst::TransitionToPlace }, u32>::new("p1", "t0", 2);
255    /// assert_eq!(post.transition_id(), "t0");
256    /// assert_eq!(post.place_id(), "p1");
257    /// ```
258    pub fn new(
259        place_id: impl Into<alloc::string::String>,
260        transition_id: impl Into<alloc::string::String>,
261        weight: Weight,
262    ) -> Self {
263        BipartiteArcConst {
264            place_id: place_id.into(),
265            transition_id: transition_id.into(),
266            weight,
267        }
268    }
269
270    /// The place endpoint id.
271    ///
272    /// ```
273    /// use wasm4pm_compat::petri::BipartiteArcConst;
274    /// use wasm4pm_compat::law::ArcDirectionConst;
275    /// let a = BipartiteArcConst::<{ ArcDirectionConst::PlaceToTransition }, u8>::new("p0", "t0", 1);
276    /// assert_eq!(a.place_id(), "p0");
277    /// ```
278    pub fn place_id(&self) -> &str {
279        &self.place_id
280    }
281
282    /// The transition endpoint id.
283    ///
284    /// ```
285    /// use wasm4pm_compat::petri::BipartiteArcConst;
286    /// use wasm4pm_compat::law::ArcDirectionConst;
287    /// let a = BipartiteArcConst::<{ ArcDirectionConst::PlaceToTransition }, u8>::new("p0", "t0", 1);
288    /// assert_eq!(a.transition_id(), "t0");
289    /// ```
290    pub fn transition_id(&self) -> &str {
291        &self.transition_id
292    }
293
294    /// The arc weight.
295    ///
296    /// ```
297    /// use wasm4pm_compat::petri::BipartiteArcConst;
298    /// use wasm4pm_compat::law::ArcDirectionConst;
299    /// let a = BipartiteArcConst::<{ ArcDirectionConst::PlaceToTransition }, u8>::new("p0", "t0", 3);
300    /// assert_eq!(a.weight(), 3u8);
301    /// ```
302    pub fn weight(&self) -> Weight {
303        self.weight
304    }
305
306    /// The arc direction encoded in the const parameter.
307    ///
308    /// ```
309    /// use wasm4pm_compat::petri::BipartiteArcConst;
310    /// use wasm4pm_compat::law::ArcDirectionConst;
311    /// let a = BipartiteArcConst::<{ ArcDirectionConst::TransitionToPlace }, u8>::new("t0", "p0", 1);
312    /// assert_eq!(a.direction(), ArcDirectionConst::TransitionToPlace);
313    /// ```
314    pub const fn direction(&self) -> crate::law::ArcDirectionConst {
315        DIR
316    }
317}
318
319// ── IsValidArc sealed trait ──────────────────────────────────────────────────
320
321mod arc_seal {
322    pub trait Sealed {}
323    impl<P, T, W> Sealed for super::PlaceToTransitionArc<P, T, W> {}
324    impl<T, P, W> Sealed for super::TransitionToPlaceArc<T, P, W> {}
325}
326
327/// Sealed marker: only [`PlaceToTransitionArc`] and [`TransitionToPlaceArc`]
328/// are valid arcs in a Petri net. No user type can implement this trait.
329///
330/// ```
331/// use wasm4pm_compat::petri::{PlaceToTransitionArc, IsValidArc};
332/// struct P1; struct T1;
333/// fn accept<A: IsValidArc>(_arc: A) {}
334/// accept(PlaceToTransitionArc::<P1, T1, u8>::new(1u8));
335/// ```
336pub trait IsValidArc: arc_seal::Sealed {}
337impl<P, T, W> IsValidArc for PlaceToTransitionArc<P, T, W> {}
338impl<T, P, W> IsValidArc for TransitionToPlaceArc<T, P, W> {}
339
340// ── Non-forgeable WF-net soundness (const-generic + private seal) ────────────
341
342mod wfnet_seal {
343    /// Private seal — only constructible inside `petri`. Prevents external
344    /// users from building `WfNetConst<{SoundnessState::Witnessed}>` directly.
345    pub(super) struct WfNetSeal;
346}
347
348/// A WF-net with soundness state encoded as a const generic parameter.
349///
350/// The `SOUNDNESS` parameter tracks whether soundness is unknown, claimed, or
351/// witnessed. Crucially, `WfNetConst<{SoundnessState::Witnessed}>` is **only**
352/// constructible via [`WfNetConst::witness_soundness`], which requires a
353/// [`SoundnessProof`] — a type that is itself only constructible inside this
354/// module or via the `wasm4pm` graduation bridge.
355///
356/// Direct struct-literal construction fails because `_seal` is a private field.
357///
358/// ```
359/// use wasm4pm_compat::petri::{WfNetConst, SoundnessProof};
360/// use wasm4pm_compat::law::SoundnessState;
361///
362/// // Build an unknown-soundness net, then claim it:
363/// let unknown = WfNetConst::<{ SoundnessState::Unknown }>::new();
364/// let claimed = unknown.claim_sound();
365/// // To reach Witnessed, you would call claimed.witness_soundness(proof)
366/// // where proof is only producible by the wasm4pm engine or this module.
367/// ```
368///
369/// ```compile_fail
370/// use wasm4pm_compat::petri::WfNetConst;
371/// use wasm4pm_compat::law::SoundnessState;
372/// // ERROR: _seal is a private field; direct forged construction is impossible.
373/// let forged: WfNetConst<{ SoundnessState::Witnessed }> = WfNetConst {
374///     _seal: todo!(),
375/// };
376/// ```
377#[doc(alias = "workflow net")]
378#[doc(alias = "WF-net")]
379pub struct WfNetConst<const SOUNDNESS: SoundnessState> {
380    // Private seal prevents direct struct-literal construction of any
381    // WfNetConst variant from outside this module.
382    _seal: wfnet_seal::WfNetSeal,
383}
384
385/// A proof token for `SoundnessState::Witnessed` — only constructible inside
386/// `petri` (or via the `wasm4pm` graduation bridge).
387///
388/// Callers outside this module cannot construct `SoundnessProof` because the
389/// inner `wfnet_seal::WfNetSeal` type is private.
390pub struct SoundnessProof(wfnet_seal::WfNetSeal);
391
392impl SoundnessProof {
393    /// Module-private constructor — only `petri` and the `wasm4pm` bridge can
394    /// produce a proof.
395    #[allow(dead_code)]
396    pub(crate) fn new() -> Self {
397        SoundnessProof(wfnet_seal::WfNetSeal)
398    }
399}
400
401/// A typed soundness-proof carrier that records *which* WF-net shape the
402/// proof was issued for, via a phantom type parameter `N`.
403///
404/// [`SoundnessProof`] is a bare token — it records that *some* net's soundness
405/// was witnessed but not *which* net. `WfNetSoundnessProofOf<N>` adds a phantom
406/// `N` so that a proof issued for one net type cannot be silently transplanted
407/// onto a different net type.
408///
409/// The phantom `N` is structural metadata only — `WfNetSoundnessProofOf` is
410/// still only constructible inside this module or via the `wasm4pm` bridge
411/// (the `_seal` field is private).
412///
413/// Structure-only: zero-cost phantom wrapper around the soundness-proof seal.
414///
415/// ```
416/// use core::marker::PhantomData;
417/// use wasm4pm_compat::petri::WfNetSoundnessProofOf;
418///
419/// // A user-defined net type marker:
420/// struct OrderFulfillmentNet;
421/// // The proof is typed to that specific net; cannot be used for other nets.
422/// let _: core::marker::PhantomData<WfNetSoundnessProofOf<OrderFulfillmentNet>>;
423/// ```
424pub struct WfNetSoundnessProofOf<N> {
425    _net: PhantomData<N>,
426    _seal: wfnet_seal::WfNetSeal,
427}
428
429impl<N> WfNetSoundnessProofOf<N> {
430    /// Module-private constructor. Only `petri` and the `wasm4pm` bridge can
431    /// produce a typed soundness proof.
432    #[allow(dead_code)]
433    pub(crate) fn new() -> Self {
434        WfNetSoundnessProofOf {
435            _net: PhantomData,
436            _seal: wfnet_seal::WfNetSeal,
437        }
438    }
439}
440
441impl Default for WfNetConst<{ SoundnessState::Unknown }> {
442    fn default() -> Self {
443        Self::new()
444    }
445}
446
447impl WfNetConst<{ SoundnessState::Unknown }> {
448    /// Construct a `WfNetConst` in the initial `Unknown` soundness state.
449    ///
450    /// ```
451    /// use wasm4pm_compat::petri::WfNetConst;
452    /// use wasm4pm_compat::law::SoundnessState;
453    /// let _wf = WfNetConst::<{ SoundnessState::Unknown }>::new();
454    /// ```
455    pub fn new() -> Self {
456        WfNetConst {
457            _seal: wfnet_seal::WfNetSeal,
458        }
459    }
460
461    /// Advance to `Claimed` soundness — a type-level re-tagging only.
462    ///
463    /// ```
464    /// use wasm4pm_compat::petri::WfNetConst;
465    /// use wasm4pm_compat::law::SoundnessState;
466    /// let claimed: WfNetConst<{ SoundnessState::Claimed }> =
467    ///     WfNetConst::<{ SoundnessState::Unknown }>::new().claim_sound();
468    /// ```
469    pub fn claim_sound(self) -> WfNetConst<{ SoundnessState::Claimed }> {
470        WfNetConst { _seal: self._seal }
471    }
472}
473
474impl WfNetConst<{ SoundnessState::Claimed }> {
475    /// Advance a *claimed* WF-net to `Witnessed` — requires a [`SoundnessProof`].
476    ///
477    /// The `SoundnessProof` is only constructible inside this module or by the
478    /// `wasm4pm` graduation bridge. This is the sanctioned, non-forgeable path
479    /// to `SoundnessState::Witnessed`.
480    ///
481    /// ```ignore
482    /// // Conceptual: the wasm4pm bridge supplies the proof after verifying soundness.
483    /// let witnessed = claimed.witness_soundness(proof_from_wasm4pm);
484    /// ```
485    pub fn witness_soundness(
486        self,
487        _proof: SoundnessProof,
488    ) -> WfNetConst<{ SoundnessState::Witnessed }> {
489        WfNetConst {
490            _seal: wfnet_seal::WfNetSeal,
491        }
492    }
493}
494
495/// A shared query surface for [`WfNetConst`] that is independent of soundness state.
496///
497/// `WfNetConst<S>` has three concrete impls (one per `SoundnessState` variant).
498/// This blanket impl adds a `soundness_state()` query so callers that receive a
499/// `WfNetConst<S>` in generic context can read the soundness state as a runtime
500/// value without knowing `S` statically.
501///
502/// The method is separately documented from the struct because the docs are on
503/// the trait, not on the three concrete impls.
504///
505/// ```
506/// use wasm4pm_compat::petri::{WfNetConst, WfNetQuery};
507/// use wasm4pm_compat::law::SoundnessState;
508/// let wf = WfNetConst::<{ SoundnessState::Unknown }>::new().claim_sound();
509/// assert_eq!(wf.soundness_state(), SoundnessState::Claimed);
510/// ```
511pub trait WfNetQuery {
512    /// The soundness state of this WF-net as a runtime value.
513    fn soundness_state(&self) -> SoundnessState;
514}
515
516impl WfNetQuery for WfNetConst<{ SoundnessState::Unknown }> {
517    fn soundness_state(&self) -> SoundnessState {
518        SoundnessState::Unknown
519    }
520}
521impl WfNetQuery for WfNetConst<{ SoundnessState::Claimed }> {
522    fn soundness_state(&self) -> SoundnessState {
523        SoundnessState::Claimed
524    }
525}
526impl WfNetQuery for WfNetConst<{ SoundnessState::Witnessed }> {
527    fn soundness_state(&self) -> SoundnessState {
528        SoundnessState::Witnessed
529    }
530}
531
532/// A place: a named token-holding location in a Petri net.
533///
534/// Structure-only: identity and name; no token dynamics.
535#[derive(Clone, Debug, PartialEq, Eq)]
536pub struct Place {
537    id: String,
538}
539
540impl Place {
541    /// Construct a place with an id.
542    ///
543    /// ```
544    /// use wasm4pm_compat::petri::Place;
545    /// assert_eq!(Place::new("p0").id(), "p0");
546    /// ```
547    pub fn new(id: impl Into<String>) -> Self {
548        Place { id: id.into() }
549    }
550
551    /// The place id.
552    ///
553    /// ```
554    /// use wasm4pm_compat::petri::Place;
555    /// assert_eq!(Place::new("p0").id(), "p0");
556    /// ```
557    pub fn id(&self) -> &str {
558        &self.id
559    }
560}
561
562/// A transition: a named firing element. An empty `label` denotes a *silent*
563/// (tau) transition.
564///
565/// Structure-only: identity and label; no firing semantics.
566#[derive(Clone, Debug, PartialEq, Eq)]
567pub struct Transition {
568    id: String,
569    label: String,
570}
571
572impl Transition {
573    /// Construct a labeled transition.
574    ///
575    /// ```
576    /// use wasm4pm_compat::petri::Transition;
577    /// let t = Transition::new("t0", "approve");
578    /// assert_eq!(t.id(), "t0");
579    /// assert!(!t.is_silent());
580    /// ```
581    pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
582        Transition {
583            id: id.into(),
584            label: label.into(),
585        }
586    }
587
588    /// Construct a silent (tau) transition with an empty label.
589    ///
590    /// ```
591    /// use wasm4pm_compat::petri::Transition;
592    /// assert!(Transition::silent("t0").is_silent());
593    /// ```
594    pub fn silent(id: impl Into<String>) -> Self {
595        Transition::new(id, "")
596    }
597
598    /// The transition id.
599    ///
600    /// ```
601    /// use wasm4pm_compat::petri::Transition;
602    /// assert_eq!(Transition::new("t0", "a").id(), "t0");
603    /// ```
604    pub fn id(&self) -> &str {
605        &self.id
606    }
607
608    /// The transition label (empty means silent).
609    ///
610    /// ```
611    /// use wasm4pm_compat::petri::Transition;
612    /// assert_eq!(Transition::new("t0", "a").label(), "a");
613    /// ```
614    pub fn label(&self) -> &str {
615        &self.label
616    }
617
618    /// Whether the transition is silent (tau).
619    ///
620    /// ```
621    /// use wasm4pm_compat::petri::Transition;
622    /// assert!(Transition::silent("t0").is_silent());
623    /// ```
624    pub fn is_silent(&self) -> bool {
625        self.label.is_empty()
626    }
627}
628
629/// The direction of a Petri-net [`Arc`] relative to a transition.
630#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
631pub enum ArcDirection {
632    /// Place → Transition (an input/consume arc).
633    PlaceToTransition,
634    /// Transition → Place (an output/produce arc).
635    TransitionToPlace,
636}
637
638/// An arc: a directed, weighted connection between a place and a transition.
639///
640/// `weight` is the arc multiplicity (default 1). An arc whose endpoints are not
641/// declared is refused as [`PetriRefusal::InvalidVariableArc`] (the closest
642/// structural law in this shape's vocabulary) or, for WF-nets, a soundness
643/// concern at `wasm4pm`.
644///
645/// Structure-only: a typed graph edge, no token flow.
646#[derive(Clone, Debug, PartialEq, Eq)]
647pub struct Arc {
648    place_id: String,
649    transition_id: String,
650    direction: ArcDirection,
651    weight: u32,
652    /// For OC-Petri-nets: the object type this arc carries, if any.
653    object_type: Option<String>,
654    /// For OC-Petri-nets: whether the arc is a *variable* arc (multiple tokens
655    /// of the object type).
656    variable: bool,
657}
658
659impl Arc {
660    /// Construct a place→transition arc with weight 1.
661    ///
662    /// ```
663    /// use wasm4pm_compat::petri::{Arc, ArcDirection};
664    /// let a = Arc::place_to_transition("p", "t");
665    /// assert_eq!(a.direction(), ArcDirection::PlaceToTransition);
666    /// assert_eq!(a.weight(), 1);
667    /// ```
668    pub fn place_to_transition(
669        place_id: impl Into<String>,
670        transition_id: impl Into<String>,
671    ) -> Self {
672        Arc {
673            place_id: place_id.into(),
674            transition_id: transition_id.into(),
675            direction: ArcDirection::PlaceToTransition,
676            weight: 1,
677            object_type: None,
678            variable: false,
679        }
680    }
681
682    /// Construct a transition→place arc with weight 1.
683    ///
684    /// ```
685    /// use wasm4pm_compat::petri::{Arc, ArcDirection};
686    /// let a = Arc::transition_to_place("t", "p");
687    /// assert_eq!(a.direction(), ArcDirection::TransitionToPlace);
688    /// ```
689    pub fn transition_to_place(
690        transition_id: impl Into<String>,
691        place_id: impl Into<String>,
692    ) -> Self {
693        Arc {
694            place_id: place_id.into(),
695            transition_id: transition_id.into(),
696            direction: ArcDirection::TransitionToPlace,
697            weight: 1,
698            object_type: None,
699            variable: false,
700        }
701    }
702
703    /// Set the arc weight (multiplicity). Builder-style.
704    ///
705    /// ```
706    /// use wasm4pm_compat::petri::Arc;
707    /// assert_eq!(Arc::place_to_transition("p", "t").with_weight(3).weight(), 3);
708    /// ```
709    pub fn with_weight(mut self, weight: u32) -> Self {
710        self.weight = weight;
711        self
712    }
713
714    /// Type the arc by object type, marking it variable or not (OC-Petri-net).
715    /// Builder-style.
716    ///
717    /// ```
718    /// use wasm4pm_compat::petri::Arc;
719    /// let a = Arc::place_to_transition("p", "t").typed("order", true);
720    /// assert_eq!(a.object_type(), Some("order"));
721    /// assert!(a.is_variable());
722    /// ```
723    pub fn typed(mut self, object_type: impl Into<String>, variable: bool) -> Self {
724        self.object_type = Some(object_type.into());
725        self.variable = variable;
726        self
727    }
728
729    /// The place endpoint id.
730    pub fn place_id(&self) -> &str {
731        &self.place_id
732    }
733
734    /// The transition endpoint id.
735    pub fn transition_id(&self) -> &str {
736        &self.transition_id
737    }
738
739    /// The arc direction.
740    ///
741    /// ```
742    /// use wasm4pm_compat::petri::{Arc, ArcDirection};
743    /// assert_eq!(Arc::place_to_transition("p", "t").direction(), ArcDirection::PlaceToTransition);
744    /// ```
745    pub fn direction(&self) -> ArcDirection {
746        self.direction
747    }
748
749    /// The arc weight (multiplicity).
750    ///
751    /// ```
752    /// use wasm4pm_compat::petri::Arc;
753    /// assert_eq!(Arc::place_to_transition("p", "t").weight(), 1);
754    /// ```
755    pub fn weight(&self) -> u32 {
756        self.weight
757    }
758
759    /// The OC-Petri-net object type carried by this arc, if any.
760    ///
761    /// ```
762    /// use wasm4pm_compat::petri::Arc;
763    /// assert_eq!(Arc::place_to_transition("p", "t").object_type(), None);
764    /// ```
765    #[must_use]
766    pub fn object_type(&self) -> Option<&str> {
767        self.object_type.as_deref()
768    }
769
770    /// Whether this is a variable arc (OC-Petri-net).
771    ///
772    /// ```
773    /// use wasm4pm_compat::petri::Arc;
774    /// assert!(!Arc::place_to_transition("p", "t").is_variable());
775    /// ```
776    pub fn is_variable(&self) -> bool {
777        self.variable
778    }
779}
780
781/// A marking: how many tokens sit on each place.
782///
783/// Structure-only: a snapshot of token counts by place id. This crate never
784/// fires a transition to move from one marking to another.
785#[derive(Clone, Debug, Default, PartialEq, Eq)]
786pub struct Marking {
787    tokens: Vec<(String, u32)>,
788}
789
790impl Marking {
791    /// Construct an empty marking.
792    ///
793    /// ```
794    /// use wasm4pm_compat::petri::Marking;
795    /// assert!(Marking::empty().is_empty());
796    /// ```
797    pub fn empty() -> Self {
798        Marking::default()
799    }
800
801    /// Construct a marking from `(place_id, token_count)` pairs.
802    ///
803    /// ```
804    /// use wasm4pm_compat::petri::Marking;
805    /// let m = Marking::new([("p0".to_string(), 1)]);
806    /// assert_eq!(m.tokens_on("p0"), 1);
807    /// ```
808    pub fn new(tokens: impl IntoIterator<Item = (String, u32)>) -> Self {
809        Marking {
810            tokens: tokens.into_iter().collect(),
811        }
812    }
813
814    /// The token count on a place (0 if unmarked).
815    ///
816    /// ```
817    /// use wasm4pm_compat::petri::Marking;
818    /// assert_eq!(Marking::empty().tokens_on("p0"), 0);
819    /// ```
820    pub fn tokens_on(&self, place_id: &str) -> u32 {
821        self.tokens
822            .iter()
823            .find(|(p, _)| p == place_id)
824            .map(|(_, n)| *n)
825            .unwrap_or(0)
826    }
827
828    /// Whether the marking places no tokens anywhere.
829    ///
830    /// ```
831    /// use wasm4pm_compat::petri::Marking;
832    /// assert!(Marking::empty().is_empty());
833    /// ```
834    pub fn is_empty(&self) -> bool {
835        self.tokens.iter().all(|(_, n)| *n == 0)
836    }
837}
838
839/// A paired initial and final marking for a WF-net.
840///
841/// WF-net soundness (van der Aalst, 1998) requires both an *initial* marking
842/// (a single token on the source place) and a *final* marking (a single token
843/// on the sink place). `InitialFinalMarkingPair` bundles these two markings as
844/// a single named shape, so a WF-net construction site can pass them together
845/// without risk of swapping the two arguments.
846///
847/// ## Structural law
848///
849/// [`InitialFinalMarkingPair::validate`] checks that neither marking is empty
850/// and that the initial and final markings do not overlap (a place carrying
851/// tokens in both is a structural defect). It does **not** check that the
852/// markings reference declared places — that check is performed by
853/// [`WfNet::validate`].
854///
855/// Structure-only: a shape. No token dynamics.
856///
857/// ```
858/// use wasm4pm_compat::petri::{InitialFinalMarkingPair, Marking, PetriRefusal};
859/// let pair = InitialFinalMarkingPair::new(
860///     Marking::new([("src".to_string(), 1)]),
861///     Marking::new([("snk".to_string(), 1)]),
862/// );
863/// assert!(pair.validate().is_ok());
864/// ```
865#[derive(Clone, Debug, PartialEq, Eq)]
866pub struct InitialFinalMarkingPair {
867    /// The initial marking (typically one token on the source place).
868    pub initial: Marking,
869    /// The final marking (typically one token on the sink place).
870    pub final_marking: Marking,
871}
872
873impl InitialFinalMarkingPair {
874    /// Construct an `InitialFinalMarkingPair` from two markings.
875    ///
876    /// ```
877    /// use wasm4pm_compat::petri::{InitialFinalMarkingPair, Marking};
878    /// let pair = InitialFinalMarkingPair::new(
879    ///     Marking::new([("src".to_string(), 1)]),
880    ///     Marking::new([("snk".to_string(), 1)]),
881    /// );
882    /// assert_eq!(pair.initial.tokens_on("src"), 1);
883    /// assert_eq!(pair.final_marking.tokens_on("snk"), 1);
884    /// ```
885    pub fn new(initial: Marking, final_marking: Marking) -> Self {
886        InitialFinalMarkingPair {
887            initial,
888            final_marking,
889        }
890    }
891
892    /// Structurally validate the marking pair.
893    ///
894    /// Returns [`PetriRefusal::MissingInitialMarking`] if the initial marking is
895    /// empty, [`PetriRefusal::MissingFinalMarking`] if the final marking is empty,
896    /// or [`PetriRefusal::InvalidVariableArc`] if any place id appears in both
897    /// markings (overlapping initial/final places is a structural defect).
898    ///
899    /// ```
900    /// use wasm4pm_compat::petri::{InitialFinalMarkingPair, Marking, PetriRefusal};
901    /// // Empty initial marking:
902    /// let bad = InitialFinalMarkingPair::new(
903    ///     Marking::empty(),
904    ///     Marking::new([("snk".to_string(), 1)]),
905    /// );
906    /// assert_eq!(bad.validate(), Err(PetriRefusal::MissingInitialMarking));
907    /// ```
908    #[must_use = "check the shape-check result"]
909    pub fn validate(&self) -> Result<(), PetriRefusal> {
910        if self.initial.is_empty() {
911            return Err(PetriRefusal::MissingInitialMarking);
912        }
913        if self.final_marking.is_empty() {
914            return Err(PetriRefusal::MissingFinalMarking);
915        }
916        // Detect overlapping place ids between initial and final markings.
917        for (pid, cnt) in &self.initial.tokens {
918            if *cnt > 0 && self.final_marking.tokens_on(pid) > 0 {
919                return Err(PetriRefusal::InvalidVariableArc);
920            }
921        }
922        Ok(())
923    }
924}
925
926/// A plain Petri net: places, transitions, arcs, and an initial marking.
927///
928/// [`PetriNet::validate`] checks structural shape (arcs reference declared
929/// nodes); it does not analyze behavior. Reachability, boundedness, and
930/// soundness graduate to `wasm4pm`.
931///
932/// Structure-only.
933#[derive(Clone, Debug, Default, PartialEq, Eq)]
934pub struct PetriNet {
935    places: Vec<Place>,
936    transitions: Vec<Transition>,
937    arcs: Vec<Arc>,
938    initial: Marking,
939}
940
941impl PetriNet {
942    /// Construct a Petri net from its parts.
943    ///
944    /// ```
945    /// use wasm4pm_compat::petri::{PetriNet, Place, Transition, Arc, Marking};
946    /// let net = PetriNet::new(
947    ///     [Place::new("p")],
948    ///     [Transition::new("t", "a")],
949    ///     [Arc::place_to_transition("p", "t")],
950    ///     Marking::new([("p".to_string(), 1)]),
951    /// );
952    /// assert!(net.validate().is_ok());
953    /// ```
954    pub fn new(
955        places: impl IntoIterator<Item = Place>,
956        transitions: impl IntoIterator<Item = Transition>,
957        arcs: impl IntoIterator<Item = Arc>,
958        initial: Marking,
959    ) -> Self {
960        PetriNet {
961            places: places.into_iter().collect(),
962            transitions: transitions.into_iter().collect(),
963            arcs: arcs.into_iter().collect(),
964            initial,
965        }
966    }
967
968    /// The places.
969    pub fn places(&self) -> &[Place] {
970        &self.places
971    }
972
973    /// The transitions.
974    pub fn transitions(&self) -> &[Transition] {
975        &self.transitions
976    }
977
978    /// The arcs.
979    pub fn arcs(&self) -> &[Arc] {
980        &self.arcs
981    }
982
983    /// The initial marking.
984    pub fn initial_marking(&self) -> &Marking {
985        &self.initial
986    }
987
988    /// Structurally validate that every arc references declared nodes.
989    ///
990    /// This is a shape check, not behavior analysis. A dangling arc is reported
991    /// as [`PetriRefusal::InvalidVariableArc`] (this shape's structural
992    /// arc-defect law). Initial marking presence is required at the WF-net
993    /// level, not here.
994    ///
995    /// ```
996    /// use wasm4pm_compat::petri::{PetriNet, Place, Transition, Arc, Marking, PetriRefusal};
997    /// // Arc references transition "ghost" that does not exist.
998    /// let net = PetriNet::new(
999    ///     [Place::new("p")],
1000    ///     [Transition::new("t", "a")],
1001    ///     [Arc::place_to_transition("p", "ghost")],
1002    ///     Marking::empty(),
1003    /// );
1004    /// assert_eq!(net.validate(), Err(PetriRefusal::InvalidVariableArc));
1005    /// ```
1006    #[must_use = "check the shape-check result"]
1007    pub fn validate(&self) -> Result<(), PetriRefusal> {
1008        use std::collections::HashSet;
1009        let pids: HashSet<&str> = self.places.iter().map(Place::id).collect();
1010        let tids: HashSet<&str> = self.transitions.iter().map(Transition::id).collect();
1011        for a in &self.arcs {
1012            if !pids.contains(a.place_id()) || !tids.contains(a.transition_id()) {
1013                return Err(PetriRefusal::InvalidVariableArc);
1014            }
1015        }
1016        Ok(())
1017    }
1018}
1019
1020/// Soundness state marker: soundness has **not** been asserted or proven.
1021///
1022/// This is an **uninhabited** type-level token (an empty enum) used only as a
1023/// `PhantomData` parameter on [`WfNet`]. It is never constructed; it carries no
1024/// data and costs nothing at runtime. It is the *default* state of a freshly
1025/// constructed WF-net.
1026#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1027pub enum SoundnessUnknown {}
1028
1029/// Soundness state marker: soundness is **claimed** but unproven here.
1030///
1031/// An uninhabited type-level token. A [`WfNet`] in this state asserts soundness
1032/// (e.g. by an upstream tool or a human) but this crate has **not** verified it —
1033/// verification is a `wasm4pm` engine. Treat this as a claim to be discharged,
1034/// not as evidence.
1035#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1036pub enum SoundnessClaimed {}
1037
1038/// Soundness state marker: soundness is **witnessed**.
1039///
1040/// An uninhabited type-level token. A [`WfNet`] reaches this state only by
1041/// re-attaching a soundness witness obtained from `wasm4pm`; this crate models
1042/// the *shape* of "soundness has been witnessed", not the witnessing itself.
1043#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1044pub enum SoundnessWitnessed {}
1045
1046/// A workflow net (WF-net): a Petri net with a single source and sink place and
1047/// a soundness state tracked at the type level by the `S` typestate parameter.
1048///
1049/// The `S` parameter is one of [`SoundnessUnknown`] (default),
1050/// [`SoundnessClaimed`], or [`SoundnessWitnessed`] — empty-enum markers used via
1051/// `PhantomData`. The field/parameter is a **claim about** soundness, never a
1052/// computed proof: [`WfNet::validate`] checks only *structural* WF-net shape
1053/// (initial and final markings present), and the soundness transition methods
1054/// merely *re-type* the claim; they do not compute soundness.
1055///
1056/// Structure-only: actual soundness witnessing graduates to `wasm4pm`, which
1057/// produces the witness that justifies moving to [`SoundnessWitnessed`].
1058#[derive(Clone, Debug, PartialEq, Eq)]
1059pub struct WfNet<S = SoundnessUnknown> {
1060    net: PetriNet,
1061    final_marking: Option<Marking>,
1062    _soundness: PhantomData<S>,
1063}
1064
1065impl WfNet<SoundnessUnknown> {
1066    /// Construct a WF-net in the [`SoundnessUnknown`] state from a Petri net and
1067    /// a final marking.
1068    ///
1069    /// ```
1070    /// use wasm4pm_compat::petri::{WfNet, PetriNet, Place, Transition, Arc, Marking};
1071    /// let net = PetriNet::new(
1072    ///     [Place::new("src"), Place::new("snk")],
1073    ///     [Transition::new("t", "a")],
1074    ///     [Arc::place_to_transition("src", "t"), Arc::transition_to_place("t", "snk")],
1075    ///     Marking::new([("src".to_string(), 1)]),
1076    /// );
1077    /// let wf = WfNet::new(net, Marking::new([("snk".to_string(), 1)]));
1078    /// assert!(wf.validate().is_ok());
1079    /// ```
1080    pub fn new(net: PetriNet, final_marking: Marking) -> Self {
1081        WfNet {
1082            net,
1083            final_marking: Some(final_marking),
1084            _soundness: PhantomData,
1085        }
1086    }
1087}
1088
1089impl<S> WfNet<S> {
1090    /// The underlying Petri net.
1091    pub fn net(&self) -> &PetriNet {
1092        &self.net
1093    }
1094
1095    /// The final marking, if declared.
1096    #[must_use]
1097    pub fn final_marking(&self) -> Option<&Marking> {
1098        self.final_marking.as_ref()
1099    }
1100
1101    /// Structurally validate the WF-net shape.
1102    ///
1103    /// Checks that the underlying net is structurally well-formed, that an
1104    /// initial marking places at least one token
1105    /// ([`PetriRefusal::MissingInitialMarking`]), and that a final marking is
1106    /// declared ([`PetriRefusal::MissingFinalMarking`]). It does **not** check
1107    /// soundness, safeness, or boundedness — those graduate to `wasm4pm`.
1108    ///
1109    /// ```
1110    /// use wasm4pm_compat::petri::{WfNet, PetriNet, Place, Transition, Arc, Marking, PetriRefusal};
1111    /// // Initial marking is empty -> MissingInitialMarking.
1112    /// let net = PetriNet::new(
1113    ///     [Place::new("src"), Place::new("snk")],
1114    ///     [Transition::new("t", "a")],
1115    ///     [Arc::place_to_transition("src", "t"), Arc::transition_to_place("t", "snk")],
1116    ///     Marking::empty(),
1117    /// );
1118    /// let wf = WfNet::new(net, Marking::new([("snk".to_string(), 1)]));
1119    /// assert_eq!(wf.validate(), Err(PetriRefusal::MissingInitialMarking));
1120    /// ```
1121    #[must_use = "check the shape-check result"]
1122    pub fn validate(&self) -> Result<(), PetriRefusal> {
1123        self.net.validate()?;
1124        if self.net.initial_marking().is_empty() {
1125            return Err(PetriRefusal::MissingInitialMarking);
1126        }
1127        match &self.final_marking {
1128            Some(m) if !m.is_empty() => Ok(()),
1129            _ => Err(PetriRefusal::MissingFinalMarking),
1130        }
1131    }
1132
1133    /// Re-type this WF-net as carrying a soundness **claim** (unproven here).
1134    ///
1135    /// This is a *type-level re-tagging only* — it computes nothing. Use it to
1136    /// record that an upstream source asserts soundness; discharge the claim by
1137    /// graduating to `wasm4pm` and witnessing it.
1138    ///
1139    /// ```
1140    /// use wasm4pm_compat::petri::{WfNet, PetriNet, Marking, SoundnessClaimed};
1141    /// let wf = WfNet::new(PetriNet::default(), Marking::new([("snk".to_string(), 1)]));
1142    /// let _claimed: WfNet<SoundnessClaimed> = wf.claim_sound();
1143    /// ```
1144    pub fn claim_sound(self) -> WfNet<SoundnessClaimed> {
1145        WfNet {
1146            net: self.net,
1147            final_marking: self.final_marking,
1148            _soundness: PhantomData,
1149        }
1150    }
1151}
1152
1153impl WfNet<SoundnessClaimed> {
1154    /// Re-type a *claimed* WF-net as **witnessed** sound.
1155    ///
1156    /// **Deprecated** — this method freely advances the typestate without a
1157    /// [`SoundnessProof`] token, making the `Claimed → Witnessed` transition
1158    /// forgeable from outside this module. Use [`WfNetConst::witness_soundness`]
1159    /// instead, which requires a `SoundnessProof` that is only constructible
1160    /// inside this module or via the `wasm4pm` graduation bridge. That path is
1161    /// non-forgeable by construction.
1162    ///
1163    /// This method is retained for source compatibility but will be removed in a
1164    /// future version. Any call site that invokes this method is relying on a
1165    /// forgeability hole in the type law.
1166    ///
1167    /// ```rust,ignore
1168    /// // pub(crate): cannot be called from doctests. Use WfNetConst<Sane> and
1169    /// // its sealed construction path for the non-forgeable witnessed state.
1170    /// use wasm4pm_compat::petri::{WfNet, PetriNet, Marking, SoundnessClaimed, SoundnessWitnessed};
1171    /// let wf = WfNet::new(PetriNet::default(), Marking::new([("snk".to_string(), 1)]))
1172    ///     .claim_sound();
1173    /// #[allow(deprecated)]
1174    /// let _w: WfNet<SoundnessWitnessed> = wf.attest_witnessed();
1175    /// ```
1176    #[deprecated(
1177        since = "26.6.5",
1178        note = "Forgeability hole: this method produces WfNet<SoundnessWitnessed> without \
1179                any structural verification. Use WfNetConst<Sane> and its sealed \
1180                construction path instead. See GAP_007 in sources/wasm4pm-compat/STRUCTURAL_GAPS.md."
1181    )]
1182    #[allow(dead_code)]
1183    pub(crate) fn attest_witnessed(self) -> WfNet<SoundnessWitnessed> {
1184        WfNet {
1185            net: self.net,
1186            final_marking: self.final_marking,
1187            _soundness: PhantomData,
1188        }
1189    }
1190}
1191
1192/// An object-centric Petri net (OC-Petri-net): a Petri net whose arcs are typed
1193/// by object type and may be variable, plus the declared object types.
1194///
1195/// [`ObjectCentricPetriNet::validate`] checks that every typed arc names a
1196/// declared object type ([`PetriRefusal::ObjectTypeNotPreserved`]) and that the
1197/// underlying net is structurally sound (arcs reference declared nodes). It does
1198/// not analyze object-centric behavior — that graduates to `wasm4pm`.
1199///
1200/// Structure-only.
1201#[derive(Clone, Debug, Default, PartialEq, Eq)]
1202pub struct ObjectCentricPetriNet {
1203    net: PetriNet,
1204    object_types: Vec<String>,
1205}
1206
1207impl ObjectCentricPetriNet {
1208    /// Construct an OC-Petri-net from a Petri net and its object types.
1209    ///
1210    /// ```
1211    /// use wasm4pm_compat::petri::{ObjectCentricPetriNet, PetriNet, Place, Transition, Arc, Marking};
1212    /// let net = PetriNet::new(
1213    ///     [Place::new("p")],
1214    ///     [Transition::new("t", "a")],
1215    ///     [Arc::place_to_transition("p", "t").typed("order", false)],
1216    ///     Marking::empty(),
1217    /// );
1218    /// let ocpn = ObjectCentricPetriNet::new(net, ["order".to_string()]);
1219    /// assert!(ocpn.validate().is_ok());
1220    /// ```
1221    pub fn new(net: PetriNet, object_types: impl IntoIterator<Item = String>) -> Self {
1222        ObjectCentricPetriNet {
1223            net,
1224            object_types: object_types.into_iter().collect(),
1225        }
1226    }
1227
1228    /// The underlying Petri net.
1229    pub fn net(&self) -> &PetriNet {
1230        &self.net
1231    }
1232
1233    /// The declared object types.
1234    pub fn object_types(&self) -> &[String] {
1235        &self.object_types
1236    }
1237
1238    /// Structurally validate the OC-Petri-net shape.
1239    ///
1240    /// Checks the underlying net structurally, then verifies every typed arc's
1241    /// object type is declared ([`PetriRefusal::ObjectTypeNotPreserved`]). No
1242    /// behavior analysis is performed.
1243    ///
1244    /// ```
1245    /// use wasm4pm_compat::petri::{ObjectCentricPetriNet, PetriNet, Place, Transition, Arc, Marking, PetriRefusal};
1246    /// // Arc typed "ghost" but only "order" is declared.
1247    /// let net = PetriNet::new(
1248    ///     [Place::new("p")],
1249    ///     [Transition::new("t", "a")],
1250    ///     [Arc::place_to_transition("p", "t").typed("ghost", false)],
1251    ///     Marking::empty(),
1252    /// );
1253    /// let ocpn = ObjectCentricPetriNet::new(net, ["order".to_string()]);
1254    /// assert_eq!(ocpn.validate(), Err(PetriRefusal::ObjectTypeNotPreserved));
1255    /// ```
1256    #[must_use = "check the shape-check result"]
1257    pub fn validate(&self) -> Result<(), PetriRefusal> {
1258        self.net.validate()?;
1259        for a in self.net.arcs() {
1260            if let Some(ot) = a.object_type() {
1261                if !self.object_types.iter().any(|t| t == ot) {
1262                    return Err(PetriRefusal::ObjectTypeNotPreserved);
1263                }
1264            }
1265        }
1266        Ok(())
1267    }
1268}
1269
1270// ── YAWL cancellation-region shape ──────────────────────────────────────────
1271
1272/// A YAWL cancellation region: the named set of place/condition/task ids whose
1273/// tokens are vacuumed when the owning task fires.
1274///
1275/// YAWL Definition 1 (van der Aalst & ter Hofstede, 2004) specifies
1276/// `rem: T ⇸ P(T ∪ C \ {i, o})` — each task optionally names a cancellation
1277/// region. This struct carries the *shape* of that region: a named set of node
1278/// ids. Token-removal execution (the actual vacuuming) graduates to `wasm4pm`.
1279///
1280/// The `#[repr(transparent)]` newtype prevents a bare `Vec<String>` from being
1281/// accidentally passed where a `CancellationRegion` is required. It is zero-cost
1282/// to hold and clone.
1283///
1284/// Structure-only: carries ids, never fires.
1285///
1286/// ```
1287/// use wasm4pm_compat::petri::CancellationRegion;
1288/// let cr = CancellationRegion::new(["p1", "t2"]);
1289/// assert_eq!(cr.members(), &["p1".to_string(), "t2".to_string()]);
1290/// ```
1291#[repr(transparent)]
1292#[derive(Clone, Debug, PartialEq, Eq, Default)]
1293pub struct CancellationRegion {
1294    /// The ids of nodes (places, conditions, tasks) in this cancellation region,
1295    /// excluding the initial and final place of the net (i, o).
1296    pub members: Vec<String>,
1297}
1298
1299impl CancellationRegion {
1300    /// Construct a cancellation region from an iterator of node ids.
1301    ///
1302    /// ```
1303    /// use wasm4pm_compat::petri::CancellationRegion;
1304    /// let cr = CancellationRegion::new(["p1", "t2"]);
1305    /// assert_eq!(cr.members().len(), 2);
1306    /// ```
1307    pub fn new<I, S>(members: I) -> Self
1308    where
1309        I: IntoIterator<Item = S>,
1310        S: Into<String>,
1311    {
1312        CancellationRegion {
1313            members: members.into_iter().map(Into::into).collect(),
1314        }
1315    }
1316
1317    /// The node ids in this cancellation region.
1318    ///
1319    /// ```
1320    /// use wasm4pm_compat::petri::CancellationRegion;
1321    /// assert!(CancellationRegion::default().members().is_empty());
1322    /// ```
1323    pub fn members(&self) -> &[String] {
1324        &self.members
1325    }
1326}
1327
1328// ── YAWL multiple-instance task spec ────────────────────────────────────────
1329
1330/// The creation kind for a YAWL multiple-instance task.
1331///
1332/// YAWL Definition 1 defines `nofi: T ⇸ N × N^∞ × N^∞ × {dynamic, static}`.
1333/// The `{Static, Dynamic}` variant names whether child instances are created
1334/// once at firing time (`Static`) or may be created incrementally during
1335/// execution (`Dynamic`). This is the structural tag; execution semantics of
1336/// dynamic spawning graduate to `wasm4pm`.
1337///
1338/// Structure-only marker; carries no instantiation behavior.
1339///
1340/// ```
1341/// use wasm4pm_compat::petri::InstanceCreationKind;
1342/// let k = InstanceCreationKind::Static;
1343/// assert_eq!(k, InstanceCreationKind::Static);
1344/// ```
1345#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1346pub enum InstanceCreationKind {
1347    /// All child instances are created at the moment the task fires.
1348    Static,
1349    /// Child instances may be created incrementally during the task's lifetime.
1350    Dynamic,
1351}
1352
1353/// A YAWL multiple-instance task specification: the four-tuple
1354/// `(min_instances, max_instances, threshold, creation_kind)`.
1355///
1356/// YAWL Definition 1 (`nofi`) mandates that every multi-instance task carries:
1357/// - `min`: the minimum number of child instances that must complete (`N`).
1358/// - `max`: the maximum number of child instances (`N^∞`), `None` = unbounded.
1359/// - `threshold`: the minimum count for collective completion (`N^∞`),
1360///   `None` = all instances must complete.
1361/// - `creation`: whether spawning is [`InstanceCreationKind::Static`] or
1362///   [`InstanceCreationKind::Dynamic`].
1363///
1364/// The structural law `min <= max` (when `max` is bounded) is checked by
1365/// [`MultipleInstanceSpec::validate`]; a violation is refused as
1366/// [`PetriRefusal::InvalidInstanceBounds`].
1367///
1368/// Structure-only: instance creation and threshold enforcement graduate to
1369/// `wasm4pm`.
1370///
1371/// ```
1372/// use wasm4pm_compat::petri::{MultipleInstanceSpec, InstanceCreationKind};
1373/// let spec = MultipleInstanceSpec::new(1, Some(4), Some(2), InstanceCreationKind::Static);
1374/// assert!(spec.validate().is_ok());
1375/// ```
1376#[derive(Clone, Debug, PartialEq, Eq)]
1377pub struct MultipleInstanceSpec {
1378    /// Minimum number of instances that must complete.
1379    pub min: u32,
1380    /// Maximum number of instances (`None` = unbounded / ∞).
1381    pub max: Option<u32>,
1382    /// Threshold for collective completion (`None` = all instances).
1383    pub threshold: Option<u32>,
1384    /// Whether child instances are created statically or dynamically.
1385    pub creation: InstanceCreationKind,
1386}
1387
1388impl MultipleInstanceSpec {
1389    /// Construct a multiple-instance spec.
1390    ///
1391    /// ```
1392    /// use wasm4pm_compat::petri::{MultipleInstanceSpec, InstanceCreationKind};
1393    /// let spec = MultipleInstanceSpec::new(1, Some(3), None, InstanceCreationKind::Dynamic);
1394    /// assert_eq!(spec.min, 1);
1395    /// assert_eq!(spec.creation, InstanceCreationKind::Dynamic);
1396    /// ```
1397    pub fn new(
1398        min: u32,
1399        max: Option<u32>,
1400        threshold: Option<u32>,
1401        creation: InstanceCreationKind,
1402    ) -> Self {
1403        MultipleInstanceSpec {
1404            min,
1405            max,
1406            threshold,
1407            creation,
1408        }
1409    }
1410
1411    /// Structurally validate the instance bounds.
1412    ///
1413    /// Returns [`PetriRefusal::InvalidInstanceBounds`] if `max` is bounded and
1414    /// `min > max`, or if `min == 0` (at least one instance is required by YAWL).
1415    ///
1416    /// ```
1417    /// use wasm4pm_compat::petri::{MultipleInstanceSpec, InstanceCreationKind, PetriRefusal};
1418    /// let bad = MultipleInstanceSpec::new(5, Some(2), None, InstanceCreationKind::Static);
1419    /// assert_eq!(bad.validate(), Err(PetriRefusal::InvalidInstanceBounds));
1420    /// let zero = MultipleInstanceSpec::new(0, Some(1), None, InstanceCreationKind::Static);
1421    /// assert_eq!(zero.validate(), Err(PetriRefusal::InvalidInstanceBounds));
1422    /// ```
1423    #[must_use = "check the shape-check result"]
1424    pub fn validate(&self) -> Result<(), PetriRefusal> {
1425        if self.min == 0 {
1426            return Err(PetriRefusal::InvalidInstanceBounds);
1427        }
1428        if let Some(max) = self.max {
1429            if self.min > max {
1430                return Err(PetriRefusal::InvalidInstanceBounds);
1431            }
1432        }
1433        Ok(())
1434    }
1435}
1436
1437// ── YAWL multiple-instance bounds — compile-time law surface ────────────────
1438
1439/// A YAWL multiple-instance spec with bounds enforced **at compile time**.
1440///
1441/// `MultipleInstanceSpecConst<MIN, MAX>` encodes the YAWL Definition 1 `nofi`
1442/// invariant `1 ≤ MIN ≤ MAX` as const-generic where-bounds so that a violation
1443/// is a **compile error**, not a runtime refusal.
1444///
1445/// Law: YAWL Definition 1 nofi — `min: N`, `max: N^∞`, `1 ≤ min ≤ max`.
1446///
1447/// ## Compile-time negative receipt
1448///
1449/// `MultipleInstanceSpecConst<5, 2>` does **not compile** because `5 <= 2` is
1450/// false — the `Require<{ MIN <= MAX }>: IsTrue` bound fails at the type level.
1451/// `MultipleInstanceSpecConst<0, 4>` does **not compile** because `0 >= 1` is
1452/// false — `Require<{ MIN >= 1 }>: IsTrue` fails.
1453///
1454/// Use [`MultipleInstanceSpec`] for runtime-validated, dynamically constructed
1455/// specs. Use this type when the bounds are known at compile time and you want
1456/// the compiler to verify them.
1457///
1458/// Structure-only: zero-cost (`_private: ()`) marker that carries the min/max
1459/// law in its type. No engine logic.
1460///
1461/// ```
1462/// # #![feature(generic_const_exprs)]
1463/// # #![allow(incomplete_features)]
1464/// use wasm4pm_compat::petri::MultipleInstanceSpecConst;
1465/// // 1 <= 4: lawful at compile time.
1466/// let _: MultipleInstanceSpecConst<1, 4> = MultipleInstanceSpecConst::new();
1467/// ```
1468///
1469/// ```compile_fail
1470/// # #![feature(generic_const_exprs)]
1471/// # #![allow(incomplete_features)]
1472/// use wasm4pm_compat::petri::MultipleInstanceSpecConst;
1473/// // 5 > 2: compile error — Require<{ 5 <= 2 }>: IsTrue not satisfied.
1474/// let _: MultipleInstanceSpecConst<5, 2> = MultipleInstanceSpecConst::new();
1475/// ```
1476pub struct MultipleInstanceSpecConst<const MIN: u32, const MAX: u32>
1477where
1478    crate::law::Require<{ MIN >= 1 }>: crate::law::IsTrue,
1479    crate::law::Require<{ MIN <= MAX }>: crate::law::IsTrue,
1480{
1481    _private: (),
1482}
1483
1484impl<const MIN: u32, const MAX: u32> MultipleInstanceSpecConst<MIN, MAX>
1485where
1486    crate::law::Require<{ MIN >= 1 }>: crate::law::IsTrue,
1487    crate::law::Require<{ MIN <= MAX }>: crate::law::IsTrue,
1488{
1489    /// Construct a `MultipleInstanceSpecConst<MIN, MAX>` — only possible when
1490    /// `MIN >= 1` and `MIN <= MAX`.
1491    ///
1492    /// ```
1493    /// # #![feature(generic_const_exprs)]
1494    /// # #![allow(incomplete_features)]
1495    /// use wasm4pm_compat::petri::MultipleInstanceSpecConst;
1496    /// let _: MultipleInstanceSpecConst<1, 1> = MultipleInstanceSpecConst::new();
1497    /// let _: MultipleInstanceSpecConst<2, 10> = MultipleInstanceSpecConst::new();
1498    /// ```
1499    pub const fn new() -> Self {
1500        MultipleInstanceSpecConst { _private: () }
1501    }
1502
1503    /// The minimum instance count encoded in the type.
1504    ///
1505    /// ```
1506    /// # #![feature(generic_const_exprs)]
1507    /// # #![allow(incomplete_features)]
1508    /// use wasm4pm_compat::petri::MultipleInstanceSpecConst;
1509    /// assert_eq!(MultipleInstanceSpecConst::<2, 5>::new().min(), 2);
1510    /// ```
1511    pub const fn min(&self) -> u32 {
1512        MIN
1513    }
1514
1515    /// The maximum instance count encoded in the type.
1516    ///
1517    /// ```
1518    /// # #![feature(generic_const_exprs)]
1519    /// # #![allow(incomplete_features)]
1520    /// use wasm4pm_compat::petri::MultipleInstanceSpecConst;
1521    /// assert_eq!(MultipleInstanceSpecConst::<2, 5>::new().max(), 5);
1522    /// ```
1523    pub const fn max(&self) -> u32 {
1524        MAX
1525    }
1526}
1527
1528impl<const MIN: u32, const MAX: u32> Default for MultipleInstanceSpecConst<MIN, MAX>
1529where
1530    crate::law::Require<{ MIN >= 1 }>: crate::law::IsTrue,
1531    crate::law::Require<{ MIN <= MAX }>: crate::law::IsTrue,
1532{
1533    fn default() -> Self {
1534        Self::new()
1535    }
1536}
1537
1538/// The specific, named laws under which Petri-net / WF-net / OC-Petri-net
1539/// structure is refused.
1540///
1541/// Each variant cites a distinct law — never a bare "invalid input".
1542/// [`PetriRefusal::SoundnessNotWitnessed`] is the boundary law for evidence:
1543/// it marks the refusal to treat a [`SoundnessClaimed`] net as if it were
1544/// [`SoundnessWitnessed`] without a witness from `wasm4pm`.
1545#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1546#[non_exhaustive]
1547pub enum PetriRefusal {
1548    /// A WF-net declares no (non-empty) initial marking.
1549    MissingInitialMarking,
1550    /// A WF-net declares no (non-empty) final marking.
1551    MissingFinalMarking,
1552    /// A transition can never fire (a behavioral defect surfaced from `wasm4pm`).
1553    DeadTransition,
1554    /// The net is not safe (a place can hold more than one token where one was
1555    /// required) — surfaced from analysis at `wasm4pm`.
1556    UnsafeNet,
1557    /// The net is unbounded — surfaced from analysis at `wasm4pm`.
1558    UnboundedNet,
1559    /// An OC-Petri-net arc carries an object type that is not declared/preserved.
1560    ObjectTypeNotPreserved,
1561    /// An arc is structurally invalid (e.g. dangling endpoint, malformed
1562    /// variable arc).
1563    InvalidVariableArc,
1564    /// Soundness was relied upon but not witnessed — graduate to `wasm4pm` to
1565    /// obtain the witness.
1566    SoundnessNotWitnessed,
1567    /// A [`CancellationRegion`] references a node id not declared in the net.
1568    ///
1569    /// Law: YAWL Definition 1 rem(t) ⊆ T ∪ C \ {i, o}.
1570    InvalidCancellationRegion,
1571    /// A [`MultipleInstanceSpec`] has `min == 0` or `min > max`.
1572    ///
1573    /// Law: YAWL Definition 1 nofi invariant: `1 ≤ min ≤ max`.
1574    InvalidInstanceBounds,
1575}
1576
1577/// A WF-net tagged as **separable** — a necessary precondition for lossless
1578/// POWL 2.0 conversion (Kourani, Park & van der Aalst, 2026).
1579///
1580/// A WF-net is *separable* if it can be decomposed into a set of structurally
1581/// independent sub-nets whose composition reproduces the original language. Only
1582/// a separable WF-net can be faithfully represented as a POWL 2.0 model via the
1583/// POWL decomposition theorem (Kourani 2026, Definition 4.1).
1584///
1585/// ## Structure-only
1586///
1587/// This marker records the *claim* that a WF-net is separable; it does not
1588/// verify separability (that graduates to `wasm4pm`). The claim prevents code
1589/// from inadvertently feeding a non-separable net to a POWL 2.0 conversion
1590/// path at the type level.
1591///
1592/// ## How to use
1593///
1594/// Wrap your `WfNetConst` in a `SeparableWfNet` after verifying separability
1595/// via the `wasm4pm` engine and attaching the result here. The type-level
1596/// marker then flows through the rest of the pipeline.
1597///
1598/// ## Paper
1599///
1600/// Kourani, Park & van der Aalst (2026) — *Hierarchical Decomposition of
1601/// Separable Workflow-Nets*. Definition 4.1 (separable WF-net) and
1602/// Theorem 4.3 (POWL 2.0 language preservation under decomposition).
1603///
1604/// Structure-only: carries no behavior, no token semantics.
1605pub struct SeparableWfNet<const SOUNDNESS: crate::law::SoundnessState> {
1606    /// The underlying WF-net with its soundness state.
1607    pub net: WfNetConst<SOUNDNESS>,
1608    // Private seal — separability claim is not forgeable from outside this module.
1609    _seal: wfnet_seal::WfNetSeal,
1610}
1611
1612impl<const SOUNDNESS: crate::law::SoundnessState> SeparableWfNet<SOUNDNESS> {
1613    /// Construct a `SeparableWfNet` from a verified `WfNetConst`.
1614    ///
1615    /// This is `pub(crate)` — the only public construction path is through the
1616    /// `wasm4pm` graduation bridge that verifies separability.
1617    ///
1618    /// ```
1619    /// use wasm4pm_compat::petri::{WfNetConst, SeparableWfNet};
1620    /// use wasm4pm_compat::law::SoundnessState;
1621    /// let _: SeparableWfNet<{ SoundnessState::Unknown }> =
1622    ///     SeparableWfNet::declare_separable(WfNetConst::new());
1623    /// ```
1624    pub fn declare_separable(net: WfNetConst<SOUNDNESS>) -> Self {
1625        SeparableWfNet {
1626            net,
1627            _seal: wfnet_seal::WfNetSeal,
1628        }
1629    }
1630}
1631
1632/// A standalone, named error type for the *missing final marking* law.
1633///
1634/// [`PetriRefusal::MissingFinalMarking`] names the law as a variant on the
1635/// shared refusal enum. `MissingFinalMarkingError` is a separate zero-sized
1636/// struct that names the same law as a **first-class type** — useful when a
1637/// function returns `Result<_, MissingFinalMarkingError>` instead of the broad
1638/// `Result<_, PetriRefusal>`, making the specific law visible in the return
1639/// type signature.
1640///
1641/// Law: a WF-net must have a declared, non-empty final marking (van der Aalst,
1642/// 1998 — "proper completion" requires a designated final state).
1643///
1644/// ## Conversion
1645///
1646/// `MissingFinalMarkingError` converts to [`PetriRefusal::MissingFinalMarking`]
1647/// via `From<MissingFinalMarkingError> for PetriRefusal`.
1648///
1649/// Structure-only: zero-sized, carries no data.
1650///
1651/// ```
1652/// use wasm4pm_compat::petri::{MissingFinalMarkingError, PetriRefusal};
1653/// let e = MissingFinalMarkingError;
1654/// let r: PetriRefusal = e.into();
1655/// assert_eq!(r, PetriRefusal::MissingFinalMarking);
1656/// ```
1657#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1658pub struct MissingFinalMarkingError;
1659
1660impl core::fmt::Display for MissingFinalMarkingError {
1661    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1662        write!(f, "WF-net refused by law: MissingFinalMarking")
1663    }
1664}
1665
1666impl From<MissingFinalMarkingError> for PetriRefusal {
1667    fn from(_: MissingFinalMarkingError) -> Self {
1668        PetriRefusal::MissingFinalMarking
1669    }
1670}
1671
1672/// An uninhabited type-level marker asserting the *separability claim* about a
1673/// WF-net, independent of the [`SeparableWfNet`] wrapper struct.
1674///
1675/// `SeparableWfNetMarker` is used as a `PhantomData` type parameter in
1676/// downstream types that need to carry a separability claim through a pipeline
1677/// without wrapping the whole WF-net. The marker is an empty enum and therefore
1678/// uninhabited — it can only appear as a `PhantomData` field.
1679///
1680/// ## Why both `SeparableWfNet` and `SeparableWfNetMarker`?
1681///
1682/// [`SeparableWfNet`] wraps a [`WfNetConst`] — it is a value-carrying container.
1683/// `SeparableWfNetMarker` is a *phantom* — it marks a type as "was derived from
1684/// a separable net" without copying the net value. Use the marker when the net
1685/// itself does not need to travel with the downstream type.
1686///
1687/// ## Paper
1688///
1689/// Kourani, Park & van der Aalst (2026) — *Hierarchical Decomposition of
1690/// Separable Workflow-Nets*. Definition 4.1.
1691///
1692/// Structure-only: uninhabited, zero-cost phantom marker.
1693///
1694/// ```
1695/// use core::marker::PhantomData;
1696/// use wasm4pm_compat::petri::SeparableWfNetMarker;
1697///
1698/// // A downstream type that remembers the net was separable.
1699/// struct PowlConversionResult<S>(PhantomData<S>);
1700/// let _: PowlConversionResult<SeparableWfNetMarker>;
1701/// ```
1702#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1703pub enum SeparableWfNetMarker {}
1704
1705// ── Stochastic Petri net structural annotations ──────────────────────────────
1706
1707/// An arc weight annotation carrying a stochastic firing rate or probability.
1708///
1709/// In a *stochastic Petri net* (SPN), each transition is associated with a
1710/// firing rate (exponentially distributed) or a weight (in Generalised SPNs,
1711/// for immediate transitions). `StochasticArcWeight` wraps the `f64` rate or
1712/// weight as a named structural annotation — it does **not** represent an
1713/// execution probability or trigger a firing.
1714///
1715/// Structure-only: a rate/weight annotation. Stochastic simulation and
1716/// steady-state analysis graduate to `wasm4pm`.
1717///
1718/// ## Paper / Reference
1719///
1720/// Molloy (1982) — *Performance Analysis Using Stochastic Petri Nets*.
1721/// Marsan et al. (1984) — *A class of Generalised Stochastic Petri Nets*.
1722///
1723/// ```
1724/// use wasm4pm_compat::petri::StochasticArcWeight;
1725/// let w = StochasticArcWeight(0.5);
1726/// assert_eq!(w.0, 0.5);
1727/// ```
1728#[derive(Clone, Copy, Debug, PartialEq)]
1729pub struct StochasticArcWeight(pub f64);
1730
1731/// A zero-time transition marker — an *immediate* transition in a
1732/// Generalised Stochastic Petri Net (GSPN).
1733///
1734/// Immediate transitions fire instantaneously (zero delay) and with a
1735/// priority that supersedes timed transitions when both are enabled.
1736/// `ImmediateTransition` is a zero-sized structural tag applied to a
1737/// [`Transition`] to assert that it is immediate. It carries no weight
1738/// or priority value — those graduate to `wasm4pm`.
1739///
1740/// Structure-only: a zero-sized marker. Priority resolution and firing
1741/// semantics graduate to `wasm4pm`.
1742///
1743/// ## Paper / Reference
1744///
1745/// Marsan et al. (1984) — GSPN Definition 2 (immediate vs. timed transitions).
1746///
1747/// ```
1748/// use wasm4pm_compat::petri::ImmediateTransition;
1749/// let _: ImmediateTransition;
1750/// ```
1751#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1752pub struct ImmediateTransition;
1753
1754/// A timed transition annotation carrying an exponential firing rate.
1755///
1756/// In a Stochastic Petri Net, a *timed* transition fires after an
1757/// exponentially-distributed delay with rate λ (`self.0`). Higher rates
1758/// mean shorter expected delays. `TimedTransition` wraps λ as a named
1759/// structural annotation; it does **not** compute delays or trigger firings.
1760///
1761/// Structure-only: a rate annotation. Simulation and analysis graduate
1762/// to `wasm4pm`.
1763///
1764/// ## Paper / Reference
1765///
1766/// Molloy (1982) — exponential firing-rate model. Marsan et al. (1984) —
1767/// GSPN Definition 2 (timed transitions with rate λ).
1768///
1769/// ```
1770/// use wasm4pm_compat::petri::TimedTransition;
1771/// let t = TimedTransition(2.5);
1772/// assert_eq!(t.0, 2.5);
1773/// ```
1774#[derive(Clone, Copy, Debug, PartialEq)]
1775pub struct TimedTransition(pub f64);
1776
1777impl core::fmt::Display for PetriRefusal {
1778    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1779        let law = match self {
1780            PetriRefusal::MissingInitialMarking => "MissingInitialMarking",
1781            PetriRefusal::MissingFinalMarking => "MissingFinalMarking",
1782            PetriRefusal::DeadTransition => "DeadTransition",
1783            PetriRefusal::UnsafeNet => "UnsafeNet",
1784            PetriRefusal::UnboundedNet => "UnboundedNet",
1785            PetriRefusal::ObjectTypeNotPreserved => "ObjectTypeNotPreserved",
1786            PetriRefusal::InvalidVariableArc => "InvalidVariableArc",
1787            PetriRefusal::SoundnessNotWitnessed => "SoundnessNotWitnessed",
1788            PetriRefusal::InvalidCancellationRegion => "InvalidCancellationRegion",
1789            PetriRefusal::InvalidInstanceBounds => "InvalidInstanceBounds",
1790        };
1791        write!(f, "Petri-net refused by law: {law}")
1792    }
1793}
1794
1795// ── FlatIncidenceMatrix ───────────────────────────────────────────────────────
1796
1797/// A flat (row-major) incidence matrix for a Petri net.
1798///
1799/// Rows are places, columns are transitions. Value at
1800/// `[place_idx * transitions_count + transition_idx]` is the net token change
1801/// (+1 produced, -1 consumed, 0 no arc).
1802///
1803/// This is structure only — it carries a precomputed matrix, never derives one.
1804/// Corresponds to `FlatIncidenceMatrix` in `wasm4pm-types::models`.
1805#[derive(Clone, Debug, PartialEq, Eq)]
1806pub struct FlatIncidenceMatrix {
1807    pub data: Vec<i32>,
1808    pub places_count: usize,
1809    pub transitions_count: usize,
1810}
1811
1812impl FlatIncidenceMatrix {
1813    pub fn get(&self, place_idx: usize, transition_idx: usize) -> i32 {
1814        self.data[place_idx * self.transitions_count + transition_idx]
1815    }
1816}
1817
1818// ── Van der Aalst mutable Petri net construction ──────────────────────────────
1819//
1820// PM4Py (van der Aalst's reference implementation) constructs Petri nets by
1821// creating an empty net, then adding places, transitions, and arcs one at a
1822// time:
1823//
1824//   net = PetriNet()
1825//   source = PetriNet.Place("source"); net.places.add(source)
1826//   t = PetriNet.Transition("t_A", "A"); net.transitions.add(t)
1827//   petri_utils.add_arc_from_to(source, t, net)
1828//
1829// PetriNetBuilder mirrors that API in Rust. Algorithms in `wasm4pm` that
1830// produce Petri nets (Alpha, Alpha++, Heuristics Miner, Inductive Miner) use
1831// this builder rather than constructing the final tuple in one shot.
1832
1833/// Mutable Petri net builder — van der Aalst's PM4Py construction pattern in Rust.
1834///
1835/// Construct a net by adding places, transitions, and arcs incrementally, then
1836/// call [`build`](PetriNetBuilder::build) to obtain a validated [`PetriNet`].
1837///
1838/// ## Example
1839///
1840/// ```
1841/// use wasm4pm_compat::petri::PetriNetBuilder;
1842///
1843/// let mut b = PetriNetBuilder::new();
1844/// b.place("source")
1845///  .place("p1")
1846///  .place("sink")
1847///  .transition("t_A", "register")
1848///  .transition("t_B", "approve")
1849///  .p_to_t("source", "t_A")
1850///  .t_to_p("t_A", "p1")
1851///  .p_to_t("p1", "t_B")
1852///  .t_to_p("t_B", "sink")
1853///  .initial_tokens([("source", 1)]);
1854/// let net = b.build().expect("valid net");
1855///
1856/// assert!(net.validate().is_ok());
1857/// assert_eq!(net.places().len(), 3);
1858/// assert_eq!(net.transitions().len(), 2);
1859/// ```
1860#[derive(Debug, Default)]
1861pub struct PetriNetBuilder {
1862    places: Vec<Place>,
1863    transitions: Vec<Transition>,
1864    arcs: Vec<Arc>,
1865    initial_tokens: Vec<(String, u32)>,
1866}
1867
1868impl PetriNetBuilder {
1869    /// Create an empty builder.
1870    pub fn new() -> Self {
1871        Self::default()
1872    }
1873
1874    /// Add a place by id. Builder-style (returns `&mut Self`).
1875    ///
1876    /// ```
1877    /// use wasm4pm_compat::petri::PetriNetBuilder;
1878    /// let mut b = PetriNetBuilder::new();
1879    /// b.place("p1").place("p2");
1880    /// let net = b.build_unchecked();
1881    /// assert_eq!(net.places().len(), 2);
1882    /// ```
1883    pub fn place(&mut self, id: impl Into<String>) -> &mut Self {
1884        self.places.push(Place::new(id));
1885        self
1886    }
1887
1888    /// Add a visible (labeled) transition.
1889    ///
1890    /// ```
1891    /// use wasm4pm_compat::petri::PetriNetBuilder;
1892    /// let mut b = PetriNetBuilder::new();
1893    /// b.place("p").transition("t", "A");
1894    /// let net = b.build_unchecked();
1895    /// assert!(!net.transitions()[0].is_silent());
1896    /// ```
1897    pub fn transition(&mut self, id: impl Into<String>, label: impl Into<String>) -> &mut Self {
1898        self.transitions.push(Transition::new(id, label));
1899        self
1900    }
1901
1902    /// Add a silent (tau / invisible) transition.
1903    ///
1904    /// Silent transitions model routing decisions without corresponding
1905    /// log events — van der Aalst's τ-transitions.
1906    ///
1907    /// ```
1908    /// use wasm4pm_compat::petri::PetriNetBuilder;
1909    /// let mut b = PetriNetBuilder::new();
1910    /// b.place("p").silent("tau_split");
1911    /// let net = b.build_unchecked();
1912    /// assert!(net.transitions()[0].is_silent());
1913    /// ```
1914    pub fn silent(&mut self, id: impl Into<String>) -> &mut Self {
1915        self.transitions.push(Transition::silent(id));
1916        self
1917    }
1918
1919    /// Add a place-to-transition arc (van der Aalst: `p → t`, input arc).
1920    ///
1921    /// Corresponds to `petri_utils.add_arc_from_to(place, transition, net)`.
1922    pub fn p_to_t(
1923        &mut self,
1924        place_id: impl Into<String>,
1925        transition_id: impl Into<String>,
1926    ) -> &mut Self {
1927        self.arcs
1928            .push(Arc::place_to_transition(place_id, transition_id));
1929        self
1930    }
1931
1932    /// Add a transition-to-place arc (van der Aalst: `t → p`, output arc).
1933    ///
1934    /// Corresponds to `petri_utils.add_arc_from_to(transition, place, net)`.
1935    pub fn t_to_p(
1936        &mut self,
1937        transition_id: impl Into<String>,
1938        place_id: impl Into<String>,
1939    ) -> &mut Self {
1940        self.arcs
1941            .push(Arc::transition_to_place(transition_id, place_id));
1942        self
1943    }
1944
1945    /// Add a weighted place-to-transition arc.
1946    pub fn p_to_t_w(
1947        &mut self,
1948        place_id: impl Into<String>,
1949        transition_id: impl Into<String>,
1950        weight: u32,
1951    ) -> &mut Self {
1952        self.arcs
1953            .push(Arc::place_to_transition(place_id, transition_id).with_weight(weight));
1954        self
1955    }
1956
1957    /// Add a weighted transition-to-place arc.
1958    pub fn t_to_p_w(
1959        &mut self,
1960        transition_id: impl Into<String>,
1961        place_id: impl Into<String>,
1962        weight: u32,
1963    ) -> &mut Self {
1964        self.arcs
1965            .push(Arc::transition_to_place(transition_id, place_id).with_weight(weight));
1966        self
1967    }
1968
1969    /// Set initial token counts for places.
1970    ///
1971    /// Each item is a `(place_id, token_count)` pair. A workflow net (WF-net)
1972    /// typically has exactly one token in the source place.
1973    pub fn initial_tokens<I, S>(&mut self, tokens: I) -> &mut Self
1974    where
1975        I: IntoIterator<Item = (S, u32)>,
1976        S: Into<String>,
1977    {
1978        self.initial_tokens
1979            .extend(tokens.into_iter().map(|(p, c)| (p.into(), c)));
1980        self
1981    }
1982
1983    /// Build the [`PetriNet`], validating structural shape.
1984    ///
1985    /// Returns [`PetriRefusal`] if any arc references an undeclared place or
1986    /// transition.
1987    pub fn build(self) -> Result<PetriNet, PetriRefusal> {
1988        let marking = Marking::new(self.initial_tokens);
1989        let net = PetriNet::new(self.places, self.transitions, self.arcs, marking);
1990        net.validate()?;
1991        Ok(net)
1992    }
1993
1994    /// Build without validation — for cases where the caller guarantees structure.
1995    ///
1996    /// Prefer [`build`](Self::build) for externally-supplied data.
1997    pub fn build_unchecked(self) -> PetriNet {
1998        let marking = Marking::new(self.initial_tokens);
1999        PetriNet::new(self.places, self.transitions, self.arcs, marking)
2000    }
2001
2002    /// Build a workflow net (WF-net) with a declared final marking.
2003    ///
2004    /// Structural shape is validated before returning.
2005    pub fn build_wf(self, final_marking: Marking) -> Result<WfNet<SoundnessUnknown>, PetriRefusal> {
2006        let net = self.build()?;
2007        Ok(WfNet::new(net, final_marking))
2008    }
2009}