Skip to main content

graphforge_core/
lib.rs

1//! GraphForge shared identities, values, options, and facade errors.
2//!
3//! Compiler, execution, storage, and domain crates consume these shared contracts.
4//! Independent utility crates can define their own types without depending on core.
5//! The public engine facade lives in `graphforge-api`, above the pipeline crates.
6//! Stage and transport crates retain dedicated error types; relevant boundaries
7//! classify errors for the facade and bindings.
8#![forbid(unsafe_code)]
9
10mod algorithm_error;
11mod bind_error;
12mod cypher_feature;
13mod lowering_error;
14mod parse_error;
15pub use algorithm_error::AlgorithmError;
16pub use bind_error::{BindError, BindErrorKind};
17pub use cypher_feature::UnsupportedCypherFeature;
18pub use lowering_error::LoweringError;
19pub use parse_error::{ParseError, ParseErrorKind};
20
21pub mod algorithms;
22pub mod canonical;
23pub mod embedding_options;
24pub mod identifier;
25pub mod manifest;
26pub mod uuid;
27
28use std::{fmt, sync::Arc};
29
30// ---------------------------------------------------------------------------
31// Span
32// ---------------------------------------------------------------------------
33
34/// Byte-offset range in the original source text.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
36pub struct Span {
37    /// Start byte offset (inclusive).
38    pub start: usize,
39    /// End byte offset (exclusive).
40    pub end: usize,
41}
42
43impl Span {
44    /// Create a new span.
45    #[must_use]
46    pub const fn new(start: usize, end: usize) -> Self {
47        Self { start, end }
48    }
49}
50
51impl fmt::Display for Span {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        write!(f, "{}..{}", self.start, self.end)
54    }
55}
56
57// ---------------------------------------------------------------------------
58// TypeId
59// ---------------------------------------------------------------------------
60
61/// Opaque integer identifier for any ontology type (entity, relation, or property).
62///
63/// IDs are assigned at compile time by the ontology compiler and are stable
64/// for the lifetime of a loaded ontology.
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
66pub struct TypeId(pub u32);
67
68/// Opaque integer identifier for a property type.
69///
70/// Distinct from [`TypeId`] (which identifies entity/relation types) so that
71/// the type system prevents accidental interchangeability.  IDs are assigned
72/// at compile time by the ontology compiler.
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
74pub struct PropId(pub u32);
75
76/// Serialisation format of an ontology definition file.
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum OntologyFormat {
79    /// YAML (`.yaml` or `.yml`).
80    Yaml,
81    /// JSON (`.json`).
82    Json,
83}
84
85/// How the binder resolves unknown labels, relation types, and property names.
86///
87/// Serialises as a lowercase string (`"exploratory"`, `"advisory"`,
88/// `"strict"`).  Lives in `graphforge-core` so that the project manifest
89/// ([`manifest::ProjectManifest`]) and the binder (`graphforge-ir`) share one
90/// definition; `graphforge-ir` re-exports it as `graphforge_ir::OntologyMode`.
91#[derive(
92    Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize,
93)]
94#[serde(rename_all = "lowercase")]
95pub enum OntologyMode {
96    /// No ontology required; the runtime catalog auto-assigns integer IDs for
97    /// every observed label/type.
98    #[default]
99    Exploratory,
100    /// Ontology present; violations produce warnings, not errors.
101    Advisory,
102    /// Ontology required; violations produce a bind error.
103    Strict,
104}
105
106// ---------------------------------------------------------------------------
107// GfError — public error enum
108// ---------------------------------------------------------------------------
109
110/// Shared error classifications used by the engine facade and bindings.
111///
112/// Compiler stages, I/O, discovery, and other subsystems also expose dedicated
113/// error types. Boundary adapters classify them as needed; [`GfError::code`]
114/// supplies stable public codes rather than a one-to-one variant/exception map.
115#[derive(thiserror::Error, Debug, Clone)]
116pub enum GfError {
117    /// Feature exists in the API but has not been implemented yet.
118    #[error("not implemented: {0}")]
119    NotImplemented(&'static str),
120
121    /// The Cypher parser rejected the input.
122    #[error("parse error at {span}: {msg}")]
123    Parse {
124        /// Original typed parser diagnostic, when supplied by the parser.
125        diagnostic: Option<Box<ParseError>>,
126        /// Human-readable description of the parse failure.
127        msg: String,
128        /// Location of the bad token in the source.
129        span: Span,
130    },
131
132    /// The binder rejected the query (e.g. undeclared variable, strict-mode
133    /// unknown label). Carries the source span of the *first* error so callers
134    /// can point at the offending token; `msg` lists every binder error.
135    ///
136    /// Publicly classified with [`GfError::Parse`] as `GF_PARSE` / `ParseError`
137    /// — semantic query-structure failures share the parse fault domain.
138    #[error("bind error at {span}: {msg}")]
139    Bind {
140        /// All original binder diagnostics, in their reported order.
141        diagnostics: Vec<BindError>,
142        /// Human-readable description (all binder errors, joined with `; `).
143        msg: String,
144        /// Source location of the first offending token.
145        span: Span,
146    },
147
148    /// A lowering failure with its original kind and payload.
149    #[error("{domain} error: {0}", domain = .0.fault_domain())]
150    Lowering(#[source] LoweringError),
151
152    /// A lowering diagnostic discovered at an execution boundary.
153    /// Keeps the established runtime fault domain rather than reclassifying it.
154    #[error("execution error: {0}")]
155    LoweringExecution(#[source] LoweringError),
156
157    /// A legacy EXPLAIN binder rejection in the established planning domain.
158    #[error("plan error: {msg}")]
159    BindPlan {
160        /// Existing public diagnostic.
161        msg: String,
162        /// Complete original binder diagnostics.
163        diagnostics: Vec<BindError>,
164    },
165
166    /// A binder rejection in the established validation fault domain.
167    #[error("validation error: {msg}")]
168    BindValidation {
169        /// Existing public diagnostic.
170        msg: String,
171        /// Complete original binder diagnostics.
172        diagnostics: Vec<BindError>,
173    },
174
175    /// An algorithm failure retaining its original kind and payload.
176    #[error("{domain} error: {0}", domain = .0.fault_domain())]
177    Algorithm(#[source] AlgorithmError),
178
179    /// The binder or query planner could not produce a valid plan.
180    #[error("plan error: {0}")]
181    Plan(String),
182
183    /// A runtime fault occurred during query execution.
184    #[error("execution error: {0}")]
185    Execution(String),
186
187    /// A redacted configured-provider invocation failed.
188    #[error("provider error: class={class} provider={provider} model={model}")]
189    Provider {
190        /// Stable provider failure class.
191        class: String,
192        /// Normalized non-secret provider identifier.
193        provider: String,
194        /// Non-secret model identifier.
195        model: String,
196    },
197
198    /// A storage I/O operation failed.
199    #[error("storage error: {0}")]
200    Storage(String),
201
202    /// The project container cannot be resolved safely.
203    #[error("{code}: {message}")]
204    Project {
205        /// Stable public error code.
206        code: ProjectErrorCode,
207        /// Safe diagnostic without participant contents or unrestricted paths.
208        message: String,
209    },
210
211    /// A structured knowledge/epistemic public API failure.
212    #[error("{code}: {message}")]
213    Api {
214        /// Stable closed public error code.
215        code: ApiErrorCode,
216        /// Safe diagnostic without record contents.
217        message: String,
218    },
219
220    /// An operation was invalid for the current instance lifecycle state.
221    #[error("lifecycle error: {0}")]
222    Lifecycle(String),
223
224    /// Input failed validation at the API boundary.
225    #[error("validation error: {0}")]
226    Validation(String),
227
228    /// An ontology file could not be loaded or applied.
229    #[error("ontology error: {0}")]
230    Ontology(String),
231}
232
233mod error_conversion;
234
235impl GfError {
236    /// Return the stable public error code for this failure.
237    #[must_use]
238    pub const fn code(&self) -> &'static str {
239        match self {
240            Self::Lowering(LoweringError::InvalidType(_))
241            | Self::BindValidation { .. }
242            | Self::Validation(_)
243            | Self::Algorithm(
244                AlgorithmError::Unavailable { .. } | AlgorithmError::DuplicateCapability { .. },
245            ) => "GF_VALIDATION",
246            Self::Lowering(_) | Self::BindPlan { .. } | Self::Plan(_) => "GF_PLAN",
247            Self::Algorithm(_)
248            | Self::LoweringExecution(_)
249            | Self::Execution(_)
250            | Self::Provider { .. } => "GF_EXECUTION",
251            Self::NotImplemented(_) => "GF_NOT_IMPLEMENTED",
252            Self::Parse { .. } | Self::Bind { .. } => "GF_PARSE",
253            Self::Storage(_) => "GF_IO",
254            Self::Project { code, .. } => code.as_str(),
255            Self::Api { code, .. } => code.as_str(),
256            Self::Lifecycle(_) => "GF_LIFECYCLE",
257            Self::Ontology(_) => "GF_ONTOLOGY",
258        }
259    }
260}
261
262/// Stable non-project knowledge/epistemic API error codes.
263#[derive(Debug, Clone, Copy, PartialEq, Eq)]
264pub enum ApiErrorCode {
265    /// Requested UUID does not exist in the selected capability snapshot.
266    NotFound,
267    /// Operation was cancelled at a deterministic checkpoint.
268    Cancelled,
269    /// Request exceeded a registered bounded-resource limit.
270    ResourceLimit,
271    /// Page token is malformed or incompatible with the method.
272    PageInvalid,
273    /// Page token names a generation that is no longer the selected snapshot.
274    PageSnapshotGone,
275    /// Persisted Arrow schema or registered fingerprint is incompatible.
276    SchemaMismatch,
277    /// Caller supplied an unknown argument.
278    UnknownArgument,
279    /// Explicit projection policy could not resolve an epistemic ambiguity.
280    AmbiguousProjection,
281    /// An identity was reused for different immutable content.
282    IdentityConflict,
283    /// Canonical fingerprint collision was detected.
284    FingerprintCollision,
285    /// A completed run did not retain its result rows.
286    ResultNotRetained,
287}
288
289impl ApiErrorCode {
290    /// Frozen external spelling.
291    #[must_use]
292    pub const fn as_str(self) -> &'static str {
293        match self {
294            Self::NotFound => "GF_NOT_FOUND",
295            Self::Cancelled => "GF_CANCELLED",
296            Self::ResourceLimit => "GF_RESOURCE_LIMIT",
297            Self::PageInvalid => "GF_PAGE_INVALID",
298            Self::PageSnapshotGone => "GF_PAGE_SNAPSHOT_GONE",
299            Self::SchemaMismatch => "GF_SCHEMA_MISMATCH",
300            Self::UnknownArgument => "GF_UNKNOWN_ARGUMENT",
301            Self::AmbiguousProjection => "GF_AMBIGUOUS_PROJECTION",
302            Self::IdentityConflict => "GF_IDENTITY_CONFLICT",
303            Self::FingerprintCollision => "GF_FINGERPRINT_COLLISION",
304            Self::ResultNotRetained => "GF_RESULT_NOT_RETAINED",
305        }
306    }
307}
308
309impl fmt::Display for ApiErrorCode {
310    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
311        f.write_str(self.as_str())
312    }
313}
314
315/// Stable project-format and project-generation error codes.
316#[derive(Debug, Clone, Copy, PartialEq, Eq)]
317pub enum ProjectErrorCode {
318    /// The root is not the current supported project format.
319    UnsupportedProjectFormat,
320    /// The v1 container has no committed generation.
321    ProjectUninitialized,
322    /// The commit pointer or selected generation is internally inconsistent.
323    ProjectCorrupt,
324    /// The project filesystem cannot provide the required atomic semantics.
325    UnsupportedFilesystem,
326    /// Another process currently owns the project writer lock.
327    WriterBusy,
328    /// A staged write cannot be applied to the latest committed generation.
329    WriteConflict,
330    /// Compatible contention exceeded the caller's bounded rebase attempts.
331    RebaseExhausted,
332    /// A transaction UUID was reused with different immutable inputs.
333    TransactionConflict,
334    /// A generation failed before or after its commit point.
335    PublicationFailed,
336    /// A capability ID/version is not implemented by this binary.
337    UnsupportedCapabilityVersion,
338    /// A capability-specific operation was requested before enablement.
339    CapabilityDisabled,
340    /// A transaction failed before publication.
341    TransactionFailed,
342    /// The named checkpoint already exists.
343    CheckpointExists,
344    /// The named checkpoint does not exist.
345    CheckpointNotFound,
346    /// Checkpoint registry state is unsafe or inconsistent.
347    CheckpointRegistryCorrupt,
348    /// A mutation was attempted through an immutable checkpoint view.
349    ReadOnlyView,
350    /// A bounded checkpoint resource limit was exceeded.
351    ResourceLimit,
352}
353
354impl ProjectErrorCode {
355    /// Return the frozen public error-code spelling.
356    #[must_use]
357    pub const fn as_str(self) -> &'static str {
358        match self {
359            Self::UnsupportedProjectFormat => "GF_UNSUPPORTED_PROJECT_FORMAT",
360            Self::ProjectUninitialized => "GF_PROJECT_UNINITIALIZED",
361            Self::ProjectCorrupt => "GF_PROJECT_CORRUPT",
362            Self::UnsupportedFilesystem => "GF_UNSUPPORTED_FILESYSTEM",
363            Self::WriterBusy => "GF_WRITER_BUSY",
364            Self::WriteConflict => "GF_WRITE_CONFLICT",
365            Self::RebaseExhausted => "GF_REBASE_EXHAUSTED",
366            Self::TransactionConflict => "GF_IDEMPOTENCY_CONFLICT",
367            Self::PublicationFailed => "GF_PUBLICATION_FAILED",
368            Self::UnsupportedCapabilityVersion => "GF_UNSUPPORTED_CAPABILITY_VERSION",
369            Self::CapabilityDisabled => "GF_CAPABILITY_DISABLED",
370            Self::TransactionFailed => "GF_TRANSACTION_FAILED",
371            Self::CheckpointExists => "GF_CHECKPOINT_EXISTS",
372            Self::CheckpointNotFound => "GF_CHECKPOINT_NOT_FOUND",
373            Self::CheckpointRegistryCorrupt => "GF_CHECKPOINT_REGISTRY_CORRUPT",
374            Self::ReadOnlyView => "GF_READ_ONLY_VIEW",
375            Self::ResourceLimit => "GF_RESOURCE_LIMIT",
376        }
377    }
378}
379
380impl fmt::Display for ProjectErrorCode {
381    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
382        f.write_str(self.as_str())
383    }
384}
385
386// ---------------------------------------------------------------------------
387// PropValue — minimal property value type
388// ---------------------------------------------------------------------------
389
390/// Consumer-neutral temporal values accepted at GraphForge data boundaries.
391///
392/// Calendar and wall-clock components remain separate: durations are never
393/// collapsed into elapsed nanoseconds, and zone-bearing datetimes retain both
394/// the observed UTC offset and optional IANA zone identifier.
395#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
396#[serde(tag = "type", rename_all = "snake_case")]
397pub enum TemporalValue {
398    /// Calendar duration with independent months, days, seconds, and nanoseconds.
399    Duration {
400        /// Calendar months.
401        months: i64,
402        /// Calendar days.
403        days: i64,
404        /// Whole elapsed seconds below the calendar components.
405        seconds: i64,
406        /// Sub-second nanoseconds.
407        nanos: i64,
408    },
409    /// UTC instant as microseconds from the Unix epoch.
410    UtcDateTime {
411        /// Signed microseconds from the Unix epoch.
412        epoch_micros: i64,
413    },
414    /// Calendar date as days from the Unix epoch.
415    Date {
416        /// Signed days from the Unix epoch.
417        epoch_days: i64,
418    },
419    /// Local date and time without an offset or zone.
420    LocalDateTime {
421        /// Signed days from the Unix epoch.
422        epoch_days: i64,
423        /// Nanoseconds since local midnight.
424        nanos: i64,
425    },
426    /// Local wall-clock time without an offset.
427    LocalTime {
428        /// Nanoseconds since local midnight.
429        nanos: i64,
430    },
431    /// Wall-clock time with its explicit UTC offset in seconds.
432    OffsetTime {
433        /// Nanoseconds since local midnight.
434        nanos: i64,
435        /// Signed UTC offset in seconds.
436        offset_seconds: i32,
437    },
438    /// Date and time with an explicit offset and optional IANA zone identity.
439    ZonedDateTime {
440        /// Signed days from the Unix epoch in the represented local date.
441        epoch_days: i64,
442        /// Nanoseconds since local midnight.
443        nanos: i64,
444        /// Signed observed UTC offset in seconds.
445        offset_seconds: i32,
446        /// Optional IANA zone identity; `None` means offset-only.
447        zone: Option<String>,
448    },
449}
450
451impl TemporalValue {
452    /// Validate the canonical ranges shared by scalar and bulk ingestion.
453    pub fn validate(&self) -> Result<(), GfError> {
454        const NANOS_PER_DAY: i64 = 86_400_000_000_000;
455        const MAX_OFFSET_SECONDS: i32 = 18 * 60 * 60;
456        let validate_time = |nanos: i64| {
457            if (0..NANOS_PER_DAY).contains(&nanos) {
458                Ok(())
459            } else {
460                Err(GfError::Validation(
461                    "temporal nanoseconds must be within one day".into(),
462                ))
463            }
464        };
465        let validate_offset = |offset: i32| {
466            if (-MAX_OFFSET_SECONDS..=MAX_OFFSET_SECONDS).contains(&offset) {
467                Ok(())
468            } else {
469                Err(GfError::Validation(
470                    "temporal UTC offset must be within plus or minus 18 hours".into(),
471                ))
472            }
473        };
474        match self {
475            Self::Duration { nanos, .. } if !(-999_999_999..=999_999_999).contains(nanos) => {
476                Err(GfError::Validation(
477                    "duration nanoseconds must be between -999999999 and 999999999".into(),
478                ))
479            }
480            Self::LocalDateTime { nanos, .. } | Self::LocalTime { nanos } => validate_time(*nanos),
481            Self::OffsetTime {
482                nanos,
483                offset_seconds,
484            } => {
485                validate_time(*nanos)?;
486                validate_offset(*offset_seconds)
487            }
488            Self::ZonedDateTime {
489                nanos,
490                offset_seconds,
491                zone,
492                ..
493            } => {
494                validate_time(*nanos)?;
495                validate_offset(*offset_seconds)?;
496                if let Some(zone) = zone
497                    && (zone.is_empty() || zone.len() > 255 || zone.chars().any(char::is_control))
498                {
499                    return Err(GfError::Validation(
500                        "temporal zone must be nonempty, control-free UTF-8 up to 255 bytes".into(),
501                    ));
502                }
503                Ok(())
504            }
505            _ => Ok(()),
506        }
507    }
508}
509
510/// Coordinate reference systems certified by GraphForge's spatial v1 profile.
511#[derive(Debug, Clone, PartialEq, Eq, Hash)]
512pub enum SpatialCrs {
513    /// WGS 84 longitude/latitude in canonical x/y order.
514    Epsg4326,
515    /// Web Mercator easting/northing in canonical x/y order.
516    Epsg3857,
517    /// Standards-valid CRS identifier preserved for interchange only.
518    Preserved(String),
519}
520
521impl serde::Serialize for SpatialCrs {
522    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
523    where
524        S: serde::Serializer,
525    {
526        serializer.serialize_str(match self {
527            Self::Epsg4326 => "EPSG:4326",
528            Self::Epsg3857 => "EPSG:3857",
529            Self::Preserved(value) => value,
530        })
531    }
532}
533
534impl<'de> serde::Deserialize<'de> for SpatialCrs {
535    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
536    where
537        D: serde::Deserializer<'de>,
538    {
539        let value = <String as serde::Deserialize>::deserialize(deserializer)?;
540        Ok(match value.as_str() {
541            "EPSG:4326" => Self::Epsg4326,
542            "EPSG:3857" => Self::Epsg3857,
543            _ => Self::Preserved(value),
544        })
545    }
546}
547
548/// Homogeneous geometry kinds in GraphForge's spatial v1 profile.
549#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
550#[serde(rename_all = "snake_case")]
551pub enum SpatialGeometryType {
552    /// One x/y coordinate.
553    Point,
554    /// One ordered sequence of vertices.
555    LineString,
556    /// One polygon represented as ordered rings.
557    Polygon,
558    /// A collection of points.
559    MultiPoint,
560    /// A collection of line strings.
561    MultiLineString,
562    /// A collection of polygons.
563    MultiPolygon,
564}
565
566/// Complete homogeneous spatial property type.
567#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
568pub struct SpatialType {
569    /// Homogeneous geometry kind.
570    pub geometry: SpatialGeometryType,
571    /// Coordinate reference system for every coordinate.
572    pub crs: SpatialCrs,
573}
574
575/// Canonical f64 coordinate payload for one spatial property value.
576#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
577pub enum SpatialCoordinates {
578    /// Point coordinate.
579    Point([f64; 2]),
580    /// Line-string vertices.
581    LineString(Vec<[f64; 2]>),
582    /// Polygon rings and their vertices.
583    Polygon(Vec<Vec<[f64; 2]>>),
584    /// Multi-point coordinates.
585    MultiPoint(Vec<[f64; 2]>),
586    /// Multi-line-string vertices.
587    MultiLineString(Vec<Vec<[f64; 2]>>),
588    /// Multi-polygon rings and vertices.
589    MultiPolygon(Vec<Vec<Vec<[f64; 2]>>>),
590}
591
592/// One canonical typed spatial property value.
593#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
594pub struct SpatialValue {
595    /// Geometry kind and CRS.
596    pub spatial_type: SpatialType,
597    /// Coordinates matching `spatial_type.geometry`.
598    pub coordinates: SpatialCoordinates,
599    /// Original extension name when the value is preserved-only.
600    #[serde(default, skip_serializing_if = "Option::is_none")]
601    pub extension_name: Option<String>,
602    /// Original extension metadata JSON when the value is preserved-only.
603    #[serde(default, skip_serializing_if = "Option::is_none")]
604    pub extension_metadata: Option<String>,
605}
606
607impl SpatialValue {
608    /// Whether this value is preserved for interchange but not certified for computation.
609    #[must_use]
610    pub fn is_preserved_only(&self) -> bool {
611        matches!(self.spatial_type.crs, SpatialCrs::Preserved(_))
612            || self.extension_name.is_some()
613            || self.extension_metadata.is_some()
614    }
615
616    /// Validate the explicit envelope required for preserved-only values.
617    pub fn validate_interchange_profile(&self) -> Result<(), &'static str> {
618        if !self.is_preserved_only() {
619            return Ok(());
620        }
621        let (Some(name), Some(metadata)) = (&self.extension_name, &self.extension_metadata) else {
622            return Err(
623                "preserved-only spatial values require extension_name and extension_metadata",
624            );
625        };
626        if name.is_empty() {
627            return Err("preserved-only spatial extension_name must not be empty");
628        }
629        let trimmed = metadata.trim();
630        let valid_metadata = trimmed.starts_with('{')
631            && trimmed.ends_with('}')
632            && trimmed.contains("\"crs\"")
633            && trimmed.contains(':');
634        if !valid_metadata {
635            return Err("preserved-only spatial extension_metadata must contain a CRS");
636        }
637        Ok(())
638    }
639}
640
641/// A graph property value.
642#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
643#[non_exhaustive]
644pub enum PropValue {
645    /// Null / missing value.
646    Null,
647    /// Boolean.
648    Bool(bool),
649    /// 64-bit signed integer.
650    Int(i64),
651    /// 64-bit float.
652    Float(f64),
653    /// UTF-8 string.
654    Str(String),
655    /// Ordered list.
656    List(Vec<PropValue>),
657    /// Typed temporal value with consumer-neutral Arrow semantics.
658    Temporal(TemporalValue),
659    /// Canonical typed spatial value.
660    Spatial(SpatialValue),
661}
662
663impl fmt::Display for PropValue {
664    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
665        match self {
666            Self::Null => write!(f, "null"),
667            Self::Bool(b) => write!(f, "{b}"),
668            Self::Int(i) => write!(f, "{i}"),
669            Self::Float(fl) => write!(f, "{fl}"),
670            Self::Str(s) => write!(f, "{s}"),
671            Self::List(l) => {
672                write!(f, "[")?;
673                for (i, v) in l.iter().enumerate() {
674                    if i > 0 {
675                        write!(f, ", ")?;
676                    }
677                    write!(f, "{v}")?;
678                }
679                write!(f, "]")
680            }
681            Self::Temporal(value) => write!(f, "temporal({value:?})"),
682            Self::Spatial(value) => write!(
683                f,
684                "spatial({:?}, {:?})",
685                value.spatial_type, value.coordinates
686            ),
687        }
688    }
689}
690
691// ---------------------------------------------------------------------------
692// NodeHandle / EdgeHandle
693// ---------------------------------------------------------------------------
694
695/// Opaque graph-instance identity used to reject handles from another graph.
696#[doc(hidden)]
697#[derive(Clone, Default)]
698pub struct GraphIdentity(Arc<()>);
699
700impl GraphIdentity {
701    /// Create a fresh graph-instance identity.
702    #[must_use]
703    pub fn new() -> Self {
704        Self::default()
705    }
706}
707
708impl fmt::Debug for GraphIdentity {
709    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
710        f.write_str("GraphIdentity(..)")
711    }
712}
713
714/// Opaque UUID handle to a node created by one GraphForge instance.
715#[derive(Debug, Clone)]
716pub struct NodeHandle {
717    /// Stable public node identity.
718    pub uuid: ::uuid::Uuid,
719    /// Primary label.
720    pub label: String,
721    owner: GraphIdentity,
722}
723
724impl NodeHandle {
725    /// Construct a handle owned by `owner`.
726    #[doc(hidden)]
727    #[must_use]
728    pub fn new(uuid: ::uuid::Uuid, label: impl Into<String>, owner: GraphIdentity) -> Self {
729        Self {
730            uuid,
731            label: label.into(),
732            owner,
733        }
734    }
735
736    /// Whether this handle belongs to the supplied graph instance.
737    #[doc(hidden)]
738    #[must_use]
739    pub fn belongs_to(&self, owner: &GraphIdentity) -> bool {
740        Arc::ptr_eq(&self.owner.0, &owner.0)
741    }
742}
743
744impl PartialEq for NodeHandle {
745    fn eq(&self, other: &Self) -> bool {
746        self.uuid == other.uuid
747    }
748}
749
750impl Eq for NodeHandle {}
751
752impl fmt::Display for NodeHandle {
753    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
754        write!(f, "{}(uuid={})", self.label, self.uuid)
755    }
756}
757
758/// Typed public node identity accepted by path algorithms.
759#[derive(Debug, Clone, PartialEq)]
760pub enum NodeSelector {
761    /// Stable UUID identity.
762    Uuid(::uuid::Uuid),
763    /// A UUID handle owned by one graph instance.
764    Handle(NodeHandle),
765    /// A unique node selected by one typed property match within a label.
766    Match {
767        /// Required node label.
768        label: String,
769        /// Required property name.
770        property: String,
771        /// Exact property value.
772        value: PropValue,
773    },
774}
775
776impl NodeSelector {
777    /// Parse a canonical UUID selector with a structured validation error.
778    pub fn uuid(value: &str) -> Result<Self, GfError> {
779        ::uuid::Uuid::parse_str(value)
780            .map(Self::Uuid)
781            .map_err(|_| GfError::Validation(format!("invalid node UUID {value:?}")))
782    }
783}
784
785/// Opaque handle to an edge created via `GraphForge::add_edge`.
786#[derive(Debug, Clone)]
787pub struct EdgeHandle {
788    /// Stable public edge identity.
789    pub uuid: ::uuid::Uuid,
790    /// Relationship type.
791    pub rel_type: String,
792}
793
794impl EdgeHandle {
795    /// Construct a UUID-backed edge handle.
796    #[doc(hidden)]
797    #[must_use]
798    pub fn new(uuid: ::uuid::Uuid, rel_type: impl Into<String>) -> Self {
799        Self {
800            uuid,
801            rel_type: rel_type.into(),
802        }
803    }
804}
805
806impl PartialEq for EdgeHandle {
807    fn eq(&self, other: &Self) -> bool {
808        self.uuid == other.uuid
809    }
810}
811
812impl Eq for EdgeHandle {}
813
814impl fmt::Display for EdgeHandle {
815    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
816        write!(f, "{}(uuid={})", self.rel_type, self.uuid)
817    }
818}
819
820// ---------------------------------------------------------------------------
821// Option structs for analyst verbs and find
822// ---------------------------------------------------------------------------
823
824/// PageRank semantics. Omitted iterations use the existing convergence rule.
825#[derive(Debug, Clone, Copy, PartialEq)]
826pub struct PageRankOptions {
827    /// Finite teleport damping in the inclusive range `[0, 1]`.
828    pub damping: f64,
829    /// Exactly this many synchronous rounds, including zero; `None` converges.
830    pub iterations: Option<u32>,
831}
832
833impl Default for PageRankOptions {
834    fn default() -> Self {
835        Self {
836            damping: 0.85,
837            iterations: None,
838        }
839    }
840}
841
842/// Definition of the local clustering coefficient.
843#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
844pub enum ClusteringNormalization {
845    /// Existing reciprocal-degree (Fagiolo) normalization.
846    #[default]
847    Fagiolo,
848    /// Directed edges among unique in/out neighbors divided by `k * (k - 1)`.
849    NeighborEdges,
850}
851
852/// Deterministic synchronous label propagation, separate from the default
853/// asynchronous community optimizer. Returned labels are never renumbered.
854#[derive(Debug, Clone, PartialEq, Eq)]
855pub struct SynchronousLabelPropagationOptions {
856    /// Exactly this many previous-round label updates, including zero.
857    pub iterations: u32,
858    /// Exact Int64 initial label property. If absent, use selected node ordinals.
859    pub initial_label_property: Option<String>,
860}
861
862/// Options for `GraphForge::rank`.
863#[derive(Debug, Clone)]
864pub struct RankOptions {
865    /// Algorithm name (e.g. `"pagerank"`).
866    pub by: algorithms::RankAlgorithm,
867    /// Optional relationship type filter.
868    pub via: Option<String>,
869    /// Whether to treat edges as directed.
870    pub directed: bool,
871    /// Optional property name to write scores back to nodes.
872    pub write_property: Option<String>,
873    /// PageRank configuration; valid only for `by=pagerank`.
874    pub pagerank: Option<PageRankOptions>,
875    /// LCC definition; valid only for `by=clustering_coefficient`.
876    pub clustering_normalization: Option<ClusteringNormalization>,
877}
878
879impl Default for RankOptions {
880    fn default() -> Self {
881        Self {
882            by: algorithms::RankAlgorithm::default(),
883            via: None,
884            directed: true,
885            write_property: None,
886            pagerank: None,
887            clustering_normalization: None,
888        }
889    }
890}
891
892/// Options for `GraphForge::cluster`.
893#[derive(Debug, Clone, Default)]
894pub struct ClusterOptions {
895    /// Algorithm name (e.g. `"louvain"`).
896    pub by: algorithms::ClusterAlgorithm,
897    /// Node property containing the feature vector for vector clustering.
898    pub vector_property: Option<String>,
899    /// Optional relationship type filter.
900    pub via: Option<String>,
901    /// Whether to treat edges as directed.
902    pub directed: bool,
903    /// Optional property name to write community IDs back to nodes.
904    pub write_property: Option<String>,
905    /// Fixed-round synchronous mode; valid only for `by=label_propagation`.
906    pub synchronous_label_propagation: Option<SynchronousLabelPropagationOptions>,
907}
908
909/// Options for `GraphForge::find`.
910#[derive(Debug, Clone)]
911pub struct FindOptions {
912    /// Optional text query.
913    pub query: Option<String>,
914    /// Node label filter.
915    pub label: Option<String>,
916    /// Optional dense vector for similarity search.
917    pub vector: Option<Vec<f32>>,
918    /// Optional existing graph node whose vector is read from the selected space.
919    pub similar_to: Option<NodeSelector>,
920    /// Optional text embedded with the selected space's compatible provider contract.
921    pub semantic_query: Option<String>,
922    /// Maximum number of results to return.
923    pub limit: usize,
924    /// Vector space identifier (e.g. `"sbert"`).
925    pub space: Option<String>,
926    /// Explicitly allow the last complete substantially stale generation.
927    pub force_stale: bool,
928}
929
930impl Default for FindOptions {
931    fn default() -> Self {
932        Self {
933            query: None,
934            label: None,
935            vector: None,
936            similar_to: None,
937            semantic_query: None,
938            limit: 10,
939            space: None,
940            force_stale: false,
941        }
942    }
943}
944
945/// Options for `GraphForge::paths`.
946#[derive(Debug, Clone)]
947pub struct PathsOptions {
948    /// Algorithm name (e.g. `"dijkstra"`, `"bfs"`, `"max_flow"`).
949    pub by: algorithms::PathAlgorithm,
950    /// Optional relationship type filter.
951    pub via: Option<String>,
952    /// Whether to treat edges as directed.
953    pub directed: bool,
954    /// Number of paths to return (e.g. Yen's k-shortest).
955    pub k: usize,
956    /// Optional edge-weight property name.
957    pub weight: Option<String>,
958    /// Optional graph-native capacity property for flow algorithms.
959    pub capacity_property: Option<String>,
960    /// Required graph-native unit-cost property for min-cost flow algorithms.
961    pub cost_property: Option<String>,
962    /// Optional node property containing an A* heuristic estimate.
963    pub heuristic: Option<String>,
964    /// Maximum number of edge transitions for random-walk paths.
965    pub walk_length: Option<usize>,
966    /// Seed for reproducible random-walk paths.
967    pub seed: Option<u64>,
968    /// Canonical resolved terminal UUIDs for explicit multi-terminal algorithms.
969    pub terminal_uuids: Vec<[u8; 16]>,
970    /// Graph-native node property containing prizes for prize-collecting Steiner trees.
971    pub prize_property: Option<String>,
972}
973
974impl Default for PathsOptions {
975    fn default() -> Self {
976        Self {
977            by: algorithms::PathAlgorithm::Bfs,
978            via: None,
979            directed: true,
980            k: 1,
981            weight: None,
982            capacity_property: None,
983            cost_property: None,
984            heuristic: None,
985            walk_length: None,
986            seed: None,
987            terminal_uuids: Vec::new(),
988            prize_property: None,
989        }
990    }
991}
992
993/// Options for `GraphForge::analyze`.
994#[derive(Debug, Clone)]
995pub struct AnalyzeOptions {
996    /// Algorithm name (e.g. `"minimum_spanning_tree"`, `"is_dag"`).
997    pub by: algorithms::AnalyzeAlgorithm,
998    /// Optional relationship type filter.
999    pub via: Option<String>,
1000    /// Whether to treat edges as directed.
1001    pub directed: bool,
1002    /// Optional edge-weight property name.
1003    pub weight: Option<String>,
1004    /// Requested result count for analyses that enumerate multiple results.
1005    pub k: Option<usize>,
1006    /// Optional node property that identifies a graph partition.
1007    pub partition_property: Option<String>,
1008}
1009
1010impl Default for AnalyzeOptions {
1011    fn default() -> Self {
1012        Self {
1013            by: algorithms::AnalyzeAlgorithm::IsDag,
1014            via: None,
1015            directed: true,
1016            weight: None,
1017            k: None,
1018            partition_property: None,
1019        }
1020    }
1021}
1022
1023/// Options for `GraphForge::similar`.
1024#[derive(Debug, Clone)]
1025pub struct SimilarOptions {
1026    /// Algorithm name (e.g. `"node_similarity"`, `"knn"`, `"cosine"`).
1027    pub by: algorithms::SimilarAlgorithm,
1028    /// Number of neighbours to return.
1029    pub k: usize,
1030    /// Optional vector property for vector-based similarity.
1031    pub vector_property: Option<String>,
1032    /// Optional relationship type filter.
1033    pub via: Option<String>,
1034}
1035
1036impl Default for SimilarOptions {
1037    fn default() -> Self {
1038        Self {
1039            by: algorithms::SimilarAlgorithm::default(),
1040            k: 10,
1041            vector_property: None,
1042            via: None,
1043        }
1044    }
1045}
1046
1047// ---------------------------------------------------------------------------
1048// ExplainStage
1049// ---------------------------------------------------------------------------
1050
1051/// Which compiler stage to inspect through `graphforge_api::GraphForge::explain_stage`.
1052///
1053/// The API facade owns parsing, binding, and planning orchestration. This neutral
1054/// selector does not make the parser depend on later compiler stages.
1055#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1056pub enum ExplainStage {
1057    /// Pretty-printed JSON of the parsed [`graphforge_ast::AstQuery`].
1058    Ast,
1059    /// Bound AST after name resolution.
1060    ///
1061    /// **Deferred** — the binder produces a `GraphPlan` directly and does not
1062    /// annotate the `AstQuery`.  This variant returns [`GfError::NotImplemented`]
1063    /// until a future milestone adds a separate annotation pass.
1064    BoundAst,
1065    /// Serialised [`graphforge_ir::GraphPlan`] produced by the binder.
1066    ///
1067    /// Uses the facade's ontology, mode, procedures, and a private runtime-catalog
1068    /// snapshot, without publishing any newly interned identities.
1069    GraphIr,
1070    /// DataFusion logical plan.
1071    LogicalPlan,
1072    /// DataFusion physical plan, rendered without executing the query.
1073    PhysicalPlan,
1074}
1075
1076// ---------------------------------------------------------------------------
1077// GraphForge — public facade
1078// ---------------------------------------------------------------------------
1079//
1080// The `GraphForge` engine facade lives in the top-level `graphforge-api` crate.
1081// Pipeline crates depend on `graphforge-core`, so core cannot depend on the
1082// compiler and execution crates the facade needs without a dependency cycle.
1083// See #716 / #583. `graphforge-core` keeps the shared value types below
1084// ([`GfError`], [`OntologyMode`], [`NodeHandle`], [`RankOptions`], …) that the
1085// facade composes.
1086
1087#[cfg(test)]
1088mod tests {
1089    use super::*;
1090
1091    #[test]
1092    fn span_display() {
1093        assert_eq!(Span::new(0, 5).to_string(), "0..5");
1094    }
1095
1096    #[test]
1097    fn gf_error_not_implemented() {
1098        let e = GfError::NotImplemented("execute");
1099        assert!(e.to_string().contains("execute"));
1100    }
1101
1102    #[test]
1103    fn node_handle_display() {
1104        let owner = GraphIdentity::new();
1105        let uuid = ::uuid::Uuid::from_bytes([1; 16]);
1106        let h = NodeHandle::new(uuid, "Person", owner.clone());
1107        assert!(h.to_string().contains("Person"));
1108        assert!(h.to_string().contains(&uuid.to_string()));
1109        assert!(h.belongs_to(&owner));
1110        assert!(!h.belongs_to(&GraphIdentity::new()));
1111        assert_eq!(h, NodeHandle::new(uuid, "Other", GraphIdentity::new()));
1112    }
1113
1114    #[test]
1115    fn edge_handle_identity_and_display_are_uuid_based() {
1116        let uuid = ::uuid::Uuid::from_bytes([2; 16]);
1117        let handle = EdgeHandle::new(uuid, "KNOWS");
1118        assert_eq!(handle.uuid, uuid);
1119        assert_eq!(handle.rel_type, "KNOWS");
1120        assert_eq!(handle, EdgeHandle::new(uuid, "OTHER"));
1121        assert_ne!(
1122            handle,
1123            EdgeHandle::new(::uuid::Uuid::from_bytes([3; 16]), "KNOWS"),
1124        );
1125        assert_eq!(handle.to_string(), format!("KNOWS(uuid={uuid})"));
1126        assert!(!handle.to_string().starts_with("Edge(id="));
1127    }
1128
1129    #[test]
1130    fn paths_options_default_to_the_canonical_bfs_contract() {
1131        let options = PathsOptions::default();
1132        assert_eq!(options.by, algorithms::PathAlgorithm::Bfs);
1133        assert_eq!(options.via, None);
1134        assert!(options.directed);
1135        assert_eq!(options.k, 1);
1136        assert_eq!(options.weight, None);
1137        assert!(options.terminal_uuids.is_empty());
1138        assert_eq!(options.prize_property, None);
1139    }
1140
1141    #[test]
1142    fn analyze_options_default_to_the_canonical_is_dag_contract() {
1143        let options = AnalyzeOptions::default();
1144        assert_eq!(options.by, algorithms::AnalyzeAlgorithm::IsDag);
1145        assert_eq!(options.via, None);
1146        assert!(options.directed);
1147    }
1148
1149    #[test]
1150    fn find_options_default_to_no_query_or_stale_override() {
1151        let options = FindOptions::default();
1152        assert_eq!(options.query, None);
1153        assert_eq!(options.label, None);
1154        assert_eq!(options.vector, None);
1155        assert_eq!(options.similar_to, None);
1156        assert_eq!(options.semantic_query, None);
1157        assert_eq!(options.limit, 10);
1158        assert_eq!(options.space, None);
1159        assert!(!options.force_stale);
1160    }
1161
1162    #[test]
1163    fn stable_error_code_enums_cover_every_public_variant() {
1164        let api = [
1165            (ApiErrorCode::NotFound, "GF_NOT_FOUND"),
1166            (ApiErrorCode::Cancelled, "GF_CANCELLED"),
1167            (ApiErrorCode::ResourceLimit, "GF_RESOURCE_LIMIT"),
1168            (ApiErrorCode::PageInvalid, "GF_PAGE_INVALID"),
1169            (ApiErrorCode::PageSnapshotGone, "GF_PAGE_SNAPSHOT_GONE"),
1170            (ApiErrorCode::SchemaMismatch, "GF_SCHEMA_MISMATCH"),
1171            (ApiErrorCode::UnknownArgument, "GF_UNKNOWN_ARGUMENT"),
1172            (ApiErrorCode::AmbiguousProjection, "GF_AMBIGUOUS_PROJECTION"),
1173            (ApiErrorCode::IdentityConflict, "GF_IDENTITY_CONFLICT"),
1174            (
1175                ApiErrorCode::FingerprintCollision,
1176                "GF_FINGERPRINT_COLLISION",
1177            ),
1178            (ApiErrorCode::ResultNotRetained, "GF_RESULT_NOT_RETAINED"),
1179        ];
1180        for (code, spelling) in api {
1181            assert_eq!(code.as_str(), spelling);
1182            assert_eq!(code.to_string(), spelling);
1183        }
1184
1185        let project = [
1186            (
1187                ProjectErrorCode::UnsupportedProjectFormat,
1188                "GF_UNSUPPORTED_PROJECT_FORMAT",
1189            ),
1190            (
1191                ProjectErrorCode::ProjectUninitialized,
1192                "GF_PROJECT_UNINITIALIZED",
1193            ),
1194            (ProjectErrorCode::ProjectCorrupt, "GF_PROJECT_CORRUPT"),
1195            (
1196                ProjectErrorCode::UnsupportedFilesystem,
1197                "GF_UNSUPPORTED_FILESYSTEM",
1198            ),
1199            (ProjectErrorCode::WriterBusy, "GF_WRITER_BUSY"),
1200            (ProjectErrorCode::WriteConflict, "GF_WRITE_CONFLICT"),
1201            (ProjectErrorCode::RebaseExhausted, "GF_REBASE_EXHAUSTED"),
1202            (
1203                ProjectErrorCode::TransactionConflict,
1204                "GF_IDEMPOTENCY_CONFLICT",
1205            ),
1206            (ProjectErrorCode::PublicationFailed, "GF_PUBLICATION_FAILED"),
1207            (
1208                ProjectErrorCode::UnsupportedCapabilityVersion,
1209                "GF_UNSUPPORTED_CAPABILITY_VERSION",
1210            ),
1211            (
1212                ProjectErrorCode::CapabilityDisabled,
1213                "GF_CAPABILITY_DISABLED",
1214            ),
1215            (ProjectErrorCode::TransactionFailed, "GF_TRANSACTION_FAILED"),
1216            (ProjectErrorCode::CheckpointExists, "GF_CHECKPOINT_EXISTS"),
1217            (
1218                ProjectErrorCode::CheckpointNotFound,
1219                "GF_CHECKPOINT_NOT_FOUND",
1220            ),
1221            (
1222                ProjectErrorCode::CheckpointRegistryCorrupt,
1223                "GF_CHECKPOINT_REGISTRY_CORRUPT",
1224            ),
1225            (ProjectErrorCode::ReadOnlyView, "GF_READ_ONLY_VIEW"),
1226            (ProjectErrorCode::ResourceLimit, "GF_RESOURCE_LIMIT"),
1227        ];
1228        for (code, spelling) in project {
1229            assert_eq!(code.as_str(), spelling);
1230            assert_eq!(code.to_string(), spelling);
1231        }
1232    }
1233
1234    #[test]
1235    fn public_value_display_and_selector_validation_cover_all_shapes() {
1236        let values = [
1237            (PropValue::Null, "null"),
1238            (PropValue::Bool(true), "true"),
1239            (PropValue::Int(-7), "-7"),
1240            (PropValue::Float(1.5), "1.5"),
1241            (PropValue::Str("x".into()), "x"),
1242            (
1243                PropValue::List(vec![PropValue::Int(1), PropValue::Null]),
1244                "[1, null]",
1245            ),
1246        ];
1247        for (value, rendered) in values {
1248            assert_eq!(value.to_string(), rendered);
1249        }
1250        assert!(matches!(
1251            NodeSelector::uuid("00000000-0000-0000-0000-000000000001"),
1252            Ok(NodeSelector::Uuid(_))
1253        ));
1254        assert!(matches!(
1255            NodeSelector::uuid("not-a-uuid"),
1256            Err(GfError::Validation(_))
1257        ));
1258        assert_eq!(format!("{:?}", GraphIdentity::new()), "GraphIdentity(..)");
1259    }
1260
1261    #[test]
1262    fn every_gf_error_fault_domain_has_a_stable_code() {
1263        let span = Span::new(1, 2);
1264        let errors = [
1265            (GfError::NotImplemented("x"), "GF_NOT_IMPLEMENTED"),
1266            (
1267                GfError::Parse {
1268                    diagnostic: None,
1269                    msg: "x".into(),
1270                    span,
1271                },
1272                "GF_PARSE",
1273            ),
1274            (
1275                GfError::Bind {
1276                    diagnostics: Vec::new(),
1277                    msg: "x".into(),
1278                    span,
1279                },
1280                "GF_PARSE",
1281            ),
1282            (GfError::Plan("x".into()), "GF_PLAN"),
1283            (GfError::Execution("x".into()), "GF_EXECUTION"),
1284            (
1285                GfError::Provider {
1286                    class: "c".into(),
1287                    provider: "p".into(),
1288                    model: "m".into(),
1289                },
1290                "GF_EXECUTION",
1291            ),
1292            (GfError::Storage("x".into()), "GF_IO"),
1293            (
1294                GfError::Project {
1295                    code: ProjectErrorCode::ProjectCorrupt,
1296                    message: "x".into(),
1297                },
1298                "GF_PROJECT_CORRUPT",
1299            ),
1300            (
1301                GfError::Api {
1302                    code: ApiErrorCode::NotFound,
1303                    message: "x".into(),
1304                },
1305                "GF_NOT_FOUND",
1306            ),
1307            (GfError::Lifecycle("x".into()), "GF_LIFECYCLE"),
1308            (GfError::Validation("x".into()), "GF_VALIDATION"),
1309            (GfError::Ontology("x".into()), "GF_ONTOLOGY"),
1310        ];
1311        for (error, code) in errors {
1312            assert_eq!(error.code(), code);
1313        }
1314    }
1315}
1316
1317/// Neutral portable package contracts.
1318pub mod portable;
1319
1320/// Identity-free public storage receipts.
1321pub mod storage_receipt;
1322
1323/// Optional process-wide SHA-256 work observation for engine diagnostics.
1324pub mod hash_observation;