trueppm-api 0.4.0b2__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 (759) hide show
  1. trueppm_api/__init__.py +14 -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 +164 -0
  8. trueppm_api/apps/access/management/commands/migrate_locked.py +96 -0
  9. trueppm_api/apps/access/management/commands/revoke_api_tokens.py +209 -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 +167 -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_userdefinedmentiongroup.py +82 -0
  24. trueppm_api/apps/access/migrations/0014_programmembership_progm_serverver_idx.py +19 -0
  25. trueppm_api/apps/access/migrations/0015_programuserdefinedmentiongroup.py +86 -0
  26. trueppm_api/apps/access/migrations/0016_externalstakeholder.py +67 -0
  27. trueppm_api/apps/access/migrations/0017_viewer_ordinal_one.py +132 -0
  28. trueppm_api/apps/access/migrations/0018_programmembership_sync_seq_and_more.py +52 -0
  29. trueppm_api/apps/access/migrations/0019_programmembership_reinstated_at_and_more.py +22 -0
  30. trueppm_api/apps/access/migrations/__init__.py +0 -0
  31. trueppm_api/apps/access/models.py +588 -0
  32. trueppm_api/apps/access/permissions.py +2837 -0
  33. trueppm_api/apps/access/serializers.py +946 -0
  34. trueppm_api/apps/access/services.py +667 -0
  35. trueppm_api/apps/access/signals.py +103 -0
  36. trueppm_api/apps/access/tasks.py +50 -0
  37. trueppm_api/apps/access/throttles.py +95 -0
  38. trueppm_api/apps/access/urls.py +191 -0
  39. trueppm_api/apps/access/views.py +1931 -0
  40. trueppm_api/apps/agents/__init__.py +6 -0
  41. trueppm_api/apps/agents/apps.py +11 -0
  42. trueppm_api/apps/agents/canonical.py +73 -0
  43. trueppm_api/apps/agents/deferred.py +108 -0
  44. trueppm_api/apps/agents/management/__init__.py +0 -0
  45. trueppm_api/apps/agents/management/commands/__init__.py +0 -0
  46. trueppm_api/apps/agents/management/commands/audit_prune.py +137 -0
  47. trueppm_api/apps/agents/management/commands/audit_verify.py +101 -0
  48. trueppm_api/apps/agents/middleware.py +44 -0
  49. trueppm_api/apps/agents/migrations/0001_initial.py +231 -0
  50. trueppm_api/apps/agents/migrations/0002_agentactioncheckpoint.py +71 -0
  51. trueppm_api/apps/agents/migrations/0003_agentactionrefusaldetail.py +58 -0
  52. trueppm_api/apps/agents/migrations/__init__.py +0 -0
  53. trueppm_api/apps/agents/models.py +371 -0
  54. trueppm_api/apps/agents/refusal.py +103 -0
  55. trueppm_api/apps/agents/serializers.py +85 -0
  56. trueppm_api/apps/agents/services.py +397 -0
  57. trueppm_api/apps/agents/signals.py +30 -0
  58. trueppm_api/apps/agents/urls.py +15 -0
  59. trueppm_api/apps/agents/views.py +108 -0
  60. trueppm_api/apps/csvimport/__init__.py +12 -0
  61. trueppm_api/apps/csvimport/apps.py +11 -0
  62. trueppm_api/apps/csvimport/mapping.py +363 -0
  63. trueppm_api/apps/csvimport/migrations/0001_initial.py +74 -0
  64. trueppm_api/apps/csvimport/migrations/0002_schedule_outline_batch_operations.py +37 -0
  65. trueppm_api/apps/csvimport/migrations/0003_csvimportrequest_date_order_and_more.py +26 -0
  66. trueppm_api/apps/csvimport/migrations/__init__.py +0 -0
  67. trueppm_api/apps/csvimport/models.py +111 -0
  68. trueppm_api/apps/csvimport/parser.py +1825 -0
  69. trueppm_api/apps/csvimport/services.py +48 -0
  70. trueppm_api/apps/csvimport/tasks.py +418 -0
  71. trueppm_api/apps/csvimport/template.py +37 -0
  72. trueppm_api/apps/csvimport/urls.py +50 -0
  73. trueppm_api/apps/csvimport/views.py +793 -0
  74. trueppm_api/apps/history/__init__.py +0 -0
  75. trueppm_api/apps/history/apps.py +12 -0
  76. trueppm_api/apps/history/changelog.py +359 -0
  77. trueppm_api/apps/history/diff_policy.py +177 -0
  78. trueppm_api/apps/history/serializers.py +104 -0
  79. trueppm_api/apps/history/signals.py +82 -0
  80. trueppm_api/apps/history/tasks.py +105 -0
  81. trueppm_api/apps/history/urls.py +38 -0
  82. trueppm_api/apps/history/views.py +443 -0
  83. trueppm_api/apps/idempotency/__init__.py +0 -0
  84. trueppm_api/apps/idempotency/apps.py +11 -0
  85. trueppm_api/apps/idempotency/migrations/0001_initial.py +71 -0
  86. trueppm_api/apps/idempotency/migrations/__init__.py +0 -0
  87. trueppm_api/apps/idempotency/mixins.py +226 -0
  88. trueppm_api/apps/idempotency/models.py +75 -0
  89. trueppm_api/apps/idempotency/tasks.py +61 -0
  90. trueppm_api/apps/integrations/__init__.py +13 -0
  91. trueppm_api/apps/integrations/apps.py +59 -0
  92. trueppm_api/apps/integrations/connections.py +806 -0
  93. trueppm_api/apps/integrations/encryption.py +92 -0
  94. trueppm_api/apps/integrations/external_sources.py +837 -0
  95. trueppm_api/apps/integrations/git_automation_services.py +231 -0
  96. trueppm_api/apps/integrations/git_webhook_auth.py +243 -0
  97. trueppm_api/apps/integrations/http.py +403 -0
  98. trueppm_api/apps/integrations/me_work.py +218 -0
  99. trueppm_api/apps/integrations/migrations/0001_initial.py +64 -0
  100. trueppm_api/apps/integrations/migrations/0001_squashed_0006_tasklink_description_tasklink_preview_type_and_more.py +187 -0
  101. trueppm_api/apps/integrations/migrations/0002_tasklink.py +67 -0
  102. trueppm_api/apps/integrations/migrations/0003_tasklink_tasklink_serverver_idx.py +17 -0
  103. trueppm_api/apps/integrations/migrations/0004_tasklink_custom_title_tasklink_labels.py +25 -0
  104. trueppm_api/apps/integrations/migrations/0005_boardautomation.py +56 -0
  105. trueppm_api/apps/integrations/migrations/0006_tasklink_description_tasklink_preview_type_and_more.py +40 -0
  106. trueppm_api/apps/integrations/migrations/0007_tasklink_created_at.py +23 -0
  107. trueppm_api/apps/integrations/migrations/0008_integrationcredential_config_externalworkitem.py +66 -0
  108. trueppm_api/apps/integrations/migrations/0009_externalsyncrequest_and_more.py +96 -0
  109. trueppm_api/apps/integrations/migrations/0010_externalworkitem_due_date.py +17 -0
  110. trueppm_api/apps/integrations/migrations/0011_tasklink_sync_seq_tasklink_tasklink_syncseq_idx.py +22 -0
  111. trueppm_api/apps/integrations/migrations/0012_boardautomation_last_delivery_at_and_more.py +42 -0
  112. trueppm_api/apps/integrations/migrations/0013_externalsyncrequest_attempt_count_and_more.py +26 -0
  113. trueppm_api/apps/integrations/migrations/__init__.py +0 -0
  114. trueppm_api/apps/integrations/models.py +622 -0
  115. trueppm_api/apps/integrations/notification_channels.py +57 -0
  116. trueppm_api/apps/integrations/opengraph.py +229 -0
  117. trueppm_api/apps/integrations/outgoing.py +201 -0
  118. trueppm_api/apps/integrations/providers.py +519 -0
  119. trueppm_api/apps/integrations/registry.py +345 -0
  120. trueppm_api/apps/integrations/serializers.py +453 -0
  121. trueppm_api/apps/integrations/services.py +171 -0
  122. trueppm_api/apps/integrations/tasks.py +731 -0
  123. trueppm_api/apps/integrations/throttles.py +165 -0
  124. trueppm_api/apps/integrations/urls.py +77 -0
  125. trueppm_api/apps/integrations/views.py +1106 -0
  126. trueppm_api/apps/jiraimport/__init__.py +8 -0
  127. trueppm_api/apps/jiraimport/apps.py +11 -0
  128. trueppm_api/apps/jiraimport/migrations/0001_initial.py +72 -0
  129. trueppm_api/apps/jiraimport/migrations/__init__.py +0 -0
  130. trueppm_api/apps/jiraimport/models.py +70 -0
  131. trueppm_api/apps/jiraimport/parser.py +350 -0
  132. trueppm_api/apps/jiraimport/services.py +46 -0
  133. trueppm_api/apps/jiraimport/tasks.py +277 -0
  134. trueppm_api/apps/jiraimport/urls.py +15 -0
  135. trueppm_api/apps/jiraimport/views.py +153 -0
  136. trueppm_api/apps/msproject/__init__.py +0 -0
  137. trueppm_api/apps/msproject/apps.py +11 -0
  138. trueppm_api/apps/msproject/dataclasses.py +153 -0
  139. trueppm_api/apps/msproject/exporter.py +500 -0
  140. trueppm_api/apps/msproject/extended_attributes.py +77 -0
  141. trueppm_api/apps/msproject/importer.py +1095 -0
  142. trueppm_api/apps/msproject/migrations/0001_import_request.py +77 -0
  143. trueppm_api/apps/msproject/migrations/0001_squashed_0002_importrequest_creates_project.py +80 -0
  144. trueppm_api/apps/msproject/migrations/0002_importrequest_creates_project.py +15 -0
  145. trueppm_api/apps/msproject/migrations/__init__.py +0 -0
  146. trueppm_api/apps/msproject/models.py +81 -0
  147. trueppm_api/apps/msproject/parser.py +1231 -0
  148. trueppm_api/apps/msproject/serializers.py +136 -0
  149. trueppm_api/apps/msproject/services.py +58 -0
  150. trueppm_api/apps/msproject/tasks.py +436 -0
  151. trueppm_api/apps/msproject/urls.py +39 -0
  152. trueppm_api/apps/msproject/views.py +531 -0
  153. trueppm_api/apps/notifications/__init__.py +7 -0
  154. trueppm_api/apps/notifications/apps.py +15 -0
  155. trueppm_api/apps/notifications/backfill.py +79 -0
  156. trueppm_api/apps/notifications/categories.py +112 -0
  157. trueppm_api/apps/notifications/delivery_limits.py +258 -0
  158. trueppm_api/apps/notifications/digests.py +447 -0
  159. trueppm_api/apps/notifications/email_backend.py +330 -0
  160. trueppm_api/apps/notifications/email_health.py +128 -0
  161. trueppm_api/apps/notifications/migrations/0001_initial.py +207 -0
  162. trueppm_api/apps/notifications/migrations/0001_squashed_0006_notification_task.py +294 -0
  163. trueppm_api/apps/notifications/migrations/0002_project_notification_preference.py +67 -0
  164. trueppm_api/apps/notifications/migrations/0003_projectnotificationpreference_paused.py +17 -0
  165. trueppm_api/apps/notifications/migrations/0004_clean_unknown_matrix_keys.py +30 -0
  166. trueppm_api/apps/notifications/migrations/0005_notification_body_notification_event_type_and_more.py +27 -0
  167. trueppm_api/apps/notifications/migrations/0006_notification_task.py +25 -0
  168. trueppm_api/apps/notifications/migrations/0007_workspaceemailsettings.py +80 -0
  169. trueppm_api/apps/notifications/migrations/0008_notification_snoozed_until.py +17 -0
  170. trueppm_api/apps/notifications/migrations/0009_usernotificationsettings.py +43 -0
  171. trueppm_api/apps/notifications/migrations/0010_projectnotificationpreference_schema_version.py +20 -0
  172. trueppm_api/apps/notifications/migrations/0011_usernotificationsettings_digest_hour_and_more.py +96 -0
  173. trueppm_api/apps/notifications/migrations/__init__.py +0 -0
  174. trueppm_api/apps/notifications/models.py +1197 -0
  175. trueppm_api/apps/notifications/receivers.py +155 -0
  176. trueppm_api/apps/notifications/schema_migrations.py +56 -0
  177. trueppm_api/apps/notifications/serializers.py +961 -0
  178. trueppm_api/apps/notifications/services.py +1451 -0
  179. trueppm_api/apps/notifications/tasks.py +711 -0
  180. trueppm_api/apps/notifications/throttles.py +98 -0
  181. trueppm_api/apps/notifications/urls.py +97 -0
  182. trueppm_api/apps/notifications/views.py +904 -0
  183. trueppm_api/apps/observability/__init__.py +0 -0
  184. trueppm_api/apps/observability/apps.py +31 -0
  185. trueppm_api/apps/observability/logging.py +213 -0
  186. trueppm_api/apps/observability/migrations/0001_initial.py +35 -0
  187. trueppm_api/apps/observability/migrations/0001_squashed_0003_alter_purgerun_trigger.py +154 -0
  188. trueppm_api/apps/observability/migrations/0002_purgerun_retentionpolicy_retentionschedule.py +131 -0
  189. trueppm_api/apps/observability/migrations/0003_alter_purgerun_trigger.py +21 -0
  190. trueppm_api/apps/observability/migrations/0004_alter_retentionpolicy_key.py +28 -0
  191. trueppm_api/apps/observability/migrations/__init__.py +0 -0
  192. trueppm_api/apps/observability/models.py +141 -0
  193. trueppm_api/apps/observability/otel/__init__.py +45 -0
  194. trueppm_api/apps/observability/otel/attributes.py +247 -0
  195. trueppm_api/apps/observability/otel/export_health.py +533 -0
  196. trueppm_api/apps/observability/otel/instrumentation.py +351 -0
  197. trueppm_api/apps/observability/otel/metrics.py +551 -0
  198. trueppm_api/apps/observability/otel/provider.py +511 -0
  199. trueppm_api/apps/observability/otel/request_attributes.py +204 -0
  200. trueppm_api/apps/observability/purge_registry.py +124 -0
  201. trueppm_api/apps/observability/retention.py +139 -0
  202. trueppm_api/apps/observability/selectors.py +969 -0
  203. trueppm_api/apps/observability/serializers.py +142 -0
  204. trueppm_api/apps/observability/services.py +407 -0
  205. trueppm_api/apps/observability/tasks.py +266 -0
  206. trueppm_api/apps/observability/urls.py +27 -0
  207. trueppm_api/apps/observability/views.py +523 -0
  208. trueppm_api/apps/profiles/__init__.py +0 -0
  209. trueppm_api/apps/profiles/apps.py +15 -0
  210. trueppm_api/apps/profiles/constants.py +54 -0
  211. trueppm_api/apps/profiles/migrations/0001_initial.py +55 -0
  212. trueppm_api/apps/profiles/migrations/0001_squashed_0004_userprofile_role_context.py +125 -0
  213. trueppm_api/apps/profiles/migrations/0002_userprofile_hidden_views.py +17 -0
  214. trueppm_api/apps/profiles/migrations/0003_projectvisit.py +63 -0
  215. trueppm_api/apps/profiles/migrations/0004_userprofile_role_context.py +26 -0
  216. trueppm_api/apps/profiles/migrations/0005_userprofile_schedule_in_deliver.py +20 -0
  217. trueppm_api/apps/profiles/migrations/0006_userprofile_date_format_userprofile_timezone.py +36 -0
  218. trueppm_api/apps/profiles/migrations/0007_userpin.py +91 -0
  219. trueppm_api/apps/profiles/migrations/0008_remove_userprofile_schedule_in_deliver.py +71 -0
  220. trueppm_api/apps/profiles/migrations/0009_auth_user_email_upper_idx.py +81 -0
  221. trueppm_api/apps/profiles/migrations/__init__.py +0 -0
  222. trueppm_api/apps/profiles/models.py +276 -0
  223. trueppm_api/apps/profiles/serializers.py +199 -0
  224. trueppm_api/apps/profiles/services.py +466 -0
  225. trueppm_api/apps/profiles/views.py +124 -0
  226. trueppm_api/apps/projects/__init__.py +0 -0
  227. trueppm_api/apps/projects/actual_date_rules.py +163 -0
  228. trueppm_api/apps/projects/amend.py +133 -0
  229. trueppm_api/apps/projects/apps.py +38 -0
  230. trueppm_api/apps/projects/asset_feed.py +459 -0
  231. trueppm_api/apps/projects/asset_views.py +317 -0
  232. trueppm_api/apps/projects/attachment_policy.py +278 -0
  233. trueppm_api/apps/projects/authentication.py +449 -0
  234. trueppm_api/apps/projects/backfill.py +287 -0
  235. trueppm_api/apps/projects/backlog_services.py +324 -0
  236. trueppm_api/apps/projects/backlog_views.py +302 -0
  237. trueppm_api/apps/projects/batch_operation_views.py +264 -0
  238. trueppm_api/apps/projects/blocker_services.py +510 -0
  239. trueppm_api/apps/projects/board_activity.py +462 -0
  240. trueppm_api/apps/projects/board_activity_views.py +186 -0
  241. trueppm_api/apps/projects/board_lanes.py +134 -0
  242. trueppm_api/apps/projects/bulk_settings.py +194 -0
  243. trueppm_api/apps/projects/bundled_templates.py +215 -0
  244. trueppm_api/apps/projects/calendar_settings.py +180 -0
  245. trueppm_api/apps/projects/ceremony_views.py +244 -0
  246. trueppm_api/apps/projects/commit_moment.py +204 -0
  247. trueppm_api/apps/projects/config_notice.py +1325 -0
  248. trueppm_api/apps/projects/custom_field_values.py +266 -0
  249. trueppm_api/apps/projects/decisions_services.py +93 -0
  250. trueppm_api/apps/projects/decisions_views.py +94 -0
  251. trueppm_api/apps/projects/estimation_scale.py +126 -0
  252. trueppm_api/apps/projects/export_bundle.py +353 -0
  253. trueppm_api/apps/projects/fixtures/seeds/atlas-platform-launch.json +4256 -0
  254. trueppm_api/apps/projects/fixtures/seeds/aurora-mobile-app.json +2000 -0
  255. trueppm_api/apps/projects/fixtures/seeds/bayside-civic-center.json +1825 -0
  256. trueppm_api/apps/projects/fixtures/seeds/ga-launch.json +1905 -0
  257. trueppm_api/apps/projects/fixtures/seeds/helios-crm-replacement.json +1409 -0
  258. trueppm_api/apps/projects/guardrail_policy_source.py +269 -0
  259. trueppm_api/apps/projects/inbound_sync.py +555 -0
  260. trueppm_api/apps/projects/iteration_label.py +85 -0
  261. trueppm_api/apps/projects/lifecycle.py +191 -0
  262. trueppm_api/apps/projects/management/__init__.py +0 -0
  263. trueppm_api/apps/projects/management/commands/__init__.py +0 -0
  264. trueppm_api/apps/projects/management/commands/backfill_in_progress_status.py +92 -0
  265. trueppm_api/apps/projects/management/commands/create_demo_share_link.py +368 -0
  266. trueppm_api/apps/projects/management/commands/export_program.py +50 -0
  267. trueppm_api/apps/projects/management/commands/import_seed.py +165 -0
  268. trueppm_api/apps/projects/management/commands/load_sample_project.py +247 -0
  269. trueppm_api/apps/projects/management/commands/seed_capacity.py +405 -0
  270. trueppm_api/apps/projects/management/commands/seed_integration_fixtures.py +178 -0
  271. trueppm_api/apps/projects/management/commands/shift_sample_dates.py +120 -0
  272. trueppm_api/apps/projects/mcp_settings.py +305 -0
  273. trueppm_api/apps/projects/methodology.py +178 -0
  274. trueppm_api/apps/projects/migrations/0001_initial.py +208 -0
  275. trueppm_api/apps/projects/migrations/0001_squashed_0094_project_decisions_policy.py +5595 -0
  276. trueppm_api/apps/projects/migrations/0002_alter_uuid_pk_serialize_false.py +57 -0
  277. trueppm_api/apps/projects/migrations/0003_rename_task_project_index.py +25 -0
  278. trueppm_api/apps/projects/migrations/0004_soft_delete_and_dependency_versioned.py +70 -0
  279. trueppm_api/apps/projects/migrations/0005_task_assignee.py +33 -0
  280. trueppm_api/apps/projects/migrations/0006_task_utilization_index.py +31 -0
  281. trueppm_api/apps/projects/migrations/0007_task_is_milestone.py +28 -0
  282. trueppm_api/apps/projects/migrations/0008_baseline.py +119 -0
  283. trueppm_api/apps/projects/migrations/0009_historicaldependency_historicalproject_and_more.py +210 -0
  284. trueppm_api/apps/projects/migrations/0010_risk.py +229 -0
  285. trueppm_api/apps/projects/migrations/0011_task_status.py +27 -0
  286. trueppm_api/apps/projects/migrations/0012_task_status.py +27 -0
  287. trueppm_api/apps/projects/migrations/0013_task_planned_start.py +24 -0
  288. trueppm_api/apps/projects/migrations/0014_short_hex_ids.py +74 -0
  289. trueppm_api/apps/projects/migrations/0015_backfill_short_ids.py +48 -0
  290. trueppm_api/apps/projects/migrations/0016_task_actual_dates.py +42 -0
  291. trueppm_api/apps/projects/migrations/0017_boardcolumnconfig.py +43 -0
  292. trueppm_api/apps/projects/migrations/0018_estimation_governance.py +62 -0
  293. trueppm_api/apps/projects/migrations/0019_backfill_wbs_paths.py +28 -0
  294. trueppm_api/apps/projects/migrations/0020_task_status_5col.py +65 -0
  295. trueppm_api/apps/projects/migrations/0021_boardsavedview.py +64 -0
  296. trueppm_api/apps/projects/migrations/0022_task_status_changed_at_priority_rank.py +35 -0
  297. trueppm_api/apps/projects/migrations/0023_risk_framework_fields.py +96 -0
  298. trueppm_api/apps/projects/migrations/0024_riskcomment.py +52 -0
  299. trueppm_api/apps/projects/migrations/0025_sprint.py +352 -0
  300. trueppm_api/apps/projects/migrations/0026_project_methodology.py +30 -0
  301. trueppm_api/apps/projects/migrations/0027_sprint_retro.py +89 -0
  302. trueppm_api/apps/projects/migrations/0028_normalize_notes_field.py +42 -0
  303. trueppm_api/apps/projects/migrations/0029_complete_implies_full_progress.py +38 -0
  304. trueppm_api/apps/projects/migrations/0030_review_implies_full_progress.py +37 -0
  305. trueppm_api/apps/projects/migrations/0031_task_remaining_points.py +22 -0
  306. trueppm_api/apps/projects/migrations/0032_task_is_subtask_and_sprint_scope_change.py +90 -0
  307. trueppm_api/apps/projects/migrations/0033_task_my_work_index.py +38 -0
  308. trueppm_api/apps/projects/migrations/0034_inbound_task_sync.py +313 -0
  309. trueppm_api/apps/projects/migrations/0035_retro_versioned_and_suggested_assignee.py +173 -0
  310. trueppm_api/apps/projects/migrations/0036_program_entity_and_membership.py +157 -0
  311. trueppm_api/apps/projects/migrations/0037_sprint_capacity_points.py +20 -0
  312. trueppm_api/apps/projects/migrations/0038_taskcomment_commentreaction_commentacknowledgement_and_more.py +233 -0
  313. trueppm_api/apps/projects/migrations/0039_apitoken_and_more.py +88 -0
  314. trueppm_api/apps/projects/migrations/0040_program_general_fields.py +119 -0
  315. trueppm_api/apps/projects/migrations/0041_ceremony_template_and_phase_gate_config.py +210 -0
  316. trueppm_api/apps/projects/migrations/0041_project_general_fields.py +106 -0
  317. trueppm_api/apps/projects/migrations/0042_merge_20260522_0410.py +12 -0
  318. trueppm_api/apps/projects/migrations/0042_program_rollup_config.py +95 -0
  319. trueppm_api/apps/projects/migrations/0042_workflow_color_and_custom_fields.py +94 -0
  320. trueppm_api/apps/projects/migrations/0043_merge_20260522_1118.py +10 -0
  321. trueppm_api/apps/projects/migrations/0044_merge_20260522_rollup.py +10 -0
  322. trueppm_api/apps/projects/migrations/0044_project_archive_program_close.py +119 -0
  323. trueppm_api/apps/projects/migrations/0045_program_risk_policy.py +78 -0
  324. trueppm_api/apps/projects/migrations/0046_merge_20260523_2248.py +12 -0
  325. trueppm_api/apps/projects/migrations/0047_apitokenauditentry_program_and_more.py +55 -0
  326. trueppm_api/apps/projects/migrations/0048_historicalprogram_color_program_color.py +32 -0
  327. trueppm_api/apps/projects/migrations/0049_backlog_item.py +112 -0
  328. trueppm_api/apps/projects/migrations/0050_backlog_item_trgm_search.py +26 -0
  329. trueppm_api/apps/projects/migrations/0051_alter_calendar_working_days_alter_dependency_lag_and_more.py +144 -0
  330. trueppm_api/apps/projects/migrations/0052_recurring_tasks.py +278 -0
  331. trueppm_api/apps/projects/migrations/0053_dependency_dep_pred_serverver_idx_and_more.py +56 -0
  332. trueppm_api/apps/projects/migrations/0054_sprintscopechange_goal_impact_and_more.py +117 -0
  333. trueppm_api/apps/projects/migrations/0055_historicaltask_sprint_pending_and_more.py +51 -0
  334. trueppm_api/apps/projects/migrations/0056_historicalproject_prioritization_model_and_more.py +276 -0
  335. trueppm_api/apps/projects/migrations/0057_historicalsprint_binding_committed_snapshot_and_more.py +58 -0
  336. trueppm_api/apps/projects/migrations/0058_historicaltask_delivery_mode_and_more.py +72 -0
  337. trueppm_api/apps/projects/migrations/0059_forecastsnapshot.py +80 -0
  338. trueppm_api/apps/projects/migrations/0060_historicalprojectsignalprivacypolicy_and_more.py +90 -0
  339. trueppm_api/apps/projects/migrations/0061_baselinetask_story_points.py +17 -0
  340. trueppm_api/apps/projects/migrations/0062_project_is_sample.py +22 -0
  341. trueppm_api/apps/projects/migrations/0063_historicalsprint_wip_limit_sprint_wip_limit.py +22 -0
  342. trueppm_api/apps/projects/migrations/0064_sprinttaskoutcome.py +97 -0
  343. trueppm_api/apps/projects/migrations/0065_historicalsprint_goal_outcome_sprint_goal_outcome.py +32 -0
  344. trueppm_api/apps/projects/migrations/0066_historicalproject_lead_project_lead.py +38 -0
  345. trueppm_api/apps/projects/migrations/0067_sprint_exclude_from_velocity.py +22 -0
  346. trueppm_api/apps/projects/migrations/0068_project_recalculated_at.py +23 -0
  347. trueppm_api/apps/projects/migrations/0069_iteration_label.py +22 -0
  348. trueppm_api/apps/projects/migrations/0070_historicalprogram_iteration_label_and_more.py +32 -0
  349. trueppm_api/apps/projects/migrations/0071_backfill_iteration_label_inherit.py +35 -0
  350. trueppm_api/apps/projects/migrations/0072_pulseresponse_retroboarditem.py +129 -0
  351. trueppm_api/apps/projects/migrations/0073_sprinttaskoutcome_demo_ready.py +17 -0
  352. trueppm_api/apps/projects/migrations/0074_historicaltask_blocked_reason_task_blocked_reason.py +30 -0
  353. trueppm_api/apps/projects/migrations/0075_risk_decimal_short_id.py +42 -0
  354. trueppm_api/apps/projects/migrations/0076_sprinttaskoutcome_review_polish.py +45 -0
  355. trueppm_api/apps/projects/migrations/0077_task_blocker_fields.py +111 -0
  356. trueppm_api/apps/projects/migrations/0078_alter_risktask_unique_together_and_more.py +22 -0
  357. trueppm_api/apps/projects/migrations/0079_historicalproject_status_date_project_status_date.py +22 -0
  358. trueppm_api/apps/projects/migrations/0080_alter_historicaltask_type_alter_task_type.py +46 -0
  359. trueppm_api/apps/projects/migrations/0081_historicalprogram_allow_guests_and_more.py +52 -0
  360. trueppm_api/apps/projects/migrations/0082_project_last_sync_version.py +90 -0
  361. trueppm_api/apps/projects/migrations/0083_historicalprogram_mc_history_attribution_audience_and_more.py +108 -0
  362. trueppm_api/apps/projects/migrations/0084_tasknote.py +71 -0
  363. trueppm_api/apps/projects/migrations/0085_sprint_ix_sprint_velocity.py +33 -0
  364. trueppm_api/apps/projects/migrations/0086_board_card_search_trgm.py +33 -0
  365. trueppm_api/apps/projects/migrations/0087_historicalprogram_task_duration_change_percent_policy_and_more.py +155 -0
  366. trueppm_api/apps/projects/migrations/0088_historicalprogram_allowed_attachment_types_and_more.py +61 -0
  367. trueppm_api/apps/projects/migrations/0089_signalceilingraiseproposal_signalceilingraisevote_and_more.py +157 -0
  368. trueppm_api/apps/projects/migrations/0090_historicaltask_proj_histdate_index.py +44 -0
  369. trueppm_api/apps/projects/migrations/0091_project_board_cadence.py +39 -0
  370. trueppm_api/apps/projects/migrations/0092_program_target_date.py +22 -0
  371. trueppm_api/apps/projects/migrations/0093_cross_project_dependency_consent.py +58 -0
  372. trueppm_api/apps/projects/migrations/0094_project_decisions_policy.py +90 -0
  373. trueppm_api/apps/projects/migrations/0095_poker_session_vote.py +127 -0
  374. trueppm_api/apps/projects/migrations/0095_squashed_0097_release_0_3.py +225 -0
  375. trueppm_api/apps/projects/migrations/0096_crossprojectslipconflict.py +91 -0
  376. trueppm_api/apps/projects/migrations/0097_backlogitem_backlogitem_tags_gin_and_more.py +32 -0
  377. trueppm_api/apps/projects/migrations/0098_historicalproject_show_baselines_and_more.py +52 -0
  378. trueppm_api/apps/projects/migrations/0099_apitoken_scopes.py +48 -0
  379. trueppm_api/apps/projects/migrations/0100_task_dep_serverver_idx_concurrent.py +61 -0
  380. trueppm_api/apps/projects/migrations/0101_dependency_deleted_at_task_deleted_at.py +22 -0
  381. trueppm_api/apps/projects/migrations/0102_project_deleted_at_project_proj_isdel_deletedat_idx.py +25 -0
  382. trueppm_api/apps/projects/migrations/0103_taskdurationchangeevent_task_dur_evt_sprint_idx.py +18 -0
  383. trueppm_api/apps/projects/migrations/0104_historicalproject_stale_task_threshold_days_and_more.py +32 -0
  384. trueppm_api/apps/projects/migrations/0104_trash_restore_1113.py +38 -0
  385. trueppm_api/apps/projects/migrations/0105_historical_changelog_indexes.py +62 -0
  386. trueppm_api/apps/projects/migrations/0106_boardsavedview_schema_version.py +20 -0
  387. trueppm_api/apps/projects/migrations/0107_task_activity_event.py +70 -0
  388. trueppm_api/apps/projects/migrations/0108_remove_apitoken_api_token_scope_xor_and_more.py +117 -0
  389. trueppm_api/apps/projects/migrations/0109_projectexportjob.py +82 -0
  390. trueppm_api/apps/projects/migrations/0110_sharelink.py +123 -0
  391. trueppm_api/apps/projects/migrations/0111_projectcalendarlayer.py +67 -0
  392. trueppm_api/apps/projects/migrations/0112_sharelink_expires_at_alter_sharelink_content_kind.py +31 -0
  393. trueppm_api/apps/projects/migrations/0113_alter_historicaltask_percent_complete_and_more.py +35 -0
  394. trueppm_api/apps/projects/migrations/0114_historicalproject_default_member_role_and_more.py +28 -0
  395. trueppm_api/apps/projects/migrations/0115_historicalproject_end_date_shift_threshold_days_and_more.py +22 -0
  396. trueppm_api/apps/projects/migrations/0116_alter_taskactivityevent_event_type.py +28 -0
  397. trueppm_api/apps/projects/migrations/0117_label_tasklabel_label_tasks_and_more.py +126 -0
  398. trueppm_api/apps/projects/migrations/0118_programexportjob.py +82 -0
  399. trueppm_api/apps/projects/migrations/0119_historicalprogram_calendar_program_calendar.py +36 -0
  400. trueppm_api/apps/projects/migrations/0120_alter_backlogitem_item_type.py +29 -0
  401. trueppm_api/apps/projects/migrations/0121_task_three_point_estimate_ordered.py +64 -0
  402. trueppm_api/apps/projects/migrations/0122_task_relation.py +173 -0
  403. trueppm_api/apps/projects/migrations/0123_remove_historicalproject_agile_features_and_more.py +20 -0
  404. trueppm_api/apps/projects/migrations/0124_dependency_is_driving.py +17 -0
  405. trueppm_api/apps/projects/migrations/0125_historicalprogram_estimation_scale_and_more.py +68 -0
  406. trueppm_api/apps/projects/migrations/0126_projectcustomfield_show_on_card_taskcustomfieldvalue.py +80 -0
  407. trueppm_api/apps/projects/migrations/0127_historicalprogram_mcp_enabled_and_more.py +32 -0
  408. trueppm_api/apps/projects/migrations/0128_acceptancecriterion_sync_seq_apitoken_sync_seq_and_more.py +192 -0
  409. trueppm_api/apps/projects/migrations/0129_sharelink_show_milestone_dates.py +20 -0
  410. trueppm_api/apps/projects/migrations/0130_programimportjob.py +85 -0
  411. trueppm_api/apps/projects/migrations/0131_historicalprogram_sprint_picker_ready_only_default_and_more.py +32 -0
  412. trueppm_api/apps/projects/migrations/0132_task_scheduled_start.py +17 -0
  413. trueppm_api/apps/projects/migrations/0133_project_status_date_floor_armed_at.py +17 -0
  414. trueppm_api/apps/projects/migrations/0134_task_edited_at_task_seeded_at_task_source_id_and_more.py +65 -0
  415. trueppm_api/apps/projects/migrations/0135_projecttemplate_templateapplication_and_more.py +175 -0
  416. trueppm_api/apps/projects/migrations/0136_schedule_outline_batch_operations.py +118 -0
  417. trueppm_api/apps/projects/migrations/0137_collapse_dual_scope_api_tokens.py +54 -0
  418. trueppm_api/apps/projects/migrations/0138_apitokenauditentry_api_token_audit_owner_idx.py +18 -0
  419. trueppm_api/apps/projects/migrations/0139_historicaltask_duration_unit_task_duration_unit.py +32 -0
  420. trueppm_api/apps/projects/migrations/0140_historicaltask_auto_container_and_more.py +113 -0
  421. trueppm_api/apps/projects/migrations/0141_historicalproject_draft_started_at_and_more.py +44 -0
  422. trueppm_api/apps/projects/migrations/0142_baseline_calendar_hours_per_day_and_more.py +31 -0
  423. trueppm_api/apps/projects/migrations/0143_sprintcloserequest_failure_reason_and_more.py +42 -0
  424. trueppm_api/apps/projects/migrations/0144_projecttemplate_source_project_and_more.py +35 -0
  425. trueppm_api/apps/projects/migrations/0145_seed_bundled_templates.py +65 -0
  426. trueppm_api/apps/projects/migrations/0146_historicaltask_board_lane_task_board_lane.py +32 -0
  427. trueppm_api/apps/projects/migrations/0147_structuraloperation.py +128 -0
  428. trueppm_api/apps/projects/migrations/0148_task_unique_task_wbs_path_per_project_live.py +76 -0
  429. trueppm_api/apps/projects/migrations/0149_drop_workshops_tables.py +42 -0
  430. trueppm_api/apps/projects/migrations/0150_clear_uncommitted_cpm_output.py +130 -0
  431. trueppm_api/apps/projects/migrations/0151_config_notice_request.py +70 -0
  432. trueppm_api/apps/projects/migrations/0152_aggregate_task_wbs_name_idx.py +20 -0
  433. trueppm_api/apps/projects/migrations/0153_program_sample_anchor_date.py +22 -0
  434. trueppm_api/apps/projects/migrations/__init__.py +0 -0
  435. trueppm_api/apps/projects/models.py +8946 -0
  436. trueppm_api/apps/projects/poker_services.py +154 -0
  437. trueppm_api/apps/projects/poker_views.py +398 -0
  438. trueppm_api/apps/projects/product_backlog_services.py +450 -0
  439. trueppm_api/apps/projects/program_label_services.py +94 -0
  440. trueppm_api/apps/projects/program_label_views.py +189 -0
  441. trueppm_api/apps/projects/program_rollup.py +575 -0
  442. trueppm_api/apps/projects/program_schedule.py +745 -0
  443. trueppm_api/apps/projects/program_views.py +3220 -0
  444. trueppm_api/apps/projects/project_templates.py +422 -0
  445. trueppm_api/apps/projects/receivers.py +234 -0
  446. trueppm_api/apps/projects/refusal_codes.py +140 -0
  447. trueppm_api/apps/projects/reorder_services.py +145 -0
  448. trueppm_api/apps/projects/restructure_hooks.py +54 -0
  449. trueppm_api/apps/projects/retro_board_services.py +368 -0
  450. trueppm_api/apps/projects/retro_services.py +285 -0
  451. trueppm_api/apps/projects/risk_import.py +396 -0
  452. trueppm_api/apps/projects/row_identity.py +113 -0
  453. trueppm_api/apps/projects/schema_migrations.py +228 -0
  454. trueppm_api/apps/projects/schemas/seed_v1.json +493 -0
  455. trueppm_api/apps/projects/schemas/seed_v2.json +892 -0
  456. trueppm_api/apps/projects/seed/__init__.py +61 -0
  457. trueppm_api/apps/projects/seed/exporter.py +1644 -0
  458. trueppm_api/apps/projects/seed/forecast_backfill.py +247 -0
  459. trueppm_api/apps/projects/seed/importer.py +2390 -0
  460. trueppm_api/apps/projects/seed/reanchor.py +551 -0
  461. trueppm_api/apps/projects/seed/reldates.py +126 -0
  462. trueppm_api/apps/projects/seed/replace.py +126 -0
  463. trueppm_api/apps/projects/seed/replay.py +1691 -0
  464. trueppm_api/apps/projects/seed/replay_ctx.py +41 -0
  465. trueppm_api/apps/projects/seed/samples.py +427 -0
  466. trueppm_api/apps/projects/seed/validation.py +1149 -0
  467. trueppm_api/apps/projects/serializers.py +12310 -0
  468. trueppm_api/apps/projects/services.py +6966 -0
  469. trueppm_api/apps/projects/share_serializers.py +132 -0
  470. trueppm_api/apps/projects/share_services.py +325 -0
  471. trueppm_api/apps/projects/share_views.py +374 -0
  472. trueppm_api/apps/projects/sharing_settings.py +187 -0
  473. trueppm_api/apps/projects/signal_privacy_services.py +909 -0
  474. trueppm_api/apps/projects/signal_privacy_views.py +476 -0
  475. trueppm_api/apps/projects/signals.py +132 -0
  476. trueppm_api/apps/projects/slip_conflict.py +266 -0
  477. trueppm_api/apps/projects/sprint_cadence.py +453 -0
  478. trueppm_api/apps/projects/sprint_picker_settings.py +132 -0
  479. trueppm_api/apps/projects/standup.py +304 -0
  480. trueppm_api/apps/projects/standup_views.py +99 -0
  481. trueppm_api/apps/projects/structural_operation_services.py +727 -0
  482. trueppm_api/apps/projects/structural_operation_views.py +311 -0
  483. trueppm_api/apps/projects/surface_visibility.py +126 -0
  484. trueppm_api/apps/projects/task_batch_services.py +379 -0
  485. trueppm_api/apps/projects/task_bulk.py +1152 -0
  486. trueppm_api/apps/projects/task_classification.py +561 -0
  487. trueppm_api/apps/projects/task_classification_defaults.py +95 -0
  488. trueppm_api/apps/projects/task_duration_settings.py +142 -0
  489. trueppm_api/apps/projects/task_grouping.py +754 -0
  490. trueppm_api/apps/projects/tasks.py +1917 -0
  491. trueppm_api/apps/projects/template_divergence.py +168 -0
  492. trueppm_api/apps/projects/template_services.py +160 -0
  493. trueppm_api/apps/projects/template_tasks.py +258 -0
  494. trueppm_api/apps/projects/template_views.py +799 -0
  495. trueppm_api/apps/projects/throttles.py +311 -0
  496. trueppm_api/apps/projects/urls.py +1011 -0
  497. trueppm_api/apps/projects/utilization.py +885 -0
  498. trueppm_api/apps/projects/views.py +20572 -0
  499. trueppm_api/apps/projects/wbs_paths.py +119 -0
  500. trueppm_api/apps/resources/__init__.py +0 -0
  501. trueppm_api/apps/resources/apps.py +8 -0
  502. trueppm_api/apps/resources/capacity.py +121 -0
  503. trueppm_api/apps/resources/migrations/0001_initial.py +68 -0
  504. trueppm_api/apps/resources/migrations/0001_squashed_0013_alter_projectresource_unique_together_and_more.py +281 -0
  505. trueppm_api/apps/resources/migrations/0002_alter_uuid_pk_serialize_false.py +36 -0
  506. trueppm_api/apps/resources/migrations/0003_resource_soft_delete.py +30 -0
  507. trueppm_api/apps/resources/migrations/0004_add_taskresource_resource_index.py +25 -0
  508. trueppm_api/apps/resources/migrations/0005_resource_job_role.py +20 -0
  509. trueppm_api/apps/resources/migrations/0006_skill.py +34 -0
  510. trueppm_api/apps/resources/migrations/0007_resource_skill.py +63 -0
  511. trueppm_api/apps/resources/migrations/0008_project_resource.py +63 -0
  512. trueppm_api/apps/resources/migrations/0009_task_skill_requirement.py +60 -0
  513. trueppm_api/apps/resources/migrations/0010_alter_resourceskill_options_and_more.py +20 -0
  514. trueppm_api/apps/resources/migrations/0011_alter_projectresource_notes.py +17 -0
  515. trueppm_api/apps/resources/migrations/0012_add_resource_user_fk.py +26 -0
  516. trueppm_api/apps/resources/migrations/0013_alter_projectresource_unique_together_and_more.py +53 -0
  517. trueppm_api/apps/resources/migrations/0014_projectresource_sync_seq_resource_sync_seq_and_more.py +37 -0
  518. trueppm_api/apps/resources/migrations/0015_projectresource_deactivated_with_resource.py +17 -0
  519. trueppm_api/apps/resources/migrations/__init__.py +0 -0
  520. trueppm_api/apps/resources/models.py +298 -0
  521. trueppm_api/apps/resources/serializers.py +426 -0
  522. trueppm_api/apps/resources/services.py +465 -0
  523. trueppm_api/apps/resources/urls.py +28 -0
  524. trueppm_api/apps/resources/views.py +1749 -0
  525. trueppm_api/apps/scheduling/__init__.py +0 -0
  526. trueppm_api/apps/scheduling/apps.py +153 -0
  527. trueppm_api/apps/scheduling/calendars.py +119 -0
  528. trueppm_api/apps/scheduling/deadletter.py +91 -0
  529. trueppm_api/apps/scheduling/forecast_history_settings.py +215 -0
  530. trueppm_api/apps/scheduling/forecast_staleness.py +313 -0
  531. trueppm_api/apps/scheduling/graph_guard.py +263 -0
  532. trueppm_api/apps/scheduling/management/__init__.py +0 -0
  533. trueppm_api/apps/scheduling/management/commands/__init__.py +0 -0
  534. trueppm_api/apps/scheduling/management/commands/prune_forecast_snapshots.py +53 -0
  535. trueppm_api/apps/scheduling/migrations/0001_failed_task.py +62 -0
  536. trueppm_api/apps/scheduling/migrations/0001_squashed_0007_projectforecastsnapshot.py +309 -0
  537. trueppm_api/apps/scheduling/migrations/0002_schedulerequest.py +78 -0
  538. trueppm_api/apps/scheduling/migrations/0003_add_schedule_request_reason.py +27 -0
  539. trueppm_api/apps/scheduling/migrations/0004_velocity_suggestion.py +94 -0
  540. trueppm_api/apps/scheduling/migrations/0005_montecarlorun.py +61 -0
  541. trueppm_api/apps/scheduling/migrations/0006_montecarlorun_distribution.py +17 -0
  542. trueppm_api/apps/scheduling/migrations/0007_projectforecastsnapshot.py +65 -0
  543. trueppm_api/apps/scheduling/migrations/0008_alter_schedulerequest_reason.py +28 -0
  544. trueppm_api/apps/scheduling/migrations/0008_folded_schedulerequest_reason.py +34 -0
  545. trueppm_api/apps/scheduling/migrations/0009_alter_schedulerequest_reason.py +29 -0
  546. trueppm_api/apps/scheduling/migrations/0010_failedtask_resolution_note_failedtask_resolved_at_and_more.py +43 -0
  547. trueppm_api/apps/scheduling/migrations/0011_failedtask_project.py +26 -0
  548. trueppm_api/apps/scheduling/migrations/0012_montecarlorun_diagnostic.py +17 -0
  549. trueppm_api/apps/scheduling/migrations/0013_montecarlorun_status_date.py +17 -0
  550. trueppm_api/apps/scheduling/migrations/0014_montecarlorun_plan_version.py +17 -0
  551. trueppm_api/apps/scheduling/migrations/__init__.py +0 -0
  552. trueppm_api/apps/scheduling/models.py +516 -0
  553. trueppm_api/apps/scheduling/receivers.py +160 -0
  554. trueppm_api/apps/scheduling/risk_premium.py +283 -0
  555. trueppm_api/apps/scheduling/serializers.py +564 -0
  556. trueppm_api/apps/scheduling/services.py +1182 -0
  557. trueppm_api/apps/scheduling/signals.py +56 -0
  558. trueppm_api/apps/scheduling/tasks.py +1984 -0
  559. trueppm_api/apps/scheduling/telemetry.py +80 -0
  560. trueppm_api/apps/scheduling/units.py +35 -0
  561. trueppm_api/apps/scheduling/urls.py +57 -0
  562. trueppm_api/apps/scheduling/views.py +2564 -0
  563. trueppm_api/apps/sso/__init__.py +0 -0
  564. trueppm_api/apps/sso/apps.py +19 -0
  565. trueppm_api/apps/sso/extensions.py +130 -0
  566. trueppm_api/apps/sso/management/__init__.py +0 -0
  567. trueppm_api/apps/sso/management/commands/__init__.py +0 -0
  568. trueppm_api/apps/sso/management/commands/seed_sso_keycloak.py +172 -0
  569. trueppm_api/apps/sso/migrations/0001_initial.py +119 -0
  570. trueppm_api/apps/sso/migrations/0002_remove_oidcprovider_workspace_ssoproviderpolicy_and_more.py +169 -0
  571. trueppm_api/apps/sso/migrations/__init__.py +0 -0
  572. trueppm_api/apps/sso/models.py +148 -0
  573. trueppm_api/apps/sso/serializers.py +437 -0
  574. trueppm_api/apps/sso/services.py +1385 -0
  575. trueppm_api/apps/sso/urls.py +47 -0
  576. trueppm_api/apps/sso/views.py +935 -0
  577. trueppm_api/apps/sync/__init__.py +0 -0
  578. trueppm_api/apps/sync/apps.py +15 -0
  579. trueppm_api/apps/sync/backfill.py +208 -0
  580. trueppm_api/apps/sync/broadcast.py +320 -0
  581. trueppm_api/apps/sync/conflict.py +262 -0
  582. trueppm_api/apps/sync/consumers.py +471 -0
  583. trueppm_api/apps/sync/migrations/0001_initial.py +59 -0
  584. trueppm_api/apps/sync/migrations/0001_squashed_0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +77 -0
  585. trueppm_api/apps/sync/migrations/0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +41 -0
  586. trueppm_api/apps/sync/migrations/0003_boardevent.py +46 -0
  587. trueppm_api/apps/sync/migrations/0004_seed_sync_seq.py +24 -0
  588. trueppm_api/apps/sync/migrations/0005_program_sync_sequence.py +35 -0
  589. trueppm_api/apps/sync/migrations/__init__.py +0 -0
  590. trueppm_api/apps/sync/models.py +225 -0
  591. trueppm_api/apps/sync/pagination.py +231 -0
  592. trueppm_api/apps/sync/sequence.py +371 -0
  593. trueppm_api/apps/sync/serializers.py +687 -0
  594. trueppm_api/apps/sync/tasks.py +260 -0
  595. trueppm_api/apps/sync/throttles.py +175 -0
  596. trueppm_api/apps/sync/upload.py +501 -0
  597. trueppm_api/apps/sync/urls.py +17 -0
  598. trueppm_api/apps/sync/views.py +930 -0
  599. trueppm_api/apps/sync/ws_auth.py +193 -0
  600. trueppm_api/apps/taskruns/__init__.py +0 -0
  601. trueppm_api/apps/taskruns/apps.py +11 -0
  602. trueppm_api/apps/taskruns/migrations/0001_initial.py +92 -0
  603. trueppm_api/apps/taskruns/migrations/__init__.py +0 -0
  604. trueppm_api/apps/taskruns/models.py +68 -0
  605. trueppm_api/apps/taskruns/serializers.py +92 -0
  606. trueppm_api/apps/taskruns/tasks.py +60 -0
  607. trueppm_api/apps/taskruns/tracker.py +286 -0
  608. trueppm_api/apps/taskruns/urls.py +46 -0
  609. trueppm_api/apps/taskruns/views.py +195 -0
  610. trueppm_api/apps/teams/__init__.py +8 -0
  611. trueppm_api/apps/teams/apps.py +16 -0
  612. trueppm_api/apps/teams/migrations/0001_initial.py +139 -0
  613. trueppm_api/apps/teams/migrations/0001_squashed_0002_default_teams.py +142 -0
  614. trueppm_api/apps/teams/migrations/0002_default_teams.py +62 -0
  615. trueppm_api/apps/teams/migrations/0003_team_sync_seq_teammembership_sync_seq.py +22 -0
  616. trueppm_api/apps/teams/migrations/__init__.py +0 -0
  617. trueppm_api/apps/teams/models.py +145 -0
  618. trueppm_api/apps/teams/permissions.py +149 -0
  619. trueppm_api/apps/teams/serializers.py +95 -0
  620. trueppm_api/apps/teams/services.py +434 -0
  621. trueppm_api/apps/teams/signals.py +61 -0
  622. trueppm_api/apps/teams/urls.py +34 -0
  623. trueppm_api/apps/teams/views.py +201 -0
  624. trueppm_api/apps/timetracking/__init__.py +0 -0
  625. trueppm_api/apps/timetracking/apps.py +9 -0
  626. trueppm_api/apps/timetracking/migrations/0001_initial.py +117 -0
  627. trueppm_api/apps/timetracking/migrations/0002_timesheetsubmission.py +46 -0
  628. trueppm_api/apps/timetracking/migrations/0003_timeentry_deleted_at_timeentry_deleted_by.py +31 -0
  629. trueppm_api/apps/timetracking/migrations/0004_timeentry_sync_seq_and_more.py +24 -0
  630. trueppm_api/apps/timetracking/migrations/__init__.py +0 -0
  631. trueppm_api/apps/timetracking/models.py +178 -0
  632. trueppm_api/apps/timetracking/serializers.py +175 -0
  633. trueppm_api/apps/timetracking/services.py +117 -0
  634. trueppm_api/apps/timetracking/urls.py +47 -0
  635. trueppm_api/apps/timetracking/views.py +526 -0
  636. trueppm_api/apps/webhooks/__init__.py +0 -0
  637. trueppm_api/apps/webhooks/apps.py +8 -0
  638. trueppm_api/apps/webhooks/backfill.py +90 -0
  639. trueppm_api/apps/webhooks/dispatch.py +144 -0
  640. trueppm_api/apps/webhooks/migrations/0001_initial.py +146 -0
  641. trueppm_api/apps/webhooks/migrations/0001_squashed_0007_sprint_lifecycle_events.py +243 -0
  642. trueppm_api/apps/webhooks/migrations/0002_alter_webhook_events.py +32 -0
  643. trueppm_api/apps/webhooks/migrations/0003_delivery_status_created_idx.py +17 -0
  644. trueppm_api/apps/webhooks/migrations/0004_webhook_program_alter_webhook_project_and_more.py +64 -0
  645. trueppm_api/apps/webhooks/migrations/0005_webhook_delivery_sequence_numbers.py +30 -0
  646. trueppm_api/apps/webhooks/migrations/0006_webhook_format_alter_webhook_events_and_more.py +61 -0
  647. trueppm_api/apps/webhooks/migrations/0007_sprint_lifecycle_events.py +63 -0
  648. trueppm_api/apps/webhooks/migrations/0008_risk_baseline_comment_events.py +73 -0
  649. trueppm_api/apps/webhooks/migrations/0009_encrypt_webhook_secret_and_failure_counters.py +90 -0
  650. trueppm_api/apps/webhooks/migrations/__init__.py +0 -0
  651. trueppm_api/apps/webhooks/models.py +372 -0
  652. trueppm_api/apps/webhooks/serializers.py +318 -0
  653. trueppm_api/apps/webhooks/tasks.py +663 -0
  654. trueppm_api/apps/webhooks/urls.py +22 -0
  655. trueppm_api/apps/webhooks/views.py +383 -0
  656. trueppm_api/apps/workflow_engine/__init__.py +0 -0
  657. trueppm_api/apps/workflow_engine/apps.py +23 -0
  658. trueppm_api/apps/workflow_engine/migrations/0001_initial.py +230 -0
  659. trueppm_api/apps/workflow_engine/migrations/0001_squashed_0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +234 -0
  660. trueppm_api/apps/workflow_engine/migrations/0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +16 -0
  661. trueppm_api/apps/workflow_engine/migrations/0003_workflowoutboxrow_workflow_outbox_recovery_idx.py +21 -0
  662. trueppm_api/apps/workflow_engine/migrations/__init__.py +0 -0
  663. trueppm_api/apps/workflow_engine/models.py +265 -0
  664. trueppm_api/apps/workflow_engine/tasks.py +368 -0
  665. trueppm_api/apps/workspace/__init__.py +0 -0
  666. trueppm_api/apps/workspace/apps.py +10 -0
  667. trueppm_api/apps/workspace/backfill.py +111 -0
  668. trueppm_api/apps/workspace/export.py +242 -0
  669. trueppm_api/apps/workspace/migrations/0001_initial.py +326 -0
  670. trueppm_api/apps/workspace/migrations/0001_squashed_0014_workspacemembership_availability_effective_from_and_more.py +829 -0
  671. trueppm_api/apps/workspace/migrations/0002_remove_workspace_fiscal_year_start_and_more.py +85 -0
  672. trueppm_api/apps/workspace/migrations/0003_workspaceexportjob.py +73 -0
  673. trueppm_api/apps/workspace/migrations/0004_alter_workspaceinvite_status.py +27 -0
  674. trueppm_api/apps/workspace/migrations/0005_workspace_iteration_label_and_more.py +26 -0
  675. trueppm_api/apps/workspace/migrations/0006_alter_groupmembership_unique_together_and_more.py +45 -0
  676. trueppm_api/apps/workspace/migrations/0007_workspace_public_sharing_override_policy.py +21 -0
  677. trueppm_api/apps/workspace/migrations/0008_workspace_mc_history_attribution_audience_and_more.py +44 -0
  678. trueppm_api/apps/workspace/migrations/0009_workspace_methodology_and_more.py +186 -0
  679. trueppm_api/apps/workspace/migrations/0010_workspace_logo.py +38 -0
  680. trueppm_api/apps/workspace/migrations/0011_historicalworkspace_task_duration_change_percent_override_policy_and_more.py +56 -0
  681. trueppm_api/apps/workspace/migrations/0012_historicalworkspace_allowed_attachment_types_and_more.py +64 -0
  682. trueppm_api/apps/workspace/migrations/0013_auditevent.py +70 -0
  683. trueppm_api/apps/workspace/migrations/0014_workspacemembership_availability_effective_from_and_more.py +39 -0
  684. trueppm_api/apps/workspace/migrations/0015_trash_restore_1113.py +34 -0
  685. trueppm_api/apps/workspace/migrations/0016_historicalworkspace_calendar_and_more.py +55 -0
  686. trueppm_api/apps/workspace/migrations/0017_historicalworkspace_estimation_scale_and_more.py +38 -0
  687. trueppm_api/apps/workspace/migrations/0018_historicalworkspace_feedback_enabled_and_more.py +32 -0
  688. trueppm_api/apps/workspace/migrations/0019_historicalworkspace_mcp_enabled_and_more.py +22 -0
  689. trueppm_api/apps/workspace/migrations/0020_viewer_ordinal_one.py +34 -0
  690. trueppm_api/apps/workspace/migrations/0021_group_sync_seq_groupmembership_sync_seq_and_more.py +27 -0
  691. trueppm_api/apps/workspace/migrations/0022_historicalworkspace_sprint_picker_ready_only_default_and_more.py +22 -0
  692. trueppm_api/apps/workspace/migrations/0023_alter_auditevent_event_type.py +32 -0
  693. trueppm_api/apps/workspace/migrations/0024_alter_auditevent_event_type.py +33 -0
  694. trueppm_api/apps/workspace/migrations/0025_alter_auditevent_event_type.py +36 -0
  695. trueppm_api/apps/workspace/migrations/0026_alter_auditevent_event_type.py +37 -0
  696. trueppm_api/apps/workspace/migrations/0027_alter_auditevent_event_type.py +42 -0
  697. trueppm_api/apps/workspace/migrations/__init__.py +0 -0
  698. trueppm_api/apps/workspace/models.py +857 -0
  699. trueppm_api/apps/workspace/permissions.py +187 -0
  700. trueppm_api/apps/workspace/serializers.py +590 -0
  701. trueppm_api/apps/workspace/services.py +919 -0
  702. trueppm_api/apps/workspace/signals.py +37 -0
  703. trueppm_api/apps/workspace/tasks.py +555 -0
  704. trueppm_api/apps/workspace/urls.py +113 -0
  705. trueppm_api/apps/workspace/views.py +1502 -0
  706. trueppm_api/asgi.py +42 -0
  707. trueppm_api/celery.py +31 -0
  708. trueppm_api/core/__init__.py +0 -0
  709. trueppm_api/core/admin_site.py +206 -0
  710. trueppm_api/core/auth_views.py +916 -0
  711. trueppm_api/core/constant_time.py +51 -0
  712. trueppm_api/core/csp.py +54 -0
  713. trueppm_api/core/db.py +87 -0
  714. trueppm_api/core/db_session.py +37 -0
  715. trueppm_api/core/exception_handlers.py +157 -0
  716. trueppm_api/core/export_downloads.py +56 -0
  717. trueppm_api/core/extension_providers.py +56 -0
  718. trueppm_api/core/extension_signals.py +147 -0
  719. trueppm_api/core/idempotent.py +260 -0
  720. trueppm_api/core/metadata.py +42 -0
  721. trueppm_api/core/middleware.py +51 -0
  722. trueppm_api/core/openapi.py +949 -0
  723. trueppm_api/core/password_reset.py +500 -0
  724. trueppm_api/core/protect_conflict.py +100 -0
  725. trueppm_api/core/ratelimit.py +148 -0
  726. trueppm_api/core/redis_throttle.py +82 -0
  727. trueppm_api/core/refresh_cookie_policy.py +156 -0
  728. trueppm_api/core/request_body.py +86 -0
  729. trueppm_api/core/security_checks.py +884 -0
  730. trueppm_api/core/storage_config.py +105 -0
  731. trueppm_api/core/text_decode.py +168 -0
  732. trueppm_api/core/throttling.py +344 -0
  733. trueppm_api/core/valkey.py +214 -0
  734. trueppm_api/core/valkey_cache.py +66 -0
  735. trueppm_api/core/valkey_checks.py +104 -0
  736. trueppm_api/core/valkey_config.py +39 -0
  737. trueppm_api/core/worker_broker_wait.py +149 -0
  738. trueppm_api/core/worker_heartbeat.py +134 -0
  739. trueppm_api/fields.py +36 -0
  740. trueppm_api/permissions.py +30 -0
  741. trueppm_api/py.typed +0 -0
  742. trueppm_api/routing.py +11 -0
  743. trueppm_api/settings/__init__.py +0 -0
  744. trueppm_api/settings/base.py +2455 -0
  745. trueppm_api/settings/dev.py +273 -0
  746. trueppm_api/settings/prod.py +228 -0
  747. trueppm_api/urls.py +153 -0
  748. trueppm_api/workflows/__init__.py +38 -0
  749. trueppm_api/workflows/backends/__init__.py +6 -0
  750. trueppm_api/workflows/backends/default.py +297 -0
  751. trueppm_api/workflows/consumers/__init__.py +11 -0
  752. trueppm_api/workflows/consumers/requeue_failed_task.py +65 -0
  753. trueppm_api/workflows/interface.py +148 -0
  754. trueppm_api/workflows/registry.py +106 -0
  755. trueppm_api/workflows/services.py +90 -0
  756. trueppm_api/wsgi.py +11 -0
  757. trueppm_api-0.4.0b2.dist-info/METADATA +70 -0
  758. trueppm_api-0.4.0b2.dist-info/RECORD +759 -0
  759. trueppm_api-0.4.0b2.dist-info/WHEEL +4 -0
