cometric 0.3.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. cometric-0.4.0/.hypothesis/constants/1452a68a7516a238 +4 -0
  2. cometric-0.4.0/.hypothesis/constants/5c08095823382694 +4 -0
  3. cometric-0.4.0/.hypothesis/constants/5e3c558a497b7b3b +4 -0
  4. cometric-0.4.0/.hypothesis/constants/6eef135e443dc63b +4 -0
  5. cometric-0.4.0/.hypothesis/constants/f19d2aecb94f07ec +4 -0
  6. {cometric-0.3.0 → cometric-0.4.0}/CHANGELOG.md +8 -0
  7. {cometric-0.3.0 → cometric-0.4.0}/PKG-INFO +1 -1
  8. {cometric-0.3.0 → cometric-0.4.0}/docs/COMETRIC_SPEC_v0.3.md +7 -3
  9. {cometric-0.3.0 → cometric-0.4.0}/docs/PATTERNS.md +11 -0
  10. {cometric-0.3.0 → cometric-0.4.0}/docs/STATUS.md +1 -1
  11. {cometric-0.3.0 → cometric-0.4.0}/pyproject.toml +1 -1
  12. {cometric-0.3.0 → cometric-0.4.0}/scripts/build_alsp.py +119 -0
  13. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/core/api.py +60 -16
  14. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/core/enums.py +12 -0
  15. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/core/models.py +29 -0
  16. cometric-0.4.0/src/cometric/core/quantities.py +324 -0
  17. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/core/quickstart.py +1 -0
  18. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/core/schema.py +2 -1
  19. cometric-0.4.0/src/cometric/examples/pattern_counting_rules.json +513 -0
  20. cometric-0.4.0/src/cometric/packs/alsp/concepts/counting_rules.yaml +51 -0
  21. cometric-0.4.0/tests/test_counting.py +149 -0
  22. {cometric-0.3.0 → cometric-0.4.0}/uv.lock +1 -1
  23. cometric-0.3.0/.hypothesis/constants/02af8a95fc854af3 +0 -4
  24. cometric-0.3.0/.hypothesis/constants/2616f13a910320c7 +0 -4
  25. cometric-0.3.0/.hypothesis/constants/8291cdcd8414a40b +0 -4
  26. cometric-0.3.0/.hypothesis/constants/8408e47180d8b9c1 +0 -4
  27. cometric-0.3.0/.hypothesis/constants/a402bbad3ba0ac69 +0 -4
  28. cometric-0.3.0/src/cometric/core/quantities.py +0 -147
  29. {cometric-0.3.0 → cometric-0.4.0}/.github/workflows/ci.yml +0 -0
  30. {cometric-0.3.0 → cometric-0.4.0}/.github/workflows/publish.yml +0 -0
  31. {cometric-0.3.0 → cometric-0.4.0}/.gitignore +0 -0
  32. {cometric-0.3.0 → cometric-0.4.0}/.hypothesis/.gitignore +0 -0
  33. {cometric-0.3.0 → cometric-0.4.0}/.hypothesis/constants/052e28aa39cf014b +0 -0
  34. {cometric-0.3.0 → cometric-0.4.0}/.hypothesis/constants/0a08468b27ab0e2b +0 -0
  35. {cometric-0.3.0 → cometric-0.4.0}/.hypothesis/constants/1a54c0e5b66b34fa +0 -0
  36. {cometric-0.3.0 → cometric-0.4.0}/.hypothesis/constants/284cdb085a69e559 +0 -0
  37. {cometric-0.3.0 → cometric-0.4.0}/.hypothesis/constants/31846ac0f55db7cc +0 -0
  38. {cometric-0.3.0 → cometric-0.4.0}/.hypothesis/constants/3570e270a2e92983 +0 -0
  39. {cometric-0.3.0 → cometric-0.4.0}/.hypothesis/constants/3faf924fe60f71dc +0 -0
  40. {cometric-0.3.0 → cometric-0.4.0}/.hypothesis/constants/52563cc61b61afeb +0 -0
  41. {cometric-0.3.0 → cometric-0.4.0}/.hypothesis/constants/64a2c31fcab5443b +0 -0
  42. {cometric-0.3.0 → cometric-0.4.0}/.hypothesis/constants/6ab7a80f708d05c3 +0 -0
  43. {cometric-0.3.0 → cometric-0.4.0}/.hypothesis/constants/85a66397c5e4b867 +0 -0
  44. {cometric-0.3.0 → cometric-0.4.0}/.hypothesis/constants/b428358d86c25f38 +0 -0
  45. {cometric-0.3.0 → cometric-0.4.0}/.hypothesis/constants/bddcf7e0e0af1f28 +0 -0
  46. {cometric-0.3.0 → cometric-0.4.0}/.hypothesis/constants/da39a3ee5e6b4b0d +0 -0
  47. {cometric-0.3.0 → cometric-0.4.0}/.hypothesis/constants/e275ad1f9cb17aaf +0 -0
  48. {cometric-0.3.0 → cometric-0.4.0}/.hypothesis/constants/ed391256aab33efd +0 -0
  49. /cometric-0.3.0/.hypothesis/constants/52445b4e87bec631 → /cometric-0.4.0/.hypothesis/constants/fd18071893a59813 +0 -0
  50. {cometric-0.3.0 → cometric-0.4.0}/LICENSE +0 -0
  51. {cometric-0.3.0 → cometric-0.4.0}/README.md +0 -0
  52. {cometric-0.3.0 → cometric-0.4.0}/docs/ALSP_COMMERCIAL_ONTOLOGY_SPEC_v0.1.pdf +0 -0
  53. {cometric-0.3.0 → cometric-0.4.0}/docs/COMETRIC_SPEC_v0.2.md +0 -0
  54. {cometric-0.3.0 → cometric-0.4.0}/docs/RECONCILIATION.md +0 -0
  55. {cometric-0.3.0 → cometric-0.4.0}/scripts/build_consulting.py +0 -0
  56. {cometric-0.3.0 → cometric-0.4.0}/scripts/build_fixtures.py +0 -0
  57. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/__init__.py +0 -0
  58. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/cli.py +0 -0
  59. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/core/__init__.py +0 -0
  60. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/core/engine.py +0 -0
  61. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/core/fingerprint.py +0 -0
  62. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/core/issues.py +0 -0
  63. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/core/ledger.py +0 -0
  64. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/core/money.py +0 -0
  65. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/core/packs.py +0 -0
  66. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/core/periods.py +0 -0
  67. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/core/predicates.py +0 -0
  68. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/core/present.py +0 -0
  69. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/core/templates.py +0 -0
  70. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/core/tiers.py +0 -0
  71. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/core/validation.py +0 -0
  72. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/examples/case_a.json +0 -0
  73. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/examples/case_b.json +0 -0
  74. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/examples/case_c.json +0 -0
  75. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/examples/case_d.json +0 -0
  76. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/examples/case_e.json +0 -0
  77. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/examples/case_f.json +0 -0
  78. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/examples/case_g.json +0 -0
  79. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/examples/case_h.json +0 -0
  80. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/examples/pattern_partial_and_hours.json +0 -0
  81. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/examples/pattern_termination.json +0 -0
  82. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/examples/pattern_tiered_minimum.json +0 -0
  83. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/examples/pattern_transition_credit.json +0 -0
  84. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/examples/spec_example.json +0 -0
  85. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/mcp/__init__.py +0 -0
  86. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/mcp/__main__.py +0 -0
  87. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/mcp/server.py +0 -0
  88. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/alsp/concepts/provider_types.yaml +0 -0
  89. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/alsp/concepts/service_categories.yaml +0 -0
  90. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/alsp/concepts/unit_templates.yaml +0 -0
  91. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/alsp/coverage.yaml +0 -0
  92. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/alsp/evidence/claims.yaml +0 -0
  93. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/alsp/evidence/sources.yaml +0 -0
  94. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/alsp/overlays/levers.yaml +0 -0
  95. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/alsp/overlays/mechanisms.yaml +0 -0
  96. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/alsp/overlays/metrics.yaml +0 -0
  97. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/alsp/overlays/pricing_models.yaml +0 -0
  98. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/alsp/pack.yaml +0 -0
  99. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/alsp/rules/rules.yaml +0 -0
  100. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/alsp/templates/tiered_units_annual_minimum.json +0 -0
  101. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/base/concepts/automation_structures.yaml +0 -0
  102. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/base/concepts/billing_bases.yaml +0 -0
  103. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/base/concepts/cost_categories.yaml +0 -0
  104. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/base/concepts/dimensions.yaml +0 -0
  105. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/base/concepts/fee_components.yaml +0 -0
  106. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/base/concepts/layers.yaml +0 -0
  107. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/base/concepts/levers.yaml +0 -0
  108. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/base/concepts/mechanisms.yaml +0 -0
  109. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/base/concepts/metrics.yaml +0 -0
  110. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/base/concepts/negotiation_exchanges.yaml +0 -0
  111. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/base/concepts/offers.yaml +0 -0
  112. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/base/concepts/pricing_models.yaml +0 -0
  113. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/base/concepts/schema_map.yaml +0 -0
  114. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/base/concepts/selection_framework.yaml +0 -0
  115. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/base/concepts/selection_heuristics.yaml +0 -0
  116. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/base/coverage.yaml +0 -0
  117. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/base/evidence/claims.yaml +0 -0
  118. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/base/evidence/sources.yaml +0 -0
  119. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/base/excluded.yaml +0 -0
  120. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/base/pack.yaml +0 -0
  121. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/base/rules/rules.yaml +0 -0
  122. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/consulting/concepts/consulting_concepts.yaml +0 -0
  123. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/consulting/concepts/provider_types.yaml +0 -0
  124. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/consulting/concepts/service_categories.yaml +0 -0
  125. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/consulting/concepts/unit_templates.yaml +0 -0
  126. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/consulting/coverage.yaml +0 -0
  127. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/consulting/overlays/pricing_models.yaml +0 -0
  128. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/consulting/pack.yaml +0 -0
  129. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/consulting/rules/rules.yaml +0 -0
  130. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/consulting/templates/capped_tm.json +0 -0
  131. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/consulting/templates/fixed_fee_plus_success.json +0 -0
  132. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/consulting/templates/phased_fixed_fee.json +0 -0
  133. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/consulting/templates/retainer_hours_drawdown.json +0 -0
  134. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/packs/consulting/templates/role_based_tm.json +0 -0
  135. {cometric-0.3.0 → cometric-0.4.0}/src/cometric/py.typed +0 -0
  136. {cometric-0.3.0 → cometric-0.4.0}/tests/builders.py +0 -0
  137. {cometric-0.3.0 → cometric-0.4.0}/tests/conftest.py +0 -0
  138. {cometric-0.3.0 → cometric-0.4.0}/tests/test_casebook.py +0 -0
  139. {cometric-0.3.0 → cometric-0.4.0}/tests/test_engine_review_fixes.py +0 -0
  140. {cometric-0.3.0 → cometric-0.4.0}/tests/test_llm_feedback.py +0 -0
  141. {cometric-0.3.0 → cometric-0.4.0}/tests/test_mcp.py +0 -0
  142. {cometric-0.3.0 → cometric-0.4.0}/tests/test_outcome_and_realization.py +0 -0
  143. {cometric-0.3.0 → cometric-0.4.0}/tests/test_packs_and_templates.py +0 -0
  144. {cometric-0.3.0 → cometric-0.4.0}/tests/test_patterns.py +0 -0
  145. {cometric-0.3.0 → cometric-0.4.0}/tests/test_pricing.py +0 -0
  146. {cometric-0.3.0 → cometric-0.4.0}/tests/test_properties.py +0 -0
  147. {cometric-0.3.0 → cometric-0.4.0}/tests/test_reconcile.py +0 -0
  148. {cometric-0.3.0 → cometric-0.4.0}/tests/test_reproducibility_and_search.py +0 -0
  149. {cometric-0.3.0 → cometric-0.4.0}/tests/test_search_and_context.py +0 -0
  150. {cometric-0.3.0 → cometric-0.4.0}/tests/test_termination.py +0 -0
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/cometric/cometric/src/cometric/core/api.py
2
+ # hypothesis_version: 6.170.14
3
+
4
+ [10000, '...', '/', '/...', '/actuals', '/actuals/term', '/assumption', '/candidates', '/constraints', '/cost_policy', '/detail', '/driver', '/name', '/objective', '/objective/sense', '/parameters', '/pointer', '/pointers', '/precision', '/results', '/risk_premium', '/scenarios', '/search_spec', '/target_margin', '/tolerance_absolute', '/tolerance_relative', '/transform', '/values', '0', '0.01', '0.15', '0.2', '100', '100000', '140', '20', '90000', '; ', '?', 'BAD_CONSTRAINT', 'BAD_POINTER', 'BASE', 'BEST_AT_GRID_EDGE', 'COSTS_NOT_EVALUATED', 'COST_POLICY_REQUIRED', 'DUPLICATE_ID', 'ERROR', 'INTERNAL_POLICY', 'MEAN', 'NO_RESULTS', 'PARAMETER_SPEC', 'PENDING_HUMAN_REVIEW', 'RANGE', 'RATE', 'RISK_MISMATCH', 'RISK_PREMIUM_RANGE', 'SCENARIO_INFEASIBLE', 'SCHEMA', 'TARGET_MARGIN_RANGE', 'TERM', 'UNKNOWN_DRIVER', 'UNKNOWN_METRIC', 'UNKNOWN_QUANTITY', 'WORST', '_doc', 'absolute', 'accumulation_period', 'actual', 'actuals', 'add', 'aggregation', 'allowances', 'approval_state', 'assumption', 'assumptions', 'base', 'base_client_price', 'base_contribution', 'base_fee', 'base_scenario', 'base_scenario_id', 'baseline', 'best', 'best_under_objective', 'binding_constraints', 'calculate_periods', 'calculated', 'calculations', 'candidates', 'capacity', 'client_downside', 'client_price', 'closest_value_slack', 'code', 'cometric_higher', 'cometric_lower', 'commitment_shortfall', 'compared', 'component', 'computed', 'constraints', 'contract_facts', 'contractual_fee', 'contribution_margin', 'contribution_profit', 'cost_per_unit', 'cost_policy', 'costs', 'costs_by_category', 'costs_evaluated', 'costs_in_floor', 'counts', 'currency', 'current', 'current_fee', 'declared_bearer', 'decreasing', 'definitions', 'derived_bearer', 'derived_quantities', 'description', 'detail', 'deviations', 'difference', 'direct_delivery_cost', 'direction', 'discrepancies', 'display', 'distribution', 'dominated_by', 'driver', 'dry_run', 'edge', 'engagement', 'engagement_id', 'engine_version', 'entitlement', 'enumerated', 'errors', 'estimated_seconds', 'evaluated', 'exact', 'explanation', 'failing_constraints', 'feasible', 'fee_change', 'fee_line_items', 'fee_total', 'fees', 'fingerprints', 'flat', 'floors', 'formula', 'from', 'frontier', 'frontier_metrics', 'frontier_truncated', 'full', 'full_cost_profit', 'grid_edges', 'grid_size', 'gross_margin', 'gross_profit', 'gt', 'gte', 'headroom', 'horizon', 'id', 'include_baseline', 'increasing', 'infeasible', 'input_fingerprint', 'invalid', 'invalid_reasons', 'issues', 'kind', 'labels', 'leaf_true', 'ledger', 'line_items', 'lower', 'lt', 'lte', 'max', 'max_candidates', 'max_returned', 'measured', 'mechanisms_priced', 'message', 'metric', 'metrics', 'min', 'minimums', 'money_policy', 'month', 'must not be negative', 'name', 'non_dominated', 'not', 'note', 'nothing_compared', 'objective', 'objective_values', 'ok', 'on_frontier', 'op', 'operation', 'origin', 'outcome_fee', 'overage_fee', 'overage_recognition', 'packs', 'parameter', 'parameters', 'path', 'percent', 'period', 'periods', 'pointer', 'pointers', 'precision', 'price_floor', 'profile', 'provider_downside', 'publishable_summary', 'quantity_spreading', 'range', 'ranking', 'rate', 'rate_assumptions', 'reason', 'reconcile', 'reconciled', 'record_type', 'reference', 'rejected', 'relative', 'relative_difference', 'results', 'risk', 'risk_exposure', 'risk_premium', 'rollover', 'rounding', 'rows', 'rules_fired', 'scale', 'scenario', 'scenario_id', 'scenarios', 'schema_version', 'scope', 'sense', 'set', 'severity', 'shortfall_rule', 'standard', 'standard_value', 'status', 'step', 'summary', 'target_margin', 'term', 'termination', 'tested_against', 'threshold', 'thresholds', 'tie_breakers', 'time_period', 'to', 'tolerance', 'transform', 'type', 'unit', 'unit_contribution', 'unit_floor', 'upper', 'valid', 'value', 'values', 'values_per_parameter', 'volume', 'volume_quantity', 'vs_baseline', 'warnings', 'whole', 'within_limit', 'within_tolerance', 'worked_hours', 'years', '~', '~0', '~1']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/cometric/cometric/src/cometric/core/quantities.py
2
+ # hypothesis_version: 6.170.14
3
+
4
+ [' + ', ' - ', ' / ', ' x ', '(', ')', '..', '/const', '/expression', '/name', '/profile', '/q', '/rounding', '/time_period', '/unit', '/value', 'ASSUMPTION_OVERLAP', 'BAD_EXPRESSION', 'DEPENDENCY_CYCLE', 'DIVISION_BY_ZERO', 'DUPLICATE_ID', 'NEGATIVE_QUANTITY', 'NOT_ALLOCATABLE', 'PROFILE_LENGTH', 'ROUNDING_REQUIRED', 'TERM', 'UNKNOWN_QUANTITY', 'UNKNOWN_UNIT', '^\\d{4}$', 'assumptions', 'const', 'count', 'currency', 'derived', 'difference', 'expression', 'flag', 'fte', 'hours', 'product', 'q', 'quotient', 'ratio', 'scenario', 'scenario.', 'sum']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/cometric/cometric/src/cometric/core/schema.py
2
+ # hypothesis_version: 6.170.14
3
+
4
+ [10000, '$schema', ', ', '.', '0', '0.15', '110', 'A bare engagement', 'Actuals (reconcile)', 'BASE', 'Default display', 'Default summary', 'MEAN', 'SearchSpec', 'WORST', 'actuals', 'add', 'adjustment', 'aggregation', 'all', 'arrangement_version', 'array', 'assumption', 'base_scenario', 'boolean', 'by_reference', 'candidate', 'commitment', 'commitment_shortfall', 'constraint', 'constraint_paths', 'constraints', 'contractual_fee', 'contribution_margin', 'contribution_profit', 'cost_component', 'cost_policy', 'delivery_plan', 'derived_quantities', 'description', 'detail', 'direct_delivery_cost', 'display', 'dry_run', 'engagement', 'entitlement', 'enum', 'error_paths', 'exact', 'examples', 'fee_component', 'from', 'frontier_metrics', 'full', 'gross_margin', 'gross_profit', 'gt', 'id', 'include_baseline', 'integer', 'items', 'lt', 'lte', 'max', 'max_candidates', 'max_returned', 'maximum', 'metric', 'min', 'name', 'not', 'object', 'objective', 'oneOf', 'outcome_fee', 'overage_fee', 'parameters', 'path', 'paths', 'periods', 'pointer', 'pointers', 'precision', 'properties', 'quantity_source', 'range', 'rate_schedule', 'required', 'rule_paths', 'scale', 'scenario', 'scenarios', 'schema_version', 'search_parameters', 'search_spec', 'sense', 'set', 'standard', 'step', 'string', 'summary', 'term', 'tie_breakers', 'title', 'to', 'tolerance_policy', 'transform', 'type', 'values', '{metric: "amount"}']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/cometric/cometric/src/cometric/core/enums.py
2
+ # hypothesis_version: 6.170.14
3
+
4
+ ['AI_INFERENCE', 'ALLOWANCE_OVERAGE', 'ALL_ELIGIBLE', 'ALL_UNITS', 'AMORTIZED', 'ANNUAL', 'APPROVAL_REQUIRED', 'AVOIDABLE', 'A_DIRECT', 'BACKLOG', 'BONUS', 'B_CORROBORATED', 'CALENDAR_DAYS', 'CAP', 'CAPACITY', 'CLIENT', 'COLLAR', 'COMPONENT', 'CONTRACTED', 'CONTRACT_YEAR', 'CORPORATE_OVERHEAD', 'COST', 'COST_PLUS', 'COUNT', 'CUMULATIVE', 'C_INFERENCE', 'DEDICATED', 'DEDICATED_LABOR', 'DEDICATED_RESOURCE', 'DELIVERY_MANAGEMENT', 'DIRECT_DELIVERY', 'DISCOUNT', 'DOWN', 'D_PROPOSED', 'EMERGING', 'ENGAGEMENT', 'ENGAGEMENT_AVOIDABLE', 'ENGAGEMENT_OVERHEAD', 'ENTIRE_SUBTOTAL', 'ERROR', 'ESTABLISHED_COMMON', 'ESTIMATED', 'EVENT', 'EXCESS_ONLY', 'FIXED', 'FIXED_PROJECT', 'FIXED_RECURRING', 'FLAT', 'FTE_MONTHS', 'FULL_MONTH_FROM_NEXT', 'GRADUATED_BLOCK', 'GROSS', 'HALF_EVEN', 'HALF_UP', 'HIGHER_IS_BETTER', 'HOLDBACK', 'HOURS', 'HYBRID', 'IDLE_POOLED_CAPACITY', 'ILLUSTRATIVE', 'INCURRED', 'INDEXATION', 'INFEASIBLE', 'INFO', 'INTERNAL_POLICY', 'JOINT', 'LINE_ITEM', 'LOWER_IS_BETTER', 'MARGINAL', 'METER', 'MINIMUM_SPEND', 'MINIMUM_VOLUME', 'MONTH', 'MONTHLY', 'NET_OF_CREDITS', 'NET_OF_DISCOUNTS', 'NEXT_PERIOD', 'NONE', 'NONE_ADVERSE', 'OBSERVED', 'ONE_TIME', 'OUTCOME', 'OUTCOME_ADJUSTMENT', 'OUTPUT', 'OVERFLOW_COST', 'PERCENT_OF_OUTCOME', 'PERIOD_END', 'PERIOD_END_TRUE_UP', 'PERIOD_TOTAL', 'PER_SEGMENT', 'PLATFORM_OPERATIONS', 'POOLED', 'POOLED_LABOR', 'PORTFOLIO_SHARED', 'PREPAID_CREDIT', 'PROFILE', 'PRORATE', 'PROVIDER', 'QA_REWORK', 'QUARTER', 'QUARTERLY', 'RATE', 'REBATE', 'REMAINDER_DUE', 'RESERVED_CAPACITY', 'RESOURCE', 'REUSABLE_ENGINEERING', 'SERVICE', 'SHARE', 'SHARED', 'SLA_CREDIT', 'STEP', 'SUBCONTRACTOR', 'SUCCESS', 'TERM', 'THIRD_PARTY', 'TIERED_VARIABLE', 'TIME', 'TOTAL', 'TRANSITION', 'UNAVOIDABLE', 'UNIT_OUTPUT', 'UNKNOWN', 'UNVERIFIED', 'UP', 'USAGE', 'USAGE_TRIGGERED', 'VARIABLE', 'WAIVED', 'WARNING', 'WEIGHTED', 'WEIGHTED_UNIT_OUTPUT', 'YEAR']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/cometric/cometric/src/cometric/core/quickstart.py
2
+ # hypothesis_version: 6.170.14
3
+
4
+ ['0.10', '0.20', '100000', '110', '12000', '1200000', '130', '140.00', '15000', '2027-01', '40', '40000', '9000', '90000', 'DEDICATED_LABOR', 'ESTIMATED', 'EX-ALT', 'FIXED', 'MEAN', 'MONTHLY', 'POOLED_LABOR', 'TERM', 'VARIABLE', 'WORST', 'accepted_units', 'actuals', 'aggregation', 'amount', 'arguments', 'arrangement_versions', 'assumptions', 'base', 'base_fee', 'behavior', 'break_even', 'by_reference', 'calculate_periods', 'candidates', 'category', 'compare_arrangements', 'components', 'constraints', 'contractual_fee', 'contribution_profit', 'conventions', 'cost_components', 'cost_policy', 'counting_rules', 'current', 'decimals', 'delivery_plan', 'derive_risk_exposure', 'detail', 'detail_and_precision', 'driver', 'engagement', 'engagement_id', 'errors', 'examples', 'explain_fee', 'fixed_amount', 'get_template', 'high', 'how', 'id', 'lookup_concept', 'low', 'lower_base', 'lt', 'max', 'metric', 'name', 'next', 'no_arithmetic', 'not', 'notes', 'objective', 'origin', 'overage_rate', 'parameters', 'path', 'patterns', 'per_review', 'period', 'periods', 'pointer', 'price_floor', 'quantities', 'rate', 'rate_schedules', 'reconcile', 'reference', 'references', 'results', 'scenario', 'scenarios', 'search_dry_run', 'search_spec', 'search_structures', 'sense', 'spec_example.json', 'summary', 'target_margin', 'team', 'term', 'termination', 'tie_breakers', 'tiers', 'time_period', 'tool', 'tools_with_examples', 'unit', 'validate_engagement', 'value', 'values']
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0 (2026-11-05)
4
+
5
+ Additive; no breaking changes. `ENGINE_VERSION` stays 0.3.0 because no existing calculation changed. Fingerprints of existing scenarios still change, because the scenario schema gained fields with default values (`derived`, and `basis` on assumptions) that are part of the fingerprint.
6
+
7
+ - Counting rules: assumptions with `basis: RATE` apply every month unchanged; scenarios can declare `derived` quantities (sum, difference, product, quotient over other quantities and constants), evaluated each month in dependency order, with explicit rounding for COUNT units. Results list derived formulas under `definitions`.
8
+ - Search parameters can set a named scenario assumption in every scenario (`"assumption": "reissue_rate"`).
9
+ - ALSP pack: counting-rule concepts (CNT-001 to CNT-003) and `pattern_counting_rules`.
10
+
3
11
  ## 0.3.0 (2026-11-05)
