@micha.bigler/ui-core-micha 3.14.3 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (367) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +7 -0
  3. package/dist/auth/UserMenu.js +1 -1
  4. package/dist/auth/apiClient.js +7 -6
  5. package/dist/chat/ChatList.js +1 -1
  6. package/dist/chat/ChatThread.js +2 -1
  7. package/dist/chat/ChatWorkspace.js +1 -1
  8. package/dist/chat/ModelPicker.js +2 -5
  9. package/dist/chat/kitSxRegistry.js +9 -0
  10. package/dist/chat/useChatSocket.js +1 -1
  11. package/dist/components/LoginForm.js +1 -1
  12. package/dist/components/SecurityComponent.js +1 -1
  13. package/dist/components/UserListComponent.js +2 -8
  14. package/dist/components/charts/ChartFrame.js +1 -1
  15. package/dist/components/charts/ScatterChart.js +2 -1
  16. package/dist/components/charts/kitSxRegistry.js +6 -0
  17. package/dist/entries/charts.js +6 -0
  18. package/dist/entries/chat.js +8 -0
  19. package/dist/entries/markdown.js +1 -0
  20. package/dist/index.js +0 -14
  21. package/dist/layout/SectionNav.js +1 -1
  22. package/dist/messaging/AttachmentList.js +2 -2
  23. package/dist/messaging/Composer.js +2 -1
  24. package/dist/messaging/ConversationLaunchers.js +1 -4
  25. package/dist/messaging/ConversationList.js +1 -1
  26. package/dist/messaging/DirectMessageLauncher.js +1 -1
  27. package/dist/messaging/MessagingProvider.js +0 -0
  28. package/dist/messaging/PollCard.js +7 -5
  29. package/dist/messaging/ReadTicks.js +2 -2
  30. package/dist/messaging/Thread.js +5 -3
  31. package/dist/notifications/NotificationBell.js +4 -7
  32. package/dist/notifications/NotificationSettings.js +1 -1
  33. package/dist/onboarding/OnboardingProvider.js +3 -6
  34. package/dist/pages/AccountPage.js +1 -2
  35. package/dist/pages/LoginPage.js +2 -3
  36. package/dist/pages/PasswordChangePage.js +2 -3
  37. package/dist/pages/PasswordInvitePage.js +2 -3
  38. package/dist/pages/PasswordResetRequestPage.js +2 -3
  39. package/dist/pages/SignUpPage.js +2 -3
  40. package/dist/pages/SignupConfirmPage.js +2 -3
  41. package/dist/theme/createAppTheme.js +5 -4
  42. package/dist/theme/kitSxEntry.js +11 -0
  43. package/dist/theme/kitSxRegistry.js +2 -15
  44. package/dist/theme/themeCompleteness.js +3 -12
  45. package/dist/utils/auth-errors.js +4 -3
  46. package/package.json +41 -16
  47. package/.github/workflows/publish.yml +0 -126
  48. package/DESIGN.md +0 -60
  49. package/WORK_ORDERS.md +0 -148
  50. package/dev/entries.jsx +0 -721
  51. package/dev/fixtures.js +0 -26
  52. package/dev/main.jsx +0 -75
  53. package/dev/mockTransport.jsx +0 -128
  54. package/docs/CHART-LAYOUT.md +0 -157
  55. package/docs/DARK-TOKENS.md +0 -140
  56. package/docs/TYPE-SCALE.md +0 -111
  57. package/docs/messaging-deviations.md +0 -118
  58. package/index.html +0 -12
  59. package/renovate.json +0 -10
  60. package/scripts/chart-api-census.mjs +0 -415
  61. package/src/auth/AuthContext.jsx +0 -158
  62. package/src/auth/ReauthModal.jsx +0 -82
  63. package/src/auth/UserMenu.jsx +0 -121
  64. package/src/auth/apiClient.jsx +0 -232
  65. package/src/auth/authApi.jsx +0 -539
  66. package/src/auth/authConfig.jsx +0 -34
  67. package/src/auth/reauth.js +0 -47
  68. package/src/chat/ChatComposer.jsx +0 -36
  69. package/src/chat/ChatList.jsx +0 -87
  70. package/src/chat/ChatThread.jsx +0 -128
  71. package/src/chat/ChatWorkspace.jsx +0 -244
  72. package/src/chat/MarkdownReply.jsx +0 -22
  73. package/src/chat/ModelPicker.jsx +0 -85
  74. package/src/chat/api.js +0 -18
  75. package/src/chat/useChatSocket.js +0 -32
  76. package/src/chat/wire.js +0 -7
  77. package/src/components/AccessCodeManager.jsx +0 -288
  78. package/src/components/AccessCodeSingleUseToggle.jsx +0 -92
  79. package/src/components/AllowedEmailDomainsManager.jsx +0 -92
  80. package/src/components/AuthFactorRequirementCard.jsx +0 -95
  81. package/src/components/BulkInviteCsvTab.jsx +0 -198
  82. package/src/components/EmptyState.jsx +0 -38
  83. package/src/components/LoginForm.jsx +0 -159
  84. package/src/components/MFAComponent.jsx +0 -350
  85. package/src/components/MfaLoginComponent.jsx +0 -252
  86. package/src/components/MobileBottomNav.jsx +0 -82
  87. package/src/components/PasskeysComponent.jsx +0 -252
  88. package/src/components/PasswordChangeForm.jsx +0 -87
  89. package/src/components/PasswordResetRequestForm.jsx +0 -43
  90. package/src/components/PasswordSetForm.jsx +0 -97
  91. package/src/components/ProfileComponent.jsx +0 -348
  92. package/src/components/QrSignupManager.jsx +0 -404
  93. package/src/components/QrSignupValidityManager.jsx +0 -106
  94. package/src/components/RegistrationMethodsManager.jsx +0 -150
  95. package/src/components/SecurityComponent.jsx +0 -141
  96. package/src/components/SocialLoginButtons.jsx +0 -83
  97. package/src/components/SoftChip.jsx +0 -93
  98. package/src/components/StatTile.jsx +0 -39
  99. package/src/components/SupportRecoveryRequestsTab.jsx +0 -482
  100. package/src/components/TurnstileWidget.jsx +0 -99
  101. package/src/components/UserInviteComponent.jsx +0 -76
  102. package/src/components/UserListComponent.jsx +0 -618
  103. package/src/components/charts/BarChart.jsx +0 -102
  104. package/src/components/charts/ChartFrame.jsx +0 -214
  105. package/src/components/charts/LineChart.jsx +0 -126
  106. package/src/components/charts/ScatterChart.jsx +0 -299
  107. package/src/components/charts/TimeSeriesChart.jsx +0 -300
  108. package/src/components/charts/chartDefaults.js +0 -644
  109. package/src/components/charts/chartLabels.js +0 -36
  110. package/src/components/charts/exportChart.js +0 -258
  111. package/src/components/charts/formatters.js +0 -39
  112. package/src/components/charts/palette.js +0 -33
  113. package/src/components/charts/yearTickInterval.js +0 -7
  114. package/src/i18n/authTranslations.ts +0 -2651
  115. package/src/i18n/chartsTranslations.ts +0 -91
  116. package/src/i18n/chatTranslations.ts +0 -55
  117. package/src/i18n/messagingTranslations.ts +0 -120
  118. package/src/i18n/notificationsTranslations.ts +0 -32
  119. package/src/i18n/onboardingTranslations.ts +0 -29
  120. package/src/i18n/sectionNavTranslations.ts +0 -20
  121. package/src/i18n/uiCoreTranslations.js +0 -29
  122. package/src/i18n/userMenuTranslations.ts +0 -5
  123. package/src/index.js +0 -158
  124. package/src/layout/PageLayout.jsx +0 -36
  125. package/src/layout/SectionNav.jsx +0 -320
  126. package/src/messaging/AttachmentList.jsx +0 -145
  127. package/src/messaging/Composer.jsx +0 -197
  128. package/src/messaging/ConversationLaunchers.jsx +0 -51
  129. package/src/messaging/ConversationList.jsx +0 -109
  130. package/src/messaging/DirectMessageLauncher.jsx +0 -112
  131. package/src/messaging/MessageBubble.jsx +0 -183
  132. package/src/messaging/MessagingProvider.jsx +0 -0
  133. package/src/messaging/MessagingScopeConfig.jsx +0 -24
  134. package/src/messaging/PollCard.jsx +0 -163
  135. package/src/messaging/ReactionBar.jsx +0 -43
  136. package/src/messaging/ReadTicks.jsx +0 -82
  137. package/src/messaging/Thread.jsx +0 -195
  138. package/src/messaging/api.js +0 -48
  139. package/src/messaging/composerImageCompression.js +0 -35
  140. package/src/messaging/linkifyText.jsx +0 -37
  141. package/src/notifications/NotificationBell.jsx +0 -84
  142. package/src/notifications/NotificationSettings.jsx +0 -330
  143. package/src/notifications/NotificationsProvider.jsx +0 -224
  144. package/src/notifications/PopupSurface.jsx +0 -105
  145. package/src/notifications/api.js +0 -55
  146. package/src/notifications/feedApi.js +0 -18
  147. package/src/notifications/realtime.jsx +0 -138
  148. package/src/notifications/serviceWorker/sw.js +0 -50
  149. package/src/onboarding/OnboardingProvider.jsx +0 -326
  150. package/src/onboarding/OnboardingWizard.jsx +0 -61
  151. package/src/onboarding/WizardDialogShell.jsx +0 -51
  152. package/src/onboarding/api.js +0 -13
  153. package/src/onboarding/stepSelection.js +0 -16
  154. package/src/onboarding/steps/BrowserPushStep.jsx +0 -121
  155. package/src/onboarding/steps/CompleteNameStep.jsx +0 -46
  156. package/src/onboarding/steps/CookieConsentStep.jsx +0 -133
  157. package/src/onboarding/steps/PwaInstallStep.jsx +0 -47
  158. package/src/pages/AccountPage.jsx +0 -394
  159. package/src/pages/LoginPage.jsx +0 -343
  160. package/src/pages/PasswordChangePage.jsx +0 -62
  161. package/src/pages/PasswordInvitePage.jsx +0 -177
  162. package/src/pages/PasswordResetRequestPage.jsx +0 -68
  163. package/src/pages/SignUpPage.jsx +0 -268
  164. package/src/pages/SignupConfirmPage.jsx +0 -155
  165. package/src/testing/checkKitIntegration.js +0 -233
  166. package/src/theme/createAppTheme.js +0 -423
  167. package/src/theme/fonts.js +0 -3
  168. package/src/theme/index.js +0 -21
  169. package/src/theme/kitSxRegistry.js +0 -285
  170. package/src/theme/themeCompleteness.js +0 -1045
  171. package/src/theme/tokens.js +0 -478
  172. package/src/utils/auth-errors.js +0 -76
  173. package/src/utils/authService.js +0 -223
  174. package/src/utils/resolveErrorText.js +0 -28
  175. package/src/utils/webauthn.js +0 -51
  176. package/tests/AccountPage.profileStatements.test.jsx +0 -162
  177. package/tests/AccountPage.test.jsx +0 -200
  178. package/tests/AuthContext.test.jsx +0 -129
  179. package/tests/BarChart.test.jsx +0 -112
  180. package/tests/BrowserPushStep.test.jsx +0 -110
  181. package/tests/ChartFrame.test.jsx +0 -336
  182. package/tests/ConversationList.test.jsx +0 -144
  183. package/tests/CookieConsentStep.test.jsx +0 -69
  184. package/tests/EmptyState.test.jsx +0 -62
  185. package/tests/LineChart.test.jsx +0 -80
  186. package/tests/MessagingProvider.test.jsx +0 -50
  187. package/tests/MobileBottomNav.test.jsx +0 -319
  188. package/tests/NotificationBell.test.jsx +0 -60
  189. package/tests/NotificationSettings.test.jsx +0 -276
  190. package/tests/NotificationsProvider.test.jsx +0 -240
  191. package/tests/OnboardingProvider.test.jsx +0 -294
  192. package/tests/OnboardingWizard.test.jsx +0 -143
  193. package/tests/PasswordChangeForm.test.jsx +0 -70
  194. package/tests/PasswordInvitePage.test.jsx +0 -142
  195. package/tests/PasswordSetForm.test.jsx +0 -70
  196. package/tests/PopupSurface.test.jsx +0 -142
  197. package/tests/ProfileComponent.refresh.test.jsx +0 -194
  198. package/tests/QrSignupManager.registrationContext.test.jsx +0 -106
  199. package/tests/QrSignupManagerInfoBox.test.jsx +0 -49
  200. package/tests/ScatterChart.test.jsx +0 -270
  201. package/tests/SectionNav.test.jsx +0 -363
  202. package/tests/SignUpPage.test.jsx +0 -208
  203. package/tests/SoftChip.test.jsx +0 -105
  204. package/tests/StatTile.test.jsx +0 -57
  205. package/tests/Thread.test.jsx +0 -151
  206. package/tests/TimeSeriesChart.test.jsx +0 -367
  207. package/tests/UserListComponent.test.jsx +0 -192
  208. package/tests/UserMenu.test.jsx +0 -171
  209. package/tests/apiClientLanguage.test.js +0 -33
  210. package/tests/apiClientPublicPaths.test.js +0 -178
  211. package/tests/authApiLogin.test.js +0 -42
  212. package/tests/authApiRegistration.test.js +0 -31
  213. package/tests/authErrorCodeRendering.test.jsx +0 -77
  214. package/tests/authErrors.test.js +0 -74
  215. package/tests/authTranslations.test.js +0 -74
  216. package/tests/chart-api-census.test.js +0 -366
  217. package/tests/chartAxisTickBand.test.jsx +0 -95
  218. package/tests/chartDefaults.test.js +0 -902
  219. package/tests/chartLabels.test.js +0 -50
  220. package/tests/chartsPalette.test.js +0 -99
  221. package/tests/chatApi.test.js +0 -48
  222. package/tests/chatDriverNames.test.jsx +0 -71
  223. package/tests/chatExports.test.js +0 -3
  224. package/tests/chatList.test.jsx +0 -84
  225. package/tests/chatPicker.test.jsx +0 -82
  226. package/tests/chatSocket.test.jsx +0 -26
  227. package/tests/chatThread.test.jsx +0 -116
  228. package/tests/chatTranslations.test.js +0 -47
  229. package/tests/chatWorkspace.test.jsx +0 -234
  230. package/tests/checkKitIntegration.test.jsx +0 -219
  231. package/tests/createAppTheme.test.js +0 -132
  232. package/tests/createAppThemeDark.test.js +0 -127
  233. package/tests/createAppThemeLightGuard.test.js +0 -40
  234. package/tests/createAppThemePageContrast.test.js +0 -108
  235. package/tests/createAppThemeTypography.test.js +0 -53
  236. package/tests/errorCallSiteGuard.test.js +0 -32
  237. package/tests/exportChart.test.js +0 -313
  238. package/tests/feedApi.test.js +0 -46
  239. package/tests/fixtures/light-theme-baseline.json +0 -2234
  240. package/tests/i18nAggregate.test.js +0 -120
  241. package/tests/i18nGermanVariant.test.js +0 -65
  242. package/tests/loginPageErrorResolution.test.jsx +0 -102
  243. package/tests/messagingApi.test.js +0 -32
  244. package/tests/messagingChunk6.test.jsx +0 -116
  245. package/tests/messagingCompactBubble.test.jsx +0 -64
  246. package/tests/messagingComposer.test.jsx +0 -136
  247. package/tests/messagingComposerChunk5.test.jsx +0 -113
  248. package/tests/messagingContractConformance.test.js +0 -69
  249. package/tests/messagingDirectMessageLauncher.test.jsx +0 -151
  250. package/tests/messagingExports.test.js +0 -21
  251. package/tests/messagingInteractions.test.jsx +0 -136
  252. package/tests/messagingLastMessagePreview.test.jsx +0 -93
  253. package/tests/messagingLinkify.test.jsx +0 -51
  254. package/tests/messagingMessageActions.test.jsx +0 -163
  255. package/tests/messagingMsg6.test.jsx +0 -105
  256. package/tests/messagingMsg6f.test.jsx +0 -155
  257. package/tests/messagingMsg6g.test.jsx +0 -166
  258. package/tests/messagingMsg6h.test.jsx +0 -136
  259. package/tests/messagingMsg6i.test.jsx +0 -129
  260. package/tests/messagingPollRendering.test.jsx +0 -142
  261. package/tests/messagingReactionMerge.test.jsx +0 -77
  262. package/tests/messagingReadTicksTooltip.test.jsx +0 -36
  263. package/tests/messagingThreadReceipts.test.jsx +0 -178
  264. package/tests/messagingThreadingQuoting.test.jsx +0 -87
  265. package/tests/messagingUnreadLifecycle.test.jsx +0 -78
  266. package/tests/notificationsApi.test.js +0 -41
  267. package/tests/notificationsExports.test.js +0 -34
  268. package/tests/onboardingDescriptors.test.js +0 -132
  269. package/tests/packageTreeShaking.test.js +0 -126
  270. package/tests/realtime.test.jsx +0 -219
  271. package/tests/resolveErrorText.test.js +0 -72
  272. package/tests/stepSelection.test.js +0 -53
  273. package/tests/themeCompleteness.test.js +0 -626
  274. package/tests/timeSeriesLegend.test.js +0 -99
  275. package/tests/userScreenTranslations.test.jsx +0 -218
  276. package/tsconfig.build.json +0 -18
  277. package/vite.config.mjs +0 -6
  278. package/work-orders/AUTH-1.md +0 -74
  279. package/work-orders/AUTH-2.md +0 -74
  280. package/work-orders/AUTH-4.md +0 -183
  281. package/work-orders/AUTH-5.md +0 -154
  282. package/work-orders/AUTH-7.md +0 -181
  283. package/work-orders/AUTH-8.md +0 -184
  284. package/work-orders/CHART-1.md +0 -285
  285. package/work-orders/CHART-2.md +0 -222
  286. package/work-orders/CHART-3.md +0 -163
  287. package/work-orders/CHART-4.md +0 -203
  288. package/work-orders/CHART-5.md +0 -226
  289. package/work-orders/CHART-6.md +0 -428
  290. package/work-orders/CHART-7.md +0 -175
  291. package/work-orders/DX-1.md +0 -169
  292. package/work-orders/DX-2.md +0 -188
  293. package/work-orders/DX-3.md +0 -195
  294. package/work-orders/I18N-1.md +0 -254
  295. package/work-orders/I18N-3.md +0 -207
  296. package/work-orders/MSG-3.md +0 -375
  297. package/work-orders/MSG-3b.md +0 -1002
  298. package/work-orders/MSG-3c.md +0 -378
  299. package/work-orders/MSG-3d.md +0 -268
  300. package/work-orders/MSG-3e.md +0 -164
  301. package/work-orders/MSG-3f.md +0 -391
  302. package/work-orders/MSG-6e.md +0 -308
  303. package/work-orders/MSG-6f.md +0 -214
  304. package/work-orders/MSG-6g.md +0 -244
  305. package/work-orders/MSG-6h.md +0 -238
  306. package/work-orders/MSG-6i.md +0 -271
  307. package/work-orders/MSG-6j.md +0 -117
  308. package/work-orders/NOTIF-12.md +0 -214
  309. package/work-orders/NOTIF-13.md +0 -142
  310. package/work-orders/NOTIF-14.md +0 -151
  311. package/work-orders/PRIM-1.md +0 -177
  312. package/work-orders/SHELL-1.md +0 -255
  313. package/work-orders/SHELL-2.md +0 -197
  314. package/work-orders/SHELL-3.md +0 -359
  315. package/work-orders/SHELL-4.md +0 -351
  316. package/work-orders/SHELL-5.md +0 -345
  317. package/work-orders/SHELL-6.md +0 -210
  318. package/work-orders/THEME-1.md +0 -587
  319. package/work-orders/THEME-10.md +0 -303
  320. package/work-orders/THEME-11.md +0 -208
  321. package/work-orders/THEME-2.md +0 -339
  322. package/work-orders/THEME-3.md +0 -210
  323. package/work-orders/THEME-4.md +0 -271
  324. package/work-orders/THEME-5.md +0 -341
  325. package/work-orders/THEME-6.md +0 -247
  326. package/work-orders/THEME-7.md +0 -82
  327. package/work-orders/THEME-8.md +0 -211
  328. package/work-orders/THEME-9.md +0 -194
  329. package/work-orders/UCM-AUTH-10.md +0 -490
  330. package/work-orders/UCM-AUTH-11.md +0 -185
  331. package/work-orders/UCM-AUTH-12.md +0 -98
  332. package/work-orders/UCM-AUTH-13.md +0 -47
  333. package/work-orders/UCM-AUTH-9.md +0 -207
  334. package/work-orders/UCM-CHART-10.md +0 -150
  335. package/work-orders/UCM-CHART-11.md +0 -132
  336. package/work-orders/UCM-CHART-12.md +0 -348
  337. package/work-orders/UCM-CHART-13.md +0 -122
  338. package/work-orders/UCM-CHART-14.md +0 -198
  339. package/work-orders/UCM-CHART-15.md +0 -187
  340. package/work-orders/UCM-CHART-16.md +0 -169
  341. package/work-orders/UCM-CHART-17.md +0 -195
  342. package/work-orders/UCM-CHART-18.md +0 -200
  343. package/work-orders/UCM-CHART-8.md +0 -147
  344. package/work-orders/UCM-CHART-9.md +0 -265
  345. package/work-orders/UCM-CHAT-1.md +0 -190
  346. package/work-orders/UCM-CHAT-2.md +0 -75
  347. package/work-orders/UCM-DEP-1.md +0 -63
  348. package/work-orders/UCM-I18N-4.md +0 -226
  349. package/work-orders/UCM-I18N-5.md +0 -211
  350. package/work-orders/UCM-MSG-20.md +0 -192
  351. package/work-orders/UCM-PRIM-2.md +0 -114
  352. package/work-orders/UCM-SHELL-7.md +0 -134
  353. package/work-orders/UCM-SHELL-8.md +0 -125
  354. package/work-orders/UCM-TEST-1.md +0 -304
  355. package/work-orders/UCM-THEME-13.md +0 -69
  356. package/work-orders/UCM-THEME-14.md +0 -312
  357. package/work-orders/UCM-THEME-15.md +0 -356
  358. package/work-orders/UCM-THEME-16.md +0 -80
  359. package/work-orders/UCM-THEME-17.md +0 -111
  360. package/work-orders/UCM-THEME-18.md +0 -184
  361. package/work-orders/assets/AUTH-4-user-list-card.html +0 -407
  362. package/work-orders/assets/PRIM-1-dashboard-primitives.html +0 -624
  363. package/work-orders/assets/SHELL-2-account-navigation.html +0 -1159
  364. package/work-orders/assets/THEME-1-baseline-reference-sheet.html +0 -1352
  365. package/work-orders/assets/UCM-CHAT-1-ai-chat.html +0 -447
  366. package/work-orders/assets/UCM-THEME-13-dark-prototype.html +0 -390
  367. package/work-orders/assets/UCM-THEME-17-type-scale.html +0 -391
