Skip to content

Scope Hierarchy

A Scope can declare an ordered list of parent scopes via its parents field. This forms a directed acyclic graph (DAG) that enables requirement inheritance: child scopes override parent definitions while still inheriting anything not explicitly changed.

Declaring parents

from fushinryu_model import Scope

base = Scope(id="base", name="Base", description="Shared requirements.")
extended = Scope(id="extended", name="Extended", description="Extended requirements.", parents=(base,))

The parents tuple is ordered: the first parent takes precedence over subsequent parents when the hierarchy is collapsed.

Collapsing the hierarchy

Scope.collapse() flattens the entire ancestry into a single Scope with no parents. The returned scope contains the merged result of every ancestor's user stories and acceptance criteria.

flat = extended.collapse()
# flat.parents == ()
# flat.user_stories contains inherited and overriding stories merged

Linearisation algorithm

collapse() determines merge order using DFS pre-order traversal with last-occurrence deduplication:

  1. The root scope is visited first (highest precedence).
  2. Parents are visited in order; each scope's entire subtree is traversed before moving to the next sibling.
  3. If a scope appears more than once (shared ancestor — the diamond pattern), only its last occurrence in the traversal is kept.

The result is a list ordered from highest to lowest merge precedence. Scopes are then merged from lowest to highest, so that higher-precedence scopes win.

Diamond example

        A (grandparent)
       / \
      B   C  (parents of D, in that order)
       \ /
        D (root)
A = Scope(id="a", name="A", description="Grandparent.")
B = Scope(id="b", name="B", description="Parent B.", parents=(A,))
C = Scope(id="c", name="C", description="Parent C.", parents=(A,))
D = Scope(id="d", name="D", description="Root.", parents=(B, C))

flat = D.collapse()
# Linearisation order (highest → lowest precedence): D, B, C, A
# A appears under both B and C but contributes only once, at the lowest precedence position.

Precedence order: D > B > C > A. B takes precedence over C because it is listed first in D.parents.

Cycle detection

collapse() raises ValueError with a "Cycle detected" message if the parent graph contains a cycle. Cycles are invalid and cannot be collapsed.

A = Scope(id="a", name="A", description=".")
# Constructing a genuine cycle requires object mutation (not possible with frozen Pydantic models),
# but the guard exists for programmatically constructed graphs.