trueppm-api 0.4.0__py3-none-any.whl

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 (799) hide show
  1. trueppm_api/__init__.py +27 -0
  2. trueppm_api/apps/access/__init__.py +0 -0
  3. trueppm_api/apps/access/apps.py +19 -0
  4. trueppm_api/apps/access/groups.py +518 -0
  5. trueppm_api/apps/access/management/__init__.py +0 -0
  6. trueppm_api/apps/access/management/commands/__init__.py +0 -0
  7. trueppm_api/apps/access/management/commands/create_admin.py +346 -0
  8. trueppm_api/apps/access/management/commands/migrate_locked.py +96 -0
  9. trueppm_api/apps/access/management/commands/revoke_api_tokens.py +483 -0
  10. trueppm_api/apps/access/migrations/0001_initial.py +61 -0
  11. trueppm_api/apps/access/migrations/0001_squashed_0012_programmembership_role_title.py +184 -0
  12. trueppm_api/apps/access/migrations/0002_alter_projectmembership_id.py +29 -0
  13. trueppm_api/apps/access/migrations/0003_projectmembership_soft_delete.py +24 -0
  14. trueppm_api/apps/access/migrations/0004_alter_projectmembership_role.py +34 -0
  15. trueppm_api/apps/access/migrations/0005_program_entity_and_membership.py +76 -0
  16. trueppm_api/apps/access/migrations/0006_role_ordinal_spacing.py +113 -0
  17. trueppm_api/apps/access/migrations/0007_projectmembership_joined_at_and_more.py +35 -0
  18. trueppm_api/apps/access/migrations/0008_projectmembership_source_group.py +25 -0
  19. trueppm_api/apps/access/migrations/0009_projectmembership_pm_proj_serverver_idx.py +20 -0
  20. trueppm_api/apps/access/migrations/0010_programmembership_joined_at_and_more.py +38 -0
  21. trueppm_api/apps/access/migrations/0011_alter_programmembership_unique_together_and_more.py +36 -0
  22. trueppm_api/apps/access/migrations/0012_programmembership_role_title.py +17 -0
  23. trueppm_api/apps/access/migrations/0013_squashed_0019_release_0_4.py +302 -0
  24. trueppm_api/apps/access/migrations/0013_userdefinedmentiongroup.py +82 -0
  25. trueppm_api/apps/access/migrations/0014_programmembership_progm_serverver_idx.py +19 -0
  26. trueppm_api/apps/access/migrations/0015_programuserdefinedmentiongroup.py +86 -0
  27. trueppm_api/apps/access/migrations/0016_externalstakeholder.py +67 -0
  28. trueppm_api/apps/access/migrations/0017_viewer_ordinal_one.py +143 -0
  29. trueppm_api/apps/access/migrations/0018_programmembership_sync_seq_and_more.py +52 -0
  30. trueppm_api/apps/access/migrations/0019_programmembership_reinstated_at_and_more.py +22 -0
  31. trueppm_api/apps/access/migrations/__init__.py +0 -0
  32. trueppm_api/apps/access/minter_role.py +38 -0
  33. trueppm_api/apps/access/models.py +588 -0
  34. trueppm_api/apps/access/permissions.py +3263 -0
  35. trueppm_api/apps/access/serializers.py +1007 -0
  36. trueppm_api/apps/access/services.py +878 -0
  37. trueppm_api/apps/access/signals.py +171 -0
  38. trueppm_api/apps/access/tasks.py +50 -0
  39. trueppm_api/apps/access/throttles.py +96 -0
  40. trueppm_api/apps/access/urls.py +191 -0
  41. trueppm_api/apps/access/views.py +1965 -0
  42. trueppm_api/apps/agents/__init__.py +6 -0
  43. trueppm_api/apps/agents/apps.py +11 -0
  44. trueppm_api/apps/agents/canonical.py +73 -0
  45. trueppm_api/apps/agents/deferred.py +108 -0
  46. trueppm_api/apps/agents/management/__init__.py +0 -0
  47. trueppm_api/apps/agents/management/commands/__init__.py +0 -0
  48. trueppm_api/apps/agents/management/commands/audit_prune.py +137 -0
  49. trueppm_api/apps/agents/management/commands/audit_verify.py +101 -0
  50. trueppm_api/apps/agents/middleware.py +44 -0
  51. trueppm_api/apps/agents/migrations/0001_initial.py +231 -0
  52. trueppm_api/apps/agents/migrations/0001_squashed_0003_release_0_4.py +344 -0
  53. trueppm_api/apps/agents/migrations/0002_agentactioncheckpoint.py +71 -0
  54. trueppm_api/apps/agents/migrations/0003_agentactionrefusaldetail.py +58 -0
  55. trueppm_api/apps/agents/migrations/__init__.py +0 -0
  56. trueppm_api/apps/agents/models.py +371 -0
  57. trueppm_api/apps/agents/refusal.py +103 -0
  58. trueppm_api/apps/agents/serializers.py +85 -0
  59. trueppm_api/apps/agents/services.py +397 -0
  60. trueppm_api/apps/agents/signals.py +30 -0
  61. trueppm_api/apps/agents/urls.py +15 -0
  62. trueppm_api/apps/agents/views.py +109 -0
  63. trueppm_api/apps/csvimport/__init__.py +12 -0
  64. trueppm_api/apps/csvimport/apps.py +11 -0
  65. trueppm_api/apps/csvimport/mapping.py +363 -0
  66. trueppm_api/apps/csvimport/migrations/0001_initial.py +74 -0
  67. trueppm_api/apps/csvimport/migrations/0001_squashed_0003_release_0_4.py +90 -0
  68. trueppm_api/apps/csvimport/migrations/0002_schedule_outline_batch_operations.py +37 -0
  69. trueppm_api/apps/csvimport/migrations/0003_csvimportrequest_date_order_and_more.py +26 -0
  70. trueppm_api/apps/csvimport/migrations/__init__.py +0 -0
  71. trueppm_api/apps/csvimport/models.py +111 -0
  72. trueppm_api/apps/csvimport/parser.py +1825 -0
  73. trueppm_api/apps/csvimport/services.py +48 -0
  74. trueppm_api/apps/csvimport/tasks.py +459 -0
  75. trueppm_api/apps/csvimport/template.py +37 -0
  76. trueppm_api/apps/csvimport/urls.py +50 -0
  77. trueppm_api/apps/csvimport/views.py +793 -0
  78. trueppm_api/apps/history/__init__.py +0 -0
  79. trueppm_api/apps/history/apps.py +12 -0
  80. trueppm_api/apps/history/changelog.py +359 -0
  81. trueppm_api/apps/history/diff_policy.py +178 -0
  82. trueppm_api/apps/history/serializers.py +104 -0
  83. trueppm_api/apps/history/signals.py +82 -0
  84. trueppm_api/apps/history/tasks.py +105 -0
  85. trueppm_api/apps/history/urls.py +38 -0
  86. trueppm_api/apps/history/views.py +443 -0
  87. trueppm_api/apps/idempotency/__init__.py +0 -0
  88. trueppm_api/apps/idempotency/apps.py +11 -0
  89. trueppm_api/apps/idempotency/migrations/0001_initial.py +71 -0
  90. trueppm_api/apps/idempotency/migrations/__init__.py +0 -0
  91. trueppm_api/apps/idempotency/mixins.py +226 -0
  92. trueppm_api/apps/idempotency/models.py +75 -0
  93. trueppm_api/apps/idempotency/tasks.py +61 -0
  94. trueppm_api/apps/integrations/__init__.py +13 -0
  95. trueppm_api/apps/integrations/apps.py +59 -0
  96. trueppm_api/apps/integrations/connections.py +806 -0
  97. trueppm_api/apps/integrations/encryption.py +92 -0
  98. trueppm_api/apps/integrations/external_sources.py +863 -0
  99. trueppm_api/apps/integrations/git_automation_services.py +231 -0
  100. trueppm_api/apps/integrations/git_webhook_auth.py +243 -0
  101. trueppm_api/apps/integrations/http.py +597 -0
  102. trueppm_api/apps/integrations/me_work.py +218 -0
  103. trueppm_api/apps/integrations/migrations/0001_initial.py +64 -0
  104. trueppm_api/apps/integrations/migrations/0001_squashed_0006_tasklink_description_tasklink_preview_type_and_more.py +187 -0
  105. trueppm_api/apps/integrations/migrations/0002_tasklink.py +67 -0
  106. trueppm_api/apps/integrations/migrations/0003_tasklink_tasklink_serverver_idx.py +17 -0
  107. trueppm_api/apps/integrations/migrations/0004_tasklink_custom_title_tasklink_labels.py +25 -0
  108. trueppm_api/apps/integrations/migrations/0005_boardautomation.py +56 -0
  109. trueppm_api/apps/integrations/migrations/0006_tasklink_description_tasklink_preview_type_and_more.py +40 -0
  110. trueppm_api/apps/integrations/migrations/0007_squashed_0014_release_0_4.py +225 -0
  111. trueppm_api/apps/integrations/migrations/0007_tasklink_created_at.py +23 -0
  112. trueppm_api/apps/integrations/migrations/0008_integrationcredential_config_externalworkitem.py +66 -0
  113. trueppm_api/apps/integrations/migrations/0009_externalsyncrequest_and_more.py +96 -0
  114. trueppm_api/apps/integrations/migrations/0010_externalworkitem_due_date.py +17 -0
  115. trueppm_api/apps/integrations/migrations/0011_tasklink_sync_seq_tasklink_tasklink_syncseq_idx.py +22 -0
  116. trueppm_api/apps/integrations/migrations/0012_boardautomation_last_delivery_at_and_more.py +42 -0
  117. trueppm_api/apps/integrations/migrations/0013_externalsyncrequest_attempt_count_and_more.py +26 -0
  118. trueppm_api/apps/integrations/migrations/0014_tasklink_created_by.py +27 -0
  119. trueppm_api/apps/integrations/migrations/__init__.py +0 -0
  120. trueppm_api/apps/integrations/models.py +635 -0
  121. trueppm_api/apps/integrations/notification_channels.py +57 -0
  122. trueppm_api/apps/integrations/opengraph.py +229 -0
  123. trueppm_api/apps/integrations/outgoing.py +201 -0
  124. trueppm_api/apps/integrations/providers.py +528 -0
  125. trueppm_api/apps/integrations/registry.py +345 -0
  126. trueppm_api/apps/integrations/serializers.py +453 -0
  127. trueppm_api/apps/integrations/services.py +171 -0
  128. trueppm_api/apps/integrations/tasks.py +734 -0
  129. trueppm_api/apps/integrations/throttles.py +165 -0
  130. trueppm_api/apps/integrations/urls.py +77 -0
  131. trueppm_api/apps/integrations/views.py +1158 -0
  132. trueppm_api/apps/jiraimport/__init__.py +8 -0
  133. trueppm_api/apps/jiraimport/apps.py +11 -0
  134. trueppm_api/apps/jiraimport/migrations/0001_initial.py +72 -0
  135. trueppm_api/apps/jiraimport/migrations/__init__.py +0 -0
  136. trueppm_api/apps/jiraimport/models.py +70 -0
  137. trueppm_api/apps/jiraimport/parser.py +350 -0
  138. trueppm_api/apps/jiraimport/services.py +46 -0
  139. trueppm_api/apps/jiraimport/tasks.py +280 -0
  140. trueppm_api/apps/jiraimport/urls.py +15 -0
  141. trueppm_api/apps/jiraimport/views.py +153 -0
  142. trueppm_api/apps/msproject/__init__.py +0 -0
  143. trueppm_api/apps/msproject/apps.py +11 -0
  144. trueppm_api/apps/msproject/dataclasses.py +153 -0
  145. trueppm_api/apps/msproject/exporter.py +551 -0
  146. trueppm_api/apps/msproject/extended_attributes.py +77 -0
  147. trueppm_api/apps/msproject/importer.py +1095 -0
  148. trueppm_api/apps/msproject/migrations/0001_import_request.py +77 -0
  149. trueppm_api/apps/msproject/migrations/0001_squashed_0002_importrequest_creates_project.py +80 -0
  150. trueppm_api/apps/msproject/migrations/0002_importrequest_creates_project.py +15 -0
  151. trueppm_api/apps/msproject/migrations/__init__.py +0 -0
  152. trueppm_api/apps/msproject/models.py +81 -0
  153. trueppm_api/apps/msproject/parser.py +1276 -0
  154. trueppm_api/apps/msproject/serializers.py +136 -0
  155. trueppm_api/apps/msproject/services.py +58 -0
  156. trueppm_api/apps/msproject/tasks.py +439 -0
  157. trueppm_api/apps/msproject/urls.py +39 -0
  158. trueppm_api/apps/msproject/views.py +531 -0
  159. trueppm_api/apps/notifications/__init__.py +7 -0
  160. trueppm_api/apps/notifications/apps.py +15 -0
  161. trueppm_api/apps/notifications/backfill.py +79 -0
  162. trueppm_api/apps/notifications/categories.py +112 -0
  163. trueppm_api/apps/notifications/delivery_limits.py +258 -0
  164. trueppm_api/apps/notifications/digests.py +462 -0
  165. trueppm_api/apps/notifications/email_backend.py +350 -0
  166. trueppm_api/apps/notifications/email_health.py +128 -0
  167. trueppm_api/apps/notifications/migrations/0001_initial.py +207 -0
  168. trueppm_api/apps/notifications/migrations/0001_squashed_0006_notification_task.py +299 -0
  169. trueppm_api/apps/notifications/migrations/0002_project_notification_preference.py +67 -0
  170. trueppm_api/apps/notifications/migrations/0003_projectnotificationpreference_paused.py +17 -0
  171. trueppm_api/apps/notifications/migrations/0004_clean_unknown_matrix_keys.py +30 -0
  172. trueppm_api/apps/notifications/migrations/0005_notification_body_notification_event_type_and_more.py +27 -0
  173. trueppm_api/apps/notifications/migrations/0006_notification_task.py +25 -0
  174. trueppm_api/apps/notifications/migrations/0007_squashed_0011_release_0_4.py +204 -0
  175. trueppm_api/apps/notifications/migrations/0007_workspaceemailsettings.py +80 -0
  176. trueppm_api/apps/notifications/migrations/0008_notification_snoozed_until.py +17 -0
  177. trueppm_api/apps/notifications/migrations/0009_usernotificationsettings.py +43 -0
  178. trueppm_api/apps/notifications/migrations/0010_projectnotificationpreference_schema_version.py +20 -0
  179. trueppm_api/apps/notifications/migrations/0011_usernotificationsettings_digest_hour_and_more.py +96 -0
  180. trueppm_api/apps/notifications/migrations/__init__.py +0 -0
  181. trueppm_api/apps/notifications/models.py +1148 -0
  182. trueppm_api/apps/notifications/receivers.py +155 -0
  183. trueppm_api/apps/notifications/schema_migrations.py +56 -0
  184. trueppm_api/apps/notifications/serializers.py +1094 -0
  185. trueppm_api/apps/notifications/services.py +1489 -0
  186. trueppm_api/apps/notifications/tasks.py +762 -0
  187. trueppm_api/apps/notifications/throttles.py +98 -0
  188. trueppm_api/apps/notifications/urls.py +97 -0
  189. trueppm_api/apps/notifications/views.py +919 -0
  190. trueppm_api/apps/observability/__init__.py +0 -0
  191. trueppm_api/apps/observability/apps.py +31 -0
  192. trueppm_api/apps/observability/logging.py +213 -0
  193. trueppm_api/apps/observability/migrations/0001_initial.py +35 -0
  194. trueppm_api/apps/observability/migrations/0001_squashed_0003_alter_purgerun_trigger.py +154 -0
  195. trueppm_api/apps/observability/migrations/0002_purgerun_retentionpolicy_retentionschedule.py +131 -0
  196. trueppm_api/apps/observability/migrations/0003_alter_purgerun_trigger.py +21 -0
  197. trueppm_api/apps/observability/migrations/0004_alter_retentionpolicy_key.py +28 -0
  198. trueppm_api/apps/observability/migrations/__init__.py +0 -0
  199. trueppm_api/apps/observability/models.py +141 -0
  200. trueppm_api/apps/observability/otel/__init__.py +45 -0
  201. trueppm_api/apps/observability/otel/attributes.py +247 -0
  202. trueppm_api/apps/observability/otel/export_health.py +533 -0
  203. trueppm_api/apps/observability/otel/instrumentation.py +351 -0
  204. trueppm_api/apps/observability/otel/metrics.py +566 -0
  205. trueppm_api/apps/observability/otel/provider.py +511 -0
  206. trueppm_api/apps/observability/otel/request_attributes.py +204 -0
  207. trueppm_api/apps/observability/purge_registry.py +124 -0
  208. trueppm_api/apps/observability/retention.py +139 -0
  209. trueppm_api/apps/observability/selectors.py +993 -0
  210. trueppm_api/apps/observability/serializers.py +142 -0
  211. trueppm_api/apps/observability/services.py +407 -0
  212. trueppm_api/apps/observability/tasks.py +266 -0
  213. trueppm_api/apps/observability/urls.py +27 -0
  214. trueppm_api/apps/observability/views.py +537 -0
  215. trueppm_api/apps/profiles/__init__.py +0 -0
  216. trueppm_api/apps/profiles/apps.py +15 -0
  217. trueppm_api/apps/profiles/constants.py +54 -0
  218. trueppm_api/apps/profiles/migrations/0001_initial.py +55 -0
  219. trueppm_api/apps/profiles/migrations/0001_squashed_0004_userprofile_role_context.py +125 -0
  220. trueppm_api/apps/profiles/migrations/0002_userprofile_hidden_views.py +17 -0
  221. trueppm_api/apps/profiles/migrations/0003_projectvisit.py +63 -0
  222. trueppm_api/apps/profiles/migrations/0004_userprofile_role_context.py +26 -0
  223. trueppm_api/apps/profiles/migrations/0005_squashed_0009_release_0_4.py +128 -0
  224. trueppm_api/apps/profiles/migrations/0005_userprofile_schedule_in_deliver.py +20 -0
  225. trueppm_api/apps/profiles/migrations/0006_userprofile_date_format_userprofile_timezone.py +36 -0
  226. trueppm_api/apps/profiles/migrations/0007_userpin.py +91 -0
  227. trueppm_api/apps/profiles/migrations/0008_remove_userprofile_schedule_in_deliver.py +71 -0
  228. trueppm_api/apps/profiles/migrations/0009_auth_user_email_upper_idx.py +81 -0
  229. trueppm_api/apps/profiles/migrations/__init__.py +0 -0
  230. trueppm_api/apps/profiles/models.py +276 -0
  231. trueppm_api/apps/profiles/serializers.py +199 -0
  232. trueppm_api/apps/profiles/services.py +541 -0
  233. trueppm_api/apps/profiles/views.py +124 -0
  234. trueppm_api/apps/projects/__init__.py +0 -0
  235. trueppm_api/apps/projects/actual_date_rules.py +163 -0
  236. trueppm_api/apps/projects/amend.py +133 -0
  237. trueppm_api/apps/projects/apps.py +38 -0
  238. trueppm_api/apps/projects/asset_feed.py +459 -0
  239. trueppm_api/apps/projects/asset_views.py +317 -0
  240. trueppm_api/apps/projects/attachment_policy.py +278 -0
  241. trueppm_api/apps/projects/authentication.py +458 -0
  242. trueppm_api/apps/projects/backfill.py +459 -0
  243. trueppm_api/apps/projects/backlog_services.py +324 -0
  244. trueppm_api/apps/projects/backlog_views.py +302 -0
  245. trueppm_api/apps/projects/batch_operation_views.py +264 -0
  246. trueppm_api/apps/projects/blocker_services.py +510 -0
  247. trueppm_api/apps/projects/board_activity.py +462 -0
  248. trueppm_api/apps/projects/board_activity_views.py +186 -0
  249. trueppm_api/apps/projects/board_lanes.py +134 -0
  250. trueppm_api/apps/projects/bulk_settings.py +194 -0
  251. trueppm_api/apps/projects/bundled_templates.py +215 -0
  252. trueppm_api/apps/projects/calendar_settings.py +180 -0
  253. trueppm_api/apps/projects/ceremony_views.py +244 -0
  254. trueppm_api/apps/projects/commit_moment.py +212 -0
  255. trueppm_api/apps/projects/config_notice.py +1330 -0
  256. trueppm_api/apps/projects/custom_field_values.py +266 -0
  257. trueppm_api/apps/projects/decisions_services.py +132 -0
  258. trueppm_api/apps/projects/decisions_views.py +97 -0
  259. trueppm_api/apps/projects/estimation_scale.py +126 -0
  260. trueppm_api/apps/projects/export_bundle.py +423 -0
  261. trueppm_api/apps/projects/fixtures/seeds/atlas-platform-launch.json +4278 -0
  262. trueppm_api/apps/projects/fixtures/seeds/aurora-mobile-app.json +2001 -0
  263. trueppm_api/apps/projects/fixtures/seeds/bayside-civic-center.json +1827 -0
  264. trueppm_api/apps/projects/fixtures/seeds/ga-launch.json +1909 -0
  265. trueppm_api/apps/projects/fixtures/seeds/helios-crm-replacement.json +1410 -0
  266. trueppm_api/apps/projects/guardrail_policy_source.py +269 -0
  267. trueppm_api/apps/projects/inbound_sync.py +555 -0
  268. trueppm_api/apps/projects/iteration_label.py +85 -0
  269. trueppm_api/apps/projects/key_views.py +468 -0
  270. trueppm_api/apps/projects/keys.py +205 -0
  271. trueppm_api/apps/projects/lifecycle.py +191 -0
  272. trueppm_api/apps/projects/management/__init__.py +0 -0
  273. trueppm_api/apps/projects/management/commands/__init__.py +0 -0
  274. trueppm_api/apps/projects/management/commands/backfill_in_progress_status.py +92 -0
  275. trueppm_api/apps/projects/management/commands/create_demo_share_link.py +368 -0
  276. trueppm_api/apps/projects/management/commands/export_program.py +50 -0
  277. trueppm_api/apps/projects/management/commands/import_seed.py +165 -0
  278. trueppm_api/apps/projects/management/commands/load_sample_project.py +514 -0
  279. trueppm_api/apps/projects/management/commands/seed_capacity.py +406 -0
  280. trueppm_api/apps/projects/management/commands/seed_integration_fixtures.py +192 -0
  281. trueppm_api/apps/projects/management/commands/shift_sample_dates.py +120 -0
  282. trueppm_api/apps/projects/mcp_settings.py +305 -0
  283. trueppm_api/apps/projects/methodology.py +178 -0
  284. trueppm_api/apps/projects/migration_helpers.py +75 -0
  285. trueppm_api/apps/projects/migrations/0001_initial.py +208 -0
  286. trueppm_api/apps/projects/migrations/0001_squashed_0094_project_decisions_policy.py +5659 -0
  287. trueppm_api/apps/projects/migrations/0002_alter_uuid_pk_serialize_false.py +57 -0
  288. trueppm_api/apps/projects/migrations/0003_rename_task_project_index.py +25 -0
  289. trueppm_api/apps/projects/migrations/0004_soft_delete_and_dependency_versioned.py +70 -0
  290. trueppm_api/apps/projects/migrations/0005_task_assignee.py +33 -0
  291. trueppm_api/apps/projects/migrations/0006_task_utilization_index.py +31 -0
  292. trueppm_api/apps/projects/migrations/0007_task_is_milestone.py +28 -0
  293. trueppm_api/apps/projects/migrations/0008_baseline.py +119 -0
  294. trueppm_api/apps/projects/migrations/0009_historicaldependency_historicalproject_and_more.py +210 -0
  295. trueppm_api/apps/projects/migrations/0010_risk.py +229 -0
  296. trueppm_api/apps/projects/migrations/0011_task_status.py +27 -0
  297. trueppm_api/apps/projects/migrations/0012_task_status.py +27 -0
  298. trueppm_api/apps/projects/migrations/0013_task_planned_start.py +24 -0
  299. trueppm_api/apps/projects/migrations/0014_short_hex_ids.py +74 -0
  300. trueppm_api/apps/projects/migrations/0015_backfill_short_ids.py +48 -0
  301. trueppm_api/apps/projects/migrations/0016_task_actual_dates.py +42 -0
  302. trueppm_api/apps/projects/migrations/0017_boardcolumnconfig.py +43 -0
  303. trueppm_api/apps/projects/migrations/0018_estimation_governance.py +62 -0
  304. trueppm_api/apps/projects/migrations/0019_backfill_wbs_paths.py +28 -0
  305. trueppm_api/apps/projects/migrations/0020_task_status_5col.py +65 -0
  306. trueppm_api/apps/projects/migrations/0021_boardsavedview.py +64 -0
  307. trueppm_api/apps/projects/migrations/0022_task_status_changed_at_priority_rank.py +35 -0
  308. trueppm_api/apps/projects/migrations/0023_risk_framework_fields.py +96 -0
  309. trueppm_api/apps/projects/migrations/0024_riskcomment.py +52 -0
  310. trueppm_api/apps/projects/migrations/0025_sprint.py +352 -0
  311. trueppm_api/apps/projects/migrations/0026_project_methodology.py +30 -0
  312. trueppm_api/apps/projects/migrations/0027_sprint_retro.py +89 -0
  313. trueppm_api/apps/projects/migrations/0028_normalize_notes_field.py +42 -0
  314. trueppm_api/apps/projects/migrations/0029_complete_implies_full_progress.py +38 -0
  315. trueppm_api/apps/projects/migrations/0030_review_implies_full_progress.py +37 -0
  316. trueppm_api/apps/projects/migrations/0031_task_remaining_points.py +22 -0
  317. trueppm_api/apps/projects/migrations/0032_task_is_subtask_and_sprint_scope_change.py +90 -0
  318. trueppm_api/apps/projects/migrations/0033_task_my_work_index.py +38 -0
  319. trueppm_api/apps/projects/migrations/0034_inbound_task_sync.py +313 -0
  320. trueppm_api/apps/projects/migrations/0035_retro_versioned_and_suggested_assignee.py +173 -0
  321. trueppm_api/apps/projects/migrations/0036_program_entity_and_membership.py +157 -0
  322. trueppm_api/apps/projects/migrations/0037_sprint_capacity_points.py +20 -0
  323. trueppm_api/apps/projects/migrations/0038_taskcomment_commentreaction_commentacknowledgement_and_more.py +233 -0
  324. trueppm_api/apps/projects/migrations/0039_apitoken_and_more.py +88 -0
  325. trueppm_api/apps/projects/migrations/0040_program_general_fields.py +119 -0
  326. trueppm_api/apps/projects/migrations/0041_ceremony_template_and_phase_gate_config.py +210 -0
  327. trueppm_api/apps/projects/migrations/0041_project_general_fields.py +106 -0
  328. trueppm_api/apps/projects/migrations/0042_merge_20260522_0410.py +12 -0
  329. trueppm_api/apps/projects/migrations/0042_program_rollup_config.py +95 -0
  330. trueppm_api/apps/projects/migrations/0042_workflow_color_and_custom_fields.py +94 -0
  331. trueppm_api/apps/projects/migrations/0043_merge_20260522_1118.py +10 -0
  332. trueppm_api/apps/projects/migrations/0044_merge_20260522_rollup.py +10 -0
  333. trueppm_api/apps/projects/migrations/0044_project_archive_program_close.py +119 -0
  334. trueppm_api/apps/projects/migrations/0045_program_risk_policy.py +78 -0
  335. trueppm_api/apps/projects/migrations/0046_merge_20260523_2248.py +12 -0
  336. trueppm_api/apps/projects/migrations/0047_apitokenauditentry_program_and_more.py +55 -0
  337. trueppm_api/apps/projects/migrations/0048_historicalprogram_color_program_color.py +32 -0
  338. trueppm_api/apps/projects/migrations/0049_backlog_item.py +112 -0
  339. trueppm_api/apps/projects/migrations/0050_backlog_item_trgm_search.py +26 -0
  340. trueppm_api/apps/projects/migrations/0051_alter_calendar_working_days_alter_dependency_lag_and_more.py +144 -0
  341. trueppm_api/apps/projects/migrations/0052_recurring_tasks.py +278 -0
  342. trueppm_api/apps/projects/migrations/0053_dependency_dep_pred_serverver_idx_and_more.py +56 -0
  343. trueppm_api/apps/projects/migrations/0054_sprintscopechange_goal_impact_and_more.py +117 -0
  344. trueppm_api/apps/projects/migrations/0055_historicaltask_sprint_pending_and_more.py +51 -0
  345. trueppm_api/apps/projects/migrations/0056_historicalproject_prioritization_model_and_more.py +276 -0
  346. trueppm_api/apps/projects/migrations/0057_historicalsprint_binding_committed_snapshot_and_more.py +58 -0
  347. trueppm_api/apps/projects/migrations/0058_historicaltask_delivery_mode_and_more.py +72 -0
  348. trueppm_api/apps/projects/migrations/0059_forecastsnapshot.py +80 -0
  349. trueppm_api/apps/projects/migrations/0060_historicalprojectsignalprivacypolicy_and_more.py +90 -0
  350. trueppm_api/apps/projects/migrations/0061_baselinetask_story_points.py +17 -0
  351. trueppm_api/apps/projects/migrations/0062_project_is_sample.py +22 -0
  352. trueppm_api/apps/projects/migrations/0063_historicalsprint_wip_limit_sprint_wip_limit.py +22 -0
  353. trueppm_api/apps/projects/migrations/0064_sprinttaskoutcome.py +97 -0
  354. trueppm_api/apps/projects/migrations/0065_historicalsprint_goal_outcome_sprint_goal_outcome.py +32 -0
  355. trueppm_api/apps/projects/migrations/0066_historicalproject_lead_project_lead.py +38 -0
  356. trueppm_api/apps/projects/migrations/0067_sprint_exclude_from_velocity.py +22 -0
  357. trueppm_api/apps/projects/migrations/0068_project_recalculated_at.py +23 -0
  358. trueppm_api/apps/projects/migrations/0069_iteration_label.py +22 -0
  359. trueppm_api/apps/projects/migrations/0070_historicalprogram_iteration_label_and_more.py +32 -0
  360. trueppm_api/apps/projects/migrations/0071_backfill_iteration_label_inherit.py +35 -0
  361. trueppm_api/apps/projects/migrations/0072_pulseresponse_retroboarditem.py +129 -0
  362. trueppm_api/apps/projects/migrations/0073_sprinttaskoutcome_demo_ready.py +17 -0
  363. trueppm_api/apps/projects/migrations/0074_historicaltask_blocked_reason_task_blocked_reason.py +30 -0
  364. trueppm_api/apps/projects/migrations/0075_risk_decimal_short_id.py +42 -0
  365. trueppm_api/apps/projects/migrations/0076_sprinttaskoutcome_review_polish.py +45 -0
  366. trueppm_api/apps/projects/migrations/0077_task_blocker_fields.py +111 -0
  367. trueppm_api/apps/projects/migrations/0078_alter_risktask_unique_together_and_more.py +22 -0
  368. trueppm_api/apps/projects/migrations/0079_historicalproject_status_date_project_status_date.py +22 -0
  369. trueppm_api/apps/projects/migrations/0080_alter_historicaltask_type_alter_task_type.py +46 -0
  370. trueppm_api/apps/projects/migrations/0081_historicalprogram_allow_guests_and_more.py +52 -0
  371. trueppm_api/apps/projects/migrations/0082_project_last_sync_version.py +90 -0
  372. trueppm_api/apps/projects/migrations/0083_historicalprogram_mc_history_attribution_audience_and_more.py +108 -0
  373. trueppm_api/apps/projects/migrations/0084_tasknote.py +71 -0
  374. trueppm_api/apps/projects/migrations/0085_sprint_ix_sprint_velocity.py +33 -0
  375. trueppm_api/apps/projects/migrations/0086_board_card_search_trgm.py +33 -0
  376. trueppm_api/apps/projects/migrations/0087_historicalprogram_task_duration_change_percent_policy_and_more.py +155 -0
  377. trueppm_api/apps/projects/migrations/0088_historicalprogram_allowed_attachment_types_and_more.py +61 -0
  378. trueppm_api/apps/projects/migrations/0089_signalceilingraiseproposal_signalceilingraisevote_and_more.py +157 -0
  379. trueppm_api/apps/projects/migrations/0090_historicaltask_proj_histdate_index.py +59 -0
  380. trueppm_api/apps/projects/migrations/0091_project_board_cadence.py +39 -0
  381. trueppm_api/apps/projects/migrations/0092_program_target_date.py +22 -0
  382. trueppm_api/apps/projects/migrations/0093_cross_project_dependency_consent.py +58 -0
  383. trueppm_api/apps/projects/migrations/0094_project_decisions_policy.py +90 -0
  384. trueppm_api/apps/projects/migrations/0095_poker_session_vote.py +127 -0
  385. trueppm_api/apps/projects/migrations/0095_squashed_0097_release_0_3.py +225 -0
  386. trueppm_api/apps/projects/migrations/0096_crossprojectslipconflict.py +91 -0
  387. trueppm_api/apps/projects/migrations/0097_backlogitem_backlogitem_tags_gin_and_more.py +32 -0
  388. trueppm_api/apps/projects/migrations/0098_historicalproject_show_baselines_and_more.py +52 -0
  389. trueppm_api/apps/projects/migrations/0099_apitoken_scopes.py +48 -0
  390. trueppm_api/apps/projects/migrations/0100_task_dep_serverver_idx_concurrent.py +75 -0
  391. trueppm_api/apps/projects/migrations/0101_dependency_deleted_at_task_deleted_at.py +22 -0
  392. trueppm_api/apps/projects/migrations/0102_project_deleted_at_project_proj_isdel_deletedat_idx.py +25 -0
  393. trueppm_api/apps/projects/migrations/0103_taskdurationchangeevent_task_dur_evt_sprint_idx.py +18 -0
  394. trueppm_api/apps/projects/migrations/0104_historicalproject_stale_task_threshold_days_and_more.py +32 -0
  395. trueppm_api/apps/projects/migrations/0104_trash_restore_1113.py +38 -0
  396. trueppm_api/apps/projects/migrations/0105_historical_changelog_indexes.py +76 -0
  397. trueppm_api/apps/projects/migrations/0106_boardsavedview_schema_version.py +20 -0
  398. trueppm_api/apps/projects/migrations/0106_squashed_0158_release_0_4.py +2357 -0
  399. trueppm_api/apps/projects/migrations/0107_task_activity_event.py +70 -0
  400. trueppm_api/apps/projects/migrations/0108_remove_apitoken_api_token_scope_xor_and_more.py +117 -0
  401. trueppm_api/apps/projects/migrations/0109_projectexportjob.py +82 -0
  402. trueppm_api/apps/projects/migrations/0110_sharelink.py +123 -0
  403. trueppm_api/apps/projects/migrations/0111_projectcalendarlayer.py +67 -0
  404. trueppm_api/apps/projects/migrations/0112_sharelink_expires_at_alter_sharelink_content_kind.py +31 -0
  405. trueppm_api/apps/projects/migrations/0113_alter_historicaltask_percent_complete_and_more.py +35 -0
  406. trueppm_api/apps/projects/migrations/0114_historicalproject_default_member_role_and_more.py +28 -0
  407. trueppm_api/apps/projects/migrations/0115_historicalproject_end_date_shift_threshold_days_and_more.py +22 -0
  408. trueppm_api/apps/projects/migrations/0116_alter_taskactivityevent_event_type.py +28 -0
  409. trueppm_api/apps/projects/migrations/0117_label_tasklabel_label_tasks_and_more.py +126 -0
  410. trueppm_api/apps/projects/migrations/0118_programexportjob.py +82 -0
  411. trueppm_api/apps/projects/migrations/0119_historicalprogram_calendar_program_calendar.py +36 -0
  412. trueppm_api/apps/projects/migrations/0120_alter_backlogitem_item_type.py +29 -0
  413. trueppm_api/apps/projects/migrations/0121_task_three_point_estimate_ordered.py +64 -0
  414. trueppm_api/apps/projects/migrations/0122_task_relation.py +173 -0
  415. trueppm_api/apps/projects/migrations/0123_remove_historicalproject_agile_features_and_more.py +20 -0
  416. trueppm_api/apps/projects/migrations/0124_dependency_is_driving.py +17 -0
  417. trueppm_api/apps/projects/migrations/0125_historicalprogram_estimation_scale_and_more.py +68 -0
  418. trueppm_api/apps/projects/migrations/0126_projectcustomfield_show_on_card_taskcustomfieldvalue.py +80 -0
  419. trueppm_api/apps/projects/migrations/0127_historicalprogram_mcp_enabled_and_more.py +32 -0
  420. trueppm_api/apps/projects/migrations/0128_acceptancecriterion_sync_seq_apitoken_sync_seq_and_more.py +192 -0
  421. trueppm_api/apps/projects/migrations/0129_sharelink_show_milestone_dates.py +20 -0
  422. trueppm_api/apps/projects/migrations/0130_programimportjob.py +85 -0
  423. trueppm_api/apps/projects/migrations/0131_historicalprogram_sprint_picker_ready_only_default_and_more.py +32 -0
  424. trueppm_api/apps/projects/migrations/0132_task_scheduled_start.py +17 -0
  425. trueppm_api/apps/projects/migrations/0133_project_status_date_floor_armed_at.py +17 -0
  426. trueppm_api/apps/projects/migrations/0134_task_edited_at_task_seeded_at_task_source_id_and_more.py +65 -0
  427. trueppm_api/apps/projects/migrations/0135_projecttemplate_templateapplication_and_more.py +175 -0
  428. trueppm_api/apps/projects/migrations/0136_schedule_outline_batch_operations.py +118 -0
  429. trueppm_api/apps/projects/migrations/0137_collapse_dual_scope_api_tokens.py +54 -0
  430. trueppm_api/apps/projects/migrations/0138_apitokenauditentry_api_token_audit_owner_idx.py +18 -0
  431. trueppm_api/apps/projects/migrations/0139_historicaltask_duration_unit_task_duration_unit.py +32 -0
  432. trueppm_api/apps/projects/migrations/0140_historicaltask_auto_container_and_more.py +113 -0
  433. trueppm_api/apps/projects/migrations/0141_historicalproject_draft_started_at_and_more.py +44 -0
  434. trueppm_api/apps/projects/migrations/0142_baseline_calendar_hours_per_day_and_more.py +31 -0
  435. trueppm_api/apps/projects/migrations/0143_sprintcloserequest_failure_reason_and_more.py +42 -0
  436. trueppm_api/apps/projects/migrations/0144_projecttemplate_source_project_and_more.py +35 -0
  437. trueppm_api/apps/projects/migrations/0145_seed_bundled_templates.py +65 -0
  438. trueppm_api/apps/projects/migrations/0146_historicaltask_board_lane_task_board_lane.py +32 -0
  439. trueppm_api/apps/projects/migrations/0147_structuraloperation.py +128 -0
  440. trueppm_api/apps/projects/migrations/0148_task_unique_task_wbs_path_per_project_live.py +76 -0
  441. trueppm_api/apps/projects/migrations/0149_drop_workshops_tables.py +42 -0
  442. trueppm_api/apps/projects/migrations/0150_clear_uncommitted_cpm_output.py +130 -0
  443. trueppm_api/apps/projects/migrations/0151_config_notice_request.py +70 -0
  444. trueppm_api/apps/projects/migrations/0152_aggregate_task_wbs_name_idx.py +20 -0
  445. trueppm_api/apps/projects/migrations/0153_program_sample_anchor_date.py +22 -0
  446. trueppm_api/apps/projects/migrations/0154_object_keys.py +184 -0
  447. trueppm_api/apps/projects/migrations/0155_task_milestone_at_day_end.py +20 -0
  448. trueppm_api/apps/projects/migrations/0156_program_import_job_replaced_project_ids.py +17 -0
  449. trueppm_api/apps/projects/migrations/0157_forecastsnapshot_cpm_finish_at_day_start.py +17 -0
  450. trueppm_api/apps/projects/migrations/0158_baselinetask_finish_at_day_start.py +17 -0
  451. trueppm_api/apps/projects/migrations/__init__.py +0 -0
  452. trueppm_api/apps/projects/models.py +9209 -0
  453. trueppm_api/apps/projects/poker_services.py +154 -0
  454. trueppm_api/apps/projects/poker_views.py +398 -0
  455. trueppm_api/apps/projects/product_backlog_services.py +450 -0
  456. trueppm_api/apps/projects/program_label_services.py +94 -0
  457. trueppm_api/apps/projects/program_label_views.py +189 -0
  458. trueppm_api/apps/projects/program_rollup.py +903 -0
  459. trueppm_api/apps/projects/program_schedule.py +767 -0
  460. trueppm_api/apps/projects/program_views.py +3445 -0
  461. trueppm_api/apps/projects/project_templates.py +422 -0
  462. trueppm_api/apps/projects/receivers.py +234 -0
  463. trueppm_api/apps/projects/refusal_codes.py +140 -0
  464. trueppm_api/apps/projects/reorder_services.py +145 -0
  465. trueppm_api/apps/projects/restructure_hooks.py +54 -0
  466. trueppm_api/apps/projects/retro_board_services.py +368 -0
  467. trueppm_api/apps/projects/retro_services.py +285 -0
  468. trueppm_api/apps/projects/risk_import.py +396 -0
  469. trueppm_api/apps/projects/row_identity.py +113 -0
  470. trueppm_api/apps/projects/schema_migrations.py +228 -0
  471. trueppm_api/apps/projects/schemas/seed_v1.json +492 -0
  472. trueppm_api/apps/projects/schemas/seed_v2.json +891 -0
  473. trueppm_api/apps/projects/seed/__init__.py +61 -0
  474. trueppm_api/apps/projects/seed/demo_landing_overlay.py +532 -0
  475. trueppm_api/apps/projects/seed/exporter.py +1717 -0
  476. trueppm_api/apps/projects/seed/forecast_backfill.py +247 -0
  477. trueppm_api/apps/projects/seed/importer.py +2764 -0
  478. trueppm_api/apps/projects/seed/reanchor.py +551 -0
  479. trueppm_api/apps/projects/seed/reldates.py +126 -0
  480. trueppm_api/apps/projects/seed/replace.py +128 -0
  481. trueppm_api/apps/projects/seed/replay.py +1695 -0
  482. trueppm_api/apps/projects/seed/replay_ctx.py +41 -0
  483. trueppm_api/apps/projects/seed/samples.py +427 -0
  484. trueppm_api/apps/projects/seed/validation.py +1169 -0
  485. trueppm_api/apps/projects/serializers.py +12904 -0
  486. trueppm_api/apps/projects/services.py +7525 -0
  487. trueppm_api/apps/projects/share_serializers.py +142 -0
  488. trueppm_api/apps/projects/share_services.py +373 -0
  489. trueppm_api/apps/projects/share_views.py +395 -0
  490. trueppm_api/apps/projects/sharing_settings.py +187 -0
  491. trueppm_api/apps/projects/signal_privacy_services.py +910 -0
  492. trueppm_api/apps/projects/signal_privacy_views.py +485 -0
  493. trueppm_api/apps/projects/signals.py +132 -0
  494. trueppm_api/apps/projects/slip_conflict.py +288 -0
  495. trueppm_api/apps/projects/sprint_cadence.py +453 -0
  496. trueppm_api/apps/projects/sprint_picker_settings.py +132 -0
  497. trueppm_api/apps/projects/standup.py +304 -0
  498. trueppm_api/apps/projects/standup_views.py +99 -0
  499. trueppm_api/apps/projects/structural_operation_services.py +727 -0
  500. trueppm_api/apps/projects/structural_operation_views.py +311 -0
  501. trueppm_api/apps/projects/surface_visibility.py +126 -0
  502. trueppm_api/apps/projects/task_batch_services.py +379 -0
  503. trueppm_api/apps/projects/task_bulk.py +1244 -0
  504. trueppm_api/apps/projects/task_classification.py +561 -0
  505. trueppm_api/apps/projects/task_classification_defaults.py +95 -0
  506. trueppm_api/apps/projects/task_duration_settings.py +142 -0
  507. trueppm_api/apps/projects/task_grouping.py +754 -0
  508. trueppm_api/apps/projects/tasks.py +1979 -0
  509. trueppm_api/apps/projects/template_divergence.py +168 -0
  510. trueppm_api/apps/projects/template_services.py +160 -0
  511. trueppm_api/apps/projects/template_tasks.py +261 -0
  512. trueppm_api/apps/projects/template_views.py +829 -0
  513. trueppm_api/apps/projects/throttles.py +311 -0
  514. trueppm_api/apps/projects/urls.py +1016 -0
  515. trueppm_api/apps/projects/utilization.py +909 -0
  516. trueppm_api/apps/projects/views.py +21225 -0
  517. trueppm_api/apps/projects/wbs_paths.py +119 -0
  518. trueppm_api/apps/resources/__init__.py +0 -0
  519. trueppm_api/apps/resources/apps.py +8 -0
  520. trueppm_api/apps/resources/capacity.py +121 -0
  521. trueppm_api/apps/resources/migrations/0001_initial.py +68 -0
  522. trueppm_api/apps/resources/migrations/0001_squashed_0013_alter_projectresource_unique_together_and_more.py +281 -0
  523. trueppm_api/apps/resources/migrations/0002_alter_uuid_pk_serialize_false.py +36 -0
  524. trueppm_api/apps/resources/migrations/0003_resource_soft_delete.py +30 -0
  525. trueppm_api/apps/resources/migrations/0004_add_taskresource_resource_index.py +25 -0
  526. trueppm_api/apps/resources/migrations/0005_resource_job_role.py +20 -0
  527. trueppm_api/apps/resources/migrations/0006_skill.py +34 -0
  528. trueppm_api/apps/resources/migrations/0007_resource_skill.py +63 -0
  529. trueppm_api/apps/resources/migrations/0008_project_resource.py +63 -0
  530. trueppm_api/apps/resources/migrations/0009_task_skill_requirement.py +60 -0
  531. trueppm_api/apps/resources/migrations/0010_alter_resourceskill_options_and_more.py +20 -0
  532. trueppm_api/apps/resources/migrations/0011_alter_projectresource_notes.py +17 -0
  533. trueppm_api/apps/resources/migrations/0012_add_resource_user_fk.py +26 -0
  534. trueppm_api/apps/resources/migrations/0013_alter_projectresource_unique_together_and_more.py +53 -0
  535. trueppm_api/apps/resources/migrations/0014_projectresource_sync_seq_resource_sync_seq_and_more.py +37 -0
  536. trueppm_api/apps/resources/migrations/0014_squashed_0015_release_0_4.py +47 -0
  537. trueppm_api/apps/resources/migrations/0015_projectresource_deactivated_with_resource.py +17 -0
  538. trueppm_api/apps/resources/migrations/__init__.py +0 -0
  539. trueppm_api/apps/resources/models.py +298 -0
  540. trueppm_api/apps/resources/serializers.py +426 -0
  541. trueppm_api/apps/resources/services.py +465 -0
  542. trueppm_api/apps/resources/urls.py +28 -0
  543. trueppm_api/apps/resources/views.py +1752 -0
  544. trueppm_api/apps/scheduling/__init__.py +0 -0
  545. trueppm_api/apps/scheduling/apps.py +153 -0
  546. trueppm_api/apps/scheduling/calendars.py +144 -0
  547. trueppm_api/apps/scheduling/deadletter.py +91 -0
  548. trueppm_api/apps/scheduling/finish_reading.py +198 -0
  549. trueppm_api/apps/scheduling/forecast_history_settings.py +215 -0
  550. trueppm_api/apps/scheduling/forecast_staleness.py +313 -0
  551. trueppm_api/apps/scheduling/graph_guard.py +263 -0
  552. trueppm_api/apps/scheduling/management/__init__.py +0 -0
  553. trueppm_api/apps/scheduling/management/commands/__init__.py +0 -0
  554. trueppm_api/apps/scheduling/management/commands/prune_forecast_snapshots.py +53 -0
  555. trueppm_api/apps/scheduling/mc_admission.py +181 -0
  556. trueppm_api/apps/scheduling/migrations/0001_failed_task.py +62 -0
  557. trueppm_api/apps/scheduling/migrations/0001_squashed_0007_projectforecastsnapshot.py +309 -0
  558. trueppm_api/apps/scheduling/migrations/0002_schedulerequest.py +78 -0
  559. trueppm_api/apps/scheduling/migrations/0003_add_schedule_request_reason.py +27 -0
  560. trueppm_api/apps/scheduling/migrations/0004_velocity_suggestion.py +94 -0
  561. trueppm_api/apps/scheduling/migrations/0005_montecarlorun.py +61 -0
  562. trueppm_api/apps/scheduling/migrations/0006_montecarlorun_distribution.py +17 -0
  563. trueppm_api/apps/scheduling/migrations/0007_projectforecastsnapshot.py +65 -0
  564. trueppm_api/apps/scheduling/migrations/0008_alter_schedulerequest_reason.py +28 -0
  565. trueppm_api/apps/scheduling/migrations/0008_folded_schedulerequest_reason.py +34 -0
  566. trueppm_api/apps/scheduling/migrations/0009_alter_schedulerequest_reason.py +29 -0
  567. trueppm_api/apps/scheduling/migrations/0010_failedtask_resolution_note_failedtask_resolved_at_and_more.py +43 -0
  568. trueppm_api/apps/scheduling/migrations/0010_squashed_0016_release_0_4.py +106 -0
  569. trueppm_api/apps/scheduling/migrations/0011_failedtask_project.py +26 -0
  570. trueppm_api/apps/scheduling/migrations/0012_montecarlorun_diagnostic.py +17 -0
  571. trueppm_api/apps/scheduling/migrations/0013_montecarlorun_status_date.py +17 -0
  572. trueppm_api/apps/scheduling/migrations/0014_montecarlorun_plan_version.py +17 -0
  573. trueppm_api/apps/scheduling/migrations/0015_projectforecastsnapshot_cpm_finish_at_day_start.py +17 -0
  574. trueppm_api/apps/scheduling/migrations/0016_montecarlorun_finish_readings.py +32 -0
  575. trueppm_api/apps/scheduling/migrations/__init__.py +0 -0
  576. trueppm_api/apps/scheduling/models.py +542 -0
  577. trueppm_api/apps/scheduling/receivers.py +160 -0
  578. trueppm_api/apps/scheduling/risk_premium.py +334 -0
  579. trueppm_api/apps/scheduling/serializers.py +723 -0
  580. trueppm_api/apps/scheduling/services.py +1251 -0
  581. trueppm_api/apps/scheduling/signals.py +56 -0
  582. trueppm_api/apps/scheduling/tasks.py +2248 -0
  583. trueppm_api/apps/scheduling/telemetry.py +80 -0
  584. trueppm_api/apps/scheduling/units.py +35 -0
  585. trueppm_api/apps/scheduling/urls.py +57 -0
  586. trueppm_api/apps/scheduling/views.py +2908 -0
  587. trueppm_api/apps/sso/__init__.py +0 -0
  588. trueppm_api/apps/sso/apps.py +19 -0
  589. trueppm_api/apps/sso/extensions.py +130 -0
  590. trueppm_api/apps/sso/management/__init__.py +0 -0
  591. trueppm_api/apps/sso/management/commands/__init__.py +0 -0
  592. trueppm_api/apps/sso/management/commands/seed_sso_keycloak.py +172 -0
  593. trueppm_api/apps/sso/migrations/0001_initial.py +119 -0
  594. trueppm_api/apps/sso/migrations/0002_remove_oidcprovider_workspace_ssoproviderpolicy_and_more.py +169 -0
  595. trueppm_api/apps/sso/migrations/__init__.py +0 -0
  596. trueppm_api/apps/sso/models.py +148 -0
  597. trueppm_api/apps/sso/serializers.py +446 -0
  598. trueppm_api/apps/sso/services.py +1473 -0
  599. trueppm_api/apps/sso/urls.py +47 -0
  600. trueppm_api/apps/sso/views.py +935 -0
  601. trueppm_api/apps/sync/__init__.py +0 -0
  602. trueppm_api/apps/sync/apps.py +15 -0
  603. trueppm_api/apps/sync/backfill.py +208 -0
  604. trueppm_api/apps/sync/broadcast.py +369 -0
  605. trueppm_api/apps/sync/conflict.py +262 -0
  606. trueppm_api/apps/sync/consumers.py +548 -0
  607. trueppm_api/apps/sync/migrations/0001_initial.py +59 -0
  608. trueppm_api/apps/sync/migrations/0001_squashed_0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +77 -0
  609. trueppm_api/apps/sync/migrations/0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +41 -0
  610. trueppm_api/apps/sync/migrations/0003_boardevent.py +46 -0
  611. trueppm_api/apps/sync/migrations/0003_squashed_0005_release_0_4.py +77 -0
  612. trueppm_api/apps/sync/migrations/0004_seed_sync_seq.py +24 -0
  613. trueppm_api/apps/sync/migrations/0005_program_sync_sequence.py +35 -0
  614. trueppm_api/apps/sync/migrations/0006_boardevent_per_project_seq.py +77 -0
  615. trueppm_api/apps/sync/migrations/__init__.py +0 -0
  616. trueppm_api/apps/sync/models.py +272 -0
  617. trueppm_api/apps/sync/pagination.py +231 -0
  618. trueppm_api/apps/sync/sequence.py +488 -0
  619. trueppm_api/apps/sync/serializers.py +688 -0
  620. trueppm_api/apps/sync/tasks.py +260 -0
  621. trueppm_api/apps/sync/throttles.py +175 -0
  622. trueppm_api/apps/sync/upload.py +556 -0
  623. trueppm_api/apps/sync/urls.py +17 -0
  624. trueppm_api/apps/sync/views.py +957 -0
  625. trueppm_api/apps/sync/ws_auth.py +193 -0
  626. trueppm_api/apps/taskruns/__init__.py +0 -0
  627. trueppm_api/apps/taskruns/apps.py +11 -0
  628. trueppm_api/apps/taskruns/migrations/0001_initial.py +92 -0
  629. trueppm_api/apps/taskruns/migrations/__init__.py +0 -0
  630. trueppm_api/apps/taskruns/models.py +68 -0
  631. trueppm_api/apps/taskruns/serializers.py +92 -0
  632. trueppm_api/apps/taskruns/tasks.py +60 -0
  633. trueppm_api/apps/taskruns/tracker.py +286 -0
  634. trueppm_api/apps/taskruns/urls.py +46 -0
  635. trueppm_api/apps/taskruns/views.py +195 -0
  636. trueppm_api/apps/teams/__init__.py +8 -0
  637. trueppm_api/apps/teams/apps.py +16 -0
  638. trueppm_api/apps/teams/migrations/0001_initial.py +139 -0
  639. trueppm_api/apps/teams/migrations/0001_squashed_0002_default_teams.py +159 -0
  640. trueppm_api/apps/teams/migrations/0002_default_teams.py +62 -0
  641. trueppm_api/apps/teams/migrations/0003_team_sync_seq_teammembership_sync_seq.py +22 -0
  642. trueppm_api/apps/teams/migrations/0004_backfill_default_teams_on_upgrade.py +49 -0
  643. trueppm_api/apps/teams/migrations/__init__.py +0 -0
  644. trueppm_api/apps/teams/models.py +145 -0
  645. trueppm_api/apps/teams/permissions.py +149 -0
  646. trueppm_api/apps/teams/serializers.py +95 -0
  647. trueppm_api/apps/teams/services.py +440 -0
  648. trueppm_api/apps/teams/signals.py +61 -0
  649. trueppm_api/apps/teams/urls.py +34 -0
  650. trueppm_api/apps/teams/views.py +201 -0
  651. trueppm_api/apps/timetracking/__init__.py +0 -0
  652. trueppm_api/apps/timetracking/apps.py +9 -0
  653. trueppm_api/apps/timetracking/migrations/0001_initial.py +117 -0
  654. trueppm_api/apps/timetracking/migrations/0001_squashed_0004_release_0_4.py +167 -0
  655. trueppm_api/apps/timetracking/migrations/0002_timesheetsubmission.py +46 -0
  656. trueppm_api/apps/timetracking/migrations/0003_timeentry_deleted_at_timeentry_deleted_by.py +31 -0
  657. trueppm_api/apps/timetracking/migrations/0004_timeentry_sync_seq_and_more.py +24 -0
  658. trueppm_api/apps/timetracking/migrations/__init__.py +0 -0
  659. trueppm_api/apps/timetracking/models.py +178 -0
  660. trueppm_api/apps/timetracking/serializers.py +175 -0
  661. trueppm_api/apps/timetracking/services.py +211 -0
  662. trueppm_api/apps/timetracking/urls.py +47 -0
  663. trueppm_api/apps/timetracking/views.py +597 -0
  664. trueppm_api/apps/webhooks/__init__.py +0 -0
  665. trueppm_api/apps/webhooks/apps.py +8 -0
  666. trueppm_api/apps/webhooks/backfill.py +90 -0
  667. trueppm_api/apps/webhooks/dispatch.py +279 -0
  668. trueppm_api/apps/webhooks/migrations/0001_initial.py +146 -0
  669. trueppm_api/apps/webhooks/migrations/0001_squashed_0007_sprint_lifecycle_events.py +249 -0
  670. trueppm_api/apps/webhooks/migrations/0002_alter_webhook_events.py +32 -0
  671. trueppm_api/apps/webhooks/migrations/0003_delivery_status_created_idx.py +17 -0
  672. trueppm_api/apps/webhooks/migrations/0004_webhook_program_alter_webhook_project_and_more.py +64 -0
  673. trueppm_api/apps/webhooks/migrations/0005_webhook_delivery_sequence_numbers.py +30 -0
  674. trueppm_api/apps/webhooks/migrations/0006_webhook_format_alter_webhook_events_and_more.py +61 -0
  675. trueppm_api/apps/webhooks/migrations/0007_sprint_lifecycle_events.py +63 -0
  676. trueppm_api/apps/webhooks/migrations/0008_risk_baseline_comment_events.py +73 -0
  677. trueppm_api/apps/webhooks/migrations/0009_encrypt_webhook_secret_and_failure_counters.py +100 -0
  678. trueppm_api/apps/webhooks/migrations/0010_alter_webhook_disabled_at.py +21 -0
  679. trueppm_api/apps/webhooks/migrations/0011_webhookeditor.py +48 -0
  680. trueppm_api/apps/webhooks/migrations/0012_alter_webhook_disabled_at.py +21 -0
  681. trueppm_api/apps/webhooks/migrations/__init__.py +0 -0
  682. trueppm_api/apps/webhooks/models.py +437 -0
  683. trueppm_api/apps/webhooks/serializers.py +326 -0
  684. trueppm_api/apps/webhooks/tasks.py +680 -0
  685. trueppm_api/apps/webhooks/urls.py +22 -0
  686. trueppm_api/apps/webhooks/views.py +533 -0
  687. trueppm_api/apps/workflow_engine/__init__.py +0 -0
  688. trueppm_api/apps/workflow_engine/apps.py +23 -0
  689. trueppm_api/apps/workflow_engine/migrations/0001_initial.py +230 -0
  690. trueppm_api/apps/workflow_engine/migrations/0001_squashed_0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +234 -0
  691. trueppm_api/apps/workflow_engine/migrations/0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +16 -0
  692. trueppm_api/apps/workflow_engine/migrations/0003_workflowoutboxrow_workflow_outbox_recovery_idx.py +21 -0
  693. trueppm_api/apps/workflow_engine/migrations/__init__.py +0 -0
  694. trueppm_api/apps/workflow_engine/models.py +265 -0
  695. trueppm_api/apps/workflow_engine/tasks.py +371 -0
  696. trueppm_api/apps/workspace/__init__.py +0 -0
  697. trueppm_api/apps/workspace/apps.py +10 -0
  698. trueppm_api/apps/workspace/backfill.py +111 -0
  699. trueppm_api/apps/workspace/export.py +254 -0
  700. trueppm_api/apps/workspace/migrations/0001_initial.py +326 -0
  701. trueppm_api/apps/workspace/migrations/0001_squashed_0014_workspacemembership_availability_effective_from_and_more.py +829 -0
  702. trueppm_api/apps/workspace/migrations/0002_remove_workspace_fiscal_year_start_and_more.py +85 -0
  703. trueppm_api/apps/workspace/migrations/0003_workspaceexportjob.py +73 -0
  704. trueppm_api/apps/workspace/migrations/0004_alter_workspaceinvite_status.py +27 -0
  705. trueppm_api/apps/workspace/migrations/0005_workspace_iteration_label_and_more.py +26 -0
  706. trueppm_api/apps/workspace/migrations/0006_alter_groupmembership_unique_together_and_more.py +45 -0
  707. trueppm_api/apps/workspace/migrations/0007_workspace_public_sharing_override_policy.py +21 -0
  708. trueppm_api/apps/workspace/migrations/0008_workspace_mc_history_attribution_audience_and_more.py +44 -0
  709. trueppm_api/apps/workspace/migrations/0009_workspace_methodology_and_more.py +186 -0
  710. trueppm_api/apps/workspace/migrations/0010_workspace_logo.py +38 -0
  711. trueppm_api/apps/workspace/migrations/0011_historicalworkspace_task_duration_change_percent_override_policy_and_more.py +56 -0
  712. trueppm_api/apps/workspace/migrations/0012_historicalworkspace_allowed_attachment_types_and_more.py +64 -0
  713. trueppm_api/apps/workspace/migrations/0013_auditevent.py +70 -0
  714. trueppm_api/apps/workspace/migrations/0014_workspacemembership_availability_effective_from_and_more.py +39 -0
  715. trueppm_api/apps/workspace/migrations/0015_squashed_0027_release_0_4.py +199 -0
  716. trueppm_api/apps/workspace/migrations/0015_trash_restore_1113.py +34 -0
  717. trueppm_api/apps/workspace/migrations/0016_historicalworkspace_calendar_and_more.py +55 -0
  718. trueppm_api/apps/workspace/migrations/0017_historicalworkspace_estimation_scale_and_more.py +38 -0
  719. trueppm_api/apps/workspace/migrations/0018_historicalworkspace_feedback_enabled_and_more.py +32 -0
  720. trueppm_api/apps/workspace/migrations/0019_historicalworkspace_mcp_enabled_and_more.py +22 -0
  721. trueppm_api/apps/workspace/migrations/0020_viewer_ordinal_one.py +34 -0
  722. trueppm_api/apps/workspace/migrations/0021_group_sync_seq_groupmembership_sync_seq_and_more.py +27 -0
  723. trueppm_api/apps/workspace/migrations/0022_historicalworkspace_sprint_picker_ready_only_default_and_more.py +22 -0
  724. trueppm_api/apps/workspace/migrations/0023_alter_auditevent_event_type.py +32 -0
  725. trueppm_api/apps/workspace/migrations/0024_alter_auditevent_event_type.py +33 -0
  726. trueppm_api/apps/workspace/migrations/0025_alter_auditevent_event_type.py +36 -0
  727. trueppm_api/apps/workspace/migrations/0026_alter_auditevent_event_type.py +37 -0
  728. trueppm_api/apps/workspace/migrations/0027_alter_auditevent_event_type.py +42 -0
  729. trueppm_api/apps/workspace/migrations/__init__.py +0 -0
  730. trueppm_api/apps/workspace/models.py +857 -0
  731. trueppm_api/apps/workspace/permissions.py +187 -0
  732. trueppm_api/apps/workspace/serializers.py +590 -0
  733. trueppm_api/apps/workspace/services.py +967 -0
  734. trueppm_api/apps/workspace/signals.py +37 -0
  735. trueppm_api/apps/workspace/tasks.py +616 -0
  736. trueppm_api/apps/workspace/urls.py +113 -0
  737. trueppm_api/apps/workspace/views.py +1581 -0
  738. trueppm_api/asgi.py +49 -0
  739. trueppm_api/celery.py +40 -0
  740. trueppm_api/core/__init__.py +0 -0
  741. trueppm_api/core/admin_site.py +206 -0
  742. trueppm_api/core/auth_views.py +934 -0
  743. trueppm_api/core/constant_time.py +51 -0
  744. trueppm_api/core/csp.py +54 -0
  745. trueppm_api/core/db.py +87 -0
  746. trueppm_api/core/db_session.py +37 -0
  747. trueppm_api/core/demo_read_only.py +314 -0
  748. trueppm_api/core/exception_handlers.py +157 -0
  749. trueppm_api/core/export_downloads.py +56 -0
  750. trueppm_api/core/extension_providers.py +56 -0
  751. trueppm_api/core/extension_signals.py +147 -0
  752. trueppm_api/core/idempotent.py +260 -0
  753. trueppm_api/core/metadata.py +42 -0
  754. trueppm_api/core/middleware.py +51 -0
  755. trueppm_api/core/openapi.py +1221 -0
  756. trueppm_api/core/password_reset.py +513 -0
  757. trueppm_api/core/protect_conflict.py +100 -0
  758. trueppm_api/core/ratelimit.py +148 -0
  759. trueppm_api/core/redis_throttle.py +82 -0
  760. trueppm_api/core/refresh_cookie_policy.py +156 -0
  761. trueppm_api/core/request_body.py +86 -0
  762. trueppm_api/core/security_checks.py +1034 -0
  763. trueppm_api/core/storage_config.py +105 -0
  764. trueppm_api/core/text_decode.py +168 -0
  765. trueppm_api/core/throttling.py +514 -0
  766. trueppm_api/core/valkey.py +214 -0
  767. trueppm_api/core/valkey_cache.py +66 -0
  768. trueppm_api/core/valkey_checks.py +104 -0
  769. trueppm_api/core/valkey_config.py +39 -0
  770. trueppm_api/core/worker_broker_wait.py +149 -0
  771. trueppm_api/core/worker_heartbeat.py +134 -0
  772. trueppm_api/core/worker_selfheal.py +205 -0
  773. trueppm_api/fields.py +36 -0
  774. trueppm_api/permissions.py +30 -0
  775. trueppm_api/py.typed +0 -0
  776. trueppm_api/routing.py +11 -0
  777. trueppm_api/settings/__init__.py +0 -0
  778. trueppm_api/settings/base.py +2605 -0
  779. trueppm_api/settings/dev.py +296 -0
  780. trueppm_api/settings/prod.py +265 -0
  781. trueppm_api/urls.py +217 -0
  782. trueppm_api/workflows/__init__.py +38 -0
  783. trueppm_api/workflows/backends/__init__.py +6 -0
  784. trueppm_api/workflows/backends/default.py +297 -0
  785. trueppm_api/workflows/consumers/__init__.py +11 -0
  786. trueppm_api/workflows/consumers/requeue_failed_task.py +65 -0
  787. trueppm_api/workflows/interface.py +148 -0
  788. trueppm_api/workflows/registry.py +106 -0
  789. trueppm_api/workflows/services.py +90 -0
  790. trueppm_api/wsgi.py +11 -0
  791. trueppm_api-0.4.0.dist-info/METADATA +273 -0
  792. trueppm_api-0.4.0.dist-info/RECORD +799 -0
  793. trueppm_api-0.4.0.dist-info/WHEEL +4 -0
  794. trueppm_api-0.4.0.dist-info/licenses/LICENSE +201 -0
  795. trueppm_api-0.4.0.dist-info/licenses/NOTICE +7 -0
  796. trueppm_api-0.4.0.dist-info/licenses/NOTICES +104 -0
  797. trueppm_api-0.4.0.dist-info/licenses/licenses/GPL-3.0.txt +674 -0
  798. trueppm_api-0.4.0.dist-info/licenses/licenses/LGPL-3.0.txt +165 -0
  799. trueppm_api-0.4.0.dist-info/licenses/licenses/MPL-2.0.txt +373 -0