4
12
 
5
13
  ### Breaking changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cometric
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Commercial Intelligence Engine: model, simulate, and optimize the economics of service engagements.
5
5
  Project-URL: Homepage, https://github.com/mfbaig35r/cometric
6
6
  Project-URL: Repository, https://github.com/mfbaig35r/cometric
@@ -1,6 +1,6 @@
1
1
  # Cometric | Commercial Intelligence Engine
2
2
 
3
- **Engineering Requirements v0.3 (as built for 0.3.0)**
3
+ **Engineering Requirements v0.3 (as built for 0.4.0)**
4
4
 
5
5
  Cometric does three things:
6
6
 
@@ -13,7 +13,7 @@ It is built for any service relationship priced through a composite arrangement:
13
13
  | | |
14
14
  |---|---|
15
15
  | Project | Cometric (`cometric` on PyPI; CLI and MCP server `cometric`) |
16
- | Status | Normative for `cometric` 0.3.0 (0.1.0 released 2026-11-04; 0.2.0 and 0.3.0 on 2026-11-05). Open-source project. Where this document and the code disagree, the code's tests are the tiebreaker and the disagreement is a defect in one of them. |
16
+ | Status | Normative for `cometric` 0.4.0 (0.1.0 released 2026-11-04; 0.2.0, 0.3.0, and 0.4.0 on 2026-11-05). Open-source project. Where this document and the code disagree, the code's tests are the tiebreaker and the disagreement is a defect in one of them. |
17
17
  | License | Apache-2.0 |
