Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"] }
thiserror = "2"
# Required by dialect::Upgrade / DialectUrn (versioned wire-contract
# decoding, forward-only minor/patch compat) — small, stable, always-on.
semver = "1"
anyhow = "1"
schemars = { version = "0.8", features = ["derive"] }
rpassword = "7"
Expand All @@ -47,12 +50,21 @@ pyo3 = { version = "0.27", features = ["extension-module"], optional = true }
# renderers. Pinned to an exact version: small bus factor (2 contributors,
# 0.x semver) per this epic's own crate-evaluation research.
scxml = { version = "=0.2.2", optional = true }
# ReqIF/ReqIFz parsing (kr0ki M2 -> ufo-types migration, 2026-09-20) — a
# direct Rust port of the StrictDoc `reqif` reference implementation.
# Optional, matching `sysml-v2-parser`/`scxml`'s existing posture: `reqrs`
# is young (created 2026-05-28, pre-1.0, single apparent maintainer) and
# this crate does not put young/small-bus-factor dependencies in its
# default feature set. Consumers that want ReqIF ingestion opt in with
# `features = ["reqif"]`; kr0ki-core does.
reqrs = { version = "0.2.2", optional = true }

[features]
sysml = ["dep:sysml-v2-parser"]
python = ["dep:pyo3", "sysml"]
statechart = ["dep:scxml"]
keyring = ["dep:keyring"]
reqif = ["dep:reqrs"]

[dev-dependencies]
serde_json = "1.0"
Expand Down
117 changes: 117 additions & 0 deletions src/dialect.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
//! Versioned wire-contract identity and forward-only decode.
//!
//! Sits *beside* the canonical typed layer, never inside it: per
//! `kr0ki`'s `docs/DESIGN-NOTE-typed-model-layer.md` §2.8, a canonical type
//! (`sysgraph::SysGraph`, `mbse::requirements::RequirementGraph`) carries no
//! `{major, minor}` field of its own — "stability is enforced by golden
//! fixtures + sha256 content hashes + frozen wire constants... a version
//! field invites the drift the fixture discipline exists to prevent."
//! [`DialectUrn`] and [`Upgrade`] name and decode a *wire artifact* that
//! produces one of those types; they never become a field on the type
//! itself.
//!
//! Distinct from an *adapter* (e.g. `reqif`'s ReqIF-XML -> `RequirementGraph`
//! lowering): an adapter crosses from a foreign format this crate does not
//! own the evolution of into a canonical type, once, with no version
//! negotiation. [`Upgrade`] is for decoding the *same* dialect — a format
//! this crate owns both ends of — at a historical minor/patch version
//! forward to the current shape.

use std::fmt;

/// A stable identity for one versioned wire dialect of a canonical type.
///
/// Renders as `urn:b00t:dialect:<crate>:<path>:<semver>`, e.g.
/// `urn:b00t:dialect:ufo-types:mbse::requirements:0.15.0` — unifying the two
/// ad hoc conventions already in use in kr0ki-core before this module
/// existed: `b00t.type/reqif-import/v1` (envelope identity, major-only) and
/// `ufo-types/0.15.0:mbse::requirements` (crate + full semver + path).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DialectUrn {
pub crate_name: &'static str,
pub path: &'static str,
pub version: semver::Version,
}

impl fmt::Display for DialectUrn {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"urn:b00t:dialect:{}:{}:{}",
self.crate_name, self.path, self.version
)
}
}

