Skip to content

Formal Grammar Specification

This document specifies the accepted structure for Monte Carlo Project Simulator project and config files.

It is written in Extended Backus-Naur Form (EBNF) plus semantic constraints that are enforced by model validators.

Notation Conventions

  • ::= means "is defined as"
  • | means alternation
  • [] means optional (zero or one)
  • {} means repetition (zero or more)
  • { ... }+ means one or more
  • () groups elements
  • "" denotes literal strings
  • <> denotes terminal or non-terminal symbols

Project File Grammar

# Top-level project file
<project_file> ::= <project_section>
                   <tasks_section>
                   [<project_risks_section>]
                   [<resources_section>]
                   [<calendars_section>]
                   [<sprint_planning_section>]

# Project section
<project_section> ::= "project:" <project_metadata>

<project_metadata> ::= <project_name>
                       [<project_description>]
                       <start_date>
                       [<hours_per_day>]
                       [<currency>]
                       [<default_hourly_rate>]
                       [<overhead_rate>]
                       [<secondary_currencies>]
                       [<fx_conversion_cost>]
                       [<fx_overhead_rate>]
                       [<fx_rates>]
                       [<confidence_levels>]
                       [<probability_thresholds>]
                       [<project_distribution>]
                       [<project_t_shirt_default_category>]
                       [<project_uncertainty_factors>]
                       [<team_size>]

<project_name> ::= "name:" <string>
<project_description> ::= "description:" <string>
<start_date> ::= "start_date:" <date_string>
<hours_per_day> ::= "hours_per_day:" <positive_number>
<currency> ::= "currency:" <string>
<default_hourly_rate> ::= "default_hourly_rate:" <non_negative_number>
<overhead_rate> ::= "overhead_rate:" <non_negative_number>
<secondary_currencies> ::= "secondary_currencies:" <string_list>
<fx_conversion_cost> ::= "fx_conversion_cost:" <non_negative_number>
<fx_overhead_rate> ::= "fx_overhead_rate:" <non_negative_number>
<fx_rates> ::= "fx_rates:" <map>
<confidence_levels> ::= "confidence_levels:" "[" <percentile_list> "]"
<probability_thresholds> ::= [<red_threshold>] [<green_threshold>]
<red_threshold> ::= "probability_red_threshold:" <probability>
<green_threshold> ::= "probability_green_threshold:" <probability>
<project_distribution> ::= "distribution:" <distribution_type>
<project_t_shirt_default_category> ::= "t_shirt_size_default_category:" <category_name>
<project_uncertainty_factors> ::= "uncertainty_factors:" <factor_map>
<team_size> ::= "team_size:" <non_negative_integer>

<distribution_type> ::= "triangular" | "lognormal"
<percentile_list> ::= <positive_integer> { "," <positive_integer> }

# Tasks
<tasks_section> ::= "tasks:" <task_list>
<task_list> ::= { <task> }+
<task> ::= "-" <task_properties>

<task_properties> ::= <task_id>
                      <task_name>
                      [<task_description>]
                      <estimate>
                      [<dependencies>]
                      [<uncertainty_factors>]
                      [<task_resources>]
                      [<task_max_resources>]
                      [<task_min_experience_level>]
                      [<task_planning_story_points>]
                      [<task_priority>]
                      [<task_spillover_probability_override>]
                      [<task_risks>]
                      [<task_fixed_cost>]

<task_id> ::= "id:" <string_like_token>
<task_name> ::= "name:" <string>
<task_description> ::= "description:" <string>
<dependencies> ::= "dependencies:" <dependency_list>
<dependency_list> ::= "[]" | "[" <string_like_token_list> "]"

<task_resources> ::= "resources:" <string_like_token_list_bracketed>
<string_like_token_list_bracketed> ::= "[]" | "[" <string_like_token_list> "]"
<task_max_resources> ::= "max_resources:" <positive_integer>
<task_min_experience_level> ::= "min_experience_level:" <experience_level>
<task_planning_story_points> ::= "planning_story_points:" <positive_integer>
<task_priority> ::= "priority:" <integer>
<task_spillover_probability_override> ::= "spillover_probability_override:" <probability>
<task_fixed_cost> ::= "fixed_cost:" <number>

# Estimates
<estimate> ::= "estimate:" <estimate_spec>
<estimate_spec> ::= <explicit_estimate>
                  | <tshirt_estimate>
                  | <story_point_estimate>

