cctally 1.24.0 → 1.26.0

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.
package/bin/_lib_share.py CHANGED
@@ -66,8 +66,14 @@ class DeltaCell:
66
66
 
67
67
  @dataclass(frozen=True)
68
68
  class ProjectCell:
69
- """Anonymization chokepoint — scrubber rewrites the `label` field."""
69
+ """Anonymization chokepoint — scrubber rewrites the `label` field.
70
+
71
+ `rank_cost` (#130): an explicit spend value used by
72
+ `_collect_project_costs` to rank anonymized labels, replacing the old
73
+ hidden-MoneyCell hack. When None, ranking falls back to summing sibling
74
+ MoneyCells (back-compat for every non-budget construction site)."""
70
75
  label: str
76
+ rank_cost: float | None = None
71
77
 
72
78
  Cell = TextCell | MoneyCell | PercentCell | DateCell | DeltaCell | ProjectCell
73
79
 
@@ -735,7 +741,8 @@ def _render_svg_footer(snap: ShareSnapshot, *, palette: dict,
735
741
 
736
742
  def _collect_project_costs(snap: ShareSnapshot) -> dict[str, float]:
737
743
  """Walk rows: for each row containing a ProjectCell, sum MoneyCell values
738
- in the same row under the project label.
744
+ in the same row under the project label — unless the ProjectCell carries an
745
+ explicit ``rank_cost``, which takes precedence over the MoneyCell sum (#130).
739
746
 
740
747
  Charts also contribute via ChartPoint.project_label + y_value (when y_value
741
748
  is in $). For consistency we union both sources; rows take precedence on
@@ -744,13 +751,16 @@ def _collect_project_costs(snap: ShareSnapshot) -> dict[str, float]:
744
751
  for row in snap.rows:
745
752
  proj_label: str | None = None
746
753
  money = 0.0
754
+ explicit_rank: float | None = None
747
755
  for cell in row.cells.values():
748
756
  if isinstance(cell, ProjectCell):
749
757
  proj_label = cell.label
758
+ explicit_rank = cell.rank_cost
750
759
  elif isinstance(cell, MoneyCell):
751
760
  money += cell.usd
752
761
  if proj_label is not None:
753
- costs[proj_label] = costs.get(proj_label, 0.0) + money
762
+ contribution = explicit_rank if explicit_rank is not None else money
763
+ costs[proj_label] = costs.get(proj_label, 0.0) + contribution
754
764
 
755
765
  if snap.chart is not None:
756
766
  chart_pts: list[ChartPoint] = []
@@ -825,7 +835,9 @@ def _apply_anon_mapping(
825
835
  new_cells: dict[str, Cell] = {}
826
836
  for key, cell in row.cells.items():
827
837
  if isinstance(cell, ProjectCell) and cell.label in mapping:
828
- new_cells[key] = ProjectCell(mapping[cell.label])
838
+ new_cells[key] = ProjectCell(
839
+ mapping[cell.label], rank_cost=cell.rank_cost
840
+ )
829
841
  else:
830
842
  new_cells[key] = cell
831
843
  new_rows.append(Row(cells=new_cells))
package/bin/cctally CHANGED
@@ -376,11 +376,13 @@ _lib_alerts_payload = _load_sibling("_lib_alerts_payload")
376
376
  _alert_text_weekly = _lib_alerts_payload._alert_text_weekly
377
377
  _alert_text_five_hour = _lib_alerts_payload._alert_text_five_hour
378
378
  _alert_text_budget = _lib_alerts_payload._alert_text_budget
379
+ _alert_text_project_budget = _lib_alerts_payload._alert_text_project_budget
379
380
  _alert_text_projected = _lib_alerts_payload._alert_text_projected
380
381
  _escape_applescript_string = _lib_alerts_payload._escape_applescript_string
381
382
  _build_alert_payload_weekly = _lib_alerts_payload._build_alert_payload_weekly
382
383
  _build_alert_payload_five_hour = _lib_alerts_payload._build_alert_payload_five_hour
383
384
  _build_alert_payload_budget = _lib_alerts_payload._build_alert_payload_budget
385
+ _build_alert_payload_project_budget = _lib_alerts_payload._build_alert_payload_project_budget
384
386
  _build_alert_payload_projected = _lib_alerts_payload._build_alert_payload_projected
385
387
 
386
388
  _lib_alert_dispatch = _load_sibling("_lib_alert_dispatch")
@@ -628,6 +630,13 @@ cmd_sync_week = _cctally_sync_week.cmd_sync_week
628
630
  # module-private (no external caller).
629
631
  _cctally_project = _load_sibling("_cctally_project")
630
632
  cmd_project = _cctally_project.cmd_project
633
+ # Shared per-project cost compute (#19/#121, spec §7.1). Re-exported so the
634
+ # per-project budget display (cmd_budget) and the Task 3 firing path reach it
635
+ # via the cctally namespace.
636
+ _sum_cost_by_project = _cctally_project._sum_cost_by_project
637
+ # Shared per-project label disambiguation (#130). Re-exported so the budget
638
+ # display, firing path, and dashboard SSE envelope reach the SAME primitive.
639
+ _project_budget_labels = _cctally_project._project_budget_labels
631
640
 
632
641
  # Eager re-export of bin/_cctally_pricing_check.py — `cmd_pricing_check`
633
642
  # is invoked via the parser's `set_defaults(func=c.cmd_pricing_check)`,
@@ -812,6 +821,7 @@ _PERCENT_NORMALIZE_DECIMALS = _cctally_record._PERCENT_NORMALIZE_DECIMALS
812
821
  _normalize_percent = _cctally_record._normalize_percent
813
822
  maybe_record_milestone = _cctally_record.maybe_record_milestone
814
823
  maybe_record_budget_milestone = _cctally_record.maybe_record_budget_milestone
824
+ maybe_record_project_budget_milestone = _cctally_record.maybe_record_project_budget_milestone
815
825
  maybe_record_projected_alert = _cctally_record.maybe_record_projected_alert
816
826
  _weekly_pct_week_avg_projection = _cctally_record._weekly_pct_week_avg_projection
817
827
  _compute_block_totals = _cctally_record._compute_block_totals
@@ -1107,6 +1117,11 @@ _render_forecast_terminal = _cctally_forecast._render_forecast_terminal
1107
1117
  _BUDGET_JSON_SCHEMA_VERSION = _cctally_forecast._BUDGET_JSON_SCHEMA_VERSION
1108
1118
  _cmd_budget_set = _cctally_forecast._cmd_budget_set
1109
1119
  _cmd_budget_unset = _cctally_forecast._cmd_budget_unset
1120
+ # Per-project budget set/unset (#19/#121, spec §4.3 / §7).
1121
+ _resolve_project_budget_target = _cctally_forecast._resolve_project_budget_target
1122
+ _cmd_budget_set_project = _cctally_forecast._cmd_budget_set_project
1123
+ _cmd_budget_unset_project = _cctally_forecast._cmd_budget_unset_project
1124
+ _build_project_budget_rows = _cctally_forecast._build_project_budget_rows
1110
1125
  _budget_render_unset = _cctally_forecast._budget_render_unset
1111
1126
  _budget_verdict_ansi_code = _cctally_forecast._budget_verdict_ansi_code
1112
1127
  _budget_render_terminal = _cctally_forecast._budget_render_terminal
@@ -2068,10 +2083,13 @@ get_milestone_cost_for_week = _cctally_milestones.get_milestone_cost_for
2068
2083
  get_milestones_for_week = _cctally_milestones.get_milestones_for_week # forecast c.; tui shim; percent-breakdown c.
2069
2084
  insert_percent_milestone = _cctally_milestones.insert_percent_milestone # record shim; idempotency-test mod.
2070
2085
  insert_budget_milestone = _cctally_milestones.insert_budget_milestone # record shim
2086
+ insert_project_budget_milestone = _cctally_milestones.insert_project_budget_milestone # record shim; project-budget-config-test ns[]
2087
+ _project_crossings = _cctally_milestones._project_crossings # record shim; milestones c. (#130 firing/reconcile shared crossing arithmetic)
2071
2088
  insert_projected_milestone = _cctally_milestones.insert_projected_milestone # record shim
2072
2089
  _projected_levels_already_latched = _cctally_milestones._projected_levels_already_latched # record shim
2073
2090
  _reconcile_budget_milestones_on_set = _cctally_milestones._reconcile_budget_milestones_on_set # test_budget_alerts ns[]
2074
2091
  _reconcile_budget_on_config_write = _cctally_milestones._reconcile_budget_on_config_write # forecast/config/dashboard c.; test_forecast_ns_patch mod. patch
2092
+ _reconcile_project_budget_milestones_on_write = _cctally_milestones._reconcile_project_budget_milestones_on_write # forecast/config/dashboard c. (forward-only project-budget reconcile)
2075
2093
 
2076
2094
 
2077
2095
  # === Update-banner predicate (spec §4.2) extracted to