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.
734 lines
24 KiB
Rust
734 lines
24 KiB
Rust
//! Persisted view intent and pure visibility decisions over actual model identities.
|
|
//! Build `ViewIndex` once per accepted model revision. Preparing a view performs
|
|
//! set unions/reachability once; testing an edge never walks the model.
|
|
use crate::{
|
|
ContextualLinkPurpose, Id, Kind, Layer, OccurrenceBody, Project, RelationBody, RelationKind,
|
|
SharedBody,
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::{BTreeMap, BTreeSet};
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum ExchangeFamily {
|
|
Work,
|
|
Participant,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub enum EdgeKind {
|
|
Relation(RelationKind),
|
|
Trace,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub enum RelationKinds {
|
|
#[default]
|
|
All,
|
|
Only(Vec<EdgeKind>),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub enum CategorySelection {
|
|
#[default]
|
|
All,
|
|
Any(Vec<Id>),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub enum ContextSelection {
|
|
#[default]
|
|
All,
|
|
Any(Vec<Id>),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum Coverage {
|
|
#[default]
|
|
All,
|
|
InBehavior,
|
|
OutsideBehavior,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum ExchangeLabels {
|
|
#[default]
|
|
Name,
|
|
Items,
|
|
NameAndItems,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(default, deny_unknown_fields)]
|
|
pub struct ViewOptions {
|
|
pub hidden_edges: Vec<Id>,
|
|
pub relation_kinds: RelationKinds,
|
|
pub work_categories: CategorySelection,
|
|
pub participant_categories: CategorySelection,
|
|
pub coverage: Coverage,
|
|
pub work_contexts: ContextSelection,
|
|
pub exchange_labels: ExchangeLabels,
|
|
}
|
|
|
|
impl ViewOptions {
|
|
pub fn visit_references(&mut self, mut visit: impl FnMut(&mut Id)) {
|
|
for id in &mut self.hidden_edges {
|
|
visit(id);
|
|
}
|
|
for selection in [&mut self.work_categories, &mut self.participant_categories] {
|
|
if let CategorySelection::Any(ids) = selection {
|
|
for id in ids {
|
|
visit(id);
|
|
}
|
|
}
|
|
}
|
|
if let ContextSelection::Any(ids) = &mut self.work_contexts {
|
|
for id in ids {
|
|
visit(id);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Removing the last selected identity retains the explicit empty selector.
|
|
pub fn retain_references(&mut self, mut keep: impl FnMut(&Id) -> bool) {
|
|
self.hidden_edges.retain(&mut keep);
|
|
for selection in [&mut self.work_categories, &mut self.participant_categories] {
|
|
if let CategorySelection::Any(ids) = selection {
|
|
ids.retain(&mut keep);
|
|
}
|
|
}
|
|
if let ContextSelection::Any(ids) = &mut self.work_contexts {
|
|
ids.retain(keep);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Stable field names allow core/source adapters to locate the offending option.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct OptionError {
|
|
pub field: &'static str,
|
|
pub reference: Option<Id>,
|
|
pub message: String,
|
|
}
|
|
|
|
pub fn exchange_family(kind: RelationKind) -> Option<ExchangeFamily> {
|
|
match kind {
|
|
RelationKind::Interaction | RelationKind::FunctionalExchange => Some(ExchangeFamily::Work),
|
|
RelationKind::BoundaryExchange | RelationKind::CommunicationMean => {
|
|
Some(ExchangeFamily::Participant)
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn category_member(p: &Project, layer: Layer, family: ExchangeFamily, id: Id) -> bool {
|
|
p.relations.get(&id).is_some_and(|r| {
|
|
p.layer_of(id) == Some(layer)
|
|
&& matches!(
|
|
(family, layer, r.kind()),
|
|
(ExchangeFamily::Work, Layer::Oa, RelationKind::Interaction)
|
|
| (
|
|
ExchangeFamily::Work,
|
|
Layer::Sa,
|
|
RelationKind::FunctionalExchange
|
|
)
|
|
| (
|
|
ExchangeFamily::Participant,
|
|
Layer::Oa,
|
|
RelationKind::CommunicationMean
|
|
)
|
|
| (
|
|
ExchangeFamily::Participant,
|
|
_,
|
|
RelationKind::BoundaryExchange
|
|
)
|
|
)
|
|
})
|
|
}
|
|
|
|
fn context_kind(kind: Kind) -> bool {
|
|
matches!(kind, Kind::Process | Kind::FunctionalChain | Kind::Scenario)
|
|
}
|
|
|
|
fn edge_layers(p: &Project, id: Id) -> Vec<Layer> {
|
|
if p.relations.contains_key(&id) {
|
|
p.layer_of(id).into_iter().collect()
|
|
} else if p.traces.get(&id).is_some_and(|trace| {
|
|
p.layer_of(trace.oa) == Some(Layer::Oa) && p.layer_of(trace.sa) == Some(Layer::Sa)
|
|
}) {
|
|
vec![Layer::Oa, Layer::Sa]
|
|
} else {
|
|
vec![]
|
|
}
|
|
}
|
|
|
|
fn error(field: &'static str, reference: Option<Id>, message: &str) -> OptionError {
|
|
OptionError {
|
|
field,
|
|
reference,
|
|
message: message.into(),
|
|
}
|
|
}
|
|
|
|
fn distinct_ids(field: &'static str, ids: &[Id], out: &mut Vec<OptionError>) {
|
|
let mut seen = BTreeSet::new();
|
|
for id in ids {
|
|
if !seen.insert(*id) {
|
|
out.push(error(
|
|
field,
|
|
Some(*id),
|
|
"Each identity must occur only once.",
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn validate_category(
|
|
p: &Project,
|
|
layer: Layer,
|
|
parent: Option<Id>,
|
|
family: ExchangeFamily,
|
|
exchanges: &[Id],
|
|
) -> Vec<OptionError> {
|
|
let mut out = vec![];
|
|
if parent.is_some() {
|
|
out.push(error(
|
|
"parent",
|
|
parent,
|
|
"Exchange categories belong at the layer root.",
|
|
));
|
|
}
|
|
distinct_ids("exchanges", exchanges, &mut out);
|
|
for id in exchanges {
|
|
if !category_member(p, layer, family, *id) {
|
|
out.push(error(
|
|
"exchanges",
|
|
Some(*id),
|
|
"Choose an existing exchange of this category's family and layer.",
|
|
));
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
pub fn validate_options(p: &Project, layer: Layer, options: &ViewOptions) -> Vec<OptionError> {
|
|
validate_options_in(p, Some(layer), options)
|
|
}
|
|
|
|
/// Traceability views admit references from either architecture layer.
|
|
pub fn validate_options_cross_layer(p: &Project, options: &ViewOptions) -> Vec<OptionError> {
|
|
validate_options_in(p, None, options)
|
|
}
|
|
|
|
fn validate_options_in(
|
|
p: &Project,
|
|
layer: Option<Layer>,
|
|
options: &ViewOptions,
|
|
) -> Vec<OptionError> {
|
|
let mut out = vec![];
|
|
distinct_ids("hidden_edges", &options.hidden_edges, &mut out);
|
|
for id in &options.hidden_edges {
|
|
let layers = edge_layers(p, *id);
|
|
if layers.is_empty() || layer.is_some_and(|layer| !layers.contains(&layer)) {
|
|
out.push(error(
|
|
"hidden_edges",
|
|
Some(*id),
|
|
"Choose an existing relationship or applicable trace in this view's layer.",
|
|
));
|
|
}
|
|
}
|
|
if let RelationKinds::Only(kinds) = &options.relation_kinds {
|
|
for (index, kind) in kinds.iter().enumerate() {
|
|
if kinds[..index].contains(kind) {
|
|
out.push(error(
|
|
"relation_kinds",
|
|
None,
|
|
"Each relationship-line kind must occur only once.",
|
|
));
|
|
}
|
|
}
|
|
}
|
|
for (field, selection, family) in [
|
|
(
|
|
"work_categories",
|
|
&options.work_categories,
|
|
ExchangeFamily::Work,
|
|
),
|
|
(
|
|
"participant_categories",
|
|
&options.participant_categories,
|
|
ExchangeFamily::Participant,
|
|
),
|
|
] {
|
|
if let CategorySelection::Any(ids) = selection {
|
|
distinct_ids(field, ids, &mut out);
|
|
for id in ids {
|
|
if !p.elements.get(id).is_some_and(|e| {
|
|
layer.is_none_or(|layer| e.layer() == layer)
|
|
&& matches!(e.shared(), Some(SharedBody::ExchangeCategory { family: f, .. }) if *f == family)
|
|
}) {
|
|
out.push(error(field, Some(*id), "Choose a category of this selector's family and layer."));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if let ContextSelection::Any(ids) = &options.work_contexts {
|
|
distinct_ids("work_contexts", ids, &mut out);
|
|
for id in ids {
|
|
if !p.elements.get(id).is_some_and(|e| {
|
|
layer.is_none_or(|layer| e.layer() == layer) && context_kind(e.kind())
|
|
}) {
|
|
out.push(error(
|
|
"work_contexts",
|
|
Some(*id),
|
|
"Choose a Process, Functional chain or Scenario in this view's layer.",
|
|
));
|
|
}
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
|
pub struct BehaviorEvidence {
|
|
pub work: BTreeSet<Id>,
|
|
pub exchanges: BTreeSet<Id>,
|
|
}
|
|
|
|
/// Explicit inclusion/reuse graph, independent of semantic-parent descendants.
|
|
#[derive(Debug, Clone, Default)]
|
|
struct BehaviorIndex {
|
|
roots: BTreeMap<Id, Layer>,
|
|
next: BTreeMap<Id, Vec<Id>>,
|
|
facts: BTreeMap<Id, BehaviorEvidence>,
|
|
}
|
|
|
|
impl BehaviorIndex {
|
|
fn new(p: &Project) -> Self {
|
|
let mut index = Self::default();
|
|
for element in p.elements.values() {
|
|
let id = element.id;
|
|
if context_kind(element.kind()) {
|
|
index.roots.insert(id, element.layer());
|
|
}
|
|
let next = index.next.entry(id).or_default();
|
|
if let Some(steps) = element.steps() {
|
|
next.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(work), ..
|
|
} => {
|
|
if p.elements
|
|
.get(work)
|
|
.is_some_and(|e| e.kind().is_work() && e.layer() == element.layer())
|
|
{
|
|
index.facts.entry(id).or_default().work.insert(*work);
|
|
}
|
|
}
|
|
OccurrenceBody::Exchange {
|
|
exchange: Some(exchange),
|
|
..
|
|
} => {
|
|
if let Some(r) = p.relations.get(exchange)
|
|
&& exchange_family(r.kind()) == Some(ExchangeFamily::Work)
|
|
&& [r.source(), r.target()].iter().all(|id| {
|
|
p.elements
|
|
.get(id)
|
|
.is_some_and(|e| e.kind().is_work() && e.layer() == element.layer())
|
|
})
|
|
{
|
|
let facts = index.facts.entry(id).or_default();
|
|
facts.exchanges.insert(*exchange);
|
|
facts.work.extend([r.source(), r.target()]);
|
|
}
|
|
}
|
|
OccurrenceBody::Fragment { branches, .. } => {
|
|
next.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()
|
|
})
|
|
{
|
|
next.push(*sequence);
|
|
}
|
|
}
|
|
OccurrenceBody::ScenarioUse {
|
|
scenario: Some(scenario),
|
|
..
|
|
} if p.elements.get(scenario).is_some_and(|target| {
|
|
target.kind() == Kind::Scenario && target.layer() == element.layer()
|
|
}) =>
|
|
{
|
|
next.push(*scenario);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
let topology = crate::topology_context::TopologyIndex::new(p);
|
|
for relation in p.relations.values() {
|
|
if let RelationBody::ContextualLink {
|
|
owner,
|
|
source,
|
|
target,
|
|
purpose: purpose @ ContextualLinkPurpose::Exchange { exchange },
|
|
} = &relation.body
|
|
&& topology
|
|
.validate_link(*owner, source, target, purpose)
|
|
.is_ok()
|
|
{
|
|
let facts = index.facts.entry(*owner).or_default();
|
|
facts.exchanges.insert(*exchange);
|
|
if let Some(exchange) = p.relations.get(exchange) {
|
|
facts.work.extend([exchange.source(), exchange.target()]);
|
|
}
|
|
}
|
|
}
|
|
index
|
|
}
|
|
|
|
fn evidence(&self, roots: impl IntoIterator<Item = Id>) -> BehaviorEvidence {
|
|
let mut pending: Vec<_> = roots
|
|
.into_iter()
|
|
.filter(|id| self.roots.contains_key(id))
|
|
.collect();
|
|
let mut seen = BTreeSet::new();
|
|
let mut result = BehaviorEvidence::default();
|
|
while let Some(id) = pending.pop() {
|
|
if !seen.insert(id) {
|
|
continue;
|
|
}
|
|
if let Some(next) = self.next.get(&id) {
|
|
pending.extend(next);
|
|
}
|
|
if let Some(facts) = self.facts.get(&id) {
|
|
result.work.extend(&facts.work);
|
|
result.exchanges.extend(&facts.exchanges);
|
|
}
|
|
}
|
|
result
|
|
}
|
|
}
|
|
|
|
pub fn behavior_evidence(p: &Project, roots: impl IntoIterator<Item = Id>) -> BehaviorEvidence {
|
|
BehaviorIndex::new(p).evidence(roots)
|
|
}
|
|
|
|
pub fn all_behavior_evidence(p: &Project) -> BehaviorEvidence {
|
|
let index = BehaviorIndex::new(p);
|
|
index.evidence(index.roots.keys().copied())
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct EdgeFact {
|
|
kind: EdgeKind,
|
|
family: Option<ExchangeFamily>,
|
|
layers: Vec<Layer>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct CategoryFact {
|
|
layer: Layer,
|
|
family: ExchangeFamily,
|
|
exchanges: BTreeSet<Id>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct ViewIndex {
|
|
edges: BTreeMap<Id, EdgeFact>,
|
|
categories: BTreeMap<Id, CategoryFact>,
|
|
behavior: BehaviorIndex,
|
|
global: BehaviorEvidence,
|
|
}
|
|
|
|
impl ViewIndex {
|
|
pub fn new(p: &Project) -> Self {
|
|
let behavior = BehaviorIndex::new(p);
|
|
let global = behavior.evidence(behavior.roots.keys().copied());
|
|
let mut edges: BTreeMap<_, _> = p
|
|
.relations
|
|
.values()
|
|
.map(|r| {
|
|
(
|
|
r.id,
|
|
EdgeFact {
|
|
kind: EdgeKind::Relation(r.kind()),
|
|
family: exchange_family(r.kind()),
|
|
layers: edge_layers(p, r.id),
|
|
},
|
|
)
|
|
})
|
|
.collect();
|
|
edges.extend(p.traces.keys().map(|id| {
|
|
(
|
|
*id,
|
|
EdgeFact {
|
|
kind: EdgeKind::Trace,
|
|
family: None,
|
|
layers: edge_layers(p, *id),
|
|
},
|
|
)
|
|
}));
|
|
let categories = p
|
|
.elements
|
|
.values()
|
|
.filter_map(|e| {
|
|
let SharedBody::ExchangeCategory { family, exchanges } = e.shared()? else {
|
|
return None;
|
|
};
|
|
Some((
|
|
e.id,
|
|
CategoryFact {
|
|
layer: e.layer(),
|
|
family: *family,
|
|
exchanges: exchanges
|
|
.iter()
|
|
.filter(|id| category_member(p, e.layer(), *family, **id))
|
|
.copied()
|
|
.collect(),
|
|
},
|
|
))
|
|
})
|
|
.collect();
|
|
Self {
|
|
edges,
|
|
categories,
|
|
behavior,
|
|
global,
|
|
}
|
|
}
|
|
|
|
pub fn all_evidence(&self) -> &BehaviorEvidence {
|
|
&self.global
|
|
}
|
|
|
|
/// Options must pass `validate_options` before model admission. Malformed
|
|
/// selectors are nevertheless fail-closed here, useful for draft previews.
|
|
pub fn prepare(&self, layer: Layer, options: &ViewOptions) -> PreparedFilter<'_> {
|
|
self.prepare_in(layer, options, false)
|
|
}
|
|
|
|
fn prepare_in(
|
|
&self,
|
|
layer: Layer,
|
|
options: &ViewOptions,
|
|
cross_layer: bool,
|
|
) -> PreparedFilter<'_> {
|
|
let category_union = |selection: &CategorySelection, family| match selection {
|
|
CategorySelection::All => None,
|
|
CategorySelection::Any(ids) => Some(
|
|
ids.iter()
|
|
.filter_map(|id| self.categories.get(id))
|
|
.filter(|c| (cross_layer || c.layer == layer) && c.family == family)
|
|
.flat_map(|c| c.exchanges.iter().copied())
|
|
.collect(),
|
|
),
|
|
};
|
|
let contexts = match &options.work_contexts {
|
|
ContextSelection::All => None,
|
|
ContextSelection::Any(ids) => Some(
|
|
self.behavior
|
|
.evidence(
|
|
ids.iter()
|
|
.filter(|id| {
|
|
self.behavior
|
|
.roots
|
|
.get(id)
|
|
.is_some_and(|candidate| cross_layer || *candidate == layer)
|
|
})
|
|
.copied(),
|
|
)
|
|
.exchanges,
|
|
),
|
|
};
|
|
PreparedFilter {
|
|
index: self,
|
|
layer,
|
|
cross_layer,
|
|
hidden: options.hidden_edges.iter().copied().collect(),
|
|
kinds: options.relation_kinds.clone(),
|
|
coverage: options.coverage,
|
|
work_categories: category_union(&options.work_categories, ExchangeFamily::Work),
|
|
participant_categories: category_union(
|
|
&options.participant_categories,
|
|
ExchangeFamily::Participant,
|
|
),
|
|
contexts,
|
|
}
|
|
}
|
|
|
|
/// Traceability diagrams admit edges, categories and contexts in both layers.
|
|
pub fn prepare_cross_layer(&self, layer: Layer, options: &ViewOptions) -> PreparedFilter<'_> {
|
|
self.prepare_in(layer, options, true)
|
|
}
|
|
}
|
|
|
|
/// Projection reports why an actual edge cannot have a drawable endpoint pair.
|
|
/// Filtering does not perform anchoring, folding, layout or placement insertion.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum EdgeAvailability {
|
|
Visible,
|
|
OutsideProjection,
|
|
EndpointNotShown { source: bool, target: bool },
|
|
FoldedInternal,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum OmissionReason {
|
|
ExplicitHide,
|
|
RelationKind,
|
|
Category(ExchangeFamily),
|
|
Coverage,
|
|
WorkContext,
|
|
OutsideProjection,
|
|
EndpointNotShown { source: bool, target: bool },
|
|
FoldedInternal,
|
|
WrongLayer,
|
|
UnknownEdge,
|
|
}
|
|
|
|
pub struct PreparedFilter<'a> {
|
|
index: &'a ViewIndex,
|
|
layer: Layer,
|
|
cross_layer: bool,
|
|
hidden: BTreeSet<Id>,
|
|
kinds: RelationKinds,
|
|
coverage: Coverage,
|
|
work_categories: Option<BTreeSet<Id>>,
|
|
participant_categories: Option<BTreeSet<Id>>,
|
|
contexts: Option<BTreeSet<Id>>,
|
|
}
|
|
|
|
impl PreparedFilter<'_> {
|
|
pub fn includes(&self, edge: Id) -> bool {
|
|
self.reasons(edge, EdgeAvailability::Visible).is_empty()
|
|
}
|
|
|
|
pub fn reasons(&self, edge: Id, availability: EdgeAvailability) -> Vec<OmissionReason> {
|
|
let mut out = vec![];
|
|
if self.hidden.contains(&edge) {
|
|
out.push(OmissionReason::ExplicitHide);
|
|
}
|
|
match availability {
|
|
EdgeAvailability::Visible => {}
|
|
EdgeAvailability::OutsideProjection => out.push(OmissionReason::OutsideProjection),
|
|
EdgeAvailability::EndpointNotShown { source, target } => {
|
|
out.push(OmissionReason::EndpointNotShown { source, target })
|
|
}
|
|
EdgeAvailability::FoldedInternal => out.push(OmissionReason::FoldedInternal),
|
|
}
|
|
let Some(fact) = self.index.edges.get(&edge) else {
|
|
out.push(OmissionReason::UnknownEdge);
|
|
return out;
|
|
};
|
|
if !fact.layers.contains(&self.layer) && !(self.cross_layer && !fact.layers.is_empty()) {
|
|
out.push(OmissionReason::WrongLayer);
|
|
}
|
|
if let RelationKinds::Only(kinds) = &self.kinds
|
|
&& !kinds.contains(&fact.kind)
|
|
{
|
|
out.push(OmissionReason::RelationKind);
|
|
}
|
|
if let Some(family) = fact.family {
|
|
let categories = match family {
|
|
ExchangeFamily::Work => &self.work_categories,
|
|
ExchangeFamily::Participant => &self.participant_categories,
|
|
};
|
|
if categories.as_ref().is_some_and(|ids| !ids.contains(&edge)) {
|
|
out.push(OmissionReason::Category(family));
|
|
}
|
|
if family == ExchangeFamily::Work {
|
|
let covered = self.index.global.exchanges.contains(&edge);
|
|
if matches!(self.coverage, Coverage::InBehavior) && !covered
|
|
|| matches!(self.coverage, Coverage::OutsideBehavior) && covered
|
|
{
|
|
out.push(OmissionReason::Coverage);
|
|
}
|
|
if self
|
|
.contexts
|
|
.as_ref()
|
|
.is_some_and(|ids| !ids.contains(&edge))
|
|
{
|
|
out.push(OmissionReason::WorkContext);
|
|
}
|
|
}
|
|
}
|
|
out
|
|
}
|
|
}
|
|
|
|
impl ExchangeLabels {
|
|
pub fn label(self, p: &Project, edge: Id) -> String {
|
|
let name = p.named(edge);
|
|
let Some(relation) = p
|
|
.relations
|
|
.get(&edge)
|
|
.filter(|r| exchange_family(r.kind()).is_some())
|
|
else {
|
|
return name.into();
|
|
};
|
|
if self == Self::Name {
|
|
return name.into();
|
|
}
|
|
let items = if relation.body.items().is_empty() {
|
|
"No items".into()
|
|
} else {
|
|
relation
|
|
.body
|
|
.items()
|
|
.iter()
|
|
.map(|id| p.named(*id))
|
|
.collect::<Vec<_>>()
|
|
.join(", ")
|
|
};
|
|
if self == Self::Items {
|
|
items
|
|
} else {
|
|
format!("{name} · {items}")
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "view_options_tests.rs"]
|
|
mod tests;
|