<explicit_estimate> ::= [<estimate_distribution>]
                        <low_key>
                        <expected_key>
                        <high_key>
                        [<unit>]

<estimate_distribution> ::= "distribution:" <distribution_type>

# Note: while the Pydantic model defines aliases (min/most_likely/max),
# current raw-parser pre-validation only accepts canonical keys below.
<low_key> ::= "low:" <non_negative_number>
<expected_key> ::= "expected:" <positive_number>
<high_key> ::= "high:" <non_negative_number>

<tshirt_estimate> ::= "t_shirt_size:" <tshirt_size>
<tshirt_size> ::= <bare_tshirt_size> | <qualified_tshirt_size>
<bare_tshirt_size> ::= <tshirt_size_token>
<qualified_tshirt_size> ::= <category_name> "." <tshirt_size_token>
<tshirt_size_token> ::= <alpha_token>

<story_point_estimate> ::= "story_points:" <story_point_value>
<story_point_value> ::= "1" | "2" | "3" | "5" | "8" | "13" | "21"

<unit> ::= "unit:" <time_unit>
<time_unit> ::= "hours" | "days" | "weeks"

# Uncertainty factors
<uncertainty_factors> ::= "uncertainty_factors:" <factor_map>
<factor_map> ::= { <factor> }
<factor> ::= <factor_name> ":" <factor_level>
<factor_name> ::= "team_experience"
                | "requirements_maturity"
                | "technical_complexity"
                | "team_distribution"
                | "integration_complexity"
<factor_level> ::= <identifier>

# Risks
<task_risks> ::= "risks:" <risk_list>
<project_risks_section> ::= "project_risks:" <risk_list>
<risk_list> ::= { <risk> }
<risk> ::= "-" <risk_properties>
<risk_properties> ::= <risk_id>
                     <risk_name>
                     <risk_probability>
                     <risk_impact>
                     [<risk_description>]
                     [<risk_cost_impact>]

<risk_id> ::= "id:" <string_like_token>
<risk_name> ::= "name:" <string>
<risk_probability> ::= "probability:" <probability>
<risk_description> ::= "description:" <string>
<risk_cost_impact> ::= "cost_impact:" <number>

<risk_impact> ::= "impact:" <impact_spec>
<impact_spec> ::= <fixed_impact>
                | <percentage_impact>
                | <absolute_impact>

# Raw numeric risk impact is interpreted as hours; may be 0 or negative
<fixed_impact> ::= <number>
<percentage_impact> ::= "type:" "percentage"
                        "value:" <positive_number>
<absolute_impact> ::= "type:" "absolute"
                      "value:" <positive_number>
                      ["unit:" <time_unit>]

# Resources and calendars
<resources_section> ::= "resources:" <resource_list>
<resource_list> ::= { <resource> }
<resource> ::= "-" <resource_properties>
<resource_properties> ::= [<resource_name>]
                         [<legacy_resource_id>]
                         [<resource_availability>]
                         [<resource_calendar>]
                         [<resource_experience_level>]
                         [<resource_productivity_level>]
                         [<resource_sickness_prob>]
                         [<resource_planned_absence>]
                         [<resource_hourly_rate>]

<resource_name> ::= "name:" <string_like_token>
<legacy_resource_id> ::= "id:" <string_like_token>
<resource_availability> ::= "availability:" <probability>
<resource_calendar> ::= "calendar:" <string_like_token>
<resource_experience_level> ::= "experience_level:" <experience_level>
<resource_productivity_level> ::= "productivity_level:" <positive_number>
<resource_sickness_prob> ::= "sickness_prob:" <probability>
<resource_planned_absence> ::= "planned_absence:" <date_list>
<resource_hourly_rate> ::= "hourly_rate:" <non_negative_number>

<calendars_section> ::= "calendars:" <calendar_list>
<calendar_list> ::= { <calendar> }
<calendar> ::= "-" <calendar_properties>
<calendar_properties> ::= [<calendar_id>]
                         [<calendar_work_hours_per_day>]
                         [<calendar_work_days>]
                         [<calendar_holidays>]

<calendar_id> ::= "id:" <string_like_token>
<calendar_work_hours_per_day> ::= "work_hours_per_day:" <positive_number>
<calendar_work_days> ::= "work_days:" <weekday_int_list>
<weekday_int_list> ::= "[]" | "[" <weekday_int_values> "]"
<weekday_int_values> ::= <weekday_int> { "," <weekday_int> }
<weekday_int> ::= "1" | "2" | "3" | "4" | "5" | "6" | "7"
<calendar_holidays> ::= "holidays:" <date_list>