18
18
  | Date | November 4, 2026 |
19
19
  | Supersedes | `COMETRIC_SPEC_v0.2.md` (kept for history, with the v0.1 to v0.2 change log) |
@@ -57,6 +57,7 @@ v0.3 records what was built. Most changes resolve questions v0.2 left implicit;
57
57
  | V3-22 | App. D | Evidence verification status replaces the v0.2 priority list. |
58
58
  | V3-23 | 4.2, 9 | ALL_UNITS cliff bands compare every earlier tier with each threshold, with an inclusive lower bound where a band starts at a tier edge (0.2.0). |
59
59
  | V3-24 | 8.3, 9, 10 | `reconcile` operation for M6, with a publishable summary (0.2.0). |
60
+ | V3-30 | 6.1, 6.4, 9 | Counting rules through RATE assumptions and derived quantities; search parameters can set a named assumption (0.4.0). |
60
61
  | V3-28 | 4.4, 6.1, 9 | Termination scenarios: `termination_date` on a scenario, `termination_rule` on minimums, `termination_fee` on a version (0.3.0). |
61
62
  | V3-29 | 3.4, 6.3, App. C | ALSP template `tiered_units_annual_minimum` and five worked patterns (`docs/PATTERNS.md`); "pending human review" defined (0.3.0). |
