///|
fn revision_value_equal(left : Value?, right : Value?) -> Bool {
match (left, right) {
(None, None) => true
(Some(a), Some(b)) => a == b
_ => false
}
}
///|
fn revision_field_changes_for_entity(
before : Entity?,
after : Entity?,
) -> Array[EntityFieldChange] {
let entity_id = match (before, after) {
(Some(entity), _) => entity.id
(_, Some(entity)) => entity.id
_ => 0
}
let before_length = before
.map(entity => entity.arguments.length())
.unwrap_or(0)
let after_length = after.map(entity => entity.arguments.length()).unwrap_or(0)
let count = if before_length > after_length {
before_length
} else {
after_length
}
let result : Array[EntityFieldChange] = []
for index in 0.. Added
(Some(_), None) => Removed
_ => Modified
}
result.push({
entity_id,
attribute: index,
before: old_value,
after: new_value,
kind,
})
}
}
result
}
///|
fn revision_entity_value(entity : Entity?, index : Int) -> Value? {
match entity {
Some(value) => value.arguments.get(index)
None => None
}
}
///|
fn revision_collect_field_changes(diff : ModelDiff) -> Array[EntityFieldChange] {
diff.changes.fold(init=Array::new(), (result, change) => {
result.append(
revision_field_changes_for_entity(change.before, change.after),
)
result
})
}
///|
fn revision_changed_ids(diff : ModelDiff) -> Array[Int] {
diff.changed_ids()
}
///|
fn revision_products_for_ids(
model : Model,
affected : Array[Int],
) -> Array[Int] {
model
.products()
.fold(init=Array::new(), (result, product) => {
if affected.contains(product.id) {
result.push(product.id)
}
result
})
}
///|
fn revision_affected_spatial_nodes(
model : Model,
affected : Array[Int],
) -> Array[Int] {
let result : Array[Int] = []
for root in spatial_roots(model.index()) {
for node in flatten_spatial(root) {
if affected.contains(node.id) ||
node.products.any(id => affected.contains(id)) {
result.push(node.id)
}
}
}
result
}
///|
fn revision_risk(diff : ModelDiff, impact : RevisionImpact) -> String {
if diff.removed > 0 {
"high"
} else if impact.affected_products.length() > 10 || diff.modified > 20 {
"medium"
} else if diff.changes.length() > 0 {
"low"
} else {
"none"
}
}
///|
/// Calculate dependency-aware impact for a model revision.
pub fn revision_impact(before : Model, after : Model) -> RevisionImpact {
let diff = diff_models(before, after)
let changed_ids = revision_changed_ids(diff)
let after_graph = entity_graph(after)
let affected_ids = after_graph.dependents(changed_ids)
let affected_products = revision_products_for_ids(after, affected_ids)
let affected_spatial_nodes = revision_affected_spatial_nodes(
after, affected_ids,
)
let field_changes = revision_collect_field_changes(diff)
let preliminary = {
changed_ids,
affected_ids,
affected_products,
affected_spatial_nodes,
field_changes,
risk: "none",
}
{ ..preliminary, risk: revision_risk(diff, preliminary) }
}
///|
/// Combine an entity diff with product-level and spatial dependency impact.
pub fn revision_report(before : Model, after : Model) -> RevisionReport {
let diff = diff_models(before, after)
let impact = revision_impact(before, after)
let before_products = before.products()
let after_products = after.products()
let added_products : Array[Int] = []
let removed_products : Array[Int] = []
let modified_products : Array[Int] = []
for product in after_products {
match product_by_id(before_products, product.id) {
None => added_products.push(product.id)
Some(old) if old.ifc_type != product.ifc_type ||
old.name != product.name ||
old.representation != product.representation =>
modified_products.push(product.id)
_ => ()
}
}
for product in before_products {
if product_by_id(after_products, product.id) is None {
removed_products.push(product.id)
}
}
{ diff, impact, added_products, removed_products, modified_products }
}
///|
pub fn compare_revision(before : Model, after : Model) -> RevisionReport {
revision_report(before, after)
}
///|
pub fn RevisionImpact::is_empty(self : RevisionImpact) -> Bool {
self.changed_ids.length() == 0
}
///|
pub fn RevisionImpact::affected_count(self : RevisionImpact) -> Int {
self.affected_ids.length()
}
///|
pub fn RevisionImpact::field_change_count(self : RevisionImpact) -> Int {
self.field_changes.length()
}
///|
pub fn RevisionImpact::contains_product(
self : RevisionImpact,
id : Int,
) -> Bool {
self.affected_products.contains(id)
}
///|
pub fn RevisionImpact::headline(self : RevisionImpact) -> String {
"changed=" +
self.changed_ids.length().to_string() +
" affected=" +
self.affected_ids.length().to_string() +
" products=" +
self.affected_products.length().to_string() +
" spatial_nodes=" +
self.affected_spatial_nodes.length().to_string() +
" fields=" +
self.field_changes.length().to_string() +
" risk=" +
self.risk
}
///|
pub fn EntityFieldChange::headline(self : EntityFieldChange) -> String {
let kind = match self.kind {
Added => "added"
Removed => "removed"
Modified => "modified"
}
"#" +
self.entity_id.to_string() +
"[" +
self.attribute.to_string() +
"] " +
kind
}
///|
pub fn RevisionReport::is_empty(self : RevisionReport) -> Bool {
self.diff.is_empty()
}
///|
pub fn RevisionReport::change_count(self : RevisionReport) -> Int {
self.diff.changes.length()
}
///|
pub fn RevisionReport::impact_count(self : RevisionReport) -> Int {
self.impact.affected_ids.length()
}
///|
pub fn RevisionReport::risk(self : RevisionReport) -> String {
self.impact.risk
}
///|
pub fn RevisionReport::headline(self : RevisionReport) -> String {
"changes=" +
self.change_count().to_string() +
" added_products=" +
self.added_products.length().to_string() +
" removed_products=" +
self.removed_products.length().to_string() +
" modified_products=" +
self.modified_products.length().to_string() +
" affected=" +
self.impact_count().to_string() +
" risk=" +
self.risk()
}
///|
pub fn RevisionReport::changed_product_ids(self : RevisionReport) -> Array[Int] {
let result = self.added_products.copy()
for id in self.removed_products + self.modified_products {
if !result.contains(id) {
result.push(id)
}
}
result
}
///|
pub fn RevisionReport::field_changes_for(
self : RevisionReport,
id : Int,
) -> Array[EntityFieldChange] {
self.impact.field_changes.filter(change => change.entity_id == id)
}
///|
pub fn RevisionReport::to_json(self : RevisionReport) -> String {
let fields = self.impact.field_changes
.map(change => {
let kind = match change.kind {
Added => "added"
Removed => "removed"
Modified => "modified"
}
"{\"entity_id\":" +
change.entity_id.to_string() +
",\"attribute\":" +
change.attribute.to_string() +
",\"kind\":\"" +
kind +
"\"}"
})
.join(",")
"{\"risk\":\"" +
self.risk() +
"\",\"added\":[" +
self.added_products.map(id => id.to_string()).join(",") +
"],\"removed\":[" +
self.removed_products.map(id => id.to_string()).join(",") +
"],\"modified\":[" +
self.modified_products.map(id => id.to_string()).join(",") +
"],\"affected\":[" +
self.impact.affected_ids.map(id => id.to_string()).join(",") +
"],\"field_changes\":[" +
fields +
"]}"
}
///|
pub fn RevisionReport::to_markdown(self : RevisionReport) -> String {
let builder = StringBuilder()
builder <+ "# Revision impact\n\n"
builder <+ "- Changes: \{self.change_count()}\n"
builder <+ "- Added products: \{self.added_products.length()}\n"
builder <+ "- Removed products: \{self.removed_products.length()}\n"
builder <+ "- Modified products: \{self.modified_products.length()}\n"
builder <+ "- Affected entities: \{self.impact.affected_ids.length()}\n"
builder <+ "- Risk: \{self.risk()}\n\n"
if self.impact.field_changes.length() > 0 {
builder <+ "| Entity | Attribute | Kind |\n"
builder <+ "| ---: | ---: | --- |\n"
for change in self.impact.field_changes {
let kind = match change.kind {
Added => "added"
Removed => "removed"
Modified => "modified"
}
builder <+ "| \{change.entity_id} | \{change.attribute} | \{kind} |\n"
}
}
builder.to_string()
}
///|
pub fn revision_changed_entity_ids(before : Model, after : Model) -> Array[Int] {
diff_models(before, after).changed_ids()
}
///|
pub fn revision_affected_entity_ids(
before : Model,
after : Model,
) -> Array[Int] {
revision_impact(before, after).affected_ids
}
///|
pub fn revision_affected_products(before : Model, after : Model) -> Array[Int] {
revision_impact(before, after).affected_products
}
///|
pub fn revision_risk_level(before : Model, after : Model) -> String {
revision_report(before, after).risk()
}