/// Why an [`Upgrade::upgrade`] call failed.
#[derive(Debug, thiserror::Error)]
pub enum DialectError {
/// `from_version`'s major component does not match the target type's
/// `Upgrade::DIALECT` major. A major bump is a breaking wire change —
/// `upgrade()` never attempts it; the caller needs an explicit,
/// separately-versioned migration instead.
#[error(
"dialect major-version mismatch: bytes declare {from_version} but {dialect} only upgrades within major {expected_major}"
)]
MajorMismatch {
from_version: semver::Version,
// Boxed: `DialectUrn` embeds a `semver::Version` too, and clippy's
// `result_large_err` flags `Result<Self, DialectError>` (this is
// `Upgrade::upgrade`'s return type) as too large to return by value
// otherwise.
dialect: Box<DialectUrn>,
expected_major: u64,
},
/// The bytes did not decode as this dialect at the declared version.
#[error("malformed {dialect} payload at declared version {from_version}: {reason}")]
Malformed {
dialect: Box<DialectUrn>,
from_version: semver::Version,
reason: String,
},
}

/// Forward-only decode: turn versioned wire bytes into the current
/// in-process value.
///
/// There is deliberately no `downgrade()` on this trait. Concurrency is
/// structural rather than incidental: `upgrade()` is a plain function per
/// call with no shared mutable dispatch state, so many callers can decode
/// many sources at many declared versions at once with zero contention —
/// e.g. a `b00t://` loader iterating several concurrently-registered
/// sources, each at its own historical minor/patch version of the same
/// dialect.
pub trait Upgrade: Sized {
/// This type's current dialect identity. Bump the version here as this
/// type's wire contract evolves within the same major; existing
/// `from_version` handling in `upgrade()` accrues, it never drops
/// support for a version this type has shipped at the current major.
const DIALECT: DialectUrn;

/// Decode `bytes`, produced against `from_version` of this same
/// dialect. Returns [`DialectError::MajorMismatch`] without attempting
/// a decode when the majors differ. Every `from_version` this type has
/// ever shipped at the current major MUST succeed — enforced by a
/// golden-fixture round-trip test per historical version, the same bar
/// `kr0ki-core`'s `normalized_graph_sha256` already sets for a single
/// version, applied per version instead of once.
fn upgrade(bytes: &[u8], from_version: &semver::Version) -> Result<Self, DialectError>;
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn dialect_urn_renders_as_urn_b00t_dialect() {
let urn = DialectUrn {
crate_name: "ufo-types",
path: "mbse::requirements",
version: semver::Version::new(0, 15, 0),
};
assert_eq!(
urn.to_string(),
"urn:b00t:dialect:ufo-types:mbse::requirements:0.15.0"
);
}
}
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ pub mod capability;
pub mod coherence;
pub mod dare;
pub mod data_format;
pub mod dialect;
pub mod iso;
pub mod iso_ir;
pub mod mbse;
Expand All @@ -169,6 +170,8 @@ pub mod pipeline_secrets;
pub mod pipeline_types;
#[cfg(feature = "python")]
mod python;
#[cfg(feature = "reqif")]
pub mod reqif;
pub mod satisfies;
#[cfg(feature = "statechart")]
pub mod statechart;
Expand Down
14 changes: 14 additions & 0 deletions src/mbse/requirements.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ use std::collections::{BTreeMap, BTreeSet, VecDeque};

use serde::{Deserialize, Serialize};

use crate::stereotype::{Stereotyped, UfoStereotype};

/// An immutable ReqIF/Flexo baseline identity.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BaselineIdentity {
Expand Down Expand Up @@ -63,6 +65,18 @@ pub struct Requirement {
pub evidence: Vec<EvidenceRef>,
}

/// A `Requirement` is UFO-A rigid and sortal: an entity cannot lose its
/// identity and remain a requirement (contrast a `Role`, which can be
/// gained or lost without changing identity — see `stereotype.rs`'s
/// classification table). `Kind`, not `SubKind`: this is the requirement
/// vocabulary's own root type, not a further specialization of some other
/// `Kind` already in the ontology.
impl Stereotyped for Requirement {
fn ufo_stereotype(&self) -> UfoStereotype {
UfoStereotype::Kind("Requirement".to_string())
}
}

/// The asserted relation vocabulary shared by ReqIF, Flexo, and OPA adapters.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
Expand Down
Loading
Loading