62
63
  | V3-27 | 6.2, 6.4, 8.3, 11 | Search returns best results first with full figures, tie-breakers, constraint results, grid-edge warnings, and baseline differences; dry run; linked and scaled parameters; detail and precision levels; the server stays stateless (0.3.0). |
@@ -660,6 +661,8 @@ A scenario is an immutable snapshot of arrangement version, cost policy, demand
660
661
 
661
662
  Assumptions stated over a longer period than a month MUST declare a `demand_distribution`: `FLAT` (exact allocation per §4.7) or an explicit monthly profile.
662
663
 
664
+ **Rates and derived quantities** `[V3-30]`. An assumption with `basis: RATE` applies its value to every month of its time period unchanged (rates, shares, ratios); the default `TOTAL` spreads a total. A scenario's `derived` list computes quantities each month from assumptions and other derived quantities with a small expression language: `{"q": name}`, `{"const": "1"}`, `{"sum": [...]}`, `{"difference": [a, b]}`, `{"product": [...]}`, `{"quotient": [a, b]}`. Derived quantities are evaluated in dependency order (cycles are errors), may be used anywhere a `quantity_source` or cost driver is, and must declare `rounding` (`NONE`, `HALF_UP`, `DOWN`, `UP`) when their unit is a COUNT unit. Counting rules such as "re-issued documents are billed once" or "a bundle is billed once at its highest tier" are written this way, so the rule's assumptions stay visible, labelled, and searchable. Results list rate assumptions and each derived quantity's formula under `definitions`.
665
+
663
666
  v1 SHALL support low/base/high scenarios and deterministic one-way sensitivity analysis. Probability distributions and Monte Carlo are deferred until calibrated data is available.