@@ -0,0 +1,1149 @@
1
+ """Validate a TruePPM JSON seed document (ADR-0109, issue #614).
2
+
3
+ ``validate_seed`` is a pure, side-effect-free function: it checks a parsed
4
+ payload against the bundled JSON Schema (draft 2020-12) and then runs a
5
+ referential-integrity pass that JSON Schema cannot express — duplicate
6
+ file-local slugs and dangling cross-references (an assignee that names no
7
+ account, a dependency edge to a task that does not exist, and so on).
8
+
9
+ Both the structural and referential phases collect *all* failures before
10
+ raising, so a seed author fixing a hand-written file sees every problem at
11
+ once rather than one-per-run. Every error is anchored to a JSON path so the
12
+ offending location is obvious.
13
+
14
+ Two entry points share one implementation:
15
+
16
+ ``inspect_seed``
17
+ Pure and total — never raises for a bad document. Returns a
18
+ :class:`SeedReport` carrying every diagnostic plus what the file *claims*
19
+ to be (version, program slug, entity counts). This is what the dry-run
20
+ endpoint and ``manage.py import_seed --check`` call (#2418).
21
+ ``validate_seed``
22
+ The original raise-on-invalid contract the importer depends on, now a thin
23
+ wrapper over ``inspect_seed``. Its behavior is unchanged in the only way
24
+ that matters: a document that failed before still fails, with at least the
25
+ same diagnostics.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import json
31
+ from dataclasses import dataclass
32
+ from functools import lru_cache
33
+ from pathlib import Path
34
+ from typing import Any
35
+
36
+ from jsonschema import Draft202012Validator, FormatChecker
37
+
38
+ # Schema majors this validator understands. The major component is the
39
+ # compatibility boundary: a payload whose major is unknown is rejected outright.
40
+ # v2 (ADR-0114) is an additive superset of v1 — relative dates + an events
41
+ # timeline — so both load through their own bundled schema. See ADR-0109/0114.
42
+ SUPPORTED_MAJORS = ("1", "2")
43
+
44
+ # Aggregate ceiling on materializable entities across the whole document.
45
+ # Per-array maxItems in the schema bound each collection; this bounds the
46
+ # *total* — a single import must not be able to create an unbounded number of
47
+ # rows in one transaction. Generous: the largest bundled sample is a few hundred
48
+ # entities.
49
+ MAX_SEED_NODES = 100_000
50
+
51
+ # Longest a single diagnostic may be. jsonschema renders the *whole offending
52
+ # instance* into ``ValidationError.message`` for container-level keywords, so a
53
+ # ``maxItems`` violation on ``projects`` echoes every project back at the caller
54
+ # — a 201-project document produces a 10 KB line, and a document padded to
55
+ # SEED_MAX_UPLOAD_MB produces megabytes, in a 400 whose body the caller chose
56
+ # the size of (#2615). The path prefix and the keyword both survive truncation,
57
+ # which is the part a caller can act on.
58
+ MAX_SEED_ERROR_CHARS = 300
59
+
60
+ _SCHEMAS_DIR = Path(__file__).resolve().parent.parent / "schemas"
61
+ _SCHEMA_PATH_BY_MAJOR = {
62
+ "1": _SCHEMAS_DIR / "seed_v1.json",
63
+ "2": _SCHEMAS_DIR / "seed_v2.json",
64
+ }
65
+
66
+
67
+ class SeedValidationError(ValueError):
68
+ """Raised when a seed document fails structural or referential validation.
69
+
70
+ ``errors`` holds every individual problem (JSON-path anchored) so callers —
71
+ the import endpoint (#615) in particular — can surface a complete report
72
+ rather than a single message.
73
+ """
74
+
75
+ def __init__(self, errors: list[str]) -> None:
76
+ self.errors = errors
77
+ count = len(errors)
78
+ noun = "error" if count == 1 else "errors"
79
+ super().__init__(f"Seed validation failed with {count} {noun}:\n" + "\n".join(errors))
80
+
81
+
82
+ @lru_cache(maxsize=len(_SCHEMA_PATH_BY_MAJOR))
83
+ def _validator(major: str) -> Draft202012Validator:
84
+ """Build (once per major) the schema validator with format checking on.
85
+
86
+ ``format`` is an annotation in JSON Schema by default; we opt into checking
87
+ it so ``"2026-13-40"`` in a ``planned_start`` is caught here rather than
88
+ blowing up later at ``date.fromisoformat`` time during import.
89
+ """
90
+ schema = json.loads(_SCHEMA_PATH_BY_MAJOR[major].read_text(encoding="utf-8"))
91
+ return Draft202012Validator(schema, format_checker=FormatChecker())
92
+
93
+
94
+ def _json_path(absolute_path: Any) -> str:
95
+ """Render a jsonschema error path as ``$.projects[0].tasks[2].wbs_path``."""
96
+ out = "$"
97
+ for part in absolute_path:
98
+ out += f"[{part}]" if isinstance(part, int) else f".{part}"
99
+ return out
100
+
101
+
102
+ @dataclass(frozen=True)
103
+ class SeedReport:
104
+ """The full result of inspecting a seed document, without importing it.
105
+
106
+ ``errors`` is the same JSON-path-anchored list :class:`SeedValidationError`
107
+ carries. The remaining fields echo back what the *document claims to be* so
108
+ an operator running a dry run can confirm they grabbed the right file
109
+ before pointing a wipe-then-recreate import at a live program slug
110
+ (ADR-0109). They are read defensively: a malformed document still produces
111
+ a report, with ``None``/``0`` where a claim could not be read.
112
+ """
113
+
114
+ valid: bool
115
+ errors: list[str]
116
+ schema_version: str | None
117
+ program_slug: str | None
118
+ program_name: str | None
119
+ project_count: int
120
+ task_count: int
121
+ resource_count: int
122
+
123
+
124
+ def inspect_seed(payload: Any, *, allow_sample_sections: bool = False) -> SeedReport:
125
+ """Inspect a parsed seed document and report every problem found.
126
+
127
+ Total by construction — a document this rejects still yields a report
128
+ rather than an exception, because the dry-run caller needs the diagnostics
129
+ precisely when the document is bad (#2418).
130
+
131
+ Args:
132
+ payload: the already-parsed JSON, not a raw string. Any type is
133
+ accepted; a non-object is itself reported as a diagnostic.
134
+ allow_sample_sections: accept the sections that create audit evidence or
135
+ credentials (see :func:`_sample_only_errors`). Only the bundled-sample
136
+ path may pass ``True``; the default rejects them, so a new caller is
137
+ fail-closed.
138
+
139
+ Returns:
140
+ A :class:`SeedReport` whose ``valid`` is ``True`` only when ``errors``
141
+ is empty.
142
+ """
143
+ if not isinstance(payload, dict):
144
+ return SeedReport(
145
+ valid=False,
146
+ errors=["$: seed document must be a JSON object"],
147
+ schema_version=None,
148
+ program_slug=None,
149
+ program_name=None,
150
+ project_count=0,
151
+ task_count=0,
152
+ resource_count=0,
153
+ )
154
+
155
+ errors = _document_errors(payload)
156
+ if not allow_sample_sections:
157
+ errors = _sample_only_errors(payload) + errors
158
+ return SeedReport(
159
+ valid=not errors,
160
+ errors=errors,
161
+ schema_version=payload.get("schema_version")
162
+ if isinstance(payload.get("schema_version"), str)
163
+ else None,
164
+ **_claimed_shape(payload),
165
+ )
166
+
167
+
168
+ def _claimed_shape(payload: dict[str, Any]) -> dict[str, Any]:
169
+ """Read the program identity + entity counts a document claims to carry.
170
+
171
+ Every access is defensive: this runs on documents that have already failed
172
+ validation, where ``program`` may be absent or the wrong type entirely.
173
+ """
174
+ program = payload.get("program")
175
+ program = program if isinstance(program, dict) else {}
176
+ projects = payload.get("projects")
177
+ projects = projects if isinstance(projects, list) else []
178
+ resources = payload.get("resources")
179
+ resources = resources if isinstance(resources, list) else []
180
+
181
+ task_count = 0
182
+ for project in projects:
183
+ if isinstance(project, dict) and isinstance(project.get("tasks"), list):
184
+ task_count += len(project["tasks"])
185
+
186
+ return {
187
+ "program_slug": program.get("slug") if isinstance(program.get("slug"), str) else None,
188
+ "program_name": program.get("name") if isinstance(program.get("name"), str) else None,
189
+ "project_count": len(projects),
190
+ "task_count": task_count,
191
+ "resource_count": len(resources),
192
+ }
193
+
194
+
195
+ def _document_errors(payload: dict[str, Any]) -> list[str]:
196
+ """Collect every diagnostic for an object-shaped document.
197
+
198
+ Version handling is the one place the passes are *not* purely additive, and
199
+ the two failure modes are deliberately asymmetric (#2418):
200
+
201
+ - **Missing** ``schema_version`` — the version is reported, and the
202
+ structural pass still runs against the newest supported schema. v2 is an
203
+ additive superset of v1 (ADR-0114), so a v1-shaped document validated
204
+ against v2 still passes structurally; running it is what lets a
205
+ version-less file report its *other* twenty problems in the same pass
206
+ instead of hiding them behind one line. The version is injected into a
207
+ shallow copy for that pass only — both bundled schemas list
208
+ ``schema_version`` as required, so without the injection the schema would
209
+ re-report the same problem in less specific language.
210
+ - **Unsupported** major — reported, and the structural pass is *skipped*.
211
+ Here there is no defensible schema to substitute: checking a 3.x document
212
+ against the v2 schema would bury the one diagnostic that matters under a
213
+ wall of misleading ones.
214
+ """
215
+ version = payload.get("schema_version")
216
+ if version is None:
217
+ latest = SUPPORTED_MAJORS[-1]
218
+ return [
219
+ "$.schema_version: required and missing",
220
+ *_schema_errors({**payload, "schema_version": f"{latest}.0"}, latest),
221
+ ]
222
+
223
+ major = version.split(".")[0] if isinstance(version, str) else None
224
+ if major not in _SCHEMA_PATH_BY_MAJOR:
225
+ supported = ", ".join(f"{m}.x" for m in SUPPORTED_MAJORS)
226
+ return [
227
+ f"$.schema_version: unsupported version {version!r}; this build supports {supported}"
228
+ ]
229
+
230
+ return _schema_errors(payload, major)
231
+
232
+
233
+ def _bounded_message(message: str) -> str:
234
+ """Clamp one jsonschema diagnostic to roughly ``MAX_SEED_ERROR_CHARS``.
235
+
236
+ Truncated in the **middle**, not at the end. jsonschema renders these as
237
+ ``<instance> is too long`` / ``<instance> has too many items`` — the
238
+ offending value first and the *verdict* last — so clipping the tail keeps
239
+ the megabytes and throws away the only part that says what is wrong. Middle
240
+ truncation keeps both ends: enough of the value to recognize it, and the
241
+ phrase naming the violated keyword.
242
+ """
243
+ if len(message) <= MAX_SEED_ERROR_CHARS:
244
+ return message
245
+ head = MAX_SEED_ERROR_CHARS * 2 // 3
246
+ tail = MAX_SEED_ERROR_CHARS - head
247
+ return f"{message[:head]} …(truncated)… {message[-tail:]}"
248
+
249
+
250
+ def _schema_errors(payload: dict[str, Any], major: str) -> list[str]:
251
+ """Run the structural pass, then the semantic passes it is a precondition for."""
252
+ errors: list[str] = [
253
+ f"{_json_path(e.absolute_path)}: {_bounded_message(e.message)}"
254
+ for e in sorted(_validator(major).iter_errors(payload), key=lambda e: list(e.absolute_path))
255
+ ]
256
+
257
+ # Referential integrity only runs when the document is structurally sound —
258
+ # the cross-reference walk assumes well-typed slugs and arrays.
259
+ if not errors:
260
+ errors.extend(_referential_errors(payload))
261
+ errors.extend(_node_budget_errors(payload))
262
+ if major == "2":
263
+ errors.extend(_event_errors(payload))
264
+ errors.extend(_v21_event_errors(payload))
265
+
266
+ return errors
267
+
268
+
269
+ def validate_seed(payload: Any, *, allow_sample_sections: bool = False) -> None:
270
+ """Validate a parsed seed document. Returns ``None`` on success.
271
+
272
+ Args:
273
+ payload: the already-parsed JSON (a ``dict``), not a raw string.
274
+ allow_sample_sections: see :func:`inspect_seed`. Default ``False``.
275
+
276
+ Raises:
277
+ SeedValidationError: with one message per problem found.
278
+ """
279
+ report = inspect_seed(payload, allow_sample_sections=allow_sample_sections)
280
+ if not report.valid:
281
+ raise SeedValidationError(report.errors)
282
+
283
+
284
+ # Restated, not imported: this module must stay importable without Django (the
285
+ # fixture build scripts in scripts/seeds call validate_seed with no settings), so it
286
+ # cannot reach projects.models. test_seed_v21 pins this copy to
287
+ # ``RESERVED_SCRUM_CEREMONY_NAMES``, and the schema's reaction ``emoji`` enum to
288
+ # ``ALLOWED_REACTION_EMOJI``, so neither can drift silently.
289
+ RESERVED_CEREMONY_NAMES = frozenset(
290
+ {
291
+ "sprint planning",
292
+ "sprint review",
293
+ "sprint retrospective",
294
+ "retrospective",
295
+ "retro",
296
+ "daily scrum",
297
+ "standup",
298
+ "daily standup",
299
+ "scrum of scrums",
300
+ }
301
+ )
302
+
303
+
304
+ def _sample_only_errors(payload: Any) -> list[str]:
305
+ """Reject the sections only a bundled sample may carry (#3603).
306
+
307
+ ``agent_actions`` writes rows into the instance's hash-chained audit log and
308
+ ``share_links`` mints public bearer credentials. A user who can import a file
309
+ must be able to do neither: the first would forge audit history, the second
310
+ would publish a board. Rejected rather than dropped, because a dropped section
311
+ reports success for a document that did not import as written.
312
+
313
+ Reads defensively — this runs even on a document that fails the schema, so the
314
+ one diagnostic that explains *why* the section is refused is never hidden.
315
+ """
316
+ if not isinstance(payload, dict):
317
+ return []
318
+ errors: list[str] = []
319
+ program = payload.get("program")
320
+ if isinstance(program, dict) and "agent_actions" in program:
321
+ errors.append(
322
+ "$.program.agent_actions: only bundled sample programs may carry agent "
323
+ "actions — audit evidence cannot be imported from a file"
324
+ )
325
+ projects = payload.get("projects")
326
+ if isinstance(projects, list):
327
+ for i, project in enumerate(projects):
328
+ if isinstance(project, dict) and "share_links" in project:
329
+ errors.append(
330
+ f"$.projects[{i}].share_links: only bundled sample programs may carry "
331
+ "share links — a public access credential cannot be imported from a file"
332
+ )
333
+ return errors
334
+
335
+
336
+ def _v21_reference_errors(payload: dict[str, Any], ctx: _RefContext, errors: list[str]) -> None:
337
+ """Cross-references for the v2.1 program/project/task sections (#3603)."""
338
+ program = payload.get("program", {})
339
+ _v21_depth_errors(payload, ctx, errors)
340
+ _check_backlog_items(program.get("backlog_items", []), ctx, errors)
341
+ _check_ceremonies(program.get("ceremonies", []), errors)
342
+ _check_agent_actions(program.get("agent_actions", []), ctx, errors)
343
+ for i, project in enumerate(payload.get("projects", [])):
344
+ pbase = f"$.projects[{i}]"
345
+ for k, link in enumerate(project.get("share_links", [])):
346
+ _check_ref(
347
+ link.get("created_by"),
348
+ ctx.account_slugs,
349
+ f"{pbase}.share_links[{k}].created_by",
350
+ "account",
351
+ errors,
352
+ )
353
+ for j, task in enumerate(project.get("tasks", [])):
354
+ for k, criterion in enumerate(task.get("acceptance_criteria", [])):
355
+ _check_ref(
356
+ criterion.get("met_by"),
357
+ ctx.account_slugs,
358
+ f"{pbase}.tasks[{j}].acceptance_criteria[{k}].met_by",
359
+ "account",
360
+ errors,
361
+ )
362
+
363
+
364
+ _WEEKDAY_BITS = {"mon": 1, "tue": 2, "wed": 4, "thu": 8, "fri": 16, "sat": 32, "sun": 64}
365
+
366
+
367
+ def _v21_depth_errors(payload: dict[str, Any], ctx: _RefContext, errors: list[str]) -> None:
368
+ """Team facets, drawer subtasks and recurrence rules (v2.1, #3498).
369
+
370
+ Mirrors the rules the API enforces on the same writes, so a seed cannot express a
371
+ team, a subtask tree or a recurrence the product would refuse to create.
372
+ """
373
+ accounts_with_role = {
374
+ a.get("slug", "") for a in payload.get("accounts", []) if a.get("role") is not None
375
+ }
376
+ for i, project in enumerate(payload.get("projects", [])):
377
+ base = f"$.projects[{i}]"
378
+ if "members" in project:
379
+ members = {m.get("account", "") for m in project.get("members", [])}
380
+ else:
381
+ members = accounts_with_role
382
+ _check_team(project.get("team"), f"{base}.team", members, ctx, errors)
383
+ tasks = project.get("tasks", [])
384
+ by_wbs = {t.get("wbs_path"): t for t in tasks}
385
+ for j, task in enumerate(tasks):
386
+ tpath = f"{base}.tasks[{j}]"
387
+ if task.get("is_subtask"):
388
+ _check_subtask(task, by_wbs, tpath, errors)
389
+ if "recurrence" in task:
390
+ _check_recurrence(task, tpath, errors)
391
+
392
+
393
+ def _check_team(
394
+ team: dict[str, Any] | None,
395
+ base: str,
396
+ project_members: set[str],
397
+ ctx: _RefContext,
398
+ errors: list[str],
399
+ ) -> None:
400
+ """A facet goes to a project member, and each facet has at most one holder.
401
+
402
+ ``TeamMembership`` mirrors ``ProjectMembership`` onto the default team, so a facet
403
+ on a non-member has no row to land on. The one-holder rule is the soft singleton
404
+ the team service enforces on reassignment.
405
+ """
406
+ if not team:
407
+ return
408
+ seen: set[str] = set()
409
+ holders = {"scrum_master": 0, "product_owner": 0}
410
+ for k, member in enumerate(team.get("members", [])):
411
+ mpath = f"{base}.members[{k}]"
412
+ account = member.get("account", "")
413
+ _check_ref(account, ctx.account_slugs, f"{mpath}.account", "account", errors)
414
+ if account in ctx.account_slugs and account not in project_members:
415
+ errors.append(f"{mpath}.account: {account!r} is not a member of this project")
416
+ if account in seen:
417
+ errors.append(f"{mpath}.account: {account!r} is listed twice")
418
+ seen.add(account)
419
+ for facet in holders:
420
+ if member.get(facet):
421
+ holders[facet] += 1
422
+ for facet, count in holders.items():
423
+ if count > 1:
424
+ errors.append(f"{base}.members: a team has at most one {facet}, found {count}")
425
+
426
+
427
+ def _check_subtask(
428
+ task: dict[str, Any], by_wbs: dict[Any, dict[str, Any]], tpath: str, errors: list[str]
429
+ ) -> None:
430
+ """Drawer subtasks are depth-1 leaves under a decomposable task (ADR-0060, #1750)."""
431
+ wbs = str(task.get("wbs_path", ""))
432
+ parent_path, _, _ = wbs.rpartition(".")
433
+ parent = by_wbs.get(parent_path) if parent_path else None
434
+ if parent is None:
435
+ where = parent_path or "?"
436
+ errors.append(f"{tpath}.is_subtask: a subtask needs a parent task at {where!r}")
437
+ return
438
+ if parent.get("is_milestone"):
439
+ errors.append(f"{tpath}.is_subtask: a milestone cannot hold subtasks")
440
+ if parent.get("is_subtask"):
441
+ errors.append(f"{tpath}.is_subtask: a subtask cannot hold subtasks")
442
+ prefix = f"{parent_path}."
443
+ for path, sibling in by_wbs.items():
444
+ child = str(path)
445
+ is_direct_child = child.startswith(prefix) and "." not in child[len(prefix) :]
446
+ if is_direct_child and not sibling.get("is_subtask"):
447
+ errors.append(
448
+ f"{tpath}.is_subtask: {parent_path!r} groups structural work, and a phase "
449
+ "cannot also hold subtasks"
450
+ )
451
+ break
452
+ if any(str(path).startswith(f"{wbs}.") for path in by_wbs):
453
+ errors.append(f"{tpath}.is_subtask: a subtask cannot have children")
454
+
455
+
456
+ def _check_recurrence(task: dict[str, Any], tpath: str, errors: list[str]) -> None:
457
+ """Mirror ``TaskRecurrenceRule.clean()`` plus the template constraints."""
458
+ rule = task.get("recurrence") or {}
459
+ rpath = f"{tpath}.recurrence"
460
+ frequency = rule.get("frequency")
461
+ if frequency == "WEEKLY" and not rule.get("weekdays"):
462
+ errors.append(f"{rpath}.weekdays: weekly recurrence requires at least one weekday")
463
+ if frequency == "MONTHLY" and rule.get("day_of_month") is None:
464
+ errors.append(f"{rpath}.day_of_month: monthly recurrence requires a day of month")
465
+ if "end_date" in rule and "end_count" in rule:
466
+ errors.append(f"{rpath}: give end_date or end_count, not both")
467
+ if task.get("is_milestone") or task.get("is_subtask"):
468
+ errors.append(f"{rpath}: a milestone or a subtask cannot be a recurrence template")
469
+
470
+
471
+ def _check_backlog_items(items: list[dict[str, Any]], ctx: _RefContext, errors: list[str]) -> None:
472
+ """A pulled item names exactly the one task it became (``pulled_task`` is 1:1)."""
473
+ _collect_slugs(items, "$.program.backlog_items", errors)
474
+ pulled: set[str] = set()
475
+ for i, item in enumerate(items):
476
+ base = f"$.program.backlog_items[{i}]"
477
+ for key in ("pulled_by", "created_by"):
478
+ _check_ref(item.get(key), ctx.account_slugs, f"{base}.{key}", "account", errors)
479
+ status = item.get("status", "proposed")
480
+ target = item.get("pulled_to")
481
+ if target is None:
482
+ if status == "pulled":
483
+ errors.append(f"{base}.pulled_to: a pulled item must name the task it became")
484
+ continue
485
+ if status != "pulled":
486
+ errors.append(
487
+ f"{base}.pulled_to: only a pulled item may name a task (status {status!r})"
488
+ )
489
+ _check_task_ref(target, "", ctx.task_index, f"{base}.pulled_to", errors)
490
+ if target in pulled:
491
+ errors.append(f"{base}.pulled_to: task {target!r} is already another item's pull")
492
+ pulled.add(target)
493
+
494
+
495
+ def _check_ceremonies(items: list[dict[str, Any]], errors: list[str]) -> None:
496
+ """Mirror ``CeremonyTemplateSerializer``: no sprint events, unique names, a clock anchor."""
497
+ seen: set[str] = set()
498
+ for i, ceremony in enumerate(items):
499
+ base = f"$.program.ceremonies[{i}]"
500
+ name = str(ceremony.get("name", "")).strip().casefold()
501
+ if name in RESERVED_CEREMONY_NAMES:
502
+ errors.append(
503
+ f"{base}.name: {ceremony.get('name')!r} is a team-level sprint event, "
504
+ "not a program ceremony (ADR-0079)"
505
+ )
506
+ if name in seen:
507
+ errors.append(f"{base}.name: duplicate ceremony name {ceremony.get('name')!r}")
508
+ seen.add(name)
509
+ cadence = ceremony.get("cadence_type")
510
+ if cadence != "on_milestone":
511
+ for key in ("cadence_day", "cadence_time"):
512
+ if not ceremony.get(key):
513
+ errors.append(f"{base}.{key}: required for a {cadence} cadence")
514
+
515
+
516
+ def _check_agent_actions(items: list[dict[str, Any]], ctx: _RefContext, errors: list[str]) -> None:
517
+ """Agent actions resolve to real accounts/projects/tasks; only refusals carry a refusal."""
518
+ for i, action in enumerate(items):
519
+ base = f"$.program.agent_actions[{i}]"
520
+ _check_ref(
521
+ action.get("principal"), ctx.account_slugs, f"{base}.principal", "account", errors
522
+ )
523
+ _check_ref(action.get("project"), ctx.project_slugs, f"{base}.project", "project", errors)
524
+ _check_task_ref(action.get("object"), "", ctx.task_index, f"{base}.object", errors)
525
+ if action.get("verdict") != "refused":
526
+ for key in ("refusal_reason", "refusal_constraint"):
527
+ if key in action:
528
+ errors.append(f"{base}.{key}: only a refused action carries a refusal")
529
+
530
+
531
+ _ACTION_TIME_LOG = "time.log"
532
+ _ACTION_TASK_REACT = "task.react"
533
+ _ACTION_TASK_ACK = "task.ack"
534
+ _ACTION_TASK_COMMENT = "task.comment"
535
+
536
+ # Actions whose attribution is the whole fact: a time entry, a reaction or an
537
+ # acknowledgement by nobody in particular is not something to fall back to the
538
+ # importing owner for, so each must name its actor.
539
+ _ACTOR_REQUIRED = frozenset({_ACTION_TIME_LOG, _ACTION_TASK_REACT, _ACTION_TASK_ACK})
540
+
541
+
542
+ def _v21_event_errors(payload: dict[str, Any]) -> list[str]:
543
+ """Comment threading, comment-targeted beats, criterion ticks and time beats (#3603)."""
544
+ errors: list[str] = []
545
+ events = payload.get("events", [])
546
+ # Top-level comment slug -> the task target it was posted on, so a reply can be
547
+ # held to the same task. Replies keep only their slug: nothing may answer them.
548
+ roots: dict[str, str] = {}
549
+ replies: set[str] = set()
550
+ for i, event in enumerate(events):
551
+ if event.get("action") != _ACTION_TASK_COMMENT or "slug" not in event:
552
+ continue
553
+ slug = event["slug"]
554
+ if slug in roots or slug in replies:
555
+ errors.append(f"$.events[{i}].slug: duplicate comment slug {slug!r}")
556
+ continue
557
+ if event.get("reply_to"):
558
+ replies.add(slug)
559
+ else:
560
+ roots[slug] = str(event.get("target", ""))
561
+
562
+ criteria_count = {
563
+ (project.get("slug", ""), task.get("wbs_path")): len(task.get("acceptance_criteria", []))
564
+ for project in payload.get("projects", [])
565
+ for task in project.get("tasks", [])
566
+ }
567
+
568
+ for i, event in enumerate(events):
569
+ base = f"$.events[{i}]"
570
+ action = event.get("action")
571
+ _check_reply_to(event, action, base, roots, replies, errors)
572
+ if action in (_ACTION_TASK_REACT, _ACTION_TASK_ACK):
573
+ _, _, slug = str(event.get("target", "")).partition(":")
574
+ if slug and slug not in roots and slug not in replies:
575
+ errors.append(f"{base}.target: no comment with slug {slug!r}")
576
+ if action in _ACTOR_REQUIRED and not event.get("actor"):
577
+ errors.append(f"{base}.actor: {action} must name the account it is attributed to")
578
+ if action == _ACTION_TASK_REACT and "emoji" not in event:
579
+ errors.append(f"{base}.emoji: task.react requires an emoji")
580
+ if action == "task.note" and not event.get("body"):
581
+ errors.append(f"{base}.body: task.note requires a body")
582
+ if action == _ACTION_TIME_LOG:
583
+ _check_time_log(event, base, errors)
584
+ if "criterion" in event:
585
+ _check_criterion(event, action, base, criteria_count, errors)
586
+ return errors
587
+
588
+
589
+ def _check_reply_to(
590
+ event: dict[str, Any],
591
+ action: Any,
592
+ base: str,
593
+ roots: dict[str, str],
594
+ replies: set[str],
595
+ errors: list[str],
596
+ ) -> None:
597
+ """Replies are one level deep and stay on their thread's task, as the API enforces."""
598
+ reply_to = event.get("reply_to")
599
+ if reply_to is None:
600
+ return
601
+ if action != _ACTION_TASK_COMMENT:
602
+ errors.append(f"{base}.reply_to: only task.comment may reply to a comment")
603
+ elif reply_to in replies:
604
+ errors.append(
605
+ f"{base}.reply_to: {reply_to!r} is itself a reply; replies are one level deep"
606
+ )
607
+ elif reply_to not in roots:
608
+ errors.append(f"{base}.reply_to: no comment with slug {reply_to!r}")
609
+ elif roots[reply_to] != event.get("target"):
610
+ errors.append(
611
+ f"{base}.reply_to: comment {reply_to!r} is on {roots[reply_to]!r}, "
612
+ "and a reply must be posted on the same task"
613
+ )
614
+
615
+
616
+ def _check_time_log(event: dict[str, Any], base: str, errors: list[str]) -> None:
617
+ """Logged time needs minutes and cannot be dated after the anchor ("today")."""
618
+ if "minutes" not in event:
619
+ errors.append(f"{base}.minutes: time.log requires minutes")
620
+ at = str(event.get("at", ""))
621
+ if at.startswith("A+"):
622
+ digits = at[2:].split("T")[0]
623
+ if digits.isdigit() and int(digits) > 0:
624
+ errors.append(
625
+ f"{base}.at: time.log cannot be forward-dated ({at!r} is after the anchor)"
626
+ )
627
+
628
+
629
+ def _check_criterion(
630
+ event: dict[str, Any],
631
+ action: Any,
632
+ base: str,
633
+ criteria_count: dict[tuple[str, Any], int],
634
+ errors: list[str],
635
+ ) -> None:
636
+ """A ``criterion`` index must address a criterion the target task declares."""
637
+ if action != "task.ac_met":
638
+ errors.append(f"{base}.criterion: only task.ac_met may tick a criterion")
639
+ return
640
+ _, _, ref = str(event.get("target", "")).partition(":")
641
+ project_slug, _, wbs = ref.partition(":")
642
+ count = criteria_count.get((project_slug, wbs))
643
+ if count is not None and event["criterion"] >= count:
644
+ errors.append(
645
+ f"{base}.criterion: task {ref!r} declares {count} acceptance criteria, "
646
+ f"so index {event['criterion']} does not exist"
647
+ )
648
+
649
+
650
+ # Which target kind each event action addresses. Every replayed action carries a
651
+ # target; an unknown action (should never reach here — the schema enum gates it)
652
+ # is treated as target-optional so validation degrades gracefully.
653
+ _EVENT_TARGET_KIND = {
654
+ "task.note": "task",
655
+ _ACTION_TIME_LOG: "task",
656
+ # A reaction/acknowledgement lands on a comment, addressed by the slug a
657
+ # task.comment beat declared. Resolution is checked in _v21_event_errors.
658
+ _ACTION_TASK_REACT: "comment",
659
+ _ACTION_TASK_ACK: "comment",
660
+ "task.status": "task",
661
+ "task.assign": "task",
662
+ "task.estimate": "task",
663
+ "task.points": "task",
664
+ _ACTION_TASK_COMMENT: "task",
665
+ "task.ac_met": "task",
666
+ "task.block": "task",
667
+ "task.unblock": "task",
668
+ "sprint.activate": "sprint",
669
+ "sprint.close": "sprint",
670
+ # Scope inject/resolve target the task being injected; its ACTIVE sprint is
671
+ # derived from task.sprint, matching record_sprint_scope_change(task, sprint).
672
+ # A re-imported export can name a task that has since left the sprint (a
673
+ # close carried it to the backlog), so replay falls back to the sprint
674
+ # running at the beat's own instant — see replay._injected_sprint (#3488).
675
+ "sprint.scope_inject": "task",
676
+ "sprint.scope_resolve": "task",
677
+ "baseline.capture": "project",
678
+ "risk.status": "risk",
679
+ "risk.note": "risk",
680
+ # retro.action creates an action item on the sprint's retro; retro.promote
681
+ # promotes one (matched by `body`) to a backlog task. Both are per-sprint —
682
+ # SprintRetro is 1:1 with Sprint — so the target is the sprint (ADR-0114 §7).
683
+ "retro.action": "sprint",
684
+ "retro.promote": "sprint",
685
+ }
686
+
687
+
688
+ def _event_errors(payload: dict[str, Any]) -> list[str]:
689
+ """Validate v2 event actor + target references against the document (ADR-0114).
690
+
691
+ Structural shape is already enforced by the v2 JSON Schema; this pass checks
692
+ that every ``actor`` names an account and every ``target`` resolves to a
693
+ task / sprint / project / risk that the same document defines.
694
+ """
695
+ errors: list[str] = []
696
+ account_slugs = {a.get("slug", "") for a in payload.get("accounts", [])}
697
+ project_slugs, task_index, sprint_index, risk_slugs = _event_indexes(payload)
698
+
699
+ for i, event in enumerate(payload.get("events", [])):
700
+ base = f"$.events[{i}]"
701
+ _check_ref(event.get("actor"), account_slugs, f"{base}.actor", "account", errors)
702
+ _check_ref(event.get("assignee"), account_slugs, f"{base}.assignee", "account", errors)
703
+ _check_event_target_ref(
704
+ event, base, project_slugs, task_index, sprint_index, risk_slugs, errors
705
+ )
706
+
707
+ return errors
708
+
709
+
710
+ def _event_indexes(
711
+ payload: dict[str, Any],
712
+ ) -> tuple[set[str], dict[str, set[str]], dict[str, set[str]], set[str]]:
713
+ """Index project slugs, task/sprint paths, and risk slugs for event refs."""
714
+ project_slugs: set[str] = set()
715
+ task_index: dict[str, set[str]] = {}
716
+ sprint_index: dict[str, set[str]] = {}
717
+ risk_slugs: set[str] = {r.get("slug", "") for r in payload.get("risks", [])}
718
+ for project in payload.get("projects", []):
719
+ slug = project.get("slug", "")
720
+ project_slugs.add(slug)
721
+ task_index[slug] = {t.get("wbs_path") for t in project.get("tasks", [])}
722
+ sprint_index[slug] = {s.get("slug") for s in project.get("sprints", [])}
723
+ risk_slugs |= {r.get("slug", "") for r in project.get("risks", [])}
724
+ return project_slugs, task_index, sprint_index, risk_slugs
725
+
726
+
727
+ def _check_event_target_ref(
728
+ event: dict[str, Any],
729
+ base: str,
730
+ project_slugs: set[str],
731
+ task_index: dict[str, set[str]],
732
+ sprint_index: dict[str, set[str]],
733
+ risk_slugs: set[str],
734
+ errors: list[str],
735
+ ) -> None:
736
+ """Validate one event's ``target`` (kind/prefix/resolution) and estimate order."""
737
+ action = event.get("action", "")
738
+ kind = _EVENT_TARGET_KIND.get(action)
739
+ target = event.get("target")
740
+ if kind is None:
741
+ return # unknown action (schema enum should have rejected it)
742
+ if target is None:
743
+ errors.append(f"{base}.target: action {action!r} requires a target")
744
+ return
745
+ prefix, _, ref = target.partition(":")
746
+ if prefix != kind:
747
+ errors.append(f"{base}.target: action {action!r} expects a {kind!r} target, got {target!r}")
748
+ return
749
+ _check_event_target(
750
+ kind, ref, base, project_slugs, task_index, sprint_index, risk_slugs, errors
751
+ )
752
+ # A task.estimate event re-points a triple mid-timeline; it must stay ordered,
753
+ # or replay persists a triple the DB CheckConstraint rejects (#2005). Only a
754
+ # target that passed the checks above reaches this point, matching the original
755
+ # sequential flow (a bad target short-circuits before the estimate check).
756
+ if action == "task.estimate":
757
+ _check_estimate_order(event, base, errors)
758
+
759
+
760
+ def _check_event_target(
761
+ kind: str,
762
+ ref: str,
763
+ base: str,
764
+ project_slugs: set[str],
765
+ task_index: dict[str, set[str]],
766
+ sprint_index: dict[str, set[str]],
767
+ risk_slugs: set[str],
768
+ errors: list[str],
769
+ ) -> None:
770
+ """Resolve one event target ref of a given kind against the document."""
771
+ if kind == "task":
772
+ # Event task refs must be project-qualified (events are global/ordered,
773
+ # there is no enclosing project to fall back to).
774
+ if ":" not in ref:
775
+ errors.append(f'{base}.target: task ref {ref!r} must be "<project-slug>:<wbs-path>"')
776
+ return
777
+ _check_task_ref(ref, "", task_index, f"{base}.target", errors)
778
+ elif kind == "sprint":
779
+ project_slug, _, sprint_slug = ref.partition(":")
780
+ sprints = sprint_index.get(project_slug)
781
+ if sprints is None:
782
+ errors.append(f"{base}.target: no project {project_slug!r} for sprint ref {ref!r}")
783
+ elif sprint_slug not in sprints:
784
+ errors.append(f"{base}.target: no sprint {sprint_slug!r} in project {project_slug!r}")
785
+ elif kind == "project" and ref not in project_slugs:
786
+ errors.append(f"{base}.target: no project with slug {ref!r}")
787
+ elif kind == "risk":
788
+ if ref not in risk_slugs:
789
+ errors.append(f"{base}.target: no risk with slug {ref!r}")
790
+
791
+
792
+ def _node_budget_errors(payload: dict[str, Any]) -> list[str]:
793
+ """Reject documents whose total materializable entity count is excessive.
794
+
795
+ Every collection counted here becomes rows the importer writes inside one
796
+ transaction, so the budget has to include the *containers* too, not only
797
+ their contents (#2615). ``projects`` and ``resources`` were previously
798
+ omitted: a project costs ~5 statements plus a ``cascade_project_soft_delete``
799
+ enqueue on the replace path, and each distinct resource costs ~2 round-trips
800
+ through ``ensure_project_resource`` — both were invisible to the ceiling
801
+ while a document of nothing but empty projects scored zero.
802
+ """
803
+ program = payload.get("program", {})
804
+ total = (
805
+ len(payload.get("projects", []))
806
+ + len(payload.get("resources", []))
807
+ + len(payload.get("accounts", []))
808
+ + len(payload.get("calendars", []))
809
+ + len(payload.get("risks", []))
810
+ # v2.1 program sections (#3603) each materialize one row per entry.
811
+ + len(program.get("backlog_items", []))
812
+ + len(program.get("ceremonies", []))
813
+ + len(program.get("agent_actions", []))
814
+ )
815
+ for project in payload.get("projects", []):
816
+ total += (
817
+ len(project.get("tasks", []))
818
+ + len(project.get("dependencies", []))
819
+ + len(project.get("sprints", []))
820
+ + len(project.get("risks", []))
821
+ + len(project.get("baselines", []))
822
+ + len(project.get("labels", []))
823
+ + len(project.get("share_links", []))
824
+ + sum(len(t.get("acceptance_criteria", [])) for t in project.get("tasks", []))
825
+ # TaskRelation rows materialize like dependencies, so they count
826
+ # toward the per-import ceiling (ADR-0455).
827
+ + sum(len(t.get("links", [])) for t in project.get("tasks", []))
828
+ )
829
+ if total > MAX_SEED_NODES:
830
+ return [f"$: seed too large — {total} entities exceeds the {MAX_SEED_NODES} limit"]
831
+ return []
832
+
833
+
834
+ @dataclass(frozen=True)
835
+ class _RefContext:
836
+ """Global slug catalogs + the task index, resolved once per seed pass.
837
+
838
+ Bundling the document-wide sets (which every per-project and per-task check
839
+ reads but none mutates) keeps the helper signatures small — the alternative
840
+ is threading five ``set`` parameters through every extraction.
841
+ """
842
+
843
+ account_slugs: set[str]
844
+ calendar_slugs: set[str]
845
+ resource_slugs: set[str]
846
+ project_slugs: set[str]
847
+ task_index: dict[str, set[str]]
848
+
849
+
850
+ def _referential_errors(payload: dict[str, Any]) -> list[str]:
851
+ """Check duplicate slugs and dangling cross-references (ADR-0109 identity)."""
852
+ errors: list[str] = []
853
+
854
+ account_slugs = _collect_slugs(payload.get("accounts", []), "$.accounts", errors)
855
+ calendar_slugs = _collect_slugs(payload.get("calendars", []), "$.calendars", errors)
856
+ resource_slugs = _collect_slugs(payload.get("resources", []), "$.resources", errors)
857
+
858
+ projects = payload.get("projects", [])
859
+ project_slugs, task_index = _build_project_index(projects, errors)
860
+ ctx = _RefContext(account_slugs, calendar_slugs, resource_slugs, project_slugs, task_index)
861
+
862
+ # program.lead -> account
863
+ program = payload.get("program", {})
864
+ _check_ref(program.get("lead"), account_slugs, "$.program.lead", "account", errors)
865
+
866
+ # resource.calendar -> calendar, resource.account -> account
867
+ for i, resource in enumerate(payload.get("resources", [])):
868
+ base = f"$.resources[{i}]"
869
+ _check_ref(resource.get("calendar"), calendar_slugs, f"{base}.calendar", "calendar", errors)
870
+ _check_ref(resource.get("account"), account_slugs, f"{base}.account", "account", errors)
871
+
872
+ # program-scoped risks
873
+ _check_risks(
874
+ payload.get("risks", []), "$.risks", None, account_slugs, task_index, project_slugs, errors
875
+ )
876
+
877
+ # per-project references
878
+ for i, project in enumerate(projects):
879
+ _project_reference_errors(i, project, ctx, errors)
880
+
881
+ _v21_reference_errors(payload, ctx, errors)
882
+ return errors
883
+
884
+
885
+ def _build_project_index(
886
+ projects: list[dict[str, Any]], errors: list[str]
887
+ ) -> tuple[set[str], dict[str, set[str]]]:
888
+ """Index project slugs and their wbs paths so task refs resolve anywhere.
889
+
890
+ Builds the global task index (project slug -> set of wbs paths) so a task
891
+ ref — bare or ``<project>:<wbs>`` qualified — can be resolved from any
892
+ scope, and records duplicate project slugs while walking.
893
+ """
894
+ project_slugs: set[str] = set()
895
+ task_index: dict[str, set[str]] = {}
896
+ for i, project in enumerate(projects):
897
+ slug = project.get("slug", "")
898
+ path = f"$.projects[{i}]"
899
+ if slug in project_slugs:
900
+ errors.append(f"{path}.slug: duplicate project slug {slug!r}")
901
+ project_slugs.add(slug)
902
+ task_index[slug] = {t.get("wbs_path") for t in project.get("tasks", [])}
903
+ return project_slugs, task_index
904
+
905
+
906
+ def _project_reference_errors(
907
+ i: int, project: dict[str, Any], ctx: _RefContext, errors: list[str]
908
+ ) -> None:
909
+ """Validate every cross-reference scoped to a single project."""
910
+ slug = project.get("slug", "")
911
+ base = f"$.projects[{i}]"
912
+ _check_ref(project.get("calendar"), ctx.calendar_slugs, f"{base}.calendar", "calendar", errors)
913
+ own_tasks = ctx.task_index.get(slug, set())
914
+
915
+ # members[].account -> accounts[] (#3092). A dangling ref here is silent
916
+ # otherwise: the importer resolves it to None and skips the grant, so the
917
+ # persona simply cannot see the project — the exact failure this key exists
918
+ # to fix, reintroduced by a typo.
919
+ for k, member in enumerate(project.get("members", [])):
920
+ _check_ref(
921
+ member.get("account"),
922
+ ctx.account_slugs,
923
+ f"{base}.members[{k}].account",
924
+ "account",
925
+ errors,
926
+ )
927
+
928
+ sprint_slugs = _collect_slugs(project.get("sprints", []), f"{base}.sprints", errors)
929
+ # Project labels (ADR-0400, #1958): collect the label catalog so each task's
930
+ # label slug refs can be checked against it (dangling label ref).
931
+ label_slugs = _collect_slugs(project.get("labels", []), f"{base}.labels", errors)
932
+ _check_sprint_milestones(project, base, own_tasks, errors)
933
+
934
+ seen_wbs: set[str] = set()
935
+ for j, task in enumerate(project.get("tasks", [])):
936
+ tpath = f"{base}.tasks[{j}]"
937
+ wbs = task.get("wbs_path", "")
938
+ if wbs in seen_wbs:
939
+ errors.append(f"{tpath}.wbs_path: duplicate path {wbs!r} in this project")
940
+ seen_wbs.add(wbs)
941
+ _task_reference_errors(
942
+ task, wbs, tpath, slug, sprint_slugs, label_slugs, own_tasks, ctx, errors
943
+ )
944
+
945
+ _check_dependencies(project, base, slug, ctx.task_index, errors)
946
+ _check_baselines(project, base, own_tasks, errors)
947
+ _check_risks(
948
+ project.get("risks", []),
949
+ f"{base}.risks",
950
+ slug,
951
+ ctx.account_slugs,
952
+ ctx.task_index,
953
+ ctx.project_slugs,
954
+ errors,
955
+ )
956
+
957
+
958
+ def _check_sprint_milestones(
959
+ project: dict[str, Any], base: str, own_tasks: set[str], errors: list[str]
960
+ ) -> None:
961
+ """A sprint's ``target_milestone`` must name a task in the same project."""
962
+ for j, sprint in enumerate(project.get("sprints", [])):
963
+ milestone = sprint.get("target_milestone")
964
+ if milestone is not None and milestone not in own_tasks:
965
+ errors.append(
966
+ f"{base}.sprints[{j}].target_milestone: no task {milestone!r} in this project"
967
+ )
968
+
969
+
970
+ def _task_reference_errors(
971
+ task: dict[str, Any],
972
+ wbs: str,
973
+ tpath: str,
974
+ project_slug: str,
975
+ sprint_slugs: set[str],
976
+ label_slugs: set[str],
977
+ own_tasks: set[str],
978
+ ctx: _RefContext,
979
+ errors: list[str],
980
+ ) -> None:
981
+ """Validate the cross-references a single task carries."""
982
+ _check_ref(task.get("assignee"), ctx.account_slugs, f"{tpath}.assignee", "account", errors)
983
+ _check_ref(task.get("sprint"), sprint_slugs, f"{tpath}.sprint", "sprint", errors)
984
+ parent = task.get("parent_epic")
985
+ if parent is not None and parent not in own_tasks:
986
+ errors.append(f"{tpath}.parent_epic: no task {parent!r} in this project")
987
+ for k, label_ref in enumerate(task.get("labels", [])):
988
+ _check_ref(label_ref, label_slugs, f"{tpath}.labels[{k}]", "label", errors)
989
+ _check_task_links(task, wbs, project_slug, ctx.task_index, tpath, errors)
990
+ _check_estimate_order(task, tpath, errors)
991
+ for k, assignment in enumerate(task.get("assignments", [])):
992
+ _check_ref(
993
+ assignment.get("resource"),
994
+ ctx.resource_slugs,
995
+ f"{tpath}.assignments[{k}].resource",
996
+ "resource",
997
+ errors,
998
+ )
999
+
1000
+
1001
+ def _check_task_links(
1002
+ task: dict[str, Any],
1003
+ wbs: str,
1004
+ project_slug: str,
1005
+ task_index: dict[str, set[str]],
1006
+ tpath: str,
1007
+ errors: list[str],
1008
+ ) -> None:
1009
+ """Validate informational task-to-task relations (ADR-0455).
1010
+
1011
+ The target must resolve to a real task; a bare wbs is enclosing-project, a
1012
+ ``<slug>:<wbs>`` ref a sibling project. Because a seed document is a single
1013
+ program, ``_check_task_ref``'s "no project with slug" error is itself the
1014
+ cross-program guard — a resolvable target is always in the same program as
1015
+ the source (the ADR-0120 D1 envelope). Self-links are rejected here too
1016
+ (inert; the DB CheckConstraint is the backstop).
1017
+ """
1018
+ for k, link in enumerate(task.get("links", [])):
1019
+ lpath = f"{tpath}.links[{k}].target"
1020
+ target = link.get("target")
1021
+ _check_task_ref(target, project_slug, task_index, lpath, errors)
1022
+ if target is not None:
1023
+ if ":" in target:
1024
+ tproj, _, twbs = target.partition(":")
1025
+ else:
1026
+ tproj, twbs = project_slug, target
1027
+ if tproj == project_slug and twbs == wbs:
1028
+ errors.append(f"{lpath}: a task cannot link to itself")
1029
+
1030
+
1031
+ def _check_estimate_order(task: dict[str, Any], tpath: str, errors: list[str]) -> None:
1032
+ """A complete three-point estimate must be ordered.
1033
+
1034
+ This is the same invariant the engine, the REST serializer, and the DB
1035
+ CheckConstraint enforce (#2005). Caught here so a mis-authored seed fails
1036
+ loudly at build/CI time (``test_sample_content`` runs ``validate_seed``)
1037
+ rather than as a runtime IntegrityError on import.
1038
+ """
1039
+ est = task.get("estimate")
1040
+ if isinstance(est, dict):
1041
+ o, m, p = est.get("optimistic"), est.get("most_likely"), est.get("pessimistic")
1042
+ if o is not None and m is not None and p is not None and not (o <= m <= p):
1043
+ errors.append(
1044
+ f"{tpath}.estimate: three-point estimate must satisfy "
1045
+ f"optimistic <= most_likely <= pessimistic (got {o} <= {m} <= {p})"
1046
+ )
1047
+
1048
+
1049
+ def _check_dependencies(
1050
+ project: dict[str, Any],
1051
+ base: str,
1052
+ project_slug: str,
1053
+ task_index: dict[str, set[str]],
1054
+ errors: list[str],
1055
+ ) -> None:
1056
+ """Both endpoints of every dependency edge must resolve to a task."""
1057
+ for j, dep in enumerate(project.get("dependencies", [])):
1058
+ dpath = f"{base}.dependencies[{j}]"
1059
+ _check_task_ref(
1060
+ dep.get("predecessor"), project_slug, task_index, f"{dpath}.predecessor", errors
1061
+ )
1062
+ _check_task_ref(
1063
+ dep.get("successor"), project_slug, task_index, f"{dpath}.successor", errors
1064
+ )
1065
+
1066
+
1067
+ def _check_baselines(
1068
+ project: dict[str, Any], base: str, own_tasks: set[str], errors: list[str]
1069
+ ) -> None:
1070
+ """Every baselined task must name a task in the same project."""
1071
+ for j, bl in enumerate(project.get("baselines", [])):
1072
+ for k, bt in enumerate(bl.get("tasks", [])):
1073
+ ref = bt.get("task")
1074
+ if ref not in own_tasks:
1075
+ errors.append(
1076
+ f"{base}.baselines[{j}].tasks[{k}].task: no task {ref!r} in this project"
1077
+ )
1078
+
1079
+
1080
+ def _collect_slugs(items: list[dict[str, Any]], base: str, errors: list[str]) -> set[str]:
1081
+ """Gather slugs from a collection, recording duplicates as errors."""
1082
+ slugs: set[str] = set()
1083
+ for i, item in enumerate(items):
1084
+ slug = item.get("slug", "")
1085
+ if slug in slugs:
1086
+ errors.append(f"{base}[{i}].slug: duplicate slug {slug!r}")
1087
+ slugs.add(slug)
1088
+ return slugs
1089
+
1090
+
1091
+ def _check_ref(value: str | None, valid: set[str], path: str, kind: str, errors: list[str]) -> None:
1092
+ """A simple slug reference must name an entry in ``valid`` when present."""
1093
+ if value is not None and value not in valid:
1094
+ errors.append(f"{path}: no {kind} with slug {value!r}")
1095
+
1096
+
1097
+ def _check_task_ref(
1098
+ ref: str | None,
1099
+ enclosing_project: str,
1100
+ task_index: dict[str, set[str]],
1101
+ path: str,
1102
+ errors: list[str],
1103
+ ) -> None:
1104
+ """Resolve a bare or ``<project>:<wbs>`` task ref against the global index."""
1105
+ if ref is None:
1106
+ return
1107
+ if ":" in ref:
1108
+ project_slug, _, wbs = ref.partition(":")
1109
+ else:
1110
+ project_slug, wbs = enclosing_project, ref
1111
+ tasks = task_index.get(project_slug)
1112
+ if tasks is None:
1113
+ errors.append(f"{path}: no project with slug {project_slug!r} for task ref {ref!r}")
1114
+ elif wbs not in tasks:
1115
+ errors.append(f"{path}: no task {wbs!r} in project {project_slug!r}")
1116
+
1117
+
1118
+ def _check_risks(
1119
+ risks: list[dict[str, Any]],
1120
+ base: str,
1121
+ enclosing_project: str | None,
1122
+ account_slugs: set[str],
1123
+ task_index: dict[str, set[str]],
1124
+ project_slugs: set[str],
1125
+ errors: list[str],
1126
+ ) -> None:
1127
+ """Validate risk owner and task-linkage references.
1128
+
1129
+ Program-scoped risks (``enclosing_project is None``) must qualify every task
1130
+ ref with a project slug, because a bare path has no project to resolve
1131
+ against.
1132
+ """
1133
+ seen: set[str] = set()
1134
+ for i, risk in enumerate(risks):
1135
+ rpath = f"{base}[{i}]"
1136
+ slug = risk.get("slug", "")
1137
+ if slug in seen:
1138
+ errors.append(f"{rpath}.slug: duplicate risk slug {slug!r}")
1139
+ seen.add(slug)
1140
+ _check_ref(risk.get("owner"), account_slugs, f"{rpath}.owner", "account", errors)
1141
+ for k, ref in enumerate(risk.get("tasks", [])):
1142
+ tref_path = f"{rpath}.tasks[{k}]"
1143
+ if enclosing_project is None and ":" not in ref:
1144
+ errors.append(
1145
+ f"{tref_path}: program-scoped risk task ref {ref!r} must be "
1146
+ f'qualified as "<project-slug>:<wbs-path>"'
1147
+ )
1148
+ continue
1149
+ _check_task_ref(ref, enclosing_project or "", task_index, tref_path, errors)