#![expect(
clippy::disallowed_types,
reason = "dev/verification tooling over JSON artifacts (the catalogue, results, wire \
exchanges) — not the application (#1694)"
)]
use crate::artifacts::ArtifactSet;
use crate::exec::driver::HttpDriver;
use crate::exec::{CaseRecord, RowOutcome, run_case};
use crate::ids::{CapabilityName, InstanceName, SmOperationRef};
use crate::ixit::Ixit;
use crate::model::assertion::assertion_refs;
use crate::model::case::{CaseCore, PartyRelationshipRequirement};
use crate::refgrammar::{IxitField, ValueRef};
use crate::vocab::CaseStatus;
#[derive(Debug, Clone, serde::Serialize)]
#[serde(rename_all = "snake_case", tag = "kind", content = "detail")]
pub enum Exception {
Unrealized(String),
ContentGeneration(String),
Guarded(String),
Status(String),
}
#[derive(Debug, Default)]
pub struct RunReport {
pub records: Vec<CaseRecord>,
pub exceptions: Vec<(crate::ids::CaseId, Exception)>,
pub interpreter_run: usize,
pub considered: usize,
pub restapi_specs_version: Option<String>,
}
impl RunReport {
#[must_use]
pub fn interpreter_coverage(&self) -> f64 {
if self.considered == 0 {
return 1.0;
}
#[expect(
clippy::as_conversions,
clippy::cast_precision_loss,
reason = "case counts << 2^52"
)]
{
self.interpreter_run as f64 / self.considered as f64
}
}
}
pub(crate) fn fully_unrealized(set: &ArtifactSet, case: &CaseCore) -> Option<String> {
let anchor = case.sm_operation.as_ref()?;
let mut citations = Vec::new();
for step in &case.flow {
let op = if step.call.contains('.') {
SmOperationRef::parse(&step.call).ok()?
} else {
anchor.sibling(&step.call)
};
let binding = set
.bindings
.iter()
.map(|(_, b)| b)
.find(|b| b.sm_operation == op)?;
if let Some(decl) = &binding.unrealized {
citations.push(format!("{op}: {}", decl.ambiguity));
}
}
(!citations.is_empty()).then(|| citations.join("; "))
}
fn step_binding<'a>(
set: &'a ArtifactSet,
case: &CaseCore,
step: &crate::model::case::FlowStep,
) -> Option<&'a crate::model::binding::OperationBinding> {
let op = if step.call.contains('.') {
SmOperationRef::parse(&step.call).ok()?
} else {
case.sm_operation.as_ref()?.sibling(&step.call)
};
let mut bindings = set.bindings.iter().map(|(_, b)| b);
if let Some(variant) = step.variant.as_deref()
&& let Some(exact) = bindings
.clone()
.find(|b| b.sm_operation == op && b.variant.as_deref() == Some(variant))
{
return Some(exact);
}
bindings.find(|b| b.sm_operation == op && b.variant.is_none())
}
fn unmet_binding_floors(
set: &ArtifactSet,
case: &CaseCore,
versions: &crate::party::SpecVersions,
) -> Vec<String> {
let mut unmet = Vec::new();
for step in &case.flow {
let Some(binding) = step_binding(set, case, step) else {
continue;
};
let Some(applies) = &binding.applies else {
continue;
};
if applies.satisfied_by(versions) {
continue;
}
let declared: Vec<String> = applies
.entries()
.into_iter()
.map(|(component, range)| format!("{} {}", component.token(), range.raw()))
.collect();
let citation = format!("{} requires {}", binding.sm_operation, declared.join(", "));
if !unmet.contains(&citation) {
unmet.push(citation);
}
}
unmet
}
fn extension_family(set: &ArtifactSet, case: &CaseCore) -> Option<String> {
let anchor = case.sm_operation.as_ref()?;
for step in &case.flow {
let op = if step.call.contains('.') {
SmOperationRef::parse(&step.call).ok()?
} else {
anchor.sibling(&step.call)
};
if let Some(decl) = set
.bindings
.iter()
.map(|(_, b)| b)
.find(|b| b.sm_operation == op)
.and_then(|b| b.extension.as_ref())
{
return Some(format!("{}; {}", decl.family, decl.ambiguity));
}
}
None
}
fn capabilities_claiming_family(set: &ArtifactSet, family: &str) -> Vec<CapabilityName> {
let mut claiming: Vec<CapabilityName> = Vec::new();
for (_, case) in &set.cases {
if !extension_family(set, case)
.is_some_and(|marker| marker.starts_with(&format!("{family};")))
{
continue;
}
for capability in &case.capabilities {
if !claiming.contains(capability) {
claiming.push(capability.clone());
}
}
}
claiming
}
fn unservable_import(
set: &ArtifactSet,
statement: Option<&crate::party::Statement>,
case: &CaseCore,
) -> Result<Option<String>, String> {
if !matches!(
case.requires.import,
Some(crate::model::case::ImportRequirement::Received { .. })
) {
return Ok(None);
}
unservable_provisioning(
set,
statement,
&[
"I_EHR_EXTRACT_SERVICE.import_ehr_extract",
"I_EHR_EXTRACT_SERVICE.import_ehr",
],
"requires.import",
"the received version this case reads cannot exist here",
)
}
fn unservable_party_relationship(
set: &ArtifactSet,
statement: Option<&crate::party::Statement>,
case: &CaseCore,
) -> Result<Option<String>, String> {
if !matches!(
case.requires.party_relationship,
Some(PartyRelationshipRequirement::Exists { .. })
) {
return Ok(None);
}
unservable_provisioning(
set,
statement,
&["I_DEMOGRAPHIC_SERVICE.create_party_relationship"],
"requires.party_relationship",
"the relationship this case reads cannot exist here",
)
}
fn unservable_provisioning(
set: &ArtifactSet,
statement: Option<&crate::party::Statement>,
operations: &[&str],
requirement: &str,
consequence: &str,
) -> Result<Option<String>, String> {
let Some(statement) = statement else {
return Ok(None);
};
let mut parsed: Vec<SmOperationRef> = Vec::with_capacity(operations.len());
for call in operations {
parsed.push(SmOperationRef::parse(call).map_err(|e| {
format!("interpreter defect: selection-law SM operation anchor {call:?}: {e}")
})?);
}
let Some(decl) = parsed.iter().find_map(|op| {
set.bindings
.iter()
.map(|(_, b)| b)
.find(|b| b.sm_operation == *op)
.and_then(|b| b.extension.as_ref())
}) else {
return Ok(None);
};
let claiming = capabilities_claiming_family(set, &decl.family);
if claiming
.iter()
.any(|c| statement.claims.capabilities.contains(c))
{
return Ok(None);
}
Ok(Some(format!(
"{requirement} provisions over the {} extension routes ({}): the ICS claims none of \
the capabilities those routes' cases gate ({}), and no openEHR specification governs \
them, so {consequence}",
decl.family,
decl.ambiguity,
claiming
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
)))
}
fn undeclared_ixit_facts(case: &CaseCore, ixit: &Ixit) -> Vec<&'static str> {
fn note(reference: &ValueRef, into: &mut Vec<IxitField>) {
if let ValueRef::Ixit(field) = reference
&& !into.contains(field)
{
into.push(*field);
}
}
let mut referenced: Vec<IxitField> = Vec::new();
for step in &case.flow {
for (_, value) in step.with_entries() {
for reference in value.refs() {
note(reference, &mut referenced);
}
}
for assertion in &step.assertions {
for reference in assertion_refs(assertion) {
note(&reference, &mut referenced);
}
}
}
for assertion in &case.postconditions {
for reference in assertion_refs(assertion) {
note(&reference, &mut referenced);
}
}
referenced
.into_iter()
.filter(|field| match field {
IxitField::SystemId => ixit.system_id.is_none(),
IxitField::DumpLocation => ixit.dump_location.is_none(),
})
.map(IxitField::token)
.collect()
}
fn undeclared_instances(case: &CaseCore, ixit: &Ixit) -> Vec<String> {
let mut missing: Vec<String> = Vec::new();
for step in &case.flow {
if let Some(name) = &step.on
&& ixit.instance(name).is_none()
&& !missing.iter().any(|m| m == name.as_str())
{
missing.push(name.as_str().to_owned());
}
}
missing
}
pub(crate) fn addressed_instances(case: &CaseCore) -> Vec<InstanceName> {
let mut named: Vec<InstanceName> = Vec::new();
let mut any_default = false;
for step in &case.flow {
match &step.on {
Some(name) if !named.contains(name) => named.push(name.clone()),
Some(_) => {}
None => any_default = true,
}
}
if any_default && let Ok(default) = InstanceName::parse("sut") {
named.insert(0, default);
}
named
}
fn unsatisfied_terminology(case: &CaseCore, ixit: &Ixit) -> Option<String> {
let required = case.requires.terminology.as_ref()?;
for name in addressed_instances(case) {
let Some(instance) = ixit.instance(&name) else {
continue;
};
let Some(lane) = ixit.terminology_of(instance) else {
return Some(format!(
"instance {name}: the ixit declares no `terminology` posture — the case needs a \
deployment wired to a terminology query server (BASE master12 §Binding \
Terminology Value-sets to Archetypes), and no released operation discloses one"
));
};
if let Some(posture) = required.posture
&& lane.posture != posture
{
return Some(format!(
"instance {name}: the case needs the `{}` unresolvable-value-set posture and this \
deployment declares `{}` (register AMB-172 — a deployment realizes exactly one)",
posture.token(),
lane.posture.token()
));
}
for namespace in &required.served {
match lane.server_for(namespace) {
Some(server) if server.is_reachable() => {}
Some(server) => {
return Some(format!(
"instance {name}: terminology namespace {namespace} is declared on server \
'{}', which the ixit declares unreachable — the case needs it answered",
server.name
));
}
None => {
return Some(format!(
"instance {name}: no declared terminology server answers for {namespace} \
— the party seeded no such namespace"
));
}
}
}
for namespace in &required.unreachable {
match lane.server_for(namespace) {
Some(server) if server.is_reachable() => {
return Some(format!(
"instance {name}: terminology namespace {namespace} is declared reachable \
on server '{}' — the case needs the terminology-server-down branch, \
which only a declared-unreachable server provides",
server.name
));
}
Some(_) => {}
None => {
return Some(format!(
"instance {name}: no declared terminology server answers for {namespace} \
— the party declares no such unreachable namespace"
));
}
}
}
if let Some(minimum) = required.distinct_servers {
let count = lane.distinct_reachable_servers(&required.served);
if count < minimum {
return Some(format!(
"instance {name}: the case needs {minimum} distinct reachable terminology \
servers across its namespaces and the ixit declares {count} (BASE master12 \
§Overview — several terminologies served at the same time)"
));
}
}
}
None
}
fn unsatisfied_spec_profile(case: &CaseCore, ixit: &Ixit) -> Option<String> {
let per_instance = case.requires.instances.as_ref();
for name in addressed_instances(case) {
let Some(instance) = ixit.instance(&name) else {
continue;
};
let required = per_instance
.and_then(|map| map.get(&name))
.and_then(|requires| requires.spec_profile)
.or(case.requires.spec_profile);
let Some(required) = required else {
continue;
};
match ixit.spec_profile_of(instance) {
None => {
return Some(format!(
"instance {name}: the ixit declares no `spec_profile` — the case's \
expectation rests on the `{}` generation set, and no released operation \
discloses which set a deployment runs (openEHR release strategy: a minor \
release is a compatible superset, so the sets differ only in accepted \
surface)",
required.token()
));
}
Some(declared) if declared != required => {
return Some(format!(
"instance {name}: the case needs the `{}` specification generation set and \
this deployment declares `{}` — one running server implements exactly one",
required.token(),
declared.token()
));
}
Some(_) => {}
}
}
None
}
const SMART_PSEUDO_INTERFACE: &str = "I_ITS_REST_SMART";
fn needs_smart_lane(case: &CaseCore) -> bool {
case.flow
.iter()
.any(crate::model::case::FlowStep::declares_scopes)
|| case
.sm_operation
.as_ref()
.is_some_and(|op| op.interface() == SMART_PSEUDO_INTERFACE)
}
fn not_applicable_record(case: &CaseCore, citation: &str) -> CaseRecord {
CaseRecord {
case: case.id.clone(),
format: None,
rows: vec![RowOutcome::NotApplicable {
citation: citation.to_owned(),
}],
rows_driven: 0,
rows_total: crate::exec::row_count(case),
}
}
fn unserved_extension(
set: &ArtifactSet,
statement: Option<&crate::party::Statement>,
case: &CaseCore,
) -> Result<Option<String>, String> {
if let Some(stmt) = statement
&& let Some(family) = extension_family(set, case)
&& !case
.capabilities
.iter()
.any(|c| stmt.claims.capabilities.contains(c))
{
return Ok(Some(format!(
"extension realization ({family}): the ICS claims none of this case's \
capabilities, and no openEHR specification governs the route — ISO/IEC 9646 \
test selection"
)));
}
let citation = match unservable_import(set, statement, case)? {
Some(citation) => Some(citation),
None => unservable_party_relationship(set, statement, case)?,
};
Ok(citation.map(|citation| format!("{citation} — ISO/IEC 9646 test selection")))
}
fn unclaimed_capabilities(
statement: Option<&crate::party::Statement>,
case: &CaseCore,
) -> Option<String> {
let statement = statement?;
if case.capabilities.is_empty()
|| case
.capabilities
.iter()
.any(|c| statement.claims.capabilities.contains(c))
{
return None;
}
Some(format!(
"the ICS claims none of the capabilities this case gates ({}) — CNF profiles \
master02-overview.adoc §Overview (a profile IS the list of capabilities a solution \
specifies); ISO/IEC 9646 test selection",
case.capabilities
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
))
}
fn selection_exception(
set: &ArtifactSet,
ixit: &Ixit,
statement: Option<&crate::party::Statement>,
case: &CaseCore,
) -> Result<Option<Exception>, String> {
if let Some(citation) = fully_unrealized(set, case) {
return Ok(Some(Exception::Unrealized(citation)));
}
if let Some(citation) = unserved_extension(set, statement, case)? {
return Ok(Some(Exception::Unrealized(citation)));
}
if let Some(citation) = unclaimed_capabilities(statement, case) {
return Ok(Some(Exception::Guarded(citation)));
}
if let Some(stmt) = statement
&& let Some(tag) = &case.option
&& !stmt.options.contains(tag)
{
return Ok(Some(Exception::Unrealized(format!(
"option {tag}: the ICS does not declare this register branch \
(statement.options) — ISO/IEC 9646 test selection"
))));
}
if let Some(stmt) = statement
&& !case.applies.satisfied_by(&stmt.spec_versions)
{
let declared: Vec<String> = case
.applies
.entries()
.into_iter()
.map(|(component, range)| format!("{} {}", component.token(), range.raw()))
.collect();
return Ok(Some(Exception::Unrealized(format!(
"case version floor unmet ({}) — the party's declared spec versions do not \
satisfy the case's applies ranges; ISO/IEC 9646 test selection",
declared.join(", ")
))));
}
if let Some(stmt) = statement {
let unmet = unmet_binding_floors(set, case, &stmt.spec_versions);
if !unmet.is_empty() {
return Ok(Some(Exception::Unrealized(format!(
"operation version floor unmet ({}) — the party's declared spec versions \
predate the release that introduced this wire; ISO/IEC 9646 test selection",
unmet.join("; ")
))));
}
}
if needs_smart_lane(case) && ixit.smart.is_none() {
return Ok(Some(Exception::Guarded(
"the ixit declares no `smart` lane — the case needs a SMART-enabled \
deployment and a minted, scope-carrying access token, neither of which any \
released operation discloses or provides; ISO/IEC 9646 test selection"
.to_owned(),
)));
}
if let Some(citation) = unsatisfied_terminology(case, ixit) {
return Ok(Some(Exception::Guarded(format!(
"{citation}; ISO/IEC 9646 test selection"
))));
}
if let Some(citation) = unsatisfied_spec_profile(case, ixit) {
return Ok(Some(Exception::Guarded(format!(
"{citation}; ISO/IEC 9646 test selection"
))));
}
let missing_instances = undeclared_instances(case, ixit);
if !missing_instances.is_empty() {
return Ok(Some(Exception::Guarded(format!(
"the ixit declares no instance {} — the case's flow addresses it with `on:` and \
this party runs no such deployment/principal; ISO/IEC 9646 test selection",
missing_instances.join(", ")
))));
}
let missing = undeclared_ixit_facts(case, ixit);
if !missing.is_empty() {
return Ok(Some(Exception::Guarded(format!(
"the ixit declares no {} — the case reads it as ${{ixit:…}} and no released \
operation discloses the value; ISO/IEC 9646 test selection",
missing.join(", ")
))));
}
if matches!(
case.requires.server,
Some(crate::vocab::ServerState::Exclusive)
) && !ixit
.environment
.as_ref()
.is_some_and(|env| env.exclusive_server)
{
return Ok(Some(Exception::Unrealized(
"requires.server: exclusive — the ixit declares a shared SUT instance \
(environment.exclusive_server: false); the global-state ground cannot \
be established"
.to_owned(),
)));
}
Ok(None)
}
pub fn execute(
set: &ArtifactSet,
ixit: &Ixit,
statement: Option<&crate::party::Statement>,
) -> Result<RunReport, String> {
let mut report = RunReport::default();
let mut ordered: Vec<&CaseCore> = set.cases.iter().map(|(_, c)| c).collect();
ordered.sort_by_key(|c| {
!matches!(
c.requires.server,
Some(crate::vocab::ServerState::Exclusive)
)
});
for case in ordered {
report.considered += 1;
if !matches!(case.status, CaseStatus::Active) {
report.exceptions.push((
case.id.clone(),
Exception::Status(format!("{:?} — never verdict-bearing", case.status)),
));
continue;
}
if let Some(exception) = selection_exception(set, ixit, statement, case)? {
let citation = match &exception {
Exception::Unrealized(c)
| Exception::ContentGeneration(c)
| Exception::Guarded(c)
| Exception::Status(c) => c.clone(),
};
report.records.push(not_applicable_record(case, &citation));
report.exceptions.push((case.id.clone(), exception));
continue;
}
let runnable = if matches!(case.kind, crate::vocab::CaseKind::Content) {
synthesize_content_case(case)
} else {
case.clone()
};
let mut driver = HttpDriver::new(set, ixit, statement.map(|s| &s.spec_versions))?;
let record = run_case(&runnable, runnable.formats.first().copied(), &mut driver)?;
report.interpreter_run += 1;
report.records.push(record);
if let Some(version) = driver.take_observed_restapi_specs_version() {
report.restapi_specs_version.get_or_insert(version);
}
}
Ok(report)
}
#[must_use]
pub fn coverage_accounting(set: &ArtifactSet) -> RunReport {
let mut report = RunReport::default();
let mut ordered: Vec<&CaseCore> = set.cases.iter().map(|(_, c)| c).collect();
ordered.sort_by_key(|c| {
!matches!(
c.requires.server,
Some(crate::vocab::ServerState::Exclusive)
)
});
for case in ordered {
report.considered += 1;
if !matches!(case.status, CaseStatus::Active) {
report.exceptions.push((
case.id.clone(),
Exception::Status(format!("{:?}", case.status)),
));
continue;
}
if let Some(citation) = fully_unrealized(set, case) {
report
.exceptions
.push((case.id.clone(), Exception::Unrealized(citation)));
continue;
}
report.interpreter_run += 1;
}
report
}
fn refused_at_parse(columns: &[String], row: &[serde_json::Value]) -> bool {
columns
.iter()
.position(|column| column == "violates")
.and_then(|index| row.get(index))
.and_then(serde_json::Value::as_array)
.is_some_and(|violations| {
violations
.iter()
.filter_map(serde_json::Value::as_str)
.any(|violation| {
violation.starts_with("rm_schema:") && violation.contains("mandatory")
})
})
}
#[must_use]
pub fn synthesize_content_case(case: &CaseCore) -> CaseCore {
let mut synthesized = case.clone();
let Some(table) = &case.decision_table else {
return synthesized;
};
let columns = table.columns.clone();
let rows: Vec<Vec<crate::model::case::MatrixCell>> = table
.rows
.iter()
.map(|row| {
columns
.iter()
.zip(row)
.map(|(column, cell)| {
if column == "expected" {
let kind = match cell.as_str() {
Some("accepted") => "created",
_ if refused_at_parse(&columns, row) => "bad_request",
_ => "validation_failed",
};
crate::model::case::MatrixCell::Literal(serde_json::Value::String(
kind.to_owned(),
))
} else {
match cell {
serde_json::Value::Null => crate::model::case::MatrixCell::Null,
other => crate::model::case::MatrixCell::Literal(other.clone()),
}
}
})
.collect()
})
.collect();
synthesized.parameters = serde_json::from_value(serde_json::json!({
"iteration": "reset_per_row",
"matrix": { "columns": columns, "rows": [] }
}))
.ok();
if let Some(parameters) = &mut synthesized.parameters
&& let Some(matrix) = &mut parameters.matrix
{
matrix.rows = rows;
}
if let Some(context) = &case.constraint_context {
synthesized.requires.server = Some(crate::vocab::ServerState::Any);
synthesized.requires.templates = if context.constraint_columns.is_empty() {
vec![context.template.clone()]
} else {
Vec::new()
};
synthesized.requires.ehr = Some(crate::model::case::EhrRequirement::Exists {
commits: crate::model::case::CommitState::None,
});
}
synthesized.sm_operation = SmOperationRef::parse("I_EHR_COMPOSITION.create_composition").ok();
synthesized.flow = serde_json::from_value(serde_json::json!([
{
"step": 1,
"call": "create_composition",
"with": { "ehr_id": "${ehr_id}", "composition": "${recipe:content_instance(row)}" },
"expect": "created"
}
]))
.unwrap_or_default();
synthesized
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_rejected_row_splits_on_its_violation_class() {
let case: CaseCore = serde_json::from_value(serde_json::json!({
"id": "CONT-DV_URI-validate_open", "kind": "content", "component": "CONTENT",
"rm_class": "DV_URI",
"test_purpose": "t", "description": "d", "spec_refs": [],
"decision_table": {
"columns": ["value", "expected", "violates"],
"rows": [
[null, "rejected", ["rm_schema: value is mandatory"]],
["xyz", "rejected", ["rm_schema: value is not a valid RFC 3986 URI"]],
["x", "rejected", ["constraint(pattern)"]],
["y", "rejected", ["rm_invariant(DV_URI.Value_valid)"]],
["z", "rejected", ["iso8601"]],
["ftp://ftp.is.co.za/rfc/rfc1808.txt", "accepted", []]
]
}
}))
.unwrap();
let synthesized = synthesize_content_case(&case);
let matrix = synthesized
.parameters
.as_ref()
.and_then(|p| p.matrix.as_ref())
.expect("the decision table becomes a parameters matrix");
let column = matrix
.columns
.iter()
.position(|c| c == "expected")
.expect("the reserved `expected` column survives synthesis");
let kinds: Vec<&str> = matrix
.rows
.iter()
.map(|row| match row.get(column) {
Some(crate::model::case::MatrixCell::Literal(serde_json::Value::String(s))) => {
s.as_str()
}
other => panic!("row expectation is not a literal kind: {other:?}"),
})
.collect();
assert_eq!(
kinds,
[
"bad_request",
"validation_failed",
"validation_failed",
"validation_failed",
"validation_failed",
"created",
]
);
}
#[test]
fn operation_version_floors_are_enforced_at_selection() {
let floored: crate::model::binding::OperationBinding =
serde_json::from_value(serde_json::json!({
"sm_operation": "I_DEFINITION_ADL14.list_opts",
"its": "its-rest",
"applies": { "its_rest": ">=1.1.0" },
"request": { "method": "GET", "path": "/definition/template/adl1.4" },
"outcomes": { "ok": { "status": 200 } }
}))
.unwrap();
let case: CaseCore = serde_json::from_value(serde_json::json!({
"id": "X-floored", "kind": "functional", "component": "DEFINITION_ADL14",
"sm_operation": "I_DEFINITION_ADL14.list_opts",
"test_purpose": "t", "description": "d", "spec_refs": [],
"flow": [{ "step": 1, "call": "list_opts", "expect": "ok" }]
}))
.unwrap();
let mut set = ArtifactSet::default();
set.bindings
.push((std::path::PathBuf::from("b.yaml"), floored));
let old = crate::party::SpecVersions {
its_rest: Some("1.0.3".to_owned()),
..crate::party::SpecVersions::default()
};
let unmet = unmet_binding_floors(&set, &case, &old);
assert_eq!(unmet.len(), 1, "{unmet:?}");
assert!(unmet[0].contains("I_DEFINITION_ADL14.list_opts"));
assert!(unmet[0].contains(">=1.1.0"));
let current = crate::party::SpecVersions {
its_rest: Some("1.1.0".to_owned()),
..crate::party::SpecVersions::default()
};
assert!(unmet_binding_floors(&set, &case, ¤t).is_empty());
let unfloored: crate::model::binding::OperationBinding =
serde_json::from_value(serde_json::json!({
"sm_operation": "I_DEFINITION_ADL14.list_opts",
"its": "its-rest",
"request": { "method": "GET", "path": "/definition/template/adl1.4" },
"outcomes": { "ok": { "status": 200 } }
}))
.unwrap();
let mut plain = ArtifactSet::default();
plain
.bindings
.push((std::path::PathBuf::from("b.yaml"), unfloored));
assert!(unmet_binding_floors(&plain, &case, &old).is_empty());
}
#[test]
fn extension_realizations_are_marked_with_their_family_and_register_entry() {
let extension: crate::model::binding::OperationBinding =
serde_json::from_value(serde_json::json!({
"sm_operation": "I_PARTY_RELATIONSHIP.get_party_relationship",
"its": "its-rest",
"extension": {
"family": "party-relationship",
"reason": "the release surfaces no PARTY_RELATIONSHIP resource",
"source": "SM i_party_relationship.adoc vs ITS-REST demographic.openapi.yaml",
"ambiguity": "AMB-32"
},
"request": { "method": "GET", "path": "/demographic/party_relationship/{versioned_object_uid}" },
"outcomes": { "ok": { "status": 200 } }
}))
.unwrap();
let released: crate::model::binding::OperationBinding =
serde_json::from_value(serde_json::json!({
"sm_operation": "I_DEFINITION_ADL14.list_opts",
"its": "its-rest",
"request": { "method": "GET", "path": "/definition/template/adl1.4" },
"outcomes": { "ok": { "status": 200 } }
}))
.unwrap();
let mut set = ArtifactSet::default();
set.bindings
.push((std::path::PathBuf::from("e.yaml"), extension));
set.bindings
.push((std::path::PathBuf::from("r.yaml"), released));
let on_extension: CaseCore = serde_json::from_value(serde_json::json!({
"id": "X-extension", "kind": "functional", "component": "DEMOGRAPHIC",
"sm_operation": "I_PARTY_RELATIONSHIP.get_party_relationship",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["PartyRelationshipOperations"],
"flow": [{ "step": 1, "call": "get_party_relationship", "expect": "ok" }]
}))
.unwrap();
let marker = extension_family(&set, &on_extension).expect("an extension marker");
assert!(marker.contains("party-relationship"), "{marker}");
assert!(
marker.contains("AMB-32"),
"the citation must stay register-linked: {marker}"
);
let on_released: CaseCore = serde_json::from_value(serde_json::json!({
"id": "X-released", "kind": "functional", "component": "DEFINITION_ADL14",
"sm_operation": "I_DEFINITION_ADL14.list_opts",
"test_purpose": "t", "description": "d", "spec_refs": [],
"flow": [{ "step": 1, "call": "list_opts", "expect": "ok" }]
}))
.unwrap();
assert!(extension_family(&set, &on_released).is_none());
}
#[test]
fn an_import_precondition_is_scoped_to_the_party_that_serves_the_family() {
let import: crate::model::binding::OperationBinding =
serde_json::from_value(serde_json::json!({
"sm_operation": "I_EHR_EXTRACT_SERVICE.import_ehr_extract",
"its": "its-rest",
"extension": {
"family": "message-extract",
"reason": "the release publishes no MESSAGE API",
"source": "SM master09 vs the released ITS-REST groups",
"ambiguity": "AMB-34"
},
"request": { "method": "POST", "path": "/message/import/{an_ehr_id}" },
"outcomes": { "updated": { "status": 204 } }
}))
.unwrap();
let released: crate::model::binding::OperationBinding =
serde_json::from_value(serde_json::json!({
"sm_operation": "I_EHR_COMPOSITION.get_versioned_composition",
"its": "its-rest",
"request": { "method": "GET", "path": "/ehr/{ehr_id}/versioned_composition/{versioned_object_uid}/version/{version_uid}" },
"outcomes": { "ok": { "status": 200 } }
}))
.unwrap();
let importer: CaseCore = serde_json::from_value(serde_json::json!({
"id": "X-import", "kind": "functional", "component": "MESSAGING",
"sm_operation": "I_EHR_EXTRACT_SERVICE.import_ehr_extract",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["EhrExtract"],
"flow": [{ "step": 1, "call": "import_ehr_extract", "expect": "updated" }]
}))
.unwrap();
let mut set = ArtifactSet::default();
set.bindings
.push((std::path::PathBuf::from("i.yaml"), import));
set.bindings
.push((std::path::PathBuf::from("r.yaml"), released));
set.cases
.push((std::path::PathBuf::from("i-case.yaml"), importer));
let reader: CaseCore = serde_json::from_value(serde_json::json!({
"id": "X-read", "kind": "functional", "component": "EHR_COMPOSITION",
"sm_operation": "I_EHR_COMPOSITION.get_versioned_composition",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["Versioning"],
"requires": {
"ehr": { "commits": "none" },
"import": {
"extract": "cnf.messaging.ehr_extract.v1",
"container": "X_VERSIONED_COMPOSITION"
}
},
"flow": [{ "step": 1, "call": "get_versioned_composition", "expect": "ok" }]
}))
.unwrap();
let statement = |caps: &[&str]| -> crate::party::Statement {
serde_json::from_value(serde_json::json!({
"product": { "name": "p", "version": "1", "vendor": "v", "identifier": "i" },
"schedule_release": "CNF-2.0",
"spec_versions": { "rm": "1.2.0", "its_rest": "1.1.0" },
"claims": { "capabilities": caps, "profiles": ["CORE"] },
"tech_profiles": [ { "its": "its-rest", "formats": ["canonical-json"] } ],
"options": []
}))
.unwrap()
};
let serving = statement(&["EhrExtract", "Versioning"]);
assert!(
unservable_import(&set, Some(&serving), &reader)
.expect("well-formed anchors")
.is_none(),
"a party claiming the family's capability drives the case"
);
let read_only = statement(&["Versioning"]);
let citation = unservable_import(&set, Some(&read_only), &reader)
.expect("well-formed anchors")
.expect("excused with a citation");
assert!(citation.contains("message-extract"), "{citation}");
assert!(
citation.contains("AMB-34"),
"the citation stays register-linked: {citation}"
);
let plain: CaseCore = serde_json::from_value(serde_json::json!({
"id": "X-plain", "kind": "functional", "component": "EHR_COMPOSITION",
"sm_operation": "I_EHR_COMPOSITION.get_versioned_composition",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["Versioning"],
"flow": [{ "step": 1, "call": "get_versioned_composition", "expect": "ok" }]
}))
.unwrap();
assert!(
unservable_import(&set, Some(&read_only), &plain)
.expect("well-formed anchors")
.is_none()
);
}
#[test]
fn a_party_relationship_precondition_is_scoped_at_selection() {
let create: crate::model::binding::OperationBinding =
serde_json::from_value(serde_json::json!({
"sm_operation": "I_DEMOGRAPHIC_SERVICE.create_party_relationship",
"its": "its-rest",
"extension": {
"family": "party-relationship",
"reason": "the release surfaces no PARTY_RELATIONSHIP resource",
"source": "SM docs/UML/classes/i_demographic_service.adoc",
"ambiguity": "AMB-32"
},
"request": { "method": "POST", "path": "/demographic/party_relationship" },
"outcomes": { "created": { "status": 201 } }
}))
.unwrap();
let released: crate::model::binding::OperationBinding =
serde_json::from_value(serde_json::json!({
"sm_operation": "I_PARTY.get_party",
"its": "its-rest",
"request": { "method": "GET", "path": "/demographic/party/{party_id}" },
"outcomes": { "ok": { "status": 200 } }
}))
.unwrap();
let creator: CaseCore = serde_json::from_value(serde_json::json!({
"id": "X-rel", "kind": "functional", "component": "DEMOGRAPHIC",
"sm_operation": "I_DEMOGRAPHIC_SERVICE.create_party_relationship",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["PartyRelationships"],
"flow": [{ "step": 1, "call": "create_party_relationship", "expect": "created" }]
}))
.unwrap();
let mut set = ArtifactSet::default();
set.bindings
.push((std::path::PathBuf::from("c.yaml"), create));
set.bindings
.push((std::path::PathBuf::from("r.yaml"), released));
set.cases
.push((std::path::PathBuf::from("c-case.yaml"), creator));
let reader: CaseCore = serde_json::from_value(serde_json::json!({
"id": "X-party-read", "kind": "functional", "component": "DEMOGRAPHIC",
"sm_operation": "I_PARTY.get_party",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["Demographics"],
"requires": {
"party_relationship": {
"source": "cnf.demographic.person.v1",
"target": "cnf.demographic.organisation.v1",
"relationship": "cnf.demographic.party_relationship.v1"
}
},
"flow": [{ "step": 1, "call": "get_party", "expect": "ok" }]
}))
.unwrap();
let statement = |caps: &[&str]| -> crate::party::Statement {
serde_json::from_value(serde_json::json!({
"product": { "name": "p", "version": "1", "vendor": "v", "identifier": "i" },
"schedule_release": "CNF-2.0",
"spec_versions": { "rm": "1.2.0", "its_rest": "1.1.0" },
"claims": { "capabilities": caps, "profiles": ["CORE"] },
"tech_profiles": [ { "its": "its-rest", "formats": ["canonical-json"] } ],
"options": []
}))
.unwrap()
};
let serving = statement(&["PartyRelationships", "Demographics"]);
assert!(
unservable_party_relationship(&set, Some(&serving), &reader)
.expect("well-formed anchors")
.is_none(),
"a party claiming the family's capability drives the case"
);
let read_only = statement(&["Demographics"]);
let citation = unservable_party_relationship(&set, Some(&read_only), &reader)
.expect("well-formed anchors")
.expect("excused with a citation");
assert!(citation.contains("party-relationship"), "{citation}");
assert!(
citation.contains("AMB-32"),
"the citation stays register-linked: {citation}"
);
let plain: CaseCore = serde_json::from_value(serde_json::json!({
"id": "X-plain-party", "kind": "functional", "component": "DEMOGRAPHIC",
"sm_operation": "I_PARTY.get_party",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["Demographics"],
"requires": { "party_relationship": "none" },
"flow": [{ "step": 1, "call": "get_party", "expect": "ok" }]
}))
.unwrap();
assert!(
unservable_party_relationship(&set, Some(&read_only), &plain)
.expect("well-formed anchors")
.is_none()
);
}
#[test]
fn smart_lane_need_is_declared_not_guessed() {
let plain: CaseCore = serde_json::from_value(serde_json::json!({
"id": "X-plain", "kind": "functional", "component": "SECURITY",
"sm_operation": "I_DEFINITION_ADL14.list_opts",
"test_purpose": "t", "description": "d", "spec_refs": [],
"flow": [{ "step": 1, "call": "list_opts", "expect": "ok" }]
}))
.unwrap();
assert!(!needs_smart_lane(&plain));
let scoped: CaseCore = serde_json::from_value(serde_json::json!({
"id": "X-scoped", "kind": "functional", "component": "SECURITY",
"sm_operation": "I_DEFINITION_ADL14.list_opts",
"test_purpose": "t", "description": "d", "spec_refs": [],
"flow": [{ "step": 1, "call": "list_opts", "scopes": [], "expect": "forbidden" }]
}))
.unwrap();
assert!(
needs_smart_lane(&scoped),
"an EMPTY scopes declaration is still a SMART-lane declaration"
);
let discovery: CaseCore = serde_json::from_value(serde_json::json!({
"id": "X-discovery", "kind": "functional", "component": "SECURITY",
"sm_operation": "I_ITS_REST_SMART.discovery",
"test_purpose": "t", "description": "d", "spec_refs": [],
"flow": [{ "step": 1, "call": "discovery", "expect": "ok" }]
}))
.unwrap();
assert!(needs_smart_lane(&discovery));
}
#[test]
fn undeclared_addressed_instances_are_collected() {
let case: CaseCore = serde_json::from_value(serde_json::json!({
"id": "X-two-deployments", "kind": "functional", "component": "SECURITY",
"sm_operation": "I_DEFINITION_ADL14.list_opts",
"test_purpose": "t", "description": "d", "spec_refs": [],
"flow": [
{ "step": 1, "call": "list_opts", "expect": "ok" },
{ "step": 2, "call": "list_opts", "on": "sut_pgp", "expect": "ok" },
{ "step": 3, "call": "list_opts", "on": "sut_pgp", "expect": "ok" }
]
}))
.unwrap();
let without: Ixit = serde_json::from_value(serde_json::json!({
"instances": { "sut": { "base_url": "http://x", "auth": { "mode": "none" } } }
}))
.unwrap();
assert_eq!(undeclared_instances(&case, &without), vec!["sut_pgp"]);
let with: Ixit = serde_json::from_value(serde_json::json!({
"instances": {
"sut": { "base_url": "http://x", "auth": { "mode": "none" } },
"sut_pgp": { "base_url": "http://y", "auth": { "mode": "none" } }
}
}))
.unwrap();
assert!(undeclared_instances(&case, &with).is_empty());
}
#[test]
fn terminology_requirements_are_selected_against_the_declaration() {
let case = |requirement: serde_json::Value| -> CaseCore {
serde_json::from_value(serde_json::json!({
"id": "X-terminology", "kind": "functional", "component": "QUERY",
"sm_operation": "I_QUERY_SERVICE.execute_ad_hoc_query",
"test_purpose": "t", "description": "d", "spec_refs": [],
"requires": { "server": "any", "terminology": requirement },
"flow": [{ "step": 1, "call": "execute_ad_hoc_query", "expect": "ok" }]
}))
.unwrap()
};
let ixit: Ixit = serde_json::from_value(serde_json::json!({
"instances": { "sut": { "base_url": "http://x", "auth": { "mode": "none" } } },
"terminology": {
"posture": "fail_open",
"servers": [
{ "name": "sct", "namespaces": ["urn:cnf:sct"] },
{ "name": "loinc", "namespaces": ["urn:cnf:loinc"] },
{ "name": "down", "reachable": false, "namespaces": ["urn:cnf:down"] }
]
}
}))
.unwrap();
let plain: CaseCore = serde_json::from_value(serde_json::json!({
"id": "X-plain", "kind": "functional", "component": "QUERY",
"sm_operation": "I_QUERY_SERVICE.execute_ad_hoc_query",
"test_purpose": "t", "description": "d", "spec_refs": [],
"flow": [{ "step": 1, "call": "execute_ad_hoc_query", "expect": "ok" }]
}))
.unwrap();
assert!(unsatisfied_terminology(&plain, &ixit).is_none());
assert!(
unsatisfied_terminology(
&case(serde_json::json!({
"posture": "fail_open",
"served": ["urn:cnf:sct", "urn:cnf:loinc"],
"distinct_servers": 2
})),
&ixit
)
.is_none()
);
let undeclared: Ixit = serde_json::from_value(serde_json::json!({
"instances": { "sut": { "base_url": "http://x", "auth": { "mode": "none" } } }
}))
.unwrap();
let citation = unsatisfied_terminology(
&case(serde_json::json!({ "served": ["urn:cnf:sct"] })),
&undeclared,
)
.expect("undeclared lane is a selection outcome");
assert!(
citation.contains("declares no `terminology` posture"),
"{citation}"
);
let citation = unsatisfied_terminology(
&case(serde_json::json!({ "posture": "fail_closed" })),
&ixit,
)
.expect("posture mismatch is a selection outcome");
assert!(citation.contains("fail_closed"), "{citation}");
assert!(
unsatisfied_terminology(
&case(serde_json::json!({ "served": ["urn:cnf:absent"] })),
&ixit
)
.is_some_and(|c| c.contains("urn:cnf:absent"))
);
assert!(
unsatisfied_terminology(
&case(serde_json::json!({ "unreachable": ["urn:cnf:down"] })),
&ixit
)
.is_none()
);
assert!(
unsatisfied_terminology(
&case(serde_json::json!({ "unreachable": ["urn:cnf:sct"] })),
&ixit
)
.is_some_and(|c| c.contains("declared reachable"))
);
assert!(
unsatisfied_terminology(
&case(serde_json::json!({
"served": ["urn:cnf:sct"], "distinct_servers": 2
})),
&ixit
)
.is_some_and(|c| c.contains("2 distinct reachable"))
);
}
#[test]
fn spec_profile_requirements_are_selected_against_the_declaration() {
let case = |requires: serde_json::Value| -> CaseCore {
serde_json::from_value(serde_json::json!({
"id": "X-profile", "kind": "functional", "component": "EHR_COMPOSITION",
"sm_operation": "I_EHR_COMPOSITION.get_composition_at_version",
"test_purpose": "t", "description": "d", "spec_refs": [],
"requires": requires,
"flow": [
{ "step": 1, "call": "get_composition_at_version", "expect": "ok" },
{ "step": 2, "call": "get_composition_at_version",
"on": "sut_stable", "expect": "conflict" }
]
}))
.unwrap()
};
let ixit: Ixit = serde_json::from_value(serde_json::json!({
"instances": {
"sut": { "base_url": "http://x", "auth": { "mode": "none" } },
"sut_stable": { "base_url": "http://y", "auth": { "mode": "none" },
"spec_profile": "stable" }
},
"spec_profile": "development"
}))
.unwrap();
assert!(unsatisfied_spec_profile(&case(serde_json::json!({})), &ixit).is_none());
assert!(
unsatisfied_spec_profile(
&case(serde_json::json!({
"spec_profile": "development",
"instances": { "sut_stable": { "spec_profile": "stable" } }
})),
&ixit
)
.is_none()
);
assert!(
unsatisfied_spec_profile(
&case(serde_json::json!({
"instances": { "sut_stable": { "spec_profile": "development" } }
})),
&ixit
)
.is_some_and(|c| c.contains("sut_stable") && c.contains("exactly one"))
);
let citation = unsatisfied_spec_profile(
&case(serde_json::json!({ "spec_profile": "stable" })),
&ixit,
)
.expect("set mismatch is a selection outcome");
assert!(
citation.contains("`stable`") && citation.contains("`development`"),
"{citation}"
);
let undeclared: Ixit = serde_json::from_value(serde_json::json!({
"instances": {
"sut": { "base_url": "http://x", "auth": { "mode": "none" } },
"sut_stable": { "base_url": "http://y", "auth": { "mode": "none" } }
}
}))
.unwrap();
let citation = unsatisfied_spec_profile(
&case(serde_json::json!({ "spec_profile": "development" })),
&undeclared,
)
.expect("undeclared set is a selection outcome");
assert!(
citation.contains("declares no `spec_profile`"),
"{citation}"
);
}
#[test]
fn coverage_gate_holds_on_the_committed_catalogue() {
let crate_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
let loaded = crate::artifacts::load_root(&crate_dir.join("artifacts")).unwrap();
assert!(loaded.errors.is_empty());
let report = coverage_accounting(&loaded.set);
let governed = report.interpreter_run
+ report
.exceptions
.iter()
.filter(|(_, e)| matches!(e, Exception::Unrealized(_)))
.count();
#[expect(
clippy::as_conversions,
clippy::cast_precision_loss,
reason = "case counts << 2^52, so the coverage ratio is exact enough"
)]
let coverage = governed as f64 / report.considered as f64;
assert!(
coverage >= 0.80,
"interpreter-governed coverage {coverage:.3} below the floor; exceptions: {:#?}",
report.exceptions.len()
);
for (case, exception) in &report.exceptions {
let text = format!("{exception:?}");
assert!(!text.is_empty(), "{case}: silent exception");
}
}
fn statement(capabilities: &[&str]) -> crate::party::Statement {
serde_json::from_value(serde_json::json!({
"product": { "name": "p", "version": "1", "vendor": "v", "identifier": "i" },
"schedule_release": "CNF-2.0",
"spec_versions": { "rm": "1.2.0", "its_rest": "1.1.0" },
"claims": { "capabilities": capabilities, "profiles": ["CORE"] },
"tech_profiles": [ { "its": "its-rest", "formats": ["canonical-json"] } ],
"options": []
}))
.unwrap()
}
#[test]
fn a_case_gating_only_unclaimed_capabilities_is_selected_away() {
let signing: CaseCore = serde_json::from_value(serde_json::json!({
"id": "SIG-VERSION-ehr_status_signature", "kind": "functional", "component": "EHR",
"sm_operation": "I_EHR_STATUS.get_ehr_status",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["Signing"],
"flow": [{ "step": 1, "call": "get_ehr_status", "expect": "ok" }]
}))
.unwrap();
let claimed = statement(&["EhrOperations", "Signing"]);
let unclaimed = statement(&["EhrOperations"]);
let citation = unclaimed_capabilities(Some(&unclaimed), &signing)
.expect("an unclaimed capability takes the case out of scope");
assert!(citation.contains("Signing"), "{citation}");
assert!(citation.contains("ISO/IEC 9646"), "{citation}");
assert!(unclaimed_capabilities(Some(&claimed), &signing).is_none());
assert!(unclaimed_capabilities(None, &signing).is_none());
let mut partly = signing.clone();
partly.capabilities = vec![
CapabilityName::parse("Signing").unwrap(),
CapabilityName::parse("EhrOperations").unwrap(),
];
assert!(unclaimed_capabilities(Some(&unclaimed), &partly).is_none());
let mut capability_less = signing;
capability_less.capabilities.clear();
assert!(unclaimed_capabilities(Some(&unclaimed), &capability_less).is_none());
}
}