664
667
 
665
668
  ### 6.2 Candidate comparison
@@ -715,7 +718,7 @@ In Cometric, "optimize" means a deterministic search over a space of candidate s
715
718
 
716
719
  **Output** `[V3-27]`: the best results come first. With an objective, `best` lists every candidate tied at the top after applying `objective.tie_breakers` in order, each with its parameter values, objective values, key metrics per scenario, difference from the baseline per scenario, the result of each constraint leaf, provider and client downside per deviation, `grid_edges` (parameters whose winning value is the lowest or highest in the grid), and calculation fingerprints. A `BEST_AT_GRID_EDGE` warning says when the grid should be widened before calling a result optimal. The baseline (default on) is the search_spec engagement as given. Then come counts (enumerated, invalid, infeasible, failing constraints, on frontier), invalid reasons, and the unranked frontier. If the frontier exceeds `max_returned` (default 20), the result says so. It never applies an undeclared ranking.
717
720
 
718
- **Parameters and sizing** `[V3-27]`: a parameter may write one `pointer` or several `pointers`, with `transform` `set` (write the value), `scale` (base × (1 + value)), or `add` (base + value), so "the same uplift on every tier" is one parameter instead of a combinatorial grid. `dry_run: true` returns grid size, calculation count, and an estimated time without running the search, so the user can confirm first.
721
+ **Parameters and sizing** `[V3-27]`: a parameter may set a named scenario assumption in every scenario (`assumption`, `[V3-30]`), or write one `pointer` or several `pointers`, with `transform` `set` (write the value), `scale` (base × (1 + value)), or `add` (base + value), so "the same uplift on every tier" is one parameter instead of a combinatorial grid. `dry_run: true` returns grid size, calculation count, and an estimated time without running the search, so the user can confirm first.
719
722
 
720
723
  **Detail and precision** `[V3-27]`: `detail` is `summary` (default: best results in full, frontier as parameter sets, key metrics only, empty nested fields dropped), `standard`, or `full`; `precision` is `display` (default: ratios to 4 places, prices to 2) or `exact`. Fingerprints are unaffected.