# Sprint planning
<sprint_planning_section> ::= "sprint_planning:" <sprint_planning_properties>

<sprint_planning_properties> ::= ["enabled:" <boolean>]
                                 "sprint_length_weeks:" <positive_integer>
                                 "capacity_mode:" <capacity_mode>
                                 ["planning_confidence_level:" <probability_exclusive>]
                                 ["removed_work_treatment:" <removed_work_treatment>]
                                 ["velocity_model:" <velocity_model>]
                                 [<history_block>]
                                 [<future_sprint_overrides_block>]
                                 [<volatility_overlay_block>]
                                 [<spillover_block>]
                                 [<sickness_block>]

<capacity_mode> ::= "story_points" | "tasks"
<removed_work_treatment> ::= "churn_only" | "reduce_backlog"
<velocity_model> ::= "empirical" | "neg_binomial"

<history_block> ::= "history:" ( <history_inline_list> | <history_external_descriptor> )
<history_inline_list> ::= { "-" <history_entry> }+
<history_external_descriptor> ::= "format:" ("json" | "csv")
                                  "path:" <string>

<history_entry> ::= "sprint_id:" <string>
                    ["sprint_length_weeks:" <positive_integer>]
                    ["completed_story_points:" <non_negative_number>]
                    ["completed_tasks:" <non_negative_integer>]
                    ["spillover_story_points:" <non_negative_number>]
                    ["spillover_tasks:" <non_negative_integer>]
                    ["added_story_points:" <non_negative_number>]
                    ["added_tasks:" <non_negative_integer>]
                    ["removed_story_points:" <non_negative_number>]
                    ["removed_tasks:" <non_negative_integer>]
                    ["holiday_factor:" <positive_number>]
                    ["end_date:" <date_string>]
                    ["team_size:" <non_negative_integer>]
                    ["notes:" <string>]

<future_sprint_overrides_block> ::= "future_sprint_overrides:" { "-" <future_override> }
<future_override> ::= ["sprint_number:" <positive_integer>]
                      ["start_date:" <date_string>]
                      ["holiday_factor:" <positive_number>]
                      ["capacity_multiplier:" <positive_number>]
                      ["notes:" <string>]

<volatility_overlay_block> ::= "volatility_overlay:"
                               ["enabled:" <boolean>]
                               ["disruption_probability:" <probability>]
                               ["disruption_multiplier_low:" <positive_number>]
                               ["disruption_multiplier_expected:" <positive_number>]
                               ["disruption_multiplier_high:" <positive_number>]

<spillover_block> ::= "spillover:"
                      ["enabled:" <boolean>]
                      ["model:" ("table" | "logistic")]
                      ["size_reference_points:" <positive_number>]
                      ["size_brackets:" <size_brackets_list>]
                      ["consumed_fraction_alpha:" <positive_number>]
                      ["consumed_fraction_beta:" <positive_number>]
                      ["logistic_slope:" <positive_number>]
                      ["logistic_intercept:" <float>]

<size_brackets_list> ::= { "-" <size_bracket> }+
<size_bracket> ::= ["max_points:" <positive_number>]
                   "probability:" <probability>

<sickness_block> ::= "sickness:"
                     ["enabled:" <boolean>]
                     ["team_size:" <positive_integer>]
                     ["probability_per_person_per_week:" <probability_exclusive>]
                     ["duration_log_mu:" <float>]
                     ["duration_log_sigma:" <positive_number>]

# Shared primitives
<identifier> ::= <letter> { <letter> | <digit> | "_" }
<category_name> ::= <letter_or_underscore> { <letter_or_underscore> }
<alpha_token> ::= <letter> { <letter> | "_" | "-" | " " }
<identifier_list> ::= <identifier> { "," <identifier> }
<string_like_token> ::= <identifier> | <string>
<string_like_token_list> ::= <string_like_token> { "," <string_like_token> }
<date_list> ::= "[]" | "[" <date_string_list> "]"
<date_string_list> ::= <date_string> { "," <date_string> }
<date_string> ::= <year> "-" <month> "-" <day>

<positive_number> ::= <integer_gt_zero> | <float_gt_zero>
<number> ::= <signed_integer> | <float>
<non_negative_number> ::= <non_negative_integer> | <non_negative_float>
<probability> ::= <float_between_0_and_1_inclusive>
<probability_exclusive> ::= <float_between_0_and_1_exclusive>

