Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
Preserve current core, source, REST, desktop, recovery, documentation and validation work, including AGENTS.md and the development summary. This is a work-in-progress checkpoint: the new compact palette has outstanding compilation errors and native acceptance; earlier passing gates retain their documented scope.
586 lines
20 KiB
Rust
586 lines
20 KiB
Rust
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 {
|
|
operation: None,
|
|
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 {
|
|
operation: None,
|
|
exchange: Some(e),
|
|
sender: Some(sender),
|
|
receiver: Some(receiver),
|
|
message_kind: MessageKind::Call,
|
|
reply_to: None,
|
|
values: vec![],
|
|
},
|
|
);
|
|
occurrence(
|
|
&mut p,
|
|
branch,
|
|
OccurrenceBody::Exchange {
|
|
operation: None,
|
|
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
|
|
}
|