721
724
 
@@ -1048,6 +1051,7 @@ The test suite MUST include:
1048
1051
  | T-044 | LLM-feedback fixes `[V3-26]` | Threshold discount and rebate: EXCESS_ONLY 3,175,000 and ENTIRE_SUBTOTAL 2,925,000 on a 3,250,000 gross with a 2,500,000 threshold; the excess discount starts in the crossing month; missing scope or period is an error with an example; several unit types give a null unit price with a warning, and `client_value_unit` with `+` sums them; bare-engagement candidates work and bad ones get a pointer; the constraint `op` alias works; no tool returns an unstructured error; every quickstart payload runs through the MCP server |
1049
1052
  | T-045 | Termination `[V3-28]` | PRORATE charges the shortfall against 150,000 x 181/365 at a 30 June termination; REMAINDER_DUE charges 90,000 for one year and 240,000 with a second year, and owes later years at a period-end termination; WAIVED charges none; the fee posts; quantities spread over the original term; unamortized transition is written off; a cut-short outcome fee is not charged; a missing rule, an out-of-range date, and a mid-month date without proration are errors |
1050
1053
  | T-046 | Patterns `[V3-29]` | Every pattern file matches figures computed by hand in `scripts/build_alsp.py`; the ALSP template round-trips |
1054
+ | T-047 | Counting rules `[V3-30]` | A RATE assumption applies each month; a derived billable quantity prices correctly; rounding is required for COUNT units and applied per month; bad expressions, unknown references, cycles, division by zero, negative results, and name clashes are errors with paths; search can vary an assumption; the counting-rules pattern matches its hand-computed fee |
1051
1055
  | T-042 | Reconciliation `[V3-24]` | Actuals equal to computed figures reconcile; a changed July invoice is reported at month 7 with the mechanism that priced it; the publishable summary contains no amounts, dates, or names; relative tolerance and unknown metrics are honored |
1052
1056
 
1053
1057
  **Property tests** SHOULD verify: non-negative accepted units; non-overlapping effective dates; contiguous tiers; under `MARGINAL` tier schedules with non-negative rates, the pre-credit fee never decreases when eligible consumption increases, holding other terms fixed. Under `ALL_UNITS` schedules the property holds outside every band reported by `TIER_CLIFF` and is not asserted inside them. T-033 runs only when the private research report is present; CI skips it.
