Integrate release recovery, semantic review, coverage and Windows packaging
This commit is contained in:
@@ -0,0 +1 @@
|
||||
include!("../../build/windows_manifest.rs");
|
||||
@@ -402,6 +402,37 @@ fn checked_in_source_project() -> SourceBundle {
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_architecture_example_warnings(compiled: &crate::source_project::CompiledSourceProject) {
|
||||
// This architecture-only example intentionally has no process/scenario
|
||||
// context. Preserve its explicit incompleteness instead of inventing work.
|
||||
let expected: std::collections::BTreeSet<_> = [
|
||||
("unspecified_exchange_mechanism", "oa_report_data"),
|
||||
("unspecified_exchange_mechanism", "sa_report_data"),
|
||||
("work_without_behavior_context", "report_distress"),
|
||||
("work_without_behavior_context", "assess_incident"),
|
||||
("work_without_behavior_context", "transmit_report"),
|
||||
("work_without_behavior_context", "assess_report"),
|
||||
("work_without_behavior_context", "review_response"),
|
||||
("exchange_without_behavior_context", "distress_signal"),
|
||||
("exchange_without_behavior_context", "validated_report"),
|
||||
("capability_without_behavior_context", "protect_life"),
|
||||
("capability_without_behavior_context", "coordinate_response"),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(code, alias)| (code, Some(identity(&compiled.decoded.project, alias))))
|
||||
.collect();
|
||||
let actual: std::collections::BTreeSet<_> = compiled
|
||||
.diagnostics
|
||||
.iter()
|
||||
.map(|issue| {
|
||||
assert_eq!(issue.diagnostic.severity, validation::Severity::Warning);
|
||||
(issue.diagnostic.code.as_str(), issue.diagnostic.object_id)
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(actual, expected);
|
||||
assert_eq!(compiled.diagnostics.len(), expected.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checked_in_nested_example_has_stable_ids_typed_exchanges_and_three_clear_views() {
|
||||
let input = checked_in_source_project();
|
||||
@@ -428,15 +459,7 @@ fn checked_in_nested_example_has_stable_ids_typed_exchanges_and_three_clear_view
|
||||
let reopened = compile_bundle(&compiled.bundle, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(reopened.bundle, compiled.bundle);
|
||||
assert!(reopened.decoded.migration.is_none());
|
||||
assert_eq!(compiled.diagnostics.len(), 2);
|
||||
assert!(
|
||||
compiled
|
||||
.diagnostics
|
||||
.iter()
|
||||
.all(|issue| issue.diagnostic.code == "unspecified_exchange_mechanism"),
|
||||
"{:?}",
|
||||
compiled.diagnostics
|
||||
);
|
||||
assert_architecture_example_warnings(&compiled);
|
||||
let project = &compiled.decoded.project;
|
||||
assert_eq!(project.diagrams.len(), 3);
|
||||
let assess = identity(project, "assess_report");
|
||||
@@ -495,15 +518,7 @@ fn checked_in_example_supports_cross_file_reparent_without_changing_allocations(
|
||||
ProjectLimits::default(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(result.diagnostics.len(), 2);
|
||||
assert!(
|
||||
result
|
||||
.diagnostics
|
||||
.iter()
|
||||
.all(|issue| issue.diagnostic.code == "unspecified_exchange_mechanism"),
|
||||
"{:?}",
|
||||
result.diagnostics
|
||||
);
|
||||
assert_architecture_example_warnings(&result);
|
||||
assert_eq!(
|
||||
result.index.declarations[&assess].path,
|
||||
"sa/assurance.arcadia"
|
||||
|
||||
@@ -53,7 +53,12 @@ pub fn state_header_height(p: &Project, element: Id) -> f32 {
|
||||
if *source == element)
|
||||
})
|
||||
.count();
|
||||
64. + if actions { 28. } else { 0. } + internal as f32 * 56.
|
||||
let typed_actions = p
|
||||
.relations
|
||||
.values()
|
||||
.filter(|r| r.kind() == RelationKind::StateAction && r.source() == element)
|
||||
.count();
|
||||
64. + if actions { 28. } else { 0. } + typed_actions as f32 * 28. + internal as f32 * 56.
|
||||
}
|
||||
|
||||
pub(crate) fn node_size(p: &Project, e: &Element, size: Size, collapsed: bool) -> Size {
|
||||
|
||||
@@ -350,10 +350,7 @@ pub(crate) fn write_fact(
|
||||
else {
|
||||
return Err("Target has no message annotations".into());
|
||||
};
|
||||
values.retain(|v| AnnotationKey::from(v) != *key);
|
||||
if let Some(row) = row {
|
||||
values.push(row);
|
||||
}
|
||||
replace_keyed_row(values, row, |v| AnnotationKey::from(v) == *key);
|
||||
Ok(())
|
||||
}
|
||||
FacetReview::Binding { keys, review } => {
|
||||
@@ -374,10 +371,7 @@ pub(crate) fn write_fact(
|
||||
else {
|
||||
return Err("Target has no scenario bindings".into());
|
||||
};
|
||||
bindings.retain(|v| v.referenced != key);
|
||||
if let Some(row) = row {
|
||||
bindings.push(row);
|
||||
}
|
||||
replace_keyed_row(bindings, row, |v| v.referenced == key);
|
||||
Ok(())
|
||||
}
|
||||
FacetReview::Child { keys, review } => {
|
||||
@@ -467,6 +461,27 @@ pub(crate) fn write_fact(
|
||||
}
|
||||
}
|
||||
|
||||
// Reviewing an unordered row does not authorize changing its authored position.
|
||||
// Preserve surviving entries in place; only genuinely new rows append.
|
||||
fn replace_keyed_row<T: PartialEq>(
|
||||
rows: &mut Vec<T>,
|
||||
row: Option<T>,
|
||||
matches: impl FnMut(&T) -> bool,
|
||||
) {
|
||||
match (rows.iter().position(matches), row) {
|
||||
(Some(index), Some(row)) => {
|
||||
if rows[index] != row {
|
||||
rows[index] = row;
|
||||
}
|
||||
}
|
||||
(Some(index), None) => {
|
||||
rows.remove(index);
|
||||
}
|
||||
(None, Some(row)) => rows.push(row),
|
||||
(None, None) => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn constructor_contains(group: &FacetSelector, facet: &FacetSelector) -> bool {
|
||||
use FacetSelector as F;
|
||||
match group {
|
||||
|
||||
@@ -2,6 +2,8 @@ use crate::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
mod coverage;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Severity {
|
||||
Error,
|
||||
@@ -179,6 +181,7 @@ fn check_ids(out: &mut Vec<Issue>, owner: Id, field: &str, ids: &[Id]) {
|
||||
|
||||
pub fn validate(p: &Project) -> Vec<Issue> {
|
||||
let mut out = Vec::new();
|
||||
let coverage = coverage::Index::new(p);
|
||||
if p.schema_version != SCHEMA_VERSION {
|
||||
error(
|
||||
&mut out,
|
||||
@@ -346,19 +349,15 @@ pub fn validate(p: &Project) -> Vec<Issue> {
|
||||
);
|
||||
}
|
||||
if matches!(e.kind(), Kind::Capability | Kind::SystemCapability)
|
||||
&& !p.relations.values().any(|r| {
|
||||
matches!(
|
||||
r.kind(),
|
||||
RelationKind::CapabilityInvolvement | RelationKind::ChainInvolvement
|
||||
) && r.target() == e.id
|
||||
})
|
||||
&& !coverage.grouping.contains(&e.id)
|
||||
&& !coverage.supported_capabilities.contains(&e.id)
|
||||
{
|
||||
warning(
|
||||
&mut out,
|
||||
e.id,
|
||||
"unsupported_capability",
|
||||
"involvement",
|
||||
"Connect the capability to involved participants, work, or a process/chain.",
|
||||
"Connect the capability to involved participants, work, a process/chain, or an explicit scenario context.",
|
||||
);
|
||||
}
|
||||
if let Some(steps) = e.steps() {
|
||||
@@ -921,6 +920,9 @@ pub fn validate(p: &Project) -> Vec<Issue> {
|
||||
);
|
||||
}
|
||||
}
|
||||
if !out.iter().any(|issue| issue.severity == Severity::Error) {
|
||||
coverage.validate(p, &mut out);
|
||||
}
|
||||
out
|
||||
}
|
||||
fn validate_sequence(p: &Project, e: &Element, steps: &[Id], out: &mut Vec<Issue>) {
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
//! Advisory presence of explicit behavior evidence, never inferred execution.
|
||||
use super::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct Index {
|
||||
pub grouping: BTreeSet<Id>,
|
||||
pub supported_capabilities: BTreeSet<Id>,
|
||||
contextual_capabilities: BTreeSet<Id>,
|
||||
supported_missions: BTreeSet<Id>,
|
||||
}
|
||||
|
||||
impl Index {
|
||||
pub fn new(p: &Project) -> Self {
|
||||
let mut index = Self::default();
|
||||
for element in p.elements.values() {
|
||||
if let Some(parent) = element.parent.and_then(|id| p.elements.get(&id))
|
||||
&& parent.kind() == element.kind()
|
||||
&& parent.layer() == element.layer()
|
||||
{
|
||||
index.grouping.insert(parent.id);
|
||||
}
|
||||
if let Some(SharedBody::Scenario {
|
||||
context: Some(id), ..
|
||||
}) = element.shared()
|
||||
&& p.elements.get(id).is_some_and(|context| {
|
||||
matches!(context.kind(), Kind::Capability | Kind::SystemCapability)
|
||||
&& context.layer() == element.layer()
|
||||
})
|
||||
{
|
||||
index.contextual_capabilities.insert(*id);
|
||||
index.supported_capabilities.insert(*id);
|
||||
}
|
||||
}
|
||||
for relation in p.relations.values() {
|
||||
let (Some(source), Some(target)) = (
|
||||
p.elements.get(&relation.source()),
|
||||
p.elements.get(&relation.target()),
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
if source.layer() != target.layer()
|
||||
|| !relation.kind().permits(source.kind(), target.kind())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
match relation.kind() {
|
||||
RelationKind::CapabilityInvolvement => {
|
||||
index.supported_capabilities.insert(target.id);
|
||||
}
|
||||
RelationKind::ChainInvolvement => {
|
||||
index.supported_capabilities.insert(target.id);
|
||||
index.contextual_capabilities.insert(target.id);
|
||||
}
|
||||
RelationKind::MissionCapability => {
|
||||
index.supported_missions.insert(source.id);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
index
|
||||
}
|
||||
|
||||
pub fn validate(&self, p: &Project, out: &mut Vec<Issue>) {
|
||||
let (work, exchanges) = included_evidence(p);
|
||||
for element in p.elements.values() {
|
||||
if element.kind().is_work()
|
||||
&& !self.grouping.contains(&element.id)
|
||||
&& !work.contains(&element.id)
|
||||
{
|
||||
warning(
|
||||
out,
|
||||
element.id,
|
||||
"work_without_behavior_context",
|
||||
"",
|
||||
"Include this leaf activity/function in a process/chain or scenario through a work action or exchange occurrence.",
|
||||
);
|
||||
}
|
||||
if matches!(element.kind(), Kind::OperationalMission | Kind::Mission)
|
||||
&& !self.supported_missions.contains(&element.id)
|
||||
{
|
||||
warning(
|
||||
out,
|
||||
element.id,
|
||||
"mission_without_capability",
|
||||
"",
|
||||
"Connect this mission to a capability that delivers it.",
|
||||
);
|
||||
}
|
||||
if matches!(element.kind(), Kind::Capability | Kind::SystemCapability)
|
||||
&& !self.grouping.contains(&element.id)
|
||||
&& self.supported_capabilities.contains(&element.id)
|
||||
&& !self.contextual_capabilities.contains(&element.id)
|
||||
{
|
||||
warning(
|
||||
out,
|
||||
element.id,
|
||||
"capability_without_behavior_context",
|
||||
"",
|
||||
"Add an explicit behavior context: link a process/chain with Chain involvement, or set a scenario's Context to this capability.",
|
||||
);
|
||||
}
|
||||
}
|
||||
for relation in p.relations.values().filter(|r| work_exchange(r.kind())) {
|
||||
let mut non_leaf = false;
|
||||
for (field, endpoint) in [("source", relation.source()), ("target", relation.target())]
|
||||
{
|
||||
if self.grouping.contains(&endpoint) {
|
||||
non_leaf = true;
|
||||
warning(
|
||||
out,
|
||||
relation.id,
|
||||
"non_leaf_work_exchange",
|
||||
field,
|
||||
"Choose the leaf activity/function responsible for this exchange; keep any typed port owned by that function.",
|
||||
);
|
||||
}
|
||||
}
|
||||
if !non_leaf && !exchanges.contains(&relation.id) {
|
||||
warning(
|
||||
out,
|
||||
relation.id,
|
||||
"exchange_without_behavior_context",
|
||||
"",
|
||||
"Use this exchange in a process/chain or scenario.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn work_exchange(kind: RelationKind) -> bool {
|
||||
matches!(
|
||||
kind,
|
||||
RelationKind::Interaction | RelationKind::FunctionalExchange
|
||||
)
|
||||
}
|
||||
|
||||
/// Only ordered, owned occurrences contribute. Each identity is visited once,
|
||||
/// including reused containers; this remains bounded even on malformed drafts.
|
||||
fn included_evidence(p: &Project) -> (BTreeSet<Id>, BTreeSet<Id>) {
|
||||
let mut pending: Vec<_> = p
|
||||
.elements
|
||||
.values()
|
||||
.filter(|e| {
|
||||
matches!(
|
||||
e.kind(),
|
||||
Kind::Process | Kind::FunctionalChain | Kind::Scenario
|
||||
)
|
||||
})
|
||||
.map(|e| e.id)
|
||||
.collect();
|
||||
let mut seen = BTreeSet::new();
|
||||
let mut work = BTreeSet::new();
|
||||
let mut exchanges = BTreeSet::new();
|
||||
while let Some(id) = pending.pop() {
|
||||
if !seen.insert(id) {
|
||||
continue;
|
||||
}
|
||||
let Some(element) = p.elements.get(&id) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(steps) = element.steps() {
|
||||
pending.extend(
|
||||
steps
|
||||
.iter()
|
||||
.filter(|step| {
|
||||
p.elements.get(step).is_some_and(|child| {
|
||||
child.kind() == Kind::Occurrence
|
||||
&& child.parent == Some(id)
|
||||
&& child.layer() == element.layer()
|
||||
})
|
||||
})
|
||||
.copied(),
|
||||
);
|
||||
}
|
||||
let Some(SharedBody::Occurrence { body, .. }) = element.shared() else {
|
||||
continue;
|
||||
};
|
||||
match body {
|
||||
OccurrenceBody::Action {
|
||||
action: Some(action),
|
||||
..
|
||||
} => {
|
||||
if p.elements
|
||||
.get(action)
|
||||
.is_some_and(|e| e.kind().is_work() && e.layer() == element.layer())
|
||||
{
|
||||
work.insert(*action);
|
||||
}
|
||||
}
|
||||
OccurrenceBody::Exchange {
|
||||
exchange: Some(exchange),
|
||||
..
|
||||
} => {
|
||||
if let Some(relation) = p.relations.get(exchange)
|
||||
&& work_exchange(relation.kind())
|
||||
&& [relation.source(), relation.target()].iter().all(|id| {
|
||||
p.elements
|
||||
.get(id)
|
||||
.is_some_and(|e| e.kind().is_work() && e.layer() == element.layer())
|
||||
})
|
||||
{
|
||||
exchanges.insert(*exchange);
|
||||
work.extend([relation.source(), relation.target()]);
|
||||
}
|
||||
}
|
||||
OccurrenceBody::Fragment { branches, .. } => {
|
||||
pending.extend(
|
||||
branches
|
||||
.iter()
|
||||
.filter(|branch| {
|
||||
p.elements.get(branch).is_some_and(|child| {
|
||||
child.parent == Some(id)
|
||||
&& child.layer() == element.layer()
|
||||
&& matches!(
|
||||
child.shared(),
|
||||
Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Branch { .. },
|
||||
..
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
.copied(),
|
||||
);
|
||||
}
|
||||
OccurrenceBody::SequenceReference {
|
||||
sequence: Some(sequence),
|
||||
} => {
|
||||
if let Some(owner) = element.parent.and_then(|id| p.elements.get(&id))
|
||||
&& matches!(owner.kind(), Kind::Process | Kind::FunctionalChain)
|
||||
&& p.elements.get(sequence).is_some_and(|target| {
|
||||
target.kind() == owner.kind() && target.layer() == element.layer()
|
||||
})
|
||||
{
|
||||
pending.push(*sequence);
|
||||
}
|
||||
}
|
||||
OccurrenceBody::ScenarioUse {
|
||||
scenario: Some(scenario),
|
||||
..
|
||||
} if p.elements.get(scenario).is_some_and(|target| {
|
||||
target.kind() == Kind::Scenario && target.layer() == element.layer()
|
||||
}) =>
|
||||
{
|
||||
pending.push(*scenario);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
(work, exchanges)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn malformed_self_reuse_is_bounded_even_without_structural_gate() {
|
||||
let mut p = Project::empty();
|
||||
let scenario = p
|
||||
.add_element(Kind::Scenario, Layer::Oa, "Self reuse", None)
|
||||
.unwrap();
|
||||
let occurrence = p
|
||||
.add_element(Kind::Occurrence, Layer::Oa, "Cycle", Some(scenario))
|
||||
.unwrap();
|
||||
if let Some(SharedBody::Occurrence { body, .. }) =
|
||||
p.elements.get_mut(&occurrence).unwrap().shared_mut()
|
||||
{
|
||||
*body = OccurrenceBody::ScenarioUse {
|
||||
scenario: Some(scenario),
|
||||
bindings: vec![],
|
||||
};
|
||||
}
|
||||
p.elements
|
||||
.get_mut(&scenario)
|
||||
.unwrap()
|
||||
.steps_mut()
|
||||
.unwrap()
|
||||
.push(occurrence);
|
||||
assert_eq!(included_evidence(&p), (BTreeSet::new(), BTreeSet::new()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,582 @@
|
||||
use oa_core::{
|
||||
language,
|
||||
source_project::*,
|
||||
validation::{self, Severity},
|
||||
*,
|
||||
};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
const CODES: [&str; 5] = [
|
||||
"non_leaf_work_exchange",
|
||||
"work_without_behavior_context",
|
||||
"exchange_without_behavior_context",
|
||||
"mission_without_capability",
|
||||
"capability_without_behavior_context",
|
||||
];
|
||||
type Located = (String, Id, Option<String>);
|
||||
fn coverage(p: &Project) -> BTreeSet<Located> {
|
||||
let issues = validation::validate(p);
|
||||
assert!(
|
||||
!issues.iter().any(|i| i.severity == Severity::Error),
|
||||
"{issues:?}"
|
||||
);
|
||||
issues
|
||||
.into_iter()
|
||||
.filter(|i| CODES.contains(&i.code.as_str()))
|
||||
.map(|i| {
|
||||
assert_eq!(i.severity, Severity::Warning);
|
||||
(i.code, i.element.unwrap(), i.field)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
fn located(code: &str, id: Id, field: &str) -> Located {
|
||||
(code.into(), id, (!field.is_empty()).then(|| field.into()))
|
||||
}
|
||||
fn add(p: &mut Project, kind: Kind, layer: Layer, name: &str) -> Id {
|
||||
p.add_element(kind, layer, name, None).unwrap()
|
||||
}
|
||||
fn occurrence(p: &mut Project, owner: Id, body: OccurrenceBody) -> Id {
|
||||
let id = p
|
||||
.add_element(
|
||||
Kind::Occurrence,
|
||||
p.elements[&owner].layer(),
|
||||
"Contribution",
|
||||
Some(owner),
|
||||
)
|
||||
.unwrap();
|
||||
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Occurrence {
|
||||
body,
|
||||
precondition: String::new(),
|
||||
postcondition: String::new(),
|
||||
};
|
||||
if let Some(steps) = p.elements.get_mut(&owner).unwrap().steps_mut() {
|
||||
steps.push(id);
|
||||
}
|
||||
id
|
||||
}
|
||||
fn exchange(id: Id) -> OccurrenceBody {
|
||||
OccurrenceBody::Exchange {
|
||||
exchange: Some(id),
|
||||
sender: None,
|
||||
receiver: None,
|
||||
message_kind: MessageKind::Asynchronous,
|
||||
reply_to: None,
|
||||
values: vec![],
|
||||
}
|
||||
}
|
||||
fn action(id: Id) -> OccurrenceBody {
|
||||
OccurrenceBody::Action {
|
||||
action: Some(id),
|
||||
lifeline: None,
|
||||
}
|
||||
}
|
||||
fn line(p: &mut Project, scenario: Id, work: Id) -> Id {
|
||||
let id = p
|
||||
.add_element(
|
||||
Kind::Lifeline,
|
||||
p.elements[&scenario].layer(),
|
||||
"Work line",
|
||||
Some(scenario),
|
||||
)
|
||||
.unwrap();
|
||||
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Lifeline {
|
||||
represented: Some(work),
|
||||
};
|
||||
id
|
||||
}
|
||||
fn set_context(p: &mut Project, scenario: Id, capability: Id) {
|
||||
if let Some(SharedBody::Scenario { context, .. }) =
|
||||
p.elements.get_mut(&scenario).unwrap().shared_mut()
|
||||
{
|
||||
*context = Some(capability);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn included_process_and_nested_scenario_evidence_is_identity_based_in_both_layers() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut p = Project::empty();
|
||||
let (
|
||||
work_kind,
|
||||
sequence_kind,
|
||||
capability_kind,
|
||||
mission_kind,
|
||||
performer_kind,
|
||||
exchange_kind,
|
||||
) = if layer == Layer::Oa {
|
||||
(
|
||||
Kind::Activity,
|
||||
Kind::Process,
|
||||
Kind::Capability,
|
||||
Kind::OperationalMission,
|
||||
Kind::Entity,
|
||||
RelationKind::Interaction,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
Kind::Function,
|
||||
Kind::FunctionalChain,
|
||||
Kind::SystemCapability,
|
||||
Kind::Mission,
|
||||
Kind::System,
|
||||
RelationKind::FunctionalExchange,
|
||||
)
|
||||
};
|
||||
let performer = add(&mut p, performer_kind, layer, "Service provider");
|
||||
let a = add(&mut p, work_kind, layer, "Request service");
|
||||
let b = add(&mut p, work_kind, layer, "Provide service");
|
||||
for work in [a, b] {
|
||||
p.relate(RelationKind::Allocation, "Performed work", performer, work);
|
||||
}
|
||||
let e = p.relate(exchange_kind, "Request", a, b);
|
||||
let inner = add(&mut p, sequence_kind, layer, "Service path");
|
||||
let outer = add(&mut p, sequence_kind, layer, "Reusable path");
|
||||
occurrence(&mut p, inner, exchange(e));
|
||||
occurrence(
|
||||
&mut p,
|
||||
outer,
|
||||
OccurrenceBody::SequenceReference {
|
||||
sequence: Some(inner),
|
||||
},
|
||||
);
|
||||
let capability = add(&mut p, capability_kind, layer, "Deliver service");
|
||||
p.relate(
|
||||
RelationKind::ChainInvolvement,
|
||||
"Service context",
|
||||
outer,
|
||||
capability,
|
||||
);
|
||||
let mission = add(&mut p, mission_kind, layer, "Serve users");
|
||||
p.relate(
|
||||
RelationKind::MissionCapability,
|
||||
"Delivered capability",
|
||||
mission,
|
||||
capability,
|
||||
);
|
||||
let scenario = add(&mut p, Kind::Scenario, layer, "Conversation");
|
||||
let sender = line(&mut p, scenario, a);
|
||||
let receiver = line(&mut p, scenario, b);
|
||||
let fragment = occurrence(
|
||||
&mut p,
|
||||
scenario,
|
||||
OccurrenceBody::Fragment {
|
||||
operator: FragmentOperator::Optional,
|
||||
branches: vec![],
|
||||
},
|
||||
);
|
||||
let branch = occurrence(
|
||||
&mut p,
|
||||
fragment,
|
||||
OccurrenceBody::Branch {
|
||||
guard: "service_requested".into(),
|
||||
occurrences: vec![],
|
||||
},
|
||||
);
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Fragment { branches, .. },
|
||||
..
|
||||
}) = p.elements.get_mut(&fragment).unwrap().shared_mut()
|
||||
{
|
||||
branches.push(branch);
|
||||
}
|
||||
let call = occurrence(
|
||||
&mut p,
|
||||
branch,
|
||||
OccurrenceBody::Exchange {
|
||||
exchange: Some(e),
|
||||
sender: Some(sender),
|
||||
receiver: Some(receiver),
|
||||
message_kind: MessageKind::Call,
|
||||
reply_to: None,
|
||||
values: vec![],
|
||||
},
|
||||
);
|
||||
occurrence(
|
||||
&mut p,
|
||||
branch,
|
||||
OccurrenceBody::Exchange {
|
||||
exchange: Some(e),
|
||||
sender: Some(receiver),
|
||||
receiver: Some(sender),
|
||||
message_kind: MessageKind::Reply,
|
||||
reply_to: Some(call),
|
||||
values: vec![],
|
||||
},
|
||||
);
|
||||
let reuse = add(&mut p, Kind::Scenario, layer, "Reused conversation");
|
||||
let outer_sender = line(&mut p, reuse, a);
|
||||
let outer_receiver = line(&mut p, reuse, b);
|
||||
occurrence(
|
||||
&mut p,
|
||||
reuse,
|
||||
OccurrenceBody::ScenarioUse {
|
||||
scenario: Some(scenario),
|
||||
bindings: vec![
|
||||
LifelineBinding {
|
||||
referenced: sender,
|
||||
local: outer_sender,
|
||||
},
|
||||
LifelineBinding {
|
||||
referenced: receiver,
|
||||
local: outer_receiver,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
assert!(coverage(&p).is_empty());
|
||||
let inner_steps = std::mem::take(p.elements.get_mut(&inner).unwrap().steps_mut().unwrap());
|
||||
p.elements
|
||||
.get_mut(&scenario)
|
||||
.unwrap()
|
||||
.steps_mut()
|
||||
.unwrap()
|
||||
.clear();
|
||||
assert_eq!(
|
||||
coverage(&p),
|
||||
BTreeSet::from([
|
||||
located("work_without_behavior_context", a, ""),
|
||||
located("work_without_behavior_context", b, ""),
|
||||
located("exchange_without_behavior_context", e, ""),
|
||||
]),
|
||||
"Unordered fragments and reused containers cannot activate omitted messages"
|
||||
);
|
||||
*p.elements.get_mut(&inner).unwrap().steps_mut().unwrap() = inner_steps;
|
||||
p.elements
|
||||
.get_mut(&scenario)
|
||||
.unwrap()
|
||||
.steps_mut()
|
||||
.unwrap()
|
||||
.push(fragment);
|
||||
assert!(coverage(&p).is_empty());
|
||||
// Removing every message leaves lifelines and reuse intact: neither is
|
||||
// an execution claim. Empty sequences remain saveable, with old warnings.
|
||||
for element in p.elements.values_mut() {
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { .. },
|
||||
..
|
||||
}) = element.shared()
|
||||
{
|
||||
*element.shared_mut().unwrap() = SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Action {
|
||||
action: None,
|
||||
lifeline: None,
|
||||
},
|
||||
precondition: String::new(),
|
||||
postcondition: String::new(),
|
||||
};
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
coverage(&p),
|
||||
BTreeSet::from([
|
||||
located("work_without_behavior_context", a, ""),
|
||||
located("work_without_behavior_context", b, ""),
|
||||
located("exchange_without_behavior_context", e, "")
|
||||
])
|
||||
);
|
||||
occurrence(&mut p, scenario, action(a)); // known work, unfinished lifeline
|
||||
assert_eq!(
|
||||
coverage(&p),
|
||||
BTreeSet::from([
|
||||
located("work_without_behavior_context", b, ""),
|
||||
located("exchange_without_behavior_context", e, "")
|
||||
])
|
||||
);
|
||||
assert_eq!(language::parse(&language::format(&p)).unwrap(), p);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incomplete_oa_repairs_exact_records_and_ordered_membership_without_inference() {
|
||||
let mut p = Project::empty();
|
||||
let performer = add(&mut p, Kind::Entity, Layer::Oa, "Crew");
|
||||
let parent = add(&mut p, Kind::Activity, Layer::Oa, "Manage service");
|
||||
let a = p
|
||||
.add_element(Kind::Activity, Layer::Oa, "Prepare", Some(parent))
|
||||
.unwrap();
|
||||
let b = p
|
||||
.add_element(Kind::Activity, Layer::Oa, "Deliver", Some(parent))
|
||||
.unwrap();
|
||||
let c = add(&mut p, Kind::Activity, Layer::Oa, "Verify");
|
||||
for work in [a, b, c] {
|
||||
p.relate(RelationKind::Allocation, "Responsibility", performer, work);
|
||||
}
|
||||
let ab = p.relate(RelationKind::Interaction, "Prepared service", a, b);
|
||||
let pc = p.relate(RelationKind::Interaction, "Grouped service", parent, c);
|
||||
let ca = p.relate(RelationKind::Interaction, "Verified service", c, a);
|
||||
let process = add(&mut p, Kind::Process, Layer::Oa, "Delivery path");
|
||||
occurrence(&mut p, process, exchange(ab));
|
||||
let mission = add(
|
||||
&mut p,
|
||||
Kind::OperationalMission,
|
||||
Layer::Oa,
|
||||
"Deliver reliably",
|
||||
);
|
||||
let capability = add(&mut p, Kind::Capability, Layer::Oa, "Reliable delivery");
|
||||
p.relate(
|
||||
RelationKind::CapabilityInvolvement,
|
||||
"Crew support",
|
||||
performer,
|
||||
capability,
|
||||
);
|
||||
p.relate(
|
||||
RelationKind::MissionInvolvement,
|
||||
"Mission responsibility",
|
||||
performer,
|
||||
mission,
|
||||
);
|
||||
let machine = p
|
||||
.add_element(Kind::StateMachine, Layer::Oa, "Crew modes", Some(performer))
|
||||
.unwrap();
|
||||
let region = p
|
||||
.add_element(Kind::Region, Layer::Oa, "Crew region", Some(machine))
|
||||
.unwrap();
|
||||
let state = p
|
||||
.add_element(Kind::State, Layer::Oa, "Available", Some(region))
|
||||
.unwrap();
|
||||
p.relate(RelationKind::StateAction, "Verification entry", state, c);
|
||||
let observation = add(&mut p, Kind::Scenario, Layer::Oa, "Observation only");
|
||||
let observed = line(&mut p, observation, c);
|
||||
occurrence(
|
||||
&mut p,
|
||||
observation,
|
||||
OccurrenceBody::State {
|
||||
state: Some(state),
|
||||
lifeline: Some(observed),
|
||||
},
|
||||
);
|
||||
let expected = BTreeSet::from([
|
||||
located("non_leaf_work_exchange", pc, "source"),
|
||||
located("work_without_behavior_context", c, ""),
|
||||
located("exchange_without_behavior_context", ca, ""),
|
||||
located("mission_without_capability", mission, ""),
|
||||
located("capability_without_behavior_context", capability, ""),
|
||||
]);
|
||||
assert_eq!(coverage(&p), expected);
|
||||
let unordered = occurrence(&mut p, process, exchange(ca));
|
||||
p.elements
|
||||
.get_mut(&process)
|
||||
.unwrap()
|
||||
.steps_mut()
|
||||
.unwrap()
|
||||
.retain(|id| *id != unordered);
|
||||
assert_eq!(coverage(&p), expected);
|
||||
assert!(
|
||||
validation::validate(&p)
|
||||
.iter()
|
||||
.any(|i| i.element == Some(unordered) && i.code == "unordered_occurrence")
|
||||
);
|
||||
let before = p.clone();
|
||||
p.elements
|
||||
.get_mut(&process)
|
||||
.unwrap()
|
||||
.steps_mut()
|
||||
.unwrap()
|
||||
.push(unordered);
|
||||
if let RelationBody::Interaction { source, .. } = &mut p.relations.get_mut(&pc).unwrap().body {
|
||||
*source = a;
|
||||
}
|
||||
occurrence(&mut p, process, exchange(pc));
|
||||
p.relate(
|
||||
RelationKind::MissionCapability,
|
||||
"Mission delivery",
|
||||
mission,
|
||||
capability,
|
||||
);
|
||||
p.relate(
|
||||
RelationKind::ChainInvolvement,
|
||||
"Capability path",
|
||||
process,
|
||||
capability,
|
||||
);
|
||||
assert!(coverage(&p).is_empty());
|
||||
// Public session admission and undo retain the previous exact warning set.
|
||||
let mut session = Session::new(before.clone());
|
||||
session
|
||||
.execute(Command::Batch(diff_commands(&before, &p)))
|
||||
.unwrap();
|
||||
assert!(coverage(session.project()).is_empty());
|
||||
session.undo();
|
||||
assert_eq!(session.project(), &before);
|
||||
assert_eq!(coverage(session.project()), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_leaf_exchange_locations_use_work_endpoints_including_typed_sa_ports() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut p = Project::empty();
|
||||
let (kind, relation_kind) = if layer == Layer::Oa {
|
||||
(Kind::Activity, RelationKind::Interaction)
|
||||
} else {
|
||||
(Kind::Function, RelationKind::FunctionalExchange)
|
||||
};
|
||||
let source = add(&mut p, kind, layer, "Source group");
|
||||
let target = add(&mut p, kind, layer, "Target group");
|
||||
let a = p
|
||||
.add_element(kind, layer, "Source leaf", Some(source))
|
||||
.unwrap();
|
||||
let b = p
|
||||
.add_element(kind, layer, "Target leaf", Some(target))
|
||||
.unwrap();
|
||||
let edge = p.relate(relation_kind, "Decompose exchange", source, target);
|
||||
assert_eq!(
|
||||
coverage(&p),
|
||||
BTreeSet::from([
|
||||
located("non_leaf_work_exchange", edge, "source"),
|
||||
located("non_leaf_work_exchange", edge, "target"),
|
||||
located("work_without_behavior_context", a, ""),
|
||||
located("work_without_behavior_context", b, ""),
|
||||
])
|
||||
);
|
||||
if let RelationBody::FunctionalExchange {
|
||||
source_port,
|
||||
target_port,
|
||||
..
|
||||
} = p.relations[&edge].body
|
||||
{
|
||||
assert_eq!(p.elements[&source_port].parent, Some(source));
|
||||
assert_eq!(p.elements[&target_port].parent, Some(target));
|
||||
assert!(
|
||||
coverage(&p)
|
||||
.iter()
|
||||
.all(|(_, id, _)| *id != source_port && *id != target_port)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grouping_and_scenario_only_capabilities_do_not_duplicate_support_warnings() {
|
||||
let mut p = Project::empty();
|
||||
let parent = add(&mut p, Kind::Capability, Layer::Oa, "Capability group");
|
||||
let leaf = p
|
||||
.add_element(Kind::Capability, Layer::Oa, "Leaf capability", Some(parent))
|
||||
.unwrap();
|
||||
let issues = validation::validate(&p);
|
||||
assert!(
|
||||
issues
|
||||
.iter()
|
||||
.any(|i| i.element == Some(leaf) && i.code == "unsupported_capability")
|
||||
);
|
||||
assert!(
|
||||
!issues
|
||||
.iter()
|
||||
.any(|i| i.element == Some(parent) && i.code.contains("capability"))
|
||||
);
|
||||
assert!(coverage(&p).is_empty());
|
||||
let scenario = add(&mut p, Kind::Scenario, Layer::Oa, "Capability scenario");
|
||||
set_context(&mut p, scenario, leaf);
|
||||
let issues = validation::validate(&p);
|
||||
assert!(!issues.iter().any(|i| i.code == "unsupported_capability"));
|
||||
assert!(
|
||||
issues
|
||||
.iter()
|
||||
.any(|i| i.element == Some(scenario) && i.code == "empty_sequence")
|
||||
);
|
||||
assert!(coverage(&p).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn structural_errors_suppress_completeness_cascades_and_rejection_keeps_the_model() {
|
||||
let mut p = Project::empty();
|
||||
let a = add(&mut p, Kind::Activity, Layer::Oa, "Request");
|
||||
let b = add(&mut p, Kind::Activity, Layer::Oa, "Respond");
|
||||
let e = p.relate(RelationKind::Interaction, "Request data", a, b);
|
||||
let process = add(&mut p, Kind::Process, Layer::Oa, "Path");
|
||||
let step = occurrence(&mut p, process, exchange(e));
|
||||
assert!(coverage(&p).is_empty());
|
||||
let mut bad = p.clone();
|
||||
if let RelationBody::Interaction { source, .. } = &mut bad.relations.get_mut(&e).unwrap().body {
|
||||
*source = Id::new_v4();
|
||||
}
|
||||
let mut wrong_owner = p.clone();
|
||||
wrong_owner.elements.get_mut(&step).unwrap().parent = Some(a);
|
||||
let mut recursive = p.clone();
|
||||
let scenario = add(&mut recursive, Kind::Scenario, Layer::Oa, "Cycle");
|
||||
occurrence(
|
||||
&mut recursive,
|
||||
scenario,
|
||||
OccurrenceBody::ScenarioUse {
|
||||
scenario: Some(scenario),
|
||||
bindings: vec![],
|
||||
},
|
||||
);
|
||||
for (candidate, expected_error) in [
|
||||
(bad, "invalid_endpoints"),
|
||||
(wrong_owner, "invalid_occurrence"),
|
||||
(recursive, "recursive_scenario_use"),
|
||||
] {
|
||||
let issues = validation::validate(&candidate);
|
||||
assert!(
|
||||
issues
|
||||
.iter()
|
||||
.any(|i| i.severity == Severity::Error && i.code == expected_error),
|
||||
"{issues:?}"
|
||||
);
|
||||
assert!(
|
||||
!issues.iter().any(|i| CODES.contains(&i.code.as_str())),
|
||||
"{issues:?}"
|
||||
);
|
||||
let mut session = Session::new(p.clone());
|
||||
assert!(
|
||||
session
|
||||
.execute(Command::Batch(diff_commands(&p, &candidate)))
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(session.project(), &p);
|
||||
assert_eq!(session.revision(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multifile_lint_preserves_core_codes_ids_and_actual_owning_source_paths() {
|
||||
let source = SourceBundle { entry: "project.arcadia".into(), files: BTreeMap::from([
|
||||
("project.arcadia".into(), "arcadia 5; project p \"Coverage\" { include \"work.arcadia\"; include \"exchanges.arcadia\"; }".into()),
|
||||
("work.arcadia".into(), "oa_activity a \"Request\" {}\noa_activity b \"Respond\" {}".into()),
|
||||
("exchanges.arcadia".into(), "interaction e \"Data\" { source @a; target @b; }".into()),
|
||||
]) };
|
||||
let compiled = compile_bundle(&source, ProjectLimits::default()).unwrap();
|
||||
let expected = coverage(&compiled.decoded.project);
|
||||
assert_eq!(expected.len(), 3);
|
||||
let diagnostics: Vec<_> = lint_bundle(&compiled.bundle, ProjectLimits::default())
|
||||
.into_iter()
|
||||
.filter(|i| CODES.contains(&i.diagnostic.code.as_str()))
|
||||
.collect();
|
||||
let actual: BTreeSet<_> = diagnostics
|
||||
.iter()
|
||||
.map(|i| {
|
||||
assert_eq!(i.diagnostic.severity, Severity::Warning);
|
||||
let id = i.diagnostic.object_id.unwrap();
|
||||
assert_eq!(i.path, compiled.index.declarations[&id].path);
|
||||
assert!(i.diagnostic.span.end > i.diagnostic.span.start);
|
||||
(i.diagnostic.code.clone(), id, i.diagnostic.field.clone())
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(actual, expected);
|
||||
assert_eq!(diagnostics.len(), expected.len());
|
||||
}
|
||||
|
||||
// Authoring-equivalent complete record edits keep the test independent of a
|
||||
// private completeness helper and exercise actual transaction/undo boundaries.
|
||||
fn diff_commands(before: &Project, after: &Project) -> Vec<Command> {
|
||||
let mut commands = Vec::new();
|
||||
for (id, element) in &after.elements {
|
||||
match before.elements.get(id) {
|
||||
None => commands.push(Command::CreateElement(element.clone())),
|
||||
Some(old) if old != element => {
|
||||
commands.push(Command::EditElement(*id, element.clone()))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
for (id, relation) in &after.relations {
|
||||
match before.relations.get(id) {
|
||||
None => commands.push(Command::CreateRelation(relation.clone())),
|
||||
Some(old) if old != relation => {
|
||||
commands.push(Command::EditRelation(*id, relation.clone()))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
commands
|
||||
}
|
||||
@@ -642,6 +642,11 @@ fn atomic_annotation_full_path_updates_only_selected_row() {
|
||||
let rows = values_mut(&mut q, b.message);
|
||||
assert_eq!(rows.len(), 2);
|
||||
assert!(rows.contains(&retained));
|
||||
assert_eq!(AnnotationKey::from(&rows[0]), b.first);
|
||||
assert_eq!(
|
||||
rows[1], retained,
|
||||
"Unselected rows retain their authored position"
|
||||
);
|
||||
assert!(
|
||||
rows.iter()
|
||||
.any(|v| AnnotationKey::from(v) == b.first && v.expression == "new first")
|
||||
@@ -651,6 +656,73 @@ fn atomic_annotation_full_path_updates_only_selected_row() {
|
||||
.any(|v| AnnotationKey::from(v) == b.second && v.expression == retained.expression)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeping_keyed_rows_with_an_unrelated_header_candidate_preserves_exact_authored_order() {
|
||||
for binding in [false, true] {
|
||||
for reverse in [false, true] {
|
||||
let mut p = Project::empty();
|
||||
let (oa, sa, selector, references) = if binding {
|
||||
let a = scenario_use(&mut p, Layer::Oa, "oa");
|
||||
let b = scenario_use(&mut p, Layer::Sa, "sa");
|
||||
(
|
||||
a.use_id,
|
||||
b.use_id,
|
||||
FacetSelector::Binding(KeyPair {
|
||||
oa: Some(a.referenced),
|
||||
sa: Some(b.referenced),
|
||||
}),
|
||||
BTreeMap::from([(a.referenced, b.referenced)]),
|
||||
)
|
||||
} else {
|
||||
let a = payload(&mut p, Layer::Oa, "oa");
|
||||
let b = payload(&mut p, Layer::Sa, "sa");
|
||||
let mut references: BTreeMap<_, _> = a
|
||||
.first
|
||||
.path
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(b.first.path.iter().copied())
|
||||
.collect();
|
||||
references.insert(a.leaf, b.leaf);
|
||||
(
|
||||
a.message,
|
||||
b.message,
|
||||
FacetSelector::Annotation(KeyPair {
|
||||
oa: Some(a.first),
|
||||
sa: Some(b.first),
|
||||
}),
|
||||
references,
|
||||
)
|
||||
};
|
||||
trace(&mut p, oa, sa);
|
||||
let (source, target) = if reverse { (sa, oa) } else { (oa, sa) };
|
||||
let mut candidate = p.elements[&target].clone();
|
||||
candidate.description = "Review the header while retaining authored rows".into();
|
||||
let mut review = request(
|
||||
source,
|
||||
target,
|
||||
vec![
|
||||
(selector, SemanticDecision::KeepTarget),
|
||||
(
|
||||
FacetSelector::Header(HeaderFacet::Description),
|
||||
SemanticDecision::Candidate,
|
||||
),
|
||||
],
|
||||
);
|
||||
if reverse {
|
||||
review.transition.direction = TransitionDirection::SaToOa;
|
||||
}
|
||||
review.references = references;
|
||||
review
|
||||
.candidates
|
||||
.push(SemanticRecord::Element(candidate.clone()));
|
||||
let accepted = applied(&p, review);
|
||||
assert_eq!(accepted.elements[&target], candidate);
|
||||
assert_eq!(accepted.elements[&source], p.elements[&source]);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn atomic_binding_updates_local_and_rejects_pair_collision_and_remapping_without_reset() {
|
||||
let mut p = Project::empty();
|
||||
@@ -671,6 +743,8 @@ fn atomic_binding_updates_local_and_rejects_pair_collision_and_remapping_without
|
||||
r.references = BTreeMap::from([(a.referenced, b.referenced), (a.alternate, b.alternate)]);
|
||||
let mut q = applied(&p, r.clone());
|
||||
assert!(bindings_mut(&mut q, b.use_id).contains(&retained));
|
||||
assert_eq!(bindings_mut(&mut q, b.use_id)[0].referenced, b.referenced);
|
||||
assert_eq!(bindings_mut(&mut q, b.use_id)[1], retained);
|
||||
assert!(
|
||||
bindings_mut(&mut q, b.use_id)
|
||||
.iter()
|
||||
|
||||
Reference in New Issue
Block a user