Domain Model
fushinryu-model is built around entity types that form a containment hierarchy.
graph TD
S[Scope] -->|contains| US[UserStory]
S -->|backlog| T[Task]
S -->|rule_categories| CAT[Category]
CAT -->|subcategories| CAT
CAT -->|ruleset| RS[Ruleset]
RS --> CMD[Commandment]
RS --> SUG[Suggestion]
US -->|contains| AC[AcceptanceCriterion]
AC -->|contains| V[ValidationEntry\nManualValidation · AutomatedValidation]
T -. references .-> AC
S -->|roles| R[Role]
R -->|parent| R
R -->|competences| RC[RoleCompetence]
RC --> C[Competence]
S -->|role_assignments| RA[RoleAssignment]
RA --> EMP[Employee]
RA --> R
EMP -->|competences| EC[EmployeeCompetence]
EC --> C
TP[TrainingProgram] -->|competences| PC[ProgramCompetence]
PC --> C
RT[ReceivedTraining] --> TP
RT --> EMP
RT --> TR[Trainer]
US -->|history| HE[UserStoryHistoryEntry]
T -->|history| HE2[HistoryEntry]
R -->|history| HE2
US -->|comments| CM[Comment]
T -->|comments| CM
S -->|risks| RISK[Risk]
RISK -->|potential_assessment / residual_assessment| RA2[RiskAssessment]
RISK -->|risk_controls| RCTL[RiskControl]
RCTL -->|procedure| P2[Procedure]
Scope
A Scope is the top-level organisational unit. It groups related UserStory instances, Task work items, and rule categories under a named, identifiable container. Scopes can declare parent scopes, forming a directed acyclic graph (DAG) that supports inheritance — see Scope Hierarchy.
Key constraints
idmust follow programming identifier rules: start with a letter or_letter, followed by letters, digits, or underscores.- User story
idvalues must be unique within a scope. - Task
idvalues must be unique within the scope's backlog. descriptionis optional free-form text; it is inherited from parent scopes when absent.- Category
namevalues must be unique withinrule_categories.
Task
A Task represents a concrete work item assigned to a scope's backlog. It carries a title, a full description, a priority, a kind, and a lifecycle_status. The on_hold flag marks a task as temporarily blocked without changing its lifecycle state.
The related_acs field is a frozenset[str] of acceptance criterion references (e.g. "AC1.2") that the task addresses. It merges as a union across the scope hierarchy, the same way UserStory.tags does.
Key constraints
| Field | Constraint |
|---|---|
id |
Integer, unique within the enclosing scope's backlog |
estimated_duration |
Required for enhancement, bug, documentation, release, deployment |
active_duration / actual_duration |
If only one is set, the other defaults to the same value |
related_acs |
String references to AC ids; merged as a union |
Requirements
Requirements are captured as UserStory instances broken down into AcceptanceCriterion entries, each backed by ValidationEntry evidence.
UserStory
A UserStory captures a high-level requirement from the perspective of a role using the classic As a / I want / so that structure, stored as who, what, and why fields.
Stories are classified as either functional or technical via the UserStoryType enum.
The optional dod field holds a free-form definition of done. The tags field is an unordered set of short label strings (frozenset[str]) used for categorisation; tags from parent and child stories are merged as a union.
Key constraints
- Acceptance criterion
idvalues must be unique within a story. - A story is considered validated (
is_validated = True) only when it is active, has at least one active acceptance criterion, and all active criteria are validated.
AcceptanceCriterion
An AcceptanceCriterion encodes the Given / When / Then structure of a testable condition.
| Field | Required | Description |
|---|---|---|
id |
yes | Integer, unique within the enclosing story |
then |
yes | The expected outcome |
given |
no | Initial context or state |
when |
no | Trigger condition |
tags |
no | Unordered set of short label strings; merged as union |
requirement_specification |
no | ISO 29148 document type (SRS, SyRS, or StRS) |
requirement_group |
no | Requirement category within the specification |
When both classification fields are provided, requirement_group must be valid for the chosen requirement_specification. Setting requirement_group without requirement_specification raises a validation error.
A criterion is validated (is_validated = True) when its validations set is non-empty and every record has passed=True.
ValidationEntry
Validation evidence is stored as a discriminated union of two frozen record types.
ManualValidation
Recorded by a human reviewer. Requires a verdict string explaining why the criterion passed or failed.
AutomatedValidation
Linked to an automated test or check. Identified by source (the file or module) and name (the test name). During merges, automated validations are deduplicated by (source, name), keeping the most recent timestamp.
The active flag
All three entity types carry an active: bool field (default True). Setting it to False performs a soft deletion — the entity is retained in the data but excluded from validation checks. An inactive UserStory implicitly deactivates all its acceptance criteria for validation purposes, regardless of their individual active values.
Rule Categories
A Scope carries a rule_categories field — a frozenset[Category] that represents the hatto (法度): the set of scoped policy rules that govern the scope.
Category
A Category groups Commandment and Suggestion instances that apply under a stated condition. Key fields:
| Field | Required | Description |
|---|---|---|
name |
yes | Identifier-pattern string; unique within its parent's subcategories |
applicability |
yes | Free-form description of when the category's rules apply |
tags |
no | Short label strings; defaults to empty frozenset |
additive |
no | Controls suggestion merge behaviour (see Merging); defaults to False |
ruleset |
no | The category's Ruleset; defaults to empty |
subcategories |
no | Nested child categories; merged by name when scopes are merged |
Categories form a tree. The effective_commandments and effective_suggestions properties return the union of a category's own rules and all its subcategories' rules, recursively.
Commandment and Suggestion
Both are frozen value objects with a single body: str field. A non-empty, non-whitespace body is required.
Commandment— must be honoured without exception.Suggestion— should be honoured; may be waived under justified conditions.
Ruleset
A Ruleset holds two frozensets: commandments and suggestions. Both default to empty.
Roles and Workforce
A Scope carries two workforce-related fields: roles and role_assignments.
Role
A Role is a named actor within a scope. Key fields:
| Field | Required | Description |
|---|---|---|
name |
yes | Identifier-pattern string; unique within Scope.roles |
description |
yes | Non-empty description of the role's responsibilities |
description_mode |
no | DescriptionMode; controls how the child's description is composed with the parent's on merge; defaults to OVERRIDE |
parent |
no | The role this one is derived from; defaults to None |
competences |
no | frozenset[RoleCompetence]; competence names must be unique |
The flatten() method returns a copy of the role with parent=None, resolving the full chain. Scope.collapse() calls flatten() on every role automatically.
When two scopes are merged, roles sharing the same name are implicitly linked: the child scope's role receives the parent scope's role as its parent, building an explicit hierarchy across scope levels.
Competence
Competence is a pure value object identified by the (area, category, name) triple. Two instances with identical fields are considered equal.
CompetenceLevel is an ordered int enum (NONE=0, BASIC=1, MODERATE=2, PROFICIENT=3) representing proficiency. CompetenceRelevance is an ordered int enum (UNRELATED=0, RELEVANT=1, DESIRABLE=2, KEY=4) acting as a multiplier in weighted computations.
RoleCompetence binds a Competence to a role:
| Field | Description |
|---|---|
competence |
The skill being bound |
relevance |
CompetenceRelevance — how important this skill is for the role |
proficiency_threshold |
CompetenceLevel — minimum required level |
When parent and child scope roles are merged, RoleCompetence bindings use max-value semantics: the independently higher relevance and proficiency threshold from either side are kept.
Employee
An Employee represents a person in the system.
| Field | Description |
|---|---|
id |
Non-empty string; unique identifier (e.g. "EMP-001") |
username |
Non-empty login name |
competences |
frozenset[EmployeeCompetence]; each competence at most once |
EmployeeCompetence binds a Competence to the employee's actual CompetenceLevel.
RoleAssignment, TrainingNeeds, and idoneity
A RoleAssignment links an Employee to a Role and exposes two derived metrics.
training_needs() -> TrainingNeeds — computes the per-competence gap between the employee's profile and the role's requirements. For each RoleCompetence on the role:
- Looks up the employee's current level. Missing competences default to
NONE. - Produces a
TrainingNeedwithpartial_distance = max(0, threshold - level) × relevance.
TrainingNeeds.total_distance sums all partial_distance values. Zero means the employee meets every threshold; higher values indicate greater training demand.
idoneity — weighted average suitability score:
Only non-UNRELATED competences contribute. Returns 0.0 when no relevant competences are defined on the role. The maximum value equals CompetenceLevel.PROFICIENT (3.0).
Scope.role_assignments is a frozenset[RoleAssignment]. When scopes are merged it follows child-override semantics keyed by (employee.id, role.name).
Training
TrainingProgram and ReceivedTraining are standalone entities — they are never attached to a Scope collection and carry no merge semantics.
TrainingProgram
Represents an available training course.
| Field | Required | Description |
|---|---|---|
id |
yes | Integer identifier |
title |
yes | Non-empty course title |
syllabus |
yes | Non-empty description of course content |
duration |
yes | Positive timedelta |
competences |
no | frozenset[ProgramCompetence]; each competence at most once; defaults to empty |
ProgramCompetence
Binds a Competence to the CompetenceLevel at which it is taught by the program.
| Field | Description |
|---|---|
competence |
The skill covered by the program |
level |
CompetenceLevel — proficiency level taught |
Trainer
Value object identifying who delivered a training session.
| Field | Required | Description |
|---|---|---|
name |
yes | Non-empty name of the trainer |
organization |
no | Employing organisation; defaults to None |
ReceivedTraining
Records that an employee completed a training program.
| Field | Required | Description |
|---|---|---|
program |
yes | The TrainingProgram completed |
employee |
yes | The Employee who attended |
received_on |
yes | datetime.date of completion |
location |
yes | Non-empty location where training took place |
trainer |
yes | The Trainer who delivered the session |
evidences |
no | frozenset[str] of certificate URLs or proof references; defaults to empty |
Procedures
A Scope carries procedures: frozenset[Procedure] — documented procedures modelled as typed flowcharts.
Procedure
| Field | Required | Description |
|---|---|---|
id |
yes | Non-empty identifier; unique within Scope.procedures; doubles as the merge key |
description |
yes | Non-empty description of the procedure's purpose |
nodes |
yes | frozenset[ProcedureNode]; node ids must be unique within the procedure |
Construction validates referential integrity: every node's linkage target (its next, a decision's case targets, or a fork's branch targets) must resolve to a node id present in the same procedure. Multiple start terminators are allowed — a procedure may have more than one entry point.
ProcedureNode kinds
ProcedureNode is a discriminated union (on the kind field) of 11 flowchart node classes. Every node carries id, description, and rasci: frozenset[RasciAssignment] (role name + RASCILevel, unique role names per node).
| Kind | kind value |
Extra fields |
|---|---|---|
| Start terminator | "start" |
next |
| End terminator | "end" |
(none — terminal) |
| Activity | "activity" |
next |
| Decision | "decision" |
cases: frozenset[DecisionCase] (case label + next; unique labels) |
| Input | "input" |
next |
| Output | "output" |
next |
| Predefined procedure | "predefined_procedure" |
procedure_id (references another Procedure.id), next |
| Manual operation | "manual_operation" |
next |
| Manual input | "manual_input" |
next |
| Fork | "fork" |
branches: frozenset[str] |
| Sync | "sync" |
next |
Risks
A Scope carries risks: frozenset[Risk] — a risk register of the project's risks and how they are being controlled.
Risk
| Field | Required | Description |
|---|---|---|
id |
yes | Integer identifier; unique within Scope.risks; doubles as the merge key |
description |
yes | Non-empty description of the risk |
rasci |
no | frozenset[RasciAssignment]; responsibility for managing the risk overall (not for executing any single control); unique role names; defaults to empty |
potential_assessment |
yes | The RiskAssessment before any controls are applied |
residual_assessment |
yes | The RiskAssessment after its controls are applied |
risk_controls |
no | frozenset[RiskControl]; defaults to empty |
RiskAssessment
Scores three raw indexes, each in [0.0, 1.0], and categorizes them via a pluggable RiskAssessmentStrategy:
| Field | Description |
|---|---|
impact / impact_category |
Consequence if the risk materialises, and its category label |
probability / probability_category |
Likelihood of occurrence, and its category label |
threat / threat_category |
Overall danger posed by the risk, and its category label |
strategy |
The RiskAssessmentStrategy instance that produced the three categories |
strategy_name |
Computed field — type(strategy).__name__ — identifying the strategy once serialized |
Categories are plain str, not a fixed enum, because different strategies may use different vocabularies. The default ThresholdRiskAssessmentStrategy splits [0.0, 1.0] into four quartile buckets and labels them using the library's own 4-level, semantically ordered enums: ImpactLevel (NEGLIGIBLE < MINOR < MAJOR < SEVERE), ProbabilityLevel (RARE < UNLIKELY < LIKELY < ALMOST_CERTAIN), and RiskLevel (LOW < MODERATE < HIGH < CRITICAL). Use RiskAssessment.assess(impact, probability, threat, strategy=...) to build one from raw indexes; a custom RiskAssessmentStrategy subclass may be substituted.
RiskControl
A measure taken to reduce a risk from its potential to its residual assessment.
| Field | Required | Description |
|---|---|---|
description |
yes | Non-empty description of the control measure |
procedure |
no | An optional, self-contained Procedure to follow; defaults to None |
History and Comments
Two opt-in mixins provide cross-cutting concerns that can be applied to any entity.
Historized
Historized adds a history: frozenset[HistoryEntry] field to an entity. History entries accumulate across scope merges — no entry is ever discarded (union semantics, identical to ManualValidation).
HistoryEntry is a frozen value object with:
| Field | Required | Description |
|---|---|---|
kind |
yes | ChangeKind — one of CREATED, UPDATED, DEACTIVATED, SUPERSEDED |
timestamp |
yes | Timezone-aware datetime of the change |
author |
no | Optional string identifying who made the change |
UserStoryHistoryEntry extends HistoryEntry with two additional fields for AC-level traceability:
| Field | Description |
|---|---|
added_acs |
frozenset[int] — AC ids added in this change event |
deactivated_acs |
frozenset[int] — AC ids deactivated in this change event |
UserStory overrides the history field type to frozenset[UserStoryHistoryEntry]. Task and Role use the base frozenset[HistoryEntry].
Commentable
Commentable adds a comments: frozenset[Comment] field to an entity. Comments accumulate across scope merges — no comment is ever discarded (union semantics).
Comment is a frozen value object with:
| Field | Required | Description |
|---|---|---|
id |
yes | Integer identifier |
body |
yes | Non-empty comment text |
timestamp |
yes | Timezone-aware datetime of the comment |
author |
no | Optional string identifying the commenter |
Which entities carry each mixin
| Entity | Historized | Commentable |
|---|---|---|
UserStory |
yes (with UserStoryHistoryEntry) |
yes |
Task |
yes | yes |
Role |
yes | no |