Skip to content

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.