<positive_integer> ::= <integer_gt_zero>
<non_negative_integer> ::= "0" | <integer_gt_zero>
<signed_integer> ::= ["-"] <integer>

<integer> ::= <digit> { <digit> }
<integer_gt_zero> ::= <non_zero_digit> { <digit> }
<float> ::= ["-"] <integer> "." <digit> { <digit> }
<float_gt_zero> ::= <integer_gt_zero> "." <digit> { <digit> }
                 | "0." <non_zero_digit> { <digit> }
<non_negative_float> ::= <integer> "." <digit> { <digit> }

<float_between_0_and_1_inclusive> ::= "0"
                                    | "1"
                                    | "0." <digit> { <digit> }
                                    | "1.0" { "0" }

<float_between_0_and_1_exclusive> ::= "0." <non_zero_digit> { <digit> }
                                    | "0." "0" { "0" } <non_zero_digit> { <digit> }

<boolean> ::= "true" | "false"
<string> ::= '"' { <character> } '"'
<digit> ::= "0" | <non_zero_digit>
<non_zero_digit> ::= "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
<letter> ::= "a".."z" | "A".."Z"
<letter_or_underscore> ::= <letter> | "_"
<year> ::= <digit> <digit> <digit> <digit>
<month> ::= "01" | "02" | "03" | "04" | "05" | "06" | "07" | "08" | "09" | "10" | "11" | "12"
<day> ::= "01".."31"
<character> ::= any printable character except '"'
<experience_level> ::= "1" | "2" | "3"

Config File Grammar

<config_file> ::= { <config_top_level_entry> }

<config_top_level_entry> ::= <uncertainty_factors_config>
                           | <t_shirt_sizes_config>
                           | <t_shirt_size_default_category_config>
                           | <t_shirt_size_unit_config>
                           | <story_points_config>
                           | <story_point_unit_config>
                           | <simulation_config>
                           | <lognormal_config>
                           | <output_config>
                           | <staffing_config>
                           | <constrained_scheduling_config>
                           | <sprint_defaults_config>
                           | <cost_config>

<uncertainty_factors_config> ::= "uncertainty_factors:" <map>
<t_shirt_sizes_config> ::= "t_shirt_sizes:" <map>
                         | "t_shirt_size_categories:" <map>
<t_shirt_size_default_category_config> ::= "t_shirt_size_default_category:" <string>
<t_shirt_size_unit_config> ::= "t_shirt_size_unit:" <time_unit>
<story_points_config> ::= "story_points:" <map>
<story_point_unit_config> ::= "story_point_unit:" <time_unit>

<simulation_config> ::= "simulation:"
                        ["default_iterations:" <positive_integer>]
                        ["random_seed:" (<signed_integer> | "null")]
                        ["max_stored_critical_paths:" <positive_integer>]

<lognormal_config> ::= "lognormal:"
                       ["high_percentile:" <integer>]

<output_config> ::= "output:"
                    ["formats:" <string_list>]
                    ["include_histogram:" <boolean>]
                    ["number_bins:" <positive_integer>]
                    ["critical_path_report_limit:" <positive_integer>]

<staffing_config> ::= "staffing:" <map>

<constrained_scheduling_config> ::= "constrained_scheduling:"
                                    ["sickness_prob:" <probability>]
                                    ["assignment_mode:" ("greedy_single_pass" | "criticality_two_pass")]
                                    ["pass1_iterations:" <positive_integer>]

<sprint_defaults_config> ::= "sprint_defaults:"
                             ["planning_confidence_level:" <probability_exclusive>]
                             ["removed_work_treatment:" ("churn_only" | "reduce_backlog")]
                             ["velocity_model:" ("empirical" | "neg_binomial")]
                             ["volatility_disruption_probability:" <probability>]
                             ["volatility_disruption_multiplier_low:" <non_negative_number>]
                             ["volatility_disruption_multiplier_expected:" <non_negative_number>]
                             ["volatility_disruption_multiplier_high:" <non_negative_number>]
                             ["spillover_model:" ("table" | "logistic")]
                             ["spillover_size_reference_points:" <positive_number>]
                             ["spillover_size_brackets:" <list>]
                             ["spillover_consumed_fraction_alpha:" <positive_number>]
                             ["spillover_consumed_fraction_beta:" <positive_number>]
                             ["spillover_logistic_slope:" <positive_number>]
                             ["spillover_logistic_intercept:" <float>]
                             [<sprint_sickness_defaults_config>]

