Merging
All domain entities — Scope, UserStory, and AcceptanceCriterion — inherit from MergeableModel (provided by the pleroma library). This gives them controlled merge semantics: a child entity overrides a parent entity field by field.
Scalar field merging
For any simple field (str, bool, int, Enum), the child's value replaces the parent's:
from fushinryu_model import AcceptanceCriterion
parent = AcceptanceCriterion(id=1, given="a user is logged in", then="the dashboard is shown")
child = AcceptanceCriterion(id=1, given=None, then="the profile is shown")
merged = AcceptanceCriterion.merge([parent, child])
# merged.given == "a user is logged in" ← parent kept (child was None)
# merged.then == "the profile is shown" ← child wins
None values from the child do not override a non-None parent value unless overwrite_none=True is passed explicitly.
Collection merging with merge_by_id
Collections of entities (acceptance criteria within a user story, user stories within a scope) are merged using the internal merge_by_id utility: entities are keyed by their integer id; overlapping ids are merged recursively; non-overlapping entities from both sets are included as-is.
from fushinryu_model import AcceptanceCriterion, UserStory, UserStoryType
parent_story = UserStory(
id=1, who="developer", what="X", why="Y", type=UserStoryType.FUNCTIONAL,
acceptance_criteria=frozenset([
AcceptanceCriterion(id=1, then="original condition"),
AcceptanceCriterion(id=2, then="only in parent"),
]),
)
child_story = UserStory(
id=1, who="developer", what="X", why="Y", type=UserStoryType.FUNCTIONAL,
acceptance_criteria=frozenset([
AcceptanceCriterion(id=1, then="overridden condition"), # same id → merged
AcceptanceCriterion(id=3, then="only in child"), # new id → added
]),
)
merged = UserStory.merge([parent_story, child_story])
# AC id=1 → "overridden condition"
# AC id=2 → "only in parent"
# AC id=3 → "only in child"
Validation record merging
Validation records follow different rules depending on their type.
| Type | Merge behaviour |
|---|---|
ManualValidation |
All records from both parent and child are accumulated |
AutomatedValidation |
Deduplicated by (source, name); the record with the most recent timestamp wins |
This means manual review evidence is never discarded, while automated test results are automatically superseded by their latest run.
When merging is used
Merging is the mechanism that powers scope hierarchy collapse. When Scope.collapse() is called, every scope in the ancestry is merged from lowest to highest precedence, producing a single flat scope with all inherited stories and criteria resolved.