@@ -54,3 +54,14 @@ To keep a transition fee outside the minimum, give the minimum `aggregation_scop
54
54
  | base | 3,154,000 |
55
55
 
56
56
  Other termination rules: `REMAINDER_DUE` charges this period's full minimum and every later period's; `WAIVED` charges none. Outcome fees and rebates whose period termination cuts short are not charged, with a `TERMINATED_MID_PERIOD` warning, because the contract decides what is owed.
57
+
58
+ ## 6. Counting rules
59
+
60
+ `pattern_counting_rules`. Pattern 1's volumes with two counting rules, written as derived quantities so every assumption is visible:
61
+
62
+ - **Re-issues billed once:** a `RATE` assumption `reissue_rate` of 5%, and `unique_k = submitted_k x (1 - reissue_rate)` for each tier.
63
+ - **Bundles billed once at the highest tier:** `bundle_share` (10% of lower-tier documents arrive in bundles) and `docs_per_bundle` (4) as `RATE` assumptions. Each lower tier keeps `unique_k - bundled_k`; tier 4 gets `unique_4 + bundles`, where `bundles = (bundled_1 + bundled_2 + bundled_3) / docs_per_bundle`.
64
+
65
+ Monthly billable units become 42.75 / 17.1 / 51.3 / 79.0875 instead of 50 / 20 / 60 / 80, so the year bills 3,552,525 before the discount and 3,497,272.50 after it, against 3,972,000 without the rules. The counts are fractional expected values (`rounding: NONE`); use `HALF_UP`, `DOWN`, or `UP` to bill whole units each month.
66
+
67
+ Monthly totals cannot show which documents arrive together, so the bundle share and size are assumptions, not facts. Search them by adding a parameter such as `{"name": "reissue", "assumption": "reissue_rate", "values": ["0", "0.05", "0.10"]}`.
@@ -1,6 +1,6 @@
1
1
  # Cometric implementation status
2
2
 
3
- Version `0.3.0` (0.1.0 released 2026-11-04; 0.2.0 added reconciliation and the fixes from two code reviews; 0.3.0 adds termination scenarios, the ALSP tiered-minimum template and patterns, and the fixes from an LLM-driven pricing analysis). The normative specification is `COMETRIC_SPEC_v0.3.md`, which incorporates every clarification below.
3
+ Version `0.4.0` (0.1.0 released 2026-11-04; 0.4.0 adds counting rules through rate assumptions and derived quantities; 0.2.0 added reconciliation and the fixes from two code reviews; 0.3.0 adds termination scenarios, the ALSP tiered-minimum template and patterns, and the fixes from an LLM-driven pricing analysis). The normative specification is `COMETRIC_SPEC_v0.3.md`, which incorporates every clarification below.
4
4
 
5
5
  ## Milestones
6
6
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cometric"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  description = "Commercial Intelligence Engine: model, simulate, and optimize the economics of service engagements."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -395,6 +395,125 @@ def patterns(t: dict) -> dict[str, dict]:
395
395
  "base": {"contractual_fee": str(base9 - (base9 - 3_000_000) // 10 + 100_000)},
396
396
  },
397
397
  }
398
+ # 6. counting rules: re-issues counted once, bundles billed once at the highest tier
399
+ from decimal import Decimal as Dm
400
+
401
+ e6 = copy.deepcopy(filled)
402
+ e6["engagement_id"] = "PATTERN-COUNTING-RULES"
403
+ monthly = {"tier_1": Dm(50), "tier_2": Dm(20), "tier_3": Dm(60), "tier_4": Dm(80)}
404
+ reissue, share, per_bundle = Dm("0.05"), Dm("0.10"), Dm(4)
405
+ unique = {t: v * (1 - reissue) for t, v in monthly.items()}
406
+ bundled = {t: unique[t] * share for t in TIERS[:3]}
407
+ billable = {t: unique[t] - bundled[t] for t in TIERS[:3]}
408
+ billable["tier_4"] = unique["tier_4"] + sum(bundled.values()) / per_bundle
409
+ month_fee = sum(billable[t] * Dm(RATES[t]) for t in TIERS)
410
+ gross6 = month_fee * 12
411
+ fee6 = gross6 - (gross6 - 3_000_000) * Dm("0.10")
412
+ sub = {"submitted_" + t.split("_")[1]: str(int(v) * 12) for t, v in monthly.items()}
413
+ assumptions = [
414
+ {
415
+ "name": n,
416
+ "value": v,
417
+ "unit": "tier_" + n.split("_")[1],
418
+ "time_period": "2027",
419
+ "origin": "ILLUSTRATIVE",
420
+ }
421
+ for n, v in sub.items()
422
+ ]
423
+ assumptions += [
424
+ {
425
+ "name": "reissue_rate",
426
+ "value": "0.05",
427
+ "unit": "ratio",
428
+ "time_period": "TERM",
429
+ "basis": "RATE",
430
+ "origin": "ESTIMATED",
431
+ },
432
+ {
433
+ "name": "bundle_share",
434
+ "value": "0.10",
435
+ "unit": "ratio",
436
+ "time_period": "TERM",
437
+ "basis": "RATE",
438
+ "origin": "ESTIMATED",
439
+ },
440
+ {
441
+ "name": "docs_per_bundle",
442
+ "value": "4",
443
+ "unit": "ratio",
444
+ "time_period": "TERM",
445
+ "basis": "RATE",
446
+ "origin": "ESTIMATED",
447
+ },
448
+ ]
449
+ derived = []
450
+ for t in TIERS:
451
+ k = t.split("_")[1]
452
+ derived.append(
453
+ {
454
+ "name": f"unique_{k}",
455
+ "unit": t,
456
+ "rounding": "NONE",
457
+ "description": "submitted less re-issues (re-issued documents are billed once)",
458
+ "expression": {
459
+ "product": [
460
+ {"q": f"submitted_{k}"},
461
+ {"difference": [{"const": "1"}, {"q": "reissue_rate"}]},
462
+ ]
463
+ },
464
+ }
465
+ )
466
+ for t in TIERS[:3]:
467
+ k = t.split("_")[1]
468
+ derived.append(
469
+ {
470
+ "name": f"bundled_{k}",
471
+ "unit": t,
472
+ "rounding": "NONE",
473
+ "description": "documents that arrive inside a bundle",
474
+ "expression": {"product": [{"q": f"unique_{k}"}, {"q": "bundle_share"}]},
475
+ }
476
+ )
477
+ derived.append(
478
+ {
479
+ "name": t,
480
+ "unit": t,
481
+ "rounding": "NONE",
482
+ "description": "billable documents in this tier",
483
+ "expression": {"difference": [{"q": f"unique_{k}"}, {"q": f"bundled_{k}"}]},
484
+ }
485
+ )
486
+ derived.append(
487
+ {
488
+ "name": "bundles",
489
+ "unit": "tier_4",
490
+ "rounding": "NONE",
491
+ "description": "bundles, each billed once at the highest tier",
492
+ "expression": {
493
+ "quotient": [
494
+ {"sum": [{"q": "bundled_1"}, {"q": "bundled_2"}, {"q": "bundled_3"}]},
495
+ {"q": "docs_per_bundle"},
496
+ ]
497
+ },
498
+ }
499
+ )
500
+ derived.append(
501
+ {
502
+ "name": "tier_4",
503
+ "unit": "tier_4",
504
+ "rounding": "NONE",
505
+ "description": "billable highest-tier documents plus bundles",
506
+ "expression": {"sum": [{"q": "unique_4"}, {"q": "bundles"}]},
507
+ }
508
+ )
509
+ out["pattern_counting_rules"] = {
510
+ "case": "pattern_counting_rules",
511
+ "title": "The tiered minimum with counting rules: 5% re-issues billed once; 10% of lower-tier documents arrive in bundles of 4, each billed once at the highest tier",
512
+ "engagement": e6,
513
+ "scenarios": [{"id": "base", "assumptions": assumptions, "derived": derived}],
514
+ "cost_policies": {},
515
+ "expected": {"base": {"contractual_fee": format(fee6, "f")}},
516
+ }
398
517
  return out
399
518
 
400
519
 
@@ -170,7 +170,7 @@ def calculate_periods(
170
170
  },
171
171
  "term": data["term"],
172
172
  "years": data["years"],
173
- "definitions": definitions(e, result),
173
+ "definitions": definitions(e, result, s),
174
174
  }
175
175
  if "termination" in result.extras:
176
176
  out["termination"] = result.extras["termination"]
@@ -193,7 +193,7 @@ QUANTITY_SPREADING = (
193
193
  )
194
194
 
195
195
 
196
- def definitions(e: Engagement, result: CalculationResult) -> dict:
196
+ def definitions(e: Engagement, result: CalculationResult, s: Scenario | None = None) -> dict:
197
197
  """The conventions a reader needs to quote these figures correctly."""
198
198
  term = result.data["term"]
199
199
  basis = term.get("client_value_quantity")
@@ -243,6 +243,23 @@ def definitions(e: Engagement, result: CalculationResult) -> dict:
243
243
  out["allowances"] = recognition
244
244
  if minimums:
245
245
  out["minimums"] = minimums
246
+ if s is not None:
247
+ rates = sorted({a.name for a in s.assumptions if a.basis == "RATE"})
248
+ if rates:
249
+ out["rate_assumptions"] = rates
250
+ if s.derived:
251
+ from .quantities import expr_text
252
+
253
+ out["derived_quantities"] = [
254
+ {
255
+ "name": d.name,
256
+ "unit": d.unit,
257
+ "formula": expr_text(d.expression),
258
+ "rounding": d.rounding,
259
+ "description": d.description,
260
+ }
261
+ for d in s.derived
262
+ ]
246
263
  return out
247
264
 
248
265
 
@@ -823,18 +840,37 @@ def validate_search_spec(spec: Any, issues: Issues) -> None:
823
840
  if not isinstance(p.get("name"), str):
824
841
  issues.error("SCHEMA", "parameter needs a name", path + "/name")
825
842
  names.append(p.get("name"))
826
- ptrs = p.get("pointers") if "pointers" in p else [p.get("pointer")]
827
- if (
828
- not isinstance(ptrs, list)
829
- or not ptrs
830
- or not all(isinstance(x, str) and x.startswith("/") for x in ptrs)
831
- ):
832
- issues.error(
833
- "SCHEMA",
834
- "parameter needs pointer (a JSON Pointer into the engagement, starting with /) or pointers (a list of them)",
835
- path + ("/pointers" if "pointers" in p else "/pointer"),
836
- example="/arrangement_versions/0/components/0/fixed_amount",
837
- )
843
+ if "assumption" in p:
844
+ if not isinstance(p["assumption"], str):
845
+ issues.error(
846
+ "SCHEMA",
847
+ "assumption is the name of a scenario assumption to set in every scenario",
848
+ path + "/assumption",
849
+ )
850
+ elif not any(
851
+ a.get("name") == p["assumption"]
852
+ for sc in (spec.get("scenarios") or [])
853
+ if isinstance(sc, dict)
854
+ for a in sc.get("assumptions", [])
855
+ ):
856
+ issues.error(
857
+ "UNKNOWN_QUANTITY",
858
+ f"no scenario has an assumption named {p['assumption']!r}",
859
+ path + "/assumption",
860
+ )
861
+ else:
862
+ ptrs = p.get("pointers") if "pointers" in p else [p.get("pointer")]
863
+ if (
864
+ not isinstance(ptrs, list)
865
+ or not ptrs
866
+ or not all(isinstance(x, str) and x.startswith("/") for x in ptrs)
867
+ ):
868
+ issues.error(
869
+ "SCHEMA",
870
+ "parameter needs pointer (a JSON Pointer into the engagement, starting with /), pointers (a list of them), or assumption (a scenario assumption name)",
871
+ path + ("/pointers" if "pointers" in p else "/pointer"),
872
+ example="/arrangement_versions/0/components/0/fixed_amount",
873
+ )
838
874
  if p.get("transform", "set") not in ("set", "scale", "add"):
839
875
  issues.error(
840
876
  "SCHEMA",
@@ -917,7 +953,7 @@ def search_structures(search_spec: dict) -> dict:
917
953
  max_returned = int(spec.get("max_returned", 20))
918
954
  try:
919
955
  grids = [_param_values(p) for p in params]
920
- writes = [_param_writes(base_engagement, p) for p in params]
956
+ writes = [[] if "assumption" in p else _param_writes(base_engagement, p) for p in params]
921
957
  except _SpecError as exc:
922
958
  return to_jsonable({"status": "invalid", "issues": {"errors": [exc.as_issue()]}})
923
959
  except (KeyError, ValueError) as exc:
@@ -957,12 +993,20 @@ def search_structures(search_spec: dict) -> dict:
957
993
  for combo in itertools.product(*grids):
958
994
  counts["enumerated"] += 1
959
995
  eng = copy.deepcopy(base_engagement)
996
+ cand_scenarios = scenarios
960
997
  values = {}
961
998
  for p, write, v in zip(params, writes, combo, strict=True):
962
999
  for ptr, transform in write:
963
1000
  _set_pointer(eng, ptr, transform(v))
1001
+ if "assumption" in p:
1002
+ if cand_scenarios is scenarios:
1003
+ cand_scenarios = copy.deepcopy(scenarios)
1004
+ for sc in cand_scenarios:
1005
+ for a in sc.get("assumptions", []):
1006
+ if a.get("name") == p["assumption"]:
1007
+ a["value"] = v
964
1008
  values[p["name"]] = v
965
- ev = _evaluate_candidate(eng, scenarios, cost_policy)
1009
+ ev = _evaluate_candidate(eng, cand_scenarios, cost_policy)
966
1010
  if not ev["valid"]:
967
1011
  counts["invalid"] += 1
968
1012
  for r in ev["results"]:
@@ -216,6 +216,18 @@ class Origin(StrEnum):
216
216
  INTERNAL_POLICY = "INTERNAL_POLICY"
217
217
 
218
218
 
219
+ class AssumptionBasis(StrEnum):
220
+ TOTAL = "TOTAL" # the value is a total for the time period, spread across its months
221
+ RATE = "RATE" # the value applies to every month of the time period unchanged
222
+
223
+
224
+ class DerivedRounding(StrEnum):
225
+ NONE = "NONE" # keep fractional values (expected-value counts)
226
+ HALF_UP = "HALF_UP"
227
+ DOWN = "DOWN"
228
+ UP = "UP"
229
+
230
+
219
231
  class Distribution(StrEnum):
220
232
  FLAT = "FLAT"
221
233
  PROFILE = "PROFILE"
@@ -13,6 +13,7 @@ from .enums import (
13
13
  AccumulationPeriod,
14
14
  AdjustmentType,
15
15
  AggregationScope,
16
+ AssumptionBasis,
16
17
  Avoidability,
17
18
  BillingBasis,
18
19
  BreachMode,
@@ -23,6 +24,7 @@ from .enums import (
23
24
  CostBehavior,
24
25
  CostCategory,
25
26
  CostPeriod,
27
+ DerivedRounding,
26
28
  Distribution,
27
29
  MarginLayer,
28
30
  Origin,
@@ -421,6 +423,10 @@ class Assumption(Model):
421
423
  unit: str
422
424
  time_period: str = Field(description="YYYY-MM, YYYY, YYYY-MM..YYYY-MM, or TERM")
423
425
  origin: Origin
426
+ basis: AssumptionBasis = Field(
427
+ default=AssumptionBasis.TOTAL,
428
+ description="TOTAL (default): value is the total for time_period, spread across its months. RATE: value applies to every month unchanged (use for rates and shares such as a re-issue rate).",
429
+ )
424
430
  distribution: Distribution = Distribution.FLAT
425
431
  profile: list[Dec] = []
426
432
 
@@ -437,10 +443,33 @@ class Assumption(Model):
437
443
  raise ValueError("time_period must be YYYY-MM, YYYY, YYYY-MM..YYYY-MM, or TERM")
438
444
 
439
445
 
446
+ class DerivedQuantity(Model):
447
+ """A quantity computed each month from other quantities, e.g. billable units after a counting rule.
448
+
449
+ expression: {"q": name} | {"const": "1"} | {"sum": [e, ...]} | {"difference": [a, b]} |
450
+ {"product": [e, ...]} | {"quotient": [a, b]}. Derived quantities may use other derived ones.
451
+ """
452
+
453
+ name: str
454
+ unit: str
455
+ expression: dict
456
+ rounding: DerivedRounding | None = Field(
457
+ default=None,
458
+ description="Required for COUNT units: NONE keeps fractional expected values; HALF_UP, DOWN, or UP round each month",
459
+ )
460
+ description: str | None = Field(
461
+ default=None, description="The counting rule or assumption this expresses, in words"
462
+ )
463
+
464
+
440
465
  class Scenario(Model):
441
466
  id: str
442
467
  horizon: Horizon | None = None
443
468
  assumptions: list[Assumption] = []
469
+ derived: list[DerivedQuantity] = Field(
470
+ default=[],
471
+ description="Quantities computed each month from assumptions, e.g. counting rules; usable anywhere a quantity_source is",
472
+ )
444
473
  breach_mode: BreachMode | None = None
445
474
  termination_date: date | None = Field(
446
475
  default=None,