<sprint_sickness_defaults_config> ::= "sickness:"
                                      ["enabled:" <boolean>]
                                      ["probability_per_person_per_week:" <probability_exclusive>]
                                      ["duration_log_mu:" <float>]
                                      ["duration_log_sigma:" <positive_number>]

<cost_config> ::= "cost:"
                  ["default_hourly_rate:" <non_negative_number>]
                  ["overhead_rate:" <non_negative_number>]
                  ["currency:" <string>]
                  ["include_in_output:" <boolean>]

# Generic placeholders for map/list structures accepted by YAML/TOML.
# Their semantic content is constrained by Pydantic models.
<map> ::= mapping object
<list> ::= list/array
<string_list> ::= list/array of strings

Semantic Constraints

Project-level

  1. start_date must be a valid ISO date in YYYY-MM-DD format.
  2. The raw parser applies a strict unknown-field allowlist for most project sections before model validation. (Notable exception: unknown keys under project.uncertainty_factors are currently ignored by model parsing.)
  3. probability_red_threshold must be strictly less than probability_green_threshold.
  4. Default confidence levels are [10, 25, 50, 75, 80, 85, 90, 95, 99].
  5. Default thresholds are:
  6. probability_red_threshold: 0.50
  7. probability_green_threshold: 0.90
  8. If team_size is omitted or 0, only explicitly listed resources are used.
  9. If team_size > explicit_resources, unnamed default resources are generated up to team_size.
  10. If team_size < explicit_resources, validation fails.
  11. t_shirt_size_default_category, when present, overrides the built-in program default but is itself overridden by a loaded config file or the --tshirt-category CLI flag.
  12. project.uncertainty_factors, when present, overrides built-in task-factor defaults.
  13. Config-file uncertainty_factors multipliers override project-file uncertainty factor assumptions.
  14. Cost-related project fields:
  15. default_hourly_rate must be >= 0
  16. overhead_rate must be in [0.0, 3.0]
  17. secondary_currencies may contain at most 5 entries
  18. fx_conversion_cost must be in [0.0, 0.50]
  19. fx_overhead_rate must be in [0.0, 1.0]
  20. fx_rates values must be positive

Tasks and estimates

  1. Task IDs must be unique.
  2. At least one task is required.
  3. Dependency IDs must reference existing tasks.
  4. Circular dependencies are not allowed.
  5. For explicit estimates:
  6. low <= expected <= high
  7. if effective distribution is lognormal, then low < expected < high
  8. unit is optional; when omitted it defaults to hours
  9. Explicit estimate keys currently accepted by the parser are canonical keys only:
  10. low
  11. expected
  12. high (The model defines aliases min / most_likely / max, but raw pre-validation currently rejects those keys as unknown.)
  13. Symbolic estimate rules:
  14. exactly one of t_shirt_size or story_points may be set
  15. unit must not be set when using symbolic estimates
  16. Explicit estimate numeric bounds:
  17. expected > 0
  18. low >= 0
  19. high >= 0
  20. t_shirt_size parser acceptance is syntax-based (<size> or <category>.<size> with alphabetic/underscore/hyphen/space tokens). Category/size validity against configured mappings is resolved later when the estimate is interpreted.
  21. max_resources >= 1.
  22. min_experience_level must be 1, 2, or 3.
  23. spillover_probability_override must be in [0.0, 1.0].
  24. fixed_cost, when present, is interpreted in project currency units and may be positive or negative.

Uncertainty factors

  1. Task-level uncertainty factor keys are constrained to:
  2. team_experience
  3. requirements_maturity
  4. technical_complexity
  5. team_distribution
  6. integration_complexity
  7. Factor levels are open-ended strings.
  8. At task resolution time, precedence is: built-in defaults < project-level uncertainty_factors < task-level uncertainty_factors.
  9. Missing factors or levels fall back to multiplier 1.0 at runtime.
  10. Project-level project.uncertainty_factors unknown keys are currently ignored (not hard-failed) by model parsing.