@@ -0,0 +1,767 @@
1
+ """Program-scoped CPM read surface (#1117 / ADR-0120 D3 read side, enables D6).
2
+
3
+ Computes the **program-true** critical path across a program's member projects
4
+ *on read*: every member project's tasks and every accepted cross-project edge are
5
+ merged into one engine graph and the deterministic CPM runs once, so an upstream
6
+ task gating another project's milestone reports ``is_critical`` and floats against
7
+ the whole program — the honest cross-project critical path. This is the
8
+ render-don't-derive (ADR-0115) source the #1118 ``ProgramSchedulePage`` consumes;
9
+ no client-side program math.
10
+
11
+ It deliberately does **not** persist. There is no write-back to per-project
12
+ tasks, no ``schedule_lock:program:{id}``, and no outbox coalescing — those belong
13
+ to the deferred D3 *dispatch* pass (ADR-0120). Until that lands, per-project
14
+ surfaces keep their per-project CPM and this endpoint is the only program-true
15
+ view. Computing on read avoids the lock/coalesce/fan-out machinery entirely at
16
+ the cost of one CPM per request, which the ``MAX_PROGRAM_TASKS`` guard bounds.
17
+
18
+ Visibility (ADR-0120 D5): a requester gated in by ``IsProgramMember`` sees full
19
+ schedule rows for member projects they belong to and the redacted
20
+ ``ExternalTaskCard`` shape (title + CPM dates only, no description/assignee/
21
+ status) for member projects they cannot read — never a bare "blocked by
22
+ [redacted]".
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import re
28
+ from collections.abc import Callable, Collection
29
+ from dataclasses import dataclass, field
30
+ from datetime import timedelta
31
+ from typing import TYPE_CHECKING, Any, NoReturn
32
+
33
+ from rest_framework.exceptions import APIException
34
+
35
+ if TYPE_CHECKING:
36
+ from trueppm_api.apps.projects.models import Program
37
+
38
+ # Engine ``SchedulerError`` messages embed the offending task as ``Task '<uuid>'``
39
+ # (e.g. the #1069 three-point-ordering check). Extract the id to attribute the
40
+ # failure back to its member project for the #1981 structured 422; unmatched
41
+ # messages fall back to a project-less "some task" response.
42
+ _TASK_ID_RE = re.compile(
43
+ r"Task '([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-"
44
+ r"[0-9a-fA-F]{4}-[0-9a-fA-F]{12})'"
45
+ )
46
+
47
+ # Soft upper bound on the merged leaf-task count for an on-read CPM. A
48
+ # compute-on-read pass is O(tasks + edges); for a pathologically large program
49
+ # the persisted D3 dispatch pass (with caching) is the right path, so we fail
50
+ # loud with a 422 rather than serving a slow request. Chosen far above any
51
+ # realistic single program so it only fires on degenerate input.
52
+ MAX_PROGRAM_TASKS = 5000
53
+
54
+
55
+ class ProgramScheduleTooLarge(APIException):
56
+ """Raised when a program exceeds ``MAX_PROGRAM_TASKS`` for an on-read CPM."""
57
+
58
+ status_code = 422
59
+ default_detail = (
60
+ "This program has too many tasks for an on-read schedule computation; "
61
+ "the persisted program-schedule pass is required for programs this large."
62
+ )
63
+ default_code = "program_schedule_too_large"
64
+
65
+
66
+ class ProgramScheduleInvalidInput(APIException):
67
+ """Raised when a member project's task data makes the merged CPM unschedulable.
68
+
69
+ The program schedule is compute-on-read, so a single degenerate task in *any*
70
+ member project (invalid three-point estimate #1069, unknown-task dependency,
71
+ unresolvable calendar, cycle) would otherwise surface as an unhandled 500 that
72
+ blanks the whole cross-project view for every member (#1981). We instead map
73
+ the engine's ``SchedulerError`` to a structured, non-500 response that names
74
+ the offending project (and task where identifiable), so the frontend can point
75
+ the user at the project to fix rather than dead-ending on a retry that can
76
+ never succeed. The body carries ``code`` so the client distinguishes this from
77
+ the sibling 422 (``program_schedule_too_large``).
78
+ """
79
+
80
+ status_code = 422
81
+ default_code = "program_schedule_invalid_input"
82
+
83
+ def __init__(
84
+ self,
85
+ *,
86
+ reason: str | None = None,
87
+ project_id: str | None = None,
88
+ project_name: str | None = None,
89
+ task_id: str | None = None,
90
+ task_name: str | None = None,
91
+ ) -> None:
92
+ if project_name:
93
+ detail_msg = (
94
+ f"A task in “{project_name}” has data the schedule engine "
95
+ "cannot compute. Open that project's schedule to fix it."
96
+ )
97
+ else:
98
+ detail_msg = (
99
+ "A task in one of this program's projects has data the schedule "
100
+ "engine cannot compute. Open the offending project's schedule to "
101
+ "fix it."
102
+ )
103
+ # Only non-null keys are included: DRF coerces every scalar leaf of an
104
+ # APIException detail into an ``ErrorDetail`` string, so a literal ``None``
105
+ # would serialize as the string ``"None"`` rather than JSON ``null``.
106
+ # Omitting the key instead gives the client a clean "absent" (undefined).
107
+ body: dict[str, Any] = {"code": self.default_code, "detail": detail_msg}
108
+ if reason is not None:
109
+ # Carries the offending task's estimate values; present only for an
110
+ # accessible offending project (ADR-0120 D5 — see the raise site).
111
+ body["reason"] = reason
112
+ if project_id:
113
+ body["project"] = {"id": project_id, "name": project_name}
114
+ if task_id:
115
+ body["task"] = {"id": task_id, "name": task_name}
116
+ super().__init__(body)
117
+
118
+
119
+ @dataclass
120
+ class ProgramScheduleGraph:
121
+ """The merged program graph plus its program-true CPM result (ADR-0120 D3).
122
+
123
+ The single output of :func:`gather_program_schedule`, shared by the on-read
124
+ endpoint (:func:`compute_program_schedule`) and the persisted dispatch pass
125
+ (``scheduling.tasks.recalculate_program_schedule``) so the gather + merged CPM
126
+ runs through one code path and the two surfaces can never drift (the #1185
127
+ failure class). ``result``/``result_map`` are ``None``/empty when the program
128
+ has no schedulable work.
129
+
130
+ ``result_map`` holds the *leaf* CPM rows only (summary tasks are excluded from
131
+ the pass, exactly as the single-project engine does); the persisted write-back
132
+ rolls summary dates up separately via ``apply_summary_rollups``.
133
+ """
134
+
135
+ member_projects: list[Any]
136
+ project_by_id: dict[Any, Any]
137
+ db_task_by_id: dict[str, Any]
138
+ children_map: dict[str, list[str]]
139
+ db_deps: list[Any]
140
+ # The expanded *leaf-level* dependency edges the CPM actually ran on (summary
141
+ # edges resolved to their leaves). The D4 firewall walks this adjacency to
142
+ # attribute downstream slips to a cross-project edge. Empty on the no-work path.
143
+ expanded_deps: list[Any]
144
+ start_date: Any
145
+ result: Any | None
146
+ result_map: dict[str, Any]
147
+ leaf_ids: set[str]
148
+ # Composed engine calendar per member project, keyed by ``str(project.id)`` —
149
+ # the same map the merged ``SchedProject`` runs on (ADR-0120 D3). The persisted
150
+ # write-back needs it to express each summary's rolled-up span in that
151
+ # project's own working days (#3530); the on-read endpoint ignores it.
152
+ calendars: dict[str, Any] = field(default_factory=dict)
153
+
154
+ @property
155
+ def summary_ids(self) -> set[str]:
156
+ """Task ids that have ≥1 child in the merged WBS — the summary nodes."""
157
+ return set(self.children_map.keys())
158
+
159
+
160
+ def _schedulable_tasks(project: Any) -> list[Any]:
161
+ """The member project's committed tasks, replicating the single-project filter.
162
+
163
+ Drops recurring templates, EPIC grouping nodes, BACKLOG cards, and soft-deleted
164
+ tombstones — matching ``CommittedTaskManager`` (#1772). Anything else would put
165
+ work in the merged CPM that the single-project pass never schedules, and the two
166
+ surfaces would drift (the #1185 failure class).
167
+ """
168
+ from trueppm_api.apps.projects.models import TaskStatus, TaskType
169
+
170
+ return [
171
+ t
172
+ for t in project.tasks.all()
173
+ if not t.is_recurring
174
+ and t.type != TaskType.EPIC
175
+ and t.status != TaskStatus.BACKLOG
176
+ and not t.is_deleted
177
+ ]
178
+
179
+
180
+ def _project_sched_tasks(project: Any, db_tasks: list[Any], *, suggest_approve: bool) -> list[Any]:
181
+ """Convert one member project's tasks to engine tasks, tagged with its calendar.
182
+
183
+ The ``calendar_id`` tag is the ADR-0120 D3 engine substrate: the merged pass
184
+ holds one calendar per member project rather than a single program-wide one, so
185
+ each task's working-day math matches its own project's composed calendar. The
186
+ engine refuses an id missing from the registry with ``InvalidScheduleInput``
187
+ (#4516), so the caller must register ``calendars[str(project.id)]`` for every
188
+ project whose tasks it tags — the merged-pass loop does both in one iteration.
189
+ """
190
+ from trueppm_api.apps.scheduling.services import build_sched_tasks
191
+
192
+ sched_tasks = build_sched_tasks(db_tasks, suggest_approve=suggest_approve)
193
+ for st in sched_tasks:
194
+ st.calendar_id = str(project.id)
195
+ return sched_tasks
196
+
197
+
198
+ def _merge_children_map(db_tasks: list[Any], children_map: dict[str, list[str]]) -> None:
199
+ """Add ``db_tasks``' parent→children edges to the merged WBS map, in place.
200
+
201
+ Built per project from ``wbs_path`` then merged — keys are globally-unique task
202
+ ids, so no namespacing is needed, but the *paths* are only unique within a
203
+ project, which is why this merges one project at a time instead of calling
204
+ ``scheduling.services.build_children_map`` over the whole merged set.
205
+
206
+ It no longer "mirrors ``_run_schedule`` exactly" as this docstring used to claim:
207
+ since #3527 that function builds its map through ``build_children_map`` (the O(N)
208
+ index from #1011), while this is still the O(N^2) parent scan that predates it.
209
+ The two produce identical output — ``tests/apps/scheduling/test_children_map.py``
210
+ pins that equivalence — so nothing is wrong today; it is a fourth copy of one
211
+ rule, and collapsing it into ``build_children_map`` is follow-up work rather than
212
+ part of the forecast fix.
213
+ """
214
+ for t in db_tasks:
215
+ if not t.wbs_path:
216
+ continue
217
+ parent_path = str(t.wbs_path).rsplit(".", 1)
218
+ if len(parent_path) < 2:
219
+ continue
220
+ for candidate in db_tasks:
221
+ if candidate.wbs_path and str(candidate.wbs_path) == parent_path[0]:
222
+ children_map.setdefault(str(candidate.id), []).append(str(t.id))
223
+ break
224
+
225
+
226
+ def _raise_invalid_input(
227
+ exc: Exception,
228
+ *,
229
+ db_task_by_id: dict[str, Any],
230
+ project_by_id: dict[Any, Any],
231
+ can_access_project: Callable[[Any], bool] | None,
232
+ ) -> NoReturn:
233
+ """Map a merged-CPM ``SchedulerError`` to the structured 422 (#1981).
234
+
235
+ Compute-on-read means a single degenerate task in any member project would
236
+ otherwise become an unhandled 500 that blanks the whole program view. Best-effort
237
+ attribution: a cycle carries its task ids directly; other invalid inputs embed the
238
+ offending id in the message.
239
+
240
+ The raw ``reason`` embeds the offending task's estimate day-values, which ADR-0120
241
+ D5 withholds for member projects the requester cannot fully read. It is included
242
+ only when the offending project is both attributable AND accessible; otherwise the
243
+ client still gets the project/task identity (already public via lane metadata and
244
+ the redacted ExternalTaskCard) but not the withheld estimate values.
245
+ """
246
+ from trueppm_scheduler.engine import CyclicDependencyError
247
+
248
+ if isinstance(exc, CyclicDependencyError):
249
+ offending_ids = [tid for tid in exc.cycle if tid in db_task_by_id]
250
+ else:
251
+ offending_ids = [tid for tid in _TASK_ID_RE.findall(str(exc)) if tid in db_task_by_id]
252
+ db_task = db_task_by_id.get(offending_ids[0]) if offending_ids else None
253
+ project = project_by_id.get(db_task.project_id) if db_task is not None else None
254
+ reason_visible = (
255
+ project is not None and can_access_project is not None and can_access_project(project.id)
256
+ )
257
+ raise ProgramScheduleInvalidInput(
258
+ reason=str(exc) if reason_visible else None,
259
+ project_id=str(project.id) if project is not None else None,
260
+ project_name=project.name if project is not None else None,
261
+ task_id=str(db_task.id) if db_task is not None else None,
262
+ task_name=db_task.name if db_task is not None else None,
263
+ ) from exc
264
+
265
+
266
+ def gather_program_schedule(
267
+ program: Program,
268
+ *,
269
+ enforce_max: bool = True,
270
+ can_access_project: Callable[[Any], bool] | None = None,
271
+ exclude_project_ids: Collection[Any] | None = None,
272
+ ) -> ProgramScheduleGraph:
273
+ """Merge every member project's tasks + accepted cross edges and run CPM once.
274
+
275
+ The shared substrate of ADR-0120 D3: every member project's committed tasks
276
+ and every ACCEPTED cross-project dependency (both endpoints in member projects)
277
+ are merged into one engine ``Project`` — each task tagged with its own
278
+ project's calendar (the per-task-calendar substrate) — and the deterministic
279
+ CPM runs a single time, so floats and criticality are program-true across the
280
+ boundary.
281
+
282
+ Args:
283
+ program: The program whose member projects are merged.
284
+ enforce_max: When True (the on-read path), raise ``ProgramScheduleTooLarge``
285
+ above ``MAX_PROGRAM_TASKS`` rather than serving a slow request. The
286
+ persisted background pass passes False — it is allowed to be slow and a
287
+ 422 has no caller there.
288
+ can_access_project: Optional ``project_id -> bool`` predicate used only when
289
+ attributing a ``ProgramScheduleInvalidInput`` (#1981). The raw engine
290
+ ``reason`` string embeds the offending task's estimate day-values, which
291
+ ADR-0120 D5 withholds for member projects the requester cannot read; the
292
+ ``reason`` is therefore included only when the offending project is
293
+ accessible. Absent (the background path never raises the 422), the
294
+ reason is omitted.
295
+
296
+ Raises:
297
+ ProgramScheduleTooLarge: When ``enforce_max`` and the merged leaf-task
298
+ count exceeds ``MAX_PROGRAM_TASKS``.
299
+ ProgramScheduleInvalidInput: When ``enforce_max`` and a member project's
300
+ task data makes the merged CPM unschedulable.
301
+ """
302
+ from trueppm_scheduler.engine import (
303
+ SchedulerError,
304
+ expand_summary_dependencies,
305
+ schedule,
306
+ )
307
+ from trueppm_scheduler.models import Calendar as SchedCalendar
308
+ from trueppm_scheduler.models import Dependency as SchedDependency
309
+ from trueppm_scheduler.models import DependencyType
310
+ from trueppm_scheduler.models import Project as SchedProject
311
+
312
+ from trueppm_api.apps.projects.lifecycle import visible_projects
313
+ from trueppm_api.apps.projects.models import Dependency, EstimationMode, Project
314
+ from trueppm_api.apps.scheduling.calendars import compose_project_calendar
315
+
316
+ # Drafts are excluded (#2962/#3128), and this one is not obvious enough to
317
+ # leave implicit. It is tempting to argue a draft is harmless here because CPM
318
+ # on disjoint components is independent, so a draft with no accepted
319
+ # cross-project edge cannot reach a committed project. That argument is WRONG.
320
+ # `engine.schedule()` anchors the backward pass on a single
321
+ # `project_finish = max(t.early_finish for t in tasks)` across the WHOLE merged
322
+ # set, and `start_date` on `min(p.start_date ...)` above. So a draft that
323
+ # simply runs later than everything else raises `project_finish`, which
324
+ # inflates `total_float` for every committed task in the program and can flip
325
+ # `is_critical` off across the board — with no dependency between them at all.
326
+ # This is the rollup case in its sharpest form: a plan nobody agreed to
327
+ # silently rewriting the critical path a PM is managing to.
328
+ member_qs = visible_projects(Project.objects.filter(program=program, is_deleted=False))
329
+ if exclude_project_ids is not None:
330
+ # ADR-0678 (#2482): a project that opted out of agent reads is DROPPED from
331
+ # the merged graph, not redacted. The ExternalTaskCard redaction that
332
+ # ``can_access_project`` drives still leaks task titles and program-true CPM
333
+ # dates — acceptable for a project the caller merely lacks membership on,
334
+ # and NOT acceptable for one that explicitly withheld consent.
335
+ member_qs = member_qs.exclude(pk__in=exclude_project_ids)
336
+ member_projects = list(
337
+ member_qs.select_related("calendar", "program__calendar")
338
+ # calendar__exceptions + calendar_layers__calendar__exceptions:
339
+ # compose_project_calendar reads each member project's base calendar
340
+ # exceptions (#1491) AND its applied overlays (#906); prefetch both to
341
+ # avoid an N+1 across the whole program (this loop iterates every member).
342
+ # program__calendar__exceptions: a member project that sets no calendar of
343
+ # its own inherits the program calendar (ADR-0441), whose exceptions the
344
+ # resolver reads — prefetch to keep the whole-program pass N+1-free.
345
+ .prefetch_related(
346
+ "tasks",
347
+ "tasks__sprint",
348
+ "calendar__exceptions",
349
+ "calendar_layers__calendar__exceptions",
350
+ "program__calendar__exceptions",
351
+ )
352
+ .order_by("start_date", "name")
353
+ )
354
+ project_by_id = {p.id: p for p in member_projects}
355
+
356
+ # --- Per-project conversion, replicating the single-project _run_schedule
357
+ # filter (drop recurring, EPIC grouping nodes, BACKLOG cards, and soft-deleted
358
+ # tombstones — matching CommittedTaskManager, #1772) and tagging each task with
359
+ # its own project's calendar so the merged pass uses per-task calendars
360
+ # (ADR-0120 D3 engine substrate). Task ids are globally-unique UUIDs, so
361
+ # merging the per-project task lists needs no namespacing.
362
+ all_sched_tasks: list[Any] = []
363
+ all_children_map: dict[str, list[str]] = {}
364
+ calendars: dict[str, SchedCalendar] = {}
365
+ db_task_by_id: dict[str, Any] = {}
366
+ total_tasks = 0
367
+
368
+ for p in member_projects:
369
+ db_tasks = _schedulable_tasks(p)
370
+ if not db_tasks:
371
+ continue
372
+ total_tasks += len(db_tasks)
373
+ if enforce_max and total_tasks > MAX_PROGRAM_TASKS:
374
+ raise ProgramScheduleTooLarge
375
+
376
+ # Shared composer (#906/#1491): overlays each member project's base +
377
+ # applied calendars into one non-working mask, including every
378
+ # CalendarException holiday/shutdown range — so the merged program-scoped
379
+ # CPM pass honors the same composed calendar as the single-project pass.
380
+ calendars[str(p.id)] = compose_project_calendar(p)
381
+ all_sched_tasks.extend(
382
+ _project_sched_tasks(
383
+ p,
384
+ db_tasks,
385
+ suggest_approve=p.estimation_mode == EstimationMode.SUGGEST_APPROVE,
386
+ )
387
+ )
388
+ db_task_by_id.update({str(t.id): t for t in db_tasks})
389
+ _merge_children_map(db_tasks, all_children_map)
390
+
391
+ # --- Dependencies: every within-project edge plus every ACCEPTED cross-project
392
+ # edge whose endpoints both sit in member projects. Pending (unaccepted) cross
393
+ # edges are excluded — an unconsented edge is not yet a modeled constraint
394
+ # (ADR-0120 D2). Within-project edges are never pending, so the single
395
+ # ``exclude(pending_acceptance=True)`` keeps them all. Computed even on the
396
+ # empty path so the (cheap) query result is uniform; the empty graph carries it.
397
+ member_ids = list(project_by_id.keys())
398
+ db_deps = list(
399
+ Dependency.live.filter(
400
+ predecessor__project_id__in=member_ids,
401
+ successor__project_id__in=member_ids,
402
+ )
403
+ .exclude(pending_acceptance=True)
404
+ .select_related("predecessor", "successor")
405
+ )
406
+
407
+ if not all_sched_tasks:
408
+ return ProgramScheduleGraph(
409
+ member_projects=member_projects,
410
+ project_by_id=project_by_id,
411
+ db_task_by_id=db_task_by_id,
412
+ children_map=all_children_map,
413
+ db_deps=db_deps,
414
+ expanded_deps=[],
415
+ start_date=None,
416
+ result=None,
417
+ result_map={},
418
+ leaf_ids=set(),
419
+ calendars=calendars,
420
+ )
421
+
422
+ included_ids = set(db_task_by_id.keys())
423
+ sched_deps = [
424
+ SchedDependency(
425
+ predecessor_id=str(d.predecessor_id),
426
+ successor_id=str(d.successor_id),
427
+ dep_type=DependencyType(d.dep_type),
428
+ lag=timedelta(days=d.lag),
429
+ )
430
+ for d in db_deps
431
+ if str(d.predecessor_id) in included_ids and str(d.successor_id) in included_ids
432
+ ]
433
+
434
+ start_date = min(p.start_date for p in member_projects)
435
+
436
+ try:
437
+ # Expansion sits inside the try with the CPM pass: it raises
438
+ # ``InvalidScheduleInput`` on ordinary member-project data (SS/SF from a
439
+ # summary, the cross-product bound, two links of different types resolving
440
+ # to one leaf pair — #4434), and outside this block that reached the client
441
+ # as an unhandled 500 instead of the structured 422 below.
442
+ leaf_tasks, expanded_deps = expand_summary_dependencies(
443
+ all_sched_tasks, sched_deps, all_children_map
444
+ )
445
+ merged = SchedProject(
446
+ id=str(program.pk),
447
+ name=program.name,
448
+ start_date=start_date,
449
+ tasks=leaf_tasks,
450
+ dependencies=expanded_deps,
451
+ # Per-task calendar_id drives each task's working-day math; this default
452
+ # is only a fallback for tasks with no calendar_id (none here).
453
+ calendar=SchedCalendar(),
454
+ calendars=calendars,
455
+ # Earliest-possible dates: per-project status_dates differ across the
456
+ # program, so a single merged data date would be arbitrary. The program
457
+ # forecast view shows the earliest honest schedule (ADR-0132 parity with
458
+ # a null status_date), not a data-date floor.
459
+ status_date=None,
460
+ )
461
+ result = schedule(merged)
462
+ except SchedulerError as exc:
463
+ # Only the on-read HTTP path (``enforce_max``) converts a degenerate-data
464
+ # failure into a structured response. The persisted background pass
465
+ # (``enforce_max=False``) must let the original ``SchedulerError``
466
+ # propagate to its ``TaskRunTracker``, which marks the run FAILED — a DRF
467
+ # ``APIException`` has no meaning in a Celery context and would corrupt
468
+ # that telemetry.
469
+ if not enforce_max:
470
+ raise
471
+ _raise_invalid_input(
472
+ exc,
473
+ db_task_by_id=db_task_by_id,
474
+ project_by_id=project_by_id,
475
+ can_access_project=can_access_project,
476
+ )
477
+ result_map = {st.id: st for st in result.tasks}
478
+ leaf_ids = set(result_map.keys())
479
+
480
+ return ProgramScheduleGraph(
481
+ member_projects=member_projects,
482
+ project_by_id=project_by_id,
483
+ db_task_by_id=db_task_by_id,
484
+ children_map=all_children_map,
485
+ db_deps=db_deps,
486
+ expanded_deps=expanded_deps,
487
+ start_date=start_date,
488
+ result=result,
489
+ result_map=result_map,
490
+ leaf_ids=leaf_ids,
491
+ calendars=calendars,
492
+ )
493
+
494
+
495
+ def _lane_working_day_durations(graph: ProgramScheduleGraph) -> dict[str, int]:
496
+ """Working-day span of each member project's lane box (#3597).
497
+
498
+ A lane box is the synthetic per-project rollup the #1118 program-schedule
499
+ view draws under each project — the client's ``laneSummaryTask`` spans it
500
+ over ``[min(early_start), max(early_finish)]`` across every task in that
501
+ project. Before #3597 that box's *displayed* duration was a raw inclusive
502
+ calendar-day count derived client-side (the same unit mistake #3530 fixed
503
+ for summary rows): correct here means the same working-day count, against
504
+ that project's own composed calendar, that ``summary_working_day_durations``
505
+ (#3530) already computes for a single project's WBS summary rows — a lane
506
+ is the same rollup shape, one level up, over the *whole* project rather
507
+ than one WBS subtree.
508
+
509
+ Spans every task in the lane (full or ADR-0120 D5 externally-redacted) —
510
+ the rollup box is drawn regardless of the caller's own per-project
511
+ visibility, matching the pre-#3597 client computation exactly.
512
+
513
+ Args:
514
+ graph: The merged program graph from :func:`gather_program_schedule`.
515
+ ``result_map``/``calendars`` are empty on the no-schedulable-work
516
+ path, so this returns ``{}`` and every lane falls back to 0.
517
+
518
+ Returns:
519
+ ``{project_id: working_day_duration}``, omitting any project with no
520
+ scheduled leaf task or no composed calendar.
521
+ """
522
+ from trueppm_scheduler.engine import _WorkingDayCounter
523
+
524
+ ranges: dict[str, tuple[Any, Any]] = {}
525
+ for tid, sched in graph.result_map.items():
526
+ db_task = graph.db_task_by_id.get(tid)
527
+ if db_task is None or sched.early_start is None or sched.early_finish is None:
528
+ continue
529
+ project_key = str(db_task.project_id)
530
+ lo, hi = ranges.get(project_key, (sched.early_start, sched.early_finish))
531
+ ranges[project_key] = (min(lo, sched.early_start), max(hi, sched.early_finish))
532
+
533
+ durations: dict[str, int] = {}
534
+ for project_key, (lo, hi) in ranges.items():
535
+ cal = graph.calendars.get(project_key)
536
+ if cal is None:
537
+ continue
538
+ counter = _WorkingDayCounter.build(lo, hi, cal)
539
+ # Same half-open-plus-conditional-finish correction as
540
+ # `summary_working_day_durations` (#3530): `between` counts
541
+ # `[lo, hi)`, and the finish day is added back only when it is itself
542
+ # a working day.
543
+ working = counter.between(lo, hi)
544
+ if cal.is_working_day(hi):
545
+ working += 1
546
+ durations[project_key] = max(1, working)
547
+ return durations
548
+
549
+
550
+ def program_has_accepted_cross_edges(program_id: Any) -> bool:
551
+ """Whether ``program_id`` has ≥1 accepted cross-project dependency (ADR-0120 D3).
552
+
553
+ The escalation predicate: a project's CPM recompute escalates to a merged,
554
+ program-scoped pass only when this is True. Cheap and index-friendly — an
555
+ EXISTS over the program's accepted (non-pending, non-deleted) edges whose two
556
+ endpoints sit in two *different* member projects. Cross-PROGRAM edges can't
557
+ match (both endpoints are required to share this ``program_id``), so the
558
+ ADR-0070 boundary is preserved here too.
559
+ """
560
+ from django.db.models import F
561
+
562
+ from trueppm_api.apps.projects.models import Dependency
563
+
564
+ return (
565
+ Dependency.live.filter(
566
+ pending_acceptance=False,
567
+ predecessor__project__program_id=program_id,
568
+ successor__project__program_id=program_id,
569
+ )
570
+ .exclude(predecessor__project_id=F("successor__project_id"))
571
+ .exists()
572
+ )
573
+
574
+
575
+ def compute_program_schedule(
576
+ program: Program,
577
+ *,
578
+ can_access_project: Callable[[Any], bool],
579
+ exclude_project_ids: Collection[Any] | None = None,
580
+ can_subscribe_live: Callable[[Any], bool] | None = None,
581
+ ) -> dict[str, Any]:
582
+ """Compute the merged, program-true schedule for ``program`` on read.
583
+
584
+ Args:
585
+ program: The program whose member projects are merged into one CPM graph.
586
+ can_access_project: Predicate ``project_id -> bool`` deciding whether the
587
+ requester may read a member project's tasks in full. Tasks of projects
588
+ for which this returns ``False`` are redacted to the ExternalTaskCard
589
+ shape. Injected by the view so this service stays request-agnostic.
590
+ can_subscribe_live: Optional predicate ``project_id -> bool`` — whether the
591
+ requester's role on a member project clears the project WebSocket's
592
+ floor (``role_can_subscribe_live``). Emitted per lane so the program
593
+ page never dials a member project's socket that would refuse it with
594
+ 4003 (#4375). ``None`` reports ``False`` for every lane (fails closed;
595
+ the client then simply does not subscribe).
596
+
597
+ Returns:
598
+ A JSON-serializable dict: ``program_id``, ``start_date``, ``finish_date``,
599
+ ``projects`` (lane metadata with per-project ``accessible`` and the lane's
600
+ rolled-up ``duration`` in working days, #3597), ``tasks`` (full — including
601
+ each task's own working-day ``duration`` — or redacted, discriminated by
602
+ ``is_external``), ``links`` (leaf-level edges with ``is_cross_project``),
603
+ ``critical_path`` (program-true task-id order), and
604
+ ``cross_project_edge_count``.
605
+
606
+ Raises:
607
+ ProgramScheduleTooLarge: When the merged leaf-task count exceeds the guard.
608
+ ProgramScheduleInvalidInput: When a member project's task data makes the
609
+ merged CPM unschedulable (#1981); the engine ``reason`` is included only
610
+ for an accessible offending project (ADR-0120 D5).
611
+ """
612
+ graph = gather_program_schedule(
613
+ program,
614
+ enforce_max=True,
615
+ can_access_project=can_access_project,
616
+ exclude_project_ids=exclude_project_ids,
617
+ )
618
+
619
+ # Lane metadata is independent of whether the program has any schedulable
620
+ # work, so build it from the member set and return it even on the empty path.
621
+ # Project has no accent color (only Program/Task do), so lane colors are
622
+ # assigned client-side by the #1118 Gantt; the lane metadata carries identity,
623
+ # the per-project access flag, and the lane's own rolled-up duration.
624
+ lane_durations = _lane_working_day_durations(graph)
625
+ lanes = [
626
+ {
627
+ "id": str(p.id),
628
+ "name": p.name,
629
+ "accessible": bool(can_access_project(p.id)),
630
+ "can_subscribe_live": bool(can_subscribe_live and can_subscribe_live(p.id)),
631
+ # Working days across the lane's own rolled-up span (#3597) — 0 for a
632
+ # project with no scheduled task, matching the pre-fix client fallback.
633
+ "duration": lane_durations.get(str(p.id), 0),
634
+ }
635
+ for p in graph.member_projects
636
+ ]
637
+
638
+ if graph.result is None:
639
+ return {
640
+ "program_id": str(program.pk),
641
+ "start_date": None,
642
+ "finish_date": None,
643
+ "projects": lanes,
644
+ "tasks": [],
645
+ "links": [],
646
+ "critical_path": [],
647
+ "cross_project_edge_count": 0,
648
+ }
649
+
650
+ result = graph.result
651
+ result_by_id = graph.result_map
652
+ leaf_ids = graph.leaf_ids
653
+ db_task_by_id = graph.db_task_by_id
654
+ project_by_id = graph.project_by_id
655
+
656
+ tasks_payload = [
657
+ _task_payload(tid, st, db_task_by_id[tid], project_by_id, can_access_project)
658
+ for tid, st in result_by_id.items()
659
+ if tid in db_task_by_id
660
+ ]
661
+
662
+ links_payload = []
663
+ cross_count = 0
664
+ for d in graph.db_deps:
665
+ pid = str(d.predecessor_id)
666
+ sid = str(d.successor_id)
667
+ if pid not in leaf_ids or sid not in leaf_ids:
668
+ # Edge touches a summary task (expanded away) or an excluded task —
669
+ # not a renderable leaf-level link.
670
+ continue
671
+ is_cross = d.predecessor.project_id != d.successor.project_id
672
+ if is_cross:
673
+ cross_count += 1
674
+ links_payload.append(
675
+ {
676
+ "predecessor_id": pid,
677
+ "successor_id": sid,
678
+ "dep_type": d.dep_type,
679
+ "lag_days": d.lag,
680
+ "is_cross_project": is_cross,
681
+ }
682
+ )
683
+
684
+ return {
685
+ "program_id": str(program.pk),
686
+ "start_date": result.project_start,
687
+ "finish_date": result.project_finish,
688
+ "projects": lanes,
689
+ "tasks": tasks_payload,
690
+ "links": links_payload,
691
+ "critical_path": list(result.critical_path),
692
+ "cross_project_edge_count": cross_count,
693
+ }
694
+
695
+
696
+ def _task_payload(
697
+ tid: str,
698
+ sched_task: Any,
699
+ db_task: Any,
700
+ project_by_id: dict[Any, Any],
701
+ can_access_project: Callable[[Any], bool],
702
+ ) -> dict[str, Any]:
703
+ """Build one task row — full when the requester can read the project, else the
704
+ ADR-0120 D5 ExternalTaskCard shape (title + CPM dates only). The CPM values
705
+ come from the *merged* result, so a redacted card still shows program-true
706
+ early dates and criticality, never the stale per-project numbers.
707
+
708
+ ``duration`` (full branch only, #3597) is read straight off the ``Task`` row,
709
+ not derived from the merged dates: a leaf task's ``duration`` is its own
710
+ working-day estimate, an *input* to CPM rather than an output of it, so it is
711
+ identical whether the row was last scheduled by its own project's
712
+ single-project pass or by this merged one — reading it here is the same value
713
+ the project schedule shows for "the same task" (the #3597 acceptance bar),
714
+ with nothing to recompute. Summary/WBS-parent tasks never reach this
715
+ function — ``gather_program_schedule`` excludes them from the merged leaf set
716
+ (docstring on ``ProgramScheduleGraph.result_map``) — so there is no working-day
717
+ rollup to do here the way ``_apply_cpm_results`` does for a summary row.
718
+ """
719
+ project = project_by_id.get(db_task.project_id)
720
+ if can_access_project(db_task.project_id):
721
+ return {
722
+ "id": tid,
723
+ "name": db_task.name,
724
+ "hex_id": db_task.short_id,
725
+ "project_id": str(db_task.project_id),
726
+ "is_milestone": db_task.is_milestone,
727
+ "is_external": False,
728
+ "wbs_path": str(db_task.wbs_path) if db_task.wbs_path else None,
729
+ "early_start": sched_task.early_start,
730
+ "early_finish": sched_task.early_finish,
731
+ "late_start": sched_task.late_start,
732
+ "late_finish": sched_task.late_finish,
733
+ "total_float_days": sched_task.total_float.days,
734
+ "is_critical": sched_task.is_critical,
735
+ "duration": db_task.duration,
736
+ # #4079: which edge of its shown day a milestone sits on, from the
737
+ # same merged pass as the dates above.
738
+ "milestone_at_day_end": bool(getattr(sched_task, "milestone_at_day_end", False)),
739
+ }
740
+ # Redacted — mirrors ExternalTaskCardSerializer's field set exactly so the two
741
+ # cross-project read surfaces stay in lockstep. The text key here is ``title``
742
+ # (NOT ``name`` as in the full branch above) deliberately: that is the
743
+ # ExternalTaskCard contract D5 already ships. Do not "align" the two branches
744
+ # to a single key — the client discriminates the redacted shape by it, and the
745
+ # divergence is the contract, not an oversight. ``duration`` is deliberately
746
+ # NOT added here: ExternalTaskCardSerializer has no such field, and D5's own
747
+ # withholding list is scoped to non-schedule facts (description, assignee,
748
+ # status, points) — widening it is a redaction-policy decision this fix does
749
+ # not make. The redacted card keeps its pre-#3597 client-derived approximation.
750
+ # ``milestone_at_day_end`` IS on the card (#4079): it is a schedule fact about
751
+ # a date the card already discloses, not one of D5's withheld facts.
752
+ return {
753
+ "id": tid,
754
+ "title": db_task.name,
755
+ "hex_id": db_task.short_id,
756
+ "project_id": str(db_task.project_id),
757
+ "project_name": project.name if project else "",
758
+ "is_milestone": db_task.is_milestone,
759
+ "is_external": True,
760
+ "early_start": sched_task.early_start,
761
+ "early_finish": sched_task.early_finish,
762
+ "is_critical": sched_task.is_critical,
763
+ # #4079: which edge of the already-disclosed early_start day a milestone
764
+ # sits on. It reveals nothing the dates do not — only where on that day
765
+ # to draw the diamond — so the card carries it like the dates themselves.
766
+ "milestone_at_day_end": bool(getattr(sched_task, "milestone_at_day_end", False)),
767
+ }