@@ -1,378 +0,0 @@
1
- # MSG-3c — deliver the rows MSG-3b left blocked against a stale dcm version
2
-
3
- Status: planned · Tier 2 · Target repo: `ui-core-micha` (main)
4
- **Binding spec:** `django-core-micha/docs/design/messaging-platform.md`. Extends the landed MSG-3b
5
- (published ucm 2.17.0), same convention as `MSG-2b` / `MSG-2c` / `NOTIF-8b`.
6
-
7
- ---
8
-
9
- ## Part A — Envelope (Expertenchat, 2026-07-31)
10
-
11
- ### Goal
12
-
13
- Deliver the seven checklist rows MSG-3b correctly refused to fake — and which became buildable while
14
- MSG-3b was still running, without the session ever learning.
15
-
16
- ### Why this WO exists
17
-
18
- MSG-3b started against dcm **2.36.1** and pinned that version explicitly in every BLOCKED entry of
19
- `docs/messaging-deviations.md`. dcm **2.37.0** (MSG-2c) landed mid-flight and delivered exactly what
20
- those entries said was missing. The WO closed — six chunks, per-chunk reviews, a clean WO-end
21
- `ui_reviewer` row-by-row cross-check — with seven rows recorded as blocked on "a future dcm work order"
22
- that had in fact already shipped.
23
-
24
- The WO-end review did not catch it because it cross-checked the checklist against **ucm's** landed code
25
- and took the dependency's state from the WO text. That is the third occurrence of one pattern in this
26
- workstream: a side verified against itself, with the seam assumed rather than read. See §5 of
27
- `notifications-messaging-roadmap.md`.
28
-
29
- **Credit where it is due, and the reason this was cheap to find:** MSG-3b wrote the dcm version into
30
- every BLOCKED entry. Without that pin the staleness would have been invisible instead of a two-minute
31
- check. Keep doing it — this WO makes it a rule.
32
-
33
- ### Verified as deliverable against dcm 2.37.0
34
-
35
- Checked in `django-core-micha` at 2.37.0 before this WO was written:
36
-
37
- | Row | What MSG-3b recorded | Actual state in 2.37.0 |
38
- |---|---|---|
39
- | 38 | "dcm does not serialize `last_message`" | `serialize_last_message` exists; `last_message` is on the conversation payload |
40
- | 51 | "no poll read serialization" | `serialize_poll` exists, embedded in `serialize_message` for `kind == "poll"` |
41
- | 52 | "per-option voter names absent" | `voters` is in the poll projection (user ids, every conversation kind) |
42
- | 53 | "closed state depends on the absent poll contract" | `closed_at` is in the projection |
43
- | 56 | "dcm 2.36.1 does not emit `reaction`" | emitted (MSG-2c wired 8 frames) |
44
- | 57 | "dcm 2.36.1 does not emit `poll_updated`" | emitted |
45
- | 58 | "reorder/preview need the missing `last_message`" | available per row 38 |
46
-
47
- ### Expected outcome
48
-
49
- The seven rows above delivered per the MSG-3b checklist's original descriptions:
50
-
51
- - **38 / 58** — real last-message preview in `ConversationList`, and list reorder plus preview refresh
52
- on new / edited / deleted messages.
53
- - **51-53** — `PollCard` renders question, options, per-option `vote_count` and `voters`, closed state
54
- with voting disabled. Read `voted_option_ids` **only** from the three poll REST responses; it is
55
- deliberately absent from `serialize_poll` and from every realtime frame, so do not look for it there
56
- and do not reconstruct it from a frame.
57
- - **56 / 57** — the existing `reaction` and `poll_updated` handlers stop being dead code; verify they
58
- merge correctly now that frames actually arrive, rather than assuming the untested paths are right.
59
- - `docs/messaging-deviations.md` corrected: these seven entries move from BLOCKED to delivered, and the
60
- closing paragraph stops claiming they need a future dcm WO.
61
-
62
- **Still legitimately BLOCKED, do not touch:** rows **27** (no readable thread reply state) and **42**
63
- (`external_key` not serialized). Both are real dcm gaps that MSG-2c did not cover; both are addressed by
64
- dcm `MSG-2d`, and a later ucm WO delivers them. MSG-3b assessed these two correctly.
65
-
66
- ### The rule this WO adds
67
-
68
- **A BLOCKED row is re-verified against the live dependency at WO close, not at WO start.** Concretely:
69
-
70
- 1. Every BLOCKED entry in `docs/messaging-deviations.md` must name the dependency version it was
71
- blocked at — MSG-3b already did this; make it explicit policy.
72
- 2. Extend the contract-conformance test from MSG-3b chunk 1: it must fail when a BLOCKED entry carries
73
- no version pin. A test cannot check whether dcm has since shipped — it has no access to it — but it
74
- can guarantee the pin exists, which is what makes the manual re-check a two-minute job instead of an
75
- archaeology exercise.
76
- 3. The WO-end review instruction changes: cross-check BLOCKED rows against the **dependency repo at its
77
- current version**, not against the WO text.
78
-
79
- ### Non-goals / do-not-touch
80
-
81
- Rows 27 and 42 (blocked on dcm `MSG-2d`). Any dcm change. Any app-side code. The jg migration (MSG-5).
82
- Search, typing, client→server WS. The decomposition and component boundaries established in MSG-3 and
83
- held through MSG-3b — new rendering lands in the component that owns it, and the ~400 LOC soft trigger
84
- and one-harness-entry-per-component rule stay in force. Do not re-open rows already delivered or
85
- already recorded as deliberate deviations.
86
-
87
- ### Required tests to WRITE
88
-
89
- - **Poll rendering** against a server-shaped payload — the projection as `serialize_poll` actually emits
90
- it, not a client-constructed object. This is the MSG-3b lesson repeated: the threading tests passed
91
- for a year of nobody noticing precisely because they used client-shaped fixtures.
92
- - **`voted_option_ids` comes only from REST:** a `poll_updated` frame carrying no such key must not
93
- clear or invert the viewer's own vote state.
94
- - **`reaction` / `poll_updated` frame merge:** an arriving frame updates the rendered message for a
95
- viewer who did not cause it; a duplicate `event_id` changes nothing.
96
- - **Last-message preview and reorder:** a new message moves its conversation to the top and updates the
97
- preview; an edit updates it; a delete empties it.
98
- - **Conformance test extension:** a BLOCKED entry without a version pin fails the build — assert the
99
- negative case, not only the positive.
100
- - Existing ucm suites stay green.
101
-
102
- ### Risks
103
-
104
- - **The dead handlers were never exercised.** Rows 56/57 are the paths most likely to contain latent
105
- bugs, because they have never run against a real frame. Treat them as new code under test, not as
106
- already-working code being unblocked.
107
- - **Fixture shape** — see the poll test above; the single most repeated failure in this workstream is a
108
- test that passes because it invented its own payload.
109
- - Deviation-doc drift: correcting seven entries by hand while leaving two correct ones intact is exactly
110
- where an over-eager edit removes a real blocker.
111
-
112
- ### Preconditions
113
-
114
- dcm 2.37.0 published (met). Approval Gate #1 = operator go on this envelope. Rows 27/42 additionally
115
- require dcm `MSG-2d`, which is **not** a precondition for this WO.
116
-
117
- ### Release
118
-
119
- One version bump + npm publish at WO end.
120
-
121
- ### Execution directive
122
-
123
- Implement through `codex exec` in the background — invoked **directly via Bash** (never the
124
- `debugger`/`*_coder` Agent wrappers) with **both** flags `--skip-git-repo-check` and
125
- `--dangerously-bypass-approvals-and-sandbox`, prompt passed as a positional argument from a file;
126
- fall back to direct Claude implementation only on Codex quota / rate-limit / non-zero exit.
127
-
128
- ### Mini-handover (pastable)
129
-
130
- Orchestrator: implement `work-orders/MSG-3c.md` in `ui-core-micha` (main). `git pull` first, read the WO
131
- + `django-core-micha/docs/design/messaging-platform.md`, then follow `orchestrate-codex` (Codex-first,
132
- own review per chunk, `ui_reviewer` at WO end, one publish at WO end). Rows 27 and 42 stay BLOCKED —
133
- they wait on dcm `MSG-2d`.
134
-
135
- ---
136
-
137
- ## Part B — Implementation map (Orchestrator)
138
-
139
- **Target repo working directory (all chunks):** `C:\Users\biglmi\Documents\webapps\ui-core-micha` (repo
140
- root). **Chunk plan (Orchestrator's own — the envelope named rows, not chunks):**
141
-
142
- 1. Last-message preview + reorder (rows 38, 58).
143
- 2. Poll rendering (rows 51-53).
144
- 3. Reaction/poll_updated frame-merge fixes (row 56/57 verification) + the conformance-test extension +
145
- the deviation-doc rewrite for all seven rows.
146
-
147
- **Verified against dcm 2.37.0 while scoping** (do not re-derive these, they're settled): `serialize_last_message`/
148
- `serialize_conversation_core` (`django-core-micha/src/django_core_micha/messaging/serializers.py:89-113`),
149
- `serialize_poll`/`serialize_reactions` (`serializers.py:41-46,52-64`), `_poll_response` — the ONLY place
150
- `voted_option_ids` is added, view-level, on top of `serialize_poll` (`views.py:32-36`, used by the
151
- create/vote/close poll responses only — never by `serialize_message`'s embedded poll, never by the
152
- `poll_updated` frame), and the three `conversation_upsert` publish sites (`services.py:95,120,165` —
153
- **new-message send only**; `edit_message`/`soft_delete_message` publish `message_edited`/`message_deleted`
154
- but never `conversation_upsert`, so a background conversation's list preview cannot live-update on an
155
- edit/delete of its last message without a dcm change — out of scope, verify via REST refresh instead,
156
- per chunk 1 below, do not invent a frame dcm doesn't send).
157
-
158
- **Two additional bugs found during this scoping, in files this WO already touches — fix them here rather
159
- than filing separately:**
160
- - `MessagingProvider.jsx:144` (`applyFrame`'s `message_edited` case): `mergeById(state.messages, payload)`
161
- where `payload` (no `.message` sub-key on this frame — it only ever carries `message_id`) falls through
162
- to the raw `frame` object, and `idOf()` (`MessagingProvider.jsx:45`, `item?.id ?? item?.conversation_id
163
- ?? item?.message_id ?? item?.poll_id`) checks `.conversation_id` **before** `.message_id` — every frame
164
- envelope carries `conversation_id`, so a `message_edited` frame gets keyed into the cache by the
165
- conversation's id instead of the message's id, silently creating a bogus cache entry and never touching
166
- the real message. Zero test coverage anywhere in this repo caught it. Fix in chunk 1 (same file as the
167
- conversation-preview work): merge with an explicit `id: frame.message_id`, not the `idOf` fallback chain.
168
- - `MessagingProvider.jsx:315-330` (`toggleReaction`) and the `reaction` frame case (`:145-149`): `serialize_reactions`
169
- (verified above) returns `[{emoji, count}]` **only, never a per-viewer `reacted` flag, ever** — not in
170
- REST responses, not in frames, by design (aggregate-only). `toggleReaction`'s optimistic update
171
- correctly sets `reacted: true/false` locally on the user's own click, but then the REST-confirm step
172
- (`:327`, `reactions: result?.reactions || ...`) and the frame handler (`:148`, `payload.reactions ||
173
- frame.reactions || ...`) both prefer the server's `reacted`-less array over the optimistic one — so
174
- confirming the user's own toggle, or literally any other viewer's reaction frame arriving for the same
175
- message, silently wipes every emoji's locally-known `reacted` state. This is exactly what row 56 asks
176
- to be verified rather than assumed. Fix in chunk 3 (bundled with the frame-merge verification, same
177
- underlying fix in both places): merge the server's `{emoji,count}` list with previously-known `reacted`
178
- flags by emoji key, never wholesale-replace.
179
-
180
- ### Chunk 1 — last-message preview and conversation reorder (rows 38, 58)
181
-
182
- **Files:** `src/messaging/MessagingProvider.jsx` (the `message_edited` bug fix above; verify — do not
183
- rebuild — the existing `conversation_upsert` handling at `:160` and `EMPTY_CACHE`/reducer shape),
184
- `src/messaging/ConversationList.jsx` (verify — likely no change needed, see below), new tests.
185
-
186
- **Much of rows 38/58 turns out to already be built.** MSG-3b wrote `applyFrame`'s `conversation_upsert`
187
- case and `ConversationList`'s `last_message?.body` rendering + `last_message_at` sort defensively,
188
- against the design doc's frame contract, before dcm actually sent the data. Now that dcm 2.37.0 does
189
- (`conversation_upsert` on new-message send carries the real `serialize_conversation_core`, including
190
- `last_message`), this is primarily a **verification + test-writing chunk**, not a rebuild — confirm this
191
- by reading `ConversationList.jsx:11` (`ordered()`, sorts by `last_message_at`) and `:71`
192
- (`conversation.last_message?.body`) before writing anything new. If you find either needs a real change
193
- beyond the `message_edited` fix above, make it — but don't assume a rewrite is needed without checking.
194
-
195
- **Row 58's "an edit updates it; a delete empties it"** must be tested via **REST refresh**
196
- (`refreshConversations()`), not a live frame — dcm does not publish `conversation_upsert` on edit/delete
197
- (see the verified call-site list above). `serialize_last_message` (`serializers.py:89-97`) already
198
- returns `excerpt: ""` for a deleted last message — that's the server behavior a refresh test should
199
- assert against, mocked with a server-shaped `listConversations` response, not invented.
200
-
201
- **Required tests (chunk 1):**
202
- - A `conversation_upsert` frame with a real `last_message` moves that conversation to the top of
203
- `ConversationList` and updates its preview text — server-shaped fixture (`last_message: {body, ...}`,
204
- not a client-invented shape).
205
- - A REST refresh reflecting an edited last message updates the preview; reflecting a deleted one
206
- (`last_message.excerpt: ""` — check the actual field name your fixture should use, verify against
207
- `serialize_last_message`'s real key, which is `excerpt` not `body`, at the `last_message` level —
208
- don't assume the `Conversation.last_message` shape matches `Message`'s own `body` field name) shows
209
- the empty/no-preview state.
210
- - The `message_edited` frame-keying regression: a `message_edited` frame with only `{message_id,
211
- conversation_id}` (the real minimal shape — verified above, it carries no other fields) must patch the
212
- message keyed by `message_id`, and must NOT create/touch any cache entry keyed by `conversation_id`.
213
- - Existing ucm suites stay green.
214
-
215
- **Progress contract / preamble:** identical to MSG-3b's chunks — `PLAN:`/`PROGRESS:`/`RESULT:` lines, no
216
- git operations, write-and-run-only-the-new-tests, leave the diff uncommitted for the orchestrator's
217
- review.
218
-
219
- **Mini-handover:** repo `C:\Users\biglmi\Documents\webapps\ui-core-micha`, branch `main`, WO
220
- `work-orders/MSG-3c.md` chunk 1 (Part B above). Follow `orchestrate-codex`.
221
-
222
- **Note on chunk 1's actual execution:** Codex reported `workspace is out of credits` on invocation
223
- (not a transient rate limit) — the orchestrator implemented chunk 1 directly per the Codex→Claude
224
- fallback rule, and an independent reviewer ran (mandatory once the orchestrator is the author). Chunks
225
- 2/3 below should still attempt Codex first per the execution directive, but expect the same
226
- out-of-credits failure and fall back the same way if it recurs — don't loop retrying it.
227
-
228
- ### Chunk 2 — poll rendering (rows 51-53)
229
-
230
- **Files:** `src/messaging/PollCard.jsx` (all three rows land here), new tests. `PollCard.jsx` was built
231
- in MSG-3 speculatively, before dcm's poll read contract existed — it guesses at field names, and two of
232
- those guesses are confirmed wrong against the real server shape.
233
-
234
- **Verified against dcm 2.37.0 (`django-core-micha/src/django_core_micha/messaging/serializers.py:52-64`,
235
- `views.py:32-36`):** `serialize_poll` returns `{id, question, allow_multiple, closed_at, created_by_id,
236
- options: [{id, text, order, vote_count, voters}]}` — `voters` is an array of user ids (row 52).
237
- `voted_option_ids` is added **only** by `_poll_response` (`views.py:32-36`), a view-level enrichment
238
- used **only** by the create/vote/close poll REST responses — it is never in `serialize_poll` itself,
239
- never in `serialize_message`'s embedded poll (message list/thread loads), and never in the
240
- `poll_updated` realtime frame. This is the WO's explicit "read `voted_option_ids` only from the three
241
- poll REST responses" instruction, now with the exact citations.
242
-
243
- **Two confirmed bugs in `PollCard.jsx` against this real shape — fix both:**
244
- 1. `PollCard.jsx:18` — `const creatorId = poll.created_by?.id ?? poll.created_by;` assumes a
245
- `created_by` object or scalar; the real field is `created_by_id` (a plain id, verified above).
246
- `poll.created_by` is always `undefined` against the real server, so `creatorId` is always
247
- `undefined`, so `canClose`'s only non-host-supplied fallback (`creatorId === currentUser?.id`,
248
- `:19`) can never be true for a genuine creator — close is effectively broken for every poll unless a
249
- host explicitly passes `canClose`. Fix: read `poll.created_by_id` directly.
250
- 2. `PollCard.jsx:15` — `useEffect(() => setSelected((poll?.options || []).filter((option) =>
251
- option.selected || option.voted)...` assumes per-option `selected`/`voted` flags that don't exist
252
- anywhere in the real projection (verified above: options only ever have `{id, text, order,
253
- vote_count, voters}`). Selection must instead derive from `poll.voted_option_ids` **when present**
254
- (only true right after a create/vote/close REST call — the WO's explicit constraint) and otherwise
255
- default to empty (a poll loaded via `listMessages`/`listThread`, or updated via a `poll_updated`
256
- frame, correctly has no way to know the current viewer's vote state from that payload alone — do not
257
- guess, do not carry over stale local state across a payload that structurally cannot contain the
258
- answer).
259
-
260
- **Row 52 (voters) — not rendered at all today:** add voter identification per option. dcm only gives
261
- user **ids** (`option.voters: [user_id, ...]`), never names — there is no user-directory endpoint
262
- (same constraint already established for the DM candidate list in `MSG-3b`). Render what's actually
263
- available: a count (`voters.length`) is always correct; showing the *current viewer's own* vote as
264
- "you voted for this" is derivable from `voted_option_ids` after a REST action, or otherwise left
265
- unmarked. Do not invent a name lookup — this is a real, structural limit of the dcm contract, not
266
- something to work around client-side. If host-supplied user resolution is later wanted, that is a
267
- future WO's host-prop pattern, not this chunk's job.
268
-
269
- **Row 53 (closed state, voting disabled)** — `PollCard.jsx:17` (`const closed =
270
- Boolean(poll.closed_at)`) is already correct; verify the disabled-while-closed wiring on the vote
271
- controls (`:32`) still works once selection is correctly derived from `voted_option_ids`, and add
272
- coverage — this row was previously untested against a real payload shape too.
273
-
274
- **Required tests (chunk 2):**
275
- - Poll rendering against a genuinely server-shaped `serialize_poll` payload (all the real keys above,
276
- not a client-invented one) — question, options, `vote_count`, some indication of `voters`.
277
- - `voted_option_ids` present only in the vote/create/close REST response correctly drives selection
278
- state; a `poll_updated` frame carrying no `voted_option_ids` key must not clear or invert the
279
- viewer's own already-known vote state (this is the WO's explicitly named required test).
280
- - Close permission: `created_by_id === currentUser.id` correctly enables the close control; a
281
- non-creator without a host-supplied `canClose` override does not get it.
282
- - Closed poll disables voting.
283
- - Existing ucm suites (chunk 1 inclusive) stay green.
284
-
285
- **Progress contract / preamble:** identical to chunk 1's — `PLAN:`/`PROGRESS:`/`RESULT:` lines, no git
286
- operations, write-and-run-only-the-new-tests, leave the diff uncommitted for the orchestrator's review.
287
-
288
- **Mini-handover:** repo `C:\Users\biglmi\Documents\webapps\ui-core-micha`, branch `main`, WO
289
- `work-orders/MSG-3c.md` chunk 2 (Part B above, after chunk 1 is committed). Follow `orchestrate-codex`.
290
-
291
- **Note from chunk 2:** the same defect class row 56/57 ask to be verified for (a `poll_updated`/
292
- `reaction` frame's viewer-independent projection silently wiping viewer-specific state the client
293
- learned from a REST response) was ALSO found and fixed in `poll_updated`'s handler while writing
294
- chunk 2's own tests (`MessagingProvider.jsx`'s `poll_updated` case now preserves a previously-known
295
- `voted_option_ids` when the incoming frame lacks it). Chunk 3 must check whether the `reaction` case
296
- has the analogous issue — it very likely does (see below), don't assume it's fine because it's a
297
- similar shape to the code that turned out to be wrong.
298
-
299
- ### Chunk 3 — reaction frame-merge fix, conformance-test extension, deviation-doc rewrite (rows 56, 57 + the WO's new rule)
300
-
301
- **Files:** `src/messaging/MessagingProvider.jsx` (`applyFrame`'s `reaction` case, and `toggleReaction`'s
302
- REST-confirm path — same underlying bug, two call sites), `tests/messagingContractConformance.test.js`
303
- (the version-pin extension), `docs/messaging-deviations.md` (full rewrite of the seven now-delivered
304
- rows + the two still-BLOCKED rows, each BLOCKED entry now carrying an explicit version pin), new tests.
305
-
306
- **The reaction bug — verified, not hypothetical:** `django-core-micha/src/django_core_micha/messaging/serializers.py:41-46`
307
- (`serialize_reactions`) returns `[{emoji, count}]` **only, never a per-viewer `reacted` flag, in any
308
- context** — not REST, not frames, by design (the design doc's "aggregate only" rule). `MessagingProvider.jsx`'s
309
- `toggleReaction` (around `:315-330`) correctly sets `reacted: true/false` optimistically on the current
310
- user's own click, but then:
311
- - its own REST-confirm step merges `reactions: result?.reactions || result?.message?.reactions ||
312
- nextReactions` — `result.reactions` (the real server array) is truthy and picked first, wiping the
313
- optimistic `reacted` flags for every emoji on that message.
314
- - `applyFrame`'s `reaction` case (`:154` area) merges `reactions: payload.reactions || frame.reactions
315
- || previous.reactions || []` — same problem: any `reaction` frame arriving for this message (the
316
- user's own echoed toggle, or literally any other viewer reacting with a different emoji) wholesale-
317
- replaces the reactions array with the server's `reacted`-less version, wiping every previously-known
318
- `reacted` flag on that message, not just the one that changed.
319
-
320
- **Fix both call sites the same way:** merge the server's `{emoji,count}` list with previously-known
321
- `reacted` flags by emoji key — never wholesale-replace. For each emoji in the incoming server list, if
322
- a previous entry for that same emoji had `reacted: true`, carry it forward (unless this merge IS the
323
- direct result of the current user's own toggle action, in which case the just-computed optimistic value
324
- is correct and already known — don't let the REST-confirm step regress what the optimistic update just
325
- set). An emoji present in the previous list but absent from the incoming one means its count hit zero
326
- and it should simply not appear (matches the existing pattern at `:320`, `.filter((reaction) =>
327
- reaction.count !== 0)`).
328
-
329
- **Required tests (chunk 3, reaction part):**
330
- - A `reaction` frame for an emoji the viewer has NOT reacted with updates its count without touching
331
- the viewer's `reacted` state on any OTHER emoji on the same message (the WO's explicitly named "an
332
- arriving frame updates the rendered message for a viewer who did not cause it" case).
333
- - The viewer's own optimistically-set `reacted: true` on one emoji survives (a) the REST confirmation
334
- of that same toggle, and (b) an unrelated `reaction` frame for a different emoji on the same message
335
- arriving afterward.
336
- - A duplicate `event_id` changes nothing (verify the existing `seenEventsRef` de-dup in the provider's
337
- `subscribe(...)` handler already covers this at the reducer-dispatch level — this may already be
338
- correct and just need a test, not a code change; check before assuming otherwise).
339
-
340
- **Conformance-test extension (the WO's new rule, required, not optional):** add a check to
341
- `tests/messagingContractConformance.test.js` that reads `docs/messaging-deviations.md` and fails if any
342
- line containing `**BLOCKED**` does not also carry an explicit dependency-version pin (e.g. a regex for
343
- `dcm \d+\.\d+\.\d+` on that line or within its paragraph — check the actual final wording you land on
344
- in the doc rewrite below and match the regex to it, don't write the regex first and hope the prose
345
- matches). **Verified: this check does not currently exist, and would currently fail** — as of this
346
- WO's start, rows 27/38/42/51/52/53/58's `**BLOCKED**` lines in `docs/messaging-deviations.md` do NOT
347
- carry a version pin (only the doc's intro sentence and rows 56/57 happen to mention "dcm 2.36.1"); only
348
- rewriting the doc's BLOCKED rows to each name the version fixes this. **Required negative-case test**
349
- (the WO is explicit: "assert the negative case, not only the positive"): construct a small in-memory
350
- BLOCKED line missing a version pin and assert the check's own logic flags it — don't only test that the
351
- real file passes, prove the check can fail.
352
-
353
- **`docs/messaging-deviations.md` rewrite — the WO's central deliverable for this chunk:**
354
- - Rows 38, 51, 52, 53, 56, 57, 58 move from `BLOCKED` to their real delivered outcome (`OK`, matching
355
- the pattern MSG-3b's own doc used for delivered rows) — each entry should reflect what was actually
356
- built across chunks 1-3 of MSG-3c (last-message preview/reorder, poll rendering incl. the
357
- `created_by_id`/`voted_option_ids` fixes, the `reaction`/`poll_updated` frame-merge fixes), not a
358
- generic "delivered" note copy-pasted seven times.
359
- - Rows **27 and 42 stay BLOCKED** — do not touch their substance, only add the now-required version pin
360
- if missing (e.g. "as of dcm 2.37.0" or whatever phrasing matches the new conformance-test regex).
361
- These are real, still-open gaps (thread-receipt read state; `external_key` serialization) waiting on
362
- a future dcm `MSG-2d`.
363
- - The closing "Required backend follow-up" paragraph (currently lists rows 27/38/42/51-53/56-58 as all
364
- needing a future dcm WO) must be corrected to name only rows 27 and 42.
365
- - **Risk named in the WO, read before editing:** "correcting seven entries by hand while leaving two
366
- correct ones intact is exactly where an over-eager edit removes a real blocker" — do not touch rows
367
- 27/42's substantive claims, only add the version pin to them if the doc rewrite's new house style
368
- requires it project-wide.
369
-
370
- **Progress contract / preamble:** identical to prior chunks — `PLAN:`/`PROGRESS:`/`RESULT:` lines, no
371
- git operations, write-and-run-only-the-new-tests, leave the diff uncommitted for the orchestrator's
372
- review.
373
-
374
- **Mini-handover:** repo `C:\Users\biglmi\Documents\webapps\ui-core-micha`, branch `main`, WO
375
- `work-orders/MSG-3c.md` chunk 3 (Part B above, after chunk 2 is committed) — the WO's last content
376
- chunk. After this chunk's independent review and commit, run the WO-end `ui_reviewer` pass per the
377
- WO's changed instruction (cross-check BLOCKED rows against the dependency repo at its **current**
378
- version, not against the WO text), then one version bump + npm publish.
@@ -1,268 +0,0 @@
1
- # MSG-3d — the last two blocked rows: unread-reply marker and managed-conversation identity
2
-
3
- Status: planned · Tier 2 · Target repo: `ui-core-micha` (main)
4
- **Binding spec:** `django-core-micha/docs/design/messaging-platform.md` — §"Thread reply state and
5
- managed-conversation identity" and §Realtime's consumer obligation. Extends the landed MSG-3c
6
- (published ucm 2.18.0), same convention as `MSG-2b` / `MSG-3b` / `MSG-3c`.
7
-
8
- ---
9
-
10
- ## Part A — Envelope (Expertenchat, 2026-08-01)
11
-
12
- ### Goal
13
-
14
- Deliver checklist rows **27** and **42** — the last two entries still marked BLOCKED in
15
- `docs/messaging-deviations.md`. dcm 2.38.0 (MSG-2d) shipped the contract they were waiting for, on the
16
- same day MSG-3c closed. After this WO the deviation doc has **zero** BLOCKED entries.
17
-
18
- ### Verified deliverable against dcm 2.38.0
19
-
20
- Checked in `django-core-micha` before this WO was written (`serializers.py`):
21
-
22
- | Row | Was blocked on | Present in 2.38.0 |
23
- |---|---|---|
24
- | 27 | no readable thread reply state | `reply_count` + `last_reply_at` on `serialize_message` (viewer-independent), `thread_last_read_at` added REST-only |
25
- | 42 | `external_key` not serialized | `external_key` on `serialize_conversation_core` |
26
-
27
- ### Expected outcome
28
-
29
- **Row 27 — unread-reply marker on the thread toggle.** Render the marker when
30
- `last_reply_at > thread_last_read_at`; a `null` receipt with at least one reply counts as unread; no
31
- replies means no toggle and no marker. `reply_count` gives the toggle its count without expanding the
32
- thread. Opening a thread marks it read and updates the receipt **from the REST response** to
33
- `POST messages/{root_id}/thread/read/` — not from a frame, which cannot carry it.
34
-
35
- **Live root update on an incoming reply.** A new reply arrives as a `message` frame carrying *the
36
- reply's* projection — its own `reply_count` is 0 and `last_reply_at` null. Nothing in the frame updates
37
- the **root's** counters, so without explicit handling the marker only appears after the next refetch.
38
- jg updates the root live (`Thread.jsx:1488-1537`: increments `reply_count` and marks the thread unread
39
- when the sender is someone else), so parity requires the same here: on a `message` frame whose
40
- `reply_to_id` names a cached root, increment that root's `reply_count` and advance its `last_reply_at`.
41
- This was a gap in the first draft of this envelope, surfaced by review — do not leave it to discovery.
42
-
43
- **Row 42 — distinguish managed conversations.** Surface `external_key` so a host can tell one managed
44
- conversation from another. **ucm must not know what the values mean.** `event_all` and `event_team` are
45
- jg's vocabulary, not the platform's; baking them in would put app-specific code in a consumer-agnostic
46
- package and break the first non-jg adopter. Expose the value and let the host supply label and icon,
47
- mirroring the existing `groupLaunchers` host-supplied pattern.
48
-
49
- **Deviation doc.** Rows 27 and 42 move from BLOCKED to delivered. The doc then contains no BLOCKED
50
- entries at all — confirm MSG-3c's version-pin conformance check still passes on an empty set rather
51
- than erroring or vacuously "passing" for the wrong reason.
52
-
53
- ### The trap this WO walks into — read before writing any merge code
54
-
55
- `thread_last_read_at` is **viewer-specific and REST-only**. `reply_count` and `last_reply_at` ride
56
- `serialize_message`, which is embedded verbatim into the `message` and `message_edited` frames. So every
57
- such frame carries a message projection **with** the reply fields and **without** the receipt.
58
-
59
- That is precisely the defect shape MSG-3c just fixed three times (`poll_updated`'s `voted_option_ids`,
60
- the `reaction` frame's `reacted` flags, `toggleReaction`'s REST confirm): a viewer-independent
61
- projection replacing a cached object and silently wiping viewer-specific state. This WO introduces a
62
- **fourth field of that shape**, arriving immediately after those fixes. Merge field-wise, preserve
63
- `thread_last_read_at`, never replace the message object wholesale — the contract obligation is now
64
- written into design §Realtime. It has a mandatory test below.
65
-
66
- ### Non-goals / do-not-touch
67
-
68
- Any dcm change. Any app-specific vocabulary in ucm (see row 42). App-side code, the jg migration
69
- (MSG-5), search, typing, client→server WS. The component boundaries from MSG-3 — the marker belongs to
70
- whichever component owns the thread toggle, not to a regrown `Thread`; the ~400 LOC soft trigger and
71
- one-harness-entry-per-component rule stay in force. Do not re-open rows already delivered or already
72
- recorded as deliberate deviations.
73
-
74
- ### Required tests to WRITE
75
-
76
- - **The merge trap (load-bearing):** a `message` and a `message_edited` frame — **server-shaped, copied
77
- from real `serialize_message` output**, therefore without `thread_last_read_at` — must leave a
78
- previously-fetched receipt intact. Assert the cached value after the merge, not just that no error
79
- was thrown.
80
- - **Marker logic:** shown when `last_reply_at > thread_last_read_at`; hidden when the receipt is equal
81
- or newer; shown when the receipt is `null` and `reply_count > 0`; absent entirely when
82
- `reply_count == 0`.
83
- - **Read on open:** expanding a thread issues the REST read call and updates the receipt from its
84
- response.
85
- - **Live root update:** a server-shaped `message` frame carrying a `reply_to_id` for a cached root
86
- raises that root's `reply_count` and advances `last_reply_at`, and the marker appears without a
87
- refetch when the sender is not the viewer.
88
- - **Row 42:** two managed conversations with different `external_key` values are distinguishable through
89
- whatever the host supplies, and **no jg-specific string appears anywhere in `src/`** — assert that
90
- directly, it is the constraint most likely to be quietly violated.
91
- - **Deviation doc / conformance:** zero BLOCKED entries and the version-pin check still meaningful.
92
- - Existing ucm suites stay green.
93
-
94
- **Fixture rule, non-negotiable:** every payload in these tests is copied from actual dcm serializer
95
- output. Two separate defects in this workstream (`reply_to` vs `reply_to_id`, `last_message.body` vs
96
- `.excerpt`) survived only because hand-written client-shaped fixtures agreed with the client's mistake.
97
-
98
- ### Risks
99
-
100
- - **The merge trap**, above — the single most likely way this WO ships a regression, and the reason its
101
- first test is mandatory rather than nice-to-have.
102
- - **jg vocabulary leaking into ucm** on row 42; it has its own assertion.
103
- - Small WO, low complexity — the risk is complacency, not difficulty.
104
-
105
- ### Reviews
106
-
107
- **Tier 2, not Tier 1 — corrected on review.** The row count is small but the change lands in
108
- `MessagingProvider`'s frame-merge code, which is exactly where MSG-3c's Tier-2 pass found three shipped
109
- bugs, and this WO's own risk section calls its merge trap the most likely way it ships a regression.
110
- That is review-sensitive work by AGENTS.md's definition; size does not lower the tier.
111
-
112
- Independent `reviewer` plus **`ui_reviewer`** at WO end: the marker is a visual
113
- affordance and the harness exists to look at it. The `ui_reviewer` must also confirm the deviation doc
114
- reaches zero BLOCKED entries honestly — by delivery, not by deletion.
115
-
116
- ### Preconditions
117
-
118
- dcm 2.38.0 published (met — verified in source, not assumed). Approval Gate #1 = operator go on this
119
- envelope.
120
-
121
- ### Release
122
-
123
- One version bump + npm publish at WO end. **This closes the post-audit remediation**: with rows 27 and
124
- 42 delivered, every row of the MSG-3b checklist is either implemented or a recorded deliberate
125
- deviation, and MSG-5 becomes unblocked.
126
-
127
- ### Execution directive
128
-
129
- Implement through `codex exec` in the background — invoked **directly via Bash** (never the
130
- `debugger`/`*_coder` Agent wrappers) with **both** flags `--skip-git-repo-check` and
131
- `--dangerously-bypass-approvals-and-sandbox`, prompt passed as a positional argument from a file;
132
- fall back to direct Claude implementation only on Codex quota / rate-limit / non-zero exit. Codex has
133
- been out of workspace credits recently — verify before assuming the fallback, and if the fallback is
134
- used, the orchestrator becomes author and the independent review is mandatory, not routine.
135
-
136
- ### Mini-handover (pastable)
137
-
138
- Orchestrator: implement `work-orders/MSG-3d.md` in `ui-core-micha` (main). `git pull` first, read the WO
139
- + `django-core-micha/docs/design/messaging-platform.md` (§"Thread reply state and managed-conversation
140
- identity", §Realtime's consumer obligation), then follow `orchestrate-codex` (Codex-first, own
141
- independent review, `ui_reviewer` at WO end, one publish at WO end).
142
-
143
- ---
144
-
145
- ## Part B — Implementation map (Orchestrator)
146
-
147
- **Single chunk** (the envelope's own framing: small WO, low complexity — Tier 2 for review rigor, not
148
- for decomposition). **Target repo working directory:** `C:\Users\biglmi\Documents\webapps\ui-core-micha`
149
- (repo root).
150
-
151
- **Verified against dcm 2.38.0 while scoping (do not re-derive):**
152
- - `serialize_message` (`django-core-micha/src/django_core_micha/messaging/serializers.py:80-95`) ALWAYS
153
- includes `reply_count`/`last_reply_at` — annotated-queryset fast path when present
154
- (`_with_reply_count`, `views.py:39-47`, used by `ConversationMessagesView`/`ThreadView`), a live
155
- aggregate fallback otherwise (`_reply_stats`, `serializers.py:68-77`). Every `message`/`message_edited`
156
- frame (built from `serialize_message` via `realtime.py`'s `publish_messaging_event`) therefore always
157
- carries fresh, viewer-independent reply stats for that specific message.
158
- - `thread_last_read_at` is added **only** by `_message_response`/`_message_page_response`
159
- (`views.py:50-74`) — REST-only, viewer-specific, from `MessageThreadReceipt`. `_with_reply_count`'s
160
- list responses (`GET conversations/{id}/messages/`, `GET messages/{root_id}/thread/`) already attach
161
- it per-row via one bulk query (not N+1) — so a normal `listMessages`/`listThread` load already gives
162
- roots both their reply stats AND the viewer's own receipt in one response; no extra call needed on
163
- mount.
164
- - `POST messages/{root_id}/thread/read/` (`views.py:342-347`, already wired as `markThreadRead` in
165
- `api.js:41`) returns `{"last_read_at": ...}` — **note the field name differs from the cached message's
166
- own `thread_last_read_at` key** — the provider action must map `last_read_at` from this response onto
167
- the root message's `thread_last_read_at` cache field, they are not the same key name.
168
- - `serialize_conversation_core` (`serializers.py:107-115` area) now includes `external_key` (`null`
169
- where the kind doesn't use one).
170
-
171
- **The merge trap — investigated, resolved by existing infrastructure, still needs its mandatory test:**
172
- `mergeById`/`reconcileMessage` (`MessagingProvider.jsx:45-79`) already do a **field-wise** merge
173
- (`{...previous, ...incoming}`), and neither the `message` nor `message_edited` `applyFrame` branches
174
- explicitly re-derive or strip `thread_last_read_at` the way the `reaction`/`poll_updated` branches used
175
- to explicitly recompute their own fields — so an incoming frame lacking `thread_last_read_at` (it always
176
- will) does NOT overwrite a previously-cached value through the current code path. This is a genuine
177
- finding from investigation, not an assumption: **do not skip the mandatory merge-trap test on the
178
- strength of this finding** — the WO requires it regardless, and any code added by this WO (the live
179
- root-update below in particular) must preserve this property rather than accidentally break it by
180
- constructing a replacement object that omits the key.
181
-
182
- **Row 27 implementation:**
183
- 1. `MessagingProvider.jsx`'s `markReplyThreadRead` (currently `:262`,
184
- `const markReplyThreadRead = useCallback((rootId, readAt) => api.markThreadRead(rootId, readAt),
185
- [api]);`) fires the REST call and **discards the response** — nothing ever updates the cache, so the
186
- receipt is never actually stored client-side today. Fix: on success, patch the root message's
187
- `thread_last_read_at` from the response's `last_read_at` (field-name mapping per above).
188
- 2. A marker component/affordance on the thread-toggle button (`Thread.jsx`'s existing
189
- `toggleReplies`/reply-count button, around where `message.reply_count` is already read at `:93-96`
190
- for the reply-count label) — show it when `message.last_reply_at > message.thread_last_read_at`, OR
191
- when `message.thread_last_read_at` is `null`/absent and `message.reply_count > 0`. No marker at all
192
- when `reply_count === 0` (matches the existing "no toggle button at all when there are no replies"
193
- behavior — don't add a marker to a control that doesn't render). This belongs in `Thread.jsx` (or a
194
- small collaborator it owns) — not a new top-level component, per the non-goals' "marker belongs to
195
- whichever component owns the thread toggle."
196
- 3. **Live root update on an incoming reply** — in `applyFrame`'s `message` case
197
- (`MessagingProvider.jsx:126-143`): after the existing reply message itself is merged via
198
- `reconcileMessage`, additionally check `message.reply_to_id` (server field name — reuse the pattern
199
- already established, this is NOT the client-shaped `reply_to` used only by locally-built optimistic
200
- rows) against `state.messages` for a cached root. If found, bump that root's `reply_count` by one and
201
- set `last_reply_at` to the incoming message's `created_at`, via a **field-wise merge that explicitly
202
- preserves the root's existing `thread_last_read_at`** (the exact discipline the merge-trap section
203
- above is about — do this by spreading the previous root object and only overriding
204
- `reply_count`/`last_reply_at`, never by constructing a fresh object from the frame's data). Apply this
205
- unconditionally (regardless of who sent the reply) — `reply_count`/`last_reply_at` are
206
- viewer-independent facts; do not add sender-based suppression logic that isn't explicitly required
207
- (the required tests only ask for the "sender is not the viewer" case, the common path — don't invent
208
- untested self-send special-casing beyond that).
209
-
210
- **Row 42 implementation:** `external_key` needs no new plumbing to reach the cache — `mergeById`/REST
211
- responses pass every field through transparently already. What's missing is a way for a host to render
212
- it. Add an optional host-supplied resolver prop to `ConversationList.jsx` (mirrors the existing
213
- `groupLaunchers`/`broadcastLauncher` host-supplied-data pattern already in this file) — e.g.
214
- `resolveManagedLabel(conversation)` — called instead of the existing `titleOf()` fallback specifically
215
- when `conversation.kind === 'managed'` and the prop is supplied; falls back to the existing `titleOf()`
216
- behavior otherwise (unset prop, or non-managed kind). **ucm must not hardcode or reference `event_all`/
217
- `event_team` or any other jg-specific vocabulary anywhere in `src/`** — the resolver receives the raw
218
- `conversation` object (including `external_key`) and the host owns all interpretation.
219
-
220
- **Deviation doc + conformance test:** move rows 27 and 42 from `BLOCKED` to their delivered outcome in
221
- `docs/messaging-deviations.md` (describe what was actually built, not a generic note, matching the
222
- established style from MSG-3c's rewrite). This leaves **zero** `BLOCKED` entries — verify
223
- `tests/messagingContractConformance.test.js`'s version-pin check (added in MSG-3c) still passes
224
- meaningfully against an empty set (i.e. `unpinnedBlockedLines()` naturally returns `[]` when there are no
225
- `**BLOCKED**` lines at all — confirm this is actually true of the regex-based approach, not just assumed
226
- because the test happens to pass; the WO's own text calls out the "passing vacuously for the wrong
227
- reason" risk explicitly).
228
-
229
- **Required tests to WRITE (all named explicitly in the WO's own "Required tests" section — treat that
230
- section as the checklist, not this paraphrase):**
231
- - The merge-trap test (load-bearing, per the WO: server-shaped `message` and `message_edited` frames,
232
- copied from real `serialize_message` shape, must leave a previously-fetched
233
- `thread_last_read_at` intact).
234
- - Marker visibility logic (shown/hidden/null-receipt/no-replies cases, all four named in the WO).
235
- - Read-on-open (expanding a thread issues the REST call and updates the receipt from its response).
236
- - Live root update (server-shaped `message` frame with `reply_to_id` raises the cached root's
237
- `reply_count`/`last_reply_at`; marker appears without a refetch for the "sender is not the viewer"
238
- case).
239
- - Row 42: two managed conversations with different `external_key` render distinguishable labels via a
240
- host resolver; **a direct assertion that no jg-specific string (`event_all`, `event_team`) appears
241
- anywhere in `src/`** — grep-based, mirroring the style of the existing contract-conformance test.
242
- - Deviation-doc/conformance: zero `BLOCKED` entries; the version-pin check's own logic still behaves
243
- correctly against that empty set (not just "the real file happens to pass").
244
- - Existing ucm suites stay green.
245
-
246
- **Fixture rule (WO's own words, binding):** every payload in these tests is copied from actual dcm
247
- serializer output — this workstream has twice shipped a bug that survived only because a hand-written
248
- client-shaped fixture agreed with the client's own mistake (`reply_to` vs `reply_to_id`,
249
- `last_message.body` vs `.excerpt`). Do not hand-invent a payload shape without checking it against the
250
- serializer citations above first.
251
-
252
- **Invariants / do-not-break:** don't touch chunks/rows already delivered; keep the ~400 LOC soft trigger
253
- in mind for `Thread.jsx`/`ConversationList.jsx`/`MessagingProvider.jsx`; no new top-level component for
254
- the marker (lives in `Thread.jsx` or a small collaborator it owns); no new DX-1 harness entry needed
255
- unless a genuinely new independently-mountable component is added (it shouldn't be, per the above).
256
-
257
- **Progress contract / preamble:** `PLAN:`/`PROGRESS:`/`RESULT:` lines, no git operations, write-and-run-
258
- only-the-new-tests, leave the diff uncommitted for the orchestrator's review. Codex has been out of
259
- workspace credits recently across the last several WOs in this workstream — verify liveness before
260
- assuming success; if Codex fails (credits, quota, non-zero exit), the orchestrator implements directly
261
- per the fallback rule, which makes the orchestrator the author and independent review mandatory
262
- (not routine) — this has been true for every chunk of MSG-3c and should be expected here too.
263
-
264
- **Mini-handover:** repo `C:\Users\biglmi\Documents\webapps\ui-core-micha`, branch `main`, WO
265
- `work-orders/MSG-3d.md` (Part B above, single chunk). Follow `orchestrate-codex`. After the independent
266
- review and commit, run the WO-end `ui_reviewer` pass (per the WO's "Reviews" section — confirm the
267
- deviation doc reaches zero BLOCKED entries honestly, by delivery not deletion, and look at the marker
268
- as a rendered visual affordance via the DX-1 harness), then one version bump + npm publish.