Resources and calendars

  1. Resource names must be unique after normalization.
  2. If a resource omits name but provides legacy id, id becomes name.
  3. If both name and id are omitted, generated names resource_001, resource_002, ... are assigned.
  4. Resource bounds:
  5. availability in (0.0, 1.0]
  6. experience_level in {1, 2, 3}
  7. productivity_level in [0.1, 2.0]
  8. sickness_prob in [0.0, 1.0]
  9. Calendar IDs must be unique.
  10. work_days values must be in 1..7.
  11. Resource calendar references must exist; when no calendars are defined, default is valid.
  12. Task resources entries must reference existing resources.
  13. If a task references named resources, each referenced resource must satisfy that task's min_experience_level.
  14. hourly_rate, when present, must be >= 0 and overrides project-level default_hourly_rate for that resource.

Sprint planning

  1. When sprint_planning.enabled is true, at least two usable historical rows with positive delivery signal are required.
  2. History rows must include exactly one of completed_story_points or completed_tasks.
  3. Unit-family fields in each row must match the completed field family.
  4. sprint_id values must be unique.
  5. In story_points capacity mode, every task must have resolvable planning story points.
  6. If spillover.enabled is true, every task must have resolvable planning story points.
  7. Future overrides:
  8. at least one locator (sprint_number or start_date) is required
  9. start_date must align to sprint boundaries
  10. if both locators are present, they must resolve to the same sprint
  11. two overrides may not resolve to the same sprint
  12. Volatility multipliers must satisfy low <= expected <= high.
  13. Sprint sickness constraints:
  14. probability_per_person_per_week in (0.0, 1.0)
  15. duration_log_sigma > 0
  16. External sprint history loading:
  17. history.format must be json or csv
  18. history.path must be non-empty and resolve to an existing file
  19. JSON input may be either an array of history rows or an object containing a sprints array
  20. CSV input must include a header row

Risks

  1. probability must be in [0.0, 1.0].
  2. Numeric impact is interpreted as hours; may be 0 (cost-only risk) or negative (time saving).
  3. Absolute structured impact supports type: absolute, value (must be > 0), and optional unit (hours, days, weeks).
  4. Percentage impact supports type: percentage and value (must be > 0).
  5. A global uniqueness constraint for risk IDs is not currently enforced by a dedicated validator.
  6. cost_impact, when present, may be positive (additional cost) or negative (cost saving / credit).
  7. cost_impact shares the same probability roll as the time impact.

Config

  1. constrained_scheduling.sickness_prob must be in [0.0, 1.0].
  2. constrained_scheduling.assignment_mode must be greedy_single_pass or criticality_two_pass.
  3. constrained_scheduling.pass1_iterations must be > 0.
  4. sprint_defaults.sickness.probability_per_person_per_week must be in (0.0, 1.0).
  5. sprint_defaults.sickness.duration_log_sigma must be > 0.
  6. sprint_defaults.sickness.duration_log_mu is any real number.
  7. cost.default_hourly_rate, when present, must be >= 0.
  8. cost.overhead_rate must be in [0.0, 3.0].
  9. cost.currency defaults to "EUR".
  10. cost.include_in_output defaults to true; when false, cost sections are suppressed in output but cost calculations still run.
  11. simulation.default_iterations is the iterations field in config (not simulation.iterations).
  12. simulation.max_stored_critical_paths belongs under simulation (not output).
  13. output.include_histogram is supported.
  14. lognormal.high_percentile must be one of {70, 75, 80, 85, 90, 95, 99}.
  15. Config loading currently uses YAML input.

Example Validation

Valid Core Project

project:
  name: "Valid Project"
  description: "Core simulation"
  start_date: "2025-11-01"
  distribution: "triangular"
  confidence_levels: [50, 80, 90, 95]
  probability_red_threshold: 0.50
  probability_green_threshold: 0.90

tasks:
  - id: "task_001"
    name: "Backend Development"
    estimate:
      low: 5
      expected: 8
      high: 15
    risks:
      - id: "tech_debt"
        name: "Technical debt discovered"
        probability: 0.30
        impact:
          type: "absolute"
          value: 2
          unit: "days"

  - id: "task_002"
    name: "Integration"
    estimate:
      distribution: "lognormal"
      low: 1
      expected: 3
      high: 8
      unit: "days"
    dependencies: ["task_001"]

Valid Sprint-Planning Project

project:
  name: "Sprint Forecast"
  start_date: "2026-05-04"

tasks:
  - id: "task_001"
    name: "Discovery"
    estimate:
      story_points: 3
    dependencies: []
  - id: "task_002"
    name: "Build"
    estimate:
      story_points: 5
    dependencies: ["task_001"]

