src/pqc_mbom/diff.py
| 1 | """Diff two MBOMs - useful for auditing model updates / fine-tunes.""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | from dataclasses import dataclass |
| 6 | |
| 7 | from pqc_mbom.component import ModelComponent |
| 8 | from pqc_mbom.mbom import MBOM |
| 9 | |
| 10 | |
| 11 | @dataclass(frozen=True) |
| 12 | class MBOMDiff: |
| 13 | added: list[ModelComponent] |
| 14 | removed: list[ModelComponent] |
| 15 | changed: list[tuple[ModelComponent, ModelComponent]] # (old, new) same id different hash |
| 16 | |
| 17 | @property |
| 18 | def is_empty(self) -> bool: |
| 19 | """True iff the two MBOMs describe the same component set with identical hashes.""" |
| 20 | return not self.added and not self.removed and not self.changed |
| 21 | |
| 22 | |
| 23 | def diff_mboms(old: MBOM, new: MBOM) -> MBOMDiff: |
| 24 | old_by_id = {c.component_id: c for c in old.components} |
| 25 | new_by_id = {c.component_id: c for c in new.components} |
| 26 | |
| 27 | added = [c for cid, c in new_by_id.items() if cid not in old_by_id] |
| 28 | removed = [c for cid, c in old_by_id.items() if cid not in new_by_id] |
| 29 | changed: list[tuple[ModelComponent, ModelComponent]] = [] |
| 30 | for cid, old_c in old_by_id.items(): |
| 31 | if cid in new_by_id and old_c.hash() != new_by_id[cid].hash(): |
| 32 | changed.append((old_c, new_by_id[cid])) |
| 33 | return MBOMDiff(added=added, removed=removed, changed=changed) |
| 34 | |