Project Models¶
Overview¶
The project model is a set of Pydantic v2 classes that together define every aspect of a simulation input. At the top sits Project, which owns ProjectMetadata, a list of Task objects, optional Risk entries, and optional ResourceSpec/CalendarSpec pools. Pydantic validators enforce structural rules at parse time: dependency integrity (no cycles, no missing IDs), symbolic estimate rules (T-shirt sizes and story points must not include an explicit unit), and threshold validation (red/green confidence thresholds). The YAML and TOML parsers both return a validated Project instance — these models are what you read or build when working programmatically.
When to use this module: Use it directly when constructing projects in code, inspecting parsed results, or adding custom validation logic on top of a parsed Project.
| Capability | Description |
|---|---|
| Hierarchical schema | Project composes metadata, tasks, risks, resources, and calendars in one validated object |
| Flexible task estimates | TaskEstimate supports explicit low/expected/high ranges, T-shirt sizes, or story points |
| Dependency validation | Model validator rejects unknown depends_on IDs and cyclic dependency graphs |
| Resource and calendar specs | ResourceSpec and CalendarSpec define availability, productivity, and working-day rules |
| Risk modelling | Risk entries carry probability, impact type (percentage or absolute), and optional distribution |
| Config-driven symbolic estimates | T-shirt and story-point mappings are resolved from Config at simulation time, not stored on the model |
Imports:
from mcprojsim.models.project import (
Project, ProjectMetadata, Task, TaskEstimate, Risk,
ResourceSpec, CalendarSpec, UncertaintyFactors,
)
Project¶
Represents the complete project definition with metadata, tasks, risks, resources, and calendars.
Fields:
| Field | Type | Default | Description |
|---|---|---|---|
project |
ProjectMetadata |
required | Project name, start date, hours/day, and thresholds |
tasks |
list[Task] |
required | Work items and their dependencies |
project_risks |
list[Risk] |
[] |
Top-level project-level risks |
resources |
list[ResourceSpec] |
[] |
Named resource pools |
calendars |
list[CalendarSpec] |
[] |
Named calendars for holidays/exclusions |
sprint_planning |
SprintPlanningSpec \| None |
None |
Sprint planning configuration if enabled |
Key methods:
get_task_by_id(task_id: str) -> Task | None— Retrieve a task by its ID
Example:
from mcprojsim.parsers import YAMLParser
parser = YAMLParser()
project = parser.parse_file("project.yaml")
# Access project metadata
print(f"Project name: {project.project.name}")
print(f"Start date: {project.project.start_date}")
print(f"Total tasks: {len(project.tasks)}")
# Find a specific task
task = project.get_task_by_id("backend_api")
if task:
print(f"Task: {task.name}, Dependencies: {task.dependencies}")
# Access all risks
for risk in project.project_risks:
print(f"Risk: {risk.name}, Probability: {risk.probability}")
# Check if sprint planning is configured
if project.sprint_planning and project.sprint_planning.enabled:
print(f"Sprint length: {project.sprint_planning.sprint_length_weeks} weeks")
Example: Building a Project Programmatically
from datetime import date
from mcprojsim.models.project import (
Project, ProjectMetadata, Task, TaskEstimate, Risk,
)
project = Project(
project=ProjectMetadata(
name="My API Project",
start_date=date(2026, 6, 1),
hours_per_day=8.0,
),
tasks=[
Task(
id="design",
name="API Design",
estimate=TaskEstimate(low=8, expected=16, high=32),
),
Task(
id="implement",
name="Implementation",
estimate=TaskEstimate(low=40, expected=80, high=160),
dependencies=["design"],
),
Task(
id="test",
name="Testing",
estimate=TaskEstimate(t_shirt_size="M"),
dependencies=["implement"],
),
],
project_risks=[
Risk(
id="scope_creep",
name="Scope Creep",
probability=0.3,
impact=40.0,
),
],
)
ProjectMetadata¶
Stores top-level project settings and characteristics.
Fields:
| Field | Type | Default | Description |
|---|---|---|---|
name |
str |
required | Project name |
description |
str \| None |
None |
Optional project description |
start_date |
date |
required | Project start date |
hours_per_day |
float |
8.0 |
Working hours per day |
currency |
str \| None |
None |
ISO 4217 currency code for cost tracking (e.g. "USD", "EUR", "SEK"). When not set, falls back to config.cost.currency (default "EUR") |
default_hourly_rate |
float \| None |
None |
Default hourly rate applied to all tasks that don't specify a per-task or per-resource rate. Must be ≥ 0. |
overhead_rate |
float |
0.0 |
Overhead multiplier applied on top of hourly rates (0.0–3.0). E.g. 0.15 adds 15 % overhead. |
secondary_currencies |
list[str] |
[] |
Up to 5 additional ISO 4217 currency codes for multi-currency cost output. |
fx_conversion_cost |
float |
0.0 |
Flat conversion fee fraction (0.0–0.50) applied when converting to secondary currencies. |
fx_overhead_rate |
float |
0.0 |
Additional overhead fraction for FX conversions (0.0–1.0). |
fx_rates |
dict[str, float] |
{} |
Manual FX rate overrides keyed by target currency code. When not set, live rates are fetched. |
confidence_levels |
list[int] |
[10, 25, 50, 75, 80, 85, 90, 95, 99] |
Percentiles to include in reports |
probability_red_threshold |
float |
0.5 |
Probability below which delivery is shown as red |
probability_green_threshold |
float |
0.9 |
Probability above which delivery is shown as green |
distribution |
DistributionType |
"triangular" |
Default task duration distribution |
team_size |
int \| None |
None |
Total team size; used for coordination overhead and auto-generating resources |
t_shirt_size_default_category |
str \| None |
None |
Default T-shirt size category for this project (overrides config when set) |
uncertainty_factors |
UncertaintyFactors \| None |
None |
Project-level uncertainty factor overrides (applied to all tasks unless overridden per-task) |
Example:
project = parser.parse_file("project.yaml")
meta = project.project
print(f"Project: {meta.name}")
print(f"Start: {meta.start_date}")
print(f"Tracked thresholds: {meta.confidence_levels}")
print(f"Hours/day: {meta.hours_per_day}")
if meta.team_size:
print(f"Team size: {meta.team_size} people")
Task¶
Represents a single work item in the project network.
Fields:
| Field | Type | Default | Description |
|---|---|---|---|
id |
str |
required | Unique task identifier |
name |
str |
required | Human-readable task name |
description |
str \| None |
None |
Optional task description |
estimate |
TaskEstimate |
required | Duration estimate |
dependencies |
list[str] |
[] |
Task IDs this task depends on |
uncertainty_factors |
UncertaintyFactors \| None |
UncertaintyFactors() |
Uncertainty adjustments; defaults to a new instance with all-medium/colocated levels |
resources |
list[str] |
[] |
Required resource names |
max_resources |
int |
1 |
Maximum number of resources that can be assigned |
min_experience_level |
int |
1 |
Minimum experience level required (1, 2, or 3) |
planning_story_points |
int \| None |
None |
Story points override for sprint planning |
priority |
int \| None |
None |
Scheduling priority hint |
spillover_probability_override |
float \| None |
None |
Override for sprint spillover probability (0.0–1.0) |
risks |
list[Risk] |
[] |
Task-specific risks |
fixed_cost |
float \| None |
None |
One-time fixed cost for this task (in the project currency). Added to the cost total regardless of duration. |
Key methods:
has_dependency(task_id: str) -> bool— Check if this task depends on another taskget_planning_story_points() -> int | None— Return sprint-planning story points (falls back toestimate.story_points)
Example:
# Iterate over all tasks
for task in project.tasks:
print(f"Task: {task.id} - {task.name}")
print(f" Description: {task.description or 'N/A'}")
print(f" Resources: {task.resources}")
print(f" Depends on: {task.dependencies}")
if task.estimate.high is not None:
print(f" Estimate: {task.estimate.low}-{task.estimate.expected}-{task.estimate.high} hours")
if task.risks:
print(f" Risks: {len(task.risks)} identified")
print()
# Find tasks with no dependencies
root_tasks = [t for t in project.tasks if not t.dependencies]
print(f"Root tasks (no dependencies): {[t.id for t in root_tasks]}")
# Find the longest task (only among tasks with explicit high estimates)
longest_task = max(project.tasks, key=lambda t: t.estimate.high if t.estimate.high is not None else 0)
print(f"Longest task: {longest_task.id}, up to {longest_task.estimate.high} hours")
TaskEstimate¶
Supports four estimation styles:
- Triangular estimates via
low,expected, andhigh - Log-normal estimates via
low,expected, andhigh - T-shirt-sized estimates via
t_shirt_size - Story Point estimates via
story_points
Fields:
| Field | Type | Default | Description |
|---|---|---|---|
distribution |
DistributionType \| None |
None |
Override the project-level distribution for this task |
low |
float \| None |
None |
Minimum duration (optimistic). Also accepted as min. |
expected |
float \| None |
None |
Best-guess duration. Also accepted as most_likely. |
high |
float \| None |
None |
Maximum duration (pessimistic). Also accepted as max. |
t_shirt_size |
str \| None |
None |
Size token, e.g. "M", "XL", or "epic.L" |
story_points |
int \| None |
None |
Story point value (allowed: 1, 2, 3, 5, 8, 13, 21) |
unit |
EffortUnit \| None |
None (hours for explicit estimates) |
Effort unit ("hours", "days", "weeks"). Must not be set for symbolic estimates — the unit comes from configuration. |
Example:
for task in project.tasks:
est = task.estimate
if est.story_points:
print(f"Task {task.id}: {est.story_points} story points")
elif est.t_shirt_size:
print(f"Task {task.id}: {est.t_shirt_size} T-shirt")
else:
print(f"Task {task.id}: {est.low}-{est.expected}-{est.high} hours")
# Show the distribution type (if overridden for this task)
if est.distribution:
print(f" Distribution: {est.distribution.value}")
Risk and RiskImpact¶
Represents task-level or project-level risk.
Risk fields:
| Field | Type | Default | Description |
|---|---|---|---|
id |
str |
required | Unique risk identifier |
name |
str |
required | Human-readable risk name |
probability |
float |
required | Probability this risk occurs (0.0–1.0) |
impact |
float \| RiskImpact |
required | Time penalty in hours (float), or a RiskImpact object |
description |
str \| None |
None |
Optional description |
cost_impact |
float \| None |
None |
Monetary cost impact when this risk triggers (in the project currency). Applied on top of the duration impact. |
RiskImpact fields:
| Field | Type | Default | Description |
|---|---|---|---|
type |
ImpactType |
required | "percentage" or "absolute" |
value |
float |
required | Percentage (0–100) or absolute duration |
unit |
EffortUnit \| None |
None |
Unit for absolute impacts ("hours", "days", "weeks") |
Risk methods:
get_impact_value(base_duration: float = 0.0, hours_per_day: float = 8.0) -> float— Calculate numeric impact in hours
Example:
# Access project-level risks
for risk in project.project_risks:
impact = risk.get_impact_value()
print(f"Project risk: {risk.name}")
print(f" Probability: {risk.probability * 100:.0f}%")
print(f" Impact: {impact:.1f} hours")
# Access task-level risks
for task in project.tasks:
for risk in task.risks:
impact = risk.get_impact_value(base_duration=task.estimate.expected)
print(f"Task {task.id} risk: {risk.name}")
print(f" Probability: {risk.probability * 100:.0f}%")
print(f" Impact: {impact:.1f} hours")
ResourceSpec¶
Defines an individual resource (team member) that can be assigned to tasks.
Fields:
| Field | Type | Default | Description |
|---|---|---|---|
name |
str \| None |
None |
Resource name (e.g. "Alice"). Auto-generated as resource_001 etc. if omitted. |
id |
str \| None |
None |
Legacy identifier used as fallback when name is not set |
availability |
float |
1.0 |
Fraction of time available (must be > 0.0, ≤ 1.0) |
calendar |
str |
"default" |
Calendar identifier to use |
experience_level |
int |
2 |
Skill level: 1 (junior), 2 (mid), or 3 (senior) |
productivity_level |
float |
1.0 |
Productivity multiplier (0.1–2.0) |
sickness_prob |
float |
0.0 |
Probability of absence per scheduling unit (0.0–1.0) |
planned_absence |
list[date] |
[] |
Specific dates this resource is unavailable |
hourly_rate |
float \| None |
None |
Per-resource hourly rate (overrides ProjectMetadata.default_hourly_rate for tasks assigned to this resource). Must be ≥ 0. |
Example:
for resource in project.resources:
print(f"Resource: {resource.name}")
print(f" Availability: {resource.availability * 100:.0f}%")
print(f" Experience level: {resource.experience_level}")
print(f" Calendar: {resource.calendar}")
if resource.planned_absence:
print(f" Planned absence: {len(resource.planned_absence)} days")
CalendarSpec¶
Defines a working calendar for scheduling.
Fields:
| Field | Type | Default | Description |
|---|---|---|---|
id |
str |
"default" |
Calendar identifier |
work_hours_per_day |
float |
8.0 |
Working hours per day |
work_days |
list[int] |
[1, 2, 3, 4, 5] |
Working days of the week (1=Monday … 7=Sunday) |
holidays |
list[date] |
[] |
Specific non-working dates |
Example:
for calendar in project.calendars:
print(f"Calendar: {calendar.id}")
print(f" Work hours/day: {calendar.work_hours_per_day}")
print(f" Work days: {calendar.work_days}")
if calendar.holidays:
print(f" Holidays: {len(calendar.holidays)} days")
UncertaintyFactors¶
Applies multipliers to adjust base task duration based on project characteristics.
Fields:
| Field | Type | Default | Description |
|---|---|---|---|
team_experience |
str \| None |
"medium" |
Team experience: "high", "medium", or "low" |
requirements_maturity |
str \| None |
"medium" |
Requirements maturity: "high", "medium", or "low" |
technical_complexity |
str \| None |
"medium" |
Technical complexity: "low", "medium", or "high" |
team_distribution |
str \| None |
"colocated" |
Team distribution: "colocated" or "distributed" |
integration_complexity |
str \| None |
"medium" |
Integration complexity: "low", "medium", or "high" |
All fields default to their neutral level ("medium" or "colocated"), which maps to a 1.0 multiplier and has no effect on the estimate. Only changing a field away from its default affects the task duration. The actual multiplier values are defined in Config.uncertainty_factors.
Example:
for task in project.tasks:
if task.uncertainty_factors:
factors = task.uncertainty_factors
print(f"Task {task.id} uncertainty adjustments:")
if factors.team_experience:
print(f" Team experience: {factors.team_experience}")
if factors.technical_complexity:
print(f" Technical complexity: {factors.technical_complexity}")
Sprint Planning Models¶
These models define the sprint-planning input configuration. They are populated from the sprint_planning: block in a project YAML file.
SprintPlanningSpec¶
Top-level sprint planning configuration.
Fields:
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool |
False |
Whether sprint planning is active |
sprint_length_weeks |
int |
required | Length of each sprint in weeks |
capacity_mode |
SprintCapacityMode |
required | "story_points" or "tasks" |
history |
list[SprintHistoryEntry] |
[] |
Historical sprint outcomes (minimum 2 usable rows required when enabled) |
planning_confidence_level |
float |
0.80 |
Confidence level for commitment guidance (0–1) |
removed_work_treatment |
RemovedWorkTreatment |
"churn_only" |
How removed scope affects forecasts: "churn_only" or "reduce_backlog" |
future_sprint_overrides |
list[FutureSprintOverrideSpec] |
[] |
Per-sprint capacity adjustments |
volatility_overlay |
SprintVolatilitySpec |
SprintVolatilitySpec() |
Sprint disruption model |
spillover |
SprintSpilloverSpec |
SprintSpilloverSpec() |
Task spillover model |
velocity_model |
SprintVelocityModel |
"empirical" |
"empirical" or "neg_binomial" |
sickness |
SprintSicknessSpec |
SprintSicknessSpec() |
Per-person sickness model |
SprintHistoryEntry¶
One historical sprint outcome row.
Fields:
| Field | Type | Default | Description |
|---|---|---|---|
sprint_id |
str |
required | Unique identifier for this sprint |
sprint_length_weeks |
int \| None |
None |
Override sprint length (defaults to parent SprintPlanningSpec.sprint_length_weeks) |
completed_story_points |
float \| None |
None |
Story points completed (mutually exclusive with completed_tasks) |
completed_tasks |
int \| None |
None |
Tasks completed (mutually exclusive with completed_story_points) |
spillover_story_points |
float |
0 |
Unfinished story points carried over |
spillover_tasks |
int |
0 |
Unfinished tasks carried over |
added_story_points |
float |
0 |
Story points added mid-sprint |
added_tasks |
int |
0 |
Tasks added mid-sprint |
removed_story_points |
float |
0 |
Story points removed mid-sprint |
removed_tasks |
int |
0 |
Tasks removed mid-sprint |
holiday_factor |
float |
1.0 |
Capacity adjustment for holidays |
end_date |
date \| None |
None |
Sprint end date |
team_size |
int \| None |
None |
Team size during this sprint |
notes |
str \| None |
None |
Free-text notes |
FutureSprintOverrideSpec¶
Forward-looking capacity adjustment for a specific future sprint. At least one of sprint_number or start_date must be provided.
Fields:
| Field | Type | Default | Description |
|---|---|---|---|
sprint_number |
int \| None |
None |
Target sprint by 1-based number |
start_date |
date \| None |
None |
Target sprint by start date (must align to a sprint boundary) |
holiday_factor |
float |
1.0 |
Capacity adjustment for holidays |
capacity_multiplier |
float |
1.0 |
Overall capacity multiplier for this sprint |
notes |
str \| None |
None |
Free-text notes |
SprintVolatilitySpec¶
Sprint-level disruption overlay (unexpected events reducing capacity).
Fields:
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool |
False |
Whether the volatility overlay is active |
disruption_probability |
float |
0.0 |
Probability of disruption per sprint (0.0–1.0) |
disruption_multiplier_low |
float |
1.0 |
Low end of triangular capacity-reduction distribution |
disruption_multiplier_expected |
float |
1.0 |
Expected value of capacity-reduction distribution |
disruption_multiplier_high |
float |
1.0 |
High end of triangular capacity-reduction distribution |
SprintSpilloverSpec¶
Task-level execution spillover model.
Fields:
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool |
False |
Whether spillover modeling is active |
model |
SprintSpilloverModel |
"table" |
"table" or "logistic" |
size_reference_points |
float |
5.0 |
Reference size for normalizing story-point brackets |
size_brackets |
list[SprintSpilloverBracketSpec] |
(default table) | Table-model probability brackets (ascending max_points, last entry must be unbounded) |
consumed_fraction_alpha |
float |
3.25 |
Beta distribution α for spilled fraction consumed |
consumed_fraction_beta |
float |
1.75 |
Beta distribution β for spilled fraction consumed |
logistic_slope |
float |
1.9 |
Logistic model slope parameter |
logistic_intercept |
float |
≈ -1.992 |
Logistic model intercept parameter |
SprintSpilloverBracketSpec¶
One bracket in the table-based spillover model, mapping a story-point range to a spillover probability.
Fields:
| Field | Type | Default | Description |
|---|---|---|---|
max_points |
float \| None |
None |
Upper bound of this bracket in story points. None creates an unbounded catch-all bracket (must be last). |
probability |
float |
required | Spillover probability for tasks in this bracket (0.0–1.0) |
SprintSicknessSpec¶
Per-person sickness model for sprint capacity.
Fields:
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool |
False |
Whether sickness modeling is active |
team_size |
int \| None |
None |
Override team size for sickness calculations |
probability_per_person_per_week |
float |
0.058 |
Sickness probability per person per week |
duration_log_mu |
float |
0.693 |
Log-normal μ for sickness duration |
duration_log_sigma |
float |
0.75 |
Log-normal σ for sickness duration |
Enums¶
The following enums are part of the model API:
DistributionType—"triangular"or"lognormal"ImpactType—"percentage"or"absolute"EffortUnit—"hours","days", or"weeks"SprintCapacityMode—"story_points"or"tasks"SprintVelocityModel—"empirical"or"neg_binomial"SprintSpilloverModel—"table"or"logistic"RemovedWorkTreatment—"churn_only"or"reduce_backlog"ConstrainedSchedulingAssignmentMode—"greedy_single_pass"or"criticality_two_pass"