sprint_planning:
  enabled: true
  sprint_length_weeks: 2
  capacity_mode: story_points
  history:
    - sprint_id: "SPR-001"
      completed_story_points: 10
      spillover_story_points: 1
    - sprint_id: "SPR-002"
      completed_story_points: 9
      spillover_story_points: 2

Valid Config Snippet

constrained_scheduling:
  sickness_prob: 0.05
  assignment_mode: criticality_two_pass
  pass1_iterations: 500

sprint_defaults:
  planning_confidence_level: 0.85
  velocity_model: neg_binomial
  sickness:
    enabled: true
    probability_per_person_per_week: 0.06
    duration_log_mu: 0.693
    duration_log_sigma: 0.75

Invalid Examples

# INVALID: min > expected
estimate:
  low: 10
  expected: 5
  high: 15

# INVALID: circular dependency
tasks:
  - id: "task_a"
    dependencies: ["task_b"]
  - id: "task_b"
    dependencies: ["task_a"]

# INVALID: unsupported story point
estimate:
  story_points: 4

# INVALID: symbolic estimate must not set unit
estimate:
  t_shirt_size: "M"
  unit: "days"

# INVALID: sprint planning needs at least two usable history rows
sprint_planning:
  enabled: true
  sprint_length_weeks: 2
  capacity_mode: story_points
  history:
    - sprint_id: "S1"
      completed_story_points: 0

Format Support

Project-file grammar is implemented for both YAML and TOML:

  • YAML (.yaml, .yml)
  • TOML (.toml)

Config-file grammar is currently implemented for YAML config files.

Parser Implementation

Core implementation components:

  • Pydantic v2 model validation
  • PyYAML parsing
  • tomllib (Python built-in, 3.11+) for TOML project-file reading

Parser Drift Checklist

Use this checklist when reviewing whether this document still matches current parser behavior.

1. Project parser entry points

  • Verify YAML/TOML project parsing flow:
  • src/mcprojsim/parsers/yaml_parser.py
  • src/mcprojsim/parsers/toml_parser.py
  • Confirm both paths still run:
  • external sprint-history loading (load_external_sprint_history)
  • raw payload validation (validate_project_payload)
  • model validation (Project(**data))

2. Raw unknown-field allowlists

  • Re-check allowlisted keys in:
  • src/mcprojsim/parsers/error_reporting.py (_allowed_fields_for_path)
  • If any key is added/removed there, update EBNF keys and semantic constraints here.

3. Project model constraints

  • Re-check model fields and validators in:
  • src/mcprojsim/models/project.py
  • Confirm this document still matches:
  • task estimate rules (TaskEstimate)
  • risk impact shape (Risk, RiskImpact)
  • resource/calendar rules (ResourceSpec, CalendarSpec)
  • sprint planning rules (SprintPlanningSpec and related specs)
  • project-level integrity checks (Project.validate_project)

4. Config schema and defaults

  • Re-check config schema in:
  • src/mcprojsim/config.py
  • Confirm field locations and names still match this doc, especially:
  • simulation.*
  • output.*
  • constrained_scheduling.*
  • sprint_defaults.*
  • cost.*
  • lognormal.high_percentile allowed values

5. External sprint history behavior

  • Re-check external history descriptors and normalization in:
  • src/mcprojsim/parsers/sprint_history_parser.py
  • Confirm supported formats, required fields, and normalization assumptions documented here remain accurate.

6. Alias and pre-validation edge cases

  • Explicitly test estimate alias behavior against current raw pre-validation (for example min/most_likely/max vs low/expected/high).
  • If parser behavior changes, update both:
  • EBNF estimate productions
  • semantic constraints text

7. CLI-level validation sanity checks

Run quick checks against real CLI validation behavior:

# Should pass
poetry run mcprojsim validate examples/quickstart_project.yaml

# Should fail on unknown estimate alias keys (unless parser behavior changed)
cat >/tmp/grammar_alias_check.yaml <<'YAML'
project:
  name: "Alias check"
  start_date: "2026-01-01"
tasks:
  - id: "t1"
    name: "Task"
    estimate:
      min: 1
      most_likely: 2
      max: 3
YAML
poetry run mcprojsim validate /tmp/grammar_alias_check.yaml

8. Documentation build sanity check

After grammar updates, run:

poetry run mkdocs build

If build passes, this file is structurally valid in the docs site.